Netzwerkgraphen erstellen und analysieren#

Die in Form von Node- und Edgelists aufbereiteten Daten werden im Folgenden mit dem Python Modul NetworkX zu einem Netzwerkgraphen zusammengefügt (Tutorial: Ladd et al. 2017). Der Graph kann später auf seine strukturellen Eigenschaften hin untersucht werden, um etwas über die betrachtete wissenschaftliche Gemeinschaft und deren Informationsaustausch auszusagen. Außerdem ist es möglich, die Position einzelner Paper zu analysieren.

import csv
from operator import itemgetter
import networkx as nx
from networkx.algorithms import community
import pandas as pd
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import numpy as np

Zitationsnetzwerke erstellen#

Knoten (Paper) und Kanten (Zitationsbeziehungen) werden zunächst aus den csv-Dateien eingelesen und dann als Netzwerkgraph gespeichert. Zitationsbeziehungen sind geeignet, um ein gerichtetes Netzwerk zu erstellen, da Zitationen selten reziprok sind. Da einige Analyseschritte aber nur mit einem ungerichteten Netzwerk möglich sind, werden sowohl ein gerichteter (G) und ein ungerichteter (G_undir) Graph erstellt.

Schritt 1: Daten einlesen#

# Pfad für Daten definieren
dataPath = "./network_data"
outputPath = "/network_graph"
# Knoten als Dataframe einlesen
dfNodesAttributes = pd.read_csv(f'{dataPath}/citation_network_NODELIST.csv', delimiter=',')
# Knoten als Liste von tuplen (ID, Eigenschaften) einlesen
with open(f'{dataPath}/citation_network_NODELIST.csv', 'r') as nodescsv:
    nodereader = csv.reader(nodescsv, delimiter=',') 
    nodes = [tuple(n) for n in nodereader][1:]
#print(nodes[:3])
# Kanten als Liste von tuplen (Source, Target) einlesen
with open(f'{dataPath}/citation_network_EDGELIST.csv', 'r') as edgecsv:
    edgereader = csv.reader(edgecsv, delimiter=';')
    edges = [tuple(e) for e in edgereader][1:]
print(edges[:3])
[('liu hy 2021', 'anderson cj 1999'), ('liu hy 2021', 'choi ds 2012'), ('liu hy 2021', 'clifton a 2017')]
# Test ob Knoten und Kanten korrekt eingelesen wurden
print('Anzahl der Knoten: ' + str(len(nodes)))
print('Anzahl der Kanten: ' + str(len(edges)))
Anzahl der Knoten: 46569
Anzahl der Kanten: 189256

Schritt 2: Netzwerkgraphen erstellen#

In diesem Schritt werden die eingelesenen Daten zu einem Netzwerk zusammengefügt. Wir erzeugen mit NetworkX auch die Knoten des Graphen über die Edgelist. Es zeigt sich, dass 71% des Gesamtkorpus Teil des Netzwerkes sind, also über min. eine Beziehung verfügen. In Anbetracht dessen, dass wir nur 10% aller Zitationsbeziehungen abbilden können, ist das eine zufriedenstellende Zahl.

# Befehl, um alle Knoten und Kanten zu löschen: 
# G = G.clear()
G = nx.DiGraph() # Gerichtetes Graph-Objekt erstellen
G_undir = nx.Graph() # Ungerichtetes Graph-Objekt erstellen
# Kanten werden zu G hinzugefügt. 
# Knoten sind daher nur die Paper, die min. eine Beziehung haben, also Teil des Netzwerks sind
G.add_edges_from(edges) # Gerichtete Beziehungen anfügen
G_undir.add_edges_from(edges) # Ungerichtete Beziehungen anfügen
print(nx.info(G))
print(str(len(G.nodes)) + ' Knoten von insg. ' + str(len(nodes)) + ' Knoten sind Teil des Netzwerkes (' 
      + str( ((len(G.nodes))/(len(nodes)))*100 ) + '%).' )
