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

A/B test-এর বিশ্লেষণ — t-test, chi-square, CUPED আর ভুল ব্যাখ্যা

Analysing A/B Tests — Two-Proportion z-Test, Chi-Square, Welch's t-Test, the Delta Method, CUPED and Common Misinterpretations

Taught in Bangla, this lesson analyses a finished A/B test on seeded shop data: two-proportion z-test, Wald CI and chi-square, Welch, bootstrap and winsorising for heavy-tailed revenue, delta-method SE for revenue per order, and CUPED with a simulation showing unchanged false positives and higher power. It also covers Holm-corrected segments, Simpson's paradox and honest result memos.

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

  • গণনা থেকে হাতে z, p-value আর Wald CI বের করা, আর chi-square-এর সঙ্গে এর সম্পর্ক দেখা
  • Heavy-tailed revenue-তে Welch, bootstrap আর winsorised গড় তুলনা করে বোঝা কোনটি কী মাপে
  • Revenue per order-এর মতো ratio metric-এর delta-method SE নিজে লিখে naive SE আর bootstrap-এর সঙ্গে মেলানো
  • CUPED-এর θ derive করা, variance কমার পরিমাণ ρ²-এর সঙ্গে মেলানো, আর simulation-এ power বাড়ার প্রমাণ দেখা
  • Segment-এর p-value সংশোধন, Simpson's paradox চেনা, আর ফলাফলের memo সঠিক ভাষায় লেখা

AI-এর ভিত্তি কোর্সের পর্ব ৩৭-এ confidence interval, t-test, p-value আর effect size-এর প্রথম পরিচয় পেয়েছেন। এই কোর্সের পর্ব ২১-এ bootstrap আর পর্ব ২২-এ Holm ও BH সংশোধন দেখেছেন। পর্ব ৬২ (A/B test-এর পরিকল্পনা — power, sample size আর peeking-এর ফাঁদ) পরীক্ষা চালানোর আগের কাজ দেখায়। এই পর্ব শুরু হয় পরীক্ষা শেষ হওয়ার পর: ডেটা হাতে, এবার কোন test, কোন interval, আর ফলাফলের কোন বাক্যটি সৎ। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python-এ NumPy, SciPy, pandas, statsmodels আর matplotlib); প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে।

১ · পরীক্ষা শেষ — বাজারঘরের checkout test আর বিশ্লেষণের আগের যাচাই

দুটি চায়ের দোকান পাশাপাশি। একটিতে নতুন মেনু বোর্ড বসানো হলো। দুই সপ্তাহ পর মালিক জানতে চান, বোর্ডটি কি বিক্রি বাড়িয়েছে? খাতা খুললে তিনটি প্রশ্ন আসে। কতজন খদ্দের কিছু কিনেছেন (অনুপাত)? প্রতি খদ্দের গড়ে কত টাকার কিনেছেন (গড়, যেখানে একজন বিয়েবাড়ির অর্ডার দিলে পুরো হিসাব কেঁপে ওঠে)? আর প্রতি অর্ডারে কত টাকা (দুটি গড়ের ভাগফল)? তিনটি প্রশ্নের অনিশ্চয়তা তিনভাবে মাপতে হয়। এই পর্ব সেই তিনটি হিসাবের পর্ব।

বাজারঘর (কাল্পনিক অনলাইন দোকান) তাদের checkout পাতার নতুন নকশা B পুরোনো নকশা A-র বিপরীতে একটি A/B test-এ যাচাই করেছে। প্রতিটি দর্শক এলোমেলোভাবে A বা B পেয়েছেন; অর্থাৎ randomisation-এর একক হলো দর্শক। পরীক্ষাটি পর্ব ৬২-এর নিয়ম মেনে আগে থেকে ঠিক করা সময় পর্যন্ত চলেছে। পরিকল্পনায় লেখা ছিল: প্রধান metric হলো conversion (দর্শকের কত ভাগ কিনেছেন); দ্বিতীয় metric হলো revenue per visitor আর revenue per order. এগুলোর পাশে guardrail metric, যেমন পাতা খোলার সময় আর ফেরতের হার, যেগুলো খারাপ হলে জয় বাতিল।

বিশ্লেষণের আগে দুটি যাচাই, যা পর্ব ৬২-এ বিস্তারিত আছে: sample ratio mismatch (৫০:৫০ ভাগ হওয়ার কথা থাকলে দুই দলের সংখ্যা কি chi-square-এ মেলে?) আর ডেটার পাইপলাইন ঠিক আছে কি না (A/A test, লগ হারানো)। এই দুটি না মিললে নিচের কোনো p-value-এর অর্থ থাকে না। আমাদের ডেটায় A পেয়েছে ৮,০০৩ জন, B ৭,৯৯৭ জন; সমান ভাগে এটি খুবই স্বাভাবিক।

ডেটা seed দিয়ে বানানো, যাতে সবাই একই সংখ্যা পান। প্রতিটি সারি একজন দর্শক: দল, device, জেলা, পরীক্ষার আগের আট সপ্তাহের খরচ (pre), কিনেছেন কি না, অর্ডারের সংখ্যা আর মোট আয়। নকশা B-তে কেনার সম্ভাবনা আসলে ১২% (আপেক্ষিক) বেশি রাখা হয়েছে; বাস্তবে এই সত্যটি কেউ জানে না, simulation-এ জানি বলে পদ্ধতিগুলো যাচাই করা যায়। ফাইলটি পাতায় লুকানো আছে, প্রতিটি ব্লক একে import করে:

# bazar_ab.py — বাজারঘরের কাল্পনিক checkout A/B test (seed দিয়ে বানানো)
import numpy as np
import pandas as pd

def make_ab(n=16000, seed=63, lift=0.12):
    rng = np.random.default_rng(seed)
    grp = np.where(rng.random(n) < 0.5, "A", "B")    # A = পুরোনো, B = নতুন checkout
    dev = np.where(rng.random(n) < 0.7, "mobile", "desktop")
    dist = rng.choice(["Dhaka", "Ctg", "Khulna", "Sylhet"], n, p=[.45, .25, .15, .15])
    taste = rng.gamma(0.4, 2.5, n)                   # কেনার ঝোঁক (চোখে দেখা যায় না)
    size = rng.lognormal(6.6, 0.8, n)                # গ্রাহকের সাধারণ অর্ডারের দাম
    pre = np.round(rng.poisson(3 * taste) * size)    # আগের ৮ সপ্তাহের খরচ, টাকা
    p = (0.01 + 0.05 * taste) * np.where(dev == "mobile", 0.8, 1.3)
    conv = rng.random(n) < np.clip(p * np.where(grp == "B", 1 + lift, 1), 0, 1)
    orders = conv * (1 + rng.poisson(0.3 * taste))   # কিনলে এক বা একাধিক অর্ডার
    rev = np.round(orders * size * rng.lognormal(0, 0.5, n))
    return pd.DataFrame({"grp": grp, "dev": dev, "dist": dist, "pre": pre,
                         "conv": conv.astype(int), "orders": orders, "rev": rev})

লক্ষ করুন taste (কেনার ঝোঁক) চোখে দেখা যায় না, কিন্তু সেটিই আগের খরচ আর এখনকার কেনাকাটা দুটিকে চালায়। অংশ ৫-এ CUPED ঠিক এই যোগসূত্রটি কাজে লাগাবে।

২ · Conversion — দুই অনুপাতের z-test, Wald CI আর chi-square

দল A-তে nA জন দর্শকের xA জন কিনেছেন, দল B-তে nB-এর xB জন। অনুপাত p̂A = xA/nA, p̂B = xB/nB, আর পার্থক্য d̂ = p̂B − p̂A. প্রতিটি দর্শকের ফল ০ বা ১, তাই একটি অনুপাতের variance p(1 − p)/n. Null hypothesis H0: দুই দলের আসল হার সমান। H0 সত্য ধরলে দুই দলের একটিই হার, তাই তাকে একসঙ্গে অনুমান করা হয়: p̂ = (xA + xB)/(nA + nB), যাকে বলে pooled হার।

z = d̂ / √[ p̂(1 − p̂)(1/nA + 1/nB) ]    Wald CI: d̂ ± 1.96 · √[ p̂A(1 − p̂A)/nA + p̂B(1 − p̂B)/nB ]

দুটি SE আলাদা কেন? Test-এর SE হিসাব হয় “H0 সত্য” জগতে, তাই pooled হার। CI-এর লক্ষ্য আসল পার্থক্য কোথায় থাকতে পারে বলা, সেখানে H0 ধরে নেওয়ার কারণ নেই, তাই প্রতিটি দলের নিজের হার। বড় নমুনায় দুটি প্রায় সমান। আর 2×2 টেবিলে (সারি = দল, কলাম = কিনেছেন/কেনেনি) Pearson-এর chi-square test continuity correction ছাড়া চালালে χ² = z² হুবহু, ১ df: একই test, দুটি পোশাক।

A: 400 of 8000 · B: 480 of 8000α = 0.05

            
  1. দুটি হার। A-তে ৫.০%, B-তে ৬.০%. পার্থক্য ১ শতাংশ বিন্দু, যা আপেক্ষিকভাবে ২০%. রিপোর্টে দুটিই লিখুন, আর কোনটি কোন তা স্পষ্ট রাখুন: “২০% বেড়েছে” শুনে অনেকে ভাবেন ৫% থেকে ২৫%.
  2. Pooled হার। H0 বলে দুই দলের হার এক, তাই সব কেনা আর সব দর্শক মিলিয়ে একটিই অনুমান: ৮৮০/১৬,০০০ = ০.০৫৫.
  3. H0-এর SE. p̂(1 − p̂) = ০.০৫১৯৭৫, আর (১/৮,০০০ + ১/৮,০০০) = ০.০০০২৫. গুণফলের বর্গমূল ০.০০৩৬০৫: “কোনো পার্থক্য না থাকলে” d̂ সাধারণত এর আশপাশে ওঠানামা করত।
  4. z আর p. পার্থক্যটি SE-র ২.৭৭৪ গুণ। Standard normal-এ এর চেয়ে দূরে (দুই দিকে) যাওয়ার সম্ভাবনা ০.০০৫৫, যা α = ০.০৫-এর কম, তাই H0 বাতিল।
  5. Wald CI. এবার প্রতিটি দলের নিজের হার দিয়ে SE = ০.০০৩৬০৪ (pooled-এর প্রায় সমান)। ৯৫% CI = ০.০১০ ± ০.০০৭০৬ = [০.০০২৯, ০.০১৭১]: আসল পার্থক্য প্রায় ০.৩ থেকে ১.৭ শতাংশ বিন্দুর মধ্যে হওয়া ডেটার সঙ্গে মেলে।
  6. Chi-square. একই সংখ্যা 2×2 টেবিলে বসিয়ে continuity correction ছাড়া Pearson χ² চালালে ঠিক z² ≈ ৭.৭০ আসে, ১ df, একই p. পরীক্ষায় দুটির যেকোনোটি লিখলে উত্তর একই।

