Python 数据处理(八)—— 应用函数

6. 函数应用

要将您自己定义的函数或其他第三方库的函数应用于 pandas 对象上,主要包含下面的方法。

具体使用哪种方法需要根据需要,是在整个 DataFrame 还是 Series 上、行或列上,或者是元素上进行操作

  1. 表应用函数:pipe()
  2. 行列应用函数:apply()
  3. 聚合函数:agg()transform()
  4. 元素级函数:applymap()
6.1 表应用函数

虽然可以将 DataFrameSeries 传递到函数中,但是如果需要链式调用函数,可以考虑使用管道函数 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_nameadd_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_nameadd_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 管道操作以及后面的 dplyrmagrittr 的启发。

上面的代码看起来是不是和我们之前介绍的 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 中定义的映射关系对应的值。

你可能感兴趣的:(Python 数据处理(八)—— 应用函数)