Como testar hipóteses e ganhar dinheiro com o Swift usando testes divididos


Olá pessoal! Meu nome é Sasha Zimin, trabalho como desenvolvedor iOS no escritório de Londres do Badoo . O Badoo tem um relacionamento muito próximo com os gerentes de produto, e eu adquiri o hábito de testar todas as hipóteses que tenho em relação ao produto. Então, comecei a escrever testes divididos para meus projetos.

A estrutura que será discutida neste artigo foi escrita para dois propósitos. Em primeiro lugar, para evitar possíveis erros, é melhor não ter dados no sistema de análise do que dados incorretos (ou mesmo dados que podem ser interpretados incorretamente e quebrados em lenha). Em segundo lugar, para simplificar a implementação de cada teste subsequente. Mas vamos começar com o que são testes divididos.

Atualmente, existem milhões de aplicativos que atendem à maioria das necessidades dos usuários; portanto, a cada dia fica mais difícil criar novos produtos competitivos. Isso levou muitas empresas e startups a realizar várias pesquisas e experimentos, a princípio, para descobrir quais recursos melhoram seu produto e quais podem ser dispensados.

Uma das principais ferramentas para a realização de tais experimentos é o teste dividido (ou teste A / B). Neste artigo, mostrarei como ele pode ser implementado no Swift.

Todas as demonstrações do projeto estão disponíveis aqui . Se você já tem uma idéia sobre o teste A / B, pode ir diretamente para o código .

Uma breve introdução ao teste de divisão


O teste de divisão ou teste A / B (esse termo nem sempre está correto, porque você pode ter mais de dois grupos de participantes), é uma maneira de verificar versões diferentes de um produto em diferentes grupos de usuários para entender qual versão é melhor. Você pode ler sobre isso na Wikipedia ou, por exemplo, neste artigo com exemplos reais.

No Badoo, executamos muitos testes divididos ao mesmo tempo. Por exemplo, uma vez que decidimos que a página de perfil do usuário em nosso aplicativo parece desatualizada e também desejamos melhorar a interação do usuário com alguns banners. Portanto, lançamos o teste dividido com três grupos:

  1. Perfil antigo
  2. Nova versão do perfil 1
  3. Nova versão do perfil 2

Como você pode ver, tínhamos três opções, mais como teste A / B / C (e é por isso que preferimos usar o termo "teste de divisão").

Então, diferentes usuários viram seus perfis:



No console do Product Manager, tínhamos quatro grupos de usuários formados aleatoriamente e com o mesmo número:



Talvez você pergunte por que temos control e control_check (se control_check é uma cópia da lógica do grupo de controle)? A resposta é muito simples: qualquer alteração afeta muitos indicadores; portanto, nunca podemos ter certeza absoluta de que uma alteração específica é o resultado de um teste de divisão, e não de outras ações.

Se você acha que alguns indicadores foram alterados devido ao teste de divisão, verifique novamente se eles são os mesmos nos grupos control e control_check.

Como você pode ver, as opiniões dos usuários podem diferir, mas a evidência empírica é uma evidência clara. A equipe de gerentes de produto analisa os resultados e entende por que uma opção é melhor que a outra.

Teste dividido e rápido


Objetivos:

  1. Crie uma biblioteca para o lado do cliente (sem usar um servidor).
  2. Salve a opção de usuário selecionada no armazenamento permanente depois que ela foi gerada acidentalmente.
  3. Envie relatórios sobre as opções selecionadas para cada teste de divisão ao serviço de análise.
  4. Aproveite ao máximo os recursos do Swift.

PS O uso dessa biblioteca para testes divididos da parte do cliente tem suas vantagens e desvantagens. A principal vantagem é que você não precisa ter uma infraestrutura de servidor ou um servidor dedicado. E a desvantagem é que, se algo der errado durante o experimento, você não poderá reverter sem fazer o download da nova versão na App Store.

Algumas palavras sobre a implementação:

  1. Durante o experimento, a opção para o usuário é selecionada aleatoriamente de acordo com o princípio igualmente provável.
  2. O serviço de teste de divisão pode usar:

  • Qualquer armazenamento de dados (por exemplo, UserDefaults, Realm, SQLite ou Core Data) como uma dependência e salva o valor atribuído ao usuário (o valor de sua variante) nele.
  • Qualquer serviço de análise (por exemplo, Amplitude ou Facebook Analytics) como uma dependência e envia a versão atual no momento em que o usuário encontra um teste de divisão.

Aqui está um diagrama de futuras classes:



Todos os testes de divisão serão apresentados usando SplitTestProtocol e cada um deles terá várias opções (grupos) que serão apresentadas no SplitTestGroupProtocol .

O teste de divisão deve poder informar o analista sobre a versão atual; portanto, ele terá o AnalyticsProtocol como uma dependência.

O serviço SplitTestingService salvará, gerará opções e gerenciará todos os testes divididos. É ele quem baixa a versão atual do usuário do armazenamento, que é determinada pelo StorageProtocol , e também passa o AnalyticsProtocol para o SplitTestProtocol .


Vamos começar a escrever código com as dependências AnalyticsProtocol e 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? } 