এবার বাজারঘরের আসল (কাল্পনিক) ডেটায় একই হিসাব। statsmodels-এর proportions_ztest pooled z দেয়, SciPy-র chi2_contingency chi-square, আর শেষ তিন লাইন একটি ছোট Bayesian দৃষ্টি যোগ করে, যা নিচে ব্যাখ্যা করা আছে।

import numpy as np
from scipy import stats
from statsmodels.stats.proportion import proportions_ztest
from bazar_ab import make_ab

df = make_ab()                                   # ১৬,০০০ দর্শক, seed 63
t = df.groupby("grp")["conv"].agg(["sum", "count"])
print(t.rename(columns={"sum": "buyers", "count": "visitors"}))
x, n = t["sum"].to_numpy(), t["count"].to_numpy()
p = x / n
d = p[1] - p[0]                                  # B − A
print(f"rate A {p[0]:.4f}  B {p[1]:.4f}  diff {d:+.4f}  ({d / p[0]:+.1%} relative)")

z, pz = proportions_ztest(x[::-1], n[::-1])      # B আগে, তাই z ধনাত্মক
se = np.sqrt(p[0] * (1 - p[0]) / n[0] + p[1] * (1 - p[1]) / n[1])
print(f"pooled z = {z:.3f}, two-sided p = {pz:.4f}")
print(f"Wald 95% CI for B - A: [{d - 1.96 * se:+.4f}, {d + 1.96 * se:+.4f}]")

table = np.column_stack([x, n - x])              # সারি A, B; কলাম কিনেছেন, কেনেনি
chi2, pc, dof, _ = stats.chi2_contingency(table, correction=False)
print(f"chi2 = {chi2:.3f} (z^2 = {z ** 2:.3f}), df = {dof}, p = {pc:.4f}")

rng = np.random.default_rng(0)                   # Beta(1, 1) prior থেকে posterior
ra = rng.beta(1 + x[0], 1 + n[0] - x[0], 200_000)
rb = rng.beta(1 + x[1], 1 + n[1] - x[1], 200_000)
print(f"Bayesian note: P(B > A) = {np.mean(rb > ra):.4f}")

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

     buyers  visitors
grp
A       412      8003
B       506      7997
rate A 0.0515  B 0.0633  diff +0.0118  (+22.9% relative)
pooled z = 3.207, two-sided p = 0.0013
Wald 95% CI for B - A: [+0.0046, +0.0190]
chi2 = 10.286 (z^2 = 10.286), df = 1, p = 0.0013
Bayesian note: P(B > A) = 0.9994

A-তে ৫.১৫%, B-তে ৬.৩৩%: পার্থক্য +১.১৮ শতাংশ বিন্দু, আপেক্ষিক +২২.৯%. z = ৩.২০৭, p = ০.০০১৩; chi-square ১০.২৮৬, যা z²-এর সঙ্গে হুবহু মেলে। CI [+০.৪৬, +১.৯০] শতাংশ বিন্দু, অর্থাৎ আপেক্ষিকভাবে প্রায় +৯% থেকে +৩৭%. এখানে একটি সতর্কতা: আমরা জানি আসল প্রভাব +১২% আপেক্ষিক, অথচ মাপা হয়েছে +২২.৯%. এটি কোনো ভুল নয়, দৈবের ওঠানামা; আসল মান CI-এর ভেতরেই আছে। কিন্তু “p ছোট, তাই +২৩% পাকা” লিখলে আশাবাদী হবেন। Significant ফলগুলো গড়ে প্রভাবকে বাড়িয়ে দেখায়, একে বলে winner's curse, বিশেষ করে যখন power কম।

Bayesian দৃষ্টি, সংক্ষেপে। AI-এর ভিত্তি কোর্সের পর্ব ৫০-এ Bayes-এর উপপাদ্য দেখেছেন। প্রতিটি দলের অজানা হারের জন্য Beta(১, ১) prior (০ থেকে ১-এর মধ্যে সব মান সমান সম্ভাব্য) নিলে x জন ক্রেতা আর n − x জন না-ক্রেতা দেখার পর posterior হয় Beta(১ + x, ১ + n − x). দুই posterior থেকে অনেক নমুনা টেনে P(B > A) গোনা যায়: এখানে ০.৯৯৯৪. এই বাক্যটি ব্যবসায়ীর কাছে সহজ (“B ভালো হওয়ার সম্ভাবনা প্রায় ৯৯.৯%”), কিন্তু তিনটি শর্ত মনে রাখুন: উত্তর prior-এর ওপর নির্ভর করে, “B ভালো” মানে কতটা ভালো তা বলে না (তাই অনেকে expected loss বা পার্থক্যের posterior interval-ও দেখেন), আর বারবার উঁকি দিয়ে থামালে Bayesian সিদ্ধান্তও ঝুঁকিমুক্ত নয়। এটি p-value নয়: “১ − p” কখনো P(B > A) নয়।

৩ · Revenue per visitor — heavy tail, Welch, bootstrap আর winsorising

Revenue per visitor (RPV) হলো দর্শকপ্রতি গড় আয়, যেখানে না-কেনা দর্শকের আয় ০. এই metric-এর বণ্টন অদ্ভুত: ৯৪%-এর বেশি সারি ০, আর বাকিদের মধ্যে অল্প কয়েকজনের অনেক বড় অর্ডার। একে বলে heavy-tailed distribution. গড় তখন কয়েকটি বড় সংখ্যার দয়ায় চলে। দুই দলের গড় তুলনায় তিনটি সাধারণ পথ:

পদ্ধতিকী মাপেভালো দিকসাবধানতা
Welch t-testগড় RPV-এর পার্থক্যদুই দলের variance সমান ধরে না (B. L. Welch, ১৯৪৭)ছোট নমুনায় heavy tail-এ normal approximation দুর্বল
Percentile bootstrapএকই গড়ের পার্থক্যবণ্টনের আকার ধরে নেয় নালেজের বিরল বড় মান নমুনায় না থাকলে bootstrap-ও তা দেখে না
Winsorised গড়বড় মান একটি সীমায় কাটা গড়Variance অনেক কমে, power বাড়েএটি আর মোট আয়ের প্রশ্ন নয়; সীমা আগে থেকে ঠিক করুন

Welch-এর SE = √(sA²/nA + sB²/nB), যেখানে s² প্রতিটি দলের নমুনা variance. Central limit theorem বলে n বড় হলে গড় প্রায় normal হয়, কিন্তু লেজ যত ভারী, “বড়” তত বড় হতে হয়। তাই এখানে bootstrap দিয়ে মিলিয়ে দেখা জরুরি। Log transform (log(১ + আয়)) আরেকটি প্রচলিত পথ, কিন্তু তখন test করা হয় log-এর গড়, যা মোট আয়ের দিক নির্দেশ করে না: B কিছু বড় ক্রেতাকে হারিয়ে অনেক ছোট ক্রেতা আনলে log-গড় বাড়তে পারে, অথচ আয় কমে।

import numpy as np
from scipy import stats
from bazar_ab import make_ab

df = make_ab()
a = df.loc[df.grp == "A", "rev"].to_numpy()
b = df.loc[df.grp == "B", "rev"].to_numpy()
top = np.sort(np.concatenate([a, b]))[::-1]
print(f"share of revenue from top 1% of visitors: {top[:160].sum() / top.sum():.1%}")
print(f"max {top[0]:.0f} Tk, 99.9th pct {np.percentile(top, 99.9):.0f} Tk")
d = b.mean() - a.mean()
print(f"revenue per visitor A {a.mean():.1f}  B {b.mean():.1f}  diff {d:+.1f} Tk")

w = stats.ttest_ind(b, a, equal_var=False)        # Welch: variance সমান ধরে না
ci = w.confidence_interval(0.95)
print(f"Welch t = {w.statistic:.3f}, p = {w.pvalue:.4f}, CI [{ci.low:+.1f}, {ci.high:+.1f}]")

rng = np.random.default_rng(1)                    # percentile bootstrap, দুই দল আলাদা
bd = [rng.choice(b, b.size).mean() - rng.choice(a, a.size).mean() for _ in range(1000)]
print(f"bootstrap 95% CI [{np.percentile(bd, 2.5):+.1f}, {np.percentile(bd, 97.5):+.1f}]")

cap = np.percentile(np.concatenate([a, b]), 99.5) # দুই দলে একই সীমা
wa, wb = np.minimum(a, cap), np.minimum(b, cap)
ww = stats.ttest_ind(wb, wa, equal_var=False)
print(f"winsorised at {cap:.0f} Tk: diff {wb.mean() - wa.mean():+.1f}, p = {ww.pvalue:.4f}")

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

share of revenue from top 1% of visitors: 56.2%
max 34779 Tk, 99.9th pct 13024 Tk
revenue per visitor A 113.3  B 139.2  diff +26.0 Tk
Welch t = 1.825, p = 0.0680, CI [-1.9, +53.8]
bootstrap 95% CI [-1.0, +55.4]
winsorised at 5249 Tk: diff +20.3, p = 0.0209

প্রথম লাইনটিই গল্প: ১৬,০০০ দর্শকের সবচেয়ে বড় ১% (১৬০ জন) মোট আয়ের ৫৬.২% এনেছেন। RPV-এর পার্থক্য +২৬.০ টাকা (প্রায় +২৩%), কিন্তু Welch-এ p = ০.০৬৮ আর CI [−১.৯, +৫৩.৮]: ০ ভেতরে, আবার +৫০ টাকার মতো বড় উন্নতিও ভেতরে। Bootstrap CI প্রায় একই ([−১.০, +৫৫.৪]), তাই এই আকারের নমুনায় Welch-এর normal approximation মোটামুটি টিকেছে। ৯৯.৫ percentile-এ (৫,২৪৯ টাকা) কেটে দিলে পার্থক্য +২০.৩, p = ০.০২১: variance কমেছে, তাই significant. কিন্তু প্রশ্নটিও বদলেছে: এখন “বড় অর্ডারকে ৫,২৪৯ টাকায় ছেঁটে গড় আয়”. আর লক্ষ করুন, conversion-এ (p = ০.০০১৩) যে প্রভাব স্পষ্ট, revenue-তে তা ঝাপসা; কারণ একই প্রভাব heavy tail-এর গোলমালে ঢাকা পড়ে। “Revenue-তে significant নয়” মানে “revenue বাড়েনি” নয়, অংশ ৮-এ এ নিয়ে আবার আসছি।

