
简介这份资源是2021年美团商业分析精英大赛的参赛代码压缩包面向具备一定数据分析与编程基础的高校学生、数据科学从业者及商业分析竞赛备赛者可用于复盘完整赛题方案、学习真实业务场景下的分析流程。包内以项目主分支目录为核心通常包含数据预处理脚本、建模代码、结果输出与说明文档等模块覆盖数据清洗、统计检验、机器学习建模、可视化呈现及业务建议撰写等环节帮助读者理解从原始数据到商业洞察的完整链路。压缩包整体约63.57MB文件类型以代码脚本与项目文档为主目录结构清晰便于按分析阶段检索学习。目前已有166人学习下载适合希望借鉴成熟赛题解法、提升数据科学综合实战能力的读者参考。1. 从一份竞赛代码包说起商业分析赛题的工程化拆解思路2021美团商业分析精英大赛参赛代码.zip 这个标题乍看只是一个压缩包的名字但它背后对应的是一类非常典型的工作给定一份业务数据集和若干开放性问题在有限时间内完成数据清洗、特征构造、建模分析和结论输出。很多同学拿到赛题后的第一反应是直接打开 Jupyter Notebook 开始df.head()然后陷入无止境的试错。我带过几届校招生的数据分析实战训练发现真正拉开差距的不是模型多花哨而是有没有一套可复现的工程化流程。这份代码包的价值不在于它用了什么高级算法而在于它展示了一条从原始数据到可交付结论的完整链路。如果你正在准备商业分析类竞赛、或者工作中需要快速对一个陌生数据集给出分析报告这套拆解思路可以直接迁移。下面我会按「理解赛题与数据 → 搭建分析框架 → 核心特征工程 → 建模与验证 → 避坑 → 进阶技巧」的顺序把这类项目的落地路径讲透。2. 赛题理解与数据初探别急着建模先把业务问题翻译成技术目标2.1 商业分析赛题的典型结构与你需要产出的东西美团商业分析精英大赛的赛题通常围绕本地生活服务场景展开比如商户经营诊断、用户行为分析、配送效率优化等方向。赛题会给出若干张业务表订单表、用户表、商户表、评价表等和一组开放性问题。你需要产出的不是单一模型而是一份包含数据洞察、量化结论和策略建议的分析报告代码只是支撑材料。这里有个关键认知商业分析竞赛的评分维度通常包括「业务理解深度」「分析逻辑严谨性」「结论可落地性」和「可视化表达」纯模型精度反而权重不高。所以你的代码结构应该服务于「快速迭代分析结论」而不是追求 SOTA。我一般会把项目目录组织成这样的结构project/ ├── data/ │ ├── raw/ # 原始数据只读不改 │ └── processed/ # 清洗后的中间数据 ├── notebooks/ │ ├── 01_eda.ipynb # 探索性分析 │ ├── 02_feature.ipynb # 特征工程 │ └── 03_model.ipynb # 建模与结论 ├── src/ │ ├── data_loader.py # 统一数据加载 │ ├── features.py # 特征函数 │ └── utils.py # 通用工具 ├── output/ │ ├── figures/ # 图表输出 │ └── report/ # 结论汇总 └── requirements.txt这个结构的好处是数据加载逻辑只写一次所有 notebook 复用特征函数可以单独测试输出目录统一管理最后打包提交不会漏文件。2.2 用 pandas 做第一轮数据体检的固定动作拿到数据后不要直接开始画图。先跑一套标准的数据体检流程把每张表的规模、字段类型、缺失情况、主键唯一性摸清楚。下面是我常用的体检脚本import pandas as pd import numpy as np def data_health_check(df, name): 对单张表做基础体检输出关键信息 print(f{*40}) print(f表名: {name}) print(f行数: {len(df)}, 列数: {df.shape[1]}) print(f内存占用: {df.memory_usage(deepTrue).sum() / 1024**2:.2f} MB) # 字段类型与缺失率 info pd.DataFrame({ dtype: df.dtypes, non_null: df.notnull().sum(), null_rate: (df.isnull().sum() / len(df)).round(4), nunique: df.nunique() }) print(info.to_string()) # 数值列描述统计 num_cols df.select_dtypes(include[np.number]).columns if len(num_cols) 0: print(\n数值列描述统计:) print(df[num_cols].describe().T.to_string()) # 检查潜在主键 for col in df.columns: if df[col].nunique() len(df): print(f\n潜在主键: {col}) return info # 批量体检所有表 tables { order: pd.read_csv(data/raw/order.csv), user: pd.read_csv(data/raw/user.csv), merchant: pd.read_csv(data/raw/merchant.csv), review: pd.read_csv(data/raw/review.csv) } for name, df in tables.items(): data_health_check(df, name)这段代码的核心逻辑是先看规模判断数据量级再看缺失率决定哪些字段可用再看唯一值数量判断主键和外键关系。参数上null_rate超过 0.5 的字段基本可以放弃或需要特殊处理nunique等于行数的字段大概率是主键数值列的describe能快速发现异常值比如金额出现负数、年龄超过 100。体检完你会得到一张「数据可用性地图」接下来才知道哪些分析方向是可行的。比如评价表如果缺失率极高基于文本情感的分析就要慎重。2.3 把业务问题翻译成可计算指标赛题里的问题通常是「如何提升商户经营效率」「哪些因素影响用户复购」这类表述。你需要把它们翻译成可计算的指标。举个例子业务问题可计算指标数据来源商户经营效率日均订单量、客单价、复购率订单表用户复购影响因素复购间隔、品类偏好、评价分数订单表评价表配送效率平均配送时长、超时率订单表商户服务质量评分分布、差评关键词评价表这个翻译过程决定了你后续所有分析的方向。我一般会先列一个「问题-指标-数据」对照表确认每个问题都有数据支撑再开始动手。如果某个问题找不到对应数据要么调整问题表述要么在报告中说明数据局限性。3. 特征工程从原始表到建模宽表的四个关键步骤3.1 时间维度特征商业分析里最容易被低估的金矿本地生活场景的数据几乎都带时间戳而时间特征往往是区分用户行为和商户表现的最强信号。很多人只提取了「星期几」就结束了实际上时间维度的挖掘空间很大。下面是我常用的时间特征构造函数def extract_time_features(df, time_col): 从时间戳列提取多维时间特征 df df.copy() df[time_col] pd.to_datetime(df[time_col]) # 基础时间特征 df[hour] df[time_col].dt.hour df[dayofweek] df[time_col].dt.dayofweek # 0周一 df[is_weekend] df[dayofweek].isin([5, 6]).astype(int) df[day] df[time_col].dt.day df[month] df[time_col].dt.month # 业务时段划分本地生活场景常用 def get_period(h): if 6 h 10: return morning elif 10 h 14: return lunch elif 14 h 17: return afternoon elif 17 h 21: return dinner else: return night df[period] df[hour].apply(get_period) # 是否节假日简化版实际可用 chinese_calendar 库 df[is_holiday] 0 # 需要外部日历数据填充 return df # 应用示例 order pd.read_csv(data/raw/order.csv) order extract_time_features(order, order_time) print(order[[order_time, hour, dayofweek, is_weekend, period]].head())逻辑说明hour和dayofweek是最细粒度的时间切片用于后续聚合is_weekend直接捕捉周末效应period把一天切成五个业务时段比单纯的小时更有业务解释性。参数上时段划分需要根据实际业务调整——外卖场景的午餐高峰在 11-13 点到店场景可能延后到 12-14 点。构造完时间特征后我通常会画一张「时段×星期」的热力图看订单量的分布模式。这张图往往能直接引出分析结论比如「周末晚餐时段订单量是工作日的 2.3 倍」。3.2 聚合特征用 groupby 把行为表压缩成实体画像原始订单表是「一行一单」但建模需要的是「一行一用户」或「一行一商户」。这个压缩过程就是聚合特征工程。核心思路是对每个实体计算其行为的统计量。def build_user_features(order_df, user_df): 构建用户维度聚合特征 # 订单侧聚合 user_order_feat order_df.groupby(user_id).agg( order_count(order_id, count), total_amount(amount, sum), avg_amount(amount, mean), max_amount(amount, max), std_amount(amount, std), first_order_time(order_time, min), last_order_time(order_time, max), merchant_count(merchant_id, nunique), avg_delivery_time(delivery_time, mean) ).reset_index() # 复购间隔 user_order_feat[lifecycle_days] ( pd.to_datetime(user_order_feat[last_order_time]) - pd.to_datetime(user_order_feat[first_order_time]) ).dt.days user_order_feat[order_freq] ( user_order_feat[order_count] / (user_order_feat[lifecycle_days] 1) ) # 与用户基础表合并 user_feat user_df.merge(user_order_feat, onuser_id, howleft) # 填充缺失没有订单的用户 fill_cols [order_count, total_amount, avg_amount, merchant_count, order_freq] user_feat[fill_cols] user_feat[fill_cols].fillna(0) return user_feat user_features build_user_features(order, user) print(f用户特征表: {user_features.shape}) print(user_features.describe().T.to_string())逻辑说明agg里每个字段对应一个统计量count衡量活跃度sum/mean/max/std衡量消费能力nunique衡量探索广度min/max时间戳算出生命周期。order_freq是派生指标用订单数除以生命周期天数比单纯看订单数更能区分「高频短周期」和「低频长周期」用户。参数上需要注意std_amount对只有一单的用户是 NaN填充 0 是合理的表示消费稳定lifecycle_days加 1 是防止除零。这些细节在代码里不写清楚后面建模时就会出现莫名其妙的 NaN 传播。3.3 交叉特征捕捉「谁在什么场景下做了什么」单一维度的聚合只能回答「这个用户消费了多少」交叉特征才能回答「这个用户在工作日午餐时段的消费模式是什么」。交叉特征的做法是先按两个维度分组再聚合。def build_cross_features(order_df): 构建用户×时段的交叉特征 # 用户×时段 订单量 cross order_df.groupby([user_id, period]).agg( period_order_count(order_id, count), period_avg_amount(amount, mean) ).reset_index() # 透视成宽表每行一个用户每列一个时段的订单量 pivot_count cross.pivot_table( indexuser_id, columnsperiod, valuesperiod_order_count, fill_value0 ) pivot_count.columns [forder_{c} for c in pivot_count.columns] # 计算时段偏好集中度熵值 def calc_entropy(row): probs row / (row.sum() 1e-6) probs probs[probs 0] return -np.sum(probs * np.log(probs)) pivot_count[period_entropy] pivot_count.apply(calc_entropy, axis1) return pivot_count.reset_index() cross_features build_cross_features(order) print(cross_features.head())逻辑说明pivot_table把长表转成宽表每个时段变成一列方便后续直接作为模型输入。period_entropy是信息熵衡量用户订单在时段上的分散程度——熵值低说明用户有强时段偏好比如只点午餐熵值高说明全天均匀下单。这个特征在用户分群时非常有用。参数上fill_value0保证没有某时段订单的用户该列为 0熵值计算加1e-6防止 log(0)。交叉特征的数量会随维度增加而膨胀建议控制在 2-3 个维度以内否则特征维度爆炸且解释性下降。3.4 特征筛选用 IV 值和相关性把宽表瘦身构造完特征后你可能会得到几十甚至上百列。直接扔进模型不仅慢还容易过拟合。我一般用两步筛选先算 IV信息量值过滤预测力弱的特征再看相关性去掉冗余特征。def calc_iv(df, feature, target, bins10): 计算单个特征的IV值 df df[[feature, target]].copy() # 数值型分箱 if df[feature].dtype ! object: df[feature] pd.qcut(df[feature], qbins, duplicatesdrop) # 计算WOE和IV grouped df.groupby(feature)[target].agg([count, sum]) grouped.columns [total, bad] grouped[good] grouped[total] - grouped[bad] total_bad grouped[bad].sum() total_good grouped[good].sum() grouped[bad_rate] grouped[bad] / total_bad grouped[good_rate] grouped[good] / total_good grouped[woe] np.log((grouped[good_rate] 1e-6) / (grouped[bad_rate] 1e-6)) grouped[iv] (grouped[good_rate] - grouped[bad_rate]) * grouped[woe] return grouped[iv].sum() # 批量计算IV feature_cols [c for c in user_features.columns if c not in [user_id, target]] iv_dict {col: calc_iv(user_features, col, target) for col in feature_cols} iv_series pd.Series(iv_dict).sort_values(ascendingFalse) # 筛选IV 0.02的特征 selected_features iv_series[iv_series 0.02].index.tolist() print(f筛选后特征数: {len(selected_features)}) print(iv_series.head(20))逻辑说明IV 值衡量特征对目标变量的区分能力一般 IV 0.02 视为弱特征0.02-0.1 为弱预测力0.1-0.3 为中等 0.3 为强。qcut做等频分箱避免数值分布不均导致的分箱偏差。1e-6防止 log 除零。参数上bins10是经验值数据量大时可以增加到 20IV 阈值 0.02 是保守选择如果特征本身不多可以放宽到 0.01。筛选后建议再画一张相关性热力图把相关系数 0.8 的特征对去掉一个保留 IV 更高的那个。4. 建模与验证商业分析场景下模型选择的务实逻辑4.1 为什么我优先选逻辑回归和决策树而不是 XGBoost商业分析竞赛的建模目标和 Kaggle 不同。Kaggle 追求预测精度商业分析追求「可解释的结论」。一个 AUC 0.92 的 XGBoost 模型如果无法解释「为什么这个用户会流失」在评委眼里不如一个 AUC 0.85 但系数清晰的逻辑回归。我的一般策略是先用逻辑回归建立基线看系数方向和显著性确认特征与目标的关系符合业务直觉再用决策树或随机森林做特征重要性排序交叉验证逻辑回归的结论如果时间充裕且数据量足够最后用 XGBoost 提升精度但报告里仍然以可解释模型为主。from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, roc_auc_score import pandas as pd # 准备数据 X user_features[selected_features] y user_features[target] # 划分训练测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 标准化逻辑回归需要 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 逻辑回归基线 lr LogisticRegression(max_iter1000, class_weightbalanced, random_state42) lr.fit(X_train_scaled, y_train) # 输出系数 coef_df pd.DataFrame({ feature: selected_features, coef: lr.coef_[0], odds_ratio: np.exp(lr.coef_[0]) }).sort_values(coef, keyabs, ascendingFalse) print(逻辑回归系数:) print(coef_df.to_string()) print(f\nAUC: {roc_auc_score(y_test, lr.predict_proba(X_test_scaled)[:, 1]):.4f}) # 决策树特征重要性 dt DecisionTreeClassifier(max_depth5, min_samples_leaf50, random_state42) dt.fit(X_train, y_train) importance_df pd.DataFrame({ feature: selected_features, importance: dt.feature_importances_ }).sort_values(importance, ascendingFalse) print(\n决策树特征重要性:) print(importance_df.head(15).to_string())逻辑说明class_weightbalanced处理类别不平衡商业场景中流失用户通常只占少数max_iter1000防止逻辑回归不收敛max_depth5和min_samples_leaf50控制决策树复杂度避免过拟合。系数表里的odds_ratio比原始系数更好解释——OR 1 表示该特征增加会提升目标概率。参数上stratifyy保证训练测试集类别比例一致random_state42保证可复现。如果 AUC 低于 0.7说明特征工程需要返工而不是换模型。4.2 交叉验证与业务验证的双轨制模型验证不能只看测试集 AUC。商业分析场景下我一般做两层验证统计验证用交叉验证看模型稳定性业务验证用「模型结论是否符合常识」来兜底。from sklearn.model_selection import StratifiedKFold # 5折交叉验证 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) cv_scores cross_val_score(lr, X_train_scaled, y_train, cvcv, scoringroc_auc) print(f交叉验证AUC: {cv_scores.mean():.4f} (/- {cv_scores.std():.4f})) print(f各折AUC: {cv_scores.round(4)}) # 业务验证检查高概率用户的特征是否符合直觉 test_result X_test.copy() test_result[pred_prob] lr.predict_proba(X_test_scaled)[:, 1] test_result[actual] y_test.values # 看预测概率最高的10%用户的平均特征 high_risk test_result[test_result[pred_prob] test_result[pred_prob].quantile(0.9)] print(\n高风险用户特征均值:) print(high_risk[selected_features].mean().sort_values(ascendingFalse).head(10))逻辑说明交叉验证的std如果超过 0.05说明模型对数据划分敏感需要检查是否有特征泄露或样本量不足。业务验证是看高风险用户的特征画像——如果模型认为「最近30天无订单」是高风险信号那高风险用户的order_count均值应该显著低于整体否则模型逻辑有问题。参数上n_splits5是标准选择数据量小可以增到 10shuffleTrue打乱顺序避免数据排序偏差。业务验证没有固定阈值靠的是对业务的理解——这一步是纯代码无法替代的。4.3 模型结论到业务建议的翻译模板建模的最终产出不是 AUC 数字而是可执行的业务建议。我一般用「特征方向 量化影响 建议动作」的三段式模板特征系数方向OR值业务含义建议动作近30天订单数负0.62订单越少流失风险越高对30天无订单用户触发召回平均客单价正1.35高客单价用户更忠诚高客单价用户提供专属权益时段熵值负0.78时段偏好分散的用户易流失推送固定时段优惠券培养习惯商户探索数正1.21探索多的用户粘性高新商户推荐给探索型用户这个表格直接放进报告评委一眼就能看懂模型结论和业务动作的对应关系。注意 OR 值的解释OR 0.62 表示该特征每增加一个单位流失 odds 变为原来的 0.62 倍即降低 38%。5. 避坑与排查竞赛代码里最容易翻车的五个地方5.1 数据泄露你的 AUC 0.99 可能是假的现象模型在测试集上 AUC 接近 1.0交叉验证也极高但换一份数据就完全失效。原因特征工程时用了未来信息。比如用「用户总订单数」预测「用户是否会下单」而总订单数本身就包含了预测目标或者在划分训练测试集之前就做了全局标准化导致测试集信息泄露到训练集。解决所有聚合特征必须基于时间窗口。比如预测「未来7天是否复购」特征只能用「过去30天」的数据计算。标准化、分箱、IV计算都要在训练集上 fit再 transform 测试集。检查方法如果某个特征的 IV 值超过 0.5先怀疑泄露。5.2 时间戳解析失败导致特征全空现象pd.to_datetime没有报错但提取的hour、dayofweek全是 NaN 或默认值。原因时间戳格式不统一有的表是2021-01-01 12:00:00有的是20210101120000有的是 Unix 时间戳。pd.to_datetime对混合格式会静默失败。解决先抽样看时间列的原始格式用format参数显式指定。Unix 时间戳用units或unitms。解析后用df[time].isnull().sum()检查失败数量超过 1% 就要排查。# 显式指定格式避免静默失败 df[order_time] pd.to_datetime(df[order_time], format%Y-%m-%d %H:%M:%S, errorscoerce) print(f解析失败数: {df[order_time].isnull().sum()})5.3 groupby 聚合后索引丢失导致 merge 失败现象groupby().agg()之后直接 merge报KeyError或结果行数暴增。原因groupby后的结果索引是分组字段不是默认的 RangeIndex。直接 merge 时 pandas 找不到对应的列。解决聚合后必须.reset_index()把分组字段变回列。merge 前用df.shape和df.columns确认两边都有共同的键列且键列的唯一性符合预期一对多还是多对多。5.4 类别不平衡时模型全预测多数类现象准确率 95%但召回率为 0模型把所有样本都预测为「不流失」。原因流失用户只占 5%模型学到「全预测不流失」就能达到 95% 准确率。准确率在不平衡场景下是误导性指标。解决用class_weightbalanced或 SMOTE 过采样评估指标换成 AUC、F1、召回率在报告中明确说明类别分布和采用的平衡策略。5.5 可视化中文乱码现象matplotlib 图表里的中文全部变成方框。原因默认字体不支持中文。解决在绘图前设置字体Windows 用SimHeiMac 用Arial Unicode MSLinux 用WenQuanYi Micro Hei。同时设置axes.unicode_minusFalse解决负号显示问题。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 按系统替换 plt.rcParams[axes.unicode_minus] False6. 进阶技巧让分析报告从「合格」到「出彩」的两个杠杆6.1 用 SHAP 值做单样本解释把模型黑匣子打开逻辑回归的系数是全局解释但评委经常追问「这个具体用户为什么被判定为高风险」。SHAP 值可以给出每个样本的每个特征贡献度把黑匣子变成可对话的解释。import shap # 用训练好的逻辑回归或树模型 explainer shap.LinearExplainer(lr, X_train_scaled) shap_values explainer.shap_values(X_test_scaled[:100]) # 单样本解释看第0个测试样本 sample_idx 0 sample_shap pd.DataFrame({ feature: selected_features, value: X_test.iloc[sample_idx].values, shap: shap_values[sample_idx] }).sort_values(shap, keyabs, ascendingFalse) print(f样本 {sample_idx} 的预测概率: {lr.predict_proba(X_test_scaled[sample_idx:sample_idx1])[0, 1]:.4f}) print(sample_shap.head(10).to_string())逻辑说明SHAP 值表示该特征对预测结果的贡献正值推高预测概率负值拉低。LinearExplainer适用于逻辑回归树模型用TreeExplainer。参数上X_test_scaled[:100]只解释前100个样本以节省时间实际报告里选3-5个典型案例即可。这个技巧的价值在于你可以在报告里写「用户A被判定为高风险主要因为近30天订单数为0SHAP -0.35和时段熵值偏高SHAP -0.12」这种颗粒度的解释比「模型AUC 0.85」有说服力得多。6.2 用「反事实分析」给出可执行的策略建议反事实分析回答的是「如果把这个特征改变多少预测结果会翻转」。这直接对应业务动作——「把用户的订单频率提升到多少他就不流失了」。def counterfactual_analysis(model, scaler, sample, feature_name, feature_range, target_prob0.5): 对单个样本做反事实分析改变某特征看预测概率变化 sample_df pd.DataFrame([sample] * len(feature_range), columnsselected_features) sample_df[feature_name] feature_range scaled scaler.transform(sample_df) probs model.predict_proba(scaled)[:, 1] result pd.DataFrame({ feature_name: feature_range, pred_prob: probs }) # 找到概率降到目标值以下的最小特征值 below_target result[result[pred_prob] target_prob] if len(below_target) 0: threshold below_target[feature_name].iloc[0] print(f当 {feature_name} 达到 {threshold:.2f} 时流失概率降至 {target_prob}) else: print(f在给定范围内{feature_name} 无法使概率降至 {target_prob}) return result # 示例对高风险用户看订单数提升到多少能降低流失概率 high_risk_sample X_test.iloc[0].to_dict() cf_result counterfactual_analysis( lr, scaler, high_risk_sample, order_count, feature_rangenp.arange(0, 20, 1), target_prob0.5 )逻辑说明构造一组只改变目标特征的样本批量预测后看概率曲线。target_prob0.5是分类阈值实际业务中可以设为更严格的 0.3。参数上feature_range要根据特征的实际分布设定订单数从 0 到 20 是合理范围。这个分析的输出可以直接写进策略建议「对高风险用户将其月订单数从 0 提升到 5 单流失概率可从 0.78 降至 0.45建议发放 5 单立减券组合」。这种量化到具体动作的建议是商业分析报告里最有价值的部分。我自己做这类项目的习惯是代码写完先跑一遍完整流程确认从原始数据到最终结论的每一步都能复现然后把关键中间结果特征表、模型系数、SHAP图单独存到 output 目录写报告时直接引用最后留一个run_all.sh脚本一键跑通全流程。这个习惯让我在多次竞赛和交付中避免了「报告写完但代码跑不通」的尴尬。希望帮到你。本文还有配套的精品资源点击获取