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

Linear model গভীরে — basis function, interaction আর regularization path

Linear Models in Depth — Polynomial, Spline and RBF Bases, Interactions and Hierarchy, the Elastic-Net Path with the One-SE Rule, and Prediction Intervals

Taught in Bangla, this lesson shows why polynomial, spline and RBF models are still linear in parameters, picks basis size by cross-validation, and reads coefficients after transforms, with a real desktop pygam run. It covers interactions, hierarchy and centering with statsmodels, the one-standard-error rule on an ElasticNetCV path, and confidence versus prediction intervals checked on held-out data.

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

  • একটি ইনপুট থেকে polynomial, B-spline আর RBF কলাম বানানো, আর হাতে একটি spline সারি হিসাব করা
  • Cross-validation দিয়ে basis-এর সংখ্যা বাছাই, আর polynomial কেন প্রান্তে দোলে তা দেখা
  • Log, spline বা standardization-এর পরে coefficient সঠিকভাবে পড়া, আর GAM-এর ধারণা
  • Interaction-এর coefficient পড়া, hierarchy নিয়ম, আর centering-এর প্রভাব
  • ElasticNetCV-এর path থেকে one-standard-error নিয়মে λ বাছাই, আর confidence বনাম prediction interval হিসাব ও যাচাই

AI-এর ভিত্তি কোর্সের পর্ব ৫৮-এ normal equation, residual plot আর statsmodels-এর OLS দেখেছেন; পর্ব ৬১-এ PolynomialFeatures-এর ডিগ্রি বাড়িয়ে overfitting দেখেছেন; আর পর্ব ৬৩-এ log, ratio আর interaction feature বানিয়েছেন। এই কোর্সের পর্ব ১৭-এ ridge আর lasso-র তত্ত্ব আর path-এর প্রথম ছবি এসেছে। এই পর্ব সেগুলো জুড়ে একটি পূর্ণ model বানানোর পদ্ধতি দেখায়: বাঁকা সম্পর্ক কোন basis দিয়ে ধরবেন, coefficient কীভাবে পড়বেন, অনেক candidate থেকে কীভাবে বাছবেন, আর অনুমানের পাশে কোন পরিসর বলবেন। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python); প্রথমবার scikit-learn আর statsmodels নামতে দশ সেকেন্ডের মতো লাগতে পারে।

১ · “Linear” মানে parameter-এ linear — basis expansion-এর মূল কথা

রিকশাভাড়ার কথা ভাবুন। পাড়ার ভেতরে প্রতি কিলোমিটারে ভাড়া দ্রুত বাড়ে, বড় রাস্তায় উঠলে বাড়ে ধীরে, আর ব্রিজ পার হতে হলে হঠাৎ একটা থোক টাকা যোগ হয়। একটি সরলরেখা দিয়ে এই ভাড়া বোঝানো যায় না। কিন্তু একজন অভিজ্ঞ রিকশাচালক মাথায় কয়েকটি আলাদা হিসাব রাখেন: “পাড়ার অংশ কতটা”, “বড় রাস্তার অংশ কতটা”, “ব্রিজ আছে কি না”। তারপর প্রতিটির একটা দর দিয়ে যোগ করেন। হিসাবগুলো বাঁকা হতে পারে, কিন্তু শেষ যোগফলটি সরল ওজনের যোগ। Basis expansion ঠিক এই কাজ।

একটি ইনপুট x (ধরুন ডেলিভারির দূরত্ব) থেকে K-টি নতুন কলাম বানানো যাক: φ1(x), φ2(x), …, φK(x). Model:

ŷ = β₀ + β₁φ₁(x) + β₂φ₂(x) + … + βKφK(x)

এখানে φj হলো আগে থেকে ঠিক করা নিয়ম (যেমন x², বা একটি ঢিবি), আর βj হলো শেখার মতো সংখ্যা। ŷ x-এর বাঁকা function হতে পারে, কিন্তু β-র দিক থেকে এটি একটি রৈখিক যোগফল। এর ফল অনেক বড়: design matrix Φ (n সারি, K কলাম) বানানোর পর বাকি সব পুরোনো হাতিয়ার হুবহু কাজ করে। ŵ = (ΦᵀΦ)−1Φᵀy, পর্ব ০৭-এর projection, পর্ব ১৭-এর ridge আর lasso, আর statsmodels-এর standard error, সবই। এই কৌশলের নাম basis expansion. পর্ব ০৬-এর ভাষায়, φ-গুলো একটি function space-এর basis, আর model সেই space-এর মধ্যে সবচেয়ে কাছের function খোঁজে।

কোনটি linear model নয়? যেখানে parameter নিজেই বাঁকা জায়গায় বসে, যেমন ŷ = β₀·eβ₁x, বা knot-এর অবস্থানও শিখতে হয়। তখন loss আর quadratic থাকে না, আর closed form হারায়। Neural network-কে এভাবে দেখা যায়: শেষ স্তরটি basis-এর ওপর linear model, কিন্তু basis-ও শেখা হয়। এখানে basis আগে থেকে ঠিক, তাই সমস্যাটি convex থাকে (পর্ব ১২)।

একটি দূরত্ব থেকে চারটি কলাম, তারপর ওজনের যোগফল

Degree-1 B-spline (hat functions), knots at 0, 4, 8, 12 km; x = 5 km

Basis expansion: a distance becomes four hat-function columns, then a weighted sum 04812 km x = 5 φ₂ = 0.75 φ₃ = 0.25 1 φ₁ = 0 φ₂ = 0.75 φ₃ = 0.25 φ₄ = 0 × β₁ = 10 × β₂ = 34 × β₃ = 46 × β₄ = 52 ∑ ŷ = 0.75×34 + 0.25×46 = 37 মিনিট
ছবি ১.১ — উপরে চারটি basis function, প্রতিটি নিজের knot-এ ১ আর পাশের knot-এ ০. ৫ km-এ কেবল সবুজ (০.৭৫) আর কমলা (০.২৫) অশূন্য। নিচে সেই চারটি সংখ্যাই design matrix-এর একটি সারি; model শুধু ওজনগুলো (β) শেখে। x বাঁকা পথে ঢোকে, β সরল পথে।

২ · তিন রকম basis — polynomial, spline আর radial basis function

Polynomial. φj(x) = xj: সবচেয়ে পরিচিত, polynomial features. দুটি সমস্যা। প্রথমত প্রতিটি কলাম পুরো x-রেখায় ছড়ানো, তাই ১০ km-এর একটি ডেটা বিন্দু ১ km-এর অনুমানও নাড়ায়। দ্বিতীয়ত উঁচু ডিগ্রিতে প্রান্তে বড় দোলা আসে। গণিতবিদ Carl Runge ১৯০১ সালে সমান দূরত্বের বিন্দুতে polynomial interpolation-এ এটি দেখান, তাই একে Runge-এর ঘটনা বলে। x, x², x³ … কলামগুলো পরস্পর খুব সম্পর্কিত, তাই ΦᵀΦ-এর condition number বিশাল; standardize না করলে x10-এর মান ১৫১০ ছাড়ায়।

Spline. x-রেখাকে কয়েকটি knot-এ ভাগ করে প্রতিটি টুকরোয় ছোট ডিগ্রির polynomial বসানো, আর জোড়াগুলো মসৃণ রাখা। নামটি এসেছে নকশাকারদের নমনীয় কাঠের পাত থেকে, যা পেরেকের সঙ্গে বেঁকে মসৃণ বক্ররেখা আঁকত; Isaac Schoenberg ১৯৪৬ সালে গণিতে শব্দটি চালু করেন। একই spline অনেক basis-এ লেখা যায়। সহজটি truncated power basis: 1, x, আর প্রতিটি knot k-এর জন্য (x − k)₊, যা x < k হলে শূন্য। এর coefficient সরাসরি পড়া যায় (“৪ km-এর পর ঢাল কত বদলায়”), কিন্তু কলামগুলো খুব সম্পর্কিত। কম্পিউটারে পছন্দের রূপ B-spline: প্রতিটি কলাম মাত্র degree + 1টি টুকরোয় অশূন্য, আর প্রতিটি x-এ সবগুলোর যোগফল ১. এর হিসাবের স্থিতিশীল recursion Cox আর de Boor ১৯৭২ সালের কাছাকাছি সময়ে আলাদাভাবে প্রকাশ করেন। scikit-learn ১.০ থেকে SplineTransformer এই basis বানায়; default-এ ৫টি knot আর degree ৩, তাই প্রতি feature-এ ৫ + ৩ − ১ = ৭টি কলাম।

Radial basis function. φj(x) = exp(−((x − cj)/w)²): cj কেন্দ্রে একটি ঢিবি, w তার চওড়া। স্থানীয়, মসৃণ, আর অনেক মাত্রায় সহজে যায় (দূরত্ব ‖x − c‖ যেকোনো মাত্রায় মাপা যায়)। Broomhead আর Lowe ১৯৮৮ সালে RBF network নামে এটি ML-এ জনপ্রিয় করেন। প্রতিটি ডেটা বিন্দুকেই কেন্দ্র বানালে আর ridge যোগ করলে আসে kernel ridge regression; সেটি পর্ব ২৯ (Kernel trick — RBF, polynomial আর Mercer-এর অন্তর্দৃষ্টি) আর পর্ব ৩০ (SVR আর kernel ridge regression)-এর বিষয়।

নিচের stepper degree-১ B-spline দিয়ে হাতে সারি বানায়, তারপর একই function truncated power basis-এ লেখে, শেষে scikit-learn-এর default cubic basis-এর একটি সারি দেখায়।

Degree 1 (hat) basis, ৪টি কলাম φ₁…φ₄; প্রতিটি নিজের knot-এ ১, পাশের knot-এ ০
ধাপ
সারি
হিসাব
মন্তব্য
  1. Knot বসানো। ০, ৪, ৮ আর ১২ km-এ চারটি knot. প্রতিটি knot-এর একটি hat function: নিজের knot-এ উচ্চতা ১, পাশের knot-এ ০, মাঝে সরলরেখা।
  2. x = ৫ কোথায়। ৫ পড়েছে ৪ আর ৮-এর মাঝে, ৪ থেকে ১ km, ৮ থেকে ৩ km দূরে। তাই কেবল φ₂ আর φ₃ অশূন্য; φ₁ আর φ₄ জানেই না x = ৫ কী।
  3. সারিটি। φ₂ = (৮ − ৫)/৪ = ০.৭৫, φ₃ = (৫ − ৪)/৪ = ০.২৫. যে knot কাছে, তার ওজন বেশি। যোগফল ১.
  4. আরও তিনটি সারি। x = ২ ঠিক ০ আর ৪-এর মাঝখানে: [০.৫, ০.৫, ০, ০]. x = ১০: [০, ০, ০.৫, ০.৫]. x = ৮ একটি knot-এর ওপর: [০, ০, ১, ০]. প্রতিটি সারিতে বড়জোর দুটি অশূন্য, তাই design matrix প্রায় খালি (sparse).
  5. অনুমান। ধরুন fit থেকে β = (১০, ৩৪, ৪৬, ৫২). ŷ(৫) = ০.৭৫×৩৪ + ০.২৫×৪৬ = ২৫.৫ + ১১.৫ = ৩৭ মিনিট। Degree ১-এ βj-এর সহজ অর্থ আছে: knot j-তে অনুমান। মাঝখানে অনুমান দুই পাশের β-র সরলরেখা।
  6. অন্য basis, একই function. Truncated power basis [1, x, (x − ৪)₊, (x − ৮)₊]-এ coefficient (১০, ৬, −৩, −১.৫): ঢাল প্রথমে (৩৪ − ১০)/৪ = ৬, ৪ km-এর পর ৩ কমে ৩, ৮ km-এর পর আরও ১.৫ কমে ১.৫. x = ৫-এ ১০ + ৩০ − ৩ = ৩৭, আগের মতোই। Basis বদলালে coefficient বদলায়, অনুমান নয়।
  7. Cubic. SplineTransformer(n_knots=5, degree=3) ০ থেকে ১২-এর ডেটায় knot ০, ৩, ৬, ৯, ১২-এ বসায় আর ৭টি কলাম বানায়। x = ৫-এর সারিতে চারটি অশূন্য (০.০০৬২, ০.৩৭০৪, ০.৫৭৪১, ০.০৪৯৪), যোগফল ১. Polynomial-এ সারি হতো [৫, ২৫, ১২৫, …], সবই অশূন্য আর দ্রুত বড়।
