Skip to content

সমাধান — অধ্যায় ১.৫ · EDA Workflow: A Case Study

এই ফাইলে ১.৫ অধ্যায়ের সব অনুশীলনীর পূর্ণ সমাধান। সব কোড Section 5-এর build_housing()clean()-এর ওপর নির্ভর করে (seed=2025) এবং runnable; নিচে প্রকৃত output দেওয়া। নিজে চালিয়ে মিলিয়ে নিন।

# সব সমাধানের সাধারণ setup (একবার চালান)
import numpy as np
import pandas as pd
# build_housing ও clean — অধ্যায়ের Section 5 থেকে হুবহু কপি করুন
df = build_housing()       # seed=2025
dc = clean(df)             # cleaned (579, 5)
num_cols = ["area_sqm", "age_years", "rooms", "price_lakh"]

Conceptual

সমাধান ১ — কেন median/IQR robust [easy]

মূল কারণ: mean ও standard deviation-এর গণনায় প্রতিটি observation যুক্ত হয়, এবং std-এ পার্থক্যকে বর্গ করা হয় — তাই কেন্দ্র থেকে দূরের একটি বিন্দু অসামঞ্জস্যপূর্ণভাবে বড় ওজন পায়। একটিমাত্র চরম error (যেমন ৯৮০ বা −15) পুরো mean/std টেনে নিতে পারে; এদের বলা হয় non-robust (অ-সহিষ্ণু)। বিপরীতে median হলো মাঝের মান (rank-ভিত্তিক) এবং IQR = \(Q_3 - Q_1\) — এরা কেবল ক্রম গোনে, মানের প্রকৃত দূরত্ব নয়; তাই অল্প কিছু চরম মান এদের প্রায় নড়াতেই পারে না (robust, breakdown point উঁচু)।

আমাদের price_lakh (raw)-এর উদাহরণ: raw data-তে একটি −15 ও কয়েকটি luxury/typo মান থাকা সত্ত্বেও —

raw price: mean = 187.60,  median = 187.05

এখানে median ও mean কাছাকাছি কারণ error মানগুলো উভয় দিকে (নিচে −15, উপরে ~575) কিছুটা ভারসাম্য করেছে; কিন্তু area_sqm-এর ৯৮০-typo-র মতো একতরফা error থাকলে mean অনেক বেশি বিকৃত হতো, median প্রায় অপরিবর্তিত থাকত। সারকথা: outlier সন্দেহ করলে কেন্দ্রের জন্য median, বিস্তারের জন্য IQR/MAD ব্যবহার নিরাপদ — এই কারণেই আমাদের clean() group-median দিয়ে impute করে।

সমাধান ২ — Anscombe-র শিক্ষা ও case study-তে তার প্রতিফলন [medium]

এক বাক্যে শিক্ষা: mean, variance ও correlation প্রায় একরকম হলেও দুটি dataset-এর প্রকৃত আকৃতি সম্পূর্ণ ভিন্ন হতে পারে — তাই summary statistic-এর পাশাপাশি ছবি দেখা আবশ্যক

case study-তে প্রতিফলন: আমরা পেয়েছিলাম —

area ~ price :  Pearson r = 0.529,  Spearman rho = 0.611

দুটি correlation সংখ্যার এই ব্যবধান একটি সংকেত: সম্পর্কটি নিখুঁত রৈখিক নয় এবং কয়েকটি চরম (luxury) মান Pearson-কে কিছুটা নিচে টানছে (Pearson মানের প্রকৃত দূরত্বে সংবেদনশীল, Spearman কেবল rank গোনে তাই বেশি)। কিন্তু এই ব্যাখ্যা শুধু সংখ্যা দেখে নিশ্চিত করা যায় না — scatter plot (Figure 3) আঁকার পরই আমরা চোখে দেখলাম outlier ও সামান্য nonlinearity। এটিই Anscombe-র মূল বার্তার জীবন্ত প্রতিফলন: একটিমাত্র \(r\) পুরো গল্প বলে না।

সমাধান ৩ — data leakage ও group-median imputation [medium]

data leakage হলো অজান্তে test/future data-র তথ্য বিশ্লেষণ বা training-এ ঢুকিয়ে ফেলা, যার ফলে model বাস্তবের চেয়ে ভালো মনে হয় কিন্তু নতুন data-তে খারাপ করে।

