【问题标题】:Count of current unique values in a pandas dfpandas df中当前唯一值的计数
【发布时间】:2018-08-10 03:54:57
【问题描述】:

我正在尝试在 pandas df 中返回 countunique 值。这是每个row 的累积计数。我的目标是合并一个函数来确定当前在任何时间点出现了多少值。

import pandas as pd

df = pd.DataFrame({          
    'A' : ['8:06:00','11:00:00','11:30:00','12:00:00','13:00:00','13:30:00','14:00:00','17:00:00'],
    'B' : ['ABC','ABC','DEF','XYZ','ABC','LMN','DEF','ABC'],          
    'C' : [1,2,1,1,3,1,2,4],            
    })

          A    B  C
0   8:06:00  ABC  1
1  11:00:00  ABC  2
2  11:30:00  DEF  1
3  12:00:00  XYZ  1
4  13:00:00  ABC  3
5  13:30:00  LMN  1
6  14:00:00  DEF  2
7  17:00:00  ABC  4

所以col['B'] 中有 4 个unique 值。我正在测量的

df1 = df['B'].nunique()

但我希望通过column 合并一个函数iterates,以识别是否再次出现任何特定值。如果不是,我希望减少计数。如果这是第一次出现该值,我想增加计数。如果该值已经出现并再次出现,则计数应保持不变。这将显示在任何时间点发生了多少值。

使用@jpp 的代码,我们生成以下内容:

cum_maxer = pd.Series(pd.factorize(df['B'])[0] + 1).cummax()
df['res'] = cum_maxer - df['B'].duplicated().cumsum()

print(df)

输出:

          A    B  C  res
0   8:06:00  ABC  1    1
1  11:00:00  ABC  2    0
2  11:30:00  DEF  1    1
3  12:00:00  XYZ  1    2
4  13:00:00  ABC  3    1
5  13:30:00  LMN  1    2
6  14:00:00  DEF  2    1
7  17:00:00  ABC  4    0

'res' 的预期输出

0  1
1  1
2  2
3  3
4  2
5  3
6  2
7  1

基本上,如果value 第一次出现我想将它添加到cumulative count。如果该值完成(稍后未出现),则计数应减少。如果值已经出现并再次出现,则计数应保持不变。

每一行的概要和预期的输出:

1st rowABC 首次出现,稍后出现。 Count = +1

2nd row, ABC 再次出现所以没有增加。它也出现在以后,所以没有减少。 Count = no change

3rd rowDEF 首次出现,稍后出现。 Count = +1

4th rowXYZ 是第一次出现,但后来没有出现。不过,此时出现了 3 个值,因此 count is 3。计数自动下降到下一行为XYZ has finished

5th row,如上所述,XYZ 已完成,因此目前只有ABCDEF 处于启用状态。 ABC 值也再次出现,因此 count is 2

6th rowLMN 首次出现,因此计数增加。这意味着ABC, DEF, LMN 在该时间点是最新的。就像row 4 一样,LMN 不会再次出现,因此当LMN 完成时,下一行的计数将减少。 Count is 3

第 7 行,DEFABC 当前处于启用状态,因此 count is 2。由于DEF 不再出现,因此计数将在下一行减少。

第 8 行,ABC 是当前唯一的值,所以 count is 1

【问题讨论】:

  • 为什么行 3 不是 3 1 在您的预期输出中?
  • 我没有跟踪 - 你是什么意思“它回落到 2 因为 XYZ 不再出现”?这是某种累积计数,还是按每个B 值分组的计数,还是某种索引?多一点解释将有助于理解您的最终向量。
  • 我没有很好地解释它,但它是唯一值的累积。我现在换个问题。
  • @JeremyAlexander 如果重复 ABC,为什么前两行有 11
  • @RafaelC 有意义吗?

标签: python pandas count unique


【解决方案1】:

你也可以使用np.unique

u = np.unique(df.B, return_index=True)
df['id'] = df.B.map(dict(zip(*u))) + 1

0    1
1    2
2    3
3    1
4    2
5    1

编辑的问题

对于您编辑的问题,这里有一个解决方案。首先,在倒置数据框中使用cumcount预见未来

df['u'] = df[::-1].groupby('B').B.cumcount()

这样u 表示对于每个B,当前B 在未来出现多少次。然后,zip Bu 与您的逻辑,使用 S_n = S_{n-1} + new_value + dec 其中 new_value 标志是 True 如果当前 val 是一个新值,decTrue 如果上一行是该值的最后一次出现(即当时的u==0)。代码类似于

ids = [1]
seen = set([df.iloc[0].B])
dec = False
for val, u in zip(df.B[1:], df.u[1:]):
    ids.append(ids[-1] + (val not in seen) - dec)
    seen.add(val)
    dec = u == 0

