时间序列分析是数据科学中最常见也最具实战价值的领域之一。无论是金融市场的股价预测、电商平台的销量预估,还是运维场景下的流量监控,本质上都是对按时间顺序排列的数据进行建模与预测。Python 生态在时间序列方向已经非常成熟:Pandas 提供了强大的时间索引与重采样能力,Statsmodels 实现了从 ARIMA 到状态空间模型的经典统计方法,而 Facebook 开源的 Prophet 则让带季节性和节假日效应的预测变得门槛极低。本文将从数据准备、平稳性检验、经典统计模型到现代预测框架,带你完整走通一条可落地的时间序列分析流水线。

一、用 Pandas 构建时间序列数据基础
Pandas 是时间序列分析的事实标准入口。它原生支持
1 | DatetimeIndex |
,可以让你像操作普通 DataFrame 一样对时间维度进行切片、重采样和滚动统计。掌握 Pandas 的时间能力是后续建模的前提。
1.1 创建时间索引与重采样
最常见的需求是把一列时间字符串转换为索引,并按目标频率进行重采样。下面这段代码演示了如何生成一份模拟的日销量数据,并按周聚合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 import pandas as pd
import numpy as np
# 生成模拟日销量数据,带趋势 + 季节性 + 噪声
dates = pd.date_range('2023-01-01', '2024-12-31', freq='D')
t = np.arange(len(dates))
sales = 100 + 0.5 * t + 20 * np.sin(2 * np.pi * t / 365) + np.random.normal(0, 8, len(t))
df = pd.DataFrame({'date': dates, 'sales': sales}).set_index('date')
# 按周重采样,取每周均值
weekly = df.resample('W').mean()
# 按月重采样,取每月总和
monthly = df.resample('M').sum()
print(weekly.head())
print(monthly.tail())
1 | resample |
的
1 | freq |
参数支持丰富的别名:
1 | D |
日、
1 | W |
周、
1 | M |
月末、
1 | Q |
季末、
1 | H |
小时、
1 | T |
分钟。重采样时通过
1 | .mean()/.sum()/.ohlc() |
等聚合方法决定如何合并数据。
1.2 滚动窗口与指数加权
移动平均是时间序列去噪的经典手段。Pandas 的
1 | rolling |
和
1 | ewm |
分别提供简单滚动窗口与指数加权窗口:
1
2
3
4
5
6
7
8
9
10 # 7 日简单移动平均
df['ma7'] = df['sales'].rolling(window=7).mean()
# 30 日移动标准差,用于波动率分析
df['vol30'] = df['sales'].rolling(window=30).std()
# 指数加权移动平均,越近的数据权重越高
df['ewma'] = df['sales'].ewm(span=14, adjust=False).mean()
print(df[['sales','ma7','ewma']].tail(10))
- rolling:窗口内各点权重相同,适合平稳波动分析。
- ewm:近期数据权重更高,对趋势变化更敏感,常用于金融指标的平滑。
- expanding:窗口从起点一直扩展到当前点,适合计算累积统计量。
1.3 缺失时间填充与时间差
真实数据往往存在时间点缺失。Pandas 提供了多种填充策略,需要根据业务语义选择:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 # 重新生成完整时间索引,缺失值用 NaN 填充
full_idx = pd.date_range(df.index.min(), df.index.max(), freq='D')
df = df.reindex(full_idx)
# 前向填充(用上一个已知值填充)
df['sales_ffill'] = df['sales'].ffill()
# 线性插值
df['sales_interp'] = df['sales'].interpolate(method='linear')
# 时间感知插值(按时间距离加权)
df['sales_time'] = df['sales'].interpolate(method='time')
# 计算相邻时间点差分,用于判断趋势方向
df['diff'] = df['sales'].diff()
# 计算百分比变化,常用于收益率
df['pct_change'] = df['sales'].pct_change()
选择插值方法时应考虑数据特性:金融价格数据通常用前向填充(避免引入未来信息),传感器类连续数据适合线性插值,而带明显季节性的数据则可以考虑季节性插值。
二、时间序列的平稳性检验
经典 ARIMA 模型要求序列是平稳的——即均值、方差和自协方差不随时间变化。在建模前必须进行平稳性检验,最常用的是 ADF(Augmented Dickey-Fuller)检验。
2.1 ADF 检验与差分阶数确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from statsmodels.tsa.stattools import adfuller
def adf_test(series, title=''):
result = adfuller(series.dropna(), autolag='AIC')
labels = ['ADF统计量', 'p值', '使用滞后阶数', '观测数', '临界值']
print(f'=== ADF检验: {title} ===')
for label, value in zip(labels, result[:4]):
print(f'{label}: {value}')
for k, v in result[4].items():
print(f' 临界值 {k}: {v}')
print('结论:', '平稳' if result[1] < 0.05 else '非平稳')
return result[1]
adf_test(df['sales_ffill'], '原始序列')
# 一阶差分
adf_test(df['sales_ffill'].diff().dropna(), '一阶差分序列')
ADF 检验的零假设是序列存在单位根(非平稳)。当 p 值小于 0.05 时拒绝零假设,认为序列平稳。如果原始序列非平稳,通常通过差分使其平稳——一阶差分对应
1 | d=1 |
,二阶差分对应
1 | d=2 |
,实践中很少超过二阶。
2.2 ACF 与 PACF 判断 AR/MA 阶数
确定差分阶数
1 | d |
后,还需要确定自回归阶数
1 | p |
和移动平均阶数
1 | q |
。自相关图(ACF)和偏自相关图(PACF)是经典的可视化工具:
1
2
3
4
5
6
7
8
9 import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
plot_acf(df['sales_ffill'].diff().dropna(), lags=40, ax=axes[0])
plot_pacf(df['sales_ffill'].diff().dropna(), lags=40, ax=axes[1], method='ywm')
plt.tight_layout()
plt.savefig('acf_pacf.png', dpi=100)
plt.show()
判断口诀如下:
| 图形 | 截尾特征 | 对应模型 |
|---|---|---|
| ACF | 缓慢衰减(拖尾) | AR(p) |
| PACF | p 阶后截尾 | AR(p) |
| ACF | q 阶后截尾 | MA(q) |
| PACF | 缓慢衰减(拖尾) | MA(q) |
| 两者均拖尾 | 缓慢衰减 | ARMA(p,q) |
三、用 Statsmodels 构建 ARIMA 模型
Statsmodels 是 Python 中最权威的经典统计建模库,其
1 | SARIMAX |
类既能拟合 ARIMA,也支持带季节性的 SARIMA 和外生变量。相比
1 | ARIMA |
老接口,
1 | SARIMAX |
功能更完整,是推荐入口。
3.1 拆分训练集与测试集
时间序列的拆分必须按时间顺序,绝不能随机打乱,否则会引入用未来预测过去的数据泄漏问题:
1
2
3
4
5
6
7
8 series = df['sales_ffill'].asfreq('D').interpolate()
# 后 30 天作为测试集
train = series.iloc[:-30]
test = series.iloc[-30:]
print('训练集:', train.index.min(), '~', train.index.max())
print('测试集:', test.index.min(), '~', test.index.max())
3.2 训练 SARIMA 模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 from statsmodels.tsa.statespace.sarimax import SARIMAX
# (p,d,q) 非季节项,(P,D,Q,s) 季节项,s=365 数据为日级且年周期
# 为演示效率,这里用周数据建模
weekly_train = train.resample('W').mean()
model = SARIMAX(
weekly_train,
order=(2, 1, 2), # p, d, q
seasonal_order=(1, 1, 1, 52), # P, D, Q, s(52 周)
enforce_stationarity=False,
enforce_invertibility=False,
)
results = model.fit(disp=False)
print(results.summary())
1 | summary() |
输出包含系数估计、标准误、p 值、AIC/BIC 以及残差诊断。重点关注:
- 各系数 p 值是否显著(小于 0.05)。
- Ljung-Box 检验 p 值是否大于 0.05(残差无自相关)。
- Jarque-Bera 检验判断残差正态性。
3.3 预测与误差评估
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 from sklearn.metrics import mean_absolute_error, mean_squared_error
import numpy as np
forecast = results.get_forecast(steps=30)
pred_mean = forecast.predicted_mean
pred_ci = forecast.conf_int()
# 评估
mae = mean_absolute_error(test.resample('W').mean(), pred_mean)
rmse = np.sqrt(mean_squared_error(test.resample('W').mean(), pred_mean))
mape = np.mean(np.abs((test.resample('W').mean() - pred_mean) / test.resample('W').mean())) * 100
print(f'MAE: {mae:.2f}')
print(f'RMSE: {rmse:.2f}')
print(f'MAPE: {mape:.2f}%')
常用的回归类误差指标对比:
| 指标 | 含义 | 优点 | 缺点 |
|---|---|---|---|
| MAE | 平均绝对误差 | 直观、对异常值稳健 | 不区分过预测与欠预测 |
| RMSE | 均方根误差 | 对大误差敏感 | 受异常值影响大 |
| MAPE | 平均绝对百分比误差 | 无量纲、便于跨数据集比较 | 真实值为 0 时失效 |
| SMAPE | 对称 MAPE | 有上下界 | 计算略复杂 |
3.4 自动定阶:pmdarima
手动判断 ACF/PACF 需要经验,
1 | pmdarima |
(原 pyramid-arima)封装了基于 AIC 的自动阶数搜索,是 ARIMA 流程的实用补充:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # pip install pmdarima
import pmdarima as pm
auto_model = pm.auto_arima(
weekly_train,
seasonal=True, m=52,
d=1, D=1,
start_p=0, max_p=3,
start_q=0, max_q=3,
trace=True,
error_action='ignore',
stepwise=True,
)
print(auto_model.order, auto_model.seasonal_order)
print(auto_model.aic())
四、用 Prophet 进行带季节性的预测
Statsmodels 的 SARIMA 对强季节性和节假日效应处理起来较为繁琐。Facebook 开源的 Prophet 采用加法回归模型,自动处理趋势变化点、季节性和节假日,对业务人员非常友好,且对缺失值和异常值有较强鲁棒性。

