Data Preparation / Data Preprocessing 📂 Data Collection · 10 of 13 184 min read

Feature Selection in Machine Learning

A story-driven, comprehensive guide to selecting the most relevant features for machine learning models — covering filter methods, wrapper methods, embedded methods, and dimensionality reduction, with live diagrams, density charts, real-world stories, and complete reusable code.

🌐
SectionSection第 01 节

Why Feature Selection Matters 🎯Feature Selection क्यों ज़रूरी है 🎯为什么特征选择很重要 🎯

More columns do not automatically mean a better model. A column that carries no signal still costs something: the model may fit its random ups and downs (overfitting), distance-based models treat it as just as important as real signals, and every extra column adds training time. With many columns and few rows, the data also becomes thin — the curse of dimensionality. Feature selection keeps the columns that help and drops the rest.

ज़्यादा columns का मतलब अपने-आप बेहतर model नहीं। जिस column में कोई संकेत नहीं, उसकी भी क़ीमत है: model उसके random उतार-चढ़ाव को fit कर सकता है (overfitting), दूरी-आधारित models उसे असली संकेतों जितना ही अहम मानते हैं, और हर extra column training समय बढ़ाता है। बहुत columns और कम rows में data पतला हो जाता है — curse of dimensionality। Feature selection काम के columns रखता है और बाक़ी हटाता है।

列越多并不自动意味着模型越好。没有信号的列也有代价:模型可能去拟合它的随机起伏(过拟合),基于距离的模型会把它看得和真实信号一样重要,而且每多一列都会增加训练时间。列多行少时,数据还会变得稀疏 —— 这就是维度灾难。特征选择保留有用的列,去掉其余的。

The Churn Model Drowning in Noiseशोर में डूबा Churn Model淹没在噪声中的流失模型
Our churn table (Section 11) has 36 columns after encoding: 12 that really drive churn, plus a customer ID, a constant country column, a near-constant test flag, a near-copy of data usage and 20 columns of pure random numbers. A KNN model on all 36 columns scores test AUC 0.711. On just the 12 true columns it scores 0.783. The noise columns pull every "nearest neighbour" in random directions. A regularised logistic regression is much calmer (0.790 → 0.798) — so how much selection helps depends on the model.
हमारी churn table (Section 11) में encoding के बाद 36 columns हैं: 12 जो सच में churn चलाते हैं, साथ में customer ID, एक constant country column, लगभग constant test flag, data usage की लगभग copy और 20 columns शुद्ध random संख्याओं के। सभी 36 columns पर KNN model test AUC 0.711 देता है। सिर्फ़ 12 सच्चे columns पर 0.783। शोर वाले columns हर "नज़दीकी पड़ोसी" को random दिशाओं में खींचते हैं। Regularised logistic regression कहीं शांत है (0.790 → 0.798) — यानी selection कितना मदद करता है यह model पर निर्भर है।
我们的流失表(第 11 节)编码后有 36 列:12 列真正影响流失,另有一个客户 ID、一个常数列 country、一个几乎不变的测试标记、一列几乎复制流量用量的列,以及 20 列纯随机数。用全部 36 列的 KNN 模型测试 AUC 为 0.711;只用 12 个真实列则为 0.783。噪声列把每个“最近邻”拉向随机方向。带正则化的逻辑回归则稳得多(0.790 → 0.798)—— 所以特征选择的帮助有多大,取决于模型。
# 1) Same models, all 36 columns vs only the useful ones
import numpy as np, pandas as pd, time
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
df = pd.read_csv("churn.csv")
X = pd.get_dummies(df.drop(columns="churn"), columns=["region", "country"], dtype=int)
y = df["churn"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0, stratify=y)
useful = ["tenure", "monthly_charges", "credit_score", "num_products", "age", "income", "expenses", "data_gb",
          "region_East", "region_North", "region_South", "region_West"]
models = {"Logistic": make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)),
          "KNN (k=25)": make_pipeline(StandardScaler(), KNeighborsClassifier(25)),
          "Random forest": RandomForestClassifier(300, min_samples_leaf=3, random_state=0, n_jobs=-1)}
print(f"{'model':<14}{'columns':>8}{'train AUC':>11}{'test AUC':>10}")
for name, m in models.items():
    for cols in [list(X.columns), useful]:
        m.fit(X_train[cols], y_train)
        tr = roc_auc_score(y_train, m.predict_proba(X_train[cols])[:, 1])
        te = roc_auc_score(y_test, m.predict_proba(X_test[cols])[:, 1])
        print(f"{name:<14}{len(cols):>8}{tr:>11.3f}{te:>10.3f}")
OUTPUT
model columns train AUC test AUC Logistic 36 0.831 0.790 Logistic 12 0.823 0.798 KNN (k=25) 36 0.784 0.711 KNN (k=25) 12 0.810 0.783 Random forest 36 1.000 0.785 Random forest 12 0.999 0.797
💡
What Selection Buys YouSelection से क्या मिलता है特征选择带来什么

(1) Less overfitting, especially for KNN, trees with deep splits and small datasets. (2) Faster training and prediction. (3) A model people can read. (4) Fewer columns to collect, clean and monitor in production. It does not always raise accuracy — a well-regularised model may already ignore most noise.

(1) कम overfitting, ख़ासकर KNN, गहरे splits वाले trees और छोटे datasets में। (2) तेज़ training और prediction। (3) ऐसा model जिसे लोग पढ़ सकें। (4) Production में इकट्ठा, साफ़ और monitor करने के लिए कम columns। यह accuracy हमेशा नहीं बढ़ाता — अच्छा regularised model ज़्यादातर शोर पहले ही नज़रअंदाज़ कर सकता है।

(1) 减少过拟合,尤其对 KNN、深度切分的树和小数据集。(2) 训练和预测更快。(3) 模型更易读懂。(4) 生产中需要收集、清洗和监控的列更少。它并不总是提高准确率 —— 正则化良好的模型可能已经忽略了大部分噪声。


SectionSection第 02 节

The Three Families 🗺️तीन परिवार 🗺️三大类方法 🗺️

🗺️ PICTURE 1 — FILTER, WRAPPER, EMBEDDEDतस्वीर 1 — FILTER, WRAPPER, EMBEDDED图 1 — 过滤法、包装法、嵌入法
Three families of feature selection 36 columns36 columns36 列useful + noiseकाम के + शोर有用 + 噪声 FilterFilter过滤法score each column alone — no modelहर column को अकेले score — कोई model नहीं逐列单独打分 —— 不用模型 VarianceThreshold · corr · F-test · χ² · MIfast · misses pairsतेज़ · जोड़ियाँ छूटती हैं快 · 会漏掉组合 WrapperWrapper包装法train a model on many subsetsकई subsets पर model train在多个子集上训练模型 RFE · RFECV · SequentialFeatureSelectorsees pairs · slow · can overfitजोड़ियाँ दिखती हैं · धीमा · overfit संभव能看到组合 · 慢 · 可能过拟合 EmbeddedEmbedded嵌入法selection happens while trainingtraining के दौरान चयन训练时同时选择 L1 penalty · tree importanceone fit · tied to that modelएक fit · उसी model से बँधा一次拟合 · 绑定该模型
FilterFilter过滤法WrapperWrapper包装法EmbeddedEmbedded嵌入法
Howकैसे方式A statistic per columnहर column का एक statistic每列一个统计量Fit a model on many subsetsकई subsets पर model fit在多个子集上拟合模型The model selects while it trainsModel training के दौरान चुनता है模型在训练中选择
Costलागत成本One pass over the dataData पर एक pass扫描数据一次RFE: about one fit per column (× CV folds). Trying every subset would be 2ⁿ — never done.RFE: हर column पर लगभग एक fit (× CV folds)। हर subset आज़माना 2ⁿ होगा — कभी नहीं किया जाता।RFE:每列约一次拟合(× 交叉验证折数)。尝试所有子集是 2ⁿ —— 从不这样做。One fit (plus tuning)एक fit (और tuning)一次拟合(加调参)
Sees column pairs?Column जोड़ियाँ दिखती हैं?能看到列的组合?Noनहीं不能Yes, as far as the model canहाँ, जितना model देख सके能,取决于模型Linear L1: only additive effects. Trees: yes.Linear L1: सिर्फ़ जोड़ वाले असर। Trees: हाँ।线性 L1:仅加性效应。树:能。
Main riskमुख्य जोखिम主要风险Drops columns that only work togetherसाथ में काम करने वाले columns गिरा देता है丢掉只在组合时才有用的列Slow; can overfit the CV scoreधीमा; CV score पर overfit हो सकता है慢;可能对 CV 分数过拟合Tied to one model; biased importancesएक model से बँधा; पक्षपाती importances绑定单一模型;重要性可能有偏

SectionSection第 03 节

Filter Methods — Cheap First Checks 🔵Filter Methods — सस्ती पहली जाँच 🔵过滤法 —— 低成本的第一步 🔵

