การทดลองใดๆ ก็ตามต้องแลกมาด้วยผลลัพธ์ที่รวดเร็วและความไวต่อตัวชี้วัด หากตัวชี้วัดที่เลือกมีความแปรปรวนมาก เราต้องรอเป็นเวลานานเพื่อให้แน่ใจว่าผลลัพธ์ของการทดลองนั้นแม่นยำ ลองพิจารณาวิธีหนึ่งที่จะช่วยให้นักวิเคราะห์เพิ่มประสิทธิภาพการทดลองของตนโดยไม่เสียเวลาหรือความไวต่อตัวชี้วัดมากเกินไป
สมมติว่าเราทำการทดลองมาตรฐานเพื่อทดสอบอัลกอริทึมการจัดอันดับใหม่ โดยใช้ระยะเวลาเซสชันเป็นตัวชี้วัดหลัก นอกจากนี้ โปรดพิจารณาว่าผู้ชมของเราสามารถแบ่งกลุ่มได้คร่าวๆ เป็นสามกลุ่ม ได้แก่ วัยรุ่น 1 ล้านคน ผู้ใช้ 2 ล้านคนที่มีอายุระหว่าง 18-45 ปี และผู้ใช้ 3 ล้านคนที่มีอายุ 45 ปีขึ้นไป การตอบสนองต่ออัลกอริทึมการจัดอันดับใหม่จะแตกต่างกันอย่างมากระหว่างกลุ่มผู้ชมเหล่านี้ ความแตกต่างที่กว้างนี้ทำให้ความอ่อนไหวของตัวชี้วัดลดลง
กล่าวอีกนัยหนึ่ง ประชากรสามารถแบ่งได้ออกเป็น 3 ชั้น ดังต่อไปนี้
สมมติว่าส่วนประกอบทุกส่วนมีการแจกแจงแบบปกติ จากนั้น เมตริกหลักสำหรับประชากรก็มีการแจกแจงแบบปกติเช่นกัน
เรา แบ่งผู้ใช้ทั้งหมดออกจากกลุ่มประชากรแบบสุ่ม ตามการออกแบบการทดลองแบบคลาสสิกโดยไม่พิจารณาความแตกต่างระหว่างผู้ใช้ของเรา ดังนั้น เราจึงพิจารณาตัวอย่างด้วยค่าที่คาดหวังและความแปรปรวนดังต่อไปนี้
อีกวิธีหนึ่งคือ การแบ่งแบบสุ่มภายในแต่ละชั้นตามน้ำหนัก ของชั้นในประชากรทั่วไป
ในกรณีนี้ค่าคาดหวังและความแปรปรวนมีดังต่อไปนี้
ค่าที่คาดหวังจะเท่ากับค่าที่เลือกครั้งแรก อย่างไรก็ตาม ความแปรปรวนจะน้อยกว่า ซึ่งรับประกันความไวต่อเมตริกที่สูงขึ้น
ตอนนี้มาพิจารณา วิธีของ Neyman กัน พวกเขาแนะนำให้แบ่งผู้ใช้แบบสุ่มในแต่ละกลยุทธ์โดยกำหนดน้ำหนักให้ชัดเจน
ดังนั้นค่าคาดหวังและความแปรปรวนจะเท่ากับค่าต่อไปนี้ในกรณีนี้
ค่าที่คาดหวังจะเท่ากับค่าที่คาดหวังในกรณีแรกแบบอะซิมโทติก อย่างไรก็ตาม ความแปรปรวนจะน้อยกว่ามาก
เราได้พิสูจน์ประสิทธิภาพของวิธีนี้ในเชิงทฤษฎีแล้ว มาจำลองตัวอย่างและทดสอบวิธีการแบ่งชั้นตามประสบการณ์กัน
มาพิจารณาสามกรณี:
เราจะใช้ทั้งสามวิธีในทุกกรณีและสร้างกราฟฮิสโทแกรมและกล่องกราฟเพื่อเปรียบเทียบ
ก่อนอื่น เราจะสร้างคลาสใน Python เพื่อจำลองประชากรทั่วไปที่ประกอบด้วยกลุ่มชั้นสามกลุ่ม
class GeneralPopulation: def __init__(self, means: [float], stds: [float], sizes: [int], random_state: int = 15 ): """ Initializes our General Population and saves the given distributions :param means: List of expectations for normal distributions :param stds: List of standard deviations for normal distributions :param sizes: How many objects will be in each strata :param random_state: Parameter fixing randomness. Needed so that when conducting experiment repeatedly with the same input parameters, the results remained the same """ self.strats = [st.norm(mean, std) for mean, std in zip(means, stds)] self._sample(sizes) self.random_state = random_state def _sample(self, sizes): """Creates a general population sample as a mixture of strata :param sizes: List with sample sizes of the corresponding normal distributions """ self.strats_samples = [rv.rvs(size) for rv, size in zip(self.strats, sizes)] self.general_samples = np.hstack(self.strats_samples) self.N = self.general_samples.shape[0] # number of strata self.count_strats = len(sizes) # ratios for every strata in GP self.ws = [size/self.N for size in sizes] # ME and Std for GP self.m = np.mean(self.general_samples) self.sigma = np.std(self.general_samples) # ME and std for all strata self.ms = [np.mean(strat_sample) for strat_sample in self.strats_samples] self.sigmas = [np.std(strat_sample) for strat_sample in self.strats_samples]
จากนั้นเรามาเพิ่มฟังก์ชันสำหรับวิธีการสุ่มตัวอย่างทั้งสามวิธีที่อธิบายไว้ในส่วนเชิงทฤษฎีกัน
def random_subsampling(self, size): """Creates a random subset of the entire population :param sizes: subsample size """ rc = np.random.choice(self.general_samples, size=size) return rc def proportional_subsampling(self, size): """Creates a subsample with the number of elements, proportional shares of strata :param sizes: subsample size """ self.strats_size_proport = [int(np.floor(size*w)) for w in self.ws] rc = [] for k in range(len(self.strats_size_proport)): rc.append(np.random.choice(self.strats_samples[k], size=self.strats_size_proport[k])) return rc def optimal_subsampling(self, size): """Creates a subsample with the optimal number of elements relative to strata :param sizes: subsample size """ sum_denom = 0 for k in range(self.count_strats): sum_denom += self.ws[k] * self.sigmas[k] self.strats_size_optimal = [int(np.floor((size*w*sigma)/sum_denom)) for w, sigma in zip(self.ws, self.sigmas)] if 0 in self.strats_size_optimal: raise ValueError('Strats size is 0, please change variance of smallest strat!') rc = [] for k in range(len(self.strats_size_optimal)): rc.append(np.random.choice(self.strats_samples[k], size=self.strats_size_optimal[k])) return rc
นอกจากนี้ ในส่วนของเชิงประจักษ์ เราต้องมีฟังก์ชันสำหรับจำลองกระบวนการทดลองอยู่เสมอ
def run_experiments(self, n_sub, subsampling_method, n_experiments=1000): """Conducts a series of experiments and saves the results :param n_sub: size of sample :param subsampling_method: method for creating a subsample :param n_experiments: number of experiment starts """ means_s = [] if(len(self.general_samples)<100): n_sub = 20 if(subsampling_method == 'random_subsampling'): for n in range(n_experiments): rc = self.random_subsampling(n_sub) mean = rc.sum()/len(rc) means_s.append(mean) else: for n in range(n_experiments): if(subsampling_method == 'proportional_subsampling'): rc = self.proportional_subsampling(n_sub) elif(subsampling_method == 'optimal_subsampling'): rc = self.optimal_subsampling(n_sub) strats_mean = [] for k in range(len(rc)): strats_mean.append(sum(rc[k])/len(rc[k])) # Mean for a mixture means_s.append(sum([w_k*mean_k for w_k, mean_k in zip(self.ws, strats_mean)])) return means_s
ถ้าหากเราพิจารณาประชากรทั่วไป ซึ่งทุกระดับของเรามีค่าและความแปรปรวนเท่ากัน ผลลัพธ์จากทั้งสามวิธีคาดว่าจะเท่ากันมากหรือน้อย
ค่าเฉลี่ยที่แตกต่างกันและความแปรปรวนที่เท่ากันทำให้ได้ผลลัพธ์ที่น่าตื่นเต้นมากขึ้น การใช้การแบ่งชั้นจะช่วยลดความแปรปรวนได้อย่างมาก
ในกรณีที่มีค่าเฉลี่ยเท่ากันแต่มีความแปรปรวนต่างกัน เราจะเห็นการลดความแปรปรวนในวิธีของ Neyman
ตอนนี้คุณสามารถใช้การแบ่งชั้นเพื่อลดความแปรปรวนของตัวชี้วัดและเพิ่มประสิทธิภาพการทดลองได้หากคุณจัดกลุ่มผู้ชมและแบ่งพวกเขาแบบสุ่มภายในแต่ละกลุ่มด้วยน้ำหนักที่เฉพาะเจาะจง