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

AI কোডে পরিচ্ছন্নতা — function, type hint, ছোট test আর seed

Clean Code for AI — Functions, Type Hints, Small Tests and Seeds

Taught in Bangla, this lesson shows why data code often fails silently and how to catch it: turning notebook cells into pure functions, type hints checked with mypy, assertions on shape and range, and small pytest and Hypothesis tests, with real tool output. It explains seeds with NumPy's default_rng and their limits, logging, a single config, Ruff linting and a review checklist for AI-written code.

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

  • ডেটা আর AI কোডে কোন ধরনের ভুল error ছাড়াই ঘটে, আর কেন “চলেছে মানেই ঠিক” একটি ফাঁদ
  • Notebook-এর এলোমেলো cell-কে ছোট, পরীক্ষাযোগ্য function-এ ভাঙা
  • NumPy আর pandas কোডে type hint লেখা, আর mypy চালিয়ে আসল error পড়া
  • Assertion আর ছোট pytest test দিয়ে shape, range, null আর unit-এর ভুল ধরা
  • Seed দিয়ে ফল পুনরুৎপাদন, logging, এক জায়গায় config, Ruff, আর AI কোড review-এর চেকলিস্ট

১ · “চলেছে, তাই ঠিক” — ডেটার কাজের সবচেয়ে বড় ফাঁদ

সাধারণ প্রোগ্রামে ভুল করলে প্রায়ই সঙ্গে সঙ্গে টের পাওয়া যায়। Login পাতা খোলে না, বোতাম কাজ করে না, প্রোগ্রাম লাল error দেখিয়ে থেমে যায়। ডেটার কাজে ব্যাপারটা আলাদা। ভুল কোডও দিব্যি চলে, একটি সুন্দর সংখ্যা বা চার্ট দেয় — শুধু সংখ্যাটা ভুল। একে বলে silent bug বা নীরব বাগ।

চায়ের দোকানের হিসাব খাতার কথা ভাবুন। একদিন কর্মচারী ভুল করে “বাকি” কলামের সংখ্যা “নগদ” কলামে যোগ করে ফেললেন। খাতা দেখতে একদম ঠিকঠাক — পরিষ্কার হাতের লেখা, যোগফলও মেলে। মাস শেষে ক্যাশবাক্সে টাকা কম পড়লে তখন বোঝা যায় কোথাও গোলমাল হয়েছে। ডেটার কোডের নীরব বাগও এমন: খাতা পরিষ্কার, হিসাব ভুল।

বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) ডেটায় এমন ভুল কোথায় কোথায় ঢুকতে পারে, তার কয়েকটি পরিচিত রূপ:

নীরব বাগবাজারঘরের উদাহরণকেন error আসে না
ভুল columnprice-এর বদলে discount দিয়ে গড় দামদুটোই সংখ্যা, গড় দুটোরই হয়
ভুল axisজেলার গড়ের বদলে দিনের গড়দুই দিকেই গড় নেওয়া বৈধ
Unit-এর গোলমালPayment API পয়সায় দেয়, কোড টাকা ধরে নেয় — ১০০ গুণ বড় বিক্রি১২৫০০০ আর ১২৫০ দুটোই বৈধ সংখ্যা
Label ফাঁস হওয়া“দেরি হবে কি না” আন্দাজের feature-এ “ডেলিভারির আসল সময়” ঢুকে গেছেModel চমৎকার স্কোর দেয়, বাস্তবে অচল
চুপচাপ বাদ পড়া সারিJoin-এর পর অর্ধেক অর্ডার নেইফল ছোট টেবিল, কিন্তু টেবিলই
অক্ষরের ভিন্ন রূপ“ঢাকা” আর “ঢাকা ” (শেষে space) আলাদা জেলাদুটোই বৈধ লেখা

চতুর্থ সারির সমস্যাটির নাম data leakage — উত্তরের তথ্য কোনো ফাঁক দিয়ে input-এ ঢুকে পড়া। পর্ব ০৫-এ (AI কোন সমস্যা সমাধান করে) এর পরিচয় পেয়েছেন, আর পর্ব ৫৬-এ train, validation, test ভাগ করার সময় এটি বিস্তারিত আসবে। এই পর্বের প্রশ্ন অন্য: কোন অভ্যাস থাকলে এমন ভুল আগেই ধরা পড়ে?

নিচের ছবিতে বাজারঘরের একটি সাধারণ কাজের ধারা। প্রতিটি ধাপে কোন নীরব বাগ ঢোকে, আর কোন অভ্যাস তাকে আটকায় — এই পর্বের বাকি অংশ এই ছবিরই বিস্তার।

ডেটার কাজের ধারা: কোথায় বাগ ঢোকে, কে পাহারা দেয়

Where silent bugs enter a data pipeline, and which habit guards each stage

ডেটার কাজের ধারায় নীরব বাগ আর পাহারা ১ · লোড (load) বাগ: পয়সাকে টাকা, ভুল column পাহারা: type hint, range assert ২ · পরিষ্কার (clean) বাগ: null চুপচাপ বাদ, space পাহারা: ছোট pytest test ৩ · Feature বাগ: ভুল axis, label ফাঁস পাহারা: shape assert ৪ · Split বাগ: প্রতিবার আলাদা ভাগ পাহারা: seed, random_state ৫ · Train ও মূল্যায়ন বাগ: কী চলেছে মনে নেই পাহারা: logging, এক config সব ধাপে: Ruff + code review তীর: ডেটা এক ধাপ থেকে পরের ধাপে যায়
ছবি ১১.১ — প্রতিটি ধাপের নিজস্ব নীরব বাগ আছে। কোনো একটি অভ্যাস সব ধরে না; কয়েকটি সস্তা পাহারা একসঙ্গে রাখলে বেশিরভাগ ভুল আগেই থামে।

এবার একটি নীরব বাগ নিজের চোখে দেখুন। বাজারঘরের ৪ দিনের বিক্রি ৩টি জেলায় (হাজার টাকায়) একটি টেবিলে রাখা — সারি মানে দিন, কলাম মানে জেলা, অর্থাৎ shape (4, 3). লক্ষ্য: প্রতিটি জেলার গড় বিক্রি। NumPy-তে axis=0 মানে “সারিগুলো জুড়ে নিচের দিকে”, ফলে প্রতি কলামে একটি গড়; axis=1 মানে প্রতি সারিতে একটি গড়। Axis-এর পুরো ব্যাখ্যা পর্ব ১৬-এ আসবে।

এই পাতার কোড আপনার ব্রাউজারেই চলে (Pyodide). প্রথমবার “চালান” চাপলে Python আর NumPy লোড হতে কয়েক সেকেন্ড লাগতে পারে; পরের বার অনেক দ্রুত।

import numpy as np

# rows = 4 days, columns = 3 districts (sales in thousand taka)
sales = np.array([[12, 30, 7],
                  [14, 28, 9],
                  [11, 35, 6],
                  [13, 31, 8]])
districts = ["Dhaka", "Ctg", "Rajshahi"]
print("sales.shape =", sales.shape)

def mean_per_district(s, axis):
    out = s.mean(axis=axis)
    assert out.shape == (s.shape[1],), f"expected ({s.shape[1]},), got {out.shape}"
    return out

wrong = sales.mean(axis=1)               # silent bug: averages each DAY
print("no guard, axis=1:", np.round(wrong, 2))   # 4 numbers, looks fine!

try:
    mean_per_district(sales, axis=1)
except AssertionError as e:
    print("guard caught it -> AssertionError:", e)

right = mean_per_district(sales, axis=0)
for d, m in zip(districts, right):
    print(f"{d:9s} mean = {m:.2f}")

আসল আউটপুট:

sales.shape = (4, 3)
no guard, axis=1: [16.33 17.   17.33 17.33]
guard caught it -> AssertionError: expected (3,), got (4,)
Dhaka     mean = 12.50
Ctg       mean = 31.00
Rajshahi  mean = 7.50

পাহারা ছাড়া axis=1 চারটি সুন্দর সংখ্যা দিল — ১৬.৩৩, ১৭, ১৭.৩৩, ১৭.৩৩। কোনো error নেই, আর সংখ্যাগুলো দেখতে বিশ্বাসযোগ্য। অথচ এগুলো দিনের গড়, জেলার নয়। জেলা তো ৩টি, সংখ্যা এলো ৪টি — এই একটি তথ্যই ভুল ধরিয়ে দেয়। assert লাইনটি ঠিক এই যাচাই করে: ফলের shape কি “জেলার সংখ্যা” (3,)? না হলে কাজ থামাতে হবে। ঠিক axis-এ ঢাকার গড় ১২.৫, চট্টগ্রামের ৩১, রাজশাহীর ৭.৫ — হাতে মিলিয়ে নিন: (১২ + ১৪ + ১১ + ১৩) ÷ ৪ = ৫০ ÷ ৪ = ১২.৫।

একই ঘটনা ধাপে ধাপে দেখুন। প্রতিটি ধাপে সবুজ লাইনটি চলছে, আর ডান দিকে মানগুলো বদলাচ্ছে।

1sales = np.array(rows)2m = sales.mean(axis=AXIS)3assert m.shape == (3,), m.shape4report(m)
মানগুলো (Watch)
AXIS
sales.shape
m.shape
m
  1. ডেটা এলো। ৪ দিন × ৩ জেলা, তাই sales.shape = (4, 3). AXIS ভুল করে ১ রাখা আছে — কোড লেখার সময় কেউ টের পায়নি।
  2. গড় নেওয়া হলো। axis=1-এ প্রতিটি সারির (দিনের) গড় এলো: চারটি সংখ্যা, shape (4,). কোনো error নেই — এটিই নীরব বাগ।
  3. পাহারাদার থামাল। assert দেখল shape (4,), অথচ জেলা ৩টি, তাই চাই (3,). শর্ত মিথ্যা, তাই AssertionError: (4,) — ভুল রিপোর্ট আর বেরোল না।
  4. Axis ঠিক করে আবার চালানো। AXIS = ০ দিলে প্রতিটি কলামের (জেলার) গড় আসে: [12.5 31. 7.5], shape (3,).
  5. শর্ত সত্য, রিপোর্ট বেরোল। Assert নীরবে পার হলো, লাইন ৪ চলল: ঢাকা ১২.৫, চট্টগ্রাম ৩১.০, রাজশাহী ৭.৫। ঠিক থাকলে assertion কিছুই বলে না — এটাই তার সৌন্দর্য।
মূল ধারণা

ডেটার কোডে “error আসেনি” কোনো প্রমাণ নয়। প্রমাণ হলো এমন যাচাই, যা ভুল হলে নিশ্চিতভাবে চিৎকার করবে — shape, row সংখ্যা, মানের সীমা, null-এর সংখ্যা, আর ছোট জানা ডেটায় জানা উত্তর।

