Commit c50b429c authored by Ingo Scholtes's avatar Ingo Scholtes
Browse files

-

parent 4ab4087f
No preview for this file type
......@@ -76,14 +76,14 @@
},
{
"cell_type": "code",
"execution_count": 137,
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Uid:\t\t\t0x7fba2852b460\n",
"Uid:\t\t\t0x7fd940332400\n",
"Type:\t\t\tNetwork\n",
"Directed:\t\tTrue\n",
"Multi-Edges:\t\tFalse\n",
......@@ -106,14 +106,14 @@
},
{
"cell_type": "code",
"execution_count": 138,
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Uid:\t\t\t0x7fba2852b220\n",
"Uid:\t\t\t0x7fd9403324f0\n",
"Type:\t\t\tNetwork\n",
"Directed:\t\tFalse\n",
"Multi-Edges:\t\tFalse\n",
......@@ -136,20 +136,20 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Uid:\t\t\t0x7fb9f86324c0\n",
"Uid:\t\t\t0x7fd9403324f0\n",
"Type:\t\t\tNetwork\n",
"Directed:\t\tFalse\n",
"Multi-Edges:\t\tFalse\n",
"Number of nodes:\t0\n",
"Number of edges:\t0\n",
"Uid:\t\t\t0x7fb9f8632ee0\n",
"Uid:\t\t\t0x7fd940332760\n",
"Type:\t\t\tNetwork\n",
"Directed:\t\tFalse\n",
"Multi-Edges:\t\tFalse\n",
......@@ -175,7 +175,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 5,
"metadata": {},
"outputs": [
{
......@@ -205,7 +205,7 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 6,
"metadata": {},
"outputs": [
{
......@@ -240,7 +240,7 @@
},
{
"cell_type": "code",
"execution_count": 93,
"execution_count": 7,
"metadata": {},
"outputs": [
{
......@@ -272,7 +272,7 @@
},
{
"cell_type": "code",
"execution_count": 94,
"execution_count": 8,
"metadata": {},
"outputs": [
{
......@@ -296,7 +296,7 @@
},
{
"cell_type": "code",
"execution_count": 95,
"execution_count": 9,
"metadata": {},
"outputs": [
{
......@@ -320,7 +320,7 @@
},
{
"cell_type": "code",
"execution_count": 96,
"execution_count": 10,
"metadata": {},
"outputs": [
{
......@@ -344,7 +344,7 @@
},
{
"cell_type": "code",
"execution_count": 97,
"execution_count": 11,
"metadata": {},
"outputs": [
{
......@@ -374,7 +374,7 @@
},
{
"cell_type": "code",
"execution_count": 98,
"execution_count": 12,
"metadata": {},
"outputs": [
{
......@@ -410,7 +410,7 @@
},
{
"cell_type": "code",
"execution_count": 99,
"execution_count": 13,
"metadata": {},
"outputs": [
{
......@@ -436,14 +436,14 @@
},
{
"cell_type": "code",
"execution_count": 100,
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Uid:\t\t\t0x7fb9f86324c0\n",
"Uid:\t\t\t0x7fd9403324f0\n",
"Type:\t\t\tNetwork\n",
"Directed:\t\tFalse\n",
"Multi-Edges:\t\tFalse\n",
......@@ -466,7 +466,7 @@
},
{
"cell_type": "code",
"execution_count": 101,
"execution_count": 15,
"metadata": {},
"outputs": [
{
......@@ -503,7 +503,7 @@
},
{
"cell_type": "code",
"execution_count": 102,
"execution_count": 16,
"metadata": {},
"outputs": [
{
......@@ -511,25 +511,25 @@
"output_type": "stream",
"text": [
"---\n",
"Uid:\t\t0x7fb9c8848640\n",
"Uid:\t\t0x7fd940332430\n",
"Type:\t\tEdge\n",
"Directed:\tFalse\n",
"Nodes:\t\t|'a', 'b'|\n",
"\n",
"---\n",
"Uid:\t\t0x7fb9c88486a0\n",
"Uid:\t\t0x7fd940332bb0\n",
"Type:\t\tEdge\n",
"Directed:\tFalse\n",
"Nodes:\t\t|'b', 'c'|\n",
"\n",
"---\n",
"Uid:\t\t0x7fb9c88484f0\n",
"Uid:\t\t0x7fd940332610\n",
"Type:\t\tEdge\n",
"Directed:\tFalse\n",
"Nodes:\t\t|'c', 'd'|\n",
"\n",
"---\n",
"Uid:\t\t0x7fb9c8848c70\n",
"Uid:\t\t0x7fd9403325e0\n",
"Type:\t\tEdge\n",
"Directed:\tFalse\n",
"Nodes:\t\t|'d', 'a'|\n",
......@@ -552,7 +552,7 @@
},
{
"cell_type": "code",
"execution_count": 103,
"execution_count": 17,
"metadata": {},
"outputs": [
{
......@@ -581,7 +581,7 @@
},
{
"cell_type": "code",
"execution_count": 104,
"execution_count": 18,
"metadata": {},
"outputs": [
{
......@@ -611,7 +611,7 @@
},
{
"cell_type": "code",
"execution_count": 105,
"execution_count": 19,
"metadata": {},
"outputs": [
{
......@@ -639,7 +639,7 @@
},
{
"cell_type": "code",
"execution_count": 107,
"execution_count": 20,
"metadata": {},
"outputs": [
{
......@@ -665,7 +665,7 @@
},
{
"cell_type": "code",
"execution_count": 108,
"execution_count": 21,
"metadata": {},
"outputs": [
{
......@@ -674,7 +674,7 @@
"3"
]
},
"execution_count": 108,
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
......@@ -694,7 +694,7 @@
},
{
"cell_type": "code",
"execution_count": 109,
"execution_count": 22,
"metadata": {},
"outputs": [
{
......@@ -726,16 +726,9 @@
},
{
"cell_type": "code",
"execution_count": 111,
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"[10-26 12:47:43: WARNING] No edge was removed!\n"
]
},
{
"name": "stdout",
"output_type": "stream",
......@@ -856,7 +849,7 @@
},
{
"cell_type": "code",
"execution_count": 116,
"execution_count": 24,
"metadata": {},
"outputs": [
{
......@@ -889,7 +882,7 @@
},
{
"cell_type": "code",
"execution_count": 117,
"execution_count": 25,
"metadata": {},
"outputs": [],
"source": [
......@@ -912,7 +905,7 @@
},
{
"cell_type": "code",
"execution_count": 118,
"execution_count": 26,
"metadata": {},
"outputs": [],
"source": [
......@@ -925,7 +918,7 @@
},
{
"cell_type": "code",
"execution_count": 119,
"execution_count": 27,
"metadata": {},
"outputs": [
{
......@@ -951,7 +944,7 @@
},
{
"cell_type": "code",
"execution_count": 120,
"execution_count": 28,
"metadata": {},
"outputs": [
{
......@@ -960,7 +953,7 @@
"2"
]
},
"execution_count": 120,
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
......@@ -971,7 +964,7 @@
},
{
"cell_type": "code",
"execution_count": 121,
"execution_count": 29,
"metadata": {},
"outputs": [
{
......@@ -980,7 +973,7 @@
"0"
]
},
"execution_count": 121,
"execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
......@@ -998,7 +991,7 @@
},
{
"cell_type": "code",
"execution_count": 122,
"execution_count": 30,
"metadata": {},
"outputs": [
{
......@@ -1007,7 +1000,7 @@
"7.0"
]
},
"execution_count": 122,
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
......@@ -1025,7 +1018,7 @@
},
{
"cell_type": "code",
"execution_count": 123,
"execution_count": 31,
"metadata": {},
"outputs": [
{
......@@ -1050,7 +1043,7 @@
},
{
"cell_type": "code",
"execution_count": 124,
"execution_count": 32,
"metadata": {},
"outputs": [
{
......@@ -1073,12 +1066,12 @@
" {'name': 'William', 'age': 323}\n",
"---\n",
"Edge attributes\n",
"Uid:\t\t0x7fb9c883b610\n",
"Uid:\t\t0x7fd95080c610\n",
"Type:\t\tEdge\n",
"Directed:\tTrue\n",
"Nodes:\t\t('t', 'b')\n",
" {'type': 'like', 'strength': 2.0, 'uid': None}\n",
"Uid:\t\t0x7fb9c883b910\n",
"Uid:\t\t0x7fd95080cbe0\n",
"Type:\t\tEdge\n",
"Directed:\tTrue\n",
"Nodes:\t\t('t', 'w')\n",
......@@ -1116,7 +1109,7 @@
},
{
"cell_type": "code",
"execution_count": 126,
"execution_count": 33,
"metadata": {},
"outputs": [
{
......@@ -1141,7 +1134,7 @@
},
{
"cell_type": "code",
"execution_count": 132,
"execution_count": 34,
"metadata": {},
"outputs": [
{
......@@ -1167,7 +1160,7 @@
},
{
"cell_type": "code",
"execution_count": 133,
"execution_count": 35,
"metadata": {},
"outputs": [
{
......@@ -1193,14 +1186,14 @@
},
{
"cell_type": "code",
"execution_count": 134,
"execution_count": 36,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Uid:\t\t\t0x7fba2852beb0\n",
"Uid:\t\t\t0x7fd95083fb50\n",
"Type:\t\t\tNetwork\n",
"Directed:\t\tTrue\n",
"Multi-Edges:\t\tTrue\n",
......@@ -1230,7 +1223,7 @@
},
{
"cell_type": "code",
"execution_count": 135,
"execution_count": 37,
"metadata": {},
"outputs": [
{
......@@ -1241,7 +1234,7 @@
" [0., 0., 0.]])"
]
},
"execution_count": 135,
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
......
%% Cell type:markdown id: tags:
# Representing networks with `pathpy`
*October 27 2021*
The tutorials covered in the previous week covered the basics of data science with `python`. With this out of the way, we now introduce `pathpy`, a network analysis and visualisation package that is being actively developed at my chair. If you are interested to participate in the development of this OpenSource project, you can either sign up for the "eXtended AI Lab" or apply for a position as student assistant in our group. For more information, please refer to our website.
Apart from being developed in our group `pathpy` has a couple of advantages compared to many other network analysis packages. First, it is easy to install as it has no dependencies that are not included in a default `anaconda` installation and it is a pure `python` package that does not require compilation. Second, `pathpy` has a user-friendly API that makes it easy to handle directed and undirected networks, networks with single and multiple edges, multi-layer networks, temporal networks, as well as networks with arbitrary node, edge or graph-level attributes. Third, it provides interactive HTML visualisations that can be directly displayed inside `jupyter` notebooks, making it particularly suitable for educational settings. And finally, different from other packages it directly supports the analysis and visualisation of time series data on networked systems, such as time-stamped edges or data on paths in networks.
To get started, we first import `pathpy` and assign the local alias `pp`:
%% Cell type:code id: tags:
```
import pathpy as pp
import numpy as np
```
%%%% Output: display_data
%% Cell type:markdown id: tags:
If the `import` statement completes without error message, the installation was successful and we can now use `pathpy` to generate, analyse, and visualise networks.
## Creating networks
For this purpose `pathpy` provides the `Network` class. Calling the constructor will return an instance that represents an empty network with no nodes and no links. By default, networks in `pathpy` are directed. If you want to create an undirected network you can pass the parameter `directed=False` in the constructor.
Printing the `Network` object will give a short string summary which tells whether the network is directed or undirected, as well as the number of unique nodes and links.
%% Cell type:code id: tags:
```
n1 = pp.Network()
print(n1)
```
%%%% Output: stream
Uid: 0x7fba2852b460
Uid: 0x7fd940332400
Type: Network
Directed: True
Multi-Edges: False
Number of nodes: 0
Number of edges: 0
%% Cell type:markdown id: tags:
A network is directed by default, but we can create an undirected network by passing the parameter `directed=False`.
%% Cell type:code id: tags:
```
n2 = pp.Network(directed=False)
print(n2)
```
%%%% Output: stream
Uid: 0x7fba2852b220
Uid: 0x7fd9403324f0
Type: Network
Directed: False
Multi-Edges: False
Number of nodes: 0
Number of edges: 0
%% Cell type:markdown id: tags:
The examples above show that each network has a unique identifier. By default, this unique ID is derived from the hash value of the underlying python object, which allows you to quickly check whether two variables actually refer to the same underlying object instance.
%% Cell type:code id: tags:
```
n3=n2
print(n3)
n4 = pp.Network(directed=False)
print(n4)
```
%%%% Output: stream
Uid: 0x7fb9f86324c0
Uid: 0x7fd9403324f0
Type: Network
Directed: False
Multi-Edges: False
Number of nodes: 0
Number of edges: 0
Uid: 0x7fb9f8632ee0
Uid: 0x7fd940332760
Type: Network
Directed: False
Multi-Edges: False
Number of nodes: 0
Number of edges: 0
%% Cell type:markdown id: tags:
If you prefer to manage your own UIDs that are easier to remember, you can assign custom IDs by explicitly passing a uid property:
%% Cell type:code id: tags:
```
n = pp.Network(directed=False, uid='MyUndirectedNetwork')
print(n)
```
%%%% Output: stream
Uid: MyUndirectedNetwork
Type: Network
Directed: False
Multi-Edges: False
Number of nodes: 0
Number of edges: 0
%% Cell type:markdown id: tags:
The simplest way to add nodes and edges is to call the functions `add_node` and `add_edge`. In both cases, we can simply pass unique string identifiers of nodes, which will then be used as UIDs of the underlying node objects. To create an undirected network with three nodes and two edges, we can write:
%% Cell type:code id: tags:
```
n = pp.Network(directed=False, multiedges=True, uid='ExampleNetwork')
n.add_node('a')
n.add_node('b')
n.add_node('c')
n.add_edge('a', 'b')
n.add_edge('b', 'c')
print(n)
```
%%%% Output: stream
Uid: ExampleNetwork
Type: Network
Directed: False
Multi-Edges: True
Number of nodes: 3
Number of edges: 2
%% Cell type:markdown id: tags:
Unless we want to explicitly add isolated nodes with no incident edges, we can omit the explicit call of the `add_node` function. If we add edges any node that does not exist already will be created and added automatically.
%% Cell type:code id: tags:
```
n = pp.Network(directed=False, multiedges=True, uid='ExampleNetwork')
n.add_edge('a', 'b')
n.add_edge('b', 'c')
print(n)
```
%%%% Output: stream
Uid: ExampleNetwork
Type: Network
Directed: False
Multi-Edges: True
Number of nodes: 3
Number of edges: 2
%% Cell type:markdown id: tags:
If we want to check explicitly whether a node exists before creating and edge, we can test this with the `in` operator on the set of node UIDS available via `Network.nodes.uids`:
%% Cell type:code id: tags:
```
print('d' in n.nodes.uids)
```
%%%% Output: stream
False
%% Cell type:markdown id: tags:
The following code will implicitly add a new node `d`, along with a new edge from `d` to `a`.
%% Cell type:code id: tags:
```
n.add_edge('c', 'd')
n.add_edge('d', 'a')
print(n)
```
%%%% Output: stream
Uid: ExampleNetwork
Type: Network
Directed: False
Multi-Edges: True
Number of nodes: 4
Number of edges: 4
%% Cell type:code id: tags:
```
print('d' in n.nodes.uids)
```
%%%% Output: stream
True
%% Cell type:markdown id: tags:
To count the number of nodes and edges in a network we can use the `number_of_nodes` and `number_of_edges` functions, or we could can compute `len` of `Network.nodes` and `Network.edges`:
%% Cell type:code id: tags:
```
print('Network has {0} nodes and {1} edges'.format(n.number_of_nodes(), n.number_of_edges()))
print('Number of nodes: {0}'.format(len(n.nodes)))
print('Number of edges: {0}'.format(len(n.edges)))
```
%%%% Output: stream
Network has 4 nodes and 4 edges
Number of nodes: 4
Number of edges: 4
%% Cell type:markdown id: tags:
### Node and Edge objects
In the simple example above, we generated nodes and edges by calling the `add_node` and `add_edge` function of the network instance. Internally, nodes and edges are represented as objects of type `Node` and `Edge` that can be referenced by one or more instances of type `Network`. Just like a `Network`, each instance of a `Node` and `Edge` has a UID. In the example above, `pathpy` has automatically created `Node` and `Edge` instances and has assigned the UIDs `a`, `b`, `c`, and `d` to those nodes. We can access those node objects via the node container `Network.nodes`. We can iterate through this dictionary to print a summary of all node objects referenced with a network object:
%% Cell type:code id: tags:
```
for v in n.nodes:
print(v)
```
%%%% Output: stream
Uid: a
Type: Node
Uid: b
Type: Node
Uid: c
Type: Node
Uid: d
Type: Node
%% Cell type:markdown id: tags:
We can also use the uid of a node to access a specific node object in a network by using the uid as an index to the `nodes` container:
%% Cell type:code id: tags:
```
print(n.nodes['a'])
```
%%%% Output: stream
Uid: a
Type: Node
%% Cell type:markdown id: tags:
Importantly, the same node object can be added to multiple networks (which comes in handy if, for instance, we want to store that the same set of nodes is connected via different network topologies). Above, we have created a second (so far empty) undirected network. We can now add the node object with uid `a` to this network as follows:
%% Cell type:code id: tags:
```
n2.add_node(n.nodes['a'])
print(n2)
```
%%%% Output: stream
Uid: 0x7fb9f86324c0
Uid: 0x7fd9403324f0
Type: Network
Directed: False
Multi-Edges: False
Number of nodes: 1
Number of edges: 0
%% Cell type:markdown id: tags:
While a node object can be added to multiple network objects, each network can contain only a single node with a given uid. If we add another node with a previously existing uid, no new new is added.
%% Cell type:code id: tags:
```
n.add_node('a')
for v in n.nodes:
print(v)
```
%%%% Output: stream
Uid: a
Type: Node
Uid: b
Type: Node
Uid: c
Type: Node
Uid: d
Type: Node
%% Cell type:markdown id: tags:
Similar to `nodes`, the `edges` container of the network contains all edges of a network and each edge is actually stored as an `Edge` object. Let us iterate through the edges container of network `n` to better understand this:
%% Cell type:code id: tags:
```
for e in n.edges:
print('---')
print(e)
```
%%%% Output: stream
---
Uid: 0x7fb9c8848640
Uid: 0x7fd940332430
Type: Edge
Directed: False
Nodes: |'a', 'b'|
---
Uid: 0x7fb9c88486a0
Uid: 0x7fd940332bb0
Type: Edge
Directed: False
Nodes: |'b', 'c'|
---
Uid: 0x7fb9c88484f0
Uid: 0x7fd940332610
Type: Edge
Directed: False
Nodes: |'c', 'd'|
---
Uid: 0x7fb9c8848c70
Uid: 0x7fd9403325e0
Type: Edge
Directed: False
Nodes: |'d', 'a'|
%% Cell type:markdown id: tags:
We see that the edge container contains one `Edge` object instance for each edge that we added before. Each `Edge` has again a unique identifier, which has been automatically created in our example above. Since we have created the network as an undirected network, the edges are undirected as well. This is indicated in the property `Directed` as well as with the notation `|'a', 'b'|`. Just like for `Node` or `Network` objects, we can manually create a directed edge object with a custom UID that connects the two nodes `a` and `b` as follows:
%% Cell type:code id: tags:
```
edge = pp.Edge(v=n.nodes['a'], w=n.nodes['b'], uid='MyEdge', directed=False)
print(edge)
```
%%%% Output: stream
Uid: MyEdge
Type: Edge
Directed: False
Nodes: |'a', 'b'|
%% Cell type:markdown id: tags:
This `Edge` object has a different UID than the existing edge betwee node `a` and `b`, which is why we can add it to (multi-edge) network `n` even though this network already contains an edge (with a different UID) between nodes `a` and `b`:
%% Cell type:code id: tags:
```
n.add_edge(edge)
print(n)
```
%%%% Output: stream
Uid: ExampleNetwork
Type: Network
Directed: False
Multi-Edges: True
Number of nodes: 4
Number of edges: 5
%% Cell type:markdown id: tags:
The summary of the network confirms that the network now contains five edges (between four pairs of nodes). This native support for multi-edge networks that can include both directed and undirected edges is an important feature of `pathpy`. It also means that every pair of nodes can be connected by more than one edge. We can access those edges via the `Network.edges` container in multiple ways. First, we can simply iterate through the edge objects as shown before. Second, we can directly access an `Edge` with a given UID as follows:
%% Cell type:code id: tags:
```
print(n.edges['MyEdge'])
```
%%%% Output: stream
Uid: MyEdge
Type: Edge
Directed: False
Nodes: |'a', 'b'|
%% Cell type:markdown id: tags:
Finally, we often want to access those edges that connect a specific pair of nodes. We can thus alternatively pass a pair of node uids as index to `Network.edges`. Since multiple edges between the same pair of nodes are possible, this generally returns a list of Edge objects, which - in the case of the node pair `a` and `b` - contains two different edge objects with different UIDs.
%% Cell type:code id: tags:
```
print(n.edges['a', 'b'])
```
%%%% Output: stream
{Edge |'a', 'b'|, Edge MyEdge}
%% Cell type:markdown id: tags:
We can access the degrees of nodes, i.e. the number of other nodes to which a node is connected, via the `degrees()` function of the Network. The degrees() function gives the undirected degree (i.e. irrespective of the directionality of an edge), while the `indegrees()` and `outdegrees()` functions give the degrees in a directed network (i.e. to how many other nodes the edges of a node point of from how many other nodes edges point to the given node).
All of those functions return a dictionary that can be indexed via the unique node ids. In the case of a multi-edge network, the degree counts multiple edges to the same neighbor, which is why the degree of node `a` in the network above is 3:
%% Cell type:code id: tags:
```
n.degrees()['a']
```
%%%% Output: execute_result
3
%% Cell type:markdown id: tags:
We can also remove nodes or edges. A network can contain isolated nodes (i.e. nodes with no incident edges), while it can (obviously) only contain edges between nodes that exist in the network.
To ensure this constraint, a call to `remove_node` will also remove all edges that are incident to the removed node, i.e. if we remove node `a` the `Edge` object with uid `MyEdge` will be removed from the network (along with two additional edges (a,b) and (a,d)):
%% Cell type:code id: tags:
```
n.remove_node('a')
print('MyEdge' in n.edges.uids)
print(n)
```
%%%% Output: stream
False
Uid: ExampleNetwork
Type: Network
Directed: False
Multi-Edges: True
Number of nodes: 3
Number of edges: 2
%% Cell type:markdown id: tags:
A call to `remove_edge` on the other hand does not remove the nodes incident to the removed edge, thus possibly leaving isolated nodes. If we remove the edge from `b` to `c` this will leave an isolated node `b`, which is still in the network:
%% Cell type:code id: tags:
```
n.remove_edge('b', 'c')
print('b' in n.nodes.uids)
print(n.nodes['b'])
print(n)
```
%%%% Output: stream
[10-26 12:47:43: WARNING] No edge was removed!
%%%% Output: stream
True
Uid: b
Type: Node
Uid: ExampleNetwork
Type: Network
Directed: False
Multi-Edges: True
Number of nodes: 3
Number of edges: 1
%% Cell type:markdown id: tags:
Note that we can either remove a specific edge between a pair of nodes or all edges between a pair of nodes. To better understand this, let us create three different edges between two nodes 'x' and 'y' and add them to a network. To simplify the construction of networks, we can use the functions `add_nodes` and `add_edges` which add multiple nodes and edges at a time:
%% Cell type:code id: tags:
```
n3 = pp.Network(multiedges=True)
n3.add_nodes('x', 'y')
e1 = pp.Edge(n3.nodes['x'], n3.nodes['y'], uid='edge1')
e2 = pp.Edge(n3.nodes['x'], n3.nodes['y'], uid='edge2')
e3 = pp.Edge(n3.nodes['x'], n3.nodes['y'], uid='edge3')
n3.add_edges(e1, e2, e3)
print(n3)
```
%%%% Output: stream
Uid: 0x7fb9f81654f0
Type: Network
Directed: True
Multi-Edges: True
Number of nodes: 2
Number of edges: 3
%% Cell type:markdown id: tags:
If we only remove the single edge with uid `edge2` between `x` and `y`, we can call:
%% Cell type:code id: tags:
```
n3.remove_edge('edge2')
print(n3.edges['x', 'y'])
```
%%%% Output: stream
{Edge edge1, Edge edge3}
%% Cell type:markdown id: tags:
At the same time, we can remove all edges between `x` and `y` by calling:
%% Cell type:code id: tags:
```
n3.remove_edge('x', 'y')
print(n3.edges['x', 'y'])
```
%%%% Output: stream
set()
%% Cell type:markdown id: tags:
## Networks, Nodes and Edges with attributes
You may wonder why `pathpy` stores nodes and edges as objects rather than as simple strings or numbers. The reason is that we often want to use networks to model relational data that contain additional information on nodes, edges, or networks. To support this, all `pathpy` objects can store arbitrary key-value data in the form of attributes. Let's explore this in a toy example for a social network:
%% Cell type:code id: tags:
```
trolls = pp.Network(uid='Trolls')
trolls.add_node('t')
trolls.add_node('b')
trolls.add_node('w')
trolls.add_edge('t', 'b', uid='t-b')
trolls.add_edge('t', 'w', uid='t-w')
print(trolls)
```
%%%% Output: stream
Uid: Trolls
Type: Network
Directed: True
Multi-Edges: False
Number of nodes: 3
Number of edges: 2
%% Cell type:markdown id: tags:
%% Cell type:code id: tags:
```
trolls.nodes['t']['name'] = 'Tom'
trolls.nodes['t']['age'] = 156
trolls.nodes['b']['name'] = 'Bert'
trolls.nodes['b']['age'] = 96