Python中分解时间序列:如何获取无缺失趋势分量用于在线变点检测?
问题背景
我需要对每日时间序列的趋势分量做在线变点检测,避免季节性因素导致误报。但用statsmodels的seasonal_decompose和STL方法时,因为底层用了中心化移动平均(CMA),趋势分量的首尾会出现NaN值。我有4年多的每日数据,需要每日更新的完整趋势分量来检测变化,但目前哪怕数据包含2个以上周期,趋势分量首尾仍各有6个月的NaN,求问题原因和替代方法。
当前使用的代码:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.seasonal import seasonal_decompose df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True) ts = df['value'].values result = seasonal_decompose(ts, model='additive', period=365) # 年度季节性 trend_component = result.trend valid_indices = ~np.isnan(trend_component) trend_component_np = trend_component[valid_indices].reshape(-1, 1) detector = rpt.Pelt(model="l2").fit(trend_component_np) change_points = detector.predict(pen=150)
问题原因解释
中心化移动平均(CMA)的计算逻辑导致了首尾NaN:
- 对于周期为365的年度季节性,
seasonal_decompose先计算365天的简单移动平均(SMA),再对这个SMA做2期移动平均来中心化,最终得到趋势分量。 - 计算开头的趋势值时,需要前182天左右((365+1)/2≈182)的前置数据;计算结尾的趋势值时,需要后182天的后置数据。
- 这是CMA方法的固有缺陷,哪怕有4年数据,首尾的182天仍然没有足够的相邻数据完成计算,因此会生成NaN,和数据包含的周期数量无关。
替代方法
以下是几种能生成完整趋势分量、适配每日在线更新的方法:
1. 简单移动平均(SMA)
直接用季节周期长度作为窗口计算SMA,替代CMA,不会产生NaN,计算简单:
# 方式1:中心化窗口,首尾仍有少量NaN,但比CMA少很多 trend_component = df['value'].rolling(window=365, center=True).mean() # 方式2:非中心化窗口,仅开头364天有NaN,适合在线更新时关注最新数据 trend_component = df['value'].rolling(window=365, center=False).mean()
- 优点:计算快,非中心化模式下无尾部NaN,每日增量更新成本低。
- 缺点:平滑效果弱于CMA,对异常值敏感度较高。
2. 指数加权移动平均(EWMA)
依赖权重衰减逻辑,不需要固定窗口,自然覆盖首尾数据,适合在线更新:
# span参数模拟365天的平滑效果,span≈2/(alpha+1),alpha为衰减系数 trend_component = df['value'].ewm(span=365, adjust=False).mean()
- 优点:无NaN生成,更新速度快,对近期数据赋予更高权重,适配渐变趋势。
- 缺点:需要调整span参数匹配季节性周期,平滑逻辑和CMA差异较大。
3. LOWESS局部加权回归
statsmodels提供的非参数拟合方法,可拟合非线性趋势,全程无NaN:
from statsmodels.nonparametric.smoothers_lowess import lowess # frac参数控制平滑程度,0.2表示用20%的数据拟合每个点 trend_component = lowess(df['value'].values, df.index, frac=0.2, return_sorted=False) trend_component = pd.Series(trend_component, index=df.index)
- 优点:无NaN,能拟合复杂非线性趋势,对异常值鲁棒性强。
- 缺点:计算量大于移动平均,在线更新时需重新拟合全序列(或做增量拟合)。
4. Prophet模型
Facebook的Prophet模型可自动分解趋势、季节性与节假日效应,输出完整趋势分量,支持在线更新:
from prophet import Prophet # 转换为Prophet要求的格式 prophet_df = df.reset_index().rename(columns={'date': 'ds', 'value': 'y'}) model = Prophet(yearly_seasonality=True) model.fit(prophet_df) # 提取趋势分量 trend_component = model.predict(prophet_df)['trend'].values trend_component = pd.Series(trend_component, index=df.index)
- 优点:自动处理季节性,输出完整趋势,支持增量更新,对缺失值鲁棒。
- 缺点:模型复杂度高,计算时间长于移动平均类方法。
在线变点检测适配
无论用哪种方法得到无NaN的趋势分量,都可直接传入变点检测代码,无需再过滤NaN:
# 假设trend_component是无NaN的Series detector = rpt.Pelt(model="l2").fit(trend_component.values.reshape(-1, 1)) change_points = detector.predict(pen=150)
内容的提问来源于stack exchange,提问作者Nikita Singh
相关产品推荐
相关产品推荐