Step 1 — Constant and near-constant columnsStep 1 — Constant और लगभग constant columns第 1 步 —— 常数列和近常数列

A column that never changes cannot help any model. VarianceThreshold removes columns whose variance is at or below a threshold. Two cautions: variance depends on units (a threshold of 0.01 means something very different for rupees and for a 0/1 flag), and it must run before scaling — after StandardScaler every column has variance 1.

जो column कभी नहीं बदलता वह किसी model की मदद नहीं कर सकता। VarianceThreshold उन columns को हटाता है जिनका variance threshold के बराबर या कम है। दो सावधानियाँ: variance इकाइयों पर निर्भर है (0.01 threshold रुपयों और 0/1 flag के लिए बहुत अलग है), और इसे scaling से पहले चलाना ज़रूरी है — StandardScaler के बाद हर column का variance 1 होता है।

从不变化的列对任何模型都没有帮助。VarianceThreshold 会删除方差不超过阈值的列。两点注意:方差取决于单位(阈值 0.01 对卢比和 0/1 标记意义完全不同);而且必须在缩放之前运行 —— StandardScaler 之后每列方差都是 1。

Step 2 — Near-duplicate columnsStep 2 — लगभग duplicate columns第 2 步 —— 近似重复的列

Two columns with |r| above about 0.9 carry almost the same information. Keep one. The original code dropped whichever column came later in the table; the version below keeps the one more related to the target. A high correlation is not always redundancy, though: income and expenses correlate at 0.87 here, and both are needed.

|r| लगभग 0.9 से ऊपर वाले दो columns लगभग एक ही जानकारी रखते हैं। एक रखें। मूल code table में बाद में आने वाला column हटा देता था; नीचे का version target से ज़्यादा जुड़ा column रखता है। फिर भी ऊँचा correlation हमेशा दोहराव नहीं: यहाँ income और expenses का correlation 0.87 है, और दोनों चाहिए।

|r| 超过约 0.9 的两列几乎携带相同信息,只留一列。原代码删除的是表中靠后的那一列;下面的版本保留与目标关系更强的那一列。不过高相关并不总是冗余:这里收入和支出的相关系数为 0.87,而两者都需要。

🔥 PICTURE 2 — REAL CORRELATIONS (TRAINING ROWS)तस्वीर 2 — असली CORRELATIONS (TRAINING ROWS)图 2 — 真实相关系数(训练集)
Correlation heatmap of ten columns tenure tenure monthly_charges monthly_charges credit_score credit_score age age income income 0.87 expenses expenses 0.87 data_gb data_gb 0.96 streaming_hours streaming_hours 0.96 customer_id customer_id noise_01 noise_01 red border: |r| > 0.90 — keep one of the pairलाल किनारा: |r| > 0.90 — जोड़ी में से एक रखें红框:|r| > 0.90 —— 两者只留一个 income ↔ expenses 0.87: related, but both neededincome ↔ expenses 0.87: जुड़े हैं, पर दोनों चाहिएincome ↔ expenses 0.87:相关,但两个都需要
# 2) Cheap filters: constant columns, then near-duplicate columns
from sklearn.feature_selection import VarianceThreshold
vt = VarianceThreshold(threshold=0.01).fit(X_train)                  # variance depends on units — check what it removes
print("removed by VarianceThreshold:", X_train.columns[~vt.get_support()].tolist())
X_tr = X_train.loc[:, vt.get_support()]
corr = X_tr.corr().abs()
upper = corr.where(np.triu(np.ones(corr.shape, dtype=bool), k=1))
target_r = X_tr.corrwith(y_train).abs()
to_drop = set()
for a in upper.columns:
    for b in upper.index[upper[a] > 0.90]:                               # pair (b, a) is highly correlated
        weaker = a if target_r[a] < target_r[b] else b                   # keep the one closer to the target
        to_drop.add(weaker)
        print(f"{b} ↔ {a}: r = {corr.loc[b, a]:.3f} | |r| with churn {target_r[b]:.4f} vs {target_r[a]:.4f} → drop {weaker}")
print("columns left:", X_tr.shape[1] - len(to_drop))
OUTPUT
removed by VarianceThreshold: ['is_test_account', 'country_IN'] data_gb ↔ streaming_hours: r = 0.958 | |r| with churn 0.1180 vs 0.1171 → drop streaming_hours columns left: 33

Step 3 — One score per columnStep 3 — हर column का एक score第 3 步 —— 每列一个分数

SelectKBest scores each column against the target and keeps the top k. f_classif (ANOVA F) asks "do the class means differ?". mutual_info_classif asks "does knowing this column reduce uncertainty about the target in any way?" — it is reported in nats (natural log), not bits, and it is an estimate that wobbles with the random seed and the sample.

SelectKBest हर column को target के सामने score करता है और top k रखता है। f_classif (ANOVA F) पूछता है "क्या classes के means अलग हैं?"। mutual_info_classif पूछता है "क्या यह column जानने से target की अनिश्चितता किसी भी तरह घटती है?" — इसे nats (natural log) में बताया जाता है, bits में नहीं, और यह एक अनुमान है जो random seed और sample के साथ हिलता है।

SelectKBest 将每列与目标比较打分,保留前 k 列。f_classif(ANOVA F)问的是“各类别的均值是否不同?”。mutual_info_classif 问的是“知道这一列能否以任何方式减少对目标的不确定性?”—— 单位是 nats(自然对数),不是 bits,而且它是一个估计值,会随随机种子和样本波动。

# 3) Univariate scores: ANOVA F-test and mutual information (both look at ONE column at a time)
from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif
num = [c for c in X_tr.columns if c not in to_drop]
F, p = f_classif(X_train[num], y_train)
mi = mutual_info_classif(X_train[num], y_train, discrete_features=[X_train[c].nunique() <= 5 for c in num], random_state=0)
scores = pd.DataFrame({"F": F, "p_value": p, "MI (nats)": mi}, index=num).sort_values("F", ascending=False)
scores["F rank"] = range(1, len(num) + 1)
scores["MI rank"] = scores["MI (nats)"].rank(ascending=False, method="first").astype(int)
print(scores.round(4).head(12).to_string())
print("...\nwhere are the tricky ones?\n", scores.loc[["age", "income", "customer_id"]].round(4).to_string())
kbest = SelectKBest(f_classif, k=8).fit(X_train[num], y_train)
print("\nSelectKBest(f_classif, k=8) keeps:", kbest.get_feature_names_out().tolist())
OUTPUT
F p_value MI (nats) F rank MI rank tenure 116.2911 0.0000 0.0496 1 1 credit_score 104.1549 0.0000 0.0382 2 2 monthly_charges 55.2263 0.0000 0.0072 3 10 num_products 28.1521 0.0000 0.0098 4 7 region_East 25.2938 0.0000 0.0085 5 8 region_South 24.8761 0.0000 0.0081 6 9 data_gb 21.1565 0.0000 0.0000 7 18 expenses 10.1835 0.0014 0.0000 8 19 region_North 8.5665 0.0035 0.0029 9 15 region_West 7.3562 0.0068 0.0024 10 16 noise_16 5.9204 0.0151 0.0000 11 20 noise_17 3.2300 0.0725 0.0000 12 21 ... where are the tricky ones? F p_value MI (nats) F rank MI rank age 0.4941 0.4822 0.0154 26 4 income 1.1830 0.2769 0.0182 20 3 customer_id 1.3360 0.2479 0.0000 17 24 SelectKBest(f_classif, k=8) keeps: ['tenure', 'monthly_charges', 'credit_score', 'num_products', 'expenses', 'data_gb', 'region_East', 'region_South']
🔍
Read the Ranks CarefullyRanks ध्यान से पढ़ें仔细看排名

Age is rank 26 by F but rank 4 by mutual information: its effect is U-shaped, so the class means are equal. Income is rank 20 by F: alone it is useless, but it matters next to expenses. The F-test top 8 dropped both. Mutual information also has misses — it ranked monthly_charges only 10th and gave expenses 0.0 — because it is estimated from 1,500 rows. Use filters to throw out obvious junk, not to make the final choice.

Age F से rank 26 पर है पर mutual information से rank 4 पर: इसका असर U-आकार का है, इसलिए classes के means बराबर हैं। Income F से rank 20 पर है: अकेले बेकार, पर expenses के साथ अहम। F-test top 8 ने दोनों को गिरा दिया। Mutual information भी चूकता है — उसने monthly_charges को सिर्फ़ 10वाँ रखा और expenses को 0.0 दिया — क्योंकि यह 1,500 rows से अनुमान है। Filters से साफ़ कचरा फेंकें, अंतिम चुनाव न करें।

年龄的 F 排名第 26,互信息排名却是第 4:它的效应是 U 形的,所以两类均值相等。收入的 F 排名第 20:单独无用,但与支出一起时很重要。F 检验前 8 名把两者都丢掉了。互信息也会漏判 —— 它把 monthly_charges 只排在第 10,给支出 0.0 —— 因为它是由 1,500 行估计出来的。用过滤法去掉明显的垃圾列,而不是做最终选择。


