今天开始梳理一下pandas的大数据处理,在数据处理领域,Pandas 凭借简洁的 API 和强大的功能成为 Python 开发者的首选工具。但当面对 GB 级甚至更大的数据集时,直接读取数据往往会触发 “内存不足” 的错误 —— 这是因为 Pandas 默认将数据全部加载到内存中进行处理。此时,分块处理(Out-of-Core) 技术就成为解决问题的关键。它通过将大文件拆分为小块,逐块加载并处理,最终整合结果,实现 “用有限内存处理无限数据” 的效果。
分块处理的本质是 “化整为零”:将原本需要一次性加载的大文件,按固定大小拆分为多个独立的子数据集(Chunk),每次只加载一个子数据集到内存中。由于单个子数据集的体积远小于内存容量,因此不会出现内存溢出问题。处理完成后,再通过聚合逻辑将各子块的中间结果合并,得到与全量数据处理一致的最终结果。
这种思路特别适合两类场景:
Pandas 的read_csv(以及read_excel等文件读取函数)提供了chunksize参数,用于指定每次读取的行数。该参数会让函数返回一个迭代器(TextFileReader),而非直接返回 DataFrame—— 通过迭代这个对象,就能逐块获取数据。
假设我们有一个 10GB 的销售数据文件sales_data.csv,包含 “日期”“地区”“销售额” 等字段。直接使用pd.read_csv('sales_data.csv')会因内存不足失败,而分块读取可以轻松解决:
import pandas as pd
# 定义分块大小(每次读取10万行)
chunksize = 100000
# 创建分块迭代器
chunk_iterator = pd.read_csv(
'sales_data.csv',
chunksize=chunksize,
parse_dates=['日期'] # 按需求指定列解析格式
)
# 迭代读取并处理分块
for i, chunk in enumerate(chunk_iterator):
print(f"处理第{i+1}个分块,数据量:{len(chunk)}行")
# 此处可添加清洗、过滤等临时处理逻辑
# 例如:删除空值行
chunk = chunk.dropna(subset=['销售额'])
在上述代码中,chunk_iterator会按需加载数据:每次迭代时才读取下一个 10 万行数据,前一个分块处理完成后会被内存自动回收。通过调整chunksize的大小(如 20 万行),可以在 “内存占用” 和 “处理效率” 之间找到平衡 —— 分块过大会增加内存压力,过小则会因频繁 IO 降低速度。
分块处理的核心挑战是如何在不加载全量数据的情况下,得到与全量聚合一致的结果。以 “按地区统计总销售额” 为例,全量处理的逻辑是df.groupby('地区')['销售额'].sum(),而分块处理需要先计算每个分块的地区销售额,再对所有分块的结果二次聚合。
import pandas as pd
# 初始化中间结果容器(存储各地区的销售额累计值)
region_sales = {}
# 分块读取数据(每次50万行)
chunksize = 500000
chunk_iterator = pd.read_csv(
'sales_data.csv',
chunksize=chunksize,
usecols=['地区', '销售额'] # 只读取需要的列,减少内存占用
)
# 逐块处理并聚合
for chunk in chunk_iterator:
# 过滤无效数据(如销售额为负数的异常值)
valid_chunk = chunk[chunk['销售额'] > 0]
# 计算当前分块的地区销售额
chunk_agg = valid_chunk.groupby('地区')['销售额'].sum().to_dict()
# 合并到中间结果容器
for region, sales in chunk_agg.items():
if region in region_sales:
region_sales[region] += sales
else:
region_sales[region] = sales
# 转换为DataFrame并排序
final_result = pd.DataFrame(
list(region_sales.items()),
columns=['地区', '总销售额']
).sort_values('总销售额', ascending=False)
print(final_result)
如果需要保留更多中间信息(如分块的聚合结果明细),可以将每个分块的聚合结果存储为 DataFrame,最后用pd.concat合并后二次聚合。这种方式更直观,且支持复杂的聚合逻辑(如同时计算总和与平均值):
# 存储各分块的聚合结果
chunk_results = []
for chunk in chunk_iterator:
valid_chunk = chunk[chunk['销售额'] > 0]
# 计算分块内的地区销售额与订单数
chunk_agg = valid_chunk.groupby('地区').agg(
分块销售额=('销售额', 'sum'),
分块订单数=('订单ID', 'count')
).reset_index()
chunk_results.append(chunk_agg)
# 合并所有分块结果并二次聚合
final_result = pd.concat(chunk_results, ignore_index=True).groupby('地区').agg(
总销售额=('分块销售额', 'sum'),
总订单数=('分块订单数', 'sum')
).reset_index()
这种方法的优势在于:中间结果保留了每个分块的计算过程,便于排查问题;同时支持多指标聚合,无需手动维护字典容器。
# 显式指定列类型,避免分块类型不一致
dtype_spec = {
'地区': 'category', # 分类列用category类型节省内存
'销售额': 'float64',
'订单ID': 'string'
}
chunk_iterator = pd.read_csv(
'sales_data.csv',
chunksize=500000,
dtype=dtype_spec
)
分块处理通过 “化整为零” 解决了内存不足的问题,但默认的 “逐块读取→处理→保存中间结果” 是串行执行的 —— 即只有前一个分块处理完成后,下一个分块才会开始加载。这就像用一个水龙头慢慢接水,虽然不会溢出,但效率有限。
如果我们把分块比作 “把大面团切成小面团”,那么并行化就是 “让多个厨师同时揉小面团”。通过将独立的分块分配给不同的 CPU 核心并行处理,能充分利用多核硬件资源,将总处理时间压缩至接近 “单块处理时间 ×(1 / 核心数)”。
并非所有分块处理都能并行化,核心要求是:各分块的处理逻辑互不依赖。例如:
幸运的是,大部分大数据处理场景(如过滤、分组聚合、简单特征工程)都满足 “分块独立” 条件,这为并行化提供了基础。
实现分块并行化的工具很多,我们重点介绍两类最常用的方案:multiprocessing(Python 原生库,适合简单场景) 和 Dask(专业大数据并行库,兼容 Pandas 语法)。
Python 的multiprocessing库提供了进程池(Pool),可将分块处理函数 “映射” 到多个进程中并行执行。核心逻辑是:
假设我们有 10GB 销售数据,需按 “地区” 统计总销售额,用 4 核 CPU 并行处理:
import pandas as pd
from multiprocessing import Pool, cpu_count
# 1. 定义单个分块的处理函数(核心逻辑)
def process_chunk(chunk):
# 分块内的清洗与聚合
valid_chunk = chunk[chunk['销售额'] > 0] # 过滤异常值
return valid_chunk.groupby('地区')['销售额'].sum().reset_index()
# 2. 生成分块迭代器(提前确定分块数量,便于分配任务)
def get_chunk_iterator(file_path, chunksize):
return pd.read_csv(
file_path,
chunksize=chunksize,
usecols=['地区', '销售额'], # 只加载需要的列
dtype={'地区': 'category', '销售额': 'float64'} # 固定类型,避免进程间类型不一致
)
# 3. 并行处理主逻辑
if __name__ == '__main__':
file_path = 'sales_data.csv'
chunksize = 500000 # 单个分块大小(根据内存调整)
chunk_iterator = get_chunk_iterator(file_path, chunksize)
# 进程池数量设为CPU核心数(避免资源浪费)
core_num = cpu_count() # 自动获取当前设备核心数(如4核)
print(f"使用{core_num}个核心并行处理")
# 用进程池并行处理所有分块
with Pool(core_num) as pool:
# 将分块迭代器转换为列表(避免进程间迭代器共享问题)
chunks = list(chunk_iterator)
# 并行执行process_chunk函数,得到所有分块的中间结果
chunk_results = pool.map(process_chunk, chunks)
# 4. 合并并行结果(与串行分块的合并逻辑一致)
final_result = pd.concat(chunk_results, ignore_index=True).groupby('地区')['销售额'].sum().reset_index()
print(final_result)
优势与适用场景:
对于超大规模数据(如 100GB+),multiprocessing的手动管理会变得繁琐(如分块数量控制、进程通信开销)。此时更推荐Dask—— 一个专为大数据并行设计的库,其dask.dataframe模块完全兼容 Pandas 语法,却能自动实现分块与并行。
Dask 的核心逻辑是:
实战示例:Dask 并行计算销售额与订单数
用 Dask 处理与前文相同的 10GB 数据,代码几乎与 Pandas 一致:
import dask.dataframe as dd
# 1. 用Dask读取大文件(自动分块,不加载全量数据)
dask_df = dd.read_csv(
'sales_data.csv',
usecols=['地区', '销售额', '订单ID'],
dtype={'地区': 'category', '销售额': 'float64', '订单ID': 'string'} # 显式指定类型
)
# 2. 定义处理逻辑(与Pandas语法完全一致)
result = dask_df[
dask_df['销售额'] > 0 # 过滤异常值
].groupby('地区').agg(
总销售额=('销售额', 'sum'),
总订单数=('订单ID', 'count')
).reset_index()
# 3. 执行计算并获取结果(触发并行执行)
final_result = result.compute() # compute()时才真正执行,自动并行
print(final_result)
优势与适用场景:
并行时多个分块会同时加载到内存(如 4 核并行会同时加载 4 个分块),因此分块大小需调整为 “单个分块内存 × 核心数 ≤ 可用内存”。例如:
并行加速的效果受 “最慢环节” 限制:
并行时各进程 / 核心独立处理分块,若分块类型推断不一致(如某分块 “地区” 列被误判为float),合并时会报错。因此必须通过dtype显式指定列类型(无论用multiprocessing还是 Dask)。
分块过小会导致 “进程通信成本” 超过并行收益(如 1000 个 1KB 的分块,进程间传递数据的时间可能比处理时间还长)。建议分块大小不小于 100MB(可通过chunksize调整,如每行 1KB 时,chunksize=100000)。
假设处理 10GB 销售数据(含 1 亿行),单核心串行分块处理需 60 分钟(每个分块处理 5 分钟,共 12 个分块):
核心越多,加速效果越明显(前提是内存足够)。
分块处理(Out-of-Core)是 Pandas 处理大数据的基础技术,通过chunksize参数实现数据拆分,结合迭代器和聚合逻辑,能够在有限内存中处理超大规模数据集。其核心价值在于:无需升级硬件,仅通过优化数据加载方式,就能突破内存限制。无论是简单的统计分析,还是复杂的特征工程,分块处理都能成为可靠的 “内存管理工具”。掌握这一技术后,面对 GB 级甚至 TB 级数据时,你将不再因 “内存不足” 而束手无策。