Преобразование датафреймов

Все курсы > Анализ и обработка данных > Занятие 2

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

Хотя сами по себе эти навыки являются скорее вспомогательными, овладеть ими совершенно необходимо, если вы хотите быстро и эффективно строить модели ML.

Содержание занятия

Откроем ноутбук к этому занятию

Изменение датафрейма

Вернемся к тому датафрейму, с которым мы работали на прошлом занятии.

датафрейм библиотеки Pandas

Посмотрим, как мы можем преобразовать этот датафрейм.

Копирование датафрейма

Метод .copy()

В первую очередь поговорим про важную особенность при копировании датафрейма. Вначале создадим копию датафрейма с помощью простого присвоения этого объекта новой переменной.

Теперь удалим строку с данными про Аргентину, а после этого выведем исходный датафрейм.

копирование датафрейма через запись в переменную

Как вы видите, изменения коснулись и его. По этой причине для создания полноценной копии лучше использовать метод .copy().

метод .copy() библиотеки Pandas

Такой тип копирования (без изменения исходного датафрейма) может быть полезен, например, когда мы хотим взять датафрейм, протестировать какие-то гипотезы, построить одну или несколько моделей, но при этом не трогать исходные данные.

Про параметр inplace

Создадим несложный датафрейм из вложенных списков.

простой датафрейм из списка списков

Как понять, сохраняется ли изменение после применение определенного метода или нет? Если метод выдает датафрейм, изменение не сохраняется.

удаление столбца с помощью метода .drop()
выведем датафрейм

При этом если метод выдает None, изменение постоянно.

удалим столбец с помощью метода .drop() и параметра inplace = True

По этой причине нельзя использовать inplace = True и записывать результат в переменную одновременно.

В этом случае мы записываем None в переменную df.

Столбцы датафрейма

Именование столбцов при создании датафрейма

Создадим список с названиями столбцов на кириллице и транспонированный массив Numpy с данными о странах.

После этого создадим датафрейм с помощью функции pd.DataFrame() с параметром columns, в который передадим названия столбцов.

датафрейм библиотеки Pandas с названиями столбцов на кириллице

Вернем прежние названия столбцов.

Переименование столбцов

Для того чтобы переименовать отдельные столбцы, можно воспользоваться методом .rename(). Внутри этого метода в параметр columns мы передаем словарь, где ключами будут текущие названия столбцов, а значениями соответствующие им новые названия.

метод .rename() в библиотеке Pandas

Тип данных в столбце

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

Изменение типа данных

Преобразовать тип данных столбца можно с помощью метода .astype(). Этот метод можно применить к конкретному столбцу.

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

Посмотрим на результат.

Тип данных category

Обратите внимание на новый для нас тип данных category. Во многом он похож на факторную переменную в R.

Тип category мы рассмотрим более подробно на занятии по кодированию категориальных переменных.

Помимо упомянутых типов данных, нам также знаком объект datetime, который используется для работы с временными рядами. Мы снова обратимся к нему на занятии по очистке данных.

Фильтр столбцов по типу данных

Выбрать столбцы в зависимости от типа содержащихся в них данных можно с помощью метода .select_dtypes(). Включить определенные типы данных можно с помощью параметра include.

метод .select_dtypes() с параметром include

Исключить определенные типы данных можно через exclude.

метод .select_dtypes() с параметром exclude

Добавление строк и столбцов

Добавление строк

Метод ._append() + словарь

Для добавления строк в первую очередь используется метод .append(). С его помощью строку можно добавить из питоновского словаря.

добавление строк: метод .append() + словарь
Метод ._append() + Series

Мы также можем добавить строки в виде объекта Series.

добавление строк: метод .append() + Series
Метод ._append() + другой датафрейм

Новая строка может также содержаться в другом датафрейме.

датафрейм для добавления в качестве строки
добавление строк: метод .append() + датафрейм
Использование .iloc[]

Если вновь вывести наш датафрейм countries, мы увидим, что данные об Испании, Нидерландах и Перу не сохранились.

метод .append(): без записи в переменную или inplace = True изменения не сохранятся

