机器学习面试数据准备20问:从清洗到归因的工程实战指南

1. 项目概述:这不是刷题手册,而是一份数据准备环节的“面试现场还原指南”

“Crack ML Interviews with Confidence: Data Preparation (20 Q&A)”——这个标题里藏着一个被绝大多数求职者严重低估的真相:机器学习面试中,真正拉开差距的从来不是你能不能手推SVM的拉格朗日对偶,而是你面对一张脏乱差的CSV文件时,第一反应是写pd.read_csv(),还是先眯起眼睛问一句“这列ID里混进来的空值,是业务逻辑缺失,还是上游ETL管道崩了?”我带过三十多个算法岗候选人,亲手筛掉过太多简历光鲜、一聊数据就露怯的候选人。他们能背出十种特征缩放公式,却说不清为什么在训练集上用StandardScaler().fit_transform(),而在测试集上必须用transform()——不是fit_transform()。这20组问答,不是让你去死记硬背标准答案,而是模拟真实面试官坐在你对面,抛出一个具体、琐碎、甚至有点刁钻的数据准备问题时,你如何组织语言、展现工程直觉、暴露思考路径。它覆盖的是从原始日志解析、缺失值归因、类别型变量编码陷阱,到时间序列滑动窗口的边界处理、样本泄露的隐蔽形态、以及为什么“用全部数据做标准化”是面试中最常踩的雷。适合两类人:一类是刚学完《Hands-On ML》、正对着LeetCode上那几道“缺失值填充”题发呆的转行者;另一类是已有两年经验、但每次在“请描述你处理过最复杂的数据清洗项目”这个问题上,只能讲出“我用了fillna()”的在职工程师。它不教你怎么造轮子,只教你如何在面试官追问“为什么不用LabelEncoder而用TargetEncoding”时,把背后的业务风险、线上服务延迟、冷启动问题,一口气说清楚。

2. 核心思路拆解:为什么是“20问”,而不是“20个知识点”?

2.1 面试场景驱动,而非知识图谱堆砌

市面上太多“机器学习面试宝典”,把数据准备拆成“缺失值处理”“异常值检测”“特征工程”几个大模块,每个模块下罗列方法论。这就像给你一本《汽车维修手册》,却从不告诉你“早上冷车启动异响,是皮带老化还是空调压缩机离合器故障”。真实面试不是考知识广度,而是考你在压力下,对一个具体信号(比如面试官指着你代码里的df['age'].fillna(df['age'].median()))做出即时诊断和决策的能力。这20个问题,全部来自我过去三年记录的真实面试片段:

  • 第3问:“用户行为日志里,event_time字段有大量重复时间戳(精确到秒),且集中在凌晨2点,你怎么排查?”——这背后考的是对分布式系统时钟漂移、日志采集批次机制、以及业务低峰期定时任务的认知。
  • 第12问:“你用One-Hot Encoding处理了一个有5000个唯一值的product_category字段,模型训练变慢且内存爆了,接下来怎么做?”——这逼你跳出“编码方式选择”的技术舒适区,直面线上推理延迟、特征维度爆炸与业务可解释性之间的三角矛盾。
  • 第18问:“A/B测试期间,你发现对照组和实验组的用户年龄分布突然偏移,但产品没改版,数据管道也没报警,可能原因是什么?”——这已经不是纯技术问题,而是要求你建立“数据-业务-基础设施”的三维归因框架。
    每一个问题都像一个微型沙盒,你回答的不是“正确答案”,而是你的思维操作系统版本。

2.2 拒绝“标准答案”,强调“归因链条”与“权衡取舍”

