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

Python আর ডেটার ইন্টারভিউ — NumPy, pandas আর live coding

Python and Data Interview Preparation — Concepts, NumPy, pandas, Live Coding with Tests and Debugging Real Errors

Taught in Bangla, this practice page prepares you for Python and data interviews: concept questions with good and weak answers, view versus copy, int32 overflow, Copy-on-Write, merge row explosion and agg versus transform, eight live coding problems with tests, and debugging rounds built from errors really produced by in-browser Python with pandas 3.0.2.

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

  • Python, NumPy আর pandas-এর সাধারণ ধারণার প্রশ্নে কারণ আর একটি ছোট উদাহরণসহ উত্তর দেওয়া
  • View আর copy, int32 আর int64, float-এর সূক্ষ্মতা, Copy-on-Write, merge-এর সারি ফুলে ওঠা আর transform বনাম agg চালিয়ে প্রমাণ করা
  • আটটি live coding সমস্যা কথা বলে বলে সমাধান করা: top-k, rolling, dedupe, বাংলা জেলার নাম, distance matrix, গ্রাহক ধরে ভাগ, pipeline আর ধীর apply
  • Leakage, chained assignment আর পাঁচটি সাধারণ বাগ সত্যিকারের error থেকে ধরে ঠিক করা
  • Live coding-এ কীভাবে কথা বলবেন, আর জমা দেওয়ার আগে কী কী মিলিয়ে নেবেন

এই পাতার প্রশ্ন সম্পর্কে: প্রশ্ন আর সমস্যাগুলো এই কোর্সের পর্ব ৭ থেকে ৩২ আর ৬২ থেকে ৬৭ থেকে সাজানো। এগুলো কোনো নির্দিষ্ট প্রতিষ্ঠান, বিশ্ববিদ্যালয় বা নিয়োগ পরীক্ষা থেকে নেওয়া নয়। এ ধরনের প্রশ্ন junior data analyst, Python developer আর ML বা AI intern ইন্টারভিউতে প্রায়ই আসে, তবে আপনার ইন্টারভিউ ভিন্ন হতে পারে। সব ডেটা বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান), seed দিয়ে বানানো। পাতার ব্লক চলে Python 3.14.2, NumPy 2.4.6, pandas 3.0.2 আর scikit-learn 1.8.0-এ। প্রথমবার চালালে package নামতে কয়েক সেকেন্ড লাগে, scikit-learn-এ প্রায় ১০ সেকেন্ড; পরের run দ্রুত।

পাড়ার মুদি দোকানে নতুন হিসাবরক্ষক নিতে গেলে মালিক শুধু জিজ্ঞেস করেন না, “যোগ-বিয়োগ পারেন?” তিনি খাতাটা হাতে দিয়ে বলেন, “গত সাত দিনের বিক্রি বের করুন।” তারপর একটি ভুল হিসাব দেখিয়ে বলেন, “এখানে গোলমাল কোথায়?” ডেটার কাজের Python ইন্টারভিউও অনেকটা তেমন: সংজ্ঞার চেয়ে বেশি দেখা হয় আপনি টেবিল হাতে পেয়ে কীভাবে ভাবেন, উত্তর যাচাই করেন, আর ভাঙা কোডের কারণ খোঁজেন।

১ · ডেটার ইন্টারভিউ সাধারণত কীভাবে চলে, আর live coding-এ কীভাবে কথা বলবেন

রিভিশন: পর্ব ৮ · Jupyter notebook, পর্ব ১১ · পরিচ্ছন্ন কোড আর ছোট test, পর্ব ৬২ · EDA, Python coding ইন্টারভিউ

প্রতিষ্ঠান ভেদে ধাপ আর নাম আলাদা। নিচের ছবিটি একটি সাধারণ কাঠামো, কোনো নির্দিষ্ট প্রতিষ্ঠানের নিয়ম নয়।

ডেটা আর ML ইন্টারভিউর Python অংশের সাধারণ দফা

A common shape, not a rule: concepts, live coding with pandas/NumPy, debugging, a small ML workflow

ডেটার ইন্টারভিউর পাঁচটি সাধারণ দফা ১ · Screeningনিজের প্রকল্প, কোন ডেটা, কী সিদ্ধান্ত ২ · ধারণার প্রশ্নlist বনাম set, view, dtype, groupby ৩ · Live codingএকটি টেবিল, pandas বা NumPy, test-সহ ৪ · ডিবাগিংলক্ষণ → প্রমাণ → কারণ → সমাধান ৫ · ছোট ML কাজ (কোথাও কোথাও)split, pipeline, leakage, baseline
ছবি ৭৮.১ — সবুজ দুটি দফায় হাতে কোড লিখতে হয়। AI-এর ধারণা, অ্যালগরিদম আর আচরণগত প্রশ্ন আছে পর্ব ৭৯-এ (AI-এর ভিত্তির ইন্টারভিউ)।

Live coding-এ কথা বলার ছক

Live coding, মানে ইন্টারভিউয়ারের সামনে সময় বেঁধে কোড লেখা, সেখানে ইন্টারভিউয়ার শুধু শেষ কোড দেখেন না, আপনার চিন্তার পথও শোনেন। এজন্য think-aloud, মানে মনে মনে যা ভাবছেন তা জোরে বলা, এখানে সবচেয়ে বড় দক্ষতা। নিচের ছয় ধাপ এই পাতার প্রতিটি সমস্যায় ব্যবহার হবে।

ধাপযা বলবেন বা করবেনউদাহরণ বাক্য
১ · চাহিদা মেলানোইনপুট, আউটপুট, সমান মান, ফাঁকা ঘর, আকার জিজ্ঞেস করা“দুটি পণ্যের বিক্রি সমান হলে কোনটি আগে যাবে?”
২ · হাতে ছোট উদাহরণ৪–৬ সারির টেবিল লিখে প্রত্যাশিত ফল বলা“ঢাকায় মধু ১৯, ইলিশ ১২, তাই top-2 এই দুটি।”
৩ · সরল সঠিক রূপআগে চলমান সমাধান, পরে দ্রুত রূপ“প্রথমে groupby আর sort, গতি পরে ভাবব।”
৪ · testassert দিয়ে হাতে বানানো উত্তর, খালি ইনপুট, সমান মান“একটি loop দিয়ে একই ফল মিলিয়ে দেখি।”
৫ · জটিলতাসময় আর memory, n বাড়লে কী হবে“Sort-এর জন্য O(n log n), memory O(n).”
৬ · উন্নতি আর সীমাবড় ডেটায়, অন্য টুলে, কী বাদ রইল“১০ কোটি সারি হলে chunk বা database-এ করতাম।”
প্র ১Live coding-এ একটি pandas সমস্যা পেয়েই কী করবেন? (সহজ)

ভালো উত্তর: প্রথম দুই-তিন মিনিট কোড নয়, প্রশ্ন। কলামের নাম আর dtype কী, ফাঁকা ঘর আছে কি না, সমান মান হলে কী হবে, ফল কোন আকারে চাই (DataFrame, Series, dict), কত সারি। তারপর ছোট একটি উদাহরণে প্রত্যাশিত ফল জোরে বলুন। এরপর বলুন, “আগে সরল চলমান রূপ, তারপর test, তারপর গতি।” প্রতিটি ছোট ধাপে চালিয়ে df.head() আর df.shape দেখুন (পর্ব ৬২-এর অভ্যাস)।

দুর্বল উত্তর: সঙ্গে সঙ্গে টাইপ শুরু, পনেরো মিনিট চুপ, শেষে একবারে চালানো। ভুল এলে ইন্টারভিউয়ার জানেন না আপনি কোথায় ছিলেন।

প্র ২যে API-র নাম মনে পড়ছে না, তখন কী বলবেন? (সহজ)

ভালো উত্তর: তিন অংশে: যা জানি, যা জানি না, কীভাবে জানব। যেমন: “Rolling-এ সময় ধরে জানালার একটি রূপ আছে, নামটা মনে হয় rolling("7D"). নিশ্চিত হতে help(pd.Series.rolling) দেখব, অথবা আপনি অনুমতি দিলে pandas-এর নথি।” Internet বা AI ব্যবহারের নিয়ম শুরুতেই জিজ্ঞেস করে নিন।

দুর্বল উত্তর: আত্মবিশ্বাসী গলায় নেই এমন একটি parameter লিখে ফেলা।

প্র ৩“আপনার একটি ডেটা প্রকল্পের কথা বলুন।” দুই মিনিটে কী বলবেন? (সহজ)

ভালো উত্তর: চার অংশে। কী প্রশ্ন, কোন ডেটা (কত সারি, কোথা থেকে)। ডেটায় কোন ঝামেলা ছিল আর কীভাবে সামলালেন, যেমন বাংলা জেলার নাম তিন বানানে লেখা ছিল। একটি সিদ্ধান্ত, যেমন “গ্রাহক ধরে ভাগ করলাম, কারণ একই গ্রাহক দুই দিকে থাকলে score ফুলে যায়।” আর ফল, একটি baseline-এর সাথে তুলনা করে। নিজের ক্যাপস্টোন (পর্ব ৭৩) এই গল্পের সবচেয়ে সৎ উৎস।

দুর্বল উত্তর: “pandas, scikit-learn, XGBoost দিয়ে একটি model বানিয়েছি, accuracy ৯৯%.” কোন split, কোন baseline, কোন leakage যাচাই, কিছুই নেই; অবিশ্বাস্য সংখ্যা উল্টো সন্দেহ জাগায়।

২ · Python-এর ভিত্তি — data structure-এর খরচ, comprehension আর mutable default

রিভিশন: পর্ব ৯ · ডেটার জন্য Python, পর্ব ১১, Python · comprehension, DSA · hash table

প্র ৪list, tuple, dict, set আর deque — কোন কাজে কোনটি, আর খরচ কত? (মাঝারি)

ভালো উত্তর: খরচের ছক মুখে বলতে পারা চাই, আর প্রতিটির একটি ডেটার উদাহরণ।

কাঠামোদ্রুতধীরডেটার কাজে উদাহরণ
listশেষে append (গড়ে O(1)), index দিয়ে পড়া O(1)x in lst O(n), শুরুতে insert O(n)ফাইল থেকে পড়া সারির ক্রম
tuplelist-এর মতো পড়া, বদলায় নাবদলানো যায় না(জেলা, পণ্য) জোড়া, dict-এর key
dictkey দিয়ে খোঁজা, বসানো গড়ে O(1)খারাপ hash-এ ধীর হতে পারেজেলার বানান থেকে আসল নামের ম্যাপ
setx in s গড়ে O(1)ক্রম রাখে না“এই order_id আগে দেখেছি?”
collections.dequeদুই মাথায় যোগ আর বাদ O(1)মাঝখানে index O(n)শেষ ৭ দিনের চলমান জানালা

আরেকটি কথা যোগ করলে উত্তর পূর্ণ হয়: Python 3.7 থেকে dict ঢোকানোর ক্রম রাখে। আর লাখ লাখ সংখ্যা হলে list নয়, NumPy array, কারণ list-এর প্রতিটি সংখ্যা আলাদা Python object (পর্ব ১৩)।

দুর্বল উত্তর: “dict দ্রুত, list ধীর।” কোন কাজে দ্রুত, তা না বললে উত্তর অর্ধেক।

প্র ৫Comprehension আর সাধারণ loop — কখন কোনটি লিখবেন? (সহজ)

ভালো উত্তর: নতুন একটি list, dict বা set বানানোর কাজ এক লাইনে পড়া গেলে comprehension: {p: q for p, q in pairs if q > 0}. এটি সাধারণত loop-এ append-এর চেয়ে কিছুটা দ্রুত, কারণ প্রতিবার append method খুঁজতে হয় না; Python 3.12 থেকে list, dict আর set comprehension আলাদা function হিসেবে না চলে inline হয় (PEP 709). তবে আসল কারণ পড়ার সুবিধা। Loop লিখবেন যখন শর্ত জটিল, বা ভেতরে print বা ফাইল লেখার মতো side effect আছে। শুধু একবার পড়ে ফেলে দিতে হলে generator expression: sum(q for q in qty), যা পুরো list memory-তে রাখে না।

দুর্বল উত্তর: শুধু শুধু print করার জন্য comprehension: [print(x) for x in rows]. এটি অকারণে None-এর একটি list বানায়।

