More about forecasting in cienciadedatos.net


Table of Contents

Introduction

Time series data is one of the most common types of data in the real world. It can range from stock prices and temperature readings to website traffic and sensor outputs. Visualising time series data can help us identify patterns, trends, and anomalies that might not be apparent from the raw numbers alone.

Python offers a rich ecosystem of data visualisation libraries, each with its own strengths. This document will explore how to plot and analyse time series data using Matplotlib, Plotly, Altair, Bokeh, HoloViews and HvPlot which cater to a variety of needs, from traditional static charts to highly interactive visualisations.

✏️ Note

This document does not aim to be a comprehensive guide to each library, but rather a practical introduction to visualising time series data using these popular tools. The reader is encouraged to explore the documentation of each library for more advanced features and customisations.

Data

# Data download
# ==============================================================================
from skforecast.datasets import fetch_dataset
import pandas as pd

data = fetch_dataset(name="vic_electricity")
╭──────────────────────────── vic_electricity ─────────────────────────────╮
│ Description:                                                             │
│ Half-hourly electricity demand for Victoria, Australia                   │
│                                                                          │
│ Source:                                                                  │
│ O'Hara-Wild M, Hyndman R, Wang E, Godahewa R (2022).tsibbledata: Diverse │
│ Datasets for 'tsibble'. https://tsibbledata.tidyverts.org/,              │
│ https://github.com/tidyverts/tsibbledata/.                               │
│ https://tsibbledata.tidyverts.org/reference/vic_elec.html                │
│                                                                          │
│ URL:                                                                     │
│ https://raw.githubusercontent.com/skforecast/skforecast-                 │
│ datasets/main/data/vic_electricity.csv                                   │
│                                                                          │
│ Shape: 52608 rows x 4 columns                                            │
╰──────────────────────────────────────────────────────────────────────────╯
# Data preparation
# ==============================================================================
data = data.loc["2012-01-01":"2012-06-30", ["Demand"]]
data = data.asfreq("1h")
data = data.sort_index()
data
Demand
Time
2012-01-01 00:00:00 4599.507418
2012-01-01 01:00:00 4938.795740
2012-01-01 02:00:00 5211.915476
2012-01-01 03:00:00 5436.420076
2012-01-01 04:00:00 5641.188556
... ...
2012-06-30 19:00:00 3490.857900
2012-06-30 20:00:00 3623.363262
2012-06-30 21:00:00 3767.340180
2012-06-30 22:00:00 4183.219012
2012-06-30 23:00:00 4615.963016

4368 rows × 1 columns

# Split data into train-test
# ==============================================================================
end_train = "2012-03-30 23:59:00"
data_train = data.loc[:end_train, :].copy()
data_test = data.loc[end_train:, :].copy()

print(
    f"Train dates: {data_train.index.min()} --- {data_train.index.max()}  (n={len(data_train)})"
)
print(
    f"Test dates : {data_test.index.min()} --- {data_test.index.max()}  (n={len(data_test)})"
)
Train dates: 2012-01-01 00:00:00 --- 2012-03-30 23:00:00  (n=2160)
Test dates : 2012-03-31 00:00:00 --- 2012-06-30 23:00:00  (n=2208)

Matplotlib

Matplotlib is the foundation of data visualisation in Python. Offering precise control over every aspect of a plot, it is perfect for producing publication-quality figures and customising visuals down to the finest detail. Although its syntax can be somewhat verbose, Matplotlib remains a reliable and flexible option for creating static visualisations.

# Libraries
# ==============================================================================
import matplotlib.pyplot as plt
from skforecast.plot import set_dark_theme
# Static plot of the time series
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(8, 4))
data_train["Demand"].plot(ax=ax, label="train")
data_test["Demand"].plot(ax=ax, label="test")
ax.set_title("Electricity Demand")
ax.legend(loc="upper left");
# Zooming time series chart
# ==============================================================================
zoom = ("2012-05-01 14:00:00", "2012-06-01 14:00:00")
fig, axs = plt.subplots(2, 1, figsize=(8, 4), gridspec_kw={"height_ratios": [1, 2]})

