Skip to content

nrkno/TLGtall

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 

Repository files navigation

Brukere som kommer til oss - og forlater oss - i desember

Innledning

Desember er NRKs – og spesielt NRK TVs – beste måned. Det er da alle tårer som gråtes over at TV2 engasjerer brukerne mer enn NRK gjennom høsten, tørkes, og vi kan igjen klappe oss selv på skulderen og tenke “vi gjør det tross alt jævlig bra”. Men så sprøytes det kaldt vann i årene igjen i januar – eller gjør det det? Dette skal vi komme til bunns i. Og dersom det faktisk er tilfellet at julebruken er så stor som vi tror, hvem er da disse brukerne som kommer og går i desember? Hva kjennetegner dem?

Desemberbrukerne

Her skal vi få noen harde fakta på bordet. Vi undersøker derfor hvor mange brukere som brukte oss kun i desember.

Hva vi vet

Fra den utmerkede PowerBI-rapporten churn rapport som Anna har laget, kan vi lese at for NRK Totalt er ukentlig gjennomtrekk 17,3 % og månedlig 12,5 %. Dette ser vi skjermbildet under.

figurer/Churnskjermdump.png

Det ukentlige gjennomtrekket har heller ikke endret seg vesentlig gjennom året.

figurer/Churnskjermdump_detalj.png

Allerede her tenker vi at det kanskje ikke er så ille som den allminnelige oppfatningen er.

Spade og bøtte

Vi går videre med å bryte ned brukerne på profiltype, aldersgruppe, tjeneste og aktive dager i perioden. Vi konsentrerer oss om brukere som besøker NRK i løpet av tre fireukersperioder som slutter henholdvis 27. november og 25. desember 2024, og 22. januar 2025. For å få til dette har vi laget følgende to spørringer:

#standardSQL
CREATE OR REPLACE TABLE `nrk-scratchbook.Emil.TLGtall_tot` AS (
WITH
  SOURCE_ALL AS (
    SELECT partitionDate dato, nrkUserId, userOrProfile, loyaltyGroup,
           IF(userOrProfile != 'user',
              CASE
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear IS NULL THEN NULL
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 13 THEN CAST(EXTRACT(YEAR FROM partitionDate) - birthYear AS STRING)
                ELSE '13'
              END,
              CASE
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear IS NULL THEN NULL
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 0 THEN '-1'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 13 THEN '0-12'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 20 THEN '13-19'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 30 THEN '20-29'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 50 THEN '30-49'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 70 THEN '50-69'
                ELSE '70+'
              END) aldersgruppe,
           CASE
             WHEN last28Days.daysVisited = 0 THEN 0
             WHEN last28Days.daysVisited < 4 THEN 3
             WHEN last28Days.daysVisited < 7 THEN 6
             WHEN last28Days.daysVisited < 11 THEN 10
             WHEN last28Days.daysVisited < 16 THEN 15
             WHEN last28Days.daysVisited < 22 THEN 21
             WHEN last28Days.daysVisited < 29 THEN 28
           END aktive_dager,
      FROM `nrk-datahub.snowplow_aggregate.total_rfv`
           JOIN `nrk-datahub.prod.registered_users_v01` USING(nrkUserId)
     WHERE partitionDate IN UNNEST(GENERATE_DATE_ARRAY('2025-01-22' - 28*12, '2025-01-22', INTERVAL 28 day))
     ORDER BY nrkUserId, dato),

  FORRIGE_PERIODE28 AS (
    SELECT sa.*,
           sa_fp.loyaltyGroup loyaltyGroup_fp, sa_fp.aktive_dager aktive_dager_fp,
           sa_ffp.loyaltyGroup loyaltyGroup_ffp, sa_ffp.aktive_dager aktive_dager_ffp,
      FROM SOURCE_ALL sa
           LEFT JOIN SOURCE_ALL sa_fp ON sa.dato = sa_fp.dato + 28 AND sa.nrkUserId = sa_fp.nrkUserId
           LEFT JOIN SOURCE_ALL sa_ffp ON sa.dato = sa_ffp.dato + 28 * 2 AND sa.nrkUserId = sa_ffp.nrkUserId)

  SELECT dato, userOrProfile, aldersgruppe, 
         loyaltyGroup,
         loyaltyGroup_fp,
         aktive_dager,
         aktive_dager_fp,
         CASE
           WHEN IFNULL(aktive_dager_ffp, 0) > 0 AND IFNULL(aktive_dager_fp, 0) > 0 THEN 'tilbakevendende'
           WHEN IFNULL(aktive_dager_ffp, 0) > 0 AND NOT IFNULL(aktive_dager_fp, 0) > 0 THEN 'mistede'
           WHEN NOT IFNULL(aktive_dager_ffp, 0) > 0 AND IFNULL(aktive_dager_fp, 0) > 0 THEN 'reaktiverte'
           ELSE 'dvale'
         END Brukstype_fp,
         CASE
           WHEN IFNULL(aktive_dager_fp, 0) > 0 AND IFNULL(aktive_dager, 0) > 0 THEN 'tilbakevendende'
           WHEN IFNULL(aktive_dager_fp, 0) > 0 AND NOT IFNULL(aktive_dager, 0) > 0 THEN 'mistede'
           WHEN NOT IFNULL(aktive_dager_fp, 0) > 0 AND IFNULL(aktive_dager, 0) > 0 THEN 'reaktiverte'
           ELSE 'dvale'
         END Brukstype,
         COUNT(nrkUserId) brukere
    FROM FORRIGE_PERIODE28
   GROUP BY ALL)