প্র ৬Mutable default argument কেন বিপজ্জনক? (সহজ, প্রায়ই আসে)

ভালো উত্তর: Default মান একবারই তৈরি হয়, যখন def লাইনটি চলে, প্রতিবার function ডাকার সময় নয়। তাই def add_to_cart(item, cart=[])-এর খালি list সব ডাকে একটিই; একজনের ঝুড়িতে যোগ করলে পরের জনের ঝুড়িতেও তা দেখা যায়। অংশ ৮-এর বাগ ক-তে এর সত্যিকারের আউটপুট আছে। সমাধান: cart=None, আর ভেতরে if cart is None: cart = []. একই ফাঁদ dict, set আর DataFrame default-এও। Linter (যেমন Ruff-এর B006 নিয়ম) এটি ধরে (পর্ব ১১)।

দুর্বল উত্তর: “Python-এর বাগ।” এটি ভাষার নথিভুক্ত আচরণ; কারণ বলতে পারাই এখানে নম্বর।

৩ · NumPy — view না copy, dtype-এর ফাঁদ আর vectorization

রিভিশন: পর্ব ১৩, পর্ব ১৪, পর্ব ১৫, পর্ব ১৬, পর্ব ১৯

আউটপুট অনুমান: কোন বদল মূল array-তে পৌঁছায়?

চালানোর আগে লিখে রাখুন: prices শেষে কী হবে, তিনটি shares_memory-এর উত্তর কী, আর ১ লাখ অর্ডারের যোগফল কত।

import numpy as np
prices = np.array([120, 80, 450, 60, 300])   # বাজারঘরের ৫টি পণ্যের দাম (টাকা)
v = prices[1:4]          # basic slice → view
c = prices[[1, 2, 3]]    # fancy index → copy
m = prices[prices > 100] # boolean mask → copy
v[0] = 0; c[1] = 0; m[0] = 0
print("prices:", prices)
print("shares memory? view:", np.shares_memory(prices, v),
      "| fancy:", np.shares_memory(prices, c),
      "| mask:", np.shares_memory(prices, m))
print("default int dtype here:", prices.dtype)
paisa = np.full(100_000, 50_000)          # ১ লাখ অর্ডার, প্রতিটি ৫০০ টাকা = ৫০,০০০ পয়সা
print("sum, default dtype:", paisa.sum())
print("sum, int64        :", paisa.sum(dtype=np.int64))
print("0.1 + 0.2 == 0.3 ?", 0.1 + 0.2 == 0.3, "| isclose:", np.isclose(0.1 + 0.2, 0.3))

সত্যিকারের আউটপুট (এই পাতার ব্রাউজার-Python):

prices: [120   0 450  60 300]
shares memory? view: True | fancy: False | mask: False
default int dtype here: int32
sum, default dtype: 705032704
sum, int64        : 5000000000
0.1 + 0.2 == 0.3 ? False | isclose: True

তিনটি বদলের মধ্যে শুধু slice-এর বদল (v[0] = 0) মূল prices-এ পৌঁছাল, ৮০ হয়ে গেল ০. Slice একটি view, একই memory-র নতুন জানালা; fancy index আর boolean mask নতুন copy দেয়। এরপর আসল ফাঁদ: এই ব্রাউজার-Python ৩২-bit WebAssembly, তাই এখানে NumPy-র default পূর্ণসংখ্যা int32. ৫০০ কোটি পয়সা int32-এর সীমা (প্রায় ২১৪ কোটি) ছাড়িয়ে ঘুরে এসেছে, কোনো সতর্কবার্তা ছাড়াই: integer overflow. সাধারণ ৬৪-bit ডেস্কটপে (NumPy 2-এ Windows-সহ) default int64, তাই সেখানে ঠিক আসত। শেষ লাইন floating-point-এর পুরোনো সত্য: 0.1 আর 0.2 binary-তে ঠিকঠাক রাখা যায় না।

প্র ৭NumPy-তে কোন কাজ view দেয় আর কোনটি copy, আর তা কীভাবে নিশ্চিত হবেন? (মাঝারি)

ভালো উত্তর: সাধারণ নিয়ম: basic slicing (a[1:4], a[:, 0]), reshape (যখন সম্ভব), .T আর ravel (contiguous হলে) view দেয়; fancy index (a[[1, 2]]), boolean mask, flatten আর .copy() copy দেয়। অনুমান না করে প্রমাণ: np.shares_memory(a, b) অথবা b.base is a. View-এর সুবিধা গতি আর memory, ঝুঁকি হলো অজান্তে মূল ডেটা বদলে ফেলা। বড় array থেকে একটি ছোট slice রেখে বাকিটা ফেলতে চাইলে .copy() দিন, নইলে ছোট view পুরো বড় buffer-কে memory-তে ধরে রাখে (পর্ব ১৪, পর্ব ১৯)।

দুর্বল উত্তর: “NumPy সবসময় copy করে” বা “সবসময় view.” দুটিই ভুল, আর এই ভুলই বাস্তবে নীরব বাগ বানায়।

প্র ৮dtype-এর কোন ফাঁদ বাস্তবে ভুল সংখ্যা দেয়? int32, int64 আর float নিয়ে বলুন। (মাঝারি)

ভালো উত্তর: তিনটি ফাঁদ বলুন, প্রতিটির প্রতিকারসহ।

  • Overflow: int32-এর সীমা প্রায় ±২১৪ কোটি। টাকা পয়সায় রাখলে বা অনেক সারি যোগ করলে সহজেই ছাড়ায়, আর NumPy চুপচাপ ঘুরে যায় (ওপরের 705032704). Default int platform ভেদে আলাদা: এই পাতার ব্রাউজার-Python-এ int32, সাধারণ ৬৪-bit ডেস্কটপে int64. প্রতিকার: টাকার হিসাবে dtype=np.int64 স্পষ্ট লিখুন, বা sum(dtype=np.int64). pandas নিজে পূর্ণসংখ্যার কলাম int64-এ রাখে, কিন্তু .to_numpy() বা নিজে বানানো array-তে এই ফাঁদ ফেরে।
  • Float-এর সমান-পরীক্ষা: 0.1 + 0.2 == 0.3 False. তুলনায় np.isclose, আর টাকার হিসাবে পয়সায় পূর্ণসংখ্যা বা decimal.
  • float32-এর নির্ভুলতা: প্রায় ৭ অঙ্ক। বড় যোগফলে বা খুব আলাদা মাপের সংখ্যায় ভুল জমে। Memory বাঁচাতে float32 চলে, হিসাবের শেষ ধাপে float64 ভালো।

ছবির pixel-এ uint8-এর ঘুরে যাওয়াও একই পরিবারের (পর্ব ১৮)।

দুর্বল উত্তর: “Python-এর int-এর তো সীমা নেই।” সত্য, কিন্তু NumPy-র int-এর সীমা আছে; প্রশ্নটা NumPy নিয়ে।

প্র ৯Vectorization কেন দ্রুত, আর কখন loop-ই ঠিক? (মাঝারি)

ভালো উত্তর: Vectorization-এ পুরো array-র ওপর একটি কাজ C-তে লেখা loop-এ চলে: প্রতিটি উপাদানে Python interpreter-এর খরচ নেই, ডেটা পাশাপাশি memory-তে, আর CPU-র SIMD ব্যবহার হতে পারে। Broadcasting ভিন্ন আকারের array-কে loop ছাড়াই মেলায়। তবে তিন ক্ষেত্রে loop বা অন্য পথ ভালো: প্রতিটি ধাপ আগের ফলের ওপর নির্ভর করলে (যেমন জটিল recursion), broadcasting-এ মধ্যবর্তী array এত বড় হলে যে memory ফুরায় (অংশ ৭-এর সমস্যা ৫-এ এর হিসাব), আর ডেটা খুব ছোট হলে, যখন পড়ার সুবিধাই বড়। np.vectorize আসলে ভেতরে Python loop, গতি দেয় না (পর্ব ১৫, পর্ব ১৯)।

দুর্বল উত্তর: “NumPy সবসময় দ্রুত।” মাপা ছাড়া গতির দাবি ইন্টারভিউতে দুর্বল শোনায়।

৪ · pandas — Copy-on-Write, memory, time zone, merge, groupby আর missing value

রিভিশন: পর্ব ২০, পর্ব ২২, পর্ব ২৩, পর্ব ২৪, পর্ব ২৫, পর্ব ২৬, পর্ব ২৭

আউটপুট অনুমান: CoW, agg বনাম transform, আর merge-এর পরে সারির সংখ্যা

চালানোর আগে তিনটি প্রশ্নের উত্তর লিখুন: orders["taka"] বদলাবে কি? agg আর transform-এর ফলে কয়টি সারি? Merge-এর পরে কয়টি সারি আর মোট টাকা কত?

import pandas as pd
orders = pd.DataFrame({"order_id": [1, 2, 3, 4, 5, 6],
    "district": ["ঢাকা", "ঢাকা", "খুলনা", "খুলনা", "খুলনা", "রাজশাহী"],
    "taka": [500, 300, 200, 400, 600, 250]})
# Copy-on-Write: সাবসেট বদলালে মূল টেবিল বদলায় না
dhaka = orders[orders["district"] == "ঢাকা"]
dhaka.loc[:, "taka"] = 0
print("orders unchanged:", orders["taka"].tolist())
# agg → জেলাপ্রতি এক সারি; transform → প্রতিটি অর্ডারে এক মান
print(orders.groupby("district")["taka"].agg("sum"))
orders["share"] = orders["taka"] / orders.groupby("district")["taka"].transform("sum")
print(orders[["district", "taka", "share"]].round(2))
# merge-এ ডানের key দুবার থাকলে সারি বেড়ে যায়
zones = pd.DataFrame({"district": ["ঢাকা", "ঢাকা", "খুলনা", "রাজশাহী"],
                      "zone": ["মধ্য", "মধ্য-২", "দক্ষিণ", "উত্তর"]})
m = orders.merge(zones, on="district", how="left")
print("rows:", len(orders), "→", len(m), "| taka total:", orders["taka"].sum(), "→", m["taka"].sum())
try:
    orders.merge(zones, on="district", how="left", validate="many_to_one")
except pd.errors.MergeError as e:
    print("MergeError:", str(e).splitlines()[0])

সত্যিকারের আউটপুট:

orders unchanged: [500, 300, 200, 400, 600, 250]
district
খুলনা      1200
ঢাকা        800
রাজশাহী     250
Name: taka, dtype: int64
  district  taka  share
0     ঢাকা   500   0.62
1     ঢাকা   300   0.38
2    খুলনা   200   0.17
3    খুলনা   400   0.33
4    খুলনা   600   0.50
5  রাজশাহী   250   1.00
rows: 6 → 8 | taka total: 2250 → 3050
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge

প্রথম লাইন: pandas 3-এ Copy-on-Write একমাত্র আচরণ, তাই dhaka-য় লেখা মূল orders-এ পৌঁছায়নি। দ্বিতীয় অংশ: agg প্রতিটি জেলার জন্য একটি সারি দিল (৩টি), আর transform প্রতিটি অর্ডারের পাশে তার জেলার যোগফল বসাল (৬টি), তাই সরাসরি ভাগ করে অংশ বের করা গেল। শেষ অংশ: ডানের টেবিলে “ঢাকা” দুবার, তাই ঢাকার দুটি অর্ডার দুবার করে এল; সারি ৬ থেকে ৮, মোট টাকা ২২৫০ থেকে ৩০৫০. এটি row explosion; validate="many_to_one" একে সঙ্গে সঙ্গে error বানাল। নিচের দুটি stepper এই দুই ঘটনা ধাপে ধাপে দেখায়।

orders (৬ সারি) ⟕ zones, on="district"
যে অর্ডার মেলানো হচ্ছে
zones-এ মিল
ফলে নতুন সারি
চলমান টাকার যোগ
  1. অর্ডার ১, ঢাকা, ৫০০ টাকা. Zones-এ ঢাকার দুটি সারি, তাই এই অর্ডার দুবার আসে। চলমান যোগ ৫০০ + ৫০০ = ১০০০.
  2. অর্ডার ২, ঢাকা, ৩০০ টাকা. আবার দুটি মিল, আবার দুই সারি। যোগ ১০০০ + ৬০০ = ১৬০০.
  3. খুলনার তিনটি অর্ডার. খুলনার মিল একটি, তাই প্রতিটি একবার: ২০০ + ৪০০ + ৬০০ = ১২০০, যোগ ২৮০০.
  4. রাজশাহী, ২৫০ টাকা. একটি মিল, একটি সারি। যোগ ৩০৫০.
  5. ফল. ৬টি অর্ডার থেকে ৮টি সারি, আর মোট টাকা ৮০০ বেশি, ঠিক ঢাকার অর্ডারের যোগফল। প্রতিকার: merge-এর আগে ডানের key-তে is_unique দেখা, merge-এ validate, আর পরে সারির সংখ্যা মেলানো।
