• pandas教程:Apply:General split-apply-combine 通常的分割-应用-合并


    10.3 Apply:General split-apply-combine(应用:通用的分割-应用-合并)

    general-purpose: 可以理解为通用,泛用。

    例子:在计算机软件中,通用编程语言(General-purpose programming language )指被设计为各种应用领域服务的编程语言。通常通用编程语言不含有为特定应用领域设计的结构。

    相对而言,特定域编程语言就是为某一个特定的领域或应用软件设计的编程语言。比如说,LaTeX就是专门为排版文献而设计的语言。

    最通用的GroupBy(分组)方法是apply,这也是本节的主题。如下图所示,apply会把对象分为多个部分,然后将函数应用到每一个部分上,然后把所有的部分都合并起来:

    返回之前提到的tipping数据集,假设我们想要根据不同组(group),选择前5个tip_pct值最大的。首先,写一个函数,函数的功能为在特定的列,选出有最大值的行:

    import numpy as np
    import pandas as pd
    
    • 1
    • 2
    tips = pd.read_csv('../examples/tips.csv')
    
    • 1
    # Add tip percentage of total bill
    tips['tip_pct'] = tips['tip'] / tips['total_bill']
    
    • 1
    • 2
    tips.head()
    
    • 1
    total_billtipsmokerdaytimesizetip_pct
    016.991.01NoSunDinner20.059447
    110.341.66NoSunDinner30.160542
    221.013.50NoSunDinner30.166587
    323.683.31NoSunDinner20.139780
    424.593.61NoSunDinner40.146808
    def top(df, n=5, column='tip_pct'):
        return df.sort_values(by=column)[-n:]
    
    • 1
    • 2
    top(tips, n=6)
    
    • 1
    total_billtipsmokerdaytimesizetip_pct
    10914.314.00YesSatDinner20.279525
    18323.176.50YesSunDinner40.280535
    23211.613.39NoSatDinner20.291990
    673.071.00YesSatDinner10.325733
    1789.604.00YesSunDinner20.416667
    1727.255.15YesSunDinner20.710345

    现在,如果我们按smoker分组,然后用apply来使用这个函数,我们能得到下面的结果:

    tips.groupby('smoker').apply(top)
    
    • 1
    total_billtipsmokerdaytimesizetip_pct
    smoker
    No8824.715.85NoThurLunch20.236746
    18520.695.00NoSunDinner50.241663
    5110.292.60NoSunDinner20.252672
    1497.512.00NoThurLunch20.266312
    23211.613.39NoSatDinner20.291990
    Yes10914.314.00YesSatDinner20.279525
    18323.176.50YesSunDinner40.280535
    673.071.00YesSatDinner10.325733
    1789.604.00YesSunDinner20.416667
    1727.255.15YesSunDinner20.710345

    我们来解释下上面这一行代码发生了什么。这里的top函数,在每一个DataFrame中的行组(row group)都被调用了一次,然后各自的结果通过pandas.concat合并了,最后用组名(group names)来标记每一部分。(译者:可以理解为,我们先按smoker这一列对整个DataFrame进行了分组,一共有NoYes两组,然后对每一组上调用了top函数,所以每一组会返还5行作为结果,最后把两组的结果整合起来,一共是10行)。

    最后的结果是有多层级索引(hierarchical index)的,而且这个多层级索引的内部层级(inner level)含有来自于原来DataFrame中的索引值(index values

    如果传递一个函数给apply,可以在函数之后,设定其他一些参数:

    tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')
    
    • 1
    total_billtipsmokerdaytimesizetip_pct
    smokerday
    NoFri9422.753.25NoFriDinner20.142857
    Sat21248.339.00NoSatDinner40.186220
    Sun15648.175.00NoSunDinner60.103799
    Thur14241.195.00NoThurLunch50.121389
    YesFri9540.174.73YesFriDinner40.117750
    Sat17050.8110.00YesSatDinner30.196812
    Sun18245.353.50YesSunDinner30.077178
    Thur19743.115.00YesThurLunch40.115982

    除了上面这些基本用法,要想用好apply可能需要一点创新能力。毕竟传给这个函数的内容取决于我们自己,而最终的结果只需要返回一个pandas对象或一个标量。这一章的剩余部分主要介绍如何解决在使用groupby时遇到的一些问题。

    可以试一试在GroupBy对象上调用describe

    result = tips.groupby('smoker')['tip_pct'].describe()
    
    • 1
    result
    
    • 1
    smoker       
    No      count    151.000000
            mean       0.159328
            std        0.039910
            min        0.056797
            25%        0.136906
            50%        0.155625
            75%        0.185014
            max        0.291990
    Yes     count     93.000000
            mean       0.163196
            std        0.085119
            min        0.035638
            25%        0.106771
            50%        0.153846
            75%        0.195059
            max        0.710345
    Name: tip_pct, dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    result.unstack('smoker')
    
    • 1
    smokerNoYes
    count151.00000093.000000
    mean0.1593280.163196
    std0.0399100.085119
    min0.0567970.035638
    25%0.1369060.106771
    50%0.1556250.153846
    75%0.1850140.195059
    max0.2919900.710345

    GroupBy内部,当我们想要调用一个像describe这样的函数的时候,其实相当于下面的写法:

    f = lambda x: x.describe()
    grouped.apply(f)
    
    • 1
    • 2

    1 Suppressing the Group Keys(抑制组键)

    在接下来的例子,我们会看到作为结果的对象有一个多层级索引(hierarchical index),这个多层级索引是由原来的对象中,组键(group key)在每一部分的索引上得到的。我们可以在groupby函数中设置group_keys=False来关闭这个功能:

    tips.groupby('smoker', group_keys=False).apply(top)
    
    • 1
    total_billtipsmokerdaytimesizetip_pct
    8824.715.85NoThurLunch20.236746
    18520.695.00NoSunDinner50.241663
    5110.292.60NoSunDinner20.252672
    1497.512.00NoThurLunch20.266312
    23211.613.39NoSatDinner20.291990
    10914.314.00YesSatDinner20.279525
    18323.176.50YesSunDinner40.280535
    673.071.00YesSatDinner10.325733
    1789.604.00YesSunDinner20.416667
    1727.255.15YesSunDinner20.710345

    2 Quantile and Bucket Analysis(分位数与桶分析)

    在第八章中,我们介绍了pandas的一些工具,比如cutqcut,通过设置中位数,切割数据为buckets with bins(有很多箱子的桶)。

    把函数通过groupby整合起来,可以在做桶分析或分位数分析的时候更方便。假设一个简单的随机数据集和一个等长的桶类型(bucket categorization),使用cut

    frame = pd.DataFrame({'data1': np.random.randn(1000),
                           'data2': np.random.randn(1000)})
    frame.head()
    
    • 1
    • 2
    • 3
    data1data2
    00.7239730.120216
    12.0536170.468000
    2-0.543073-1.874073
    3-0.9151360.159179
    40.7759650.105447
    quartiles = pd.cut(frame.data1, 4)
    quartiles[:10]
    
    • 1
    • 2
    0     (0.194, 1.795]
    1     (1.795, 3.395]
    2    (-1.407, 0.194]
    3    (-1.407, 0.194]
    4     (0.194, 1.795]
    5     (0.194, 1.795]
    6     (0.194, 1.795]
    7    (-1.407, 0.194]
    8    (-1.407, 0.194]
    9    (-1.407, 0.194]
    Name: data1, dtype: category
    Categories (4, object): [(-3.0139, -1.407] < (-1.407, 0.194] < (0.194, 1.795] < (1.795, 3.395]]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    cut返回的Categorical object(类别对象)能直接传入groupby。所以我们可以在data2列上计算很多统计值:

    def get_stats(group):
        return {'min': group.min(), 'max': group.max(),
                'count': group.count(), 'mean': group.mean()}
    
    • 1
    • 2
    • 3
    grouped = frame.data2.groupby(quartiles)
    
    • 1
    grouped.apply(get_stats).unstack()
    
    • 1
    countmaxmeanmin
    data1
    (-3.0139, -1.407]70.02.0351660.113238-2.363707
    (-1.407, 0.194]481.03.284688-0.044535-2.647341
    (0.194, 1.795]407.02.402272-0.043887-2.898145
    (1.795, 3.395]42.02.0518430.095178-2.234979

    也有相同长度的桶(equal-length buckets);想要按照样本的分位数得到相同长度的桶,用qcut。这里设定labels=False来得到分位数的数量:

    # Return quantile numbers
    grouping = pd.qcut(frame.data1, 10, labels=False)
    
    • 1
    • 2

    译者:上面的代码是把framedata1列分为10个bin,每个bin都有相同的数量。因为一共有1000个样本,所以每个bin里有100个样本。grouping保存的是每个样本的index以及其对应的bin的编号。

    grouped = frame.data2.groupby(grouping)
    
    • 1
    grouped.apply(get_stats).unstack()
    
    • 1
    countmaxmeanmin
    data1
    0100.02.1786530.078390-2.363707
    1100.03.284688-0.018699-2.647341
    2100.02.214011-0.066341-2.262063
    3100.02.880188-0.014041-2.475753
    4100.02.741344-0.007952-2.576095
    5100.02.346857-0.109602-2.898145
    6100.02.4022720.004522-1.911955
    7100.02.351513-0.161472-2.640625
    8100.02.135995-0.016079-1.986676
    9100.02.0518430.037685-2.513164

    对于pandasCategorical类型,会在第十二章做详细介绍。

    3 Example: Filling Missing Values with Group-Specific Values(例子:用组特异性值来填充缺失值)

    在处理缺失值的时候,一些情况下我们会直接用dropna来把缺失值删除,但另一些情况下,我们希望用一些固定的值来代替缺失值,而fillna就是用来做这个的,例如,这里我们用平均值mean来代替缺失值NA

    s = pd.Series(np.random.randn(6))
    
    • 1
    s[::2] = np.nan
    
    • 1
    s
    
    • 1
    0         NaN
    1    0.878562
    2         NaN
    3   -0.264051
    4         NaN
    5    0.760488
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    s.fillna(s.mean())
    
    • 1
    0    0.458333
    1    0.878562
    2    0.458333
    3   -0.264051
    4    0.458333
    5    0.760488
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    假设我们想要给每一组填充不同的值。一个方法就是对数据分组后,用apply来调用fillna,在每一个组上执行一次。这里有一些样本是把美国各州分为西部和东部:

    states = ['Ohio', 'New York', 'Vermont', 'Florida',
              'Oregon', 'Nevada', 'California', 'Idaho']
    
    • 1
    • 2
    group_key = ['East'] * 4 + ['West'] * 4
    group_key
    
    • 1
    • 2
    ['East', 'East', 'East', 'East', 'West', 'West', 'West', 'West']
    
    • 1
    data = pd.Series(np.random.randn(8), index=states)
    data
    
    • 1
    • 2
    Ohio          0.683283
    New York     -1.059896
    Vermont       0.105837
    Florida      -0.328586
    Oregon        1.973413
    Nevada        0.656673
    California    0.001700
    Idaho        -0.713295
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们令data中某些值为缺失值:

    data[['Vermont', 'Nevada', 'Idaho']] = np.nan
    data
    
    • 1
    • 2
    Ohio          0.683283
    New York     -1.059896
    Vermont            NaN
    Florida      -0.328586
    Oregon        1.973413
    Nevada             NaN
    California    0.001700
    Idaho              NaN
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    data.groupby(group_key).mean()
    
    • 1
    East   -0.235066
    West    0.987556
    dtype: float64
    
    • 1
    • 2
    • 3

    然后我们可以用每个组的平均值来填充NA

    fill_mean = lambda g: g.fillna(g.mean())
    
    • 1
    data.groupby(group_key).apply(fill_mean)
    
    • 1
    Ohio          0.683283
    New York     -1.059896
    Vermont      -0.235066
    Florida      -0.328586
    Oregon        1.973413
    Nevada        0.987556
    California    0.001700
    Idaho         0.987556
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在另外一些情况下,我们可能希望提前设定好用于不同组的填充值。因为group有一个name属性,我们可以利用这个:

    fill_values = {'East': 0.5, 'West': -1}
    
    • 1
    fill_func = lambda g: g.fillna(fill_values[g.name])
    
    • 1
    data.groupby(group_key).apply(fill_func)
    
    • 1
    Ohio          0.683283
    New York     -1.059896
    Vermont       0.500000
    Florida      -0.328586
    Oregon        1.973413
    Nevada       -1.000000
    California    0.001700
    Idaho        -1.000000
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4 Example: Random Sampling and Permutation(例子:随机抽样和排列)

    假设我们想要从一个很大的数据集里随机抽出一些样本,这里我们可以在Series上用sample方法。为了演示,这里县创建一副模拟的扑克牌:

    # Hearts红桃,Spades黑桃,Clubs梅花,Diamonds方片
    suits = ['H', 'S', 'C', 'D']
    card_val = (list(range(1, 11)) + [10] * 3) * 4
    base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q']
    cards = []
    for suit in ['H', 'S', 'C', 'D']:
        cards.extend(str(num) + suit for num in base_names)
    
    deck = pd.Series(card_val, index=cards)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这样我们就得到了一个长度为52的Series,索引(index)部分是牌的名字,对应的值为牌的点数,这里的点数是按Blackjack(二十一点)的游戏规则来设定的。

    Blackjack(二十一点): 2点至10点的牌以牌面的点数计算,J、Q、K 每张为10点,A可记为1点或为11点。这里为了方便,我们只把A记为1点。

    deck[:13]
    
    • 1
    AH      1
    2H      2
    3H      3
    4H      4
    5H      5
    6H      6
    7H      7
    8H      8
    9H      9
    10H    10
    JH     10
    KH     10
    QH     10
    dtype: int64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    现在,就像我们上面说的,随机从牌组中抽出5张牌:

    def draw(deck, n=5):
        return deck.sample(n)
    
    • 1
    • 2
    draw(deck)
    
    • 1
    7H     7
    6D     6
    AC     1
    JH    10
    JS    10
    dtype: int64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    假设我们想要从每副花色中随机抽取两张,花色是每张牌名字的最后一个字符(即H, S, C, D),我们可以根据花色分组,然后使用apply

    get_suit = lambda card: card[-1] # last letter is suit
    
    • 1
    deck.groupby(get_suit).apply(draw, n=2)
    
    • 1
    C  QC    10
       9C     9
    D  3D     3
       JD    10
    H  KH    10
       6H     6
    S  3S     3
       7S     7
    dtype: int64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    另外一种写法:

    deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
    
    • 1
    7C     7
    KC    10
    AD     1
    4D     4
    AH     1
    8H     8
    7S     7
    9S     9
    dtype: int64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    5 Example: Group Weighted Average and Correlation(例子:组加权平均和相关性)

    groupbysplit-apply-combine机制下,DataFrame的两列或两个Series,计算组加权平均(Group Weighted Average)是可能的。这里举个例子,下面的数据集包含组键,值,以及权重:

    df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
                                    'b', 'b', 'b', 'b'],
                       'data': np.random.randn(8),
                       'weights': np.random.rand(8)})
    df
    
    • 1
    • 2
    • 3
    • 4
    • 5
    categorydataweights
    0a0.0980200.008455
    1a1.3894960.826219
    2a0.2028690.258955
    3a-0.2424030.470473
    4b-0.8205070.628758
    5b0.8663260.653632
    6b-1.2973750.639703
    7b0.5250190.012664

    category分组来计算组加权平均:

    grouped = df.groupby('category')
    
    • 1
    get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
    
    • 1
    grouped.apply(get_wavg)
    
    • 1
    category
    a    0.695189
    b   -0.399497
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4

    另一个例子,考虑一个从Yahoo!财经上得到的经济数据集,包含一些股票交易日结束时的股价,以及S&P 500指数(即SPX符号):

    标准普尔500指数英文简写为S&P 500 Index,是记录美国500家上市公司的一个股票指数。这个股票指数由标准普尔公司创建并维护。

    标准普尔500指数覆盖的所有公司,都是在美国主要交易所,如纽约证券交易所、Nasdaq交易的上市公司。与道琼斯指数相比,标准普尔500指数包含的公司更多,因此风险更为分散,能够反映更广泛的市场变化。

    close_px = pd.read_csv('../examples/stock_px_2.csv', parse_dates=True,
                           index_col=0)
    
    • 1
    • 2
    close_px.info()
    
    • 1
    
    DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14
    Data columns (total 4 columns):
    AAPL    2214 non-null float64
    MSFT    2214 non-null float64
    XOM     2214 non-null float64
    SPX     2214 non-null float64
    dtypes: float64(4)
    memory usage: 86.5 KB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    close_px[-4:]
    
    • 1
    AAPLMSFTXOMSPX
    2011-10-11400.2927.0076.271195.54
    2011-10-12402.1926.9677.161207.25
    2011-10-13408.4327.1876.371203.66
    2011-10-14422.0027.2778.111224.58

    一个比较有意思的尝试是计算一个DataFrame,包括与SPX这一列逐年日收益的相关性(计算百分比变化)。一个可能的方法是,我们先创建一个能计算不同列相关性的函数,然后拿每一列与SPX这一列求相关性:

    spx_corr = lambda x: x.corrwith(x['SPX'])
    
    • 1

    然后我们通过pct_changeclose_px上计算百分比的变化:

    rets = close_px.pct_change().dropna()
    
    • 1

    最后,我们按年来给这些百分比变化分组,年份可以从每行的标签中通过一个一行函数提取,然后返回的结果中,用datetime标签来表示年份:

    get_year = lambda x: x.year
    
    • 1
    by_year = rets.groupby(get_year)
    
    • 1
    by_year.apply(spx_corr)
    
    • 1
    AAPLMSFTXOMSPX
    20030.5411240.7451740.6612651.0
    20040.3742830.5885310.5577421.0
    20050.4675400.5623740.6310101.0
    20060.4282670.4061260.5185141.0
    20070.5081180.6587700.7862641.0
    20080.6814340.8046260.8283031.0
    20090.7071030.6549020.7979211.0
    20100.7101050.7301180.8390571.0
    20110.6919310.8009960.8599751.0

    我们也可以计算列内的相关性。这里我们计算苹果和微软每年的相关性:

    by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
    
    • 1
    2003    0.480868
    2004    0.259024
    2005    0.300093
    2006    0.161735
    2007    0.417738
    2008    0.611901
    2009    0.432738
    2010    0.571946
    2011    0.581987
    dtype: float64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    6 Example: Group-Wise Linear Regression(例子:组对组的线性回归)

    就像上面介绍的例子,使用groupby可以用于更复杂的组对组统计分析,只要函数能返回一个pandas对象或标量。例如,我们可以定义regress函数(利用statsmodels库),在每一个数据块(each chunk of data)上进行普通最小平方回归(ordinary least squares (OLS) regression)计算:

    import statsmodels.api as sm
    
    • 1
    def regress(data, yvar, xvars):
        Y = data[yvar]
        X = data[xvars]
        X['intercept'] = 1
        result = sm.OLS(Y, X).fit()
        return result.params
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    现在,按年用苹果AAPL在标普SPX上做线性回归:

    by_year.apply(regress, 'AAPL', ['SPX'])
    
    • 1
    SPXintercept
    20031.1954060.000710
    20041.3634630.004201
    20051.7664150.003246
    20061.6454960.000080
    20071.1987610.003438
    20080.968016-0.001110
    20090.8791030.002954
    20101.0526080.001261
    20110.8066050.001514
  • 相关阅读:
    02-Gin请求参数
    收藏-即时通讯(IM)开源项目OpenIM-功能手册
    [Leetcode] 0836. 矩形重叠
    浅谈量子纠缠,易经,能量,世界(一)
    钢铁行业B2B供应链集采平台:完善供应商管理,健全供应商管理机制
    postgresql-类型转换函数
    内核双链表篇:list.h——list_for_each宏与list_for_each_safe宏(遍历链表)
    Python2也不错
    R语言编写用户自定义函数: 使用RStudio的快捷键重新格式化代码、使得代码更好地排列、缩进等
    Mybatis( If条件失效 )
  • 原文地址:https://blog.csdn.net/weixin_46530492/article/details/134431938