SectionSection第 04 节

Chi-Square vs Correlation 🧮Chi-Square बनाम Correlation 🧮卡方检验 vs 相关系数 🧮

The right test depends on the types of the two variables. Pearson correlation measures a straight-line relationship between two numbers. Chi-square tests whether two categorical variables are independent. The classic mistake is to turn categories into codes (North = 1, South = 2, …) and run Pearson — the answer then depends on how you numbered them.

सही test दोनों variables के प्रकार पर निर्भर है। Pearson correlation दो संख्याओं के बीच सीधी-रेखा संबंध मापता है। Chi-square जाँचता है कि दो categorical variables स्वतंत्र हैं या नहीं। आम ग़लती है categories को codes (North = 1, South = 2, …) बनाकर Pearson चलाना — तब जवाब इस पर निर्भर करता है कि आपने उन्हें कैसे नंबर दिया।

选择哪种检验取决于两个变量的类型。Pearson 相关衡量两个数值之间的直线关系。卡方检验判断两个类别变量是否独立。经典错误是把类别变成编码(North = 1、South = 2……)再算 Pearson —— 结果就取决于你怎么编号。

The Region Effect Pearson Almost MissedRegion का असर जिसे Pearson लगभग चूक गयाPearson 几乎漏掉的地区效应
Churn really differs by region in our data: about 30% in the East, 35% North, 47% West and 52% South. Chi-square on the region × churn table gives χ² = 48.6, p = 1.6e-10 — a clear association. Now code the regions North = 1, South = 2, East = 3, West = 4 and run Pearson: r = +0.033, p = 0.196, "not significant". Re-number them East, North, West, South and r jumps to +0.176. The regions did not change; only the labels did. (The original page told a made-up story with invented numbers; this one is measured.)
हमारे data में churn सच में region के हिसाब से अलग है: East में लगभग 30%, North 35%, West 48% और South 52%। Region × churn table पर chi-square χ² = 48.6, p = 1.6e-10 देता है — साफ़ संबंध। अब regions को North = 1, South = 2, East = 3, West = 4 code करें और Pearson चलाएँ: r = +0.033, p = 0.196, "significant नहीं"। उन्हें East, North, West, South क्रम में नंबर दें और r +0.176 हो जाता है। Regions नहीं बदले; सिर्फ़ labels बदले। (मूल page में गढ़ी हुई संख्याओं वाली कहानी थी; यह मापी हुई है।)
在我们的数据中,流失率确实因地区而异:East 约 30%,North 35%,West 48%,South 52%。对地区 × 流失列联表做卡方检验,χ² = 48.6,p = 1.6e-10 —— 关联明确。现在把地区编码为 North = 1、South = 2、East = 3、West = 4 再算 Pearson:r = +0.033,p = 0.196,“不显著”。按 East、North、West、South 重新编号,r 跳到 +0.176。地区没变,变的只是标签。(原页面讲的是编造数字的故事;这里是实测结果。)
LIVELIVE互动Same Regions, Different Numbersवही Regions, अलग नंबर同样的地区,不同的编号

Bars: churn rate per region. Dashed line: the best straight line through the codes 1–4. Change the code order and watch Pearson r move while chi-square stays fixed.

Bars: हर region की churn दर। Dashed line: codes 1–4 से गुज़रती सबसे अच्छी सीधी रेखा। Code क्रम बदलें और देखें Pearson r हिलता है जबकि chi-square वही रहता है।

柱:各地区的流失率。虚线:穿过编码 1–4 的最佳直线。改变编码顺序,看 Pearson r 变化而卡方保持不变。

Codes

🧭 PICTURE 3 — WHICH TEST?तस्वीर 3 — कौन सा TEST?图 3 — 用哪种检验?
Which statistical test feature type → target typefeature प्रकार → target प्रकार特征类型 → 目标类型 categorical → categoricalcategorical → categorical类别 → 类别 chi2_contingency · Cramér's V · chi2 (counts)city → churn numeric → categoricalnumeric → categorical数值 → 类别 f_classif (ANOVA F) · point-biserialtenure → churn numeric → numericnumeric → numeric数值 → 数值 f_regression (Pearson r) · Spearmanarea → price any → any, non-linear tooany → any, non-linear भी任意 → 任意,含非线性 mutual_info_classif / _regressionage (U-shape) → churnage (U-shape) → churnage(U 形)→ churn
Propertyगुण性质Chi-squarePearson rANOVA FMutual informationMutual information互信息
Feature → targetFeature → target特征 → 目标categorical → categoricalcategorical → categorical类别 → 类别numeric → numericnumeric → numeric数值 → 数值numeric → categoricalnumeric → categorical数值 → 类别any (mark discrete columns)कोई भी (discrete columns बताएँ)任意(需标明离散列)
Findsपकड़ता है能发现any difference between groupsgroups में कोई भी अंतर组间任何差异straight-line trends onlyसिर्फ़ सीधी-रेखा रुझान仅直线趋势differences in means onlyसिर्फ़ means में अंतर仅均值差异any dependence, also non-linearकोई भी निर्भरता, non-linear भी任何依赖,包括非线性
Strength measureताक़त का माप强度度量Cramér's V (0–1)|r| (0–1)η² (0–1)nats (≥ 0, no upper limit)nats (≥ 0, कोई ऊपरी सीमा नहीं)nats(≥ 0,无上限)
sklearnchi2 (non-negative counts / 0-1)f_regression, r_regressionf_classifmutual_info_classif
# 4) A categorical column: chi-square vs Pearson on label codes
from scipy.stats import chi2_contingency, pearsonr
train = df.loc[X_train.index]
table = pd.crosstab(train["region"], train["churn"])
chi2_stat, p_val, dof, _ = chi2_contingency(table)
cramers_v = np.sqrt(chi2_stat / (table.values.sum() * (min(table.shape) - 1)))
print(table.assign(churn_rate=(table[1] / table.sum(axis=1)).round(3)))
print(f"\nchi-square = {chi2_stat:.1f}, dof = {dof}, p = {p_val:.1e}, Cramér's V = {cramers_v:.3f}")
for order in [["North", "South", "East", "West"], ["East", "North", "West", "South"], ["South", "East", "West", "North"]]:
    codes = train["region"].map({r: i + 1 for i, r in enumerate(order)})
    r, p = pearsonr(codes, train["churn"])
    print("codes " + ", ".join(f"{g}={i + 1}" for i, g in enumerate(order)) + f" → Pearson r = {r:+.3f}, p = {p:.3f}")
OUTPUT
churn 0 1 churn_rate region East 257 112 0.304 North 226 120 0.347 South 199 213 0.517 West 196 177 0.475 chi-square = 48.6, dof = 3, p = 1.6e-10, Cramér's V = 0.180 codes North=1, South=2, East=3, West=4 → Pearson r = +0.033, p = 0.196 codes East=1, North=2, West=3, South=4 → Pearson r = +0.176, p = 0.000 codes South=1, East=2, West=3, North=4 → Pearson r = -0.081, p = 0.002
💡
Reading Chi-SquareChi-Square कैसे पढ़ें如何解读卡方

A small p-value says the variables are probably not independent. It says nothing about how strong the link is — with enough rows, a tiny effect becomes "significant". For strength, use Cramér's V (here 0.180: modest). Also note sklearn's chi2 scorer treats feature values as counts, so use it for non-negative counts or 0/1 columns; for a raw category column, build a crosstab and use scipy.stats.chi2_contingency.

छोटा p-value बताता है कि variables शायद स्वतंत्र नहीं हैं। यह नहीं बताता कि संबंध कितना मज़बूत है — काफ़ी rows हों तो छोटा असर भी "significant" हो जाता है। ताक़त के लिए Cramér's V इस्तेमाल करें (यहाँ 0.180: मध्यम)। ध्यान दें sklearn का chi2 scorer feature मानों को counts मानता है, इसलिए इसे non-negative counts या 0/1 columns पर इस्तेमाल करें; कच्चे category column के लिए crosstab बनाकर scipy.stats.chi2_contingency इस्तेमाल करें।

p 值小说明变量可能不独立,但不说明关联有多强 —— 行数足够多时,微小效应也会“显著”。衡量强度请用 Cramér's V(这里为 0.180:中等)。另外,sklearn 的 chi2 打分函数把特征值当作计数,所以只用于非负计数或 0/1 列;对原始类别列,先做列联表再用 scipy.stats.chi2_contingency。


SectionSection第 05 节

Density Plots — Look Before You Test 👀Density Plots — Test से पहले देखें 👀密度图 —— 检验之前先看 👀

Plot each column's distribution separately for each class. If the curves sit apart, the column separates the classes. If they lie on top of each other, the column is probably useless on its own. A one-line version: sns.kdeplot(data=train, x="age", hue="churn", common_norm=False) — use training rows only.