clean()-এ leakage কীভাবে ঘটতে পারে: আমাদের clean() পুরো dataset-এর location-group median দিয়ে NaN পূরণ করে। যদি এই dataset-এ পরে train/test split করা হয়, তবে test সারির মানও median হিসাবে অংশ নিয়েছিল → test-এর তথ্য train imputation-এ চুঁইয়ে গেল (এবং উল্টোটাও)। একইভাবে IQR fence/standardization-এর parameter পুরো data থেকে শিখলে leakage হয়।

কীভাবে এড়াবেন: 1. আগে train/test split করুন। 2. group-median (বা যেকোনো impute/scale parameter) কেবল train থেকে হিসাব করুন। 3. সেই train-derived মান train ও test উভয়ে প্রয়োগ করুন (test থেকে কিছু শিখবেন না)।

scikit-learn-এ এটি Pipeline + fit (train) / transform (test) দিয়ে স্বয়ংক্রিয়ভাবে নিশ্চিত হয় (Part VI-তে বিস্তারিত)। অধ্যায়ের EDA-তে আমরা পুরো sample ব্যবহার করেছি কারণ লক্ষ্য বর্ণনা, ভবিষ্যদ্বাণী নয়।


Computational

সমাধান ৪ — cleaned data-তে age-এর IQR fence [easy]

q1, q3 = dc["age_years"].quantile([.25, .75])
iqr = q3 - q1
lo, hi = q1 - 1.5*iqr, q3 + 1.5*iqr
n_out = ((dc["age_years"] < lo) | (dc["age_years"] > hi)).sum()
print(f"age fence: [{lo:.2f}, {hi:.2f}]  ->  {int(n_out)} outliers  (IQR={iqr:.2f})")

Output:

age fence: [-8.60, 37.00]  ->  27 outliers  (IQR=11.40)

ব্যাখ্যা: age-এ ২৭টি মান flag হলো — price-এর (৯টি) চেয়ে অনেক বেশি। কারণ age_years ডানদিকে skewed (gamma distribution থেকে তৈরি): ডান লেজে স্বাভাবিকভাবেই অনেক বৈধ বড় মান থাকে, যাদের IQR fence "outlier" বলে চিহ্নিত করে। নিচের সীমা −8.6 অর্থহীন (বয়স ঋণাত্মক হয় না)। শিক্ষা: IQR fence symmetric distribution-এ ভালো কাজ করে; skewed data-তে এটি ডান লেজের বৈধ মানকে অতিরিক্ত flag করে — তাই এখানে অন্ধভাবে মোছা ভুল হতো (Section 4.1-এর নীতি)। skewed হলে log-রূপান্তর বা skew-adjusted fence ভালো।

সমাধান ৫ — price_per_sqm ও location তুলনা [medium]

d = dc.copy()
d["price_per_sqm"] = d["price_lakh"] / d["area_sqm"]
print("median =", round(d["price_per_sqm"].median(), 4),
      " IQR =", round(d["price_per_sqm"].quantile(.75) - d["price_per_sqm"].quantile(.25), 4))
print(d.groupby("location")["price_per_sqm"].mean().round(4))

Output:

median = 1.5007   IQR = 0.5175
location
central    2.0017
outer      1.2417
suburb     1.4836

ব্যাখ্যা (গুরুত্বপূর্ণ insight / অন্তর্দৃষ্টি): Section 3.5-এ মোট দামে outer ছিল সবচেয়ে দামি (গড় ≈ 205)। কিন্তু per-sqm দামে চিত্রটি উল্টে গেল — central সবচেয়ে দামি (2.00), outer সবচেয়ে সস্তা (1.24)! এর অর্থ: outer-এর উঁচু মোট দাম কেবল কারণ সেখানকার ফ্ল্যাট বড়; প্রতি বর্গমিটারে central-ই সবচেয়ে দামি (যা বাস্তব শহরে স্বাভাবিক — কেন্দ্রস্থলের জমি দামি)। এটিই confounding-এর সরাসরি প্রমাণ: area-র জন্য সমন্বয় করলে (per-sqm = area-normalized) location-এর প্রকৃত প্রভাব উল্টো দিকে। multivariate চিন্তা (এখানে একটি ratio, Part V-এ regression) ছাড়া আমরা ভুল উপসংহারে পৌঁছাতাম।

