Data Preparation / Data Preprocessing 📂 Data Collection · 6 of 13 262 min read

Fixing Inconsistent Data & Outlier Detection in Python

A story-driven, hands-on guide to identifying and fixing inconsistent data formats, standardising categories, and detecting and treating outliers using pandas, scipy, and scikit-learn — with live diagrams and real-world examples throughout.

🌐
SectionSection第 01 节

The Hidden Cost of Inconsistent Data 🕵️Inconsistent Data की छिपी क़ीमत 🕵️不一致数据的隐性代价 🕵️

After missing values and duplicates, a quieter problem remains: inconsistency. The values are not empty, the type looks right, and no check fails — but "Mumbai", "MUMBAI", "Mum." and "Bombay" are counted as four different cities, and "05/03/2024" means different days in different systems.

Missing values और duplicates के बाद एक शांत समस्या बचती है: inconsistency। मान ख़ाली नहीं हैं, type सही लगता है, और कोई जाँच fail नहीं होती — पर "Mumbai", "MUMBAI", "Mum." और "Bombay" चार अलग cities गिने जाते हैं, और "05/03/2024" अलग systems में अलग दिन का मतलब रखता है।

处理完缺失值和重复之后,还剩一个更隐蔽的问题:不一致。值不为空,类型看起来也对,没有任何检查报错 —— 但 “Mumbai”、“MUMBAI”、“Mum.” 和 “Bombay” 被当成四个不同的城市,“05/03/2024” 在不同系统里代表不同的日子。

The City Column With 847 Cities847 Cities वाला City Column有 847 个城市的城市列
Imagine a delivery company where sales staff type the city into a phone app. The first groupby("city") shows 847 cities instead of 28. Mumbai alone appears as "mumbai", "MUMBAI", "Mum.", "Bombay", "Mumbay" and more. Every count, average and map is split across these spellings — and nothing raises an error. A few lines of pandas fix it; finding it is the hard part.
मान लीजिए एक delivery कंपनी के sales कर्मचारी phone app में city type करते हैं। पहला groupby("city") 28 की जगह 847 cities दिखाता है। अकेला Mumbai "mumbai", "MUMBAI", "Mum.", "Bombay", "Mumbay" और कई रूपों में है। हर गिनती, औसत और नक्शा इन वर्तनियों में बँट जाता है — और कोई error नहीं आती। कुछ lines का pandas इसे ठीक कर देता है; ढूँढना ही कठिन हिस्सा है।
设想一家配送公司,销售人员在手机应用里手动输入城市。第一次 groupby("city") 显示 847 个城市,而不是 28 个。光是孟买就有 “mumbai”、“MUMBAI”、“Mum.”、“Bombay”、“Mumbay” 等多种写法。每个计数、平均值和地图都被这些写法拆开了 —— 却没有任何报错。几行 pandas 就能修好;难的是发现它。
🔇
Inconsistency Is SilentInconsistency चुप रहती है不一致是无声的

A missing value shows up in isnull(); a wrong type often breaks your code. But "male" vs "M", or a price in dollars next to prices in rupees, passes every automatic check. You have to look for it on purpose — mostly with value_counts(), nunique() and describe().

Missing value isnull() में दिखता है; ग़लत type अक्सर code तोड़ देता है। पर "male" बनाम "M", या रुपयों के बीच डॉलर में कीमत, हर automatic जाँच पार कर जाती है। आपको इसे जानबूझकर ढूँढना पड़ता है — ज़्यादातर value_counts(), nunique() और describe() से।

缺失值会出现在 isnull() 里;类型错误常常会让代码出错。但 “male” 与 “M”、或者卢比价格中夹着的美元价格,能通过所有自动检查。你必须主动去找 —— 主要用 value_counts()、nunique() 和 describe()。

Six Kinds of InconsistencyInconsistency के छह प्रकार六类不一致

🔠 CaseCase (बड़े/छोटे अक्षर)大小写
"Mumbai" / "mumbai" / "MUMBAI"
.str.lower()
.str.lower()
.str.lower()
␣ WhitespaceWhitespace (ख़ाली जगह)空白字符
" Mumbai " / "Delhi "
.str.strip(), .str.replace(r"\s+", " ")
.str.strip(), .str.replace(r"\s+", " ")
.str.strip(), .str.replace(r"\s+", " ")
🏷️ Aliases & typosAliases और typos别名与拼写错误
"Bombay" / "Mum." / "Mumbay"
alias map + careful fuzzy matchingalias map + सावधान fuzzy matching别名表 + 谨慎的模糊匹配
alias map + careful fuzzy matchingalias map + सावधान fuzzy matching别名表 + 谨慎的模糊匹配
alias map + careful fuzzy matchingalias map + सावधान fuzzy matching别名表 + 谨慎的模糊匹配
📅 Date formatsDate formats日期格式
"12/03/2024" / "2024-03-12" / "12-Mar-24"
to_datetime with an explicit formatexplicit format के साथ to_datetime指定格式的 to_datetime
to_datetime with an explicit formatexplicit format के साथ to_datetime指定格式的 to_datetime
to_datetime with an explicit formatexplicit format के साथ to_datetime指定格式的 to_datetime
⚖️ Units & currencyइकाई और मुद्रा单位与货币
500 g / 0.5 kg · $50 / ₹4,175
convert to one unit, keep a unit columnएक इकाई में बदलें, unit column रखें换算成同一单位,保留单位列
convert to one unit, keep a unit columnएक इकाई में बदलें, unit column रखें换算成同一单位,保留单位列
convert to one unit, keep a unit columnएक इकाई में बदलें, unit column रखें换算成同一单位,保留单位列
🔣 Encodings & fake NAsEncodings और नक़ली NA编码与伪缺失值
yes / Y / 1 / "True" · "N/A" / "-" / "none"
map + na_values at load timemap + load के समय na_valuesmap + 读取时设置 na_values
map + na_values at load timemap + load के समय na_valuesmap + 读取时设置 na_values
map + na_values at load timemap + load के समय na_valuesmap + 读取时设置 na_values

All examples below use one practice file, orders_raw.csv (Section 9 builds it), with 3,000 orders from six source systems and all six problems planted on purpose.

नीचे के सभी उदाहरण एक practice file, orders_raw.csv (Section 9 इसे बनाता है), इस्तेमाल करते हैं — छह source systems से 3,000 orders, और छहों समस्याएँ जानबूझकर डाली गई हैं।

下面所有示例都使用一个练习文件 orders_raw.csv(第 9 节生成):来自六个源系统的 3,000 个订单,六类问题都是故意埋入的。

# 1) Diagnose: inconsistencies never raise an error, so you must look for them
import pandas as pd, numpy as np
raw = pd.read_csv("orders_raw.csv")
df = raw.copy()
print("city   unique:", df["city"].nunique(), " (7 real cities + 1 neighbour)")
print("gender unique:", df["gender"].nunique(), "→", sorted(df["gender"].dropna().unique()))
print("order_date dtype:", df["order_date"].dtype, "| e.g.", df.groupby("source")["order_date"].first().tolist())
print("rating dtype:", df["rating"].dtype, "| real NaN:", df["rating"].isna().sum(),
      "| fake-NA text left:", sorted(set(df["rating"].dropna()) - {"1.0", "2.0", "3.0", "4.0", "5.0"}))
