Examples

Computing and drawing visibility graphs

This example builds the visibility graph for a randomly generated motion time series and draws the graph with the help of NetworkX.

from ts2vg import NaturalVG
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt

# 1. Generate random time series (Brownian motion)
rng = np.random.default_rng(110)
ts = rng.standard_normal(size=200)
ts = np.cumsum(ts)

# 2. Build visibility graph
g = NaturalVG(directed=None).build(ts)
nxg = g.as_networkx()

# 3. Make plots
fig, [ax0, ax1, ax2] = plt.subplots(ncols=3, figsize=(12, 3.5))

ax0.plot(ts)
ax0.set_title("Time Series")

graph_plot_options = {
    "with_labels": False,
    "node_size": 2,
    "node_color": [(0, 0, 0, 1)],
    "edge_color": [(0, 0, 0, 0.15)],
}

nx.draw_networkx(nxg, ax=ax1, pos=g.node_positions(), **graph_plot_options)
ax1.tick_params(bottom=True, labelbottom=True)
ax1.plot(ts)
ax1.set_title("Visibility Graph")

nx.draw_networkx(nxg, ax=ax2, pos=nx.kamada_kawai_layout(nxg), **graph_plot_options)
ax2.set_title("Visibility Graph")
Example plots of a visibility graph

Obtaining the adjacency matrix

This example shows how to obtain the adjacency matrix for the visibility graph of a time series.

See adjacency_matrix() for more options.

from ts2vg import NaturalVG

ts = [6.0, 3.0, 1.8, 4.2, 6.0, 3.0, 1.8, 4.8]

g = NaturalVG().build(ts)

g.adjacency_matrix(triangle="lower")
array([[0, 0, 0, 0, 0, 0, 0, 0],
   [1, 0, 0, 0, 0, 0, 0, 0],
   [1, 1, 0, 0, 0, 0, 0, 0],
   [1, 1, 1, 0, 0, 0, 0, 0],
   [1, 1, 0, 1, 0, 0, 0, 0],
   [0, 0, 0, 0, 1, 0, 0, 0],
   [0, 0, 0, 0, 1, 1, 0, 0],
   [0, 0, 0, 0, 1, 1, 1, 0]], dtype=uint8)

Obtaining the degree distribution

This example shows how to get the degree distribution for the visibility graph of a given time series.

Here, we generate a random time series with 100.000 data points and then compute and plot its degree distribution.

from ts2vg import NaturalVG
import numpy as np
import matplotlib.pyplot as plt

# 1. Generate random time series (Brownian motion)
rng = np.random.default_rng(0)
ts = rng.standard_normal(size=100_000)
ts = np.cumsum(ts)

# 2. Build visibility graph
g = NaturalVG().build(ts, only_degrees=True)

# 3. Get degree distribution
ks, ps = g.degree_distribution

# 4. Make plots
fig, [ax0, ax1, ax2] = plt.subplots(ncols=3, figsize=(12, 3.5))

ax0.plot(ts, c="#000", linewidth=1)
ax0.set_title("Time Series")
ax0.set_xlabel("t")

ax1.scatter(ks, ps, s=2, c="#000", alpha=1)
ax1.set_title("Degree Distribution")
ax1.set_xlabel("k")
ax1.set_ylabel("P(k)")

ax2.scatter(ks, ps, s=2, c="#000", alpha=1)
ax2.set_yscale("log")
ax2.set_xscale("log")
ax2.set_title("Degree Distribution (log-log)")
ax2.set_xlabel("k")
ax2.set_ylabel("P(k)")
Example degree distribution

Building directed graphs

This example illustrates different options for the directed parameter when building visibility graphs.

from ts2vg import NaturalVG
import matplotlib.pyplot as plt

ts = [6.0, 3.0, 1.8, 4.2, 6.0, 3.0, 1.8, 4.8]

direction_options = [
    None,
    "left_to_right",
    "top_to_bottom",
]

fig, axs = plt.subplots(ncols=3, figsize=(12, 3.5))

for d, ax in zip(direction_options, axs):
    ax.set_title(f"directed={repr(d)}")

    nvg = NaturalVG(directed=d).build(ts)
    plot_nvg(nvg, ax=ax)
Example directed graphs

Code for plot_nvg()

from matplotlib.patches import ArrowStyle, FancyArrowPatch


def plot_nvg(
    vg,
    ax,
    edge_color=(0.25, 0.25, 0.25, 0.7),
):
    bars = ax.bar(vg.xs, vg.ts, color="#ccc", edgecolor="#000", width=0.3)
    ax.set_xticks(vg.xs)

    for (n1, n2) in vg.edges:
        x1, y1 = vg.xs[n1], vg.ts[n1]
        x2, y2 = vg.xs[n2], vg.ts[n2]

        arrow = FancyArrowPatch(
            (x1, y1),
            (x2, y2),
            arrowstyle=ArrowStyle("->", head_length=6, head_width=2.5) if vg.is_directed else ArrowStyle("-"),
            shrinkA=0,
            shrinkB=0,
            color=edge_color,
            linewidth=2,
        )

        ax.add_patch(arrow)


