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

Train, validation আর test — data leakage-এর ফাঁদ

Train, Validation and Test Sets — Stratified, Group and Time-Based Splits, Cross-Validation and the Many Faces of Data Leakage

Taught in Bangla, this lesson explains why data is split into train, validation and test sets, how stratify and seeds change a single split, k-fold, group and time-based cross-validation, and the many forms of data leakage: target, ID, duplicate, temporal and preprocessing leakage, plus test-set snooping. Every trap is reproduced with real in-browser and desktop scikit-learn runs.

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

  • ডেটা কেন আলাদা রাখতে হয়, আর train, validation ও test-এর আলাদা দায়িত্ব
  • train_test_split, random_state, stratify, আর একটি ভাগের ফল কতটা ভাগ্যনির্ভর
  • k-fold cross-validation-এর ধারণা, group split আর time-based split
  • Data leakage-এর পাঁচটি পরিবার, সত্যিকারের পরীক্ষাসহ, আর Pipeline দিয়ে preprocessing leakage ঠেকানো
  • Test set কেন একবারই, আর একটি leakage চেকলিস্ট যা যেকোনো প্রকল্পে খাটে

পর্ব ৫৫ (যন্ত্র কীভাবে শেখে — supervised, unsupervised আর reinforcement)-এ দেখেছেন, supervised learning মানে label দেওয়া উদাহরণ থেকে একটি নিয়ম শেখা, আর আসল লক্ষ্য generalization: নতুন, না-দেখা উদাহরণে ঠিক উত্তর। এই পর্বের প্রশ্ন সোজা: “নতুন উদাহরণে কেমন করবে”, তা আমরা আগে থেকে সৎভাবে মাপব কীভাবে? পর্ব ৫-এ leakage শব্দটি প্রথম শুনেছেন, আর পর্ব ২৬-এ shift(-1)-এর ফাঁদ দেখেছেন; এখানে পুরো ছবিটা। চালানো যায় এমন ব্লকগুলো পাতাতেই চলে (ব্রাউজারের Python, NumPy 2.4, scikit-learn 1.8.0). scikit-learn প্রথমবার নামতে প্রায় ১০ সেকেন্ড লাগে; পরের run দ্রুত।

১ · কেন ডেটা আলাদা রাখি — ফাঁস হওয়া প্রশ্নপত্রের গল্প

ধরুন কোনো পরীক্ষার আগের রাতে প্রশ্নপত্র ফাঁস হলো। এক ছাত্র উত্তরগুলো মুখস্থ করে ১০০-তে ৯৮ পেল। এই ৯৮ কি বলে সে বিষয়টি বোঝে? না; এটি শুধু বলে সে এই প্রশ্নগুলোর উত্তর জানে। নতুন প্রশ্নে সে কেমন করবে, তা জানার একমাত্র উপায় এমন প্রশ্ন দেওয়া, যা সে আগে দেখেনি। Machine learning-এ test set ঠিক সেই না-দেখা প্রশ্নপত্র। আর data leakage মানে কোনো না কোনো পথে প্রশ্নপত্র আগেই ফাঁস হয়ে যাওয়া।

Training-এর উদাহরণে model-এর ফল দিয়ে নতুন উদাহরণের কথা বলা যায় না, কারণ model সেগুলো মুখস্থ করে ফেলতে পারে। এই মুখস্থ করাকে বলে overfitting (পর্ব ৬১-এ পুরো আলোচনা)। তাই নিয়ম একটাই: যে ডেটা দিয়ে মাপবেন, model যেন তা কোনোভাবে আগে না দেখে। “কোনোভাবে” শব্দটিই এই পর্বের কেন্দ্র। Model সরাসরি না দেখলেও তথ্য চুইয়ে ঢুকতে পারে: একটি কলামের মাধ্যমে, ডুপ্লিকেট সারির মাধ্যমে, গড় হিসাবের মাধ্যমে, এমনকি আপনার নিজের বারবার দেখা আর বাছাইয়ের মাধ্যমে। এই চুইয়ে ঢোকারই নাম data leakage.

Leakage-এর সবচেয়ে বড় বিপদ: এটি কোনো error দেয় না, আর সংখ্যাটি ভালো আসে। ভুল ধরা পড়ে model বাস্তবে চালু হওয়ার পরে। তাই এই পর্বে প্রতিটি ফাঁদ হাতে-কলমে ঘটিয়ে দেখব, সৎ সংস্করণের পাশে।

২ · Train, validation আর test — তিন ভাগ, তিন কাজ

সবচেয়ে প্রচলিত নকশায় ডেটা তিন ভাগে যায়:

📘 Training set

Model এখানে শেখে: Naive Bayes-এর গোনা, linear regression-এর সহগ, tree-এর শাখা। Training set সাধারণত সবচেয়ে বড় ভাগ।

fit

🧪 Validation set

সিদ্ধান্তের জায়গা: কোন model, কোন hyperparameter (যেমন Naive Bayes-এর α), কোন feature. Validation set দেখে আমরা বাছাই করি, তাই এটি আর পুরো নিরপেক্ষ থাকে না।

বাছাই

🔒 Test set

তালাবন্ধ খাম। সব সিদ্ধান্ত শেষ হলে একবার খোলা হয়, আর যে সংখ্যা আসে, সেটিই রিপোর্ট। Test set-এর ফল দেখে কিছু বদলালে সেটি আর test থাকে না।

একবার

অনুপাত কত? ৬০/২০/২০, ৭০/১৫/১৫ বা ৮০/১০/১০ সবই চালু, কিন্তু এগুলো প্রচলিত অভ্যাস, কোনো সূত্র থেকে আসা নিয়ম নয়। আসল প্রশ্ন হলো, test set এত বড় কি না যে তার score যথেষ্ট স্থির। ১০০টি test উদাহরণে একটি ভুল মানে accuracy-তে ০.০১-এর লাফ; ১০ লাখ উদাহরণের ডেটায় ১% test-ও দশ হাজার উদাহরণ, যা যথেষ্ট। ডেটা কম হলে আলাদা validation set রাখার বদলে cross-validation (অংশ ৪) প্রায়ই ভালো। এক শর্ত সব ক্ষেত্রে এক: ভাগ সবার আগে, পরিষ্কার, scaling বা feature বাছাইয়ের আগে।