Denne spørringen gir samme resultat som over, men fordeler bruken på tjenestene NRK.no, NRK Radio, NRK TV og NRK Super.

#standardSQL
CREATE OR REPLACE TABLE `nrk-scratchbook.Emil.TLGtall` AS (
WITH
  SOURCE_ALL AS (
    SELECT partitionDate dato, nrkUserId, nrkService, userOrProfile, loyaltyGroup.groupName loyaltyGroup,
           IF(userOrProfile != 'user',
              CASE
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear IS NULL THEN NULL
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 13 THEN CAST(EXTRACT(YEAR FROM partitionDate) - birthYear AS STRING)
                ELSE '13'
              END,
              CASE
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear IS NULL THEN NULL
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 0 THEN '-1'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 13 THEN '0-12'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 20 THEN '13-19'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 30 THEN '20-29'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 50 THEN '30-49'
                WHEN EXTRACT(YEAR FROM partitionDate) - birthYear < 70 THEN '50-69'
                ELSE '70+'
              END) aldersgruppe,
           CASE
             WHEN last28Days.daysVisited = 0 THEN 0
             WHEN last28Days.daysVisited < 4 THEN 3
             WHEN last28Days.daysVisited < 7 THEN 6
             WHEN last28Days.daysVisited < 11 THEN 10
             WHEN last28Days.daysVisited < 16 THEN 15
             WHEN last28Days.daysVisited < 22 THEN 21
             WHEN last28Days.daysVisited < 29 THEN 28
           END aktive_dager,
      FROM `nrk-datahub.snowplow_aggregate.rfv_v01`
           JOIN `nrk-datahub.prod.registered_users_v01` USING(nrkUserId)
     WHERE partitionDate IN UNNEST(GENERATE_DATE_ARRAY('2025-01-22' - 28*12, '2025-01-22', INTERVAL 28 day))
       AND nrkService IN ('nrkno', 'nrkradio', 'nrktv', 'nrksuper')
     ORDER BY nrkUserId, dato),

  FORRIGE_PERIODE28 AS (
    SELECT sa.*,
           sa_fp.loyaltyGroup loyaltyGroup_fp, sa_fp.aktive_dager aktive_dager_fp, --sa_fp.ny_bruker ny_bruker_fp,
           sa_ffp.loyaltyGroup loyaltyGroup_ffp, sa_ffp.aktive_dager aktive_dager_ffp,-- sa_ffp.ny_bruker ny_bruker_ffp
      FROM SOURCE_ALL sa
           LEFT JOIN SOURCE_ALL sa_fp ON sa.dato = sa_fp.dato + 28 AND sa.nrkUserId = sa_fp.nrkUserId AND sa.nrkService = sa_fp.nrkService
           LEFT JOIN SOURCE_ALL sa_ffp ON sa.dato = sa_ffp.dato + 28 * 2 AND sa.nrkUserId = sa_ffp.nrkUserId AND sa.nrkService = sa_ffp.nrkService)

  SELECT dato, userOrProfile, aldersgruppe, nrkService,
         loyaltyGroup,
         loyaltyGroup_fp,
         aktive_dager,
         aktive_dager_fp,
         CASE
           WHEN IFNULL(aktive_dager_ffp, 0) > 0 AND IFNULL(aktive_dager_fp, 0) > 0 THEN 'tilbakevendende'
           WHEN IFNULL(aktive_dager_ffp, 0) > 0 AND NOT IFNULL(aktive_dager_fp, 0) > 0 THEN 'mistede'
           WHEN NOT IFNULL(aktive_dager_ffp, 0) > 0 AND IFNULL(aktive_dager_fp, 0) > 0 THEN 'reaktiverte'
           ELSE 'dvale'
         END Brukstype_fp,
         CASE
           WHEN IFNULL(aktive_dager_fp, 0) > 0 AND IFNULL(aktive_dager, 0) > 0 THEN 'tilbakevendende'
           WHEN IFNULL(aktive_dager_fp, 0) > 0 AND NOT IFNULL(aktive_dager, 0) > 0 THEN 'mistede'
           WHEN NOT IFNULL(aktive_dager_fp, 0) > 0 AND IFNULL(aktive_dager, 0) > 0 THEN 'reaktiverte'
           ELSE 'dvale'
         END Brukstype,
         COUNT(nrkUserId) brukere
    FROM FORRIGE_PERIODE28
   GROUP BY ALL)