Winsorising-এর সততার শর্ত। সীমা (যেমন ৯৯.৫ percentile) পরীক্ষার আগে পরিকল্পনায় লিখুন, দুই দল মিলিয়ে একই সীমা নিন, আর ফল দেখে সীমা বদলাবেন না। কয়েকটি সীমা চেষ্টা করে যেটিতে p ছোট সেটি বাছা হলো forking paths, যা AI-এর ভিত্তি কোর্সের পর্ব ৩৭ আর এই কোর্সের পর্ব ০৪-এ দেখেছেন।

৪ · Ratio metric — revenue per order আর delta method

Revenue per order (RPO) = মোট আয় ÷ মোট অর্ডার। এটি একটি ratio metric: লব আর হর দুটিই দর্শক-স্তরের যোগফল। প্রলোভন হলো প্রতিটি অর্ডারকে একটি সারি ধরে সাধারণ গড়ের SE নেওয়া। ভুলটি এখানে: randomise হয়েছে দর্শক, অর্ডার নয়। একজন দর্শকের তিনটি অর্ডার পরস্পর স্বাধীন নয় (একই মানুষ, একই রুচি, একই পকেট), তাই অর্ডারকে একক ধরলে স্বাধীন তথ্যের পরিমাণ বাড়িয়ে গোনা হয় আর SE ছোট দেখায়। এটি পর্ব ২১-এর cluster bootstrap-এর একই শিক্ষা।

সঠিক পথ: প্রতিটি দর্শক i-এর Yi = আয় আর Xi = অর্ডার। তাহলে R = Ȳ/X̄, যা দুটি গড়ের ভাগফল, আর দর্শকেরা স্বাধীন। Delta method একটি function-এর variance তার প্রথম ক্রমের Taylor বিস্তার দিয়ে অনুমান করে। μY, μX-এর চারপাশে f(Ȳ, X̄) = Ȳ/X̄ বিস্তার করলে:

R − r ≈ (Ȳ − μY)/μX − r(X̄ − μX)/μX  ⇒  Var(R) ≈ [ σY² − 2r·σXY + r²·σX² ] / (n · μX²)

এখানে r = μY/μX, σY² আর σX² দর্শক-স্তরের variance, σXY তাদের covariance, আর n দর্শকের সংখ্যা। ছোট উদাহরণ: তিনজন দর্শকের (আয়, অর্ডার) = (২,০০০, ২), (১,০০০, ১), (০, ০)। R = ৩,০০০/৩ = ১,০০০ টাকা। প্রতিটি দর্শকের “R থেকে বিচ্যুতি” Yi − R·Xi = ০, ০, ০: এই তিনজনের মধ্যে প্রতি অর্ডারের দাম একই, তাই R প্রায় নড়বে না। Delta method আসলে এই Y − R·X-এর variance-কেই μX² দিয়ে ভাগ করে। Alex Deng, Ulf Knoblich আর Jiannan Lu ২০১৮ সালে KDD-তে online পরীক্ষায় delta method-এর ব্যবহার নিয়ে একটি বহুল পঠিত প্রবন্ধ লেখেন। নিচের ব্লক তিনটি SE মেলায়: delta, naive (অর্ডার = সারি) আর দর্শক ধরে bootstrap.

import numpy as np
from bazar_ab import make_ab

def delta_ratio(y, x):                       # R = mean(y)/mean(x); একক = দর্শক
    r = y.mean() / x.mean()
    c = np.cov(y, x)                         # [[var y, cov], [cov, var x]]
    v = (c[0, 0] - 2 * r * c[0, 1] + r ** 2 * c[1, 1]) / (len(y) * x.mean() ** 2)
    return r, np.sqrt(v)

df = make_ab()
rng = np.random.default_rng(2)
res = {}
for g in ("A", "B"):
    s = df[df.grp == g]
    y, x = s.rev.to_numpy(), s.orders.to_numpy()
    r, se_d = delta_ratio(y, x)
    k = x > 0                                # naive: প্রতিটি অর্ডার আলাদা সারি
    per = np.repeat(y[k] / x[k], x[k].astype(np.intp))
    se_n = per.std(ddof=1) / np.sqrt(per.size)
    boot = []                                # দর্শক ধরে bootstrap
    for _ in range(500):
        i = rng.integers(0, len(y), len(y))
        boot.append(y[i].sum() / x[i].sum())
    res[g] = (r, se_d)
    print(f"{g}: {x.sum()} orders, revenue/order {r:.1f} Tk | SE delta {se_d:.1f}"
          f", naive {se_n:.1f}, bootstrap {np.std(boot, ddof=1):.1f}")
d = res["B"][0] - res["A"][0]
se = np.hypot(res["A"][1], res["B"][1])
print(f"B - A = {d:+.1f} Tk, 95% CI [{d - 1.96 * se:+.1f}, {d + 1.96 * se:+.1f}]")

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

A: 785 orders, revenue/order 1154.8 Tk | SE delta 69.3, naive 47.2, bootstrap 67.5
B: 944 orders, revenue/order 1179.5 Tk | SE delta 67.8, naive 42.6, bootstrap 64.1
B - A = +24.7 Tk, 95% CI [-165.3, +214.7]

দুই দলেই delta-method SE (৬৯.৩ আর ৬৭.৮) দর্শক ধরে bootstrap-এর (৬৭.৫ আর ৬৪.১) কাছাকাছি, কিন্তু naive SE (৪৭.২ আর ৪২.৬) প্রায় এক-তৃতীয়াংশ ছোট। Naive পথে পার্থক্যের CI হতো প্রায় ±১২৫ টাকা, আসলে তা ±১৯০: ভুল এককে আত্মবিশ্বাস বাড়িয়ে দেখানো হয়। ফলটি নিজেও শিক্ষণীয়: প্রতি অর্ডারের আয়ে পার্থক্য +২৪.৭ টাকা, CI [−১৬৫, +২১৫], অর্থাৎ অমীমাংসিত। Simulation-এ B শুধু কেনার সম্ভাবনা বাড়ায়, অর্ডারের দাম বদলায় না, তাই এটি প্রত্যাশিত। আরেকটি সূক্ষ্ম কথা: ratio metric-এর হরও treatment বদলাতে পারে। B যদি নতুন, ছোট-ঝুড়ির ক্রেতা আনে, তাহলে revenue per order কমতে পারে অথচ মোট আয় বাড়ে। তাই ratio metric সাধারণত সিদ্ধান্তের metric নয়, ব্যাখ্যার metric.

৫ · CUPED — আগের খরচ দিয়ে variance কমানো

ক্রিকেটে দুই নতুন ব্যাটিং কোচের তুলনা করতে চান। কোচ ক-এর দলে দৈবক্রমে দুজন তারকা পড়েছেন, যাঁরা আগের মৌসুমেও প্রচুর রান করেছেন। শুধু এই মৌসুমের রান দেখলে কোচ ক-কে ভালো মনে হবে। বুদ্ধিমান বিশ্লেষক দেখবেন, প্রত্যেকে আগের মৌসুমের তুলনায় কতটা এগোলেন। আগের মৌসুমের রান কোচের ওপর নির্ভর করে না (কোচ তখন আসেননি), তাই এই সংশোধন কোনো পক্ষপাত আনে না, শুধু “কে আগে থেকেই ভালো” সেই গোলমাল সরিয়ে দেয়।

CUPED (Controlled-experiment Using Pre-Experiment Data) ঠিক এই কাজ করে। Alex Deng, Ya Xu, Ron Kohavi আর Toby Walker ২০১৩ সালে WSDM সম্মেলনে Microsoft Bing-এর পরীক্ষার অভিজ্ঞতা থেকে এটি প্রকাশ করেন; প্রবন্ধে কিছু metric-এ variance উল্লেখযোগ্যভাবে কমার কথা বলা হয়েছে, তবে কতটা কমবে তা metric আর covariate-এর ওপর নির্ভর করে। ধারণাটি পুরোনো: Monte Carlo-র control variate আর পরিসংখ্যানের ANCOVA বা regression adjustment-এর আত্মীয়।

Y = পরীক্ষার সময়ের আয়, X = পরীক্ষার আগের আট সপ্তাহের খরচ। নতুন metric Ycv = Y − θ(X − X̄). যেকোনো θ-তে দুই দলের গড়ের পার্থক্যের প্রত্যাশা বদলায় না, কারণ randomisation-এর জন্য দুই দলে X-এর প্রত্যাশিত গড় এক: E[X̄B − X̄A] = ০. তাহলে θ এমনভাবে বাছি যাতে variance সবচেয়ে কম হয়:

Var(Y − θX) = σY² − 2θ·σXY + θ²·σX²  →  d/dθ = 0  →  θ = σXY/σX²  →  Var(Ycv) = σY²(1 − ρ²)

θ হলো Y-কে X-এর ওপর regression করলে ঢাল, আর ρ হলো X আর Y-এর correlation. ঢালটি বসালে variance কমে ঠিক ρ² ভাগ। ρ = ০.৫ হলে ২৫% কম variance, যা একই নির্ভুলতার জন্য প্রায় ২৫% কম দর্শক। শর্ত একটিই, কিন্তু কঠোর: X-কে treatment প্রভাবিত করতে পারবে না। তাই পরীক্ষা শুরুর আগের মাপ নিন। পরীক্ষার সময়ের কোনো চলক (যেমন সেশনের সংখ্যা, যা নতুন checkout বদলাতে পারে) নিলে প্রভাবের একটি অংশ “সংশোধনে” মুছে যেতে পারে।

X = আগের খরচ · Y = এখনকার আয়দল A: ১, ৩ · দল B: ২, ৪

            
  1. ডেটা। চারজন দর্শক, প্রতিজনের আগের খরচ X আর এখনকার আয় Y (শত টাকায়)। দর্শক ১ আর ৩ দল A-তে, ২ আর ৪ দল B-তে। দুই চলকেরই গড় ৩.
  2. Covariance আর variance-এর যোগফল। গড় থেকে বিচ্যুতি গুণ করে যোগ করলে ১৪, X-এর বিচ্যুতির বর্গের যোগ ২০. দুটিকেই n − ১ দিয়ে ভাগ করতে হতো, কিন্তু θ-তে সেটি কেটে যায়।
  3. θ আর সংশোধিত মান। θ = ১৪/২০ = ০.৭. যার আগের খরচ গড়ের নিচে (দর্শক ১, ২), তার মান বাড়ে; যার ওপরে (৩, ৪), তার মান কমে।
  4. Variance কতটা কমল। Y-এর বিচ্যুতির বর্গের যোগ ১৪ থেকে ৪.২-এ নেমেছে, অর্থাৎ ৭০% কম। ρ² = ১৪²/(২০ × ১৪) = ০.৭০: সূত্রের সঙ্গে হুবহু মিলেছে।
  5. সাধারণ তুলনা। B-র গড় ৪.৫, A-র ১.৫, পার্থক্য ৩.০. কিন্তু খেয়াল করুন, B-তে দৈবক্রমে আগে থেকেই বেশি খরচ করা মানুষ পড়েছেন (X̄ ৪ বনাম ২)।
  6. CUPED তুলনা। সংশোধিত গড়ে পার্থক্য ১.৬ = ৩.০ − θ × (৪ − ২). দৈবক্রমে হওয়া অসম ভাগের অংশ সরে গেছে। বড় নমুনায় এই অসমতা গড়ে ০, তাই অনুমান unbiased থাকে, শুধু প্রতিটি পরীক্ষায় কম নড়ে।