Добавим данные об этих странах на постоянной основе с помощью метода .iloc[] и посмотрим на результат.

использование метода .iloc[] для добавления строк

Обратите внимание, что строки добавились строго на те индексы, которые были указаны в методе .iloc[] (т.е. 5, 6 и 7) и заменили данные, ранее находившиеся на этих индексах (Боливия, Южная Африка и Канада).


Важно! В версии Pandas 2.0.0, которая была опубликована 3 апреля 2023 года, метод .append() был удален, и применение нижнего подчеркивания, хотя и позволяет выполнить присоединение строк, не является удачным решением.

Разработчики рекомендуют использовать метод .concat(). О нем мы поговорим ниже.


Добавление столбцов

Объявление нового столбца

Новый столбец датафрейма можно просто объявить и сразу добавить в него необходимые данные.

создание нового столбца датафрейма через его объявление
Метод .insert()

Добавим столбец с кодами стран с помощью метода .insert().

Обратите внимание, метод .insert() по умолчанию (без перезаписи в переменную или параметра inplace = True) сохраняет результат.

метод .insert() для добавления нового столбца

Теперь рассмотрим несколько способов добавления столбца с рассчитанным значением.

Метод .assign()

Создадим столбец area_miles, в который поместим площадь в милях. Вначале используем метод .assign().

метод .assign() для добавления нового столбца

Удалим этот столбец, чтобы рассмотреть другие методы.

Можно сложнее

Мы можем усложнить код и добиться такого же результата, применив методы .iterrows() и .iloc[].

методы .iterrows() и .iloc[] для добавления нового столбца
Можно проще

При этом конечно есть гораздо более простой способ добавления нового столбца.

расчет значений одного столбца и добавление результата в новый столбец

Удаление строк и столбцов

Удаление строк

Для удаления строк можно использовать метод .drop() с параметрами labels (индекс удаляемых строк) и axis = 0.

удаление строк методом .drop() с параметрами labels и axis = 0

Кроме того, можно использовать метод .drop() с единственным параметром index.

удаление строк методом .drop() с параметром index

Мы также можем в параметр index передать индекс датафрейма через атрибут index.

удаление строк методом .drop() с параметром index и атрибутом датафрейма index

С атрибутом датафрейма index мы можем делать срезы.

удаление строк методом .drop() с параметром index и атрибутом датафрейма index (срез)

Удаление столбцов

Параметры labels (номера столбцов) и axis = 1 метода .drop() позволяют удалять столбцы.

удаление столбцов методом .drop() с параметрами labels и axis = 1

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

удаление строк методом .drop() с параметром columns

Через атрибут датафрейма columns мы можем передать номера удаляемых столбцов.

удаление столбцов методом .drop() с параметром columns и атрибутом датафрейма columns

Наконец удалим пятую строку и несколько столбцов разобранными выше способами и сохраним изменения.

библиотека Pandas: удаление строк и столбцов датафрейма

Удаление по многоуровнему индексу

Давайте посмотрим, как удалять строки и столбцы в датафрейме с многоуровневым (иерархическим) индексом. Вначале вновь создадим соответствующий датафрейм.

библиотека Pandas: датафрейм с многоуровневым (иерархическим) индексом
Удаление строк

Вначале обратимся к строкам и удалим азиатский регион. Для этого воспользуемся методом .drop(), которому передадим параметр labels = ‘Asia’. Кроме того, укажем, что удаляем именно строки (axis = 0) и что Азия находится в индексе под названием region (т.е. level = 0).

удаление строк по многоуровневому индексу через параметры labels, level, axis = 0

Кроме того, строки можно удалять с помощью параметра index и указанием через level, по какому столбцу индекса мы будем искать удаляемую строку.

удаление строк по многоуровневому индексу через параметры index и level
Удаление столбцов

Удаление столбцов датафрейма с многоуровневым индексом происходит аналогично строкам. Передадим методу .drop() параметры labels, level и axis = 1 для удаления столбца по его наименованию (labels) на нужном нам уровне (level) индекса.

