Predict Used Car Prices with PyTorch

Build an MLP regression pipeline to predict used car prices from real data.

Introduction

30 Second Summary

Ever wondered how car resale platforms decide what a used car is worth? Thousands of factors play a role. The year, the mileage, the fuel type, the brand. Humans can estimate, but a trained neural network can learn the hidden patterns across thousands of real listings.

In this project, you will build a Multi-Layer Perceptron in PyTorch that predicts used car selling prices from real-world data. You will clean messy features, encode categories, train the network, and evaluate how well it predicts prices it has never seen before.

What You'll Build

A complete MLP regression pipeline in a Google Colab notebook that loads real used-car listing data, preprocesses it for neural network input, trains a multi-layer perceptron, and evaluates its predictions with metrics and visualizations.

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

  • A trained PyTorch MLP model that predicts used car selling prices from real features like engine power, mileage, and age.
  • A complete data preprocessing pipeline that handles messy real-world data including string cleaning, one-hot encoding, and feature scaling.
  • Evaluation metrics and visualizations (RMSE, MAE, R-squared, training loss curve, predicted vs actual scatter plot) showing how well your model performs.
  • Secret Mission: Add Dropout and Batch Normalization to your model to reduce overfitting and improve prediction accuracy.

Are there any prerequisites?

This project assumes you have basic Python knowledge and understand PyTorch fundamentals (tensors, nn.Module). You should also have a general understanding of what an MLP is. No GPU is required.

Before We Start

Before We Start...

Set Up Your Colab Notebook

Before you can train a neural network on car price data, you need a workspace with the right tools loaded and the data ready to explore.

In this step, you will create a Google Colab notebook, import the Python libraries for deep learning and data processing, then load the CarDekho dataset so you can see what you are working with.

In this step, get ready to:
  • Import all required libraries (PyTorch, pandas, scikit-learn, matplotlib).
  • Download and upload the CarDekho used-car dataset.
  • Verify the dataset loaded correctly with the expected shape and columns.
Create a new Colab notebook and import libraries

Google Colab gives you a free cloud-based Python environment with machine learning libraries pre-installed. You do not need to install PyTorch, scikit-learn, or pandas yourself.

  • Open Google Colab in your browser.
  • Click File in the top menu bar.
  • Click New notebook.
  • Rename the notebook by clicking the title at the top left and typing Car_Price_MLP.

A fresh notebook opens with one empty code cell.

  • Import all the libraries you will use throughout this project by pasting the following code into the first cell:
# Cell 1: Import Libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from google.colab import files

What does this code do?

This cell loads every library you will need for the entire project in one place.

  • torch, torch.nn, and torch.optim provide the neural network building blocks, loss functions, and optimizers.
  • pandas and numpy handle data loading and numeric operations.
  • matplotlib.pyplot creates charts and visualizations.
  • The sklearn imports provide data splitting, feature scaling, and evaluation metrics.
  • google.colab.files enables the file upload widget you will use to load data.
  • Run the cell by pressing Shift+Enter.
  • The cell should complete with no output and no errors. This confirms all libraries are available.
  • To verify PyTorch specifically, click into the next empty cell, type torch.__version__, and press Shift+Enter. You should see a version string starting with 2.

Seeing an import error?

All of these libraries come pre-installed on Colab. If you see an error, make sure you are running your notebook on Colab's hosted runtime (not a local runtime).

Check the top-right corner of your notebook for a green checkmark or Connected status. If it says Not connected, click it and select Connect to a hosted runtime.

help me fix an import error in my Google Colab notebook for PyTorch and scikit-learn

Download and upload the CarDekho dataset

The dataset you will use contains real listings from CarDekho, an Indian used-car marketplace. It has over 8,000 entries with features like engine size, mileage, and transmission type.

  • Open the CarDekho dataset page on Kaggle in a new browser tab.
  • Click the Download button near the top of the page. This downloads a ZIP file to your computer.
  • Unzip the downloaded file. Inside, find the file named Car details v3.csv.

Why download manually instead of using the Kaggle API?

The Kaggle API requires setting up authentication credentials. Downloading the file and uploading it through Colab's built-in widget is faster for a single dataset and avoids extra setup.

  • Back in your Colab notebook, paste the following code into a new cell to trigger the upload widget and load the data:
# Cell 2: Upload and Load Dataset
print("Upload the 'Car details v3.csv' file from Kaggle:")
uploaded = files.upload()

df = pd.read_csv('Car details v3.csv')
print(f"Dataset shape: {df.shape}")
print(df.head())

What does this code do?

  • files.upload() opens a file picker widget in your notebook so you can select a file from your computer.
  • pd.read_csv('Car details v3.csv') reads the CSV into a pandas DataFrame called df.
  • df.shape prints the number of rows and columns.
  • df.head() displays the first five rows so you can preview the data.
  • Run the cell by pressing Shift+Enter.
  • When the file upload widget appears, click Choose Files and select the Car details v3.csv file from your computer.
  • After the upload completes, the output should show Dataset shape: (8128, 13) followed by a preview table of the first five rows.

Upload seems stuck or file not found?

Make sure you selected the correct file. The filename must be exactly Car details v3.csv (with spaces). If you renamed it or selected the ZIP instead of the CSV, the read will fail.

