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

Dashboard — Streamlit দিয়ে model আর metric দেখানো

Dashboards — Metric Hierarchy, Streamlit Reruns, Caching, Session State and Model What-If Views

Taught in Bangla, this lesson designs a weekly operations dashboard for a fictional shop: a north star metric split into multiplicative drivers and guardrails, week-over-week changes with a simple noise band, and Streamlit's rerun model, st.cache_data, st.cache_resource and session state. A model what-if view shows split conformal ranges and input guards, and a real app is tested with AppTest.

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

  • Dashboard-এর উদ্দেশ্য, দর্শক আর refresh-এর ছন্দ ঠিক করা, আর north star, driver ও guardrail দিয়ে metric hierarchy সাজানো
  • pandas-এ metric layer: সাপ্তাহিক মাপ, সপ্তাহে সপ্তাহে বদল আর একটি সরল noise band, যাতে এলোমেলো ওঠানামাকে খবর না ভাবেন
  • Streamlit-এর rerun, widget আর layout, st.cache_data বনাম st.cache_resource, stale cache-এর ফাঁদ আর session state
  • সংরক্ষিত pipeline দিয়ে what-if, conformal পরিসর দিয়ে অনিশ্চয়তা, আর ইনপুট পাহারা
  • একটি আসল app চালিয়ে AppTest দিয়ে পরীক্ষা, আর ডেটার সতেজতা, access control ও deploy-এর বিকল্পগুলো

এই মডিউলে এ পর্যন্ত বিশ্লেষণ হয়েছে notebook-এ: SQL দিয়ে cohort (পর্ব ৬১), A/B test (পর্ব ৬২–৬৩), causal প্রশ্ন (পর্ব ৬৪) আর ক্রেতার বিশ্লেষণ (পর্ব ৬৫)। কিন্তু অপারেশন ম্যানেজার notebook খোলেন না। তাঁর দরকার এমন একটি পাতা, যা প্রতি সোমবার সকালে বলে কী ভালো চলছে, কী খারাপ, আর কোথায় হাত দিতে হবে। Chart বাছাইয়ের নিয়ম AI-এর ভিত্তি কোর্সের পর্ব ২৯-এ আর interactive chart একই কোর্সের পর্ব ৩১-এ আছে; এখানে সেগুলো ধরে নিয়ে প্রশ্ন হবে, কোন সংখ্যা দেখাবেন আর app-টি কীভাবে চলে। চারটি চালানো যায় এমন ব্লক পাতাতেই চলে (ব্রাউজারের Python); প্রথমবার scikit-learn নামতে প্রায় দশ সেকেন্ড লাগে। Streamlit পাতায় চলে না, তাই app-টি ডেস্কটপে সত্যিকারের run করে আসল আউটপুট দেওয়া হয়েছে।

১ · Dashboard কীসের জন্য — সিদ্ধান্ত, দর্শক আর ছন্দ

বাসচালকের সামনের প্যানেলে মাত্র কয়েকটি কাঁটা: গতি, তেল, ইঞ্জিনের তাপ। ইঞ্জিনের হাজারটা মাপ আছে, কিন্তু চালকের সিদ্ধান্ত (জোরে, আস্তে, থামা) এই কয়েকটিতেই চলে। গ্যারেজের মিস্ত্রির প্যানেল আলাদা, অনেক বেশি খুঁটিনাটি, কারণ তাঁর সিদ্ধান্তও আলাদা। Dashboard-ও তেমন: কার সিদ্ধান্তের জন্য, সেটিই ঠিক করে কী থাকবে।

একটি dashboard শুরুর আগে তিনটি প্রশ্ন লিখে ফেলুন। কোন সিদ্ধান্ত? যেমন “কোন জেলায় এই সপ্তাহে বাড়তি রাইডার পাঠাব।” কে দেখবেন? অপারেশন ম্যানেজার, যিনি পরিসংখ্যানবিদ নন। কত দিন পরপর? সাপ্তাহিক মিটিংয়ে; তাহলে প্রতি মিনিটে হালনাগাদের দরকার নেই, কিন্তু সোমবার সকালে আগের সপ্তাহের ডেটা পুরো থাকতে হবে। এই উত্তরগুলো থেকে তিন ধরনের dashboard আলাদা হয়।

ধরনদর্শক ও ছন্দবাজারঘরে উদাহরণ
Operational (চলতি কাজ)শিফটের সুপারভাইজার; মিনিট বা ঘণ্টায়এখন কতগুলো অর্ডার দেরিতে, কোন hub-এ জট
Analytical (বিশ্লেষণ)বিশ্লেষক, ম্যানেজার; সপ্তাহেসময়মতো পৌঁছানোর হার কেন নামল, কোন জেলায়
Strategic (কৌশল)পরিচালক; মাসে বা প্রান্তিকেNorth star-এর প্রবণতা, লক্ষ্যের তুলনায় অবস্থান

এই পর্বের app একটি সাপ্তাহিক analytical dashboard, সঙ্গে একটি model-এর what-if পাতা। Data freshness (ডেটা শেষ কবে পর্যন্ত) পাতায় সবসময় লেখা থাকবে, কারণ পুরোনো ডেটায় নেওয়া সিদ্ধান্ত ভুল ডেটায় নেওয়া সিদ্ধান্তের মতোই বিপজ্জনক।

২ · Metric hierarchy — north star, driver আর guardrail

বিশটি সমান-গুরুত্বের সংখ্যা মানে কোনোটিই গুরুত্বপূর্ণ নয়। তাই সংখ্যাগুলো তিন স্তরে সাজানো হয়। সবার ওপরে একটি north star metric: এমন একটি মাপ যা ক্রেতা মূল্য পেলে বাড়ে, আর ব্যবসাও তাতে টেকে। বাজারঘরের জন্য আমরা নিয়েছি “সপ্তাহে সময়মতো পৌঁছানো অর্ডার”। মোট বিক্রি (GMV) নিলে দেরিতে পৌঁছানো বা পরে ফেরত আসা অর্ডারও গোনা হতো; visitor নিলে কেনাকাটা না হলেও সংখ্যা বাড়ত।

তার নিচে driver metric, যেগুলো মিলে north star বানায় আর যেগুলোতে দল সরাসরি কাজ করতে পারে। গুণফলে লিখলে একটি metric tree পাওয়া যায়:

NS = visitors × conversion × (১ − cancel rate) × on-time rate

এখানে conversion = অর্ডার ÷ visitor, cancel rate = বাতিল ÷ অর্ডার, on-time rate = সময়মতো পৌঁছানো ÷ পৌঁছানো। গুণফল হওয়ায় log নিলে যোগফল হয়: north star-এর log-বদল = প্রতিটি driver-এর log-বদলের যোগ। তাই কোন driver বদলটি ঘটাল, তা সংখ্যায় আলাদা করা যায়। সবশেষে guardrail metric: যে মাপগুলো খারাপ হওয়া চলবে না, যেমন বাতিল আর ফেরতের হার। Guardrail না থাকলে একটি দল north star বাড়াতে গিয়ে অন্য জায়গায় ক্ষতি করতে পারে, যা Goodhart’s law-এর চেনা রূপ। নিচের stepper বাজারঘরের শেষ দুই সপ্তাহের আসল সংখ্যা দিয়ে hierarchy সাজায় (সংখ্যাগুলো অংশ ৩-এর ব্লকের একই ডেটা থেকে)।

দর্শক: অপারেশন ম্যানেজারছন্দ: সাপ্তাহিক

            
  1. সিদ্ধান্ত থেকে north star. প্রার্থী চারটি। GMV আর orders দেরিতে পৌঁছানো অর্ডারও গোনে, visitors কেনাকাটা ছাড়াই বাড়ে। সময়মতো পৌঁছানো অর্ডার ক্রেতার পাওয়া মূল্য আর ব্যবসা দুটিকেই ধরে।
  2. গুণফলের গাছ। সপ্তাহ ১০-এ ৮৯,৪৩০ visitor × ০.০৩১৭০ × ০.৯৪২৫০ × ০.৮৫৩৬৭ = ২,২৮১, ঠিক north star. সপ্তাহ ৯-এ একই হিসাবে ২,৩০৬।
  3. বদলের ভাগ। North star মাত্র −১.০৯% (log-এ)। ভেতরে visitor +২.৭৬, কিন্তু সময়মতো পৌঁছানো −৩.০২ আর বাতিল না-হওয়া −০.৯৭. বাড়তি visitor খারাপ খবরটি ঢেকে দিয়েছে; শুধু north star দেখলে মনে হতো সব ঠিক আছে।
  4. Guardrail. বাতিলের হার ৪.৮৩% থেকে ৫.৭৫%, ফেরতের হার ৩.৮৯% থেকে ৪.২৭%. দুটিই ৭%-এর সতর্ক-রেখার নিচে, তবু নজরে রাখার মতো।
  5. মিটিংয়ের তালিকা। যে বদল স্বাভাবিক ওঠানামার সীমা পেরোয় শুধু সেটি আলোচনায় যায় (সীমার হিসাব অংশ ৩-এ)। On-time-কে জেলায় ভাঙলে কারণ পরিষ্কার: চট্টগ্রামে ৮৭.৪% থেকে ৭৪.৩%. বাতিলের “SIGNAL” আসলে কাকতালীয়; কেন, তা অংশ ৩ দেখায়।

৩ · Metric layer pandas-এ — সাপ্তাহিক বদল আর noise band

Dashboard-এর chart-এর নিচে একটি metric layer থাকে: প্রতিটি metric-এর একটিমাত্র, লিখিত সংজ্ঞা, যা app, report আর notebook সবাই একই কোড থেকে নেয়। নইলে দুই দলের “conversion” দুই রকম হয়। নিচের module বাজারঘরের (কাল্পনিক দোকান) দশ সপ্তাহের দৈনিক হিসাব seed দিয়ে বানায়: চারটি জেলা, শুক্রবারে ভিড় বেশি, দিনে দিনে এলোমেলো ওঠানামা, আর শেষ সপ্তাহে শুধু চট্টগ্রামে দেরি বেশি। বাতিল আর ফেরতের আসল হার পুরো সময় স্থির (৫% আর ৪%). Model-এর জন্য ২,০০০টি ডেলিভারির ডেটাও এখানে।

# বাজারঘর (কাল্পনিক দোকান): seed দিয়ে বানানো দৈনিক হিসাব আর ডেলিভারির ডেটা
import numpy as np
import pandas as pd

