Data Preparation / Data Preprocessing 📂 Data Collection · 8 of 13 220 min read

Encoding Categorical Variables

A story-driven, comprehensive guide to converting categorical text data into numeric form using Label Encoding, One-Hot Encoding, Ordinal Encoding, and advanced techniques — with live diagrams, before/after tables, real-world stories, and complete reusable code.

🌐
SectionSection第 01 节

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 的两倍”。

The City Column That Lost Its Signalवह City Column जिसका संकेत खो गया丢失了信号的城市列
In our churn data (Section 9), city matters a lot — Chennai, Hyderabad, Kolkata and Pune customers leave far more often — but the effect goes up and down, not in any alphabetical order. A logistic regression on city alone scores test AUC 0.515 with alphabetical codes (almost a coin flip) and 0.610 with one-hot columns. Same data, same model — only the encoding changed.
हमारे churn data (Section 9) में city बहुत मायने रखती है — Chennai, Hyderabad, Kolkata और Pune के ग्राहक कहीं ज़्यादा छोड़ते हैं — पर असर ऊपर-नीचे होता है, किसी वर्णमाला क्रम में नहीं। सिर्फ़ city पर logistic regression, वर्णमाला codes के साथ test AUC 0.515 (लगभग सिक्का उछालना) और one-hot columns के साथ 0.610 देता है। वही data, वही model — सिर्फ़ encoding बदली।
在我们的流失数据(第 9 节)中,城市影响很大 —— Chennai、Hyderabad、Kolkata 和 Pune 的客户流失多得多 —— 但影响忽高忽低,与字母顺序毫无关系。只用城市的逻辑回归,用字母顺序编码时测试 AUC 为 0.515(几乎是抛硬币),用独热编码时为 0.610。数据相同,模型相同 —— 只是编码不同。
LIVELIVE互动A False Number Lineनक़ली Number Line虚假的数轴

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 चाहिए:

每种列需要各自的编码器:

Label 🔢Label 🔢标签 🔢
LabelEncoder
Integers in alphabetical order, for one 1-D array. Made for the target y. For features, use OrdinalEncoder.
वर्णमाला क्रम में integers, एक 1-D array के लिए। Target y के लिए बना। Features के लिए OrdinalEncoder इस्तेमाल करें।
按字母顺序分配整数,作用于一维数组。专为目标 y 设计。特征请用 OrdinalEncoder。
One-hot 🔳One-hot 🔳独热 🔳
OneHotEncoder
One 0/1 column per value. No order implied. The default for nominal columns with a modest number of values.
हर मान का एक 0/1 column। कोई क्रम नहीं। ठीक-ठाक संख्या के मानों वाले nominal columns के लिए default।
每个取值一列 0/1。不暗示顺序。取值不太多的名义列的默认选择。
Ordinal 📶Ordinal 📶有序 📶
OrdinalEncoder(categories=[…])
Integers that follow an order you give. For real orders (Low < Medium < High), and fine for tree models.
वे integers जो आपके दिए क्रम पर चलें। असली क्रम (Low < Medium < High) के लिए, और tree models के लिए ठीक।
按你给定顺序分配的整数。适用于真实顺序(Low < Medium < High),树模型也可用。
Target 🎯Target 🎯目标 🎯
TargetEncoder (sklearn)
Replaces each value with a smoothed mean of the target. One column, strong signal — must be cross-fitted.
हर मान को target के smoothed mean से बदलता है। एक column, मज़बूत संकेत — cross-fit करना ज़रूरी।
用目标的平滑均值替换每个取值。一列、信号强 —— 必须交叉拟合。
Frequency 📊Frequency 📊频率 📊
value_counts(normalize=True)
Replaces each value with its share of rows. Uses no target. Different values with the same count collide.
हर मान को rows में उसके हिस्से से बदलता है। Target नहीं इस्तेमाल करता। बराबर गिनती वाले मान टकराते हैं।
用该取值所占行比例替换。不使用目标。出现次数相同的不同取值会冲突。
Binary / hashing 🔗Binary / hashing 🔗二进制 / 哈希 🔗
log₂(N) cols · FeatureHasher
Pack many values into few columns. Binary codes imply odd similarities; hashing has collisions.
कई मानों को कम columns में समेटते हैं। Binary codes अजीब समानताएँ बताते हैं; hashing में collisions होते हैं।
把大量取值压进少数几列。二进制码会暗示奇怪的相似性;哈希会有冲突。
🧭 PICTURE 1 — WHICH ENCODER?तस्वीर 1 — कौन सा ENCODER?图 1 — 选哪个编码器?
Choosing an encoder Is it the target y?क्या यह target y है?它是目标 y 吗? yesहाँ是 noनहीं否 LabelEncoder Do the values have a real order?क्या मानों में असली क्रम है?取值有真实顺序吗? yesहाँ是 noनहीं否 OrdinalEncoder with YOUR orderआपके क्रम के साथ OrdinalEncoder按你指定顺序的 OrdinalEncoder About 15 values or fewer?लगभग 15 या कम मान?大约 15 个或更少的取值? yesहाँ是 noनहीं否 OneHotEncoder Many values (city, pincode, SKU)कई मान (city, pincode, SKU)取值很多(城市、邮编、SKU) TargetEncoder (cross-fitted) · frequency · OHE with min_frequency · hashingTargetEncoder (cross-fitted) · frequency · min_frequency वाला OHE · hashingTargetEncoder(交叉拟合)· 频率 · 带 min_frequency 的独热 · 哈希 Tree models: OrdinalEncoder is fine for nominal columns too (or HistGradientBoosting with categorical_features="from_dtype").Tree models: nominal columns के लिए भी OrdinalEncoder ठीक है (या categorical_features="from_dtype" के साथ HistGradientBoosting)।树模型:名义列也可以用 OrdinalEncoder(或使用 categorical_features="from_dtype" 的 HistGradientBoosting)。
# 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"])))
OUTPUT
city 8 income_bracket 4 pincode 592 gender 2 plan 3 pincode rows: top 5 = 2090 | pincodes with < 5 rows: 253 pincodes in test but not in train: 12

