Section 2 · Lesson 3

Preprocess without leaking

Turn raw columns into clean numbers, and fit every learned step on training data only.

You'll learn

Two pipelines for two jobs

The project has two preprocessing designs, and the difference teaches you something:

Both follow the same safety rule, covered later in this lesson.

Clean the data

Milestone 2 handled each data-quality issue with a stated reason:

IssueRowsActionReason
Exact duplicates1,116Drop from train (leaves 278,630 rows)Copies in both a training fold and a validation fold inflate the score.
Conflicting duplicates43 groupsKeepYou can't tell which label is right.
Invalid max_term_*62Cap at 1,125 daysThe rest of each row is valid.
Scores with zero reviews4Set scores to missingMatches the 54,000 other unreviewed listings.

The project doesn't drop duplicates from the test set, because every test listing needs a prediction.

Fill missing values

The method follows the reason for the gap (Lesson 2):

Dropping rows would lose 20-77% of the data. Mean imputation gets pulled by the long right tails. KNN and iterative imputation are slow on 280,000 rows and add little for tree models.

Tame heavy tails

Count columns such as review_count have long right tails. For the linear baseline, the pipeline winsorizes at the 0.5th and 99.5th training percentiles, applies log1p, and then scales. The skew of review_count falls from 5.36 to 0.08. The clip bounds are learned in fit, so a custom transformer stores them:

class Clipper(BaseEstimator, TransformerMixin):
    def __init__(self, low=0.005, high=0.995):
        self.low, self.high = low, high
    def fit(self, X, y=None):
        self.lo_ = np.nanquantile(X, self.low, axis=0)
        self.hi_ = np.nanquantile(X, self.high, axis=0)
        return self
    def transform(self, X):
        return np.clip(X, self.lo_, self.hi_)
Histograms of review_count and reviews_per_month before and after clipping and a log transform.
Skewed counts before and after clipping and log1p (Milestone 2, Figure Q4.1).

Clip and log-transform a skewed count

The data is a synthetic log-normal sample of 4,000 values, standing in for a column such as review_count. Lower the clip percentile and switch on log1p, and watch the skewness drop.

Trees don't care. Gradient-boosted trees split on the order of values, so clipping, logs, and scaling change little for them. The project applies these steps in Milestone 2 because the baseline is a logistic regression, which is sensitive to scale. The final LightGBM notebook skips them.

Encode categories by their cardinality

Milestone 2 chose an encoding for each column based on its number of levels:

Fit on training data only

Data leakage means information from outside the training data influences the model or its validation score. The most common cause is fitting a learned step, such as an imputer's median, a scaler's mean, or an encoder's category means, on data that includes the validation fold or the test set. The score looks better than reality, and the gap appears only on new data.

The project follows these rules:

Diagram of the Milestone 2 pipeline blocks and the number of output columns per block.
The Milestone 2 pipeline: 363 numeric columns and no missing cells, for both train and test (Figure Q9.1).

You compute the median of review_count from train and validation together, then fill the gaps in both. What went wrong?

  1. Nothing. The median is only a summary.
  2. The validation fold influenced a learned step, so the validation score is slightly optimistic.
  3. The median should have been the mean.

Any statistic computed with validation rows leaks a little information about them. The effect is small for a median, but the habit matters, and it grows large for learned encodings.

Does preprocessing matter? A logistic regression test

Milestone 2 tested the effect on a logistic regression, with 5-fold stratified cross-validation and macro F1. The minimal setup used only the numeric columns with median filling. The preprocessed setup used the full pipeline, refit inside each fold.

0.358
Minimal (std 0.008)
0.682
Preprocessed (std 0.002)
+0.325
Gain from preprocessing
Bar chart of logistic regression macro F1: minimal 0.358 and preprocessed 0.682.
Logistic regression macro F1 with and without preprocessing (Figure Q10.1).

The minimal model scores far lower partly because unscaled columns such as max_term_days, in the hundreds of millions, make the optimization badly conditioned, so 300 iterations aren't enough. It also lacks every categorical, facility, and operator signal. For context, the final LightGBM notebook reaches about 0.758.

Why does the final LightGBM notebook skip scaling and one-hot encoding?

  1. LightGBM can't read scaled data.
  2. Scaling always hurts accuracy.
  3. Trees split on the order of values, so scale doesn't matter, and LightGBM handles categories natively.

Scale matters for linear and distance-based models. The notebook instead spends its effort on features that tell the trees something new.