If the upload widget does not appear, try re-running the cell. Colab sometimes needs a moment to render interactive widgets.

help me upload a CSV file to Google Colab using files.upload()

Verify the dataset structure

Before you start cleaning data, confirm the DataFrame has the expected number of columns, correct data types, and check for missing values.

  • Explore the dataset structure and check for missing values by pasting this into a new cell:
# Cell 3: Explore the Data
print("\nDataset Info:")
print(df.info())
print("\nMissing values:")
print(df.isnull().sum())
print("\nSummary statistics:")
print(df.describe())

What does this code do?

  • df.info() prints each column's name, non-null count, and data type. This reveals which columns store numbers versus text.
  • df.isnull().sum() counts missing values per column. You will need to handle these before training.
  • df.describe() shows summary statistics (mean, min, max) for numeric columns, giving you a sense of value ranges.
  • Run the cell with Shift+Enter.
  • Confirm the output shows 8,128 entries and 13 columns. You should see columns like selling_price, mileage, engine, and max_power listed as object type (strings with units attached).

Why are numeric columns showing as object type?

The mileage, engine, and max_power columns contain numbers with units attached (like "19.67 kmpl" or "1248 CC").

Pandas reads these as text strings because of the unit suffixes. In the next step, you will extract the numeric values and convert them to proper floats.

Seeing a different row count?

The dataset should have approximately 8,128 rows. If you see significantly fewer rows, you may have downloaded a different version of the dataset. Go back to the Kaggle page and make sure you downloaded the file named Car details v3.csv (not car data.csv which only has about 300 rows).

help me verify my CarDekho dataset loaded correctly in pandas

✔️ Awesome, I've got everything!

Great. Double check you have three cells in your notebook and all ran without errors.

ⓧ I'd like to double check the full code

Here is the complete code you should have across all three cells so far:

# Cell 1: Import Libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from google.colab import files

# Cell 2: Upload and Load Dataset
print("Upload the 'Car details v3.csv' file from Kaggle:")
uploaded = files.upload()

df = pd.read_csv('Car details v3.csv')
print(f"Dataset shape: {df.shape}")
print(df.head())

# Cell 3: Explore the Data
print("\nDataset Info:")
print(df.info())
print("\nMissing values:")
print(df.isnull().sum())
print("\nSummary statistics:")
print(df.describe())

Your notebook is ready and the data is loaded. Next up, you will dig into the features, clean up those messy string columns, and visualize which features correlate most strongly with selling price.

Explore and Clean the Data

Your notebook has the CarDekho dataset loaded and you can see its structure from the previous step. Now it is time to understand what the data actually contains and get it into a shape that a neural network can work with.

Real-world datasets are rarely ready for machine learning out of the box. In this step, you will examine the features, clean messy string values, and visualize which features matter most for predicting car prices.

In this step, get ready to:
  • Identify the target variable and spot problematic string columns.
  • Clean the data by extracting numeric values from strings and dropping unusable columns.
  • Visualize feature correlations to understand which inputs drive selling price.
Understand the features and target

Scroll up to the output of Cell 3 where you printed summary statistics with .describe().

  • Look at the selling_price column in the summary statistics output.
  • Notice the wide range of values (from tens of thousands to millions of INR).
  • Check which columns are missing from the numeric summary.

Why are some columns missing from describe?

The .describe() method only shows statistics for numeric columns by default. Columns like mileage, engine, and max_power are missing because they contain strings with units attached (like "19.67 kmpl", "1248 CC", "74 bhp").

The selling_price column is your target variable. This is what the neural network will learn to predict based on the other features.

Clean the data

Several columns need fixing before the data is usable. The name column has too many unique values to be useful, and torque has inconsistent units that make parsing impractical.

  • Create a new code cell (Cell 4) and add the following data cleaning code:
# Cell 4: Clean the Data
# Drop columns we won't use
df = df.drop(columns=['name', 'torque'])

# Extract numeric values from string columns
df['mileage'] = df['mileage'].str.extract(r'([\d.]+)').astype(float)
df['engine'] = df['engine'].str.extract(r'([\d.]+)').astype(float)
df['max_power'] = df['max_power'].str.extract(r'([\d.]+)').astype(float)

# Drop rows with missing values
df = df.dropna()

print(f"Cleaned dataset shape: {df.shape}")
print(df.dtypes)

What does this code do?

  • df.drop(columns=['name', 'torque']) removes the name column (thousands of unique car names are not useful as features) and torque (its values use inconsistent units like Nm and kgm, making it too messy to parse).
  • str.extract(r'([\d.]+)') uses a regex to pull out just the numeric portion of each string. For example, "19.67 kmpl" becomes 19.67 and "1248 CC" becomes 1248.0.
  • .astype(float) converts the extracted strings into actual float numbers.
  • df.dropna() removes any rows that still have missing values after the extraction (some entries may have had blank fields).
  • Run Cell 4 by pressing Shift+Enter.
  • You should see the cleaned dataset shape printed as approximately (7906, 11) and all columns showing numeric data types for mileage, engine, and max_power.

Seeing an error on the extract line?

