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

Методы .apply() и .map() объектов DataFrame/Series в pandas

Применяет пользовательскую функцию к строке/столбцу или поэлементно

Содержание:


Метод DataFrame.apply() VS DataFrame.map()

Метод DataFrame.apply() применяет пользовательскую функцию func вдоль оси axis, т. е. работает со всеми значениями столбца или строки. Другими словами, пользовательская функция принимает в качестве обязательного аргумента Series - сразу все значения столбца или строки (в зависимости от аргумента axis) и что-то с ними делает.

>>> import pandas as pd
>>> import numpy as np
>>> df = pd.DataFrame(np.random.randn(4, 3), columns=list('abc'))
>>> df
#           a         b         c
# 0  0.532464  2.241091  0.688753
# 1 -0.586088  0.111004  1.007061
# 2  0.205406  1.580366  0.609771
# 3  0.968165  1.196149 -0.727974

# определяем функцию, которая принимает `Series`
>>> fn = lambda ser: ser.max() - ser.min()
>>> df.apply(fn)
# a    1.554252
# b    2.130087
# c    1.735035
# dtype: float64

При этом метод DataFrame.apply() может также изменять каждый элемент, если передаваемая функция - является универсальной функцией numpy:

>>> df = pd.DataFrame([[4, 9]] * 3, columns=['A', 'B'])
>>> df
#    A  B
# 0  4  9
# 1  4  9
# 2  4  9

# например, `np.sqrt()`
>>> df.apply(np.sqrt)
#      A    B
# 0  2.0  3.0
# 1  2.0  3.0
# 2  2.0  3.0

Метод DataFrame.map() тупо применяет пользовательскую функцию к DataFrame поэлементно. Другими словами, пользовательская функция принимает в качестве обязательного аргумента только один элемент DataFrame (перебирает DataFrame поэлементно) и что то с ним делает.

>>> df = pd.DataFrame([[1, 2.12], [3.356, 4.567]])
>>> df
#        0      1
# 0  1.000  2.120
# 1  3.356  4.567

>>> df.map(lambda x: len(str(x)))
#    0  1
# 0  3  4
# 1  5  5

Резюме к сравнению методов

  • Если пользовательская функция должна одновременно работать со всеми значениями строки или столбца, то однозначно используем метод DataFrame.apply(). Например: lambda ser: ser.max() - ser.min()

  • Если функция должна быть применена к каждому элементу:

    1. строки или столбца DataFrame, то используем метод DataFrame.apply();
    2. всего DataFrame, то используем метод DataFrame.map() (ранее назывался DataFrame.applymap())

DataFrame.apply(func, axis=0, raw=False, result_type=None, args=(), by_row='compat', **kwargs):

Метод DataFrame.apply() применяет пользовательскую функцию вдоль оси axis к каждому столбцу или строке.

Объекты, передаваемые в пользовательскую функцию, являются объектами Series, индекс которых является либо индексом DataFrame (axis=0), либо столбцами DataFrame (axis=1). По умолчанию (result_type=None) окончательный тип возвращаемого значения выводится из типа возвращаемого значения применяемой функции. В противном случае это зависит от аргумента result_type.

Принимаемые аргументы:

  • func: пользовательская функция, применяемая к каждому столбцу или строке.
  • axis: ось, вдоль которой применяется функция:
    • 0 или 'index': применить функцию к каждому столбцу.
    • 1 или 'columns': применить функцию к каждой строке.
  • raw: определяет, передается ли строка или столбец как объект Series или ndarray:
    • False : передает в функцию каждую строку или столбец в виде серии.
    • True : переданная функция получит объекты numpy.ndarray. Если применяется функция NumPy, то это позволит добиться гораздо большей производительности.
  • result_type: действует только тогда, когда axis=1 (столбцы). Поведение по умолчанию (None) зависит от возвращаемого значения прикладной функции: результаты, подобные списку, будут возвращены в виде их Series. Однако, если DataFrame.apply() возвращает серию, они расширяются до столбцов.
    • 'expand' : результаты, подобные списку, будут преобразованы в столбцы.
    • 'reduce' : по возможности возвращает ряд, а не расширяет результаты, подобные списку. Это противоположно 'expand'.
    • 'broadcast': результаты будут транслироваться в исходную форму фрейма данных, исходный индекс и столбцы будут сохранены.
  • args: кортеж с позиционными аргументами для передачи в func в дополнение к Series.
  • by_row: имеет эффект только тогда, аргумент func не является строкой, а является списком или словарем вызываемых объектов. Если by_row='compat', то, по возможности, pandas переведет функцию в методы (например, Series().apply(np.sum) будет переведен в Series().sum()). Если это не сработает, попробует снова вызвать DataFrame.apply() с помощью by_row=True, а если это не удастся, то снова вызовет DataFrame.apply() с помощью by_row=False (обратная совместимость). Если значение равно False, то функции будут переданы всей серии сразу.
  • **kwargs: дополнительные ключевые аргументы для передачи в func.