orders.groupby("district")["taka"]
ধাপ
ঢাকা
খুলনা
রাজশাহী
ফলের দৈর্ঘ্য
  1. Split. জেলা ধরে তিনটি দল: ঢাকায় দুটি, খুলনায় তিনটি, রাজশাহীতে একটি অর্ডার।
  2. Apply. প্রতিটি দলে sum: ৮০০, ১২০০, ২৫০.
  3. agg-এর combine. প্রতিটি দল থেকে একটি সারি: ৩ সারির একটি Series, জেলা তার index. রিপোর্টের জন্য এটিই চাই।
  4. transform-এর combine. একই যোগফল, কিন্তু প্রতিটি মূল সারির জায়গায় ফিরে বসে: ৬টি মান, মূল index-এ সাজানো। তাই মূল টেবিলে সরাসরি নতুন কলাম বানানো যায়।
  5. ব্যবহার. প্রতিটি অর্ডার তার জেলার কত অংশ: ৫০০ ÷ ৮০০ = ০.৬২৫, ছাপায় ০.৬২. agg দিয়ে করলে আগে merge লাগত; transform এক লাইনে করে।
প্র ১০pandas 3-এর Copy-on-Write কী বদলাল, আর chained assignment কেন আর কাজ করে না? (মাঝারি)

ভালো উত্তর: Copy-on-Write-এ নিয়ম সরল: যেকোনো indexing বা method-এর ফল ব্যবহারকারীর কাছে সবসময় copy-র মতো আচরণ করে। ভেতরে pandas memory ভাগ করে রাখে, আর সত্যিকারের copy করে শুধু তখন, যখন কেউ লিখতে যায়। ফলে দুটি বড় পরিবর্তন। প্রথমত, পুরোনো “SettingWithCopyWarning”-এর ধাঁধা নেই; সাবসেট বদলালে মূল টেবিল কখনো বদলায় না। দ্বিতীয়ত, chained assignment df[mask]["fee"] = 60 কখনো মূল টেবিল বদলায় না, pandas একটি ChainedAssignmentError সতর্কবার্তা দেয়। ঠিক লেখা এক ধাপে: df.loc[mask, "fee"] = 60, অথবা assign দিয়ে নতুন টেবিল। অংশ ৮-এর সমস্যা ১০-এ এটি চালিয়ে দেখবেন (পর্ব ২২)।

দুর্বল উত্তর: “সতর্কবার্তা এলে .copy() লাগিয়ে দিই।” এতে সতর্কবার্তা যায়, কিন্তু যে টেবিল বদলাতে চেয়েছিলেন সেটি তখনও বদলায় না।

প্র ১১একটি DataFrame কত memory নিচ্ছে কীভাবে মাপবেন, আর category dtype কখন সাহায্য করে? (মাঝারি)

ভালো উত্তর: মাপুন df.memory_usage(deep=True) বা df.info(memory_usage="deep") দিয়ে; deep ছাড়া লেখার কলামের আসল খরচ গোনা হয় না (deep memory usage). তারপর তিনটি উপায়: সংখ্যার কলাম pd.to_numeric(downcast=...) দিয়ে ছোট dtype-এ; বারবার ফিরে আসা অল্প কয়েকটি লেখা (জেলা, পেমেন্টের ধরন) category-তে, যেখানে প্রতিটি সারিতে শুধু একটি ছোট পূর্ণসংখ্যার code থাকে; আর যে কলাম লাগবে না তা পড়ার সময়ই বাদ (usecols). পর্ব ২৭-এ ২ লাখ সারির বানানো অর্ডার টেবিলে এই কাজে মোট 25.11 MB থেকে 2.4 MB হয়েছিল (ব্রাউজার-Python-এ একবারের মাপ)। প্রায় সব মান আলাদা হলে (order_id, ফোন নম্বর) category খারাপ পছন্দ।

দুর্বল উত্তর: মাপা ছাড়াই “RAM কম পড়লে বড় মেশিন নিই।”

প্র ১২বাজারঘরের অর্ডারের সময় UTC-তে রাখা। “দিনপ্রতি বিক্রি” কীভাবে বের করবেন? (মাঝারি)

ভালো উত্তর: সময় রাখুন UTC-তে, কিন্তু “দিন” মাপুন ব্যবসার ঘড়িতে। প্রথমে নিশ্চিত হোন কলামটি aware কি না; naive হলে tz_localize("UTC"), তারপর dt.tz_convert("Asia/Dhaka"), তারপর তারিখ ধরে groupby. বাংলাদেশ UTC+6, এখন daylight saving নেই (২০০৯ সালে অল্প সময়ের জন্য ছিল, পর্ব ২৬)। তাই UTC-তে groupby করলে ঢাকার রাত ১২টা থেকে সকাল ৬টার বিক্রি আগের দিনে চলে যায় (অংশ ৮-এর বাগ গ)। ব্রাউজার-Python-এ zone-এর তথ্যের জন্য আগে import tzdata লাগে।

দুর্বল উত্তর: “সব সময় থেকে ৬ ঘণ্টা যোগ করে দিই।” হাতে যোগ করলে কলামটি তখনও UTC বলে দাবি করে, আর অন্য দেশের zone বা ইতিহাসের DST-তে ভুল হয়।

প্র ১৩Merge-এর পরে সারি বেড়ে গেল। কেন, আর কীভাবে আটকাবেন? (মাঝারি, প্রায়ই আসে)

ভালো উত্তর: যে টেবিলকে lookup ভাবছেন তার key অনন্য নয়, তাই প্রতিটি মিলের জন্য একটি করে সারি তৈরি হয় (ওপরের stepper). চারটি অভ্যাস: merge-এর আগে right["key"].is_unique দেখা; validate="many_to_one" বা "one_to_one" দেওয়া, যাতে ভুল হলে MergeError; indicator=True দিয়ে কোন সারি কোথা থেকে মিলল দেখা; আর merge-এর আগে-পরে len ও মোট টাকা মেলানো। (পর্ব ২৫)।

দুর্বল উত্তর: “পরে drop_duplicates() করে দিই।” কোন সারি রাখলেন আর কেন, তা না জেনে মুছলে আরেকটি নীরব ভুল তৈরি হয়।

প্র ১৪groupby-তে agg, transform, filter আর apply-এর পার্থক্য কী? (মাঝারি)

ভালো উত্তর: ফলের আকার দিয়ে মনে রাখুন। agg: প্রতিটি দলে একটি সারি, রিপোর্টের জন্য (named aggregation agg(total=("taka", "sum")) পড়তে সহজ)। transform: মূল টেবিলের সমান দৈর্ঘ্য, প্রতিটি সারিতে তার দলের মান; অংশ, দলের ভেতরে z-score, বা দলের গড় দিয়ে ফাঁকা ঘর ভরতে। filter: পুরো দল রাখা বা বাদ দেওয়া, যেমন “যে জেলায় ৩টির কম অর্ডার তা বাদ”। apply: যেকোনো function, সবচেয়ে নমনীয় কিন্তু সাধারণত সবচেয়ে ধীর, আর ফলের আকার অনুমান করা কঠিন; আগে বাকি তিনটি দিয়ে চেষ্টা করুন (পর্ব ২৪)।

দুর্বল উত্তর: “সব কাজ apply দিয়ে করি।” কাজ হয়, কিন্তু বড় ডেটায় ধীর, আর ইন্টারভিউয়ার জানতে চান আপনি সঠিক টুল চেনেন কি না।

প্র ১৫Vectorised কলাম-হিসাব আর apply(axis=1) — পার্থক্য আর মাপ। (মাঝারি)

ভালো উত্তর: df.apply(f, axis=1) প্রতিটি সারির জন্য একটি Series বানিয়ে Python function ডাকে, তাই সারির সংখ্যার সাথে সাথে Python-এর খরচ বাড়ে। একই নিয়ম পুরো কলামে np.where, np.select, তুলনা আর গাণিতিক চিহ্ন দিয়ে লিখলে কাজটি C-তে হয়। অংশ ৭-এর সমস্যা ৮-এ ২০,০০০ সারিতে দুটি পথ মেপে দেখবেন। তবে আগে সঠিকতা: দুটির ফল np.allclose দিয়ে মিলিয়ে তারপর গতির কথা। (পর্ব ২৭)।

দুর্বল উত্তর: “iterrows দিয়ে loop করি।” এটি সাধারণত আরও ধীর।

প্র ১৬Missing value কীভাবে সামলাবেন? “সব NaN-এ ০ বসিয়ে দিই” কেন দুর্বল? (মাঝারি)

ভালো উত্তর: আগে মাপুন, পরে কারণ খুঁজুন, শেষে সিদ্ধান্ত। df.isna().mean() দিয়ে কলামপ্রতি অনুপাত। তারপর প্রশ্ন: কেন ফাঁকা? ডেলিভারির দিন ফাঁকা কারণ অর্ডারটি এখনো পৌঁছায়নি, আর বিক্রি ফাঁকা কারণ দোকান বন্ধ ছিল, এ দুটি আলাদা ঘটনা (MCAR, MAR, MNAR, পর্ব ২৩)। বিকল্প: সারি বাদ (অল্প হলে), median বা দলের median দিয়ে ভরা, “ফাঁকা ছিল” নামে একটি missing indicator কলাম যোগ, অথবা model-এ SimpleImputer, যা শুধু train দেখে শেখে (পর্ব ৬৪)। pandas 3-এ পূর্ণসংখ্যার কলামে ফাঁকা ঘর রাখতে nullable "Int64"; সাধারণ int-এ রূপান্তর NaN পেলে error দেয় (বাগ ঘ)।

দুর্বল উত্তর: সব NaN-এ ০. “অজানা” আর “শূন্য” মিশে গেলে গড়, model আর রিপোর্ট সবই নীরবে বেঁকে যায়।

৫ · পুনরুৎপাদনযোগ্যতা আর ML-এর কাজের ধারা — seed, lockfile, split আর pipeline

রিভিশন: পর্ব ৭, পর্ব ১১, পর্ব ১২, পর্ব ৫৬, পর্ব ৬৩, পর্ব ৬৪, পর্ব ৬৬

প্র ১৭“আপনার notebook অন্যের মেশিনে একই ফল দেবে” — কীভাবে নিশ্চিত করবেন? (মাঝারি)

ভালো উত্তর: তিন স্তরে বলুন। কোড আর এলোমেলোতা: একটি np.random.default_rng(42) বানিয়ে সেটিই সবখানে পাঠানো, আর scikit-learn-এ প্রতিটি এলোমেলো ধাপে random_state (seed); notebook উপর থেকে নিচে “Restart & Run All” করে চলে কি না দেখা (পর্ব ৮)। পরিবেশ: uv-তে uv.lock নামের lockfile প্রতিটি package-এর ঠিক version আর hash লিখে রাখে; অন্য মেশিনে uv sync --locked ঠিক সেই পরিবেশ বানায়, আর lockfile পুরোনো হলে থেমে যায়। Python-এর version-ও লিখে রাখুন (পর্ব ৭)। ডেটা: ডেটার hash বা version, আর কোন ফাইল থেকে কোন ফল, তা লিখে রাখা (পর্ব ১২)।

সৎ সীমাও বলুন: একই seed-এও ভিন্ন CPU বা BLAS-এ দশমিকের শেষ অঙ্কে পার্থক্য আসতে পারে, তাই test-এ np.isclose.

দুর্বল উত্তর: “requirements.txt-এ package-এর নাম আছে।” Version আর transitive dependency ছাড়া এটি ছয় মাস পরে অন্য পরিবেশ দেয়।

প্র ১৮Data leakage-এর তিনটি সাধারণ রূপ, কোডের ভাষায়। (মাঝারি)

