Логотип компании МТС
МТС

Гайд по обработке данных с помощью Pandas: часть первая

Разбираем, как начать работу с Pandas. В этой части самые базовые приёмы: чтение и запись данных, индексирование, агрегация и другие основы.

1953
Обложка поста Гайд по обработке данных с помощью Pandas: часть первая

Pandas — это библиотека на Python, предназначенная для обработки и анализа структурированных табличных данных. С её помощью можно фильтровать, сортировать, агрегировать и преобразовывать данные, а также интегрироваться с различными источниками для чтения и записи. Благодаря такому богатому функционалу, эффективности и удобству использования, Pandas — один из наиболее популярных инструментов в области анализа данных.

Руководитель группы видеоаналитики MTS AI Андрей Дугин рассказывает, как начать работу с Pandas и избежать распространённых ошибок.

Где и для чего применяется Pandas

Pandas скорее академический исследовательский инструмент: для промышленных масштабов производительность у библиотеки относительно низкая, управление памятью не самое эффективное и нет параллелизма. Тем не менее, библиотека отлично подходит дата-сайентистам и аналитикам для прототипирования и исследования гипотез в нересурснозатратных проектах. Её можно применить практически в любой области, где требуется обработка и анализ табличных данных или временных рядов:

  • Предиктивная аналитика для агрегаторов. Например, на сайтах недвижимости. Pandas поможет структурировать массив данных по квартирам: этаж, площадь, количество комнат, год постройки дома и так далее. На этих данных можно строить модели машинного обучения и предсказывать, сколько будет стоить недвижимость.
  • Финансовый анализ. Pandas пригодится для анализа динамики цен акций, котировок валют, индексов и так далее. Это помогает при прогнозировании рыночных трендов, определении рисков и оценке инвестиционных стратегий.
  • Маркетинговый анализ. Проанализировав с помощью Pandas данные о продажах, потребительском поведении, рекламных кампаниях можно определять оптимальные стратегии маркетинга.
  • Исследования в области биоинформатики. Pandas помогает анализировать и обрабатывать биологические данные для выявления закономерностей, например, в болезнях.
  • Обработка данных сенсоров и IoT. С помощью Pandas можно обрабатывать и анализировать данные сенсоров, полученных от устройств IoT, чтобы управлять системами, мониторить и диагностировать их.

Рассмотрим некоторые базовые приёмы работы с Pandas на основе классического датасета Titanic. Его используют студенты, изучающие машинное обучение, для предсказания шансов пассажиров на выживание в зависимости от их пола, возраста, класса каюты и других факторов. Подробнее почитать о значении полей можно в описании датасета на Kaggle.

Начало работы с Pandas

DISCLAIMER

  • Поскольку примеры кода и вывод выполняются в Jupyter Notebook, то для отображения результата нам не обязательно оборачивать выражения в функцию print(), однако это может понадобиться в других IDE.
  • По этой же причине при демонстрации мы иногда не модифицируем исходные данные (не делаем присваивание A = B, а показываем только правую часть выражения; используем inplace=False), а просто выводим результат.

Установка и импорт

Pandas легко установить через стандартный пакетный менеджер Python, используя команду pip install pandas.

После установки следует импортировать саму библиотеку. Поскольку Pandas строится на базе библиотеки NumPy — инструментария для работы с многомерными массивами — для удобства рекомендуется импортировать и её. Она также будет установлена автоматически в процессе установки Pandas.

			import numpy as np
import pandas as pd
		

Структуры хранения данных

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

Структур хранения данных в Pandas две — Series и DataFrame.

  • Series — это одномерный массив данных с метками. Он может хранить различные типы данных, включая числа, строки и произвольные объекты Python. Каждому элементу в Series соответствует метка, доступ к которой можно получить через атрибут index.
			series = pd.Series([1, 2, 3], index=["a", "b", "c"], dtype=np.uint8)
series

		
pandas series
Здесь и далее будет приводиться вывод в Jupyter Notebook
			series.index

		
pandas series index

