DEV Community

zohir Benmalek
zohir Benmalek

Posted on

Building a Car Price Predictor with Scikit-learn and Flask

Machine learning tutorials often end after model.fit(). But training a model is only one part of building a useful ML application.

A more interesting workflow is to take a dataset, prepare the data, engineer useful features, compare several models, save the best pipeline, and finally turn it into a web application.

That is what this car price predictor project does using Python, Pandas, Scikit-learn, Joblib, and Flask.

The complete walkthrough is available on SoftwareJournal.blog, while the project source code is available on GitHub.

The Architecture

The project follows a simple machine learning pipeline:

Dataset
   ↓
Data Cleaning
   ↓
Feature Engineering
   ↓
Preprocessing
   ↓
Model Training
   ↓
Model Evaluation
   ↓
Save Pipeline
   ↓
Flask Application
   ↓
Prediction
Enter fullscreen mode Exit fullscreen mode

The repository contains a Jupyter notebook for experimentation and a Flask application for serving predictions.

car_price_predictor/
├── notebook.ipynb
├── README.md
└── car_price_predictor/
    ├── app.py
    ├── requirements.txt
    └── templates/
        └── index.html
Enter fullscreen mode Exit fullscreen mode

Preparing the Dataset

The project uses a second-hand car dataset with fields such as manufacturer, model, engine size, fuel type, year, mileage, and price.

Pandas is used to load the CSV file:

import pandas as pd

df = pd.read_csv("car_sales_data.csv")
Enter fullscreen mode Exit fullscreen mode

Before training anything, the notebook checks the dataset using methods such as:

df.isnull().sum()
df.info()
Enter fullscreen mode Exit fullscreen mode

This is an important step in any machine learning project. Problems with missing values or unexpected data types are much easier to fix before the training pipeline is created.

Feature Engineering

One of the project's main transformations is converting the manufacturing year into the age of the car.

df["Age"] = 2024 - df["Year of manufacture"]
Enter fullscreen mode Exit fullscreen mode

The original year column is then removed from the model inputs:

X = df.drop(columns=["Price", "Year of manufacture"])
y = df["Price"]
Enter fullscreen mode Exit fullscreen mode

This gives the model a more directly meaningful feature: how old is the vehicle?

There is also an important caveat here. The reference year is hardcoded to 2024, so this should be changed to a dynamic calculation in a production application.

Preprocessing with Scikit-learn

The dataset contains both numerical and categorical features.

Numerical features include:

  • Engine size
  • Mileage
  • Age

Categorical features include:

  • Manufacturer
  • Fuel type
  • Model

Scikit-learn's ColumnTransformer makes it possible to process those columns differently.

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder

preprocessor = ColumnTransformer(
    transformers=[
        ("num", StandardScaler(), numerical_features),
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features)
    ]
)
Enter fullscreen mode Exit fullscreen mode

StandardScaler standardizes numerical values, while OneHotEncoder converts categories into numerical columns.

One particularly useful option is:

handle_unknown="ignore"
Enter fullscreen mode Exit fullscreen mode

This means the application can encounter an unseen categorical value without immediately failing during prediction.

Why Pipeline Is So Useful

The preprocessing object is combined with the machine learning model:

from sklearn.pipeline import Pipeline

pipeline = Pipeline([
    ("preprocessor", preprocessor),
    ("regressor", model)
])
Enter fullscreen mode Exit fullscreen mode

This is one of the best design choices in the project.

Instead of saving preprocessing logic separately from the trained model, the pipeline becomes one object containing the entire process.

That means the Flask application can later receive raw data and simply call:

pipeline.predict(input_data)
Enter fullscreen mode Exit fullscreen mode

There is no need to manually perform scaling and one-hot encoding again.

This significantly reduces the risk of training-serving inconsistencies.

Comparing Four Models

The project evaluates four regression algorithms:

models = {
    "LinearRegression": LinearRegression(),
    "Ridge": Ridge(),
    "RandomForest": RandomForestRegressor(
        n_estimators=100,
        random_state=42,
        n_jobs=-1
    ),
    "GradientBoosting": GradientBoostingRegressor(
        n_estimators=100,
        random_state=42
    )
}
Enter fullscreen mode Exit fullscreen mode

The dataset is divided into training and testing sets:

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42
)
Enter fullscreen mode Exit fullscreen mode

Each model is trained through the same preprocessing pipeline and evaluated using Mean Absolute Error (MAE) and .

The reported results are:

Model MAE
Linear Regression 5786.31 0.7102
Ridge 5786.14 0.7102
Random Forest 286.04 0.9986
Gradient Boosting 1037.39 0.9899

Random Forest performs the best on this dataset.

The result is interesting because the linear models struggle to capture the nonlinear relationships between car age, mileage, manufacturer, model, and price.

Tree-based ensemble models are much better suited to that kind of relationship.