df['S'] = ids

    A           B   C   u   S   expected
0   8:06:00     ABC 1   3   1          1
1   11:00:00    ABC 2   2   1          1
2   11:30:00    DEF 1   1   2          2
3   12:00:00    XYZ 1   0   3          3
4   13:00:00    ABC 3   1   2          2
5   13:30:00    LMN 1   0   3          3
6   14:00:00    DEF 2   0   2          2
7   17:00:00    ABC 4   0   1          1

在哪里

>>> (df.S == df.expected).all()
True

时间

df = pd.DataFrame({          
'A' : ['8:06:00','11:00:00','11:30:00','12:00:00','13:00:00','13:30:00','14:00:00','17:00:00'],
'B' : ['ABC','ABC','DEF','XYZ','ABC','LMN','DEF','ABC'],          
'C' : [1,2,1,1,3,1,2,4],            
})

def matt(df):
    valsets = df['B'].apply(lambda x: {x})
    union_sets = np.frompyfunc(lambda x, y: x | y, 2, 1)
    intersect_count = np.frompyfunc(lambda x, y: len(x & y), 2, 1)

    seen = union_sets.accumulate(valsets, dtype=np.object)
    to_be_seen = union_sets.accumulate(valsets[::-1], dtype=np.object)[::-1]
    df['res'] = intersect_count(seen, to_be_seen)
    return df

def raf(df):
    ids = [1]
    seen = set([df.iloc[0].B])
    dec = False
    df['u'] = df[::-1].groupby('B').B.cumcount()
    for val, u in zip(df.B[1:], df.u[1:]):
        ids.append(ids[-1] + (val not in seen) - dec)
        seen.add(val)
        dec = u == 0

    df['S'] = ids
    return df

df = pd.concat([df]*10000).reset_index()

结果

%timeit matt(df)
168 ms ± 12.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit raf(df)
64.2 ms ± 2.04 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