Есть проводить аналогию со словарём dict, то индекс — это ключи словаря, а сам массив данных — значения, к которым можно получить доступ по ключу:

			series["b"]  # Получаем доступ по метке, как по ключу в словаре
		
pandas series
  • DataFrame — это двумерная структура данных, представляющая собой таблицу с метками для строк и столбцов. Каждый столбец в DataFrame является объектом типа Series. Вместе они формируют двумерную таблицу с общим индексом. В DataFrame присутствуют две оси индексации: index для строк и columns для столбцов. Метки столбцов — это их названия.
			dataframe = pd.DataFrame([[1, "Ivan", 5.0], [2, "Sergey", 4.3], [3, "Dmitry", 4.5]], columns=["#", "Name", "Score"])
dataframe
		
pandas dataframe
			dataframe["Name"]  # Колонка — это объект Series
		
pandas dataframe
			dataframe["Name"].name  # У объекта Series есть собственное имя
		
pandas dataframe

Операции чтения и записи данных

С помощью Pandas можно читать и записывать данные из различных источников: баз данных, файлов в форматах CSV, Excel, JSON и тому подобных. Для каждого типа данных существуют специализированные функции: read_csv()read_excel() и другие вида read_*().

			titanic = pd.read_csv("titanic.csv")
titanic.head()

		
pandas read csv

Можно даже автоматически спарсить таблицу из веб-страницы, указав URL и порядковый номер таблицы:

			from urllib.parse import quote
url = quote("https://ru.wikipedia.org/wiki/Таблица", safe=":/")  # Кодируем кириллицу
pd.read_html(url)[0]  # Берём первую таблицу из списка всех найденных на веб-странице
		
pandas read

Функции чтения принимают много параметров, так можно настраивать все необходимые опции, включая явное задание типов данных или соответствующих функций-конвертеров. Это позволяет предобработать данные или преобразовать их типы к более эффективным ещё на этапе чтения из источника.

Записать данные в файл так же просто, как и прочитать — используйте семейство методов .to_*(), например, .to_excel():

			titanic.to_excel("titanic.xlsx")
		

Первичное исследование данных

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

Первые несколько строк датафрейма можно получить с помощью метода .head(), а последние — .tail():

			titanic.head(5)
		
pandas head

Тип данных object чаще всего соответствует строковым значениям:

			titanic.dtypes
		
pandas object

Pandas позволяет автоматически подобрать наиболее эффективные представления с помощью метода .convert_dtypes():

			titanic.convert_dtypes().dtypes  # Строковые значения теперь представлены типом string, а не object
		
pandas convert

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

			titanic.info()
		
pandas info
Например, в датасете 891 строка, однако количество ненулевых (non-null) значений в колонках Age, Cabin и Embarked меньше, значит, некоторые данные отсутствуют

Некоторую статистику по числовым столбцам можно собрать с помощью метода .describe():

			titanic.describe()
		
pandas describe

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

			titanic["Survived"].value_counts(normalize=True) # Какой процент пассажиров выжил?
		
pandas value counts
			titanic["Pclass"].value_counts(dropna=False)  # Сколько пассажиров было в каждом классе?
		
pandas value counts

Индексирование

Операции обращения к элементам данных называются индексированием. 

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

			titanic.set_index("Name", inplace=True)  # Производим изменения на месте — inplace

		
			titanic.head()
		
pandas head

Элементы индекса называются метками (лейблами):

			titanic.index  # Это вертикальный индекс; для списка используйте titanic.index.tolist()
		
pandas index

Названия колонок — тоже метки:

			titanic.columns  # Это горизонтальный индекс
		
pandas columns

К колонкам можно обращаться по их названиям:

			titanic["Ticket"]
		
pandas index

Если имя колонки удовлетворяет правилам именования переменных в Python и не совпадает с одним из уже существующих методов или атрибутов DataFrame, то к колонке можно обратиться как к атрибуту DataFrame:

			titanic.Ticket;  # То же самое, что titanic["Ticket"]
		

Метод .loc[] есть и у Series, и у DataFrame. Он позволяет обращаться к данным по меткам:

			titanic.loc["Heikkinen, Miss. Laina", "Ticket"]  # Метод объекта DataFrame
		
