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?
Her skal vi få noen harde fakta på bordet. Vi undersøker derfor hvor mange brukere som brukte oss kun i desember.
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.
Det ukentlige gjennomtrekket har heller ikke endret seg vesentlig gjennom året.
Allerede her tenker vi at det kanskje ikke er så ille som den allminnelige oppfatningen er.
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)
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
dato | Brukstype_fp | Brukstype | Brukere |
---|---|---|---|
2 024-12-25 | dvale | reaktiverte | 132 385 |
2 024-12-25 | dvale | nye brukere | 41 071 |
2 024-12-25 | dvale | dvale | 18 501 |
2 024-12-25 | mistede | reaktiverte | 85 071 |
2 024-12-25 | mistede | dvale | 103 604 |
2 024-12-25 | nye brukere | reaktiverte | 128 |
2 024-12-25 | nye brukere | dvale | 11 |
2 024-12-25 | nye brukere | tilbakevendende | 35 212 |
2 024-12-25 | nye brukere | mistede | 14 965 |
2 024-12-25 | reaktiverte | tilbakevendende | 193 173 |
2 024-12-25 | reaktiverte | mistede | 93 507 |
2 024-12-25 | tilbakevendende | tilbakevendende | 1 574 456 |
2 024-12-25 | tilbakevendende | mistede | 106 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.
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
Det er bare 7,6 % av barneprofilene og 2,8 % av voksenprofilene som kommer til oss og forlater oss i desember.
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
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.
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
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
Dette har Elise laget en PowerBi-rapport på.