0

0

Pandas:基于动态阈值的条件分组与聚合策略

花韻仙語

花韻仙語

发布时间:2025-08-01 14:54:22

|

731人浏览过

|

来源于php中文网

原创

Pandas:基于动态阈值的条件分组与聚合策略

本教程探讨如何在Pandas中实现一种动态分组聚合策略。当数据框按多列分组时,如果某个分组的行数低于预设阈值,则停止在该级别继续细分,而是将其向上合并;对于行数超过阈值的组,则继续按更细粒度分组。文章将详细介绍一种高效的迭代聚合方法,以实现这种复杂的条件分组逻辑。

引言:动态阈值分组的需求

在数据分析中,我们经常需要根据一个或多个变量对数据进行分组并计算组的大小。然而,有时业务逻辑要求我们对分组行为进行更精细的控制:如果某个分组的成员数量过少(例如,低于某个预设阈值),我们就不希望继续在该分组内进行更细粒度的划分,而是将其视为一个整体,将其统计结果向上汇总到其父级分组中。对于那些成员数量足够大的分组,我们则希望继续按照所有指定的分组键进行细致的划分。

考虑以下示例数据集:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'a': [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2],
    'b': [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
    'c': [1, 1, 1, 2, 2, 2, 3, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2]
})
print("原始数据框:")
print(df)

我们的目标是根据列 a, b, c 进行分组。假设阈值为3。

  • 对于 a=1, b=1, c=1,有3行,达到阈值,应保留。
  • 对于 a=1, b=1, c=2,有3行,达到阈值,应保留。
  • 对于 a=1, b=2, c=3,有1行,低于阈值3,不应继续细分到 c。它应与 a=1, b=2, c=4 合并,形成 a=1, b=2 的组。
  • 对于 a=1, b=2, c=4,有2行,低于阈值3。
    • 因此,a=1, b=2 组的总行数为 1 + 2 = 3,达到阈值,应保留 a=1, b=2 这一层级。
  • 对于 a=2, b=2, c=2,有9行,远超阈值,应保留。

期望的输出结果如下,其中 NaN 表示在该层级不再进行细分:

   a  b    c  group_size
0  1  1  1.0           3
1  1  1  2.0           3
2  1  2  NaN           3
3  2  2  2.0           9

传统分组的局限性

Pandas的 groupby() 函数通常会将数据一直分组到所有指定键的最细粒度。例如,df.groupby(['a', 'b', 'c']).size() 会直接计算每个 (a, b, c) 组合的行数,这无法实现动态停止分组的需求。如果一个 (a, b, c) 组合的行数很少,我们无法在计算时自动将其向上合并到 (a, b) 层面。

核心策略:迭代式向上聚合

解决这种动态分组问题的核心思想是采用迭代式的向上聚合方法。其基本流程如下:

Artbreeder
Artbreeder

创建令人惊叹的插画和艺术

下载
  1. 从最细粒度开始计数: 首先,计算所有指定分组键组合下的行数。
  2. 识别并分离: 遍历这些最细粒度的组。如果一个组的行数达到了预设阈值,则将其标记为“已完成”的组,并将其从待处理列表中移除。
  3. 向上合并: 对于那些行数低于阈值的组,我们认为它们过于稀疏,不适合继续保持最细粒度的划分。我们将这些组向上合并到其父级分组(即,移除最右侧的分组键,重新计算父级分组的总行数)。
  4. 重复: 对合并后的父级分组重复步骤2和3,直到所有分组都达到阈值,或者所有分组键都被移除。

这种方法避免了复杂的条件判断和多次数据框合并操作,利用了Pandas groupby 在多级索引上的灵活性。

Pandas实现详解

下面是实现上述策略的详细步骤和代码:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'a': [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2],
    'b': [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
    'c': [1, 1, 1, 2, 2, 2, 3, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2]
})

thresh = 3 # 设定阈值

# 1. 初始化:获取最细粒度计数
# df.value_counts() 比 df.groupby(list(df)).size() 更高效,尤其在数据量大时
s = df.value_counts()
print("\n初始最细粒度计数 (value_counts()):")
print(s)