SectionSection第 02 节

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_])
OUTPUT
{'Bengaluru': 0, 'Chennai': 1, 'Delhi': 2, 'Hyderabad': 3, 'Jaipur': 4, 'Kolkata': 5, 'Mumbai': 6, 'Pune': 7} churn rate by code: [0.25, 0.5, 0.28, 0.48, 0.26, 0.43, 0.29, 0.43] decoding gender with the LAST encoder: ['Plus', 'Plus', 'Plus'] ← should be ['Male', 'Male', 'Male'] OrdinalEncoder categories: [['Female', 'Male'], ['Basic', 'Plus', 'Premium']]
🐛
The Reused-Encoder Bugदोबारा इस्तेमाल किए Encoder का Bug复用编码器的错误

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 处理所有特征列。

🌳
Trees Are More ForgivingTrees ज़्यादा माफ़ करने वाले हैं树模型更宽容

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")。


SectionSection第 03 节

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)")
OUTPUT
get_dummies on train: 580 columns, on test: 434 columns ← they do not line up city → 8 columns: ['city_Bengaluru', 'city_Chennai', 'city_Delhi'] ... drop='first' + unknown city: [[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]] ← both rows look like Bengaluru pincode one-hot: 580 columns → with min_frequency=20: 47 (rare ones pooled)
LIVELIVE互动One-Hot Options — drop and Unknown ValuesOne-Hot Options — drop और अनजाने मान独热选项 —— drop 与未知值

Five rows. Toggle drop="first", then add a city the encoder has never seen.

पाँच rows। drop="first" बदलें, फिर ऐसी city जोड़ें जो encoder ने कभी नहीं देखी।

五行数据。切换 drop="first",再加入一个编码器从未见过的城市。

drop
unknown

🪤
The Dummy Trap — Who Really Needs drop?Dummy Trap — drop की असल ज़रूरत किसे है?虚拟变量陷阱 —— 谁真正需要 drop?

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" 是不错的折中 —— 它把只有两个取值的列变成一列。

📐 PICTURE 2 — REAL OUTPUT: HOW MANY COLUMNS EACH COLUMN BECOMESतस्वीर 2 — असली OUTPUT: हर COLUMN कितने COLUMNS बनता है图 2 — 真实输出:每列会变成多少列
Columns created by each encoding 1 10 100 580 genderif_binary 2 (plain one-hot) 1 plan 3 (plain one-hot) 3 city 8 (plain one-hot) 8 pincodemin_frequency=20 580 (plain one-hot) 47 pincodeTargetEncoder 580 (plain one-hot) 1 columns on a log scale · red: plain one-hot · green: with the option showncolumns log scale पर · लाल: सादा one-hot · हरा: दिखाए option के साथ列数(对数刻度)· 红:普通独热 · 绿:使用所示选项

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” 列。


SectionSection第 04 节

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}")
OUTPUT
explicit : {'Low': 0, 'Medium': 1, 'High': 2, 'Very High': 3} alphabetical: {'High': 0, 'Low': 1, 'Medium': 2, 'Very High': 3} logistic regression on income only, explicit order: test AUC 0.640 logistic regression on income only, alphabetical : test AUC 0.503
LIVELIVE互动Explicit vs Alphabetical OrderExplicit बनाम वर्णमाला क्रम明确顺序 vs 字母顺序

Four income brackets with their real churn rates. Switch the order and watch the codes move.

