Comment tester des hypothèses et gagner de l'argent sur Swift à l'aide de tests fractionnés


Bonjour à tous! Je m'appelle Sasha Zimin, je travaille en tant que développeur iOS dans le bureau londonien de Badoo . Badoo a une relation très étroite avec les chefs de produit, et j'ai pris l'habitude de tester toutes les hypothèses que j'ai sur le produit. J'ai donc commencé à écrire des tests fractionnés pour mes projets.

Le cadre qui sera discuté dans cet article a été écrit à deux fins. Premièrement, afin d'éviter d'éventuelles erreurs, il est préférable de ne pas avoir de données dans le système d'analyse que des données incorrectes (ou même des données qui peuvent être mal interprétées et cassées en bois de chauffage). Deuxièmement, pour simplifier la mise en œuvre de chaque test ultérieur. Mais commençons par ce que sont les tests fractionnés.

De nos jours, il existe des millions d'applications qui répondent à la plupart des besoins des utilisateurs, il devient donc chaque jour plus difficile de créer de nouveaux produits compétitifs. Cela a conduit de nombreuses entreprises et startups à mener diverses recherches et expériences dans un premier temps pour découvrir quelles fonctionnalités amélioraient leur produit et lesquelles pouvaient être supprimées.

L'un des principaux outils pour mener de telles expériences est le test fractionné (ou test A / B). Dans cet article, je dirai comment il peut être implémenté sur Swift.

Toutes les démos de projets sont disponibles ici . Si vous avez déjà une idée sur les tests A / B, vous pouvez accéder directement au code .

Une brève introduction aux tests fractionnés


Le test fractionné, ou test A / B (ce terme n'est pas toujours correct, car vous pouvez avoir plus de deux groupes de participants), est un moyen de vérifier différentes versions d'un produit sur différents groupes d'utilisateurs afin de comprendre quelle version est meilleure. Vous pouvez en lire plus sur Wikipedia ou, par exemple, dans cet article avec de vrais exemples.

Chez Badoo, nous effectuons beaucoup de tests fractionnés en même temps. Par exemple, une fois que nous avons décidé que la page de profil utilisateur de notre application était obsolète, nous voulions également améliorer l'interaction des utilisateurs avec certaines bannières. Par conséquent, nous avons lancé des tests fractionnés avec trois groupes:

  1. Ancien profil
  2. Nouveau profil version 1
  3. Nouveau profil version 2

Comme vous pouvez le voir, nous avions trois options, plus comme les tests A / B / C (et c'est pourquoi nous préférons utiliser le terme «tests fractionnés»).

Ainsi, différents utilisateurs ont vu leurs profils:



Dans la console Product Manager, nous avions quatre groupes d'utilisateurs formés au hasard et ayant le même nombre:



Vous demandez peut-être pourquoi nous avons control et control_check (si control_check est une copie de la logique du groupe de contrôle)? La réponse est très simple: tout changement affecte de nombreux indicateurs, nous ne pouvons donc jamais être absolument sûrs qu'un changement particulier est le résultat d'un test fractionné, et non d'autres actions.

Si vous pensez que certains indicateurs ont changé en raison du test de fractionnement, vous devez vérifier à nouveau qu'ils sont les mêmes dans les groupes control et control_check.

Comme vous pouvez le voir, les opinions des utilisateurs peuvent différer, mais les preuves empiriques sont des preuves claires. L'équipe de chefs de produit analyse les résultats et comprend pourquoi une option est meilleure qu'une autre.

Tests fractionnés et Swift


Objectifs:

  1. Créez une bibliothèque côté client (sans utiliser de serveur).
  2. Enregistrez l'option utilisateur sélectionnée dans le stockage permanent après sa génération accidentelle.
  3. Envoyez des rapports sur les options sélectionnées pour chaque test fractionné au service d'analyse.
  4. Tirez le meilleur parti des capacités de Swift.

PS L'utilisation d'une telle bibliothèque pour les tests fractionnés de la partie client a ses avantages et ses inconvénients. Le principal avantage est que vous n'avez pas besoin d'avoir une infrastructure de serveur ou un serveur dédié. Et l'inconvénient est que si quelque chose se passe mal pendant l'expérience, vous ne pouvez pas revenir en arrière sans télécharger la nouvelle version dans l'App Store.

Quelques mots sur l'implémentation:

  1. Au cours de l'expérience, l'option pour l'utilisateur est choisie au hasard selon le principe également probable.
  2. Le service de test fractionné peut utiliser:

  • Tout magasin de données (par exemple, UserDefaults, Realm, SQLite ou Core Data) en tant que dépendance et y enregistre la valeur attribuée à l'utilisateur (la valeur de sa variante).
  • Tout service d'analyse (par exemple, Amplitude ou Facebook Analytics) en tant que dépendance et envoyer la version actuelle au moment où l'utilisateur rencontre un test fractionné.

Voici un schéma des futures classes:



Tous les tests fractionnés seront présentés à l'aide de SplitTestProtocol , et chacun d'eux aura plusieurs options (groupes) qui seront présentées dans SplitTestGroupProtocol .

Le test fractionné devrait être en mesure d'informer l'analyste de la version actuelle, par conséquent, il aura AnalyticsProtocol comme dépendance.

Le service SplitTestingService enregistre, génère des options et gère tous les tests de fractionnement. C'est lui qui télécharge la version actuelle de l'utilisateur à partir du stockage, qui est déterminé par StorageProtocol , et transmet également AnalyticsProtocol à SplitTestProtocol .


Commençons à écrire du code avec les dépendances AnalyticsProtocol et 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? } 

Le rôle de l'analytique est d'enregistrer un événement une seule fois. Par exemple, pour résoudre le problème, l'utilisateur A se trouve dans le groupe bleu lors du test de division bouton_couleur , lorsqu'il voit un écran avec ce bouton.

Le rôle du référentiel est d'enregistrer une option spécifique pour l'utilisateur actuel (après que le SplitTestingService a généré cette option), puis de la relire chaque fois que le programme accède à ce test de fractionnement.

Jetons donc un coup d'œil à SplitTestGroupProtocol , qui caractérise un ensemble d'options pour un test de fractionnement spécifique:

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

Étant donné que RawRepresentable où RawValue est une chaîne, vous pouvez facilement créer une variante à partir d'une chaîne ou la reconvertir en chaîne, ce qui est très pratique pour travailler avec l'analyse et le stockage. SplitTestGroupProtocol contient également un tableau de testGroups, qui peut indiquer la composition des options actuelles (ce tableau sera également utilisé pour une génération aléatoire à partir des options disponibles).

Voici le prototype de base du test de fractionnement SplitTestProtocol lui - même :

 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 contient:

  1. Un type GroupType qui implémente le protocole SplitTestGroupProtocol pour représenter un type qui définit un ensemble d'options.
  2. Identificateur de valeur de chaîne pour les clés d'analyse et de stockage.
  3. La variable currentGroup pour enregistrer une instance spécifique de SplitTestProtocol .
  4. Dépendance Analytics pour la méthode hitSplitTest .
  5. Et la méthode hitSplitTest , qui informe l'analyste que l'utilisateur a vu le résultat du test de fractionnement.

La méthode hitSplitTest vous permet de vous assurer que les utilisateurs ne sont pas seulement dans une version particulière, mais ont également vu le résultat du test. Si vous marquez un utilisateur qui n'a pas visité la section shopping comme "saw_red_button_on_purcahse_screen", cela faussera les résultats.

Nous sommes maintenant prêts pour 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 Dans cette classe, nous utilisons la fonction Int.random, tirée de
ici , mais dans Swift 4.2, il est déjà intégré par défaut.

Cette classe contient une méthode publique fetchSplitTest et trois méthodes privées: saveGroup , getGroup , randomGroup .

La méthode randomGroup génère une variante aléatoire pour le test de division sélectionné, tandis que getGroup et saveGroup vous permettent d'enregistrer ou de charger la variante pour un test de division spécifique pour l'utilisateur actuel.

La fonction principale et publique de cette classe est fetchSplitTest: elle essaie de renvoyer la version actuelle du stockage persistant et, si elle ne réussit pas, génère et enregistre une version aléatoire avant de la renvoyer.



Nous sommes maintenant prêts à créer notre premier test fractionné:

 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       }   } } 

