Files
aki_prj23_transparenzregister/documentations/research/networkx_pyvis_pandas.ipynb

18 KiB

Networkx und Pyvis - Minimal Working Example

Referenzen:

Networkx ist eine Python Bibliothek zur Erstellung und Analyse von Netzwerken. Pyvis ist eine Python Bibliothek zur interaktiven Visualisierung von Netzwerkgraphen. Beide können mit pip installiert werden.

In [2]:
# install networkx and pyvis using pip
!pip install networkx
!pip install pyvis
Requirement already satisfied: networkx in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (2.6.3)
Requirement already satisfied: pyvis in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (0.3.2)
Requirement already satisfied: ipython>=5.3.0 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pyvis) (7.29.0)
Requirement already satisfied: jsonpickle>=1.4.1 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pyvis) (3.0.1)
Requirement already satisfied: networkx>=1.11 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pyvis) (2.6.3)
Requirement already satisfied: jinja2>=2.9.6 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pyvis) (2.11.3)
Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (3.0.20)
Requirement already satisfied: jedi>=0.16 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (0.18.0)
Requirement already satisfied: traitlets>=4.2 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (5.1.0)
Requirement already satisfied: pexpect>4.3 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (4.8.0)
Requirement already satisfied: setuptools>=18.5 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (58.0.4)
Requirement already satisfied: matplotlib-inline in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (0.1.2)
Requirement already satisfied: decorator in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (5.1.0)
Requirement already satisfied: backcall in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (0.2.0)
Requirement already satisfied: pygments in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (2.10.0)
Requirement already satisfied: appnope in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (0.1.2)
Requirement already satisfied: pickleshare in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from ipython>=5.3.0->pyvis) (0.7.5)
Requirement already satisfied: parso<0.9.0,>=0.8.0 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from jedi>=0.16->ipython>=5.3.0->pyvis) (0.8.2)
Requirement already satisfied: MarkupSafe>=0.23 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from jinja2>=2.9.6->pyvis) (1.1.1)
Requirement already satisfied: ptyprocess>=0.5 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pexpect>4.3->ipython>=5.3.0->pyvis) (0.7.0)
Requirement already satisfied: wcwidth in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=5.3.0->pyvis) (0.2.5)

Panda Dataframe mit Beispieldaten

Um ein Netzwerk aufbauen zu können, brauchen wir Daten für die Knoten (nodes) und Kanten (edges). Die Daten speichern wir jeweils in einem Panda Dataframe. Pandas kann ebenfalls mit pip installiert werden.

In [3]:
# install pandas using pip
!pip install pandas
Requirement already satisfied: pandas in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (1.3.4)
Requirement already satisfied: python-dateutil>=2.7.3 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pandas) (2.8.2)
Requirement already satisfied: pytz>=2017.3 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pandas) (2021.3)
Requirement already satisfied: numpy>=1.17.3 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from pandas) (1.20.3)
Requirement already satisfied: six>=1.5 in /Users/kim/opt/anaconda3/lib/python3.9/site-packages (from python-dateutil>=2.7.3->pandas) (1.16.0)

Die Knoten unseres Netzwerks sollen die Unternehmen und Personen darstellen. Eine id ermöglicht die eindeutige Identifizierung eines Knoten und hilft Duplikate zu vermeiden. Um Unternehmen von Personen differenzieren zu können, wurde zusätzlich die Information type aufgenommen. Sie dient in unserem Beispiel dazu, die Form des Knoten zu bestimmen. Durch label bekommt der Knoten eine für den User verständliche Bezeichnung. Weitere Informationen, wie zum Beispiel branche, können später für das Mouse Over oder die Größe oder Farbe der Knoten verwendet werden.

Um in einem späteren Schritt die Attribute der Knoten an das Netzwerk zu übergeben, generieren wir zusätzlich eine Spalte shape, eine Spalte color und eine Spalte title.

In [4]:
# import pandas
import pandas as pd

# create dataframe based on the sample data
df_nodes = pd.read_csv("nodes.csv", sep=";")

# define shape based on the type
node_shape = {"Company": "dot", "Person": "triangle"}
df_nodes["shape"] = df_nodes["type"].map(node_shape)

# define color based on branche
node_color = {
    "Branche 1": " #f3e8eeff",
    "Branche 2": "#bacdb0ff",
    "Branche 3": "#729b79ff",
    "Branche 4": "#475b63ff",
    "Branche 5": "#2e2c2fff",
}
df_nodes["color"] = df_nodes["branche"].map(node_color)