DiGraph with 32687 nodes and 176557 edges
32687 Knoten von insg. 46569 Knoten sind Teil des Netzwerkes (70.19047005518692%).
/var/folders/q0/0x5x7rwd62s15f9x0xbjv18h0000gp/T/ipykernel_36971/4171752995.py:5: DeprecationWarning: info is deprecated and will be removed in version 3.0.

  print(nx.info(G))

Schritt 3: Attribute hinzufügen#

In diesem Schritt werden die Attribute der Knoten, d.h. das Publikationsjahr, die Kategorienzuordnung und der Titel des Papers, hinzugefügt. Hier wird das Ausmaß des Fehlers durch die ID-Zuordnung sichtbar, der bereits im vorherigen Notebook angesprochen wurde. Für jede Knoten-ID wird ein Dictionary erstellt, in dem die Attribute verzeichnet sind. Die Dictionaries haben sehr viel weniger Schlüssel, als unsere Nodelist Knoten enthält. Das liegt daran, dass einige IDs doppelt vorkommen und im Dictionary fälschlicherweise zusammengefügt werden. Die Anzahl an Papern, die eine ID mit min. einem anderen Paper teilen, liegt bei 6309, das sind mehr als 13% aller Paper. Diese hohe Zahl spricht gegen die Verlässlichkeit unserer Klassifikationsmethode und macht deutlich, dass eine eindeutige Klassifikation der Paper bei gleichzeitiger Erkennbarkeit bei der Suche nach Beziehungen, ein Desiderat für zukünftige Ansätze ist.

# Leere Dictionaries für die Attribute erstellen
year_dict = {}
cat_dict = {}
title_dict = {}
for node in nodes: # Attribute auslesen und den Dictionaries zuordnen
    attributes = node[1].split(',')
    year_dict[node[0]] = int(node[1])
    cat_dict[node[0]] = node[2]
    title_dict[node[0]] = node[3]
# Anzahl der Elemente in den Dicts anzeigen 
print(len(year_dict), len(cat_dict), len(title_dict))
doubles = len(pd.concat(g for _, g in dfNodesAttributes.groupby("id") if len(g) > 1))
print('Grund für den Verlust einiger Knoten:')
print(str(doubles) + # Fehler durch identische IDs ausgeben
      ' Paper haben die selbe ID wie min. ein anderes Paper.')
42949 42949 42949
Grund für den Verlust einiger Knoten:
6432 Paper haben die selbe ID wie min. ein anderes Paper.
# Attribute mit dem Graphen verknüpfen
nx.set_node_attributes(G, year_dict, 'publication_year') 
nx.set_node_attributes(G_undir, year_dict, 'publication_year') 
nx.set_node_attributes(G, cat_dict, 'category')
nx.set_node_attributes(G_undir, cat_dict, 'category')
nx.set_node_attributes(G, title_dict, 'title')
nx.set_node_attributes(G_undir, title_dict, 'title')

Schritt 4: Graphen für historische Netzwerke erstellen#

Für die historischen Zitationsnetzwerke bis 2015, 2005 und 1995 erstellen wir lediglich den gerichteten Graphen.

# Graphen erstellen
G_2015 = nx.DiGraph() # Gerichtetes Graph-Objekt erstellen
G_2005 = nx.DiGraph()
G_1995 = nx.DiGraph()
# Kanten einlesen
with open(f'{dataPath}/citation_network_EDGELIST_bis2015.csv', 'r') as edgecsv:
    edgereader = csv.reader(edgecsv, delimiter=';')
    edges_2015 = [tuple(e) for e in edgereader][1:]
with open(f'{dataPath}/citation_network_EDGELIST_bis2005.csv', 'r') as edgecsv:
    edgereader = csv.reader(edgecsv, delimiter=';')
    edges_2005 = [tuple(e) for e in edgereader][1:]
with open(f'{dataPath}/citation_network_EDGELIST_bis1995.csv', 'r') as edgecsv:
    edgereader = csv.reader(edgecsv, delimiter=';')
    edges_1995 = [tuple(e) for e in edgereader][1:]
