Dane do testu A/B z GA4 w BigQuery — plan przed startem

Kalkulator wielkości próby potrzebuje dwóch liczb z danych: ilu użytkowników wchodzi w tygodniu na testowaną stronę i ilu z nich kupuje. Najczęściej bierze się je z raportu GA4 — sesje z ostatniego tygodnia, konwersje z całej witryny — i plan rozjeżdża się z wynikiem, zanim test w ogóle ruszy. Test liczy osoby od pierwszego wejścia, a plan policzono na czymś innym.

Dane do testu A/B z GA4 w BigQuery liczysz przed startem tą samą definicją, którą po teście ocenisz wynik. Poniżej zapytanie i powód każdej decyzji w nim.

Dlaczego dane do testu A/B liczysz w BigQuery, nie w raporcie GA4

Raport GA4 odpowiada na inne pytanie. Pokazuje ruch całej witryny, a konwersję liczy na sesjach albo na wszystkich użytkownikach — bez warunku, że zakup nastąpił po wejściu na testowaną stronę. Test A/B karty produktu dotyczy węższej grupy: ludzi, którzy kartę zobaczyli, bo tylko oni mogli trafić do wariantu. Zakup sprzed tego wejścia nie zależał od wariantu, więc nie ma go w wyniku testu — i nie powinno go być w planie.

Eksport GA4 do BigQuery daje surowe zdarzenia. Na nich ustawiasz definicje sam:

  • Populacja — użytkownicy, którzy wykonali zdarzenie wejścia do testu. Dla karty produktu to view_item; dla koszyka byłoby view_cart.
  • Konwersja — purchase po pierwszym wejściu. Zakup sprzed wejścia odpada.
  • Jednostka — osoba, nie sesja i nie zamówienie. Ktoś, kto kupił dwa razy, liczy się raz.

Dlaczego osoby, a nie sesje, opisałem w testach A/B dla analityków: test statystyczny zakłada niezależne obserwacje, a pięć sesji jednej osoby nimi nie jest. Te same trzy definicje stoją w zapytaniu po teście, które pokazałem przy własnych testach A/B w GTM. Plan i ocena liczą wtedy to samo — pod warunkiem, że wariant przydzielasz na tym samym zdarzeniu, od którego liczysz plan.

Pułapka: tydzień razy liczba tygodni

Najprostszy pomysł brzmi: policz jeden tydzień i pomnóż. Test potrwa cztery tygodnie, więc zbierze cztery razy tylu użytkowników. Nie zbierze.

Ludzie wracają. Ktoś, kto obejrzał kartę produktu w poniedziałek pierwszego tygodnia i wrócił w trzecim, w teście liczy się raz — od pierwszego wejścia. Unikalnych użytkowników w oknie czterech tygodni jest więc mniej niż cztery razy tyle co w jednym. Test zbiera ich wolniej, niż sugeruje tydzień.

Drugi efekt działa w przeciwną stronę. Im dłuższe okno, tym więcej czasu na zakup, więc CR liczony na unikalnych użytkownikach rośnie. Mniej użytkowników wydłuża test, wyższy CR go skraca. Który efekt wygra, zależy od sklepu — dlatego nie zgadujesz, tylko liczysz na danych, w oknie tak długim jak planowany test.

Wynik zapytania wygląda tak (liczby poglądowe):

Okno (tygodnie)Użytkownicy na tydzieńKonwertujący na tydzień
125 000375
222 500383
321 000392
420 000400
519 200403

„Na tydzień” to wartość z okna podzielona przez liczbę tygodni. Spada, bo powracający liczą się raz. CR rośnie z 1,5% w oknie tygodniowym do 2,1% w pięciotygodniowym.

Z pierwszego wiersza wyszłoby 100 000 użytkowników w czterech tygodniach zamiast 80 000 i CR 1,5% zamiast 2%. W tym przykładzie oba błędy prawie się znoszą: kalkulator i tak policzy 4 tygodnie, ale 49 193 użytkowników na wariant zamiast 36 693. W sklepie, w którym CR rośnie z oknem wolniej, sam zawyżony ruch skróci plan poniżej tego, czego test naprawdę potrzebuje.

Zapytanie: okna narastające na eksporcie GA4

Zapytanie liczy okna od 1 do max_weeks tygodni, wszystkie od tego samego poniedziałku. Każdy użytkownik wchodzi do okna od pierwszego view_item, a jako konwertujący — jeśli kupił po tym wejściu do końca okna.