৬ · CUPED-এর ফল আর simulation — একই false positive, বেশি power

এবার পুরো ১৬,০০০ জনে। ব্লকের প্রথম অংশ θ, ρ² আর আসল variance কমা মেলায়, তারপর সাধারণ আর CUPED দুই পথে Welch চালায়। দ্বিতীয় অংশ একটি simulation: একই ১৬,০০০ জনকে ১,০০০ বার নতুন করে এলোমেলো দুই ভাগে ভাগ করা হয়। প্রথমে কোনো প্রভাব ছাড়া (A/A test), যেখানে p < ০.০৫-এর হার হলো false positive-এর হার, আর তা ৫%-এর কাছে থাকার কথা। তারপর B-ভাগের আয় কৃত্রিমভাবে ২৫% বাড়িয়ে, যেখানে সেই হারই power. দ্রুত চালাতে simulation-এ Welch-এর statistic সরাসরি ±১.৯৬-এর সঙ্গে মেলানো হয়েছে, কারণ এত বড় নমুনায় t আর normal প্রায় এক।

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from bazar_ab import make_ab

df = make_ab()
y, x = df.rev.to_numpy(), df.pre.to_numpy()
b = (df.grp == "B").to_numpy()
theta = np.cov(x, y)[0, 1] / x.var(ddof=1)        # দুই দল মিলিয়ে একটিই θ
yc = y - theta * (x - x.mean())                   # CUPED-adjusted revenue
print(f"theta = {theta:.4f}, rho^2 = {np.corrcoef(x, y)[0, 1] ** 2:.3f}, "
      f"variance reduction = {1 - yc.var() / y.var():.3f}")
for name, v in (("raw  ", y), ("CUPED", yc)):
    w = stats.ttest_ind(v[b], v[~b], equal_var=False)
    print(f"{name} diff {v[b].mean() - v[~b].mean():+.1f} Tk, p = {w.pvalue:.4f}")
def z(v, g):                                      # Welch statistic, n বড় তাই ±1.96
    se = np.sqrt(v[g].var() / g.sum() + v[~g].var() / (~g).sum())
    return (v[g].mean() - v[~g].mean()) / se
rng = np.random.default_rng(3)                    # একই ১৬,০০০ জন, নতুন এলোমেলো ভাগ
rej = np.zeros((2, 2))
for i, lift in enumerate((0.0, 0.25)):            # A/A, তারপর B-তে +২৫% আয়
    for _ in range(1000):
        g = rng.random(y.size) < 0.5
        yy = np.where(g, y * (1 + lift), y)
        yyc = yy - np.cov(x, yy)[0, 1] / x.var(ddof=1) * (x - x.mean())
        rej[i] += [abs(z(yy, g)) > 1.96, abs(z(yyc, g)) > 1.96]
rej /= 1000
print("reject rate [raw, CUPED]  A/A:", rej[0], " +25%:", rej[1])
fig, ax = plt.subplots(figsize=(6, 3.2))
for j, lab in enumerate(["plain Welch", "CUPED + Welch"]):
    ax.bar(np.arange(2) + 0.36 * j - 0.18, rej[:, j], 0.36, label=lab)
ax.axhline(0.05, color="gray", ls="--", lw=1); ax.legend(frameon=False)
ax.set_xticks([0, 1], ["A/A (no effect)", "B revenue +25%"])
ax.set_ylabel("share of tests with p < 0.05")
fig.subplots_adjust(left=0.13, bottom=0.12, right=0.98, top=0.95)

আসল আউটপুট (ব্রাউজারের Python), নিচে ছবিসহ:

theta = 0.0439, rho^2 = 0.155, variance reduction = 0.155
raw   diff +26.0 Tk, p = 0.0680
CUPED diff +29.2 Tk, p = 0.0257
reject rate [raw, CUPED]  A/A: [0.051 0.048]  +25%: [0.512 0.587]

θ = ০.০৪৩৯: আগের আট সপ্তাহে ১০০ টাকা বেশি খরচ করা দর্শক পরীক্ষার সময়ে গড়ে প্রায় ৪.৪ টাকা বেশি আনেন। ρ² = ০.১৫৫ আর মাপা variance কমা ০.১৫৫, সূত্রের সঙ্গে হুবহু (θ একই ডেটা থেকে অনুমান করা হয়েছে বলে এখানে মিল নিখুঁত)। পার্থক্য +২৬.০ থেকে +২৯.২ টাকা হলো কেন? দৈবক্রমে B দলের আগের খরচ গড়ে প্রায় ৭৩ টাকা কম ছিল, আর CUPED সেই অসমতা ফিরিয়ে দিয়েছে: −θ × (−৭৩) ≈ +৩.২. p ০.০৬৮ থেকে ০.০২৬-এ নেমেছে, কারণ SE ছোট হয়েছে।

Simulation-এর ছবিতে বাঁ দিকে A/A: কাঁচা Welch ০.০৫১, CUPED ০.০৪৮, দুটিই ৫%-এর রেখার কাছে (১,০০০ বারে ±০.০১৪-এর মতো ওঠানামা স্বাভাবিক)। অর্থাৎ CUPED মিথ্যা জয় বাড়ায়নি। ডান দিকে +২৫% প্রভাবে power ০.৫১২ থেকে ০.৫৮৭: একই দর্শকে প্রায় ৭.৫ শতাংশ বিন্দু বেশি আসল প্রভাব ধরা পড়ছে, যা ১/(১ − ০.১৫৫) ≈ ১.১৮ গুণ বড় নমুনার সমান। সততার শর্ত মনে রাখুন: CUPED পরিকল্পনায় আগে থেকে লেখা ছিল বলেই ০.০২৬ বিশ্বাসযোগ্য। কাঁচা p ০.০৬৮ দেখে তারপর CUPED খুঁজলে সেটি forking paths. (এটি একটি simulation-এর একটি run; আপনার ডেটায় ρ আর লাভ আলাদা হবে।)

দুটি ব্যবহারিক খুঁটিনাটি। (১) নতুন দর্শকের আগের খরচ নেই; তাঁদের X = ০ ধরলে চলে, কারণ X শুধু treatment-এর আগের তথ্য হলেই হলো। ভালো অভ্যাস হলো একটি “নতুন দর্শক কি না” চলক যোগ করে regression adjustment করা। (২) θ দুই দল মিলিয়ে একবার অনুমান করুন। বড় নমুনায় এটি regression-এ Y ~ দল + X-এর দলের coefficient-এর প্রায় সমান; Winston Lin ২০১৩ সালে দেখান যে randomised পরীক্ষায় দল আর covariate-এর interaction-সহ regression adjustment বড় নমুনায় অন্তত সমান নির্ভুল হয় (তাঁর প্রবন্ধটি regression adjustment নিয়ে আগের একটি সমালোচনার জবাব)।

৭ · অনেক metric, অনেক segment আর Simpson's paradox

প্রধান ফল দেখার পর প্রায় সবাই জিজ্ঞেস করেন: “কোন জেলায় বা কোন device-এ B সবচেয়ে ভালো?” প্রশ্নটি স্বাভাবিক, কিন্তু চারটি জেলা × দুটি device = আটটি segment মানে আটটি test. সব segment-এ প্রভাব সমান হলেও কোনো একটিতে ছোট p পাওয়ার সম্ভাবনা অনেক বেশি। পর্ব ২২-এর Holm (একটিও মিথ্যা দাবি নয়) আর BH (দাবিগুলোর মধ্যে মিথ্যার অনুপাত সীমিত) এখানেই কাজে লাগে। নিচের হিসাবটি চারটি ব্লকের সীমার কারণে পাতায় চালানো ব্লক নয়; একই bazar_ab.py দিয়ে ডেস্কটপে সত্যিকারের run (Python 3.14.4, NumPy 2.5.4, pandas 3.1.1, statsmodels 0.15.0, লেখার সময়ে):

import pandas as pd
from statsmodels.stats.proportion import proportions_ztest
from statsmodels.stats.multitest import multipletests
from bazar_ab import make_ab

df = make_ab()
rows = []
for (dist, dev), s in df.groupby(["dist", "dev"]):
    c = s.groupby("grp")["conv"].agg(["sum", "count"])
    z, p = proportions_ztest(c["sum"].to_numpy()[::-1], c["count"].to_numpy()[::-1])
    rate = c["sum"] / c["count"]
    rows.append([dist, dev, int(c["count"].sum()), rate["A"], rate["B"], p])
seg = pd.DataFrame(rows, columns=["district", "device", "n", "rate_A", "rate_B", "p"])
seg["p_holm"] = multipletests(seg["p"], method="holm")[1]
seg["p_bh"] = multipletests(seg["p"], method="fdr_bh")[1]
print(seg.round(4).to_string(index=False))
$ python segments.py
district  device    n  rate_A  rate_B      p  p_holm   p_bh
     Ctg desktop 1247  0.0649  0.0887 0.1148  0.5738 0.2295
     Ctg  mobile 2835  0.0427  0.0477 0.5250  1.0000 0.5250
   Dhaka desktop 2139  0.0657  0.0909 0.0304  0.2125 0.1214
   Dhaka  mobile 5055  0.0435  0.0475 0.5016  1.0000 0.5250
  Khulna desktop  711  0.0665  0.0849 0.3531  1.0000 0.4708
  Khulna  mobile 1696  0.0463  0.0577 0.2902  1.0000 0.4642
  Sylhet desktop  716  0.1000  0.0628 0.0685  0.4111 0.1827
  Sylhet  mobile 1601  0.0412  0.0777 0.0021  0.0169 0.0169

আমরা জানি, simulation-এ প্রতিটি segment-এ আসল প্রভাব একই (+১২% আপেক্ষিক)। তবু টেবিল দুটি “গল্প” বলছে। প্রথম, Sylhet mobile-এ B প্রায় দ্বিগুণ (৪.১% থেকে ৭.৮%, p = ০.০০২১), Holm-এর পরেও ০.০১৭. প্রভাবটি সম্ভবত শূন্য নয়, কিন্তু +৮৯% আকারটি winner's curse: আটটির মধ্যে সবচেয়ে বড়টিকে বাছলে আকার বাড়িয়ে দেখা প্রায় নিশ্চিত। দ্বিতীয়, Sylhet desktop-এ B যেন খারাপ (১০.০% থেকে ৬.৩%, p = ০.০৬৯), মাত্র ৭১৬ জনে শুধু দৈব। কেউ যদি লেখেন “Sylhet-এর desktop ব্যবহারকারীদের জন্য পুরোনো checkout রাখুন”, তিনি গোলমালকে নীতি বানাচ্ছেন। Dhaka desktop-এর p = ০.০৩০৪ সংশোধনের পর ০.২১. নিয়ম: আগে থেকে পরিকল্পনায় না থাকা segment বিশ্লেষণ অনুসন্ধান, সিদ্ধান্ত নয়; সংশোধিত p দিন আর সন্দেহজনক segment নতুন পরীক্ষায় যাচাই করুন।

