Сообщить об ошибке.

Преобразование объектов DataFrame/DataFrameGroupBy в pandas

Синтаксис:

df = DataFrame.transform(func, axis=0, *args, **kwargs)

df = Series.transform(func, *args, **kwargs)

df = DataFrameGroupBy.transform(func, *args, engine=None, engine_kwargs=None, **kwargs)

Параметры:

  • func - функция для преобразования данных. Принимает в качестве аргумента Series (столбец/строку, в зависимости от axis), а результат функции не должен менять исходную форму.

    Допустимые комбинации:

    • объект функции;
    • строка с именем функции;
    • список функций и/или имен функций, например [np.exp, 'sqrt'];
    • словарь, где ключи - это метки осей, а значениями могут быть: объект функции, имя функции или список функций.

    Если func одновременно похож на список и на словарь, то поведение, подобное словарю, имеет приоритет.

  • axis=0 - если 0 или 'index': функция применяется к каждому столбцу. Если 1 или 'columns': функция применяется к каждой строке.

    Для Series.transform() этот аргумент не используется.

  • *args - позиционные аргументы для передачи в func.

  • **kwargs - ключевые аргументы для передачи в func.

Возвращаемое значение:

  • DataFrame/Series, который имеет ту же длину, что и исходный.

Описание DataFrame.transform()

Методы DataFrame.transform() и Series.transform() модуля pandas вызывают функцию func для самостоятельного создания DataFrame/Series с той же формой оси, что и у исходного объекта.

Этот метод в основном используется с групповыми операциями (смотрите ниже)

Если возвращаемый DataFrame имеет длину, отличную от исходного DataFrame/Series, то поднимается исключение ValueError.

С обычным DataFrame пользовательская функция func метода DataFrame.transform() принимает на вход столбец или строку (в зависимости от axis) в виде Series, и может работать со всеми элементами Series, но не должна изменять форму DataFrame.

>>> import pandas as pd
>>> df = pd.DataFrame({'A': range(3), 'B': range(1, 4)})
>>> df
#    A  B
# 0  0  1
# 1  1  2
# 2  2  3

# принимает `Series` и может работать 
# со всеми элементами `Series` (`x.max()`)
>>> df.transform(lambda x: x + x.max())
#    A  B
# 0  2  4
# 1  3  5
# 2  4  6

# но не должна изменять форму `DataFrame`
>>> df.transform(lambda x: x.max())
# Traceback (most recent call last):
# ...
# ValueError: Function did not transform

# метод `.apply()` может менять форму `DataFrame`
>>> df.apply(lambda x: x.max())
# A    2
# B    3
# dtype: int64

Несмотря на то, что результирующий DataFrame должен иметь ту же длину, что и исходный, можно передать несколько функций:

>>> import numpy as np
>>> s = pd.Series(range(3))
>>> s
# 0    0
# 1    1
# 2    2
# dtype: int64

>>> s.transform([np.sqrt, np.exp])
#        sqrt        exp
# 0  0.000000   1.000000
# 1  1.000000   2.718282
# 2  1.414214   7.389056

Описание DataFrameGroupBy.transform()

Метод .transform() также имеют объекты DataFrameGroupBy и Resampler. Здесь и проявляются главные отличия от метода .apply().

Здесь аргумент func может быть:

  • строкой с именем функции,
  • объектом функции
  • JIT-функцией Numba с указанием engine='numba'. (Для 'numba', функцию должна быть определена пользователем со значениями и индексом в качестве первого и второго аргументов соответственно. Индекс каждой группы будет передан пользовательской функции и опционально доступен для использования).