নিচের stepper বাজারঘরের ২০টি কাল্পনিক অর্ডার (#01–#20) ভাগ করে দেখায়। চারটি অর্ডার ফেরত এসেছিল (তারকা দিয়ে চিহ্নিত), অর্থাৎ ২০%. ভাগগুলো scikit-learn-এর train_test_split (stratify আর random_state=42 সহ) দিয়ে ডেস্কটপে সত্যিই করা।

২০টি অর্ডার (#01–#20), * = ফেরতপ্রতি ধাপে একটি কাজ

            
  1. পুরো ডেটা। ২০টি অর্ডার, তার ৪টি ফেরত (#03, #08, #12, #17). এখনো কিছু ধরা হয়নি: কোনো গড়, কোনো chart, কোনো পরিষ্কার নয়।
  2. আগে test আলাদা। ৪টি অর্ডার (২০%) সরিয়ে তালাবন্ধ করা হলো, তার ১টি ফেরত। ২০টিতে ৪টি ফেরতকে ৫ ভাগ করলে প্রতি ভাগে ১টি, তাই stratify এখানে ঠিক ১টি ফেরত রেখেছে।
  3. বাকি ১৬টি থেকে validation. আরও ৪টি অর্ডার, তার ১টি ফেরত (#17). Validation আসে test বাদ দেওয়ার পরের অংশ থেকে, কখনো test থেকে নয়।
  4. বাকিটা train. ১২টি অর্ডার, তার ২টি ফেরত। ভাগ তিনটির অনুপাত ৬০/২০/২০, আর প্রতিটিতে ফেরতের হার ১৬.৭% থেকে ২৫%, মূল ২০%-এর কাছাকাছি।
  5. শেখা আর বাছাই। কয়েকটি model বা সেটিং train-এ fit হয়, validation-এ তুলনা হয়, সেরাটি বাছা হয়। এই পুরো সময় test খোলা হয়নি।
  6. শেষ ধাপ। সব সিদ্ধান্ত শেষ হলে অনেকে train আর validation মিলিয়ে বাছা সেটিংয়ে আবার fit করেন, তারপর test একবার খোলেন। এই সংখ্যাটিই রিপোর্ট হয়; এটি দেখে আর কিছু বদলানো চলবে না।

৩ · train_test_split, random_state আর stratify — একটি ভাগ কতটা নড়বড়ে

scikit-learn-এ ভাগ করার মূল হাতিয়ার train_test_split. তিনটি argument মনে রাখুন। test_size (যেমন ০.২৫) বলে কত ভাগ test-এ যাবে। random_state একটি seed: একই সংখ্যা দিলে প্রতিবার একই ভাগ, তাই আপনার আর সহকর্মীর ফল মেলে। আর stratify=y বলে প্রতিটি শ্রেণির অনুপাত দুই ভাগে মূল ডেটার মতো রাখতে; একে বলে stratified ভাগ, যা পর্ব ৫১-এ SMS ভাগ করতে ব্যবহার করেছিলেন।

stratify সবচেয়ে জরুরি যখন একটি শ্রেণি বিরল, অর্থাৎ class imbalance. বাজারঘরে ধরুন মাত্র ৫% অর্ডার ফেরত আসে। নিচের ব্লক দুটি জিনিস মাপে: (ক) ৪০০টি অর্ডারের (২০টি ফেরত) ২০টি ভিন্ন seed-এ ভাগ করলে test-এর ১০০টিতে কয়টি ফেরত পড়ে, stratify ছাড়া আর সহ; (খ) একই model আর একই ডেটায় শুধু seed বদলালে accuracy কতটা নড়ে। দ্বিতীয় অংশে scikit-learn-এর সঙ্গে আসা Wisconsin breast cancer ডেটা (৫৬৯টি টিউমারের মাপ, UCI থেকে নেওয়া) আর পর্ব ৫১-এর পরিচিত GaussianNB.

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
from sklearn.naive_bayes import GaussianNB

rng = np.random.default_rng(42)
y = np.zeros(400, dtype=int); y[:20] = 1     # ৪০০ অর্ডার, ২০টি (৫%) ফেরত
rng.shuffle(y)
X = rng.normal(size=(400, 3))                 # feature এখানে শুধু জায়গা ভরাট
print("পুরো ডেটায় ফেরত:", y.sum(), "/", len(y))

for strat in (None, y):
    counts = []
    for seed in range(20):
        *_, y_te = train_test_split(X, y, test_size=0.25,
                                    random_state=seed, stratify=strat)
        counts.append(int(y_te.sum()))
    name = "random    " if strat is None else "stratified"
    print(name, "test-এর ১০০টিতে ফেরত:", counts)
    print("           সর্বনিম্ন", min(counts), "সর্বোচ্চ", max(counts))

# একই model, একই ডেটা, শুধু ভাগ বদলায়
Xc, yc = load_breast_cancer(return_X_y=True)
acc = []
for seed in range(20):
    a, b, c, d = train_test_split(Xc, yc, test_size=0.25,
                                  random_state=seed, stratify=yc)
    acc.append(GaussianNB().fit(a, c).score(b, d))
acc = np.array(acc)
print(f"20টি ভাগে accuracy: গড় {acc.mean():.3f}, sd {acc.std(ddof=1):.3f}")
print(f"সর্বনিম্ন {acc.min():.3f} (seed {acc.argmin()}), "
      f"সর্বোচ্চ {acc.max():.3f} (seed {acc.argmax()})")

আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):

পুরো ডেটায় ফেরত: 20 / 400
random     test-এর ১০০টিতে ফেরত: [7, 4, 6, 2, 4, 7, 3, 5, 4, 4, 6, 5, 1, 8, 3, 7, 6, 3, 3, 2]
           সর্বনিম্ন 1 সর্বোচ্চ 8
stratified test-এর ১০০টিতে ফেরত: [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
           সর্বনিম্ন 5 সর্বোচ্চ 5
20টি ভাগে accuracy: গড় 0.939, sd 0.022
সর্বনিম্ন 0.909 (seed 12), সর্বোচ্চ 0.986 (seed 17)

Stratify ছাড়া test-এ ফেরত এসেছে কখনো ১টি, কখনো ৮টি। ১টি ফেরত নিয়ে “ফেরত ধরার ক্ষমতা” মাপা প্রায় অর্থহীন: একটি ঠিক বা ভুলেই ফল ০% বা ১০০%. Stratify দিলে প্রতিবার ঠিক ৫টি, মূল ৫%-এর সমান।

দ্বিতীয় অংশটি আরও শিক্ষণীয়। Model, ডেটা, সব এক; শুধু কোন ১৪৩টি টিউমার test-এ গেল তা বদলেছে, আর accuracy ০.৯০৯ থেকে ০.৯৮৬. কেউ seed 17 দেখে বলবেন “৯৮.৬%”, কেউ seed 12 দেখে “৯০.৯%”; দুজনেই সৎ, দুজনেই অসম্পূর্ণ। একটি ভাগের score একটি নমুনা, সত্য নয়; তার পাশে পরিসর বা standard deviation লাগে (পর্ব ৩৭-এর confidence interval মনে করুন)। আর “সেরা seed” বেছে রিপোর্ট করা নিজেই এক রকম প্রতারণা।

Stratify-এর সীমা: শ্রেণিভিত্তিক stratify classification-এর জন্য। Regression-এ লক্ষ্য সংখ্যা (যেমন ডেলিভারির সময়) হলে অনেকে সেটিকে কয়েকটি bin-এ ভাগ করে সেই bin দিয়ে stratify করেন। আর stratify শুধু label-এর অনুপাত মেলায়; একই গ্রাহক বা একই দিনের সমস্যা (অংশ ৫) এতে মেটে না।

৪ · k-fold cross-validation — প্রতিটি অংশ একবার করে পরীক্ষক

একটি ভাগ নড়বড়ে হলে স্বাভাবিক সমাধান: অনেক ভাগ করে গড় নেওয়া, আর এমনভাবে, যাতে প্রতিটি উদাহরণ ঠিক একবার পরীক্ষায় বসে। এটিই k-fold cross-validation: ডেটাকে k সমান অংশে (fold) ভাগ করুন; প্রতিবার একটি fold-কে পরীক্ষার জন্য রেখে বাকি k − 1টিতে train; k বার ঘুরিয়ে k-টি score, তারপর গড় আর ছড়ানো। k = ৫ বা ১০ বহুল প্রচলিত। k = n (উদাহরণের সংখ্যা) হলে সেটি leave-one-out, যা পর্ব ৫১-এ ৬০টি SMS-এ চালিয়েছিলেন।

৫-fold cross-validation: প্রতিটি সারি একটি round, কমলা ঘর সেই round-এর পরীক্ষার fold

5-fold CV on the breast cancer data (GaussianNB, StratifiedKFold, random_state=42): one real run

5-fold cross-validation: each fold is the test fold once round fold 1 … fold 5 (প্রতিটি প্রায় ১১৪টি) score 12345 0.9650.9040.9300.9300.965 train পরীক্ষা গড় 0.939 ± 0.026

এখন stepper-এ একটি round ঘুরিয়ে দেখুন। সংখ্যাগুলো ওপরের ছবির একই সত্যিকারের run থেকে (ডেস্কটপ, scikit-learn 1.8.0). ৫৬৯টি টিউমারের ৩৫৭টি benign (label 1), ২১২টি malignant.

GaussianNB, StratifiedKFold(5, shuffle=True, random_state=42)
Round
পরীক্ষার fold
train / পরীক্ষা
পরীক্ষার fold-এ label 1
এই round-এর accuracy
এ পর্যন্ত score
  1. Round ১. fold 1 (১১৪টি) পরীক্ষায়, বাকি ৪৫৫টিতে নতুন করে fit. ১১৪টির ১১০টি ঠিক: ০.৯৬৫.
  2. Round ২. এবার fold 2 পরীক্ষায়, আর model আবার শূন্য থেকে fit, fold 1 এখন train-এর অংশ। ১০৩টি ঠিক: ০.৯০৪, এই run-এর সবচেয়ে খারাপ।
  3. Round ৩. fold 3 পরীক্ষায়: ১০৬/১১৪ = ০.৯৩০. খেয়াল করুন, StratifiedKFold প্রতিটি fold-এ label 1 প্রায় সমান (৭১ বা ৭২) রেখেছে।
  4. Round ৪. fold 4: আবার ০.৯৩০.
  5. Round ৫. fold 5-এ ১১৩টি (৫৬৯ পাঁচ ভাগে সমান হয় না, তাই এক fold একটি ছোট): ১০৯টি ঠিক, ০.৯৬৫.
  6. সারাংশ। প্রতিটি টিউমার ঠিক একবার পরীক্ষায় বসেছে। গড় ০.৯৩৯, sd ০.০২৬, পরিসর ০.৯০৪ থেকে ০.৯৬৫. অংশ ৩-এর ২০টি এলোমেলো ভাগের গড়ও ছিল ০.৯৩৯; একটি ভাগের চেয়ে এই ছবি অনেক বেশি তথ্য দেয়।

মনে রাখুন, cross-validation একটি পদ্ধতির মাপ দেয়, কোনো একটি fit করা model-এর নয়; আর k বার fit লাগে, তাই খরচ k গুণ। cross-validation দিয়ে model আর hyperparameter বাছাই, nested cross-validation আর GridSearchCV পর্ব ৬৬ (Model বাছাই — cross-validation আর hyperparameter search)-এর বিষয়; এখানে শুধু ধারণাটুকু।

৫ · একই গ্রাহক, একই সময় — group split আর time-based split

এলোমেলো ভাগ একটি নীরব অনুমান করে: প্রতিটি সারি অন্যগুলো থেকে স্বাধীন। বাজারঘরের অর্ডারে তা সত্য নয়। একজন গ্রাহক দশটি অর্ডার দেন; তাঁর বয়স, জেলা আর কেনার ধরন দশটিতেই এক, আর ফেরত দেওয়ার অভ্যাসও। এলোমেলো ভাগে তাঁর সাতটি অর্ডার train-এ আর তিনটি test-এ পড়লে model পরীক্ষায় “নতুন অর্ডার” দেখছে না, দেখছে চেনা গ্রাহক। কিন্তু আসল প্রশ্ন যদি হয় “নতুন গ্রাহকের অর্ডার ফেরত আসবে কি না”, তাহলে এই score আশাবাদী।

ডাক্তারি পরীক্ষায় একই রোগীর পাঁচটি এক্স-রে ছবির তিনটি দিয়ে শেখালে আর দুটি দিয়ে পরীক্ষা নিলে model হয়তো রোগ নয়, রোগীর হাড়ের গড়ন চিনে ফেলে। প্রশ্নটি তাই প্রথমেই করতে হয়: বাস্তবে model কোন একক নিয়ে কাজ করবে? নতুন রোগী, নতুন গ্রাহক, নতুন দোকান, নাকি আগামী সপ্তাহ? Test set-এ সেই এককটিকেই নতুন রাখতে হবে।

এর সমাধান group split: প্রতিটি সারির সঙ্গে একটি group (এখানে গ্রাহকের ID) দিন, আর ভাগ এমনভাবে হোক যাতে একটি group-এর সব সারি এক দিকেই যায়। scikit-learn-এ একবারের ভাগের জন্য GroupShuffleSplit, cross-validation-এর জন্য GroupKFold. নিচের ব্লকে ৬০ জন কাল্পনিক গ্রাহক, প্রত্যেকের ৬–১৪টি অর্ডার। প্রত্যেক গ্রাহকের একটি লুকোনো অভ্যাস: কেউ বেশি ফেরত দেন (প্রতি অর্ডারে ৮০% সম্ভাবনা), কেউ কম (১০%). Feature শুধু গ্রাহকের বয়স, জেলা আর ঝুড়ির গড় দাম, যা একজন গ্রাহককে প্রায় চিনিয়ে দেয়। Model একটি DecisionTreeClassifier; tree কীভাবে শাখা গড়ে তা এখানে জরুরি নয়, শুধু জানুন এটি ডেটা মুখস্থ করতে পারদর্শী।

import numpy as np
from sklearn.model_selection import KFold, GroupKFold, GroupShuffleSplit, cross_val_score
from sklearn.tree import DecisionTreeClassifier

rng = np.random.default_rng(42)
n_cust = 60
sizes = rng.integers(6, 15, size=n_cust)            # প্রতি গ্রাহকের ৬–১৪টি অর্ডার
g = np.repeat(np.arange(n_cust), sizes.astype(np.intp))  # কোন অর্ডার কোন গ্রাহকের
age = rng.integers(18, 60, size=n_cust)
district = rng.integers(0, 8, size=n_cust)
basket = rng.normal(800, 250, size=n_cust)
# লুকোনো অভ্যাস: তরুণদের মধ্যে "বেশি ফেরত দেন" এমন গ্রাহক বেশি, তবে নিশ্চিত নয়
high = rng.random(n_cust) < np.where(age < 35, 0.7, 0.3)
y = (rng.random(g.size) < np.where(high, 0.8, 0.1)[g]).astype(int)
X = np.column_stack([age[g], district[g],
                     basket[g] + rng.normal(0, 20, size=g.size)])
print("অর্ডার:", len(y), "| গ্রাহক:", n_cust, "| ফেরতের হার:", round(y.mean(), 3))

tree = DecisionTreeClassifier(min_samples_leaf=3, random_state=42)
plans = {"KFold, এলোমেলো অর্ডার": KFold(5, shuffle=True, random_state=42),
         "GroupKFold, গ্রাহক ধরে ": GroupKFold(5)}
for name, cv in plans.items():
    grp = g if isinstance(cv, GroupKFold) else None    # KFold groups নেয় না
    s = cross_val_score(tree, X, y, cv=cv, groups=grp)
    print(f"{name}: {np.round(s, 2)} গড় {s.mean():.3f}")
print("baseline (সবসময় 'ফেরত নয়'):", round(1 - y.mean(), 3))

tr, te = next(GroupShuffleSplit(n_splits=1, test_size=0.25,
                                random_state=42).split(X, y, groups=g))
print("GroupShuffleSplit: train-এ গ্রাহক", len(set(g[tr])), "test-এ", len(set(g[te])),
      "| দুই ভাগে একই গ্রাহক:", len(set(g[tr]) & set(g[te])))

আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):

অর্ডার: 611 | গ্রাহক: 60 | ফেরতের হার: 0.457
KFold, এলোমেলো অর্ডার: [0.72 0.73 0.79 0.8  0.78] গড় 0.761
GroupKFold, গ্রাহক ধরে : [0.67 0.46 0.52 0.46 0.51] গড় 0.522
baseline (সবসময় 'ফেরত নয়'): 0.543
GroupShuffleSplit: train-এ গ্রাহক 45 test-এ 15 | দুই ভাগে একই গ্রাহক: 0

এলোমেলো ভাগে model দেখাচ্ছে ০.৭৬১, যা শুনতে বেশ ভালো। গ্রাহক ধরে ভাগ করলে একই model নামে ০.৫২২-এ, আর এটি baseline-এর (সবসময় “ফেরত নয়” বলা, ০.৫৪৩) নিচে। অর্থাৎ tree শিখেছিল “কোন গ্রাহক কে”, ফেরতের কোনো সাধারণ নিয়ম নয়। নতুন গ্রাহকের জন্য এটি কোনো কাজের নয়। বয়সের সঙ্গে অভ্যাসের একটি সত্যিকারের সম্পর্ক ডেটায় রাখা ছিল, কিন্তু মাত্র ৬০ জন গ্রাহকে সেটি এই model ধরতে পারেনি; ছোট কাল্পনিক ডেটার এই সংখ্যাগুলো একটি run-এর, সাধারণ সত্য নয়। ডেস্কটপে একই কোডে GroupKFold গ্রাহকদের fold-এ একটু অন্যভাবে সাজিয়ে গড় ০.৪৬৬ দিয়েছিল; fold-ভেদে ০.৪৬ থেকে ০.৬৭-এর দোলাও দেখায়, এত ছোট ডেটায় একটি সংখ্যায় ভরসা নেই। শেষ লাইন নিশ্চিত করে GroupShuffleSplit-এ কোনো গ্রাহক দুই দিকে নেই। (astype(np.intp) লাগে কারণ ব্রাউজারের Python 32-bit, আর সেখানে np.repeat int64-এর গোনা নিতে চায় না; ডেস্কটপে এটি ছাড়াও চলে।)

সময়ের ডেটা: অতীতে শেখা, ভবিষ্যতে পরীক্ষা

দৈনিক বিক্রির পূর্বাভাসে এলোমেলো ভাগ মানে বুধবার আর শুক্রবারের ডেটা দিয়ে শিখে বৃহস্পতিবার “ভবিষ্যদ্বাণী” করা; বাস্তবে আগামীকালের পরের দিনের ডেটা কখনো হাতে থাকে না। Time-based split-এ তাই একটি তারিখ ধরে কাটা হয়: তার আগে train, পরে test. পর্ব ২৬-এ দেখেছেন তারিখ দিয়ে এভাবে ভাগ করা। Cross-validation-এর সময়ের সংস্করণ হলো scikit-learn-এর TimeSeriesSplit: train-এর জানালা ক্রমে বড় হয়, আর test সবসময় তার ঠিক পরের অংশ।

import numpy as np
from sklearn.model_selection import TimeSeriesSplit

days = np.arange(1, 13)                               # ১২ দিনের বিক্রি
for k, (tr, te) in enumerate(TimeSeriesSplit(n_splits=4).split(days), 1):
    print(f"ভাগ {k}: train দিন {days[tr].tolist()} | test দিন {days[te].tolist()}")

সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0):

~/bazarghar-ai$ python ts_split.py
ভাগ 1: train দিন [1, 2, 3, 4] | test দিন [5, 6]
ভাগ 2: train দিন [1, 2, 3, 4, 5, 6] | test দিন [7, 8]
ভাগ 3: train দিন [1, 2, 3, 4, 5, 6, 7, 8] | test দিন [9, 10]
ভাগ 4: train দিন [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | test দিন [11, 12]

TimeSeriesSplit: train (নীল) সবসময় test-এর (কমলা) আগে, আর ক্রমে বাড়ে

Expanding-window splits on 12 days, n_splits=4

TimeSeriesSplit with 4 splits over 12 days ভাগ ১ভাগ ২ভাগ ৩ভাগ ৪ সময় → (দিন ১ … দিন ১২)

দুটি বাড়তি বিবেচনা। এক, train আর test-এর মাঝে একটি ফাঁকা অংশ (gap parameter) রাখা যায়, যদি feature বানাতে কয়েক দিনের পুরোনো ডেটা লাগে বা ফল জানতে দেরি হয়। দুই, সময়ের সঙ্গে বণ্টন বদলায় (distribution shift), যেমন ঈদের মাসে; এলোমেলো ভাগ এই বদল লুকায়, সময় ধরে ভাগ তা সামনে আনে।

৬ · Data leakage-এর চেহারা — target, contamination, ID আর সময়

Shachar Kaufman, Saharon Rosset আর Claudia Perlich ২০১১ সালে KDD সম্মেলনে, আর পরে Ori Stitelman-সহ ২০১২ সালে ACM TKDD জার্নালে, leakage-কে মোটামুটি এভাবে সংজ্ঞায়িত করেন: model-এ এমন তথ্য ঢুকে পড়া, যা ভবিষ্যদ্বাণীর মুহূর্তে বৈধভাবে পাওয়া যেত না। তাঁদের প্রস্তাবিত মূল প্রতিকার “learn-predict separation”: শেখার জন্য শুধু সেই তথ্য, যা ভবিষ্যদ্বাণীর সময় সত্যিই হাতে থাকবে। নিচের পরীক্ষাগুলো বাজারঘরের ২০০০টি কাল্পনিক অর্ডারে (ফেরতের হার প্রায় ৩৫%), feature হলো দাম, ডেলিভারির দিন আর প্রথম অর্ডার কি না।

(ক) Target leakage — ফল জানার পরের কলাম

অর্ডারের টেবিলে একটি কলাম আছে refund_processed: টাকা ফেরত প্রক্রিয়া হয়েছে কি না। শুনতে নিরীহ, কিন্তু টাকা ফেরত হয় ফেরতের পরে। অর্ডার দেওয়ার মুহূর্তে, যখন model-কে আসলে প্রশ্নটি করা হবে, এই কলাম সবসময় ০. একে বলে target leakage: feature-টি আসলে label-এর ছায়া।

(খ) ID আর ক্রমের মধ্য দিয়ে

ধরুন ফেরত হওয়া অর্ডারগুলো পরে একটি আলাদা ফাইল থেকে টেবিলের শেষে জোড়া হয়েছিল, আর তারপর সারির নম্বর দিয়ে row_id বানানো হয়েছে। তখন বড় ID মানেই ফেরত। Model ID থেকে শিখবে, ফেরতের কারণ থেকে নয়। একই কারণে ক্রম অনুযায়ী সাজানো ডেটায় shuffle=False দিয়ে k-fold চালালে কোনো fold-এ শুধু এক শ্রেণি পড়তে পারে।

(গ) Train-test contamination — ডুপ্লিকেট সারি

একটি রপ্তানি স্ক্রিপ্ট ভুল করে প্রতিটি অর্ডার ১–৩ বার লিখেছে (পর্ব ২৩-এ duplicate row খোঁজা দেখেছেন)। এলোমেলো ভাগে একই অর্ডারের এক কপি train-এ, আরেক কপি test-এ পড়ে: model প্রশ্নটা হুবহু আগেই দেখেছে। একে বলে train-test contamination. তিনটি পরীক্ষার কোডের মূল অংশ:

# df: ২০০০টি কাল্পনিক অর্ডার; returned = লক্ষ্য
df["refund_processed"] = ((df["returned"] == 1) & (rng.random(n) < 0.97)).astype(int)
honest = ["price", "delivery_days", "first_order"]
print("সৎ feature:               ", score(honest))
print("+ refund_processed:        ", score(honest + ["refund_processed"]))

# ফেরত হওয়া অর্ডার পরে জোড়া হয়েছিল, তারপর সারির নম্বর = row_id
d2 = pd.concat([df[df.returned == 0], df[df.returned == 1]], ignore_index=True)
d2["row_id"] = np.arange(len(d2))

# প্রতিটি অর্ডার ১–৩ বার রপ্তানি; ভাগের আগে বনাম পরে drop_duplicates
dup = base.loc[base.index.repeat(rng.integers(1, 4, size=len(base)))]
for name, data in [("ডুপ্লিকেটসহ ভাগ   ", dup), ("আগে drop_duplicates", dup.drop_duplicates())]:
    Xtr, Xte, ytr, yte = train_test_split(data[honest], data["returned"], test_size=0.25,
                                          random_state=42)
    seen = Xte.merge(Xtr.drop_duplicates(), how="inner").shape[0]
    ...

সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0, pandas 3.1.1; প্রথম দুটিতে LogisticRegression আর max_depth=3-এর tree, তৃতীয়টিতে RandomForestClassifier):

~/bazarghar-ai$ python leaks_demo.py
scikit-learn 1.8.0 | pandas 3.1.1
অর্ডার: 2000 | ফেরতের হার: 0.354

[1] target leakage
baseline (সবসময় 'ফেরত নয়'): 0.646
সৎ feature:                0.656
+ refund_processed:         0.982

[2] ID-র মধ্য দিয়ে leakage
সৎ feature (tree):           0.644
+ row_id (tree):             1.0

[3] train-test contamination: ডুপ্লিকেট সারি
আসল অর্ডার 600 | ডুপ্লিকেটসহ সারি 1185
ডুপ্লিকেটসহ ভাগ   : test 297 সারি, train-এ হুবহু আছে 220, accuracy 0.916
আগে drop_duplicates: test 150 সারি, train-এ হুবহু আছে 0, accuracy 0.673

তিনটিই একই গল্প বলে। সৎ feature দিয়ে model baseline-এর সামান্য ওপরে (০.৬৫৬ বনাম ০.৬৪৬); এই কাল্পনিক ডেটায় ফেরত আগে থেকে বলা সত্যিই কঠিন, আর সেটাই সৎ খবর। refund_processed যোগ করতেই ০.৯৮২; row_id দিতেই নিখুঁত ১.০; ডুপ্লিকেট রেখে ভাগ করলে test-এর ২৯৭টির ২২০টি train-এ হুবহু ছিল, আর accuracy ০.৬৭৩ থেকে ফুলে ০.৯১৬. খেয়াল করুন, এর কোনোটিতে কোনো warning আসেনি।

মূল ধারণা

সন্দেহজনকভাবে ভালো ফল হলো leakage-এর প্রথম লক্ষণ। যে সমস্যা মানুষের কাছেও কঠিন (ফেরত, churn, রোগের পূর্বাভাস), সেখানে হঠাৎ ৯৮% দেখলে আনন্দের আগে প্রশ্ন করুন: কোন feature সবচেয়ে বেশি টানছে, আর সেই তথ্য কি ভবিষ্যদ্বাণীর মুহূর্তে সত্যিই হাতে থাকবে?

(ঘ) Temporal leakage — ভবিষ্যতের তথ্য

পর্ব ২৬-এ দেখেছেন: আগামীকালের বিক্রি বলতে চাইলে shift(-1) দিয়ে লক্ষ্য বানানো ঠিক, কিন্তু shift(-1) বা ভবিষ্যৎমুখী চলমান গড় feature-এ ঢুকলে model “নিখুঁত” দেখায়। এই temporal leakage-এর আরও সূক্ষ্ম রূপ আছে: সারা বছরের ডেটা দিয়ে হিসাব করা “গ্রাহকের মোট ফেরতের হার” feature হিসেবে জানুয়ারির অর্ডারে বসানো; বা পরে সংশোধিত ডেটা (যেমন পরে ঠিক করা ঠিকানা) দিয়ে পুরোনো সিদ্ধান্তের পরীক্ষা। প্রতিকার একটাই নিয়ম: প্রতিটি feature-এর জন্য লিখে রাখুন, “এই মান কোন মুহূর্তে জানা যায়”, আর সেটি ভবিষ্যদ্বাণীর মুহূর্তের আগে কি না। Lag feature শুধু অতীতের দিকে তাকায় বলেই নিরাপদ।

৭ · Preprocessing leakage আর Pipeline — fit শুধু train-এ

সবচেয়ে প্রচলিত leakage কোনো কলাম থেকে আসে না, আসে প্রস্তুতির ধাপ থেকে। Standardization (প্রতিটি কলাম থেকে গড় বাদ দিয়ে standard deviation দিয়ে ভাগ) বা imputation (ফাঁকা ঘরে গড় বসানো) করতে একটি গড় লাগে। সেই গড় পুরো ডেটা থেকে নিলে test-এর মানও তাতে মিশে যায়। একে বলে preprocessing leakage. নিচের stepper ছোট সংখ্যায় দেখায় তথ্যটা ঠিক কোথায় চুইয়ে যায়: ছয়টি train অর্ডারের ডেলিভারির দিন, আর test-এ দুটি, যার একটি অস্বাভাবিক দেরির (৩০ দিন)।

ডেলিভারির দিন · z = (x − গড়) ÷ sd (StandardScaler, population sd)
ধাপ
train (৬)
test (২)
গড়, sd
  1. ডেটা। train-এ ছয়টি অর্ডার, ডেলিভারি ৩ থেকে ৭ দিন। test-এ দুটি: ৬ দিন আর একটি অস্বাভাবিক ৩০ দিন। বাস্তবে test হলো ভবিষ্যৎ, এখনো আসেনি।
  2. ঠিক পথ, fit. Scaler শুধু train দেখে: গড় ৫.০, sd ১.২৯১. Test-এর কোনো মান এই হিসাবে নেই।
  3. ঠিক পথ, রূপান্তর। Train-এর z −১.৫৫ থেকে ১.৫৫. Test-এর ৩০ দিন হয় ১৯.৩৬: স্পষ্ট বলে দেয় “এমন কিছু আগে দেখিনি”, যা বাস্তবে নতুন অর্ডারেও ঘটবে।
  4. ভুল পথ, fit. Scaler আটটি মানই দেখে। ৩০-এর টানে গড় ৮.২৫, sd ৮.৩০৩: test-এর একটি মান train-এর মাপকাঠি বদলে দিয়েছে।
  5. ভুল পথ, train. এখন train-এর সব z −০.৬৩ থেকে −০.১৫-এর সরু ফালিতে, সবই ঋণাত্মক। Model training-এর সময়েই “জানে” কোথাও একটি বড় মান আছে, যা সে কখনো দেখেনি।
  6. তুলনা। ভুল পথে test-এর ৩০ দিন মাত্র ২.৬২, তেমন অস্বাভাবিক দেখায় না। প্রভাব প্রায়ই ছোট, কিন্তু দিকটা সবসময় এক: test-কে train-এর মতো দেখায়, তাই score একটু আশাবাদী হয়।

বাস্তবে এই প্রভাব কত বড়? Breast cancer ডেটায় LogisticRegression দিয়ে ১০ রকম ৫-fold ভাগে গড় করে দেখা হয়েছে, একবার ২০% ঘর ফাঁকা করে imputation-সহ। সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0):

~/bazarghar-ai$ python prep_leak.py
scaler: আগে পুরো ডেটায়            গড় accuracy 0.9801
scaler: Pipeline-এর ভেতরে          গড় accuracy 0.9798
imputer+scaler: আগে পুরো ডেটায়    গড় accuracy 0.9749
imputer+scaler: Pipeline-এর ভেতরে  গড় accuracy 0.9747

পার্থক্য মাত্র ০.০০০৩ আর ০.০০০২: এখানে প্রায় নগণ্য। তাহলে এত সতর্কতা কেন? কারণ যে ধাপ label দেখে (feature বাছাই, target encoding, oversampling) বা যেখানে feature সংখ্যা উদাহরণের চেয়ে অনেক বেশি, সেখানে একই ভুল ভয়ংকর হয়। নিচের ব্লক সেই ক্লাসিক পরীক্ষা।

এলোমেলো ডেটা থেকে ০.৯১ accuracy

১০০ জন মানুষ, প্রত্যেকের ২০০০টি সংখ্যা, সব পুরো এলোমেলো; label-ও এলোমেলো, অর্ধেক ০ আর অর্ধেক ১. এখানে শেখার কিছুই নেই, সৎ উত্তর ০.৫-এর কাছে। ভুল পথ: আগে পুরো ডেটায় SelectKBest দিয়ে label-এর সঙ্গে সবচেয়ে বেশি মেলে এমন ২০টি feature বাছা (feature selection), তারপর cross-validation. ঠিক পথ: বাছাইকে Pipeline-এর প্রথম ধাপ বানানো, যাতে প্রতিটি fold-এ বাছাই শুধু সেই fold-এর train অংশ দেখে করে।

import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import make_pipeline

rng = np.random.default_rng(42)
X = rng.normal(size=(100, 2000))              # ১০০ জন, ২০০০টি পুরো এলোমেলো feature
y = rng.permutation(np.repeat([0, 1], 50))    # label-ও এলোমেলো: সৎ উত্তর ≈ 0.5
cv = StratifiedKFold(5, shuffle=True, random_state=42)

# ভুল পথ: আগে পুরো ডেটায় y-এর সঙ্গে সবচেয়ে মেলে এমন ২০টি feature বাছা, তারপর CV
X_sel = SelectKBest(f_classif, k=20).fit_transform(X, y)
leaky = cross_val_score(LogisticRegression(), X_sel, y, cv=cv)

# ঠিক পথ: বাছাই Pipeline-এর ভেতরে, তাই প্রতিটি fold-এ শুধু train অংশ দেখে
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression())
honest = cross_val_score(pipe, X, y, cv=cv)

