More about forecasting in cienciadedatos.net
- ARIMA and SARIMAX models with python
- Time series forecasting with machine learning
- Forecasting time series with gradient boosting: XGBoost, LightGBM and CatBoost
- Global Forecasting Models: Multi-series forecasting
- Global Forecasting Models: Comparative Analysis of Single and Multi-Series Forecasting Modeling
- Probabilistic forecasting
- Forecasting with deep learning
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()
# 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()
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()
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
# 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()
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)
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! 😊
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.