Два основных различия между DataFrameGroupBy.apply() и DataFrameGroupBy.transform() в групповых операциях:

  1. Входные данные для функции:

    • метод DataFrameGroupBy.apply() неявно передает func ВСЕ столбцы для каждой группы в виде DataFrame,
    • метод DataFrameGroupBy.transform() неявно передает func КАЖДЫЙ столбец для каждой группы по отдельности в виде Series.
  2. Выходные данные функции:

    • пользовательская функция в DataFrameGroupBy.apply(), может возвращать скаляр, или Series, или DataFrame (или массив numpy, или даже список).
    • пользовательская функция в DataFrameGroupBy.transform(), должна возвращать последовательность (Series, массив или список) той же длины, что и группа.

Таким образом, функция DataFrameGroupBy.transform() работает только с одной Series за раз, а DataFrameGroupBy.apply() работает сразу со всем DataFrame.

Несколько примеров, чтобы понять о чем речь:

>>> import pandas as pd

df = pd.DataFrame({'str':['one', 'one', 'two', 'two'], 
                   'a':[4,5,1,3], 'b':[6,10,3,11]})

>>> df
#    str  a   b
# 0  one  4   6
# 1  one  5  10
# 2  two  1   3
# 3  two  3  11

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

def inspect(x):
    print(type(x))
    raise

Теперь передадим функцию inspect() соответствующим методам операции .groupby(), и посмотрим, какой объект ей передается:

>>> df.groupby('str').apply(inspect, include_groups=False)
# <class 'pandas.core.frame.DataFrame'>
# Traceback (most recent call last):
# ...

>>> df.groupby('str').transform(inspect)
# <class 'pandas.core.series.Series'>
# Traceback (most recent call last):
# ...

Таким образом, DataFrameGroupBy.transform() разрешено работать только с одной Series одновременно. Невозможно, чтобы DataFrameGroupBy.transform() воздействовал на два столбца одновременно. Итак, если попытаться вычесть столбец a из b внутри пользовательской функции, то при преобразовании получим ошибку:

def subtract_two(x):
    return x['a'] - x['b']

>>> df.groupby('str').transform(subtract_two)
# Traceback (most recent call last):
# ...
# KeyError: 'a'

Получаем ошибку KeyError, т.к. pandas пытается найти индекс a, которого не существует. Эту операцию будет выполнена с помощью DataFrameGroupBy.apply(), т.к. она работает со всем DataFrame:

>>> df.groupby('str').apply(subtract_two, include_groups=False)
# str   
# one  0   -2
#      1   -5
# two  2   -2
#      3   -8
# dtype: int64

Метод DataFrameGroupBy.transform() должен возвращать последовательность того же размера, что и группа.

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

>>> import numpy as np

def return_three(x):
    return np.array([1, 2, 3])

>>> df.groupby('str').transform(return_three)
# Traceback (most recent call last):
# ...
# ValueError: Length mismatch: Expected axis has 7 elements, 
# new values have 4 elements

# такая функция будет работать
def rand_group_len(x):
    return np.random.rand(len(x))

>>> df.groupby('str').transform(rand_group_len)
#           a         b
# 0  0.113465  0.311376
# 1  0.917720  0.068328
# 2  0.180872  0.871791
# 3  0.163254  0.703114

Возврат одного скалярного объекта для DataFrameGroupBy.transform()

Если из пользовательская функция возвращает только один скаляр, то transform будет использовать его для каждой строки в группе:

>>> df.groupby('str').transform(group_sum)
#    a   b
# 0  9  16
# 1  9  16
# 2  4  14
# 3  4  14

# по сравнения с groupby().apply()
>>> df.groupby('str').apply(group_sum, include_groups=False)
#      a   b
# str       
# one  9  16
# two  4  14

Благодаря такому поведению можно посчитать количество элементов в каждой группе

df = pd.DataFrame({
    "c": [1, 1, 1, 2, 2, 2, 2],
    "type": ["m", "n", "o", "m", "m", "n", "n"]
})

>>> df
#    c type
# 0  1    m
# 1  1    n
# 2  1    o
# 3  2    m
# 4  2    m
# 5  2    n
# 6  2    n

