missing values: first, don't guess blindly

Before filling in a missing value, ask why it's missing — the answer changes what "filling it in" even means. If author_seniority_years is blank because the run was triggered by an automated dependency-bot rather than a human, there's no real number to recover; the value is missing because the underlying quantity genuinely doesn't apply, and inventing one would be fabricating information. If lines_changed is blank because of a one-off logging bug on a specific date, the true value existed and simply wasn't captured — that's a case worth trying to recover or estimate.
ApproachTrade-off
Drop the columnsimplest, but throws away every other value in that column, even the useful ones — only reasonable when the vast majority of a column is missing
Impute (fill with the mean, median, or most common value)keeps the column and the row; the filled value is never exactly right, but this usually beats dropping the column entirely
Impute + add a "was missing" indicator columnkeeps the imputed guess honest by giving the model a way to tell "this was a real measurement" from "this was filled in" — sometimes meaningfully better, sometimes no different, worth testing

scaling vs. normalization: not the same operation

The two terms get used interchangeably in casual conversation, but they do different things. Scaling changes the range of a feature — squeezing lines_changed (which might span 1 to 5,000) and files_changed (which might span 1 to 40) onto a comparable range, without changing the shape of their distributions. Normalization changes the shape of a feature's distribution — reshaping it to look more like a standard bell curve, for algorithms that assume roughly normally-distributed input.
The practical motivation for scaling connects directly back to Gradient Descent: features on wildly different scales distort the cost surface into a long, narrow valley instead of a round bowl, and gradient descent zig-zags slowly down a narrow valley instead of heading straight for the minimum. Distance-based algorithms have their own, separate reason to care — k-nearest neighbors and SVMs (both covered later in this track) measure literal distance between points, so an unscaled feature with a huge numeric range would dominate that distance calculation regardless of how relevant it actually is.

two ways to scale: mean normalization and z-score


# mean normalization -- squeezes values into roughly [-1, 1]
x_scaled = (x - mean(x)) / (max(x) - min(x))

# z-score normalization (standardization) -- centers on 0 with unit spread,
# not bounded to a fixed range the way mean normalization is
x_scaled = (x - mean(x)) / std(x)
            
Both center the data around zero; the difference is what they divide by. Mean normalization uses the full range (max minus min), so it's sensitive to a single extreme outlier — one 50,000-line diff stretches the denominator for every other value. Z-score normalization divides by the standard deviation instead, which is far less distorted by one extreme point, and is the more common default for exactly that reason.

the leakage trap: fit scalers on training data only

The mean and standard deviation used to scale a feature must be computed from the training split only, then applied unchanged to the validation and test splits. Computing them from the full dataset before splitting leaks information from validation/test rows into the numbers used to transform the training data — a subtle version of exactly the leakage problem covered in Train/Test Splits & Cross-Validation. In code, this means calling .fit() only on the training split, then .transform() (never .fit() again) on validation and test — a distinction the scikit-learn API is deliberately built around.

encoding categorical features

Linear Regression already flagged the core trap: assigning arbitrary integers to categories (0, 1, 2...) implies an ordering and a distance between categories that usually isn't real. The right encoding depends on whether that ordering genuinely exists.
EncodingUse whenExample
Ordinal / label encodingcategories have a real, meaningful ordertest_severity: low=0, medium=1, high=2
One-hot encodingcategories have no order, and there aren't too many of thembranch_type: feature/release/hotfix → three binary columns
Target encodingtoo many distinct categories for one-hot to be practical (high cardinality)author, with hundreds of distinct contributors
One-hot encoding turns a k-category feature into k binary columns — clean and unambiguous, but a author column with 400 distinct contributors would explode into 400 mostly-empty columns. Target encoding solves the high-cardinality case differently: replace each category with a number derived from the target itself — each author's historical failure rate, for instance, instead of an arbitrary index or a wall of one-hot columns.
Target encoding has a sharp edge worth naming explicitly: computing "this author's failure rate" directly from the same rows being used to train the model leaks the label into the feature — a new, subtler instance of the leakage problem raised twice already on this page. The standard fix is smoothing, blending each category's own average with the overall average, weighted by how much data that category actually has:

weight = n / (n + m)   # n = how many times this author appears in training data
                       # m = a smoothing hyperparameter, larger m trusts the overall average more

encoded_value = weight * this_authors_failure_rate + (1 - weight) * overall_failure_rate
            
A prolific author with hundreds of runs gets a weight close to 1 — their own history is trusted. A first-time contributor with one run gets a weight close to 0 — their single data point is too noisy to trust alone, so the encoding falls back toward the overall average instead of overreacting to one observation.

creating new features, briefly

Not all feature engineering is about reshaping existing columns — sometimes the most useful feature is a new one, built by combining what's already there. lines_changed / files_changed (average lines touched per file) can carry signal that neither input carries alone; a count feature like "number of risk factors present" (touches core code AND skips tests AND is a large diff, summed as 0/1 flags) aggregates several weak signals into one stronger one. This kind of feature creation is where domain knowledge about CI systems — or whatever the actual problem is — tends to matter more than any general-purpose technique.

practical notes

from sklearn.preprocessing import StandardScaler| https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html | z-score normalization -- scaler.fit(X_train) then scaler.transform(X_train) and scaler.transform(X_test), never fit() on test data |'fe_sk1'
from sklearn.preprocessing import OneHotEncoder| https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html | handle_unknown='ignore' avoids a crash when the test split contains a category never seen during training |'fe_sk2'
from sklearn.compose import ColumnTransformer| https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html | applies different preprocessing to different columns (scale the numeric ones, one-hot the categorical ones) in a single fit/transform step |'fe_sk3'
df['lines_changed'].isnull().sum()| | pandas' standard way to audit missing values per column before deciding drop vs. impute |'fe_sk4'

where to go from here

Next in this track: Ensemble Methods — random forests and gradient boosting, combining many decision trees to fix the instability from that page.
Linear Regression — where the categorical-encoding trap first came up.
Train/Test Splits & Cross-Validation — the split discipline every leakage warning on this page depends on.

reference

scikit-learn — Preprocessing Data
Google ML Crash Course — Working with Categorical Data