# add information column that can be used for the mouse over in the graph
df_nodes = df_nodes.fillna("")
df_nodes["title"] = df_nodes["label"] + "\n" + df_nodes["branche"]

# show first five entries of the dataframe
print(df_nodes.head())
   id    label     type    branche shape       color               title
0   1  Firma 1  Company  Branche 1   dot   #f3e8eeff  Firma 1\nBranche 1
1   2  Firma 2  Company  Branche 2   dot   #bacdb0ff  Firma 2\nBranche 2
2   3  Firma 3  Company  Branche 3   dot   #729b79ff  Firma 3\nBranche 3
3   4  Firma 4  Company  Branche 4   dot   #475b63ff  Firma 4\nBranche 4
4   5  Firma 5  Company  Branche 5   dot   #2e2c2fff  Firma 5\nBranche 5

Die Kanten visualisieren die Beziehungen zwischen den Unternehmen und Personen. Um in Pyvis eine Kante darzustellen braucht es minimal die Information zwischen welchen beiden Knoten eine Kante dargestellt werden soll. In den Beispieldaten entspricht dies from und to. Es wird jeweils auf die eindeutige id der jeweiligen Knoten referenziert. label bezeichnet hier die Art der Beziehung, z.B. AR = Aufsichtsrat.

In [5]:
# create dataframe based on the sample data
df_edges = pd.read_csv("edges.csv", sep=";")

# show first five entries of the dataframe
print(df_edges.head())
   from  to label  weight
0     1  50    AR       2
1     1  41     V       4
2     1  46    WP       5
3     1  48    AR       1
4     1  40     V       4

Erstellung eines Netzwerks mit networkx

Zur Erstellung des Netzwerks nutzen wir networkx, da diese Bibliothek bessere Analysemöglichkeiten hat als pyvis. Das mit networkx erstellte Netzwerk können wir später an pyvis zur interaktiven Visualisierung übergeben werden.

Wir erstellen die Knoten und Kanten auf Basis unsere beiden Dataframes.

In [6]:
# import networkx
import networkx as nx

# initiate graph
graph = nx.MultiGraph()

# create edges from dataframe
graph = nx.from_pandas_edgelist(
    df_edges, source="from", target="to", edge_attr=["label"]
)  # , 'weight'])

# pos = nx.spring_layout(graph, weight = 'weight')
# df_nodes['x'] = df_nodes['id'].map(lambda x: pos[x][0])
# df_nodes['y'] = df_nodes['id'].map(lambda x: pos[x][1])

# update node attributes from dataframe
nodes_attr = df_nodes.set_index("id").to_dict(orient="index")
nx.set_node_attributes(graph, nodes_attr)

Mit Hilfe von single_source_shortest_path_length lässt sich die Anzahl der Nachbarn in unterschiedlichen Ebenen bestimmen. Durch die Eingrenzung des cutoff listet es alle Nachbarn und bis dahin benötigte Schritte.

In [7]:
# create empty list to save k-neighbours for each node
k_neighbours = []

# loop all nodes in the graph
for node in graph.nodes:
    # create empty dictionary
    dict = {}
    # get node id
    dict["id"] = node
    # get k-neighbours for k=1,2,3, subtract -1 since output of single_source_shortest_path_length contains node itself
    dict["k=1"] = len(nx.single_source_shortest_path_length(graph, node, cutoff=1)) - 1
    dict["k=2"] = len(nx.single_source_shortest_path_length(graph, node, cutoff=2)) - 1
    dict["k=3"] = len(nx.single_source_shortest_path_length(graph, node, cutoff=3)) - 1
    # append list for each node
    k_neighbours.append(dict)

print(k_neighbours[:5])
[{'id': 1, 'k=1': 9, 'k=2': 38, 'k=3': 49}, {'id': 50, 'k=1': 4, 'k=2': 19, 'k=3': 46}, {'id': 41, 'k=1': 8, 'k=2': 30, 'k=3': 48}, {'id': 46, 'k=1': 4, 'k=2': 20, 'k=3': 47}, {'id': 48, 'k=1': 5, 'k=2': 21, 'k=3': 46}]

Visualisierung des Netzwerks mit pyvis

Für die Visualisierung importieren wir Network von pyvis.network und initialisiern das pyvis Netzwerk. Mit der Methode from_nx können wir das networkx Netzwerk übergeben.

Die Größe der Knoten bestimmen wir je nach Auswahl entweder aufgrund der Anzahl der Verbindungen zu anderen Knoten oder anhand der Eigenvektor-Zentralität. Knoten mit vielen Verbindungen bzw. höherer Zentralität werden größer dargestellt.

