<jupyter_start><jupyter_text>Python для анализа данных Как обработать датасет: стратегии работы с пропущенными, неинформативными и несогласованными данными. Основы работы с сайтом соревнований по машинному обучению kaggle.*<NAME>, НИУ ВШЭ* Когда мы работали с датасетом о пассажирах Титаника, мы уже встретились с пропущенными значениями в некоторых колонках. Теперь мы попробуем поамотреть на похожую ситуацию, но уже с другими данными. Сначала дадим "Определение".Очистка данных – это процесс обнаружения и исправления (или удаления) поврежденных или неточных записей из набора записей, таблицы или базы данных. Процесс включает в себя выявление неполных, неправильных, неточных или несущественных данных, а затем замену, изменение или удаление «загрязненных» данных.Звучит так себе. По порядку пройдемся по кусочкам этого определения и разберем методы поиска и исправления:* отсутствующих данных;* нетипичных данных – выбросов;* неинформативных данных – дубликатов;* несогласованных данных – одних и тех же данных, представленных в разных регистрах или форматах.<jupyter_code># импорт пакетов
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
import matplotlib
plt.style.use('ggplot')
from matplotlib.pyplot import figure
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = (24,16)
v = None<jupyter_output><empty_output><jupyter_text>Для начала посмотрим , где лежат наши данные (заодно и познакомимся с kaggle):https://www.kaggle.com/c/sberbank-russian-housing-market/overview/description Это данные о стоимости жилья.<jupyter_code># чтение данных
df = pd.read_csv('train.csv')
# shape and data types of the data
print(df.shape)
# print(df.dtypes)
# отбор числовых колонок
df_numeric = df.select_dtypes(include=[np.number]) # трюк отбора числовых колонок
numeric_cols = df_numeric.columns.values
# print(numeric_cols)
# отбор нечисловых колонок
df_non_numeric = df.select_dtypes(exclude=[np.number])
non_numeric_cols = df_non_numeric.columns.values
# print(non_numeric_cols)
df_numeric.columns.values<jupyter_output><empty_output><jupyter_text>Набор данных состоит из 30471 строки и 292 столбцов. Отделим яисловые и нечисловые колонки.Теперь мы можем просмотреть, что же тут такого «грязного» и как это очистить.  Отсутствующие данныеРабота с отсутствующими значениями – одна из самых сложных, но и самых распространенных проблем очистки. Многие ML-модели не предполагают пропусков.  Метод 1 - Тепловая карта Приведенная карта построена для первых 30 признаков набора. По горизонтальной оси расположены признаки, по вертикальной – номера записей/строк. Желтый цвет соответствует пропускам данных.Заметно, например, что признак life_sq имеет довольно много пустых строк, а признак floor – напротив, всего парочку – около 7000 строки.<jupyter_code>cols = df.columns[:30] # первые 30 колонок
# определяем цвета 
# желтый - пропущенные данные, синий - не пропущенные
colors = ['#000099', '#ffff00'] 
a = sns.heatmap(df[cols].isnull(), cmap=sns.color_palette(colors))<jupyter_output><empty_output><jupyter_text>Метод 2 - Процентный список пропущенных данныхЕсли в наборе много признаков и визуализация занимает много времени, можно составить список долей отсутствующих записей для каждого признака. Скажем, картинка для всех признаков датасета будет выглядеть чудовищно.  метод enumerate<jupyter_code>myList = ['red', 'g', 'blue']

for i in enumerate(myList):
    print(i)
for i, col in enumerate(df.columns):
    pct_missing = np.mean(df[col].isnull())
    print('{} - {}%'.format(col, round(pct_missing*100)))
