如何在分类变量中获取类别的各种组合并同时对其进行聚合?

How to get various combinations of categories in a categorical variable and at the same time aggregate it?

我正在尝试获取三列数据的各种组合,同时我还想聚合(求和)这些值。

我的数据如下所示,下面是我的示例输出:

Dim1    Dim2    Dim3    Spend
A       X       Z       100
A       Y       Z       200
B       X       Z       300
B       Y       Z       400

示例输出:

Dim 1   Dim 2   Dim 3   Spend
A       NaN     NaN     300
A       X       NaN     100
A       Y       NaN     200
A       NaN     Z       300
B       NaN     NaN     700
B       X       NaN     300
B       Y       NaN     400
B       NaN     Z       700
NaN     X       Z       400
NaN     Y       Z       600
NaN     NaN     Z       1000
NaN     X       NaN     400
NaN     Y       NaN     600
A       X       Z       100
A       Y       Z       200
B       X       Z       300
B       Y       Z       400

Dim1Dim2Dim3 是分类变量,Spend 是 value/metric。我们需要找到分类变量的所有可能组合的总数 Spend,这部分我可以使用 itertools.combinations() 来实现。现在,不仅是三列,我们还可以获得任意数量的此类变量的组合,例如 Dim1、Dim2、Dim3 .. Dim 30 等等。

我的问题是我无法聚合相同的内容,例如,在第 12 行中,对于类别 ZSpend 值,我们正在执行 sum() Z 出现在主要数据中的所有值,因此值为 1000。我们如何实现聚合的那个值?


可重现的数据:

data = pd.DataFrame({'Dim1': ['A', 'A', 'B', 'B'],
 'Dim2': ['X', 'Y', 'X', 'Y'],
 'Dim3': ['Z', 'Z', 'Z', 'Z'],
 'Spend': [100, 200, 300, 400]})

摘要

您使用 itertools.combinations() 的想法是正确的。更多关键步骤:

  1. 对要求和的每个可能的维数应用 itertools.combinations()(从 1n_dim-1)。即 itertools.combinations(range(1, 1+n_dim), i),对于 i in range(1, 1+n_dim)
  2. 使用df.groupby(by=column_combinations).sum()自动从classes的组合中得到结果。

代码

该程序由 3 个逻辑部分组成。

  1. 从每个维度按 class 聚合。这部分基本上和你做的一样,只是re-designed通过一个DFS的方法来减少处理的数据总量。当有数百万行要处理时,这会很有用。后面的步骤也是基于这个中间数据集而不是原始数据集计算的。
  2. 一个生成器,用于循环遍历摘要 1 中提到的维度组合,并且没有显式枚举。
  3. 执行摘要 2 中提到的 group-by 计算并输出结果数据帧列表,可以在程序末尾连接这些数据帧。

警告:务必在生产使用中测试性能和内存问题。

import pandas as pd
import numpy as np
import itertools

df = pd.DataFrame(
    {'Dim1': ['A', 'A', 'B', 'B'],
     'Dim2': ['X', 'Y', 'X', 'Y'],
     'Dim3': ['Z', 'Z', 'Z', 'Z'],
     'Spend': [100, 200, 300, 400]
     }
)

# constants: column names and dimensions
n_dim = 3
dim_cols = [f"Dim{i}" for i in range(1, n_dim + 1)]
cols = dim_cols + ["Spend"]


# 1. compute sums with every dimension
def dfs(df, ls_out, dim_now=1, ls_classes=[]):

    # termination condition (every dimension has been traversed)
    if dim_now == n_dim + 1:
        # perform aggregation
        sum = df["Spend"].sum()
        ls_classes.append(sum)
        ls_out.append(ls_classes)
        return

    # proceed
    col = f"Dim{dim_now}"

    # get categories
    classes = df[col].unique()
    classes.sort()

    for c in classes:
        # recurse next dimension with subset data
        dfs(df[df[col] == c], ls_out,
            dim_now=dim_now + 1,
            ls_classes=ls_classes + [c])

ls_out = []  # the output container
dfs(df, ls_out)
# convert to dataframe
df_every_dim = pd.DataFrame(data=ls_out, columns=df.columns)
del ls_out
print(df_every_dim)


# 2. generate combinations of groupby-dimensions
def multinomial_combinations(n_dim):
    for i in range(1, 1+n_dim):
        for tup in itertools.combinations(range(1, 1+n_dim), i):
            yield tup

print("Check multinomial_combinations(4):")
for i in multinomial_combinations(4):
    print(i)

# 3. Sum based on from df_every_dim
def aggr_by_dims(df, by_dims):

    # guard
    if not (0 < len(by_dims) < n_dim):
        raise ValueError(f"Wrong n_dim={n_dim}, len(by_dims)={len(by_dims)}")

    # by-columns
    by_cols = [f"Dim{i}" for i in by_dims]

    # groupby-sum
    df_grouped = df.groupby(by=by_cols).sum().reset_index()

    # create none-columns (cannot be empty here)
    arr = np.ones(n_dim+1, dtype=int)
    arr[list(by_dims)] = 0
    for i in range(1, 1+n_dim):
        if arr[i] == 1:
            df_grouped[f"Dim{i}"] = None  # or np.nan as you wish

    # reorder columns
    return df_grouped[cols]

print("\nCheck aggr_by_dims(df_every_dim, [1, 3]):")
print(aggr_by_dims(df_every_dim, [1, 3]))

# combine 2. and 3.
ls = []
for by_dims in multinomial_combinations(n_dim):
    if len(by_dims) < n_dim:
        df_grouped = aggr_by_dims(df_every_dim, by_dims)
        ls.append(df_grouped)

# no none-dimensions
ls.append(df_every_dim)

# final result
df_ans = pd.concat(ls, axis=0)
df_ans.reset_index(drop=True, inplace=True)
print(df_ans)

输出

(省略了中间输出)

    Dim1  Dim2  Dim3  Spend
0      A  None  None    300
1      B  None  None    700
2   None     X  None    400
3   None     Y  None    600
4   None  None     Z   1000
5      A     X  None    100
6      A     Y  None    200
7      B     X  None    300
8      B     Y  None    400
9      A  None     Z    300
10     B  None     Z    700
11  None     X     Z    400
12  None     Y     Z    600
13     A     X     Z    100
14     A     Y     Z    200
15     B     X     Z    300
16     B     Y     Z    400