हर column का distribution हर class के लिए अलग से plot करें। अगर curves अलग हों, तो column classes को अलग करता है। अगर एक-दूसरे पर हों, तो column शायद अकेले बेकार है। एक line में: sns.kdeplot(data=train, x="age", hue="churn", common_norm=False) — सिर्फ़ training rows इस्तेमाल करें।

为每个类别分别画出每一列的分布。曲线分开,说明该列能区分类别;曲线重叠,说明该列单独看可能无用。一行代码:sns.kdeplot(data=train, x="age", hue="churn", common_norm=False) —— 只用训练行。

LIVELIVE互动Does This Column Separate the Classes?क्या यह Column Classes को अलग करता है?这一列能区分类别吗?

Real density curves from the training rows. Each column also shows its F-score, mutual information and the AUC you would get from that column alone.

Training rows से असली density curves। हर column का F-score, mutual information और सिर्फ़ उसी column से मिलने वाला AUC भी दिखता है।

来自训练行的真实密度曲线。每列同时显示 F 值、互信息,以及仅用该列能得到的 AUC。

Column

⚠️
Separation Is Not the Same as a High F-Scoreअलगाव और ऊँचा F-score एक नहीं可分性不等于 F 值高

The F-test only compares means. Age has F = 0.49 even though its curves clearly differ in shape. And a column can look useless alone yet be strong in combination: income alone has AUC 0.514, but the ratio expenses ÷ income has F = 90.3. Density plots catch the first problem; only multi-column methods (Sections 6–7) catch the second.

F-test सिर्फ़ means की तुलना करता है। Age का F = 0.49 है, जबकि उसकी curves का आकार साफ़ अलग है। और कोई column अकेले बेकार दिखकर भी मिलकर मज़बूत हो सकता है: अकेले income का AUC 0.514 है, पर ratio expenses ÷ income का F = 90.3 है। Density plots पहली समस्या पकड़ते हैं; दूसरी सिर्फ़ multi-column methods (Sections 6–7) पकड़ते हैं।

F 检验只比较均值。年龄的 F = 0.49,尽管它的曲线形状明显不同。而且一列单独看可能无用,组合起来却很强:收入单独的 AUC 为 0.514,但支出 ÷ 收入这个比率的 F = 90.3。密度图能发现第一个问题;第二个问题只有多列方法(第 6–7 节)才能发现。


SectionSection第 06 节

Wrapper Methods — RFE and RFECV 🟢Wrapper Methods — RFE और RFECV 🟢包装法 —— RFE 与 RFECV 🟢

RFE (recursive feature elimination) fits a model, removes the column with the smallest importance (for a linear model: the smallest |coefficient| on scaled data), and repeats. RFECV does this inside cross-validation and picks the number of columns with the best average score. Because the model sees all remaining columns together, it can keep columns that only work in combination.

RFE (recursive feature elimination) model fit करता है, सबसे कम importance वाला column हटाता है (linear model के लिए: scaled data पर सबसे छोटा |coefficient|), और दोहराता है। RFECV यह cross-validation के अंदर करता है और सबसे अच्छे औसत score वाली columns की संख्या चुनता है। क्योंकि model बचे हुए सभी columns को साथ देखता है, वह ऐसे columns रख सकता है जो सिर्फ़ मिलकर काम करते हैं।

RFE(递归特征消除)先拟合模型,删除重要性最小的列(线性模型即标准化数据上 |系数| 最小的列),然后重复。RFECV 在交叉验证中做这件事,并选出平均得分最高的列数。由于模型同时看到所有剩余列,它能保留那些只有组合起来才有用的列。

The Hidden Pairछिपी जोड़ी隐藏的组合
In our data, churn depends on the share of income a customer spends. Income alone gives cross-validated AUC 0.477 — no better than a coin — and expenses alone 0.558. Together they give 0.626, because a linear model can combine "high expenses" with "low income". The F-test top 8 dropped income (rank 20) and scored test AUC 0.775. RFECV kept both and scored 0.796 with 9 columns. (The original page described a bank whose AUC rose from 0.82 to 0.89; those numbers were invented.)
हमारे data में churn इस पर निर्भर है कि ग्राहक अपनी income का कितना हिस्सा ख़र्च करता है। अकेले income से cross-validated AUC 0.477 है — सिक्के से बेहतर नहीं — और अकेले expenses से 0.558। साथ में 0.626, क्योंकि linear model "ज़्यादा ख़र्च" को "कम income" के साथ जोड़ सकता है। F-test top 8 ने income (rank 20) गिरा दी और test AUC 0.775 पाया। RFECV ने दोनों रखे और 9 columns के साथ 0.796 पाया। (मूल page में एक bank की AUC 0.82 से 0.89 होने की कहानी थी; वे संख्याएँ गढ़ी हुई थीं।)
在我们的数据中,流失取决于客户把收入花掉的比例。只用收入时交叉验证 AUC 为 0.477 —— 不比抛硬币好 —— 只用支出为 0.558。两者一起是 0.626,因为线性模型能把“支出高”和“收入低”结合起来。F 检验前 8 名丢掉了收入(排名第 20),测试 AUC 为 0.775。RFECV 保留了两者,用 9 列得到 0.796。(原页面描述某银行 AUC 从 0.82 升到 0.89,这些数字是编造的。)
LIVELIVE互动Watch RFECV Drop ColumnsRFECV को Columns हटाते देखें观看 RFECV 剔除列

Top: cross-validated AUC for each number of columns (band = ±1 std across folds). Bottom: columns in the order RFE keeps them — 1 survives longest. Slide or press play.

ऊपर: हर columns संख्या के लिए cross-validated AUC (band = folds में ±1 std)। नीचे: columns उस क्रम में जिसमें RFE उन्हें रखता है — 1 सबसे देर तक बचता है। Slide करें या play दबाएँ।

上:每种列数下的交叉验证 AUC(带 = 各折 ±1 标准差)。下:RFE 保留列的顺序 —— 第 1 个存活最久。拖动滑块或点击播放。

# 5) Wrapper: RFECV — fit, drop the weakest column, repeat; score every size with 5-fold CV
from sklearn.feature_selection import RFECV
from sklearn.model_selection import cross_val_score
logit = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
rfecv = RFECV(logit, step=1, cv=5, scoring="roc_auc", importance_getter="named_steps.logisticregression.coef_")
rfecv.fit(X_train[num], y_train)
cv_auc = rfecv.cv_results_["mean_test_score"]
print(f"best size: {rfecv.n_features_} columns → kept {rfecv.get_feature_names_out().tolist()}")
print("CV AUC by number of columns:", {k: round(float(cv_auc[k - 1]), 3) for k in [1, 2, 3, 5, 8, 10, 15, 20, len(num)]})
for cols in [["income"], ["expenses"], ["income", "expenses"]]:
    print(f"CV AUC using only {cols}: {cross_val_score(logit, X_train[cols], y_train, cv=5, scoring='roc_auc').mean():.3f}")
for name, cols in [("all columns", num), ("F-test top 8", kbest.get_feature_names_out().tolist()),
                   ("RFECV", rfecv.get_feature_names_out().tolist())]:
    logit.fit(X_train[cols], y_train)
    print(f"logistic, {name:<13} ({len(cols):>2} cols): test AUC {roc_auc_score(y_test, logit.predict_proba(X_test[cols])[:, 1]):.3f}")
OUTPUT
best size: 9 columns → kept ['tenure', 'monthly_charges', 'credit_score', 'num_products', 'income', 'expenses', 'data_gb', 'region_South', 'region_West'] CV AUC by number of columns: {1: 0.519, 2: 0.626, 3: 0.679, 5: 0.773, 8: 0.806, 10: 0.815, 15: 0.809, 20: 0.811, 33: 0.813} CV AUC using only ['income']: 0.477 CV AUC using only ['expenses']: 0.558 CV AUC using only ['income', 'expenses']: 0.626 logistic, all columns (33 cols): test AUC 0.788 logistic, F-test top 8 ( 8 cols): test AUC 0.775 logistic, RFECV ( 9 cols): test AUC 0.796
⚖️
RFE Is Not MagicRFE जादू नहींRFE 不是魔法

RFE ranks by coefficient size, which is not the same as usefulness: here the last two survivors are expenses and income — big opposite coefficients that cancel out — while tenure, the best single column, goes earlier. The CV curve is also flat after about 8 columns (0.806 → 0.813), so "the best k" is partly noise. RFECV removes the need to guess k, but it still fits on one dataset, costs many fits, and its choice can change with the folds.

RFE coefficient के आकार से rank करता है, जो उपयोगिता जैसा नहीं: यहाँ अंतिम दो बचे expenses और income हैं — बड़े उलटे coefficients जो एक-दूसरे को काटते हैं — जबकि tenure, सबसे अच्छा अकेला column, पहले हट जाता है। CV curve लगभग 8 columns के बाद सपाट भी है (0.806 → 0.813), इसलिए "सबसे अच्छा k" कुछ हद तक शोर है। RFECV को k का अंदाज़ा नहीं लगाना पड़ता, पर वह फिर भी एक dataset पर fit होता है, कई fits लेता है, और उसका चुनाव folds के साथ बदल सकता है।

