- Add policy event markers to time series charts - Create methodology page (/toronto/methodology) with data sources - Add demo data module for testing without full pipeline - Update README with project documentation - Add health check endpoint (/health) - Add database initialization script - Export new figure factory functions Closes #21 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
258 lines
9.1 KiB
Python
258 lines
9.1 KiB
Python
"""Demo/sample data for testing the Toronto Housing Dashboard without full pipeline.
|
|
|
|
This module provides synthetic data for development and demonstration purposes.
|
|
Replace with real data from the database in production.
|
|
"""
|
|
|
|
from datetime import date
|
|
from typing import Any
|
|
|
|
|
|
def get_demo_districts() -> list[dict[str, Any]]:
|
|
"""Return sample TRREB district data."""
|
|
return [
|
|
{"district_code": "W01", "district_name": "Long Branch", "area_type": "West"},
|
|
{"district_code": "W02", "district_name": "Mimico", "area_type": "West"},
|
|
{
|
|
"district_code": "W03",
|
|
"district_name": "Kingsway South",
|
|
"area_type": "West",
|
|
},
|
|
{"district_code": "W04", "district_name": "Edenbridge", "area_type": "West"},
|
|
{"district_code": "W05", "district_name": "Islington", "area_type": "West"},
|
|
{"district_code": "W06", "district_name": "Rexdale", "area_type": "West"},
|
|
{"district_code": "W07", "district_name": "Willowdale", "area_type": "West"},
|
|
{"district_code": "W08", "district_name": "York", "area_type": "West"},
|
|
{
|
|
"district_code": "C01",
|
|
"district_name": "Downtown Core",
|
|
"area_type": "Central",
|
|
},
|
|
{"district_code": "C02", "district_name": "Annex", "area_type": "Central"},
|
|
{
|
|
"district_code": "C03",
|
|
"district_name": "Forest Hill",
|
|
"area_type": "Central",
|
|
},
|
|
{
|
|
"district_code": "C04",
|
|
"district_name": "Lawrence Park",
|
|
"area_type": "Central",
|
|
},
|
|
{
|
|
"district_code": "C06",
|
|
"district_name": "Willowdale East",
|
|
"area_type": "Central",
|
|
},
|
|
{"district_code": "C07", "district_name": "Thornhill", "area_type": "Central"},
|
|
{"district_code": "C08", "district_name": "Waterfront", "area_type": "Central"},
|
|
{"district_code": "E01", "district_name": "Leslieville", "area_type": "East"},
|
|
{"district_code": "E02", "district_name": "The Beaches", "area_type": "East"},
|
|
{"district_code": "E03", "district_name": "Danforth", "area_type": "East"},
|
|
{"district_code": "E04", "district_name": "Birch Cliff", "area_type": "East"},
|
|
{"district_code": "E05", "district_name": "Scarborough", "area_type": "East"},
|
|
]
|
|
|
|
|
|
def get_demo_purchase_data() -> list[dict[str, Any]]:
|
|
"""Return sample purchase data for time series visualization."""
|
|
import random
|
|
|
|
random.seed(42)
|
|
data = []
|
|
|
|
base_prices = {
|
|
"W01": 850000,
|
|
"C01": 1200000,
|
|
"E01": 950000,
|
|
}
|
|
|
|
for year in [2024, 2025]:
|
|
for month in range(1, 13):
|
|
if year == 2025 and month > 12:
|
|
break
|
|
|
|
for district, base_price in base_prices.items():
|
|
# Add some randomness and trend
|
|
trend = (year - 2024) * 12 + month
|
|
price_variation = random.uniform(-0.05, 0.05)
|
|
trend_factor = 1 + (trend * 0.002) # Slight upward trend
|
|
|
|
avg_price = int(base_price * trend_factor * (1 + price_variation))
|
|
sales = random.randint(50, 200)
|
|
|
|
data.append(
|
|
{
|
|
"district_code": district,
|
|
"full_date": date(year, month, 1),
|
|
"year": year,
|
|
"month": month,
|
|
"avg_price": avg_price,
|
|
"median_price": int(avg_price * 0.95),
|
|
"sales_count": sales,
|
|
"new_listings": int(sales * random.uniform(1.2, 1.8)),
|
|
"active_listings": int(sales * random.uniform(2.0, 3.5)),
|
|
"days_on_market": random.randint(15, 45),
|
|
"sale_to_list_ratio": round(random.uniform(0.95, 1.05), 2),
|
|
}
|
|
)
|
|
|
|
return data
|
|
|
|
|
|
def get_demo_rental_data() -> list[dict[str, Any]]:
|
|
"""Return sample rental data for visualization."""
|
|
data = []
|
|
|
|
zones = [
|
|
("Zone01", "Downtown"),
|
|
("Zone02", "Midtown"),
|
|
("Zone03", "North York"),
|
|
("Zone04", "Scarborough"),
|
|
("Zone05", "Etobicoke"),
|
|
]
|
|
|
|
bedroom_types = ["bachelor", "1_bedroom", "2_bedroom", "3_bedroom"]
|
|
|
|
base_rents = {
|
|
"bachelor": 1800,
|
|
"1_bedroom": 2200,
|
|
"2_bedroom": 2800,
|
|
"3_bedroom": 3400,
|
|
}
|
|
|
|
for year in [2021, 2022, 2023, 2024, 2025]:
|
|
for zone_code, zone_name in zones:
|
|
for bedroom in bedroom_types:
|
|
# Rental trend: ~5% increase per year
|
|
year_factor = 1 + ((year - 2021) * 0.05)
|
|
base_rent = base_rents[bedroom]
|
|
|
|
data.append(
|
|
{
|
|
"zone_code": zone_code,
|
|
"zone_name": zone_name,
|
|
"survey_year": year,
|
|
"full_date": date(year, 10, 1),
|
|
"bedroom_type": bedroom,
|
|
"average_rent": int(base_rent * year_factor),
|
|
"median_rent": int(base_rent * year_factor * 0.98),
|
|
"vacancy_rate": round(
|
|
2.5 - (year - 2021) * 0.3, 1
|
|
), # Decreasing vacancy
|
|
"universe": 5000 + (year - 2021) * 200,
|
|
}
|
|
)
|
|
|
|
return data
|
|
|
|
|
|
def get_demo_policy_events() -> list[dict[str, Any]]:
|
|
"""Return sample policy events for annotation."""
|
|
return [
|
|
{
|
|
"event_date": date(2024, 6, 5),
|
|
"effective_date": date(2024, 6, 5),
|
|
"level": "federal",
|
|
"category": "monetary",
|
|
"title": "BoC Rate Cut (25bp)",
|
|
"description": "Bank of Canada cuts overnight rate by 25 basis points to 4.75%",
|
|
"expected_direction": "bullish",
|
|
},
|
|
{
|
|
"event_date": date(2024, 7, 24),
|
|
"effective_date": date(2024, 7, 24),
|
|
"level": "federal",
|
|
"category": "monetary",
|
|
"title": "BoC Rate Cut (25bp)",
|
|
"description": "Bank of Canada cuts overnight rate by 25 basis points to 4.50%",
|
|
"expected_direction": "bullish",
|
|
},
|
|
{
|
|
"event_date": date(2024, 9, 4),
|
|
"effective_date": date(2024, 9, 4),
|
|
"level": "federal",
|
|
"category": "monetary",
|
|
"title": "BoC Rate Cut (25bp)",
|
|
"description": "Bank of Canada cuts overnight rate by 25 basis points to 4.25%",
|
|
"expected_direction": "bullish",
|
|
},
|
|
{
|
|
"event_date": date(2024, 10, 23),
|
|
"effective_date": date(2024, 10, 23),
|
|
"level": "federal",
|
|
"category": "monetary",
|
|
"title": "BoC Rate Cut (50bp)",
|
|
"description": "Bank of Canada cuts overnight rate by 50 basis points to 3.75%",
|
|
"expected_direction": "bullish",
|
|
},
|
|
{
|
|
"event_date": date(2024, 12, 11),
|
|
"effective_date": date(2024, 12, 11),
|
|
"level": "federal",
|
|
"category": "monetary",
|
|
"title": "BoC Rate Cut (50bp)",
|
|
"description": "Bank of Canada cuts overnight rate by 50 basis points to 3.25%",
|
|
"expected_direction": "bullish",
|
|
},
|
|
{
|
|
"event_date": date(2024, 9, 16),
|
|
"effective_date": date(2024, 12, 15),
|
|
"level": "federal",
|
|
"category": "regulatory",
|
|
"title": "CMHC 30-Year Amortization",
|
|
"description": "30-year amortization extended to all first-time buyers and new builds",
|
|
"expected_direction": "bullish",
|
|
},
|
|
{
|
|
"event_date": date(2024, 9, 16),
|
|
"effective_date": date(2024, 12, 15),
|
|
"level": "federal",
|
|
"category": "regulatory",
|
|
"title": "Insured Mortgage Cap $1.5M",
|
|
"description": "Insured mortgage cap raised from $1M to $1.5M",
|
|
"expected_direction": "bullish",
|
|
},
|
|
]
|
|
|
|
|
|
def get_demo_summary_metrics() -> dict[str, dict[str, Any]]:
|
|
"""Return summary metrics for KPI cards."""
|
|
return {
|
|
"avg_price": {
|
|
"value": 1067968,
|
|
"title": "Avg. Price (2025)",
|
|
"delta": -4.7,
|
|
"delta_suffix": "%",
|
|
"prefix": "$",
|
|
"format_spec": ",.0f",
|
|
"positive_is_good": True,
|
|
},
|
|
"total_sales": {
|
|
"value": 67610,
|
|
"title": "Total Sales (2024)",
|
|
"delta": 2.6,
|
|
"delta_suffix": "%",
|
|
"format_spec": ",.0f",
|
|
"positive_is_good": True,
|
|
},
|
|
"avg_rent": {
|
|
"value": 2450,
|
|
"title": "Avg. Rent (2025)",
|
|
"delta": 3.2,
|
|
"delta_suffix": "%",
|
|
"prefix": "$",
|
|
"format_spec": ",.0f",
|
|
"positive_is_good": False,
|
|
},
|
|
"vacancy_rate": {
|
|
"value": 1.8,
|
|
"title": "Vacancy Rate",
|
|
"delta": -0.4,
|
|
"delta_suffix": "pp",
|
|
"suffix": "%",
|
|
"format_spec": ".1f",
|
|
"positive_is_good": False,
|
|
},
|
|
}
|