首页 > 解决方案 > 按多列中的任何共享值分组

问题描述

通常,group-by 通过将表中的唯一键组合在一起来工作。但是,如果它们共享任何相同的值,我想将它们组合在一起。

假设我想在这张表中按 A、B、C 分组......

一种 C
1 2 3
4 5 6
1 3 4
2 2 8

我想将 (1, 2, 3), (1, 3, 4), (2, 2, 8) 组合在一起,因为它们每个都与组中的另一个元素共享至少一个列值。

(4, 5, 6) 虽然不与任何其他元素共享任何值,因此它将是它自己的组。

所以预期的输出将是两组:

一种 C
2 2 8
1 2 3
1 3 4
一种 C
4 5 6

关于如何实现这种行为的任何想法(理想情况下以某种有效的方式,因为我的 pandas DataFrame 中有大约 100 万行)?

这个问题要求同样的事情,但特定于 SQL: SQL query like GROUP BY with OR condition

标签: pythonpandaspandas-groupby

解决方案


也许这不是最漂亮的解决方案,但我们开始吧。首先,我们melt使用原始数据框,以便所有行值成为单个列的一部分。此外,通过使用reset_indexandset_index我们创建一个新index的来跟踪每个值来自的原始行。

df_melt = (df.reset_index()
             .melt(id_vars='index')
             .set_index('index'))

到目前为止,我们有以下数据框:

      variable  value
index                
0            A      1
1            A      4
2            A      1
3            A      2
0            B      2
1            B      5
2            B      3
3            B      2
0            C      3
1            C      6
2            C      4
3            C      8

现在我们使用 agroupby来过滤原始数据帧中具有相同值的行。由于我们还需要跟踪值的列,因此我们执行以下操作groupby

grouped = df_melt.groupby(['variable', 'value']).groups

这给了我们:

# print(grouped)
{('A', 1): [0, 2], ('A', 2): [3], ('A', 4): [1], 
 ('B', 2): [0, 3], ('B', 3): [2], ('B', 5): [1], 
 ('C', 3): [0], ('C', 4): [2], ('C', 6): [1], ('C', 8): [3]}

然后我们创建一个字典来总结每列的组:

groups_per_col = {
    col: [set(value) for key, value in grouped.items() if key[0] == col] for col in cols
}
# {'A': [{0, 2}, {3}, {1}], 'B': [{0, 3}, {2}, {1}], 'C': [{0}, {2}, {1}, {3}]}

最后我们遇到了真正的问题,即在所有列中找到所有独立的行集。

final_groups = []
for col in groups_per_col:
    my_groups = groups_per_col[col]

    for g in my_groups:
        for i, aux in enumerate(final_groups):
            if len(aux.intersection(g)) > 0:
                final_groups[i] = aux.union(g)
                break
        else:
            final_groups.append(g)

    aux_groups = []
    while True:
        for g in final_groups:
            for i, aux in enumerate(aux_groups):
                if len(aux.intersection(g)) > 0:
                    aux_groups[i] = aux.union(g)
                    break
            else:
                aux_groups.append(g)
        if len(aux_groups) == len(final_groups):
            break
        else:
            final_groups = aux_groups[:]
            aux_groups = []

这可能需要一些时间,具体取决于行数(这在您的情况下很糟糕,有 100 万行),但也取决于您的 df.xml 中可能值的数量。例如,如果您的值介于 0 到 100 之间,则以下代码将在大约 20 秒内运行(在我简陋的笔记本电脑中),但如果有 1000 个可能的值,则此时间会增加到 2 分钟。这段代码在效率方面并不完美,但至少它是一个起点。

代码输出是final_groups,它为我们提供了一个集合列表,其中每个集合都包含属于同一组的行:

[{0, 2, 3}, {1}]

由您决定如何处理这些组。例如,您可以使用标识符在数据框中创建一个新列,例如:

df['Group'] = ['G{}'.format(
              [index in group for group in final_groups].index(True)) 
              for index in df.index]

这导致以下数据框:

   A  B  C Group
0  1  2  3    G0
1  4  5  6    G1
2  1  3  4    G0
3  2  2  8    G0

欢迎任何人提出改进建议。希望这对你有用!


推荐阅读