你的 Python 代码可能慢得离谱
上周帮一个朋友看代码。他处理一个 2GB 的 CSV 文件,跑了 40 分钟还没出结果。
我扫了一眼,三个问题:循环读取、重复创建 DataFrame、没利用向量化。
改完以后,3 分钟搞定。
不是他的逻辑有问题,是方法不对。下面这些技巧,能帮你避开大部分常见坑。
技巧 1:别用 read_csv 读大文件
很多人写数据分析代码,第一行就是:
df = pd.read_csv('huge_file.csv')
文件超过 1GB 的时候,这行代码会吃掉你大量内存。
换成分块读取:
chunks = []
for chunk in pd.read_csv('huge_file.csv', chunksize=100000):
filtered = chunk[chunk['status'] == 'active']
chunks.append(filtered)
df = pd.concat(chunks, ignore_index=True)
这样每次只加载 10 万行,内存压力小很多。
关键点: 在分块阶段就做好过滤和聚合,别把所有数据都攒到最后。
技巧 2:用 .loc 替代逐行循环
这是最常见的性能杀手。
# 错误示范
for index, row in df.iterrows():
if row['age'] > 30:
row['category'] = 'senior'
这种写法在处理 10 万行数据时,可能要跑好几分钟。
换成向量化操作:
df.loc[df['age'] > 30, 'category'] = 'senior'
一行搞定,速度快几十倍。
如果逻辑太复杂,向量化写不出来,至少用 apply():
df['category'] = df.apply(lambda x: 'senior' if x['age'] > 30 else 'junior', axis=1)
虽然不如纯向量化快,但比 iterrows 好得多。
技巧 3:数据类型选对,内存省一半
Pandas 默认会把数字列读成 float64,字符串列读成 object。很多时候没必要。
# 默认读取
df = pd.read_csv('data.csv')
print(df.dtypes)
# age: float64
# price: float64
# category: object
手动指定类型:
df = pd.read_csv('data.csv', dtype={
'age': 'int8',
'price': 'float32',
'category': 'category'
})
category 类型特别适合身份证号、地区编码这类重复值多的字段。内存占用能降 80% 以上。
技巧 4:用 DuckDB 替代 Pandas 做查询
如果你的数据量已经大到 Pandas 处理吃力,试试 DuckDB。
它长得像 SQL,跑起来像数据库,还能直接读 CSV/Parquet。
import duckdb
result = duckdb.query("""
SELECT city, AVG(price) as avg_price
FROM 'sales.csv'
WHERE date >= '2026-01-01'
GROUP BY city
ORDER BY avg_price DESC
""").df()
DuckDB 的优势是:
- 支持 SQL 语法,不用学新 API
- 自动并行处理
- 能处理比内存更大的数据集
- 和 Pandas 无缝切换
对于数据清洗和聚合场景,DuckDB 经常比 Pandas 快 5-10 倍。
技巧 5:保存为 Parquet,别再用 CSV
CSV 是文本格式,读取时要解析每一行。Parquet 是列式存储,体积小、速度快。
# 保存
df.to_parquet('data.parquet', engine='pyarrow')
# 读取
df = pd.read_parquet('data.parquet')
实测对比(同一个 500MB 文件):
- CSV 写入:约 12 秒
- Parquet 写入:约 4 秒
- CSV 读取:约 8 秒
- Parquet 读取:约 2 秒
而且 Parquet 会保留数据类型,不会出现 CSV 常见的数字变字符串的问题。
建议: 中间结果一律存 Parquet,只有最终导出才转 CSV。
技巧 6:用 merge 替代多次 join
很多人习惯链式拼接:
df1.merge(df2).merge(df3).merge(df4)
数据量大时,每 merge 一次都要重新构建索引。
更好的做法是一次性合并:
tables = {'df2': df2, 'df3': df3, 'df4': df4}
result = df1
for key, table in tables.items():
result = result.merge(table, on='id', how='left')
虽然还是循环,但至少避免了不必要的中间对象创建。
如果表结构相似,也可以用 pd.concat 做纵向合并,速度更快。
技巧 7:处理缺失值的正确姿势
缺失值处理是数据清洗的重灾区。
常见错误:
# 直接删掉所有含缺失值的行
df.dropna()
这会导致大量有效数据丢失。
正确做法是先分析缺失模式:
missing_ratio = df.isnull().sum() / len(df)
print(missing_ratio[missing_ratio > 0].sort_values(ascending=False))
df['age'].fillna(df['age'].median(), inplace=True)
df['city'].fillna('未知', inplace=True)
原则: 数值型用中位数填充,分类用众数填充,时间序列用前向填充。别一股脑全删。
技巧 8:善用 profiling 找瓶颈
最后这个技巧最重要——别猜哪里慢,测一下。
import cProfile
cProfile.run('your_function()', sort='cumulative')
或者用更直观的 line profiler:
%load_ext line_profiler
%lprun -f your_function your_function()
这样你能看到每一行代码的实际耗时。
大部分时候你会发现:真正拖慢速度的是你以为"应该很快"的那几行。
总结一张表
| 技巧 | 适用场景 | 预期提升 |
|---|---|---|
| 分块读取 | 大文件处理 | 内存降低 70%+ |
| 向量化操作 | 条件判断、列计算 | 速度提升 10-50 倍 |
| 优化数据类型 | 内存敏感场景 | 内存降低 30-80% |
| DuckDB 查询 | 复杂聚合分析 | 速度提升 5-10 倍 |
| Parquet 存储 | 频繁读写中间数据 | I/O 降低 60%+ |
| 批量 merge | 多表关联 | 减少中间对象 |
| 智能填补缺失值 | 数据清洗 | 保留更多有效数据 |
| Profiling 定位瓶颈 | 性能优化 | 精准找到慢点 |
最后说一句
数据处理这事儿,80% 的性能问题都来自同样的几个坏习惯。
别循环,别全量加载,别乱用默认类型。
先把这三点做到,你的代码就能快一大截。剩下的 20%,靠 profiling 慢慢调。
有具体场景拿不准怎么优化的?把数据量和需求发出来,帮你看看。
本文基于实际测试经验整理。代码示例基于 Python 3.11 + Pandas 2.x + DuckDB 1.x。