Building weighted graphs

This example illustrates different options for the weighted parameter when building visibility graphs. See Weighted graphs for a complete list of available values for weighted.

from ts2vg import NaturalVG
import matplotlib.pyplot as plt

ts = [6.0, 3.0, 1.8, 4.2, 6.0, 3.0, 1.8, 4.8]

weight_options = [
    "slope",
    "abs_slope",
    "distance",
    "h_distance",
    "v_distance",
    "abs_v_distance",
]

fig, axs = plt.subplots(ncols=3, nrows=2, figsize=(12, 6))
cbar_ax = fig.add_axes([0.96, 0.2, 0.01, 0.6])

for w, ax in zip(weight_options, axs.flat):
    ax.set_title(f"weighted='{w}'")

    nvg = NaturalVG(weighted=w).build(ts)
    plot_weighted_nvg(nvg, ax=ax, cbar_ax=cbar_ax)
Example weighted graphs

Code for plot_weighted_nvg()

from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize
from matplotlib.patches import ArrowStyle, FancyArrowPatch


def plot_weighted_nvg(
    vg,
    ax,
    cbar_ax,
    weights_cmap="coolwarm_r",
    weights_range=(-3.5, 3.5),
):
    bars = ax.bar(vg.xs, vg.ts, color="#ccc", edgecolor="#000", width=0.3)
    ax.set_xticks(vg.xs)

    color_mappable = ScalarMappable(norm=Normalize(*weights_range), cmap=weights_cmap)
    cbar_ax.get_figure().colorbar(color_mappable, cax=cbar_ax, orientation="vertical", aspect=30, pad=0.05)

    for (n1, n2, w) in vg.edges:
        x1, y1 = vg.xs[n1], vg.ts[n1]
        x2, y2 = vg.xs[n2], vg.ts[n2]

        arrow = FancyArrowPatch(
            (x1, y1),
            (x2, y2),
            arrowstyle=ArrowStyle("-"),
            shrinkA=0,
            shrinkB=0,
            color=color_mappable.to_rgba(w, alpha=1),
            linewidth=2,
        )

        ax.add_patch(arrow)


Building horizontal visibility graphs

This example illustrates different options for horizontal visibility graphs. Note that horizontal visibility graphs can also be directed and/or weighted.

from ts2vg import HorizontalVG
import matplotlib.pyplot as plt

ts = [6.0, 3.0, 1.8, 4.2, 6.0, 3.0, 1.8, 4.8]

direction_options = [
    None,
    "left_to_right",
    "top_to_bottom",
]

fig, axs = plt.subplots(ncols=3, figsize=(12, 3.5))

for d, ax in zip(direction_options, axs):
    ax.set_title(f"directed={repr(d)}")

    hvg = HorizontalVG(directed=d).build(ts)
    plot_hvg(hvg, ax=ax)
Example horizontal visibility graphs

Code for plot_utils()

from matplotlib.patches import ArrowStyle, FancyArrowPatch


def plot_hvg(
    vg,
    ax,
    edge_color=(0.25, 0.25, 0.25, 0.7),
    bar_width=0.3,
    prevent_overlap=False,
):
    occupied_heights = set()

    bars = ax.bar(vg.xs, vg.ts, color="#ccc", edgecolor="#000", width=bar_width)
    ax.set_xticks(vg.xs)

    for i, (n1, n2) in enumerate(vg.edges):
        y = min(vg.ts[n1], vg.ts[n2])

        if prevent_overlap:
            # very naive overlap prevention
            while round(y, 2) in occupied_heights:
                y -= 0.18

            occupied_heights.add(round(y, 2))

        if n1 < n2:
            x1, y1 = vg.xs[n1] + (bar_width / 2), y
            x2, y2 = vg.xs[n2] - (bar_width / 2), y
        else:
            x1, y1 = vg.xs[n1] - (bar_width / 2), y
            x2, y2 = vg.xs[n2] + (bar_width / 2), y

        arrow = FancyArrowPatch(
            (x1, y1),
            (x2, y2),
            arrowstyle=(
                ArrowStyle("->", head_length=6, head_width=2.5)
                if vg.is_directed
                else ArrowStyle("<->", head_length=6, head_width=2.5)
            ),
            shrinkA=0,
            shrinkB=0,
            color=edge_color,
            linewidth=2,
        )

        ax.add_patch(arrow)


Building limited penetrable visibility graphs

Limited penetrable visibility graphs (LPVG) are a variation of visibility graphs in which nodes are allowed to have a certain number of obstructions between them and still have a connecting edge in the resulting graph. Limited penetrable visibility graphs might be more robust to noise in the data.

The maximum number of data points that are allowed to obstruct two nodes is given by the penetrable_limit parameter. Note that when penetrable_limit is 0, the behavior is exactly the same as a regular (non-penetrable) visibility graph.

from ts2vg import NaturalVG, HorizontalVG
import matplotlib.pyplot as plt


ts = [6.0, 3.0, 1.8, 4.2, 6.0, 3.0, 1.8, 4.8]