удаление столбцов по многоуровневому индексу через параметры labels, level, axis = 1

Для удаления столбцов можно использовать параметр columns с указанием соответствующего уровня индекса (level) столбцов.

удаление столбцов по многоуровневому индексу через параметры columns и level

Обратите внимание, что удалению столбцов не помешал тот факт, что они находятся в разных индексах первого (level = 0) уровня, а именно city находится в names, а population — в data.

Применение функций

Библиотека Pandas позволяет использовать функции для изменения данных в датафрейме.

применение функций к датафрейму библиотеки Pandas

Метод .map()

Предположим, что мы хотим явно прописать мужской (male) и женский (female) пол в наших данных. В этому случае можно воспользоваться методом .map(). Вначале создадим карту (map) того, как преобразовать существующие значения в новые.

Применим эту карту к нужному нам столбцу датафрейма.

метод .map() библиотеки Pandas + словарь

Те значения, которые в карте не указаны (в четвертой строке была ошибка, пол был помечен цифрой два), превращаются в NaN (not a number), пропущенное значение.

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

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

В метод .map() мы можем передать и lambda-функцию.

метод .map() библиотеки Pandas + lambda-функция

Удалим только что созданный столбец age_group.

Вместо lambda-функции, например, для более сложных преобразований, можно испольвовать обычную собственную функцию.

Применим эту функцию к столбцу age.

метод .map() библиотеки Pandas + собственная функция

Функция np.where()

Точно такой же результат мы можем получить, применив функцию np.where() библиотеки Numpy к нужному нам столбцу.

применение функции np.where() к датафрейму библиотеки Pandas

Замечу, что такой способ превращения количественных данных в категориальные называется binning или bucketing. О нем мы подробно поговорим на занятии по преобразованию данных.

Метод .where()

Метод .where() библиотеки Pandas действует немного иначе. В нем мы прописываем условие, которое хотим применить к отдельному столбцу или всему датафрейму.

  • Если условие выполняется (т.е. оценивается как True), мы сохраняем текущее значение датафрейма.
  • Если условие не выполняется (False), то значение заменяется на новое, указанное в методе .where().

Рассмотрим применение этого метода на примерах.

В примере выше возраст тех, кто не моложе 18 (True), остался без изменений. Для остальных (False) значение изменилось на пропущенное (параметр other). Обратите внимание, что тип данных этого столбца превратился во float. Это связано с тем, что в столбце появились пропущенные значения.

библиотека Pandas: датафрейм из чисел
применение метода .where() библиотеки Pandas

Здесь можно отметить два интересных момента:

  • Мы применили метод .where() ко всему датафрейму
  • В качестве значения, на которое нужно заменить текущее при False, мы использовали сами значения датафрейма, но со знаком минус ( -nums)

Перейдем к методу .apply().

Метод .apply()

Применение функции с аргументами

В отличие от .map(), метод .apply() позволяет передавать именованные аргументы в применяемую функцию.

метод .apply(), применение функции с именованными аргументами
Применение к столбцам

В метод .apply() можно передать уже имеющуюся в Питоне функцию, например, из библиотеки Numpy.

метода .apply(): применение функции к столбцам
Применение к строкам

Теперь поработаем со строками. Создадим функцию, которая считает индекс массы тела (body mass index, BMI) на основе веса и роста человека.

Теперь применим эту функцию к каждой строке и сохраним результат в новом столбце.

метод .apply(): применение функции к строкам

Метод .applymap()

Метод .applymap() позволяет применять функции с именованными аргументами ко всему датафрейму (метод .apply() применяется только к строкам или столбцам). Рассмотрим несложный пример.

простой числовой датафрейм библиотеки Pandas
метод .applymap(): применение функции ко всему датафрейму

Метод .pipe()

Метод .pipe(), как следует из его названия, позволяет создать pipeline и последовательно применить несколько функций к датафрейму. Вновь создадим исходный датафрейм с параметрами нескольких людей.

датафрейм перед применением метода .pipe()

Объявим несколько функций, которые мы могли бы применить к датафрейму.

