So testen Sie Hypothesen und verdienen mit Split-Tests Geld mit Swift


Hallo allerseits! Mein Name ist Sasha Zimin, ich arbeite als iOS-Entwickler im Londoner Büro von Badoo . Badoo hat eine sehr enge Beziehung zu Produktmanagern, und ich habe mir angewöhnt, alle Hypothesen zu testen, die ich in Bezug auf das Produkt habe. Also fing ich an, Split-Tests für meine Projekte zu schreiben.

Das Framework, das in diesem Artikel behandelt wird, wurde für zwei Zwecke geschrieben. Erstens ist es zur Vermeidung möglicher Fehler besser, keine Daten im Analysesystem zu haben als falsche Daten (oder sogar Daten, die falsch interpretiert und in Brennholz zerlegt werden können). Zweitens, um die Implementierung jedes nachfolgenden Tests zu vereinfachen. Aber fangen wir mit den Split-Tests an.

Heutzutage gibt es Millionen von Anwendungen, die die meisten Anforderungen der Benutzer erfüllen. Daher wird es von Tag zu Tag schwieriger, neue wettbewerbsfähige Produkte zu entwickeln. Dies hat viele Unternehmen und Startups dazu veranlasst, zunächst verschiedene Forschungen und Experimente durchzuführen, um herauszufinden, welche Eigenschaften ihr Produkt verbessern und auf welche verzichtet werden kann.

Eines der Hauptwerkzeuge für die Durchführung solcher Experimente ist das Split-Testen (oder A / B-Testen). In diesem Artikel werde ich erklären, wie es auf Swift implementiert werden kann.

Alle Projektdemos finden Sie hier . Wenn Sie bereits eine Idee zu A / B-Tests haben, können Sie direkt zum Code wechseln .

Eine kurze Einführung in Split Testing


Split-Tests oder A / B-Tests (dieser Begriff ist nicht immer korrekt, da Sie mehr als zwei Teilnehmergruppen haben können) bieten eine Möglichkeit, verschiedene Versionen eines Produkts in verschiedenen Benutzergruppen zu überprüfen, um zu verstehen, welche Version besser ist. Sie können darüber in Wikipedia oder zum Beispiel in diesem Artikel mit realen Beispielen lesen.

Bei Badoo führen wir viele Split-Tests gleichzeitig durch. Zum Beispiel, als wir entschieden haben, dass die Benutzerprofilseite in unserer Anwendung veraltet aussieht, und wollten auch die Benutzerinteraktion mit einigen Bannern verbessern. Aus diesem Grund haben wir Split-Tests mit drei Gruppen gestartet:

  1. Altes Profil
  2. Neues Profil Version 1
  3. Neues Profil Version 2

Wie Sie sehen können, hatten wir drei Optionen, eher wie A / B / C-Tests (und deshalb bevorzugen wir den Begriff „Split-Test“).

So sahen verschiedene Benutzer ihre Profile:



In der Produktmanager-Konsole wurden vier Benutzergruppen zufällig gebildet und hatten dieselbe Nummer:



Vielleicht fragen Sie sich, warum wir control und control_check haben (wenn control_check eine Kopie der Logik der Kontrollgruppe ist)? Die Antwort ist sehr einfach: Jede Änderung wirkt sich auf viele Indikatoren aus, sodass wir niemals absolut sicher sein können, dass eine bestimmte Änderung das Ergebnis eines Split-Tests ist und keine anderen Aktionen.

Wenn Sie der Meinung sind, dass sich einige Indikatoren aufgrund des Split-Tests geändert haben, sollten Sie überprüfen, ob sie innerhalb der Gruppen control und control_check identisch sind.

Wie Sie sehen können, können die Meinungen der Benutzer unterschiedlich sein, aber die empirischen Beweise sind eindeutige Beweise. Das Team von Produktmanagern analysiert die Ergebnisse und versteht, warum eine Option besser ist als eine andere.

Split Testing und Swift


Ziele:

  1. Erstellen Sie eine Bibliothek für die Clientseite (ohne Verwendung eines Servers).
  2. Speichern Sie die ausgewählte Benutzeroption im permanenten Speicher, nachdem sie versehentlich generiert wurde.
  3. Senden Sie Berichte zu den ausgewählten Optionen für jeden Split-Test an den Analysedienst.
  4. Machen Sie das Beste aus den Fähigkeiten von Swift.

PS Die Verwendung einer solchen Bibliothek zum Split-Testen des Client-Teils hat Vor- und Nachteile. Der Hauptvorteil besteht darin, dass Sie keine Serverinfrastruktur oder einen dedizierten Server benötigen. Und der Nachteil ist, dass Sie, wenn während des Experiments etwas schief geht, kein Rollback durchführen können, ohne die neue Version im App Store herunterzuladen.

Ein paar Worte zur Implementierung:

  1. Während des Experiments wird die Option für den Benutzer nach dem ebenso wahrscheinlichen Prinzip zufällig ausgewählt.
  2. Der Split-Testdienst kann Folgendes verwenden:

  • Jeder Datenspeicher (z. B. UserDefaults, Realm, SQLite oder Core Data) als Abhängigkeit und speichert den dem Benutzer zugewiesenen Wert (den Wert seiner Variante) darin.
  • Jeder Analysedienst (z. B. Amplitude oder Facebook Analytics) als Abhängigkeit und sendet die aktuelle Version zu dem Zeitpunkt, zu dem der Benutzer auf einen Split-Test stößt.

Hier ist ein Diagramm zukünftiger Klassen:



Alle Split-Tests werden mit SplitTestProtocol dargestellt , und jeder von ihnen verfügt über mehrere Optionen (Gruppen), die in SplitTestGroupProtocol dargestellt werden .

Der Split-Test sollte den Analysten über die aktuelle Version informieren können, daher wird AnalyticsProtocol als Abhängigkeit verwendet.

Service SplitTestingService speichert, generiert Optionen und verwaltet alle Split-Tests. Er lädt die aktuelle Version des Benutzers aus dem von StorageProtocol festgelegten Speicher herunter und übergibt das AnalyticsProtocol an SplitTestProtocol .


Beginnen wir mit dem Schreiben von Code mit den Abhängigkeiten AnalyticsProtocol und StorageProtocol :

protocol AnalyticsServiceProtocol {    func setOnce(value: String, for key: String) } protocol StorageServiceProtocol {    func save(string: String?, for key: String)    func getString(for key: String) -> String? } 

Die Rolle der Analyse besteht darin, ein Ereignis einmal aufzuzeichnen. Um beispielsweise zu beheben, dass sich Benutzer A während des Split-Tests von button_color in der blauen Gruppe befindet , wenn er einen Bildschirm mit dieser Schaltfläche sieht.

Die Rolle des Repositorys besteht darin, eine bestimmte Option für den aktuellen Benutzer zu speichern (nachdem der SplitTestingService diese Option generiert hat) und sie dann jedes Mal zurückzulesen, wenn das Programm auf diesen Split-Test zugreift.

Schauen wir uns also SplitTestGroupProtocol an , das eine Reihe von Optionen für einen bestimmten Split-Test charakterisiert:

 protocol SplitTestGroupProtocol: RawRepresentable where RawValue == String {   static var testGroups: [Self] { get } } 

Da RawRepresentable, bei dem RawValue eine Zeichenfolge ist, können Sie einfach eine Variante aus einer Zeichenfolge erstellen oder sie wieder in eine Zeichenfolge konvertieren, was für die Arbeit mit Analysen und Speicher sehr praktisch ist. SplitTestGroupProtocol enthält auch ein Array von Testgruppen, die die Zusammensetzung der aktuellen Optionen angeben können (dieses Array wird auch für die zufällige Generierung aus den verfügbaren Optionen verwendet).

Dies ist der Basisprototyp für den SplitTestProtocol- Split-Test selbst :

 protocol SplitTestProtocol {   associatedtype GroupType: SplitTestGroupProtocol   static var identifier: String { get }   var currentGroup: GroupType { get }   var analytics: AnalyticsServiceProtocol { get }   init(currentGroup: GroupType, analytics: AnalyticsServiceProtocol) } extension SplitTestProtocol {   func hitSplitTest() {       self.analytics.setOnce(value: self.currentGroup.rawValue, for: Self.analyticsKey)   }   static var analyticsKey: String {       return "split_test-\(self.identifier)"   }   static var dataBaseKey: String {       return "split_test_database-\(self.identifier)"   } } 