print("currencies:", df["currency"].value_counts().to_dict(), "| units:", df["unit"].value_counts().to_dict())
OUTPUT
city unique: 35 (7 real cities + 1 neighbour) gender unique: 9 → ['F', 'Female', 'M', 'MALE', 'Male', 'Other', 'f', 'female', 'male'] order_date dtype: str | e.g. ['20240904', '23-Dec-24', '25.11.2024', 'December 05, 2024', '28/03/2024', '2024-03-14'] rating dtype: str | real NaN: 82 | fake-NA text left: ['-', 'none', 'unknown'] currencies: {'INR': 2756, 'USD': 244} | units: {'g': 2429, 'kg': 571}

SectionSection第 02 节

Fixing Text: Case, Spaces, Aliases and Typos 🔤Text ठीक करना: Case, Spaces, Aliases और Typos 🔤修正文本:大小写、空格、别名和拼写错误 🔤

Apply the steps in a fixed order. Each step makes the next one work: if you map aliases before lowering the case, "BOMBAY" will not match the key "bombay".

क़दम एक तय क्रम में लगाएँ। हर क़दम अगले को काम करने देता है: अगर आप case छोटा करने से पहले aliases map करें, तो "BOMBAY" key "bombay" से मेल नहीं खाएगा।

按固定顺序执行各步骤,每一步都为下一步铺路:如果在转小写之前做别名映射,“BOMBAY” 就匹配不上键 “bombay”。

# 2) Normalise text in a fixed order: lower → strip → squeeze spaces → drop punctuation → aliases
city = (df["city"].str.lower()
          .str.strip()
          .str.replace(r"\s+", " ", regex=True)        # "delhi  " / "new  delhi" → single spaces
          .str.replace(r"[^a-z ]", "", regex=True))    # "mum." → "mum", "hyd." → "hyd"
print("after basic steps:", city.nunique(), "values")
aliases = {"mum": "mumbai", "bombay": "mumbai", "new delhi": "delhi", "blr": "bengaluru", "bangalore": "bengaluru",
           "madras": "chennai", "hyd": "hyderabad", "calcutta": "kolkata"}
city = city.replace(aliases)
print("after alias map  :", city.nunique(), "values")
print(city.value_counts().to_string())
OUTPUT
after basic steps: 21 values after alias map : 13 values city mumbai 670 delhi 546 bengaluru 460 chennai 312 hyderabad 300 pune 271 kolkata 183 dehli 51 mumbay 49 chenai 48 banglore 39 navi mumbai 37 kolkatta 34
🏙️ PICTURE 1 — REAL DATA: SEVEN SPELLINGS OF MUMBAI BECOME ONEतस्वीर 1 — असली DATA: MUMBAI की सात वर्तनियाँ एक हो जाती हैं图 1 — 真实数据:孟买的七种写法合而为一
Seven spellings of Mumbai merge into one raw "city" values for MumbaiMumbai के raw "city" मानMumbai 的原始 “city” 值 "Mumbai" 478 "Mumbay" 49 "MUMBAI" 37 "mumbai" 33 " Mumbai " 31 "mum" 31 "Bombay" 30 "Mum." 30 clean "city" (all 3,000 rows)साफ़ "city" (सभी 3,000 rows)清洗后的 “city”(全部 3,000 行) Mumbai717 Delhi595 Bengaluru497 Chennai359 Hyderabad299 Pune270 Kolkata217 Navi Mumbai37
LIVELIVE互动Text Normaliser — Watch the Categories MergeText Normaliser — Categories को मिलते देखें文本规范化 —— 看类别逐步合并

All 35 raw city values with their counts. Apply the steps one by one. At the fuzzy step, lower the threshold and see what goes wrong.

सभी 35 raw city मान उनकी गिनती के साथ। क़दम एक-एक करके लगाएँ। Fuzzy क़दम पर threshold घटाएँ और देखें क्या ग़लत होता है।

全部 35 个原始城市值及其计数。逐步应用各步骤。在模糊匹配步骤降低阈值,看看会出什么错。

Fuzzy Matching for Typos — CarefullyTypos के लिए Fuzzy Matching — सावधानी से用模糊匹配修正拼写错误 —— 要谨慎

rapidfuzz scores how similar two strings are (0–100). It fixes small typos like "Mumbay" or "Chenai". But a score is not a meaning: "Navi Mumbai" is a real, separate city that scores 71 against "mumbai", and a looser scorer such as WRatio gives it 90. Use a threshold, keep a list of protected values, and review every new match by eye.

rapidfuzz बताता है कि दो strings कितनी मिलती-जुलती हैं (0–100)। यह "Mumbay" या "Chenai" जैसे छोटे typos ठीक करता है। पर score मतलब नहीं है: "Navi Mumbai" एक असली, अलग city है जिसका "mumbai" से score 71 है, और WRatio जैसा ढीला scorer इसे 90 देता है। Threshold रखें, सुरक्षित मानों की list रखें, और हर नए मेल को आँख से जाँचें।

rapidfuzz 给两个字符串的相似度打分(0–100),能修正 “Mumbay”、“Chenai” 这类小错误。但分数不等于含义:“Navi Mumbai” 是一个真实存在的独立城市,与 “mumbai” 的得分为 71,而 WRatio 这类宽松的评分器会给出 90。要设阈值、保留受保护值列表,并人工检查每个新匹配。

# 3) Fuzzy matching for typos — with a threshold, a review list, and a protected list
from rapidfuzz import process, fuzz
canonical = ["mumbai", "delhi", "bengaluru", "chennai", "hyderabad", "pune", "kolkata"]
protected = {"navi mumbai"}                                   # real places that LOOK like typos

def fuzzy_fix(val, threshold=80):
    if val in canonical or val in protected:
        return val
    match, score, _ = process.extractOne(val, canonical, scorer=fuzz.ratio)
    return match if score >= threshold else val

for v in ["mumbay", "dehli", "chenai", "kolkatta", "banglore", "navi mumbai"]:
    m, s, _ = process.extractOne(v, canonical, scorer=fuzz.ratio)
    print(f"{v:<12} best: {m:<10} score {s:5.1f} → {fuzzy_fix(v)}")
city = city.map(fuzzy_fix).replace({"banglore": "bengaluru"})   # fuzzy missed it: add to the alias list after review
df["city"] = city.str.title()
print("final:", df["city"].nunique(), "cities →", sorted(df["city"].unique()))
OUTPUT
mumbay best: mumbai score 83.3 → mumbai dehli best: delhi score 80.0 → delhi chenai best: chennai score 92.3 → chennai kolkatta best: kolkata score 93.3 → kolkata banglore best: bengaluru score 58.8 → banglore navi mumbai best: mumbai score 70.6 → navi mumbai final: 8 cities → ['Bengaluru', 'Chennai', 'Delhi', 'Hyderabad', 'Kolkata', 'Mumbai', 'Navi Mumbai', 'Pune']

