Мой первый open-source продукт
pip install nonaВ реальных наборах данных пропущенные значения создают проблему для дальнейшей обработки. Большую ценность имеет подстановка или заполнение отсутствующих значений. К сожалению, стандартные «ленивые» методы, такие как простое использование медианы столбца или среднего значения, не всегда работают должным образом.
В 2021-ом году ко мне пришла идея создания алгоритма на основе методов машинного обучения с прогнозированием по каждому столбцу с пропусками. Данную идею я воплотил сначала схематично на бумаге.

Суть алгоритма заключается в заполнении пропусков различными методами машинного обучения. Циклом проходим по всем столбцам, если столбец с пропущенными значениями — останавливаемся и делаем этот столбец — target. Предыдущие столбцы делим на X_train, X_test. X_test будет соответствовать пропущенным значениям. Значения Y_train берем из столбца (на котором остановились в цикле) в которых нет пропусков. Обучаем X_train и y_train на выбранном нами методе, например гребневой регрессии (Ridge regression). Делаем предсказания на X_test, заполняем пропущенные значения в столбце предсказанными значениями. Так делаем по всем столбцам с пропусками.
Данный алгоритм я регулярно использовал на практике для заполнения пропущенных значений.
В 2023-м году решил на его основе написать библиотеку для Python и провести сравнение с иными методами заполнения пропусков.
Преимущества метода:
- Простое и быстрое заполнение пропущенных значений. 
- Кастомизация используемых методов машинного обучения. 
- Высокая точность прогнозирования. 
Установка
Исходный код в настоящее время размещен на GitHub по адресу: GitHub — AbdualimovTP/nona: библиотека для заполнения пропущенных значений с использованием методов искусственного интеллекта
Двоичные установщики для последней выпущенной версии доступны в каталоге пакетов Python (PyPI)
# PyPI
pip install nonaЗависимости
Быстрый старт
" Из коробки" используем гребневую регрессию, чтобы заполнить пробелы в столбцах где необходимо решить задачу регрессии, и RandomForestClassifier для задачи классификации.
# Загружаем библиотеку
from nona.nona import nona
# Подготовьте ваш датасет, только численные значения в датасете
# Заполняем пропущенные значения
nona(YOUR_DATA)Повышение точности алгоритма
Вы можете вставить в функцию иные методы машинного обучения. Они должны поддерживать простую реализацию fit и predict.
Параметры алгоритма:
- data: подготовленный набор данных 
- algreg: Алгоритм регрессии для прогнозирования отсутствующих значений в столбцах 
- algclass: Алгоритм классификации для прогнозирования пропущенных значений в столбцах 
# Загружаем библиотеку
from nona.nona import nona
# Подготовьте ваш датасет, только численные значения в датасете
# Заполняем пропущенные значения
nona(data=YOUR_DATA, algreg=make_pipeline(StandardScaler(with_mean=False), Ridge(alpha=0.1)), algclass=RandomForestClassifier(max_depth=2, random_state=0))Сравнение с иными методами заполнения пропусков
Сравниваемые методы:
Baseline - заполнение средним по столбцу.
KNN - заполнение пропущенных значений с использованием k-ближайших соседей. Отсутствующие значения каждой выборки заполняются с использованием среднего значения ближайших соседей n_neighbors, найденных в обучающем наборе.
MICE - Использование класса IterativeImputer sklearn, который моделирует каждую функцию с отсутствующими значениями как функцию других функций и использует эту оценку для заполнения. Он делает это в итерированном циклическом режиме: на каждом шаге столбец признаков обозначается как выход y, а другие столбцы признаков обрабатываются как входы X. Регрессор подходит для (X, y) для известного y. Затем регрессор используется для предсказания пропущенных значений y. Это делается для каждого признака итеративно, а затем повторяется для раундов заполнения max_iter. Возвращаются результаты финального раунда заполнения.
MissForest - алгоритм заполнения данных на основе машинного обучения, который работает на основе алгоритма Random Forest.
NoNA - мой алгоритм "постолбцового" заполнения пропусков при помощи различных методов машинного обучения.
Для работы я взял Framingham heart study dataset доступный на Kaggle.
На данном датасете мы будем моделировать пропуски в объёме 10%, 20%, 30%, 40%, 50%, 70%, 90% пропущенных значений. Заполнять их описанными методами (каждым по отдельности). И сравнивать результаты с истинными значениями. На выходе получим среднеквадратичную ошибку (RMSE).
for i in [0.1, 0.2, 0.3, 0.4, 0.5, 0.7, 0.9]:
    # we create a random matrix the size of a dataset, randomly fill it with gaps and zeros
    randomMatrixNA = np.random.choice([0,np.NaN], (data.shape[0], data.shape[1]), p=[1-i, i])
    # fill dataset with missing values
    dataWithNA = data + randomMatrixNA
    # create datasets with filled gaps
    
    # fill in the middle
    Baseline_1_mean = dataWithNA.fillna(dataWithNA.mean())
    print(f'Baseline_MEAN, {i*100}, RMSE:' , np.round(mean_squared_error(data, Baseline_1_mean, squared=False), 2))
    dataFrameRMSE.loc['Baseline_MEAN'][f'{int(i*100)}%'] = np.round(mean_squared_error(data, Baseline_1_mean, squared=False), 2)
    
    # KNN
    imputer = KNNImputer(n_neighbors=15)
    KNN = imputer.fit_transform(dataWithNA)
    dataFrameRMSE.loc['KNN'][f'{int(i*100)}%'] = np.round(mean_squared_error(data, KNN, squared=False), 2)
    print(f'KNN, {i*100}, RMSE:' , np.round(mean_squared_error(data, KNN, squared=False), 2))
    
    # MICE
    mice = IterativeImputer(max_iter=10, random_state=0)
    MICE = mice.fit_transform(dataWithNA)
    dataFrameRMSE.loc['MICE'][f'{int(i*100)}%'] = np.round(mean_squared_error(data, MICE, squared=False), 2)
    print(f'MICE, {i*100}, RMSE:' , np.round(mean_squared_error(data, MICE, squared=False), 2))
    
    # MISSFOREST
    missforest = MissForest(random_state=0, verbose=0)
    MISSFOREST = missforest.fit_transform(dataWithNA)
    dataFrameRMSE.loc['MISSFOREST'][f'{int(i*100)}%'] = np.round(mean_squared_error(data, MISSFOREST, squared=False), 2)
    print(f'MISSFOREST, {i*100}, RMSE:' , np.round(mean_squared_error(data, MISSFOREST, squared=False), 2))
    
    # nona_Base
    dataWithNA_NonaBase = dataWithNA.copy(deep=True)
    nona(dataWithNA_NonaBase)
    dataFrameRMSE.loc['NONA'][f'{int(i*100)}%'] = np.round(mean_squared_error(data, dataWithNA_NonaBase, squared=False), 2)
    print(f'NONA, {i*100}, RMSE:' , np.round(mean_squared_error(data, dataWithNA_NonaBase, squared=False), 2))Результаты