#     if i>=10:
#         break
df.isnull().mean()
print(f'Varibale equel to {[1232,232]}')<jupyter_output>Varibale equel to [1232, 232]<jupyter_text>У признака life_sq отсутствует 21% значений, а у floor – только 1%.Этот список является полезным резюме, которое дополняет визуализацию тепловой карты. На практике часто проще применить именно такое описание.   Метод 3 - Гистограмма пропущенных данныхЕще одна хорошая техника визуализации для наборов с большим количеством признаков – построение гистограммы для числа отсутствующих значений в записи.<jupyter_code># сначала создаем индикатор для признаков с пропущенными данными
for col in df.columns:
    missing = df[col].isnull()
    num_missing = np.sum(missing)
    
    if num_missing > 0:  
#         print('created missing indicator for: {}'.format(col))
        df['{}_ismissing'.format(col)] = missing
        
# затем на основе индикатора строим гистограмму
ismissing_cols = [col for col in df.columns if 'ismissing' in col]
df['num_missing'] = df[ismissing_cols].sum(axis=1)
u = df['num_missing'].value_counts().reset_index().sort_values(by='index').plot.bar(x='index')<jupyter_output><empty_output><jupyter_text>Что делать с пропущенными значениями?Не существует общих решений для проблемы отсутствующих данных. Для каждого конкретного набора приходится искать наиболее подходящие методы или их комбинации.Разберем четыре самых распространенных техники. Они помогут в простых ситуациях, но, скорее всего, придется проявить творческий подход и поискать нетривиальные решения, например, промоделировать пропуски.  Отбрасывание записейВыбросим все строки с пропущенными значениями. Это решение подходит только в том случае, если недостающие данные не являются информативными.Для отбрасывания можно использовать и другие критерии. Например, из гистограммы, построенной в предыдущем разделе, мы узнали, что лишь небольшое количество строк содержат более 35 пропусков. Мы можем создать новый набор данных df_less_missing_rows, в котором отбросим эти строки.<jupyter_code># отбрасываем строки с большим количеством пропусков
ind_missing = df[df['num_missing'] > 35].index
df_less_missing_rows = df.drop(ind_missing, axis=0)<jupyter_output><empty_output><jupyter_text>Отбрасывание признаковКак и предыдущая техника, отбрасывание признаков может применяться только для неинформативных признаков.В процентном списке, построенном ранее, мы увидели, что признак hospital_beds_raion имеет высокий процент недостающих значений – 47%. Иногда от таких признаков можно отказаться совсем.<jupyter_code>cols_to_drop = ['hospital_beds_raion']
df_less_hos_beds_raion = df.drop(cols_to_drop, axis=1)<jupyter_output><empty_output><jupyter_text>Внесение недостающих значенийДля численных признаков можно воспользоваться методом принудительного заполнения пропусков. Например, на место пропуска можно записать среднее, наиболее частое или медианное значение, полученное из остальных записей.Для категориальных признаков можно использовать в качестве заполнителя наиболее часто встречающееся значение.Возьмем для примера признак life_sq и заменим все недостающие значения медианой этого признака:<jupyter_code>med = df['life_sq'].median()
print(med)
df['life_sq'] = df['life_sq'].fillna(med)
df['life_sq']<jupyter_output><empty_output><jupyter_text>Одну и ту же стратегию заполнения можно применить сразу для всех числовых признаков:<jupyter_code># impute the missing values and create the missing value indicator variables for each numeric column.
df_numeric = df.select_dtypes(include=[np.number])
numeric_cols = df_numeric.columns.values
for col in numeric_cols:
    missing = df[col].isnull()
    num_missing = np.sum(missing)
    
    if num_missing > 0:  # only do the imputation for the columns that have missing values.
#         print('imputing missing values for: {}'.format(col))
        df['{}_ismissing'.format(col)] = missing
        med = df[col].median()
        df[col] = df[col].fillna(med)<jupyter_output><empty_output><jupyter_text>К счастью, в нашем наборе не нашлось пропусков в категориальных признаках. Но это не мешает нам продемонстрировать использование той же стратегии:<jupyter_code>df_non_numeric = df.select_dtypes(exclude=[np.number])
