2 Introducing pathpy

Ingo Scholtes
Data Analytics Group
Department of Informatics (IfI)
University of Zurich

September 5 2018

In the introductory lecture we have seen that higher-order modelling, visualisation, and analysis techniques are useful to analyze temporal network data that provide us with statistics of paths in complex networks. But how can we apply higher-order network analytics to such such data in practice?

In this tutorial, I will introduce pathpy, an OpenSource python package that provides higher-order data analytics and representation learning techniques. It contains data structures, algorithms, data import/export methods, and visualisation techniques for various types of time series data on complex networks.

pathpy is pure python code with no platform-specific dependencies. It only depends on numpy and scipy, which come with Anaconda, so it should be very easy to install. In principle installing the latest 2.0 version of pathpy should be as easy as running

pip install pathpy2

on the terminal. In any case, you can find more detailed setup instructions on the tutorial website.

**TODO:** Import the package `pathpy` and rename it to `pp`

In [1]:
import pathpy as pp

A core functionality of pathpy is to read, calculate, store, manipulate, and model path statistics extracted from different kinds of temporal data on complex networks. For this pathpy provides the class Paths, which can store collections of paths with varying lengths. All classes and methods in pathpy are documented using python's docstring feature so we can access the documentation using the standard help function.

**TODO:** Use the `help` function to obtain a description of the class `Paths`.

In [2]:
help(pp.Paths)
Help on class Paths in module pathpy.classes.paths:

