28 февраля 2022

🐼 25 возможностей Pandas, о которых вы не знали

Разработчик ПО (системы PDM/PLM) с 1993 года, компания "ИНТЕРМЕХ" (www.intermech.ru). В 2020-м успешно закончил курсы "Основы Data Science" (минская IT Academy) Референт-переводчик технической литературы с английского языка.
Новые и малоизвестные возможности Pandas, о которых полезно знать каждому специалисту по Data Science.
🐼 25 возможностей Pandas, о которых вы не знали

Сколько раз вы говорили: «было бы здорово, если бы я мог это сделать в Pandas»?

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

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

Автор <a href="https://www.pexels.com/@introspectivedsgn" target="_blank" rel="noopener noreferrer nofollow">Erik Mclean</a>. Источник <a href="https://www.pexels.com/photo/unrecognizable-man-in-panda-head-sitting-near-car-5199661" target="_blank" rel="noopener noreferrer nofollow">Pexels</a>
Автор Erik Mclean. Источник Pexels

1. ExcelWriter

ExcelWriter – это обобщенный класс для создания файлов Excel (с листами!) и записи в них DataFrame. Допустим, у нас есть два набора данных:

        # Загрузим два набора данных
diamonds = sns.load_dataset("diamonds")
tips = sns.load_dataset("tips")

# Пишем оба в один и тот же файл Excel
with pd.ExcelWriter("data/data.xlsx") as writer:
    diamonds.to_excel(writer, sheet_name="diamonds")
    tips.to_excel(writer, sheet_name="tips")
    

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

2. pipe

Автор <a href="https://www.pexels.com/@leah-kelley-50725" target="_blank" rel="noopener noreferrer nofollow">Leah Kelley</a>. Источник <a href="https://www.pexels.com/photo/grayscale-photo-of-man-holding-tobacco-pipe-192473/" target="_blank" rel="noopener noreferrer nofollow">Pexels</a>
Автор Leah Kelley. Источник Pexels

pipe – одна из лучших функций для проведения очистки данных в Pandas кратким и четким образом. Она позволяет объединять несколько пользовательских функций в одну операцию.

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

        df_preped = (diamonds.pipe(drop_duplicates).
                      pipe(remove_outliers, ['price', 'carat', 'depth']).
                      pipe(encode_categoricals, ['cut', 'color', 'clarity'])
            )
    

Мне нравится, что эта функция похожа на конвейеры Sklearn. Вы можете сделать с ее помощью намного больше – читайте документацию или эту полезную статью.

Больше полезной информации вы найдете на нашем телеграм-канале «Библиотека data scientist’а»

3. factorize

Эта функция – альтернатива LabelEncoder из Sklearn в Pandas:

        # Обратите внимание на [0] в конце
diamonds["cut_enc"] = pd.factorize(diamonds["cut"])[0]

>>> diamonds["cut_enc"].sample(5)

52103    2
39813    0
31843    0
10675    0
6634     0
Name: cut_enc, dtype: int64
    

В отличие от LabelEncoder'а, factorize возвращает кортеж из двух значений: закодированный столбец и список уникальных категорий.

        codes, unique = pd.factorize(diamonds["cut"], sort=True)

>>> codes[:10]
array([0, 1, 3, 1, 3, 2, 2, 2, 4, 2], dtype=int64)

>>> unique
['Ideal', 'Premium', 'Very Good', 'Good', 'Fair']
    

4. explode – Бабах!

 Автор <a href="https://unsplash.com/@joshuas" target="_blank" rel="noopener noreferrer nofollow">Joshua Sukoff</a>. Источник <a href="https://unsplash.com/s/photos/explode" target="_blank" rel="noopener noreferrer nofollow">Unsplash</a>
Автор Joshua Sukoff. Источник Unsplash

explode – это функция с интересным именем (взрыв). Давайте сначала посмотрим пример, а потом объясним:

        data = pd.Series([1, 6, 7, [46, 56, 49], 45, [15, 10, 12]]).to_frame("dirty")

>>> data
    
🐼 25 возможностей Pandas, о которых вы не знали