non_numeric_cols = df_non_numeric.columns.values
for col in non_numeric_cols:
    missing = df[col].isnull()
    num_missing = np.sum(missing)
    
    if num_missing > 0:  # only do the imputation for the columns that have missing values.
        print('imputing missing values for: {}'.format(col))
        df['{}_ismissing'.format(col)] = missing
        
        top = df[col].describe()['top'] # impute with the most frequent value.
        df[col] = df[col].fillna(top)<jupyter_output><empty_output><jupyter_text>Метод 4 - Замена недостающих значенийМожно использовать некоторый дефолтный плейсхолдер для пропусков, например, новую категорию _MISSING_ для категориальных признаков или число -999 для числовых.Таким образом, мы сохраняем данные о пропущенных значениях, что тоже может быть ценной информацией.<jupyter_code># категориальные признаки
df['sub_area'] = df['sub_area'].fillna('_MISSING_')
# численные признаки
df['life_sq'] = df['life_sq'].fillna(-999)<jupyter_output><empty_output><jupyter_text>А еще все эти методы уже сделали за нас<jupyter_code>df = pd.read_csv('train.csv')
test1 = df[cols].copy()
test1.head()
from sklearn.impute import SimpleImputer
imp_mf = SimpleImputer( strategy='most_frequent')
imp_mf.fit(test1)
imputed_test1_df = imp_mf.transform(test1)
pd.DataFrame(imputed_test1_df, columns = cols).head()
df = pd.read_csv('train.csv')
test1 = df[cols[3:10]].copy()
imp_mean = SimpleImputer( strategy='mean')
imp_mean.fit(test1)
imputed_test1_df = imp_mean.transform(test1)
pd.DataFrame(imputed_test1_df, columns = list(cols[3:10])).head()<jupyter_output><empty_output><jupyter_text>А еще можно подробненько прочесть вот тут:https://towardsdatascience.com/6-different-ways-to-compensate-for-missing-values-data-imputation-with-examples-6022d9ca0779  Нетипичные данные (выбросы)Выбросы – это данные, которые существенно отличаются от других наблюдений. Они могут соответствовать реальным отклонениям, но могут быть и просто ошибками.  Как обнаружить выбросы?Для численных и категориальных признаков используются разные методы изучения распределения, позволяющие обнаружить выбросы.  ГистограммаЕсли признак численный, можно построить гистограмму или коробчатую диаграмму (ящик с усами). Посмотрим на примере уже знакомого нам признака life_sq.<jupyter_code>matplotlib.rcParams['figure.figsize'] = (12,8)
a = df['life_sq'].hist(bins=100)
df['life_sq'].nlargest(100)
df['life_sq'].describe()<jupyter_output><empty_output><jupyter_text>Чтобы изучить особенность поближе, построим коробчатую диаграмму (aka ящик с усами)<jupyter_code>df.boxplot(column=['life_sq'])<jupyter_output><empty_output><jupyter_text>Столбчатая диаграммаДля категориальных признаков можно построить столбчатую диаграмму – для визуализации данных о категориях и их распределении.Например, распределение признака ecology вполне равномерно и допустимо. Но если существует категория только с одним значением "другое", то это будет выброс.<jupyter_code>a = df['ecology'].value_counts().plot.bar()<jupyter_output><empty_output><jupyter_text>А еще есть огромное количество более умных методов обнаружения выбросов!Особым любителям математики читать тут: https://towardsdatascience.com/a-brief-overview-of-outlier-detection-techniques-1e0b2c19e561 Выбросы довольно просто обнаружить, но выбор способа их устранения слишком существенно зависит от специфики набора данных и целей проекта. Их обработка во многом похожа на обработку пропущенных данных, которую мы разбирали в предыдущем разделе. Можно удалить записи или признаки с выбросами, либо скорректировать их, либо оставить без изменений.Переходим к более простой части очистки данных – удалению мусора.Вся информация, поступающая в модель, должна служить целям проекта. Если она не добавляет никакой ценности, от нее следует избавиться.Три основных типа «ненужных» данных:* неинформативные признаки с большим количеством одинаковых значений,* нерелевантные признаки,* дубликаты записей.  Неинформативные признакиЕсли признак имеет слишком много строк с одинаковыми значениями, он не несет полезной информации для проекта. Составим список признаков, у которых более 95% строк содержат одно и то же значение.<jupyter_code>num_rows = len(df.index)
low_information_cols = [] #
for col in df.columns:
    cnts = df[col].value_counts(dropna=False)
    top_pct = (cnts/num_rows).iloc[0]
    
    if top_pct > 0.95:
        low_information_cols.append(col)
        print('{0}: {1:.5f}%'.format(col, top_pct*100))
        print(cnts)
        print()<jupyter_output>oil_chemistry_raion: 99.02858%