penetrable_limit_options = [
    0,
    1,
    2,
]

fig, axs = plt.subplots(ncols=3, nrows=2, figsize=(12, 7))

# plot limited penetrable visibility graphs
for penetrable_limit, ax in zip(penetrable_limit_options, axs.flat[:3]):
    ax.set_title(f"NVG, penetrable_limit={penetrable_limit}")

    nvg = NaturalVG(penetrable_limit=penetrable_limit).build(ts)
    plot_nvg(nvg, ax=ax)

# plot limited penetrable horizontal visibility graphs
for penetrable_limit, ax in zip(penetrable_limit_options, axs.flat[3:]):
    ax.set_title(f"HVG, penetrable_limit={penetrable_limit}")

    hvg = HorizontalVG(penetrable_limit=penetrable_limit).build(ts)
    plot_hvg(hvg, ax=ax, prevent_overlap=True)
Example limited penetrable visibility graphs

Code for plot_nvg(), plot_hvg()

from matplotlib.patches import ArrowStyle, FancyArrowPatch


def plot_nvg(
    vg,
    ax,
    edge_color=(0.25, 0.25, 0.25, 0.7),
):
    bars = ax.bar(vg.xs, vg.ts, color="#ccc", edgecolor="#000", width=0.3)
    ax.set_xticks(vg.xs)

    for (n1, n2) in vg.edges:
        x1, y1 = vg.xs[n1], vg.ts[n1]
        x2, y2 = vg.xs[n2], vg.ts[n2]

        arrow = FancyArrowPatch(
            (x1, y1),
            (x2, y2),
            arrowstyle=ArrowStyle("->", head_length=6, head_width=2.5) if vg.is_directed else ArrowStyle("-"),
            shrinkA=0,
            shrinkB=0,
            color=edge_color,
            linewidth=2,
        )

        ax.add_patch(arrow)


from matplotlib.patches import ArrowStyle, FancyArrowPatch


def plot_hvg(
    vg,
    ax,
    edge_color=(0.25, 0.25, 0.25, 0.7),
    bar_width=0.3,
    prevent_overlap=False,
):
    occupied_heights = set()

    bars = ax.bar(vg.xs, vg.ts, color="#ccc", edgecolor="#000", width=bar_width)
    ax.set_xticks(vg.xs)

    for i, (n1, n2) in enumerate(vg.edges):
        y = min(vg.ts[n1], vg.ts[n2])

        if prevent_overlap:
            # very naive overlap prevention
            while round(y, 2) in occupied_heights:
                y -= 0.18

            occupied_heights.add(round(y, 2))

        if n1 < n2:
            x1, y1 = vg.xs[n1] + (bar_width / 2), y
            x2, y2 = vg.xs[n2] - (bar_width / 2), y
        else:
            x1, y1 = vg.xs[n1] - (bar_width / 2), y
            x2, y2 = vg.xs[n2] + (bar_width / 2), y

        arrow = FancyArrowPatch(
            (x1, y1),
            (x2, y2),
            arrowstyle=(
                ArrowStyle("->", head_length=6, head_width=2.5)
                if vg.is_directed
                else ArrowStyle("<->", head_length=6, head_width=2.5)
            ),
            shrinkA=0,
            shrinkB=0,
            color=edge_color,
            linewidth=2,
        )

        ax.add_patch(arrow)


Partitioning time series via community detection

Time series can be partitioned by applying community detection algorithms to their visibility graphs.

from ts2vg import NaturalVG
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt

# 1. Generate Brownian motion series
rng = np.random.default_rng(110)
ts = rng.standard_normal(size=200)
ts = np.cumsum(ts)

# 2. Build visibility graph
g = NaturalVG(directed=None).build(ts)
nxg = g.as_networkx()

# 3. Partition the graph into communities
communities = nx.algorithms.community.greedy_modularity_communities(nxg)

# 4. Make plots

COLORS = [
    "#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B3",
    "#937860", "#DA8BC3", "#8C8C8C", "#CCB974", "#64B5CD",
]

node_colors = ["#000000"] * len(ts)
for community_id, community_nodes in enumerate(communities):
    for node in community_nodes:
        node_colors[node] = COLORS[community_id % len(COLORS)]

fig, [ax0, ax1, ax2] = plt.subplots(ncols=3, figsize=(12, 3.5))

ax0.plot(ts)
ax0.set_title("Time Series")

graph_plot_options = {
    "with_labels": False,
    "node_size": 6,
    "node_color": [node_colors[n] for n in nxg.nodes],
}

nx.draw_networkx(nxg, ax=ax1, pos=g.node_positions(), edge_color=[(0, 0, 0, 0.05)], **graph_plot_options)
ax1.tick_params(bottom=True, labelbottom=True)
ax1.plot(ts, c=(0, 0, 0, 0.15))
ax1.set_title("Visibility Graph")

nx.draw_networkx(nxg, ax=ax2, pos=nx.kamada_kawai_layout(nxg), edge_color=[(0, 0, 0, 0.15)], **graph_plot_options)
ax2.set_title("Visibility Graph")
Example partitioning of visibility graphs