В столбце dirty есть две строки, в которых хранятся не единственные значения, а списки. Такие данные часто встречаются в опросах, поскольку некоторые вопросы допускают несколько ответов.

        >>> data.explode("dirty", ignore_index=True)
    
🐼 25 возможностей Pandas, о которых вы не знали

explode распространяет данные из ячейки с массивоподобным значением на несколько строк. Установите ignore_index в True, чтобы сохранить числовой индекс по порядку.

5. squeeze

 Автор <a href="https://www.pexels.com/@cottonbro?utm_content=attributionCopyText&amp;utm_medium=referral&amp;utm_source=pexels" target="_blank" rel="noopener noreferrer nofollow">cottonbro</a>. Источник <a href="https://www.pexels.com/photo/close-up-photo-of-sausage-5875701/" target="_blank" rel="noopener noreferrer nofollow">Pexels</a>
Автор cottonbro. Источник Pexels

Еще одна функция с забавным именем – это squeeze (сжатие), используемая в очень редких, но надоедливых случаях. Один из таких случаев – когда запрос подмножества из DataFrame возвращает единственное значение. Рассмотрим следующий пример:

        subset = diamonds.loc[diamonds.index < 1, ["price"]]

>>> subset
    
🐼 25 возможностей Pandas, о которых вы не знали

Хотя результат – всего одна ячейка, он возвращается как DataFrame. Это неудобно, поскольку вам придется еще раз использовать .loc и передать как имя столбца, так и индекс, чтобы получить цену.

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

        >>> subset.squeeze()
326
    

Теперь мы получили скалярное значение. Можно также указать, какую ось нужно убрать:

        >>> subset.squeeze("columns")  # или "rows"

0    326
Name: price, dtype: int64
    

Обратите внимание, что squeeze работает только с DataFrame и Series, имеющими единственное значение.

6. between

 Автор <a href="https://unsplash.com/@jujudreaminx" target="_blank" rel="noopener noreferrer nofollow">Justin Dream</a>. Источник <a href="https://unsplash.com/photos/BxNyZdCbdz0" target="_blank" rel="noopener noreferrer nofollow">Pexels</a>
Автор Justin Dream. Источник Pexels

Это довольно изящная функция для бинарного индексирования числовых признаков по принадлежности диапазону:

        # Выбрать бриллианты с ценой от 3500 до 3700 долларов
diamonds[diamonds["price"]\
      .between(3500, 3700, inclusive="neither")].sample(5)
    
🐼 25 возможностей Pandas, о которых вы не знали

7. T

У всех DataFrame есть простой атрибут T, что значит «транспонирование». Возможно, вы не будете часто его использовать, но я считаю его довольно полезным при выводе DataFrame'ов после метода describe():

        >>> boston.describe().T.head(10)
    
🐼 25 возможностей Pandas, о которых вы не знали

Набор данных о недвижимости Бостона содержит 30 числовых столбцов. Если просто вызвать describe, результирующий DataFrame будет сжат по горизонтали, и сравнить статистику будет трудно. Транспонирование поменяет оси местами, и по горизонтали будут уже статистические параметры.

8. Стилизатор Pandas

А вы знали, что Pandas позволяет вам задавать стили DataFrame'ов?

У них есть атрибут style, открывающий дверь к настройкам и стилям, ограниченным только вашими знаниями HTML и CSS. Я не буду рассказывать обо всем, что вы можете сделать с помощью style, покажу лишь свои любимые функции:

        >>> diabetes.describe().T.drop("count", axis=1)\
                 .style.highlight_max(color="darkred")
    
🐼 25 возможностей Pandas, о которых вы не знали

Мы выделили ячейки, содержащие максимальные значения каждого столбца. Еще один полезный стилизатор – background_gradient, присваивающий ячейкам градиентный фоновый цвет на основе их значений:

        diabetes.describe().T.drop("count", axis=1).style.background_gradient(
    subset=["mean", "50%"], cmap="Reds"
)
    
🐼 25 возможностей Pandas, о которых вы не знали

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

9. Опции Pandas

