Composite Accessibility Scoring
The moveq_core.score module implements multi-indicator composite index evaluation on a normalized 0.0 – 100.0 scale with dynamic weight renormalization and explicit audit trail tracking.
Mathematical Formulation
Given a dictionary of indicator terms \(t_i \in [0.0, 1.0]\) and design weights \(w_i > 0\), the composite score is evaluated as:
\text{Score} = 100 \times \sum_{i \in \text{Present}} t_i \cdot w_i^* \quad \text{where} \quad w_i^* = \frac{w_i}{\sum_{j \in \text{Present}} w_j}
The Missing Indicator Problem
In urban data science, datasets collected across different transit authorities or historical periods often have missing fields (e.g. weekend night bus frequencies or low-floor bus counts might not be available in rural districts).
The Danger of Silent Zero Padding
Traditional scoring software replaces missing values with
0. If a neighborhood lacks night bus data, filling with 0 artificially collapses its overall accessibility score, penalizing the agency unfairly for missing telemetry.
moveq drops missing terms and renormalizes the weights of present terms, generating a clear audit note explaining which terms were omitted.
Python Code Example
composite_score.py
Python 3.10+
from moveq_core.score import compute_score
result = compute_score(
terms={
"buffer_400m": 0.85, # 85% coverage
"evening_freq": 0.60, # 60% standard
"frequency": None, # Missing in this dataset cut
},
weights={
"buffer_400m": 0.50,
"evening_freq": 0.30,
"frequency": 0.20,
},
labels={
"buffer_400m": "400m Buffer Coverage",
"evening_freq": "Evening Service Frequency",
"frequency": "Peak Frequency",
},
n_areas=420,
context={"city": "Manchester", "year": 2026}
)
print(f"Final Score: {result.score:.1f}")
# Output: Final Score: 75.6
print(f"Audit Note: {result.note}")
# Output: Audit Note: peak frequency not in this cut — weights renormalised.
# Export full dictionary payload for JSON APIs
import json
print(json.dumps(result.to_dict(), indent=2))
ScoreResult and ScoreComponent
compute_score returns an immutable, type-annotated ScoreResult dataclass containing full breakdown metadata:
| Field | Type | Description |
|---|---|---|
| score | float | None | Overall composite score in [0.0, 100.0] rounded to 1 decimal place. If all terms are None, returns None. |
| components | list[ScoreComponent] | Detailed breakdown per term: key, label, value, design_weight, weight_used, is_missing. |
| note | str | Human-readable audit note describing dropped terms or empty inputs. |
| n_areas | int | None | Optional count of spatial units in the evaluation cohort. |
Validation & Invariants
- Unit Interval Values: Term values \(t_i\) must be in \([0.0, 1.0]\). Values outside this interval are safely clipped to \([0.0, 1.0]\).
- Weight Positivity: Weights must be strictly positive (\(w_i > 0\)). If any weight is \(\le 0\), raises
ValueError. - Non-Finite Weights: If any weight is
NaNorInf, raisesValueError. - All Terms Missing: If every term in
termsisNone,result.scoreevaluates toNonewith note"No indicators available in this cut."