class Paths(builtins.object)
 |  Path statistics that can be analyzed using higher- and multi-order network
 |  models. This object can be read from sequence data, or it can be generated
 |  from random walks, directed acyclic graphs, time-stamped network data, 
 |  origin/destination statistics, etc. via the functions provided in the submodule 
 |  path_extraction.
 |  
 |  Methods defined here:
 |  
 |  __add__(self, other)
 |      add path statistics of one object to the other
 |      
 |      Parameters
 |      ----------
 |      other : Paths
 |      
 |      Returns
 |      -------
 |      Paths
 |          Default operator +, which returns the sum of two Path objects
 |  
 |  __iadd__(self, other)
 |      in place addition avoids unnecessary copies of the object
 |      
 |      Parameters
 |      ----------
 |      other
 |      
 |      Returns
 |      -------
 |      None
 |  
 |  __imul__(self, factor)
 |      in-place scaling of path statistics
 |      
 |      Parameters
 |      ----------
 |      factor
 |      
 |      Returns
 |      -------
 |      None
 |  
 |  __init__(self, separator=',')
 |      Creates an empty Paths object
 |  
 |  __mul__(self, factor)
 |      multiplies all path statistics by factor
 |      
 |      Parameters
 |      ----------
 |      factor
 |      
 |      Returns
 |      -------
 |      a Paths object with multiplied frequencies
 |  
 |  __rmul__(self, factor)
 |      right multiply
 |  
 |  __str__(self)
 |      Returns the default string representation of
 |      this Paths instance
 |  
 |  add_path(self, path, frequency=1, expand_subpaths=True, separator=',')
 |      Adds a path to this Paths instance. The path argument can either be a list, tuple or
 |      a string ngram with a customisable node separator.
 |      
 |      Parameters
 |      ----------
 |      path: tuple, list, str
 |          The path to be added to this Paths instance. This can either be a list or tuple of
 |          objects that can be turned into strings, e.g. ('a', 'b', 'c') or (1, 3, 5), or
 |          a single string ngram "a,b,c", where nodes are separated by a user-defined
 |          separator character (default separator is ',').
 |      frequency: int, tuple
 |          Either an integer frequency, or a tuple (x,y) indicating the frequency of this
 |          path as subpath (first component) and as longest path (second component). Integer
 |          values x are automatically converted to (0, x). Default value is 1.
 |      expand_subpaths: bool
 |          Whether or not to calculate subpath statistics. Default value is True.            
 |      separator: str
 |          A string sepcifying the character that separates nodes in the ngram. Default is 
 |          ','.
 |      Returns
 |      -------
 |  
 |  expand_subpaths(self)
 |      This function implements the sub path expansion, i.e.
 |      for a four-gram a,b,c,d, the paths a->b, b->c, c->d of
 |      length one and the paths a->b->c and b->c->d of length
 |      two will be counted.
 |      
 |      This process will consider restrictions to the maximum
 |      sub path length defined in self.max_subpath_length
 |  
 |  filter_nodes(self, node_filter, min_length=0, max_length=9223372036854775807, split_paths=True)
 |      Returns a new Path instance that only cntains paths between nodes in a given
 |      filter set. For each of path in the current Paths object, the set of
 |      maximally contained subpaths between nodes in node_filter is extracted by default.
 |      This method is useful when studying (sub-)paths passing through a subset of nodes.
 |      
 |      Parameters
 |      ----------
 |      node_filter: set
 |          The set of nodes for which paths should be extracted from the current set of paths.
 |      min_length: int
 |          The minimum length of paths that shall pass the filter. Default 0.
 |      max_length: int
 |          The maximum length of paths that shall pass the filter. Default sys.maxsize.
 |      split_paths: bool
 |          Whether or not allow splitting paths in subpaths. If set to False, either
 |          the full path must pass the filter or the whole path is discarded. If set to True
 |          maximally contained subpaths that pass the filter will be considered as well. Default is True.
 |      
 |      Returns
 |      -------
 |      Paths
 |  
 |  path_lengths(self)
 |      compute the length of all paths
 |      
 |      Returns
 |      -------
 |      dict
 |          Returns a dictionary containing the distribution of path lengths
 |          in this Path object. In the returned dictionary, entry
 |          lengths ``k`` is a ``numpy.array`` ``x`` where
 |          ``x[0]`` is the number of sub paths with length ``k``, and ``x[1]``
 |          is the number of (longest) paths with length ``k``
 |  
 |  project_paths(self, mapping)
 |      Returns a new path object in which nodes are mapped to labels
 |      given by an arbitrary mapping function. For a mapping
 |      {'a': 'x', 'b': 'x', 'c': 'y', 'd': 'y'} path (a,b,c,d) is mapped to
 |      (x,x,y,y). This is useful e.g. to map page click streams to topic
 |      click streams, using a mapping from pages to topics.
 |      
 |      Parameters
 |      ----------
 |      mapping: dict
 |          A dictionary that maps nodes to the new labels.
 |      
 |      Returns
 |      -------
 |      Paths
 |  
 |  sequence(self, stop_char='|')
 |      Parameters
 |      ----------
 |      stop_char : str
 |          the character used to separate paths
 |      
 |      Returns
 |      -------
 |      tuple:
 |          Returns a single sequence in which all paths have been concatenated.
 |          Individual paths are separated by a stop character.
 |  
 |  summary(self)
 |      Returns
 |      -------
 |      str
 |          Returns a string containing basic summary info of this Paths instance
 |  
 |  unique_paths(self, l=0, consider_longer_paths=True)
 |      Returns the number of different paths that have (at least)
 |      a length l and that have been observed as a longest path at
 |      least once.
 |      
 |      Parameters
 |      ----------
 |      l : int
 |          count only unique longest path observations with at least length l. 
 |          Default is 0.
 |      consider_longer_paths : bool
 |          if True, the method will return the number of unique
 |          longest paths with *at least* length l. Default is True.
 |      
 |      Returns
 |      -------
 |      int
 |          The number of different paths that have been observed as a longest path at least once.
 |  
 |  write_file(self, filename, separator=',')
 |      Writes path statistics data to a file. Each line in this file captures a
 |      longest path (v0,v1,...,vl), as well as its frequency f as follows
 |      
 |      Parameters
 |      ----------
 |      filename: str
 |          name of the file to write to
 |      separator: str
 |          character that shall be used to separate nodes and frequencies
 |      
 |      Returns
 |      -------
 |  
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |  
 |  read_file(filename, separator=',', frequency=True, maxlines=9223372036854775807, max_ngram_length=9223372036854775807, expand_sub_paths=True, max_subpath_length=9223372036854775807) from builtins.type
 |      Reads path data from a file containing multiple lines of n-grams of the form
 |      ``a,b,c,d,frequency`` (where frequency is optional). Each n-gram is interpreted
 |      as path of length n-1.
 |      
 |      Parameters
 |      ----------
 |      filename : str
 |          path to the n-gram file to read the data from
 |      separator : str
 |          the character used to separate nodes on the path, i.e. using a
 |          separator character of ';' n-grams are represented as ``a;b;c;...``
 |      frequency : bool
 |          if set to ``True`` (default), the last entry in each n-gram will be interpreted as
 |          weight (i.e. frequency of the path), e.g. ``a,b,c,d,4`` means that four-gram
 |          ``a,b,c,d`` has weight four. If this is ``False`` each path occurrence is assigned
 |          a default weight of 1 (adding weights for multiple occurrences).
 |      maxlines : int
 |          number of lines/n-grams to read, if left at None the whole file is read in.
 |      max_ngram_length : int
 |          The maximum n for the n-grams to read, i.e. setting max_ngram_length to 15
 |          will ignore
 |          all n-grams of length 16 and longer, which means that only paths up to length
 |          n-1 are considered.
 |      expand_sub_paths : bool
 |          Whether or not subpaths of the n-grams are generated, i.e. for an input file with 
 |          a single trigram a;b;c a path a->b->c of length two will be generated as well as
 |          two subpaths a->b and b->c of length one. Defalt is True.
 |      max_subpath_length : int
 |      
 |      Returns
 |      -------
 |      Paths
 |          a ``Paths`` object obtained from the n-grams file
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  contained_paths(p, node_filter)
 |      Returns the list of maximum-length sub-paths of the path p, which only contain
 |      nodes that appear in the node_filter. As an example, for the path (a,b,c,d,e,f,g)
 |      and a node_filter [a,b,d,f,g], the method will return [(a,b), (d,), (f,g)].
 |      
 |      Parameters
 |      ----------
 |      p: tuple
 |          A path tuple to check for contained paths.
 |      node_filter: set
 |          A set of nodes to which contained paths should be limited.
 |      
 |      Returns
 |      -------
 |      list
 |  
 |  read_edges(filename, separator=',', weight=False, undirected=False, maxlines=None)
 |      Read path in edgelist format
 |      
 |      Reads data from a file containing multiple lines of *edges* of the
 |      form "v,w,frequency,X" (where frequency is optional and X are
 |      arbitrary additional columns). The default separating character ','
 |      can be changed.
 |      
 |      Parameters
 |      ----------
 |      filename : str
 |          path to edgelist file
 |      separator : str
 |          character separating the nodes
 |      weight : bool
 |          is a weight given? if ``True`` it is the last element in the edge
 |          (i.e. ``a,b,2``)
 |      undirected : bool
 |          are the edges directed or undirected
 |      maxlines : int
 |          number of lines to read (useful to test large files). None means the entire file is
 |          read
 |      Returns
 |      -------
 |      Paths
 |          a ``Paths`` object obtained from the edgelist
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  nodes
 |      Returns the list of nodes for the underlying
 |      set of paths
 |  
 |  observation_count
 |      Returns the total number of observed pathways of any length
 |      (includes multiple observations for paths observed more than one)