২ · Notebook cell থেকে ছোট pure function

Jupyter notebook-এ (পর্ব ০৮-এ বিস্তারিত) কাজ শুরু করা খুব আরামের: একটি cell লিখুন, চালান, ফল দেখুন। সমস্যা শুরু হয় যখন cell-এর সংখ্যা পঞ্চাশ ছাড়ায়। কোন cell কোন ক্রমে চলেছিল, কোন variable কোথায় বদলেছে — মনে থাকে না। একে বলে hidden state: notebook-এর স্মৃতিতে এমন অবস্থা জমে থাকে, যা পাতায় লেখা কোড পড়ে বোঝা যায় না।

প্রতিকার হলো বারবার ব্যবহারের কাজগুলোকে ছোট pure function-এ তুলে আনা। Pure function দুটি শর্ত মানে: (১) একই input দিলে সবসময় একই output দেয়; (২) বাইরের কিছু বদলায় না — কোনো global variable, কোনো ফাইল, input DataFrame-টিও নয়। বাইরের কিছু বদলানোকে বলে side effect. Python-এ function-এর সাধারণ নিয়ম আর default argument-এর ফাঁদ Python কোর্সের function পর্বে আছে; এখানে শুধু ডেটার দিকটি দেখি।

নিচের stepper-এ একটি অতি পরিচিত notebook-বাগ: যে cell একটি column-কে “নিজের জায়গায়” বদলায়, তাকে ভুল করে দুবার চালালে কী হয়।

  1. Celldf["price"] /= 100জায়গায় বদল
  2. দুবার চালানো12.5error নেই
  3. Input/Outputorders → নতুন dfচিহ্নিত করা
  4. Functionadd_taka()নতুন কপি ফেরত
Notebook cell [7]প্রথমবার চালানো
df["price"] = df["price"] / 100
df["price"].iloc[0]
→ 1250.0   # ছিল 125000 পয়সা
একই cell [7]ভুল করে আবার চালানো
df["price"] = df["price"] / 100
df["price"].iloc[0]
→ 12.5   # আবার ১০০ দিয়ে ভাগ!
প্রশ্নকী ঢোকে, কী বেরোয়?
input : orders (price_paisa কলামসহ)
output: নতুন DataFrame, price_taka কলামসহ
বদলাবে না: orders নিজে
cleaning.pypure function
def add_taka(orders: pd.DataFrame) -> pd.DataFrame:
    out = orders.copy()
    out["price_taka"] = out["price_paisa"] / 100
    return out
দুবার ডাকাফল একই
add_taka(orders)["price_taka"].iloc[0]  → 1250.0
add_taka(orders)["price_taka"].iloc[0]  → 1250.0
orders["price_paisa"].iloc[0]           → 125000
  1. একটি সাধারণ cell. প্রথম অর্ডারের দাম ১২৫০০০ পয়সা। Cell সেটিকে ১০০ দিয়ে ভাগ করে একই column-এ রাখল: ১২৫০ টাকা। ঠিক আছে — আপাতত।
  2. দ্বিতীয়বার চালানো। কেউ ওপরের cell-এ কিছু বদলে “Run all below” চাপলেন, অথবা ভুল করে এই cell আবার চালালেন। দাম হয়ে গেল ১২.৫ টাকা। কোনো error নেই — notebook-এর স্মৃতিতে পুরোনো df বদলে গেছে।
  3. Input আর output চিহ্নিত করা। কাজটি আসলে কী নেয় আর কী দেয়? নেয় পয়সার কলামসহ অর্ডার, দেয় টাকার কলামসহ নতুন টেবিল। মূল টেবিলটি ছোঁয়া চলবে না।
  4. Function লেখা। copy() করে নতুন টেবিলে কাজ, নতুন নামের column (price_taka), আর type hint দিয়ে স্পষ্ট লেখা — কী ঢোকে, কী বেরোয়।
  5. যতবার খুশি ডাকুন। দুবার ডাকলেও ফল ১২৫০.০, আর মূল টেবিলে এখনো ১২৫০০০ পয়সা। যে কাজ বারবার করলেও একই ফল দেয়, তাকে বলে idempotent. এখন এর জন্য test লেখাও সহজ।

কোন কাজকে function বানাবেন? একটি সহজ নিয়ম: যে কোড দুবার copy-paste করেছেন, যে কোডের ফল অন্য কোড ব্যবহার করে, আর যে কোড ভুল হলে টাকার হিসাব ভুল হয় — এগুলো function হবে। Notebook-এ থাকুক শুধু ডাকা আর দেখা: clean = clean_orders(raw), তারপর একটি চার্ট।

  • এক function, এক কাজ: “লোড করো, পরিষ্কার করো, model চালাও, চার্ট আঁকো” একটি function-এ নয়।
  • ফাইল path, column-এর নাম, হার — argument হিসেবে: function-এর ভেতরে লুকানো না রেখে।
  • ছবি আঁকা আর print আলাদা: হিসাবের function সংখ্যা বা টেবিল ফেরত দেবে, দেখানোর কাজ ডাকার জায়গায়।
  • Randomness থাকলে seed বা generator argument হিসেবে — অংশ ৬-এ দেখবেন।

বিতর্ক আছে: কেউ কেউ বলেন notebook শুধু অনুসন্ধানের জন্য, আসল কাজ .py ফাইলে; কেউ কেউ notebook-কেই পূর্ণ কাজের জায়গা মনে করেন, বিশেষত ব্যাখ্যা আর চার্ট একসঙ্গে দেখাতে। ব্যবহারিক মাঝামাঝি পথ: function থাকুক .py ফাইলে, notebook সেগুলো import করে ব্যবহার করুক। তাতে test আর review দুটোই সহজ হয়। প্রকল্পের folder সাজানো পর্ব ১২-এ আসবে।

৩ · Type hint — ডেটার কোডে কী লিখবেন, mypy কী ধরে

Type hint হলো কোডে লেখা একটি ঘোষণা: “এই argument একটি int, এই function একটি DataFrame ফেরত দেয়।” Python নিজে চালানোর সময় এগুলো যাচাই করে না — এগুলো মানুষের জন্য আর static type checker-এর জন্য, যেমন mypy বা pyright. এরা কোড না চালিয়েই পড়ে দেখে, ঘোষণার সঙ্গে ব্যবহার মিলছে কি না। Type hint-এর পূর্ণ ব্যাকরণ (list[int], Optional, generic) Python কোর্সের type hint পর্বে আছে।

ডেটার কোডে কয়েকটি কাজের রূপ:

কীType hintমন্তব্য
NumPy arrayNDArray[np.float64]from numpy.typing import NDArray; dtype বলে, shape বলে না
pandas টেবিলpd.DataFrame, pd.Seriesভালো যাচাইয়ের জন্য pandas-stubs install করতে হয়
হয়তো নেই এমন মানstr | Nonemypy মনে করিয়ে দেয় None যাচাই করতে
Unit আলাদা রাখাPaisa = NewType("Paisa", int)শুধু checker-এর জন্য; চলার সময় সাধারণ int
Config@dataclass-এর fieldঅংশ ৭-এ

এবার সত্যিকারের run. একটি ছোট ফাইলে ইচ্ছে করে চারটি ভুল রাখা হলো, আর একটি জিনিস রাখা হলো যা ভুল হলেও mypy ধরতে পারবে না।

import numpy as np
import pandas as pd
from numpy.typing import NDArray


def paisa_to_taka(paisa: int) -> int:
    return paisa / 100


def district_mean(prices: NDArray[np.float64]) -> NDArray[np.float64]:
    return prices.mean(axis=0)


def find_district(code: str, table: dict[str, str]) -> str | None:
    return table.get(code)


def clean_orders(df: pd.DataFrame) -> pd.DataFrame:
    return df.dropna(subset=["price"])


orders = pd.DataFrame({"price": [120.0, None, 80.0]})
name = find_district("DHK", {"DHK": "Dhaka"})
print(name.upper())
print(clean_orders(orders["price"]))
print(paisa_to_taka("12500"))

সত্যিকারের run (লেখার সময়ে Python 3.13.5, mypy 2.4.1, numpy 2.5.4, pandas 3.1.0, pandas-stubs সহ):

~/bazarghar-ai$ mypy --version
mypy 2.4.1 (compiled: yes)
~/bazarghar-ai$ mypy typed_bugs.py
typed_bugs.py:7: error: Incompatible return value type (got "float", expected "int")  [return-value]
typed_bugs.py:24: error: Item "None" of "str | None" has no attribute "upper"  [union-attr]
typed_bugs.py:25: error: Argument 1 to "clean_orders" has incompatible type "Series[Any]"; expected "DataFrame"  [arg-type]
typed_bugs.py:26: error: Argument 1 to "paisa_to_taka" has incompatible type "str"; expected "int"  [arg-type]
Found 4 errors in 1 file (checked 1 source file)

চারটি error একে একে পড়ি। লাইন ৭: / সবসময় float দেয়, অথচ ঘোষণা int — mypy ধরিয়ে দিল যে টাকায় পয়সার ভগ্নাংশ থাকতে পারে, ঘোষণাটাই ভুল। লাইন ২৪: জেলা না পেলে None আসবে, তখন .upper() চালালে প্রোগ্রাম ভেঙে পড়বে; mypy আগেই বলছে None যাচাই করুন। লাইন ২৫: orders["price"] একটি Series, পুরো DataFrame নয় — ভুল column বা ভুল বস্তু পাঠানোর ক্লাসিক ভুল। লাইন ২৬: CSV থেকে আসা "12500" লেখা, সংখ্যা নয়।

আর যা ধরা পড়েনি: district_mean-এ axis=0 ঠিক না ভুল, mypy জানে না, কারণ NDArray[np.float64] শুধু dtype বলে, shape বলে না। Type hint shape-এর ভুল ধরে না — সেজন্য assertion আর test লাগবে।

দুটি ব্যবহারিক কথা। এক, সব library type তথ্য দেয় না। একই প্রকল্পে scikit-learn import করা ফাইলে mypy বলেছিল: Skipping analyzing "sklearn.linear_model": module is installed, but missing library stubs or py.typed marker. সমাধান হিসেবে pyproject.toml-এ ওই module-এর জন্য ignore_missing_imports = true লেখা হয়েছে, তারপর Success: no issues found in 2 source files এসেছে (সত্যিকারের run). এর মানে sklearn-এর ব্যবহার mypy যাচাই করছে না — এটুকু মেনে নিয়েই কাজ। দুই, pyright (VS Code-এর Pylance-এর ভেতরেও এটি) একই ধরনের কাজ করে, কিছু নিয়মে আলাদা আচরণ করে; এখানে চালানো হয়নি।

