1.11 性能优化与内存管理
前几章完成了 Pandas 的功能性学习,但数据量变大后会出现两个问题:内存占用过高、运算速度变慢。Pandas 提供了多种优化手段,包括评估内存占用、向下转型、使用分类类型、表达式求值加速、分块读取。本节介绍这些技巧,帮助数据大到内存吃紧时保持程序可用。
1.11.1 内存使用评估
.memory_usage()
.memory_usage() 返回每列占用的字节数,deep=True 计入 object 列内部对象的开销:
import pandas as pd
df = pd.DataFrame({'A': range(10000), 'B': ['x'] * 10000})
print(df.memory_usage())
print(df.memory_usage(deep=True))deep=True 对字符串列影响很大,浅层统计会严重低估。
.info(memory_usage='deep')
.info() 加 memory_usage='deep' 输出包含深度的总内存统计:
df.info(memory_usage='deep')1.11.2 数据类型向下转型
数据的实际取值范围往往比默认类型小,向下转型可以显著节省内存。整数默认 int64,如果值都在 0-255 之间可以转成 uint8:
df = pd.DataFrame({'年龄': [25, 30, 28, 31]})
df['年龄'] = df['年龄'].astype('int8')
print(df['年龄'].dtype) # int8浮点默认 float64,精度要求不高时可转 float32:
df['身高'] = df['身高'].astype('float32')pd.to_numeric 配合 downcast 参数自动选择最小可容纳的类型:
import numpy as np
s = pd.Series([1, 2, 3])
print(pd.to_numeric(s, downcast='integer').dtype) # int81.11.3 使用 category 类型
对取值重复度高的分类列,转成 category 可以大幅节省内存。category 只存储类别列表与整数编码,不重复存储每个字符串:
df = pd.DataFrame({'城市': ['北京'] * 5000 + ['上海'] * 5000})
print(df.memory_usage(deep=True)['城市'])
df['城市'] = df['城市'].astype('category')
print(df.memory_usage(deep=True)['城市'])类别数远小于行数时收益明显,类别接近行数时收益很小甚至变差。
1.11.4 pd.eval 与 pd.query 表达式求值
pd.eval() 用 numexpr 引擎求值表达式,避免创建中间 DataFrame,加速复合算术:
df = pd.DataFrame({'A': range(1000000), 'B': range(1000000)})
result = pd.eval('A + B + A * B')pd.query() 用同样的引擎加速条件筛选,前面已介绍。numexpr 引擎对大型浮点/整数运算的加速明显,需安装 numexpr 库:
pip install numexprpd.eval 在链式运算中能减少内存峰值,因为不会为每个中间步骤生成完整副本。
1.11.5 分块读取大文件
pd.read_csv(..., chunksize=...) 按块读取大型 CSV,每块是一个 DataFrame,配合迭代器逐块处理:
chunks = pd.read_csv('big.csv', chunksize=10000)
total = 0
for chunk in chunks:
total += chunk['值'].sum()
print(total)chunksize 指定每块行数,返回的 TextFileReader 对象可迭代。对无法整块装入内存的大文件,这是标准处理方式。
1.11.6 inplace 参数与副本
inplace=True 直接在原对象上修改,避免创建副本。但多数操作返回新对象更安全,Pandas 文档建议谨慎使用 inplace:
df = pd.DataFrame({'A': [1, 2, 3]})
df.drop('A', axis=1, inplace=True) # 原地删除,不再返回新对象Pandas 3.0 起 inplace 参数将被移除,建议用返回值链式赋值。
1.11.7 copy_on_write 模式
Pandas 2.0 引入 Copy-on-Write(写时复制)模式,pd.set_option('mode.copy_on_write', True) 启用。它延迟数据复制,只有在真正写入时才复制,避免修改视图时意外改动原数据,同时减少不必要的复制开销:
pd.set_option('mode.copy_on_write', True)
df = pd.DataFrame({'A': [1, 2, 3]})
sub = df['A']
sub[0] = 99
print(df) # A 列不受影响,视图修改不会回写到原对象启用后 .loc 等操作返回的结果修改不再影响原 DataFrame,也消除了大量链式赋值的告警。
1.11.8 索引优化
有序索引能加速选择和切片。sort_index() 排序后,.loc 的范围选择可以使用二分查找而非线性扫描:
df = pd.DataFrame({'值': range(1000000)}).sort_index()
print(df.loc[100:200]) # 有序索引下更快pd.RangeIndex 是最高效的索引,能显式表达时优先用整数范围索引。
练习题
第1题 概念理解
说明 category 类型节省内存的原理;说明 copy_on_write 解决的问题。
参考答案
category 只存储类别表与整数编码,不重复存储字符串本身,重复度高的列收益大。copy_on_write 延迟复制,修改视图不再回写原对象,避免意外改数据和多余复制。
第2题 代码编写
生成 10 万行的 DataFrame,比较转换 category 前后某列的内存占用;对数值列做向下转型并查看 dtype;用 .info(memory_usage='deep') 输出总内存。
参考答案
import pandas as pd
df = pd.DataFrame({'类别': ['A'] * 50000 + ['B'] * 50000,
'值': range(100000)})
print(df.memory_usage(deep=True)['类别'])
df['类别'] = df['类别'].astype('category')
print(df.memory_usage(deep=True)['类别'])
df['值'] = pd.to_numeric(df['值'], downcast='integer')
print(df['值'].dtype)
df.info(memory_usage='deep')第3题 进阶练习
用 pd.eval 计算一个大 DataFrame 的复合表达式并对比时间;用 chunksize 分块读取并汇总;开启 copy_on_write 后验证视图修改不再影响原对象。
参考答案
import pandas as pd
df = pd.DataFrame({'A': range(1000000), 'B': range(1000000)})
result = pd.eval('A + B')
with open('tmp.csv', 'w') as f:
f.write('值\n1\n2\n3\n4\n5\n')
total = 0
for chunk in pd.read_csv('tmp.csv', chunksize=2):
total += chunk['值'].sum()
print(total)
pd.set_option('mode.copy_on_write', True)
d = pd.DataFrame({'A': [1, 2, 3]})
sub = d['A']
sub[0] = 99
print(d) # A 列仍为 [1, 2, 3]常见错误
错误 1 · memory_usage() 统计的内存远小于任务管理器显示
原因:默认 deep=False,object 列内部对象的开销未被计入。
解决:用 deep=True。
错误 2 · 转 category 后内存反而变大
原因:类别数与行数接近时,类别表加编码的开销超过直接存储。
解决:只在类别数远小于行数时使用 category。
错误 3 · 分块读取时报内存错误
原因:chunksize 没有真正分块处理,或每块内又做了整块复制。
解决:确认在循环里逐块消费数据,避免把全部块累计成一个大对象。
错误 4 · 视图修改意外改变原 DataFrame
原因:未启用 copy_on_write 时,部分选择结果与原对象共享数据。
解决:启用 mode.copy_on_write,或修改前显式 .copy()。