no     30175
yes      296
Name: oil_chemistry_raion, dtype: int64

railroad_terminal_raion: 96.27187%
no     29335
yes     1136
Name: railroad_terminal_raion, dtype: int64

nuclear_reactor_raion: 97.16780%
no     29608
yes      863
Name: nuclear_reactor_raion, dtype: int64

big_road1_1line: 97.43691%
no     29690
yes      781
Name: big_road1_1line, dtype: int64

railroad_1line: 97.06934%
no     29578
yes      893
Name: railroad_1line, dtype: int64

cafe_count_500_price_high: 97.25641%
0    29635
1      787
2       38
3       11
Name: cafe_count_500_price_high, dtype: int64

mosque_count_500: 99.51101%
0    30322
1      149
Name: mosque_count_500, dtype: int64

cafe_count_1000_price_high: 95.52689%
0    29108
1     1104
2      145
3       51
4       39
5       15
6        8
7        1
Name: cafe_count_1000_price_high, dtype: int64

mosque_count_1000: 98.08342%
0    29887
1      584
Name: mosque_count_1000, dtype: int64

mosque_count_1500: 96.21936%
0    29[...]<jupyter_text>Что делать?Если после анализа причин получения повторяющихся значений вы пришли к выводу, что признак не несет полезной информации, используйте drop().  Нерелевантные признакиНерелевантные признаки обнаруживаются ручным отбором и оценкой значимости. Например, признак, регистрирующий температуру воздуха в Торонто точно не имеет никакого отношения к прогнозированию цен на российское жилье. Если признак не имеет значения для проекта, его нужно исключить. Но этот вопрос надо обсудить отдельно: иногда можно выявить эффект "ложной корреляции" и счесть важным в действительности неважный признак.  Дубликаты записейЕсли значения признаков (всех или большинства) в двух разных записях совпадают, эти записи называются дубликатами.  Как обнаружить повторяющиеся записи?Способ обнаружения дубликатов зависит от того, что именно мы считаем дубликатами. Например, в наборе данных есть уникальный идентификатор id. Если две записи имеют одинаковый id, мы считаем, что это одна и та же запись. Удалим все неуникальные записи:<jupyter_code># отбрасываем неуникальные строки
df_dedupped = df.drop('id', axis=1).drop_duplicates(keep='first')
# сравниваем формы старого и нового наборов
print(df.shape)
print(df_dedupped.shape)<jupyter_output>(30471, 292)
(30461, 291)<jupyter_text>Получаем в результате 10 отброшенных дубликатов.Другой распространенный способ вычисления дубликатов: по набору ключевых признаков. Например, неуникальными можно считать записи с одной и той же площадью жилья, ценой и годом постройки.Найдем в нашем наборе дубликаты по группе критических признаков – **full_sq, life_sq, floor, build_year, num_room, price_doc:**<jupyter_code>key = ['timestamp', 'full_sq', 'life_sq', 'floor', 'build_year', 'num_room', 'price_doc']
df.fillna(-999).groupby(key)['id'].count().sort_values(ascending=False).head(20)<jupyter_output><empty_output><jupyter_text>Получили в результате 16 дублирующихся записей.  Что делать с дубликатами?Очевидно, что повторяющиеся записи нам не нужны, значит, их нужно исключить из набора.Вот так выглядит удаление дубликатов, основанное на наборе ключевых признаков:<jupyter_code>key = ['timestamp', 'full_sq', 'life_sq', 'floor', 'build_year', 'num_room', 'price_doc']
df_dedupped2 = df.drop_duplicates(subset=key)
print(df.shape)
print(df_dedupped2.shape)<jupyter_output>(30471, 292)
(30455, 292)<jupyter_text>Новый набор df_dedupped2 стал короче на 16 записей. Большая проблема очистки данных – разные форматы записей. Для корректной работы модели важно, чтобы набор данных соответствовал определенным стандартам – необходимо тщательное исследование с учетом специфики самих данных. Мы рассмотрим четыре самых распространенных несогласованности:* Разные регистры символов.* Разные форматы данных (например, даты).* Опечатки в значениях категориальных признаков.* Адреса.  Разные регистры символовНепоследовательное использование разных регистров в категориальных значениях является очень распространенной ошибкой, которая может существенно повлиять на анализ данных.  Как обнаружить?Давайте посмотрим на признак sub_area:<jupyter_code>df['sub_area'].value_counts(dropna=False)[:10]<jupyter_output><empty_output><jupyter_text>Но если в какой-то записи вместо Poselenie Sosenskoe окажется poselenie sosenskoe, они будут расценены как два разных значения.  Что делать?Проблема легко решается принудительным изменением регистра:<jupyter_code># пусть все будет в нижнем регистре
df['sub_area_lower'] = df['sub_area'].str.lower()
df['sub_area_lower'].value_counts(dropna=False)[:10]<jupyter_output><empty_output><jupyter_text>Разные форматы данныхРяд данных в наборе находится не в том формате, с которым нам было бы удобно работать. Например, даты, записанные в виде строки, следует преобразовать в формат DateTime. Признак timestamp представляет собой строку, хотя является датой:  Что же делать?Чтобы было проще анализировать транзакции по годам и месяцам, значения признака timestamp следует преобразовать в удобный формат:<jupyter_code>df['timestamp_dt'] = pd.to_datetime(df['timestamp'], format='%Y-%m-%d')
df['year'] = df['timestamp_dt'].dt.year
df['month'] = df['timestamp_dt'].dt.month
df['weekday'] = df['timestamp_dt'].dt.weekday
print(df['year'].value_counts(dropna=False))
print()
print(df['month'].value_counts(dropna=False))<jupyter_output>2014    13662
2013     7978
2012     4839
2015     3239
2011      753
Name: year, dtype: int64