Static checker কোড চালায় না। কিন্তু ডেটা আসে চলার সময় — CSV, API, ব্যবহারকারী। তাই সীমান্তে একটি runtime যাচাইও দরকার। নিচের কোডে একটি payment API পয়সায় টাকা পাঠায়। NewType দিয়ে “Paisa” নামের একটি আলাদা type ঘোষণা করা হলো, যাতে mypy ভুল মিশ্রণ ধরতে পারে; আর একটি সীমা-যাচাই চলার সময় অবাস্তব গড় থামায়।

from typing import NewType

Paisa = NewType("Paisa", int)        # a label for type checkers only

def paisa_to_taka(p: Paisa) -> float:
    return p / 100

def average_order_taka(amounts_taka: list[float]) -> float:
    avg = sum(amounts_taka) / len(amounts_taka)
    if not 10 <= avg <= 50_000:       # runtime guard: plausible taka range
        raise ValueError(f"average {avg:,.0f} taka is not plausible; "
                         "paisa passed as taka?")
    return avg

shop = [1250.0, 980.0, 1475.5]                          # already taka
gateway = [Paisa(125000), Paisa(98000), Paisa(147550)]  # payment API: paisa

p = Paisa(125000)
print("Paisa(125000) at runtime:", p, type(p).__name__)
print("shop average:", round(average_order_taka(shop), 2))
taka = [paisa_to_taka(x) for x in gateway]
print("converted   :", round(average_order_taka(taka), 2))
try:
    average_order_taka(gateway)      # mypy would flag this; Python does not
except ValueError as e:
    print("runtime guard ->", e)

আসল আউটপুট:

Paisa(125000) at runtime: 125000 int
shop average: 1235.17
converted   : 1235.17
runtime guard -> average 123,517 taka is not plausible; paisa passed as taka?

প্রথম লাইনটি গুরুত্বপূর্ণ: চলার সময় Paisa(125000) একটি সাধারণ int. NewType শুধু checker-এর জন্য একটি নামফলক, Python নিজে কিছু থামায় না। তাই পয়সার তালিকা সরাসরি পাঠালে Python খুশিমনে গড় বের করল — ১,২৩,৫১৭ “টাকা”, আসলের ১০০ গুণ। সেটি ধরল দ্বিতীয় পাহারা: গড় ১০ থেকে ৫০,০০০ টাকার বাইরে গেলে কাজ থামবে। হাতে মিলিয়ে নিন: (১২৫০ + ৯৮০ + ১৪৭৫.৫) ÷ ৩ = ৩৭০৫.৫ ÷ ৩ ≈ ১২৩৫.১৭। সীমাটি ব্যবসা জেনে ঠিক করতে হয়; ভুল সীমা দিলে সঠিক ডেটাও আটকাবে, তাই সীমা নিয়মিত হালনাগাদ করাও কাজের অংশ।

৪ · Assertion — সস্তা কিন্তু সজাগ পাহারাদার

Assertion হলো কোডের মাঝখানে লেখা একটি দাবি: “এখানে এই কথা অবশ্যই সত্য।” Python-এ assert শর্ত, "বার্তা" লিখলে শর্ত মিথ্যা হলে সঙ্গে সঙ্গে AssertionError ওঠে। ডাক্তার যেমন অপারেশনের আগে রোগীর নাম আর কোন পাশে অপারেশন — দুটো আবার মিলিয়ে নেন, assertion তেমনই একটি ছোট, সস্তা মিলিয়ে নেওয়া।

ডেটার কোডে কোন দাবিগুলো সবচেয়ে কাজের:

📐 Shape আর সারির সংখ্যা

assert X.shape[1] == 2; join-এর পর assert len(merged) == len(orders) — সারি চুপচাপ হারালে বা দ্বিগুণ হলে ধরা পড়ে।

shape

📏 মানের সীমা

assert (df["price_taka"] > 0).all(); সম্ভাবনা হলে ০ থেকে ১-এর মধ্যে — unit-এর ভুল প্রায়ই সীমা ভাঙে।

range

🕳️ Null আর ডুপ্লিকেট

assert not df["district"].isna().any(); assert df["order_id"].is_unique.

null

🚧 Label-এর নিরাপত্তা

assert "delivered_at" not in features — আন্দাজের সময় যা জানা থাকবে না, তা feature-এ থাকতে পারবে না।

leakage

দুটি সীমা মনে রাখুন। প্রথমত, Python-কে -O (optimize) দিয়ে চালালে সব assert বাদ পড়ে যায়। তাই ব্যবহারকারীর input বা বাইরের ডেটার যাচাই — যেটি সবসময় চলতেই হবে — সেখানে if ...: raise ValueError(...) লিখুন, যেমন ওপরের টাকা-পয়সার কোডে। Assertion হলো প্রোগ্রামারের নিজের ভুল ধরার জন্য। দ্বিতীয়ত, assert (x > 0, "msg") — বন্ধনী দিলে এটি একটি tuple, যা সবসময় সত্য; ফলে assertion কখনো ব্যর্থ হয় না। Python এ ক্ষেত্রে একটি SyntaxWarning দেয়, আর linter-ও ধরে।

বড় প্রকল্পে টেবিলের প্রতিটি column-এর নিয়ম (type, সীমা, null চলবে কি না) একটি “schema” হিসেবে লিখে রাখার জন্য তৃতীয় পক্ষের library-ও আছে, যেমন pandera. শুরুতে কয়েকটি সাধারণ assert-ই যথেষ্ট; প্রয়োজন বাড়লে তখন ভাবুন।

৫ · ছোট test — জানা ডেটায় জানা উত্তর

Assertion চলে আসল ডেটার সঙ্গে, প্রতিবার। Unit test চলে আলাদা করে, একটি ছোট, হাতে বানানো ডেটায়, যার সঠিক উত্তর আপনি আগে থেকেই জানেন। পরীক্ষার খাতা দেখার আগে শিক্ষক যেমন একটি “উত্তরমালা” বানিয়ে রাখেন, test তেমন উত্তরমালা। Testing-এর তত্ত্ব — কী test করবেন, কতটুকু — Software Engineering কোর্সের unit testing পর্বে আছে, আর pytest-এর পূর্ণ ব্যবহার Python কোর্সের pytest পর্বে।

Test runner আসলে কী করে, তা বুঝতে প্রথমে নিজের হাতে একটি খুদে runner লিখি। এটি pytest নয়, তার একটি শেখার মডেল: প্রতিটি test function চালানো, AssertionError বা অন্য error এলে FAIL লেখা, না এলে PASS. পরীক্ষার বিষয় একটি দাম-পরিষ্কারের function — “৳1,250” বা “১২৫০ টাকা” থেকে সংখ্যা বের করা, আর ফাঁকা হলে None.

def clean_price(text):
    """'৳1,250' -> 1250.0 ; empty text -> None"""
    t = text.replace("৳", "").replace(",", "").replace("টাকা", "").strip()
    return float(t)

def test_symbol_and_comma():
    assert clean_price("৳1,250") == 1250.0

def test_bangla_digits():
    assert clean_price("১২৫০ টাকা") == 1250.0

def test_empty_is_none():
    assert clean_price("  ") is None

def run_tests(tests):
    passed = 0
    for t in tests:
        try:
            t()
            print("PASS", t.__name__)
            passed += 1
        except AssertionError:
            print("FAIL", t.__name__, "-> assertion was false")
        except Exception as e:
            print("FAIL", t.__name__, "->", type(e).__name__ + ":", e)
    print(f"{passed} passed, {len(tests) - passed} failed")

run_tests([test_symbol_and_comma, test_bangla_digits, test_empty_is_none])

আসল আউটপুট:

PASS test_symbol_and_comma
PASS test_bangla_digits
FAIL test_empty_is_none -> ValueError: could not convert string to float: ''
2 passed, 1 failed

দ্বিতীয় test-টি অনেককে অবাক করে: Python-এর float() আর int() বাংলা অঙ্কও (১২৫০) পড়তে পারে, কারণ এগুলো Unicode-এর যেকোনো দশমিক অঙ্ক গ্রহণ করে। তৃতীয় test ধরিয়ে দিল আসল বাগ: ফাঁকা লেখায় function None দেওয়ার কথা, কিন্তু সে ValueError দিয়ে ভেঙে পড়ছে। সমাধান: strip()-এর পর if not t: return None. লক্ষ করুন, এই বাগ আসল ডেটায় হয়তো হাজারে একবার আসত — আর তখন রাত দুটোয় পুরো কাজ থেমে যেত।

আসল pytest — একটি ছোট নির্দিষ্ট dataset আর একটি property

এবার আসল pytest. বাজারঘরের clean_orders function তিনটি কাজ করে: দাম না থাকা সারি বাদ দেয়, পয়সাকে টাকায় নেয়, জেলার নামের space ছাঁটে। Test-এ একটি তিন সারির খুদে টেবিল আছে, যেটি একটি fixture হিসেবে লেখা — pytest প্রতিটি test-কে এই টেবিলের নতুন কপি দেয়। শেষ test-টি অন্য ধরনের: নির্দিষ্ট উদাহরণ নয়, একটি সাধারণ নিয়ম — “পরিষ্কারের পর মোট টাকা × ১০০ = মোট পয়সা”, আর Hypothesis নামের library শত শত এলোমেলো তালিকা বানিয়ে নিয়মটি যাচাই করে। একে বলে property-based testing.

import pandas as pd
import pytest
from hypothesis import given
from hypothesis import strategies as st

from cleaning import clean_orders


@pytest.fixture
def tiny_orders() -> pd.DataFrame:
    return pd.DataFrame({
        "district": [" dhaka", "Sylhet ", "khulna"],
        "price_paisa": [12550, None, 8000],
    })


def test_drops_missing_price(tiny_orders):
    out = clean_orders(tiny_orders)
    assert len(out) == 2


def test_paisa_becomes_taka(tiny_orders):
    out = clean_orders(tiny_orders)
    assert out["price_taka"].tolist() == [125.5, 80.0]


def test_district_names_cleaned(tiny_orders):
    out = clean_orders(tiny_orders)
    assert out["district"].tolist() == ["Dhaka", "Khulna"]


@given(st.lists(st.integers(min_value=0, max_value=10_000_000), min_size=1))
def test_total_is_preserved(paisa):
    df = pd.DataFrame({"district": ["x"] * len(paisa), "price_paisa": paisa})
    out = clean_orders(df)
    assert out["price_taka"].sum() * 100 == pytest.approx(sum(paisa))

clean_orders-এর প্রথম সংস্করণে টাকায় নেওয়ার লাইন ছিল out["price_paisa"] // 100 — // মানে ভাগ করে নিচের পূর্ণসংখ্যা। সত্যিকারের run (pytest 9.2.0, Hypothesis 6.170.14):

