The Dirty Data Problem 🧹गंदे Data की समस्या 🧹脏数据问题 🧹
Real datasets are rarely clean. Values are missing, rows are repeated, the same city is spelled five ways, and some numbers are impossible. Surveys of data scientists say a large share of their time — often quoted as 40–80% — goes into finding and fixing these problems before any model is trained.
असली datasets शायद ही कभी साफ़ होते हैं। मान ग़ायब होते हैं, rows दोहराई जाती हैं, एक ही city पाँच तरह से लिखी जाती है, और कुछ संख्याएँ असंभव होती हैं। Data scientists के surveys कहते हैं कि उनके समय का बड़ा हिस्सा — अक्सर 40–80% बताया जाता है — किसी model को train करने से पहले इन समस्याओं को ढूँढने और ठीक करने में जाता है।
真实数据集很少是干净的:有缺失值、重复行,同一个城市有五种写法,还有些数字根本不可能。对数据科学家的调查显示,他们很大一部分时间 —— 常见说法是 40–80% —— 花在训练模型之前发现并修复这些问题上。
A stronger algorithm does not fix dirty data. Missing values can bias averages, duplicates inflate counts and totals, and messy labels create fake categories. Every cleaning choice is an assumption about the world, so make it on purpose and write it down.
ज़्यादा ताक़तवर algorithm गंदे data को ठीक नहीं करता। ग़ायब मान औसत को biased कर सकते हैं, duplicates गिनती और जोड़ बढ़ाते हैं, और बिखरे labels नक़ली categories बनाते हैं। सफ़ाई का हर फ़ैसला दुनिया के बारे में एक मान्यता है, इसलिए इसे सोच-समझकर लें और लिख लें।
更强的算法修不好脏数据。缺失值会让平均数产生偏差,重复会抬高计数和总额,混乱的标签会制造假类别。每个清洗选择都是对现实的一个假设,所以要有意识地做,并记录下来。
.info(), .isnull(), .duplicated(), .value_counts() — measure every problem first..info(), .isnull(), .duplicated(), .value_counts() — पहले हर समस्या मापें।.info()、.isnull()、.duplicated()、.value_counts() —— 先量化每个问题。This tutorial uses one messy file, customers_raw.csv (Section 8 builds it). Its problems were planted on purpose, and we kept the complete version, so you can see which methods truly work.
यह tutorial एक बिखरी file, customers_raw.csv (Section 8 इसे बनाता है), इस्तेमाल करता है। इसकी समस्याएँ जानबूझकर डाली गईं, और हमने पूरा version रखा है, ताकि आप देख सकें कि कौन से तरीके सच में काम करते हैं।
本教程使用一个杂乱的文件 customers_raw.csv(第 8 节生成)。其中的问题是故意埋入的,我们还保留了完整版本,所以你能看到哪些方法真正有效。
Step 1 — Audit Before You Clean 🔍क़दम 1 — सफ़ाई से पहले Audit 🔍第 1 步 —— 先审查再清洗 🔍
Before you change a single value, measure the damage: how many values are missing in each column, how many rows are repeated, and how many spellings each label has.
एक भी मान बदलने से पहले नुक़सान मापें: हर column में कितने मान ग़ायब हैं, कितनी rows दोहराई गई हैं, और हर label की कितनी वर्तनियाँ हैं।
改动任何值之前,先量化问题:每列缺多少值、有多少重复行、每个标签有几种写法。
# 1) Audit before you clean
import pandas as pd, numpy as np
df_raw = pd.read_csv("customers_raw.csv", parse_dates=["order_date"])
df = df_raw.copy() # never edit the raw data
print(df.shape)
null_report = pd.DataFrame({
"missing": df.isnull().sum(),
"pct": (df.isnull().mean() * 100).round(1),
"dtype": df.dtypes.astype(str),
}).query("missing > 0").sort_values("pct", ascending=False)
print(null_report)
print(f"\nExact duplicate rows : {df.duplicated().sum()}")
print(f"Duplicate customer_id: {df['customer_id'].duplicated().sum()}")
print(f"City spellings : {df['city'].nunique()} (6 real cities)")
notes is almost empty (88%) — drop it. rating, monthly_income and age need a real strategy. city and is_returned have only a few gaps.
notes लगभग ख़ाली है (88%) — इसे हटाएँ। rating, monthly_income और age के लिए असली रणनीति चाहिए। city और is_returned में बस कुछ कमियाँ हैं।
notes 几乎全空(88%)—— 删掉。rating、monthly_income 和 age 需要认真选择策略。city 和 is_returned 只有少量缺失。
Why Is the Data Missing? 🤔Data ग़ायब क्यों है? 🤔数据为什么缺失?🤔
The reason a value is missing decides which fix is safe. Statisticians (Rubin, 1976) name three cases:
कोई मान क्यों ग़ायब है, यही तय करता है कि कौन सा सुधार सुरक्षित है। Statisticians (Rubin, 1976) तीन स्थितियाँ बताते हैं:
值为什么缺失,决定了哪种修复是安全的。统计学家(Rubin,1976)把它分成三种情况:
- Not linked to anything, seen or unseenकिसी चीज़ से नहीं जुड़ा, दिखने वाली या छिपी与任何变量(可见或不可见)都无关
- Example: a form field randomly failed to saveउदाहरण: form का field random तरीक़े से save नहीं हुआ例:表单字段随机保存失败
- Kept rows are a fair sampleबची rows निष्पक्ष sample हैं保留的行是公平样本
- Here: ageयहाँ: age本例:age
- Linked to other columns you can seeदूसरे दिखने वाले columns से जुड़ा与其他可见的列有关
- Example: slow deliveries are rated less oftenउदाहरण: धीमी deliveries को कम rating मिलती है例:配送慢的订单较少被评分
- Fill using those columns (by group, KNN, model)उन columns से भरें (group, KNN, model)利用这些列填充(分组、KNN、模型)
- Here: rating (depends on delivery_days)यहाँ: rating (delivery_days पर निर्भर)本例:rating(取决于 delivery_days)
- Linked to the missing value itselfख़ुद ग़ायब मान से जुड़ा与缺失值本身有关
- Example: high earners skip the income questionउदाहरण: ज़्यादा कमाने वाले income सवाल छोड़ते हैं例:高收入者不回答收入问题
- Any simple fill is biased; needs domain knowledgeहर सरल fill biased है; domain knowledge चाहिए任何简单填充都有偏;需要领域知识
- Here: monthly_incomeयहाँ: monthly_income本例:monthly_income
Left: missing ages are scattered everywhere — MCAR. Right: missing incomes sit at the top — the people who skipped the question earn the most. Dropping or median-filling them pulls the average down.
बाएँ: ग़ायब उम्रें हर जगह फैली हैं — MCAR। दाएँ: ग़ायब incomes ऊपर हैं — सवाल छोड़ने वाले सबसे ज़्यादा कमाते हैं। उन्हें हटाने या median से भरने से औसत नीचे खिंचता है।
左:缺失的年龄到处都有 —— MCAR。右:缺失的收入集中在高端 —— 不回答的人收入最高。删除或用中位数填充都会拉低平均值。
# 2) Why is it missing? Compare rows with and without a value
for col in ["age", "monthly_income", "rating"]:
miss = df[col].isnull()
print(f"{col:<15} missing {miss.mean():5.1%} | "
f"median purchase: missing ₹{df.loc[miss, 'purchase_amount'].median():>6,.0f} "
f"present ₹{df.loc[~miss, 'purchase_amount'].median():>6,.0f} | "
f"mean delivery_days: {df.loc[miss, 'delivery_days'].mean():.1f} vs {df.loc[~miss, 'delivery_days'].mean():.1f}")
Make a flag (1 = missing) and compare it with the other columns, as above. If nothing differs, MCAR is plausible (Little's MCAR test does this formally). If something differs — like delivery_days for rating, or purchase for income — the data is not MCAR. But data alone cannot separate MAR from MNAR: that needs the unseen values, so you need domain knowledge. Here, income-missingness linked to purchase is really MNAR, because income drives both.
एक flag बनाएँ (1 = ग़ायब) और ऊपर की तरह दूसरे columns से तुलना करें। अगर कुछ अलग नहीं, तो MCAR संभव है (Little का MCAR test यह औपचारिक रूप से करता है)। अगर कुछ अलग है — जैसे rating के लिए delivery_days, या income के लिए purchase — तो data MCAR नहीं है। पर सिर्फ़ data से MAR और MNAR को अलग नहीं किया जा सकता: इसके लिए छिपे मान चाहिए, इसलिए domain knowledge ज़रूरी है। यहाँ income का purchase से जुड़ा ग़ायब होना असल में MNAR है, क्योंकि income दोनों को चलाती है।
建一个标记(1 = 缺失),像上面那样与其他列比较。若没有差别,MCAR 是可能的(Little 的 MCAR 检验可以正式检验)。若有差别 —— 比如 rating 对应 delivery_days、income 对应 purchase —— 数据就不是 MCAR。但仅凭数据无法区分 MAR 和 MNAR:那需要看不到的值,所以必须依靠领域知识。本例中 income 的缺失与 purchase 相关,实际上是 MNAR,因为收入同时决定了两者。
300 customers. Choose why incomes go missing and how many. Red rings show the true values you lost.
300 ग्राहक। चुनें कि incomes क्यों और कितनी ग़ायब हों। लाल घेरे वे असली मान दिखाते हैं जो खो गए।
300 位顾客。选择收入缺失的原因和比例。红圈是你丢失的真实值。
Handling Missing Values — Strategy by Strategy 🛠️ग़ायब मान संभालना — एक-एक रणनीति 🛠️处理缺失值 —— 逐个策略 🛠️
There is no single right method. The choice depends on the mechanism, how much is missing, the column type, and whether the column is your target. Use the helper below, then read the strategies.
कोई एक सही तरीका नहीं है। चुनाव mechanism, कितना ग़ायब है, column के प्रकार, और column target है या नहीं, इस पर निर्भर है। नीचे का helper इस्तेमाल करें, फिर रणनीतियाँ पढ़ें।
没有唯一正确的方法。选择取决于缺失机制、缺失比例、列的类型,以及该列是否为目标变量。先用下面的助手,再读各个策略。
Describe your column. The path through the questions lights up and the matching code appears.
अपना column बताएँ। सवालों से गुज़रता रास्ता चमकता है और मेल खाता code दिखता है।
描述你的列。经过的问题路径会亮起,并显示对应的代码。
Strategy 1 — Drop (dropna)रणनीति 1 — हटाएँ (dropna)策略 1 —— 删除(dropna)
dropna() with no arguments checks every column, and one sparse column can wipe out most of your data.dropna() हर column जाँचता है, और एक विरल column आपका ज़्यादातर data मिटा सकता है।dropna() 会检查每一列,一个稀疏列就可能删掉大部分数据。# 3) Strategy 1 — drop (dropna)
print(f"dropna() on every column : {len(df.dropna()):,} of {len(df):,} rows left") # 'notes' empties almost everything
df = df.drop(columns=df.columns[df.isnull().mean() > 0.5]) # drop columns more than 50% empty
print("columns dropped :", sorted(set(df_raw.columns) - set(df.columns)))
cc = df.dropna(subset=["monthly_income"])
print(f"median income, complete rows only: ₹{cc['monthly_income'].median():.1f}k (true value: ₹{pd.read_csv('truth.csv')['monthly_income'].median():.1f}k)")
Strategy 2 — Fill With a Statistic (fillna)रणनीति 2 — किसी Statistic से भरें (fillna)策略 2 —— 用统计量填充(fillna)
Median for numbers (it ignores extreme values), mode for categories, and a group-wise median when the gaps depend on another column (MAR). Always save a missing-flag first — the fact that a value was missing can itself be useful to a model.
संख्याओं के लिए median (यह अति मानों को नज़रअंदाज़ करता है), categories के लिए mode, और जब कमियाँ किसी दूसरे column पर निर्भर हों (MAR) तो group-wise median। हमेशा पहले missing-flag save करें — मान का ग़ायब होना ख़ुद model के काम आ सकता है।
数值用中位数(不受极端值影响),类别用众数;当缺失取决于另一列(MAR)时用分组中位数。务必先保存缺失标记 —— 值曾经缺失这件事本身对模型就可能有用。
# 4) Strategy 2 — fill with a statistic (fillna). Assign the result back:
# df["age"].fillna(..., inplace=True) does NOTHING in pandas 3 (Copy-on-Write).
df["age_missing"] = df["age"].isnull().astype(int) # keep the fact that it was missing
before = df["age"].describe()
df["age"] = df["age"].fillna(df["age"].median())
df["city"] = df["city"].fillna(df["city"].mode()[0])
df["is_returned"] = df["is_returned"].fillna(False).astype(bool)
df["rating"] = df.groupby("delivery_days")["rating"].transform(lambda s: s.fillna(s.median())) # group-wise
print(f"age median used: {df['age'].median():.0f}")
print(f"age std before {before['std']:.2f} → after {df['age'].std():.2f}")
print(f"rows with age exactly {df['age'].median():.0f}: before {int((df_raw['age'] == df['age'].median()).sum())}, after {int((df['age'] == df['age'].median()).sum())}")
print(df.isnull().sum()[lambda s: s > 0])
df["age"].fillna(34, inplace=True) was a common pattern. With Copy-on-Write (default in pandas 3), df["age"] is a copy, so the fill is lost — pandas warns ChainedAssignmentError and df is unchanged. Write df["age"] = df["age"].fillna(34) instead. It works in every pandas version.
df["age"].fillna(34, inplace=True) एक आम तरीका था। Copy-on-Write (pandas 3 में default) के साथ df["age"] एक copy है, इसलिए fill खो जाता है — pandas ChainedAssignmentError चेतावनी देता है और df नहीं बदलता। इसकी जगह df["age"] = df["age"].fillna(34) लिखें। यह हर pandas version में चलता है।
df["age"].fillna(34, inplace=True) 曾是常见写法。在写时复制(pandas 3 默认开启)下,df["age"] 是一个副本,所以填充会丢失 —— pandas 会发出 ChainedAssignmentError 警告,而 df 不变。请改写为 df["age"] = df["age"].fillna(34),它在所有 pandas 版本中都有效。
| id | age | city | rating |
|---|---|---|---|
| 12503 | 29 | Mumbai | NaN |
| 13710 | NaN | Mumbai | NaN |
| 10716 | 39 | Bengaluru | NaN |
| 14352 | NaN | Pune | 3 |
| 13878 | NaN | Chennai | 4 |
| 13149 | NaN | Chennai | 5 |
| id | age | city | rating |
|---|---|---|---|
| 12503 | 29 | Mumbai | 3 |
| 13710 | 34 | Mumbai | 3 |
| 10716 | 39 | Bengaluru | 5 |
| 14352 | 34 | Pune | 3 |
| 13878 | 34 | Chennai | 4 |
| 13149 | 34 | Chennai | 5 |
The filled values pile up on one bar. Fine for a quick model; but tests, confidence intervals and correlations computed on this column will be too optimistic.
भरे गए मान एक ही bar पर ढेर हो जाते हैं। जल्दी के model के लिए ठीक; पर इस column पर निकाले tests, confidence intervals और correlations बहुत आशावादी होंगे।
填充值都堆在一根柱子上。快速建模可以接受,但基于这一列的检验、置信区间和相关系数都会过于乐观。
400 ages, some missing at random. Pick a method and the missing share, and watch the orange (filled) bars.
400 उम्रें, कुछ random ग़ायब। तरीका और ग़ायब हिस्सा चुनें, और नारंगी (भरे) bars देखें।
400 个年龄,部分随机缺失。选择方法和缺失比例,观察橙色(填充)的柱子。
Strategy 3 — Time Series: Interpolate (and Think About Cycles)रणनीति 3 — Time Series: Interpolate (और चक्रों के बारे में सोचें)策略 3 —— 时间序列:插值(并考虑周期)
# 5) Strategy 3 — time series: interpolate, or borrow the same hour from the day before
ts = pd.read_csv("temps.csv", parse_dates=["timestamp"]).set_index("timestamp")["temperature"]
truth = pd.read_csv("temps_truth.csv", parse_dates=["timestamp"]).set_index("timestamp")["temperature"]
gap = ts.isnull()
fills = {
"mean-fill": ts.fillna(ts.mean()),
"ffill": ts.ffill(),
"linear": ts.interpolate(method="time"),
"same hour −1d": ts.fillna(ts.shift(24)), # uses yesterday's pattern
}
for name, s in fills.items():
err = (s[gap] - truth[gap]).abs().mean()
print(f"{name:<14} mean abs error in the gap: {err:4.2f} °C gap peak: {s[gap].max():.1f} °C")
print(f"{'true values':<14} gap peak: {truth[gap].max():.1f} °C")
ffill() copies the last known value forward — fine for a short gap in a price feed, because it only uses the past. bfill() copies the next value backward, and interpolation also uses the value after the gap. In a forecasting model this lets future information into the past (data leakage). Use limit= so long gaps are not filled blindly.
ffill() आख़िरी ज्ञात मान को आगे copy करता है — price feed की छोटी कमी के लिए ठीक, क्योंकि यह सिर्फ़ अतीत इस्तेमाल करता है। bfill() अगले मान को पीछे copy करता है, और interpolation भी कमी के बाद वाला मान इस्तेमाल करता है। Forecasting model में यह भविष्य की जानकारी अतीत में डाल देता है (data leakage)। limit= इस्तेमाल करें ताकि लंबी कमियाँ आँख मूँदकर न भरी जाएँ।
ffill() 把最后一个已知值向后复制 —— 适合价格数据中的短缺口,因为它只用过去的数据。bfill() 把下一个值向前复制,插值也会用到缺口之后的值。在预测模型里,这会把未来信息带进过去(数据泄漏)。用 limit= 参数,避免盲目填充长缺口。
One week of hourly temperatures. Move the gap and change its length, then compare methods.
एक हफ़्ते का हर घंटे का तापमान। कमी खिसकाएँ और उसकी लंबाई बदलें, फिर तरीकों की तुलना करें।
一周的逐小时温度。移动缺口、改变长度,然后比较各种方法。
Strategy 4 — KNN and Model-Based Imputationरणनीति 4 — KNN और Model-आधारित Imputation策略 4 —— KNN 与基于模型的填补
KNNImputer fills a gap with the average of the k most similar rows. IterativeImputer predicts each column from the others in turns. Two rules: scale first (KNN uses distances, so income in thousands would swamp delivery days), and never include ID columns.
KNNImputer कमी को k सबसे मिलती-जुलती rows के औसत से भरता है। IterativeImputer बारी-बारी हर column का दूसरों से अनुमान लगाता है। दो नियम: पहले scale करें (KNN दूरी इस्तेमाल करता है, इसलिए हज़ारों में income delivery दिनों को दबा देगी), और ID columns कभी शामिल न करें।
KNNImputer 用 k 个最相似行的平均值来填补。IterativeImputer 轮流用其他列预测每一列。两条规则:先标准化(KNN 依赖距离,以千为单位的收入会压倒配送天数),并且绝不把 ID 列放进去。
# 6) Strategy 4 — KNN imputation. Scale first: KNN uses distances.
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import KNNImputer
cols = ["age", "monthly_income", "purchase_amount", "delivery_days"] # NOT customer_id
true_inc = pd.read_csv("truth.csv").set_index("customer_id")["monthly_income"]
miss = df["monthly_income"].isnull()
knn = make_pipeline(StandardScaler(), KNNImputer(n_neighbors=5))
filled = pd.DataFrame(knn.fit_transform(df[cols]), columns=cols, index=df.index)
filled = filled * knn[0].scale_ + knn[0].mean_ # back to original units
t = true_inc.loc[df.loc[miss, "customer_id"]].to_numpy()
for name, est in [("median fill", np.full(miss.sum(), df["monthly_income"].median())),
("KNN (scaled)", filled.loc[miss, "monthly_income"].to_numpy())]:
print(f"{name:<13} mean abs error ₹{np.abs(est - t).mean():5.1f}k mean of filled ₹{est.mean():5.1f}k true mean ₹{t.mean():5.1f}k")
from sklearn.experimental import enable_iterative_imputer # still required in scikit-learn 1.9
from sklearn.impute import IterativeImputer
imp = make_pipeline(StandardScaler(), IterativeImputer(max_iter=10, random_state=42))
# In a real project: fit on the TRAINING rows only, then transform validation/test rows.
| Situationस्थिति情况 | Good defaultअच्छा default推荐做法 |
|---|---|
| MCAR, number, few %MCAR, संख्या, कुछ %MCAR、数值、少量 | drop rows, or median fillrows हटाएँ, या median fill删行或中位数填充 |
| MCAR, categoryMCAR, categoryMCAR、类别 | mode fill, or a new "Unknown" categorymode fill, या नई "Unknown" category众数填充,或新建 “Unknown” 类别 |
| MARMARMAR | group-wise fill, KNN or IterativeImputer (scaled) + missing-flaggroup-wise fill, KNN या IterativeImputer (scaled) + missing-flag分组填充、KNN 或 IterativeImputer(标准化)+ 缺失标记 |
| MNARMNARMNAR | missing-flag, domain expert, compare results under several fillsmissing-flag, domain expert, कई fills के तहत नतीजों की तुलना缺失标记、领域专家、比较多种填充下的结果 |
| Time seriesTime series时间序列 | interpolate short gaps; seasonal fill for long ones; avoid bfill when forecastingछोटी कमियाँ interpolate; लंबी के लिए seasonal fill; forecasting में bfill से बचें短缺口插值;长缺口季节性填充;预测时避免 bfill |
| Target columnTarget column目标列 | drop the rowsrows हटाएँ删除这些行 |
| Over ~50% missingलगभग 50% से ज़्यादा ग़ायब缺失超过约 50% | drop the column (keep a flag if the gap is informative)column हटाएँ (अगर कमी कुछ बताती हो तो flag रखें)删除该列(若缺失本身有信息,保留标记) |
Removing Duplicates 👯Duplicates हटाना 👯去除重复 👯
A duplicate repeats information that is already in the table. It may be an exact copy, a repeated key (the same customer with two versions of their record), or a fuzzy match (the same thing spelled differently). Duplicates inflate counts and totals, and if copies land in both training and test sets they make a model look better than it is.
Duplicate वह जानकारी दोहराता है जो table में पहले से है। यह हूबहू copy, दोहराई key (एक ही ग्राहक के record के दो versions), या fuzzy मेल (एक ही चीज़ अलग वर्तनी में) हो सकता है। Duplicates गिनती और जोड़ बढ़ाते हैं, और अगर copies training और test दोनों में पहुँच जाएँ तो model असल से बेहतर दिखता है।
重复行重复了表里已有的信息。它可能是完全副本、重复的键(同一顾客有两个版本的记录),或模糊匹配(同一事物的不同写法)。重复会抬高计数和总额;若副本同时进入训练集和测试集,模型会显得比实际更好。
df.duplicated(subset=["order_id"]).sum() after the merge would show the problem at once.df.duplicated(subset=["order_id"]).sum() समस्या तुरंत दिखा देता।df.duplicated(subset=["order_id"]).sum(),就能立刻发现问题。# 7) Duplicates: look first, then decide
print("exact duplicates :", df.duplicated().sum())
print(df[df.duplicated(keep=False)].sort_values("customer_id").head(4)[["customer_id", "order_date", "purchase_amount"]])
df = df.drop_duplicates()
key = df["customer_id"].duplicated(keep=False)
print("\nsame customer_id, different values:", int(key.sum()), "rows")
df = (df.sort_values("order_date") # oldest → newest
.drop_duplicates(subset="customer_id", keep="last") # keep the newest record
.sort_index())
# Fuzzy spellings: normalise text, then map known aliases
c = df["city"].str.strip().str.lower().str.replace(r"[^a-z ]", "", regex=True)
aliases = {"mum": "mumbai", "bombay": "mumbai", "blr": "bengaluru", "bangalore": "bengaluru", "new delhi": "delhi"}
df["city"] = c.replace(aliases).str.title()
print("rows now:", len(df), "| cities:", sorted(df["city"].unique()))
A common snippet is str.lower().str.strip() followed by {"mum": "mumbai"}. But "Mum." becomes "mum." — with the dot — so the map never matches. Remove punctuation first (str.replace(r"[^a-z ]", "", regex=True)), and list real aliases like "Bombay" and "New Delhi". For true typos, libraries like rapidfuzz can score how similar two strings are.
एक आम snippet है str.lower().str.strip() और फिर {"mum": "mumbai"}। पर "Mum." "mum." बनता है — बिंदु के साथ — इसलिए map कभी मेल नहीं खाता। पहले विराम चिह्न हटाएँ (str.replace(r"[^a-z ]", "", regex=True)), और "Bombay" और "New Delhi" जैसे असली aliases लिखें। असली typos के लिए rapidfuzz जैसी libraries दो strings की समानता का score दे सकती हैं।
常见写法是 str.lower().str.strip() 再接 {"mum": "mumbai"}。但 “Mum.” 会变成 “mum.” —— 带着句点 —— 所以映射永远匹配不上。先去掉标点(str.replace(r"[^a-z ]", "", regex=True)),再列出真实别名,如 “Bombay” 和 “New Delhi”。真正的拼写错误可用 rapidfuzz 等库计算字符串相似度。
A small orders table with all three problems. Press the steps in order (or Play) and watch the total sales.
तीनों समस्याओं वाली एक छोटी orders table। क़दम क्रम से दबाएँ (या Play) और कुल बिक्री देखें।
一个包含三类问题的小订单表。按顺序点击各步骤(或播放),观察销售总额。
Step 3 — Validate the Clean Data ✅क़दम 3 — साफ़ Data जाँचें ✅第 3 步 —— 验证清洗后的数据 ✅
After cleaning, check in code that the problems are gone and that no new ones appeared. Make each check fail loudly. Python's assert is fine in a notebook, but it is switched off when Python runs with -O, so pipelines should raise an error themselves.
सफ़ाई के बाद code में जाँचें कि समस्याएँ चली गईं और कोई नई नहीं आई। हर जाँच को ज़ोर से fail होने दें। Notebook में Python का assert ठीक है, पर Python -O के साथ चलने पर यह बंद हो जाता है, इसलिए pipelines को ख़ुद error raise करनी चाहिए।
清洗之后,用代码检查问题确实消失了,且没有引入新问题。每个检查都要明确报错。在笔记本里用 Python 的 assert 没问题,但用 -O 运行时它会被关闭,所以数据管道应自己抛出错误。
# 8) Validate — fail loudly if a rule is broken
df = df.drop(columns=["monthly_income"]) # MNAR and 12% missing: no safe fill here, so drop it (or ask the business)
def check(cond, msg):
if not cond:
raise ValueError(msg) # 'assert' is skipped when Python runs with -O
check(df.isnull().sum().sum() == 0, "missing values left")
check(df.duplicated().sum() == 0 and df["customer_id"].is_unique, "duplicates left")
check(df["age"].between(18, 100).all(), "age out of range")
check(df["purchase_amount"].ge(0).all(), "negative purchase")
check(df["rating"].between(1, 5).all(), "rating outside 1–5")
report = {"rows_before": len(df_raw), "rows_after": len(df), "rows_removed": len(df_raw) - len(df),
"nulls_remaining": int(df.isnull().sum().sum()), "dupes_remaining": int(df.duplicated().sum())}
print(pd.Series(report))
5,270 rows → 5,000 real customers (150 exact copies and 120 old versions removed); 6,950 missing cells → 0; 15 city spellings → 6. Save this report next to the clean file.
5,270 rows → 5,000 असली ग्राहक (150 हूबहू copies और 120 पुराने versions हटे); 6,950 ग़ायब cells → 0; 15 city वर्तनियाँ → 6। यह report साफ़ file के साथ save करें।
5,270 行 → 5,000 位真实顾客(删除 150 个完全副本和 120 个旧版本);6,950 个缺失单元格 → 0;15 种城市写法 → 6。把这份报告和清洗后的文件保存在一起。
A Reusable Cleaning Function 🧰दोबारा इस्तेमाल लायक़ Cleaning Function 🧰可复用的清洗函数 🧰
This function does the routine steps in a safe order: duplicates first (so they do not distort the medians), then mostly-empty columns, then fills with missing-flags. It never fills the ID column, and it handles pandas 3's str dtype.
यह function नियमित क़दम सुरक्षित क्रम में करता है: पहले duplicates (ताकि वे medians न बिगाड़ें), फिर ज़्यादातर ख़ाली columns, फिर missing-flags के साथ fills। यह ID column कभी नहीं भरता, और pandas 3 का str dtype संभालता है।
这个函数按安全的顺序完成常规步骤:先去重(以免扭曲中位数),再删几乎全空的列,然后带缺失标记进行填充。它从不填充 ID 列,并能处理 pandas 3 的 str 类型。
# 9) A reusable cleaning function (a starting point — always review its choices)
def clean_dataframe(df, id_col=None, date_col=None, max_missing=0.5, verbose=True):
out = df.copy()
report = {"rows_start": len(out), "cols_start": out.shape[1]}
# 1. Exact duplicates first, so they do not distort the medians below
out = out.drop_duplicates()
if id_col and date_col: # keep the newest record per id
out = out.sort_values(date_col).drop_duplicates(subset=id_col, keep="last").sort_index()
# 2. Drop empty rows, and columns that are mostly empty
out = out.dropna(how="all")
out = out.loc[:, out.isnull().mean() <= max_missing]
report["cols_dropped"] = report["cols_start"] - out.shape[1]
# 3. Missing-indicator + median for numbers (never fill the id)
for col in out.select_dtypes(include="number").columns.drop(id_col, errors="ignore"):
if out[col].isnull().any():
out[col + "_missing"] = out[col].isnull().astype(int)
out[col] = out[col].fillna(out[col].median())
# 4. Mode for text — 'string' is needed for pandas 3's str dtype
for col in out.select_dtypes(include=["object", "string", "category"]).columns:
if out[col].isnull().any():
out[col] = out[col].fillna(out[col].mode()[0])
report.update(rows_end=len(out), nulls_remaining=int(out.isnull().sum().sum()), dupes_remaining=int(out.duplicated().sum()))
if verbose:
print(f"{report['rows_start']:,} rows → {report['rows_end']:,} | columns dropped: {report['cols_dropped']} | "
f"nulls left: {report['nulls_remaining']} | dupes left: {report['dupes_remaining']}")
return out, report
df_clean, rep = clean_dataframe(df_raw, id_col="customer_id", date_col="order_date")
print("new indicator columns:", [c for c in df_clean if c.endswith("_missing")])
It cannot know your city aliases, your valid ranges, or that monthly_income is MNAR. It fills that column with the median and adds a flag — a reasonable default, but you should still review it. Treat the function as a first pass, then add the domain rules from Sections 3–6.
यह आपके city aliases, सही ranges, या यह नहीं जान सकता कि monthly_income MNAR है। यह उस column को median से भरता है और flag जोड़ता है — एक उचित default, पर आपको फिर भी इसकी समीक्षा करनी चाहिए। Function को पहला दौर मानें, फिर Sections 3–6 के domain नियम जोड़ें।
它不知道你的城市别名、有效范围,也不知道 monthly_income 是 MNAR。它用中位数填充该列并加上标记 —— 这是合理的默认做法,但你仍应复核。把这个函数当作第一遍处理,再加上第 3–6 节的领域规则。
Make the Messy Practice Data 🐍बिखरा Practice Data बनाएँ 🐍生成杂乱的练习数据 🐍
Run this first. It builds the complete data, saves it as truth.csv, then plants the problems: MCAR gaps in age, MNAR gaps in income, MAR gaps in rating, messy city names, 150 exact copies and 120 updated records. It also makes the temperature series.
इसे पहले चलाएँ। यह पूरा data बनाता है, उसे truth.csv के रूप में save करता है, फिर समस्याएँ डालता है: age में MCAR कमियाँ, income में MNAR, rating में MAR, बिखरे city नाम, 150 हूबहू copies और 120 updated records। यह तापमान series भी बनाता है।
请先运行它。它生成完整数据并保存为 truth.csv,然后埋入问题:age 的 MCAR 缺失、income 的 MNAR 缺失、rating 的 MAR 缺失、混乱的城市名、150 个完全副本和 120 条更新记录。它还生成温度序列。
# 0) Build a messy practice dataset with KNOWN problems (so we can check our cleaning)
import numpy as np, pandas as pd
rng = np.random.default_rng(7)
n = 5000
age = np.clip(np.round(18 + rng.gamma(4, 4.5, n)), 18, 75)
income = np.round(np.exp(3.2 + 0.012 * age + rng.normal(0, 0.45, n)), 1) # ₹ thousand / month
cities = rng.choice(["Mumbai", "Delhi", "Bengaluru", "Chennai", "Pune", "Kolkata"], n, p=[.26, .22, .2, .12, .11, .09])
delivery = rng.integers(1, 11, n)
df = pd.DataFrame({
"customer_id": np.arange(10001, 10001 + n),
"order_date": pd.Timestamp("2024-01-01") + pd.to_timedelta(rng.integers(0, 366, n), unit="D"),
"age": age, "city": cities, "monthly_income": income,
"purchase_amount": np.round(np.exp(5.6 + 0.9 * np.log(income) + rng.normal(0, 0.5, n)), -1),
"delivery_days": delivery,
"rating": np.clip(np.round(5.3 - 0.3 * delivery + rng.normal(0, 0.7, n)), 1, 5),
"is_returned": rng.random(n) < 0.08,
"notes": np.where(rng.random(n) < 0.12, "gift wrap", None),
})
df.to_csv("truth.csv", index=False) # the complete data — we only keep it to check our work
# --- Plant missing values with three different mechanisms ---
df["is_returned"] = df["is_returned"].astype(object)
df.loc[rng.random(n) < 0.12, "age"] = np.nan # MCAR: random form error
rank = df["monthly_income"].rank(pct=True)
df.loc[rng.random(n) < np.where(rank > 0.8, 0.45, 0.03), "monthly_income"] = np.nan # MNAR: top earners skip it
df.loc[rng.random(n) < 0.02 + 0.025 * (df["delivery_days"] - 1), "rating"] = np.nan # MAR: slow orders rarely rated
df.loc[rng.random(n) < 0.04, "city"] = np.nan
df.loc[rng.random(n) < 0.03, "is_returned"] = np.nan
# --- Messy spellings of the same city ---
variants = {"Mumbai": ["mumbai", " Mumbai ", "Mum.", "Bombay"], "Bengaluru": ["Bangalore", "BLR", "bengaluru "], "Delhi": ["delhi", "New Delhi"]}
for city, vs in variants.items():
idx = df.index[(df["city"] == city) & (rng.random(n) < 0.15)]
df.loc[idx, "city"] = rng.choice(vs, len(idx))
# --- Duplicates: a batch loaded twice, and customers whose record was updated later ---
exact = df.sample(150, random_state=1)
updated = df.sample(120, random_state=2).copy()
updated["order_date"] += pd.to_timedelta(rng.integers(5, 40, 120), unit="D")
updated["rating"] = np.clip(updated["rating"].fillna(3) + rng.choice([-1, 1], 120), 1, 5)
messy = pd.concat([df, exact, updated]).sample(frac=1, random_state=3).reset_index(drop=True)
messy.to_csv("customers_raw.csv", index=False)
# --- Hourly temperature for one July week, with a 6-hour sensor outage ---
t = pd.date_range("2024-07-08", periods=7 * 24, freq="h")
temp = np.asarray(30 + 4.5 * np.sin(2 * np.pi * (t.hour - 9) / 24) + rng.normal(0, 0.35, len(t)) + np.linspace(0, 1.5, len(t)))
pd.DataFrame({"timestamp": t, "temperature": temp.round(1)}).to_csv("temps_truth.csv", index=False)
temp[(t >= "2024-07-10 11:00") & (t < "2024-07-10 17:00")] = np.nan
pd.DataFrame({"timestamp": t, "temperature": temp.round(1)}).to_csv("temps.csv", index=False)
print(f"customers_raw.csv: {len(messy):,} rows temps.csv: {len(t)} hours (6 missing)")
Golden Rules of Data Cleaning 🏆Data Cleaning के सुनहरे नियम 🏆数据清洗黄金法则 🏆
df = df_raw.copy()) so every mistake can be undone.df = df_raw.copy()) ताकि हर ग़लती सुधारी जा सके।df = df_raw.copy()),这样每个错误都能撤销。.info(), .isnull().mean(), .duplicated().sum() and .value_counts() on every text column..info(), .isnull().mean(), .duplicated().sum() और .value_counts()।.info()、.isnull().mean()、.duplicated().sum() 和 .value_counts()。df["col_missing"] = df["col"].isnull().astype(int).df["col_missing"] = df["col"].isnull().astype(int)।df["col_missing"] = df["col"].isnull().astype(int)。df[df.duplicated(keep=False)].df[df.duplicated(keep=False)]।df[df.duplicated(keep=False)]。df[col] = df[col].fillna(...)). inplace=True on a column does nothing in pandas 3.df[col] = df[col].fillna(...))। Column पर inplace=True pandas 3 में कुछ नहीं करता।df[col] = df[col].fillna(...))。在 pandas 3 中对列用 inplace=True 毫无作用。Data cleaning is where many of the most important decisions in a project are made. Every fill and every dropped row is an assumption about the world. Check the reason behind each problem, choose the fix on purpose, test it, and write it down — then your model learns from the world as it is, not from the gaps in your data.
Data cleaning वह जगह है जहाँ project के कई सबसे ज़रूरी फ़ैसले होते हैं। हर fill और हर हटाई गई row दुनिया के बारे में एक मान्यता है। हर समस्या के पीछे का कारण जाँचें, सुधार सोच-समझकर चुनें, उसे परखें, और लिख लें — तब आपका model दुनिया जैसी है उससे सीखता है, आपके data की कमियों से नहीं।
数据清洗是项目中许多最重要决策发生的地方。每一次填充、每一行删除都是对现实的一个假设。查清每个问题背后的原因,有意识地选择修复方法,验证它,并记录下来 —— 这样你的模型学到的是真实世界,而不是数据里的缺口。