DISTRICTS = ["Dhaka", "Chattogram", "Sylhet", "Rajshahi"]

def daily_ops(weeks=10, seed=66):
    rng = np.random.default_rng(seed)
    d = pd.MultiIndex.from_product(
        [pd.date_range("2026-08-24", periods=7 * weeks), DISTRICTS],
        names=["date", "district"]).to_frame(index=False)
    d["week"] = (d.date - d.date.min()).dt.days // 7 + 1
    size = d.district.map(dict(zip(DISTRICTS, [6000, 2500, 1200, 1300])))
    friday = np.where(d.date.dt.dayofweek == 4, 1.25, 1.0)   # শুক্রবারে ভিড় বেশি
    mood = rng.lognormal(0, 0.08, len(d))                     # দিনের এলোমেলো ওঠানামা
    d["visitors"] = rng.poisson(size * friday * (1 + 0.01 * d.week) * mood)
    d["orders"] = rng.binomial(d.visitors, 0.032)
    d["cancelled"] = rng.binomial(d.orders, 0.05)
    d["delivered"] = d.orders - d.cancelled
    late = 0.12 + 0.15 * ((d.district == "Chattogram") & (d.week == weeks))
    d["on_time"] = rng.binomial(d.delivered, 1 - late)      # শেষ সপ্তাহে চট্টগ্রামে দেরি
    d["returned"] = rng.binomial(d.delivered, 0.04)
    return d

def deliveries(n=2000, seed=66):
    rng = np.random.default_rng(seed)
    district = rng.choice(DISTRICTS, n, p=[0.5, 0.2, 0.15, 0.15])
    km = rng.uniform(1, 40, n).round(1)
    rain = rng.binomial(1, 0.3, n)
    extra = pd.Series(district).map(dict(zip(DISTRICTS, [0, 3, 6, 5]))).to_numpy()
    noise = (1 + 0.05 * km) * rng.standard_normal(n)        # দূরে গেলে অনিশ্চয়তা বাড়ে
    hours = 8 + 0.6 * km + 4 * rain + extra + noise
    X = pd.DataFrame({"km": km, "district": district, "rain": rain})
    return X, np.maximum(hours, 1).round(1)

এখন প্রশ্ন: কোন week-over-week বদল সত্যিকারের খবর? প্রতি সপ্তাহে প্রতিটি সংখ্যা একটু নড়ে। একটি সরল, বহুল-ব্যবহৃত নিয়ম: আগের সপ্তাহগুলোর বদলের মানক বিচ্যুতি (SD) মাপুন, আর তার দুই গুণকে noise band ধরুন। এই সপ্তাহের বদল সীমার বাইরে গেলে “SIGNAL”, ভেতরে থাকলে “noise”. এটি আনুষ্ঠানিক hypothesis test নয়; কারখানার control chart-এর মতো একটি সতর্কসংকেত। ব্লকটি তুলনার জন্য দুটি তাত্ত্বিক z-ও ছাপে (z-test-এর মূল কথা AI-এর ভিত্তি কোর্সের পর্ব ৩৭-এ)।

import numpy as np
import pandas as pd
from bazar_dash import daily_ops

d = daily_ops()
w = d.groupby("week")[["visitors", "orders", "delivered",
                       "on_time", "cancelled", "returned"]].sum()
m = pd.DataFrame({
    "north_star": w.on_time,                  # সময়মতো পৌঁছানো অর্ডার
    "visitors": w.visitors,                   # driver
    "conversion": w.orders / w.visitors,      # driver
    "on_time_rate": w.on_time / w.delivered,  # driver
    "cancel_rate": w.cancelled / w.orders,    # guardrail
    "return_rate": w.returned / w.delivered}) # guardrail

chg = m.pct_change() * 100                    # সপ্তাহ থেকে সপ্তাহে % বদল
last, past = chg.iloc[-1], chg.iloc[1:-1]     # শেষ সপ্তাহ বনাম আগের ৮টি বদল
band = 2 * past.std()                         # সাধারণ ওঠানামার সীমা
out = pd.DataFrame({"week10": m.iloc[-1], "wow_pct": last,
                    "noise_band": band,
                    "flag": np.where(last.abs() > band, "SIGNAL", "noise")})
pd.set_option("display.width", 70)
print(out.round(3))

# একই visitors বদলে Poisson ধরে z (দিনের ওঠানামা ভুলে গেলে)
v1, v0 = w.visitors.iloc[-1], w.visitors.iloc[-2]
print(f"visitors: {v0} -> {v1}, Poisson z = {(v1 - v0) / np.sqrt(v1 + v0):.1f}")
o1, o0 = w.on_time.iloc[-1], w.on_time.iloc[-2]
n1, n0 = w.delivered.iloc[-1], w.delivered.iloc[-2]
p = (o1 + o0) / (n1 + n0)
z = (o1 / n1 - o0 / n0) / np.sqrt(p * (1 - p) * (1 / n1 + 1 / n0))
print(f"on_time_rate: two-proportion z = {z:.1f}")

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

                 week10  wow_pct  noise_band    flag
north_star     2281.000   -1.084       8.305   noise
visitors      89430.000    2.797       4.469   noise
conversion        0.032    0.141       4.085   noise
on_time_rate      0.854   -2.972       1.893  SIGNAL
cancel_rate       0.057   19.055      12.761  SIGNAL
return_rate       0.043    9.631      21.659   noise
visitors: 86997 -> 89430, Poisson z = 5.8
on_time_rate: two-proportion z = -2.8

সময়মতো পৌঁছানোর হার −২.৯৭%, সীমা ±১.৮৯: আসল সংকেত, আর two-proportion z = −২.৮-ও তা সমর্থন করে। আমরা জানি এটি সত্যি, কারণ ডেটায় চট্টগ্রামের দেরি নিজেরাই ঢুকিয়েছি। বাতিলের হার +১৯.১%, সীমা ±১২.৮: “SIGNAL”, অথচ আসল হার পুরো সময় ৫%. এটি একটি ভুল সংকেত। দুই-SD সীমায় প্রতিটি metric-এ মোটামুটি ৫% সম্ভাবনায় এমন হয়; ছয়টি স্বাধীন metric হলে অন্তত একটি ভুল সংকেতের সম্ভাবনা ১ − ০.৯৫৬ ≈ ২৬%. এটি multiple comparisons-এর সমস্যা (পর্ব ২২)। Visitor-এর Poisson z = ৫.৮ দেখে মনে হয় বিশাল বদল, কিন্তু আসল ওঠানামার সীমার ভেতরেই (+২.৮% বনাম ±৪.৫%). কারণ দিনে দিনে ভিড়ের ওঠানামা Poisson-এর চেয়ে অনেক বেশি (overdispersion); তাত্ত্বিক সূত্র এখানে noise কম ধরে।

সীমা নিজেও অনিশ্চিত: মাত্র আটটি পুরোনো বদল থেকে SD মাপা হয়েছে, তাই সীমাটি মোটা দাগের। ছুটির সপ্তাহ বা প্রচারণা আগের বদলে থাকলে সীমা অযথা চওড়া হয়। Dashboard-এ “SIGNAL” মানে “একবার খুঁজে দেখুন”, “প্রমাণিত” নয়। আনুষ্ঠানিক প্রশ্নের (নতুন checkout কি ভালো?) উত্তর আসে পরিকল্পিত A/B test থেকে, যা পর্ব ৬২ (A/B test-এর পরিকল্পনা — power, sample size আর peeking-এর ফাঁদ) আর পর্ব ৬৩-এর বিষয়।

৪ · চারটি মূল ছবি — hierarchy-কে chart-এ সাজানো

Metric hierarchy থেকেই পাতার ক্রম আসে: ওপরে north star, তারপর driver, তারপর guardrail, আর শেষে যে ভাঙনটি কারণ দেখায়। নিচের ব্লক আগের ব্লকের m আর d ব্যবহার করে (দুটি ব্লক একই namespace ভাগ করে, তাই আগেরটি আগে চালান)। ছবির লেখা ইংরেজিতে, কারণ পাতার matplotlib বাংলা অক্ষর ঠিকমতো আঁকে না।

import matplotlib.pyplot as plt

fig, ax = plt.subplots(2, 2, figsize=(7.5, 5.6))
fig.subplots_adjust(hspace=0.5, wspace=0.45, left=0.12, top=0.92)
ax[0, 0].plot(m.index, m.north_star, "o-", color="tab:blue")
ax[0, 0].set_title("North star: on-time orders / week", fontsize=9)
for col in ["visitors", "conversion", "on_time_rate"]:
    ax[0, 1].plot(m.index, 100 * m[col] / m[col].iloc[0], label=col)
ax[0, 1].set_title("Drivers (week 1 = 100)", fontsize=9)
ax[0, 1].legend(fontsize=7)
ax[1, 0].plot(m.index, 100 * m.cancel_rate, label="cancel %")
ax[1, 0].plot(m.index, 100 * m.return_rate, label="return %")
ax[1, 0].axhline(7, color="r", ls="--", lw=1, label="alert line 7%")
ax[1, 0].set_ylim(0, 8); ax[1, 0].legend(fontsize=7)
ax[1, 0].set_title("Guardrails (should stay low)", fontsize=9)
g = d[d.week >= 9].groupby(["district", "week"])[["on_time", "delivered"]].sum()
rate = (g.on_time / g.delivered).unstack("week") * 100
rate.plot.barh(ax=ax[1, 1], color=["lightgray", "tab:orange"])
ax[1, 1].legend(["week 9", "week 10"], fontsize=7, loc="lower right")
ax[1, 1].set_xlim(70, 92); ax[1, 1].set_ylabel("")
ax[1, 1].set_title("On-time % by district: wk 9 vs 10", fontsize=9)
for a in ax.flat[:3]:
    a.set_xlabel("week", fontsize=8)
print(rate.round(1))
plt.show()

আসল আউটপুট (ব্রাউজারের Python; চারটি ছবি ব্লকের নিচে আঁকা হয়):

week          9     10
district
Chattogram  87.4  74.3
Dhaka       87.8  87.4
Rajshahi    88.1  91.7
Sylhet      89.9  90.1

