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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

>>> imdb_series.drop('titleColumn href', axis='columns', inplace=True)
>>> print(imdb_series)
     titleColumn               titleColumn 2 secondaryInfo  ratingColumn
0            1.0             Planet Earth II        (2016)           9.4
1            2.0                Breaking Bad        (2008)           9.4
2            3.0                Planet Earth        (2006)           9.4
3            4.0            Band of Brothers        (2001)           9.4
4            5.0                   Chernobyl        (2019)           9.3
..           ...                         ...           ...           ...
245        246.0                 Black Books        (2000)           8.4
246        247.0                 Foyle's War        (2002)           8.4
247        248.0            The Defiant Ones        (2017)           8.4
248        249.0                Happy Valley        (2014)           8.4
249        250.0  X-Men: The Animated Series        (1992)           8.4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

8

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

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

8

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

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

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

8

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

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

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

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

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

8

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

>>> import pandas as pd
>>> wv7 = pd.read_csv('WV7.csv')
>>> wv7_rus = wv7[wv7['B_COUNTRY'] == 643]
>>> colums_to_subset = ['D_INTERVIEW', 'Q46', 'Q47', 'Q48', 'Q49', 'Q50', 'Q51', 'Q52', 'Q53', 'Q54', 'Q55', 'Q56',  'Q260', 'Q262', 'Q275', 'Q281', 'Q288']
>>> wv7_rusHappy = wv7_rus[colums_to_subset]
>>> print(wv7_rusHappy)
       D_INTERVIEW  Q46  Q47   Q48   Q49  Q50  Q51  Q52  Q53  Q54  Q55  Q56
12915    643070031  2.0  3.0   8.0   8.0  5.0  4.0  4.0  4.0  4.0  4.0  1.0   
12916    643070064  2.0  2.0   6.0   7.0  6.0  4.0  4.0  4.0  4.0  4.0  3.0   
12917    643070130  3.0  3.0  10.0   7.0  6.0  4.0  4.0  4.0  4.0  4.0  1.0   
12918    643070153  2.0  3.0  10.0  10.0  6.0  4.0  2.0  4.0  4.0  4.0  1.0   
12919    643070229  1.0  2.0   6.0   9.0  NaN  4.0  4.0  4.0  4.0  4.0  1.0   
...            ...  ...  ...   ...   ...  ...  ...  ...  ...  ...  ...  ...   
77971    643071165  2.0  2.0   7.0   7.0  6.0  4.0  4.0  4.0  4.0  4.0  3.0   
77972    643071257  4.0  4.0   9.0   1.0  1.0  4.0  1.0  2.0  1.0  4.0  3.0   
77973    643071403  2.0  1.0   6.0   7.0  7.0  4.0  4.0  4.0  4.0  4.0  3.0   
77974    643071440  2.0  2.0   6.0  10.0  5.0  4.0  4.0  4.0  4.0  4.0  2.0   
77975    643071496  2.0  2.0   7.0   7.0  7.0  4.0  3.0  4.0  4.0  4.0  3.0  

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

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

>>> print(wv7_rusHappy.isnull().sum())
D_INTERVIEW     0
Q46            51
Q47             6
Q48            43
Q49            15
Q50            13
Q51            16
Q52            16
Q53            13
Q54            15
Q55             8
Q56            86
Q260            0
Q262            0
Q275           10
Q281           44
Q288           63
dtype: int64

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

>>> print(wv7_rusHappy.isnull().mean()*100)
D_INTERVIEW    0.000000
Q46            2.817680
Q47            0.331492
Q48            2.375691
Q49            0.828729
Q50            0.718232
Q51            0.883978
Q52            0.883978
Q53            0.718232
Q54            0.828729
Q55            0.441989
Q56            4.751381
Q260           0.000000
Q262           0.000000
Q275           0.552486
Q281           2.430939
Q288           3.480663
dtype: float64

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

>>> wv7_rusHappy_drop = wv7_rh.copy(deep=True)
>>> wv7_rusHappy_drop.dropna(how='any', inplace=True)
>>> print(wv7_rusHappy.shape)
>>> print(wv7_rusHappy_drop.shape)
(1810, 17)
(1521, 17)

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

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

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

>>> pip install fancyimpute

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

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

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

print(wv7_rusHappy_mice.isnull().mean()*100)
D_INTERVIEW    0.0
Q46            0.0
Q47            0.0
Q48            0.0
Q49            0.0
Q50            0.0
Q51            0.0
Q52            0.0
Q53            0.0
Q54            0.0
Q55            0.0
Q56            0.0
Q260           0.0
Q262           0.0
Q275           0.0
Q281           0.0
Q288           0.0
dtype: float64

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

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

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

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

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