RFE 按系数大小排名,这不等于有用程度:这里最后存活的两列是支出和收入 —— 两个大小相近、方向相反的系数 —— 而单独最强的 tenure 反而更早被删。CV 曲线在约 8 列之后也很平(0.806 → 0.813),所以“最佳 k”部分是噪声。RFECV 免去了猜 k,但它仍在一份数据上拟合、需要大量拟合,而且选择会随折的划分而变化。


SectionSection第 07 节

Embedded Methods — L1 and Tree Importance 🟡Embedded Methods — L1 और Tree Importance 🟡嵌入法 —— L1 与树的重要性 🟡

L1 penalty (Lasso) for classificationClassification के लिए L1 penalty (Lasso)用于分类的 L1 惩罚(Lasso)

An L1 penalty pushes small coefficients to exactly zero, so fitting the model also selects columns. For a 0/1 target, use L1 logistic regression — the original page fitted LassoCV, a regression model, to the churn labels. In scikit-learn 1.8+ write LogisticRegression(l1_ratio=1.0, solver="saga"); the old penalty="l1" is deprecated. Smaller C means a stronger penalty and fewer columns. Scale first, or the penalty hits columns unevenly.

L1 penalty छोटे coefficients को ठीक शून्य पर धकेलती है, इसलिए model fit करना ही columns चुनना है। 0/1 target के लिए L1 logistic regression इस्तेमाल करें — मूल page ने churn labels पर LassoCV, एक regression model, fit किया था। scikit-learn 1.8+ में LogisticRegression(l1_ratio=1.0, solver="saga") लिखें; पुराना penalty="l1" deprecated है। छोटा C यानी मज़बूत penalty और कम columns। पहले scale करें, वरना penalty columns पर असमान पड़ती है।

L1 惩罚会把小系数压到恰好为零,因此拟合模型的同时就选出了列。对 0/1 目标,请用 L1 逻辑回归 —— 原页面把回归模型 LassoCV 拟合到流失标签上。在 scikit-learn 1.8+ 中写 LogisticRegression(l1_ratio=1.0, solver="saga");旧的 penalty="l1" 已弃用。C 越小惩罚越强、列越少。请先缩放,否则惩罚对各列不均衡。

LIVELIVE互动Slide the L1 PenaltyL1 Penalty को Slide करें拖动 L1 惩罚

Coefficients of 13 columns (scaled data) as the penalty changes. Hollow circle = exactly zero. Red = noise or the ID.

Penalty बदलने पर 13 columns के coefficients (scaled data)। खोखला वृत्त = ठीक शून्य। लाल = शोर या ID।

惩罚变化时 13 列的系数(标准化数据)。空心圆 = 恰好为零。红色 = 噪声或 ID。

# 6) Embedded: L1-penalised logistic regression (the classification "Lasso")
from sklearn.linear_model import LogisticRegressionCV
from sklearn.feature_selection import SelectFromModel
cols6 = X_tr.columns.tolist()                                           # constant columns gone, BOTH data_gb and streaming_hours kept
sc6 = StandardScaler().fit(X_train[cols6])
Xs = sc6.transform(X_train[cols6])
l1cv = LogisticRegressionCV(Cs=np.logspace(-3, 1, 25), l1_ratios=[1.0], solver="saga", cv=5, scoring="roc_auc",
                            max_iter=5000, random_state=0, use_legacy_attributes=False).fit(Xs, y_train)
coef = pd.Series(l1cv.coef_[0], index=cols6)
print(f"best C = {l1cv.C_:.4f} → {int((coef != 0).sum())} non-zero of {len(cols6)}")
print(coef[coef != 0].round(3).sort_values(key=abs, ascending=False).to_string())
print("test AUC:", round(roc_auc_score(y_test, l1cv.predict_proba(sc6.transform(X_test[cols6]))[:, 1]), 3))
# which twin survives? refit on 100 bootstrap samples at the chosen C
pick = {"data_gb": 0, "streaming_hours": 0, "both": 0, "neither": 0}
r = np.random.default_rng(0)
for b in range(100):
    i = r.integers(0, len(Xs), len(Xs))
    c = LogisticRegression(C=l1cv.C_, l1_ratio=1.0, solver="saga", max_iter=5000).fit(Xs[i], y_train.to_numpy()[i]).coef_[0]
    a, s = c[cols6.index("data_gb")] != 0, c[cols6.index("streaming_hours")] != 0
    pick["both" if a and s else "data_gb" if a else "streaming_hours" if s else "neither"] += 1
print("twins kept over 100 bootstrap fits:", pick)
OUTPUT
best C = 0.4642 → 30 non-zero of 34 expenses 1.359 income -1.294 credit_score -0.753 tenure -0.730 monthly_charges 0.536 region_East -0.426 num_products -0.364 streaming_hours 0.333 region_North -0.274 region_South 0.119 noise_16 0.110 noise_18 -0.109 noise_04 0.088 noise_08 0.085 noise_14 -0.076 noise_01 0.073 noise_17 0.069 noise_07 0.067 noise_02 -0.066 customer_id 0.065 noise_19 -0.062 noise_05 0.060 noise_03 -0.059 noise_09 -0.054 noise_10 -0.037 noise_06 0.037 noise_12 0.031 data_gb 0.018 noise_11 -0.015 noise_15 0.004 test AUC: 0.792 twins kept over 100 bootstrap fits: {'data_gb': 6, 'streaming_hours': 27, 'both': 67, 'neither': 0}
🎲
Zero Does Not Mean Irrelevantशून्य का मतलब बेकार नहीं系数为零不等于无关

Two lessons from the output. First, cross-validation picked C = 0.46, which keeps 30 of 34 columns — including most noise columns with small weights. CV-tuned Lasso is built to predict well, not to find the true columns, and it tends to keep too many. Second, data_gb and streaming_hours are near-copies (r = 0.96): over 100 bootstrap refits, Lasso kept both 67 times, only streaming_hours 27 times and only data_gb 6 times. Which twin survives is partly chance, so a zero coefficient does not prove a column is useless.

Output से दो सबक़। पहला, cross-validation ने C = 0.46 चुना, जो 34 में से 30 columns रखता है — ज़्यादातर शोर columns छोटे weights के साथ। CV-tuned Lasso अच्छी prediction के लिए बना है, सच्चे columns ढूँढने के लिए नहीं, और ज़्यादा रखता है। दूसरा, data_gb और streaming_hours लगभग copies हैं (r = 0.96): 100 bootstrap refits में Lasso ने 67 बार दोनों रखे, 27 बार सिर्फ़ streaming_hours और 6 बार सिर्फ़ data_gb। कौन सा जुड़वाँ बचेगा यह कुछ हद तक संयोग है, इसलिए शून्य coefficient यह साबित नहीं करता कि column बेकार है।

输出给出两点教训。第一,交叉验证选了 C = 0.46,保留了 34 列中的 30 列 —— 包括大部分以小权重进入的噪声列。用 CV 调参的 Lasso 是为预测而设计的,不是为找出真实列,往往保留太多。第二,data_gb 与 streaming_hours 几乎是复制品(r = 0.96):在 100 次自助重拟合中,Lasso 67 次两者都保留,27 次只留 streaming_hours,6 次只留 data_gb。哪个“双胞胎”存活部分是偶然的,所以系数为零并不能证明该列无用。

Random forest importance — impurity vs permutationRandom forest importance — impurity बनाम permutation随机森林重要性 —— 不纯度 vs 置换

feature_importances_ adds up how much each column reduced impurity in the training trees. It is biased toward columns with many distinct values — continuous noise and IDs offer many possible split points, and a deep tree uses them to memorise training rows. Permutation importance shuffles one column on held-out data and measures how much the score drops; useless columns score near zero.

feature_importances_ जोड़ता है कि हर column ने training trees में impurity कितनी घटाई। यह बहुत अलग-अलग मानों वाले columns की ओर पक्षपाती है — continuous शोर और IDs कई split points देते हैं, और गहरा tree उनसे training rows याद कर लेता है। Permutation importance held-out data पर एक column को shuffle करके मापता है कि score कितना गिरा; बेकार columns का score लगभग शून्य होता है।

feature_importances_ 累加每列在训练树中降低的不纯度。它偏向取值很多的列 —— 连续噪声和 ID 提供大量可切分点,深树会用它们记住训练行。置换重要性在留出数据上打乱某一列,测量得分下降多少;无用的列得分接近零。