~/bazarghar-ai$ pytest -q
.F.F                                                                     [100%]
================================== FAILURES ===================================
___________________________ test_paisa_becomes_taka ___________________________
…
>       assert out["price_taka"].tolist() == [125.5, 80.0]
E       assert [125.0, 80.0] == [125.5, 80.0]
E
E         At index 0 diff: 125.0 != 125.5
…
___________________________ test_total_is_preserved ___________________________
…
paisa = [1]
…
>       assert out["price_taka"].sum() * 100 == pytest.approx(sum(paisa))
E       assert np.int64(0) == 1 ± 1.0e-06
…
E       Failing test case: test_total_is_preserved(
E           paisa=[1],
E       )
…
=========================== short test summary info ===========================
FAILED test_cleaning.py::test_paisa_becomes_taka - assert [125.0, 80.0] == [1...
FAILED test_cleaning.py::test_total_is_preserved - assert np.int64(0) == 1 ± ...
2 failed, 2 passed in 2.07s

দুটি test একই বাগ দুই দিক থেকে ধরল। নির্দিষ্ট-ডেটার test বলছে ১২৫৫০ পয়সা হওয়ার কথা ১২৫.৫ টাকা, এসেছে ১২৫.০ — ৫০ পয়সা হাওয়া। Property test আরও মজার কাজ করেছে: এলোমেলো অনেক তালিকায় নিয়ম ভাঙার পর Hypothesis উদাহরণটিকে ছোট করতে করতে (একে বলে shrinking) সবচেয়ে সরল ব্যর্থ উদাহরণ খুঁজে দিয়েছে — paisa=[1]: ১ পয়সা ঢুকলে বেরোয় ০ টাকা। লাইনটি / 100 করার পর আবার চালানো হলো:

~/bazarghar-ai$ pytest -q
....                                                                     [100%]
4 passed in 0.58s

আরেকটি লক্ষণীয় বিষয়: fixture-এ None থাকায় pandas পুরো কলামকে float করে দিয়েছে (12550.0) — ডেটার type চুপচাপ বদলানোর আরেকটি পরিচিত উদাহরণ।

ডেটার function-এর জন্য কোন test লিখবেন? কয়েকটি ধরন প্রায় সবসময় কাজে লাগে: (১) তিন-চার সারির হাতে বানানো টেবিলে হাতে হিসাব করা উত্তর; (২) কিনারার ঘটনা — ফাঁকা টেবিল, সব null, একটিমাত্র সারি, বাংলা ও ইংরেজি মেশানো নাম; (৩) property — মোট টাকা ঠিক থাকে, সারি কখনো বাড়ে না, ফল সবসময় ০ থেকে ১-এর মধ্যে; (৪) আগে ধরা পড়া প্রতিটি বাগের জন্য একটি test, যাতে সে আর ফিরে না আসে।

৬ · Randomness আর reproducibility — seed-এর কাজ ও সীমা

AI-এর কাজে randomness সবখানে: ডেটা এলোমেলো করে train আর test ভাগ, model-এর শুরুর ওজন, নমুনা বাছাই। কম্পিউটারের “random” সংখ্যা আসলে একটি হিসাবের সূত্র থেকে আসে — একে বলে pseudo-random number generator (PRNG). সূত্রটি একটি শুরুর সংখ্যা থেকে চলে; সেই সংখ্যাই seed. একই seed দিলে একই ধারা আসে। এর ফলে আপনার ফল অন্য কেউ (বা ছয় মাস পরের আপনি) হুবহু আবার পেতে পারেন — একে বলে reproducibility.

লুডুর ছক্কাকে যদি এমন একটি যন্ত্র বানানো যেত, যেটি একটি লম্বা আগে-ছাপা তালিকা থেকে সংখ্যা পড়ে — তাহলে seed হলো “তালিকার কোন পাতা থেকে শুরু”। একই পাতা থেকে শুরু করলে দুজন খেলোয়াড় একই চাল পাবেন। Seed না দিলে যন্ত্র নিজে একটি পাতা বেছে নেয় (সাধারণত অপারেটিং সিস্টেমের এলোমেলো তথ্য থেকে), আর প্রতিবার খেলা আলাদা হয়।

নিচের কোডে চারটি জিনিস একসঙ্গে: একই seed-এ দুই run, seed ছাড়া দুই run, একই generator দুবার ডাকা, আর Python-এর random ও NumPy-র পুরোনো global পদ্ধতি।

import random
import numpy as np

def sample(seed=None):
    rng = np.random.default_rng(seed)
    return rng.integers(100, 1000, size=5)   # 5 order ids

print("seed 42, run 1:", sample(42))
print("seed 42, run 2:", sample(42))
a, b = sample(), sample()
print("no seed, two runs equal?", np.array_equal(a, b))

rng = np.random.default_rng(7)
print("same rng, 1st call:", rng.integers(1, 7, size=4))
print("same rng, 2nd call:", rng.integers(1, 7, size=4))

random.seed(42)
print("random.seed(42):", [random.randint(1, 6) for _ in range(4)])
np.random.seed(42)                       # old global way
print("legacy np.random.seed(42):", np.random.randint(100, 1000, size=5))

আসল আউটপুট:

seed 42, run 1: [180 796 689 494 489]
seed 42, run 2: [180 796 689 494 489]
no seed, two runs equal? False
same rng, 1st call: [6 4 5 6]
same rng, 2nd call: [4 5 6 2]
random.seed(42): [6, 1, 1, 6]
legacy np.random.seed(42): [202 535 960 370 206]

Seed ৪২ দিয়ে দুই run হুবহু এক: ১৮০, ৭৯৬, ৬৮৯, ৪৯৪, ৪৮৯। Seed ছাড়া দুই run আলাদা (False) — তাই seed-ছাড়া সংখ্যাগুলো এখানে ছাপা হয়নি, আপনার স্ক্রিনে প্রতিবার অন্যরকম আসত। একই generator দুবার ডাকলে দ্বিতীয়বার পরের সংখ্যাগুলো আসে — seed মানে “একই ধারা”, “একই সংখ্যা বারবার” নয়। আর লক্ষ করুন, পুরোনো np.random.seed(42) আর নতুন default_rng(42) একই seed নিয়েও সম্পূর্ণ আলাদা সংখ্যা দেয়, কারণ ভেতরের অ্যালগরিদম আলাদা (পুরোনোটি Mersenne Twister, নতুনটি PCG64).

তিনটি আলাদা run · rng.integers(100, 1000, size=5)
A · seed 42
B · seed 42
C · seed নেই
  1. Generator তৈরি। A আর B দুটোই default_rng(42) — একই শুরুর অবস্থা। C-তে seed নেই, তাই NumPy অপারেটিং সিস্টেম থেকে এলোমেলো শুরু নেয়।
  2. প্রথম সংখ্যা। A আর B দুটোতেই ১৮০. C-র সংখ্যা আগে থেকে জানার উপায় নেই।
  3. পাঁচটি সংখ্যা। A আর B: ১৮০, ৭৯৬, ৬৮৯, ৪৯৪, ৪৮৯ — হুবহু এক। C অন্য পাঁচটি দিয়েছে।
  4. একই generator আবার ডাকা। A আর B দুটোই এবার পরের পাঁচটি দিল: ৮৭২, ১৭৭, ৭২৭, ২৮১, ১৮৪। ধারা এগিয়ে চলে, কিন্তু দুই run-এ একসঙ্গে।
  5. তুলনা। A = B, তাই কেউ আপনার ফল হুবহু আবার পেতে পারেন। C কারও সঙ্গে মিলবে না। শর্ত: একই NumPy version আর একই ক্রমে ডাকা — পরের অনুচ্ছেদে দেখুন।

কোনটি ব্যবহার করবেন

  • NumPy: নতুন কোডে rng = np.random.default_rng(seed) বানিয়ে সেই rng function-এ argument হিসেবে পাঠান। পুরোনো np.random.seed() পুরো প্রোগ্রামের একটি global অবস্থা বদলায় — কোনো library-র ভেতরের একটি ডাক সেই ধারা থেকে সংখ্যা খেয়ে ফেললে আপনার পরের সংখ্যাগুলো বদলে যায়, অথচ কোডে কিছু বদলায়নি। NumPy-র docs-ও নতুন কোডে Generator ব্যবহারের পরামর্শ দেয়। Ruff-এর NPY নিয়ম এটি ধরে (সত্যিকারের run): NPY002 Replace legacy `np.random.seed` call with `np.random.Generator`.
  • Python-এর random: random.seed(42) চলে, তবে আলাদা উদ্দেশ্যে আলাদা random.Random(42) object রাখা আরও পরিষ্কার।
  • scikit-learn: যেখানে randomness আছে (train_test_split, অনেক model), সেখানে random_state=42 দিন। একটি int দিলে প্রতিবার একই ফল; None দিলে প্রতিবার আলাদা।

একই seed, তবু আলাদা ফল — কখন? (১) Library-র version বদলালে। NumPy-র নীতি অনুযায়ী পুরোনো RandomState-এর ধারা স্থির রাখা হয়েছে, কিন্তু নতুন Generator-এর ধারা version-ভেদে বদলাতে পারে বলে docs সতর্ক করে; scikit-learn-ও version বদলালে একই random_state-এ ফল বদলাতে পারে। Python-এর random-এর docs শুধু random()-এর ধারা স্থির রাখার কথা বলে, অন্য method-এর নয়। (২) ডাকার ক্রম বদলালে — মাঝে একটি নতুন random ডাক ঢুকলেই পরের সব সংখ্যা সরে যায়। (৩) Parallel কাজ আর GPU — thread-এর ক্রম আর কিছু GPU অপারেশন প্রতিবার একরকম না-ও হতে পারে। (৪) Floating-point যোগের ক্রম বদলালে শেষ দশমিকে পার্থক্য। তাই seed-এর সঙ্গে version-ও লিখে রাখুন — lockfile পর্ব ১২-এ আসবে। NumPy-র random নিয়ে আরও কাজ পর্ব ১৬-এ।

আরেকটি সতর্কতা: seed দিয়ে একটি ভালো ফল “বেছে নেওয়া” প্রতারণার কাছাকাছি। ৪২ দিয়ে accuracy ০.৮৮, ৭ দিয়ে ০.৮১ — তাহলে ৪২-ই লিখব? না। সৎ পথ হলো কয়েকটি seed-এ চালিয়ে গড় আর ছড়ানো (spread) দুটোই জানানো। Seed ফল পুনরুৎপাদনের জন্য, ফল সাজানোর জন্য নয়।

৭ · লম্বা কাজে logging, আর সব সেটিং এক জায়গায়

পাঁচ মিনিটের কাজে print চলে। কিন্তু model training যখন রাতভর চলে, তখন সকালে প্রশ্ন আসে: কখন শুরু হয়েছিল? কোন সেটিং-এ? কোথায় সতর্কবার্তা এসেছিল? Logging প্রতিটি বার্তার সঙ্গে সময়, গুরুত্বের মাত্রা (DEBUG, INFO, WARNING, ERROR) আর উৎসের নাম রাখে, আর এক লাইনে ঠিক করা যায় কোন মাত্রা দেখাবে বা ফাইলে লিখবে। Python-এর logging module-এর বিস্তারিত Python কোর্সের debugging ও logging পর্বে আছে।

দ্বিতীয় অভ্যাস: configuration এক জায়গায়। Seed, ডেটার আকার, test-এর ভাগ, threshold — কোডের বিশ জায়গায় ছড়ানো “জাদুর সংখ্যা” থাকলে একটি বদলাতে গিয়ে আরেকটি ভুলে যান। একটি @dataclass (dataclass পর্ব) সব সেটিং এক জায়গায় রাখে, আর frozen=True দিলে মাঝপথে কেউ বদলাতে পারে না। সবচেয়ে বড় সুবিধা: log-এর প্রথম লাইনেই পুরো config ছাপা হয়, ফলে প্রতিটি ফলের পাশে তার সেটিং লেখা থাকে।

"""Toy late-delivery model for Bazarghar: config in one place, logging, seeds."""

import logging
from dataclasses import dataclass

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

log = logging.getLogger("bazarghar.train")


@dataclass(frozen=True)
class Config:
    seed: int = 42
    n_orders: int = 2000
    test_size: float = 0.25
    late_score: float = 5.0


def make_orders(cfg: Config) -> tuple[np.ndarray, np.ndarray]:
    rng = np.random.default_rng(cfg.seed)
    dist_km = rng.uniform(1, 20, cfg.n_orders)
    rain_mm = rng.uniform(0, 60, cfg.n_orders)
    score = 0.3 * dist_km + 0.08 * rain_mm + rng.normal(0, 1, cfg.n_orders)
    X = np.column_stack([dist_km, rain_mm])
    y = (score > cfg.late_score).astype(int)
    assert X.shape == (cfg.n_orders, 2), X.shape
    assert not np.isnan(X).any(), "NaN in features"
    return X, y


def main(cfg: Config) -> None:
    log.info("config %s", cfg)
    X, y = make_orders(cfg)
    log.info("orders=%d late_rate=%.3f", len(y), y.mean())
    X_tr, X_te, y_tr, y_te = train_test_split(
        X, y, test_size=cfg.test_size, random_state=cfg.seed, stratify=y
    )
    model = LogisticRegression().fit(X_tr, y_tr)
    log.info("test accuracy=%.3f", model.score(X_te, y_te))
    if model.score(X_te, y_te) < 0.7:
        log.warning("accuracy below 0.7, check the data")


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
    )
    main(Config())

