Network API

These helpers analyse OD data obtained through Mobility. Use Zones for geographic context and territorial mappings. For runnable examples, start with Network examples.

  • build_network() constructs a directed network from processed OD rows.

  • NetworkSpec selects the weight column and self-loop policy.

  • SparseMobilityNetwork holds the sparse matrix, zone IDs and flow audit.

  • NodeIndex preserves node order across independently built networks.

  • symmetrize_network() combines directions using an explicit weight rule.

Build a network

pyspainmobility.network.builder.build_network(data: str | Path | DataFrame | DataFrame | LazyFrame, *, spec: NetworkSpec | None = None, node_ids: Sequence[object] | None = None, node_index: NodeIndex | None = None, provenance: Mapping[str, object] | None = None) → SparseMobilityNetwork[source]

Build a directed, weighted CSR network from processed OD observations.

Repeated OD rows, including observations from different dates, hours or categories, are summed for each origin-destination pair. Select the rows needed for your analysis before calling this function.

Parameters:
  • data (str, pathlib.Path, pandas.DataFrame, polars.DataFrame or polars.LazyFrame) – A processed OD Parquet path, a pandas/Polars DataFrame, or a Polars LazyFrame. It must contain the columns named in spec.

  • spec (NetworkSpec, optional) – Weight column and self-loop policy. Defaults to directed sum aggregation with weight='n_trips' and self_loops='keep'.

  • node_ids (sequence, optional) – Optional complete, ordered node universe. It preserves the same row and column positions across independently built networks and includes nodes with no observed flows. With None, observed endpoints are sorted lexicographically to create a deterministic index.

  • node_index (NodeIndex, optional) – Optional NodeIndex replacing node_ids. It associates the matrix with a zoning identifier/version and must not be combined with node_ids.

  • provenance (mapping, optional) – Optional audit context carried into the immutable network result.

Returns:

Canonical CSR matrix, node IDs, and an audit trail. Missing or non-finite OD values are rejected rather than silently becoming a missing flow.

Return type:

SparseMobilityNetwork

Examples

>>> import polars as pl
>>> from pyspainmobility import build_network
>>> od = pl.DataFrame({
...     "id_origin": ["A", "A"], "id_destination": ["B", "B"],
...     "n_trips": [3.0, 2.0],
... })
>>> network = build_network(od)
>>> network.total_weight
5.0
>>> network.to_edge_table().rows()
[('A', 'B', 5.0)]

Construction options

class pyspainmobility.network.model.NetworkSpec(origin: str = 'id_origin', destination: str = 'id_destination', weight: str = 'n_trips', directed: bool = True, aggregation: str = 'sum', self_loops: str = 'keep')[source]

Rules used to turn an OD table into a network.

Pass these options to build_network() or build_temporal_network().

Parameters:
  • origin (str) – Origin ID column. Default is 'id_origin'.

  • destination (str) – Destination ID column. Default is 'id_destination'.

  • weight (str) – Additive flow column. Default is 'n_trips'. For total trip-kilometres, use 'trips_total_length_km'.

  • directed (bool) – Must be True. After construction, use symmetrize_network() to combine directions with an explicit rule.

  • aggregation (str) – Must be 'sum'; repeated OD observations are added together.

  • self_loops ({'keep', 'drop'}) – Retain or exclude flows within the same zone. Default is 'keep'.

Examples

>>> from pyspainmobility import NetworkSpec
>>> spec = NetworkSpec(weight="n_trips", self_loops="drop")
>>> spec.self_loops
'drop'

Network result

class pyspainmobility.network.model.SparseMobilityNetwork(adjacency: csr_array, node_ids: ndarray, metadata: NetworkMetadata, provenance: Mapping[str, object] | None = None, node_index: NodeIndex | None = None)[source]

A weighted mobility network with an immutable node contract.

node_ids[i] is the zone represented by row and column i in adjacency. The IDs are sorted lexicographically when inferred from the OD data, or retain the caller-provided order when node_ids is supplied to build_network().

Variables:
  • adjacency (scipy.sparse.csr_array) – Read-only matrix of edge weights, with origins as rows and destinations as columns. Keep this representation sparse for large networks.

  • node_ids (numpy.ndarray) – Zone IDs in matrix order.

  • metadata (NetworkMetadata) – Weight field, construction rules and represented/excluded flow totals.

  • provenance (mapping) – Immutable context and transformation history; audit() returns a serializable description.

  • node_index (NodeIndex) – Shared node order and optional zoning identity.

