""" config.py -- Project-wide configuration for hedonic pricing analysis. Centralises every tunable constant, file path, spatial reference, modelling parameter, and visual-style setting so that downstream modules never hard-code values. """ from __future__ import annotations import os from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Tuple # --------------------------------------------------------------------------- # 1. Directory layout # --------------------------------------------------------------------------- PROJECT_ROOT: Path = Path(__file__).resolve().parent.parent # Data directories DATA_DIR: Path = PROJECT_ROOT / "data" DATA_RAW: Path = DATA_DIR / "raw" DATA_INTERIM: Path = DATA_DIR / "interim" DATA_PROCESSED: Path = DATA_DIR / "processed" DATA_DUCKDB: Path = DATA_DIR / "duckdb" # Output directories OUTPUTS_DIR: Path = PROJECT_ROOT / "results" OUTPUTS_TABLES: Path = OUTPUTS_DIR / "tables" OUTPUTS_FIGURES: Path = OUTPUTS_DIR / "figures" OUTPUTS_MODELS: Path = OUTPUTS_DIR / "models" # Paper / LaTeX directory PAPER_DIR: Path = PROJECT_ROOT / "paper" # Ensure all directories exist at import time (idempotent) for _d in ( DATA_RAW, DATA_INTERIM, DATA_PROCESSED, DATA_DUCKDB, OUTPUTS_TABLES, OUTPUTS_FIGURES, OUTPUTS_MODELS, PAPER_DIR, ): _d.mkdir(parents=True, exist_ok=True) # --------------------------------------------------------------------------- # 2. Raw data file paths # --------------------------------------------------------------------------- # Primary cleaned sample (always present) CLEANED_SAMPLE_CSV: Path = PROJECT_ROOT / "data" / "raw" / "cleaned_dataset_sample.csv" # Inside Airbnb full extracts (may or may not exist) LISTINGS_CSV_GZ: Path = DATA_RAW / "listings.csv.gz" CALENDAR_CSV_GZ: Path = DATA_RAW / "calendar.csv.gz" REVIEWS_CSV_GZ: Path = DATA_RAW / "reviews.csv.gz" NEIGHBOURHOODS_CSV: Path = DATA_RAW / "neighbourhoods.csv" NEIGHBOURHOODS_GEOJSON: Path = DATA_RAW / "neighbourhoods.geojson" # DuckDB database DUCKDB_PATH: Path = DATA_DUCKDB / "airbnb.duckdb" # --------------------------------------------------------------------------- # 3. Spatial / geographic constants # --------------------------------------------------------------------------- @dataclass(frozen=True) class CityCenter: """WGS-84 centroid of a Quebec city.""" name: str lat: float lon: float CITY_CENTERS: Dict[str, CityCenter] = { "Montréal": CityCenter("Montréal", 45.5017, -73.5673), "Montreal": CityCenter("Montréal", 45.5017, -73.5673), "Québec": CityCenter("Québec", 46.8139, -71.2080), "Quebec": CityCenter("Québec", 46.8139, -71.2080), "Gatineau": CityCenter("Gatineau", 45.4765, -75.7013), "Sherbrooke": CityCenter("Sherbrooke", 45.4042, -71.8929), "Laval": CityCenter("Laval", 45.6066, -73.7124), "Trois-Rivières": CityCenter("Trois-Rivières", 46.3432, -72.5432), "Trois-Rivieres": CityCenter("Trois-Rivières", 46.3432, -72.5432), "Saguenay": CityCenter("Saguenay", 48.4279, -71.0548), "Lévis": CityCenter("Lévis", 46.8032, -71.1780), "Levis": CityCenter("Lévis", 46.8032, -71.1780), "Terrebonne": CityCenter("Terrebonne", 45.7005, -73.6474), "Longueuil": CityCenter("Longueuil", 45.5312, -73.5185), "Rimouski": CityCenter("Rimouski", 48.4489, -68.5234), "Saint-Hyacinthe": CityCenter("Saint-Hyacinthe", 45.6307, -72.9570), "Drummondville": CityCenter("Drummondville", 45.8838, -72.4843), "Granby": CityCenter("Granby", 45.4001, -72.7329), "Mont-Tremblant": CityCenter("Mont-Tremblant", 46.2088, -74.5849), "Magog": CityCenter("Magog", 45.2668, -72.1419), "Charlevoix": CityCenter("Charlevoix", 47.4389, -70.5025), } # Quebec bounding box (generous, covers all of southern Quebec + some northern) QUEBEC_LAT_RANGE: Tuple[float, float] = (44.5, 55.5) QUEBEC_LON_RANGE: Tuple[float, float] = (-80.0, -56.0) # Spatial buffer distances in metres SPATIAL_BUFFERS_M: List[int] = [250, 500, 1_000, 2_000] # Earth radius for haversine (km) EARTH_RADIUS_KM: float = 6_371.0 # --------------------------------------------------------------------------- # 4. Price & winsorization # --------------------------------------------------------------------------- # Winsorization percentile pairs (lower, upper) WINSORIZE_1_99: Tuple[float, float] = (0.01, 0.99) WINSORIZE_2_98: Tuple[float, float] = (0.02, 0.98) WINSORIZE_5_95: Tuple[float, float] = (0.05, 0.95) # Default winsorization used in modelling DEFAULT_WINSORIZE: Tuple[float, float] = WINSORIZE_1_99 # Minimum valid nightly price in CAD PRICE_FLOOR: float = 10.0 # Maximum plausible nightly price in CAD PRICE_CEILING: float = 25_000.0 # --------------------------------------------------------------------------- # 5. Quantile regression thresholds # --------------------------------------------------------------------------- QUANTILE_THRESHOLDS: List[float] = [0.10, 0.25, 0.50, 0.75, 0.90] # --------------------------------------------------------------------------- # 6. Feature groups # --------------------------------------------------------------------------- FEATURE_GROUPS: Dict[str, List[str]] = { "structural": [ "bedrooms", "bathrooms", "guests_count", "amenities_count", ], "location": [ "lat", "long", "dist_city_center_km", ], "quality": [ "rating", "quality_score", "num_reviews", "num_images", ], "host": [ "is_superhost", "host_response_rate", "host_acceptance_rate", "host_listings_count", ], "property": [ "property_type", "room_type", ], "amenity_flags": [ "pets_allowed", "is_guest_favorite", ], } # Flat list of all feature names (order-preserving) ALL_FEATURES: List[str] = [ feat for group in FEATURE_GROUPS.values() for feat in group ] # --------------------------------------------------------------------------- # 7. Property / room type mappings # --------------------------------------------------------------------------- PROPERTY_TYPE_MAP: Dict[str, str] = { "Rental unit": "Rental unit", "House": "House", "Cabin/Chalet": "Cabin/Chalet", "Condo": "Condo", "Apartment": "Apartment", "Other": "Other", # Inside Airbnb variants "Entire rental unit": "Rental unit", "Private room in rental unit": "Rental unit", "Entire home": "House", "Entire house": "House", "Entire cabin": "Cabin/Chalet", "Entire chalet": "Cabin/Chalet", "Entire condo": "Condo", "Entire condominium (condo)": "Condo", "Entire loft": "Apartment", "Entire guest suite": "Apartment", "Entire guesthouse": "House", "Entire townhouse": "House", "Entire cottage": "Cabin/Chalet", "Entire bungalow": "House", "Entire villa": "House", "Tiny home": "Other", "Treehouse": "Other", "Boat": "Other", "Camper/RV": "Other", "Farm stay": "Other", "Yurt": "Other", "Tent": "Other", "Castle": "Other", "Private room": "Other", "Shared room": "Other", } ROOM_TYPE_MAP: Dict[str, str] = { "Entire home/apt": "Entire home/apt", "Private room": "Private room", "Shared room": "Shared room", "Hotel room": "Hotel room", # Normalised aliases "entire home": "Entire home/apt", "entire place": "Entire home/apt", "private": "Private room", "shared": "Shared room", "hotel": "Hotel room", } # --------------------------------------------------------------------------- # 8. Model catalogue # --------------------------------------------------------------------------- MODEL_NAMES: List[str] = [ "ols_linear", "ols_log", "ols_semilog", "robust_huber", "robust_bisquare", "quantile_10", "quantile_25", "quantile_50", "quantile_75", "quantile_90", "spatial_lag", "spatial_error", "gwr", "rf_regressor", "gradient_boosting", "xgboost", "lasso", "ridge", "elastic_net", ] # --------------------------------------------------------------------------- # 9. Reproducibility # --------------------------------------------------------------------------- RANDOM_SEED: int = 42 TEST_SIZE: float = 0.20 CV_FOLDS: int = 5 # --------------------------------------------------------------------------- # 10. Visualisation palettes & figure settings # --------------------------------------------------------------------------- # Colour palettes PALETTE_SEQUENTIAL: str = "YlOrRd" PALETTE_DIVERGING: str = "RdBu_r" PALETTE_CATEGORICAL: List[str] = [ "#1f77b4", # muted blue "#ff7f0e", # safety orange "#2ca02c", # cooked-asparagus green "#d62728", # brick red "#9467bd", # muted purple "#8c564b", # chestnut brown "#e377c2", # raspberry-yoghurt pink "#7f7f7f", # middle grey "#bcbd22", # curry yellow-green "#17becf", # blue-teal ] PALETTE_PROPERTY_TYPE: Dict[str, str] = { "Rental unit": "#1f77b4", "House": "#2ca02c", "Cabin/Chalet": "#8c564b", "Condo": "#ff7f0e", "Apartment": "#9467bd", "Other": "#7f7f7f", } PALETTE_PRICE_CATEGORY: Dict[str, str] = { "Budget (<$100)": "#2ca02c", "Moderate ($100-200)": "#1f77b4", "Premium ($200-400)": "#ff7f0e", "Luxury ($400-1000)": "#d62728", "Ultra-Luxury (>$1000)": "#9467bd", "Unknown": "#7f7f7f", } # Figure defaults FIGURE_DPI: int = 300 FIGURE_FORMAT: str = "pdf" FIGURE_SIZE_SINGLE: Tuple[float, float] = (6.5, 4.5) FIGURE_SIZE_DOUBLE: Tuple[float, float] = (13.0, 4.5) FIGURE_SIZE_FULL: Tuple[float, float] = (13.0, 9.0) FIGURE_FONT_FAMILY: str = "serif" FIGURE_FONT_SIZE: int = 11 # --------------------------------------------------------------------------- # 11. LaTeX table formatting # --------------------------------------------------------------------------- @dataclass(frozen=True) class LaTeXSettings: """Settings for stargazer-style LaTeX table output.""" float_format: str = "%.4f" significance_levels: Tuple[float, ...] = (0.01, 0.05, 0.10) significance_symbols: Tuple[str, ...] = ("***", "**", "*") caption_position: str = "top" label_prefix: str = "tab:" font_size: str = "\\footnotesize" column_sep: str = " & " booktabs: bool = True threeparttable: bool = True note_width: str = "0.95\\linewidth" LATEX: LaTeXSettings = LaTeXSettings() # --------------------------------------------------------------------------- # 12. Logging # --------------------------------------------------------------------------- LOG_LEVEL: str = os.environ.get("HEDONIC_LOG_LEVEL", "INFO") LOG_FORMAT: str = "%(asctime)s | %(name)-20s | %(levelname)-8s | %(message)s" LOG_DATE_FORMAT: str = "%Y-%m-%d %H:%M:%S"