সত্যিকারের run, পরপর দুবার (scikit-learn 1.9.1, numpy 2.5.4, Python 3.13.5):

~/bazarghar-ai$ python train.py
2026-11-04 19:20:15,726 INFO    bazarghar.train: config Config(seed=42, n_orders=2000, test_size=0.25, late_score=5.0)
2026-11-04 19:20:15,726 INFO    bazarghar.train: orders=2000 late_rate=0.580
2026-11-04 19:20:15,741 INFO    bazarghar.train: test accuracy=0.882
~/bazarghar-ai$ python train.py
2026-11-04 19:20:16,744 INFO    bazarghar.train: config Config(seed=42, n_orders=2000, test_size=0.25, late_score=5.0)
2026-11-04 19:20:16,744 INFO    bazarghar.train: orders=2000 late_rate=0.580
2026-11-04 19:20:16,753 INFO    bazarghar.train: test accuracy=0.882

দুই run-এ সময় আলাদা, কিন্তু সংখ্যা হুবহু এক: দেরির হার ০.৫৮০, test accuracy ০.৮৮২ — কারণ ডেটা তৈরি আর split দুটোই একই seed থেকে আসে। এটি একটি local run-এর ফল, কোনো সাধারণ মাপ নয়। Log দেখে ছয় মাস পরেও বলা যাবে কোন সেটিং-এ এই ফল এসেছিল। কাজের ভেতরের function-গুলো শুধু log.info(...) ডাকে; কোথায় ও কীভাবে দেখাবে, তা ঠিক হয় একবারই, basicConfig-এ। আরেকটি খুঁটিনাটি: log.info("... %s", cfg) — f-string নয়, কারণ logging দরকার হলে তবেই লেখাটি বানায়।

Config-এর মান কোথা থেকে আসবে — কোডে default, একটি config.toml ফাইল, নাকি command line — প্রকল্পভেদে আলাদা। নিয়ম একটাই: একটি উৎস, একবার পড়া, log-এ ছাপা। আর গোপন তথ্য (যেমন API key) কখনো config ফাইলে বা কোডে নয়; পরিবেশ বা আলাদা গোপন ভাণ্ডার থেকে, আর উদাহরণে শুধু example-secret এর মতো স্পষ্ট নকল মান।

৮ · Ruff — lint আর format এক টুলে

Linter কোড চালায় না, পড়ে — আর সন্দেহজনক ধাঁচ খোঁজে: ব্যবহার না হওয়া import, == None, সব error গিলে ফেলা except:, mutable default argument. Formatter কোডের চেহারা (ফাঁকা লাইন, space, লাইনের দৈর্ঘ্য) একটি নির্দিষ্ট নিয়মে সাজায়, যাতে review-এ চেহারা নিয়ে তর্ক না হয়। Ruff (Astral নামের প্রতিষ্ঠানের, Rust-এ লেখা, ২০২২ সাল থেকে) দুটোই করে, আর খুব দ্রুত। Flake8, isort, Black-এর মতো পুরোনো টুলের অনেক নিয়ম এতে আছে; আবার অনেক দল এখনো সেই পুরোনো টুলগুলোই ব্যবহার করে — দুটোই চলে।

নিচের ফাইলটি notebook থেকে তাড়াহুড়োয় তুলে আনা কোডের মতো:

import os
import numpy as np
import pandas as pd

def load(path, cols=[]):
    df = pd.read_csv(path)
    for c in cols:
        if df[c].isna().sum() == None:
            print(f"no missing")
    try:
        df["price"] = df["price"].astype(float)
    except:
        pass
    return df

def summarise(df):
    l = df["price"].mean()
    return l

pyproject.toml-এ নিয়মের দল বেছে দেওয়া হয়েছে: select = ["E", "F", "B", "I", "NPY", "PD"] — pycodestyle, Pyflakes, bugbear, import সাজানো, NumPy আর pandas-এর নিয়ম। সত্যিকারের run (ruff 0.17.3):

~/bazarghar-ai$ ruff check --output-format concise messy.py
messy.py:1:1: I001 [*] Import block is un-sorted or un-formatted
messy.py:1:8: F401 [*] `os` imported but unused
messy.py:2:17: F401 [*] `numpy` imported but unused
messy.py:5:21: B006 Do not use mutable data structures for argument defaults
messy.py:8:34: E711 Comparison to `None` should be `cond is None`
messy.py:9:19: F541 [*] f-string without any placeholders
messy.py:12:5: E722 Do not use bare `except`
messy.py:17:5: E741 Ambiguous variable name: `l`
Found 8 errors.
[*] 4 fixable with `--fix` (2 unsafe fixes available with `--fix --unsafe-fixes`).
~/bazarghar-ai$ ruff format --diff messy.py
--- messy.py
+++ messy.py
@@ -2,6 +2,7 @@
 import numpy as np
 import pandas as pd

+
 def load(path, cols=[]):
…
1 file would be reformatted

আটটি বার্তার মধ্যে দুটি আসল ডেটা-বাগ। E711, লাইন ৮: isna().sum() একটি সংখ্যা, সেটি কখনো None নয় — তাই শর্তটি কখনো সত্য হয় না, এবং “missing আছে কি না” যাচাই আসলে কিছুই যাচাই করছে না। Ruff শুধু ধাঁচটি ধরেছে; আসল সংশোধন == 0. E722, লাইন ১২: খালি except: pass দাম রূপান্তরের যেকোনো ভুল গিলে ফেলে — দাম লেখা থেকে যায়, পরে গড় নিতে গিয়ে অন্য কোথাও অদ্ভুত error আসে। B006: cols=[] সব ডাকে একই তালিকা ভাগ করে। [*] চিহ্নিতগুলো ruff check --fix নিজেই সারায়; বাকিগুলো মানুষকে ভেবে সারাতে হয়। ruff format শুধু চেহারা বদলায় — function-এর আগে দুটি ফাঁকা লাইন — অর্থ নয়।

অভ্যাস হিসেবে: কোড commit করার আগে ruff check আর ruff format, আর editor-এ Ruff-এর extension চালু রাখুন, যাতে লেখার সময়েই দাগ পড়ে। Lint সবুজ মানে কোড ঠিক — তা নয়; lint ধরে পরিচিত ধাঁচের ভুল, যুক্তির ভুল নয়। সেজন্য test আর review.

৯ · AI কোড review — একটি চেকলিস্ট

Code review মানে কোড মূল ধারায় মেলানোর আগে আরেকজন (বা আপনি নিজে, এক দিন পরে) পড়ে দেখা। সাধারণ review-এর নিয়ম — ছোট পরিবর্তন, সম্মানজনক মন্তব্য — Software Engineering কোর্সের code review পর্বে আছে। ডেটা ও AI কোডে বাড়তি কিছু প্রশ্ন জিজ্ঞেস করতে হয়, কারণ এখানকার বাগ নীরব। পাঠযোগ্য কোডের সাধারণ নীতি দেখতে পারেন clean code পর্বে।

ডেটা ও AI কোড review-এর চেকলিস্ট

আরও গভীরে: Google-এর একদল প্রকৌশলীর ২০১৫ সালের প্রবন্ধ “Hidden Technical Debt in Machine Learning Systems” (Sculley ও সহকর্মী, NeurIPS) দেখায় যে বাস্তব ML সিস্টেমে model-এর কোড ছোট একটি অংশ; চারপাশের ডেটা, config আর জোড়া লাগানোর কোডে লুকানো “ঋণ” জমে। ২০১৭ সালে Breck ও সহকর্মীদের “The ML Test Score” প্রবন্ধে ডেটা, model আর পরিকাঠামোর জন্য test-এর একটি তালিকা প্রস্তাব করা হয়। Property-based testing-এর ধারণা এসেছে Haskell-এর QuickCheck (Claessen ও Hughes, ২০০০) থেকে; Python-এ Hypothesis সেই ধারার একটি জনপ্রিয় library.

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় যে ধরনের প্রশ্ন সাধারণত আসে):

  • Pure function কী, আর ডেটার কাজে কেন দরকারি? — একই input-এ একই output, কোনো side effect নেই; তাই test করা যায় আর আবার চালালে ফল বদলায় না।
  • np.random.seed আর default_rng-এর পার্থক্য? — প্রথমটি global অবস্থা বদলায় (legacy); দ্বিতীয়টি একটি আলাদা Generator object দেয়, যা argument হিসেবে পাঠানো যায়।
  • Type hint কি Python চলার সময় যাচাই করে? — না; mypy বা pyright-এর মতো static checker করে। চলার সময়ের যাচাই নিজে লিখতে হয়।
  • assert দিয়ে ব্যবহারকারীর input যাচাই করা ঠিক কি না? — না, python -O-তে assert বাদ পড়ে; সেখানে raise ValueError.
  • (4, 3) shape-এর array-তে mean(axis=0)-এর shape কী? — (3,); axis=1 দিলে (4,).