Make sure you ran Cell 2 first (loading the CSV). If the DataFrame is empty or mileage is not a string column, the .str.extract() method will fail.

Also check that you did not accidentally re-run this cell twice. The second run would fail because the columns have already been converted to floats (floats do not have a .str accessor). Re-run Cells 2 and 3 first, then Cell 4.

Help me debug my data cleaning code.

Visualize feature correlations

Now that all features are numeric, you can measure how strongly each one correlates with the selling price. This tells you which inputs the neural network will find most useful.

  • Create a new code cell (Cell 5) and add the correlation visualization code:
# Cell 5: Visualize Correlations
numeric_cols = df.select_dtypes(include=[np.number]).columns
correlation = df[numeric_cols].corr()['selling_price'].sort_values(ascending=False)
print("\nCorrelation with selling_price:")
print(correlation)

plt.figure(figsize=(10, 6))
correlation.drop('selling_price').plot(kind='bar', color='steelblue')
plt.title('Feature Correlation with Selling Price')
plt.ylabel('Correlation Coefficient')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()

What does this code do?

  • select_dtypes(include=[np.number]) grabs only the numeric columns from the DataFrame.
  • .corr()['selling_price'] calculates the Pearson correlation coefficient between every numeric feature and the target. Values close to 1 mean a strong positive relationship, and values close to -1 mean a strong inverse relationship.
  • The bar chart visualizes these correlations so you can quickly see which features the model should pay most attention to.
  • Run Cell 5 by pressing Shift+Enter.
  • You should see a printed table of correlation values and a bar chart. The max_power and engine features should have the tallest positive bars.

Why do max_power and engine correlate most with price?

Cars with more powerful engines and higher displacement tend to be premium models that command higher resale prices. Meanwhile, km_driven typically shows a negative correlation because higher mileage decreases a car's value.

Your neural network will learn to weight these features accordingly during training.

Bar chart not displaying?

Make sure you ran the import cell (Cell 1) that includes import matplotlib.pyplot as plt. If you restarted the Colab runtime, re-run all cells from Cell 1 onwards.

Help me fix my matplotlib chart in Colab.

✔️ Awesome, I've got everything!

Great. Double check you have both Cell 4 and Cell 5 saved in your notebook.

ⓧ I'd like to double check the full code

Here is the complete notebook code up to this point:

# Cell 1: Import Libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from google.colab import files

# Cell 2: Upload and Load Dataset
print("Upload the 'Car details v3.csv' file from Kaggle:")
uploaded = files.upload()

df = pd.read_csv('Car details v3.csv')
print(f"Dataset shape: {df.shape}")
print(df.head())

# Cell 3: Explore the Data
print("\nDataset Info:")
print(df.info())
print("\nMissing values:")
print(df.isnull().sum())
print("\nSummary statistics:")
print(df.describe())

# Cell 4: Clean the Data
# Drop columns we won't use
df = df.drop(columns=['name', 'torque'])

# Extract numeric values from string columns
df['mileage'] = df['mileage'].str.extract(r'([\d.]+)').astype(float)
df['engine'] = df['engine'].str.extract(r'([\d.]+)').astype(float)
df['max_power'] = df['max_power'].str.extract(r'([\d.]+)').astype(float)

# Drop rows with missing values
df = df.dropna()

print(f"Cleaned dataset shape: {df.shape}")
print(df.dtypes)

# Cell 5: Visualize Correlations
numeric_cols = df.select_dtypes(include=[np.number]).columns
correlation = df[numeric_cols].corr()['selling_price'].sort_values(ascending=False)
print("\nCorrelation with selling_price:")
print(correlation)

plt.figure(figsize=(10, 6))
correlation.drop('selling_price').plot(kind='bar', color='steelblue')
plt.title('Feature Correlation with Selling Price')
plt.ylabel('Correlation Coefficient')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()

Your data is clean and you know which features matter most. Next up, you will preprocess this data into a format that PyTorch can actually consume: encoding categories, scaling features, and creating DataLoaders.

Preprocess Data for the Neural Network

Your data is clean and you understand which features correlate most with selling price. Now it's time to transform this data into a format your neural network can actually consume.

Neural networks only work with numbers. Your categorical columns like fuel and transmission are still strings. In this step, you'll encode those categories into binary columns, scale all features to a uniform range, and package everything into PyTorch tensors the model can train on.

In this step, get ready to:
  • Encode categorical features using one-hot encoding.
  • Scale features and split into training and test sets.
  • Convert data to PyTorch tensors and create DataLoaders.
Encode categorical features

Your DataFrame has four categorical columns: fuel, seller_type, transmission, and owner. A neural network would interpret raw integer labels (like 1, 2, 3 for fuel types) as having an order, which misrepresents the data.

What is one-hot encoding?

One-hot encoding creates a separate binary column for each category value. For example, a fuel column with values Petrol, Diesel, and CNG becomes three columns: fuel_Petrol, fuel_Diesel, and fuel_CNG. Each row gets a 1 in the matching column and 0s everywhere else.

This ensures the neural network treats each category as independent, with no implied ordering between them.

  • Create a new code cell and apply one-hot encoding by running:
# Cell 6: One-Hot Encode Categorical Features
categorical_cols = ['fuel', 'seller_type', 'transmission', 'owner']
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=False)