Oversikt over utgravingsområdet

La oss se hvordan hvordan kombinasjoner av brukstype siste og nest siste periode ser ut.

#standardSQL
WITH
  A AS (
    SELECT dato, Brukstype_fp, Brukstype, SUM(brukere) Brukere
      FROM `nrk-scratchbook.Emil.TLGtall_tot`
     GROUP BY ALL)

  SELECT *
    FROM A
   WHERE dato = '2024-12-25'
   ORDER BY 1, 2
datoBrukstype_fpBrukstypeBrukere
2 024-12-25dvalereaktiverte132 385
2 024-12-25dvalenye brukere41 071
2 024-12-25dvaledvale18 501
2 024-12-25mistedereaktiverte85 071
2 024-12-25mistededvale103 604
2 024-12-25nye brukerereaktiverte128
2 024-12-25nye brukeredvale11
2 024-12-25nye brukeretilbakevendende35 212
2 024-12-25nye brukeremistede14 965
2 024-12-25reaktivertetilbakevendende193 173
2 024-12-25reaktivertemistede93 507
2 024-12-25tilbakevendendetilbakevendende1 574 456
2 024-12-25tilbakevendendemistede106 886

Hva betyr disse?

dvale
betyr at brukeren ikke har besøkt NRK siste eller nest siste periode.
mistede
er brukere som besøkte NRK nest site periode, men ikke siste periode.
nye brukere
er brukere som har registrert seg siste periode.
reaktiverte
er brukere som ikke besøkte NRK nest siste periode, men besøkte oss siste periode.
tilbakevendende
er brukere som besøkte oss både nest siste og siste periode.

Vi er her interessert i å finne ut om flere brukere besøker NRK i desember og ikke kommer tilbake i januar enn andre måneder. Vi er dermed ute etter brukere som ble reaktivert eller opprettet i desember og mistet i januar. Så vi finner også snittet for fireukersperiodene vi har data for.

Det første jordlaget

Vi begynner med å se på hvor stor andel av brukerne som bare har besøkt NRK i desember, og sammenligner med de andre månedene.

#standardSQL
WITH
  GL AS (
    SELECT userOrProfile, dato, 
           SUM(IF((Brukstype_fp = 'reaktiverte' OR Brukstype_fp = 'nye brukere')
                  AND (Brukstype = 'mistede'), brukere, 0)) `Kommer og drar`,
           SUM(IF(Brukstype_fp != 'dvale' OR Brukstype_fp != 'mistede', brukere, 0)) `Tot brukere`,
      FROM `nrk-scratchbook.Emil.TLGtall_tot`
     GROUP BY ALL),

  UR AS (
  SELECT CONCAT(FORMAT_DATE('%d. %h', LAG(dato, 2) OVER(PARTITION BY userOrProfile ORDER BY dato)), ' - ',
                FORMAT_DATE('%d. %h', LAG(dato) OVER(PARTITION BY userOrProfile ORDER BY dato))) periode,
         *,
         ROUND(`Kommer og drar` / `Tot brukere`, 3) Andel
    FROM GL)

  SELECT * EXCEPT(dato)
    FROM UR
   WHERE periode IS NOT NULL
   ORDER BY 2, dato
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import pandas_gbq
df = pandas_gbq.read_gbq(spørring, dialect = "standard")
df.periode = df.periode.str.replace("-", "-\n")
benevning = ["Andel", "Antall"]
tittel = " brukere som kommer og går i samme periode"
x_lapp = "Periode"
y_lapp = " brukere"

