Data Preparation / Data Preprocessing 📂 Data Collection · 5 of 13 250 min read

Data Cleaning in Python — Handling Missing Values & Removing Duplicates

A story-driven, comprehensive guide to cleaning real-world messy data using pandas — covering missing value detection, filling strategies, duplicate removal, and validation, with live diagrams and real code throughout.

🌐
SectionSection第 01 节

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% —— 花在训练模型之前发现并修复这些问题上。

The Health Algorithm That Used the Wrong Numberवह Health Algorithm जिसने ग़लत संख्या इस्तेमाल की用错了数字的医疗算法
In 2019, researchers (Obermeyer et al., Science) studied an algorithm used by US hospitals to pick patients for extra care. It predicted future health-care costs and used them as a stand-in for health need. But Black patients had lower recorded costs at the same level of illness, because they had less access to care. So the algorithm judged them healthier than they were. The data was not "missing" in the usual sense — it was shaped by who got care. The lesson for cleaning: always ask why a value is low, empty or absent, not only how to fill it.
2019 में शोधकर्ताओं (Obermeyer et al., Science) ने अमेरिकी अस्पतालों में इस्तेमाल एक algorithm का अध्ययन किया जो अतिरिक्त देखभाल के लिए मरीज़ चुनता था। यह भविष्य के health-care ख़र्च का अनुमान लगाता था और उसे स्वास्थ्य ज़रूरत की जगह इस्तेमाल करता था। पर बराबर बीमारी पर Black मरीज़ों का दर्ज ख़र्च कम था, क्योंकि उन्हें देखभाल तक कम पहुँच थी। इसलिए algorithm ने उन्हें असल से ज़्यादा स्वस्थ माना। Data सामान्य अर्थ में "ग़ायब" नहीं था — वह इस बात से बना था कि देखभाल किसे मिली। सफ़ाई के लिए सीख: हमेशा पूछें कि कोई मान कम, ख़ाली या ग़ायब क्यों है, सिर्फ़ यह नहीं कि उसे कैसे भरें।
2019 年,研究人员(Obermeyer 等,《Science》)研究了美国医院用来挑选额外护理患者的一个算法。它预测未来的医疗费用,并把费用当作健康需求的替代指标。但在病情相同时,黑人患者记录的费用更低,因为他们获得医疗的机会更少。于是算法认为他们比实际更健康。这些数据并不是通常意义上的“缺失”—— 而是由谁得到了医疗所塑造的。对数据清洗的启示:永远要问一个值为什么偏低、为空或缺失,而不只是怎么填。
⚠️
Cleaning Is Part of the Analysisसफ़ाई विश्लेषण का हिस्सा है清洗是分析的一部分

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 बनाते हैं। सफ़ाई का हर फ़ैसला दुनिया के बारे में एक मान्यता है, इसलिए इसे सोच-समझकर लें और लिख लें।

更强的算法修不好脏数据。缺失值会让平均数产生偏差,重复会抬高计数和总额,混乱的标签会制造假类别。每个清洗选择都是对现实的一个假设,所以要有意识地做,并记录下来。

01
AuditAudit审查
.info(), .isnull(), .duplicated(), .value_counts() — measure every problem first.
.info(), .isnull(), .duplicated(), .value_counts() — पहले हर समस्या मापें।
.info()、.isnull()、.duplicated()、.value_counts() —— 先量化每个问题。
02
Understand the gapsकमियाँ समझें理解缺失
MCAR, MAR or MNAR? Compare rows with and without a value.
MCAR, MAR या MNAR? मान वाली और बिना मान वाली rows की तुलना करें।
MCAR、MAR 还是 MNAR?比较有值和无值的行。
03
Remove duplicatesDuplicates हटाएँ去除重复
Exact copies, repeated keys and fuzzy spellings — before computing fill values.
हूबहू copies, दोहराई keys और fuzzy वर्तनियाँ — fill मान निकालने से पहले।
完全副本、重复键和模糊拼写 —— 在计算填充值之前。
04
Handle missing valuesग़ायब मान संभालें处理缺失值
Drop, fill, interpolate or impute — plus a missing-flag.
हटाएँ, भरें, interpolate या impute करें — साथ में missing-flag।
删除、填充、插值或建模填补 —— 再加缺失标记。
05
Validateजाँचें验证
Checks that fail loudly; a short report saved with the data.
ज़ोर से fail होने वाली जाँचें; data के साथ save की गई छोटी report।
会明确报错的检查;与数据一起保存一份简短报告。

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 节生成)。其中的问题是故意埋入的,我们还保留了完整版本,所以你能看到哪些方法真正有效。