Simpson's paradox — প্রতিটি দিনে জেতে, মোটে হারে

Randomise করা পরীক্ষায় device বা জেলার মিশ্রণ দুই দলে প্রায় এক, তাই Simpson's paradox সাধারণত আসে না। এটি আসে যখন ভাগের অনুপাত সময়ের সঙ্গে বদলায়, যেমন ramp-up: প্রথম দিনে নিরাপত্তার জন্য B পেয়েছে মাত্র ১০% দর্শক, দ্বিতীয় দিনে ৫০%. ধরুন প্রথম দিনটি বেতনের দিন, কেনার হার বেশি:

দিনA: দর্শক → ক্রেতাB: দর্শক → ক্রেতাকে ভালো
দিন ১ (বেতনের দিন, B ১০%)৯,০০০ → ৭২০ (৮.০%)১,০০০ → ৮৪ (৮.৪%)B
দিন ২ (সাধারণ দিন, B ৫০%)৫,০০০ → ১৫০ (৩.০%)৫,০০০ → ১৬০ (৩.২%)B
দুই দিন যোগ করে১৪,০০০ → ৮৭০ (৬.২%)৬,০০০ → ২৪৪ (৪.১%)A !

প্রতিটি দিনে B এগিয়ে, অথচ যোগফলে A অনেক এগিয়ে। কারণ A-র দর্শকদের বড় অংশ (৯,০০০/১৪,০০০) এসেছেন বেশি-কেনার দিনে, আর B-র বেশির ভাগ (৫,০০০/৬,০০০) কম-কেনার দিনে। দিন এখানে confounder: এটি দল পাওয়ার সম্ভাবনা আর কেনার হার দুটিকেই প্রভাবিত করেছে। প্রতিকার: দিনভিত্তিক (stratified) পার্থক্য নিয়ে ওজন দিয়ে মেলানো (যেমন দুই দিনের মোট দর্শক অনুযায়ী: (১০,০০০ × ০.৪ + ১০,০০০ × ০.২)/২০,০০০ = +০.৩ শতাংশ বিন্দু), বা বিশ্লেষণে শুধু সেই সময়ের ডেটা নেওয়া যখন ভাগের অনুপাত স্থির ছিল। কোন দিক “ঠিক”, তা ডেটা একা বলে না, কারণ-কাঠামো বলে; পর্ব ৬৪ (Causal inference-এর পরিচয় — DAG, matching আর difference-in-differences) এই প্রশ্নটি গুছিয়ে দেখাবে।

৮ · ভুল ব্যাখ্যা আর সিদ্ধান্তের memo

Test সঠিক হলেও বাক্য ভুল হতে পারে। American Statistical Association ২০১৬ সালের বিবৃতিতে (Ronald Wasserstein আর Nicole Lazar) ছয়টি নীতি দেয়, যার সারকথা: p-value H0 সত্য হওয়ার সম্ভাবনা নয়, প্রভাবের আকার বা গুরুত্ব মাপে না, আর শুধু একটি সীমা পার হওয়া দিয়ে সিদ্ধান্ত নেওয়া উচিত নয়। নিচের stepper একটি কাল্পনিক memo লাইন ধরে ধরে পড়ে।

কাল্পনিক memo, প্রতি দলে ~৫১,০০০ দর্শকব্যবহারিক সীমা +০.২৫ শ. বি.

            
  1. “p = ০.০৩, তাই ৯৭%”. সবচেয়ে পরিচিত ভুল। p-value হিসাব হয় H0 সত্য ধরে: সেই জগতে এত বড় বা আরও বড় পার্থক্য দেখার সম্ভাবনা ৩%. “B ভালো হওয়ার সম্ভাবনা” জানতে prior লাগে, যা p-value-এ নেই।
  2. আকার আর অনিশ্চয়তা। একই memo-র আসল তথ্য হলো পার্থক্য +০.৩০ শতাংশ বিন্দু আর CI [+০.০৩, +০.৫৭]. CI ০ ছোঁয় না, কিন্তু আগে ঠিক করা ব্যবহারিক সীমা +০.২৫ CI-এর ভেতরে: প্রভাব কাজের মতো বড় কি না, এখনো নিশ্চিত নয়।
  3. “Not significant = প্রভাব নেই”. Revenue-র CI [−৩.১, +১৪.১] টাকা: ০ আছে, আবার দর্শকপ্রতি +১৪ টাকার মতো বড় লাভও আছে। সঠিক বাক্য “অমীমাংসিত”, “প্রভাব নেই” নয়। প্রভাব নেই দাবি করতে CI-কে পুরোটা ব্যবহারিক সীমার ভেতরে থাকতে হয়।
  4. Base rate. সব ধারণা কাজ করে না। যদি ১০% ধারণা সত্যিই কাজ করে, power ৮০% আর α = ০.০৫, তাহলে significant ফলের ০.০৪৫/০.১২৫ = ৩৬% মিথ্যা জয়। এজন্যই ছোট p-ও একা প্রমাণ নয়, আর বিস্ময়কর ফল আবার যাচাই করা হয়।
  5. Segment আর থামানো। পরে খুঁজে পাওয়া একটি segment-এর “+৪০%” আর p = ০.০৩ দেখে থামানো, দুটিই false positive বাড়ায়: প্রথমটি বহু তুলনা, দ্বিতীয়টি পর্ব ৬২-এর peeking. প্রতিকার Holm, আগে ঠিক করা সময়, আর নতুন পরীক্ষায় নিশ্চিত করা।
  6. ঠিক memo. প্রভাবের আকার, interval, p, সময়কাল, অমীমাংসিত metric-এর সৎ বর্ণনা, guardrail আর সিদ্ধান্ত, সঙ্গে কী নজরে রাখা হবে। এক অনুচ্ছেদে পুরো ছবি।

একটি CI, চারটি সম্ভাব্য সিদ্ধান্ত

Reading a confidence interval against zero and a practical threshold

Four confidence intervals and the decisions they support B − A (শতাংশ বিন্দু) ১ · পুরোটা সীমার ওপরে → চালু ২ · ০-এর ওপরে, সীমা অনিশ্চিত ৩ · চওড়া, ০ ভেতরে → অমীমাংসিত ৪ · সরু, ০-এর পাশে → কার্যত নেই −1 0 δ +1.5
ছবি ৮.১ — ধূসর দাগ ০, কমলা দাগ δ = আগে ঠিক করা ব্যবহারিক সীমা। শুধু p < ০.০৫ দেখলে ১ আর ২ এক রকম, ৩ আর ৪ এক রকম মনে হয়; অথচ চারটির সিদ্ধান্ত আলাদা। ৩-এ দরকার আরও ডেটা, ৪-এ “প্রভাব নেই” বলা যায়।

একটি ভালো memo-র কাঠামো: (১) প্রশ্ন আর পরিকল্পনা (প্রধান metric, সময়কাল, ব্যবহারিক সীমা); (২) ডেটার যাচাই (SRM, A/A); (৩) প্রধান ফল: আকার, CI, p, আর যদি CUPED বা winsorising হয়ে থাকে, সেটি আগে থেকে ঠিক করা ছিল কি না; (৪) দ্বিতীয় metric আর guardrail; (৫) অনুসন্ধানমূলক segment, সংশোধিত p-সহ আর “অনুসন্ধান” লেবেলে; (৬) সিদ্ধান্ত আর ঝুঁকি। Ron Kohavi, Diane Tang আর Ya Xu তাঁদের ২০২০ সালের বই Trustworthy Online Controlled Experiments-এ Twyman's law-এর কথা বলেন: যে সংখ্যা খুব আকর্ষণীয় বা অস্বাভাবিক দেখায়, তা সাধারণত কোনো ভুলের ফল। বিস্ময়কর জয় পেলে আগে পাইপলাইন খুঁজুন, তারপর উদ্‌যাপন। পর্ব ৬৭ (Data storytelling — বিশ্লেষণ থেকে সিদ্ধান্ত আর report) এই memo-কে পাঠকের জন্য সাজানো দেখাবে।

আরও গভীরে: CUPED-এর মূল প্রবন্ধ Deng, Xu, Kohavi আর Walker (WSDM ২০১৩); ratio metric-এর জন্য Deng, Knoblich আর Lu (KDD ২০১৮)। Covariate হিসেবে একটি ML model-এর অনুমান ব্যবহার করার ধারণাও প্রকাশিত হয়েছে (কিছু লেখায় CUPAC নামে); শর্ত একই, model-এর input-এ শুধু পরীক্ষার আগের তথ্য। পরীক্ষা চলাকালে বারবার দেখার বৈধ পথ হলো sequential বা always-valid পদ্ধতি (পর্ব ৬২). Heterogeneous প্রভাব খোঁজার আধুনিক পথ causal forest-এর মতো পদ্ধতি, যেখানে আলাদা ডেটায় অনুসন্ধান আর নিশ্চিতকরণ করা হয়।

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

  • z-test-এ pooled হার কেন, CI-তে কেন নয়? Test-এর SE হিসাব হয় H0 সত্য ধরে, যেখানে দুই হার এক; CI H0 ধরে না, তাই নিজ নিজ হার।
  • 2×2 টেবিলের chi-square আর two-proportion z-test-এর সম্পর্ক? Continuity correction ছাড়া χ² = z², ১ df, একই p.
  • Revenue per order-এ অর্ডারকে সারি ধরলে কী ভুল? Randomisation-এর একক দর্শক; একজনের অর্ডারগুলো correlated, SE ছোট দেখায়। Delta method বা দর্শক ধরে bootstrap.
  • CUPED-এ θ কত আর variance কতটা কমে? θ = Cov(X, Y)/Var(X); variance কমে ρ² ভাগ। X অবশ্যই পরীক্ষার আগের।
  • p = ০.২১ দেখে “প্রভাব নেই” বলা যায়? না; CI দেখুন। CI-তে কাজের মতো বড় মান থাকলে ফল অমীমাংসিত।

৯ · AI-এর যুগে — A/B বিশ্লেষণে AI-এর কাজ যাচাই