In Visual Studio Code, the documentation of classes, methods, and properties is automatically shown as a tooltip as you type. If you use the browser-based jupyter notebook editor, you can bring up the documentation by pressing Shift+Tab as you type. You can try this with the Paths class.

**TODO:** Create an empty `Paths` instance `toy_paths` by calling the constructor with no arguments.

In [3]:
toy_paths = pp.Paths()

We now have an empty Paths instance toy_paths that we can use to add path statistics to generate a small toy example. We can add paths using the method add_path. As the first parameter, it accepts any iterable (list, string, etc.) of string variables (or objects that can be cast to string), where each entry in the iterable is one step (i.e. node) on a path. The optional frequency parameter captures the number of times a specific path has been observed.

**TODO:** Add 10 observations of a path $a \rightarrow c \rightarrow e$ between three nodes $a$, $c$, and $e$ to the `toy_paths` instance.

In [4]:
toy_paths.add_path(('a', 'c', 'd'), frequency=10)

Each class in pathpy provides a properly formatted string representation, which can be shown by invoking print on an instance.

**TODO**: Print a string summary of the instance `toy_paths`

In [5]:
print(toy_paths)
Total path count: 		10.0 
[Unique / Sub paths / Total]: 	[1.0 / 50.0 / 60.0]
Nodes:				3 
Edges:				2
Max. path length:		2
Avg path length:		2.0 
Paths of length k = 0		0.0 [ 0.0 / 30.0 / 30.0 ]
Paths of length k = 1		0.0 [ 0.0 / 20.0 / 20.0 ]
Paths of length k = 2		10.0 [ 1.0 / 0.0 / 10.0 ]