In [8]:
# visualize using pyvis
from pyvis.network import Network

# initiate network
net = Network(
    directed=False, neighborhood_highlight=True, bgcolor="white", font_color="black"
)

# pass networkx graph to pyvis
net.from_nx(graph)

# set edge options
net.inherit_edge_colors(False)
net.set_edge_smooth("dynamic")

# chose size format
size_type = "edges"  # select 'edges' or 'eigen'

adj_list = net.get_adj_list()

if size_type == "eigen":
    eigenvector = nx.eigenvector_centrality(graph)

# calculate and update size of the nodes depending on their number of edges
for node_id, neighbors in adj_list.items():
    if size_type == "edges":
        size = len(neighbors) * 5
    if size_type == "eigen":
        size = eigenvector[node_id] * 900
    next(
        (node.update({"value": size}) for node in net.nodes if node["id"] == node_id),
        None,
    )
    next(
        (node.update({"size": size}) for node in net.nodes if node["id"] == node_id),
        None,
    )

# set the node distance and spring lenght using repulsion
net.repulsion(node_distance=250, spring_length=150)

# activate physics buttons to further explore the available solvers:
# barnesHut, forceAtlas2Based, repulsion, hierarchicalRepulsion
net.show_buttons(filter_=["physics"])

# save graph as HTML
net.save_graph("networkx_pyvis.html")
In [9]:
eigenvector = nx.eigenvector_centrality(graph)
print(eigenvector)
{1: 0.2590276672203281, 50: 0.11573458719186203, 41: 0.23089631495685015, 46: 0.09723686259252076, 48: 0.11963178876421071, 40: 0.19182246741215414, 37: 0.11584617541421141, 2: 0.2156948179967803, 44: 0.09069008350377152, 36: 0.12164282800223333, 49: 0.07333232608496432, 14: 0.05222928767261443, 15: 0.09120494717875655, 16: 0.07647921544493948, 17: 0.08405528712304138, 18: 0.07986204168307376, 19: 0.1323918034494897, 31: 0.2928131232735568, 20: 0.10812229868312798, 21: 0.08788249236713751, 22: 0.1186928914916743, 23: 0.11742853245579209, 6: 0.2077969102871851, 25: 0.0594562062424749, 39: 0.05827758860927808, 26: 0.12023885869170473, 27: 0.07040921471375026, 28: 0.04832885692517859, 29: 0.1266153741460298, 3: 0.19455723550826837, 35: 0.3112735938438682, 4: 0.1867770233044344, 5: 0.18288061376562756, 43: 0.10566912562335516, 7: 0.08267466177528007, 45: 0.061959219104767996, 8: 0.1277676533326448, 9: 0.15188022282282598, 32: 0.3558117649603071, 38: 0.07531646453054974, 47: 0.060242987519435645, 34: 0.08786367126085788, 33: 0.055712820471659395, 10: 0.1540931474648185, 11: 0.08703487502978537, 12: 0.02664931447399513, 13: 0.03161820778804042, 30: 0.042565390898229194, 24: 0.04996908258118087, 42: 0.011302736073531213}

Offene Fragen

  • Gibt es Knoten ohne Verbindung? Wenn erst die Kanten generiert werden, werden diese vermutlich bisher nicht berücksichtigt.
  • Bei der Auswahl eines Unternehmens werden verbundene Knoten nicht farblich angezeigt
  • Bei mehreren Verbindung zwischen zwei Knoten wird derzeit nur die erste angezeigt. Dies kann umgehen werden, wenn man das Netzwerk die Option directed = True mitgibt. Allerdings werden dadurch die Kanten zu Pfeilen und man muss bei der Speicherung der Verbindungen aufpassen. Gibt es auch Möglichkeiten für undirected graphs?
  • Sollen die Kanten zusätzlich gewichtet werden?

Resultierende Anforderungen an die Daten

Relationale Daten für die Kanten und Ecken sind ausreichend. Für die Knoten (= Unternehmen, Personen) werden benötigt:

  • Eindeutige ID
  • Bezeichnung, z.B. Name des Unternehmens bzw. der Person
  • Weitere Informationen, die im Mouse Over angezeigt oder nach denen die Farben oder Größen der Knoten konfiguriert werden sollen

Für die Kanten (= Verbindungen) werden benötigt:

  • Eindeutige IDs zwischen denen die Verbindung besteht
  • Art der Verbindung
  • Ggfs. Gewichtungen