# Main plot
data["Demand"].plot(ax=axs[0], color="lightgray", alpha=0.5)
axs[0].axvspan(zoom[0], zoom[1], color="#30a2da", alpha=0.7)
axs[0].set_title("Electricity demand")
axs[0].set_xlabel("")

# Zoom plot
data.loc[zoom[0] : zoom[1], "Demand"].plot(ax=axs[1], color="#30a2da")
axs[1].set_title(f"Zoom: {zoom[0]} to {zoom[1]}", fontsize=10)

plt.tight_layout()
plt.show()
# Annual, weekly and daily seasonality plots
# ==============================================================================
fig, axs = plt.subplots(2, 2, figsize=(9, 5), sharex=False, sharey=True)
axs = axs.ravel()
flierprops = dict(
    marker="o",
    color="white",
    markerfacecolor="white",
    markeredgecolor="white",
    markersize=3,
    alpha=0.5,
)

# Demand distribution by month
data["month"] = data.index.month
data.boxplot(column="Demand", by="month", ax=axs[0], flierprops=flierprops)
data.groupby("month")["Demand"].median().plot(style="o-", linewidth=0.8, ax=axs[0])
axs[0].set_ylabel("Demand")
axs[0].set_title("Demand distribution by month", fontsize=9)

# Demand distribution by week day
data["week_day"] = data.index.day_of_week + 1
data.boxplot(
    column="Demand",
    by="week_day",
    ax=axs[1],
    flierprops=flierprops,
)
data.groupby("week_day")["Demand"].median().plot(style="o-", linewidth=0.8, ax=axs[1])
axs[1].set_ylabel("Demand")
axs[1].set_title("Demand distribution by weekday", fontsize=9)

# Demand distribution by the hour of the day
data["hour_day"] = data.index.hour + 1
data.boxplot(
    column="Demand",
    by="hour_day",
    ax=axs[2],
    flierprops=flierprops,
)
data.groupby("hour_day")["Demand"].median().plot(style="o-", linewidth=0.8, ax=axs[2])
axs[2].set_ylabel("Demand")
axs[2].set_title("Demand distribution by hour of the day", fontsize=9)

# Demand distribution by week day and hour of the day
mean_day_hour = data.groupby(["week_day", "hour_day"])["Demand"].mean()
mean_day_hour.plot(ax=axs[3])
axs[3].set(
    title="Mean Demand during week",
    xticks=[i * 24 for i in range(7)],
    xticklabels=["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    xlabel="Day and hour",
    ylabel="Number of Demand",
)
axs[3].title.set_size(10)

fig.suptitle("Seasonality plots", fontsize=12)
fig.tight_layout()
# Autocorrelation plot
# ==============================================================================
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, ax = plt.subplots(figsize=(5, 2))
plot_acf(data["Demand"], ax=ax, lags=60)
plt.show()
# Partial Autocorrelation plot
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2))
plot_pacf(data["Demand"], ax=ax, lags=60)
plt.show()
# Time series descomposition plot
# ==============================================================================
from statsmodels.tsa.seasonal import seasonal_decompose

result = seasonal_decompose(
    x=data_train["Demand"].head(200), model="additive", period=7
)
fig = result.plot()
plt.show()

Plotly

Plotly is a powerful Python library used for creating interactive, publication-quality graphs and dashboards. It supports a wide range of chart types, including line plots, bar charts, scatter plots, 3D graphs, maps, and more. Built on top of D3.js and React, Plotly allows for highly customizable and dynamic visualizations that can be viewed in web browsers or embedded in notebooks. The library has two main interfaces:

  • Plotly Express – a high-level API for quick, concise plotting.
  • Graph Objects – a low-level API for fine-tuned control over figures.
# Libraries
# ==============================================================================
import plotly.graph_objects as go
import plotly.io as pio
import plotly.offline as poff

