NetworkX Backend¶
NetworkX provides in-memory graph analysis without requiring an external database. Ideal for prototyping, local analysis, and integration with Python data science tools.
Dependencies¶
NetworkX is an optional extra:
pip install "ddigraph[networkx]"
For visualization:
pip install matplotlib # basic plots
pip install pyvis # interactive HTML visualization
Basic Usage¶
import os
from ddigraph.backends.networkx import to_networkx
G = to_networkx(os.environ["FIXTURE"])
print(f"Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges")
The source can be DDI XML of any flavor, or an RDF export such as
survey.ttl. You can also pass chunks you already have from
ddigraph.iter_graph().
What Is in the Graph¶
G is a MultiDiGraph. It is directed, because DDI links are. It is
multi, because two nodes can be joined by more than one kind of link.
- Node ids look like
"Variable:v1": the node's type, then its key. The type is part of the id because a key is only unique within its type. A codebook may usec1for a category and for a concept. - Node attributes are
node_type,node_key, the identity fields and every property that has a value. The type is callednode_type, notlabel, because most DDI records have alabelof their own: the human-readable one. - Edges carry
relationship, their type. The edge key is the type too, so loading the same file twice does not double any edge.
import os
from collections import Counter
from ddigraph.backends.networkx import to_networkx
G = to_networkx(os.environ["FIXTURE"])
types = Counter(data["node_type"] for _, data in G.nodes(data=True))
for node_type, count in types.most_common():
print(f"{node_type}: {count}")
Several Files, One Graph¶
Pass graph= to add to a graph you already have. Nodes the files share
merge into one:
G = to_networkx("wave1.xml")
G = to_networkx("wave2.xml", graph=G)
Graph Analysis¶
Basic Statistics¶
# Graph info
print(f"{G.number_of_nodes()} nodes, {G.number_of_edges()} edges")
# Connected components (for undirected view)
undirected = G.to_undirected()
components = list(nx.connected_components(undirected))
print(f"Connected components: {len(components)}")
# Density
print(f"Density: {nx.density(G):.4f}")
Centrality Metrics¶
# Degree centrality
degree_cent = nx.degree_centrality(G)
# Betweenness centrality
betweenness = nx.betweenness_centrality(G)
# PageRank
pagerank = nx.pagerank(G)
# Find most important nodes
important = sorted(pagerank.items(), key=lambda x: x[1], reverse=True)[:10]
for node_id, score in important:
print(f"{G.nodes[node_id]['node_type']}: {score:.4f}")
Path Analysis¶
# Find all paths between nodes
instrument_nodes = [n for n, d in G.nodes(data=True) if d.get("node_type") == "Instrument"]
question_nodes = [n for n, d in G.nodes(data=True) if d.get("node_type") == "QuestionItem"]
if instrument_nodes and question_nodes:
paths = list(nx.all_simple_paths(G, instrument_nodes[0], question_nodes[0], cutoff=10))
print(f"Found {len(paths)} paths")
# Shortest path
if nx.has_path(G, instrument_nodes[0], question_nodes[0]):
path = nx.shortest_path(G, instrument_nodes[0], question_nodes[0])
print(f"Shortest path: {' -> '.join(path)}")
Subgraph Extraction¶
# Extract subgraph by node type
question_items = [n for n, d in G.nodes(data=True) if d.get("node_type") == "QuestionItem"]
code_lists = [n for n, d in G.nodes(data=True) if d.get("node_type") == "CodeList"]
categories = [n for n, d in G.nodes(data=True) if d.get("node_type") == "Category"]
subgraph_nodes = set(question_items + code_lists + categories)
subgraph = G.subgraph(subgraph_nodes)
print(f"Subgraph: {subgraph.number_of_nodes()} nodes")
# Extract ego network (neighbors of a node)
ego = nx.ego_graph(G, instrument_nodes[0], radius=2)
print(f"Ego network: {ego.number_of_nodes()} nodes")
Visualization¶
Matplotlib¶
import matplotlib.pyplot as plt
# Simple layout
pos = nx.spring_layout(G, k=2, iterations=50)
# Color by node type
color_map = {
"Instrument": "red",
"Sequence": "blue",
"QuestionConstruct": "green",
"QuestionItem": "orange",
"CodeList": "purple",
"Category": "yellow",
}
colors = [color_map.get(G.nodes[n].get("node_type", ""), "gray") for n in G.nodes()]
plt.figure(figsize=(16, 12))
nx.draw(G, pos, node_color=colors, node_size=50, with_labels=False, alpha=0.7)
plt.savefig("ddi_graph.png", dpi=150)
plt.show()
PyVis (Interactive HTML)¶
from pyvis.network import Network
net = Network(height="800px", width="100%", directed=True)
# Add nodes with colors
for node_id, data in G.nodes(data=True):
node_type = data.get("node_type", "Unknown")
label = data.get("label", node_id)[:30]
color = color_map.get(node_type, "gray")
net.add_node(node_id, label=label, color=color, title=f"{node_type}: {label}")
# Add edges
for source, target, data in G.edges(data=True):
rel = data.get("relationship", "")
net.add_edge(source, target, title=rel)
net.show("ddi_interactive.html")
Export Formats¶
GraphML and GEXF cannot store lists, and some DDI properties are lists.
Build the graph with flatten_lists=True to join them with | first:
import os
import networkx as nx
from ddigraph.backends.networkx import to_networkx
G = to_networkx(os.environ["FIXTURE"], flatten_lists=True)
nx.write_graphml(G, "survey.graphml")
nx.write_gexf(G, "survey.gexf") # for Gephi
Node-link JSON keeps lists as they are:
import json
with open("graph.json", "w") as f:
json.dump(nx.node_link_data(G, edges="edges"), f, indent=2)
As Tables Instead¶
For tables, use the pandas backend. It reads the same file into one DataFrame of nodes and one of relationships.
Memory¶
The whole graph lives in memory, attributes included. If you only need the structure, drop the attributes you will not use:
for _, data in G.nodes(data=True):
for key in [k for k in data if k not in ("node_type", "node_key")]:
del data[key]
See Also¶
- pandas - The same graph, as two tables
- Gremlin - The same graph, in a database
- NetworkX Documentation