print(f"Shape after encoding: {df_encoded.shape}")
print(df_encoded.columns.tolist())

What does this code do?

  • categorical_cols lists the four string columns that need encoding.
  • pd.get_dummies() replaces each categorical column with binary columns (one per unique value).
  • drop_first=False keeps all category columns so the data is easier to interpret.
  • Run the cell.
  • You should see the shape increase from 11 columns to approximately 20+ columns. The printed column list shows your original numeric features plus all the new binary columns like fuel_Diesel and transmission_Automatic.

Seeing fewer columns than expected?

Make sure you spelled the column names exactly as they appear in the DataFrame. Check that Cell 4 (the cleaning step) ran successfully before this cell.

Help me debug my one-hot encoding output

Scale features and split the data

Neural networks train best when all input features are on a similar scale. Right now, km_driven can be in the hundreds of thousands while seats is a single digit. Without scaling, the large-valued features would dominate the learning process.

  • Create a new code cell and prepare your features for training by running:
# Cell 7: Prepare Features and Target
X = df_encoded.drop(columns=['selling_price']).values
y = df_encoded['selling_price'].values

# Scale features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Train/Test split
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, 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")
print(f"Number of input features: {X_train.shape[1]}")

What does this code do?

  • X holds all feature columns as a numpy array. y holds the target (selling_price).
  • StandardScaler transforms each feature to have mean 0 and standard deviation 1. This puts all features on the same scale.
  • train_test_split reserves 20% of the data for testing. The model never sees this portion during training, so it gives an honest measure of performance.
  • random_state=42 ensures the split is the same every time you run the cell, making your results reproducible.
  • Run the cell.
  • You should see approximately 6,324 training samples and 1,582 test samples printed, along with the total number of input features.

Seeing a NameError for StandardScaler?

Make sure Cell 1 (imports) ran successfully. StandardScaler is imported from sklearn.preprocessing in that first cell.

Help me fix my feature scaling error

Convert to PyTorch tensors and create DataLoaders

PyTorch models don't work with numpy arrays directly. You need to convert your data into PyTorch tensors. You also need a DataLoader to feed data to the model in small batches during training.

  • Create a new code cell and convert your data to PyTorch format by running:
# Cell 8: Convert to PyTorch Tensors and Create DataLoaders
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.FloatTensor(y_train).unsqueeze(1)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.FloatTensor(y_test).unsqueeze(1)

train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

What does this code do?

  • torch.FloatTensor() converts numpy arrays into 32-bit floating point tensors that PyTorch can process.
  • .unsqueeze(1) reshapes the target from a flat array [N] to a column vector [N, 1]. The model outputs shape [N, 1], so the target must match.
  • TensorDataset pairs each input row with its corresponding price label.
  • DataLoader serves batches of 32 samples at a time. Shuffling the training data each epoch prevents the model from memorizing the order.
  • Run the cell.
  • You won't see printed output from this cell. The tensors and DataLoaders are created in memory, ready for the model to use in the next step.

Seeing a TypeError about FloatTensor?

This usually means X_train or y_train still contains non-numeric data. Go back to Cell 7 and confirm it ran without errors. The StandardScaler step should have already produced pure float arrays.

Help me fix my tensor conversion error

✔️ Awesome, I've got everything!

Great. Double check you've run all three cells (6, 7, and 8) in order before moving on.

ⓧ I'd like to double check the full code

Here is the full notebook code up to this point. Compare it against your cells to spot any differences.

# Cell 1: Import Libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from google.colab import files

# Cell 2: Upload and Load Dataset
print("Upload the 'Car details v3.csv' file from Kaggle:")
uploaded = files.upload()

df = pd.read_csv('Car details v3.csv')
print(f"Dataset shape: {df.shape}")
print(df.head())

# Cell 3: Explore the Data
print("\nDataset Info:")
print(df.info())
print("\nMissing values:")
print(df.isnull().sum())
print("\nSummary statistics:")
print(df.describe())

# Cell 4: Clean the Data
# Drop columns we won't use
df = df.drop(columns=['name', 'torque'])

# Extract numeric values from string columns
df['mileage'] = df['mileage'].str.extract(r'([\d.]+)').astype(float)
df['engine'] = df['engine'].str.extract(r'([\d.]+)').astype(float)
df['max_power'] = df['max_power'].str.extract(r'([\d.]+)').astype(float)

# Drop rows with missing values
df = df.dropna()

print(f"Cleaned dataset shape: {df.shape}")
print(df.dtypes)

# Cell 5: Visualize Correlations
numeric_cols = df.select_dtypes(include=[np.number]).columns
correlation = df[numeric_cols].corr()['selling_price'].sort_values(ascending=False)
print("\nCorrelation with selling_price:")
print(correlation)

plt.figure(figsize=(10, 6))
correlation.drop('selling_price').plot(kind='bar', color='steelblue')
plt.title('Feature Correlation with Selling Price')
plt.ylabel('Correlation Coefficient')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()

# Cell 6: One-Hot Encode Categorical Features
categorical_cols = ['fuel', 'seller_type', 'transmission', 'owner']
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=False)

print(f"Shape after encoding: {df_encoded.shape}")
print(df_encoded.columns.tolist())