-- Użytkownicy i konwertujący na tydzień
-- w oknach 1..max_weeks tygodni przed testem
DECLARE window_start DATE DEFAULT DATE '2020-11-02';
DECLARE max_weeks INT64 DEFAULT 5;

ASSERT EXTRACT(DAYOFWEEK FROM window_start) = 2
  AS 'window_start musi być poniedziałkiem';
ASSERT max_weeks BETWEEN 1 AND 8
  AS 'max_weeks: od 1 do 8 tygodni';

WITH events AS (
  SELECT
    user_pseudo_id,
    event_name,
    event_timestamp,
    DIV(DATE_DIFF(
      PARSE_DATE('%Y%m%d', event_date),
      window_start, DAY), 7) + 1 AS week_no
  FROM `projekt.analytics_123456.events_*`
  -- _TABLE_SUFFIX ogranicza skanowane dni i koszt
  WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', window_start)
    AND FORMAT_DATE('%Y%m%d', DATE_ADD(
      window_start, INTERVAL max_weeks * 7 - 1 DAY))
    AND event_name IN ('view_item', 'purchase')
),

-- pierwsze wejście na kartę produktu
first_view AS (
  SELECT
    user_pseudo_id,
    MIN(event_timestamp) AS first_view_ts,
    MIN(week_no) AS first_week
  FROM events
  WHERE event_name = 'view_item'
  GROUP BY user_pseudo_id
),

-- tydzień pierwszego zakupu PO wejściu
first_purchase AS (
  SELECT
    f.user_pseudo_id,
    MIN(p.week_no) AS purchase_week
  FROM first_view AS f
  JOIN events AS p
    ON p.user_pseudo_id = f.user_pseudo_id
    AND p.event_name = 'purchase'
    AND p.event_timestamp > f.first_view_ts
  GROUP BY f.user_pseudo_id
),

-- okno k: weszli w tyg. 1..k, kupili do końca k
windows AS (
  SELECT
    w AS weeks,
    COUNTIF(f.first_week <= w) AS users_unique,
    COUNTIF(f.first_week <= w
      AND fp.purchase_week <= w) AS converters
  FROM UNNEST(GENERATE_ARRAY(1, max_weeks)) AS w
  CROSS JOIN first_view AS f
  LEFT JOIN first_purchase AS fp
    ON fp.user_pseudo_id = f.user_pseudo_id
  GROUP BY w
)

SELECT
  weeks,
  CAST(ROUND(users_unique / weeks) AS INT64)
    AS users_per_week,
  CAST(ROUND(converters / weeks) AS INT64)
    AS converters_per_week
FROM windows
ORDER BY weeks;

Kilka decyzji, które łatwo przeoczyć:

  • Pełne tygodnie od poniedziałku. W poniedziałek i w sobotę kupują inni ludzie, więc każde okno ma pełne tygodnie, czyli każdy dzień tygodnia tyle samo razy. Poniedziałek dlatego, że test startuje w poniedziałek, a okna mają kończyć się dzień przed nim. ASSERT zatrzymuje zapytanie, gdy window_start wypada w inny dzień. Drugi ASSERT pilnuje, żeby max_weeks mieściło się między 1 a 8 — dłuższego testu kalkulator nie dopuszcza.
  • Okno kończy się przed testem. Dane z okresu testu mieszają ruch z wariantem B. Bierzesz tygodnie bezpośrednio przed planowanym startem, bez bieżącego, niepełnego tygodnia.
  • purchase_week to tydzień pierwszego zakupu. Gdy ktoś kupił w tygodniu 2 i 4, w oknie trzytygodniowym jest konwertującym. W dwutygodniowym też. W jednotygodniowym — nie.
  • CROSS JOIN z tablicą tygodni liczy wszystkie okna jednym przebiegiem, bez pętli i bez pięciu osobnych zapytań.

Na własnym projekcie podmieniasz tylko nazwę tabeli. Bez własnego eksportu wstaw w miejsce projekt.analytics_123456.events_* publiczny bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_* (dane z 1 listopada 2020 do 31 stycznia 2021). Domyślne daty pasują do tego zbioru.

Jak przenieść wynik do kalkulatora

Wiersz wybierasz po planowanej długości testu, a długość testu wynika z kalkulatora. To pętla, ale krótka:

  1. Planujesz 5 tygodni, więc bierzesz wiersz 5 i wpisujesz 19 200 w „Użytkownicy w tygodniu” oraz 403 w „Konwertujący w tygodniu”.
  2. Kalkulator wielkości próby przy MDE +15%, 95% ufności, 80% mocy, teście dwustronnym i 2 wariantach liczy 3,64 tygodnia, czyli 4 pełne tygodnie.
  3. Bierzesz wiersz 4: 20 000 i 400. Kalkulator liczy 3,67 tygodnia, czyli znowu 4.
  4. Wiersz i wynik się zgadzają — plan gotowy.