SplitTestProtocol enthält:

  1. Ein GroupType- Typ, der das SplitTestGroupProtocol- Protokoll implementiert, um einen Typ darzustellen, der eine Reihe von Optionen definiert.
  2. Zeichenfolgenwertkennung für Analyse- und Speicherschlüssel.
  3. Die Variable currentGroup zum Aufzeichnen einer bestimmten Instanz von SplitTestProtocol .
  4. Analytics- Abhängigkeit für die hitSplitTest- Methode.
  5. Und die hitSplitTest- Methode, die den Analysten darüber informiert, dass der Benutzer das Ergebnis des Split-Tests gesehen hat.

Mit der hitSplitTest-Methode können Sie sicherstellen, dass Benutzer nicht nur eine bestimmte Version haben, sondern auch das Testergebnis sehen. Wenn Sie einen Benutzer, der den Einkaufsbereich nicht besucht hat, als "saw_red_button_on_purcahse_screen" markieren, werden die Ergebnisse verzerrt.

Jetzt sind wir bereit für SplitTestingService :

 protocol SplitTestingServiceProtocol {   func fetchSplitTest<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value } class SplitTestingService: SplitTestingServiceProtocol {   private let analyticsService: AnalyticsServiceProtocol   private let storage: StorageServiceProtocol   init(analyticsService: AnalyticsServiceProtocol, storage: StorageServiceProtocol) {       self.analyticsService = analyticsService       self.storage = storage   }   func fetchSplitTest<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value {       if let value = self.getGroup(splitTestType) {           return Value(currentGroup: value, analytics: self.analyticsService)       }       let randomGroup = self.randomGroup(Value.self)       self.saveGroup(splitTestType, group: randomGroup)       return Value(currentGroup: randomGroup, analytics: self.analyticsService)   }   private func saveGroup<Value: SplitTestProtocol>(_ splitTestType: Value.Type, group: Value.GroupType) {       self.storage.save(string: group.rawValue, for: Value.dataBaseKey)   }   private func getGroup<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value.GroupType? {       guard let stringValue = self.storage.getString(for: Value.dataBaseKey) else {           return nil       }       return Value.GroupType(rawValue: stringValue)   }   private func randomGroup<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value.GroupType {       let count = Value.GroupType.testGroups.count       let random = Int.random(lower: 0, count - 1)       return Value.GroupType.testGroups[random]   } } 

PS In dieser Klasse verwenden wir die Int.random-Funktion aus
hier , aber in Swift 4.2 ist es bereits standardmäßig eingebaut.

Diese Klasse enthält eine öffentliche fetchSplitTest- Methode und drei private Methoden: saveGroup , getGroup , randomGroup .

Die randomGroup-Methode generiert eine zufällige Variante für den ausgewählten Split-Test, während Sie mit getGroup und saveGroup die Variante für einen bestimmten Split-Test für den aktuellen Benutzer speichern oder laden können.

Die Haupt- und öffentliche Funktion dieser Klasse ist fetchSplitTest: Sie versucht, die aktuelle Version aus dem persistenten Speicher zurückzugeben. Wenn dies nicht erfolgreich ist, wird vor dem Zurückgeben eine zufällige Version generiert und gespeichert.



Jetzt können wir unseren ersten Split-Test erstellen:

 final class ButtonColorSplitTest: SplitTestProtocol {   static var identifier: String = "button_color"   var currentGroup: ButtonColorSplitTest.Group   var analytics: AnalyticsServiceProtocol   init(currentGroup: ButtonColorSplitTest.Group, analytics: AnalyticsServiceProtocol) {       self.currentGroup = currentGroup       self.analytics = analytics   }   typealias GroupType = Group   enum Group: String, SplitTestGroupProtocol {       case red = "red"       case blue = "blue"       case darkGray = "dark_gray"       static var testGroups: [ButtonColorSplitTest.Group] = [.red, .blue, .darkGray]   } } extension ButtonColorSplitTest.Group {   var color: UIColor {       switch self {       case .blue:           return .blue       case .red:           return .red       case .darkGray:           return .darkGray       }   } } 

Es sieht beeindruckend aus, aber keine Sorge: Sobald Sie SplitTestProtocol als separate Klasse implementieren, werden Sie vom Compiler aufgefordert, alle erforderlichen Eigenschaften zu implementieren.

