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

  • быть представлены в виде, не подходящем для обработки (например, числовые данные сохранены как текст);
  • состоять из разных файлов, которые необходимо корректно объединить;
  • содержать пропущенные значения, которые невозможно получить из источника данных и поэтому необходимо восстановить.

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

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

В этом разделе мы будем работать с несколькими наборами данных про кино: топ-250 фильмов по версии IMDb и топ-250 сериалов по версии IMDb.

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

Давайте взглянем на датасеты.

1>>> import pandas as pd
2>>> imdb_films = pd.read_csv('imdb.csv')
3>>> print(imdb_films.head(10))
4   number                                              title  year  rating
50     1.0                           The Shawshank Redemption -1994     9.3
61     2.0                                      The Godfather -1972     9.2
72     3.0                                    The Dark Knight -2008     9.0
83     4.0                              The Godfather Part II -1974     9.0
94     5.0                                       12 Angry Men -1957     9.0
105     6.0                                   Schindler's List -1993     9.0
116     7.0      The Lord of the Rings: The Return of the King -2003     9.0
127     8.0                                       Pulp Fiction -1994     8.9
138     9.0  The Lord of the Rings: The Fellowship of the Ring -2001     8.8
149    10.0                     The Good, the Bad and the Ugly -1966     8.8
15
1>>> imdb_series = pd.read_csv('imdb_series_original.csv')
2>>> print(imdb_series.head(10))
3     titleColumn                titleColumn 2 secondaryInfo  ratingColumn
40            1.0              Planet Earth II        (2016)           9.4
51            2.0                 Breaking Bad        (2008)           9.4
62            3.0                 Planet Earth        (2006)           9.4
73            4.0             Band of Brothers        (2001)           9.4
84            5.0                    Chernobyl        (2019)           9.3
95            6.0                     The Wire        (2002)           9.3
106            7.0               Blue Planet II        (2017)           9.2
117            8.0   Avatar: The Last Airbender        (2005)           9.2
128            9.0  Cosmos: A Spacetime Odyssey        (2014)           9.2
139           10.0                 The Sopranos        (1999)           9.2

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

Чистка данных

В первых параграфах мы научились выбирать интересующие нас колонки и сортировать строки. Например, мы можем выбрать только фильмы после 2000 года. Однако, как быть, если нам нужны все значения, но необходимо поменять содержимое?

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

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

1>>> print(imdb_films.info())
2<class 'pandas.core.frame.DataFrame'>
3RangeIndex: 250 entries, 0 to 249
4Data columns (total 4 columns):
5 #   Column  Non-Null Count  Dtype
6---  ------  --------------  -----
7 0   number  250 non-null    float64
8 1   title   250 non-null    object
9 2   year    250 non-null    int64
10 3   rating  250 non-null    float64
11dtypes: float64(2), int64(1), object(1)
12memory usage: 7.9+ KB
13None

Колонка year, которая нас интересует, состоит из целых чисел. Это значит, что мы можем применить все стандартные операции, доступные для них: сложение, вычитание, умножение и так далее. Самой простой способ превратить отрицательное число в положительное — это взять это число по модулю. Для этого в python есть методы abs()

Давай посмотрим на примере, как мы применим эту команду ко всем значениям в колонке:

1>>> cols = ['year']
2>>> imdb_films[cols] = imdb_films[cols].abs()
3>>> print(imdb_films.head())
4  number                     title  year  rating
50     1.0  The Shawshank Redemption  1994     9.3
61     2.0             The Godfather  1972     9.2
72     3.0           The Dark Knight  2008     9.0
83     4.0     The Godfather Part II  1974     9.0
94     5.0              12 Angry Men  1957     9.0

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

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

1>>> imdb_series= pd.read_csv('imdb_series_original.csv')
2>>> print(imdb_series)

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

Для начала, избавимся от колонки со ссылками на фильмы:

1>>> imdb_series.drop('titleColumn href', axis='columns', inplace=True)
2>>> print(imdb_series)
3     titleColumn               titleColumn 2 secondaryInfo  ratingColumn
40            1.0             Planet Earth II        (2016)           9.4
51            2.0                Breaking Bad        (2008)           9.4
62            3.0                Planet Earth        (2006)           9.4
73            4.0            Band of Brothers        (2001)           9.4
84            5.0                   Chernobyl        (2019)           9.3
9..           ...                         ...           ...           ...
10245        246.0                 Black Books        (2000)           8.4
11246        247.0                 Foyle's War        (2002)           8.4
12247        248.0            The Defiant Ones        (2017)           8.4
13248        249.0                Happy Valley        (2014)           8.4
14249        250.0  X-Men: The Animated Series        (1992)           8.4
15

Используем метод drop. Нам нужно указать название и направление поиска (по колонкам или по строкам). В данном случае поиск необходимо осуществлять по колонкам. Обратите внимание на параметр inplace. Если установлено значение True, изменившийся датафрейм будет переписан поверх старого.

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

