Day 44: Feature Engineering & Scaling
✅ Today you will learn
- Why raw columns are rarely model-ready
- Scaling: Standard, MinMax, Robust – when each
- Encoding: one-hot, ordinal, frequency
- New features: ratios, dates, text, interactions
- Data leakage – the silent killer
🏢 In the real world
Two teams get the same loan data and the same algorithm. Team A feeds raw columns and gets AUC 0.65. Team B adds a loan-to-income ratio, scales features and encodes employment – AUC 0.91. Feature preparation, not the algorithm, made the difference. That is why “feature engineering” appears in nearly every data-science job description.
Same model, better features
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.metrics import roc_auc_score
l = pd.read_csv("loan_applications.csv")
y = l["approved"]
raw = l[["monthly_income", "credit_score", "loan_amount", "years_employed"]]
eng = raw.assign(lti=l["loan_amount"] / (l["monthly_income"] * 12), salaried=(l["employment"] == "Salaried").astype(int))
for name, X, model in [("raw columns", raw, LogisticRegression(max_iter=5000)),
("+ scaling", raw, make_pipeline(StandardScaler(), LogisticRegression())),
("+ scaling + new features", eng, make_pipeline(StandardScaler(), LogisticRegression()))]:
a, b, c, d = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
print(f"{name:26s} AUC = {roc_auc_score(d, model.fit(a, c).predict_proba(b)[:, 1]):.3f}")Scaling
import pandas as pd
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
l = pd.read_csv("loan_applications.csv")
x = l[["monthly_income"]]
out = pd.DataFrame({"raw": x["monthly_income"],
"standard": StandardScaler().fit_transform(x).ravel(),
"minmax": MinMaxScaler().fit_transform(x).ravel(),
"robust": RobustScaler().fit_transform(x).ravel()})
print(out.describe().loc[["mean", "std", "min", "50%", "max"]].round(2))| Scaler | Formula | Use when |
|---|---|---|
| StandardScaler | (x − mean) / std | Default for linear models, KNN, SVM, neural nets |
| MinMaxScaler | (x − min) / (max − min) → 0–1 | Bounded inputs, images, neural nets |
| RobustScaler | (x − median) / IQR | Heavy outliers (incomes, prices) |
| None | – | Tree models (trees, forests, boosting) |
If you call fit on all data before splitting, the test set’s mean and spread leak into training. Use fit on train, transform on test – or simply put the scaler inside a Pipeline (Day 45).
Encoding categories
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
h = pd.read_csv("house_prices.csv")
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore").fit(h[["city"]])
print(ohe.get_feature_names_out(), ohe.transform(pd.DataFrame({"city": ["Pune", "Indore"]})))
ordi = OrdinalEncoder(categories=[["Unfurnished", "Semi-Furnished", "Furnished"]]).fit(h[["furnishing"]])
print(ordi.transform(pd.DataFrame({"furnishing": ["Unfurnished", "Furnished"]})).ravel())import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler
h = pd.read_csv("house_prices.csv")
num = ["area_sqft", "bedrooms", "bathrooms", "age_years", "metro_km"]
pre = ColumnTransformer([
("num", StandardScaler(), num),
("city", OneHotEncoder(handle_unknown="ignore", sparse_output=False), ["city", "parking"]),
("furn", OrdinalEncoder(categories=[["Unfurnished", "Semi-Furnished", "Furnished"]]), ["furnishing"]),
])
Xt = pre.fit_transform(h)
print(Xt.shape)
print(pd.DataFrame(Xt, columns=pre.get_feature_names_out()).head(3).round(2))Creating new features
import numpy as np
import pandas as pd
h = pd.read_csv("house_prices.csv")
f = pd.DataFrame(index=h.index)
f["area_per_bedroom"] = h["area_sqft"] / h["bedrooms"] # ratio
f["near_metro"] = (h["metro_km"] <= 1).astype(int) # flag from domain knowledge
f["age_band"] = pd.cut(h["age_years"], [-1, 5, 15, 30], labels=[0, 1, 2]).astype(int) # binning
f["log_area"] = np.log(h["area_sqft"]) # skew fix
f["area_x_metro"] = h["area_sqft"] * h["metro_km"] # interaction
print(f.head().round(2))
print(f.corrwith(h["price_lakh"]).round(2).to_string())import pandas as pd
s = pd.read_csv("sales_data.csv", parse_dates=["order_date"])
feat = pd.DataFrame({"month": s["order_date"].dt.month,
"weekday": s["order_date"].dt.dayofweek,
"is_weekend": (s["order_date"].dt.dayofweek >= 5).astype(int),
"days_to_diwali": (pd.Timestamp("2024-10-31") - s["order_date"]).dt.days.clip(lower=0),
"festive": s["order_date"].between("2024-10-01", "2024-11-15").astype(int)})
print(feat.head(3))
r = pd.read_csv("product_reviews.csv")
txt = pd.DataFrame({"n_words": r["review"].str.split().str.len(),
"n_exclaim": r["review"].str.count("!"),
"has_refund": r["review"].str.contains("refund|return", case=False).astype(int)})
print(txt.assign(stars=r["stars"]).groupby("has_refund")["stars"].mean().round(2))Missing values inside the model
import pandas as pd
from sklearn.impute import SimpleImputer
s = pd.read_csv("sales_data.csv")
imp = SimpleImputer(strategy="median", add_indicator=True)
out = imp.fit_transform(s[["customer_age", "rating"]])
print(pd.DataFrame(out, columns=imp.get_feature_names_out()).iloc[s["rating"].isna().to_numpy().nonzero()[0][:3]])Data leakage
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
l = pd.read_csv("loan_applications.csv")
rng = np.random.default_rng(0)
# a column only known AFTER the decision: the disbursed amount (0 if rejected)
l["disbursed_amount"] = np.where(l["approved"] == 1, l["loan_amount"] * rng.uniform(0.9, 1.0, len(l)), 0)
y = l["approved"]
for cols in [["credit_score", "monthly_income", "loan_amount"], ["credit_score", "monthly_income", "loan_amount", "disbursed_amount"]]:
a, b, c, d = train_test_split(l[cols], y, test_size=0.25, random_state=0, stratify=y)
auc = roc_auc_score(d, RandomForestClassifier(random_state=0).fit(a, c).predict_proba(b)[:, 1])
print(f"{'+ disbursed_amount' if len(cols) == 4 else 'honest features':20s} AUC = {auc:.3f}")AUC 1.000 is not a great model – it is cheating. Ask for every feature: “Will I know this value at the moment I make the prediction?” If not, remove it. Other leaks: scaling before splitting, duplicates across train and test, using future dates in time series.
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
area_per_bedroom, near_metro and log_area to the house model from Day 39. Does MAE improve? Then add city × area and metro × area interactions instead.Show solution
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
h = pd.read_csv("house_prices.csv")
X = pd.get_dummies(h.drop(columns="price_lakh"), drop_first=True, dtype=int)
X2 = X.assign(area_per_bed=h["area_sqft"] / h["bedrooms"], near_metro=(h["metro_km"] <= 1).astype(int), log_area=np.log(h["area_sqft"]))
X3 = X.copy()
for c in [k for k in X if k.startswith("city_")]:
X3[c + "_x_area"] = X[c] * h["area_sqft"] # each city gets its own price per sqft
X3["metro_x_area"] = h["metro_km"] * h["area_sqft"]; X3["age_x_area"] = h["age_years"] * h["area_sqft"]
for name, XX in [("original", X), ("+ ratio/flag/log", X2), ("+ interactions", X3)]:
a, b, c, d = train_test_split(XX, h["price_lakh"], test_size=0.25, random_state=42)
print(f"{name:17s} MAE ₹{mean_absolute_error(d, LinearRegression().fit(a, c).predict(b)):.2f} lakh")
print("Prices are multiplicative: a sqft costs more in Bengaluru than in Lucknow – only interactions capture that.")Show solution
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
st = pd.read_csv("students_performance.csv")
pre = ColumnTransformer([("n", StandardScaler(), ["study_hours", "attendance"]),
("o", OneHotEncoder(drop="first", sparse_output=False), ["gender", "school_type", "tuition", "internet"]),
("e", OrdinalEncoder(categories=[["School", "Graduate", "Post-Graduate"]]), ["parent_education"])])
print(pre.fit_transform(st).shape, list(pre.get_feature_names_out()))Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
What is feature engineering? Give examples.
Transforming raw data into inputs that expose the signal: ratios (loan/income), aggregates (orders in last 30 days), date parts (weekday, festive), text counts, binning, log transforms, interactions and proper encoding of categories.
How do you avoid data leakage?
Split first, then fit all transformations on training data only (use Pipelines); remove features not available at prediction time; split time series by time; deduplicate across splits; be suspicious of near-perfect scores.
Summary
- Better features often beat better algorithms.
- Scale for linear/KNN/neural models; not needed for trees.
- One-hot for nominal, ordinal for ordered categories – via ColumnTransformer.
- Create ratios, flags, dates, text and interactions; never leak future information.
Day 45: pipelines, cross-validation and hyper-parameter tuning – build models the professional way.
