2023年天府杯全国大学生数学建模竞赛

B题 跨境电商新品爆款潜力识别及销量预测研究

原题再现:

  问题背景:
  在跨境电商领域,商品的生命周期短、市场风向变化快,能否在商品上架初期准确识别出具有“爆款”潜力的商品,直接关系到平台的库存周转效率、现金流健康度和商户的备货决策。然而,新品上市初期(上架后前 7 天)往往只有有限的流量和转化数据,传统的基于长期销售历史的预测方法难以适用。平台需要一套能够在 上架后第 7 天 即可对未来一个月销售走势做出预判的数据模型,从而提前安排采购备货和营销资源倾斜。某跨境电商平台新上架了 1000款商品,现提供每款商品上架后 连续7天 的运营数据,以及历史爆款商品的参考样本,要求完成潜力商品的早期识别和销量预测任务。爆款定义标准为上架后第15天至第45天之间,任意连续7天的日均销量 ≥ 100件。
  需要解决的问题:
  问题 1:请从 1000 款商品前 7 天的运营数据中提取能够刻画“流量获取能力”“转化效率”“用户粘性”及“增长趋势”的核心特征,采用合适的聚类算法对商品进行分群,利用肘部法则和轮廓系数确定最佳聚类数,对各群进行特征画像分析;基于 200 款历史爆款样本建立爆款判别准则,从1000款商品中筛选出最有可能成为爆款的候选商品群,并以可视化方式呈现聚类结果。
  问题2: 针对问题1筛选出的候选爆款商品,分析其前7天销量的变化趋势特征并据此选择恰当的预测模型,建立第 15 天至第 45天共 31 天的逐日销量预测模型,给出每款候选商品的预测销量及95%置信区间,并汇总候选群的总备货需求量;选取至少两款候选商
品,将预测结果与历史爆款真实生命周期曲线进行对比验证。
  问题3: 请分析问题1中聚类筛选阈值的宽严变化对问题2预测总需求量的影响,通过模拟实验给出量化结论;讨论从“早期行为识别”到“未来销量预测”完整流程中误差的主要来源及传播路径,并提出至少两条可提升整体建模稳定性的改进策略。