| 10% | 20% | 30% | 40% | 50% | 70% | 90% | |
| Baseline - MEAN | 2.67 | 3.8 | 4.7 | 5.66 | 6.4 | 7.4 | 8.43 | 
| KNN | 2.48 | 3.7 | 4.57 | 5.55 | 6.35 | 7.47 | 8.49 | 
| MICE | 2.12 | 3.17 | 4.59 | 5.41 | 5.94 | 7.33 | 8.61 | 
| MISSFOREST | 2.26 | 3.36 | 4.31 | 5.33 | 6.15 | 8.06 | 9.85 | 
| NONA | 2.24 | 3.35 | 4.28 | 5.16 | 5.83 | 7.12 | 8.43 | 
Неплохие результаты для работы алгоритма "из коробки".
На 30%, 40%, 50%, 70%, 90% алгоритм NONA показал лучшую RMSE на данном датасете со смоделированными пропусками. На 10%, 20% второе место, первое за MICE.
В дальнейшем планирую проверить точность заполнения на иных датасетах. Также вижу возможности для улучшения качества прогнозирования. Планирую реализовать в следующих версиях библиотеки.
Комментарии (15)
 - Refridgerator10.01.2023 07:33+1- Решал на работе похожую задачу связанную с отслеживанием вагонов. Естественно, без машинного обучения — это смерть человека можно списать на неизлечимую и неизвестную науке болезнь, а на производстве такое не прокатит. Да и неоткуда было взяться статистике для биг дата. 
 В математике задача восстановления пропущенных данных — это задача интерполяции, для которой существует множество уже готовых, детерминированных и предсказуемых решений. Одна только проблема — во всё это надо вникать, а вникать в математику не всем интересно, а врачам наверно и тем более.
 - Discivery10.01.2023 08:04- На мой взгляд, заполнение пропусков в данных - вредная идея. Это может быть полезно только в случае малых выборок, да и то при условия простых структур данных.  - koresh_builder10.01.2023 08:38+1- Вредная или полезная- вопрос применения. Для ориентировочных прикидок, когда учитывается риск неполных исходных данных, имхо, вполне полезная. Или когда непринятие решения, к примеру, дороже, чем принятие неверного.  - Discivery10.01.2023 10:34+1- У меня сложилось впечатление, что на мой комментарий ответил ИИ - набор абсолютно пустых бессмысленных фраз ) 
 
 
 - dvbondarev10.01.2023 08:05- Проше пардону, но как можно сочинить то, чего нет, и выдавать это за реальные данные? Не есть ли это случай так называемого подгона под красивый ответ?  - economist7510.01.2023 17:12+1- Сочинять аналитикам приходится постоянно. Реальные данные всегда настолько большие, что: - пустоты в них есть всегда, стоит лишь начать искать. Выбрасывать строки с пустотами нельзя: часто это важные строки, перед или после которых что-то существенное стряслось. Пример: "Сначала вышел из строя спидометр, а потом они врезались в столб и все погибли". 
