Home / Featured work / Bulldozer price regression

Bulldozer price regression: what will equipment sell for at auction?

Classic tabular machine learning done properly on 412,698 real auction records — time-aware validation, a tuned XGBoost model, and an interpretability finding that says as much about data collection as about bulldozers.

RoleSolo project
StackPython · pandas · scikit-learn · XGBoost · Docker · MLflow
Headline result~$10,480 validation MAE · 0.398 RMSLE · 0.648 R²

Problem

Auction buyers and sellers need a practical way to estimate equipment value from historical sale records. The goal: predict sale price from equipment attributes, sale timing, machine metadata, and listing details.

Data

412,698 auction records from the Kaggle Blue Book for Bulldozers competition — real-world heavy-equipment sales with dozens of categorical configuration fields and substantial missingness. Per Kaggle's terms the raw data is not redistributed in the repo; the README documents how to place the competition files locally to reproduce every result.

Method

  • Parse sale dates and engineer year, month, day, day-of-week, and day-of-year features.
  • Encode categoricals numerically and add a missingness indicator column per field — treating "this value was never recorded" as information rather than noise.
  • Fill numeric gaps with medians.
  • Split validation by sale year, not randomly — the model is always judged on selling the future, never on memorizing it.
  • Compare Random Forest and XGBoost; tune XGBoost; evaluate with MAE, RMSLE (the Kaggle competition metric), and R².

Results

Tuned XGBoost, validation split (from the project README's saved results)
MetricValidation result
MAE (average dollar error)about $10,480
RMSLE (competition metric)0.398
R²0.648
Scatter plot of validation prediction errors for the tuned XGBoost model
Validation prediction errors for the tuned model, from the notebook.

What mattered — the interesting part

The model's strongest signals aren't horsepower or age. They're missingness fields: whether the listing bothered to record a scarifier, a coupler system, a product size. Incomplete records line up with older, less-documented, lower-value equipment — so listing completeness itself carries pricing signal. For a business, that finding changes data-collection policy, not just the model.

Top ten feature importances for the tuned XGBoost model, led by Scarifier-is-missing at 0.43
Top ten features: missingness indicators dominate (Scarifier_is_missing at 0.43).

Reproducibility

The notebook is the narrative artifact; a command-line training wrapper (src/train.py) reruns the tuned model outside it, logging parameters, metrics, feature importance, predictions, and the fitted model to MLflow. A Dockerfile makes the whole training run a one-line container command. If it can't be rerun from a fresh clone, it isn't done.

Takeaway

A real dataset, a practical pricing problem, time-aware validation, model comparison, and interpretable results — with deliberate restraint: Spark was evaluated and rejected because the data fits in memory, and Docker + MLflow were chosen instead because they improve reproducibility without changing the project's goal.

Limitations

  • Results are validation-split metrics, not competition leaderboard scores on the hidden test set.
  • The competition data ends in the early 2010s — patterns learned here describe that market era.
  • Median imputation is deliberately simple; the missingness indicators carry the signal instead.

Next improvements

  • Move the notebook and saved outputs fully into the project folder.
  • Add a small saved-output sample after a local Kaggle rerun.