【讨论】:

    【解决方案2】:

    您可以使用pd.factorize 为每个唯一值分配一个整数标识符,然后在结果上使用cummax 进行滚动计数。

    df['id'] = pd.factorize(df['B'])[0] + 1
    df['count'] = df['id'].cummax()
    
    print(df)
    
              A    B  C  id  count
    0   8:06:00  ABC  1   1      1
    1  11:00:00  DEF  1   2      2
    2  12:00:00  XYZ  1   3      3
    3  13:00:00  ABC  2   1      3
    4  13:30:00  LMN  1   4      4
    5  14:00:00  DEF  2   2      4
    6  17:00:00  ABC  3   1      4
    

    更新

    对于您想要的输出,您可以像以前一样计算cummax 并减去重复的累积计数:

    cum_maxer = pd.Series(pd.factorize(df['B'])[0] + 1).cummax()
    df['res'] = cum_maxer - df['B'].duplicated().cumsum()
    
    print(df)
    
              A    B  C  res
    0   8:06:00  ABC  1    1
    1  11:00:00  DEF  1    2
    2  12:00:00  XYZ  1    3
    3  13:00:00  ABC  2    2
    4  13:30:00  LMN  1    3
    5  14:00:00  DEF  2    2
    6  17:00:00  ABC  3    1
    

    【讨论】:

    • 这很棒。如果Col B 中的value 完成,有什么方法可以确定cumulative count 的减少吗?
    • 谢谢@jpp。这很棒,但在达到最大值之前它不会减少。请参阅更新的问题。你已经回答了第一个问题,所以我很高兴奖励赏金并发布另一个问题。谢谢。
    • 很抱歉成为一个痛苦的伙伴,但这也不起作用。我知道您只能离开示例数据集,但代码必须类似于描述。即迭代并检查未来值,如果没有减少。如前所述,我很高兴获得赏金。这是我的错。我会再次更新问题。
    • @JeremyAlexander,我不担心赏金,只是想得到你想要的!输出似乎相同,cumsum 的逻辑似乎与您的文本描述相匹配。
    【解决方案3】:

    更新速度更快

    我希望我在给出下面的答案之前已经注意到@RafaelC 的groupby.cumcount() 技术。这让我想到了一种更快的方法。正如@RafaelC 所注意到的,当您处理行时,无需使用完整的观察列表;只需知道当前符号早晚出现多少次就足够了。事实上,正如您在更新中指出的那样,您真正需要知道的是当前行上的符号是否第一次出现(将计数加 1)以及上一行上的符号是否刚刚出现最后一次(从计数中减去 1)。考虑到这一点,您可以使用这个相当简单和精简的代码:

    将 numpy 导入为 np,将 pandas 导入为 pd

    import numpy as np, pandas as pd
    
    df = pd.DataFrame({          
        'A' : ['8:06:00','11:00:00','11:30:00','12:00:00','13:00:00','13:30:00','14:00:00','17:00:00'],
        'B' : ['ABC','ABC','DEF','XYZ','ABC','LMN','DEF','ABC'],          
        'C' : [1,2,1,1,3,1,2,4],            
    })
    
    groups = df.groupby('B')['B']
    # flag the first and last appearance of each symbol
    first_appearance = (groups.cumcount() == 0).astype(int)
    last_appearance = (groups.cumcount(False) == 0).astype(int)
    # delay effect of last_appearance by one step
    last_appearance = pd.np.concatenate(([0], last_appearance.values[:-1]))
    df['res'] = (first_appearance - last_appearance).cumsum()
    print df
    #           A    B  C  res
    # 0   8:06:00  ABC  1    1
    # 1  11:00:00  ABC  2    1
    # 2  11:30:00  DEF  1    2
    # 3  12:00:00  XYZ  1    3
    # 4  13:00:00  ABC  3    2
    # 5  13:30:00  LMN  1    3
    # 6  14:00:00  DEF  2    2
    # 7  17:00:00  ABC  4    1
    

    调用matthias2 并重新运行@RafaelC 的基准测试会得到以下结果:

    %timeit matthias1(df)
    10 loops, best of 3: 109 ms per loop
    %timeit raf(df)
    1 loops, best of 3: 230 ms per loop
    %timeit matthias2(df)
    100 loops, best of 3: 7 ms per loop
    

    原答案,比较慢

    下面的代码怎么样?这样做的想法是使用两个累积集:一个显示从列表开始到当前点已经看到的所有项目,一个显示列表中尚未看到的所有项目。后一个集合的创建方式与第一个相同,只需反转列表,构建累积集,然后再次反转列表。

    Pandas 没有通用的 accumulate 函数来执行此操作。您可能可以使用pd.Series.expanding 到达那里,但这会在每一步重新累积系列的大片,这会产生缓慢的 n^2 时间依赖性。所以我使用numpyaccumulate 函数来构建集合,如下图所示。这应该会非常有效地运行并且几乎一样清晰。

    import numpy as np, pandas as pd
    
    df = pd.DataFrame({          
        'A' : ['8:06:00','11:00:00','11:30:00','12:00:00','13:00:00','13:30:00','14:00:00','17:00:00'],
        'B' : ['ABC','ABC','DEF','XYZ','ABC','LMN','DEF','ABC'],          
        'C' : [1,2,1,1,3,1,2,4],            
    })
    
    # convert individual values to sets to make the next steps easier
    valsets = df['B'].apply(lambda x: {x})
    
    # define numpy ufuncs to get union of sets and size of intersection of sets
    # note that union_sets.accumulate() will give a "cumulative union" of sets
    union_sets = np.frompyfunc(lambda x, y: x | y, 2, 1)
    intersect_count = np.frompyfunc(lambda x, y: len(x & y), 2, 1)
    
    # create numpy vectors showing how many unique values have been seen up to 
    # each point, and how many will be seen from there to the end
    seen = union_sets.accumulate(valsets, dtype=np.object)
    to_be_seen = union_sets.accumulate(valsets[::-1], dtype=np.object)[::-1]
    
    # count how many are in both the have-been-seen and to-be-seen sets
    df['res'] = intersect_count(seen, to_be_seen)
    
    # add intermediate vectors for illustration
    df['seen'] = seen
    df['to_be_seen'] = to_be_seen
    
    print(df)
              A    B  C res                  seen            to_be_seen
    0   8:06:00  ABC  1   1                 {ABC}  {XYZ, ABC, DEF, LMN}
    1  11:00:00  ABC  2   1                 {ABC}  {XYZ, ABC, LMN, DEF}
    2  11:30:00  DEF  1   2            {ABC, DEF}  {XYZ, ABC, DEF, LMN}
    3  12:00:00  XYZ  1   3       {XYZ, ABC, DEF}  {XYZ, ABC, LMN, DEF}
    4  13:00:00  ABC  3   2       {XYZ, ABC, DEF}       {ABC, DEF, LMN}
    5  13:30:00  LMN  1   3  {XYZ, ABC, LMN, DEF}       {ABC, LMN, DEF}
    6  14:00:00  DEF  2   2  {XYZ, ABC, DEF, LMN}            {ABC, DEF}
    7  17:00:00  ABC  4   1  {XYZ, ABC, LMN, DEF}                 {ABC}
    

    请注意,我将中间向量存储在数据框中,以便您了解算法的工作原理。但是在您的生产代码中没有必要这样做。

    【讨论】:

      猜你喜欢
      • 2018-11-05
      • 2014-11-30
      • 1970-01-01
      • 2017-02-26
      • 2019-04-13
      • 1970-01-01
      • 1970-01-01
      • 2019-02-12
      • 1970-01-01
      相关资源
      最近更新 更多