সমাধান — অধ্যায় ০.৬ · Python On-ramp for Scientific Computing¶
এই ফাইলে ০.৬ অধ্যায়ের সব অনুশীলনীর পূর্ণ সমাধান। প্রতিটি কোড runnable; নিচে প্রকৃত output দেওয়া।
Conceptual¶
সমাধান ১ — vectorization বনাম for-loop [easy]¶
মূল পার্থক্য: Python for-loop প্রতিটি উপাদান এক-এক করে interpreted কোডে প্রক্রিয়া করে; প্রতিটি iteration-এ interpreter type যাচাই, object তৈরি, reference গণনা ইত্যাদি overhead সামলায়। Vectorization-এ পুরো array-র উপর operation চালায় আগে থেকে compile করা C কোড, যেখানে এই per-element overhead নেই এবং উপাদানগুলো memory-তে পাশাপাশি থাকায় CPU দক্ষভাবে (cache-friendly, প্রায়ই SIMD-সহ) কাজ করে।
কেন দ্রুত (সংক্ষেপে): 1. interpreter loop-overhead বাদ যায়, 2. data সন্নিহিত (contiguous) memory-তে, 3. নিম্নস্তরে optimized C/Fortran routine চলে।
ফল: একই গণনা প্রায়ই ১০–১০০ গুণ দ্রুত (অধ্যায়ের benchmark-এ ~৩৪ গুণ)। তাই বড় data-তে loop যথাসম্ভব এড়িয়ে array operation ব্যবহার করা উচিত।
সমাধান ২ — seed না দিলে কী সমস্যা [medium]¶
seed না দিলে প্রতিবার কোড চালালে ভিন্ন এলোমেলো সংখ্যা আসে, ফলে:
- গবেষণা: আপনার রিপোর্ট করা ফলাফল (গড়, p-value, plot) অন্য কেউ — এমনকি আপনি নিজেও — হুবহু পুনরুৎপাদন করতে পারবেন না; বৈজ্ঞানিক যাচাই ভেঙে পড়ে।
- Debugging: কোনো বাগ যদি কেবল নির্দিষ্ট random মানে দেখা দেয়, seed ছাড়া তা প্রতিবার ফিরিয়ে আনা যায় না — সমস্যা খুঁজে বের করা কঠিন।
- তুলনা: দুটি method একই random data-তে তুলনা করতে হলে উভয়কে একই seed দিতে হবে; নাহলে পার্থক্য আসলটা method-জনিত নাকি randomness-জনিত তা বলা যায় না।
সমাধান (default_rng(0)): pseudo-random generator একটি নির্ধারক algorithm; নির্দিষ্ট seed (এখানে 0) মানে সবসময় একই শুরুর অবস্থা → একই সংখ্যা-ক্রম। ফলে ফলাফল পুনরুৎপাদনযোগ্য, যাচাইযোগ্য ও debuggable হয়।
Computational¶
সমাধান ৩ — z-score রূপান্তর [easy]¶
import numpy as np
x = np.array([10, 12, 14, 16, 18])
z = (x - x.mean()) / x.std() # vectorized: পুরো array-তে একসাথে
print("z =", z.round(4))
print("z.mean() =", round(float(z.mean()), 10))
print("z.std() =", round(float(z.std()), 6))
Output:
ব্যাখ্যা: \(\bar{x}=14\), \(\sigma\approx 2.83\)। প্রতিটি মান থেকে গড় বিয়োগ ও \(\sigma\) দিয়ে ভাগ — সবই broadcasting/vectorization-এ এক লাইনে। গঠনগতভাবে z-score-এর গড় সর্বদা \(0\) ও std সর্বদা \(1\) হয়, যা output নিশ্চিত করে।
সমাধান ৪ — column-centered matrix [medium]¶
import numpy as np
rng = np.random.default_rng(99) # reproducibility
M = rng.integers(1, 10, size=(4, 3))
print("M =\n", M)
print("column means =", M.mean(axis=0))
col_centered = M - M.mean(axis=0) # (4,3) - (3,) → broadcasting
print("new column means (≈0) =", col_centered.mean(axis=0).round(10))
Output:
M =
[[9 5 7]
[6 2 5]
[9 9 9]
[6 3 6]]
column means = [7.5 4.75 6.75]
new column means (≈0) = [0. 0. 0.]
ব্যাখ্যা: M.mean(axis=0)-এর shape (3,) (প্রতি কলামের গড়)। M - M.mean(axis=0)-এ broadcasting এই (3,) array-কে প্রতিটি সারির সাথে মিলিয়ে নেয়, ফলে প্রতিটি কলাম থেকে তার নিজস্ব গড় বিয়োগ হয়। তাই নতুন কলাম-গড় ঠিক \(0\)।
Debugging / coding¶
সমাধান ৫ — row-centering error ঠিক করা [medium]¶
সমস্যা: M-এর shape (4, 3), কিন্তু M.mean(axis=1)-এর shape (4,)। broadcasting ডান দিক থেকে shape মেলায়: (4, 3) বনাম (4,) → শেষ মাত্রায় 3 বনাম 4 — মেলে না, তাই ValueError। (লক্ষণীয়: axis=0-এর ক্ষেত্রে (3,) ঠিকই মিলত, কিন্তু axis=1-এ নয়।)
ঠিক করা (keepdims=True): গড় নেওয়ার সময় মাত্রা ধরে রাখলে shape হয় (4, 1), যা (4, 3)-এর সাথে broadcasting-এ মেলে।
import numpy as np
rng = np.random.default_rng(99)
M = rng.integers(1, 10, size=(4, 3))
row_centered = M - M.mean(axis=1, keepdims=True) # (4,3) - (4,1) ✓
print("row_centered =\n", row_centered)
print("new row means (≈0) =", row_centered.mean(axis=1).round(10))
Output:
row_centered =
[[ 2. -2. 0. ]
[ 1.66666667 -2.33333333 0.66666667]
[ 0. 0. 0. ]
[ 1. -2. 1. ]]
new row means (≈0) = [0. 0. 0. 0.]
বিকল্প fix: M.mean(axis=1).reshape(-1, 1) বা M.mean(axis=1)[:, None] — তিনটিই shape (4,1) দেয়।
Mini-project¶
সমাধান ৬ — কৃত্রিম exam dataset workflow [hard]¶
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
rng = np.random.default_rng(2025) # reproducibility
n = 150
# --- build ---
df = pd.DataFrame({
"section": rng.choice(["A", "B", "C"], size=n),
"hours": rng.uniform(1, 10, size=n).round(1),
})
section_bonus = df["section"].map({"A": 0, "B": 5, "C": -3})
df["score"] = (40 + 4 * df["hours"] + section_bonus
+ rng.normal(0, 6, n)).clip(0, 100).round(1)
print("shape:", df.shape)
print(df.head())
# --- (ক) per-section summary ---
print("\nper-section summary:")
print(df.groupby("section")["score"].agg(["mean", "std", "count"]).round(2))
# --- (খ) correlation ---
print("\ncorr(hours, score) =", round(df["hours"].corr(df["score"]), 3))
# --- (গ) scatter plot ---
fig, ax = plt.subplots(figsize=(6.4, 4.2))
ax.scatter(df["hours"], df["score"], s=20, alpha=0.6, color="#4C72B0")
ax.set_xlabel("Study hours"); ax.set_ylabel("Score")
ax.set_title("Score vs study hours (simulated)")
fig.tight_layout()
fig.savefig("0-6-solution-scatter.png", dpi=150) # local; assets নয়
Output:
shape: (150, 3)
section hours score
0 B 4.7 72.9
1 C 9.9 73.0
2 C 8.7 68.9
3 B 2.4 49.5
4 C 7.7 66.5
per-section summary:
mean std count
section
A 62.19 12.63 47
B 66.25 11.05 53
C 58.69 12.20 50
corr(hours, score) = 0.849
ব্যাখ্যা:
- build: score = 40 + 4·hours + (section bonus) + noise, তারপর .clip(0,100) দিয়ে বাস্তবসম্মত সীমায় রাখা। section B-তে +৫, C-তে −৩ — তাই গড়ে B > A > C।
- (ক) groupby summary section-ভিত্তিক গড় ঠিক সেই pattern দেখায় (B≈66, A≈62, C≈59)।
- (খ) correlation ≈ 0.85 — দৃঢ় ধনাত্মক, কারণ score সরাসরি hours-এর উপর নির্ভরশীল (এর সাথে noise ও section effect)।
- (গ) scatter-এ বিন্দুগুলো উপরের দিকে ঢালু মেঘের মতো — hours বাড়লে score বাড়ে।
নোট: এই workflow-ই (
build → summarize → visualize) Part I থেকে পরবর্তী প্রতিটি অধ্যায়ের code lab-এর মূল কাঠামো। seed নির্দিষ্ট থাকায় উপরের সংখ্যাগুলো হুবহু পুনরুৎপাদনযোগ্য।