১০ · AI-এর যুগে — AI সহকারীর লেখা ডেটা-কোড যাচাই

AI সহকারী দিয়ে cleaning function বা training script লেখানো আজ খুব সাধারণ। সমস্যা হলো, AI-এর লেখা কোডও প্রথমবারেই চলে — আর এই পর্বের মূল কথা, চলা মানেই ঠিক নয়। AI সহকারী প্রায়ই যে ভুলগুলো করে:

AI সহকারীর ভুলকীভাবে ধরবেন
পুরোনো API — pandas-এর DataFrame.append, np.float, global np.random.seedনিজের version-এর docs; Ruff-এর NPY ও PD নিয়ম
অস্তিত্বহীন function বা parameterচালানো, mypy চালানো, docs-এ নাম খোঁজা
Error চাপা দেওয়া: except: pass, warnings.filterwarnings("ignore")Ruff-এর E722; প্রতিটি warning-এর কারণ পড়া
Split-এর আগে পুরো ডেটায় scaling — test ডেটা training-এ ফাঁসসবার আগে split; পর্ব ৫৬ ও পর্ব ৬৪
Training set-এ metric দেখানোআলাদা test ডেটা, baseline-এর সঙ্গে তুলনা
বিশ্বাসযোগ্য কিন্তু ভুল সংখ্যা বা “নিখুঁত” test, যা শুধু নিজের কোডই মেলায়হাতে হিসাব করা ছোট ডেটায় নিজে test লেখা
Unit-এর অনুমান (টাকা না পয়সা) না জিজ্ঞেস করেই ধরে নেওয়াColumn-এর নামে unit, range assert

AI সহকারীকে এমনভাবে বলুন, যাতে সে নিজেই পাহারা বসায় আর অনুমানগুলো সামনে আনে। লক্ষ করুন — আসল গ্রাহকের ডেটা নয়, শুধু column-এর নাম আর গঠন দেওয়া হয়েছে:

আমি Python 3.13, pandas 3 আর numpy 2 ব্যবহার করি। একটি অর্ডার-টেবিল আছে: order_id (str), district (str, বাংলা বা ইংরেজি, আগে-পরে space থাকতে পারে), price_paisa (int, কখনো ফাঁকা)। ১. clean_orders(df) নামে একটি pure function লেখো: type hint সহ, ইনপুট DataFrame বদলাবে না, দাম না থাকা সারি বাদ দেবে, price_taka (float) বানাবে। ২. Shape, null আর দামের সীমা (০-এর বেশি) যাচাইয়ের assertion বা ValueError দাও। ৩. একটি ৩–৪ সারির নির্দিষ্ট ডেটায় pytest test আর একটি property test-এর ধারণা দাও; প্রতিটি প্রত্যাশিত মান হাতে হিসাব করে দেখাও। ৪. Randomness থাকলে np.random.default_rng(seed) ব্যবহার করো, global np.random.seed নয়। ৫. কোনো API নিয়ে নিশ্চিত না হলে স্পষ্ট করে "নিশ্চিত নই" লেখো, আর কোন docs পাতায় যাচাই করব বলো।

AI সহকারীর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

যে কোড ভুল হলে চিৎকার করে না, সেই কোডকে বিশ্বাস করবেন না — তাকে চিৎকার করতে শেখান।

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

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

প্র ০১একটি spreadsheet-এর ভুল কীভাবে অর্থনীতির নীতি-বিতর্কে ঢুকে পড়েছিল, আর ডেটার কোডের জন্য এর শিক্ষা কী?

২০১০ সালে অর্থনীতিবিদ Carmen Reinhart ও Kenneth Rogoff “Growth in a Time of Debt” নামে একটি প্রভাবশালী প্রবন্ধ প্রকাশ করেন। এর একটি বহুল উদ্ধৃত সিদ্ধান্ত ছিল: সরকারি ঋণ মোট দেশজ উৎপাদনের প্রায় ৯০%-এর বেশি হলে প্রবৃদ্ধি অনেক কমে যায়। এই ফল বিভিন্ন দেশে ব্যয় সংকোচনের পক্ষে বিতর্কে উদ্ধৃত হয়েছিল বলে ব্যাপকভাবে আলোচিত।

২০১৩ সালে ম্যাসাচুসেটস বিশ্ববিদ্যালয়ের (আমহার্স্ট) ছাত্র Thomas Herndon, তাঁর শিক্ষক Michael Ash ও Robert Pollin-এর সঙ্গে, মূল spreadsheet হাতে পেয়ে ফলটি আবার হিসাব করার চেষ্টা করেন। তাঁরা কয়েকটি সমস্যা পান: একটি Excel সূত্রের পরিসর কয়েকটি দেশের সারি বাদ দিয়ে গিয়েছিল, কিছু তথ্য বাদ পড়েছিল, আর গড় নেওয়ার পদ্ধতি নিয়েও প্রশ্ন ছিল। সংশোধিত হিসাবে প্রবৃদ্ধির পতন অনেক কম নাটকীয় দেখায়। মূল লেখকেরা সূত্রের ভুল স্বীকার করেন, তবে বলেন তাঁদের মূল বক্তব্যের ওপর এর প্রভাব সীমিত; এ নিয়ে অর্থনীতিবিদদের মধ্যে বিতর্ক চলেছে।

এই ঘটনা এই পর্বের প্রায় সব কথাকে একসঙ্গে তুলে ধরে। প্রথমত, এটি একটি নিখুঁত নীরব বাগ: spreadsheet কোনো error দেখায়নি, একটি সুন্দর সংখ্যা দিয়েছিল। দ্বিতীয়ত, ভুলটি ধরা পড়েছে কারণ কেউ পুরো হিসাব আবার করতে চেয়েছিলেন — reproducibility-র শক্তি। তৃতীয়ত, ভুলের ধরনটি ছিল “ভুল সারির পরিসর” — কোডে যেটি ভুল slice বা ভুল filter. সারির সংখ্যার একটি সাধারণ assertion (“২০টি দেশই আছে তো?”) এটি ধরতে পারত।

ডেটার কোডের জন্য শিক্ষা: হিসাব কোডে রাখুন, যাতে প্রতিটি ধাপ পড়া যায়; দেশ, জেলা বা সারির সংখ্যা assert করুন; কোড ও ডেটা অন্যকে দেখতে দিন। Spreadsheet খারাপ নয়, কিন্তু লুকানো সূত্রের পরিসর review করা কঠিন।

মূল উপলব্ধি: বড় সিদ্ধান্তের পেছনের সংখ্যা যত গুরুত্বপূর্ণ, তার হিসাব তত খোলা আর আবার-চালানো যায় এমন হওয়া উচিত। নীরব বাগ ধরার সবচেয়ে ভালো উপায় হলো অন্য কাউকে আপনার হিসাব আবার করতে দেওয়া।

প্র ০২বিজ্ঞানীরা জিনের নামই বদলে ফেলেছিলেন একটি software-এর কারণে — ঘটনাটি কী, আর “type চুপচাপ বদলানো” কেন বিপজ্জনক?

মানুষের কিছু জিনের সংক্ষিপ্ত নাম দেখতে তারিখের মতো: যেমন SEPT1 বা MARCH1. Excel-এর মতো spreadsheet-এ এগুলো লিখলে software নিজে থেকেই তারিখ ধরে নিয়ে “1-Sep” বা “1-Mar” বানিয়ে ফেলত। ২০১৬ সালে Ziemann, Eren ও El-Osta “Genome Biology” পত্রিকায় দেখান যে প্রকাশিত বহু গবেষণাপত্রের সঙ্গে দেওয়া জিন-তালিকার Excel ফাইলে এই ভুল আছে — তাঁদের পরীক্ষিত জার্নালগুলোর জিন-তালিকাযুক্ত প্রবন্ধের মোটামুটি এক-পঞ্চমাংশে। পরে আরও গবেষণাতেও সমস্যাটি থেকে যাওয়ার কথা আসে।

শেষে ২০২০ সালে মানুষের জিনের নাম ঠিক করার দায়িত্বপ্রাপ্ত কমিটি (HGNC) নির্দেশিকা হালনাগাদ করে এমন কিছু জিনের নামই বদলে দেয় — SEPT1 হয় SEPTIN1, MARCH1 হয় MARCHF1. অর্থাৎ software বদলানোর চেয়ে নাম বদলানো সহজ মনে হয়েছিল! (Excel-এ পরে এই স্বয়ংক্রিয় রূপান্তর বন্ধ করার একটি সেটিং আনা হয়েছে বলে জানা যায়।)

এটি “type চুপচাপ বদলানো”র ক্লাসিক উদাহরণ: লেখা ছিল, হয়ে গেল তারিখ, কোনো সতর্কবার্তা ছাড়া। Python আর pandas-এও এর আত্মীয় আছে। এই পর্বের test-এ দেখেছেন, একটি None থাকায় পূর্ণসংখ্যার কলাম float হয়ে গেছে। CSV পড়ার সময় “00123” অর্ডার নম্বর সংখ্যা হয়ে 123 হয়ে যেতে পারে; ফোন নম্বরের শুরুর ০ হারিয়ে যেতে পারে; “NA” লেখা একটি জেলার কোড ফাঁকা মান ধরা হতে পারে। বাংলাদেশি ডেটায় তারিখের দিন-মাসের ক্রম (০৩/০৪ মানে ৩ এপ্রিল না ৪ মার্চ?) নিয়েও একই ঝুঁকি।

প্রতিকার: পড়ার সময় type নিজে বলে দেওয়া (dtype={"order_id": str}), পড়ার পর প্রতিটি column-এর type assert করা, আর একটি ছোট test রাখা যেখানে “00123” আর বাংলা নাম আছে। ফাইলের ধরন আর encoding পর্ব ১০-এ, আর pandas-এ type পরিষ্কার করা পর্ব ২৩-এ আসবে।