সমাধান ৬ — cleaning-এর আগে/পরে correlation [medium]

raw = df.dropna(subset=["area_sqm", "price_lakh"])
r_raw   = raw["area_sqm"].corr(raw["price_lakh"])              # সব raw (980-typo সহ)
r_no980 = raw[raw["area_sqm"] <= 400]["area_sqm"].corr(
          raw[raw["area_sqm"] <= 400]["price_lakh"])           # শুধু 980-typo বাদ
r_clean = dc["area_sqm"].corr(dc["price_lakh"])               # পূর্ণ cleaned
print(f"r (raw, 980 সহ)      = {r_raw:.3f}")
print(f"r (শুধু 980 বাদ)     = {r_no980:.3f}")
print(f"r (পূর্ণ cleaned)    = {r_clean:.3f}")

Output:

r (raw, 980 সহ)      = 0.386
r (শুধু 980 বাদ)     = 0.520
r (পূর্ণ cleaned)    = 0.529

ব্যাখ্যা: একটিমাত্র ৯৮০-typo বাদ দিলেই Pearson \(r\) লাফিয়ে 0.386 → 0.520 হলো — অর্থাৎ ৬০০টির মধ্যে একটিমাত্র ভুল বিন্দু correlation-কে কৃত্রিমভাবে দুর্বল দেখাচ্ছিল (ওই বিন্দুতে area বিশাল কিন্তু price স্বাভাবিক, তাই সম্পর্ক ভেঙে দিচ্ছিল)। বাকি cleaning (price>0, age≤100) তেমন পরিবর্তন আনল না (0.520 → 0.529)। সারকথা: non-robust Pearson \(r\)-এ একটিমাত্র error outlier পুরো সিদ্ধান্ত বদলে দিতে পারে — তাই correlation রিপোর্টের আগে cleaning ও scatter-পরিদর্শন বাধ্যতামূলক (Section 4.2-এর সরাসরি দৃষ্টান্ত)।


Mini-project

সমাধান ৭ — seed=7-এ পুরো pipeline ও reproducibility [hard]

df7 = build_housing(seed=7)
dc7 = clean(df7)
print("raw shape:", df7.shape, " clean shape:", dc7.shape)
print("missing:", df7.isna().sum().to_dict())
print("price describe:\n", df7["price_lakh"].describe().round(2))
c7 = dc7[num_cols].corr()
print(f"area~price={c7.loc['area_sqm','price_lakh']:.3f}, "
      f"rooms~price={c7.loc['rooms','price_lakh']:.3f}, "
      f"area~rooms={c7.loc['area_sqm','rooms']:.3f}, "
      f"age~price={c7.loc['age_years','price_lakh']:.3f}")
print(dc7.groupby("location")["price_lakh"].agg(["mean","median","count"]).round(1))

Output (সংক্ষেপে):

raw shape: (600, 5)   clean shape: (579, 5)
missing: {'location': 0, 'area_sqm': 0, 'age_years': 48, 'rooms': 30, 'price_lakh': 18}
price describe: min = -15.00, max = 728.97, mean = 188.50, median = 187.60
area~price=0.552, rooms~price=0.480, area~rooms=0.887, age~price=-0.368
            mean  median  count
central    176.7   176.8    192
outer      207.1   206.8    150
suburb     187.4   185.0    237

EDA সারাংশ (নমুনা, ৩–৪ বাক্য): seed=7 sample-এও বাড়ির দাম সবচেয়ে জোরালোভাবে যুক্ত area_sqm-এর সাথে (Pearson \(r \approx 0.55\)), তারপর rooms (\(0.48\)), এবং ঋণাত্মকভাবে age_years (\(-0.37\))। area_sqmrooms-এর মধ্যে আবারও তীব্র collinearity (\(r \approx 0.89\)), তাই modeling-এ দুটোই একসাথে রাখা ঝুঁকিপূর্ণ। data-quality সমস্যা একই ধরনের: ঋণাত্মক দাম (−15), অস্বাভাবিক উঁচু মান (max ≈ 729), এবং age_years/rooms/price_lakh-এ যথাক্রমে 48/30/18টি missing — cleaning-এ ২১টি সারি বাদ পড়ল।