ওপরে বাঁয়ে north star: সপ্তাহ ১০-এ সামান্য নামা, যা আগের ওঠানামার মধ্যেই দেখায়। ওপরে ডানে তিনটি driver একই সূচকে (সপ্তাহ ১ = ১০০), যাতে আলাদা এককের সংখ্যা একসঙ্গে তুলনা করা যায়: visitor উঠছে, on-time শেষে হঠাৎ নামছে। নিচে বাঁয়ে guardrail, y-অক্ষ শূন্য থেকে আর সতর্ক-রেখাসহ, যাতে ছোট ওঠানামা নাটকীয় না দেখায়। নিচে ডানে কারণ: চট্টগ্রাম ৮৭.৪ থেকে ৭৪.৩, বাকি তিন জেলা স্থির। ম্যানেজারের সিদ্ধান্ত এই শেষ ছবি থেকেই আসে।

কয়েকটি নকশার নিয়ম, যা উপরের ছবিতে মানা হয়েছে: (ক) প্রতিটি সংখ্যার পাশে তুলনা থাকবে (আগের সপ্তাহ, লক্ষ্য বা সতর্ক-রেখা); একা “৮৫.৪%” কিছু বলে না। (খ) একক আলাদা হলে একই অক্ষে না এঁকে সূচক বা আলাদা প্যানেল। (গ) Line chart-এ অক্ষ শূন্য থেকে না শুরু হলে চলে, bar chart-এ চলে না। (ঘ) রঙ শুধু অর্থের জন্য: এখানে কমলা মানে “এই সপ্তাহ”। (ঙ) জেলার মতো ভাঙন মূল পাতায় একটি, বাকিগুলো ছাঁকনির পেছনে। ভুল-বোঝানো chart চেনা পর্ব ৬৭ (Data storytelling — বিশ্লেষণ থেকে সিদ্ধান্ত আর report)-এর বিষয়।

৫ · Streamlit-এর মূল নিয়ম — প্রতিটি ক্লিকে পুরো script আবার চলে

Streamlit একটি open-source Python library: একটি সাধারণ .py ফাইল লিখুন, st.title, st.metric, st.line_chart ডাকুন, আর streamlit run app.py চালালে ব্রাউজারে একটি web app খোলে। HTML, JavaScript বা route লিখতে হয় না। এর দাম একটি অস্বাভাবিক নিয়ম, যাকে এই পর্বে বলছি rerun model: ব্যবহারকারী কোনো widget (slider, selectbox, বোতাম) বদলালেই পুরো script প্রথম লাইন থেকে শেষ লাইন পর্যন্ত আবার চলে। Widget ডাকলে সে তার বর্তমান মান ফেরত দেয়, যেমন weeks = st.sidebar.slider(...) লাইনটি এবার ৫ ফেরত দেবে।

বাজারঘরের app-এর নকশা (wireframe)

বড় পর্দায় sidebar বাঁয়ে থাকে, ফোনে একটি বোতামের পেছনে; জায়গার জন্য এখানে ওপরে আঁকা। সংখ্যাগুলো AppTest-এর আসল run থেকে।

Wireframe of the Bazarghar Streamlit dashboard Sidebar District All ▾ Weeks to show 8 Bazarghar operations Overview What-if North starOn-timeCancel 2,28185.4%5.7% ▼ 1.1%▼ 3.0%▲ 19.1% Signal: on-time rate, cancel rate north_star, last 8 weeks Data up to 2026-11-01, cached 10 min ১ ২ ৩ ৪
ছবি ৫.১ — (১) st.sidebar-এ ছাঁকনি; (২) st.tabs আর st.columns(3)-এ তিনটি st.metric, যেখানে বাতিলের হারে delta_color="inverse" (বাড়া খারাপ, তাই লাল); (৩) noise band পেরোনো metric-এর caption; (৪) ডেটার সতেজতা।

Layout-এর তিনটি মূল উপকরণ: st.sidebar (ছাঁকনির জায়গা), st.columns (পাশাপাশি কার্ড) আর st.tabs (একই পাতায় আলাদা দৃশ্য)। নিচের stepper দেখায়, slider ৮ থেকে ৫-এ সরালে একটি rerun-এ ঠিক কী ঘটে।

1import streamlit as st2@st.cache_data(ttl=600)3def load_daily(): ...4@st.cache_resource5def load_model(): ...6if "history" not in st.session_state:7    st.session_state.history = []8district = st.sidebar.selectbox(...)9weeks = st.sidebar.slider(..., 4, 10, 8)10d = load_daily()11m = weekly(d)12st.line_chart(m.tail(weeks))
অবস্থা
weeks
history
DB query
  1. ব্রাউজারে ক্লিক। ব্যবহারকারী slider ৮ থেকে ৫-এ সরালেন। ব্রাউজার নতুন widget মান server-কে পাঠায় আর একটি rerun চায়। Script এখনো চলেনি।
  2. ওপর থেকে শুরু। Server-এ script প্রথম লাইন থেকে আবার চলে। Decorator-গুলো function দুটি আবার সংজ্ঞায়িত করে, কিন্তু cache-এর ভাণ্ডার script-এর বাইরে থাকে, তাই আগের ফল হারায় না।
  3. Session state টিকে থাকে। history আগে থেকেই আছে, তাই খালি তালিকা দিয়ে মুছে ফেলা হয় না। সাধারণ variable হলে প্রতি rerun-এ নতুন করে শুরু হতো।
  4. Widget-এর মান। Selectbox আগের মতো “All” ফেরত দেয়, slider এবার ৫। কোডে কোথাও “slider বদলালে এই কাজ” লেখা নেই; নতুন মান নিয়ে পুরো script চলাই সেই কাজ।
  5. Cache hit. load_daily() একই argument-এ আগে চলেছে আর TTL পেরোয়নি, তাই ডেটাবেসে query যায় না; cache থেকে DataFrame-এর একটি copy আসে। weekly(d) সস্তা, তাই আবার হিসাব হয়।
  6. পাতা হালনাগাদ। Chart এবার ৫ সপ্তাহের। ব্রাউজার শুধু বদলানো element বদলায়, পুরো পাতা নয়। Run শেষ; পরের ক্লিকের অপেক্ষা। Run চলার মধ্যে আবার ক্লিক এলে Streamlit সাধারণত চলমান run থামিয়ে নতুন করে শুরু করে।

এই নিয়মের সুবিধা: কোড ওপর থেকে নিচে পড়লেই app বোঝা যায়, callback-এর জট নেই। অসুবিধা: ধীর কাজ (ডেটা আনা, model load) প্রতি ক্লিকে আবার চলবে, যদি না আপনি তা cache করেন। কয়েকটি widget একসঙ্গে বদলে একবারে পাঠাতে st.form আছে; শুধু পাতার একটি অংশ আবার চালাতে নতুন version-এ st.fragment আছে।

৬ · Caching আর session state — stale data-এর ফাঁদ

Streamlit-এ cache-এর দুটি decorator. st.cache_data function-এর ফল argument অনুযায়ী মনে রাখে, আর প্রতিবার ফলের একটি নতুন copy দেয় (ভেতরে serialize করে)। তাই DataFrame বা query-র ফলের জন্য এটি। st.cache_resource একবার object বানিয়ে সব rerun আর সব ব্যবহারকারীকে একই object দেয়, copy ছাড়া; model বা database connection-এর জন্য এটি। দুটিরই key হয় function-এর কোড আর argument থেকে; argument-এর নাম _ দিয়ে শুরু হলে সেটি key-তে গোনা হয় না।

বিপদ হলো stale cache: ডেটাবেসে নতুন অর্ডার এসেছে, কিন্তু cache পুরোনো ফলই দিয়ে যাচ্ছে, কারণ argument বদলায়নি। সমাধান time to live (TTL): ফলের একটি মেয়াদ। নিচের ব্লক Streamlit ছাড়া সাধারণ Python-এ বিষয়টি দেখায়: একটি নকল ডেটাবেস, নকল ঘড়ি (যাতে ফল সবসময় একই), আর একটি সবচেয়ে সরল cache. TTL বসানো হয়েছে একটি সময়ের key দিয়ে: t // 300 প্রতি ৩০০ সেকেন্ডে বদলায়, তাই key-ও বদলায়।

import copy

db = {"Dhaka": 120}            # নকল ডেটাবেস: আজকের অর্ডার

def cached(fn):                # সবচেয়ে সরল cache: key = argument-এর tuple
    store, stats = {}, {"miss": 0}
    def wrapper(*args):
        if args not in store:
            stats["miss"] += 1         # আসল query চলল
            store[args] = fn(*args)
        return store[args]
    wrapper.stats = stats
    return wrapper

def query(district, *ignored):
    return {"district": district, "orders": db[district]}
naive = cached(query)
with_ttl = cached(query)
TTL = 300                      # ৫ মিনিট
print("   t   db  naive  ttl_key  ttl")
for t in [0, 30, 90, 200, 330, 620]:
    if t == 90:                # t: নকল ঘড়ি (সেকেন্ড)
        db["Dhaka"] = 180      # নতুন অর্ডার এল
    a = naive("Dhaka")["orders"]
    key = t // TTL             # time-to-live key: প্রতি ৩০০ সেকেন্ডে বদলায়
    b = with_ttl("Dhaka", key)["orders"]
    print(f"{t:4} {db['Dhaka']:4} {a:6} {key:8} {b:4}")
print("real queries:", naive.stats["miss"], "vs", with_ttl.stats["miss"])

r = naive("Dhaka"); r["orders"] = -1           # এক জায়গায় ফল বদলানো হলো
print("next rerun sees:", naive("Dhaka")["orders"])
safe = lambda *a: copy.deepcopy(with_ttl(*a))   # cache_data-এর মতো copy
s = safe("Dhaka", 2); s["orders"] = -1
print("with a copy, cache keeps:", safe("Dhaka", 2)["orders"])

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

   t   db  naive  ttl_key  ttl
   0  120    120        0  120
  30  120    120        0  120
  90  180    120        0  120
 200  180    120        0  120
 330  180    120        1  180
 620  180    120        2  180
real queries: 1 vs 3
next rerun sees: -1
with a copy, cache keeps: 180

