Upgrade to Pro — share decks privately, control downloads, hide ads and more …

A/B Testing on Android - Yelp Engineering

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →
Avatar for MSiddeek MSiddeek
September 13, 2017

A/B Testing on Android - Yelp Engineering

Making Measurable Improvements to your App with Experiments

RECORDING: https://youtu.be/fuHaos3XFWo

Experimentation is a great way to prove the value of your changes in your app; you split your user base into groups, each receiving a different treatment, and based on their usage you decide which treatment is better. Sounds easy? Well… In practice, things get tricky. I want to share with you how we run experiments at Yelp and how we manage to do that effectively. In this talk you will learn about:

- Different types of experiments and ways to assign treatments.
- Structuring your app to support experimentation.
- Reporting results and pitfalls that can ruin your analysis.

By the end of this you'll be itching to start the next experiment on your app!

Avatar for MSiddeek

MSiddeek

September 13, 2017

More Decks by MSiddeek

Other Decks in Technology

Transcript

  1. A/B Testing Statistical procedure to show that some change to

    your app
 affects your users’ experience “
  2. “IF we make pineapples a default topping on our “Pizza

    Order” page, THEN daily orders will increase, BECAUSE pineapple is the most frequently selected topping. ✔
  3. THEN We Get Result Y CLICK THROUGH RATE SESSION LENGTH

    REVENUE Low-Level High-Level Quicker Feedback Harder To Analyze
  4. • Population size • Session length • Events/session • Number

    of signed in users • … Test Over? Sanity Check!
  5. User I User II User III Dimensions DE DE 7.1

    8.0 Country IT 7.1 App
 Version
  6. User I User II User III Custom Dimensions DE DE

    7.1 8.0 Country App
 Version IT 7.1 A A Experiment Bucket B
  7. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket() { if (user.getCountry()

    == “IT”) { return Bucket.B; } else { return Bucket.A; } }
  8. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket() { if (user.getCountry()

    == “IT”) { return Bucket.B; } else { return Bucket.A; } } ✘
  9. Bucket[] buckets = {Bucket.A, Bucket.B}; int rand = new Random().nextInt();

    Bucket getBucket() { return buckets[rand % buckets.length]; }
  10. Bucket[] buckets = {Bucket.A, Bucket.B}; int rand = new Random().nextInt();

    Bucket getBucket() { return buckets[rand % buckets.length]; }
  11. Bucket[] buckets = {Bucket.A, Bucket.B}; int rand = new Random().nextInt();

    Bucket getBucket() { if (getCachedBucket() == null) { cacheBucket(buckets[rand % buckets.length]); } return getCachedBucket(); }
  12. Bucket[] buckets = {Bucket.A, Bucket.B}; int rand = new Random().nextInt();

    Bucket getBucket() { if (getCachedBucket() == null) { cacheBucket(buckets[rand % buckets.length]); } return getCachedBucket(); } Local Random Experiment ✔
  13. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId) { return

    buckets[userId % buckets.length]; } Unit of Diversion
  14. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { return buckets[(userId % 10) * buckets.length / 10]; }
  15. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { return buckets[(userId % 10) * buckets.length / 10]; } {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} * 2 / 10
  16. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { return buckets[(userId % 10) * buckets.length / 10]; } {0, 2, 4, 6, 8, 10, 12, 14, 16, 18} / 10
  17. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { return buckets[(userId % 10) * buckets.length / 10]; } {0, 0, 0, 0, 0, 1, 1, 1, 1, 1}
  18. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { return buckets[(userId % 10) * buckets.length / 10]; } {0, 0, 0, 0, 0, 1, 1, 1, 1, 1}
  19. 0 1 2 3 4 5 6 7 8 9

    btnClr rateApp payText autoVid fakeSlp
  20. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { return buckets[(userId % 10) * buckets.length / 10]; }
  21. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { int bucketId = userId + expId.hashCode(); return buckets[(bucketId % 10) * buckets.length / 10]; }
  22. 0 1 2 3 4 5 6 7 8 9

    btnClr rateApp payText autoVid fakeSlp
  23. 0 1 2 3 4 5 6 7 8 9

    btnClr rateApp payText autoVid fakeSlp
  24. Bucket[] buckets = {Bucket.A, Bucket.B}; Bucket getBucket(int userId, String expId)

    { int bucketId = userId ⊕ expId.hashCode(); return buckets[(bucketId % 10) * buckets.length / 10]; } Local Persistent Experiment ✔
  25. 0 1 2 3 4 5 6 7 8 9

    exp1 50% 50%
  26. 0 1 2 3 4 5 6 7 8 9

    exp1 50% 50% exp2 80% 20%
  27. 0 1 2 3 4 5 6 7 8 9

    exp1 50% 50% exp2 80% 20% exp3 40% 20% 20% 20%
  28. 0 1 2 3 4 5 6 7 8 9

    exp1 50% 50% exp2 80% 20% exp3 40% 20% 20% 20% flag1 DE US (any)
  29. Local Persistent
 Experiment Local Random
 Experiment Remote
 Experiment Persistent Persistent

    Not Persistent Simple Client is Simple Client is Complex Easy to Re-configure Not Configurable Not Configurable
  30. Local Persistent
 Experiment Local Random
 Experiment Remote
 Experiment Persistent Persistent

    Not Persistent Simple Client is Simple Client is Complex Easy to Re-configure Not Configurable Not Configurable Asynchronous Synchronous Synchronous
  31. Local Persistent
 Experiment Local Random
 Experiment Remote
 Experiment Persistent Persistent

    Not Persistent Simple Client is Simple Client is Complex Easy to Re-configure Not Configurable Not Configurable Asynchronous Synchronous Synchronous
  32. NewUser SignUp Join Point of Diversion screenBldr = new HitBuilders

    .ScreenViewBuilder(); tracker.setScreenName(“Welcome”); tracker.send(screenBldr.build()); AppStart Welcome Welcome
  33. NewUser screenBldr = new HitBuilders .ScreenViewBuilder() .setCustomDimension( cdIndex, cdValue );

    tracker.setScreenName(“Welcome”); tracker.send(screenBldr.build()); AppStart SignUp Join Welcome Welcome
  34. NewUser screenBldr = new HitBuilders .ScreenViewBuilder() .setCustomDimension( cdIndex, cdValue );

    tracker.setScreenName(“Welcome”); tracker.send(screenBldr.build()); AppStart SignUp Join Welcome Welcome
  35. NewUser screenBldr = new HitBuilders .ScreenViewBuilder() .setCustomDimension( cdIndex, cdValue );

    tracker.setScreenName(“Welcome”); tracker.send(screenBldr.build()); AppStart SignUp Join Welcome Welcome 0-199
  36. NewUser screenBldr = new HitBuilders .ScreenViewBuilder() .setCustomDimension( cdIndex, cdValue );

    tracker.setScreenName(“Welcome”); tracker.send(screenBldr.build()); AppStart SignUp Join Welcome Welcome 0-199 any string
  37. NewUser int cdIndex = CD.exp_wlcmBtn; String cdValue; switch (getBucket()) {

    case Bucket.B: cdValue = “SignUp”; break; default: cdValue = “Join”; break; } AppStart SignUp Join Welcome Welcome
  38. NewUser int cdIndex = CD.exp_wlcmBtn; String cdValue; switch (getBucket()) {

    case Bucket.B: cdValue = “wlcmBtnSignUp”; break; default: cdValue = “wlcmBtnJoin”; break; } AppStart wlcmBtnSignUp wlcmBtnJoin Welcome Welcome
  39. int cdIndex = CD.exp_wlcmBtn; String cdValue; switch (getBucket()) { case

    Bucket.B: cdValue = “wlcmRateOn”; break; default: cdValue = “wlcmRateOff”; break; } AppStart RateApp Welcome Welcome NewsFeed
  40. wlcmRateOff wlcmRateOn int cdIndex = CD.exp_wlcmBtn; String cdValue; switch (getBucket())

    { case Bucket.B: cdValue = “wlcmRateOn”; break; default: cdValue = “wlcmRateOff”; break; } AppStart Welcome Welcome RateApp NewsFeed
  41. wlcmRateOff wlcmRateOn int cdIndex = CD.exp_wlcmBtn; String cdValue; switch (getBucket())

    { case Bucket.B: cdValue = “wlcmRateOn”; break; default: cdValue = “wlcmRateOff”; break; } AppStart Welcome Welcome RateApp NewsFeed
  42. wlcmRateOff wlcmRateOn int cdIndex = CD.exp_wlcmBtn; String cdValue; switch (getBucket())

    { case Bucket.B: cdValue = “wlcmRateOn”; break; default: cdValue = “wlcmRateOff”; break; } DeepLink Welcome Welcome RateApp NewsFeed
  43. wlcmRateOff wlcmRateOn int cdIndex = CD.exp_wlcmBtn; String cdValue = “<no-bucket>”;

    switch (getBucket()) { case Bucket.B: cdValue = “wlcmRateOn”; break; case Bucket.A: cdValue = “wlcmRateOff”; break; } DeepLink Welcome Welcome RateApp NewsFeed
  44. $ adb shell setprop log.tag.GAv4 DEBUG $ adb logcat -s

    GAv4 Tip - Test Custom Dimensions E2E
  45. $ adb shell setprop log.tag.GAv4 DEBUG $ adb logcat -s

    GAv4 09-10 15:46:17.975 12296-12326/com.msiddeek.app D/GAv4:
 Hit delivery requested: ht=1505051177805, _s=2, _v=ma10.2.40, a=474619346, aid=com.msiddeek.nomnom, an=Nom Nom, av=1.0, cd=Welcome, cd1=bucket_a, sr=360x592, t=screenview, tid=12, ul=en-us, v=1, cid=89296d03-6e60-434e-8cf7-842d496ff220, Tip - Test Custom Dimensions E2E
  46. $ adb shell setprop log.tag.GAv4 DEBUG $ adb logcat -s

    GAv4 09-10 15:46:17.975 12296-12326/com.msiddeek.app D/GAv4:
 Hit delivery requested: ht=1505051177805, _s=2, _v=ma10.2.40, a=474619346, aid=com.msiddeek.nomnom, an=Nom Nom, av=1.0, cd=Welcome, cd1=bucket_a, sr=360x592, t=screenview, tid=12, ul=en-us, v=1, cid=89296d03-6e60-434e-8cf7-842d496ff220, Tip - Test Custom Dimensions E2E