整体求解过程概述(摘要)

  针对问题一,为了在仅有上架前7天行为数据的条件下构造可解释的爆款早期识别机制,首先对新品运营表与历史生命周期表进行主键关联、完整性审查、逻辑一致性检验和冗余变量诊断。结果表明,新品数据包含1000款商品共7000条日记录,历史参考数据包含200款商品第8—45天共7600条记录,两表存在200个可一一匹配的商品ID;所有字段均无缺失、无重复主键,且支付金额恒等于“价格×支付订单数”,故将该共线关系从核心行为特征中剔除。随后围绕“流量获取能力、转化效率、用户粘性、增长趋势”构造比率、对数趋势斜率、稳定度等指标,采用1%—99%分位Winsorize抑制长尾影响,并引入CRITIC客观赋权形成四维综合指数。基于标准化指数使用KMeans聚类,肘部法在k=4处出现明显折点,轮廓系数在k=2时最高但只能形成粗粒度二分;综合结构解释力与群体画像后选取k=4。进一步依据题目定义重新计算200个历史样本的真实标签,其中53款满足“第15—45天任意连续7天日均销量≥100件”,147款不满足。使用Logistic、随机森林、ExtraTrees与RBF-SVM建立判别模型,并以5折OOF软投票给出爆款概率,AUC=0.977、F1=0.879。最终锁定历史爆款率为48.28%的核心高潜群,并以Youden阈值0.397二次筛选,从118款待测高潜商品中得到62款候选爆款。
  针对问题二,鉴于问题一已经完成数据清洗与行为特征构造,后续不再重复进行缺失值和异常值处理,而是在相同商品级特征基础上进一步引入D1—D7支付订单序列、对数销量斜率、波动系数与趋势拟合优度,用以刻画短序列的水平、方向和稳定性。考虑到训练样本仅200款、预测目标却为第15—45天31个连续日销量,若逐日独立建模会忽略各预测步长之间的共同生命周期结构,因此构建对log(1+y)目标进行联合学习的多输出树模型,并比较Ridge、KNN、随机森林和ExtraTrees。5折交叉验证显示,RF-ET集成模型的WAPE=0.244、31日总量MAPE=0.093、总量Spearman相关系数=0.981,明显优于线性与邻近基线。为了避免仅给出点预测造成备货决策过度确定化,在OOF残差上引入有限样本Conformal校准,形成逐日95%预测区间;对62款候选商品的第15—45天总预测销量为292,811件,基于残差自助模拟得到总需求95%区间[288,763, 319,657]件。
  针对问题三,为了量化“筛得更宽或更严”对后续采购量的影响,在保持高潜聚类不变的基础上,将监督判别概率阈值从0.25连续提高至0.80,并对每一个阈值重新形成候选集合、汇总31日销量,再利用历史OOF总量残差进行Monte Carlo抽样,从而同时考虑候选集合变化和单品预测误差。模拟结果呈现清晰的阶梯效应:阈值越严格,候选数与预测总需求总体单调下降;在基准阈值附近,小幅收紧阈值造成的需求下降相对有限,而当阈值进入高概率区间后,边际剔除商品对应的需求损失明显扩大。进一步构建“数据测量误差→特征构造误差→聚类边界误差→爆款判别误差→销量预测误差→总备货偏差”的传播链,指出候选集合选择误差与高增长商品的长步长低估是两类最主要风险,并提出交叉拟合、概率校准、滚动更新、分层安全库存和模型集成等改进策略。
  综合三问,本文形成了一套由早期行为刻画、无量纲客观赋权、群体聚类、监督判别、31日联合预测、区间量化和阈值压力测试组成的闭环模型。该方法既利用聚类保持了商品类型的可解释性,又利用带标签历史样本提高爆款识别准确度;同时通过多输出集成与Conformal区间把“是否值得重点投入”和“应当准备多少库存”连接在同一决策链中。模型对当前附件数据具有较好的判别与排序能力,且输出形式可直接服务于跨境电商的采购、投放和库存分层管理。

模型假设:

  假设1:附件中同一商品ID在两张表中表示同一款商品。由于200个历史ID与新品运营表ID完全可匹配,故将其D1—D7运营行为与D8—D45销量拼接为完整历史样本。
  假设2:支付订单数可作为前7天“日销量”的一致代理。附件未提供单笔订单件数,因此按照每笔支付订单对应一件核心销售单位处理;该假设只用于短期趋势刻画,不改变题目对D15—D45历史真实日销量的爆款定义。
  假设3:第7天时能够获得的变量均可用于预测,且不使用第8天之后信息构造输入特征,从而避免时间穿越与数据泄漏。
  假设4:同一商品前7天的类目、价格、包邮、主图视频和活动状态为静态属性。数据检查显示1000款商品内部这些字段均保持一致,因此可按商品级保留一份。
  假设5:浏览量、访客数、加购、支付、收藏和评价的极端大值主要反映真实高热度而非录入错误。为防止极端值支配距离,仅对用于综合指数的连续指标进行1%—99%分位Winsorize,而不直接删除商品。
  假设6:200个历史样本与待预测800款新品来自相同或相近运营机制,满足交叉验证、Conformal校准所需要的近似交换性。若平台流量机制或市场环境发生结构突变,需要滚动更新模型。