SectionSection第 03 节

Fixing Dates From Several Systems 📅कई Systems से आई Dates ठीक करना 📅修正来自多个系统的日期 📅

The Timeline That Was Sorted Alphabeticallyवह Timeline जो वर्णमाला क्रम में Sort हुई按字母顺序排序的时间线
A sales team merges dates from a shop till ("28/03/2024"), a web store ("2024-03-14"), an old ERP ("23-Dec-24") and a finance tool ("07.09.2024"). pandas reads them all as text, so sorting by date is really sorting by characters: "01/04/2024" comes before "2024-03-15", which comes before "23-Dec-24". The training window becomes a random mix of months. The usual one-line fix, shown below, makes it worse in a different way.
एक sales team दुकान के till ("28/03/2024"), web store ("2024-03-14"), पुराने ERP ("23-Dec-24") और finance tool ("07.09.2024") से dates जोड़ती है। pandas इन सबको text के रूप में पढ़ता है, इसलिए date से sort करना असल में अक्षरों से sort करना है: "01/04/2024" "2024-03-15" से पहले आता है, जो "23-Dec-24" से पहले आता है। Training window महीनों का random मिश्रण बन जाती है। नीचे दिखाया आम एक-line सुधार इसे दूसरे तरीक़े से और बिगाड़ता है।
销售团队合并了来自门店收银(“28/03/2024”)、网店(“2024-03-14”)、旧 ERP(“23-Dec-24”)和财务工具(“07.09.2024”)的日期。pandas 把它们都读成文本,所以“按日期排序”其实是按字符排序:“01/04/2024” 排在 “2024-03-15” 前面,后者又排在 “23-Dec-24” 前面。训练窗口变成了各个月份的随机混合。下面那个常见的一行修复,会以另一种方式让情况更糟。
# 4) Dates from six systems. The usual one-liner silently loses most of them:
quick = pd.to_datetime(df["order_date"], dayfirst=True, errors="coerce")
print(f"to_datetime(dayfirst=True, errors='coerce'): {quick.isna().sum():,} of {len(df):,} became NaT")

mixed = pd.to_datetime(df["order_date"], format="mixed", dayfirst=True, errors="coerce")
truth = pd.read_csv("true_dates.csv", parse_dates=["true_date"])["true_date"]
print(f"format='mixed', dayfirst=True : {mixed.isna().sum()} NaT, but {(mixed != truth).sum()} WRONG dates (ISO days/months swapped)")

# Safe way: one explicit format per source
fmt = {"pos": "%d/%m/%Y", "web": "%Y-%m-%d", "erp": "%d-%b-%y", "manual": "%B %d, %Y", "finance": "%d.%m.%Y", "api": "%Y%m%d"}
parsed = pd.Series(pd.NaT, index=df.index, dtype="datetime64[ns]")
for s, f in fmt.items():
    rows = df["source"] == s
    parsed[rows] = pd.to_datetime(df.loc[rows, "order_date"], format=f, errors="coerce")
print(f"explicit format per source     : {parsed.isna().sum()} NaT, {(parsed != truth).sum()} wrong")
df["order_date"] = parsed
df["month"] = df["order_date"].dt.month
df["is_weekend"] = df["order_date"].dt.dayofweek >= 5
OUTPUT
to_datetime(dayfirst=True, errors='coerce'): 2,143 of 3,000 became NaT format='mixed', dayfirst=True : 0 NaT, but 328 WRONG dates (ISO days/months swapped) explicit format per source : 0 NaT, 0 wrong
📅 PICTURE 2 — REAL OUTPUT: THREE WAYS TO PARSE 3,000 DATESतस्वीर 2 — असली OUTPUT: 3,000 DATES PARSE करने के तीन तरीके图 2 — 真实输出:解析 3,000 个日期的三种方法
Date parsing results by method dayfirst=True, errors="coerce" 857 2,143 format="mixed", dayfirst=True 2,672 328 explicit format per source 3,000 correctसही正确 wrong date (silent!)ग़लत date (चुपचाप!)日期错误(无提示!) NaT — lostNaT — खोईNaT —— 丢失

The first method loses 71% of dates as NaT (pandas does print a UserWarning). The second loses none but gets 328 dates wrong with no warning — the most dangerous result. Explicit formats per source are both complete and correct.

पहला तरीका 71% dates NaT के रूप में खो देता है (pandas UserWarning ज़रूर दिखाता है)। दूसरा कुछ नहीं खोता पर 328 dates बिना चेतावनी ग़लत करता है — सबसे ख़तरनाक नतीजा। हर source का explicit format पूरा और सही दोनों है।

第一种方法有 71% 的日期变成 NaT(pandas 会打印 UserWarning)。第二种一个都没丢,却在没有任何警告的情况下解析错了 328 个 —— 这是最危险的结果。按来源指定格式,既完整又正确。

LIVELIVE互动Date Parser — Which Method Is Safe?Date Parser — कौन सा तरीका सुरक्षित है?日期解析 —— 哪种方法安全?

Nine real rows from six systems. Switch the parsing method and the sort order.

छह systems से नौ असली rows। Parsing का तरीका और sort का क्रम बदलें।

来自六个系统的九行真实数据。切换解析方法和排序方式。

method
sort

🕐
Safe Date HabitsDates की सुरक्षित आदतें日期处理的安全习惯

(1) Keep a source column when you merge files, so you know each row's format. (2) Parse with an explicit format=. (3) Use errors="coerce" and then count the NaT rows — never ignore them. (4) Compare the parsed min/max date and monthly counts with what you expect. (5) Store dates as ISO YYYY-MM-DD from then on.

(1) Files जोड़ते समय source column रखें, ताकि हर row का format पता रहे। (2) Explicit format= के साथ parse करें। (3) errors="coerce" इस्तेमाल करें और फिर NaT rows गिनें — उन्हें कभी नज़रअंदाज़ न करें। (4) Parse की गई min/max date और महीने की गिनती अपनी उम्मीद से मिलाएँ। (5) इसके बाद dates ISO YYYY-MM-DD में रखें।

(1) 合并文件时保留 source 列,这样就知道每行的格式。(2) 用明确的 format= 解析。(3) 使用 errors="coerce",然后统计 NaT 行数 —— 绝不忽略。(4) 把解析后的最早/最晚日期和每月计数与预期对比。(5) 此后统一以 ISO 格式 YYYY-MM-DD 存储日期。


SectionSection第 04 节

Units, Currencies, Codes and Fake NAs ⚖️इकाइयाँ, मुद्राएँ, Codes और नक़ली NA ⚖️单位、货币、编码与伪缺失值 ⚖️

