跳至主要内容

Python 时间序列分析教程

获取“diet”“gym”等关键词的 Google Trends 数据,观察其随时间的变化,同时学习时间序列中的趋势与季节性。
· 15分钟 阅读

使用 AI 探索

在 1 月 4 日的 Facebook Live 实操课程中,我们查看了关键词“diet”“gym”和“finance”的 Google 趋势数据,看看它们随时间如何变化。我们也问自己:当大家在一月立下新年目标时,这些词的搜索量会不会更多?

在本教程中,您将逐步走过我们在课程中写下的代码。您不会做太多数学推导,重点将放在对该数据集的可视化探索上。


想进一步学习 pandas,请查看 DataCamp 的Python 数据操作学习路径。想了解使用 pandas 的时间序列,请查看在 Python 中处理时间序列数据课程。

导入包与数据

所以问题仍在:当大家在一月下定决心重新开始时,这些词的搜索会不会更多?

让我们通过访问此处查看数据找出答案。注意,本教程灵感来源于FiveThirtyEight 的这篇文章。

您也可以将数据下载为 .csv,保存到文件并导入您自己的 Python 环境,进行自主分析。我们现在就来这么做。开始吧!

首先,导入一些包:这里将使用 numpy、pandas、matplotlib 和 seaborn。

此外,如果您希望在 Jupyter Notebook 中绘图显示,可以在代码中加入 IPython 魔法命令 %matplotlib inline。或者,您也可以通过 sns.set() 切换到 Seaborn 的默认样式:

# Import packages
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set()

使用 .read_csv() 导入您下载的数据,并用 .head() 查看前几行。

注意,这里添加了 skiprows 参数以跳过文件开头的第一行。

 df = pd.read_csv('data/multiTimeline.csv', skiprows=1)
df.head()
 Monthdiet: (Worldwide)gym: (Worldwide)finance: (Worldwide)
02004-011003148
12004-02752649
22004-03672447
32004-04702248
42004-05722243

您也可以使用 .info() 方法查看数据类型、行数等信息:

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 168 entries, 0 to 167
Data columns (total 4 columns):
Month                   168 non-null object
diet: (Worldwide)       168 non-null int64
gym: (Worldwide)        168 non-null int64
finance: (Worldwide)    168 non-null int64
dtypes: int64(3), object(1)
memory usage: 5.3+ KB

现在您已经从 Google 趋势导入了数据并做了初步查看,接下来要整理数据,让它更适合分析。

整理数据

首先,您需要重命名 DataFrame df 的列,使其不包含空格。这有多种做法,此处我们直接给 df.columns 重新赋值为期望的列名列表。

通过调用 df.head() 再次核对重命名结果:

df.columns = ['month', 'diet', 'gym', 'finance']
df.head()
 monthdietgymfinance
02004-011003148
12004-02752649
22004-03672447
32004-04702248
42004-05722243

接着,您将把 'month' 列转换为 DateTime 类型,并将其设为 DataFrame 的索引。

注意,之所以这样做,是因为 .info() 的结果显示 'Month' 列的数据类型是 object。该泛型类型可以表示字符串、整数等多种类型,但在处理时间序列时并不理想。因此使用 .to_datetime() 将 DataFrame 中的 'month' 列转换为 DateTime。

请小心!在为 DataFrame df 设定索引时,确保包含 inplace 参数,以实际修改原有索引并设为 'month' 列。

df.month = pd.to_datetime(df.month)
df.set_index('month', inplace=True)
df.head()
 dietgymfinance
month   
2004-01-011003148
2004-02-01752649
2004-03-01672447
2004-04-01702248
2004-05-01722243

现在是时候对 DataFrame 进行可视化探索了。

一点探索性数据分析(EDA)

您可以使用 pandas 的内置可视化方法 .plot(),将数据绘制为同一图中的 3 条折线图(每个列各一条,即 'diet'、'gym' 和 'finance')。

注意,您也可以为该方法指定一些参数,如 figsize、linewidth 和 fontsize,分别控制图像尺寸、线宽和字体大小。