pandas loc
			titanic["Ticket"].loc["Heikkinen, Miss. Laina"]  # Метод объекта Series
		
pandas loc
			titanic.loc["Heikkinen, Miss. Laina", ["Ticket", "Age"]]  # Можно выбрать несколько строк или колонок
		
pandas loc

Метод .iloc[] работает аналогично, но вместо меток используются номера строк или столбцов:

			titanic.iloc[2, 7]  # Нумерация начинается с нуля
		
pandas iloc
			titanic.iloc[0:3, 2:8]
		
pandas iloc

С помощью .at[] можно обратиться к конкретной ячейке данных — как в Excel:

			titanic.at["Heikkinen, Miss. Laina", "Ticket"]
		
pandas at

При совершении операций над объектами DataFrame и Series Pandas пытается сопоставить элементы с одинаковыми индексами. Рассмотрим для примера последовательность Series с геометрической прогрессией, где каждый следующий элемент больше предыдущего в два раза:

			series = pd.Series(2 ** np.arange(10))
series
		
pandas series

Попытка попарно посчитать отношение следующего элемента к предыдущему приведёт к «странному» результату:

			series[1:] / series[:-1]
		
pandas series

На самом деле мы хотели получить следующее:

			series[1:] / series[:-1].values  # У массива .values уже нет индекса, и элементы сопоставляются просто последовательными парами
		
pandas series

Объединение данных

Для объединения нескольких датафреймов можно использовать близкие по смыслу функции: merge()concat() и join(). Вот несколько базовых примеров:

			df1 = pd.DataFrame({"key": ["A", "B", "C", "D"], "value": [1, 2, 3, 4]})
df1

		
pandas dataframe
			df2 = pd.DataFrame({"key": ["B", "D", "E", "F"], "value": [5, 6, 7, 8]})
df2
		
pandas dataframe
			pd.merge(df1, df2, on="key", how="inner")
		
pandas merge
			pd.concat([df1, df2], axis=0, ignore_index=True)
		
pandas concat
			df1.join(df2, how="inner", lsuffix="_1", rsuffix="_2")
		
pandas join

Агрегация данных

С помощью этих функций и методов можно взглянуть на данные в различных разрезах:

  • groupby() — для группировки и агрегации данных с большой гибкостью;
  • pivot_table() — для создания сводных таблиц с возможностью применения множественных агрегаций;
  • crosstab() — для подсчёта частоты встречаемости категорий.

Рассчитаем средний возраст пассажиров с разбиением по полу:

			titanic.groupby("Sex").Age.mean()
		
pandas groupby

Построим сводную таблицу, рассчитав вероятность выживания в зависимости от пола и класса:

			pd.pivot_table(titanic, values="Survived", index="Sex", columns="Pclass", aggfunc="mean")
		
pandas pivot table

Посчитаем, сколько человек каждого пола было в трёх классах кают:

			pd.crosstab(titanic.Sex, titanic.Pclass)
		
pandas crosstab

Групповые операции

При обработке данных новички часто используют циклы для итераций по строкам или ячейкам. Такой код выполняется медленно. Лучше использовать методы .apply(), .map(), .transform(), которые работают быстрее за счёт векторизации.

			# Функция, которая в зависимости от возраста будет возвращать child/adult/senior
def categorize_age(age):
  if age < 18:
    return"child"
  if age < 60:
    return"adult"
  else:       
    return "senior"

# Создадим новую колонку, применив нашу функцию к существующей
titanic["Age_category"] = titanic["Age"].apply(categorize_age)
titanic[["Age", "Age_category"]].head()
		
pandas titanic

Кстати, такое же разбиение на возрастные группы можно выполнить с помощью функции pd.cut():

			pd.cut(titanic.Age, bins=[0, 18, 60, titanic.Age.max()], labels=["child", "adult", "senior"]).head()
		
pandas cut

Работа с пропущенными данными