pio.templates.default = "plotly_dark"  # "seaborn"
poff.init_notebook_mode(connected=True)
# Interactive plot of time series
# ==============================================================================
fig = go.Figure()
fig.add_trace(
    go.Scatter(
        x=data_train.index,
        y=data_train["Demand"],
        mode="lines",
        name="Train",
        line=dict(color="#30a2da"),
    )
)
fig.add_trace(
    go.Scatter(
        x=data_test.index,
        y=data_test["Demand"],
        mode="lines",
        name="Test",
        line=dict(color="#fc4f30"),
    )
)
fig.update_layout(
    title="Hourly energy demand",
    xaxis_title="Time",
    yaxis_title="Demand",
    legend_title="Partition:",
    width=800,
    height=400,
    margin=dict(l=20, r=20, t=35, b=20),
    legend=dict(orientation="h", yanchor="top", y=1, xanchor="left", x=0.001),
)
# fig.update_xaxes(rangeslider_visible=True)
fig.show()
Jan 2012Feb 2012Mar 2012Apr 2012May 2012Jun 2012300040005000600070008000
Partition:TrainTestHourly energy demandTimeDemand
# Interactive Autocorrelation plot
# ==============================================================================
from statsmodels.tsa.stattools import acf

acf_values = acf(data_train["Demand"], nlags=60)
fig = go.Figure()
fig.add_trace(go.Bar(x=list(range(61)), y=acf_values))
fig.update_layout(
    title="Autocorrelation Plot",
    xaxis_title="Lag",
    yaxis_title="Autocorrelation",
    width=800,
    height=400
)
fig.show()
0102030405060−0.500.51
Autocorrelation PlotLagAutocorrelation

Altair

Altair is a declarative statistical visualization library for Python, built on top of Vega-Lite. It emphasizes a simple and concise syntax, allowing users to describe plots in terms of the data and the visual encodings they want (like position, color, or size) rather than focusing on low-level plotting details. Altair is particularly well-suited for exploratory data analysis and works seamlessly with Pandas DataFrames. It automatically handles scales, legends, and axes, producing clean, interactive visualizations with minimal code.

# Libraries
# ==============================================================================
import altair as alt
alt.data_transformers.enable("vegafusion")  # Enable vegafusion for large datasets
DataTransformerRegistry.enable('vegafusion')
# !pip install vl-convert-python
# !pip install "vegafusion>=2.0.3"
# Interactive plot of time series
# ==============================================================================
# Prepare data for Altair
df_train = pd.DataFrame(
    {"Date": data_train.index, "Demand": data_train["Demand"], "Partition": "Train"}
)
df_test = pd.DataFrame(
    {"Date": data_test.index, "Demand": data_test["Demand"], "Partition": "Test"}
)
df = pd.concat([df_train, df_test])

# Enable dark theme
alt.theme.enable("dark")

# Interactive selection for legend-based filtering
selection = alt.selection_point(fields=["Partition"], bind="legend")

# Line chart
chart = (
    alt.Chart(df)
    .mark_line()
    .encode(
        x=alt.X("Date:T", title="Time"),
        y=alt.Y("Demand:Q", title="Demand"),
        color=alt.Color(
            "Partition:N",
            scale=alt.Scale(domain=["Train", "Test"], range=["#30a2da", "#fc4f30"]),
        ),
        opacity=alt.condition(selection, alt.value(1), alt.value(0.15)),
        tooltip=[
            alt.Tooltip("Date:T", title="Date", format="%Y-%m-%d %H:%M"),
            alt.Tooltip("Demand:Q", title="Demand", format=".2f"),
            alt.Tooltip("Partition:N", title="Partition"),
        ],
    )
    .add_params(selection)
    .properties(
        title="Hourly energy demand",
        width=700,
        height=350,
    )
)

chart.interactive()
2012Jan 08Jan 15Jan 22Jan 29Feb 05Feb 12Feb 19Feb 26Mar 04Mar 11Mar 18Mar 25AprilApr 08Apr 15Apr 22Apr 29May 06May 13May 20May 27Jun 03Jun 10Jun 17Jun 24Time01,0002,0003,0004,0005,0006,0007,0008,0009,000DemandTrainTestPartitionHourly energy demand
interval = alt.selection_interval(encodings=['x'])

base = (
    alt.Chart(df)
    .mark_line()
    .encode(
        x='Date:T',
        y='Demand:Q',
        #y2='Demand:Q',
        color='Partition:N'
    )
)

