Why Feature Scaling Matters 📏Feature Scaling क्यों ज़रूरी है 📏为什么特征缩放很重要 📏
Real data mixes units: age in years (18–90), blood pressure in mmHg (80–209), creatinine in mg/dL (0.2–7) and income in rupees (₹80,000 to ₹44 lakh). Many models compare rows by distance or learn by gradient steps. Both treat a change of 1 the same in every column — so the column with the biggest numbers takes over, whatever it means. Feature scaling puts the columns on comparable scales so each one can count.
असली data में इकाइयाँ मिली होती हैं: उम्र सालों में (18–90), blood pressure mmHg में (80–209), creatinine mg/dL में (0.2–7) और income रुपयों में (₹80,000 से ₹44 लाख)। कई models rows की तुलना दूरी से करते हैं या gradient क़दमों से सीखते हैं। दोनों हर column में 1 के बदलाव को बराबर मानते हैं — इसलिए सबसे बड़ी संख्याओं वाला column हावी हो जाता है, चाहे उसका मतलब कुछ भी हो। Feature scaling columns को तुलनीय scales पर लाता है ताकि हर column गिना जाए।
真实数据混合了各种单位:年龄以岁计(18–90),血压以 mmHg 计(80–209),肌酐以 mg/dL 计(0.2–7),收入以卢比计(₹8 万到 ₹440 万)。很多模型用距离比较样本,或用梯度步来学习。两者都把每一列中“变化 1”视为相同 —— 于是数值最大的那一列就会占据主导,不管它代表什么。特征缩放把各列放到可比的尺度上,让每一列都能发挥作用。
StandardScaler() — and F1 jumps to 0.584. Same data, same model.StandardScaler() — और F1 0.584 हो जाता है। वही data, वही model।StandardScaler(),F1 就升到 0.584。数据相同,模型相同。A new patient and their 5 nearest training patients, found using all five features. Each bar shows how much each feature adds to that distance. Switch the scaler and watch who counts as "near".
एक नया मरीज़ और उसके 5 सबसे नज़दीकी training मरीज़, सभी पाँच features से ढूँढे गए। हर bar दिखाता है कि उस दूरी में हर feature कितना जोड़ता है। Scaler बदलें और देखें कि "नज़दीक" कौन गिना जाता है।
一位新患者及其 5 个最近的训练患者(用全部五个特征计算)。每根条显示各特征在该距离中所占的份额。切换缩放器,看看谁算“近”。
On one shared raw axis, only income is visible — the four medical columns are squashed against zero. After StandardScaler they all sit on the same scale (creatinine keeps its long right tail).
एक साझा raw axis पर सिर्फ़ income दिखती है — चारों medical columns शून्य से चिपके हैं। StandardScaler के बाद सब एक scale पर हैं (creatinine की लंबी दाईं tail बनी रहती है)।
在同一原始坐标轴上只看得见收入 —— 四个医学列都被压在零附近。StandardScaler 之后它们处在同一尺度上(肌酐仍保留长长的右尾)。
# 1) The scale problem in numbers
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
from sklearn.metrics import f1_score
df = pd.read_csv("triage.csv")
feats = ["age", "systolic_bp", "creatinine", "heart_rate", "annual_income"]
X_train, X_test, y_train, y_test = train_test_split(df[feats], df["critical"], test_size=0.25, random_state=0, stratify=df["critical"])
print(X_train.agg(["min", "max", "std"]).T.round(2))
var = X_train.var()
print("\nshare of squared distance (raw):", (var / var.sum() * 100).round(4).to_dict())
for name, model in [("raw", KNeighborsClassifier(15)), ("StandardScaler", make_pipeline(StandardScaler(), KNeighborsClassifier(15)))]:
model.fit(X_train, y_train)
print(f"KNN F1 on critical patients, {name:<14}: {f1_score(y_test, model.predict(X_test)):.3f}")
The Four Core Scalers 🧰चार मुख्य Scalers 🧰四种核心缩放器 🧰
All four are linear: they shift the column and divide by a number. They differ only in which centre and which spread they use — and that decides how they react to extreme values and to sparse data. The worked examples use the real training split.
चारों linear हैं: column को खिसकाते हैं और किसी संख्या से भाग देते हैं। फ़र्क़ बस इसमें है कि कौन सा केंद्र और कौन सा फैलाव लेते हैं — और यही तय करता है कि वे अति मानों और sparse data पर कैसा बर्ताव करते हैं। उदाहरण असली training split से हैं।
四者都是线性的:先平移,再除以一个数。区别只在于用哪个中心、哪个尺度 —— 这决定了它们如何应对极端值和稀疏数据。下面的例子使用真实的训练集。
These scalers move and stretch a column; they do not make a skewed column symmetric or remove outliers. Creatinine is just as right-skewed after StandardScaler (max 9.95 standard deviations). To change the shape, use a transform such as np.log or PowerTransformer — see the Data Transformation tutorial.
ये scalers column को खिसकाते और खींचते हैं; ये टेढ़े column को सममित नहीं बनाते न outliers हटाते हैं। StandardScaler के बाद भी creatinine उतना ही दाईं ओर टेढ़ा है (max 9.95 standard deviations)। आकार बदलने के लिए np.log या PowerTransformer जैसा transform इस्तेमाल करें — Data Transformation tutorial देखें।
这些缩放器只平移、拉伸一列,不会让偏斜的列变对称,也不会去掉离群值。StandardScaler 之后肌酐依然右偏(最大值为 9.95 个标准差)。要改变形状,请用 np.log 或 PowerTransformer 这类变换 —— 见“数据变换”教程。
# 2) The four scalers, fitted on TRAIN, checked with describe()
from sklearn.preprocessing import MinMaxScaler, RobustScaler, MaxAbsScaler
cols = ["age", "creatinine", "annual_income"]
for scaler in [MinMaxScaler(), StandardScaler(), RobustScaler(), MaxAbsScaler()]:
Z = pd.DataFrame(scaler.fit_transform(X_train[cols]), columns=cols)
print(f"{type(scaler).__name__:<14}", " ".join(f"{c}: [{Z[c].min():6.2f}, {Z[c].max():6.2f}] mean {Z[c].mean():5.2f}" for c in ["age", "creatinine"]))
mm = MinMaxScaler().fit(X_train[["age"]])
print(f"\nage 35 → MinMax {(35 - mm.data_min_[0]) / mm.data_range_[0]:.3f} (min {mm.data_min_[0]:.0f}, max {mm.data_max_[0]:.0f})")
rs = RobustScaler().fit(X_train[["creatinine"]])
print(f"creatinine 3.0 → Robust {(3.0 - rs.center_[0]) / rs.scale_[0]:.2f} (median {rs.center_[0]:.2f}, IQR {rs.scale_[0]:.2f})")
mm2 = MinMaxScaler(feature_range=(-1, 1)).fit(X_train[["age"]])
new = pd.DataFrame({"age": [18, 54, 90, 100]})
print("\nfeature_range=(-1, 1):", mm2.transform(new).ravel().round(3), "← age 100 was never seen, so it lands above 1")
print("clip=True :", MinMaxScaler(feature_range=(-1, 1), clip=True).fit(X_train[["age"]]).transform(new).ravel().round(3))
Min-Max Scaling — A Fixed Range 📏Min-Max Scaling — तय Range 📏Min-Max 缩放 —— 固定范围 📏
MinMaxScaler maps the training minimum to 0 and the training maximum to 1 (or any feature_range). It is useful when an algorithm or a format expects bounded inputs — image pixels (divide by 255), some neural-network inputs, or features fed into distance measures that assume [0, 1].
MinMaxScaler training minimum को 0 और training maximum को 1 पर ले जाता है (या किसी भी feature_range पर)। यह तब काम का है जब algorithm या format सीमित inputs चाहता है — image pixels (255 से भाग), कुछ neural-network inputs, या [0, 1] मानने वाले distance measures।
MinMaxScaler 把训练集最小值映射为 0、最大值映射为 1(或任意 feature_range)。当算法或格式要求有界输入时很有用 —— 图像像素(除以 255)、某些神经网络输入,或假定取值在 [0, 1] 的距离度量。
| Problemसमस्या问题 | What happensक्या होता है会发生什么 |
|---|---|
| One extreme valueएक अति मान一个极端值 | Creatinine max 7.12 squeezes everyone else into a narrow band (mean 0.14)Creatinine max 7.12 बाक़ी सबको पतली पट्टी में दबा देता है (mean 0.14)肌酐最大值 7.12 把其他人挤进窄带(均值 0.14) |
| New data outside the rangeRange के बाहर नया data超出范围的新数据 | Age 100 → 1.278 with range (−1, 1)उम्र 100 → range (−1, 1) में 1.278年龄 100 → 在 (−1, 1) 范围下为 1.278 |
| Fixउपाय对策 | Whenकब何时 |
|---|---|
| Log first, or RobustScalerपहले log, या RobustScaler先取对数,或用 RobustScaler | Long tails and real extremesलंबी tails और असली अति मान长尾和真实极端值 |
clip=True | A model that truly needs the boundsजब model को सच में सीमाएँ चाहिए模型确实需要边界时 |
The earlier code fitted a second scaler on data that was already scaled, and its comment said "min should be 0" after asking for range (−1, 1) — where the minimum is −1. Fit each scaler once, on the raw training data. The output above shows the correct result: −1, 0, 1, and 1.278 for an unseen age of 100 (1.0 with clip=True).
पहले के code ने दूसरा scaler पहले से scaled data पर fit किया, और range (−1, 1) माँगने के बाद comment में लिखा था "min should be 0" — जबकि minimum −1 है। हर scaler एक बार, raw training data पर fit करें। ऊपर का output सही नतीजा दिखाता है: −1, 0, 1, और अनदेखी उम्र 100 के लिए 1.278 (clip=True के साथ 1.0)।
原来的代码在已经缩放过的数据上又拟合了一个缩放器,并且在设定范围 (−1, 1) 之后注释写着“最小值应为 0” —— 实际最小值是 −1。每个缩放器只在原始训练数据上拟合一次。上面的输出显示了正确结果:−1、0、1,以及未见过的年龄 100 对应 1.278(clip=True 时为 1.0)。
Standardisation (Z-score) — The Default 📐Standardisation (Z-score) — Default 📐标准化(Z 分数)—— 默认选择 📐
StandardScaler subtracts the training mean and divides by the training standard deviation, so each column has mean 0 and standard deviation 1. A value of +2 means "two standard deviations above the average patient". It is the usual first choice for KNN, SVM, logistic and linear models with regularisation, PCA and neural networks.
StandardScaler training mean घटाता है और training standard deviation से भाग देता है, ताकि हर column का mean 0 और standard deviation 1 हो। +2 का मतलब है "औसत मरीज़ से दो standard deviations ऊपर"। KNN, SVM, regularisation वाले logistic और linear models, PCA और neural networks के लिए यह आम पहली पसंद है।
StandardScaler 减去训练集均值、除以训练集标准差,使每列均值为 0、标准差为 1。+2 表示“比平均患者高两个标准差”。它是 KNN、SVM、带正则化的逻辑/线性模型、PCA 和神经网络的常用首选。
A common myth says z-scores only work for "approximately normal" data. The scaler is just a shift and a stretch — it works on any column. What normality changes is how you read the numbers: only for a bell curve does "±2 covers about 95%" hold. Two details: sklearn uses the population standard deviation (ddof=0), while pandas .std() uses ddof=1; and the outputs have no fixed range.
एक आम भ्रम है कि z-scores सिर्फ़ "लगभग normal" data पर चलते हैं। Scaler बस खिसकाव और खिंचाव है — किसी भी column पर काम करता है। Normality सिर्फ़ यह बदलती है कि आप संख्याओं को कैसे पढ़ते हैं: "±2 में लगभग 95%" सिर्फ़ bell curve पर सही है। दो बातें: sklearn population standard deviation (ddof=0) इस्तेमाल करता है, जबकि pandas .std() ddof=1; और outputs की कोई तय range नहीं होती।
一个常见误解是 Z 分数只适用于“近似正态”的数据。缩放器只是平移加拉伸 —— 对任何列都有效。正态性只影响你如何解读这些数字:只有钟形曲线才满足“±2 覆盖约 95%”。两个细节:sklearn 用总体标准差(ddof=0),而 pandas 的 .std() 用 ddof=1;输出没有固定范围。
In a linear or logistic model, a raw coefficient means "change per 1 rupee" or "per 1 year", so their sizes cannot be compared. After standardising, each coefficient means "change per 1 standard deviation", which makes a rough side-by-side ranking possible (correlated features still blur it).
Linear या logistic model में raw coefficient का मतलब है "हर 1 रुपये पर बदलाव" या "हर 1 साल पर", इसलिए उनके आकार की तुलना नहीं हो सकती। Standardise करने के बाद हर coefficient का मतलब "हर 1 standard deviation पर बदलाव" है, जिससे मोटी तुलना संभव होती है (correlated features फिर भी इसे धुँधला करते हैं)।
在线性或逻辑模型中,原始系数表示“每 1 卢比的变化”或“每 1 岁的变化”,大小无法相互比较。标准化之后,每个系数表示“每 1 个标准差的变化”,可以粗略并排比较(相关特征仍会使其模糊)。
Robust Scaling — Resistant to Extremes 🛡️Robust Scaling — अति मानों से कम प्रभावित 🛡️稳健缩放 —— 抗极端值 🛡️
RobustScaler subtracts the median and divides by the IQR (the 75th minus the 25th percentile). A few huge values hardly move the median or the IQR, so the bulk of the data keeps a sensible spread. Use it when a column has real extreme values you want to keep — incomes, claim amounts, transaction sizes.
RobustScaler median घटाता है और IQR (75वाँ minus 25वाँ percentile) से भाग देता है। कुछ बहुत बड़े मान median या IQR को मुश्किल से हिलाते हैं, इसलिए ज़्यादातर data का फैलाव समझदार रहता है। इसे तब इस्तेमाल करें जब column में असली अति मान हों जिन्हें आप रखना चाहते हैं — income, claim राशि, transaction आकार।
RobustScaler 减去中位数,再除以 IQR(第 75 百分位减第 25 百分位)。少数巨大值几乎不影响中位数和 IQR,所以大部分数据保持合理的分布。当某列含有你想保留的真实极端值时使用 —— 收入、理赔金额、交易额。
RobustScaler protects the scaling of the other rows; it does not shrink the extremes themselves. Creatinine 7.12 still becomes 8.90 after RobustScaler (and 9.95 after StandardScaler). A KNN or SVM will still feel that point strongly. If extremes are errors, fix them; if they are real but too influential, log-transform or clip first.
RobustScaler बाक़ी rows की scaling बचाता है; ख़ुद अति मानों को छोटा नहीं करता। Creatinine 7.12, RobustScaler के बाद भी 8.90 बनता है (और StandardScaler के बाद 9.95)। KNN या SVM उस बिंदु को फिर भी ज़ोर से महसूस करेगा। अगर अति मान ग़लती हैं तो ठीक करें; अगर असली पर बहुत प्रभावशाली हैं तो पहले log-transform या clip करें।
RobustScaler 保护的是其他行的缩放,并不缩小极端值本身。肌酐 7.12 经 RobustScaler 后仍是 8.90(经 StandardScaler 后为 9.95)。KNN 或 SVM 仍会强烈感受到这个点。如果极端值是错误,就修正;如果是真实但影响过大,先取对数或截断。
An illustrative case, not a real company: an insurer clusters claims where most are ₹20,000–₹2 lakh but a few are ₹2 crore. With MinMaxScaler, ordinary claims are squeezed near 0 and the clusters split only on "huge claim or not". RobustScaler keeps the ordinary claims spread out, so clusters can follow the other features too.
एक काल्पनिक उदाहरण, असली कंपनी नहीं: एक insurer claims को cluster करता है जिनमें ज़्यादातर ₹20,000–₹2 लाख हैं पर कुछ ₹2 करोड़। MinMaxScaler के साथ सामान्य claims 0 के पास दब जाते हैं और clusters सिर्फ़ "बड़ा claim या नहीं" पर बँटते हैं। RobustScaler सामान्य claims को फैला रखता है, ताकि clusters दूसरे features भी देख सकें।
示意性例子,并非真实公司:某保险公司对理赔做聚类,大多数金额在 ₹2 万–₹20 万,但少数达 ₹2000 万。用 MinMaxScaler 时,普通理赔被挤在 0 附近,聚类只按“是否巨额”划分。RobustScaler 让普通理赔保持分散,聚类也能考虑其他特征。
Max-Abs Scaling — For Sparse Data 🔢Max-Abs Scaling — Sparse Data के लिए 🔢Max-Abs 缩放 —— 用于稀疏数据 🔢
Text features (TF-IDF, word counts) and one-hot columns are sparse: mostly zeros, stored by keeping only the non-zero values. Subtracting a mean would turn every zero into a non-zero number and blow up memory. MaxAbsScaler only divides by the largest absolute value, so zeros stay zero and the matrix stays sparse. StandardScaler(with_mean=False) does the same with the standard deviation.
Text features (TF-IDF, word counts) और one-hot columns sparse होते हैं: ज़्यादातर शून्य, जिन्हें सिर्फ़ non-zero मान रखकर store किया जाता है। Mean घटाने से हर शून्य non-zero बन जाएगा और memory फट जाएगी। MaxAbsScaler सिर्फ़ सबसे बड़े absolute मान से भाग देता है, इसलिए शून्य शून्य रहते हैं और matrix sparse रहती है। StandardScaler(with_mean=False) यही standard deviation से करता है।
文本特征(TF-IDF、词频)和独热列都是稀疏的:大部分是零,只存储非零值。减去均值会把每个零变成非零数,内存随之爆炸。MaxAbsScaler 只除以最大绝对值,零仍是零,矩阵保持稀疏。StandardScaler(with_mean=False) 则用标准差做同样的事。
# 3) Sparse text features: MaxAbsScaler keeps the zeros, StandardScaler cannot centre sparse data
from sklearn.feature_extraction.text import TfidfVectorizer
docs = ["fast delivery good packing", "late delivery poor packing", "good product fast refund",
"poor product late refund", "good delivery", "late late late"] * 50
X_sparse = TfidfVectorizer().fit_transform(docs)
X_mas = MaxAbsScaler().fit_transform(X_sparse)
print(f"TF-IDF: {X_sparse.shape}, stored values {X_sparse.nnz} → after MaxAbsScaler {X_mas.nnz} (still sparse)")
try:
StandardScaler().fit(X_sparse)
except ValueError as e:
print("StandardScaler():", str(e).split(".")[0])
print("StandardScaler(with_mean=False) works:", StandardScaler(with_mean=False).fit_transform(X_sparse).nnz, "stored values")
Which Algorithms Need Scaling? 🤖किन Algorithms को Scaling चाहिए? 🤖哪些算法需要缩放?🤖
We trained eight models on the same split, once on raw columns and once after StandardScaler. The rule behind the results is simple: models that use distances, gradient descent or a penalty on coefficient size care about scale. Tree-based models split one column at a time on a threshold, so they do not.
हमने एक ही split पर आठ models train किए, एक बार raw columns पर और एक बार StandardScaler के बाद। नतीजों के पीछे नियम सरल है: जो models दूरियाँ, gradient descent या coefficient आकार पर penalty इस्तेमाल करते हैं, वे scale की परवाह करते हैं। Tree-based models एक बार में एक column को threshold पर बाँटते हैं, इसलिए नहीं करते।
我们在同一划分上训练了八个模型,一次用原始列,一次在 StandardScaler 之后。背后的规律很简单:使用距离、梯度下降或系数大小惩罚的模型在乎尺度。树模型每次只按阈值切分一列,所以不在乎。
Grey: F1 on raw columns. Green: F1 after StandardScaler. Click a model to see why it reacts the way it does.
धूसर: raw columns पर F1। हरा: StandardScaler के बाद F1। किसी model पर click करें और देखें कि वह ऐसा क्यों करता है।
灰色:原始列上的 F1。绿色:StandardScaler 之后的 F1。点击模型查看原因。
| AlgorithmAlgorithm算法 | Scale?Scale?缩放? | Whyक्यों原因 | Our F1 raw → scaledहमारा F1 raw → scaled我们的 F1 原始 → 缩放 |
|---|---|---|---|
| KNN, K-Means, DBSCAN | Yesहाँ需要 | Euclidean distanceEuclidean दूरी欧氏距离 | 0.202 → 0.584 |
| SVM (RBF / linear) | Yesहाँ需要 | Distances in the kernel, and the margin penaltyKernel में दूरियाँ, और margin penalty核函数中的距离,以及间隔惩罚 | 0.000 → 0.598 |
| Neural networks | Yesहाँ需要 | Gradient descent; very slow or failed training without itGradient descent; इसके बिना training बहुत धीमी या असफल梯度下降;不缩放训练极慢或失败 | 0.000 → 0.621 |
| PCA, LDA | Yesहाँ需要 | Variance-based: biggest units winVariance-आधारित: सबसे बड़ी इकाई जीतती है基于方差:单位最大者胜出 | see Section 8Section 8 देखें见第 8 节 |
| Logistic / Ridge / Lasso | Usuallyआमतौर पर通常需要 | The penalty treats all coefficients alike; solvers converge fasterPenalty सब coefficients को बराबर मानती है; solvers जल्दी converge होते हैं惩罚对所有系数一视同仁;求解器收敛更快 | 0.598 → 0.598 (217 → 6 iterations)(217 → 6 iterations)(217 → 6 次迭代) |
| Plain LinearRegression (OLS) | Noनहीं不需要 | Predictions identical (max difference 2.6e-13); scale only for reading coefficientsPredictions बिल्कुल वही (max अंतर 2.6e-13); सिर्फ़ coefficients पढ़ने के लिए scale करें预测完全相同(最大差 2.6e-13);只为解读系数才缩放 | — |
| Gaussian Naive Bayes | Not in theoryसिद्धांत में नहीं理论上不需要 | Each feature has its own variance, but var_smoothing borrows from the largest oneहर feature का अपना variance, पर var_smoothing सबसे बड़े से उधार लेता है每个特征有自己的方差,但 var_smoothing 借用了最大的方差 | 0.431 → 0.600 |
| Decision tree, Random forest, Gradient boosting | Noनहीं不需要 | Threshold splits do not depend on unitsThreshold splits इकाइयों पर निर्भर नहीं阈值切分与单位无关 | 0.619 → 0.619 · 0.620 → 0.615 · 0.616 → 0.616 |
Real results from the code below. The random forest moved by 0.005 — just the randomness of which rows land near a threshold.
नीचे के code से असली नतीजे। Random forest 0.005 हिला — बस इस randomness से कि कौन सी rows threshold के पास आती हैं।
来自下方代码的真实结果。随机森林变化了 0.005 —— 只是阈值附近样本落点的随机性。
# 4) Which algorithms care? Same split, raw vs StandardScaler
import warnings
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression, LinearRegression
from sklearn.neural_network import MLPClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier
models = {"KNN": KNeighborsClassifier(15), "SVM (RBF)": SVC(), "Logistic (L2)": LogisticRegression(max_iter=5000),
"Neural net (MLP)": MLPClassifier(hidden_layer_sizes=(32,), max_iter=500, random_state=0),
"Naive Bayes": GaussianNB(), "Decision tree": DecisionTreeClassifier(max_depth=5, random_state=0),
"Random forest": RandomForestClassifier(300, random_state=0), "HistGradBoosting": HistGradientBoostingClassifier(random_state=0)}
rows = []
with warnings.catch_warnings():
warnings.simplefilter("ignore") # the raw MLP / logistic emit ConvergenceWarnings
for name, m in models.items():
raw = f1_score(y_test, m.fit(X_train, y_train).predict(X_test))
sc = f1_score(y_test, make_pipeline(StandardScaler(), m).fit(X_train, y_train).predict(X_test))
rows.append((name, raw, sc))
bench = pd.DataFrame(rows, columns=["model", "F1 raw", "F1 scaled"]).set_index("model")
bench["change"] = bench["F1 scaled"] - bench["F1 raw"]
print(bench.round(3))
lin_raw = LinearRegression().fit(X_train, y_train).predict(X_test)
lin_sc = make_pipeline(StandardScaler(), LinearRegression()).fit(X_train, y_train).predict(X_test)
print("\nplain LinearRegression: max difference in predictions raw vs scaled =", f"{abs(lin_raw - lin_sc).max():.1e}")
nb = GaussianNB().fit(X_train, y_train)
print(f"GaussianNB var_smoothing adds {nb.epsilon_:,.0f} to EVERY variance (1e-9 × the largest one, from income);"
f" creatinine's own variance is {X_train['creatinine'].var():.2f}")
1. Linear regression is not "must scale". Ordinary least squares gives exactly the same predictions either way (we measured a difference of 2.6e-13). Scale it when you add a penalty (Ridge, Lasso), use a gradient solver, or want to compare coefficients. 2. Naive Bayes is not fully scale-free in sklearn. GaussianNB adds 1e-9 × the largest variance to every feature for stability. Here that is 177 — far bigger than creatinine's own variance of 0.36 — so the medical signal was blurred until we scaled.
1. Linear regression "ज़रूर scale करें" नहीं है। Ordinary least squares दोनों तरह बिल्कुल वही predictions देता है (हमने 2.6e-13 का अंतर मापा)। तब scale करें जब penalty जोड़ें (Ridge, Lasso), gradient solver इस्तेमाल करें, या coefficients की तुलना करनी हो। 2. sklearn में Naive Bayes पूरी तरह scale-मुक्त नहीं। GaussianNB स्थिरता के लिए हर feature में सबसे बड़े variance का 1e-9 गुना जोड़ता है। यहाँ यह 177 है — creatinine के अपने variance 0.36 से कहीं बड़ा — इसलिए scaling तक medical संकेत धुँधला रहा।
1. 线性回归并非“必须缩放”。普通最小二乘无论是否缩放,预测完全相同(我们测得差值为 2.6e-13)。只有在加入惩罚(Ridge、Lasso)、使用梯度求解器或需要比较系数时才缩放。2. sklearn 中的朴素贝叶斯并不完全与尺度无关。GaussianNB 为了数值稳定,会给每个特征加上最大方差的 1e-9 倍。这里是 177 —— 远大于肌酐自身的方差 0.36 —— 所以在缩放之前医学信号被抹平了。
Gradient Descent and PCA ⛰️Gradient Descent और PCA ⛰️梯度下降与 PCA ⛰️
Gradient descent: a stretched bowlGradient descent: खिंचा हुआ कटोरा梯度下降:被拉长的碗
For a model trained by gradient descent, the loss surface is a bowl. If one feature is far wider than another, the bowl becomes a long, thin valley: steep in one direction, almost flat in the other. A learning rate small enough not to blow up in the steep direction crawls along the flat one. The real run below shows this — and it does not mean convergence is impossible, only slow or fragile.
Gradient descent से train होने वाले model के लिए loss surface एक कटोरा है। अगर एक feature दूसरे से बहुत चौड़ा है, तो कटोरा लंबी, पतली घाटी बन जाता है: एक दिशा में खड़ा, दूसरी में लगभग सपाट। खड़ी दिशा में न फटने लायक छोटी learning rate सपाट दिशा में रेंगती है। नीचे का असली run यही दिखाता है — और इसका मतलब यह नहीं कि convergence असंभव है, बस धीमा या नाज़ुक है।
对于用梯度下降训练的模型,损失曲面是一个碗。如果某个特征比另一个宽得多,碗就变成又长又窄的山谷:一个方向陡峭,另一个方向几乎平坦。小到不会在陡峭方向发散的学习率,在平坦方向上就会爬得很慢。下面的真实运行展示了这一点 —— 这并不意味着无法收敛,只是慢或脆弱。
Contours of the loss for two features. Pick raw or standardised features, set the learning rate, and run. Try the largest rate on both.
दो features के loss की contours। Raw या standardised features चुनें, learning rate तय करें, और चलाएँ। दोनों पर सबसे बड़ी rate आज़माएँ।
两个特征的损失等高线。选择原始或标准化特征,设置学习率并运行。在两者上都试试最大的学习率。
# 5) Gradient descent: same model, same learning rate — raw vs scaled
from sklearn.linear_model import SGDClassifier
for name, X in [("raw", X_train), ("scaled", StandardScaler().fit_transform(X_train))]:
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
lr = LogisticRegression(max_iter=10000).fit(X, y_train)
print(f"LogisticRegression (lbfgs) {name:<6}: {lr.n_iter_[0]:>5} iterations")
for name, (Xa, Xb) in [("raw", (X_train, X_test)), ("scaled", tuple(make_pipeline(StandardScaler()).fit(X_train).transform(v) for v in (X_train, X_test)))]:
sgd = SGDClassifier(loss="log_loss", random_state=0, max_iter=50, tol=None)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
sgd.fit(Xa, y_train)
print(f"SGDClassifier 50 epochs {name:<6}: F1 {f1_score(y_test, sgd.predict(Xb)):.3f}")
LogisticRegression still reached the same answer on raw data — its lbfgs solver just needed 217 iterations instead of 6. Plain SGDClassifier, with a fixed budget of 50 epochs, never got anywhere on raw data (F1 0.000) and reached 0.580 when scaled. The MLP in Section 7 failed the same way.
LogisticRegression raw data पर भी उसी जवाब तक पहुँचा — उसके lbfgs solver को बस 6 की जगह 217 iterations लगे। सीधा SGDClassifier, 50 epochs के तय budget के साथ, raw data पर कहीं नहीं पहुँचा (F1 0.000) और scaled पर 0.580 पहुँचा। Section 7 का MLP भी इसी तरह असफल हुआ।
LogisticRegression 在原始数据上也得到了同样的答案 —— 只是 lbfgs 求解器需要 217 次迭代而不是 6 次。普通的 SGDClassifier 在固定 50 轮的预算下,原始数据上毫无进展(F1 0.000),缩放后达到 0.580。第 7 节中的 MLP 也是这样失败的。
PCA: finding the units instead of the patternPCA: pattern की जगह इकाइयाँ ढूँढनाPCA:找到的是单位而不是规律
PCA looks for the direction of largest variance. On raw columns, variance is measured in each column's own units — and rupees squared are enormous. So PC1 is simply "income" and explains 100.0% of the variance. After scaling, PC1 explains 29.4% and is driven by age and blood pressure, which really do move together in this data.
PCA सबसे बड़े variance की दिशा ढूँढता है। Raw columns पर variance हर column की अपनी इकाई में मापा जाता है — और रुपये का square बहुत बड़ा है। इसलिए PC1 बस "income" है और variance का 100.0% समझाता है। Scaling के बाद PC1 29.4% समझाता है और उम्र व blood pressure से चलता है, जो इस data में सच में साथ बदलते हैं।
PCA 寻找方差最大的方向。在原始列上,方差以各列自身单位计算 —— 卢比的平方极其巨大。所以 PC1 就是“收入”,解释了 100.0% 的方差。缩放之后,PC1 解释 29.4%,由年龄和血压驱动,它们在这份数据中确实一同变化。
Bars show how much each feature contributes to the first principal component (absolute loading).
Bars दिखाते हैं कि पहले principal component में हर feature कितना योगदान देता है (absolute loading)।
柱子表示每个特征对第一主成分的贡献(载荷绝对值)。
# 6) PCA: without scaling, the first component is just "income"
from sklearn.decomposition import PCA
for name, X in [("raw", X_train), ("scaled", StandardScaler().fit_transform(X_train))]:
p = PCA().fit(X)
load = pd.Series(np.abs(p.components_[0]), index=feats).round(3)
print(f"{name:<6} PC1 explains {p.explained_variance_ratio_[0]:.1%} | biggest loadings:", load.nlargest(2).to_dict())
Leakage: Fit the Scaler on Training Data Only 🔒Leakage: Scaler सिर्फ़ Training Data पर Fit करें 🔒泄漏:只在训练数据上拟合缩放器 🔒
A scaler learns numbers — a mean, a standard deviation, a min and max. If it learns them from all rows before the split, the test rows have helped shape the training features. The rule is always the same: split first, fit on the training rows, transform the test rows.
Scaler संख्याएँ सीखता है — mean, standard deviation, min और max। अगर वह split से पहले सभी rows से सीखे, तो test rows ने training features को आकार देने में मदद की। नियम हमेशा एक है: पहले split, training rows पर fit, test rows को transform।
缩放器会学习一些数字 —— 均值、标准差、最小值和最大值。如果在划分之前从全部行学习,测试行就参与塑造了训练特征。规则始终如一:先划分,在训练行上 fit,再对测试行 transform。
How much does this particular mistake cost? We measured it: 200 random splits at each dataset size, KNN accuracy with a leaky scaler minus accuracy with an honest one.
यह ख़ास ग़लती कितनी महँगी है? हमने मापा: हर dataset आकार पर 200 random splits, leaky scaler वाली KNN accuracy minus ईमानदार scaler वाली।
这个具体错误代价多大?我们做了测量:每种数据规模下 200 次随机划分,用泄漏缩放器的 KNN 准确率减去诚实缩放器的准确率。
Histogram of the 200 gaps. Slide the dataset size.
200 अंतरों का histogram। Dataset का आकार बदलें।
200 个差值的直方图。拖动数据规模。
# 7) Leakage: fit the scaler on ALL rows vs on TRAIN rows only — how big is the effect?
def gap(n_rows, reps=200):
out = []
for r in range(reps):
d = df.sample(n_rows, random_state=r)
Xa, Xb, ya, yb = train_test_split(d[feats], d["critical"], test_size=0.3, random_state=r)
leaky = StandardScaler().fit(pd.concat([Xa, Xb]))
honest = StandardScaler().fit(Xa)
knn = KNeighborsClassifier(5)
a1 = knn.fit(leaky.transform(Xa), ya).score(leaky.transform(Xb), yb)
a2 = knn.fit(honest.transform(Xa), ya).score(honest.transform(Xb), yb)
out.append(a1 - a2)
return np.array(out)
for n_rows in [60, 300, 3000]:
g = gap(n_rows)
print(f"{n_rows:>5} rows: leaky − honest accuracy, mean {g.mean():+.4f}, middle 90% [{np.percentile(g, 5):+.3f}, {np.percentile(g, 95):+.3f}]")
For a plain scaler the leak is small: the average gap is about zero, and at 3,000 rows the scores almost never differ by more than half a point. With 60 rows the scores swing widely in both directions, so the leaky number is simply less trustworthy. Keep the habit anyway — the same "fit on everything" mistake with target encoding, feature selection or SMOTE can inflate scores a lot, and a Pipeline makes it impossible.
सामान्य scaler के लिए leak छोटा है: औसत अंतर लगभग शून्य है, और 3,000 rows पर scores शायद ही कभी आधे point से ज़्यादा अलग होते हैं। 60 rows पर scores दोनों दिशाओं में बहुत झूलते हैं, इसलिए leaky संख्या बस कम भरोसेमंद है। फिर भी आदत रखें — target encoding, feature selection या SMOTE में यही "सब पर fit" ग़लती scores को बहुत फुला सकती है, और Pipeline इसे असंभव बना देती है।
对普通缩放器来说泄漏很小:平均差值约为零,3,000 行时得分几乎不会相差超过半个百分点。只有 60 行时得分向两个方向大幅波动,因此泄漏得到的数字只是更不可信。但仍要保持这个习惯 —— 同样“在全部数据上拟合”的错误放在目标编码、特征选择或 SMOTE 上,可能大幅虚高得分,而 Pipeline 能彻底杜绝它。
Choosing a Scaler and Building the Pipeline 🧭Scaler चुनना और Pipeline बनाना 🧭选择缩放器并搭建管道 🧭
A starting point, not a law. When two choices seem fine, compare them with cross-validation.
शुरुआत की जगह, क़ानून नहीं। जब दो विकल्प ठीक लगें, cross-validation से तुलना करें।
这是起点而非定律。两种选择都可行时,用交叉验证比较。
Both are common. Standardised inputs (mean 0, std 1) suit the usual weight initialisations well and are the default in most tabular work; [0, 1] is natural for pixels. What matters most is that every input is on a similar small scale.
दोनों आम हैं। Standardised inputs (mean 0, std 1) आम weight initialisations के साथ अच्छे बैठते हैं और ज़्यादातर tabular काम में default हैं; pixels के लिए [0, 1] स्वाभाविक है। सबसे ज़रूरी यह है कि हर input मिलते-जुलते छोटे scale पर हो।
两者都很常见。标准化输入(均值 0、标准差 1)与常见的权重初始化配合良好,是大多数表格数据任务的默认;像素用 [0, 1] 很自然。最重要的是每个输入都处在相近的小尺度上。
In a real project, different columns need different treatment. ColumnTransformer sends each group of columns to its own scaler, Pipeline joins it to the model, and cross_val_score refits the scalers inside every fold. Here creatinine is log-transformed first (its skew), income gets RobustScaler, and the model is an SVM — a model that actually needs scaling.
असली project में अलग columns को अलग इलाज चाहिए। ColumnTransformer हर column समूह को उसके अपने scaler पर भेजता है, Pipeline उसे model से जोड़ती है, और cross_val_score हर fold के अंदर scalers को फिर से fit करता है। यहाँ creatinine पहले log-transform होता है (उसका skew), income को RobustScaler मिलता है, और model SVM है — ऐसा model जिसे सच में scaling चाहिए।
在真实项目中,不同列需要不同处理。ColumnTransformer 把每组列交给各自的缩放器,Pipeline 把它与模型串联,cross_val_score 在每一折内重新拟合缩放器。这里肌酐先取对数(因为偏斜),收入用 RobustScaler,模型是 SVM —— 一个真正需要缩放的模型。
# 8) A scaling pipeline: different scalers for different columns
import joblib
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.model_selection import cross_val_score
X_train2 = X_train.assign(log_creatinine=np.log(X_train["creatinine"]))
X_test2 = X_test.assign(log_creatinine=np.log(X_test["creatinine"]))
pre = ColumnTransformer([
("standard", Pipeline([("imp", SimpleImputer(strategy="median")), ("sc", StandardScaler())]), ["age", "systolic_bp", "heart_rate", "log_creatinine"]),
("robust", Pipeline([("imp", SimpleImputer(strategy="median")), ("sc", RobustScaler())]), ["annual_income"]),
])
pipe = Pipeline([("prep", pre), ("model", SVC())])
cv = cross_val_score(pipe, X_train2, y_train, cv=5, scoring="f1")
pipe.fit(X_train2, y_train)
print(f"SVM pipeline: 5-fold CV F1 {cv.mean():.3f} ± {cv.std():.3f} | test F1 {f1_score(y_test, pipe.predict(X_test2)):.3f}")
joblib.dump(pipe, "triage_pipeline.joblib")
sc = pipe["prep"].named_transformers_["standard"]["sc"]
print("learned on train only → means:", dict(zip(["age", "systolic_bp", "heart_rate", "log_creatinine"], sc.mean_.round(2).tolist())))
The original pipeline scaled features and then fed them to gradient boosting — which ignores scale (Section 7: 0.616 → 0.616). It does no harm, but it teaches the wrong lesson. Put scalers in front of models that need them.
मूल pipeline features scale करके gradient boosting को देती थी — जो scale की परवाह नहीं करता (Section 7: 0.616 → 0.616)। नुक़सान नहीं, पर ग़लत सबक़ सिखाता है। Scalers उन models के आगे लगाएँ जिन्हें उनकी ज़रूरत है।
原来的管道先缩放特征再交给梯度提升 —— 而它不在乎尺度(第 7 节:0.616 → 0.616)。这样做无害,但传达了错误的经验。请把缩放器放在真正需要它的模型前面。
Make the Practice Data 🐍Practice Data बनाएँ 🐍生成练习数据 🐍
Run this first. Creatinine (right-skewed) carries most of the signal, with age and blood pressure helping; heart rate and income have no effect. Income is in rupees, so its numbers are about 20,000 times larger than any medical column.
इसे पहले चलाएँ। ज़्यादातर संकेत creatinine (दाईं ओर टेढ़ा) में है, उम्र और blood pressure मदद करते हैं; heart rate और income का कोई असर नहीं। Income रुपयों में है, इसलिए उसकी संख्याएँ किसी भी medical column से लगभग 20,000 गुना बड़ी हैं।
请先运行它。大部分信号来自肌酐(右偏),年龄和血压起辅助作用;心率和收入没有影响。收入以卢比计,数值比任何医学列大约大 20,000 倍。
# 0) Practice data: kidney-risk triage. The signal is in creatinine; income is huge in scale but irrelevant.
import numpy as np, pandas as pd
rng = np.random.default_rng(3)
n = 3000
age = rng.integers(18, 91, n)
systolic_bp = np.clip(np.round(rng.normal(125 + 0.35 * (age - 50), 18, n)), 80, 220)
creatinine = np.round(np.exp(rng.normal(0.05 + 0.006 * (age - 50), 0.45, n)), 2) # mg/dL, long right tail
heart_rate = np.clip(np.round(rng.normal(78, 12, n)), 45, 150)
income = np.round(np.exp(rng.normal(13.2, 0.6, n)), -3) # ₹ per year — no effect
z = 2.4 * np.log(creatinine) + 0.025 * (age - 50) + 0.02 * (systolic_bp - 125) - 1.2 + rng.normal(0, 0.4, n)
critical = (rng.random(n) < 1 / (1 + np.exp(-z))).astype(int)
pd.DataFrame({"age": age, "systolic_bp": systolic_bp, "creatinine": creatinine, "heart_rate": heart_rate,
"annual_income": income, "critical": critical}).to_csv("triage.csv", index=False)
print(f"triage.csv: {n:,} rows, critical {critical.mean():.1%}")
Golden Rules of Feature Scaling 🏆Feature Scaling के सुनहरे नियम 🏆特征缩放黄金法则 🏆
StandardScaler. It does not need normal data.StandardScaler से शुरू करें। इसे normal data की ज़रूरत नहीं।StandardScaler 开始。它不要求正态分布。RobustScaler (or a log first) when real extreme values would squash everyone else.RobustScaler (या पहले log) इस्तेमाल करें।RobustScaler(或先取对数)。MinMaxScaler when you truly need a bounded range; add clip=True if unseen values must stay inside it.MinMaxScaler; अगर अनदेखे मान अंदर ही रहने चाहिए तो clip=True जोड़ें।MinMaxScaler;若未见过的值也必须在范围内,加上 clip=True。MaxAbsScaler or StandardScaler(with_mean=False).MaxAbsScaler या StandardScaler(with_mean=False)।MaxAbsScaler 或 StandardScaler(with_mean=False)。Pipeline so cross-validation stays honest.Pipeline के अंदर रखें ताकि cross-validation ईमानदार रहे।Pipeline,让交叉验证保持诚实。Scaling decides which features a distance or a gradient can "see". On our triage data it lifted KNN from 0.202 to 0.584, rescued SVM and the neural net from 0, cut logistic regression from 217 to 6 iterations, and turned PCA from "income" into a real pattern — while trees did not change at all. Pick the scaler from the data (StandardScaler by default, Robust for extremes, MaxAbs for sparse), and keep it inside a pipeline.
Scaling तय करता है कि दूरी या gradient कौन से features "देख" सकते हैं। हमारे triage data पर इसने KNN को 0.202 से 0.584 किया, SVM और neural net को 0 से बचाया, logistic regression को 217 से 6 iterations पर लाया, और PCA को "income" से असली pattern बनाया — जबकि trees बिल्कुल नहीं बदले। Scaler data से चुनें (default StandardScaler, अति मानों के लिए Robust, sparse के लिए MaxAbs), और उसे pipeline के अंदर रखें।
缩放决定了距离或梯度能“看见”哪些特征。在我们的分诊数据上,它把 KNN 从 0.202 提升到 0.584,把 SVM 和神经网络从 0 救了回来,让逻辑回归从 217 次迭代降到 6 次,并让 PCA 从“收入”变成真实规律 —— 而树模型完全不变。根据数据选择缩放器(默认 StandardScaler,极端值用 Robust,稀疏数据用 MaxAbs),并把它放进管道。