Python 64.6%
TeX 23.3%
HTML 11.6%
Makefile 0.5%
1"""2config.py -- Project-wide configuration for hedonic pricing analysis.34Centralises every tunable constant, file path, spatial reference,5modelling parameter, and visual-style setting so that downstream6modules never hard-code values.7"""89from __future__ import annotations1011import os12from dataclasses import dataclass, field13from pathlib import Path14from typing import Any, Dict, List, Tuple1516# ---------------------------------------------------------------------------17# 1. Directory layout18# ---------------------------------------------------------------------------1920PROJECT_ROOT: Path = Path(__file__).resolve().parent.parent2122# Data directories23DATA_DIR: Path = PROJECT_ROOT / "data"24DATA_RAW: Path = DATA_DIR / "raw"25DATA_INTERIM: Path = DATA_DIR / "interim"26DATA_PROCESSED: Path = DATA_DIR / "processed"27DATA_DUCKDB: Path = DATA_DIR / "duckdb"2829# Output directories30OUTPUTS_DIR: Path = PROJECT_ROOT / "results"31OUTPUTS_TABLES: Path = OUTPUTS_DIR / "tables"32OUTPUTS_FIGURES: Path = OUTPUTS_DIR / "figures"33OUTPUTS_MODELS: Path = OUTPUTS_DIR / "models"3435# Paper / LaTeX directory36PAPER_DIR: Path = PROJECT_ROOT / "paper"3738# Ensure all directories exist at import time (idempotent)39for _d in (40 DATA_RAW, DATA_INTERIM, DATA_PROCESSED, DATA_DUCKDB,41 OUTPUTS_TABLES, OUTPUTS_FIGURES, OUTPUTS_MODELS, PAPER_DIR,42):43 _d.mkdir(parents=True, exist_ok=True)4445# ---------------------------------------------------------------------------46# 2. Raw data file paths47# ---------------------------------------------------------------------------4849# Primary cleaned sample (always present)50CLEANED_SAMPLE_CSV: Path = PROJECT_ROOT / "data" / "raw" / "cleaned_dataset_sample.csv"5152# Inside Airbnb full extracts (may or may not exist)53LISTINGS_CSV_GZ: Path = DATA_RAW / "listings.csv.gz"54CALENDAR_CSV_GZ: Path = DATA_RAW / "calendar.csv.gz"55REVIEWS_CSV_GZ: Path = DATA_RAW / "reviews.csv.gz"56NEIGHBOURHOODS_CSV: Path = DATA_RAW / "neighbourhoods.csv"57NEIGHBOURHOODS_GEOJSON: Path = DATA_RAW / "neighbourhoods.geojson"5859# DuckDB database60DUCKDB_PATH: Path = DATA_DUCKDB / "airbnb.duckdb"6162# ---------------------------------------------------------------------------63# 3. Spatial / geographic constants64# ---------------------------------------------------------------------------6566@dataclass(frozen=True)67class CityCenter:68 """WGS-84 centroid of a Quebec city."""69 name: str70 lat: float71 lon: float727374CITY_CENTERS: Dict[str, CityCenter] = {75 "Montréal": CityCenter("Montréal", 45.5017, -73.5673),76 "Montreal": CityCenter("Montréal", 45.5017, -73.5673),77 "Québec": CityCenter("Québec", 46.8139, -71.2080),78 "Quebec": CityCenter("Québec", 46.8139, -71.2080),79 "Gatineau": CityCenter("Gatineau", 45.4765, -75.7013),80 "Sherbrooke": CityCenter("Sherbrooke", 45.4042, -71.8929),81 "Laval": CityCenter("Laval", 45.6066, -73.7124),82 "Trois-Rivières": CityCenter("Trois-Rivières", 46.3432, -72.5432),83 "Trois-Rivieres": CityCenter("Trois-Rivières", 46.3432, -72.5432),84 "Saguenay": CityCenter("Saguenay", 48.4279, -71.0548),85 "Lévis": CityCenter("Lévis", 46.8032, -71.1780),86 "Levis": CityCenter("Lévis", 46.8032, -71.1780),87 "Terrebonne": CityCenter("Terrebonne", 45.7005, -73.6474),88 "Longueuil": CityCenter("Longueuil", 45.5312, -73.5185),89 "Rimouski": CityCenter("Rimouski", 48.4489, -68.5234),90 "Saint-Hyacinthe": CityCenter("Saint-Hyacinthe", 45.6307, -72.9570),91 "Drummondville": CityCenter("Drummondville", 45.8838, -72.4843),92 "Granby": CityCenter("Granby", 45.4001, -72.7329),93 "Mont-Tremblant": CityCenter("Mont-Tremblant", 46.2088, -74.5849),94 "Magog": CityCenter("Magog", 45.2668, -72.1419),95 "Charlevoix": CityCenter("Charlevoix", 47.4389, -70.5025),96}9798# Quebec bounding box (generous, covers all of southern Quebec + some northern)99QUEBEC_LAT_RANGE: Tuple[float, float] = (44.5, 55.5)100QUEBEC_LON_RANGE: Tuple[float, float] = (-80.0, -56.0)101102# Spatial buffer distances in metres103SPATIAL_BUFFERS_M: List[int] = [250, 500, 1_000, 2_000]104105# Earth radius for haversine (km)106EARTH_RADIUS_KM: float = 6_371.0107108# ---------------------------------------------------------------------------109# 4. Price & winsorization110# ---------------------------------------------------------------------------111112# Winsorization percentile pairs (lower, upper)113WINSORIZE_1_99: Tuple[float, float] = (0.01, 0.99)114WINSORIZE_2_98: Tuple[float, float] = (0.02, 0.98)115WINSORIZE_5_95: Tuple[float, float] = (0.05, 0.95)116117# Default winsorization used in modelling118DEFAULT_WINSORIZE: Tuple[float, float] = WINSORIZE_1_99119120# Minimum valid nightly price in CAD121PRICE_FLOOR: float = 10.0122123# Maximum plausible nightly price in CAD124PRICE_CEILING: float = 25_000.0125126# ---------------------------------------------------------------------------127# 5. Quantile regression thresholds128# ---------------------------------------------------------------------------129130QUANTILE_THRESHOLDS: List[float] = [0.10, 0.25, 0.50, 0.75, 0.90]131132# ---------------------------------------------------------------------------133# 6. Feature groups134# ---------------------------------------------------------------------------135136FEATURE_GROUPS: Dict[str, List[str]] = {137 "structural": [138 "bedrooms", "bathrooms", "guests_count", "amenities_count",139 ],140 "location": [141 "lat", "long", "dist_city_center_km",142 ],143 "quality": [144 "rating", "quality_score", "num_reviews", "num_images",145 ],146 "host": [147 "is_superhost", "host_response_rate", "host_acceptance_rate",148 "host_listings_count",149 ],150 "property": [151 "property_type", "room_type",152 ],153 "amenity_flags": [154 "pets_allowed", "is_guest_favorite",155 ],156}157158# Flat list of all feature names (order-preserving)159ALL_FEATURES: List[str] = [160 feat for group in FEATURE_GROUPS.values() for feat in group161]162163# ---------------------------------------------------------------------------164# 7. Property / room type mappings165# ---------------------------------------------------------------------------166167PROPERTY_TYPE_MAP: Dict[str, str] = {168 "Rental unit": "Rental unit",169 "House": "House",170 "Cabin/Chalet": "Cabin/Chalet",171 "Condo": "Condo",172 "Apartment": "Apartment",173 "Other": "Other",174 # Inside Airbnb variants175 "Entire rental unit": "Rental unit",176 "Private room in rental unit": "Rental unit",177 "Entire home": "House",178 "Entire house": "House",179 "Entire cabin": "Cabin/Chalet",180 "Entire chalet": "Cabin/Chalet",181 "Entire condo": "Condo",182 "Entire condominium (condo)": "Condo",183 "Entire loft": "Apartment",184 "Entire guest suite": "Apartment",185 "Entire guesthouse": "House",186 "Entire townhouse": "House",187 "Entire cottage": "Cabin/Chalet",188 "Entire bungalow": "House",189 "Entire villa": "House",190 "Tiny home": "Other",191 "Treehouse": "Other",192 "Boat": "Other",193 "Camper/RV": "Other",194 "Farm stay": "Other",195 "Yurt": "Other",196 "Tent": "Other",197 "Castle": "Other",198 "Private room": "Other",199 "Shared room": "Other",200}201202ROOM_TYPE_MAP: Dict[str, str] = {203 "Entire home/apt": "Entire home/apt",204 "Private room": "Private room",205 "Shared room": "Shared room",206 "Hotel room": "Hotel room",207 # Normalised aliases208 "entire home": "Entire home/apt",209 "entire place": "Entire home/apt",210 "private": "Private room",211 "shared": "Shared room",212 "hotel": "Hotel room",213}214215# ---------------------------------------------------------------------------216# 8. Model catalogue217# ---------------------------------------------------------------------------218219MODEL_NAMES: List[str] = [220 "ols_linear",221 "ols_log",222 "ols_semilog",223 "robust_huber",224 "robust_bisquare",225 "quantile_10",226 "quantile_25",227 "quantile_50",228 "quantile_75",229 "quantile_90",230 "spatial_lag",231 "spatial_error",232 "gwr",233 "rf_regressor",234 "gradient_boosting",235 "xgboost",236 "lasso",237 "ridge",238 "elastic_net",239]240241# ---------------------------------------------------------------------------242# 9. Reproducibility243# ---------------------------------------------------------------------------244245RANDOM_SEED: int = 42246TEST_SIZE: float = 0.20247CV_FOLDS: int = 5248249# ---------------------------------------------------------------------------250# 10. Visualisation palettes & figure settings251# ---------------------------------------------------------------------------252253# Colour palettes254PALETTE_SEQUENTIAL: str = "YlOrRd"255PALETTE_DIVERGING: str = "RdBu_r"256PALETTE_CATEGORICAL: List[str] = [257 "#1f77b4", # muted blue258 "#ff7f0e", # safety orange259 "#2ca02c", # cooked-asparagus green260 "#d62728", # brick red261 "#9467bd", # muted purple262 "#8c564b", # chestnut brown263 "#e377c2", # raspberry-yoghurt pink264 "#7f7f7f", # middle grey265 "#bcbd22", # curry yellow-green266 "#17becf", # blue-teal267]268269PALETTE_PROPERTY_TYPE: Dict[str, str] = {270 "Rental unit": "#1f77b4",271 "House": "#2ca02c",272 "Cabin/Chalet": "#8c564b",273 "Condo": "#ff7f0e",274 "Apartment": "#9467bd",275 "Other": "#7f7f7f",276}277278PALETTE_PRICE_CATEGORY: Dict[str, str] = {279 "Budget (<$100)": "#2ca02c",280 "Moderate ($100-200)": "#1f77b4",281 "Premium ($200-400)": "#ff7f0e",282 "Luxury ($400-1000)": "#d62728",283 "Ultra-Luxury (>$1000)": "#9467bd",284 "Unknown": "#7f7f7f",285}286287# Figure defaults288FIGURE_DPI: int = 300289FIGURE_FORMAT: str = "pdf"290FIGURE_SIZE_SINGLE: Tuple[float, float] = (6.5, 4.5)291FIGURE_SIZE_DOUBLE: Tuple[float, float] = (13.0, 4.5)292FIGURE_SIZE_FULL: Tuple[float, float] = (13.0, 9.0)293FIGURE_FONT_FAMILY: str = "serif"294FIGURE_FONT_SIZE: int = 11295296# ---------------------------------------------------------------------------297# 11. LaTeX table formatting298# ---------------------------------------------------------------------------299300@dataclass(frozen=True)301class LaTeXSettings:302 """Settings for stargazer-style LaTeX table output."""303 float_format: str = "%.4f"304 significance_levels: Tuple[float, ...] = (0.01, 0.05, 0.10)305 significance_symbols: Tuple[str, ...] = ("***", "**", "*")306 caption_position: str = "top"307 label_prefix: str = "tab:"308 font_size: str = "\\footnotesize"309 column_sep: str = " & "310 booktabs: bool = True311 threeparttable: bool = True312 note_width: str = "0.95\\linewidth"313314LATEX: LaTeXSettings = LaTeXSettings()315316# ---------------------------------------------------------------------------317# 12. Logging318# ---------------------------------------------------------------------------319320LOG_LEVEL: str = os.environ.get("HEDONIC_LOG_LEVEL", "INFO")321LOG_FORMAT: str = "%(asctime)s | %(name)-20s | %(levelname)-8s | %(message)s"322LOG_DATE_FORMAT: str = "%Y-%m-%d %H:%M:%S"323