fig,ax = plt.subplots(2, 1, figsize = (11,7), layout="constrained")
ax[0].plot(df.loc[df.userOrProfile=="profile"].periode,
           df.loc[df.userOrProfile=="profile"].Andel,label="profile")
ax[0].plot(df.loc[df.userOrProfile=="user"].periode,
           df.loc[df.userOrProfile=="user"].Andel,label="user")

ax[1].plot(df.loc[df.userOrProfile=="profile"].periode,
           df.loc[df.userOrProfile=="profile", "Kommer og drar"], label="profile")
ax[1].plot(df.loc[df.userOrProfile=="user"].periode,
           df.loc[df.userOrProfile=="user", "Kommer og drar"], label="user")

ax[0].legend(loc="upper left")
for i in range(2):
    ax[i].set_title(benevning[i]+tittel)
    ax[i].set_xlabel(x_lapp)
    ax[i].set_ylabel(benevning[i]+y_lapp)
fig

figurer/profiler.png

Det er bare 7,6 % av barneprofilene og 2,8 % av voksenprofilene som kommer til oss og forlater oss i desember.

Lojalitet

Hvordan ser dette ut per lojalitetsgruppe? Blanke verdier er brukere som ikke har fått noen gruppe ennå.

#standardSQL
WITH
  GL AS (
    SELECT userOrProfile, dato, loyaltyGroup_fp lojalitet,
           SUM(IF((Brukstype_fp = 'reaktiverte' OR Brukstype_fp = 'nye brukere')
                  AND (Brukstype = 'mistede'), brukere, 0)) `Kommer og drar`,
           SUM(IF(Brukstype_fp != 'dvale' OR Brukstype_fp != 'mistede', brukere, 0)) `Tot brukere`,
      FROM `nrk-scratchbook.Emil.TLGtall_tot`
     GROUP BY ALL),

  UR AS (
  SELECT CONCAT(FORMAT_DATE('%d. %h', LAG(dato,2) OVER(PARTITION BY userOrProfile, lojalitet ORDER BY dato)), ' - ',
                FORMAT_DATE('%d. %h', LAG(dato) OVER(PARTITION BY userOrProfile, lojalitet ORDER BY dato))) periode,
         *,
         ROUND(`Kommer og drar` / `Tot brukere`, 3) Andel
    FROM GL)

  SELECT * EXCEPT(dato)
    FROM UR
   WHERE periode IS NOT NULL
   ORDER BY 2, dato, 3
df = pandas_gbq.read_gbq(spørring, dialect = "standard")
df.periode = df.periode.str.replace("-", "-\n")
df = df.loc[~df.lojalitet.isna()]
fig,ax = plt.subplots(2, 1, figsize = (11,7), layout="constrained")
for p, l in  [[p,l] for l in df.lojalitet.unique()[::-1] for p in df.userOrProfile.unique()]:   
    ax[0].plot(df.loc[(df.userOrProfile==p) & (df.lojalitet==l)].periode,
               df.loc[(df.userOrProfile==p) & (df.lojalitet==l)].Andel, label=p+": "+l)
    ax[1].plot(df.loc[(df.userOrProfile==p) & (df.lojalitet==l)].periode,
               df.loc[(df.userOrProfile==p) & (df.lojalitet==l), "Kommer og drar"], label=p+": "+l)

ax[0].legend(loc="upper left")
for i in range(2):
    ax[i].set_title(benevning[i]+tittel)
    ax[i].set_xlabel(x_lapp)
    ax[i].set_ylabel(benevning[i]+y_lapp)

fig

figurer/profiler_og_lojalitet.png

Ikke overraskende er det flest antall brukerne som er minst lojale som kommer og går i desember, men dette gjelder kun barneprofilene. Blant voksenprofilene er andelen som kommer og går i desember mindre enn andre måneder. Det er også blant disse brukerne andelen som kommer og går i desember er størst.

Tjeneste

Nå gjør vi samme øvelse per tjeneste:

#standardSQL
WITH
  GL AS (
    SELECT userOrProfile, dato, nrkService tjeneste, 
           SUM(IF(Brukstype_fp = 'reaktiverte'
                  AND Brukstype = 'mistede', brukere, 0)) `Kommer og drar`,
           SUM(IF(Brukstype_fp IN ('reaktiverte', 'tilbakevendende'), brukere, 0)) `Tot brukere`,
      FROM `nrk-scratchbook.Emil.TLGtall`
     GROUP BY ALL),

  UR AS (
  SELECT CONCAT(FORMAT_DATE('%d. %h', LAG(dato,2) OVER(PARTITION BY userOrProfile, tjeneste ORDER BY dato)), ' - ',
                FORMAT_DATE('%d. %h', LAG(dato) OVER(PARTITION BY userOrProfile, tjeneste ORDER BY dato))) periode,
         *,
         ROUND(`Kommer og drar` / `Tot brukere`, 3) Andel
    FROM GL
   WHERE `Kommer og drar` > 0)

  SELECT * EXCEPT(dato)
    FROM UR
   WHERE periode IS NOT NULL
     AND `Kommer og drar` > 1
   ORDER BY 2, dato, 3
df = pandas_gbq.read_gbq(spørring, dialect = "standard")
df.periode = df.periode.str.replace("-", "-\n")
fig,ax = plt.subplots(2, 1, figsize = (11,7), layout="constrained")
for p, l in  [[p,l] for l in df.tjeneste.unique()[::-1] for p in df.userOrProfile.unique()]:   
    ax[0].plot(df.loc[(df.userOrProfile==p) & (df.tjeneste==l)].periode,
               df.loc[(df.userOrProfile==p) & (df.tjeneste==l)].Andel, label=p+": "+l)
    ax[1].plot(df.loc[(df.userOrProfile==p) & (df.tjeneste==l)].periode,
               df.loc[(df.userOrProfile==p) & (df.tjeneste==l), "Kommer og drar"], label=p+": "+l)

ax[0].legend(loc="upper left")
for i in range(2):
    ax[i].set_title(benevning[i]+tittel)
    ax[i].set_xlabel(x_lapp)
    ax[i].set_ylabel(benevning[i]+y_lapp)

fig

figurer/profiler_og_tjeneste.png

Aldersgrupper

Nå skal vi gjøre samme øvelse for voksenprofiler fordelt på alder

#standardSQL
WITH
  GL AS (
    SELECT dato, aldersgruppe, userOrProfile,
           SUM(IF((Brukstype_fp = 'reaktiverte' OR Brukstype_fp = 'nye brukere')
                  AND (Brukstype = 'mistede'), brukere, 0)) `Kommer og drar`,
           SUM(IF(Brukstype_fp != 'dvale' OR Brukstype_fp != 'mistede', brukere, 0)) `Tot brukere`,
      FROM `nrk-scratchbook.Emil.TLGtall_tot`
     GROUP BY ALL),

  UR AS (
    SELECT CONCAT(FORMAT_DATE('%d. %h', LAG(dato, 2) OVER(PARTITION BY aldersgruppe ORDER BY dato)), ' - ',
                  FORMAT_DATE('%d. %h', LAG(dato) OVER(PARTITION BY aldersgruppe ORDER BY dato))) periode,
           *,
           ROUND(`Kommer og drar` / `Tot brukere`, 3) Andel
      FROM GL
     WHERE userOrProfile = 'user')

  SELECT * EXCEPT(dato, userOrProfile)
    FROM UR
   WHERE periode IS NOT NULL
     AND NOT (aldersgruppe = '-1' OR aldersgruppe IS NULL)
   ORDER BY 2, dato, 3
df = pandas_gbq.read_gbq(spørring, dialect = "standard")
df.periode = df.periode.str.replace("-", "-\n")
fig,ax = plt.subplots(2, 1, figsize = (11,7), layout="constrained")
for a in df.aldersgruppe.unique():
    ax[0].plot(df.loc[df.aldersgruppe == a].periode, df.loc[df.aldersgruppe == a].Andel, label=a)
    ax[1].plot(df.loc[df.aldersgruppe == a].periode, df.loc[df.aldersgruppe == a, "Kommer og drar"], label=a)

ax[0].legend(loc="upper left")
for i in range(2):
    ax[i].set_title(benevning[i]+tittel)
    ax[i].set_xlabel(x_lapp)
    ax[i].set_ylabel(benevning[i]+y_lapp)
fig

Hvem var brukerne som kom og dro i desember?

Dette har Elise laget en PowerBi-rapport på.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published