Predict Bengaluru House Prices with ML

Build a regression model and deploy it as a REST API for Bangalore real estate.

Introduction

30 Second Summary

Bangalore is one of the fastest-growing property markets in India. With prices varying wildly across 200+ locations, figuring out whether a listing is a good deal or overpriced feels like guesswork.

In this project, you will build a machine learning model that predicts Bengaluru house prices based on real listing data, then deploy it as a REST API you can query from any application.

What You'll Build

A regression model trained on 13,320 real property listings from Bengaluru, served through a FastAPI endpoint that accepts property details and returns a predicted price in Lakhs.

By the end of this project, you'll have:

  • A trained ML model that predicts house prices based on location, square footage, bedrooms, and bathrooms.
  • Hands-on experience with the full ML pipeline: cleaning messy real-world data, engineering features, training and comparing models, and serializing the best one for deployment.
  • A locally running REST API that accepts property details as JSON and returns a price prediction you can test with curl or interactive docs.
  • Secret Mission: input validation, graceful error handling for unknown locations, and a model metadata endpoint.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, loops, lists). No prior machine learning experience is needed.

This project uses scikit-learn, pandas, and NumPy. You will install everything from scratch in Step 1.

Before We Start

Before writing any machine learning code, you need the right tools in place. An ML project depends on specialized libraries for data processing, model training, and visualization.

In this step, you'll set up VS Code for notebook-based development, install the Python libraries that Indian ML job postings require, and download a real Bengaluru property dataset from Kaggle.

In this step, get ready to:
  • Install VS Code extensions for Python and Jupyter notebook support.
  • Create a project folder with a virtual environment and all required libraries.
  • Download the Bengaluru house price dataset and verify your setup.
Install the Python and Jupyter extensions in VS Code

VS Code needs two extensions to work with Jupyter notebooks. The Python extension provides language support, and the Jupyter extension lets you run notebook cells directly inside VS Code.

  • Press the Super key to open your application launcher.
  • Type Visual Studio Code and press Enter to open it.
  • Open the Extensions sidebar by pressing Ctrl+Shift+X.
  • Type Python in the search bar.
  • Find the extension published by Microsoft and click Install.
  • Clear the search bar, type Jupyter, find the extension published by Microsoft, and click Install.

What do these extensions do?

The Python extension gives VS Code language features like autocomplete, linting, and the ability to select a Python interpreter. The Jupyter extension adds notebook support so you can run code cells, see inline output, and render visualizations without leaving your editor.

Create your project folder and install libraries

You'll create a dedicated folder on your Desktop so you can find your project easily. Inside it, you'll set up a Python virtual environment to keep your libraries isolated from the rest of your system.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir bengaluru-house-prices
cd bengaluru-house-prices
  • Confirm the folder exists by running:
ls ~/Desktop | grep bengaluru

You should see bengaluru-house-prices in the output.

  • Create a virtual environment and activate it by running these commands:
python3 -m venv venv
source venv/bin/activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

Seeing an error about ensurepip?

On some Debian/Ubuntu systems, the venv module is not installed by default. Fix it by running:

sudo apt install python3.12-venv

Then try the python3 -m venv venv command again.

Help me fix my virtual environment error on Linux.

  • Install all the project libraries by running this command:
pip install scikit-learn==1.9.0 pandas==3.0.3 numpy==2.4.6 matplotlib==3.11.0 seaborn==0.13.2 "fastapi[standard]==0.139.0" joblib==1.5.3

What are all these libraries?

pandas handles data loading and manipulation. NumPy provides fast numerical operations. scikit-learn is the industry-standard ML library for model training and evaluation.

Matplotlib and seaborn create data visualizations. FastAPI is a modern Python web framework you'll use to serve your model as an API. joblib saves your trained model to disk.

The install takes 1-2 minutes. You should see a Successfully installed message at the end listing all the packages.

Install command failing?

Make sure you see (venv) in your terminal prompt. If not, run source venv/bin/activate first.

If you see a network timeout error, check your internet connection and try the install command again.

Help me fix my pip install error.

Download the dataset and create your notebook

Kaggle is a platform for data science competitions and datasets. You'll download a real dataset of 13,320 Bengaluru property listings that contains messy, real-world data.

  • Open your web browser and go to kaggle.com.
  • Create a free account using your Google or email sign-up (takes 1-2 minutes).
  • Navigate to the dataset page at kaggle.com/datasets/amitabhajoy/bengaluru-house-price-data.
  • Click the Download button near the top of the page.
  • Unzip the downloaded file. Inside you'll find Bengaluru_House_Data.csv.
  • Move Bengaluru_House_Data.csv into your bengaluru-house-prices folder on your Desktop.

Move the file with the terminal

If the file downloaded to your Downloads folder, you can move it by running:

mv ~/Downloads/Bengaluru_House_Data.csv ~/Desktop/bengaluru-house-prices/
  • Confirm the CSV file is in your project folder by running:
ls

You should see Bengaluru_House_Data.csv and venv listed in the output.