print("ভুল পথ (বাছাই আগে):    ", np.round(leaky, 2), "গড়", round(leaky.mean(), 3))
print("ঠিক পথ (বাছাই ভেতরে):  ", np.round(honest, 2), "গড়", round(honest.mean(), 3))

# একবারের ভাগ্য নয়: আরও ১০টি এলোমেলো ডেটাসেটে একই পরীক্ষা
gap = []
for s in range(10):
    r = np.random.default_rng(100 + s)
    Xs, ys = r.normal(size=(100, 2000)), r.permutation(np.repeat([0, 1], 50))
    a = cross_val_score(LogisticRegression(),
                        SelectKBest(f_classif, k=20).fit_transform(Xs, ys), ys, cv=cv).mean()
    b = cross_val_score(pipe, Xs, ys, cv=cv).mean()
    gap.append((a, b))
gap = np.array(gap)
print("১০টি নতুন ডেটাসেটে ভুল পথ:", np.round(gap[:, 0], 2), "গড়", round(gap[:, 0].mean(), 3))
print("১০টি নতুন ডেটাসেটে ঠিক পথ:", np.round(gap[:, 1], 2), "গড়", round(gap[:, 1].mean(), 3))

আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):

ভুল পথ (বাছাই আগে):     [0.95 0.9  0.8  0.95 0.95] গড় 0.91
ঠিক পথ (বাছাই ভেতরে):   [0.4  0.4  0.55 0.35 0.4 ] গড় 0.42
১০টি নতুন ডেটাসেটে ভুল পথ: [0.83 0.82 0.88 0.83 0.87 0.87 0.85 0.87 0.91 0.86] গড় 0.859
১০টি নতুন ডেটাসেটে ঠিক পথ: [0.41 0.46 0.48 0.54 0.48 0.6  0.47 0.49 0.58 0.59] গড় 0.51