# Knoten einlesen
dfNodes_2015 = pd.read_csv(f'{dataPath}/citation_network_NODELIST_bis2015.csv', delimiter=',')
dfNodes_2005 = pd.read_csv(f'{dataPath}/citation_network_NODELIST_bis2005.csv', delimiter=',')
dfNodes_1995 = pd.read_csv(f'{dataPath}/citation_network_NODELIST_bis1995.csv', delimiter=',')
# Kanten anfügen
G_2015.add_edges_from(edges_2015)
G_2005.add_edges_from(edges_2005)
G_1995.add_edges_from(edges_1995)
# Knoten 
dfNodes_2015 = pd.read_csv(f'{dataPath}/citation_network_NODELIST_bis2015.csv', delimiter=',')
# Übersicht
print('Zitationsnetzwerk bis 2015: ', len(G_2015.nodes), ' Knoten.', "{:.2f}".format((len(G_2015.nodes)/len(dfNodes_2015.index))*100), '% der', len(dfNodes_2015.index), 'Paper bis 2015.')
print('Zitationsnetzwerk bis 2005: ', len(G_2005.nodes), ' Knoten.', "{:.2f}".format((len(G_2005.nodes)/len(dfNodes_2005.index))*100), '% der', len(dfNodes_2005.index), 'Paper bis 2005.')
print('Zitationsnetzwerk bis 1995: ', len(G_1995.nodes), ' Knoten.', "{:.2f}".format((len(G_1995.nodes)/len(dfNodes_1995.index))*100), '% der', len(dfNodes_1995.index), 'Paper bis 1995.')
Zitationsnetzwerk bis 2015:  12684  Knoten. 63.40 % der 20006 Paper bis 2015.
Zitationsnetzwerk bis 2005:  1802  Knoten. 54.18 % der 3326 Paper bis 2005.
Zitationsnetzwerk bis 1995:  441  Knoten. 46.72 % der 944 Paper bis 1995.
# Attribute mit den Graphen verknüpfen
nx.set_node_attributes(G_2015, year_dict, 'publication_year') # bis 2015
nx.set_node_attributes(G_2015, cat_dict, 'category')
nx.set_node_attributes(G_2015, title_dict, 'title')
nx.set_node_attributes(G_2005, year_dict, 'publication_year') # bis 2005
nx.set_node_attributes(G_2005, cat_dict, 'category')
nx.set_node_attributes(G_2005, title_dict, 'title')
nx.set_node_attributes(G_1995, year_dict, 'publication_year') # bis 1995
nx.set_node_attributes(G_1995, cat_dict, 'category')
nx.set_node_attributes(G_1995, title_dict, 'title')

Nachdem die Graphen erstellt und die Attribute der Knoten angefügt wurden, können mit NetworkX nun sowohl strukturelle Eigenschaften des Gesamtnetzwerkes, als auch Zentralitätsmaße für die Positionen der einzelnen Knoten berechnet werden. Wir berechnen die verschiedenen Maße für unser Zitationsnetzwerk.

Strukturelle Netzwerkeigenschaften analysieren#

Die Netzwerkdichte ist mit ca. 0,017% gering, nur 0,017% aller Möglichen Verbindungen sind in unserem Netzwerk realisiert. Unser Netzwerk besteht außerdem aus mehreren, unverbundenen Subnetzwerken. Die Dichte der größten Komponente ist mit ca. 0,036% doppelt so hoch wie die des gesamten Netzwerkes. Ein Diameter von 18 bedeutet außerdem, dass die am weitesten entfernten Knoten innerhalb der größten Komponente 18 Schritte zueinander brauchen würden.

