利用 Pandas 高效分配数据框中的分组值:一种可扩展的解决方案

碧海醫心
发布: 2025-12-04 11:19:19
原创
838人浏览过

利用 Pandas 高效分配数据框中的分组值:一种可扩展的解决方案

本文探讨了如何使用 pandas 高效且可扩展地处理数据框中按组分配值的场景。针对传统 `iloc` 手动赋值方式的局限性,文章提出了一种基于 `groupby().transform()` 的优化方法,通过一个简洁的函数实现了按优先级和最大值限制(如每人最多100个箱子,单人组全部分配)的智能分配,极大地提升了代码的可维护性和性能。

引言:数据框分组值分配的挑战

在数据分析和处理中,我们经常需要根据特定条件对数据框中的分组数据进行值分配。一个常见场景是,在每个组内,按照一定的优先级和限制(例如,每个工作者最多分配100个箱子,但如果只有一个工作者,则分配所有箱子),将总数分配给组内的各个成员。

传统上,一些开发者可能会尝试使用 iloc 结合 if/elif 语句来手动处理每个组内成员的赋值。然而,这种方法存在显著的局限性:

  1. 不可扩展性: 当组内的成员数量增加时,需要手动添加更多的 elif 分支和 iloc 索引,导致代码冗长且难以维护。
  2. 效率低下: 大量的条件判断和逐个索引操作可能导致性能瓶颈,尤其是在处理大型数据集时。

本文将介绍一种更为高效和可扩展的 Pandas 解决方案,它利用 groupby().transform() 结合一个简洁的函数来优雅地解决这个问题。

初始数据准备

首先,我们创建一个示例 Pandas 数据框,它包含商店(store)、工作者(worker)、现有箱子数量(boxes)以及待填充的优化箱子数量(optimal_boxes)。

import pandas
import numpy

data_stack_exchange = {'store': ['A','B', 'B', 'C', 'C', 'C', 'D', 'D', 'D', 'D'],
        'worker': [1,1,2,1,2,3,1,2,3,4],
        'boxes': [105, 90, 100, 80, 10, 200, 70, 210, 50, 0],
        'optimal_boxes': [0,0,0,0,0,0,0,0,0,0]}
df_stack_exchange = pandas.DataFrame(data_stack_exchange)

print("原始数据框:")
print(df_stack_exchange)
登录后复制

输出的原始数据框如下:

  store  worker  boxes  optimal_boxes
0     A       1    105              0
1     B       1     90              0
2     B       2    100              0
3     C       1     80              0
4     C       2     10              0
5     C       3    200              0
6     D       1     70              0
7     D       2    210              0
8     D       3     50              0
9     D       4      0              0
登录后复制

我们的目标是根据以下规则填充 optimal_boxes 列:

  • 工作者优先级按 worker 列的数值顺序。
  • 每个工作者最多分配100个箱子。
  • 如果一个商店只有一个工作者,则该工作者获得所有箱子,即使超过100个。
  • 箱子分配完为止。

低效的 iloc 解决方案回顾

为了说明问题,我们简要回顾一下原始问题中提出的基于 iloc 的解决方案。虽然它能达到预期结果,但其扩展性极差。

# 这是一个低效且不推荐的解决方案,仅用于说明问题
def box_optimizer_inefficient(x):
    total_boxes_in_group = x['boxes'].sum()
    group_size = x['optimal_boxes'].count()

    # 初始化一个与组大小相同的列表来存储分配结果
    assigned_boxes = [0] * group_size
    remaining_to_assign = total_boxes_in_group

    if group_size == 1:
        assigned_boxes[0] = total_boxes_in_group
    else:
        for i in range(group_size):
            if remaining_to_assign >= 100:
                assigned_boxes[i] = 100
                remaining_to_assign -= 100
            else:
                assigned_boxes[i] = remaining_to_assign
                remaining_to_assign = 0
            # 确保不会分配负数
            if remaining_to_assign < 0:
                remaining_to_assign = 0

    x['optimal_boxes'] = assigned_boxes
    return x