SectionSection第 02 节

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)")
OUTPUT
(5270, 10) missing pct dtype notes 4636 88.0 str rating 705 13.4 float64 monthly_income 623 11.8 float64 age 616 11.7 float64 city 228 4.3 str is_returned 142 2.7 object Exact duplicate rows : 150 Duplicate customer_id: 270 City spellings : 15 (6 real cities)
📊 PICTURE 1 — REAL AUDIT: MISSING % PER COLUMNतस्वीर 1 — असली AUDIT: हर COLUMN में MISSING %图 1 — 真实审查:各列缺失比例
Missing values per column 0% 25% 50% 75% 100% notes 88.0% rating 13.4% monthly_income 11.8% age 11.7% city 4.3% is_returned 2.7% under 5%: drop rows or simple fill5% से कम: rows हटाएँ या सरल fill低于 5%:删行或简单填充 5–50%: choose a fill strategy5–50%: fill का तरीका चुनें5–50%:选择填充策略 over 50%: drop column50% से ज़्यादा: column हटाएँ超过 50%:删列

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 只有少量缺失。


SectionSection第 03 节

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)把它分成三种情况:

MCAR — Missing Completely At RandomMCAR — पूरी तरह Random ग़ायबMCAR —— 完全随机缺失
🎲
  • 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
MAR — Missing At RandomMAR — Random ग़ायब (शर्त के साथ)MAR —— 随机缺失(有条件)
🔗
  • 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)
MNAR — Missing Not At RandomMNAR — Random नहींMNAR —— 非随机缺失
⚠️
  • 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
🎯 PICTURE 2 — WHERE THE GAPS ARE (WE KNOW THE TRUE VALUES)तस्वीर 2 — कमियाँ कहाँ हैं (हमें असली मान पता हैं)图 2 — 缺失在哪里(我们知道真实值)
Where the missing values are: MCAR age versus MNAR income MCAR — red spread everywhereMCAR — लाल हर जगह फैलेMCAR —— 红点到处分布 20 30 40 50 60 70 age (true value)age (असली मान)age(真实值) MNAR — red only at the topMNAR — लाल सिर्फ़ ऊपरMNAR —— 红点只在高端 10 30 100 250 monthly income, ₹k (true value, log)monthly income, ₹k (असली मान, log)月收入,千卢比(真实值,对数) ₹1k ₹10k ₹100k y = purchase amount (log) · blue: value present · red ring: value missing in the raw filey = purchase राशि (log) · नीला: मान मौजूद · लाल घेरा: raw file में मान ग़ायबy = 消费金额(对数)· 蓝:有值 · 红圈:原始文件中缺失

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}")
OUTPUT
age missing 11.7% | median purchase: missing ₹ 7,420 present ₹ 7,000 | mean delivery_days: 5.7 vs 5.5 monthly_income missing 11.8% | median purchase: missing ₹11,090 present ₹ 6,650 | mean delivery_days: 5.6 vs 5.5 rating missing 13.4% | median purchase: missing ₹ 7,350 present ₹ 6,990 | mean delivery_days: 7.1 vs 5.3
💡
How to Check the Mechanism (and Its Limit)Mechanism कैसे जाँचें (और इसकी सीमा)如何检查缺失机制(以及局限)

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,因为收入同时决定了两者。

LIVELIVE互动MCAR, MAR, MNAR — See the BiasMCAR, MAR, MNAR — Bias देखेंMCAR、MAR、MNAR —— 看见偏差

300 customers. Choose why incomes go missing and how many. Red rings show the true values you lost.

300 ग्राहक। चुनें कि incomes क्यों और कितनी ग़ायब हों। लाल घेरे वे असली मान दिखाते हैं जो खो गए।

300 位顾客。选择收入缺失的原因和比例。红圈是你丢失的真实值。


SectionSection第 04 节

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 इस्तेमाल करें, फिर रणनीतियाँ पढ़ें।

没有唯一正确的方法。选择取决于缺失机制、缺失比例、列的类型,以及该列是否为目标变量。先用下面的助手,再读各个策略。