O papel da análise é registrar um evento uma vez. Por exemplo, para corrigir esse usuário A, está no grupo azul durante o teste de divisão button_color , quando ele vê uma tela com esse botão.

A função do repositório é salvar uma opção específica para o usuário atual (depois que o SplitTestingService gerou essa opção) e, em seguida, lê-la novamente sempre que o programa acessar esse teste de divisão.

Então, vamos dar uma olhada no SplitTestGroupProtocol , que caracteriza um conjunto de opções para um teste de divisão específico:

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

Como RawRepresentable, em que RawValue é uma string, você pode facilmente criar uma variante de uma string ou convertê-la novamente em uma string, o que é muito conveniente para trabalhar com análises e armazenamento. O SplitTestGroupProtocol também contém uma matriz de testGroups, que pode indicar a composição das opções atuais (essa matriz também será usada para geração aleatória a partir das opções disponíveis).

Este é o protótipo base para o próprio teste de divisão SplitTestProtocol :

 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 contém:

  1. Um tipo de GroupType que implementa o protocolo SplitTestGroupProtocol por representar um tipo que define um conjunto de opções.
  2. O identificador do valor da sequência para análise e chaves de armazenamento.
  3. A variável currentGroup para registrar uma instância específica de SplitTestProtocol .
  4. Dependência do Analytics para o método hitSplitTest .
  5. E o método hitSplitTest , que informa ao analista que o usuário viu o resultado do teste de divisão.

O método hitSplitTest permite garantir que os usuários não estejam apenas em uma versão específica, mas também tenham visto o resultado do teste. Se você marcar um usuário que não visitou a seção de compras como "saw_red_button_on_purcahse_screen", isso distorcerá os resultados.

Agora estamos prontos para o 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 Nesta classe, usamos a função Int.random, tirada de
aqui , mas no Swift 4.2, ele já está embutido por padrão.

Esta classe contém um método público fetchSplitTest e três métodos privados: saveGroup , getGroup , randomGroup .

O método randomGroup gera uma variante aleatória para o teste de divisão selecionado, enquanto getGroup e saveGroup permitem salvar ou carregar a variante para um teste de divisão específico para o usuário atual.

A função principal e pública dessa classe é fetchSplitTest: ela tenta retornar a versão atual do armazenamento persistente e, se não funcionar, gera e salva uma versão aleatória antes de devolvê-la.



Agora estamos prontos para criar nosso primeiro teste de divisão:

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

Parece impressionante, mas não se preocupe: assim que você implementar o SplitTestProtocol como uma classe separada, o compilador solicitará que você implemente todas as propriedades necessárias.

A parte importante aqui é o tipo de grupo enum . Você deve colocar todos os seus grupos nele (em nosso exemplo, vermelho, azul e darkGray) e definir valores de sequência aqui para garantir a transferência correta para o analytics.

Também temos uma extensão ButtonColorSplitTest.Group , permitindo que você use todo o potencial do Swift. Agora vamos criar os objetos para o AnalyticsProtocol e o 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   } } 

Para StorageProtocol , usaremos a classe UserDefaults porque é fácil de implementar, mas em seus projetos você pode trabalhar com qualquer outro armazenamento persistente (por exemplo, escolhi o Keychain por mim mesmo, pois ele salva o grupo para o usuário mesmo após a exclusão).

Neste exemplo, vou criar uma classe de análise fictícia, mas você pode usar análises reais em seu projeto. Por exemplo, você pode usar o serviço 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)   } } 

Agora estamos prontos para usar nosso teste de divisão:

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

Basta criar sua própria instância, extrair o teste de divisão e usá-lo. As generalizações permitem chamar buttonSplitTest.currentGroup.color.

Durante o primeiro uso, você pode ver algo como ( Registrar uma vez o valor ) : split_test-button_color para key: dark_gray e, se você não remover o aplicativo do dispositivo, o botão será o mesmo sempre que você iniciar.



O processo de implementação dessa biblioteca leva algum tempo, mas depois disso, cada novo teste de divisão dentro do seu projeto será criado em alguns minutos.

Aqui está um exemplo de uso do mecanismo em um aplicativo real: nas análises, segmentamos os usuários pelo coeficiente de complexidade e probabilidade de comprar a moeda do jogo.



As pessoas que nunca encontraram esse fator de dificuldade (nenhum) provavelmente não jogam e não compram nada em jogos (o que é lógico), e é por isso que é importante enviar o resultado (versão gerada) do teste de divisão para o servidor no momento em que os usuários realmente encontram seu teste.

Sem um fator de dificuldade, apenas 2% dos usuários compraram a moeda do jogo. Com uma pequena proporção, as compras já foram feitas em 3%. E com um alto fator de dificuldade, 4% dos jogadores compraram a moeda. Isso significa que você pode continuar aumentando o coeficiente e observando os números. :)

Se você estiver interessado em analisar os resultados com a máxima confiabilidade, recomendamos que você use esta ferramenta .

Agradeço à equipe maravilhosa que me ajudou a trabalhar neste artigo (especialmente Igor , Kelly e Hiro ).

Todo o projeto de demonstração está disponível neste link .

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


All Articles