সরাসরি পর্বে যান
পর্ব ৬২ · ৭৯-এর মধ্যে · মডিউল ১১

Exploratory data analysis — প্রশ্ন, পরিসংখ্যান আর chart

Exploratory Data Analysis — Questions First, Data Dictionary, Missingness Patterns, Segment Rates with Intervals, Outliers, Leaky Columns, an EDA Log and an EDA Report

Taught in Bangla, this lesson runs exploratory data analysis as a question-driven workflow on 1,500 fictional orders, locking the test period away first. Real in-browser pandas runs find missingness patterns, a misspelled district, an impossible price, a seasonal spike, a strong segment with Wilson intervals and a leaky column with AUC 1.0, all recorded in an EDA log and a short report.

এই পর্বে যা শিখবেন

  • EDA-কে প্রশ্নভিত্তিক কাজের ধারা হিসেবে চালানো, আর data dictionary ও EDA লগ রাখার অভ্যাস
  • প্রথম নজরের profile: shape, dtype, কলাম আর segment ধরে missing value, describe
  • Missingness-এর ধরন পড়া, এক কলাম ও দুই কলামের chart, আর লক্ষ্যের সঙ্গে সম্পর্ক
  • Segment-এর হারের পাশে n, Wilson interval আর bootstrap; ছোট দলের চরম হার কেন খবর নয়
  • Leaky কলাম আর outlier ধরা, test-এ EDA কেন leakage, আর একটি EDA রিপোর্টের ছাঁচ

মডিউল ১০-এ model শেখানো আর মাপার কাঠামো তৈরি হয়েছে: পর্ব ৫৬-এ সৎ ভাগ আর leakage, পর্ব ৬০-এ metric আর তার অনিশ্চয়তা। এই মডিউল সেই কাঠামোকে একটি পূর্ণ কাজের ধারায় সাজায়, আর তার প্রথম ধাপ হলো ডেটাকে চেনা। pandas-এর হাতিয়ার (পর্ব ২৩, পর্ব ২৪) আর chart-এর ব্যাকরণ (পর্ব ২৯, পর্ব ৩০) আপনার জানা; এখানে সেগুলো আবার শেখানো হবে না। এখানে শিখবেন কোন ক্রমে, কোন প্রশ্ন নিয়ে, আর কী লিখে রেখে সেগুলো ব্যবহার করতে হয়। চালানো যায় এমন ব্লকগুলো পাতাতেই চলে (ব্রাউজারের Python, pandas 3.0.2); প্রথম run-এ pandas আর matplotlib নামতে কয়েক সেকেন্ড লাগে।

১ · EDA কী, আর কেন প্রশ্ন আগে

একজন ভালো ডাক্তার রোগী ঢুকতেই ওষুধ লেখেন না, আবার সব পরীক্ষাও একসাথে দেন না। আগে প্রশ্ন: কবে থেকে, কোথায় ব্যথা। তারপর সেই প্রশ্নের জন্য নির্দিষ্ট পরীক্ষা, আর প্রতিটি ফল রোগীর ফাইলে লেখা। Exploratory data analysis ঠিক এই রোগনির্ণয়: ডেটাকে প্রশ্ন করা, পরীক্ষা করা, আর ফাইল রাখা।

Exploratory data analysis (EDA) মানে model বানানোর আগে ডেটাকে চেনা: কী আছে, কী নেই, কোথায় গোলমাল, আর কোন সম্পর্ক প্রথম দেখায় চোখে পড়ে। লক্ষ্য তিনটি: গুণমান (ভুল বানান, ফাঁকা ঘর, অসম্ভব মান), গঠন (কেমন ছড়ানো, কোন দলে কী আলাদা, সময়ে কী বদলায়), আর সন্দেহ: এত ভালো সংকেত যে বিশ্বাস করা কঠিন, যেমন leakage.

ইতিহাস থেকে: “Exploratory data analysis” নামটি জনপ্রিয় করেন মার্কিন পরিসংখ্যানবিদ John W. Tukey, মূলত ১৯৭৭ সালে প্রকাশিত তাঁর একই নামের বইয়ের মাধ্যমে। তার আগে, ১৯৬২ সালের “The Future of Data Analysis” প্রবন্ধে তিনি যুক্তি দেন যে ডেটা বিশ্লেষণ শুধু তত্ত্ব প্রমাণের হাতিয়ার নয়, ডেটা দেখে প্রশ্ন খোঁজাও এর কাজ। Tukey exploratory (খোঁজা) আর confirmatory (প্রমাণ করা) বিশ্লেষণকে আলাদা রাখতে বলেছিলেন; box plot আর stem-and-leaf display-এর মতো হাতে আঁকা যায় এমন কৌশল তাঁর বইয়ের মাধ্যমে ব্যাপক পরিচিতি পায় (পর্ব ২৯-এ box plot দেখেছেন)। বইটির অনেক কৌশল কাগজ-কলমের যুগের; তাই কৌশলের চেয়ে মনোভাবটিই বেশি টিকেছে।

“আগে প্রশ্ন” কেন? কারণ প্রশ্ন ছাড়া EDA-র কোনো শেষ নেই। ১১টি কলামের প্রতিটি জোড়ায় একটি chart মানে ৫৫টি chart, তার ওপর প্রতিটি জেলা, প্রতিটি মাস। অনেক chart দেখলে কোনো না কোনোটিতে “কিছু একটা” দেখা যাবেই, আর তার বেশিরভাগই কাকতাল। লেখা প্রশ্ন বলে দেয় কোন যাচাই দরকার, আর পরে মনে করিয়ে দেয় আপনি কতগুলো জিনিস দেখেছিলেন; দ্বিতীয়টি অংশ ৬ আর ৮-এ জরুরি হবে।

আমাদের পরিস্থিতি: বাজারঘরের অপারেশন দল জানতে চায়, অর্ডার দেওয়ার মুহূর্তেই কোন অর্ডার ফেরত আসার ঝুঁকিতে, যাতে ফোনে নিশ্চিত করা যায়। এটি একটি classification প্রশ্ন, আর পর্ব ৬৩–৬৬-এ এর model তৈরি হবে। তার আগে ডেটা-প্রকৌশলী একটি ফাইল দিয়েছেন: ১৫০০টি অর্ডার, নভেম্বর ২০২৫ থেকে অক্টোবর ২০২৬. ফাইলটি কাল্পনিক, seed দিয়ে বানানো, আর এতে ইচ্ছে করে ছয়টি সমস্যা আর ধরন লুকোনো আছে। আপনি সেগুলোর তালিকা জানেন না, ঠিক যেমন বাস্তবে জানবেন না। অংশ ৯-এর শেষে কীভাবে বানানো হয়েছে, তা খোলা আছে; EDA শেষ করার আগে না দেখাই ভালো।

২ · কাজ শুরুর আগে — data dictionary, তালাবন্ধ test আর প্রশ্নের তালিকা

প্রথম কোড চালানোর আগেই তিনটি জিনিস লেখা দরকার। প্রথমটি data dictionary: প্রতিটি কলাম কী, তার একক, সম্ভাব্য মান, আর সবচেয়ে জরুরি, কখন জানা যায়। শেষ প্রশ্নটি পর্ব ৫৬-এর leakage-এর চাবি। অভিধানটি ডেটা-প্রকৌশলীর দেওয়া, তাই এটিও যাচাই করতে হবে।

কলামঅর্থ ও এককপ্রত্যাশিত মানকখন জানা যায়
order_idঅর্ডারের নম্বরঅনন্য পূর্ণসংখ্যাঅর্ডারের সময়
order_dateঅর্ডারের তারিখনভেম্বর ২০২৫–অক্টোবর ২০২৬অর্ডারের সময়
districtডেলিভারির জেলা৮টি বিভাগীয় সদর জেলাঅর্ডারের সময়
channelঅর্ডার কোথা থেকেapp, webঅর্ডারের সময়
courierকোন ডেলিভারি সংস্থাA, Bঅর্ডারের সময় ঠিক হয়
categoryপণ্যের ধরন৫টি ধরনঅর্ডারের সময়
paymentপরিশোধের পথCOD, mobile, cardঅর্ডারের সময়
priceদাম, টাকায়শত থেকে কয়েক হাজারঅর্ডারের সময়
delivery_daysপ্রতিশ্রুত ডেলিভারির দিন১–১২অর্ডারের সময় (courier জানালে)
returnedলক্ষ্য: ফেরত এসেছে কি না০ বা ১ডেলিভারির কয়েক দিন পরে
refund_takaফেরত দেওয়া টাকা০ বা দামের কাছাকাছি?? (অভিধানে লেখা নেই)

শেষ ঘরের “??” নিজেই প্রথম লগের যোগ্য: যে কলামের সময় কেউ জানে না, সেটি সন্দেহের তালিকায়। দ্বিতীয় জিনিস: test set তালাবন্ধ করা, কোনো chart দেখার আগে। মডেল চলবে ভবিষ্যতের অর্ডারে, তাই time-based split: সেপ্টেম্বর আর অক্টোবর ২০২৬-এর অর্ডার আলাদা, বাকি দশ মাসে EDA. কেন এত আগে, তা অংশ ৮-এ বিস্তারিত। তৃতীয় জিনিস: প্রশ্নের তালিকা, প্রত্যাশাসহ। প্রত্যাশা লিখে রাখলে অবাক হওয়ার মুহূর্তটা ধরা যায়।

🧾 গুণমানের প্রশ্ন

dtype কি অভিধানের মতো? জেলা কি ৮টি? কোথায় ফাঁকা, কোন দলে বেশি? অবিশ্বাস্য দাম আছে?

স্বাস্থ্য

📈 গঠনের প্রশ্ন

Category ভেদে দাম কতটা আলাদা? সপ্তাহে অর্ডার কি সমান, নাকি মৌসুম আছে? কোন জেলা কোন courier পায়?

ধরন

🎯 লক্ষ্যের প্রশ্ন

মোট ফেরতের হার কত? কোন দলে বেশি? কোনো কলাম কি সন্দেহজনক ভালো আলাদা করে?

ফেরত

EDA-র চক্র: প্রশ্ন থেকে যাচাই, ফল থেকে লগ, লগ থেকে নতুন প্রশ্ন; test set পুরো সময় বাইরে

The question-driven EDA loop, with the test set locked outside it

Question-driven EDA loop with a locked test set data dictionary · ব্যবসার প্রশ্ন EDA শুধু train অংশে (দশ মাস) ১ · প্রশ্ন লিখুন প্রত্যাশাসহ ২ · যাচাই কোড, টেবিল, chart ৩ · ফল সংখ্যা, n, ব্যবধান ৪ · EDA লগ প্রশ্ন·প্রমাণ·সিদ্ধান্ত নতুন প্রশ্ন 📄 EDA রিপোর্ট feature-এর কাঁচামাল 🔒 test set শেষ পর্যন্ত বন্ধ চক্রের কোনো তীর test set ছোঁয় না

চক্রটি একবার পুরো ঘুরিয়ে দেখা যাক, একটি নিরীহ প্রশ্ন দিয়ে। সংখ্যাগুলো অংশ ৩-এর ব্লক আর একটি ছোট বাড়তি groupby থেকে (ব্রাউজারে চালানো, একই seed-এর ডেটা)।

  1. প্রশ্নdistrict৮টি জেলা?
  2. যাচাইvalue_countsগুনে দেখা
  3. ফল9 ≠ 8অবাক হওয়া
  4. সিদ্ধান্তmapএক বানান
  5. লগ#2নতুন প্রশ্ন
প্রশ্ন আর প্রত্যাশাকোড চালানোর আগে লেখা
প্রশ্ন: district-এ কি ঠিক
  ৮টি জেলা, অভিধানের বানানে?
প্রত্যাশা: Dhaka সবচেয়ে বেশি;
  কোনো অচেনা নাম নেই।
কেন: পরে জেলা ধরে ফেরতের
  হার হিসাব হবে।