সাধারণ cache একবার query চালিয়ে চিরকাল ১২০ দেখায়, যদিও ৯০ সেকেন্ডে ডেটাবেসে ১৮০ এসেছে। TTL-key সহ cache-ও ৯০ থেকে ২০০ সেকেন্ড পর্যন্ত পুরোনো মান দেখায়, কিন্তু key বদলাতেই (৩৩০ সেকেন্ডে) নতুন query চলে; মোট তিনটি query, প্রতিটি মেয়াদে একটি। অর্থাৎ TTL পুরোনো ডেটাকে শেষ করে না, শুধু তার বয়সের একটি সীমা দেয়; তাই পাতায় “cached 10 min” লেখা উচিত। শেষ দুই লাইন দ্বিতীয় ফাঁদ: copy ছাড়া cache একই object ফেরত দেয়, তাই কোডের এক জায়গায় ফল বদলালে পরের rerun-এ সবাই −১ দেখে। Copy দিলে cache অক্ষত থাকে; st.cache_data ঠিক এ কারণেই copy দেয়, আর st.cache_resource দেয় না।

আসল st.cache_data(ttl=600) সময়ের key-এর বদলে প্রতিটি ফলের বয়স দেখে, আর মেয়াদোত্তীর্ণ ফল সরিয়ে দেয়; আমাদের সরল cache-এ পুরোনো key জমতেই থাকে। ফল সংখ্যায় সীমা দিতে max_entries আছে, আর হাতে মুছতে load_daily.clear().

Session state তৃতীয় জিনিস, cache নয়। st.session_state একজন ব্যবহারকারীর একটি browser tab-এর অভিধান: rerun-এর মধ্যে মান টিকে থাকে, কিন্তু অন্য ব্যবহারকারী তা দেখে না, আর পাতা reload করলে নতুন করে শুরু হয়। Cache সবার মধ্যে ভাগ হয়, তাই একজনের what-if-এর ইতিহাস cache-এ বা module-স্তরের সাধারণ list-এ রাখা মানে সবার ইতিহাস মিশে যাওয়া। নিচের stepper চারটি object-এর জন্য জায়গা বাছে।

প্রশ্ন: copy চাই? সবার মধ্যে ভাগ? মেয়াদ?

            
  1. DataFrame. Query-র ফল serialize করা যায়, কোডের কোথাও বদলানো হতে পারে, আর ডেটাবেসে নতুন অর্ডার আসে। তাই copy-দেওয়া st.cache_data, সঙ্গে TTL.
  2. Model. বড় object; প্রতি rerun-এ copy করা সময় আর memory-র অপচয়। সবাই একই model শুধু পড়ে (predict), বদলায় না। তাই st.cache_resource. Model-কে কেউ বদলালে (যেমন আবার fit) সেটি সবার জন্য বদলাবে, সতর্ক থাকুন।
  3. Connection. Connection serialize করা যায় না, তাই st.cache_data চলবেই না। একটি connection খুলে সবাই ব্যবহার করে; ভেঙে গেলে আবার খুলতে validate function বা TTL কাজে লাগে।
  4. ব্যক্তিগত ইতিহাস। একজনের জিনিস, অন্যদের দেখার কথা নয়। দুটি cache-ই সবার মধ্যে ভাগ হয়, তাই এখানে st.session_state.
  5. নিয়ম। ডেটা আর ফল cache_data-এ, ভারী ভাগ করা object cache_resource-এ, ব্যক্তিগত অবস্থা session_state-এ। আর যে হিসাব মিলিসেকেন্ডে হয়, তাকে cache না করাই ভালো: cache-এর key বানাতেও খরচ আছে।

৭ · Model দেখানো — what-if, পরিসর আর ইনপুট পাহারা

Dashboard-এ model আনার সবচেয়ে কাজের রূপ what-if analysis: ম্যানেজার দূরত্ব, জেলা আর বৃষ্টি বদলে দেখেন, অনুমিত ডেলিভারি সময় কত। Model আগে শেখানো হয়, একটি পুরো pipeline (encoding + model) হিসেবে ফাইলে রাখা হয় (AI-এর ভিত্তি কোর্সের পর্ব ৬৪), আর app শুধু load করে predict ডাকে। শুধু একটি সংখ্যা দেখানো বিপজ্জনক, কারণ মানুষ “৩৪.২ ঘণ্টা”-কে প্রতিশ্রুতি ভাবেন। তাই সঙ্গে পর্ব ৫৮-এর split conformal পরিসর।

নিচের ব্লক app-এর ভেতরের কাজটিই করে: pipeline শেখানো, ৫০০টি calibration অর্ডারে q̂ মাপা, আর একটি what_if function, যাকে app-এর ফর্ম ঠিক এভাবে এক সারির DataFrame দিয়ে ডাকে। শেষ দুটি ইনপুট ইচ্ছা করে অদ্ভুত।

import numpy as np
import pandas as pd
from sklearn.compose import make_column_transformer
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder
from bazar_dash import DISTRICTS, deliveries

X, y = deliveries()
tr, cal = np.split(np.random.default_rng(0).permutation(len(y)), [1500])
pre = make_column_transformer(
    (OneHotEncoder(sparse_output=False), ["district"]), remainder="passthrough")
model = make_pipeline(pre, HistGradientBoostingRegressor(random_state=0))
model.fit(X.iloc[tr], y[tr])
s = np.sort(np.abs(y[cal] - model.predict(X.iloc[cal])))
k = int(np.ceil((len(cal) + 1) * 0.9))
q_hat = s[k - 1]                                   # পর্ব ৫৮-এর split conformal
print(f"n_cal={len(cal)}  k={k}  q_hat={q_hat:.2f} h")

def what_if(km, district, rain):
    """app-এর form ঠিক এভাবেই ডাকে: এক সারির DataFrame"""
    if district not in DISTRICTS:
        return f"{district}: unknown district, no estimate"
    note = "" if 1 <= km <= 40 else "  <- outside training range!"
    row = pd.DataFrame({"km": [km], "district": [district], "rain": [rain]})
    p = model.predict(row)[0]
    return f"{p:5.1f} h  [{p - q_hat:5.1f}, {p + q_hat:5.1f}]{note}"

for args in [(5, "Dhaka", 0), (5, "Dhaka", 1), (30, "Sylhet", 1),
             (30, "Dhaka", 1), (40, "Dhaka", 0), (80, "Dhaka", 0),
             (10, "Barishal", 0)]:
    print(f"{str(args):22}", what_if(*args))

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

n_cal=500  k=451  q_hat=3.41 h
(5, 'Dhaka', 0)         10.8 h  [  7.4,  14.2]
(5, 'Dhaka', 1)         14.8 h  [ 11.4,  18.2]
(30, 'Sylhet', 1)       34.2 h  [ 30.8,  37.6]
(30, 'Dhaka', 1)        28.3 h  [ 24.9,  31.8]
(40, 'Dhaka', 0)        32.6 h  [ 29.2,  36.0]
(80, 'Dhaka', 0)        32.6 h  [ 29.2,  36.0]  <- outside training range!
(10, 'Barishal', 0)    Barishal: unknown district, no estimate

বৃষ্টি ঢাকার ৫ km-এ প্রায় ৪ ঘণ্টা যোগ করে (১০.৮ থেকে ১৪.৮), ডেটা বানানোর সময় যা ঢোকানো ছিল। পরিসর সবখানে ŷ ± ৩.৪১; ৫০০টি score-এর ৪৫১তম, ঠিক পর্ব ৫৮-এর নিয়মে। প্রস্থ সব অর্ডারে এক, যদিও ডেটায় দূরে গেলে ছড়ানো বাড়ে; দূরত্ব অনুযায়ী প্রস্থ চাইলে পর্ব ৫৮-এর CQR. ৮০ km-এ model ৪০ km-এর উত্তরই দেয় (৩২.৬), কারণ gradient boosting-এর গাছ ডেটার সীমার বাইরে সমতল থাকে (extrapolation পারে না); ডেটার নিয়মে আসল সময় হতো প্রায় ৫৬ ঘণ্টা। কোনো error আসে না, তাই সীমা-পাহারা কোডেই রাখতে হয়। বরিশাল training-এ নেই; পাহারা না থাকলে encoder error দিত বা (handle_unknown="ignore" দিলে) চুপচাপ একটি সংখ্যা।

App-এর caption-এ পরিসরের অর্থও লিখে দিন: “৯০% পরিসর, অনেক অর্ডারের গড়ে, প্রতিটি অর্ডারের নিশ্চয়তা নয়” (marginal coverage). ব্যাখ্যা দেখাতে চাইলে একটি অর্ডারের SHAP bar (পর্ব ৫৭) যোগ করা যায়, কিন্তু লিখে দিন যে এটি model কীভাবে হিসাব করল তার ব্যাখ্যা, বাস্তবে কী কারণে দেরি হয় তার প্রমাণ নয় (causal প্রশ্ন পর্ব ৬৪-এর)।

Model ফাইল নিরাপদে: joblib ভেতরে pickle ব্যবহার করে, আর অচেনা pickle ফাইল খোলা মানে তার ভেতরের যেকোনো কোড চালানো। তাই শুধু নিজের বানানো ফাইল load করুন। বাইরে থেকে আসা model-এর জন্য skops-এর skops.io ফরম্যাট খোলার আগে অচেনা type দেখায়। scikit-learn-এর version বদলালে পুরোনো ফাইল load-এ সতর্কবার্তা আসে, ফল বদলাতেও পারে; তাই ফাইলের সঙ্গে version লিখে রাখুন আর app-এর পরিবেশে একই version pin করুন (AI-এর ভিত্তি কোর্সের পর্ব ১২)।

৮ · আসল app — app.py, streamlit run আর AppTest

এবার সব জোড়া লাগানো। একটি আলাদা ফোল্ডারে uv দিয়ে ছোট project বানিয়ে আসল run করা হয়েছে। লেখার সময়ে Python 3.13.5, Streamlit 1.67.0, scikit-learn 1.9.1, pandas 3.1.1, joblib 1.6.0 (পাতার ব্রাউজারে scikit-learn 1.8.0, তাই ডেস্কটপের ফাইল পাতায় load করবেন না)। bazar_dash.py উপরের module-টিই, metrics.py-তে অংশ ৩-এর হিসাব দুটি function-এ (weekly আর wow_flags), আর train.py অংশ ৭-এর pipeline শিখিয়ে {"model", "q_hat", "sklearn"} একটি joblib ফাইলে রাখে।

$ uv init --vcs none --author-from none bazarghar-dash
$ cd bazarghar-dash
$ uv add streamlit scikit-learn pandas joblib
 …
 + streamlit==1.67.0
 + scikit-learn==1.9.1
 …
$ uv run python train.py
saved: q_hat=3.46 h, sklearn 1.9.1

app.py তিন টুকরোয়। প্রথম টুকরো: cache, session state আর sidebar.