Basisস্থানীয়?নমনীয়তার চাবিভালো দিকসাবধান
Polynomialনাডিগ্রিকম কলামে মসৃণ বাঁক; সহজ সূত্রপ্রান্তে দোলা, collinear কলাম, extrapolation-এ বিস্ফোরণ
Spline (B-spline)হ্যাঁknot-এর সংখ্যা ও স্থান, বা শাস্তি λস্থিতিশীল, ভাঁজ (kink) ধরে, sparseknot-এর বাইরে আচরণ নিজে ঠিক করতে হয় (extrapolation)
RBFহ্যাঁকেন্দ্রের সংখ্যা ও চওড়া wঅনেক মাত্রায় চলে, kernel-এর সঙ্গে যোগw ভুল হলে ফাঁক বা অতিরিক্ত মসৃণ; কেন্দ্রের বাইরে শূন্যে নামে

৩ · কতটা নমনীয়? Cross-validation দিয়ে basis বাছাই

কলাম বাড়ালে training ভুল কমতেই থাকে, কিন্তু নতুন ডেটার ভুল একসময় বাড়ে; এটিই bias–variance trade-off, যা AI-এর ভিত্তি কোর্সের পর্ব ৬১-এ U-আকৃতির curve হিসেবে দেখেছেন। এখানে প্রশ্নটি একটু বড়: একই সংখ্যক কলামে কোন basis ভালো? নিচের ব্লক বাজারঘরের ৮০টি কাল্পনিক ডেলিভারি বানায়। শহরের ভেতরে (৫ km পর্যন্ত) প্রতি km-এ ৬ মিনিট, তারপর ১.৫ মিনিট, আর ১০ km-এর কাছে একটি ব্রিজের জট (প্রায় ১২ মিনিটের ঢিবি)। তিন রকম basis-এ ৪ থেকে ৩০টি কলাম, প্রতিবার একই pipeline-এ খুব ছোট ridge (α = 10−6, শুধু সংখ্যার স্থিতির জন্য), আর ৫-fold CV-র RMSE. RBF-এর জন্য scikit-learn-এ সরাসরি transformer নেই (RBFSampler অন্য জিনিস, random Fourier feature), তাই FunctionTransformer দিয়ে নিজেই বানানো হয়েছে।

import numpy as np, pandas as pd
import matplotlib.pyplot as plt
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import (PolynomialFeatures, SplineTransformer,
                                   StandardScaler, FunctionTransformer)
from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold, cross_val_score

rng = np.random.default_rng(23)              # কাল্পনিক বাজারঘর: ৮০টি ডেলিভারি
d = rng.uniform(0.5, 15, 80)                 # দূরত্ব (km)
truth = lambda x: np.where(x < 5, 10 + 6 * x, 40 + 1.5 * (x - 5)) + 12 * np.exp(-((x - 10) / 0.8) ** 2)
y = truth(d) + rng.normal(0, 3, 80)          # ডেলিভারির মিনিট
X = d.reshape(-1, 1)
def rbf(k):                                  # k-টি Gaussian ঢিবি, সমান দূরে কেন্দ্র
    c = np.linspace(0.5, 15, k); w = c[1] - c[0]
    return FunctionTransformer(lambda Z: np.exp(-((Z - c) / w) ** 2))
make = {"poly":   lambda k: make_pipeline(StandardScaler(), PolynomialFeatures(k, include_bias=False), StandardScaler(), Ridge(1e-6)),
        "spline": lambda k: make_pipeline(SplineTransformer(n_knots=k - 2, degree=3), Ridge(1e-6)),
        "rbf":    lambda k: make_pipeline(rbf(k), Ridge(1e-6))}
cv = KFold(5, shuffle=True, random_state=42)
ks = list(range(4, 31, 2))                   # কলামের সংখ্যা: spline-এ n_knots + 2
res = pd.DataFrame({name: [-cross_val_score(m(k), X, y, cv=cv, scoring="neg_root_mean_squared_error").mean()
                           for k in ks] for name, m in make.items()}, index=ks)
res.index.name = "columns"
print(res.loc[[4, 8, 12, 16, 20, 24, 30]].round(2))
print("best k:", res.idxmin().to_dict(), " best RMSE:", res.min().round(2).to_dict())
fig, ax = plt.subplots(1, 2, figsize=(9, 3.6), layout="constrained")
res.plot(ax=ax[0], marker="o"); ax[0].set(ylim=(3, 12), xlabel="number of basis columns", ylabel="5-fold CV RMSE (min)")
g = np.linspace(0.5, 15, 300).reshape(-1, 1)
ax[1].scatter(d, y, s=8, c="0.6")
for name, k in [("poly", 16), ("spline", res["spline"].idxmin()), ("rbf", res["rbf"].idxmin())]:
    ax[1].plot(g, make[name](k).fit(X, y).predict(g), lw=2, label=f"{name}, k={k}")
ax[1].plot(g, truth(g), "k--", lw=1, label="truth"); ax[1].legend(fontsize=8)
ax[1].set(xlabel="distance (km)", ylabel="delivery minutes"); plt.show()

আসল আউটপুট (ব্রাউজারের Python):

         poly  spline    rbf
columns
4        3.92    3.92   4.16
8        3.57    3.81   3.65
12       3.64    3.42   3.33
16       7.26    3.47   3.46
20       9.87    3.59   3.54
24      11.28    5.12   4.09
30      15.64    4.72  33.42
best k: {'poly': 8, 'spline': 14, 'rbf': 12}  best RMSE: {'poly': 3.57, 'spline': 3.35, 'rbf': 3.33}

Noise-এর standard deviation ৩, তাই কোনো model গড়ে ৩-এর চেয়ে খুব কম RMSE পাওয়ার কথা নয়। চারটি কলামে তিনটিই প্রায় ৩.৯ থেকে ৪.২: ভাঁজ আর ঢিবি কোনোটিই ধরা পড়েনি (underfit). Spline ১৪টি কলামে ৩.৩৫ আর RBF ১২টিতে ৩.৩৩-এ নেমেছে, প্রায় noise-এর মেঝেতে। Polynomial-এর সেরা ৮ ডিগ্রিতে ৩.৫৭; তারপর ১৬ ডিগ্রিতে ৭.২৬ আর ৩০-এ ১৫.৬৪, কারণ প্রতিটি CV fold-এ বাদ পড়া প্রান্তের বিন্দুতে polynomial বিশাল দোলে। স্থানীয় basis অনেক বেশি সহনশীল: spline ২০টি কলামেও ৩.৫৯. তবে তারও সীমা আছে; ২৪টির পর spline ৫-এর ওপরে, আর ৩০টি RBF-এ ৩৩.৪২, কারণ ৮০টি বিন্দুর ৬৪টি দিয়ে fit করার সময় কিছু সরু ঢিবির নিচে প্রায় কোনো ডেটা থাকে না, তাদের coefficient লাগামছাড়া হয়। ডান ছবিতে ১৬ ডিগ্রির polynomial (নীল) দুই প্রান্তে বাঁকছে, যেখানে spline আর RBF ব্রিজের ঢিবি ধরে প্রান্তে শান্ত। (এই সংখ্যা একটি seed-এর কাল্পনিক ডেটার; অন্য seed-এ সেরা k একটু বদলাবে।)

ব্যবহারিক দুটি পরামর্শ। প্রথম, knot গুনে বাছাই করার চেয়ে প্রায়ই ভালো হলো অনেক knot রেখে ridge-ধাঁচের শাস্তি দেওয়া, আর শাস্তির মাপ λ CV-তে বাছা। Paul Eilers আর Brian Marx ১৯৯৬ সালে এই P-spline প্রস্তাব করেন: পাশাপাশি B-spline coefficient-এর পার্থক্যের ওপর শাস্তি, যাতে বক্ররেখা মসৃণ থাকে। তখন নমনীয়তার মাপ হয় effective degrees of freedom, ঠিক পর্ব ১৭-এর মতো। দ্বিতীয়, knot-এর বাইরে কী হবে আগে ভাবুন। SplineTransformer-এর extrapolation default-এ "constant", তাই ১৫ km-এর বেশি দূরত্বে অনুমান প্রান্তের মানে আটকে যায়; "linear" দিলে শেষ ঢাল ধরে চলে। ঘণ্টার মতো চক্রাকার feature-এ extrapolation="periodic" ২৩টা আর ০টাকে পাশাপাশি রাখে।

৪ · Transform-এর পরে coefficient পড়া, আর GAM-এর ধারণা

Basis বদলালে coefficient বদলায় কিন্তু অনুমান বদলায় না, এটি stepper-এ দেখেছেন। তাই রূপান্তরের পরে “এই coefficient মানে কী” প্রশ্নের উত্তর সবসময় রূপান্তর জেনে দিতে হয়। পাঁচটি সাধারণ ক্ষেত্র:

  • Spline বা RBF: একক coefficient প্রায় অর্থহীন; cubic B-spline-এর βj কোনো সহজ ঢাল বা মান নয়। পড়ুন বক্ররেখা: একটি grid-এ অনুমান আঁকুন, বা দুটি বিন্দুর পার্থক্য বলুন (“৩ থেকে ৪ km-এ কত মিনিট বাড়ে”)। বিশ্বাসযোগ্যতার জন্য বক্ররেখার পাশে CI বা bootstrap ব্যান্ড দিন (পর্ব ২১)।
  • Polynomial: ŷ = β₀ + β₁x + β₂x² হলে x-এর প্রভাব β₁ + 2β₂x, অর্থাৎ x-এর মানের ওপর নির্ভর করে। β₁ একা মানে “x = ০-তে ঢাল”, যা হয়তো ডেটার বাইরে।
  • Log target: log(অর্ডারের মূল্য)-এ promo-র β = ০.১২ মানে মূল্য গুণ হয় e০.১২ = ১.১২৭৫ দিয়ে, অর্থাৎ প্রায় ১২.৭৫% বেশি, টাকার কোনো স্থির অঙ্ক নয়। সাবধান: exp(log-এর অনুমান) দেয় median-ধাঁচের মান, গড় নয়; গড় চাইলে Duan (১৯৮৩)-এর “smearing” সংশোধন লাগে।
  • Log feature: y-তে log x-এর coefficient β মানে x ১% বাড়লে y প্রায় β/১০০ একক বাড়ে।
  • Standardized feature: standardization-এর পরে β মানে “এক standard deviation বাড়লে”; মূল এককে ফিরতে β-কে ওই কলামের sd দিয়ে ভাগ করুন। Pipeline-এর শেষ ধাপের coef_ সরাসরি km বা টাকায় নয়।

অনেক feature-এ বাঁকা সম্পর্ক: generalized additive model

