Python汽车销售数据可视化与销量预测实战
简介基于Python实现的汽车销售数据可视化与预测项目面向汽车行业数据分析师、相关课程设计与毕业设计选题者以及希望快速上手时间序列预测的Python开发者。项目围绕销量、厂商、车型三个维度展开涵盖数据获取与预处理、波动性与同比增长分析、Top厂商与车型市场份额计算并应用ACF、PACF与SARIMA模型预测未来销量是一套完整可参考的实战方案。压缩包内共23个文件以16张可视化结果图、3份Excel数据表为主另有1个Python主脚本、数据库文件、配置文件与说明文档整包仅3.98MB结构清晰便于按需查阅。通过该资源可系统学习从数据清洗到SARIMA建模的完整流程能实际查看销量波动性、滚动标准差、同比增长率等关键图表厂商与车型维度还包括总销量、平均月销量、市场份额排名及最佳/最差表现分析可直接参考其绘图与建模代码复用至其他行业数据集。已有875人学习下载适合用来快速启动同类数据分析任务。1. 汽车销售数据可视化与预测不是报表是决策用的做了几年汽车行业的数据项目被问得最多的不是“图表好看吗”而是“下个月能卖多少台、哪个区域增长最快”。Python做汽车销售数据可视化加预测就是把这两件事一次性解决把分散在Excel台账里的销售记录清洗成干净的结构化数据再用ECharts把区域、车型、门店的贡献拆开看最后用时间序列模型给出未来一到三个月的销量区间。这套东西适合4S店集团的数据专员、经销商运营和刚接触Python数据分析的从业者。它不是报表工具是决策工具。看完这篇你能从原始台账一路做到可交互的预测页面并知道哪些坑值得提前绕开。2. 数据准备字段设计与清洗这一层偷懒后面全白做2.1 数据源形态4S店台账和你想的不一样真实门店给的台账很少是一张干净的表。常见形态是每个门店一份Excel甚至一个门店一个sheet列名还不统一。有的叫“开票日期”有的叫“成交日期”有的直接写“日期”。我的做法是先做一张字段映射表把多份台账对齐到统一结构再进pandas。下面这张是汽车销售项目里最常用的核心字段表你可以直接拿去作为清洗模板。字段名业务含义典型类型清洗要点sale_date开票/成交日期datetime统一格式区分开票口径与提车口径vin车架号string用于去重一车一单model车型string统一大小写与简称series车系string用于车型聚合如“速腾”“迈腾”province / city销售地区string需与地图匹配名称对齐store_id门店编号string缺失时从省市反推quantity销量int通常为1但试驾车、大客户单要剔除amount销售额float含税/不含税口径必须统一discount优惠金额float负数表示加价需单独处理customer_type客户类型string个人/大客户/员工购车拿到台账后第一件事不是写分析代码而是先做一次摸查读进来、看shape、看每列缺失率、看日期范围。我一般会输出一份数据体检报告包含记录数、门店数、车型数、月度销量分布确认数据没有结构性断裂再往下走。2.2 清洗脚本从Excel到DataFrame的四步处理下面这段代码是这个项目的数据清洗起点覆盖读取、日期解析、去重、缺失值补全四个常规动作。import pandas as pd import numpy as np # 读取多sheet台账所有sheet纵向堆叠 sheets pd.read_excel(sales_2024.xlsx, sheet_nameNone, parse_dates[开票日期, 成交日期]) df pd.concat(sheets.values(), ignore_indexTrue) # 统一列名后续代码全部基于这套标准字段 df.columns [sale_date, deliver_date, vin, model, series, province, city, store_id, quantity, amount, discount, customer_type] # 按车架号去重同一台车出现多次属于开票重录 print(去重前:, len(df)) df df.drop_duplicates(subset[vin], keepfirst) print(去重后:, len(df)) # 缺失省份从城市映射表回填门店维度获取 city_province pd.read_csv(city_province_map.csv) df df.merge(city_province, oncity, howleft) df[province] df[province].fillna(df[province_map]) # 剔除异常记录数量0、金额0、日期超出业务周期 df df[(df[quantity] 0) (df[amount] 0)] df df[(df[sale_date] 2021-01-01) (df[sale_date] 2024-12-31)]这段代码每一步都有明确的业务目的。parse_dates参数指定要转成时间类型的列避免后面单独做字符串转日期时格式报错。按vin去重靠的是车架号的唯一性一台车从开票到上牌只应出现一次重复往往来自重复导入或开票撤销。省市映射表回填是汽车销售台账里最常见的修补方式因为门店常常只填城市不填省份而地图组件需要省级维度。最后的过滤条件看起来简单实际作用很大大客户批量采购单里偶尔会出现quantity0或负数的冲红记录不清掉会把月度汇总带偏。2.3 特征工程日期之外还要看节假日和营业日清洗只是第一步要预测销量光有日期和销量两个字段是不够的。汽车销售有明显的节假日效应和营业日效应春节前后、五一、十一、年末冲量都是峰值。我的做法是在月度建模之外构造一组外生特征让模型能看到“这个月有几个工作日”“有没有节假日”。特征名计算方式用途workdays当月剔除周末与法定假日后的天数修正自然月天数差异holiday_cnt当月含法定节假日的天数捕捉黄金周效应month_end_days月末三天内的销量占比捕捉4S店月末冲量习惯avg_discount月度平均优惠金额反映促销力度avg_price当月成交均价反映价格带变化model_age车型上市至当月的月份数描述车型生命周期# 按车系聚合计算月度特征 df[ym] df[sale_date].dt.to_period(M) monthly df.groupby([ym, series]).agg( sales(quantity, sum), amount(amount, sum), avg_discount(discount, mean), avg_price(amount, mean) ).reset_index() # 引入营业日特征使用chinese_calendar库 import chinese_calendar as cal def count_workdays(year, month): total 0 for day in pd.date_range(f{year}-{month:02d}-01, f{year}-{month:02d}-{month_days[month]}): if cal.is_workday(day.date()): total 1 return total营业日特征对预测的重要性常被低估。2月和10月天数差不多但销量可能差百分之二三十原因不是市场变了而是工作日和节假日分布完全不同。chinese_calendar这个库能识别中国法定节假日和调休安排比直接用workday库准确因为后者不处理春节和国庆调休。月度特征表构造好后建议保存成parquet或csv后续可视化和建模都从这张表读取避免每次重跑清洗。3. 可视化层把销量拆到区域、车型和门店三个维度3.1 地图下钻省份显示不出来多半是地理名称不匹配销售数据可视化最直观的表达是地图。按省份聚合销量用颜色深浅表示量级管理层一眼就能看出华东强、西北弱接下来才谈得上资源调配。Python里做地图我首选pyecharts它封装了ECharts的底层能力配合pandas数据能直接出图。from pyecharts import options as opts from pyecharts.charts import Map province_sales df.groupby(province)[quantity].sum().reset_index() map_chart ( Map() .add(销量, [list(z) for z in zip(province_sales[province], province_sales[quantity])], maptypechina) .set_global_opts( title_optsopts.TitleOpts(title各省份汽车销量分布), visualmap_optsopts.VisualMapOpts(max_province_sales[quantity].max()) ) ) map_chart.render(province_sales.html)这段代码的关键在maptypechina和省份名称的匹配。有个坑必须提前说pyecharts的地图组件依赖内置的地图数据文件新版pyecharts已经把地图资源移到pyecharts_snapshot或需要额外安装echarts-countries-js如果你看到页面上一片空白或只有地图轮廓没有颜色先检查是不是缺地图文件。另一个坑是地理名称规范数据库里写“北京市”“内蒙古自治区”pyecharts里要写成“北京”“内蒙古”不一致就显示不出来。我一般会准备一个标准省份名称映射表入库前先统一。3.2 车型销量排行与门店分布用堆叠柱状图看结构地图看区域格局车型排行看产品结构。汽车销售业务里管理层关心的是“主力车型是哪个、每个门店的销量结构是否健康”。这两个维度用一张堆叠柱状图就能讲清楚横轴是车型堆叠段是不同门店的销量贡献。from pyecharts.charts import Bar pivot df.pivot_table(indexmodel, columnsstore_id, valuesquantity, aggfuncsum).fillna(0) bar Bar() for store in pivot.columns.tolist(): bar.add_series(store, pivot[store].tolist()) bar.set_global_opts( title_optsopts.TitleOpts(title主力车型门店销量贡献), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate45)), yaxis_optsopts.AxisOpts(name销量) ) bar.render(model_store_bar.html)这里有三个细节值得注意。第一pivot_table的index必须是车型、columns是门店汇总逻辑是求和缺省门店的空值要fillna(0)否则堆叠图上会出现缺口。第二门店数量多的时候图例太长我通常只取销量前8的门店其余合并成“其他”不然图例比图表本身还占地方。第三旋转x轴标签是必须的车型名称动辄六七个字不旋转45度会重叠成一团黑色。3.3 趋势联动把月销量、金额、优惠率放到同一个页签里地图和柱状图单独看都清楚但实际汇报时决策者希望在同一个页面里切换视角。我的做法是用Tab组件把多个图表串起来并用一个Grid把月销量趋势和月度优惠金额放在同一张图里对照。from pyecharts.charts import Tab, Line, Grid monthly_total monthly.groupby(ym)[sales].sum() line Line().add_xaxis(monthly_total.index.astype(str).tolist()) line.add_yaxis(月度销量, monthly_total.values.tolist()) grid Grid() grid.add(line, grid_optsopts.GridOpts(pos_left5%, pos_right20%)) tab Tab() tab.add(map_chart, 区域分布) tab.add(bar, 车型门店) tab.add(grid, 趋势对照) tab.render(dashboard.html)强调一个小经验不要把map_chart, bar, grid三个图形对象直接塞进Tab后重新render而是要按上面代码的写法先各自完成配置再统一塞进Tab。如果你在add之后就调用了render后续再复用同一个图表对象容易产生样式错乱。另一个值得说的是趋势图和优惠率放在一起看比单独看销量更有洞察力——销量涨了但如果是靠大幅优惠换来的这个涨就不一定健康。把两条折线放同一坐标系虽然量纲不同但趋势方向是否一致肉眼就能判断。4. 预测层ARIMA与LSTM双轨对比找到能用而不是好看的那个4.1 双轨建模为什么要同时跑两个模型预测汽车销量我从来不为只选一个模型。原因很简单汽车销售数据通常只有三到五年的月度历史样本量小、周期性明显ARIMA这类统计模型在处理短序列时有天然优势但销量受促销、新车上市、价格调整影响这些非线性因素又需要LSTM这类深度模型来捕捉。双轨跑一遍不是为了炫技而是用结果互相当参照避免单模型在特定月份给出离谱值。对比维度ARIMALSTM数据需求百条左右的月度序列可用通常需要更长历史非线性捕捉弱强训练时间秒级分钟级可解释性系数可用统计检验解释黑匣子节假日特征需作为外生变量传入可作为特征拼入输入适合场景平稳或差分后平稳的序列有足够样本的非线性序列选型逻辑是先对序列做平稳性检验如果差分一到两次就平稳ARIMA优先同时准备一份LSTM作为交叉验证。两个模型的预测结果取加权平均还是选优要看滚动验证的结果不能拍脑袋。4.2 ARIMA定阶ADF检验、ACF/PACF与滚动预测ARIMA的参数定阶是最容易让人卡壳的地方。我的习惯是先做ADF检验定d再用ACF/PACF初步判断p和q范围最后用AIC最小化确定最终阶数。import statsmodels.api as sm from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf series monthly.set_index(ym)[sales].astype(float) # ADF检验判断是否平稳 result adfuller(series) print(ADF统计量:, result[0], p值:, result[1]) # 若p0.05则做一阶差分 if result[1] 0.05: diff_series series.diff().dropna() adfuller_result adfuller(diff_series) print(差分后p值:, adfuller_result[1])这段代码解决的是ARIMA里最难的部分到底要不要差分。ADF检验的p值小于0.05说明序列平稳不需要差分反之则做一阶差分后再检验。汽车销售数据受绝对销量规模影响通常不平稳一阶差分后基本平稳所以p1的情况占多数。差分序列稳定后我看ACF图在几阶截尾、PACF图在几阶截尾初步锁定p和q再结合AIC把几个候选组合跑一遍选出AIC最小的。不要用暴力搜索所有p和q样本少的时候容易过拟合而且时间也耗不起。from statsmodels.tsa.arima.model import ARIMA # 拟合ARIMA模型exog可外生添加节假日特征 model ARIMA(series, order(1, 1, 1), exogmonthly[[workdays, holiday_cnt]]) result model.fit() print(result.summary()) # 滚动预测预测下三个月并输出95%置信区间 forecast result.get_forecast(steps3, exogexog_future[[workdays, holiday_cnt]]) pred_mean forecast.predicted_mean pred_ci forecast.conf_int()order(1,1,1)是起步值实际项目中我会同时试(0,1,1)、(2,1,1)、(1,1,0)等组合再定AIC最低的那个。这里必须强调如果当初是用差分序列建模预测结果要加回差分才能得到真实销量statsmodels的ARIMA对象内部已经处理了逆差分所以pred_mean直接就是原始量级的预测值。很多人在这里翻车拿着差分序列的预测值去和原始真实值对比误差大得离谱其实只是差了一步还原。4.3 LSTM实现窗口构造、归一化和早停的细节LSTM在小型项目里被浪费使用的情况很多一上来就堆三层训练一晚上结果还没ARIMA准。这个领域和汽车销售数据结合我更推荐克制的做法两层LSTM加一层Dense序列长度选12个月用过去一年预测下一个月的销量。import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from sklearn.preprocessing import MinMaxScaler values monthly[[workdays, holiday_cnt, sales]].values scaler MinMaxScaler() scaled scaler.fit_transform(values) def build_sequences(data, seq_len12): x, y [], [] for i in range(seq_len, len(data)): x.append(data[i - seq_len:i, :]) y.append(data[i, -1]) return np.array(x), np.array(y) X, y build_sequences(scaled) split int(len(X) * 0.8) model Sequential([ LSTM(32, return_sequencesTrue, input_shape(X.shape[1], X.shape[2])), Dropout(0.2), LSTM(16, return_sequencesFalse), Dense(1) ]) model.compile(optimizeradam, lossmse) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience20, restore_best_weightsTrue ) history model.fit(X[:split], y[:split], validation_data(X[split:], y[split:]), epochs100, batch_size16, callbacks[early_stop], verbose0)代码里每个细节都是踩坑换来的。MinMaxScaler是对包括销量在内的所有特征做归一化因为LSTM使用激活函数输入量级太大梯度会震荡但一定要记住预测完要把结果inverse_transform回去否则输出的只是一个0到1的数字没法直接读。return_sequencesTrue是因为第一层LSTM需要给第二层输出完整序列。EarlyStopping的patience20代表连续20轮验证损失不下降就停止restore_best_weights保证回到最优权重防止过拟合。汽车销售数据只有几十个月LSTM不配早停到后面几乎必然过拟合。另一个注意点是特征排列我把workdays和holiday_cnt放在销量前面因为切Windows时y取的是最后一列data[i, -1]销量正好放在最后一列这是个小技巧避免每次手动索引出问题。4.4 评价指标MAE、RMSE、MAPE各管什么用建完模型后评价指标直接决定你能不能信任这个预测。汽车销售业务里我要给管理者一个数这个数不能是拍脑袋也不能是模型黑话。我通常同时给出三个指标。指标公式含义业务判读MAE平均绝对误差平均每个月的预测差多少台RMSE均方根误差大误差被放大反映最大偏差恶化程度MAPE平均绝对百分比误差相对偏差不同量级车型之间可对比from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(y_true, y_pred) rmse mean_squared_error(y_true, y_pred, squaredFalse) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 print(fMAE{mae:.1f}, RMSE{rmse:.1f}, MAPE{mape:.1f}%)三个指标要配合着看。MAE是业务能直接听的数字比如“平均每月差40台”RMSE比MAE大说明误差分布里有极端月份比如每年12月的预测误差可能远大于均值MAPE在车型销量普遍偏低时很敏感一款月销10台的冷门车差2台就是20%的误差你要看具体跑在哪个车型上再决定要不要保留它。我习惯把ARIMA和LSTM在同一个滚动窗口上面回测把每个月的预测误差拉成一张对比表哪个模型在最近十二个月赢的次数多就用哪个作为主预测模型。提示MAPE在销量接近0的时候会剧烈膨胀如果序列里有长期冷门车型建议剔除后再计算否则一个边缘车型的误差会掩盖主销车型的实际精度。5. 避坑指南时间序列与可视化里最容易翻车的五个场景5.1 现象预测曲线整体滞后真实值一个月第一次用ARIMA跑通后我把预测值和真实值画在同一张图里看发现曲线形状很像却整体向右偏了一个月。原因ARIMA对无趋势或趋势微弱的序列一阶差分会吃掉大部分趋势信息模型倾向于输出“最近值的延续”于是预测值接近上期值视觉上就是滞后。解决把外生变量加进去之后滞后会明显改善另外用滚动预测代替一次性多步预测每一步都用前一步的真实值作为输入也能减少误差累积。5.2 现象热力地图渲染出来是空白的只有轮廓没有色块这个坑在新版pyecharts里特别常见。原因地图的GeoJSON资源没有随pyecharts本体安装要么是maptypechina对应的地图数据缺失要么是省份名不匹配。解决安装pyecharts后在命令行手动确认地图资源目录里有china.js这类文件如果没有安装echarts-countries-js等地图包。然后再用省份标准名对照表把“北京”“内蒙古”“广西”统一成地图组件的标准写法。5.3 现象LSTM训练损失很低预测结果却是一条接近水平的直线我遇到过两次每次都是同一个原因归一化时把所有数据压到0到1之间模型训练的损失很小但预测时输入进入平时没见过的数值区间输出趋近于训练集的均值表现为直线。解决先检查是否对全部序列做了归一化预测前要确认输入窗口的数值落在训练时的分布范围内否则用滚动预测时每一步都不要重新归一化。另外隐藏层单元数过大、样本量不足导致的过拟合也会压缩输出方差降低LSTM的层数或单元数试试。5.4 现象12月的销量看上去特别低但门店说年底明明冲了量这是口径问题。4S店的“销量”有开票口径和提车口径开票可能延迟到下个月而提车发生在当月。如果你按开票日期统计12月的销售业绩会被挪到1月导致12月看起来少了一个量级。解决分析前和业务方确认统计口径如果做预测建议按月统一采用提车日期或交强险日期或者把口径差异作为特征让模型自己学习。最怕的是混用上半年用提车日期、下半年用开票日期序列内部就断裂了。5.5 现象pyecharts生成的HTML很大浏览器打开卡顿地图资源最明显一个china.js几兆甚至十几兆页面加载慢正常。解决不要把所有图表都放进一个HTML里用Tab拆分页面地图单独放一个页签只在用户点击时异步加载资源或者把地图资源放到本地服务器CDN不要每次都从文件路径读取。另外一个优化是用pyecharts的render_embed方法只输出核心脚本搭配公司内部的静态资源服务器引用ECharts库页面体积能减少一半以上。6. 落地与交付PyWebIO把图表和预测包成可交付的页面6.1 不写Flask也能交付PyWebIO轻量集成项目做到最后最怕的不是模型不准而是交付不了。给业务方一个HTML文件他们打开后不知道怎么改参数给他们一个Jupyter Notebook他们根本没装环境。我的方案是用PyWebIO做一个极简的交互页面下拉选择车型点击按钮就输出对应的历史趋势图、区域分布和未来三个月预测区间。PyWebIO的核心优势是后端逻辑和前端界面都在Python里完成不用写一行JavaScript部署只需要一个python app.py。from pywebio.input import select from pywebio.output import put_html, put_text from pywebio import start_server def dashboard(): model_list monthly[series].unique().tolist() series_name select(选择车系, model_list) # 从已训练结果中读取该车型的预测 pred forecast_table[forecast_table[series] series_name] future_month pred[ym].astype(str).tolist() pred_low pred[pred_low].round(0).tolist() pred_high pred[pred_high].round(0).tolist() actual monthly[monthly[series] series_name][sales] put_text(f{series_name} 未来三个月预测区间) for m, low, high in zip(future_month, pred_low, pred_high): put_text(f{m}: {low} ~ {high} 台) put_html(trend_chart.render_embed()) if __name__ __main__: start_server(dashboard, port8080)这段代码做的事很少却很有效select从下拉列表里取车系forecast_table是提前跑好的预测结果表render_embed()把ECharts图形以HTML片段嵌入页面。PyWebIO适合内部的工具型页面它的短板是自定义样式能力弱如果你是给外部客户做展示大屏还是回到Flask或FastAPI。这套东西的部署也很简单内网机器装好Python环境跑python app.py就能在浏览器打开业务方不需要懂代码也能自己切换车型看结果。6.2 给决策者看预测时务必带上区间而不是一个数字交付过程中我摔过最多次的地方不是技术而是表达。当我只给一个“预计下月销量420台”时业务方拿这个数字去和实际对比一旦偏差超过预期整套模型的信任就崩了。后来我把输出改成了区间表达“下月销量预计380到460台中位最优预测420台”。区间来自ARIMA的置信区间或LSTM滚动预测的残差标准差虽然没有单点数字那么精确但它诚实业务方也不会因为你给了一个固定数字而对你的模型产生不合理的期待。从那以后我每次交付预测模块都强制把区间输出做到页面里并配上一句话“预测用于辅助决策不是财务目标”。这样沟通起来比任何华丽的技术方案都少很多麻烦希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →