⚙️ Setup¶
import numpy as np
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler, MinMaxScaler, RobustScaler
from sklearn.compose import ColumnTransformer
from sklearn.model_selection import train_test_split
rng = np.random.default_rng(242)
print("Ready.")
Ready.
df = pd.DataFrame({"color": ["red","green","blue","red","green"]})
oh = pd.get_dummies(df["color"], prefix="color").astype(int)
print(oh)
print(f"\n{df['color'].nunique()} categories -> {oh.shape[1]} one-hot columns")
color_blue color_green color_red 0 0 0 1 1 0 1 0 2 1 0 0 3 0 0 1 4 0 1 0 3 categories -> 3 one-hot columns
Answer: Three categories become three 0/1 columns (color_blue, color_green, color_red). One-hot is correct because color is nominal, it has no order. Integer labels (red=0, green=1, blue=2) would tell a linear or distance model that blue is "twice" green and farther from red, a fake ordering. For an unregularized linear model you would also drop one column (drop_first) to avoid the dummy-variable trap.
df = pd.DataFrame({"rating": ["Good","Poor","Excellent","Fair","Good"]})
order = ["Poor","Fair","Good","Excellent"]
oe = OrdinalEncoder(categories=[order])
df["code"] = oe.fit_transform(df[["rating"]]).astype(int)
print(df)
rating code 0 Good 2 1 Poor 0 2 Excellent 3 3 Fair 1 4 Good 2
Answer: Give OrdinalEncoder the explicit order ["Poor","Fair","Good","Excellent"] so it maps Poor=0 ... Excellent=3, preserving the real ranking. Letting an encoder assign codes alphabetically (Excellent=0, Fair=1, Good=2, Poor=3) would scramble the order, telling the model that "Poor" is the highest rating. For genuinely ordinal data the order is information you must not lose.
x = np.array([20, 22, 21, 23, 22, 24, 1000.0]).reshape(-1,1)
for name, sc in [("Standard",StandardScaler()),("MinMax",MinMaxScaler()),("Robust",RobustScaler())]:
out = sc.fit_transform(x).ravel()
print(f"{name:9}: inliers span {out[:-1].max()-out[:-1].min():.3f}, outlier at {out[-1]:.2f}")
Standard : inliers span 0.012, outlier at 2.45 MinMax : inliers span 0.004, outlier at 1.00 Robust : inliers span 2.000, outlier at 489.00
Answer: RobustScaler keeps the six inliers spread out; StandardScaler and especially MinMaxScaler crush them into a tiny range because the 1000 inflates the mean/SD and the max. RobustScaler centers on the median and scales by the IQR, both unmoved by a single extreme value (Chapter 21), so it is the scaler of choice when outliers are present. (Trees, by contrast, need no scaling at all.)
income = pd.Series([18, 22, 25, 30, 35, 40, 55, 70, 90, 120, 200, 350])
width = pd.cut(income, bins=4)
freq = pd.qcut(income, q=4)
print("equal-WIDTH (cut):"); print(width.value_counts().sort_index().to_string())
print("\nequal-FREQUENCY (qcut):"); print(freq.value_counts().sort_index().to_string())
equal-WIDTH (cut): (17.668, 101.0] 9 (101.0, 184.0] 1 (184.0, 267.0] 1 (267.0, 350.0] 1 equal-FREQUENCY (qcut): (17.999, 28.75] 3 (28.75, 47.5] 3 (47.5, 97.5] 3 (97.5, 350.0] 3
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(10, 3.2))
width.value_counts().sort_index().plot(kind="bar", ax=ax[0], color="#7c3aed"); ax[0].set_title("Equal-WIDTH bins: uneven counts")
freq.value_counts().sort_index().plot(kind="bar", ax=ax[1], color="#059669"); ax[1].set_title("Equal-FREQUENCY bins: even counts")
for a in ax: a.set_xticks([]); a.set_ylabel("count")
plt.tight_layout(); plt.show()
Answer: pd.cut(bins=4) makes four equal-width intervals over the range, so the high incomes pile most rows into the lowest bin and leave the top bins nearly empty. pd.qcut(q=4) makes four equal-frequency bins (quartiles), each with about the same count, but with very different widths. Use width when the scale is meaningful, frequency when you want balanced groups; both lose resolution, which is why modern models often skip binning.
df = pd.DataFrame({"age": rng.integers(20,60,120), "score": rng.normal(70,10,120), "plan":rng.choice(["A","B","C"],120)})
tr, te = train_test_split(df, test_size=0.3, random_state=1)
ct = ColumnTransformer([
("num", StandardScaler(), ["age","score"]),
("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), ["plan"]),
])
ct.fit(tr)
print("features:", list(ct.get_feature_names_out()))
print(f"train {ct.transform(tr).shape}, test {ct.transform(te).shape}")
features: ['num__age', 'num__score', 'cat__plan_A', 'cat__plan_B', 'cat__plan_C'] train (84, 5), test (36, 5)
Answer: The ColumnTransformer applies StandardScaler to the numeric columns and OneHotEncoder to the categorical one in a single object, and get_feature_names_out() shows the combined output columns. Fitting on the training split only means the scaler's mean/SD and the encoder's category list come from training data, so no information about the test set leaks into the transform. handle_unknown="ignore" keeps the test transform working if a category was unseen in training. Wrap the whole thing in a Pipeline with a model and the rule holds automatically inside cross-validation.