Skip to content

Commit eefabe5

Browse files
committed
feat(scoring): replace weighted Urban95 scores with categorical status averages
- Remove weights and numeric total scores from Urban95 calculations - Classify indicators as Disappointing, Functioning, or Thriving - Infer continuous building and neighborhood status surfaces - Redesign legends, explainers, and comparisons around stoplight statuses
1 parent 6a0e192 commit eefabe5

76 files changed

Lines changed: 4941 additions & 2585 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ This project maps how easy it is to reach everyday services and child-relevant f
99
## What you get
1010

1111
- **Interactive map** (`docs/`) — MapLibre GL, with optional deck.gl clusters for dense points and Chart.js for dashboards.
12-
- **Two scoring models****Urban95** uses the weighted Urban95 methodology (0-100 score); **Amenities Focus** (internally `expanded`) uses a broader amenity-count classification.
13-
- **Three scales** — Single building (with walking-time area), neighborhood comparison, and citywide summary with rankings.
12+
- **Two scoring models****Urban95** uses equal-mean traffic-light statuses; **Amenities Focus** (internally `expanded`) uses a broader amenity-count classification.
13+
- **Three scales** — Single building (with walking-time area), neighborhood comparison, and citywide summary.
1414

1515
---
1616

@@ -36,53 +36,33 @@ Processed layers for the website live under **`docs/data/`** (GeoJSON plus `neig
3636

3737
Typical files include building footprints with accessibility fields, amenity layers (`amenities_new.geojson`, `amenities_all.geojson`), trees, parks, street lights, precomputed walking isochrones, and neighborhood boundaries. Exact filenames must match `docs/app.js`.
3838

39-
### Building score fields used by the app
39+
### Building fields used by the app
4040

41-
- **Urban95 top-level category scores**: `score_weighted_environmental_quality_*`, `score_weighted_nature_*`, `score_weighted_play_*`, `score_weighted_safety_mobility_*`, `score_weighted_family_services_*`.
42-
- **Urban95 subcategory scores**: `score_weighted_sub_<category>_<subcategory>_*` (generated by `python -m pipeline run score` via `stages/urban95_scoring.py` from `lib/urban95_weights.py`).
41+
- **Urban95 overview status**: `u95_status_*`.
42+
- **Urban95 category status**: `u95_status_<category>_*`.
43+
- **Urban95 direct indicator status**: `u95_status_sub_<category>_<indicator>_*`.
44+
- **Urban95 diagnostic status**: `u95_status_detail_<category>_<parent>_<child>_*`. School and Kindergarten are Education diagnostics; Clinic and Tipat Halav are Health diagnostics. They are visible in drill-downs but excluded from means.
4345
- **Amenities Focus score**: `score_expanded_*`.
4446

45-
(`*` is one of `_5min`, `_10min`, `_15min`.)
47+
`*` is one of `_5min`, `_10min`, `_15min`; the frontend currently consumes `_10min`. Urban95 publishes status strings, raw explanation measurements such as `summer_si`, and no numeric overview total.
4648

4749
---
4850

4951
## Scoring methodology
5052

51-
### Urban95 (displayed as direct 0-100 score)
52-
53-
Urban95 total score is a weighted sum of five category scores. It is a methodology-driven score: category and subcategory definitions are fixed, and score logic is not directly tied to amenity filter categories in the UI.
54-
55-
- Environmental Quality: **20%**
56-
- Nature: **15%**
57-
- Play: **15%**
58-
- Safety & Mobility: **25%**
59-
- Family Services: **25%**
60-
61-
Each category is itself built from subcategory weights in `lib/urban95_weights.py`:
62-
63-
- **Environmental Quality**
64-
- Shade: 40% — Beer Sheva BDAR **Spatial Shade Index (`summer_SI`)** from the [Derech Tzel shading metrics guide](https://tzel.org.il/wp-content/uploads/2025/08/Shade-Indicators_eng-2.6.pdf). Values are used as-is (**SI, not SAI**; not recalculated). Each building gets a **300 m area-weighted mean `summer_SI` around the building footprint (near-edge buffer)**, then stored/displayed `summer_si` is **rounded to 1 decimal place with standard half-up ties before output and scoring** (`0.15 → 0.2`, `0.35 → 0.4`). The map shows continuous 0–1 SI using the official interpretation buckets (`<0.10 severe lack`, `0.10–<0.20 significant lack`, `0.20–<0.40 needs improvement`, `0.40–<0.60 good shade`, `≥0.60 excellent shade`). The Urban95 shade sub-score keeps a project-specific ternary mapping on that **rounded** building SI: `<0.20 → 0`, `0.20–<0.40 → 50`, `≥0.40 → 100`.
65-
- Trees: 20% — count of trees within **20 m of the building footprint edge** (0 / 1–2 / ≥3 → 0 / 50 / 100)
66-
- Distance from fast roads: 40% — nearest fast road distance measured to the **building footprint edge**
67-
- **Nature**
68-
- Parks: 50%
69-
- Urban nature areas: 50% (any site polygon within 300 m)
70-
- **Play**
71-
- Playgrounds: 100%
72-
- **Safety & Mobility**
73-
- Street lights: 15%
74-
- Bicycle access: 15%
75-
- Bus stops: 30%
76-
- Shelters: 40%
77-
- **Family Services**
78-
- Education: 30%
79-
- Community centers: 20%
80-
- Business centers: 20%
81-
- Health: 30%
82-
83-
`Explain score` in the app uses these saved top-level and subcategory score columns so each component shows both its weight and its score.
84-
85-
Urban95 columns are stored per `_5min/_10min/_15min` for consistency with the rest of the data model, but Urban95 logic itself is mostly based on fixed-distance **near-edge** rules (buffers/distances from the building footprint) inside category functions in `lib/urban95_weights.py` rather than UI amenity filters. Amenities Focus walking isochrones still originate from building centroids.
53+
### Urban95 (equal-mean traffic-light statuses)
54+
55+
Urban95 preserves the existing spatial indicator rules but presents every public Urban95 result as **Disappointing**, **Functioning**, **Thriving**, or **Unknown**. It does not publish, display, rank, or reconstruct a numeric Urban95 overview total.
56+
57+
The hierarchy is Environmental Quality (Shade, Trees, Distance from fast roads), Nature (Parks, Urban nature areas), Play (Playgrounds), Safety & Mobility (Street lights, Bicycle access, Bus stops, Shelters), and Family Services (Education, Community centers, Business centers, Health). Direct indicators retain their project rules and internal 0/0.5/1 attainment levels. Sibling direct indicators contribute equally to their category; the five categories contribute equally to the overview. Thus categories with more indicators do not have extra influence.
58+
59+
Category and overview means use the following status cutoffs: attainment below `0.25` is Disappointing, `0.25` through below `0.75` is Functioning, and `0.75` through `1.00` is Thriving. Exact midpoint ties classify upward. If any required direct child is Unknown, its category is Unknown; if any category is Unknown, the overview is Unknown. Unknown is never replaced with zero or omitted from a denominator.
60+
61+
Missing, unreadable, schema-invalid, or failed source evidence produces Unknown for dependent results. A valid empty source or valid empty subtype remains available and follows the normal indicator rule. Per-building calculation failures produce Unknown only for that building. Area summaries count all four statuses and use the uniquely predominant building status only; ties and unsupported areas publish Unknown with a summary reason rather than a hidden average.
62+
63+
**Environmental Quality / Shade** — Beer Sheva BDAR **Spatial Shade Index (`summer_SI`)** comes from the [Derech Tzel shading metrics guide](https://tzel.org.il/wp-content/uploads/2025/08/Shade-Indicators_eng-2.6.pdf). Values are used as-is (**SI, not SAI**; not recalculated). Each building gets a **300 m area-weighted mean `summer_SI` around the building footprint (near-edge buffer)**, then stored/displayed `summer_si` is **rounded to 1 decimal place with standard half-up ties before output and status calculation** (`0.15 → 0.2`, `0.35 → 0.4`). The separate official SI interpretation remains `<0.10 severe lack`, `0.10–<0.20 significant lack`, `0.20–<0.40 needs improvement`, `0.40–<0.60 good shade`, and `≥0.60 excellent shade`. Urban95's status rule is project-defined: rounded SI `<0.20`, `0.20–<0.40`, and `≥0.40` map to low, middle, and high attainment respectively.
64+
65+
Urban95 status columns retain `_5min/_10min/_15min` for compatibility even though its direct rules are generally fixed-distance **near-edge** rules from building footprints. Amenities Focus walking isochrones still originate from building centroids.
8666

8767
### Amenities Focus (internally `expanded`)
8868

@@ -153,7 +133,7 @@ The commands and browser-facing output contracts above are unchanged. In `run al
153133

154134
For acceptance-only warm-run checks, set `PIPELINE_FORBID_MAPBOX=1`. A guarded run validates the complete `(building_id, minutes)` warm aggregate and aborts before token/session/network work when it is incomplete; ordinary unguarded runs retain their existing Mapbox/cache behavior.
155135

156-
To recompute Urban95 weighted columns (including shade SI) on existing buildings without Mapbox or isochrones:
136+
To recompute Urban95 status fields (including shade SI) on existing buildings without Mapbox or isochrones:
157137

158138
```powershell
159139
$env:PYTHONPATH="src"
@@ -168,7 +148,7 @@ You can refresh **one concern** without Mapbox or a full rebuild — but only if
168148

169149
| You changed… | Run (no Mapbox unless noted) | Must refresh for the live app |
170150
|--------------|------------------------------|-------------------------------|
171-
| Urban95 weights / shade SI mapping | `run shade` (if SI inputs changed) → `run rescore``run neighborhoods` | `buildings_lookup` (+ gz), buildings `.gz`, neighborhoods / charts / citywide |
151+
| Urban95 status rules / shade SI mapping | `run shade` (if SI inputs changed) → `run rescore``run neighborhoods` | `buildings_lookup` (+ gz), buildings `.gz`, neighborhoods / charts / citywide |
172152
| Clean amenities / trees / lights inventory (raw) | `run amenity_metrics``run score``run export_web``run neighborhoods` | Same publish set + `amenities_new` / trees / lights sync; **isochrones reused from cache** |
173153
| Isochrone geometry / walk sheds | `run isochrones` (**Mapbox** if cache cold) → `run amenity_metrics``run score``run export_web``run neighborhoods` | Isochrones + lookup + aggregates |
174154
| Neighborhood chart logic only | `run neighborhoods` | neighborhoods / surface / charts / citywide (buildings unchanged) |

docs/app.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,17 @@ const {
4545
Urban95PointDataSources,
4646
urban95RuntimeLoaders,
4747
Urban95ScoreModel,
48+
Urban95StatusScale,
4849
AMENITY_TYPE_CONFIG,
4950
DEFAULT_CONFIG,
5051
WEIGHTED_CATEGORY_LABEL_BY_STEM,
5152
getAmenityConfig,
5253
amenityTypeToBuildingStatKey,
5354
percentileBreakpoints,
54-
buildHistogramDistributionFromScores,
5555
getColorForValue,
5656
bulkPercentileRanks,
5757
formatMetricNumber,
5858
formatScoreInteger,
59-
weightedCategoryHighlightsFromSource,
60-
weightedSubcategoryComparisonRows,
6159
Urban95WeightedMetricShowRegistry,
6260
Urban95ScoreContext,
6361
Urban95ScoreExplain,
@@ -70,6 +68,7 @@ const {
7068
Urban95NeighborhoodScores,
7169
Urban95RenderState,
7270
resolveBuildingContracts,
71+
createBuildingFillColorExpression,
7372
createPmtilesProtocol,
7473
createBuildingsSource,
7574
createBuildingsFillLayer,
@@ -134,8 +133,10 @@ const BUILDINGS_FILL_LAYER_ID = BUILDING_LAYER_CONTRACTS.fillLayerId;
134133
const BUILDINGS_SELECTED_LAYER_ID = BUILDING_LAYER_CONTRACTS.selectedLayerId;
135134
const BUILDINGS_VECTOR_LAYER_ID = BUILDING_LAYER_CONTRACTS.vectorLayerId;
136135
const BUILDINGS_SYM_PCT_STATE_KEY = BUILDING_LAYER_CONTRACTS.symPctStateKey;
136+
const BUILDINGS_STATUS_STATE_KEY = BUILDING_LAYER_CONTRACTS.statusStateKey;
137137
const BUILDINGS_SELECTED_STATE_KEY = BUILDING_LAYER_CONTRACTS.selectedStateKey;
138-
const BUILDINGS_CHOROPLETH_FILL_COLOR_EXPR = BUILDING_LAYER_CONTRACTS.fillColorExpression;
138+
const DEFAULT_URBAN95_METRIC = Urban95ScoreModel.getWeightedMetric("u95.overall");
139+
const BUILDINGS_CHOROPLETH_FILL_COLOR_EXPR = createBuildingFillColorExpression(DEFAULT_URBAN95_METRIC);
139140
const _urban95BuildingsSource = createBuildingsSource({
140141
artifacts: GENERATED_ARTIFACTS,
141142
buildingsPmtilesPath: BUILDINGS_PMTILES_URL,
@@ -237,8 +238,6 @@ const getWeightedNeighborhoodMetricValue =
237238
Urban95RenderState.getWeightedNeighborhoodMetricValue;
238239
const hasWeightedNeighborhoodMetricData =
239240
Urban95RenderState.hasWeightedNeighborhoodMetricData;
240-
const getWeightedHistogramDistribution =
241-
Urban95RenderState.getWeightedHistogramDistribution;
242241
const scoreContext = Urban95ScoreContext.create({
243242
scoreModel: Urban95ScoreModel,
244243
state: {
@@ -495,11 +494,15 @@ Urban95MapRenderers.configure({
495494
collectBuildingScores: collectBuildingScores,
496495
bulkPercentileRanks: bulkPercentileRanks,
497496
symPctKey: SYM_PCT_KEY,
497+
normalizeStatus: Urban95StatusScale.normalize,
498+
statusMatchExpression: Urban95StatusScale.matchExpression,
498499
buildingsMapSourceId: BUILDINGS_MAP_SOURCE_ID,
499500
buildingsVectorLayerId: BUILDINGS_VECTOR_LAYER_ID,
500501
buildingsSymPctStateKey: BUILDINGS_SYM_PCT_STATE_KEY,
502+
buildingsStatusStateKey: BUILDINGS_STATUS_STATE_KEY,
501503
buildingsFillLayerId: BUILDINGS_FILL_LAYER_ID,
502504
buildingsChoroplethFillColorExpr: BUILDINGS_CHOROPLETH_FILL_COLOR_EXPR,
505+
createBuildingFillColorExpression: createBuildingFillColorExpression,
503506
getNeighborhoodSurfaceData: function () {
504507
return neighborhoodSurfaceData;
505508
},
@@ -783,6 +786,7 @@ const neighborhoodSelection = Urban95NeighborhoodSelection.create({
783786
return Urban95ScoreModel.neighborhoodIsComparable(feature && feature.properties, {
784787
scoreMode: getScoreModeState(),
785788
minutes: getScoreMinutes(),
789+
activeMetric: getActiveMetricState(),
786790
});
787791
},
788792
});
@@ -830,12 +834,6 @@ Urban95NeighborhoodSidebar.configure({
830834
renderDeps: {
831835
pieSlicesFromInventoryCounts: Urban95Dashboards.pieSlicesFromInventoryCounts,
832836
getActiveMetric: getActiveMetricState,
833-
getWeightedHistogramDistribution: getWeightedHistogramDistribution,
834-
weightedCategoryHighlightsFromSource: weightedCategoryHighlightsFromSource,
835-
weightedSubcategoryComparisonRows: weightedSubcategoryComparisonRows,
836-
renderWeightedSubcategoryComparisonList: scoreExplain.renderWeightedSubcategoryComparisonList,
837-
buildHistogramDistributionFromScores: buildHistogramDistributionFromScores,
838-
collectBuildingScores: collectBuildingScores,
839837
getColorForValue: getColorForValue,
840838
getNeighborhoodPercentileKey: getNeighborhoodPercentileKey,
841839
getOrdinalSuffix: scoreExplain.getOrdinalSuffix,
@@ -890,9 +888,7 @@ Urban95CitySidebar.configure({
890888
getScoreModeLabel: getScoreModeLabel,
891889
getOrdinalSuffix: scoreExplain.getOrdinalSuffix,
892890
heroPercentileMeterFillStyle: scoreExplain.heroPercentileMeterFillStyle,
893-
getWeightedHistogramDistribution: getWeightedHistogramDistribution,
894891
getNeighborhoodPercentileKey: getNeighborhoodPercentileKey,
895-
buildHistogramDistributionFromScores: buildHistogramDistributionFromScores,
896892
collectBuildingScores: collectBuildingScores,
897893
percentileBreakpoints: percentileBreakpoints,
898894
getColorForValue: getColorForValue,

0 commit comments

Comments
 (0)