另外,您会发现 x 轴上显示的并非月份(尽管默认标签如此暗示),而是年份。为使图形更准确,我们将 x 轴标签指定为 'Year',并将字体大小设为 20。

提示:如果想抑制 Matplotlib 的输出,只需在最后一行代码后加分号 ;!

df.plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

pandas visualization line graph

注意,这些数据是相对值。正如 Google 趋势所述:

数值表示相对于所选地区与时段中该图表最高点的搜索热度。数值 100 表示该术语的峰值人气。数值 50 表示该术语的人气为峰值的一半。同样地,得分 0 表示该术语的人气低于峰值的 1%。

如果您愿意,也可以将 'diet' 列单独绘制为时间序列:

df[['diet']].plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

pandas visualization graph time series

注意:首先可以看到季节性:每年一月都有明显的跳升。同时似乎也存在趋势:先略有上升,再下降,又上升,然后再下降。换言之,该时间序列既包含趋势,也包含季节性成分。

考虑到这些,接下来您将学习如何识别时间序列中的趋势!

识别时间序列趋势有多种思路。其中一个常见方法是计算滚动平均:即对每个时间点,取其两侧若干点的平均值。注意,这里的点数由您选择的窗口大小决定。

由于取了平均,往往能平滑噪声与季节性。下面马上来看一个示例。使用 pandas 内置方法查看 'diet' 的滚动平均。

关于窗口大小的确定,这里先试 12 个月,因为我们讨论的是年度季节性。

diet = df[['diet']]
diet.rolling(12).mean().plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

time series graph

注意,上面的代码使用了两层方括号来将 'diet' 列提取为 DataFrame;如果只用一层方括号,如 df['diet'],则会得到 pandas 的 Series。

上面的代码还演示了方法链式调用:在一个对象上连续调用方法。方法链很流行,而 pandas 尤其支持这种风格!

现在您得到了想要的趋势!与之前的图相比,大部分季节性已经被去除。

您也可以用相同的窗口大小,使用 pandas 内置方法绘制 'gym' 的滚动平均:

gym = df[['gym']]
gym.rolling(12).mean().plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

time series graph 2

季节性被成功去除,您能看到“gym”呈上升趋势!但这两个搜索词如何对比?

可以把 'gym' 与 'diet' 的趋势绘制在同一图中进行比较:

df_rm = pd.concat([diet.rolling(12).mean(), gym.rolling(12).mean()], axis=1)
df_rm.plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

time series graph 3

您创建了新的 DataFrame df_rm,其中包含 'diet' 与 'gym' 的滚动平均两列。这里使用了 pd.concat() 函数,第一参数传入列的列表,并因按列合并而将 axis 设为 1。

随后像之前一样用 plot() 绘图!因此,在去除季节性后,可以看到 diet 可能仍有某种季节性,而 gym 实际在上升!

在识别出数据中的趋势之后,接下来思考季节性,即时间序列的重复性。正如本教程开头所见,这些数据的时间序列似乎同时具有趋势与季节性成分。

时间序列数据中的季节性模式

研究数据的季节性成分的一种方式是移除时间序列中的趋势,从而更容易考察季节性。移除趋势的一种做法是将您上面计算的趋势(滚动平均)从原始信号中减去。但这会依赖于您平均的点数。

另一个移除趋势的方法叫“差分”,即考察相邻数据点之间的差(称为“一阶差分”,因为只看当前点与其前一个点的差)。

一阶差分

您可以使用 pandas 的 diff() 与 plot() 方法,计算并绘制 'diet' 序列的一阶差分:

diet.diff().plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

time series graph 3

您会发现大部分趋势被移除,并且可以清晰看到每年一月的峰值。每逢一月,最高搜索项会出现 20% 或更多的巨大跃升!

注意:如果趋势尚未完全移除,也可以进行二阶差分,即考察一个点与其前两个点之间的差。关于差分的更多信息见此处。

差分对于将时间序列转为平稳序列非常有用。尽管这里不会展开,平稳时间序列指统计特性(如均值与方差)不随时间变化的序列。许多时间序列预测方法都基于时间序列近似平稳的假设,因此这类序列很有用。