Последовательно применим эти функции с помощью нескольких методов .pipe().

Посмотрим на результат и кроме того убедимся, что исходный датафрейм не изменился.

датафрейм после применения метода .pipe()
исходный датафрейм

Перейдем к следующему разделу, который посвящен соединению датафреймов.

Соединение датафреймов

Рассмотрим, как мы можем соединить два датафрейма с помощью функций/методов pd.concat(), pd.merge() и .join(). Начнем с функции pd.concat().

pd.concat()

В качестве примера возьмем информацию о стоимости канцелярских товаров в двух магазинах.

Выведем результат.

датафрейм с информацией о стоимости товаров в первом магазине
датафрейм с информацией о стоимости товаров во втором магазине

Соединение «один на другой»

В первую очередь мы можем совместить два датафрейма, поставив их «один на другой».

pd.concat(), axis = 0

Как вы видите, индекс не обновился.

pd.concat(), axis = 0, ignore_index = True

Именно таким способом предлагается добавлять новые строки вместо удаленного из библиотеки метода .append(). При этом здесь есть нюанс, в функцию pd.concat() нельзя передать словарь, только объекты Series и DataFrame.

При соединении датафреймов мы можем создать многоуровневый (иерархический) индекс. Например, создадим отдельную группу для товаров первого магазина (s1) и товаров второго (s2).

pd.concat(), axis = 0, многоуровневый индекс строк

Посмотрим на созданный индекс.

Выведем первую запись в первой группе.

Соединение «рядом друг с другом»

Датафреймы можно расположить «рядом друг с другом».

pd.concat(), axis = 1, многоуровневый индекс столбцов

Выберем вторую группу (второй магазин) с помощью метода .iloc[].

pd.concat() + .iloc[]

Полученный через соединение результат и в целом любой датафрейм можно транспонировать.

библиотека Pandas, транспонирование датафрейма

Итак, pd.concat() выполняет простое «склеивание» датафреймов по вертикали или по горизонтали. Теперь посмотрим, что делать, если датафреймы нужно соединить по определенному столбцу.

pd.merge() и .join()

Возьмем три несложных датафрейма.

В первом содержатся оценки студентов ВУЗа по математике (по 100-бальной шкале).

оценки студентов по математике

Во втором указано, по какой программе (бакалавр или магистр) учатся студенты.

учебные программы

В третьем содержатся данные об оценках по информатике.

оценки студентов по информатике

Left join

Предположим, что мы хотим объединить данные об оценках студентов по математике с информацией о том, по какой программе они учатся. Для этого можно воспользоваться соединением слева (left join) через функцию pd.merge().

библиотека Pandas, left join

Точно такой же результат можно получить, применив метод .join(). По умолчанию, он как раз выполняет соединение слева (left join) по индексу (index-on-index).

метод .join() библиотеки Pandas

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

Например, соединим датафреймы math и cs по столбцу name для того, чтобы увидеть оценки студентов и по математике, и по информатике. В группе по математике не учатся Ольга и Евгений (они изучают только информатику), а в группе по информатике не учатся Елена и Антон (им ближе математика). Посмотрим, что произойдет после left join.

left join: главным является датафрейм слева

Как вы видите, Pandas сохранил всех математиков (левый датафрейм), поставив NaN для тех из них, кто не изучается информатику. В правом датафрейме (информатика) сохранились только те студенты, которые также изучают и математику.

Концепцию соединения по определенному столбцу можно также объяснить через теорию множеств. На диаграмме Эйлера left join будет выглядеть следующим образом.

left join на диаграмме Эйлера

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

Left excluding join

Левое исключающее соединение (left excluding join, left anti-join) предполагает, что мы оставляем только те наблюдения, которые есть в левом датафрейме и исключаем записи только правого датафрейма, а также записи, которые содержатся в обоих.

left excluding join: сохраняем наблюдения только левого датафрейма

На Питоне вначале выполним левое соединение и с помощью параметра indicator = True создадим служебный столбец _merge, в котором указано, в каком из датафреймов присутствует каждая строка.

