CO2 Topic 8 of 25
Feature engineering: scaling, encoding and missing values
Models eat numbers. Decide how to scale them, how to encode categories, and how to fill gaps, then put it all into one pipeline that travels with the model.
You will be able to
- Choose between StandardScaler and MinMaxScaler
- Apply one-hot, ordinal and target encoding
- Compare mean, median and model-based imputation
- Use MissingIndicator features
- Build a ColumnTransformer pipeline
Read
- Textbook 1, Ch. 2, pp. 67–89
- Textbook 3, Ch. 3, pp. 71–120
Intuition
Our table contains numbers on different scales, a category (Stream), an order (CGPA_Tier), and gaps. Each needs its own treatment. Done well, this often improves results more than changing the algorithm.
How it works
Scaling:
| Method | Formula | When |
|---|---|---|
| StandardScaler | (x − mean) / std | The default for gradient-based models and distances |
| MinMaxScaler | (x − min) / (max − min) | When a fixed range is needed |
Trees do not need scaling, as they compare one feature at a time.
Encoding categories:
| Method | Example | Use for | Watch out |
|---|---|---|---|
| One-hot | CS → 1,0,0,… | No natural order, few values | Many values means many columns |
| Ordinal | Low, Mid, High → 0,1,2 | A real order | Invents an order if there isn't one |
| Target | Stream → its placement rate | Many values | Leaks the target unless fitted carefully |
| Embedding (preview) | Stream → a learned vector | Very many values | Needs a neural network; see DLAC |
Missing values:
| Method | Idea | Trade-off |
|---|---|---|
| Deletion | Drop rows or columns | Loses data, and may bias what is left |
| Mean or median | One fixed value per column | Simple. The median resists outliers |
| Model-based (KNN, iterative) | Predict the gap from other columns | Better, but slower and more complex |
| MissingIndicator | Add a "was missing" column | Keeps the signal if a gap means something |
A worked standardisation. Values 2, 4, 6, 8: mean 5, population standard deviation √5 = 2.236. So 8 becomes (8 − 5)/2.236 = 1.342.
The golden rule. Every statistic, including the mean, the scale and the category list, is learned from the training data only. A Pipeline guarantees this, and it travels with the model, which prevents the skew of Topic 3.
Run it: compare the choices
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer, KNNImputer, SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler, TargetEncoder
from placement_predict import CATEGORICAL, NUMERIC, load_data, train_val_test
df = load_data()
X_train, X_val, X_test, y_train, y_val, y_test = train_val_test(df)
def build(imputer, encoder, indicator=False):
if indicator:
imputer.set_params(add_indicator=True)
numeric = Pipeline([("fill", imputer), ("scale", StandardScaler())])
category = Pipeline([("fill", SimpleImputer(strategy="most_frequent")), ("encode", encoder)])
return ColumnTransformer([("num", numeric, NUMERIC), ("cat", category, CATEGORICAL)])
def score(prep):
model = make_pipeline(prep, LogisticRegression(max_iter=1000)).fit(X_train, y_train)
return roc_auc_score(y_val, model.predict_proba(X_val)[:, 1]), prep.transform(X_train).shape[1]
onehot = OneHotEncoder(handle_unknown="ignore")
choices = {
"Median + one-hot": build(SimpleImputer(strategy="median"), onehot),
"Mean + one-hot": build(SimpleImputer(strategy="mean"), onehot),
"Median + indicator": build(SimpleImputer(strategy="median"), onehot, indicator=True),
"KNN imputer": build(KNNImputer(n_neighbors=5), onehot),
"Iterative imputer": build(IterativeImputer(random_state=0), onehot),
"Median + target enc": build(SimpleImputer(strategy="median"), TargetEncoder(random_state=0)),
}
print(f"{'Pipeline':22} {'val AUC':>8} {'columns':>8}")
for name, prep in choices.items():
auc, columns = score(prep)
print(f"{name:22} {auc:8.4f} {columns:8}")
print("\nRows lost if we simply deleted gaps:", int(X_train.isna().any(axis=1).sum()), "of", len(X_train))
Here the imputers score about the same. That is a useful result too: use the simplest choice that does the job.
Try this: make Workshops go missing only for students with no workshops, using df.loc[df.Workshops == 0, "Workshops"] = np.nan. Does the indicator now help?
Common mistakes
- Fitting on all data before splitting. That leaks information from the validation data.
- One-hot encoding a student ID. It adds thousands of useless columns.
- Ignoring unseen categories. Without
handle_unknown="ignore", a new Stream crashes the service.
Problems
- Standardise 10, 20 and 30 by hand, using the population standard deviation.
- Normalise 5 to the range 0 to 1, where min = 0 and max = 20.
- A column has 6 categories. How many columns does one-hot encoding make?
- Why can plain target encoding leak the label? How does cross-fitting help?
- Add a new feature,
SkillScore, as the mean of the coding, aptitude and communication scores, and test whether it helps.
Flashcards
Say the meaning to yourself, then select the card to check.
- StandardScaler
- (x − mean) / std.
- One-hot encoding
- One column per category.
- MissingIndicator
- Adds a column recording that a value was missing.
- Golden rule of preprocessing
- Learn every statistic from training data only.