দূরত্ব ছাড়াও ডেলিভারি সময় দিনের ঘণ্টা (সন্ধ্যার ভিড়) আর বৃষ্টির ওপর নির্ভর করে। প্রতিটির জন্য আলাদা একটি মসৃণ function শিখে যোগ করলে পাওয়া যায় generalized additive model (GAM): ŷ = β₀ + f₁(দূরত্ব) + f₂(ঘণ্টা) + β·বৃষ্টি. প্রতিটি fj একটি penalized spline, তাই পুরো model এখনো parameter-এ linear, শুধু শাস্তিসহ। Trevor Hastie আর Robert Tibshirani ১৯৮৬ সালে ধারণাটি প্রকাশ করেন, ১৯৯০ সালে বই; R-এর mgcv package (Simon Wood) আজ এর সবচেয়ে প্রচলিত বাস্তবায়ন। GAM-এর বড় সুবিধা পড়তে পারা: প্রতিটি fj আলাদা করে আঁকা যায়। দাম হলো, feature-দের মধ্যে interaction নিজে থেকে আসে না; চাইলে আলাদা করে যোগ করতে হয় (যেমন দূরত্ব × বৃষ্টির tensor spline).

Python-এ pygam package এটি দেয়, কিন্তু ব্রাউজারের Python-এ নেই। তাই ডেস্কটপে সত্যিকারের run: লেখার সময়ে Python 3.14.4, pygam 0.12.0, NumPy 2.5.4 আর SciPy 1.16.3, একটি venv-এ। ডেটা উপরের মতোই, শুধু ৬০০টি অর্ডার আর দুটি নতুন feature:

import numpy as np
from pygam import LinearGAM, s, f

rng = np.random.default_rng(23)                 # কাল্পনিক বাজারঘর: ৬০০টি ডেলিভারি
n = 600
km = rng.uniform(0.5, 15, n)
hour = rng.uniform(8, 23, n)
rain = rng.integers(0, 2, n)
f_km = np.where(km < 5, 10 + 6 * km, 40 + 1.5 * (km - 5)) + 12 * np.exp(-((km - 10) / 0.8) ** 2)
f_hour = 8 * np.exp(-((hour - 19) / 1.5) ** 2)  # সন্ধ্যার ভিড়
y = f_km + f_hour + 6 * rain + rng.normal(0, 3, n)
X = np.column_stack([km, hour, rain])

gam = LinearGAM(s(0, n_splines=20) + s(1, n_splines=20) + f(2)).gridsearch(X, y, progress=False)
gam.summary()
for x in (3.0, 9.0, 10.0, 13.0):                 # দূরত্বের partial effect: ১ km বাড়লে কত মিনিট?
    a, b = gam.partial_dependence(term=0, X=np.array([[x, 0, 0], [x + 1, 0, 0]]))
    print(f"km {x:>4} -> {x + 1:>4}: {b - a:+.1f} min")
~/bazarghar-ml$ python gam_delivery.py
…/gam_delivery.py:15: UserWarning: KNOWN BUG: p-values computed in this summary are likely much smaller than they should be.
…
LinearGAM
=============================================== ==========================================================
Distribution:                        NormalDist Effective DoF:                                     29.8614
Link Function:                     IdentityLink Log Likelihood:                                 -1484.4614
Number of Samples:                          600 AIC:                                             3030.6455
                                                AICc:                                             3034.107
                                                GCV:                                                9.5209
                                                Scale:                                              2.9448
                                                Pseudo R-Squared:                                   0.9518
==========================================================================================================
Feature Function                  Lambda               Rank         EDoF         P > x        Sig. Code
================================= ==================== ============ ============ ============ ============
s(0)                              [0.2512]             20           15.1         1.11e-16     ***
s(1)                              [0.2512]             20           13.8         1.11e-16     ***
f(2)                              [0.2512]             2            1.0          1.11e-16     ***
intercept                                              1            0.0          1.11e-16     ***
==========================================================================================================
… (Significance codes-এর লাইন আর p-value নিয়ে দুটি WARNING অনুচ্ছেদ)
km  3.0 ->  4.0: +6.9 min
km  9.0 -> 10.0: +8.8 min
km 10.0 -> 11.0: -5.2 min
km 13.0 -> 14.0: +1.6 min

প্রতিটি অংশের rank (কলাম) ২০, কিন্তু শাস্তির পরে effective df দূরত্বে ১৫.১ আর ঘণ্টায় ১৩.৮; পুরো model প্রায় ৩০টি parameter-এর সমান নমনীয়। gridsearch GCV (generalized cross-validation) কমিয়ে সব অংশে একই λ = ০.২৫১২ বেছেছে। শেষ চার লাইন coefficient না পড়ে বক্ররেখা পড়ার উদাহরণ: ৩ থেকে ৪ km-এ +৬.৯ মিনিট (আসল ৬), ১৩ থেকে ১৪-এ +১.৬ (আসল ১.৫), আর ব্রিজের দুই পাশে +৮.৮ আর −৫.২ (আসল প্রায় +১১ আর −৮): শাস্তি ঢিবিটিকে একটু চ্যাপ্টা করেছে, যা শাস্তির স্বাভাবিক bias. pygam নিজেই সতর্ক করছে যে এই summary-র p-value (সব ১.১১e-16) বিশ্বাসযোগ্য নয়, কারণ λ ডেটা থেকে বাছা হয়েছে; এই ধরনের সতর্কবার্তা চাপা না দিয়ে পড়ুন। scikit-learn-এ কাছাকাছি রূপ: ColumnTransformer-এ প্রতিটি কলামে SplineTransformer, তারপর RidgeCV.

৫ · Interaction আর hierarchy — একটি coefficient-এর মানে কীভাবে বদলায়

Basis expansion একটি feature-কে বাঁকায়; interaction দুটি feature-কে জোড়ে। বাজারঘরের প্রশ্ন: promo code কি প্রথমবারের ক্রেতার অর্ডার বেশি বাড়ায়? Model ŷ = b₀ + b₁·promo + b₂·first + b₃·(promo × first), যেখানে দুটিই ০/১. promo × first কলামটি কেবল “প্রথমবারের ক্রেতা আর promo পেয়েছেন” ঘরে ১. ফলে b₁-এর মানে আর “promo-র প্রভাব” নয়, হয়ে যায় “first = ০ হলে promo-র প্রভাব”. নিচের stepper চারটি ঘর একে একে ভরে।

promo, first ∈ {0, 1}; প্রথমবারের ক্রেতা মোটের ৩০%
ধাপ
(promo, first)
হিসাব
promo-র প্রভাব
  1. Model. চারটি coefficient, চারটি ঘর (promo আছে/নেই × প্রথমবার/ফেরত আসা)। Dummy কোডিংয়ে চারটি সংখ্যা চারটি ঘরের গড় হুবহু ধরে।
  2. (০, ০). সব কলাম শূন্য, তাই ŷ = b₀ = ৮৫০: ফেরত আসা ক্রেতা, promo ছাড়া। এটিই তুলনার ভিত্তি।
  3. (১, ০). ৮৫০ + ১২০ = ৯৭০. তাই b₁ = ১২০ মানে কেবল ফেরত আসা ক্রেতার জন্য promo-র প্রভাব, সবার গড় নয়।
  4. (০, ১). ৮৫০ − ২০০ = ৬৫০. b₂ = −২০০ মানে promo ছাড়া প্রথমবারের ক্রেতা ফেরত আসাদের চেয়ে ২০০ টাকা কম কেনেন।
  5. (১, ১). চারটি পদই যোগ হয়: ৯৫০. প্রথমবারের ক্রেতার জন্য promo-র প্রভাব ৯৫০ − ৬৫০ = ৩০০ = b₁ + b₃.
  6. b₃. দুই দলের promo-প্রভাবের পার্থক্য: ৩০০ − ১২০ = ১৮০, “পার্থক্যের পার্থক্য”. b₃ শূন্য হলে দুই দলে promo-র প্রভাব সমান।
  7. Center করলে। first-এর বদলে first − ০.৩ বসালে promo-র coefficient হয় ১২০ + ১৮০ × ০.৩ = ১৭৪, যা ৭০% ফেরত আসা আর ৩০% প্রথমবারের ক্রেতার ওজনে গড় প্রভাব। b₃ বদলায় না; অনুমানও বদলায় না।

Hierarchy নিয়ম: model-এ promo × first রাখলে promo আর first-এর main effect-ও রাখুন। John Nelder ১৯৭০-এর দশকে একে “marginality” নামে জোর দিয়ে বলেছেন। কারণ: main effect বাদ দিলে আপনি গোপনে একটি শর্ত চাপান (যেমন “promo ছাড়া প্রথমবার আর ফেরত আসা ক্রেতার গড় সমান”), আর interaction-এর coefficient সেই ভুল শর্তের ক্ষতিপূরণ করতে গিয়ে অন্য মানে নেয়। তা ছাড়া কোডিং বদলালে (০/১-এর বদলে ১/০, বা center করলে) hierarchy-ছাড়া model-এর অনুমানই বদলে যায়, যা hierarchy-সহ model-এ হয় না। নিচের ব্লক statsmodels-এর OLS দিয়ে চারটি model তুলনা করে।

import numpy as np
import pandas as pd
import statsmodels.api as sm

rng = np.random.default_rng(7)               # কাল্পনিক বাজারঘর: ২০০০টি অর্ডার
n = 2000
promo = rng.integers(0, 2, n)                # promo code দেওয়া হয়েছিল কি না
first = (rng.random(n) < 0.3).astype(int)    # প্রথমবারের ক্রেতা কি না (~30%)
value = 850 + 120 * promo - 200 * first + 180 * promo * first + rng.normal(0, 150, n)
df = pd.DataFrame({"promo": promo, "first": first, "value": value})
print(df.pivot_table("value", "first", "promo").round(0))   # চারটি ঘরের গড়

def fit(cols, label):
    X = sm.add_constant(df[cols].astype(float))
    r = sm.OLS(df["value"], X).fit()
    print(f"\n{label}\n" + r.params.round(1).to_string() + f"\n(SE of promo: {r.bse['promo']:.1f})")

df["promo_x_first"] = df["promo"] * df["first"]
fit(["promo", "first", "promo_x_first"], "A. hierarchy-সহ পূর্ণ model")
df["first_c"] = df["first"] - df["first"].mean()      # first-কে center করা
df["promo_x_first_c"] = df["promo"] * df["first_c"]
fit(["promo", "first_c", "promo_x_first_c"], "B. first center করে")
fit(["promo", "first"], "C. interaction ছাড়া")
fit(["promo", "promo_x_first"], "D. first-এর main effect বাদ (hierarchy ভাঙা)")

আসল আউটপুট (ব্রাউজারের Python):

promo      0      1
first
0      853.0  970.0
1      650.0  939.0

A. hierarchy-সহ পূর্ণ model
const            852.8
promo            117.0
first           -203.1
promo_x_first    172.3
(SE of promo: 8.0)

B. first center করে
const              792.7
promo              168.0
first_c           -203.1
promo_x_first_c    172.3
(SE of promo: 6.7)

C. interaction ছাড়া
const    827.3
promo    168.0
first   -115.1
(SE of promo: 6.9)

D. first-এর main effect বাদ (hierarchy ভাঙা)
const            793.9
promo            175.9
promo_x_first    -30.8
(SE of promo: 8.0)

চারটি ঘরের গড় ৮৫৩, ৯৭০, ৬৫০ আর ৯৩৯. Model A-র coefficient এই ঘরগুলো থেকেই আসে: const = ৮৫২.৮ (প্রথম ঘর), promo = ৯৭০ − ৮৫৩ ≈ ১১৭, first = ৬৫০ − ৮৫৩ ≈ −২০৩, interaction = (৯৩৯ − ৬৫০) − (৯৭০ − ৮৫৩) ≈ ১৭২. আসল মান ১২০, −২০০, ১৮০-এর কাছেই। Model B-তে শুধু center করায় promo হয়েছে ১৬৮.০: এটি গড় প্রভাব, ১১৭ + ১৭২.৩ × (প্রথমবারের অনুপাত, প্রায় ০.২৯৬)। interaction আর first অপরিবর্তিত। promo-র SE ৮.০ থেকে ৬.৭-এ নেমেছে, কারণ এখন এটি সবার তথ্য দিয়ে মাপা, শুধু ফেরত আসা ক্রেতাদের নয়। Model C (interaction ছাড়া) একই ১৬৮.০ দেয়, কিন্তু দাবি করে “promo সবার জন্য সমান”. Model D সবচেয়ে বিভ্রান্তিকর: interaction হয়ে গেছে −৩০.৮, অর্থাৎ ভুলভাবে বলছে প্রথমবারের ক্রেতায় promo কম কাজ করে। কারণ first-এর main effect না থাকায় promo ছাড়া দুই দলের গড় একই (৭৯৩.৯) ধরা হয়েছে, আর সেই ভুল ঢাকতে interaction উল্টো দিকে গেছে।