Now create your Jupyter notebook file.

  • In VS Code, press Ctrl+Shift+P to open the Command Palette.
  • Type Create: New Jupyter Notebook and select it from the dropdown.
  • A new untitled notebook opens. Save it immediately by pressing Ctrl+S.
  • Name the file house_prices.ipynb and save it inside your bengaluru-house-prices folder.

VS Code might ask you to select a kernel

When the notebook opens, VS Code may prompt you to pick a Python kernel. Click Select Kernel in the top right corner of the notebook, choose Python Environments, and select the venv interpreter from your project folder. This ensures your notebook uses the libraries you just installed.

  • Verify your setup by typing the following code into the first cell of your notebook:
import pandas as pd
import fastapi
print(pd.__version__, fastapi.__version__)
  • Run the cell by pressing Shift+Enter.

You should see 3.0.3 0.139.0 printed below the cell. This confirms both pandas and FastAPI installed correctly.

Seeing a ModuleNotFoundError?

This means your notebook is not using the virtual environment where you installed the libraries. Click the kernel selector in the top right corner of the notebook and choose the Python interpreter inside your venv folder.

If you don't see venv listed, try reloading VS Code by pressing Ctrl+Shift+P and selecting Developer: Reload Window.

Help me fix the ModuleNotFoundError in my Jupyter notebook.

Your environment is ready. Next up, you'll load the Bengaluru house price dataset, explore its messy real-world columns, and clean it into shape for model training.

Load, Clean, and Preprocess the Data

Your environment is set up and ready to go. Now it's time to load the raw Bengaluru house price data and transform it from messy real-world listings into a clean format your model can learn from.

Real estate datasets are notoriously messy, with text values stuffed into numeric columns, inconsistent formats, and missing values scattered everywhere. You'll tackle each of these issues systematically in this step.

In this step, get ready to:
  • Load and explore 13,320 property listings to identify data quality issues.
  • Drop unnecessary columns and parse text values into numeric features.
  • Convert inconsistent square footage entries into clean float values.
Load the dataset and explore its structure

Before you can build a model, you need to understand what your data looks like. Loading and exploring the dataset reveals the quality issues you'll need to fix before training.

  • In your house_prices.ipynb notebook, select the verification cell from Step 1.
  • Delete it by pressing Esc to enter command mode, then pressing d twice.
  • Add the following imports in the first cell by typing or pasting the code below:
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import joblib

Why import everything at the top?

In Jupyter notebooks, the convention is to import all libraries in the first cell. This way, if an import fails, you catch it immediately rather than halfway through your analysis.

You won't use every library in this step. Matplotlib, seaborn, and the scikit-learn models come into play in Step 3.

  • Run the cell by pressing Shift+Enter.

If no errors appear, all libraries loaded successfully. Shift+Enter also creates a new empty cell below, ready for your next block of code.

Seeing a ModuleNotFoundError?

This means a library isn't installed in the active Python environment. Make sure your notebook kernel is using the venv virtual environment from Step 1.

  • Click the kernel selector in the top-right corner of the notebook (it shows a Python version).
  • Select Python Environments and choose the venv interpreter from your project folder.

Why can't my notebook find the libraries I installed?

  • In the new cell below, load and explore the dataset by pasting the following code:
# Cell 2: Load the data
df = pd.read_csv('Bengaluru_House_Data.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumns: {list(df.columns)}")
print(f"\nMissing values:")
print(df.isnull().sum())
print(f"\nSize column sample values:")
print(df['size'].value_counts().head(10))
df.head()

What does this code do?

  • pd.read_csv() loads the CSV file into a pandas DataFrame, a table-like structure you can filter, transform, and analyze.
  • df.shape shows the number of rows and columns (expect 13,320 rows and 9 columns).
  • df.isnull().sum() counts missing values per column, revealing which columns have gaps.
  • df['size'].value_counts() shows the text formats in the size column, like "2 BHK" and "3 Bedroom".
  • Run the cell with Shift+Enter.

You should see the dataset shape (13320, 9), a list of missing values per column, and sample values from the size column showing entries like "2 BHK" and "4 Bedroom".

What data issues should I notice?

Look at the output carefully. Three major issues stand out:

  • The size column contains text ("2 BHK", "3 Bedroom") instead of numbers.
  • The total_sqft column has ranges like "1200-1500" mixed with plain numbers.
  • The society column is about 41% missing values.

Seeing FileNotFoundError?

This means Bengaluru_House_Data.csv isn't in the same folder as your notebook. Make sure the CSV file is inside your bengaluru-house-prices project folder (not in a subfolder or on your Desktop).

Help me fix my FileNotFoundError when loading the CSV

Drop unnecessary columns and extract BHK

Not every column in the dataset helps predict house prices. Columns like society (41% missing) and availability (possession date) add noise without predictive value.

  • In the next empty cell, drop the unnecessary columns and rows with missing values by pasting this code:
# Cell 3: Drop unnecessary columns and handle missing values
df = df.drop(['area_type', 'society', 'availability', 'balcony'], axis=1)
df = df.dropna(subset=['location', 'size', 'total_sqft', 'bath', 'price'])
print(f"Shape after dropping columns and nulls: {df.shape}")

What does this code do?

  • df.drop() removes the four columns that won't help your model: area_type, society, availability, and balcony.
  • df.dropna() removes any row where the essential columns (location, size, total_sqft, bath, price) have missing values.
  • Run the cell with Shift+Enter.

You should see the shape printed, showing 5 remaining columns and slightly fewer rows than the original 13,320.

The size column still contains text like "2 BHK". Your model needs a numeric value, so you'll extract just the number.

  • In the next cell, extract the numeric BHK value from the size column by pasting this code:
# Cell 4: Extract BHK from size column
df['bhk'] = df['size'].str.extract(r'(\d+)', expand=False).astype(int)
df = df.drop('size', axis=1)
print(f"BHK value counts:")
print(df['bhk'].value_counts().sort_index())

What does this code do?

  • str.extract(r'(\d+)') uses a regular expression to pull the first number from each text entry. For example, "2 BHK" becomes 2 and "4 Bedroom" becomes 4.
  • The result is stored as a new integer column called bhk, and the original text size column is dropped.
  • Run the cell with Shift+Enter.

You should see BHK value counts showing mostly 2 and 3 BHK properties, with a few 1 BHK and larger listings.

Seeing a KeyError for the size column?

This happens if you run Cell 4 more than once. The first run drops the size column, so running it again fails because the column no longer exists.

To fix this, re-run all cells from Cell 2 onwards. Click on Cell 2 and use Shift+Enter to run each cell in sequence.

Help me fix the KeyError in my notebook

Convert total square footage to numeric values

The total_sqft column looks numeric but contains entries like "1200-1500" representing a range. You need a function that handles both plain numbers and ranges by averaging the endpoints.

  • In the next cell, define the conversion function and apply it by pasting this code:
# Cell 5: Convert total_sqft to numeric
def convert_sqft(x):
    try:
        return float(x)
    except ValueError:
        tokens = x.split('-')
        if len(tokens) == 2:
            try:
                return (float(tokens[0]) + float(tokens[1])) / 2
            except ValueError:
                return None
        return None

df['total_sqft'] = df['total_sqft'].apply(convert_sqft)
df = df.dropna(subset=['total_sqft'])
print(f"Shape after sqft conversion: {df.shape}")
print(f"\nData verification - no nulls:")
print(df.isnull().sum())

What does this code do?

  • convert_sqft() first tries to convert the value to a float directly. If that fails (because the value is a range like "1200-1500"), it splits on the hyphen and averages the two numbers.
  • If neither approach works (rare entries with text like "34.46Sq. Meter"), it returns None, which gets dropped by dropna().
  • The final df.isnull().sum() verifies that your DataFrame has zero missing values across all columns.
  • Run the cell with Shift+Enter.

You should see approximately 13,000 rows remaining with 5 columns, and every column showing 0 null values. Your DataFrame now has columns: location (text), total_sqft (float), bath (float), price (float), and bhk (int).

Still seeing null values in the output?

If your null count is not all zeros, make sure you ran the cells in order from Cell 2 through Cell 5. Jupyter cells must be executed sequentially because each cell depends on the DataFrame state from the previous cell.

Help me fix null values in my cleaned DataFrame

✔️ Awesome, I've got everything!

Great. Make sure you've saved your notebook with Ctrl+S.

ⓧ I'd like to double check the full code

Here's what your notebook should contain so far (Cells 1 through 5):

# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import joblib

# Cell 2: Load the data
df = pd.read_csv('Bengaluru_House_Data.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumns: {list(df.columns)}")
print(f"\nMissing values:")
print(df.isnull().sum())
print(f"\nSize column sample values:")
print(df['size'].value_counts().head(10))
df.head()

# Cell 3: Drop unnecessary columns and handle missing values
df = df.drop(['area_type', 'society', 'availability', 'balcony'], axis=1)
df = df.dropna(subset=['location', 'size', 'total_sqft', 'bath', 'price'])
print(f"Shape after dropping columns and nulls: {df.shape}")

# Cell 4: Extract BHK from size column
df['bhk'] = df['size'].str.extract(r'(\d+)', expand=False).astype(int)
df = df.drop('size', axis=1)
print(f"BHK value counts:")
print(df['bhk'].value_counts().sort_index())

# Cell 5: Convert total_sqft to numeric
def convert_sqft(x):
    try:
        return float(x)
    except ValueError:
        tokens = x.split('-')
        if len(tokens) == 2:
            try:
                return (float(tokens[0]) + float(tokens[1])) / 2
            except ValueError:
                return None
        return None

df['total_sqft'] = df['total_sqft'].apply(convert_sqft)
df = df.dropna(subset=['total_sqft'])
print(f"Shape after sqft conversion: {df.shape}")
print(f"\nData verification - no nulls:")
print(df.isnull().sum())

Your data is clean and structured. Next up, you'll engineer new features, visualize price patterns, and train two machine learning models to predict house prices.

Engineer Features, Visualize Data, and Train Models

Your DataFrame is clean and structured. Now it is time to turn that raw data into something a machine learning model can actually learn from.

In this step, you will engineer new features from the existing columns, visualize price patterns to build intuition about the data, and then train two different models to see which one predicts Bengaluru house prices more accurately.

In this step, get ready to:
  • Create a price-per-square-foot feature and group rare locations to reduce noise.
  • Build three visualizations to explore price distributions and location trends.
  • Train and compare a Linear Regression model against a Random Forest model.
Create features and visualize price patterns

Before feeding data into a model, you need features that capture meaningful patterns. A raw price column alone does not tell you whether a property is expensive relative to its size.

  • Create a new cell in your notebook and add the feature engineering code by pasting the following:
# Cell 6: Feature engineering - price per sqft and location grouping
df['price_per_sqft'] = df['price'] * 100000 / df['total_sqft']

location_counts = df['location'].value_counts()
locations_less_than_10 = location_counts[location_counts < 10].index
df['location'] = df['location'].apply(
    lambda x: 'Other' if x in locations_less_than_10 else x
)
print(f"Unique locations after grouping: {df['location'].nunique()}")

What does this code do?

This cell creates two key improvements to your data:

  • `price_per_sqft` divides the price (converted from Lakhs to Rupees) by square footage. This lets you compare properties fairly regardless of size.
  • The location grouping finds locations with fewer than 10 listings and renames them all to "Other". This prevents the model from overfitting to locations with very few data points.
  • Run the cell by pressing **Shift+Enter**.
  • You should see a print statement showing a reduced number of unique locations (around 240 down from 1000+).

Seeing a NameError for df?

This means cells 1-5 from the previous step were not executed in this session. Click **Run All** at the top of the notebook to re-run all cells in order.

Help me fix this NameError

Now that you have engineered features, let's visualize the data to understand price distributions and identify which locations command the highest prices.

  • Create a new cell and add the visualization code by pasting the following:
# Cell 7: Visualizations
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Price distribution
sns.histplot(df['price'], bins=50, kde=True, ax=axes[0])
axes[0].set_title('Distribution of House Prices')
axes[0].set_xlabel('Price (Lakhs)')
axes[0].set_xlim(0, 500)

# Price vs sqft
axes[1].scatter(df['total_sqft'], df['price'], alpha=0.2)
axes[1].set_title('Price vs Total Sqft')
axes[1].set_xlabel('Total Sqft')
axes[1].set_ylabel('Price (Lakhs)')
axes[1].set_xlim(0, 5000)
axes[1].set_ylim(0, 500)

# Top 10 locations
top_locations = df.groupby('location')['price'].mean().sort_values(ascending=False).head(10)
top_locations.plot(kind='bar', ax=axes[2])
axes[2].set_title('Top 10 Most Expensive Locations')
axes[2].set_xlabel('Location')
axes[2].set_ylabel('Avg Price (Lakhs)')
axes[2].tick_params(axis='x', rotation=45)

plt.tight_layout()
plt.show()

What do these visualizations show?

  • The histogram shows that most Bengaluru properties are priced under 200 Lakhs, with a long tail of expensive outliers.
  • The scatter plot reveals the general relationship between area and price. Larger properties cost more, but the spread shows that location and other factors matter too.
  • The bar chart identifies which Bengaluru localities have the highest average prices, giving you intuition about why location is a powerful feature.
  • Run the cell by pressing **Shift+Enter**.
  • You should see three plots rendered inline below the cell: a price histogram with a KDE curve, a scatter plot of price vs area, and a bar chart of the top 10 most expensive locations.

Plots not showing inline?

Make sure you are running the cell inside the Jupyter notebook (the .ipynb file), not in a regular Python terminal. Plots render inline only in notebook cells.

Help me get my plots to render

Prepare features and split data for training

Models need numeric input. Your location column is still text, which means it needs to be converted into numbers the model can process.

  • Create a new cell and prepare your features by pasting the following:
# Cell 8: Prepare features for modeling
X = df.drop(['price', 'price_per_sqft'], axis=1)
X = pd.get_dummies(X, columns=['location'], drop_first=True, dtype=int)
y = df['price']
print(f"Features shape: {X.shape}")
print(f"Target shape: {y.shape}")

What does this code do?

`X` contains all the input features the model will learn from. You drop price (that is what you are predicting) and price_per_sqft (it is calculated from price, so including it would be data leakage).

`pd.get_dummies()` performs one-hot encoding on the location column. Each unique location becomes its own binary column (1 if the property is in that location, 0 otherwise). The drop_first=True avoids redundancy by dropping one category.

  • Run the cell by pressing **Shift+Enter**.
  • You should see a features shape of approximately (13000, 240+) columns. The large number of columns comes from the one-hot encoded locations.

With features prepared, you need to split the data into a training set (which the model learns from) and a test set (which measures how well it generalizes to new data).

  • Create a new cell and split your data by pasting the following:
# Cell 9: Train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

What is a train-test split?

A train-test split holds back 20% of your data as a test set that the model never sees during training. This lets you measure real-world performance instead of just memorization.

`random_state=42` ensures the split is reproducible. Every time you run this cell, you get the same training and test samples.

  • Run the cell by pressing **Shift+Enter**.
  • You should see approximately 10,400 training samples and 2,600 test samples printed.
Train and compare two regression models

Now comes the core ML step. You will train two models and compare their accuracy to see which one handles Bengaluru's property market better.

  • Create a new cell and train a Linear Regression model by pasting the following:
# Cell 10: Train Linear Regression
lr_model = LinearRegression()
lr_model.fit(X_train, y_train)
lr_predictions = lr_model.predict(X_test)

lr_mae = mean_absolute_error(y_test, lr_predictions)
lr_rmse = np.sqrt(mean_squared_error(y_test, lr_predictions))
lr_r2 = r2_score(y_test, lr_predictions)

print("Linear Regression Results:")
print(f"  MAE:  {lr_mae:.2f} Lakhs")
print(f"  RMSE: {lr_rmse:.2f} Lakhs")
print(f"  R2:   {lr_r2:.4f}")

What are MAE, RMSE, and R²?

These are the standard metrics for evaluating regression models:

  • MAE (Mean Absolute Error): the average prediction error in Lakhs. Lower is better.
  • RMSE (Root Mean Squared Error): similar to MAE but penalizes large errors more heavily.
  • R² (R-squared): measures what fraction of price variation your model explains. 1.0 is perfect, 0.0 means the model is no better than guessing the average.
  • Run the cell by pressing **Shift+Enter**.
  • You should see three metrics printed. Linear Regression provides a reasonable baseline, but notice the R² score leaves room for improvement.

A single straight-line model might not capture the complex relationships between location, size, and price. A Random Forest builds many decision trees and averages their predictions for more accuracy.

  • Create a new cell and train a Random Forest model by pasting the following:
# Cell 11: Train Random Forest
rf_model = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
rf_predictions = rf_model.predict(X_test)

rf_mae = mean_absolute_error(y_test, rf_predictions)
rf_rmse = np.sqrt(mean_squared_error(y_test, rf_predictions))
rf_r2 = r2_score(y_test, rf_predictions)

print("Random Forest Results:")
print(f"  MAE:  {rf_mae:.2f} Lakhs")
print(f"  RMSE: {rf_rmse:.2f} Lakhs")
print(f"  R2:   {rf_r2:.4f}")

Why 100 trees?

`n_estimators=100` means the forest contains 100 individual decision trees. Each tree sees a random subset of the data and features, then the final prediction is the average of all 100 trees.

More trees generally improve accuracy but take longer to train. 100 is a solid default for datasets of this size.

  • Run the cell by pressing **Shift+Enter**.
  • You should see improved metrics compared to Linear Regression. The R² score should be noticeably higher.

Cell taking a long time?

Random Forest trains 100 separate trees, so it takes longer than Linear Regression. On most machines this finishes in under 30 seconds. If it hangs for more than a minute, check that your DataFrame is not unexpectedly large by running X_train.shape in a new cell.

Help me debug the slow training

Finally, let's put both models side by side so you can clearly see the winner.

  • Create a new cell and add the model comparison code by pasting the following:
# Cell 12: Model comparison
results = pd.DataFrame({
    'Model': ['Linear Regression', 'Random Forest'],
    'MAE (Lakhs)': [lr_mae, rf_mae],
    'RMSE (Lakhs)': [lr_rmse, rf_rmse],
    'R2 Score': [lr_r2, rf_r2]
})
print("\nModel Comparison:")
print(results.to_string(index=False))

# Actual vs Predicted plots
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

axes[0].scatter(y_test, lr_predictions, alpha=0.3)
axes[0].plot([0, 500], [0, 500], 'r--', linewidth=2)
axes[0].set_title('Linear Regression: Actual vs Predicted')
axes[0].set_xlabel('Actual Price (Lakhs)')
axes[0].set_ylabel('Predicted Price (Lakhs)')
axes[0].set_xlim(0, 500)
axes[0].set_ylim(0, 500)

axes[1].scatter(y_test, rf_predictions, alpha=0.3)
axes[1].plot([0, 500], [0, 500], 'r--', linewidth=2)
axes[1].set_title('Random Forest: Actual vs Predicted')
axes[1].set_xlabel('Actual Price (Lakhs)')
axes[1].set_ylabel('Predicted Price (Lakhs)')
axes[1].set_xlim(0, 500)
axes[1].set_ylim(0, 500)

plt.tight_layout()
plt.show()

How to read the Actual vs Predicted plots

The red dashed line represents perfect predictions (where actual equals predicted). The closer the dots cluster around this line, the better the model performs.

You should see that Random Forest's dots are tighter around the line compared to Linear Regression. This visual confirms what the R² numbers show: Random Forest captures the price patterns more accurately.

  • Run the cell by pressing **Shift+Enter**.
  • You should see a comparison table with both models' MAE, RMSE, and R² values, followed by two scatter plots. Random Forest should have a higher R² score than Linear Regression.

R² values look identical or very low?

Make sure you ran Cell 10 and Cell 11 in separate cells (not both in the same cell). Also confirm that X_train and y_train exist by running X_train.shape in a new cell. If they do not exist, re-run Cell 9 first.

Help me debug my model comparison

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook before moving on to deployment.

ⓧ I'd like to double check the full code

Here are cells 6-12 together for comparison:

# Cell 6: Feature engineering - price per sqft and location grouping
df['price_per_sqft'] = df['price'] * 100000 / df['total_sqft']

location_counts = df['location'].value_counts()
locations_less_than_10 = location_counts[location_counts < 10].index
df['location'] = df['location'].apply(
    lambda x: 'Other' if x in locations_less_than_10 else x
)
print(f"Unique locations after grouping: {df['location'].nunique()}")

# Cell 7: Visualizations
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Price distribution
sns.histplot(df['price'], bins=50, kde=True, ax=axes[0])
axes[0].set_title('Distribution of House Prices')
axes[0].set_xlabel('Price (Lakhs)')
axes[0].set_xlim(0, 500)

# Price vs sqft
axes[1].scatter(df['total_sqft'], df['price'], alpha=0.2)
axes[1].set_title('Price vs Total Sqft')
axes[1].set_xlabel('Total Sqft')
axes[1].set_ylabel('Price (Lakhs)')
axes[1].set_xlim(0, 5000)
axes[1].set_ylim(0, 500)

# Top 10 locations
top_locations = df.groupby('location')['price'].mean().sort_values(ascending=False).head(10)
top_locations.plot(kind='bar', ax=axes[2])
axes[2].set_title('Top 10 Most Expensive Locations')
axes[2].set_xlabel('Location')
axes[2].set_ylabel('Avg Price (Lakhs)')
axes[2].tick_params(axis='x', rotation=45)

plt.tight_layout()
plt.show()

# Cell 8: Prepare features for modeling
X = df.drop(['price', 'price_per_sqft'], axis=1)
X = pd.get_dummies(X, columns=['location'], drop_first=True, dtype=int)
y = df['price']
print(f"Features shape: {X.shape}")
print(f"Target shape: {y.shape}")

# Cell 9: Train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")

# Cell 10: Train Linear Regression
lr_model = LinearRegression()
lr_model.fit(X_train, y_train)
lr_predictions = lr_model.predict(X_test)

lr_mae = mean_absolute_error(y_test, lr_predictions)
lr_rmse = np.sqrt(mean_squared_error(y_test, lr_predictions))
lr_r2 = r2_score(y_test, lr_predictions)

print("Linear Regression Results:")
print(f"  MAE:  {lr_mae:.2f} Lakhs")
print(f"  RMSE: {lr_rmse:.2f} Lakhs")
print(f"  R2:   {lr_r2:.4f}")

# Cell 11: Train Random Forest
rf_model = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
rf_predictions = rf_model.predict(X_test)

rf_mae = mean_absolute_error(y_test, rf_predictions)
rf_rmse = np.sqrt(mean_squared_error(y_test, rf_predictions))
rf_r2 = r2_score(y_test, rf_predictions)

print("Random Forest Results:")
print(f"  MAE:  {rf_mae:.2f} Lakhs")
print(f"  RMSE: {rf_rmse:.2f} Lakhs")
print(f"  R2:   {rf_r2:.4f}")

# Cell 12: Model comparison
results = pd.DataFrame({
    'Model': ['Linear Regression', 'Random Forest'],
    'MAE (Lakhs)': [lr_mae, rf_mae],
    'RMSE (Lakhs)': [lr_rmse, rf_rmse],
    'R2 Score': [lr_r2, rf_r2]
})
print("\nModel Comparison:")
print(results.to_string(index=False))

# Actual vs Predicted plots
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

axes[0].scatter(y_test, lr_predictions, alpha=0.3)
axes[0].plot([0, 500], [0, 500], 'r--', linewidth=2)
axes[0].set_title('Linear Regression: Actual vs Predicted')
axes[0].set_xlabel('Actual Price (Lakhs)')
axes[0].set_ylabel('Predicted Price (Lakhs)')
axes[0].set_xlim(0, 500)
axes[0].set_ylim(0, 500)

axes[1].scatter(y_test, rf_predictions, alpha=0.3)
axes[1].plot([0, 500], [0, 500], 'r--', linewidth=2)
axes[1].set_title('Random Forest: Actual vs Predicted')
axes[1].set_xlabel('Actual Price (Lakhs)')
axes[1].set_ylabel('Predicted Price (Lakhs)')
axes[1].set_xlim(0, 500)
axes[1].set_ylim(0, 500)

plt.tight_layout()
plt.show()

You have two trained models and clear evidence that Random Forest outperforms Linear Regression on this dataset. Next up, you will serialize the winning model and deploy it as a REST API that anyone can query for price predictions.

Serialize the Model and Deploy as a FastAPI Endpoint

Your model comparison is complete. Random Forest outperformed Linear Regression, giving you a clear winner to deploy.

A trained model sitting in a notebook is useful for experiments, but not for real applications. In this step, you'll save the Random Forest model to disk and wrap it in a FastAPI web server that accepts property details and returns price predictions.

In this step, get ready to:
  • Serialize the trained model and supporting metadata to disk.
  • Build a FastAPI app that loads the model and exposes a prediction endpoint.
  • Run and test the API with a sample property request.
Serialize the trained model and metadata

Your trained Random Forest model currently lives only in your notebook's memory. If you close the notebook or restart the kernel, the model disappears. joblib lets you save Python objects to disk so they can be loaded by other scripts.

  • In house_prices.ipynb, create a new cell (Cell 13) below your model comparison plots and paste the following code:
# Cell 13: Serialize model and metadata for deployment
joblib.dump(rf_model, 'model.joblib')
joblib.dump(list(X_train.columns), 'columns.joblib')

locations = [col.replace('location_', '') for col in X_train.columns if col.startswith('location_')]
joblib.dump(locations, 'locations.joblib')
joblib.dump({'r2': rf_r2, 'training_samples': X_train.shape[0]}, 'metadata.joblib')

print("Saved: model.joblib, columns.joblib, locations.joblib, metadata.joblib")
print(f"Model R2: {rf_r2:.4f}")
print(f"Training samples: {X_train.shape[0]}")
print(f"Supported locations: {len(locations)}")

What does this code do?

  • joblib.dump(rf_model, 'model.joblib') saves the entire trained Random Forest to a file. This includes all 100 decision trees and their learned parameters.
  • columns.joblib stores the exact column order used during training. The prediction endpoint needs this to match incoming data to the right features.
  • locations.joblib saves a clean list of location names (without the location_ prefix). The API uses this for the Secret Mission metadata endpoint.
  • metadata.joblib stores the R² score and training sample count so the API can report model performance without recomputing it.
  • Run the cell with Shift+Enter.
  • You should see output confirming four files were saved, followed by the model's R² score, training sample count, and number of supported locations.

Seeing a NameError?

If you see a NameError mentioning rf_model, your kernel lost the trained model. Run all cells from Cell 1 through Cell 11 again to retrain the model before running Cell 13.

Help me fix this NameError in my notebook.

Build the FastAPI prediction endpoint

With the model saved to disk, you need a way for other programs to request predictions from it. A REST API gives any application a standard way to send property details and receive a price prediction back as JSON.

  • Create a new file called app.py in your bengaluru-house-prices folder. In VS Code, click the new file icon in the Explorer sidebar and type app.py.
  • Paste the following code at the top of app.py to set up the app and define the request schema:
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import pandas as pd

app = FastAPI(title="Bengaluru House Price Predictor")

# Load model and metadata at startup
model = joblib.load('model.joblib')
columns = joblib.load('columns.joblib')
locations = joblib.load('locations.joblib')
metadata = joblib.load('metadata.joblib')


class HouseFeatures(BaseModel):
    location: str
    sqft: float
    bath: int
    bhk: int

What does this code do?

  • The imports bring in FastAPI (web framework), Pydantic (data validation), joblib (model loading), and pandas (data manipulation).
  • FastAPI(title="Bengaluru House Price Predictor") creates the app instance. The title appears in the auto-generated API docs.
  • The four joblib.load() calls run once at server startup, keeping the model in memory for fast predictions.
  • HouseFeatures is a Pydantic model that defines what the API expects in each request. FastAPI uses it to validate incoming JSON automatically.

You won't see a change yet. The next chunk adds the prediction logic that makes this app functional.

  • Below the HouseFeatures class in app.py, paste the prediction endpoint:
@app.post("/predict")
def predict_price(features: HouseFeatures):
    x = pd.DataFrame(
        [[features.sqft, features.bath, features.bhk]],
        columns=['total_sqft', 'bath', 'bhk']
    )
    for col in columns:
        if col.startswith('location_') and col not in x.columns:
            x[col] = 0

    loc_col = f'location_{features.location}'
    if loc_col in columns:
        x[loc_col] = 1

    x = x.reindex(columns=columns, fill_value=0)
    prediction = model.predict(x)[0]

    return {
        "location": features.location,
        "predicted_price_lakhs": round(prediction, 2)
    }

What does this endpoint do?

  • @app.post("/predict") registers this function to handle POST requests at the /predict URL.
  • It creates a one-row DataFrame with the numeric features (sqft, bath, bhk), then adds all the one-hot encoded location columns set to 0.
  • If the requested location matches a column from training (e.g. location_Whitefield), that column is set to 1.
  • reindex(columns=columns, fill_value=0) ensures the DataFrame has exactly the same columns in the same order the model was trained on.
  • The model returns the predicted price in Lakhs, which gets rounded and sent back as JSON.
  • Save app.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved app.py before moving on.

ⓧ I'd like to double check the full code

Here's the complete app.py file for comparison:

from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import pandas as pd

app = FastAPI(title="Bengaluru House Price Predictor")

# Load model and metadata at startup
model = joblib.load('model.joblib')
columns = joblib.load('columns.joblib')
locations = joblib.load('locations.joblib')
metadata = joblib.load('metadata.joblib')


class HouseFeatures(BaseModel):
    location: str
    sqft: float
    bath: int
    bhk: int


@app.post("/predict")
def predict_price(features: HouseFeatures):
    x = pd.DataFrame(
        [[features.sqft, features.bath, features.bhk]],
        columns=['total_sqft', 'bath', 'bhk']
    )
    for col in columns:
        if col.startswith('location_') and col not in x.columns:
            x[col] = 0

    loc_col = f'location_{features.location}'
    if loc_col in columns:
        x[loc_col] = 1

    x = x.reindex(columns=columns, fill_value=0)
    prediction = model.predict(x)[0]

    return {
        "location": features.location,
        "predicted_price_lakhs": round(prediction, 2)
    }
Run and test the API

Your API code is ready. Time to start the server and send it a real prediction request.

  • Open a terminal in VS Code (Ctrl+`). Make sure your virtual environment is active (you should see (venv) in the prompt).
  • Start the FastAPI development server by running:
fastapi dev app.py

What does this command do?

fastapi dev starts a development server powered by Uvicorn with auto-reload enabled. It serves your app at http://127.0.0.1:8000 and automatically restarts when you save changes to app.py.

It also auto-generates interactive documentation at http://127.0.0.1:8000/docs where you can test your endpoints in the browser.

  • You should see output showing the server is running at http://127.0.0.1:8000. Keep this terminal running.
  • Open a second terminal by clicking the + icon in the terminal panel. Make sure your virtual environment is active in this new terminal too.
  • Test the prediction endpoint by running this curl command:
curl -X POST http://127.0.0.1:8000/predict -H "Content-Type: application/json" -d '{"location": "Whitefield", "sqft": 1200, "bath": 2, "bhk": 2}'
  • You should see a JSON response containing the location name and a predicted_price_lakhs value. The exact number depends on your trained model.
  • Open http://127.0.0.1:8000/docs in your browser to see the interactive API documentation. You should see your /predict endpoint listed with a Try it out button.

Server not starting?

  • If you see a ModuleNotFoundError, make sure your virtual environment is active. Run source venv/bin/activate and try again.
  • If you see a file not found error for model.joblib, make sure you ran Cell 13 in your notebook first. The .joblib files must exist in the same folder as app.py.
  • If port 8000 is already in use, stop any other process on that port before running the command again.

Help me debug my FastAPI server startup error.

Your model is now live as an API. Any application can send property details to your endpoint and get a price prediction back in milliseconds.

Secret mission

Input Validation and Model Metadata Endpoint

Your API works, but it is not production-ready. What happens when someone sends a sqft of 5 or a bhk of 100? Add input validation, graceful error handling for unknown locations, and a model metadata endpoint to make your API robust.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • FastAPI dev server (running process in your terminal).
  • bengaluru-house-prices project folder on your Desktop (includes virtual environment, notebook, model files, dataset, and API code).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with the model.

  • Your project folder stays on your Desktop with all files intact.
  • The FastAPI dev server continues running in your terminal until you close it.
  • Your Kaggle account and VS Code extensions (Python, Jupyter) remain available for future ML projects.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your project files stay on your Desktop for later.

  • Stop the FastAPI dev server by pressing Ctrl+C in the terminal where it is running.
  • To resume later, open a terminal in your bengaluru-house-prices folder, activate the virtual environment with source venv/bin/activate, and run fastapi dev app.py.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes everything including your trained model, notebook, and virtual environment.

  • Stop the FastAPI dev server by pressing Ctrl+C in the terminal where it is running.
  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/bengaluru-house-prices

On Windows, run this instead:

rmdir /s /q %USERPROFILE%\Desktop\bengaluru-house-prices
  • Confirm the folder is gone by running:
ls ~/Desktop | grep bengaluru

You should see no output, confirming the folder has been deleted.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete machine learning pipeline from raw, messy real estate data all the way to a live prediction API.

You've learned how to:

  • Clean and preprocess real-world data by parsing text columns, handling missing values, converting inconsistent formats, and engineering features like price_per_sqft and location grouping.
  • Train, evaluate, and compare ML models using scikit-learn by building both a Linear Regression baseline and a Random Forest model, then selecting the best performer based on MAE, RMSE, and R².
  • Serialize and deploy a trained model as a REST API using joblib for model persistence and FastAPI for serving predictions through a POST endpoint, tested with curl and interactive docs.
  • Secret Mission: Added input validation with Pydantic field validators, graceful handling of unknown locations, and a /model-info GET endpoint that exposes model metadata.

Ready to quiz yourself?