LIVELIVE互动Which Strategy? — A Decision Helperकौन सी रणनीति? — फ़ैसले का Helper选哪种策略?—— 决策助手

Describe your column. The path through the questions lights up and the matching code appears.

अपना column बताएँ। सवालों से गुज़रता रास्ता चमकता है और मेल खाता code दिखता है।

描述你的列。经过的问题路径会亮起,并显示对应的代码。

type
why


Strategy 1 — Drop (dropna)रणनीति 1 — हटाएँ (dropna)策略 1 —— 删除(dropna)

The Survey With a Missing Answerवह Survey जिसमें जवाब ग़ायब था缺了答案的问卷
A company surveys 50,000 customers and wants to predict "overall satisfaction". 1,400 people (2.8%) left that question blank. Because it is the target, you cannot invent it — drop those rows. Dropping is also reasonable when a few % of rows are MCAR, or when a column is mostly empty. But dropna() with no arguments checks every column, and one sparse column can wipe out most of your data.
एक कंपनी 50,000 ग्राहकों का survey करती है और "overall satisfaction" का अनुमान लगाना चाहती है। 1,400 लोगों (2.8%) ने वह सवाल ख़ाली छोड़ा। क्योंकि यह target है, आप इसे गढ़ नहीं सकते — वे rows हटाएँ। हटाना तब भी ठीक है जब कुछ % rows MCAR हों, या कोई column ज़्यादातर ख़ाली हो। पर बिना arguments के dropna() हर column जाँचता है, और एक विरल column आपका ज़्यादातर data मिटा सकता है।
一家公司调查了 5 万名顾客,想预测“总体满意度”。有 1,400 人(2.8%)没回答这一题。因为它是目标变量,不能编造 —— 删掉这些行。少量 MCAR 缺失的行,或几乎全空的列,也适合删除。但不带参数的 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)")
OUTPUT
dropna() on every column : 393 of 5,270 rows left columns dropped : ['notes'] median income, complete rows only: ₹36.0k (true value: ₹37.7k)

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])
OUTPUT
age median used: 34 age std before 9.02 → after 8.49 rows with age exactly 34: before 241, after 857 monthly_income 623 dtype: int64
🐼
pandas 3: inplace=True on a Column Does Nothingpandas 3: Column पर inplace=True कुछ नहीं करताpandas 3:对列使用 inplace=True 毫无作用

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 版本中都有效。

❌ Before fillna (real rows)fillna से पहले (असली rows)fillna 之前(真实行)
idagecityrating
1250329MumbaiNaN
13710NaNMumbaiNaN
1071639BengaluruNaN
14352NaNPune3
13878NaNChennai4
13149NaNChennai5
✅ After fillnafillna के बादfillna 之后
idagecityrating
1250329Mumbai3
1371034Mumbai3
1071639Bengaluru5
1435234Pune3
1387834Chennai4
1314934Chennai5
📈 PICTURE 3 — WHAT A MEDIAN FILL DOES TO THE AGE COLUMNतस्वीर 3 — MEDIAN FILL AGE COLUMN के साथ क्या करता है图 3 — 中位数填充对年龄列做了什么
Age distribution after median fill 0 200 400 600 800 857 rows at age 34 (241 real + 616 filled)उम्र 34 पर 857 rows (241 असली + 616 भरी)年龄 34 处有 857 行(241 真实 + 616 填充) 20 30 40 50 60 70 ages that were recordedजो उम्रें दर्ज थीं原本记录的年龄 616 gaps filled with the median (34)616 कमियाँ median (34) से भरीं616 个缺口用中位数(34)填充 std before 9.02 → after 8.49: the data now looks more certain than it isstd पहले 9.02 → बाद में 8.49: data अब असल से ज़्यादा निश्चित दिखता है标准差 9.02 → 8.49:数据看起来比实际更确定

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 बहुत आशावादी होंगे।

填充值都堆在一根柱子上。快速建模可以接受,但基于这一列的检验、置信区间和相关系数都会过于乐观。

LIVELIVE互动What Does Each Fill Do to the Data?हर Fill Data के साथ क्या करता है?每种填充对数据做了什么?

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 —— 时间序列:插值(并考虑周期)