These values look like valid numbers, so they pass range checks — but they are off by a fixed factor. A weight of 0.5 in kg next to 500 in grams, or $12 next to ₹1,000, quietly breaks every sum and average.

ये मान सही संख्याओं जैसे दिखते हैं, इसलिए range जाँच पार कर जाते हैं — पर एक तय गुणक से ग़लत होते हैं। Grams में 500 के बगल में kg में 0.5, या ₹1,000 के बगल में $12, हर जोड़ और औसत को चुपचाप बिगाड़ देता है।

这些值看起来是有效数字,能通过范围检查 —— 但相差一个固定倍数。以千克记的 0.5 挨着以克记的 500,或 $12 挨着 ₹1,000,会悄悄破坏每个求和与平均值。

# 5) Fake NAs, yes/no codes, currencies, units, gender
df = df.copy()
df["rating"] = pd.to_numeric(df["rating"].replace(["N/A", "NA", "-", "none", "unknown"], np.nan))
# (better: pd.read_csv("orders_raw.csv", na_values=["N/A", "NA", "-", "none", "unknown"]))
yes_no = {"yes": True, "y": True, "1": True, "true": True, "no": False, "n": False, "0": False, "false": False}
df["is_returned"] = df["is_returned"].astype(str).str.strip().str.lower().map(yes_no)
usd = df["currency"] == "USD"
df.loc[usd, "amount"] = (df.loc[usd, "amount"] * 83.5).round(2)    # use the rate for each order's date in real work
df["currency"] = "INR"
kg = df["unit"].str.lower() == "kg"
df.loc[kg, "weight"] = df.loc[kg, "weight"] * 1000
df["unit"] = "g"
g = df["gender"].str.strip().str.lower().map({"m": "Male", "male": "Male", "f": "Female", "female": "Female", "other": "Other"})
df["gender"] = g.fillna("Unknown")
print("rating:", df["rating"].dtype, "missing", df["rating"].isna().sum(), "| is_returned:", df["is_returned"].value_counts().to_dict())
print("gender:", df["gender"].value_counts().to_dict())
print(f"median amount ₹{df['amount'].median():,.0f} | median weight {df['weight'].median():,.0f} g")
OUTPUT
rating: float64 missing 183 | is_returned: {False: 2673, True: 327} gender: {'Male': 1488, 'Female': 1414, 'Unknown': 50, 'Other': 48} median amount ₹2,970 | median weight 1,008 g
💡
Catch Fake NAs When You Load the FileFile Load करते समय नक़ली NA पकड़ें读取文件时就捕获伪缺失值

By default read_csv already treats "NA", "N/A", "null" and empty cells as missing — but not "none", "-" or "unknown". Add them: pd.read_csv(path, na_values=["-", "none", "unknown"]). Matching is exact and case-sensitive, so list the spellings that really occur (check with value_counts()). Also note: .astype(str) turns a real NaN into the text "nan", and a float 1.0 into "1.0" — so build yes/no maps from the values you actually see.

Default रूप से read_csv "NA", "N/A", "null" और ख़ाली cells को पहले ही missing मानता है — पर "none", "-" या "unknown" को नहीं। इन्हें जोड़ें: pd.read_csv(path, na_values=["-", "none", "unknown"])। मिलान हूबहू और case-sensitive है, इसलिए वही वर्तनियाँ लिखें जो सच में आती हैं (value_counts() से जाँचें)। ध्यान दें: .astype(str) असली NaN को text "nan" बना देता है, और float 1.0 को "1.0" — इसलिए yes/no maps उन मानों से बनाएँ जो आप सच में देखते हैं।

默认情况下,read_csv 已经把 “NA”、“N/A”、“null” 和空单元格当作缺失值 —— 但不包括 “none”、“-” 或 “unknown”。请加上:pd.read_csv(path, na_values=["-", "none", "unknown"])。匹配是精确且区分大小写的,所以要列出实际出现的写法(用 value_counts() 检查)。另外注意:.astype(str) 会把真正的 NaN 变成文本 “nan”,把浮点数 1.0 变成 “1.0” —— 所以 yes/no 映射要依据你实际看到的值来写。

📊 PICTURE 3 — REAL OUTPUT: DISTINCT VALUES BEFORE AND AFTERतस्वीर 3 — असली OUTPUT: पहले और बाद के अलग मान图 3 — 真实输出:清洗前后的不同值数量
Distinct values before and after cleaning city 35 8 gender 10 4 order_date formats 6 1 rating "NA" spellings 4 1 is_returned codes 8 2 currency 2 1 weight unit 2 1 raw fileraw file原始文件 after Sections 2–4Sections 2–4 के बाद第 2–4 节之后

SectionSection第 05 节

Outliers — Error or Important Fact? 🎯Outliers — ग़लती या ज़रूरी तथ्य? 🎯异常值 —— 错误还是重要事实?🎯

An outlier is a value far from most of the data. "Far" depends on the shape of the data and on the domain. Before choosing a method, ask the key question: is this a mistake or a real extreme?

Outlier वह मान है जो ज़्यादातर data से दूर हो। "दूर" data के आकार और domain पर निर्भर है। तरीका चुनने से पहले मुख्य सवाल पूछें: क्या यह ग़लती है या असली अति मान?

异常值是远离大部分数据的值。“远”取决于数据的形状和所属领域。选择方法之前,先问关键问题:这是错误还是真实的极端值?

The Model That Never Saw a Big Customerवह Model जिसने कभी बड़ा ग्राहक नहीं देखा从没见过大客户的模型
Imagine an analyst building a churn model. She removes every row beyond the IQR fence on "annual revenue" without looking. One removed row is the company's largest client. The model now has no examples of very large customers at all, so its predictions for them are guesses — and that client is the one the business most needs to keep. Inspecting the flagged rows for five minutes would have prevented it.
मान लीजिए एक analyst churn model बना रही है। वह बिना देखे "annual revenue" पर IQR fence से बाहर की हर row हटा देती है। हटाई गई एक row कंपनी का सबसे बड़ा client है। अब model के पास बहुत बड़े ग्राहकों का कोई उदाहरण नहीं, इसलिए उनके बारे में उसके अनुमान बस अंदाज़े हैं — और वही client है जिसे business को सबसे ज़्यादा बचाए रखना है। चिह्नित rows को पाँच मिनट देखना इसे रोक देता।
设想一位分析师在构建客户流失模型。她没有查看就删掉了“年收入”超出 IQR 围栏的所有行,其中一行恰好是公司最大的客户。模型从此完全没有超大客户的样本,对他们的预测只能靠猜 —— 而这正是公司最需要留住的客户。花五分钟检查被标记的行,就能避免这一切。
🗺️ PICTURE 4 — THE OUTLIER DECISION FRAMEWORKतस्वीर 4 — OUTLIER फ़ैसले का ढाँचा图 4 — 异常值决策框架
Outlier decision framework A value is flagged. Why is it there?एक मान चिह्नित हुआ। यह यहाँ क्यों है?某个值被标记了。它为什么在这里? Data errorData की ग़लती数据错误 age = 999, ₹331k for a ₹3.3k itemage = 999, ₹3.3k चीज़ के लिए ₹331kage = 999,₹3.3k 的商品记成 ₹331k Fix it from the source, or remove itSource से ठीक करें, या हटाएँ从源头修正,或删除 Real extremeअसली अति मान真实极端值 ₹42 lakh enterprise order₹42 लाख का enterprise order₹420 万的企业订单 Keep it; use robust methods or a logरखें; robust तरीके या log इस्तेमाल करें保留;用稳健方法或取对数 Not sureपक्का नहीं不确定 odd weight, odd rating combinationअजीब weight और rating का मेल重量和评分组合异常 Ask a domain expert; flag it meanwhileDomain expert से पूछें; तब तक flag रखें请教领域专家;先打上标记 Statistics can only FLAG a value. Deciding what it means needs domain knowledge.Statistics सिर्फ़ किसी मान को चिह्नित कर सकता है। उसका मतलब तय करने के लिए domain knowledge चाहिए।统计方法只能标记一个值。判断它的含义需要领域知识。 Never delete automatically — look at the flagged rows first.कभी अपने आप न हटाएँ — पहले चिह्नित rows देखें।绝不自动删除 —— 先查看被标记的行。

