Why Models Cannot Read Text Labels 🔤Models Text Labels क्यों नहीं पढ़ सकते 🔤为什么模型读不懂文本标签 🔤
A model is a mathematical function: it multiplies, adds and measures distances. "Mumbai" or "Premium" means nothing to it until we turn it into numbers. Encoding does that — and the goal is to keep the real information without inventing relationships that do not exist, such as "Delhi is twice Chennai".
Model एक गणितीय function है: यह गुणा करता है, जोड़ता है और दूरियाँ मापता है। "Mumbai" या "Premium" का उसके लिए कोई मतलब नहीं जब तक हम उसे संख्या न बनाएँ। Encoding यही करता है — और लक्ष्य है असली जानकारी बनाए रखना, बिना ऐसे संबंध गढ़े जो मौजूद ही नहीं, जैसे "Delhi, Chennai का दोगुना है"।
模型是一个数学函数:做乘法、加法、计算距离。在变成数字之前,“Mumbai” 或 “Premium” 对它毫无意义。编码就是做这件事 —— 目标是保留真实信息,又不捏造并不存在的关系,比如“Delhi 是 Chennai 的两倍”。
Blue dots: real churn rate of each city (training data). Squares: what logistic regression can fit with each encoding.
नीले बिंदु: हर city की असली churn दर (training data)। वर्ग: हर encoding के साथ logistic regression क्या fit कर सकता है।
蓝点:每个城市的真实流失率(训练数据)。方块:每种编码下逻辑回归能拟合出的结果。
Each kind of column needs its own encoder:
हर तरह के column को अपना encoder चाहिए:
每种列需要各自的编码器:
# 1) Look at every categorical column first: type, number of values, order?
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
df = pd.read_csv("customers.csv")
cat_cols = ["city", "income_bracket", "pincode", "gender", "plan"]
print(df[cat_cols].nunique().to_string())
print("\npincode rows: top 5 =", df["pincode"].value_counts().head(5).sum(),
"| pincodes with < 5 rows:", (df["pincode"].value_counts() < 5).sum())
train, test = train_test_split(df, test_size=0.25, random_state=0, stratify=df["churn"])
print("pincodes in test but not in train:", len(set(test["pincode"]) - set(train["pincode"])))
Label Encoding — and Why It Is for the Target 🔢Label Encoding — और यह Target के लिए क्यों है 🔢标签编码 —— 以及为什么它是给目标用的 🔢
LabelEncoder sorts the values alphabetically and numbers them 0, 1, 2, … It works on one 1-D array and is designed for class labels (y). On a feature, the numbers form a number line that a linear, distance-based or neural model takes seriously, even though the order is an accident of spelling.
LabelEncoder मानों को वर्णमाला क्रम में लगाकर 0, 1, 2, … नंबर देता है। यह एक 1-D array पर काम करता है और class labels (y) के लिए बना है। Feature पर ये संख्याएँ एक number line बनाती हैं जिसे linear, दूरी-आधारित या neural model गंभीरता से लेता है, भले ही क्रम सिर्फ़ वर्तनी का संयोग हो।
LabelEncoder 把取值按字母排序并编号 0、1、2……它作用于一维数组,专为类别标签(y)设计。用在特征上时,这些数字会形成一条数轴,线性模型、基于距离的模型或神经网络都会当真,尽管这个顺序只是拼写的偶然结果。
# 2) LabelEncoder: alphabetical integers — meant for the TARGET, not features
from sklearn.preprocessing import LabelEncoder, OrdinalEncoder
le = LabelEncoder().fit(train["city"])
print(dict(zip(le.classes_, range(len(le.classes_)))))
print("churn rate by code:", train.groupby(le.transform(train["city"]))["churn"].mean().round(2).tolist())
# A common bug: one LabelEncoder reused in a loop — the last fit wins
for col in ["gender", "plan"]:
le = LabelEncoder()
train[col + "_enc"] = le.fit_transform(train[col])
print("decoding gender with the LAST encoder:", le.inverse_transform(train["gender_enc"].head(3)).tolist(),
"← should be", train["gender"].head(3).tolist())
# For features use OrdinalEncoder (2-D, many columns, handles unknowns)
oe = OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1).fit(train[["gender", "plan"]])
print("OrdinalEncoder categories:", [list(c) for c in oe.categories_])
Fitting a new LabelEncoder inside a loop and then calling le.inverse_transform afterwards decodes with the last encoder — above, gender codes came back as plan names. Keep one fitted encoder per column (a dict), or better, one OrdinalEncoder for all feature columns.
Loop के अंदर नया LabelEncoder fit करके बाद में le.inverse_transform बुलाने पर आख़िरी encoder से decode होता है — ऊपर gender codes, plan के नाम बनकर लौटे। हर column का एक fit किया encoder रखें (dict में), या बेहतर, सभी feature columns के लिए एक OrdinalEncoder।
在循环里每次新建并拟合 LabelEncoder,之后再调用 le.inverse_transform,会用最后一个编码器来解码 —— 上面的性别编码被还原成了套餐名称。每列保留一个拟合好的编码器(存在字典里),更好的做法是用一个 OrdinalEncoder 处理所有特征列。
Decision trees split on thresholds, so arbitrary integer codes do not mislead them in the same way — they can still isolate any city, though it may take several splits. In Section 7 boosting scores almost the same with codes or one-hot. HistGradientBoostingClassifier can also treat a column as categorical directly (categorical_features="from_dtype" with a pandas category column).
Decision trees thresholds पर बँटते हैं, इसलिए मनमाने integer codes उन्हें उसी तरह नहीं भटकाते — वे फिर भी किसी भी city को अलग कर सकते हैं, भले कई splits लगें। Section 7 में boosting codes और one-hot दोनों से लगभग बराबर स्कोर करता है। HistGradientBoostingClassifier किसी column को सीधे categorical भी मान सकता है (pandas category column के साथ categorical_features="from_dtype")।
决策树按阈值分裂,所以任意整数编码不会以同样的方式误导它 —— 它仍能把任意城市单独分出来,只是可能需要多次分裂。在第 7 节中,梯度提升用整数编码和独热编码的得分几乎相同。HistGradientBoostingClassifier 还能直接把某列当作类别处理(对 pandas category 列使用 categorical_features="from_dtype")。
One-Hot Encoding — The Default for Nominal Columns 🔳One-Hot Encoding — Nominal Columns का Default 🔳独热编码 —— 名义列的默认选择 🔳
One-hot creates one 0/1 column per value; each row has a single 1. No order, no fake distances — every city gets its own coefficient. Use OneHotEncoder fitted on training data in real projects; pd.get_dummies is fine for a quick look but does not remember the categories.
One-hot हर मान का एक 0/1 column बनाता है; हर row में बस एक 1 होता है। कोई क्रम नहीं, कोई नक़ली दूरी नहीं — हर city को अपना coefficient मिलता है। असली projects में training data पर fit किया OneHotEncoder इस्तेमाल करें; pd.get_dummies जल्दी देखने के लिए ठीक है पर categories याद नहीं रखता।
独热编码为每个取值创建一列 0/1,每行只有一个 1。没有顺序,没有虚假距离 —— 每个城市有自己的系数。正式项目里请用在训练数据上拟合的 OneHotEncoder;pd.get_dummies 适合快速查看,但它不会记住类别。
# 3) One-hot encoding — and three traps
from sklearn.preprocessing import OneHotEncoder
a = pd.get_dummies(train[["pincode"]]); b = pd.get_dummies(test[["pincode"]])
print(f"get_dummies on train: {a.shape[1]} columns, on test: {b.shape[1]} columns ← they do not line up")
ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=False).fit(train[["city"]])
print("city →", len(ohe.get_feature_names_out()), "columns:", list(ohe.get_feature_names_out())[:3], "...")
trap = OneHotEncoder(drop="first", handle_unknown="ignore", sparse_output=False).fit(train[["city"]])
new = pd.DataFrame({"city": ["Bengaluru", "Lucknow"]}) # Lucknow never seen
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
print("drop='first' + unknown city:", trap.transform(new).astype(int).tolist(), "← both rows look like Bengaluru")
rare = OneHotEncoder(min_frequency=20, handle_unknown="infrequent_if_exist").fit(train[["pincode"]])
print("pincode one-hot:", OneHotEncoder(handle_unknown="ignore").fit(train[["pincode"]]).get_feature_names_out().size,
"columns → with min_frequency=20:", rare.get_feature_names_out().size, "(rare ones pooled)")
Five rows. Toggle drop="first", then add a city the encoder has never seen.
पाँच rows। drop="first" बदलें, फिर ऐसी city जोड़ें जो encoder ने कभी नहीं देखी।
五行数据。切换 drop="first",再加入一个编码器从未见过的城市。
With N one-hot columns plus an intercept, the columns always add up to 1, so an unregularised linear regression cannot find unique coefficients. That is the only case that needs drop="first". sklearn's LogisticRegression uses L2 regularisation by default, and trees do not care, so both can keep all N columns. Avoid drop="first" together with handle_unknown="ignore": an unseen value then looks exactly like the dropped reference. drop="if_binary" is a good middle way — it turns a two-value column into one column.
N one-hot columns और intercept के साथ columns का जोड़ हमेशा 1 होता है, इसलिए बिना regularisation वाला linear regression अनोखे coefficients नहीं ढूँढ सकता। सिर्फ़ इसी स्थिति में drop="first" चाहिए। sklearn का LogisticRegression default रूप से L2 regularisation इस्तेमाल करता है, और trees को फ़र्क़ नहीं पड़ता, इसलिए दोनों सभी N columns रख सकते हैं। drop="first" को handle_unknown="ignore" के साथ न इस्तेमाल करें: तब अनजाना मान हटाए गए reference जैसा ही दिखता है। drop="if_binary" अच्छा बीच का रास्ता है — यह दो-मान वाले column को एक column बनाता है।
N 个独热列加上截距项时,这些列之和恒为 1,所以无正则化的线性回归无法求出唯一的系数。只有这种情况才需要 drop="first"。sklearn 的 LogisticRegression 默认使用 L2 正则化,树模型也不受影响,所以两者都可以保留全部 N 列。不要把 drop="first" 和 handle_unknown="ignore" 一起用:那样未知取值看起来和被删除的参考类别完全一样。drop="if_binary" 是不错的折中 —— 它把只有两个取值的列变成一列。
min_frequency=20 keeps the 46 pincodes with at least 20 training rows and pools the other 534 into one "infrequent" column.
min_frequency=20 कम-से-कम 20 training rows वाले 46 pincodes रखता है और बाक़ी 534 को एक "infrequent" column में मिला देता है।
min_frequency=20 保留训练行数不少于 20 的 46 个邮编,其余 534 个合并成一个 “infrequent” 列。
Ordinal Encoding — When Order Is Real 📶Ordinal Encoding — जब क्रम असली हो 📶有序编码 —— 当顺序真实存在时 📶
For income bracket, education level or clothing size, order carries information: one coefficient can say "each step up lowers churn". But the order must come from you. Left alone, OrdinalEncoder also sorts alphabetically: High, Low, Medium, Very High.
Income bracket, शिक्षा स्तर या कपड़ों के size में क्रम जानकारी देता है: एक coefficient कह सकता है "हर क़दम ऊपर churn घटाता है"। पर क्रम आपसे आना चाहिए। अपने-आप छोड़ने पर OrdinalEncoder भी वर्णमाला क्रम लगाता है: High, Low, Medium, Very High।
对于收入档、学历或衣服尺码,顺序本身就是信息:一个系数就能表达“每升一级,流失率下降”。但顺序必须由你提供。不指定时,OrdinalEncoder 也会按字母排序:High、Low、Medium、Very High。
# 4) Ordinal encoding: give the order yourself
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
order = [["Low", "Medium", "High", "Very High"]]
right = OrdinalEncoder(categories=order).fit(train[["income_bracket"]])
alpha = OrdinalEncoder().fit(train[["income_bracket"]]) # alphabetical
print("explicit :", dict(zip(right.categories_[0], range(4))))
print("alphabetical:", dict(zip(alpha.categories_[0], range(4))))
for name, enc in [("explicit order", right), ("alphabetical", alpha)]:
m = LogisticRegression().fit(enc.transform(train[["income_bracket"]]), train["churn"])
auc = roc_auc_score(test["churn"], m.predict_proba(enc.transform(test[["income_bracket"]]))[:, 1])
print(f"logistic regression on income only, {name:<14}: test AUC {auc:.3f}")
Four income brackets with their real churn rates. Switch the order and watch the codes move.
चार income brackets उनकी असली churn दरों के साथ। क्रम बदलें और codes को खिसकते देखें।
四个收入档及其真实流失率。切换顺序,观察编码如何移动。
New data will contain values the encoder never saw. Use OneHotEncoder(handle_unknown="ignore") (all zeros, with a warning) or "infrequent_if_exist", and OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1). Count how often this happens: a sudden rise in unknown values usually means the data source changed.
नए data में ऐसे मान होंगे जो encoder ने कभी नहीं देखे। OneHotEncoder(handle_unknown="ignore") (सब शून्य, चेतावनी के साथ) या "infrequent_if_exist", और OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1) इस्तेमाल करें। गिनें कि ऐसा कितनी बार होता है: अनजाने मानों में अचानक बढ़त का मतलब आम तौर पर data source का बदलना है।
新数据中一定会出现编码器从未见过的取值。使用 OneHotEncoder(handle_unknown="ignore")(全零,并给出警告)或 "infrequent_if_exist",以及 OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1)。统计这种情况的频率:未知取值突然增多,通常意味着数据源变了。
Target Encoding — Powerful, and Easy to Get Wrong 🎯Target Encoding — ताक़तवर, और ग़लत करना आसान 🎯目标编码 —— 强大,但很容易出错 🎯
For a column with hundreds of values, target encoding replaces each value with the average target for that value — one column instead of hundreds. The danger: if a row's own target is part of its average, the feature "contains the answer". Small categories are worst, and real data is full of them.
सैकड़ों मानों वाले column के लिए target encoding हर मान को उस मान के औसत target से बदलता है — सैकड़ों की जगह एक column। ख़तरा: अगर किसी row का अपना target उसके औसत में शामिल है, तो feature में "जवाब छिपा" होता है। छोटी categories सबसे बुरी हैं, और असली data उनसे भरा होता है।
对于有几百个取值的列,目标编码用该取值对应的目标均值替换它 —— 一列代替几百列。危险在于:如果某行自己的目标值参与了它的均值计算,这个特征就“包含了答案”。小类别最危险,而真实数据里到处都是小类别。
# 5) Target encoding for pincode (600 values) — naive vs cross-fitted
from sklearn.model_selection import KFold
from sklearn.preprocessing import TargetEncoder
import category_encoders as ce
def auc_pair(tr_enc, te_enc):
return roc_auc_score(train["churn"], tr_enc), roc_auc_score(test["churn"], te_enc)
means = train.groupby("pincode")["churn"].mean(); g = train["churn"].mean()
naive = auc_pair(train["pincode"].map(means), test["pincode"].map(means).fillna(g))
with warnings.catch_warnings():
warnings.simplefilter("ignore")
cet = ce.TargetEncoder(cols=["pincode"], smoothing=10).fit(train[["pincode"]], train["churn"])
smooth = auc_pair(cet.transform(train[["pincode"]])["pincode"], cet.transform(test[["pincode"]])["pincode"])
skl = TargetEncoder(cv=KFold(5, shuffle=True, random_state=0))
cross = auc_pair(skl.fit_transform(train[["pincode"]], train["churn"]).ravel(), skl.transform(test[["pincode"]]).ravel())
for name, (a, b) in [("naive group mean", naive), ("smoothing only (ce)", smooth), ("cross-fitted (sklearn)", cross)]:
print(f"{name:<24} AUC train {a:.3f} test {b:.3f} gap {a - b:+.3f}")
A train score far above the test score means leakage. Smoothing (category_encoders) narrows the gap but still over-promises on train. sklearn's TargetEncoder cross-fits inside fit_transform, so its train score is honest.
Train score का test score से बहुत ऊपर होना leakage है। Smoothing (category_encoders) अंतर घटाता है पर train पर फिर भी ज़्यादा वादा करता है। sklearn का TargetEncoder fit_transform के अंदर cross-fit करता है, इसलिए उसका train score ईमानदार है।
训练得分远高于测试得分就意味着泄漏。平滑(category_encoders)缩小了差距,但在训练集上仍然过于乐观。sklearn 的 TargetEncoder 在 fit_transform 内部做交叉拟合,所以它的训练得分是诚实的。
The same train/test split as above. Choose a method and the smoothing strength m.
ऊपर वाला वही train/test split। तरीका और smoothing की ताक़त m चुनें।
与上面相同的训练/测试划分。选择方法和平滑强度 m。
A popular claim is that category_encoders.TargetEncoder "handles leakage automatically with smoothing". It does not: smoothing shrinks small categories toward the overall mean, but a row's own target is still inside its encoding. Use sklearn.preprocessing.TargetEncoder (cross-fitting in fit_transform), a K-fold function like the one in the original lesson, or fit the encoder on a separate slice of data.
एक लोकप्रिय दावा है कि category_encoders.TargetEncoder "smoothing से leakage अपने-आप संभालता है"। ऐसा नहीं है: smoothing छोटी categories को कुल mean की ओर खींचता है, पर row का अपना target अब भी उसकी encoding में है। sklearn.preprocessing.TargetEncoder (fit_transform में cross-fitting), K-fold function, या data के अलग हिस्से पर fit किया encoder इस्तेमाल करें।
一种流行的说法是 category_encoders.TargetEncoder“用平滑自动处理泄漏”。并非如此:平滑把小类别拉向总体均值,但每行自己的目标值仍在它的编码里。请使用 sklearn.preprocessing.TargetEncoder(在 fit_transform 中交叉拟合)、K 折函数,或在单独的一部分数据上拟合编码器。
Frequency, Binary and Hashing — Other High-Cardinality Tools 📊Frequency, Binary और Hashing — High-Cardinality के दूसरे Tools 📊频率、二进制与哈希 —— 处理高基数的其他工具 📊
Frequency (or count) encoding uses no target, so it cannot leak — but it only says how common a value is, and two different pincodes with the same count become identical. Learn the counts on the training data and map unseen values to 0. Hashing squeezes any number of values into a fixed number of columns; the price is collisions.
Frequency (या count) encoding target इस्तेमाल नहीं करता, इसलिए leak नहीं कर सकता — पर यह बस बताता है कि मान कितना आम है, और बराबर गिनती वाले दो अलग pincodes एक जैसे हो जाते हैं। गिनती training data पर सीखें और अनजाने मानों को 0 दें। Hashing कितने भी मानों को तय संख्या के columns में समेटता है; क़ीमत है collisions।
频率(或计数)编码不使用目标,所以不会泄漏 —— 但它只说明某个取值有多常见,两个出现次数相同的不同邮编会变得一模一样。在训练数据上学习计数,未见过的取值映射为 0。哈希把任意数量的取值压进固定数量的列,代价是冲突。
# 6) Frequency / count encoding, and hashing for huge columns
freq = train["pincode"].value_counts(normalize=True) # learned on TRAIN only
test_freq = test["pincode"].map(freq).fillna(0) # unseen → 0
print("most common pincodes:", freq.head(3).round(4).to_dict())
print("test rows with an unseen pincode:", int((test_freq == 0).sum()))
from sklearn.feature_extraction import FeatureHasher
h = FeatureHasher(n_features=64, input_type="string")
codes = h.transform([[p] for p in train["pincode"].unique()])
used = len({tuple(r.indices) for r in codes})
print(f"hashing: {train['pincode'].nunique()} pincodes → {used} distinct columns used of 64 "
f"(≈{train['pincode'].nunique() / used:.0f} pincodes share each column: collisions)")
Slide the number of categories and compare the columns each encoding needs.
Categories की संख्या खिसकाएँ और हर encoding के लिए ज़रूरी columns की तुलना करें।
拖动类别数量,比较各种编码需要的列数。
Side-by-Side: Same Data, Different Encodings ⚖️साथ-साथ: वही Data, अलग Encodings ⚖️并排比较:相同数据,不同编码 ⚖️
Here city and pincode are encoded three ways (income always uses the explicit order), and each version is fed to a linear and a tree model.
यहाँ city और pincode को तीन तरह encode किया गया है (income हमेशा explicit क्रम में), और हर version को एक linear और एक tree model को दिया गया है।
这里城市和邮编用三种方式编码(收入档始终用明确顺序),每个版本分别输入一个线性模型和一个树模型。
# 7) Same data, same models, different encodings for city + pincode
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import HistGradientBoostingClassifier
X_cols = ["city", "pincode", "income_bracket", "tenure_months"]
inc = OrdinalEncoder(categories=order)
def prep(city_enc, pin_enc):
return ColumnTransformer([("city", city_enc, ["city"]), ("pin", pin_enc, ["pincode"]),
("inc", inc, ["income_bracket"]), ("num", "passthrough", ["tenure_months"])])
setups = {
"label (alphabetical)": (OrdinalEncoder(), OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1)),
"one-hot": (OneHotEncoder(handle_unknown="ignore", sparse_output=False),
OneHotEncoder(handle_unknown="ignore", min_frequency=20, sparse_output=False)),
"target (cross-fitted)": (TargetEncoder(cv=KFold(5, shuffle=True, random_state=0)), TargetEncoder(cv=KFold(5, shuffle=True, random_state=0))),
}
rows = []
for name, (ce_, pe) in setups.items():
for mname, model in [("logistic", LogisticRegression(max_iter=2000)), ("boosting", HistGradientBoostingClassifier(random_state=0))]:
steps = [prep(ce_, pe)] + ([StandardScaler(with_mean=False)] if mname == "logistic" else []) + [model]
p = make_pipeline(*steps).fit(train[X_cols], train["churn"])
rows.append((name, mname, roc_auc_score(test["churn"], p.predict_proba(test[X_cols])[:, 1])))
res = pd.DataFrame(rows, columns=["encoding", "model", "AUC"]).pivot(index="encoding", columns="model", values="AUC")
print(res.round(3))
Alphabetical codes cost logistic regression about 0.10 AUC; boosting barely notices. Here one-hot slightly beats target encoding because pincode effects are noisy and many pincodes are tiny — the best choice depends on your data, so measure it.
वर्णमाला codes से logistic regression का लगभग 0.10 AUC जाता है; boosting को मुश्किल से फ़र्क़ पड़ता है। यहाँ one-hot, target encoding से थोड़ा बेहतर है क्योंकि pincode के असर शोर भरे हैं और कई pincodes बहुत छोटे हैं — सबसे अच्छा चुनाव आपके data पर निर्भर है, इसलिए मापें।
字母顺序编码让逻辑回归损失约 0.10 的 AUC;梯度提升几乎不受影响。这里独热略胜目标编码,因为邮编效应噪声大、许多邮编样本极少 —— 最佳选择取决于你的数据,所以要实际测量。
| Methodतरीका方法 | ColumnsColumns列数 | Many values?कई मान?取值很多? | Leakage riskLeakage जोखिम泄漏风险 | Best forकिसके लिए अच्छा适用于 |
|---|---|---|---|---|
| LabelEncoder | 1 | — | Noneकोई नहीं无 | the target ytarget y目标 y |
| OrdinalEncoder | 1 | Yesहाँ是 | Noneकोई नहीं无 | real orders; tree modelsअसली क्रम; tree models真实顺序;树模型 |
| OneHotEncoder | N (or fewer with min_frequency) | With min_frequency / sparsemin_frequency / sparse के साथ配合 min_frequency / 稀疏输出 | Noneकोई नहीं无 | nominal columns, linear modelsnominal columns, linear models名义列、线性模型 |
| TargetEncoder | 1 | Yesहाँ是 | High unless cross-fittedCross-fit न करें तो ज़्यादा不交叉拟合则很高 | many values with a real signalअसली संकेत वाले कई मान取值很多且有真实信号 |
| Frequency / countFrequency / count频率 / 计数 | 1 | Yesहाँ是 | Noneकोई नहीं无 | when popularity itself mattersजब लोकप्रियता ख़ुद मायने रखे当流行程度本身有意义时 |
| Binary / hashingBinary / hashing二进制 / 哈希 | log₂(N) · fixed | Yesहाँ是 | None (collisions)कोई नहीं (collisions)无(但有冲突) | very many values, limited memoryबहुत सारे मान, कम memory取值极多、内存有限 |
A Complete Encoding Pipeline 🔗पूरी Encoding Pipeline 🔗完整的编码管道 🔗
Put the right encoder on each group of columns with ColumnTransformer, chain it with the model in a Pipeline, and every category list, order and target mean is learned from training data only — even inside cross-validation.
ColumnTransformer से columns के हर समूह पर सही encoder लगाएँ, Pipeline में model से जोड़ें, और हर category list, क्रम और target mean सिर्फ़ training data से सीखा जाता है — cross-validation के अंदर भी।
用 ColumnTransformer 给每组列配上合适的编码器,再用 Pipeline 与模型串联,所有类别列表、顺序和目标均值都只从训练数据中学习 —— 即使在交叉验证内部也是如此。
# 8) A production pipeline: right encoder for each column type
import joblib
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.model_selection import cross_val_score
pre = ColumnTransformer([
("nominal", Pipeline([("impute", SimpleImputer(strategy="most_frequent")),
("ohe", OneHotEncoder(handle_unknown="ignore", drop="if_binary"))]), ["city", "gender", "plan"]),
("ordinal", OrdinalEncoder(categories=order, handle_unknown="use_encoded_value", unknown_value=-1), ["income_bracket"]),
("high_card", TargetEncoder(cv=KFold(5, shuffle=True, random_state=0)), ["pincode"]),
("numeric", StandardScaler(), ["tenure_months"]),
])
pipe = Pipeline([("prep", pre), ("model", LogisticRegression(max_iter=2000))])
cols = ["city", "gender", "plan", "income_bracket", "pincode", "tenure_months"]
cv = cross_val_score(pipe, train[cols], train["churn"], cv=5, scoring="roc_auc")
pipe.fit(train[cols], train["churn"])
print(f"5-fold CV AUC {cv.mean():.3f} ± {cv.std():.3f} | test AUC {roc_auc_score(test['churn'], pipe.predict_proba(test[cols])[:, 1]):.3f}")
print("columns after encoding:", pipe["prep"].get_feature_names_out().size)
joblib.dump(pipe, "churn_pipeline.joblib")
new = pd.DataFrame([{"city": "Lucknow", "gender": "Male", "plan": "Basic",
"income_bracket": "Low", "pincode": "PIN999999", "tenure_months": 6}])
with warnings.catch_warnings(record=True) as w: # unknown city → all zeros, with a UserWarning
warnings.simplefilter("always")
p = joblib.load("churn_pipeline.joblib").predict_proba(new)[:, 1]
print(f"unseen city + pincode still predicts: {p[0]:.3f} | warnings raised: {len(w)} ({w[0].category.__name__})")
Make the Practice Data 🐍Practice Data बनाएँ 🐍生成练习数据 🐍
Run this first. City has an up-and-down effect in alphabetical order (so alphabetical codes carry almost no linear signal); income has a real order; pincode has 600 values with their own effects and a long tail of tiny ones; gender has no effect at all.
इसे पहले चलाएँ। City का असर वर्णमाला क्रम में ऊपर-नीचे है (इसलिए वर्णमाला codes में लगभग कोई linear संकेत नहीं); income में असली क्रम है; pincode के 600 मान हैं, सबका अपना असर, और छोटे pincodes की लंबी tail; gender का कोई असर नहीं।
请先运行它。城市的影响按字母顺序忽高忽低(所以字母编码几乎不含线性信号);收入有真实顺序;邮编有 600 个取值,各有影响,并有大量样本极少的长尾;性别完全没有影响。
# 0) Practice data: customer churn with four kinds of categorical column
import numpy as np, pandas as pd
rng = np.random.default_rng(8)
n = 8000
cities = ["Bengaluru", "Chennai", "Delhi", "Hyderabad", "Jaipur", "Kolkata", "Mumbai", "Pune"]
city_eff = dict(zip(cities, [-0.7, 0.8, -0.6, 0.7, -0.5, 0.6, -0.4, 0.5])) # up-down-up: no order at all
city = rng.choice(cities, n)
income = rng.choice(["Low", "Medium", "High", "Very High"], n, p=[.3, .35, .25, .1])
inc_eff = {"Low": 0.9, "Medium": 0.3, "High": -0.3, "Very High": -0.9} # a real order
pins = np.array([f"PIN{400000 + k * 37}" for k in range(600)])
pin_w = 1 / np.arange(1, 601) ** 0.9; pin_w /= pin_w.sum() # a few big pincodes, many tiny ones
pin_eff = dict(zip(pins, rng.normal(0, 0.8, 600)))
pincode = rng.choice(pins, n, p=pin_w)
gender = rng.choice(["Female", "Male"], n) # binary, no effect
plan = rng.choice(["Basic", "Plus", "Premium"], n, p=[.5, .35, .15])
tenure = rng.integers(1, 61, n)
z = (np.vectorize(city_eff.get)(city) + np.vectorize(inc_eff.get)(income) + np.vectorize(pin_eff.get)(pincode)
- 0.025 * tenure - 0.3 + rng.normal(0, 0.3, n))
churn = (rng.random(n) < 1 / (1 + np.exp(-z))).astype(int)
pd.DataFrame({"city": city, "income_bracket": income, "pincode": pincode, "gender": gender, "plan": plan,
"tenure_months": tenure, "churn": churn}).to_csv("customers.csv", index=False)
print(f"customers.csv: {n:,} rows, churn rate {churn.mean():.1%}")
Golden Rules of Categorical Encoding 🏆Categorical Encoding के सुनहरे नियम 🏆类别编码黄金法则 🏆
LabelEncoder only for the target. For features use OrdinalEncoder or OneHotEncoder.LabelEncoder सिर्फ़ target के लिए इस्तेमाल करें। Features के लिए OrdinalEncoder या OneHotEncoder।LabelEncoder 只用于目标。特征请用 OrdinalEncoder 或 OneHotEncoder。OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]]).OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]])।OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]])。handle_unknown="ignore" or "infrequent_if_exist", and do not combine drop="first" with ignore.handle_unknown="ignore" या "infrequent_if_exist", और drop="first" को ignore के साथ न मिलाएँ।handle_unknown="ignore" 或 "infrequent_if_exist",且不要把 drop="first" 与 ignore 一起用。drop="if_binary").drop="if_binary")।drop="if_binary")。min_frequency, cross-fitted TargetEncoder, frequency or hashing — and compare them with cross-validation; "15 values" is only a rule of thumb.min_frequency वाला one-hot, cross-fitted TargetEncoder, frequency या hashing आज़माएँ — और cross-validation से तुलना करें; "15 मान" बस एक rule of thumb है।min_frequency 的独热、交叉拟合的 TargetEncoder、频率或哈希 —— 并用交叉验证比较;“15 个取值”只是经验法则。Pipeline + ColumnTransformer, and save the whole pipeline.Pipeline + ColumnTransformer के अंदर training data पर fit करें, और पूरी pipeline save करें।Pipeline + ColumnTransformer,在训练数据上拟合,并保存整条管道。Encoding decides which relationships a model is allowed to learn. On the same data, alphabetical codes reduced city to noise (AUC 0.515) while one-hot recovered it (0.610); an explicit order turned income from useless (0.503) to useful (0.640); and naive target encoding promised far more on training data than it delivered. Match the encoder to the column, cross-fit anything that uses the target, and let a pipeline keep it consistent.
Encoding तय करता है कि model कौन से संबंध सीख सकता है। उसी data पर वर्णमाला codes ने city को शोर बना दिया (AUC 0.515) जबकि one-hot ने उसे वापस पाया (0.610); explicit क्रम ने income को बेकार (0.503) से काम का (0.640) बनाया; और naive target encoding ने training data पर जितना वादा किया उससे बहुत कम दिया। Encoder को column से मिलाएँ, target इस्तेमाल करने वाली हर चीज़ को cross-fit करें, और pipeline से सब एक जैसा रखें।
编码决定了模型被允许学习哪些关系。在同一份数据上,字母顺序编码让城市变成噪声(AUC 0.515),独热编码则找回了它(0.610);明确的顺序让收入档从无用(0.503)变得有用(0.640);而朴素目标编码在训练数据上承诺的远多于它实际能交付的。让编码器匹配列的类型,凡是用到目标的都要交叉拟合,并让管道保持一致。