out = [] # 用于存储最终符合阈值的分组结果
cols = list(df.columns) # 初始分组键列表:['a', 'b', 'c']

# 2. 迭代处理:逐层向上合并
while cols and len(s):
    # 根据当前 cols 进行分组并求和(s 已经是计数,这里是聚合计数)
    # level=cols 表示按多级索引的指定层级进行分组
    s = s.groupby(level=cols).sum()
    print(f"\n当前分组键: {cols}, 聚合结果:\n{s}")

    # 找出行数低于阈值的组 (m)
    m = s < thresh
    print(f"低于阈值 ({thresh}) 的组:\n{s[m]}")

    # 将符合阈值的组(非 m)添加到结果列表 out
    # s[~m] 是那些计数 >= thresh 的组
    out.append(s[~m])
    print(f"符合阈值的组(已提取):\n{s[~m]}")

    # 更新 s,只保留低于阈值的组,供下一轮迭代处理(向上合并)
    s = s[m]
    print(f"待下一轮处理的组(低于阈值):\n{s}")

    # 移除最右边的分组键,准备进行更粗粒度的分组
    if cols: # 避免列表为空时pop()报错
        cols.pop()
    print(f"移除最右侧键后,剩余分组键: {cols}")

# 循环结束后,如果 s 中还有剩余的组(即所有键都移除后仍有组),则将其添加到结果中
# 这通常发生在即使只按第一个键分组,其计数仍低于阈值的情况下
if len(s):
    out.append(s)

# 3. 结果整合与格式化
# 将所有结果 Series 转换为 DataFrame 并重置索引
# reset_index() 会将多级索引转换为列
out_df = pd.concat([x.reset_index() for x in out])

# 默认的计数列名为 'count',可以根据需要重命名
out_df = out_df.rename(columns={'count': 'group_size'})

# 排序以便于查看,并处理 NaN
out_df = out_df.sort_values(by=list(df.columns)).reset_index(drop=True)
print("\n最终结果:")
print(out_df)

代码解析:

  1. thresh = 3: 定义了分组的最小阈值。
  2. s = df.value_counts(): 这是整个流程的起点。value_counts() 方法能够快速地计算数据框中所有列组合的唯一值及其出现次数,返回一个以多级索引(对应于 a, b, c)的 Series。其效率通常高于 groupby().size()。
  3. out = []: 一个列表,用于收集每一轮迭代中满足阈值条件的那些分组结果。
  4. cols = list(df.columns): 初始化分组键列表,这里是 ['a', 'b', 'c']。
  5. while cols and len(s):: 循环条件。只要还有分组键可以移除 (cols 不为空) 并且还有待处理的组 (s 不为空),就继续迭代。
  6. s = s.groupby(level=cols).sum(): 这是核心步骤。s 是一个多级索引 Series,我们通过 groupby(level=cols) 再次对其进行分组。level=cols 告诉 Pandas 根据 s 当前多级索引中 cols 指定的层级进行分组。sum() 操作将这些分组的计数累加起来,实现了“向上合并”的效果。例如,如果 cols 是 ['a', 'b'],它会将所有 (a,b,c) 组合中相同的 (a,b) 组合的计数加起来。
  7. m = s : 创建一个布尔掩码,标记出当前聚合后计数低于阈值的组。
  8. out.append(s[~m]): 将计数大于或等于阈值的组(~m 表示 m 的反向,即不小于阈值)添加到 out 列表中。这些组已经符合条件,不再需要进一步处理。
  9. s = s[m]: 更新 s,只保留那些计数仍然低于阈值的组。这些组需要在下一轮迭代中继续向上合并(即移除一个分组键,再次聚合)。
  10. cols.pop(): 移除 cols 列表中的最后一个元素。例如,如果 cols 是 ['a', 'b', 'c'],第一次循环后会变成 ['a', 'b'];第二次循环后变成 ['a']。这确保了每次迭代都在一个更粗的粒度上进行聚合。
  11. out.append(s): 循环结束后,如果 s 中还有剩余的元素(这意味着即使只按最左边的键分组,其计数仍然低于阈值),则将它们添加到 out 中。
  12. pd.concat([x.reset_index() for x in out]): 最后,将 out 列表中收集到的所有 Series 合并成一个 DataFrame。reset_index() 将多级索引转换为常规列,方便后续操作。
  13. out_df = out_df.rename(columns={'count': 'group_size'}): 默认的计数列名为 count,为了可读性,将其重命名为 group_size。
  14. out_df.sort_values(by=list(df.columns)).reset_index(drop=True): 对最终结果进行排序,使其更符合逻辑顺序,并重置索引。

