Прежде чем перейдем к практике, давайте вспомним сложности, с которыми вы можете столкнуться при анализе данных. Все они уже упоминались раньше, просто систематизируем их. Данные могут:
- быть представлены в виде, не подходящем для обработки (например, числовые данные сохранены как текст);
- состоять из разных файлов, которые необходимо корректно объединить;
- содержать пропущенные значения, которые невозможно получить из источника данных и поэтому необходимо восстановить.
Чтобы решить эти проблемы, нам нужно привести данные к целевому виду. Это не только заполнение пропущенных значений — но и любые преобразования, которые помогут нам лучше изучить их и сделать на их основе надёжные выводы.
Это важно, поскольку вы будете заниматься приведением к целевому виду практически всегда, прежде чем приступить к работе, поскольку данные очень редко собираются непосредственно под ваши задачи.
В этом разделе мы будем работать с несколькими наборами данных про кино: топ-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()
Результат выглядит удручающе:
Сделаем то же самое с колонкой, которую мы преобразовали. Мы можем изменить её тип со строки на число. До очистки данных мы не могли это сделать.
imdb_series['Year2'] = imdb_series['Year2'].astype('int')
>>> imdb_series['Year2'].hist(bins=20)
>>> plt.show()
Изменение типа привело к тому, что matplotlib
корректно распознал интервалы. Без очистки мы не смогли бы преобразовать тип. На всякий случай дадим ссылку на очищенный датасет.
Напомним, что до этого мы точно так же разобрались с годами в датасете про фильмы. Теперь мы можем их сравнить:
>>> imdb_series['Year2'].hist(bins=20, alpha = 0.5)
>>> imdb_films['year'].hist(bins=20, alpha = 0.5)
>>> plt.show()
Такая визуализация стала возможной, благодаря приведению данных к целевому виду. Глядя на эти данные легко сравнить распределение наиболее популярных фильмов и сериалов и увидеть, когда началась эра сериалов. Очень часто чистка данных и их адаптация под наши задачи занимает гораздо больше времени, чем непосредственно работа с ними, но это необходимый этап процесса.
Восстановление пропущенных значений с помощью метода MICE
Пропущенные значения — большая проблема для исследователей, поэтому важно уметь с ними работать. Импутация — сложная процедура, поэтому тут мы познакомимся только с одним из методов.
Мы будем работать с другим датасетом — опросом о ценностях World Value Survey.
WVS — большой международный проект, который проводит свои опросы в разных странах. В опросе более двухсот вопросов и для работы с ним нам необходимо пользоваться кодбуком, котрый можно найти по ссылке. Как мы видим, в кодбуке указан вопрос и то, как кодировался ответ респондентов:
Датасет 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 могут помочь в их организации и интерпретации результатов.