12    3400
4     3191
3     2972
11    2970
10    2736
6     2570
5     2496
9     2346
2     2275
7     1875
8     1831
1     1809
Name: month, dtype: int64<jupyter_text>Ссылочка для тех, кто хочет крутить даты в Python как жонглер из Cirque Du Soleil:https://towardsdatascience.com/how-to-manipulate-date-and-time-in-python-like-a-boss-ddea677c6a4d  ОпечаткиОпечатки в значениях категориальных признаков приводят к таким же проблемам, как и разные регистры символов. Как обнаружить?Для обнаружения опечаток требуется особый подход. В нашем наборе данных о недвижимости опечаток нет, поэтому для примера создадим новый набор. В нем будет признак city, а его значениями будут torontoo и tronto. В обоих случаях это опечатки, а правильное значение – toronto.Простой способ идентификации подобных элементов – [расстояние Левенштейна](https://ru.wikipedia.org/wiki/Расстояние_Левенштейна). Суть этого метода заключается в измерении количества букв (расстояния), которые нам нужно изменить, чтобы из одного слова получить другое.Предположим, нам известно, что в признаке city должно находиться одно из четырех значений: toronto, vancouver, montreal или calgary. Мы вычисляем расстояние между всеми значениями и словом toronto (и vancouver).Те слова, в которых содержатся опечатки, имеют меньшее расстояние с правильным словом, так как отличаются всего на пару букв.<jupyter_code>from nltk.metrics import edit_distance
df_city_ex = pd.DataFrame(data={'city': ['torontoo', 'toronto', 'tronto', 'vancouver', 'vancover', 'vancouvr', 'montreal', 'calgary']})
df_city_ex['city_distance_toronto'] = df_city_ex['city'].map(lambda x: edit_distance(x, 'toronto'))
df_city_ex['city_distance_vancouver'] = df_city_ex['city'].map(lambda x: edit_distance(x, 'vancouver'))
df_city_ex
df_city_ex['city'].value_counts()<jupyter_output><empty_output><jupyter_text>Мы можем установить критерии для преобразования этих опечаток в правильные значения.Например, если расстояние некоторого значения от слова toronto не превышает 2 буквы, мы преобразуем это значение в правильное – toronto.<jupyter_code>msk = df_city_ex['city_distance_toronto'] <= 2
df_city_ex.loc[msk, 'city'] = 'toronto'
msk = df_city_ex['city_distance_vancouver'] <= 2
df_city_ex.loc[msk, 'city'] = 'vancouver'
df_city_ex<jupyter_output><empty_output><jupyter_text>Оставим главную боль напоследокПроще предположить, что проблема разных форматов адреса точно существует. Даже если визуально вы не обнаружили беспорядка в этом признаке, все равно стоит стандартизировать их для надежности.В нашем наборе данных по соображениям конфиденциальности отсутствует признак адреса, поэтому создадим новый набор df_add_ex:<jupyter_code>df_add_ex = pd.DataFrame(['123 MAIN St Apartment 15', '123 Main Street Apt 12   ', '543 FirSt Av', '  876 FIRst Ave.'], columns=['address'])
df_add_ex<jupyter_output><empty_output><jupyter_text>Что делать?Минимальное форматирование включает следующие операции:* приведение всех символов к нижнему регистру;* удаление пробелов в начале и конце строки;* удаление точек;* стандартизация формулировок: замена street на st, apartment на apt и т. д.<jupyter_code>df_add_ex['address_std'] = df_add_ex['address'].str.lower()
df_add_ex['address_std'] = df_add_ex['address_std'].str.strip()
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\\.', '') 
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\\bstreet\\b', 'st') 
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\\bapartment\\b', 'apt') 
df_add_ex['address_std'] = df_add_ex['address_std'].str.replace('\\bav\\b', 'ave') 
df_add_ex<jupyter_output><empty_output><jupyter_text>А еще это идеальный момент для применения регулярных выражений :)  datetime<jupyter_code>import datetime<jupyter_output><empty_output><jupyter_text>2Sun 10 May 2015 13:54:36 -0700Sun 10 May 2015 13:54:36 -0000Sat 02 May 2015 19:54:36 +0530Fri 01 May 2015 13:54:36 -0000<jupyter_code>times = int(input())
tcase = []
for i in range(times*2):
    tcase.append(input())
tcase[0]
def delter (t0, t1):
    t0 = datetime.datetime.strptime(t0, '%a %d %b %Y %H:%M:%S %z')
    t1 = datetime.datetime.strptime(t1, '%a %d %b %Y %H:%M:%S %z')
    print(int((t0-t1).total_seconds()))

for i in range(times):
    delter(tcase[i*2], tcase[i*2+1])<jupyter_output>25200
88200
{'fraction_non_alphanumeric': 0.08683002227576488, 'fraction_numerical': 0.0288221121061963, 'mean_word_length': 6.077863577863578, 'pattern_counts': {'":': 0, '<': 110, '<?xml version=': 0, '>': 114, 'https://': 5, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '28', 'dir': 'jupyter-structured-clean-dedup', 'id': '230045', 'license': 'MIT', 'n_tokens_mistral': 8889, 'n_tokens_neox': 9752, 'n_words': 2142, 'path': '01 python/lecture 15 materials/2020_DPO_15_1_Missing_values.ipynb', 'repo_name': 'ksetdekov/HSE_DS', 'size': '21998'}