Back to work

Case study

Battery Remaining-Useful-Life Predictor

Predicts remaining useful life of Li-ion cells from cycling data, with an ablation that separates the sensor signal from the cycle count.

Role
Model development and analysis
Tools
Python, pandas, scikit-learn, XGBoost, SHAP
Status
Status: Completed

Key results

Sensor-only R²
0.945
XGBoost, no cycle counter, held-out cells
Sensor-only MAE
56.7 cycles
Same model and test cells
Cycle-counter baseline MAE
6.30 cycles
Linear regression, held-out cells

Overview

I built a pipeline that predicts remaining useful life (RUL), in cycles, for Li-ion cells from cycle-level charge and discharge measurements. The main finding is about the data, not the model: on this dataset, knowing where a cell is in its life explains almost all of the variance in RUL. A linear regression on the cycle counter alone reaches R² 0.9992 and a mean absolute error (MAE) of 6.30 cycles on two held-out cells, and the gradient-boosted models do not beat it.

The more useful result is the ablation. With the cycle counter removed, XGBoost predicts RUL from sensor features alone at R² 0.945 and MAE 56.7 cycles. That is the number that matters for estimating health from measurements without a full cycle history.

Problem and constraints

  • Data. The “Battery Remaining Useful Life” dataset from the Hawaii Natural Energy Institute (HNEI), published on Kaggle: a pre-engineered cycle-level feature table for 14 Li-ion cells, with 15,064 cycle records in total, 1,051 to 1,081 per cell. The cells are not labeled in the raw file. I detect cell boundaries where RUL resets upward between consecutive rows.
  • Target. The RUL column supplied with the table, which counts down monotonically to 0 at each cell’s last record.
  • Evaluation. The split is by cell, not by row, so every test cycle comes from a cell the model never saw. Cells 0 to 11 are training data (12,941 rows), and cells 12 and 13 are the held-out test set (2,123 rows).
  • Leakage. The processed table also carries rul, rul_pct and soh. In the code, soh and rul_pct are both computed as rul / max(rul) per cell, so they are a rescaled copy of the target. All three are excluded from the inputs.

Approach

Twelve inputs, taken from the feature list in the code:

  • The per-cell cycle counter (cycle_within_battery).
  • Seven raw measurements: discharge time, time for the discharge voltage to fall from 3.6 V to 3.4 V, max discharge voltage, min charge voltage, time at 4.15 V, time at constant current, and charging time.
  • Four derived features: charge efficiency ratio (discharge time / charging time), voltage spread (max discharge voltage minus min charge voltage), constant-current fraction (constant-current time / charging time), and a 5-cycle rolling mean of discharge time computed per cell.

Four models, all trained on the same split:

  1. Linear regression on the cycle counter only, as a baseline.
  2. Random forest on all twelve inputs (200 trees, max depth 10).
  3. XGBoost on all twelve inputs (300 estimators, learning rate 0.05, max depth 6).
  4. XGBoost with the same hyperparameters on the eleven sensor and derived inputs, with the cycle counter removed.

Analysis

Held-out test cells (12 and 13)

Model Inputs R² MAE (cycles) RMSE (cycles)
Linear regression Cycle counter only 0.9992 6.30 9.25
Random forest All 12 0.9991 6.50 9.43
XGBoost All 12 0.9991 6.81 9.73
XGBoost, sensor only 11, no cycle counter 0.9447 56.68 75.82

With the cycle counter available, the tree models match the one-feature baseline and do not improve on it, because the counter already encodes the signal they would otherwise have to learn from the measurements. Removing the counter raises MAE from 6.81 to 56.68 cycles, about 8.3 times the full XGBoost model’s error and about 9.0 times the baseline’s. An MAE of 56.68 cycles is about 5% of the mean record length of 1,076 cycles per cell.

Leave-one-battery-out cross-validation

To check that the result is not an artifact of which two cells were held out, I ran leave-one-battery-out (LOBO) cross-validation across all 14 cells. Each fold trains on 13 cells and tests on the 14th.

LOBO was run on the full XGBoost model, which includes the cycle counter. The sensor-only model was not cross-validated with LOBO, so its only result is the fixed split above.

LOBO, full XGBoost (with cycle counter) Value
Mean R² 0.9992
Mean MAE 5.90 cycles (std 4.47)
Folds with MAE under 8 cycles 13 of 14
Worst fold Cell 4, MAE 20.94 cycles, R² 0.9949

Feature attribution

SHAP values (tree explainer) for the full XGBoost model on the test cells confirm the same structure. The cycle counter has a mean absolute SHAP value of about 272 cycles. The next three inputs, discharge time, charge efficiency ratio and max discharge voltage, are each about 2 cycles or less.

Result

  • With the cycle counter, RUL on this dataset is close to a linear function of cycle position, and a one-feature linear model is the right baseline to beat. None of my models beat it.
  • Without the counter, the sensor and derived features still carry real degradation signal (R² 0.945 on unseen cells), at an MAE of 56.68 cycles.
  • The LOBO result (mean MAE 5.90 cycles) shows the fixed split was not unusually easy, but it applies to the model with the cycle counter.

Limitations

  • Only 14 cells, all from one dataset.
  • One degradation protocol. Nothing here shows how the models behave under different temperatures, C-rates or usage profiles.
  • No deployment and no hardware test. The models have not been run on a battery management system or against cells I cycled myself.
  • The sensor-only model, the result that matters most, was evaluated on one fixed split of two cells, not by LOBO.

What I’d do differently

  • Run LOBO on the sensor-only model as well, so its error has a spread across cells and not a single number.
  • Validate on a second, independently collected dataset with a different cycling protocol before claiming the sensor-only result generalizes.