aperta.accessibility

Accessibility metrics computed against tiered OD tables.

Inputs are always:
  1. A cost TieredODPairs (subclass) — see “Key space” below.

  2. One or more pre-aggregated property weights, position-aligned with the cost ODM at each tier — a dict[name -> TieredODPairs].

  3. A cell_to_zone mapping that gives each cell-tier origin its parent zone-tier key (for stitching the zones_to_zones far tier — the cells_to_cells and cells_to_zones tiers are already cell-keyed and don’t need the indirection).

The per-origin stitching of cells_to_cells / cells_to_zones / zones_to_zones tiers is done once, then reused across every (parameter × property) combination — so adding more bins, decays, or k values is essentially free relative to a single-parameter call.

Key space — node-keyed vs geo-keyed.

Two valid input shapes, distinguished by the costs/weights subclass:

  • `TieredODNodePairs` (node-keyed): origins and dests are network node IDs. Each origin row in the output is a network node. Per-cell origin overhead cannot be applied here — the function returns per-NODE accessibilities. Build the cell_to_zone map from od_pairs.build_cell_to_zone_node_map(cells, zones, node_column) (cell-tier node → zone-tier node). Weights from od_pairs.dest_values (per-node sums).

  • `TieredODGeoPairs` (geo-keyed): origins and dests are geo-unit IDs (cells_to_cells → cell_id; zones_to_zones → zone_id; etc.). Each origin row in the output is a cell. Per-cell origin overhead should be baked into the ODM before calling this function via overhead.add_origin_cell_overhead. Build the cell_to_zone map directly: cells[‘zone_id’].to_dict(). Weights from od_pairs.dest_values_geo (per-cell direct lookup, no implicit summing).

The same three functions (cumulative_opportunities, gravity, nearest_k) accept either shape; output index name (‘node’ vs ‘cell’) reflects the input.

For cross-modal accessibility (“destinations within X min by ANY mode”, cross-modal logsum), combine per-mode TieredODGeoPairs cost ODMs with od_pairs.aggregate_across_modes before passing here. Node-keyed cross-modal is not supported — different modes live on different graphs.

For gravity in particular, the intrazonal-cost issue (cell-tier self-pairs route at cost 0, which sends exp(0)=1 to maximum weight) is addressed by calling routing.floor_intrazonal_costs on the cost ODM before passing here.

Provides:
  • Bin namedtuple — half-open [lo, hi) cost bin with a name.

  • Decay namedtuple — named callable for gravity-style cost decay.

  • exp_decay, power_decay — convenience constructors for common families.

  • cumulative_opportunities — sum each property’s weights over destinations within each cost bin (cumulative-opportunity accessibility).

  • gravity — sum each property’s weights weighted by f(cost), over all destinations, for one or more decay specs.

  • nearest_k — mean cost (or cost-at-k) to the k nearest weight-units, for one or more k values. Lower is better; canonical “mean travel time to the nearest k opportunities” formulation.

class aperta.accessibility.Bin(name, lo, hi)[source]

Bases: NamedTuple

Half-open cost bin: lo <= cost < hi. name labels the output column.

Bins should be mutually exclusive (the function does not check); a destination falling in multiple bins would be counted multiple times.

Parameters:
name: str

Alias for field number 0

lo: float

Alias for field number 1

hi: float

Alias for field number 2

class aperta.accessibility.Decay(name, fn)[source]

Bases: NamedTuple

Named cost-decay specification for gravity.

fn is a vectorised callable mapping a cost array to a weight array; name labels the corresponding output column. Use exp_decay / power_decay for the common families, or construct directly with any user-defined callable.

Multiple Decay specs can be passed to a single gravity call; the per-OD stitching is then amortised across all of them.

Parameters:
name: str

Alias for field number 0

fn: Callable[[ndarray], ndarray]

Alias for field number 1

aperta.accessibility.exp_decay(name, beta)[source]

Exponential decay: f(c) = exp(-beta * c). beta > 0 for sensible decay.

Parameters:
Return type:

Decay

aperta.accessibility.power_decay(name, beta)[source]

Power-law decay: f(c) = c ** (-beta). beta > 0; c = 0 yields inf, so callers should apply routing.add_intrazonal_cost first to replace self-pair cost-0 entries with a finite intrazonal cost.

Parameters:
Return type:

Decay

aperta.accessibility.cumulative_opportunities(costs, weights, cell_to_zone, bins)[source]

Sum each property’s weights over destinations whose cost falls in each bin.