Transitivität bezieht sich wie die Dichte auf den Grad, in dem das Netzwerk miteinander verbunden ist. Während die Dichte den Anteil der realisierten an allen möglichen Verbindungen darstellt, bezeichnet Transitivität die Anzahl der realisierten 3-er Verbindungen (vorstellbar als Dreick, in dem alle 3 Paper eine Zitationsbeziehung miteinander haben) an den möglichen 3-er Verbindungen. Genau wie die Dichte ist auch die Transitivität unseres Netzwerkes gering, unser gebildetes Zitationsnetzwerk ist also recht locker verbunden-

density = nx.density(G)
print("Network density (*100): ", density*100)
Network density (*100):  0.016525252399019124
# Ungerichteter Graph:
# Test, ob alle Knoten verbunden sind oder der Graph aus mehreren Subnetzwerken besteht
print(nx.is_connected(G_undir))
False
# Ungerichteter Graph: 
# Die größte Komponente finden und als Subnetzwerk speichern
components = nx.connected_components(G_undir) # Liste aller Komponenten erstellen
largest_component = max(components, key=len) # Größte Komponente finden
subgraph = G_undir.subgraph(largest_component)
print('nodes in largest component: ', len(subgraph.nodes))
print('Network density largest component (*100): ', nx.density(subgraph)*100)
print('Triadic closure largest component: ', nx.transitivity(subgraph))
nodes in largest component:  31323
Network density largest component (*100):  0.03575989761017044
Triadic closure largest component:  0.03188490210655254
# Diameter für die größte Komponente berechnen
diameter = nx.diameter(subgraph) # Längsten Pfad berechnen
print("Pfadlänge zwischen den am weitesten entfernten Knoten (in der größten Komponente): ", diameter)
Pfadlänge zwischen den am weitesten entfernten Knoten (in der größten Komponente):  18
# Transitivität berechnen
triadic_closure = nx.transitivity(G)
triadic_closure_undir = nx.transitivity(G_undir)
print("Triadic closure (gerichteter Graph):", triadic_closure)
print("Triadic closure (ungerichteter Graph):", triadic_closure_undir)
Triadic closure (gerichteter Graph): 0.06715628759274808
Triadic closure (ungerichteter Graph): 0.03188763489192373

Vergleich mit den historischen Netzwerken#

Die historischen Netzwerke bis 2015 bzw. 2005 und 1995 haben höhere Dichten und Transitivitätswerte. Diese sind allerdings vergleichend schwierig zu interpretieren, da die Netzwerke eine viel kleinere Anzahl an Knoten haben, was auch eine sehr viel geringere Anzahl an möglichen Verbindungen bedeutet.

# Dichte berechnen
print('Dichte (*100):')
print('Zitationsnetzwerk bis 2015: ', nx.density(G_2015)*100)
print('Zitationsnetzwerk bis 2005: ', nx.density(G_2005)*100)
print('Zitationsnetzwerk bis 1995: ', nx.density(G_1995)*100)
# Transitivität
print('Transitivität:')
print('Zitationsnetzwerk bis 2015: ', nx.transitivity(G_2015))
print('Zitationsnetzwerk bis 2005: ', nx.transitivity(G_2005))
print('Zitationsnetzwerk bis 1995: ', nx.transitivity(G_1995))
Dichte (*100):
Zitationsnetzwerk bis 2015:  0.03447789887426195
Zitationsnetzwerk bis 2005:  0.1748627750891877
Zitationsnetzwerk bis 1995:  0.541640898783756
Transitivität
Zitationsnetzwerk bis 2015:  0.08343338450501618
Zitationsnetzwerk bis 2005:  0.10291509643094658
Zitationsnetzwerk bis 1995:  0.09906661009758168

Community Detection#

NetworkX bietet verschiedene Möglichkeiten, um Gruppenstrukturen innerhalb von Netzwerken zu entdecken. Wir nutzen im Folgenden die Methode greedy_modularity_communities, um die Paper unterschiedlichen Gruppen zuzuordnen. Hierbei wird versucht, Subgruppen zu identifizieren, die nach innen stark, nach außen zu anderen Subgruppen aber nur leicht verbunden sind. Die Gruppenzugehörigkeit wird sodann als Attribut den Knoten des Netzwerkes hinzugefügt - wir berechnen sie auch für die historischen Zitationsnetzwerke von 2015, 2005 und 1995.