We get summary statistics of the Paths instance. Our toy example contains 10 observed paths between three nodes. These paths imply a graph topology with two edges (a,b) and (b,c). Both the maximum and the average path length is two (the path length counts the number of edge traversals of a path).

To understand the last three lines and the second line in the output, we must look into the inner workings of pathpy. For the fitting of higher-order graphical models as well as for the representation learning algorithm, pathpy uses all path statistics available. Specifically to fit, say, a second-order model to a set of paths that all have length 10 or longer, we calculate which paths of length two are contained as sub-paths within these observations of longer paths. For this reason, pathpy automatically calculates the statistics of actual path observations as well as the statistics of sub-paths contained in these observed paths.

In our case, we have $10$ observations of a single path $a \rightarrow b \rightarrow c$ of length two, thus the last line in the output above. Each of these paths additionally contains two sub-paths $a \rightarrow b$ and $b \rightarrow c$ of length two, thus the number $20.0$ in the sub-path count in the line $k=1$. Finally, each of the paths contains three "paths" of length zero, which are just observations of a single node (i.e. there is no transition across an edge), thus the sub-path count of $30.0$ in the line $k=0$. This amounts to a total of $50.0$ sub-paths + $10$ observations of an actual (longest) path, thus explaining the second line in the output.

Apart from adding paths as a tuple, we can also add them as string-encoded n-grams, using the parameter separator to specify a character that separates nodes.

**TODO:** Create a new `Paths` instance `ngram_paths`, add 10 path observations using the ngram `"b-c-e"`, and print a summary of the resulting instance.

In [6]:
ngram_paths = pp.Paths()
ngram_paths.add_path('b-c-e',  separator='-', frequency=10)
print(ngram_paths)
Total path count: 		10.0 
[Unique / Sub paths / Total]: 	[1.0 / 50.0 / 60.0]
Nodes:				3 
Edges:				2
Max. path length:		2
Avg path length:		2.0 
Paths of length k = 0		0.0 [ 0.0 / 30.0 / 30.0 ]
Paths of length k = 1		0.0 [ 0.0 / 20.0 / 20.0 ]
Paths of length k = 2		10.0 [ 1.0 / 0.0 / 10.0 ]

We obtain a Paths object with 10 observations of path $b\rightarrow c \rightarrow$. We can add this to our previous toy_paths instance by using arithmetic operators on instances of the Paths class.

**TODO:** Use arithmetic operators to add `toy_paths` and `ngram_paths` and print a summary of the result.

In [7]:
toy_paths += ngram_paths
print(toy_paths)
Total path count: 		20.0 
[Unique / Sub paths / Total]: 	[2.0 / 100.0 / 120.0]
Nodes:				5 
Edges:				4
Max. path length:		2
Avg path length:		2.0 
Paths of length k = 0		0.0 [ 0.0 / 60.0 / 60.0 ]
Paths of length k = 1		0.0 [ 0.0 / 40.0 / 40.0 ]
Paths of length k = 2		20.0 [ 2.0 / 0.0 / 20.0 ]

We obtain a new Paths instance with $20$ observed paths between five nodes $a$, $b$, $c$, $d$, and $e$ across four edges $(a,c)$, $(c,d)$, $(b,c)$ and $(c,e)$. Let us first use the function Paths.write_file to save these paths for later use.

**TODO:** Save the paths to an ngram file `data/toy_paths.ngram`.

In [8]:
toy_paths.write_file('../data/toy_paths.ngram')

We often analyse or visualise graph or network topologies in which the observed paths have occurred. For this, pathpy provides the class Network, which you can use to read, manipulate, analyse, and visualise directed, undirected, weighted, and unweighted networks.

We can easily turn any Paths instance into a network by using the class method Network.from_paths. This will cut each path $v_0 \rightarrow v_1 \rightarrow v_2 \rightarrow \ldots$ into multiple directed dyadic relations $(v_i, v_{i+1})$ that are represented by directed edges.

**TODO**: Create a `Network` instance `toy_graph` from the `toy_paths` instance and print a summary of the network.

In [9]:
toy_graph = pp.Network.from_paths(toy_paths)
print(toy_graph)
Directed network
Nodes:				5
Links:				4

We obtain a network with five nodes $a$, $b$, $c$, $d$, and $e$ and four directed edges. The number of times each edge is traversed by a path is captured by the weights of these edges. We can access these weights using the dictionary toy_graph.edges[edge].

**TODO:** Print the `weight` of edge `(a, c)` in `toy_graph`.