df.district.value_counts()train অংশ, ১২৯১টি অর্ডার
Dhaka         429
Chattogram    185
Khulna        127
Sylhet        117
Rajshahi      106
Mymensingh     94
Rangpur        90
Barishal       86
Chittagong     57
অবাক হওয়ার মুহূর্ত৯টি নাম, প্রত্যাশা ছিল ৮
Chittagong = Chattogram-এর
  পুরোনো ইংরেজি বানান
channel ধরে গুনে:
  Chittagong  web 57  app 0
  Chattogram  web 32  app 153
→ শুধু web ফর্ম থেকে আসে
সিদ্ধান্ত আর তার প্রভাবএকটি নিয়ম, যা test-এও খাটবে
নিয়ম: Chittagong → Chattogram
মিলিয়ে: 185 + 57 = 242টি
ফেরত আলাদা: 30/185 = 0.162
             8/57 = 0.140
মিলিয়ে:     38/242 = 0.157
→ ভাগ থাকলে একই জেলা দুই
  ছোট দলে, তুলনা বিভ্রান্তিকর
EDA লগ, এন্ট্রি ২পরের প্রশ্ন এখান থেকেই
প্রশ্ন: জেলার নাম এক রকম?
প্রমাণ: Chittagong 57 (সব web)
সিদ্ধান্ত: এক বানানে মেলানো,
  নিয়মটি pipeline-এ
নতুন প্রশ্ন: অচেনা নাম এলে
  pipeline কি থামবে?
  1. প্রশ্ন আগে। প্রত্যাশা: ৮টি জেলা, অভিধানের বানানে। প্রত্যাশা না লিখলে ৯টি নাম দেখেও চোখ এড়িয়ে যেতে পারে।
  2. যাচাই। সবচেয়ে সরল হাতিয়ার value_counts(). তালিকার শেষে ৯ নম্বর নাম Chittagong, ৫৭টি অর্ডার।
  3. ফল ও উৎস। এটি চট্টগ্রামের পুরোনো ইংরেজি বানান। ৫৭টিই web থেকে, app থেকে একটিও নয়: সমস্যাটি পুরোনো web ফর্মের।
  4. সিদ্ধান্ত। একটি mapping নিয়ম: Chittagong থেকে Chattogram. মেলালে ২৪২টি অর্ডার, ফেরত ৩৮টি, হার ০.১৫৭. নিয়মটি লেখা থাকে, যাতে test আর ভবিষ্যতের ডেটায় একইভাবে খাটে।
  5. লগ আর নতুন প্রশ্ন। এন্ট্রিতে প্রশ্ন, প্রমাণ, সিদ্ধান্ত। আর একটি নতুন প্রশ্ন জন্মাল: ভবিষ্যতে তৃতীয় কোনো বানান এলে কী হবে? অচেনা category সামলানো পর্ব ৬৩ আর ৬৪-এর বিষয়।

৩ · প্রথম নজর — shape, dtype, missing আর describe

প্রথম ব্লকটি একটি data profiling: প্রতিটি কলামের একটি সংক্ষিপ্ত স্বাস্থ্য-কার্ড। ডেটা আসে bazarghar_orders.py নামের একটি ছোট মডিউল থেকে, যা পাতার সঙ্গেই আছে আর প্রতিবার একই ১৫০০টি অর্ডার বানায়। প্রথম কাজ test তালাবন্ধ করা; তারপর dtype, কোথায় ফাঁকা, ফাঁকা কি কোনো দলে বেশি, সংখ্যার কলামের সারাংশ, আর category কলামে কয়টি মান। (পর্ব ২৩-এর স্বাস্থ্য পরীক্ষার মতো, শুধু প্রতিটি লাইন একটি প্রশ্নের উত্তর।)

import pandas as pd
from bazarghar_orders import load_orders
pd.set_option("display.width", 60)

orders = load_orders()
cut = pd.Timestamp("2026-09-01")
df = orders[orders.order_date < cut]          # EDA শুধু এই অংশে
print("পুরো:", orders.shape, "| EDA:", df.shape,
      "| তালাবন্ধ:", int((orders.order_date >= cut).sum()), "সারি")
print(df.dtypes.astype(str).to_string())

miss = df.isna().sum()
print("\nফাঁকা ঘর:", miss[miss > 0].to_dict())
m = df.delivery_days.isna()
print("courier ধরে ফাঁকার হার:")
print(m.groupby(df.courier).agg(["sum", "size", "mean"]).round(3))
print("জেলা ধরে ফাঁকার হার:")
print(m.groupby(df.district).mean().round(2).sort_values().to_string())

print("\nসংখ্যার কলাম:")
num = df[["price", "delivery_days", "returned", "refund_taka"]]
print(num.describe().T[["mean", "50%", "min", "max"]].round(2))
for c in ["district", "channel", "courier", "category", "payment"]:
    vc = df[c].value_counts()
    print(f"{c}: {len(vc)}টি মান, সবচেয়ে কম '{vc.index[-1]}' ({vc.iloc[-1]})")

আসল আউটপুট (ব্রাউজারের Python, pandas 3.0.2):

পুরো: (1500, 11) | EDA: (1291, 11) | তালাবন্ধ: 209 সারি
order_id                  int32
order_date       datetime64[us]
district                    str
channel                     str
courier                     str
category                    str
payment                     str
price                   float64
delivery_days           float64
returned                  int32
refund_taka             float64

ফাঁকা ঘর: {'delivery_days': 198}
courier ধরে ফাঁকার হার:
         sum  size   mean
courier                  
A          6   760  0.008
B        192   531  0.362
জেলা ধরে ফাঁকার হার:
district
Chittagong    0.05
Chattogram    0.08
Khulna        0.10
Dhaka         0.11
Sylhet        0.16
Rajshahi      0.23
Mymensingh    0.26
Rangpur       0.30
Barishal      0.33

সংখ্যার কলাম:
                  mean     50%    min      max
price          1781.63  1010.0  130.0  65000.0
delivery_days     3.59     3.0    1.0     12.0
returned          0.12     0.0    0.0      1.0
refund_taka     172.20     0.0    0.0  12840.0
district: 9টি মান, সবচেয়ে কম 'Chittagong' (57)
channel: 2টি মান, সবচেয়ে কম 'web' (461)
courier: 2টি মান, সবচেয়ে কম 'B' (531)
category: 6টি মান, সবচেয়ে কম 'toys' (3)
payment: 3টি মান, সবচেয়ে কম 'card' (163)

এক ব্লকে চারটি খবর। এক: dtype অভিধানের সঙ্গে মেলে। লেখার কলামগুলোর dtype str, কারণ pandas 3.0 থেকে লেখার জন্য আলাদা string dtype নিজে থেকেই আসে। order_id আর returned এখানে int32, কারণ ব্রাউজারের Python 32-bit; ডেস্কটপে একই কোড int64 দেখায়। দুই: শুধু delivery_days-এ ফাঁকা (১৫.৩%), courier B-তে ৩৬.২%, A-তে ০.৮%; বিস্তারিত অংশ ৪-এ। তিন: দামের mean ১৭৮১.৬৩ কিন্তু median ১০১০, আর সর্বোচ্চ ৬৫০০০; mean আর median-এর এত ফারাক মানে ডানদিকে লম্বা লেজ বা কোনো চরম মান। refund_taka-র median ০: বেশিরভাগ সারিতে ০. চার: জেলায় ৯টি মান (অংশ ২-এর stepper), আর category-তে ৬টি, যদিও অভিধানে ৫টি; ষষ্ঠটি toys, মাত্র ৩টি অর্ডার।

describe একা এই কাজ পারত না: সে দল ধরে ফাঁকা গোনে না, আর category কলাম এড়িয়ে যায়। স্বয়ংক্রিয় profiling tool অনেক chart একসাথে দেয়, কিন্তু কোন সংখ্যাটি অবাক করার মতো, তা বলতে আপনার প্রত্যাশা লাগে।

৪ · Missingness-এর ধরন — ফাঁকা ঘর কোথায়, কেন

ফাঁকা ঘরের সংখ্যা জানা যথেষ্ট নয়; জানতে হবে ফাঁকা হওয়ার ধরন। পর্ব ২৩-এ তিনটি নাম শিখেছেন, এক লাইনে মনে করিয়ে দিই: MCAR (পুরো এলোমেলো, কোনো কিছুর ওপর নির্ভর করে না), MAR (দেখা যায় এমন অন্য কলামের ওপর নির্ভর করে), আর MNAR (হারানো মানটির নিজের ওপর নির্ভর করে, যেমন যে ডেলিভারি খুব দেরি হয়েছে সেটিই জানানো হয়নি)। EDA-র কাজ হলো ডেটা দিয়ে যতটা সম্ভব আলাদা করা। একটি missingness pattern পড়ার কৌশল সহজ: ফাঁকা কি না, সেটিকে একটি ০/১ কলাম ধরে অন্য কলাম ধরে তার হার দেখা।

m = df.delivery_days.isna() · train অংশ, ১২৯১টি অর্ডার
ধাপ
কী দেখছি
সংখ্যা
পড়া
  1. মোট হার। ১২৯১টির ১৯৮টি ফাঁকা, ১৫.৩%. এই সংখ্যা একা কিছু বলে না: এলোমেলো ফাঁকা আর এক দলে জমা ফাঁকার ফল একেবারে আলাদা।
  2. জেলা ধরে। Chittagong-এ ৫%, Dhaka-য় ১১%, Barishal-এ ৩৩%. প্রথম অনুমান “দূরের জেলায় তথ্য কম আসে”; লগে লেখার আগে আরেকটি কলাম দেখা দরকার।
  3. Courier ধরে। A-তে ৭৬০টির ৬টি (০.৮%), B-তে ৫৩১টির ১৯২টি (৩৬.২%). পার্থক্য প্রায় ৪৫ গুণ। ফাঁকা হওয়া প্রায় পুরোটা courier B-এর অভ্যাস।
  4. জেলা × courier. Dhaka-র অর্ডারের ২৫% যায় B-তে, Rangpur-এর ৭৯%, Barishal-এর ৮৪%. অর্থাৎ জেলার পার্থক্য আসলে courier-এর মিশ্রণের ছায়া। জেলাকে কারণ বললে ভুল হতো।
  5. লক্ষ্য ধরে। না-ফেরত অর্ডারে ১৫.৬%, ফেরতে ১৩.৩% ফাঁকা। এই ডেটায় ফাঁকা হওয়া ফেরতের বড় সংকেত নয়।
  6. সিদ্ধান্ত। ফাঁকা হওয়া দেখা যায় এমন কলামের (courier) ওপর নির্ভর করে, তাই MAR-এর মতো আচরণ। ফাঁকা সারি ফেলে দিলে Barishal-এর ৮৬টির ২৮টি হারাবে, জেলার মিশ্রণ বদলাবে। তাই একটি missing indicator কলাম আর পরে pipeline-এর ভেতরে imputation. তবে MNAR আছে কি না, শুধু এই ডেটা থেকে প্রমাণ করা যায় না।

শেষ ধাপের সতর্কতাটি গুরুত্বপূর্ণ। MCAR কিছুটা যাচাই করা যায় (ফাঁকা হওয়া কোনো কলামের সঙ্গে সম্পর্কহীন কি না), কিন্তু MAR আর MNAR-এর পার্থক্য দেখা ডেটা দিয়ে প্রমাণ করা যায় না, কারণ যে মান হারিয়েছে, সেটিই তো আমাদের হাতে নেই। courier B যদি ঠিক দেরির অর্ডারেই দিন না জানায়, ডেটায় তা ধরা পড়বে না। উত্তর আসে মানুষের কাছ থেকে: courier B-এর সিস্টেম কখন ঘরটি ফাঁকা রাখে, তা জিজ্ঞেস করে। তাই লগে সিদ্ধান্তের পাশে একটি খোলা প্রশ্নও থাকে।

মূল ধারণা

ফাঁকা ঘর নিজেও তথ্য। “কোথায় ফাঁকা” প্রশ্নটি অন্তত তিনটি কলাম ধরে জিজ্ঞেস করুন: যে দল সবচেয়ে সন্দেহজনক, লক্ষ্য, আর সময়। ধরনটি ঠিক করে দেয় সারি ফেলবেন, পূরণ করবেন, নাকি ফাঁকা থাকাটাকেই একটি missing indicator feature বানাবেন (পর্ব ৬৩)।

