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:
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.
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:
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:
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.
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_invarianttest_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), raisesValueError("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
NaNorInf, raisesValueError("Input contains non-finite values (NaN/Inf)").