Zwykle wystarczy jedna albo dwie iteracje. Gdy wynik skacze między dwoma wierszami, weź dłuższy: test, który trwa tydzień za długo, kosztuje mniej niż test o zbyt małej mocy.

Przychód na tydzień — żeby MDE miało cenę w złotówkach

MDE to najmniejszy wzrost, który test ma wykryć. Biznesowi trudno ocenić „+14%”, łatwiej „+18 tys. zł tygodniowo” (przy przychodzie 125 tys. zł na tydzień). Kalkulator przeliczy wykrywalne MDE na złotówki, jeśli w polu „Tygodniowy przychód ze strony testowej” dostanie przychód z tej samej populacji:

-- Przychód na tydzień od tych, którzy weszli
-- na kartę produktu: 4 pełne tygodnie przed testem
DECLARE window_end DATE DEFAULT DATE '2020-12-06';
DECLARE window_weeks INT64 DEFAULT 4;

WITH events AS (
  SELECT
    user_pseudo_id,
    event_name,
    event_timestamp,
    ecommerce.purchase_revenue AS revenue
  FROM `projekt.analytics_123456.events_*`
  WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(
        window_end, INTERVAL window_weeks * 7 - 1 DAY))
    AND FORMAT_DATE('%Y%m%d', window_end)
    AND event_name IN ('view_item', 'purchase')
),

first_view AS (
  SELECT
    user_pseudo_id,
    MIN(event_timestamp) AS first_view_ts
  FROM events
  WHERE event_name = 'view_item'
  GROUP BY user_pseudo_id
)

SELECT
  CAST(ROUND(IFNULL(SUM(p.revenue), 0)
    / window_weeks) AS INT64) AS weekly_revenue
FROM events AS p
JOIN first_view AS f
  ON f.user_pseudo_id = p.user_pseudo_id
  AND p.event_timestamp > f.first_view_ts
WHERE p.event_name = 'purchase';

Cztery pełne tygodnie zamiast 30 dni, bo każdy dzień tygodnia wchodzi tyle samo razy. Tydzień zamiast miesiąca, bo w tej samej jednostce kalkulator dostaje ruch. Na publicznym eksporcie użyj purchase_revenue_in_usd — sklep demo raportuje w dolarach.

Kalkulator mnoży przychód tygodniowy przez wykrywalne MDE i pokazuje kwotę obok każdego tygodnia testu. To założenie, że przychód na kupującego się nie zmieni — test mierzy CR, nie przychód.

Czego to zapytanie nie załatwi

  • user_pseudo_id to przeglądarka, nie osoba. Ten sam człowiek na telefonie i laptopie to dwóch użytkowników, a wyczyszczone cookies robią z niego nowego. Test liczy tak samo, więc plan i wynik są spójne — ale żaden nie liczy ludzi.
  • Bez zgody nie ma danych. W podstawowym trybie Consent Mode użytkownicy bez zgody na analitykę nie trafiają do eksportu. Plan i test widzą tę samą populację, więc porównanie się trzyma. Przychód tygodniowy z eksportu jest za to niższy niż w systemie sprzedaży, więc kwota w kalkulatorze jest raczej zaniżona niż zawyżona.
  • Zdublowany purchase zawyża przychód. Tag potrafi wysłać zakup dwa razy, np. po odświeżeniu strony podziękowania. Raporty GA4 usuwają takie duplikaty po transaction_id, eksport do BigQuery ich nie usuwa. Konwertujących to nie zmienia, bo liczysz osoby. Przychód — tak. Jeśli widzisz duplikaty, licz przychód po unikalnym ecommerce.transaction_id.
  • Segmenty trzeba zaplanować osobno. Plan dla samych nowych użytkowników wymaga filtra w first_view, statusu z chwili pierwszego wejścia i osobnego przebiegu kalkulatora. Segment dopisany po obejrzeniu wyniku to szukanie wzrostu, nie analiza.
  • Sezonowość zostaje. Okno przed Black Friday nie powie nic o ruchu w Black Friday. Gdy test ma objąć nietypowy okres, licz okno z analogicznego okresu rok wcześniej.

Kalkulator robi statystykę. Jakość planu zależy od tego, czy liczby, które do niego wpisujesz, mierzą to samo co test. Godzina nad SQL-em przed startem oszczędza miesiąc testu, który nic nie rozstrzygnie.

Powiązane wpisy