SectionSection第 06 节

Outlier Detection Methods 🔎Outlier पहचानने के तरीके 🔎异常值检测方法 🔎

IQR rule 📦IQR नियम 📦IQR 规则 📦
x < Q1 − 1.5·IQR or x > Q3 + 1.5·IQR
Uses quartiles, so it does not assume a bell curve. A good default — but on skewed data it flags many real values in the long tail.
Quartiles इस्तेमाल करता है, इसलिए घंटी-आकार नहीं मानता। अच्छा default — पर skewed data पर लंबी tail के कई असली मान चिह्नित करता है।
使用四分位数,不假设钟形分布。是不错的默认选择 —— 但在偏态数据上会把长尾中的许多真实值标记出来。
z-score 📐z-score 📐z 分数 📐
|x − mean| / sd > 3
Assumes a roughly normal shape. The extremes inflate the mean and sd they are measured with, so big outliers can hide others (masking).
लगभग normal आकार मानता है। अति मान उसी mean और sd को फुलाते हैं जिनसे वे मापे जाते हैं, इसलिए बड़े outliers दूसरों को छिपा सकते हैं (masking)।
假设分布大致正态。极端值会抬高用来衡量它们的均值和标准差,所以大的异常值会掩盖其他异常值(掩蔽效应)。
Modified z 🛡️Modified z 🛡️修正 z 分数 🛡️
0.6745·|x − median| / MAD > 3.5
Uses the median and the median absolute deviation, which barely move when extremes appear (Iglewicz & Hoaglin). If MAD is 0, it divides by zero — check first.
Median और median absolute deviation इस्तेमाल करता है, जो अति मान आने पर मुश्किल से हिलते हैं (Iglewicz और Hoaglin)। अगर MAD 0 हो तो शून्य से भाग होता है — पहले जाँचें।
使用中位数和中位数绝对偏差,极端值出现时它们几乎不变(Iglewicz 与 Hoaglin)。若 MAD 为 0 会出现除以零 —— 先检查。
Isolation Forest 🌲Isolation Forest 🌲孤立森林 🌲
short average path in random treesrandom trees में छोटा औसत path在随机树中平均路径短
Looks at several columns together, so it finds odd combinations. You choose contamination, which sets how many rows it flags.
कई columns एक साथ देखता है, इसलिए अजीब मेल ढूँढता है। contamination आप चुनते हैं, जो तय करता है कितनी rows चिह्नित हों।
同时考虑多列,因此能发现异常的组合。contamination 由你设定,它决定标记多少行。

Method 1 — IQR, Then Lookतरीका 1 — IQR, फिर देखें方法 1 —— IQR,然后查看

# 6) IQR fences — then LOOK at what was flagged
x = df["amount"]
q1, q3 = x.quantile([0.25, 0.75]); iqr = q3 - q1
lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
df["out_iqr"] = (x < lo) | (x > hi)
print(f"fences: ₹{lo:,.0f} to ₹{hi:,.0f} | flagged {df['out_iqr'].sum()} rows ({df['out_iqr'].mean():.1%})")
print(df[df["out_iqr"]].nlargest(5, "amount")[["order_id", "city", "amount"]].to_string(index=False))
print("age errors:", df.loc[~df["age"].between(18, 100), "age"].value_counts().to_dict())
OUTPUT
fences: ₹-3,575 to ₹10,545 | flagged 171 rows (5.7%) order_id city amount 2501 Pune 4200000.00 1334 Bengaluru 510000.00 223 Delhi 444000.40 12 Chennai 330999.85 975 Chennai 34170.00 age errors: {999.0: 6}

Method 2 — z-Score and Modified z-Scoreतरीका 2 — z-Score और Modified z-Score方法 2 —— z 分数与修正 z 分数

# 7) z-score vs modified z-score (median + MAD)
from scipy import stats
z = np.abs(stats.zscore(x, nan_policy="omit"))          # same length as df — safe to assign
df["out_z"] = z > 3
med = x.median(); mad = (x - med).abs().median()
df["out_modz"] = (0.6745 * (x - med) / mad).abs() > 3.5
print(f"z > 3 : {df['out_z'].sum():3d} rows   (mean ₹{x.mean():,.0f}, std ₹{x.std():,.0f} — inflated by the extremes)")
print(f"mod-z : {df['out_modz'].sum():3d} rows   (median ₹{med:,.0f}, MAD ₹{mad:,.0f})")
x_rest = x.drop(x.nlargest(4).index)                    # set the 4 biggest values aside
print(f"z > 3 on the other rows: {(np.abs(stats.zscore(x_rest)) > 3).sum()} rows  ← the 4 giants inflated std and hid these (masking)")
OUTPUT
z > 3 : 4 rows (mean ₹5,868, std ₹77,882 — inflated by the extremes) mod-z : 154 rows (median ₹2,970, MAD ₹1,520) z > 3 on the other rows: 56 rows ← the 4 giants inflated std and hid these (masking)
🐛
Two Bugs in the Usual Codeआम Code में दो Bugs常见代码中的两个错误

stats.zscore(df[col].dropna()) returns fewer values than the DataFrame has rows if anything is missing, so assigning it back raises an error (or misaligns). Use nan_policy="omit" on the full column instead. The same applies to Isolation Forest: fit_predict on features.dropna() must be written back with df.loc[ok, ...], as in the next block.

अगर कुछ भी ग़ायब हो तो stats.zscore(df[col].dropna()) DataFrame की rows से कम मान लौटाता है, इसलिए वापस assign करने पर error आती है (या मान ग़लत जगह बैठते हैं)। इसकी जगह पूरे column पर nan_policy="omit" इस्तेमाल करें। Isolation Forest पर भी यही लागू है: features.dropna() पर fit_predict को df.loc[ok, ...] से वापस लिखना चाहिए, जैसा अगले block में है।