ভালো উত্তর: Data leakage মানে মূল্যায়নের সময় model এমন তথ্য পায় যা আসল ব্যবহারে পাবে না। কোডে তিনটি চেনা রূপ: (১) ভাগের আগে পুরো ডেটায় fit: scaler, imputer, feature বাছাই (preprocessing leakage; অংশ ৮-এর সমস্যা ৯); (২) একই গ্রাহক বা একই পণ্যের সারি train আর test দুই দিকে (সমস্যা ৬-এর group split); (৩) সময়ের ডেটায় ভবিষ্যৎ ঢুকে পড়া: এলোমেলো ভাগ, বা shift(-1) feature-এ (temporal leakage). লক্ষণ প্রায় সবসময় একই: অবিশ্বাস্য ভালো score. প্রতিকার: আগে ভাগ, সব শেখার ধাপ Pipeline-এর ভেতরে, আর baseline-এর সাথে তুলনা (পর্ব ৫৬)। ধারণাগত দিক আর model-এর যুক্তি আরও আছে পর্ব ৭৯-এ।

দুর্বল উত্তর: “Test set আলাদা রাখলেই leakage হয় না।” আলাদা রাখার আগে কী কী দেখা হয়েছে, সেটিই আসল প্রশ্ন।

৬ · Live coding ১ — pandas-এর চারটি সমস্যা, test-সহ

রিভিশন: পর্ব ২৪, পর্ব ২৬, পর্ব ২৩, পর্ব ৬৭, পর্ব ১০

প্রতিটি সমস্যায় একই ছক: প্রশ্ন আর শর্ত, যা জিজ্ঞেস করবেন, জোরে বলার পরিকল্পনা, সমাধান, তারপর assert দিয়ে test. ইন্টারভিউতে কোড একটু লম্বা হলেও ক্ষতি নেই; test না থাকাটাই ক্ষতি।

সমস্যা ১ · জেলাপ্রতি সবচেয়ে বেশি বিক্রি হওয়া k-টি পণ্য

প্রশ্ন: “orders টেবিলে district, product, qty. প্রতিটি জেলায় মোট পরিমাণে শীর্ষ ২টি পণ্য বের করুন।” শর্ত: loop নয়, pandas দিয়ে; ফল একটি DataFrame; সমান হলে পণ্যের নামের ক্রমে।

যা জিজ্ঞেস করবেন: সমান মান হলে দুটিই রাখব, না একটি? কোনো জেলায় k-এর কম পণ্য থাকলে? খালি টেবিলে কী ফেরত দেব?

পরিকল্পনা জোরে বলুন: “আগে (জেলা, পণ্য) ধরে qty যোগ, তাহলে প্রতিটি জোড়া এক সারি। তারপর জেলা, তারপর পরিমাণ কমতে, তারপর নাম ধরে sort. শেষে প্রতিটি জেলা থেকে প্রথম k সারি: groupby().head(k). Test-এ একটি সরল dict-এর loop দিয়ে একই ফল মিলাব।”

import numpy as np
import pandas as pd
rng = np.random.default_rng(7)
orders = pd.DataFrame({
    "district": rng.choice(["ঢাকা", "চট্টগ্রাম", "খুলনা"], 60),
    "product": rng.choice(["চা", "মধু", "গামছা", "ইলিশ", "শাড়ি"], 60),
    "qty": rng.integers(1, 6, 60)})

def top_k(df, k=2):
    t = df.groupby(["district", "product"], as_index=False)["qty"].sum()
    t = t.sort_values(["district", "qty", "product"],
                      ascending=[True, False, True])   # সমান হলে নামের ক্রম
    return t.groupby("district").head(k).reset_index(drop=True)

res = top_k(orders)
print(res)

# test: ধীর কিন্তু নিশ্চিত পথে মিলিয়ে দেখা
for d, g in orders.groupby("district"):
    tot = {}
    for p, q in zip(g["product"], g["qty"]):
        tot[p] = tot.get(p, 0) + q
    best = sorted(tot.items(), key=lambda kv: (-kv[1], kv[0]))[:2]
    got = res[res["district"] == d][["product", "qty"]].values.tolist()
    assert got == [list(b) for b in best], d
assert top_k(orders.iloc[:0]).empty           # খালি ইনপুট
print("all tests passed")

সত্যিকারের আউটপুট:

    district product  qty
0      খুলনা     মধু   21
1      খুলনা      চা   20
2  চট্টগ্রাম   গামছা   19
3  চট্টগ্রাম    ইলিশ   14
4       ঢাকা     মধু   19
5       ঢাকা    ইলিশ   12
all tests passed

প্রতিটি জেলায় দুটি সারি, পরিমাণ কমতে। Test দুই স্তরে: সহজে বিশ্বাসযোগ্য dict-এর loop একই উত্তর দিল, আর খালি টেবিলে function ভাঙল না। তৃতীয় sort key “product” থাকায় সমান পরিমাণেও ফল স্থির।

প্র ১৯ফলো-আপ: সমান মান হলে সবাইকে রাখতে চাইলে? জটিলতা কত? (মাঝারি)

উত্তর: সমান মান রাখতে t["rk"] = t.groupby("district")["qty"].rank(method="dense", ascending=False), তারপর t[t["rk"] <= k]; তখন কোনো জেলায় k-এর বেশি সারি আসতে পারে, সেটি আগে বলে নিন। জটিলতা: groupby-যোগ প্রায় O(n), যোগের পরে m টি (জেলা, পণ্য) জোড়ার sort O(m log m), memory O(m). m খুব বড় আর k ছোট হলে পুরো sort না করে দলপ্রতি top-k বের করা যায় (NumPy-র argpartition-এর ধারণা, পর্ব ১৬)। SQL-এ একই কাজ ROW_NUMBER() দিয়ে (DBMS · window function).

সমস্যা ২ · ফাঁকা দিন ভরে ৭ দিনের চলমান বিক্রি

প্রশ্ন: “দৈনিক অর্ডারের টেবিলে কিছু দিন নেই, আবার কোনো দিনে একাধিক অর্ডার। প্রতিদিনের মোট বিক্রি আর শেষ ৭ দিনের যোগফল দিন।” শর্ত: প্রতিটি তারিখের একটি সারি; পূর্ণ ৭ দিন না হলে ফাঁকা।

যা জিজ্ঞেস করবেন: যে দিন নেই, সেদিন বিক্রি শূন্য, নাকি ডেটা হারিয়েছে? ৭ দিন মানে ৭টি সারি, নাকি ৭টি ক্যালেন্ডার দিন? তারিখে সময় আর time zone আছে কি?

পরিকল্পনা জোরে বলুন: “দোকান খোলা ছিল কিন্তু অর্ডার আসেনি, ধরে নিচ্ছি, তাই ফাঁকা দিন = ০. আগে তারিখ ধরে যোগ, তারপর প্রথম থেকে শেষ দিন পর্যন্ত একটি পূর্ণ date_range দিয়ে reindex, তারপর rolling(7). Test-এ হাতে যোগ করা দুটি জানালা।”

import pandas as pd
sales = pd.DataFrame({
    "date": pd.to_datetime(["2026-04-01", "2026-04-02", "2026-04-02", "2026-04-04",
                            "2026-04-05", "2026-04-08", "2026-04-09"]),
    "taka": [1200, 500, 300, 1500, 700, 900, 1100]})

def rolling_7d(df):
    daily = df.groupby("date")["taka"].sum()          # একই দিনের অর্ডার যোগ
    days = pd.date_range(daily.index.min(), daily.index.max(), freq="D")
    daily = daily.reindex(days, fill_value=0)         # ফাঁকা দিন = ০ বিক্রি
    return pd.DataFrame({"taka": daily,
                         "roll7": daily.rolling(7, min_periods=7).sum()})

out = rolling_7d(sales)
print(out)

assert len(out) == 9                                   # ১ থেকে ৯ এপ্রিল, সব দিন
assert out.loc["2026-04-07", "roll7"] == 4200          # ১–৭ এপ্রিল
assert out.loc["2026-04-08", "roll7"] == 3900          # ২–৮ এপ্রিল
alt = sales.set_index("date")["taka"].rolling("7D").sum()
print("time-based window, last row:", alt.iloc[-1])
print("all tests passed")

সত্যিকারের আউটপুট:

            taka   roll7
2026-04-01  1200     NaN
2026-04-02   800     NaN
2026-04-03     0     NaN
2026-04-04  1500     NaN
2026-04-05   700     NaN
2026-04-06     0     NaN
2026-04-07     0  4200.0
2026-04-08   900  3900.0
2026-04-09  1100  4200.0
time-based window, last row: 4200.0
all tests passed

৩, ৬ আর ৭ এপ্রিল আসল টেবিলে ছিল না, এখন ০ দিয়ে আছে, আর ২ এপ্রিলের দুটি অর্ডার (৫০০ + ৩০০) এক সারিতে ৮০০. প্রথম ছয় দিনে পূর্ণ জানালা নেই, তাই NaN. শেষ লাইনের বিকল্প rolling("7D") ক্যালেন্ডারের ৭ দিন ধরে যোগ করে, তাই শেষ দিনে একই ৪২০০; তবে তাতে ফাঁকা দিনের সারি থাকে না।

daily (১–৯ এপ্রিল), rolling(7).sum()
ধাপ
দৈনিক সারি
জানালার হিসাব
roll7
  1. তারিখ ধরে যোগ. সাতটি অর্ডার থেকে ছয়টি তারিখ, কারণ ২ এপ্রিলে দুটি অর্ডার, ৫০০ আর ৩০০, মিলে ৮০০.
  2. ফাঁকা দিন ভরা. ১ থেকে ৯ এপ্রিলের পূর্ণ তালিকায় reindex; ৩, ৬ আর ৭ এপ্রিলে ০ বসল। এখন ৯টি সারি। পূর্ণ ৭ দিন না হওয়ায় প্রথম ছয়টি roll7 ফাঁকা।
  3. প্রথম পূর্ণ জানালা. ১ থেকে ৭ এপ্রিল: ১২০০ + ৮০০ + ০ + ১৫০০ + ৭০০ + ০ + ০ = ৪২০০.
  4. এক দিন সরল. ১ এপ্রিলের ১২০০ বেরিয়ে গেল, ৮ এপ্রিলের ৯০০ ঢুকল: ৩৯০০. ফাঁকা দিন না ভরে সারি ধরে ৭টি নিলে এই জানালা ভুল করে ১ এপ্রিলকেও রাখত।
  5. আরেক দিন. ২ এপ্রিলের ৮০০ বেরোল, ৯ এপ্রিলের ১১০০ ঢুকল: ৪২০০. প্রতিটি ধাপে একটি বিয়োগ আর একটি যোগ, তাই চলমান যোগফলের খরচ O(n).
প্র ২০ফলো-আপ: “ফাঁকা দিন মানে ডেটা হারিয়েছে” হলে কী বদলাবেন? জটিলতা? (মাঝারি)

উত্তর: তখন ০ বসানো ভুল, কারণ তা বিক্রি কম দেখায়। বিকল্প: ফাঁকাকে NaN রাখা আর rolling(7, min_periods=5) দিয়ে কমপক্ষে ৫টি আসল দিনে গড়, অথবা interpolation, আর রিপোর্টে লেখা কোন দিন অনুমানে ভরা। সময়সহ ডেটা হলে আগে Asia/Dhaka-য় রূপান্তর (প্র ১২)। জটিলতা: groupby O(n), reindex আর rolling যোগ O(d), যেখানে d দিনের সংখ্যা; memory O(d). Forecasting-এর feature বানালে shift(1) দিয়ে আজকের মান বাদ দিন, নইলে temporal leakage (পর্ব ২৬)।

সমস্যা ৩ · অর্ডারের ডুপ্লিকেট সরিয়ে প্রতিটির সর্বশেষ অবস্থা রাখা

প্রশ্ন: “events টেবিলে একই order_id কয়েকবার এসেছে, প্রতিবার নতুন status আর updated_at. প্রতিটি অর্ডারের সর্বশেষ সারিটি রাখুন।” শর্ত: ফলের ক্রম ইনপুটের ক্রমের ওপর নির্ভর করবে না।

যা জিজ্ঞেস করবেন: একই অর্ডারের দুটি সারির সময় হুবহু সমান হলে কোনটি? সময় ফাঁকা থাকতে পারে কি? সময় কোন zone-এ?

