Pandas: 项目:时间序列

最后更新:2026-08-26

时间序列预测不只是建模——在用 ML 之前,理解趋势和季节性就能做基础预测。本课以 Alice 的咖啡店销售预测为场景,用 resample + rolling + shift 完成从预处理到基础预测的全流程。这是第 17 课工具的实战应用,也是后续 ML 课程的前奏。

⚠️ 注意: 以下代码需在本地 Python 环境中运行。

1. 你将学到


2. 项目背景:咖啡店销售预测

(1) 任务

Alice 想预测下月咖啡店销售额——历史数据有趋势(逐月增长)和季节性(冬季热饮多)。

(2) 分析流程

100%
graph TB
    A["1. 预处理<br>resample+fillna"] --> B["2. 趋势提取<br>rolling+diff"]
    B --> C["3. 季节性分析<br>groupby month"]
    C --> D["4. 移动平均预测<br>rolling mean"]
    D --> E["5. 评估<br>MAE/MAPE"]
    E --> F["6. 下月预测"]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

3. 数据预处理

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:时间序列预处理(难度⭐⭐)

PYTHON
import pandas as pd
import numpy as np

# ============================================
# Step 1: Generate & Preprocess
# ============================================

np.random.seed(42)

# Generate 365 days of sales with trend + seasonality + noise
dates = pd.date_range('2023-01-01', periods=365)
trend = np.linspace(300, 450, 365)  # upward trend
seasonality = 50 * np.sin(2 * np.pi * np.arange(365) / 365 * 2)  # biannual cycle
winter_effect = np.where(dates.month.isin([11, 12, 1, 2]), 30, 0)  # winter boost
noise = np.random.normal(0, 20, 365)

daily_sales = pd.DataFrame({
    'sales': np.round(trend + seasonality + winter_effect + noise, 0),
    'customers': np.round((trend + seasonality + winter_effect + noise) / 8, 0)
}, index=dates)

# Inject missing days
missing_idx = np.random.choice(365, 15, replace=False)
daily_sales.iloc[missing_idx] = np.nan

# Preprocess: fill missing → ensure daily frequency
daily_sales = daily_sales.asfreq('D')  # ensure no missing dates
daily_sales['sales'] = daily_sales['sales'].interpolate(method='time')
daily_sales['customers'] = daily_sales['customers'].interpolate(method='time')

print(f"Date range: {daily_sales.index[0].date()} to {daily_sales.index[-1].date()}")
print(f"Missing after fill: {daily_sales.isnull().sum().sum()}")
print(f"\nDaily sales stats:\n{daily_sales['sales'].describe()}")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

4. 趋势提取

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:趋势与去趋势(难度⭐⭐)

PYTHON
# ============================================
# Step 2: Trend Extraction
# ============================================

# 30-day moving average = trend
daily_sales['trend'] = daily_sales['sales'].rolling(30, center=True).mean()

# Detrended = actual - trend (shows seasonality + noise)
daily_sales['detrended'] = daily_sales['sales'] - daily_sales['trend']

# Month-over-month growth rate
monthly = daily_sales['sales'].resample('M').sum()
monthly_growth = monthly.pct_change() * 100

print("=== Monthly Sales ===")
print(monthly.tail(6))
print(f"\nAvg monthly growth: {monthly_growth.mean():.1f}%")
print(f"Total annual growth: {((monthly.iloc[-1] / monthly.iloc[0]) - 1) * 100:.1f}%")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

5. 季节性分析

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:季节性指数(难度⭐⭐⭐)

PYTHON
# ============================================
# Step 3: Seasonality Analysis
# ============================================

# Monthly average (across all years)
daily_sales['month'] = daily_sales.index.month
monthly_avg = daily_sales.groupby('month')['sales'].mean()

# Seasonal index = monthly avg / overall avg
overall_avg = daily_sales['sales'].mean()
seasonal_index = (monthly_avg / overall_avg).round(3)

print("=== Seasonal Index ===")
for m, idx in seasonal_index.items():
    label = "↑" if idx > 1.05 else ("↓" if idx < 0.95 else "→")
    print(f"  Month {m:2d}: {idx:.3f} {label}")

# Day of week pattern
daily_sales['dayofweek'] = daily_sales.index.dayofweek
dow_avg = daily_sales.groupby('dayofweek')['sales'].mean()
dow_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
print(f"\n=== Day of Week Avg ===")
for i, name in enumerate(dow_names):
    print(f"  {name}: ${dow_avg[i]:.0f}")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

6. 移动平均预测

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:rolling 预测(难度⭐⭐⭐)

PYTHON
# ============================================
# Step 4: Moving Average Forecast
# ============================================

# Train/test split: last 30 days as test
train = daily_sales.iloc[:-30]
test = daily_sales.iloc[-30:]

# Simple moving average forecast (use last N days' avg)
def ma_forecast(train_series, window, horizon):
    """Forecast next 'horizon' days using moving average"""
    last_ma = train_series.rolling(window).mean().iloc[-1]
    return pd.Series([last_ma] * horizon,
                     index=pd.date_range(train.index[-1] + pd.Timedelta(days=1),
                                         periods=horizon))