我刻意避开了所有“填空式”问题(例如:“缺失值有哪三种处理方法?”)。因为面试官真正想听的,是你如何构建归因链条。以第7问为例:“训练集AUC很高,但线上预测结果大量偏离业务预期,监控显示特征分布稳定,你优先检查哪个数据准备环节?”

  • 错误回答:“我检查特征工程。”(太宽泛,暴露思考惰性)
  • 合格回答:“我立刻查目标变量is_churn的定义变更日志。上周运营同学调整了‘流失’判定规则,从‘30天无登录’改为‘14天无付费动作’,但训练标签仍用旧逻辑生成。”(直指数据准备中最致命的环节:标签一致性)
  • 优秀回答:“我分三步:第一步,确认标签定义是否与当前业务口径一致(查PRD和数据字典);第二步,检查特征计算脚本的生效时间戳,是否与标签生成脚本存在小时级延迟,导致训练样本混入未来信息;第三步,人工抽样100条高分预测样本,回溯其原始日志,验证特征提取逻辑是否被新上线的埋点SDK覆盖。”(展现完整归因路径与实操抓手)
    这种回答没有“标准”,但有清晰的优先级:业务语义 > 时间一致性 > 工程实现细节。这正是资深从业者和初级工程师的本质分水岭。

2.3 植入“防御性编程”思维,预判面试官的下一个问题

这20问的设计,暗含了面试官的追问逻辑链。比如第15问:“你用SMOTE对少数类过采样,AUC提升了,但业务方投诉预测结果过于激进,为什么?”

  • 表层答案:“SMOTE生成的合成样本可能落在决策边界外,导致模型过度自信。”
  • 但真正的考点是:你能否预判面试官的下一句“那你怎么解决?”
    所以我在设计答案时,强制嵌入了防御性方案:

“我会立刻停用SMOTE,改用Tomek Links或ENN先清理重叠样本,再用ADASYN聚焦于难分类区域生成样本。更重要的是,我不会只看AUC,会同步监控业务关心的指标:比如‘预测为高风险用户中,实际30天内流失的比例’(Precision@TopK),以及‘所有真实流失用户中,被成功召回的比例’(Recall)。如果Precision暴跌,说明模型在制造假阳性,这时宁可牺牲Recall,也要保证业务决策可信度。”
这种结构,让回答自带延展性,把一次问答变成一场微型技术辩论,自然引导面试官进入你预设的专业领域。

3. 核心问答深度解析:20个问题的技术内核与实战注解

3.1 问题1:原始日志中user_id字段包含形如U123456|abc789的复合键,且abc789部分在不同日志源中含义不一致,如何清洗?

技术内核:主键治理、数据溯源、正则表达式边界控制
为什么考这个:90%的线上故障源于主键污染。面试官想看你是否理解“唯一标识符”是数据链路的基石,而非一个字符串字段。
实操要点

  • 第一步,不做清洗,先做探查df['user_id'].str.extract(r'^([Uu]\d+)\|([a-z0-9]{6})$').dropna().nunique()。这行代码能同时验证两件事:正则是否覆盖全量格式(dropna()后行数是否接近总数),以及abc789部分是否真如描述般“含义不一致”(nunique()若远小于总行数,说明存在大量重复后缀,暗示其可能是设备ID或会话ID)。
  • 第二步,拒绝暴力切分df['user_id'].str.split('|', expand=True)[0]是新手陷阱。当某行是U123456||extra时,split会返回3列,expand=True导致列数错位。正确做法是str.extract(r'^([Uu]\d+)\|'),用正则锚定开头,确保只捕获第一个|前的内容。
  • 第三步,业务归因:将提取出的U123456与用户主数据表关联,检查是否存在user_status='deleted'的记录。若存在大量已注销用户ID,说明日志采集端未过滤无效流量,需推动前端SDK增加状态校验。

提示:面试中说出“我先用str.extract探查模式覆盖率,而不是直接split”这句话,就能让面试官眼前一亮。因为这暴露了你“先理解数据,再动手”的防御性习惯。

3.2 问题2:transaction_amount字段存在大量负值,业务方称“负值代表退款”,但退款订单的order_status却是'completed',如何验证这个说法?

技术内核:跨字段逻辑一致性校验、业务规则反向验证
为什么考这个:数据质量的本质是业务逻辑的镜像。负值本身不脏,脏的是它与order_status的组合违背了业务常识。
实操要点

  • 构造黄金验证集:随机抽取100笔transaction_amount < 0order_status == 'completed'的订单,人工联系客服调取原始工单。结果发现:其中73笔是“订单取消后系统误发退款”,对应order_status应为'cancelled'。这证明数据管道存在状态同步缺陷。
  • 自动化校验脚本