AI assistant A/B বিশ্লেষণের কোড দ্রুত লেখে, আর প্রায় সবসময় একটি p-value দেয়। কিন্তু ভুল একক, ভুল test আর ভুল বাক্যও সমান আত্মবিশ্বাসে আসে। পরিচিত ভুলগুলো:

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
অর্ডার-স্তরের টেবিলে ttest_ind চালায়Randomisation-এর একক দর্শক; SE ছোট দেখায়দর্শক-স্তরে গুটিয়ে delta method বা bootstrap
“p = ০.০৩, তাই ৯৭% সম্ভাবনায় B ভালো” লেখেp-value-এর সংজ্ঞা উল্টে দেয়বাক্যটি H0 ধরে লিখে দেখুন; Bayesian দাবি হলে prior জিজ্ঞেস করুন
পরীক্ষার সময়ের চলক দিয়ে CUPED করেTreatment-এর প্রভাব covariate-এ ঢোকে, অনুমান বিকৃত হয়X-এর সময়কাল পরীক্ষার আগে কি না যাচাই
সব segment-এ test চালিয়ে ছোট p-গুলো “insight” হিসেবে দেয়বহু তুলনা আর winner's curseকতগুলো test হলো গুনুন, Holm বা BH, নতুন পরীক্ষায় নিশ্চিতকরণ
Outlier “পরিষ্কার” করতে বড় অর্ডার মুছে দেয়আয়ের আসল অংশ ফেলে দেয়, প্রশ্ন বদলায়আগে ঠিক করা winsorising সীমা, মূল আর সংশোধিত দুটি ফলই রিপোর্ট
পুরোনো বা বানানো API দেয়ভুল argument নীরবে ভুল ফল দিতে পারেনিজের version-এর docs; ছোট উদাহরণে হাতে হিসাবের সঙ্গে মেলানো

AI-কে শুধু “A/B test analyse করো” না বলে, পরিকল্পনা, একক আর যাচাই চাইতে বলুন:

আমার কাছে একটি শেষ হওয়া A/B test-এর দর্শক-স্তরের ডেটা আছে (কলাম: group, converted, orders, revenue, pre_period_spend). Randomisation দর্শক ধরে, পরিকল্পনায় প্রধান metric conversion. ১. প্রথমে sample ratio mismatch-এর chi-square test চালাও আর ফল দেখাও; না মিললে থামো। ২. Conversion-এর জন্য pooled z-test, Wald CI আর continuity correction ছাড়া chi-square দাও, আর দেখাও χ² = z². ৩. Revenue per visitor-এ Welch t-test আর দর্শক ধরে bootstrap CI পাশাপাশি দাও। ৪. Revenue per order-এর SE delta method দিয়ে দর্শক-স্তরে হিসাব করো; অর্ডারকে আলাদা সারি ধোরো না। ৫. CUPED করলে শুধু pre_period_spend নাও, θ আর variance কমা ছাপো, আর ρ²-এর সঙ্গে মেলাও। ৬. Segment ফল দিলে Holm-সংশোধিত p দাও আর সেগুলোকে "অনুসন্ধান" লেবেল দাও। ৭. প্রতিটি function আমার statsmodels আর SciPy version-এ আছে কি না যাচাইয়ের পথ বলো, seed নির্দিষ্ট রাখো। ৮. শেষে লেখো কোন দাবিগুলো এই ফল থেকে করা যায় না।

AI-এর দেওয়া A/B বিশ্লেষণ হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১p-value নিয়ে এত ভুল ব্যাখ্যা কেন হয়, আর ২০১৬ সালে American Statistical Association-কে আনুষ্ঠানিক বিবৃতি দিতে হলো কেন?

p-value-এর ধারণাটি Ronald Fisher ১৯২০-এর দশকে জনপ্রিয় করেন একটি নমনীয় প্রমাণের মাপ হিসেবে: ছোট p মানে “আরও খতিয়ে দেখার মতো”. প্রায় একই সময়ে Jerzy Neyman আর Egon Pearson একটি ভিন্ন কাঠামো দেন: আগে থেকে α ঠিক করে দীর্ঘমেয়াদে ভুলের হার নিয়ন্ত্রণ, যেখানে সিদ্ধান্ত হ্যাঁ বা না। পাঠ্যবইগুলো পরে দুটিকে মিশিয়ে ফেলে, আর “p < ০.০৫ মানে সত্য” একটি যান্ত্রিক নিয়মে পরিণত হয়। দুই পক্ষের মূল লেখকেরা নিজেরাই এ নিয়ে তর্ক করেছেন।

ভুলের মনস্তাত্ত্বিক কারণও আছে। মানুষ যা জানতে চায় তা হলো “আমার ধারণা সত্য হওয়ার সম্ভাবনা কত”, অর্থাৎ P(H | ডেটা). p-value দেয় উল্টো দিকের একটি অংশ, P(এত চরম ডেটা | H0). দুটিকে গুলিয়ে ফেলা হলো আদালতের পরিচিত “prosecutor's fallacy”: “নির্দোষ হলে এমন প্রমাণ পাওয়ার সম্ভাবনা ১%” মানে “নির্দোষ হওয়ার সম্ভাবনা ১%” নয়। ধাপ ৪-এর হিসাবই দেখায়: ধারণাগুলোর মাত্র ১০% সত্য হলে significant ফলের ৩৬% মিথ্যা হতে পারে।

মনোবিজ্ঞান, চিকিৎসা আর অন্য কিছু ক্ষেত্রে ২০১০-এর দশকে “replication crisis” দেখা দেয়: বহু প্রকাশিত ফল নতুন পরীক্ষায় টেকেনি। কারণ হিসেবে p-hacking, forking paths, কম power আর শুধু significant ফল প্রকাশের প্রবণতা আলোচিত হয়। এই প্রেক্ষাপটে ASA ২০১৬ সালে Wasserstein আর Lazar-এর লেখা বিবৃতিতে ছয়টি নীতি দেয়, যার মধ্যে আছে: p-value ধারণাটি সত্য হওয়ার সম্ভাবনা মাপে না, সিদ্ধান্ত শুধু একটি সীমা দিয়ে নেওয়া উচিত নয়, আর পূর্ণ রিপোর্ট ও স্বচ্ছতা দরকার। ২০১৯ সালে একটি সম্পাদকীয়তে কেউ কেউ “statistically significant” শব্দটি বাদ দেওয়ার প্রস্তাব করেন; এ নিয়ে মতভেদ রয়ে গেছে।

A/B test-এ এর ব্যবহারিক অর্থ হলো memo-তে প্রভাবের আকার আর CI প্রথমে, p পরে; আর সিদ্ধান্তের নিয়ম আগে থেকে লেখা।

মূল উপলব্ধি: p-value H0 ধরে ডেটার অস্বাভাবিকতা মাপে; ধারণাটি সত্য কি না, প্রভাব কত বড়, আর কী করা উচিত, সেই উত্তর আসে আকার, অনিশ্চয়তা, পূর্বজ্ঞান আর খরচ মিলিয়ে।

প্র ০২Revenue-এর মতো heavy-tailed metric-এ গড় নাকি median, winsorised গড় নাকি log? কোনটি “সঠিক” metric?

প্রশ্নটি পরিসংখ্যানের আগে ব্যবসার। বাজারঘরের মালিক শেষ পর্যন্ত মোট আয় চান, আর মোট আয় = দর্শক × গড় আয়। তাই সিদ্ধান্তের metric হিসেবে গড় RPV-এর জোর যুক্তি আছে, যত গোলমেলেই হোক। Median এখানে প্রায় অর্থহীন: ৯৪%-এর বেশি দর্শকের আয় ০, তাই দুই দলেরই median ০. ক্রেতাদের মধ্যে median নিলে প্রশ্নটি হয় “সাধারণ ক্রেতার অর্ডার”, যা মোট আয়ের প্রশ্ন নয়।

Winsorising নামটি জীববিজ্ঞানী ও পরিসংখ্যানবিদ Charles Winsor-এর নামে, আর John Tukey-সহ অনেকে robust statistics-এ এর প্রচলন করেন। এটি একটি আপস: variance অনেক কমে (অংশ ৩-এ p ০.০৬৮ থেকে ০.০২১), কিন্তু estimand বদলায়। যদি নতুন checkout বিশেষ করে বড় ক্রেতাদের বেশি কেনায়, winsorised গড় সেই প্রভাবের বড় অংশ কেটে ফেলে; উল্টো দিকে বড় ক্রেতাদের হারালে সেটিও লুকায়। তাই অনেক দল winsorised metric-কে “সংবেদনশীল সংকেত” আর কাঁচা গড়কে “ব্যবসার সত্য” হিসেবে পাশাপাশি রাখে।

Log transform আরও বড় বদল আনে। log-এর গড়ের পার্থক্য মোটামুটি geometric গড়ের অনুপাত, আর ০ থাকলে log(১ + y)-এর মতো কৃত্রিম কৌশল লাগে, যার ফল এককের ওপর নির্ভর করে (টাকা না হাজার টাকা)। Mann–Whitney-র মতো rank test আরেকটি প্রশ্ন করে: এলোমেলো একজন B-দর্শক এলোমেলো একজন A-দর্শকের চেয়ে বেশি খরচ করার সম্ভাবনা। এটি গড়ের প্রশ্ন নয়, আর গড় বাড়লেও এর ফল উল্টো হতে পারে।

সমাধান তাই একটি নীতি: সিদ্ধান্তের metric আগে বাছুন (সাধারণত গড় RPV বা মোট আয়), সংবেদনশীলতা বাড়াতে CUPED আর আগে ঠিক করা winsorising ব্যবহার করুন, আর দুটি ফল আলাদা হলে কারণ খুঁজুন। বড় নমুনা আর bootstrap দিয়ে কাঁচা গড়ের CI সৎভাবে রিপোর্ট করুন।

মূল উপলব্ধি: প্রতিটি রূপান্তর আলাদা প্রশ্নের উত্তর দেয়; ব্যবসার প্রশ্ন ঠিক করে metric, আর পরিসংখ্যানের কৌশল শুধু সেই metric-কে নির্ভুল করে, প্রশ্ন বদলায় না।

প্র ০৩CUPED কেন পক্ষপাত আনে না, আর কোন পরিস্থিতিতে এটি বিপজ্জনক হয়ে ওঠে?

মূল যুক্তি randomisation-এ। X যদি পরীক্ষার আগের মাপ হয়, তাহলে কোন দর্শক কোন দলে যাবেন তার সঙ্গে X-এর কোনো সম্পর্ক নেই; তাই E[X̄B − X̄A] = ০. CUPED-এর অনুমান = (ȲB − ȲA) − θ(X̄B − X̄A); দ্বিতীয় পদের প্রত্যাশা ০, তাই অনুমানটি যেকোনো নির্দিষ্ট θ-তে unbiased. বাস্তবে θ ডেটা থেকে অনুমান করা হয়, তাই ছোট নমুনায় সামান্য পক্ষপাত আসতে পারে, কিন্তু বড় নমুনায় তা নগণ্য। CUPED আসলে প্রতিটি পরীক্ষায় দৈবক্রমে ঘটা অসম ভাগ (অংশ ৫-এর stepper-এ B-তে বেশি খরচের মানুষ) সংশোধন করে, তাই variance কমে।

এটি পরিসংখ্যানের পুরোনো ধারণা। R. A. Fisher কৃষি পরীক্ষায় covariance analysis (ANCOVA) ব্যবহার করেছিলেন, আর Monte Carlo সিমুলেশনে control variate একই গণিত। David Freedman ২০০৮ সালে randomised পরীক্ষায় regression adjustment-এর সমালোচনা করেন: ছোট নমুনায় পক্ষপাত আর ভুল SE. Winston Lin ২০১৩ সালে দেখান যে দল × covariate interaction আর robust SE নিলে বড় নমুনায় adjustment অন্তত সমান নির্ভুল। Deng ও সহকর্মীরা ২০১৩ সালে এটিকে বিশাল online পরীক্ষার ব্যবহারিক হাতিয়ার বানান।

বিপদ তিনটি জায়গায়। প্রথম, covariate পরীক্ষার সময়ের হলে: নতুন checkout যদি সেশন বাড়ায় আর আপনি সেশন দিয়ে সংশোধন করেন, তাহলে প্রভাবের সেই অংশ মুছে যায়, যা একধরনের “post-treatment bias”. দ্বিতীয়, covariate-এর সময়কাল নিয়ে লুকানো ভুল: “আগের ৮ সপ্তাহ” হিসাবের তারিখ ভুলে পরীক্ষার প্রথম দিন ঢুকে গেলে একই সমস্যা ছোট আকারে। তৃতীয়, forking paths: পাঁচটি covariate আর তিনটি সময়কাল চেষ্টা করে সবচেয়ে ছোট p-ওয়ালাটি বাছলে CUPED সংবেদনশীলতার বদলে p-hacking হয়ে যায়।

তাই নিয়ম: covariate আর তার সময়কাল পরিকল্পনায় লিখুন, শুধু treatment-এর আগের তথ্য নিন, আর কাঁচা ও CUPED দুটি ফলই রাখুন। অংশ ৬-এর simulation এই অভ্যাসটিই দেখায়: A/A-তে false positive-এর হার বাড়েনি।

মূল উপলব্ধি: CUPED-এর নিরাপত্তা আসে randomisation আর “আগের তথ্য” শর্ত থেকে; শর্ত ভাঙলে variance কমানোর হাতিয়ারই পক্ষপাতের উৎস হয়।

প্র ০৪Simpson's paradox-এর বিখ্যাত উদাহরণ কী শেখায়, আর A/B test-এ segment-ভিত্তিক দাবিকে কতটা বিশ্বাস করবেন?

E. H. Simpson ১৯৫১ সালে এই ঘটনাটি নিয়ে লেখেন, যদিও Karl Pearson আর Udny Yule আগেই এর রূপ দেখেছিলেন। সবচেয়ে পরিচিত উদাহরণ University of California, Berkeley-র ১৯৭৩ সালের ভর্তির তথ্য, যা Peter Bickel, Eugene Hammel আর J. W. O'Connell ১৯৭৫ সালে Science-এ বিশ্লেষণ করেন। মোট হিসাবে পুরুষ আবেদনকারীদের ভর্তির হার নারীদের চেয়ে বেশি দেখাচ্ছিল। বিভাগ ধরে দেখলে বেশির ভাগ বিভাগে সেই পার্থক্য প্রায় ছিল না, কিছুতে উল্টো। কারণ নারীরা বেশি আবেদন করেছিলেন এমন বিভাগে, যেখানে সবার জন্যই ভর্তি কঠিন। বিভাগটি ছিল confounder.

শিক্ষাটি হলো, কোন স্তরের সংখ্যা “ঠিক”, তা কারণ-কাঠামো ঠিক করে। ভর্তির প্রশ্নে বিভাগ-ভিত্তিক তুলনা বেশি অর্থপূর্ণ ছিল। আবার এমন ক্ষেত্রও আছে যেখানে স্তরে ভাগ করাই ভুল, যেমন স্তরটি নিজেই treatment-এর ফল (mediator). A/B test-এ ramp-up-এর উদাহরণে দিন হলো confounder, তাই দিন ধরে স্তরে ভাগ করে ওজন দেওয়া সঠিক। Kohavi ও সহকর্মীরা তাঁদের বইয়ে ramp-up-এ এই ফাঁদ নিয়ে সতর্ক করেছেন।

Segment দাবির ক্ষেত্রে আরও দুটি সমস্যা। বহু তুলনা: আটটি segment-এ একটি ছোট p প্রায় প্রত্যাশিত, তাই Holm বা BH. আর power: প্রতিটি segment-এ নমুনা পুরো পরীক্ষার একটি ভগ্নাংশ, তাই আসল পার্থক্যও প্রায়ই ধরা পড়ে না, আর যা ধরা পড়ে তা winner's curse-এ বাড়ানো। অংশ ৭-এর টেবিলে আসল প্রভাব সব জায়গায় সমান, তবু Sylhet mobile দ্বিগুণ আর Sylhet desktop নেতিবাচক দেখাল। segment-ভেদে প্রভাবের পার্থক্য (interaction) পরীক্ষা করতে আলাদা করে অনেক বেশি নমুনা লাগে।

তাই segment ফলকে পরের পরীক্ষার জন্য hypothesis হিসেবে দেখুন: আগে থেকে ঘোষিত segment (যেমন “mobile আলাদা দেখব”) হলে বিশ্বাসযোগ্যতা বেশি, পরে খোঁজা হলে কম। নতুন পরীক্ষায় সেই segment-কেই প্রধান প্রশ্ন বানিয়ে যাচাই করুন।

মূল উপলব্ধি: মোট আর segment-এর সংখ্যা উল্টো কথা বললে কারণ-কাঠামো দেখুন; আর পরে খোঁজা segment-এর জয় নতুন প্রশ্ন, উত্তর নয়।

প্র ০৫অনেক A/B tool এখন p-value-এর বদলে “B ভালো হওয়ার সম্ভাবনা” দেখায়। Bayesian A/B কি frequentist-এর চেয়ে ভালো?

Bayesian পথের আকর্ষণ সহজ ভাষায়। P(B > A) = ০.৯৯৯৪ বাক্যটি ব্যবসায়ী সরাসরি বোঝেন, আর “B বেছে নিলে প্রত্যাশিত ক্ষতি কত” (expected loss) সিদ্ধান্তের ভাষার আরও কাছে। Conversion-এর জন্য Beta-Binomial জোড়া (AI-এর ভিত্তি কোর্সের পর্ব ৫০-এর Bayes হালনাগাদ) এত সহজ যে হিসাব কয়েক লাইনে হয়, যেমন অংশ ২-এর ব্লকে। আগের পরীক্ষাগুলোর অভিজ্ঞতা prior হিসেবে ব্যবহার করা যায়, যা ছোট পরীক্ষায় অতিরঞ্জিত ফলকে সংযত করে।

কিন্তু কয়েকটি দাবি নিয়ে সতর্কতা দরকার। প্রথম, “Bayesian পদ্ধতিতে peeking সমস্যা নেই” কথাটি প্রায়ই অতিরঞ্জিত। Posterior-এর ব্যাখ্যা থামার নিয়মের ওপর নির্ভর না করলেও, “P(B > A) > ০.৯৫ হলেই থামুন” নিয়মে বারবার দেখলে, A আর B আসলে সমান হলেও ভুল জয়ের দীর্ঘমেয়াদি হার বাড়ে; এ নিয়ে পরিসংখ্যানবিদদের মধ্যে আলোচনা চলছে। দ্বিতীয়, prior-এর প্রভাব: সমতল Beta(১, ১) নিরপেক্ষ মনে হলেও, অনেক ধারণার প্রভাব আসলে ছোট, আর সেই তথ্য prior-এ না থাকলে বড় প্রভাবের দাবি সহজে আসে। তৃতীয়, P(B > A) আকার বলে না: ০.৯৯ সম্ভাবনায় B এক পয়সা ভালো হতে পারে।

Frequentist দিকেও উন্নতি হয়েছে: always-valid p-value আর sequential test (পর্ব ৬২) বারবার দেখার সমস্যা সামলায়, আর CI প্রভাবের আকার দেয়। বড় নমুনায় সমতল prior-এর Bayesian interval আর Wald CI সংখ্যায় প্রায় এক হয়ে যায়।

তাই প্রশ্নটি “কোন দর্শন” নয়, “কোন অভ্যাস”: আগে থেকে ঠিক করা থামার নিয়ম, প্রভাবের আকার আর অনিশ্চয়তা, ব্যবহারিক সীমা, আর একাধিক তুলনার হিসাব। Tool যে সংখ্যাই দেখাক, তার পেছনের prior, থামার নিয়ম আর metric-এর সংজ্ঞা জানতে চান।

মূল উপলব্ধি: Bayesian A/B সিদ্ধান্তের ভাষায় কথা বলে, কিন্তু prior আর থামার নিয়ম প্রকাশ্য অনুমান; ভালো বিশ্লেষণ দুই পথেই আকার, অনিশ্চয়তা আর পূর্বঘোষিত নিয়মের ওপর দাঁড়ায়।