৫ · এক কলাম, তারপর জোড়ায় — দাম, সময় আর ডেলিভারি

সারাংশের সংখ্যা ছবির বিকল্প নয়। পর্ব ৩৭-এ Anscombe-এর চারটি ডেটাসেট মনে করুন: একই mean, variance আর correlation, অথচ ছবিতে একেবারে আলাদা চেহারা। EDA-তে দুই রকম ছবি লাগে। Univariate analysis: একটি কলাম একা কেমন ছড়ানো, যেমন দামের histogram বা সপ্তাহে অর্ডারের সংখ্যা। Bivariate analysis: দুটি কলাম একসাথে, যেমন category ধরে দাম, বা ডেলিভারির দিন ধরে ফেরতের হার। কোন প্রশ্নে কোন chart, তা পর্ব ২৯-এর বিষয়।

  • প্রশ্ন ক: category ভেদে দাম কতটা আলাদা, আর কোনো দাম কি অবিশ্বাস্য? দাম ডানে হেলানো (skewed), তাই box plot log scale-এ।
  • প্রশ্ন খ: সপ্তাহে অর্ডারের সংখ্যা কি মোটামুটি সমান? এখানে seasonality বা হঠাৎ চূড়া খোঁজা।
  • প্রশ্ন গ: প্রতিশ্রুত ডেলিভারির দিন বাড়লে ফেরত কি বাড়ে? (শুধু অন্তত ২০টি অর্ডারের দল)
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from bazarghar_orders import load_orders

df = load_orders().query("order_date < '2026-09-01'")
cats = ["books", "beauty", "clothing", "home", "electronics"]

fig, ax = plt.subplots(figsize=(6.4, 3.4))
ax.boxplot([df.loc[df.category == c, "price"] for c in cats], tick_labels=cats)
ax.set_yscale("log")
ax.set(title="Price by category (log scale)", ylabel="price (taka)")

week = df.set_index("order_date").resample("W").size()
fig, ax = plt.subplots(figsize=(6.4, 3))
ax.plot(week.index, week.to_numpy(), marker=".")
ax.axhline(week.median(), ls="--", c="gray", label="median week")
ax.set(title="Orders per week", ylabel="orders")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b"))
ax.legend()

r = df.groupby("delivery_days").returned.agg(["mean", "size"])
r = r[r["size"] >= 20]
fig, ax = plt.subplots(figsize=(6.4, 3))
ax.bar(r.index, r["mean"])
ax.set(title="Return rate by delivery days (groups with n >= 20)",
       xlabel="delivery days", ylabel="return rate")

print("সপ্তাহে অর্ডার: median", week.median(), "| সর্বোচ্চ", week.max(),
      "| সেই সপ্তাহ শেষ", week.idxmax().date())
print("median-এর দ্বিগুণের বেশি:", {str(k.date()): v for k, v in week[week > 2 * week.median()].items()})
print("প্রথম ও শেষ সপ্তাহ:", week.iloc[0], week.iloc[-1], "(আংশিক সপ্তাহ)")
print("ডেলিভারির দিন ধরে ফেরতের হার:", r["mean"].round(2).to_dict())
print("জেলা × courier (সারির অনুপাত):")
print((df.groupby("district").courier.value_counts(normalize=True)
         .unstack().round(2)).loc[["Dhaka", "Rangpur", "Barishal"]])

আসল আউটপুট (ব্রাউজারের Python, matplotlib 3.10.8; ছবিগুলো ব্লক চালালে আউটপুটের নিচে আসে):

সপ্তাহে অর্ডার: median 26.0 | সর্বোচ্চ 94 | সেই সপ্তাহ শেষ 2026-03-08
median-এর দ্বিগুণের বেশি: {'2026-03-08': 94, '2026-03-15': 87, '2026-03-22': 63}
প্রথম ও শেষ সপ্তাহ: 7 3 (আংশিক সপ্তাহ)
ডেলিভারির দিন ধরে ফেরতের হার: {1.0: 0.08, 2.0: 0.1, 3.0: 0.14, 4.0: 0.14, 5.0: 0.11, 6.0: 0.19, 7.0: 0.14, 8.0: 0.09}
জেলা × courier (সারির অনুপাত):
courier      A     B
district            
Dhaka     0.75  0.25
Rangpur   0.21  0.79
Barishal  0.16  0.84

ছবি ১ (দাম): পাঁচটি category পরিষ্কার আলাদা স্তরে, books সবচেয়ে সস্তা (median ৪১০ টাকা), electronics সবচেয়ে দামি। বাক্সের ওপরের বিন্দুগুলো হেলানো দামের স্বাভাবিক লেজ। কিন্তু books-এর একটি বিন্দু ১০৪-এরও ওপরে, electronics-এর সবচেয়ে দামি পণ্যের চেয়েও বেশি: একটি বইয়ের দাম ৬৫,০০০ টাকা (অংশ ৭-এ যাচাই)। সব category একসাথে দেখলে এটি এত চোখে পড়ত না।

ছবি ২ (সময়): বেশিরভাগ সপ্তাহে ২০ থেকে ৩৫টি অর্ডার (সব মিলিয়ে ১৭ থেকে ৩৮), median ২৬. মার্চের প্রথম তিন সপ্তাহে ৯৪, ৮৭ আর ৬৩, median-এর দ্বিগুণের বেশি; ঈদের আগের কেনাকাটার মতো একটি চূড়া। সময় ধরে ভাগ করলে মার্চ কোন দিকে পড়ে তা ফল বদলাবে, আর মাস বা “উৎসবের আগের সপ্তাহ” পরে feature হতে পারে। প্রথম সপ্তাহে ৭টি আর শেষটিতে ৩টি অর্ডার ব্যবসার পতন নয়: ওই দুটি সপ্তাহ আংশিক।

ছবি ৩ (ডেলিভারি আর ফেরত): হার ০.০৮ থেকে ০.১৯-এর মধ্যে ওঠানামা করে, ৩ দিনের পরে কিছুটা বেশি, কিন্তু কোনো পরিষ্কার সরলরেখা নেই; ৬ দিনে ০.১৯ হলেও ৮ দিনে আবার ০.০৯. দলগুলো ছোট, তাই ওঠানামার বড় অংশ কাকতাল হতে পারে; সৎ লগ এন্ট্রি “দুর্বল ঝোঁক, প্রমাণ নয়”। শেষ টেবিল অংশ ৪-এর সঙ্গে মেলে।

seaborn দিয়ে একই ছবি: পর্ব ৩০-এর sns.boxplot(data=df, x="category", y="price", log_scale=True) বা sns.barplot (যা নিজে থেকে bootstrap ব্যবধান আঁকে) কম লাইনে একই কাজ করে। এখানে matplotlib ব্যবহার করা হয়েছে শুধু এই কারণে যে seaborn পাতায় প্রথমবার নামতে আরও ১০–২০ সেকেন্ড লাগে।

৬ · লক্ষ্যের সঙ্গে সম্পর্ক — segment-এর হার, n আর ব্যবধান

আসল প্রশ্ন: কোথায় ফেরত বেশি? একটি groupby আর গড় যথেষ্ট নয়: ৩টিতে ৩টি আর ৩০০টিতে ৩০০টি, দুটোই “১০০%”. তাই প্রতিটি হারের পাশে n আর একটি ব্যবধান (সত্যিকারের হার মোটামুটি কোথায় থাকতে পারে)।

অনুপাতের জন্য সবচেয়ে প্রচলিত ব্যবধানটি হলো p̂ ± 1.96 × √(p̂(1 − p̂)/n), যাকে Wald interval বলে; এখানে p̂ মানে নমুনার হার (k/n), আর 1.96 আসে normal distribution-এর ৯৫% থেকে (পর্ব ৩৭)। ছোট n বা চরম হারে এটি ভেঙে পড়ে: ৩/৩-এ p̂ = 1, তাই √(1 × 0) = 0, আর ব্যবধান [1, 1], যেন আমরা নিশ্চিত! এর ভালো বিকল্প Wilson interval:

কেন্দ্র = (p̂ + z²/2n) ÷ (1 + z²/n)  ·  অর্ধেক প্রস্থ = z × √(p̂(1 − p̂)/n + z²/4n²) ÷ (1 + z²/n)

এখানে z = 1.96 (৯৫%-এর জন্য)। সূত্রটি ব্যবধানের কেন্দ্রকে একটু ০.৫-এর দিকে টানে আর ছোট n-এ প্রস্থ বাড়ায়, তাই ৩/৩-এও একটি সৎ উত্তর দেয়। আরেকটি পথ bootstrap: সারিগুলো থেকে ফেরতসহ নমুনা বারবার টেনে হার হিসাব করা, যা পর্ব ৬০-এ metric-এর জন্য দেখেছেন। নিচের ব্লক দুটিই করে, তারপর একটি তৃতীয় প্রশ্ন করে: আমরা কতগুলো ছোট ঘর দেখেছি, আর কাকতালে কয়টিতে ১০০% আসত?

import numpy as np
from bazarghar_orders import load_orders

df = load_orders().query("order_date < '2026-09-01'")

def wilson(k, n, z=1.96):            # ৯৫% Wilson interval, অনুপাত k/n-এর জন্য
    p = k / n
    mid = (p + z**2 / (2 * n)) / (1 + z**2 / n)
    half = z * np.sqrt(p * (1 - p) / n + z**2 / (4 * n**2)) / (1 + z**2 / n)
    return mid - half, mid + half

def rate_table(by):
    t = df.groupby(by).returned.agg(k="sum", n="size")
    t["rate"] = t.k / t.n
    t["low"], t["high"] = wilson(t.k, t.n)
    return t.round(3).sort_values("rate", ascending=False)

print("সব অর্ডারে ফেরতের হার:", round(df.returned.mean(), 3), "n =", len(df))
print(rate_table("category"))
print(rate_table(["category", "payment"]).head(5))

rng = np.random.default_rng(62)      # bootstrap দিয়ে একই প্রশ্ন, clothing+COD
seg = df.query("category == 'clothing' and payment == 'COD'").returned.to_numpy()
boot = rng.choice(seg, size=(2000, seg.size)).mean(axis=1)
print("bootstrap ৯৫%:", np.percentile(boot, [2.5, 97.5]).round(3))

cells = ["district", "category", "payment"]     # ছোট ছোট ঘর
real = df.groupby(cells).returned.agg(["mean", "size"])
print("ঘর:", len(real), "| n ≤ 3 এমন ঘর:", int((real["size"] <= 3).sum()),
      "| ১০০% ফেরতের ঘর:", int((real["mean"] == 1).sum()))
fake = []                            # ফেরত এলোমেলো করে দিলে, অর্থাৎ কোনো আসল পার্থক্য নেই
for _ in range(200):
    y = rng.permutation(df.returned.to_numpy())
    fake.append(int((df.assign(returned=y).groupby(cells).returned.mean() == 1).sum()))
print("এলোমেলো label-এ ১০০% ঘর, গড়:", np.mean(fake), "| ২০০ বারের সর্বোচ্চ:", max(fake))

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4.6, pandas 3.0.2):

সব অর্ডারে ফেরতের হার: 0.122 n = 1291
              k    n   rate    low   high
category                                 
toys          3    3  1.000  0.438  1.000
clothing     82  389  0.211  0.173  0.254
books        24  246  0.098  0.066  0.141
electronics  15  195  0.077  0.047  0.123
beauty       15  198  0.076  0.046  0.121
home         19  260  0.073  0.047  0.111
                   k    n   rate    low   high
category payment                              
toys     mobile    1    1  1.000  0.207  1.000
         card      1    1  1.000  0.207  1.000
         COD       1    1  1.000  0.207  1.000
clothing COD      72  241  0.299  0.244  0.359
beauty   card      3   24  0.125  0.043  0.310
bootstrap ৯৫%: [0.241 0.357]
ঘর: 136 | n ≤ 3 এমন ঘর: 40 | ১০০% ফেরতের ঘর: 3
এলোমেলো label-এ ১০০% ঘর, গড়: 1.575 | ২০০ বারের সর্বোচ্চ: 5

