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