অনুশীলন

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

  1. পূর্ণ numerical: (ক) দল A-তে ৬,০০০ দর্শকের ৩০০ জন কিনেছেন, দল B-তে ৬,০০০-এর ৩৬০ জন। Pooled z, দুই দিকের p (z = ২.৪০-এ এক দিকের লেজ ≈ ০.০০৮২) আর ৯৫% Wald CI বের করুন। (খ) একটি revenue metric-এর variance ৪০০, আর পরীক্ষার আগের খরচের সঙ্গে correlation ρ = ০.৬. CUPED-এর পর variance কত, আর একই নির্ভুলতার জন্য দর্শক কত ভাগ কম লাগবে?

    (ক) p̂A = ০.০৫০, p̂B = ০.০৬০, d̂ = ০.০১০. Pooled p̂ = ৬৬০/১২,০০০ = ০.০৫৫. SE0 = √[০.০৫৫ × ০.৯৪৫ × (২/৬,০০০)] = √০.০০০০১৭৩২৫ ≈ ০.০০৪১৬২. z = ০.০১০/০.০০৪১৬২ ≈ ২.৪০, p ≈ ২ × ০.০০৮২ = ০.০১৬. Wald SE = √[(০.০৪৭৫ + ০.০৫৬৪)/৬,০০০] ≈ ০.০০৪১৬১, তাই CI = ০.০১০ ± ০.০০৮১৬ = [০.০০১৮, ০.০১৮২], অর্থাৎ +০.১৮ থেকে +১.৮২ শতাংশ বিন্দু। (খ) Variance = ৪০০ × (১ − ০.৩৬) = ২৫৬. SE-র বর্গ variance/n-এর সমানুপাতিক, তাই একই SE-র জন্য n-কে ০.৬৪ গুণ করলেই চলে: প্রায় ৩৬% কম দর্শক।

  2. সমালোচনা ও নকশা: একজন বিশ্লেষক (বা একটি AI assistant) লিখেছেন: “নতুন checkout পরীক্ষা ৫ দিনে থামিয়েছি, কারণ তখন p = ০.০৪ হয়েছিল। Revenue per order-এর জন্য অর্ডার-স্তরের টেবিলে t-test করে p = ০.০০৯ পেয়েছি। Revenue per visitor-এ p = ০.৪, তাই আয়ে কোনো প্রভাব নেই। CUPED-এর জন্য পরীক্ষা চলাকালের সেশন সংখ্যা ব্যবহার করেছি। ১২টি জেলা দেখে Rangpur-এ +৬০% পেয়েছি, তাই Rangpur-এ আগে চালু করছি।” কী কী সমস্যা, আর সঠিক বিশ্লেষণ কী?

    সমস্যা: (১) p = ০.০৪ দেখে থামানো peeking, false positive-এর হার ৫%-এর বেশি; আর ৫ দিনে সপ্তাহের পুরো চক্রও ধরা পড়েনি। (২) অর্ডার-স্তরের t-test-এ একক ভুল: একজনের অর্ডারগুলো correlated, তাই SE ছোট আর p আশাবাদী। (৩) p = ০.৪ মানে “প্রভাব নেই” নয়; CI না দেখে বলা যায় না। (৪) সেশন পরীক্ষার সময়ের চলক, যা নতুন checkout বদলাতে পারে; CUPED-এর covariate হিসেবে এটি প্রভাব মুছে দিতে পারে। (৫) ১২টি জেলার সবচেয়ে বড়টি বাছা: বহু তুলনা আর winner's curse. সঠিক বিশ্লেষণ: আগে ঠিক করা পুরো সময় (অন্তত পুরো সপ্তাহ) চালানো; SRM যাচাই; conversion-এর z-test আর CI; RPV-এ Welch আর bootstrap, পরিকল্পনায় থাকলে আগের খরচ দিয়ে CUPED; RPO-এর SE delta method বা দর্শক ধরে bootstrap দিয়ে; জেলাগুলো Holm বা BH-সহ “অনুসন্ধান” হিসেবে, আর Rangpur নিয়ে আলাদা নিশ্চিতকরণ পরীক্ষা।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    import numpy as np
    from scipy import stats
    t = np.array([[50, 950], [70, 930]])
    chi2, p, dof, exp = stats.chi2_contingency(t, correction=False)
    print(round(chi2, 3), dof)
    print(exp)
    3.546 1
    [[ 60. 940.]
     [ 60. 940.]]

    মোট ক্রেতা ১২০, মোট দর্শক ২,০০০, তাই pooled হার ০.০৬. প্রত্যাশিত গণনা: প্রতি সারিতে ১,০০০ × ০.০৬ = ৬০ ক্রেতা আর ৯৪০ না-ক্রেতা। χ² = ∑(দেখা − প্রত্যাশিত)²/প্রত্যাশিত = ১০০/৬০ + ১০০/৯৪০ + ১০০/৬০ + ১০০/৯৪০ = ৩.৩৩৩ + ০.২১৩ = ৩.৫৪৬. 2×2 টেবিলে df = (২ − ১)(২ − ১) = ১. এটি z = −১.৮৮৩-এর বর্গ (৫% বনাম ৭%), p ≈ ০.০৬. NumPy প্রত্যাশিত মানগুলো দশমিক বিন্দুসহ float হিসেবে ছাপে।

  4. Simpson আর stratified হিসাব: একটি পরীক্ষায় mobile: A ৮,০০০ জনে ২৪০ ক্রেতা, B ২,০০০ জনে ৭০; desktop: A ২,০০০ জনে ১৬০, B ৮,০০০ জনে ৬৮০ (ভাগের অনুপাত device-ভেদে আলাদা, একটি bug-এর কারণে)। প্রতিটি device-এ আর মোটে কে ভালো? দুই device-এর মোট দর্শক দিয়ে ওজন দিয়ে পার্থক্য কত? Bug-টি আর কী সন্দেহ জাগায়?

    Mobile: A ২৪০/৮,০০০ = ৩.০%, B ৭০/২,০০০ = ৩.৫%; desktop: A ১৬০/২,০০০ = ৮.০%, B ৬৮০/৮,০০০ = ৮.৫%. দুটিতেই B +০.৫ শতাংশ বিন্দু এগিয়ে। মোটে A = ৪০০/১০,০০০ = ৪.০%, B = ৭৫০/১০,০০০ = ৭.৫%: এখানে দিক উল্টায়নি, কিন্তু মোট পার্থক্য +৩.৫ শতাংশ বিন্দু, যা প্রতিটি device-এর সাত গুণ, কারণ B-তে বেশি-কেনা desktop দর্শক বেশি। ওজন দিয়ে: প্রতিটি device-এ মোট দর্শক ১০,০০০, তাই (১০,০০০ × ০.৫ + ১০,০০০ × ০.৫)/২০,০০০ = +০.৫ শতাংশ বিন্দু. Simpson's paradox-এর আসল রূপ হলো মিশ্রণের অসমতা, দিক উল্টানো তার একটি চরম ফল। Bug-এর কারণে device-ভেদে ভাগ অসম মানে sample ratio mismatch: অন্য কোনো চলকেও (জেলা, নতুন দর্শক) ভাগ বিকৃত হতে পারে, তাই stratified হিসাবও পুরো সমাধান নয়; bug ঠিক করে আবার চালানোই নিরাপদ।

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

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

ক্রমশেষ হওয়া একটি A/B test বিশ্লেষণের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. পরিকল্পনা খুলে প্রধান metric, সময়কাল আর ব্যবহারিক সীমা মিলিয়ে নেওয়া
  2. Sample ratio mismatch আর ডেটার পাইপলাইন যাচাই করা
  3. প্রধান metric-এর প্রভাবের আকার, CI আর p বের করা (পরিকল্পনায় থাকলে CUPED-সহ)
  4. দ্বিতীয় metric আর guardrail দেখা
  5. অনুসন্ধানমূলক segment, Holm বা BH-সংশোধিত p-সহ
  6. আকার, অনিশ্চয়তা আর সিদ্ধান্তসহ memo লেখা

দল A-তে ৪,০০০ দর্শকের ২০০ জন, দল B-তে ৪,০০০-এর ২৬০ জন কিনেছেন। Pooled two-proportion z-test-এর z কত?

  • প্রায় ২.৮৮
  • প্রায় ১.৪৪
  • ০.০১৫
  • প্রায় ৮.৩

একটি A/B test-এ conversion-এর পার্থক্যের p = ০.০৩. কোন বাক্যটি সঠিক?

  • H0 সত্য হওয়ার সম্ভাবনা ৩%
  • B ভালো হওয়ার সম্ভাবনা ৯৭%
  • কোনো পার্থক্য না থাকলে এত বড় বা আরও বড় পার্থক্য দেখার সম্ভাবনা ৩%
  • প্রভাবটি ব্যবসার জন্য যথেষ্ট বড়

Revenue per visitor আর পরীক্ষার আগের খরচের correlation ρ = ০.৫. CUPED-এ সর্বোত্তম θ নিলে metric-এর variance প্রায় কত ভাগ কমে?

  • ৫০%
  • ২৫%
  • ১৩%
  • কমে না

দর্শক ধরে randomise করা পরীক্ষায় revenue per order-এর SE অর্ডার-স্তরের টেবিল থেকে সাধারণ সূত্রে হিসাব করা হলো। প্রধান সমস্যা কী?

  • Revenue-র বণ্টন normal নয়
  • অর্ডারের সংখ্যা বেশি, তাই power কম
  • Welch-এর বদলে সাধারণ t-test ব্যবহার হয়েছে
  • একই দর্শকের অর্ডারগুলো স্বাধীন নয়, তাই SE ছোট দেখায়

Ramp-up-এ দিন ১-এ B পেয়েছে ১০% দর্শক, দিন ২-এ ৫০%. দুই দিনেই B-র conversion বেশি, কিন্তু দুই দিন যোগ করলে A-র বেশি। সবচেয়ে সম্ভাব্য কারণ কী?

  • দুই দিনে কেনার হার আলাদা, আর দুই দলে দিনের মিশ্রণ অসম
  • মোট হিসাবে গণনার ভুল হয়েছে
  • নমুনা ছোট, তাই দৈবক্রমে উল্টেছে
  • B আসলে খারাপ, দিনভিত্তিক সংখ্যাগুলো ভুল

Revenue per visitor-এ পার্থক্য +৫.৫ টাকা, ৯৫% CI [−৩.১, +১৪.১], p = ০.২১; আগে ঠিক করা ব্যবহারিক সীমা +৩ টাকা। সবচেয়ে সঠিক সিদ্ধান্ত কোনটি?

  • B-র আয়ে কোনো প্রভাব নেই
  • B আয় বাড়িয়েছে, কারণ গড় পার্থক্য ধনাত্মক
  • ফল অমীমাংসিত; বেশি ডেটা বা variance কমানোর পদ্ধতি দরকার
  • ফল দেখে winsorising করে আবার test করা উচিত

শব্দকোষ

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

মনে রাখুন

  1. Conversion: pooled z = d̂/√[p̂(1 − p̂)(1/nA + 1/nB)]; CI-তে নিজ নিজ হার; 2×2 chi-square (correction ছাড়া) = z².
  2. Revenue: heavy tail-এ Welch আর bootstrap পাশাপাশি; winsorising আর log প্রশ্ন বদলায়, তাই আগে থেকে ঠিক করুন।
  3. Ratio metric: একক = randomisation-এর একক। Var(R) ≈ [σY² − 2rσXY + r²σX²]/(nμX²); অর্ডার-স্তরের naive SE ছোট দেখায়।
  4. CUPED: Y − θ(X − X̄), θ = Cov(X, Y)/Var(X), variance কমে ρ² ভাগ; X শুধু পরীক্ষার আগের। False positive একই, power বেশি।
  5. ব্যাখ্যা: p ≠ P(H0 সত্য); not significant ≠ প্রভাব নেই; segment-এ Holm আর winner's curse; Simpson-এ মিশ্রণ দেখুন; memo-তে আকার, CI আর সিদ্ধান্ত।

পরের পর্বে: পর্ব ৬৪ (Causal inference-এর পরিচয় — DAG, matching আর difference-in-differences): randomise করা যায় না এমন পরিস্থিতিতে, যেমন কিছু জেলায় ডেলিভারি ফি বদল, প্রভাব কীভাবে অনুমান করবেন, আর এই পর্বের Simpson's paradox কেন কারণ-কাঠামো ছাড়া মেটে না।

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

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