चार income brackets उनकी असली churn दरों के साथ। क्रम बदलें और codes को खिसकते देखें।

四个收入档及其真实流失率。切换顺序,观察编码如何移动。

🧷
Unknown Values in ProductionProduction में अनजाने मान生产环境中的未知取值

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)。统计这种情况的频率:未知取值突然增多,通常意味着数据源变了。


SectionSection第 05 节

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 उनसे भरा होता है।

对于有几百个取值的列,目标编码用该取值对应的目标均值替换它 —— 一列代替几百列。危险在于:如果某行自己的目标值参与了它的均值计算,这个特征就“包含了答案”。小类别最危险,而真实数据里到处都是小类别。

📉 PICTURE 3 — REAL DATA: MANY PINCODES ARE TINYतस्वीर 3 — असली DATA: कई PINCODES बहुत छोटे हैं图 3 — 真实数据:许多邮编样本极少
Rows per pincode, sorted 1 10 100 309 pincodes have fewer than 5 rows309 pincodes में 5 से कम rows309 个邮编少于 5 行 their mean churn rate is mostly noiseउनकी औसत churn दर ज़्यादातर शोर है它们的平均流失率大多是噪声 580 training pincodes, from most to fewest rows (log scale)580 training pincodes, सबसे ज़्यादा से सबसे कम rows (log scale)580 个训练集邮编,按行数从多到少排列(对数刻度)
# 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}")
OUTPUT
naive group mean AUC train 0.754 test 0.651 gap +0.104 smoothing only (ce) AUC train 0.729 test 0.674 gap +0.056 cross-fitted (sklearn) AUC train 0.641 test 0.653 gap -0.012
🎯 PICTURE 4 — REAL OUTPUT: TRAIN VS TEST AUC FOR THREE TARGET ENCODERSतस्वीर 4 — असली OUTPUT: तीन TARGET ENCODERS के TRAIN बनाम TEST AUC图 4 — 真实输出:三种目标编码器的训练与测试 AUC
Target encoding: train vs test AUC 0.5 0.6 0.7 0.8 0.754train 0.651test naive group meannaive group mean朴素分组均值 0.729train 0.674test smoothing only (category_encoders)सिर्फ़ smoothing (category_encoders)仅平滑(category_encoders) 0.641train 0.653test cross-fitted (sklearn TargetEncoder)cross-fitted (sklearn TargetEncoder)交叉拟合(sklearn TargetEncoder)

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 内部做交叉拟合,所以它的训练得分是诚实的。

LIVELIVE互动Target-Encode 592 Real Pincodes592 असली Pincodes को Target-Encode करें对 592 个真实邮编做目标编码

The same train/test split as above. Choose a method and the smoothing strength m.

ऊपर वाला वही train/test split। तरीका और smoothing की ताक़त m चुनें।

与上面相同的训练/测试划分。选择方法和平滑强度 m。

⚠️
Smoothing Is Not Cross-FittingSmoothing, Cross-Fitting नहीं है平滑不等于交叉拟合

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 折函数,或在单独的一部分数据上拟合编码器。


SectionSection第 06 节

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)")
OUTPUT
most common pincodes: {'PIN400000': 0.1068, 'PIN400037': 0.0553, 'PIN400074': 0.038} test rows with an unseen pincode: 17 hashing: 580 pincodes → 64 distinct columns used of 64 (≈9 pincodes share each column: collisions)
LIVELIVE互动How Many Columns?कितने Columns?会有多少列?

Slide the number of categories and compare the columns each encoding needs.

Categories की संख्या खिसकाएँ और हर encoding के लिए ज़रूरी columns की तुलना करें।

拖动类别数量,比较各种编码需要的列数。


SectionSection第 07 节

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))
OUTPUT
model boosting logistic encoding label (alphabetical) 0.745 0.671 one-hot 0.752 0.770 target (cross-fitted) 0.750 0.759
⚖️ PICTURE 5 — REAL OUTPUT: TEST AUC BY ENCODING AND MODELतस्वीर 5 — असली OUTPUT: ENCODING और MODEL के हिसाब से TEST AUC图 5 — 真实输出:不同编码与模型的测试 AUC
Test AUC by encoding and model 0.60 0.65 0.70 0.75 0.80 0.671 0.745 label (alphabetical) 0.770 0.752 one-hot 0.759 0.750 target (cross-fitted) logistic regressionHistGradientBoosting

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किसके लिए अच्छा适用于
LabelEncoder1—Noneकोई नहीं无the target ytarget y目标 y
OrdinalEncoder1Yesहाँ是Noneकोई नहीं无real orders; tree modelsअसली क्रम; tree models真实顺序;树模型
OneHotEncoderN (or fewer with min_frequency)With min_frequency / sparsemin_frequency / sparse के साथ配合 min_frequency / 稀疏输出Noneकोई नहीं无nominal columns, linear modelsnominal columns, linear models名义列、线性模型
TargetEncoder1Yesहाँ是High unless cross-fittedCross-fit न करें तो ज़्यादा不交叉拟合则很高many values with a real signalअसली संकेत वाले कई मान取值很多且有真实信号
Frequency / countFrequency / count频率 / 计数1Yesहाँ是Noneकोई नहीं无when popularity itself mattersजब लोकप्रियता ख़ुद मायने रखे当流行程度本身有意义时
Binary / hashingBinary / hashing二进制 / 哈希log₂(N) · fixedYesहाँ是None (collisions)कोई नहीं (collisions)无(但有冲突)very many values, limited memoryबहुत सारे मान, कम memory取值极多、内存有限