Dokumentation des genutzten Algorithmus: https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.community.modularity_max.greedy_modularity_communities.html

Diese Gruppenzugehörigkeit stellt die Basis unserer Analyse der Interdisziplinarität von Zitationsgemeinschaften im Feld der Sozialen Netzwerkanalyse dar. Im nächsten Notebook werden wir diese Gruppen näher beschreiben, indem wir die Verteilung der Disziplinen-Kategorien, besonders zentrale Paper und besonders häufige Worte in den Titeln betrachten. In Kombination mit dem historischen Vergleich hoffen wir so, interdisziplinäre Zusammenarbeit bzw. Informationsflüsse und thematische Überschneidungen, sowie zentrale Einflüsse sichtbar zu machen.

# Knoten werden verschiedenen Gruppen zugeteilt
communities = community.greedy_modularity_communities(G)
# Dictionary mit Knoten und deren Gruppenzuordnung erstellen 
modularity_dict = {}
for i,c in enumerate(communities):
    for name in c:
        modularity_dict[name] = i
nx.set_node_attributes(G, modularity_dict, 'modularity') # Gruppenzuordnung als Attribut anfügen

Community Detection für die historischen Netzwerke#

# Gruppen berechnen
communities_2015 = community.greedy_modularity_communities(G_2015)
communities_2005 = community.greedy_modularity_communities(G_2005)
communities_1995 = community.greedy_modularity_communities(G_1995)
# Gruppenzugehörigkeit als Attribut hinzufügen
modularity_dict_2015 = {}
modularity_dict_2005 = {}
modularity_dict_1995 = {}
for i,c in enumerate(communities_2015):
    for name in c:
        modularity_dict_2015[name] = i
nx.set_node_attributes(G_2015, modularity_dict_2015, 'modularity')
for i,c in enumerate(communities_2005):
    for name in c:
        modularity_dict_2005[name] = i
nx.set_node_attributes(G_2005, modularity_dict_2005, 'modularity')
for i,c in enumerate(communities_1995):
    for name in c:
        modularity_dict_1995[name] = i
nx.set_node_attributes(G_1995, modularity_dict_1995, 'modularity')

Zentralitätsmaße berechnen#

Mit NetworkX können oben stehende Zentralitätsmaße für alle Knoten des Netzwerkes berechnet werden. Degree bezeichnet die Anzahl aller Beziehungen eines Knotens, In-Degree nur die eingehenden, d.h. hier, wie oft das Paper zitiert wurde. Die Betweenness berechnet sich aus der Anzahl der kürzesten Pfade aller Knoten untereinander, die über diesen einen Knoten laufen. Knoten mit hoher Betweenness-Zentralität sind daher Schnittpunkte wichtiger Informationsflüsse.

# Degree-Zentralität 
degree_dict = dict(G.degree(G.nodes()))
nx.set_node_attributes(G, degree_dict, 'degree')
# In-Degree Zentralität berechnen
indegree_dict = dict(G.in_degree(G.nodes()))
nx.set_node_attributes(G, indegree_dict, 'in-degree')
# Betweenness-Zentralität berechnen
betweenness_dict = nx.betweenness_centrality(G) 
nx.set_node_attributes(G, betweenness_dict, 'betweenness')
# Eigenvektor-Zentralität berechnen (ungerichteter Graph)
eigenvector_dict = nx.eigenvector_centrality(G_undir)
nx.set_node_attributes(G_undir, eigenvector_dict, 'eigenvector_undir')

Mit dem unten stehenden Code können die Knoten nach ihren unterschiedlichen Zentralitätsmaßen sortiert und durchsucht werden. Hier ist es spannend zu sehen, dass die zentralsten Paper, sowohl was In-Degree, als auch Betweenness-Zentralität betrifft, neben der Soziologie, aus der Mathematik, der Physik und multidisziplinären Journalen kommen.