পরিকল্পনা জোরে বলুন: “order_id, তারপর সময়, তারপর সারি আসার ক্রম seq ধরে sort, যাতে সমান সময়েও উত্তর একটিই। তারপর drop_duplicates(keep="last"). Test-এ প্রত্যাশিত অবস্থা হাতে লিখব, ইনপুট উল্টে দিয়েও একই ফল চাইব।”

import pandas as pd
events = pd.DataFrame({
    "order_id": [101, 102, 101, 103, 102, 101, 103],
    "status": ["placed", "placed", "paid", "placed", "cancelled", "delivered", "paid"],
    "updated_at": pd.to_datetime([
        "2026-04-01 10:00", "2026-04-01 10:05", "2026-04-01 12:00", "2026-04-02 09:00",
        "2026-04-01 11:00", "2026-04-03 16:30", "2026-04-02 09:00"]),
    "seq": [1, 2, 3, 4, 5, 6, 7]})        # সারি আসার ক্রম, সমান সময় ভাঙতে

def latest(df):
    return (df.sort_values(["order_id", "updated_at", "seq"])
              .drop_duplicates("order_id", keep="last")
              .reset_index(drop=True))

out = latest(events)
print(out)

assert out["order_id"].is_unique
assert dict(zip(out["order_id"], out["status"])) == {
    101: "delivered", 102: "cancelled", 103: "paid"}
# বিকল্প পথ: সময় ধরে সাজিয়ে প্রতিটি দলের শেষ সারি
alt = events.sort_values(["updated_at", "seq"]).groupby("order_id").tail(1)
assert set(zip(alt["order_id"], alt["status"])) == set(zip(out["order_id"], out["status"]))
assert latest(events.iloc[::-1]).equals(out)   # ইনপুটের ক্রমে ফল বদলায় না
print("all tests passed")

সত্যিকারের আউটপুট:

   order_id     status          updated_at  seq
0       101  delivered 2026-04-03 16:30:00    6
1       102  cancelled 2026-04-01 11:00:00    5
2       103       paid 2026-04-02 09:00:00    7
all tests passed

অর্ডার ১০৩-এ দুটি সারির সময় হুবহু এক (২ এপ্রিল সকাল ৯টা)। শুধু সময় ধরে sort করলে বিজয়ী ইনপুটের ক্রমের ওপর নির্ভর করত; seq থাকায় পরে আসা “paid” জিতল, আর ইনপুট উল্টে দিলেও ফল হুবহু এক।

প্র ২১ফলো-আপ: ৫ কোটি সারি হলে? সময় না থাকলে? জটিলতা? (মাঝারি)

উত্তর: জটিলতা: sort O(n log n), drop_duplicates hash দিয়ে প্রায় O(n), memory O(n). বিকল্প df.loc[df.groupby("order_id")["updated_at"].idxmax()] sort ছাড়াই O(n), তবে সমান সময়ে প্রথমটিকে নেয়, তাই tie-এর নিয়ম আলাদা করে ভাবতে হয়। খুব বড় ডেটায় শুধু দরকারি কলাম পড়া, Parquet, অথবা database-এ window function (পর্ব ২৭)। সময় না থাকলে “সর্বশেষ” কী তা বলা যায় না; ফাইলের সারির ক্রমের ওপর ভরসা করা ঝুঁকি, তাই আগে জিজ্ঞেস করুন। আর ডুপ্লিকেট মোছার আগে গুনে রাখুন কয়টি গেল, যাতে রিপোর্টে লেখা যায় (পর্ব ২৩)।

সমস্যা ৪ · এলোমেলো বাংলা জেলার কলাম পরিষ্কার করা

প্রশ্ন: “গ্রাহকেরা জেলার নাম হাতে লেখেন: ‘ ঢাকা ’, ‘Dhaka’, ‘ঢাকা।’, ‘CTG’, ভুল বানান, আর কখনো দেখতে একই কিন্তু ভেতরে আলাদা অক্ষর। একটি function লিখুন যা আসল নাম ফেরত দেয়, আর অজানাগুলো আলাদা করে।” শর্ত: অজানা মান অনুমানে বসানো যাবে না।

যা জিজ্ঞেস করবেন: আসল জেলার তালিকা কোথায়? অজানা হলে NaN, নাকি আলাদা “অন্যান্য”? ইংরেজি আর বাংলা দুই রূপই আসে কি?

পরিকল্পনা জোরে বলুন: “প্রথমে Unicode NFC normalization, কারণ ‘ড়’ একটি অক্ষরে বা ‘ড’ আর নুক্তা দুটি অক্ষরে লেখা যায়, দেখতে একই। তারপর zero-width অক্ষর মোছা, দুই পাশের ফাঁকা জায়গা আর দাঁড়ি কাটা, ইংরেজিকে ছোট হাতের অক্ষরে। তারপর একটি alias dict, শেষে allow-list-এ না থাকলে NaN.”

import unicodedata
import pandas as pd
bogura_1 = "বগু" + chr(0x09DC) + "া"               # ড় একটি অক্ষর
bogura_2 = "বগু" + chr(0x09A1) + chr(0x09BC) + "া"  # ড + নুক্তা, দেখতে একই
raw = pd.Series([" ঢাকা ", "Dhaka", "ঢাকা।", "dhaka city", "চট্রগ্রাম", "CTG",
                 bogura_1, bogura_2, None, "ঢাকা" + chr(0x200C), "কুমিল্লা"])
ZW = dict.fromkeys([0x200C, 0x200D])        # অদৃশ্য zero-width অক্ষর মুছতে

def norm(x):
    x = unicodedata.normalize("NFC", x).translate(ZW)
    return x.strip(" ।.,").casefold()

ALIASES = {norm(k): norm(v) for k, v in {
    "dhaka": "ঢাকা", "dhaka city": "ঢাকা", "ctg": "চট্টগ্রাম",
    "chattogram": "চট্টগ্রাম", "চট্রগ্রাম": "চট্টগ্রাম", "bogura": bogura_1}.items()}
KNOWN = {norm(k) for k in ["ঢাকা", "চট্টগ্রাম", "খুলনা", bogura_1]}

def clean_district(s):
    t = s.dropna().map(norm).replace(ALIASES)
    return t.where(t.isin(KNOWN)).reindex(s.index)   # অজানা → NaN

clean = clean_district(raw)
print(pd.DataFrame({"raw": raw, "clean": clean}))
assert bogura_1 != bogura_2 and clean[6] == clean[7]
assert set(clean.dropna()) <= KNOWN
print(clean.value_counts(dropna=False).to_dict())
print("needs review:", raw[clean.isna() & raw.notna()].tolist())

সত্যিকারের আউটপুট:

           raw      clean
0        ঢাকা        ঢাকা
1        Dhaka       ঢাকা
2        ঢাকা।       ঢাকা
3   dhaka city       ঢাকা
4    চট্রগ্রাম  চট্টগ্রাম
5          CTG  চট্টগ্রাম
6        বগুড়া     বগুড়া
7       বগুড়া     বগুড়া
8          NaN        NaN
9        ঢাকা‌       ঢাকা
10    কুমিল্লা        NaN
{'ঢাকা': 5, 'চট্টগ্রাম': 2, 'বগুড়া': 2, nan: 2}
needs review: ['কুমিল্লা']

সারি ৬ আর ৭ পর্দায় হুবহু একই “বগুড়া”, কিন্তু Python-এর কাছে আলাদা: প্রথমটিতে ‘ড়’ একটি code point (U+09DC), দ্বিতীয়টিতে ‘ড’ আর নুক্তা (U+09A1 U+09BC). NFC দুটিকে এক রূপে আনল, তাই assert পার হলো। সারি ৯-এ অদৃশ্য zero-width non-joiner ছিল, না মুছলে তুলনা ব্যর্থ হতো। “কুমিল্লা” আসল জেলা, কিন্তু এই ছোট allow-list-এ নেই, তাই অনুমানে না বসিয়ে “needs review” তালিকায়।

প্র ২২ফলো-আপ: ৬৪ জেলার পুরো তালিকা আর হাজারো ভুল বানান হলে? (মাঝারি)

উত্তর: ধাপগুলো একই থাকে, শুধু alias-এর উৎস বদলায়। প্রথমে value_counts() দেখে সবচেয়ে বেশি আসা ভুলগুলো হাতে ম্যাপ করুন। বাকিদের জন্য string similarity (যেমন standard library-র difflib.get_close_matches) দিয়ে প্রস্তাব বানানো যায়, কিন্তু তা মানুষে যাচাই করে alias-এ যোগ হবে, সরাসরি নয়: “বরগুনা” আর “বগুড়া”-র মতো কাছাকাছি নাম ভুল জোড়া লাগতে পারে। প্রতিটি পরিবর্তন একটি cleaning log-এ রাখুন। জটিলতা: প্রতিটি মানে normalization আর dict-খোঁজা, মোট O(n × l), l লেখার দৈর্ঘ্য; একই মান বারবার এলে আগে unique()-এ কাজ করে পরে map দ্রুত (পর্ব ২৩, পর্ব ৬৭, পর্ব ১০)।

৭ · Live coding ২ — NumPy আর ML-এর চারটি সমস্যা

রিভিশন: পর্ব ১৫, পর্ব ১৭, পর্ব ৫৭, পর্ব ৫৬, পর্ব ৬৪, পর্ব ২৭

সমস্যা ৫ · Loop ছাড়া pairwise distance matrix

প্রশ্ন: “X-এর আকার (n, d): বাজারঘরের n টি গুদামের অবস্থান। প্রতিটি জোড়ার Euclidean distance দিয়ে (n, n) আকারের distance matrix দিন।” শর্ত: Python loop নয়; শুধু NumPy.

যা জিজ্ঞেস করবেন: n কত বড় হতে পারে? দূরত্ব কোন মাপে, সরলরেখা না রাস্তা? float32 চলবে কি?

পরিকল্পনা জোরে বলুন: “একটি নতুন axis যোগ করে broadcasting: (n, 1, d) থেকে (1, n, d) বিয়োগ করলে (n, n, d) আকারের সব পার্থক্য। বর্গ করে শেষ axis-এ যোগ, তারপর বর্গমূল। দ্বিতীয় রূপে কম memory-র সূত্র। Test: দুই-loop-এর সরল রূপ, matrix প্রতিসম, কর্ণে শূন্য।”

Broadcasting দিয়ে সব জোড়ার পার্থক্য

X[:, None, :] − X[None, :, :] → shape (4, 4, 2) → sum over last axis → (4, 4)

Pairwise distance-এর broadcasting (4, 1, 2) x0x1x2x3 − (1, 4, 2) x0x1x2x3 প্রসারিত হয়ে (4, 4, 2) ঘর (i, j) =xi − xj(Δx, Δy) √∑(·)² (4, 4) D 02.752.750 00 কর্ণ ০, প্রতিসম
ছবি ৭৮.২ — নতুন axis (None) দুটি array-কে ভিন্ন দিকে প্রসারিত করে, তাই একটি বিয়োগেই সব জোড়া। নিচের ব্লকের আসল X দিয়ে D[0, 1] = 2.75. খরচ মধ্যবর্তী (n, n, d) array-তে: n = 10,000 আর d = 2 হলে float64-এ 10⁴ × 10⁴ × 2 × 8 B = 1.6 GB.
import numpy as np
rng = np.random.default_rng(42)
X = np.round(rng.random((4, 2)) * 10, 1)   # ৪টি গুদামের (x, y), কিলোমিটারে
print("X =\n", X)

def pairwise(X):
    diff = X[:, None, :] - X[None, :, :]     # (n,1,d) - (1,n,d) → (n,n,d)
    return np.sqrt((diff ** 2).sum(axis=-1)) # → (n,n)

def pairwise_gram(X):                        # কম memory: |a|² + |b|² − 2a·b
    sq = (X ** 2).sum(axis=1)
    d2 = sq[:, None] + sq[None, :] - 2 * X @ X.T
    return np.sqrt(np.maximum(d2, 0))        # ছোট ঋণাত্মক rounding কাটা