Пример использования DataFrame.apply():

Пользовательские функции, изменяющие переданный объект, могут приводить к непредвиденному поведению или ошибкам и не поддерживаются.

>>> import pandas as pd
>>> import numpy as np
>>> df = pd.DataFrame([[4, 9]] * 3, columns=['A', 'B'])
>>> df
#   A  B
# 0  4  9
# 1  4  9
# 2  4  9

Используем универсальную функцию numpy:

>>> df.apply(np.sqrt)
#      A    B
# 0  2.0  3.0
# 1  2.0  3.0
# 2  2.0  3.0

Использование групповой функции по осям axis:

# суммирование значений столбца
>>> df.apply(np.sum, axis=0)
# A    12
# B    27
# dtype: int64

# суммирование значений строк столбцов
>>> df.apply(np.sum, axis=1)
# 0    13
# 1    13
# 2    13
# dtype: int64

Если пользовательская функция возвращает список, то это приведет к Series

>>> df.apply(lambda x: [1, 2], axis=1)
# 0    [1, 2]
# 1    [1, 2]
# 2    [1, 2]
# dtype: object

Передача result_type='expand' расширит результаты в виде списка до столбцов DataFrame

>>> df.apply(lambda x: [1, 2], axis=1, result_type='expand')
#    0  1
# 0  1  2
# 1  1  2
# 2  1  2

Внутри пользовательской функции можно возвращать Series, что будет аналогично передаче result_type='expand'. Результирующие имена столбцов будут индексом ряда.

>>> df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1)
#    foo  bar
# 0    1    2
# 1    1    2
# 2    1    2

Передача result_type='broadcast' обеспечит один и тот же результат, независимо от того, будет ли функция возвращать список или скаляр, и транслировать его вдоль оси. Результирующие имена столбцов будут исходными.

>>> df.apply(lambda x: [1, 2], axis=1, result_type='broadcast')
#    A  B
# 0  1  2
# 1  1  2
# 2  1  2

Series.apply(func, convert_dtype=_NoDefault.no_default, args=(), *, by_row='compat', **kwargs):

Метод Series.apply() вызывает пользовательскую функцию func для значений Series. Если func возвращает объект Series, то результатом будет DataFrame.

Принимаемые аргументы:

  • func: функция может быть функцией |NumPy|, которая применяется ко всему Series или функция Python, которая работает только с одиночными значениями.
  • convert_dtype: находит лучший dtype для результатов поэлементных функций. Если False, то оставляет dtype=object. Аргумент устарел, начиная с версии 2.1.0. Если необходимо применить convert_dtype=False, то нужно выполнить ser.astype(object).apply().
  • args: кортеж с позиционными аргументами для передачи в func после значений Series.
  • by_row: имеет эффект только тогда, аргумент func не является строкой, а является списком или словарем вызываемых объектов. Если by_row='compat', то, по возможности, pandas переведет функцию в методы (например, Series().apply(np.sum) будет переведен в Series().sum()). Если это не сработает, попробует снова вызвать Series.apply() с помощью by_row=True, а если это не удастся, то снова вызовет Series.apply() с помощью by_row=False (обратная совместимость). Если значение равно False, то функции будут переданы всей серии сразу.
  • **kwargs: дополнительные ключевые аргументы для передачи в func.

Пример использования Series.apply():

Пользовательские функции, изменяющие переданный объект, могут приводить к непредвиденному поведению или ошибкам и не поддерживаются.

Создадим серию с летними температурами для городов.

>>> import pandas as pd
>>> s = pd.Series([20, 21, 12],
...               index=['London', 'New York', 'Helsinki'])
>>> s
# London      20
# New York    21
# Helsinki    12
# dtype: int64

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

>>> def square(x):
...     return x ** 2
>>> s.apply(square)
# London      400
# New York    441
# Helsinki    144
# dtype: int64

Возвести значения в квадрат, можно анонимной функцией lambda.

>>> s.apply(lambda x: x ** 2)
# London      400
# New York    441
# Helsinki    144
# dtype: int64

Определим пользовательскую функцию, которой требуются дополнительные позиционные аргументы, и передадим эти дополнительные аргументы с помощью ключевого аргумента args.

>>> def subtract_custom_value(x, custom_value):
...     return x - custom_value
>>> s.apply(subtract_custom_value, args=(5,))
# London      15
# New York    16
# Helsinki     7
# dtype: int64

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

>>> def add_custom_values(x, **kwargs):
...     for month in kwargs:
...         x += kwargs[month]
...     return x
>>> s.apply(add_custom_values, june=30, july=20, august=25)
# London      95
# New York    96
# Helsinki    87
# dtype: int64

Используем функцию из библиотеки |NumPy|.

>>> s.apply(np.log)
# London      2.995732
# New York    3.044522
# Helsinki    2.484907
# dtype: float64

DataFrameGroupBy.apply(func, *args, **kwargs):

Метод DataFrameGroupBy.apply() применяет функцию func по группам и объединяет результаты.

