6. 函数应用
要将您自己定义的函数或其他第三方库的函数应用于 pandas
对象上,主要包含下面的方法。
具体使用哪种方法需要根据需要,是在整个 DataFrame
还是 Series
上、行或列上,或者是元素上进行操作
- 表应用函数:
pipe()
- 行列应用函数:
apply()
- 聚合函数:
agg()
与transform()
- 元素级函数:
applymap()
6.1 表应用函数
虽然可以将 DataFrame
和 Series
传递到函数中,但是如果需要链式调用函数,可以考虑使用管道函数 pipe()
我们先进行一些设置
In [142]: def extract_city_name(df):
.....: """
.....: Chicago, IL -> Chicago for city_name column
.....: """
.....: df["city_name"] = df["city_and_code"].str.split(",").str.get(0)
.....: return df
.....:
In [143]: def add_country_name(df, country_name=None):
.....: """
.....: Chicago -> Chicago-US for city_name column
.....: """
.....: col = "city_name"
.....: df["city_and_country"] = df[col] + country_name
.....: return df
.....:
In [144]: df_p = pd.DataFrame({"city_and_code": ["Chicago, IL"]})
extract_city_name
和 add_country_name
函数传入和返回的都是 DataFrame
现在进行下面的比较
In [145]: add_country_name(extract_city_name(df_p), country_name="US")
Out[145]:
city_and_code city_name city_and_country
0 Chicago, IL Chicago ChicagoUS
相当于
In [146]: df_p.pipe(extract_city_name).pipe(add_country_name, country_name="US")
Out[146]:
city_and_code city_name city_and_country
0 Chicago, IL Chicago ChicagoUS
pandas
鼓励使用第二种方式,即链式函数调用。pipe
使您可以轻松地在函数链中使用您自己定义的或另一个库的函数,以及 pandas
中的函数。
在上面的示例中,函数 extract_city_name
和 add_country_name
的第一个参数都是一个 DataFrame
,如果想要把 DataFrame
作为第二个或其他位置的参数怎么办呢?
我们可以为 .pipe
函数传递 (callable, data_keyword)
元组,data_keyword
是字符串,其值为 callable
函数的某一参数名,pipe
会把 DataFrame
作为 data_keyword
指定参数的值传递到函数中
例如
>>> df = pd.DataFrame(np.random.rand(6, 4) * np.random.randint(1, 10), columns=list('ABCD'))
>>> df
A B C D
0 2.895628 1.021764 3.549697 3.946251
1 3.032729 5.527509 4.111962 4.246071
2 0.587101 4.009382 3.330098 0.671954
3 5.891730 2.829773 3.349024 5.687257
4 2.103148 2.658920 4.398308 2.653573
5 3.576252 2.512895 4.871405 1.283442
>>> def func(a, data, b, c):
...: return data[a] + b - c
>>> df.query('-2 < B < 3').pipe((func, 'data'), a='A', b=1, c=1)
0 2.895628
3 5.891730
4 2.103148
5 3.576252
Name: A, dtype: float64
我们将 DataFrame
传递给函数 func
的第二个参数 data
,其他参数以命名参数的方式指定。
我们可以结合这一方式,使用 statmodels
来拟合回归。它们的 API
的第一个参数是计算公式,第二个参数是 data
,接受一个 DataFrame
对象。
In [147]: import statsmodels.formula.api as sm
In [148]: bb = pd.read_csv("data/baseball.csv", index_col="id")
In [149]: (
.....: bb.query("h > 0")
.....: .assign(ln_h=lambda df: np.log(df.h))
.....: .pipe((sm.ols, "data"), "hr ~ ln_h + year + g + C(lg)")
.....: .fit()
.....: .summary()
.....: )
.....:
Out[149]:
"""
OLS Regression Results
==============================================================================
Dep. Variable: hr R-squared: 0.685
Model: OLS Adj. R-squared: 0.665
Method: Least Squares F-statistic: 34.28
Date: Wed, 20 Jan 2021 Prob (F-statistic): 3.48e-15
Time: 11:49:07 Log-Likelihood: -205.92
No. Observations: 68 AIC: 421.8
Df Residuals: 63 BIC: 432.9
Df Model: 4
Covariance Type: nonrobust
===============================================================================
coef std err t P>|t| [0.025 0.975]
-------------------------------------------------------------------------------
Intercept -8484.7720 4664.146 -1.819 0.074 -1.78e+04 835.780
C(lg)[T.NL] -2.2736 1.325 -1.716 0.091 -4.922 0.375
ln_h -1.3542 0.875 -1.547 0.127 -3.103 0.395
year 4.2277 2.324 1.819 0.074 -0.417 8.872
g 0.1841 0.029 6.258 0.000 0.125 0.243
==============================================================================
Omnibus: 10.875 Durbin-Watson: 1.999
Prob(Omnibus): 0.004 Jarque-Bera (JB): 17.298
Skew: 0.537 Prob(JB): 0.000175
Kurtosis: 5.225 Cond. No. 1.49e+07
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.49e+07. This might indicate that there are
strong multicollinearity or other numerical problems.
"""
注意:当只有两个参数时,可以省略元组后面的参数名称,直接传入变量,如果是超过两个参数,需要通过指定参数名赋值方式。
这个 pipe()
函数是受 Unix
管道操作以及后面的 dplyr
和 magrittr
的启发。
上面的代码看起来是不是和我们之前介绍的 dplyr
操作很像,哈哈,可以好好比较一下。
6.2 行列行数应用
可以使用 apply()
函数沿 DataFrame
对应的轴应用任意函数(默认应用在每列上),该方法与统计描述方法一样,使用可选的 axis
参数指定应用的轴
In [150]: df.apply(np.mean)
Out[150]:
one 0.811094
two 1.360588
three 0.187958
dtype: float64
In [151]: df.apply(np.mean, axis=1)
Out[151]:
a 1.583749
b 0.734929
c 1.133683
d -0.166914
dtype: float64
In [152]: df.apply(lambda x: x.max() - x.min())
Out[152]:
one 1.051928
two 1.632779
three 1.840607
dtype: float64
In [153]: df.apply(np.cumsum)
Out[153]:
one two three
a 1.394981 1.772517 NaN
b 1.738035 3.684640 -0.050390
c 2.433281 5.163008 1.177045
d NaN 5.442353 0.563873
In [154]: df.apply(np.exp)
Out[154]:
one two three
a 4.034899 5.885648 NaN
b 1.409244 6.767440 0.950858
c 2.004201 4.385785 3.412466
d NaN 1.322262 0.541630
apply()
函数也可以指派一个字符串方法名
In [155]: df.apply("mean")
Out[155]:
one 0.811094
two 1.360588
three 0.187958
dtype: float64
In [156]: df.apply("mean", axis=1)
Out[156]:
a 1.583749
b 0.734929
c 1.133683
d -0.166914
dtype: float64
默认情况下,apply()
里面调用的函数的返回类型会影响其输出结果的类型。
如果调用的函数返回的是
Series
,输出结果的类型是DataFrame
。而且输出的列的索引与函数返回的Series
索引相匹配。如果函数返回的是其它任意类型,输出的结果将是 Series。
result_type
参数可以覆盖默认行为(只有当 axis=1
,即应用在列上才能发挥作用),该参数有三个可选的值:
-
expand
: 类似列表的结果将被转换成列 -
reduce
: 如果可能,尽可能返回一个Series
而不是扩展成列表状的结果。这与expand
相反 -
broadcast
: 结果将广播到与原来DataFrame
相同的形状,原始的索引和列名将会保留
这些值决定了返回值是否扩展为 DataFrame
。
熟悉 apply()
的使用技巧,可以轻易的获取数据中的信息,例如,假设我们要提取每列中最大值对应的日期:
In [157]: tsdf = pd.DataFrame(
.....: np.random.randn(1000, 3),
.....: columns=["A", "B", "C"],
.....: index=pd.date_range("1/1/2000", periods=1000),
.....: )
.....:
In [158]: tsdf.apply(lambda x: x.idxmax())
Out[158]:
A 2000-08-06
B 2001-01-18
C 2001-07-18
dtype: datetime64[ns]
您还可以将其他参数和关键字参数传递给 apply()
方法,例如我们定义如下函数
def subtract_and_divide(x, sub, divide=1):
return (x - sub) / divide
你可以使用像这样使用
df.apply(subtract_and_divide, args=(5,), divide=3)
还可以对每行或每列应用 Series
函数
In [159]: tsdf
Out[159]:
A B C
2000-01-01 -0.158131 -0.232466 0.321604
2000-01-02 -1.810340 -3.105758 0.433834
2000-01-03 -1.209847 -1.156793 -0.136794
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 -0.653602 0.178875 1.008298
2000-01-09 1.007996 0.462824 0.254472
2000-01-10 0.307473 0.600337 1.643950
In [160]: tsdf.apply(pd.Series.interpolate)
Out[160]:
A B C
2000-01-01 -0.158131 -0.232466 0.321604
2000-01-02 -1.810340 -3.105758 0.433834
2000-01-03 -1.209847 -1.156793 -0.136794
2000-01-04 -1.098598 -0.889659 0.092225
2000-01-05 -0.987349 -0.622526 0.321243
2000-01-06 -0.876100 -0.355392 0.550262
2000-01-07 -0.764851 -0.088259 0.779280
2000-01-08 -0.653602 0.178875 1.008298
2000-01-09 1.007996 0.462824 0.254472
2000-01-10 0.307473 0.600337 1.643950
apply()
函数有一个 raw
参数,其默认值为 False
,即在应用函数前,会自动将每行或每列转换为 Series
。
如果设置为 True
,会将数据转换为 ndarray
对象,在不需要使用索引的情况下,能显著提高性能。
6.3 聚合函数 API
聚合 API
能够以一种简洁的方式来表达多个可能的聚合操作,使用 DataFrame.aggregate()
或者别名DataFrame.agg()
来进行操作
我们使用与上面类似的示例数据
In [161]: tsdf = pd.DataFrame(
.....: np.random.randn(10, 3),
.....: columns=["A", "B", "C"],
.....: index=pd.date_range("1/1/2000", periods=10),
.....: )
.....:
In [162]: tsdf.iloc[3:7] = np.nan
In [163]: tsdf
Out[163]:
A B C
2000-01-01 1.257606 1.004194 0.167574
2000-01-02 -0.749892 0.288112 -0.757304
2000-01-03 -0.207550 -0.298599 0.116018
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 0.814347 -0.257623 0.869226
2000-01-09 -0.250663 -1.206601 0.896839
2000-01-10 2.169758 -1.333363 0.283157
如果只使用单个函数,其效果等同于 apply()
。您也可以传递字符串变量名,将会返回一个 Series
输出结果
In [164]: tsdf.agg(np.sum)
Out[164]:
A 3.033606
B -1.803879
C 1.575510
dtype: float64
In [165]: tsdf.agg("sum")
Out[165]:
A 3.033606
B -1.803879
C 1.575510
dtype: float64
In [166]: tsdf.sum()
Out[166]:
A 3.033606
B -1.803879
C 1.575510
Series
上的单个聚合将返回标量值
In [167]: tsdf["A"].agg("sum")
Out[167]: 3.033606102414146
6.3.1 多函数聚合
还可以将多个聚合函数以列表形式传入,每个函数的计算结果是 DataFrame
的一行,对应的索引是对应函数的函数名。
In [168]: tsdf.agg(["sum"])
Out[168]:
A B C
sum 3.033606 -1.803879 1.57551
多个函数产生多个行
In [169]: tsdf.agg(["sum", "mean"])
Out[169]:
A B C
sum 3.033606 -1.803879 1.575510
mean 0.505601 -0.300647 0.262585
在一个 Series
上,多个函数返回一个 Series
,并以函数名作为索引。
In [170]: tsdf["A"].agg(["sum", "mean"])
Out[170]:
sum 3.033606
mean 0.505601
Name: A, dtype: float64
传递 lambda
函数将产生一个名为
的行
In [171]: tsdf["A"].agg(["sum", lambda x: x.mean()])
Out[171]:
sum 3.033606
0.505601
Name: A, dtype: float64
传递一个命名的函数将以相应的函数名称作为索引
In [172]: def mymean(x):
.....: return x.mean()
.....:
In [173]: tsdf["A"].agg(["sum", mymean])
Out[173]:
sum 3.033606
mymean 0.505601
Name: A, dtype: float64
6.3.2 传入字典聚合
可以用传入字典的方式来指定每列应用的函数
In [174]: tsdf.agg({"A": "mean", "B": "sum"})
Out[174]:
A 0.505601
B -1.803879
dtype: float64
注意:输出结果的顺序不是固定的,因为字典是无序的。如果想要让输出顺序与输入顺序一致,可以使用 OrderedDict
from collections import OrderedDict
如果输入的字典中包含列表时,会返回 DataFrame
形式的输出结果。输出结果的索引是唯一的函数名,未应用该函数的列对应的值会被赋值为 NaN
In [175]: tsdf.agg({"A": ["mean", "min"], "B": "sum"})
Out[175]:
A B
mean 0.505601 NaN
min -0.749892 NaN
sum NaN -1.803879
6.3.3 混合类型
当 DataFrame
中存在无法聚合的混合类型时,.agg()
只会对能够聚合的列进行的聚合。
In [176]: mdf = pd.DataFrame(
.....: {
.....: "A": [1, 2, 3],
.....: "B": [1.0, 2.0, 3.0],
.....: "C": ["foo", "bar", "baz"],
.....: "D": pd.date_range("20130101", periods=3),
.....: }
.....: )
.....:
In [177]: mdf.dtypes
Out[177]:
A int64
B float64
C object
D datetime64[ns]
dtype: object
In [178]: mdf.agg(["min", "sum"])
Out[178]:
A B C D
min 1 1.0 bar 2013-01-01
sum 6 6.0 foobarbaz NaT
你可能会对 NaT
有疑问,难道是写错了?不是的,它类似于 NaN
,表示:Not a Time
。也是一种缺失值
6.3.4 自定义 describe
使用 .agg()
我们可以扩展类似于内建函数 describe
的功能
In [179]: from functools import partial
In [180]: q_25 = partial(pd.Series.quantile, q=0.25)
In [181]: q_25.__name__ = "25%"
In [182]: q_75 = partial(pd.Series.quantile, q=0.75)
In [183]: q_75.__name__ = "75%"
In [184]: tsdf.agg(["count", "mean", "std", "min", q_25, "median", q_75, "max"])
Out[184]:
A B C
count 6.000000 6.000000 6.000000
mean 0.505601 -0.300647 0.262585
std 1.103362 0.887508 0.606860
min -0.749892 -1.333363 -0.757304
25% -0.239885 -0.979600 0.128907
median 0.303398 -0.278111 0.225365
75% 1.146791 0.151678 0.722709
max 2.169758 1.004194 0.896839
6.4 变换 API
transform()
方法返回一个与原始对象索引相同(大小相同)的对象。
这个 API
允许你同时提供多个操作,而不是一个一个的操作。它的 API
与 .agg API
十分相似
例如,对于下面的数据
In [185]: tsdf = pd.DataFrame(
.....: np.random.randn(10, 3),
.....: columns=["A", "B", "C"],
.....: index=pd.date_range("1/1/2000", periods=10),
.....: )
.....:
In [186]: tsdf.iloc[3:7] = np.nan
In [187]: tsdf
Out[187]:
A B C
2000-01-01 -0.428759 -0.864890 -0.675341
2000-01-02 -0.168731 1.338144 -1.279321
2000-01-03 -1.621034 0.438107 0.903794
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 0.254374 -1.240447 -0.201052
2000-01-09 -0.157795 0.791197 -1.144209
.transform()
可以传入 NumPy
函数、字符串函数名以及自定义函数实现对数据的变换
In [188]: tsdf.transform(np.abs)
Out[188]:
A B C
2000-01-01 0.428759 0.864890 0.675341
2000-01-02 0.168731 1.338144 1.279321
2000-01-03 1.621034 0.438107 0.903794
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 0.254374 1.240447 0.201052
2000-01-09 0.157795 0.791197 1.144209
2000-01-10 0.030876 0.371900 0.061932
In [189]: tsdf.transform("abs")
Out[189]:
A B C
2000-01-01 0.428759 0.864890 0.675341
2000-01-02 0.168731 1.338144 1.279321
2000-01-03 1.621034 0.438107 0.903794
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 0.254374 1.240447 0.201052
2000-01-09 0.157795 0.791197 1.144209
2000-01-10 0.030876 0.371900 0.061932
In [190]: tsdf.transform(lambda x: x.abs())
Out[190]:
A B C
2000-01-01 0.428759 0.864890 0.675341
2000-01-02 0.168731 1.338144 1.279321
2000-01-03 1.621034 0.438107 0.903794
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 0.254374 1.240447 0.201052
2000-01-09 0.157795 0.791197 1.144209
2000-01-10 0.030876 0.371900 0.061932
在这里 transform()
只接收一个函数,类似于
In [191]: np.abs(tsdf)
Out[191]:
A B C
2000-01-01 0.428759 0.864890 0.675341
2000-01-02 0.168731 1.338144 1.279321
2000-01-03 1.621034 0.438107 0.903794
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 0.254374 1.240447 0.201052
2000-01-09 0.157795 0.791197 1.144209
2000-01-10 0.030876 0.371900 0.061932
如果 .transform()
应用对象是 Series
,返回的也是一个 Series
In [192]: tsdf["A"].transform(np.abs)
Out[192]:
2000-01-01 0.428759
2000-01-02 0.168731
2000-01-03 1.621034
2000-01-04 NaN
2000-01-05 NaN
2000-01-06 NaN
2000-01-07 NaN
2000-01-08 0.254374
2000-01-09 0.157795
2000-01-10 0.030876
Freq: D, Name: A, dtype: float64
6.4.1 多函数变换
传递多个函数将产生一个包含多级列名的 DataFrame
。第一级列名是原始列名,第二级是转换函数的名称。
In [193]: tsdf.transform([np.abs, lambda x: x + 1])
Out[193]:
A B C
absolute absolute absolute
2000-01-01 0.428759 0.571241 0.864890 0.135110 0.675341 0.324659
2000-01-02 0.168731 0.831269 1.338144 2.338144 1.279321 -0.279321
2000-01-03 1.621034 -0.621034 0.438107 1.438107 0.903794 1.903794
2000-01-04 NaN NaN NaN NaN NaN NaN
2000-01-05 NaN NaN NaN NaN NaN NaN
2000-01-06 NaN NaN NaN NaN NaN NaN
2000-01-07 NaN NaN NaN NaN NaN NaN
2000-01-08 0.254374 1.254374 1.240447 -0.240447 0.201052 0.798948
2000-01-09 0.157795 0.842205 0.791197 1.791197 1.144209 -0.144209
2000-01-10 0.030876 0.969124 0.371900 1.371900 0.061932 1.061932
将多个函数传递给 Series
将产生一个 DataFrame
,列名为转换函数的名称
In [194]: tsdf["A"].transform([np.abs, lambda x: x + 1])
Out[194]:
absolute
2000-01-01 0.428759 0.571241
2000-01-02 0.168731 0.831269
2000-01-03 1.621034 -0.621034
2000-01-04 NaN NaN
2000-01-05 NaN NaN
2000-01-06 NaN NaN
2000-01-07 NaN NaN
2000-01-08 0.254374 1.254374
2000-01-09 0.157795 0.842205
2000-01-10 0.030876 0.969124
6.4.2 使用字典变换
传递函数字典将允许对每个列进行选择性转换
In [195]: tsdf.transform({"A": np.abs, "B": lambda x: x + 1})
Out[195]:
A B
2000-01-01 0.428759 0.135110
2000-01-02 0.168731 2.338144
2000-01-03 1.621034 1.438107
2000-01-04 NaN NaN
2000-01-05 NaN NaN
2000-01-06 NaN NaN
2000-01-07 NaN NaN
2000-01-08 0.254374 -0.240447
2000-01-09 0.157795 1.791197
2000-01-10 0.030876 1.371900
传递一个包含列表的字典将生成一个具有这些选择性转换的多级列名 DataFrame
。
In [196]: tsdf.transform({"A": np.abs, "B": [lambda x: x + 1, "sqrt"]})
Out[196]:
A B
A sqrt
2000-01-01 0.428759 0.135110 NaN
2000-01-02 0.168731 2.338144 1.156782
2000-01-03 1.621034 1.438107 0.661897
2000-01-04 NaN NaN NaN
2000-01-05 NaN NaN NaN
2000-01-06 NaN NaN NaN
2000-01-07 NaN NaN NaN
2000-01-08 0.254374 -0.240447 NaN
2000-01-09 0.157795 1.791197 0.889493
2000-01-10 0.030876 1.371900 0.609836
6.5 元素级函数应用
由于并不是所有的函数都可以向量化,即能够传入 NumPy
数组并返回另一个数组或值。
因此 DataFrame
上的 applymap()
方法和 Series
上的 map()
方法可以接受任何能够传入单个值并返回单个值的 Python
函数。例如
In [197]: df4
Out[197]:
one two three
a 1.394981 1.772517 NaN
b 0.343054 1.912123 -0.050390
c 0.695246 1.478369 1.227435
d NaN 0.279344 -0.613172
In [198]: def f(x):
.....: return len(str(x))
.....:
In [199]: df4["one"].map(f)
Out[199]:
a 18
b 19
c 18
d 3
Name: one, dtype: int64
In [200]: df4.applymap(f)
Out[200]:
one two three
a 18 17 3
b 19 18 20
c 18 18 16
d 3 19 19
Series.map()
还有另一个功能,能够连接或映射到另一个 Series
,这与我们后面要讲的连接操作关系密切
In [201]: s = pd.Series(
.....: ["six", "seven", "six", "seven", "six"], index=["a", "b", "c", "d", "e"]
.....: )
.....:
In [202]: t = pd.Series({"six": 6.0, "seven": 7.0})
In [203]: s
Out[203]:
a six
b seven
c six
d seven
e six
dtype: object
In [204]: s.map(t)
Out[204]:
a 6.0
b 7.0
c 6.0
d 7.0
e 6.0
dtype: float64
可以看到 s
中对应位置的值会替换为 t
中定义的映射关系对应的值。