feat(osm-equity): uniform 2-sq-mi tile units + areal equity interpolation#29
Conversation
…nterpolation Adds an alternative to irregular census tracts: - build_tiles(bbox, sqmi=2.0): a regular grid of ~2-square-mile square tiles as tract-shaped units (uniform size => cleaner cross-area comparison, less modifiable-areal-unit-problem noise than variable tracts). - areal_interpolate_equity(tiles, tract_records): dasymetric-style areal interpolation that intersection-area-weights ACS tract equity onto each tile (tiles over no populated tract are omitted, not zeroed). The OSM-quality metrics and the MapLibre heat map work unchanged on tiles. On real SF data the 2-sq-mi tiling gives 45 tiles and raises Global Moran's I of attribute completeness from 0.33 (tracts) to 0.48 (tiles) - a cleaner spatial signal. 3 offline unit tests added; 16 offline tests pass; ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a regular square-tile grid analysis option as an alternative to irregular census tracts, adding functions to build tiles and perform dasymetric areal interpolation of equity metrics, along with corresponding unit tests and documentation. The reviewer feedback suggests enhancing robustness by adding input validation for tile generation parameters, implementing guard checks for empty inputs, and handling potential topological errors and missing dictionary keys during spatial intersection calculations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| """Break a bbox into a regular grid of ~`sqmi`-square-mile tiles, as | ||
| tract-shaped analysis units (uniform size makes cross-area OSM-quality | ||
| comparison cleaner than variable census tracts).""" | ||
| xmin, ymin, xmax, ymax = bbox |
There was a problem hiding this comment.
Add defensive validation for the sqmi parameter and the bounding box coordinates to prevent potential division-by-zero errors or invalid grid generation.
| xmin, ymin, xmax, ymax = bbox | |
| xmin, ymin, xmax, ymax = bbox | |
| if sqmi <= 0: | |
| raise ValueError("sqmi must be a positive number.") | |
| if xmin >= xmax or ymin >= ymax: | |
| raise ValueError("Invalid bounding box: xmin must be < xmax and ymin must be < ymax.") |
| from shapely.strtree import STRtree | ||
|
|
There was a problem hiding this comment.
Add a guard check at the beginning of areal_interpolate_equity to handle empty inputs gracefully and avoid unnecessary processing or potential issues with empty STRtree initialization.
| from shapely.strtree import STRtree | |
| if not tiles or not tract_records: | |
| return {} | |
| from shapely.strtree import STRtree |
| eq = tract_records[idx]["equity"] | ||
| if eq["median_income"] <= 0: | ||
| continue | ||
| inter = poly.intersection(geoms[idx]) | ||
| w = inter.area | ||
| if w <= 0: | ||
| continue | ||
| for f in _EQ_FIELDS: | ||
| acc[f] += eq[f] * w | ||
| wsum += w |
There was a problem hiding this comment.
Improve robustness by using safe dictionary access for the equity profile and its fields, and handle potential topological errors (e.g., self-intersections in real-world TIGERweb data) during spatial intersection using a fallback .buffer(0).
| eq = tract_records[idx]["equity"] | |
| if eq["median_income"] <= 0: | |
| continue | |
| inter = poly.intersection(geoms[idx]) | |
| w = inter.area | |
| if w <= 0: | |
| continue | |
| for f in _EQ_FIELDS: | |
| acc[f] += eq[f] * w | |
| wsum += w | |
| eq = tract_records[idx].get("equity") | |
| if not eq or eq.get("median_income", 0) <= 0: | |
| continue | |
| try: | |
| inter = poly.intersection(geoms[idx]) | |
| except Exception: | |
| try: | |
| inter = poly.buffer(0).intersection(geoms[idx].buffer(0)) | |
| except Exception: | |
| continue | |
| w = inter.area | |
| if w <= 0: | |
| continue | |
| for f in _EQ_FIELDS: | |
| acc[f] += eq.get(f, 0.0) * w | |
| wsum += w |
Breaks a city into a regular grid of ~2-square-mile tiles as an alternative to irregular census tracts:
build_tiles(bbox, sqmi=2.0)— uniform square-tile units.areal_interpolate_equity— dasymetric intersection-area weighting of ACS tract equity onto tiles (tiles over no populated tract omitted, not zeroed).Metrics + heat map work unchanged on tiles. On real SF data, 2-sq-mi tiling gives 45 tiles and raises Global Moran's I of attribute completeness from 0.33 (tracts) → 0.48 (tiles) — a cleaner spatial signal (less MAUP noise). 3 offline unit tests; 16 offline tests pass; ruff clean.
🤖 Generated with Claude Code