# Try different windows
for w in [7, 14, 30]:
    forecast = ma_forecast(train['sales'], w, 30)
    mae = (forecast - test['sales']).abs().mean()
    mape = ((forecast - test['sales']) / test['sales']).abs().mean() * 100
    print(f"MA({w:2d}): MAE=${mae:.0f}, MAPE={mape:.1f}%")

# Seasonal-adjusted forecast
# forecast = trend_forecast × seasonal_index
last_trend = train['sales'].rolling(30).mean().iloc[-1]
monthly_trend_growth = train['sales'].rolling(30).mean().diff(30).mean()
forecast_months = test.index.month
seasonal_forecast = pd.Series(
    [(last_trend + monthly_trend_growth * i) * seasonal_index[m]
     for i, m in enumerate(forecast_months)],
    index=test.index
)
mae_seasonal = (seasonal_forecast - test['sales']).abs().mean()
mape_seasonal = ((seasonal_forecast - test['sales']) / test['sales']).abs().mean() * 100
print(f"\nSeasonal-adjusted: MAE=${mae_seasonal:.0f}, MAPE={mape_seasonal:.1f}%")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

7. 预测评估

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

:评估与下月预测(难度⭐⭐)

PYTHON
# ============================================
# Step 5-6: Evaluation & Next Month Forecast
# ============================================

# Evaluation metrics
def evaluate(actual, forecast, name="Model"):
    mae = (forecast - actual).abs().mean()
    rmse = ((forecast - actual) ** 2).mean() ** 0.5
    mape = ((forecast - actual) / actual).abs().mean() * 100
    print(f"{name}: MAE=${mae:.0f}, RMSE=${rmse:.0f}, MAPE={mape:.1f}%")
    return mae, rmse, mape

# Evaluate best model
print("=== Model Evaluation ===")
evaluate(test['sales'], ma_forecast(train['sales'], 14, 30), "MA(14)")
evaluate(test['sales'], seasonal_forecast, "Seasonal-adjusted")

# Next month forecast (Jan 2024)
next_month_days = pd.date_range('2024-01-01', periods=31)
last_trend_full = daily_sales['sales'].rolling(30).mean().iloc[-1]
jan_index = seasonal_index[1]
next_month_forecast = last_trend_full * jan_index

print(f"\n=== January 2024 Forecast ===")
print(f"  Daily forecast: ${next_month_forecast:.0f}")
print(f"  Monthly forecast: ${next_month_forecast * 31:,.0f}")
print(f"  Based on trend: ${last_trend_full:.0f} × seasonal index: {jan_index:.3f}")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。

❓ 常见问题

Q 移动平均窗口怎么选?
A 窗口=数据周期最稳妥。日数据周周期=7,月周期=30。窗口太小→噪声大,窗口太大→滞后严重。建议先试与自然周期相同的窗口,再±几个对比 MAPE。本课中 MA(14) 通常比 MA(7) 和 MA(30) 更平衡。
Q 趋势和季节性怎么分离?
A 趋势=长期移动平均(30 日以上),季节性=月度均值/总均值的比率。去趋势=原始值-趋势值(加法模型)或原始值/趋势值(乘法模型)。零售数据通常用乘法模型(季节波动与水平成正比),气温数据用加法模型。
Q MAPE 多少算好?
A MAPE<10% 高精度,10-20% 良好,20-50% 合理,>50% 不可靠。但要看场景——日销波动大时 MAPE 20% 已经不错,月度汇总后 MAPE 应降到 10% 以内。预测越远越不准——7 天预测比 30 天预测可靠得多。
Q 预测多久可靠?
A 经验法则:可靠预测范围≈历史数据的 1/3 到 1/2。1 年历史数据最多可靠预测 4-6 个月。移动平均只能预测"平"——无法预测拐点。趋势变化或突发事件会让预测完全失效。预测=辅助决策,不是水晶球。
Q 和 ML 预测区别?
A 移动平均预测假设"未来是过去的延续"——只能捕捉趋势和季节性。ML(ARIMA/LSTM/Prophet)能建模更复杂的模式(非线性趋势、多变量交互、假期效应)。本课是 ML 的基线——如果简单移动平均 MAPE 15%,ML 应该做到 10% 以下才值得用。
Q 季节指数怎么算?
A 每月平均值/总体平均值。如 1 月均值 420,总体均值 380,则季节指数=420/380=1.105(1 月比平均高 10.5%)。指数>1 是旺季,<1 是淡季。至少需要 2 年数据算可靠的季节指数(每年 1 月可能不同)。
Q 缺失值用 interpolate 还是 ffill?
A 时间序列推荐 interpolate——用相邻值插值更平滑(保持趋势)。ffill 用上一个值填充,会在缺失期间产生阶梯(趋势断层)。大数据缺失用 interpolate(method='time') 考虑时间距离,小缺失用 linear 即可。

📖 小节


📝 作业

  1. 基础题(难度⭐):生成 180 天销售数据(含趋势),用 resample('M') 月度汇总,计算月环比增长率。
  2. 进阶题(难度⭐⭐):生成含季节性的 365 天数据,提取 30 日趋势线,计算 12 个月季节指数,识别旺季和淡季。
  3. 挑战题(难度⭐⭐⭐):完成完整预测项目:预处理→趋势+季节性→MA(7/14/30) 预测→季节调整预测→MAPE 评估→下月预测输出。

← 上一课:项目-数据分析 · 下一课:项目-综合分析 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