# Cell 7: Prepare Features and Target
X = df_encoded.drop(columns=['selling_price']).values
y = df_encoded['selling_price'].values

# Scale features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Train/Test split
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, 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")
print(f"Number of input features: {X_train.shape[1]}")

# Cell 8: Convert to PyTorch Tensors and Create DataLoaders
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.FloatTensor(y_train).unsqueeze(1)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.FloatTensor(y_test).unsqueeze(1)

train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

Your data is fully preprocessed and packaged into DataLoaders. Next up, you'll build the MLP model itself and train it on this prepared data.

Build and Train the MLP Model

Your data is preprocessed, scaled, and loaded into DataLoaders. Now it's time to build the neural network that will learn price patterns from car features.

In this step, you'll define a Multi-Layer Perceptron with a pyramid architecture that starts wide and narrows toward the output. You'll then train it for 100 epochs and watch the loss decrease as the model learns to predict car prices.

In this step, get ready to:
  • Define the MLP model class with a pyramid architecture.
  • Set up the MSE loss function and Adam optimizer.
  • Train the model for 100 epochs and track loss.
Define the MLP architecture

A pyramid architecture means each hidden layer has fewer neurons than the one before it. The network starts with 128 neurons to capture complex feature interactions, then compresses through 64 and 32 neurons, forcing it to distill the most important patterns before producing a single price prediction.

  • Create a new code cell in your Colab notebook and define the model class by pasting this code:
# Cell 9: Define the MLP Model
class CarPriceMLP(nn.Module):
    def __init__(self, input_size):
        super(CarPriceMLP, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(input_size, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Linear(32, 1)
        )

    def forward(self, x):
        return self.network(x)

input_size = X_train.shape[1]
model = CarPriceMLP(input_size)
print(model)

What does this code do?

The CarPriceMLP class inherits from nn.Module, which is PyTorch's base class for all neural networks.

  • nn.Sequential chains layers together so data flows through them in order.
  • nn.Linear(input_size, 128) is the first hidden layer. It takes all your input features and maps them to 128 neurons.
  • ReLU activation functions between layers let the network learn non-linear patterns (like how engine power and age interact to affect price).
  • The final nn.Linear(32, 1) outputs a single continuous value. There is no activation here because regression predicts any number, not a bounded category.

Why a pyramid shape?

Starting wide (128 neurons) lets the network discover many feature interactions. Narrowing to 64, then 32, forces the network to compress those discoveries into the most essential patterns. This is the standard approach for tabular regression tasks.

  • Run the cell.
  • You should see the model architecture printed, showing each layer and its dimensions (including Linear and ReLU layers stacked in order).

Seeing a NameError?

If you see an error about X_train or nn not being defined, make sure you ran all previous cells in order. Colab cells depend on each other.

help me debug a NameError in my Colab notebook when defining my model

Set up training components

The model needs two things to learn: a loss function that measures how wrong its predictions are, and an optimizer that adjusts the weights to reduce that error.

  • Create a new code cell and define the loss function and optimizer by pasting this code:
# Cell 10: Set Up Training Components
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

What does this code do?

  • nn.MSELoss() is Mean Squared Error loss. It squares the difference between predicted and actual prices, penalizing large errors heavily.
  • Adam is an optimizer that adapts the learning rate for each weight individually. The lr=0.001 sets how big each weight update step is.
  • Run the cell. There is no printed output, but the loss function and optimizer are now ready to use in the training loop.
Train the model

Training a neural network means repeatedly showing it batches of data, measuring how wrong its predictions are, and nudging its weights in the right direction. Each full pass through the training set is called an epoch.

  • Create a new code cell and write the training loop by pasting this code:
# Cell 11: Train the Model
num_epochs = 100
train_losses = []

model.train()
for epoch in range(num_epochs):
    epoch_loss = 0.0
    for batch_X, batch_y in train_loader:
        # Forward pass
        predictions = model(batch_X)
        loss = criterion(predictions, batch_y)

        # Backward pass
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()

    avg_loss = epoch_loss / len(train_loader)
    train_losses.append(avg_loss)

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

What does this code do?

This is the standard PyTorch training loop. Each epoch iterates through all batches in train_loader and performs four key operations:

  • Forward pass: the model predicts prices from the input batch (model(batch_X)).
  • Loss calculation: criterion(predictions, batch_y) measures how far off the predictions are from actual prices.
  • optimizer.zero_grad() resets the gradients so they don't accumulate from the previous batch.
  • loss.backward() computes gradients, then optimizer.step() updates all model weights based on those gradients.

The average loss per epoch is stored in train_losses so you can visualize the training curve in the next step.

  • Run the cell. Training takes about 10-30 seconds on Colab's CPU.
  • You should see loss values printed every 10 epochs. The loss should decrease over time, starting high and settling to a lower value by epoch 100.

Loss not decreasing?

If your loss stays flat or jumps around wildly, make sure you ran the data preprocessing cells correctly. The features must be scaled with StandardScaler before training. Unscaled features cause the optimizer to struggle with vastly different value ranges.

help me debug why my MLP training loss isn't decreasing

✔️ Awesome, I've got everything!

Great. Double check you've run all three cells (9, 10, and 11) and that your loss is decreasing over epochs.

ⓧ I'd like to double check the full code

Here is the full notebook code up to this point. Compare it against your cells to spot any differences.

# Cell 1: Import Libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from google.colab import files

# Cell 2: Upload and Load Dataset
print("Upload the 'Car details v3.csv' file from Kaggle:")
uploaded = files.upload()

df = pd.read_csv('Car details v3.csv')
print(f"Dataset shape: {df.shape}")
print(df.head())

# Cell 3: Explore the Data
print("\nDataset Info:")
print(df.info())
print("\nMissing values:")
print(df.isnull().sum())
print("\nSummary statistics:")
print(df.describe())

# Cell 4: Clean the Data
# Drop columns we won't use
df = df.drop(columns=['name', 'torque'])

# Extract numeric values from string columns
df['mileage'] = df['mileage'].str.extract(r'([\d.]+)').astype(float)
df['engine'] = df['engine'].str.extract(r'([\d.]+)').astype(float)
df['max_power'] = df['max_power'].str.extract(r'([\d.]+)').astype(float)

# Drop rows with missing values
df = df.dropna()

print(f"Cleaned dataset shape: {df.shape}")
print(df.dtypes)

# Cell 5: Visualize Correlations
numeric_cols = df.select_dtypes(include=[np.number]).columns
correlation = df[numeric_cols].corr()['selling_price'].sort_values(ascending=False)
print("\nCorrelation with selling_price:")
print(correlation)

plt.figure(figsize=(10, 6))
correlation.drop('selling_price').plot(kind='bar', color='steelblue')
plt.title('Feature Correlation with Selling Price')
plt.ylabel('Correlation Coefficient')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()

# Cell 6: One-Hot Encode Categorical Features
categorical_cols = ['fuel', 'seller_type', 'transmission', 'owner']
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=False)