chart = base.encode(
    x=alt.X(
        'Date:T',
        scale=alt.Scale(domain=interval)
    )
).properties(
    width=700,
    height=300
)

view = base.add_params(
    interval
).properties(
    title="Hourly energy demand",
    width=700,
    height=50,
)

chart & view
2012Jan 08Jan 15Jan 22Jan 29Feb 05Feb 12Feb 19Feb 26Mar 04Mar 11Mar 18Mar 25AprilApr 08Apr 15Apr 22Apr 29May 06May 13May 20May 27Jun 03Jun 10Jun 17Jun 24Date01,0002,0003,0004,0005,0006,0007,0008,0009,000Demand2012Jan 08Jan 15Jan 22Jan 29Feb 05Feb 12Feb 19Feb 26Mar 04Mar 11Mar 18Mar 25AprilApr 08Apr 15Apr 22Apr 29May 06May 13May 20May 27Jun 03Jun 10Jun 17Jun 24Date05,000DemandHourly energy demandTestTrainPartition
# Interactive Autocorrelation plot
# ==============================================================================
acf_values = acf(data_train["Demand"], nlags=60)
acf_df = pd.DataFrame({"lag": list(range(61)), "autocorrelation": acf_values})

alt.Chart(acf_df).mark_bar().encode(
    x=alt.X("lag:O", title="Lag"),
    y=alt.Y("autocorrelation:Q", title="Autocorrelation"),
    tooltip=[
        alt.Tooltip("lag:O", title="Lag"),
        alt.Tooltip("autocorrelation:Q", title="Autocorrelation", format=".4f"),
    ],
).properties(
    title="Autocorrelation Plot (Altair)",
    width=700,
    height=350,
).interactive()
0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960Lag−0.6−0.4−0.20.00.20.40.60.81.0AutocorrelationAutocorrelation Plot (Altair)

Bokeh, Holoviews and hvPlot

Bokeh is an interactive visualization library for Python that enables the creation of dynamic, web-ready graphics. It is designed for building interactive plots, dashboards, and data applications that can run in modern web browsers. Bokeh supports a wide variety of chart types and integrates easily with NumPy, Pandas, and Jupyter notebooks. Its strength lies in enabling real-time interactivity—such as zooming, panning, and tooltips—and it can also be used to create server-based applications for live data visualization. It is similar to Matplotlib, but for interactive web visualizations.

HoloViews is a high-level library for building complex visualizations easily. It provides a simple and consistent API for creating a wide range of plots and charts, allowing users to focus on the data rather than the intricacies of plotting. HoloViews integrates seamlessly with Bokeh, enabling users to leverage the strengths of this library while benefiting from HoloViews' simplicity.

hvPlot is a high-level .plot()-style API built on top of HoloViews.

# Libraries
# ==============================================================================
from bokeh.plotting import figure, show
from bokeh.models import HoverTool
from bokeh.io import output_notebook
from bokeh.io import curdoc
from bokeh.io import output_file, save
# Interactive plot of time series
# ==============================================================================
output_notebook()
curdoc().theme = "dark_minimal"


p = figure(x_axis_type="datetime", title="Hourly energy demand", width=800, height=400)
train_line = p.line(
    x=data_train.index,
    y=data_train["Demand"],
    line_width=2,
    color="#30a2da",
    legend_label="Train Data",
)

test_line = p.line(
    x=data_test.index,
    y=data_test["Demand"],
    line_width=2,
    color="#fc4f30",
    legend_label="Test Data",
)

# Add hover tool
hover = HoverTool(
    tooltips=[
        ("Date", "@x{%F}"),  # show date in YYYY-MM-DD format
        ("Value", "@y{0.00}"),  # show value with 2 decimals
    ],
    formatters={"@x": "datetime"},  # interpret x as datetime
    mode="vline",  # show hover vertically across both lines
)

p.add_tools(hover)

# Customize plot appearance
p.legend.location = "top_right"
p.legend.orientation = "horizontal"
p.legend.click_policy = "hide"
p.xaxis.axis_label = "Date"
p.yaxis.axis_label = "Demand"
p.title.text_font_size = "14pt"

show(p)
BokehJS 3.8.1 successfully loaded.