সংখ্যার feature-এও একই কথা: দূরত্ব × বৃষ্টি রাখলে বৃষ্টির main effect মানে “০ km-এ বৃষ্টির প্রভাব”; দূরত্ব center করলে “গড় দূরত্বে”. আর বেশি interaction-এ সাবধান: ১০টি feature থেকে জোড়ায় জোড়ায় ৪৫টি interaction, তিনটি করে ১২০টি। সব যোগ করলে model দ্রুত noise ধরতে শুরু করে; পরের অংশ এই সমস্যাটিই সমাধান করে।

৬ · অনেক candidate feature — regularization path আর one-standard-error নিয়ম

বাস্তবে প্রশ্নটি প্রায়ই এমন: ১০টি মূল feature আছে, তাদের জোড়ায় জোড়ায় interaction মিলে ৫৫টি candidate, আর ৩০০টি training সারি। কোনগুলো রাখবেন? পর্ব ১৭-এ দেখেছেন, lasso আর elastic net-এর α বড় থেকে ছোট করলে একটি একটি করে feature ঢোকে; এই পুরো যাত্রাই regularization path. ElasticNetCV প্রতিটি fold-এ পুরো path চালায় (আগের α-র সমাধান থেকে শুরু করে, যাকে warm start বলে), আর প্রতিটি α-র validation MSE রাখে mse_path_-এ। তারপর প্রশ্ন: কোন α?

সবচেয়ে কম গড় CV ভুলের α স্বাভাবিক উত্তর, কিন্তু CV ভুল নিজেই অনিশ্চিত; কাছাকাছি কয়েকটি α-র পার্থক্য প্রায়ই noise. One-standard-error নিয়ম বলে: সর্বনিম্ন গড় ভুলের সঙ্গে তার standard error যোগ করে একটি সীমা ধরুন, আর সেই সীমার নিচে থাকা সবচেয়ে সরল model (সবচেয়ে বড় α) নিন। Breiman, Friedman, Olshen আর Stone-এর CART বই (১৯৮৪) tree ছাঁটাইয়ের জন্য এই নিয়ম দেয়; পরে Hastie, Tibshirani আর Friedman-এর বই আর R-এর glmnet (lambda.1se) একে lasso-তে জনপ্রিয় করে। এটি একটি প্রথা, উপপাদ্য নয়।

α বড় থেকে ছোট (বড় α = সরল model); প্রতিটি সারিতে ৫টি fold-এর validation MSE
ধাপ
α = 2.0 (২টি feature)
α = 1.0 (৪টি)
α = 0.5 (৫টি)
α = 0.25 (৮টি)
α = 0.12 (১৩টি)
α = 0.06 (২১টি)
  1. CV টেবিল। ছয়টি α, প্রতিটিতে পাঁচটি fold-এর validation MSE. বন্ধনীতে সেই α-তে কতটি feature অশূন্য। α ছোট হলে feature বাড়ে।
  2. গড়। প্রতিটি সারির পাঁচটি সংখ্যার গড়: ৩১.৪, ২৩.০, ১৮.২, ১৭.৪, ১৮.০, ১৯.০. সবচেয়ে কম ১৭.৪, α = ০.২৫-এ।
  3. SE. সর্বনিম্ন সারির (১৬, ২০, ১৫, ১৯, ১৭) নমুনা standard deviation (n − 1 দিয়ে ভাগ) ২.০৭৪; fold ৫টি, তাই SE = ২.০৭৪/√৫ = ০.৯২৭.
  4. সীমা। ১৭.৪ + ০.৯২৭ = ১৮.৩৩. যে α-র গড় এর নিচে, সেটি “সর্বনিম্নের থেকে পরিসংখ্যানগতভাবে আলাদা করা যায় না”। α = ০.৫ (১৮.২) আর ০.১২ (১৮.০) সীমার ভেতরে; ০.০৬ (১৯.০) বাইরে।
  5. বড় α থেকে খোঁজা। ২.০ আর ১.০ সীমার বাইরে। প্রথম মিল α = ০.৫. সীমার ভেতরে ০.১২-ও আছে, কিন্তু সেটি জটিলতর (১৩টি feature), তাই নিয়ম সেটি নেয় না।
  6. বাছাই। α = ০.৫, ৮টির বদলে ৫টি feature, CV ভুল ১৭.৪-এর বদলে ১৮.২. সতর্কতা: fold-গুলো একই ডেটা ভাগ করে নেয় বলে এই SE আসলের চেয়ে ছোট হতে পারে (পর্ব ২২), আর নিয়মটি কেবল সর্বনিম্ন বিন্দুর SE ব্যবহার করে।

এবার আসল কাজ। নিচের ব্লকে ৪০০টি কাল্পনিক ডেলিভারি, ১০টি মূল feature, যার মাত্র চারটি (দূরত্ব, পণ্যের সংখ্যা, বৃষ্টি, ভিড়ের সময়) আর একটি interaction (বৃষ্টি × দূরত্ব) সময়কে প্রভাবিত করে। বাকিগুলো (promo, প্রথমবারের ক্রেতা, সপ্তাহান্ত, রাইডারের অভিজ্ঞতা, নতুন app, আর একটি নিছক noise) কিছুই করে না। ২৫% সারি শুরুতেই test হিসেবে তালাবন্ধ। Pipeline: standardize (center), তারপর জোড়ায় জোড়ায় গুণফল, তারপর আবার scale, তারপর ElasticNetCV (l1_ratio ০.৫ আর ১.০, ৫০টি α)। alphas=50 লেখা হয়েছে, কারণ n_alphas scikit-learn ১.৭-এ deprecated হয়েছে আর ১.৯-এ বাদ যাবে; max_iter=50000 যাতে কোনো ConvergenceWarning না আসে।

import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import ElasticNetCV, ElasticNet
from sklearn.model_selection import KFold, train_test_split

rng = np.random.default_rng(5)               # কাল্পনিক বাজারঘর: ৪০০টি ডেলিভারি, ১০টি মূল feature
names = ["dist", "items", "rain", "peak", "promo", "first", "weekend", "rider_exp", "app_new", "noise"]
n = 400
B = np.column_stack([rng.uniform(1, 12, n), rng.integers(1, 9, n)] + [rng.integers(0, 2, n) for _ in range(5)]
                    + [rng.uniform(0, 36, n), rng.integers(0, 2, n), rng.normal(size=n)]).astype(float)
y = 12 + 3 * B[:, 0] + 1.5 * B[:, 1] + 5 * B[:, 2] + 6 * B[:, 3] + 0.8 * B[:, 2] * B[:, 0] + rng.normal(0, 4, n)
B_tr, B_te, y_tr, y_te = train_test_split(B, y, test_size=0.25, random_state=42)   # test আগে আলাদা
poly = PolynomialFeatures(2, interaction_only=True, include_bias=False)
feat = poly.fit(B_tr).get_feature_names_out(names)           # 10 + 45 = 55টি candidate
pipe = lambda est: make_pipeline(StandardScaler(), poly, StandardScaler(), est)   # center → গুণ → scale
cv = KFold(5, shuffle=True, random_state=42)
m = pipe(ElasticNetCV(l1_ratio=[0.5, 1.0], alphas=50, cv=cv, max_iter=50000)).fit(B_tr, y_tr)[-1]
i = list(m.l1_ratio).index(m.l1_ratio_)
mse, alphas = m.mse_path_[i], m.alphas_[i]                    # (50 alpha × 5 fold), alpha বড় → ছোট
mean, se = mse.mean(1), mse.std(1, ddof=1) / np.sqrt(mse.shape[1])
j = mean.argmin(); j1 = np.flatnonzero(mean <= mean[j] + se[j]).min()   # 1-SE: সবচেয়ে বড় alpha
print(f"l1_ratio={m.l1_ratio_}  alpha_min={alphas[j]:.3f}  alpha_1se={alphas[j1]:.3f}")
print(f"CV MSE: min {mean[j]:.2f} ± SE {se[j]:.2f}, threshold {mean[j] + se[j]:.2f}, at 1se {mean[j1]:.2f}")
for tag, a in [("min", alphas[j]), ("1se", alphas[j1])]:
    p = pipe(ElasticNet(alpha=a, l1_ratio=m.l1_ratio_, max_iter=50000)).fit(B_tr, y_tr)
    nz = np.flatnonzero(p[-1].coef_)
    rmse = np.sqrt(np.mean((p.predict(B_te) - y_te) ** 2))
    print(f"\n{tag}: {len(nz)} of {len(feat)} features, test RMSE {rmse:.2f}\n" + ", ".join(feat[nz]))
plt.figure(figsize=(6, 3.4), layout="constrained")
plt.errorbar(np.log10(alphas), mean, yerr=se, fmt="o-", ms=3, capsize=2)
plt.axvline(np.log10(alphas[j]), ls="--", c="k", label="min CV")
plt.axvline(np.log10(alphas[j1]), c="r", label="1-SE rule")
plt.yscale("log"); plt.xlabel("log10(alpha)"); plt.ylabel("5-fold CV MSE (log scale)"); plt.legend(); plt.show()

আসল আউটপুট (ব্রাউজারের Python):

l1_ratio=1.0  alpha_min=0.395  alpha_1se=0.695
CV MSE: min 17.55 ± SE 1.64, threshold 19.20, at 1se 18.66

min: 8 of 55 features, test RMSE 3.69
dist, items, rain, peak, weekend, rider_exp, dist rain, rain rider_exp

1se: 5 of 55 features, test RMSE 3.92
dist, items, rain, peak, dist rain

ElasticNetCV দুটি l1_ratio-র মধ্যে ১.০ (অর্থাৎ খাঁটি lasso) বেছেছে। সর্বনিম্ন CV MSE ১৭.৫৫, SE ১.৬৪, তাই সীমা ১৯.২০; সীমার নিচে সবচেয়ে বড় α = ০.৬৯৫, যেখানে CV MSE ১৮.৬৬. সর্বনিম্ন-α-র model ৮টি feature রেখেছে, যার তিনটি (weekend, rider_exp, rain × rider_exp) ভুয়া। One-SE model ঠিক পাঁচটি রেখেছে: চারটি আসল main effect আর dist × rain, একটিও ভুয়া নয়। কিন্তু তালাবন্ধ test set-এ সর্বনিম্ন-α-র RMSE ৩.৬৯, one-SE-র ৩.৯২: সরল model-এর অনুমান একটু খারাপ, কারণ বড় α আসল coefficient-গুলোকেও বেশি ছোট করেছে। অর্থাৎ one-SE নিয়ম ব্যাখ্যা আর স্থিরতার জন্য ভালো, নিখুঁত অনুমানের জন্য সবসময় নয়। ছবিতে CV ভুলের বক্ররেখা বড় α-তে দ্রুত ওঠে, আর বাঁয়ে প্রায় সমতল: এই সমতল অঞ্চলেই one-SE নিয়ম কাজ করে।