1>>> imdb_series.rename(columns = {'titleColumn':'Number', 'titleColumn 2':'Title', 'secondaryInfo':'Year', 'ratingColumn':'Rating'}, inplace = True)
2>>> print(imdb_series)

Здесь мы создаём словарь, который содержит старые и новые значения. Метод rename использует его, чтобы изменить названия колонок.

Обратимся к колонке year, значения которой записаны в скобочках. Последовательность здесь такая же:

  • нам нужно понять, с каким типом данных мы имеем дело;
  • определить способ, которым мы хотим преобразовать переменную;
  • преобразовать переменную.
1>>> imdb_series.info()
2<class 'pandas.core.frame.DataFrame'>
3RangeIndex: 250 entries, 0 to 249
4Data columns (total 4 columns):
5 #   Column  Non-Null Count  Dtype
6---  ------  --------------  -----
7 0   Number  250 non-null    float64
8 1   Title   250 non-null    object
9 2   Year    250 non-null    object
10 3   Rating  250 non-null    float64
11dtypes: float64(2), object(2)
12memory usage: 7.9+ KB

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

1>>> imdb_series['Year'] = imdb_series['Year'].astype('string')
2>>> imdb_series.info()
3<class 'pandas.core.frame.DataFrame'>
4RangeIndex: 250 entries, 0 to 249
5Data columns (total 4 columns):
6 #   Column  Non-Null Count  Dtype
7---  ------  --------------  -----
8 0   Number  250 non-null    float64
9 1   Title   250 non-null    object
10 2   Year    250 non-null    string
11 3   Rating  250 non-null    float64
12dtypes: float64(2), object(1), string(1)

Отлично! Теперь мы можем работать с этим значением, как с текстовой строкой. Более подробно о работе с текстом мы поговорим в следующих параграфах, но пока познакомимся с одним простым методом: replace. Как всегда, сначала посмотрим, как он работает на практике:

1>>> imdb_series['Year2']= imdb_series['Year'].str.replace('(','')

Мы создаем вторую колонку с годами Year2, для этого мы берем колонку Year, каждое значение берем как строку и с помощью команды replace меняем знак ( на ничего — поскольку наша цель избавиться от этого знака. Посмотрим, что получилось.

1>>> imdb_series.head()
2   number                     title  year   rating
30     1.0  The Shawshank Redemption  1994)     9.2
41     2.0             The Godfather  1972)     9.2
52     3.0           The Dark Knight  2008)     9.0
63     4.0     The Godfather Part II  1974)     9.0
74     5.0              12 Angry Men  1957)     9.0

Первая скобка исчезла. Теперь нам нужно преобразовать эту строку и убрать вторую, мешающую нам скобку.

1imdb_series['Year2']= imdb_series['Year2'].str.replace(')','')
2imdb_series.head()

Как и для большинства задач в Python — это не единственное возможное решение. У нас есть две колонки “Year” и “Year2”, теперь, когда мы избавились от шума, нам гораздо проще корректно визуализировать данные. Создадим гистограмму с неочищенными данными.

1>>> import matplotlib.pyplot as plt
2>>> imdb_series['Year'].hist(bins=20)
3>>> plt.show()

Результат выглядит удручающе:

8

Сделаем то же самое с колонкой, которую мы преобразовали. Мы можем изменить её тип со строки на число. До очистки данных мы не могли это сделать.

1imdb_series['Year2'] = imdb_series['Year2'].astype('int')
2>>> imdb_series['Year2'].hist(bins=20)
3>>> plt.show()

8

Изменение типа привело к тому, что matplotlib корректно распознал интервалы. Без очистки мы не смогли бы преобразовать тип. На всякий случай дадим ссылку на очищенный датасет.

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

1>>> imdb_series['Year2'].hist(bins=20, alpha = 0.5)
2>>> imdb_films['year'].hist(bins=20, alpha = 0.5)
3>>> plt.show()

8

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

Восстановление пропущенных значений с помощью метода MICE

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

Мы будем работать с другим датасетом — опросом о ценностях World Value Survey.

WVS — большой международный проект, который проводит свои опросы в разных странах. В опросе более двухсот вопросов и для работы с ним нам необходимо пользоваться кодбуком, котрый можно найти по ссылке. Как мы видим, в кодбуке указан вопрос и то, как кодировался ответ респондентов:

8

Датасет WVS очень большой и работать с ним целиком неудобно и обычно не нужно, поэтому мы будем использовать только часть вопросов и только в отношении России.