By default, Bokeh plots are not exported when using nbconvert to convert notebooks to HTML. To embed Bokeh plots in HTML output, it is needed to save the plot to an HTML file and then display it using an IFrame.

# from IPython.display import IFrame, display
# output_file("train_test_plot.html")
# _ = save(p)
# display(IFrame(src="train_test_plot.html", width=850, height=450))

Alternatively, you can use holoviews library, which can use bokeh as a backend and provides better integration with jupyter notebooks and exporting to HTML.

# Interactive plot of time series using HoloViews
# ==============================================================================
import holoviews as hv

hv.extension("bokeh")
hv.renderer("bokeh").theme = "dark_minimal"
train_curve = hv.Curve(
    (data_train.index, data_train["Demand"]), "Date", "Demand", label="Train Data"
).opts(color="#30a2da")

test_curve = hv.Curve(
    (data_test.index, data_test["Demand"]), "Date", "Demand", label="Test Data"
).opts(color="#fc4f30")

plot = (train_curve * test_curve).opts(
    width=800,
    height=400,
    title="Hourly energy demand",
    xlabel="Date",
    ylabel="Demand",
    fontscale=1.2,
    legend_cols=2,
    show_grid=True,
)
plot
# Interactive plot of time series using hvPlot
# ==============================================================================
import hvplot.pandas  # enables .hvplot() method on DataFrames
import holoviews as hv  # set a dark theme (hvPlot uses HoloViews under the hood)

hv.extension("bokeh")
hv.renderer("bokeh").theme = "dark_minimal"
train_plot = data_train.hvplot.line(label="Train Data", color="#30a2da")
test_plot = data_test.hvplot.line(label="Test Data", color="#fc4f30")
plot = (train_plot * test_plot).opts(
    width=800,
    height=400,
    title="Hourly energy demand",
    xlabel="Date",
    ylabel="Demand",
    fontscale=1.2,
    legend_cols=2,
    show_grid=True,
)
plot

Session information

import session_info
session_info.show(html=False)
-----
altair              6.0.0
bokeh               3.8.1
holoviews           1.22.0
hvplot              0.12.1
matplotlib          3.10.8
pandas              2.3.3
plotly              6.4.0
session_info        v1.0.1
skforecast          0.19.0
statsmodels         0.14.5
-----
IPython             9.7.0
jupyter_client      8.6.3
jupyter_core        5.9.1
-----
Python 3.13.9 | packaged by conda-forge | (main, Oct 22 2025, 23:12:41) [MSC v.1944 64 bit (AMD64)]
Windows-11-10.0.26100-SP0
-----
Session information updated at 2025-11-28 23:21

Citation

How to cite this document

If you use this document or any part of it, please acknowledge the source, thank you!

Visualizing time series data in Python by Joaquín Amat Rodrigo and Javier Escobar Ortiz, available under Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) at https://cienciadedatos.net/documentos/py71-visualizing-time-series-data.html

How to cite skforecast

If you use skforecast for a publication, we would appreciate it if you cite the published software.

Zenodo:

Amat Rodrigo, Joaquin, & Escobar Ortiz, Javier. (2025). skforecast (v0.19.0). Zenodo. https://doi.org/10.5281/zenodo.8382788

APA:

Amat Rodrigo, J., & Escobar Ortiz, J. (2025). skforecast (Version 0.19.0) [Computer software]. https://doi.org/10.5281/zenodo.8382788

BibTeX:

@software{skforecast, author = {Amat Rodrigo, Joaquin and Escobar Ortiz, Javier}, title = {skforecast}, version = {0.19.0}, month = {11}, year = {2025}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382788} }


Did you like the article? Your support is important

Your contribution will help me to continue generating free educational content. Many thanks! 😊

Become a GitHub Sponsor Become a GitHub Sponsor

Creative Commons Licence

This work by Joaquín Amat Rodrigo and Javier Escobar Ortiz is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International.

Allowed:

  • Share: copy and redistribute the material in any medium or format.

  • Adapt: remix, transform, and build upon the material.

Under the following terms:

  • Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.

  • NonCommercial: You may not use the material for commercial purposes.

  • ShareAlike: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.