import joblib
import pandas as pd
import streamlit as st
from bazar_dash import DISTRICTS, daily_ops
from metrics import weekly, wow_flags

st.set_page_config(page_title="Bazarghar operations", layout="wide")

@st.cache_data(ttl=600)            # ডেটা: ১০ মিনিট পর আবার আনা হবে
def load_daily():
    return daily_ops()

@st.cache_resource                 # model: একবার load, সব session-এ একই object
def load_model():
    return joblib.load("delivery_model.joblib")

if "history" not in st.session_state:
    st.session_state.history = []   # এই ব্যবহারকারীর what-if-এর তালিকা

district = st.sidebar.selectbox("District", ["All"] + DISTRICTS)
weeks = st.sidebar.slider("Weeks to show", 4, 10, 8)
d = load_daily()
if district != "All":
    d = d[d.district == district]
m = weekly(d)
f = wow_flags(m)
overview, whatif = st.tabs(["Overview", "What-if"])
with overview:
    st.title("Bazarghar operations")
    ns, rate, cancel = st.columns(3)
    ns.metric("On-time orders (north star)", f"{m.north_star.iloc[-1]:,}",
              f"{f.wow_pct.north_star:+.1f}%")
    rate.metric("On-time rate", f"{m.on_time_rate.iloc[-1]:.1%}",
                f"{f.wow_pct.on_time_rate:+.1f}%")
    cancel.metric("Cancel rate (guardrail)", f"{m.cancel_rate.iloc[-1]:.1%}",
                  f"{f.wow_pct.cancel_rate:+.1f}%", delta_color="inverse")
    signals = ", ".join(f.index[f.flag == "SIGNAL"]) or "none"
    st.caption(f"Week {m.index[-1]} vs {m.index[-2]}. Changes beyond the "
               f"usual weekly noise: {signals}")
    st.line_chart(m.tail(weeks)[["north_star"]])
    st.caption(f"Data up to {load_daily().date.max():%Y-%m-%d}, cached 10 min")
with whatif:
    art = load_model()
    with st.form("what_if"):
        km = st.number_input("Distance (km)", 1.0, 40.0, 10.0)
        dist = st.selectbox("Delivery district", DISTRICTS)
        rain = st.checkbox("Raining")
        go = st.form_submit_button("Estimate")
    if go:
        row = pd.DataFrame({"km": [km], "district": [dist], "rain": [int(rain)]})
        p = float(art["model"].predict(row)[0])
        lo, hi = p - art["q_hat"], p + art["q_hat"]
        st.session_state.history.append((km, dist, rain, round(p, 1)))
        st.metric("Estimated delivery time", f"{p:.1f} h")
        st.caption(f"90% range {lo:.1f} to {hi:.1f} h (split conformal; "
                   "holds on average over orders, not for each one)")
    st.write(f"Estimates this session: {len(st.session_state.history)}")

ইনপুট পাহারা এখানে widget নিজেই দেয়: number_input-এর সীমা ১ থেকে ৪০, আর selectbox-এ শুধু চেনা চারটি জেলা। Form-এর কারণে তিনটি ইনপুট বদলালেও rerun হয় একবার, “Estimate” চাপলে। চালানো:

সত্যিকারের run (ডেস্কটপ; server শুধু নিজের যন্ত্রে, একটি অপ্রচলিত port-এ, পরে process ID ধরে বন্ধ করা হয়েছে):

$ uv run streamlit run app.py --server.address 127.0.0.1 --server.port 18566 --server.headless true

  You can now view your Streamlit app in your browser.

  URL: http://127.0.0.1:18566

$ curl http://127.0.0.1:18566/_stcore/health
ok

ব্রাউজারে ঠিকানাটি খুললে ছবি ৫.১-এর মতো পাতা আসে: বাঁয়ে sidebar-এ দুটি ছাঁকনি, ওপরে দুটি tab, তিনটি metric কার্ড (বাতিলের হারের তীর লাল, কারণ delta_color="inverse"), সংকেতের caption, chart আর ডেটার তারিখ। এই পাতায় ছবির বদলে ওই wireframe রাখা হয়েছে।

ক্লিক করে দেখা যাচাই নয়। AppTest (streamlit.testing.v1) browser ছাড়াই app চালায়, widget-এ মান বসায় আর ফলের element পড়ে। নিচের পরীক্ষা জেলা বদলায়, slider সরায়, তারপর ফর্মে দুটি what-if চালায়।

from streamlit.testing.v1 import AppTest

at = AppTest.from_file("app.py", default_timeout=30).run()
show("first run, District=All")         # সব metric আর caption ছাপে
at.sidebar.selectbox[0].select("Chattogram").run()
show("District=Chattogram")
at.sidebar.slider[0].set_value(5).run()
at.number_input[0].set_value(30.0)
at.main.selectbox[0].select("Sylhet")    # ফর্মের selectbox
at.checkbox[0].check()
at.button[0].click().run()               # form submit = একটি rerun
show("what-if 30 km, Sylhet, rain")
# … একইভাবে 5 km, Dhaka, বৃষ্টি ছাড়া
print("history:", at.session_state.history)
print("exceptions:", len(at.exception))

সত্যিকারের run (uv run python test_app.py; শুরুতে Streamlit-এর একটি “bare mode” সতর্কবার্তা আসে, যা এখানে বাদ দেওয়া হয়েছে):

streamlit 1.67.0
--- first run, District=All
On-time orders (north star): 2,281 (-1.1%)
On-time rate: 85.4% (-3.0%)
Cancel rate (guardrail): 5.7% (+19.1%)
caption: Week 10 vs 9. Changes beyond the usual weekly noise: on_time_rate, cancel_rate
caption: Data up to 2026-11-01, cached 10 min
--- District=Chattogram
On-time orders (north star): 427 (-14.6%)
On-time rate: 74.3% (-15.0%)
Cancel rate (guardrail): 7.7% (+77.2%)
…
slider now: 5
--- what-if 30 km, Sylhet, rain
…
Estimated delivery time: 34.3 h ()
caption: 90% range 30.9 to 37.8 h (split conformal; holds on average over orders, not for each one)
Estimates this session: 1
--- what-if 5 km, Dhaka, dry
…
Estimated delivery time: 10.9 h ()
caption: 90% range 7.4 to 14.3 h (split conformal; holds on average over orders, not for each one)
history: [(30.0, 'Sylhet', True, 34.3), (5.0, 'Dhaka', False, 10.9)]
exceptions: 0

প্রথম run-এর সংখ্যা পাতার ব্লকের সঙ্গে হুবহু মেলে (২,২৮১, −১.১%, ৮৫.৪%), কারণ app আর ব্লক একই metric layer ব্যবহার করে। চট্টগ্রামে north star −১৪.৬%. বাতিলের হার এখানে +৭৭%: ছোট জেলায় সংখ্যা কম, তাই হার বেশি লাফায়; জেলা-ভিত্তিক সীমাও সেই জেলার নিজের ইতিহাস থেকে আসে। What-if এখানে ৩৪.৩ আর ১০.৯ ঘণ্টা দেয়, অংশ ৭-এর পাতার ব্লক দেয় ৩৪.২ আর ১০.৮; q̂-ও ৩.৪৬ বনাম ৩.৪১. একই ডেটা আর একই seed, কিন্তু ডেস্কটপে scikit-learn 1.9.1, পাতায় 1.8.0, আর প্ল্যাটফর্মও আলাদা; এই একটি run-এ HistGradientBoosting-এর ফলে এটুকু ফারাক এসেছে। Model ফাইলের সঙ্গে version লিখে রাখার কারণ ঠিক এটি। history-তে দুটি অনুমান, session state-এ জমা। একটি শিক্ষা এই run থেকেই: প্রথম চেষ্টায় আমরা ফর্মের selectbox সূচক দিয়ে (at.selectbox[1]) ধরতে গিয়ে sidebar-এর জেলা বদলে ফেলেছিলাম; at.sidebar আর at.main দিয়ে ধরলে এ ভুল হয় না।

৯ · সতেজতা, access control আর deploy

সতেজতা। প্রতিটি পাতায় ডেটার শেষ তারিখ আর cache-এর মেয়াদ লিখুন। রাতের ETL ব্যর্থ হলে dashboard আগের দিনের সংখ্যা দেখাবে, কোনো error ছাড়া; তাই “ডেটা ৩৬ ঘণ্টার বেশি পুরোনো” হলে পাতার ওপরে সতর্কবার্তা দেখানো ভালো অভ্যাস। ডেটার পাইপলাইন আর তার নজরদারি পর্ব ৬৯ (ML system-এর test আর নজরদারি)-এর বিষয়।

Access control. অর্ডার, ক্রেতার জেলা আর model-এর ফল ব্যবসার গোপন তথ্য। Streamlit app নিজে থেকে কাউকে আটকায় না। বিকল্পগুলো: সংস্থার login-এর পেছনে reverse proxy (Cloud & DevOps কোর্সের nginx পর্ব), hosting সেবার নিজস্ব অনুমতি-ব্যবস্থা, আর নতুন version-এ OpenID Connect দিয়ে login-এর built-in সুবিধা (st.login). গোপন তথ্য (ডেটাবেসের পাসওয়ার্ড) কোডে নয়, st.secrets বা environment variable-এ; উদাহরণে শুধু example-secret-এর মতো নকল মান।

Deployment-এর বিকল্প (দাম আর সীমা প্রায়ই বদলায়, তাই সিদ্ধান্তের আগে নিজে দেখে নিন):

পথকেমনকী মাথায় রাখবেন
Streamlit-এর নিজস্ব community hostingGit repository থেকে সরাসরি; শেখা আর ডেমোর জন্য সহজসম্পদ সীমিত; কে দেখবে তার নিয়ম আর গোপন ডেটা রাখা যাবে কি না যাচাই করুন
Model-hub বা data platform-এর app hostingকিছু model hub আর data warehouse Streamlit app চালানোর ব্যবস্থা রাখেসেবাভেদে ধরন আলাদা, সময়ের সঙ্গে বদলায়; ডেটা যেখানে, app সেখানে রাখা সুবিধাজনক
নিজের server বা containerDocker image, reverse proxy, সংস্থার loginসবচেয়ে নিয়ন্ত্রণ, সবচেয়ে বেশি দায়িত্ব: update, নজরদারি, নিরাপত্তা (Full-Stack কোর্সের deployment পর্ব)