4.1 Prophet 的模型结构
Prophet 把时间序列分解为三个可加成分:
1 y(t) = g(t) + s(t) + h(t) + eps(t)
- g(t):趋势项,支持线性与饱和增长(logistic)两种形式。
- s(t):季节项,用傅里叶级数建模年/周/日周期。
- h(t):节假日效应,由用户提供的节假日表驱动。
- eps(t):残差,假设服从正态分布。
这种显式分解的设计让每类业务效应都可被解释和调整,是 Prophet 相对黑箱模型的最大优势。
4.2 最小可运行示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 # pip install prophet
import pandas as pd
from prophet import Prophet
# Prophet 要求列名固定为 ds(时间)和 y(目标值)
ts = df['sales_ffill'].asfreq('D').interpolate().reset_index()
ts.columns = ['ds', 'y']
train_df = ts.iloc[:-30]
m = Prophet(
growth='linear',
yearly_seasonality=True,
weekly_seasonality=True,
daily_seasonality=False,
changepoint_prior_scale=0.05, # 趋势灵活度,越大越敏感
seasonality_prior_scale=10, # 季节性强度
)
m.fit(train_df)
# 构造未来 30 天的时间表
future = m.make_future_dataframe(periods=30, freq='D')
forecast = m.predict(future)
print(forecast[['ds','yhat','yhat_lower','yhat_upper']].tail(10))
4.3 加入节假日与自定义季节性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # 定义节假日 DataFrame
holidays = pd.DataFrame({
'holiday': 'spring_festival',
'ds': pd.to_datetime(['2024-02-10', '2024-02-11', '2024-02-12']),
'lower_window': -1, # 节前 1 天也受影响
'upper_window': 2, # 节后 2 天也受影响
})
m2 = Prophet(holidays=holidays, yearly_seasonality=True)
m2.add_seasonality(name='monthly', period=30.5, fourier_order=5)
m2.fit(train_df)
# 可视化分解成分
from prophet.plot import plot_components
fig = m2.plot_components(m2.predict(future))
fig.savefig('prophet_components.png')
几个关键调参方向值得牢记:
- changepoint_prior_scale:默认 0.05,调大可捕捉更频繁的趋势转折,但易过拟合。
- seasonality_prior_scale:默认 10,调大让季节波动更明显。
- holidays_prior_scale:控制节假日效应的强度。
- fourier_order:自定义季节性的傅里叶阶数越高,曲线越灵活,但同样容易过拟合。
五、模型选择与交叉验证策略
时间序列不能用普通的 k 折交叉验证,因为会破坏时间顺序。推荐使用滚动窗口或扩展窗口交叉验证:
1
2
3
4
5
6 from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5, test_size=30)
for fold, (tr_idx, te_idx) in enumerate(tscv.split(series), 1):
print(f'Fold {fold}: 训练 {len(tr_idx)} 天, 测试 {len(te_idx)} 天')
选择模型时应综合考虑以下几点,而不是只看单次 MAPE:
| 维度 | SARIMA / Statsmodels | Prophet |
|---|---|---|
| 季节性建模 | 需指定 (P,D,Q,s) | 自动傅里叶分解 |
| 节假日 | 需作为外生变量 | 内置支持 |
| 多变量 | SARIMAX 支持外生变量 | 通过 add_regressor 支持 |
| 可解释性 | 统计推断丰富 | 成分分解直观 |
| 训练速度 | 中等 | 快(基于 Stan) |
| 对缺失值 | 需预处理 | 原生鲁棒 |
六、生产环境落地建议
把时间序列模型从 Notebook 推到生产,有几个工程化要点值得关注。
6.1 模型持久化与版本管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14 import joblib
from prophet.serialize import model_to_json, model_from_json
# Statsmodels 模型用 joblib 保存
joblib.dump(results, 'sarima_model.pkl')
# Prophet 模型用官方序列化
with open('prophet_model.json', 'w') as f:
f.write(model_to_json(m2))
# 重新加载
loaded = joblib.load('sarima_model.pkl')
with open('prophet_model.json') as f:
m_loaded = model_from_json(f.read())
6.2 监控与重训周期
时间序列模型存在概念漂移问题——数据分布随时间变化,旧模型预测质量会逐步下降。建议:
- 每日计算最新窗口的预测误差,当误差超过阈值时触发告警。
- 设定固定重训周期(如每周/每月),用最新数据重新拟合模型。
- 保留多版本模型,便于 A/B 对比与回滚。
- 对预测区间(置信带)做监控,区间异常变宽通常意味着模型不确定性上升。
6.3 与调度系统集成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # 伪代码:Airflow DAG 示例
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta
def retrain_model(**ctx):
# 拉取最新数据
df = fetch_latest_data()
# 训练并评估
model = Prophet().fit(df)
save_model(model)
default_args = {'owner':'ds','retries':1,'retry_delay':timedelta(minutes=5)}
with DAG('ts_retrain', default_args=default_args,
schedule='0 2 * * 1', start_date=datetime(2024,1,1)) as dag:
retrain = PythonOperator(task_id='retrain', python_callable=retrain_model)
总结
本文从数据准备到模型上线,完整梳理了 Python 时间序列分析的核心路径。Pandas 负责时间索引、重采样与窗口统计,是所有后续工作的基石;Statsmodels 的 SARIMAX 提供了严谨的经典统计建模能力,适合需要统计推断的场景;Prophet 则以显式分解和强鲁棒性降低了带季节性与节假日效应的建模门槛。在选型上不必执着于哪个更先进,而应结合数据特征、可解释性需求与工程成本综合判断。最后别忘了把交叉验证、模型持久化和重训周期纳入工程流程——这往往比换一个更复杂的模型更能提升线上效果。
汤不热吧