property number_of_nodes: int

Number of nodes, including explicitly supplied isolates.

property number_of_edges: int

Number of logical non-zero edges under the direction contract.

property total_weight: float

Total logical edge weight without double-counting undirected pairs.

to_edge_table(origin: str = 'id_origin', destination: str = 'id_destination', weight: str = 'weight', *, sort: bool = True) → DataFrame[source]

Return one row per non-zero connection as a Polars DataFrame.

Parameters:
  • origin (str) – Output column names. Defaults are 'id_origin', 'id_destination' and 'weight'; names must be distinct.

  • destination (str) – Output column names. Defaults are 'id_origin', 'id_destination' and 'weight'; names must be distinct.

  • weight (str) – Output column names. Defaults are 'id_origin', 'id_destination' and 'weight'; names must be distinct.

  • sort (bool) – Sort by origin and destination IDs. Default is True.

Returns:

Zone IDs and edge weights. Undirected pairs appear once, with endpoints ordered by their matrix positions.

Return type:

polars.DataFrame

audit() → Dict[str, object][source]

Return flow accounting together with matrix dimensions.

node_positions() → Dict[str, int][source]

Return the immutable node-ID-to-matrix-index mapping.

align_to(target: SparseMobilityNetwork | NodeIndex) → SparseMobilityNetwork[source]

Return this network reordered to a compatible target node index.

Shared node order

class pyspainmobility.network.model.NodeIndex(node_ids: Sequence[object], zoning_id: str | None = None, zoning_version: str | None = None)[source]

The scientific identity and order of a network’s matrix nodes.

node_ids[i] identifies row and column i. Zoning metadata is optional for backwards compatibility, but when provided it is checked before aligning or comparing networks.

Parameters:
  • node_ids (sequence) – Unique, non-missing zone IDs in the desired matrix order. IDs are converted to strings; include zones with no flows to retain isolates.

  • zoning_id (str, optional) – Name identifying the zoning system used by these IDs.

  • zoning_version (str, optional) – Version of that zoning system.

Examples

>>> from pyspainmobility import NodeIndex
>>> nodes = NodeIndex(["A", "B", "C"], zoning_id="example", zoning_version="v1")
>>> nodes.positions()["B"]
1
positions() → Dict[str, int][source]

Map node ID to the corresponding matrix row/column position.

assert_compatible(other: NodeIndex) → None[source]

Require equal zoning identity and exactly the same ordered nodes.

permutation_to(target: NodeIndex) → ndarray[source]

Return source positions ordered as target after compatibility checks.

describe() → Dict[str, object][source]

Return serialisable node-index identity metadata.

Combine flow directions

pyspainmobility.network.transforms.symmetrize_network(network: SparseMobilityNetwork, *, method: Literal['sum', 'mean', 'max', 'mutual'] = 'sum') → SparseMobilityNetwork[source]

Turn a directed OD network into an auditable undirected network.

The returned adjacency is symmetric, with one logical undirected edge per unordered node pair. The explicit rule determines its edge weight:

  • sum: total bilateral OD flow, A[i, j] + A[j, i];

  • mean: mean bilateral OD flow, treating a missing direction as zero;

  • max: strongest direction;

  • mutual: weakest direction, removing non-reciprocal pairs.

Self-loops are preserved once under every rule. Provenance records both logical edge weight and stored symmetric-matrix weight, avoiding accidental double counting of off-diagonal edges.

Parameters:
  • network (SparseMobilityNetwork) – Directed network to transform.

  • method ({'sum', 'mean', 'max', 'mutual'}) – Rule for combining the two directions, as defined above. Default is 'sum'.

Returns:

Undirected network with the same node index. total_weight and to_edge_table() count each undirected connection once.

Return type:

SparseMobilityNetwork

Examples

>>> import polars as pl
>>> from pyspainmobility import build_network, symmetrize_network
>>> od = pl.DataFrame({
...     "id_origin": ["A", "B"], "id_destination": ["B", "A"],
...     "n_trips": [3.0, 2.0],
... })
>>> symmetrize_network(build_network(od), method="sum").to_edge_table().rows()
[('A', 'B', 5.0)]