SectionSection第 08 节

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__})")
OUTPUT
5-fold CV AUC 0.736 ± 0.012 | test AUC 0.758 columns after encoding: 15 unseen city + pincode still predicts: 0.638 | warnings raised: 1 (UserWarning)
🔗 PICTURE 6 — THE PIPELINE ABOVEतस्वीर 6 — ऊपर की PIPELINE图 6 — 上面的管道
Encoding pipeline X_trainraw columns city, gender, planOneHotEncoder(ignore, if_binary) income_bracketOrdinalEncoder(Low→Very High) pincodeTargetEncoder(cv=KFold(5)) tenure_monthsStandardScaler 15 columns →LogisticRegression fit() learns categories, orders, target means and scale from X_train onlyfit() categories, क्रम, target means और scale सिर्फ़ X_train से सीखता हैfit() 只从 X_train 学习类别、顺序、目标均值和缩放参数

SectionSection第 09 节

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%}")
OUTPUT
customers.csv: 8,000 rows, churn rate 36.4%

SectionSection第 10 节

Golden Rules of Categorical Encoding 🏆Categorical Encoding के सुनहरे नियम 🏆类别编码黄金法则 🏆

🎯 9 Rules Every Data Scientist Should Follow9 नियम जो हर Data Scientist को मानने चाहिए每位数据科学家都应遵守的 9 条法则
1
Decide the type of each column first: nominal, ordinal, binary, or high-cardinality. The type picks the encoder.
पहले हर column का प्रकार तय करें: nominal, ordinal, binary, या high-cardinality। प्रकार ही encoder चुनता है।
先判断每列的类型:名义、有序、二元还是高基数。类型决定编码器。
2
Use LabelEncoder only for the target. For features use OrdinalEncoder or OneHotEncoder.
LabelEncoder सिर्फ़ target के लिए इस्तेमाल करें। Features के लिए OrdinalEncoder या OneHotEncoder।
LabelEncoder 只用于目标。特征请用 OrdinalEncoder 或 OneHotEncoder。
3
Never give a linear, distance-based or neural model arbitrary integer codes for a nominal column — one-hot it.
Linear, दूरी-आधारित या neural model को nominal column के मनमाने integer codes कभी न दें — one-hot करें।
绝不要把名义列的任意整数编码交给线性模型、基于距离的模型或神经网络 —— 用独热编码。
4
Always write the order for ordinal columns: OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]]).
Ordinal columns का क्रम हमेशा लिखें: OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]])।
有序列一定要写明顺序:OrdinalEncoder(categories=[["Low", "Medium", "High", "Very High"]])。
5
Plan for unseen values: 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 一起用。
6
Drop a one-hot column only for unregularised linear models; otherwise keep all N (or use drop="if_binary").
One-hot column सिर्फ़ बिना regularisation वाले linear models के लिए हटाएँ; वरना सभी N रखें (या drop="if_binary")।
只有无正则化的线性模型才需要删掉一个独热列;否则保留全部 N 列(或用 drop="if_binary")。
7
For many values, try one-hot with 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 个取值”只是经验法则。
8
Never target-encode with means that include the row itself. Smoothing alone is not enough — cross-fit.
ऐसे means से कभी target-encode न करें जिनमें वही row शामिल हो। सिर्फ़ smoothing काफ़ी नहीं — cross-fit करें।
绝不用包含该行自身的均值做目标编码。仅靠平滑不够 —— 要交叉拟合。
9
Fit every encoder on training data inside a Pipeline + ColumnTransformer, and save the whole pipeline.
हर encoder को Pipeline + ColumnTransformer के अंदर training data पर fit करें, और पूरी pipeline save करें।
把每个编码器放进 Pipeline + ColumnTransformer,在训练数据上拟合,并保存整条管道。
🧮
Key Takeawayमुख्य सीख核心要点

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);而朴素目标编码在训练数据上承诺的远多于它实际能交付的。让编码器匹配列的类型,凡是用到目标的都要交叉拟合,并让管道保持一致。