1. 什么是特征变换它不是“数据清洗”也不是“模型调参”而是建模前最关键的“翻译工作”“Feature Transformation”——这个词在机器学习项目里出现频率极高但很多人第一次看到时会下意识把它和“数据清洗”“标准化”画等号甚至有人直接当成“给模型喂点好数据”的模糊动作。其实完全不是。我带过二十多个从零起步的工业预测项目几乎每个团队在第二周都会卡在这里模型指标上不去特征重要性图一片混沌明明业务专家说“这个字段肯定关键”可放进模型后贡献值却排倒数。最后回溯发现90%的问题根源不在算法选型也不在超参搜索而是在“Feature Transformation”这一步——他们压根没做或者做了个“假变换”。简单说特征变换是把原始数据字段按照数学规律、业务逻辑和模型脾性重新编码、重组、重释义的过程。它不是让数据“更干净”而是让数据“更懂行话”。比如你有一列“用户最近一次下单时间”原始值是2024-03-17 14:22:05。对人来说这是个具体时刻但对树模型来说它只是一串无法比较大小的字符串对线性模型来说它是个毫无物理意义的大整数时间戳对LSTM来说它又缺了前后序列关系。这时候“变换”就不是加个StandardScaler那么简单——你要拆解出“距今小时数”“是否工作日”“是否午间高峰”“与上单间隔天数”“近7天下单频次滚动均值”……这些新特征才是模型真正能“听懂”的语言。关键词“Feature Transformation”背后实际承载着三重任务语义对齐把业务语言转成模型语言、尺度归一消除量纲干扰比如收入单位是万元点击次数是百位数不处理就会让模型误判“收入更重要”、结构增强挖掘隐藏关系比如“订单金额/用户历史平均客单价”比单纯“订单金额”更能反映异常消费。它不依赖模型训练却决定模型上限它不产生新数据却极大扩展数据的信息密度。适合谁不是只给算法工程师看的——数据工程师要据此设计ETL逻辑业务分析师要据此验证假设产品同学要据此理解“为什么推荐结果突然变了”。它是一条贯穿数据、算法、业务的隐形主线。我常跟新人说如果你的特征变换做完连非技术同事都能指着新特征说“哦这个我懂它代表XXX”那这步才算真正落地。2. 特征变换的整体设计思路为什么不能“先标准化再扔进模型”2.1 核心误区把变换当成“预处理流水线”忽视模型特异性很多团队一上来就建个“feature_engineering.py”里面堆满MinMaxScaler、LabelEncoder、OneHotEncoder然后统一apply到所有字段美其名曰“标准化流程”。我见过最典型的一个案例某电商风控项目把“用户注册渠道”取值自然流量、SEM、KOC、APP推送用LabelEncoder转成0/1/2/3再喂给XGBoost。模型训练飞快AUC也上了0.82但上线后误拒率飙升——查原因才发现LabelEncoder赋予的数值顺序自然流量0SEM1…被树模型当成了“渠道质量递增”的序关系导致模型错误地认为“APP推送渠道风险天然高于自然流量”。而实际上业务侧明确说过各渠道风险无天然序只是类别不同。这里的问题不是编码错了而是没想清楚“这个变换服务于哪个模型又是否符合该模型的数学假设”。所以特征变换的第一原则是模型驱动而非工具驱动。不同模型对输入特征的“脾气”差异极大线性模型LR、Ridge极度敏感于量纲和分布。要求连续特征近似正态、无长尾类别特征必须独热编码One-Hot否则会强行引入不存在的序关系交互项需显式构造如x1*x2模型本身不会自动发现。树模型XGBoost、LightGBM、RF对量纲不敏感能自动处理序关系但对类别基数高的特征如用户ID极其脆弱——LightGBM默认对64类别的特征禁用直方图优化XGBoost在类别100时训练速度断崖下跌。此时必须用Target Encoding或Embedding降维而不是硬上One-Hot。深度模型DNN、TabNet需要固定长度向量输入对缺失值容忍度低且强烈依赖特征缩放通常要求[-1,1]或[0,1]。但它的优势在于能学类别嵌入Category Embedding无需人工构造高维稀疏矩阵。时序模型LSTM、TCN核心诉求是“保持时间结构”。对“日期”字段不能简单转成int而要拆解为周期性分量sin/cos编码小时、星期、月份否则模型无法感知“周一早10点”和“周五早10点”的行为模式差异。提示没有万能变换模板。我给自己定的铁律是——每次新增一个特征变换必问三句话① 这个变换是为哪个模型服务的② 它是否强化了该模型最擅长捕捉的模式③ 它是否弱化了该模型最易被误导的陷阱2.2 设计框架四层漏斗式筛选法基于十年实战我把特征变换设计压缩成一个可复用的四层漏斗每层过滤掉一批“无效努力”确保精力花在刀刃上第一层业务价值过滤Why Layer目标剔除所有“技术上可行但业务上无意义”的变换。操作拉上业务方对每个原始字段问“如果这个字段的值翻倍/归零/变成异常值会对核心指标如转化率、坏账率、留存率产生什么可解释的影响”举例“用户近30天登录天数”翻倍 → 活跃度提升 → 转化率大概率上升“用户设备型号”翻倍 → 无业务含义型号是类别不是数值→ 直接进入下一层判断。实操心得这一步必须由业务方主导算法工程师只做记录员。我曾坚持让信贷经理手写10个“最可能预示逾期的用户行为信号”结果发现其中7个根本不在原始数据表里——这直接推动了数据采集方案的迭代。漏斗第一层筛掉的往往不是代码而是需求。第二层统计可行性过滤What Layer目标验证变换在数据层面是否稳定可靠。关键检查项缺失率 30% 的字段慎用Imputation插补优先考虑“是否缺失本身即信号”如“用户未填写学历”可能比“学历高中”更具风险类别型字段若Top1类别占比 85%则One-Hot后90%以上是0浪费内存且易过拟合应合并小类别或改用Target Encoding连续型字段若Shapiro-Wilk检验p值 0.01强拒绝正态假设且对数变换后仍偏态则放弃Box-Cox改用分位数变换QuantileTransformer或离散化KBinsDiscretizer。我习惯用一个自查表快速过一遍见下表5分钟内完成评估字段名类型缺失率类别数/分布形态变换候选方案可行性自评✓/×用户年龄连续2.1%右偏峰值在25-35LogStandardScaler×Log后仍偏改用Quantile所在城市类别0%387城Top1上海占12%One-Hot限Top50 其余归“其他”✓最近下单距今小时数连续0%强长尾90%72h最大值12000h分箱0-24h,24-168h,168h✓第三层模型适配性过滤How Layer目标匹配变换方法与模型约束。这里必须对照模型文档动手验证。以LightGBM为例官方明确说明categorical_feature参数仅支持字符串或int类型且int值必须从0开始连续不能是[1,3,5]对高基数类别特征cat_l2和cat_smooth参数必须调大否则容易过拟合若启用enable_bundle默认True则One-Hot后的稀疏矩阵会被自动压缩但Target Encoding需手动关闭此选项。我吃过亏曾把用户职业300类用Target Encoding后忘记在LightGBM中设置categorical_featureNone结果模型把编码值当连续变量处理重要性排序全乱。后来形成习惯每次用新变换必跑一个最小闭环——单特征单样本最小树深看输出是否符合预期。第四层工程落地性过滤Where Layer目标确保变换逻辑能无缝嵌入生产Pipeline。重点排查是否引入未来信息Look-Ahead Leakage例如用“当日总GMV”构造用户小时级特征但该GMV在T1才可得线上推理时不可用是否依赖实时计算如“过去5分钟点击流窗口均值”需Flink/Kafka支撑若当前架构只有离线Hive则必须降级为“近1小时均值”是否增加特征维度爆炸One-Hot后维度从1变500而线上服务内存限制2GB则必须用Hashing Trick或Embedding。我们团队的红线是所有变换代码必须通过“离线-在线一致性测试”——同一份原始数据离线脚本输出的特征向量与线上Serving模块输出的必须逐位相等误差1e-6。这逼着我们在设计阶段就考虑序列化、精度损失、时区对齐等细节。3. 核心变换技术详解与实操实现从原理到一行代码的真相3.1 连续特征变换为什么“标准化”常常是第一步却绝不是最后一步连续特征变换的核心矛盾在于模型需要数值稳定业务需要语义可读。标准化StandardScaler解决前者但彻底牺牲后者。我见过太多团队把“用户月均消费额”标准化后特征重要性排第一但业务方完全无法理解“重要性2.31”意味着什么。所以真正的连续特征工程是“标准化业务解构”的组合拳。第一步识别并处理长尾分布必须前置长尾是连续特征的头号杀手。以“用户历史总消费额”为例95%用户5000元但头部0.1%用户50万元直接StandardScaler会导致95%的数据挤在[-0.5,0.5]区间模型难以区分普通用户。正确做法分三步可视化诊断用seaborn.histplot(data, xtotal_amount, statdensity, kdeTrue)看分布形态叠加正态分布曲线统计验证scipy.stats.shapiro(data[total_amount])p0.01即拒绝正态选择变换若右偏多数值小少数极大首选Yeo-Johnson变换比Box-Cox优势支持负值和零若存在明确业务阈值如“VIP门槛10000元”则分箱WOE编码Weight of Evidence既保留业务含义又使模型学习到“跨越门槛”的非线性效应。# Yeo-Johnson变换实操sklearn 1.2 from sklearn.preprocessing import PowerTransformer pt PowerTransformer(methodyeo-johnson, standardizeTrue) data[total_amount_yj] pt.fit_transform(data[[total_amount]]) # WOE分箱实操需安装category_encoders from category_encoders import WOEEncoder # 先用KBinsDiscretizer分5箱 from sklearn.preprocessing import KBinsDiscretizer kb KBinsDiscretizer(n_bins5, encodeordinal, strategyquantile) data[total_amount_bin] kb.fit_transform(data[[total_amount]]) # 再WOE编码target为二分类标签如is_vip woe WOEEncoder(cols[total_amount_bin]) data woe.fit_transform(data, data[is_vip])注意WOE编码必须用训练集标签拟合且线上推理时需保存分箱边界和WOE映射表。我习惯把kb.bin_edges_和woe.mapping_一起存为joblib文件避免线上加载时因版本差异出错。第二步构造业务衍生特征不可跳过标准化只是让数值“看起来舒服”真正提升效果的是业务逻辑注入。以电商场景为例“用户近7天访问频次”单独标准化意义有限但结合“近7天加购次数/访问频次”衡量访问质量、“近7天访问频次/注册天数”衡量活跃衰减率就形成了有业务灵魂的特征簇。我的经验是每个原始连续字段至少构造2个衍生特征且必须满足一个反映绝对水平如“近30天订单数”一个反映相对变化如“近30天订单数/历史均值”一个反映结构比例如“近30天移动端订单数/总订单数”。# 实操代码构造三维度特征 import pandas as pd import numpy as np # 假设data为用户粒度宽表含user_id, visit_cnt_7d, order_cnt_7d, reg_days # 绝对水平 data[visit_abs] data[visit_cnt_7d] # 相对变化需先计算历史均值用滑动窗口或分组均值 # 这里用分组均值模拟实际项目用Spark/Hive窗口函数 history_mean data.groupby(user_id)[visit_cnt_7d].transform(mean) data[visit_rel] np.where(history_mean 0, data[visit_cnt_7d] / history_mean, 0) # 结构比例需关联设备数据简化为字段device_order_ratio data[visit_struct] data[visit_cnt_7d] / (data[visit_cnt_7d] data[order_cnt_7d] 1e-8)第三步标准化与缩放收尾非起点此时才进行最终缩放。注意不要对衍生特征统一StandardScaler因为“visit_rel”已是比值0~10而“visit_abs”可能是0~1000统一缩放会抹平业务含义。正确做法是分组缩放绝对水平类原始值、求和值→ StandardScaler比例类百分比、比率→ MinMaxScaler缩放到[0,1]相对变化类倍数、增速→ RobustScaler用中位数和四分位距抗异常值。from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler # 分组定义 abs_cols [visit_abs, order_abs] ratio_cols [visit_struct, order_struct] rel_cols [visit_rel, order_rel] # 分别拟合 scaler_abs StandardScaler().fit(data[abs_cols]) scaler_ratio MinMaxScaler().fit(data[ratio_cols]) scaler_rel RobustScaler().fit(data[rel_cols]) # 应用 data[abs_cols] scaler_abs.transform(data[abs_cols]) data[ratio_cols] scaler_ratio.transform(data[ratio_cols]) data[rel_cols] scaler_rel.transform(data[rel_cols])3.2 类别特征变换One-Hot不是银弹Target Encoding藏着魔鬼细节类别特征变换的坑90%集中在两个地方一是高基数特征如用户ID、商品ID的暴力One-Hot二是Target Encoding的泄露与偏差。我带的一个推荐项目曾因Target Encoding没做平滑导致新用户冷启动特征全是NaN线上CTR直接跌30%。One-Hot的生存指南适用场景低基数10类、无序类别、模型明确支持稀疏输入如XGBoost。致命陷阱内存爆炸10万用户IDOne-Hot后10万维单样本内存占用从1KB飙到800KB维度诅咒高维稀疏特征让树模型分裂收益骤降LightGBM默认max_cat_to_onehot4超限自动转Target Encoding训练/推理不一致训练时有100个城市线上来了第101个One-Hot报错。解决方案强制截断只对Top N类别One-Hot其余归“Other”。N的选择公式N min(50, int(0.95 * total_categories))保证覆盖95%样本Hashing Trick用FeatureHasher将类别映射到固定维度如1024牺牲少量信息换稳定性Embedding深度模型专属用tensorflow.keras.layers.Embedding维度√类别数如1000类→32维需端到端训练。# Hashing Trick实操适合线上服务 from sklearn.feature_extraction import FeatureHasher # 假设city列有1000城市 hasher FeatureHasher(n_features1024, input_typestring) # 将city转为字典格式 city_dict data[city].apply(lambda x: {city: x}) hash_matrix hasher.transform(city_dict) # hash_matrix是稀疏矩阵可直接喂给模型Target Encoding的魔鬼细节Target Encoding本质是用目标变量的均值替代类别但均值估计不准会引入严重偏差。核心问题有三小样本偏差某城市仅3个样本全为正样本均值1.0但真实概率可能仅0.3数据泄露用全局均值编码训练时模型看到“未来信息”过拟合对低频类别编码值波动剧烈模型学到了噪声。我的实操方案是“三重防护”防护一平滑Smoothing公式encoded (sum(target) prior * global_mean) / (count prior)prior经验值prior max(10, int(0.05 * len(data)))取5%样本量或10取大者防护二交叉验证编码CV Encoding不用全局均值而用K折交叉验证第k折的编码值用其他k-1折的均值计算。category_encoders库的TargetEncoder默认开启。防护三添加噪声Noise Injection对编码值加高斯噪声noise np.random.normal(0, 0.01 * std_of_encoded)std取训练集编码值的标准差。这能防止模型对低频类别过拟合。# Target Encoding完整实操防泄露平滑噪声 from category_encoders import TargetEncoder import numpy as np # 初始化自动CV编码 te TargetEncoder( cols[city, occupation], smoothing10, # prior值 handle_unknownvalue, handle_missingvalue ) # 拟合内部已做CV data_encoded te.fit_transform(data, data[is_purchase]) # 添加噪声仅训练集 np.random.seed(42) noise_std data_encoded[city].std() * 0.01 data_encoded[city] np.random.normal(0, noise_std, len(data_encoded))注意Target Encoding的smoothing参数不是越大越好。我测试过smoothing100时所有城市编码趋同丢失区分度smoothing1时小城市噪声太大。最佳值需在验证集上AUC曲线拐点处选取——通常在10~30之间。3.3 时间特征变换为什么“年月日时分秒”是最危险的原始字段时间字段是特征变换的“雷区”因为它的信息密度最高陷阱也最多。新手常犯的错是把“2024-03-17”转成int1700000000或直接One-Hot年份/月份。结果模型学到“2024年风险高”纯粹是数据切分导致的巧合。正确打开方式周期性分解 趋势剥离时间包含三重信息周期性Cyclic小时、星期、月份有天然循环23点后是0点周日之后是周一趋势性Trend年份、季度体现长期增长/衰退事件性Event节假日、促销日、系统升级日等离散事件。周期性编码必须用sin/cos不能用One-Hot理由One-Hot破坏了“23点与0点相邻”的物理事实。sin/cos编码将24小时映射到单位圆上23点330°和0点0°在向量空间距离最近。# 小时周期性编码 data[hour_sin] np.sin(2 * np.pi * data[hour] / 24) data[hour_cos] np.cos(2 * np.pi * data[hour] / 24) # 验证23点和0点的欧氏距离 sqrt((sin330-sin0)^2 (cos330-cos0)^2) ≈ 0.52远小于23点与12点的距离≈1.93趋势性编码用相对时间而非绝对时间绝对时间如2024年在跨时间切分时失效。正确做法是计算“距某个锚点的时间差”对用户行为距注册时间的天数dt - reg_date对订单距首次下单的天数dt - first_order_date对预测距预测时间的天数pred_date - dt确保线上推理可用。事件性编码用布尔标记而非日期值把“是否春节”“是否618大促”“是否系统维护日”作为独立布尔特征。关键是要建立事件日历表CSV而非在代码里硬编码日期方便业务方随时更新。# 构建事件日历外部CSV event_calendar pd.read_csv(event_calendar.csv) # columns: date, is_spring_festival, is_618, is_maintenance # 关联到主表 data data.merge(event_calendar, left_ondate, right_ondate, howleft) # 填充缺失非事件日为False data[[is_spring_festival, is_618, is_maintenance]] data[[is_spring_festival, is_618, is_maintenance]].fillna(False)4. 实操全流程与避坑指南从本地Jupyter到千台服务器集群4.1 本地开发阶段如何用100行代码搭建可复现的变换Pipeline本地阶段的核心目标快、准、可复现。我拒绝一切“配置文件yaml复杂框架”坚持用纯Python函数封装因为这样最易调试、最易解释给业务方听。我的标准模板如下已脱敏可直接套用# feature_pipeline.py import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler, MinMaxScaler from category_encoders import TargetEncoder def load_data(): 加载原始数据返回DataFrame # 这里替换为你的数据源如pd.read_parquet(raw_data.parq) return pd.read_csv(sample_data.csv) def engineer_features(df): 主变换函数所有逻辑集中在此 df df.copy() # 步骤1连续特征处理 df _process_continuous(df) # 步骤2类别特征处理 df _process_categorical(df) # 步骤3时间特征处理 df _process_datetime(df) # 步骤4特征缩放最后一步 df _scale_features(df) return df def _process_continuous(df): # 示例处理age字段 df[age_log] np.log1p(df[age]) # log1p防0 df[age_group] pd.cut(df[age], bins[0,18,35,60,100], labels[minor,young,mid,senior]) return df def _process_categorical(df): # 示例对city做Target Encoding te TargetEncoder(cols[city], smoothing15) df te.fit_transform(df, df[label]) return df def _process_datetime(df): # 示例处理order_time df[order_hour] pd.to_datetime(df[order_time]).dt.hour df[order_hour_sin] np.sin(2*np.pi*df[order_hour]/24) df[order_hour_cos] np.cos(2*np.pi*df[order_hour]/24) return df def _scale_features(df): # 仅缩放数值型跳过布尔和类别编码列 numeric_cols df.select_dtypes(include[np.number]).columns.tolist() scaler StandardScaler() df[numeric_cols] scaler.fit_transform(df[numeric_cols]) return df # 快速验证入口 if __name__ __main__: data load_data() print(原始特征数:, data.shape[1]) data_eng engineer_features(data) print(变换后特征数:, data_eng.shape[1]) print(前5行:\n, data_eng.head())实操心得这个模板的威力在于“所有变换逻辑可见、可打断点、可单步执行”。我曾用它帮一个金融客户在2小时内定位到特征泄露——在_process_datetime函数里加一行print(df[label].corr(df[order_hour_sin]))发现相关性高达0.8立刻意识到“订单时间”和“是否欺诈”在训练集里强相关但线上无法获取精确到小时的下单时间必须降级为“是否工作日”。4.2 离线生产阶段如何让特征变换扛住每天10TB数据离线生产的核心挑战一致性、时效性、可监控。我负责的一个广告平台日增数据30TB特征变换作业曾因OOM失败17次/天。后来重构为三层架构第一层原子化变换函数UDF把每个变换逻辑封装成独立函数如udf_age_log(x)、udf_city_target_encode(x, city_map)在Spark UDF中注册。优势可单独测试、单独发布失败时精准定位到哪个函数支持SQL直接调用业务方可自助写特征。# Spark UDF示例 from pyspark.sql.functions import udf from pyspark.sql.types import DoubleType udf(returnTypeDoubleType()) def udf_age_log(age): return np.log1p(float(age)) if age and age 0 else 0.0 # 在SQL中使用 spark.sql( SELECT user_id, udf_age_log(age) as age_log FROM raw_table )第二层特征血缘追踪Lineage Tracking每生成一个特征自动记录源字段如raw_table.age变换函数如udf_age_log参数如basee生效时间2024-03-17 10:00:00。我们用Neo4j存储血缘图当某特征异常时一键追溯上游所有依赖3分钟内定位是数据源问题还是变换逻辑问题。第三层自动化漂移检测Drift Detection每天凌晨跑一次计算每个特征的统计量均值、方差、缺失率、类别分布与基线上周均值对比delta 阈值则告警对类别特征用PSIPopulation Stability Index量化分布变化。阈值设定经验连续特征均值delta 5% 或 方差delta 20%类别特征PSI 0.1轻微漂移 0.25严重漂移需人工介入。# PSI计算示例 def calculate_psi(expected, actual, n_bins10): expected/actual为一维数组 expected_percents np.histogram(expected, binsn_bins)[0] / len(expected) actual_percents np.histogram(actual, binsn_bins)[0] / len(actual) psi 0 for i in range(n_bins): if expected_percents[i] 0: continue if actual_percents[i] 0: psi expected_percents[i] * np.log(expected_percents[i] / 1e-5) else: psi (actual_percents[i] - expected_percents[i]) * np.log(actual_percents[i] / expected_percents[i]) return psi4.3 在线服务阶段如何让特征变换毫秒级响应且零误差线上服务的黄金法则是所有变换必须无状态、无IO、无随机。任何依赖外部API、数据库查询、随机数生成的操作都是线上延迟和错误的源头。我的线上特征服务架构已验证千万QPS预计算层对静态特征如用户基础画像在离线ETL中完成所有变换存入Redis Hashkeyuser_id, fieldfeature_name, valuefeature_value实时计算层对动态特征如“近5分钟点击数”用Flink实时聚合结果存入Redis Sorted Set线上服务用ZCOUNT毫秒获取混合层对“近1小时订单均值”这类中频特征用Lambda架构——离线批处理提供基准值实时流更新增量线上服务取两者加权。关键避坑点精度陷阱浮点数运算在Java/Python/C中结果可能微异。解决方案所有数值特征在离线和线上都用decimal或int存储如金额存分为单位时区陷阱离线用UTC线上服务必须统一时区。我们强制所有时间字段在入库前转为UTC线上不作任何时区转换缓存穿透新用户首次请求Redis无数据。必须设置空值缓存SET user:123:feature EX 300避免打穿下游。提示线上特征服务上线前必做“一致性压测”——用同一份原始数据同时调用离线脚本和线上API对比10万样本的特征向量要求100%相等。我们曾发现一个bug离线用np.round(x, 6)线上用Math.round(x*1e6)/1e6在特定值上产生1e-15级误差导致模型预测结果不同。这种细节只有压测能暴露。5. 常见问题与独家排查技巧那些文档里不会写的真相5.1 “模型效果突然下降特征变换背锅了吗”这是最高频的故障。我的排查清单按优先级排序检查特征漂移首要运行PSI/JS散度检测重点关注Top10重要性特征查看告警日志是否有特征缺失率突增如某城市数据源中断独家技巧对连续特征画“分布对比图”——用seaborn.kdeplot叠绘训练集和线上集分布肉眼可见偏移。我见过最隐蔽的漂移某支付渠道因政策调整手续费从0.6%涨到0.8%导致“订单金额/手续费”特征整体右移但均值变化仅0.3%PSI未触发靠分布图揪出。检查变换逻辑变更第二对比Git历史过去7天内feature_pipeline.py是否有提交独家技巧在变换函数开头加版本号和哈希值def _process_continuous(df): # v1.2.3 | hash: a1b2c3d4 ...线上服务启动时打印此信息确保离线和线上用同一版逻辑。检查数据源变更第三查看数据平台血缘该特征依赖的上游表Schema是否新增字段分区是否延迟独家技巧对关键字段加“数据契约”校验# 在load_data()中 assert data[age].min() 0, age cannot be negative assert data[city].nunique() 500, city categories exploded5.2 “为什么Target Encoding后新类别OOV的预测效果极差”这是Target Encoding的原生缺陷。标准方案是填global_mean但效果