পুরো এলোমেলো ডেটায় ভুল পথ দেখাচ্ছে ০.৯১, আর ১০টি নতুন এলোমেলো ডেটাসেটে প্রতিবার ০.৮২ থেকে ০.৯১. একটি গবেষণাপত্রে এমন সংখ্যা দেখে কেউ সন্দেহ করতেন না। ঠিক পথে গড় ০.৫১, অর্থাৎ কাকতালের সমান; প্রথম ডেটাসেটে ০.৪২ এসেছে, যা ছোট ডেটায় কাকতালের স্বাভাবিক দোলা।

কারণ সহজ: ২০০০টি এলোমেলো কলামের মধ্যে কিছু কলাম শুধু কাকতালেই এই ১০০টি label-এর সঙ্গে মেলে (পর্ব ৩৭-এর multiple comparisons-এর সমস্যা)। পুরো ডেটায় বাছাই করলে সেই কাকতালীয় মিল test fold-এর সারিগুলোতেও আছে, কারণ বাছাই তাদের label দেখেই হয়েছে। Pipeline-এর ভেতরে বাছাই হলে প্রতিটি fold-এর test অংশ বাছাইয়ের কাছে অদেখা থাকে, তাই কাকতালীয় মিল সেখানে টেকে না।

make_pipeline(ধাপ১, ধাপ২, …, model) একটি বস্তু বানায়, যার fit প্রতিটি ধাপকে ক্রমে শুধু দেওয়া ডেটায় fit করে, আর predict একই শেখা মাপকাঠিতে রূপান্তর করে। cross_val_score-কে পুরো pipeline দিলে প্রতিটি fold-এ পুরো প্রস্তুতি নতুন করে হয়: leakage-মুক্ত হওয়ার সবচেয়ে সহজ উপায়। ColumnTransformer, বিভিন্ন কলামে বিভিন্ন ধাপ আর pipeline সংরক্ষণ পর্ব ৬৪ (scikit-learn pipeline — fit, transform আর leakage-মুক্ত কাজ)-এ।