Der wichtige Teil hier ist der Aufzählungstyp . Sie sollten alle Ihre Gruppen darin einfügen (in unserem Beispiel Rot, Blau und Dunkelgrau) und hier Zeichenfolgenwerte definieren, um die korrekte Übertragung an die Analyse sicherzustellen.

Wir haben auch eine Erweiterung ButtonColorSplitTest.Group , mit der Sie das volle Potenzial von Swift nutzen können. Erstellen wir nun die Objekte für AnalyticsProtocol und StorageProtocol :

 extension UserDefaults: StorageServiceProtocol {   func save(string: String?, for key: String) {       self.set(string, forKey: key)   }   func getString(for key: String) -> String? {       return self.object(forKey: key) as? String   } } 

Für StorageProtocol verwenden wir die UserDefaults-Klasse, da sie einfach zu implementieren ist. In Ihren Projekten können Sie jedoch mit jedem anderen persistenten Speicher arbeiten (ich habe beispielsweise Keychain für mich ausgewählt, da die Gruppe auch nach dem Löschen für den Benutzer gespeichert wird).

In diesem Beispiel werde ich eine fiktive Analyseklasse erstellen, aber Sie können echte Analysen in Ihrem Projekt verwenden. Sie können beispielsweise den Amplituden- Dienst verwenden.

 // Dummy class for example, use something real, like Amplitude class Analytics {   func logOnce(property: NSObject, for key: String) {       let storageKey = "example.\(key)"       if UserDefaults.standard.object(forKey: storageKey) == nil {           print("Log once value: \(property) for key: \(key)")           UserDefaults.standard.set("", forKey: storageKey) // String because of simulator bug       }   } } extension Analytics: AnalyticsServiceProtocol {   func setOnce(value: String, for key: String) {       self.logOnce(property: value as NSObject, for: key)   } } 

Jetzt können wir unseren Split-Test verwenden:

 let splitTestingService = SplitTestingService(analyticsService: Analytics(),                                                      storage: UserDefaults.standard) let buttonSplitTest = splitTestingService.fetchSplitTest(ButtonColorSplitTest.self) self.button.backgroundColor = buttonSplitTest.currentGroup.color buttonSplitTest.hitSplitTest() 

Erstellen Sie einfach Ihre eigene Instanz, extrahieren Sie den Split-Test und verwenden Sie ihn. Mit Verallgemeinerungen können Sie buttonSplitTest.currentGroup.color.

Bei der ersten Verwendung wird Folgendes angezeigt ( Wert einmal protokollieren ) : split_test-button_color für key: dark_gray . Wenn Sie die Anwendung nicht vom Gerät entfernen, ist die Schaltfläche bei jedem Start dieselbe.



Die Implementierung einer solchen Bibliothek dauert einige Zeit. Danach wird jeder neue Split-Test in Ihrem Projekt in wenigen Minuten erstellt.

Hier ist ein Beispiel für die Verwendung der Engine in einer realen Anwendung: In der Analytik haben wir Benutzer nach dem Komplexitätskoeffizienten und der Wahrscheinlichkeit des Kaufs von Spielwährung segmentiert.



Personen, die noch nie auf diesen Schwierigkeitsgrad gestoßen sind (keiner), spielen wahrscheinlich überhaupt nicht und kaufen nichts in Spielen (was logisch ist). Deshalb ist es wichtig, das Ergebnis (generierte Version) des Split-Tests zu einem Zeitpunkt an den Server zu senden Benutzer begegnen Ihrem Test wirklich.

Ohne Schwierigkeitsgrad kauften nur 2% der Benutzer Spielwährung. Mit einer geringen Quote wurden bereits 3% eingekauft. Und mit einem hohen Schwierigkeitsgrad kauften 4% der Spieler die Währung. Dies bedeutet, dass Sie den Koeffizienten weiter erhöhen und die Zahlen beobachten können. :) :)

Wenn Sie daran interessiert sind, die Ergebnisse mit maximaler Zuverlässigkeit zu analysieren, empfehle ich Ihnen, dieses Tool zu verwenden .

Vielen Dank an das wundervolle Team, das mir bei der Arbeit an diesem Artikel geholfen hat (insbesondere Igor , Kelly und Hiro ).

Das gesamte Demo-Projekt finden Sie unter diesem Link .

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


All Articles