一个时间序列可视化神器:Plotnine


一图胜千言,可视化是一种从数据中快速有效获取有效信息的方法。本文云朵君和大家一起学习使用图形探索时间序列数据。

我们将利用6种不同的图表来揭示时间序列数据的各个方面。重点介绍Python中的plotnine库,这是一种基于图形语法(Grammar of Graphics)的绘图工具。

探索性数据分析(Exploratory Data Analysis,EDA)旨在揭示数据集内在的结构和模式。这个过程几乎总是涉及使用可视化技术来呈现数据。

对于时间序列数据,使用图形进行分析可以帮助我们快速发现:

  • 基本模式,如趋势或周期性规律
  • 异常情况,包括缺失值或异常值
  • 数据分布的变化

本文需要用到的python库如下:

from datasetsforecast.m3 import M3
import plotnine as p9
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import acf
from sklearn.model_selection import train_test_split
from mizani.breaks import date_breaks
from mizani.formatters import date_format
from numerize import numerize
from statsmodels.tsa.seasonal import STL

探索时间序列

我们首先加载一个时间序列。这里我们将使用 M3 数据集中提供的月度时间序列。直接从datasetsforecast库中获取它:

from datasetsforecast.m3 import M3
dataset, *_ = M3.load('./data''Monthly')
series = dataset.query(f'unique_id=="M400"')

注意,使用该方法是需要满足pandas版本要求,否则会报错。直接安装对应新版即可。

现在云朵君将和大家一起学习如何使用 plotnine 创建图形。这个库是 Python 的一种 ggplot2。

如果你还没有安装,直接安装即可。

从设置主题开始:

import plotnine as p9

MY_THEME = p9.theme_538(base_family='Palatino', base_size=12) + \
           p9.theme(plot_margin=.025,
                    axis_text_y=p9.element_text(size=10),
                    panel_background=p9.element_rect(fill='white'),
                    plot_background=p9.element_rect(fill='white'),
                    strip_background=p9.element_rect(fill='white'),
                    legend_background=p9.element_rect(fill='white'),
                    axis_text_x=p9.element_text(size=10))

我们将使用基于theme_538的主题并进行一些额外的修改。

时间序列图

绘制时间序列图是时间序列分析的第一步。时间序列图是一种线形图,用于展示数据值随时间的变化趋势。

import plotnine as p9

time_plot = p9.ggplot(data=series) + \
            p9.aes(x='ds', y='y') + \
            MY_THEME + \
            p9.geom_line(color='#58a63e', size=1) + \
            p9.labs(x='Datetime', y='value')

time_plot + p9.theme(figure_size=(8,3))

时间序列图

通过观察时间序列图,我们可以快速发现数据中存在的一些基本模式,如趋势、周期性等。同时,如果数据的均值或方差出现明显变化,在图上也能一目了然。

示例数据表现出一种随机趋势,数据值先是上升到一个拐点,之后开始下降。同时周期性的波动表明数据中可能存在季节性成分。

还可以使用分解数据构建时间图。首先,我们使用 STL 分解时间序列:

import pandas as pd
from statsmodels.tsa.seasonal import STL

ts_decomp = STL(series['y'], period=12).fit()

components = {
    'Trend': ts_decomp.trend,
    'Seasonal': ts_decomp.seasonal,
    'Residuals': ts_decomp.resid,
}

components_df = pd.DataFrame(components).reset_index()
melted_data = components_df.melt('index')

然后,我们使用facet_grid为每个部分创建时间图,如下所示:

from numerize import numerize 

# 一个在图形中总结大值的好技巧
labs = lambda lst: [numerize.numerize(x) for x in lst] 

decomposed_timeplot = \ 
    p9.ggplot(melted_data) + \ 
    p9.aes(x= 'index' , y= 'value' ) + \ 
    p9.facet_grid( 'variable ~.' , scales= 'free' ) + \ 
    MY_THEME + \ 
    p9.geom_line(color= '#58a63e' , size= 1 ) + \ 
    p9.labs(x= 'Datetime index' ) + \ 
    p9.scale_y_continuous(labels=labs)

时间序列分解图

此变体使检查每个组件变得更容易。在这种情况下,趋势和季节性影响变得清晰。

我们使用numerize使大数字更清晰易读。你也可以将此样式添加到任何其他绘图中。

滞后图

滞后散点图是将时间序列的当前值与前一个值(滞后值)画在平面坐标系上。

X = [series['y'].shift(i) for i in list(range(20-1))]
X = pd.concat(X, axis=1).dropna()
X.columns = ['t-1''t']