print(f"Shape after encoding: {df_encoded.shape}")
print(df_encoded.columns.tolist())

# Cell 7: Prepare Features and Target
X = df_encoded.drop(columns=['selling_price']).values
y = df_encoded['selling_price'].values

# Scale features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Train/Test split
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, 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")
print(f"Number of input features: {X_train.shape[1]}")

# Cell 8: Convert to PyTorch Tensors and Create DataLoaders
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.FloatTensor(y_train).unsqueeze(1)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.FloatTensor(y_test).unsqueeze(1)

train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Cell 9: Define the MLP Model
class CarPriceMLP(nn.Module):
    def __init__(self, input_size):
        super(CarPriceMLP, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(input_size, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Linear(32, 1)
        )

    def forward(self, x):
        return self.network(x)

input_size = X_train.shape[1]
model = CarPriceMLP(input_size)
print(model)

# Cell 10: Set Up Training Components
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Cell 11: Train the Model
num_epochs = 100
train_losses = []

model.train()
for epoch in range(num_epochs):
    epoch_loss = 0.0
    for batch_X, batch_y in train_loader:
        # Forward pass
        predictions = model(batch_X)
        loss = criterion(predictions, batch_y)

        # Backward pass
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()

    avg_loss = epoch_loss / len(train_loader)
    train_losses.append(avg_loss)

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

Your model has now learned patterns from thousands of used car listings. Next up, you'll visualize how well it trained and measure its prediction accuracy on cars it has never seen before.

Evaluate and Visualize Results

Your model has trained for 100 epochs, and you watched the loss values drop at each checkpoint. But a list of numbers does not tell you whether the model actually learned meaningful patterns from the car data.

In this step, you will visualize how training progressed, measure prediction accuracy with standard metrics, and plot predictions against real prices. These evaluation tools reveal whether your MLP can generalize to cars it has never seen before.

In this step, get ready to:
  • Plot the training loss curve to confirm the model learned.
  • Calculate RMSE, MAE, and R-squared on the test set.
  • Visualize predicted vs actual prices with a scatter plot.
Plot the training loss curve

A training loss curve plots the average loss value at each epoch. A healthy curve trends downward, showing the model is learning to reduce its prediction errors over time.

  • Create a new code cell in your notebook and paste the following code to plot the training loss:
# Cell 12: Plot Training Loss
plt.figure(figsize=(10, 5))
plt.plot(train_losses, color='steelblue', linewidth=2)
plt.title('Training Loss Over Epochs')
plt.xlabel('Epoch')
plt.ylabel('MSE Loss')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

What does this code do?

This uses matplotlib to create a line chart from the train_losses list you built during training. Each point on the x-axis is an epoch, and the y-axis shows the average MSE loss for that epoch.

If training worked well, you should see a curve that drops steeply early on and then flattens out as the model converges.

  • Run the cell.
  • You should see a line chart with loss decreasing from left to right. The steepest drop happens in the first 10-20 epochs.

Seeing a flat or erratic line?

If the loss line is flat or jumping around wildly, your training cell (Cell 11) may not have run correctly. Go back and re-run Cell 11, then re-run this plotting cell.

Make sure you did not re-run the model definition cell (Cell 9) after training. That would reset the model weights.

Help me debug my training loss curve.

Make predictions and calculate metrics

The loss curve shows the model improved during training, but it was measured on training data. To know if the model generalizes, you need to test it on data it has never seen. You will run the test set through the model and measure three standard regression metrics.

  • Create a new code cell and paste this code to evaluate the model on unseen test data:
# Cell 13: Evaluate the Model
model.eval()
with torch.no_grad():
    y_pred_tensor = model(X_test_tensor)

y_pred = y_pred_tensor.numpy().flatten()

rmse = np.sqrt(mean_squared_error(y_test, y_pred))
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"\nModel Evaluation Metrics:")
print(f"RMSE: {rmse:,.0f} INR")
print(f"MAE: {mae:,.0f} INR")
print(f"R-squared: {r2:.4f}")