如果有缺失值,stats.zscore(df[col].dropna()) 返回的值比 DataFrame 的行数少,赋值回去会报错(或错位)。应对整列使用 nan_policy="omit"。孤立森林也一样:对 features.dropna() 调用 fit_predict 后,必须用 df.loc[ok, ...] 写回,如下一段代码所示。

Method 3 — Isolation Forest (Several Columns)तरीका 3 — Isolation Forest (कई Columns)方法 3 —— 孤立森林(多列)

# 8) Isolation Forest — several columns at once
from sklearn.ensemble import IsolationForest
feats = ["age", "amount", "weight", "rating"]
ok = df[feats].notna().all(axis=1)                      # IsolationForest cannot take NaN
X = df.loc[ok, feats].assign(amount=np.log1p(df.loc[ok, "amount"]))
iso = IsolationForest(n_estimators=200, contamination=0.01, random_state=42)
df["out_iso"] = False
df.loc[ok, "out_iso"] = iso.fit_predict(X) == -1        # align by index, not by position
df.loc[ok, "anomaly_score"] = iso.score_samples(X)
print("IsolationForest flagged:", int(df["out_iso"].sum()), "rows (contamination=1% sets this number)")
print(df[df["out_iso"]].nsmallest(5, "anomaly_score")[["order_id", "age", "amount", "weight", "rating"]].to_string(index=False))
OUTPUT
IsolationForest flagged: 29 rows (contamination=1% sets this number) order_id age amount weight rating 223 25.0 444000.40 833.0 1.0 2860 25.0 2660.00 4895.0 2.0 1843 35.0 13060.24 4160.0 2.0 2501 23.0 4200000.00 71.0 5.0 876 18.0 950.00 5284.0 4.0
🔎 PICTURE 5 — REAL OUTPUT: WHAT EACH RULE FLAGS ON 3,000 ORDERSतस्वीर 5 — असली OUTPUT: 3,000 ORDERS पर हर नियम क्या चिह्नित करता है图 5 — 真实输出:每种规则在 3,000 个订单上标记了什么
Amounts on a log scale with outlier rules IQR (1.5×): 171 flagged z > 3: 4 flagged modified z > 3.5: 154 flagged ₹100 ₹1k ₹10k ₹1 lakh ₹10 lakh IQR fence ₹10,545IQR fence ₹10,545IQR 围栏 ₹10,545 order amount in ₹ (log scale) · every 8th normal order shown; all flagged orders shownorder राशि ₹ में (log scale) · हर आठवाँ सामान्य order दिखाया; सभी चिह्नित orders दिखाए订单金额(₹,对数刻度)· 正常订单每 8 个显示 1 个;所有被标记的订单都显示

IQR and modified z flag the long right tail (mostly real, large orders). Plain z flags only the four giants, because they inflate the standard deviation to ₹77,882.

IQR और modified z लंबी right tail चिह्नित करते हैं (ज़्यादातर असली, बड़े orders)। सादा z सिर्फ़ चार दिग्गज चिह्नित करता है, क्योंकि वे standard deviation को ₹77,882 तक फुला देते हैं।

IQR 和修正 z 标记了右侧长尾(大多是真实的大订单)。普通 z 只标记了四个巨额值,因为它们把标准差抬到了 ₹77,882。

LIVELIVE互动Three Rules, One Datasetतीन नियम, एक Dataset三种规则,一个数据集

200 values. Switch between bell-shaped and skewed data, then add one giant value and watch the z-score fences move.

200 मान। घंटी जैसे और skewed data में बदलें, फिर एक बहुत बड़ा मान जोड़ें और z-score fences को खिसकते देखें।

200 个值。在钟形和偏态数据之间切换,然后加入一个巨大值,观察 z 分数边界如何移动。


SectionSection第 07 节

Treating Outliers 🛠️Outliers का इलाज 🛠️处理异常值 🛠️

Strategyरणनीति策略CodeCode代码Use whenकब适用情况Riskजोखिम风险
Removeहटाएँ删除df = df[~is_error]confirmed errors onlyसिर्फ़ पक्की ग़लतियाँ仅限确认的错误High if the value was realमान असली हो तो ज़्यादा若值是真实的则风险高
Cap (winsorise)Cap (winsorise)截断(缩尾).clip(p01, p99)real extremes, linear modelsअसली अति मान, linear models真实极端值、线性模型Medium: big values lose their sizeमध्यम: बड़े मान अपना आकार खोते हैं中:大值失去原有大小
TransformTransform变换np.log1p(x)long right tail (money, counts)लंबी right tail (पैसा, गिनती)右侧长尾(金额、计数)Low; remember to transform backकम; वापस बदलना याद रखें低;记得转换回去
Keep + robust toolsरखें + robust tools保留 + 稳健工具RobustScaler, tree modelsreal extremesअसली अति मान真实极端值Lowकम低
# 9) Treatments: remove errors, cap, log, Box-Cox, robust scaling
from scipy.stats import boxcox
from sklearn.preprocessing import RobustScaler, StandardScaler
err = ~df["age"].between(18, 100) | df["order_id"].isin([12, 223, 1334])   # confirmed errors only
df = df[~err].copy()
print(f"removed {err.sum()} confirmed errors → {len(df):,} rows")
a = df["amount"]
p01, p99 = a.quantile([0.01, 0.99])
print(f"skew raw {a.skew():6.2f} | clipped 1–99% {a.clip(p01, p99).skew():5.2f} | log1p {np.log1p(a).skew():5.2f}", end="")
bc, lam = boxcox(a + 1)
print(f" | Box-Cox {pd.Series(bc).skew():5.2f} (λ = {lam:.3f})")
typical = a.between(a.quantile(0.25), a.quantile(0.75))
for name, sc in [("StandardScaler", StandardScaler()), ("RobustScaler", RobustScaler())]:
    v = sc.fit_transform(df[["amount"]]).ravel()
    print(f"{name:<14} middle 50% of orders span {v[typical].min():6.2f} to {v[typical].max():5.2f}   largest order → {v.max():8.1f}")
OUTPUT
removed 9 confirmed errors → 2,991 rows skew raw 54.51 | clipped 1–99% 1.88 | log1p 0.15 | Box-Cox -0.01 (λ = -0.039) StandardScaler middle 50% of orders span -0.05 to -0.00 largest order → 54.6 RobustScaler middle 50% of orders span -0.35 to 0.65 largest order → 1189.0
📉 PICTURE 6 — REAL OUTPUT: log1p TURNS A SKEW OF 54 INTO 0.15तस्वीर 6 — असली OUTPUT: log1p 54 के SKEW को 0.15 बनाता है图 6 — 真实输出:log1p 把偏度从 54 降到 0.15
Histogram before and after log1p skew = 54.51 raw amount (₹, values above ₹40k shown at the edge)raw राशि (₹, ₹40k से ऊपर के मान किनारे पर)原始金额(₹,超过 ₹40k 的值画在边缘) ₹0₹40k+ skew = 0.15 np.log1p(amount) 5.415.3

