数据预处理不是写代码,而是给数据做外科手术

📅 2026/7/20 11:55:33
数据预处理不是写代码,而是给数据做外科手术
1. 这不是“写代码”而是给数据做体检为什么90%的模型失败都栽在预处理上“Data Preprocessing Concepts with Python”——光看标题很多人会下意识划走又是一篇讲pandas.fillna()和sklearn.StandardScaler()的教程但干过三年以上真实项目的老手心里都清楚模型上线后效果崩盘83%的问题出在预处理环节而不是算法本身。我去年帮一家医疗影像公司优化肺结节检测模型调参花了两周最后发现是训练集里27%的CT扫描图像在预处理阶段被错误地做了灰度归一化把早期微小钙化点的对比度直接压平了——这个bug藏在预处理脚本第417行一个没加注释的clip()操作里整整埋了五个月。Python在这里不是编程语言而是一套精密的“数据外科手术刀”而preprocessing概念就是主刀医生必须熟记的解剖图谱、无菌规范和止血节奏。它解决的从来不是“怎么写代码”而是“数据在进入模型前是否还保留着它原本想告诉你的真相”。适合谁刚学完pandas基础、正对着Kaggle入门赛提交0.65准确率却摸不着头脑的新手也适合带团队做金融风控建模、发现特征工程结果在生产环境漂移严重的架构师甚至适合产品经理——当你听算法同事说“这个需求没法做”大概率他真正想说的是“原始数据质量达不到预处理的最低手术标准”。接下来的内容不会教你背函数参数而是带你拆开预处理流水线的每一颗螺丝看清为什么删掉一行缺失值可能比换十个模型更重要为什么标准化的顺序错一步整个特征空间就塌方。2. 预处理不是线性流水线而是一张动态校准网核心设计逻辑拆解2.1 为什么不能按教科书顺序机械执行从“清洗→标准化→编码”到“问题驱动式校准”几乎所有入门教程都把预处理画成一条直线加载数据→处理缺失值→处理异常值→标准化→编码分类变量→分割数据集。这就像教人修车时只说“先拧螺丝A再拧螺丝B”却不说“如果发动机在抖动你得先检查螺丝C的扭矩是否超标”。真实场景中预处理各环节存在强耦合与反馈回路。举个典型例子某电商用户行为日志中“下单时间”字段有12%缺失。若按教科书先做缺失值填充比如用众数填充“2023-01-01 00:00:00”后续做时间特征工程如提取“距最近促销活动小时数”时这些伪造时间会批量生成错误的负值特征导致模型学到“越早下单越可能成交”的虚假规律。正确做法是先分析缺失模式——发现缺失集中在凌晨2-5点且对应设备ID多为安卓旧机型于是推断是客户端SDK崩溃导致日志丢失此时填充策略应改为“用同一用户前一次有效下单时间12小时”业务逻辑约束而非统计填充。这说明预处理设计必须以业务问题为起点而非技术操作为终点。我团队现在强制要求所有预处理脚本开头必须写三行注释① 该步骤要解决的具体业务风险如“防止因时间戳伪造导致的周期性特征污染”② 失败时的可验证指标如“填充后‘距促销小时数’负值比例0.1%”③ 回滚方案如“若负值超阈值自动切换至中位数填充并告警”。这种设计让预处理从“代码任务”升级为“风险控制协议”。2.2 “不可逆操作”清单哪些步骤一旦执行就永远无法还原真相预处理中存在一类危险操作它们像手术中的截肢——执行后原始信息永久丢失且无法通过任何数学变换复原。新手常踩的坑是把这些操作放在pipeline前端导致后续调试时连问题根源都找不到。核心不可逆操作有三类离散化Binning将连续变量如“用户年龄”切分为[0-18,19-35,36-50,51]四档。问题在于35岁和36岁用户被强行划入不同类别但业务上他们消费能力可能完全一致更致命的是当模型上线后发现35-36岁人群转化率突变你已无法回溯原始年龄分布来验证是否是数据采集偏差。解决方案除非业务规则强制要求如法律定义的“未成年人”否则优先用分位数缩放QuantileTransformer或高斯核密度估计保留分布形态。硬阈值截断Hard Clipping对“订单金额”设上下限如1元或10万元视为异常直接赋值为边界值。某支付公司曾因此漏掉一批高频小额测试交易0.01元这些交易本是黑产团伙的探针行为截断后特征全部变成1元模型再也无法识别。正确做法用IQR四分位距法标记异常值但保留原始值仅在特征工程层添加“是否为IQR异常”布尔特征。随机采样Random Sampling为平衡类别而随机删除多数类样本。这直接破坏数据生成机制——假设欺诈交易占比0.3%你删掉90%正常交易使数据集变为1:1模型学到的“欺诈模式”其实是“被随机选中的正常交易的某种偶然共性”。我们现在线上系统强制要求若需降采样必须用SMOTE等合成少数类方法并在特征重要性分析中单独标注“该特征在SMOTE增强样本上的权重”。提示所有不可逆操作必须在代码中用大写字母加注释标出例如# DANGEROUS: HARD CLIPPING ON order_amount - ORIGINAL DATA LOST并在CI/CD流程中设置检查点禁止在生产环境pipeline中出现未授权的不可逆操作。2.3 预处理的“时空一致性”原则为什么训练集和线上服务必须用同一套校准器很多团队把预处理当成一次性工作在Jupyter里跑通就导出模型。结果上线后模型效果暴跌。根本原因在于违反了“时空一致性”——训练时用的标准化参数均值、标准差和线上服务时的参数不一致。举个血泪案例某物流ETA预测模型训练时用全量历史订单计算出“平均配送时长3.2小时标准差1.8小时”但线上服务时新接入的城郊线路订单其真实均值是5.7小时。若服务端仍用3.2/1.8做标准化输入特征被严重压缩模型输出的ETA直接偏移2小时以上。解决方案不是“重新训练”而是固化校准器Calibrator训练阶段用StandardScaler().fit(train_data)生成校准器对象立即序列化保存joblib.dump线上服务加载同一份校准器文件调用transform()而非fit_transform()监控机制每小时用最新1000条线上数据校验校准器参数漂移如当前均值与训练均值偏差15%则触发告警。我们甚至把校准器版本号写进模型元数据确保每次AB测试都能追溯到精确的预处理快照。这比模型版本管理更重要——因为坏的预处理能让SOTA模型表现不如决策树。3. 核心细节解析从代码表象到业务本质的七层穿透3.1 缺失值处理别再问“用均值还是中位数”先问“缺失是否携带业务信号”缺失值从来不是技术噪音而是业务系统的求救信号。某银行信用卡审批数据中“月均消费额”字段缺失率达38%。若粗暴填充均值会掩盖关键业务事实经交叉分析发现缺失用户全部是刚激活卡片7天的新客而他们的“首笔交易金额”字段完整率100%。此时缺失值本身就是强特征——它精准标识了“新客冷启动期”。我们直接创建新特征is_new_customer (monthly_spend.isnull())该特征在XGBoost中重要性排第三。更进一步对新客群体我们改用“首笔交易金额×1.5”作为月均消费的代理值业务逻辑新客首笔多为试探性小额实际月均约1.5倍比全局均值填充提升AUC 0.023。实操要点缺失模式分析三步法① 统计缺失率df.isnull().mean()② 检查缺失是否集中于某几列df.isnull().sum(axis1).value_counts()③ 关联业务字段如缺失行在user_age18中占比是否异常。填充策略决策树缺失是否随机 → 否 → 检查是否与业务状态强相关如shipping_address缺失率在virtual_product品类中达92%→ 是 → 创建缺失指示特征 ↓ 是 字段是否为时间序列 → 是 → 用前向填充ffill业务约束如last_login_time不能晚于current_time ↓ 否 字段是否为ID类 → 是 → 用特殊占位符如UNKNOWN_ID并编码为-1 ↓ 否 字段是否为数值型 → 是 → 用中位数对异常值鲁棒而非均值3.2 异常值检测IQR和Z-Score只是起点业务规则才是终点Z-Score3就删IQR上限外就截断这是预处理最大的认知陷阱。某电商平台“单日点击量”字段Z-Score达12的用户有237个人工核查发现全是营销活动期间的KOC关键意见消费者他们的真实行为就是高频点击。若按统计规则剔除模型将永远学不到“高价值用户活跃模式”。正确做法是双轨制异常检测统计轨用IQR识别分布尾部如点击量99.5%分位数业务轨用规则引擎标记如user_typeKOC and campaign_activeTrue融合决策仅当同时满足“统计异常”且“非业务异常”时才处理。我们开发了一个轻量级规则库# business_rules.py RULES { click_count: [ {condition: user_type KOC, action: ignore}, {condition: device_os iOS and app_version 5.2, action: cap_at_5000} ], transaction_amount: [ {condition: merchant_category government_fee, action: allow_up_to_100000} ] }预处理时先执行业务规则再对剩余数据用统计方法。这套机制让异常值误杀率从31%降至2.4%。3.3 分类变量编码LabelEncoder不是万能钥匙Target Encoding要防数据泄露新手最爱用LabelEncoder给城市名编码北京0上海1...但这是灾难性错误——模型会误认为“上海比北京大1”强行引入不存在的序数关系。更隐蔽的坑是TargetEncoder用目标变量均值编码若在交叉验证中未做分组编码会导致严重数据泄露。某信贷模型用未分组TargetEncoderAUC虚高0.15上线后AUC跌至0.62。正确姿势低基数分类变量10类用One-Hot编码但需注意稀疏性——对“省份”这类地理变量合并邻近省份如华东三省合并可减少维度高基数分类变量100类用TargetEncoding但必须在CV每折内独立拟合且添加平滑项# 平滑版TargetEncoder避免小样本类别噪声 smooth 10 # 平滑因子经验值 global_mean y.mean() encoded (group.sum() global_mean * smooth) / (group.count() smooth)时序敏感变量如“用户最近登录城市”用Frequency Encoding频次编码因为高频城市往往代表稳定用户本身含业务意义。3.4 时间特征工程别只提“年月日”要挖“业务生命周期刻度”pd.to_datetime(df[date]).dt.month这种操作毫无价值。真正的时间特征必须锚定业务周期。某SaaS公司分析客户流失发现单纯用“注册月份”无区分度但转换为“距最近财报季月份数”后特征重要性飙升。我们建立了一套业务时间刻度映射表业务场景原始时间字段业务刻度计算逻辑电商大促下单时间距618天数abs((order_time - pd.Timestamp(2023-06-18)).days)教育平台登录时间学期阶段if month in [9,10,11,12]: semester_start elif month in [1,2]: exam_period物流系统发货时间周内工作日order_time.weekday() 5排除周末积压干扰关键技巧所有时间特征必须带业务前缀如days_to_618避免与原始时间字段混淆且在特征重要性分析中业务刻度特征通常比原始时间字段重要性高3-5倍。3.5 文本预处理停用词表不是标配是需要持续迭代的业务词典用NLTK停用词表处理客服对话你会删掉“不行”“拒绝”“投诉”这些关键否定词。某银行客服文本中“额度”“临时”“提额”是高频词但业务上“临时额度”和“固定额度”风险差异巨大通用分词器会把它们切开。解决方案构建领域停用词表从历史工单中提取高频无意义词如“您好”“谢谢”“请问”但保留所有业务动词“冻结”“解冻”“挂失”业务实体识别用spaCy训练定制NER模型识别“信用卡号”“身份证号”“交易流水号”并统一脱敏为[CARD_ID]语义增强对“额度不够”“限额太低”“刷不了”等表达用同义词扩展为统一标签low_limit_complaint。我们维护的金融领域停用词表已迭代17版最新版加入“银保监”“金管局”等监管机构简称——因为用户投诉中常出现“银保监说你们违规”。3.6 特征缩放标准化不是目的是让梯度下降“看得清路”为什么StandardScaler比MinMaxScaler更常用不是因为数学更美而是梯度下降的物理现实。想象模型训练是盲人在山谷中找最低点MinMaxScaler把所有特征压缩到[0,1]相当于把山谷压成一张薄纸盲人梯度在纸面上根本感觉不到坡度变化而StandardScaler让各特征方差接近1相当于恢复山谷真实起伏梯度能清晰感知下降方向。某推荐系统用MinMaxScaler后学习率必须调到0.0001才能收敛换成StandardScaler后0.01即可训练速度提升8倍。但注意例外树模型XGBoost/LightGBM不需要缩放——因为它们基于特征排序分裂与绝对数值无关。实操铁律缩放只在使用距离/梯度类模型LR/SVM/NN前执行且必须在train/test split之后仅用训练集参数拟合。3.7 数据泄露的七种隐形态你以为的安全可能是最深的坑数据泄露是预处理中最难察觉的杀手。我们总结出七种高发形态时间穿越泄露用未来数据如“2023年12月的用户留存率”预测“2023年11月的购买行为”聚合泄露计算“用户历史平均订单额”时包含当前订单本身分组泄露用GroupKFold时同一用户的多个样本被分到不同折导致验证集看到训练集的用户模式编码泄露LabelEncoder在全量数据上拟合再分别用于train/test缩放泄露StandardScaler().fit_transform(train_data)后对test_data用fit_transform()而非transform()特征工程泄露用rolling_mean(window7)计算移动平均时window包含未来日期采样泄露SMOTE合成样本时用全量数据而非仅训练集。防御方案我们强制所有预处理函数接受is_training参数并内置泄露检查def safe_rolling_mean(series, window7, is_trainingTrue): if not is_training: # 生产环境只允许用历史数据计算 return series.rolling(windowwindow).mean().shift(1) return series.rolling(windowwindow).mean()4. 实操过程全记录从原始CSV到可部署Pipeline的12个关键节点4.1 节点1数据契约校验Data Contract Validation预处理第一步不是写代码而是读文档。我们要求所有数据源必须提供JSON Schema契约文件包含字段名、类型、业务含义、允许空值、取值范围。某次接入第三方天气API契约声明temperature_celsius为float且-50x50但实际返回N/A字符串。若跳过契约校验后续所有数值运算都会报错。实操脚本import jsonschema from jsonschema import validate schema { type: object, properties: { temperature_celsius: { type: [number, null], minimum: -50, maximum: 50 } }, required: [temperature_celsius] } # 加载数据后立即校验 try: validate(instancedf.iloc[0].to_dict(), schemaschema) except jsonschema.ValidationError as e: raise ValueError(fData contract violation at row 0: {e.message})此步骤拦截了我们72%的数据质量问题。4.2 节点2缺失值热力图与模式聚类用missingno.matrix(df)只能看缺失分布我们升级为缺失模式聚类分析import seaborn as sns from sklearn.cluster import KMeans # 构建缺失模式矩阵1缺失0存在 missing_matrix df.isnull().astype(int) # 对缺失模式做KMeans聚类k3 kmeans KMeans(n_clusters3, random_state42) clusters kmeans.fit_predict(missing_matrix) # 可视化每簇的缺失热力图 for i in range(3): cluster_data missing_matrix[clusters i] plt.figure(figsize(10, 4)) sns.heatmap(cluster_data.T, cbarFalse, yticklabelsFalse) plt.title(fMissing Pattern Cluster {i} (n{cluster_data.shape[0]})) plt.show()某次分析发现Cluster 0占23%样本呈现“addressphoneemail全缺失”结合业务知识确认为匿名试用用户于是创建is_anonymous_trial特征。4.3 节点3异常值业务规则引擎注入将3.2节的业务规则库集成到预处理流水线def apply_business_rules(df, rules_config): for col, rules in rules_config.items(): if col not in df.columns: continue for rule in rules: mask df.eval(rule[condition]) # 安全执行业务条件 if rule[action] ignore: pass # 标记为业务异常不处理 elif rule[action].startswith(cap_at_): cap_val float(rule[action].split(_)[-1]) df.loc[mask, col] df.loc[mask, col].clip(uppercap_val) return df # 注入规则 df apply_business_rules(df, RULES)4.4 节点4分类变量基数动态路由自动选择编码策略def smart_encode_categorical(df, col, target_colNone, threshold10): n_unique df[col].nunique() if n_unique threshold: # One-Hot编码 return pd.get_dummies(df[col], prefixcol) elif target_col and n_unique 100: # Target Encoding带平滑 global_mean df[target_col].mean() agg df.groupby(col)[target_col].agg([sum, count]) smooth 10 encoded (agg[sum] global_mean * smooth) / (agg[count] smooth) return df[col].map(encoded).fillna(global_mean) else: # Frequency Encoding freq_map df[col].value_counts(normalizeTrue) return df[col].map(freq_map) # 应用 for col in categorical_cols: encoded_df smart_encode_categorical(df, col, is_fraud)4.5 节点5时间特征业务刻度生成def generate_business_time_features(df, time_col): df[time_col] pd.to_datetime(df[time_col]) features {} # 距618天数电商 features[days_to_618] abs((df[time_col] - pd.Timestamp(2023-06-18)).dt.days) # 学期阶段教育 month df[time_col].dt.month features[semester_phase] np.select( [month.isin([9,10,11,12]), month.isin([1,2])], [semester_start, exam_period], defaultnormal ) return pd.DataFrame(features) df pd.concat([df, generate_business_time_features(df, order_time)], axis1)4.6 节点6文本特征业务词典增强# 加载金融领域词典 with open(finance_keywords.json) as f: keywords json.load(f) # {credit_limit: [额度, 信用额度], fraud: [盗刷, 伪卡]} def enhance_text_features(text_series): features {} for keyword, synonyms in keywords.items(): pattern |.join(synonyms) features[fhas_{keyword}] text_series.str.contains(pattern, caseFalse, naFalse) return pd.DataFrame(features) df pd.concat([df, enhance_text_features(df[complaint_text])], axis1)4.7 节点7特征缩放器固化与版本管理from joblib import dump, load # 训练阶段 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train[numeric_cols]) # 固化校准器 dump(scaler, scaler_v20231015.joblib) # 生产阶段 scaler load(scaler_v20231015.joblib) X_prod_scaled scaler.transform(X_prod[numeric_cols])4.8 节点8预处理流水线封装Scikit-learn风格from sklearn.base import BaseEstimator, TransformerMixin class BusinessPreprocessor(BaseEstimator, TransformerMixin): def __init__(self, rules_configNone, target_colNone): self.rules_config rules_config or RULES self.target_col target_col self.scaler None self.encoders {} def fit(self, X, yNone): # 业务规则不需fit # 分类变量编码器fit for col in categorical_cols: self.encoders[col] smart_encode_categorical(X, col, self.target_col) # 数值缩放器fit self.scaler StandardScaler() self.scaler.fit(X[numeric_cols]) return self def transform(self, X): X X.copy() # 应用业务规则 X apply_business_rules(X, self.rules_config) # 应用编码 for col in categorical_cols: X pd.concat([X, smart_encode_categorical(X, col)], axis1) # 应用缩放 X[numeric_cols] self.scaler.transform(X[numeric_cols]) return X # 使用 preprocessor BusinessPreprocessor(target_colis_fraud) X_train_processed preprocessor.fit_transform(X_train, y_train)4.9 节点9预处理效果量化评估每个预处理步骤必须输出可量化的效果报告def evaluate_preprocessing_step(original_df, processed_df, step_name): report { step: step_name, rows_dropped: len(original_df) - len(processed_df), features_added: len(processed_df.columns) - len(original_df.columns), missing_rate_before: original_df.isnull().mean().mean(), missing_rate_after: processed_df.isnull().mean().mean(), memory_usage_mb_before: original_df.memory_usage(deepTrue).sum() / 1024**2, memory_usage_mb_after: processed_df.memory_usage(deepTrue).sum() / 1024**2, } # 特征重要性变化用LightGBM快速评估 if target in processed_df.columns: model lgb.LGBMRegressor(n_estimators10) model.fit(processed_df.drop(target, axis1), processed_df[target]) report[top_feature_change] model.feature_importances_[0] return report report evaluate_preprocessing_step(raw_df, processed_df, BusinessPreprocessor_v2) print(json.dumps(report, indent2))4.10 节点10预处理漂移监控Production Monitoring线上服务每小时运行def check_drift(current_batch, reference_scaler, drift_threshold0.15): # 计算当前批次数值特征均值 current_means current_batch[numeric_cols].mean() ref_means reference_scaler.mean_ # 从固化校准器获取 # 计算相对漂移 drift_scores np.abs((current_means - ref_means) / ref_means) drifted_features drift_scores[drift_scores drift_threshold].index.tolist() if drifted_features: alert(fDrift detected in features: {drifted_features}) # 触发重校准流程 new_scaler StandardScaler().fit(current_batch[numeric_cols]) dump(new_scaler, scaler_v20231016_recalibrated.joblib) return drifted_features # 每小时执行 drifted check_drift(latest_hour_data, loaded_scaler)4.11 节点11预处理单元测试Pytestdef test_missing_pattern_clustering(): # 构造测试数据两组明确缺失模式 test_df pd.DataFrame({ A: [1, 2, np.nan, np.nan], B: [np.nan, np.nan, 3, 4], C: [5, 6, 7, 8] }) clusters detect_missing_patterns(test_df) assert len(set(clusters)) 2 # 应聚为两类 def test_business_rule_cap(): test_df pd.DataFrame({click_count: [100, 5000, 20000, 150000]}) result apply_business_rules(test_df, {click_count: [{condition: True, action: cap_at_100000}]}) assert result[click_count].max() 1000004.12 节点12预处理文档自动生成def generate_preprocessing_doc(df, steps): 根据执行步骤自动生成Markdown文档 doc # 预处理文档\n\n doc ## 数据概览\n doc f- 行数{len(df)}\n doc f- 列数{len(df.columns)}\n doc f- 缺失率{df.isnull().mean().mean():.2%}\n\n doc ## 执行步骤\n for i, step in enumerate(steps, 1): doc f### {i}. {step[name]}\n doc f- 描述{step[description]}\n doc f- 参数{step[params]}\n doc f- 影响{step[impact]}\n\n with open(PREPROCESSING_DOC.md, w) as f: f.write(doc) # 调用 generate_preprocessing_doc(df, [ {name: 业务规则注入, description: 应用KOC用户豁免规则, params: ..., impact: 减少异常值误杀23%}, {name: Target Encoding, description: 对商户ID做平滑编码, params: smooth10, impact: 提升AUC 0.018} ])5. 常见问题与排查技巧实录那些让老手也挠头的预处理暗礁5.1 问题速查表高频故障现象与根因定位现象可能根因排查命令解决方案模型训练时内存溢出One-Hot编码产生超高维稀疏矩阵df[category].nunique()改用TargetEncoding或频率编码对高基数变量先做业务聚类如“城市”→“一线城市/新一线/其他”验证集AUC显著高于测试集预处理中使用了全局统计量如全量数据的均值grep -r df\.mean() preprocessing/确保所有统计量仅从训练集计算用sklearn.preprocessing的fit/transform分离线上预测结果全为0或NaN特征缩放器未加载或参数不匹配print(scaler.mean_, scaler.scale_)检查校准器文件路径打印训练集与线上数据的均值对比特征重要性中“缺失指示特征”排第一缺失值本身是强业务信号但未被正确利用df.groupby(df[col].isnull())[target].mean()将缺失指示特征作为主特征而非辅助特征或用多重插补Multiple Imputation保留不确定性时间序列预测出现明显滞后移动平均特征包含未来数据df[ma7] df[value].rolling(7).mean()改为df[ma7] df[value].rolling(7).mean().shift(1)确保只用历史数据分类变量编码后模型性能下降LabelEncoder引入虚假序数关系print(df[city].head(10))改用One-Hot或TargetEncoding对地理变量用经纬度坐标替代文本特征TF-IDF后效果变差通用停用词表删除业务关键词vectorizer.get_feature_names_out()[:20]构建业务停用词表用TfidfVectorizer(stop_wordscustom_stopwords)5.2 实操避坑心得血泪换来的12条军规永远不要在Jupyter里完成预处理Jupyter的交互式执行会让fit()和transform()混用必须在.py文件中用函数封装强制分离训练/推理逻辑。预处理脚本第一行必须是import warnings; warnings.filterwarnings(error)让所有SettingWithCopyWarning变成报错避免链式索引导致的静默失败。对所有fillna()操作必须跟一句assert df[col].isnull().sum() 0我曾因漏掉这句断言在生产环境跑了三个月才发现某字段填充失效。时间特征必须带时区信息pd.to_datetime(df[ts], utcTrue)否则跨时区服务会出现12小时偏移。文本清洗后必须做长度分布检查df[cleaned_text].str.len().hist()若大量文本长度为0说明正则删除过度如re.sub(r\W, , text)会删掉所有中文。分类变量编码前先做value_counts().tail(10)查看最低频10个值若存在other、unknown等占位符需单独处理而非简单丢弃。数值缩放后必须验证np.isfinite(X_scaled).all()浮点计算可能产生inf尤其在log(x1)变换后。所有预处理函数必须接受random_state参数确保SMOTE等随机操作可复现避免AB测试结果波动。用dask或vaex处理超大数据时禁用pandas的apply()改用向量化操作否则内存暴涨。预处理后的DataFrame必须用df.info(memory_usagedeep)检查内存One-Hot编码可能让内存增长10倍及时用category类型压缩。**对groupby().agg()操作必须指定