问题分析:

  问题一分析
  本题属于半监督框架下跨境电商新品爆款早期识别建模问题,是整套 “潜力识别‑销量预测‑备货决策” 体系的上游筛选模块。核心难点:仅有上架前 7 天短周期运营行为,缺少中后期销量信息;原始浏览、加购、支付指标存在强漏斗共线性,直接输入聚类会造成信息重复加权;题目爆款定义依赖第 15‑45 天连续 7 天日均销量,不能直接使用文件标签;聚类仅完成群体分群,需要结合带标签历史样本实现个体判别。建模思路:完成主键关联、完整性、逻辑一致性数据校验,构造流量获取、转化效率、用户粘性、增长趋势四维业务特征,采用 1%‑99% 分位 Winsorize 缩尾抑制长尾效应,利用 CRITIC 客观赋权得到四维综合能力指数;使用肘部法、轮廓系数、Calinski‑Harabasz 指标联合确定 KMeans 聚类数,完成商品群体画像;依托 200 条可匹配历史样本严格按照赛题定义生成真实爆款标签,搭建 Logistic、随机森林、ExtraTrees、RBF‑SVM 多模型,采用分层 5 折 OOF 软投票输出爆款概率;以 Youden 指数确定最优判别阈值,采用 “高潜聚类 + 概率门槛” 两级筛选得到候选爆款集合,输出候选商品清单,为下游销量预测划定处理对象。
  问题二分析
  本题属于小样本短输入多步长联合销量预测问题,完全继承问题一清洗数据集与商品级特征,面向筛选后的候选爆款开展第 15‑45 天逐日销量预测。核心难点:单款商品仅 7 个早期观测点,传统时序模型参数估计不稳定;样本量有限,深度序列模型易过拟合;不仅需要点预测,还要量化备货所需的不确定性区间;存在部分商品后期突发加速增长,模型容易回归均值低估需求。建模思路:在已有特征基础上补充 D1‑D7 订单序列、对数增长斜率、波动系数、趋势拟合优度等序列统计特征;对比 Ridge、KNN、随机森林、ExtraTrees 多输出回归模型,选用 RF‑ET 集成模型对 log (1+y) 变换目标进行 31 维联合学习;基于 OOF 残差采用 Conformal 非参数校准,得到逐日 95% 预测区间;利用历史残差 Monte Carlo 抽样得到候选群体总需求 95% 模拟区间;设计爆款概率‑预测销量‑不确定度三维分层备货决策框架,选取历史爆款做伪新品回测检验泛化性能,输出单品逐日预测结果、总需求以及运营分层建议,为库存采购提供量化依据。
  问题三分析
  本题属于决策阈值敏感性与全链路误差传播分析问题,固定前两问训练完成的模型,仅改变爆款判别阈值开展压力测试。核心难点:阈值变化带来候选集合是 0‑1 离散跳变,不是连续微小扰动;误差沿着数据‑特征‑聚类‑判别‑预测逐层传递,分为候选集合选择离散误差与单品销量连续预测误差两类;需要给出可落地的稳定性改进策略。建模思路:保持高潜聚类划分不变,将概率阈值在 0.25‑0.80 区间扫描,每个阈值下重构候选集合,读取对应商品预测销量;对每组候选集合做残差蒙特卡洛模拟,得到不同阈值下总备货需求以及模拟 95% 区间,量化候选数、总需求随筛选宽严的阶梯变化规律;结构化拆解从原始观测到最终备货偏差完整误差传播链,区分离散集合选择误差和连续预测误差;针对边界漏筛误判、长周期低估、样本不平衡等风险,提出阈值缓冲人工复核、D8‑D14 滚动更新预测、概率校准、不确定性分层安全库存、模型漂移监控五条改进策略,完成从模型输出向商业库存决策的闭环转化。

模型的建立与求解整体缩略图

在这里插入图片描述

部分程序代码:

from __future__ import annotations
import argparse, json, math, warnings
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import gridspec, patches, font_manager
from matplotlib.colors import LinearSegmentedColormap
from sklearn.preprocessing import StandardScaler, MinMaxScaler, OneHotEncoder
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import (
    silhouette_score, calinski_harabasz_score, davies_bouldin_score,
    roc_auc_score, average_precision_score, f1_score, recall_score, precision_score,
    confusion_matrix, roc_curve, precision_recall_curve
)
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.model_selection import StratifiedKFold, KFold, cross_val_predict
from sklearn.linear_model import LogisticRegression, Ridge
from sklearn.ensemble import (
    RandomForestClassifier, ExtraTreesClassifier,
    RandomForestRegressor, ExtraTreesRegressor
)
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsRegressor
from scipy.stats import spearmanr

warnings.filterwarnings('ignore')
RANDOM_STATE = 42

