Network examples¶
Use Mobility to obtain processed flows and Zones to understand their geographic IDs. The network helpers then let you:
Build a directed, weighted network from origin-destination (OD) rows.
Measure how much flow each zone sends and receives.
Compare observed days and calculate averages with explicit source coverage.
Aggregate zones using a verified territorial mapping.
The examples below use a small table and require no downloads. Run them in
order; later examples reuse od and network. They work with the base
installation. The network API reference documents
parameters and results for each function.
Build a network¶
Repeated observations for the same origin-destination pair are summed. Here A sends 3 + 2 = 5 trips to B, and B sends 4 trips to A.
import polars as pl
from pyspainmobility import build_network
od = pl.DataFrame({
"date": ["2024-01-01", "2024-01-01", "2024-01-03"],
"hour": [0, 1, 0],
"id_origin": ["A", "A", "B"],
"id_destination": ["B", "B", "A"],
"n_trips": [3.0, 2.0, 4.0],
"trips_total_length_km": [30.0, 20.0, 40.0],
})
network = build_network(od)
print(network.node_ids.tolist())
print(network.number_of_edges)
print(network.total_weight)
['A', 'B']
2
9.0
network.adjacencyis a SciPy sparse matrix: rows are origins and columns are destinations.network.node_idsidentifies their order.network.to_edge_table()returns origin, destination and weight columns as a Polars DataFrame.The default weight is
n_tripsand self-loops (flows within one zone) are retained.network.audit()reports construction rules and flow totals.A static network sums all supplied dates, hours and categories. Select the observations you need before building it.
For example, retain only hour 0 on 1 January:
from pyspainmobility import select_od
selected = select_od(od, filters={"date": "2024-01-01", "hour": [0]})
morning = build_network(selected)
print(morning.total_weight)
3.0
With downloaded data, pass the DataFrame returned by
mobility.get_od_data(return_df=True) or the path to its processed Parquet
file directly to build_network().
Measure flow by zone¶
Strength is the sum of incident edge weights. Because this example uses
n_trips, the values below count trips.
from pyspainmobility import node_strengths
strengths = node_strengths(network)
print(strengths.select("node_id", "out_strength", "in_strength").rows())
[('A', 5.0, 4.0), ('B', 4.0, 5.0)]
Column |
Meaning for a directed trip network |
|---|---|
|
Zone identifier. |
|
Sum of trips departing from the zone. |
|
Sum of trips arriving in the zone. |
|
Out-strength plus in-strength. An internal flow contributes to both. |
These are weighted flow totals. If you build a network with
NetworkSpec(weight="trips_total_length_km"), the strengths are total
trip-kilometres. For an undirected network, total_strength is the incident
weight, counting each self-loop once.
Compare observed days¶
An observed day with no retained flows and a missing source day have different meanings. Specify which days were observed when constructing temporal networks. Here, 1 and 3 January were observed; 2 January was missing.
from pyspainmobility import build_temporal_network
temporal = build_temporal_network(
od,
requested_dates=["2024-01-01", "2024-01-02", "2024-01-03"],
observed_dates=["2024-01-01", "2024-01-03"],
)
first = temporal.snapshot("2024-01-01")
last = temporal.snapshot("2024-01-03")
print(temporal.coverage.missing_source_dates)
print(temporal.sum_network().total_weight)
print(temporal.mean_per_observed_day().total_weight)
('2024-01-02',)
9.0
4.5
The mean is 9 trips divided by two observed days. A genuinely observed empty day would count in the denominator; the missing day does not.
For downloaded OD data, use the source manifest from Mobility:
from pyspainmobility import Mobility, build_temporal_network
mobility = Mobility(version=2, zones="municipalities", start_date="2024-01-01")
downloaded_od = mobility.get_od_data(return_df=True)
downloaded_temporal = build_temporal_network(
downloaded_od, acquisition_manifest=mobility.get_acquisition_manifest("Viajes")
)
Here downloaded_od is the DataFrame returned by that same Mobility
request. For version 1, the OD manifest name is "maestra1". Without a
manifest or observed_dates, coverage is inferred from data rows.
To inspect changes between the two example days:
from pyspainmobility import compare_networks, edge_changes
comparison = compare_networks(first, last)
print(comparison.added_edge_count, comparison.removed_edge_count)
print(edge_changes(first, last).select(
"id_origin", "id_destination", "delta_weight", "change"
).rows())
1 1
[('A', 'B', -5.0, 'removed'), ('B', 'A', 4.0, 'added')]
compare_networks()summarises shared, added and removed connections, similarity and changes in total flow.edge_changes()reports individual OD pairs, including unchanged pairs. Its delta is the second network’s weight minus the first network’s weight.destination_similarity()measures the similarity of each origin’s destination profile. See Flow measures and comparisons for definitions, output columns and empty-network conventions.Comparisons require the same node universe, direction, weight field and normalization. Temporal snapshots already share one node index.
Aggregate zones¶
A mapping must assign each source zone to exactly one target area. Mapping both A and B to P makes all nine trips internal to P:
from pyspainmobility import aggregate_od_network
grouped = aggregate_od_network(od, {"A": "P", "B": "P"})
print(grouped.to_edge_table().rows())
print(grouped.total_weight)
[('P', 'P', 9.0)]
9.0
Internal flows are kept as self-loops.
self_loops="drop"removes them and records the excluded weight ingrouped.audit().Use
aggregate_od_network()when the OD table is available, oraggregate_network()when you already have a sparse network.get_network_mapping()andget_province_mapping()provide checked territorial correspondences. Ambiguous or incomplete strict mappings raise.For province-level data and geometries, start with
Mobility(zones="provinces")andZones(zones="provinces"). Their Mobility and Zones pages explain derived provinces and excluded territories.
Choose node order and direction¶
For independently built networks, provide the same node index to preserve matrix order and include zones with no observed flows:
from pyspainmobility import NodeIndex, NetworkSpec, symmetrize_network
nodes = NodeIndex(["A", "B", "C"], zoning_id="example", zoning_version="v1")
ordered = build_network(od, node_index=nodes)
print(ordered.node_ids.tolist())
print(node_strengths(ordered).filter(pl.col("node_id") == "C").rows())
['A', 'B', 'C']
[('C', 0.0, 0.0, 0.0)]
NetworkSpec(self_loops="drop")excludes internal flows during construction.symmetrize_network(network, method="sum")combines both directions into one undirected connection. Other rules aremean,maxandmutual; their definitions are in Network API.External algorithm adapters connect these results to NetworkX and Infomap. The graph algorithms and community detection are supplied by those projects.