Как и в Matplotlib, в Pandas есть глобальные настройки, которые можно изменять для настройки поведения по умолчанию:

        >>> dir(pd.options)
['compute', 'display', 'io', 'mode', 'plotting']
    

Эти настройки разделены на 5 модулей. Давайте посмотрим, какие настройки есть в модуле display:

        >>> dir(pd.options.display)

['chop_threshold',
 'max_columns',
 'max_colwidth',
 'max_info_columns',
 'max_info_rows',
 'max_rows',
 ...
 'precision',
 'show_dimensions',
 'unicode',
 'width']
    

В группе display много настроек, но я в основном использую max_columns и precision:

        # Убрать лимит на изображаемое количество столбцов
pd.options.display.max_columns = None

# Показывать только 5 цйфр после запятой
pd.options.display.precision = 5  # избавиться от научной нотации
    

Больше подробностей об этих замечательных настройках можно найти в документации.

10. convert_dtypes

Все мы знаем, что Pandas имеет раздражающую привычку присваивать столбцам тип данных object. Вместо того чтобы назначать типы данных вручную, вы можете использовать метод convert_dtypes, который пытается подобрать наиболее подходящий тип данных:

        sample = pd.read_csv(
    "data/station_day.csv",
    usecols=["StationId", "CO", "O3", "AQI_Bucket"],
)

>>> sample.dtypes

StationId      object
CO            float64
O3            float64
AQI_Bucket     object
dtype: object

>>> sample.convert_dtypes().dtypes

StationId      string
CO            float64
O3            float64
AQI_Bucket     string
dtype: object
    

К сожалению, метод не может разбирать даты из-за нюансов различных форматов их представления.

11. select_dtypes

Функция, которую я использую постоянно – это select_dtypes. Ее функционал очевиден из ее названия: выбор по типам данных. У нее есть параметры include и exclude, позволяющие выбрать столбцы, включая или исключая определенные типы данных. Например, можно выбрать только числовые столбцы, указав np.number:

        # Выбрать только числовые столбцы
diamonds.select_dtypes(include=np.number).head()
    
🐼 25 возможностей Pandas, о которых вы не знали

или, наоборот, исключить их:

        # Исключить числовые столбцы
diamonds.select_dtypes(exclude=np.number).head()
    
🐼 25 возможностей Pandas, о которых вы не знали

12. mask

 Автор <a href="https://www.pexels.com/@pixabay" target="_blank" rel="noopener noreferrer nofollow">Pixabay</a>. Источник <a href="https://www.pexels.com/photo/guy-fawkes-mask-and-red-flower-on-hand-38275/" target="_blank" rel="noopener noreferrer nofollow">Pexels.</a>
Автор Pixabay. Источник Pexels.

Функция mask позволяет быстро заменять значения в тех ячейках, для которых выполняется определенное условие. Предположим, что у нас есть данные опроса людей от 50 до 60:

        # Create sample data
ages = pd.Series([55, 52, 50, 66, 57, 59, 49, 60]).to_frame("ages")

ages
    
🐼 25 возможностей Pandas, о которых вы не знали

Мы будем считать возраст, выходящий за границы от 50 до 60 лет, ошибками ввода (таких ошибок две – 49 и 66 лет), которые заменим на NaN:

        ages.mask(cond=~ages["ages"].between(50, 60), other=np.nan)
    
🐼 25 возможностей Pandas, о которых вы не знали

Таким образом, mask заменяет значения ячеек, не удовлетворяющие cond, значениями other.

13. min и max по строкам и столбцам

Хотя функции min и max широко известны, они имеют еще одно применение, полезное для особых случаев. Давайте рассмотрим следующий набор данных:

        index = ["Diamonds", "Titanic", "Iris", "Heart Disease", "Loan Default"]
libraries = ["XGBoost", "CatBoost", "LightGBM", "Sklearn GB"]

df = pd.DataFrame(
    {lib: np.random.uniform(90, 100, 5) for lib in libraries}, index=index
)

>>> df
    
🐼 25 возможностей Pandas, о которых вы не знали

