Berechnung der Kannibalisierung basierend auf dem klassischen A / B-Test und der Bootstrap-Methode

Dieser Artikel beschreibt eine Methode zur Berechnung der Kannibalisierung für eine mobile Anwendung basierend auf dem klassischen A / B-Test. In diesem Fall werden Zielaktionen als Teil des Prozesses der Neuzuweisung von einer Werbequelle (Direct, Criteo, AdWords UAC und andere) im Vergleich zu Zielaktionen in der Gruppe, für die die Werbung deaktiviert wurde, berücksichtigt und bewertet.

Der Artikel gibt einen Überblick über klassische Methoden zum Vergleich unabhängiger Stichproben mit einer kurzen theoretischen Grundlage und eine Beschreibung der verwendeten Bibliotheken, einschließlich Beschreibt kurz das Wesentliche der Bootstrap-Methode und ihre Implementierung in der FaceBook Bootstrapped-Bibliothek sowie die Probleme, die in der Praxis bei der Anwendung dieser Techniken auftreten, und wie sie gelöst werden können.

Der Nachweis wird entweder verschleiert oder nicht erbracht, um eine Geheimhaltungsvereinbarung aufrechtzuerhalten.

In Zukunft plane ich, diesen Artikel zu ergänzen und geringfügig zu ändern, sobald neue Fakten erscheinen, sodass diese Version als erste Version betrachtet werden kann. Ich wäre dankbar für die Kommentare und Bewertungen.

Einführung


Kannibalisierung ist der Prozess des vollständigen und gezielten Verkehrsflusses von einem Kanal zum anderen.

Vermarkter verwenden diesen Indikator normalerweise als zusätzlichen K-Koeffizienten bei der Berechnung des CPA: Der berechnete CPA wird mit 1 + K multipliziert. In diesem Fall bedeutet CPA die Gesamtkosten für die Gewinnung von Traffic / die Anzahl der gezielten Aktionen, die direkt monetarisiert werden, dh die den tatsächlichen Gewinn gebracht haben - beispielsweise einen gezielten Anruf und / oder indirekt monetarisiert -, z. B. das Volumen der Anzeigendatenbank erhöhen, die Zielgruppe erhöhen und so weiter.

Wenn kostenlose Kanäle (z. B. Besuche von organischen SERPs, Klicks auf Links auf Websites, die für uns kostenlos sind) gegen Bezahlung ausschlachten (Direkt, AdWords anstelle von organischen Inhalten, Werbung in Feeds von sozialen Netzwerken anstelle von Klicks auf Anzeigen), ist dies kostenlos In Gruppen eingeteilt usw.) birgt dies das Risiko eines finanziellen Verlusts. Daher ist es wichtig, die Kannibalisierungsrate zu kennen.

In unserem Fall bestand die Aufgabe darin, die Kannibalisierung von "organischen" Übergängen zur Anwendung durch Übergänge aus dem Criteo-Werbenetzwerk zu berechnen. Die Überwachung ist ein Gerät oder eine Benutzer-UID (GAID / ADVID und IDFA).

Versuchsvorbereitung


Sie können die Zielgruppe auf das Experiment vorbereiten, indem Sie die Benutzer in der Benutzeroberfläche des AdJust-Analysesystems in Gruppen unterteilen, um diejenigen zu isolieren, die Anzeigen aus einem bestimmten Werbenetzwerk sehen (Kontrollbeispiel), und diejenigen, denen keine Anzeigen mit GAID oder ADVID bzw. IDFA angezeigt werden (AdJust stellt die Audience Builder-API bereit.) Anschließend können Sie im Kontrollbeispiel eine Werbekampagne in das im Experiment untersuchte Werbenetzwerk aufnehmen.

