Source code for pyunicorn.timeseries.visibility_graph

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# This file is part of pyunicorn.
# Copyright (C) 2008--2015 Jonathan F. Donges and pyunicorn authors
# URL: <http://www.pik-potsdam.de/members/donges/software>
# License: BSD (3-clause)

"""
Provides classes for the analysis of dynamical systems and time series based
on recurrence plots, including measures of recurrence quantification
analysis (RQA) and recurrence network analysis.
"""

# array object and fast numerics
import numpy as np

from .. import InteractingNetworks, weave_inline


#
#  Class definitions
#

[docs]class VisibilityGraph(InteractingNetworks): """ Class VisibilityGraph for generating and analyzing visibility graphs of time series. Visibility graphs were initially applied for time series analysis by [Lacasa2008]_. """ # # Internal methods #
[docs] def __init__(self, time_series, timings=None, missing_values=False, horizontal=False, silence_level=0): """ Missing values are handled as infinite values, effectively separating the visibility graph into different disconnected components. .. note:: Missing values have to be marked by the Numpy NaN flag! :type time_series: 2D array (time, dimension) :arg time_series: The time series to be analyzed, can be scalar or multi-dimensional. :arg str timings: Timings of the observations in :attr:`time_series`. :arg bool missing_values: Toggle special treatment of missing values in :attr:`time_series`. :arg bool horizontal: Indicates whether a horizontal visibility relation is used. :arg number silence_level: Inverse level of verbosity of the object. """ # Set silence_level self.silence_level = silence_level """The inverse level of verbosity of the object.""" # Set missing_values flag self.missing_values = missing_values """Controls special treatment of missing values in :attr:`time_series`.""" # Store time series self.time_series = time_series.copy().astype("float32") """The time series from which the visibility graph is constructed.""" if timings is not None: timings = timings.copy().astype("float32") else: timings = np.arange(len(time_series), dtype="float32") # Store timings self.timings = timings """The timimgs of the time series data points.""" # Get missing value indices if self.missing_values: self.missing_value_indices = np.isnan(self.time_series) # Determine visibility relations if not horizontal: A = self.visibility_relations() else: A = self.visibility_relations_horizontal() # Initialize Network object InteractingNetworks.__init__(self, A, directed=False, silence_level=silence_level)
[docs] def __str__(self): """ Returns a string representation. """ return 'VisibilityGraph: time series shape %s.\n%s' % ( self.time_series.shape, InteractingNetworks.__str__(self)) # # Visibility methods #
[docs] def visibility_relations(self): """ TODO """ if self.silence_level <= 1: print "Calculating visibility relations..." # Prepare x = self.time_series t = self.timings N = len(self.time_series) A = np.zeros((N, N), dtype="int8") if self.missing_values: mv_indices = self.missing_value_indices code = r""" int i,j,k; float test; for (i = 0; i < N - 2; i++) { for (j = i + 2; j < N; j++) { k = i + 1; test = (x(j) - x(i)) / (t(j) - t(i)); while (!mv_indices(k) && (x(k) - x(i)) / (t(k) - t(i)) < test && k < j) { k++; } if (k == j) A(i,j) = A(j,i) = 1; } } // Add trivial connections of subsequent observations // in time series for (i = 0; i < N - 1; i++) { if (!mv_indices(i) && !mv_indices(i+1)) A(i,i+1) = A(i+1,i) = 1; } """ args = ['x', 't', 'N', 'A', 'mv_indices'] else: code = r""" int i,j,k; float test; for (i = 0; i < N - 2; i++) { for (j = i + 2; j < N; j++) { k = i + 1; test = (x(j) - x(i)) / (t(j) - t(i)); while ((x(k) - x(i)) / (t(k) - t(i)) < test && k < j) k++; if (k == j) A(i,j) = A(j,i) = 1; } } // Add trivial connections of subsequent observations // in time series for (i = 0; i < N - 1; i++) A(i,i+1) = A(i+1,i) = 1; """ args = ['x', 't', 'N', 'A'] weave_inline(locals(), code, args) return A
[docs] def visibility_relations_horizontal(self): """ TODO """ if self.silence_level <= 1: print "Calculating horizontal visibility relations..." # Prepare x = self.time_series t = self.timings N = len(self.time_series) A = np.zeros((N, N), dtype="int8") code = r""" int i,j,k; float minimum; for (i = 0; i < N - 2; i++) { for (j = i + 2; j < N; j++) { k = i + 1; minimum = fmin(x(i), x(j)); while (x(k) < minimum && k < j) k++; if (k == j) A(i,j) = A(j,i) = 1; } } // Add trivial connections of subsequent observations // in time series for (i = 0; i < N - 1; i++) A(i,i+1) = A(i+1,i) = 1; """ weave_inline(locals(), code, ['x', 't', 'N', 'A']) return A # # Specific measures for visibility graphs #
[docs] def retarded_degree(self): """Return number of neighbors in the past of a node.""" # Prepare retarded_degree = np.zeros(self.N) A = self.adjacency for i in xrange(self.N): retarded_degree[i] = A[i, :i].sum() return retarded_degree
[docs] def advanced_degree(self): """Return number of neighbors in the future of a node.""" # Prepare advanced_degree = np.zeros(self.N) A = self.adjacency for i in xrange(self.N): advanced_degree[i] = A[i, i:].sum() return advanced_degree
[docs] def retarded_local_clustering(self): """ Return probability that two neighbors of a node in its past are connected. """ # Prepare retarded_clustering = np.zeros(self.N) # Get full adjacency matrix A = self.adjacency # Get number of nodes N = self.N # Get left degree retarded_degree = self.retarded_degree() # Prepare normalization factor norm = retarded_degree * (retarded_degree - 1) / 2. code = """ long counter; // Loop over all nodes for (int i = 2; i < N; i++) { // Check if i has right degree larger than 1 if (norm(i) != 0) { // Reset counter counter = 0; // Loop over unique pairs of nodes in the past of i for (int j = 0; j < i; j++) { for (int k = 0; k < j; k++) { if (A(i,j) == 1 && A(j,k) == 1 && A(k,i) == 1) { counter++; } } } retarded_clustering(i) = counter / norm(i); } } """ weave_inline(locals(), code, ['N', 'A', 'norm', 'retarded_clustering']) return retarded_clustering
[docs] def advanced_local_clustering(self): """ Return probability that two neighbors of a node in its future are connected. """ # Prepare advanced_clustering = np.zeros(self.N) # Get full adjacency matrix A = self.adjacency # Get number of nodes N = self.N # Get right degree advanced_degree = self.advanced_degree() # Prepare normalization factor norm = advanced_degree * (advanced_degree - 1) / 2. code = """ long counter; // Loop over all nodes for (int i = 0; i < N - 2; i++) { // Check if i has right degree larger than 1 if (norm(i) != 0) { // Reset counter counter = 0; // Loop over unique pairs of nodes in the future of i for (int j = i + 1; j < N; j++) { for (int k = i + 1; k < j; k++) { if (A(i,j) == 1 && A(j,k) == 1 && A(k,i) == 1) { counter++; } } } advanced_clustering(i) = counter / norm(i); } } """ weave_inline(locals(), code, ['N', 'A', 'norm', 'advanced_clustering']) return advanced_clustering
[docs] def retarded_closeness(self): """Return average path length to nodes in the past of a node.""" # Prepare retarded_closeness = np.zeros(self.N) path_lengths = self.path_lengths() for i in xrange(self.N): retarded_closeness[i] = path_lengths[i, :i].mean() ** (-1) return retarded_closeness
[docs] def advanced_closeness(self): """Return average path length to nodes in the future of a node.""" # Prepare advanced_closeness = np.zeros(self.N) path_lengths = self.path_lengths() for i in xrange(self.N): advanced_closeness[i] = path_lengths[i, i+1:].mean() ** (-1) return advanced_closeness
[docs] def retarded_betweenness(self): """ Return betweenness of a node with respect to all pairs of nodes in its past. """ # Prepare retarded_betweenness = np.zeros(self.N) for i in xrange(self.N): retarded_indices = np.arange(i) retarded_betweenness[i] = self.nsi_betweenness( sources=retarded_indices, targets=retarded_indices)[i] return retarded_betweenness
[docs] def advanced_betweenness(self): """ Return betweenness of a node with respect to all pairs of nodes in its future. """ # Prepare advanced_betweenness = np.zeros(self.N) for i in xrange(self.N): advanced_indices = np.arange(i+1, self.N) advanced_betweenness[i] = self.nsi_betweenness( sources=advanced_indices, targets=advanced_indices)[i] return advanced_betweenness
[docs] def trans_betweenness(self): """ Return betweenness of a node with respect to all pairs of nodes with one node the past and one node in the future, respectively. """ # Prepare trans_betweenness = np.zeros(self.N) for i in xrange(self.N): retarded_indices = np.arange(i) advanced_indices = np.arange(i+1, self.N) trans_betweenness[i] = self.nsi_betweenness( sources=retarded_indices, targets=advanced_indices)[i] return trans_betweenness # # Measures corrected for boundary effects #
[docs] def boundary_corrected_degree(self): """Return a weighted degree corrected for trivial boundary effects.""" # Prepare N_past = np.arange(self.N) N_future = N_past[::-1] cdegree = (self.retarded_degree() * N_past + self.advanced_degree() * N_future) / float(self.N - 1) return cdegree
[docs] def boundary_corrected_closeness(self): """ Return a weighted closeness corrected for trivial boundary effects. """ # Prepare N_past = np.arange(self.N) N_future = N_past[::-1] ccloseness = (self.N - 1) * (self.retarded_closeness() / N_past + self.advanced_closeness() / N_future) return ccloseness