Мой первый open-source продукт

GitHub — AbdualimovTP/nona: библиотека для заполнения пропущенных значений с использованием методов искусственного интеллекта 

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)


  1. OBIEESupport
    10.01.2023 00:44

    Как любопытно! Интересно, а среднее по больнице, если не известны этимологии болезней, ваша система сгенерить сможет? Допустим, медсестры забыли поставить градусники, а в корпусе лежит 500 неходячих больных, у которых температура - один из показателей качества лечения? А без температуры больных робот-разносильщик лекарств не дает разнести лекарства.


    1. vassabi
      10.01.2023 02:13
      +2

      Допустим, медсестры забыли поставить градусники

      при том что

      температура - один из показателей качества лечения

      (плачет)

      я думаю что сможет сгенерить.

      главное только чтобы потом не сделали стрелочником автора библиотеки :)


      1. OBIEESupport
        10.01.2023 02:34
        +1

        Если бы вы только знали, насколько физика каждого реального процесса тонка, когда в нее встраивают KPI! Как программисты начинают разворачивать циклы, делать CASE по UNICODE? Больница - это еще простой пример. Есть интереснее. Допустим: забыли у вагонов проверить пары при выведении их на главный ход Транссиба. Через сколько времени есть вероятность схода вагона, если допустимая скорость на 1000 км трассы не превышает 25 км в час, а предельный допуск расхождения пути на холоде превышен в два раза?


        1. vassabi
          10.01.2023 03:24
          +1

          японская бензопила и сибирские лесорубы? https://4tob.ru/anekdots/2457 :)


          1. OBIEESupport
            10.01.2023 14:36

            Это как раз корректный пример экспериментального определения уровня вязкости инструментальной стали в условиях избытка пиломатериалов и достаточного количества времени на починку.


    1. economist75
      11.01.2023 12:48

      Сможет, опираясь на предшествующую динамику измерений. Но если у Васи Пупкина ни разу не измерили температуру (весь ряд данных - <NA>) - то ML, не имея точки >U< (36.7), по идее, не должен ничего сочинять.

      В том и сила ML (и вообще всей статистики), что они не просто могут что-то утверждать, но и всегда говорят о степени своей уверенности, вероятности, доверительном интервале итд. Другое дело что в эти метрики редко заглядывают, но в случае с пациентами - шансы выше.


  1. Refridgerator
    10.01.2023 07:33
    +1

    Решал на работе похожую задачу связанную с отслеживанием вагонов. Естественно, без машинного обучения — это смерть человека можно списать на неизлечимую и неизвестную науке болезнь, а на производстве такое не прокатит. Да и неоткуда было взяться статистике для биг дата.

    В математике задача восстановления пропущенных данных — это задача интерполяции, для которой существует множество уже готовых, детерминированных и предсказуемых решений. Одна только проблема — во всё это надо вникать, а вникать в математику не всем интересно, а врачам наверно и тем более.


  1. Discivery
    10.01.2023 08:04

    На мой взгляд, заполнение пропусков в данных - вредная идея. Это может быть полезно только в случае малых выборок, да и то при условия простых структур данных.


    1. koresh_builder
      10.01.2023 08:38
      +1

      Вредная или полезная- вопрос применения. Для ориентировочных прикидок, когда учитывается риск неполных исходных данных, имхо, вполне полезная. Или когда непринятие решения, к примеру, дороже, чем принятие неверного.


      1. Discivery
        10.01.2023 10:34
        +1

        У меня сложилось впечатление, что на мой комментарий ответил ИИ - набор абсолютно пустых бессмысленных фраз )


  1. dvbondarev
    10.01.2023 08:05

    Проше пардону, но как можно сочинить то, чего нет, и выдавать это за реальные данные? Не есть ли это случай так называемого подгона под красивый ответ?


    1. economist75
      10.01.2023 17:12
      +1

      Сочинять аналитикам приходится постоянно. Реальные данные всегда настолько большие, что:

      • пустоты в них есть всегда, стоит лишь начать искать. Выбрасывать строки с пустотами нельзя: часто это важные строки, перед или после которых что-то существенное стряслось. Пример: "Сначала вышел из строя спидометр, а потом они врезались в столб и все погибли".

      • заполненные пустоты выглядят "просто красиво", а заполненные ML-методами - еще и правдоподобны. Статья описывает библиотеку, имеющую все шансы "досочинить" пустоты полагаясь на другие данные. В примере со спидометром: если есть данные тахометра, микрофона, датчика вибрации итд - данные спидометра можно сочинить в верном направлении и даже близко к истине

      • сами данные никому не показывают, а ML-модель, обученная на частично ML-досочиненных данных - выглядит точно также непонятно, как "черный ящик"

      • раз истинных данных все равно не существует - можно сочинять смело.

      • к красивому ответу часто аналитика толкают силком. Вот где настоящая беда. Там и пустоты не помешают.


  1. Refridgerator
    10.01.2023 11:08

    Вот у вас в сравнении упоминается KNN - заполнение пропущенных значений с использованием k-ближайших соседей. И сразу вопросы: а данные-то для этого уже отсортированы? Если да, то по какому критерию и почему он признан наилучшим? А значение k взято с потолка или есть обоснование? А веса где — по умолчанию, все единицы? Так это наихудший случай. А метод наименьших квадратов где, почему среди всех линейных методов вы взяли наименее подходящий?


  1. MyWave
    10.01.2023 15:13
    -1

    Привет.
    Но это уже нативно реализовано в sklearn:

    https://scikit-learn.org/stable/modules/generated/sklearn.impute.IterativeImputer.html#sklearn.impute.IterativeImputer


    1. CrazyElf
      10.01.2023 18:12
      +1

      Вы статью то читали? В ней и этот метод тоже сравнивается с предлагаемым.