reproducibility-র উপসংহার: নির্দিষ্ট সংখ্যা সামান্য বদলেছে (যেমন area~price 0.529 → 0.552), কিন্তু সব গুণগত প্যাটার্ন হুবহু টিকে আছে — area↔price ধনাত্মক মাঝারি, area↔rooms জোরালো collinear, age↔price ঋণাত্মক, outer-এর মোট দাম সর্বোচ্চ। এটাই reproducible synthetic data-র সৌন্দর্য: একই generative process ভিন্ন seed-এও স্থির গঠন দেয়, আর fixed seed দিলে সংখ্যাও হুবহু পুনরুৎপাদনযোগ্য (0.6-এর reproducibility নীতি)।

সমাধান ৮ — dashboard-এ পঞ্চম প্যানেল (age vs price) যোগ [hard]

import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

groups = ["central", "suburb", "outer"]
fig = plt.figure(figsize=(11.6, 12))
gs = gridspec.GridSpec(3, 2, figure=fig, hspace=0.42, wspace=0.34)

# (a)-(d): Section 6 Figure 2-এর চার প্যানেল হুবহু (সংক্ষেপে এখানে শুধু নতুন প্যানেল দেখানো)
# ... axA, axB, axC, axD আগের কোডের মতো gs[0,0], gs[0,1], gs[1,0], gs[1,1]-এ ...

# (e) NEW: age vs price scatter + fit line  -> gs[2, :] (পুরো নিচের সারি)
axE = fig.add_subplot(gs[2, :])
age = dc["age_years"].values
price = dc["price_lakh"].values
axE.scatter(age, price, s=18, alpha=0.5, color="#8172B3")
slope, intercept = np.polyfit(age, price, 1)
xs = np.linspace(age.min(), age.max(), 100)
axE.plot(xs, intercept + slope*xs, "k--", lw=2,
         label=f"fit: price = {intercept:.0f} + {slope:.2f}·age")
axE.set_xlabel("Age (years)"); axE.set_ylabel("Price (lakh BDT)")
axE.set_title(f"(e) Age vs price  (slope = {slope:.2f}, Pearson r = {np.corrcoef(age,price)[0,1]:.2f})")
axE.legend(loc="upper right")

fig.suptitle("EDA dashboard (extended): city housing dataset", fontsize=14, y=0.99)
fig.savefig("../_assets/1-5-dashboard-ext.png", dpi=150)   # নিজের ফাইলনাম; shared asset নয়

print(f"age~price slope = {slope:.3f}, intercept = {intercept:.1f}")

Output:

age~price slope = -1.323, intercept = 208.2

ব্যাখ্যা: fit রেখার slope −1.32 (ঋণাত্মক) — অর্থাৎ গড়ে প্রতি অতিরিক্ত ১ বছর বয়সে দাম ≈ 1.3 লাখ কমে। এর চিহ্ন (negative) heatmap-এ পাওয়া age_years ↔ price_lakh correlation −0.31-এর সাথে সঙ্গতিপূর্ণ: correlation ও regression-slope উভয়েই ঋণাত্মক, কারণ গাণিতিকভাবে slope = \(r \cdot (s_y / s_x)\)\(r<0\) হলে slope-ও \(<0\)। scatter চোখে দেখলেও পুরনো বাড়িগুলোর price-cloud নিচের দিকে ঝুঁকে থাকা দৃশ্যমান। (ছবি নিজের ফাইল 1-5-dashboard-ext.png-এ সংরক্ষণ — shared asset নয়, যাতে অন্য কারো কাজে হস্তক্ষেপ না হয়।)


সব সমাধানের সাধারণ শিক্ষা: EDA-তে প্রতিটি সংখ্যার (mean, \(r\), slope) পাশে অন্তত একটি ছবি দেখা, robust ও non-robust statistic পাশাপাশি রাখা, error বনাম genuine outlier আলাদা করা, এবং leakage-সচেতন থাকা — এই অভ্যাসই data থেকে নির্ভরযোগ্য সিদ্ধান্ত দেয়। Part IV (inference) ও Part V (regression)-এ এই ভিত্তিই কাজে লাগ