🌲 PICTURE 4 — SAME FOREST, TWO IMPORTANCESतस्वीर 4 — वही FOREST, दो IMPORTANCES图 4 — 同一片森林,两种重要性
Impurity vs permutation importance same random forest, two ways to measure importanceवही random forest, importance मापने के दो तरीके同一个随机森林,两种重要性度量 impurity (feature_importances_) tenure 0.0778 credit_score 0.0739 monthly_charges 0.0534 age 0.0483 data_gb 0.0395 expenses 0.0378 noise_19 0.0305 noise_01 0.0303 noise_03 0.0297 customer_id 0.0293 permutation on test (AUC drop) +0.1058 +0.0599 +0.0341 +0.0415 +0.0116 +0.0197 -0.0001 +0.0005 +0.0007 +0.0012 red = pure noise or an ID: impurity rewards them for having many split pointsलाल = शुद्ध शोर या ID: impurity उन्हें कई split points के लिए इनाम देता है红色 = 纯噪声或 ID:不纯度会因为它们可切分点多而给高分

Impurity gives the 20 noise columns 56% of all importance; permutation gives them almost nothing.

Impurity 20 शोर columns को कुल importance का 56% देता है; permutation लगभग कुछ नहीं।

不纯度把全部重要性的 56% 给了 20 个噪声列;置换重要性几乎为零。

# 7) Random forest importance: impurity (built in) vs permutation (on the test set)
from sklearn.inspection import permutation_importance
rf = RandomForestClassifier(300, random_state=0, n_jobs=-1).fit(X_train[num], y_train)
perm = permutation_importance(rf, X_test[num], y_test, scoring="roc_auc", n_repeats=20, random_state=0, n_jobs=-1)
imp = pd.DataFrame({"impurity": rf.feature_importances_, "permutation": perm.importances_mean,
                    "perm_std": perm.importances_std}, index=num)
imp["impurity rank"] = imp["impurity"].rank(ascending=False).astype(int)
imp["perm rank"] = imp["permutation"].rank(ascending=False).astype(int)
print(imp.sort_values("impurity", ascending=False).round(4).head(10).to_string())
c = imp.loc["customer_id"]
print(f"customer_id: impurity {c.impurity:.4f} (rank {int(c['impurity rank'])}) | permutation {c.permutation:+.4f} (rank {int(c['perm rank'])})")
print(f"\nimpurity says the 20 noise columns hold {imp.loc[imp.index.str.startswith('noise'), 'impurity'].sum():.0%} of the importance;"
      f" permutation gives them {imp.loc[imp.index.str.startswith('noise'), 'permutation'].sum():+.4f} AUC in total")
sfm = SelectFromModel(rf, prefit=True, threshold="mean")
print("SelectFromModel(threshold='mean') on impurity keeps:", np.array(num)[sfm.get_support()].tolist())
OUTPUT
impurity permutation perm_std impurity rank perm rank tenure 0.0778 0.1058 0.0146 1 1 credit_score 0.0739 0.0599 0.0149 2 2 monthly_charges 0.0534 0.0341 0.0072 3 4 age 0.0483 0.0415 0.0103 4 3 data_gb 0.0395 0.0116 0.0043 5 7 expenses 0.0378 0.0197 0.0067 6 5 noise_19 0.0305 -0.0001 0.0025 7 28 noise_01 0.0303 0.0005 0.0019 8 27 noise_03 0.0297 0.0007 0.0019 9 24 customer_id 0.0293 0.0012 0.0016 10 18 customer_id: impurity 0.0293 (rank 10) | permutation +0.0012 (rank 18) impurity says the 20 noise columns hold 56% of the importance; permutation gives them +0.0073 AUC in total SelectFromModel(threshold='mean') on impurity keeps: ['tenure', 'monthly_charges', 'credit_score', 'age', 'expenses', 'data_gb', 'noise_01', 'noise_19']

So SelectFromModel(rf, threshold="mean") on impurity importance kept noise_01 and noise_19, and the ID ranked 10th. The original page said the ID had "near-zero importance" — with impurity importance it does not. Use permutation importance on held-out data (or on CV folds) when you select with a forest.

इसलिए impurity importance पर SelectFromModel(rf, threshold="mean") ने noise_01 और noise_19 रखे, और ID 10वें स्थान पर था। मूल page ने कहा था कि ID की "लगभग शून्य importance" है — impurity importance में ऐसा नहीं। Forest से चुनते समय held-out data (या CV folds) पर permutation importance इस्तेमाल करें।

所以基于不纯度重要性的 SelectFromModel(rf, threshold="mean") 保留了 noise_01 和 noise_19,而 ID 排在第 10。原页面说 ID 的“重要性接近零”—— 用不纯度重要性时并非如此。用森林做选择时,请在留出数据(或交叉验证折)上使用置换重要性。


SectionSection第 08 节

The Selection Trap — Select Inside Cross-Validation 🔒Selection का जाल — Cross-Validation के अंदर चुनें 🔒选择陷阱 —— 在交叉验证内部做选择 🔒

Feature selection looks at the target. If you choose columns using all rows and then cross-validate, the test folds have already helped pick the columns. With many candidate columns, some will match the labels by pure chance, and the score becomes fiction. The fix: put the selector inside a Pipeline so it is refitted on the training part of every fold.

Feature selection target को देखता है। अगर आप सभी rows से columns चुनकर फिर cross-validate करते हैं, तो test folds पहले ही columns चुनने में मदद कर चुके हैं। बहुत candidate columns हों तो कुछ संयोग से labels से मेल खाएँगे, और score कहानी बन जाता है। उपाय: selector को Pipeline में रखें ताकि हर fold के training हिस्से पर दोबारा fit हो।

特征选择会看目标。如果先用全部行选列再做交叉验证,测试折其实已经参与了选列。候选列很多时,总有一些会纯属偶然地与标签吻合,分数就成了虚构。解决办法:把选择器放进 Pipeline,让它在每一折的训练部分上重新拟合。

LIVELIVE互动Pure Noise, Impressive Scoreशुद्ध शोर, शानदार Score纯噪声,漂亮的分数

100 rows, random labels, random columns — the true accuracy is 50%. Red: pick the top 20 columns using all rows, then CV. Green: pick them inside each CV fold.

100 rows, random labels, random columns — असली accuracy 50% है। लाल: सभी rows से top 20 columns चुनें, फिर CV। हरा: हर CV fold के अंदर चुनें।

100 行、随机标签、随机列 —— 真实准确率是 50%。红:先用全部行选出前 20 列再做 CV。绿:在每一折内部选列。

# 8) The selection trap: choose columns on ALL rows, then cross-validate → a fake score on pure noise
from sklearn.pipeline import Pipeline
def selection_gap(n_cols, seed, n_rows=100, k=20):
    r = np.random.default_rng(seed)
    Xn, yn = r.normal(size=(n_rows, n_cols)), r.integers(0, 2, n_rows)      # NO real signal anywhere
    keep = SelectKBest(f_classif, k=k).fit(Xn, yn).get_support()             # wrong: selector saw every row
    wrong = cross_val_score(LogisticRegression(max_iter=1000), Xn[:, keep], yn, cv=5).mean()
    right = cross_val_score(Pipeline([("sel", SelectKBest(f_classif, k=k)), ("lr", LogisticRegression(max_iter=1000))]),
                            Xn, yn, cv=5).mean()                             # right: selection redone inside each fold
    return wrong, right
for n_cols in [20, 100, 1000, 5000]:
    res = np.array([selection_gap(n_cols, s) for s in range(20)])
    print(f"{n_cols:>5} noise columns → top 20 kept | CV accuracy wrong {res[:, 0].mean():.3f}, right {res[:, 1].mean():.3f}")
OUTPUT
20 noise columns → top 20 kept | CV accuracy wrong 0.521, right 0.521 100 noise columns → top 20 kept | CV accuracy wrong 0.701, right 0.525 1000 noise columns → top 20 kept | CV accuracy wrong 0.826, right 0.504 5000 noise columns → top 20 kept | CV accuracy wrong 0.877, right 0.500
🚨
This Leak Is Largeयह Leak बड़ा है这个泄漏很大

Unlike fitting a scaler on all rows (usually a tiny effect), selecting on all rows can invent a strong model from nothing: 88% "accuracy" from 5,000 random columns, while the honest pipeline gives 50%. The same applies to RFECV, Lasso or importance-based selection run before the split. Rule: everything that looks at y belongs inside the pipeline.

सभी rows पर scaler fit करने (आमतौर पर छोटा असर) के उलट, सभी rows पर चुनाव कुछ नहीं से मज़बूत model गढ़ सकता है: 5,000 random columns से 88% "accuracy", जबकि ईमानदार pipeline 50% देती है। यही बात split से पहले चलाए गए RFECV, Lasso या importance-आधारित चयन पर लागू है। नियम: जो कुछ भी y देखता है, वह pipeline के अंदर है।

与在全部行上拟合缩放器(通常影响很小)不同,在全部行上做选择能凭空造出一个“强”模型:5,000 个随机列得到 88% 的“准确率”,而诚实的管道只有 50%。在划分前运行 RFECV、Lasso 或基于重要性的选择也一样。规则:凡是看 y 的步骤,都要放进管道里。


SectionSection第 09 节

Before vs After — All Methods Compared 📊पहले बनाम बाद — सभी Methods की तुलना 📊前后对比 —— 所有方法比较 📊