৮ · Test set একবারই — বারবার উঁকি দিলে যা হয়

শেষ leakage-টি কোডে নয়, অভ্যাসে। Test-এ ০.৭১, একটি feature বদলে আবার test-এ ০.৭৩, আরেকটি সেটিংয়ে ০.৭৫: প্রতিবার test দেখে সিদ্ধান্ত নিলে test চুপিচুপি validation set হয়ে যায়। এই অভ্যাসের নাম data snooping. ব্যাপারটা ঠিক কতটা বিপজ্জনক, তা একটি চরম পরীক্ষায় দেখা যায়: যেখানে শেখার কিছুই নেই, সেখানে ৫০টি model বানিয়ে test দেখে সেরাটি বাছা।

import numpy as np
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(42)
def noise(n):                                   # feature আর label দুটোই এলোমেলো
    return rng.normal(size=(n, 30)), rng.integers(0, 2, size=n)
X_tr, y_tr = noise(200)
X_te, y_te = noise(100)                         # "test set"
X_new, y_new = noise(5000)                      # ভবিষ্যতের সত্যিকারের নতুন ডেটা
rows = []
for i in range(50):                             # ৫০টি "model": আলাদা feature আর C
    cols = rng.choice(30, size=5, replace=False)
    C = 10 ** rng.uniform(-3, 2)
    m = LogisticRegression(C=C).fit(X_tr[:, cols], y_tr)
    rows.append((m.score(X_te[:, cols], y_te), m.score(X_new[:, cols], y_new), i))

test = np.array([r[0] for r in rows])
best_test, best_new, best_i = max(rows)
print(f"৫০টি model-এর test accuracy: গড় {test.mean():.3f}, "
      f"সর্বনিম্ন {test.min():.3f}, সর্বোচ্চ {test.max():.3f}")
print(f"test দেখে বাছা সেরা: model {best_i}, test-এ {best_test:.3f}")
print(f"সেই model নতুন ৫০০০টিতে: {best_new:.3f}")

# পুরো পরীক্ষাটি ২০ বার: "সেরা"-র test score বনাম নতুন ডেটার score
gaps = []
for rep in range(20):
    Xa, ya = noise(200); Xb, yb = noise(100); Xc, yc = noise(2000)
    best = (-1, 0)
    for i in range(50):
        cols = rng.choice(30, size=5, replace=False)
        m = LogisticRegression(C=10 ** rng.uniform(-3, 2)).fit(Xa[:, cols], ya)
        best = max(best, (m.score(Xb[:, cols], yb), m.score(Xc[:, cols], yc)))
    gaps.append(best)
g = np.array(gaps)
print(f"২০ বারের গড়: সেরার test {g[:, 0].mean():.3f}, নতুন ডেটায় {g[:, 1].mean():.3f}")

আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):

৫০টি model-এর test accuracy: গড় 0.497, সর্বনিম্ন 0.420, সর্বোচ্চ 0.570
test দেখে বাছা সেরা: model 24, test-এ 0.570
সেই model নতুন ৫০০০টিতে: 0.496
২০ বারের গড়: সেরার test 0.587, নতুন ডেটায় 0.499

৫০টি model-এর গড় test score ০.৪৯৭, ঠিক যা হওয়ার কথা। কিন্তু ১০০টি উদাহরণের test-এ কাকতালেই কেউ ০.৪২, কেউ ০.৫৭ পায়। “সেরা”টি বেছে নিলে আমরা আসলে কাকতালের সর্বোচ্চটি বাছছি: ০.৫৭০, অথচ নতুন ৫০০০টি উদাহরণে সেই model ০.৪৯৬. পুরো পরীক্ষাটি ২০ বার করলে গড়ে বাছা model-এর test score ০.৫৮৭, নতুন ডেটায় ০.৪৯৯. এই প্রায় ৯ শতাংশ-বিন্দুর ফোলা পুরোটাই বাছাইয়ের ফল, শেখার নয়।

আসল প্রকল্পে ফোলা কম চোখে পড়ে, কিন্তু দিকটা একই। প্রতিকার: সব বাছাই validation বা cross-validation-এ; test একবার, শেষে। যদি test দেখে ফেলেন আর কিছু বদলাতে চান, তাহলে সৎভাবে বলুন যে সেটি আর নিরপেক্ষ test নয়, আর সম্ভব হলে নতুন একটি holdout জোগাড় করুন।

৯ · বাস্তবের leakage আর একটি চেকলিস্ট

Leakage শুধু শিক্ষার্থীর ভুল নয়। ২০২৩ সালে Sayash Kapoor আর Arvind Narayanan Patterns জার্নালে ML-ভিত্তিক বিজ্ঞানের গবেষণার একটি জরিপ প্রকাশ করেন। তাঁদের হিসাবে ১৭টি গবেষণা ক্ষেত্রে leakage-এর সমস্যা আগেই চিহ্নিত হয়েছিল, যা মিলিয়ে ২৯৪টি গবেষণাপত্রকে প্রভাবিত করেছিল (preprint সংস্করণে সংখ্যাগুলো একটু আলাদা ছিল)। নিজেদের একটি case study-তে, রাজনীতিবিজ্ঞানের গৃহযুদ্ধ পূর্বাভাসে, তাঁরা দেখান যে leakage সংশোধনের পর জটিল ML model-গুলো পুরোনো logistic regression-এর চেয়ে উল্লেখযোগ্যভাবে ভালো থাকে না। তাঁরা leakage-এর একটি শ্রেণিবিভাগ আর প্রতিটি গবেষণায় পূরণ করার জন্য “model info sheet” প্রস্তাব করেন। এর মানে এই নয় যে এসব ক্ষেত্রের সব কাজ ভুল; মানে হলো, সতর্কতা সব স্তরে দরকার।

ID-র leakage-এর একটি বহুল উদ্ধৃত উদাহরণ: Saharon Rosset ও সহকর্মীরা ২০০৮ সালের KDD Cup (স্তন ক্যান্সার শনাক্তকরণের প্রতিযোগিতা) জেতার অভিজ্ঞতায় জানান, রোগীর ID-তেই ক্যান্সারের সম্ভাবনার তথ্য ছিল। প্রতিযোগিতায় এমন সংকেত score বাড়ায়, হাসপাতালে নতুন রোগীর ক্ষেত্রে তার মূল্য নেই।

প্রশ্নকীভাবে যাচাই করবেন
ভাগ কি সবকিছুর আগে?Notebook-এর প্রথম দিকের cell-এ split; তার আগে কোনো fit, গড় বা feature বাছাই নেই
প্রতিটি feature কি ভবিষ্যদ্বাণীর মুহূর্তে জানা?প্রতিটি কলামের পাশে “কখন জানা যায়” লিখুন; label-এর পরের ঘটনা (refund, বাতিল, রিভিউ) বাদ
বাস্তবের একক কী?নতুন গ্রাহক বা রোগী হলে group split; ভবিষ্যৎ হলে time-based split
Train আর test-এ ডুপ্লিকেট আছে?ভাগের আগে drop_duplicates; পরে দুই ভাগের merge করে মিল গুনুন
ID, তারিখ বা সারির ক্রম কি feature হয়ে গেছে?ID কলাম বাদ দিন; feature importance-এর শীর্ষে ID বা ক্রম থাকলে সন্দেহ
সব preprocessing কি Pipeline-এ?Scaler, imputer, encoder, feature বাছাই, oversampling সব Pipeline-এর ভেতরে
Test কতবার দেখা হয়েছে?একবার। বাছাই validation বা cross-validation-এ; test দেখে বদলালে তা রিপোর্টে লিখুন
ফল কি সন্দেহজনকভাবে ভালো?baseline-এর সঙ্গে তুলনা; শীর্ষ feature একে একে বাদ দিয়ে দেখুন score কোথায় ভেঙে পড়ে

১০ · AI-এর যুগে — AI assistant-এর লেখা split আর মূল্যায়ন যাচাই

“এই CSV দিয়ে একটা model বানিয়ে accuracy দেখাও” বললে AI assistant যে কোড দেয়, তাতে leakage খুব সাধারণ, কারণ ইন্টারনেটের অনেক টিউটোরিয়ালেই পুরো ডেটায় scaling করে তারপর ভাগ করা হয়। কোড চলে, সংখ্যা ভালো আসে, তাই ভুল চোখে পড়ে না।

AI যা প্রায়ই করেকীভাবে ধরবেন
StandardScaler().fit_transform(X) বা SelectKBest আগে, train_test_split পরেকোডে প্রথম fit-এর লাইন খুঁজুন; তা split-এর পরে আর Pipeline-এর ভেতরে কি না
Training set-এ accuracy রিপোর্ট করে, বা test দেখে hyperparameter বাছেকোন ডেটায় সংখ্যাটি মাপা, তা জিজ্ঞেস করুন; বাছাই validation বা cross-validation-এ চান
সময় বা গ্রাহকের কাঠামো উপেক্ষা করে এলোমেলো ভাগ করেডেটার একক আর তারিখ কলাম নিজে দেখুন; group বা time split চান
অস্তিত্বহীন parameter বানায়, যেমন train_test_split(..., groups=g)নিজের version-এর অফিসিয়াল docs দেখুন; group-এর জন্য GroupShuffleSplit
একটি seed-এর একটি ভাগের ফলকে চূড়ান্ত বলেকয়েকটি seed বা cross-validation-এর গড় আর পরিসর চান
সন্দেহজনক ৯৯% দেখে অভিনন্দন জানায়baseline আর শীর্ষ feature দেখুন; label-এর পরের কলাম আছে কি না খুঁজুন

নির্দিষ্ট করে চান, আর ভাগের কাঠামো নিজে বলে দিন। আসল গ্রাহকের ডেটা পেস্ট করবেন না; কলামের নাম আর কয়েকটি কাল্পনিক সারিই যথেষ্ট।

আমার orders.csv-তে কলাম: order_id, customer_id, order_date, price, delivery_days, first_order, refund_processed, returned (লক্ষ্য, 0/1, প্রায় ৫% ফেরত). Python 3.14, pandas 3 আর scikit-learn 1.8 দিয়ে কোড লেখো যা: ১. order_id আর refund_processed feature থেকে বাদ দেবে, আর comment-এ লিখবে কেন (ID আর ফেরতের পরে জানা তথ্য)। ২. drop_duplicates করবে, তারপর order_date ধরে শেষ ২০% সময়কে test হিসেবে আলাদা রাখবে; train অংশের ভেতরে customer_id ধরে GroupKFold(5) দিয়ে cross-validation করবে। ৩. সব preprocessing (imputer, scaler) একটি Pipeline-এর ভেতরে রাখবে; Pipeline-এর বাইরে কোনো fit থাকবে না। ৪. cross-validation-এর গড় আর পরিসর, সবসময় "ফেরত নয়" বলার baseline, আর শেষে test-এর ফল একবার ছাপাবে। ৫. train আর test-এ একই customer_id কয়টি আর ডুপ্লিকেট সারি কয়টি, তা গুনে ছাপাবে। কোনো deprecated API ব্যবহার করবে না।

