YAOTU INSIGHTS

新能源电动车销量预测模型实战:从数据对齐到LightGBM融合与避坑

新能源电动车销量预测模型实战:从数据对齐到LightGBM融合与避坑
简介这份PDF文献面向新能源行业研究者、政策分析人员及企业市场决策者聚焦新能源电动车乘用车销量预测这一能源转型背景下的关键课题。资源以2012至2015年16个季度全国销量数据为基础系统讲解二次指数平滑法、融入季节变动指数的改进灰色GM11模型以及基于IOWA算子的组合预测模型并对2016年四个季度销量进行实例仿真验证。压缩包内仅含1个PDF文件约1.07MB为期刊论文全文包含公式推导、参数估计与仿真结果便于读者直接研读建模思路与数据结论。目前已有148人学习下载适合需要参考文献支撑、希望掌握组合预测方法并应用于市场战略与政策制定的中高级读者。1. 新能源电动车销量预测模型从一份 PDF 到一套能跑的系统新能源电动车销量预测模型的应用这个标题乍看像一份行业报告实际落到工程上它要解决的是一个很具体的问题给定历史月度上险量、车型上市节奏、政策退坡节点、电池成本曲线怎么在下一季度开始前给出一个误差可控的销量区间而不是一个拍脑袋的点估计。我做过几个类似的预测系统最深的体会是——销量预测的难点从来不在模型选得多花哨而在数据口径统一、特征工程贴合业务节奏、以及预测结果能不能被业务方当成决策依据。这份 PDF 如果只讲模型结构那它大概率是学术向的但如果它讲的是「怎么把预测结果接进排产、备货、渠道返利」那才是真正值得复现的东西。这篇文章我按后者来写适合两类人一类是想把时序预测落地到汽车行业的算法工程师一类是手里有销量数据、想搭一套可解释预测管线的数据分析师。下面从数据、特征、模型、评估、避坑一路讲到进阶技巧每一步都给可抄的代码和参数。2. 数据从哪来、口径怎么统一销量预测的第一道生死线2.1 新能源销量数据的四个来源与可信度排序做新能源电动车销量预测第一步不是建模是把数据源理清楚。常见来源有四类一是中汽协或乘联会发布的月度批发/零售销量颗粒度到厂商和车型级别但发布时间滞后约 10 到 15 天二是保险上险数据颗粒度最细能到城市和配置但需要付费且口径按上险日期而非开票日期三是车企自己披露的交付量通常按季度或月度但口径经常调整四是经销商库存和终端折扣数据用于判断渠道压力。可信度排序上我一般把上险数据当基准批发数据当补充车企披露当校验。原因是上险数据直接对应真实交付不容易被压库干扰。如果只有公开的批发销量那就要接受一个事实你的预测目标其实是「批发口径」不是「终端需求」两者在政策切换月份可能差 20% 以上。2.2 用 Python 把多源销量数据对齐到「车型-月」粒度多源数据对齐的核心是统一主键和时间粒度。主键用「厂商车型动力类型」时间粒度统一到自然月遇到周度数据就按 ISO 周归属月份聚合。下面这段代码演示怎么把两份不同口径的 CSV 对齐并处理车型改名导致的实体不一致。import pandas as pd import numpy as np # 读取两份数据一份是上险量insurance一份是批发量wholesale ins pd.read_csv(insurance_sales.csv, parse_dates[reg_date]) whl pd.read_csv(wholesale_sales.csv, parse_dates[stat_month]) # 统一时间粒度上险数据按上险日期归到自然月 ins[month] ins[reg_date].dt.to_period(M).dt.to_timestamp() # 批发数据本身是月度直接截断到月初 whl[month] whl[stat_month].dt.to_period(M).dt.to_timestamp() # 车型名称标准化去掉空格、统一大小写、处理常见后缀差异 def normalize_model(name): name str(name).strip().upper().replace( , ) # 常见改名映射实际项目里这张表要单独维护 alias {MODEL3: MODEL 3, MODELY: MODEL Y, 秦PLUS: QIN PLUS} return alias.get(name, name) ins[model_std] ins[model_name].apply(normalize_model) whl[model_std] whl[model_name].apply(normalize_model) # 按 厂商车型月 聚合上险量求和批发量求和 ins_agg ins.groupby([oem, model_std, month], as_indexFalse)[volume].sum() whl_agg whl.groupby([oem, model_std, month], as_indexFalse)[volume].sum() # 外连接合并保留两边都有和只有一边的记录 merged pd.merge( ins_agg, whl_agg, on[oem, model_std, month], howouter, suffixes(_ins, _whl) ).fillna(0) # 计算批零差比率用于后续判断渠道库存压力 merged[wholesale_retail_gap] merged[volume_whl] - merged[volume_ins] merged[gap_ratio] merged[wholesale_retail_gap] / merged[volume_ins].replace(0, np.nan) print(merged.head(10))这段代码的关键点有三个。第一时间粒度统一用to_period(M)而不是dt.month因为跨年时月份会重复用 Period 能保留年份信息。第二车型名称标准化必须单独维护一张别名表实际项目里车型改名、配置拆分、出口和内销混在一起是常态靠自动模糊匹配会引入大量假合并。第三gap_ratio这个特征后面会直接进模型它反映的是渠道库存累积速度比值持续大于 0.3 通常意味着接下来两个月批发量会被动下调。参数上howouter是为了不丢数据但后续建模时要根据目标口径决定用哪一列作为标签如果预测终端需求就用volume_ins如果预测排产就用volume_whl。2.3 缺失值、异常值和口径断点的处理原则销量数据里最常见的三类脏数据一是新车型上市前没有历史表现为整行缺失二是政策切换月出现脉冲式异常比如补贴退坡前一个月销量翻倍三是口径断点比如某厂商从某月起把出口量并入总销量。处理原则我一般这样定新车型缺失不填充而是用「上市月份数」作为特征让模型自己学冷启动脉冲异常不删除而是加一个「政策事件」哑变量让模型把异常归因到事件上口径断点必须人工确认后做回溯调整不能靠算法自动检测因为算法分不清是口径变了还是需求真变了。这里有个血泪经验曾经有一版模型在补贴退坡月预测误差突然从 8% 跳到 35%排查两天才发现是训练集里退坡月的标签用了调整后口径而预测目标用的是调整前口径两边根本不是一回事。3. 特征工程把政策、竞品和电池成本变成模型能吃的信号3.1 时序特征、滞后特征和滚动窗口的构造方法销量预测的特征体系里时序特征是最基础的一层。我一般会构造这几类滞后特征lag_1、lag_2、lag_3、lag_12滚动统计rolling_mean_3、rolling_std_6以及趋势特征同比、环比。滞后 12 期是为了捕捉年度季节性新能源车在年底和年初的销量节奏受补贴和牌照政策影响很大。滚动窗口的选择要看业务节奏月度数据用 3 期和 6 期比较稳用 12 期会把太多历史信息平滑掉。下面这段代码展示怎么在一个车型的时间序列上批量构造这些特征。def build_time_features(df, target_colvolume_ins, lags(1, 2, 3, 12), windows(3, 6)): df df.sort_values([oem, model_std, month]).copy() grp df.groupby([oem, model_std], group_keysFalse) # 滞后特征注意 shift 要在组内做否则会跨车型串数据 for lag in lags: df[flag_{lag}] grp[target_col].shift(lag) # 滚动统计用 shift(1) 避免把当前月信息泄漏进特征 for w in windows: df[froll_mean_{w}] grp[target_col].apply( lambda s: s.shift(1).rolling(w, min_periodsmax(2, w // 2)).mean() ) df[froll_std_{w}] grp[target_col].apply( lambda s: s.shift(1).rolling(w, min_periodsmax(2, w // 2)).std() ) # 同比和环比同比用 12 期前环比用 1 期前 df[yoy] df[target_col] / grp[target_col].shift(12) - 1 df[mom] df[target_col] / grp[target_col].shift(1) - 1 # 上市月份数用于处理新车型冷启动 df[months_since_launch] grp.cumcount() return df逻辑说明所有滞后和滚动特征都必须在组内做groupby的group_keysFalse是为了保持原索引方便后续合并。滚动统计里shift(1)是关键如果不加当前月的销量会进入自己的特征造成标签泄漏模型在训练集上表现很好但上线就崩。min_periods设成窗口的一半是为了让新车型在数据不足时也能有值而不是整行丢掉。参数上lags和windows不是越多越好我一般先用 1、2、3、12 和 3、6 跑一版基线再看特征重要性决定要不要加。months_since_launch这个特征对新能源车特别重要因为新车型上市前 6 个月的销量爬坡曲线和成熟车型完全不同模型需要知道自己在预测哪个阶段。3.2 政策事件、竞品上市和电池成本的量化方式新能源销量预测绕不开政策。补贴退坡、购置税减免延续、地方牌照额度调整每一个都会在月度销量上留下明显印记。量化方式我一般用三种一是事件哑变量退坡前一个月设 1退坡当月设 -1其余 0二是政策强度连续变量比如补贴金额除以车价得到的补贴率三是政策预期变量用「距离政策切换还有几个月」作为特征。竞品上市的处理类似用「同价格带竞品数量」和「竞品上市月份数」两个特征。电池成本这块碳酸锂价格是公开的可以直接作为外生变量但要注意它的影响有滞后通常滞后 2 到 3 个月才传导到终端定价。下面这张表是我在某次项目里用的特征清单可以直接作为模板。特征名类型计算方式业务含义subsidy_rate连续补贴金额 / 指导价政策优惠力度months_to_policy_change连续切换月 - 当前月政策预期紧迫度competitor_count连续同价格带在售车型数竞争强度months_since_competitor_launch连续竞品上市月数竞品爬坡阶段li_price_lag2连续碳酸锂价格滞后 2 月电池成本传导gap_ratio连续批零差 / 上险量渠道库存压力months_since_launch连续车型上市月数产品生命周期这张表里的每个特征都要做平稳性检验如果和销量之间没有稳定关系宁可不用。我见过有人把十几个政策哑变量全塞进去结果模型在样本外完全失效因为政策事件太稀疏模型学到的是噪声。3.3 特征筛选为什么我很少用 PCA 而更信业务过滤特征筛选上我一般分两步先做业务过滤再做统计筛选。业务过滤就是问一句「这个特征在预测时点真的能拿到吗」拿不到的坚决不用比如当月终端折扣率如果月底才统计出来那它就不能用来预测当月销量。统计筛选我常用 LightGBM 的特征重要性加 SHAP 值看每个特征对预测的边际贡献方向是否符合业务直觉。PCA 我很少用原因是主成分的业务含义不清晰业务方问「为什么这个月预测下调」时你没法解释。销量预测最终是要给人做决策用的可解释性比降维带来的那点精度提升重要得多。如果特征之间共线性严重我宁可手动删掉相关性高于 0.85 的一对中的一个也不做 PCA。4. 模型选型与训练从 LightGBM 基线到融合模型4.1 为什么我总先跑一个 LightGBM 基线新能源销量预测的模型选型我的习惯是先跑 LightGBM 基线再考虑要不要上时序深度模型。原因很实际销量数据样本量通常不大一个厂商几十个车型乘以几十个月也就几千行深度学习容易过拟合LightGBM 对缺失值和异常值鲁棒特征重要性直接可解释训练速度快调参成本低。基线跑出来如果 MAPE 已经在 10% 以内那基本没必要上复杂模型。下面这段代码是一个可以直接复用的 LightGBM 训练和验证框架按时间切分而不是随机切分。import lightgbm as lgb from sklearn.metrics import mean_absolute_percentage_error def train_lgbm(df, feature_cols, target_col, split_month): # 按时间切分split_month 之前做训练之后做验证 train df[df[month] split_month].dropna(subset[target_col]) valid df[df[month] split_month].dropna(subset[target_col]) # 只保留特征列和目标列避免把 month 等泄漏字段带进去 X_train, y_train train[feature_cols], train[target_col] X_valid, y_valid valid[feature_cols], valid[target_col] # 参数说明见下方 params { objective: regression_l1, # 用 L1 损失对销量脉冲异常更鲁棒 metric: mape, learning_rate: 0.05, num_leaves: 31, min_data_in_leaf: 20, # 防止叶子节点过拟合小样本 feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 1.0, verbose: -1, seed: 42 } dtrain lgb.Dataset(X_train, y_train) dvalid lgb.Dataset(X_valid, y_valid, referencedtrain) model lgb.train( params, dtrain, num_boost_round2000, valid_sets[dvalid], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)] ) pred model.predict(X_valid, num_iterationmodel.best_iteration) mape mean_absolute_percentage_error(y_valid, pred) print(fValidation MAPE: {mape:.4f}) return model, pred参数说明objective用regression_l1而不是regression是因为销量数据里脉冲异常多L1 损失对异常值的敏感度低于 L2预测更稳。min_data_in_leaf设 20 是经验值样本量小的时候可以降到 10但再低就容易记住噪声。early_stopping(100)表示验证集指标 100 轮不提升就停配合num_boost_round2000基本不会欠拟合。feature_fraction和bagging_fraction都设 0.8 是为了增加随机性减少过拟合。验证方式必须按时间切分随机切分会让模型看到未来信息MAPE 会虚低好几个点这是最常见的翻车点之一。4.2 时序模型Prophet、ARIMA、LSTM什么时候值得上LightGBM 基线跑完如果 MAPE 还在 15% 以上或者业务方明确要求给出置信区间那可以考虑时序模型。Prophet 适合有明显季节性和节假日效应的序列优点是自带不确定性区间缺点是没法方便地融入竞品、政策这类外生变量虽然可以加 regressor但灵活性不如 GBDT。ARIMA 适合单车型、序列平稳的情况多车型批量预测时参数选择很痛苦。LSTM 和 Transformer 类模型在销量预测上我持保留态度除非你有几百个车型乘以五年以上的月度数据否则很难比 LightGBM 好。我一般的做法是LightGBM 出点预测Prophet 出区间两者做加权融合权重按验证集误差倒数分配。这样既有 GBDT 对外生变量的利用能力又有 Prophet 的区间估计。4.3 融合策略与超参数搜索的实操参数融合策略上我常用的是误差倒数加权和分位数融合两种。误差倒数加权简单适合两个模型误差水平接近的情况分位数融合是把 LightGBM 的预测当作中位数Prophet 的上下界按比例缩放后作为最终区间适合业务方要「保守估计」的场景。超参数搜索我一般用 Optuna搜索空间控制在 5 个参数以内因为销量预测的验证集通常只有 6 到 12 个月搜索空间太大容易过拟合验证集。下面是一个 Optuna 搜索的示例配置。import optuna def objective(trial): params { objective: regression_l1, metric: mape, learning_rate: trial.suggest_float(learning_rate, 0.01, 0.1, logTrue), num_leaves: trial.suggest_int(num_leaves, 15, 63), min_data_in_leaf: trial.suggest_int(min_data_in_leaf, 10, 50), feature_fraction: trial.suggest_float(feature_fraction, 0.6, 1.0), lambda_l2: trial.suggest_float(lambda_l2, 0.1, 10.0, logTrue), verbose: -1, seed: 42 } # 这里复用上面的训练逻辑返回验证集 MAPE model, pred train_lgbm_with_params(df, feature_cols, target_col, split_month, params) return mean_absolute_percentage_error(y_valid, pred) study optuna.create_study(directionminimize) study.optimize(objective, n_trials50, timeout1800) print(study.best_params)参数说明n_trials50配合timeout1800是控制搜索成本实际项目里如果验证集少于 12 个月我建议n_trials不超过 30否则搜出来的参数只对验证集那几个月好。learning_rate用对数均匀分布是因为它对模型影响是非线性的线性搜索会偏向大值。num_leaves上限设 63 是因为样本量小的时候再大就过拟合。搜索完成后一定要用滚动回测再验证一遍不能只看单次验证集结果。5. 避坑与排查销量预测里最容易翻车的五件事5.1 标签泄漏为什么你的验证 MAPE 只有 3%上线却 30%现象离线验证 MAPE 极低上线后误差翻十倍。原因特征里混入了预测时点拿不到的信息最常见的是滚动统计没加shift(1)、用了当月折扣率、或者把未来月份的政策变量填进了训练集。解决把所有特征按「预测时点能否拿到」过一遍写一个特征可用性检查函数对每个特征检查它在预测月的值是否依赖当月或未来数据。我一般会在特征表里加一列available_at标注每个特征最早可获取的时间点训练时只允许available_at 预测月第一天的特征进入。5.2 政策脉冲被当成趋势退坡前暴涨后暴跌怎么建模现象模型在政策切换月前后误差巨大且方向相反。原因模型把退坡前的抢装脉冲学成了趋势退坡后继续外推导致高估。解决加政策事件哑变量并且在训练时对政策切换月做样本加权让模型知道这些月份是特殊事件而不是常态。另一个做法是把政策切换月从训练集里剔除单独用规则修正预测值比如退坡后一个月按历史退坡后平均降幅打折。5.3 新车型冷启动没有历史销量怎么预测现象新车型上市前三个月预测误差普遍在 50% 以上。原因滞后和滚动特征全为空模型只能靠车型级别特征硬猜。解决用「同厂商同价格带已上市车型的爬坡曲线」作为先验构造一个expected_ramp特征具体做法是取过去两年同厂商新车型上市后 1 到 6 个月的销量中位数按价格带分组。同时把months_since_launch作为强特征让模型对不同爬坡阶段用不同规则。如果新车型完全没有参照那就老实给一个宽区间不要硬给点估计。5.4 口径变更厂商合并、出口内销拆分导致的断点现象某个月销量突然跳变模型误差持续几个月偏高。原因厂商调整了披露口径比如把出口量并入总销量或者子品牌合并到主品牌。解决维护一张口径变更表记录每次变更的时间和影响范围训练时对变更前的数据做回溯调整或者加一个口径哑变量。这件事没有算法能自动做必须人工确认。我一般会在数据入库时加一个caliber_version字段每次口径变更就升版本建模时按版本分别处理。5.5 评估指标选错MAPE 在低销量车型上会骗人现象整体 MAPE 看起来不错但低销量车型误差极大。原因MAPE 对分母敏感月销 100 台和月销 10000 台的车型同样差 50 台MAPE 差 100 倍。解决分车型销量段分别评估高销量段看 MAPE低销量段看 MAE 或 SMAPE。另外要按预测 horizon 分别评估1 个月、3 个月、6 个月的误差水平完全不同混在一起看会掩盖长周期预测的问题。我一般会出一张按销量段和 horizon 交叉的误差表业务方看这张表比看一个总体 MAPE 有用得多。6. 进阶技巧把预测区间做窄、把回测做扎实预测做到最后拼的不是点估计精度而是区间估计的可靠性和回测的严谨性。我分享两个自己常用的技巧。第一个是「分位数回归 保序调整」。LightGBM 支持分位数目标可以分别训练 0.1、0.5、0.9 三个分位数模型但直接训出来的分位数可能交叉也就是 0.1 分位数的预测值大于 0.5 分位数。这时候要做保序调整用sklearn.isotonic对三个分位数序列做单调回归保证区间不交叉。这样得到的 80% 预测区间通常比 Prophet 的区间窄 20% 到 30%因为分位数回归直接优化了条件分位数而不是假设正态分布。第二个是「滚动回测 多起点评估」。单次时间切分的验证结果偶然性太大我一般做至少 6 个起点的滚动回测每个起点用之前 24 个月训练、之后 3 个月验证最后看误差的均值和方差。如果某个起点的误差显著高于其他起点那就要去查那个时间段是不是有政策事件或口径变更。下面这张表是我某次回测的结果模板可以直接套用。回测起点训练月数验证 MAPE1月验证 MAPE3月备注2023-01248.2%12.5%正常2023-04249.1%14.3%正常2023-07247.8%11.9%正常2023-102415.6%22.1%政策退坡月需单独处理2024-01248.5%13.2%正常2024-04249.8%15.7%新车型集中上市这张表里 2023-10 那个起点误差明显偏高排查后发现是退坡政策导致后续我在特征里加了政策事件哑变量再回测就降到了 10% 左右。这件事给我的教训是回测不是为了证明模型好而是为了找到模型在什么条件下会坏。我现在的习惯是每次模型上线前必须跑完 6 个起点的滚动回测任何一个起点的 3 月 MAPE 超过 20% 就不上线先查原因。另外预测结果交付给业务方时我一定会附上「本预测在历史回测中的误差范围」和「当前月份是否处于政策或新车密集期」两个说明让业务方知道这个数字该信几分。希望帮到你。本文还有配套的精品资源点击获取