欢迎光临

Python时间序列分析完全指南:Pandas时间索引、Statsmodels ARIMA与Prophet预测建模实战

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

Python时间序列分析数据可视化

一、用 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 采用加法回归模型,自动处理趋势变化点、季节性和节假日,对业务人员非常友好,且对缺失值和异常值有较强鲁棒性。

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 则以显式分解和强鲁棒性降低了带季节性与节假日效应的建模门槛。在选型上不必执着于哪个更先进,而应结合数据特征、可解释性需求与工程成本综合判断。最后别忘了把交叉验证、模型持久化和重训周期纳入工程流程——这往往比换一个更复杂的模型更能提升线上效果。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Python时间序列分析完全指南:Pandas时间索引、Statsmodels ARIMA与Prophet预测建模实战
分享到: 更多 (0)