Table of Contents

    House Price Prediction

    Machine Learning Project

    House Price Prediction

    Build an end-to-end Machine Learning regression project that predicts residential property prices from numerical and categorical features using reproducible preprocessing, baseline comparison, Linear Regression, Random Forest, error analysis, and model persistence.

    Project Introduction

    House price prediction is a supervised Machine Learning problem in which a model learns relationships between property attributes and observed sale prices.

    A property may be represented by its living area, number of rooms, age, condition, location, parking capacity, and other characteristics. The model uses those inputs to estimate a continuous numerical target: the expected property price.

    Although house price prediction is often introduced as a simple regression exercise, a reliable project requires careful attention to data quality, missing values, categorical encoding, location, changing market conditions, leakage, unusual properties, evaluation design, uncertainty, and production monitoring.

    Project goal: Train and evaluate a reproducible regression pipeline that estimates house prices while preserving preprocessing consistency and preventing information leakage.

    Learning Objectives

    By the End of This Project

    • Define house price prediction as a regression problem
    • Identify numerical, categorical, and target variables
    • Inspect data types, distributions, and missing values
    • Recognize target leakage
    • Create training and test partitions
    • Build a median-value baseline
    • Impute missing numerical and categorical values
    • Encode categorical variables using one-hot encoding
    • Use ColumnTransformer and Pipeline
    • Train a Linear Regression model
    • Train a Random Forest regression model
    • Evaluate models using MAE, RMSE, and \(R^2\)
    • Interpret residuals and large prediction errors
    • Inspect model coefficients and feature importance
    • Perform cross-validation correctly
    • Avoid preprocessing leakage
    • Save the complete fitted pipeline
    • Reload the pipeline for inference
    • Validate incoming prediction data
    • Document limitations and responsible-use requirements
    • Define monitoring for a deployed regression model

    Prerequisites

    Recommended Knowledge

    • Python variables, functions, classes, and modules
    • NumPy arrays
    • pandas DataFrames
    • Matplotlib and Seaborn
    • Missing-value handling
    • Categorical encoding
    • Train-test splitting
    • Feature engineering
    • Linear Regression
    • Regression evaluation metrics
    • Overfitting and underfitting
    • Basic scikit-learn pipelines

    Required Packages

    • pandas
    • NumPy
    • Matplotlib
    • Seaborn
    • scikit-learn
    • joblib
    python -m pip install pandas numpy matplotlib seaborn scikit-learn joblib

    Problem Definition

    Let each property contain a feature vector \(x_i\), and let its observed sale price be \(y_i\). The regression model learns a function that estimates price from the available property features.

    Regression Objective
    \[ \hat{y}_i=f(x_i) \]
    Project Element Definition
    Input Property characteristics available at prediction time
    Target Observed sale price
    Task Type Supervised regression
    Prediction Estimated property price
    Primary Evaluation Errors on unseen properties
    Unit of Analysis One property sale or valuation record
    Decision requirement: Define whether the model is estimating sale price, asking price, rental value, appraised value, or another target. These values are not interchangeable.

    Expected Dataset

    Save an authorized housing dataset as housing.csv. The examples below expect the following learning schema.

    Column Type Description
    price Numerical Observed property price and prediction target
    area_sqft Numerical Property living area in square feet
    bedrooms Numerical Number of bedrooms
    bathrooms Numerical Number of bathrooms
    property_age Numerical Age of the property in years
    lot_area_sqft Numerical Total plot or lot area
    parking_spaces Numerical Number of parking spaces
    location Categorical Neighborhood, district, or locality
    property_type Categorical Apartment, detached house, townhouse, or another type
    condition Categorical Recorded property-condition category
    Data notice: The article does not manufacture housing records. Use a properly licensed, authorized, and documented dataset. If your column names differ, update the schema lists in the code.

    Data Governance

    Review Before Modeling

    • Confirm that the dataset is authorized for the intended use
    • Record the data source and collection period
    • Remove unnecessary personal identifiers
    • Restrict access to confidential transaction information
    • Review retention and deletion requirements
    • Document currency and measurement units
    • Identify duplicate or repeated property records
    • Document how missing and corrected values were produced
    • Review whether geography creates fairness or legal concerns
    • Preserve the dataset version used for training

    Load the Dataset

    from pathlib import Path
    
    import numpy as np
    import pandas as pd
    
    
    data_path = Path(
        "housing.csv"
    )
    
    
    if not data_path.exists():
    
        raise FileNotFoundError(
            "housing.csv was not found. "
            "Add an authorized dataset before running the project."
        )
    
    
    housing = pd.read_csv(
        data_path
    )
    
    
    print(
        housing.head()
    )
    
    print(
        housing.shape
    )
    
    print(
        housing.dtypes
    )

    Validate the Required Schema

    target_column = "price"
    
    
    numerical_features = [
        "area_sqft",
        "bedrooms",
        "bathrooms",
        "property_age",
        "lot_area_sqft",
        "parking_spaces"
    ]
    
    
    categorical_features = [
        "location",
        "property_type",
        "condition"
    ]
    
    
    required_columns = {
        target_column,
        *numerical_features,
        *categorical_features
    }
    
    
    missing_columns = (
        required_columns
        - set(housing.columns)
    )
    
    
    if missing_columns:
    
        raise ValueError(
            "The dataset is missing required columns: "
            f"{sorted(missing_columns)}"
        )
    
    
    housing = housing[
        [
            *numerical_features,
            *categorical_features,
            target_column
        ]
    ].copy()
    
    
    housing[target_column] = pd.to_numeric(
        housing[target_column],
        errors="coerce"
    )
    
    
    housing = housing.dropna(
        subset=[
            target_column
        ]
    )
    
    
    if housing.empty:
    
        raise ValueError(
            "No usable rows remain after validating the target."
        )
    
    
    if (
        housing[target_column]
        <=
        0
    ).any():
    
        raise ValueError(
            "The price target contains zero or negative values. "
            "Review the source records before training."
        )

    Initial Data Audit

    print(
        housing.info()
    )
    
    
    print(
        housing.describe(
            include="all"
        ).transpose()
    )
    
    
    missing_summary = (
        housing
        .isna()
        .sum()
        .sort_values(
            ascending=False
        )
    )
    
    
    duplicate_count = housing.duplicated().sum()
    
    
    print(
        "Missing values:"
    )
    
    print(
        missing_summary
    )
    
    
    print(
        "Complete duplicate rows:",
        duplicate_count
    )

    Audit Questions

    • Are prices represented in one currency?
    • Are all area measurements expressed in the same unit?
    • Are room counts plausible?
    • Are property ages valid?
    • Are locations spelled consistently?
    • Do repeated records represent duplicates or separate sales?
    • Are missing values random or systematically concentrated?
    • Are any columns created after the sale occurred?

    Handle Duplicate Records Carefully

    Complete duplicate rows may represent accidental duplication, but repeated properties are not automatically duplicates. The same property may be sold more than once.

    complete_duplicates = housing[
        housing.duplicated(
            keep=False
        )
    ]
    
    
    print(
        complete_duplicates.head()
    )
    Unsafe Cleaning Remove every repeated address or property identifier without checking whether the records represent different legitimate sales.
    Better Practice Review property identity, transaction date, sale status, and source records before deleting repeated observations.

    Explore the Target Distribution

    import matplotlib.pyplot as plt
    import seaborn as sns
    
    
    plt.figure(
        figsize=(10, 5)
    )
    
    
    sns.histplot(
        data=housing,
        x=target_column,
        kde=True
    )
    
    
    plt.title(
        "House Price Distribution"
    )
    
    plt.xlabel(
        "Observed Price"
    )
    
    plt.ylabel(
        "Property Count"
    )
    
    plt.tight_layout()
    
    plt.show()

    Inspect the Distribution For

    • Strong right skew
    • Suspiciously repeated prices
    • Extreme luxury properties
    • Zero or negative values
    • Mixed currencies
    • Different market periods
    • Unusual clusters caused by location

    Explore Feature Relationships

    plt.figure(
        figsize=(9, 6)
    )
    
    
    sns.scatterplot(
        data=housing,
        x="area_sqft",
        y=target_column,
        hue="property_type",
        alpha=0.7
    )
    
    
    plt.title(
        "Living Area and House Price"
    )
    
    plt.xlabel(
        "Living Area in Square Feet"
    )
    
    plt.ylabel(
        "Observed Price"
    )
    
    plt.tight_layout()
    
    plt.show()
    numerical_analysis_columns = [
        *numerical_features,
        target_column
    ]
    
    
    correlation_matrix = (
        housing[
            numerical_analysis_columns
        ]
        .corr(
            numeric_only=True
        )
    )
    
    
    plt.figure(
        figsize=(10, 7)
    )
    
    
    sns.heatmap(
        correlation_matrix,
        annot=True,
        fmt=".2f",
        cmap="coolwarm"
    )
    
    
    plt.title(
        "Numerical Feature Correlation"
    )
    
    plt.tight_layout()
    
    plt.show()
    Interpretation caution: Correlation describes association in the available data. It does not prove that changing a feature will cause the price to change by the observed amount.

    Prevent Target Leakage

    Target leakage occurs when a feature contains information that would not be available when the prediction must be made or contains a direct transformation of the target.

    Potential Leakage Feature Why It Is Risky
    Final sale amount component Directly reveals part of the target
    Post-sale tax assessment May be created after the transaction
    Negotiated discount from final price Depends on knowing the final price
    Appraisal performed after sale May incorporate the observed transaction
    Future neighborhood statistic Uses information unavailable at prediction time
    Dataset-wide target encoding Can leak validation or test targets into features
    Leakage Check For every feature, ask whether its value would be available at the exact moment the model is expected to make a prediction.

    Choose an Evaluation Split

    A random split is appropriate only when it represents the intended prediction setting. If the model predicts future transactions, a chronological split may provide a more realistic evaluation.

    Split Strategy When It May Be Appropriate
    Random Split Rows are reasonably independent and deployment resembles the same period and market
    Chronological Split The system predicts later sales from earlier sales
    Grouped Split Repeated properties, developments, buildings, or sellers must remain together
    Geographical Holdout The model must generalize to unseen locations
    from sklearn.model_selection import train_test_split
    
    
    features = housing[
        [
            *numerical_features,
            *categorical_features
        ]
    ]
    
    
    target = housing[
        target_column
    ]
    
    
    X_train, X_test, y_train, y_test = train_test_split(
        features,
        target,
        test_size=0.20,
        random_state=42
    )
    
    
    print(
        "Training rows:",
        len(X_train)
    )
    
    print(
        "Test rows:",
        len(X_test)
    )
    Project assumption: The code uses a random holdout for demonstration. Replace it with a time-based, grouped, or geographic split when that better represents the intended deployment.

    Build a Median Baseline

    Before training a complex model, compare it with a simple baseline. The following baseline predicts the median training price for every test property.

    from sklearn.metrics import mean_absolute_error
    from sklearn.metrics import mean_squared_error
    from sklearn.metrics import r2_score
    
    
    baseline_price = y_train.median()
    
    
    baseline_predictions = np.full(
        shape=len(y_test),
        fill_value=baseline_price,
        dtype=float
    )
    
    
    baseline_mae = mean_absolute_error(
        y_test,
        baseline_predictions
    )
    
    
    baseline_rmse = mean_squared_error(
        y_test,
        baseline_predictions
    ) ** 0.5
    
    
    baseline_r2 = r2_score(
        y_test,
        baseline_predictions
    )
    
    
    print(
        "Baseline MAE:",
        baseline_mae
    )
    
    print(
        "Baseline RMSE:",
        baseline_rmse
    )
    
    print(
        "Baseline R2:",
        baseline_r2
    )
    Baseline rule: A trained model should demonstrate meaningful improvement over a simple baseline under the same test set and metrics.

    Build the Preprocessing Pipeline

    from sklearn.compose import ColumnTransformer
    from sklearn.impute import SimpleImputer
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import OneHotEncoder
    from sklearn.preprocessing import StandardScaler
    
    
    numerical_pipeline = Pipeline(
        steps=[
            (
                "imputer",
                SimpleImputer(
                    strategy="median"
                )
            ),
            (
                "scaler",
                StandardScaler()
            )
        ]
    )
    
    
    categorical_pipeline = Pipeline(
        steps=[
            (
                "imputer",
                SimpleImputer(
                    strategy="most_frequent"
                )
            ),
            (
                "encoder",
                OneHotEncoder(
                    handle_unknown="ignore",
                    min_frequency=2
                )
            )
        ]
    )
    
    
    preprocessor = ColumnTransformer(
        transformers=[
            (
                "numerical",
                numerical_pipeline,
                numerical_features
            ),
            (
                "categorical",
                categorical_pipeline,
                categorical_features
            )
        ],
        remainder="drop"
    )
    Leakage prevention: The imputer, scaler, and encoder are fitted only through the training pipeline. Their learned statistics are then applied to the test set without refitting.

    Train Linear Regression

    from sklearn.linear_model import LinearRegression
    
    
    linear_model = Pipeline(
        steps=[
            (
                "preprocessor",
                preprocessor
            ),
            (
                "regressor",
                LinearRegression()
            )
        ]
    )
    
    
    linear_model.fit(
        X_train,
        y_train
    )
    
    
    linear_predictions = linear_model.predict(
        X_test
    )
    Multiple Linear Regression
    \[ \hat{y} = \beta_0 + \beta_1x_1 + \beta_2x_2 + \cdots + \beta_px_p \]

    Train Random Forest Regression

    from sklearn.ensemble import RandomForestRegressor
    
    
    random_forest_model = Pipeline(
        steps=[
            (
                "preprocessor",
                preprocessor
            ),
            (
                "regressor",
                RandomForestRegressor(
                    n_estimators=300,
                    max_depth=None,
                    min_samples_leaf=2,
                    random_state=42,
                    n_jobs=-1
                )
            )
        ]
    )
    
    
    random_forest_model.fit(
        X_train,
        y_train
    )
    
    
    random_forest_predictions = (
        random_forest_model.predict(
            X_test
        )
    )

    Linear Regression

    • Provides an interpretable baseline
    • Models additive linear relationships
    • Can extrapolate beyond observed values
    • Can be sensitive to outliers and multicollinearity

    Random Forest

    • Captures nonlinear relationships
    • Models feature interactions
    • Requires fewer linearity assumptions
    • Usually does not extrapolate reliably beyond observed target patterns

    Regression Metrics

    Mean Absolute Error
    \[ MAE = \frac{1}{n} \sum_{i=1}^{n} \left| y_i-\hat{y}_i \right| \]
    Root Mean Squared Error
    \[ RMSE = \sqrt{ \frac{1}{n} \sum_{i=1}^{n} \left( y_i-\hat{y}_i \right)^2 } \]
    Coefficient of Determination
    \[ R^2 = 1 - \frac{ \sum_{i=1}^{n} \left( y_i-\hat{y}_i \right)^2 }{ \sum_{i=1}^{n} \left( y_i-\bar{y} \right)^2 } \]
    Metric Interpretation Important Limitation
    MAE Average absolute prediction error in price units Treats all absolute errors linearly
    RMSE Gives greater influence to large errors Can be dominated by extreme properties
    \(R^2\) Compares error with variation around the target mean Does not directly describe error in currency units

    Reusable Evaluation Function

    def evaluate_regression_model(
        model_name,
        actual_values,
        predicted_values
    ):
        mae = mean_absolute_error(
            actual_values,
            predicted_values
        )
    
        rmse = mean_squared_error(
            actual_values,
            predicted_values
        ) ** 0.5
    
        r2 = r2_score(
            actual_values,
            predicted_values
        )
    
        return {
            "model": model_name,
            "mae": float(mae),
            "rmse": float(rmse),
            "r2": float(r2)
        }
    
    
    model_results = [
        {
            "model": "Median Baseline",
            "mae": float(baseline_mae),
            "rmse": float(baseline_rmse),
            "r2": float(baseline_r2)
        },
        evaluate_regression_model(
            model_name="Linear Regression",
            actual_values=y_test,
            predicted_values=linear_predictions
        ),
        evaluate_regression_model(
            model_name="Random Forest",
            actual_values=y_test,
            predicted_values=(
                random_forest_predictions
            )
        )
    ]
    
    
    results_table = pd.DataFrame(
        model_results
    ).sort_values(
        by="mae"
    )
    
    
    print(
        results_table
    )
    No fabricated result: The article does not state which model wins. The best model depends on the authorized dataset and must be selected from the calculated validation evidence.

    Actual vs Predicted Values

    plt.figure(
        figsize=(7, 7)
    )
    
    
    plt.scatter(
        y_test,
        random_forest_predictions,
        alpha=0.65
    )
    
    
    minimum_value = min(
        y_test.min(),
        random_forest_predictions.min()
    )
    
    
    maximum_value = max(
        y_test.max(),
        random_forest_predictions.max()
    )
    
    
    plt.plot(
        [
            minimum_value,
            maximum_value
        ],
        [
            minimum_value,
            maximum_value
        ],
        linestyle="--"
    )
    
    
    plt.title(
        "Actual vs Predicted House Prices"
    )
    
    plt.xlabel(
        "Actual Price"
    )
    
    plt.ylabel(
        "Predicted Price"
    )
    
    plt.tight_layout()
    
    plt.show()

    Predictions close to the diagonal reference line are close to the observed values. Systematic deviation from the line may reveal underprediction or overprediction in particular price ranges.

    Residual Analysis

    A residual is the difference between an observed target and its prediction.

    Residual
    \[ e_i = y_i-\hat{y}_i \]
    residuals = (
        y_test.to_numpy()
        - random_forest_predictions
    )
    
    
    plt.figure(
        figsize=(9, 5)
    )
    
    
    plt.scatter(
        random_forest_predictions,
        residuals,
        alpha=0.65
    )
    
    
    plt.axhline(
        y=0,
        linestyle="--"
    )
    
    
    plt.title(
        "Residuals vs Predicted Prices"
    )
    
    plt.xlabel(
        "Predicted Price"
    )
    
    plt.ylabel(
        "Residual"
    )
    
    plt.tight_layout()
    
    plt.show()
    Residual Pattern Possible Concern
    Residuals increase with price Error magnitude may grow for expensive properties
    Mostly positive residuals The model tends to underpredict
    Mostly negative residuals The model tends to overpredict
    Curved residual pattern Important nonlinear structure may remain
    Separate residual clusters Location or property groups may behave differently
    Several extreme residuals Possible outliers, unusual properties, or data-quality issues

    Inspect the Largest Errors

    error_analysis = X_test.copy()
    
    
    error_analysis[
        "actual_price"
    ] = y_test
    
    
    error_analysis[
        "predicted_price"
    ] = random_forest_predictions
    
    
    error_analysis[
        "residual"
    ] = (
        error_analysis["actual_price"]
        - error_analysis["predicted_price"]
    )
    
    
    error_analysis[
        "absolute_error"
    ] = (
        error_analysis["residual"]
        .abs()
    )
    
    
    largest_errors = (
        error_analysis
        .sort_values(
            by="absolute_error",
            ascending=False
        )
        .head(20)
    )
    
    
    print(
        largest_errors
    )

    Review Large Errors For

    • Incorrect price or area values
    • Mixed measurement units
    • Unusual luxury or distressed sales
    • Rare locations
    • Missing important amenities
    • Property-condition inconsistencies
    • Duplicate or repeated transactions
    • Market-period changes
    • Transactions with unusual circumstances
    Incorrect Practice Delete every row with a large model error simply because it reduces the reported RMSE.
    Correct Practice Investigate whether the record is erroneous, rare but valid, or representative of an important deployment case.

    Evaluate by Location and Property Type

    group_analysis = error_analysis.copy()
    
    
    group_analysis[
        "absolute_error"
    ] = (
        group_analysis["actual_price"]
        - group_analysis["predicted_price"]
    ).abs()
    
    
    location_performance = (
        group_analysis
        .groupby(
            "location",
            dropna=False
        )
        .agg(
            property_count=(
                "absolute_error",
                "size"
            ),
            mean_absolute_error=(
                "absolute_error",
                "mean"
            ),
            mean_actual_price=(
                "actual_price",
                "mean"
            )
        )
        .sort_values(
            by="mean_absolute_error",
            ascending=False
        )
    )
    
    
    print(
        location_performance
    )
    Small-group caution: Group results based on very few properties can be unstable. Report group size and avoid strong conclusions from limited samples.

    Cross-Validation

    Cross-validation evaluates a complete pipeline across several training and validation partitions. Preprocessing must be fitted independently inside each fold.

    from sklearn.model_selection import KFold
    from sklearn.model_selection import cross_validate
    
    
    cross_validation = KFold(
        n_splits=5,
        shuffle=True,
        random_state=42
    )
    
    
    scoring = {
        "mae": "neg_mean_absolute_error",
        "rmse": "neg_root_mean_squared_error",
        "r2": "r2"
    }
    
    
    cross_validation_results = cross_validate(
        random_forest_model,
        features,
        target,
        cv=cross_validation,
        scoring=scoring,
        n_jobs=-1,
        return_train_score=False
    )
    
    
    cross_validation_summary = {
        "mean_mae": float(
            -cross_validation_results[
                "test_mae"
            ].mean()
        ),
        "mean_rmse": float(
            -cross_validation_results[
                "test_rmse"
            ].mean()
        ),
        "mean_r2": float(
            cross_validation_results[
                "test_r2"
            ].mean()
        )
    }
    
    
    print(
        cross_validation_summary
    )
    Split caution: Replace ordinary K-fold validation with time-aware or grouped validation when properties are temporally ordered or related.

    Hyperparameter Tuning

    Hyperparameter tuning must use training data only. The final test set should remain untouched until the selected configuration is ready for independent evaluation.

    from sklearn.model_selection import RandomizedSearchCV
    
    
    parameter_distributions = {
        "regressor__n_estimators": [
            200,
            300,
            500
        ],
        "regressor__max_depth": [
            None,
            8,
            12,
            16
        ],
        "regressor__min_samples_split": [
            2,
            5,
            10
        ],
        "regressor__min_samples_leaf": [
            1,
            2,
            4
        ],
        "regressor__max_features": [
            "sqrt",
            0.7,
            1.0
        ]
    }
    
    
    search = RandomizedSearchCV(
        estimator=random_forest_model,
        param_distributions=(
            parameter_distributions
        ),
        n_iter=20,
        scoring=(
            "neg_mean_absolute_error"
        ),
        cv=cross_validation,
        random_state=42,
        n_jobs=-1,
        refit=True
    )
    
    
    search.fit(
        X_train,
        y_train
    )
    
    
    best_model = search.best_estimator_
    
    
    best_predictions = best_model.predict(
        X_test
    )
    
    
    best_results = evaluate_regression_model(
        model_name="Tuned Random Forest",
        actual_values=y_test,
        predicted_values=best_predictions
    )
    
    
    print(
        search.best_params_
    )
    
    print(
        best_results
    )

    Feature Importance

    fitted_preprocessor = (
        best_model.named_steps[
            "preprocessor"
        ]
    )
    
    
    feature_names = (
        fitted_preprocessor
        .get_feature_names_out()
    )
    
    
    feature_importances = (
        best_model.named_steps[
            "regressor"
        ]
        .feature_importances_
    )
    
    
    importance_table = (
        pd.DataFrame(
            {
                "feature": feature_names,
                "importance": feature_importances
            }
        )
        .sort_values(
            by="importance",
            ascending=False
        )
    )
    
    
    print(
        importance_table.head(20)
    )
    Interpretation caution: Feature importance indicates how the fitted model used available variables. It does not establish causation, fairness, or the effect of changing a property characteristic.

    Consider a Log-Transformed Target

    When prices are strongly right-skewed, modeling the logarithm of the target may reduce the influence of extremely expensive properties. The final predictions must be converted back to the original price scale.

    from sklearn.compose import TransformedTargetRegressor
    
    
    log_target_model = Pipeline(
        steps=[
            (
                "preprocessor",
                preprocessor
            ),
            (
                "regressor",
                TransformedTargetRegressor(
                    regressor=RandomForestRegressor(
                        n_estimators=300,
                        min_samples_leaf=2,
                        random_state=42,
                        n_jobs=-1
                    ),
                    func=np.log1p,
                    inverse_func=np.expm1
                )
            )
        ]
    )
    
    
    log_target_model.fit(
        X_train,
        y_train
    )
    
    
    log_target_predictions = (
        log_target_model.predict(
            X_test
        )
    )
    
    
    log_target_results = (
        evaluate_regression_model(
            model_name=(
                "Log-Target Random Forest"
            ),
            actual_values=y_test,
            predicted_values=(
                log_target_predictions
            )
        )
    )
    
    
    print(
        log_target_results
    )
    Evaluation rule: Compare models using predictions transformed back to the original currency scale. Do not report only log-scale errors to end users.

    Save the Complete Pipeline

    import json
    import joblib
    import sklearn
    
    
    model_path = (
        "house_price_pipeline.joblib"
    )
    
    
    metadata_path = (
        "house_price_metadata.json"
    )
    
    
    joblib.dump(
        best_model,
        model_path
    )
    
    
    metadata = {
        "target_column": target_column,
        "numerical_features": numerical_features,
        "categorical_features": categorical_features,
        "currency": (
            "Replace with the dataset currency"
        ),
        "area_unit": "square feet",
        "scikit_learn_version": (
            sklearn.__version__
        ),
        "test_metrics": best_results
    }
    
    
    with open(
        metadata_path,
        "w",
        encoding="utf-8"
    ) as metadata_file:
    
        json.dump(
            metadata,
            metadata_file,
            indent=4
        )
    
    
    print(
        "Saved model:",
        model_path
    )
    
    print(
        "Saved metadata:",
        metadata_path
    )
    Artifact security: Load serialized model artifacts only from trusted and controlled sources. Store checksums, dependency versions, model ownership, and approval status with the artifact.

    Reload the Pipeline

    loaded_model = joblib.load(
        "house_price_pipeline.joblib"
    )
    
    
    with open(
        "house_price_metadata.json",
        "r",
        encoding="utf-8"
    ) as metadata_file:
    
        loaded_metadata = json.load(
            metadata_file
        )
    
    
    print(
        loaded_metadata
    )

    Predict a New Property

    Replace the following illustrative values with validated application input. The example is not a valuation of a real property.

    new_property = pd.DataFrame(
        [
            {
                "area_sqft": 1800,
                "bedrooms": 3,
                "bathrooms": 2,
                "property_age": 8,
                "lot_area_sqft": 2400,
                "parking_spaces": 1,
                "location": "Example Location",
                "property_type": "Apartment",
                "condition": "Good"
            }
        ]
    )
    
    
    predicted_price = loaded_model.predict(
        new_property
    )[0]
    
    
    print(
        "Estimated price:",
        predicted_price
    )
    Illustrative-input notice: The property values above are synthetic demonstration inputs. They are not connected to a real property or current housing market.

    Validate Prediction Input

    def validate_property_input(
        property_data
    ):
        expected_columns = {
            *numerical_features,
            *categorical_features
        }
    
        received_columns = set(
            property_data.columns
        )
    
        missing_input_columns = (
            expected_columns
            - received_columns
        )
    
        unexpected_columns = (
            received_columns
            - expected_columns
        )
    
        if missing_input_columns:
    
            raise ValueError(
                "Missing prediction columns: "
                f"{sorted(missing_input_columns)}"
            )
    
        if unexpected_columns:
    
            raise ValueError(
                "Unexpected prediction columns: "
                f"{sorted(unexpected_columns)}"
            )
    
        if (
            property_data["area_sqft"]
            <=
            0
        ).any():
    
            raise ValueError(
                "area_sqft must be greater than zero."
            )
    
        if (
            property_data["bedrooms"]
            <
            0
        ).any():
    
            raise ValueError(
                "bedrooms cannot be negative."
            )
    
        if (
            property_data["bathrooms"]
            <
            0
        ).any():
    
            raise ValueError(
                "bathrooms cannot be negative."
            )
    
        if (
            property_data["property_age"]
            <
            0
        ).any():
    
            raise ValueError(
                "property_age cannot be negative."
            )
    
        return property_data[
            [
                *numerical_features,
                *categorical_features
            ]
        ]
    
    
    validated_property = validate_property_input(
        new_property
    )
    
    
    validated_prediction = loaded_model.predict(
        validated_property
    )[0]
    
    
    print(
        "Validated estimated price:",
        validated_prediction
    )

    Prediction Is Not a Formal Valuation

    A model prediction is an estimate based on patterns in its training data. It does not automatically include all property details, transaction circumstances, legal conditions, structural defects, current negotiations, local regulations, or recent market events.

    High-Risk Use Treat the model output as a guaranteed sale price or use it as the only basis for a consequential financial decision.
    Responsible Use Present the estimate with its data date, currency, model version, known limitations, relevant error range, and an appropriate professional-review process.

    Common Project Mistakes

    Mistake Fit the imputer, scaler, or encoder before splitting the data.
    Correction Place preprocessing inside a pipeline fitted using training data.
    Mistake Include post-sale information among the features.
    Correction Use only information available at prediction time.
    Mistake Report only \(R^2\).
    Correction Report currency-based errors, residual behavior, and group-level outcomes.
    Mistake Remove all expensive properties automatically.
    Correction Distinguish invalid records from valid rare properties.
    Mistake Use a random split for a future-market prediction without review.
    Correction Use chronological evaluation when production predicts later sales.
    Mistake Save the fitted regressor but reconstruct preprocessing manually.
    Correction Save and deploy the complete fitted pipeline.
    Mistake Present every prediction as equally reliable.
    Correction Analyze error by price range, property type, location, and data coverage.

    Responsible AI Considerations

    Review Requirements

    • Document the intended and prohibited uses
    • Review whether historical prices reflect unfair practices
    • Evaluate errors across relevant locations and property groups
    • Avoid using protected characteristics as decision shortcuts
    • Review proxy variables carefully
    • Protect personal and transaction information
    • Provide human review for consequential decisions
    • Allow correction of inaccurate input data
    • Document model limitations
    • Monitor changing market conditions
    Fairness caution: Historical sale prices may reflect unequal access, neighborhood differences, market distortions, or prior discriminatory behavior. Predictive accuracy alone does not establish responsible use.

    Production Monitoring

    Monitoring Area Example Measure
    Input Quality Missing, invalid, or out-of-range values
    Category Drift New or changing locations and property types
    Feature Drift Changes in area, age, room count, or property mix
    Prediction Distribution Changes in estimated-price range
    Delayed Accuracy MAE and RMSE after actual prices become available
    Group Performance Error by location, type, and price range
    Latency Time required to validate and predict
    Model Usage Prediction volume, failures, and manual overrides
    Market Change Difference between training conditions and current transactions

    Recommended Project Workflow

    1

    Define the Prediction

    Specify the target value, prediction time, user, decision, currency, market, and acceptable error.

    2

    Audit and Version the Data

    Review authorization, units, missing values, duplicates, geography, time coverage, and target quality.

    3

    Prevent Leakage

    Remove post-sale and future-derived features before model development.

    4

    Select a Realistic Split

    Use random, chronological, grouped, or geographic evaluation according to deployment.

    5

    Build a Baseline

    Evaluate a median or another simple reference prediction.

    6

    Build the Pipeline

    Combine imputation, scaling, encoding, and regression in one reproducible workflow.

    7

    Compare Models

    Compare interpretable and nonlinear models under identical validation conditions.

    8

    Analyze Errors

    Review residuals, large errors, locations, property groups, and unusual transactions.

    9

    Save and Test Artifacts

    Save the complete pipeline, metadata, schema, metrics, and dependency information.

    10

    Deploy Responsibly

    Validate input, communicate uncertainty, preserve human review, monitor drift, and define rollback conditions.

    Hands-On Practice

    1

    Adapt the Schema

    Map the columns in an authorized dataset to numerical features, categorical features, and the target.

    2

    Create Data-Quality Rules

    Define valid ranges and units for price, area, rooms, age, and parking.

    3

    Compare Split Strategies

    Compare random and chronological evaluation where transaction dates are available.

    4

    Compare Three Models

    Compare a baseline, Linear Regression, and Random Forest using MAE, RMSE, and \(R^2\).

    5

    Analyze Residuals

    Plot residuals and inspect the twenty properties with the largest absolute errors.

    6

    Evaluate Groups

    Compare error by location, property type, and target-price range.

    7

    Save and Reload the Pipeline

    Verify that predictions remain consistent after artifact restoration.

    8

    Create a Model Card

    Document the purpose, data, metrics, intended use, limitations, risks, and monitoring requirements.

    Knowledge Check

    1

    Why is house price prediction a regression problem?

    Explain why the target is a continuous numerical quantity.

    2

    Why is a baseline model required?

    Explain how it establishes whether Machine Learning adds useful predictive value.

    3

    What is target leakage?

    Explain how future or post-sale information can create unrealistically strong evaluation results.

    4

    Why place preprocessing inside a pipeline?

    Explain leakage prevention and consistent transformation during training and inference.

    5

    What does MAE measure?

    Explain average absolute error in the original target unit.

    6

    Why is RMSE sensitive to large errors?

    Explain the effect of squaring each residual.

    7

    Why should residuals be analyzed?

    Explain systematic underprediction, overprediction, heteroscedasticity, and unusual property errors.

    8

    Why might a chronological split be preferable?

    Explain how it evaluates prediction of later transactions from earlier market information.

    9

    Why save the complete fitted pipeline?

    Explain the need to preserve preprocessing and prediction logic together.

    10

    Why is a model estimate not a guaranteed property valuation?

    Explain data limitations, market change, missing property details, unusual transactions, uncertainty, and professional review.

    Project Summary

    Project Stage Main Outcome
    Problem Definition Defines the target, prediction time, user, and acceptable error
    Data Audit Reviews schema, missing values, duplicates, units, and quality
    Leakage Review Removes future and post-sale information
    Evaluation Split Creates realistic independent evaluation data
    Baseline Provides a simple reference prediction
    Preprocessing Imputes, scales, and encodes features consistently
    Linear Regression Provides an interpretable model baseline
    Random Forest Models nonlinear relationships and interactions
    Evaluation Measures MAE, RMSE, \(R^2\), residuals, and group errors
    Persistence Saves the complete fitted pipeline and metadata
    Inference Validates new data before producing an estimate
    Monitoring Tracks data quality, drift, delayed error, usage, and failures

    Key Takeaway

    A reliable house price prediction project requires more than fitting a regression algorithm. You must define the valuation target, audit the data, prevent leakage, choose a split that reflects deployment, establish a baseline, preserve preprocessing inside a pipeline, compare models using currency-based errors, inspect residuals and important groups, save complete artifacts, validate inference inputs, communicate limitations, and monitor changing market conditions. The resulting prediction is an estimate supported by historical data, not a guaranteed sale price or a substitute for appropriate professional judgment.

    Get Machine Learning updates by email

    Short, practical lessons and course updates from Rumman Ansari. No spam.