left excluding join: служебный столбец _merge

Как мы видим, Елена и Антон присутствуют только в левом датафрейме, Андрей и Татьяна — в обоих. Теперь с помощью метода .query() выберем только записи из левого датафрейма и удалим столбец _merge.

left excluding join: результат с .query() и без _merge

Ожидаемо, в столбце cs_score появились NaN (у тех, кто учит только математику, не может быть оценок по информатике).

Также обратите внимание на обратный слэш (\), который позволяет перенести код на следующую строку.

Right join

Правое соединение (right join) — зеркальная операция по отношению к левому соединению. Мы выбираем те записи, которые содержатся только в правом или в обоих датафреймах.

right join: записи, которые есть только в правом или в обоих датафреймах
right join: результат

В результате остались те, кто учит информатику, а также одновременно и математику и информатику.

Right excluding join

Правое исключающее соединение (right excluding join, right anti-join) соответственно зеркально операции с левой стороны. Мы берем наблюдения, которые есть только в правом датафрейме.

right excluding join: сохраняем наблюдения только правого датафрейма

На Питоне мы вначале выполним правое соединение с параметром indicator = True и посмотрим, в каком из датафреймов содержится каждое наблюдение.

right excluding join: служебный столбец _merge

Теперь воспользуемся методом .query() и оставим записи, которые есть только в правом датафрейме. После этого удалим столбец _merge.

right excluding join: результат с .query() и без _merge

Outer join

Внешнее соединение (outer join) сохраняет все строки обоих датафреймов.

outer join: сохраняем все строки обоих датафреймов

Сразу посмотрим на код на Питоне.

outer join: результат соединения датафреймов

Как мы видим, при таком соединении мы получили данные по всем студентам по обоим предметам. Одновременно, ожидаемо, для студентов только математики или только информатики не изучаемый ими предмет заполнен пропусками.

Full excluding join

Полное исключающее соединение (full excluding join, full anti-join) сохраняет только те наблюдения, которые есть в левом либо правом датасетах, но не в обоих.

full excluding join: наблюдения либо левого, либо правого датасетов

Воспользуемся параметрами how = ‘outer’ и indicator = True. Так мы найдем те строки, которые есть только в левом датафрейме (left_only), только в правом датафрейме (right_only) и в обоих (both).

full excluding join: служебный столбец _merge

Оставим только те записи, которых нет в обоих датафреймах.

full excluding join: результат соединения датафреймов

Inner join

Внутреннее соединение (inner join) сохраняет только те наблюдения, которые есть в обоих датафреймах.

inner join: наблюдения одновременно обоих датафреймов

В нашем случае это те студенты, которые учатся и на курсе по математике, и на курсе по информатике.

inner join через pd.merge() с указанием параметра how = 'inner'

Обращу ваше внимание на то, что по умолчанию pd.merge() выполняет именно внутренне соединение.

inner join через pd.merge() с параметрами по умолчанию

Здесь нужно быть аккуратным, потому что если левый датафрейм был главным (его записи должны быть сохранены в обязательном порядке), использование pd.merge() с параметром по умолчанию (how = ‘inner’) вызовет потерю данных (лучше использовать left join).

Соединение датафреймов и дубликаты

Также замечу, что если в ваших данных есть дубликаты (повторяющиеся значения), нужно быть особенно внимательным при выборе типа соединения.

Первый пример

Создадим два датайфрейма: один с наименованием товара, другой — с ценой.

датафрейм с наименованием товара
датафрейм с ценой товара

Выполним left join по столбцу code (код товара).

left join данных с повторяющимися значениями

В результате мы получили данные о наименовании и цене товаров и так как у нас есть две цены для холодильника (и мы не знаем какая из них верная), оба этих значения вошли в финальный датафрейм. Вместо цены телевизора ожидаемо появился NaN.

Нам лишь останется разобраться с повторяющимся значением, однако этим мы займемся на занятии по очистке данных.

Если бы мы выбрали, например, правое соединение, то часть данных была бы потеряна.

right join данных с повторяющимися значениями
Второй пример