Функция, передаваемая в DataFrameGroupBy.apply(), должна принимать в качестве первого аргумента DataFrame и возвращать DataFrame, Series или скаляр. Затем DataFrameGroupBy.apply() позаботится об объединении результатов в один DataFrame или Series. Кроме того, вызываемый объект может принимать позиционные *args и ключевые **kwargs аргументы.

Таким образом, apply является очень гибким методом группировки.

Хотя DataFrameGroupBy.apply() - очень гибкий метод, недостатком является то, что его использование может быть немного медленнее, чем использование более конкретных методов, таких как .agg() или .transform(). Pandas предлагает широкий спектр методов, которые будут намного быстрее, чем использование .apply().

Пример использования DataFrameGroupBy.apply():

>>> import pandas as pd

df = pd.DataFrame({'A': 'a a b'.split(),
                   'B': [1,2,3],
                   'C': [4,6,5]})

>>> g = df.groupby('A', group_keys=False)

Пример 1. Функция принимает DataFrame в качестве аргумента и возвращает DataFrame. Метод apply объединяет результаты для каждой группы в новый DataFrame:

>>> g[['B', 'C']].apply(lambda x: x / x.sum())
#           B    C
# 0  0.333333  0.4
# 1  0.666667  0.6
# 2  1.000000  1.0

Пример 2. Функция принимает DataFrame в качестве аргумента и возвращает Series. Метод apply объединяет результаты для каждой группы в новый DataFrame.

>>> g[['B', 'C']].apply(lambda x: x.astype(float).max() - x.min())
#      B    C
# A
# a  1.0  2.0
# b  0.0  0.0

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

>>> g.apply(lambda x: x.C.max() - x.B.min())
# A
# a    5
# b    2
# dtype: int64

DataFrame.map(func, na_action=None, **kwargs):

Метод DataFrame.map() применяет пользовательскую функцию к DataFrame поэлементно. Этот метод применяет функцию func, которая принимает и возвращает скаляр для каждого элемента DataFrame.

Принимаемые аргументы:

  • func - функция Python, которая принимает и возвращает скаляр для каждого элемента DataFrame
  • na_action=None - если na_action='ignore', то распространяет значения NaN, не передавая их в func.
  • **kwargs - дополнительные ключевые аргументы для передачи в func.

Пример использования DataFrame.map():

>>> df = pd.DataFrame([[1, 2.12], [3.356, 4.567]])
>>> df
#        0      1
# 0  1.000  2.120
# 1  3.356  4.567

>>> df.map(lambda x: len(str(x)))
#    0  1
# 0  3  4
# 1  5  5

Как и в Series.map(), значения NA можно игнорировать:

>>> df_copy = df.copy()
>>> df_copy.iloc[0, 0] = pd.NA
>>> df_copy.map(lambda x: len(str(x)), na_action='ignore')
#      0  1
# 0  NaN  4
# 1  5.0  5

Обратите внимание, что часто существует векторизованная версия функции, которая работает намного быстрее. Например, возведение каждого число в квадрат поэлементно.

# в этом случае лучше избегать `map`.
>>> df.map(lambda x: x**2)
#            0          1
# 0   1.000000   4.494400
# 1  11.262736  20.857489

# векторизованная версия функции
>>> df ** 2
#            0          1
# 0   1.000000   4.494400
# 1  11.262736  20.857489

Series.map(arg, na_action=None):

Метод Series.map() используется для замены каждого значения в Series другим значением, которое может быть получено из функции, словаря dict или Series.

Принимаемые аргументы:

  • arg - функция, словарь dict или Series.
  • na_action=None - если na_action='ignore', то распространяет значения NaN, не передавая их в func.

Внимание. Когда arg является словарем, то значения в последовательностях, которых нет в словаре (как ключей), преобразуются в NaN. НО, если словарь является подклассом dict, который определяет __missing__ (предоставляет метод для значений по умолчанию), то используется это значение по умолчанию, а не NaN.

Пример использования DataFrame.map():

>>> s = pd.Series(['cat', 'dog', np.nan, 'rabbit'])
>>> s
# 0      cat
# 1      dog
# 2      NaN
# 3   rabbit
# dtype: object

Метод Series.map() принимает dict или Series. Значения, которые не найдены в словаре, преобразуются в NaN, если словарь не имеет значения по умолчанию (например, collections.defaultdict):

>>> s.map({'cat': 'kitten', 'dog': 'puppy'})
# 0   kitten
# 1    puppy
# 2      NaN
# 3      NaN
# dtype: object

Аргумент arg также принимает функцию:

>>> s.map('I am a {}'.format)
# 0       I am a cat
# 1       I am a dog
# 2       I am a nan
# 3    I am a rabbit
# dtype: object

Чтобы избежать применения функции к отсутствующим значениям (и сохранить их как NaN), можно использовать na_action='ignore'.

>>> s.map('I am a {}'.format, na_action='ignore')
# 0     I am a cat
# 1     I am a dog
# 2            NaN
# 3  I am a rabbit
# dtype: object