简介:面向新能源汽车行业分析与机器学习建模场景,这份数据集提供2025年三千余条真实电动汽车记录,覆盖特斯拉、宝马、日产等品牌,包含电池化学成分与容量、续航里程、充电标准与时间、价格、制造产地、自动驾驶等级、二氧化碳排放、安全评级、2024年销量及保修年限等关键字段,可用于市场趋势研判、车型对比和预测模型训练。压缩包共三个文件,分别提供XLSX表格、CSV数据与JSON格式,大小仅492KB,便于Excel查看、Pandas读取或程序化调用。已有203人浏览/学习。数据记录以唯一ID标识每款车型,包含多品牌、多年份混合样本,并保留分类字段的供应商差异,适合数据清洗、特征工程和新能源车研究入门及进阶练习。
1. 电动汽车数据集值钱,但先得想清楚它能回答什么问题
2025 年前后冒出的这批以 3K+ 条记录为卖点的电动汽车数据集,可不像网上拼来的表格那样只写着“品牌、车型、价格”,它把特斯拉、宝马、日产的主力车型电池规格和同年销售数据放在一张表里。这类数据真正值钱的地方不是“能查配置”,而是能回答“某个电池版本的车,实际成交价中位数是多少”“三元锂和磷酸铁锂版本,价格差在哪个区间”“2025 年新上牌记录里,哪个品牌把长续航版本做成了主力”这类问题。对做新能源行业分析、二手车定价模型、电池容量趋势研究的人,这是一份能当原料用的基础表。
但我先泼一盆冷水:这类数据集从来不会“打开就能用”。车企官网的电池容量写着 60kWh,到了第三方采集表里可能变成“60 千瓦时”“60 kWh”“60000Wh”三种写法;续航里程有 NEDC、WLTP、CLTC 三种口径,混在同一条记录里的情况我见过太多次。这篇文章我就按真实动手做数据分析的路线,讲清楚拿到这份电动汽车数据集之后,怎么拆字段、怎么清洗、怎么建模、有哪些坑是新手一定会踩的。整个过程只用 pandas 和 sklearn 就能落地,不需要分布式环境,也不需要写一堆 Spark 代码。
2. 把电池规格从“人类文本”拆成“机器能算的列”:字段设计与清洗的第一次选择
2.1 3K+ 记录里躺着哪几类字段:销售事实、电池参数、车型标识
拿到表以后,我第一件事不是看数据,而是看列名和每列的非空比例。常见的做法是先读入再快速info(),但真正的老手会在读入前就确认分隔符和编码。这类 CSV 通常是 UTF-8 编码,偶尔有 Excel 导出的 UTF-8 with BOM 文件,列名首列会多出一个\ufeff,如果后面要按列名取数,这一步就要先处理。
import pandas as pd df = pd.read_csv('ev_2025_records.csv', encoding='utf-8-sig') print(df.shape) print(df.columns.tolist()) print(df.dtypes)这里encoding='utf-8-sig'是处理 BOM 的保险写法,即使文件没有 BOM 也不会出错。读取后先看shape,确认是不是 3000 多条记录;再看dtypes,判断哪些列被错误识别成了 object。这一步不需要写任何业务逻辑,但能帮你提前发现问题,比如价格列被读成了字符串,或者年份列变成了浮点数。
看完成列类型,紧接着就是人工过一遍字段含义。以我手上这类数据集的常见结构,字段大约分成三组:第一组是车型标识,包括品牌、车型名称、年款、车身形式,这些都是分类变量;第二组是电池参数,包括电池容量(kWh)、续航里程(km)、电池类型、充电功率,这组是后面建模的主要特征;第三组是销售事实,包括销售日期、价格、销量或上牌数量。三组字段的用途完全不同,车型标识用于分组和横向对比,电池参数用于特征工程,销售事实用于标注目标变量,千万不能混在一起处理。
这里有个容易忽略的点:很多第三方数据集的“销售数据”并不是真实上牌数,而是线索量、订单量或经销商报备量。我看到过把订单量当销量算市占率的翻车案例。所以在动手前,应该先确认这个“2025 年销售数据”到底是什么口径。数据集里如果只写了“销售数据”四个字,我一般会按“记录条数”来处理,而不去断言它是上牌量还是批发量,做分析的时候也只说“记录分布”,不说“市场占有率”。
2.2 用 pandas 把电池容量和续航拆成数值列
电池容量列是这类数据集里最混乱的地方。常见的原始值包括“60 kWh”“60kWh”“60 千瓦时”“60.0”“60000 Wh”以及干脆就是空值。要把这些统一成数值,最稳的方法是先把它变成字符串,再用正则提取数字部分和单位部分。
import re df['电池容量_raw'] = df['电池容量'].astype(str) def parse_battery_capacity(s): s = s.strip().lower() if pd.isna(s) or s == 'nan' or s == '': return None # 匹配数字部分,兼容小数 num_match = re.search(r'(\d+(?:\.\d+)?)', s) if not num_match: return None num = float(num_match.group(1)) # 判断单位:kwh / 千瓦时 / 度 -> 保留;ah -> 按电压换算;wh -> 除以1000 if 'ah' in s: return None # Ah 需要电压值才能换算,见 5.1 if 'wh' in s and 'kwh' not in s: return num / 1000.0 return num df['电池容量_kWh'] = df['电池容量_raw'].apply(parse_battery_capacity)这段代码的逻辑说明:先统一成小写,再用正则抓第一个数字,最后按单位换算。Ah这种单位不能直接换算成 kWh,因为还需要电压参数,强行按照 3.7V 去算三元锂电池、按 3.2V 去算磷酸铁锂电池,结果会和你最终的建模结果强烈挂钩,但数据源里并不会明确告诉你每一条记录对应的电压平台。所以这里遇到Ah我选择返回None,宁可后续填充或丢弃,也不能拿错误数据硬算。
这里有另一个细节:字符串里可能出现“60/100 kWh”这样的区间写法,正则只会抓到第一个 60。这种情况我会在apply之后再检查一下电池容量_raw中是否还有包含/的值,单独列出来人工处理。不要想着写一个万能正则一次搞定,数据清洗的本质是分层处理。
续航里程的处理逻辑和电池容量类似,但它多了一个工况维度,也就是 NEDC、WLTP、CLTC 的区别。我会把续航拆成两列:一列是纯数值,一列是工况类型。
def parse_range(s): s = str(s).strip().upper() if s == 'NAN' or s == '': return None, None num_match = re.search(r'(\d+(?:\.\d+)?)', s) if not num_match: return None, None num = float(num_match.group(1)) if 'NEDC' in s or '新标' in s: condition = 'NEDC' elif 'WLTP' in s: condition = 'WLTP' elif 'CLTC' in s or 'CLTC-P' in s: condition = 'CLTC' else: condition = 'UNKNOWN' return num, condition df[['续航里程数值', '续航工况']] = df['续航里程'].apply(lambda x: pd.Series(parse_range(x)))parse_range返回两个值,分别存到两列。后续做对比分析时,要么只保留同一个工况的数据,要么把不同工况做系数折算。这里我多说一句:不要轻易做工况折算。NEDC 和 WLTP 的差异不是固定系数能描述的,它和车型、电池热管理、驾驶循环都有关系,WLTP 数值普遍是 NEDC 的 0.75 到 0.9 倍,但具体到每一款车差异很大。建模的时候更推荐的做法是把工况作为分类特征放进模型,让模型自己去学这个差异,而不是预先折算。
2.3 单位与测试工况是两条独立的轴,不要塞进同一列
接上面的思路,数据集的字段设计有个很容易犯的错:把单位写在值里,把工况写在续航数值后面,比如“500km(NEDC)”。这种设计对人来说很友好,但对后续的查询、分组、画图都是灾难,每次操作都得先解析字符串。
我做表格的时候,会把一切可计算的物理量拆成“数值列 + 单位列 + 口径列”三部分。电池容量拆成电池容量_kWh,单位统一成 kWh;续航里程拆成续航里程数值,单位统一成 km;工况单独一列续航工况。销售价格同理,拆成价格_万元和价格_币种,避免出现美元和人民币混在同一列里。这样设计的直接好处是 pandas 的groupby、describe、corr都能直接作用于数值列,不需要每次都写自定义解析函数。
df['价格_万元'] = pd.to_numeric(df['价格'], errors='coerce') df['价格_币种'] = df['价格'].apply(lambda x: 'USD' if 'USD' in str(x).upper() or '$' in str(x) else 'CNY')errors='coerce'会把无法转换成数字的值变成 NaN,这样方便后续统一处理,但要注意别把合法价格干掉了。比如“35.8万元”里有“万元”两个字,to_numeric直接转不出来,需要先去掉非数字字符再转。实际处理时,我用的是先做单位拆列,再去掉单位字符、转成纯数字,顺序不能乱。单位识别错了,后面所有按价格分组统计的结果都会偏。
3. 对 3K+ 条真实电动汽车数据集做 EDA:特斯拉、宝马、日产谁在卖什么电池
3.1 品牌×电池容量分布:看入门版和长续航版的分层
清洗完字段,就进入真正的探索性分析阶段。我先做品牌和电池容量的交叉分布,重点看三个品牌的电池版本分层,这能直接反映各家的产品策略:特斯拉 Model 3 后驱版一般在 60kWh 左右,长续航版到 75kWh 以上;宝马 iX3 这种单版本车型,容量集中在 74kWh 附近;日产 Leaf 则可能在 40kWh 和 62kWh 两个档位上明显分开。
eda = df.groupby(['品牌', pd.cut(df['电池容量_kWh'], bins=[0, 50, 65, 80, 100, np.inf])]).size().unstack(fill_value=0) print(eda)pd.cut把电池容量分成四到五个区间,这样比直接看原始分布更直观。这里的区间划分不是拍脑袋,而是根据当前市场主流 EV 电池容量分布来定的:小于 50kWh 一般是 A0 级小车,50-65kWh 是紧凑型入门,65-80kWh 是中型车主流水准,80kWh 以上就是长续航或性能版。当然具体区间阈值可以根据数据集实际情况调整,比如某一年份集中了一批 100kWh 以上的车型,那就要单独加一个区间。
这一步做完,常见的三个结论模式是:特斯拉的容量分布呈双峰甚至三峰,对应标准版、长续航版、高性能版;宝马比较集中,说明产品线相对精简;日产如果只有 Leaf 一款车,那两档电池容量与年款强相关。这类结论是后续做车型分层建模的重要依据,如果建模时把所有品牌混在一起,电池容量对价格的影响会被“品牌溢价”这个更高权重的变量完全遮蔽。
3.2 容量与续航的散点及相关系数:为什么会被工况口径撕裂
接下来做电池容量与续航里程的关系分析。直觉上容量越大续航越长,但实际算相关系数往往只有 0.7 左右,远达不到接近 1 的水平,原因就出在续航工况不一致上。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 6)) for condition, grp in df.groupby('续航工况'): plt.scatter(grp['电池容量_kWh'], grp['续航里程数值'], label=condition, alpha=0.6) plt.xlabel('电池容量 kWh') plt.ylabel('续航里程 km') plt.legend() plt.show() print(df.groupby('续航工况')['续航里程数值'].mean())这段代码同时画散点和分组均值。这里有一个经常会忽略的操作要点:画图之前要确认续航工况列里没有混入“UNKNOWN”之外的错误值,比如把 CLTC 写成 CTLC、把 WLTP 写成 WTLC。这种拼写错误不会报错,但会生成一个单独的图例分组,往往成为最显眼的离群点来源。我习惯先df['续航工况'].value_counts()看一下枚举值是否干净。
看到散点图以后,不要急着下结论。如果图上出现明显的两个簇,先想想是不是工况不同导致的,而不是车型差异导致的。区分方法是:给每个簇填充颜色,如果颜色大多数按品牌或车系分布,那就是产品策略差异;如果颜色按工况分布,那就是口径差异,需要按工况拆分后重新审视曲线关系。
3.3 按年款看 2025 年的销售记录结构
数据集标记了“2025 年销售数据”,这意味着每一行记录都有一个时间戳或年款字段。我一般会再盘一次年款分布,确认这 3K+ 条记录是单一年份的,还是横跨多个年份的。这两者对应完全不同的分析方式:单一年份能做截面分析,多年份能做趋势分析。
if '销售日期' in df.columns: df['销售年份'] = pd.to_datetime(df['销售日期'], errors='coerce').dt.year elif '年款' in df.columns: df['销售年份'] = df['年款'].astype(int) print(df['销售年份'].value_counts().sort_index())这里要注意一个坑:pd.to_datetime处理“2025/3/15”和“2025-03-15”都没问题,但遇到“2025.3.15”这种带中文句号的格式会全部返回 NaT。如果销售年份列大量缺失,先把原始字符串的格式统计一遍再说,不要急着fillna。
大多数时候,销售记录里同一年份会落在 2025 年,但年款字段会显示“2024 款”“2025 款”,这两个概念经常被混淆。我的处理建议是:销售年份用于时间维度分析,年款用于车型代际区分,二者都保留,不要合并成一个字段。
4. 用电池规格和销售数据做价格模型:从相关性筛选到一个能跑的 baseline
4.1 特征选择:哪些字段能进模型,哪些只能当标签
走到建模这一步,先要明确一个问题:你打算预测什么?数据集里最合适的连续目标变量是“价格”,因为价格直接受电池容量、品牌定位、车型级别、续航能力影响,而且读者也最容易理解。续航里程也可以做目标,但它和电池容量相关性太高,做回归容易变成在学一个固定换算关系,业务价值没有那么强。
cat_cols = ['品牌', '车身形式', '电池类型', '续航工况'] for c in cat_cols: if c in df.columns: df[c] = df[c].astype('category') feat_cols = ['电池容量_kWh', '电机功率_kW', '车身形式_code', '品牌_code', '续航工况_code']分类变量转成类别型只是第一步,建模前还要转成数值编码。LabelEncoder在这里是可以用的,因为它处理的是无序分类变量对应的树模型输入,对线性回归则不建议单独使用,需要转成 one-hot。我用的是pd.get_dummies生成稀疏列,直接喂给线性回归。
特征范围方面,以下是我的常见做法:电池容量、电机功率、车身尺寸是物理特征,无论线性模型还是树模型都可以直接用;品牌、电池类型(三元锂/磷酸铁锂)、驱动形式是类别特征,需要编码;价格是目标变量,单位统一为万元;续航里程建议先不放进特征集,因为它和电池容量的相关系数通常超过 0.75,放进模型会掩盖其他特征的信息,让回归系数的解读变得困难。
4.2 最小可运行的回归 baseline:训练、评估和最容易翻车的三种做法
基线模型的搭建要遵守一个原则:先跑通,再调优。我不主张一上来就用 XGBoost,第一步先用带了OneHotEncoder的线性回归,把数据管线跑通,确认没有空值和类型错误之后,再考虑更复杂的模型。
import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.metrics import mean_absolute_error, r2_score # 构造完整的训练数据 model_df = df.dropna(subset=['电池容量_kWh', '价格_万元']).copy() # 分类特征做 one-hot,数值特征直接保留 ct = ColumnTransformer([ ('num', 'passthrough', ['电池容量_kWh', '电机功率_kW']), ('cat', OneHotEncoder(handle_unknown='ignore'), ['品牌', '车身形式', '电池类型']) ]) pipe = Pipeline([ ('encoder', ct), ('model', LinearRegression()) ]) X = model_df[['电池容量_kWh', '电机功率_kW', '品牌', '车身形式', '电池类型']] y = model_df['价格_万元'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) pipe.fit(X_train, y_train) y_pred = pipe.predict(X_test) print('MAE:', mean_absolute_error(y_test, y_pred)) print('R2:', r2_score(y_test, y_pred))这段代码里最关键的是OneHotEncoder(handle_unknown='ignore'),这个参数必须写。因为测试集里可能出现训练集没见过的车型或电池类型,如果不忽略未知类别,预测阶段直接报错。ColumnTransformer把数值列直接透传,分类列做 one-hot,线性回归拿到的是拼接后的稠密矩阵或稀疏矩阵,sklearn 内部会自动处理,不需要手动toarray。
跑完基线,你需要关注三件事:第一,MAE 绝对值是多少,如果 MAE 超过 10 万元,说明模型连“平均水平”都没学到;第二,R2 是不是负数,如果测试集 R2 为负,说明模型解释力比“直接取均值”还差,大概率是特征没对齐或存在严重的口径错误;第三,特征系数是否可解释,比如电池容量的系数应为正,品牌的系数差异应大致反映品牌溢价。
最容易翻车的不是模型本身,而是这三件事:一是划分训练集之前没有检查重复记录,导致同一个车型的相近配置同时出现在训练集和测试集里,MAE 低到失真;二是没有做价格去除异常值的处理,把“87 万”和“8.7万”混在同一个模型里,R2 被离群点拉得很高,但实际预测效果很差;三是把 2025 年新上市的车型和 2020 款老车型混在一起训练,时间跨度带来的配置差异会被模型误当成品牌差异或容量差异来学习。
5. 把电动汽车数据集清洗干净:四大常见坑,全是这几年的血泪经验
5.1 电池容量到底是 kWh 还是 Ah:同列不同单位,靠字符指纹来排查
现象:数据集的电池容量列里,90% 的行是“60kWh”,但少量行显示“4.0Ah”。直接画图时,出现一个孤立点在 4.0 附近,完全脱离主分布。
原因:部分型号的电池规格在数据源中用的是单体容量或电池包安时数,经过抓取后混进了同一列。只写了 Ah 没有电压值,就不可能准确换算成 kWh。
解决:单独把这部分记录筛选出来看电压上下文。如果同车型、同年款的其他记录有 kWh 值,可以用这些值估算电压平台;否则宁可删除这些行。我的筛选做法是用字符指纹。所谓字符指纹,就是把每一行的容量字段转成“数字+单位”的正则结构,单位和数字分别存开,这样value_counts一眼就能看出有哪些单位混入。
unit_count = df['电池容量_raw'].str.extract(r'([a-zA-Z\u4e00-\u9fa5]+)$')[0].value_counts() print(unit_count)这里$锚定字符串结尾,把单位部分单独抓出来。统计结果如果出现“kwh”“千瓦时”“KWH”“Ah”等不同写法,说明单位需要归一化。对kwh的大小写变体,统一为小写再匹配;对Ah的行,不要做任何换算,直接标记为缺失,等后续做记录剔除或人工补数。
5.2 NEDC 续航被当 WLTP 用,模型误差直接放大一倍
现象:建模时把续航里程直接作为特征,模型表现稳定,但换一个数据集之后,同一个模型的 MAE 翻倍。重新检查发现,两个数据集的续航工况分布完全不同。
原因:续航里程的数值强弱由测试工况决定,NEDC 普遍比 WLTP 高 10% 到 25%。模型学到了“续航里程”和“价格”之间的关系,但这个关系其实是“工况 + 车型”混杂后的伪关系,一旦数据分布变化,预测立刻失效。
解决:永远把续航工况当成一个独立特征,别写死在流程里。建模时要么按工况分别建模,要么把工况当作类别特征参与训练。做预测的时候,先确认输入数据的续航口径和你训练数据一致,不一致就用保守策略——同一车型的当前工况,加上 WLTP 与 NEDC 的经验差异做粗调整,但调整系数不做全局统一,而是分车系来处理。
5.3 同一车型、同一电池版本,2025 年记录里出现两条“看似重复”的行
现象:df.duplicated()查到多条完全重复的行,看起来是采集重复了,删掉后发现记录数从 3K+ 变成 2K 出头,删得太多。
原因:这些“重复”并不是完全重复。用duplicated()默认的整行判断可能没问题,但这类数据往往存在隐藏字段,例如销售城市、经销商代号、内饰颜色,或者根本没被显示在 DataFrame 里的子版本编号,导致了同车型多条记录其实是合法的重复。
解决:先df.duplicated(subset=['品牌', '车型', '电池容量_kWh', '续航里程数值', '价格_万元'])看一下核心字段维度下的重复情况,再做决定。如果核心字段重复但存在其他销售属性不同,按业务逻辑处理:
dup_core = df.duplicated(subset=['品牌', '车型名称', '电池容量_kWh', '续航里程数值'], keep=False) dup_summary = df[dup_core].sort_values(['品牌', '车型名称']) print(dup_summary)处理策略上,我习惯按“版本标识”来去重:如果同车型同电池容量但价格不同,通常是不同配置版本,保留;如果完全相同,则删除重复行。如果数据集提供了 VIN 或配置单号,优先用这个字段判断。
5.4 销售价格与厂商指导价混用:口径不统一,做统计时偏差极大
现象:做品牌均价排序时,特斯拉排到第一,但细看发现特斯拉的价格用的都是官网指导价,而宝马用的却是经销商终端的折后落地价,两者完全不是一回事。
原因:采集来源不同。第三方数据集经常会把官网指导价和实际成交价放进同一个“价格”字段,没有对齐口径。指导价通常高于最终成交价 3% 到 10%,但在新能源车型上,个别车型指导价和实际落地价能差 20% 以上。
解决:先看价格列的分布形态。如果大量价格的尾数都是 0 或 9,且呈明显的分组特征,多半是指导价;如果价格有非常零散的小数位,比如 29.98、30.51,多半是成交价。对价格列做统计时,优先按分位数而不是均值来报告。如果数据集中提供了价格类型或价格来源字段,那就直接按价格类型分组,不要混合统计。
6. 自己给自己当复核:用分组对比和排序稳定性检验 3K+ 条记录的数据质量
6.1 分品牌核对均价与中位价
建模做完了,数据质量到底行不行,不是由模型 R2 说了算,而是要回头用最简单的统计量来检验。我会先按品牌计算价格的平均值、中位数、样本量,再和公开渠道的标价对比,看差异是否在合理范围内。
price_check = df.groupby('品牌').agg( record_count=('价格_万元', 'count'), mean_price=('价格_万元', 'mean'), median_price=('价格_万元', 'median') ).round(2) print(price_check)这里用中位数和均值一起看,是因为均值会被个别高端车型带偏,中位数更能代表该品牌主力车型的价格带。如果特斯拉的均价在 35 万左右、中位数在 32 万到 36 万之间,说明长续航和高性能版本比较多;如果宝马中位数明显高于均值,说明产品线里低配走量车型偏多。这些数字如果和常识相差太远,不要怀疑市场的多样性,先怀疑数据本身,大概率是价格列混入了不同币种或把美元当人民币计价了。
6.2 用秩相关替代线性相关做鲁棒性检验
线性回归主要看变量之间的线性相关性,而真实数据里往往有离群值。这种情况下我会用 Spearman 秩相关来对变量关系做一个非参数版本的验证,而不是直接相信 Pearson 相关系数。
from scipy.stats import spearmanr spearman_val, p_value = spearmanr( df['电池容量_kWh'].dropna(), df.loc[df['电池容量_kWh'].notna(), '价格_万元'] ) print('Spearman correlation:', round(spearman_val, 3))考虑到电池容量和价格之间有品牌这个混杂因素,我会在做完整体相关系数之后,再按品牌拆分做一次组内相关,看相关方向是否在每组内保持一致。如果整体相关系数为正,但某个大品牌内部相关系数变成负的,那通常说明价格被“品牌档次”主导,而不是被电池容量主导,这时候线性回归输出的容量系数就没有稳定的业务解释力。
这是我做数据核验时最常留的一个习惯:任何一个数据集拿到手,先做一次分组均值对比,再跑一次秩相关,花不了三分钟,却能挡住大多数在清洗阶段埋下的隐患。希望这套流程能帮你在处理电动汽车数据集时少走弯路,把精力留在真正有业务价值的分析上。
本文还有配套的精品资源,点击获取