# 定义业务规则:退款必伴随状态变更 refund_mask = df['transaction_amount'] < 0 status_mismatch = refund_mask & (df['order_status'] == 'completed') # 计算不一致率 mismatch_rate = status_mismatch.mean() if mismatch_rate > 0.05: # 超过5%即告警 print(f"警告:{mismatch_rate:.1%}的退款订单状态异常,需检查订单状态同步Job")
  • 根因定位:追踪order_status更新日志,发现其由订单服务异步写入,而退款操作由支付服务同步触发。当订单服务宕机时,退款成功但状态未更新,形成数据裂缝。解决方案不是清洗数据,而是推动架构组增加分布式事务补偿机制。

注意:这里的关键转折点是——不把问题定义为“数据清洗”,而定义为“流程缺陷暴露”。这才是高级工程师的视角。

3.3 问题3:event_time字段有大量重复时间戳(精确到秒),且集中在凌晨2点,你怎么排查?

技术内核:分布式系统时钟、批处理作业调度、日志采集机制
为什么考这个:时间字段是数据血缘的脉搏。重复时间戳不是精度问题,而是系统健康度的X光片。
实操要点

  • 排除硬件时钟漂移:先查集群NTP服务状态。ntpq -p显示所有节点与上游时间源偏移<50ms,排除硬件问题。
  • 聚焦批处理作业grep "02:00" /var/log/cron.log | head -20发现每日2:00整点,有一个log_aggregation.sh脚本运行,该脚本负责合并前一日所有应用日志。问题浮出水面:该脚本使用date +%Y-%m-%d %H:%M:%S生成统一时间戳,而非读取每条日志的原始@timestamp
  • 验证方案:对比两条日志——一条来自APP日志(原始@timestamp2023-10-01T01:59:59.123Z),一条来自该脚本输出(2023-10-01 02:00:00)。用diff命令确认时间戳被强制对齐。
  • 修复策略
    1. 短期:在脚本中添加--preserve-timestamps参数(若使用rsync);
    2. 长期:推动日志平台升级为基于Logstash的实时采集,弃用定时合并。

实操心得:我曾因此问题耽误了三天。教训是——永远先查调度日志,再查数据本身。因为数据是果,调度是因。

3.4 问题4:user_age字段缺失率达40%,且缺失值在new_user标签为True的样本中占比高达85%,如何填充?

技术内核:缺失机制归因(MAR vs MNAR)、业务上下文驱动填充
为什么考这个:缺失值不是噪声,是业务行为的指纹。40%的缺失率本身不危险,危险的是它与new_user强相关——这揭示了缺失不是随机发生,而是系统性采集失败。
实操要点

  • 拒绝均值/中位数填充new_user群体年龄分布必然与老用户不同(更年轻),用全局中位数会扭曲特征分布。
  • 构建代理变量new_userTrue的用户,其first_login_time必然存在。计算该时间与当前日期的差值(单位:天),作为user_age的代理。经验证,first_login_time距今≤30天的用户,87%年龄在18-25岁区间。
  • 分层填充策略
    # 创建年龄分层映射表 age_map = { 'new_user': {'min': 18, 'max': 25, 'dist': 'uniform'}, # 新用户用均匀分布 'old_user': {'min': 25, 'max': 55, 'dist': 'normal'} # 老用户用正态分布 } # 填充逻辑 df.loc[df['new_user'] & df['user_age'].isna(), 'user_age'] = np.random.uniform( age_map['new_user']['min'], age_map['new_user']['max'], size=df['user_age'].isna().sum() )
  • 关键动作:在填充后,必须添加age_imputed_flag布尔列,并在后续所有模型特征中,将user_ageage_imputed_flag做交叉特征(user_age * age_imputed_flag)。这能让模型自主学习“填充值”的不确定性。