Ich stelle von mir selbst fest, dass, wie es intuitiv erscheint, die folgende Durchführung des Experiments in diesem Fall kompetenter wäre: vier Gruppen auszuwählen - diejenigen, bei denen das Retargeting aus allen Kanälen deaktiviert war (1), als Versuchsgruppe und diejenigen, die dies getan haben nur Retargeting mit Criteo (2) aktiviert; diejenigen, bei denen das Retargeting nur mit Criteo (3) deaktiviert war, diejenigen, bei denen das Retargeting (4) aktiviert war. Dann wäre es möglich, (1) / (2) zu berechnen, nachdem der tatsächliche Wert der Kannibalisierung von Werbekampagnen des Criteo-Netzwerks für „organische“ Übergänge zur Anwendung erhalten wurde, und (3) / (4), wenn Kannitalisierung von Criteo in der „natürlichen“ Umgebung erhalten wurde (schließlich natürlich Criteo) kann auch andere kostenpflichtige Kanäle ausschlachten). Das gleiche Experiment sollte für andere Werbenetzwerke wiederholt werden, um die Auswirkungen der einzelnen Netzwerke herauszufinden. In einer idealen Welt wäre es schön, die Kannibalisierung zwischen allen wichtigen bezahlten Quellen zu untersuchen, die den größten Anteil am Gesamtverkehr ausmachen, aber es würde so viel Zeit in Anspruch nehmen (sowohl für die Vorbereitung von Experimenten aus Sicht der Entwicklung als auch für die Bewertung der Ergebnisse), was dazu führen würde Kritik an unvernünftiger Sorgfalt.

Tatsächlich wurde unser Experiment unter den Bedingungen (3) und (4) durchgeführt, die Proben wurden im Verhältnis von 10% zu 90% aufgeteilt, das Experiment wurde 2 Wochen lang durchgeführt.

Datenaufbereitung und -verifizierung


Ein wichtiger Schritt vor Beginn einer Studie ist die kompetente Vorschulung und Datenbereinigung.

Es sollte beachtet werden, dass tatsächlich die aktiven Vorrichtungen für den Versuchszeitraum zweimal geringer waren (42,5% bzw. 50% der Kontroll- und Versuchsgruppen) als die Vorrichtungen in den vollständigen Anfangsproben, was durch die Art der Daten erklärt wird:

  1. Erstens (und dies ist der Hauptgrund) enthält die Auswahl für das Retargeting unter Anpassen die Kennungen aller Geräte, die die Anwendung jemals installiert haben, dh der Geräte, die nicht mehr verwendet werden, und der Geräte, mit denen die Anwendung bereits verwendet wurde gelöscht
  2. Zweitens ist es nicht erforderlich, dass sich alle Geräte während des Experiments bei der Anwendung angemeldet haben.

Wir haben jedoch die Kannibalisierung anhand von Daten aus einer vollständigen Stichprobe berechnet. Für mich persönlich scheint die Richtigkeit einer solchen Berechnung immer noch ein strittiger Punkt zu sein - meiner Meinung nach ist es im Allgemeinen korrekter, alle diejenigen zu bereinigen, die die Anwendung deinstalliert und nicht über die entsprechenden Tags installiert haben, sowie diejenigen, die sich seit mehr als einem Jahr nicht mehr bei der Anwendung angemeldet haben. In diesem Zeitraum kann der Benutzer das Gerät wechseln. Minus - Auf diese Weise können für das Experiment diejenigen Benutzer aus der Auswahl entfernt werden, die nicht zur Anwendung gewechselt sind, dies aber konnten, wenn wir ihnen Anzeigen im Criteo-Netzwerk zeigen. Ich möchte darauf hinweisen, dass in einer guten Welt all diese erzwungenen Vernachlässigungen und Annahmen separat untersucht und verifiziert werden sollten, aber wir leben in einer Welt, in der es schnell und pelzig geht.

In unserem Fall ist es wichtig, die folgenden Punkte zu überprüfen:

  1. Wir überprüfen den Schnittpunkt in unseren ersten Proben - experimentell und kontrolliert. In einem korrekt durchgeführten Experiment sollten solche Schnittpunkte nicht vorhanden sein. In unserem Fall befanden sich jedoch mehrere Duplikate aus der experimentellen Probe in der Kontrolle. In unserem Fall war der Anteil dieser Duplikate am Gesamtvolumen der an dem Experiment beteiligten Geräte gering, daher haben wir diese Bedingung vernachlässigt. Wenn es> 1% Duplikate gab, sollte das Experiment als falsch angesehen werden und ein zweites Experiment sollte durchgeführt werden, nachdem zuvor die Duplikate gereinigt wurden.
  2. Wir stellen sicher, dass die Daten im Experiment wirklich betroffen waren - das Retargeting sollte in der experimentellen Probe (zumindest mit Criteo im korrekt eingestellten Experiment - von allen Kanälen aus) deaktiviert werden. Daher muss beim Retargeting mit Criteo überprüft werden, ob DeviceID aus dem Experiment fehlt. In unserem Fall fiel DeviceID aus der Versuchsgruppe dennoch in das Retargeting, aber es gab weniger als 1%, was vernachlässigbar ist.

