Overview G Core Inequality S Composite Scoring H Harmonization DataFrames CLI Reference
Docs / Core Engines / Core Inequality

Core Inequality Engine

The moveq_core.equity module provides pure NumPy implementations of the standard mathematical metrics used in spatial equity and environmental justice analysis.

1. Population-Weighted Gini (compute_gini)

The Gini Coefficient measures the overall dispersion of transit service across a population. It is mathematically defined as twice the area between the 45-degree line of perfect equality and the cumulative population-service Lorenz curve:

G = 1 - 2 \int_0^1 L(p) \, dp = 1 - \sum_{i=1}^n (p_i - p_{i-1})(S_i + S_{i-1})

Where \(p_i\) is the cumulative population fraction and \(S_i\) is the cumulative service fraction after ordering spatial units by service level.

Function Signature

def compute_gini(
    values: np.ndarray | Sequence[float],
    weights: np.ndarray | Sequence[float]
) -> float

Parameters

Parameter Type Description
values np.ndarray Required 1D array of service levels per areal zone (e.g. trips/hour, departures, jobs accessible within 30 min). Values must be non-negative.
weights np.ndarray Required 1D array of population weights per zone. Must have the same length as values. Weights must be non-negative and sum to \(> 0\).

Returns

A float in the interval [0.0, 1.0]. Returns 0.0 when service is perfectly uniform across all residents, and approaches 1.0 under extreme concentration.

Zero Total Service Convention
If every zone in the city has 0 service (total service is 0), compute_gini returns 0.0 by convention, avoiding divide-by-zero errors.

2. Continuous Boundary Palma Ratio (compute_palma_ratio)

The Palma Ratio focuses on the extremes of the transit distribution: the ratio of transit service captured by the top 10% most-served population divided by the service captured by the bottom 40% least-served population:

\text{Palma Ratio} = \frac{\text{Mean Service of Top 10\% Population}}{\text{Mean Service of Bottom 40\% Population}} = \frac{S_{90-100} / 0.10}{S_{0-40} / 0.40}
Why Continuous Boundary Splitting Matters
In real-world spatial planning, geographic census zones (LSOAs, Tracts, IRIS units) rarely align exactly on the 40th or 90th cumulative population percentiles. Standard quantile algorithms (like Pandas pd.qcut) allocate the entire boundary polygon to one bucket, causing erratic jumps depending on polygon sort order.

moveq uses continuous proportional boundary splitting: any zone straddling the 40% or 90% threshold has its population and service partitioned proportionally across both tiers.

Function Signature

def compute_palma_ratio(
    values: np.ndarray | Sequence[float],
    weights: np.ndarray | Sequence[float]
) -> float

3. Wagstaff Concentration Index (compute_concentration_index)

The Concentration Index (CI) measures whether transit service is disproportionately allocated toward socioeconomically advantaged or disadvantaged populations. It plots the cumulative share of transit service against the cumulative population ranked by socioeconomic status:

CI = \frac{2}{\mu} \operatorname{Cov}_w(y_i, R_i)

Where \(\mu\) is the weighted mean service, \(y_i\) is service in zone \(i\), and \(R_i\) is the fractional socioeconomic rank in \([0, 1]\).

  • \(CI < 0\) (Negative): Pro-Poor distribution. Service is concentrated in more deprived areas (desirable for equity interventions).
  • \(CI = 0\) (Zero): Neutral distribution. Transit service has zero correlation with neighborhood deprivation.
  • \(CI > 0\) (Positive): Pro-Rich distribution. Higher transit service is concentrated in less deprived, affluent areas.
Tied Rank Group-Averaging
When multiple zones share the same deprivation decile or rank (tied ranks), standard ranking can introduce sorting bias. moveq uses vectorized group-averaging (np.add.at) so all tied units receive the exact centroid fractional rank, ensuring 100% deterministic results regardless of row order.

Function Signature

def compute_concentration_index(
    service: np.ndarray | Sequence[float],
    rank: np.ndarray | Sequence[float],
    population: np.ndarray | Sequence[float]
) -> float

Mathematical Invariants & Guarantees

The moveq-core engine guarantees the following properties across its 50-test formal verification suite:

Invariant Property Mathematical Statement Test Proof
Permutation / Order Invariance \(f(\mathbf{v}_\pi, \mathbf{w}_\pi) = f(\mathbf{v}, \mathbf{w})\) for any permutation \(\pi\) test_gini_is_order_invariant
test_palma_order_invariant_for_tied_values
Scale Invariance \(f(\alpha \mathbf{v}, \beta \mathbf{w}) = f(\mathbf{v}, \mathbf{w})\) for \(\alpha, \beta > 0\) test_palma_ratio_equal_service_is_one
Tied Rank Symmetry Identical rank allocations receive centroid fractional ranks test_concentration_index_is_order_invariant_for_tied_ranks
Extreme Bounds \(0.0 \le G \le 1.0\), \(-1.0 \le CI \le 1.0\) test_gini_extreme_inequality_approaches_one

Validation & Error Handling

moveq-core fails early with descriptive Python exceptions if corrupted data is supplied:

  • Length Mismatch: If len(values) != len(weights), raises ValueError("values and weights must have the same length").
  • Negative Weights: If any weight is \(< 0\), raises ValueError("Weights must be non-negative").
  • All Zero Weights: If \(\sum w_i = 0\), raises ValueError("Total weight must be positive").
  • Negative Service Values: If any service value is \(< 0\), raises ValueError("Values must be non-negative").
  • Non-Finite Inputs: If any input contains NaN or Inf, raises ValueError("Input contains non-finite values (NaN/Inf)").