AI-এর লেখা মূল্যায়নের কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

আরও গভীরে: Christophe Ambroise আর Geoffrey McLachlan ২০০২ সালে PNAS-এ microarray ডেটায় দেখান, cross-validation-এর বাইরে gene বাছাই করলে প্রায় শূন্য ভুলের হার দেখা যেতে পারে, যদিও সৎ মাপে তা অনেক বেশি; Hastie, Tibshirani আর Friedman-এর The Elements of Statistical Learning বইয়ে “cross-validation-এর ভুল আর ঠিক পথ” নামে একই পরীক্ষা আছে। Test set বারবার ব্যবহারের সমস্যাকে গবেষকেরা “adaptive data analysis” বলেন; Cynthia Dwork ও সহকর্মীরা ২০১৫ সালে Science-এ “reusable holdout” নামে একটি কৌশল প্রস্তাব করেন, যা differential privacy-র ধারণা দিয়ে holdout-এর তথ্য ফাঁস সীমিত রাখে।

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

  • Validation আর test set-এর পার্থক্য? Validation দেখে model ও hyperparameter বাছা হয়; test একেবারে শেষে একবার, নিরপেক্ষ মাপের জন্য।
  • ৫% positive-এর ২০০০টি সারির ২৫% stratified test-এ কয়টি positive? ৫০০টি test সারি × ০.০৫ = ২৫টি।
  • Data leakage কী, একটি উদাহরণ? ভবিষ্যদ্বাণীর মুহূর্তে অজানা তথ্য model-এ ঢোকা; যেমন ফেরতের পূর্বাভাসে refund_processed কলাম।
  • Scaler কেন শুধু train-এ fit? পুরো ডেটায় fit করলে test-এর গড় ও sd মাপকাঠিতে ঢোকে; Pipeline-এ রাখলে cross-validation-এর প্রতিটি fold-এ তা ঠিকভাবে হয়।
  • Time series-এ এলোমেলো k-fold কেন ভুল? ভবিষ্যতের ডেটা দিয়ে শিখে অতীত “পূর্বাভাস” হয়; TimeSeriesSplit-এ train সবসময় test-এর আগে।

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

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

প্র ০১পুরো এলোমেলো ডেটাতেও ভাগের আগে feature বাছলে ০.৯১ accuracy আসে। এই ভুল গবেষণায় কতটা পুরোনো, আর কেন এটি বারবার ফিরে আসে?

এই ফাঁদের সবচেয়ে বিখ্যাত বিবরণ জীববিজ্ঞান থেকে। ২০০০-এর দশকের শুরুতে microarray প্রযুক্তিতে একটি নমুনায় হাজার হাজার gene-এর মাত্রা মাপা যেত, অথচ রোগী থাকতেন কয়েক ডজন। গবেষকেরা স্বাভাবিকভাবেই আগে সব রোগীর ডেটা দেখে রোগের সঙ্গে সবচেয়ে বেশি মেলে এমন কয়েকটি gene বাছতেন, তারপর সেই gene দিয়ে classifier বানিয়ে cross-validation করতেন। ২০০২ সালে Christophe Ambroise আর Geoffrey McLachlan PNAS-এ দেখান যে এভাবে প্রায় নিখুঁত ফল আসতে পারে এমনকি যখন সৎ মাপে ভুলের হার অনেক বেশি, আর তাঁরা একে “selection bias” বলেন। পরে Hastie, Tibshirani আর Friedman তাঁদের পরিচিত পাঠ্যবইয়ে একই পরীক্ষা “ভুল পথ আর ঠিক পথ” নামে শিক্ষার্থীদের সামনে রাখেন।

এটি বারবার ফেরে কারণ ভুল পথটি দেখতে যুক্তিসঙ্গত। “আগে ভালো feature বেছে নিই, তারপর model” শুনতে পরিষ্কার পরিকল্পনা। সমস্যাটা অদৃশ্য: বাছাই নিজেই label দেখে একটি শেখার কাজ করেছে, আর সেই শেখার মধ্যে test-এর সারিগুলোও ছিল। Feature যত বেশি আর উদাহরণ যত কম, কাকতালীয় মিল তত বেশি, আর ফাঁদ তত গভীর। আজকের genomics, text বা ছবির ডেটায় হাজার হাজার feature সাধারণ ঘটনা, তাই ফাঁদটিও প্রাসঙ্গিক থাকে। এই পর্বের পরীক্ষায় ১০টি নতুন এলোমেলো ডেটাসেটেও ভুল পথ প্রতিবার ০.৮২-এর বেশি দিয়েছে, যা দেখায় এটি এক-আধবারের দুর্ভাগ্য নয়।

সমাধানের নিয়মটি সাধারণ: label দেখে এমন প্রতিটি ধাপ cross-validation-এর ভেতরে থাকবে। Feature বাছাই, target encoding, oversampling, এমনকি “কোন কলামগুলো রাখব” নিয়ে chart দেখে নেওয়া সিদ্ধান্তও এর মধ্যে পড়ে।

মূল উপলব্ধি: মূল্যায়ন তখনই সৎ, যখন test অংশ শেখার পুরো প্রক্রিয়ার কাছে অদেখা, শুধু শেষ model-এর কাছে নয়; feature বাছাইও শেখার অংশ।

প্র ০২Kapoor ও Narayanan ML-ভিত্তিক বিজ্ঞানে leakage-কে “reproducibility crisis”-এর অংশ বলেছেন। একটি গবেষণার ফল পুনরুৎপাদন করা গেলেও তা কেন ভুল হতে পারে?

Reproducibility নিয়ে আলোচনায় প্রায়ই দুটি ধারণা মিশে যায়। এক, একই কোড আর একই ডেটা চালালে একই সংখ্যা আসে কি না (computational reproducibility). দুই, সংখ্যাটি যা দাবি করে, তা সত্য কি না। Leakage দ্বিতীয়টির সমস্যা। একটি leaky pipeline পুরোপুরি নির্ধারিত; seed দিলে প্রতিবার হুবহু একই ০.৯১ দেয়। কোড শেয়ার করা, seed রাখা, সব ভালো অভ্যাস মেনেও ফল ভুল থাকতে পারে।

Sayash Kapoor আর Arvind Narayanan-এর ২০২৩ সালের Patterns-এর প্রবন্ধটি এই বিষয়ে একটি পরিচিত সূত্র। তাঁরা বিভিন্ন ক্ষেত্রের প্রকাশিত পর্যালোচনা জড়ো করে দেখান যে ১৭টি ক্ষেত্রে leakage আগেই ধরা পড়েছিল, মোট ২৯৪টি গবেষণাপত্রে। নিজেদের case study-তে, রাজনীতিবিজ্ঞানের গৃহযুদ্ধ পূর্বাভাসে, তাঁরা কয়েকটি প্রকাশিত কাজ আবার চালিয়ে দেখান যে leakage শুধরে নিলে জটিল ML-এর কথিত শ্রেষ্ঠত্ব কার্যত মিলিয়ে যায়, পুরোনো logistic regression-এর সঙ্গে বড় পার্থক্য থাকে না। তাঁরা leakage-এর একটি শ্রেণিবিভাগও দেন, যার মধ্যে আছে train আর test আলাদা না রাখা, বৈধ নয় এমন feature ব্যবহার, আর test-এর বণ্টন বাস্তবের প্রশ্নের সঙ্গে না মেলা (যেমন সময় বা group-এর কাঠামো উপেক্ষা)।

সংখ্যাগুলো পড়তে সতর্ক থাকা দরকার: এগুলো তাঁদের জড়ো করা প্রকাশিত পর্যালোচনার হিসাব, প্রতিটি ক্ষেত্রের সব গবেষণার নমুনা নয়, আর preprint সংস্করণে সংখ্যাগুলো একটু ভিন্ন ছিল। তবু বার্তাটি স্পষ্ট: যেখানে ML একটি বৈজ্ঞানিক দাবির প্রমাণ, সেখানে leakage শুধু প্রযুক্তিগত ভুল নয়, ভুল জ্ঞান। তাঁদের প্রস্তাবিত “model info sheet” একটি প্রশ্নমালা, যেখানে লেখককে বলতে হয় test কীভাবে আলাদা রাখা হয়েছে আর প্রতিটি feature কেন বৈধ।

মূল উপলব্ধি: একই সংখ্যা বারবার পাওয়া মানে সংখ্যাটি সত্য নয়; সততার প্রমাণ হলো ভাগের নকশা আর প্রতিটি feature-এর বৈধতা লিখিতভাবে দেখানো।

প্র ০৩একটি সংখ্যা কখন feature হতে পারে আর কখন তা leakage? “ভবিষ্যদ্বাণীর মুহূর্ত” ধারণাটি কেন সব প্রশ্নের চাবি?

একই কলাম এক প্রশ্নে বৈধ, অন্য প্রশ্নে leakage. ধরুন refund_processed. “অর্ডার দেওয়ার সময় বলুন ফেরত আসবে কি না” প্রশ্নে এটি leakage, কারণ তখন এর মান সবসময় ০. কিন্তু “একটি ফেরত হওয়া অর্ডারে গ্রাহক আবার কিনবেন কি না” প্রশ্নে এটি পুরোপুরি বৈধ, কারণ সেই মুহূর্তে টাকা ফেরত হয়ে গেছে। তাই leakage কলামের স্বভাব নয়; কলাম আর প্রশ্নের সময়ের সম্পর্ক।

Kaufman, Rosset আর Perlich ২০১১ সালে এই দৃষ্টিভঙ্গিকে আনুষ্ঠানিক রূপ দেন। তাঁদের মূল পরামর্শ ছিল প্রতিটি উদাহরণের সঙ্গে একটি “ভবিষ্যদ্বাণীর মুহূর্ত” রাখা, আর শুধু সেই মুহূর্তের আগে জানা তথ্য দিয়ে feature বানানো; তাঁরা একে learn-predict separation বলেন। Rosset ও সহকর্মীরা KDD Cup ২০০৮-এর অভিজ্ঞতায় দেখেন রোগীর ID-তেই ক্যান্সারের সম্ভাবনার তথ্য ছিল, যা কোনো চিকিৎসা-জ্ঞান নয়, ডেটা জোগাড়ের প্রক্রিয়ার ছাপ। চিকিৎসার ডেটায় আরেকটি পরিচিত ধরন: রোগনির্ণয়ের পূর্বাভাসে এমন কোনো চিকিৎসা বা পরীক্ষার কলাম ঢুকে যাওয়া, যা ডাক্তার রোগ সন্দেহ করার পরেই দেন; তখন model আসলে ডাক্তারের সন্দেহ শিখছে।