The Weather Station GapWeather Station की कमी气象站的缺口
A sensor records the temperature every hour. A power cut loses six readings, from 11:00 to 17:00 — exactly over the afternoon peak. A weekly mean would draw a flat line. Linear interpolation is much better, but a straight line from 11:00 to 17:00 cuts off the peak. Because temperature repeats every day, filling from the same hours yesterday works best here. Below are the real errors.
एक sensor हर घंटे तापमान दर्ज करता है। बिजली जाने से 11:00 से 17:00 तक छह readings खो जाती हैं — ठीक दोपहर की चोटी के ऊपर। हफ़्ते का mean एक सपाट line बनाएगा। Linear interpolation बहुत बेहतर है, पर 11:00 से 17:00 तक सीधी line चोटी काट देती है। क्योंकि तापमान हर दिन दोहराता है, यहाँ कल के उन्हीं घंटों से भरना सबसे अच्छा है। नीचे असली ग़लतियाँ हैं।
传感器每小时记录一次温度。停电导致 11:00 到 17:00 的六个读数丢失 —— 恰好覆盖午后高峰。用周均值会画出一条平线。线性插值好得多,但从 11:00 到 17:00 的直线会削掉高峰。由于温度每天重复,这里用前一天同一时段的值来填效果最好。下面是真实误差。
# 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")
OUTPUT
mean-fill mean abs error in the gap: 3.71 °C gap peak: 30.6 °C ffill mean abs error in the gap: 2.52 °C gap peak: 31.8 °C linear mean abs error in the gap: 1.27 °C gap peak: 33.9 °C same hour −1d mean abs error in the gap: 0.17 °C gap peak: 35.1 °C true values gap peak: 35.3 °C
🌡️ PICTURE 4 — REAL OUTPUT: THREE WAYS TO FILL A 6-HOUR GAPतस्वीर 4 — असली OUTPUT: 6 घंटे की कमी भरने के तीन तरीके图 4 — 真实输出:填补 6 小时缺口的三种方法
Filling a six-hour gap in hourly temperature 25°C 30°C 35°C mean-fill: error 3.71 °C linear: error 1.27 °C same hour −1 day: error 0.17 °C true values (hidden)असली मान (छिपे)真实值(隐藏) 9 Jul 00:00 10 Jul 00:00 11 Jul 00:00 12 Jul 00:00
⏭️
ffill Is Safe, bfill Can Leak the Futureffill सुरक्षित है, bfill भविष्य लीक कर सकता हैffill 安全,bfill 可能泄露未来

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= 参数,避免盲目填充长缺口。

LIVELIVE互动Fill the Gapकमी भरें填补缺口

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")
OUTPUT
median fill mean abs error ₹ 32.2k mean of filled ₹ 36.0k true mean ₹ 65.9k KNN (scaled) mean abs error ₹ 22.7k mean of filled ₹ 47.1k true mean ₹ 65.9k
🤖 PICTURE 5 — REAL OUTPUT: NO FILL CAN FULLY FIX MNARतस्वीर 5 — असली OUTPUT: कोई FILL MNAR को पूरी तरह ठीक नहीं कर सकता图 5 — 真实输出:任何填充都无法完全修复 MNAR
Mean of filled incomes versus the truth
true mean of the missing incomesग़ायब incomes का असली mean缺失收入的真实均值
₹65.9k
median fillmedian fill中位数填充
₹36.0k
KNN on scaled columnsscaled columns पर KNN标准化后 KNN
₹47.1k Both fills are too low: the missing incomes were the high ones (MNAR). KNN only gets part of the way.दोनों fills बहुत कम हैं: ग़ायब incomes ऊँची वाली थीं (MNAR)। KNN सिर्फ़ कुछ दूर तक पहुँचता है।两种填充都偏低:缺失的正是高收入(MNAR)。KNN 只修正了一部分。
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” 类别
MARMARMARgroup-wise fill, KNN or IterativeImputer (scaled) + missing-flaggroup-wise fill, KNN या IterativeImputer (scaled) + missing-flag分组填充、KNN 或 IterativeImputer(标准化)+ 缺失标记
MNARMNARMNARmissing-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 रखें)删除该列(若缺失本身有信息,保留标记)

SectionSection第 05 节

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 असल से बेहतर दिखता है।

重复行重复了表里已有的信息。它可能是完全副本、重复的键(同一顾客有两个版本的记录),或模糊匹配(同一事物的不同写法)。重复会抬高计数和总额;若副本同时进入训练集和测试集,模型会显得比实际更好。