একটি ছোট পরিবর্তন অনেক কিছু বদলায়। ডেস্কটপে একটি স্থানীয় run-এ (scikit-learn 1.8.0) একই ডেটায় শুরুর standardize ধাপটি বাদ দিয়ে, অর্থাৎ center না করেই গুণফল বানিয়ে, one-SE model ১১টি feature রেখেছে, যার ছয়টি ভুয়া interaction (যেমন items × rain, peak × weekend), আর test RMSE ৪.০৫. কারণ: center না করা গুণফল rain × dist নিজের উপাদান rain-এর সঙ্গে খুব সম্পর্কিত, আর lasso সম্পর্কিত কলামের মধ্যে খেয়ালখুশিমতো ওজন ভাগ করে (পর্ব ১৭-এর bootstrap পরীক্ষা মনে করুন)। আরেকটি সমস্যা: lasso hierarchy মানে না, main effect বাদ দিয়ে interaction রাখতে পারে। তখন হয় বাদ পড়া main effect-গুলো ফিরিয়ে আনুন, নয়তো hierarchy মানে এমন পদ্ধতি নিন (Bien, Taylor আর Tibshirani-র ২০১৩ সালের hierNet, বা group lasso).

৭ · Confidence interval বনাম prediction interval — গড়ের পরিসর আর একটি অর্ডারের পরিসর

আবহাওয়া অফিস বলে, “আগামী মাসে ঢাকায় গড় সর্বোচ্চ তাপমাত্রা ৩২ থেকে ৩৩ ডিগ্রির মধ্যে থাকবে।” এটি একটি সরু পরিসর, কারণ অনেক দিনের গড় স্থির। কিন্তু “আগামী মঙ্গলবারের সর্বোচ্চ তাপমাত্রা” বলতে গেলে পরিসর অনেক চওড়া হয়, কারণ একটি দিনের নিজস্ব ওঠানামা আছে। প্রথমটি গড়ের পরিসর, দ্বিতীয়টি একটি একক ঘটনার পরিসর।

একটি নতুন অর্ডারের ইনপুট x₀ (সামনে ১ বসানো সারি)। অনুমান ŷ₀ = x₀ᵀβ̂. দুটি প্রশ্ন আলাদা: (ক) এই দূরত্বের সব অর্ডারের গড় সময় কোথায়? (খ) এই অর্ডারটির সময় কোথায় পড়বে? Residual-এর standard deviation s, আর h₀ = x₀ᵀ(XᵀX)−1x₀ (x₀ ডেটার কেন্দ্র থেকে কত দূরে, তার মাপ; পর্ব ১৭-এর hat matrix-এর একটি ঘর)। তাহলে:

CI: ŷ₀ ± tn−p · s√h₀     PI: ŷ₀ ± tn−p · s√(1 + h₀)

এখানে tn−p হলো n − p degrees of freedom-এর t বণ্টনের ৯৭.৫ শতাংশ বিন্দু (৯৫% পরিসরের জন্য)। এক feature-এর regression-এ h₀ = 1/n + (x₀ − x̄)²/∑(xi − x̄)². Confidence interval-এ কেবল β̂-র অনিশ্চয়তা, যা n বাড়লে ১/√n হারে শূন্যের দিকে যায়। Prediction interval-এ বাড়তি “১”, অর্থাৎ নতুন অর্ডারের নিজের noise, যা n যত বড়ই হোক থাকে। তাই n → ∞-এ CI-এর চওড়া → ০, কিন্তু PI-এর চওড়া → 2 × 1.96 × σ. দুটি সূত্রই ধরে নেয় model ঠিক, noise স্বাধীন আর সব x-এ একই variance (homoscedastic), আর PI-এর জন্য noise প্রায় normal. নিচের ব্লক statsmodels-এর get_prediction দিয়ে দুটি পরিসর বানায়, হাতে সূত্র মেলায়, আর ৫০০০টি ভবিষ্যৎ অর্ডারে যাচাই করে।

import numpy as np
import pandas as pd
import statsmodels.api as sm

def orders(n, seed):                          # কাল্পনিক বাজারঘর: শহরের ভেতরের ডেলিভারি
    r = np.random.default_rng(seed)
    km = r.uniform(1, 8, n)
    return km, 10 + 5 * km + r.normal(0, 6, n)    # আসল গড় 10 + 5·km, noise-এর sd 6
fit = lambda k, yy: sm.OLS(yy, sm.add_constant(k)).fit()

km, y = orders(100, 1)                        # ১০০টি পুরোনো অর্ডারে fit
res = fit(km, y)
x0 = np.array([[1.0, 5.0]])                   # নতুন অর্ডার: ৫ km
print(res.get_prediction(x0).summary_frame(alpha=0.05).round(2).T.to_string(header=False))
h = (x0 @ np.linalg.inv(res.model.exog.T @ res.model.exog) @ x0.T).item()
s = np.sqrt(res.scale)                        # residual-এর standard deviation
print(f"s = {s:.2f}  h0 = {h:.4f}  SE(mean) = {s*np.sqrt(h):.2f}  SE(new) = {s*np.sqrt(1 + h):.2f}\n")

km_new, y_new = orders(5000, 99)              # ভবিষ্যতের ৫০০০টি অর্ডার (held-out)
rows = []
for n in (20, 100, 1000):
    f = fit(*orders(n, 1)).get_prediction(sm.add_constant(km_new)).summary_frame(alpha=0.05)
    rows.append({"n": n, "CI width": (f.mean_ci_upper - f.mean_ci_lower).mean(),
                 "PI width": (f.obs_ci_upper - f.obs_ci_lower).mean(),
                 "new y in CI": ((f.mean_ci_lower <= y_new) & (y_new <= f.mean_ci_upper)).mean(),
                 "new y in PI": ((f.obs_ci_lower <= y_new) & (y_new <= f.obs_ci_upper)).mean()})
print(pd.DataFrame(rows).round(3).to_string(index=False))

hit_ci = hit_pi = 0                           # ৫০০ বার নতুন training set: ৫ km-এ কতবার ঢাকে?
for rep in range(500):
    fr = fit(*orders(100, 1000 + rep)).get_prediction(x0).summary_frame(alpha=0.05).iloc[0]
    y5 = 35 + np.random.default_rng(rep).normal(0, 6)       # ৫ km-এর একটি নতুন অর্ডার
    hit_ci += fr.mean_ci_lower <= 35 <= fr.mean_ci_upper
    hit_pi += fr.obs_ci_lower <= y5 <= fr.obs_ci_upper
print(f"\n500 repeats: CI covers true mean 35 in {hit_ci/500:.3f}, PI covers a new order in {hit_pi/500:.3f}")

আসল আউটপুট (ব্রাউজারের Python):

mean           34.65
mean_se         0.61
mean_ci_lower  33.44
mean_ci_upper  35.86
obs_ci_lower   22.71
obs_ci_upper   46.59
s = 5.99  h0 = 0.0104  SE(mean) = 0.61  SE(new) = 6.02

   n  CI width  PI width  new y in CI  new y in PI
  20     9.766    31.878        0.564        0.988
 100     3.286    24.000        0.220        0.957
1000     1.055    24.268        0.071        0.959

500 repeats: CI covers true mean 35 in 0.960, PI covers a new order in 0.966

৫ km-এর অনুমান ৩৪.৬৫ মিনিট (আসল গড় ৩৫)। summary_frame-এর mean_ci_* হলো CI: ৩৩.৪৪ থেকে ৩৫.৮৬, আর obs_ci_* হলো PI: ২২.৭১ থেকে ৪৬.৫৯. হাতের হিসাব মেলে: s = ৫.৯৯, h₀ = ০.০১০৪, SE(গড়) = ৫.৯৯ × √০.০১০৪ = ০.৬১, SE(নতুন) = ৫.৯৯ × √১.০১০৪ = ৬.০২; ৯৮ df-এর t ≈ ১.৯৮৪, তাই ৩৪.৬৫ ± ১.২১ আর ৩৪.৬৫ ± ১১.৯৫. টেবিলে n ২০ থেকে ১০০০ হলে CI-এর গড় চওড়া ৯.৮ থেকে ১.১-এ নেমেছে, কিন্তু PI প্রায় ২৪-এ আটকে, যা 2 × 1.96 × 6 = ২৩.৫-এর কাছে। CI-কে একটি অর্ডারের পরিসর বললে n = ১০০০-এ মাত্র ৭.১% নতুন অর্ডার তাতে পড়ে; PI-এ পড়ে ৯৫.৭ থেকে ৯৮.৮%. n = ২০-এ ৯৮.৮% হওয়ার কারণ: একটিমাত্র training set-এর s আর β̂ ঘটনাক্রমে যেমন এসেছে; ৯৫%-এর প্রতিশ্রুতি অনেক training set-এর গড়ে। শেষ লাইন ঠিক সেটি দেখায়: ৫০০টি নতুন training set-এ CI আসল গড় ৩৫-কে ৯৬.০% বার ঢেকেছে, আর PI একটি নতুন অর্ডারকে ৯৬.৬% বার। ৫০০ বারের পরীক্ষায় অনুপাতের নিজের SE প্রায় ০.০১, তাই দুটিই ৯৫%-এর সঙ্গে সঙ্গতিপূর্ণ।

এই সুন্দর ফলাফলের পেছনে ডেটা নিজেই সূত্রের অনুমান মেনে বানানো। বাস্তবে ডেলিভারির ভুল দূরত্বের সঙ্গে বাড়ে (heteroscedasticity), আর কিছু অর্ডার হঠাৎ অনেক দেরি হয়। তখন একই চওড়া PI কাছের অর্ডারে বেশি চওড়া আর দূরের অর্ডারে কম চওড়া হয়, আর সামগ্রিক ৯৫% ঠিক থাকলেও প্রতিটি দলে ভুল হয়। এর দুটি প্রতিকার আসছে: পর্ব ২৪ (Robust আর quantile regression — outlier আর অনুমানের পরিসর) সরাসরি ১০ম আর ৯০তম percentile শেখে, আর পর্ব ৫৮ (Conformal prediction) প্রায় কোনো অনুমান ছাড়াই coverage-এর নিশ্চয়তা দেয়। আর ridge বা lasso দিয়ে fit করা model-এ উপরের সূত্র খাটে না, কারণ shrinkage-এর bias আছে; সেখানে bootstrap (পর্ব ২১) বা conformal পদ্ধতি লাগে।

আরও গভীরে: Basis expansion, spline আর regularization-এর একসঙ্গে সবচেয়ে পরিষ্কার আলোচনা Hastie, Tibshirani আর Friedman-এর The Elements of Statistical Learning-এর পঞ্চম অধ্যায়ে; smoothing spline সেখানে দেখায় যে বক্রতার শাস্তিসহ সব function-এর মধ্যে সেরা উত্তর হুবহু প্রতিটি ডেটা বিন্দুতে knot-ওয়ালা natural cubic spline. GAM-এর আধুনিক তত্ত্ব আর REML দিয়ে λ বাছাই Simon Wood-এর Generalized Additive Models: An Introduction with R (দ্বিতীয় সংস্করণ ২০১৭)-এ। Model বাছাইয়ের পরে বৈধ inference-এর প্রশ্নটি “post-selection inference” নামে আলাদা গবেষণার ক্ষেত্র (যেমন Berk ও সহকর্মীদের ২০১৩ সালের PoSI, আর Lee ও সহকর্মীদের ২০১৬ সালের lasso-র জন্য exact inference).

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

  • Polynomial regression কি linear regression? হ্যাঁ, parameter-এ linear; x, x², x³ শুধু নতুন কলাম, তাই least squares-এর closed form খাটে।
  • Cubic spline-এ ৫টি knot থাকলে কতটি কলাম? B-spline basis-এ n_knots + degree − 1 = ৫ + ৩ − ১ = ৭ (scikit-learn-এর SplineTransformer-এর default).
  • y = b₀ + b₁A + b₂B + b₃AB-এ b₁ মানে কী? B = ০ হলে A এক একক বাড়লে y-এর বদল; B center করলে B-এর গড়ে A-র প্রভাব।
  • One-standard-error নিয়ম কী? সর্বনিম্ন CV ভুল + তার SE-কে সীমা ধরে সীমার নিচের সবচেয়ে সরল model (সবচেয়ে বড় λ) নেওয়া।
  • PI কেন CI-এর চেয়ে চওড়া? PI-এর variance s²(1 + h₀), CI-এর s²h₀; বাড়তি s² নতুন পর্যবেক্ষণের নিজের noise, যা n বাড়লেও কমে না।