Теперь возьмем случай, когда повтор встречается в левом, а не правом датафрейме.

В первом датафрейме содержится информация о результатах экзамена с фамилией экзаменатора, идентификатором студента и оценкой.

преподаватели и оценки за экзамен

Во втором, идентификатор студента и его или ее имя.

идентификаторы и имена студентов

Как вы видите, студент с идентификатором 101 (Андрей) сдавал экзамен двум преподавателям, Погорельцеву и Иванову. В данном случае дубликат сохранится при любом типе соединения left join, right join, outer join, inner join.

соединение данных с дубликатами в левом датафрейме

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

Кроме того замечу, что маркеры _x и _y, которые автоматически выставляются, если в датафреймах есть столбцы с одинаковыми названиями, можно изменить с помощью параметра suffixes = ('_x', '_y').

Cross join

Перекрестное соединение (cross join) по своей сути представляет собой декартово произведение. В этом случае каждому элементу первого датафрейма ставится в соответствие каждый элемент второго.

cross join: датафрейм с символами
cross join: датафрейм с числами

С помощью параметра how = ‘cross’ мы можем поставить как значению x, так и значению y первого датафрейма каждый из элементов второго.

cross join: декартово произведение

Для сравнения соединим датафреймы с помощью right join.

сравнение cross join и right join

pd.merge_asof()

В некоторых случаях бывает нужно соединить датафреймы не по точному значению столбцов, а по ближайшему. Для этого существует функция pd.merge_asof(). Рассмотрим пример, который приведен в документации⧉ к этой функции.

В первом будет содержаться информация о сделках, совершенных с ценными бумагами.

информация о сделках

Во втором, котировки ценных бумаг в определенный момент времени.

котировки ценных бумаг

Функция pd.merge_asof() выполняет левое соединение, а это значит, что мы будем ориентироваться на левый датафрейм trades. Вначале посмотрим на код.

функция pd.merge_asof()

Разберем приведенный код более подробно.

  • Строкам 0 и 1 левого датафрейма trades (тикер MSFT, Microsoft) мы можем поставить в соответствие строки 1 и 2 датафрейма quotes. В первом случае, у нас будет полное совпадение по времени (30 миллисекунд), во втором, разница будет находится в пределах допустимого интервала в 10 миллисекунд (38 — 30 = 8 миллисекунд).
  • Хотя время в строке 3 датафрейма quotes ближе к строке 1 в trades (41 и 38 миллисекунд соответственно), алгоритм соединения по умолчанию ищет совпадения в предыдущих, а не будущих наблюдениях.
  • Строкам 2 и 3 левого датафрейма соответствует строка 4 правого (полное совпадение).
  • Строке 4 левого датафрейма не нашлось совпадения в правом датафрейме (хотя тикер AAPL, Apple, в нем есть и время отличается всего на одну миллисекунду) опять же, потому что алгоритм искал совпадения в предыдущих, а не будущих периодах.

Изменим некоторые параметры фунции pd.merge_asof().

  • Во-первых, уменьшим интервал до пяти миллисекунд.
  • Во-вторых, разрешим алгоритму искать совпадения как в предыдущих, так и в будущих периодах.
функция pd.merge_asof() с измененными параметрами

Что изменилось?

  • Строке 1 левого датафрейма теперь соответствует строка 3 правого (она ближе по времени)
  • Для тикера AAPL (строка 4 левого датафрейма) теперь нашлось соответствие (строка 5 правого)

Перейдем к разделу, посвященному группировке показателей.

Группировка

В библиотеке Pandas можно группировать данные, а затем, например, рассчитывать какие-либо статистические показатели внутри каждой группы.

Метод .groupby()

Вначале рассмотрим метод .groupby(), который используется для группировки данных. Вновь в качестве примера используем датасет «Титаник».

датасет "Титаник": метод .groupby()

Сам по себе метод .groupby() создает объект DataFrameGroupBy.

Как и у любого объекта в Питоне, в DataFrameGroupBy есть атрибуты и методы. Например, воспользуемтся атрибутом ngroups, чтобы узнать на сколько групп были разбиты данные по столбцу Sex.

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