মূল উপলব্ধি: যে software “সাহায্য করতে” আপনার ডেটার type অনুমান করে, সে মাঝে মাঝে আপনার ডেটা বদলেও দেয়। Type অনুমানের ওপর ছেড়ে না দিয়ে ঘোষণা করুন, আর যাচাই করুন।

প্র ০৩Unit-এর গোলমালে একটি মহাকাশযান হারিয়েছিল — টাকা আর পয়সার ছোট ভুলের সঙ্গে এর মিল কোথায়, আর কোডে কীভাবে আটকাবেন?

১৯৯৯ সালে NASA-র Mars Climate Orbiter মঙ্গলের কক্ষপথে ঢোকার সময় হারিয়ে যায়। পরের তদন্তে মূল কারণ হিসেবে উঠে আসে একটি unit-এর অমিল: মহাকাশযানের নির্মাতা প্রতিষ্ঠানের একটি ground software ইঞ্জিনের ধাক্কার মান দিচ্ছিল ইংরেজ পদ্ধতির unit-এ (pound-force second), আর NASA-র নেভিগেশন দল সেটি ধরে নিচ্ছিল মেট্রিক unit-এ (newton second). পার্থক্যটি প্রায় সাড়ে চার গুণ। মাসের পর মাস ছোট ছোট ভুল জমে মহাকাশযানটি পরিকল্পনার চেয়ে অনেক নিচ দিয়ে মঙ্গলের কাছে যায়। তদন্ত প্রতিবেদন শুধু unit-কে দোষ দেয়নি; যাচাই-প্রক্রিয়া আর দলগুলোর মধ্যে যোগাযোগের ঘাটতিকেও বড় কারণ বলেছে।

মিলটা স্পষ্ট। দুই দল, দুই অনুমান, মাঝখানে একটি সংখ্যা, যার গায়ে unit লেখা নেই। বাজারঘরে payment API পয়সায় পাঠায়, বিশ্লেষণের কোড টাকা ধরে — ফল ১০০ গুণ বড় বিক্রি, আর সেটি দেখতে “ঈদের বাজার ভালো গেছে” বলে মনে হতে পারে। মহাকাশযানের মতোই ভুলটা একবারে ধরা পড়ে না, বিশ্বাসযোগ্য দেখায়।

কোডে আটকানোর কয়েকটি স্তর আছে। এক, নামে unit: price_paisa, dist_km, duration_min. দুই, type দিয়ে আলাদা করা: NewType("Paisa", int) mypy-কে মিশ্রণ ধরতে সাহায্য করে, যদিও চলার সময় কিছু থামায় না। তিন, সীমান্তে রূপান্তর: বাইরের ডেটা যেখানে ঢোকে, ঠিক সেখানেই একবার পয়সা থেকে টাকা, তারপর ভেতরে সবখানে টাকা। চার, range assert বা ValueError: “গড় অর্ডার ৫০,০০০ টাকার বেশি” হলে থেমে যাওয়া। পাঁচ, property test: “মোট টাকা × ১০০ = মোট পয়সা”। বড় বৈজ্ঞানিক কোডে unit-সচেতন library (যেমন Python-এর pint) ব্যবহারের চলও আছে।

মূল উপলব্ধি: Unit ছাড়া সংখ্যা একটি অসম্পূর্ণ বাক্য। নামে, type-এ আর সীমা-যাচাইয়ে unit লিখে রাখলে দুই দলের দুই অনুমান কোডের ভেতরেই সংঘর্ষে আসে — মাঠে নয়।

প্র ০৪Machine learning গবেষণায় “seed বদলালে ফল বদলায়” — এটি কতটা বড় সমস্যা, আর গবেষণা-সমাজ কী করেছে?

২০১৮ সালে Henderson ও সহকর্মীদের প্রবন্ধ “Deep Reinforcement Learning that Matters” (AAAI) একটি অস্বস্তিকর জিনিস দেখায়: একই অ্যালগরিদম, একই সেটিং, শুধু random seed আলাদা — তাতেই ফল এত বদলায় যে কয়েকটি seed বেছে নিলে মনে হতে পারে একটি পদ্ধতি অন্যটির চেয়ে ভালো, আবার অন্য কয়েকটি seed নিলে উল্টো। তাঁরা দেখান, অল্প কয়েকটি run-এর গড় দিয়ে তুলনা করলে ভুল সিদ্ধান্তে পৌঁছানো সহজ। একই সময়ে কোড-ভিত্তিক খুঁটিনাটি (library-র সংস্করণ, ছোট বাস্তবায়নগত পার্থক্য) ফল বদলায় বলেও আলোচনা হয়।

গবেষণা-সমাজ কিছু পদক্ষেপ নিয়েছে। Joelle Pineau ও সহকর্মীদের উদ্যোগে NeurIPS সম্মেলনে ২০১৯ সালের দিকে একটি reproducibility checklist চালু হয় — কোড দেওয়া হয়েছে কি না, কতগুলো seed-এ চালানো হয়েছে, ফলের ছড়ানো (যেমন standard deviation বা confidence interval) জানানো হয়েছে কি না, কোন hardware আর সংস্করণ। কয়েকটি সম্মেলনে “reproducibility challenge”-ও হয়েছে, যেখানে অন্যরা প্রকাশিত ফল আবার করে দেখেন। এগুলো সমস্যা পুরো মেটায়নি, তবে প্রত্যাশা বদলেছে।

আপনার নিজের কাজে এর মানে কী? প্রথমত, একটি seed-এর একটি সংখ্যা দিয়ে কোনো model-কে “ভালো” বলবেন না। কমপক্ষে কয়েকটি seed-এ চালিয়ে গড় আর ছড়ানো জানান, বিশেষত যখন দুটি model-এর পার্থক্য সামান্য। দ্বিতীয়ত, seed বেছে বেছে ভালো ফল দেখানো — যাকে কখনো “seed hacking” বলা হয় — সচেতনভাবে এড়িয়ে চলুন; আগে থেকে seed-এর তালিকা ঠিক করে নিন। তৃতীয়ত, seed-এর সঙ্গে library-র সংস্করণও লিখুন, কারণ একই seed-এ ভিন্ন সংস্করণ ভিন্ন ধারা দিতে পারে।

মূল উপলব্ধি: Seed পুনরুৎপাদন নিশ্চিত করে, কিন্তু সত্য নিশ্চিত করে না। একটি seed-এর ফল একটি নমুনা; সত্যের কাছাকাছি যেতে অনেক seed-এর ছড়ানোটাও দেখতে হয়।

প্র ০৫GitHub-এর লক্ষাধিক Jupyter notebook নিয়ে গবেষণায় কী পাওয়া গিয়েছিল — notebook কি খারাপ, নাকি অভ্যাসের সমস্যা?

২০১৯ সালে Pimentel, Murta, Braganholo ও Freire “A Large-scale Study about Quality and Reproducibility of Jupyter Notebooks” (MSR সম্মেলন) প্রবন্ধে GitHub থেকে প্রায় ১৪ লক্ষ notebook বিশ্লেষণ করেন। যেগুলো তাঁরা আবার চালানোর চেষ্টা করেছিলেন, তার মধ্যে মোটামুটি এক-চতুর্থাংশ error ছাড়া শেষ পর্যন্ত চলেছিল, আর মাত্র কয়েক শতাংশ আগের মতো একই ফল দিয়েছিল। কারণের মধ্যে ছিল: cell-গুলো এলোমেলো ক্রমে চালানো (hidden state), কোন package-এর কোন সংস্করণ লাগবে তা লেখা না থাকা, আর বাইরের ফাইলের ওপর নির্ভরতা। সংখ্যাগুলো তাঁদের নমুনা আর পদ্ধতির ওপর নির্ভরশীল, কিন্তু প্রবণতাটি স্পষ্ট।

প্রায় একই সময়ে, ২০১৮ সালের JupyterCon-এ প্রোগ্রামার Joel Grus “I don't like notebooks” নামে একটি আলোচিত বক্তৃতা দেন — hidden state, খারাপ অভ্যাস তৈরি, test আর module লেখায় বাধা নিয়ে। অন্যদিকে notebook-এর সমর্থকেরা বলেন, ডেটা অনুসন্ধান, শেখানো আর ফল ব্যাখ্যায় কোড, লেখা আর চার্ট একসঙ্গে দেখানোর এর চেয়ে ভালো উপায় কম। এরপর notebook-এর টুলেও উন্নতি এসেছে: “Restart and run all”, notebook-কে সাধারণ .py ফাইলে রাখার টুল, আর কিছু নতুন notebook পরিবেশ, যা cell-গুলোর নির্ভরতা নিজে মেনে চলে।

তাহলে উত্তর কী? সম্ভবত দুটোই — টুলের নকশা কিছু ভুলকে সহজ করে, আর অভ্যাস সেই ভুল ঠেকাতে পারে। এই পর্বের অভ্যাসগুলো ঠিক সেখানেই কাজ করে: বারবার লাগা কোড .py ফাইলের pure function-এ, notebook শুধু সেগুলো ডাকে; কাজ শেষে একবার “Restart and run all” করে দেখা যে উপর থেকে নিচে চলে; seed আর config এক জায়গায়; package-এর সংস্করণ লেখা (পর্ব ০৭ ও পর্ব ১২)। Notebook তখন একটি খোলা খাতা, গোলকধাঁধা নয়।

মূল উপলব্ধি: Notebook নিজে ভালো বা খারাপ নয়; hidden state আর লেখা-না-থাকা সংস্করণ হলো আসল শত্রু। “উপর থেকে নিচে আবার চালালে একই ফল” — এই একটি পরীক্ষা যেকোনো notebook-কে বিশ্বাসযোগ্যতার দিকে অনেকটা এগিয়ে দেয়।

অনুশীলন