৮ · AI-এর যুগে — basis, interaction আর interval-এর দাবি যাচাই

“একটু polynomial যোগ করো”, “সব interaction দিয়ে lasso চালাও”, “৯৫% interval দাও”: AI assistant এই তিনটি কাজ দ্রুত করে দেয়, আর কোড প্রায় সবসময় চলে। ভুল থাকে অর্থে: কোন পরিসর, কোন coefficient, কোন ডেটায় বাছাই। নিচের টেবিলে সাধারণ ভুলগুলো।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
পুরো ডেটায় PolynomialFeatures আর scaler fit করে, তারপর ভাগ করে; বা test set দেখে ডিগ্রি বাছেLeakage, আর test আর নিরপেক্ষ নয়শুরুতেই test আলাদা; সব ধাপ একটি Pipeline-এ, ডিগ্রি বা knot CV-তে
Interaction রেখে main effect বাদ দেয়, বা “promo-র coefficient = promo-র প্রভাব” বলেInteraction থাকলে main effect শর্তসাপেক্ষ; hierarchy ভাঙলে অর্থ বদলায়চারটি ঘরের গড় হাতে বের করে coefficient-এর সঙ্গে মেলান; center করে আবার fit করুন
Spline বা standardized model-এর coef_ সরাসরি “প্রতি km-এ মিনিট” বলে পড়েCoefficient রূপান্তরিত এককে; spline-এ একক coefficient-এর সহজ অর্থ নেইGrid-এ অনুমান এঁকে পার্থক্য পড়ুন; standardized হলে sd দিয়ে ফেরত আনুন
পুরোনো n_alphas লেখে, বা one-SE-তে SE-র বদলে sd ব্যবহার করে, বা α-র ক্রম উল্টো ধরে১.৭-এ deprecated parameter; sd দিলে সীমা √K গুণ বড়; ক্রম ভুলে সবচেয়ে জটিল model আসেনিজের version-এর docs পড়ুন; alphas_ ছাপিয়ে ক্রম দেখুন; stepper-এর মতো ছোট টেবিলে হাতে মেলান
Confidence interval-কে একটি অর্ডারের পরিসর বলেCI গড়ের; একক পর্যবেক্ষণের জন্য PI লাগেHeld-out ডেটায় empirical coverage মাপুন, যেমন অংশ ৭-এর ব্লক
Lasso দিয়ে বাছা feature-এর ওপর OLS চালিয়ে সাধারণ p-value রিপোর্ট করেবাছাই একই ডেটায় হলে p-value আশাবাদীআলাদা ডেটায় যাচাই, বা post-selection পদ্ধতি; দাবি নরম করুন

AI-কে কোডের সঙ্গে অর্থ আর যাচাইও চাইতে বলুন:

আমার কাছে একটি ডেলিভারি-সময়ের ডেটা আছে: দূরত্ব, পণ্যের সংখ্যা, বৃষ্টি, ভিড়ের সময় আর আরও ৬টি feature, ৪০০টি সারি। scikit-learn 1.8 আর statsmodels ব্যবহার করবে। ১. শুরুতেই test set আলাদা করো; দূরত্বের জন্য SplineTransformer আর বাকিদের জন্য StandardScaler একটি ColumnTransformer-এ রাখো। ২. জোড়ায় জোড়ায় interaction বানানোর আগে কেন center করতে হয়, এক লাইনে বলো। ৩. ElasticNetCV চালাও (alphas পূর্ণসংখ্যা হিসেবে, n_alphas নয়); mse_path_ থেকে one-standard-error নিয়মে alpha বের করার কোড দাও, SE = sd/√(fold সংখ্যা) সহ। ৪. বাছা model-এ কোনো interaction থাকলে তার main effect আছে কি না পরীক্ষা করো। ৫. statsmodels-এর get_prediction দিয়ে একটি নতুন অর্ডারের prediction interval দাও, আর test set-এ CI আর PI-এর empirical coverage ছাপো। ৬. কোনো warning চাপা দিও না, আর কোন দাবি এই ফল থেকে করা যায় না, সেটিও লেখো।

AI-এর দেওয়া linear model হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Tree আর neural network যখন নিজেই বাঁকা সম্পর্ক শেখে, তখন হাতে basis বানানো linear model আজও কেন টিকে আছে?

প্রথম কারণ পড়তে পারা। একটি spline বা GAM-এর প্রতিটি অংশ আলাদা করে আঁকা যায়: “দূরত্বের প্রভাব ৫ km পর্যন্ত খাড়া, তারপর সমতল, ১০ km-এ ব্রিজের জট”. এই ছবি একজন অপারেশন ম্যানেজার নিজে যাচাই করতে পারেন। ঋণ, বিমা বা চিকিৎসার মতো নিয়ন্ত্রিত ক্ষেত্রে অনেক প্রতিষ্ঠান এমন model পছন্দ করে, কারণ প্রতিটি ইনপুট সিদ্ধান্তকে কীভাবে নাড়ায় তা দেখানো যায়। Microsoft Research-এর Rich Caruana ও সহকর্মীরা ২০১৫ সালে নিউমোনিয়া রোগীর ঝুঁকির একটি GAM-ধাঁচের model-এ দেখান যে model হাঁপানির রোগীদের কম ঝুঁকিপূর্ণ বলছে; কারণ ডেটায় হাঁপানির রোগীরা আগেই নিবিড় চিকিৎসা পেতেন। পড়তে পারা model বলেই এই বিপজ্জনক নিদর্শন চোখে পড়েছিল; একটি কালো বাক্সে তা লুকিয়ে থাকত।

দ্বিতীয় কারণ ছোট ডেটা আর পরিসংখ্যান। কয়েকশ সারিতে একটি penalized spline-এর variance কম, সমস্যাটি convex, আর interval-এর তত্ত্ব পরিচিত। তৃতীয় কারণ গতি: অনুমান একটি গুণফল, খুব সস্তা আর পূর্বানুমেয়।

তবে সীমাও স্পষ্ট। অনেক feature-এর জটিল interaction হাতে basis বানিয়ে ধরা অবাস্তব; সেখানে tree-ভিত্তিক model এগিয়ে, যা মডিউল ৭-এ আসবে। Explainable Boosting Machine-এর মতো পদ্ধতি মাঝামাঝি পথ নেয়: GAM-এর কাঠামো রেখে boosting দিয়ে প্রতিটি অংশ শেখে। Tabular ডেটায় কোন পরিবার কখন ভালো, তা নিয়ে মতভেদ আছে; পর্ব ৩৮ এই প্রশ্নে ফিরবে।

মূল উপলব্ধি: basis expansion নমনীয়তা দেয় কিন্তু পড়তে পারা আর পরিসংখ্যানের তত্ত্ব হারায় না; যেখানে ব্যাখ্যা আর অনিশ্চয়তা জরুরি, সেখানে এটি প্রায়ই প্রথম পছন্দ।

প্র ০২Polynomial দিয়ে বাঁকা সম্পর্ক ধরার পুরোনো অভ্যাস থেকে spline-এ সরে আসার পেছনে কোন অভিজ্ঞতা কাজ করেছে?

Polynomial-এর আকর্ষণ পুরোনো আর যুক্তিসঙ্গত: Weierstrass-এর উপপাদ্য (১৮৮৫) বলে যে একটি বদ্ধ পরিসরে যেকোনো continuous function-কে polynomial দিয়ে যত ইচ্ছা কাছাকাছি আনা যায়। কিন্তু উপপাদ্যটি কেবল অস্তিত্বের কথা বলে, কোন ডেটা থেকে কীভাবে শিখবেন তা নয়। Carl Runge ১৯০১ সালে দেখান, সমান দূরত্বের বিন্দুতে উঁচু ডিগ্রির interpolating polynomial প্রান্তে আরও বেশি দোলে, ডিগ্রি বাড়ালে ভুল কমে না, বাড়ে। এই পর্বের অংশ ৩-এর ব্লকে একই ঘটনা: ৮ ডিগ্রিতে CV RMSE ৩.৫৭, ১৬-তে ৭.২৬, ৩০-এ ১৫.৬৪.

মূল সমস্যা polynomial-এর বৈশ্বিক চরিত্র। xj কলামটি পুরো পরিসরে অশূন্য, তাই ডেটার এক কোণের একটি বিন্দু সব coefficient নাড়ায়, আর সেই নাড়া দূরের প্রান্তে বড় হয়ে দেখা দেয়। বাস্তব সম্পর্কে প্রায়ই স্থানীয় ঘটনা থাকে (শহরের সীমা, একটি ব্রিজ, একটি নিয়ম বদলের তারিখ); polynomial সেগুলো ধরতে গেলে সব জায়গায় দোলে।

Spline-এর ধারণা এসেছে প্রকৌশল থেকে। জাহাজ আর বিমানের নকশাকাররা বহুকাল ধরে কয়েকটি ভারী বাটখারা দিয়ে আটকানো নমনীয় কাঠের পাত দিয়ে মসৃণ বক্ররেখা আঁকতেন। Isaac Schoenberg ১৯৪৬ সালে এর গাণিতিক রূপ দেন। ১৯৬০-এর দশকে মোটরগাড়ির নকশায় আর পরে কম্পিউটার গ্রাফিক্সে টুকরো টুকরো polynomial মানক হয়ে ওঠে। Carl de Boor আর Maurice Cox ১৯৭২ সালের কাছাকাছি B-spline হিসাবের স্থিতিশীল recursion দেন, যা সংখ্যার দিক থেকে নিরাপদ। পরিসংখ্যানে Grace Wahba-র smoothing spline-এর কাজ (১৯৭০-এর দশক থেকে) আর পরে Eilers ও Marx-এর P-spline একে regression-এর দৈনন্দিন হাতিয়ার বানায়।

তবু কম ডিগ্রির (২ বা ৩) polynomial অল্প ডেটায় এখনো ভালো, আর Chebyshev-এর মতো orthogonal polynomial সংখ্যার অস্থিরতা অনেকটা কমায়।

মূল উপলব্ধি: approximation-এর তত্ত্বে কী সম্ভব আর সসীম noisy ডেটায় কী শেখা যায়, দুটি আলাদা প্রশ্ন; স্থানীয় basis দ্বিতীয় প্রশ্নের উত্তরে বেশি নির্ভরযোগ্য।

প্র ০৩Hierarchy নিয়ম কি সবসময় মানতে হবে? কোনো ক্ষেত্রে main effect ছাড়া interaction রাখা কি যুক্তিসঙ্গত?