Метод .size() выдает количество элементов в каждой группе.

Метод .first() выдает первые встречающиеся наблюдения в каждой из групп.

датасет "Титаник": метод .groupby() и метод .first()

Обратите внимание, результатом этого метода будет уже датафрейм. Использование метода .get_group() позволяет выбрать наблюдения только одной группы.

датасет "Титаник": метод .get_group()

Агрегирование данных

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

Статистика по столбцам

Например, мы можем рассчитать статистические показатели для каждой группы по каждому столбцу. Рассмотрим на примерах.

Вначале выведем статистику по одному столбцу.

В данном случае мы последовательно применили несколько методов: сформировали объект DataFrameGroupBy, выбрали столбец Age, нашли медиану в каждой группе по этому столбцу и округлили результат. Рассчитаем статистику по нескольким столбцам.

датасет "Титаник": агрегирование данных (два столбца)

Если не указывать конкретные столбцы, статистический показатель будет рассчитан для всего датафрейма.

датасет "Титаник": агрегирование данных (все столбцы)

Мы также можем группировать по нескольким признакам.

датасет "Титаник": группировка по нескольким признакам

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

Значение атрибута ngroups библиотека Pandas считает по подгруппам.

Метод .agg()

Метод .agg() позволяет рассчитать сразу несколько статистических показателей. Применим этот метод к одному столбцу (Sex) и найдем максимальное и минимальное значения, количество наблюдений, а также среднее арифметическое и медиану.

метод .agg()

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

переименование столбцов при группировке

Метод .agg() можно применить к нескольким столбцам.

метод .agg(): несколько столбцов

Кроме того, мы можем применить метод .agg() ко всем столбцам одновременно.

применение метода .agg() ко всем столбцам одновременно

В качестве параметра метод .agg() можем принимать и объявленную нами функцию.

метод .agg() и собственная функция

Преобразование данных

Мы уже научились применять функции для преобразования данных в датафрейме. Агрегирование позволяет выполнять такие преобразования отдельно внутри каждой группы. Вначале объявим несложную lambda-функцию, которая стандартизирует данные.

При стандартизации (standartization) из каждого значения мы вычитаем среднее арифметическое и делим на среднее квадратическое отклонение. В результате среднее значение становится равно нулю, а СКО — единице.

Воспользуемся методом .apply() для применения lambda-функции к сгруппированному столбцу Age.

Мы также можем найти агрегированную статистику по группе (например, среднее арифметическое).

агрегированная статистика по группе, параметр axis = 0

Примечание. Обратим внимание, что для расчета среднего арифметического по столбцам нужно указывать именно axis = 0. В других методах, например, в методе .drop() такое значение аргумента подразумевает работу со строками.

Логика разработчиков в данном случае заключается в том, что при axis = 0 мы применяем np.mean() вдоль индекса (т.е. по всему столбцу), а это, как мы знаем, нулевая ось. Если же мы хотим применить к строке, то нужно идти вдоль столбца, axis = 1.

агрегированная статистика по группе, параметр axis = 'index'

Фильтрация

Сгруппированные данные можно фильтровать. Например, найдем среднее арифметическое возраста внутри каждого из классов каюты.

фильтрация сгруппированных данных

Теперь предположим, что мы хотим оставить только те классы кают, в которых средний возраст пассажиров составляет не менее 26 лет (то есть первый и второй классы). Для этого выполним следующие действия:

  • Применим метод .filter() к сгруппированным по классу данным
  • Этому методу передадим lambda-функцию, которая отберет те строки, в которых среднегрупповое значение возраста составляет не менее 26 лет.
примение метода .filter() к сгруппированным данным

Убедимся, что у нас действительно осталось только два класса.

Сводные таблицы

Сводные таблицы (pivot tables) — это еще один способ группировки данных. Принцип сводных таблиц проще всего продемонстрировать на примере.

Подготовка данных

Скачаем и подгрузим в сессионное хранилище необходимые данные.

Взглянем на содержание датасета.