# ---------- 图形风格 ----------
FONT_PATH='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'
if Path(FONT_PATH).exists():
    try:
        font_manager.fontManager.addfont(FONT_PATH)
        _zhfont=font_manager.FontProperties(fname=FONT_PATH).get_name()
        plt.rcParams['font.family']=_zhfont
        plt.rcParams['font.sans-serif']=[_zhfont]
    except Exception:
        plt.rcParams['font.sans-serif']=['DejaVu Sans']
else:
    plt.rcParams['font.sans-serif']=['DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['figure.dpi'] = 150
plt.rcParams['savefig.dpi'] = 320
plt.rcParams['axes.titleweight'] = 'bold'

PALETTE = ['#2F5597','#70AD47','#ED7D31','#A5A5A5','#5B9BD5','#C55A11','#8064A2','#4BACC6']

def savefig(fig, path: Path):
    fig.savefig(path, bbox_inches='tight', facecolor='white')
    plt.close(fig)


def safe_ratio(a, b):
    return float(a) / (float(b) + 1e-8)


def lin_slope(y, log=False):
    y = np.asarray(y, dtype=float)
    if log:
        y = np.log1p(np.clip(y, 0, None))
    x = np.arange(1, len(y)+1, dtype=float)
    return float(np.polyfit(x, y, 1)[0])


def trend_r2(y):
    y = np.asarray(y, dtype=float)
    x = np.arange(1, len(y)+1, dtype=float)
    c = np.polyfit(x, y, 1)
    p = np.polyval(c, x)
    sst = np.sum((y-y.mean())**2)
    return 0.0 if sst < 1e-12 else float(1 - np.sum((y-p)**2)/sst)


def load_data(new_path, hist_path):
    new = pd.read_csv(new_path, encoding='utf-8-sig')
    hist = pd.read_csv(hist_path, encoding='utf-8-sig')
    return new, hist


def data_quality(new, hist):
    static = ['类目','价格','是否包邮','是否有主图视频','是否参加活动']
    q = {
        '新品行数': len(new), '新品商品数': int(new['商品ID'].nunique()),
        '历史行数': len(hist), '历史商品数': int(hist['商品ID'].nunique()),
        'ID交集数': len(set(new['商品ID']) & set(hist['商品ID'])),
        '新品缺失值': int(new.isna().sum().sum()), '历史缺失值': int(hist.isna().sum().sum()),
        '新品重复键': int(new.duplicated(['商品ID','上架天数']).sum()),
        '历史重复键': int(hist.duplicated(['商品ID','上架天数']).sum()),
        '静态属性不一致商品数': int(sum((new.groupby('商品ID')[c].nunique()>1).sum() for c in static)),
        '支付金额=价格×订单数比例': float(np.mean(new['支付金额'] == new['价格']*new['支付订单数'])),
        '逻辑异常_访客>浏览': int((new['访客数']>new['浏览量']).sum()),
        '逻辑异常_订单>加购': int((new['支付订单数']>new['加购次数']).sum()),
        '逻辑异常_评价>订单': int((new['评价数']>new['支付订单数']).sum()),
    }
    return q


def build_product_features(new):
    metrics = ['浏览量','访客数','加购次数','支付订单数','支付金额','收藏数','评价数']
    rows=[]
    for pid,g in new.groupby('商品ID'):
        g=g.sort_values('上架天数')
        r={'商品ID':pid,'类目':g['类目'].iloc[0],'价格':float(g['价格'].iloc[0]),
           '是否包邮':int(g['是否包邮'].iloc[0]),'是否有主图视频':int(g['是否有主图视频'].iloc[0]),
           '是否参加活动':int(g['是否参加活动'].iloc[0])}
        for m in metrics:
            y=g[m].to_numpy(float)
            r[m+'_均值']=y.mean(); r[m+'_总量']=y.sum()
            r[m+'_变异系数']=y.std(ddof=0)/(y.mean()+1e-8)
            r[m+'_对数斜率']=lin_slope(y,log=True)
            r[m+'_趋势R2']=trend_r2(np.log1p(y))
            r[m+'_首末比']=safe_ratio(y[-1]+1,y[0]+1)
        r['浏览深度']=safe_ratio(g['浏览量'].sum(),g['访客数'].sum())
        r['加购率']=safe_ratio(g['加购次数'].sum(),g['访客数'].sum())
        r['支付转化率']=safe_ratio(g['支付订单数'].sum(),g['访客数'].sum())
        r['加购支付转化率']=safe_ratio(g['支付订单数'].sum(),g['加购次数'].sum())
        # 支付金额严格等于价格×支付订单数,故客单价与价格完全共线,不重复用于聚类
        r['收藏率']=safe_ratio(g['收藏数'].sum(),g['访客数'].sum())
        r['评价率']=safe_ratio(g['评价数'].sum(),g['支付订单数'].sum())
        r['互动率']=safe_ratio((g['加购次数']+g['收藏数']).sum(),g['访客数'].sum())
        r['流量增长率']=np.exp(lin_slope(g['访客数'],log=True))-1
        r['销量增长率']=np.exp(lin_slope(g['支付订单数'],log=True))-1
        r['加购增长率']=np.exp(lin_slope(g['加购次数'],log=True))-1
        r['收藏增长率']=np.exp(lin_slope(g['收藏数'],log=True))-1
        r['销量趋势稳定度']=trend_r2(np.log1p(g['支付订单数'].to_numpy()))
        r['流量趋势稳定度']=trend_r2(np.log1p(g['访客数'].to_numpy()))
        for d,val in zip(g['上架天数'], g['支付订单数']):
            r[f'销量D{int(d)}']=float(val)
        rows.append(r)
    return pd.DataFrame(rows).set_index('商品ID')


def build_history_label(hist):
    label_rows=[]; future_rows=[]
    for pid,g in hist.groupby('商品ID'):
        g=g.sort_values('上架天数')
        s=g[(g['上架天数']>=15)&(g['上架天数']<=45)].set_index('上架天数')['日销量']
        max7=float(s.rolling(7).mean().max())
        label_rows.append([pid,int(max7>=100),max7])
        future_rows.append(pd.Series({f'Y{d}':float(s.get(d,np.nan)) for d in range(15,46)},name=pid))
    labels=pd.DataFrame(label_rows,columns=['商品ID','爆款标签','最大7日均销量']).set_index('商品ID')
    future=pd.DataFrame(future_rows)
    return labels, future


def critic_scores(feat):
    groups={
        '流量获取能力':['访客数_均值','浏览量_均值','浏览深度','流量增长率'],
        '转化效率':['加购率','支付转化率','加购支付转化率','支付订单数_趋势R2'],
        '用户粘性':['收藏率','评价率','互动率','浏览深度'],
        '增长趋势':['销量增长率','流量增长率','加购增长率','收藏增长率','销量趋势稳定度'],
    }
    scores=pd.DataFrame(index=feat.index); weights={}; processed={}
    for name, cols in groups.items():
        X=feat[cols].copy()
        for c in cols:
            lo,hi=X[c].quantile([0.01,0.99]); X[c]=X[c].clip(lo,hi)
        Xn=pd.DataFrame(MinMaxScaler().fit_transform(X),index=X.index,columns=cols)
        std=Xn.std(ddof=0); R=Xn.corr().abs().fillna(0)
        C=std*(1-R).sum(axis=1)
        w=pd.Series(1/len(cols),index=cols) if C.sum()==0 else C/C.sum()
        scores[name]=(Xn*w).sum(axis=1)
        weights[name]=w; processed[name]=Xn
    return scores, weights, groups, processed


def select_k_and_cluster(scores):
    Z=StandardScaler().fit_transform(scores)
    records=[]; models={}
    for k in range(2,9):
        km=KMeans(n_clusters=k,n_init=60,random_state=RANDOM_STATE).fit(Z)
        records.append([k,km.inertia_,silhouette_score(Z,km.labels_),
                        calinski_harabasz_score(Z,km.labels_),davies_bouldin_score(Z,km.labels_)])
        models[k]=km
    met=pd.DataFrame(records,columns=['k','SSE','轮廓系数','CH指数','DBI'])
    x=(met.k-met.k.min())/(met.k.max()-met.k.min())
    y=(met.SSE-met.SSE.min())/(met.SSE.max()-met.SSE.min())
    x1,y1=x.iloc[0],y.iloc[0]; x2,y2=x.iloc[-1],y.iloc[-1]
    d=np.abs((y2-y1)*x-(x2-x1)*y+x2*y1-y2*x1)/np.sqrt((y2-y1)**2+(x2-x1)**2)
    elbow=int(met.loc[d.idxmax(),'k']); sil=int(met.loc[met['轮廓系数'].idxmax(),'k'])
    # 若肘部与轮廓峰值差异明显,兼顾结构细分解释力:以SSE拐点为主,同时检查DBI是否继续改善
    kstar=elbow
    km=models[kstar]
    return Z, met, elbow, sil, kstar, km.labels_, km


def classification_model(feat, labels):
    train_ids=labels.index.intersection(feat.index)
    num=['流量获取能力','转化效率','用户粘性','增长趋势','价格','是否包邮','是否有主图视频','是否参加活动',
         '浏览量_均值','访客数_均值','加购率','支付转化率','收藏率','评价率','销量增长率','流量增长率',
         '支付订单数_变异系数','支付订单数_趋势R2']
    X=feat.loc[train_ids,num+['类目']].copy(); y=labels.loc[train_ids,'爆款标签'].astype(int)
    pre=ColumnTransformer([('num',StandardScaler(),num),('cat',OneHotEncoder(handle_unknown='ignore'),['类目'])])
    models={
        'Logistic':LogisticRegression(max_iter=3000,class_weight='balanced',C=1.0),
        'RandomForest':RandomForestClassifier(n_estimators=140,min_samples_leaf=3,class_weight='balanced_subsample',random_state=RANDOM_STATE,n_jobs=-1),
        'ExtraTrees':ExtraTreesClassifier(n_estimators=140,min_samples_leaf=3,class_weight='balanced',max_features='sqrt',random_state=RANDOM_STATE,n_jobs=-1),
        'SVM-RBF':SVC(probability=True,class_weight='balanced',C=1.5,gamma='scale',random_state=RANDOM_STATE),
    }
    cv=StratifiedKFold(5,shuffle=True,random_state=RANDOM_STATE)
    oof={}; rows=[]
    for name,m in models.items():
        pipe=Pipeline([('pre',pre),('m',m)])
        p=cross_val_predict(pipe,X,y,cv=cv,method='predict_proba',n_jobs=1)[:,1]
        oof[name]=p
        fpr,tpr,thr=roc_curve(y,p); th=float(thr[np.argmax(tpr-fpr)])
        yh=(p>=th).astype(int)
        rows.append([name,roc_auc_score(y,p),average_precision_score(y,p),f1_score(y,yh),recall_score(y,yh),precision_score(y,yh),th])
    ens=np.mean(np.column_stack([oof['Logistic'],oof['RandomForest'],oof['ExtraTrees']]),axis=1)
    fpr,tpr,thr=roc_curve(y,ens); threshold=float(thr[np.argmax(tpr-fpr)])
    yh=(ens>=threshold).astype(int)
    rows.append(['SoftVoting',roc_auc_score(y,ens),average_precision_score(y,ens),f1_score(y,yh),recall_score(y,yh),precision_score(y,yh),threshold])
    perf=pd.DataFrame(rows,columns=['模型','AUC','AP','F1','召回率','精确率','阈值'])
    fitted={}; allp=[]
    for name in ['Logistic','RandomForest','ExtraTrees']:
        pipe=Pipeline([('pre',pre),('m',models[name])]); pipe.fit(X,y); fitted[name]=pipe
        allp.append(pipe.predict_proba(feat[num+['类目']])[:,1])
    feat=feat.copy(); feat['爆款概率']=np.mean(allp,axis=0)
    return feat, train_ids, X, y, pre, models, fitted, oof, ens, threshold, perf


def cluster_profile(feat, train_ids, labels):
    prof=feat.groupby('聚类')[['流量获取能力','转化效率','用户粘性','增长趋势','爆款概率']].mean()
    prof['样本数']=feat.groupby('聚类').size()
    tmp=feat.loc[train_ids,['聚类']].join(labels['爆款标签'])
    prof['历史爆款率']=tmp.groupby('聚类')['爆款标签'].mean()
    prof['历史样本数']=tmp.groupby('聚类').size()
    # 按历史爆款率从高到低重命名,避免KMeans标签无序造成解释混乱
    order=list(prof['历史爆款率'].sort_values(ascending=False).index)
    mapping={old:i+1 for i,old in enumerate(order)}
    return prof, mapping


def forecasting_model(feat, labels, future, train_ids):
    num=['价格','是否包邮','是否有主图视频','是否参加活动','流量获取能力','转化效率','用户粘性','增长趋势','爆款概率',
         '浏览量_均值','访客数_均值','加购率','支付转化率','收藏率','评价率','支付订单数_均值',
         '支付订单数_变异系数','支付订单数_对数斜率','支付订单数_趋势R2']+[f'销量D{d}' for d in range(1,8)]
    X=feat.loc[train_ids,num+['类目']].copy(); Y=future.loc[train_ids,[f'Y{d}' for d in range(15,46)]].copy()
    pre=ColumnTransformer([('num',StandardScaler(),num),('cat',OneHotEncoder(handle_unknown='ignore',sparse_output=False),['类目'])],sparse_threshold=0)
    models={
        'Ridge':Ridge(alpha=20),
        'RandomForest':RandomForestRegressor(n_estimators=140,min_samples_leaf=2,max_features=0.8,random_state=RANDOM_STATE,n_jobs=-1),
        'ExtraTrees':ExtraTreesRegressor(n_estimators=160,min_samples_leaf=2,max_features=0.9,random_state=RANDOM_STATE,n_jobs=-1),
        'KNN':KNeighborsRegressor(n_neighbors=10,weights='distance',p=2),
    }
    kf=KFold(5,shuffle=True,random_state=RANDOM_STATE)
    Ylog=np.log1p(Y.values); oof={}; rows=[]
    for name,m in models.items():
        pipe=Pipeline([('pre',pre),('m',m)])
        plog=cross_val_predict(pipe,X,Ylog,cv=kf,n_jobs=1)
        p=np.maximum(0,np.expm1(plog)); oof[name]=p
        t=Y.values
        mae=np.mean(np.abs(p-t)); rmse=np.sqrt(np.mean((p-t)**2)); wape=np.abs(p-t).sum()/(np.abs(t).sum()+1e-9)
        smape=np.mean(2*np.abs(p-t)/(np.abs(p)+np.abs(t)+1e-6))
        total_mape=np.mean(np.abs(p.sum(1)-t.sum(1))/(t.sum(1)+1e-6))
        rho=spearmanr(t.sum(1),p.sum(1)).statistic
        rows.append([name,mae,rmse,wape,smape,total_mape,rho])
    ensemble=(oof['RandomForest']+oof['ExtraTrees'])/2
    t=Y.values
    rows.append(['RF-ET Ensemble',np.mean(np.abs(ensemble-t)),np.sqrt(np.mean((ensemble-t)**2)),
                 np.abs(ensemble-t).sum()/t.sum(),np.mean(2*np.abs(ensemble-t)/(ensemble+t+1e-6)),
                 np.mean(np.abs(ensemble.sum(1)-t.sum(1))/(t.sum(1)+1e-6)),spearmanr(t.sum(1),ensemble.sum(1)).statistic])
    perf=pd.DataFrame(rows,columns=['模型','MAE','RMSE','WAPE','sMAPE','31日总量MAPE','Spearman'])
    fitted={}
    for name in ['RandomForest','ExtraTrees']:
        p=Pipeline([('pre',pre),('m',models[name])]); p.fit(X,Ylog); fitted[name]=p
    return num, X, Y, pre, models, fitted, oof, ensemble, perf
全部论文请见下方“ 只会建模 QQ名片” 点击QQ名片即可
Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