Hierarchy-র পক্ষে সবচেয়ে জোরালো যুক্তি দেন John Nelder, ১৯৭৭ সালে Journal of the Royal Statistical Society-তে “marginality” নিয়ে লিখে: interaction-এর অর্থ তখনই পরিষ্কার, যখন তার উপাদান main effect-ও model-এ থাকে। এই পর্বের Model D সেটি দেখিয়েছে। first-এর main effect বাদ দিতেই interaction ১৭২ থেকে −৩১-এ উল্টে গেছে, কারণ model একটি লুকানো শর্ত মেনেছে (“promo ছাড়া দুই দলের গড় সমান”)। আরেকটি গুরুত্বপূর্ণ বৈশিষ্ট্য: hierarchy-সহ model-এ feature-এর শূন্য বিন্দু সরালে (যেমন center করলে) অনুমান একই থাকে। Hierarchy-ছাড়া model-এ একই ডেটা, একই তথ্য, কিন্তু কেবল কোডিং বদলালেই অনুমান বদলায়। ফলাফল যদি কে কীভাবে ০ আর ১ বসিয়েছে তার ওপর নির্ভর করে, সেটি বিজ্ঞান নয়, দুর্ঘটনা।

তবু কিছু বিরল ক্ষেত্রে ব্যতিক্রম যুক্তিসঙ্গত। যেমন পদার্থবিদ্যা বা প্রকৌশলের কোনো সূত্র যদি বলে প্রভাবটি কেবল গুণফল হিসেবেই আসে (যেমন কাজ = বল × দূরত্ব), আর শূন্য বিন্দুটির বাস্তব অর্থ আছে (বল শূন্য মানে সত্যিই কাজ শূন্য), তখন main effect ছাড়া গুণফল রাখা তত্ত্ব-সমর্থিত। পরিমাপের শূন্য বিন্দু অর্থপূর্ণ হলেই কেবল এমন model-এর সাহস করা যায়; তাপমাত্রার সেলসিয়াস বা “বছর”-এর মতো যেখানে শূন্য স্বেচ্ছাচারী, সেখানে নয়।

অন্যদিকে অনুমানই একমাত্র লক্ষ্য হলে কেউ কেউ বলেন lasso যা বাছে তা-ই যথেষ্ট। কিন্তু অভিজ্ঞতায় hierarchy মানা model প্রায়ই সমান ভালো অনুমান দেয় আর বেশি স্থির থাকে। Bien, Taylor আর Tibshirani-র hierNet (২০১৩) দুটি রূপ দেয়: strong hierarchy (interaction থাকলে দুটি main effect-ই থাকবে) আর weak (অন্তত একটি)। আধুনিক অনুশীলনে default হিসেবে strong hierarchy আর ব্যতিক্রম হলে লিখিত যুক্তি, এটিই নিরাপদ পথ।

মূল উপলব্ধি: hierarchy মানলে coefficient কোডিং-নিরপেক্ষ আর ব্যাখ্যাযোগ্য থাকে; ভাঙতে হলে তত্ত্ব আর অর্থপূর্ণ শূন্য বিন্দু লাগে, সুবিধা নয়।

প্র ০৪One-standard-error নিয়ম পাতার উদাহরণে ঠিক feature বেছেছে, কিন্তু test ভুল বাড়িয়েছে। তাহলে কখন এটি ব্যবহার করবেন, আর কখন নয়?

নিয়মটির উৎস Breiman, Friedman, Olshen আর Stone-এর ১৯৮৪ সালের CART বই, যেখানে decision tree ছাঁটাইয়ের জন্য তাঁরা দেখেন CV ভুলের বক্ররেখা সর্বনিম্নের কাছে প্রায়ই সমতল আর অস্থির; সর্বনিম্ন বিন্দুটি নিলে কখনো অকারণে বড় tree আসে। “এক SE-র মধ্যে সবচেয়ে সরল” বেছে তাঁরা স্থির আর ছোট tree পেতেন, নির্ভুলতায় সামান্য ক্ষতি নিয়ে। পরে glmnet-এর lambda.1se আর ESL বই একে lasso-র default পরামর্শের কাছাকাছি নিয়ে যায়।

One-SE model ঠিক পাঁচটি আসল feature রেখেছে, সর্বনিম্ন-α-র model তিনটি ভুয়া যোগ করেছে। কিন্তু test RMSE ৩.৬৯ থেকে ৩.৯২-এ উঠেছে, কারণ বড় α আসল coefficient-গুলোকেও বেশি ছোট করেছে (lasso-র shrinkage bias). তাই প্রশ্নটি লক্ষ্যের: আপনার কাজ কি “কোন feature সত্যিই কাজ করে” খোঁজা, যাতে একটি dashboard বা নীতি বানাবেন, নাকি “পরের অর্ডারের সময়ের সবচেয়ে ভালো অনুমান”? প্রথমটিতে one-SE ভালো। দ্বিতীয়টিতে সর্বনিম্ন-CV α, বা বাছাইয়ের পরে কম শাস্তিতে আবার fit (relaxed lasso, Meinshausen ২০০৭), প্রায়ই ভালো।

আরও সাবধানতা: fold-গুলো একই ডেটা ভাগ করে, তাই sd/√K আসল অনিশ্চয়তা কম দেখাতে পারে (পর্ব ২২); মাত্র পাঁচটি সংখ্যার SE বলে seed বদলালে one-SE-র α লাফাতে পারে, যা repeated CV স্থির করে। আর কোনো নিয়মই তালাবন্ধ test set-এর বিকল্প নয়।

মূল উপলব্ধি: one-SE নিয়ম ব্যাখ্যা আর স্থিরতার জন্য সামান্য অনুমান-ক্ষমতা বিসর্জন দেয়; লক্ষ্য ঠিক করে নিয়ম বাছুন, আর সিদ্ধান্তটি রিপোর্টে লিখুন।

প্র ০৫বাজারঘর ক্রেতাকে “আপনার অর্ডার ৩৫ ± ১ মিনিটে পৌঁছাবে” দেখাতে চায়। পরিসংখ্যানের দিক থেকে এই বার্তায় কী ভুল, আর সৎ বার্তা কেমন হবে?

“± ১ মিনিট” সংখ্যাটি সম্ভবত এসেছে গড়ের confidence interval থেকে। অংশ ৭-এর ব্লকে ১০০টি অর্ডারে CI ছিল ৩৩.৪৪ থেকে ৩৫.৮৬, প্রায় ± ১.২. কিন্তু এটি “৫ km দূরত্বের সব অর্ডারের গড় সময়” কোথায় আছে তার পরিসর, একটি নির্দিষ্ট অর্ডারের নয়। একই ব্লকে n = ১০০০-এ CI-এর ভেতরে মাত্র ৭.১% নতুন অর্ডার পড়েছে। অর্থাৎ বার্তাটি বেশিরভাগ ক্রেতার কাছে ভুল প্রতিশ্রুতি হবে, আর ক্রেতার আস্থা হারানোর খরচ সংখ্যার চেয়ে বড়।

সৎ বার্তার প্রথম ধাপ prediction interval: একই ডেটায় ২২.৭ থেকে ৪৬.৬ মিনিট। এটি চওড়া, কিন্তু সত্য। ব্যবসার দিক থেকে প্রায়ই একতরফা সীমা বেশি কাজের: “বেশিরভাগ অর্ডার ৪৫ মিনিটের মধ্যে পৌঁছায়” ধরনের বাক্য, যা ক্রেতা বোঝেন।

দ্বিতীয় ধাপ অনুমানগুলো যাচাই। Normal-ভিত্তিক PI ধরে নেয় noise-এর variance সব জায়গায় সমান। বাস্তবে দূরের অর্ডার, বৃষ্টির দিন বা সন্ধ্যার ভিড়ে ছড়ানো অনেক বেশি; আর ডেলিভারির দেরি প্রায়ই ডানদিকে লম্বা লেজওয়ালা। তখন একই চওড়া PI কাছের অর্ডারে অকারণে চওড়া আর দূরের অর্ডারে বিপজ্জনকভাবে সরু। প্রতিকার হিসেবে দলভিত্তিক coverage মাপুন (বৃষ্টি/রোদ, দূরত্বের ভাগ), quantile regression দিয়ে সরাসরি ৯০তম percentile শিখুন (পর্ব ২৪), বা conformal prediction দিয়ে coverage-এর নিশ্চয়তা নিন (পর্ব ৫৮)।

তৃতীয় ধাপ নজরদারি: ঈদের ভিড় বা নতুন রাস্তায় অতীত আর ভবিষ্যতের মতো থাকে না, তাই প্রতিদিন দেখুন কত শতাংশ অর্ডার ঘোষিত সীমার মধ্যে পৌঁছাল (পর্ব ৬৯)।

