Python 64.6%
TeX 23.3%
HTML 11.6%
Makefile 0.5%
1#!/usr/bin/env python32"""Master pipeline for Airbnb hedonic pricing analysis."""34from __future__ import annotations56import argparse7import logging8import sys9import time10import traceback11from contextlib import contextmanager12from pathlib import Path13from typing import Any, Dict, List, Optional1415import numpy as np16import pandas as pd1718PROJECT_ROOT = Path(__file__).resolve().parent.parent19if str(PROJECT_ROOT) not in sys.path:20 sys.path.insert(0, str(PROJECT_ROOT))2122from src import config2324logger = logging.getLogger("run_analysis")252627def _setup_logging(output_dir: Path) -> None:28 output_dir.mkdir(parents=True, exist_ok=True)29 root = logging.getLogger()30 root.setLevel(logging.DEBUG)31 root.handlers.clear()32 fmt = logging.Formatter(config.LOG_FORMAT, datefmt=config.LOG_DATE_FORMAT)33 ch = logging.StreamHandler(sys.stdout)34 ch.setLevel(logging.INFO)35 ch.setFormatter(fmt)36 root.addHandler(ch)37 fh = logging.FileHandler(str(output_dir / "analysis.log"), mode="w", encoding="utf-8")38 fh.setLevel(logging.DEBUG)39 fh.setFormatter(fmt)40 root.addHandler(fh)414243@contextmanager44def step_timer(step_name: str, timings: Dict[str, float]):45 logger.info("=" * 72)46 logger.info("STEP START : %s", step_name)47 logger.info("=" * 72)48 t0 = time.perf_counter()49 try:50 yield51 finally:52 elapsed = time.perf_counter() - t053 timings[step_name] = elapsed54 logger.info("STEP END : %s [%.2f s]", step_name, elapsed)555657# ---------------------------------------------------------------------------58# Step 1: Load data59# ---------------------------------------------------------------------------60def step_load_data(data_file: Path) -> pd.DataFrame:61 logger.info("Loading data from %s", data_file)62 df = pd.read_csv(data_file)63 logger.info("Loaded %d rows x %d cols", *df.shape)64 return df656667# ---------------------------------------------------------------------------68# Step 2: Clean prices69# ---------------------------------------------------------------------------70def step_clean_prices(df: pd.DataFrame) -> pd.DataFrame:71 from src.cleaning import run_full_cleaning, create_price_variants72 try:73 df = run_full_cleaning(df)74 logger.info("Full cleaning complete: %d rows, %d cols", *df.shape)75 except Exception:76 logger.warning("run_full_cleaning failed, applying fallback", exc_info=True)77 if "price_numeric" in df.columns:78 df = df[df["price_numeric"].notna()].copy()79 df = df[(df["price_numeric"] >= config.PRICE_FLOOR) &80 (df["price_numeric"] <= config.PRICE_CEILING)].copy()81 df["log_price"] = np.log(df["price_numeric"])82 logger.info("Fallback cleaning: %d rows remain", len(df))83 return df848586# ---------------------------------------------------------------------------87# Step 3: Feature engineering88# ---------------------------------------------------------------------------89def step_engineer_features(df: pd.DataFrame, *, skip_nlp: bool = False) -> pd.DataFrame:90 # Ensure log_price exists91 if "log_price" not in df.columns and "price_numeric" in df.columns:92 df["log_price"] = np.log(df["price_numeric"].clip(lower=1))9394 # 3a. Core features95 try:96 from src.feature_engineering import create_all_features97 df = create_all_features(df)98 logger.info("Core features: %d cols", df.shape[1])99 except Exception:100 logger.warning("feature_engineering failed", exc_info=True)101102 # 3b. Amenity indices103 try:104 from src.amenities import create_amenity_indices105 df = create_amenity_indices(df)106 logger.info("Amenity indices added: %d cols", df.shape[1])107 except Exception:108 logger.warning("amenity indices failed", exc_info=True)109110 # 3c. Geospatial features111 try:112 from src.geospatial import compute_distance_features113 df = compute_distance_features(df)114 logger.info("Geospatial features: %d cols", df.shape[1])115 except Exception:116 logger.warning("geospatial features failed", exc_info=True)117118 # 3d. NLP features119 if not skip_nlp:120 try:121 from src.nlp_features import create_all_nlp_features122 df, price_words = create_all_nlp_features(df)123 logger.info("NLP features: %d cols", df.shape[1])124 if price_words is not None:125 price_words.to_csv(config.OUTPUTS_DIR / "tables" / "price_associated_words.csv", index=False)126 logger.info("Price-associated words saved")127 except Exception:128 logger.warning("NLP features failed", exc_info=True)129130 return df131132133# ---------------------------------------------------------------------------134# Step 4: Hedonic models135# ---------------------------------------------------------------------------136def step_hedonic_models(df: pd.DataFrame) -> Dict[str, Any]:137 from src.hedonic_models import build_model_sequence, compare_models, compute_price_premiums138 from src.tables import export_table139140 y_col = "log_price"141 if y_col not in df.columns:142 logger.warning("No log_price column; skipping hedonic models")143 return {}144145 results = build_model_sequence(df, y_col=y_col)146 logger.info("Hedonic models estimated: %s", list(results.keys()))147148 if results:149 comparison = compare_models(results)150 export_table(comparison, "model_comparison", output_dir=str(config.OUTPUTS_DIR / "tables"))151 logger.info("Model comparison table:\n%s", comparison.to_string())152153 # Price premiums from full model154 full_key = [k for k in results.keys() if "Full" in k or "9" in k]155 if full_key:156 premiums = compute_price_premiums(results[full_key[-1]])157 export_table(premiums, "price_premiums", output_dir=str(config.OUTPUTS_DIR / "tables"))158 logger.info("Price premiums:\n%s", premiums.to_string())159160 return results161162163# ---------------------------------------------------------------------------164# Step 5: Quantile models165# ---------------------------------------------------------------------------166def step_quantile_models(df: pd.DataFrame, x_cols: List[str]) -> Dict[str, Any]:167 from src.quantile_models import estimate_quantile_sequence, compare_quantile_coefficients168169 y_col = "log_price"170 if y_col not in df.columns or not x_cols:171 logger.warning("No log_price or no x_cols; skipping quantile models")172 return {}173174 results = estimate_quantile_sequence(df, y_col, x_cols)175 logger.info("Quantile models estimated: taus=%s", list(results.keys()))176177 if results:178 coef_df = compare_quantile_coefficients(results)179 coef_df.to_csv(config.OUTPUTS_DIR / "tables" / "quantile_coefficients.csv", index=False)180181 return results182183184# ---------------------------------------------------------------------------185# Step 6: ML models186# ---------------------------------------------------------------------------187def step_ml_models(df: pd.DataFrame, x_cols: List[str]) -> Dict[str, Any]:188 from src.ml_models import prepare_ml_data, train_all_models, compare_all_models189 from src.tables import export_table190191 y_col = "log_price"192 if y_col not in df.columns or not x_cols:193 logger.warning("No log_price or no x_cols; skipping ML models")194 return {}195196 X_train, X_test, y_train, y_test, feat_names = prepare_ml_data(df, y_col, x_cols)197 logger.info("ML data prepared: train=%d, test=%d, features=%d", len(X_train), len(X_test), len(feat_names))198199 models = train_all_models(X_train, y_train)200 logger.info("ML models trained: %s", list(models.keys()))201202 if models:203 comparison = compare_all_models(models, X_test, y_test)204 export_table(comparison, "ml_comparison", output_dir=str(config.OUTPUTS_DIR / "tables"))205 logger.info("ML comparison:\n%s", comparison.to_string())206207 return {208 "models": models,209 "X_train": X_train, "X_test": X_test,210 "y_train": y_train, "y_test": y_test,211 "feature_names": feat_names,212 }213214215# ---------------------------------------------------------------------------216# Step 7: SHAP values217# ---------------------------------------------------------------------------218def step_shap_analysis(ml_data: Dict[str, Any]) -> Dict[str, Any]:219 from src.shap_analysis import compute_shap_values, plot_shap_summary, get_shap_importance, plot_partial_dependence220221 if not ml_data or "models" not in ml_data:222 logger.warning("No ML models; skipping SHAP")223 return {}224225 models = ml_data["models"]226 X_test = ml_data["X_test"]227 feat_names = ml_data["feature_names"]228 fig_dir = str(config.OUTPUTS_DIR / "figures")229 results = {}230231 # Use best tree-based model for SHAP232 for model_name in ["xgboost", "lightgbm", "gradient_boosting", "random_forest"]:233 if model_name in models:234 model = models[model_name]235 try:236 shap_vals = compute_shap_values(model, X_test, feat_names)237 plot_shap_summary(shap_vals, X_test, feat_names, f"{fig_dir}/10_shap_summary")238 importance_df = get_shap_importance(shap_vals, feat_names)239 importance_df.to_csv(config.OUTPUTS_DIR / "tables" / "shap_importance.csv", index=False)240 logger.info("SHAP analysis complete for %s", model_name)241242 # Partial dependence plots243 key_features = ["bedrooms", "bathrooms", "guests_count", "distance_to_center", "rating"]244 available_features = [f for f in key_features if f in feat_names]245 if available_features:246 feat_indices = [feat_names.index(f) for f in available_features]247 try:248 plot_partial_dependence(model, X_test, feat_indices, feat_names,249 f"{fig_dir}/11_partial_dependence")250 except Exception:251 logger.warning("PDP plot failed", exc_info=True)252253 results["shap_values"] = shap_vals254 results["model_name"] = model_name255 results["importance"] = importance_df256 break257 except Exception:258 logger.warning("SHAP failed for %s", model_name, exc_info=True)259260 return results261262263# ---------------------------------------------------------------------------264# Step 8: Spatial models265# ---------------------------------------------------------------------------266def step_spatial_models(df: pd.DataFrame, x_cols: List[str]) -> Dict[str, Any]:267 from src.spatial_models import (build_weights_matrix, estimate_spatial_lag,268 estimate_spatial_error, estimate_spatial_durbin,269 test_spatial_autocorrelation, compare_spatial_models)270271 y_col = "log_price"272 if y_col not in df.columns or not x_cols:273 logger.warning("No log_price or no x_cols; skipping spatial models")274 return {}275276 lat_col = "lat" if "lat" in df.columns else "latitude"277 lon_col = "long" if "long" in df.columns else "longitude"278279 # Filter to rows with valid coords, price, and all x_cols280 valid_cols = [y_col, lat_col, lon_col] + [c for c in x_cols if c in df.columns]281 sp_df = df[valid_cols].dropna().copy()282 logger.info("Spatial analysis: %d rows after dropping NaN", len(sp_df))283284 if len(sp_df) < 50:285 logger.warning("Too few rows for spatial models (%d)", len(sp_df))286 return {}287288 try:289 w = build_weights_matrix(sp_df, k=5, lat_col=lat_col, lon_col=lon_col)290 logger.info("Spatial weights matrix built (k=5, n=%d)", len(sp_df))291 except Exception:292 logger.warning("Failed to build spatial weights", exc_info=True)293 return {}294295 try:296 moran_result = test_spatial_autocorrelation(sp_df, y_col, w)297 logger.info("Moran's I test: %s", moran_result)298 except Exception:299 logger.warning("Moran's I test failed", exc_info=True)300301 results = {}302 sp_x_cols = [c for c in x_cols if c in sp_df.columns]303 for name, func in [("spatial_lag", estimate_spatial_lag),304 ("spatial_error", estimate_spatial_error),305 ("spatial_durbin", estimate_spatial_durbin)]:306 try:307 result = func(sp_df, y_col, sp_x_cols, w)308 results[name] = result309 logger.info(" %s estimated successfully", name)310 except Exception:311 logger.warning(" %s failed", name, exc_info=True)312313 if results:314 try:315 comp = compare_spatial_models(results)316 comp.to_csv(config.OUTPUTS_DIR / "tables" / "spatial_comparison.csv")317 except Exception:318 logger.warning("compare_spatial_models failed", exc_info=True)319320 return results321322323# ---------------------------------------------------------------------------324# Step 9: Robustness checks325# ---------------------------------------------------------------------------326def step_robustness(df: pd.DataFrame, x_cols: List[str]) -> Dict[str, Any]:327 from src.robustness import run_all_robustness, format_robustness_summary328 from src.tables import export_table329330 y_col = "log_price"331 if y_col not in df.columns or not x_cols:332 return {}333334 results = run_all_robustness(df, y_col, x_cols)335 logger.info("Robustness checks: %s", list(results.keys()))336337 summary = format_robustness_summary(results)338 export_table(summary, "robustness_summary", output_dir=str(config.OUTPUTS_DIR / "tables"))339340 return results341342343# ---------------------------------------------------------------------------344# Step 10: Generate tables345# ---------------------------------------------------------------------------346def step_generate_tables(df: pd.DataFrame, hedonic_results: Dict, quantile_results: Dict,347 ml_data: Dict, spatial_results: Dict) -> List[str]:348 from src.tables import (summary_statistics_table, price_by_room_type_table,349 correlation_matrix_table, regression_comparison_table,350 export_table)351352 generated = []353 out = str(config.OUTPUTS_DIR / "tables")354355 # Table 1: Summary stats356 try:357 numeric_cols = [c for c in df.select_dtypes(include=[np.number]).columns358 if c not in ("lat", "long", "price_segment_code")]359 tbl = summary_statistics_table(df, numeric_cols)360 export_table(tbl, "table_01_summary_statistics", output_dir=out,361 caption="Summary Statistics", label="tab:summary")362 generated.append("summary_statistics")363 logger.info(" Table 1: summary statistics")364 except Exception:365 logger.warning(" Table 1 failed", exc_info=True)366367 # Table 2: Price by property type368 try:369 tbl = price_by_room_type_table(df)370 export_table(tbl, "table_02_price_by_type", output_dir=out,371 caption="Price Distribution by Property Type", label="tab:pricetype")372 generated.append("price_by_type")373 logger.info(" Table 2: price by type")374 except Exception:375 logger.warning(" Table 2 failed", exc_info=True)376377 # Table 3: Correlation matrix378 try:379 corr_cols = ["log_price", "bedrooms", "bathrooms", "guests_count",380 "amenities_count", "rating", "num_reviews"]381 corr_cols = [c for c in corr_cols if c in df.columns]382 tbl = correlation_matrix_table(df, corr_cols)383 export_table(tbl, "table_03_correlations", output_dir=out,384 caption="Correlation Matrix", label="tab:corr")385 generated.append("correlations")386 logger.info(" Table 3: correlations")387 except Exception:388 logger.warning(" Table 3 failed", exc_info=True)389390 # Table 4-5: Regression results391 if hedonic_results:392 try:393 tbl = regression_comparison_table(hedonic_results)394 export_table(tbl, "table_04_hedonic_regressions", output_dir=out,395 caption="Hedonic Regression Results", label="tab:hedonic")396 generated.append("hedonic_regressions")397 logger.info(" Table 4-5: hedonic regressions")398 except Exception:399 logger.warning(" Table 4-5 failed", exc_info=True)400401 return generated402403404# ---------------------------------------------------------------------------405# Step 11: Generate figures406# ---------------------------------------------------------------------------407def step_generate_figures(df: pd.DataFrame, hedonic_results: Dict,408 quantile_results: Dict, ml_data: Dict) -> List[str]:409 from src.figures import generate_all_figures410411 fig_dir = str(config.OUTPUTS_DIR / "figures")412 try:413 result = generate_all_figures(df, results=hedonic_results, save_dir=fig_dir)414 generated = []415 for fmt_files in result.values():416 generated.extend(fmt_files)417 logger.info("Figures generated: %d files", len(generated))418 return [str(f) for f in generated]419 except Exception:420 logger.warning("generate_all_figures failed", exc_info=True)421 return []422423424# ---------------------------------------------------------------------------425# Step 12: Price decomposition426# ---------------------------------------------------------------------------427def step_decomposition(df: pd.DataFrame, hedonic_results: Dict,428 feature_groups: Dict[str, List[str]]) -> Dict[str, Any]:429 from src.report import explain_listing_price, decompose_all_prices, generate_decomposition_summary430431 if not hedonic_results:432 logger.warning("No hedonic models for decomposition")433 return {}434435 # Use the fullest model available436 model_key = list(hedonic_results.keys())[-1]437 model = hedonic_results[model_key]438 logger.info("Decomposing prices with model: %s", model_key)439440 try:441 decomp_df = decompose_all_prices(model, df, feature_groups)442 summary = generate_decomposition_summary(decomp_df)443 decomp_df.to_csv(config.OUTPUTS_DIR / "tables" / "price_decomposition.csv", index=False)444 summary.to_csv(config.OUTPUTS_DIR / "tables" / "decomposition_summary.csv")445446 # Example listing447 sample = explain_listing_price(model, df, 0, feature_groups)448 logger.info("Sample decomposition: actual=%.0f, predicted=%.2f",449 sample.get("actual_price", 0), sample.get("predicted_price", 0))450451 return {"decomp_df": decomp_df, "summary": summary, "model_used": model_key}452 except Exception:453 logger.warning("Decomposition failed", exc_info=True)454 return {}455456457# ---------------------------------------------------------------------------458# Feature group & column resolution459# ---------------------------------------------------------------------------460def resolve_feature_groups(df: pd.DataFrame) -> Dict[str, List[str]]:461 try:462 from src.feature_engineering import get_feature_groups463 groups = get_feature_groups()464 except Exception:465 groups = {}466467 filtered = {}468 for name, cols in groups.items():469 present = [c for c in cols if c in df.columns]470 if present:471 filtered[name] = present472 return filtered473474475def get_x_cols(df: pd.DataFrame, feature_groups: Dict[str, List[str]]) -> List[str]:476 exclude = {"log_price", "price_numeric", "price_category", "price_segment",477 "price_segment_code", "price_per_guest", "price_per_bedroom",478 "name", "city", "property_type", "rating_category", "lat", "long"}479 x_cols = []480 for cols in feature_groups.values():481 for c in cols:482 if c not in exclude and c not in x_cols and c in df.columns:483 x_cols.append(c)484485 # Also add important columns if present486 extras = ["bedrooms", "bathrooms", "guests_count", "amenities_count",487 "is_superhost", "rating", "num_reviews", "num_images",488 "is_guest_favorite", "pets_allowed"]489 for c in extras:490 if c in df.columns and c not in x_cols:491 x_cols.append(c)492493 return x_cols494495496# ---------------------------------------------------------------------------497# Summary report498# ---------------------------------------------------------------------------499def print_summary(df, hedonic, quantile, ml_data, spatial, timings,500 tables_gen, figures_gen, decomp):501 sep = "=" * 72502 lines = ["", sep, "HEDONIC PRICING ANALYSIS — SUMMARY REPORT", sep, ""]503 lines.append(f" Dataset: {len(df):,} rows x {df.shape[1]} cols")504 lines.append("")505506 lines.append(" HEDONIC MODELS:")507 if hedonic:508 for k in hedonic:509 r2 = hedonic[k].rsquared if hasattr(hedonic[k], "rsquared") else "?"510 lines.append(f" {k}: R²={r2:.4f}" if isinstance(r2, float) else f" {k}")511 else:512 lines.append(" (none)")513514 lines.append(f" QUANTILE MODELS: {len(quantile)} estimated")515516 ml_models = ml_data.get("models", {}) if ml_data else {}517 lines.append(f" ML MODELS: {list(ml_models.keys()) if ml_models else '(none)'}")518 lines.append(f" SPATIAL MODELS: {list(spatial.keys()) if spatial else '(none)'}")519 lines.append("")520 lines.append(f" Tables: {len(tables_gen)} | Figures: {len(figures_gen)}")521522 if decomp:523 lines.append(f" Decomposition: {decomp.get('model_used', '?')}")524525 lines.append("")526 lines.append(" TIMING:")527 for step, t in timings.items():528 lines.append(f" {step:<40s} {t:>7.2f}s")529 lines.append(f" {'TOTAL':<40s} {sum(timings.values()):>7.2f}s")530 lines.append(sep)531532 report = "\n".join(lines)533 logger.info(report)534535536# ---------------------------------------------------------------------------537# CLI538# ---------------------------------------------------------------------------539def parse_args(argv=None):540 p = argparse.ArgumentParser(description="Airbnb hedonic pricing pipeline")541 p.add_argument("--data-file", type=Path, default=Path("cleaned_dataset_sample.csv"))542 p.add_argument("--output-dir", type=Path, default=config.OUTPUTS_DIR)543 p.add_argument("--skip-spatial", action="store_true")544 p.add_argument("--skip-nlp", action="store_true")545 p.add_argument("--skip-ml", action="store_true")546 p.add_argument("--models-only", action="store_true")547 p.add_argument("--figures-only", action="store_true")548 return p.parse_args(argv)549550551# ---------------------------------------------------------------------------552# Main553# ---------------------------------------------------------------------------554def main(argv=None) -> int:555 args = parse_args(argv)556 _setup_logging(args.output_dir)557558 timings: Dict[str, float] = {}559 df = pd.DataFrame()560 hedonic_results: Dict[str, Any] = {}561 quantile_results: Dict[str, Any] = {}562 ml_data: Dict[str, Any] = {}563 shap_results: Dict[str, Any] = {}564 spatial_results: Dict[str, Any] = {}565 tables_gen: List[str] = []566 figures_gen: List[str] = []567 decomp_info: Dict[str, Any] = {}568569 logger.info("Pipeline started. data=%s output=%s", args.data_file, args.output_dir)570 logger.info("Flags: skip_spatial=%s skip_nlp=%s skip_ml=%s models_only=%s figures_only=%s",571 args.skip_spatial, args.skip_nlp, args.skip_ml, args.models_only, args.figures_only)572573 try:574 # Step 1575 with step_timer("1. Load data", timings):576 df = step_load_data(args.data_file)577578 # Step 2579 with step_timer("2. Clean prices", timings):580 df = step_clean_prices(df)581582 # Step 3583 with step_timer("3. Feature engineering", timings):584 df = step_engineer_features(df, skip_nlp=args.skip_nlp)585586 feature_groups = resolve_feature_groups(df)587 x_cols = get_x_cols(df, feature_groups)588 logger.info("Feature groups: %s", {k: len(v) for k, v in feature_groups.items()})589 logger.info("Total x_cols for modeling: %d", len(x_cols))590591 # Ensure boolean columns are numeric592 for c in df.columns:593 if df[c].dtype == bool:594 df[c] = df[c].astype(int)595596 if args.figures_only:597 with step_timer("11. Generate figures", timings):598 figures_gen = step_generate_figures(df, hedonic_results, quantile_results, ml_data)599 print_summary(df, hedonic_results, quantile_results, ml_data,600 spatial_results, timings, tables_gen, figures_gen, decomp_info)601 return 0602603 # Step 4604 with step_timer("4. Hedonic models", timings):605 hedonic_results = step_hedonic_models(df)606607 # Step 5608 with step_timer("5. Quantile models", timings):609 # Use structural + key cols for quantile models610 qx = [c for c in x_cols if c in df.columns][:15]611 quantile_results = step_quantile_models(df, qx)612613 # Step 6614 if not args.skip_ml:615 with step_timer("6. ML models", timings):616 ml_data = step_ml_models(df, x_cols)617 else:618 logger.info("ML models skipped")619 timings["6. ML models"] = 0.0620621 # Step 7622 if not args.skip_ml and ml_data:623 with step_timer("7. SHAP values", timings):624 shap_results = step_shap_analysis(ml_data)625 else:626 timings["7. SHAP values"] = 0.0627628 # Step 8629 if not args.skip_spatial:630 with step_timer("8. Spatial models", timings):631 sp_cols = [c for c in x_cols if c in df.columns][:10]632 spatial_results = step_spatial_models(df, sp_cols)633 else:634 timings["8. Spatial models"] = 0.0635636 # Step 9637 with step_timer("9. Robustness checks", timings):638 rob_cols = [c for c in ["bedrooms", "bathrooms", "guests_count",639 "amenities_count", "is_superhost", "rating",640 "num_reviews", "is_entire_home"] if c in df.columns]641 try:642 step_robustness(df, rob_cols)643 except Exception:644 logger.warning("Robustness checks failed", exc_info=True)645646 if args.models_only:647 print_summary(df, hedonic_results, quantile_results, ml_data,648 spatial_results, timings, tables_gen, figures_gen, decomp_info)649 return 0650651 # Step 10652 with step_timer("10. Generate tables", timings):653 tables_gen = step_generate_tables(df, hedonic_results, quantile_results,654 ml_data, spatial_results)655656 # Step 11657 with step_timer("11. Generate figures", timings):658 figures_gen = step_generate_figures(df, hedonic_results, quantile_results, ml_data)659660 # Step 12661 with step_timer("12. Price decomposition", timings):662 decomp_info = step_decomposition(df, hedonic_results, feature_groups)663664 # Step 13665 with step_timer("13. Summary report", timings):666 print_summary(df, hedonic_results, quantile_results, ml_data,667 spatial_results, timings, tables_gen, figures_gen, decomp_info)668669 logger.info("Pipeline finished successfully in %.2f s", sum(timings.values()))670 return 0671672 except KeyboardInterrupt:673 logger.warning("Interrupted by user")674 return 130675 except Exception as exc:676 logger.error("Pipeline failed: %s", exc, exc_info=True)677 print_summary(df, hedonic_results, quantile_results, ml_data,678 spatial_results, timings, tables_gen, figures_gen, decomp_info)679 return 1680681682if __name__ == "__main__":683 sys.exit(main())684