D = pairwise(X)
print("D =\n", D.round(2))
# test: সরল দুই-loop-এর সাথে মেলানো, আর distance-এর ধর্ম
n = len(X)
slow = np.array([[np.sqrt(((X[i] - X[j]) ** 2).sum()) for j in range(n)] for i in range(n)])
assert np.allclose(D, slow) and np.allclose(D, pairwise_gram(X))
assert np.allclose(D, D.T) and np.allclose(np.diag(D), 0)
print("nearest to warehouse 0:", np.argsort(D[0])[1])
print("all tests passed")

সত্যিকারের আউটপুট:

X =
 [[7.7 4.4]
 [8.6 7. ]
 [0.9 9.8]
 [7.6 7.9]]
D =
 [[0.   2.75 8.68 3.5 ]
 [2.75 0.   8.19 1.35]
 [8.68 8.19 0.   6.96]
 [3.5  1.35 6.96 0.  ]]
nearest to warehouse 0: 1
all tests passed

হাতে একটি ঘর মিলিয়ে নিন: গুদাম ০ (7.7, 4.4) আর ১ (8.6, 7.0)-এর পার্থক্য (0.9, 2.6), দূরত্ব √(0.81 + 6.76) = √7.57 ≈ 2.75. Test তিনটি: দুই-loop-এর রূপের সাথে মিল, “গ্রাম” সূত্রের (|a|² + |b|² − 2a·b) সাথে মিল, আর দূরত্বের ধর্ম। গ্রাম সূত্রে rounding-এ খুব ছোট ঋণাত্মক সংখ্যা আসতে পারে, তাই বর্গমূলের আগে np.maximum(d2, 0).

প্র ২৩ফলো-আপ: দুটি রূপের জটিলতা, আর n = ১ লাখ হলে? (কঠিন)

উত্তর: সময় দুটিতেই O(n² d). Memory আলাদা: broadcasting রূপ মধ্যবর্তী (n, n, d) array বানায়, O(n² d); গ্রাম রূপ শুধু (n, n), O(n²), আর X @ X.T ভেতরে দ্রুত BLAS ব্যবহার করে (পর্ব ১৭)। বিনিময়ে গ্রাম রূপে প্রায় সমান দুটি বিন্দুর দূরত্বে catastrophic cancellation-এর ঝুঁকি। n = ১ লাখ হলে ফল নিজেই 10¹⁰ ঘর, float64-এ ৮০ GB: পুরো matrix কোনো রূপেই চলবে না। তখন প্রশ্নটাই বদলান: “প্রতিটি গুদামের সবচেয়ে কাছের k টি” চাই কি? তাহলে ব্লক ধরে (যেমন ১০০০ সারি করে) হিসাব, অথবা k-d tree-এর মতো সূচি (পর্ব ৫৭)। বাস্তবে scipy.spatial.distance.cdist আছে; অক্ষাংশ-দ্রাঘিমাংশ হলে haversine দূরত্ব লাগে।

সমস্যা ৬ · গ্রাহক ধরে train আর test ভাগ

প্রশ্ন: “অর্ডারের টেবিলে একই গ্রাহকের অনেক সারি। গ্রাহক ধরে ভাগ করুন, যাতে কোনো গ্রাহক train আর test দুই দিকে না থাকে।” শর্ত: প্রায় ২০% গ্রাহক test-এ; seed দিলে ফল একই; scikit-learn ছাড়া নিজে লিখুন, পরে তুলনা।

যা জিজ্ঞেস করবেন: ২০% সারি, নাকি ২০% গ্রাহক? Label-এর অনুপাতও মেলাতে হবে কি? সময়ের ক্রম কি গুরুত্বপূর্ণ?

পরিকল্পনা জোরে বলুন: “অনন্য গ্রাহক নিয়ে sort, যাতে ক্রম স্থির। নিজের default_rng(seed) দিয়ে replace ছাড়া ২০% বাছাই। তারপর isin দিয়ে সারি ভাগ। Test: দুই দিকের গ্রাহক আলাদা, সারির যোগফল মেলে।”

import numpy as np
import pandas as pd
from sklearn.model_selection import GroupShuffleSplit
rng = np.random.default_rng(42)
orders = pd.DataFrame({"customer": rng.integers(1, 21, 100),   # ২০ জন ক্রেতা, ১০০ অর্ডার
                       "taka": rng.integers(100, 2000, 100)})

def group_split(df, col, test_frac=0.2, seed=42):
    ids = np.sort(df[col].unique())
    r = np.random.default_rng(seed)
    test_ids = r.choice(ids, size=max(1, round(len(ids) * test_frac)), replace=False)
    is_test = df[col].isin(test_ids)
    return df[~is_test], df[is_test]

train, test = group_split(orders, "customer")
print("train rows:", len(train), "| test rows:", len(test))
print("test customers:", sorted(test["customer"].unique().tolist()))
assert set(train["customer"]).isdisjoint(test["customer"])     # কেউ দুই দিকে নেই
assert len(train) + len(test) == len(orders)
assert test["customer"].nunique() == 4
# লাইব্রেরির পথ: একই নিয়ম, ভিন্ন এলোমেলো বাছাই
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
tr_idx, te_idx = next(gss.split(orders, groups=orders["customer"]))
assert set(orders.iloc[tr_idx]["customer"]).isdisjoint(orders.iloc[te_idx]["customer"])
print("GroupShuffleSplit test customers:", sorted(orders.iloc[te_idx]["customer"].unique().tolist()))

সত্যিকারের আউটপুট:

train rows: 73 | test rows: 27
test customers: [2, 9, 13, 14]
GroupShuffleSplit test customers: [1, 2, 16, 18]

২০ জনের ৪ জন test-এ, কিন্তু সারি ২৭টি, ২০টি নয়: group split-এ সারির অনুপাত গ্রাহকের অনুপাতের সমান হয় না। এটি নিজে থেকে বলার মতো পর্যবেক্ষণ। GroupShuffleSplit একই নিয়ম মানে, শুধু এলোমেলো বাছাই আলাদা।

প্র ২৪ফলো-আপ: কেন group split দরকার? Cross-validation-এ কী করবেন? (মাঝারি)

উত্তর: একই গ্রাহকের অর্ডারগুলো একে অপরের মতো: একই ঠিকানা, একই পেমেন্টের অভ্যাস। এলোমেলো সারি-ভাগে model test-এ “চেনা” গ্রাহক দেখে, তাই score আসল নতুন গ্রাহকের চেয়ে ভালো আসে। প্রশ্নটা হলো model কাদের ওপর চলবে: নতুন গ্রাহক হলে group split, পুরোনো গ্রাহকের পরের অর্ডার হলে সময় ধরে ভাগ বেশি সৎ। Cross-validation-এ GroupKFold, আর label-এর অনুপাতও মেলাতে StratifiedGroupKFold (পর্ব ৫৬, পর্ব ৬৬)। জটিলতা: unique আর isin hash দিয়ে প্রায় O(n), sort O(g log g), g গ্রাহকের সংখ্যা।

সমস্যা ৭ · সংখ্যা আর লেখার মিশ্র কলামে scikit-learn pipeline

প্রশ্ন: “অর্ডার ফেরত আসবে কি না অনুমান করুন। কলাম: taka, delivery_days (কিছু ফাঁকা), district, payment. পুরো preprocessing আর model একটি pipeline-এ রাখুন।” শর্ত: নতুন জেলা এলে কোড ভাঙবে না; baseline-এর সাথে তুলনা।

যা জিজ্ঞেস করবেন: কোন metric গুরুত্বপূর্ণ, accuracy না recall? Class-এর অনুপাত কেমন? Model কখন চলবে, অর্ডারের সময় না ডেলিভারির পরে? (delivery_days অর্ডারের সময় জানা থাকে কি? না থাকলে এটিও leakage.)

পরিকল্পনা জোরে বলুন: “সংখ্যায় median দিয়ে ভরা আর scale; লেখায় সবচেয়ে বেশি আসা মান দিয়ে ভরা আর one-hot, অজানা category উপেক্ষা। দুটি ColumnTransformer-এ, তারপর LogisticRegression. Stratify করে ভাগ, তারপর শুধু train-এ fit.”

import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
rng = np.random.default_rng(42); n = 400
df = pd.DataFrame({"taka": rng.integers(200, 5000, n).astype(float),
                   "delivery_days": rng.integers(1, 8, n).astype(float),
                   "district": rng.choice(["ঢাকা", "খুলনা", "সিলেট"], n),
                   "payment": rng.choice(["cod", "bkash", "card"], n)})
df.loc[rng.random(n) < 0.1, "delivery_days"] = np.nan          # ১০% ফাঁকা
logit = 0.6 * (df["delivery_days"].fillna(4) - 4) + (df["payment"] == "cod") - 0.5
df["returned"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)

num, cat = ["taka", "delivery_days"], ["district", "payment"]
pre = ColumnTransformer([
    ("num", make_pipeline(SimpleImputer(strategy="median"), StandardScaler()), num),
    ("cat", make_pipeline(SimpleImputer(strategy="most_frequent"),
                          OneHotEncoder(handle_unknown="ignore")), cat)])
model = make_pipeline(pre, LogisticRegression(max_iter=1000))
X, y = df[num + cat], df["returned"]
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
model.fit(Xtr, ytr)
print("test accuracy:", round(model.score(Xte, yte), 3), "| majority baseline:", round(max(yte.mean(), 1 - yte.mean()), 3))
new = pd.DataFrame([{"taka": 1200.0, "delivery_days": np.nan, "district": "বরিশাল", "payment": "cod"}])
print("new order, unseen district → P(return):", model.predict_proba(new)[0, 1].round(3))
print("features after transform:", model[0].get_feature_names_out().size)

সত্যিকারের আউটপুট:

test accuracy: 0.8 | majority baseline: 0.51
new order, unseen district → P(return): 0.622
features after transform: 8

Test accuracy 0.8, যেখানে সবাইকে বেশি আসা class বলে দিলে 0.51; তাই model সত্যিই কিছু শিখেছে। নতুন অর্ডারে “বরিশাল” train-এ ছিল না, আর delivery_days ফাঁকা, তবু কোড ভাঙেনি: imputer train-এর median বসাল, আর handle_unknown="ignore" অজানা জেলার one-hot ঘরগুলো শূন্য রাখল (unseen category). ৮টি feature = ২টি সংখ্যা + ৩ + ৩টি one-hot. ডেটা বানানো, তাই 0.8 শুধু এই উদাহরণের।

প্র ২৫ফলো-আপ: hyperparameter কীভাবে বাছবেন? model কীভাবে সংরক্ষণ করবেন? (মাঝারি)

উত্তর: পুরো pipeline-ই GridSearchCV-কে দিন, parameter-এর নাম ধাপের নাম দিয়ে, যেমন logisticregression__C; তাহলে প্রতিটি fold-এ imputer আর scaler শুধু সেই fold-এর train দেখে (পর্ব ৬৬)। Test set শেষে একবারই। সংরক্ষণে joblib.dump(model, ...) চলে, তবে pickle অবিশ্বস্ত উৎস থেকে খোলা বিপজ্জনক, আর scikit-learn-এর version বদলালে খুলতে সমস্যা হতে পারে; তাই version lockfile-এ বাঁধা থাকে (পর্ব ৬৪, পর্ব ১২)। হাজারো category হলে one-hot-এর feature-এর সংখ্যা ফুলে ওঠে; তখন high cardinality-র কৌশল লাগে (পর্ব ৬৩)।

সমস্যা ৮ · ধীর apply-কে vectorised করা

প্রশ্ন: “ডেলিভারি চার্জের এই apply(axis=1) কোডটি ২০,০০০ সারিতে ধীর। একই ফল দ্রুত দিন।” নিয়ম: ঢাকায় ৬০ টাকা, অন্যত্র ১২০; ১ কেজির পরে প্রতি শুরু হওয়া কেজিতে ২০ টাকা; ৫০০০ টাকা বা বেশি হলে চার্জ ০. শর্ত: ফল হুবহু মিলতে হবে।

যা জিজ্ঞেস করবেন: সীমার মান (ঠিক ১ কেজি, ঠিক ৫০০০ টাকা) কোন দিকে? ওজন ফাঁকা থাকতে পারে কি?

পরিকল্পনা জোরে বলুন: “প্রতিটি if-কে কলামের np.where-এ, max(0, …)-কে clip(lower=0)-এ। আগে দুটির ফল মিলাব, সীমার মানে হাতে লেখা test, তারপর সময় মাপব।”