注意事项与最佳实践

  • 列顺序的重要性:cols.pop() 操作是从列表的末尾移除元素。这意味着原始 df.columns 的顺序决定了向上合并的粒度。如果你希望在 c 不满足时合并到 b,在 b 不满足时合并到 a,那么你的列顺序应该是 ['a', 'b', 'c']。如果顺序是 ['c', 'b', 'a'],则逻辑会完全不同。
  • NaN 值的含义:在最终结果中,那些向上合并的行(例如 a=1, b=2 的行)其原始更细粒度的列(如 c)会显示为 NaN。这表示在该层级,我们不再关心 c 的具体值,因为整个 (a,b) 组合已经作为一个整体被统计。
  • 性能考量
    • df.value_counts() 在处理大型数据集时通常比 df.groupby(list(df)).size() 更快,因为它利用了内部优化的C实现。
    • 迭代次数取决于分组键的数量。对于少量分组键,此方法非常高效。
    • 每次迭代中的 groupby().sum() 操作可能会在非常大的 s Series 上消耗一定时间,但由于每次迭代 s 的大小都在减小,整体效率较高。
  • 灵活性:此方法非常灵活,可以轻松调整阈值 thresh 或分组键 cols 来适应不同的业务需求。

总结

本教程介绍了一种在Pandas中实现动态阈值条件分组的有效策略。通过迭代地从最细粒度向上聚合,并根据预设阈值分离已完成的分组和待处理的分组,我们能够优雅地解决常规 groupby 无法满足的复杂场景。这种方法不仅逻辑清晰,而且在性能上具有优势,是处理此类条件分组问题的推荐方案。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
Python 时间序列分析与预测
Python 时间序列分析与预测

本专题专注讲解 Python 在时间序列数据处理与预测建模中的实战技巧,涵盖时间索引处理、周期性与趋势分解、平稳性检测、ARIMA/SARIMA 模型构建、预测误差评估,以及基于实际业务场景的时间序列项目实操,帮助学习者掌握从数据预处理到模型预测的完整时序分析能力。

51

2025.12.04

counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

197

2023.11.20

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

85

2023.09.25

append用法
append用法

append是一个常用的命令行工具,用于将一个文件的内容追加到另一个文件的末尾。想了解更多append用法相关内容,可以阅读本专题下面的文章。

343

2023.10.25

python中append的用法
python中append的用法

在Python中,append()是列表对象的一个方法,用于向列表末尾添加一个元素。想了解更多append的更多内容,可以阅读本专题下面的文章。

1070

2023.11.14

python中append的含义
python中append的含义

本专题整合了python中append的相关内容,阅读专题下面的文章了解更多详细内容。

174

2025.09.12

数据分析的方法
数据分析的方法

数据分析的方法有:对比分析法,分组分析法,预测分析法,漏斗分析法,AB测试分析法,象限分析法,公式拆解法,可行域分析法,二八分析法,假设性分析法。php中文网为大家带来了数据分析的相关知识、以及相关文章等内容。

464

2023.07.04

数据分析方法有哪几种
数据分析方法有哪几种

数据分析方法有:1、描述性统计分析;2、探索性数据分析;3、假设检验;4、回归分析;5、聚类分析。本专题为大家提供数据分析方法的相关的文章、下载、课程内容,供大家免费下载体验。

278

2023.08.07

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

36

2026.01.14

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号