lag_plot = p9.ggplot(X) + \
           p9.aes(x='t-1', y='t') + \
           MY_THEME + \
           p9.geom_point(color='#58a63e') + \
           p9.labs(x='Series at time t-1',
                   y='Series at time t') + \
           p9.scale_y_continuous(labels=labs) + \
           p9.scale_x_continuous(labels=labs)

滞后散点图可以帮助我们了解时间序列的内在结构。如果数据点沿对角线密集分布,说明该时间序列存在自相关性,点分布越集中则自相关性越强。如果数据点分散分布,则表明该序列是随机的,前值对后值没有预测作用。

时间序列滞后图

滞后散点图还可用于发现异常值,异常值点将远离数据点的密集区域。

示例数据的点倾向于沿对角线分布,但当值越大时,离散程度也越大。这种特征表明该序列可能存在自回归结构。

自相关图

自相关性是衡量时间序列在过去值(滞后)中观察到的与自身相关的程度的指标。绘制自相关系数图有助于了解时间序列的内部结构。

你可以使用statsmodels来计算自相关:

import numpy as np
from statsmodels.tsa.stattools import acf

acf_x = acf(
    series['y'],
    nlags=24,
    alpha=0.05,
    bartlett_confint=True
)

acf_vals, acf_conf_int = acf_x[:2]

acf_df = pd.DataFrame({
    'ACF': acf_vals,
    'ACF_low': acf_conf_int[:, 0],
    'ACF_high': acf_conf_int[:, 1],
})

acf_df['Lag'] = ['t'] + [f't-{i}' for i in range(125)]
acf_df['Lag'] = pd.Categorical(acf_df['Lag'], categories=acf_df['Lag'])

然后,我们使用plotnine构建一个棒棒糖图:

significance_thr = 2 / np.sqrt(len(series['y']))

acf_plot = p9.ggplot(acf_df, p9.aes(x='Lag', y='ACF')) + \
           p9.geom_hline(yintercept=significance_thr,
                         linetype='dashed',
                         color='#58a63e',
                         size=.8) + \
           p9.geom_hline(yintercept=-significance_thr,
                         linetype='dashed',
                         color='#58a63e',
                         size=.8) + \
           p9.geom_hline(yintercept=0, linetype='solid', color='black', size=1) + \
           p9.geom_segment(p9.aes(x='Lag',
                                  xend='Lag',
                                  y=0, yend='ACF'),
                           size=1.5,
                           color='#58a63e'
                           ) + \
           p9.geom_point(size=4, color='darkgreen', ) + \
           MY_THEME

自相关图

如果自相关系数随滞后阶数的增加而缓慢衰减,表明数据可能存在趋势成分;如果自相关系数呈现出明显的波动模式,峰值出现在特定的滞后阶数上,则说明数据中可能存在明显的周期性。如果自相关系数始终接近于0,则表明该序列可能是白噪声序列,即随机序列。

季节子序列图

有些图形工具专门用于探究时间序列的季节性成分,如季节子序列图。

季节子序列图的绘制方法是:根据数据的季节周期,将整个序列分组,每组包含一个完整的季节周期。然后将每个周期的数据值绘制在同一张图上,从而可视化观察序列在不同季节的表现模式。如下所示:

grouped_df = series.groupby('Month')['y']
group_avg = grouped_df.mean()
group_avg = group_avg.reset_index()
series['Month'] = pd.Categorical(series['Month'], 
                                 categories=series['Month'].unique())
group_avg['Month'] = pd.Categorical(group_avg['Month'], 
                                    categories=series['Month'].unique())

seas_subseries_plot = \
    p9.ggplot(series) + \
    p9.aes(x='ds',
           y='y') + \
    MY_THEME + \
    p9.theme(axis_text_x=p9.element_text(size=8, angle=90),
             legend_title=p9.element_blank(),
             strip_background_x=p9.element_text(color='#58a63e'),
             strip_text_x=p9.element_text(size=11)) + \
    p9.geom_line() + \
    p9.facet_grid('. ~Month') + \
    p9.geom_hline(data=group_avg,
                  mapping=p9.aes(yintercept='y'),
                  colour='darkgreen',
                  size=1) + \
    p9.scale_y_continuous(labels=labs) + \
    p9.scale_x_datetime(breaks=date_breaks('2 years'), 
                        labels=date_format('%Y')) + \
    p9.labs(y='value')

seas_subseries_plot + p9.theme(figure_size=(10,4))

季节子序列图