প্রথম টেবিলের শীর্ষে toys: ৩/৩, হার ১.০০০. কিন্তু তার Wilson ব্যবধান ০.৪৩৮ থেকে ১.০০০; সত্যিকারের হার ৪৪% হলেও এই ফল অস্বাভাবিক নয়। দ্বিতীয় স্থানে clothing: ৩৮৯টির ৮২টি, ০.২১১, ব্যবধান ০.১৭৩ থেকে ০.২৫৪. এই ব্যবধান বাকি চারটি category-র ব্যবধানের (সর্বোচ্চ ০.১৪১) সঙ্গে একটুও মেলে না, তাই এটি একটি শক্ত পার্থক্য। Category × payment-এ ভাঙলে আসল ছবি: clothing + COD, ২৪১টির ৭২টি, ০.২৯৯ [০.২৪৪, ০.৩৫৯]. Bootstrap একই segment-এ [০.২৪১, ০.৩৫৭] দিয়েছে, Wilson-এর প্রায় সমান, কারণ n বড় আর হার চরম নয়।

শেষ দুই লাইন সবচেয়ে শিক্ষণীয়। জেলা × category × payment মিলিয়ে ১৩৬টি ঘর, যার ৪০টিতে ৩টি বা তার কম অর্ডার, আর বাস্তবে ৩টি ঘরে ১০০% ফেরত (toys-এর তিনটি অর্ডার, তিনটি আলাদা ঘরে)। Label এলোমেলো করলে (যেখানে কোনো ঘরেই আসল পার্থক্য নেই) ২০০ বারের গড়ে ১.৬টি ঘরে ১০০% আসে, কখনো ৫টি পর্যন্ত।

train অংশ · মোট ফেরতের হার 158/1291 = 0.122
ধাপ
toys
clothing + COD
পড়া
  1. লোভনীয় শিরোনাম। টেবিলের শীর্ষে toys, ১০০% ফেরত। পাশে clothing + COD-এর ২৯.৯% নিরীহ দেখায়।
  2. n দেখুন। toys-এর মাত্র ৩টি অর্ডার। একটি অর্ডার কম ফেরত এলেই হার ৬৭%, দুটি কম হলে ৩৩%. clothing + COD-এ ২৪১টি অর্ডার, একটির প্রভাব ০.৪ শতাংশ বিন্দু।
  3. ব্যবধান। Wilson ৯৫%: toys ০.৪৩৮ থেকে ১.০০০, প্রায় অর্ধেক পরিসর জুড়ে। clothing + COD ০.২৪৪ থেকে ০.৩৫৯, সরু, আর মোট হার ০.১২২ থেকে স্পষ্ট দূরে।
  4. কাকতালের সম্ভাবনা। সব অর্ডারের হার ০.১২২ হলে নির্দিষ্ট ৩টি অর্ডারের সবকটি ফেরত আসার সম্ভাবনা ০.১২২ × ০.১২২ × ০.১২২ ≈ ০.০০১৮. ছোট, তাই toys-কে উপেক্ষাও ঠিক নয়; কিন্তু হিসাবটি ধরে নেয় আমরা শুধু এই একটি ঘর দেখেছি।
  5. কতগুলো ঘর দেখেছি। আমরা ১৩৬টি ঘর একসাথে দেখেছি, যার ৪০টি খুব ছোট। এলোমেলো label-এর ২০০টি জগতে গড়ে ১.৬টি ঘরে ১০০% এসেছে। অনেক জায়গায় খুঁজলে চরম কিছু পাওয়া স্বাভাবিক; একে বলে multiple comparisons-এর সমস্যা (পর্ব ৩৭)।
  6. সিদ্ধান্ত। clothing + COD যায় মূল ফলে, n আর ব্যবধানসহ। toys যায় “নজরে রাখার” তালিকায়: নতুন category, জুন থেকে মাত্র ৩টি অর্ডার, আরও ডেটা জমলে আবার দেখা।

ছোট দলের চরম মান দুই দিকেই আসে। সবচেয়ে বেশি আর সবচেয়ে কম ফেরতের segment দুটোই প্রায়ই ছোট দল। দ্রুত কৌশল: হারকে n-এর বিপরীতে আঁকা; ছোট n-এর দিকে বিন্দুগুলো ফানেলের মতো ছড়ায়।

৭ · সন্দেহজনক ভালো সংকেত আর অবিশ্বাস্য মান — leaky কলাম, outlier আর EDA লগ

refund_taka কলামটি অভিধানে “কখন জানা যায়” ছাড়া এসেছিল। পরীক্ষাটি সরল: প্রতিটি সংখ্যার কলাম একাই label কতটা আলাদা করে? মাপ হিসেবে AUC (পর্ব ৬০): এলোমেলোভাবে নেওয়া একটি ফেরত অর্ডারের মান একটি না-ফেরত অর্ডারের চেয়ে বড় হওয়ার সম্ভাবনা। ০.৫ মানে কোনো আলাদা করার ক্ষমতা নেই; ১.০ বা ০.০ মানে perfect separation, একটি সীমা দিয়েই সব সারি নির্ভুল ভাগ হয়। কঠিন সমস্যায় একটি কলামের AUC ১.০ প্রায় সবসময় leakage-এর লক্ষণ। AUC এখানে rank দিয়ে হিসাব হয় (Mann–Whitney-এর সূত্র)।

Outlier-এর জন্য একটি সতর্কতা। পর্ব ৩৭-এর IQR নিয়ম (Q3 + 1.5 × IQR-এর ওপরে) পুরো দামের কলামে চালালে ১২৯১টির ১৮৪টি সারি “outlier” হয়, books-এর ভেতরেও ১১টি (একই ডেটায় ডেস্কটপে গোনা); বেশিরভাগই সাধারণ দামি পণ্য, কারণ দাম ডানে হেলানো। তাই দুটি বদল: দামের log, আর প্রতিটি category-র ভেতরে median ও MAD দিয়ে একটি robust z-score: (x − median) ÷ (1.4826 × MAD). ১.৪৮২৬ গুণকটি normal ডেটায় MAD-কে standard deviation-এর মাপে আনে। সীমা ৫, ইচ্ছে করে কড়া। ব্লকের শেষে আগের ব্লকগুলোর খোঁজ মিলিয়ে EDA লগ ছাপা হয়।

import numpy as np
from bazarghar_orders import load_orders

df = load_orders().query("order_date < '2026-09-01'")
y = df.returned
log = [  # EDA লগ: (প্রশ্ন, প্রমাণ, সিদ্ধান্ত); আগের ব্লকের ফল হাতে লেখা
    ("delivery_days কেন ফাঁকা?", "courier B 36.2%, A 0.8%", "ফাঁকার indicator রাখা"),
    ("জেলার নাম এক রকম?", "Chittagong 57, Chattogram 185", "এক বানানে মেলানো"),
    ("অর্ডারে মৌসুমি চূড়া?", "মার্চের ৩ সপ্তাহ median-এর দ্বিগুণ+", "সময় ধরে ভাগ"),
    ("কোথায় ফেরত বেশি?", "clothing+COD 0.299 [0.244, 0.359]", "ব্যবসার দলকে জানানো")]

def auc(x, y):                            # একটি কলাম একাই label কতটা আলাদা করে
    r = x.rank()
    n1, n0 = y.sum(), (1 - y).sum()
    return (r[y == 1].sum() - n1 * (n1 + 1) / 2) / (n1 * n0)

for c in ["price", "delivery_days", "refund_taka"]:
    a = auc(df[c].fillna(df[c].median()), y)
    lo1, hi0 = df.loc[y == 1, c].min(), df.loc[y == 0, c].max()
    print(f"{c:14} AUC {a:.3f} | ফেরতে min {lo1:6.0f} | না-ফেরতে max {hi0:6.0f}")
    if a > 0.95 or a < 0.05:
        log.append((f"{c} একাই label বলে দেয়?", f"AUC {a:.3f}", "leakage: feature থেকে বাদ"))

lp = np.log10(df.price)                   # দামের outlier: category-র ভেতরে, log স্কেলে
g = lp.groupby(df.category)
mad = g.transform(lambda s: (s - s.median()).abs().median())
z = (lp - g.transform("median")) / (1.4826 * mad)
out = df[z.abs() > 5]
print(out[["order_id", "order_date", "category", "price"]].to_string(index=False))
for i, r in out.iterrows():
    log.append((f"অর্ডার {r.order_id}-এর দাম ঠিক?", f"robust z {z[i]:.1f}", "উৎসে যাচাই"))

print("\nEDA লগ:")
for k, (q, ev, dec) in enumerate(log, 1):
    print(f"{k}. {q}\n   প্রমাণ: {ev}\n   সিদ্ধান্ত: {dec}")

আসল আউটপুট (ব্রাউজারের Python, pandas 3.0.2):

price          AUC 0.512 | ফেরতে min    130 | না-ফেরতে max  65000
delivery_days  AUC 0.547 | ফেরতে min      1 | না-ফেরতে max     12
refund_taka    AUC 1.000 | ফেরতে min    100 | না-ফেরতে max      0
 order_id order_date category   price
    10345 2026-02-04    books 65000.0

EDA লগ:
1. delivery_days কেন ফাঁকা?
   প্রমাণ: courier B 36.2%, A 0.8%
   সিদ্ধান্ত: ফাঁকার indicator রাখা
2. জেলার নাম এক রকম?
   প্রমাণ: Chittagong 57, Chattogram 185
   সিদ্ধান্ত: এক বানানে মেলানো
3. অর্ডারে মৌসুমি চূড়া?
   প্রমাণ: মার্চের ৩ সপ্তাহ median-এর দ্বিগুণ+
   সিদ্ধান্ত: সময় ধরে ভাগ
4. কোথায় ফেরত বেশি?
   প্রমাণ: clothing+COD 0.299 [0.244, 0.359]
   সিদ্ধান্ত: ব্যবসার দলকে জানানো
5. refund_taka একাই label বলে দেয়?
   প্রমাণ: AUC 1.000
   সিদ্ধান্ত: leakage: feature থেকে বাদ
6. অর্ডার 10345-এর দাম ঠিক?
   প্রমাণ: robust z 11.0
   সিদ্ধান্ত: উৎসে যাচাই

price (AUC ০.৫১২) আর delivery_days (০.৫৪৭) একা প্রায় কিছুই আলাদা করে না; এটিই স্বাভাবিক, ফেরতের পূর্বাভাস কঠিন। refund_taka-র AUC ঠিক ১.০০০: প্রতিটি ফেরত অর্ডারে অন্তত ১০০ টাকা, প্রতিটি না-ফেরত অর্ডারে ঠিক ০. দুই দলের পরিসর একটুও মেলে না। টাকা ফেরত হয় পণ্য ফেরতের পরে, তাই অর্ডারের মুহূর্তে এই কলাম সবসময় ০; এটি label-এর ছায়া, অর্থাৎ target leakage.

Outlier পরীক্ষা train অংশে ঠিক একটি সারি ধরেছে: অর্ডার ১০৩৪৫, ফেব্রুয়ারির একটি বই, ৬৫,০০০ টাকা, robust z ১১.০. books-এর পরের সবচেয়ে দামি অর্ডার ১৩৭০ টাকা। সম্ভাব্য ব্যাখ্যা: ৬৫০ টাকা লিখতে গিয়ে দুটি শূন্য বেশি। কিন্তু EDA-র সিদ্ধান্ত “মুছে ফেলা” নয়, “উৎসে যাচাই”: হয়তো সত্যিই একটি দুর্লভ বইয়ের সংগ্রহ। নীরবে মুছে ফেলা outlier পরে কেউ খুঁজে পায় না।

লগটি দেখুন: ছয়টি এন্ট্রি, প্রতিটিতে প্রশ্ন, প্রমাণ (একটি সংখ্যা), আর একটি কাজের সিদ্ধান্ত। প্রথম চারটি আগের ব্লকগুলোর ফল থেকে হাতে লেখা, শেষ দুটি এই ব্লকের পরীক্ষা নিজেই যোগ করেছে। বাস্তবে লগ প্রায়ই একটি markdown ফাইল; কোডে রাখলে নতুন ডেটায় একই পরীক্ষা আবার চালানো যায়।