হিসাব, কোড, আগাম অনুমান আর নকশা-সমালোচনা — চার রকম কাজ।

  1. আগাম বলুন (predict the output): a = np.arange(12).reshape(3, 4). চালানোর আগে লিখে ফেলুন: a.sum(axis=0) আর a.mean(axis=1)-এর মান ও shape কী হবে? assert a.sum(axis=1).shape == (4,) পাস করবে কি না?

    a হলো তিন সারি: [0 1 2 3], [4 5 6 7], [8 9 10 11]. axis=0 মানে কলাম ধরে যোগ: 0 + 4 + 8 = 12, তারপর 15, 18, 21 — তাই [12 15 18 21], shape (4,). axis=1-এ প্রতিটি সারির গড়: (0 + 1 + 2 + 3) ÷ 4 = 1.5, তারপর 5.5, 9.5 — তাই [1.5 5.5 9.5], shape (3,).

    a.sum(axis=1)-এর shape (3,), তাই assert ... == (4,) ব্যর্থ হবে এবং AssertionError উঠবে। মনে রাখার কৌশল: যে axis দিয়ে হিসাব করেন, সেই axis-টি shape থেকে “মুছে যায়” — (3, 4) থেকে axis 0 মুছলে (4,), axis 1 মুছলে (3,).

  2. কোড লিখুন: খুদে test runner-এর কোড-বাক্সে clean_price এমনভাবে ঠিক করুন, যাতে তিনটি test-ই পাস করে। তারপর চতুর্থ একটি test যোগ করুন: "৳ 1,250.50" থেকে 1250.5. কোন লাইন বদলাতে হলো?

    strip()-এর পরে একটি শর্ত যোগ করলেই ফাঁকা লেখার সমস্যা মেটে:

    def clean_price(text):
        t = text.replace("৳", "").replace(",", "").replace("টাকা", "").strip()
        if not t:
            return None
        return float(t)
    
    def test_with_paisa():
        assert clean_price("৳ 1,250.50") == 1250.5

    চতুর্থ test-এর জন্য নতুন কিছু বদলাতে হয় না — "৳ 1,250.50" থেকে ৳ আর কমা সরালে থাকে " 1250.50", strip()-এ "1250.50", আর float() দেয় 1250.5. runner-এর তালিকায় test_with_paisa যোগ করলে আউটপুটের শেষ লাইন হবে 4 passed, 0 failed. লক্ষ করুন: test আগে লিখে তারপর কোড সারালে আপনি নিশ্চিত হন যে test সত্যিই বাগটি ধরতে পারে।

  3. হিসাব করুন: বাজারঘরের তিনটি অর্ডারের দাম payment API থেকে এসেছে পয়সায়: ২৫০০০০, ৭৫০০০, ১২০০০০। ভুল করে এগুলো টাকা ধরা হলে গড় অর্ডার কত দেখাবে, আর সঠিক গড় কত? “গড় ১০ থেকে ৫০,০০০ টাকার মধ্যে” — এই পাহারা কোনটিকে থামাবে? দুটি অর্ডারের দাম ৫০০ ও ৮০০ পয়সা (৫ ও ৮ টাকা) হলে, ভুলটি কি এই পাহারা ধরতে পারত?

    ভুল গড় = (২৫০০০০ + ৭৫০০০ + ১২০০০০) ÷ ৩ = ৪৪৫০০০ ÷ ৩ ≈ ১৪৮৩৩৩.৩৩ “টাকা”। সঠিক গড় = ১৪৮৩৩৩.৩৩ ÷ ১০০ ≈ ১৪৮৩.৩৩ টাকা। পাহারা ভুলটি থামাবে (১৪৮৩৩৩ > ৫০০০০), সঠিকটি পার হবে।

    দ্বিতীয় ক্ষেত্রে ভুল গড় = (৫০০ + ৮০০) ÷ ২ = ৬৫০ “টাকা” — এটি ১০ থেকে ৫০,০০০-এর ভেতরে, তাই পাহারা ধরবে না। শিক্ষা: সীমা-যাচাই শুধু বড় অমিল ধরে; ছোট মানের ক্ষেত্রে unit-এর ভুল সীমার ভেতরে লুকিয়ে যেতে পারে। তাই সীমা-যাচাইয়ের পাশাপাশি নামে unit (price_paisa), সীমান্তে একবার রূপান্তর আর type (Paisa) দরকার — একাধিক স্তরের পাহারা।

  4. নকশা-সমালোচনা: একজন সহকর্মী AI সহকারী দিয়ে একটি script বানিয়েছেন। এর মূল অংশ: np.random.seed(1); df = pd.read_csv("orders.csv"); df["price"] = df["price"] / 100; df = df.fillna(0); X = df.drop(columns=["late"]); try: model.fit(X, df["late"]) except: pass; শেষে print("accuracy", model.score(X, df["late"])). এই পর্বের চেকলিস্ট ধরে অন্তত পাঁচটি সমস্যা চিহ্নিত করুন আর প্রতিটির সংশোধন বলুন।
    • Global seed: np.random.seed(1) legacy; default_rng আর model-এ random_state, seed config-এ।
    • জায়গায় বদল আর unit: df["price"] / 100 — দুবার চালালে আবার ভাগ; আর price কি সত্যিই পয়সা? নতুন নাম price_taka, একটি pure function, range assert.
    • fillna(0) সব কলামে: দাম না থাকা মানে ০ টাকা নয়; null-এর সংখ্যা log করা, কলাম ধরে সিদ্ধান্ত।
    • Label ফাঁসের ঝুঁকি: late বাদ দিলেও “delivered_at”-এর মতো ভবিষ্যতের কলাম থেকে যেতে পারে; feature-এর তালিকা স্পষ্ট লেখা আর assert.
    • except: pass: fit ব্যর্থ হলেও চুপ; error উঠতে দেওয়া বা log করে থামা।
    • Training ডেটায় accuracy: একই X-এ score — split নেই; আগে train/test ভাগ, baseline-এর পাশে metric.
    • print, config, test নেই: logging, এক জায়গায় config, cleaning-এর জন্য ছোট test, আর Ruff ও mypy.

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

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

ক্রমNotebook-এর একটি cleaning cell-কে বিশ্বাসযোগ্য কোডে নেওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔍 Cell কী নেয় আর কী দেয় — input ও output চিহ্নিত করা
  2. 🧩 Type hint-সহ একটি pure function-এ তুলে আনা, মূল টেবিল না বদলে
  3. 🛡️ Shape, null আর মানের সীমার assertion বসানো
  4. 🧪 ছোট নির্দিষ্ট ডেটায় pytest test লিখে চালানো
  5. 🧹 Ruff আর mypy চালিয়ে বার্তাগুলো সারানো, তারপর review

৪ দিন × ৩ জেলার বিক্রির একটি NumPy array-র shape (4, 3). প্রতিটি জেলার গড় চাই। sales.mean(axis=1) লিখলে কী ঘটবে?

  • Python একটি error দিয়ে থামবে
  • প্রতিটি জেলার সঠিক গড় আসবে
  • Error ছাড়াই ৪টি দিনের গড় আসবে, shape (4,) — একটি নীরব বাগ
  • একটি (4, 3) shape-এর array আসবে

একটি function-এর ঘোষণা def paisa_to_taka(paisa: int) -> int, আর ভেতরে return paisa / 100. Python দিয়ে paisa_to_taka(12550) চালালে আর mypy চালালে কী হবে?

  • Python চালালেই TypeError দেবে
  • Python 125.5 দেবে; mypy “got float, expected int” error দেখাবে
  • Python 125 দেবে; mypy কিছু বলবে না
  • দুটোই কোনো সমস্যা দেখাবে না

rng = np.random.default_rng(7) বানিয়ে পরপর দুবার rng.integers(1, 7, size=4) ডাকা হলো। কোন কথাটি সঠিক?

  • দুবারই হুবহু একই চারটি সংখ্যা আসবে
  • প্রোগ্রাম আবার চালালে দুটি ডাকের ফলই বদলে যাবে
  • ঠিক ফলের জন্য আগে np.random.seed(7) ডাকতে হবে
  • দুই ডাকের ফল আলাদা হতে পারে, কিন্তু প্রোগ্রাম আবার চালালে দুটোই আগের মতো আসবে

ব্যবহারকারীর পাঠানো CSV-তে দাম ঋণাত্মক হলে কাজ থামাতে হবে, আর production-এ Python কখনো কখনো -O দিয়ে চালানো হয়। কোন পদ্ধতি ঠিক?

  • if (df["price_taka"] < 0).any(): raise ValueError(...)
  • assert (df["price_taka"] >= 0).all()
  • assert ((df["price_taka"] >= 0).all(), "negative")
  • Function-এ price: float type hint দেওয়া

একটি property-based test-এ নিয়ম লেখা হলো “পরিষ্কারের পর মোট টাকা × ১০০ = মোট পয়সা”। Function-এ টাকায় নেওয়ার লাইন paisa // 100. Hypothesis-এর মতো টুল সাধারণত কোন উদাহরণে ব্যর্থতা জানাবে?

  • [100, 200]
  • ফাঁকা তালিকা []
  • [1] — এক পয়সা, যা ০ টাকা হয়ে যায়
  • একটি বিশাল এলোমেলো তালিকা, যা আর ছোট করা যায় না

Ruff একটি লাইনে E711 Comparison to `None` should be `cond is None` দেখাল: if df[c].isna().sum() == None:. এখানে আসল সমস্যা কী?

  • শুধু লেখার ধরন; is None লিখলেই কোড ঠিক
  • যোগফল কখনো None হয় না, তাই যাচাইটি কখনো কাজ করে না; লেখা উচিত == 0
  • isna() pandas-এ নেই
  • ফাইলটি format করা হয়নি

একটি model-কে seed ৪২ দিয়ে চালিয়ে accuracy ০.৮৮, seed ৭ দিয়ে ০.৮১ পাওয়া গেল। রিপোর্টে সবচেয়ে সৎ কাজ কোনটি?

  • শুধু ০.৮৮ লেখা, কারণ সেটি reproducible
  • Seed বাদ দিয়ে একবার চালিয়ে যা আসে তা লেখা
  • দুটির গড় ০.৮৪৫ লিখে ছেড়ে দেওয়া
  • আগে থেকে ঠিক করা কয়েকটি seed-এ চালিয়ে গড় ও ছড়ানো, seed-তালিকা আর version-সহ জানানো

শব্দকোষ

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

মনে রাখুন

  1. ডেটার কোডে error না আসা কোনো প্রমাণ নয় — ভুল axis, ভুল column, পয়সা-টাকা, label ফাঁস নীরবে ঘটে।
  2. বারবার লাগা কাজ ছোট pure function-এ: স্পষ্ট input-output, মূল টেবিল অক্ষত, আবার চালালে একই ফল।
  3. তিন স্তরের পাহারা: type hint ও mypy (চালানোর আগে), assertion ও ValueError (চলার সময়), ছোট pytest test (জানা ডেটায় জানা উত্তর)।
  4. default_rng(seed) আর random_state দিয়ে পুনরুৎপাদন; কিন্তু version বদলালে ফল বদলাতে পারে, আর ভালো seed বেছে নেওয়া চলবে না।
  5. লম্বা কাজে logging, এক জায়গায় config, Ruff — আর শেষে চেকলিস্ট ধরে review.

পরের পর্বে: এক ফাইলের পরিচ্ছন্নতা থেকে পুরো প্রকল্পের পরিচ্ছন্নতা — folder সাজানো, lockfile দিয়ে package-এর সংস্করণ বেঁধে রাখা, ডেটার সংস্করণ আর Git, যাতে আজকের ফল ছয় মাস পরেও হুবহু পাওয়া যায়।

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

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