آزمایش بیزی AB ــ قسمت دوم: درآمد
این متن، دومین قسمت از مجموعه روشهای آزمایش AB بیزی در موقعیتهای زندگی واقعی است. در اینجا از برخی مفاهیم معرفیشده در قسمت اول (لینک دسترسی به آن در انتهای مطلب قرار دارد) استفاده میشود:
- مدلسازی و تجزیه و تحلیل معیارهای آزمایش بر اساس تبدیل (معیارهای نرخی)
- مدلسازی و تجزیه و تحلیل معیارهای آزمایشی بر اساس درآمد (معیارهای پیوسته)
- محاسبهی طول زمان آزمایش
- انتخاب احتمال پیشین مناسب
- اجرای آزمایشات با چند متغیر
بستر آزمایش
فرض کنید اخیراً در یکی از ویژگیهای فروشگاه در برنامه خود تغییرات UX ایجاد کردهایم. باور ما بر این است که این تغییرات باعث شدهاند کاربران خریدهای بیشتری از طریق این برنامه انجام دهند؛ قبل از اعمال این تغییرات در کل پایگاه کاربری قصد داریم این باور را در چارچوب AB بیازماییم. پس مفروض این است که تغییرات ایجادشده باعث میشوند میانگین درآمد حاصل از هر کاربر به میزان چشمگیری افزایش پیدا کند.
درآمد حاصل از هر کاربر را بهصورت یک متغیر تصادفی R=X * Y مدل میکنیم.
در این معادله، X یک متغیر تصادفی برنولی است و بر این دلالت دارد که آیا کاربر خریدی انجام داده است یا خیر، با احتمال تبدیل: ?: ? ∼ ???(?).
Y یک متغیر تصادفی نمایی است و حجم خرید را نشان میدهد، پارامتر نرخ این متغیر عبارت است از: ?: ? ∼ ??(?).
در این مدل، میانگین درآمد حاصل از هر فروش به صورت 1/? و میانگین درآمد بهازای هر کاربر به صورت ?/? مشخص میشود.
گام اول این است که با توجه به دادههای گذشته، توزیعهای پیشین مناسب برای پارامترهای کلیدی ? و ? در این مدل انتخاب شوند.
سپس آستانهی زیان تعیین میشود؛ یعنی بزرگترین مقدار زیان مورد انتظار که در صورت بهاشتباه انتخابکردن متغیر، در این سناریو پذیرفته میشود. روشهای آزمایش بیزی نیز همچون سایر مدلهای آماری بر برآورد دادههای دنیای واقعی مبتنی هستند. به همین دلیل، همیشه، امکان نتیجهگیری اشتباه از آزمایشها هست. آستانه زیان کمک میکند که در صورت به دست آوردن نتایج غلط، مطمئن شد که میانگین درآمد حاصل از هر کاربر بیشتر از حد آستانه افت نخواهد کرد.
در نهایت نمونهها در قالب آزمایش تصادفی بیان میشوند و از آنها برای بهروزرسانی توزیعهای پیشین، و اصلاح باورهایمان، راجع به ? و ? در نسخههای کنترل و تیمار از فروشگاه استفاده میشود. توزیعهای پسین که بدین طریق به دست میآیند در محاسبهی احتمالات و زیان موردانتظاری که مورد نظر ماست نقش دارند.
بهمنظور انتخاب توزیع پیشین برای این پارامترها، ابتدا باید دادههایی را که اخیراً از خرید کاربران جمعآوری شده است بررسی کرد. یک دیتاست پیشینی نمونه برای این آزمایشها تولید شده است.
import pandas as pd import numpy as npprior_data = pd.read_csv('prior_data.csv')print(prior_data.head()) print(prior_data.shape)
از آنجایی که این دیتاست به صورت مصنوعی تولید شده است، قبلاً قالب ایدهآلی برای این آزمایش دارد. در دنیای واقعی، احتمالاً باید مقداری عملیات ETL (استخراج، تبدیل، بارگذاری) روی دادهها انجام داد تا به قالب مورد نظر درآیند. پرداختن به این موضوع خارج از حوصله این مقاله است.
همانطور که مشاهده میکنید، تعداد کاربران نمونه 5268 نفر است، و برای هر کاربر مشخص شده که خرید انجام شده است یا خیر. برای کاربرانی که خرید کردهاند، میتوان حجم خرید را هم دید. در ادامه میتوان نرخ تبدیل پیشین، میانگین درآمد حاصل از هر فروش، پارامتر نرخ برای درآمد حاصل از هر فروش و میانگین درآمد حاصل از هر کاربر را به دست آورد.
conversion_rate = prior_data['converted'].sum()/prior_data.shape[0]converted = prior_data[prior_data['converted'] == 1] avg_purchase = converted['revenue'].mean()print(f'Prior Conversion Rate is {round(conversion_rate, 3)}. Average Revenue per Sale is {round(avg_purchase, 3)}.')print(f'Rate Parameter for Revenue per Sale is {round(1/avg_purchase, 3)}. Average Revenue per User is {round(conversion_rate*avg_purchase, 3)}.')
انتخاب توزیع پیشین
با استفاده از اطلاعات بالا میتوان توزیعهای پیشین را برای ? و ? انتخاب کرد. طبق منطق قسمت اول، میتوان توزیع پیشین Beta(7,15) را برای احتمال تبدیل ? برگزید. به بیان خلاصه، دلیل انتخاب توزیع بتا این است که توزیعی انعطافپذیر در بازه [0,1] بوده و یک پیشین مزدوج خوب به شمار میرود.
برای ?، پارامتر نرخ درآمد حاصل از هر فروش، از توزیع گاما استفاده میشود، زیرا هم توزیع انعطافپذیری در بازه [0,∞) و هم یک پیشین مزدوج خوب است. این توزیع محاسبات پسینها بر اساس دادههای آزمایشی را آسانتر میکند.
میتوان توزیع پیشین بسیار ضعیف ?????(0.1, 0.1) را انتخاب کرد.
تنظیم آستانه زیان
بعد از انتخاب توزیع پیشین، باید مقدار ? را مشخص کرد؛ ? بالاترین مقدار زیان موردانتظار است که در صورت ارتکاب اشتباه در انتخاب متغیر همچنان قابلقبول خواهد بود. در این تمرین فرض میشود که فروشگاه منبع اصلی درآمد نیست، اما بسیار مهم است و به همین دلیل، باید در انتخاب مقدار ? محتاط بود: ? = 0.005.
اکنون توزیعهای پیشین و آستانه زیان برای زیان مورد انتظار در اختیار است، پس میتوان آزمایش را اجرا کرد و دادههای لازم را از آن جمعآوری نمود.
نتایج آزمایشات
فرض کنید، چند هفتهای،آزمایشها در حال اجرا بودهاند و میخواهیم ببنیم میتوان از دادههای آن نتیجهای استخراج کرد یا خیر. بدین منظور، باید با استفاده از دادههای آزمایش، توزیعهای پسین را محاسبه کرد. سپس، از این توزیعهای پسین برای محاسبه احتمال بهبود هر متغیر و احتمال زیان موردانتظار بهخاطر انتخاب اشتباه هر متغیر استفاده کرد.
experiment_data = pd.read_csv('experiment_data.csv')print(experiment_data.head()) print(experiment_data.shape)
همانطور که مشاهده میکنید این دیتاست مشابه دیتاست قبلی است که یک ستون اضافی دارد. این ستون گروهی که کاربر به آن اختصاص داده شده و متغیری که کاربران دیدهاند مشخص میکند. بار دیگر اشاره به این تذکر سودمند است که چون این دیتاست بهطور مصنوعی تولید شده، از پیش قالب ایدهآل دارد و لازم نیست عملیات ETL اضافی روی آن اجرا شود.
حالا وقت تجمیع دادههاست.
results = experiment_data.groupby('group').agg({'userId': pd.Series.nunique, 'converted': sum, 'revenue': sum})results.rename({'userId': 'sampleSize'}, axis=1, inplace=True)results['conversionRate'] = results['converted']/results['sampleSize']results['revenuePerSale'] = results['revenue']/results['converted'] print(results)
با بررسی نتایج در مییابیم که هر دو گروه نرخ تبدیل یکسانی دارند، اما درآمد حاصل از هر فروش برای گروه تیمار بیشتر است. برای بهروزرسانی باورهایمان درباره ? و ? این دو متغیر (تیمار و کنترل)، باید محاسبات بیشتری انجام شود.
با استفاده از محاسباتی که در مقاله قبلی توضیح داده شده است، میتوان توزیع پسین ? را برای هر دو نوع متغیر حساب کرد.
from scipy.stats import beta, gamma import seaborn as sns import matplotlib.pyplot as pltcontrol_cr = beta(7 + results.loc['control', 'converted'], 15 + results.loc['control', 'sampleSize'] - results.loc['control', 'converted'])treatment_cr = beta(7 + results.loc['treatment', 'converted'], 15 + results.loc['treatment', 'sampleSize'] - results.loc['treatment', 'converted'])fig, ax = plt.subplots()x = np.linspace(0,1,1000)ax.plot(x, control_cr.pdf(x), label='control') ax.plot(x, treatment_cr.pdf(x), label='treatment') ax.set_xlabel('Conversion Probability') ax.set_ylabel('Density') ax.set_title('Experiment Posteriors') ax.legend()
توزیعهای پسین ?_? و ?_? تقریباً یکشکل هستند. بعد از تجزیه و تحلیل متوجه میشویم که تیمار تأثیر چندانی روی احتمال تبدیل نداشته است.
حال باید دید تیمار چطور روی پارامتر نرخ درآمد تأثیر گذاشته است. از نتیجه [2] برای محاسبه ?_? و ?_? استفاده میشود.
فرض کنید پیشین این بوده است:
? ∼ ????(?, Θ)
فرض کنید متغیری برای n بازدیدکننده نمایش داده شود، c کاربر با میانگین درآمد حاصل از هر فروش s متقاعد به خرید شدهاند. پس، توزیع پسین بدین شکل مشخص میشود:
?|?, ∼ ?????(? + ?, Θ/(1 + Θ??))
در ادامه، توزیعهای پسین ?_? و ?_? را محاسبه میکنیم.
control_rr = gamma(a=(0.1 + results.loc['control', 'converted']), scale=(0.1/(1 + (0.1)*results.loc['control', 'converted']*results.loc['control', 'revenuePerSale'])))treatment_rr = gamma(a=(0.1 + results.loc['treatment', 'converted']), scale=(0.1/(1 + (0.1)*results.loc['treatment', 'converted']*results.loc['treatment', 'revenuePerSale'])))fig, ax = plt.subplots()x = np.linspace(0,3,1000)ax.plot(x, control_rr.pdf(x), label='control') ax.plot(x, treatment_rr.pdf(x), label='treatment') ax.set_xlabel('Rate Parameter') ax.set_ylabel('Density') ax.set_title('Experiment Posteriors') ax.legend()
توزیعهای پسین هیچگونه همپوشانی ندارند، پس میتوانیم تقریباً با قطعیت بگوییم که یکی از متغیرها از دیگری بهتر بوده است. پارامتر نرخ تیمار از پارامتر نرخ کنترل کمتر بوده است، پس متغیر تیمار بهتر بوده است، زیرا منجر به میانگین درآمد حاصل از هر فروش بالاتری میشود. برای درک بهتر این نکته، توزیعهای پسین میانگین درآمد حاصل از هر فروش رسم میشود.
fig, ax = plt.subplots()x = np.linspace(0,3,1000) z = [1/i for i in x]ax.plot(x, control_rr.pdf(z), label='control') ax.plot(x, treatment_rr.pdf(z), label='treatment') ax.set_xlabel('Avg Revenue per Sale') ax.set_ylabel('Density') ax.set_title('Experiment Posteriors') ax.legend()
بهوضوح میتوان دید میانگین درآمد حاصل از هر فروش برای متغیر تیمار بیشتر از متغیر کنترل است.
پس هر دو نوع متغیر نرخ تبدیل مشابهی دارند، اما تیمار میانگین درآمد حاصل از هر فروش بالاتری دارد، بنابراین، میانگین درآمد حاصل از هر کاربر نیز برای این متغیر باید بیشتر باشد. حال میخواهیم نمودار پسین میانگین درآمد حاصل از هر کاربر را رسم کنیم تا نتیجهگیری انجامشده تأیید شود. مقداری شبیهسازی انجام میدهیم تا در این مسئله به ما کمک کند.
control_conversion_simulation = np.random.beta(7 + results.loc['control', 'converted'], 15 + results.loc['control', 'sampleSize'] - results.loc['control', 'converted'], size=100000)treatment_conversion_simulation = np.random.beta(7 + results.loc['treatment', 'converted'], 15 + results.loc['treatment', 'sampleSize'] - results.loc['treatment', 'converted'], size=100000)control_revenue_simulation = np.random.gamma(shape=(0.1 + results.loc['control', 'converted']), scale=(0.1/(1 + (0.1)*results.loc['control', 'converted']*results.loc['control', 'revenuePerSale'])), size=100000)treatment_revenue_simulation = np.random.gamma(shape=(0.1 + results.loc['treatment', 'converted']), scale=(0.1/(1 + (0.1)*results.loc['treatment', 'converted']*results.loc['treatment', 'revenuePerSale'])), size=100000)control_avg_purchase = [i/j for i,j in zip(control_conversion_simulation, control_revenue_simulation)]treatment_avg_purchase = [i/j for i,j in zip(treatment_conversion_simulation, treatment_revenue_simulation)]fig, ax = plt.subplots()x = np.linspace(0,1,1000)ax.hist(control_avg_purchase, density=True, label='control', histtype='stepfilled', bins=100) ax.hist(treatment_avg_purchase, density=True, label='treatment', histtype='stepfilled', bins=100) ax.set_xlabel('Avg Revenue per User') ax.set_ylabel('Density') ax.set_title('Experiment Posteriors') ax.legend()
بهوضوح، متغیر تیمار نسبت به متغیر کنترل به نتایج بهتری در میانگین درآمد حاصل از هر کاربر دست یافته است. بعد از بررسی این توزیعهای پسین، میتوان با سطح اطمینان بالایی گفت متغیر تیمار بهتر از متغیر کنترل است. با این حال، برای کمیسازی این یافته باید ?(?_?/?_? ≥ ?_?/?_?) و ?[?](?) (زیان موردانتظار در صورت انتخاب متغیر تیمار اشتباه) را محاسبه کرد.
بر اساس نتایج شبیهسازی متوجه میشویم که ?(?_?/?_? ≥ ?_?/?_?) = 1 ، پس، تیمار به احتمال 100% از کنترل بهتر است.
treatment_won = [i <= j for i,j in zip(control_avg_purchase, treatment_avg_purchase)]chance_to_beat_ctrl = np.mean(treatment_won) print(f'Chance of beating control: {round(chance_to_beat_ctrl, 3)}.')
بعد از محاسبه احتمال بهتربودن تیمار، باید ?[?](?) محاسبه شود. تابع زیان هر متغیر بدین شکل به دست میآید:
از این فرمول برای محاسبه زیان موردانتظار استفاده میشود.
loss_control = [max(j - i, 0) for i,j in zip(control_avg_purchase, treatment_avg_purchase)]loss_treatment = [max(i - j, 0) for i,j in zip(control_avg_purchase, treatment_avg_purchase)]all_loss_control = [int(i)*j for i,j in zip(treatment_won, loss_control)]all_loss_treatment = [(1 - int(i))*j for i,j in zip(treatment_won, loss_treatment)]expected_loss_control = np.mean(all_loss_control) expected_loss_treatment = np.mean(all_loss_treatment)print(f'Expected loss of choosing control: {round(expected_loss_control, 3)}. Expected loss of choosing treatment: {round(expected_loss_treatment, 3)}')
بعد از اجرای شبیهسازیها میتوان دید:
?[?](?)=0 < 0.005=?.
از آنجایی که زیان موردانتظار یکی از متغیرها پایینتر از مقدار آستانهای است که پیشتر، در آغاز آزمایش، تنظیم شده است، میتوان گفت آزمایش به معناداری رسیده است. پس با اطمینان بالا میتوان نتیجه گرفت که تیمار از کنترل بهتر است و هزینه مورد انتظار در صورت انتخاب اشتباه تیمار بزرگتر از آنچه قابل قبول بوده نخواهد بود. بدین ترتیب با قطعیت پیشنهاد میشود که متغیر تیمار صفحه پرفروشها برای باقی پایگاه کاربری نیز پیاده شود.
شما میتوانید از طریق لینک زیر به قسمت اول این مطلب دسترسی داشته باشید:
[button href=”https://hooshio.com/%d8%aa%d8%a8%d8%af%db%8c%d9%84-%d9%87%d8%a7-%d8%af%d8%b1-%d8%a2%d8%b2%d9%85%d8%a7%db%8c%d8%b4-%d8%a8%db%8c%d8%b2%db%8c-ab/” type=”btn-default” size=”btn-lg”]قسمت اول[/button]