However, an R² of 0.9986 is exceptionally high. The dataset is a mock/synthetic dataset, so these numbers should not be interpreted as evidence that the same model would achieve similar accuracy on a real-world used-car market.

Saving the Models

Once the pipelines have been trained, Joblib is used to serialize them:

joblib.dump(
    pipeline,
    f"saved_models/{name}_pipeline.joblib"
)
Enter fullscreen mode Exit fullscreen mode

This means the trained pipeline can be loaded later without running the complete training process again.

The winning Random Forest pipeline can then be reloaded:

loaded_pipeline = joblib.load(
    "saved_models/RandomForest_pipeline.joblib"
)
Enter fullscreen mode Exit fullscreen mode

Testing the Saved Pipeline

Before connecting the model to Flask, the project tests the serialized pipeline with new examples.

For example:

new_data = pd.DataFrame({
    "Manufacturer": ["BMW", "Ford", "Toyota"],
    "Model": ["X5", "Focus", "Camry"],
    "Engine size": [4.4, 1.6, 2.5],
    "Fuel type": ["Gasoline", "Gasoline", "Hybrid"],
    "Mileage": [35000, 80000, 15000],
    "Age": [4, 7, 1]
})

predicted_prices = loaded_pipeline.predict(new_data)
Enter fullscreen mode Exit fullscreen mode

This is a useful test because it verifies that the saved object can be loaded successfully and used with previously unseen input.

Connecting the Model to Flask

The next step is turning the trained model into a web application.

The Flask application loads the pipeline when the server starts:

model = joblib.load(
    "saved_models/RandomForest_pipeline.joblib"
)
Enter fullscreen mode Exit fullscreen mode

The main route handles both page loading and form submission:

@app.route("/", methods=["GET", "POST"])
def home():
    ...
Enter fullscreen mode Exit fullscreen mode

When the user submits the form, Flask retrieves values such as:

manufacturer = request.form.get("manufacturer")
model_name = request.form.get("model")
year = int(request.form.get("year"))
mileage = int(request.form.get("mileage"))
engine_size = float(request.form.get("engine_size"))
fuel_type = request.form.get("fuel_type")
Enter fullscreen mode Exit fullscreen mode

The application then calculates the vehicle's age and creates a DataFrame using the same column structure used during training.

input_data = pd.DataFrame({
    "Manufacturer": [manufacturer],
    "Model": [model_name],
    "Engine size": [engine_size],
    "Fuel type": [fuel_type],
    "Mileage": [mileage],
    "Age": [age]
})
Enter fullscreen mode Exit fullscreen mode

Finally:

prediction = model.predict(input_data)
Enter fullscreen mode Exit fullscreen mode

The prediction is sent back to the HTML template and displayed to the user.

The Frontend

The application uses a straightforward HTML interface rendered with Jinja2 and styled with Bootstrap.

Instead of asking users to type arbitrary manufacturer and model names, the interface uses dropdowns.

That reduces typing mistakes and helps keep the input consistent with the categories the model was trained on.

The frontend does not require a JavaScript framework or a separate build process. Flask renders the page directly.

Running the Application

After installing the dependencies:

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

the application can be started with:

python car_price_predictor/app.py
Enter fullscreen mode Exit fullscreen mode

The Flask development server will normally be available at:

http://127.0.0.1:5000
Enter fullscreen mode Exit fullscreen mode

You can then enter the vehicle information and receive a predicted price through the browser.

What I Like About This Project

The most useful lesson here isn't the particular Random Forest model.

It is the structure of the project.

The same preprocessing pipeline is used during training and inference. Multiple algorithms are compared instead of assuming the first model will be good enough. The trained pipeline is persisted so it can be reused later. Finally, Flask provides a simple interface between the model and an ordinary web user.

That is a much more realistic machine learning workflow than a notebook that simply prints an accuracy score.

What Could Be Improved?

There are several improvements that would make this project stronger for production use.

The 2024 reference year should be calculated dynamically rather than hardcoded.

Cross-validation and hyperparameter tuning could provide more reliable model evaluation than relying on a single train/test split.

The Flask form could also have stronger validation for invalid or missing values.

Another improvement would be moving feature engineering into shared Python code so that the notebook and Flask application cannot accidentally implement different versions of the same transformation.

Final Thoughts

This car price predictor is a good example of how a relatively small Python project can connect several important areas of development:

Pandas handles the data.

Scikit-learn handles preprocessing and machine learning.

Pipeline keeps transformations and prediction together.

Joblib stores the trained model.

Flask exposes the model through a web application.

The result is a complete path from dataset to browser:

Raw Data
→ Machine Learning
→ Saved Pipeline
→ Flask
→ Web Prediction
Enter fullscreen mode Exit fullscreen mode

For developers learning machine learning deployment, this is often the step that makes everything click: the model isn't the entire application. It is one component inside a larger software system.

You can find the original technical walkthrough on SoftwareJournal.blog and the complete source code on GitHub.


Originally published on SoftwareJournal.blog.

Top comments (0)