简介:这份资源面向具备一定Python基础、希望入门数据分析与时间序列预测的学习者,围绕汽车销售场景提供一套完整的数据可视化与销量预测实战方案。内容涵盖数据获取与清洗、销量波动性与同比增长分析、ACF与PACF定阶及SARIMA未来销量预测,并延伸至厂商与车型维度的市场份额、时间趋势和最佳最差表现对比,帮助读者把统计方法与业务分析串联起来。压缩包共23个文件,约3.98MB,包含1个Python主脚本、3个Excel销售数据表、1个SQLite数据库、1个JSON配置、1份Markdown说明及16张可视化结果图,脚本可直接运行复现,图表与数据表便于对照理解每一步分析结论。目前已有875人学习下载,适合课程设计、数据分析练手或需要快速搭建汽车销量预测原型的读者参考。
1. 汽车销售数据可视化与预测:从一张 Excel 到能落地的分析链路
手里拿到一份汽车销售明细表,字段无非是日期、车型、经销商、销量、成交价,几十万行丢进 Excel 直接卡死,老板还想要「下个月大概能卖多少」。这就是「基于 Python 实现汽车销售数据可视化 + 预测」要解决的真实场景:把脏数据清洗成规整的时间序列,用可视化把趋势和结构讲清楚,再用预测模型给出一个带区间的销量估计。它适合两类人——刚学完 Python 基础、想找一个完整项目练手的入门者,以及手头真有销售数据、需要快速搭一套分析看板的从业者。整条链路不依赖复杂环境,pandas 做清洗、matplotlib 和 pyecharts 做图、statsmodels 或 scikit-learn 做预测,一台普通笔记本就能跑通。下面按「数据怎么进、图怎么出、预测怎么做、坑在哪」的顺序拆开讲。
2. 数据准备:把销售明细洗成能建模的表
2.1 先看清字段类型,再决定清洗策略
汽车销售数据最常见的来源是经销商系统导出的 Excel 或 CSV,字段大致分四类:时间字段(销售日期、开票日期)、维度字段(品牌、车型、经销商、区域)、度量字段(销量、成交价、指导价)、以及一堆备注文本。清洗前先做一件事——用dtypes和describe把每列的真实类型和分布摸一遍。很多「预测结果离谱」的问题,根子不在模型,而在日期列是字符串、销量列混了「暂无」这种文本,模型拿到手直接算错。
时间字段统一转成datetime64,维度字段统一去空格和大小写,度量字段强制转数值并把无法转换的置为NaN再决定丢弃还是填充。这一步没有捷径,靠的是逐列确认。我一般会先写一个字段体检函数,把每列的非空率、唯一值数量、样例值打出来,肉眼过一遍再动手。
import pandas as pd import numpy as np # 读取原始销售明细,注意编码,国内系统导出常见 gbk df = pd.read_csv("car_sales_raw.csv", encoding="gbk") # 字段体检:非空率、唯一值数、样例 def inspect(df): report = pd.DataFrame({ "non_null_rate": df.notna().mean(), "n_unique": df.nunique(), "sample": df.iloc[0] }) return report print(inspect(df)) # 日期列统一转换,errors="coerce" 把非法日期变成 NaT df["sale_date"] = pd.to_datetime(df["sale_date"], errors="coerce") # 销量列去掉千分位逗号和单位,再转数值 df["sales_volume"] = ( df["sales_volume"].astype(str) .str.replace(",", "", regex=False) .str.replace("辆", "", regex=False) ) df["sales_volume"] = pd.to_numeric(df["sales_volume"], errors="coerce") # 丢弃日期或销量缺失的行,这两列是后续分析的骨架 df = df.dropna(subset=["sale_date", "sales_volume"])这段代码的关键在三个参数:encoding="gbk"应对国内系统导出,errors="coerce"让非法日期不报错而是变NaT,dropna的subset只针对骨架列,避免误删其他有用行。清洗完先看还剩多少行、日期范围覆盖多久,如果只剩几个月的数据,做月度预测意义不大,得先跟数据提供方确认。
2.2 聚合粒度决定后面所有分析的口径
原始明细是「每笔成交一行」,但可视化和预测通常需要按天、按周或按月聚合。粒度选错,后面全白做。日粒度适合看短期波动和促销效果,但噪声大;月粒度平滑,适合看趋势和季节性,但样本点少。汽车销售有明显季节性(年底冲量、春节淡季),我一般同时准备日粒度和月粒度两张表,日粒度做可视化,月粒度做预测。
聚合时要注意:销量用sum,成交价用mean或加权平均,别把均价直接sum起来。按品牌、区域分组时用groupby加agg一次算完,避免多次遍历。
# 日粒度:总销量 + 平均成交价 daily = ( df.groupby(df["sale_date"].dt.date) .agg(total_volume=("sales_volume", "sum"), avg_price=("deal_price", "mean")) .reset_index() ) daily["sale_date"] = pd.to_datetime(daily["sale_date"]) daily = daily.sort_values("sale_date") # 月粒度:用 resample 按月末聚合,方便做趋势预测 monthly = ( df.set_index("sale_date") .resample("M") .agg(total_volume=("sales_volume", "sum"), avg_price=("deal_price", "mean")) .reset_index() ) # 按品牌 + 月份双维度聚合,供后续结构分析 brand_month = ( df.groupby([df["sale_date"].dt.to_period("M"), "brand"])["sales_volume"] .sum() .unstack(fill_value=0) )resample("M")的M是月末频率,如果想让每个月的标签落在月初用MS。unstack把品牌从行变成列,得到「月份 × 品牌」的矩阵,后面画堆叠图或算各品牌占比直接用它。聚合完记得检查有没有月份缺口,汽车销售数据偶尔会缺某个月,预测前要补零或插值,否则时间序列会断。
3. 可视化:让趋势、结构和异常一眼可见
3.1 用 matplotlib 画趋势和分布,先解决中文和密集横轴
可视化第一步不是选图,是把中文显示和横轴拥挤这两个老问题解决掉。matplotlib 默认不支持中文,会显示成方框;日期横轴点一多就糊成一片。这两个坑几乎每个新手都会踩,提前配好省得反复调。
import matplotlib.pyplot as plt import matplotlib.dates as mdates # 中文显示:Windows 用 SimHei,Mac 用 Arial Unicode MS plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False # 负号正常显示 fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(daily["sale_date"], daily["total_volume"], linewidth=1.2, label="日销量") # 横轴按月定位,避免日期标签挤在一起 ax.xaxis.set_major_locator(mdates.MonthLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m")) plt.xticks(rotation=45) # 标注最大值点,让异常一眼可见 max_idx = daily["total_volume"].idxmax() ax.annotate(f"峰值 {daily.loc[max_idx, 'total_volume']:.0f}", xy=(daily.loc[max_idx, "sale_date"], daily.loc[max_idx, "total_volume"]), xytext=(10, 20), textcoords="offset points", arrowprops=dict(arrowstyle="->")) ax.set_title("汽车日销量趋势") ax.legend() plt.tight_layout() plt.savefig("daily_trend.png", dpi=150)MonthLocator和DateFormatter配合解决横轴密集,rotation=45让标签斜排。annotate标注峰值是让图「会说话」的关键——老板看趋势图第一眼就想知道最高点在哪、什么时候。dpi=150保证导出图清晰,tight_layout防止标签被裁掉。
3.2 用 pyecharts 做可交互看板,结构分析更直观
静态图适合放进报告,交互图适合放进看板让人自己点。pyecharts 输出 HTML,浏览器打开就能悬停看数值、切换图例,做品牌占比、区域分布这类结构分析特别顺手。它和 matplotlib 不冲突,各管一段。
from pyecharts.charts import Bar, Pie, Line from pyecharts import options as opts # 各品牌月度销量堆叠柱状图 brands = brand_month.columns.tolist() months = [str(m) for m in brand_month.index] bar = Bar(init_opts=opts.InitOpts(width="1000px", height="500px")) bar.add_xaxis(months) for b in brands: bar.add_yaxis(b, brand_month[b].tolist(), stack="total") bar.set_global_opts( title_opts=opts.TitleOpts(title="各品牌月度销量结构"), datazoom_opts=opts.DataZoomOpts(), # 月份多时可拖动缩放 toolbox_opts=opts.ToolboxOpts() ) bar.render("brand_month.html") # 品牌总销量占比饼图 brand_total = brand_month.sum().sort_values(ascending=False) pie = Pie() pie.add("", [list(z) for z in zip(brand_total.index, brand_total.values.round(0))]) pie.set_global_opts(title_opts=opts.TitleOpts(title="品牌销量占比")) pie.render("brand_share.html")stack="total"让各品牌堆叠,一眼看出总量和各品牌贡献。DataZoomOpts在月份多时能拖动缩放,这是静态图做不到的。ToolboxOpts自带保存图片和切换视图的按钮。饼图数据用round(0)取整,避免标签太长。生成的两个 HTML 可以直接嵌进内网看板,不需要额外服务。
4. 预测:从移动平均到 LSTM,按数据量选模型
4.1 先立基线,别一上来就上神经网络
预测汽车销量,很多人第一反应是 LSTM。但如果只有两三年月度数据(二三十个点),LSTM 根本喂不饱,训练出来还不如移动平均稳。正确顺序是先立基线:移动平均、指数平滑、季节性分解,看基线误差多少,再决定要不要上复杂模型。基线跑通还能帮你判断数据里到底有没有可预测的信号——如果连趋势都看不出来,换什么模型都白搭。
from statsmodels.tsa.holtwinters import ExponentialSmoothing # 月度销量序列,索引设为时间 ts = monthly.set_index("sale_date")["total_volume"].asfreq("MS") # Holt-Winters:趋势 + 季节,seasonal_periods=12 表示年度周期 model = ExponentialSmoothing( ts, trend="add", seasonal="add", seasonal_periods=12 ).fit() # 预测未来 6 个月 forecast = model.forecast(6) print(forecast) # 看拟合误差,判断基线是否可用 from sklearn.metrics import mean_absolute_percentage_error fitted = model.fittedvalues mape = mean_absolute_percentage_error(ts[1:], fitted[1:]) print(f"基线 MAPE: {mape:.2%}")trend="add"和seasonal="add"是加法模型,适合季节性波动幅度不随销量增长而放大的情况;如果波动幅度随规模变大,改成"mul"。seasonal_periods=12对应月度数据的年度周期。MAPE 低于 15% 说明基线已经能用,高于 30% 就得回头查数据质量或换模型。这一步的产出是一个可解释、可复现的基准,后面所有复杂模型都要跟它比。
4.2 数据量够时用 LSTM 抓非线性,注意窗口和归一化
当你有三年以上日粒度数据,或者多个品牌、多个区域一起建模时,LSTM 这类序列模型才有发挥空间。它的优势是能捕捉非线性、多变量之间的交互,比如促销、季节、竞品动作的叠加影响。但 LSTM 对输入格式敏感,窗口长度、归一化方式、批次大小都会影响结果,调参不当很容易「训练集完美、测试集崩盘」。
import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from sklearn.preprocessing import MinMaxScaler # 用月度序列,归一化到 0-1 values = ts.values.reshape(-1, 1) scaler = MinMaxScaler() scaled = scaler.fit_transform(values) # 构造监督学习样本:用前 12 个月预测下 1 个月 def make_windows(data, window=12): X, y = [], [] for i in range(len(data) - window): X.append(data[i:i+window]) y.append(data[i+window]) return np.array(X), np.array(y) X, y = make_windows(scaled, window=12) # 简单 LSTM:一层 50 单元,输出单值 model = Sequential([ LSTM(50, activation="relu", input_shape=(12, 1)), Dense(1) ]) model.compile(optimizer="adam", loss="mse") model.fit(X, y, epochs=200, batch_size=8, verbose=0) # 预测并反归一化 last_window = scaled[-12:].reshape(1, 12, 1) pred_scaled = model.predict(last_window) pred = scaler.inverse_transform(pred_scaled) print(f"下月预测销量: {pred[0,0]:.0f}")window=12表示用过去 12 个月预测下一个月,月度数据里对应一年周期。MinMaxScaler把销量压到 0-1,LSTM 对量纲敏感,不归一化几乎训不动。epochs=200、batch_size=8是小样本的保守设置,数据多可以调大。预测完必须inverse_transform还原成真实销量,忘了这步会得到一个 0 到 1 之间的「假数字」。LSTM 的结果要跟 4.1 的基线对比,如果 MAPE 没明显下降,说明数据里的非线性信号不值得上神经网络,老老实实用指数平滑更稳。
5. 避坑与排查:那些让结果翻车的细节
5.1 日期解析失败导致整段时间序列错位
现象:预测结果整体偏移一个月,或者某些月份凭空消失。原因:原始日期列格式不统一,比如混了「2023/1/5」和「2023-01-05」,pd.to_datetime默认解析可能把日当月。解决:显式指定format参数,或先用errors="coerce"找出解析失败的行单独处理,确认无误再聚合。
5.2 中文乱码让图表标题变成方框
现象:matplotlib 图上所有中文显示成方块。原因:默认字体不含中文字形。解决:设置plt.rcParams["font.sans-serif"],Windows 用SimHei,Mac 用Arial Unicode MS,Linux 装WenQuanYi并指定。同时设axes.unicode_minus=False,否则负号也会出问题。
5.3 归一化后忘记反归一化
现象:LSTM 预测出来是 0.3、0.5 这种小数,跟真实销量差几个数量级。原因:训练时用了MinMaxScaler,预测后没还原。解决:保存 scaler 对象,预测结果一律inverse_transform再输出。这个坑血泪经验,第一次做序列预测几乎必踩。
5.4 用未来数据训练造成数据泄漏
现象:模型在测试集上表现好得离谱,上线后一塌糊涂。原因:构造特征时用了未来信息,比如用整月均值填充当月缺失,或归一化时用了全量数据的最大最小值。解决:所有统计量只在训练集上计算,再应用到测试集;时间序列切分要按时间先后,不能随机打乱。
5.5 样本太少硬上深度学习
现象:LSTM 训练损失降不下去,或者预测结果几乎是一条直线。原因:月度数据只有二三十个点,参数量远超样本量。解决:先跑指数平滑基线,数据量不足两年就别上神经网络;真要上,用迁移学习或先做数据增强,并严格用交叉验证评估。
6. 把预测做成可复用的脚本:参数化与滚动验证
做到这一步,单次跑通不难,难的是每周新数据进来能自动重跑、结果可比。我的习惯是把整条链路写成一个带参数的脚本,数据路径、聚合粒度、预测步长、模型类型都从命令行或配置读,避免每次改代码。更关键的是加滚动验证——用历史数据模拟「当时只能看到过去」的场景,逐月预测再对比真实值,这样得到的误差才是可信的。
import argparse from statsmodels.tsa.holtwinters import ExponentialSmoothing def rolling_validate(ts, horizon=3, min_train=24): errors = [] for i in range(min_train, len(ts) - horizon + 1): train = ts[:i] test = ts[i:i+horizon] model = ExponentialSmoothing( train, trend="add", seasonal="add", seasonal_periods=12 ).fit() pred = model.forecast(horizon) mape = np.mean(np.abs((test.values - pred.values) / test.values)) errors.append(mape) return np.mean(errors), np.std(errors) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--data", default="car_sales_raw.csv") parser.add_argument("--horizon", type=int, default=3) args = parser.parse_args() # 读取、清洗、聚合后调用 rolling_validate mean_mape, std_mape = rolling_validate(ts, horizon=args.horizon) print(f"滚动验证 MAPE: {mean_mape:.2%} ± {std_mape:.2%}")min_train=24表示至少用 24 个月训练,保证季节项能估出来。滚动验证的均值反映整体误差水平,标准差反映稳定性——标准差大说明模型在某些月份特别不准,要回去看是不是有促销或政策扰动。这套脚本每周跑一次,把 MAPE 记下来,误差突然变大就是数据或市场出了变化,比盯着单次预测值有用得多。
参数化之后,换品牌、换区域、换预测步长都只是改参数,不用动逻辑。我一般还会把每次运行的配置和误差写进一张日志表,时间长了能看出模型在什么条件下靠谱、什么条件下失灵。做数据分析和预测,最值钱的不是某次预测多准,而是知道它什么时候不准。希望帮到你。
本文还有配套的精品资源,点击获取