Three Regional Tables, One Payment Systemतीन क्षेत्रीय Tables, एक Payment System三张地区表,一个支付系统
Imagine a retailer merges its North, South and West sales tables. Some orders pass through a shared payment system, so they appear in two tables. Demand in those regions now looks higher than it is, and a forecast built on it orders too much stock. A single df.duplicated(subset=["order_id"]).sum() after the merge would show the problem at once.
मान लीजिए कोई retailer अपनी North, South और West sales tables जोड़ता है। कुछ orders एक साझा payment system से गुज़रते हैं, इसलिए वे दो tables में दिखते हैं। अब उन क्षेत्रों में माँग असल से ज़्यादा दिखती है, और उस पर बना forecast बहुत ज़्यादा stock मँगाता है। जोड़ने के बाद एक df.duplicated(subset=["order_id"]).sum() समस्या तुरंत दिखा देता।
设想一家零售商合并北区、南区和西区的销售表。部分订单经过共享的支付系统,因此出现在两张表里。这些地区的需求看起来比实际更高,基于此的预测就会订购过多库存。合并后只需一句 df.duplicated(subset=["order_id"]).sum(),就能立刻发现问题。
📋 PICTURE 6 — THREE KINDS OF DUPLICATES, FROM OUR RAW FILEतस्वीर 6 — तीन तरह के DUPLICATES, हमारी RAW FILE से图 6 — 三类重复,来自我们的原始文件
Three kinds of duplicates
Exact duplicateहूबहू duplicate完全重复
id date amount rating 10080 2024-04-15 30,440 5.0 10080 2024-04-15 30,440 5.0 every column identicalहर column एक जैसा所有列都相同 150 such rows in the raw fileraw file में ऐसी 150 rows原始文件中有 150 行 df.drop_duplicates()
Key duplicateKey duplicate键重复
id date amount rating 10739 2024-02-07 7,350 4.0 10739 2024-02-19 7,350 5.0 same id, newer dateवही id, नई date同一 id,日期更新 120 such ids after exact dedupहूबहू dedup के बाद ऐसी 120 ids去除完全重复后仍有 120 个 id subset="customer_id", keep="last"
Fuzzy duplicateFuzzy duplicate模糊重复
"Mumbai"1077 " Mumbai "58 "mumbai"57 "Mum."55 "Bombay"49 5 spellings → 1 city5 वर्तनियाँ → 1 city5 种写法 → 1 个城市 .str.strip().str.lower() + map
# 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()))
OUTPUT
exact duplicates : 150 customer_id order_date purchase_amount 217 10080 2024-04-15 30440.0 2708 10080 2024-04-15 30440.0 832 10201 2024-08-25 7570.0 3175 10201 2024-08-25 7570.0 same customer_id, different values: 240 rows rows now: 5000 | cities: ['Bengaluru', 'Chennai', 'Delhi', 'Kolkata', 'Mumbai', 'Pune']
🔤
A Bug in the Usual Fuzzy Fixआम Fuzzy सुधार में एक Bug常见模糊修复中的一个错误

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 等库计算字符串相似度。

LIVELIVE互动De-duplicate, Step by Stepक़दम-दर-क़दम De-duplicate一步步去重

A small orders table with all three problems. Press the steps in order (or Play) and watch the total sales.

तीनों समस्याओं वाली एक छोटी orders table। क़दम क्रम से दबाएँ (या Play) और कुल बिक्री देखें।

一个包含三类问题的小订单表。按顺序点击各步骤(或播放),观察销售总额。


SectionSection第 06 节

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))
OUTPUT
rows_before 5270 rows_after 5000 rows_removed 270 nulls_remaining 0 dupes_remaining 0 dtype: int64
📊 PICTURE 7 — REAL OUTPUT: BEFORE VS AFTER CLEANINGतस्वीर 7 — असली OUTPUT: सफ़ाई से पहले बनाम बाद图 7 — 真实输出:清洗前后对比
Dataset before and after cleaning
rowsrows行数
5,270 5,000
missing cellsग़ायब cells缺失单元格
6,950 0
exact duplicatesहूबहू duplicates完全重复
150 0
duplicate idsduplicate ids重复 id
270 0
city spellingscity वर्तनियाँ城市写法
15 6 raw fileraw file原始文件 after cleaning (Sections 4–6)सफ़ाई के बाद (Sections 4–6)清洗后(第 4–6 节)

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。把这份报告和清洗后的文件保存在一起。