লুকোনো ছয়টি জিনিসই ধরা পড়েছে, প্রতিটি আলাদা কৌশলে: value_counts, দল ধরে ফাঁকার হার, category-র ভেতরে log-স্কেল আর robust z, সময়ের line chart, n আর ব্যবধানসহ হার, আর একার AUC. কোনো একটি function সব ধরত না।

৮ · EDA নিজেই যখন leakage — test set, চোখ আর বাছাইয়ের পথ

পর্ব ৫৬-এ দেখেছেন, পুরো ডেটায় feature বাছলে এলোমেলো ডেটাতেও cross-validation ০.৯১ দেখায়। EDA-তে বাছাই করেন আপনি নিজে: কোন কলাম রাখবেন, outlier-এর সীমা, কোন segment feature হবে, ফাঁকা ঘরে কী বসবে। প্রতিটি সিদ্ধান্ত পরে model-এর অংশ। Test দেখে এসব ঠিক করলে test-এর তথ্য আপনার মাথার ভেতর দিয়ে model-এ ঢোকে। EDA-ও শেখার অংশ, তাই test থেকে দূরে।

কাজকোথায়কেন
ফাইল খোলে কি না, সারি ও কলামের সংখ্যা, তারিখের পরিসরপুরো ফাইলভাগ করার জন্যই এটুকু লাগে; label বা বণ্টন দেখা নয়
Label-এর সঙ্গে সম্পর্ক, segment-এর হার, AUCশুধু trainসরাসরি লক্ষ্য দেখা; test-এ করলে এটিই test-এ বাছাই
Outlier-এর সীমা, imputation-এর মান, কোন কলাম বাদশুধু trainএগুলো শেখা মান; test-এ শুধু প্রয়োগ হবে
বানান মেলানোর নিয়ম, অচেনা মান হলে কী হবেtrain-এ নিয়ম, test-এ প্রয়োগtest-এ নতুন বানান এলে pipeline-এর সতর্কসংকেত দেখাবে, আপনার চোখ নয়
Test-এর তথ্য জানার ইচ্ছাএকেবারে শেষে, একবারমূল্যায়নের দিন; তারপর কিছু বদলালে রিপোর্টে লিখতে হবে

এই পর্বে তাই প্রতিটি ব্লকের প্রথম লাইন query("order_date < '2026-09-01'"), আর তালাবন্ধ ২০৯টি অর্ডারের একটি সংখ্যাও কোথাও ছাপা হয়নি। আরেকটি সূক্ষ্ম রূপ train-এর ভেতরেও আছে: আপনি যত বেশি segment, chart আর সীমা পরীক্ষা করবেন, তত বেশি সম্ভাবনা যে কোনো কাকতালীয় ধরন “আবিষ্কার” হিসেবে টিকে যাবে। পরিসংখ্যানবিদ Andrew Gelman আর Eric Loken এর নাম দিয়েছেন garden of forking paths: প্রতিটি “এটা দেখি, ওটা বাদ দিই” একটি শাখা, আর শেষে যে পথটি পাওয়া যায় তা বহু পথের মধ্যে বাছা। প্রতিকার: প্রশ্ন আগে লেখা, লগে নেতিবাচক ফলও রাখা, আর বড় দাবি নতুন ডেটায় বা validation-এ যাচাই করা।

Exploratory বনাম confirmatory: Tukey-র পার্থক্যটি এখানে কাজে লাগে। EDA-তে পাওয়া “clothing + COD-এ ফেরত বেশি” একটি অনুমান, যা train-এর দশ মাস থেকে এসেছে। এটি সত্যি কি না, তা নিশ্চিত হয় এমন ডেটায়, যা অনুমানটি গড়তে ব্যবহার হয়নি: validation অংশ, পরের মাসের অর্ডার, বা শেষে test. EDA প্রশ্ন জন্ম দেয়; প্রমাণ আসে অন্য ডেটা থেকে।

৯ · EDA রিপোর্ট — একটি ছোট ছাঁচ

EDA-র শেষ ফল কোনো notebook-ভরা chart নয়; এক-দুই পাতার একটি রিপোর্ট, যা পড়ে একজন সহকর্মী (বা ছয় মাস পরের আপনি) বুঝবেন কী দেখা হয়েছে, কী পাওয়া গেছে আর কী সিদ্ধান্ত হয়েছে। নিচের ছাঁচটি বাজারঘরের ফল দিয়ে ভরা; নিজের প্রকল্পে শিরোনামগুলো রেখে বিষয়বস্তু বদলান।

# EDA রিপোর্ট: বাজারঘরের অর্ডার ফেরত (train অংশ)

১. প্রশ্ন ও প্রসঙ্গ
   অর্ডারের মুহূর্তে ফেরতের ঝুঁকি বলা, যাতে ফোনে নিশ্চিত করা যায়।
   লক্ষ্য: returned (0/1). Model চলবে নতুন মাসের অর্ডারে।

২. ডেটা ও অভিধান
   ১৫০০টি অর্ডার, নভেম্বর ২০২৫ থেকে অক্টোবর ২০২৬, ১১টি কলাম।

৩. কী আলাদা রাখা হয়েছে
   সেপ্টেম্বর ও অক্টোবর ২০২৬ (২০৯টি) test, খোলা হয়নি।
   EDA: বাকি ১২৯১টি। মোট ফেরতের হার 0.122.

৪. গুণমানের সমস্যা ও সিদ্ধান্ত
   - delivery_days ফাঁকা 15.3%; courier B 36.2%, A 0.8% → indicator রাখা
   - Chittagong (57, সব web) আর Chattogram একই জেলা → এক বানান
   - অর্ডার 10345: বইয়ের দাম 65000 (robust z 11.0) → উৎসে যাচাই চলছে
   - অভিধানে নেই এমন category: toys (3টি, জুন থেকে)

৫. মূল ফল (n আর ৯৫% ব্যবধানসহ)
   - clothing + COD: 72/241 = 0.299 [0.244, 0.359]; বাকি প্রায় সব ঘর 0.05–0.12
   - মার্চের প্রথম তিন সপ্তাহে অর্ডার median-এর দ্বিগুণের বেশি
   - ডেলিভারির দিন ও ফেরত: দুর্বল, অনিয়মিত ঝোঁক; প্রমাণ নয়

৬. Leakage-এর সন্দেহ
   - refund_taka: AUC 1.000, ফেরতের পরে লেখা হয় → feature থেকে বাদ

৭. পরের ধাপের সিদ্ধান্ত
   - সময় ধরে validation; মার্চ কোন ভাগে পড়ে তা উল্লেখ
   - feature: category × payment, মাস, delivery_days + ফাঁকার indicator
   - সব imputation আর mapping pipeline-এর ভেতরে

৮. খোলা প্রশ্ন ও সীমা
   - courier B কেন দিন জানায় না? (MNAR কি না ডেটা দিয়ে বলা যায় না)
   - toys: আরও ডেটা জমলে আবার দেখা
   - ডেটা কাল্পনিক ও ছোট; সংখ্যাগুলো এই একটি seed-এর

রিপোর্টের তিনটি অংশ সবচেয়ে বেশি বাদ পড়ে, আর সেগুলোই সবচেয়ে কাজের: অংশ ৩ (কী আলাদা রাখা), অংশ ৫-এর n আর ব্যবধান, আর অংশ ৮ (খোলা প্রশ্ন)। ডেটাসেটের “datasheet”-এর ধারণা পর্ব ৭২ (দায়িত্বশীল AI-এর অভ্যাস — bias যাচাই, গোপনীয়তা আর নথিপত্র)-এ আসবে। পর্ব ৬৩ এই রিপোর্টের অংশ ৭ থেকে feature বানাবে, আর পর্ব ৬৪ অংশ ৪ ও ৬-এর সিদ্ধান্তগুলো একটি leakage-মুক্ত pipeline-এ বসাবে।

বাজারঘরের ডেটা কীভাবে বানানো (EDA শেষ করার পরে পড়ুন)

নিচের মডিউলটিই পাতার প্রতিটি ব্লক ব্যবহার করে। ডেস্কটপে এটি bazarghar_orders.py নামে রেখে একই ফোল্ডারে ব্লকগুলো চালালে একই ডেটা পাবেন (লেখার কলামের dtype আর পূর্ণসংখ্যার আকার ছাড়া)। মিলিয়ে দেখুন, EDA-র খোঁজগুলো কোথায় রাখা হয়েছিল।

"""বাজারঘরের ১৫০০টি কাল্পনিক অর্ডার, নভেম্বর ২০২৫ থেকে অক্টোবর ২০২৬ (seed 62)"""
import numpy as np
import pandas as pd

def load_orders(seed=62):
    rng = np.random.default_rng(seed)
    n = 1500
    days = pd.date_range("2025-11-01", "2026-10-31", freq="D")
    w = np.where((days >= "2026-03-01") & (days <= "2026-03-19"), 3.5, 1.0)
    date = rng.choice(days, size=n, p=w / w.sum())
    dist = rng.choice(["Dhaka", "Chattogram", "Khulna", "Rajshahi", "Sylhet",
                       "Barishal", "Rangpur", "Mymensingh"], size=n,
                      p=[.34, .18, .1, .09, .08, .07, .07, .07])
    channel = rng.choice(["app", "web"], size=n, p=[.65, .35])
    north = np.isin(dist, ["Rangpur", "Barishal", "Mymensingh", "Rajshahi"])
    courier = np.where(rng.random(n) < np.where(north, .8, .25), "B", "A")
    cat = rng.choice(["clothing", "electronics", "books", "home", "beauty"],
                     size=n, p=[.32, .14, .2, .2, .14])
    pay = rng.choice(["COD", "mobile", "card"], size=n, p=[.55, .33, .12])
    mu = pd.Series({"clothing": 7.0, "electronics": 8.6, "books": 6.0,
                    "home": 7.2, "beauty": 6.5})[cat].to_numpy()
    price = np.round(np.exp(rng.normal(mu, 0.45)), -1)
    deliv = np.clip(np.round(rng.gamma(2.2, 1.3, n) + np.where(north, 1.5, 0.5)), 1, 12)
    p_ret = np.where((cat == "clothing") & (pay == "COD"), 0.30, 0.06) + 0.01 * (deliv - 3)
    returned = (rng.random(n) < np.clip(p_ret, 0.01, 0.9)).astype(int)
    df = pd.DataFrame({"order_id": np.arange(10001, 10001 + n), "order_date": date,
                       "district": dist, "channel": channel, "courier": courier,
                       "category": cat, "payment": pay, "price": price,
                       "delivery_days": deliv, "returned": returned})
    df = df.sort_values("order_date", ignore_index=True)
    df["order_id"] = np.arange(10001, 10001 + n)
    # পুরোনো web ফর্মের কিছু অর্ডারে জেলার পুরোনো বানান
    old = (df.district == "Chattogram") & (df.channel == "web") & (rng.random(n) < .6)
    df.loc[old, "district"] = "Chittagong"
    # courier B অনেক সময় ডেলিভারির দিন জানায় না
    miss = rng.random(n) < np.where(df.courier == "B", .38, .01)
    df.loc[miss, "delivery_days"] = np.nan
    # একটি বইয়ের দামে দুটি শূন্য বেশি
    i = df.index[(df.category == "books") & (df.order_date.dt.month == 2)][3]
    df.loc[i, "price"] = df.loc[i, "price"] * 100
    # তিনটি নতুন খেলনার অর্ডার, তিনটিই ফেরত
    t = df.index[df.order_date.dt.month == 6][[5, 40, 77]]
    df.loc[t, ["category", "returned"]] = ["toys", 1]
    # ফেরতের পরে লেখা হয় এমন কলাম
    df["refund_taka"] = np.where(df.returned == 1, np.round(df.price * rng.uniform(.8, 1, n), -1), 0.0)
    return df