What does this code do?

  • model.eval() switches the model out of training mode. This disables behaviors like dropout that only apply during training.
  • torch.no_grad() tells PyTorch not to track gradients during prediction, which saves memory and speeds up inference.
  • RMSE (Root Mean Squared Error) measures average prediction error in the same units as the target (INR). Lower is better.
  • MAE (Mean Absolute Error) is the average absolute difference between predicted and actual prices. Less sensitive to outliers than RMSE.
  • R-squared measures how much variance in price the model explains. A value of 1.0 is a perfect fit. Values between 0.65 and 0.85 are expected for this dataset.
  • Run the cell.
  • You should see three printed metrics. Your RMSE will be in the hundreds of thousands (INR), and your R-squared should be somewhere between 0.65 and 0.85.

Why are the prices in INR?

The CarDekho dataset lists selling prices in Indian Rupees. RMSE and MAE are reported in the same unit as the target variable, so your error values are in INR. A large RMSE number does not necessarily mean poor performance. It reflects the price scale of the data.

Seeing very high RMSE or negative R-squared?

A negative R-squared means the model is predicting worse than a simple average. This usually happens if you re-ran the model definition cell (Cell 9) after training, which resets all learned weights.

Re-run cells 9 through 11 in order (define, set up, train), then re-run this evaluation cell.

Help me debug my model evaluation metrics.

Plot predicted vs actual prices

Metrics give you numbers, but a scatter plot gives you intuition. By plotting each test car's predicted price against its actual price, you can visually see where the model does well and where it struggles.

  • Create a new code cell and paste this code to generate the scatter plot:
# Cell 14: Plot Predicted vs Actual Prices
plt.figure(figsize=(8, 8))
plt.scatter(y_test, y_pred, alpha=0.5, color='steelblue', edgecolors='navy', linewidth=0.5)

# Perfect prediction line
max_val = max(y_test.max(), y_pred.max())
plt.plot([0, max_val], [0, max_val], 'r--', linewidth=2, label='Perfect Prediction')

plt.xlabel('Actual Selling Price (INR)')
plt.ylabel('Predicted Selling Price (INR)')
plt.title('Predicted vs Actual Car Selling Prices')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

What does this code do?

Each blue dot represents one car from the test set. Its x-position is the actual selling price, and its y-position is the model's predicted price.

The red dashed line is the "perfect prediction" diagonal. If the model predicted every price exactly right, all dots would sit directly on this line. Points above the line are overestimated. Points below the line are underestimated.

  • Run the cell.
  • You should see a scatter plot with blue dots clustering around the red diagonal line, especially for lower-priced cars. Higher-priced cars tend to scatter more because there are fewer examples in the training data.

Dots are scattered randomly with no pattern?

If the dots look completely random instead of clustering near the diagonal, your model did not learn. Check that you ran the training cell (Cell 11) successfully before running the evaluation and plotting cells.

Also confirm that y_test and y_pred have the same length by running print(len(y_test), len(y_pred)) in a new cell.

Help me fix my predicted vs actual scatter plot.

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook in Colab.

ⓧ I'd like to double check the full code

Here is the complete notebook code from Cell 1 through Cell 14. Compare it against what you have in your Colab cells.

# Cell 1: Import Libraries
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from google.colab import files

# Cell 2: Upload and Load Dataset
print("Upload the 'Car details v3.csv' file from Kaggle:")
uploaded = files.upload()

df = pd.read_csv('Car details v3.csv')
print(f"Dataset shape: {df.shape}")
print(df.head())

# Cell 3: Explore the Data
print("\nDataset Info:")
print(df.info())
print("\nMissing values:")
print(df.isnull().sum())
print("\nSummary statistics:")
print(df.describe())

# Cell 4: Clean the Data
# Drop columns we won't use
df = df.drop(columns=['name', 'torque'])

# Extract numeric values from string columns
df['mileage'] = df['mileage'].str.extract(r'([\d.]+)').astype(float)
df['engine'] = df['engine'].str.extract(r'([\d.]+)').astype(float)
df['max_power'] = df['max_power'].str.extract(r'([\d.]+)').astype(float)

# Drop rows with missing values
df = df.dropna()

print(f"Cleaned dataset shape: {df.shape}")
print(df.dtypes)

# Cell 5: Visualize Correlations
numeric_cols = df.select_dtypes(include=[np.number]).columns
correlation = df[numeric_cols].corr()['selling_price'].sort_values(ascending=False)
print("\nCorrelation with selling_price:")
print(correlation)