Dies ist besonders vor dem Hintergrund der gesamten Verteilung der Kategorien in unserem Korpus spannend, da Physik und Mathematik hier anteilig nur gering vertreten sind neben einer Mehrheit an sozialwissenschaftlichen Papern. Dass die zentralsten Paper trotzdem so häufig aus diesen Disziplinen kommen, zeigt ihre Relevanz und Strahlkraft in einem mehrheitlich sozialwissenschaftlichen Korpus.

dfNodes = pd.DataFrame.from_dict(dict(G.nodes(data=True)), orient='index')
pd.set_option('display.max_rows', None)
dfNodes.sort_values('in-degree', ascending = False) # Korpus nach Zentralitätsmaß sortiert anzeigen

Vergleich mit historischen Netzwerken#

Für die historischen Zitationsnetzwerke berechnen wir die beiden wichtigen Zentralitätsmaße In-Degree und Betweenness. Auch hier können die Knoten wieder danach sortiert und durchgeschaut werden.

# In-Degree
indegree_dict_2015 = dict(G_2015.in_degree(G_2015.nodes())) # bis 2015
nx.set_node_attributes(G_2015, indegree_dict_2015, 'in-degree')
indegree_dict_2005 = dict(G_2005.in_degree(G_2005.nodes())) # bis 2005
nx.set_node_attributes(G_2005, indegree_dict_2005, 'in-degree')
indegree_dict_1995 = dict(G_1995.in_degree(G_1995.nodes())) # bis 1995
nx.set_node_attributes(G_1995, indegree_dict_1995, 'in-degree')
# Betweenness
betweenness_dict_2015 = nx.betweenness_centrality(G_2015) 
nx.set_node_attributes(G_2015, betweenness_dict_2015, 'betweenness')
betweenness_dict_2005 = nx.betweenness_centrality(G_2005) 
nx.set_node_attributes(G_2005, betweenness_dict_2005, 'betweenness')
betweenness_dict_1995 = nx.betweenness_centrality(G_1995) 
nx.set_node_attributes(G_1995, betweenness_dict_1995, 'betweenness')
# bis 2015
dfNodes_2015 = pd.DataFrame.from_dict(dict(G_2015.nodes(data=True)), orient='index')
dfNodes_2015.sort_values('betweenness', ascending = False) # Korpus nach Zentralitätsmaß sortiert anzeigen
# bis 2005
dfNodes_2005 = pd.DataFrame.from_dict(dict(G_2005.nodes(data=True)), orient='index')
dfNodes_2005.sort_values('betweenness', ascending = False) # Korpus nach Zentralitätsmaß sortiert anzeigen
# bis 1995
dfNodes_1995 = pd.DataFrame.from_dict(dict(G_1995.nodes(data=True)), orient='index')
dfNodes_1995.sort_values('betweenness', ascending = False) # Korpus nach Zentralitätsmaß sortiert anzeigen

Schaut man sich die wichtigsten bzw. zentralsten Paper in den historischen Zitationsnetzwerken bis 2015, 2005 und 1995 an, so lässt sich die im wissenschaftsgeschichtlichen Teil zur Sozialen Netzwerkanalyse bereits beschriebene Entwicklung gut nachvollziehen. Aufgrund dieser wissenschaftsgeschichtlichen Erzählung zum Eintritt der Physiker:innen ins Feld der Sozialen Netzwerkanalyse hatten wir folgende Zeitpunkte definiert:

  1. 1995: Stand des Netzwerks kurz vor dem Eintritt der Physiker:innen

  2. 2005: Stand des Netzwerks kurz nach dem Eintritt der Physiker:innen und den ersten relevanten Veröffentlichungen

  3. 2015: Stand des Netzwerkes nachdem sich die ersten Wogen zwischen sozialen Netzwerk-Analyst:innen und Physiker:innen geglättet haben und sich das Forschungsfeld gewandelt hat

  4. 2022: Der aktuelle Stand des Forschungsfeldes