খেয়াল করুন, ডেলিভারির দিন বাড়লে ফেরতের সম্ভাবনা সত্যিই একটু বাড়ানো ছিল (প্রতি দিনে ১ শতাংশ বিন্দু), অথচ অংশ ৫-এর ছবিতে তা “দুর্বল, অনিয়মিত ঝোঁক” হিসেবেই দেখা গেছে। ছোট প্রভাব ছোট ডেটায় এভাবেই ঝাপসা থাকে।

১০ · AI-এর যুগে — AI assistant-এর করা EDA যাচাই

একটি CSV দিয়ে “EDA চাই” বললে AI assistant কয়েক সেকেন্ডে বিশটি chart আর একটি আত্মবিশ্বাসী সারাংশ দেয়। গতিটা কাজের, কিন্তু ঝুঁকিগুলো এই পর্বের প্রতিটি অংশের সঙ্গে মেলে।

AI যা প্রায়ই করেকীভাবে ধরবেন
পুরো ফাইলে EDA, test আলাদা না করেইপ্রথম cell-এ ভাগ আছে কি না দেখুন; না থাকলে নিজে আগে ভাগ করে শুধু train দিন
ছোট segment-এর ১০০% বা ০%-কে “মূল insight” বলেপ্রতিটি হারের পাশে n আর ব্যবধান চান; n < ৩০ হলে আলাদা চিহ্ন
refund_taka-র মতো কলামকে সেরা feature বলেশীর্ষ সংকেতের প্রতিটির জন্য জিজ্ঞেস করুন “এই মান কখন জানা যায়”
ফাঁকা সারি dropna() করে বা গড় দিয়ে ভরে চুপচাপ এগোয়ফাঁকা হওয়া কোন দলে বেশি, তা আগে দেখতে বলুন; সারি ফেলার আগে কোন দল হারাল গুনুন
পুরোনো API: df.append, boxplot(labels=…), ভুল dtype-এর ধারণানিজের version-এর docs দেখুন; matplotlib 3.9 থেকে tick_labels, pandas 3-এ লেখার dtype str

AI-কে কাজের ধারাটি বলে দিন, আর ডেটার জায়গায় data dictionary দিন। আসল গ্রাহকের নাম, ফোন নম্বর বা ঠিকানা কখনো পেস্ট করবেন না; কলামের নাম আর কয়েকটি কাল্পনিক সারিই যথেষ্ট।

আমার orders.csv-এর কলাম: order_id, order_date, district, channel, courier, category, payment, price, delivery_days, returned (লক্ষ্য, 0/1), refund_taka. Python 3.14, pandas 3 আর matplotlib 3.10 দিয়ে একটি EDA কোড লেখো যা: ১. প্রথমেই order_date ধরে শেষ দুই মাস আলাদা করবে আর তারপর আর কখনো সেটি ছোঁবে না। ২. প্রতিটি ধাপের আগে comment-এ প্রশ্নটি লিখবে। ৩. প্রতিটি কলামের dtype, ফাঁকার সংখ্যা, আর ফাঁকার হার courier, district আর returned ধরে ছাপাবে। ৪. category আর category × payment ধরে ফেরতের হার ছাপাবে, পাশে k, n আর ৯৫% Wilson interval. ৫. প্রতিটি সংখ্যার কলামের একার AUC ছাপাবে আর 0.95-এর বেশি হলে "leakage সন্দেহ" লিখবে। ৬. দামের outlier category-র ভেতরে log স্কেলে robust z দিয়ে খুঁজবে, কোনো সারি মুছবে না, শুধু তালিকা দেবে। ৭. শেষে (প্রশ্ন, প্রমাণ, সিদ্ধান্ত) আকারে একটি EDA লগ ছাপাবে। কোনো deprecated API ব্যবহার করবে না।

AI-এর দেওয়া EDA হাতে পেলে — যাচাইয়ের চেকলিস্ট

আরও গভীরে: ফাঁকা ডেটার MCAR/MAR ভাষা এসেছে Donald Rubin-এর ১৯৭৬ সালের Biometrika-র প্রবন্ধ “Inference and Missing Data” থেকে। অনুপাতের ব্যবধানে Edwin B. Wilson ১৯২৭ সালে তাঁর পদ্ধতি প্রকাশ করেন, আর Lawrence Brown, T. Tony Cai ও Anirban DasGupta ২০০১ সালে Statistical Science-এ দেখান যে প্রচলিত Wald interval বড় n-এও অপ্রত্যাশিতভাবে খারাপ আচরণ করতে পারে, এবং Wilson-এর মতো বিকল্পকে সুপারিশ করেন। ০টি ঘটনায় দ্রুত নিয়ম “rule of three”: ৯৫% ঊর্ধ্বসীমা মোটামুটি 3/n.

পরীক্ষা ও ইন্টারভিউতে:

  • EDA কেন শুধু train-এ? EDA-র সিদ্ধান্তও শেখার অংশ; test দেখে নিলে test-এর তথ্য model-এ ঢোকে।
  • ৪০টি অর্ডারে একটিও ফেরত নেই; ৯৫% ঊর্ধ্বসীমা মোটামুটি কত? Rule of three: ৩ ÷ ৪০ = ০.০৭৫.
  • একটি কলাম একাই AUC ১.০ দিলে কী করবেন? সেটি কখন জানা যায় দেখবেন; প্রায় সবসময় leakage, বাদ দিয়ে কারণ লগে লিখবেন।
  • MAR আর MNAR আলাদা করা যায় কীভাবে? শুধু দেখা ডেটা দিয়ে পুরোপুরি যায় না; ডেটা তৈরির প্রক্রিয়া জানতে হয়।
  • Skewed দামে IQR নিয়ম কেন অনেক outlier দেখায়? লম্বা ডান লেজে অনেক সাধারণ মানও সীমার বাইরে পড়ে; log স্কেল আর দলের ভেতরে robust z ভালো।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Tukey কেন exploratory আর confirmatory বিশ্লেষণ আলাদা রাখতে বলেছিলেন, আর আধুনিক machine learning-এর কাজের ধারায় সেই পার্থক্য কোথায় টিকে আছে?

Tukey-র সময়ে পরিসংখ্যানের মূলধারা ছিল confirmatory: আগে একটি অনুমান, তারপর ডেটা সংগ্রহ, তারপর একটি test যা বলে অনুমানটি টেকে কি না। ১৯৬২ সালের “The Future of Data Analysis” প্রবন্ধে Tukey যুক্তি দেন যে এই কাঠামো ডেটা বিশ্লেষণের বড় অংশ বাদ দেয়: বাস্তবে অনেক সময় আমরা জানিই না কোন প্রশ্ন করা উচিত, আর ডেটা দেখেই প্রশ্ন জন্মায়। প্রায়ই উদ্ধৃত হয় তাঁর একটি ভাবনা, মোটামুটি এরকম: ভুল প্রশ্নের নিখুঁত উত্তরের চেয়ে সঠিক প্রশ্নের আনুমানিক উত্তর অনেক ভালো।

কিন্তু Tukey খোঁজাকে প্রমাণের বিকল্প বলেননি। দুটির দায়িত্ব আলাদা। খোঁজার সময় আপনি স্বাধীন: যেকোনো chart, যেকোনো ভাগ, যেকোনো রূপান্তর। কিন্তু সেই স্বাধীনতার দাম হলো, এই পর্যায়ে পাওয়া ধরন প্রমাণ নয়, কারণ আপনি বহু পথ দেখে সবচেয়ে আকর্ষণীয়টি বেছেছেন। Gelman ও Loken-এর “garden of forking paths” আর p-hacking-এর আলোচনা এই সমস্যারই আধুনিক রূপ।

Machine learning-এর কাজের ধারায় এই পার্থক্য খুব স্পষ্টভাবে টিকে আছে, শুধু নাম বদলেছে। Train অংশে EDA আর feature-এর ধারণা তৈরি হলো খোঁজা; validation বা cross-validation-এ model আর সেটিং বাছাই হলো আধা-খোঁজা; আর শেষে একবারের test হলো confirmatory ধাপ। পর্ব ৫৬-এর “test একবারই” নিয়ম আসলে Tukey-র পার্থক্যের প্রয়োগ।

এই পর্বের উদাহরণে clothing + COD-এর ০.২৯৯ train-এর দশ মাসে পাওয়া একটি খোঁজ। n বড় আর ব্যবধান সরু বলে এটি বিশ্বাসযোগ্য, তবু “প্রমাণিত” বলা যাবে তখনই, যখন নতুন মাসের ডেটায় একই ধরন দেখা যাবে।

মূল উপলব্ধি: EDA প্রশ্ন জন্ম দেয়, প্রমাণ দেয় না; খোঁজা আর প্রমাণের ডেটা আলাদা রাখাই Tukey-র শিক্ষার সবচেয়ে টেকসই অংশ।

প্র ০২একই mean, variance আর correlation, অথচ ছবি একেবারে আলাদা: Anscombe-এর quartet আর পরের “Datasaurus” কেন EDA-তে chart-কে অপরিহার্য করে?

১৯৭৩ সালে পরিসংখ্যানবিদ Francis Anscombe The American Statistician-এ চারটি ছোট ডেটাসেট প্রকাশ করেন, প্রতিটিতে ১১টি বিন্দু। চারটির x আর y-এর mean, variance, correlation, এমনকি regression রেখাও প্রায় হুবহু এক। অথচ ছবিতে একটি সাধারণ রৈখিক মেঘ, একটি সুন্দর বাঁকা রেখা, একটি নিখুঁত রেখা যার একটি বিন্দু দূরে, আর একটি যেখানে প্রায় সব x একই, শুধু একটি বিন্দু পুরো সম্পর্ক তৈরি করে। Anscombe দেখাতে চেয়েছিলেন যে সংখ্যার সারাংশ আর ছবি দুটোই লাগে। Anscombe's quartet পর্ব ৩৭-এও দেখেছেন।

২০১৭ সালে Justin Matejka আর George Fitzmaurice CHI সম্মেলনে বিষয়টিকে আরও নাটকীয় করেন। Alberto Cairo-র আঁকা একটি ডাইনোসর-আকৃতির বিন্দুর মেঘ থেকে শুরু করে তাঁরা simulated annealing দিয়ে বিন্দুগুলো একটু একটু সরান, শর্ত রেখে যে সারাংশ পরিসংখ্যান দশমিকের কয়েক ঘর পর্যন্ত বদলাবে না। ফল “Datasaurus Dozen”: তারকা, বৃত্ত, সমান্তরাল রেখা, সবকটির একই সারাংশ।

EDA-তে এর ব্যবহারিক অর্থ এই পর্বেই দেখেছেন। describe দামের সর্বোচ্চ ৬৫০০০ দেখিয়েছিল, কিন্তু সেটি books-এর ভেতরে কতটা অস্বাভাবিক, তা বোঝা গেছে category ধরে box plot-এ। সপ্তাহের line chart না আঁকলে মার্চের চূড়া একটি গড়ের ভেতরে মিশে থাকত। উল্টো দিকে, শুধু ছবিও যথেষ্ট নয়: ডেলিভারির দিন ধরে ফেরতের bar chart একটি ঝোঁক “দেখায়”, কিন্তু n আর ব্যবধান ছাড়া সেটি কতটা বিশ্বাসযোগ্য তা বলা যায় না।

আরেকটি সূক্ষ্ম কথা: ছবিও একটি সারাংশ। Histogram-এর bin, box plot-এর whisker, log বা linear axis, এসব সিদ্ধান্ত কোনো ধরন লুকাতে বা দেখাতে পারে (পর্ব ৩২)।

মূল উপলব্ধি: সারাংশ সংখ্যা আর ছবি একে অপরের পরিপূরক; একটি অন্যটির ভুল ধরে, আর EDA-তে দুটোই লাগে।

প্র ০৩“ছোট দলই সবচেয়ে চরম”: Howard Wainer যাকে “সবচেয়ে বিপজ্জনক সমীকরণ” বলেছেন, তা segment তুলনায় কী শেখায়?