掌握上述内容后,接下来您将通过查看自相关函数来分析时间序列的周期性。不过在此之前,先简单回顾一下相关性。

周期性与自相关

如果一个时间序列以等间隔重复自身(例如每 12 个月),则称其为周期性。

换个思路,如果时间序列在某处有峰值,那么在 12 个月后也会出现峰值;如果在某处有谷值,12 个月后也会出现谷值。

再换一种说法:该时间序列与其自身平移 12 个月后的序列具有相关性。也就是说,如果把时间序列向前或向后移动 12 个月,在某种意义上它能映射回自身。

考虑时间序列与其自身平移版本之间的相关性,这正是自相关的概念所刻画的。

很快就会用到这一点。

首先,让我们用一种更直观的方式回忆一下相关性的概念!

两个变量的相关系数反映它们线性相关的程度。为理解这一点,我们借助包含花朵测量值的 iris 数据集,来看一个实际示例。

为深入研究,导入 scikit-learn 的 iris 数据集,将其转换为 DataFrame,并用 .head() 查看前几行:

from sklearn import datasets
iris = datasets.load_iris()
df_iris = pd.DataFrame(data= np.c_[iris['data'], iris['target']],
                     columns= iris['feature_names'] + ['target'])
df_iris.head()
 sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)target
05.13.51.40.20.0
14.93.01.40.20.0
24.73.21.30.20.0
34.63.11.50.20.0
45.03.61.40.20.0

提醒一下,为便于理解该数据集:所有花都有萼片(sepal)和花瓣(petal)。萼片包裹花瓣,通常为绿色且叶状;花瓣通常色彩鲜艳。目标变量 'target' 是鸢尾花的品种,可能是 Versicolor、Virginica 或 Setosa。上表中分别编码为 0、1、2。

接下来,为思考相关性,我们来查看萼片长度与萼片宽度的相关情况。为此,使用 pandas 或 seaborn 绘制 'sepal length' 与 'sepal width' 的散点图:

sns.lmplot(x='sepal length (cm)', y='sepal width (cm)', fit_reg=False, data=df_iris);

correlation graph

注意,我们通过将 fit_reg 设为 False 关闭了线性回归拟合。

萼片长度与宽度在所有花中是正相关还是负相关?在每个品种内又是正相关还是负相关?这是一个关键区别。

前者意味着当萼片长度增加时,萼片宽度也以线性方式增加;后者则意味着当萼片长度增加时,萼片宽度会以线性方式减少。

乍看之下,上图似乎呈负相关:随着萼片长度增加,萼片宽度略有下降。

现在我们绘制按目标(species)着色的 'sepal length' 与 'sepal width' 散点图:

sns.lmplot(x='sepal length (cm)', y='sepal width (cm)', fit_reg=False, data=df_iris, hue='target');

correlation graph 2

乍看之下,上图呈现正相关:在每个鸢尾花品种中,随着萼片长度增加,萼片宽度也增加。

可视化有助于建立对相关性的直觉,而进一步的方式是实际计算相关系数。

您可以使用 .corr() 方法计算每对测量值的相关系数:

df_iris.corr()
 sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)target
sepal length (cm)1.000000-0.1093690.8717540.8179540.782561
sepal width (cm)-0.1093691.000000-0.420516-0.356544-0.419446
petal length (cm)0.871754-0.4205161.0000000.9627570.949043
petal width (cm)0.817954-0.3565440.9627571.0000000.956464
target0.782561-0.4194460.9490430.9564641.000000

注意,“sepal length (cm)”与“sepal width (cm)”在整体上似乎为负相关!确实如此:全体花样本上的相关系数为 -0.1。但在各个品种内部却不是负相关,其中与 target 的系数为 0.78。

对于感兴趣的读者,这被称为辛普森悖论,在因果推断中非常关键。详情可阅读这里。

我们进一步探索:计算各品种内部每对测量值的相关系数。做法是链式调用 .groupby() 与 .corr(),按目标分组并计算相关系数:

df_iris.groupby(['target']).corr()
  petal length (cm)petal width (cm)sepal length (cm)sepal width (cm)
