ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

新能源汽车销售数据时空特征分析与趋势预测:从粒度对齐到模型选型

新能源汽车销售数据时空特征分析与趋势预测:从粒度对齐到模型选型 简介新能源汽车销售数据时空特征分析与趋势预测完整代码数据集面向数据分析初学者、新能源汽车研究人员以及需要市场研判的从业者用于学习从数据处理到销量预测的完整Python方案。资源基于Python完成数据探索、缺失值处理、标准化、特征工程与模型构建覆盖季节性、周期性时间序列分析和地域空间分布挖掘并可对销量趋势进行机器学习建模。压缩包共104个文件包含51张可视化图表、36个模型权重文件、7个HTML报告页面以及Python脚本、JSON配置和原始CSV数据整体仅2.64MB目录结构清晰便于按模块复用。已有34人学习。借助Pandas、NumPy、scikit-learn等库可快速掌握数据清洗、特征构建、模型评估与结果可视化全流程通过图表直观理解不同地区、不同时期的销售特征为后续研究或业务决策提供可扩展的代码基础。1. 新能源汽车销售数据时空特征分析与趋势预测先把空间粒度选对再谈模型做新能源销量预测的人第一版模型大概率都会翻车拿全国销量训练一个时序模型预测下个月总量看起来拟合不错可一旦拆到城市级别误差直接翻倍。问题不在模型在数据粒度。新能源汽车销售数据天然带着两个维度——时间上受补贴政策、车型上市、充电网络铺设有节奏影响空间上呈现明显的城市集群效应上海卖得好不代表苏州一定好。因此真正能落地的新能源汽车销售数据时空特征分析与趋势预测第一步不是选模型而是把「省-市-网点」三级空间粒度和「日-周-月」三级时间粒度对齐再做特征分析和预测。这套方案适合新能源车企的区域运营、充电网络规划、经销商库存管理以及看板系统的数据团队。本文给出完整可跑的 python 代码路径按「数据管道 → 时空特征 → 预测建模 → 避坑 → 验证」推进。2. 从原始销量表到时空数据集字段设计、粒度对齐与数据管道2.1 一份能支撑时空分析的数据集最少得有这 7 个字段很多团队拿到的原始数据就是一张每日订单表字段只有「日期、城市、销量」这不够。要让「时空特征分析」有东西可挖必须至少补齐下面这 7 类字段我把它按维度拆开维度字段说明时间date订单日期精确到天空间province/city/dealer_code省、市、经销商编码三级粒度目标sales_qty当日销量后续可聚合为周/月渠道channel_type直营/经销/代理影响铺货节奏线索lead_cnt/test_drive_cnt意向客户数、试驾量是销量的先行指标外部charge_pile_density充电桩密度按城市月度更新事件is_holiday/policy_window节假日标记、补贴政策窗口期数据来源各团队不一样内部 DMS 系统导订单出行APP或充电运营商给充电桩数据政策文件人工标注窗口期。常见做法是把这四张表按「城市 日期」做 key 对齐这一步在 Excel 里做容易错直接用 pandas 写管道更稳。2.2 用 pandas 把四张表对齐左连接顺序决定数据质量先给一套可跑的数据管道示例代码假设你手上有orders.csv订单、charge.csv充电桩、holiday.csv节假日、city_info.csv城市映射import pandas as pd # 订单表日期、城市、经销商、销量、线索、试驾 orders pd.read_csv(orders.csv, parse_dates[date]) orders[city] orders[city].str.strip() # 充电桩表城市、月份、桩密度先转成和订单一致的粒度 charge pd.read_csv(charge.csv, parse_dates[month]) charge[year_month] charge[month].dt.to_period(M) # 订单表也生成月份键用于连接 orders[year_month] orders[date].dt.to_period(M) # 第一步先补外部变量再补节假日顺序不能反 df orders.merge( charge[[city, year_month, charge_pile_density]], on[city, year_month], howleft ) # 第二步节假日按日期精确连接 df df.merge(holiday, ondate, howleft) df[is_holiday] df[is_holiday].fillna(0) # 第三步城市映射表补省份和区域 df df.merge(city_info[[city, province, region]], oncity, howleft) # 数据完整性检查外部变量没匹配上的城市一查便知 miss df[df[charge_pile_density].isna()] if len(miss) 0: print(f注意{miss[city].nunique()} 个城市缺少充电桩数据)逻辑说明合并顺序有讲究先补充电桩这类数值型外部变量再补节假日的 0/1 标记最后补省份区域。如果先合并节假日再来补城市映射时可能因为城市名不规范新增 NaN 行。howleft是刻意为之保留订单表全量宁可外部特征缺失也不丢销量样本。完整性检查这段代码能帮你在数据集下载下来之后第一眼看出哪些城市没法做空间分析——缺充电桩密度的城市后面算空间权重矩阵时得先剔除。参数说明parse_dates保证时间列被读成 datetime 类型避免后面dt.to_period(M)报错。str.strip()处理城市名前后空格这一步不做merge 时「上海」和「 上海」会被当成两个城市这种坑最隐蔽。fillna(0)只对节假日标记生效充电桩密度缺失不要填 0填 0 会把没有充电桩的城市和「有桩但没录入」混为一谈。2.3 三个粒度怎么选预测主体在市分析主体在省归因在网点对齐完字段后必须决定分析落在哪个粒度。我的经验是分层处理省级粒度做大盘趋势判断判断「现在是上行期还是下行期」市级粒度做预测主体因为新能源的地方补贴、牌照政策、充电桩密度全部落在市一级网点级粒度做渠道归因看直营和经销的销量贡献变化。# 按市周聚合作为预测建模的主表 df[week] df[date].dt.to_period(W).apply(lambda r: r.start_time) city_weekly ( df.groupby([province, city, week], as_indexFalse) .agg( sales_qty(sales_qty, sum), lead_cnt(lead_cnt, sum), test_drive_cnt(test_drive_cnt, sum), dealer_cnt(dealer_code, nunique), ) ) # 计算城市周均单价和试驾转化率作为衍生特征 city_weekly[lead_to_test_rate] ( city_weekly[test_drive_cnt] / city_weekly[lead_cnt].replace(0, 1) )聚合时有个关键取舍dealer_cnt用nunique而非count因为经销商编码在同一城市可能重复出现计数会虚高。lead_to_test_rate是销售漏斗里最有预测力的先行指标试驾转化率上升通常领先销量 1 到 2 周这个滞后关系后面建模时要做成交叉特征。市周粒度做预测聚合段用周而不是月是因为月度粒度下样本量太少市级预测每城市一年只有 12 个点没法训练周粒度一年有 52 个点勉强够。3. 时空特征分析莫兰指数、冷热点与时间序列分解的组合拳3.1 空间自相关不是玄学Morans I 帮你判断「销量是否扎堆」拿到市周聚合表后第一个要回答的问题是新能源汽车销量在空间上到底有没有聚集性如果城市之间互相独立那直接用单变量时序模型就行如果存在空间溢出效应——比如上海放量带动苏州 ——就必须把邻接城市的销量做进特征。判断聚集性最常用的指标是全局莫兰指数Morans I。import geopandas as gpd import libpysal from esda.moran import Moran # 读取城市边界做空间权重矩阵 gdf gpd.read_file(city_boundary.geojson) gdf gdf.merge( city_weekly[city_weekly[week] 2024-10-07], oncity, howinner ) # 用 Queen 邻接规则共边或共点即视为相邻 w libpysal.weights.Queen.from_dataframe(gdf) w.transform r # 行标准化权重之和为1 # 计算莫兰指数 moran Moran(gdf[sales_qty], w) print(fMorans I {moran.I:.3f}, p {moran.p_sim:.3f})逻辑说明Morans I 的取值范围接近 [-1, 1]正数说明相似值扎堆高销量城市旁边也是高销量负数说明高值和低值交替分布。单纯看数值不够必须看p_sim它是通过随机置换空间位置得到的经验 p 值p_sim 0.05才能说聚集性显著。w.transform r这行经常被忽略不做行标准化的话邻接城市多的区域会主导全局指标。参数说明Queen 邻接规则适合省市级别的分析因为城市边界不规则用 Rook仅共边会漏掉一些仅有点接触的邻接关系比如隔江相望的两个城市。如果做网点级别的分析就不建议用 Queen 了网点之间的空间竞争关系更接近距离衰减换成距离权重矩阵比如阈值 20 公里内的网点两两相连。3.2 用 Getis-Ord Gi* 找出真正的冷热点城市全局莫兰指数只能回答「有没有聚集」回答不了「哪里是热点」。地方空间自相关指标 Getis-Ord Gi* 可以画出每个城市的冷热程度它能识别出「高销量城市周围也是高销量」的热点区以及「低销量城市抱团」的冷点区。这一步的实际价值是给区域运营划分出重点城市和培育城市。from esda.getisord import G_Local # 基于上一步的权重矩阵 w计算每个城市的局部 Gi* gi G_Local(gdf[sales_qty], w, transformr) # 把结果写回地图数据 gdf[gi_star] gi.Zs gdf[hotspot] 不显著 gdf.loc[(gdf[gi_star] 1.96), hotspot] 热点 gdf.loc[(gdf[gi_star] -1.96), hotspot] 冷点 # 输出热点城市清单 hot_cities gdf[gdf[hotspot] 热点][city].tolist() print(f识别到 {len(hot_cities)} 个热点城市)逻辑说明gi.Zs是标准化后的 Z 得分1.96 这个阈值对应 95% 置信水平大于 1.96 判为热点小于 -1.96 判为冷点。比起直接看销量排名Gi* 的优势在于它考虑了邻域环境——一个销量本身不高但周围城市都在放量的城市会被识别为热点一个自身销量高但周围全是低销量城市的孤岛反而不算热点。这在区域协同运营里价值很大热点城市的充电桩可以共享冷点区域则要单独评估是否值得投入。3.3 时间维度拆解趋势、季节、节假日残差要分开看空间维度做完回到时间维度。新能源汽车销量时间序列和传统燃油车最大的差异是受补贴退坡、新车型上市、突发事件影响它经常出现结构性突变而非平滑趋势。用 statsmodels 的 STL 分解把序列拆成三部分是建模前必须做的一步。from statsmodels.tsa.seasonal import STL # 取某个热点城市近 12 个月的周销量 series city_weekly[city_weekly[city] Shanghai].set_index(week)[sales_qty] # 周期设为 52周度数据的年度季节周期robust 抗异常值 stl STL(series, period52, robustTrue).fit() # 分析趋势与季节性占比判断建模策略 trend_ratio stl.trend.var() / series.var() seasonal_ratio stl.seasonal.var() / series.var() print(f趋势贡献占比: {trend_ratio:.2f}, 季节性占比: {seasonal_ratio:.2f})参数说明period52是周度数据的典型设置对应一年 52 周。如果数据只有两年period52也能跑但季节成分的可信度会下降建议至少三年数据再谈季节分解。robustTrue很重要它会用低权重的鲁棒拟合避免春节前一周的畸形销量把趋势曲线拉歪。判断方法看方差占比如果趋势贡献大、季节贡献小适合用带趋势项的模型如果季节贡献大就得把周期特征显式放入模型。时间序列分解的结论直接影响第 4 章的建模选型。趋势主导时 Prophet 或线性趋势模型就能打季节主导时要用 STL 的 seasonal 成分做特征两者都不是、残差很大时说明时间维度已经解释不了得靠时空交叉特征——这正好衔接下一章的预测模型。4. 趋势预测建模Prophet、XGBoost 与 LSTM 的选型与最小示例4.1 三条建模路线怎么选先看样本量再看外部变量预测模型的选型不该凭偏好按数据条件来。我把三条路线整理成一个可决策的对照表模型适用场景样本量要求外部变量支持可解释性Prophet周/月粒度趋势季节为主2 年以上周数据支持需手动加回归量中趋势和季节可拆XGBoost 时序特征版市级周粒度有先行指标单城市 1 年以上即可强随意加交叉特征高特征重要度可看LSTM网点级日数据数据量超大单序列 1000 样本弱需要拼特征向量低黑匣子实际项目中市级周粒度数据通常只有 100 到 200 个点硬上 LSTM 属于自己给自己挖坑。我的经验是凡是城市数量在 50 个以上的优先 XGBoost 滞后特征它可以一次学完所有城市不用为每个城市单独训练城市少、外部变量也不全时用 Prophet 更稳LSTM 只留给省级日粒度或经销商日粒度的大样本场景。4.2 Prophet 最小可行示例把政策窗口期当作回归量塞进去Prophet 被很多人当黑匣子用其实它的核心参数就那么几个growth决定趋势形式yearly_seasonality管周期add_regressor加外部变量。下面这段代码是我常用的配置重点在把补贴政策窗口和节假日两个外部变量传进去import pandas as pd from prophet import Prophet # 准备 Prophet 输入ds 是日期y 是销量额外列是外部变量 train city_weekly[city_weekly[city] Shanghai][[week, sales_qty]].copy() train.columns [ds, y] train[policy_window] train[ds].apply(lambda d: 1 if is_policy_window(d) else 0) train[is_holiday_week] train[ds].apply(lambda d: 1 if is_holiday_week(d) else 0) model Prophet( growthlinear, yearly_seasonality4, # 年份季节用 4 阶傅里叶级数 weekly_seasonalityFalse, # 周度季节在周数据上没有意义 changepoint_prior_scale0.05 ) model.add_regressor(policy_window) model.add_regressor(is_holiday_week) model.fit(train) # 未来 8 周预测同样需要未来的外部变量值 future model.make_future_dataframe(periods8, freqW) future[policy_window] future[ds].apply(lambda d: 1 if is_policy_window(d) else 0) future[is_holiday_week] future[ds].apply(lambda d: 1 if is_holiday_week(d) else 0) forecast model.predict(future)参数说明yearly_seasonality4把傅里叶级数的阶数从默认 10 降到 4因为新能源销量没有强季节性阶数太高会开始拟合噪声预测反而变差。changepoint_prior_scale0.05是趋势突变点的灵活度默认 0.05 偏保守如果你的数据里补贴退坡导致趋势拐弯很明显可以上调到 0.1但注意过拟合风险如果序列平稳下调到 0.01 更安全。逻辑说明add_regressor里传的一定要是未来能提前预知的值。政策窗口期可以手工排节假日周可以按日历生成但如果试图把「试驾量」塞进去做回归量就必须先单独预测试驾量否则预测未来销量时没有未来试驾量可用。这是新手最容易踩的坑。4.3 XGBoost 时序特征版滞后特征 邻域特征 日历特征XGBoost 不知道时间是什么它只认特征。所以做时序预测时核心工作是把时间信息转成特征。我用到的特征分三类滞后特征过去 1、2、4 周销量、邻域特征邻接城市的周均销量、日历特征月份、季度、是否月底冲量。这里给出特征工程的关键代码import numpy as np import pandas as pd import xgboost as xgb from sklearn.model_selection import train_test_split # city_weekly 已经包含省、市、周、销量 df city_weekly.sort_values([city, week]).copy() # 为每个城市构造滞后特征 for lag in [1, 2, 4, 8]: df[fsales_lag_{lag}] ( df.groupby(city)[sales_qty].shift(lag) ) # 邻域特征同省其他城市的平均销量 df[province_avg_excl_self] ( (df.groupby([province, week])[sales_qty].transform(sum) - df[sales_qty]) / (df.groupby([province, week])[dealer_cnt].transform(count) - 1) ) df[province_avg_excl_self] df[province_avg_excl_self].replace([np.inf, -np.inf], np.nan) # 日历特征月份和季度 df[month] df[week].dt.month df[quarter] df[week].dt.quarter # 剔除前 8 周没有滞后特征的行 df df.dropna(subset[sales_lag_8]) feature_cols [ sales_lag_1, sales_lag_2, sales_lag_4, sales_lag_8, lead_to_test_rate, province_avg_excl_self, month, quarter ] X df[feature_cols] y df[sales_qty] # 按时间划分不能用随机划分否则是数据泄露 split_date df[week].max() - pd.Timedelta(weeks8) X_train, X_test X[df[week] split_date], X[df[week] split_date] y_train, y_test y[df[week] split_date], y[df[week] split_date] model xgb.XGBRegressor( n_estimators300, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, early_stopping_rounds20 ) model.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse) # 特征重要度给出可解释结论 imp sorted(zip(feature_cols, model.feature_importances_), keylambda t: -t[1]) for feat, score in imp[:5]: print(f{feat}: {score:.3f})逻辑说明shift(lag)是时序滞后特征的标准写法按城市分组后 shift避免把上一个城市的销量错位到当前城市。province_avg_excl_self是省均减自身如果直接把省均值放进去本城销量会通过省均值泄露回自身。dropna(subset[sales_lag_8])保证进入模型的每个样本都有完整的滞后历史。参数说明max_depth4对于几十个特征足够了过深会学到滞后特征之间的偶然交互learning_rate0.05配n_estimators300是一组保守组合防止过拟合。early_stopping_rounds20是后悔药训练集不设低估验证集连续 20 轮不改善就停避免把 n_estimators 调到 1000 硬莽。这里最重要的一句是「按时间划分不能用随机划分」——如果用train_test_split的默认参数随机切模型见过未来数据测试指标会虚高得离谱。5. 时空预测的 5 个常见坑现象、原因与处理办法5.1 数据泄露滞后特征里混进了未来信息现象训练集和测试集 RMSE 都很好看但上线后预测误差比测试阶段大 30% 以上。原因做滞后特征时如果整份数据集统一做了shift再随机切分部分「历史滞后值」实际取自测试时间段模型等于提前看到了答案。解决严格按时间截断训练集只使用截止日之前的样本测试集只使用截止日之后的样本。切分点之前不准出现任何测试期数据。我一般把滞后特征的构造放在切分之前但切分必须放在所有特征构造完成之后、建模之前顺序不能乱。5.2 节假日脉冲被模型当成噪声或者趋势现象春节前一周销量暴涨节后暴跌模型在春节前后总是偏差巨大。原因销量脉冲是事件驱动不是趋势也不是季节STL 分解会把这类脉冲的一部分吸收进趋势项导致趋势曲线在节后出现虚假的下降。解决把节假日脉冲单独编码成 0/1 特征或者拆成节前一周、节中、节后一周三个哑变量让模型显式学习这个脉冲。还有一个数据技巧春节日期每年不同用公历月份做季节特征时它会漂移最好按「距春节周数」构造时间特征而不是只用月份。5.3 空间权重矩阵选错莫兰指数失灵现象Morans I 计算结果 p 值一直很大空间聚集性不显著但地图上肉眼明显看到销量扎堆。原因城市边界数据精度不够或者用了全局固定阈值权重。省边界坐标偏移会导致邻接关系错判而用 50 公里固定距离阈值时东部城市密集区每一个城市都和十来个城市相邻西部城市相距 300 公里都没有邻居权重矩阵极不平衡。解决先用libpysal.weights.Queen检查每个城市的邻居数量打印出来看是否有孤立节点再用 k 近邻权重矩阵KNN.from_arrayk 取 5替代固定阈值。还有一坑城市边界 shapefile 的坐标系必须统一为 WGS84 经纬度或投影坐标混用两种坐标系算距离会差一个量级。5.4 补贴政策窗口期造成结构性突变模型系统性低估现象某城市发布地方补贴后连续 3 个月销量翻倍模型预测值始终滞后 1 个月跟不上且误差集中在政策生效期。原因补贴政策是外生事件时间序列模型学不到「未来会有政策」Prophet 的 changepoint 检测在突变发生后才能识别天然滞后。解决政策窗口期不要交给模型自动学习手工标注一个回归变量policy_window并和 Prophe 的changepoint_prior_scale调参配合。更稳妥的办法是把政策分成「发布期、生效期、退坡预警期」三个窗口分别建模。你要知道政策的具体时间表实际上这就是为什么这个项目的数据集里必须包含政策事件标注字段的原因。5.5 新车型上市脉冲与产能爬坡叠加月度聚合掩盖了真实波动现象某品牌新车型上市当月全国销量整体拉高但分城市看有的城市暴涨有的城市不涨。若用全国总量建模模型把新车型影响混入残差导致后续几个月预测持续偏高。原因新车型上市是产品事件不全是市场事件。它受产能爬坡限制销量释放是逐周递增的而且不同城市的投放节奏不同。月度聚合会把这个脉冲拉平月初上市的车型月末看起来「平稳放量」模型学不到投放节奏。解决至少拆到周粒度并用「上市周数」作为特征而不是用简单的月份。把新车型 SKU 单独拎出来做事件标签将其销量从其他序列中分离。如果数据集里没有 SKU 级数据宁可去掉上市前后各 4 周的数据也不要把脉冲污染进来的噪声算进训练集。6. 模型验证的进阶方法空间交叉验证与预测分布诊断到了验证这一步常规的按时间切分已经不够。时空预测的验证有两个层次第一要验证模型在新城市上的泛化能力而不是只验证已在训练里的城市第二要验证预测的不确定性而不只是点预测的误差。空间交叉验证是我现在必做的一项。做法很简单把 50 个城市按区域划分成 5 折每次留出一整个区域的所有城市作为验证集其余区域的城市训练。这和随机抽城市不同它检验的是模型能否把华东学到的规律迁移到西南。关键代码思路如下from sklearn.model_selection import GroupKFold # 以区域为分组保证验证集整块区域不被训练集看到 gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(X, y, groupsdf[region]): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] # 在此处训练并记录验证集误差空间交叉验证的效果往往是同区域内的城市误差远小于跨区域迁移的误差。这不是算法问题是新能源汽车市场本身有区域差异。看到这个结果后你会理解为什么不能只做一个全国模型——正确的做法是区域级建模华东、华南各训一个模型甚至每个模型单独调参。这就是「时空特征分析」和「趋势预测」结合最紧密的落地点。除了交叉验证我每次上线新版本模型前必做预测分布诊断# 计算每个预测点的误差按城市分组看误差分布 test[error] test[sales_qty] - test[pred] city_error test.groupby(city)[error].agg([mean, std]) # 找出误差标准差最大的 5 个城市单独检查 print(city_error.sort_values(std, ascendingFalse).head(5))如果一个城市的误差标准差远大于其他城市说明这个城市的销量波动有特殊驱动因素模型特征没覆盖到。此时补数据比调参数更有效——回想第 2 章提到的充电桩密度你会发现没接充电桩字段的城市通常就是误差最大的那几个。我个人的习惯是每个季度重跑一次莫兰指数因为城市的空间聚集格局会随补贴政策变化。去年华东是热点区今年可能华南就成了新热点。模型参数要跟着调但先确认空间格局有没有变再动参数顺序反了就是在黑匣子里瞎调。希望这套思路帮你在新能源销售数据上少走弯路把时空分析和趋势预测真正落到能用的预测系统里。本文还有配套的精品资源点击获取
返回列表