Cela semble impressionnant, mais ne vous inquiétez pas: dès que vous implémentez SplitTestProtocol en tant que classe distincte, le compilateur vous demandera d'implémenter toutes les propriétés nécessaires.

La partie importante ici est le type de groupe enum . Vous devez y mettre tous vos groupes (dans notre exemple, rouge, bleu et darkGray) et définir des valeurs de chaîne ici pour garantir le transfert correct vers l'analyse.

Nous avons également une extension ButtonColorSplitTest.Group , vous permettant d'utiliser tout le potentiel de Swift. Créons maintenant les objets pour AnalyticsProtocol et 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   } } 

Pour StorageProtocol, nous utiliserons la classe UserDefaults car elle est facile à implémenter, mais dans vos projets, vous pouvez travailler avec n'importe quel autre stockage persistant (par exemple, j'ai choisi le trousseau pour moi-même, car il enregistre le groupe pour l'utilisateur même après la suppression).

Dans cet exemple, je vais créer une classe d'analyse fictive, mais vous pouvez utiliser de vraies analyses dans votre projet. Par exemple, vous pouvez utiliser le service Amplitude .

 // 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)   } } 

Nous sommes maintenant prêts à utiliser notre test de fractionnement:

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

Créez simplement votre propre instance, extrayez le test fractionné et utilisez-le. Les généralisations vous permettent d'appeler buttonSplitTest.currentGroup.color.

Lors de la première utilisation, vous pouvez voir quelque chose comme ( valeur Log once ) : split_test-button_color pour key: dark_gray , et si vous ne supprimez pas l'application de l'appareil, le bouton sera le même à chaque démarrage.



Le processus de mise en œuvre d'une telle bibliothèque prend un certain temps, mais après cela, chaque nouveau test fractionné à l'intérieur de votre projet sera créé en quelques minutes.

Voici un exemple d'utilisation du moteur dans une application réelle: en analytique, nous avons segmenté les utilisateurs par le coefficient de complexité et la probabilité d'achat de la monnaie du jeu.



Les personnes qui n'ont jamais rencontré ce facteur de difficulté (aucun) ne jouent probablement pas du tout et n'achètent rien dans les jeux (ce qui est logique), c'est pourquoi il est important d'envoyer le résultat (version générée) du test fractionné au serveur à un moment où les utilisateurs rencontrent vraiment votre test.

Sans facteur de difficulté, seulement 2% des utilisateurs ont acheté de la monnaie de jeu. Avec un petit ratio, les achats ont déjà été effectués à 3%. Et avec un facteur de difficulté élevé, 4% des joueurs ont acheté la devise. Cela signifie que vous pouvez continuer à augmenter le coefficient et observer les chiffres. :)

Si vous souhaitez analyser les résultats avec une fiabilité maximale, je vous conseille d'utiliser cet outil .

Merci à la merveilleuse équipe qui m'a aidé à travailler sur cet article (en particulier Igor , Kelly et Hiro ).

Le projet de démonstration complet est disponible sur ce lien .

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


All Articles