মূল উপলব্ধি: একক ঘটনার প্রতিশ্রুতিতে prediction interval লাগে, আর সেই interval-এর coverage held-out আর প্রতিটি গুরুত্বপূর্ণ দলে মেপে তবেই ক্রেতাকে দেখানো উচিত।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। চাইলে অংশ ৫ আর ৭-এর ব্লক বদলে যাচাই করুন।

  1. পূর্ণ numerical: পাঁচটি অর্ডারের দূরত্ব x = (১, ২, ৩, ৪, ৫) km আর সময় y = (৩, ৫, ৬, ৮, ৮) দশ-মিনিট এককে। (ক) Least squares-এর b₀, b₁ বের করুন। (খ) Residual থেকে s² (n − 2 দিয়ে ভাগ) বের করুন। (গ) x₀ = ৬-এ গড়ের ৯৫% CI আর নতুন অর্ডারের ৯৫% PI বের করুন (৩ df-এ t = ৩.১৮২)। (ঘ) x₀ = ৩-এ দুটির চওড়া কেন কম?

    (ক) x̄ = ৩, ȳ = ৬, ∑(x − x̄)² = ১০, ∑(x − x̄)(y − ȳ) = (−২)(−৩) + (−১)(−১) + ০ + (১)(২) + (২)(২) = ১৩. তাই b₁ = ১.৩, b₀ = ৬ − ১.৩ × ৩ = ২.১. (খ) অনুমান ৩.৪, ৪.৭, ৬.০, ৭.৩, ৮.৬; residual −০.৪, ০.৩, ০, ০.৭, −০.৬; বর্গের যোগ ১.১০; s² = ১.১০/৩ = ০.৩৬৬৭, s = ০.৬০৫৫. (গ) ŷ₀ = ২.১ + ১.৩ × ৬ = ৯.৯. h₀ = ১/৫ + (৬ − ৩)²/১০ = ১.১. SE(গড়) = ০.৬০৫৫ × √১.১ = ০.৬৩৫, SE(নতুন) = ০.৬০৫৫ × √২.১ = ০.৮৭৭. CI = ৯.৯ ± ৩.১৮২ × ০.৬৩৫ = ৭.৮৮ থেকে ১১.৯২; PI = ৯.৯ ± ৩.১৮২ × ০.৮৭৭ = ৭.১১ থেকে ১২.৬৯ (statsmodels-এর get_prediction-এর সঙ্গে মেলে)। (ঘ) x₀ = ৩ = x̄-এ h₀ = ১/৫ = ০.২, সবচেয়ে ছোট: CI ৫.১৪ থেকে ৬.৮৬, PI ৩.৮৯ থেকে ৮.১১. ডেটার কেন্দ্র থেকে যত দূরে, ঢালের অনিশ্চয়তা তত বড় হয়ে দেখা দেয়; x₀ = ৬ ডেটার বাইরে, তাই h₀ > ১. Extrapolation-এ interval চওড়া হওয়া একটি সৎ সতর্কবার্তা।

  2. সমালোচনা ও নকশা: বাজারঘরের একজন বিশ্লেষক (বা একটি AI assistant) এভাবে কাজ করেছেন: পুরো ডেটায় দূরত্বের ১২ ডিগ্রির PolynomialFeatures আর সব জোড়ার interaction বানানো; center না করে LassoCV চালানো; lasso যে interaction রেখেছে (promo × weekend) তার main effect বাদ পড়লেও রেখে দেওয়া; আর রিপোর্টে লেখা “promo-র coefficient ৪৫, তাই promo প্রতিটি অর্ডারে ৪৫ টাকা বাড়ায়, ৯৫% CI ৪০ থেকে ৫০, তাই প্রায় প্রতিটি promo অর্ডার ৪০ থেকে ৫০ টাকা বেশি হবে”. ভুলগুলো ধরুন আর একটি সঠিক নকশা দিন।

    ভুল: (১) পুরো ডেটায় রূপান্তর আর বাছাই, কোনো তালাবন্ধ test নেই। (২) ১২ ডিগ্রির polynomial প্রান্তে দোলে; ডিগ্রি CV-তে বাছা হয়নি, আর spline বেশি নিরাপদ। (৩) Center না করা interaction উপাদানের সঙ্গে খুব সম্পর্কিত, তাই lasso-র বাছাই অস্থির আর ভুয়া interaction আসে। (৪) promo × weekend রেখে promo বা weekend-এর main effect বাদ: hierarchy ভাঙা, coefficient-এর অর্থ কোডিং-নির্ভর। (৫) Interaction থাকলে “promo-র coefficient” মানে weekend = ০ হলে promo-র প্রভাব, সবার নয়। (৬) Lasso-র coefficient shrink করা, আর একই ডেটায় বাছাইয়ের পরে সাধারণ CI বৈধ নয়। (৭) CI গড়ের পরিসর; “প্রতিটি অর্ডার ৪০ থেকে ৫০ টাকা বেশি” দাবির জন্য PI লাগে, যা অনেক চওড়া হবে। (৮) Promo কাকে দেওয়া হয়েছে তা এলোমেলো না হলে এটি কারণগত প্রভাবও নয়। সঠিক নকশা: শুরুতেই test আলাদা; pipeline-এ standardize, দূরত্বে SplineTransformer (knot বা শাস্তি CV-তে), তারপর center করা interaction; ElasticNetCV আর one-SE বা min-CV, লক্ষ্য লিখে; বাছা interaction-এর main effect ফেরত আনা; promo-র গড় প্রভাব চাইলে weekend center করা; অনিশ্চয়তা bootstrap বা আলাদা ডেটায়; একক অর্ডারের জন্য PI আর তার coverage; আর কারণগত দাবির জন্য A/B test (পর্ব ৬২)।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    import numpy as np
    from sklearn.preprocessing import SplineTransformer, PolynomialFeatures
    st = SplineTransformer(degree=1, knots=np.array([[0.0], [5.0], [10.0]]))
    print(st.fit_transform(np.array([[2.5], [7.0], [10.0]])))
    pf = PolynomialFeatures(2, interaction_only=True, include_bias=False).fit(np.zeros((1, 3)))
    print(pf.get_feature_names_out(["a", "b", "c"]))

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

    [[0.5 0.5 0. ]
     [0.  0.6 0.4]
     [0.  0.  1. ]]
    ['a' 'b' 'c' 'a b' 'a c' 'b c']

    তিনটি knot (০, ৫, ১০), degree ১, তাই ৩ + ১ − ১ = ৩টি hat কলাম। ২.৫ ঠিক ০ আর ৫-এর মাঝে: [০.৫, ০.৫, ০]. ৭ পড়েছে ৫ আর ১০-এর মাঝে, ৫ থেকে ২: (১০ − ৭)/৫ = ০.৬ আর (৭ − ৫)/৫ = ০.৪. ১০ শেষ knot-এর ওপর: [০, ০, ১]. প্রতিটি সারির যোগফল ১. দ্বিতীয় লাইন: interaction_only=True বর্গ (a², b², c²) বাদ দেয়, তাই তিনটি মূল কলাম আর তিনটি জোড়া; নামের মধ্যে গুণফল বোঝাতে scikit-learn ফাঁকা জায়গা দেয়।

  4. কোডিং: one_se_alpha(model) নামে একটি function লিখুন, যা একটি fit করা LassoCV নেয় আর one-standard-error নিয়মের α ফেরত দেয়। alphas_-এর ক্রমের ওপর ভরসা না করে কাজ করতে হবে। তারপর stepper-এর টেবিলের সংখ্যা (৬টি α, ৫টি fold) একটি NumPy array-এ বসিয়ে যাচাই করুন যে উত্তর ০.৫.
    import numpy as np
    
    def one_se_from(alphas, mse):            # mse: (n_alpha, n_fold)
        mean = mse.mean(axis=1)
        se = mse.std(axis=1, ddof=1) / np.sqrt(mse.shape[1])
        j = mean.argmin()
        ok = mean <= mean[j] + se[j]
        return alphas[ok].max()              # সীমার ভেতরে সবচেয়ে বড় α = সরলতম
    
    def one_se_alpha(model):                 # fit করা LassoCV
        return one_se_from(np.asarray(model.alphas_), model.mse_path_)
    
    alphas = np.array([0.06, 2.0, 0.5, 0.25, 1.0, 0.12])     # ইচ্ছা করে এলোমেলো ক্রম
    mse = np.array([[18, 22, 16, 21, 18], [30, 34, 29, 33, 31], [17, 20, 16, 20, 18],
                    [16, 20, 15, 19, 17], [22, 25, 21, 24, 23], [17, 21, 15, 20, 17]], float)
    print(one_se_from(alphas, mse))

    ডেস্কটপে সত্যিকারের run ছাপে 0.5. কৌশলটি হলো সূচক না খুঁজে সরাসরি “সীমার ভেতরের α-গুলোর সর্বোচ্চ” নেওয়া, তাই ক্রম যেমনই হোক উত্তর ঠিক। দুটি সাধারণ ভুল: ddof=1 ভুলে যাওয়া (তখন sd ১.৮৫৫, SE ০.৮৩, সীমা ১৮.২৩; এখানে ফল একই থাকে, অন্য ডেটায় নাও থাকতে পারে) আর √K দিয়ে ভাগ না করা (তখন সীমা ১৯.৪৭; এখানে উত্তর একই থাকে, কিন্তু সীমা √৫ গুণ ঢিলা হওয়ায় অন্য ডেটায় দরকারের চেয়ে সরল model আসতে পারে)। ElasticNetCV-এর জন্য আগে সেরা l1_ratio_-এর সারি বেছে নিতে হয়, কারণ সেখানে mse_path_-এর আকার (n_l1_ratio, n_alpha, n_fold).

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

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

ক্রমঅনেক candidate feature থেকে regularized linear model বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. শুরুতেই test set আলাদা করে তালাবন্ধ রাখা
  2. Pipeline-এ standardize (center), basis আর interaction কলাম বানানো
  3. ElasticNetCV দিয়ে প্রতিটি fold-এ পুরো regularization path চালানো
  4. mse_path_ থেকে গড় আর SE বের করে one-SE বা min-CV নিয়মে α বাছা
  5. বাছা interaction-এর main effect আছে কি না দেখা, তারপর test set-এ একবার মাপা

কোন model “linear model” (parameter-এ linear) নয়? এখানে β-গুলো শেখার মতো সংখ্যা।

  • ŷ = β₀ + β₁x + β₂x² + β₃x³
  • ŷ = β₀ + β₁ log x + β₂ sin x
  • ŷ = β₀ · eβ₁x
  • ŷ = β₀ + β₁x + β₂(x − 4)₊, knot ৪ আগে থেকে ঠিক

Knot ০, ৪, ৮ আর ১২-এ degree-১ B-spline (hat function) basis-এ x = ৭-এর সারি কোনটি?

  • [0, 0.75, 0.25, 0]
  • [0, 0.25, 0.75, 0]
  • [0.1, 0.2, 0.6, 0.1]
  • [7, 3, 0, 0]

ŷ = 850 + 120·promo − 200·first + 180·promo·first (promo, first ০ বা ১)। প্রথমবারের ক্রেতার জন্য promo-র প্রভাব কত?

  • ১২০
  • ১৮০
  • ১০০ (১২০ − ২০০ + ১৮০)
  • ৩০০

৫-fold CV-তে সর্বনিম্ন গড় MSE ১৭.৪ (α = ০.২৫), আর সেই সারির পাঁচটি fold-এর sd ২.০৭৪. বাকি α-র গড়: α = ১.০-এ ২৩.০, ০.৫-এ ১৮.২, ০.১২-এ ১৮.০. One-SE নিয়মে কোন α?

  • ০.৫
  • ০.২৫
  • ০.১২
  • ১.০

একটি OLS-এ s = ৬, আর x₀-এ h₀ = x₀ᵀ(XᵀX)−1x₀ = ০.০১. n খুব বড়, তাই t ≈ ১.৯৬. গড়ের ৯৫% CI আর নতুন পর্যবেক্ষণের ৯৫% PI-এর অর্ধ-চওড়া প্রায় কত?

  • দুটিই প্রায় ১.১৮
  • CI ০.১২, PI ১১.৭৬
  • CI প্রায় ১.১৮, PI প্রায় ১১.৮২
  • CI প্রায় ১১.৮২, PI প্রায় ১.১৮

Promo × weekend interaction রেখে weekend-এর main effect বাদ দেওয়া একটি OLS model-এ weekend-কে ০/১-এর বদলে ১/০ কোড করা হলো। কী ঘটবে?

  • কিছুই বদলাবে না, কারণ কোডিং কেবল নাম
  • অনুমান নিজেই বদলাতে পারে, কারণ hierarchy-ছাড়া model কোডিং-নির্ভর
  • কেবল interaction-এর চিহ্ন উল্টাবে, অনুমান একই থাকবে
  • Model আর fit করা যাবে না

একই ৮০টি বিন্দুতে ৫-fold CV-তে ৩০ ডিগ্রির polynomial-এর RMSE ১৫.৬, আর ১৪টি কলামের cubic spline-এর ৩.৩৫. সবচেয়ে ভালো ব্যাখ্যা কোনটি?

  • Polynomial-এ কলাম বেশি, তাই training ডেটাও ভালো ধরতে পারে না
  • Spline non-linear model, তাই ভালো
  • Polynomial-এ standardization ছিল না
  • Polynomial basis বৈশ্বিক, তাই উঁচু ডিগ্রিতে প্রান্তে দোলে; spline স্থানীয় আর স্থির

শব্দকোষ

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

মনে রাখুন

  1. Basis expansion: ŷ = β₀ + ∑βjφj(x) parameter-এ linear; বাঁকা সম্পর্ক ধরতে φ বদলান, পদ্ধতি নয়।
  2. Basis বাছাই: polynomial বৈশ্বিক আর প্রান্তে দোলে; B-spline আর RBF স্থানীয়। কলামের সংখ্যা বা শাস্তি CV-তে; cubic spline-এ কলাম = n_knots + ২.
  3. Coefficient পড়া: রূপান্তরের এককে পড়ুন; spline-এ বক্ররেখা পড়ুন; log target-এ (eβ − ১) × ১০০%; GAM = আলাদা মসৃণ অংশের যোগফল।
  4. Interaction: b₁ মানে অন্যটি শূন্য হলে প্রভাব; b₃ = পার্থক্যের পার্থক্য; hierarchy মানুন, গড় প্রভাব চাইলে center করুন।
  5. Path আর interval: one-SE = সর্বনিম্ন + SE-র নিচে সবচেয়ে বড় α (সরল, কিন্তু অনুমানে সামান্য খারাপ হতে পারে); CI = s√h₀, PI = s√(1 + h₀), একক অর্ডারে PI আর তার coverage যাচাই।

পরের পর্বে: এখানকার prediction interval ধরে নিয়েছে noise সব জায়গায় সমান আর কোনো বড় outlier নেই। পর্ব ২৪ (Robust আর quantile regression — outlier আর অনুমানের পরিসর) দেখাবে outlier কীভাবে least squares টেনে নেয়, Huber আর RANSAC কীভাবে তা ঠেকায়, আর quantile regression কীভাবে সরাসরি ১০ম ও ৯০তম percentile শিখে অসমান ছড়ানোর ডেটায় সৎ পরিসর দেয়।

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

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