এই পর্বে যা শিখবেন
- Ufunc কী, কোন কোন গাণিতিক function সবচেয়ে বেশি লাগে, আর
out=ওwhere=দিয়ে নিরাপদ ভাগ inf,nan, শূন্য দিয়ে ভাগের সতর্কবার্তা,np.errstateআরnp.iscloseদিয়ে float-এর কিনারা সামলানোsum,mean,median,std,var,percentile,cumsumহাতে হিসাব করে মেলানো, আরaxisওkeepdimsনিশ্চিতভাবে বোঝাnp.random.default_rngদিয়ে পুনরুৎপাদনযোগ্য random সংখ্যা, ওজনসহ নমুনা,shuffleবনামpermutation, আর আলাদা stream- চাহিদা আর ডেলিভারির দেরি simulate করা, আর একটি Monte Carlo অনুমানের ভুল কীভাবে নমুনার সংখ্যার সঙ্গে কমে, তা মেপে দেখা
আগের তিনটি পর্বে (পর্ব ১৩-এ array, shape আর dtype; পর্ব ১৪-এ indexing আর slicing; পর্ব ১৫-এ vectorization আর broadcasting) দেখেছেন NumPy array কী আর কীভাবে তার ভেতরের অংশ বেছে নেওয়া যায়। এই পর্বে সেই array দিয়ে আসল হিসাব: গাণিতিক function, সারাংশ (মোট, গড়, ছড়ানো) আর random সংখ্যা। শেষে এই তিনটি মিলিয়ে বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) একটি ব্যবসায়িক প্রশ্নের উত্তর খুঁজবেন: একটি অর্ডার ৩ দিনের বেশি সময় নেওয়ার সম্ভাবনা কত? সব কোড পাতাতেই চলে; প্রথমবার চালালে NumPy নামতে কয়েক সেকেন্ড লাগতে পারে। Seed-এর মৌলিক ধারণা আগেই এসেছে পর্ব ১১-এ; এখানে তার পরের ধাপ।
১ · Ufunc — পুরো array-এ একসঙ্গে গাণিতিক কাজ
বাজারঘরের গুদামে ৫০০টি পণ্যের দাম একটি array-তে আছে। সব দামে ১৫% ভ্যাট যোগ করতে হবে, ঋণাত্মক ভুল দাম শূন্য করতে হবে, আর বিশ্লেষণের জন্য দামের logarithm লাগবে। খাঁটি Python-এ প্রতিটি কাজের জন্য একটি loop লাগত। NumPy-তে এক লাইন: np.log(price). এই ধরনের function-এর নাম universal function, ছোট করে ufunc: এমন একটি function, যা একটি array নেয় আর প্রতিটি উপাদানের ওপর আলাদাভাবে একই কাজ করে একই shape-এর নতুন array ফেরত দেয়। Loop-টি চলে NumPy-র ভেতরের compiled C কোডে, যা পর্ব ৯-এ মেপে দেখা vectorization-এর মূল কারণ।
পরীক্ষার খাতা দেখার পর ফল প্রকাশের কথা ভাবুন। প্রধান শিক্ষক ঘোষণা দিলেন: “সবার নম্বরে ৫ গ্রেস যোগ হবে, তবে ১০০-র বেশি হবে না।” কেউ একজন একটি একটি খাতা খুলে যোগ করেন না; একটি নিয়ম পুরো তালিকায় একসঙ্গে বসে যায়। ufunc ঠিক সেই নিয়ম: np.minimum(marks + 5, 100). নিয়ম একটি, প্রয়োগ প্রতিটি ঘরে।
সবচেয়ে বেশি কাজে লাগে এমন ufunc-গুলো এক নজরে:
| ufunc | কী করে | বাজারঘরে কোথায় |
|---|---|---|
np.sqrt, np.abs | বর্গমূল, পরম মান (চিহ্ন বাদ) | standard deviation, পূর্বাভাসের ভুলের মাপ |
np.exp, np.log | ex আর স্বাভাবিক logarithm (ভিত্তি e ≈ 2.718) | অনেক বড়-ছোট দামকে এক স্কেলে আনা; পরে softmax আর log-probability |
np.round, np.rint | নির্দিষ্ট দশমিক ঘরে round | টাকা ছাপার আগে; মাঝখানের মান জোড়ের দিকে যায় |
np.clip(a, lo, hi) | নিচের সীমার নিচের মান lo, ওপরের সীমার ওপরের মান hi | ঋণাত্মক চাহিদা শূন্য করা, রেটিং ১–৫-এ আটকানো |
np.maximum(a, b) | দুটি array-এর ঘরে ঘরে বড়টি | “দাম অন্তত খরচের সমান” নিয়ম |
খেয়াল করুন, np.maximum আর np.max এক জিনিস নয়। প্রথমটি দুটি array-কে ঘরে ঘরে তুলনা করে একই আকারের array দেয়; দ্বিতীয়টি একটি array-এর সব সংখ্যার মধ্যে সবচেয়ে বড়টি, মানে একটি সারাংশ (অংশ ৩)। +, *, >-এর মতো চিহ্নও আসলে ufunc: a + b লিখলে ভেতরে np.add(a, b) চলে। দুটি array-এর shape আলাদা হলে broadcasting-এর নিয়ম (পর্ব ১৫) ঠিক করে কোন ঘর কার সঙ্গে মিলবে।
out= আর where=
প্রতিটি ufunc দুটি কাজের argument নেয়। out= বলে দেয় ফল কোন আগে-বানানো array-তে লেখা হবে; বড় array-তে বারবার নতুন মেমরি না নিয়ে একই জায়গা ব্যবহারে এটি কাজে লাগে। where= একটি boolean mask (পর্ব ১৪) নেয়: যেখানে True, শুধু সেখানে কাজ হয়; যেখানে False, সেখানে out-এ আগের মানই থাকে। তাই দুটি একসঙ্গে দিলে শূন্য দিয়ে ভাগ এড়ানোর একটি পরিচ্ছন্ন উপায় পাওয়া যায়: প্রতিটি পণ্যের “বিক্রির হার” = বিক্রি ÷ মজুত, কিন্তু মজুত শূন্য হলে হার ০ ধরা হবে।
নিচের কোডটি লেখকের যন্ত্রে সত্যিকারের run (Python 3.14.4, NumPy 2.5.1); এখানে ব্রাউজারের ব্লক না রেখে কাগজে দেওয়া হলো, কারণ পাতার চারটি চালানো যায় এমন ব্লক পরের অংশগুলোর জন্য রাখা। আপনি কোডটি পরের যেকোনো ব্লকে বসিয়ে চালিয়ে মেলাতে পারেন।
~/bazarghar-ai$ python ufunc_demo.py
import numpy as np
price = np.array([120.0, 85.5, 300.0, 49.99]) # টাকা
print(np.sqrt(np.array([4.0, 9.0, 2.0])))
print(np.exp(np.array([0.0, 1.0, 2.0])))
print(np.log(np.array([1.0, np.e, 100.0])))
print(np.abs(np.array([-3, 0, 5])))
print(np.round(np.array([2.5, 3.5, 0.125]), 2), np.round(np.array([2.5, 3.5])))
print(np.clip(np.array([-2, 3, 12, 7]), 0, 10))
print(np.maximum(np.array([3, 8, 1]), np.array([5, 2, 4])))
print(type(np.sqrt), np.add.nin, np.add.nout)
out = np.empty(4)
r = np.multiply(price, 1.15, out=out) # ভ্যাটসহ দাম
print(r is out, out)
stock = np.array([10.0, 0.0, 4.0, 0.0])
sold = np.array([5.0, 0.0, 1.0, 3.0])
rate = np.zeros(4)
np.divide(sold, stock, out=rate, where=stock > 0)
print(rate)
সত্যিকারের run-এর আউটপুট:
[2. 3. 1.41421356]
[1. 2.71828183 7.3890561 ]
[0. 1. 4.60517019]
[3 0 5]
[2.5 3.5 0.12] [2. 4.]
[ 0 3 10 7]
[5 8 4]
<class 'numpy.ufunc'> 2 1
True [138. 98.325 345. 57.4885]
[0.5 0. 0.25 0. ]
কয়েকটি জিনিস লক্ষ করুন। log(100) = 4.605, কারণ e4.605 ≈ 100; NumPy-র log ভিত্তি e ধরে, ভিত্তি ১০ চাইলে np.log10. np.round-এর লাইনটি অবাক করতে পারে: 2.5 থেকে 2, 3.5 থেকে 4, আর 0.125 থেকে 0.12. NumPy ঠিক মাঝখানের মানকে জোড় অঙ্কের দিকে নেয় (“banker's rounding”), Python-এর round-এর মতোই। np.sqrt-এর type numpy.ufunc, আর np.add.nin = 2, nout = 1 বলছে যোগে দুটি input আর একটি output. r is out সত্যি, মানে নতুন array তৈরি হয়নি, ফল সরাসরি out-এ লেখা হয়েছে। শেষ লাইনে দ্বিতীয় আর চতুর্থ পণ্যের মজুত শূন্য; where= সেখানে ভাগই করেনি, তাই rate-এর শুরুর 0 রয়ে গেছে, আর কোনো সতর্কবার্তাও আসেনি।
where= দিলে out=-ও দিন। শুধু where= দিলে যে ঘরগুলোতে কাজ হয় না, সেখানে NumPy একটি নতুন, শুরু-না-করা (uninitialized) array-এর যা খুশি মান রেখে দেয়; আপনার যন্ত্রে হয়তো শূন্য দেখাবে, আরেকটিতে আবর্জনা সংখ্যা। NumPy-র docs-ও এ ব্যাপারে সতর্ক করে। তাই out=np.zeros(...) বা যে মান “ফাঁকা” বোঝায়, তা দিয়ে আগে array বানিয়ে নিন।
প্রতিটি দুই-input ufunc-এর সঙ্গে কয়েকটি method-ও আসে, যা পরের অংশের সেতু: np.add.reduce(a) সব উপাদান যোগ করে একটি সংখ্যা দেয় (এটিই np.sum), আর np.add.accumulate(a) চলতি যোগফল দেয় (এটিই np.cumsum). মানে সারাংশের function-গুলো আসলে ufunc-কে বারবার প্রয়োগ করা।
২ · Float-এর কিনারা — inf, nan আর শূন্য দিয়ে ভাগ
Python-এ 1 / 0 লিখলে প্রোগ্রাম থেমে যায় ZeroDivisionError দিয়ে। NumPy-র float array-তে একই ভাগ থামে না; ফলের ঘরে একটি বিশেষ মান বসে, আর সাধারণত একটি সতর্কবার্তা (warning) ছাপা হয়। কারণ NumPy floating-point সংখ্যার আন্তর্জাতিক নিয়ম IEEE 754 মেনে চলে, যেখানে এমন কিছু “সংখ্যা” আছে যা সাধারণ সংখ্যা নয়:
- inf (অসীম):
900.0 / 0.0হলোinf,-900.0 / 0.0হলো-inf. খুব বড় ফল, যেমনnp.exp(1000.0), float-এ না ধরলে সেটিওinfহয় (একে বলে overflow). - nan (Not a Number, সংখ্যা নয়): যার কোনো অর্থপূর্ণ উত্তর নেই, যেমন
0.0 / 0.0,inf - inf, বাnp.sqrt(-1.0). ডেটায় “ফাঁকা ঘর” বোঝাতেও nan ব্যবহার হয়; pandas-এ CSV-র খালি ঘর পড়লে প্রায়ই nan আসে (পর্ব ২৩)।
nan-এর সবচেয়ে বড় ফাঁদ: nan কোনো কিছুর সমান নয়, নিজেরও নয়। np.nan == np.nan হলো False. তাই if x == np.nan: কখনো সত্যি হয় না, আর ফাঁকা ঘর খুঁজতে এভাবে লিখলে কোড চুপচাপ কিছুই খুঁজে পায় না। সঠিক পরীক্ষা np.isnan(x); আর np.isfinite(x) একসঙ্গে nan আর দুই দিকের inf বাদ দেয়। দ্বিতীয় ফাঁদ: nan ছোঁয়াচে। যেকোনো হিসাবে একটি nan ঢুকলে ফলও nan: nan + 5, nan * 0, আর পুরো array-এর sum-ও।
সতর্কবার্তা আর np.errstate
শূন্য দিয়ে ভাগে NumPy যে সতর্কবার্তা দেয়, তা লেখকের যন্ত্রে সাধারণ Python-এ চালালে দেখতে এমন (সত্যিকারের run, NumPy 2.5.1):
<string>:3: RuntimeWarning: divide by zero encountered in divide
<string>:3: RuntimeWarning: invalid value encountered in divide
<string>:4: RuntimeWarning: divide by zero encountered in log
<string>:4: RuntimeWarning: invalid value encountered in sqrt
“divide by zero” মানে শূন্য দিয়ে ভাগে inf এসেছে; “invalid value” মানে nan এসেছে। NumPy-র উত্তর np.errstate, একটি context manager (Python-এর with ব্লক; বিস্তারিত Python কোর্সের context manager পর্বে)। এর ভেতরে প্রতিটি ধরনের সমস্যার জন্য আচরণ ঠিক করা যায়: "ignore" (চুপ থাকা), "warn" (সতর্ক করা, default), "raise" (FloatingPointError দিয়ে থেমে যাওয়া)। ধরনগুলো হলো divide, invalid, over আর under.
সতর্কবার্তা লুকাবেন না, সীমিত করুন। warnings.filterwarnings("ignore") পুরো প্রোগ্রামের সব সতর্কবার্তা বন্ধ করে দেয়, এমনকি যেগুলো আসল বাগ দেখাত। with np.errstate(divide="ignore"): শুধু একটি জায়গায়, শুধু একটি ধরনের সমস্যা চুপ করায়। আর যেখানে শূন্য দিয়ে ভাগ হওয়াই ভুল, সেখানে divide="raise" দিয়ে প্রোগ্রাম থামিয়ে দিন।
Float তুলনা — np.isclose
পর্ব ৯-এ দেখেছেন, 0.1 + 0.2 == 0.3 হলো False, আর Python-এ তুলনা হয় math.isclose দিয়ে। NumPy-র সমতুল্য np.isclose(a, b), যা ঘরে ঘরে কাজ করে, আর পুরো array একসঙ্গে মেলাতে np.allclose. শর্তটি হলো |a − b| ≤ atol + rtol × |b|, যেখানে rtol হলো আপেক্ষিক সহনশীলতা (default 1e-05, মানে b-এর এক লাখ ভাগের এক ভাগ) আর atol হলো পরম সহনশীলতা (default 1e-08). একটি সূক্ষ্ম ফাঁদ: atol-এর কারণে খুব ছোট সংখ্যা শূন্যের “কাছাকাছি” ধরা পড়ে। np.isclose(1e-9, 0.0) হলো True; আপনার ডেটা যদি নিজেই খুব ছোট এককে হয় (যেমন সম্ভাবনা 10-9), তাহলে atol=0 দিন। চালানো যায় এমন উদাহরণ অংশ ৫-এ।
৩ · Aggregation — অনেক সংখ্যা থেকে একটি সারাংশ
ক্রিকেটের স্কোরকার্ডের কথা ভাবুন। প্রতিটি বলের রান লেখা থাকে, কিন্তু খেলা শেষে আমরা জানতে চাই মোট রান, গড়, সর্বোচ্চ ইনিংস। অনেক সংখ্যা থেকে একটি সংখ্যায় আসার এই কাজের নাম aggregation. ufunc array-এর shape বদলায় না; aggregation এক বা একাধিক দিক ভেঙে ছোট করে। একটি উদাহরণ দিয়ে সবগুলো হাতে হিসাব করি: ঢাকা থেকে চার দিনে (শনি থেকে মঙ্গল) বাজারঘরের অর্ডার এসেছে x = [12, 15, 9, 14].
| function | হাতে হিসাব | ফল |
|---|---|---|
x.sum() | 12 + 15 + 9 + 14 | 50 |
x.mean() | 50 ÷ 4 | 12.5 |
np.median(x) | সাজালে 9, 12, 14, 15; মাঝের দুটির গড় (12 + 14) ÷ 2 | 13.0 |
x.min(), x.max() | সবচেয়ে ছোট আর বড় | 9, 15 |
x.argmin(), x.argmax() | সবচেয়ে ছোট/বড়টির index | 2 (সোম), 1 (রবি) |
x.var() | গড় থেকে দূরত্ব −0.5, 2.5, −3.5, 1.5; বর্গ 0.25 + 6.25 + 12.25 + 2.25 = 21; 21 ÷ 4 | 5.25 |
x.var(ddof=1) | 21 ÷ (4 − 1) | 7.0 |
x.std(), x.std(ddof=1) | √5.25, √7 | 2.291, 2.646 |
np.percentile(x, 25) | সাজানো তালিকায় অবস্থান (4 − 1) × 0.25 = 0.75; 9 + 0.75 × (12 − 9) | 11.25 |
np.percentile(x, 90) | অবস্থান 2.7; 14 + 0.7 × (15 − 14) | 14.7 |
np.cumsum(x) | 12, 12 + 15, 27 + 9, 36 + 14 | [12 27 36 50] |
প্রতিটি সংখ্যা লেখকের যন্ত্রে NumPy দিয়ে মেলানো। এবার শব্দগুলোর মানে। Median হলো সাজানো তালিকার ঠিক মাঝের মান; জোড় সংখ্যক মান থাকলে মাঝের দুটির গড়। Variance (ভেদাঙ্ক, চিহ্ন σ2) হলো গড় থেকে দূরত্বের বর্গের গড়, আর standard deviation (আদর্শ বিচ্যুতি, σ) তার বর্গমূল, যা মূল এককে (এখানে “অর্ডার”) ছড়ানো মাপে। Percentile p মানে এমন একটি মান, যার নিচে মোটামুটি p% ডেটা; ২৫, ৫০ আর ৭৫ percentile-কে quartile বলে, আর ৫০ percentile-ই median. NumPy-র default পদ্ধতি দুই পাশের মানের মাঝে সরলরেখায় ভাগ করে (method="linear"), যা ওপরের হিসাবে দেখলেন; অন্য সফটওয়্যার অন্য পদ্ধতি নিতে পারে, তাই ছোট ডেটায় সংখ্যা সামান্য আলাদা আসতে পারে। cumsum (cumulative sum) চলতি যোগফল: মাসের প্রথম দিন থেকে আজ পর্যন্ত মোট বিক্রি।
ddof — ৪ দিয়ে ভাগ, নাকি ৩ দিয়ে?
ddof (delta degrees of freedom) বলে দেয় ভাগ হবে n − ddof দিয়ে। NumPy-র default ddof=0: চারটি সংখ্যাই যদি আপনার পুরো জগৎ (population) হয়, তাহলে n দিয়ে ভাগ ঠিক। কিন্তু এই চার দিন যদি সারা বছরের একটি নমুনা (sample) হয়, আর আপনি সারা বছরের ছড়ানো অনুমান করতে চান, তাহলে n − 1 দিয়ে ভাগ করলে অনুমানটি গড়ে পক্ষপাতহীন হয় (variance-এর জন্য; একে Bessel's correction বলে)। ব্যবহারিক ফাঁদটি হলো: pandas-এর std আর var-এর default ddof=1, NumPy-র 0. একই ডেটায় দুই library দুই উত্তর দেয় (2.291 বনাম 2.646), আর কেউ ভুল নয়। নমুনা, পক্ষপাতহীন অনুমান আর এর তত্ত্ব আসবে পর্ব ৩৭-এ।
গড় না median? ঈদের আগের দিন ঢাকা থেকে ১২০টি অর্ডার এলো: [12, 15, 9, 14, 120]. গড় লাফিয়ে 34.0, অথচ median মাত্র 14.0 (লেখকের যন্ত্রে মেলানো)। একটি অস্বাভাবিক বড় মান (outlier) গড়কে টেনে নেয়, median-কে প্রায় নড়াতে পারে না। ডেলিভারির সময়, বেতন বা দামের মতো এক দিকে লম্বা লেজের ডেটায় তাই গড়ের সঙ্গে median-ও দেখান। কোন সারাংশ কোন প্রশ্নের উত্তর দেয়, তার পূর্ণ আলোচনা পর্ব ৩৭-এ।
৪ · axis — জেলা × দিনের টেবিলে কোন দিকে হিসাব
এক জেলার চার দিনের ডেটা ছিল এক মাত্রার। আসল ডেটা প্রায় সবসময় টেবিল: বাজারঘরের তিনটি জেলা (ঢাকা, চট্টগ্রাম, খুলনা) আর চারটি দিন (শনি, রবি, সোম, মঙ্গল)। সারি জেলা, কলাম দিন, তাই shape (3, 4). এখন orders.sum() লিখলে সব ১২টি ঘর যোগ হয়ে একটি সংখ্যা আসে। কিন্তু ম্যানেজার জানতে চান দুটি আলাদা প্রশ্ন: “প্রতিটি দিনে মোট কত?” আর “প্রতিটি জেলায় মোট কত?” এটিই axis argument-এর কাজ।
নিয়মটি একটি বাক্যে: axis=k মানে k নম্বর মাত্রাটি ভেঙে যাবে (collapse), বাকিগুলো থাকবে। shape (3, 4)-এ মাত্রা ০ হলো সারি (৩টি জেলা), মাত্রা ১ হলো কলাম (৪টি দিন)।
axis=0: সারির দিকটি ভাঙে। হিসাব চলে ওপর থেকে নিচে, প্রতিটি কলামের ভেতরে। ফল প্রতিটি দিনের জন্য একটি সংখ্যা, shape(4,).axis=1: কলামের দিকটি ভাঙে। হিসাব চলে বাঁ থেকে ডানে, প্রতিটি সারির ভেতরে। ফল প্রতিটি জেলার জন্য একটি সংখ্যা, shape(3,).
অনেকে উল্টো মনে রাখেন: “axis=0 মানে সারি, তাই সারির যোগফল।” না; axis=0 মানে সারিগুলো মিশে যায়, তাই যা বাকি থাকে তা কলাম। মনে রাখার সহজ উপায়: ফলের shape লিখুন, আর axis-এর ঘরটি কেটে দিন। (3, 4) থেকে axis=0 কাটলে (4,). নিচের ধাপে দেখুন।
- টেবিল। সারি = জেলা (মাত্রা ০, দৈর্ঘ্য ৩), কলাম = দিন (মাত্রা ১, দৈর্ঘ্য ৪)। shape
(3, 4). - axis=0, প্রথম কলাম। শনিবারের তিনটি ঘর ওপর থেকে নিচে যোগ: 12 + 8 + 5 = 25. তিনটি জেলা মিশে একটি সংখ্যা হলো।
- axis=0, দ্বিতীয় কলাম। রবিবার: 15 + 6 + 7 = 28. প্রতিটি কলাম আলাদাভাবে, একই নিয়মে।
- axis=0 শেষ। চারটি কলামের ফল
[25 28 25 24], shape(4,). সারির মাত্রাটি নেই, দিনের মাত্রাটি আছে। রবিবার সবচেয়ে ব্যস্ত দিন। - axis=1, প্রথম সারি। এবার ঢাকার সারি বাঁ থেকে ডানে: 12 + 15 + 9 + 14 = 50. চারটি দিন মিশে একটি সংখ্যা।
- axis=1 শেষ। তিনটি সারির ফল
[50 31 21], shape(3,). কলামের মাত্রাটি নেই, জেলার মাত্রাটি আছে। - যাচাই। দিনগুলোর মোট 25 + 28 + 25 + 24 = 102, জেলাগুলোর মোট 50 + 31 + 21 = 102. একই সংখ্যা দুই পথে;
orders.sum()(axis না দিলে) সব মাত্রা ভেঙে সরাসরি 102 দেয়, shape().
এবার একই হিসাব সত্যিই চালিয়ে দেখুন, প্রতিটি ফলের shape সহ। সঙ্গে mean(axis=1) (প্রতিটি জেলার দৈনিক গড়) আর argmax (সবচেয়ে বড়টির index) দিয়ে “সেরা দিন” খোঁজা। শেষের দুই লাইনে keepdims, যা পরের অংশে ধাপে ধাপে দেখবেন।
import numpy as np
districts = ["ঢাকা", "চট্টগ্রাম", "খুলনা"]
days = ["শনি", "রবি", "সোম", "মঙ্গল"]
orders = np.array([[12, 15, 9, 14], # সারি = জেলা
[ 8, 6, 10, 7], # কলাম = দিন
[ 5, 7, 6, 3]], dtype=np.int64)
print("table shape:", orders.shape)
by_day = orders.sum(axis=0) # ওপর থেকে নিচে, প্রতিটি কলাম
by_district = orders.sum(axis=1) # বাঁ থেকে ডানে, প্রতিটি সারি
print("axis=0:", by_day, by_day.shape)
print("axis=1:", by_district, by_district.shape)
print("সব মিলিয়ে:", orders.sum(), "| দুই পথে:", by_day.sum(), by_district.sum())
for name, avg in zip(districts, orders.mean(axis=1)):
print(f"{name} দিনে গড় {avg:.2f}টি অর্ডার")
print("সেরা দিন:", days[orders.sum(axis=0).argmax()])
print("প্রতিটি জেলার সেরা দিন:", [days[i] for i in orders.argmax(axis=1)])
tot = orders.sum(axis=1, keepdims=True) # shape (3, 1)
print("keepdims:", tot.shape, "→ ভাগের পর", (orders / tot).shape)
np.set_printoptions(precision=3)
print(orders / tot)
print("প্রতি সারির যোগ:", (orders / tot).sum(axis=1))আসল আউটপুট:
table shape: (3, 4)
axis=0: [25 28 25 24] (4,)
axis=1: [50 31 21] (3,)
সব মিলিয়ে: 102 | দুই পথে: 102 102
ঢাকা দিনে গড় 12.50টি অর্ডার
চট্টগ্রাম দিনে গড় 7.75টি অর্ডার
খুলনা দিনে গড় 5.25টি অর্ডার
সেরা দিন: রবি
প্রতিটি জেলার সেরা দিন: ['রবি', 'সোম', 'রবি']
keepdims: (3, 1) → ভাগের পর (3, 4)
[[0.24 0.3 0.18 0.28 ]
[0.258 0.194 0.323 0.226]
[0.238 0.333 0.286 0.143]]
প্রতি সারির যোগ: [1. 1. 1.]
Stepper-এর প্রতিটি সংখ্যা মিলেছে: দিনভিত্তিক [25 28 25 24], জেলাভিত্তিক [50 31 21], দুই পথেই 102. গড়গুলোও হাতে মেলান: ঢাকা 50 ÷ 4 = 12.5, চট্টগ্রাম 31 ÷ 4 = 7.75, খুলনা 21 ÷ 4 = 5.25. argmax(axis=1) প্রতিটি সারিতে সবচেয়ে বড় ঘরের কলাম-index দেয়: ঢাকায় 15 (রবি), চট্টগ্রামে 10 (সোম), খুলনায় 7 (রবি)। শেষ টেবিলটি প্রতিটি জেলার মোট অর্ডারের কত ভাগ কোন দিনে এসেছে: ঢাকার রবিবার 15 ÷ 50 = 0.3. প্রতিটি সারির ভাগগুলোর যোগ 1, যা দেখায় ভাগ ঠিক জেলার মোট দিয়েই হয়েছে, দিনের মোট দিয়ে নয়। dtype=np.int64 স্পষ্ট লেখা হয়েছে, কারণ ব্রাউজারের ৩২-বিট Python-এ dtype না বললে NumPy-র পূর্ণসংখ্যা int32 (পর্ব ১৩)।
তিন বা তার বেশি মাত্রাতেও নিয়ম একই: (7, 3, 4) (সপ্তাহ × জেলা × দিন)-এ sum(axis=(0, 2)) প্রতিটি জেলার মোট দেয়, shape (3,), আর axis=-1 মানে শেষ মাত্রা। পর্ব ২৪-এর groupby-এও একই ভাবনা: কোন দিক মিশবে, কোন দিক থাকবে।
৫ · keepdims আর ফাঁকা দিন — shape ধরে রাখা আর nan-সচেতন হিসাব
keepdims=True কেন লাগে
ওপরের কোডের শেষ অংশে প্রশ্নটি ছিল: প্রতিটি জেলার নিজের মোটের কত ভাগ কোন দিনে? মানে টেবিলের প্রতিটি সারিকে সেই সারির যোগফল দিয়ে ভাগ। স্বাভাবিকভাবে লিখতে ইচ্ছা করে orders / orders.sum(axis=1). কিন্তু এটি error দেয়, কারণ broadcasting-এর নিয়ম shape-গুলো ডান দিক থেকে মেলায় (পর্ব ১৫)। keepdims=True দিলে ভাঙা মাত্রাটি মুছে না গিয়ে দৈর্ঘ্য ১ হয়ে থেকে যায়, আর তখন broadcasting ঠিক যেমন চাই তেমন কাজ করে। ধাপে ধাপে দেখুন।
1tot = orders.sum(axis=1)2orders / tot # (3,4) / (3,)3tot = orders.sum(axis=1, keepdims=True)4share = orders / tot # (3,4) / (3,1)5share.sum(axis=1)
| tot | |
| tot.shape | |
| shape মেলানো | |
| ঢাকার সারি |
- keepdims ছাড়া যোগ।
sum(axis=1)কলামের মাত্রা মুছে দেয়: ফল[50 31 21], shape(3,), মানে এক মাত্রার তিনটি সংখ্যা। - ভাগের চেষ্টা।
(3, 4)আর(3,)ডান দিক থেকে মেলানো হয়: 4 আর 3, সমানও নয়, কোনোটি 1-ও নয়। তাইValueError. NumPy ভেবেছিল আপনি প্রতিটি কলামকে একটি সংখ্যা দিয়ে ভাগ করতে চান, আর কলাম ৪টি, সংখ্যা ৩টি। - keepdims=True. একই যোগফল, কিন্তু কলামের মাত্রাটি দৈর্ঘ্য ১ হয়ে থেকে যায়: shape
(3, 1), মানে তিন সারি আর এক কলামের একটি খাড়া array. - এবার shape মেলে। ডানে 4 আর 1: যেটি 1, সেটি টেনে 4 করা হয় (প্রতিটি জেলার মোট চারটি দিনের পাশে বসে)। বাঁয়ে 3 আর 3, সমান। ফলের shape
(3, 4). - ঢাকার সারি। প্রতিটি ঘর ঢাকার মোট 50 দিয়ে ভাগ: 0.24, 0.3, 0.18, 0.28. চট্টগ্রামের সারি ভাগ হয় 31 দিয়ে, খুলনার 21 দিয়ে।
- যাচাই। প্রতিটি সারির ভাগের যোগ 1. এই এক লাইনের যাচাই নিশ্চিত করে ভাগ সঠিক দিকে হয়েছে। একই কাজ
tot[:, np.newaxis]লিখেও হয়, কিন্তুkeepdims=Trueউদ্দেশ্যটি বেশি স্পষ্ট বলে।
একটি বিপজ্জনক ব্যতিক্রম মনে রাখুন: টেবিলটি যদি বর্গাকার হতো, ধরুন (4, 4), তাহলে orders / orders.sum(axis=1) কোনো error দিত না। (4, 4) আর (4,) ডান থেকে মেলে, তাই NumPy চুপচাপ প্রতিটি কলামকে ভাগ করত একটি সারির মোট দিয়ে। ফল ভুল, কিন্তু চোখে পড়ার মতো কোনো লক্ষণ নেই। এ কারণেই কোনো সারাংশ দিয়ে আবার মূল টেবিলে হিসাব করতে হলে keepdims=True অভ্যাস করুন, আর শেষে একটি যাচাই (যেমন সারির যোগ 1) রাখুন।
ফাঁকা দিন — nanmean আর বন্ধুরা
বাজারঘরের একটি শাখার পাঁচ দিনের বিক্রির খাতা থেকে মঙ্গলবারের পাতাটি হারিয়ে গেছে। ডেটায় সেই ঘরে nan. এখন sales.mean() দেয় nan, কারণ nan ছোঁয়াচে। NumPy-র সমাধান nan-সচেতন function-এর একটি পরিবার: np.nanmean, np.nansum, np.nanmedian, np.nanstd, np.nanmin, np.nanmax, np.nanpercentile. এরা nan-গুলোকে বাদ দিয়ে বাকিগুলোর হিসাব করে। কিন্তু “বাদ দেওয়া” একটি সিদ্ধান্ত: হারানো দিনটি হয়তো ঈদের ছুটি ছিল (বিক্রি আসলে শূন্য), বা হয়তো রেকর্ড হয়নি (বিক্রি হয়েছে কিন্তু জানা নেই)। প্রথম ক্ষেত্রে nan নয়, 0 বসানো উচিত। ফাঁকা ঘর পূরণ করার কৌশল (imputation) pandas দিয়ে পর্ব ২৩-এ।
নিচের কোডে চারটি জিনিস: হারানো দিন আর nanmean; nan == nan-এর ফাঁদ; শূন্য দিয়ে ভাগ, প্রথমে np.errstate দিয়ে ইচ্ছা করে চুপ করানো, তারপর where= দিয়ে এড়ানো, তারপর "raise" দিয়ে ধরার চেষ্টা (এর একটি চমক আছে); আর np.isclose.
import numpy as np
sales = np.array([4200.0, 3900.0, np.nan, 5100.0, 4800.0]) # মঙ্গলবার খাতা হারিয়েছে
print("mean:", sales.mean(), "| nanmean:", np.nanmean(sales))
print("কয়টি ফাঁকা:", np.isnan(sales).sum())
print("nan == nan ?", np.nan == np.nan, "| sales[2] == np.nan ?", sales[2] == np.nan)
print("ঠিক পরীক্ষা:", np.isnan(sales[2]))
revenue = np.array([0.0, 900.0, 1500.0])
orders = np.array([0.0, 0.0, 6.0])
with np.errstate(divide="ignore", invalid="ignore"):
per_order = revenue / orders # 0/0 আর 900/0
print("প্রতি অর্ডার:", per_order)
print("finite?", np.isfinite(per_order))
safe = np.divide(revenue, orders, out=np.zeros(3), where=orders > 0)
print("where দিয়ে নিরাপদ:", safe)
try:
with np.errstate(divide="raise"):
revenue[1] / orders[1:2]
except FloatingPointError as e:
print("ধরা পড়ল:", e) # সাধারণ Python-এ
else:
print("কিছু ধরা পড়েনি (ব্রাউজারের Python)")
a = 0.1 + 0.2
print(a == 0.3, np.isclose(a, 0.3), np.isclose(1e-9, 0.0), np.isclose(1e-9, 0.0, atol=0))
print("inf + 1 =", np.inf + 1, "| inf - inf =", np.inf - np.inf)আসল আউটপুট:
mean: nan | nanmean: 4500.0
কয়টি ফাঁকা: 1
nan == nan ? False | sales[2] == np.nan ? False
ঠিক পরীক্ষা: True
প্রতি অর্ডার: [ nan inf 250.]
finite? [False False True]
where দিয়ে নিরাপদ: [ 0. 0. 250.]
কিছু ধরা পড়েনি (ব্রাউজারের Python)
False True True False
inf + 1 = inf | inf - inf = nan
হাতে মেলান: চারটি জানা দিনের যোগ 4200 + 3900 + 5100 + 4800 = 18000, ভাগ 4 = 4500.0; nanmean ঠিক এটিই করেছে, পাঁচ দিয়ে নয়, চার দিয়ে ভাগ। sales[2] == np.nan মিথ্যা, যদিও ঘরটি সত্যিই nan; শুধু np.isnan সত্যি বলছে। ভাগের লাইনে 0 ÷ 0 = nan আর 900 ÷ 0 = inf, আর isfinite দিয়ে সমস্যার ঘর দুটি ধরা গেছে; where=-এর পথে সেই ঘরগুলোতে ভাগই হয়নি। এরপরের লাইনটি এই পাতার একটি সত্যিকারের সীমা দেখায়: ব্রাউজারের Python (Pyodide) WebAssembly-তে চলে, যেখানে float-এর এই সতর্কসংকেত (floating-point exception flag) নেই। তাই এখানে NumPy শূন্য দিয়ে ভাগ টেরই পায় না: সতর্কবার্তাও আসে না, "raise"-ও কাজ করে না, আর else অংশটি চলে। লেখকের যন্ত্রে সাধারণ Python 3.14.4 আর NumPy 2.5.1-এ একই কোডের সেই লাইন ছিল ধরা পড়ল: divide by zero encountered in divide (সত্যিকারের run), আর errstate ছাড়া ভাগের লাইনে অংশ ২-এর দুটি RuntimeWarning আসত। শিক্ষা: সতর্কবার্তার ওপর ভরসা না করে np.isfinite দিয়ে ফল নিজে যাচাই করুন, কারণ পরিবেশভেদে সতর্কবার্তা না-ও আসতে পারে। শেষের আগের লাইনে isclose: 0.1 + 0.2 আর 0.3 “কাছাকাছি” (True), 10-9 আর 0 default atol-এ কাছাকাছি, কিন্তু atol=0-এ নয়।
৬ · Random-এর নতুন পথ — default_rng আর Generator-এর কাজ
পর্ব ১১-এ দেখেছেন, কম্পিউটারের random সংখ্যা আসে একটি PRNG থেকে, যা একটি seed থেকে শুরু করে একটি নির্দিষ্ট ধারা দেয়। এখানে দেখব NumPy-তে সেই যন্ত্রটি কীভাবে সাজানো, আর ডেটার কাজে কোন method কখন লাগে।
NumPy-র আধুনিক random ব্যবস্থার তিনটি স্তর। নিচের ছবিতে ওপর থেকে নিচে পড়ুন।
Seed থেকে নমুনা — NumPy-র random ব্যবস্থার তিন স্তর
seed → SeedSequence → BitGenerator (PCG64) → Generator → methods; spawn gives independent child streams
np.random.default_rng(42) এক লাইনে তিনটি কাজ করে: seed থেকে একটি SeedSequence, তা থেকে PCG64 নামের BitGenerator, আর তার ওপরে একটি Generator. আপনি সবসময় শুধু ওপরের Generator (rng)-এর method ডাকেন। নিচে spawn: একই উৎস থেকে এমন সন্তান stream, যাদের ধারা একে অন্যের সঙ্গে না মেলার সম্ভাবনা অত্যন্ত বেশি (অংশ ৭)।কেন পুরোনো np.random.seed নয়
অনেক পুরোনো টিউটোরিয়াল আর AI সহকারীর কোডে এখনো দেখবেন np.random.seed(42) আর np.random.randint(...). এগুলো legacy (পুরোনো, শুধু পেছনের সামঞ্জস্যের জন্য রাখা) পদ্ধতি: পুরো প্রোগ্রামে একটিমাত্র লুকানো global অবস্থা, যার ভেতরে Mersenne Twister (MT19937) নামের PRNG. সমস্যা তিনটি। এক, কোনো library-র ভেতরের একটি ডাক সেই একই ধারা থেকে সংখ্যা নিলে আপনার পরের সব সংখ্যা সরে যায়, আর কেন, তা কোড দেখে বোঝা যায় না। দুই, দুটি আলাদা কাজের জন্য আলাদা ধারা রাখা কঠিন। ২০১৯ সালে NumPy 1.17-এ নতুন ব্যবস্থা আসে (NumPy-র নকশা দলিল NEP 19-এ এর যুক্তি আছে): default_rng একটি নিজস্ব Generator ফেরত দেয়, যা আপনি একটি সাধারণ variable-এর মতো function-এ পাঠাতে পারেন। Default BitGenerator হলো PCG64, যা ছোট অবস্থায় দ্রুত আর পরিসংখ্যানের পরীক্ষায় ভালো ফল দেয় বলে বেছে নেওয়া হয়েছিল। NumPy-র docs নতুন কোডে Generator ব্যবহারের পরামর্শ দেয়।
ডেটার কাজের প্রধান method
| method | কী দেয় | খেয়াল রাখুন |
|---|---|---|
rng.integers(lo, hi, size) | lo থেকে hi-এর আগে পর্যন্ত পূর্ণসংখ্যা | ছক্কা চাইলে integers(1, 7); শেষ মান ধরতে endpoint=True. default dtype int64 |
rng.random(size) | [0, 1) পরিসরে সমান সম্ভাবনার float | 0 আসতে পারে, 1 কখনো না |
rng.normal(loc, scale, size) | গড় loc, standard deviation scale-এর normal distribution | ঋণাত্মক মানও আসে; চাহিদায় clip লাগতে পারে |
rng.choice(a, size, replace, p) | a থেকে নমুনা; p দিলে ওজনসহ | p-এর যোগ 1 হতে হবে; replace=False-এ size ≤ len(a) |
rng.permutation(a) | এলোমেলো ক্রমের একটি নতুন copy | মূল array অপরিবর্তিত |
rng.shuffle(a) | মূল array-কেই জায়গায় এলোমেলো করে | ফেরত দেয় None; 2D-তে শুধু প্রথম axis (সারির ক্রম) |
choice-এর দুটি রূপের পার্থক্য বাস্তবে গুরুত্বপূর্ণ। ফেরত দিয়ে (with replacement, default) মানে লটারির বাক্স থেকে একটি কাগজ তুলে নাম লিখে আবার বাক্সে রাখা: একই জিনিস বারবার আসতে পারে। ফেরত ছাড়া (replace=False) মানে কাগজ আর ফেরত যায় না: ৫ জন কর্মীর মধ্যে ৩ জনকে আলাদা আলাদা কাজে বাছাই, বা ডেটা থেকে train আর test সারি বাছাই, যেখানে একই সারি দুবার আসা চলবে না। আর p= দিয়ে weighted sampling: বাজারঘরে যদি ৬০% অর্ডার ক্যাশ অন ডেলিভারি (COD), ৩০% bKash আর ১০% কার্ডে হয়, তাহলে p=[0.6, 0.3, 0.1] দিলে বানানো অর্ডারগুলোতেও প্রায় সেই অনুপাত আসে।
import numpy as np
rng_a = np.random.default_rng(42)
rng_b = np.random.default_rng(42) # একই seed, আলাদা generator
print("একই seed:", rng_a.integers(1, 7, size=5), rng_b.integers(1, 7, size=5))
print("পরের ৫টি:", rng_a.integers(1, 7, size=5)) # stream এগিয়ে গেছে
rng = np.random.default_rng(2026)
print("random():", rng.random(3).round(3)) # [0, 1)
demand = rng.normal(loc=40, scale=6, size=7) # দৈনিক চাহিদা
print("normal:", demand.round(1))
print("দৈনিক অর্ডার (পূর্ণসংখ্যা):", np.clip(np.rint(demand), 0, None).astype(np.int64))
products = np.array(["চা", "ডাল", "চাল", "তেল", "চিনি"])
print("ফেরত দিয়ে:", rng.choice(products, size=6))
print("ফেরত ছাড়া:", rng.choice(products, size=3, replace=False))
pay = rng.choice(["COD", "bKash", "card"], size=10_000, p=[0.6, 0.3, 0.1])
kinds, counts = np.unique(pay, return_counts=True)
print(dict(zip(kinds.tolist(), (counts / pay.size).round(3).tolist())))
ids = np.arange(1, 7)
print("permutation:", rng.permutation(ids), "| মূল:", ids)
rng.shuffle(ids) # জায়গাতেই বদলায়
print("shuffle-এর পরে মূল:", ids)
child_rngs = np.random.default_rng(7).spawn(2) # আলাদা stream
print("দুই সন্তান stream:", [r.integers(0, 100, size=3).tolist() for r in child_rngs])আসল আউটপুট:
একই seed: [1 5 4 3 3] [1 5 4 3 3]
পরের ৫টি: [6 1 5 2 1]
random(): [0.179 0.64 0.467]
normal: [48.4 43.8 38.2 38.1 41.8 38.4 38.6]
দৈনিক অর্ডার (পূর্ণসংখ্যা): [48 44 38 38 42 38 39]
ফেরত দিয়ে: ['চা' 'চিনি' 'তেল' 'চিনি' 'ডাল' 'তেল']
ফেরত ছাড়া: ['ডাল' 'চা' 'তেল']
{'COD': 0.594, 'bKash': 0.298, 'card': 0.108}
permutation: [4 2 1 3 6 5] | মূল: [1 2 3 4 5 6]
shuffle-এর পরে মূল: [2 3 6 5 1 4]
দুই সন্তান stream: [[30, 79, 20], [68, 48, 39]]
প্রথম লাইন: দুটি আলাদা Generator, একই seed, হুবহু এক ধারা। দ্বিতীয় লাইন: একই Generator আবার ডাকলে পরের সংখ্যা আসে। “ফেরত দিয়ে” লাইনে চিনি আর তেল দুবার করে এসেছে; “ফেরত ছাড়া” লাইনে তিনটিই আলাদা। ওজনসহ ১০,০০০টি অর্ডারে অনুপাত 0.594, 0.298, 0.108: ঠিক 0.6, 0.3, 0.1 নয়, কাছাকাছি। এই ছোট পার্থক্যই নমুনার ভুল (sampling error), যা অংশ ৮-এ মাপবেন। permutation-এর পর মূল ids অপরিবর্তিত, কিন্তু shuffle-এর পর মূলটিই বদলে গেছে। একই seed দিলে আপনার পর্দাতেও হুবহু এই সংখ্যাগুলো আসার কথা; NumPy-র ভিন্ন version বা ভিন্ন ক্রমে ডাকলে বদলাতে পারে (অংশ ৭)।
সাধারণ ভুল: ids = rng.shuffle(ids). shuffle জায়গায় কাজ করে আর None ফেরত দেয়, তাই এই লাইনের পর ids হয়ে যায় None. নতুন copy চাইলে permutation, মূলটি বদলাতে চাইলে শুধু rng.shuffle(ids). আর এক মাত্রার বদলে 2D array-তে সারিগুলোর ক্রম এলোমেলো করতে rng.permutation(len(X)) দিয়ে একটি index-ক্রম বানিয়ে X[idx] আর y[idx] দুটিতেই একই ক্রম বসান, যাতে feature আর label জোড়া না ভাঙে।
৭ · পুনরুৎপাদন আর আলাদা stream — seed, spawn আর SeedSequence
Reproducibility-এর জন্য seed দরকার, কিন্তু যথেষ্ট নয়। NumPy-র docs-এর “compatibility policy” পাতা নিজেই শর্তগুলো লিখেছে: একই BitGenerator, একই seed, একই ক্রমে একই argument দিয়ে একই method ডাকা, একই NumPy build, একই পরিবেশ, একই যন্ত্র হলে একই ধারা পাওয়ার কথা, আর docs মেনে নেয় শর্তগুলো “খুব কড়া”। কয়েকটি ব্যবহারিক ফল:
- ডাকার আকারও ধারার অংশ। docs অনুযায়ী
rng.random()পাঁচবার ডাকা আরrng.random(5)একবার ডাকা একই সংখ্যা দেবে, এমন নিশ্চয়তা নেই। - Version বদলালে Generator-এর ধারা বদলাতে পারে। নতুন feature বা গতির জন্য Generator-এর ধারা ভাঙার অনুমতি আছে, তবে সাবধানে আর শুধু feature release-এ; ভুল সংশোধনে bugfix release-এও। উল্টো দিকে পুরোনো
RandomState-এর ধারা স্থির রাখা হয়, ভুল থাকলেও আর বদলানো হবে না। তাই seed-এর পাশে NumPy-র version লিখে রাখুন, lockfile দিয়ে (পর্ব ১২)। - মাঝখানে একটি নতুন ডাক পুরো ধারা সরিয়ে দেয়। Simulation-এর মাঝে একটি debug লাইনে
rng.random()ঢোকালেই পরের সব ফল বদলে যায়। প্রতিটি আলাদা উদ্দেশ্যের জন্য আলাদা stream এর সমাধান।
আলাদা stream — কেন seed + 1 নয়
ধরুন বাজারঘরের simulation-এ দুটি অংশ: চাহিদা আর কুরিয়ারের দেরি। আপনি চান চাহিদার অংশে কোড বদলালেও কুরিয়ারের সংখ্যা না বদলাক। আবার একই simulation চার CPU core-এ ভাগ করে চালালে প্রতিটি core-এর আলাদা ধারা লাগবে। সহজ চিন্তা: core ০-এর seed 42, core ১-এর 43, এভাবে। NumPy-র parallel random পাতা এই পদ্ধতিকে স্পষ্ট ভাষায় “UNSAFE” বলে: এক run-এ ধারাগুলো আলাদা হলেও, আরেক দিন root seed 43 দিয়ে চালালে আগের run-এর core ১-এর ধারাই আবার আসে, ফলে বিভিন্ন run-এর ফল গোপনে একে অন্যের ওপর নির্ভর করে, আর গড়ে পক্ষপাত ঢোকে।
নিরাপদ পথ দুটি। প্রথমটি spawn: np.random.default_rng(7).spawn(2) একই উৎস থেকে দুটি সন্তান Generator দেয় (ওপরের কোডের শেষ লাইন)। প্রতিটি সন্তানের SeedSequence-এ মূল seed-এর সঙ্গে সন্তানের অবস্থানও মিশে থাকে, তাই docs-এর ভাষায় “অত্যন্ত উচ্চ সম্ভাবনায়” স্বাধীন ধারা আসে। Generator.spawn এসেছে তুলনামূলক নতুন NumPy-তে (রিলিজ নোট অনুযায়ী 1.25); পুরোনো version-এ একই কাজ হয় np.random.SeedSequence(7).spawn(2) দিয়ে, তারপর প্রতিটি থেকে default_rng(child). দ্বিতীয় পথ: seed হিসেবে একটি list দেওয়া, যেমন default_rng([worker_id, root_seed]); docs এটিকে seed + i-এর নিরাপদ বিকল্প হিসেবে দেখায়।
root = np.random.SeedSequence(2026)
demand_ss, courier_ss = root.spawn(2) # প্রতিটি অংশের নিজস্ব উৎস
rng_demand = np.random.default_rng(demand_ss)
rng_courier = np.random.default_rng(courier_ss)
print(root.entropy) # 2026 — log-এ লিখে রাখুন
এখানে seed না দিলে SeedSequence() অপারেটিং সিস্টেম থেকে একটি বড় এলোমেলো সংখ্যা নেয়, আর root.entropy সেটি দেখায়। প্রথমবার seed ছাড়া চালিয়ে সেই সংখ্যাটি log-এ লিখে রাখলে পরে হুবহু একই run আবার পাওয়া যায়; প্রতিটি নতুন পরীক্ষায় “৪২”-ই না বসানোর একটি পরিচ্ছন্ন উপায়।
৮ · Simulation আর Monte Carlo — দেরিতে ডেলিভারির সম্ভাবনা
বাজারঘরের ম্যানেজারের প্রশ্ন: “একটি অর্ডার ক্রেতার হাতে পৌঁছাতে ৩ দিনের বেশি লাগার সম্ভাবনা কত?” ডেলিভারির সময় দুটি অংশের যোগফল। গুদামে প্যাক হতে লাগে ০, ১ বা ২ দিন, সম্ভাবনা যথাক্রমে ০.৫, ০.৩, ০.২। কুরিয়ারে লাগে ১, ২ বা ৩ দিন, সম্ভাবনা ০.৬, ০.৩, ০.১। ধরে নিচ্ছি দুটি অংশ একে অন্যের ওপর নির্ভর করে না (independent; এর সঠিক অর্থ পর্ব ৩৫-এ)।
আগে হাতে হিসাব
মোট সময় ৩ দিনের বেশি, মানে অন্তত ৪ দিন। কোন কোন জোড়ায় তা হয়?
- গুদাম ১, কুরিয়ার ৩: 0.3 × 0.1 = 0.03
- গুদাম ২, কুরিয়ার ২: 0.2 × 0.3 = 0.06
- গুদাম ২, কুরিয়ার ৩: 0.2 × 0.1 = 0.02
P(মোট > 3) = 0.03 + 0.06 + 0.02 = 0.11
স্বাধীন দুটি অংশের সম্ভাবনা গুণ হয়, আর আলাদা আলাদা জোড়া (যারা একসঙ্গে ঘটতে পারে না) যোগ হয়। এখানে হাতে হিসাব সহজ, কারণ মাত্র ৯টি জোড়া। কিন্তু অংশ অনেক হলে, বা অংশগুলো একে অন্যের ওপর নির্ভর করলে (বৃষ্টিতে কুরিয়ার ধীর), সূত্র না খুঁজে অনেকবার ঘটনাটি ঘটিয়ে দেখা সহজ। এর নাম Monte Carlo পদ্ধতি: random নমুনা দিয়ে একটি সংখ্যার অনুমান। এখানে সঠিক উত্তর 0.11 জানা আছে বলেই আমরা অনুমানের ভুল মাপতে পারব।
একটি trial (একবারের পরীক্ষা) মানে একটি কাল্পনিক অর্ডার: ওজনসহ choice দিয়ে গুদামের দিন, আরেকটি দিয়ে কুরিয়ারের দিন, যোগ, তারপর “৩-এর বেশি?” প্রশ্নের True বা False. অনেক trial-এর মধ্যে True-র ভাগই সম্ভাবনার অনুমান। নিচে seed 0 দিয়ে প্রথম ছয়টি trial একটি একটি করে দেখুন।
1rng = np.random.default_rng(0)2store = rng.choice([0, 1, 2], size=6, p=[.5, .3, .2])3courier = rng.choice([1, 2, 3], size=6, p=[.6, .3, .1])4late = store + courier > 35late.cumsum() / np.arange(1, 7)
| trial | |
| গুদাম + কুরিয়ার | |
| > 3 ? | |
| এ পর্যন্ত দেরি | |
| অনুমান |
- Trial ১. গুদাম ১ দিন, কুরিয়ার ২ দিন, মোট ৩। “৩-এর বেশি” নয়, তাই False. এক trial-এর পর অনুমান 0/1 = 0.
- Trial ২. 0 + 2 = 2, False. অনুমান এখনো 0.
- Trial ৩. 0 + 1 = 1, দ্রুততম সম্ভাব্য ডেলিভারি। False.
- Trial ৪. 0 + 3 = 3. ঠিক সীমায়, কিন্তু শর্ত
> 3, তাই False. সীমার মান কোন দিকে পড়বে (>না>=), তা প্রশ্নের ভাষা থেকে ঠিক করতে হয়। - Trial ৫. 2 + 2 = 4, প্রথম দেরি! True. অনুমান লাফিয়ে 1/5 = 0.20, আসল মানের প্রায় দ্বিগুণ।
- Trial ৬. 2 + 1 = 3, False. অনুমান 1/6 ≈ 0.17.
- সারাংশ। ছয় trial-এ অনুমান 0 থেকে 0.20 পর্যন্ত নেচেছে। কোড আসলে loop ছাড়াই সব trial একসঙ্গে চালায়;
late.cumsum()চলতি দেরির সংখ্যা, আর তাকে trial-এর সংখ্যা দিয়ে ভাগ করলে এই চলতি অনুমান। অল্প নমুনায় অনুমান অনির্ভরযোগ্য; কতটা, তা পরের কোডে মাপুন।
নমুনা বাড়লে ভুল কতটা কমে
True/False-এর গড়কে অনুমান ধরলে তার ভুলের সাধারণ মাপ হলো standard error:
SE = √( p (1 − p) / n )
এখানে p আসল সম্ভাবনা (0.11) আর n trial-এর সংখ্যা। n = 100 হলে SE = √(0.11 × 0.89 / 100) = √0.000979 ≈ 0.031; n = 10,000 হলে ≈ 0.0031; n = 10,00,000 (দশ লাখ) হলে ≈ 0.0003. নিয়মটি হলো ভুল কমে 1/√n হারে: নমুনা ১০০ গুণ করলে ভুল ১০ ভাগের ১ ভাগ, আর ভুল অর্ধেক করতে নমুনা লাগে ৪ গুণ। অনেক নমুনার গড় যে আসল মানের দিকে যায়, তার পেছনের নিয়মটির নাম law of large numbers; তত্ত্বটি পর্ব ৩৬–৩৭-এ। নিচের কোডে তিনটি আকারে অনুমান চালান, আর শেষে n = 100-এর পরীক্ষাটি ২০০ বার করে দেখুন অনুমান কতটা ছড়ায়।
import numpy as np
EXACT = 0.11 # হাতে হিসাব (অংশ ৮)
rng = np.random.default_rng(42)
def estimate(n):
store = rng.choice([0, 1, 2], size=n, p=[0.5, 0.3, 0.2]) # গুদাম
courier = rng.choice([1, 2, 3], size=n, p=[0.6, 0.3, 0.1]) # কুরিয়ার
late = (store + courier) > 3 # ৩ দিনের বেশি
return late.mean() # True-র ভাগ = সম্ভাবনার অনুমান
print("n │ অনুমান │ আসল থেকে দূরত্ব │ প্রত্যাশিত ভুল (SE)")
for n in (100, 10_000, 1_000_000):
p_hat = estimate(n)
se = np.sqrt(EXACT * (1 - EXACT) / n) # standard error
print(f"{n:>9,} {p_hat:>8.4f} {abs(p_hat - EXACT):>8.4f} {se:>8.4f}")
# একই n=100 পরীক্ষা ২০০ বার: অনুমান কতটা ছড়ায়?
many = np.array([estimate(100) for _ in range(200)])
print("n=100, ২০০ বার: সর্বনিম্ন", many.min(), "সর্বোচ্চ", many.max(),
"std", many.std().round(4))আসল আউটপুট:
n │ অনুমান │ আসল থেকে দূরত্ব │ প্রত্যাশিত ভুল (SE)
100 0.0600 0.0500 0.0313
10,000 0.1109 0.0009 0.0031
1,000,000 0.1098 0.0002 0.0003
n=100, ২০০ বার: সর্বনিম্ন 0.03 সর্বোচ্চ 0.18 std 0.0304
১০০টি অর্ডারে অনুমান এসেছে 0.06, আসল 0.11 থেকে 0.05 দূরে, যা প্রত্যাশিত ভুলের প্রায় দেড় গুণ; এমন ঘটনা অস্বাভাবিক নয়। ১০,০০০-এ দূরত্ব 0.0009, দশ লাখে 0.0002. প্রতিটি ধাপে নমুনা ১০০ গুণ, আর প্রত্যাশিত ভুল (শেষ কলাম) ঠিক ১০ ভাগের ১ ভাগ। আসল দূরত্ব একটি একক run-এর ভাগ্য, তাই তা কখনো SE-র চেয়ে ছোট, কখনো বড়। শেষ লাইনটি সবচেয়ে শিক্ষণীয়: একই n = 100-এর পরীক্ষা ২০০ বার করলে অনুমান 0.03 থেকে 0.18 পর্যন্ত ছড়িয়েছে, আর তাদের standard deviation 0.0304, সূত্রের SE 0.0313-এর প্রায় সমান। মানে সূত্রটি সত্যিই বলে দেয় একটি অনুমান কতটা নড়বে।
একই ছাঁচে মজুত ফুরিয়ে যাওয়ার সম্ভাবনাও অনুমান করা যায় (অনুশীলন ৪)। তবে মডেলটি ভুল হলে (ধরুন বৃষ্টির দিনে কুরিয়ার ধীর হয়, অথচ আমরা দুটি অংশকে স্বাধীন ধরেছি) কোটি trial-ও ভুল উত্তরকেই নিখুঁতভাবে অনুমান করবে। Monte Carlo শুধু হিসাবের ভুল কমায়, অনুমানের (assumption) ভুল নয়। Randomized algorithm হিসেবে Monte Carlo আর Las Vegas-এর পার্থক্য আছে ABCL TECH-এর DSA কোর্সের randomized algorithm পর্বে।
আরও গভীরে। PCG পরিবারের PRNG প্রস্তাব করেন Melissa O'Neill (২০১৪ সালের একটি technical report), আর Mersenne Twister প্রকাশ করেন Makoto Matsumoto ও Takuji Nishimura (১৯৯৮)। দুটিই cryptography-র জন্য নিরাপদ নয়; পাসওয়ার্ড বা token বানাতে Python-এর secrets module লাগে। NumPy-র choice-এ p দিলে ভেতরে মোটামুটি এভাবে কাজ হয়: সম্ভাবনাগুলোর চলতি যোগফল (cumsum) বানিয়ে একটি [0, 1) random সংখ্যা কোন খোপে পড়ে, তা খোঁজা; মানে এই পর্বের cumsum আর random একসঙ্গে। আর Monte Carlo-র ভুল 1/√n হারে কমা ধীর মনে হলে, একই কাজ কম নমুনায় করার কৌশল আছে (variance reduction, quasi-Monte Carlo), যা বিশেষায়িত বইয়ের বিষয়।
পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):
- shape
(3, 4)-এর array-এsum(axis=0)-এর shape কী?(4,); মাত্রা ০ (সারি) ভেঙে যায়, প্রতিটি কলামে একটি যোগফল। np.nan == np.nanকেনFalse, আর ফাঁকা ঘর কীভাবে খুঁজবেন? IEEE 754-এ nan কোনো কিছুর সমান নয়;np.isnanব্যবহার করুন।- NumPy আর pandas-এর
stdআলাদা উত্তর দেয় কেন? NumPy-র defaultddof=0(n দিয়ে ভাগ), pandas-এরddof=1(n − 1 দিয়ে)। shuffleআরpermutation-এর পার্থক্য? প্রথমটি মূল array জায়গায় বদলায় আরNoneদেয়; দ্বিতীয়টি নতুন copy দেয়।- Monte Carlo অনুমানের ভুল অর্ধেক করতে কত নমুনা লাগে? চার গুণ, কারণ ভুল কমে 1/√n হারে।
৯ · AI-এর যুগে — AI সহকারীর লেখা NumPy আর simulation যাচাই
“বিক্রির টেবিল থেকে প্রতিটি জেলার ভাগ বের করো” বা “ডেলিভারির দেরি simulate করো” লিখলে একটি AI সহকারী কয়েক সেকেন্ডে কোড দেবে। এই পর্বের বিষয়গুলোতেই এমন কোড সবচেয়ে বেশি চুপচাপ ভুল করে। AI সহকারীরা প্রায়ই যা করে:
- পুরোনো API লেখে:
np.random.seedআরnp.random.randint, NumPy 1.24-এ সরানোnp.floatবাnp.int, বা pandas-এর সরিয়ে দেওয়াappend; কখনো এমন parameter বানায় যা নেই (যেমনnp.mean(..., skipna=True), যা pandas-এর, NumPy-র নয়)। axisউল্টো দেয়, আর বর্গাকার টেবিলে সেই ভুল কোনো error ছাড়াই পার পেয়ে যায়;keepdimsছাড়া ভাগ করে।x == np.nanদিয়ে ফাঁকা খোঁজে,mean-এর nan-কেfillna(0)দিয়ে “ঠিক” করে (ফাঁকাকে শূন্য বিক্রি বানিয়ে), আরwarnings.filterwarnings("ignore")দিয়ে সব সতর্কবার্তা চাপা দেয়।- NumPy আর pandas-এর
stdমিশিয়ে ফেলে; অল্প trial-এর Monte Carlo ফলকে নিশ্চিত সত্য হিসেবে লেখে, ভুলের মাপ ছাড়াই; seed বদলে বদলে “ভালো” ফলটি দেখায়। - মডেলের কাজে ডেটা ভাগের আগেই পুরো ডেটার গড় বা std দিয়ে scale করে, ফলে test ডেটার তথ্য train-এ ঢুকে যায় (data leakage; মডেলের পর্বগুলোতে বিস্তারিত), আর train ডেটাতেই মাপা ফল দেখায়।
কোড চাওয়ার সময় shape, একক, ফাঁকা ঘরের নিয়ম আর যাচাইয়ের উপায় নিজেই বলে দিন। নিচের প্রম্পটে আসল ডেটা নেই, শুধু বানানো ছোট টেবিল।
AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১nan কেন নিজের সমান নয়? এটি কি নকশার ভুল, নাকি ভেবেচিন্তে নেওয়া সিদ্ধান্ত?
১৯৮০-এর দশকের শুরুতে বিভিন্ন কম্পিউটার নির্মাতা float-এর হিসাব নিজের নিজের নিয়মে করত। একই প্রোগ্রাম এক যন্ত্রে এক উত্তর, আরেকটিতে আরেক উত্তর দিত, আর শূন্য দিয়ে ভাগের মতো ঘটনায় কোনো যন্ত্র থেমে যেত, কোনোটি আবর্জনা সংখ্যা দিত। এই বিশৃঙ্খলা দূর করতে IEEE-র একটি কমিটি কাজ করে, যার প্রধান স্থপতিদের একজন ছিলেন William Kahan; ফল IEEE 754 standard, প্রথম প্রকাশ ১৯৮৫ সালে (পরে ২০০৮ আর ২০১৯-এ হালনাগাদ)। inf আর nan এই standard-এরই অংশ।
nan-এর পেছনের ধারণাটি হলো: একটি অর্থহীন হিসাব (০ ÷ ০) প্রোগ্রাম না থামিয়ে একটি চিহ্নিত মান দিক, যা পরের হিসাবে ছড়িয়ে পড়ে আর শেষে দেখা যায়। এতে লম্বা হিসাবের মাঝখানে একটি ঘর খারাপ হলে পুরো কাজ থামে না, আবার খারাপ ফলটি লুকিয়েও থাকে না। তুলনার নিয়মটি এর সঙ্গে মিল রেখে বানানো: nan কোনো সংখ্যা নয়, তাই তা কোনো কিছুর “সমান”, “ছোট” বা “বড়” নয়। দুটি nan দুটি আলাদা অর্থহীন হিসাব থেকে আসতে পারে (একটি ০ ÷ ০, আরেকটি √−১), তাদের সমান বলার যুক্তি নেই।
একটি বহুল প্রচলিত ব্যাখ্যা (Kahan-এর সঙ্গে যুক্ত করে বলা হয়, তবে খুঁটিনাটি সূত্রভেদে আলাদা) হলো: standard তৈরির সময় অনেক ভাষায় isnan-এর মতো function ছিল না, আর x != x পরীক্ষাটি যেকোনো ভাষায় লেখা যেত, যা শুধু nan-এর জন্যই সত্যি। দাম হিসেবে আজ আমরা সেই ফাঁদ পাই যা অংশ ২-এ দেখেছেন: x == np.nan কখনো সত্যি নয়। ডেটাবেসের SQL-এ NULL-এর তুলনাও একই রকম “অজানা” দেয় (DBMS ও SQL কোর্স), যদিও নিয়মটি আলাদা দর্শন থেকে এসেছে।
মূল উপলব্ধি: nan-এর আচরণ বাগ নয়, ভেবেচিন্তে নেওয়া নকশা: অর্থহীন হিসাব থামায় না কিন্তু চিহ্ন রেখে যায়; তাই nan খোঁজার একমাত্র নির্ভরযোগ্য উপায় np.isnan.
প্র ০২NumPy কেন global np.random.seed ছেড়ে নিজস্ব Generator-এর দিকে গেল? এই বদলের দাম কী ছিল?
অনেক বছর NumPy-তে random সংখ্যার একমাত্র পথ ছিল RandomState, যার একটি লুকানো global কপি np.random.seed আর np.random.rand-এর মতো function-গুলো ব্যবহার করত। ভেতরের PRNG ছিল Mersenne Twister, যা ১৯৯৮ সালে প্রকাশিত হয়ে দ্রুত বিজ্ঞানের সফটওয়্যারে প্রচলিত হয়। তিনটি সমস্যা ধীরে ধীরে বড় হয়ে ওঠে। প্রথমত, global অবস্থা: একটি প্রোগ্রামের কোনো দূরের অংশ বা একটি library সংখ্যা নিলে আপনার ধারা সরে যেত, আর ফল কেন বদলাল তা বের করা কঠিন ছিল। দ্বিতীয়ত, সমান্তরাল কাজ: অনেক process-কে নিরাপদে আলাদা ধারা দেওয়ার পরিষ্কার উপায় ছিল না, তাই মানুষ seed + i-এর মতো ঝুঁকিপূর্ণ কৌশল নিত। তৃতীয়ত, NumPy কঠোর প্রতিশ্রুতি দিয়েছিল যে একই seed-এ ধারা কখনো বদলাবে না; ফলে ভেতরের কোনো অ্যালগরিদম উন্নত করাও অসম্ভব ছিল।
NumPy-র নকশা দলিল NEP 19 এই নীতি বদলের যুক্তি দেয়, আর ২০১৯ সালে NumPy 1.17-এ নতুন ব্যবস্থা আসে: BitGenerator (কাঁচা বিটের উৎস, যেমন PCG64, Philox, SFC64, MT19937) আর তার ওপরে Generator (বণ্টনের অ্যালগরিদম)। Generator-এর ধারা ভবিষ্যতে উন্নতির জন্য বদলানোর অনুমতি রাখা হয়, আর পুরোনো RandomState হুবহু স্থির রেখে দেওয়া হয়, যাতে পুরোনো গবেষণার কোড একই সংখ্যা দেয়।
দাম কী? দুই রকম API (অসংখ্য পুরোনো টিউটোরিয়াল আর AI সহকারী আজও পুরোনো পথটি লেখে), একই seed-এ দুই পথে সম্পূর্ণ আলাদা সংখ্যা, আর version লিখে রাখার দায় এখন ব্যবহারকারীর। বেশির ভাগ বিশেষজ্ঞ মনে করেন লাভটি দামের চেয়ে বড়, কারণ স্পষ্ট Generator object কোডকে পরীক্ষাযোগ্য করে।
মূল উপলব্ধি: লুকানো global অবস্থা ছোট স্ক্রিপ্টে সুবিধার, বড় প্রকল্পে বিপদের; random-কেও অন্য যেকোনো input-এর মতো স্পষ্ট argument হিসেবে পাঠানোই আধুনিক অভ্যাস।
প্র ০৩Monte Carlo পদ্ধতির জন্ম কীভাবে, আর ধীর 1/√n হারে ভুল কমা সত্ত্বেও কেন এটি আজও এত জনপ্রিয়?
Random নমুনা দিয়ে একটি সংখ্যা অনুমানের ধারণা পুরোনো। কিন্তু আধুনিক Monte Carlo পদ্ধতির জন্ম বলা হয় ১৯৪০-এর দশকের শেষে, যুক্তরাষ্ট্রের Los Alamos-এ। গণিতবিদ Stanisław Ulam-এর নিজের স্মৃতিচারণ অনুযায়ী, অসুস্থতার সময় solitaire তাস খেলতে খেলতে তিনি ভাবেন, জেতার সম্ভাবনা জটিল combinatorics দিয়ে বের করার চেয়ে অনেকবার খেলে গুনে দেখা সহজ। তিনি John von Neumann-এর সঙ্গে ধারণাটি নিয়ে কাজ করেন, নিউট্রনের চলাচলের মতো সমস্যায় প্রয়োগ হয়, আর প্রথম দিকের ইলেকট্রনিক কম্পিউটার ENIAC-এ এমন হিসাব চালানো হয়। “Monte Carlo” নামটি Nicholas Metropolis-এর দেওয়া বলে প্রচলিত, মোনাকোর বিখ্যাত জুয়ার আসরের নামে; ১৯৪৯ সালে Metropolis আর Ulam-এর একটি প্রবন্ধ এই নামেই প্রকাশিত হয়। খুঁটিনাটি নিয়ে বিভিন্ন বিবরণে ছোট পার্থক্য আছে।
1/√n হার সত্যিই ধীর: এক দশমিক ঘর বেশি নির্ভুলতা চাইলে ১০০ গুণ নমুনা। তবু জনপ্রিয়তার তিনটি কারণ। প্রথমত, হারটি সমস্যার মাত্রার ওপর নির্ভর করে না। গ্রিড ধরে হিসাবের পদ্ধতিতে প্রতিটি নতুন চলকের সঙ্গে কাজ গুণিতকে বাড়ে; ১০টি অনিশ্চিত অংশের ডেলিভারি মডেলে সব জোড়া গোনা অসম্ভব হতে পারে, কিন্তু Monte Carlo-র ভুল তখনো 1/√n. দ্বিতীয়ত, লেখা সহজ: মডেলটি একবার “চালাতে” পারলেই হয়, সূত্র লাগে না। তৃতীয়ত, ভুলের মাপ নিজেই হাতে আসে, যেমন অংশ ৮-এ দেখলেন।
মূল উপলব্ধি: Monte Carlo প্রতিটি নমুনায় ধীর, কিন্তু মাত্রা আর জটিলতায় প্রায় অপরাজেয়; আর তার ফল সবসময় একটি অনুমান, যার সঙ্গে ভুলের মাপ দেওয়া বাধ্যতামূলক।
প্র ০৪standard deviation-এ n দিয়ে ভাগ, নাকি n − 1 দিয়ে? সফটওয়্যারভেদে default আলাদা হওয়া কেন বাস্তব সমস্যা?
অংশ ৩-এ ঢাকার চার দিনের অর্ডারে দুটি উত্তর পেয়েছেন: ddof=0-এ 2.291, ddof=1-এ 2.646. দুটিই সঠিক, কিন্তু ভিন্ন প্রশ্নের। চারটি দিনই যদি আপনার পুরো আগ্রহের জগৎ হয় (এই চার দিনের ছড়ানো কত ছিল), তাহলে n দিয়ে ভাগ। আর যদি এই চার দিন বড় একটি জগতের নমুনা হয় (সাধারণভাবে দৈনিক অর্ডার কতটা ওঠানামা করে), তাহলে n − 1 দিয়ে ভাগ করলে variance-এর অনুমান গড়ে আসল মানের সমান হয়। এই সংশোধনের নাম Friedrich Bessel-এর নামে Bessel's correction. অন্তর্দৃষ্টি: গড়টি এই নমুনা থেকেই বের করা, আর নমুনার নিজের গড় সংখ্যাগুলোর যতটা কাছে, আসল (অজানা) গড় তার চেয়ে কাছে হতে পারে না; তাই n দিয়ে ভাগ করলে ছড়ানো গড়ে একটু কম আসে।
বাস্তব সমস্যাটি হলো default-এর অমিল। NumPy-র std default-এ n দিয়ে ভাগ করে, pandas-এর std n − 1 দিয়ে। একজন বিশ্লেষক NumPy-তে হিসাব করলেন, আরেকজন pandas-এ, রিপোর্টে দুই সংখ্যা এলো, আর কেউ বুঝল না কেন। ছোট n-এ পার্থক্য বড়: চারটি সংখ্যায় দুই উত্তরের অনুপাত √(4/3) ≈ 1.155, মানে প্রায় ১৫%; এক হাজার সংখ্যায় তা নগণ্য। scikit-learn-এর StandardScaler-ও তার docs অনুযায়ী n দিয়ে ভাগ করে। সমাধান সরল: সবসময় ddof স্পষ্ট লিখুন, আর রিপোর্টে বলুন কোনটি।
মূল উপলব্ধি: ddof একটি প্রশ্নের নির্বাচন (জগৎ, নাকি নমুনা থেকে অনুমান), গণিতের খুঁটিনাটি নয়; আর default-এর ওপর ভরসা না করে তা কোডে স্পষ্ট লেখাই নিরাপদ।
প্র ০৫একই seed দিলেই কি ফল “সত্য”? seed বেছে ফল সাজানো আর অল্প নমুনার অনুমান কীভাবে বিভ্রান্ত করে?
Seed একটি পরীক্ষাকে পুনরুৎপাদনযোগ্য করে, কিন্তু ফলটিকে নির্ভরযোগ্য করে না। অংশ ৮-এর শেষ লাইনে দেখেছেন, একই n = 100-এর simulation ২০০টি ভিন্ন ধারায় 0.03 থেকে 0.18 পর্যন্ত অনুমান দিয়েছে। কেউ যদি প্রতিবার আলাদা seed দিয়ে চালিয়ে শুধু 0.03-এর ফলটি রিপোর্ট করেন (“আমাদের দেরির হার মাত্র ৩%”), তাহলে কোডটি হুবহু পুনরুৎপাদনযোগ্য, অথচ দাবিটি বিভ্রান্তিকর। মেশিন লার্নিং গবেষণাতেও এই উদ্বেগ আছে: বেশ কয়েকটি প্রকাশিত পর্যালোচনা দেখিয়েছে, শুধু random seed বদলালেই একটি মডেলের মাপা দক্ষতা উল্লেখযোগ্যভাবে বদলাতে পারে, বিশেষ করে reinforcement learning আর ছোট ডেটার fine-tuning-এ। তাই একটি run-এর সংখ্যা দিয়ে “নতুন পদ্ধতি পুরোনোটির চেয়ে ভালো” দাবি দুর্বল।
এর সঙ্গে যুক্ত একটি বড় ধারণা p-hacking বা “forking paths”: অনেকগুলো বিকল্প (seed, ডেটার ভাগ, কোন outlier বাদ, কোন metric) চেষ্টা করে শুধু সবচেয়ে ভালো ফলটি দেখানো। প্রতিটি সিদ্ধান্ত আলাদাভাবে নিরীহ, কিন্তু একসঙ্গে মিথ্যা আবিষ্কারের সম্ভাবনা অনেক বাড়িয়ে দেয়; বিভিন্ন বিজ্ঞানের “replication crisis” এই সমস্যাকে বিস্তৃত আলোচনায় এনেছে।
সৎ অভ্যাসগুলো সরল। এক, কয়েকটি (যেমন ৫ বা ১০টি) ভিন্ন seed-এ চালিয়ে গড়ের সঙ্গে ছড়ানো (standard deviation বা বিস্তার) রিপোর্ট করুন। দুই, Monte Carlo-র ফলের সঙ্গে n আর standard error দিন। তিন, seed আর বিশ্লেষণের পরিকল্পনা আগে ঠিক করে লিখে রাখুন, ফল দেখে নয়। চার, যত বড় সিদ্ধান্ত (যেমন ক্রেতাকে ডেলিভারির প্রতিশ্রুতি), তত বেশি নমুনা আর তত সরল, যাচাইযোগ্য মডেল।
মূল উপলব্ধি: Seed ফল পুনরুৎপাদনের জন্য, ফল বাছাইয়ের জন্য নয়; random-নির্ভর যেকোনো ফলের সঙ্গে তার ছড়ানোও জানানো সততার ন্যূনতম শর্ত।
অনুশীলন
প্রথমটি হাতে হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লিখে simulation-এর।
-
হাতে হিসাব: দুটি জেলার তিন দিনের অর্ডার: সিলেট
[6, 9, 3], বরিশাল[4, 4, 7], মানে shape (2, 3)-এর টেবিলT. (ক)T.sum(axis=0)আরT.sum(axis=1), তাদের shape সহ। (খ) প্রতিটি জেলারmean, আরvarওstd,ddof=0আরddof=1দুভাবেই। (গ) একটি Monte Carlo অনুমানে আসল সম্ভাবনা প্রায় 0.11; standard error 0.001 বা তার কম চাইলে অন্তত কত trial লাগবে?(ক) axis=0 (কলাম ধরে): 6 + 4 = 10, 9 + 4 = 13, 3 + 7 = 10, ফল
[10 13 10], shape (3,). axis=1 (সারি ধরে): 6 + 9 + 3 = 18, 4 + 4 + 7 = 15, ফল[18 15], shape (2,).(খ) সিলেট: গড় 6; গড় থেকে দূরত্ব 0, 3, −3; বর্গের যোগ 18. var = 18 ÷ 3 = 6 (ddof=0) বা 18 ÷ 2 = 9 (ddof=1); std = √6 ≈ 2.449 বা √9 = 3. বরিশাল: গড় 5; দূরত্ব −1, −1, 2; বর্গের যোগ 6. var = 2 বা 3; std ≈ 1.414 বা ≈ 1.732.
(গ) √(p(1 − p)/n) ≤ 0.001 মানে n ≥ p(1 − p) ÷ 0.0012 = 0.11 × 0.89 ÷ 0.000001 = 97,900. মানে প্রায় এক লাখ trial.
লেখকের যন্ত্রে NumPy দিয়ে মেলানো:
[10 13 10] [18 15] [6. 5.] [2.449 1.414] [3. 1.732] [6. 2.] [9. 3.]. -
আউটপুট অনুমান করুন: চালানোর আগে প্রতিটি লাইনের আউটপুট লিখুন, তারপর যেকোনো ব্লকে বসিয়ে মিলিয়ে দেখুন।
import numpy as np a = np.array([[1, 2, 3], [4, 5, 6]]) print(a.sum(axis=0), a.sum(axis=1).shape, a.mean(axis=1, keepdims=True).shape) b = np.array([1.0, np.nan, 3.0]) print(b.sum(), np.nansum(b), np.nanmax(b), np.argmax(b)) print(np.nan in b, (b == b).tolist())[5 7 9] (2,) (2, 1) nan 4.0 3.0 1 False [True, False, True]প্রথম লাইন: কলাম ধরে 1 + 4, 2 + 5, 3 + 6; সারির যোগের shape (2,); keepdims দিলে (2, 1). দ্বিতীয় লাইন: সাধারণ
sum-এ nan ছোঁয়াচে;nansum= 1 + 3 = 4;nanmax= 3. কিন্তুargmaxnan-কে সবচেয়ে বড় ধরে তার index 1 দেয়, তাই nan-ওয়ালা ডেটায় “সেরা দিন” খুঁজতেnp.nanargmaxলাগে। তৃতীয় লাইন: array-এরinভেতরে==দিয়ে মেলায়, আর nan কিছুর সমান নয়, তাই nan থাকা সত্ত্বেওFalse.b == bশুধু nan-এর ঘরেFalse, যাx != xকৌশলের ভিত্তি (লেখকের যন্ত্রে মেলানো, NumPy 2.5.1). -
নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) বাজারঘরের সাপ্তাহিক রিপোর্টের জন্য নিচের কোড লিখেছেন।
salesহলো shape (7, 7)-এর টেবিল: সারি = সাতটি জেলা, কলাম = সাতটি দিন, কিছু ঘর nan. অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো সংস্করণ লিখুন।import warnings; warnings.filterwarnings("ignore") np.random.seed(1) def report(sales): sales[sales == np.nan] = 0 share = sales / sales.sum(axis=0) # প্রতিটি জেলার দিনভিত্তিক ভাগ noise = np.random.normal(0, 1, sales.shape) return share, sales.std()- সব সতর্কবার্তা বন্ধ: global
filterwarnings("ignore")শূন্য দিয়ে ভাগ আর nan-এর সব সংকেত লুকিয়ে দেয়। sales == np.nanকখনো সত্যি নয়: একটি ঘরও বদলায় না; লাগবেnp.isnan. আর nan-কে 0 বসানোই ঠিক কি না (রেকর্ড হয়নি, নাকি সত্যিই বিক্রি শূন্য), তা ঠিক করা হয়নি।- ভুল axis, আর error নেই: জেলার ভাগ চাইলে সারি ধরে যোগ (
axis=1, keepdims=True) লাগে। টেবিলটি বর্গাকার (7, 7), তাইaxis=0চুপচাপ broadcast হয়ে প্রতিটি কলামকে ভাগ করে; ফল ভুল, error নেই। - input বদলে ফেলে:
sales[...] = 0ডাকার জায়গার মূল array-কেই বদলায় (পর্ব ১৪-এর view-এর মতো ফাঁদ)। - Legacy global random আর অকারণ কাজ:
np.random.seedglobal অবস্থা বদলায়;noiseবানানো হয় কিন্তু ব্যবহার হয় না, তবু ধারা খেয়ে ফেলে। - অস্পষ্ট সারাংশ:
sales.std()সব ৪৯টি ঘরের একটি সংখ্যা, ddof অস্পষ্ট, আর nan থাকলে nan.
def report(sales: np.ndarray) -> tuple[np.ndarray, np.ndarray]: """সারি = জেলা, কলাম = দিন; nan মানে রেকর্ড হয়নি।""" tot = np.nansum(sales, axis=1, keepdims=True) # (7, 1) share = np.divide(sales, tot, out=np.full(sales.shape, np.nan), where=tot > 0) spread = np.nanstd(sales, axis=1, ddof=1) # প্রতি জেলায় assert share.shape == sales.shape return share, spreadএখন মূল array অপরিবর্তিত, ভাগ ঠিক দিকে, মোট শূন্য হলে ভাগ হয় না, nan থাকলে সেই ঘরের ভাগও nan থাকে (লুকায় না), আর random-এর দরকারই নেই। random লাগলে
rngargument হিসেবে নিন। - সব সতর্কবার্তা বন্ধ: global
-
কোড লিখে simulation: বাজারঘরের একটি পণ্যের দৈনিক চাহিদা মোটামুটি normal, গড় 40, standard deviation 6. এক সপ্তাহের (৭ দিন) জন্য গুদামে 300 পিস রাখা হয়।
default_rng(42)দিয়ে ১,০০,০০০টি সপ্তাহ simulate করে সপ্তাহের চাহিদা 300 ছাড়ানোর সম্ভাবনা অনুমান করুন, একটিaxisব্যবহার করে। তুলনার জন্য: স্বাধীন সাতটি normal-এর যোগ নিজেও normal, গড় 280, standard deviation 6√7 ≈ 15.87.import math import numpy as np rng = np.random.default_rng(42) week = rng.normal(40, 6, size=(100_000, 7)).sum(axis=1) # (100000,) p_hat = (week > 300).mean() exact = 0.5 * math.erfc((300 - 280) / (6 * math.sqrt(7)) / math.sqrt(2)) se = math.sqrt(exact * (1 - exact) / 100_000) print(week.shape, round(float(p_hat), 4), round(exact, 4), round(se, 4))লেখকের যন্ত্রে সত্যিকারের run (NumPy 2.5.1):
(100000,) 0.1033 0.1039 0.001. অনুমান 0.1033, সূত্রের মান 0.1039, পার্থক্য 0.0006, যা SE 0.001-এর ভেতরে।size=(100_000, 7)একটি টেবিল বানায় (সারি = সপ্তাহ, কলাম = দিন), আরsum(axis=1)প্রতিটি সপ্তাহের সাত দিন যোগ করে।erfcদিয়ে normal distribution-এর লেজের সম্ভাবনা বের হয়; তার তত্ত্ব পর্ব ৩৬-এ। সতর্কতা: আসল চাহিদা ঋণাত্মক হয় না আর পূর্ণসংখ্যা, আর ঈদের সপ্তাহে গড়ই বদলে যায়; মডেলটি যতটা সরল, উত্তরও ততটাই নির্ভরযোগ্য।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমদেরিতে ডেলিভারির সম্ভাবনা Monte Carlo দিয়ে অনুমানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🎯 প্রশ্ন আর মডেল ঠিক করা: কোন অংশ, কোন সম্ভাবনা, “দেরি” মানে > 3 দিন
- 🌱 seed দিয়ে rng = np.random.default_rng(seed) বানানো
- 🎲 ওজনসহ choice দিয়ে n-টি trial-এর গুদাম আর কুরিয়ারের দিন তোলা
- ➕ দিনগুলো যোগ করে (store + courier) > 3 দিয়ে True/False বানানো
- 📊 True-র গড় নিয়ে অনুমান আর √(p(1 − p)/n) দিয়ে ভুলের মাপ
- 📝 অনুমান, n, ভুলের মাপ, seed আর NumPy version একসঙ্গে রিপোর্ট করা
NumPy-তে orders-এর shape (3, 4) (৩টি জেলা × ৪টি দিন)। orders.sum(axis=0)-এর shape কী, আর এটি কী দেয়?
(3,), প্রতিটি জেলার মোট(4,), প্রতিটি দিনের মোট(1, 4), প্রতিটি দিনের মোট(), সব ঘরের মোট
NumPy-তে x = np.array([4.0, np.nan, 6.0]). একজন লিখলেন mask = (x == np.nan) আর x.mean(). কোনটি ঘটবে?
maskমাঝের ঘরে True, আরmean()5.0maskসব False, আরmean()5.0- mask সব False, আর
mean()nan mean()একটিValueErrorদেয়
NumPy-তে x = np.array([2, 4, 6]). x.var() আর x.var(ddof=1)-এর মান কত?
- ≈ 2.667 আর 4.0
- 4.0 আর ≈ 2.667
- ≈ 1.633 আর 2.0
- দুটিই 4.0
NumPy-তে rng = np.random.default_rng(0), ids = np.arange(5), তারপর ids = rng.shuffle(ids). এরপর ids-এ কী থাকে?
- এলোমেলো ক্রমের ০ থেকে ৪
- আগের মতোই
[0 1 2 3 4] - একটি
TypeError - None
একটি Monte Carlo অনুমানে ১০,০০০ trial-এ standard error প্রায় 0.004. ভুল অর্ধেক (প্রায় 0.002) করতে মোটামুটি কত trial লাগবে?
- ২০,০০০
- ৪০,০০০
- ১০,০০,০০০
- trial নয়, seed বদলাতে হবে
NumPy-তে একটি লাইনে শূন্য দিয়ে ভাগ ঘটতে পারে, আর তা জেনেবুঝেই। প্রোগ্রামের বাকি অংশের সতর্কবার্তা ঠিক রেখে শুধু এই লাইনের সতর্কবার্তা সামলানোর সবচেয়ে ভালো উপায় কোনটি?
- ফাইলের শুরুতে
warnings.filterwarnings("ignore") np.seterr(all="ignore")একবার ডাকাwith np.errstate(divide="ignore"):ব্লকে শুধু সেই লাইনটি- লাইনটিকে
try/except ZeroDivisionError-এ রাখা
NumPy-তে চারটি সমান্তরাল process-এর জন্য আলাদা random ধারা দরকার। কোনটি NumPy-র docs অনুযায়ী নিরাপদ পদ্ধতি?
np.random.default_rng(seed).spawn(4)- প্রতিটি process-এ
default_rng(seed + i) - প্রতিটি process-এ একই
default_rng(seed) - শুরুতে একবার
np.random.seed(seed)
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Ufunc প্রতিটি ঘরে একই কাজ করে, shape বদলায় না;
out=দিয়ে ফলের জায়গা,where=দিয়ে কোন ঘরে কাজ, আরwhere=দিলেout=-ও দিন। nan != nan: খুঁজুনnp.isnanদিয়ে, হিসাব করুনnanmean-পরিবার দিয়ে; সতর্কবার্তা লুকাবেন না,np.errstateদিয়ে নির্দিষ্ট জায়গায় সামলান; float তুলনায়np.isclose.axis=kমানে মাত্রা k ভেঙে যায়: (3, 4)-এ axis=0 → (4,), axis=1 → (3,); সারাংশ দিয়ে আবার মূল টেবিলে হিসাব করতেkeepdims=True, আর বর্গাকার টেবিলে বিশেষ সাবধান।- NumPy-র
std-এর defaultddof=0, pandas-এর1; outlier থাকলে গড়ের পাশে median দেখান। - Random মানে
rng = np.random.default_rng(seed), argument হিসেবে পাঠানো; আলাদা ধারায়spawn; Monte Carlo-র ভুল কমে 1/√n হারে, তাই ফলের সঙ্গে n আর standard error দিন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৭ · NumPy-তে linear algebra — vector, matrix, dot product আর normপরবর্তী পর্বMatrix, matmul, linear system সমাধান আর eigen/SVD-এর পরিচয় NumPy দিয়ে।
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ১১ · AI কোডে পরিচ্ছন্নতা — function, type hint, ছোট test আর seedঝালাইSeed-এর মৌলিক ধারণা আর তার সীমা।
- Python · সংখ্যা ও হিসাব — int, float-এর ফাঁদ আর DecimalঝালাইFloat-এর ভেতরের গঠন আর তুলনার নিয়ম।
- DSA · Randomized অ্যালগরিদম — Las Vegas আর Monte Carloপাশের ট্র্যাকRandom দিয়ে অ্যালগরিদম আর তার বিশ্লেষণ।
- Python · ডেটা সায়েন্সের প্রথম ধাপ — NumPy আর pandasপাশের ট্র্যাকPython কোর্সের দিক থেকে NumPy-র সংক্ষিপ্ত পরিচয়।