Im Netzwerk bis 1995 finden sich dementsprechend auch tatsächlich gar keine Paper aus der Physik. Die Neigung der Disziplin zur Interdisziplinarität zeigt sich allerdings auch hier schon, da ein mathematisches Paper das am häufigsten zitierte Paper ist. Interessant ist, dass sich dieses Bild auch im Netzwerk bis 2005 noch zeigt: Hier dominieren mit den höchsten Zitationszahlen soziologische Artikel aus den 1980er und -90er Jahren, das Small-World-Paper von Newman & Watts (2000) taucht allerdings schon recht weit oben auf. 2015 hat die Physik dann Einzug in den soziologischen netzwerkanalytischen Kanon gefunden und wird schon häufiger zitiert, zusammen mit Papern aus Mathematik und multidisziplinären Journals.

Netzwerkgraphen exportieren#

Die Graphen des gesamten, aktuellen Netzwerkes sowie der Ausschnitte zu unterschiedlichen Zeitpunkte werden als .gexf-Datei expotiert, die sowohl mit NetworkX, als auch verschiedenen Programmen zur Netzwerkanalyse (wie etwa Gephi) eingelesen werden können. Außerdem werden die Knoten und ihre Attribute als csv-Datei exportiert, um die z.T. sehr zeitaufwenidgen Berechnungen (v.a. Betweenness und Community Detection) nicht immer von Neuem durchführen zu müssen.

# Gesamtnetzwerk als gexf und csv exportieren
nx.write_gexf(G, f'{outputPath}citation_network.gexf') # Graphendatei
dfNodes = dfNodes.reset_index()
dfNodes.to_csv(f'{outputPath}/citation_network_nodes.csv', encoding='utf-8', index=False) # Knoten als csv
# DFs für Export vorbereiten damit Index mitkommt
dfNodes_2015 = dfNodes_2015.reset_index()
dfNodes_2005 = dfNodes_2005.reset_index()
dfNodes_1995 = dfNodes_1995.reset_index()
# Graphen der Zeitabschnitte exportieren
nx.write_gexf(G_2015, f'{outputPath}/citation_network_2015.gexf') # Graphendatei
dfNodes_2015.to_csv(f'{outputPath}/citation_network_nodes_2015.csv', encoding='utf-8', index=False)
nx.write_gexf(G_2005, f'{outputPath}/citation_network_2005.gexf') # Graphendatei
dfNodes_2005.to_csv(f'{outputPath}/citation_network_nodes_2005.csv', encoding='utf-8', index=False)
nx.write_gexf(G_1995, f'{outputPath}/citation_network_1995.gexf') # Graphendatei
dfNodes_1995.to_csv(f'{outputPath}/citation_network_nodes_1995.csv', encoding='utf-8', index=False)

(Graphen-Datei von 2015 bis 2022 für die Visualisierung erstellen)

dfnodes = pd.read_csv(f'{outputPath}/citation_network_nodes.csv')
dfnodes_filter = dfnodes.loc[dfnodes['publication_year'] >= 2015]
node_attr = dfnodes_filter.set_index('label').to_dict('index')
g = nx.DiGraph() # Graphen erstellen
# Kanten als Liste von tuplen (Source, Target) einlesen
with open(f'{dataPath}/citation_network_EDGELIST_2015bis2022.csv', 'r') as edgecsv:
    edgereader = csv.reader(edgecsv, delimiter=';')
    edges = [tuple(e) for e in edgereader][1:]
# Kanten werden zu G hinzugefügt. 
g.add_edges_from(edges)
print(nx.info(g))
print(str(len(g.nodes)) + ' Knoten von insg. ' + str(len(nodes)) + ' Knoten sind Teil des Netzwerkes (' 
      + str( ((len(g.nodes))/(len(nodes)))*100 ) + '%).' )
nx.set_node_attributes(g, node_attr) # Attribute aus dem dict. anfügen
nx.write_gexf(g, f'{outputPath}/citation_network_2015bis2022.gexf') # Graphendatei exporieren