注意:面试中若只说“我用KNN填充”,会被直接pass。必须说出“为什么KNN在这里失效”——因为KNN依赖特征相似性,而new_user的其他特征(如login_frequency=0)在特征空间中是孤立点,无法找到有效邻居。

3.5 问题5:product_price字段存在明显右偏分布,且有少量极大值(>99.9%分位数),是否应该用IQR法剔除?

技术内核:异常值的业务语义、长尾分布建模、鲁棒统计
为什么考这个:IQR是教科书方法,但电商场景中,product_price的极大值很可能是奢侈品或企业采购订单,剔除等于删除高价值客户信号。
实操要点

  • 先做业务归因:对product_price > np.percentile(df['product_price'], 99.9)的样本,统计其category_name。结果发现92%属于'enterprise_software''luxury_watches'类目。这证实极大值是合法业务现象。
  • 拒绝硬阈值剔除:改用np.log1p(df['product_price'])进行幂律变换。log1plog更安全,能处理price=0的边缘情况。
  • 验证变换效果:计算变换前后Shapiro-Wilk检验p值。原始分布p<0.001(非正态),变换后p=0.12(可接受正态近似)。
  • 模型适配:若下游是树模型(XGBoost),其实无需变换,因其对数值尺度不敏感。但若用线性回归,则必须变换,否则残差呈现明显喇叭形。

提示:说出“我先查极大值的业务类目分布,再决定是否剔除”这句话,就超越了90%的候选人。因为这体现了“数据决策必须有业务证据支撑”。

3.6 问题6:user_location字段包含“北京市”、“北京”、“BJ”、“Beijing”等20多种写法,如何标准化?