import time
import numpy as np
import pandas as pd
rng = np.random.default_rng(42); n = 20_000
df = pd.DataFrame({"district": rng.choice(["ঢাকা", "চট্টগ্রাম", "খুলনা", "সিলেট"], n),
                   "weight_kg": rng.uniform(0.2, 8, n).round(1),
                   "taka": rng.integers(100, 6000, n)})

def fee_row(r):                                  # ধীর রূপ: প্রতি সারিতে Python
    base = 60 if r["district"] == "ঢাকা" else 120
    extra = 20 * max(0.0, np.ceil(r["weight_kg"] - 1))
    return 0 if r["taka"] >= 5000 else base + extra

def fee_vec(d):                                  # দ্রুত রূপ: পুরো কলাম একসাথে
    base = np.where(d["district"] == "ঢাকা", 60, 120)
    extra = 20 * np.ceil(d["weight_kg"] - 1).clip(lower=0)
    return pd.Series(np.where(d["taka"] >= 5000, 0, base + extra), index=d.index)

t0 = time.perf_counter(); slow = df.apply(fee_row, axis=1); t1 = time.perf_counter()
fast = fee_vec(df); t2 = time.perf_counter()
assert np.allclose(slow, fast)                   # আগে সঠিকতা, পরে গতি
edge = pd.DataFrame({"district": ["ঢাকা", "সিলেট"], "weight_kg": [1.0, 1.1], "taka": [5000, 4999]})
assert fee_vec(edge).tolist() == [0, 140]
print(f"apply: {1000 * (t1 - t0):.0f} ms | vectorised: {1000 * (t2 - t1):.1f} ms")
print("speed-up about", round((t1 - t0) / (t2 - t1)), "times")

একবারের সত্যিকারের আউটপুট (সময় আপনার ডিভাইস আর ব্রাউজারে আলাদা আসবে):

apply: 524 ms | vectorised: 7.0 ms
speed-up about 75 times

দুটি পথের ফল প্রতিটি সারিতে মিলেছে, আর সীমার দুটি হাতে লেখা case-ও: ঠিক ৫০০০ টাকায় চার্জ ০, আর সিলেটে ১.১ কেজিতে ১২০ + ২০ = ১৪০. গতির পার্থক্য এই একবারের মাপে কয়েক ডজন গুণ; গুণফল ডিভাইস ভেদে বদলায়। ধাপ বেশি হলে np.select আরও পরিষ্কার।

প্র ২৬ফলো-আপ: কেন দ্রুত হলো, আর কখন vectorised করা কঠিন? (মাঝারি)

উত্তর: apply(axis=1) প্রতিটি সারির জন্য একটি Series বানায় আর Python function ডাকে: ২০,০০০ বার Python-এর খরচ। Vectorised রূপে কয়েকটি C-তে লেখা কাজ পুরো কলামে একবার করে চলে। দুটিই O(n), কিন্তু প্রতি সারির ধ্রুবক খরচে বিশাল পার্থক্য; Big-O এই পার্থক্য দেখায় না, মাপতে হয় (পর্ব ২৭)। কঠিন হয় যখন প্রতিটি সারির ফল আগের সারির ফলের ওপর নির্ভর করে (যেমন চলমান ব্যালান্স কোনো সীমায় আটকে যায়), বা যখন প্রতিটি সারিতে বাইরের API ডাকতে হয়। তখন itertuples বা Numba দেখুন, আর সময় মাপুন timeit দিয়ে, কয়েকবার (পর্ব ১৯)।

৮ · ডিবাগিং রাউন্ড — ভাঙা কোড, সত্যিকারের error আর সমাধান

রিভিশন: পর্ব ৫৬, পর্ব ২২, পর্ব ৯, পর্ব ২৪, পর্ব ২৬, পর্ব ২৩

ডিবাগিং রাউন্ডে সাধারণত একটি ছোট কোড আর একটি অভিযোগ দেওয়া হয়। ভালো উত্তরের ক্রম সবসময় এক: লক্ষণ মিলিয়ে নিই, ছোট করে আবার ঘটাই, একটি অনুমান বলি, print বা assert দিয়ে প্রমাণ করি, ঠিক করি, আর একটি test দিয়ে আটকে রাখি। প্রথম দুটি সমস্যা চালানো যায়, বাগওয়ালা আর ঠিক রূপ পাশাপাশি। এরপরের পাঁচটি বাগের error এই পাতার একই ব্রাউজার-Python-এ (Python 3.14.2, pandas 3.0.2) সত্যিই চালিয়ে পাওয়া; লম্বা traceback “…” দিয়ে ছোট করা।

সমস্যা ৯ · Leakage: এলোমেলো ডেটায় ৭৫% accuracy

লক্ষণ: “২০০০টি feature, ২০০ গ্রাহক। Feature বাছাই করে model বানালাম, test accuracy 0.75. কিন্তু production-এ মুদ্রা ছোড়ার মতো।”

কারণ খোঁজা: প্রথম সন্দেহ, ভাগের আগে কোন ধাপ পুরো ডেটা দেখেছে? এখানে SelectKBest পুরো y দেখে সেই ২০টি feature বেছেছে যা হঠাৎ করে test সারির label-এর সাথেও মেলে। প্রমাণ: label নিজেই এলোমেলো, তাই সৎ উত্তর ০.৫-এর কাছে হওয়ার কথা।

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

rng = np.random.default_rng(0)
X = rng.normal(size=(200, 2000))     # ২০০ গ্রাহক, ২০০০টি একেবারে এলোমেলো feature
y = rng.integers(0, 2, 200)          # লক্ষ্যও এলোমেলো: আসল accuracy ~0.5

# বাগ: পুরো ডেটা দেখে feature বাছাই, তারপর ভাগ
X_sel = SelectKBest(f_classif, k=20).fit_transform(X, y)
Xtr, Xte, ytr, yte = train_test_split(X_sel, y, test_size=0.3, random_state=0)
bug = LogisticRegression(max_iter=1000).fit(Xtr, ytr).score(Xte, yte)

# ঠিক: আগে ভাগ, বাছাই pipeline-এর ভেতরে, শুধু train দেখে
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0)
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression(max_iter=1000))
fixed = pipe.fit(Xtr, ytr).score(Xte, yte)
print(f"leaky test accuracy : {bug:.2f}")
print(f"honest test accuracy: {fixed:.2f}")
assert bug > 0.7 and abs(fixed - 0.5) < 0.15

সত্যিকারের আউটপুট:

leaky test accuracy : 0.75
honest test accuracy: 0.45

একই ডেটা, একই model, শুধু ধাপের ক্রম আলাদা। বাগওয়ালা রূপে বাছাই test সারিগুলোও দেখেছিল, তাই বাছা ২০টি feature test-এর label-এর সাথেও কাকতালীয়ভাবে মেলে। ঠিক রূপে ফল 0.45, এলোমেলো label-এ যা প্রত্যাশিত (ছোট test set-এ 0.5 থেকে এটুকু সরা স্বাভাবিক)। Feature অনেক আর সারি কম হলে এই leakage সবচেয়ে বড় দেখায়।

সমস্যা ১০ · Chained assignment: ঢাকার চার্জ বদলায় না

লক্ষণ: “ঢাকার ডেলিভারি চার্জ ৬০ করতে চাইলাম, কোনো error নেই, কিন্তু টেবিলে আগের ৫০ রয়ে গেছে। শুধু একটি সতর্কবার্তা আসে।”

কারণ খোঁজা: df[mask] একটি নতুন DataFrame দেয়; তার ["fee"]-এ লেখা সেই অস্থায়ী টেবিলে যায়, মূল df-এ নয়। pandas 3-এর Copy-on-Write-এ এটি কখনোই কাজ করে না। নিচের ব্লক সতর্কবার্তাটি ধরে তার নাম ছাপে, যাতে পাতায় দেখা যায়।

import warnings
import pandas as pd
def make():
    return pd.DataFrame({"order_id": [1, 2, 3, 4],
                         "district": ["ঢাকা", "খুলনা", "ঢাকা", "সিলেট"],
                         "fee": [50, 80, 50, 80]})
# বাগওয়ালা রূপ: শিকলবাঁধা assignment
df = make()
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    df[df["district"] == "ঢাকা"]["fee"] = 60
print("bug  :", df["fee"].tolist(), "| warning:", type(caught[0].message).__name__)

# ঠিক রূপ ১: এক ধাপে loc[row_mask, column]
df = make()
df.loc[df["district"] == "ঢাকা", "fee"] = 60
print("fix 1:", df["fee"].tolist())

# ঠিক রূপ ২: নতুন টেবিল ফেরত, মূলটি অক্ষত
base = make()
new = base.assign(fee=base["fee"].mask(base["district"] == "ঢাকা", 60))
print("fix 2:", new["fee"].tolist(), "| base:", base["fee"].tolist())
assert df["fee"].tolist() == new["fee"].tolist() == [60, 80, 60, 80]

সত্যিকারের আউটপুট:

bug  : [50, 80, 50, 80] | warning: ChainedAssignmentError
fix 1: [60, 80, 60, 80]
fix 2: [60, 80, 60, 80] | base: [50, 80, 50, 80]

বাগওয়ালা লাইনের পরে চার্জ অপরিবর্তিত, আর pandas ChainedAssignmentError নামের সতর্কবার্তা দিয়েছে। ধরার ব্যবস্থা না থাকলে script-এ বার্তাটি এমন দেখায়, যা পর্ব ২২-এর সত্যিকারের run থেকে নেওয়া: ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. ঠিক রূপ দুটি: এক ধাপে loc[row_mask, column], অথবা assign দিয়ে নতুন টেবিল, মূলটি অক্ষত রেখে।

বাগ ক · Mutable default: দুই ক্রেতার ঝুড়ি এক

def add_to_cart(item, cart=[]):
    cart.append(item)
    return cart

rahim = add_to_cart("চা")
karim = add_to_cart("মধু")
print("rahim:", rahim)
print("karim:", karim)

সত্যিকারের আউটপুট:

rahim: ['চা', 'মধু']
karim: ['চা', 'মধু']

কারণ আর সমাধান: খালি list একবারই তৈরি হয়েছে, def-এর সময়; করিমের ডাকেও সেই একই list. প্রমাণ: rahim is karim True. সমাধান cart=None, ভেতরে if cart is None: cart = []. Test: দুটি আলাদা ডাকে দুটি আলাদা ফল (পর্ব ৯)।

বাগ খ · groupby().mean() লেখার কলামে আটকে যায়

import pandas as pd
orders = pd.DataFrame({"district": ["ঢাকা", "ঢাকা", "খুলনা"],
                       "product": ["চা", "মধু", "চা"],
                       "taka": [500, 300, 200]})
print(orders.groupby("district").mean())

সত্যিকারের error (traceback ছোট করা):

Traceback (most recent call last):
  File "main.py", line 5, in <module>
    print(orders.groupby("district").mean())
…
TypeError: dtype 'str' does not support operation 'mean'

কারণ আর সমাধান: pandas 2 থেকে numeric_only-এর default False, আর pandas 3-এ লেখার কলাম str dtype-এ। তাই “product” কলামের গড় করতে গিয়ে error. সমাধান: কোন কলামের গড় চাই তা স্পষ্ট লিখুন, orders.groupby("district")["taka"].mean(), অথবা agg(avg_taka=("taka", "mean")). numeric_only=True-ও চলে, তবে কোন কলাম বাদ গেল তা লুকিয়ে ফেলে (পর্ব ২৪)।

বাগ গ · ঢাকার রাতের বিক্রি আগের দিনে চলে যায়

import tzdata
import pandas as pd
orders = pd.DataFrame({"order_id": [1, 2, 3],
    "created_utc": pd.to_datetime(["2026-04-13 17:30", "2026-04-13 19:10",
                                   "2026-04-14 05:00"], utc=True),
    "taka": [900, 400, 700]})
bug = orders.groupby(orders["created_utc"].dt.date)["taka"].sum()
print(bug)
local = orders["created_utc"].dt.tz_convert("Asia/Dhaka")
fix = orders.groupby(local.dt.date)["taka"].sum()
print(fix)

সত্যিকারের আউটপুট (প্রথমটি বাগওয়ালা, দ্বিতীয়টি ঠিক):

created_utc
2026-04-13    1300
2026-04-14     700
Name: taka, dtype: int64
created_utc
2026-04-13     900
2026-04-14    1100
Name: taka, dtype: int64