বাস্তবে এটি যাচাইয়ের একটি সহজ পদ্ধতি: প্রতিটি কলামের জন্য টেবিলে একটি “কখন জানা যায়” ঘর রাখা, আর ডেটা ইঞ্জিনিয়ারদের কাছে জানতে চাওয়া কলামটি কোন সিস্টেম কখন লেখে বা হালনাগাদ করে। পরে সংশোধিত মান (যেমন পরে ঠিক করা ঠিকানা) সবচেয়ে কপট, কারণ টেবিলে তা শুধু শেষ মানটি দেখায়। অনেক দল এর জন্য “point-in-time” ডেটা রাখে: প্রতিটি মান কোন তারিখে কী ছিল।

মূল উপলব্ধি: “এই feature কি ভালো?” প্রশ্নের আগে জিজ্ঞেস করুন “এই মান কখন জানা যায়?”; ভবিষ্যদ্বাণীর মুহূর্তের পরে জানা যে কোনো তথ্য, যত নিরীহই দেখাক, leakage.

প্র ০৪বড় benchmark-এর test set বছরের পর বছর হাজারো গবেষক ব্যবহার করেন। এতে কি পুরো গবেষণা-সমাজ মিলে test set-এ “overfit” করে ফেলে?

অংশ ৮-এর পরীক্ষা বলে, একজন মানুষ ৫০টি model-এর মধ্যে test দেখে সেরাটি বাছলে test score ফুলে ওঠে। তাহলে যখন হাজারো দল একই public test set-এ নিজেদের model প্রকাশ করে, আর পরের দল আগের সেরা ফল ছাড়িয়ে যাওয়ার চেষ্টা করে, তখন কি পুরো ক্ষেত্রটিই সেই test-এ বাছাই করছে না? প্রশ্নটি গুরুতর, আর গবেষকেরা একে “adaptive data analysis”-এর সমস্যা বলেন। Cynthia Dwork ও সহকর্মীরা ২০১৫ সালে Science-এ দেখান কীভাবে একটি holdout-কে নিয়ন্ত্রিতভাবে বারবার ব্যবহার করা যায়, প্রতিটি প্রশ্নে কতটুকু তথ্য ফেরত যাবে তা সীমিত রেখে।

বাস্তবে কী ঘটেছে, তা যাচাইয়ের একটি সরাসরি চেষ্টা Benjamin Recht ও সহকর্মীদের ২০১৯ সালের কাজ: তাঁরা CIFAR-10 আর ImageNet-এর জন্য মূল পদ্ধতি অনুসরণ করে নতুন test set বানান আর প্রকাশিত অনেক model সেখানে চালান। Accuracy উল্লেখযোগ্যভাবে কমেছিল, কিন্তু মজার ব্যাপার, model-গুলোর আপেক্ষিক ক্রম মোটামুটি একই ছিল। লেখকদের ব্যাখ্যা অনুযায়ী পতনের বড় অংশ সম্ভবত নতুন ডেটার সূক্ষ্ম বণ্টনগত পার্থক্য থেকে, বারবার ব্যবহারের ফলে adaptive overfitting থেকে ততটা নয়। অর্থাৎ ভয়টা বাস্তব, কিন্তু অন্তত এই উদাহরণে সবচেয়ে বড় ধাক্কা এসেছে বণ্টনের বদল থেকে।

বাজারঘরের মতো একটি দলের জন্য শিক্ষাটি দুই রকম। এক, ছোট test set-এ বারবার উঁকি দেওয়া বিপজ্জনক, কারণ সেখানে কাকতালের দোলা বড়। দুই, test set যত যত্নেই রাখুন, বাস্তবের ডেটা সময়ের সঙ্গে বদলাবে; তাই প্রকাশের পর নতুন ডেটায় নিয়মিত মাপা আর একটি “তাজা” holdout বাঁচিয়ে রাখা দরকার।

মূল উপলব্ধি: একটি test set-এর বিশ্বাসযোগ্যতা প্রতিবার ব্যবহারে একটু ক্ষয় হয়; আর ব্যবহার না করলেও বাস্তব বদলে যায়, তাই সৎ মূল্যায়ন একবারের কাজ নয়।

প্র ০৫একই রোগী, একই গ্রাহক, একই দোকান: কখন group split জরুরি, আর কখন তা প্রয়োজনের চেয়ে কড়া? সঠিক ভাগ বাছার নীতি কী?

মূল নীতি: test set বাস্তবে model যে পরিস্থিতিতে কাজ করবে, তার যত কাছাকাছি সম্ভব নকল করবে। চিকিৎসার ছবিতে এর পরিচিত উদাহরণ পাওয়া যায়: একজন রোগীর অনেকগুলো ছবি বা একটি স্ক্যানের অনেকগুলো টুকরো থাকে। ছবি ধরে এলোমেলো ভাগ করলে একই রোগীর ছবি দুই দিকে পড়ে, আর model রোগীর শারীরিক বৈশিষ্ট্য বা যন্ত্রের ছাপ চিনে ফেলতে পারে। চিকিৎসা-ইমেজিংয়ের বিভিন্ন পর্যালোচনায় রোগী-স্তরে ভাগ না করাকে একটি সাধারণ ত্রুটি হিসেবে উল্লেখ করা হয়েছে। এই পর্বের ছোট পরীক্ষাতেও এলোমেলো ভাগ ০.৭৬১ আর গ্রাহক ধরে ভাগ ০.৫২২ দিয়েছে: পার্থক্যটা “চেনা মানুষ” আর “নতুন মানুষ”-এর।

কিন্তু group split সবসময় ঠিক উত্তর নয়। যদি বাজারঘরের model শুধু নিয়মিত গ্রাহকদের পরের অর্ডারে চলবে, তাহলে গ্রাহকের পুরোনো আচরণ জানা থাকা বৈধ তথ্য, আর তখন সঠিক ভাগ হলো সময় ধরে: প্রতিটি গ্রাহকের পুরোনো অর্ডার train-এ, নতুনগুলো test-এ। আবার কিছু ক্ষেত্রে দুই স্তরই লাগে: নতুন দোকানে নতুন মাসে কাজ করবে এমন model-এর জন্য দোকান ধরে আর সময় ধরে, দুইভাবেই আলাদা রাখা। প্রয়োজনের চেয়ে কড়া ভাগের দাম হলো কম ডেটায় train আর হতাশাজনক কিন্তু অপ্রাসঙ্গিক সংখ্যা।

বাস্তবে সিদ্ধান্তটা আসে ব্যবসার প্রশ্ন থেকে, কোড থেকে নয়: model চালু হলে কাকে বা কোন সময়কে সে প্রথমবার দেখবে? সেই একক যদি test-এ নতুন না হয়, তাহলে score সেই প্রশ্নের উত্তর নয়। ভালো অভ্যাস হলো রিপোর্টে এক লাইনে লিখে দেওয়া: “test = নভেম্বরের অর্ডার, train-এ নেই এমন গ্রাহকের”, যাতে পাঠক জানেন সংখ্যাটি কোন দাবি সমর্থন করে।

মূল উপলব্ধি: ভাগের ধরন একটি দাবির ঘোষণা; “নতুন অর্ডার”, “নতুন গ্রাহক” আর “আগামী মাস” তিনটি আলাদা প্রশ্ন, আর প্রতিটির জন্য test-এ আলাদা জিনিসকে নতুন রাখতে হয়।