此图对于揭示季节内和跨季节的模式很有用。

在示例时间序列中,我们可以看到平均值在 3 月份最低。在某些月份(例如 5 月),该序列显示出强劲的正趋势。

分组密度图

现实中的时间序列数据往往会受到各种因素的干扰和影响,导致数据模式产生变化。

我们可以利用分组密度图等可视化工具,来观察这些干扰事件对数据的影响。将数据按照干扰事件进行分组,每组对应一个不同的状态,然后分别绘制每组数据的密度曲线,从而比较不同状态下数据的分布差异。如下所示:

# 某些事件发生在索引 23
change_index = 23

before, after = train_test_split(series, train_size=change_index, shuffle=False)

n_bf, n_af = before.shape[0], after.shape[0]

p1_df = pd.DataFrame({'Series': before['y'], 'Id': range(n_bf)})
p1_df['Part'] = 'Before change'
p2_df = pd.DataFrame({'Series': after['y'], 'Id': range(n_af)})
p2_df['Part'] = 'After change'

df = pd.concat([p1_df, p2_df])
df['Part'] = pd.Categorical(df['Part'], categories=['Before change''After change'])

group_avg = df.groupby('Part').mean()['Series']

density_plot = \
    p9.ggplot(df) + \
    p9.aes(x='Series', fill='Part') + \
    MY_THEME + \
    p9.theme(legend_position='top') + \
    p9.geom_vline(xintercept=group_avg,
                  linetype='dashed',
                  color='steelblue',
                  size=1.1,
                  alpha=0.7) + \
    p9.geom_density(alpha=.2)

在这个特定的例子中,索引 23 处发生了一些事件。这里随意选择了这个特定的时间步骤。但是,你可以使用变化点检测方法来检测重要的时间步骤。

分组密度图

我们绘制了临界点前后的分布图。分布有明显的变化。

写在最后

探索性数据分析是时间序列分析和预测的基础环节。本文介绍了6种有助于探索时间序列内在模式和结构的可视化图形技术:

  • 时间序列图: 直观展示数据随时间的变化趋势,发现潜在的趋势和周期性。
  • 分解时间序列图: 将原始序列分解为趋势、周期、残差等不同成分,有助于进一步分析。
  • 滞后散点图: 将当前值与前若干滞后值绘制在散点图上,检验序列的自相关性。
  • 自相关系数图: 绘制不同滞后阶数下的自相关系数,判断序列中趋势和周期性的存在。
  • 季节子序列图: 根据季节周期对序列分组,展现不同季节下的数据模式。
  • 分组密度图: 根据干扰事件对数据进行分组,比较不同状态下数据分布的差异。本文使用Python的plotnine库进行可视化,它提供了丰富的统计绘图功能,是基于R的ggplot2设计的。实例代码可在plotnine官网查阅:https://plotnine.org/tutorials/。

🏴‍☠️宝藏级🏴‍☠️ 原创公众号『数据STUDIO』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括可戳👉 PythonMySQL数据分析数据可视化机器学习与数据挖掘爬虫 等,从入门到进阶!

长按👇关注- 数据STUDIO -设为星标,干货速递

相关推荐

  • 2024 META新作:SUM技术进行大规模在线用户表示,提升广告个性化效果
  • 干大模型的月薪快 10w 了
  • 60 个“特征工程”计算函数(Python 代码)
  • 解约!211 新校区,不建了!
  • AI浪潮,Spring也赶上了!?
  • 2024年上半年大模型发展回顾暨7月份半月度KG/RAG/LLM技术总结
  • 如果网站的 Cookie 超过 4K,会发生什么情况?
  • 上手 Day.js 日期处理库
  • 飞书一键复制网页内容为图片原理
  • 大脑一片空白:难倒 90% 前端的 Vue 面试题!
  • 万万没想到,用浏览器打开终端竟这么容易实现
  • 萝卜快写、萝卜快画来了?自动写小说、自动画漫画,两个最新的开源项目
  • 高手必知的Linux三剑客 (grep、sed、awk)
  • 列表是怎么实现的?解密列表的数据结构
  • 一文读懂数据血缘分析原理与建设方法
  • 橙单,一个免费的代码生成神器
  • Git版本管理工具,每个工程师都应该知道的基础操作!
  • Obsidian插件:Make.md为你量身打造一个完美的个人系统。
  • 从零预训练LLAMA3的完整指南:一个文件,探索Scaling Law
  • 开源仅 1 天就斩获近万星!超越 RAG、让大模型拥有超强记忆力的 Mem0 火了!