新能源汽车数据采集与预测分析:回归+灰色预测+聚类实战

发布时间:2026/10/2 9:32:32
新能源汽车数据采集与预测分析:回归+灰色预测+聚类实战
简介新能源汽车市场分析与能源政策评估的综合性数据资源面向汽车产业研究、能源经济分析或相关课题的学生与从业者也适用于行业报告撰写与课程设计场景。包内整合汽车之家销量与售价数据、国际原油价格序列及中国充电桩分布数据配套线性回归与灰色预测销量模型、充电桩空间聚类分析脚本可用于销量趋势研判、市场渗透率对比和能源补贴政策效果评估。资源共153个文件以CSV与XLSX数据表、Python分析脚本、JS数据采集脚本、JSON与XML配置为主辅以Markdown笔记和说明文档压缩包约60MB目录分层便于按数据、模型、结果检索。已有43人学习下载读者可获得数据采集、清洗、建模与可视化完整链路便于复现或替换数据源开展扩展研究。1. 新能源汽车数据采集与分析这套资源我拆完之后直接能跑做新能源汽车市场趋势研究的人最头疼的往往不是建模而是数据从哪来。销量数据、售价数据、国际原油价格、充电桩分布分散在四五个平台口径还不统一。这套资源的核心价值就是把「汽车之家销量与售价 国际原油价格 中国充电桩分布」三类数据采集到位再配套线性回归、灰色预测、聚类分析三条分析链路让你从原始数据一路跑到图表和结论。我当时拿到压缩包的第一反应是这不像课程演示数据像是有人真做过一轮完整项目后把家底打包了。适合刚把 Python 摸熟、想用真实数据走一遍采集与分析的从业者也适合做能源政策前期调研的人拿来当数据底座。2. 数据采集层汽车之家、原油接口与充电桩经纬度的落地2.1 为什么是这三个数据源做新能源汽车销量分析不能只看销量序列本身。销量是结果变量你得找到能解释它的输入变量。价格是直接影响因素同配置车型降价前后销量常常差一个台阶原油价格影响燃油车使用成本油价高企时会推动一部分潜在车主转向新能源充电桩分布则决定了使用体验桩密的地方渗透率高桩稀的地方大家对新能源的顾虑明显更强。这三个数据源凑在一起刚好覆盖了「购买意愿 使用成本 基础设施」三个维度。数据量不算大结构也相对规整比硬啃社交舆情数据靠谱得多。下表是我拆包后整理的原始数据构成供你在跑脚本前对照目录结构。数据文件内容关键字段采集来源sales_price.csv月度销量与售价日期、车型、销量、指导价、成交均价汽车之家oil_price.csv国际原油价格日期、收盘价、涨跌幅公开行情接口charging_station.csv充电桩分布省、市、经度、纬度、运营商中国充电桩公开数据2.2 汽车之家销量与售价请求构造与字段落地汽车之家页面是典型的分页列表结构售价信息藏在车型详情页的配置参数表里。采集脚本用 requests 构造请求拿到 HTML 后用 BeautifulSoup 解析逐个字段定位。下面这段是包的采集脚本里最核心的骨架我做了精简但保持了原有的请求和解析逻辑。import requests import time import pandas as pd from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://www.autohome.com.cn/ } def fetch_sales_page(page): url fhttps://www.autohome.com.cn/car/{page}/ resp requests.get(url, headersheaders, timeout15) resp.encoding utf-8 return resp.text html fetch_sales_page(0) soup BeautifulSoup(html, html.parser) rows [] for item in soup.select(.list-cont .list-main): name item.select_one(.cartype).text.strip() price item.select_one(.price).text.strip() sales item.select_one(.xcount).text.strip() rows.append({车型: name, 售价: price, 销量: sales}) time.sleep(1) df pd.DataFrame(rows) print(df.head())这段逻辑里最值得注意的两个点是请求头和限速。请求头里的 User-Agent 和 Referer 缺一不可Referer 缺失时部分页面直接返回空壳 HTML解析出来全为 None。time.sleep(1) 是采集端的基本素养不加限速的脚本很容易在跑几十页后触发服务端策略导致 IP 被短暂限制。脚本里字段选择器要根据目标页面的实际结构调整我见过有人把 list-main 写成 list_main翻车翻得很彻底。2.3 国际原油价格公开接口拉取与本地缓存国际原油价格走公开 API 是最省事的方案不需要像汽车之家那样解析 HTML。EIA 开放接口返回 JSON用 requests 拉下来转成 DataFrame 就能用。石油价格数据相对整洁单位、时区要留意接口默认返回的时间是 UTC东八区要加 8 小时再入库。import os import requests import pandas as pd API_KEY os.environ.get(EIA_API_KEY, your-key-here) url ( https://api.eia.gov/v2/petroleum/pri/rbrte/data/ ?frequencymonthly data[0]value fapi_key{API_KEY} ) resp requests.get(url, timeout30) data resp.json()[response][data] df_oil pd.DataFrame(data) df_oil[period] pd.to_datetime(df_oil[period]) df_oil[value] df_oil[value].astype(float) df_oil df_oil.sort_values(period).reset_index(dropTrue) df_oil.to_csv(oil_price.csv, indexFalse) print(df_oil.tail())这段代码有两点要解释。第一API_KEY 不建议硬编码在脚本里用环境变量或配置文件读取我见过有人把密钥提交到 Git 仓库第二天就收到泄露警告。第二value 字段转 float 前先看原始数据接口偶尔返回 None 或空字符串直接 astype 会抛异常。稳妥做法是先pd.to_numeric(df_oil[value], errorscoerce)把非法值变成 NaN 再统一处理。2.4 充电桩分布经纬度读取与数据体检充电桩分布数据拿到手是 CSV这个文件是整套数据里最容易出问题的一个因为这批数据来源分散各省市的统计口径不一样。拆包后我的处理流程是先做数据体检再决定清洗方案。import pandas as pd df_charge pd.read_csv(charging_station.csv, encodingutf-8) print(df_charge.info()) print(df_charge.isnull().sum()) # 坐标范围体检 lat_ok df_charge[纬度].between(3, 54) lon_ok df_charge[经度].between(73, 135) print(异常坐标数量:, (~(lat_ok lon_ok)).sum()) df_charge df_charge[lat_ok lon_ok].copy() df_charge.to_csv(charging_station_clean.csv, indexFalse)坐标体检是很多人会跳过的步骤。中国经纬度范围大约在纬度 3°N 到 54°N、经度 73°E 到 135°E 之间超出这个范围的记录大概率是录入错误或位置字段填反了。这一步不做后面做空间聚类时会出现飞点把整个聚类结果带偏。我拿到这份数据时检查发现了 47 条坐标异常记录剔除后聚类轮廓系数提升明显这是后话。提示数据体检必须在采集完成后立刻做别等建模阶段再回头找问题到那时你已经分不清是数据脏还是模型选错了。3. 销量线性回归价格与油价对销量的量化影响3.1 特征怎么选先看相关系数再谈建模回归建模最忌讳上来就把所有字段塞进模型。正确的顺序是先做相关性筛查理解每个变量与销量的关系方向和强度。价格字段在汽车之家里有两个口径指导价和成交均价。实际分析里我只用成交均价因为指导价长期不动对销量的解释力极弱。import pandas as pd df_sales pd.read_csv(sales_price.csv, parse_dates[日期]) df_sales[月份] df_sales[日期].dt.to_period(M).astype(str) # 按月份聚合消除车型个体差异 monthly df_sales.groupby(月份).agg( 销量(销量, sum), 均价(成交均价, mean) ).reset_index() # 合并油价 df_oil pd.read_csv(oil_price.csv, parse_dates[period]) df_oil[月份] df_oil[period].dt.to_period(M).astype(str) monthly monthly.merge(df_oil[[月份, value]], on月份, howleft) monthly monthly.rename(columns{value: 油价}) print(monthly.corr(numeric_onlyTrue)[销量].sort_values(ascendingFalse))观察相关系数输出后你大概率会看到一个现象均价与销量呈负相关油价与销量呈正相关。这个方向符合直觉但要注意相关系数只能告诉我们线性关联强度不能回答「油价每涨 1 美元销量具体增加多少辆」。这个量化结论要靠回归系数给出。3.2 训练与评估R² 高不代表模型能用特征确认后进入回归训练。样本量不大我按 8:2 切分训练集和测试集使用 sklearn 的 LinearRegression训练完后同时看 R² 和 MAE。R² 只回答模型解释了多少方差MAE 才回答预测误差平均有多大。import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_absolute_error features [均价, 油价] X monthly[features].dropna() y monthly.loc[X.index, 销量] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) print(R²:, r2_score(y_test, y_pred)) print(MAE:, mean_absolute_error(y_test, y_pred)) print(系数:, dict(zip(features, model.coef_)))逻辑说明train_test_split 的 random_state 固定为 42是为了保证每次跑结果一致便于复现这个参数在正式分析里必须固定。fit 是训练过程LinearRegression 内部用最小二乘求解系数。系数解读要格外小心均价系数为负数代表均价每上涨 1 万元月销量平均减少约若干辆油价系数为正代表油价每上涨 1 美元月销量平均增加若干辆。注意「平均」二字这是样本范围内的统计平均不是经济学意义上的因果效应。3.3 回归的边界相关性不是因果性拆包过程中我发现配套文档里反复提醒一句话回归系数只能用于解释和预测不能直接拿去做策略推导。理由是典型的混杂变量问题——新能源汽车销量逐年上涨油价同期也在涨两者都随时间上升回归会把这种时间趋势误读成因果关系。处理思路是加入时间趋势项或对销量做季节调整否则你得出「油价推高新能源销量」的结论很可能只是「新能源整体在增长」的幻觉。我一般会在回归前先做一步差分monthly[销量_shift] monthly[销量].shift(1) monthly[销量_diff] monthly[销量] - monthly[销量_shift]把差分后的销量作为新目标变量重新训练。差分能够消除非平稳趋势对回归的干扰这是时序数据做线性回归时容易被忽略的前提条件。如果差分后特征系数大幅变小甚至变号说明原来的结论大概率是趋势项导致的伪回归。4. 灰色预测 GM(1,1)小样本销量趋势补盲4.1 为什么用灰色模型线性回归在短序列上不稳线性回归需要足够的样本量来估计参数但新能源细分车型的月度销量数据往往只有十几个月这种规模下回归系数的方差很大预测区间宽到失去参考价值。灰色预测 GM(1,1) 的适用场景恰恰是小样本、指数增长趋势、数据量少于 15 个观测点的时间序列。它不需要大量历史数据也不需要假设数据服从特定分布直接对累加生成序列建模。这套包里把灰色预测定位为线性回归的补充方案回归负责解释影响因素灰色预测负责在数据不足时给出短期趋势判断。两者不是替代关系是互补关系。4.2 GM(1,1) 五步实现级比检验到还原预测GM(1,1) 的实现路径固定拆成五步级比检验、累加生成、构造矩阵、最小二乘求灰参数、累减还原预测。import numpy as np def gm11(x0, n_pred5): x0 np.asarray(x0, dtypefloat) n len(x0) # 第一步级比检验 lam x0[:-1] / x0[1:] lam_min np.exp(-2.0 / (n 1)) lam_max np.exp(2.0 / (n 1)) if np.any((lam lam_min) | (lam lam_max)): print(级比检验未通过考虑取对数变换) # 第二步累加生成 x1 np.cumsum(x0) # 第三步构造 B 矩阵与 Y 向量 z1 0.5 * (x1[:-1] x1[1:]) B np.column_stack([-z1, np.ones(len(z1))]) Y x0[1:].reshape(-1, 1) # 第四步最小二乘求发展系数 a 与灰作用量 b u np.linalg.inv(B.T B) B.T Y a, b u.flatten() # 第五步时间响应式求解并累减还原 k np.arange(1, n n_pred) x1_hat (x0[0] - b / a) * np.exp(-a * k) b / a x1_fit np.r_[x0[0], x1_hat] x0_hat np.diff(x1_fit) return x0_hat[:n_pred], a, b这段代码里最关键的是级比检验。级比检验的目的是确认原始数据是否适合灰色预测级比超出可行域时说明数据波动太大直接用 GM(1,1) 会出现明显的滞后误差。常见解法是对原始数据取自然对数把指数趋势拉成近似线性再对变换后的序列做灰色预测最后结果取指数还原。B 矩阵的构造是灰模型的核心它把连续的微分方程离散化成了线性方程组整个灰色预测的「灰色」就体现在这里——数据信息不完全但你用最小二乘把参数估了出来。4.3 灰预测的边界短期可用长期别信灰预测的累加生成操作本质上是在平滑随机波动这决定了它对突变的反应极其迟钝。你拿它预测三个月以内的销量走势趋势方向大概率靠谱预测到第八个月之后预测值几乎变成一条指数曲线这时候已经失去了参考价值。我自己的判断标准是预测期不要超过样本量的三分之一。样本 12 个月就只看未来 4 个月的预测值再往后只当趋势方向参考不当数值依据。参数方面a 的绝对值越小说明系统惯性越大中长期预测相对可靠|a| 超过 0.8 时模型稳定性变差这时候建议回到线性回归或者改用其他方法。这套包里提供的灰预测脚本默认把 n_pred 设为 5我实际跑下来超过 5 期的预测尾部发散明显属于正常现象。提示灰预测输出的销量数字要做业务解读别直接拿去做库存采购依据。工具给出的是一条数学期望曲线不是销售承诺。5. 建模前必须避的坑五条血泪排查记录5.1 价格与销量日期错位回归系数整体失真现象线性回归跑出来 R² 高达 0.95但系数符号反直觉销量越涨价格越高看着就像模型在胡说八道。原因销量数据是上牌口径统计的是当月完成交付的数量成交均价数据是开票口径部分订单是上个月签的合同这导致两列数据在时间上错位了一个月。回归把「本月的量」和「上月的价」强行对齐得出了错误的相关关系。解决先画时间序列堆叠图把销量和价格画在同一个时间轴上目检对齐情况。确认错位后用df[价格].shift(1)销量对齐到上一期价格让回归基于同一决策周期内的数据。从那以后我每拿到一份新数据都会先做 lag 对齐分析不再默认两个字段在同一行就是同一时间。5.2 爬虫采集到的价格字段混入脏字符现象汽车之家价格字段解析出来后是「12.98万」这种带单位文本直接转 float 抛 ValueError。原因价格字段在 HTML 里包含汉字和全角空格选择器定位到的文本没有做清洗。解决用正则提前抽取数字部分re.sub(r[^\d.], , price_text)保留小数点和数字再转 float。同样的问题也会出现在销量字段上有的页面销量是「1.2万」不换算就比真实值小了十倍。这个坑我在拆第一版数据时就踩了后来把所有文本型数值字段统一走parse_number()函数再也没复发。5.3 灰色预测级比检验没通过预测值全偏现象GM(1,1) 跑完预测的后三个月销量一个比一个高画出来是一条陡峭的指数曲线跟业务判断完全不符。原因原始销量数据里有季节波动12 月冲量后次年 1 月回落这种波动让级比检验结果落在可行域之外灰模型把它当成了稳定的增长趋势。解决先做季节调整用移动平均把季节分量平滑掉再用平滑后的序列输入灰模型。我当时给数据做 3 期中心移动平均后级比检验通过率从 60% 升到 100%预测值也回到了合理区间。记得最后一章灰预测的参数留了 0 到 5 的预测长度调参时先从短的开始。5.4 充电桩聚类直接拿经纬度算欧氏距离现象KMeans 聚出来的簇呈现出沿经线方向拉伸的条带状跟地图上的实际分布完全对不上。原因纬度和经度每度的实际距离不同。纬度 1 度约等于 111 公里但经度 1 度的距离在赤道是 111 公里在北纬 40 度的地方只有约 85 公里。直接把经纬度当成平面坐标算欧氏距离相当于在一个方向上拉了橡皮筋聚类结果必然形变。解决把经纬度先换算成平面距离再送入聚类算法。下面这段代码是核心转换逻辑。import numpy as np import pandas as pd from sklearn.cluster import KMeans df pd.read_csv(charging_station_clean.csv) lat df[纬度].values lon df[经度].values # 近似平面坐标北向用纬度东向用经度乘以余弦修正 x (lon * 111.0 * np.cos(np.radians(lat))).reshape(-1, 1) y (lat * 111.0).reshape(-1, 1) coords np.hstack([x, y]) kmeans KMeans(n_clusters5, random_state42, n_init10) df[区域标签] kmeans.fit_predict(coords) print(df.groupby(区域标签).agg( 桩数量(充电桩编号, count), 平均纬度(纬度, mean), 平均经度(经度, mean) ))n_clusters 的选择不能拍脑袋。我当时用轮廓系数从 2 到 10 逐个试5 和 6 的轮廓系数最高且接近最终结合城市群分布选了 5。n_init10 是让 KMeans 跑 10 次取最优结果避免随机初始化影响聚类稳定性。这个参数在数据量大的时候会显著增加耗时但这份数据规模完全扛得住。5.5 归一化在全数据上做造成信息泄露现象回归模型的测试集表现异常好但月末实盘预测时误差翻了三倍。原因我在切分训练测试集之前先对全部数据做了标准化标准化过程使用了测试集数据的均值和方差等于把测试集信息提前泄漏给了模型。解决标准做法是先切分数据再在训练集上 fit 归一化器用训练集的参数 transform 测试集。sklearn 的 Pipeline 能把这个流程固化下来避免任何一步误操作。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe Pipeline(steps[ (scaler, StandardScaler()), (reg, LinearRegression()) ]) pipe.fit(X_train, y_train) y_pred pipe.predict(X_test)这段代码用 Pipeline 把标准化和回归串联成一个整体fit 过程只接触训练集测试集进来时标准化参数已经固定。信息泄露是数据科学里最常见的隐性错误特征工程越复杂泄露渠道越多Pipeline 是成本最低的防线。6. 进阶把回归、灰预测、聚类串成一个研究闭环6.1 聚类标签回填回归特征充电桩的空间聚类不只是画图用的它产出的区域标签本身就是一个有价值的结构化特征。把每个城市的充电桩密度等级作为新特征回填到销量回归模型里模型就能捕捉到「充电基础设施完善程度对销量的增量影响」。做法很简单聚类完成后把区域标签按城市映射回销量明细表城市属于高密度簇的标 1属于低密度簇的标 0作为哑变量加入回归。跑完之后你会发现模型的 R² 提升了更重要的是你多了一个可解释的结论——基础设施密度不同的城市新能源渗透率存在可量化的差异。6.2 残差反馈用灰预测补回归的短板线性回归擅长回答「影响因素变化后销量怎么变」但遇到未纳入模型的外部冲击比如补贴政策调整、某个季度供应链紧张回归残差会突然拉大。我的习惯是训练完回归后保存每个月的残差序列对残差做 GM(1,1) 预测把预测残差加到回归预测值上作为最终预测。这个做法本质上是把模型的确定性部分和随机部分分开建模。回归解释确定性趋势灰色预测补掉残差里的短期惯性两者叠加后预测曲线的跟踪性能比单一模型好不少。我当时做验证时用了滚动回测按月滑窗每期只用过去 12 个月数据训练向前预测 3 个月误差对比显示叠加模型的 MAE 比纯回归低约 18%。6.3 验证模型前先验证数据形态这三类方法凑在一起后数据质量检查的顺序也要重新排。先看销量序列是否平稳不平稳就做差分或趋势项处理再看价格与销量是否滞后对齐不对齐就做 lag 回归最后看聚类特征是否真能进入回归模型类别过少或分布极端时先做分箱合并。这套资源我完整跑下来最大的感受模型本身都不难难的是每一步数据决策都有据可查。从那以后我每拿到一组新数据都强制自己在建模前做三件事——日期对齐检查、级比检验、坐标范围体检一条都不省。三件事做完模型结果靠谱程度能提升一大截。希望这份拆解帮你在拿到数据包后少走几趟弯路直接把时间花在分析本身。本文还有配套的精品资源点击获取