Этот DataFrame содержит воображаемую точность моделей, полученных в результате четырех видов градиентного бустинга на пяти различных наборах данных. Мы хотим найти лучшую точность на каждом наборе. Вот как элегантно это можно сделать с помощью max:

        >>> df.max(axis=1)

Diamonds         99.52684
Titanic          99.63650
Iris             99.10989
Heart Disease    99.31627
Loan Default     97.96728
dtype: float64
    

Если вы хотите найти максимум или минимум по каждому столбцу, достаточно изменить 1 на 0.

14. nlargest и nsmallest

Иногда вам недостаточно получить максимальное или минимальное значение столбца. Вы хотите получить N максимальных (или минимальных) значений. Для этого пригодятся функции nlargest и nsmallest. Давайте выведем 5 самых дорогих и самых дешевых бриллиантов:

        diamonds.nlargest(5, "price")
    
🐼 25 возможностей Pandas, о которых вы не знали
        diamonds.nsmallest(5, "price")
    
🐼 25 возможностей Pandas, о которых вы не знали

15. idxmax и idxmin

Когда вы вызываете max или min для столбца, pandas возвращает максимальное или минимальное значение. Однако иногда вам нужна позиция этого значения, а не оно само. Для этого используйте функции idxmax и idxmin:

        >>> diamonds.price.idxmax()
27749

>>> diamonds.carat.idxmin()
14
    

Можно также задать axis='columns', при этом функции будут возвращать номер индекса нужного столбца.

16. value_counts с параметром dropna=False

Для нахождения процента пропущенных значений чаще всего используется комбинация isnull и sum и деление на длину массива. Но вы можете сделать то же самое с помощью value_counts, если задать соответствующие аргументы:

        ames_housing = pd.read_csv("data/train.csv")

>>> ames_housing["FireplaceQu"].value_counts(dropna=False, normalize=True)

NaN    0.47260
Gd     0.26027
TA     0.21438
Fa     0.02260
Ex     0.01644
Po     0.01370
Name: FireplaceQu, dtype: float64
    

В этом столбце 47% пустых значений (NaN).

17. clip

 Автор <a href="https://www.pexels.com/@ann-h-45017" target="_blank" rel="noopener noreferrer nofollow">Ann H</a>. Источник <a href="https://www.pexels.com/photo/a-lot-of-paper-clips-2448452/" target="_blank" rel="noopener noreferrer nofollow">Pexels</a>
Автор Ann H. Источник Pexels

Обнаружение и удаление выбросов часто используется в анализе данных. Функция clip позволяет очень легко найти выбросы, выходящие за пределы диапазона и заменить их предельными значениями. Давайте вернемся к примеру с возрастами людей от 50 до 60:

🐼 25 возможностей Pandas, о которых вы не знали

На этот раз мы заменим значения, выходящие за диапазон от 50 до 60, крайними значениями диапазона.

        ages.clip(50, 60)
    
🐼 25 возможностей Pandas, о которых вы не знали

Быстро и эффективно!

18. at_time и between_time

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

        index = pd.date_range("2021-08-01", periods=100, freq="H")
data = pd.DataFrame({"col": list(range(100))}, index=index)

>>> data.head()
    
🐼 25 возможностей Pandas, о которых вы не знали

Функция at_time позволяет выбирать значения по заданным дате или времени. Давайте выделим все строки, соответствующие 15.00:

        data.at_time("15:00")
    
🐼 25 возможностей Pandas, о которых вы не знали

Здорово, правда? А теперь давайте используем between_time, чтобы выделить строки в заданном интервале времени:

        from datetime import datetime

>>> data.between_time("09:45", "12:00")
    
🐼 25 возможностей Pandas, о которых вы не знали

Заметьте, что обе функции требуют DateTimeIndex, и они работают только с временем. Если вы хотите выделить строки в определенном интервале DateTime, используйте between.

19. bdate_range

bdate_range – это функция для быстрого создания индексов TimeSeries с частотой в один бизнес-день.

        series = pd.bdate_range("2021-01-01", "2021-01-31")  # A period of one month