Direkte Auswertung des Experiments


Wir werden die Änderung in den folgenden Zielmetriken berücksichtigen: absolut - die Anzahl der Anrufe und relativ - die Anzahl der Anrufe pro Benutzer in der Kontrollgruppe (Sägenanzeigen im Criteo-Netzwerk) und in der experimentellen Gruppe (Anzeigen wurden deaktiviert). Im folgenden Code beziehen sich die variablen Daten auf die pandas.DataFrame-Struktur, die aus den Ergebnissen einer Versuchs- oder Kontrollprobe gebildet wird.

Es gibt parametrische und nichtparametrische Methoden zur Bewertung der statistischen Signifikanz der Wertdifferenz in nicht verwandten Proben. Parametrische Bewertungskriterien bieten eine größere Genauigkeit, haben jedoch Einschränkungen in ihrer Anwendung - insbesondere ist eine der Hauptbedingungen, dass die gemessenen Werte für die Beobachtungen in der Probe normal verteilt werden sollten.

1. Die Untersuchung der Verteilung der Werte in den Proben auf Normalität


Der erste Schritt besteht darin, die vorhandenen Stichproben anhand von Standardtests auf die Art der Werteverteilung und die Gleichheit der Varianzen der Stichproben zu untersuchen - die Kriterien von Kolmogorov-Smirnov und Shapiro-Wilks sowie den in der Bibliothek sklearn.stats implementierten Bartlett-Test mit einem p-Wert von = 0,05:

#    : def norm_test(df, pvalue = 0.05, test_name = 'kstest'): if test_name == 'kstest': st = stats.kstest(df, 'norm') if test_name == 'shapiro': st = stats.shapiro(df) sys.stdout.write('According to {} {} is {}normal\n'.format(test_name, df.name, {True:'NOT ', False:''}[st[1] < pvalue])) #    : def barlett_test(df1, df2, pvalue = 0.05): st = stats.bartlett(df1, df2) sys.stdout.write('Variances of {} and {} is {}equals\n'.format(df1.name, df2.name, {True:'NOT ', False:''}[st[1] < pvalue])) 

Zusätzlich können Sie zur visuellen Beurteilung der Ergebnisse die Histogrammfunktion verwenden.

 data_agg = data.groupby(['bucket']).aggregate({'device_id': 'nunique', 'calls': 'sum'}).fillna(0) data_conv = data_agg['calls_auto']/data_agg['device_id'] data_conv.hist(bins=20) 

Bild

Sie können das Histogramm folgendermaßen lesen: 10 Mal in der Stichprobe gab es eine Umrechnung von 0,08, 1 - 0,14. Dies sagt nichts über die Anzahl der Geräte als Beobachtungen für einen der Umrechnungsindikatoren aus.

In unserem Fall ist die Verteilung des Parameterwerts sowohl in absoluten Werten als auch in relativen Werten (Anzahl der Aufrufe an das Gerät) in den Stichproben nicht normal.
In diesem Fall können Sie entweder den nichtparametrischen Wilcoxon-Test verwenden, der in der Standardbibliothek sklearn.stats implementiert ist, oder versuchen, die Verteilung der Werte in den Stichproben auf die normale Form zu bringen und eines der parametrischen Kriterien anzuwenden - Student's aka t-test oder Shapiro-Wilks-Test.

2. Methoden zur Reduzierung der Werteverteilung in Proben auf die Normalform


2.1. Untereimer

Ein Ansatz, um die Verteilung zu normalisieren, ist die Sub-Bucket-Methode. Sein Wesen ist einfach, und die folgende mathematische These ist die theoretische Grundlage: Nach dem klassischen zentralen Grenzwertsatz tendiert die Verteilung der Mittel zur Normalität - die Summe von n unabhängigen identisch verteilten Zufallsvariablen hat eine Verteilung nahe der Normalen und äquivalent die Verteilung der Stichprobenmittel des ersten n unabhängigen identisch verteilten Zufalls Mengen tendieren zu normal. Daher können wir die vorhandenen Bucket'es in Sub-Bucket'y aufteilen und dementsprechend, wenn wir die Durchschnittswerte von Sub-Bucket'y für jeden der Bucket'ov nehmen, eine Verteilung erhalten, die nahezu normal ist:

 #   subbucket' data['subbucket'] = data['device_id'].apply(lambda x: randint(0,1000)) # Variant 1 data['subbucket'] = data['device_id'].apply(lambda x: hash(x)%1000) # Variant 2 

Es gibt viele Optionen für die Aufteilung, alles hängt von der Vorstellungskraft und den moralischen Prinzipien des Entwicklers ab. Sie können ehrliche Zufallszahlen verwenden oder Hash aus dem ursprünglichen Bucket verwenden und dabei den Mechanismus für die Ausgabe im Schema berücksichtigen.

In der Praxis haben wir jedoch nach mehreren Dutzend Codestarts die Normalverteilung nur einmal erhalten, dh diese Methode ist weder garantiert noch stabil.

Darüber hinaus stimmt das Verhältnis von Zielaktionen und Benutzern zur Gesamtzahl der Aktionen und Benutzer im Unterbereich möglicherweise nicht mit den anfänglichen Backets überein. Sie müssen daher zunächst überprüfen, ob das Verhältnis beibehalten wird.

 data[data['calls'] > 0].device_id.nunique()/data.device_id.nunique() # Total buckets = data.groupby(['bucket']).aggregate({'device_id': 'nunique', 'calls': 'sum'}) buckets[buckets['calls'] > 0].device_id.nunique()/buckets.device_id.nunique() # Buckets subbuckets = data.groupby(['subbucket']).aggregate({'device_id': 'nunique', 'calls': 'sum'}) subbuckets[subbuckets['calls'] > 0].device_id.nunique()/subbuckets.device_id.nunique() # Subbuckets 

Bei dieser Überprüfung haben wir festgestellt, dass die Umrechnungsverhältnisse für Subbuckets relativ zur ursprünglichen Auswahl nicht erhalten bleiben. Da wir zusätzlich die Konsistenz des Verhältnisses des Anteils der Aufrufe in den Ausgabe- und Quellstichproben gewährleisten müssen, verwenden wir den Klassenausgleich und fügen die Gewichtung hinzu, sodass die Daten getrennt nach Untergruppen ausgewählt werden: getrennt von Beobachtungen mit Zielaktionen und getrennt von Beobachtungen ohne Zielaktionen im richtigen Verhältnis. Außerdem waren in unserem Fall die Proben ungleich verteilt; intuitiv scheint sich der Durchschnitt nicht zu ändern, aber wie sich die Ungleichmäßigkeit der Proben auf die Varianz auswirkt, ist aus der Dispersionsformel nicht ersichtlich. Um zu klären, ob der Unterschied in der Größe der Stichproben das Ergebnis beeinflusst, wird das Xi-Quadrat-Kriterium verwendet. Wenn ein statistisch signifikanter Unterschied festgestellt wird, wird ein größerer Datenrahmen mit einer kleineren Größe abgetastet:

 def class_arrays_balancer(df1, df2, target = 'calls', pvalue=0.05): df1_target_size = len(df1[df1[target] > 0]) print(df1.columns.to_list()) df2_target_size = len(df2[df2[target] > 0]) total_target_size = df1_target_size + df2_target_size chi2_target, pvalue_target, dof_target, expected_target = chi2_contingency([[df1_target_size, total_target_size], [df2_target_size, total_target_size]]) df1_other_size = len(df1[df1[target] == 0]) df2_other_size = len(df1[df1[target] == 0]) total_other_size = df1_other_size + df2_other_size chi2_other, pvalue_other, dof_other, expected_other = chi2_contingency([[df1_other_size, total_other_size], [df2_other_size, total_other_size]]) df1_target, df2_target, df1_other, df2_other = None, None, None, None if pvalue_target < pvalue: sample_size = min([df1_target_size, df2_target_size]) df1_rnd_indx = np.random.choice(df1_target_size, size=sample_size, replace=False) df2_rnd_indx = np.random.choice(df2_target_size, size=sample_size, replace=False) df1_target = pd.DataFrame((np.asarray(df1[df1[target] == 1])[df1_rnd_indx]).tolist(), columns = df1.columns.tolist()) df2_target = pd.DataFrame((np.asarray(df2[df2[target] == 1])[df2_rnd_indx]).tolist(), columns = df2.columns.tolist()) if p_value_other < pvalue: sample_size = min([df1_other_size, df2_other_size]) df1_rnd_indx = np.random.choice(df1_other_size, size=sample_size, replace=False) df2_rnd_indx = np.random.choice(df2_other_size, size=sample_size, replace=False) df1_other = pd.DataFrame((np.asarray(df1[df1[target] == 0])[df1_rnd_indx]).tolist(), columns = df1.columns.tolist()) df2_other = pd.DataFrame((np.asarray(df2[df2[target] == 0])[df2_rnd_indx]).tolist(), columns = df2.columns.tolist()) df1 = pd.concat([df1_target, df1_other]) df2 = pd.concat([df2_target, df2_other]) return df1, df2 exp_classes, control_classes = class_arrays_balancer(data_exp, data_control) 

Am Ausgang erhalten wir Daten, deren Größe ausgewogen ist und die mit den anfänglichen Umrechnungsverhältnissen übereinstimmen, den untersuchten Metriken (berechnet für Durchschnittswerte für den Unterbereich), in denen sie bereits normal verteilt sind, was sowohl visuell als auch anhand der Ergebnisse der Anwendung der uns bereits bekannten Testkriterien ersichtlich ist Normalität (mit p-Wert> = 0,05). Zum Beispiel für relative Indikatoren:

 data_conv = (data[data['calls'] > 0].groupby(['subbucket']).calls.sum()*1.0/data.groupby(['subbucket']).device_id.nunique()) data_conv.hist(bins = 50) 

Jetzt kann der T-Test auf den Durchschnitt über Sub-Bucket'es angewendet werden (daher ist es nicht device_id, kein Gerät, sondern Sub-Bucket, das als Beobachtung dient).

Nachdem wir sichergestellt haben, dass die Änderungen statistisch signifikant sind, können wir mit gutem Gewissen das tun, wofür wir alle begonnen haben - die Kannibalisierung berechnen:

 (data_exp.groupby(['subbucket']).calls.avg() - data_cntrl.groupby(['subbucket']).calls.avg() )/ data_exp.groupby(['subbucket']).calls.avg() 

Der Nenner sollte Verkehr ohne Werbung sein, dh experimentell.

3. Bootstrap-Methode


Die Bootstrap-Methode ist eine Erweiterung der Sub-Bucket-Methode und stellt die erweiterte und verbesserte Version dar. Eine Software-Implementierung dieser Methode in Python finden Sie in der Facebook Bootstrapped-Bibliothek.
Kurz gesagt, die Idee des Bootstraps kann wie folgt beschrieben werden: Eine Methode ist nichts anderes als ein Konstruktor von Samples, die auf ähnliche Weise wie Sub-Bucket-Methoden zufällig generiert wurden, jedoch mit möglichen Wiederholungen. Wir können die Platzierung aus der Allgemeinbevölkerung (wenn man die Originalstichprobe nennen kann) mit der Rückgabe sagen. Am Ausgang werden Mittelwerte (oder Mediane, Beträge usw.) aus den Durchschnittswerten für jede der erzeugten Unterproben gebildet.

Die wichtigsten Methoden der FaceBook Bootstrap-Bibliothek :
 bootstrap() 
- implementiert einen Mechanismus zur Bildung von Teilproben; Gibt standardmäßig die Untergrenze (5 Perzentil) und die Obergrenze (95 Perzentil) zurück. Um eine diskrete Verteilung in diesem Bereich zurückzugeben, muss der Parameter return_distribution = True gesetzt werden (er wird von der Hilfsfunktion generate_distributions () generiert ).

Sie können die Anzahl der Iterationen mithilfe des Parameters num_iterations angeben , in dem Unterproben generiert werden, und die Anzahl der Unterproben iteration_batch_size für jede Iteration. Bei der Ausgabe von generate_distributions () wird eine Stichprobe mit einer Größe generiert, die der Anzahl der Iterationen num_iterations entspricht , deren Elemente der Durchschnitt der Werte der bei jeder Iteration berechneten Stichproben von iteration_batch_size sind. Bei großen Stichprobenmengen passen die Daten möglicherweise nicht mehr in den Speicher. In solchen Fällen ist es daher ratsam, den Wert von iteration_batch_size zu verringern.

Beispiel : Die ursprüngliche Stichprobe sei 2.000.000; num_iterations = 10.000, iteration_batch_size = 300. Bei jeder der 10.000 Iterationen werden 300 Listen mit 2.000.000 Elementen im Speicher gespeichert.

Die Funktion ermöglicht auch die parallele Berechnung auf mehreren Prozessorkernen und auf mehreren Threads, wobei die erforderliche Anzahl mithilfe des Parameters num_threads festgelegt wird.

 bootstrap_ab() 

führt dieselben Aktionen aus wie die oben beschriebene Funktion bootstrap (). Zusätzlich wird die Aggregation von Durchschnittswerten jedoch auch mit der in stat_func angegebenen Methode durchgeführt - aus den Werten von num_iterations . Als nächstes wird die im Parameter compare_func angegebene Metrik berechnet und die statistische Signifikanz geschätzt.

 compare_functions 

- eine Klasse von Funktionen, die Werkzeuge zur Bildung von Metriken für die Bewertung bereitstellt:
 compare_functions.difference() compare_functions.percent_change() compare_functions.ratio() compare_functions.percent_difference() # difference = (test_stat - ctrl_stat) # percent_change = (test_stat - ctrl_stat) * 100.0 / ctrl_stat # ratio = test_stat / ctrl_stat # percent_difference = (test_stat - ctrl_stat) / ((test_stat + ctrl_stat) / 2.0) * 100.0 

 stats_functions 
- eine Klasse von Funktionen, aus denen die Aggregationsmethode der untersuchten Metrik ausgewählt wird:
 stats_functions.mean stats_functions.sum stats_functions.median stats_functions.std 

Als stat_func können Sie auch eine benutzerdefinierte benutzerdefinierte Funktion verwenden, zum Beispiel:

 def test_func(test_stat, ctrl_stat): return (test_stat - ctrl_stat)/test_stat bs.bootstrap_ab(test.values, control.values, stats_functions.mean, test_func, num_iterations=5000, alpha=0.05, iteration_batch_size=100, scale_test_by=1, num_threads=4) 

Tatsächlich ist (test_stat - ctrl_stat) / test_stat die Formel zur Berechnung unserer Kannibalisierung.

Alternativ oder zum Zweck eines praktischen Experiments können Sie zunächst Verteilungen mit bootstrap () abrufen, die statistische Signifikanz von Unterschieden in Zielmetriken mithilfe von t-test überprüfen und dann die erforderlichen Manipulationen auf diese anwenden.
Ein Beispiel dafür, wie mit dieser Methode eine Normalverteilung „Qualität“ erzielt werden kann:



Eine ausführlichere Dokumentation finden Sie auf der Repository-Seite .

Im Moment ist dies alles, worüber ich sprechen wollte (oder konnte). Ich habe versucht, die verwendeten Methoden und den Prozess ihrer Implementierung kurz, aber klar zu beschreiben. Es ist möglich, dass die Methoden angepasst werden müssen, daher bin ich für Feedback und Bewertungen dankbar.

Ich möchte auch meinen Kollegen für ihre Hilfe bei der Vorbereitung dieser Arbeit danken. Wenn der Artikel überwiegend positives Feedback erhält, werde ich hier deren Namen oder Spitznamen angeben (nach vorheriger Absprache).

Beste Wünsche an alle! :) :)

PS Sehr geehrter Championship Channel , die Aufgabe, die Ergebnisse von A / B-Tests auszuwerten, ist eine der wichtigsten in Data Science, da kein einziger Start eines neuen ML-Modells in der Produktion ohne A / B abgeschlossen ist. Vielleicht ist es an der Zeit, einen Wettbewerb zu organisieren, um ein System zur Bewertung der Ergebnisse von A / B-Tests zu entwickeln? :) :)

Source: https://habr.com/ru/post/de451488/


All Articles