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

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 NaN or Inf, raises ValueError.
  • All Terms Missing: If every term in terms is None, result.score evaluates to None with note "No indicators available in this cut."