Linear regression does not need normal features — it assumes the errors are roughly normal. But a log often makes relationships more linear and stops a few huge values from dominating the fit.

Linear regression को normal features नहीं चाहिए — यह मानता है कि errors लगभग normal हैं। पर log अक्सर संबंधों को ज़्यादा linear बनाता है और कुछ बहुत बड़े मानों को fit पर हावी होने से रोकता है।

线性回归并不要求特征服从正态分布 —— 它假设的是误差近似正态。但取对数常能让关系更接近线性,并防止少数巨大值主导拟合。

LIVELIVE互动Compare Treatmentsइलाजों की तुलना करें比较各种处理方式

400 randomly chosen real orders from the cleaned file. Try each treatment.

साफ़ file से random चुने गए 400 असली orders। हर इलाज आज़माएँ।

从清洗后的文件中随机抽取的 400 笔真实订单。逐一尝试各种处理。

LIVELIVE互动StandardScaler vs RobustScalerStandardScaler बनाम RobustScalerStandardScaler 与 RobustScaler

40 typical orders. Drag the size of one extra, very large order.

40 आम orders। एक अतिरिक्त, बहुत बड़े order का आकार खींचें।

40 笔典型订单。拖动一笔额外巨额订单的大小。


SectionSection第 08 节

Reusable Helpers 🧰दोबारा इस्तेमाल लायक़ Helpers 🧰可复用的辅助函数 🧰

These helpers do the routine part — normalise text and flag outliers with a report. They deliberately do not cap or delete anything: that decision needs the inspection steps above. Note that the IQR rule flags 8 people aged 65–80 here; they are real customers, not errors.

ये helpers नियमित हिस्सा करते हैं — text normalise करना और report के साथ outliers चिह्नित करना। ये जानबूझकर कुछ cap या delete नहीं करते: उस फ़ैसले के लिए ऊपर के जाँच वाले क़दम ज़रूरी हैं। ध्यान दें कि IQR नियम यहाँ 65–80 साल के 8 लोगों को चिह्नित करता है; वे असली ग्राहक हैं, ग़लतियाँ नहीं।

这些辅助函数完成常规工作 —— 规范化文本,并在报告中标记异常值。它们故意不做截断或删除:那需要上面的检查步骤来决定。注意,这里 IQR 规则标记了 8 位 65–80 岁的顾客;他们是真实顾客,不是错误。

# 10) Reusable helpers — they FLAG and report; you decide what to change
def normalise_text(df, cols, aliases=None):
    out = df.copy()
    for c in cols:
        s = (out[c].astype("string").str.lower().str.strip()
               .str.replace(r"\s+", " ", regex=True).str.replace(r"[^\w ]", "", regex=True))
        out[c] = s.replace((aliases or {}).get(c, {}))
    return out

def flag_outliers(df, cols, k=1.5):
    out, rows = df.copy(), []
    for c in cols:
        q1, q3 = out[c].quantile([0.25, 0.75]); lo, hi = q1 - k * (q3 - q1), q3 + k * (q3 - q1)
        out[c + "_outlier"] = ~out[c].between(lo, hi) & out[c].notna()
        rows.append({"column": c, "low": round(lo, 1), "high": round(hi, 1), "flagged": int(out[c + "_outlier"].sum())})
    return out, pd.DataFrame(rows).set_index("column")

test = normalise_text(raw, ["city"], {"city": {"bombay": "mumbai", "mum": "mumbai"}})
print("city values:", raw["city"].nunique(), "→", test["city"].nunique())
_, rep = flag_outliers(df, ["amount", "age", "weight"])
print(rep)
OUTPUT
city values: 35 → 19 low high flagged column amount -3575.0 10545.0 168 age 8.0 64.0 8 weight -991.2 3174.8 105

SectionSection第 09 节

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

Run this first. It plants all the problems used above: 35 city spellings (and the real city Navi Mumbai), nine gender codes, six date formats from six systems, USD next to INR, kg next to g, eight yes/no codes, fake "NA" strings, six ages of 999, three amounts with two extra zeros, and one genuine ₹42 lakh order.

इसे पहले चलाएँ। यह ऊपर इस्तेमाल हुई सभी समस्याएँ डालता है: 35 city वर्तनियाँ (और असली city Navi Mumbai), नौ gender codes, छह systems से छह date formats, INR के बगल में USD, g के बगल में kg, आठ yes/no codes, नक़ली "NA" strings, 999 वाली छह उम्रें, दो अतिरिक्त शून्य वाली तीन राशियाँ, और एक असली ₹42 लाख का order।

请先运行它。它埋入了上面用到的所有问题:35 种城市写法(以及真实城市 Navi Mumbai)、9 种性别编码、六个系统的六种日期格式、INR 中夹着 USD、g 中夹着 kg、8 种是/否编码、伪 “NA” 字符串、6 个 999 岁的年龄、3 个多了两个零的金额,以及一笔真实的 ₹420 万订单。

# 0) Build a practice file with planted inconsistencies and outliers
import numpy as np, pandas as pd
rng = np.random.default_rng(21)
n = 3000
city = rng.choice(["Mumbai", "Delhi", "Bengaluru", "Chennai", "Hyderabad", "Pune", "Kolkata"], n, p=[.25, .2, .17, .12, .1, .09, .07])
dirty = {"Mumbai": ["mumbai", "MUMBAI", " Mumbai ", "Mum.", "mum", "Bombay", "Mumbay"],
         "Delhi": ["delhi", "DELHI", "New Delhi", "Dehli", "Delhi  "],
         "Bengaluru": ["Bangalore", "bengaluru", "BLR", "Banglore"],
         "Chennai": ["chennai", "Madras", "Chenai"], "Hyderabad": ["hyderabad", "Hyd.", "HYD"],
         "Pune": ["pune", "PUNE "], "Kolkata": ["Calcutta", "kolkata", "Kolkatta"]}
city = np.array([rng.choice(dirty[c]) if rng.random() < 0.35 else c for c in city], dtype=object)
city[rng.random(n) < 0.01] = "Navi Mumbai"                       # a REAL, different city
gender = rng.choice(["Male", "Female", "male", "female", "M", "F", "MALE", "f", "Other", "N/A"], n,
                    p=[.3, .3, .08, .08, .07, .07, .03, .03, .02, .02])

# Dates from six systems, each with its own format
src = rng.choice(["pos", "web", "erp", "manual", "finance", "api"], n, p=[.3, .3, .15, .1, .1, .05])
true_date = pd.Timestamp("2024-01-01") + pd.to_timedelta(rng.integers(0, 366, n), unit="D")
fmt = {"pos": "%d/%m/%Y", "web": "%Y-%m-%d", "erp": "%d-%b-%y", "manual": "%B %d, %Y", "finance": "%d.%m.%Y", "api": "%Y%m%d"}
order_date = [d.strftime(fmt[s]) for d, s in zip(true_date, src)]

