马儿慢些走

人生惶惶,悲喜一场。

Python reduce 改为 chain ,性能优化

本文为和AI大模型KIMI的对话记录,仅供参考。

起因为执行Python代码时reduce才做耗费太多时间和内存,改为chain操作后加速效果明显,本质上属于列表该为迭代器。

Python reduce的性能问题

User: labels_all = reduce(lambda x, y: x + y, df['labels'].to_list(), [])
label_samples_cnt = Counter(labels_all)

这两行Python代码中如果数据量太大会造成内存占用和时间消耗吧?怎么改进?

Kimi: 是的,这两行代码在数据量很大时确实可能成为性能瓶颈,原因如下:


🔍 问题分析

labels_all = reduce(lambda x, y: x + y, df['labels'].to_list(), [])
  • 内存消耗大df['labels'].to_list() 会把整个列加载成一个 Python 列表,占用大量内存。
  • 时间消耗大reduce(lambda x, y: x + y, ...) 会不断创建新的列表,导致 O(n²) 的时间复杂度(每次拼接都复制前一次的结果)。

✅ 改进方案

✅ 方法 1:使用 itertools.chain(推荐)

from itertools import chain
from collections import Counter

label_samples_cnt = Counter(chain.from_iterable(df['labels']))
  • 优点
    • 不需要一次性把所有列表加载到内存。
    • 使用生成器,内存占用低
    • 时间复杂度接近 O(n)

✅ 方法 2:使用 explode(Pandas 原生)

label_samples_cnt = df['labels'].explode().value_counts()
  • 优点
    • 简洁,Pandas 原生支持。
    • 性能也不错,适合中等规模数据。
  • 注意
    • explode 会生成一个中间 Series,内存占用比 chain,但比 reduce 好太多。

✅ 总结建议

方法 内存效率 时间效率 推荐场景
chain ⭐⭐⭐⭐ ⭐⭐⭐⭐ 大数据量,最优选
explode ⭐⭐ ⭐⭐⭐ 中小数据量,简洁

✅ 最终推荐代码(大数据量)

from itertools import chain
from collections import Counter

label_samples_cnt = Counter(chain.from_iterable(df['labels']))

如数据量极大(千万级以上),甚至可以考虑 分批处理Dask 等分布式方案。需要我帮你写分批处理的版本吗?


如果数据量更大怎么办?

User: 如果数据量还是非常大怎么办?

Kimi: 如果数据量已经大到单机内存装不下(例如几十 GB 甚至 TB 级),上面“内存友好”的 itertools.chain 也扛不住,因为整列仍然要先读进内存。此时必须升级为外存/分布式方案。下面给出三条可行路线,按“从易到难”排序,你可以根据公司基础设施和自己时间成本选择。


  1. 单机外存:逐行流式 + 磁盘级 Counter
    (无需集群,10~100 GB 级最划算)

思路:

  • 不一次性把列读成 list,而是用 pandas 的 chunk 迭代器 一条一条刷过去;
  • collections.Counter原地加法 增量聚合;
  • 只保留最终计数表在内存,数据本身不落内存。

代码模板(可直接跑)

from collections import Counter
import pandas as pd

label_cnt = Counter()
chunksize = 500_000          # 按行数调,保证每块内存 < 1 GB 即可
for chunk in pd.read_csv('big_file.csv',      # 或 read_parquet
                         usecols=['labels'],  # 只读需要的列
                         chunksize=chunksize,
                         converters={'labels': pd.eval}):  # 如果存的是字符串 list
    for row in chunk['labels']:
        label_cnt.update(row)

print(label_cnt)
  • 内存峰值 ≈ 一块 chunk + Counter 大小,远低于总行数
  • 纯 Python,无额外依赖,最快 30 min 可上线

  1. 单机并行:Dask DataFrame(百 GB~TB 级,零集群)

思路:

  • 把大文件切成很多块,Dask 自动多线程/多进程并行;
  • 每块内部 explode → value_counts(),再全局聚合;
  • 代码几乎和 pandas 一样,但省内存、自动并行。

安装

pip install "dask[complete]"

代码

import dask.dataframe as dd

df = dd.read_parquet('big_file.parquet', columns=['labels'])  # or read_csv
out = df.labels.explode().value_counts().compute()            # 返回 pandas Series
print(out)
  • 无需自己写 chunk 循环;
  • 如果机器有 16+ 核、NVMe,1 TB 数据 10~30 min 跑完
  • 失败节点自动重试,比手写多进程稳。

  1. 分布式:PySpark / Spark SQL(TB 级以上,公司有 Yarn/K8s)

思路:

  • 数据已落在 HDFS/S3,直接 Spark SQL 一行搞定;
  • Spark 自动在成百上千台机器上做 explode + count;
  • 内存/磁盘溢写全自动,理论上无上限

PySpark 代码

from pyspark.sql import SparkSession
from pyspark.sql.functions import explode, col

spark = SparkSession.builder.appName("label_cnt").getOrCreate()
df = spark.read.parquet("hdfs:///path/big_file.parquet")
out = (df
       .withColumn("label", explode(col("labels")))
       .groupBy("label")
       .count()
       .orderBy("count", ascending=False)
       .collect())   # 或者 .write.parquet(...) 落盘
for row in out:
    print(row.label, row['count'])
  • 只要集群够大,PB 级也能跑
  • 缺点:需要 Spark 环境,调试周期比 Dask 长。

怎么选?

数据规模 硬件环境 推荐方案 预估耗时
1~50 GB 单机 16 GB 内存 方案 1(chunk 流式) 5~20 min
50 GB~1 TB 单机 32~128 GB 核多 方案 2(Dask) 10~30 min
1 TB 以上 公司 Spark 集群 方案 3(PySpark) 横向扩展

小结一句话
“数据再大,只要不把整列一次性塞进内存,就能跑。”
先上方案 1(零依赖),如果单机 CPU 吃满还不够,再平滑迁到 Dask 或 Spark。需要我帮你把方案 1 改成多进程加速版吗?

posted on 2025-12-09 17:37  马儿慢些走  阅读(1)  评论(0)    收藏  举报

导航