Обзор основных функций
В этом разделе мы рассмотрим основные функции Pandas, которые могут быть полезны при повседневной работе с данной библиотекой, но сначала как обычно выполним все необходимые импорты:
import numpy as np
import pandas as pd
Бинарные операции
Датафреймы и серии имеют множество методов для выполнения бинарных операций (add, sub, div и т.д.), которые позволяют определить, то как должны транслироваться одномерные объекты (например серии): по столбцам или по строкам датафреймов. Давайте сначала создадим датафрейм:
idx = list('abcde')
cols = ['col_' + str(i) for i in range(4)]
df = pd.DataFrame(np.arange(20).reshape(5, 4),
index=idx,
columns=cols)
df
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | 0 | 1 | 2 | 3 |
b | 4 | 5 | 6 | 7 |
c | 8 | 9 | 10 | 11 |
d | 12 | 13 | 14 | 15 |
e | 16 | 17 | 18 | 19 |
А теперь создадим два одномерных массива:
a = np.arange(12, 16)
b = np.arange(2, 19, 4)
a, b
(array([12, 13, 14, 15]), array([ 2, 6, 10, 14, 18]))
По умолчанию, транслирование выполняется вдоль столбцов, при этом длина одномерного массива должна быть равна количеству столбцов датафрейма:
df.sub(a) # эквивалентно команде df.sub(a, axis='columns')
# или df.sub(a, axis=1)
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | -12 | -12 | -12 | -12 |
b | -8 | -8 | -8 | -8 |
c | -4 | -4 | -4 | -4 |
d | 0 | 0 | 0 | 0 |
e | 4 | 4 | 4 | 4 |
Если длина не совпадает с количеством столбцов, то мы увидим ошибку:
df.sub(b)
ValueError: Unable to coerce to Series, length must be 4: given 5
Что бы выполнить транслирование вдоль строк, необходимо что бы длина массива была равна количеству строк датафрейма:
df.sub(b, axis='index') # эквивалентно df.sub(b, axis=0)
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | -2 | -1 | 0 | 1 |
b | -2 | -1 | 0 | 1 |
c | -2 | -1 | 0 | 1 |
d | -2 | -1 | 0 | 1 |
e | -2 | -1 | 0 | 1 |
То же самое произойдет, если вместо одномерных массивов, мы будем использовать серии:
row = pd.Series(range(12, 16),
index=cols)
row
col_0 12 col_1 13 col_2 14 col_3 15 dtype: int64
# транслирование серии по столбцам:
df.sub(row)
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | -12 | -12 | -12 | -12 |
b | -8 | -8 | -8 | -8 |
c | -4 | -4 | -4 | -4 |
d | 0 | 0 | 0 | 0 |
e | 4 | 4 | 4 | 4 |
column = pd.Series(range(2, 19, 4),
index=idx)
column
a 2 b 6 c 10 d 14 e 18 dtype: int64
# транслирование серии по строкам:
df.sub(column, axis=0)
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | -2 | -1 | 0 | 1 |
b | -2 | -1 | 0 | 1 |
c | -2 | -1 | 0 | 1 |
d | -2 | -1 | 0 | 1 |
e | -2 | -1 | 0 | 1 |
Но серии в отличие от массивов, вовсе не обязаны соответствовать правилам совместимости принятым в NumPy, т.е. длины серий не обязаны совпадать ни с количеством строк ни с количеством столбцов в датафреймах.
row = pd.Series([12, 15, 100],
index=['col_0', 'col_3', 'col_4'])
row
col_0 12 col_3 15 col_4 100 dtype: int64
df.sub(row)
col_0 | col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|---|
a | -12.0 | NaN | NaN | -12.0 | NaN |
b | -8.0 | NaN | NaN | -8.0 | NaN |
c | -4.0 | NaN | NaN | -4.0 | NaN |
d | 0.0 | NaN | NaN | 0.0 | NaN |
e | 4.0 | NaN | NaN | 4.0 | NaN |
В данном случае операция вычитания выполнилась не только с транслированием вдоль столбцов, но еще и выравниванием индексов серии с названиями столбцов датафрейма. При этом результат содержит столько столбцов, сколько их в объединении множеств индекса серии и столбцов датафрейма. Тоже самое касается и транслирования вдоль строк:
column = pd.Series(range(1, 34, 8),
index=list('aceXY'))
column
a 1 c 9 e 17 X 25 Y 33 dtype: int64
df.sub(column, axis='index')
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
X | NaN | NaN | NaN | NaN |
Y | NaN | NaN | NaN | NaN |
a | -1.0 | 0.0 | 1.0 | 2.0 |
b | NaN | NaN | NaN | NaN |
c | -1.0 | 0.0 | 1.0 | 2.0 |
d | NaN | NaN | NaN | NaN |
e | -1.0 | 0.0 | 1.0 | 2.0 |
Серии и индексы (объекты Index) поддерживают встроенную в Python функцию divmod, которая возвращает результат целочисленного деления и остатки от деления:
s = pd.Series(range(7))
s
0 0 1 1 2 2 3 3 4 4 5 5 6 6 dtype: int64
d, m = divmod(s, 2)
d
0 0 1 0 2 1 3 1 4 2 5 2 6 3 dtype: int64
m
0 0 1 1 2 0 3 1 4 0 5 1 6 0 dtype: int64
idx = pd.Index(range(7))
idx
RangeIndex(start=0, stop=7, step=1)
d, m = divmod(idx, 2)
d, m
(Int64Index([0, 0, 1, 1, 2, 2, 3], dtype='int64'), Int64Index([0, 1, 0, 1, 0, 1, 0], dtype='int64'))
m
Int64Index([0, 1, 0, 1, 0, 1, 0], dtype='int64')
Причем, возвращаемый результат всегда имеет тот же тип, что и первый входной аргумент divmod(). При этом есть возможность поэлементного выполнения данной функции в том случае, если второй аргумент, является последовательностью чисел той же длины:
d, m = divmod(idx, [2, 2, 2, 2, 4, 4, 4])
d, m
(Int64Index([0, 0, 1, 1, 1, 1, 1], dtype='int64'), Int64Index([0, 1, 0, 1, 0, 1, 2], dtype='int64'))
Бинарные операции с отсутствующими данными
Все методы серий и датафреймов для выполнения бинарных арифметических операций содержат аргумент fill_value, который позволяет заменять отсутствующие элементы на указанные значения. Но нужно иметь ввиду, что NaN-ы заменяются только в том случае, если соответствующие элементы не равны NaN одновременно:
a = np.random.choice([11, 33, np.nan], size=(5, 4))
b = np.random.choice([22, 44, np.nan], size=(5, 4))
cols = ['col_' + str(i) for i in range(1, 5)]
df_1 = pd.DataFrame(a, columns=cols)
df_1
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
0 | 11.0 | NaN | 11.0 | 11.0 |
1 | NaN | 11.0 | 11.0 | NaN |
2 | 11.0 | 33.0 | NaN | 33.0 |
3 | NaN | 33.0 | 11.0 | 11.0 |
4 | NaN | NaN | 33.0 | NaN |
df_2 = pd.DataFrame(b, columns=cols)
df_2
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
0 | NaN | 44.0 | 22.0 | 22.0 |
1 | 22.0 | NaN | NaN | 22.0 |
2 | 22.0 | 44.0 | NaN | NaN |
3 | 44.0 | 44.0 | 44.0 | 44.0 |
4 | 44.0 | NaN | 44.0 | 22.0 |
df_1.add(df_2, fill_value=9999)
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
0 | 10010.0 | 10043.0 | 33.0 | 33.0 |
1 | 10021.0 | 10010.0 | 10010.0 | 10021.0 |
2 | 33.0 | 77.0 | NaN | 10032.0 |
3 | 10043.0 | 77.0 | 55.0 | 55.0 |
4 | 10043.0 | NaN | 77.0 | 10021.0 |
Дальнейшее заменение NaN-ов можно выполнить с помощью метода fillna()
, например так:
s = df_1.add(df_2, fill_value=9999)
s.fillna(-111)
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
0 | 10010.0 | 10043.0 | 33.0 | 33.0 |
1 | 10021.0 | 10010.0 | 10010.0 | 10021.0 |
2 | 33.0 | 77.0 | -111.0 | 10032.0 |
3 | 10043.0 | 77.0 | 55.0 | 55.0 |
4 | 10043.0 | -111.0 | 77.0 | 10021.0 |
Сравнение значений
Еще один тип полезных бинарных операций - это операции сравнения, для них есть следующие методы:
eq()
- equal (равно)==
;ne()
- not equal (не равно)!=
;le()
- less than or equal (меньше или равно)<=
;lt()
- less than (меньше чем)<
;ge()
- greater than or equal (больше или равно)>=
;gt()
- greater than (больше)>
;
df_1 = pd.DataFrame(np.random.randint(0, 5, (5, 5)))
df_1
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 3 | 0 | 0 | 1 | 2 |
1 | 2 | 4 | 2 | 4 | 0 |
2 | 2 | 0 | 4 | 3 | 0 |
3 | 1 | 2 | 2 | 4 | 1 |
4 | 0 | 0 | 4 | 0 | 4 |
df_2 = pd.DataFrame(np.random.randint(2, 7, (5, 5)))
df_2
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 4 | 4 | 6 | 4 | 4 |
1 | 5 | 6 | 5 | 4 | 6 |
2 | 5 | 6 | 5 | 5 | 3 |
3 | 6 | 4 | 2 | 3 | 3 |
4 | 4 | 2 | 6 | 6 | 2 |
# какие элементы датафреймов имеют
# одинаковые значения?
df_1.eq(df_2)
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | False | False | False | False | False |
1 | False | False | False | True | False |
2 | False | False | False | False | False |
3 | False | False | True | False | False |
4 | False | False | False | False | False |
# какие элементы df_1 меньше чем
# соответствующие элементы df_2?
df_1.lt(df_2)
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | True | True | True | True | True |
1 | True | True | True | False | True |
2 | True | True | True | True | True |
3 | True | True | False | False | True |
4 | True | True | True | True | False |
Конечно, вместо данных методов всегда можно воспользоваться операторами сравнения:
df_1 < df_2
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | True | True | True | True | True |
1 | True | True | True | False | True |
2 | True | True | True | True | True |
3 | True | True | False | False | True |
4 | True | True | True | True | False |
Но не забывайте, что данные методы поддерживают механизм транслирования, как по строкам так и по столбцам:
s = pd.Series([3, 4, 10, 4, 3])
s
0 3 1 4 2 10 3 4 4 3 dtype: int64
# транслирование сравнения по строкам:
df_1.lt(s, axis=0)
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | False | True | True | True | True |
1 | True | False | True | False | True |
2 | True | True | True | True | True |
3 | True | True | True | False | True |
4 | True | True | False | True | False |
# транслирование сравнения по столбцам:
df_1.lt(s, axis=1) # эквивалентно df_1 < s
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | False | True | True | True | True |
1 | True | False | True | False | True |
2 | True | True | True | True | True |
3 | True | True | True | False | True |
4 | True | True | True | True | False |
Возвращаемые объекты могут использоваться для булевой индексации (как логические маски), благодаря чему в результирующий массив попадают только те элементы, которым в булевом массиве соответствует значение True, а те которым соответствует False, заменяются на NaN:
df_2[df_1 < s]
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | NaN | 4.0 | 6 | 4.0 | 4.0 |
1 | 5.0 | NaN | 5 | NaN | 6.0 |
2 | 5.0 | 6.0 | 5 | 5.0 | 3.0 |
3 | 6.0 | 4.0 | 2 | NaN | 3.0 |
4 | 4.0 | 2.0 | 6 | 6.0 | NaN |
Методы any()
и all()
позволяют выполнять логические операции ИЛИ
и И
вдоль столбцов датафреймов с булевыми значениями:
df_1.index = list('abcde')
df_1.columns = ['col_' + str(i) for i in range(5)]
df_1
col_0 | col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|---|
a | 3 | 0 | 0 | 1 | 2 |
b | 2 | 4 | 2 | 4 | 0 |
c | 2 | 0 | 4 | 3 | 0 |
d | 1 | 2 | 2 | 4 | 1 |
e | 0 | 0 | 4 | 0 | 4 |
s.index = ['col_' + str(i) for i in range(5)]
(df_1 < s).all()
col_0 False col_1 False col_2 True col_3 False col_4 False dtype: bool
(df_1 < 1).any()
col_0 True col_1 True col_2 True col_3 True col_4 True dtype: bool
Применительно к сериям методы any()
и all()
работают точно так же как и аналогичные функции в Python:
(s < 5).any()
True
Именно поэтому, они могут применяться последовательно к датафреймам:
(df_1 < s).all().all()
False
Что бы убедиться в том, что датафрейм или серия не является пустой, можно воспользоваться атрибутом empty:
# Пустая серия:
s = pd.Series()
s
Series([], dtype: float64)
s.empty
True
# Пустой датафрейм:
df = pd.DataFrame(columns=list('abc'))
df
a | b | c |
---|
df.empty
True
Оценить одноэлементные объекты с логическими значениями, можно воспользоваться методом bool()
:
pd.Series([True]).bool()
True
pd.DataFrame([[False]]).bool()
False
Датафреймы и серии с логическими значениями не могут учавствовать в логических опреациях:
s1 = pd.Series([False, True, False])
s2 = pd.Series([False, True, True])
# например вот эта команда вернет ошибку:
s1 and s2
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
После нижеприведенной конструкции интерпретатор тоже выдаст ошибку:
if s1:
pass
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
Однако, подобные операции очень необходимы. Здесь главное вспомнить что False интерпретируется как 0, а True как 1, а это значит что логические операции могут быть заменены на побитовые. Звучит странно, но это работает:
s1 & s2 # логическое and
0 False 1 True 2 False dtype: bool
~s1 # логическое not
0 True 1 False 2 True dtype: bool
# не забывайте ставить скобки
(s1 | s2) & (s1 | s2) # "|" - логическое or
0 False 1 True 2 True dtype: bool
А конструкции типа if s1:
или while s1:
можно заменить, например, на if s1.all():
, while s2.empty()
или while s1.any():
.
Проверка на эквивалентность
Иногда нужно убедиться в том, что две серии или два датафрейма состоят из элементов с одинаковыми значениями. Например, у нас есть вот такой датафрейм:
df = pd.DataFrame(np.arange(16, dtype=np.float).reshape(4, 4),
index=list('abcd'),
columns=['col_' + str(i) for i in range(1, 5)])
df
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
a | 0.0 | 1.0 | 2.0 | 3.0 |
b | 4.0 | 5.0 | 6.0 | 7.0 |
c | 8.0 | 9.0 | 10.0 | 11.0 |
d | 12.0 | 13.0 | 14.0 | 15.0 |
Разумеется результаты выражений 3*df
и df + df + df
вернут абсолютно одинаковые датафреймы, в чем легко убедиться:
3*df == df + df + df
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
a | True | True | True | True |
b | True | True | True | True |
c | True | True | True | True |
d | True | True | True | True |
Но если в исходных датафреймах (или сериях) есть хоть один элемент с значением NaN, то результат будет неожиданным:
df['col_4'][0] = np.nan
df
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
a | 0.0 | 1.0 | 2.0 | NaN |
b | 4.0 | 5.0 | 6.0 | 7.0 |
c | 8.0 | 9.0 | 10.0 | 11.0 |
d | 12.0 | 13.0 | 14.0 | 15.0 |
3*df == df + df + df
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
a | True | True | True | False |
b | True | True | True | True |
c | True | True | True | True |
d | True | True | True | True |
Дело в том, что NaN
не равно NaN
:
np.nan == np.nan
False
Но мы ведь знаем что датафреймы равны! Что бы выполнить, абсолютно корректное с нашей точки зрения сравнение, можно воспользоваться методом equals()
, который так же как и мы воспринимает значения NaN на одинаковых позициях, как равные:
(3*df).equals(df + df + df)
True
Но имейте в виду, что индексы должны иметь одинаковый порядок, так как их автоматическое выравнивание не выполняется:
df_2 = df[::-1].copy()
df_2
col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|
d | 12.0 | 13.0 | 14.0 | 15.0 |
c | 8.0 | 9.0 | 10.0 | 11.0 |
b | 4.0 | 5.0 | 6.0 | 7.0 |
a | 0.0 | 1.0 | 2.0 | NaN |
# мы думаем, что индексы будут выровнены,
# но нет:
(df).equals(df_2)
False
# а вот если их выровнять вручную,
# то все получится:
(df).equals(df_2.sort_index())
True
Сравнение массиво-подобных объектов
Выполнить сравнение объекта Pandas с каким-нибудь скалярным значением - это проще простого:
s = pd.Series(np.random.randint(0, 2, 10))
s
0 0 1 0 2 1 3 1 4 1 5 1 6 1 7 0 8 0 9 0 dtype: int32
s == 0
0 True 1 True 2 False 3 False 4 False 5 False 6 False 7 True 8 True 9 True dtype: bool
idx = pd.Index(list('abaabba'))
idx
Index(['a', 'b', 'a', 'a', 'b', 'b', 'a'], dtype='object')
idx == 'b'
array([False, True, False, False, True, True, False])
Точно так же выполняется сравнение между массиво-подобными объектами одинаковой длины:
pd.Series(list('ababaa')) == pd.Index(list('aabbaa'))
0 True 1 False 2 False 3 True 4 True 5 True dtype: bool
pd.Series(list('ababaa')) == np.array(list('abaaaa'))
0 True 1 True 2 True 3 False 4 True 5 True dtype: bool
Но если попробовать сравнить два объекта разной длины, то мы увидим ошибку:
pd.Series(list('ababaa')) == pd.Series(['a'])
ValueError: Can only compare identically-labeled Series objects
pd.Series(list('ababaa')) == pd.Index(['a', 'b', 'a'])
ValueError: ('Lengths must match to compare', (6,), (3,))
Это очень сильно отличается от поведения массивов NumPy в котором операция сравнения может транслироваться:
np.array([2, 3, 5, 2]) < np.array([3])
array([ True, False, False, True])
Или просто вернуть False, если транслирование не может быть выполнено:
np.array([2, 3, 5, 2]) == np.array([3, 5])
DeprecationWarning: elementwise comparison failed; this will raise an error in the future. False
Объединение перекрывающихся наборов данных
Иногда появляется необходимость комбинировать несколько наборов данных, при котором имеющиеся значения одного набора могли бы заменять соответствующие отсутствующие значения другого набора. Например у нас есть две серии:
s1 = pd.Series([0, np.nan, 2, 3, np.nan])
s2 = pd.Series([np.nan, 1, 2, 3, np.nan])
Мы видим, что некоторые пропущенные значения в одной серии, могут быть заменены на имеющиеся значения из другой, а значит было бы круто объединить эти эначения вместе. Это можно сделать с помощью метода combine_first():
s1.combine_first(s2)
0 0.0 1 1.0 2 2.0 3 3.0 4 NaN dtype: float64
В итоге осталось всего одно значение NaN, которое отсутствует в обеих сериях. Однако, данный метод несколько интереснее чем может показаться. Допустим, в одной серии хранятся свежеполученные данные, но этих данных мало и некоторые из них являются пропущенными, а в другой серии, хранятся не совсем актуальные, но все же полезные данные. Мы конечно можем заменять NaN-ы на среднее или медианное значение, но иногда гораздо целесообразнее заменять их на соответствующие значения из предыдущих выборок. Итак, допустим первый набор данных выглядит так:
data_1 = pd.Series([0, 1, np.nan, 3, 4, np.nan])
А другой, вот так:
data_2 = pd.Series([1, np.nan, 3, np.nan, 9, 5, 4, 1])
Тогда объединить два набора можно так:
data_1.combine_first(data_2)
0 0.0 1 1.0 2 3.0 3 3.0 4 4.0 5 5.0 6 4.0 7 1.0 dtype: float64
Заметьте, что в первом наборе данных NaN-ы были заменены на соответствующие значения из второго набора, а так же к нему в "хвост" так же добавились данные из второго набора. Иногда такое добавление данных в конец является нежелательным и поэтому в результат можно включить только срез необходимой длины:
data_1.combine_first(data_2)[:len(data_1)]
0 0.0 1 1.0 2 3.0 3 3.0 4 4.0 5 5.0 dtype: float64
Данный метод, так же подходит и для датафреймов, но на самом деле данный метод вызывает более общий - DataFrame.combine()
, который позволяет задавать собственные функции, определяющие, то как должно выполняться объединение.
Методы описательной статистики
В Pandas существует множества функций для вычисления описательных статистик данных, хранящихся в сериях и датафреймах. Многие из этих функций являются агрегирующими и выдают результат в виде серий и датафреймов меньших размеров (например sum()
, mean()
), другие (например cumsum()
и cumprod()
) и выдают результат того же размера.
Все эти методы принимают аргумент axis
который позволяет задать ось вдоль которой должна выполняться функция. Для серий данный аргумент не имеет никакого смысла, так как они являются одномерными, а для датафреймов он может быть равен одному из следующих значений:
-
'index'
(т.е.axis = 'index'
или что тоже самоеaxis = 0
) - что соответствует вычислениям по столбцам (данное значение установлено по умолчанию); -
'columns'
(т.е.axis = 'columns'
или что тоже самоеaxis = 1
)- соответствует вычислениям вдоль строк.
s = pd.Series(range(10))
s.sum(), s.mean()
(45, 4.5)
df = pd.DataFrame(np.arange(16, dtype=np.float).reshape(4, 4),
index=list('abcd'),
columns=['col_' + str(i) for i in range(4)])
df
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | 0.0 | 1.0 | 2.0 | 3.0 |
b | 4.0 | 5.0 | 6.0 | 7.0 |
c | 8.0 | 9.0 | 10.0 | 11.0 |
d | 12.0 | 13.0 | 14.0 | 15.0 |
# сумма по столбцам:
df.sum() # axis='index' по умолчанию
col_0 24 col_1 28 col_2 32 col_3 36 dtype: int64
# сумма по строкам:
df.sum(axis='columns')
a 6 b 22 c 38 d 54 dtype: int64
У всех этих методов есть аргумент skipna
позволяющий исключать отсутствующие значения:
df['col_3'][0] = np.nan
df
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | 0.0 | 1.0 | 2.0 | NaN |
b | 4.0 | 5.0 | 6.0 | 7.0 |
c | 8.0 | 9.0 | 10.0 | 11.0 |
d | 12.0 | 13.0 | 14.0 | 15.0 |
df.sum(axis=1)
a 3.0 b 22.0 c 38.0 d 54.0 dtype: float64
df.sum(axis=1, skipna=False)
a NaN b 22.0 c 38.0 d 54.0 dtype: float64
Учитывая, что арифметические операции транслируются, то данные методы могут быть использованы в математических выражениях:
((df - df.mean()) / df.std()).std()
col_0 1.0 col_1 1.0 col_2 1.0 col_3 1.0 dtype: float64
Аккумулирующие функции сохраняют размеры датафреймов и серий, но если в них содержатся NaN-ы то их расположение так же сохраняется:
pd.Series([1, 2, np.nan, 4, 5, np.nan, 6]).cumsum()
0 1.0 1 3.0 2 NaN 3 7.0 4 12.0 5 NaN 6 18.0 dtype: float64
df.cumsum()
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
a | 0.0 | 1.0 | 2.0 | NaN |
b | 4.0 | 6.0 | 8.0 | 7.0 |
c | 12.0 | 15.0 | 18.0 | 18.0 |
d | 24.0 | 28.0 | 32.0 | 33.0 |
Так же обратите внимание на то, что некоторые аналогичные методы NumPy могут игнорировать значения NaN в сериях:
np.sum(df['col_3'])
33.0
np.sum(df['col_3'].to_numpy())
nan
np.nansum(df['col_3'].to_numpy())
33.0
Каждый такой метод принимает аргумент level
, который нужен при работе с иерархическими индексами, но подробнее этот нюанс будет рассмотрен на странице по мультииндексу.
Вот краткая информация по основным функция описательной статистики:
count
- количество значений не равных NaN;;sum
- сумма значений элементов;mean
- среднее значение элементов;;mad
- среднее абсолютных значений элементов;;median
- медиана значений элементов (половина значений меньше медианы, другая половина больше);;min
- минимальное значение;;max
- максимальное значение;;mode
- наиболее часто-встречающееся (типичное) значение;abs
- абсолютное значение элементов;;prod
- произведение значений;std
- стандартное отклонение с поправкой Бесселя (используется n - 1 вместо n);var
- несмещенная (исправленная) дисперсия;sem
- стандартная ошибка среднего;skew
- асимметрия распределения выборки (третий момент);kurt
- куртозис (коэфициент эксцеса) распределения выборки, характеризует остроту вершин распределения выборки и тяжесть (толщину) хвостов;quantile
- квантиль значений, т.е. значение которое не будет превышено с заданной вероятностью (т.е. просто процент количества значений, которые являются меньше чем указанное значение);cumsum
- кумулятивная сумма;cumprod
- кумулятивное произведение;cummax
- кумулятивный максимум;cummin
- кумулятивный минимум;
Краткая статистическая сводка
Иногда бывает полезно быстро узнать какие-нибудь сводные статистики о данных в серии или каждого столбца в датафрейме. Для этого существует метод describe()
который вычисляет эти статистики (без учета значений NaN):
s = pd.Series(np.random.randn(1000))
s[::3] = np.nan
s
0 NaN 1 0.694777 2 -0.260088 3 NaN 4 -0.067500 ... 995 -0.058663 996 NaN 997 0.995671 998 0.298379 999 NaN Length: 1000, dtype: float64
s.describe()
count 666.000000 mean -0.063356 std 0.980980 min -3.485490 25% -0.695968 50% -0.058447 75% 0.598769 max 2.737608 dtype: float64
Глядя на данный вывод мы можем заметить, что среднее значение близко к 0, а стандартное значение близко к 1. К тому же 50-й процентиль (он же второй квантиль, он же медиана) тоже очень близок к среднему значению, так что перед нами, судя по всему, действительно, нормально распределенная случайная величина.
Для датафреймов подобные статистики вычисляются для каждого столбца в отдельности:
data = pd.DataFrame(np.random.randn(2000, 4),
columns = ['col_' + str(i) for i in range(4)])
data.iloc[::3] = np.nan
data
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
0 | NaN | NaN | NaN | NaN |
1 | 0.673580 | 0.469629 | 2.033159 | -1.201297 |
2 | 1.347816 | -0.652112 | 0.238076 | 0.692468 |
3 | NaN | NaN | NaN | NaN |
4 | 1.637430 | -0.292464 | 0.160859 | -0.463325 |
... | ... | ... | ... | ... |
1995 | NaN | NaN | NaN | NaN |
1996 | 0.975141 | -0.754659 | 0.769076 | 1.164425 |
1997 | -0.644748 | 1.060235 | 0.949261 | -0.667111 |
1998 | NaN | NaN | NaN | NaN |
1999 | -2.609587 | -1.201645 | -1.526409 | 0.419282 |
data.describe()
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
count | 1333.000000 | 1333.000000 | 1333.000000 | 1333.000000 |
mean | -0.007838 | -0.054281 | 0.007605 | 0.005540 |
std | 0.992171 | 1.020226 | 0.981725 | 1.020494 |
min | -3.729444 | -3.570644 | -3.632059 | -2.835269 |
25% | -0.652890 | -0.778349 | -0.677309 | -0.665894 |
50% | 0.028582 | -0.055274 | 0.014708 | 0.042450 |
75% | 0.673580 | 0.643916 | 0.690836 | 0.674617 |
max | 3.290922 | 3.803188 | 3.008416 | 3.390709 |
Можно указать дополнительное количество процентилей:
data.describe(percentiles=[0.05, 0.25, 0.5, 0.75, 0.95])
col_0 | col_1 | col_2 | col_3 | |
---|---|---|---|---|
count | 1333.000000 | 1333.000000 | 1333.000000 | 1333.000000 |
mean | -0.007838 | -0.054281 | 0.007605 | 0.005540 |
std | 0.992171 | 1.020226 | 0.981725 | 1.020494 |
min | -3.729444 | -3.570644 | -3.632059 | -2.835269 |
5% | -1.688970 | -1.710802 | -1.574646 | -1.720128 |
25% | -0.652890 | -0.778349 | -0.677309 | -0.665894 |
50% | 0.028582 | -0.055274 | 0.014708 | 0.042450 |
75% | 0.673580 | 0.643916 | 0.690836 | 0.674617 |
95% | 1.596295 | 1.632087 | 1.587718 | 1.642854 |
max | 3.290922 | 3.803188 | 3.008416 | 3.390709 |
Для серий состоящих из нечисловых значений метод describe()
возвращает:
count
- количество не равных NaN элементов;unique
- количество уникальных элементов;top
- наиболее часто встречающееся значение;freq
- число появленийtop
-значения в данных.
s = pd.Series(list('ababaaacbabbaa'))
s[[1, 3, 5]] = np.nan
s
0 a 1 NaN 2 a 3 NaN 4 a 5 NaN 6 a 7 c 8 b 9 a 10 b 11 b 12 a 13 a dtype: object
s.describe()
count 11 unique 3 top a freq 7 dtype: object
Для датафреймов, состоящих из числовых и нечисловых столбцов, по умолчанию, всегда выводится сводка только по числовым столбцам:
data = pd.DataFrame({'cat': list('a'*3 + 'b'*7),
'num': np.random.rand(10)})
data
cat | num | |
---|---|---|
0 | a | 0.233899 |
1 | a | 0.704844 |
2 | a | 0.277895 |
3 | b | 0.388364 |
4 | b | 0.379752 |
5 | b | 0.431376 |
6 | b | 0.944682 |
7 | b | 0.804738 |
8 | b | 0.595195 |
9 | b | 0.088990 |
data.describe()
num | |
---|---|
count | 10.000000 |
mean | 0.484973 |
std | 0.270816 |
min | 0.088990 |
25% | 0.303359 |
50% | 0.409870 |
75% | 0.677431 |
max | 0.944682 |
Указать, какие именно столбцы должны попасть в сводку можно с помощью параметра include
:
data.describe(include=['object'])
cat | |
---|---|
count | 10 |
unique | 2 |
top | b |
freq | 7 |
data.describe(include=['number'])
num | |
---|---|
count | 10.000000 |
mean | 0.484973 |
std | 0.270816 |
min | 0.088990 |
25% | 0.303359 |
50% | 0.409870 |
75% | 0.677431 |
max | 0.944682 |
data.describe(include='all')
cat | num | |
---|---|---|
count | 10 | 10.000000 |
unique | 2 | NaN |
top | b | NaN |
freq | 7 | NaN |
mean | NaN | 0.484973 |
std | NaN | 0.270816 |
min | NaN | 0.088990 |
25% | NaN | 0.303359 |
50% | NaN | 0.409870 |
75% | NaN | 0.677431 |
max | NaN | 0.944682 |
Метод describe()
позволяет так же указывать какго типа столбцы должны попасть в сводку, но обычно это очень редко используется на практике. Однако, если практика на то и практика что бы там могло понадобиться все что угодно, так что не забывайте обращаться к официальной документации.
Очень часто бывает полезным подсчитать количество уникальных значений в серии (кроме NaN, естественно), для этого подойдет метод nunique()
:
s = pd.Series([1, 1, np.nan, 2, 2, 2])
s.nunique()
2
s = pd.Series(np.random.rand(10))
s[-3:] = np.nan
s[:5] = 0.5
s
0 0.500000 1 0.500000 2 0.500000 3 0.500000 4 0.500000 5 0.063941 6 0.618402 7 NaN 8 NaN 9 NaN dtype: float64
s.nunique()
3
Индексы минимальных и максимальных значений
Что бы узнать индекс максимального или минимального элемента в серии можно воспользоваться методом idxmin()
или idxmax()
(если таких значений несколько то будет возвращен индекс первого встретившегося):
s = pd.Series([1, 1, 2, 2, 3, 3])
s.idxmin(), s.idxmax()
(0, 4)
s = pd.Series(np.random.randint(100, 1000, 1000))
# индекс элемента с максимальным значением
s.idxmax()
980
# сам элемент с максимальным значением
s[s.idxmax()]
999
Если серия снабжена дополнительным индексом, то возвращены будут метки из данного индекса:
s = pd.Series([1, 2, 0, 4],
index=list('abcd'))
s
a 1 b 2 c 0 d 4 dtype: int64
s.idxmin()
'c'
Для датафреймов, данные методы позволяют указывать направление оси вдоль которой выполняется поиск:
df = pd.DataFrame(np.random.randint(0, 20, (5, 5)),
index=list('abcde'),
columns=['C_' + str(i) for i in range(5)])
df
C_0 | C_1 | C_2 | C_3 | C_4 | |
---|---|---|---|---|---|
a | 3 | 6 | 6 | 19 | 17 |
b | 5 | 7 | 19 | 0 | 0 |
c | 4 | 10 | 12 | 14 | 13 |
d | 16 | 10 | 18 | 8 | 19 |
e | 15 | 7 | 18 | 12 | 0 |
# метка строки с максимальным элементом
# по каждому столбцу:
df.idxmax() # равносильно df.idxmin(axis=0)
# или df.idxmin(axis='index')
C_0 d C_1 c C_2 b C_3 a C_4 d dtype: object
# метка столбца с максимальным элементом
# по каждой строке:
df.idxmax(axis='columns') # равносильно df.idxmin(axis=1)
a C_3 b C_2 c C_3 d C_4 e C_2 dtype: object
df.idxmin(axis=1)
a C_0 b C_3 c C_0 d C_3 e C_4 dtype: object
Подсчет значений и мода значений
Иногда данные являются категориальными или состоят из небольшого множества значений. В таких ситуациях часто возникает необходимость, подсчета количества каждого из значений. Сделать это можно с помощью метода value_counts()
:
s = pd.Series(list('ababccaabccab'))
s
0 a 1 b 2 a 3 b 4 c 5 c 6 a 7 a 8 b 9 c 10 c 11 a 12 b dtype: object
s.value_counts()
a 5 b 4 c 4 dtype: int64
s = pd.Series(np.random.randint(0, 10, 2000))
s.value_counts()
8 222 0 214 3 213 4 209 7 206 9 204 5 196 1 192 2 179 6 165 dtype: int64
Данный метод так же доступен в виде функции и может применяться к обычным одномерным массивам:
data = np.random.randint(0, 10, 2000)
pd.value_counts(data)
7 214 6 212 5 207 2 207 8 202 0 200 1 193 4 191 9 187 3 187 dtype: int64
Как видите, подсчет возвращается в неотсортированном виде, что бы исправить это можно воспользоваться методом sort_index() (или воспользоваться дополнительными параметра данного метода):
pd.value_counts(data).sort_index()
0 200 1 193 2 207 3 187 4 191 5 207 6 212 7 214 8 202 9 187 dtype: int64
Метод value_counts()
может использоваться и для подсчета уникальных комбинаций в столбцах датафреймов:
data = np.random.randint(0, 2, 20).reshape(10, 2)
df = pd.DataFrame(data, columns=['a', 'b'])
df
a | b | |
---|---|---|
0 | 0 | 1 |
1 | 0 | 1 |
2 | 1 | 1 |
3 | 0 | 1 |
4 | 0 | 1 |
5 | 0 | 1 |
6 | 0 | 0 |
7 | 0 | 1 |
8 | 1 | 0 |
9 | 0 | 0 |
df.value_counts()
a b 0 1 6 0 2 1 1 1 0 1 dtype: int64
Получить наиболее часто встречающееся значение (или значения, если их несколько) можно с помощью метода mode()
:
s = pd.Series([1, 2, 3, 3, 3, 4, 5])
s.mode()
0 3 dtype: int64
s = pd.Series([1, 1, 1, 2, 3, 3, 3])
s.mode()
0 1 1 3 dtype: int64
data_1 = np.random.randint(0, 5, 100)
data_2 = np.random.randint(5, 10, 100)
df = pd.DataFrame({'a': data_1,
'b': data_2})
df.mode()
a | b | |
---|---|---|
0 | 2.0 | 6 |
1 | NaN | 8 |
Данный вывод означает что в столбце a чаще всего встречается значение 2, а в столбце b чаще всего встречаются значения 6 и 8 которых поровну в чем очень дегко убедиться:
df['a'].value_counts()
2 26 4 24 0 19 1 16 3 15 Name: a, dtype: int64
df['b'].value_counts()
8 24 6 24 7 22 9 16 5 14 Name: b, dtype: int64
Дискретизация непрерывных значений
Некоторые величины, например такие как возраст или количество выпавших осадков, являются непрерывными. Тем не менее иногда приходится раскладывать наблюдения таких величин по дискретным "ящикам" - интервалам значений. Для возраста, мы можем выделить интервалы (18, 35], (35, 75], (75, 110] и назвать их "молодой", "зрелый" и "пожилой" соответственно. Выполнить подобное разбиение непрерывной величины в Pandas можно с помощью метода cut()
:
s = pd.Series([0.11, 0.19, 0.23, 0.27, 0.33, 0.39])
hist_s = pd.cut(s, 3)
hist_s
0 (0.11, 0.203] 1 (0.11, 0.203] 2 (0.203, 0.297] 3 (0.203, 0.297] 4 (0.297, 0.39] 5 (0.297, 0.39] dtype: category Categories (3, interval[float64]): [(0.11, 0.203] < (0.203, 0.297] < (0.297, 0.39]]
В данном случае, мы разбили значения [0.11, 0.19, 0.23, 0.27, 0.33, 0.39]
на три непересекающихся интервала одинаковой длины, причем в каждом оказалось по два значения, в чем легко убедиться:
pd.value_counts(hist_s)
(0.297, 0.39] 2 (0.203, 0.297] 2 (0.11, 0.203] 2 dtype: int64
Интервалы вовсе не обязательно должны быть одинаковыми и вместо их количества мы можем задавать их границы:
data = 10*np.random.randn(300)
hist_data = pd.cut(data, [-30, -5, 0, 5, 30])
hist_data
[(0, 5], (-30, -5], (5, 30], (-5, 0], (0, 5], ..., (-30, -5], (5, 30], (-30, -5], (-30, -5], (5, 30]] Length: 300 Categories (4, interval[int64]): [(-30, -5] < (-5, 0] < (0, 5] < (5, 30]]
pd.value_counts(hist_data)
(-30, -5] 94 (5, 30] 88 (-5, 0] 65 (0, 5] 51 dtype: int64
Вообще, данные могут быть и целого типа, но тогда нужно указывать вещественные границы интервалов, так чтобы в них точно попадали все значения:
data = np.random.randint(0, 10, 300)
hist_data = pd.cut(data, [0, 5, 10])
hist_data
[(0, 5], (5, 10], (0, 5], (5, 10], (5, 10], ..., NaN, (5.0, 10.0], (0.0, 5.0], (0.0, 5.0], (0.0, 5.0]] Length: 300 Categories (2, interval[int64]): [(0, 5] < (5, 10]]
В примере выше мы указали границы, как [0, 5, 10] что соответствует двум интервалам (0, 5] и (5, 10], но 0 не входит в интервал (0, 5], а значит не учитывается при дискретизации, в чем тоже очень легко убедиться:
pd.value_counts(hist_data).sum()
272
При указывании границ интервалов можно использовать значения np.inf, т.е. по сути можно просто разбить данные пополам относительно некоторого значения:
data = np.random.randn(300)
hist_data = pd.cut(data, [-np.inf, 0, np.inf])
pd.value_counts(hist_data)
(-inf, 0.0] 155 (0.0, inf] 145 dtype: int64
Еще один способ дискретизации заключается в том что бы указывать не количество интервалов или их границы, а квантили. Сделать это можно с помощью метода qcut()
:
data = np.random.randn(50)
hist_data = pd.qcut(data, [0, 0.25, 0.75, 1])
hist_data
[(0.411, 1.765], (-0.715, 0.411], (-2.1229999999999998, -0.715], (0.411, 1.765], (-0.715, 0.411], ..., (0.411, 1.765], (-0.715, 0.411], (-0.715, 0.411], (-0.715, 0.411], (-0.715, 0.411]] Length: 50 Categories (3, interval[float64]): [(-2.1229999999999998, -0.715] < (-0.715, 0.411] < (0.411, 1.765]]
pd.value_counts(hist_data)
(-0.715, 0.411] 24 (0.411, 1.765] 13 (-2.1229999999999998, -0.715] 13 dtype: int64
Метод qcut()
полезен когда мы работаем с асемметричным распределением:
data = np.random.beta(2, 3, 150)
hist_data = pd.qcut(data, [0, 0.25, 0.75, 1])
hist_data
[(0.535, 0.919], (0.0114, 0.24], (0.535, 0.919], (0.24, 0.535], (0.0114, 0.24], ..., (0.24, 0.535], (0.0114, 0.24], (0.24, 0.535], (0.0114, 0.24], (0.24, 0.535]] Length: 150 Categories (3, interval[float64]): [(0.0114, 0.24] < (0.24, 0.535] < (0.535, 0.919]]
pd.value_counts(hist_data)
(0.24, 0.535] 74 (0.535, 0.919] 38 (0.0114, 0.24] 38 dtype: int64
В данном случае мы просто получаем интервалы такой ширины, что значения выборки могут попасть в них только с указанной межквантильной вероятностью. Например, для интервала границы которого определяют 0.25 и 0.75 квантили, вероятность попадания значений в него равна 0.5. А для остальных двух интервалов вероятность равна 0.25.
Ну и конечно же удобен такой метод тем, что значения всегда можно очень легко разделить на две части используя медиану (0.5-й квантиль) в качестве разделителя:
data = np.random.beta(2, 3, 150)
hist_data = pd.qcut(data, [0, 0.5, 1])
hist_data
[(0.395, 0.863], (0.00799, 0.395], (0.00799, 0.395], (0.00799, 0.395], (0.00799, 0.395], ..., (0.395, 0.863], (0.00799, 0.395], (0.00799, 0.395], (0.395, 0.863], (0.00799, 0.395]] Length: 150 Categories (2, interval[float64]): [(0.00799, 0.395] < (0.395, 0.863]]
pd.value_counts(hist_data)
(0.395, 0.863] 75 (0.00799, 0.395] 75 dtype: int64
Применение функций
Для того что бы обрабатывать данные в датафреймах и сериях нужно использовать собственные функции или функции сторонних библиотек. Для этого нужно четко понимать над чем собирается работать функция: над серией или над датафреймом, будет ли она выполняться построчно или по столбцам, а может она будет работать поэлементно? Со всем этим мы и попробуем разобраться в данном разделе.
В Pandas есть следующие методы, которые облегчают работу с функциями и их применение:
pipe()
- для применения табличных функций;apply()
- для применения функций к строкам и столбцам;agg()
иtransform()
- для функций выполняющих агрегацию;applymap()
- для применения функций к каждому элементу.
Применение табличных функций
Датафреймы и серии могут быть аргументами функций и для их применения может пригодиться метод pipe()
. Давайте придумаем какой-нибудь простой пример. Допустим у нас есть вот такой датафрейм:
data = pd.DataFrame(np.random.randint(1, 10, (5, 3)),
columns=list('XYZ'))
data
X | Y | Z | |
---|---|---|---|
0 | 8 | 1 | 1 |
1 | 8 | 9 | 2 |
2 | 3 | 3 | 8 |
3 | 1 | 6 | 4 |
4 | 6 | 2 | 9 |
Допустим, нам нужно вычислить среднее геометрическое значений каждой строки, для чего мы создали вот таку функцию:
def geom(df, n):
df['geom'] = (df['X'] * df['Y'] * df['Z'])**n
return df
Мы можем просто воспользоваться этой функцией и получить необходимый результат:
geom(data, 1/3)
X | Y | Z | geom | |
---|---|---|---|---|
0 | 8 | 1 | 1 | 2.000000 |
1 | 8 | 9 | 2 | 5.241483 |
2 | 3 | 3 | 8 | 4.160168 |
3 | 1 | 6 | 4 | 2.884499 |
4 | 6 | 2 | 9 | 4.762203 |
Но метод pipe()
позволяет использовать данную функцию вот так:
data.pipe(geom, n=1/3)
X | Y | Z | geom | |
---|---|---|---|---|
0 | 8 | 1 | 1 | 2.000000 |
1 | 8 | 9 | 2 | 5.241483 |
2 | 3 | 3 | 8 | 4.160168 |
3 | 1 | 6 | 4 | 2.884499 |
4 | 6 | 2 | 9 | 4.762203 |
Казалось бы? что в таких ухищрениях нет никакой надобности, но очень часто к одному датафрейму (или серии) применяется несколько функций. Допустим у нас появилась еще одна функция, вычисляющая среднее гармоническое элементов каждой строки:
def harm(df, n):
df['harm'] = n / (1/df['X'] + 1/df['Y'] + 1/df['Z'])
return df
Без метода pipe()
мы попытались бы выполнить вот такой код и он сработает:
harm(geom(data, 1/3), 3)
X | Y | Z | geom | harm | |
---|---|---|---|---|---|
0 | 8 | 1 | 1 | 2.000000 | 1.411765 |
1 | 8 | 9 | 2 | 5.241483 | 4.075472 |
2 | 3 | 3 | 8 | 4.160168 | 3.789474 |
3 | 1 | 6 | 4 | 2.884499 | 2.117647 |
4 | 6 | 2 | 9 | 4.762203 | 3.857143 |
Но если таких функций не две, а больше двух? В этом случае нам придется сначала проследить глазами за цепочкой функций, а только потом понять над чем они выполняются. Метод pipe()
позволяет сначала обращать внимание на данные, а только потом следить за тем что с ними делается, т.е. какие функции и скакими параметрами используются:
data.pipe(harm, 3).pipe(geom, 1/3)
X | Y | Z | geom | harm | |
---|---|---|---|---|---|
0 | 8 | 1 | 1 | 2.000000 | 1.411765 |
1 | 8 | 9 | 2 | 5.241483 | 4.075472 |
2 | 3 | 3 | 8 | 4.160168 | 3.789474 |
3 | 1 | 6 | 4 | 2.884499 | 2.117647 |
4 | 6 | 2 | 9 | 4.762203 | 3.857143 |
Если функций слишком много, то всю команду можно обернуть в скобки и расставить переносы с отступами:
(data.pipe(harm, 3)
.pipe(geom, 1/3))
X | Y | Z | geom | harm | |
---|---|---|---|---|---|
0 | 8 | 1 | 1 | 2.000000 | 1.411765 |
1 | 8 | 9 | 2 | 5.241483 | 4.075472 |
2 | 3 | 3 | 8 | 4.160168 | 3.789474 |
3 | 1 | 6 | 4 | 2.884499 | 2.117647 |
4 | 6 | 2 | 9 | 4.762203 | 3.857143 |
Сейчас мы рассмотрели только те функции, которые принимают данные в качестве первого аргумента, но некоторые функции могут принимать в качестве первого аргумента что-то другое, а данные в качестве ключевого аргумента. И ключевого означает, что данные передаются в функцию по ключу, а не в том смысле, что данные являются главным аргументом функции. Давайте приведем пример такой функции:
def poly(n, XYZ=None, a=1, b=1, c=1):
data['poly'] = a*data['X']**n + b*data['Y']**(n - 1) + \
c*data['Z']**(n - 2)
return data
Данная функция принимает данные в качестве второго ключевого аргумента по имени 'XYZ'
, что бы она заработала в методе pipe()
, ее нужно указать в кортеже вместе с именем аргумента, принимающим данные:
data.pipe((poly, 'XYZ'), 3, a=3, b=4, c=2)
X | Y | Z | geom | harm | poly | |
---|---|---|---|---|---|---|
0 | 8 | 1 | 1 | 2.000000 | 1.411765 | 1542 |
1 | 8 | 9 | 2 | 5.241483 | 4.075472 | 1864 |
2 | 3 | 3 | 8 | 4.160168 | 3.789474 | 133 |
3 | 1 | 6 | 4 | 2.884499 | 2.117647 | 155 |
4 | 6 | 2 | 9 | 4.762203 | 3.857143 | 682 |
А цепочка из всех трех функций может выглядеть следующим образом:
(data.pipe(harm, 3)
.pipe(geom, 1/3)
.pipe((poly, 'XYZ'), 3, a=3, b=4, c=2))
X | Y | Z | geom | harm | poly | |
---|---|---|---|---|---|---|
0 | 8 | 1 | 1 | 2.000000 | 1.411765 | 1542 |
1 | 8 | 9 | 2 | 5.241483 | 4.075472 | 1864 |
2 | 3 | 3 | 8 | 4.160168 | 3.789474 | 133 |
3 | 1 | 6 | 4 | 2.884499 | 2.117647 | 155 |
4 | 6 | 2 | 9 | 4.762203 | 3.857143 | 682 |
Выполнение функций по столбцам и строкам
Многие методы датафреймов и серий, например, методы описательной статистики, могут принимать необезательный аргумент axis
который позволяет указать вдоль чего должны выполняться эти методы: строк или столбцов. Метод apply()
позволяет использовать произвольные функции точно так же, указывая в каком направлении (вдоль какой оси) они должны выполняться. Допустим у нас есть вот такой датафрейм:
df = pd.DataFrame(np.random.randint(1, 10, (5, 5)),
index=list('abcde'),
columns=['col_' + str(i) for i in range(5)])
df
col_0 | col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|---|
a | 6 | 5 | 5 | 9 | 9 |
b | 7 | 9 | 4 | 8 | 7 |
c | 2 | 3 | 5 | 7 | 9 |
d | 3 | 9 | 8 | 2 | 1 |
e | 2 | 3 | 2 | 8 | 6 |
Допустим мы хотим вычислить среднее арифметическое значений каждого столбца, для чего можем воспользоваться методом mean()
:
df.mean() # df.mean(axis='index') или df.mean(axis=0)
col_0 4.0 col_1 5.8 col_2 4.8 col_3 6.8 col_4 6.4 dtype: float64
Если нам нужно вычислить среднее арифметическое, вдоль строк, то мы просто указываем что axis=1 (или axis='columns'):
df.mean(axis=1)
a 6.8 b 7.0 c 5.2 d 4.6 e 4.2 dtype: float64
Но тот же самый метод взятый из библиотеки NumPy может быть использован с помощью метода apply()
:
df.apply(np.mean)
col_0 4.0 col_1 5.8 col_2 4.8 col_3 6.8 col_4 6.4 dtype: float64
df.apply(np.mean, axis=1)
a 6.8 b 7.0 c 5.2 d 4.6 e 4.2 dtype: float64
А благодаря тому, что apply()
позволяет использовать лямбда-функции мы можем вычислить среднее арифметическое даже вот так:
df.apply(lambda vec: np.sum(vec) / len(vec), axis=1)
a 6.8 b 7.0 c 5.2 d 4.6 e 4.2 dtype: float64
Лямбда функции, как раз очень удобны тем, что позволяют быстро выполнить какие-то действия, которые не предусмотрены используемыми библиотеками:
df.apply(lambda x: x**2 + 1)
col_0 | col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|---|
a | 37 | 26 | 26 | 82 | 82 |
b | 50 | 82 | 17 | 65 | 50 |
c | 5 | 10 | 26 | 50 | 82 |
d | 10 | 82 | 65 | 5 | 2 |
e | 5 | 10 | 5 | 65 | 37 |
Как вы заметили, функции, передаваемые методу apply()
могут выполняться не только вдоль строк или столбцов, но и поэлементно:
df.apply(np.sqrt)
col_0 | col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|---|
a | 2.449490 | 2.236068 | 2.236068 | 3.000000 | 3.000000 |
b | 2.645751 | 3.000000 | 2.000000 | 2.828427 | 2.645751 |
c | 1.414214 | 1.732051 | 2.236068 | 2.645751 | 3.000000 |
d | 1.732051 | 3.000000 | 2.828427 | 1.414214 | 1.000000 |
e | 1.414214 | 1.732051 | 1.414214 | 2.828427 | 2.449490 |
Если необходимая функция находится в пространстве имен Pandas, то ее имя можно передать в виде строки:
df.apply('mean', axis=1)
a 6.8 b 7.0 c 5.2 d 4.6 e 4.2 dtype: float64
Учитывая что, лямбда функции могут возвращать последовательности, то мы можем вычислять сразу несколько функций вдоль строк или столбцов:
df.apply(lambda x: [np.var(x), np.std(x)], axis=1)
a [3.3600000000000003, 1.8330302779823362] b [2.8, 1.6733200530681511] c [6.5600000000000005, 2.5612496949731396] d [10.64, 3.2619012860600183] e [5.76, 2.4] dtype: object
Полученные последовательности мы можем распаковать с помощью параметра result_type
установленного в значение 'expand'
:
df.apply(lambda x: [np.var(x), np.std(x)], axis=1, result_type='expand')
0 | 1 | |
---|---|---|
a | 3.36 | 1.833030 |
b | 2.80 | 1.673320 |
c | 6.56 | 2.561250 |
d | 10.64 | 3.261901 |
e | 5.76 | 2.400000 |
А если нам хочется получить в результате датафрейм с осмысленными столбцами, то можем использовать серии, указав необходимые названия индексов, которые в итоге станут метками столбцов (или строк):
df.apply(lambda x: pd.Series([np.var(x), np.std(x)],
index=['var', 'std']),
axis=1)
var | std | |
---|---|---|
a | 3.36 | 1.833030 |
b | 2.80 | 1.673320 |
c | 6.56 | 2.561250 |
d | 10.64 | 3.261901 |
e | 5.76 | 2.400000 |
df.apply(lambda x: pd.Series([np.var(x), np.std(x)],
index=['var', 'std']),
axis=0)
col_0 | col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|---|
var | 4.400000 | 7.360000 | 3.760000 | 6.160000 | 8.640000 |
std | 2.097618 | 2.712932 | 1.939072 | 2.481935 | 2.939388 |
В результате работы функций вдоль очей или столбцов мы получаем серию:
df.apply(lambda x: np.sum(x), axis=1)
a 34 b 35 c 26 d 23 e 21 dtype: int64
Но иногда, бывает нужно транслировать полученный результат на все столбцы или строки:
df.apply(lambda x: np.sum(x), axis=1, result_type='broadcast')
col_0 | col_1 | col_2 | col_3 | col_4 | |
---|---|---|---|---|---|
a | 34 | 34 | 34 | 34 | 34 |
b | 35 | 35 | 35 | 35 | 35 |
c | 26 | 26 | 26 | 26 | 26 |
d | 23 | 23 | 23 | 23 | 23 |
e | 21 | 21 | 21 | 21 | 21 |
Но если функция возвращает последовательность чисел длина которой несовместима с исходным датафреймом, то это приведет к ошибке:
# вот это сработает:
df.apply(lambda x: [2, 4, 6, 8, 0], axis=0, result_type='broadcast')
# а это выдаст ошибку:
df.apply(lambda x: [2, 4, 0], axis=0, result_type='broadcast')
Метод apply()
хорош тем, что он позволяет очень быстро ответить на многие вопросы о данных, например, очень часто бывает необходимо выяснить, в какие даты наблюдались максимальные и минимальные значения той или инной величины:
data = pd.DataFrame(np.random.randint(100, 300, (2000, 3)),
index=pd.date_range('1/1/2010', periods=2000),
columns=['a', 'b', 'c'])
data
a | b | c | |
---|---|---|---|
2010-01-01 | 115 | 182 | 221 |
2010-01-02 | 100 | 121 | 252 |
2010-01-03 | 146 | 161 | 251 |
2010-01-04 | 143 | 277 | 145 |
2010-01-05 | 249 | 225 | 170 |
... | ... | ... | ... |
2015-06-19 | 233 | 132 | 209 |
2015-06-20 | 212 | 211 | 257 |
2015-06-21 | 133 | 133 | 160 |
2015-06-22 | 162 | 141 | 135 |
2015-06-23 | 263 | 125 | 150 |
data.apply(lambda x: pd.Series([x.idxmax(), x.idxmin()],
index=['max', 'min']))
a | b | c | |
---|---|---|---|
max | 2010-04-12 | 2010-02-13 | 2011-03-17 |
min | 2010-01-02 | 2010-07-24 | 2010-06-06 |
Если у нас есть функция с позиционными и ключевыми аргументами, то она тоже может быть передана методу apply()
. Допустим, у нас есть вот такой датафрейм:
df = pd.DataFrame(np.arange(16).reshape(4, 4),
columns=list('ABCD'))
df
A | B | C | D | |
---|---|---|---|---|
0 | 0 | 1 | 2 | 3 |
1 | 4 | 5 | 6 | 7 |
2 | 8 | 9 | 10 | 11 |
3 | 12 | 13 | 14 | 15 |
И нам нужно использовать вот такую функцию:
def prod_and_sum(x, term, factor=1):
return factor*(x + term)
Позиционные аргументы могут быть переданы в apply()
виде кортежа и присвоены специальному параметру args
, а ключевые аргументы просто перечисляются с указанием их значений:
df.apply(prod_and_sum, args=(2,), factor=2)
A | B | C | D | |
---|---|---|---|---|
0 | 4 | 6 | 8 | 10 |
1 | 12 | 14 | 16 | 18 |
2 | 20 | 22 | 24 | 26 |
3 | 28 | 30 | 32 | 34 |
Еще одна особенность метода apply()
заключается в том, что ему можно передать любой метод объектов Series, в результате чего указанный метод, может быть применен, как к строкам, так и столбцам датафрейма:
df.apply(pd.Series.sum)
A 24 B 28 C 32 D 36 dtype: int64
df.apply(pd.Series.sum, axis=1)
0 6 1 22 2 38 3 54 dtype: int64
Поддержка агрегации
Для того что бы применять агрегирующие функции существует метод aggregate() и его псевдоним agg(). Метод agg() может быть применен как к серии так и датафрейму, но лучше всего продемонстрировать его использование можно на датафрейме:
df = pd.DataFrame(np.arange(12).reshape(4, 3),
index=list('abcd'),
columns=list('XYZ'))
df
X | Y | Z | |
---|---|---|---|
a | 0 | 1 | 2 |
b | 3 | 4 | 5 |
c | 6 | 7 | 8 |
d | 9 | 10 | 11 |
df.agg(np.mean)
X 4.5 Y 5.5 Z 6.5 dtype: float64
df.agg('mean')
X 4.5 Y 5.5 Z 6.5 dtype: float64
Можем использовать данный метод для серий:
df.loc['d'].agg('mean')
10.0
Вы можете снова спросить зачем нужен это метод, ведь мы всегда можем воспользоваться методами этих объектов, которые напрямую вычисляют то что нам нужно, например вот так:
df.mean()
X 4.5 Y 5.5 Z 6.5 dtype: float64
df.loc['d'].mean()
10.0
Но метод agg()
хорош тем, что позволяет использовать несколько агрегирующих функций одновременно, просто перечислив их имена в списке:
df.agg(['sum', np.mean, 'std'])
X | Y | Z | |
---|---|---|---|
sum | 18.000000 | 22.000000 | 26.000000 |
mean | 4.500000 | 5.500000 | 6.500000 |
std | 3.872983 | 3.872983 | 3.872983 |
Обратите внимание на то, что если используются методы из пространства имен Pandas, то мы просто указываем эти методы в виде строк, а методы сторонних библиотек указываются по имени и без круглых скобок.
Применить несколько агрегирующих методов можно и к серии:
df.loc['d'].agg(['sum', 'mean', 'std'])
sum 30.0 mean 10.0 std 1.0 Name: d, dtype: float64
Наряду с обычными функциями, мы можем использовать и лямбда функции:
df.agg(['sum', lambda x: (x**2).sum()])
X | Y | Z | |
---|---|---|---|
sum | 18 | 22 | 26 |
<lambda> | 126 | 166 | 214 |
Однако, если вы будете использовать несколько лямбда-функций, то скорее всего запутаетесь, так как все результирующие строки будут иметь одинаковую метку <lambda>:
df.agg([lambda x: x.sum(), lambda x: x.mean()])
X | Y | Z | |
---|---|---|---|
<lambda> | 18.0 | 22.0 | 26.0 |
<lambda> | 4.5 | 5.5 | 6.5 |
В этом случае лучше всего создавать обычные функции и использовать их имена:
def sq_mean(x):
return (x**2).mean()
def cb_mean(x):
return (x**3).mean()
df.agg([sq_mean, cb_mean])
X | Y | Z | |
---|---|---|---|
sq_mean | 31.5 | 41.5 | 53.5 |
cb_mean | 243.0 | 352.0 | 494.0 |
Другой интересной особенностью данного метода является то, что ему можно передавать словари с ключами - именами столбцов и значениями - именами функций:
df.agg({'X': 'sum', 'Z': sq_mean,
'Y': lambda x: (1/x).sum()})
X 18.000000 Z 53.500000 Y 1.492857 dtype: float64
Это, конечно, может быть удобно, но старайтесь не запутаться, так как в выводе не указывается какая функция и к какому столбцу применялась. Однако, данный способ позволяет применять к столбцам несколько, причем, несколько разных методов:
df.agg({'X': ['min', 'max'],
'Y': ['sum', 'mean'],
'Z': ['min', 'max', 'sum', 'mean']})
X | Y | Z | |
---|---|---|---|
max | 9.0 | NaN | 11.0 |
mean | NaN | 5.5 | 6.5 |
min | 0.0 | NaN | 2.0 |
sum | NaN | 22.0 | 26.0 |
Значения NaN в данном случае означают что функция к столбце не применялась.
Если датафрейм содержит данные разного типа, то будут выполняться только вычислимые агрегирующие функции.
data = pd.DataFrame({'A': np.arange(5),
'B': np.arange(5, dtype=np.float),
'C': ['id_' + str(i) for i in range(5)],
'D': pd.date_range('2020-11-24', periods=5)})
data
A | B | C | D | |
---|---|---|---|---|
0 | 0 | 0.0 | id_0 | 2020-11-24 |
1 | 1 | 1.0 | id_1 | 2020-11-25 |
2 | 2 | 2.0 | id_2 | 2020-11-26 |
3 | 3 | 3.0 | id_3 | 2020-11-27 |
4 | 4 | 4.0 | id_4 | 2020-11-28 |
data.agg(['min', 'max', 'sum', 'mean'])
A | B | C | D | |
---|---|---|---|---|
min | 0.0 | 0.0 | id_0 | 2020-11-24 |
max | 4.0 | 4.0 | id_4 | 2020-11-28 |
sum | 10.0 | 10.0 | id_0id_1id_2id_3id_4 | NaT |
mean | 2.0 | 2.0 | NaN | 2020-11-26 |
Еще один метод, который поддерживает агрегацию по столбцам, это метод transform()
. Этот метод очень похож на метод agg()
, но не агрегирует результат, а работает поэлементно. Давайте сначала взглянем на датафрейм, что бы было проще понять работу метода:
df
X | Y | Z | |
---|---|---|---|
a | 0 | 1 | 2 |
b | 3 | 4 | 5 |
c | 6 | 7 | 8 |
d | 9 | 10 | 11 |
а теперь взглянем на пример:
df.transform([np.sqrt, lambda x: np.log(x + 1)])
X | Y | Z | ||||
---|---|---|---|---|---|---|
sqrt | <lambda> | sqrt | <lambda> | sqrt | <lambda> | |
a | 0.000000 | 0.000000 | 1.000000 | 0.693147 | 1.414214 | 1.098612 |
b | 1.732051 | 1.386294 | 2.000000 | 1.609438 | 2.236068 | 1.791759 |
c | 2.449490 | 1.945910 | 2.645751 | 2.079442 | 2.828427 | 2.197225 |
d | 3.000000 | 2.302585 | 3.162278 | 2.397895 | 3.316625 | 2.484907 |
Как видите, функция действительно работает поэлементно, но результат работы кажой функции для отдельных столбцов группируются вместе. Во всем остальном, метод transform()
очень похож на метод agg()
.
Поэлементные функции
Векторизованные функции применяются сразу ко всему массиву:
a = np.arange(10)
a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
a**2 + 1
array([ 1, 2, 5, 10, 17, 26, 37, 50, 65, 82], dtype=int32)
Согласитесь - это очень удобно. Но не все функции могут быть векторизованы и зачастую они принимают и возвращают только один аргумент. Как раз для таких функций и существуют методы applymap()
- для датафреймов, и, map()
- для серий, которые избавляют нас от необходимости писать циклы. Допустим у нас есть следующий датафрейм:
df = pd.DataFrame(np.arange(10, 22).reshape(4, 3),
index=list('abcd'),
columns=list('XYZ'))
df
X | Y | Z | |
---|---|---|---|
a | 10 | 11 | 12 |
b | 13 | 14 | 15 |
c | 16 | 17 | 18 |
d | 19 | 20 | 21 |
И представим, что нам необходимо выполнить следующую функцию к его каждому элементу:
def inverse(x):
return int(str(x)[::-1])
Тогда выполнение этой функции можно сделать так:
df.applymap(inverse)
X | Y | Z | |
---|---|---|---|
a | 1 | 11 | 21 |
b | 31 | 41 | 51 |
c | 61 | 71 | 81 |
d | 91 | 2 | 12 |
А применить туже функцию, но к серии так:
df['Z'].map(inverse)
a 21 b 51 c 81 d 12 Name: Z, dtype: int64
Метод map()
может быть использован для связывания или (сопоставления) значений одной серии с значениями из другой:
s_1 = pd.Series(['cat', 'cat', 'dog', 'cat', 'dog'],
index=list('abcde'))
s_1
a cat b cat c dog d cat e dog dtype: object
s_2 = pd.Series([100, 500], index=['cat', 'dog'])
s_2
cat 100 dog 500 dtype: int64
s_1.map(s_2)
a 100 b 100 c 500 d 100 e 500 dtype: int64