В процессе обработки и анализа данных их значения могут преобразовываться. При этом можно оперировать с конкретными элементами или же применять действия одновременно ко всем в столбце или строке. Последний вариант является более предпочтительным в использовании, так как увеличивает производительность при обработке больших объемов данных.
В этом уроке мы познакомимся с методами, которые позволяют уйти от поэлементных операций и применять сценарии обработки ко всему набору данных.
Преобразования данных методом apply()
Метод apply()
— это инструмент для преобразования объекта DataFrame
. аpply()
можно применять как к одному столбцу, так и к нескольким.
Разберем каждую ситуацию подробнее.
apply()
для преобразования одного столбца
Рассмотрим пример на данных о кликах на сайте с четырьмя магазинами. Допустим, произошла техническая ошибка, и по первому магазину количество кликов завышено на 50 штук. Исправим это с помощью apply()
. Подгрузим данные:
import pandas as pd
df_clicks = pd.read_json('./data/Cite_clicks.csv', index_col=0)
print(df_clicks.head())
# => SHOP1 SHOP2 SHOP3 SHOP4
# day
# 1 319.0 -265.0 319.0 328.0
# 2 292.0 274.0 292.0 301.0
# 3 283.0 301.0 274.0 283.0
# 4 328.0 364.0 328.0 NaN
# 5 391.0 355.0 373.0 337.0
Уменьшим значения продаж для первого магазина на 50 пунктов:
df_clicks['SHOP1'] = df_clicks['SHOP1'].apply(lambda x: x - 50)
print(df_clicks.head())
# => SHOP1 SHOP2 SHOP3 SHOP4
# day
# 1 269.0 -265.0 319.0 328.0
# 2 242.0 274.0 292.0 301.0
# 3 233.0 301.0 274.0 283.0
# 4 278.0 364.0 328.0 NaN
# 5 341.0 355.0 373.0 337.0
В аргументах apply()
находится лямбда-функция языка Python. У нее нет названия, и она используется только в конкретном участке кода. В нашем случае лямбда-функция принимает на вход параметр x
и возвращает x - 50
. Каждое значение в столбце — это вход указанной функции. Новый столбец формируется из ее выходных значений.
Теперь представим, что появилась дополнительная информация. Количество кликов у магазина 2 завышено на 50 штук, если кликов не более 200, и на 100, если кликов более 200. Попробуем исправить и эту ошибку:
def correct_clicks(x):
if x < 200:
return x - 50
else:
return x - 100
df_clicks['SHOP2'] = df_clicks['SHOP2'].apply(correct_clicks)
print(df_clicks.head())
# => SHOP1 SHOP2 SHOP3 SHOP4
# day
# 1 269.0 -315.0 319.0 328.0
# 2 242.0 174.0 292.0 301.0
# 3 233.0 201.0 274.0 283.0
# 4 278.0 264.0 328.0 NaN
# 5 341.0 255.0 373.0 337.0
Здесь вместо лямбда-функции в аргументах метода apply()
отдельная функция correct_clicks()
. Она принимает на вход каждый элемент столбца и выполняет над ним описанное выше преобразование.
Теперь мы узнаем, что у второго столбца порог 200 кликов — это порог между разницей в 50 или 100, у третьего этот порог равен 150 и у четвертого — 250. Выполним параметризацию функции correct_clicks()
:
def correct_clicks(x, threshold):
if x < threshold:
return x - 50
else:
return x - 100
df_clicks['SHOP3'] = df_clicks['SHOP3'].apply(lambda x: correct_clicks(x, 150))
df_clicks['SHOP4'] = df_clicks['SHOP4'].apply(lambda x: correct_clicks(x, 250))
print(df_clicks.head())
# => SHOP1 SHOP2 SHOP3 SHOP4
# day
# 1 269.0 -315.0 169.0 328.0
# 2 242.0 174.0 142.0 301.0
# 3 233.0 201.0 124.0 283.0
# 4 278.0 264.0 178.0 NaN
# 5 341.0 255.0 173.0 337.0
В случае параметризации необходимо использовать correct_clicks()
в теле лямда-функции.
apply()
для преобразования нескольких столбцов
Предположим, что нам необходимо найти наибольшее число кликов за каждый день среди магазинов 1 и 3. Сформируем новый столбец MAX_SHOP1_SHOP3
:
df_clicks['SHOP1'] = df_clicks['SHOP1'].fillna(df_clicks['SHOP1'].mean())
df_clicks['SHOP3'] = df_clicks['SHOP3'].fillna(df_clicks['SHOP3'].mean())
df_clicks['MAX_SHOP1_SHOP3'] = df_clicks.apply(lambda x: max(x['SHOP1'], x['SHOP3']), axis=1)
print(df_clicks.head())
# => SHOP1 SHOP2 SHOP3 SHOP4 MAX_SHOP1_SHOP3
# day
# 1 269.0 -315.0 169.0 328.0 269.0
# 2 242.0 174.0 142.0 301.0 242.0
# 3 233.0 201.0 124.0 283.0 233.0
# 4 278.0 264.0 178.0 NaN 278.0
# 5 341.0 255.0 173.0 337.0 341.0
В первых двух строчках мы заполнили пропуски в данных средними значениями с помощью метода fillna()
. Это нужно, чтобы не было ошибок при вызове функции max()
. Далее apply()
применяется ко всему DataFrame
. Параметр axis
задает направление обхода набора данных:
- 0 задает обход по столбцам
- 1 задает обход по строкам
Мы использовали значение 1, так в качестве аргумента x
лямбда-функции выступают строки набора данных. Так как мы ищем максимум среди 1 и 3 магазинов, то обращаемся к соответствующим значениям по ключам магазинов: SHOP1
и SHOP3
.
Теперь учтем следующую информацию: если максимум окажется больше, чем 200, то мы должны заполнить столбец числом -1. Это будет говорить о некоторой технической ошибке:
def correct_max(x, threshold):
if max(x['SHOP1'], x['SHOP3']) > threshold:
return -1
return max(x['SHOP1'], x['SHOP3'])
df_clicks['MAX_SHOP1_SHOP3_CORRECT'] = df_clicks.apply(lambda x: correct_max(x, 200), axis=1)
print(df_clicks.head())
# => SHOP1 SHOP2 SHOP3 SHOP4 MAX_SHOP1_SHOP3 MAX_SHOP1_SHOP3_CORRECT
# day
# 1 269.0 -315.0 169.0 328.0 269.0 -1.0
# 2 242.0 174.0 142.0 301.0 242.0 -1.0
# 3 233.0 201.0 124.0 283.0 233.0 -1.0
# 4 278.0 264.0 178.0 NaN 278.0 -1.0
# 5 341.0 255.0 173.0 337.0 341.0 -1.0
Векторизованные функции
В отличие от метода apply()
векторизованные функции преобразовывают столбцы не поэлементно, а целыми списками. Это позволяет выполнять вычисления быстрее. Найдем сумму кликов двух магазинов с помощью разных подходов, и сравним скорость их выполнения:
%time df_clicks['SUM'] = df_clicks.apply(lambda x: x['SHOP1'] + x['SHOP2'], axis=1)
# => CPU times: user 1.11 ms, sys: 57 µs, total: 1.17 ms
# Wall time: 1.15 ms
%time df_clicks['SUM'] = df_clicks['SHOP1'] + df_clicks['SHOP2']
# => CPU times: user 541 µs, sys: 16 µs, total: 557 µs
# Wall time: 526 µs
Разница в производительности больше в два раза. Векторизованные функции более производительны. Но функционал векторизованных функций ограничен. При этом с помощью метода apply()
можно выполнять сложные преобразования.
Векторные арифметические операции
Над столбцами можно выполнять основные арифметические операции. Выше был пример операции сложений. Ниже случаи для других операций:
df_clicks['MULTIPLE'] = df_clicks['SHOP1'] * df_clicks['SHOP2']
print(df_clicks[['SHOP1', 'SHOP2', 'MULTIPLE']].head())
# => SHOP1 SHOP2 MULTIPLE
# day
# 1 319.0 -265.0 -84535.0
# 2 292.0 274.0 80008.0
# 3 283.0 301.0 85183.0
# 4 328.0 364.0 119392.0
# 5 391.0 355.0 138805.0
df_clicks['MINUS'] = df_clicks['SHOP1'] - 100
print(df_clicks[['SHOP1', 'MINUS']].head())
# => SHOP1 MINUS
# day
# 1 319.0 219.0
# 2 292.0 192.0
# 3 283.0 183.0
# 4 328.0 228.0
# 5 391.0 291.0
Из примеров видно, что арифметические операции можно выполнять как между столбцами, так и между столбцом и другим объектом, например, числом. Этот прием называется укладыванием. Он также позволяет выполнять операции быстрее, чем проход по элементам в цикле.
Изменение типов данных
Тип столбцов важен для DataFrame
. От него зависит корректность проведения тех или иных операций. Иногда его приходится изменять.
Предположим, что нам в каких-то целях необходимо поработать с числовым столбцом, как со строковыми данными. Это можно легко сделать с помощью метода astype()
:
print(df_clicks[['SHOP1', 'SHOP2']].info())
# => <class 'pandas.core.frame.DataFrame'>
# Int64Index: 28 entries, 1 to 28
# Data columns (total 2 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 SHOP1 28 non-null float64
# 1 SHOP2 27 non-null float64
# dtypes: float64(2)
# memory usage: 1.7 KB
Преобразуем тип для SHOP1
:
df_clicks['SHOP1'] = df_clicks['SHOP1'].astype(str)
print(df_clicks[['SHOP1', 'SHOP2']].info())
# => <class 'pandas.core.frame.DataFrame'>
# Int64Index: 28 entries, 1 to 28
# Data columns (total 2 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 SHOP1 28 non-null object
# 1 SHOP2 27 non-null float64
# dtypes: float64(1), object(1)
# memory usage: 1.7+ KB
Метод astype()
ожидает на вход тип объекта, к которому необходимо привести элементы столбца.
Замена элементов согласно словарю
Существуют преобразования данных в столбцах DataFrame
, которые требуют заменить одно значение на другое. Если подстановку значений можно задать с помощью словаря, то достаточно воспользоваться методом map()
.
Допустим, 150 кликов и более — это хорошо, а меньше — это плохо. Добавим столбец с категорией значений 0 и 1 согласно данному условию:
df_clicks['GOOD_OR_BAD'] = df_clicks['SHOP2'].apply(lambda x: 0 if x < 150 else 1)
print(df_clicks[['SHOP2', 'GOOD_OR_BAD']].head())
# => SHOP2 GOOD_OR_BAD
# day
# 1 -315.0 0
# 2 174.0 1
# 3 201.0 1
# 4 264.0 1
# 5 255.0 1
Создадим словарь, согласно которому будет выполняться замена:
map_dict = {0:'BAD', 1:'GOOD'}
Заменим значения столбца GOOD_OR_BAD
:
df_clicks['GOOD_OR_BAD'] = df_clicks['GOOD_OR_BAD'].map(map_dict)
print(df_clicks[['SHOP2','GOOD_OR_BAD']].head())
# => SHOP2 GOOD_OR_BAD
# day
# 1 -315.0 BAD
# 2 174.0 GOOD
# 3 201.0 GOOD
# 4 264.0 GOOD
# 5 255.0 GOOD
Выводы
В этом уроке мы познакомились с несколькими способами преобразования строк и столбцов объекта DataFrame
библиотеки Pandas.
Обычно выделяют два подхода:
- Использование метода
apply()
- Применение векторизованных функций
В случае использования метода apply()
можно выполнять сложное преобразование, но вычисления будут сравнительно долгими. В случае векторизованных функций все наоборот — получаем высокую производительность, но ограниченный функционал. Обычно аналитики делают свой выбор в зависимости от условий задачи.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.