SPB Git forge

spb/wp4_uqo

Public
3commits 1branches 1releases
5.4 MBsize
maindefault branch
1 h agolast push
Python 64.6% TeX 23.3% HTML 11.6% Makefile 0.5%
11.2 KB · 323 lines python
Raw Blame History
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