датасет cars

В этом разделе мы будем использовать данные о предлагаемых к продаже автомобилях в США и Канаде. Для каждого автомобиля мы можем посмотреть цену (price), марку (brand), модель (model), год выпуска (year), требует ли автомобиль ремонта (title_status), пробег (mileage), цвет кузова (color), штат (state) и страну (country) продажи.

Группировка по строкам

Теперь предположим, что нас интересуют не отдельные автомобили, а статистика по маркам. Воспользуемся функцией pd.pivot_table().

В индексе (строках) мы разместим названия марок (параметр index), а в столбцах будем рассчитывать агрегированные показатели количественных признаков (параметр values).

функция pd.pivot_table()

Добавим параметр aggfunc и укажем, какую функцию использовать при агрегации.

функция pd.pivot_table() с параметрами values и aggfunc

Для каждого столбца (например, цены) можно рассчитать несколько статистических показателей. Кроме того, мы можем использовать не только готовые, но и собственные функции.

функция pd.pivot_table() с применением встроенных и собственных функций

Возможна группировка по нескольким признакам.

функция pd.pivot_table(): группировка по нескольким признакам

Группировка по строкам и столбцам

В отличие от метода .groupby() сводные таблицы позволяют выполнять группировку по двум измерениям. Часть признаков группируется по строкам, часть — по столбцам.

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

  • в параметр index мы передадим значение brand;
  • в columns — title_status;
  • нам также нужно указать, откуда мы будем брать значения (values) для заполнения таблицы, в нашем случае это цена (price);
  • в качестве функции при агрегации (aggfunc) используем медиану (median).
функция pd.pivot_table(): группировка по строкам и столбцам

Про title_status. Говоря упрощенно, если автомобиль относится к категории clean vehicle, он не был в серьеных ДТП и его можно застраховать. Если же это salvage vehicle, то автомобиль поврежден настолько, что его нельзя эксплуатировать на дорогах общего пользования и на него нельзя купить страховку.

При желании мы можем добавить еще один показатель и транспонировать датафрейм.

функция pd.pivot_table(): группировка по строкам и столбцам, несколько метрик и транспонирование датафрейма

Рассмотрим некоторые дополнительные возможности.

Дополнительные возможности

Используя метод .style.background_gradient(), мы можем маркировать данные с помощью цвета.

метод .style.background_gradient()

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

Если же в получившейся сводной таблице имеются пропущенные значения, мы также можем выделить их с помощью, например, желтого цвета через метод .style.highlight_null().

метод .style.highlight_null()

Данные сводных таблиц можно выводить не только с помощью чисел, но и графически. Например, посчитаем количество автомобилей (aggfunc = ‘count’) со статусом clean и salvage (columns = ‘title_status’), сгруппированных по маркам (index = ‘brand’). Затем выведем первые три марки и применим метод .plot.barh().

метод .plot.barh()

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

Если применить к сводной таблице метод .unstack(), то мы как бы лишаем ее второго измерения, группировки по столбцам. Остается только группировка по строкам (как в методе .groupby).

Для последующих примеров выберем исключительно автомобили марки «БМВ».

датасет cars, только автомобили "БМВ"

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

группировка по двум признакам, но только по строкам
когда группировка выполняется только по строкам, мы можем получить аналогичный результат с помощью метода .groupby()

Метод .query() позволяет отфильтровать данные.

функция pd.pivot_table() и метод .query()

Наконец, применим метод .style.bar() и создадим встроенную горизонтальную диаграмму.

метод .style.bar()

Подведем итог

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

Вопросы для закрепления

Вопрос. Для чего нужен метод .copy()?

Посмотреть правильный ответ

Вопрос. Какой риск существует при соединении датафреймов с помощью pd.merge() и .join()?

Посмотреть правильный ответ

Вопрос. Чем метод .groupby() отличается от функции pd.pivot_table()?

Посмотреть правильный ответ

На следующем занятии мы начнем изучать процесс исследовательского анализа данных. В частности, мы познакомимся с классификацией данных и задачами EDA.