target     
0.0petal length (cm)1.0000000.3063080.2638740.176695
petal width (cm)0.3063081.0000000.2790920.279973
sepal length (cm)0.2638740.2790921.0000000.746780
sepal width (cm)0.1766950.2799730.7467801.000000
1.0petal length (cm)1.0000000.7866680.7540490.560522
petal width (cm)0.7866681.0000000.5464610.663999
sepal length (cm)0.7540490.5464611.0000000.525911
sepal width (cm)0.5605220.6639990.5259111.000000
2.0petal length (cm)1.0000000.3221080.8642250.401045
petal width (cm)0.3221081.0000000.2811080.537728
sepal length (cm)0.8642250.2811081.0000000.457228
sepal width (cm)0.4010450.5377280.4572281.000000

从该相关矩阵可见:

  • 对于 target 0,萼片长度与宽度的相关为 0.75;
  • 对于 target 1,系数为 0.5;以及
  • 对于 target 2,相关为 0.46。

这些都是逐渐减小的正相关,但都远高于最初整体上的负相关。

这非常有启发性,再次提醒我们要彻底分析数据的重要性。

既然已更深入地了解相关性,您就可以通过查看自相关函数来分析时间序列的周期性了!

首先,再次绘制所有时间序列,回顾它们的形态:

df.plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

time series graph 4

然后,使用 .corr() 计算这些时间序列之间的相关系数:

df.corr()
 dietgymfinance
diet1.000000-0.100764-0.034639
gym-0.1007641.000000-0.284279
finance-0.034639-0.2842791.000000

以上结果说明了什么?

聚焦 'diet' 与 'gym':它们是负相关的。这很有意思!回想我们有季节性与趋势两部分。从相关系数看,“diet”与“gym”为负相关。但从时间序列图看,它们的季节性成分似乎是正相关,而趋势成分似乎是负相关。

总体相关系数实际同时捕捉了这两部分。

接下来要做的是:先绘制这些时间序列的一阶差分,再计算它们的一阶差分的相关性,因为这将近似反映季节性成分的相关性。请记住,去除趋势后可能会揭示季节性之间的相关。

首先使用 .diff() 与 .plot() 绘制一阶差分:

df.diff().plot(figsize=(20,10), linewidth=5, fontsize=20)
plt.xlabel('Year', fontsize=20);

time series graph 5

您会看到,在去除趋势后,'diet' 与 'gym' 的相关性非常强。现在计算这些时间序列一阶差分的相关系数:

df.diff().corr()
 dietgymfinance
diet1.0000000.7587070.373828
gym0.7587071.0000000.301111
finance0.3738280.3011111.000000

注意,再次验证:在考虑趋势与季节成分时存在轻微负相关;而就季节性成分而言,'diet' 与 'gym' 高度相关,系数为 0.76。

自相关

现在您已了解变量间相关与时间序列相关,接下来绘制 'diet' 序列的自相关图:x 轴为滞后(lag),y 轴为该时间序列在该滞后下与自身的相关程度。

这意味着,如果原始时间序列每两天重复一次,您会期望在自相关函数的 2 天处看到一个峰值。

这里我们查看图形,预期在 12 个月处出现自相关峰值:时间序列与自身平移 12 个月后相关。

使用 pandas 的 plotting 接口中的 autocorrelation_plot() 函数来绘制 'diet' 的自相关:

pd.plotting.autocorrelation_plot(diet);

autocorrelation plot

如果在坐标轴上包含更多滞后值,您会看到在 12 个月处出现巨大相关峰值;在 24 个月也有一个峰;在 36 个月也有,但随着滞后变大,相关性会逐渐减弱。

当然,在滞后为 0 时,序列与自身的相关为 1。

图中的虚线表示相关性的统计显著性。在本例中,可以认为 'diet' 序列在 12 个月滞后处确实具有自相关。

至此,您已经识别出 12 个月重复的季节性!

总结

在本教程中,您走过了不少内容!我们查看了关键词“diet”“gym”,并粗略查看了“finance”的 Google 趋势数据,考察它们随时间的变化。您学习了季节性、趋势、相关性、自相关等概念……

对于跃跃欲试的数据科学家,有两件事可以立即开展:

主题