11 Полезных функций Pandas, которые вы, возможно, упустили из виду
Я совершенно уверен, что Pandas не нуждается в представлении. В этой статье мы продолжим изучать некоторые полезные функции pandas, о которых вы, возможно, не слышали.
Давайте начинать!
Подготовка
Поскольку мы будем изучать функциональные возможности Pandas на простых примерах, нам не нужен длинный список библиотек для этой задачи:
import pandas as pd
import numpy as np
Теперь мы готовы исследовать некоторые из скрытых жемчужин Pandas!
1. nth
Как следует из названия, этот метод возвращает n-ю строку. Используя соглашение об индексации Python, nth(0)
вернёт первую строку:
df = pd.DataFrame(
data={
"x": ["a", "a", "a", "b", "b", "b", "c"],
"y": [np.NaN, 2, 3, 1, np.NaN, 3, np.NaN],
}
)
df.groupby("x").nth(0)
Хотя этот подход может показаться очень похожим на методы head /tail
, мы можем использовать метод nth
для более гибкого выбора. Например, мы можем использовать его, если нам нужно получить доступ к первой и третьей строкам внутри группы:
df.groupby("x").nth([0, 2])
Ещё одно сходство, которое вы могли бы заметить, заключается в методах first/last
, которые возвращают первое/последнее ненулевое значение внутри каждой группы. Ключевой компонент предыдущего предложения не является нулевым. Используя то же условие группировки, метод first
возвращает следующие значения:
df.groupby("x").first()
Разница в поведении между методом nth
и методами first
/last
хорошо видна для значения a
в столбце x
. Используя первый метод, мы пропускаем недостающее значение в первой строке.
2. pop
Этот простой метод удаляет один столбец из фрейма данных и сохраняет его как новый объект серии.
X = pd.DataFrame(
data={
"x": ["a", "a", "a", "b", "b", "b", "c"],
"y": [np.NaN, 2, 3, 1, np.NaN, 3, np.NaN],
}
)
y = X.pop("y")
print(X.shape)
print(y.shape)
Если мы проверим объект X
после запуска фрагмента, это будет фрейм данных с одним столбцом под названием x
.
3. compare
При манипулировании данными мы часто хотим определить различия между двумя фреймами данных. Чтобы сделать именно это, мы можем использовать метод compare
:
a = pd.DataFrame(
data={
"col_1": [1, 2, 3, 4],
"col_2": [5, 6, 7, 8],
}
)
b = pd.DataFrame(
data={
"col_1": [1, 2, 3, 9],
"col_2": [5, 6, 7, 8],
}
)
a.compare(b)
Выходные данные показывают выявленные различия. Единственное различие возникает в 4-й строке, в которой значение в первом фрейме данных (называемом self) равно 4, в то время как во втором (называемом other) оно равно 9.
Поскольку метод возвращает различия, выполнение следующего фрагмента кода ничего не возвращает:
a.compare(a)
Стоит отметить, что существует довольно много аргументов метода compare
, которые мы можем использовать для изменения поведения метода. Например, мы можем использовать аргумент keep_shape
, чтобы указать, что мы хотим сохранить исходную форму фреймов данных и заменить все идентичные значения NaNs
.
a.compare(b, keep_shape=True)
Если мы дополнительно укажем аргумент keep_equal
, значения NaN
, используемые для обозначения идентичных значений, будут заменены фактическими значениями.
a.compare(b, keep_shape=True, keep_equal=True)
Следует иметь в виду, что мы можем сравнивать только те фреймы данных, которые имеют идентичную маркировку и форму.
4. align
align
– это полезный метод, который мы можем использовать для выравнивания двух фреймов данных по их осям с помощью указанного типа соединения. Вам будет легче понять, что это значит, на примере:
X = pd.DataFrame(
data={
"a": [1, 2, 3, 4, 5, 6],
"b": [2, 3, 4, 5, 6, 7],
"y": [3, 4, 5, 6, 7, 8],
}
)
y = X.pop("y")
X = X.iloc[[0, 3, 5], :]
X
Во-первых, у нас есть фрейм данных с именем X
, из которого мы удаляем один столбец с именем y
. В контексте ML вы могли бы думать о них как о функциях, но это не единственно возможная интерпретация.
Возвращаясь к примеру, затем мы вручную отфильтровали фрейм данных X
, что привело к тому, что объекты больше не были выровнены — серия y
содержит больше наблюдений, чем фрейм данных X
.
Чтобы ещё раз выровнять объекты по их индексам, мы используем метод align
. В этом случае мы хотели бы иметь совпадающие индексы, поэтому мы выбрали внутреннее соединение.
y, X = y.align(X, join="inner")
X.index == y.index
Теперь оба объекта имеют по 3 строки с одинаковыми индексами.
5. to_markdown
Часто мы хотим встроить фрейм данных в отчёт или документ. Мы можем легко сделать это, используя метод to_markdown
.
df = pd.DataFrame(
data={
"x": ["a", "a", "a", "b", "b", "b", "c"],
"y": [np.NaN, 2, 3, 1, np.NaN, 3, np.NaN],
}
)
print(df.to_markdown())
Мы можем использовать аргумент tablefmt
, чтобы выбрать один из более чем 30 доступных форматов таблиц. Для получения полного списка доступных форматов обратитесь к документации. В следующем фрагменте мы создаём один из типов таблиц, совместимых с LaTeX.
print(df.to_markdown(tablefmt="latex"))
6. convert_dtypes
pandas предлагает множество опций для обработки преобразований типов данных. В моей предыдущей статье я объяснил, как использовать определённые типы данных для оптимизации использования памяти. При таком подходе нам приходилось выбирать типы данных вручную.
В качестве альтернативы мы можем использовать метод convert_dtypes
, который преобразует столбцы в (надеюсь) наиболее подходящий тип данных. Чтобы управлять ожиданиями, pandas попытается определить наилучший тип данных для представления данного столбца, что не обязательно означает, что он будет оптимизирован с точки зрения использования памяти. Так что, если проблема в памяти, нам, вероятно, всё равно придется самим указывать типы данных.
Возвращаясь к примеру, давайте сначала определим фрейм данных со столбцами, содержащими различные типы данных. Столбцы также будут содержать некоторые пропущенные значения:
df = pd.DataFrame(
{
"a": [1, 2, 3, 4, 5],
"b": [1, 2, np.nan, 4, 5],
"c": ["x", "y", np.nan, "x", "y"],
"d": pd.Series([True, False, True, True, False], dtype="object"),
"e": [np.nan, 100.5, 200, 200, 100],
"f": ["a", "b", "c", "a", "c"],
}
)
df
Затем давайте проверим типы данных:
df.dtypes
Теперь давайте воспользуемся методом convert_dtypes
в нашем фрейме данных:
df_2 = df.convert_dtypes()
df_2
Проверив выходные данные, мы также можем увидеть, что pandas теперь поддерживает отсутствующие значения для целых столбцов (используя pd.NA
), поэтому их больше не нужно представлять в виде чисел с плавающей точкой.
Давайте ещё раз проверим типы данных. Мы можем видеть, что типы данных были скорректированы с помощью метода convert_dtypes
:
df_2.dtypes
Метод convert_dtypes
также имеет полезные аргументы, которые мы можем использовать для точной настройки его поведения. Например, мы можем использовать аргумент convert_boolean
, чтобы указать, что мы предпочитаем сохранять логические столбцы, закодированные с использованием единиц и нулей вместо значений True/False
.
df_3 = df.convert_dtypes(convert_boolean=False)
df_3
Мы можем видеть, что столбец d
теперь содержит целочисленное представление логических значений.
7. ordered CategoricalDtype
Говоря о типах данных, мы уже знаем, что мы можем использовать тип данных category
для хранения повторяющихся строковых значений более производительным способом, то есть для экономии большого объёма памяти.
Чтобы преобразовать строковый столбец в категориальный, мы можем использовать следующий фрагмент кода:
df["x"].astype("category")
Чтобы сделать ещё один шаг вперёд, мы также можем использовать упорядоченные категориальные типы данных. Например, давайте представим, что мы работаем в компании по производству одежды, и у нас есть изделия 5 размеров.
Чтобы закодировать эту информацию, сохраняя логику размеров, мы можем создать пользовательский тип данных CategoricalDtype
с правильным порядком категориальных значений и указать упорядоченный аргумент как True
. Наконец, мы можем преобразовать столбец во вновь созданный тип данных, используя знакомый метод astype
.
from pandas.api.types import CategoricalDtype
df = pd.DataFrame({
"size": ["XL", "S", "M", "XS", "L"],
"sales": [50, 10, 20, 90, 100]}
)
categories = CategoricalDtype(
["XS", "S", "M", "L", "XL"],
ordered=True
)
df["size"] = df["size"].astype(categories)
df
В чём преимущество использования упорядоченного категориального типа данных? Например, мы можем легко выполнить сортировку по этой категории и получить соответствующую сортировку вместо использования алфавитного порядка имён строк.
df.sort_values(by="size")
Кроме того, мы также можем использовать упорядоченный категориальный тип данных для более релевантной фильтрации. В следующем фрагменте мы хотим сохранить только размеры, превышающие M
:
df[df["size"] > "M"]
8. SparseDtype
Мы уже обсуждали использование категориального типа данных как потенциальный способ оптимизации памяти, используемой pandas. Другой способ сделать это – использовать разреженный тип данных.
Например, у нас могут быть числовые столбцы, содержащие в основном нули. Мы можем значительно сократить потребление памяти, рассматривая такие столбцы как разреженные. И чтобы быть точным, большинство значений не обязательно должны быть нулями, они могут быть представлены NaNs
или любым другим значением. До тех пор, пока это единственное значение часто повторяется.
Под капотом разреженные объекты сжимаются таким образом, что любые данные, соответствующие определенному значению (0, NaN или любому другому, имеющему большинство значений), опускаются. Чтобы сэкономить место, такие сжатые значения фактически не сохраняются в массиве.
Давайте сгенерируем массивный фрейм данных с большинством значений, равных нулю:
df = pd.DataFrame(np.random.randint(0, 100, size=(10000000, 5)))
df[df <= 90] = 0
Затем мы определяем вспомогательную функцию, используемую для оценки потребления памяти фреймом данных:
def memory_usage(df):
return(round(df.memory_usage(deep=True).sum() / 1024 ** 2, 2))
Первоначальное потребление составляет:
memory_usage(df)
Мы можем попытаться уменьшить числовой тип до наименьшего доступного — uint8
.
df_1 = df.astype("uint8")
memory_usage(df_1)
Это привело к сокращению используемой памяти на 88%. В качестве следующего шага давайте используем тип данных sparse
:
df_2 = df.astype(pd.SparseDtype("uint8", 0))
memory_usage(df_2)
Используя разреженный тип данных, мы добились сокращения на 55% по сравнению с предыдущим решением (с использованием типа данных uint8
) и на 94% по сравнению с исходным фреймом данных.
9. crosstab
Агрегирование – отличный способ обобщить огромные объёмы данных во всеобъемлющее и информативное резюме. Одной из функций pandas, используемых для агрегирования данных, является перекрёстная таблица. По умолчанию он подсчитывает вхождения определённых комбинаций значений в столбцах фрейма данных.
Сначала давайте сгенерируем фрейм данных с некоторыми категориальными и числовыми значениями:
N = 1000
df = pd.DataFrame({
"group": np.random.choice(["AA", "BB"], N),
"region": np.random.choice(["a", "b", "c"], N, p=[0.5, 0.3, 0.2]),
"category": np.random.choice(["x", "y", "z"], N, p=[0.3, 0.3, 0.4]),
"sales": np.random.normal(1000, 50, N)
})
df
Используя простейшую форму функции перекрёстной таблицы, генерируется следующая таблица подсчётов значений между столбцами region
и category
:
pd.crosstab(df['region'], df['category'])
Мы можем пойти ещё дальше и агрегировать данные сразу по нескольким категориям:
pd.crosstab(df['region'], [df["group"], df['category']])
Мы также можем отобразить итоговые данные по строкам и столбцам, используя аргумент margins
:
pd.crosstab(df['region'], df['category'], margins=True)
Если нас интересуют не подсчёты, а распределение, мы можем использовать аргумент normalize
для отображения процентов:
pd.crosstab(df['region'], df['category'], normalize=True)
Мы также можем объединить поля и нормализовать аргументы так, чтобы нормализация выполнялась по строкам или столбцам. Чтобы сделать это, мы должны передать index
или columns
в аргумент normalize
, в то время как margins
имеет значение True
.
Наконец, мы также можем использовать crosstab
для агрегирования числовых данных. Используя следующий фрагмент, мы рассчитываем средние продажи по регионам и категориям.
pd.crosstab(
df["region"],
df["category"],
values = df["sales"],
aggfunc = "mean"
).round(2)
10. swaplevel
Честно говоря, я не фанат работы с мультииндексами, так как это всегда приводит к небольшой головной боли. Однако swaplevel
– это один из методов, который может упростить работу с мультииндексами. Что он делает, так это просто меняет местами позиции индексов внутри мультииндекса.
Давайте повторно используем часть кода из примера crosstab
и сгенерируем простую таблицу агрегирования:
df_agg = pd.crosstab([df["group"], df['category']], df['region'])
df_agg
Если мы хотим изменить положение индексов, мы можем использовать метод swaplevels
.
df_agg.swaplevel()
Для более сложных случаев мы можем указать целочисленное расположение индексов, которые мы хотим поменять местами, а также ось (строки или столбцы), по которой мы хотим поменять индексы местами.
Естественно, это был упрощённый пример, и мы могли бы просто поменять местами индексы, изменив порядок столбцов в функции crosstab
. Но я надеюсь, что вы поняли идею 🙂
11. resample
resample
– это очень полезный метод, который мы можем использовать для группировки и агрегирования данных временных рядов. Одним из условий для работы метода является то, что фрейм данных должен иметь DateTimeIndex
.
Сначала мы создаём образец фрейма данных со значением для каждого дня 2023 года:
df = pd.DataFrame(
index=pd.date_range("2023-01-01", "2023-12-31")
)
df["value"] = list(range(len(df)))
df
Затем мы можем агрегировать данные с еженедельной периодичностью и подсчитать, сколько наблюдений находится в каждой ячейке:
df.resample("W").count().head()
Или мы можем легко суммировать значения за каждый месяц (проиндексированные по началу месяца):
df.resample("MS").sum()
По умолчанию resample
закрыта в левой части диапазона. Используя приведённую выше таблицу, это означает, что она суммирует все значения, начиная с “2023-01-01” вплоть до самого последнего момента перед “2023-02-01” (и не включая это значение). Мы можем изменить это поведение, используя аргумент closed
.
Кроме того, resample
очень гибка с точки зрения правил, используемых для агрегирования, и мы можем выполнять повторную выборку с произвольными частотами. Например, мы можем легко агрегировать данные за каждые 4 месяца, используя следующий фрагмент кода:
df.resample("4M").max()
Исходя из опыта, гибкость аргумента правила действительно проявляется и при работе с компонентом времени, например, с данными, регистрируемыми каждый час, минуту или секунду.
Заключение
На сегодня всё! Я надеюсь, что, прочитав эту статью, вы открыли для себя некоторые новые функциональные возможности pandas, которые облегчат ваш анализ. Вы можете найти код, использованный для этой статьи, на моём GitHub.