# df_stack_exchange_function = df_stack_exchange.groupby('store', as_index=False, group_keys=False).apply(box_optimizer_inefficient)
# print(df_stack_exchange_function)
登录后复制

上述代码的 if/elif 结构在原始问题中更加繁琐,需要为每个可能的组大小编写重复的代码块。这种方法不仅代码量大,而且在组大小变动时需要不断修改函数,难以维护。

优化方案:使用 groupby().transform()

为了解决上述问题,我们可以设计一个更通用的函数,并利用 Pandas 的 groupby().transform() 方法。transform() 方法的特点是它会将函数的结果广播回原始数据框的相应位置,确保输出的长度与输入组的长度一致。

核心分配函数 assign_boxes

我们定义一个 assign_boxes 函数,它接收一个 Pandas Series(代表一个组的 boxes 列),并返回一个与该 Series 长度相同的列表或 Series,其中包含计算出的 optimal_boxes 值。

def assign_boxes(s):
    """
    根据业务规则为每个工作者组分配箱子。

    参数:
    s (pandas.Series): 一个商店中所有工作者的 'boxes' 值的 Series。

    返回:
    list: 包含每个工作者分配到的 'optimal_boxes' 值的列表。
    """
    total_boxes = s.sum()  # 计算当前组(商店)的总箱子数
    num_workers = len(s)   # 当前组(商店)的工作者数量

    # 特殊情况:如果只有一个工作者,他将获得所有箱子
    if num_workers == 1:
        return [total_boxes]

    # 计算可以分配满100个箱子的工作者数量
    # min(total_boxes // 100, num_workers - 1) 是关键:
    # total_boxes // 100 给出理论上能分满100箱子的工人数量
    # num_workers - 1 确保至少有一个工人会获得剩余的箱子(而不是被计算为满100箱子)
    # 这样,即使总箱子数超过 (num_workers - 1) * 100,最后一个工人也会得到剩余的箱子,而不是0
    num_full_100_boxes_workers = min(total_boxes // 100, num_workers - 1)

    # 构建分配结果列表
    # 1. 前 num_full_100_boxes_workers 个工作者获得 100 个箱子
    assigned = [100] * num_full_100_boxes_workers

    # 2. 计算剩余箱子,分配给下一个工作者
    remaining_boxes = total_boxes - 100 * num_full_100_boxes_workers
    assigned.append(remaining_boxes)

    # 3. 如果还有未分配的工作者,他们获得 0 个箱子(因为箱子已经分完)
    # (num_workers - num_full_100_boxes_workers - 1) 是指除了那些分到100个和分到剩余箱子的工人之外的工人数量
    assigned.extend([0] * (num_workers - num_full_100_boxes_workers - 1))

    return assigned

# 应用函数到数据框
df_stack_exchange['optimal_boxes'] = df_stack_exchange.groupby('store')['boxes'].transform(assign_boxes)

print("\n优化后的数据框:")
print(df_stack_exchange)
登录后复制

输出结果

经过 assign_boxes 函数处理后的数据框 df_stack_exchange 如下所示:

  store  worker  boxes  optimal_boxes
0     A       1    105            105
1     B       1     90            100
2     B      2    100             90
3     C       1     80            100
4     C       2     10            100
5     C       3    200             90
6     D       1     70            100
7     D       2    210            100
8     D       3     50            100
9     D       4      0             30
登录后复制

这个结果与预期完全一致。

assign_boxes 函数详解

让我们深入分析 assign_boxes 函数的关键逻辑:

  1. total_boxes = s.sum() 和 num_workers = len(s):

    • s.sum() 计算当前商店(组)中所有工作者的 boxes 总和。
    • len(s) 获取当前商店的工作者数量。
  2. if num_workers == 1: return [total_boxes]:

    • 这是处理单工作者商店的特殊规则。如果只有一个工作者,他将获得 total_boxes 的所有箱子,无论是否超过100。
  3. num_full_100_boxes_workers = min(total_boxes // 100, num_workers - 1):

    DubbingX智声云配
    DubbingX智声云配

    多情绪免费克隆AI音频工具

    DubbingX智声云配 975
    查看详情 DubbingX智声云配
    • 这是算法的核心。
    • total_boxes // 100:计算理论上可以分到完整100个箱子的工作者数量。
    • num_workers - 1:确保至少有一个工作者会得到剩余的箱子。通过将其作为 min 函数的第二个参数,我们可以防止所有工作者都被分配100个箱子后,没有工作者来接收剩余的零散箱子。例如,如果有4个工作者和330个箱子,total_boxes // 100 是3。num_workers - 1 是3。min(3, 3) 结果是3。这意味着前3个工作者各分100,最后一个工作者分剩余的30。如果 num_workers - 1 不存在,且总数是300,total_boxes // 100 是3,num_workers 是3,那么所有工人都会被分到100,但这样无法处理剩余的箱子。这个 num_workers - 1 巧妙地保证了最后一个有资格获得箱子的工作者会得到所有剩余的箱子。
  4. *`assigned = [100] num_full_100_boxes_workers`**:

    • 创建一个列表,其中前 num_full_100_boxes_workers 个元素都是100,代表这些工作者各分得100个箱子。
  5. *`remaining_boxes = total_boxes - 100 num_full_100_boxes_workers`**:

    • 计算在分配完那些获得100个箱子的工作者后,还剩下多少箱子。
  6. assigned.append(remaining_boxes):

    • 将剩余的箱子分配给下一个工作者(即 num_full_100_boxes_workers 之后的第一个工作者)。
  7. *`assigned.extend([0] (num_workers - num_full_100_boxes_workers - 1))`**:

    • 如果还有其他工作者(即除了那些分到100个和分到剩余箱子的工作者之外),他们将获得0个箱子,因为所有箱子都已分配完毕。

示例演练

为了更好地理解,我们以 store 'D' 的数据为例: s = pd.Series([70, 210, 50, 0]) (原始 boxes 值)

  • total_boxes = s.sum() = 70 + 210 + 50 + 0 = 330
  • num_workers = len(s) = 4
  1. num_workers 不等于1。

  2. num_full_100_boxes_workers = min(330 // 100, 4 - 1)

    • 330 // 100 = 3
    • 4 - 1 = 3
    • min(3, 3) = 3。这意味着有3个工作者将获得100个箱子。
  3. assigned = [100] * 3 -> [100, 100, 100]

  4. remaining_boxes = 330 - (100 * 3) = 330 - 300 = 30

  5. assigned.append(30) -> [100, 100, 100, 30]

  6. assigned.extend([0] * (4 - 3 - 1)) -> assigned.extend([0] * 0) -> [] (没有额外的0需要添加)

最终返回 [100, 100, 100, 30],这正是 store 'D' 的预期 optimal_boxes 值。

总结与注意事项

通过 groupby().transform() 结合一个精心设计的函数,我们成功地实现了一个可扩展且高效的数据框分组值分配方案。这种方法避免了手动 iloc 索引和冗长的 if/elif 结构,使代码更简洁、更易于维护,并能更好地适应未来数据规模和规则的变化。

注意事项:

  • transform() 方法要求你的函数返回一个与输入组长度相同的 Series 或列表。
  • 在设计函数时,要充分考虑所有边缘情况,例如本例中的单工作者组和箱子不足以分满100的情况。
  • 对于非常大的数据集,虽然 transform 已经很高效,但如果业务逻辑极其复杂,可能需要考虑使用 Numba 或 Cython 等工具进一步优化计算密集型部分。

掌握 groupby().transform() 是 Pandas 高级数据处理的关键技能之一,它在许多需要对分组数据进行计算并广播结果的场景中都非常有用。

以上就是利用 Pandas 高效分配数据框中的分组值:一种可扩展的解决方案的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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