8-2 query与内存优化:出院前的最后核对 本节摘要:query用字符串表达式做多条件筛选,省掉一串中间布尔列;eval在大表上省临时对象;category、downcast与读取期dtype规划能把内存压掉一半以上;放不下的大文件用chunksize分块。本节以一张出院核对清单收官全册。 康复训练(8-1)解决了速度,本节解决内存与表达力。这也是整条数据体检流水线的最后一站。 query:把条件写成一句话 query省的不只是括号:传统写法每个比较都生成一个与原表等长的布尔Series,五个条件五份临时内存;query用numexpr一次编译执行,大表多条件时内存峰值显著更低。
本节摘要:query用字符串表达式做多条件筛选,省掉一串中间布尔列;eval在大表上省临时对象;category、downcast与读取期dtype规划能把内存压掉一半以上;放不下的大文件用chunksize分块。本节以一张出院核对清单收官全册。
康复训练(8-1)解决了速度,本节解决内存与表达力。这也是整条数据体检流水线的最后一站。
import pandas as pd import numpy as np n = 500_000 df = pd.DataFrame({ '门店': np.random.choice(['A', 'B', 'C'], n), '渠道': np.random.choice(['线上', '线下'], n), '金额': np.random.randint(50, 20000, n), }) # 传统写法:三串布尔中间列 + and改& + 括号纪律(2-2的记忆) mask = (df['金额'] > 1000) & (df['门店'] == 'A') & (df['渠道'] == '线上') # query写法:一句话,and/or直接用,括号免了 result = df.query('金额 > 1000 and 门店 == "A" and 渠道 == "线上"') print(len(result)) # 83271(随机种子不同数字不同) # 局部变量用@引入 threshold = 5000 print(df.query('金额 > @threshold').shape) # (121947, 3) # in写法直接支持 print(df.query('门店 in ["A", "B"]').shape) # (333545, 3)
query省的不只是括号:传统写法每个比较都生成一个与原表等长的布尔Series,五个条件五份临时内存;query用numexpr一次编译执行,大表多条件时内存峰值显著更低。代价是字符串里的列名失去IDE补全与静态检查,写错列名要到运行时才炸——小表和探索阶段用传统写法更顺手,生产管线的大表筛选用query。
# 多列联合计算的中间对象优化 df['净额'] = df.eval('金额 * 0.93 - 10') # 等价传统写法要生成两份中间Series # df['净额'] = df['金额'] * 0.93 - 10 # 复杂组合时eval的优势更明显 df['评分'] = df.eval('(金额 / 1000) + (渠道 == "线上") * 2') print(df[['金额', '净额', '评分']].head(3)) # 金额 净额 评分 # 0 9051 8407.43 11.051 # 1 1792 1654.56 3.792 # 2 1399 1291.07 3.399
print(f'原始占用:{df.memory_usage(deep=True).sum() / 1e6:.1f} MB') # 原始占用:95.3 MB # 斧一:低基数列转category(4-1的知识兑现) df['门店'] = df['门店'].astype('category') df['渠道'] = df['渠道'].astype('category') # 斧二:数值downcast,用更窄的血型装下数据 df['金额'] = pd.to_numeric(df['金额'], downcast='integer') # 斧三:读取期就规划好(1-3的知识兑现) # df = pd.read_csv('big.csv', dtype={'门店': 'category', '金额': 'int32'}, # usecols=['门店', '渠道', '金额']) print(f'瘦身后:{df.memory_usage(deep=True).sum() / 1e6:.1f} MB') # 瘦身后:18.7 MB 压到约两成
usecols值得单独一提:五十列的宽表只用三列时,读取期裁列比读进来再切片省掉的不只是内存,还有整个读取时间。
# chunksize把大文件切成生成器,逐块处理再汇总 total = 0 count = 0 for chunk in pd.read_csv('huge.csv', chunksize=100_000, usecols=['金额']): total += chunk['金额'].sum() # 每块只留聚合结果,不留明细 count += len(chunk) print(f'总金额 {total},共 {count} 笔,均单 {total / count:.1f}') # 总金额 4998231040,共 5000000 笔,均单 999.6
分块的心法:每块只带走聚合结果,绝不在循环里攒明细DataFrame(6-2的教训)。累计明细用列表攒、末次concat;能一遍出结果的用分块聚合,两遍出结果的(如需要全局均值的标准化)先跑一遍统计再跑一遍应用。
def final_check(df): """全册知识的收口自检:结构、质量、性能三关""" report = { '行数': len(df), '缺失格数': int(df.isna().sum().sum()), # 第03章 '整行重复': int(df.duplicated().sum()), # 第03章 'object列': df.select_dtypes('object').columns.tolist(), # 第04章 '内存MB': round(df.memory_usage(deep=True).sum() / 1e6, 1), # 第08章 } return report print(final_check(df)) # {'行数': 500000, '缺失格数': 0, '整行重复': 0, # 'object列': [], '内存MB': 18.7}

三十二章到此收官。把流水线内化成习惯,下一份脏数据进门时,你就是体检中心本身。