অনুশীলন

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

  1. ভাগের হিসাব আর scaler (হিসাব): (ক) বাজারঘরের ২০০০টি অর্ডারের ৬% ফেরত। test_size=0.25 আর stratify দিয়ে ভাগ করলে test আর train-এ কয়টি সারি আর কয়টি ফেরত? তারপর train অংশে ৫-fold stratified cross-validation করলে প্রতিটি পরীক্ষার fold-এ কয়টি সারি আর কয়টি ফেরত? (খ) Train-এ ডেলিভারির দিন ২, ৪, ৬; test-এ ১০. StandardScaler (population sd) শুধু train-এ fit করলে আর চারটি মানেই fit করলে test-এর z কত?
    • (ক) মোট ফেরত ২০০০ × ০.০৬ = ১২০. Test: ৫০০ সারি, ১২০ × ০.২৫ = ৩০টি ফেরত; train: ১৫০০ সারি, ৯০টি ফেরত। ৫-fold-এ প্রতিটি পরীক্ষার fold ১৫০০ ÷ ৫ = ৩০০ সারি, ফেরত ৯০ ÷ ৫ = ১৮টি; প্রতি round-এ train ১২০০ সারি।
    • (খ) ঠিক পথ: গড় (২ + ৪ + ৬)/৩ = ৪; variance (৪ + ০ + ৪)/৩ = ৮/৩, sd = √(৮/৩) ≈ ১.৬৩৩. z = (১০ − ৪)/১.৬৩৩ ≈ ৩.৬৭.
    • ভুল পথ: গড় (২ + ৪ + ৬ + ১০)/৪ = ৫.৫; বিচ্যুতির বর্গ ১২.২৫, ২.২৫, ০.২৫, ২০.২৫, যোগ ৩৫, variance ৩৫/৪ = ৮.৭৫, sd ≈ ২.৯৫৮. z = (১০ − ৫.৫)/২.৯৫৮ ≈ ১.৫২.

    scikit-learn-এ যাচাই (ডেস্কটপ, সত্যিকারের run): StandardScaler train-এ গড় 4.0, scale 1.633, z 3.674; চারটি মানে গড় 5.5, scale 2.958, z 1.521. ভুল পথে test-এর মানটি নিজের অস্বাভাবিকতার অর্ধেকের বেশি লুকিয়ে ফেলেছে।

  2. Leakage পাহারাদার (কোড): একটি function check_split(train_df, test_df, id_col) লিখুন যা তিনটি জিনিস ছাপায়: (ক) দুই ভাগে একই id_col (যেমন customer_id) কয়টি, (খ) feature কলামগুলোতে হুবহু মিলে যায় এমন test সারি কয়টি, (গ) test-এর সবচেয়ে পুরোনো তারিখ train-এর সবচেয়ে নতুন তারিখের পরে কি না (কলাম order_date).
    import pandas as pd
    
    def check_split(train_df, test_df, id_col, date_col="order_date"):
        shared = set(train_df[id_col]) & set(test_df[id_col])
        feats = [c for c in train_df.columns if c not in (id_col, date_col)]
        dup = test_df[feats].merge(train_df[feats].drop_duplicates(), how="inner")
        in_order = test_df[date_col].min() > train_df[date_col].max()
        print(f"দুই ভাগে একই {id_col}: {len(shared)}")
        print(f"train-এ হুবহু আছে এমন test সারি: {len(dup)}")
        print(f"test পুরোপুরি train-এর পরে: {in_order}")
        return len(shared) == 0 and len(dup) == 0 and in_order
    
    tr = pd.DataFrame({"customer_id": [1, 1, 2], "price": [500, 700, 300],
                       "order_date": pd.to_datetime(["2026-01-03", "2026-01-09", "2026-01-12"])})
    te = pd.DataFrame({"customer_id": [2, 3], "price": [300, 900],
                       "order_date": pd.to_datetime(["2026-01-10", "2026-01-20"])})
    print(check_split(tr, te, "customer_id"))

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

    দুই ভাগে একই customer_id: 1
    train-এ হুবহু আছে এমন test সারি: 1
    test পুরোপুরি train-এর পরে: False
    False

    ছোট উদাহরণে তিনটি সমস্যাই ধরা পড়েছে: গ্রাহক ২ দুই দিকে, তাঁর ৩০০ টাকার অর্ডারের feature হুবহু train-এ আছে, আর test-এর ১০ জানুয়ারি train-এর ১২ জানুয়ারির আগে। খেয়াল করুন, feature মিলে যাওয়া মানেই সবসময় ডুপ্লিকেট নয়; দুটি আলাদা অর্ডারেরও একই দাম থাকতে পারে, তাই সংখ্যাটি সতর্কসংকেত, প্রমাণ নয়। এমন একটি function প্রতিটি প্রকল্পের শুরুতে চালানো ভালো অভ্যাস, আর তা একটি ছোট pytest test হিসেবেও রাখা যায় (পর্ব ১১)।

  3. সমালোচনা ও নতুন নকশা: একজন সহকর্মীর churn (গ্রাহক চলে যাবেন কি না) model-এর বিবরণ: “২০২৫ আর ২০২৬ সালের সব গ্রাহকের ডেটা নিয়েছি। Feature-এ আছে মোট অর্ডার, গত ৩০ দিনের লগইন আর ‘account_closed_date’ ফাঁকা কি না। আগে পুরো ডেটায় StandardScaler আর SelectKBest, তারপর এলোমেলো ৮০/২০ ভাগ। ৩০টি model test-এ চালিয়ে সেরাটি নিয়েছি: accuracy ৯৭%.” কী কী ভুল, আর আপনি কীভাবে সাজাবেন?
    • Target leakage: account_closed_date ফাঁকা কি না, সেটিই প্রায় label; চলে যাওয়ার পরেই তা ভরে। বাদ দিতে হবে। “গত ৩০ দিনের লগইন” কোন তারিখ ধরে মাপা, তাও দেখতে হবে: চলে যাওয়ার পরের ৩০ দিন হলে সেটিও leakage.
    • Preprocessing leakage: scaler আর বিশেষ করে SelectKBest ভাগের আগে; দুটিই Pipeline-এর ভেতরে যাবে।
    • ভুল ধরনের ভাগ: churn একটি ভবিষ্যতের প্রশ্ন; একটি তারিখ ধরে আগে train, পরে test. একই গ্রাহকের একাধিক সারি থাকলে গ্রাহক ধরেও আলাদা রাখা।
    • Test-এ বাছাই: ৩০টি model-এর সেরাটি test দেখে বাছা; ৯৭% তাই ফোলা। বাছাই validation বা cross-validation-এ, test একবার।
    • Baseline নেই: চলে যাওয়া গ্রাহক কম হলে “কেউ চলে যাবে না” বলেই হয়তো ৯০%+; সেই তুলনা দরকার।

    নতুন নকশা: ভবিষ্যদ্বাণীর মুহূর্ত ঠিক করা (যেমন প্রতি মাসের ১ তারিখ), প্রতিটি feature সেই মুহূর্তের আগের ডেটা থেকে বানানো; শেষ কয়েক মাস test হিসেবে তালাবন্ধ; train অংশে সময় ধরে cross-validation; Pipeline-এ সব প্রস্তুতি; baseline আর কয়েকটি ভাগের পরিসর রিপোর্ট; শেষে test একবার।

  4. আউটপুট অনুমান: নিচের কোড কী ছাপবে?
    import numpy as np
    from sklearn.model_selection import TimeSeriesSplit, KFold
    
    for tr, te in TimeSeriesSplit(n_splits=3).split(np.arange(9)):
        print(tr.tolist(), te.tolist())
    print([len(te) for _, te in KFold(4).split(np.arange(10))])

    সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0):

    [0, 1, 2] [3, 4]
    [0, 1, 2, 3, 4] [5, 6]
    [0, 1, 2, 3, 4, 5, 6] [7, 8]
    [3, 3, 2, 2]

    TimeSeriesSplit-এ test-এর আকার n ÷ (n_splits + 1) = ৯ ÷ ৪, পূর্ণসংখ্যায় ২; শেষ ৩ × ২ = ৬টি সারি তিনটি test-এ যায়, বাকি প্রথম ৩টি সবসময় train. প্রতিবার train আগের সব সারি নেয়, তাই ৩ থেকে ৫ থেকে ৭. KFold(4)-এ ১০টি সারি সমান ভাগ হয় না: প্রথম ১০ mod ৪ = ২টি fold একটি বেশি পায়, তাই ৩, ৩, ২, ২. খেয়াল করুন, shuffle না দিলে KFold সারির ক্রম ধরে কাটে; সাজানো ডেটায় এটি নিজেই একটি ফাঁদ।

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

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

ক্রমএকটি leakage-মুক্ত মূল্যায়নের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রশ্ন আর ভবিষ্যদ্বাণীর মুহূর্ত ঠিক করা, তারপর সেই মুহূর্তের পরের কলাম বাদ দেওয়া
  2. ডুপ্লিকেট সরিয়ে একক (গ্রাহক বা সময়) অনুযায়ী test set আলাদা করে তালাবন্ধ রাখা
  3. Preprocessing আর model একটি Pipeline-এ সাজানো
  4. Train অংশে cross-validation দিয়ে model আর সেটিং বাছাই
  5. বাছা সেটিংয়ে পুরো train অংশে আবার fit করা
  6. Test set একবার খুলে baseline-এর পাশে ফল রিপোর্ট করা

৪০০টি অর্ডারের ৫% ফেরত। train_test_split(..., test_size=0.25, stratify=y) দিলে test set-এ কয়টি ফেরত থাকবে?

  • ২০টি
  • ১৫টি
  • ৫টি
  • seed-এর ওপর নির্ভর করে, আগে বলা যায় না

একটি ফেরতের পূর্বাভাস model-এ কোন feature সবচেয়ে স্পষ্ট target leakage?

  • পণ্যের দাম
  • refund_processed: টাকা ফেরত প্রক্রিয়া হয়েছে কি না
  • গ্রাহকের এটি প্রথম অর্ডার কি না
  • প্রতিশ্রুত ডেলিভারির দিন

১০০টি সারি আর ২০০০টি পুরো এলোমেলো feature-এর ডেটায় (label-ও এলোমেলো) কেউ আগে পুরো ডেটায় সবচেয়ে ভালো ২০টি feature বেছে তারপর ৫-fold cross-validation করলেন। কী আশা করবেন?

  • প্রায় ০.৫, কারণ ডেটা এলোমেলো
  • প্রায় ০.৫, কারণ cross-validation সব ঠিক করে দেয়
  • প্রায় ০, কারণ feature বেশি
  • কাকতালের অনেক ওপরে, যেমন ০.৮–০.৯, যা পুরোপুরি leakage

একই গ্রাহকের অনেক অর্ডার আছে, আর model নতুন গ্রাহকের ওপর চলবে। কোন ভাগ সবচেয়ে উপযুক্ত?

  • GroupKFold বা GroupShuffleSplit, groups = customer_id
  • train_test_split(..., stratify=y)
  • KFold(5, shuffle=True)
  • Leave-one-out

Train-এ ডেলিভারির দিন 4, 5, 6, 5, 7, 3 আর test-এ 6, 30. StandardScaler আটটি মানেই fit করলে গড় 8.25 হয়। এর মূল সমস্যা কী?

  • গড়টি ভুল হিসাব হয়েছে
  • Scaling কখনো করা উচিত নয়
  • Test-এর মান মাপকাঠিতে ঢুকেছে; fit শুধু train-এ (গড় 5.0) হওয়া উচিত
  • sd-এর বদলে sample sd নেওয়া উচিত ছিল

একটি দল একই ১০০টি উদাহরণের test set-এ ৫০টি model মেপে সেরাটির score ০.৫৭ রিপোর্ট করল। নতুন ডেটায় সেই model-এর score সম্পর্কে কী বলা যায়?

  • ঠিক ০.৫৭ থাকবে, কারণ test set আলাদা ছিল
  • সম্ভবত কম, কারণ test দেখে বাছাই score ফুলিয়েছে
  • সম্ভবত বেশি, কারণ সেরা model বাছা হয়েছে
  • Test-এর আকার ১০০ হলে বাছাইয়ের কোনো প্রভাব নেই

দৈনিক বিক্রির পূর্বাভাসে ১২ দিনের ডেটায় TimeSeriesSplit(n_splits=4) দিলে প্রথম ভাগে কোন দিনগুলো test-এ যায়?

  • দিন ১, ২, ৩
  • এলোমেলো ৩টি দিন
  • দিন ১১, ১২
  • দিন ৫, ৬

শব্দকোষ

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

মনে রাখুন

  1. তিন ভাগ: train-এ শেখা, validation-এ বাছাই, test-এ শেষে একবার মাপা। অনুপাত প্রচলিত অভ্যাস; ভাগ হয় সবকিছুর আগে।
  2. একটি ভাগ একটি নমুনা: বিরল শ্রেণিতে stratify দিন; seed বদলালেই accuracy ০.৯০৯ থেকে ০.৯৮৬ হতে পারে, তাই cross-validation বা কয়েকটি ভাগের গড় আর পরিসর দিন।
  3. এককটি নতুন রাখুন: নতুন গ্রাহকের জন্য group split (এলোমেলো ০.৭৬১ বনাম গ্রাহক ধরে ০.৫২২), ভবিষ্যতের জন্য time-based split আর TimeSeriesSplit.
  4. Leakage-এর পরিবার: ফল জানার পরের কলাম, ডুপ্লিকেট, ID বা ক্রম, ভবিষ্যতের তথ্য, ভাগের আগে preprocessing. প্রতিটি feature-এর জন্য জিজ্ঞেস করুন “কখন জানা যায়”.
  5. Pipeline আর একবারের test: label দেখে এমন প্রতিটি ধাপ cross-validation-এর ভেতরে (এলোমেলো ডেটায় ০.৯১ বনাম ০.৫১); test দেখে বাছাই করলে score ফোলে (০.৫৮৭ বনাম ০.৪৯৯)।

পরের পর্বে: এখন আমাদের কাছে সৎভাবে মাপার কাঠামো আছে। পর্ব ৫৭ (k-Nearest Neighbors — NumPy দিয়ে নিজে লেখা)-এ প্রথম model নিজের হাতে লিখব: দূরত্বের matrix, প্রতিবেশীদের ভোট, k বাছাই আর scaling কেন দূরত্ব বদলে দেয়, আর সবকিছু এই পর্বের নিয়মে ভাগ করা ডেটায় মাপা।

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

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