SectionSection第 07 节

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")])
OUTPUT
5,270 rows → 5,000 | columns dropped: 1 | nulls left: 0 | dupes left: 0 new indicator columns: ['age_missing', 'monthly_income_missing', 'rating_missing']
✅
What It Does Not Doयह क्या नहीं करता它不做什么

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 节的领域规则。


SectionSection第 08 节

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)")
OUTPUT
customers_raw.csv: 5,270 rows temps.csv: 168 hours (6 missing)

SectionSection第 09 节

Golden Rules of Data Cleaning 🏆Data Cleaning के सुनहरे नियम 🏆数据清洗黄金法则 🏆

🎯 9 Rules Every Data Scientist Should Follow9 नियम जो हर Data Scientist को मानने चाहिए每位数据科学家都应遵守的 9 条法则
1
Never edit the raw file. Work on a copy (df = df_raw.copy()) so every mistake can be undone.
Raw file कभी न बदलें। Copy पर काम करें (df = df_raw.copy()) ताकि हर ग़लती सुधारी जा सके।
绝不修改原始文件。在副本上操作(df = df_raw.copy()),这样每个错误都能撤销。
2
Audit first: .info(), .isnull().mean(), .duplicated().sum() and .value_counts() on every text column.
पहले audit: हर text column पर .info(), .isnull().mean(), .duplicated().sum() और .value_counts()।
先审查:对每个文本列运行 .info()、.isnull().mean()、.duplicated().sum() 和 .value_counts()。
3
Ask why values are missing. Compare rows with and without a value; remember that data alone cannot prove MAR vs MNAR.
पूछें कि मान क्यों ग़ायब हैं। मान वाली और बिना मान वाली rows की तुलना करें; याद रखें कि सिर्फ़ data MAR बनाम MNAR साबित नहीं कर सकता।
问清楚值为什么缺失。比较有值和无值的行;记住仅凭数据无法证明是 MAR 还是 MNAR。
4
Add a missing-flag before you fill: df["col_missing"] = df["col"].isnull().astype(int).
भरने से पहले missing-flag जोड़ें: df["col_missing"] = df["col"].isnull().astype(int)।
填充前先加缺失标记:df["col_missing"] = df["col"].isnull().astype(int)。
5
Prefer the median for skewed numbers, and remember every single-value fill shrinks the spread.
Skewed संख्याओं के लिए median बेहतर है, और याद रखें कि हर एक-मान fill फैलाव घटाता है।
偏态数值优先用中位数,并记住任何单值填充都会缩小离散程度。
6
Remove duplicates before you compute fill values, and look at them first: df[df.duplicated(keep=False)].
Fill मान निकालने से पहले duplicates हटाएँ, और पहले उन्हें देखें: df[df.duplicated(keep=False)]।
在计算填充值之前去重,并先查看它们:df[df.duplicated(keep=False)]。
7
Assign results back (df[col] = df[col].fillna(...)). inplace=True on a column does nothing in pandas 3.
नतीजे वापस assign करें (df[col] = df[col].fillna(...))। Column पर inplace=True pandas 3 में कुछ नहीं करता।
把结果赋值回去(df[col] = df[col].fillna(...))。在 pandas 3 中对列用 inplace=True 毫无作用。
8
Fit imputers on training data only, and never use future values (bfill, interpolation) when forecasting.
Imputers सिर्फ़ training data पर fit करें, और forecasting में भविष्य के मान (bfill, interpolation) कभी न इस्तेमाल करें।
只在训练数据上拟合填补器;做预测时绝不使用未来值(bfill、插值)。
9
Validate with checks that fail loudly, and save a short report of every decision next to the clean data.
ज़ोर से fail होने वाली जाँचों से validate करें, और हर फ़ैसले की छोटी report साफ़ data के साथ save करें।
用会明确报错的检查来验证,并把每个决定的简短报告与清洗后的数据保存在一起。
🧮
Key Takeawayमुख्य सीख核心要点

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 की कमियों से नहीं।

数据清洗是项目中许多最重要决策发生的地方。每一次填充、每一行删除都是对现实的一个假设。查清每个问题背后的原因,有意识地选择修复方法,验证它,并记录下来 —— 这样你的模型学到的是真实世界,而不是数据里的缺口。