plt.figure(figsize=(10, 6))
correlation.drop('selling_price').plot(kind='bar', color='steelblue')
plt.title('Feature Correlation with Selling Price')
plt.ylabel('Correlation Coefficient')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()

# Cell 6: One-Hot Encode Categorical Features
categorical_cols = ['fuel', 'seller_type', 'transmission', 'owner']
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=False)

print(f"Shape after encoding: {df_encoded.shape}")
print(df_encoded.columns.tolist())

# Cell 7: Prepare Features and Target
X = df_encoded.drop(columns=['selling_price']).values
y = df_encoded['selling_price'].values

# Scale features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Train/Test split
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, 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")
print(f"Number of input features: {X_train.shape[1]}")

# Cell 8: Convert to PyTorch Tensors and Create DataLoaders
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.FloatTensor(y_train).unsqueeze(1)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.FloatTensor(y_test).unsqueeze(1)

train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

# Cell 9: Define the MLP Model
class CarPriceMLP(nn.Module):
    def __init__(self, input_size):
        super(CarPriceMLP, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(input_size, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Linear(32, 1)
        )

    def forward(self, x):
        return self.network(x)

input_size = X_train.shape[1]
model = CarPriceMLP(input_size)
print(model)

# Cell 10: Set Up Training Components
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Cell 11: Train the Model
num_epochs = 100
train_losses = []

model.train()
for epoch in range(num_epochs):
    epoch_loss = 0.0
    for batch_X, batch_y in train_loader:
        # Forward pass
        predictions = model(batch_X)
        loss = criterion(predictions, batch_y)

        # Backward pass
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()

    avg_loss = epoch_loss / len(train_loader)
    train_losses.append(avg_loss)

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

# Cell 12: Plot Training Loss
plt.figure(figsize=(10, 5))
plt.plot(train_losses, color='steelblue', linewidth=2)
plt.title('Training Loss Over Epochs')
plt.xlabel('Epoch')
plt.ylabel('MSE Loss')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Cell 13: Evaluate the Model
model.eval()
with torch.no_grad():
    y_pred_tensor = model(X_test_tensor)

y_pred = y_pred_tensor.numpy().flatten()

rmse = np.sqrt(mean_squared_error(y_test, y_pred))
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"\nModel Evaluation Metrics:")
print(f"RMSE: {rmse:,.0f} INR")
print(f"MAE: {mae:,.0f} INR")
print(f"R-squared: {r2:.4f}")

# Cell 14: Plot Predicted vs Actual Prices
plt.figure(figsize=(8, 8))
plt.scatter(y_test, y_pred, alpha=0.5, color='steelblue', edgecolors='navy', linewidth=0.5)

# Perfect prediction line
max_val = max(y_test.max(), y_pred.max())
plt.plot([0, max_val], [0, max_val], 'r--', linewidth=2, label='Perfect Prediction')

plt.xlabel('Actual Selling Price (INR)')
plt.ylabel('Predicted Selling Price (INR)')
plt.title('Predicted vs Actual Car Selling Prices')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Secret mission

Add Regularization to Improve Your Model

Your baseline model works, but it could be better. Add Dropout and Batch Normalization layers to reduce overfitting, and implement a learning rate scheduler that adapts as training progresses.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely in Google Colab, so there are no ongoing costs. Decide whether to keep your notebook for future reference or delete it from your Google Drive.

Resources you used:

  • Google Colab notebook (stored in your Google Drive).
  • Uploaded Car details v3.csv file (temporary session storage).

Keep everything running

No action needed. Choose this if you plan to revisit the notebook, experiment with different architectures, or try new datasets.

  • Your notebook stays in Google Drive under the Colab Notebooks folder.
  • The Colab runtime disconnects automatically after inactivity, freeing all compute resources.
  • The uploaded CSV is cleared when the runtime disconnects. You will need to re-upload it next time you open the notebook.

Pause - I'll come back to this later

Shut down the Colab runtime to free up resources. Your notebook code stays safe in Google Drive.

  • In your Colab notebook, click Runtime in the top menu bar.
  • Click Disconnect and delete runtime.
  • This clears all variables, uploaded files, and session data. Your notebook code is preserved in Google Drive.

Delete - I don't want to use this again

Remove the notebook entirely from your Google Drive.

  • In your Colab notebook, click Runtime in the top menu bar, then click Disconnect and delete runtime.
  • Open Google Drive at drive.google.com.
  • Navigate to the Colab Notebooks folder.
  • Right-click your notebook file and select Move to trash.
  • Empty your trash to permanently delete the file.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete neural network regression pipeline that predicts used car prices from real-world data.

You've learned how to:

  • Built a data preprocessing pipeline that cleans messy real-world data by extracting numbers from strings, one-hot encoding categorical features, and scaling inputs for neural network consumption.
  • Designed and trained a multi-layer perceptron in PyTorch with a pyramid architecture (128-64-32-1), using MSE loss and the Adam optimizer in a batch training loop.
  • Evaluated model performance with RMSE, MAE, and R-squared metrics, and visualized prediction quality with a predicted vs actual scatter plot.
  • Secret Mission: Added Dropout, Batch Normalization, and a learning rate scheduler to reduce overfitting and improve prediction accuracy.

Ready to quiz yourself?