1>>> import pandas as pd
2>>> wv7 = pd.read_csv('WV7.csv')
3>>> wv7_rus = wv7[wv7['B_COUNTRY'] == 643]
4>>> colums_to_subset = ['D_INTERVIEW', 'Q46', 'Q47', 'Q48', 'Q49', 'Q50', 'Q51', 'Q52', 'Q53', 'Q54', 'Q55', 'Q56',  'Q260', 'Q262', 'Q275', 'Q281', 'Q288']
5>>> wv7_rusHappy = wv7_rus[colums_to_subset]
6>>> print(wv7_rusHappy)
7       D_INTERVIEW  Q46  Q47   Q48   Q49  Q50  Q51  Q52  Q53  Q54  Q55  Q56
812915    643070031  2.0  3.0   8.0   8.0  5.0  4.0  4.0  4.0  4.0  4.0  1.0   
912916    643070064  2.0  2.0   6.0   7.0  6.0  4.0  4.0  4.0  4.0  4.0  3.0   
1012917    643070130  3.0  3.0  10.0   7.0  6.0  4.0  4.0  4.0  4.0  4.0  1.0   
1112918    643070153  2.0  3.0  10.0  10.0  6.0  4.0  2.0  4.0  4.0  4.0  1.0   
1212919    643070229  1.0  2.0   6.0   9.0  NaN  4.0  4.0  4.0  4.0  4.0  1.0   
13...            ...  ...  ...   ...   ...  ...  ...  ...  ...  ...  ...  ...   
1477971    643071165  2.0  2.0   7.0   7.0  6.0  4.0  4.0  4.0  4.0  4.0  3.0   
1577972    643071257  4.0  4.0   9.0   1.0  1.0  4.0  1.0  2.0  1.0  4.0  3.0   
1677973    643071403  2.0  1.0   6.0   7.0  7.0  4.0  4.0  4.0  4.0  4.0  3.0   
1777974    643071440  2.0  2.0   6.0  10.0  5.0  4.0  4.0  4.0  4.0  4.0  2.0   
1877975    643071496  2.0  2.0   7.0   7.0  7.0  4.0  3.0  4.0  4.0  4.0  3.0  

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

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

1>>> print(wv7_rusHappy.isnull().sum())
2D_INTERVIEW     0
3Q46            51
4Q47             6
5Q48            43
6Q49            15
7Q50            13
8Q51            16
9Q52            16
10Q53            13
11Q54            15
12Q55             8
13Q56            86
14Q260            0
15Q262            0
16Q275           10
17Q281           44
18Q288           63
19dtype: int64

С помощью метода isnull() мы получаем оценку True или False для каждого значения в датасете, где True — означает, что значение пропущено. Затем мы суммируем их и получаем информацию о количестве пропущенных значений. Само по себе количество пропущенных значений мало о чем говорит, поэтому мы можем оценить это значение в процентах.

1>>> print(wv7_rusHappy.isnull().mean()*100)
2D_INTERVIEW    0.000000
3Q46            2.817680
4Q47            0.331492
5Q48            2.375691
6Q49            0.828729
7Q50            0.718232
8Q51            0.883978
9Q52            0.883978
10Q53            0.718232
11Q54            0.828729
12Q55            0.441989
13Q56            4.751381
14Q260           0.000000
15Q262           0.000000
16Q275           0.552486
17Q281           2.430939
18Q288           3.480663
19dtype: float64

Мы видим, что результат нигде не превышает 5%, а чаще значительно ниже. Хотя их мало, но их наличие будет мешать нам работать с этими данными. Простой способ, которым можно воспользоваться в этой ситуации — просто удалить пропущенные значения. Для этого можно использовать команду dropna().

1>>> wv7_rusHappy_drop = wv7_rh.copy(deep=True)
2>>> wv7_rusHappy_drop.dropna(how='any', inplace=True)
3>>> print(wv7_rusHappy.shape)
4>>> print(wv7_rusHappy_drop.shape)
5(1810, 17)
6(1521, 17)

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

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

Среди разных методов заполнения пропущенных значений, мы рассмотрим метод MICE. Он основан на многократных регрессиях, о которых мы ещё поговорим. Этот метод есть в разных библиотеках, написанных для Python, но мы будем использовать fancyimpute. Для начала установим эту библиотеку:

1>>> pip install fancyimpute

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

1from fancyimpute import IterativeImputer
2MICE_imputer = IterativeImputer()
3wv7_rusHappy_mice = wv7_rusHappy.copy(deep=True)
4wv7_rusHappy_mice.iloc[:, :] = MICE_imputer.fit_transform(wv7_rusHappy_mice) 

Проверим, остались ли в новом датасете пропущенные значения:

1print(wv7_rusHappy_mice.isnull().mean()*100)
2D_INTERVIEW    0.0
3Q46            0.0
4Q47            0.0
5Q48            0.0
6Q49            0.0
7Q50            0.0
8Q51            0.0
9Q52            0.0
10Q53            0.0
11Q54            0.0
12Q55            0.0
13Q56            0.0
14Q260           0.0
15Q262           0.0
16Q275           0.0
17Q281           0.0
18Q288           0.0
19dtype: float64

Вот и всё. Теперь вы умеете приводить данные к целевому виду: очищать значения, менять тип и заполнять пропуски.

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

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф8.2. Обработка пропущенных значений
Следующий параграф9.1. Экспериментальные данные, их правовой и этический статус