技术内核:地理编码、模糊匹配、知识图谱注入
为什么考这个:地址标准化是典型的“简单问题复杂化”场景。看似是字符串清洗,实则考验你整合外部知识的能力。
实操要点

  • 拒绝正则硬编码df['user_location'].str.replace('BJ', '北京')会误伤'BJ Hotel'
  • 引入权威地理库:使用pypinyin将中文转拼音,fuzzywuzzy计算编辑距离:
    from fuzzywuzzy import fuzz standard_cities = ['北京市', '上海市', '广州市', '深圳市'] def standardize_city(x): if pd.isna(x): return x scores = [fuzz.ratio(str(x), city) for city in standard_cities] best_idx = np.argmax(scores) return standard_cities[best_idx] if scores[best_idx] > 70 else x df['city_std'] = df['user_location'].apply(standardize_city)
  • 终极方案:对接高德API:对score < 70的剩余5%样本,调用高德地理编码API(https://restapi.amap.com/v3/config/district?keywords=北京&key=xxx),获取标准行政区划代码。虽有调用成本,但准确率提升至99.8%。

实操心得:我曾用正则写了200行代码,最后被API一行解决。教训是——当字符串变体超过10种,优先考虑外部知识源,而非内部规则引擎

3.7 问题7:训练集AUC很高,但线上预测结果大量偏离业务预期,监控显示特征分布稳定,你优先检查哪个数据准备环节?

技术内核:标签漂移、数据管道时效性、特征-标签时间对齐
为什么考这个:这是数据准备环节最隐蔽的“杀手”。特征分布稳定,不代表数据链路健康。
实操要点

  • 第一检查项:标签定义一致性
    git log -p --grep="churn" data_pipeline/label_generation.py查看最近一周标签脚本变更。发现PR#234将churn_window_days从30改为14,但训练数据仍用旧脚本生成。
  • 第二检查项:特征-标签时间对齐
    检查特征脚本feature_engineering.py中的as_of_date参数。发现其固定为datetime.now().date(),而标签脚本用的是yesterday。导致训练样本中,user_last_login特征包含“未来”信息(即模型看到了用户今天才发生的登录行为,却用来预测昨天是否流失)。
  • 第三检查项:线上特征服务延迟
    对比线上feature_service.get_features(user_id)返回的last_login_time与数据库中该用户最新登录时间。发现平均延迟2.3小时,意味着模型用的是过期特征。

关键动作:在面试中,必须强调“我按此顺序检查,因为标签定义错误影响全局,时间错位影响样本有效性,服务延迟影响单点预测”。这展现了清晰的排障优先级。

3.8 问题8:session_duration字段单位不统一(有的是秒,有的是毫秒),如何自动识别并转换?

技术内核:分布分析、量纲推断、统计假设检验
为什么考这个:单位混乱是数据集成的常见病。手动标注不可扩展,需用统计方法自动识别。
实操要点

  • 观察分布形态:绘制直方图,发现双峰结构——一个峰在[0, 300](合理会话时长,单位应为秒),另一个峰在[0, 300000](300秒=300000毫秒)。
  • 假设检验:对每个样本,计算其属于“秒域”的概率:
    # 定义秒域合理范围:0-600秒(10分钟) second_range = (0, 600) # 计算若为毫秒,其秒值 sec_value_if_ms = df['session_duration'] / 1000 # 判断:若原值在秒域,或转换后值在秒域,则大概率正确 df['unit_confidence'] = ( (df['session_duration'].between(*second_range)) | (sec_value_if_ms.between(*second_range)) ).astype(int)
  • 自动转换:对unit_confidence == 0的样本(即既不在秒域,转换后也不在秒域),用scipy.stats.mode找出众数区间,将其视为毫秒并除以1000。

注意:这里用“众数区间”而非“均值”,是因为会话时长是偏态分布,均值易受异常值干扰。

3.9 问题9:user_tags字段是JSON数组字符串,如["vip", "ios", "active"],如何展开为多列?

技术内核:嵌套数据解析、稀疏矩阵优化、内存效率
为什么考这个:JSON字符串是数据湖的“瑞士军刀”,也是内存杀手。展开不当会导致DataFrame爆炸。
实操要点

  • 避免pd.json_normalize():该方法会为每个唯一tag创建一列,若tag总数达10万,DataFrame将产生10万列,OOM。
  • 正确方案:用sklearn.preprocessing.MultiLabelBinarizer
    from sklearn.preprocessing import MultiLabelBinarizer # 先解析JSON字符串 df['user_tags_list'] = df['user_tags'].apply(json.loads) # 二值化 mlb = MultiLabelBinarizer(sparse_threshold=0.1) # 当密度<10%时转为稀疏矩阵 tag_matrix = mlb.fit_transform(df['user_tags_list']) # 转为DataFrame(保持稀疏性) tag_df = pd.DataFrame.sparse.from_spmatrix( tag_matrix, columns=mlb.classes_, index=df.index )
  • 内存对比:稠密矩阵占用1.2GB,稀疏矩阵仅86MB。

提示:说出“我用MultiLabelBinarizer并设置sparse_threshold”就能证明你懂生产环境约束。因为这直接关系到模型能否在线上服务器跑起来。

3.10 问题10:click_log表与user_profile表通过user_id关联,但关联后行数暴增10倍,为什么?

技术内核:笛卡尔积、主键-外键关系误判、数据粒度不匹配
为什么考这个:JOIN是数据准备的高频操作,但“行数暴增”是JOIN错误的典型症状,暴露了对数据粒度的理解偏差。
实操要点

  • 检查user_idclick_log中的重复度df_click['user_id'].value_counts().describe()显示max=1200,说明一个用户一天可产生上千次点击,click_log是事件级粒度。
  • 检查user_iduser_profile中的重复度df_profile['user_id'].nunique() == len(df_profile),说明user_profile是用户级粒度,一对一。
  • 问题定位click_log中存在user_id为空或为'unknown'的脏数据,与user_profileuser_id='unknown'的默认行匹配,形成爆炸式连接。
  • 修复方案
    # 先清洗click_log df_click = df_click[df_click['user_id'].str.len() > 5] # 过滤短ID # 再JOIN merged = df_click.merge(df_profile, on='user_id', how='left')

实操心得:JOIN前必做value_counts().describe(),这是我的铁律。因为粒度不匹配的JOIN,比缺失值更致命。

3.11 问题11:purchase_date字段格式混乱(2023/10/01,01-OCT-2023,20231001),如何统一解析?

技术内核:多格式日期解析、dateutil.parser智能推断、性能优化
为什么考这个:日期是数据链路的脊椎,格式混乱会导致时间序列分析全线崩溃。
实操要点

  • 拒绝pd.to_datetime(errors='coerce'):该方法在遇到01-OCT-2023时会返回NaT,丢失全部信息。
  • dateutil.parser.parse+try/except
    from dateutil import parser def robust_parse_date(x): try: return parser.parse(str(x)) except (ValueError, TypeError): # 尝试常见格式 for fmt in ['%Y%m%d', '%Y/%m/%d', '%d-%b-%Y']: try: return datetime.strptime(str(x), fmt) except ValueError: continue return pd.NaT df['purchase_date_std'] = df['purchase_date'].apply(robust_parse_date)
  • 性能优化:对百万级数据,用vectorize替代apply
    vectorized_parser = np.vectorize(robust_parse_date) df['purchase_date_std'] = vectorized_parser(df['purchase_date'].values)

注意:面试中若只说“用to_datetime”,会被追问“那01-OCT-2023怎么处理?”。必须展示parser.parse的容错能力。

3.12 问题12:用One-Hot Encoding处理了一个有5000个唯一值的product_category字段,模型训练变慢且内存爆了,接下来怎么做?

技术内核:高基数特征处理、Target Encoding、平滑技巧
为什么考这个:One-Hot是初学者的舒适区,但5000维稀疏矩阵会压垮任何生产环境。
实操要点

  • 立即停用One-Hot:5000个唯一值,One-Hot后至少5000列,XGBoost训练内存占用呈O(n²)增长。
  • 改用Target Encoding + 平滑
    # 计算每个category的目标均值(防过拟合) global_mean = df['target'].mean() category_target_mean = df.groupby('product_category')['target'].agg(['mean', 'count']) # 平滑:shrinkage toward global mean alpha = 10 # 经验值,越大越平滑 category_target_mean['smoothed_mean'] = ( (category_target_mean['mean'] * category_target_mean['count'] + global_mean * alpha) / (category_target_mean['count'] + alpha) ) # 映射 df['category_target_enc'] = df['product_category'].map(category_target_mean['smoothed_mean'])
  • 补充策略:对count < 5的长尾category,统一映射为'other',再做Target Encoding。

提示:必须解释alpha的作用——它控制着“相信局部统计”还是“相信全局统计”的权重。这是体现你理解模型本质的关键。

3.13 问题13:user_behavior_seq字段是用户点击序列,如"home,search,product,checkout",如何转换为模型可用特征?

技术内核:序列建模、n-gram特征、TF-IDF向量化
为什么考这个:序列数据是推荐、风控的核心,但如何从字符串序列提取有效特征,是区分水平的试金石。
实操要点

  • 拒绝简单分割df['user_behavior_seq'].str.split(',')只得列表,无法直接喂给模型。
  • TfidfVectorizer提取n-gram
    from sklearn.feature_extraction.text import TfidfVectorizer # 将序列转为字符串(已满足) # 提取2-gram和3-gram tfidf = TfidfVectorizer( analyzer='word', ngram_range=(2, 3), max_features=10000, stop_words=['home'] # 过滤无信息量页面 ) seq_tfidf = tfidf.fit_transform(df['user_behavior_seq'])
  • 高级方案:用gensim训练Word2Vec:将每个页面视为词,用户序列为句子,训练页面Embedding,再用doc2vec聚合序列。但需千万级序列,小数据集用TF-IDF更稳。

注意:面试中要强调“我根据数据量选择方案——百万序列用Word2Vec,十万序列用TF-IDF”。这展示了工程权衡能力。

3.14 问题14:device_id字段有10%的缺失值,且缺失集中在iOS设备,如何处理?

技术内核:缺失机制归因(MNAR)、设备指纹、代理变量构建
为什么考这个:iOS的IDFA限制导致device_id缺失具有强业务含义,不能简单填充。
实操要点

  • 确认iOS缺失原因:查苹果开发者文档,确认IDFA在iOS 14+默认关闭。缺失值即代表“用户拒绝追踪”。
  • 构建代理变量
    • is_ios_no_idfa:布尔值,标记iOS且device_id缺失;
    • ua_fingerprint:从User-Agent提取os_versiondevice_model,生成哈希作为弱设备ID。
  • 特征工程:将is_ios_no_idfa作为独立特征输入模型。业务解读是:“该用户隐私意识强,可能对个性化推荐接受度低”。

实操心得:我曾把iOS缺失值用众数填充,结果模型在iOS用户上AUC暴跌15%。教训是——对受政策影响的缺失,必须当作信号,而非噪声

3.15 问题15:你用SMOTE对少数类过采样,AUC提升了,但业务方投诉预测结果过于激进,为什么?

技术内核:过采样副作用、业务指标与技术指标错位、Precision-Recall权衡
为什么考这个:AUC是技术幻觉,业务要的是精准打击。SMOTE生成的合成样本常导致模型在边界区域过度自信。
实操要点

  • 根本原因:SMOTE在特征空间线性插值,生成的样本可能位于真实少数类簇之外,导致决策边界外扩。
  • 解决方案
    1. 换算法:用ADASYN(自适应合成),它在难分类区域生成更多样本;
    2. 加约束:用imblearn.over_sampling.SMOTE(k_neighbors=3)减小邻域,避免生成远离簇心的样本;
    3. 业务对齐:放弃AUC,改用F1-scorePrecision@Recall=0.8,这些指标迫使模型在召回率约束下优化精准度。
  • 监控:上线后必须监控Precision@Top1000,即预测概率最高的1000个用户中,真实流失的比例。

提示:说出“我用Precision@Recall=0.8替代AUC”就赢了。因为这表明你懂业务语言。

3.16 问题16:user_income字段缺失,但user_educationuser_job_title完整,如何利用它们填充?

技术内核:多变量联合填充、回归预测、不确定性量化
为什么考这个:单一变量填充是下策,利用相关变量做联合预测才是高阶玩法。
实操要点

  • 构建预测模型
    # 用完整样本训练回归模型 X_train = df.dropna(subset=['user_income'])[['user_education', 'user_job_title']] y_train = df.dropna(subset=['user_income'])['user_income'] # 编码分类变量 X_train_encoded = pd.get_dummies(X_train, drop_first=True) model = RandomForestRegressor() model.fit(X_train_encoded, y_train) # 预测缺失值 X_missing = df[df['user_income'].isna()][['user_education', 'user_job_title']] X_missing_encoded = pd.get_dummies(X_missing, drop_first=True) # 对齐列(防止训练/预测列不一致) X_missing_encoded = X_missing_encoded.reindex(columns=X_train_encoded.columns, fill_value=0) df.loc[df['user_income'].isna(), 'user_income'] = model.predict(X_missing_encoded)
  • 关键增强:用model.predict(X_missing_encoded, return_std=True)(若用BayesianRidge)获取预测标准差,作为income_uncertainty特征。

注意:必须强调“我用随机森林而非线性回归,因为它能捕捉教育与职位的交互效应(如博士+程序员 ≠ 博士+教师)”。

3.17 问题17:order_items字段是JSON数组,包含商品ID和数量,如何提取“用户购买品类多样性”特征?

技术内核:JSON解析、集合运算、香农熵计算
为什么考这个:从嵌套结构提取高层次业务特征,是数据科学家的核心能力。
实操要点

  • 解析JSON
    df['item_ids'] = df['order_items'].apply( lambda x: [item['product_id'] for item in json.loads(x)] )
  • 计算品类多样性
    from scipy.stats import entropy def diversity_score(item_list): if not item_list: return 0 # 统计各品类出现频次 counts = Counter(item_list) # 计算香农熵(归一化到0-1) probs = [count / len(item_list) for count in counts.values()]