পরিসংখ্যানবিদ Howard Wainer ২০০৭ সালে American Scientist-এ “The Most Dangerous Equation” নামে একটি প্রবন্ধ লেখেন। সমীকরণটি de Moivre-এর নামে পরিচিত একটি সরল সম্পর্ক: নমুনার গড়ের standard error হলো σ ÷ √n, অর্থাৎ ছোট নমুনার গড় বেশি দোলে। Wainer-এর যুক্তি, এটি ভুলে যাওয়ার ফল খুব ব্যয়বহুল।

তাঁর সবচেয়ে আলোচিত উদাহরণ যুক্তরাষ্ট্রের স্কুল। পরীক্ষার ফলের তালিকায় শীর্ষ স্কুলগুলোর মধ্যে ছোট স্কুল বেশি দেখে কিছু উদ্যোগ ছোট স্কুল গড়ায় বিপুল বিনিয়োগ করে। Wainer দেখান যে তালিকার একেবারে নিচেও ছোট স্কুল বেশি; ছোট স্কুল ভালো বা খারাপ নয়, তাদের গড় শুধু বেশি দোলে। (উদ্যোগগুলোর সাফল্য বা ব্যর্থতার বিস্তারিত নিয়ে বিতর্ক আছে, তবে পরিসংখ্যানের বিষয়টি স্পষ্ট।) রোগের মানচিত্রেও একই ব্যাপার: কম জনসংখ্যার এলাকায় হার সবচেয়ে বেশি আর সবচেয়ে কম, দুই রকমই দেখা যায়।

বাজারঘরের toys ঠিক এই ঘটনা। ৩টি অর্ডারে হার কেবল ০, ৩৩, ৬৭ বা ১০০% হতে পারে; চরম হওয়াটাই স্বাভাবিক। প্রতিকার তিনটি। এক, প্রতিটি হারের পাশে n আর Wilson-এর মতো ব্যবধান, যা ছোট n-এ নিজে থেকেই চওড়া। দুই, “funnel plot”: হারকে n-এর বিপরীতে আঁকা, সঙ্গে প্রত্যাশিত দোলার সীমা; David Spiegelhalter ২০০৫ সালে হাসপাতালের ফল তুলনায় এটি জনপ্রিয় করেন। তিন, shrinkage: ছোট দলের হারকে মোট হারের দিকে কিছুটা টেনে আনা, যার একটি রূপ target encoding-এর আলোচনায় (পর্ব ৬৩) আসবে।

একটি সতর্কতা উল্টো দিকেও। ছোট দল উপেক্ষা করাও ঠিক নয়: toys-এর ৩/৩ কাকতালে আসার সম্ভাবনা ০.০০২-এর কাছাকাছি, আর এটি একটি নতুন category. সঠিক ভাষা “নজরে রাখার মতো”, “প্রমাণিত” বা “অর্থহীন” কোনোটিই নয়।

মূল উপলব্ধি: ছোট দলের চরম হার প্রথমে তার আকারের কথা বলে, তারপর হয়তো তার আচরণের; n আর ব্যবধান না দেখে কোনো দলকে সেরা বা সবচেয়ে খারাপ বলবেন না।

প্র ০৪Rubin-এর MCAR/MAR/MNAR ভাষা বাস্তবে কতটা কাজে লাগে, যখন MAR আর MNAR ডেটা দিয়ে আলাদা করাই যায় না?

Donald Rubin ১৯৭৬ সালে Biometrika-য় প্রশ্ন তোলেন: কোন অবস্থায় ফাঁকা ঘর উপেক্ষা করে বিশ্লেষণ করলেও সঠিক সিদ্ধান্ত পাওয়া যায়? তাঁর উত্তর থেকে আসে তিনটি শ্রেণি। MCAR-এ ফাঁকা হওয়া কিছুর সঙ্গে সম্পর্কিত নয়, তাই সম্পূর্ণ সারিগুলো একটি এলোমেলো নমুনা। MAR-এ ফাঁকা হওয়া শুধু দেখা কলামের ওপর নির্ভর করে, তাই সেই কলামগুলো ধরে ঠিকভাবে মডেল করলে পক্ষপাত সামলানো যায়। MNAR-এ ফাঁকা হওয়া হারানো মানটির ওপরই নির্ভর করে, আর তখন দেখা ডেটা একা যথেষ্ট নয়।

সমস্যাটা হলো, MAR আর MNAR-এর পার্থক্য হারানো মানের ওপর নির্ভর করে, যা আমাদের হাতে নেই। বাজারঘরে দেখলাম ফাঁকা হওয়া courier-এর সঙ্গে জোরালোভাবে জড়িত, যা MCAR বাতিল করে আর MAR-এর সঙ্গে মেলে। কিন্তু courier B যদি দেরির ডেলিভারিতেই বেশি ঘর ফাঁকা রাখে, তাহলে MNAR; আর ডেটার কোনো chart তা দেখাবে না। তাই এই শ্রেণিগুলো প্রমাণের নয়, অনুমানের ভাষা: “আমরা MAR ধরে নিচ্ছি, কারণ …”, তারপর sensitivity analysis, অর্থাৎ MNAR-এর কয়েকটি রূপ ধরে ফল কতটা বদলায় তা দেখা।

ভাষাটা তবু খুব কাজের, কারণ এটি সঠিক প্রশ্ন করায়। EDA-তে তিনটি ব্যবহারিক ধাপ আসে এখান থেকে। এক, ফাঁকা হওয়ার indicator-কে দল, লক্ষ্য আর সময় ধরে দেখা, যা এই পর্বের stepper-এ করেছেন। দুই, ডেটা তৈরির প্রক্রিয়া জানা: কোন সিস্টেম কলামটি লেখে, কখন লেখে না। তিন, model-এর জন্য সিদ্ধান্ত: সম্পূর্ণ সারি বাদ দেওয়া MCAR ছাড়া পক্ষপাত আনতে পারে; imputation MAR-এ যুক্তিসঙ্গত; আর ফাঁকা হওয়ার indicator অনেক সময় model-কে MNAR-এর কিছু সংকেতও ধরতে দেয়।

চিকিৎসার ডেটায় একটি পরিচিত ধরন: কোনো পরীক্ষার ফল ফাঁকা থাকা মানে প্রায়ই ডাক্তার পরীক্ষাটি দরকারই মনে করেননি; সেখানে ফাঁকা হওয়াটাই রোগীর অবস্থার তথ্য।

মূল উপলব্ধি: MCAR/MAR/MNAR ডেটা থেকে পুরোপুরি প্রমাণ করা যায় না, কিন্তু এই ভাষা আপনাকে ফাঁকা হওয়ার কারণ খুঁজতে আর নিজের অনুমান লিখে রাখতে বাধ্য করে।

প্র ০৫স্বয়ংক্রিয় profiling tool আর AI assistant কয়েক সেকেন্ডে শত chart দেয়। তাহলে প্রশ্নভিত্তিক, লগ-রাখা EDA-র মূল্য কোথায়?

স্বয়ংক্রিয় profiling অনেক দিনের পুরোনো ধারণা। pandas-profiling (এখন ydata-profiling) নামের open-source প্যাকেজ একটি DataFrame থেকে প্রতিটি কলামের histogram, ফাঁকার হার, correlation আর সতর্কসংকেতসহ একটি HTML রিপোর্ট বানায়; এমন আরও কয়েকটি tool আছে। আজ AI assistant একই কাজ আরও নমনীয়ভাবে করে, এমনকি সারাংশও লেখে। এগুলো সত্যিই কাজের, বিশেষ করে প্রথম দশ মিনিটে।

কিন্তু এই পর্বের ছয়টি খোঁজের অন্তত তিনটি এমন, যা প্রশ্ন ছাড়া আসে না। দামের outlier ধরা পড়েছে category-র ভেতরে দেখায়, পুরো কলামে নয়। Missingness-এর আসল কারণ (courier) বোঝা গেছে জেলা থেকে courier-এ গিয়ে, অর্থাৎ একটি অনুমান যাচাই করে। আর refund_taka-কে leakage বলার জন্য দরকার হয়েছে ব্যবসার জ্ঞান: টাকা ফেরত হয় পণ্য ফেরতের পরে। একটি tool বড়জোর বলতে পারে “এই কলাম label-এর সঙ্গে খুব জড়িত”; সেটিকে খারাপ খবর বলে চেনা মানুষের কাজ।

দ্বিতীয় সমস্যা পরিমাণ। শত chart মানে শত তুলনা, আর multiple comparisons-এর হিসাবে কয়েকটি কাকতালীয় “insight” প্রায় নিশ্চিত। লেখা প্রশ্নের তালিকা দেখায় কতগুলো পথ পরীক্ষা হয়েছে, তাই কোন খোঁজ কতটা বিশ্বাস করা যায় তার একটি মাপকাঠি থাকে। তৃতীয় সমস্যা দায়বদ্ধতা: একটি সিদ্ধান্ত (“ফাঁকা সারি রাখা”, “এই দাম যাচাই”) কে কেন নিয়েছে, তা লগ ছাড়া ছয় মাস পরে কেউ জানে না।

ভালো ভারসাম্য তাই দুটোকে মেলানো: স্বয়ংক্রিয় রিপোর্ট বা AI দিয়ে দ্রুত প্রথম নজর, তারপর তার প্রতিটি সতর্কসংকেত থেকে একটি লেখা প্রশ্ন, নিজের চালানো যাচাই, আর লগে এন্ট্রি। আর একটি কঠোর শর্ত: profiling-ও শুধু train অংশে।

মূল উপলব্ধি: tool গতি দেয়, প্রশ্ন দিক দেয়, আর লগ বিশ্বাসযোগ্যতা দেয়; EDA-র মূল্য chart-এর সংখ্যায় নয়, লেখা সিদ্ধান্তের মানে।

অনুশীলন