В реальных датасетах часть данных может отсутствовать. В библиотеке Pandas эти пропуски обычно представлены как NaN (Not a Number) в ячейках DataFrame или Series. Чтобы с ними работать можно использовать ключевые методы:

  • .isna() — идентифицирует отсутствующие значения и возвращает булевую маску, где True указывает на пропущенные данные;
  • .fillna() — позволяет заполнить отсутствующие значения с помощью указанных значений или методов интерполяции;
  • .dropna() — удаляет строки или столбцы, содержащие отсутствующие значения.

Метод .isna() есть как у DataFrame, так и у Series, а результатом его вызова будет булевая маска того же класса:

			titanic.isna().head()
		
pandas isna
			titanic.loc[titanic.Age.isna()]  # Найдём строки, в которых отсутствует возраст пассажира
		
pandas isna
			titanic.dropna(subset=["Age"], inplace=False) # Удалим строки, в которых отсутствует возраст пассажира
		
pandas dropna

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

Например, можно заменить их средними значениями в столбце:

			titanic.loc[titanic.Age.isna(), "Age"] = titanic.Age.mean()
		

Или заполнить предыдущим, либо последующим значением:

			titanic.Age.fillna(method="ffill", inplace=True)
		
Методы генерации и заполнения отсутствующих значений называются импутингом. В библиотеке scikit-learn есть модуль sklearn.impute, в котором представлены несколько классов импутеров, основанных на различных алгоритмах.

Визуализация данных

Pandas интегрирована с библиотекой Matplotlib — инструментом для создания двумерных и трёхмерных графических представлений данных. В Pandas можно либо явно использовать её функции для визуализации данных, либо применять встроенные методы, такие как .plot() и .hist(). Второй вариант облегчает построение графиков, автоматически определяя наилучший способ визуализации данных.

			import matplotlib.pyplot as plt
titanic.loc[titanic.Sex == "male", "Age"].hist(bins=10);
		
pandas matplotlib

То же самое можно сделать через прямой вызов функций matplotlib:

			plt.hist(titanic[titanic.Sex == "male"].Age, bins=10);
plt.grid(True)
plt.title("Распределение пассажиров-мужчин по возрасту");
plt.xlabel("Возрастной диапазон");
plt.ylabel("Количество человек");
		
pandas matplotlib

Работа с временными рядами

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

Допустим, у нас есть статистика перевозки пассажиров некой авиакомпании. Сгенерируем эти данные по месяцам:

			# Сгенерируем диапазон дат с частотой 1 месяц
date_rng = pd.date_range(start="1949-01-01", end="1960-12-01", freq="MS")

# Создадим соответствующий датафрейм
df = pd.DataFrame(date_rng, columns=["date"])

# Случайным образом сгенерируем колонку с числом пассажиров в каждом месяце
df["passengers"] = np.random.randint(100, 500, size=len(date_rng))

# Сделаем колонку с датами индексной
df.set_index("date", inplace=True)

df.head()
		
pandas dataframe

А теперь рассчитаем среднемесячное количество пассажиров по каждому году:

			df_downsampled = df.resample("A").mean()
		
			plt.figure(figsize=(10, 6))
plt.plot(df.index, df["passengers"], label="Исходные данные")
plt.plot(df_downsampled.index, df_downsampled["passengers"], label="Среднее по годам", linestyle="--")
plt.xlabel("Год")
plt.ylabel("Количество пассажиров")
plt.grid(True)
plt.legend()
plt.show()
		
pandas matplotlib

Применим скользящее окно и рассчитаем среднее количество пассажиров за последние 12 месяцев:

			df_rolled = df.rolling(12).mean()
		
			df_rolled.head(15)
		
pandas rolling

В первых 11 строках по умолчанию появились значения NaN, так как количество проанализированных строк меньше размера окна. Это поведение можно изменить, задав параметр min_periods — минимальное количество строк, для которых определён результат:

			df.rolling(12, min_periods=1).mean().head()
		
pandas rolling

В заключение

В этой части статьи мы рассмотрели, как использовать Pandas для разных базовых задач. Во второй части разберём, как сделать работу с Pandas эффективнее, узнаем, какие есть альтернативы для этой библиотеки и какие источники можно почитать, чтобы ещё лучше в ней ориентироваться. Следите за обновлениями!

1953