In [10]:
print('Weight of edge (a,c) is {0}'.format(toy_graph.edges[('a', 'c')]['weight']))
Weight of edge (a,c) is 10.0

In fact, since each edge can be viewed as a path of length one, the edge weights in our example are nothing more than the statistics of corresponding sub-paths of length one in the toy_paths. We can check the statistics of paths in a Paths instance by accessing p.paths[length][path_tuple]. This will return an array of two numbers, where the first entry is the number of times a path has been observed as sub-path of length l, and the second number gives the number of times a path has been observed as an actual (longest) path of length l.

**TODO:** Verify that the sub-path frequency of path `(a,c)` in `toy_paths` coincides with the weight of edge `(a,c)` in `toy_graph`.

In [11]:
print('Frequency of path (a,c) as subpath of length one is {0}'.format(toy_paths.paths[1][('a', 'c')][0]))
Frequency of path (a,c) as subpath of length one is 10.0

When dealing with rich, real-world data we often want to store additional attributes of nodes and edges. pathpy's Network class accomodates for such settings by supporting arbitrary attributes both at the level of nodes and edges. Moreover, we can find nodes or edges with certain attributes by passing lambda functions to the find_nodes or find_edges method.

The attribute x of a node v or an edge e in a network instance net can be accessed simply by reading or writing to the dictionary net.nodes[v][x] and net.edges[e][x] respectively.

**TODO:** Set the node and edge attributes of some nodes and edges in `toy_graph`, and use `find_nodes` and `find_edges` to identify nodes and edges that have certain attributes.

In [12]:
# we can assign arbitrary additional attribute values to nodes and edges
toy_graph.edges[('a', 'c')]['edge_property'] = 'some value'
toy_graph.edges[('c', 'd')]['edge_property'] = 'some other value'
toy_graph.nodes['a']['node_property'] = 42.0
toy_graph.nodes['b']['node_property'] = 0.0
toy_graph.edges[('a', 'c')]['label'] = 'my_label'

# return edges with a given attribute value
print('Edges that satisfy edge_propery == "some value": ' + \
      str(toy_graph.find_edges(select_edges = lambda e: 'edge_property' in e and e['edge_property'] == 'some value')))

# return nodes with a given attribute value
print('Nodes that satisfy node_property > 40.0: ' + \
      str(toy_graph.find_nodes(select_node = lambda v: 'node_property' in v and v['node_property'] > 40.0)))
Edges that satisfy edge_propery == "some value": [('a', 'c')]
Nodes that satisfy node_property > 40.0: ['a']

Finally, pathpy offers rich, interactive HTML5 visualisations of all of its internal objects that can also be shown inline in a jupyter notebook. To embed a visualisation of our example network in our notebook, we simply have to write the name of the variable as the last line of a jupyter cell.

Note that you can interact with the generated graph using the mouse. We can drag nodes as well as pan and zoom. Hovering above a node will show the name of that node.

**TODO**: Visualise the network `toy_graph`, drag a node, hover above a node, and pan and zoom.

In [13]:
toy_graph
Out[13]:
[save svg]

Note the [save svg] label in the top-part of the output cell. Clicking this label will download the current view of the visualisation as a publication-grade scalable vector graphics file. This file is a good basis for further fine-tuning in SVG-compliant tools like Inkscape or Adobe Illustrator.

**TODO**: Export the file above as SVG by clicking the label.

We can also programmatically style our network by using the generic plot function in the module pathpy.visualisation. This function allows us to pass parameters that will influence the visualisation.

**TODO**: Check the documentation of `pp.visualisation.plot` and change the color of nodes to red.

In [14]:
pp.visualisation.plot(toy_graph, node_color='red')
[save svg]

We often want to reuse the same visualisation parameters in multiple plots. For this, it is convenient to store our parameters in a dictionary and then pass all of them at once.

Let us explore some of the features supported by pathpy's default visualisation templates. In the second session we will learn more about custom visualisation templates that can be defined by the user, and which can take arbitrary visualisation parameters.

**TODO**: Use the `help` function to show which parameters are supported by the function `pp.visualisation.plot`.

In [15]:
help(pp.visualisation.plot)
Help on function plot in module pathpy.visualisation.html:

plot(network, **params)
    Plots an interactive visualisation of pathpy objects
    in a jupyter notebook. This generic function supports instances of
    pathpy.Network, pathpy.TemporalNetwork, pathpy.HigherOrderNetwork,
    pathpy.MultiOrderModel, and pathpy.Paths. See description of different
    visualisations in the parameter description.
    
    Parameters
    ----------
    network: Network, TemporalNetwork, HigherOrderNetwork, MultiOrderModel, Paths
        The object to visualize. Depending on the type of the object passed, the following
        visualisations are generated:
            Network: interactive visualisation of a network with a force-directed layout.
            HigherOrderNetwork: interactive visualisation of the first-order network
                with forces calculated based on the higher-order network. By setting
                plot_higher_order_nodes=True a network with unprojected
                higher-order nodes can be plotted instead.
            MultiOrderModel: interactive visualisation of the first-order network
                with forces calculated based on the multi-order model.
            TemporalNetwork: interactive and dynamic visualisation of a temporal
                network.
            Paths: alluvial diagram showing markov or non-Markov trajectories through
                a given focal node.
    params: dict
        A dictionary with visualisation parameters. These parameters are passed through to
        visualisation templates that are extendable by the user. The default pathpy templates
        support the following parameters, depending on the type of object being visualised
        (see brackets).
            width: int (all)
                Width of the div element containing the jupyter visualization.
                Default value is 400.
            height: int (all)
                Height of the div element containing the jupyter visualization.
                Default value is 400.
            template: string (all)
                Path to custom visualization template file. If this parameter is omitted, the
                default pathpy visualistion template fitting the corresponding object will be used.
            d3js_path: string (all)
                URL to the d3js library. By default, d3js will be loaded from https://d3js.org/d3.v4.min.js.
                For offline operation, the URL to a local copy of d3js can be specified instead. For custom
                templates, a specific d3js version can be used.
            node_size: int, dict (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderModel)
                Either an int value that specifies the radius of all nodes, or
                a dictionary that assigns custom node sizes to invidual nodes.
                Default value is 5.0.
            edge_width: int, float, dict (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderModel)
                Either an int value that specifies the radius of all edges, or
                a dictionary that assigns custom edge widths to invidual edges.
                Default value is 0.5.
            node_color: string, dict (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderModel)
                Either a string value that specifies the HTML color of all nodes,
                or a dictionary that assigns custom node colors to invidual nodes.
                Both HTML named colors ('red, 'blue', 'yellow') or HEX-RGB values can
                be used. Default value is "#99ccff". (lightblue)
            edge_color: string, dict (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderModel)
                Either a string value that specifies the HTML color of all edges,
                or a dictionary that assigns custom edge color to invidual edges.
                Both HTML named colors ('red, 'blue', 'yellow') or HEX-RGB values can
                be used. Default value is "#cccccc" (lightgray).
            edge_opacity: float (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderModel)
                The opacity of all edges in a range from 0.0 to 1.0. Default value is 1.0.             
            edge_arrows: bool (Network, HigherOrderNetwork, MultiOrderNetwork)
                Whether to draw edge arrows for directed networks. Default value is True.
            label_color: string (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderModel)
                The HTML color of node labels.  Both HTML named colors ('red, 'blue', 'yellow') 
                or HEX-RGB values can be used.Default value is '#cccccc' (lightgray).
            label_opacity: float (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderModel)
                The opacity of the label. Default value is 1.0.
            label_size: str (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderNetwork)
                CSS string specifying the font size to be used for labels. Default is '8px'.
            label_offset: list (Network, HigherOrderNetwork, TemporalNetwork, MultiOrderNetwork)
                The offset [x,y] of the label from the center of a node. For [0,0] labels will be
                displayed in the center of nodes. Positive values for the first and second component
                move the label to the right and top respectively. Default is [0, -10], which
                displays labels above nodes to accomodate for labels wider than nodes.
            force_charge: float, int ()
                The charge strength of nodes to be used in the force-directed layout. Default value is -20
            force_repel: float, int (all)
                The strength of the repulsive force between nodes. Larger negative values will increase the distance
                between nodes. Default value is -200.
            force_alpha: float (all)
                The alpha target (convergence threshold) to be passed to the underlying force-directed
                layout algorithm. Default value is 0.0.
            plot_higher_order_nodes: HigherOrderNetwork
                If set to True, a raw higher-order network with higher-order nodes will be plotted. If
                False, a first-order projection with a higher-order force-directed layout will be plotted.
                The default value is True.
            ms_per_frame: int (TemporalNetwork)
                how many milliseconds each frame shall be displayed in the visualisation of a TemporalNetwork.
                The 1000/ms_per_frame specifies the framerate of the visualisation. The default value of 20 yields a
                framerate of 50 fps.
            ts_per_frame: int (TemporalNetwork)
                How many timestamps in a temporal network shall be displayed in every frame
                of the visualisation. For a value of 1 each timestamp is shown in a separate frame.
                For higher values, multiple timestamps will be aggregated in a single frame. For a
                value of zero, simulation speed is adjusted to the inter event time distribution such
                that on average five interactions are shown per second. Default value is 1.
            look_behind: int (TemporalNetwork)
                The look_ahead and look_behind parameters define a temporal range around the current time
                stamp within which time-stamped edges will be considered for the force-directed layout.
                Values larger than one result in smoothly changing layouts.
                Default value is 10.
            look_ahead: int (TemporalNetwork)
                The look_ahead and look_behind parameters define a temporal range around the current time
                stamp within which time-stamped edges will be considered for the force-directed layout.
                Values larger than one result in smoothly changing layouts.
                Default value is 10.
            max_time: int (TemporalNetwork)
                maximum time stamp to visualise. Useful to limit visualisation of very long Temporal Networks. 
                If None, the whole sequence will be shown. Default is None.
            active_edge_width: float (TemporalNetwork)
                A float value that specifies the width of currently active edges.
                Default value is 4.0.
            inactive_edge_width: float (TemporalNetwork)
                A float value that specifies the width of currently active edges.
                Default value is 0.5.
            active_edge_color: string (TemporalNetwork)
                A string value that specifies the HTML color of currently active edges.
                Both HTML named colors ('red, 'blue', 'yellow') or HEX-RGB values can
                be used. Default value is "#ff0000" (red).
            inactive_edge_color: string (TemporalNetwork)
                A string value that specifies the HTML color of inactive edges.
                Both HTML named colors ('red, 'blue', 'yellow') or HEX-RGB values can
                be used. Default value is "#cccccc" (lightgray).
            active_node_color: string (TemporalNetwork)
                A string value that specifies the HTML color of active nodes.
                Both HTML named colors ('red, 'blue', 'yellow') or HEX-RGB values can
                be used. Default value is "#ff0000" (red).
            inactive_node_color: string (TemporalNetwork)
                A string value that specifies the HTML color of inactive nodes.
                Both HTML named colors ('red, 'blue', 'yellow') or HEX-RGB values can
                be used. Default value is "#cccccc" (lightgray).
    
    Examples:
    ---------
    >>> paths = pp.Paths()
    >>> paths.add_path('a,b,c')
    >>> n = pp.Network.from_paths(paths)
    >>> params = {'label_color': '#ff0000',
                  'node_color': { 'a': '#ff0000', 'b': '#00ff00', 'c': '#0000ff'}
    >>>          }
    >>> pp.visualisation.plot(n, **params)
    >>> [inline visualisation]
    >>> pp.visualisation.export_html(n, filename='myvisualisation.html', **params)

**TODO**: Create a parameter dictionary `style` that changes the plot size, switches off edge arrows, assigns individual colors to nodes, changes label position, color and font size, and adjust node size and edge width.

In [16]:
style = {'width': 300, 
          'height': 300,
          'node_size': 18.0,
          'edge_width' : 4.0,
          'node_color' : {'a': '#aacc99', 'b': '#aacc99', 'd': '#aacc99', 'e': '#aacc99', 'c': '#cc6666'},
          'edge_color' : '#ffaaaa',
          'edge_arrows': False,
          'label_color': '#000000',
          'label_opacity': 1,
          'label_offset': [0,0],
          'label_size': '20px',
          'edge_opacity': 1, 
          'force_charge': -10.0, 
          'force_repel': -550, 
          'force_alpha': 0.01
         }
pp.visualisation.plot(toy_graph, **style)
[save svg]

Once we are satisfied with our visualisation, we can use the method pp.visualisation.export_html to save it as a stand-alone HTML file. This file will run in any HTML5 browser and we can share or publish as an interactive visualisation on the web.

**TODO**: Save your visualisation to a file `test_network.html`. Reuse the visualisation parameters from above.

In [17]:
pp.visualisation.export_html(toy_graph, filename='../visualisations/2_simple_network.html', **style)