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:
60
portfolio_app/toronto/schemas/amenities.py
Normal file
60
portfolio_app/toronto/schemas/amenities.py
Normal 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}
|
||||
Reference in New Issue
Block a user