What’s one thing you learned? What’s still confusing?
Recommender Systems: From Collaborative Filtering to Matrix Factorization
Open Netflix and stare at the homepage. Open Spotify and look at Discover Weekly. Open YouTube and watch "Up next" auto-queue. Open Amazon and read "Customers who bought…
Time-Series Fundamentals: Stationarity, ARIMA & Prophet
*Related Lessons Across Tracks*
Welcome to AI
What is AI? What is machine learning? Start your journey here — no experience needed.
Interactive Labs for This Track
Linear Regression
Predict house prices based on square footage — drag points and watch the best-fit line adjust
Gradient Descent Explorer
You're blindfolded on a hilly field trying to find the lowest valley — feel the slope and take steps downhill
Decision Boundary Explorer
You're a bank deciding who gets a loan — draw the line that separates approved from denied
Ask questions, share insights
Walk the hallway of any Kaggle grandmaster, any fraud team at Stripe, any churn analyst at Spotify, and ask them what actually moved the needle on their last big win. Almost nobody says "I switched from XGBoost to LightGBM." Almost everybody says some version of: "I built a feature that captured X." The unglamorous truth of applied ML is that the model is usually a commodity — sklearn ships dozens of them, all roughly equivalent on clean data — and the differentiator is the features you feed it. Andrew Ng calls this "data-centric AI." Pedro Domingos calls it "the unreasonable effectiveness of features." Practitioners just call it the job.
The reason this lesson is 40 minutes long instead of 10 is that "feature engineering" is not one trick — it is a discipline. The principles are simple, but the failure modes are everywhere, and most of them silently inflate your cross-validation score without you noticing. By the end you will have a mental checklist that survives contact with messy real data.
This is why the same algorithm can win or lose by 20 points depending on who is driving. Two data scientists, one XGBoost, two completely different feature sets — two completely different leaderboard finishes.
Most ML algorithms have an implicit assumption about how features are scaled. Get the scaling wrong and the model misbehaves silently — coefficients explode, gradients vanish, regularization punishes the wrong things, distance metrics get bullied by the feature with the biggest units.
There are four scalers you should know cold.
The most common: subtract the mean, divide by the standard deviation. Each transformed feature has mean 0 and standard deviation 1.
Use StandardScaler when: features are roughly symmetric, you are using a linear model (linear/logistic regression, ridge, lasso), an SVM with RBF/linear kernel, k-means, k-NN, PCA, or a neural network. Basically the default for anything that has a notion of distance or that regularizes weights.
[0, 1].[0, 1] (image pixels, neural network inputs that feed sigmoids, anything bounded). The downside: it is extremely sensitive to outliers. One outlier pushes x_max way out, and 99% of your data ends up squeezed into a sliver near zero.Use the median and IQR instead of mean and std. Outlier-resistant.
Use RobustScaler when: you have known outliers you cannot or do not want to remove (fraud amounts, salary, real estate prices in a city with mansions). The median ignores the tails; the mean does not.
Use Normalizer when: you care about direction, not magnitude. TF-IDF + cosine similarity is the canonical case. Document length should not determine similarity.
You are building a Random Forest or gradient boosting model. Do you need to scale your numerical features?
The rule of thumb worth memorizing: anything that measures distance or regularizes weights needs scaling. Trees do not.
Real-world numerical data is rarely Gaussian. Income is right-skewed (a few people make millions, most make tens of thousands). House prices are right-skewed. Click counts, session durations, file sizes — all right-skewed. A linear model trying to fit a feature where 99% of values are between 1 and 100, but 1% are between 10,000 and 1,000,000, gets pulled around by the long tail.
Three transformations solve this:
x' = log(x + 1) (the +1 avoids log(0)).log(1 + x), income distributions are roughly Gaussian. Click counts become roughly Gaussian. Anything with multiplicative structure (where doubling matters more than adding 100) benefits.Requires strictly positive inputs.
x' = sqrt(x). Milder than log. Use for count data with low values (number of children, number of doctor visits per year) where log would over-compress.λ that finds the optimal power for normality:The catch: Box-Cox requires strictly positive values. Has any zero or negative number? Box-Cox crashes.
You have a 'net_profit' feature that ranges from -$50,000 to +$2,000,000 (some companies lose money). You want a power transform to make it roughly Gaussian. Which do you reach for?
Sometimes the right move is to throw away precision. Convert a numerical feature into discrete buckets.
k equal-size intervals. Simple, but unbalanced if the distribution is skewed (most of your data ends up in one bin).(x, y), use the leaves as bins) captures non-linear relationships a linear model could not.Why bin? Three reasons:
[0-18, 18-25, 25-40, 40-65, 65+] gives a linear model the ability to express "older customers spend more, but seniors spend less than middle-aged" without polynomial features.The cost: you throw away ordering precision. A bin says "between 40 and 65," but loses the distinction between 41 and 64.
price is roughly sqft * price_per_sqft. You have to hand it the product. PolynomialFeatures(degree=2) in sklearn creates all pairwise products and squares of your features. For n features, you get O(n²) new features — explodes quickly.In practice, you do not blindly polynomial-expand everything. You either:
bmi = weight / height², revenue_per_user = revenue / dau).Categorical features are where most beginners get stuck. They cannot be fed to most models directly — you have to encode them as numbers. How you do that matters a lot.
The standard for low-to-medium cardinality (under ~50 unique values). Each category becomes its own 0/1 column.
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(
handle_unknown="ignore", # don't crash on new categories at predict time
sparse_output=False,
drop="first", # drop one column to avoid the dummy variable trap
)
X_encoded = ohe.fit_transform(X_categorical)drop="first": if you have a feature color in {red, green, blue} and you create three columns is_red, is_green, is_blue, they sum to exactly 1 for every row. That means one is a linear combination of the other two and the bias term — perfect multicollinearity. For linear regression with no regularization, the matrix X^T X becomes singular and the normal equation fails. The fix: drop="first" to drop one category and make it the implicit baseline (encoded as all-zeros).drop="first" is unnecessary (the regularizer breaks the degeneracy), but it does not hurt.0, 1, 2, .... Only valid when there is a real, meaningful order to the categories.from sklearn.preprocessing import OrdinalEncoder
# CORRECT use: education has a natural order
size_encoder = OrdinalEncoder(categories=[
["high_school", "bachelors", "masters", "phd"]
])
# WRONG use: cities have no order, but this would force one
# city_encoder = OrdinalEncoder() # would map alphabetically — meaninglessYou have a 'city' feature with values {New York, Los Angeles, Chicago, Houston}. You ordinal-encode it as {NYC=0, LA=1, CHI=2, HOU=3} and feed it to a linear regression for house price. What goes wrong?
The killer technique for high-cardinality categoricals (hundreds or thousands of unique values). Instead of creating one column per category, replace each category with the mean of the target for that category.
zipcode avg_house_price
10001 1,200,000
60601 650,000
77002 420,000
...
A feature with 30,000 zip codes becomes a single highly informative numerical column. One-hot would have produced 30,000 mostly-zero columns and overfit catastrophically.
The catch: target encoding leaks if you do not smooth and cross-validate it. Two bugs to know:
TargetEncoder (added in 1.3) that does this for you, or use category_encoders.TargetEncoder with cv set.Simpler cousin: replace each category with how often it appears.
freq_map = X_train["category"].value_counts(normalize=True).to_dict()
X_train["category_freq"] = X_train["category"].map(freq_map)
X_test["category_freq"] = X_test["category"].map(freq_map).fillna(0)Cheap, no leakage (counts do not use the target), often surprisingly useful — rare categories are often qualitatively different from common ones, and a single frequency column captures that.
k fixed buckets:from sklearn.feature_extraction import FeatureHasher
hasher = FeatureHasher(n_features=2**18, input_type="string")
X_hashed = hasher.transform([[s] for s in X_train["url"]])k = 2^18 = 262,144 buckets and most categories rare, collisions add a tolerable amount of noise. This is how online ad systems and real-time recommendation engines handle billion-category sparse data.Your 'user_id' feature has 50,000 unique values, with a long-tail distribution (a few users appear thousands of times, most appear once or twice). Which encoder should you reach for first?
nn.Embedding(num_users=50_000, dim=16) maps each user to a 16-dimensional vector that gets optimized end-to-end. This is how recommendation systems at YouTube, Netflix, TikTok, and Spotify handle user/item IDs. We come back to this in the deep learning track — for now, the takeaway is: when you outgrow target encoding, you do not start one-hot encoding harder. You learn embeddings.Text is its own world (full lesson in the NLP track), but for tabular ML with a text column, two techniques cover 90% of cases:
TfidfVectorizer in sklearn.n-character substrings. Robust to misspellings, captures word fragments, works for languages without clean word boundaries. TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5)).For deep learning text (BERT, sentence transformers), see the NLP track. For a free-text column inside a tabular pipeline, TF-IDF + char n-grams + truncated SVD (to reduce dimensionality) is the boring-but-strong workflow.
1715600000 does not mean anything. Expand it into components:df["hour"] = df["ts"].dt.hour
df["day_of_week"] = df["ts"].dt.dayofweek # 0 = Monday
df["day"] = df["ts"].dt.day
df["month"] = df["ts"].dt.month
df["quarter"] = df["ts"].dt.quarter
df["year"] = df["ts"].dt.year
df["is_weekend"] = df["day_of_week"].isin([5, 6]).astype(int)
df["is_holiday"] = df["ts"].dt.date.isin(US_HOLIDAYS).astype(int)
df["days_since_signup"] = (df["ts"] - df["signup_ts"]).dt.days(sin, cos) is needed — using only one collapses two different hours to the same value (hour 6 and hour 18 both have sin(2π·6/24) = sin(2π·18/24) = 1 and -1 respectively, but cos distinguishes them).hour ≤ 0 and hour ≥ 22 separately and recover the cyclical structure. For linear models, neural networks, and SVMs, cyclical encoding is the difference between a model that thinks midnight is far from 1 AM and one that doesn't.k rows most similar on the other (non-missing) features and average their values. More accurate, much slower, requires scaling first.IterativeImputer in sklearn. Most accurate, also the most expensive.is_missing column for each feature with missing values:for col in numerical_cols_with_missing:
df[f"{col}_was_missing"] = df[col].isna().astype(int)
df[col] = df[col].fillna(df[col].median())For linear models, k-NN, SVMs, neural networks — you must impute. NaN inputs break the math.
Once you have generated 200 features, which ones do you keep? Three families of methods.
k. Fast, model-agnostic.SelectKBest(score_func=mutual_info_classif, k=50).Cheap and easy. Weakness: ignores feature interactions — a feature can be useless alone but powerful in combination, and filter methods will throw it away.
Search over subsets of features, evaluating each subset by training a model and measuring CV score.
Better than filter methods because they respect interactions. Much slower — you train a model for every subset evaluated.
The model itself does the selection during training. Best of both worlds.
alpha until you have the count you want.feature_importances_ — how much each feature reduced impurity across all trees. Top-k by importance.In practice, the production pipeline is usually: filter (variance threshold to drop trivially useless features) → embedded (Lasso or tree importance to drop weak features) → keep what matters.
Every senior ML interviewer has the same favorite trap: ask the candidate to describe their preprocessing pipeline and listen for data leakage. Most candidates fail.
# BROKEN
scaler = StandardScaler().fit(X) # uses test statistics!
X_scaled = scaler.transform(X)
X_train, X_test = train_test_split(X_scaled, ...)The scaler's mean and standard deviation were computed using test data. Information about the test distribution leaked into the transformation. Your cross-validation score is optimistic.
The fix:
# CORRECT
X_train, X_test = train_test_split(X, ...)
scaler = StandardScaler().fit(X_train) # train only
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test) # apply train stats to testfit method. Fit on train only.A more insidious leak: a feature that looks innocent but is computed using the target.
These are nasty because the model train/test split does not catch them. The test set has the same leak as the train set. Only production reveals the bug.
For any problem with time, you must split temporally — train on the past, test on the future. Random shuffling lets information from the future bleed into training:
KFold instead of TimeSeriesSplit on time-series data.Which of these is data leakage?
Pipeline, and use ColumnTransformer to apply different transformations to different columns. There is no reason to do this manually. Pipelines:pipeline.fit(X_train) fits every step on training data; pipeline.transform(X_test) reuses the fit. Impossible to accidentally fit a scaler on test data.cross_val_score(pipeline, X, y, cv=5) fits the entire pipeline (scalers, encoders, model) inside each fold. Manual preprocessing followed by cross_val_score(model, ...) leaks across folds.joblib.dump(pipeline, "model.pkl") saves the whole transform-and-predict path. Production inference is one pipeline.predict(new_row) call.Let's build one.
Theory is one thing. Let's actually watch what happens when target encoding is done wrong vs. right. The cell below computes target encoding two ways: (1) using the full training set (the classic bug), (2) using out-of-fold computation. The cross-validation score for the broken version will be optimistically inflated.
The gap between the two AUC bars is the "lie" — the amount your model would appear to have improved if you had not noticed the leak. In a real Kaggle context, this lie has cost real teams real prize money when their public leaderboard score (computed on leaked features) collapsed on the private leaderboard.
You are one-hot encoding a 'color in {red, green, blue}' feature for an unregularized linear regression. Why drop one column?
When you sit down with a new tabular dataset, the order of operations:
df.info(), df.describe(), df.isna().sum(), histograms. Get a feel for distributions and missingness before you touch anything.train_test_split before any transformation. Never let a fitted transformer see test data.is_missing flag + imputer; or leave NaN alone for XGBoost/LightGBM.ColumnTransformer to apply different sub-pipelines to different columns. The full chain must end in the model.is_missing flags before imputing. XGBoost/LightGBM/CatBoost handle NaN natively — for them, don't impute at all.cross_val_score(pipeline, X, y, cv=...)