amount_inr = np.round(np.exp(rng.normal(8.0, 0.8, n)), -1)       # skewed spend, ₹
amount_inr[[11, 222, 1333]] *= 100                               # typing errors: two extra zeros
amount_inr[2500] = 4_200_000                                      # one genuine enterprise order
currency = np.where(rng.random(n) < 0.08, "USD", "INR")
amount = np.where(currency == "USD", np.round(amount_inr / 83.5, 2), amount_inr)
age = np.clip(np.round(rng.normal(36, 10, n)), 18, 80)
age[rng.choice(n, 6, replace=False)] = 999                        # impossible ages
weight = np.round(rng.gamma(2, 600, n))                           # grams
unit = np.where(rng.random(n) < 0.2, "kg", "g")
weight = np.where(unit == "kg", np.round(weight / 1000, 2), weight)
ret = rng.random(n) < 0.1
returned = [rng.choice(["yes", "Y", "1", "True"]) if r else rng.choice(["no", "N", "0", "False"]) for r in ret]
rating = np.clip(np.round(rng.normal(4, 0.8, n)), 1, 5).astype(object)
fake = rng.random(n) < 0.06
rating[fake] = rng.choice(["N/A", "-", "none", "NA", "unknown"], fake.sum())

pd.DataFrame({"order_id": np.arange(1, n + 1), "source": src, "order_date": order_date, "city": city, "gender": gender,
              "age": age, "currency": currency, "amount": amount, "weight": weight, "unit": unit,
              "is_returned": returned, "rating": rating}).to_csv("orders_raw.csv", index=False)
pd.DataFrame({"order_id": np.arange(1, n + 1), "true_date": true_date}).to_csv("true_dates.csv", index=False)
print(f"orders_raw.csv: {n:,} rows")
OUTPUT
orders_raw.csv: 3,000 rows

SectionSection第 10 节

Golden Rules — Inconsistency and Outliers 🏆सुनहरे नियम — Inconsistency और Outliers 🏆黄金法则 —— 不一致与异常值 🏆

🎯 9 Rules Every Data Scientist Should Follow9 नियम जो हर Data Scientist को मानने चाहिए每位数据科学家都应遵守的 9 条法则
1
Run value_counts() and nunique() on every text column. The number should match what you know about the world.
हर text column पर value_counts() और nunique() चलाएँ। संख्या दुनिया के बारे में आपकी जानकारी से मेल खानी चाहिए।
对每个文本列运行 value_counts() 和 nunique()。数量应当与你对现实的了解相符。
2
Normalise in order: lower → strip → squeeze spaces → remove punctuation → alias map → fuzzy match (with a threshold and a review).
क्रम में normalise करें: lower → strip → spaces दबाएँ → विराम चिह्न हटाएँ → alias map → fuzzy match (threshold और समीक्षा के साथ)।
按顺序规范化:转小写 → 去首尾空格 → 压缩空格 → 去标点 → 别名映射 → 模糊匹配(设阈值并人工复核)。
3
Fuzzy matching finds look-alikes, not meanings. Protect real values such as "Navi Mumbai" and check every automatic merge.
Fuzzy matching मिलते-जुलते शब्द ढूँढता है, मतलब नहीं। "Navi Mumbai" जैसे असली मानों को सुरक्षित रखें और हर अपने-आप हुए मेल को जाँचें।
模糊匹配找的是长得像的词,而不是含义。保护 “Navi Mumbai” 这样的真实值,并检查每一次自动合并。
4
Parse dates with an explicit format= per source. Avoid dayfirst=True on mixed data and format="mixed" on ISO dates; count NaT after errors="coerce".
हर source के लिए explicit format= से dates parse करें। मिले-जुले data पर dayfirst=True और ISO dates पर format="mixed" से बचें; errors="coerce" के बाद NaT गिनें।
按来源用明确的 format= 解析日期。混合数据上避免 dayfirst=True,ISO 日期上避免 format="mixed";使用 errors="coerce" 后统计 NaT。
5
Convert every unit and currency to one standard, and record the rate or factor you used (exchange rates change by date).
हर इकाई और मुद्रा को एक मानक में बदलें, और इस्तेमाल किया rate या गुणक दर्ज करें (विनिमय दरें तारीख़ के साथ बदलती हैं)।
把所有单位和货币换算成同一标准,并记录所用的汇率或换算系数(汇率随日期变化)。
6
Catch fake NAs at load time with na_values=, using the exact spellings you found.
Load के समय na_values= से नक़ली NA पकड़ें, उन्हीं वर्तनियों के साथ जो आपको मिलीं।
读取时用 na_values= 捕获伪缺失值,并使用你实际发现的写法。
7
Never delete outliers automatically. Sort and read the flagged rows first; remove only confirmed errors.
Outliers कभी अपने आप न हटाएँ। पहले चिह्नित rows को sort करके पढ़ें; सिर्फ़ पक्की ग़लतियाँ हटाएँ।
绝不自动删除异常值。先排序并阅读被标记的行;只删除确认的错误。
8
Pick the detector to fit the shape: IQR or modified z for skewed data, plain z only for roughly normal data; Isolation Forest for odd combinations.
आकार के हिसाब से detector चुनें: skewed data के लिए IQR या modified z, सादा z सिर्फ़ लगभग normal data के लिए; अजीब मेल के लिए Isolation Forest।
根据形状选择检测方法:偏态数据用 IQR 或修正 z,普通 z 只用于近似正态的数据;异常组合用孤立森林。
9
Prefer keeping real extremes with robust tools (log, RobustScaler, tree models) over cutting them, and write down every decision.
असली अति मानों को काटने की बजाय robust tools (log, RobustScaler, tree models) के साथ रखना बेहतर है, और हर फ़ैसला लिखें।
对于真实极端值,优先用稳健工具(对数、RobustScaler、树模型)保留它们,而不是截掉;并记录每个决定。
🧮
Key Takeawayमुख्य सीख核心要点

Inconsistent values and outliers are different problems with the same cure: look before you change anything. Count the categories, check the dates against the truth you know, and read the flagged rows. A blanket fix applied without looking can do more damage than the dirty data did.

Inconsistent मान और outliers अलग समस्याएँ हैं पर इलाज एक है: कुछ भी बदलने से पहले देखें। Categories गिनें, dates को अपनी जानकारी से मिलाएँ, और चिह्नित rows पढ़ें। बिना देखे लगाया गया एक-सा सुधार गंदे data से ज़्यादा नुक़सान कर सकता है।

不一致的值和异常值是两种不同的问题,但药方相同:改动之前先查看。统计类别数,把日期与已知事实对照,阅读被标记的行。不看就套用的一刀切修复,可能比脏数据本身造成更大的伤害。