>>> df['size'] = df.groupby('c')['type'].transform(len)
>>> df
#    c type size
# 0  1    m    3
# 1  1    n    3
# 2  1    o    3
# 3  2    m    4
# 4  2    m    4
# 5  2    n    4
# 6  2    n    4

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

>>> import numpy as np
>>> s = pd.Series(range(3))
>>> s
# 0    0
# 1    1
# 2    2
# dtype: int64

>>> s.transform([np.sqrt, np.exp])
#        sqrt        exp
# 0  0.000000   1.000000
# 1  1.000000   2.718282
# 2  1.414214   7.389056

Примеры и различия в использовании метода .transform()

Дополнительно смотрите примеры с groupby.transform() в материале "Примеры групповых операций в pandas со StackOverflow"

Для объекта DataFrame/Series

>>> import pandas as pd
>>> df = pd.DataFrame({'A': range(3), 'B': range(1, 4)})
>>> df
#    A  B
# 0  0  1
# 1  1  2
# 2  2  3

>>> df.transform(lambda x: x + 1)
#    A  B
# 0  1  2
# 1  2  3
# 2  3  4

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

>>> import numpy as np
>>> s = pd.Series(range(3))
>>> s
# 0    0
# 1    1
# 2    2
# dtype: int64

>>> s.transform([np.sqrt, np.exp])
#        sqrt        exp
# 0  0.000000   1.000000
# 1  1.000000   2.718282
# 2  1.414214   7.389056

Для объекта DataFrameGroupBy

df = pd.DataFrame({
    "Date": [
        "2015-05-08", "2015-05-07", "2015-05-06", "2015-05-05",
        "2015-05-08", "2015-05-07", "2015-05-06", "2015-05-05"],
    "Data": [5, 8, 6, 1, 50, 100, 60, 120],
})
>>> df
#          Date  Data
# 0  2015-05-08     5
# 1  2015-05-07     8
# 2  2015-05-06     6
# 3  2015-05-05     1
# 4  2015-05-08    50
# 5  2015-05-07   100
# 6  2015-05-06    60
# 7  2015-05-05   120

>>> df.groupby('Date')['Data'].transform('sum')
# 0     55
# 1    108
# 2     66
# 3    121
# 4     55
# 5    108
# 6     66
# 7    121
# Name: Data, dtype: int64
df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',
                          'foo', 'bar'],
                   'B' : ['one', 'one', 'two', 'three',
                          'two', 'two'],
                   'C' : [1, 5, 5, 2, 5, 5],
                   'D' : [2.0, 5., 8., 1., 2., 9.]})

>>> grouped = df.groupby('A')[['C', 'D']]
>>> grouped.transform(lambda x: (x - x.mean()) / x.std())
#         C         D
# 0 -1.154701 -0.577350
# 1  0.577350  0.000000
# 2  0.577350  1.154701
# 3 -1.154701 -1.000000
# 4  0.577350 -0.577350
# 5  0.577350  1.000000

Трансляция результата трансформации

>>> grouped.transform(lambda x: x.max() - x.min())
#     C    D
# 0  4.0  6.0
# 1  3.0  8.0
# 2  4.0  6.0
# 3  3.0  8.0
# 4  4.0  6.0
# 5  3.0  8.0

>>> grouped.transform("mean")
#     C    D
# 0  3.666667  4.0
# 1  4.000000  5.0
# 2  3.666667  4.0
# 3  4.000000  5.0
# 4  3.666667  4.0
# 5  4.000000  5.0

Для объекта Resampler

s = pd.Series([1, 2],
              index=pd.date_range('20180101',
                                  periods=2,
                                  freq='1h'))

>>> s
# 2018-01-01 00:00:00    1
# 2018-01-01 01:00:00    2
# Freq: h, dtype: int64

>>> resampled = s.resample('15min')
>>> resampled.transform(lambda x: (x - x.mean()) / x.std())
# 2018-01-01 00:00:00   NaN
# 2018-01-01 01:00:00   NaN
# Freq: h, dtype: float64