V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
yaleyu
V2EX  ›  Python

开新帖求教 pandas 大拿,关于 groupby 和 cumsum 和 rolling

  •  
  •   yaleyu · 2021-03-11 18:49:28 +08:00 · 1321 次点击
    这是一个创建于 516 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原贴: https://www.v2ex.com/t/760567 求助问题:对每个 A 列里面的值,当 C 列为 False 时候,D 列为 0,当 C 列为 True 时候,D 列为上一个 True 之后的第一个 False 到当前行的 B 列总和。

    更改了一下数据,更加接近原始数据

    df = pd.DataFrame([['S1', 10, False], ['S1', 10, True],
        ['S2', 20, False], ['S2', 10, False], ['S2', 10, True],
        ['S3', 200, False], ['S3', 100, False], ['S3', 100, True]],
        columns=list('ABC'))
    print(df)
        A    B      C
    0  S1   10  False
    1  S1   10   True
    2  S2   20  False
    3  S2   10  False
    4  S2   10   True
    5  S3  200  False
    6  S3  100  False
    7  S3  100   True
    

    用 for 循环来切片然后再处理,能得到希望的结果:

    codes = df.A.unique()
    dfs = []
    for code in codes:
        subdf = df[df.A == code].reset_index()
        slices = subdf[subdf.C].index
        slices = slices.insert(0, -1)
        for i in range(len(slices) - 1):
            tempdf = subdf.loc[slices[i]+1: slices[i+1]].copy()
            tempdf['D'] = np.where(tempdf.C, tempdf.groupby('A').B.sum(), 0)
            dfs.append(tempdf)
    df_with_d = pd.concat(dfs).reset_index()
    print(df_with_d[list('ABCD')])
        A    B      C    D
    0  S1   10  False    0
    1  S1   10   True   20
    2  S2   20  False    0
    3  S2   10  False    0
    4  S2   10   True   40
    5  S3  200  False    0
    6  S3  100  False    0
    7  S3  100   True  400
    

    觉得效率不高,求更有效的方法!

    按原贴 @necomancer 的方法

    df['D'] = np.where(df.C, df.groupby(df.C.eq(False).cumsum()).B.cumsum(), 0)
    print(df)
        A    B      C    D
    0  S1   10  False    0
    1  S1   10   True   20
    2  S2   20  False    0
    3  S2   10  False    0
    4  S2   10   True   20
    5  S3  200  False    0
    6  S3  100  False    0
    7  S3  100   True  200
    

    第 4 行 D 列的结果不对,应该是 40 (20+10+10),第 7 行 D 列应该是 400

    按 @cassidyhere 的方法

    class CustomIndexer(BaseIndexer):
        def get_window_bounds(self, num_values, min_periods, center, closed):
            start = np.empty(num_values, dtype=np.int64)
            end = np.empty(num_values, dtype=np.int64)
            for i in range(num_values):
                end[i] = i + 1
                j = i
                while j > 0 and self.use_expanding[j]:
                    j -= 1
                    start[i] = j
            return start, end
        
    window_size = df.C.groupby((df.C != df.C.shift(1)).cumsum()).agg('sum').max() # 最大连续次数
    indexer = CustomIndexer(window_size=window_size, use_expanding=df.C)
    df['D'] = np.where(df.C, df.B.rolling(indexer, min_periods=2).sum().fillna(0), 0)
    print(df)
        A    B      C      D
    0  S1   10  False    0.0
    1  S1   10   True   20.0
    2  S2   20  False    0.0
    3  S2   10  False    0.0
    4  S2   10   True   20.0
    5  S3  200  False    0.0
    6  S3  100  False    0.0
    7  S3  100   True  200.0
    

    也是有同样的问题

    第 1 条附言  ·  2021-03-12 13:35:41 +08:00
    综合各位大拿的思路,套在实际数据和循环切片得到的结果一一对比,下面这个方法最简洁:
    ```
    df['D'] = np.where(df.C, df.groupby(df.C.eq(True).shift(fill_value=False).cumsum()).B.cumsum(), 0)
    ```

    循环切片性能:
    12.2 ms ± 22.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

    上面的方法的性能:
    1.87 ms ± 4.07 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

    果然是 pandas 自有的方法效率高太多了。

    再次谢谢各位大拿。
    12 条回复    2021-03-15 15:59:05 +08:00
    HelloViper
        1
    HelloViper  
       2021-03-12 10:08:58 +08:00
    个人认为不要在 pandas 上做处理,应当吧 b 列和 c 列单独 tolist,通过单层遍历就算出 d 列的 list,在组装回去

    随手写点,没细想边界值之类的:

    d=[]
    last_false = 0
    for i,(x,y) in enumerate(b,c):
    if y:
    d.append(sum(b[last_false:i+1])
    last_false=i+1
    else:
    d.append(0)
    necomancer
        2
    necomancer  
       2021-03-12 10:39:24 +08:00
    你上个帖子里说
    如下一个表,想每当 C 列为 False 时候,D 列为 0,为 True 时候,D 列为 B 列的上一次 C 列为 False 到当前列的加总

    这次就变成
    求助问题:对每个 A 列里面的值,当 C 列为 False 时候,D 列为 0,当 C 列为 True 时候,D 列为上一个 True 之后的第一个 False 到当前行的 B 列总和。

    大屁眼子!
    TimePPT
        3
    TimePPT  
       2021-03-12 10:43:17 +08:00
    试试换个思路加辅助列呢

    df = pd.DataFrame([['S1', 10, False], ['S1', 10, True], ['S2', 20, False], ['S2', 10, False], ['S2', 10, True], ['S3', 200, False], ['S3', 100, False], ['S3', 100, True]], columns=list('ABC'))

    df['D'] = df['B'].cumsum()
    df_tmp = df[df['C']]
    df_tmp['X'] = df_tmp['D'].diff()
    df = pd.merge(left=df, right=df_tmp, on=['A', 'B', 'C', 'D'], how='left')
    df['D'] = np.where(df['C']==False, 0, df['D'])
    df['D'] = np.where(((df['C'] == True) & (df['X'].isna() == False)), df['X'], df['D'])
    df = df[['A', 'B', 'C', 'D']]

    print(df)
    necomancer
        4
    necomancer  
       2021-03-12 10:52:48 +08:00
    df['D'] = np.where(df.C, df.groupby(pd.Series(np.diff(df.C, prepend=0)).eq(-1).cumsum()).B.cumsum(),0)

    df

    A B C D
    0 S1 10 False 0
    1 S1 10 True 20
    2 S2 20 False 0
    3 S2 10 False 0
    4 S2 10 True 40
    5 S3 200 False 0
    6 S3 100 False 0
    7 S3 100 True 400
    zone10
        5
    zone10  
       2021-03-12 11:09:42 +08:00   ❤️ 1
    @necomancer 根据你的思路,

    df['D'] = np.where(df.C, df.groupby(df.C.eq(True).cumsum().shift(1, fill_value=0)).B.cumsum(), 0)
    print(df)
    yaleyu
        6
    yaleyu  
    OP
       2021-03-12 11:49:07 +08:00 via Android
    @necomancer 哈哈哈,莫怪莫怪,第一次需求没说清
    yaleyu
        7
    yaleyu  
    OP
       2021-03-12 11:50:22 +08:00 via Android
    yaleyu
        8
    yaleyu  
    OP
       2021-03-12 11:52:05 +08:00 via Android
    @zone10 这个和在 stackoverflow 求助得到的一样了,我把思路套进实际数据比对一下,谢谢
    princelai
        9
    princelai  
       2021-03-12 14:35:32 +08:00
    想到一个思路不太一样的方法

    ```
    import pandas as pd
    import numpy as np

    df.loc[df.C == True, 'Z'] =range(df.C.sum())
    df.Z.bfill(inplace=True)
    df['D'] = np.where(df.C,df.groupby('Z')['B'].transform('sum'),0)
    df.drop(columns='Z',inplace=True)
    ```
    yaleyu
        10
    yaleyu  
    OP
       2021-03-13 22:11:46 +08:00
    @HelloViper 原来的代码跑出来有错,没考虑到 A,稍微改了一下
    ```
    a = df.A.to_list()
    b = df.B.to_list()
    c = df.C.to_list()
    d = []
    first_false = 0
    for i, (x, y, z) in enumerate(zip(a, b, c)):
    if a[i] != a[i-1] and not z:
    first_false = i
    if z:
    d.append(sum(b[first_false: i+1]))
    else:
    d.append(0)
    df['D'] = d
    print(df)
    ```
    居然性能是最快的:
    226 µs ± 4.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    necomancer
        11
    necomancer  
       2021-03-14 20:35:35 +08:00
    @yaleyu 这速度测试……是数据集太小了吧……
    HelloViper
        12
    HelloViper  
       2021-03-15 15:59:05 +08:00
    @yaleyu groupby 写的爽但肯定影响性能的,这种需求可以使用标识位通过单层遍历一把梭,o(n),而且可读性强,我回的时候正好有事,边界值 zip 什么的全写漏了哈哈
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1117 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 20:29 · PVG 04:29 · LAX 13:29 · JFK 16:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.