Each method chose its columns from training rows only. Then the same two models were trained and judged on the untouched test set. "the 12 true columns" is a reference you never have in real life.

हर method ने सिर्फ़ training rows से columns चुने। फिर वही दो models train हुए और अनछुए test set पर परखे गए। "the 12 true columns" एक reference है जो असल ज़िंदगी में कभी नहीं मिलता।

每种方法都只用训练行选列。然后用同样两个模型训练,并在未动过的测试集上评估。“12 个真实列”只是参考,现实中你永远拿不到。

📊 PICTURE 5 — TEST AUC BY METHODतस्वीर 5 — METHOD के हिसाब से TEST AUC图 5 — 各方法的测试 AUC
Test AUC by selection method 0.65 0.70 0.75 0.80 0.85 0.790 0.711 all36 cols 0.788 0.693 var+corr33 cols 0.775 0.750 F top 88 cols 0.753 0.743 MI top 1010 cols 0.796 0.747 RFECV9 cols 0.794 0.733 L118 cols 0.756 0.745 RF imp.8 cols 0.798 0.783 true 1212 cols logistic regressionKNN (k = 25)test AUC (y axis starts at 0.65)

KNN gains up to 0.04 from most methods. Logistic regression barely moves with RFECV or L1, but loses about 0.04 when a filter drops income. The y axis starts at 0.65.

ज़्यादातर methods से KNN को 0.04 तक फ़ायदा। RFECV या L1 से logistic regression मुश्किल से हिलता है, पर जब कोई filter income गिराता है तो लगभग 0.04 खोता है। Y axis 0.65 से शुरू।

大多数方法让 KNN 提升最多约 0.04。逻辑回归在 RFECV 或 L1 下几乎不变,但当过滤法丢掉收入时下降约 0.04。纵轴从 0.65 开始。

# 9) Every method side by side — the same two models, judged on the untouched test set
from sklearn.feature_selection import SelectFromModel
mi_top10 = scores["MI (nats)"].nlargest(10).index.tolist()
l1_c = LogisticRegression(C=0.05, l1_ratio=1.0, solver="saga", max_iter=5000).fit(Xs, y_train).coef_[0]
sets = {"all columns": list(X.columns),
        "variance + correlation": num,
        "F-test top 8": kbest.get_feature_names_out().tolist(),
        "MI top 10": mi_top10,
        "RFECV (logistic)": rfecv.get_feature_names_out().tolist(),
        "L1 logistic (C = 0.05)": [c for c, w in zip(cols6, l1_c) if w != 0],
        "RF impurity > mean": np.array(num)[sfm.get_support()].tolist(),
        "the 12 true columns": useful}
rows = []
for name, cols in sets.items():
    row = {"method": name, "columns": len(cols)}
    for mname, m in [("logistic", make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))),
                     ("KNN", make_pipeline(StandardScaler(), KNeighborsClassifier(25)))]:
        m.fit(X_train[cols], y_train)
        row[f"{mname} test AUC"] = roc_auc_score(y_test, m.predict_proba(X_test[cols])[:, 1])
    rows.append(row)
bench = pd.DataFrame(rows).set_index("method")
print(bench.round(3).to_string())
OUTPUT
columns logistic test AUC KNN test AUC method all columns 36 0.790 0.711 variance + correlation 33 0.788 0.693 F-test top 8 8 0.775 0.750 MI top 10 10 0.753 0.743 RFECV (logistic) 9 0.796 0.747 L1 logistic (C = 0.05) 18 0.794 0.733 RF impurity > mean 8 0.756 0.745 the 12 true columns 12 0.798 0.783
⚖️
An Honest Summaryएक ईमानदार सार诚实的总结

No method found the perfect set. Wrapper (RFECV) did best for logistic regression; every method except the variance + correlation filter helped KNN; filters that judge columns alone hurt logistic regression by dropping income. The original page showed a jump from 61% to 79% test accuracy and from 48 s to 4.8 s training time — those numbers were made up. Real gains depend on the model, the noise and the sample size, so measure them.

किसी method को perfect set नहीं मिला। Logistic regression के लिए wrapper (RFECV) सबसे अच्छा रहा; variance + correlation filter को छोड़कर हर method ने KNN की मदद की; columns को अकेले परखने वाले filters ने income गिराकर logistic regression को नुक़सान पहुँचाया। मूल page में test accuracy 61% से 79% और training समय 48 s से 4.8 s दिखाया गया था — वे संख्याएँ गढ़ी हुई थीं। असली फ़ायदा model, शोर और sample आकार पर निर्भर है, इसलिए मापें।

没有哪种方法找到完美的列集。对逻辑回归,包装法(RFECV)最好;除方差 + 相关过滤外,每种方法都帮助了 KNN;逐列判断的过滤法丢掉了收入,反而伤害了逻辑回归。原页面展示测试准确率从 61% 升到 79%、训练时间从 48 秒降到 4.8 秒 —— 这些数字是编造的。真实收益取决于模型、噪声和样本量,所以要实际测量。


SectionSection第 10 节

A Complete Selection Pipeline 🔗पूरी Selection Pipeline 🔗完整的特征选择管道 🔗

Put every step in one Pipeline: imputation, the variance check (before scaling), scaling, encoding, selection and the model. Cross-validation then refits the selector in each fold, and the saved pipeline applies exactly the same columns to new data. Two bugs fixed from the original: pipeline.score() returns accuracy, not AUC, so we compute AUC with roc_auc_score; and new_raw_data was never defined, so we predict on a real test row.

हर क़दम एक Pipeline में रखें: imputation, variance जाँच (scaling से पहले), scaling, encoding, selection और model। तब cross-validation हर fold में selector को दोबारा fit करता है, और saved pipeline नए data पर बिल्कुल वही columns लगाती है। मूल से दो bugs ठीक किए: pipeline.score() accuracy देता है, AUC नहीं, इसलिए हम roc_auc_score से AUC निकालते हैं; और new_raw_data कभी define नहीं हुआ था, इसलिए हम एक असली test row पर predict करते हैं।

把每一步放进同一个 Pipeline:填补、方差检查(缩放前)、缩放、编码、选择和模型。这样交叉验证会在每一折重新拟合选择器,保存的管道对新数据使用完全相同的列。修正了原代码的两个错误:pipeline.score() 返回的是准确率而不是 AUC,所以我们用 roc_auc_score 计算 AUC;new_raw_data 从未定义,所以我们对一条真实测试行做预测。

