The problem in brief
A carbon-monoxide sensor is expensive and wears out quickly, so the task is to build a soft sensor: predict the hourly response of a tin-oxide CO sensor (the column PT08.S1(CO)) from four cheaper measurements, namely temperature, relative humidity, NO₂ and total NOx. You get 6 549 labelled hours, then 1 872 "validation" hours (public leaderboard) and 936 "test" hours (private leaderboard) without labels. Broken readings are stored as -200, in the features and in the target.
The data comes from the Kaggle competition archive. The labels of the validation and test hours are private and we never submitted anything, so all scores below are 5-fold cross-validation on the 6 549 training hours.
How you are scored
Root mean squared error:
Lower is better. Squaring makes big misses very costly, which matters a lot here, as we will see.
What to aim for. The only public-leaderboard entry is the organisers' baseline, 166.7 on the validation hours.
First look at the data

The target is a sensor resistance around 1 050 (most values between 650 and 2 000), except for 251 rows equal to -200 (left panel). Those are not real CO levels; they are failure codes. Looking at when they happen gives the first insight:
| temperature = -200 | temperature valid | |
|---|---|---|
| target = -200 | 251 | 0 |
| target valid | 0 | 6 298 |
The target is -200 exactly when the temperature is -200, on every single training row. Temperature, humidity and the CO sensor sit in the same device: when it fails, all three report -200. NO₂ and NOx come from other analysers, are missing on 18 % of the rows, and their failures are unrelated to the target's.
Why does this matter so much? A model that ignores it predicts about 1 050 on those rows, an error of 1 250. On 3.8 % of the rows, that alone adds
to the RMSE, more than the whole baseline score. The validation and test files have the same share of failed temperatures (4.4 % and 3.4 %), so the same rows will appear there.
On the valid rows, the CO sensor follows the traffic pollution: its correlation with is 0.72 (middle panel). Cars emit CO and NOx together, so NOx is a good proxy for CO.
A first baseline
| approach | CV RMSE |
|---|---|
| constant mean | 327.7 |
| constant mean, but -200 when the temperature is -200 | 212.4 |
| linear regression (missing values imputed by the median) + the same rule | 140.5 |
The rule alone takes the constant predictor from 328 to 212. A linear model on the four features then reaches 140.5, already better than the published baseline.
The key idea
Treat -200 as information, not as a number. There are two kinds of -200 and they need different treatments:
- In the target, it is a deterministic consequence of a feature: predict it with the rule
Temp = -200 → -200, and train the regression only on the rows with a real reading. Otherwise these 251 outliers dominate a squared-error fit and bias every other prediction. - In the features, it is a missing value. Replacing it by
NaNand using gradient-boosted trees, which learn a separate direction for missing values at each split, lets the model use "NOx analyser offline" as a signal instead of reading -200 as an extremely clean hour.
A tree model can also find the rule by itself, because a missing temperature is a perfect split. That is exactly what happens: LightGBM without the explicit rule scores 122.9, with the rule 122.8. The rule is crucial for a linear model and harmless for trees, so we keep it explicitly: it documents the physics and protects against any model that might smooth over it.
Two physics-inspired features add a little:
- Absolute humidity. Tin-oxide sensors react to water vapour, and the amount of water in the air depends on both temperature and relative humidity: grams per cubic metre (Magnus formula).
- The NOx / NO₂ ratio, which changes with the age of the emissions and the type of vehicles.
The full solution
- Predict -200 wherever the temperature is -200.
- On all other rows, replace -200 by
NaN, add absolute humidity and the NOx / NO₂ ratio. - Fit LightGBM (1 500 trees, learning rate 0.01, 15 leaves, at least 30 rows per leaf, row and column subsampling 0.8, L2 = 3) on the training rows with a valid target.
ok = Xtr.Temp_Lima.values != -200 # train on real readings only
m = lgb.LGBMRegressor(**PARAMS).fit(features(Xtr)[ok], ytr[ok])
pred = np.where(Xte.Temp_Lima.values == -200, -200.0, m.predict(features(Xte)))
- Predict the validation and test hours and write both into the submission file.
Results
5-fold cross-validated RMSE on the training hours (the official labels are private).
| approach | RMSE, all rows | RMSE, valid rows only |
|---|---|---|
| constant mean | 327.7 | 222.3 |
| constant mean + failure rule | 212.4 | 216.6 |
| linear regression + failure rule | 140.5 | 143.3 |
| LightGBM, raw features, no rule | 122.9 | 125.3 |
| LightGBM + failure rule | 122.8 | 125.2 |
| LightGBM + rule + humidity and ratio features | 121.1 | 123.5 |
| same, tuned (final) | 120.9 | 123.4 |
The final model reaches RMSE 120.9 in cross-validation, against 166.7 for the organisers' baseline on the public leaderboard. The remaining error is large in absolute terms (about 10 % of the typical value), and that is expected: four features cannot tell rush hour from night, wind from calm air, or a fresh sensor from an aged one. Without the time of day the model has no way to recover more, and the gains from tuning are already tiny.
What to remember
- Look at the special values before modelling. A sentinel code like -200 is not a measurement; count where it appears and in which combinations.
- A deterministic rule beats any model. "Target is -200 exactly when the temperature is -200" holds on all 6 549 rows; hard-code it, and train the model on the rest.
- Missing is a value. Trees with native missing-value handling use "sensor offline" as information; median imputation throws it away.
- RMSE punishes outliers quadratically. 4 % of rows wrong by 1 250 cost more than everything else together, so they deserve the first look.
- Domain knowledge gives features. Absolute humidity is what a gas sensor physically responds to; relative humidity alone is only half of it.