首页 / 资讯中心 / 文章详情

Python汽车销售数据可视化与时间序列预测实战

Python汽车销售数据可视化与时间序列预测实战 ★ FEATURED ARTICLE
简介这份资源面向具备一定Python基础的数据分析与可视化学习者围绕汽车销售数据提供一套从数据处理到建模预测的完整实战案例。内容涵盖数据获取与清洗、销量波动性与同比增长分析、时间序列分解并借助ACF、PACF与SARIMA模型预测未来销量同时从厂商与车型两个维度展开市场份额、时间趋势及最佳最差表现对比帮助读者掌握真实业务场景下的分析思路。压缩包共23个文件约3.98MB包含16张png可视化图表、3个xlsx原始数据表、1个py主程序脚本、1个db数据库、1个json配置及1个md说明文档结构清晰便于按模块复现与二次修改。目前已有875人学习下载适合课程设计、数据分析入门或需要完整案例参考的读者可直接获得可运行代码、数据与图表成果快速理解汽车销量预测的建模流程。1. 汽车销售数据可视化与预测从一张 Excel 到能用的预测模型手里拿到一份汽车销售明细表字段无非是日期、车型、门店、销量、成交价这几列但真要回答「下个月哪款车会卖爆」「哪个门店在拖后腿」光靠 Excel 透视表翻来覆去地拖效率低还容易看走眼。这个标题讲的就是用 Python 把这份数据先可视化看清楚再套一个预测模型给出未来走势属于典型的「数据分析 时间序列预测」组合场景。适合手里有销售流水、想快速搭一套可复用分析脚本的运营、数据分析初学者也适合想拿一个完整案例练手的 Python 入门者。核心链路只有三步读数据、画图找规律、建模做预测难点不在算法多高深而在数据清洗和特征构造这些脏活上。2. 数据准备把销售流水读进来并洗成能建模的样子2.1 为什么先做数据体检而不是直接画图很多人拿到 CSV 第一反应是df.plot()一把梭结果图出来横坐标挤成一团、销量曲线锯齿乱跳根本看不出趋势。汽车销售数据常见的脏法有几种日期列是字符串格式、门店名有前后空格、销量列混进了「暂无」这种文本、同一天同一车型有多条记录需要聚合。不先做体检后面所有可视化都是错的。我一般会先跑一段探查代码把行列数、缺失值、类型、唯一值数量一次性打出来心里有数再动手。import pandas as pd import numpy as np # 读入销售明细日期列直接解析成 datetime df pd.read_csv(car_sales.csv, parse_dates[sale_date]) # 数据体检看形状、类型、缺失、各列唯一值 print(形状:, df.shape) print(df.dtypes) print(缺失值:\n, df.isnull().sum()) print(门店唯一值:, df[store].nunique()) print(车型唯一值:, df[model].nunique()) # 销量列如果有非数字强制转数值转不了的变 NaN df[sales] pd.to_numeric(df[sales], errorscoerce) # 丢掉销量为空的行这些行对建模没意义 df df.dropna(subset[sales, sale_date]) # 门店名去空格避免「北京店」和「北京店 」被当成两个店 df[store] df[store].str.strip()这段代码的关键在parse_dates和errorscoerce两个参数。parse_dates让 pandas 直接把日期列转成 datetime 类型省得后面再pd.to_datetime转一次errorscoerce的作用是遇到无法转换的值不报错、直接置为 NaN配合dropna把脏行清掉。门店名去空格这步看着不起眼但如果不做后面按门店分组统计时会凭空多出几个「幽灵门店」这是血泪经验。2.2 按天聚合与缺失日期补齐原始流水是一笔一笔的成交记录建模需要的是按天汇总的销量序列。聚合之后还会遇到一个问题某些日期没有销售记录序列里就断了时间序列模型对不连续的时间轴很敏感需要把缺失日期补上并填 0。# 按天聚合总销量 daily df.groupby(sale_date)[sales].sum().reset_index() daily daily.sort_values(sale_date) # 构造完整日期范围补齐缺失日期 full_range pd.date_range(daily[sale_date].min(), daily[sale_date].max(), freqD) daily daily.set_index(sale_date).reindex(full_range) daily[sales] daily[sales].fillna(0) # 没记录说明当天没卖填 0 daily.index.name sale_date daily daily.reset_index() print(补齐后行数:, len(daily))reindex是补齐时间轴的核心它按给定的完整日期索引重新排列数据缺失的位置自动变 NaN再用fillna(0)填上。这里填 0 而不是填均值是因为汽车销售场景下「某天没记录」通常就是「当天没成交」填均值反而会引入虚假的平稳性。如果业务上存在「数据没采集到」的情况那就该填插值而不是 0这个判断得结合数据来源来定。3. 可视化用 Matplotlib 把趋势、车型、门店三个维度画清楚3.1 销量趋势折线图与横坐标拥挤的解法趋势图是最基础也最容易被做废的一张图。数据跨度一长横坐标日期标签就会挤成一坨黑线这是搜索里高频出现的「python画图横坐标太密集」问题。解法有两种一是用DateFormatter控制标签格式和密度二是旋转标签角度。我一般两个一起用。import matplotlib.pyplot as plt import matplotlib.dates as mdates 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[sales], color#2c7fb8, linewidth1.5) # 横坐标按月显示格式 2024-01 ax.xaxis.set_major_locator(mdates.MonthLocator(interval1)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) plt.xticks(rotation45) ax.set_title(汽车日销量趋势) ax.set_xlabel(日期) ax.set_ylabel(销量) plt.tight_layout() plt.savefig(trend.png, dpi150)MonthLocator(interval1)表示每隔一个月打一个刻度DateFormatter(%Y-%m)决定标签长什么样。旋转 45 度是为了让标签不重叠。tight_layout自动调整边距避免标签被裁掉。这套组合基本能解决 90% 的横坐标拥挤问题剩下的靠调figsize宽度。3.2 车型与门店对比柱状图和堆叠图的选型趋势看的是时间维度车型和门店看的是分类维度。单维度对比用柱状图想看每个门店里各车型的构成就用堆叠柱状图。选型逻辑很简单只比总量用普通柱状图要拆结构用堆叠。# 各车型总销量对比 model_sales df.groupby(model)[sales].sum().sort_values(ascendingFalse) fig, ax plt.subplots(figsize(10, 5)) ax.bar(model_sales.index, model_sales.values, color#41ab5d) ax.set_title(各车型总销量) ax.set_ylabel(销量) plt.xticks(rotation30) plt.tight_layout() plt.savefig(model_bar.png, dpi150) # 门店 x 车型 堆叠图 pivot df.pivot_table(indexstore, columnsmodel, valuessales, aggfuncsum, fill_value0) pivot.plot(kindbar, stackedTrue, figsize(12, 6), colormaptab20) plt.title(各门店车型销量构成) plt.ylabel(销量) plt.tight_layout() plt.savefig(store_stack.png, dpi150)pivot_table把长表转成「门店为行、车型为列」的宽表aggfuncsum保证同一组合多条记录被加总fill_value0把没有销售的门店车型组合填 0否则堆叠图会出现断口。colormaptab20在车型数量多时能提供足够的区分色车型超过 20 个就得考虑换配色方案或者只画 Top N。4. 预测建模从移动平均到 LSTM 的选型与实现4.1 先跑通基线移动平均和线性回归一上来就上 LSTM 是新手最容易翻车的地方。数据量不够、序列太短、噪声太大LSTM 训出来的结果还不如移动平均。我的习惯是先跑两个基线7 日移动平均和线性回归把误差基准定下来再决定要不要上复杂模型。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error # 基线17日移动平均 daily[ma7] daily[sales].rolling(window7).mean() # 基线2用「天数序号」做特征的线性回归 daily[t] np.arange(len(daily)) train daily.iloc[:-30] # 前段训练 test daily.iloc[-30:] # 后30天测试 lr LinearRegression() lr.fit(train[[t]], train[sales]) pred_lr lr.predict(test[[t]]) print(线性回归 MAE:, mean_absolute_error(test[sales], pred_lr))rolling(window7)是滑动窗口取最近 7 天均值能抹平短期波动看出趋势。线性回归这里只用了一个「时间序号」特征是最朴素的趋势外推。MAE 是平均绝对误差单位跟销量一致比 RMSE 更好解释。先看这两个数如果 MAE 已经小到业务能接受就没必要折腾深度学习。4.2 LSTM 时间序列预测的输入构造如果基线误差太大再考虑 LSTM。LSTM 做时间序列预测的核心是把序列切成「用前 N 天预测第 N1 天」的样本对这个切窗操作是新手最容易写错的地方。import torch import torch.nn as nn from sklearn.preprocessing import MinMaxScaler # 归一化LSTM 对量纲敏感 scaler MinMaxScaler() scaled scaler.fit_transform(daily[[sales]].values) def make_sequences(data, window14): X, y [], [] for i in range(len(data) - window): X.append(data[i:iwindow]) y.append(data[iwindow]) return np.array(X), np.array(y) X, y make_sequences(scaled, window14) X_t torch.tensor(X, dtypetorch.float32) y_t torch.tensor(y, dtypetorch.float32) class LSTMModel(nn.Module): def __init__(self, hidden32): super().__init__() self.lstm nn.LSTM(input_size1, hidden_sizehidden, batch_firstTrue) self.fc nn.Linear(hidden, 1) def forward(self, x): out, _ self.lstm(x) return self.fc(out[:, -1, :]) # 取最后一个时间步 model LSTMModel() criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.01)window14表示用前 14 天预测第 15 天这个窗口大小要结合业务周期调汽车销售有周周期就设 7 或 14有月周期就设 30。batch_firstTrue让输入张量形状是(batch, seq, feature)符合直觉。out[:, -1, :]取 LSTM 最后一个时间步的隐状态因为预测的是序列末尾之后的那一天。归一化用MinMaxScaler把销量压到 0-1训练完预测结果要记得inverse_transform还原回真实销量这一步忘了就会得到一堆 0 到 1 之间的「假销量」。4.3 训练循环与早停训练循环本身不复杂但要加早停防止过拟合尤其是数据量小的时候。best_loss float(inf) patience, wait 10, 0 for epoch in range(200): model.train() optimizer.zero_grad() output model(X_t) loss criterion(output, y_t) loss.backward() optimizer.step() if loss.item() best_loss: best_loss loss.item() wait 0 torch.save(model.state_dict(), best_lstm.pt) else: wait 1 if wait patience: print(f早停于 epoch {epoch}) break if epoch % 20 0: print(fepoch {epoch}, loss {loss.item():.5f})patience10表示连续 10 轮验证损失没下降就停torch.save保存最优权重而不是最后一轮权重。学习率lr0.01对 LSTM 偏大如果 loss 震荡不降就调到 0.001。这套训练逻辑在销量这种单变量序列上够用多变量比如加入价格、促销标记就把input_size改成特征数输入张量多拼几列。5. 避坑与排查这几个坑我替你踩过了5.1 中文乱码图里全是方框现象Matplotlib 画出来的标题和标签全是方块。原因默认字体不含中文。解决plt.rcParams[font.sans-serif] [SimHei]Linux 上没有 SimHei 就换成系统里有的中文字体用fc-list :langzh查一下装了哪些。5.2 预测结果全是同一条水平线现象LSTM 预测出来是一条几乎不变的直线。原因归一化后忘了反归一化或者序列本身太平稳、模型学不到东西。解决先确认inverse_transform有没有调用如果序列确实平稳检查是不是用了错误的窗口大小或者数据本身就没有可预测的趋势这时候该退回移动平均而不是硬训 LSTM。5.3 日期解析报错或时区错乱现象parse_dates报 ValueError或者日期整体偏移一天。原因原始日期格式不统一或者带时区信息。解决先用pd.to_datetime(df[sale_date], errorscoerce)看有多少解析失败失败的单独打印出来看格式带时区的用dt.tz_localize(None)去掉时区再处理。5.4 门店分组多出幽灵门店现象按门店分组后数量比实际门店多。原因门店名有前后空格或大小写不一致。解决df[store] df[store].str.strip().str.lower()统一清洗后再分组。这个坑在数据来自多个系统拼接时特别常见。5.5 训练集测试集按随机划分现象时间序列预测效果好得离谱上线就崩。原因用了train_test_split随机划分把未来数据混进了训练集。解决时间序列必须按时间顺序切前 80% 训练、后 20% 测试绝不能随机打乱。这是时序预测最经典的翻车点。6. 进阶技巧用滚动预测验证模型真实能力单次切分测试只能看一个时间点的表现说服力有限。更靠谱的做法是滚动预测walk-forward validation每次用历史数据预测下一天然后把真实值并入历史再预测下一天如此滚动。这样能模拟模型上线后每天真实预测的场景得到的误差更接近实际。def walk_forward(model, series, window14, steps30): history list(series[:len(series)-steps]) preds [] model.eval() for _ in range(steps): x torch.tensor(history[-window:], dtypetorch.float32).view(1, window, 1) with torch.no_grad(): p model(x).item() preds.append(p) history.append(p) # 用预测值续上模拟真实滚动 return preds # 注意series 需为归一化后的列表 preds_scaled walk_forward(model, scaled.flatten().tolist()) preds_real scaler.inverse_transform(np.array(preds_scaled).reshape(-1, 1))这里有个关键选择滚动时用预测值续上还是用真实值续上。用预测值续上更接近真实上线场景因为未来真实值你拿不到但误差会累积用真实值续上是「理想滚动」误差更小但偏乐观。我一般两个都跑对比一下差距差距大说明模型对自身误差敏感上线要谨慎。验证方法上除了 MAE我还会看预测值和真实值的走势是否同步——如果 MAE 不大但预测曲线和真实曲线完全错位那模型只是学到了均值没有学到趋势这种模型不能用于决策。判断标准很简单把预测曲线和真实曲线画在一张图上肉眼看是否贴合拐点。最后说个习惯每次做完一个预测模型我都会把「基线模型的误差」和「复杂模型的误差」并排记在笔记里。如果 LSTM 比移动平均只好一点点我宁愿上线移动平均因为它的可解释性和稳定性远胜黑盒模型。汽车销售这种业务老板要的是「下个月大概卖多少」而不是「模型有多深」能解释、能复现、能维护比精度高零点几个点重要得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站