stlite একটি community প্রকল্প, যা Pyodide-এর সাহায্যে Streamlit app পুরোপুরি ব্রাউজারে চালায়, এই কোর্সের ব্লকগুলোর মতোই। Server লাগে না, তাই ছোট ডেমো বা স্থির HTML পাতায় app বসানোর জন্য আকর্ষণীয়। তবে সব package চলে না, প্রথম load ধীর, আর ডেটা ও কোড ব্যবহারকারীর ব্রাউজারে যায়, তাই গোপন ডেটা বা access control-এর কাজে এটি নয়। প্রকল্পটি সক্রিয়ভাবে বদলায়; ব্যবহারের আগে তার বর্তমান docs দেখে নিন।

আরও গভীরে: Streamlit-এর নিয়মটি GUI প্রোগ্রামিংয়ের “immediate mode” ধারণার আত্মীয়: প্রতি frame-এ পুরো ইন্টারফেস কোড থেকে আবার আঁকা, অবস্থা আলাদা রাখা। Widget-এর পরিচয় তার ধরন, label আর key থেকে আসে; একই label-এর দুটি widget-এ তাই আলাদা key লাগে। Cache-এর key-তে function-এর কোড থাকে বলে কোড বদলালে cache নিজে থেকেই অবৈধ হয়। এই version-এর st.cache_data-তে scope নামে একটি parameter-ও আছে (default "global"); নতুন parameter এলে নিজের version-এর API reference দেখুন।

পরীক্ষা ও ইন্টারভিউতে: (১) Streamlit-এ slider সরালে কী চলে? পুরো script ওপর থেকে আবার; widget নতুন মান ফেরত দেয়। (২) Model কোন cache-এ? st.cache_resource, কারণ বড়, ভাগ করা, শুধু পড়া হয়; DataFrame st.cache_data-এ, TTL দিয়ে। (৩) আগের ৮টি সাপ্তাহিক বদলের SD ১.৫%, এ সপ্তাহে −৪.০%: সংকেত? সীমা ±৩.০%, তাই হ্যাঁ। (৪) ৬টি স্বাধীন metric, প্রতিটিতে ৫% ভুল সংকেত: অন্তত একটি? ১ − ০.৯৫৬ ≈ ০.২৬. (৫) Visitor ১,০০,০০০, conversion ৩%, বাতিল ৫%, on-time ৯০%: north star? ১,০০,০০০ × ০.০৩ × ০.৯৫ × ০.৯ = ২,৫৬৫.

১০ · AI-এর যুগে — dashboard কোড যাচাই

AI-কে একটি Streamlit dashboard বানাতে বললে AI assistant দ্রুত চলনসই app দেয়। ভুলগুলো সাধারণত চুপচাপ থাকে, আর ধরা পড়ে অনেক ব্যবহারকারী আসার পর।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
পুরোনো st.cache বা st.experimental_memo লেখেStreamlit 1.67.0-এ এই নামগুলো নেইনিজের version-এর API reference; st.cache_data বা st.cache_resource
ডেটা query-তে TTL ছাড়া cacheServer চলা পর্যন্ত পুরোনো সংখ্যাডেটাবেসে একটি সারি বদলে পাতায় কখন আসে দেখুন
DataFrame st.cache_resource-এ, পরে কোডে বদলায়সব ব্যবহারকারীর ডেটা নষ্টডেটায় cache_data; বদলানোর আগে copy
ব্যবহারকারীর ইতিহাস module-স্তরের list-এসব ব্যবহারকারীর তথ্য মিশে যায়st.session_state; দুটি browser tab খুলে পরীক্ষা
প্রতি rerun-এ model আবার fitধীর, আর প্রতিবার একটু আলাদা modelআলাদা train.py; app শুধু load করে
সংখ্যার পাশে তুলনা বা noise-এর ধারণা নেই; “+১৯% বাতিল!” লাল করে দেখায়এলোমেলো ওঠানামায় মিটিং চলেNoise band, অনেক metric-এর ভুল সংকেতের হিসাব
পাসওয়ার্ড কোডে লেখেRepository-তে গোপন তথ্য ফাঁসst.secrets বা environment variable

AI-কে কোডের সঙ্গে নকশার যুক্তি আর পরীক্ষাও চাইতে বলুন:

আমার কাছে দৈনিক অর্ডারের টেবিল: date, district, visitors, orders, cancelled, delivered, on_time. সাপ্তাহিক অপারেশন মিটিংয়ের জন্য একটি Streamlit dashboard চাই। ১. আগে metric hierarchy প্রস্তাব করো: একটি north star, তার driver (গুণফলে), আর guardrail; প্রতিটির সংজ্ঞা একটি আলাদা metrics.py-তে। ২. প্রতিটি metric-এর সাপ্তাহিক বদলের পাশে আগের সপ্তাহগুলোর বদলের ২×SD সীমা দাও, আর অনেক metric-এ ভুল সংকেতের সমস্যাটি এক লাইনে লেখো। ৩. Streamlit-এর কোন version ধরে লিখছ বলো। ডেটায় st.cache_data(ttl=…), model-এ st.cache_resource, ব্যক্তিগত অবস্থায় st.session_state, প্রতিটির কারণসহ। ৪. পাতায় ডেটার শেষ তারিখ দেখাও। ৫. streamlit.testing.v1.AppTest দিয়ে একটি পরীক্ষা লেখো, যা ছাঁকনি বদলে metric-এর মান যাচাই করে। ৬. কোনো পাসওয়ার্ড কোডে রেখো না।

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

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

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

প্র ০১ব্যবস্থাপনার জন্য “এক পাতায় মূল সংখ্যা” দেখানোর ধারণা কয়েক দশকের পুরোনো। তবু অনেক dashboard বানানোর কয়েক মাস পর আর কেউ খোলেন না। ইতিহাস থেকে এর কী শিক্ষা?

আশির দশকে বড় প্রতিষ্ঠানে executive information system নামে ব্যবস্থা চালু হয়, যা পরিচালকদের জন্য মূল সংখ্যা এক জায়গায় আনত। ১৯৯২ সালে Robert Kaplan আর David Norton Harvard Business Review-এ Balanced Scorecard প্রস্তাব করেন: শুধু আর্থিক সংখ্যা নয়, ক্রেতা, ভেতরের প্রক্রিয়া আর শেখা-বাড়ার মাপও পাশাপাশি দেখা। এটি আজকের driver আর guardrail-এর পূর্বসূরি বলা যায়: একটি সংখ্যা বাড়াতে গিয়ে অন্যগুলোর ক্ষতি যেন চোখে পড়ে। ২০০৬ সালে Stephen Few-এর বই Information Dashboard Design dashboard-কে সংজ্ঞায়িত করে এক পর্দায় দেখা যায় এমন, লক্ষ্যভিত্তিক তথ্য হিসেবে, আর অপ্রয়োজনীয় সাজসজ্জার কড়া সমালোচনা করে।

অব্যবহৃত dashboard-এর কারণ নিয়ে বিশ্লেষকদের মধ্যে কিছু চেনা ব্যাখ্যা ঘোরে, যদিও নির্ভরযোগ্য পরিমাপ কম। প্রথমত, কোনো নির্দিষ্ট সিদ্ধান্তের সঙ্গে যুক্ত না থাকা: “সব দেখা যাক” ভেবে বানানো পাতায় কোন সংখ্যা দেখে কী করতে হবে, কেউ জানে না। দ্বিতীয়ত, বিশ্বাস হারানো: দুটি পাতায় একই metric-এর দুটি মান, বা পুরোনো ডেটা একবার ধরা পড়লে মানুষ আবার spreadsheet-এ ফেরেন। তৃতীয়ত, শব্দের ভিড়: প্রতি সপ্তাহে দশটি লাল তীর, যার বেশিরভাগ এলোমেলো ওঠানামা।

তাই এই পর্বের নকশা তিনটি প্রতিরোধ রাখে: শুরুতে সিদ্ধান্ত আর দর্শক লিখে নেওয়া, একটি metric layer (এক সংজ্ঞা, এক কোড), আর noise band. চতুর্থ অভ্যাসও কাজের: কয়েক মাস পরপর দেখা, কোন পাতা কেউ খোলেন না, আর সেটি সরিয়ে দেওয়া।

মূল উপলব্ধি: Dashboard টেকে বিশ্বাস আর সিদ্ধান্তের সংযোগে; chart-এর সংখ্যা বা সৌন্দর্যে নয়।

প্র ০২বাজারঘর “সময়মতো পৌঁছানো অর্ডার”-কে north star করল আর রাইডারদের বোনাস এর সঙ্গে জুড়ে দিল। কী কী ভুল হতে পারে, আর guardrail কীভাবে বাঁচায়?

অর্থনীতিবিদ Charles Goodhart ১৯৭৫ সালে মুদ্রানীতির প্রসঙ্গে লক্ষ করেন, কোনো পরিসংখ্যানকে নিয়ন্ত্রণের লক্ষ্য বানালে তার পুরোনো সম্পর্ক ভেঙে যায়। পরে এটি জনপ্রিয় হয় এই রূপে: “মাপ লক্ষ্য হয়ে গেলে ভালো মাপ থাকে না।” প্রায় একই সময়ে সমাজবিজ্ঞানী Donald Campbell লেখেন, সামাজিক সিদ্ধান্তে কোনো সংখ্যার ব্যবহার যত বাড়ে, সেটি তত বিকৃতির চাপে পড়ে।

বাজারঘরে এর রূপ সহজেই কল্পনা করা যায়। “সময়মতো” কে ঠিক করে? যদি রাইডার নিজে অ্যাপে “পৌঁছেছে” চাপেন, তবে বাসার কাছে গিয়েই চাপার লোভ থাকবে। যদি প্রতিশ্রুত সময় সিস্টেম ঠিক করে, তবে কেউ প্রতিশ্রুত সময়টাই লম্বা করে দিতে পারেন: on-time হার বাড়বে, ক্রেতার অভিজ্ঞতা খারাপ হবে। দূরের বা কঠিন এলাকার অর্ডার এড়িয়ে যাওয়ার চাপও তৈরি হতে পারে, যা বাতিলের হার বাড়াবে।

