Занятие 2
Библиотека Pandas¶
В прошлом занятии мы пользовались библиотекой Numpy для анализа данных, а теперь давайте разберёмся с Pandas.
import numpy as np
import pandas as pd
Объекты класса pandas.Series
¶
Серия — это массив NumPy, но с именем и с дополнительной индексацией ячеек: индексами на основе меток (label-based indexing).
Если индексы-метки не указать при создании серии, то по умолчанию они совпадают с порядковыми индексами от 0.
ages = pd.Series([22, 35, 25, 16], name="age")
print(ages)
Можно явно указать индексы через параметр конструктора index
:
named_ages = pd.Series(data=[22, 35, 25, 16], index=["Aline", "Beatrice", "Clara", "Diana"], dtype=np.int32, name="age")
print(named_ages)
Доступ с квадратными скобками series[i]
возможен как через числовые индексы, так и по меткам:
named_ages["Beatrice"]
named_ages[1]
Сложности двойной индексации и их решение: loc
, iloc
, at
, iat
¶
Создадим простую серию с числами и возьмём её срез:
numbers = pd.Series(range(0, 1000, 100))
the_slice = numbers[2:7]
print(the_slice)
Обратите внимание, что после среза индексы-метки остались закреплёнными за своими значениями элементов. Это правильно с точки зрения идентификации хранимых данных. Особенно, если данные про одну сущность будут находиться в разных сериях и потом их нужно будет сопоставлять.
Однако, в результате появляется неоднозначность индексации.
Доступ с квадратными скобками теперь работает только по индексам-меткам:
the_slice[2]
Если же для такой неоднозначной ситуации взять срез, то мы получим "жёлтую карточку" — предупреждение от Pandas:
the_slice[2:5]
Как видите, мы получили срез по порядковым индексам, но библиотека предупреждает о будущем изменении поведения: скоро в таких ситуациях будет осуществляться срез по индексам-меткам.
Достоверно доступиться к элементам серии по порядковому индексу можно через локатор iloc
:
the_slice.iloc[2:5]
А достоверный доступ и срез через индексы-метки мы получим через локатор loc
:
the_slice.loc[2:5]
Взгляните! элемент с индексом-меткой 5 включён в срез, что противоречит логике range(start, stop, step)
.
Почему так?! Подумайте про срезы с метками строкового типа и посмотрите на пример ниже:
named_ages.loc["Beatrice":"Diana"]
Если доступ нужен только к одному элементу, то можно вместо loc
и iloc
воспользоватсья at
и iat
:
the_slice.at[4]
the_slice.iat[4]
Добавление данных в серию, удаление¶
В отличие от массивов NumPy, серии имеют переменный размер:
s = pd.Series(data=[0, 10, 20, 30], index=[1, 2, 3, 4])
print(s)
s.pop(1) # удаление элемента с индексом-меткой 1
print(s)
Добавление элементов с присваиванием индекса-метки можно делать через локатор loc
:
s.loc[5] = 50
print(s)
Обратите внимание, что таким добавлением можно нарушить порядок индексов:
s.loc[1] = 100
print(s)
Если индексы-метки нужно пересортировать, можно сделать это при помощи sort_index
:
s.sort_index(inplace=True)
print(s)
Ещё более курьёзную ситуацию с индексами можно создать при помощи конкатенации серий функцией pd.concat
:
a = pd.Series(data=[10, 20, 40], index=[1, 2, 4])
b = pd.Series(data=[100, 200, 300], index=[1, 2, 3])
c = pd.concat([a, b])
print(c)
c.index
c[1] = 10000
print(c)
Итак, индексы-метки могут оказаться не уникальными!!! И присваивание в такую метку приводит к изменению всех значений с этой меткой.
Лучше такой ситуации не создавать. Вот так можно проверить, что метки уникальны:
c.index.is_unique
Редукция серий¶
Агрегирующие методы по сравнению с массивами NumPy.ndarray
в сериях переопределены: в них допускаются и просто игнорируются пустые ячейки, которые представлены как NaN
(Not a Number).
values = [1, 3, 5, np.nan, 1, np.nan, 3]
array = np.array(values)
series = pd.Series(values)
for container in array, series:
print(type(container))
print(container.min(), container.max(), container.sum(), container.prod(), sep='\t')
Уникальные значения из серии можно получить методом unique
. Только нужно понимать, что результат — это уже не серия, а просто массив NumPy, поскольку для массива уникальных значений теряется смысл индексов-меток:
series.unique()
Частотный анализ для значений в серии можно запустить методом value_counts
:
series.value_counts()
Для статистической обработки есть большое количество методов:
series.mean() # среднее арифметическое
series.std() # среднеквадратическое отклонение
series.median() # медиана
series.quantile(3/4) # произвольный квантиль, например, 3-я квартиль (число, меньше либо равно которого 75% чисел серии)
Все функции выше вычислялись только для содержательных — не пустых элементов серии. Узнать их количество можно методом count
:
series.count() # количество содержательных элементов в серии
Отсутствующие значения в серии¶
Поскольку в серии допускаются значения NaN
, которые по-умолчанию не учитываются в статистических функциях, нужно уметь с ними работать:
- проверять факт их наличия в серии,
- считать их количество,
- выбрасывать (
drop
) такие ячейки из серии, - массово заменять их на другие значения.
series.isna()
series.isna().any()
Просуммировав эту серию, мы узнаем количество пустых ячеек (т.к. при арифметическом сложении превращаются: True
— в 1
, а False
— в 0
):
series.isna().sum()
series.dropna()
Надо помнить, что метод dropna
, как и многие другие методы, не модифицирует текущую серию, а создаёт и возвращает новую. Поэтому наша серия останется в старом состоянии:
series
Если мы хотим поменять текущую серию, нужно указать значение ключевого параметра inplace=True
:
series.dropna(inplace=True)
series
То же касается метода fillna
массовой замены значений NaN
на другое:
series = pd.Series([1, 3, 5, np.nan, 1, np.nan, 3])
series.fillna(0, inplace=True)
series
Векторные (массовые операции)¶
Как и для массивов NumPy для серий работают массовые операции:
A = pd.Series(data=[10, 20, 30, 40], index=["A", "B", "C", "D"])
B = pd.Series(data=[1, 2, 3, 4], index=["A", "B", "C", "D"])
A + B
A * B
Массовые операции работают не только с двумя сериями, но и с константами:
-A + 25
A > 20
B == 3
B != 3
Применить какую-то функцию к каждому элементу серии можно при помощи метода apply
:
B.apply(lambda x: x*100)
Значения из одной серии можно массово скопировать в другую серию методом update
, при этом опора будет производиться на индексы-метки, а не на последовательность элементов. При этом для меток старой серии, к которой не найдены метки новой серии, будут оставлены старые значения:
A = pd.Series(data=[100, 200, 500, 0], index=["Aline", "Beatrice", "Clara", "Diana"], name="money")
B = pd.Series(data=[300, 0], index=["Diana", "Aline"])
print(A)
A.update(B)
print(A)
Объекты класса DataFrame¶
После знакомства с сериями Series
легче понять как формируется лист с данными.
По сути это несколько серий-столбцов с одинаковым индексом по вертикали, а последовательность имён столбцов составляет горизонтальный индекс таблицы.
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]},
index = ["first", "second", "third"])
df
df.index
df.columns
Доступ к сериям-столбцам можно делать через квадратные скобки или через оператор .
(если имя является допустимым идентификатором):
df['A']
df.A
type(df.A)
Однако, отличие в доступе всё-таки есть. Через точку нельзя создать новый столбец:
df.D = df.A*100 + df.B*10 + df.C
df
df['D'] = df.A*100 + df.B*10 + df.C
df
df['D']
Сложности индексации и их решение: loc, iloc.¶
Квадратные скобки дают как доступ к столбцам, так и, в случае среза — к строкам:
df[1:3] # срез по порядковым индексам
df['first':'second'] # срез по индексам-меткам (включая конечную)
Как вы помните, это может приводить к неоднозначности, поэтому лучше использовать loc
или iloc
:
df.iloc[1:3]
df.loc['first':'second']
Новая возможность — локатор позволяет получить серию значений одной строки. В этом случае индекс строки становится name
серии:
df.loc['second']
Запятая в квадратных скобках позволяет достать значение конкретной ячейки или срез по строкам и столбцам одновременно:
df.loc['first', 'A']
df.loc['first':'second', 'B':'C']
Также интересно то, что локатор в квадратных скобках может принимать список индексов, причём в произвольном порядке:
df.loc[['third', 'first'], ['D', 'B', 'C']]
В срезах можно пропускать параметры начала и конца, а также указывать третий параметр — шаг:
df.iloc[:, ::-1]
Редукция столбцов и строк¶
Очевидно, редукцию одного конкретного столбца или строки с конкретным индексом можно выполнить, предварительно выделив её как Series
, но есть и возможность массовой редукции по заданной оси:
df.sum(axis=1) # указываем axis=1 для свёртки по горизонтали
df.sum() # по-умолчанию axis=0, свёртка по вертикали
df.max()
df.idxmax()
Можно сделать свёртку сразу несколькими функциями при помощи метода aggregate
(alias agg
):
df.aggregate(['min', 'idxmin', 'max', 'idxmax', 'mean', 'sum'])
Чистка данных. Методы drop
и dropna
¶
Исходные данные часто содержат лишнюю информацию, которую требуется исключить. Если нужно сохранить изменения в самой таблице, то следует добавлять параметр inplace=True
, но сейчас мы этого делать не будем, оставляя лист в его исходном состоянии.
Чтобы отбросить лишние столбцы, используют метод drop
с ключевым параметром columns
и списком имён:
df.drop(columns=['B', 'D'])
Для отбрасывания строк используют метод drop
с ключевым параметром labels
:
df.drop(labels=['first'])
Теперь добавим в лист одну ячейку NaN
:
df.loc['second', 'C'] = np.nan
df
Теперь посмотрите как работает метод dropna
:
df.dropna() # по умолчанию выбрасываются все строки, в которых есть хотя бы одно значение NaN
df.dropna(axis='columns') # можно вместо строк с NaN выбросить столбцы:
Кроме стратегии выбрасывания данных есть ещё возможность заполнить пропуски константой:
df.fillna(0)
Или, как вариант, заполнить ячейки NaN
через интерполяцию [[1]](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%BF%D0%BE%D0%BB%D1%8F%D1%86%D0%B8%D1%8F) по соседним ячейкам серии-столбца:
df.interpolate()
Сложные приёмы обработки данных¶
Есть некоторые приёмы работы с DataFrame
, которые позволяют быстро переформатировать данные так, чтобы необходимая информация лежала на поверхности.
Представьте себе ситуацию, когда исходная таблица с данными содержит случаи, которые можно классифицировать по нескольким категориям. Например, друзья работали на огороде и собирали картошку и морковку. Вот таблица их "вкладов" в общую корзину:
commits = pd.DataFrame({"name": ["Петя", "Вася", "Таня", "Петя", "Петя", "Таня", "Вася", "Петя", "Таня"],
"potatoes": [3, 4, 0, 3, 4, 0, 6, 3, 0],
"carrots": [0, 2, 5, 2, 1, 4, 15, 2, 6]})
commits
Конечно, мы можем найти суммарное количество картошек и морковок в общей корзине или минимальное/максимальное количество за 1 вклад:
commits.sum()
commits.agg(["min", "max"])
Однако, имена тут только мешают, не имеют содержательной информации. А хотелось бы узнать статистику по каждому другу отдельно...
Метод groupby
¶
Метод groupby
осуществляет что-то вроде корзинной сортировки для последующей редукции.
Группировка происходит по уникальным значениям в указанном столбце. Эти значения будут играть роль индекса в новом листе.
commits.groupby("name") # разложим все вклады по именам вкладчиков
Объект со сгруппированными по указанному столбцу объектами мы и видим, но чтобы его использовать по назначению, требуется агрегировать случаи в каждой группе
commits.groupby("name").sum() # количество овощей, *сгруппированное* по имени вкладчика:
Чтобы вычислить несколько статистических функций, можно использовать метод agg
(синоним aggregate
).
commits.groupby("name").carrots.aggregate(["min", "max"]) # мин. и макс. количество морковок за один вклад
Если агрегацию при этом сделать сразу по нескольким числовым столбцам, то у нас получится двухуровневый индекс для столбцов:
commits.groupby("name").aggregate(["min", "max"]) # мин. и макс. количество овощей за один вклад
Сводная таблица pivot_table
¶
Иногда нужно осуществить группировку случаев-строк сразу по нескольким категориальным параметрам.
Допустим, участники олимпиады по программированию отправляли задачи в электронный контест. Таблица отправок в систему содержит:
run_id
— идентификатором запуска проверки,username
— хешем имени пользователя,problem
— буквой-идентификатором решаемой задачи,result
— вердиктом проверяющей системы,tests
— количеством успешно пройденных тестов при запуске,score
— набранными очками.
runs = pd.read_csv("http://cs.mipt.ru/pydatan_2023/extra/data/olymp3_results.csv", index_col='run_id')
runs.head()
Обратите внимание, что один и тот же участник мог отправлять задачу много раз. Допустим, нас интересует максимальное количество очков за каждый вид задачи, причём — отдельно для каждого пользователя.
В этом случае разумно сделать в сводной таблице идентификатор пользователя индексом-меткой строки, а букву-идентфикатор задачи — индексом-меткой столбца. При этом информация из столбцов result
, test
и индекса run_id
нас не интересует совсем — в качестве значений нас интересует только score
:
standings = runs.pivot_table(values="score", index="username", columns="problem", aggfunc="max")
standings # сводная таблица результатов участников
Из пожеланий к этой таблице результатов:
# 1. Заменить `NaN` на 0.
standings.fillna(0, inplace=True) # можно было указать fill_value=0 для pivot_table
standings.head(3)
# 2. В последнем столбце `score` посчитать сумму
# максимальных достигнутых баллов для каждого пользователя.
standings["score"] = standings.sum(axis=1)
standings.head(3)
# 3. Отсортировать по убыванию `score` все строки таблицы.
standings.sort_values(by="score", ascending=False, inplace=True)
standings.head(3) # лучшие по сумме баллов 3 участника олимпиады:
Разворачивание сводной таблицы melt
¶
Обратное преобразование из сводной таблицы в таблицу случаев невозможно!
Это очевидно, поскольку:
- при подготовке сводной таблицы производилась агрегация (редукция серии чисел к одному числу),
- часть столбцов, не относящаяся к
values
, игнорировалась.
Однако, можно развернуть ту небольшую статистическую выжимку, которая у нас осталась. Для таблицы результатов олимпиады это будут только лучшие отправки по каждому пользователю и задаче:
standings.reset_index().melt(id_vars=['username'], value_vars=['A', 'B', 'C', 'C', 'E', 'F', 'G'])
Замечание. Для того, чтобы использовать username
в качестве id_vars
пришлось вначале убрать его из индекса в обычный столбец при помощи reset_index()
.
scores1 = pd.DataFrame({'login': ['login1', 'login2', 'login3'],
'A': [2, 0, 5], 'B': [5, 3, 5], 'C': [5, 3, 5],})
scores1
scores2 = pd.DataFrame({'login': ['login4', 'login5'],
'A': [1, 3], 'B': [3, 0], 'C': [4, 2]})
scores2
scores = pd.concat([scores1, scores2])
scores
# можно было при pd.concat() добавить ignore_index=True,
# но можно и починить поломанный индекс вручную (методом его отбрасывания):
scores.reset_index(inplace=True)
scores.drop(columns=["index"], inplace=True)
scores
Возможна и другая конкатенация — по горизонтали (axis=1
):
scores3 = pd.DataFrame({'login': ['login2', 'login5', 'login6'],
'D': [8, 2, 7], 'E': [3, 0, 10]})
scores3
pd.concat([scores, scores3], axis=1)
Осторожно!!! Обратите внимание, что теперь сломана сама логика объединения, так при соединении нужно учитывать равенство login
.
Объединение таблиц при помощи merge
¶
Вместо функции pd.concat()
давайте применим метод merge
:
scores.merge(scores3)
У нас правильное соединились данные! Но есть нюанс в том, как это произошло.
Мы не указали merge()
никаких значений параметров, а параметр how
по умолчанию равен 'inner'
. Эта стратегия — взять только те строки, где есть полнота данных и слева, и справа. За ключи объединения берутся столбцы с одинаковыми названиями в обеих таблицах (у нас это login
).
Вот и получается, что при how='inner'
в пересечении множеств ключей (по значениями столбца login
) только два: login2
и login5
.
Есть ещё 4 стратегии объединения:
# `left`: сохранить всё из левой таблицы,
# информация из правой -- при совпадении ключей
scores.merge(scores3, how='left')
# `right`: сохранить всё из правой таблицы,
# информация из левой -- при совпадении ключей
scores.merge(scores3, how='right')
#`outer`: использует объединение ключей из обеих таблиц,
# сохранены все ключи и вся информация из обеих таблиц,
# зато пустые ячейки теперь есть во всех столбцах...
scores.merge(scores3, how='outer')
Стратегия слияния how='cross'
создает декартово произведение данных из обеих таблиц.
Для таблиц выше это не имеет смысла, поэтому приведём другой пример.
Есть юноши и девушки, для которых мы хотим вычислить степень их сходства:
boys = pd.DataFrame({'name': ['Петр', 'Василий', 'Тихон'],
'height': [183, 157, 172], 'weight': [78, 65, 95]})
boys
girls = pd.DataFrame({'name': ['Мария', 'Наталья', 'Ирина'],
'height': [173, 164, 154], 'weight': [81, 62, 52]})
girls
pairs = boys.merge(girls, how='cross')
pairs
Допустим, мы считаем пару подходящей, если юноша выше девушки, но не более, чем на 20 сантиметров, а её вес меньше его хотя бы на 5 кг:
pairs["match"] = (abs(pairs.height_x - pairs.height_y - 10) <= 10) & (pairs.weight_x >= pairs.weight_y + 5)
pairs
Осталось взять из таблицы только подходящие пары, а затем выбросить и сам столбец match
:
# в квадратных скобках можно использовать логическую серию
matched = pairs[pairs.match].drop(columns=['match'])
matched
Теперь ясно кого с кем нужно пробовать поженить :-)
Объединение таблиц при помощи join
¶
Метод join
очень похож на merge
, но отличается поведением по умолчанию:
- При отсутствии ключевого параметра
on=
для объединения используются индексы. - При отсутствии ключевого параметра
how=
выбирается стратегия'left'
.
boys.join(girls, lsuffix='_of_boy', rsuffix='_of_girl')
Если вы сталкивались с реляционными базами данных, то упомянутые стратегии merge
напоминают вам стратегии JOIN
в SQL.
Листы с данными типа DataFrame
имеют сходство с таблицами реляционных SQL баз данных, хотя есть и отличия:
SQL таблица | Pandas DataFrame |
---|---|
столбцы с именами | столбцы с индексами-метками |
столбцы со строгим типом | столбцы типизированные (почти всегда) |
порядок строк не задан | порядок строк определён, можно сорировать |
доступ к строке по ключу | доступ к строке по индексу-метке или числовому индексу |
первичный ключ уникален | индекс-метка может быть не уникальна |
Мастер-класс по анализу данных¶
Проанализируем данные из системы Яндекс.Контест по домашней работе к лекциям №1 и №2 с помощью бибилиотеки Pandas.
import pandas as pd
import matplotlib.pyplot as plt
Для начала загрузим данные по 1-му домашнему заданию и посмотрим на начало таблицы:
df_res1 = pd.read_csv("http://cs.mipt.ru/pydatan_2023/extra/data/result_lec01.csv")
df_res1.head()
Иногда бывает полезно посмотреть на хвост файла (иногда там может быть "грязь"):
df_res1.tail(3)
Посмотрим ещё раз на все названия столбцов:
df_res1.columns
Удаляем колонки, которые нам не пригодятся для дальнейшего анализа:
df_res1.drop(['Unnamed: 0', 'place', 'Penalty'], axis = 1, inplace=True)
df_res1.shape
Строк всего 778, а колонок всего 8. Переименуем колонки для дальнейшей работы:
column_names = df_res1.columns
df_res1.columns = [col_name.lower().replace(')', '').replace('(', '_').replace(' ', '_') for col_name in column_names]
df_res1.columns
Запросим более подробную информацию о колонках датасета: название, количество непустых ячеек, тип значений.
df_res1.info()
Факультет — это категориальный признак. Распечатаем уникальные значения по столбцу department
:
df_res1['department'].unique()
Давайте посчитаем количество студентов по факультетам:
df_res1['department'].value_counts()
Проверим, есть ли студенты, не заполнившие свой факультет:
df_res1['department'].isna().sum()
Их всего лишь 44 человека. Давайте посмотрим на них внимательно:
df_res1[df_res1.department.isna()]
К сожалению, эти не все безответственные товарищи являются виртуальными — только 7 из них не решили ни одной задачи. Но мы всё равно удалим их из нашей таблицы и на всякий случай сбросим индексы:
df_res1.dropna(subset=['department'], axis=0, inplace=True)
df_res1.reset_index(inplace=True)
df_res1.drop(columns=['index'], inplace=True)
Добавим визуализацию количества студентов по факультетам:
df_res1['department'].value_counts().sort_values(ascending=True).plot(kind='barh', figsize=(9, 8))
Теперь нужно сделать удобным анализ информации о задачах.
df_res1["1_быстрая_черепашка"].dtypes
dtype('O')
означает, что у них "объектный тип", а значит (в нашем случае) — строковый str
либо float('NaN')
.
Кроме того, эти ячейки сейчас имеют следующий формат:
+n
— задача сдана (число — это количество посылок в систему)-n
— решение задачи неверное (число — это количество посылок в систему)пустое значение
— задача не решалась
Заменим пустые ячейки и значения с минусом на 0, а затем все значения с плюсом на 1.
df_res1.fillna('-', inplace=True)
df_res1.sample(5) # случайная выборка 5 строк из таблицы
Дли приведения ячеек о сдачах к числовому формату (1 — сдана, 0 — не сдана) определим следующую преобразующую функцию:
def task_is_solved(result):
if result[0] == '+':
return 1
else:
return 0
Применим её ко всем значениям из первых 5 столбцов:
for column in df_res1.columns[:5]:
df_res1[column] = df_res1[column].apply(func=task_is_solved)
df_res1.head()
# импорт модулей
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
Поработаем с несколькими небольшими датасетами¶
# !wget https://raw.githubusercontent.com/hse-ds/iad-intro-ds/master/2022/seminars/grades.csv
df = pd.read_csv('https://raw.githubusercontent.com/hse-ds/iad-intro-ds/master/2022/seminars/grades.csv',
sep=',',
index_col=0)
# вывести голову таблицы
df.head(5)
# вывести хвост таблицы
df.tail()
# выбрать 5 случайных строк
df.sample(n=5)
# выбрать 30% случайных строк
df.sample(frac=0.3)
# выбрать 5 строк с наибольшими значениями в столбцах '3', '4'
df.nlargest(n=5, columns=['3', '4'])
# подсчитать количество уникальных значений в столбце 3
df['3'].nunique()
# подсчитать сколько раз встретилось в столбце '3' каждое уникальное значение
df['3'].value_counts()
# склеить две таблицы по строкам
df1 = df.iloc[:5]
df2 = df.iloc[10:15]
pd.concat([df1, df2])
# склеить две таблицы по столбцам
df1 = df[['hash', '1']]
df2 = df[['3', '4']]
pd.concat([df1, df2], axis=1).head()
DataFrame.merge - объединение таблиц (аналог SQL: JOIN)¶
# таблица с оценками
df_grades = pd.read_csv('https://raw.githubusercontent.com/hse-ds/iad-intro-ds/master/2022/seminars/grades.csv',
sep=',', index_col=0)
df_grades.head()
# # таблица хэшей: ФИО студента <-> хэш
# !wget https://raw.githubusercontent.com/hse-ds/iad-intro-ds/master/2022/seminars/hashes.csv
df_hashes = pd.read_csv('https://raw.githubusercontent.com/hse-ds/iad-intro-ds/master/2022/seminars/hashes.csv')
df_hashes.head()
df_grades.hash.nunique()
df_hashes.hash.nunique()
# присоединить подходящие строки из df_grades к df_hashes
df = pd.merge(df_hashes, df_grades, on='hash', how='left')
print(df.shape)
df.head(10)
# присоединить подходящие строки из df_hashes к df_grades
df = pd.merge(df_hashes, df_grades, on='hash', how='right')
print(df.shape)
df.head(10)
# пересечение таблиц
# в данном случае эквивалентно 'right', т.к. в df_grades нет таких хэшей, которые отсутствуют в df_hashes
df = pd.merge(df_hashes, df_grades, on='hash', how='inner')
print(df.shape)
df.head(10)
# объединение таблиц
# в данном случае эквивалентно 'left', т.к. в df_grades нет таких хэшей, которые отсутствуют в df_hashes
df = pd.merge(df_hashes, df_grades, on='hash', how='outer')
print(df.shape)
df.head(10)
# выбрать из таблицы хэшей только те строки, в которых хэш есть в таблице оценок
# т.е. отобрать тех студентов, которые писали контрольную и были оценены
df = df_hashes[df_hashes.hash.isin(df_grades.hash)]
df = df_hashes[df_hashes['hash'].isin(df_grades.hash)]
print(df.shape)
df.head()
# table_name[condition]
# df_hashes['hash']
# df_hashes.hash
# print(df_hashes.iloc[5:10])
# сколько человек из каждой группы были оценены?
df.Группа.value_counts()
DataFrame.groupby - группировка строк¶
# сгруппировать строки по столбцу '1'
gr = df_grades.groupby(by='1')
gr
# какая средняя оценка (и ее дисперсия) за другие задачи у студентов, получивших конкретную оценку по задаче '1'?
gr.mean()
# дисперсия
gr.var()
# откуда взялись NaN?
gr.get_group(0.2)
Самый знаменитый датасет от Титаника¶
Каждая строчка наборов данных содержит следующие поля:
Pclass — класс пассажира (1 — высший, 2 — средний, 3 — низший);
Name — имя;
Sex — пол;
Age — возраст;
SibSp — количество братьев, сестер, сводных братьев, сводных сестер, супругов на борту титаника;
Parch — количество родителей, детей (в том числе приемных) на борту титаника;
Ticket — номер билета;
Fare — плата за проезд;
Cabin — каюта;
Embarked — порт посадки (C — Шербур; Q — Квинстаун; S — Саутгемптон);
Survived - пассажир выжил или нет.
В поле Age приводится количество полных лет. Для детей меньше 1 года — дробное. Если возраст не известен точно, то указано примерное значение в формате xx.5.
df = pd.read_csv("https://raw.githubusercontent.com/iad34/seminars/master/materials/data_sem1.csv", sep=";")
#типы данных
df.dtypes
#сравним сколько место занимает столбец
df['SibSp'].astype('int64').memory_usage()
df['SibSp'].astype('int8').memory_usage()
df.shape
df.head()
#характеристики датасета
df.describe()
df.describe(include='object')
df['Sex'].value_counts()
df[df.Sex == 'unknown']
#корреляция Пирсона между столбцами = мера линейной зависимости
df.corr(method = 'pearson')
df.corr(method='spearman') # наличие какой-то зависимости (не обязательно линейной) между столбцами
df.head()
df.columns
df.index
# на просмотр может быть выведен только один стобец
Y = df["Survived"]
Y.head()
df[["Survived", "Age"]].head()
# удаление данных одного из столбцов
X = df[df.columns.drop("Survived")]
y = df['Survived']
X = df.drop("Survived", axis=1) # same thing as previous cell
X.shape, df.shape
# выделение части строк
X.loc[[5, 8, 10],['Age','Sex']] # selected by index; select rows
X.iloc[0:5,0:5] # по индексам
# изменение индексации
df.set_index("Name").head()
Анализ данных¶
df.describe()
set(df["Sex"])
len(set(df["Name"]))
df["Sex"].value_counts()
df.pivot_table('PassengerId', 'Sex', 'Survived', 'count').plot(kind='bar', stacked=True)
#Вывод из полученных гистограмм?
df.pivot_table('PassengerId', 'Pclass', 'Survived', 'count').plot(kind='bar', stacked=True)
#Вывод из полученных гистограмм?
df.pivot_table('PassengerId', 'Parch', 'Survived', 'count').plot(kind='bar', stacked=True)
#Вывод из полученных гистограмм?
fig, axes = plt.subplots(ncols=2)
df.pivot_table('PassengerId', ['SibSp'], 'Survived', 'count').plot(ax=axes[0], title='SibSp')
df.pivot_table('PassengerId', ['Parch'], 'Survived', 'count').plot(ax=axes[1], title='Parch')
#Вывод?
df.plot(x="PassengerId", y="Fare", kind="bar")
Обработка и преобразование данных¶
df.head(2)
# преобразуем текстовый признак "Пол" в числовые значения
#df["DecodedSex"] = df["Sex"].map({"male":1, "female":-1, "unknown":0})
def f(x):
if x == 'male':
return 1
elif x == 'female':
return -1
return 0
#df['DecodedSex'] = df['Sex'].apply(lambda x: f(x))
df.head(2)
%%time
df['DecodedSex'] = df['Sex'].apply(f)
df['DecodedSex'] = df['Sex'].map(f)
%%time
df["DecodedSex"] = df["Sex"].map({"male":1, "female":-1, "unknown":0})
# добавим еще одну характеристику для каждого объекта датасета
def fun(age):
return age / 100
df["NewAge"] = df["Age"].apply(fun)
df.head(2)
# то же самое можно сделатьс. помощью лямбда функции
df["NewAge"] = df["Age"].apply(lambda age: age/100)
df.head(2)
df
# выделим фамилию из данных
df["Surname"] = df["Name"].apply(lambda name: name.split(";")[0]) # option1
df["Surname"] = df["Name"].apply(lambda name: name[:name.find(";")]) # p[tion 2]
df["Surname"].value_counts().head()
df.values # df -> numpy.array
# Исследуем возраст пассажиров Титаника
df.groupby("Sex")["Age"].mean()
df.groupby("Sex")["Age"].apply(np.mean)
df.groupby("Sex")["Age"].apply(lambda ages: np.mean(ages)**2)
df.groupby("Survived")["Age"].apply(np.mean)
#группировка по нескольким столбцам и агрегация нескольких полей сразу
df.groupby(["Sex", "Pclass"]).agg(avg=('Age', 'mean'), avg_surv=('Survived', 'mean'))
# .mean -> .count
# Сколько семей больше трех человек?
np.sum(df.groupby("Surname")["Name"].count() > 3)
# Сколько семей, в которых минимальный возраст меньше 10 лет?
np.sum(df.groupby("Surname")["Age"].apply(min) < 10)
# можно выделять объекты с помощью масок
# cоздание маски
(df["Age"]>10) & (df["Age"]<20)
# пассажиры, удовлетворяющие условию
df.loc[(df["Age"]>10) & (df["Age"]<20)]
Доп материалы¶
Задания:¶
- Какова доля семей, в которых минимальный возраст меньше 20 (семьи с детьми)?
# your code here
- Какова доля выживших пассажиров из класса 3? А пассажиров из класса 1?
# your code here
- Сколько пассажиров выжило, а сколько - нет?
#your code here
- Создайте столбец "IsChild", который равен 1, если возраст меньше 20, и 0 иначе. Для пропущенных значений поведение функции может быть произвольным.
# your code here
- Какова доля выживших женщин из первого класса? А доля выживших мужчин из 3 класса?
# your code here