Parameters:
  • costs (TieredODPairs) – tiered travel costs. Subclass determines output indexing: TieredODNodePairs → per-node output; TieredODGeoPairs → per-cell output. Non-finite entries (np.inf, np.nan) won’t match any finite bin and are silently dropped.

  • weights (dict[str, TieredODPairs]) – {property_name -> TieredODPairs}, position-aligned with costs per tier. Must share the costs’ key space (node-keyed weights for node-keyed costs; geo-keyed for geo-keyed). Build via od_pairs.dest_values (node-keyed) or od_pairs.dest_values_geo (geo-keyed). Missing origins / tiers contribute zeros, not errors.

  • cell_to_zone (dict) – {cell_tier_key -> zone_tier_key} map for tier stitching. Build from od_pairs.build_cell_to_zone_node_map (node-keyed: cell_node → zone_node) or directly from cells[‘zone_id’].to_dict() (geo-keyed: cell_id → zone_id).

  • bins (list[Bin]) – half-open [lo, hi) cost bins. Should be mutually exclusive (not checked).

Returns:

DataFrame indexed by origin key with (bin_name, property_name) MultiIndex on columns. Order: bins outer, properties inner. Dtype follows the input costs ODM (FP32 by default, FP64 if the caller opted in upstream).

Return type:

DataFrame

Per-cell overhead: for TieredODGeoPairs inputs, bake per-cell origin overhead into the cost ODM upfront via overhead.add_origin_cell_overhead.

aperta.accessibility.gravity(costs, weights, cell_to_zone, decays)[source]

Gravity-based accessibility: sum each property’s weights, weighted by f(cost), over all destinations — for one or more decay specs in a single call.

For each origin i, each property w, and each decay spec f:

A_i^{f,w} = Σ_j w_j · f(cost_ij)

Multiple decay specs share the per-OD stitching, so calling with a list of Decay specs is much cheaper than calling once per spec — useful for sensitivity analyses across decay-coefficient ranges.

For utility-based accessibility, pass the per-OD utility values as the cost ODM and an exponential decay with the desired scale (β=1 gives the standard Σ_j w_j · exp(-U_ij) form, on which logsum accessibility is ln(…) of the same sum).

Parameters:
  • costs (TieredODPairs) – see cumulative_opportunities.

  • weights (dict[str, TieredODPairs]) – see cumulative_opportunities.

  • cell_to_zone (dict) – see cumulative_opportunities.

  • decays (Decay | list[Decay]) – a single Decay or list of Decay specs. Output columns are MultiIndex (decay_name, property_name) with decay names outer.

Returns:

DataFrame indexed by origin key with MultiIndex columns (decay, property). Dtype follows the input costs ODM (FP32 by default, FP64 if the caller opted in upstream).

Return type:

DataFrame

aperta.accessibility.nearest_k(costs, weights, cell_to_zone, ks, *, aggregator='cost_mean')[source]

Nearest-k accessibility: cost (mean, or at-k) over the k nearest weight-units.

Each destination is treated as carrying weight_j opportunities at cost cost_ij. Destinations are sorted ascending by cost; the first k weight-units (with fractional contribution at the boundary) define the “nearest k opportunities”. The aggregator decides what to return:

  • `’cost_mean’` (default): the mean cost over the first k weight-units,

    A_i^{k,w} = cost_j · weight_j contributed, fractional at the boundary) / k. The canonical “mean travel cost to the nearest k opportunities” formulation — directly comparable across k values (k=3 and k=5 are on the same scale, in cost units).

  • `’cost_at_k’`: the cost of the k-th weight-unit — i.e., the cost

    at which the cumulative weight first reaches k. Answers “how far is the k-th nearest opportunity?”.

Both aggregators return a value in the same units as costs, with lower values = better accessibility. NaN is returned where the total available (finite-cost, positive-weight) opportunities at an origin is less than k — i.e., the k-th opportunity is unreachable in finite cost.

Multiple k values share the per-OD sort, so a multi-k call is much cheaper than k individual calls.

Parameters:
  • costs (TieredODPairs) – see cumulative_opportunities.

  • weights (dict[str, TieredODPairs]) – see cumulative_opportunities.

  • cell_to_zone (dict) – see cumulative_opportunities.

  • ks (int | float | list[int | float]) – a single k or list of k`s; positive values, integer or float. Output columns are MultiIndex `(k, property_name) with k outer.

  • aggregator (str) – ‘cost_mean’ (default) or ‘cost_at_k’.

Returns:

DataFrame indexed by origin key with MultiIndex columns (k, property). Dtype follows the input costs ODM (FP32 by default, FP64 if the caller opted in upstream). NaN where the k-th opportunity is unreachable.

Return type:

DataFrame

aperta.accessibility.flatten_index(df)[source]

Collapse a 2-level column MultiIndex into single strings joined by __.

Convenience for the accessibility outputs (which carry (bin, property) or (decay, property) MultiIndex columns) when downstream code prefers flat single-string column names (e.g. for CSV export). Mutates in place and also returns df.

Parameters:

df (DataFrame)

Return type:

DataFrame