Guardrail এখানে সতর্কঘণ্টা। বাতিলের হার, ফেরতের হার, ক্রেতার অভিযোগ আর প্রতিশ্রুত সময়ের গড় দৈর্ঘ্য যদি guardrail হয়, তবে উপরের প্রতিটি কৌশল কোথাও না কোথাও দাগ রাখে। তবু guardrail-ও খেলা যায়; তাই মাঝে মাঝে নমুনা যাচাই (কিছু ডেলিভারির GPS সময় মিলিয়ে দেখা) আর সংজ্ঞা লিখিত রাখা দরকার। বোনাস একটি সংখ্যার সঙ্গে না জুড়ে কয়েকটির সমন্বয়ে জোড়া, আর সংজ্ঞা বদলালে আগাম জানানো, এগুলোও প্রচলিত প্রতিরোধ।

মূল উপলব্ধি: North star যত গুরুত্বপূর্ণ, তাকে খেলার চাপ তত বেশি; guardrail আর লিখিত সংজ্ঞা ছাড়া সে দ্রুত অর্থ হারায়।

প্র ০৩কোড দিয়ে বানানো dashboard (Streamlit, Dash, Shiny) আর ক্লিক করে বানানো BI টুল (Power BI, Tableau, Metabase-এর মতো) — কোনটি কখন? এ নিয়ে দলগুলোর মধ্যে মতভেদ কোথায়?

দুই ধারার শিকড় আলাদা। BI টুল বহু বছর ধরে ব্যবসা-বিশ্লেষকদের জন্য বানানো: ডেটাবেসে যুক্ত হন, টেনে এনে chart বানান, অনুমতি আর নির্ধারিত হালনাগাদ ভেতরেই থাকে। কোডভিত্তিক ধারা এসেছে বিজ্ঞানী আর data scientist-দের দিক থেকে: R-এর Shiny প্রায় ২০১২ সালে, Plotly-র Dash ২০১৭ সালের দিকে, আর Streamlit ২০১৯ সালে উন্মুক্ত হয়; Snowflake ২০২২ সালে Streamlit কিনে নেয়।

BI টুলের পক্ষে যুক্তি: বিশ্লেষক নিজেই পাতা বদলাতে পারেন, প্রতিষ্ঠানের login আর ভূমিকাভিত্তিক অনুমতি তৈরি থাকে, আর অনেক ব্যবহারকারীর জন্য পরীক্ষিত। কোডের পক্ষে যুক্তি: Python-এর পুরো শক্তি (model, conformal পরিসর, যেকোনো হিসাব), version control, code review আর AppTest-এর মতো পরীক্ষা। What-if-এর মতো model-চালিত পাতা BI টুলে প্রায়ই কষ্টকর।

মতভেদ মূলত রক্ষণাবেক্ষণ নিয়ে। কোডের পক্ষের লোকেরা বলেন, ক্লিকে বানানো পাতার যুক্তি লুকিয়ে থাকে, পর্যালোচনা কঠিন। BI-এর পক্ষের লোকেরা বলেন, একজন data scientist-এর বানানো Streamlit app তিনি চলে গেলে অনাথ হয়ে পড়ে, আর নিরাপত্তা নিজেকে সামলাতে হয়। বাস্তবে অনেক দল দুটিই রাখে: নিয়মিত ব্যবসার সংখ্যা BI টুলে, model-ভিত্তিক বা পরীক্ষামূলক পাতা কোডে। মূল কথা হলো metric layer এক রাখা, যাতে দুই জায়গায় একই সংজ্ঞা চলে। কোন টুল “সেরা”, তার সাধারণ উত্তর নেই; দলের দক্ষতা, ডেটা কোথায় আছে আর কে পাতা রক্ষণাবেক্ষণ করবেন, সেটিই ঠিক করে।

মূল উপলব্ধি: টুল বাছাই রক্ষণাবেক্ষণের প্রশ্ন; সংজ্ঞা এক জায়গায় থাকলে দুটি ধারা একসঙ্গে চলতে পারে।

প্র ০৪বেশিরভাগ web framework callback-এ চলে (“এই বোতাম চাপলে এই function”). Streamlit পুরো script আবার চালায়। এই নকশার পেছনের যুক্তি কী, আর কোথায় এর দাম দিতে হয়?

প্রচলিত GUI আর web framework-এ অবস্থা থাকে object-এর ভেতরে, আর প্রতিটি ঘটনার জন্য আলাদা callback লেখা হয়। অ্যাপ বড় হলে কোন callback কোন অবস্থা বদলায়, তা মেলানো কঠিন হয়। গেম-প্রোগ্রামিংয়ে এর একটি বিকল্প ধারা জনপ্রিয় হয়েছিল, যাকে “immediate mode GUI” বলে; ২০০৫ সালের দিকে Casey Muratori-র একটি আলোচনা এটি ছড়াতে ভূমিকা রাখে বলে ধরা হয়। সেখানে প্রতিটি frame-এ পুরো ইন্টারফেস কোড থেকে আবার আঁকা হয়, আর বোতাম ডাকলে সে বলে দেয় এখনই চাপা হয়েছে কি না।

Streamlit একই ধারণা web-এ আনে, notebook-এর মতো ওপর-থেকে-নিচে script-এর রূপে। লাভ স্পষ্ট: data scientist যেভাবে notebook লেখেন, প্রায় সেভাবেই app লেখেন; কোডের ক্রম আর পাতার ক্রম এক; লুকানো callback নেই। React-এর মতো আধুনিক UI library-ও “অবস্থা থেকে পুরো UI আবার হিসাব” ভাবনা নেয়, যদিও তারা খুঁটিনাটিতে অনেক বেশি নিয়ন্ত্রণ রাখে।

দাম তিন জায়গায়। প্রথম, গতি: প্রতিটি ক্লিকে সব কোড চলে, তাই cache ভুল হলে app ধীর, আর cache বেশি হলে ডেটা পুরোনো। দ্বিতীয়, অবস্থা: সাধারণ variable rerun-এ মুছে যায়, তাই session state লাগে, আর widget-এর key নিয়ে সূক্ষ্ম ভুল হয়। তৃতীয়, বড় app: অনেক পাতা আর অনেক ব্যবহারকারীতে পুরো rerun খরচ বাড়ায়। Streamlit তাই form, fragment আর callback (on_change) যোগ করেছে, অর্থাৎ বিশুদ্ধ নিয়ম থেকে কিছুটা সরে এসেছে।

মূল উপলব্ধি: Rerun-এর নিয়ম লেখা সহজ করে, চালানো নয়; caching আর session state-ই সেই দামের হিসাব।

প্র ০৫“আগের বদলের দুই SD পেরোলে SIGNAL” নিয়মটি কোথা থেকে এসেছে? এর সীমা কী, আর অনেক metric একসঙ্গে দেখলে কী করবেন?

ধারণাটির শিকড় কারখানায়। ১৯২৪ সালে Bell Telephone Laboratories-এ Walter Shewhart একটি স্মারকে প্রথম control chart-এর রূপ দেন: একটি মাপ সময়ের সঙ্গে আঁকা, আর তার স্বাভাবিক ওঠানামার ওপর-নিচের সীমা। Shewhart দুই ধরনের বৈচিত্র্য আলাদা করেন: সাধারণ কারণ (প্রক্রিয়ার স্বাভাবিক শব্দ) আর বিশেষ কারণ (কিছু একটা সত্যিই বদলেছে)। তাঁর প্রচলিত সীমা ছিল গড় ± ৩ SD. ১৯৫৬ সালে Western Electric-এর মান-নিয়ন্ত্রণ হ্যান্ডবুক আরও কিছু নিয়ম যোগ করে, যেমন পরপর কয়েকটি বিন্দু একই দিকে থাকলেও সংকেত।

এই পর্বের নিয়ম তার একটি হালকা রূপ: সীমা ২ SD, আর মাপা হয় সাপ্তাহিক বদলের ওপর। ২ SD বেছে নেওয়ায় সংকেত তাড়াতাড়ি আসে, কিন্তু ভুল সংকেতও বেশি: বদল প্রায় normal হলে প্রতিটি metric-এ প্রায় ৫%. ছয়টি metric-এ অন্তত একটি ভুল সংকেতের সম্ভাবনা প্রায় ২৬%, দশটিতে প্রায় ৪০%. বাজারঘরের বাতিলের হারের “SIGNAL” ঠিক এমন একটি। সীমা আটটি পুরোনো বদল থেকে মাপায় নিজেই অনিশ্চিত, আর মৌসুম বা উৎসব থাকলে স্বাভাবিক ওঠানামাই বদলায়।

প্রতিকার কয়েকটি। মূল সিদ্ধান্তের metric অল্প রাখুন, আর তাদের জন্য কড়া সীমা (৩ SD) বা পরপর দুই সপ্তাহ সীমা পেরোনোর শর্ত। অনেক metric হলে Holm বা Benjamini–Hochberg-এর মতো সংশোধন (পর্ব ২২)। আগের বছরের একই সপ্তাহের সঙ্গে তুলনা মৌসুম সামলায়। আর সবচেয়ে গুরুত্বপূর্ণ: সংকেতকে প্রশ্ন হিসেবে নিন, জেলা বা পণ্যে ভেঙে কারণ খুঁজুন; কারণ না মিললে পরের সপ্তাহে আবার দেখুন।