# 10) A production pipeline: selection is fitted inside every CV fold and saved with the model
#     (RFECV inside cross_val_score = nested CV, so this takes a few seconds)
import joblib
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
raw = df.drop(columns="churn")
R_train, R_test = raw.loc[X_train.index], raw.loc[X_test.index]
num_cols = [c for c in raw.columns if c not in ("region", "country")]
prep = ColumnTransformer([
    ("num", Pipeline([("imp", SimpleImputer(strategy="median")),
                     ("var", VarianceThreshold(0.01)),        # BEFORE scaling: after it every variance is 1
                     ("sc", StandardScaler())]), num_cols),
    ("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), ["region", "country"])])
pipe = Pipeline([
    ("prep", prep),
    ("drop_constant", VarianceThreshold(0.0)),             # catches the one-hot column of "country" (always IN)
    ("select", RFECV(LogisticRegression(max_iter=2000), cv=5, scoring="roc_auc")),   # wrapper, refitted inside every fold
    ("model", KNeighborsClassifier(25))])
cv = cross_val_score(pipe, R_train, y_train, cv=5, scoring="roc_auc")
pipe.fit(R_train, y_train)
test_auc = roc_auc_score(y_test, pipe.predict_proba(R_test)[:, 1])      # not pipe.score(): that is accuracy
print(f"CV AUC {cv.mean():.3f} ± {cv.std():.3f} | test AUC {test_auc:.3f}")
print("columns the model uses:", pipe[:-1].get_feature_names_out().tolist())
joblib.dump(pipe, "churn_pipeline.joblib")
print("new customer → churn probability", round(pipe.predict_proba(R_test.head(1))[0, 1], 3))
OUTPUT
CV AUC 0.761 ± 0.015 | test AUC 0.773 columns the model uses: ['num__tenure', 'num__monthly_charges', 'num__credit_score', 'num__num_products', 'num__income', 'num__expenses', 'num__streaming_hours', 'cat__region_East', 'cat__region_North', 'cat__region_South', 'cat__region_West'] new customer → churn probability 0.2

RFECV inside the pipeline kept 11 columns — no noise, no ID — and lifted KNN from test AUC 0.711 (all columns) to 0.773.

Pipeline के अंदर RFECV ने 11 columns रखे — कोई शोर नहीं, कोई ID नहीं — और KNN को test AUC 0.711 (सभी columns) से 0.773 पर पहुँचाया।

管道内的 RFECV 保留了 11 列 —— 没有噪声,也没有 ID —— 把 KNN 的测试 AUC 从 0.711(全部列)提升到 0.773。


SectionSection第 11 节

Make the Practice Data 🐍Practice Data बनाएँ 🐍生成练习数据 🐍

Run this first. Churn depends on tenure, monthly charges, credit score, number of products, region, data usage, a U-shaped age effect and the expenses ÷ income ratio. Everything else is a trap: an ID, a constant, a near-constant flag, a near-copy and 20 noise columns.

इसे पहले चलाएँ। Churn tenure, monthly charges, credit score, products की संख्या, region, data usage, U-आकार के age असर और expenses ÷ income ratio पर निर्भर है। बाक़ी सब जाल है: एक ID, एक constant, लगभग constant flag, एक लगभग copy और 20 शोर columns।

请先运行它。流失取决于在网时长、月费、信用分、产品数、地区、流量用量、U 形的年龄效应以及支出 ÷ 收入比率。其余都是陷阱:一个 ID、一个常数列、一个近常数标记、一个近似副本和 20 个噪声列。

# 0) Practice data: a telecom churn table with useful, redundant, useless and tricky columns
import numpy as np, pandas as pd
rng = np.random.default_rng(11)
n = 2000
tenure = rng.integers(1, 73, n)                                        # months as a customer
monthly_charges = np.round(rng.uniform(200, 1500, n), -1)              # ₹ per month
credit_score = np.round(rng.normal(700, 60, n))
num_products = rng.integers(1, 6, n)
age = rng.integers(18, 76, n)
log_inc = rng.normal(11, 0.5, n)
income = np.round(np.exp(log_inc), -2)                                 # ₹ per month
ratio = np.clip(rng.normal(0.65, 0.18, n), 0.15, 1.3)                  # expenses ÷ income
expenses = np.round(income * ratio, -2)
data_gb = np.round(np.clip(rng.gamma(4, 6, n), 0.5, None), 1)          # monthly data used
streaming_hours = np.round(data_gb * 1.9 + rng.normal(0, 7, n), 1)     # almost a copy of data_gb
region = rng.choice(["North", "South", "East", "West"], n)
z = (-0.035 * (tenure - 36) + 0.0016 * (monthly_charges - 850) - 0.012 * (credit_score - 700)
     - 0.25 * (num_products - 3) + 0.003 * (age - 46) ** 2 - 1.6
     + 5.0 * (ratio - 0.65) + 0.03 * (data_gb - 24)
     + pd.Series(region).map({"North": 0.0, "South": 0.9, "East": -0.5, "West": 0.6}).to_numpy())
churn = (rng.random(n) < 1 / (1 + np.exp(-z))).astype(int)
df = pd.DataFrame({"customer_id": rng.permutation(np.arange(100001, 100001 + n)), "tenure": tenure,
                   "monthly_charges": monthly_charges, "credit_score": credit_score, "num_products": num_products,
                   "age": age, "income": income, "expenses": expenses, "data_gb": data_gb,
                   "streaming_hours": streaming_hours, "region": region, "country": "IN",
                   "is_test_account": (rng.random(n) < 0.003).astype(int)})
for k in range(1, 21):                                                 # 20 columns of pure noise
    df[f"noise_{k:02d}"] = np.round(rng.normal(0, 1, n), 3)
df["churn"] = churn
df.to_csv("churn.csv", index=False)
print(f"churn.csv: {df.shape[0]:,} rows × {df.shape[1] - 1} feature columns, churn rate {churn.mean():.1%}")
OUTPUT
churn.csv: 2,000 rows × 33 feature columns, churn rate 41.4%

SectionSection第 12 节

Golden Rules of Feature Selection 🏆Feature Selection के सुनहरे नियम 🏆特征选择黄金法则 🏆

🎯 9 Rules Every Data Scientist Should Follow9 नियम जो हर Data Scientist को मानने चाहिए每位数据科学家都应遵守的 9 条法则
1
Remove obvious junk first: constant columns (before scaling), IDs, near-copies and anything that would not exist at prediction time.
पहले साफ़ कचरा हटाएँ: constant columns (scaling से पहले), IDs, लगभग copies और जो कुछ prediction के समय मौजूद न हो।
先去掉明显的垃圾:常数列(缩放前)、ID、近似副本,以及预测时不会存在的任何列。
2
Match the test to the data types: chi-square for category × category, F-test for number × class, Pearson for number × number, mutual information for anything non-linear. Never run Pearson on label codes.
Test को data types से मिलाएँ: category × category के लिए chi-square, number × class के लिए F-test, number × number के लिए Pearson, non-linear के लिए mutual information। Label codes पर Pearson कभी नहीं।
让检验匹配数据类型:类别 × 类别用卡方,数值 × 类别用 F 检验,数值 × 数值用 Pearson,非线性用互信息。绝不对标签编码算 Pearson。
3
Treat filter scores as a first pass. They judge one column at a time and miss U-shapes (F-test) and columns that only work together.
Filter scores को पहला दौर मानें। वे एक बार में एक column परखते हैं और U-आकार (F-test) व साथ में काम करने वाले columns चूकते हैं।
把过滤法分数当作初筛。它们一次只评一列,会漏掉 U 形关系(F 检验)和只在组合时有用的列。
4
Look at class density plots — they show shape, not just means.
Class density plots देखें — वे आकार दिखाते हैं, सिर्फ़ means नहीं।
看分类别的密度图 —— 它们展示形状,而不只是均值。
5
Use RFECV or L1 when columns may work together, and remember both are noisy: flat CV curves and correlated twins make the exact choice unstable.
जब columns साथ में काम कर सकते हों तब RFECV या L1 इस्तेमाल करें, और याद रखें दोनों में शोर है: सपाट CV curves और correlated जुड़वाँ सटीक चुनाव को अस्थिर बनाते हैं।
列可能需要组合时用 RFECV 或 L1,并记住两者都有噪声:平坦的 CV 曲线和高度相关的“双胞胎”会让具体选择不稳定。
6
For classification use L1 logistic regression, not Lasso; and do not trust CV-tuned C to give a small set — it usually keeps too many.
Classification के लिए L1 logistic regression इस्तेमाल करें, Lasso नहीं; और CV-tuned C से छोटे set की उम्मीद न करें — वह आमतौर पर ज़्यादा रखता है।
分类问题用 L1 逻辑回归,而不是 Lasso;也别指望 CV 调出的 C 给出小的列集 —— 它通常保留太多。
7
Select with permutation importance on held-out data, not with a forest's impurity importance.
Forest की impurity importance से नहीं, held-out data पर permutation importance से चुनें।
用留出数据上的置换重要性做选择,而不是森林的不纯度重要性。
8
Put every step that looks at the target inside a Pipeline so cross-validation refits it in each fold.
Target देखने वाला हर क़दम Pipeline के अंदर रखें ताकि cross-validation हर fold में उसे दोबारा fit करे।
把所有会看目标的步骤放进 Pipeline,让交叉验证在每一折重新拟合。
9
Judge every choice on test or CV scores, and save the whole fitted pipeline so production uses exactly the same columns.
हर चुनाव को test या CV scores पर परखें, और पूरी fitted pipeline save करें ताकि production बिल्कुल वही columns इस्तेमाल करे।
用测试或 CV 分数评判每个选择,并保存整个拟合好的管道,确保生产环境使用完全相同的列。
🧮
Key Takeawayमुख्य सीख核心要点

Feature selection removes noise, but every method has blind spots. On our churn data the F-test missed age and income, Pearson on codes missed region, impurity importance gave noise 56% of the credit, and CV-tuned Lasso kept 30 of 34 columns. What worked: cheap filters for junk, a multi-column method (RFECV) for the rest, and everything inside a pipeline — which lifted KNN from 0.711 to 0.773 and kept the score honest. Select on training data only: on pure noise, selecting first and validating later produced 88% "accuracy".

Feature selection शोर हटाता है, पर हर method के अंधे कोने हैं। हमारे churn data पर F-test ने age और income चूके, codes पर Pearson ने region चूका, impurity importance ने शोर को 56% श्रेय दिया, और CV-tuned Lasso ने 34 में से 30 columns रखे। जो चला: कचरे के लिए सस्ते filters, बाक़ी के लिए multi-column method (RFECV), और सब कुछ pipeline के अंदर — जिसने KNN को 0.711 से 0.773 पर पहुँचाया और score ईमानदार रखा। सिर्फ़ training data पर चुनें: शुद्ध शोर पर पहले चुनकर बाद में validate करने से 88% "accuracy" बनी।

特征选择能去除噪声,但每种方法都有盲区。在我们的流失数据上,F 检验漏掉了年龄和收入,对编码算 Pearson 漏掉了地区,不纯度重要性把 56% 的功劳给了噪声,CV 调参的 Lasso 保留了 34 列中的 30 列。有效的做法是:用低成本过滤法清除垃圾,用多列方法(RFECV)处理其余部分,并把一切放进管道 —— 这让 KNN 从 0.711 提升到 0.773,同时分数保持诚实。只在训练数据上做选择:在纯噪声上先选再验证,会得到 88% 的“准确率”。