☰
新能源汽车数据采集与预测分析:回归+灰色预测+聚类实战
2026/10/2 9:32:31 网站建设 项目流程

简介:新能源汽车市场分析与能源政策评估的综合性数据资源,面向汽车产业研究、能源经济分析或相关课题的学生与从业者,也适用于行业报告撰写与课程设计场景。包内整合汽车之家销量与售价数据、国际原油价格序列及中国充电桩分布数据,配套线性回归与灰色预测销量模型、充电桩空间聚类分析脚本,可用于销量趋势研判、市场渗透率对比和能源补贴政策效果评估。资源共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 = f"https://www.autohome.com.cn/car/{page}/" resp = requests.get(url, headers=headers, timeout=15) 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/" "?frequency=monthly" "&data[0]=value" f"&api_key={API_KEY}" ) resp = requests.get(url, timeout=30) 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(drop=True) df_oil.to_csv("oil_price.csv", index=False) print(df_oil.tail())

这段代码有两点要解释。第一,API_KEY 不建议硬编码在脚本里,用环境变量或配置文件读取,我见过有人把密钥提交到 Git 仓库,第二天就收到泄露警告。第二,value 字段转 float 前先看原始数据,接口偶尔返回 None 或空字符串,直接 astype 会抛异常。稳妥做法是先pd.to_numeric(df_oil["value"], errors="coerce"),把非法值变成 NaN 再统一处理。

2.4 充电桩分布:经纬度读取与数据体检

充电桩分布数据拿到手是 CSV,这个文件是整套数据里最容易出问题的一个,因为这批数据来源分散,各省市的统计口径不一样。拆包后我的处理流程是先做数据体检,再决定清洗方案。

import pandas as pd df_charge = pd.read_csv("charging_station.csv", encoding="utf-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", index=False)

坐标体检是很多人会跳过的步骤。中国经纬度范围大约在纬度 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="月份", how="left") monthly = monthly.rename(columns={"value": "油价"}) print(monthly.corr(numeric_only=True)["销量"].sort_values(ascending=False))

观察相关系数输出后,你大概率会看到一个现象:均价与销量呈负相关,油价与销量呈正相关。这个方向符合直觉,但要注意相关系数只能告诉我们线性关联强度,不能回答「油价每涨 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_size=0.2, random_state=42 ) 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_pred=5): x0 = np.asarray(x0, dtype=float) 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_clusters=5, random_state=42, n_init=10) df["区域标签"] = kmeans.fit_predict(coords) print(df.groupby("区域标签").agg( 桩数量=("充电桩编号", "count"), 平均纬度=("纬度", "mean"), 平均经度=("经度", "mean") ))

n_clusters 的选择不能拍脑袋。我当时用轮廓系数从 2 到 10 逐个试,5 和 6 的轮廓系数最高且接近,最终结合城市群分布选了 5。n_init=10 是让 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 回归;最后看聚类特征是否真能进入回归模型,类别过少或分布极端时先做分箱合并。

这套资源我完整跑下来最大的感受:模型本身都不难,难的是每一步数据决策都有据可查。从那以后,我每拿到一组新数据,都强制自己在建模前做三件事——日期对齐检查、级比检验、坐标范围体检,一条都不省。三件事做完,模型结果靠谱程度能提升一大截。希望这份拆解帮你在拿到数据包后少走几趟弯路,直接把时间花在分析本身。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询