কারণ আর সমাধান: অর্ডার ২ UTC-তে ১৩ এপ্রিল সন্ধ্যা ৭টা ১০, কিন্তু ঢাকায় তখন ১৪ এপ্রিল রাত ১টা ১০. UTC-র তারিখে groupby করায় সেটি ১৩ তারিখে গেছে: ১৩ এপ্রিল ১৩০০ (আসলে ৯০০), ১৪ এপ্রিল ৭০০ (আসলে ১১০০)। মোট ঠিক আছে, ভাগ ভুল, তাই মোট মিলিয়ে এই বাগ ধরা যায় না। সমাধান: আগে dt.tz_convert("Asia/Dhaka"), তারপর তারিখ। Test: সীমার কাছে, যেমন UTC সন্ধ্যা ৬টার আগে-পরে, একটি করে অর্ডার (পর্ব ২৬)।

বাগ ঘ · ফাঁকা ঘরসহ কলাম int-এ রূপান্তর

import pandas as pd
qty = pd.Series(["2", "3", None, "5"])
print(qty.astype(float).astype(int))

সত্যিকারের error (traceback ছোট করা):

Traceback (most recent call last):
  File "main.py", line 3, in <module>
    print(qty.astype(float).astype(int))
…
pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')

কারণ আর সমাধান: সাধারণ NumPy int-এ “ফাঁকা”-র জায়গা নেই। Error-এর বার্তাই পথ দেখায় (বার্তায় দুটি ফাঁকা জায়গার অভাব pandas-এর নিজের): nullable "Int64", যেখানে ফাঁকা ঘর <NA> হয়ে থাকে। লেখা থেকে সরাসরি pd.to_numeric(qty, errors="coerce").astype("Int64"). অথবা আগে সিদ্ধান্ত নিন ফাঁকা পরিমাণের অর্থ কী (পর্ব ২৩)। fillna(0) দিয়ে চুপ করানো এখানে প্র ১৬-এর সেই দুর্বল উত্তর।

বাগ ঙ · Float-এ ==: ০.৩ কেজির প্যাকেট খুঁজে পাওয়া যায় না

import pandas as pd
packs = pd.DataFrame({"product": ["চা", "মসলা"], "kg_each": [0.1, 0.25], "n": [3, 2]})
packs["kg"] = packs["kg_each"] * packs["n"]
print(packs)
print(packs[packs["kg"] == 0.3])
print(repr(packs.loc[0, "kg"]))

সত্যিকারের আউটপুট:

  product  kg_each  n   kg
0      চা     0.10  3  0.3
1    মসলা     0.25  2  0.5
Empty DataFrame
Columns: [product, kg_each, n, kg]
Index: []
np.float64(0.30000000000000004)

কারণ আর সমাধান: টেবিল ছাপায় 0.3, কারণ pandas গোল করে দেখায়, কিন্তু আসল মান 0.30000000000000004. তাই == কোনো সারি পায়নি। শেষ লাইনের repr আসল মান দেখিয়ে কারণ প্রমাণ করে। সমাধান: np.isclose(packs["kg"], 0.3), অথবা ওজন গ্রামে পূর্ণসংখ্যায় রাখা। ছাপানো রূপ আর রাখা মান এক নয় (পর্ব ১৬)।

৯ · জমা দেওয়ার আগের চেকলিস্ট

রিভিশন: পর্ব ১১, পর্ব ১২, পর্ব ৭৩, পর্ব ৭৪, SQL ইন্টারভিউ

সমাধান জমা দেওয়ার আগে: আট লাইনের চেকলিস্ট

আরও গভীরে: pandas-এর Copy-on-Write-এর নকশা আর কারণ PDEP-7 নামের প্রস্তাবে লেখা, আর pandas 3.0-এর release notes-এ str dtype আর CoW-এর পূর্ণ তালিকা আছে; ইন্টারভিউর আগে নিজের version-এর “What's new” পড়ে নেওয়া ভালো অভ্যাস। Floating-point-এর গভীর ব্যাখ্যা David Goldberg-এর ১৯৯১ সালের প্রবন্ধ “What Every Computer Scientist Should Know About Floating-Point Arithmetic”-এ। Unicode normalization-এর নিয়ম Unicode Standard Annex #15-এ, যেখানে composition exclusion-এর তালিকায় বাংলার ড়, ঢ় আর য় আছে; এজন্যই NFC এগুলোকে ভেঙে দুই অক্ষরে রাখে।

AI-এর যুগে Python আর ডেটার ইন্টারভিউর প্রস্তুতি

AI সহকারী একজন ধৈর্যশীল নকল ইন্টারভিউয়ার হতে পারে, তবে ডেটার কোডে তার ভুলগুলো চেনা দরকার। সে প্রায়ই পুরোনো API শেখায়: pandas-এর সরানো df.append, NumPy-র সরানো np.float, chained assignment, বা লেখার কলামে ভাঙা groupby().mean() (বাগ খ)। নেই এমন parameter বানায়। ভাগের আগে পুরো ডেটায় scaler বা feature বাছাই fit করে, train set-এ metric দেখিয়ে “৯৮% accuracy” বলে, আর warning চাপা দিতে warnings.filterwarnings("ignore") বসায়। চালানো ছাড়াই বিশ্বাসযোগ্য দেখতে পরিসংখ্যান দেয়। আর গ্রাহকের নাম বা ফোন নম্বরসহ ডেটা prompt-এ দিলে তা ফাঁস হতে পারে। Live ইন্টারভিউতে অনুমতি ছাড়া AI ব্যবহার করবেন না।

AI-কে উত্তর লিখতে না বলে আপনাকে চেপে ধরতে বলুন। তারপর তার প্রতিটি দাবি নিজে চালিয়ে মিলিয়ে নিন।

তুমি একজন কড়া data/ML ইন্টারভিউয়ার। আমার স্তর: [শুরু/মাঝারি/উন্নত]। Version: Python 3.14, NumPy 2.4, pandas 3.0, scikit-learn 1.8. ১. আমাকে একটি pandas বা NumPy live coding সমস্যা দাও, ৩০ লাইনের মধ্যে করা যায় এমন। ছোট একটি নমুনা টেবিল দাও (কাল্পনিক দোকানের ডেটা)। শুধু চাহিদা বলো, সমাধান বলো না। ২. আমি কোড দিলে আগে জিজ্ঞেস করো: খালি টেবিল, NaN, সমান মান আর dtype-এ কী হবে? জটিলতা কত? ৩. তারপর একটি বাগওয়ালা কোড দাও আর শুধু লক্ষণ বলো। আমি প্রমাণ ছাড়া অনুমান করলে থামিয়ে জিজ্ঞেস করো, "কীভাবে নিশ্চিত হবে?" ৪. কোনো API নিয়ে নিশ্চিত না হলে "নিশ্চিত নই" লেখো, আর pandas.pydata.org বা numpy.org-এর কোন পাতা দেখব তা বলো। পুরোনো API (df.append, np.float) ব্যবহার করবে না। ৫. শেষে পাঁচটি ক্ষেত্রে ০ থেকে ২ নম্বর দাও: চাহিদা মেলানো, সঠিকতা, test, জটিলতা, ব্যাখ্যা।

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

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

ক্রমLive coding-এ একটি pandas সমস্যা পেলে ভালো কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ইনপুট, আউটপুট, সমান মান আর ফাঁকা ঘর নিয়ে প্রশ্ন করে চাহিদা মেলানো
  2. ৪–৬ সারির ছোট উদাহরণে প্রত্যাশিত ফল জোরে বলা
  3. সরল কিন্তু সঠিক সমাধান লিখে ধাপে ধাপে চালানো
  4. assert দিয়ে হাতের উত্তর আর edge case যাচাই
  5. জটিলতা আর বড় ডেটায় কী বদলাবেন তা বলা

(সহজ) a = np.array([10, 20, 30, 40, 50]); b = a[1:4]; b[0] = 99. এরপর a[1] কত?

  • 20
  • 99
  • IndexError
  • 10

(মাঝারি) pandas 3-এ sub = df[df["taka"] > 100], তারপর sub.loc[:, "taka"] = 0. মূল df-এর কী হয়?

  • ১০০-র বেশি সব মান ০ হয়ে যায়
  • একটি exception আসে, কিছুই বদলায় না
  • df অপরিবর্তিত থাকে, শুধু sub-এর মান ০
  • কখনো বদলায়, কখনো না, memory-র ওপর নির্ভর করে

(মাঝারি) orders-এ ৬টি সারি, তার ২টি জেলা “ঢাকা”-র। zones টেবিলে “ঢাকা” দুবার, বাকি জেলাগুলো একবার করে, আর orders-এর সব জেলাই zones-এ আছে। orders.merge(zones, on="district", how="left")-এ কয়টি সারি?

  • ৬
  • ১২
  • ৭
  • ৮

(সহজ) ১০টি সারির একটি DataFrame-এ ৩টি জেলা। df.groupby("district")["taka"].transform("sum")-এর দৈর্ঘ্য কত?

  • ১০
  • , প্রতিটি দলে একটি সারি; transform নয়।">৩
  • ১, মোট যোগফল
  • ১৩

(মাঝারি) যে Python-এ NumPy-র default পূর্ণসংখ্যা int32 (যেমন ব্রাউজারের ৩২-bit Python), সেখানে np.full(100_000, 50_000).sum()-এর ফল কী?

  • 5000000000
  • 705032704, কোনো সতর্কবার্তা ছাড়া
  • OverflowError
  • 5e9, float হয়ে

(সহজ) একটি অর্ডারের সময় UTC-তে 2026-04-13 19:10. বাংলাদেশের সময় (Asia/Dhaka, UTC+6) অনুযায়ী অর্ডারটি কোন তারিখের?

  • ১৩ এপ্রিল, সন্ধ্যা ৭টা ১০
  • ১২ এপ্রিল, দুপুর ১টা ১০
  • ১৪ এপ্রিল, রাত ১টা ১০
  • ১৪ এপ্রিল, রাত ২টা ১০

(মাঝারি) ২০০ সারি, ২০০০টি এলোমেলো feature আর এলোমেলো label. পুরো ডেটায় SelectKBest(k=20) চালিয়ে তারপর train/test ভাগ করে test accuracy এল 0.75. সবচেয়ে সম্ভাব্য কারণ কী?

  • ২০টি feature-এ সত্যিকারের সংকেত আছে
  • LogisticRegression-এর বদলে অন্য model লাগত
  • Test set ছোট, তাই শুধু ভাগ্য
  • Feature বাছাই ভাগের আগে পুরো ডেটা দেখেছে: preprocessing leakage

(সহজ) def add_to_cart(item, cart=[]): cart.append(item); return cart. a = add_to_cart("চা"), তারপর b = add_to_cart("মধু"). a কী?

  • ['চা', 'মধু']
  • ['চা']
  • []
  • TypeError

শব্দকোষ

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

মনে রাখুন

  1. ফলের আকার আগে বলুন: agg দলপ্রতি এক সারি, transform মূলের সমান; merge-এর পরে সারি বাড়লে ডানের key অনন্য নয়, validate দিন।
  2. অনুমান নয়, প্রমাণ: view কি না np.shares_memory, dtype .dtype-এ, আসল মান repr-এ, সময়ের zone dt.tz-এ।
  3. নীরব বাগই বিপজ্জনক: int32-এর overflow, float-এর ==, UTC-র তারিখ আর chained assignment কোনো error দেয় না; হাতের হিসাব আর মোট মিলিয়ে ধরুন।
  4. Live coding-এ ছয় ধাপ: চাহিদা, ছোট উদাহরণ, সরল সঠিক রূপ, assert, জটিলতা, উন্নতি; পুরো সময় জোরে ভাবুন।
  5. ML-এর কোডে আগে ভাগ: সব fit pipeline-এর ভেতরে, একই গ্রাহক এক দিকে, seed আর uv.lock-এ পুনরুৎপাদন, আর baseline-এর সাথে তুলনা।

পরের পর্বে: পর্ব ৭৯ · AI-এর ভিত্তির ইন্টারভিউ। Search আর logic থেকে probability, ML-এর যুক্তি, আধুনিক AI আর দায়িত্বশীল AI-এর প্রশ্ন, সাথে আচরণগত প্রশ্নের উত্তর সাজানোর পথ।

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

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