মূল উপলব্ধি: Noise band একটি সতর্কসংকেত, প্রমাণ নয়; অনেক সংকেত একসঙ্গে দেখলে কিছু ভুল আসবেই।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। চাইলে পাতার ব্লকগুলোতে সংখ্যা বদলে যাচাই করুন।

  1. পূর্ণ numerical: একটি দোকানের দুই সপ্তাহ। সপ্তাহ A: visitor ৫০,০০০, অর্ডার ১,৫০০, পৌঁছানো ১,৪২৫, সময়মতো ১,২৫৪. সপ্তাহ B: ৫২,০০০, ১,৫০৮, ১,৪৪০, ১,২১০. (ক) দুই সপ্তাহের conversion, পৌঁছানোর অনুপাত, on-time হার আর north star (সময়মতো অর্ডার) বের করুন। (খ) North star-এর log-বদলকে চারটি driver-এ ভাগ করুন। (গ) আগের সপ্তাহগুলোর on-time হারের সাপ্তাহিক বদলের SD ১.৮% হলে এ সপ্তাহের বদল কি সংকেত?

    (ক) A: conversion ১,৫০০/৫০,০০০ = ০.০৩০০, পৌঁছানো ১,৪২৫/১,৫০০ = ০.৯৫০০, on-time ১,২৫৪/১,৪২৫ = ০.৮৮০০, north star ১,২৫৪. B: ০.০২৯০, ০.৯৫৪৯, ০.৮৪০৩, north star ১,২১০; বদল −৩.৫১%.

    (খ) ১০০ × ln(নতুন/পুরোনো): visitor +৩.৯২, conversion −৩.৩৯, পৌঁছানো +০.৫২, on-time −৪.৬২; যোগ −৩.৫৭ = ১০০ × ln(১,২১০/১,২৫৪)। Visitor বাড়লেও conversion আর on-time দুটিই নেমেছে; on-time সবচেয়ে বড় কারণ।

    (গ) On-time-এর শতকরা বদল ০.৮৪০৩/০.৮৮ − ১ = −৪.৫%; সীমা ২ × ১.৮ = ±৩.৬%. সীমার বাইরে, তাই সংকেত; জেলা বা hub-এ ভেঙে কারণ খুঁজুন।

  2. সমালোচনা ও নকশা: একজন সহকর্মী নিচের app লিখেছেন। অন্তত পাঁচটি সমস্যা খুঁজুন আর প্রতিটির প্রতিকার লিখুন।
    import streamlit as st, pandas as pd, joblib
    from sklearn.ensemble import HistGradientBoostingRegressor
    DB_PASSWORD = "example-secret"
    history = []
    
    @st.cache_resource
    def orders():
        return pd.read_sql("select * from orders", f"postgresql://u:{DB_PASSWORD}@db/shop")
    
    df = orders()
    df["fee"] = df["fee"].fillna(0)
    model = HistGradientBoostingRegressor().fit(df[["km"]], df["hours"])
    km = st.slider("km", 0, 500, 10)
    history.append(km)
    st.metric("Hours", model.predict([[km]])[0])
    st.metric("Orders this week", len(df), "+19%")

    (১) পাসওয়ার্ড কোডে (নকল হলেও অভ্যাসটি বিপজ্জনক): st.secrets বা environment variable. (২) DataFrame st.cache_resource-এ, TTL ছাড়া, আর তারপর fillna দিয়ে সেই ভাগ করা object-ই বদলানো হচ্ছে: সব ব্যবহারকারীর ডেটা বদলায় আর কখনো হালনাগাদ হয় না। প্রতিকার @st.cache_data(ttl=600). (৩) প্রতি rerun-এ model আবার fit: ধীর, আর ভাগ করা ডেটার ওপর। আলাদা train.py, app-এ st.cache_resource দিয়ে load. (৪) history module-স্তরের list: Streamlit প্রতি rerun-এ script আবার চালায়, তাই সাধারণত খালি হয়ে যায়, আর module-স্তরে থাকলে ব্যবহারকারীদের মধ্যে মিশতে পারে; st.session_state. (৫) Slider ০–৫০০ km, অথচ model training ডেটার সীমার বাইরে সমতল; সীমা ডেটার পরিসরে, আর পরিসর বা অনিশ্চয়তা নেই। (৬) “+১৯%” লেখা, কিসের তুলনায় আর noise-এর সীমা কত, কিছুই নেই; ডেটার তারিখও নেই। (৭) random_state নেই, model প্রতিবার একটু আলাদা।

  3. আউটপুট অনুমান করুন: চালানোর আগে ছয়টি লাইন লিখুন।
    store = {}
    def get(day, now, ttl=60):
        key = (day, now // ttl)
        if key not in store:
            store[key] = f"query@{now}"
        return store[key]
    for now in [0, 59, 60, 61, 130]:
        print(now, get("mon", now))
    print(len(store))
    0 query@0
    59 query@0
    60 query@60
    61 query@60
    130 query@130
    3

    Key-র দ্বিতীয় অংশ now // 60: ০ আর ৫৯-এ ০, ৬০ আর ৬১-এ ১, ১৩০-এ ২. তাই query চলে তিনবার (০, ৬০, ১৩০ সেকেন্ডে), আর ৫৯ সেকেন্ডে ০ সেকেন্ডের পুরোনো ফল আসে। Store-এ তিনটি key জমে; সত্যিকারের cache-এ পুরোনো key সরানোর ব্যবস্থা না থাকলে memory বাড়তেই থাকে।

  4. কোড লিখুন: একটি function wow_flag(values, k=2) লিখুন, যা সাপ্তাহিক মানের একটি list নেয়, শেষ সপ্তাহের শতকরা বদল আর আগের বদলগুলোর k × SD সীমা হিসাব করে, আর (change, band, "SIGNAL" বা "noise") ফেরত দেয়। [100, 102, 99, 101, 100, 103, 94] দিয়ে চালান।
    import numpy as np
    def wow_flag(values, k=2):
        v = np.asarray(values, dtype=float)
        chg = 100 * (v[1:] / v[:-1] - 1)          # প্রতিটি সাপ্তাহিক বদল (%)
        last, past = chg[-1], chg[:-1]
        band = k * past.std(ddof=1)               # pandas-এর std-এর মতো ddof=1
        return round(last, 2), round(band, 2), "SIGNAL" if abs(last) > band else "noise"
    print(wow_flag([100, 102, 99, 101, 100, 103, 94]))
    # (-8.74, 4.98, 'SIGNAL')

    আগের পাঁচটি বদল +২.০০, −২.৯৪, +২.০২, −০.৯৯, +৩.০০; তাদের SD প্রায় ২.৪৯, সীমা ±৪.৯৮. শেষ বদল ৯৪/১০৩ − ১ = −৮.৭৪%, সীমার বাইরে। লক্ষ করুন ddof=1: NumPy-র default ০, pandas-এর ১; অংশ ৩-এর ব্লক pandas ব্যবহার করে।

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

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

ক্রমএকটি সাপ্তাহিক dashboard বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সিদ্ধান্ত, দর্শক আর হালনাগাদের ছন্দ লিখে ফেলা
  2. North star, driver আর guardrail বাছাই করা
  3. এক জায়গায় metric-এর সংজ্ঞা আর noise band-এর হিসাব লেখা
  4. Hierarchy অনুযায়ী chart আর layout সাজানো
  5. Caching, session state আর AppTest দিয়ে app পরীক্ষা করা
  6. ডেটার সতেজতা, access control আর deploy ঠিক করা

একটি Streamlit app-এ ব্যবহারকারী একটি slider সরালেন। এরপর কী ঘটে?

  • শুধু slider-এর সঙ্গে যুক্ত function চলে
  • পুরো script ওপর থেকে নিচ পর্যন্ত আবার চলে, slider নতুন মান ফেরত দেয়
  • পুরো পাতা reload হয় আর session state মুছে যায়
  • কিছুই ঘটে না, যতক্ষণ না একটি বোতাম চাপা হয়

একটি Streamlit app-এ ৫০ MB-এর একটি শেখানো scikit-learn model load করতে হবে, যা সব ব্যবহারকারী শুধু predict-এ ব্যবহার করবেন। কোনটি সবচেয়ে উপযুক্ত?

  • @st.cache_data
  • st.session_state-এ রাখা
  • @st.cache_resource
  • কোনো cache ছাড়া প্রতি rerun-এ joblib.load

একটি metric-এর আগের আটটি সাপ্তাহিক বদলের SD ১.৫%, আর এই সপ্তাহের বদল −৪.০%. “২ × SD” noise band নিয়মে সিদ্ধান্ত কী?

  • SIGNAL, কারণ ∣−৪.০∣ > ৩.০
  • noise, কারণ ৪.০ > ১.৫ হলেও তা এক SD-র নিয়ম
  • noise, কারণ বদলটি ৬%-এর কম
  • সিদ্ধান্ত নেওয়া যায় না, t-test লাগবে

একটি dashboard দশটি স্বাধীন metric দেখায়, প্রতিটিতে noise-এর কারণে ভুল সংকেতের সম্ভাবনা ৫%. কোনো সপ্তাহে কোনো বাস্তব বদল না থাকলে অন্তত একটি ভুল সংকেতের সম্ভাবনা প্রায় কত?

  • ৫%
  • ৫০%
  • প্রায় ০%
  • প্রায় ৪০%

একটি Streamlit app প্রতিটি ব্যবহারকারীর what-if অনুমানের তালিকা রাখতে চায়, যা rerun-এ টিকবে কিন্তু অন্য ব্যবহারকারী দেখবেন না। কোথায় রাখবেন?

  • Script-এর ওপরে একটি সাধারণ list-এ
  • st.session_state-এ
  • @st.cache_resource function-এর ফেরত দেওয়া list-এ
  • @st.cache_data function-এর ফেরত দেওয়া list-এ

একটি দোকানে north star = visitors × conversion × (১ − cancel rate) × on-time rate. এক সপ্তাহে visitor ১,০০,০০০, conversion ৩%, বাতিল ৫%, on-time ৯০%. North star কত?

  • ৩,০০০
  • ২,৭০০
  • ২,৫৬৫
  • ২,৮৫০

শব্দকোষ

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

মনে রাখুন

  1. সিদ্ধান্ত আগে: কে দেখবেন, কত দিন পরপর, কোন কাজের জন্য; তারপর north star, গুণফলে driver, আর guardrail.
  2. Noise থেকে সংকেত: সাপ্তাহিক বদলের পাশে আগের বদলের ২ × SD সীমা; অনেক metric-এ ভুল সংকেত আসবেই (৬টিতে প্রায় ২৬%).
  3. Rerun: প্রতিটি widget-বদলে পুরো script আবার চলে; ধীর কাজ cache-এ, form দিয়ে কয়েকটি ইনপুট একসঙ্গে।
  4. তিন জায়গা: ডেটা st.cache_data-এ TTL দিয়ে, model আর connection st.cache_resource-এ, ব্যক্তিগত অবস্থা st.session_state-এ।
  5. Model সৎভাবে: what-if-এর পাশে conformal পরিসর আর তার অর্থ, ইনপুটের সীমা-পাহারা, ডেটার তারিখ, access control, আর AppTest দিয়ে পরীক্ষা।

পরের পর্বে: Dashboard সংখ্যা দেখায়, কিন্তু সিদ্ধান্ত নেওয়াতে গল্প লাগে। পর্ব ৬৭ (Data storytelling — বিশ্লেষণ থেকে সিদ্ধান্ত আর report) দেখাবে কীভাবে একটি বিশ্লেষণকে উত্তর-আগে কাঠামোয়, সৎ chart আর এক পাতার decision memo-তে রূপ দিতে হয়।

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

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