Jak Onet pracuje na Facebooku?

W poprzednim poście zrobiłem analizę postów publikowanych przez polskie redakcje informacyjne na Facebooku. Zabrakło tam redakcji z grupy Ringier Axel Springer Polska. W związku z licznymi prośbami - dzisiaj pogłębiona analiza postów Onetu. Zajmiemy się fanpage’em “głównym” i ostatnimi dwoma tysiącami postów.

fb_page_posts <- get_fanpage_posts("Onet", 2000, token)

Skorzystałem z funkcji get_fanpage_posts() napisanej w poprzednim poście, więc zainteresowanych tam odsyłam po szczegóły techniczne. Zobaczmy jakiego typu posty publikuje Onet:

fb_page_posts %>%
   count(type) %>%
   mutate(p=100*n/sum(n)) %>%
   ungroup() %>%
   arrange(n) %>%
   mutate(type = factor(type, levels=type)) %>%
   ggplot() +
   geom_bar(aes(1, p, fill=type), stat="identity") +
   labs(fill="Typ postu") +
   theme_void() +
   coord_polar(theta="y")

Prawie 77% - linki, 16.75% wideo, 6% zdjęcia. Jak to wygląda ilościowo - liczba postów publikowanych w ciągu dnia w zależności od dnia tygodnia, im bardziej żółte tym więcej:

fb_page_posts %>%
   count(hour, wday) %>%
   ungroup() %>%
   mutate(wday = factor(wday, rev(c("pn","wt","śr","cz","pt","sb","nd")))) %>%
   ggplot() +
   geom_tile(aes(hour, wday, fill=n)) +
   theme_minimal() +
   labs(x="Godzina", y="Dzień tygodnia") +
   scale_fill_gradient(low="#800026", high="#ffffcc") +
   theme(legend.position="none")

O drugiej w nocy to w Onecie się śpi. Automaty publikujące też śpią. Jak wyglądają globalnie interakcje z postami w czasie? Zobaczmy od razu na wygładzonych krzywych:

ggplot(fb_page_posts) +
   geom_smooth(aes(created_time, likes_count),
               color="red", se = FALSE) +
   geom_smooth(aes(created_time, comments_count),
               color="green", se = FALSE) +
   geom_smooth(aes(created_time, shares_count),
               color="blue", se = FALSE) +
   theme_minimal() +
   labs(title="Liczba interakcji na post", x="Czas",
        y="Liczba interakcji na post")

Czerwone - lajki, zielone - komentarze, a niebieskie to “szery”. Jak widać - like dominuje, co jest naturalne. Z ciekawości sprawdźmy jeszcze rozkłady - czy na grubych wartościach przewaga lajków jest widoczne?

summary(fb_page_posts$likes_count)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##     0.0    28.0    79.0   259.1   208.0 15800.0
summary(fb_page_posts$comments_count)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    0.00    1.00    8.00   30.44   28.00  922.00
summary(fb_page_posts$shares_count)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##     0.0     2.0    13.0    77.2    53.0  6387.0

8.5 raza więcej lajków niż komentarzy (średnie 259.1 do 30.44, podobnie w medianach), dwa razy więcej “szerów” niż komentarzy. Ma to sens. Mamy zgrubny obraz działań redakcyjnych, ale są jeszcze użytkownicy, którzy wchodzą w interakcje z postami. Kim są? Co robią? Kiedy to robią? Potrzebujemy danych szczegółowych każdego postu. Przygotujemy odpowiednią funkcję i pobierzemy szczegóły (komentarze i lajki):

get_comments <- function(post_id, token, n_comments=500, n_likes = 500) {

   # zabezpieczenie dla pętli wywołującej funkcję
   # poprawnie te warunki powinny być poza funkcją!
   if(n_comments == 0) n_comments <- 500
   if(n_likes == 0) n_likes <- 500
   
   coms <- getPost(post_id, token=token,
                   n.comments = n_comments,
                   n.likes = n_likes,
                   n = n_comments + n_likes)

   return(coms)
}


# pobierz komentarze i lajki do wszystkich postów
fb_comments <- data.frame()
fb_likes <- data.frame()
for(i in 266:nrow(fb_page_posts)) {
   post_data <- get_comments(fb_page_posts[i, "id"], token, 
                             fb_page_posts[i, "comments_count"],
                             fb_page_posts[i, "likes_count"])
   
   
   fb_comments_tmp <- post_data$comments
   # czy są jakieś komentarze?
   # jeśli tak - dodaj do listy wszystkich
   if(!is.null(fb_comments_tmp))
      if(nrow(fb_comments_tmp) > 0) {
         fb_comments_tmp$post_id <- fb_page_posts[i, "id"]
         fb_comments <- rbind(fb_comments, fb_comments_tmp)
      }
   

   fb_likes_tmp <- post_data$likes
   # czy są jakieś likes?
   # jeśli tak - dodaj do listy wszystkich
   if(!is.null(fb_likes_tmp))
      if(nrow(fb_likes_tmp) > 0) {
         fb_likes_tmp$post_id <- fb_page_posts[i, "id"]
         fb_likes <- rbind(fb_likes, fb_likes_tmp)
   }
}
# data w przyjaznym formacie
fb_comments$created_time = ymd_hms(fb_comments$created_time)

Kod komentuje się sam i specjalnie jest napisany w tak oczywisty sposób ;) Zresztą moje założenie jest takie, że jeśli czytasz tego bloga bo chcesz się nauczyć R to musisz trochę już znać język. A jeśli interesują Cię wyniki analiz - kod nie jest Ci potrzebny. Sprawdźmy co widać w pobranych danych - na przykład kto lajkuje najwięcej?

n_posts <- nrow(fb_page_posts)

fb_likes %>%
   group_by(from_id) %>%
   mutate(n=n()) %>%
   ungroup() %>%
   select(from_id, n) %>%
   unique() %>%
   mutate(p = 100*n/n_posts) %>%
   arrange(desc(n)) %>%
   head(10)
from_idnp
76579264348828796248.10
11736956876568370835.40
162676277421808760330.15
150945499271223757928.95
13440608024566651625.80
149782040712522051125.55
69438115395963150525.25
159205675768367446123.05
27445825297095946123.05
139767888055542339319.65

W tabeli powyżej mogłyby pojawić się nazwiska (wystarczy zamienić from_id na from_name), ale nie chcę tego robić (chociaż jak widać te informacje są publiczne). Lider(ka) dał(a) lajka prawie połowie postów (48%)! No to musi być miłość… Kto napisał najwięcej komentarzy? Znowu - tylko IDki użytkowników, nie nazwiska:

fb_comments %>%
   group_by(from_id) %>%
   mutate(n=n()) %>%
   ungroup() %>%
   select(from_id, n) %>%
   unique() %>%
   mutate(p = 100*n/n_posts) %>%
   arrange(desc(n)) %>%
   head(10)
from_idnp
146698476696001931415.70
5079830793436461095.45
543957139059303964.80
122810131418675874.35
1464805913849186844.20
1030590347068474723.60
115975748747575663.30
769660986403035653.25
528748793932975643.20
219332305072990623.10

Weźmy tego pierwszego (skomentował prawie co szóstego posta!) - czy to komentarze do różnych postów czy do jednego (jakaś dyskusja)?

fb_comments %>%
   filter(from_id =="1466984766960019") %>%
   count(post_id) %>%
   ungroup() %>%
   summary(n)
##    post_id                n        
##  Length:313         Min.   :1.000  
##  Class :character   1st Qu.:1.000  
##  Mode  :character   Median :1.000  
##                     Mean   :1.003  
##                     3rd Qu.:1.000  
##                     Max.   :2.000

Przy takim rozkładzie (średnia nieco ponad 1, trzeci kwartyl równy jeden, maksimum równe dwa) można być praktycznie pewnym, że tylko kilka komentarzy dotyczy tego samego postu. Proszę sprawdzić samodzielnie. A kto udzielał się w dyskusji (czyli napisał dużo komentarzy pod jednym postem)? Jeśli ktoś taki jest to pod jakim postem?

fb_comments %>%
   count(from_id, post_id) %>%
   arrange(desc(n))
from_idpost_idn
1409834469287770185265414715_1015439475494471621
720664394689826185265414715_1015446825117971621
144237015938786185265414715_1015450145103971620
845900115496276185265414715_1015450145103971616
1525684074381514185265414715_1015448744175471615
1659327344324010185265414715_1015447500472471614
843981655682542185265414715_1015450145103971612
422307901239180185265414715_1015447257214971610
627261217384010185265414715_1015443234439471610
630185733702545185265414715_1015439475494471610

Mamy post o ID 185265414715_10154394754944716 i komentującego o ID 1409834469287770 - co to?

fb_comments %>%
   filter(post_id=="185265414715_10154394754944716",
          from_id=="1409834469287770") %>%
   select(created_time, message) %>%
   arrange(created_time)
created_timemessage
2017-02-01 12:54:44
2017-02-01 12:58:16
2017-02-01 12:58:28
2017-02-01 13:07:53
2017-02-01 13:08:03
2017-02-01 13:08:12
2017-02-01 13:08:18
2017-02-01 13:08:25
2017-02-01 13:08:31
2017-02-01 13:08:37
2017-02-01 13:08:43
2017-02-01 13:08:49
2017-02-01 13:08:56
2017-02-01 13:09:02
2017-02-01 13:09:10
2017-02-01 13:09:16
2017-02-01 13:09:22
2017-02-01 13:09:28
2017-02-01 13:09:34
2017-02-01 13:09:42
2017-02-01 13:09:48

Komentarze co 8 sekund, bez żadnej treści słownej - proszę zobaczyć na post i komentarze do niego, wszystko się wyjaśni. W danych można sprawdzić kim jest ten użytkownik (albo po jego ID wklejonym w URLa do Facebooka). Przeanalizowaliśmy dość szczegółowo jeden post. Wróćmy do globalnego przeglądu - ile czasu mija od publikacji postu do komentarza?

# same komentarze i daty
com_times <- fb_comments %>%
   transmute(post_id=post_id, created_time_com=created_time)

post_times <- fb_page_posts %>%
   transmute(post_id=id, created_time_post=created_time)

# złączmy to po ID postu
times <- left_join(com_times, post_times, by="post_id") %>%
   mutate(diff = floor(as.numeric(created_time_com - created_time_post) / 60)) %>%
   filter(diff >= 0)

Prosta różnica czasów daje wynik w sekundach - dlatego dzielimy przez 60 (do minut) i zaokrąglamy w dół do najbliższej całkowitej (floor()). Pierwsza doba wygląda tak (czerwone kreski to kolejne godziny):

times %>% 
   filter(diff <= 24*60) %>%
   ggplot() +
   geom_histogram(aes(diff), bins = 24*60) +
   geom_vline(xintercept = 60*(1:24), color="red") +
   theme_minimal()

Dużo na początku, im później tym mniej. Centyle po godzinach:

quantile(floor(times$diff/60), seq(0, 1, 0.1))
##   0%  10%  20%  30%  40%  50%  60%  70%  80%  90% 100% 
##    0    0    0    1    2    3    6    9   14   22  795

Widać, że połowa komentarzy pojawia się do maksymalnie czterech godzin po publikacji, a w pierwszej dobie mamy ponad 90% komentarzy. W pierwszej godzinie mieści się 30% komentarzy. Zobaczmy jak dokładniej wygląda ta pierwsza godzina:

times %>% 
   filter(diff <= 60) %>%
   ggplot() +
   geom_histogram(aes(diff), bins = 60) +
   scale_x_continuous(breaks = seq(0, 60, 5))

Skąd pik przy 30 minutach? Ten pik w połączeniu z 50% percentylem w okolicach 3 godziny łączyłbym z sposobem sortowania treści na wallu jaki widzimy na Facebooku. Ciekaw jestem (wie to zapewne Facebook) jaki procent ogląda swojego walla z poziomu sortowania “najnowsze najpierw”, a jaki “najpopularniejsze najpierw”. Zgadnijcie, których jest więcej… Czy widać jakieś szczególne pory kiedy pojawiają się komentarze?

fb_comments %>%
   mutate(hour = hour(created_time)) %>%
   count(hour) %>%
   ungroup() %>%
   ggplot() +
   geom_bar(aes(hour, n), stat="identity") +
   theme_minimal() +
   labs(x="Godzina", y="Łączna liczba komentarzy")

To jest na sumach a nie średnich per post czy cokolwiek… Metodologicznie nie za dobrze, ale ważny jest charakter rozkładu (uśredniony będzie podobny - sprawdźcie). Znowu widać rozkład “konsumpcji” internetu w Polsce - do porannej kawy i po południu albo wieczorem. O tym było też poprzednio. Sprawdźmy jeszcze to samo jako mapa cieplna w tygodniu (żółty = bardziej):

fb_comments %>%
   mutate(hour = hour(created_time),
          wday = wday(created_time)) %>%
   mutate(wday = factor(wday,
                        levels=c(2,3,4,5,6,7,1),
                        labels=c("pn","wt","śr","cz","pt","sb","nd"))) %>%
   count(wday, hour) %>%
   ungroup() %>%
   ggplot() +
   geom_tile(aes(hour, wday, fill=n)) +
   theme_minimal() +
   labs(x="Godzina", y="Dzień tygodnia") +
   scale_fill_gradient(low="#800026", high="#ffffcc") +
   theme(legend.position="none")

Weźmy ten najbardziej żółty moment - to jest godzina 18 w piątek. Co to?

fb_comments %>%
   mutate(hour = hour(created_time),
          wday = wday(created_time)) %>%
   mutate(wday = factor(wday,
                        levels=c(2,3,4,5,6,7,1),
                        labels=c("pn","wt","śr","cz","pt","sb","nd"))) %>%
   filter(wday=="pt", hour==18) %>%
   count(post_id) %>%
   ungroup() %>%
   arrange(desc(n))
post_idn
185265414715_10154420101434716155
185265414715_1015449869210971688
185265414715_1015444093359971654
185265414715_1015440182406971647
185265414715_1015447586672971646
185265414715_1015443980559471641
185265414715_1015441980435971630
185265414715_1015439867988971626
185265414715_1015449830315471624
185265414715_180039630361732623

Widzimy, że maksymalna liczba komentarzy pojawiła się pod poste numer 185265414715_10154420101434716 - co to było? Ta dam!

fb_page_posts[fb_page_posts$id=="185265414715_10154420101434716", "message"]
## [1] "Premier trafiła do szpitala"

Na koniec jeszcze zobaczmy na treść postów. Z grubsza, “bez czytania” (uwielbiam to podejście - zobaczcie wyniki Oskarów o ile nie znacie). Zobaczymy jakie słowa są najbardziej popularne w postach jak i w komentarzach. Pojedyncze słowa - zmniejszamy litery, rozdzielamy, liczymy, zostawiamy te co mają co najmniej 3 znaki.

library(tidytext)

words_comments <- fb_comments %>%
   transmute(message = tolower(iconv(message, "UTF-8", "LATIN2"))) %>%
   unnest_tokens(words, message) %>%
   count(words) %>%
   ungroup() %>%
   filter(nchar(words) > 2)
   
words_posts <- fb_page_posts %>%
   transmute(message = tolower(iconv(message, "UTF-8", "LATIN2"))) %>%
   unnest_tokens(words, message) %>%
   count(words) %>%
   ungroup() %>%
   filter(nchar(words) > 2)

W tym momencie warto zerknąć w powstałe tabelki words_posts oraz words_comments. Można pozwolić sobie na jeszcze nieco filtrowania, na pewno polskie stop-words (słownik znalazłem w Google):

pl_stop_words <- readLines("../textmining/polish_stopwords.txt", encoding = "UTF-8")
words_comments <- filter(words_comments, !words %in% pl_stop_words)
words_posts <- filter(words_posts, !words %in% pl_stop_words)

To samo powtarzamy dla tych samych słów pozbawionych polskich literek - słownik jest ten sam, ręcznie edytowany przez “znajdź i zamień” znaki z ogonkami na takie bez ogonków. Bo ludziom się nie chce czasem wcisnąć ALT. Później zaglądamy w dane, szczególnie do words_posts i znajdujemy tam dużą liczbę przy słowie “wiem” - to reklamowy hash tag Onetu, widać go często, także na Twitterze. Usuwamy. Teraz możemy narysować chmurki najpopularniejszych słów. Najpierw potrzebne biblioteki i paleta kolorów, a już za moment wyniki.

library(wordcloud)
library(RColorBrewer)
pal <- brewer.pal(12, "Paired")

Najpopularniejesze słowa w postach:

wordcloud(words_posts$words, words_posts$n,
          scale=c(1.9,.9), colors = pal,
          max.words = 100)

Najpopularniejsze słowa w komentarzach:

wordcloud(words_comments$words, words_comments$n,
          scale=c(1.9,.9), colors = pal,
          max.words = 100)

Co jeszcze można zrobić? Można spróbować na podstawie częstości występowania słów w postach określić “charakteru” postu (sport, polityka - tego trzeba maszynę nauczyć). Później z takim “charakterem” też można się pobawić - kiedy więcej polityki, a kiedy informacje bardziej rozrywkowe? Co przynosi więcej lajków, a co więcej komentarzy? Jakie są to komentarze?