Reddit je jedna z nejpopulárnějších sociálních sítí na světě. Skládá se z mnoha (doslova tisíců) různých subredditů, tedy fór zaměřených na konkrétní témata. Členové subredditů do nich posílají své příspěvky, komentují příspěvky cizí, a — a to je pro naši analýzu nejdůležitější — navzájem si příspěvky hodnotí pomocí upvotů a downvotů.
V této ministudii prozkoumáváme, zda je možné určit ideální čas pro poslání příspěvku, je-li naším cílem maximalizovat pravděpodobnost získání co největšího počtu upvotů.
Jak to tak bývá, získat data bylo skoro tak složité, jako je analyzovat. Po několika neúspěšných pokusech jsme došli k následujícímu řešení:
Pro všechny měsíce, které nás zajímají, nejdříve musíme ručně stáhnout data z Pushshift archivu. Každý soubor obsahuje všechny příspěvky z Redditu za daný měsíc a po rozbalení má kolem 30GB.
Protože se tyto archivy nevlezou do paměti, je potřeba zpracovávat je řádek po řádku. Naštěstí jsou v json
formátu, můžeme tedy použít krásnou command-line utilitku jq
, pomocí které vyfiltrujeme potřebné subreddity.
cat 2018/05 | jq -c 'select(.subreddit == "de" or ...)' > 2018/limited_05
Výsledkem je několik menších json
souborů — co měsíc, to soubor — ve kterých je na každém řádku jeden záznam. Tyto soubory se již do paměti vlezou, převod z json
do csv
tabulky již proto můžeme provést v R.
Nyní sice máme tabulku s detaily jednotlivých příspěvků, to nám však nestačí. Příspěvky jsou Pushshiftem archivovány vždy na konci měsíce, u mnohých tedy nemáme kompletní informaci o jejich výsledném skóre. Musíme proto využít Reddit API a u každého příspěvku se podle jeho id podívat, jaké skóre má v současnosti.
Protože v R jsme žádný pěkný wrapper Reddit API nenašli, v této části jsme použili Pythonovskou knihovnu praw. Funkce stahující současná skóre příspěvků z konkrétního subredditu vypadá nějak takto:
def get_post_scores(inpath, outpath):
df = pd.read_csv(inpath)
df.assign(submission_object=pd.Series(reddit.info(list("t3_" + df["id"]))))
# Když už děláme dotazy na API, uložíme i jiné detaily, kdyby náhodou
df = df.assign(
distinguished=
safe_get("submission_object", "distinguished", lambda s: s.distinguished),
num_comments=
safe_get("submission_object", "num_comments", lambda s: s.num_comments),
score=
safe_get("submission_object", "score", lambda s: s.score),
upvote_ratio=
safe_get("submission_object", "upvote_ratio", lambda s: s.upvote_ratio)
)
df = df.drop(columns=["submission_object"])
df.to_csv(outpath)
return df
A máme hotovo! Výsledkem je pro každý subreddit jeden csv
soubor s příspěvky za zkoumané období.
Pozn.: U každého příspěvku známe pouze počet jeho upvotů, tj. jeho konečné reálné skóre, které může být i záporné, neznáme. Přesto budeme “počet upvotů” a “skóre” v následujícím textu brát jako synonyma.
Abychom se z případných vzorců chování mohli dohadovat, co za nimi stojí, potřebujeme znát lokální čas poslání příspevku — co by nám bylo platné vědět, že někdo na r/brasil slavil velký úspěch v 19:00 UTC.
Reddit bohužel u příspěvků lokální čas autora neukládá a neukládá ani jeho polohu či jiné informace, které by mohly být použity k lokalizaci. Ke zkoumání se proto nejvíce hodí subreddity zaobírající se nějakou regionální problematikou a ještě lépe takové z nich, které jsou i v regionálním jazyce (aby nám je Američani ovlivňovali co nejméně).
Ke zkoumání jsme si proto vybrali mnoho regionálních subredditů, které jsme doplnili několika málo globálními, abychom zachovali určitou tematickou pestrost.
Toliko k úvodu. Níže můžete vidět kód, který se zaobírá načítáním jednotlivých souborů a určováním lokálních časů. U regionálních subredditů jsme lokální čas odhadli z toho globálního pomocí časové zóny daného regionu. U zbylých, globálních subredditů bylo určení časové zóny složitější — protože však je převážná většina uživatelů Redditu z USA, použili jsme v těchto případech časovou zónu America/New_York
.
df <-
# Načítání jednotlivých souborů + specifikace časové zóny
list(
# Globální
c(sub = "czech", tz = "Europe/Prague"),
c(sub = "japan", tz = "Japan"),
c(sub = "singapore", tz = "Singapore"),
c(sub = "de", tz = "Europe/Berlin"),
c(sub = "germany", tz = "Europe/Berlin"),
c(sub = "California", tz = "US/Pacific"),
c(sub = "brasil", tz = "America/Sao_Paulo"),
# Lokální
c(sub = "Overwatch", tz = "America/New_York"),
c(sub = "teenagers", tz = "America/New_York"),
c(sub = "gonewild", tz = "America/New_York"),
c(sub = "leagueoflegends", tz = "America/New_York")
) %>%
map(read_scores) %>%
reduce(bind_rows) %>%
filter(!is.na(score)) %>%
# Přidání dne a hodiny, kdy byl příspěvek poslán (v lokálním čase)
group_by(tz) %>%
mutate(
created_local = with_tz(created_utc, tzone = first(tz)),
wday_local =
factor(
wday(created_local, label = TRUE),
levels = as.character(wday(c(2:7, 1), label = TRUE))
),
hour_local = hour(created_local),
month_local = month(created_local),
upvotes = score,
) %>%
ungroup() %>%
select(-score, -created_local) %>%
# Seřazení subredditů podle velikosti
mutate(
subreddit =
factor(
subreddit,
levels = subreddit %>% table() %>% sort(decreasing = TRUE) %>% names()
)
)
df
## # A tibble: 891,507 x 24
## id title created_utc retrieved_on over_18 pinned stickied
## <chr> <chr> <dttm> <dttm> <lgl> <lgl> <lgl>
## 1 7nci… "Wha… 2018-01-01 01:51:54 2018-02-13 00:07:41 FALSE FALSE FALSE
## 2 7ndv… "Pro… 2018-01-01 07:03:11 2018-02-13 00:19:00 FALSE FALSE FALSE
## 3 7nii… "Ple… 2018-01-01 23:36:29 2018-02-13 00:58:05 FALSE FALSE FALSE
## 4 7np8… "Wha… 2018-01-02 20:41:48 2018-02-13 01:54:48 FALSE FALSE FALSE
## 5 7npl… "Nee… 2018-01-02 21:35:13 2018-02-13 01:58:03 FALSE FALSE FALSE
## 6 7nqm… "Hel… 2018-01-03 00:05:43 2018-02-13 02:06:43 FALSE FALSE FALSE
## 7 7nvk… "Hi … 2018-01-03 16:08:05 2018-02-13 02:48:16 FALSE FALSE FALSE
## 8 7o1u… "IGF… 2018-01-04 09:15:36 2018-02-13 03:41:16 FALSE FALSE FALSE
## 9 7o5c… "Why… 2018-01-04 19:25:36 2018-02-13 04:10:37 FALSE FALSE FALSE
## 10 7o5g… "Why… 2018-01-04 19:39:30 2018-02-13 04:11:23 FALSE FALSE FALSE
## # … with 891,497 more rows, and 17 more variables: url <chr>, subreddit <fct>,
## # subreddit_id <chr>, is_self <lgl>, is_video <lgl>,
## # author_comment_karma <dbl>, author_link_karma <dbl>, author_id <chr>,
## # author_name <chr>, distinguished <chr>, num_comments <dbl>,
## # upvote_ratio <dbl>, tz <chr>, wday_local <ord>, hour_local <int>,
## # month_local <dbl>, upvotes <dbl>
Pro kontrolu můžeme u každého ze subredditů vizualizovat počet příspěvků v závislosti na dni v týdnu a hodině. Jak je vidět, všechny křivky mají podobný posun, což naznačuje, že jsme ve většině případů časovou zónu odhadli správně. Je ale dobré pamatovat, že tato čísla — zvláště u globálních subredditů — je nutné brát s velkou rezervou. Pozor také na to, že jsme pro různé subreddity použili různé škálování osy \(y\).
Pozn.: Subreddity jsou na tomto grafu i na dalších grafech seřazeny podle celkového počtu příspěvků (r/gonewild nejvíce, r/czech nejméně).
df %>%
ggplot(aes(hour_local)) +
geom_bar(aes(fill = wday_local), position = "dodge") +
facet_grid(rows = vars(subreddit), scales = "free_y") +
labs(
title = "Počet příspěvků v závislosti na dni v týdnu a hodině",
x = "Hodina (lokální čas)",
y = "Počet příspěvků",
fill = "Den v týdnu (lokální čas)"
)
Pro úplnost uvádíme průměrný počet příspěvků v subredditu za měsíc.
df %>%
group_by(subreddit) %>%
count(month_local) %>%
summarise(mean_monhtly_posts = mean(n))
## # A tibble: 11 x 2
## subreddit mean_monhtly_posts
## <fct> <dbl>
## 1 gonewild 39413.
## 2 teenagers 26296.
## 3 leagueoflegends 25507.
## 4 Overwatch 22352.
## 5 de 4827
## 6 brasil 4556
## 7 singapore 2083
## 8 germany 771.
## 9 japan 733.
## 10 California 661.
## 11 czech 184.
Koněčně se dostáváme k samotné analýze. Naší první \(H_0\) bude “průměrné skóre příspěvku není závislé na dni v týdnu, kdy byl poslán”, což v našem případě znamená, že průměrné skóre příspěvků z konkrétního dne se nebude zvlášť lišit od očekávaného průměrného skóre, tj. čísla
\[\mu_r = \frac{\sum_{p \in r} \text{skóre příspěvku } p}{\text{počet příspěvků v } r},\] kde \(r\) slouží k parametrizaci zkoumaného subredditu.
Tuto odlišnost a její signifikanci budeme určovat pomocí \(t\)-testu. Funkce run_t_test
níže nejprve spočítá všechna \(\mu_r\) a poté určí \(t\)-hodnoty a \(p\)-hodnoty průměrného skóre u skupin, které jsou dány parametrem grouping
. U \(H_0\) výše bude grouping
nastaven na den v týdnu.
run_t_test <- function(df, grouping) {
# Párování subreddit — průměrný počet upvotů
upvotes_mean_df <-
df %>%
group_by(subreddit) %>%
summarise(
upvotes_sd = sd(upvotes),
upvotes_mean = mean(upvotes)
)
df %>%
group_by(subreddit, {{ grouping }}) %>%
left_join(upvotes_mean_df, by = c(subreddit = "subreddit")) %>%
summarise(
# Má vzorek upvotes statisticky významně odlišný průměr od mu?
t_test =
list(t.test(
upvotes,
mu = first(upvotes_mean)
)),
p_value = t_test %>% map_dbl(~ .x$p.value),
t_estimate = t_test %>% map_dbl(~ .x$estimate),
t_null = t_test %>% map_dbl(~ .x$null.value),
t_value = t_test %>% map_dbl(~ .x$statistic),
upvotes_mean = first(upvotes_mean)
) %>%
ungroup()
}
Abychom se nemuseli pořád jen koukat na čísla, výsledky vizualizujeme; modře vyznačíme dny (popř. časy, či jiné skupiny), u nichž bývá skóre nadprůměrné, červeně ty, kde je tomu naopak. O dnech (skupinách), kde je \(p\)-hodnota menší než stanovená hladina význmanosti \(\alpha\), nemůžeme s jistotou říci nic — změníme tedy jejich hodnotu na neutrální nulu.
plot_matrix <- function(df, grouping, alpha = 0.05) {
df %>%
mutate(difference = if_else(p_value <= alpha, t_estimate - t_null, 0)) %>%
ggplot(aes({{ grouping }}, subreddit, fill = difference)) +
geom_tile() +
scale_fill_gradient2()
}
Nyní se již dostáváme k samotné vizualizaci výsledků. Použijeme \(\alpha = 0.05\) — jinými slovy, jsme ochotni riskovat, že v pěti procentech případů uděláme chybu prvního druhu.
df %>%
run_t_test(wday_local) %>%
plot_matrix(wday_local) +
labs(
title = "Ve které dny se (ne)vyplatí posílat příspěvky?",
subtitle = "p-hodnota < 0.05",
x = "Den",
y = "Subreddit",
fill = "Δ skóre"
)
Náš test v datech nenalezl skoro nic zajímavého, není ale nutno věšet hlavu. Můžeme se podívat, jak to vypadá s výhodností časů během dne — \(H_0\) je “průměrné skóre příspěvku není závislé na hodině, kdy byl poslán”. Zde informací vytěžíme podstatně více.
df %>%
run_t_test(hour_local) %>%
plot_matrix(hour_local) +
labs(
title = "Ve které hodiny se (ne)vyplatí posílat příspěvky?",
subtitle = "p-hodnota < 0.05",
x = "Hodina",
y = "Subreddit",
fill = "Δ skóre"
)
O závislosti skóre na dnech jsme zjistili až překvapivě málo — zdá se, že jsou tyto závislosti slabší, než jsme předpokládali, a bylo by proto vhodné použít citlivější metody, popřípadě najít vhodnější a bohatší data. Závislost skóre na času dopadla podstatně lépe, přesto je však vidět, že i zde by se hodilo mít více dat, zvláště pro menší subreddity. Získat tato naštěstí data nebude problém, všechna jsou uložena v Pushishift archivu.
Ty nejúspěšnější časy jsou mezi šestou a devátou hodinou ranní, kdy se nejpíše většina uživatelů Redditu probouzí a cestuje do práce či do školy; tento jev je zvláště markantní u r/teenagers. Je však pravděpodobné, že úspěšnost těchto časů závisí i na dalších faktorech.
Zdá se nám totiž, že úspěšnost příspěvku v závislosti na čase nepřímoúměrně odpovídá počtu příspěvků, které jsou v tento čas na subreddit poslány. Dá se předpokládat, že v časy, kdy je na subredditu málo příspěvků, je jednoduché dostat se na subredditovou frontpage; díky toho si pak příspěvku všimne více uživatelů, kteří jej pozitivně ohodnotí a spustí tak smyčku pozitivní zpětné vazby.
Bylo by zajímavé zkusit tento efekt pozitivní zpětné vazby odfiltrovat a zjistit, zda role času souvisí i s něčím jiným než jen s počtem uživatelů (a cestou do práce) — například zda existuje nějaká vazba mezi preferovanými časy a tématy.
Pro zajímavost nyní přikládáme vizualizace bez jakéhokoli omezení p-hodnoty a snažíme se z nich vyčíst, jakým směrem se ubírat s budoucí analýzou.
df %>%
run_t_test(wday_local) %>%
plot_matrix(wday_local, 1) +
labs(
title = "Ve které dny se (ne)vyplatí posílat příspěvky?",
subtitle = "p-hodnota neomezená",
x = "Den",
y = "Subreddit",
fill = "Δ skóre"
)
Přestože tomuto obrázků již z podstaty nemůžeme věřit, zdá se, že víkendy by mohly mít na úspěšnost příspěvku vliv. Zvláště zajímavý je potenciální rozdíl mezi r/gonewild a zbytkem subredditů. Věříme, že přidáním dalších dat by se nám podařilo tento signál zaznamenat i pomocí jednoduchého \(t\)-testu.
U seskupování podle časů bychom si s více daty zase mohli doplnit obrázek, který se rýsoval už na grafech výše:
df %>%
run_t_test(hour_local) %>%
plot_matrix(hour_local, 1) +
labs(
title = "Ve které hodiny se (ne)vyplatí posílat příspěvky?",
subtitle = "p-hodnota neomezená",
x = "Hodina",
y = "Subreddit",
fill = "Δ skóre"
)
A konečně, kdybychom měli více dat, dávalo by rovněž smysl podívat se na závislost skóre na dni a času.
Nejsme si ale jistí, zda jsme vůbec schopni nashromáždit tolik dat, aby tato 2D analýza dávala smysl. V takovém případě bychom mohli rozdělit den na méně než 24 dílů, například na ráno, poledne, večer, a noc, a tím tak nároky na data omezit. Riskovali bychom však, že do analýzy zaneseme své biasy.