হাতে হিসাব, কোড, সমালোচনা আর আউটপুট অনুমান; প্রতিটির উত্তর নিচে লুকানো আছে।

  1. ব্যবধানের হিসাব (হিসাব): (ক) একটি segment-এ ২৪১টি অর্ডারের ৭২টি ফেরত। Wald ৯৫% ব্যবধান p̂ ± 1.96 × √(p̂(1 − p̂)/n) হাতে বের করুন। (খ) ৩টি অর্ডারের ৩টি ফেরত হলে Wald কী দেয়, আর Wilson-এর নিচের সীমা কত? (গ) একটি নতুন জেলায় ৪০টি অর্ডারে একটিও ফেরত নেই; rule of three অনুযায়ী ৯৫% ঊর্ধ্বসীমা কত?
    • (ক) p̂ = ৭২ ÷ ২৪১ ≈ ০.২৯৯. p̂(1 − p̂) = ০.২৯৯ × ০.৭০১ ≈ ০.২০৯৫; ভাগ ২৪১ = ০.০০০৮৬৯; বর্গমূল ≈ ০.০২৯৫. 1.96 × ০.০২৯৫ ≈ ০.০৫৮. ব্যবধান ≈ [০.২৪১, ০.৩৫৭]. Wilson দেয় [০.২৪৪, ০.৩৫৯]: বড় n-এ দুটি কাছাকাছি।
    • (খ) Wald: p̂ = 1, √(1 × 0 ÷ 3) = 0, তাই [1, 1], যেন পুরো নিশ্চিত; এটি অর্থহীন। Wilson: z² = 3.8416; z²/n = 1.2805; কেন্দ্র = (1 + 3.8416/6) ÷ 2.2805 = 1.6403 ÷ 2.2805 ≈ 0.7193; অর্ধেক প্রস্থ = 1.96 × √(0 + 3.8416/36) ÷ 2.2805 = 1.96 × 0.3267 ÷ 2.2805 ≈ 0.2808; নিচের সীমা ≈ ০.৪৩৮.
    • (গ) ৩ ÷ ৪০ = ০.০৭৫, অর্থাৎ সত্যিকারের হার ৭.৫% পর্যন্তও হতে পারে। “০% ফেরত” লিখলে ভুল বার্তা যায়।
  2. Missingness রিপোর্ট (কোড): একটি function missing_report(df, col, by) লিখুন যা by কলামের প্রতিটি দলের জন্য col-এর ফাঁকার সংখ্যা, দলের আকার আর হার দেয়, হার অনুযায়ী সাজায়, আর যে দলের হার মোট হারের দ্বিগুণের বেশি সেখানে flag = True বসায়। বাজারঘরের train অংশে delivery_days আর district দিয়ে চালান।
    import pandas as pd
    from bazarghar_orders import load_orders
    
    def missing_report(df, col, by):
        m = df[col].isna()
        t = m.groupby(df[by]).agg(missing="sum", n="size", rate="mean")
        t["flag"] = t["rate"] > 2 * m.mean()
        return t.sort_values("rate", ascending=False).round(3)
    
    df = load_orders().query("order_date < '2026-09-01'")
    print("মোট ফাঁকার হার:", round(df.delivery_days.isna().mean(), 3))
    print(missing_report(df, "delivery_days", "district"))

    সত্যিকারের run (ডেস্কটপ, Python 3.14.4, pandas 3.1.1, একই bazarghar_orders.py):

    মোট ফাঁকার হার: 0.153
                missing    n   rate   flag
    district                              
    Barishal         28   86  0.326   True
    Rangpur          27   90  0.300  False
    Mymensingh       24   94  0.255  False
    Rajshahi         24  106  0.226  False
    Sylhet           19  117  0.162  False
    Dhaka            46  429  0.107  False
    Khulna           13  127  0.102  False
    Chattogram       14  185  0.076  False
    Chittagong        3   57  0.053  False

    শুধু Barishal (০.৩২৬) মোট হারের (০.১৫৩) দ্বিগুণ পেরিয়েছে; Rangpur ০.৩০০, সীমা ০.৩০৬-এর একটু নিচে। এটি মনে করিয়ে দেয় যে যেকোনো কাটা-সীমা কিছুটা খামখেয়ালি; ফ্ল্যাগ প্রশ্ন জাগানোর জন্য, সিদ্ধান্তের জন্য নয়। টেবিলটি অংশ ৪-এর সঙ্গে মিলিয়ে পড়ুন: জেলার এই ক্রম courier B-এর ভাগের ক্রমের সঙ্গে মেলে। agg(missing="sum", n="size", rate="mean") হলো named aggregation (পর্ব ২৪)।

  3. সমালোচনা ও নতুন নকশা: একজন সহকর্মীর EDA সারাংশ: “পুরো ২০২৫–২৬ সালের ফাইলে EDA করেছি। ফাঁকা delivery_days-এর সারিগুলো dropna() করেছি, তাতে ডেটা পরিষ্কার। সবচেয়ে বড় insight: ‘gift wrap’ নেওয়া অর্ডারে ফেরত ১০০%, তাই এটি বন্ধ করা উচিত। দামের গড় ১৭৮২ টাকা। সবচেয়ে শক্তিশালী feature refund_taka, correlation ০.৯।” কী কী সমস্যা, আর আপনি কীভাবে সাজাবেন?
    • Test আলাদা নেই: test-এর তথ্য আগেই সিদ্ধান্তে ঢুকেছে। আগে সময় ধরে শেষ অংশ তালাবন্ধ।
    • নীরব dropna(): ফাঁকা হওয়া courier B-নির্ভর হলে কিছু জেলার বড় অংশ বাদ যায়; কোন দল কতটা হারাল তা গোনা হয়নি। আগে ফাঁকার ধরন, তারপর indicator বা pipeline-এ imputation.
    • n ছাড়া ১০০%: gift wrap কয়টি অর্ডারে? ৩টি হলে Wilson ব্যবধান প্রায় ০.৪৪ থেকে ১; একটি ব্যবসায়িক সিদ্ধান্তের জন্য অনেক কম প্রমাণ।
    • গড় একা: হেলানো দামে আর সম্ভাব্য ভুল মানসহ mean বিভ্রান্তিকর; median আর category ধরে বণ্টন দরকার।
    • Leakage-কে feature বলা: refund_taka ফেরতের পরে লেখা হয়; “সবচেয়ে শক্তিশালী” হওয়াটাই সন্দেহের কারণ। বাদ, আর কারণ লগে।

    নতুন নকশা: data dictionary-তে “কখন জানা যায়” পূরণ; সময় ধরে test তালাবন্ধ; প্রশ্নের তালিকা; dtype, ফাঁকার ধরন, বণ্টন, n ও ব্যবধানসহ segment-এর হার, একার AUC দিয়ে leakage পরীক্ষা; সব খোঁজ লগে; শেষে অংশ ৯-এর ছাঁচে রিপোর্ট, যেখানে gift wrap “নজরে রাখার তালিকায়”।

  4. আউটপুট অনুমান: নিচের কোড কী ছাপবে?
    import pandas as pd
    df = pd.DataFrame({"seg": ["a", "a", "a", "b", "b", "c"],
                       "ret": [1, 0, 0, 1, 1, 1],
                       "d": [2, None, 4, None, None, 3]})
    print(df.groupby("seg").ret.agg(["mean", "size"]))
    print(df.d.isna().groupby(df.seg).mean())
    print(df.d.mean(), df.d.fillna(0).mean())

    সত্যিকারের run (ডেস্কটপ, Python 3.14.4, pandas 3.1.1):

             mean  size
    seg                
    a    0.333333     3
    b    1.000000     2
    c    1.000000     1
    seg
    a    0.333333
    b    1.000000
    c    0.000000
    Name: d, dtype: float64
    3.0 1.5

    দল a-তে ৩টির ১টি ফেরত (০.৩৩৩), b-তে ২টির ২টি, c-তে ১টির ১টি: দুটি দলেই “১০০%”, কিন্তু n ২ আর ১. ফাঁকার হার দল ভেদে ০ থেকে ১. শেষ লাইন সবচেয়ে শিক্ষণীয়: mean() ফাঁকা ঘর বাদ দিয়ে (২ + ৪ + ৩) ÷ ৩ = ৩.০ দেয়, আর fillna(0) করলে ৯ ÷ ৬ = ১.৫. ফাঁকাকে ০ ধরা একটি নীরব সিদ্ধান্ত, যা গড় অর্ধেক করে দিয়েছে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি প্রশ্নভিত্তিক EDA-র ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ব্যবসার প্রশ্ন আর data dictionary লেখা, প্রতিটি কলাম কখন জানা যায় তাসহ
  2. Test অংশ আলাদা করে তালাবন্ধ রাখা
  3. প্রথম নজর: shape, dtype, ফাঁকার সংখ্যা আর দল ধরে ফাঁকার হার
  4. এক কলাম, দুই কলাম আর লক্ষ্যের সঙ্গে সম্পর্ক, n আর ব্যবধানসহ
  5. সন্দেহজনক সংকেত (leakage, outlier) যাচাই করে EDA লগে লেখা
  6. EDA রিপোর্ট লিখে feature আর pipeline-এর সিদ্ধান্ত নেওয়া

একটি অনলাইন দোকানের ১২ মাসের অর্ডারে ফেরতের model বানানো হবে, model চলবে ভবিষ্যতের অর্ডারে। EDA শুরুর আগে প্রথম কাজ কোনটি?

  • সব কলামের correlation matrix আঁকা
  • শেষ কয়েক মাসের অর্ডার test হিসেবে আলাদা করে রাখা
  • সব ফাঁকা সারি বাদ দেওয়া
  • দামের outlier মুছে ফেলা

একটি নতুন পণ্য-category-র ৩টি অর্ডারের ৩টিই ফেরত এসেছে, যেখানে মোট ফেরতের হার ১২%. সবচেয়ে সঠিক ব্যাখ্যা কোনটি?

  • এই category-র সত্যিকারের ফেরতের হার ১০০%
  • ৩টি অর্ডার কিছুই বলে না, তাই পুরো উপেক্ষা করা উচিত
  • হার সম্ভবত বেশি, কিন্তু ব্যবধান প্রায় ০.৪৪ থেকে ১; আরও ডেটা দরকার
  • Wald ব্যবধান [1, 1], তাই ফল নিশ্চিত

Courier A-এর অর্ডারে delivery_days ফাঁকা ০.৮%, courier B-এর অর্ডারে ৩৬.২%. ফাঁকা হওয়ার ধরন সম্পর্কে কোনটি সবচেয়ে ঠিক?

  • MCAR নয়; দেখা কলামের ওপর নির্ভর করে বলে MAR-এর মতো, যদিও MNAR পুরো বাদ দেওয়া যায় না
  • MCAR, তাই ফাঁকা সারি নিশ্চিন্তে বাদ দেওয়া যায়
  • নিশ্চিতভাবে MNAR, কারণ courier B দেরির অর্ডার লুকায়
  • ধরনটি গুরুত্বহীন, কারণ ফাঁকা মোটে ১৫%

EDA-তে দেখা গেল refund_taka কলামটি একাই ফেরত আর না-ফেরত অর্ডার নিখুঁত আলাদা করে, AUC 1.000. ফেরত আগে থেকে বলার model-এ সবচেয়ে ঠিক সিদ্ধান্ত কী?

  • সেরা feature হিসেবে রাখা
  • StandardScaler দিয়ে scale করে রাখা
  • এর outlier সরিয়ে রাখা
  • কখন জানা যায় দেখে target leakage হিসেবে বাদ দেওয়া, কারণসহ লগে

১২৯১টি অর্ডারের ডানে হেলানো দামের কলামে IQR নিয়ম (Q3 + 1.5 × IQR) চালালে ১৮৪টি “outlier” আসে। এর সবচেয়ে সম্ভাব্য কারণ আর ভালো বিকল্প কী?

  • ডেটার ১৪% ভুল, সব মুছে ফেলা উচিত
  • লম্বা লেজ আর category-র আলাদা স্তর; log স্কেলে category-র ভেতরে robust z ভালো
  • IQR-এর বদলে mean ± 3 SD ব্যবহার করলেই ঠিক হবে
  • IQR নিয়মে হিসাবের ভুল হয়েছে

একজন বিশ্লেষক ১৩৬টি জেলা × category × payment ঘরের ফেরতের হার দেখে সবচেয়ে বেশি হারের ঘরটিকে “আবিষ্কার” বললেন। মূল সমস্যা কী?

  • groupby তিনটি কলামে চালানো যায় না
  • হারের বদলে মোট সংখ্যা দেখা উচিত ছিল
  • অনেক ঘর থেকে চরমটি বাছা multiple comparisons-এর ফাঁদ; ছোট ঘরে কাকতালেই চরম হার আসে
  • সবচেয়ে বেশি হারের ঘর সবসময় আসল প্রভাব দেখায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. প্রশ্নের চক্র: প্রশ্ন আর প্রত্যাশা, যাচাই, ফল (n আর ব্যবধানসহ), লগে “প্রশ্ন · প্রমাণ · সিদ্ধান্ত”, তারপর নতুন প্রশ্ন।
  2. Test আগে তালাবন্ধ: EDA-র সিদ্ধান্তও শেখার অংশ; test দেখে নেওয়া EDA নিজেই leakage.
  3. ফাঁকা ঘরের ধরন: দল, লক্ষ্য আর সময় ধরে ফাঁকার হার দেখুন; courier B ৩৬.২% বনাম A ০.৮%. MAR আর MNAR ডেটা দিয়ে আলাদা হয় না, প্রক্রিয়া জানতে হয়।
  4. ছোট দলের চরম হার: ৩/৩-এর Wilson ব্যবধান ০.৪৩৮ থেকে ১; অনেক ঘর দেখলে এলোমেলো label-এও ১০০% আসে। মূল ফল clothing + COD, ০.২৯৯ [০.২৪৪, ০.৩৫৯].
  5. সন্দেহজনক ভালো সংকেত: একার AUC ১.০০০ দেখলে কলামটি কখন জানা যায় দেখুন। Outlier দলের ভেতরে, log স্কেলে খুঁজুন, আর মুছে না ফেলে উৎসে যাচাই করুন।

পরের পর্বে: EDA রিপোর্টের সিদ্ধান্তগুলো এবার feature হবে। পর্ব ৬৩ (Feature engineering — scaling, encoding আর নতুন feature)-এ scaling, category কলামকে সংখ্যায় আনা, ফাঁকার indicator, তারিখের feature আর target encoding-এর ঝুঁকি।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান