feat: Implement Phase 3 neighbourhood data model

Add schemas, parsers, loaders, and models for Toronto neighbourhood-centric
data including census profiles, crime statistics, and amenities.

Schemas:
- NeighbourhoodRecord, CensusRecord, CrimeRecord, CrimeType
- AmenityType, AmenityRecord, AmenityCount

Models:
- BridgeCMHCNeighbourhood (zone-to-neighbourhood mapping with weights)
- FactCensus, FactCrime, FactAmenities

Parsers:
- TorontoOpenDataParser (CKAN API for neighbourhoods, census, amenities)
- TorontoPoliceParser (crime rates, MCI data)

Loaders:
- load_census_data, load_crime_data, load_amenities
- build_cmhc_neighbourhood_crosswalk (PostGIS area weights)

Also updates CLAUDE.md with projman plugin workflow documentation.

Closes #53, #54, #55, #56, #57, #58, #59

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 11:07:13 -05:00
parent f69d0c15a7
commit 053acf6436
14 changed files with 1466 additions and 2 deletions

View File

@@ -0,0 +1,60 @@
"""Pydantic schemas for Toronto amenities data.
Includes schemas for parks, schools, childcare centres, and transit stops.
"""
from decimal import Decimal
from enum import Enum
from pydantic import BaseModel, Field
class AmenityType(str, Enum):
"""Types of amenities tracked in the neighbourhood dashboard."""
PARK = "park"
SCHOOL = "school"
CHILDCARE = "childcare"
TRANSIT_STOP = "transit_stop"
LIBRARY = "library"
COMMUNITY_CENTRE = "community_centre"
HOSPITAL = "hospital"
class AmenityRecord(BaseModel):
"""Amenity location record for a neighbourhood.
Represents a single amenity (park, school, etc.) with its location
and associated neighbourhood.
"""
neighbourhood_id: int = Field(
ge=1, le=200, description="Neighbourhood ID containing this amenity"
)
amenity_type: AmenityType = Field(description="Type of amenity")
amenity_name: str = Field(max_length=200, description="Name of the amenity")
address: str | None = Field(
default=None, max_length=300, description="Street address"
)
latitude: Decimal | None = Field(
default=None, ge=-90, le=90, description="Latitude (WGS84)"
)
longitude: Decimal | None = Field(
default=None, ge=-180, le=180, description="Longitude (WGS84)"
)
model_config = {"str_strip_whitespace": True}
class AmenityCount(BaseModel):
"""Aggregated amenity count for a neighbourhood.
Used for dashboard metrics showing amenity density per neighbourhood.
"""
neighbourhood_id: int = Field(ge=1, le=200, description="Neighbourhood ID")
amenity_type: AmenityType = Field(description="Type of amenity")
count: int = Field(ge=0, description="Number of amenities of this type")
year: int = Field(ge=2020, le=2030, description="Year of data snapshot")
model_config = {"str_strip_whitespace": True}