Занятие 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