- заполненные пустоты выглядят "просто красиво", а заполненные ML-методами - еще и правдоподобны. Статья описывает библиотеку, имеющую все шансы "досочинить" пустоты полагаясь на другие данные. В примере со спидометром: если есть данные тахометра, микрофона, датчика вибрации итд - данные спидометра можно сочинить в верном направлении и даже близко к истине 
- сами данные никому не показывают, а ML-модель, обученная на частично ML-досочиненных данных - выглядит точно также непонятно, как "черный ящик" 
- раз истинных данных все равно не существует - можно сочинять смело. 
- к красивому ответу часто аналитика толкают силком. Вот где настоящая беда. Там и пустоты не помешают. 
 
 
 - Refridgerator10.01.2023 11:08- Вот у вас в сравнении упоминается KNN - заполнение пропущенных значений с использованием k-ближайших соседей. И сразу вопросы: а данные-то для этого уже отсортированы? Если да, то по какому критерию и почему он признан наилучшим? А значение k взято с потолка или есть обоснование? А веса где — по умолчанию, все единицы? Так это наихудший случай. А метод наименьших квадратов где, почему среди всех линейных методов вы взяли наименее подходящий? 
 
           
 

OBIEESupport
Как любопытно! Интересно, а среднее по больнице, если не известны этимологии болезней, ваша система сгенерить сможет? Допустим, медсестры забыли поставить градусники, а в корпусе лежит 500 неходячих больных, у которых температура - один из показателей качества лечения? А без температуры больных робот-разносильщик лекарств не дает разнести лекарства.
vassabi
при том что
(плачет)
я думаю что сможет сгенерить.
главное только чтобы потом не сделали стрелочником автора библиотеки :)
OBIEESupport
Если бы вы только знали, насколько физика каждого реального процесса тонка, когда в нее встраивают KPI! Как программисты начинают разворачивать циклы, делать CASE по UNICODE? Больница - это еще простой пример. Есть интереснее. Допустим: забыли у вагонов проверить пары при выведении их на главный ход Транссиба. Через сколько времени есть вероятность схода вагона, если допустимая скорость на 1000 км трассы не превышает 25 км в час, а предельный допуск расхождения пути на холоде превышен в два раза?
vassabi
японская бензопила и сибирские лесорубы? https://4tob.ru/anekdots/2457 :)
OBIEESupport
Это как раз корректный пример экспериментального определения уровня вязкости инструментальной стали в условиях избытка пиломатериалов и достаточного количества времени на починку.
economist75
Сможет, опираясь на предшествующую динамику измерений. Но если у Васи Пупкина ни разу не измерили температуру (весь ряд данных - <NA>) - то ML, не имея точки >U< (36.7), по идее, не должен ничего сочинять.
В том и сила ML (и вообще всей статистики), что они не просто могут что-то утверждать, но и всегда говорят о степени своей уверенности, вероятности, доверительном интервале итд. Другое дело что в эти метрики редко заглядывают, но в случае с пациентами - шансы выше.