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

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

Давайте рассмотрим пять задач, с которыми можно столкнуться в рамках очистки данных. Это не исчерпывающий список, но он послужит хорошим ориентиром при работе с реальными датасетами.

Все примеры мы будем рассматривать на Ames Housing Dataset, который содержит информацию о продажах жилой недвижимости в городе Эймс, штат Айова, США

Удаление дубликатов

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

Примечание: когда мы говорим о дубликатах в наборах данных, мы имеем в виду дублирование целых строк, каждая из которых представляет собой отдельное наблюдение. А в столбце могут быть повторяющиеся значения, это нормально. Мы говорим именно о полном дублировании наблюдений, которое является ошибкой.

К счастью, в библиотеке pandas есть метод, который помогает обнаружить дубликаты:

duplicate_rows = df[df.duplicated()]
duplicate_rows

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

В датасете Ames Housing дубликатов нет. Но если вы хотите попрактиковаться в работе с ними, взгляните на набор данных CITES Wildlife Trade Database и попробуйте найти там дубликаты при помощи приведённого выше метода.

После того как мы обнаружили дубликаты, их необходимо удалить, чтобы избежать искажения результатов. Код ниже удаляет дубликаты, сбрасывает индекс датафрейма и сохраняет результат в переменной df_cleaned:

df_cleaned = df.drop_duplicates()
df_cleaned.reset_index(drop=True, inplace=True)
df_cleaned

В pandas есть и другие функции для работы с дублями, но приведенного выше кода достаточно для самых необходимых базовых действий.

Работа с некорректными значениями

Некорректные значения могут возникать из-за ошибок при вводе данных или сбоев в процессе их сбора. Например, в датасете Ames Housing нереалистичными можно считать отрицательные значения цены продажи (SalePrice) или числовое значение для категориального признака, такого как стиль крыши (Roof Style).

Обнаружение нереалистичных значений требует комплексного подхода, который включает:

  • анализ сводных статистик,

  • определение допустимых границ значений при их сборе,

  • выявление данных, выходящих за пределы этих границ,

  • использование визуализаций для обнаружения аномалий и подозрительных паттернов.

Нереалистичные значения являются источником шума и могут создавать проблемы при анализе, поэтому их необходимо обрабатывать. Однако способ их обработки зависит от контекста. Если таких значений немного по сравнению с общим объемом данных, то можно просто удалить такие записи. Например, если мы обнаружили нереалистичное значение в строке 214, то можно воспользоваться методом drop из pandas для её удаления. Код ниже удалит это наблюдение из датафрейма, сбросит индекс и отобразит результат:

df_cleaned = df_cleaned.drop(index=214)
df_cleaned.reset_index(drop=True, inplace=True)
df_cleaned

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

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

Итого: выбор способа обработки нереалистичных значений зависит от их количества в наборе данных, специфики сбора данных, а также других факторов, таких как наша экспертиза в предметной области.

Форматирование данных

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

При такой проблеме нужно привести все значения к какому-то выбранному стандарту. Например, для округления можно использовать метод .round(). Вот так можно округлить значения в столбце SalePrice до двух знаков после точки:

df_cleaned['SalePrice'] = df_cleaned['SalePrice].round(2)
df_cleaned.head()

Другой пример: если в столбце HouseStyle у нас есть вариативная запись, такая как "1Story" и "OneStory", и мы уверены, что они означают одно и то же, можно использовать следующий код для исправления ситуации:

df_cleaned[HouseStyle'] = df_cleaned['HouseStyle'].replace('OneStory', '1Story')

Стандартизация формата записи значений помогает избежать путаницы и упрощает дальнейший анализ.

Работа с выбросами

Выбросы (outliers) — это распространенное явление в наборах данных, однако то, как мы их будем обрабатывать (и будем ли вообще), сильно зависит от контекста. Один из самых простых способов обнаружить выбросы — использовать диаграмму "ящик с усами" (box plot), которую можно построить при помощи библиотек seaborn или matplotlib.

Для примера давайте построим ящик с усами для столбца SalePrice из датасета Ames Housing:

import seaborn as sns
import matplotlib.pyplot as plt

# Создаем диаграмму "ящик с усами" для SalePrice
plt.figure(figsize=(10, 6))
sns.boxplot(x=df_cleaned['SalePrice'])
plt.title('Box Plot of SalePrice')
plt.xlabel('SalePrice')
plt.show()

Диаграмма наглядно демонстрирует, что распределение характеризуется правосторонней асимметрией, так как вертикальная линия медианы внутри «ящика» смещена влево от центра. Это говорит о том, что большинство домов имеют более низкую цену, что вполне ожидаемо. На диаграмме также заметно множество выбросов справа — небольшое количество домов, стоимость которых значительно превышает медианную.

С этими выбросами вполне можно ничего не делать, так как естественно, что небольшая часть домов будет стоить значительно дороже, чем остальные. Но конечное решение всегда зависит от цели анализа. Например, если цель нашей работы состоит в анализе домов для такой аудитории, которая заведомо не может себе позволить себе дорогие особняки, то такие выбросы можно удалить.

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

Вообще обработка выбросов обычно осуществляется одним из двух способов:

  1. Удаление выбросов.

  2. Использование статистик, менее чувствительных к выбросам.

Чтобы удалить выбросы, необходимо точно определить, какие строки к ним относятся. Мы только что посмотрели на графики, но существует множество методов, позволяющих расчётно определить значения выбросов. Одним из таких методов является использование модифицированного Z-score.

Обычный Z-score определяется как:

Z-score = \frac{X - \mu}{\sigma}

где:

  • X — значение признака,

  • μ — среднее арифметическое признака,

  • σ — стандартное отклонение признака.

Однако и среднее значение, и стандартное отклонение чувствительны к выбросам. Поэтому будем использовать модифицированный Z-score:

\text{Modified Z-score} = \frac{X - \text{median}}{\text{MAD}}

где:

  • X — значение признака,

  • median — медиана признака,

  • MAD — медианное абсолютное отклонение признака.

Например, если наш набор данных содержит следующие значения:

1, 2, 2, 2, 3, 3, 3, 5, 9

то медиана будет равна 3. Тогда абсолютные отклонения от медианы будут такими:

2, 1, 1, 1, 0, 0, 0, 2, 5

Сортируем их:

0, 0, 0, 1, 1, 1, 2, 2, 5

Таким образом медиана таких отклонений будет равна 1.

Теперь рассчитаем этот показатель для столбца SalePrice:

import pandas as pd
from scipy.stats import median_abs_deviatio

# Предполагаем, что 'data' — это наш дашатфрейм, и он содержит столбец 'SalePrice'e'
# Вычисляем медиану для столбца SalePrice
median_sale_price = data['SalePrice'].median()

# Вычисляем медианное абсолютное отклонение (MAD) для столбца SalePrice
mad_sale_price = median_abs_deviation(data['SalePrice'], scale='normal')

# Вычисляем модифицированный Z-score для столбца SalePrice
data['Modified_Z_Score'] = (data['SalePrice'] - median_sale_price) / mad_sale_price

# Выводим первые несколько строк с модифицированным Z-score
print(data[['SalePrice', 'Modified_Z_Score']].head())

После выполнения этого кода нам нужно решить, какие значения считать выбросами на основе Z-score. Обычно выбросом считают значение с Z-score >= 3 или <= -3, но это правило можно менять в зависимости от конкретного набора данных.

Теперь напишем код, который найдёт выбросы с учётом рассчитанного Z-score:

# Фильтруем строки, где модифицированный Z-score >= 3 или <= -3
outliers = data[(data['Modified_Z_Score'] >= 3) | (data['Modified_Z_Score'] <= -3)]

# Выводим отфильтрованные строки
outliers = outliers[['SalePrice', 'Modified_Z_Score']]
outliers

И удалим их:

data_without_outliers = data.drop(index=outliers.index)

# Выводим новый датафрейм без выбросов
print(data_without_outliers)

Теперь вновь построим ящик с усами уже после удаления выбросов:

import seaborn as sns
import matplotlib.pyplot as plt

# Строим ящик с усами для SalePrice
plt.figure(figsize=(10, 6))
sns.boxplot(x=data_without_outliers['SalePrice'])
plt.title('Box Plot of SalePrice')
plt.xlabel('SalePrice')
plt.show()

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

Посчитаем, какой процент данных мы удалили:

# Вычисляем количество наблюдений в исходном и отфильтрованном датафреймах
original_count = len(data)
filtered_count = len(data_without_outliers)

# Вычисляем количество удаленных строк
removed_count = original_count - filtered_count

# Вычисляем процент удаленных строк
percentage_removed = (removed_count / original_count) * 100

# Выводим процент
print(f"Процент удаленных наблюдений: {percentage_removed:.2f}%")

Получается, что мы убрали 5.67% данных.

Если же мы оставляем выбросы, то разумно использовать статистики, менее чувствительные к ним, такие как медиана и межквартильный размах (IQR), они помогут сделать более надёжные выводы.

Пропуски в данных

В данных, которые мы рассматриваем, пропуски точно есть:

Но нам важно определить, как именно распределены пропуски в данных. Возможны 3 варианта:

  1. Пропуски распределены полностью случайно (Missing Completely at Random, MCAR).

  2. Пропуски распределены случайно (Missing at Random, MAR).

  3. Пропуски распределены не случайно (Missing Not at Random, MNAR).

Пропуски распределены полностью случайно (MCAR)

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

Полностью случайные пропуски встречаются редко, но с ними проще всего работать. Если у нас относительно небольшое количество таких пропусков, то наиболее распространенный подход — удалить их. Это не должно повлиять на статистики набора данных и, следовательно, на выводы, которые мы сделаем.

Пропуски распределены случайно (MAR)

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

Давайте вновь обратимся к набору данных Ames Housing. Возможно, признак Lot Frontage чаще отсутствует для домов, проданных определёнными агентствами недвижимости. В этом случае пропуски могут быть связаны с особенностями практик сбора данных у этих агентств. То есть отсутствие данных по признаку Lot Frontage обусловлено способами обработки информации конкретными агентствами, а не самими значениями этого признака.

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

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

Пропуски распределены не случайно (MNAR)

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

Вернёмся к датасету Ames Housing и пропускам в Lot Frontage. Один из сценариев пропусков, распределенных не случайно — это когда продавцы намеренно не указывают размер участка, если считают его маленьким, так как это может снизить цену продажи дома. Если вероятность пропуска в Lot Frontage зависит от его размера (который мы не знаем), то меньшие размеры участков с меньшей вероятностью будут указаны.

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

Визуализация пропущенных данных

Когда данные отсутствуют, важно определить, есть ли в распределении пропусков какой-то паттерн. Если паттерн существует, это проблема, которую, скорее всего, придётся решить перед тем, как данные анализировать.

Одним из наиболее простых способов поиска таких закономерностей является построение тепловых карт (heatmap). Однако перед тем как приступить к коду, давайте исключим переменные, в которых отсутствуют пропуски:

# Определяем столбцы с пропусками
columns_with_missing = data.columns[data.isnull().any()]

# Создаем новый датафрейм только с теми столбцами, где есть пропускики
data_with_missingness = data[columns_with_missing]

# Выводим новый датафрейм
data_with_missingness

А теперь построим тепловую карту:

import seaborn as sns
import matplotlib.pyplot as plt

# Транспонируем датафрейм
transposed_data = data_with_missingness.T

# Создаем тепловую карту для визуализации пропусков
plt.figure(figsize=(12, 8))
sns.heatmap(transposed_data.isnull(), cbar=False, yticklabels=True)
plt.title('Тепловая карта пропущенных данных (транспонированная)')
plt.xlabel('Наблюдения')
plt.ylabel('Признаки')
plt.tight_layout()
plt.show()

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

В одной группе мы видим, что Bsmt Qual, Bsmt Cond, Bsmt Exposure, BsmtFin Type 1 и Bsmt Fin Type 2 отсутствуют в одних и тех же наблюдениях. В другой группе отсутствуют Garage Type, Garage Yr Bit, Garage Finish, Garage Qual и Garage Cond.

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

Также можно использовать библиотеку missingno, которая специально предназначена для удобных визуализаций пропусков.

Заключение

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

И вот несколько рекомендаций, которые сделают вашу работу лучше:

  • чётко формулируйте цель и задачи вашей работы. Это поможет принять верные решения при выборе альтернатив в способах подготовки данных.

  • Очень важно, чтобы все наши действия были воспроизводимыми.

  • Хорошо структурируйте свой Jupyter-ноутбук, делайте все действия строго последовательно и используйте Markdown для описания и обоснования принятых решений на каждом этапе работы.

  • Когда делайте любые преобразование, то делайте их не в исходном файле, либо базе данных.

?Если тебе интересны и другие полезные материалы по IT и Python, то можешь подписаться на мой канал PythonTalk или посетить сайт OlegTalks?

Комментарии (0)