2025MathorCup大数据竞赛B题思路模型详细分析:物流理赔风险识别及服务升级问题,完整论文模型见文末名片
2025年MathorCup赛道B(物流理赔风险识别)思路+模型+代码详解
咱们今天就盯着赛道B的三个问题往深了聊,所有内容都扣着你给的《2025年MathorCup大数据挑战赛-赛道B初赛.pdf》里的要求来------比如文件里明确的"索赔差额公式""三类诉求占比约束""运单特征列表",还有"严重超额样本少"的坑,咱们一步一步拆,从思路到代码都讲明白,不用花里胡哨的术语,就像平时练手一样。
一、先统一认知:B题的核心是"表格数据+业务逻辑"
首先得明确,B题所有任务都基于"结构化运单数据"(就是Excel/CSV表格),文件里的附表1列了所有可用特征------比如运单属性里的"线路类型""保价金额",寄收信息里的"始发城市""寄件人账号",运输情况里的"配送超时时长""异常原因"(比如Pickup Error、Damage这些),还有商品信息里的"商品类型"(Fresh、Electronic Product这些)。
代码工具也不用复杂,就用Python的pandas处理表格、sklearn做特征工程、xgboost/lightgbm建模------这些都是学生最常用的工具,不用搞深度学习(比如CNN、Transformer),纯属浪费时间,表格数据里树模型效果最好,还快。
二、问题1:风险标注模型(给附件1运单分三类)
文件里把这个问题的规则说死了:用"索赔差额"(实际赔付金额 索赔金额)和"实际赔付金额"分"合理诉求""诉求偏高""严重超额",还得满足三个硬约束:
- 严重超额占比<3%,合理诉求≥85%;
- 实际赔付金额越高,需要更高的索赔差额才标"偏高/超额"(比如赔1000块的单,客户多要200可能算偏高;赔100块的,多要50就可能算超额);
- 合理诉求的索赔差额"密集",严重超额的"稀疏"。
1. 核心思路:先摸数据分布,再分档定阈值
别一上来就写代码,先打开附件1(假设是attachment1.csv),看看"实际赔付金额"和"索赔差额"长啥样------这步叫EDA(探索性数据分析) ,是关键。
先算索赔差额:用文件给的公式索赔差额 = 实际赔付金额 索赔金额(注意:客户要多了,这个值会是负数,比如实际赔500,客户要600,差额就是-100);
画直方图:看索赔差额的分布------合理诉求的柱子会堆在中间(比如-50~0之间),严重超额的会散在最左边(比如-200以下);
画散点图:横轴"实际赔付金额",纵轴"索赔差额"------你会发现,赔付金额高的点,纵轴得负很多才是超额,这就是文件说的"赔付越高,差额标准越松"。
然后分档定阈值 :按"实际赔付金额"分几档(比如按25%、50%、75%分位数分成4档:0100、100300、300~800、800+),每档单独算索赔差额的阈值,确保每档都符合"合理≥85%,超额<3%"。
2. 代码实现(手把手教)
第一步:加载数据+计算索赔差额
python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# 加载附件1(假设是CSV格式,实际按竞赛给的格式改,比如Excel)
df = pd.read_csv("attachment1.csv")
# 1. 计算索赔差额(严格按文件公式:实际赔付金额 索赔金额)
df["索赔差额"] = df["实际赔付金额"] df["索赔金额"]
# 看看数据基本情况,避免有缺失值(文件没说有,但得检查)
print("缺失值情况:")
print(df[["实际赔付金额", "索赔金额", "索赔差额"]].isnull().sum())
# 有缺失值的话,比如实际赔付金额缺失,用同商品类型的均值补(结合文件特征)
df["实际赔付金额"] = df.groupby("商品类型")["实际赔付金额"].transform(
lambda x: x.fillna(x.mean())
)
第二步:EDA分析(画分布图)
python
# 设置中文字体(不然乱码)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 索赔差额直方图(看密集/稀疏情况)
plt.figure(figsize=(10, 6))
plt.hist(df["索赔差额"], bins=50, color="lightblue", edgecolor="black")
plt.axvline(x=df[df["索赔差额"].notna()]["索赔差额"].quantile(0.03),
color="red", linestyle="--", label="3%分位数(超额阈值参考)")
plt.axvline(x=df[df["索赔差额"].notna()]["索赔差额"].quantile(0.85),
color="green", linestyle="--", label="85%分位数(合理阈值参考)")
plt.xlabel("索赔差额")
plt.ylabel("运单数量")
plt.title("附件1索赔差额分布(红色=超额参考,绿色=合理参考)")
plt.legend()
plt.savefig("索赔差额分布.png") # 保存图,论文里要用
plt.close()
# 2. 实际赔付金额vs索赔差额散点图(看分档必要性)
plt.figure(figsize=(10, 6))
plt.scatter(df["实际赔付金额"], df["索赔差额"], alpha=0.5, color="orange")
plt.xlabel("实际赔付金额")
plt.ylabel("索赔差额")
plt.title("实际赔付金额与索赔差额关系(看分档需求)")
plt.savefig("赔付金额vs差额.png")
plt.close()
第三步:分档定阈值+标注类别
python
# 1. 给实际赔付金额分档(按分位数分4档,文件没说档数,合理就行)
df["赔付金额档位"] = pd.qcut(
df["实际赔付金额"],
q=[0, 0.25, 0.5, 0.75, 1], # 25%、50%、75%分位数
labels=["0~100", "100~300", "300~800", "800+"] # 实际值按你数据改,比如你数据25%分位数是120,就标0~120
)
# 2. 每档单独算阈值(核心:确保合理≥85%,超额<3%)
def get_threshold_by_bin(bin_data):
# bin_data是某一档的所有数据
total = len(bin_data)
# 严重超额阈值:取3%分位数(≤这个值的是超额,占比≈3%)
excess_threshold = bin_data["索赔差额"].quantile(0.03)
# 合理诉求阈值:取85%分位数(≥这个值的是合理,占比≈85%)
reasonable_threshold = bin_data["索赔差额"].quantile(0.85)
return excess_threshold, reasonable_threshold
# 按档位分组,计算每档阈值
bin_thresholds = {}
for bin_name, bin_data in df.groupby("赔付金额档位"):
excess_th, reasonable_th = get_threshold_by_bin(bin_data)
bin_thresholds[bin_name] = (excess_th, reasonable_th)
print(f"档位{bin_name}:严重超额阈值={excess_th:.2f},合理诉求阈值={reasonable_th:.2f}")
# 3. 给每单标注类别(严格按文件三类)
def label_risk(row):
bin_name = row["赔付金额档位"]
excess_th, reasonable_th = bin_thresholds[bin_name]
diff = row["索赔差额"]
if diff <= excess_th:
return "严重超额"
elif diff >= reasonable_th:
return "合理诉求"
else:
return "诉求偏高"
df["风险标注结果"] = df.apply(label_risk, axis=1)
# 4. 验证占比是否符合文件要求
label_count = df["风险标注结果"].value_counts(normalize=True) * 100
print("\n风险标注占比(需符合文件:合理≥85%,超额<3%):")
print(label_count)
# 若不符合,微调阈值(比如把超额阈值再调低,让超额占比<3%)
# 5. 保存标注结果(文件要求输出附件1的标注结果)
df[["运单号", "实际赔付金额", "索赔金额", "索赔差额", "赔付金额档位", "风险标注结果"]].to_csv(
"attachment1_风险标注结果.csv", index=False
)
3. 关键注意事项(紧扣文件)
别一刀切:必须分档,因为文件明确"实际赔付金额越高,需要更高索赔差额才标偏高/超额",不分档会不符合业务逻辑;
占比硬约束:要是标注后严重超额占了4%,就把该档的"超额阈值"再调低(比如从-150改成-180),直到占比<3%;
保留运单号:文件后续要求"不改变表格中的运单号",所以所有操作都要带着"运单号"列,别丢了。
三、问题2:预测附件2的实际赔付金额(回归任务)
文件要求:用附件1的数据建模型,预测附件2每单的"实际赔付金额",要说明评估指标,结果填到result文件,不改运单号。这是典型的表格数据回归任务,核心是"特征工程"(文件里的特征要用好)。
1. 核心思路:特征工程>模型
文件里的附表1给了19个特征(比如"是否为c2c""保价金额""异常原因""商品类型"),这些都是建模的关键,思路分三步:
- 特征筛选:删没用的(比如"运单号",唯一值,无意义),留所有业务相关特征(比如"保价金额"------保价高的肯定赔得多,符合文件里"保价金额是申报价值"的定义);
- 特征处理 :
分类特征:比如"商品类型"(Fresh/Electronic)、"异常原因"(Damage/Lost),用One-Hot编码(模型不认文字);
缺失值:比如"保价金额"缺失,用同"商品类型+线路类型"的均值补(结合文件里的业务关联);
异常值:比如实际赔付金额10万(明显是珠宝类高价商品),用99%分位数"盖帽"(避免带偏模型); - 模型选择:用XGBoost/LightGBM(树模型抗干扰强,还能输出特征重要性,方便论文分析------比如"保价金额"是Top1特征,符合业务常识)。
2. 代码实现
第一步:加载数据+特征筛选
python
# 加载附件1(带风险标注的,用来训练)和附件2(要预测的)
df_train = pd.read_csv("attachment1_风险标注结果.csv") # 附件1,有实际赔付金额(标签)
df_test = pd.read_csv("attachment2.csv") # 附件2,要预测实际赔付金额
# 1. 筛选特征(按文件附表1,删没用的,留业务相关的)
useless_features = ["运单号", "赔付金额档位", "风险标注结果"] # 运单号要保留,最后输出用,先不删
feature_cols = [col for col in df_train.columns if col not in useless_features + ["实际赔付金额"]]
print("建模特征(来自文件附表1):", feature_cols)
# 2. 分离X(特征)和y(标签:实际赔付金额)
X_train = df_train[feature_cols].copy()
y_train = df_train["实际赔付金额"].copy()
X_test = df_test[feature_cols].copy()
test_order_id = df_test["运单号"].copy() # 保留运单号,最后输出
第二步:特征工程(关键!)
python
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
# 1. 区分特征类型(按文件附表1)
# 分类特征:比如商品类型、异常原因、进线渠道、是否为c2c(0/1也是分类)
cat_features = ["商品类型", "异常原因", "进线渠道", "是否为c2c", "是否生鲜妥投及时", "寄件是否内部", "新旧程度", "进线人身份", "寄件B/C"]
# 数值特征:比如保价金额、配送超时时长、妥投到进线时长、网点统计特征(始发网点发单量等)
num_features = [col for col in feature_cols if col not in cat_features]
# 2. 分类特征处理:填充缺失值(用众数)+ One-Hot编码
cat_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")), # 分类特征缺失用众数补
("onehot", OneHotEncoder(handle_unknown="ignore")) # 忽略测试集新类别
])
# 3. 数值特征处理:填充缺失值(用均值)+ 标准化(让模型收敛快)
num_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="mean")), # 数值特征缺失用均值补
("scaler", StandardScaler()) # 标准化
])
# 4. 合并所有特征处理(ColumnTransformer按特征类型分别处理)
preprocessor = ColumnTransformer(
transformers=[
("num", num_transformer, num_features),
("cat", cat_transformer, cat_features)
])
第三步:建模+训练+评估
python
from sklearn.model_selection import train_test_split
from xgboost import XGBRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error
# 1. 拆分训练集和验证集(用附件1的80%训练,20%验证,别全用)
X_train_split, X_val_split, y_train_split, y_val_split = train_test_split(
X_train, y_train, test_size=0.2, random_state=2025
)
# 2. 建模型 pipeline(预处理+XGB回归)
model = Pipeline(steps=[
("preprocessor", preprocessor),
("regressor", XGBRegressor(
n_estimators=100, # 树的数量,可调
max_depth=5, # 树深度,防过拟合
learning_rate=0.1, # 学习率
random_state=2025
))
])
# 3. 训练模型
model.fit(X_train_split, y_train_split)
# 4. 验证模型(文件要求说明评估指标)
y_val_pred = model.predict(X_val_split)
mae = mean_absolute_error(y_val_split, y_val_pred)
rmse = np.sqrt(mean_squared_error(y_val_split, y_val_pred))
print(f"\n模型评估(附件1验证集):")
print(f"MAE(平均绝对误差):{mae:.2f} 元") # 直观,比如平均差25元
print(f"RMSE(均方根误差):{rmse:.2f} 元") # 对大误差敏感,符合文件"控制成本"需求
# 5. 用全量附件1训练模型(最后预测用)
model.fit(X_train, y_train)
# 6. 预测附件2的实际赔付金额
df_test["预测实际赔付金额"] = model.predict(X_test)
# 7. 保存结果(文件要求填到result文件,不改变运单号)
result_df = pd.DataFrame({
"运单号": test_order_id,
"预测实际赔付金额": df_test["预测实际赔付金额"].round(2) # 保留2位小数,符合金额格式
})
result_df.to_csv("result_问题2_预测赔付金额.csv", index=False)
3. 关键注意事项(紧扣文件)
特征别漏:文件附表1里的"网点统计特征"(比如始发网点万单理赔率)很重要------理赔率高的网点,赔付金额可能更高,别漏了;
评估指标:必须说MAE和RMSE,文件没指定,但这俩最符合"理赔成本控制"的业务目标(大误差会导致多赔或少赔);
结果格式:预测金额保留2位小数(金额格式),运单号顺序别乱,文件后续要合并问题2和3的结果。
四、问题3:风险分类预测+方法对比(B题最难)
文件要求:用问题1的标注规则(附件1的三类结果)建分类模型,预测附件2的风险类别;还要说清"严重超额样本少"的处理方法,对比"直接分类"和"先预测赔付再分类(问题2方法)"的优劣势。
1. 核心思路:先解决不平衡,再建模对比
文件明确"严重超额占比通常<3%",样本极不平衡------直接建模会漏判超额单(模型只认多数类"合理诉求"),所以第一步是处理样本不平衡,然后建分类模型,最后对比两种方法。
两种方法的逻辑:
直接分类:用附件1的"特征+风险标注结果"建分类模型,直接预测附件2的类别;
先预测赔付再分类:用问题2预测的"实际赔付金额",结合问题1的分档阈值,给附件2标类别(比如预测赔付500,按300~800档的阈值标类别)。
2. 代码实现
第一步:处理样本不平衡+建分类模型
python
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, f1_score
from imblearn.over_sampling import SMOTE # 处理不平衡的SMOTE算法
from sklearn.model_selection import cross_val_score
# 1. 准备分类数据(X用问题2的特征,y用问题1的风险标注结果)
X_classify = X_train.copy() # 和问题2的特征一样
y_classify = df_train["风险标注结果"].copy()
# 2. 处理"严重超额样本少"(文件核心难点)
# 方法1:SMOTE过采样(造严重超额的假样本,平衡三类)
smote = SMOTE(random_state=2025, sampling_strategy="minority") # 只给少数类(严重超额)过采样
X_resampled, y_resampled = smote.fit_resample(preprocessor.fit_transform(X_classify), y_classify)
print(f"过采样前样本数:{y_classify.value_counts()}")
print(f"过采样后样本数:{pd.Series(y_resampled).value_counts()}") # 三类样本数接近
# 3. 建分类模型(用随机森林,简单易解释)
classifier = RandomForestClassifier(
n_estimators=100,
max_depth=6,
class_weight="balanced", # 再给少数类加权重,双重保险
random_state=2025
)
# 4. 交叉验证(看模型稳定性,避免过拟合)
cv_scores = cross_val_score(
classifier, X_resampled, y_resampled,
cv=5, scoring="f1_macro" # 用F1,不用准确率(不平衡数据准确率骗人)
)
print(f"\n5折交叉验证F1分数:{cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
# 5. 训练模型+预测附件2
classifier.fit(X_resampled, y_resampled)
# 先对附件2的特征做预处理(和训练时一致)
X_test_processed = preprocessor.transform(X_test)
df_test["直接分类_风险预测结果"] = classifier.predict(X_test_processed)
# 6. 评估模型(用附件1的验证集)
X_val_processed = preprocessor.transform(X_val_split)
y_val_pred_class = classifier.predict(X_val_processed)
print("\n附件1验证集分类评估(重点看严重超额的F1):")
print(classification_report(df_train.loc[X_val_split.index, "风险标注结果"], y_val_pred_class))
第二步:实现"先预测赔付再分类"(问题2方法)
python
# 1. 用问题2的预测结果(附件2的预测实际赔付金额)
df_test["预测赔付金额档位"] = pd.qcut(
df_test["预测实际赔付金额"],
q=[0, 0.25, 0.5, 0.75, 1],
labels=["0~100", "100~300", "300~800", "800+"] # 和问题1的档位一致
)
# 2. 用问题1的阈值,给附件2标类别(先预测赔付再分类)
def label_by_pred_payout(row):
bin_name = row["预测赔付金额档位"]
# 用问题1算好的阈值(bin_thresholds是问题1里的字典)
excess_th, reasonable_th = bin_thresholds[bin_name]
# 索赔差额=预测实际赔付金额 客户索赔金额(附件2里有"索赔金额")
diff = row["预测实际赔付金额"] row["索赔金额"]
if diff <= excess_th:
return "严重超额"
elif diff >= reasonable_th:
return "合理诉求"
else:
return "诉求偏高"
df_test["先预测赔付_风险预测结果"] = df_test.apply(label_by_pred_payout, axis=1)
第三步:合并结果+对比两种方法
python
# 1. 合并问题2和3的结果(文件要求放一个result文件)
final_result = pd.DataFrame({
"运单号": test_order_id,
"预测实际赔付金额": df_test["预测实际赔付金额"].round(2),
"直接分类_风险类别": df_test["直接分类_风险预测结果"],
"先预测赔付_风险类别": df_test["先预测赔付_风险预测结果"]
})
final_result.to_csv("result_问题2+3_最终结果.csv", index=False)
# 2. 对比两种方法的优劣势(紧扣文件业务,论文里要写)
print("\n两种方法优劣势对比(基于文件业务目标:控制成本+减少人工):")
print("1. 直接分类(问题3方法):")
print(" 优势:无误差传递(不用先预测赔付,少一次错),严重超额识别准(SMOTE处理后F1高),控成本好;")
print(" 劣势:解释性差(业务人员问为啥是超额,只能说模型算的,不符合文件"清晰策略"需求);")
print("2. 先预测赔付再分类(问题2方法):")
print(" 优势:符合业务流程(文件里理赔就是先算该赔多少,再判合理,解释性强),合理诉求识别准;")
print(" 劣势:误差传递(赔付预测错了,分类也错),严重超额漏判率高(比如赔付预测低了,按低档位阈值标,漏超额);")
3. 关键注意事项(紧扣文件)
不平衡处理:必须说SMOTE+类别权重,文件明确"严重超额样本占比严重不均衡",这是得分点;
对比别空谈:要结合文件里的"控制理赔成本""减少人工审核"------比如直接分类控成本好,先预测赔付减少人工争议;
结果合并:文件要求"问题2、3的结果放在一个result文件",所以最后要合并成一个CSV,别分开。
五、最后给学生的实战建议
- 别等附件1/2: 竞赛里附件会晚给,但可以先写好代码框架(比如特征处理、分档函数),等数据来了改改列名就能用;
- 紧扣文件: 所有步骤都要对应文件里的规则(比如索赔差额公式、占比约束),别自己造逻辑;
- 论文要配图: 问题1的分布图、问题2的特征重要性图、问题3的分类混淆矩阵,这些能让论文更充实,符合竞赛要求。
B题不难,但"细"------把文件里的特征、规则吃透,代码一步一步来,拿分很稳;要是脱离文件瞎建模,再复杂也没用。