>>> len(series)
21
    

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

20. autocorr

Один из важнейших компонентов анализа временных последовательностей – это изучение автокорреляции переменной. Автокорреляция – это старый добрый коэффициент корреляции, но взятый по сравнению с лагом той же последовательности. Точнее, автокорреляция при lag=k вычисляется следующим образом:

1. Последовательность сдвигается k периодов времени:

        time_series = tips[["tip"]]
time_series["lag_1"] = time_series["tip"].shift(1)
time_series["lag_2"] = time_series["tip"].shift(2)
time_series["lag_3"] = time_series["tip"].shift(3)
time_series["lag_4"] = time_series["tip"].shift(4)
# time_series['lag_k'] = time_series['tip'].shift(k)

>>> time_series.head()
    
🐼 25 возможностей Pandas, о которых вы не знали

2. Рассчитывается корреляция между исходным значением и каждым лагом.

Вместо того чтобы делать все это вручную, вы можете использовать функцию Pandas autocorr:

        # Автокорреляция tip при lag_8
>>> time_series["tip"].autocorr(lag=8)
0.07475238789967077
    

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

21. hasnans

Pandas предлагает метод для простой проверки, содержит ли Series какие-либо пустые значения – атрибут hasnans:

        series = pd.Series([2, 4, 6, "sadf", np.nan])

>>> series.hasnans
True
    

Согласно его документации, атрибут существенно ускоряет производительность. Заметьте, что он работает только для Series.

22. at и iat

Эти два метода доступа к значениям – гораздо более быстрые альтернативы loc и iloc, но с существенным недостатком: они позволяют получить или изменить только одно значение:

        # [index, label]
>>> diamonds.at[234, "cut"]
'Ideal'

# [index, index]
>>> diamonds.iat[1564, 4]
61.2

# Заменить 16541-ю строку столбца price
>>> diamonds.at[16541, "price"] = 10000
    

23. argsort

Эту функцию стоит использовать каждый раз, когда вы хотите выделить индексы, образующие отсортированный массив:

        tips.reset_index(inplace=True, drop=True)

sort_idx = tips["total_bill"].argsort(kind="mergesort")

# Теперь выведем значения `tips` в порядке сортировки по total_bill
tips.iloc[sort_idx].head()
    
🐼 25 возможностей Pandas, о которых вы не знали

24. Метод доступа cat

Общеизвестно, что Pandas позволяет использовать встроенные функции Python на датах и строках с помощью методов доступа вроде dt или str. Кроме того, в Pandas есть специальный тип данных category для категориальных переменных, как показано ниже:

        >>> diamonds.dtypes

carat       float64
cut        category
color      category
clarity    category
depth       float64
table       float64
price         int64
x           float64
y           float64
z           float64
cut_enc       int64
dtype: object
    

Если столбец имеет тип category, над ним можно выполнять несколько специальных функций с помощью метода доступа cat. Например, давайте рассмотрим различные виды огранки бриллиантов:

        >>> diamonds["cut"].cat.categories
['Ideal', 'Premium', 'Very Good', 'Good', 'Fair']
    

Есть и такие функции, как remove_categories, rename_categories и т. д.:

        diamonds["new_cuts"] = diamonds["cut"].cat.rename_categories(list("ABCDE"))

>>> diamonds["new_cuts"].cat.categories
Index(['A', 'B', 'C', 'D', 'E'], dtype='object')
    

Полный список функций, доступных через метод доступа cat, можно посмотреть здесь.

25. GroupBy.nth

Эта функция работает только для объектов GroupBy. После группировки она возвращает n-ю строку каждой группы:

        diamonds.groupby("cut").nth(5)
    
🐼 25 возможностей Pandas, о которых вы не знали

Заключение

Несмотря на то, что новые библиотеки вроде Dask и datatable потихоньку вытесняют Pandas благодаря их сверкающим новым возможностям для обработки огромных массивов данных, Pandas по-прежнему остается самым популярным средством манипуляции данными в экосистеме Data Science на Python.

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

Спасибо за внимание!

***

Материалы по теме

Источники

Комментарии

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