Why Data Transformation Matters 🔧Data Transformation क्यों ज़रूरी है 🔧为什么需要数据变换 🔧
Raw data comes in units that suit people: age in years, salary in rupees, credit score in points. Many algorithms only see numbers. If salary runs into lakhs and dependents run from 0 to 8, a distance-based model like KNN measures almost everything in rupees and ignores the rest. Data transformation puts features on a fair footing — by scaling them, reshaping skewed ones, and turning categories into numbers.
Raw data लोगों के हिसाब की इकाइयों में आता है: उम्र सालों में, salary रुपयों में, credit score points में। कई algorithms सिर्फ़ संख्याएँ देखते हैं। अगर salary लाखों में है और dependents 0 से 8 तक, तो KNN जैसा दूरी-आधारित model लगभग सब कुछ रुपयों में मापता है और बाक़ी को नज़रअंदाज़ कर देता है। Data transformation features को बराबरी पर लाता है — उन्हें scale करके, skewed features का आकार बदलकर, और categories को संख्याओं में बदलकर।
原始数据使用适合人类的单位:年龄用年、薪资用卢比、信用分用分数。许多算法只看数字。如果薪资是几十万、家属人数是 0 到 8,KNN 这类基于距离的模型几乎完全用卢比来衡量,其他特征被忽略。数据变换让各特征处于公平的起点 —— 通过缩放、重塑偏态特征,以及把类别变成数字。
StandardScaler, the same KNN scores 76.8%. Nothing else changed.StandardScaler के बाद वही KNN 76.8% स्कोर करता है। और कुछ नहीं बदला।StandardScaler 后,同一个 KNN 的准确率为 76.8%。其他什么都没变。# 1) Why scale? KNN on raw features vs scaled features
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
df = pd.read_csv("loans.csv", parse_dates=["registration_date", "application_date"])
num = ["age", "years_employed", "dependents", "salary", "credit_score", "loan_amount"]
X_train, X_test, y_train, y_test = train_test_split(df[num], df["default"], test_size=0.25,
random_state=0, stratify=df["default"])
print(df[num].agg(["min", "max"]).T.astype(int))
sq = (X_train - X_train.mean()) ** 2 # typical squared gap per column
print("\nshare of squared distance from each column (raw):")
print((sq.mean() / sq.mean().sum() * 100).round(3).to_string())
raw = KNeighborsClassifier(n_neighbors=15).fit(X_train, y_train)
scaled = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=15)).fit(X_train, y_train)
print(f"\nKNN accuracy raw: {raw.score(X_test, y_test):.3f} scaled: {scaled.score(X_test, y_test):.3f}"
f" (always guessing 'no default': {1 - y_test.mean():.3f})")
On the raw axis, four of the six features are too small to see. After standardising, each has mean 0 and std 1, and all are visible.
Raw axis पर छह में से चार features दिखने के लिए बहुत छोटे हैं। Standardise करने के बाद हर एक का mean 0 और std 1 है, और सब दिखते हैं।
在原始坐标轴上,六个特征中有四个小到看不见。标准化后,每个特征均值为 0、标准差为 1,全部清晰可见。
300 real applicants. The purple diamond is a new applicant; the 7 nearest neighbours are ringed. Switch the scaling and watch who counts as "near".
300 असली आवेदक। बैंगनी हीरा नया आवेदक है; 7 सबसे नज़दीकी पड़ोसी घेरे में हैं। Scaling बदलें और देखें कि कौन "नज़दीक" गिना जाता है।
300 位真实申请人。紫色菱形是新申请人,7 个最近邻已圈出。切换缩放方式,看看谁算“近”。
Any scaler rescues KNN. The random forest barely moves (78.0–78.4%): trees split on thresholds, so rescaling a column does not change which splits are possible.
कोई भी scaler KNN को बचा लेता है। Random forest मुश्किल से हिलता है (78.0–78.4%): trees thresholds पर बँटते हैं, इसलिए column को rescale करने से संभव splits नहीं बदलते।
任何缩放器都能挽救 KNN。随机森林几乎不变(78.0–78.4%):树按阈值分裂,缩放某一列不会改变可选的分裂点。
Need it: distance-based models (KNN, K-Means, SVM with RBF kernel), PCA, models trained by gradient descent (neural networks, SGDClassifier), and regularised linear models (Ridge, Lasso, logistic regression with a penalty — the penalty treats all coefficients alike, so their units must be comparable). Do not need it: decision trees, random forests and gradient-boosted trees (XGBoost, LightGBM). Scaling them does no harm, but it does not help either.
ज़रूरत है: दूरी-आधारित models (KNN, K-Means, RBF kernel वाला SVM), PCA, gradient descent से train होने वाले models (neural networks, SGDClassifier), और regularised linear models (Ridge, Lasso, penalty वाला logistic regression — penalty सभी coefficients को एक जैसा मानती है, इसलिए उनकी इकाइयाँ तुलनीय होनी चाहिए)। ज़रूरत नहीं: decision trees, random forests और gradient-boosted trees (XGBoost, LightGBM)। इन्हें scale करने से नुक़सान नहीं, पर फ़ायदा भी नहीं।
需要:基于距离的模型(KNN、K-Means、RBF 核 SVM)、PCA、用梯度下降训练的模型(神经网络、SGDClassifier),以及正则化线性模型(Ridge、Lasso、带惩罚项的逻辑回归 —— 惩罚项对所有系数一视同仁,所以它们的单位必须可比)。不需要:决策树、随机森林和梯度提升树(XGBoost、LightGBM)。对它们缩放没有坏处,但也没有好处。
The Main Tools at a Glance 🧰मुख्य Tools एक नज़र में 🧰主要工具一览 🧰
Min-Max Normalisation 📏Min-Max Normalisation 📏Min-Max 归一化 📏
Min-Max maps the smallest training value to 0 and the largest to 1. The shape of the distribution does not change — a skewed column stays exactly as skewed. It is popular for neural-network inputs and image pixels (divide by 255).
Min-Max सबसे छोटे training मान को 0 और सबसे बड़े को 1 पर ले जाता है। Distribution का आकार नहीं बदलता — skewed column ठीक उतना ही skewed रहता है। Neural-network inputs और image pixels (255 से भाग) के लिए यह लोकप्रिय है।
Min-Max 把训练集最小值映射为 0,最大值映射为 1。分布形状不变 —— 偏态列的偏度完全保持不变。它常用于神经网络输入和图像像素(除以 255)。
# 2) Min-Max: fit on TRAIN, then transform both
from sklearn.preprocessing import MinMaxScaler
mm = MinMaxScaler() # feature_range=(0, 1) by default
Xtr_mm = pd.DataFrame(mm.fit_transform(X_train), columns=num, index=X_train.index)
Xte_mm = pd.DataFrame(mm.transform(X_test), columns=num, index=X_test.index)
print("train range:", Xtr_mm.min().min().round(3), "to", Xtr_mm.max().max().round(3))
print("test range:", Xte_mm.min().min().round(3), "to", Xte_mm.max().max().round(3), "← new data CAN fall outside [0, 1]; that is expected")
print("salary skew before", X_train["salary"].skew().round(2), "after", Xtr_mm["salary"].skew().round(2), "(shape unchanged)")
back = mm.inverse_transform(Xtr_mm)[:2, 3]
print("inverse_transform gives back salaries:", back.round(0), "| original:", X_train["salary"].iloc[:2].to_numpy())
Inputs on very different scales make gradient descent slow and unstable: one weight needs tiny steps while another needs big ones. Scaling inputs to a similar range (Min-Max, or standardising) usually makes training faster and smoother. It is strongly recommended, though modern layers such as batch normalisation reduce the problem.
बहुत अलग scales वाले inputs gradient descent को धीमा और अस्थिर बनाते हैं: एक weight को छोटे क़दम चाहिए, दूसरे को बड़े। Inputs को मिलती-जुलती range में scale करना (Min-Max, या standardise) आम तौर पर training को तेज़ और सहज बनाता है। यह ज़ोरदार सलाह है, हालाँकि batch normalisation जैसी आधुनिक layers इस समस्या को घटाती हैं।
尺度差异很大的输入会让梯度下降变慢、变得不稳定:一个权重需要很小的步长,另一个却需要很大的步长。把输入缩放到相近的范围(Min-Max 或标准化)通常能让训练更快、更平稳。强烈推荐这样做,不过批归一化等现代层能减轻这一问题。
Learn min/max (or mean/std) from the training rows only, then apply the same numbers to the test rows. Fitting on all the data lets test information into training. For a simple scaler this leak is usually small, but the same habit with target encoding (Section 7) can make useless features look brilliant. A Pipeline (Section 9) makes the right habit automatic.
Min/max (या mean/std) सिर्फ़ training rows से सीखें, फिर वही संख्याएँ test rows पर लगाएँ। पूरे data पर fit करने से test की जानकारी training में घुस जाती है। सादे scaler में यह leak आम तौर पर छोटा होता है, पर target encoding (Section 7) में यही आदत बेकार features को शानदार दिखा सकती है। Pipeline (Section 9) सही आदत को अपने-आप बना देती है।
只用训练行学习最小值/最大值(或均值/标准差),再把同样的数值用于测试行。在全部数据上拟合会让测试信息进入训练。对简单的缩放器,这种泄漏通常很小;但同样的习惯用在目标编码(第 7 节)上,会让无用的特征看起来非常出色。Pipeline(第 9 节)能自动保证正确的做法。
Standardisation (Z-Score) 📐Standardisation (Z-Score) 📐标准化(z 分数)📐
Standardising subtracts the mean and divides by the standard deviation, so each feature has mean 0 and std 1. It does not cap values and does not require normal data. It is the usual default for linear and logistic regression, PCA and SVM.
Standardising, mean घटाता है और standard deviation से भाग देता है, ताकि हर feature का mean 0 और std 1 हो। यह मानों पर सीमा नहीं लगाता और normal data की ज़रूरत नहीं रखता। Linear और logistic regression, PCA और SVM के लिए यह आम default है।
标准化减去均值再除以标准差,使每个特征均值为 0、标准差为 1。它不限制取值范围,也不要求数据服从正态分布。它是线性回归、逻辑回归、PCA 和 SVM 的常用默认做法。
# 3) Standardisation: mean 0, std 1 — and comparable coefficients
from sklearn.linear_model import LogisticRegression
sc = StandardScaler().fit(X_train)
Z = pd.DataFrame(sc.transform(X_train), columns=num)
print(Z.agg(["mean", "std"]).round(3))
print("note: sklearn divides by the population std (ddof=0), pandas .std() uses ddof=1 → tiny difference")
lr_std = LogisticRegression(max_iter=1000).fit(Z, y_train)
coef = pd.Series(lr_std.coef_[0], index=num).sort_values(key=abs, ascending=False)
print("\nstandardised coefficients (effect of +1 std):"); print(coef.round(3).to_string())
Raw: the salary coefficient is -1.3e-06 per rupee and looks like nothing next to dependents (0.45). Standardised: salary (-0.84 per std) matters more than dependents (0.48). The units, not the importance, made the raw numbers tiny.
Raw: salary का coefficient प्रति रुपया -1.3e-06 है और dependents (0.45) के आगे कुछ नहीं लगता। Standardised: salary (-0.84 प्रति std) dependents (0.48) से ज़्यादा मायने रखती है। Raw संख्याओं को इकाइयों ने छोटा बनाया, महत्व ने नहीं।
原始:薪资系数为每卢比 -1.3e-06,和家属人数(0.45)相比似乎微不足道。标准化后:薪资(每个标准差 -0.84)比家属人数(0.48)更重要。让原始数字变小的是单位,而不是重要性。
Standardised coefficients answer "what happens if this feature moves by one typical step?" That makes them comparable across features. They are still not a full measure of importance: correlated features share credit, and a coefficient shows association, not cause.
Standardised coefficients बताते हैं "अगर यह feature एक आम क़दम चले तो क्या होगा?" इससे वे features के बीच तुलनीय बनते हैं। फिर भी वे महत्व का पूरा माप नहीं हैं: correlated features श्रेय बाँटते हैं, और coefficient संबंध दिखाता है, कारण नहीं।
标准化系数回答的是:“这个特征变化一个典型步长会怎样?”这让它们在特征之间可以比较。但它们仍不是重要性的完整度量:相关的特征会分摊功劳,而且系数显示的是关联,不是因果。
Robust Scaling — When Extremes Are Real 🛡️Robust Scaling — जब अति मान असली हों 🛡️稳健缩放 —— 当极端值真实存在时 🛡️
If a few very large values are genuine (a ₹6 crore salary does exist), you may want to keep them. RobustScaler centres on the median and divides by the IQR, so those few values cannot squash everyone else.
अगर कुछ बहुत बड़े मान असली हैं (₹6 करोड़ की salary होती है), तो आप उन्हें रखना चाह सकते हैं। RobustScaler median पर केंद्रित करता है और IQR से भाग देता है, इसलिए वे कुछ मान बाक़ी सबको दबा नहीं सकते।
如果少数极大值是真实的(确实存在 ₹6 千万的年薪),你可能想保留它们。RobustScaler 以中位数为中心、除以 IQR,所以这几个值无法把其他所有人挤压在一起。
# 4) One extreme value: MinMax vs Standard vs Robust vs MaxAbs
from sklearn.preprocessing import RobustScaler, MaxAbsScaler
s = X_train[["salary"]].copy()
s.iloc[0, 0] = 60_000_000 # one real but extreme salary: ₹6 crore
mid = s["salary"].between(*s["salary"].quantile([0.25, 0.75]))
for name, scaler in [("MinMax", MinMaxScaler()), ("Standard", StandardScaler()), ("Robust", RobustScaler()), ("MaxAbs", MaxAbsScaler())]:
v = scaler.fit_transform(s).ravel()
print(f"{name:<8} middle 50% spans {v[mid].min():7.3f} to {v[mid].max():6.3f} (width {v[mid].max() - v[mid].min():.3f}) extreme → {v.max():7.2f}")
120 salaries. Drag the size of one extra salary and watch the shaded middle 50% in each scaler.
120 salaries। एक अतिरिक्त salary का आकार खींचें और हर scaler में छायांकित बीच के 50% को देखें।
120 个薪资。拖动一个额外薪资的大小,观察每种缩放器中阴影标出的中间 50%。
| ScalerScaler缩放器 | Formulaसूत्र公式 | Output rangeOutput range输出范围 | Affected by extremes?अति मानों से प्रभावित?受极端值影响? | Good forकिसके लिए अच्छा适用于 |
|---|---|---|---|---|
| MinMaxScaler | (x − min) / (max − min) | [0, 1] on traintrain पर在训练集上 | Yes, stronglyहाँ, बहुत是,非常大 | neural networks, bounded inputsneural networks, सीमित inputs神经网络、有界输入 |
| StandardScaler | (x − mean) / std | unboundedअसीमित无界 | Yesहाँ是 | linear models, PCA, SVMlinear models, PCA, SVM线性模型、PCA、SVM |
| RobustScaler | (x − median) / IQR | unboundedअसीमित无界 | Hardlyमुश्किल से几乎不 | real extreme values you keepअसली अति मान जो आप रखते हैं你要保留的真实极端值 |
| MaxAbsScaler | x / max|x| | [−1, 1] | Yes, stronglyहाँ, बहुत是,非常大 | sparse data (keeps zeros)sparse data (शून्य बचाता है)稀疏数据(保留零值) |
Log and Power Transforms — Changing the Shape 📉Log और Power Transforms — आकार बदलना 📉对数与幂变换 —— 改变形状 📉
Scalers change the range; they keep the shape. A long right tail — common in loan amounts, incomes and page views — needs a shape-changing transform. These help linear models, which can be dominated by a few huge values, and they often make relationships more linear.
Scalers range बदलते हैं; आकार बना रहता है। लंबी right tail — जो loan amounts, incomes और page views में आम है — के लिए आकार बदलने वाला transform चाहिए। ये linear models की मदद करते हैं, जिन पर कुछ बहुत बड़े मान हावी हो सकते हैं, और अक्सर संबंधों को ज़्यादा linear बनाते हैं।
缩放器改变的是范围,保留的是形状。贷款金额、收入、页面浏览量中常见的右侧长尾,需要能改变形状的变换。这对可能被少数巨大值主导的线性模型很有帮助,也常能让关系更接近线性。
# 5) Changing the SHAPE: log, sqrt, Yeo-Johnson, Box-Cox, quantile
from sklearn.preprocessing import PowerTransformer, QuantileTransformer
x = X_train[["loan_amount"]]
out = {"original": x["loan_amount"],
"sqrt": np.sqrt(x["loan_amount"]),
"log1p": np.log1p(x["loan_amount"])}
yj = PowerTransformer(method="yeo-johnson").fit(x)
bc = PowerTransformer(method="box-cox").fit(x) # needs x > 0
qt = QuantileTransformer(output_distribution="normal", n_quantiles=1000, random_state=42).fit(x)
out["yeo-johnson"] = pd.Series(yj.transform(x).ravel())
out["box-cox"] = pd.Series(bc.transform(x).ravel())
out["quantile→normal"] = pd.Series(qt.transform(x).ravel())
for k, v in out.items():
print(f"{k:<16} skew {v.skew():6.2f}")
print(f"lambda: Yeo-Johnson {yj.lambdas_[0]:.3f}, Box-Cox {bc.lambdas_[0]:.3f} (0 would be a pure log)")
MinMax gives the same shape as the original. sqrt helps a little. Here log1p over-corrects slightly (skew −0.35), because the best power is λ ≈ 0.15, not 0. Yeo-Johnson finds that λ automatically.
MinMax मूल जैसा ही आकार देता है। sqrt थोड़ी मदद करता है। यहाँ log1p थोड़ा ज़्यादा सुधार कर देता है (skew −0.35), क्योंकि सबसे अच्छी power λ ≈ 0.15 है, 0 नहीं। Yeo-Johnson वह λ अपने-आप ढूँढता है।
MinMax 的形状与原始数据完全相同。sqrt 略有帮助。这里 log1p 稍微矫枉过正(偏度 −0.35),因为最佳幂次是 λ ≈ 0.15 而不是 0。Yeo-Johnson 会自动找到这个 λ。
500 real loan amounts. λ = 1 leaves the shape alone, λ = 0 is a log. Find the λ that makes skewness 0.
500 असली loan amounts। λ = 1 आकार नहीं बदलता, λ = 0 log है। वह λ ढूँढें जो skewness को 0 बनाए।
500 个真实贷款金额。λ = 1 不改变形状,λ = 0 就是对数。找出让偏度为 0 的 λ。
Skewness near 0 means the two sides balance; the data can still have heavy tails or two peaks — check a histogram or Q-Q plot. If you transform the target (for example predicting log(price)), remember to transform predictions back with np.expm1, and note that the back-transformed mean is biased low. Also, QuantileTransformer forces any shape into a normal curve, but it is non-linear and can distort distances between values.
0 के पास skewness का मतलब है दोनों तरफ़ संतुलन; data में फिर भी भारी tails या दो चोटियाँ हो सकती हैं — histogram या Q-Q plot से जाँचें। अगर आप target बदलते हैं (जैसे log(price) का अनुमान), तो predictions को np.expm1 से वापस बदलना याद रखें, और ध्यान दें कि वापस बदला mean नीचे की ओर biased होता है। साथ ही, QuantileTransformer किसी भी आकार को normal curve में ढाल देता है, पर यह non-linear है और मानों के बीच की दूरियाँ बिगाड़ सकता है।
偏度接近 0 只说明两侧平衡;数据仍可能有厚尾或双峰 —— 要用直方图或 Q-Q 图检查。如果你变换的是目标变量(例如预测 log(price)),记得用 np.expm1 把预测值转换回来,并注意转换回来的均值会偏低。另外,QuantileTransformer 能把任何形状强行变成正态曲线,但它是非线性的,可能扭曲数值之间的距离。
Encoding Categories 🏷️Categories को Encode करना 🏷️类别编码 🏷️
Most models need numbers, so text columns must be encoded. The right method depends on whether the categories have a real order, how many there are, and whether you may use the target.
ज़्यादातर models को संख्याएँ चाहिए, इसलिए text columns को encode करना पड़ता है। सही तरीका इस पर निर्भर है कि categories में असली क्रम है या नहीं, कितनी हैं, और क्या आप target इस्तेमाल कर सकते हैं।
大多数模型需要数字,所以文本列必须编码。正确的方法取决于类别是否有真实顺序、类别有多少,以及能否使用目标变量。
# 6) Encoding categories
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, TargetEncoder
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import KFold
tr, te = train_test_split(df, test_size=0.25, random_state=0, stratify=df["default"])
ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=False).fit(tr[["city"]])
print("one-hot columns:", list(ohe.get_feature_names_out()))
oe = OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]]).fit(tr[["income_bracket"]])
print("ordinal:", dict(zip(oe.categories_[0], range(4))))
# branch_id has 400 values and NO real link to default. Naive target encoding still "finds" one:
means = tr.groupby("branch_id")["default"].mean()
naive_tr, naive_te = tr["branch_id"].map(means), te["branch_id"].map(means).fillna(tr["default"].mean())
print(f"\nnaive target encoding AUC train {roc_auc_score(tr['default'], naive_tr):.3f} test {roc_auc_score(te['default'], naive_te):.3f}")
tenc = TargetEncoder(cv=KFold(5, shuffle=True, random_state=0)) # cross-fitting inside fit_transform
cf_tr = tenc.fit_transform(tr[["branch_id"]].astype(str), tr["default"]).ravel()
cf_te = tenc.transform(te[["branch_id"]].astype(str)).ravel()
print(f"TargetEncoder (cv) AUC train {roc_auc_score(tr['default'], cf_tr):.3f} test {roc_auc_score(te['default'], cf_te):.3f}")
freq = tr["branch_id"].map(tr["branch_id"].value_counts(normalize=True))
print("frequency encoding example:", freq.head(3).round(4).tolist())
Six rows. Pick an encoding and watch the new columns appear.
छह rows। एक encoding चुनें और नए columns को आते देखें।
六行数据。选择一种编码,看新列逐一出现。
| Methodतरीका方法 | Use forकिसके लिए适用于 | New columnsनए columns新列数 | Main riskमुख्य जोखिम主要风险 |
|---|---|---|---|
| One-hotOne-hot独热 | unordered, few values (city, colour)बिना क्रम, कम मान (city, रंग)无序、取值少(城市、颜色) | one per categoryहर category का एक每个类别一列 | Very wide with many categoriesकई categories में बहुत चौड़ा类别多时非常宽 |
| OrdinalOrdinal有序 | real order (Low < Medium < High)असली क्रम (Low < Medium < High)真实顺序(Low < Medium < High) | 1 | Low — if you give the order yourselfकम — अगर क्रम आप ख़ुद दें低 —— 前提是由你指定顺序 |
| Label (LabelEncoder)Label (LabelEncoder)标签(LabelEncoder) | the target y onlyसिर्फ़ target y只用于目标 y | 1 | Alphabetical false order on featuresFeatures पर वर्णमाला का नक़ली क्रम在特征上产生按字母的虚假顺序 |
| TargetTarget目标 | many values (branch, postcode)कई मान (branch, postcode)取值很多(分行、邮编) | 1 | Leakage unless cross-fittedCross-fit न करें तो leakage不交叉拟合就会泄漏 |
| FrequencyFrequency频率 | many values, no target allowedकई मान, target की इजाज़त नहीं取值很多、不能用目标 | 1 | Different categories can collideअलग categories टकरा सकती हैं不同类别可能得到相同编码 |
2,000 rows with a purely random target. Increase the number of categories and compare naive and cross-fitted target encoding.
पूरी तरह random target वाली 2,000 rows। Categories की संख्या बढ़ाएँ और naive व cross-fitted target encoding की तुलना करें।
2,000 行数据,目标完全随机。增加类别数,比较朴素目标编码和交叉拟合目标编码。
(1) pd.get_dummies on train and test separately can give different columns (a city missing from test). OneHotEncoder(handle_unknown="ignore") fitted on train avoids this. (2) For plain linear regression without regularisation, all one-hot columns plus an intercept are perfectly collinear — use drop="first" there. Regularised models and trees do not need it. (3) ">15 categories → avoid one-hot" is only a rule of thumb; trees and linear models with regularisation handle a few dozen columns well.
(1) Train और test पर अलग-अलग pd.get_dummies अलग columns दे सकता है (test में कोई city ग़ायब)। Train पर fit किया OneHotEncoder(handle_unknown="ignore") इससे बचाता है। (2) बिना regularisation वाले सादे linear regression में सभी one-hot columns और intercept पूरी तरह collinear होते हैं — वहाँ drop="first" इस्तेमाल करें। Regularised models और trees को इसकी ज़रूरत नहीं। (3) ">15 categories → one-hot से बचें" बस एक rule of thumb है; trees और regularisation वाले linear models कुछ दर्जन columns अच्छे से संभालते हैं।
(1) 在训练集和测试集上分别使用 pd.get_dummies,可能得到不同的列(测试集缺少某个城市)。在训练集上拟合的 OneHotEncoder(handle_unknown="ignore") 能避免这个问题。(2) 对于没有正则化的普通线性回归,全部独热列加上截距项会完全共线 —— 这时用 drop="first"。正则化模型和树模型不需要。(3) “超过 15 个类别就别用独热”只是经验法则;树模型和带正则化的线性模型能很好地处理几十列。
Feature Engineering — Creating New Signals 💡Feature Engineering — नए संकेत बनाना 💡特征工程 —— 创造新信号 💡
Sometimes the useful signal is a combination of columns. In lending, what matters is not the loan or the salary alone but the loan-to-salary ratio. A linear model cannot build that ratio by itself, so we give it one.
कभी-कभी काम का संकेत columns का मेल होता है। Lending में अकेला loan या अकेली salary नहीं, बल्कि loan-to-salary ratio मायने रखता है। Linear model यह ratio ख़ुद नहीं बना सकता, इसलिए हम उसे देते हैं।
有时有用的信号是多个列的组合。在放贷中,重要的不是贷款额或薪资本身,而是贷款与薪资之比。线性模型自己构造不出这个比值,所以我们替它构造。
# 7) Feature engineering: a ratio can matter more than its parts
from sklearn.model_selection import cross_val_score
d = df.copy()
d["loan_to_salary"] = d["loan_amount"] / d["salary"]
d["log_loan_to_salary"] = np.log(d["loan_to_salary"])
snapshot = pd.Timestamp("2024-12-31") # fixed date, NOT today(): reproducible
d["days_since_reg"] = (snapshot - d["registration_date"]).dt.days
d["days_reg_to_app"] = (d["application_date"] - d["registration_date"]).dt.days
d["app_month"] = d["application_date"].dt.month
d["age_group"] = pd.cut(d["age"], bins=[20, 30, 40, 50, 66], labels=["21-30", "31-40", "41-50", "51-65"])
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
for cols in [num, num + ["log_loan_to_salary"]]:
auc = cross_val_score(model, d[cols], d["default"], cv=5, scoring="roc_auc").mean()
print(f"{len(cols)} features {'+ log(loan/salary)' if len(cols) > len(num) else '(raw only)'}".ljust(30) + f"CV AUC {auc:.3f}")
from sklearn.preprocessing import PolynomialFeatures
pf = PolynomialFeatures(degree=2, include_bias=False).fit(d[["age", "dependents"]])
print("polynomial names:", list(pf.get_feature_names_out()))
pd.Timestamp.today() - registration_date gives a different value every day you run it, so results cannot be reproduced, and the model is trained on "age as of today" but used on "age as of prediction day". Use a fixed snapshot date, or the event's own date (here: the application date). Also make sure a feature only uses information available at prediction time.
pd.Timestamp.today() - registration_date हर दिन चलाने पर अलग मान देता है, इसलिए नतीजे दोहराए नहीं जा सकते, और model "आज तक की उम्र" पर train होता है पर "prediction के दिन तक की उम्र" पर इस्तेमाल होता है। एक तय snapshot date, या घटना की अपनी date (यहाँ: application date) इस्तेमाल करें। यह भी पक्का करें कि feature सिर्फ़ वही जानकारी इस्तेमाल करे जो prediction के समय उपलब्ध हो।
pd.Timestamp.today() - registration_date 每天运行都会得到不同的值,结果无法复现;而且模型用“截至今天的时长”训练,却用“截至预测当天的时长”来预测。请使用固定的快照日期,或事件本身的日期(这里是申请日期)。还要确保特征只使用预测时刻能拿到的信息。
One Pipeline for Everything 🔗हर चीज़ के लिए एक Pipeline 🔗一条管道搞定一切 🔗
ColumnTransformer sends each group of columns through its own steps; Pipeline chains those steps with the model. Calling fit learns every median, mean, std and category from the training rows only, and predict applies exactly the same steps to new data.
ColumnTransformer columns के हर समूह को उसके अपने क़दमों से गुज़ारता है; Pipeline उन क़दमों को model से जोड़ता है। fit बुलाने पर हर median, mean, std और category सिर्फ़ training rows से सीखी जाती है, और predict नए data पर ठीक वही क़दम लगाता है।
ColumnTransformer 让每组列各自经过对应的步骤;Pipeline 把这些步骤和模型串起来。调用 fit 时,所有中位数、均值、标准差和类别都只从训练行学习;predict 会对新数据执行完全相同的步骤。
# 8) Everything in one Pipeline: no leakage, one object to save
import joblib
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
d["log_loan_amount"], d["log_salary"] = np.log(d["loan_amount"]), np.log(d["salary"])
num_cols = ["age", "years_employed", "dependents", "credit_score", "log_salary", "log_loan_amount", "log_loan_to_salary"]
pre = ColumnTransformer([
("num", Pipeline([("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler())]), num_cols),
("nom", Pipeline([("impute", SimpleImputer(strategy="most_frequent")),
("ohe", OneHotEncoder(handle_unknown="ignore"))]), ["city"]),
("ord", OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]]), ["income_bracket"]),
])
pipe = Pipeline([("prep", pre), ("model", LogisticRegression(max_iter=1000))])
cols = num_cols + ["city", "income_bracket"]
tr, te = train_test_split(d, test_size=0.25, random_state=0, stratify=d["default"])
pipe.fit(tr[cols], tr["default"])
print(f"test AUC: {roc_auc_score(te['default'], pipe.predict_proba(te[cols])[:, 1]):.3f}")
print("features after ColumnTransformer:", pipe["prep"].get_feature_names_out().size)
joblib.dump(pipe, "loan_pipeline.joblib")
same = (joblib.load("loan_pipeline.joblib").predict_proba(te[cols]) == pipe.predict_proba(te[cols])).all()
print("reloaded pipeline gives identical predictions:", bool(same))
Cross-validation on the whole pipeline (cross_val_score(pipe, X, y)) refits the transformers inside every fold — the honest way to estimate performance.
पूरी pipeline पर cross-validation (cross_val_score(pipe, X, y)) हर fold के अंदर transformers को दोबारा fit करता है — performance आँकने का ईमानदार तरीका।
对整条管道做交叉验证(cross_val_score(pipe, X, y))会在每一折内部重新拟合变换器 —— 这才是诚实的性能评估方式。
joblib.dump(pipe, "loan_pipeline.joblib") stores the fitted scalers, encoders and model together. Load it with the same scikit-learn version; pickled models are not guaranteed to work across versions. Only load files you trust — loading a pickle can run code.
joblib.dump(pipe, "loan_pipeline.joblib") fit किए गए scalers, encoders और model को साथ में रखता है। इसे उसी scikit-learn version के साथ load करें; pickled models के versions के बीच चलने की गारंटी नहीं। सिर्फ़ भरोसेमंद files load करें — pickle load करने से code चल सकता है।
joblib.dump(pipe, "loan_pipeline.joblib") 会把拟合好的缩放器、编码器和模型一起保存。请用相同版本的 scikit-learn 加载;pickle 序列化的模型不保证跨版本可用。只加载你信任的文件 —— 加载 pickle 可能会执行代码。
Make the Practice Data 🐍Practice Data बनाएँ 🐍生成练习数据 🐍
Run this first. Default risk is built from credit score, the loan-to-salary ratio, dependents and years employed (plus a little from city). branch_id has 400 values and no effect at all — ideal for showing leakage.
इसे पहले चलाएँ। Default का जोखिम credit score, loan-to-salary ratio, dependents और years employed (और city से थोड़ा) से बना है। branch_id के 400 मान हैं और कोई असर नहीं — leakage दिखाने के लिए आदर्श।
请先运行它。违约风险由信用分、贷款与薪资之比、家属人数和工作年限(外加少量城市因素)决定。branch_id 有 400 个取值,但完全没有影响 —— 非常适合演示泄漏。
# 0) A practice loan dataset — the default risk is planted, so we can check every step
import numpy as np, pandas as pd
rng = np.random.default_rng(5)
n = 4000
age = rng.integers(21, 66, n)
years_employed = np.clip(np.round((age - 21) * rng.uniform(0.2, 0.9, n)), 0, 40).astype(int)
dependents = rng.poisson(1.4, n).clip(0, 8)
salary = np.round(np.exp(rng.normal(13.4, 0.55, n) + 0.01 * years_employed), -3) # ₹ per year
credit_score = np.clip(np.round(rng.normal(690, 70, n) + 1.2 * years_employed), 300, 900).astype(int)
loan_amount = np.round(salary * rng.uniform(0.2, 2.5, n), -3)
city = rng.choice(["Mumbai", "Delhi", "Bengaluru", "Chennai", "Hyderabad", "Pune", "Kolkata", "Jaipur"], n)
branch_id = rng.integers(1, 401, n) # 400 branches, no real effect
income_bracket = pd.cut(salary, [0, 4e5, 8e5, 15e5, np.inf], labels=["Low", "Medium", "High", "Very High"]).astype(str)
reg = pd.Timestamp("2018-01-01") + pd.to_timedelta(rng.integers(0, 2000, n), unit="D")
app = reg + pd.to_timedelta(rng.integers(30, 900, n), unit="D")
# risk: low credit score, high loan-to-salary ratio, many dependents, few years employed
z = (-0.012 * (credit_score - 690) + 1.6 * np.log(loan_amount / salary) + 0.35 * dependents
- 0.06 * years_employed + 0.25 * (city == "Kolkata") - 1.4 + rng.normal(0, 0.5, n))
default = (rng.random(n) < 1 / (1 + np.exp(-z))).astype(int)
pd.DataFrame({"age": age, "years_employed": years_employed, "dependents": dependents, "salary": salary,
"credit_score": credit_score, "loan_amount": loan_amount, "city": city, "branch_id": branch_id,
"income_bracket": income_bracket, "registration_date": reg, "application_date": app,
"default": default}).to_csv("loans.csv", index=False)
print(f"loans.csv: {n:,} rows, default rate {default.mean():.1%}")
Golden Rules of Data Transformation 🏆Data Transformation के सुनहरे नियम 🏆数据变换黄金法则 🏆
Pipeline do it for you.Pipeline पर छोड़ दें।Pipeline 来做。.skew() and a histogram; for a long tail try log or a power transform first..skew() और histogram जाँचें; लंबी tail हो तो पहले log या power transform आज़माएँ।.skew() 和直方图;有长尾时先试对数或幂变换。TargetEncoder). A naive version can make pure noise look predictive.TargetEncoder) के साथ करें। Naive version शुद्ध शोर को भी काम का दिखा सकता है।TargetEncoder)。朴素做法能让纯噪声看起来很有预测力。today().today() कभी नहीं।today()。describe(), histograms) after every step.describe(), histograms)।describe()、直方图)。Transformation decides what the model can see. On our loan data, the same KNN went from worse-than-guessing (73.1%) to 76.8% with one scaler; one ratio feature lifted a linear model's AUC from 0.800 to 0.820; and a careless target encoding made a useless column look predictive. Choose each transform for a reason, fit it on training data only, and keep it all in one pipeline.
Transformation तय करता है कि model क्या देख सकता है। हमारे loan data पर वही KNN एक scaler से अंदाज़े से भी ख़राब (73.1%) से 76.8% पर पहुँचा; एक ratio feature ने linear model का AUC 0.800 से 0.820 किया; और लापरवाह target encoding ने एक बेकार column को काम का दिखाया। हर transform किसी कारण से चुनें, सिर्फ़ training data पर fit करें, और सब एक pipeline में रखें।
变换决定了模型能看到什么。在我们的贷款数据上,同一个 KNN 只换了一个缩放器,就从不如瞎猜(73.1%)提升到 76.8%;一个比值特征让线性模型的 AUC 从 0.800 升到 0.820;而一次草率的目标编码让一个无用的列看起来很有预测力。每种变换都要有理由,只在训练数据上拟合,并全部放进一条管道。