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

Customer analytics — RFM, retention, churn আর lifetime value

Customer Analytics — RFM Segmentation, Cohort Retention, Churn Prediction, Uplift Modelling and Customer Lifetime Value

Taught in Bangla, this lesson uses 2,000 simulated shop customers to build RFM scores and segments, cohort retention tables, churn labels for a non-contractual business and a time-split churn model that exposes target leakage. It compares response and T-learner uplift on a randomised coupon test and checks CLV from historical averages, cohort curves and BG/NBD with Gamma-Gamma against the simulated truth, from real runs.

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

  • লেনদেনের টেবিল থেকে RFM score, নামসহ segment, তাদের আয়ের ভাগ আর প্রতিটির জন্য পদক্ষেপ ঠিক করা
  • যোগদানের ত্রৈমাসিক ধরে cohort retention টেবিল আর বক্ররেখা বানানো ও পড়া
  • চুক্তিহীন ব্যবসায় churn-এর সংজ্ঞা বেছে নেওয়া, আর observation window ও label window মেনে leakage-মুক্ত churn model বানানো
  • এলোমেলো কুপন-পরীক্ষা থেকে T-learner দিয়ে uplift মাপা, আর কেন response model ভুল মানুষকে বাছে তা দেখা
  • Discount-সহ CLV হাতে গোনা, আর ইতিহাসের গড়, cohort বক্ররেখা ও BG/NBD + Gamma-Gamma-কে simulation-এর সত্যের সঙ্গে মেলানো

পর্ব ৬১ (Data science-এর জন্য SQL — window function, cohort আর funnel) ডেটা টেনে আনার কৌশল দেখায়, পর্ব ৬২–৬৪ পরীক্ষা আর কারণ-ফলের প্রশ্ন। এই পর্বে সব একসঙ্গে লাগে দোকানের ক্রেতার ওপর। ভিত্তি চারটি: পর্ব ০২-এর prediction time আর label window, পর্ব ৫৫-এর censoring আর survival, AI-এর ভিত্তি কোর্সের পর্ব ৫৬-এর leakage, আর একই কোর্সের পর্ব ৬৫-এ k-means দিয়ে RFM ভাগ করার প্রথম দেখা। চারটি চালানো যায় এমন ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy, pandas, scikit-learn আর matplotlib); প্রথমবার package নামতে কয়েক সেকেন্ড, scikit-learn-এ প্রায় দশ সেকেন্ড লাগে।

১ · Customer analytics-এর চারটি প্রশ্ন আর বাজারঘরের লেনদেন

পাড়ার মুদি দোকানের মালিক খাতা না খুলেই জানেন: কোন খালাম্মা প্রতি সপ্তাহে আসেন, কোন ভাই ঈদের আগে একবার বড় বাজার করেন, আর কে তিন মাস ধরে আসছেন না। তিনি নিয়মিতদের জন্য ভালো চাল তুলে রাখেন, আর অনেক দিন না আসা কাউকে দেখলে একটু বেশি খাতির করেন। Customer analytics এই অভিজ্ঞ চোখকে হাজার হাজার ক্রেতার জন্য সংখ্যায় লেখার চেষ্টা।

Customer analytics-এর কাজ মূলত চারটি প্রশ্নের উত্তর: (১) কে কেমন? ক্রেতাদের অর্থপূর্ণ দলে ভাগ (segmentation). (২) কতজন ফিরছেন? যোগদানের দল ধরে সময়ের সঙ্গে ধরে রাখার হার (retention). (৩) কে চলে যাচ্ছেন, আর কাকে ছোঁয়া উচিত? Churn-এর ঝুঁকি আর হস্তক্ষেপের প্রভাব (uplift). (৪) একজন ক্রেতার দাম কত? ভবিষ্যতের আয়ের বর্তমান মূল্য (customer lifetime value, CLV). চারটিই একই কাঁচামাল থেকে আসে: লেনদেনের টেবিল, যেখানে প্রতি সারিতে ক্রেতা, তারিখ আর টাকা।

নিচের module ২০০০ জন কাল্পনিক বাজারঘর ক্রেতার ১ জুলাই ২০২৪ থেকে ৩১ ডিসেম্বর ২০২৫, অর্থাৎ ১৮ মাসের লেনদেন বানায়। প্রত্যেকের নিজস্ব কেনার হার আর অর্ডারের গড় টাকা আছে, আর প্রতিটি অর্ডারের পরে নিজস্ব সম্ভাবনায় তিনি চুপচাপ চলে যান। ১ অক্টোবর ২০২৫-এ এলোমেলো অর্ধেক ক্রেতা একটি কুপন পান (treat). Simulation ডেটা কাটার দিনের পরেও এক বছর চলে: TX_ALL-এ সেই “ভবিষ্যৎ”, যা বাস্তবে পাওয়া যায় না। বিশ্লেষণ শুধু tx (৩১ ডিসেম্বর পর্যন্ত) দেখে; ভবিষ্যৎ শুধু পদ্ধতির ভুল মাপতে।

# বাজারঘর (কাল্পনিক দোকান): ২০০০ ক্রেতার ১৮ মাসের লেনদেন, seeded
import numpy as np
import pandas as pd

START, CUT = pd.Timestamp("2024-07-01"), pd.Timestamp("2025-12-31")
EXP = pd.Timestamp("2025-10-01")         # কুপন পরীক্ষার দিন
HORIZON = 365                            # CUT-এর পরের "সত্য" (শুধু simulation-এ)

def simulate(n=2000, seed=65):
    rng = np.random.default_rng(seed)
    end = (CUT - START).days + HORIZON
    signup = rng.integers(0, (CUT - START).days, n)      # START থেকে দিন
    lam = rng.gamma(1.5, 0.02, n)       # দিনে কেনার হার (গড় ০.০৩)
    p = rng.beta(1.5, 8.0, n)           # প্রতি অর্ডারের পর চলে যাওয়ার সম্ভাবনা
    nu = rng.gamma(4.0, 1 / 400, n)     # খরচের rate: গড় খরচ 6/nu টাকা
    treat = rng.binomial(1, 0.5, n)     # কুপন পাবেন কি না (এলোমেলো)
    rows, e = [], (EXP - START).days
    for i in range(n):
        t, days, gone = signup[i], [], False
        while t < end:
            days.append(t)
            if rng.uniform() < p[i]:
                gone = True
                break                   # চুপচাপ চলে গেলেন
            t = t + rng.exponential(1 / lam[i])
        days = np.floor(days).astype(np.int64)
        alive = not gone or days[-1] >= e
        rec = e - days[days < e].max() if signup[i] < e - 30 else -1
        win = np.any((days >= e) & (days < e + 30))
        if treat[i] and rec > 0 and not win:     # কুপনের প্রভাব
            u = 0.45 * np.exp(-((rec - 110) / 70) ** 2) if alive else 0.03
            if rng.uniform() < u:
                days = np.sort(np.r_[days, e + rng.integers(0, 30)])
        amt = rng.gamma(6.0, 1 / nu[i], days.size).round()
        rows.append((np.full(days.size, i), days, amt))
    c, d, a = (np.concatenate(z) for z in zip(*rows))
    tx = pd.DataFrame({"customer": c, "day": d, "amount": a})
    tx["date"] = START + pd.to_timedelta(tx.day, unit="D")
    cust = pd.DataFrame({"signup": START + pd.to_timedelta(signup, unit="D"),
                         "treat": treat, "lam": lam, "p": p})
    return tx, cust

TX_ALL, CUST = simulate()
tx = TX_ALL[TX_ALL.date <= CUT][["customer", "date", "amount"]]   # যা দেখা যায়

def future_revenue(months=12, d=0.01):
    """সত্য: CUT-এর পরের ১২ মাসের আয়, মাসিক d হারে discount করা"""
    f = TX_ALL[(TX_ALL.date > CUT) & (TX_ALL.day <= (CUT - START).days + 30 * months)]
    k = (f.day - (CUT - START).days - 1) // 30 + 1          # ১, ২, …, ১২ মাস
    return (f.amount / (1 + d) ** k).groupby(f.customer).sum().reindex(CUST.index, fill_value=0)

এই ছাঁদের নাম চুক্তিহীন (non-contractual) ব্যবসা: মোবাইল প্যাকেজ বা ইন্টারনেট সংযোগের মতো কোনো চুক্তি বাতিলের দিন নেই। ক্রেতা চলে গেলে তা দেখা যায় না, শুধু অর্ডার আর আসে না। এই অদৃশ্যতাই পরের সব অংশের মূল সমস্যা।

২ · RFM — তিনটি সংখ্যায় ক্রেতা চেনা

RFM সবচেয়ে পুরোনো আর এখনো সবচেয়ে বেশি ব্যবহৃত segmentation-গুলোর একটি। তিনটি সংখ্যা, সবই একটি নির্দিষ্ট তারিখে (এখানে ৩১ ডিসেম্বর ২০২৫) মাপা:

  • Recency (R): শেষ অর্ডারের পর কত দিন। কম মানে ভালো।
  • Frequency (F): মোট কতটি অর্ডার।
  • Monetary (M): মোট কত টাকা (কেউ কেউ অর্ডারপ্রতি গড় নেন)।

প্রতিটি সংখ্যাকে ৫টি সমান ভাগে (quintile) ভেঙে ১ থেকে ৫ score দেওয়া হয়: সবচেয়ে সাম্প্রতিক ২০% পান R = ৫, সবচেয়ে বেশি অর্ডারের ২০% পান F = ৫. তারপর score-এর জোড়া থেকে নামসহ segment. নিয়মটি এই পর্বে R আর F দিয়ে: Champions (R ≥ ৪, F ≥ ৪), Loyal (R ≥ ৩, F ≥ ৩), New (R ≥ ৪, F ≤ ২), At risk (R ≤ ২, F ≥ ৩), Lost (R ≤ ২, F ≤ ২), বাকিরা Need attention. নিয়মগুলো ক্রমে পরীক্ষা হয়; প্রথম যে নিয়ম খাটে, সেটিই segment. এই সীমাগুলো কোনো মানদণ্ড নয়, ব্যবসার আলোচনায় ঠিক হয়।

Quintile সীমা (২০০০ ক্রেতা থেকে): R দিন 0–22→5, 23–66→4, 67–144→3, 145–302→2, 303+→1 · F অর্ডার 1→1, 2→2, 3–4→3, 5–7→4, 8+→5 · M টাকা ≤815→1, ≤1483→2, ≤2620→3, ≤5110→4, তার বেশি→5
এখন দেখা হচ্ছে
রহিমা
কামাল
সুমি
  1. কাঁচা সংখ্যা। তিনজনের recency, frequency আর মোট খরচ। উপরে ২০০০ ক্রেতার quintile সীমা, যা কোড থেকে নেওয়া।
  2. R. Recency-তে কম মানে ভালো, তাই উল্টো ক্রম: রহিমা ১০ দিনে R = ৫, সুমি ৪০ দিনে R = ৪, কামাল ২০০ দিনে R = ২.
  3. F. রহিমার ৯ অর্ডার, F = ৫; কামালের ৬, F = ৪; সুমির ১, F = ১. সাবধান: ৫০১ জন ক্রেতার ঠিক ১টি অর্ডার, যা ২০%-এর (৪০০) বেশি। কোড rank দিয়ে এই tie ভাঙে, তাই একই আচরণের ১০১ জন F = ২ পান।
  4. M. ৳৭,৮০০ পঞ্চম ভাগে (M = ৫), ৳৪,১০০ চতুর্থে (M = ৪), ৳৬৫০ প্রথমে (M = ১).
  5. Segment. রহিমা R৫ F৫: Champions. কামাল R২ F৪: প্রথম চার নিয়মের মধ্যে “R ≤ ২, F ≥ ৩” খাটে, তাই At risk. সুমি R৪ F১: New. একই M = ৪ হলেও কামাল আর একজন Loyal ক্রেতার পদক্ষেপ সম্পূর্ণ আলাদা।

এবার পুরো ২০০০ জনে। প্রথম ব্লক তিনটি সংখ্যা গোনে, quintile score দেয়, segment বসায়, আর প্রতিটি segment-এর আকার, median recency, গড় অর্ডার আর আয়ের ভাগ ছাপায়। এটি প্রথমবার pandas আনে, তাই কয়েক সেকেন্ড লাগতে পারে।

import numpy as np
import pandas as pd
from bazar_cust import tx, CUT

pd.set_option("display.width", 72)
print(tx.head(3))
g = tx.groupby("customer")
rfm = pd.DataFrame({"recency": (CUT - g.date.max()).dt.days,
                    "frequency": g.size(),
                    "monetary": g.amount.sum()})
def score(s, higher_better=True):         # quintile: 1 থেকে 5
    r = s.rank(method="first", ascending=higher_better)   # tie ভাঙা
    return pd.qcut(r, 5, labels=[1, 2, 3, 4, 5]).astype(int)
rfm["R"] = score(rfm.recency, higher_better=False)
rfm["F"] = score(rfm.frequency)
rfm["M"] = score(rfm.monetary)
print(rfm.head(3))
R, F = rfm.R, rfm.F
rfm["segment"] = np.select(
    [(R >= 4) & (F >= 4), (R >= 3) & (F >= 3), (R >= 4) & (F <= 2),
     (R <= 2) & (F >= 3), (R <= 2) & (F <= 2)],
    ["Champions", "Loyal", "New", "At risk", "Lost"], "Need attention")
seg = rfm.groupby("segment").agg(n=("R", "size"), recency=("recency", "median"),
                                  orders=("frequency", "mean"),
                                  revenue=("monetary", "sum"))
seg["cust %"] = (100 * seg.n / seg.n.sum()).round(1)
seg["rev %"] = (100 * seg.revenue / seg.revenue.sum()).round(1)
print(seg.drop(columns="revenue").sort_values("rev %", ascending=False)
      .round(1))

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

   customer       date  amount
0         0 2024-10-04   807.0
1         0 2024-11-19   663.0
2         0 2025-01-14  1631.0
          recency  frequency  monetary  R  F  M
customer                                       
0             157         11    9956.0  2  5  5
1              81          5    2706.0  3  4  4
2             129          2    1860.0  3  2  3
                  n  recency  orders  cust %  rev %
segment                                            
Champions       402     17.0     9.4    20.1   44.0
At risk         397    286.0     5.4    19.8   23.6
Loyal           401     81.0     4.9    20.0   19.8
Lost            403    336.0     1.3    20.2    5.8
New             251     24.0     1.4    12.6    4.2
Need attention  146    104.5     1.4     7.3    2.6

প্রথম তিন সারি লেনদেনের কাঁচামাল: ক্রেতা ০-এর তিনটি অর্ডার। RFM টেবিলে ক্রেতা ০-এর ১১টি অর্ডার আর প্রায় ১০ হাজার টাকা (F৫, M৫), কিন্তু শেষ অর্ডার ১৫৭ দিন আগে (R২): এমন মানুষই সবচেয়ে দামি সতর্কসংকেত। ক্রেতা ২-এর ২টি অর্ডার পেয়েছে F = ২, অথচ quintile সীমায় ২ পড়ে দুই ভাগের মাঝে; tie ভাঙার নিয়মই এখানে ঠিক করেছে। Segment টেবিল Pareto-ধাঁচের ছবি দেখায়: ২০.১% Champions আয়ের ৪৪.০% এনেছেন, median recency ১৭ দিন, গড়ে ৯.৪টি অর্ডার। At risk দল (১৯.৮%) এনেছে আরও ২৩.৬%, কিন্তু median recency ২৮৬ দিন: অতীতে ভালো ক্রেতা, এখন নিঃশব্দ। Lost দলে ২০.২% মানুষ, আয়ের মাত্র ৫.৮%. দলের আকার প্রায় সমান, কারণ quintile নিজেই ২০% করে কাটে।

Segmentকী বোঝায়প্রস্তাবিত পদক্ষেপ (পরীক্ষা করে যাচাইযোগ্য)
Championsসম্প্রতি, বারবার কেনেনছাড় নয়; আগে নতুন পণ্য দেখা, দ্রুত ডেলিভারি, মতামত চাওয়া
Loyalনিয়মিত, তবে সর্বোচ্চ ননপাশের শ্রেণির পণ্য সুপারিশ (পর্ব ৫৩)
Newসম্প্রতি প্রথম বা দ্বিতীয় অর্ডারদ্বিতীয় অর্ডারের পথ সহজ করা; ডেলিভারির অভিজ্ঞতা ঠিক রাখা
At riskঅতীতে ভালো, এখন অনেক দিন নেইকারণ খোঁজা (অভিযোগ, দাম); এখানেই uplift পরীক্ষার সবচেয়ে বেশি সুযোগ
Need attentionমাঝারি recency, কম অর্ডারসস্তা বার্তা, প্রয়োজনীয় পণ্যের মনে করিয়ে দেওয়া
Lostঅনেক দিন আগে একবার-দুবারখরচ কম রাখা; বড় ছাড় সাধারণত লাভজনক নয়

RFM বনাম clustering: k-means (AI-এর ভিত্তি কোর্সের পর্ব ৬৫) ডেটার নিজস্ব গুচ্ছ খোঁজে, কিন্তু নতুন ডেটায় দলের সীমা বদলাতে পারে; RFM-এর নিয়ম স্থির আর ব্যাখ্যা সহজ। কোনোটিই বলে না কোন পদক্ষেপ কাজ করবে; সেটি অংশ ৫-এর পরীক্ষার প্রশ্ন।

৩ · চুক্তিহীন ব্যবসায় churn-এর সংজ্ঞা — ৬০, ৯০ না ১৮০ দিন

মোবাইল অপারেটরে churn একটি ঘটনা: গ্রাহক নম্বর বন্ধ করেন, তারিখ জানা। বাজারঘরে কেউ ঘোষণা দেন না। তাই churn একটি সংজ্ঞা, যা বিশ্লেষক বেছে নেন: “cutoff-এর পরের W দিনে কোনো অর্ডার নেই”। W ছোট হলে label দ্রুত আসে, কিন্তু মাঝে মাঝে কেনা মানুষ ভুল করে churn হন; W বড় হলে ভুল কমে, অপেক্ষা বাড়ে।

Simulation-এ আমরা জানি কে আর কখনো কিনবেন না। ৩১ মার্চ ২০২৫-এর cutoff-এ যাঁরা আগের ১৮০ দিনে অন্তত একবার কিনেছেন (৮৭৯ জন), তাঁদের তিনটি সংজ্ঞায় label দিয়ে দেখা যাক, “churn” বলা কতজন পরে ফিরে এসেছেন। হিসাবটি অনুশীলনের চতুর্থ প্রশ্নের কোডে আছে।

Cutoff ৩১ মার্চ ২০২৫ · সক্রিয় ৮৭৯ জন · simulation-এর সত্যে ৩৫৮ জন আর কখনো কেনেননি
সংজ্ঞা
Churn label
পরে ফিরে এলেন
Label পেতে অপেক্ষা
  1. প্রশ্ন। ৮৭৯ জন সক্রিয় ক্রেতার ৩৫৮ জন আসলে আর কখনো ফিরবেন না। একটি নিখুঁত সংজ্ঞা ঠিক এই ৩৫৮ জনকে churn বলত। কিন্তু “আর কখনো না” জানতে অনন্তকাল লাগে।
  2. ৬০ দিন। ৫৪৫ জন churn, যার ১৮৭ জন (৩৪.৩%) পরে আবার কিনেছেন। তিনজনে একজন “চলে যাওয়া” আসলে শুধু বিরতিতে ছিলেন; এঁদের ছাড় দিলে টাকা নষ্ট।
  3. ৯০ দিন। ৪৮৮ জন churn, ভুল ১৩০ জন (২৬.৬%). অপেক্ষা দেড় গুণ।
  4. ১৮০ দিন। ৪২০ জন, ভুল মাত্র ৬২ (১৪.৮%). কিন্তু ছয় মাস অপেক্ষা; ১৮ মাসের ডেটায় এমন cutoff খুব কম বসানো যায়, আর জানার আগেই হয়তো ক্রেতা হারিয়ে গেছেন।
  5. সিদ্ধান্ত। তিনটিতেই আসল ৩৫৮ জন ধরা পড়েছেন; পার্থক্য শুধু ভুল “churn” আর অপেক্ষায়। এখানে ৯০ দিন একটি মাঝামাঝি পছন্দ। ভালো অভ্যাস: অর্ডারের মাঝের স্বাভাবিক ফাঁকের বণ্টন দেখে (যেমন ৯০% ক্রেতা কত দিনের মধ্যে আবার কেনেন) W ঠিক করা, আর সংজ্ঞাটি লিখে রাখা।

জানালা বাছাই এড়ানোর দুটি পথ আছে। Churn-কে সময়ের প্রশ্ন ধরে censoring সামলানো: পর্ব ৫৫-এর Kaplan–Meier আর Cox, যা এখানে আবার করা হবে না। আর চলে যাওয়াকে একটি অদৃশ্য অবস্থা ধরে তার সম্ভাবনা অনুমান: অংশ ৭-এর BG/NBD.

৪ · সময় মেনে churn prediction — observation window, label window আর leakage

Churn model-এর প্রতিটি সারির পেছনে তিনটি সময়: একটি cutoff (prediction time), তার আগের observation window, যেখান থেকে feature আসে, আর তার পরের label window, যেখানে label বসে। পর্ব ০২-এ একটি cutoff আর ডেটা তোলার দিনের গোলমাল দেখা হয়েছে। এখানে নতুন দুটি দিক: সময় ধরে train আর test, আর রোজ রাতে হালনাগাদ হওয়া একটি “মোট অর্ডার” কলামের সূক্ষ্ম ফাঁস।

দুটি cutoff: আগেরটিতে train, পরেরটিতে test

নীল = feature-এর জানালা; কমলা = label-এর ৯০ দিন; লাল = ফাঁস (label window-এর অর্ডারও গোনা)

Observation window, label window and leakage for two cutoffs জুল ২৪জানু ২৫জুল ২৫ডিসে ২৫ Train (cutoff ৩১ মার্চ) Test (cutoff ৩০ জুন) ফাঁস কুপন ৯০৯০ featurefeature ডেটা কাটা
ছবি ৪.১ — প্রতিটি সারিতে নীল অংশ শেষ হয় cutoff-এ (কালো দাগ), কমলা ৯০ দিনে label বসে। লাল বার একটি “মোট অর্ডার” কলাম, যা cutoff-এ থামেনি: label window-এর অর্ডারও গুনেছে। ডান দিকে অক্টোবরের কুপন পরীক্ষা (সবুজ), দুটি label window-এর পরে, যাতে churn label কুপনে প্রভাবিত না হয়।

দ্বিতীয় ব্লক একটি snapshot function দিয়ে প্রতিটি cutoff-এর টেবিল বানায়: unit হলো cutoff-এর আগের ১৮০ দিনে অন্তত একবার কেনা ক্রেতা, feature চারটি (recency, যোগদান থেকে দিন, অর্ডারের সংখ্যা, গড় খরচ), label = পরের ৯০ দিনে কোনো অর্ডার নেই। সৎ টেবিলে চারটিই cutoff পর্যন্ত গোনা। ফাঁসের টেবিলে অর্ডারের সংখ্যা আর গড় খরচ গোনা হয় label window-এর শেষ পর্যন্ত, যেন কেউ ডেটাবেসের “মোট অর্ডার” কলামটি সরাসরি তুলে এনেছেন। Model দুটিই logistic regression (AI-এর ভিত্তি কোর্সের পর্ব ৫৯), মার্চের cutoff-এ train আর জুনের cutoff-এ test: একটি time-based split.

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from bazar_cust import tx

def snapshot(cut, feat_end, win=90):
    """cut পর্যন্ত সক্রিয় ক্রেতা; feature feat_end পর্যন্ত; label cut-এর পরের win দিন"""
    cut, feat_end = pd.Timestamp(cut), pd.Timestamp(feat_end)
    g = tx[tx.date <= cut].groupby("customer")
    h = tx[tx.date <= feat_end].groupby("customer")       # "মোট অর্ডার" কলাম
    X = pd.DataFrame({"rec": (cut - g.date.max()).dt.days,
                      "tenure": (cut - g.date.min()).dt.days,
                      "freq": h.size(), "spend": h.amount.mean()})
    last = g.date.max()
    active = last.index[(cut - last).dt.days <= 180]       # unit: সক্রিয় ক্রেতা
    future = tx[(tx.date > cut) & (tx.date <= cut + pd.Timedelta(days=win))]
    y = (~active.isin(future.customer)).astype(int)        # 1 = churn
    return np.log1p(X.loc[active]), pd.Series(y, index=active)

def auc(train, test):
    m = LogisticRegression().fit(*train)
    return roc_auc_score(test[1], m.predict_proba(test[0])[:, 1])

Xa, ya = snapshot("2025-03-31", "2025-03-31")              # সৎ, train
Xb, yb = snapshot("2025-06-30", "2025-06-30")              # সৎ, পরের cutoff
La, _ = snapshot("2025-03-31", "2025-06-29")               # ফাঁস: label window-এর তথ্য
Lb, _ = snapshot("2025-06-30", "2025-09-28")
print("rows:", len(ya), len(yb), "| churn rate:", round(ya.mean(), 3), round(yb.mean(), 3))
print(f"leaky : train Mar, test Jun  AUC {auc((La, ya), (Lb, yb)):.3f}")
print(f"honest: train Mar, test Jun  AUC {auc((Xa, ya), (Xb, yb)):.3f}")
print(f"leaky model, scored honestly AUC {auc((La, ya), (Xb, yb)):.3f}")

আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):

rows: 879 1032 | churn rate: 0.555 0.54
leaky : train Mar, test Jun  AUC 0.827
honest: train Mar, test Jun  AUC 0.740
leaky model, scored honestly AUC 0.634

টেবিল: মার্চে ৮৭৯ আর জুনে ১০৩২ জন সক্রিয় ক্রেতা, দুই cutoff-এ churn-এর হার ৫৫.৫% আর ৫৪.০%: এই simulation-এ অনেকেই একবার-দুবার কিনে চলে যান। ফাঁস: label window-এর অর্ডার গোনা model জুনের পরীক্ষায় AUC ০.৮২৭ দেখায়, সৎ model-এর ০.৭৪০-এর অনেক ওপরে। কারণ সহজ: যিনি পরের ৯০ দিনে কিনেছেন, তাঁর “মোট অর্ডার” এক বেড়েছে, অর্থাৎ feature-এর ভেতরে label-এর একটি টুকরো (target leakage). Test-ও একইভাবে ফাঁস টেবিল থেকে এসেছে বলে কাগজে ফল চমৎকার। বাস্তবে: প্রতিদিনের scoring-এর সময় ভবিষ্যৎ অর্ডার নেই, তাই model পায় সৎ সংখ্যা। শেষ লাইন ঠিক সেটি করে, আর AUC নামে ০.৬৩৪-এ, সৎ model-এর চেয়েও খারাপ: সে শিখেছিল “এক-দুটি বাড়তি অর্ডার মানে থাকবেন”, যা বাস্তবে কখনো পাওয়া যায় না। নিয়ম: অবাক করা ভালো ফল দেখলে আগে প্রতিটি কলামের “কোন তারিখ পর্যন্ত” লিখে যাচাই করুন।

আরও দুটি ফাঁদ: (১) একই ক্রেতার অনেক cutoff-এর সারি এলোমেলো ভাগে train আর test-এ পড়লে model ভবিষ্যৎ দেখে; সব train cutoff test cutoff-এর আগে রাখুন, মাঝে অন্তত একটি label window-এর ফাঁক। (২) Churn-এর হার এখানে প্রায় ৫৫%, কিন্তু অনেক ব্যবসায় অনেক কম; তখন পর্ব ৫০-এর threshold আর precision-recall ভাবনা লাগে, আর পর্ব ২৬-এর calibration, কারণ score দিয়ে বাজেট ভাগ হবে।

৫ · কাকে কুপন দেবেন — response model বনাম uplift

Churn score হাতে পেলে স্বাভাবিক প্রস্তাব: “ঝুঁকি বেশি যাঁদের, তাঁদের কুপন দিই”, বা “কুপন পেলে যাঁরা কিনবেন বলে model বলছে, তাঁদের দিই”। দুটিই ভুল প্রশ্নের উত্তর। আসল প্রশ্ন: কুপনের কারণে কে বাড়তি কিনবেন? যিনি এমনিই কিনতেন, তাঁকে কুপন দিলে শুধু ছাড়ের টাকা যায়।

যে রোগী দুই দিনে এমনিই সেরে উঠতেন, তাঁর সেরে ওঠা ওষুধের কৃতিত্ব নয়; কিছু রোগীর ওষুধে লাভই নেই। ভালো ডাক্তার খোঁজেন তাঁদের, যাঁদের ক্ষেত্রে ওষুধই পার্থক্য গড়ে। Uplift modeling এই খোঁজ: সম্ভাবনা নয়, সম্ভাবনার পার্থক্য।

প্রচলিত ভাষায় ক্রেতা চার রকম: persuadables (কুপন পেলে কিনবেন, না পেলে নয়), sure things (এমনিই কিনবেন), lost causes (কুপনেও কিনবেন না), আর sleeping dogs (যোগাযোগ করলেই বিরক্ত হয়ে চলে যান)। শুধু প্রথম দলে কুপন লাভজনক। সমস্যা: একজন মানুষকে একসঙ্গে কুপন দেওয়া আর না দেওয়া যায় না, তাই কারও ব্যক্তিগত প্রভাব কখনো সরাসরি দেখা যায় না। পর্ব ৬৪ (Causal inference-এর পরিচয় — DAG, matching আর difference-in-differences) এই মৌলিক সমস্যার কাঠামো দেখায়। এখানে আমরা সবচেয়ে নির্ভরযোগ্য পথ নিই: একটি এলোমেলো পরীক্ষা (A/B test), যার নকশা আর বিশ্লেষণ পর্ব ৬২ আর ৬৩-এর বিষয়।

বাজারঘরের পরীক্ষা: ১ অক্টোবর ২০২৫-এ অন্তত এক মাস পুরোনো সব ক্রেতার এলোমেলো অর্ধেক কুপন পান। ফল: পরের ৩০ দিনে কোনো অর্ডার আছে কি না। এলোমেলো ভাগ বলে x-এর প্রতিটি মানে দুই দল তুলনীয়, তাই conditional average treatment effect τ(x) = P(কিনবেন | কুপন, x) − P(কিনবেন | কুপন নেই, x) অনুমান করা যায়। সবচেয়ে সরল পদ্ধতি T-learner: কুপন-দলে একটি model, কুপনহীন দলে আরেকটি, তারপর দুটির অনুমানের বিয়োগ। তুলনার জন্য একটি response model: শুধু কুপন-দলে P(কিনবেন | x). দুটিই logistic regression, recency-র বাঁকা প্রভাব ধরতে আগে একটি spline রূপান্তর (পর্ব ২৩-এর basis function). প্রতিটি ক্রেতার score আসে এমন model থেকে, যা তাঁকে train-এ দেখেনি: দুই ভাগে cross-fitting. শেষে score অনুযায়ী ১০টি ভাগ (decile) করে প্রতিটি ভাগে দেখা uplift = কুপন-দলের কেনার হার − কুপনহীন দলের হার।

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import SplineTransformer
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import KFold
from bazar_cust import tx, CUST, EXP
g = tx[tx.date < EXP].groupby("customer")           # কুপনের আগের ইতিহাস
X = pd.DataFrame({"rec": (EXP - g.date.max()).dt.days, "freq": g.size(),
                  "spend": g.amount.mean(), "tenure": (EXP - g.date.min()).dt.days})
X = X[X.tenure > 30]
won = tx[(tx.date >= EXP) & (tx.date < EXP + pd.Timedelta(days=30))].customer
y, t = X.index.isin(won).astype(int), CUST.treat[X.index].to_numpy()
Z = np.log1p(X.to_numpy())
print("n =", len(y), "| bought in 30 d: coupon", y[t == 1].mean().round(3),
      "no coupon", y[t == 0].mean().round(3))
fit = lambda m: make_pipeline(SplineTransformer(n_knots=5), LogisticRegression()).fit(Z[m], y[m])
idx, resp, upl = np.arange(len(y)), np.zeros(len(y)), np.zeros(len(y))
for tr, te in KFold(2, shuffle=True, random_state=65).split(Z):   # cross-fitting
    p1 = fit(np.isin(idx, tr) & (t == 1)).predict_proba(Z[te])[:, 1]  # কুপন-দল
    p0 = fit(np.isin(idx, tr) & (t == 0)).predict_proba(Z[te])[:, 1]  # কুপনহীন দল
    resp[te], upl[te] = p1, p1 - p0              # response বনাম T-learner
out = {}
for name, s in [("response", resp), ("T-learner", upl)]:
    dec = pd.qcut(-s, 10, labels=range(1, 11))           # 1 = সবচেয়ে বেশি score
    r = pd.DataFrame({"d": dec, "y": y, "t": t}).groupby(["d", "t"], observed=True).y.mean().unstack()
    out[name] = r[1] - r[0]                              # decile-এ দেখা uplift
    top = np.asarray(dec.astype(int) <= 3)
    print(f"{name:9s} top 30%: extra orders ~ {(y[top & (t == 1)].mean() - y[top & (t == 0)].mean()) * top.sum():.0f}")
u = pd.DataFrame(out)
print(u.round(2).T)
ax = u.plot.bar(figsize=(6, 3.6), rot=0, xlabel="decile (1 = top score)",
                ylabel="coupon minus no coupon", title="Observed uplift by decile")
ax.figure.subplots_adjust(bottom=0.17); plt.show()

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

n = 1559 | bought in 30 d: coupon 0.272 no coupon 0.185
response  top 30%: extra orders ~ 14
T-learner top 30%: extra orders ~ 67
d             1     2     3     4     5     6     7     8     9    10
response  -0.03  0.01  0.09  0.16  0.23  0.12  0.05  0.08  0.04  0.05
T-learner  0.12  0.15  0.14  0.08  0.04  0.13  0.00  0.07  0.10  0.04

গড় প্রভাব: ১৫৫৯ জনে কুপন-দলের ২৭.২% পরের ৩০ দিনে কিনেছেন, কুপনহীন দলের ১৮.৫%: গড় uplift প্রায় ৮.৭ শতাংশ-বিন্দু, পুরো দলে প্রায় ১৩৬টি বাড়তি অর্ডার (০.০৮৭ × ১৫৫৯). Response model-এর শীর্ষ decile-এ uplift −০.০৩, দ্বিতীয়তে ০.০১: সে ঠিকই খুঁজেছে কারা কিনবেন, কিন্তু তাঁরা “sure things”, কুপন ছাড়াও কিনতেন। T-learner-এর প্রথম তিন decile-এ ০.১২, ০.১৫, ০.১৪, গড়ের প্রায় দেড় গুণ। শীর্ষ ৩০%-কে কুপন দিলে T-learner-এর বাছাই থেকে প্রায় ৬৭টি বাড়তি অর্ডার, response model-এর বাছাই থেকে প্রায় ১৪টি: একই বাজেটে প্রায় পাঁচ গুণ। সাবধানতা: প্রতিটি decile-এ প্রায় ১৫৬ জন (দুই দলে ~৭৮ করে), তাই প্রতিটি বারের standard error প্রায় ০.০৬–০.০৭; একক বার নয়, শীর্ষ ভাগগুলোর যোগফল আর পর্ব ২১-এর bootstrap পরিসর দেখুন।

এই simulation-এ কুপনের সত্যিকারের প্রভাব সবচেয়ে বেশি তাঁদের, যাঁদের শেষ অর্ডার প্রায় ১১০ দিন আগে আর যাঁরা এখনো চলে যাননি: “At risk” দলের একটি অংশ। দুই-model পদ্ধতির নিজস্ব দুর্বলতা: দুটি model-এর ভুল বিয়োগে যোগ হয়, তাই প্রভাব ছোট হলে সংকেত noise-এ ডোবে। বিকল্প S-learner (কুপন একটি feature), X-learner আর uplift tree; মূল্যায়নে Qini বা uplift curve (শীর্ষ k% পর্যন্ত মোট বাড়তি ফল)।

৬ · Cohort retention আর lifetime value — অতীত থেকে ভবিষ্যৎ

Cohort analysis-এ ক্রেতাদের ভাগ করা হয় শুরুর সময় ধরে (এখানে প্রথম অর্ডারের ত্রৈমাসিক), তারপর দেখা হয় প্রথম অর্ডারের k মাস পরে প্রতিটি দলের কত ভাগ আবার কিনেছেন: এক সারিতে এক দল, এক কলামে এক বয়স, এই হলো retention টেবিল। SQL-এ এটি পর্ব ৬১-এর বিষয় (“ডাটাবেস ও SQL” কোর্সের window function-ও দেখুন); এখানে pandas-এ। জরুরি নিয়ম: যে মাস কারও জন্য পুরো দেখা হয়নি, তা হরেও আসবে না, লবেও না; নইলে নতুন দলের retention মিথ্যা করে কম দেখায়, পর্ব ৫৫-এর censoring-এর মতো।

CLV আসে টাকার সময়-মূল্য থেকে: আজকের ১০০ টাকা এক বছর পরের ১০০ টাকার চেয়ে দামি, তাই ভবিষ্যতের আয়কে একটি discount rate d দিয়ে ছোট করা হয়। মাসিক margin m, মাস t-তে সক্রিয় থাকার সম্ভাবনা rt−১, আর টাকা আসে মাসের শেষে ধরলে

CLV = ∑t=১∞ m · rt−১ / (১ + d)t = m / (১ + d − r)

এখানে m টাকা/মাস, r আর d ভগ্নাংশ। ১২ মাসে থামলে যোগফল m/(১ + d − r) · [১ − (r/(১ + d))১২]. কিছু বইয়ে প্রথম মাসের টাকাও টিকে থাকার শর্তে আসে; তখন সূত্র m·r/(১ + d − r). কোন নিয়ম, তা লিখে রাখাই আসল কথা।

মাস t-এর মূল্য = m · r^(t−1) / (1 + d)^t · m = 200, r = 0.8, d = 0.01
এখন দেখা হচ্ছে
এই মাসের হিসাব
চলমান যোগফল
সরল (ভুল) হিসাব
  1. অনুমান। মাসিক margin ২০০ টাকা, প্রতি মাসে ৮০% সম্ভাবনায় থাকেন, মাসিক discount ১%. “প্রতি মাসে ২০০, এক বছরে ২৪০০” সরল হিসাব ধরে নেয় কেউ চলে যান না আর টাকার সময়-মূল্য নেই।
  2. মাস ১. ক্রেতা আছেন (r০ = ১), টাকা এক মাস পরে: ২০০/১.০১ = ১৯৮.০২.
  3. মাস ২. থাকার সম্ভাবনা ০.৮, discount দুই মাসের: ১৬০/১.০২০১ = ১৫৬.৮৫. চলমান যোগ ৩৫৪.৮৭.
  4. মাস ৩. ০.৮² = ০.৬৪, ১২৮/১.০৩০৩০১ = ১২৪.২৪; যোগ ৪৭৯.১০. প্রতি মাসে অবদান প্রায় ২১% কমে (০.৮/১.০১ = ০.৭৯২).
  5. যোগফল। ১২ মাসে ৮৯৪.৩০, অসীমে ২০০/০.২১ = ৯৫২.৩৮. সরল ২৪০০-এর অর্ধেকেরও কম; ভুলের প্রায় সবটাই churn থেকে, discount থেকে অল্প। তাই CLV-তে r-এর অনুমানই সবচেয়ে গুরুত্বপূর্ণ সংখ্যা।

বাস্তবে r সবার জন্য এক নয়, আর সময়ের সঙ্গে বদলায়। চতুর্থ ব্লক retention টেবিল আর বক্ররেখা আঁকে, তারপর পরের ১২ মাসের মূল্য (মাসিক ১% discount) দুইভাবে অনুমান করে সত্যের সঙ্গে মেলায়। (ক) ইতিহাসের গড়: প্রতিটি ক্রেতার এ পর্যন্ত মাসিক গড় আয় সামনেও চলবে। (খ) Cohort বক্ররেখা: বয়স k মাসে শুরু করা প্রতি ক্রেতার গড় আয় R(k), দেখা বয়সের বাইরে log-রেখায় বাড়ানো, আর প্রতিটি ক্রেতার বর্তমান বয়স থেকে পরের ১২ মাস যোগ।

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from bazar_cust import tx, CUT, future_revenue
first = tx.groupby("customer").date.min()
seen = (CUT - first).dt.days // 30          # কত মাস পুরো দেখা হয়েছে
cohort = first.dt.to_period("Q").astype(str)
d = tx.assign(age=(tx.date - tx.customer.map(first)).dt.days // 30)
d = d[(d.age > 0) & (d.age < d.customer.map(seen))]      # শুধু পুরো দেখা মাস
act = d.drop_duplicates(["customer", "age"])
num = act.groupby([act.customer.map(cohort).rename("cohort"), "age"]).size().unstack()
den = pd.DataFrame({k: (seen > k).groupby(cohort).sum() for k in num.columns})
ret = (num / den).where(den >= 30)
print((100 * ret.iloc[:, :6]).round(1))
ax = ret.T.plot(marker=".", figsize=(6, 3.6), xlabel="months since first order",
                ylabel="share who ordered", title="Retention by sign-up quarter")
ax.figure.subplots_adjust(bottom=0.15); plt.show()

disc = 1 / 1.01 ** np.arange(1, 13)            # মাসিক ১% discount
hist = tx.groupby("customer").amount.sum() / np.maximum(seen, 1) * disc.sum()
R = d.groupby("age").amount.sum() / den.sum()   # বয়স k-তে ক্রেতাপ্রতি আয়
b = np.polyfit(R.index[2:], np.log(R.values[2:]), 1)   # লেজের জন্য log-রেখা
coh = seen.map(lambda a: (np.exp(np.polyval(b, a + np.arange(1, 13))) * disc).sum())
truth = future_revenue()
print("revenue per customer, months 1-4:", R.head(4).round(0).tolist())
print("next 12 months, discounted (Tk):")
for name, v in [("historical avg", hist), ("cohort curve", coh), ("truth", truth)]:
    print(f"  {name:15s} {v.sum():>12,.0f}")

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

age        1     2     3     4     5     6
cohort                                    
2024Q3  34.8  31.7  29.3  23.8  22.0  21.0
2024Q4  38.5  29.3  29.3  27.0  21.6  21.0
2025Q1  32.7  27.7  26.5  24.3  23.4  22.4
2025Q2  37.0  35.5  30.6  30.0  26.7  21.7
2025Q3  40.4  34.3  30.1  26.0   NaN   NaN
2025Q4  35.6   NaN   NaN   NaN   NaN   NaN
revenue per customer, months 1-4: [477.0, 402.0, 351.0, 339.0]
next 12 months, discounted (Tk):
  historical avg    13,701,096
  cohort curve       3,102,434
  truth              3,834,581

টেবিল পড়া: ২০২৪-এর তৃতীয় ত্রৈমাসিকে যোগ দেওয়াদের ৩৪.৮% প্রথম অর্ডারের পরের মাসে আবার কিনেছেন, ষষ্ঠ মাসে ২১.০%. বক্ররেখা দ্রুত নেমে ধীরে চ্যাপ্টা হয়; ১২ মাসের পরে পুরোনো দলগুলো ৮–১৫%-এর মধ্যে। NaN মানে ওই বয়স এখনো পুরো দেখা হয়নি (বা ৩০ জনের কম)। দলগুলো প্রায় একই, কারণ simulation-এ সব দল একই নিয়মে তৈরি; বাস্তবে এই টেবিল দেখায় কোন ত্রৈমাসিকের প্রচারণা টেকসই ক্রেতা এনেছে। মূল্য: সত্য ৩৮,৩৪,৫৮১ টাকা। ইতিহাসের গড় বলে ১,৩৭,০১,০৯৬: সাড়ে তিন গুণ। কারণ চলে যাওয়া ক্রেতাদেরও সে কিনতে থাকা ধরে, আর এক মাসের কম পুরোনো ক্রেতার প্রথম অর্ডারকেই মাসিক হার ধরে। Cohort বক্ররেখা বলে ৩১,০২,৪৩৪, সত্যের প্রায় ১৯% কম। কারণ লেজ: log-রেখা প্রতি মাসে প্রায় ৮% পতন ধরে চলে, কিন্তু টিকে থাকা ক্রেতারা বেশি অনুগত বলে আসল বক্ররেখা চ্যাপ্টা হয়। এই ভিন্নতা সামলানোই পরের অংশের model-এর কাজ।

৭ · BG/NBD আর Gamma-Gamma — ক্রেতা কি এখনো আছেন?

চুক্তিহীন ব্যবসার জন্য একটি probabilistic model পরিবার আছে, যা প্রতিটি ক্রেতার শুধু তিনটি সংখ্যা চায়: পুনরাবৃত্ত অর্ডারের সংখ্যা x, প্রথম থেকে শেষ অর্ডার পর্যন্ত সময় tx, আর প্রথম অর্ডার থেকে ডেটা কাটার দিন পর্যন্ত সময় T. RFM-এর সঙ্গে মিলটি লক্ষ করুন। BG/NBD model (Peter Fader, Bruce Hardie আর Ka Lok Lee, Marketing Science, ২০০৫) অনুমান করে:

  • সক্রিয় থাকা পর্যন্ত একজন ক্রেতা λ হারে Poisson ছাঁদে কেনেন; λ ক্রেতাভেদে gamma(r, α) বণ্টনে ছড়ানো।
  • প্রতিটি অর্ডারের পরে তিনি p সম্ভাবনায় চিরতরে চলে যান; p ক্রেতাভেদে beta(a, b) বণ্টনে ছড়ানো।

এই দুই অনুমান থেকে বন্ধ সূত্রে আসে প্রতিটি ক্রেতার P(alive), অর্থাৎ ডেটা কাটার দিনে এখনো সক্রিয় থাকার সম্ভাবনা, আর ভবিষ্যৎ সময়ে প্রত্যাশিত অর্ডার। মূল অন্তর্দৃষ্টি: দীর্ঘ নীরবতা বিপদ মূলত ঘন ঘন কেনা মানুষের জন্য; কম কেনা মানুষের দুই মাসের নীরবতা স্বাভাবিক। টাকার জন্য আলাদা Gamma-Gamma model: প্রতিটি অর্ডারের টাকা ক্রেতার নিজস্ব গড়ের চারপাশে gamma বণ্টনে, আর সেই গড় ক্রেতাভেদে ছড়ানো; ধরে নেওয়া হয় খরচের মাপ কেনার হারের সঙ্গে সম্পর্কহীন। দুটি মিলিয়ে CLV.

আমাদের simulation ইচ্ছা করেই প্রায় এই কাঠামোয় তৈরি (lam, p, nu), তাই এটি model-এর জন্য সহজ পরীক্ষা; বাস্তব ডেটা এত বাধ্য নয়। পুরোনো lifetimes library আর সক্রিয়ভাবে হালনাগাদ হয় না; তার CLV অংশ PyMC-Marketing-এর clv module-এ নতুন করে লেখা হয়েছে। এটি পাতার Python-এ নেই, তাই ডেস্কটপে uv প্রকল্পে চালানো হলো (AI-এর ভিত্তি কোর্সের পর্ব ৭), পাতার একই bazar_cust.py দিয়ে।

~/bazarghar-clv$ uv init --bare --vcs none --author-from none
~/bazarghar-clv$ uv add pymc-marketing numpy pandas scipy
 …
 + numpy==2.5.4
 + pymc==5.28.5
 + pymc-marketing==0.19.4
 + pytensor==2.38.2
 …
~/bazarghar-clv$ uv run python -c "import pymc_marketing.clv"
 …
pydantic_core._pydantic_core.SchemaError: Error building "definitions" validator:
 …
  SchemaError: 'cls' must be valid as the first argument to 'isinstance'
~/bazarghar-clv$ uv add "numpy<2.5"
 - numpy==2.5.4
 + numpy==2.4.6

লক্ষ করুন: সর্বশেষ NumPy-র সঙ্গে PyMC-Marketing import-ই হয়নি (ত্রুটি pydantic-এর একটি type যাচাই থেকে); NumPy ২.৪.৬-এ নামিয়ে ঠিক হলো। এমন অমিল সাধারণ, তাই lockfile আর version লিখে রাখার অভ্যাস (AI-এর ভিত্তি কোর্সের পর্ব ১২) জরুরি।

import numpy as np
import pandas as pd
from scipy.stats import spearmanr
from pymc_marketing import clv
from bazar_cust import tx, CUT, future_revenue

data = clv.utils.rfm_summary(tx, "customer", "date", "amount",
                             observation_period_end=CUT)
bg = clv.BetaGeoModel(data=data[["customer_id", "frequency", "recency", "T"]])
bg.fit(method="map")
print(bg.fit_summary().round(3))
rep = data[data.frequency > 0]
gg = clv.GammaGammaModel(data=rep[["customer_id", "frequency", "monetary_value"]])
gg.fit(method="map")
print(gg.fit_summary().round(3))

alive = bg.expected_probability_alive(data).values.ravel()
val = gg.expected_customer_lifetime_value(transaction_model=bg, data=data,
        future_t=12, discount_rate=0.01, time_unit="D").values.ravel()
truth = future_revenue().loc[data.customer_id].to_numpy()
print("P(alive) median:", round(np.median(alive), 3))
print(f"BG/NBD + GG total: {val.sum():,.0f}   truth: {truth.sum():,.0f}")
print("Spearman with truth:", round(spearmanr(val, truth)[0], 3))
seen = (CUT - tx.groupby("customer").date.min()).dt.days // 30
hist = (tx.groupby("customer").amount.sum() / np.maximum(seen, 1)).to_numpy()
for name, s in [("BG/NBD+GG", val), ("historical", hist)]:
    top = s >= np.quantile(s, 0.9)
    print(f"top 10% by {name}: {truth[top].sum() / truth.sum():.1%} of true value")

সত্যিকারের run (ডেস্কটপ, Python 3.14.4; লেখার সময়ে pymc-marketing 0.19.4, pymc 5.28.5, pytensor 2.38.2, NumPy 2.4.6, pandas 3.1.1; MAP fit, অর্থাৎ শুধু সবচেয়ে সম্ভাব্য parameter, পুরো posterior নয়):

~/bazarghar-clv$ uv run python clv_bgnbd.py
alpha            30.396
phi_dropout       0.126
kappa_dropout    16.114
r                 0.756
a                 2.038
b                14.077
Name: value, dtype: float64
p      6.810
q      3.755
v    329.574
Name: value, dtype: float64
P(alive) median: 0.784
BG/NBD + GG total: 3,760,581   truth: 3,834,581
Spearman with truth: 0.626
top 10% by BG/NBD+GG: 43.0% of true value
top 10% by historical: 38.2% of true value

পড়া: (PyTensor-এর একটি C compiler না পাওয়ার সতর্কবার্তা আর fit-এর progress লাইন বাদ দেওয়া হয়েছে।) Simulation-এ λ-এর gamma-র shape ১.৫, scale ০.০২; fit পেয়েছে r = ০.৭৫৬, α = ৩০.৪ (scale ১/৩০.৪ ≈ ০.০৩৩). চলে যাওয়ার সম্ভাবনার গড় সত্যে ১.৫/৯.৫ ≈ ০.১৫৮, fit-এ a/(a + b) = ০.১২৬ (phi_dropout). হুবহু মেলে না: কুপন-অর্ডার model-এর অনুমানের বাইরে, আর অনেকের মাত্র এক-দুটি অর্ডার। Gamma-Gamma-র p = ৬.৮১, সত্যে ৬. মোট মূল্য: ৩৭,৬০,৫৮১ বনাম সত্য ৩৮,৩৪,৫৮১, প্রায় ২% কম; আগের দুই পদ্ধতির চেয়ে অনেক কাছে। ক্রেতা-স্তরে: Spearman rank correlation ০.৬২৬; model-এর শীর্ষ ১০% ক্রেতার হাতে আসল ভবিষ্যৎ মূল্যের ৪৩.০%, ইতিহাসের গড়ের শীর্ষ ১০%-এ ৩৮.২%. একটি seed-এ একটি run, সাধারণ ফল নয়।

আরও গভীরে: BG/NBD এসেছে David Schmittlein, Donald Morrison আর Richard Colombo-র Pareto/NBD (Management Science, ১৯৮৭) থেকে, যেখানে চলে যাওয়া ঘটে যেকোনো সময়ে; Fader ও সহকর্মীরা চলে যাওয়াকে অর্ডারের পরে বসিয়ে হিসাব অনেক সহজ করেন, তাই প্রবন্ধের নাম “Counting your customers the easy way”. একটি পরিচিত দুর্বলতা: BG/NBD-তে একটিও পুনরাবৃত্ত অর্ডার না থাকা ক্রেতার P(alive) সবসময় ১. Gamma-Gamma-র ধারণা Richard Colombo আর Weina Jiang-এর ১৯৯৯ সালের কাজ থেকে, CLV-তে জনপ্রিয় করে Fader, Hardie আর Lee-র ২০০৫ সালের iso-value বক্ররেখার প্রবন্ধ। পুরো Bayesian fit (MCMC) প্রতিটি ক্রেতার CLV-র পরিসর দেয়; বড় ডেটায় MAP দ্রুত কিন্তু অনিশ্চয়তা লুকায়।

পরীক্ষা ও ইন্টারভিউতে: (১) RFM-এ R score কীভাবে দেন? Recency ছোট মানে ভালো, তাই উল্টো ক্রমে quintile: সবচেয়ে সাম্প্রতিক ২০% পান ৫. (২) চুক্তিহীন ব্যবসায় churn কীভাবে মাপবেন? একটি inactivity window বেছে (যেমন ৯০ দিন) label; ভুল-churn আর অপেক্ষার মধ্যে বাছাই, বা survival/BG/NBD. (৩) Churn model-এ leakage কীভাবে ধরবেন? প্রতিটি feature-এর “কোন তারিখ পর্যন্ত” যাচাই; সময় ধরে train/test; অবাস্তব AUC দেখলে সন্দেহ। (৪) Uplift আর response model-এর পার্থক্য? Response: P(কিনবেন | কুপন); uplift: P(কিনবেন | কুপন) − P(কিনবেন | কুপন নেই), এলোমেলো পরীক্ষা লাগে। (৫) m = ১০০, r = ০.৯, d = ১% হলে অসীম CLV? ১০০/(১.০১ − ০.৯) ≈ ৯০৯ (মাসের শেষে টাকা, প্রথম মাসে নিশ্চিত)।

৮ · Targeting-এর নৈতিকতা — কার সঙ্গে, কেন আর কতটা

প্রতিটি score শেষে একটি সিদ্ধান্ত: কে ছাড় পাবেন, কে দ্রুত সেবা, কাকে বার্তা। তাই প্রশ্নগুলো শুধু প্রযুক্তিগত নয়।

  • সমান মানুষ, অসমান দাম: Uplift-এর যুক্তিতে অনুগত ক্রেতা ছাড় পান না, কারণ তাঁরা “এমনিই কিনবেন”। ক্রেতারা যদি দেখেন বিশ্বস্ততার “শাস্তি” বেশি দাম, আস্থা ভাঙে; নীতি স্বচ্ছ আর দামের পার্থক্য সীমিত রাখুন।
  • প্রতিনিধি চলক: RFM বা CLV নিজে জেলা বা লিঙ্গ দেখে না, কিন্তু ডেলিভারির খরচ বা আয় জেলাভেদে আলাদা হলে CLV-ভিত্তিক “প্রিমিয়াম সেবা” পরোক্ষভাবে কিছু এলাকাকে বাদ দেয়। Segment-ভেদে সিদ্ধান্তের হার মেপে দেখুন (পর্ব ৬০-এর ন্যায্যতার মাপ আর প্রতিকার)।
  • সংবেদনশীল অনুমান: কেনাকাটার ইতিহাস থেকে অসুস্থতা, গর্ভধারণ বা আর্থিক টানাপোড়েনের মতো তথ্য অনুমান করা যায়। এমন বার্তার আগে প্রয়োজন, সম্মতি আর কম ডেটায় কাজ হয় কি না দেখুন (AI-এর ভিত্তি কোর্সের পর্ব ৭২); প্রযোজ্য আইন ও প্রতিষ্ঠানের নীতিও।
  • দুর্বল ক্রেতা: সবচেয়ে বেশি “uplift” হয়তো তাঁদের, যাঁরা ছাড় দেখলে নিজেকে সামলাতে পারেন না। শুধু অর্ডার নয়, ফেরত, অভিযোগ আর দীর্ঘমেয়াদি সন্তুষ্টিও guardrail হিসেবে মাপুন।
  • Churn ঠেকানোর অন্ধকার নকশা: অ্যাকাউন্ট বন্ধ করা কঠিন করা বা ভয় দেখানো বার্তায় “retention” বাড়ে, আস্থা কমে।

৯ · AI-এর যুগে — customer analytics কোড যাচাই

AI assistant churn আর CLV-র কোড দ্রুত দেয়, কিন্তু এই বিষয়ের ভুল চালালে ধরা পড়ে না: সব চলে, শুধু সংখ্যার অর্থ ভুল। প্রায়ই চোখে পড়ে:

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
RFM-এ pd.qcut সরাসরি frequency-তে চালায়অনেক tie-তে ত্রুটি, বা duplicates="drop" দিয়ে চুপচাপ কম দলপ্রতিটি score-এর আকার গুনুন; tie-এর নিয়ম লিখুন
Recency ডেটা তোলার দিন থেকে মাপেLabel window-এর তথ্য feature-এ ঢোকেপ্রতিটি কলামের “কোন তারিখ পর্যন্ত” টেবিল; সময় ধরে test
“ঝুঁকি বেশি যাঁদের, তাঁদের কুপন” লেখেResponse বা ঝুঁকি আর uplift এক নয়এলোমেলো holdout রেখে decile-ভিত্তিক uplift দেখুন
CLV-তে churn বা discount বাদ দেয়, বা সব কাল্পনিক “গড় জীবনকাল” দিয়ে গুণমূল্য কয়েক গুণ ফোলানোহাতে ছোট উদাহরণ; পুরোনো দলের ওপর backtest
পুরোনো lifetimes API বা বানানো parameter নাম লেখেLibrary বদলেছে; ভুল নামে ত্রুটি বা নীরব ভুলনিজের version-এর docs; ছোট ডেটায় চালিয়ে মোট অর্ডারের সঙ্গে মেলান

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

আমার কাছে বাজারঘরের একটি লেনদেনের টেবিল আছে: customer, date, amount; ডেটা কাটার দিন ২০২৫-১২-৩১. ১. pandas-এ RFM score বানাও (quintile, tie কীভাবে ভাঙছ লেখো), প্রতিটি segment-এর আকার আর আয়ের ভাগ দেখাও। ২. ৯০ দিনের inactivity ধরে churn label বানাও। প্রতিটি feature কোন তারিখ পর্যন্ত গোনা, তার একটি টেবিল দাও; আগের cutoff-এ train আর পরের cutoff-এ test করো, একটি সরল baseline-সহ। ৩. এলোমেলো কুপন-পরীক্ষার ডেটায় T-learner দিয়ে uplift, decile-ভিত্তিক uplift আর তার অনিশ্চয়তা দেখাও; response model-এর সঙ্গে তুলনা করো। ৪. পরের ১২ মাসের CLV (মাসিক ১% discount) বের করো; কোন library-র কোন version, আর পুরোনো ডেটায় backtest-এর ফল লেখো। ৫. কোনো warning চাপা দিয়ো না, আর যেখানে নিশ্চিত নও, সেখানে স্পষ্ট বলো।

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

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

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

প্র ০১ডাকযোগে পাঠানো ক্যাটালগের যুগের RFM আজও কেন টিকে আছে, আর তার সীমা কোথায়?

RFM-এর জন্ম ডাকযোগে বিক্রির ব্যবসায় (direct mail). বিশ শতকের মাঝামাঝি থেকে ক্যাটালগ কোম্পানিগুলো দেখত, প্রতিটি ছাপা ক্যাটালগ আর ডাকমাশুলের খরচ আছে, অথচ বেশির ভাগ প্রাপক কিছুই কেনেন না। কাকে পাঠালে লাভ, তা ঠিক করতে তারা অভিজ্ঞতা থেকে তিনটি সংকেত পেয়েছিল: সম্প্রতি কিনেছেন কি না, কতবার কিনেছেন, কত টাকার। প্রচলিত বর্ণনায় ১৯৯০-এর দশকে Arthur Hughes-এর মতো লেখকদের database marketing-এর বই এই পদ্ধতিকে সুসংবদ্ধ করে জনপ্রিয় করে; পদ্ধতিটি তার আগেই শিল্পে চালু ছিল বলে নির্দিষ্ট একজন “আবিষ্কারক” বলা কঠিন।

টিকে থাকার কারণ তিনটি। প্রথমত, এটি সহজ: SQL-এর একটি query বা pandas-এর কয়েক লাইনে হয়ে যায়, কোনো model train করতে হয় না। দ্বিতীয়ত, ব্যাখ্যা সহজ: “শেষ কেনা ২০০ দিন আগে, কিন্তু আগে ৬ বার কিনেছেন” কথাটি মার্কেটিং দলের যে কেউ বোঝেন। তৃতীয়ত, তিনটি সংখ্যা সত্যিই শক্তিশালী সংকেত: অংশ ৭-এর BG/NBD model-ও শুধু এই তিনটির আত্মীয় (x, tx, T) দিয়েই কাজ করে। অনেক তুলনায় দেখা যায়, অনেক feature-এর ML model RFM-এর চেয়ে ভালো করে, কিন্তু ব্যবধান প্রায়ই প্রত্যাশার চেয়ে ছোট।

সীমাগুলোও স্পষ্ট। Quintile আপেক্ষিক: ক্রেতাদের সামগ্রিক আচরণ খারাপ হলেও ২০% সবসময় “৫” পাবেন। Tie-এর ভাঙা এলোমেলো, অংশ ২-এ একই এক অর্ডারের ১০১ জন F = ২ পেয়েছেন। Segment-এর সীমা বিচারভিত্তিক। আর সবচেয়ে বড় কথা, RFM বর্ণনা করে, পদক্ষেপ বলে না: “At risk” দলে ছাড় দেওয়া লাভজনক কি না, তা জানতে পরীক্ষা লাগে। মৌসুমি পণ্যে (ঈদের পোশাক) দীর্ঘ নীরবতা স্বাভাবিকও হতে পারে।

মূল উপলব্ধি: RFM টিকে আছে কারণ এটি সস্তা, বোধগম্য আর আশ্চর্য রকম তথ্যপূর্ণ; একে শুরুর baseline আর আলোচনার ভাষা হিসেবে ব্যবহার করুন, পদক্ষেপের প্রমাণ হিসেবে নয়।

প্র ০২চুক্তিভিত্তিক আর চুক্তিহীন ব্যবসায় “ক্রেতা হারানো” কেন দুটি আলাদা গাণিতিক সমস্যা?

মোবাইল সংযোগ, ইন্টারনেট বা পত্রিকার গ্রাহকত্বে চুক্তি আছে। গ্রাহক চলে গেলে একটি তারিখ লেখা হয়। এখানে churn একটি দৃশ্যমান ঘটনা, আর প্রশ্নটি পর্ব ৫৫-এর survival analysis-এর সরাসরি ক্ষেত্র: কবে চুক্তি শেষ হবে, কোন বৈশিষ্ট্যে ঝুঁকি বেশি, censoring সামলে।

অনলাইন দোকান, রেস্তোরাঁ বা ওষুধের দোকানে চুক্তি নেই। ক্রেতা চলে গেলে কোনো সংকেত আসে না; শুধু নীরবতা। একজন ক্রেতা ১২০ দিন কেনেননি: তিনি কি চলে গেছেন, নাকি তাঁর স্বাভাবিক ছন্দই এমন? এই অনিশ্চয়তাই চুক্তিহীন সমস্যার কেন্দ্র। David Schmittlein, Donald Morrison আর Richard Colombo ১৯৮৭ সালে Management Science-এ Pareto/NBD model দিয়ে প্রশ্নটিকে আনুষ্ঠানিক রূপ দেন: প্রত্যেকে একটি অদৃশ্য সময়ে “মরে যান”, আর তার আগে Poisson ছাঁদে কেনেন। Model-টি শক্তিশালী হলেও হিসাব কঠিন ছিল, তাই বহু বছর মূলত গবেষণায় সীমিত ছিল।

২০০৫ সালে Peter Fader, Bruce Hardie আর Ka Lok Lee Marketing Science-এ BG/NBD প্রকাশ করেন, যেখানে চলে যাওয়া ঘটে শুধু অর্ডারের ঠিক পরে। এই ছোট বদলে likelihood অনেক সহজ হয়, আর স্প্রেডশিটেও fit করা যায় বলে লেখকেরা জোর দেন। পরে Python-এ lifetimes library আর এখন PyMC-Marketing-এর মতো library এই model-গুলো সাধারণ বিশ্লেষকের হাতে এনেছে।

ব্যবহারিক পার্থক্য: চুক্তিভিত্তিক ক্ষেত্রে label নির্ভরযোগ্য, তাই classifier বা survival model সরাসরি চলে। চুক্তিহীন ক্ষেত্রে label নিজেই একটি বাছাই (৬০, ৯০ না ১৮০ দিন), আর অংশ ৩-এর stepper দেখিয়েছে, সেই বাছাই ভুল-churn-এর হার তিন ভাগের এক থেকে সাত ভাগের এক পর্যন্ত বদলায়। Probabilistic model এই বাছাই এড়িয়ে “এখনো সক্রিয় থাকার সম্ভাবনা” দেয়, কিন্তু নিজের কড়া অনুমানের দামে।

মূল উপলব্ধি: চুক্তি থাকলে churn একটি দেখা ঘটনা, না থাকলে একটি অনুমিত অবস্থা; পদ্ধতি বাছার আগে প্রথমে ঠিক করুন আপনার ব্যবসা কোন দলে।

প্র ০৩“কে কিনবেন” থেকে “কুপনের জন্য কে কিনবেন”: uplift modeling কেন একটি আলাদা ক্ষেত্র হলো?

সাধারণ predictive model প্রশ্ন করে P(y | x): এই মানুষের কেনার সম্ভাবনা কত। মার্কেটিং-এর সিদ্ধান্তে প্রশ্নটি ভিন্ন: যোগাযোগ করলে সম্ভাবনা কতটা বদলায়। দুটির পার্থক্য অংশ ৫-এ সংখ্যায় দেখা গেছে: response model-এর সেরা decile-এর uplift প্রায় শূন্য, কারণ সেখানে নিয়মিত ক্রেতা, যাঁরা এমনিই কিনতেন।

ব্রিটেনে Nicholas Radcliffe আর Patrick Surry ১৯৯০-এর দশকের শেষ দিক থেকে এই সমস্যা নিয়ে লেখেন; তাঁদের লেখায় persuadables, sure things, lost causes আর sleeping dogs-এর ভাগ জনপ্রিয় হয়, আর Radcliffe ২০০৭ সালে মূল্যায়নের জন্য Qini বক্ররেখার প্রস্তাব দেন। “Sleeping dogs” বিশেষ শিক্ষণীয়: চুক্তিভিত্তিক ব্যবসায় কিছু গ্রাহককে “রিনিউ করার কথা” মনে করালেই তাঁরা দাম তুলনা শুরু করে চলে যান। এমন মানুষকে ঝুঁকির score দিয়ে বেছে বার্তা পাঠালে churn কমে না, বাড়ে।

মূল বাধা দর্শনগত: একজন মানুষের ক্ষেত্রে কুপন দেওয়া আর না দেওয়া দুটি ফল একসঙ্গে দেখা যায় না। Paul Holland ১৯৮৬ সালে একে causal inference-এর মৌলিক সমস্যা বলেন। তাই uplift কখনো একজন ক্রেতার জন্য “মাপা” হয় না, একই রকম x-এর মানুষের দলে অনুমান করা হয়, আর সেই অনুমান বিশ্বাসযোগ্য হয় মূলত এলোমেলো পরীক্ষায়। পর্যবেক্ষণের ডেটায় (যাঁদের আগে কুপন দেওয়া হয়েছিল, তাঁরা কারা ছিলেন) বাছাইয়ের পক্ষপাত ঢুকে পড়ে; পর্ব ৬৪ সেই সমস্যার কিছু প্রতিকার দেখায়।

Model-এর দিক থেকে Sören Künzel ও সহকর্মীরা ২০১৯ সালে PNAS-এ S-, T- আর X-learner নামে “meta-learner”-দের তুলনা করেন: যেকোনো ML model দিয়ে treatment effect অনুমানের কাঠামো। কোনোটিই সব ক্ষেত্রে সেরা নয়; দলের আকার অসমান হলে X-learner, প্রভাব ছোট হলে সরাসরি uplift tree বা পরিসংখ্যানে শক্ত পদ্ধতি ভালো করতে পারে।

মূল উপলব্ধি: Targeting-এর লক্ষ্য সম্ভাবনা নয়, সম্ভাবনার পরিবর্তন; সেটি মাপতে এলোমেলো তুলনা লাগে, আর একটি holdout দল সবসময় রেখে দেওয়াই নিরাপদ অভ্যাস।

প্র ০৪Lifetime value কি একটি সংখ্যা, না একটি সিদ্ধান্তের হাতিয়ার? CLV-ভিত্তিক সিদ্ধান্তের লাভ আর বিপদ কী?

CLV-র মূল ধারণা অর্থশাস্ত্রের net present value থেকে: ভবিষ্যতের নগদ প্রবাহকে আজকের মূল্যে আনা। মার্কেটিং গবেষণায় ১৯৯০ আর ২০০০-এর দশকে ধারণাটি জোর পায়; Sunil Gupta আর Donald Lehmann-এর মতো লেখকেরা দেখান, m·r/(১ + d − r)-এর মতো সরল সূত্রেও ক্রেতাদের একটি “সম্পদ” হিসেবে মূল্যায়ন করা যায়, এমনকি কোম্পানির মূল্যের মোটা দাগের অনুমানও।

সিদ্ধান্তে CLV-র কাজ তিনটি। (১) নতুন ক্রেতা আনতে কত খরচ যুক্তিসংগত: প্রচারণায় একজন ক্রেতা আনতে যদি ৮০০ টাকা লাগে আর তাঁর প্রত্যাশিত মূল্য (margin-এ) ৬০০, প্রচারণা লোকসানের। (২) কোন চ্যানেল বা ত্রৈমাসিক ভালো ক্রেতা আনে: অংশ ৬-এর cohort টেবিল আর মূল্যের তুলনা। (৩) সেবার মাত্রা: উচ্চ-মূল্যের ক্রেতাকে দ্রুত সহায়তা।

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

ব্যবহারিক পরামর্শ: CLV-কে একক সংখ্যা নয়, পরিসর হিসেবে রিপোর্ট করুন (Bayesian fit বা bootstrap থেকে); revenue নয়, margin-এ মাপুন (ফেরত আর ডেলিভারির খরচ বাদ দিয়ে); নিয়মিত পুরোনো cohort-এ backtest করুন; আর CLV-ভিত্তিক নীতির প্রভাব segment-ভেদে মেপে দেখুন।

মূল উপলব্ধি: CLV একটি অনুমান, যার ভুল সিদ্ধান্তে বড় হয়ে ফেরে; তাকে পরিসরসহ, margin-এ আর নিয়মিত যাচাই করে ব্যবহার করুন, আর তার ভিত্তিতে মানুষের সঙ্গে আচরণের নৈতিক দিক আলাদাভাবে ভাবুন।

প্র ০৫Churn model-এ leakage এত সাধারণ কেন, আর কীভাবে একটি দল তা নিয়মিত ঠেকাতে পারে?

Shachar Kaufman, Saharon Rosset আর Claudia Perlich ২০১২ সালে ACM Transactions on Knowledge Discovery from Data-তে “Leakage in data mining” প্রবন্ধে দেখান, ডেটা-বিজ্ঞানের প্রতিযোগিতা আর বাস্তব প্রকল্পে leakage কতটা ছড়ানো, আর কীভাবে তা “অবাস্তব ভালো” ফল আনে। Churn-এ ঝুঁকি বিশেষ বেশি, কারণ প্রায় সব feature সময়ের সঙ্গে জমা হয় (মোট অর্ডার, শেষ অর্ডারের তারিখ, অভিযোগের সংখ্যা), আর ডেটাবেসের টেবিল সাধারণত “আজকের” অবস্থা রাখে, ইতিহাস নয়।

অংশ ৪-এর পরীক্ষায় ফাঁস ছিল ছোট: শুধু অর্ডারের সংখ্যা আর গড় খরচ label window-এর শেষ পর্যন্ত গোনা। তবু AUC ০.৭৪০ থেকে ০.৮২৭-এ উঠেছে, আর বাস্তবে চালালে ০.৬৩৪-এ নেমেছে। বাস্তবে আরও সূক্ষ্ম ফাঁস দেখা যায়: “শেষ যোগাযোগের কারণ = অ্যাকাউন্ট বন্ধের অনুরোধ” কলাম, retention দলের ফোন করার তালিকা (যা churn-এর ঝুঁকি দেখেই বানানো), বা অন্য টেবিল join করার সময় তারিখ না মেলানো।

দলের অভ্যাসে চারটি ব্যবস্থা কাজে দেয়। (১) প্রতিটি feature-এর জন্য একটি লিখিত “as-of” নিয়ম আর point-in-time join (pandas-এ merge_asof, SQL-এ তারিখের শর্ত); feature store এই কাজ স্বয়ংক্রিয় করে। (২) সময় ধরে মূল্যায়ন: আগের cutoff-এ train, পরের cutoff-এ test, মাঝে label window-এর ফাঁক। (৩) Shadow deployment: model কয়েক সপ্তাহ বাস্তবে score দেয় কিন্তু সিদ্ধান্ত নেয় না; পরে offline আর online AUC তুলনা। বড় ফাঁক মানে leakage বা distribution shift (পর্ব ৫৯)। (৪) Importance-এর তালিকায় একটি feature একাই সব ব্যাখ্যা করলে সন্দেহ (পর্ব ৫৬)।

মানবিক দিকও আছে। ভালো ফল দেখলে আনন্দে আর প্রশ্ন করা হয় না। দলে একজন “সন্দেহবাদী পর্যালোচক”-এর ভূমিকা রাখা, যিনি প্রতিটি অবাক করা উন্নতির পরে প্রথম প্রশ্ন করেন “কোন তারিখ পর্যন্ত?”, সস্তা কিন্তু কার্যকর।

মূল উপলব্ধি: Churn-এর leakage আসে সময় না মানা থেকে; লিখিত as-of নিয়ম, সময় ধরে test আর offline–online তুলনা একে নিয়মিত ধরে ফেলে।

অনুশীলন

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

  1. পূর্ণ numerical: (ক) RFM সীমা: R দিন ০–২২ → ৫, ২৩–৬৬ → ৪, ৬৭–১৪৪ → ৩, ১৪৫–৩০২ → ২, ৩০৩+ → ১; F অর্ডার ১ → ১, ২ → ২, ৩–৪ → ৩, ৫–৭ → ৪, ৮+ → ৫. ক্রেতা A: ৯০ দিন, ৩ অর্ডার; ক্রেতা B: ১৫ দিন, ২ অর্ডার। R, F আর segment (Champions R ≥ ৪ ও F ≥ ৪; Loyal R ≥ ৩ ও F ≥ ৩; New R ≥ ৪ ও F ≤ ২; At risk R ≤ ২ ও F ≥ ৩; Lost R ≤ ২ ও F ≤ ২; বাকিরা Need attention) বের করুন। (খ) মাসিক margin ১৫০ টাকা, r = ০.৭৫, d = ১%: প্রথম তিন মাসের discount-সহ মূল্য আর অসীম CLV (মাসের শেষে টাকা, মাস t-তে থাকার সম্ভাবনা rt−১)। (গ) এলোমেলো পরীক্ষায় কুপন পাওয়া ৪০০ জনের ১২০ জন আর না পাওয়া ৪০০ জনের ৯০ জন কিনেছেন। Uplift আর বাড়তি অর্ডার কত? কুপনে প্রতিটি অর্ডারে ১০০ টাকা ছাড় হলে (শুধু যাঁরা কেনেন, তাঁরা ছাড় পান), প্রতিটি বাড়তি অর্ডারের দাম কত পড়ল?

    (ক) A: R = ৩, F = ৩, তাই Champions বা New নয়, Loyal (R ≥ ৩, F ≥ ৩)। B: R = ৫, F = ২, তাই New. (খ) মাস ১: ১৫০/১.০১ = ১৪৮.৫১; মাস ২: ১৫০ × ০.৭৫/১.০২০১ = ১১০.২৮; মাস ৩: ১৫০ × ০.৫৬২৫/১.০৩০৩০১ = ৮১.৮৯; তিন মাসে ৩৪০.৬৮. অসীম: ১৫০/(১.০১ − ০.৭৫) = ১৫০/০.২৬ = ৫৭৬.৯২. (গ) কেনার হার ০.৩০ বনাম ০.২২৫, uplift ০.০৭৫, অর্থাৎ ৪০০ জনে ৩০টি বাড়তি অর্ডার. ছাড় পেয়েছেন ১২০ জন, খরচ ১২,০০০ টাকা; প্রতিটি বাড়তি অর্ডারের দাম ১২,০০০/৩০ = ৪০০ টাকা, কারণ ৯০ জন এমনিই কিনতেন আর তাঁরাও ছাড় পেয়েছেন। অর্ডারপ্রতি margin ৪০০ টাকার কম হলে কুপনটি লোকসানের।

  2. সমালোচনা ও নকশা: একজন বিশ্লেষকের প্রস্তাব: “২০২৫-এর ডিসেম্বরে ডেটাবেস থেকে সব ক্রেতার ‘মোট অর্ডার’, ‘শেষ অর্ডারের তারিখ’ আর ‘মোট খরচ’ তুলেছি। Label: সেপ্টেম্বর–নভেম্বরে অর্ডার নেই = churn. এলোমেলো ৮০/২০ ভাগে gradient boosting-এ AUC ০.৯৬! এখন churn-ঝুঁকির শীর্ষ ২০% ক্রেতাকে ২০% ছাড়ের কুপন পাঠাব, আর CLV = গড় অর্ডারের টাকা × গড় অর্ডারের সংখ্যা × ৩ বছর।” কী কী ভুল, আর আপনার নকশা কী?

    সমস্যা: (১) Feature ডিসেম্বরে তোলা, label সেপ্টেম্বর–নভেম্বরের: “শেষ অর্ডারের তারিখ” আর “মোট অর্ডার” label window-কে সরাসরি দেখে, তাই AUC ০.৯৬ মূলত target leakage. (২) কোনো cutoff বা unit নেই: কারা সেপ্টেম্বরে সক্রিয় ছিলেন, সেটিই ঠিক হয়নি; বহু আগে চলে যাওয়া মানুষও “churn”. (৩) এলোমেলো ভাগ, সময় ধরে নয়। (৪) ঝুঁকি বেশি মানে কুপনে সাড়া বেশি নয় (অনেকেই “lost causes”), আর holdout না থাকায় প্রভাব মাপা যাবে না। (৫) CLV churn আর discount উপেক্ষা করে, সবাই তিন বছর থাকবেন ধরে; revenue-তে, margin-এ নয়। নকশা: churn-এর সংজ্ঞা (যেমন ৯০ দিন) আর unit লিখে নেওয়া; একাধিক cutoff, feature শুধু cutoff পর্যন্ত, আগের cutoff-এ train ও পরের cutoff-এ test, RFM-ভিত্তিক baseline-সহ; কুপনের জন্য এলোমেলো পরীক্ষা চালিয়ে T-learner বা অনুরূপ uplift model, আর স্থায়ী ১০% holdout; CLV-র জন্য cohort বক্ররেখা বা BG/NBD + Gamma-Gamma, margin-এ, পুরোনো cohort-এ backtest-সহ, পরিসর দিয়ে রিপোর্ট।

  3. আউটপুট অনুমান করুন: চালানোর আগে তিনটি লাইন লিখুন।
    import pandas as pd
    s = pd.Series([1, 1, 1, 2, 5, 9])      # ছয়জন ক্রেতার অর্ডার সংখ্যা
    r = s.rank(method="first")
    print(r.tolist())
    print(pd.qcut(r, 3, labels=[1, 2, 3]).astype(int).tolist())
    print(s.rank(method="average").tolist())

    ডেস্কটপে সত্যিকারের run (Python 3.13.5, pandas 3.0.2):

    [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
    [1, 1, 2, 2, 3, 3]
    [2.0, 2.0, 2.0, 4.0, 5.0, 6.0]

    method="first" সমান মানগুলোকে তাদের অবস্থানের ক্রমে আলাদা rank দেয়: তিনটি “১” পায় ১, ২, ৩. তিন ভাগে কাটলে প্রথম দুজন score ১, তৃতীয়জন score ২: একই এক অর্ডারের তিন ক্রেতার দুই রকম score, শুধু টেবিলে কে আগে ছিলেন তার জন্য। method="average" তিনজনকে একই গড় rank ২ দেয়; তখন quintile কাটলে tie একসঙ্গে থাকে কিন্তু দলের আকার অসমান হয়, আর অনেক tie থাকলে qcut-এর সীমা মিলে গিয়ে ত্রুটি দিতে পারে। কোন নিয়ম নিলেন, তা লিখে রাখুন।

  4. কোডিং: পাতার bazar_cust.py দিয়ে একটি function লিখুন churn_labels(cut, w), যা cutoff-এর আগের ১৮০ দিনে সক্রিয় ক্রেতাদের জন্য “পরের w দিনে অর্ডার নেই” label ফেরত দেয়। Cutoff ২০২৫-০৩-৩১-এ w = ৬০, ৯০, ১৮০-এর জন্য churn-এর হার ছাপান, আর TX_ALL (simulation-এর ভবিষ্যৎ) দিয়ে গুনুন churn বলা কতজন পরে আবার কিনেছেন। কোন w বাছবেন, কেন?
    import pandas as pd
    from bazar_cust import tx, TX_ALL
    
    def churn_labels(cut, w):
        cut = pd.Timestamp(cut)
        last = tx[tx.date <= cut].groupby("customer").date.max()
        active = last.index[(cut - last).dt.days <= 180]
        later = tx[(tx.date > cut) & (tx.date <= cut + pd.Timedelta(days=w))]
        return pd.Series(~active.isin(later.customer), index=active)
    
    lost = lambda idx: ~idx.isin(TX_ALL[TX_ALL.date > "2025-03-31"].customer)
    for w in (60, 90, 180):
        lab = churn_labels("2025-03-31", w)
        back = (lab & ~lost(lab.index)).sum()          # churn বলা হলো, পরে ফিরলেন
        print(f"{w:>3} d: churn {lab.mean():.1%}, came back later {back}"
              f" ({back / lab.sum():.1%} of churn labels), wait {w} d")

    ডেস্কটপে সত্যিকারের run (Python 3.13.5, pandas 3.0.2, NumPy 2.4.6) ছাপে:

     60 d: churn 62.0%, came back later 187 (34.3% of churn labels), wait 60 d
     90 d: churn 55.5%, came back later 130 (26.6% of churn labels), wait 90 d
    180 d: churn 47.8%, came back later 62 (14.8% of churn labels), wait 180 d

    এগুলোই অংশ ৩-এর stepper-এর সংখ্যা। ৬০ দিনে প্রায় তিনজনে একজন “churn” আসলে বিরতিতে ছিলেন; ১৮০ দিনে সাতজনে একজন, কিন্তু ছয় মাস অপেক্ষা। বাস্তবে TX_ALL নেই, তাই পুরোনো cutoff আর পরের এক বছরের ডেটা দিয়ে একই হিসাব করুন। বাছাই নির্ভর করে ভুল ছাড় বনাম দেরির খরচে; এই ডেটায় ৯০ দিন একটি যুক্তিসংগত মাঝামাঝি।

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

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

ক্রমএকটি চুক্তিহীন অনলাইন দোকানে সময় মেনে churn model বানিয়ে কুপনের সিদ্ধান্ত নেওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ব্যবসার প্রশ্ন আর churn-এর সংজ্ঞা (যেমন ৯০ দিন অর্ডার নেই) লিখে নেওয়া
  2. Cutoff তারিখ আর unit (cutoff-এ সক্রিয় ক্রেতা) ঠিক করা
  3. শুধু cutoff পর্যন্ত তথ্য দিয়ে feature বানানো
  4. Cutoff-এর পরের ৯০ দিন দেখে label বসানো
  5. আগের cutoff-এ train করে পরের cutoff-এ test করা
  6. এলোমেলো পরীক্ষা আর uplift দিয়ে কাকে কুপন দেবেন তা ঠিক করা

RFM-এ quintile score দেওয়া হচ্ছে। একজন ক্রেতার শেষ অর্ডার ৫ দিন আগে, যা সব ক্রেতার মধ্যে সবচেয়ে সাম্প্রতিক ২০%-এর মধ্যে পড়ে। তাঁর R score কত?

  • ১, কারণ recency-র সংখ্যা সবচেয়ে ছোট
  • ৩
  • ৫
  • F আর M না জেনে বলা যায় না

একটি চুক্তিহীন অনলাইন দোকানে churn-এর সংজ্ঞা “৬০ দিন অর্ডার নেই” থেকে বদলে “১৮০ দিন অর্ডার নেই” করা হলো। সাধারণত কী ঘটে?

  • Churn-এর হার বাড়ে আর label দ্রুত পাওয়া যায়
  • ভুল করে churn বলা মানুষ কমে, কিন্তু label পেতে অনেক বেশি অপেক্ষা
  • কিছুই বদলায় না, কারণ যাঁরা চলে গেছেন তাঁরা দুই সংজ্ঞাতেই churn
  • ভুল-churn বাড়ে কিন্তু অপেক্ষা কমে

একটি churn model-এ cutoff ৩১ মার্চ, label = ১ এপ্রিল–২৯ জুনে অর্ডার নেই। “মোট অর্ডার” feature-টি ডেটাবেস থেকে ৩০ জুন তোলা হয়েছে। Test AUC ০.৮৩, আর সব feature cutoff-এ গুনলে ০.৭৪. সবচেয়ে সম্ভাব্য ব্যাখ্যা কী?

  • ৩০ জুনের feature বেশি তথ্যবহুল, তাই সেটিই রাখা উচিত
  • Model overfit করেছে, regularization বাড়ালে ঠিক হবে
  • Class imbalance-এর কারণে AUC বিভ্রান্তিকর
  • Feature-এ label window-এর তথ্য ঢুকেছে (target leakage)

একটি এলোমেলো কুপন-পরীক্ষার পরে দুটি model-এর শীর্ষ decile দেখা হলো। Model A-র শীর্ষ decile-এ কুপন-দলের কেনার হার ০.৯০ আর কুপনহীন দলের ০.৮৯. Model B-র শীর্ষ decile-এ ০.৪০ আর ০.২২. বাজেট শুধু এক decile-এর জন্য। কোন model-এর বাছাই দিয়ে কুপন দেওয়া উচিত?

  • Model B, কারণ তার decile-এ কুপনের কারণে বাড়তি কেনা অনেক বেশি
  • Model A, কারণ তার decile-এর কেনার হার সবচেয়ে বেশি
  • দুটি সমান, কারণ দুই decile-এই কুপন-দল বেশি কিনেছে
  • কোনোটিই নয়, এলোমেলো পরীক্ষা থেকে uplift মাপা যায় না

একজন ক্রেতার মাসিক margin ১০০ টাকা, প্রতি মাসে থেকে যাওয়ার সম্ভাবনা ০.৯, মাসিক discount rate ১%. টাকা আসে মাসের শেষে, আর মাস t-তে সক্রিয় থাকার সম্ভাবনা ০.৯t−১. অসীম সময়ের CLV প্রায় কত?

  • ১০০০ টাকা
  • ৯০৯ টাকা
  • ৮১৮ টাকা
  • ১২০০ টাকা

BG/NBD model-এর একটি অনুমান কোনটি?

  • ক্রেতা সময়ের যেকোনো মুহূর্তে একটি স্থির হারে চলে যান
  • সব ক্রেতার কেনার হার সমান
  • প্রতিটি অর্ডারের পরে ক্রেতা একটি নিজস্ব সম্ভাবনায় চিরতরে চলে যান
  • অর্ডারের টাকা বেশি হলে চলে যাওয়ার সম্ভাবনা কম

একটি cohort retention টেবিলে ডিসেম্বরে যোগ দেওয়া ক্রেতাদের “তৃতীয় মাসের retention” ৫%, অথচ পুরোনো সব দলের তৃতীয় মাসে ২৫–৩০%. ডেটা কাটা হয়েছে ৩১ জানুয়ারি। প্রথমে কী সন্দেহ করবেন?

  • ডিসেম্বরের প্রচারণা খুব খারাপ ক্রেতা এনেছে
  • শীতকালে সবাই কম কেনেন
  • দলটি ছোট বলে এলোমেলো ওঠানামা
  • তৃতীয় মাসটি পুরো দেখা হয়নি, তবু হিসাবে ঢুকেছে

শব্দকোষ

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

মনে রাখুন

  1. RFM: recency (উল্টো ক্রমে), frequency, monetary-র quintile score; segment-এর নিয়ম ব্যবসার সিদ্ধান্ত, tie-এর নিয়ম লিখে রাখুন। RFM বর্ণনা করে, পদক্ষেপ প্রমাণ করে না।
  2. Churn-এর সংজ্ঞা: চুক্তিহীন ব্যবসায় inactivity window একটি বাছাই; ছোট জানালায় ভুল-churn বেশি, বড় জানালায় অপেক্ষা বেশি। সময়ের প্রশ্নে survival (পর্ব ৫৫)।
  3. সময় মেনে model: feature শুধু cutoff পর্যন্ত, label পরের জানালায়, আগের cutoff-এ train ও পরের cutoff-এ test. অবাক করা AUC মানে আগে leakage খুঁজুন।
  4. Uplift: লক্ষ্য P(কিনবেন | কুপন) − P(কিনবেন | কুপন নেই), এলোমেলো পরীক্ষা থেকে; T-learner = দুই দলে দুটি model-এর বিয়োগ। Response model “sure things” বাছে।
  5. CLV: m/(১ + d − r) হাতে; বাস্তবে cohort বক্ররেখা বা BG/NBD + Gamma-Gamma, margin-এ, পরিসরসহ আর backtest করে। ইতিহাসের গড় বহুগুণ ফোলাতে পারে।

পরের পর্বে: পর্ব ৬৬ (Dashboard — Streamlit দিয়ে model আর metric দেখানো)। এই পর্বের RFM segment, retention বক্ররেখা আর churn score কীভাবে একটি dashboard-এ সিদ্ধান্ত নেওয়ার মানুষের সামনে আনবেন, আর কোন metric কোথায় বসবে।

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

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