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
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
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")
Before training anything, the notebook checks the dataset using methods such as:
df.isnull().sum()
df.info()
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"]
The original year column is then removed from the model inputs:
X = df.drop(columns=["Price", "Year of manufacture"])
y = df["Price"]
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)
]
)
StandardScaler standardizes numerical values, while OneHotEncoder converts categories into numerical columns.
One particularly useful option is:
handle_unknown="ignore"
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)
])
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)
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
)
}
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
)
Each model is trained through the same preprocessing pipeline and evaluated using Mean Absolute Error (MAE) and R².
The reported results are:
| Model | MAE | R² |
|---|---|---|
| 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"
)
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"
)
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)
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"
)
The main route handles both page loading and form submission:
@app.route("/", methods=["GET", "POST"])
def home():
...
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")
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]
})
Finally:
prediction = model.predict(input_data)
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
the application can be started with:
python car_price_predictor/app.py
The Flask development server will normally be available at:
http://127.0.0.1:5000
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
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)