Análise de dados do Youtube

1.Introdução

Abordarei neste tópico uma forma de explorar informações contidas em um determinado canal do Youtube. Como exemplo, estarei demonstrando a obtenção e análise dos dados proveniente do canal “Nerdologia”, dos autores Atila Iamarino e Filipe Figueiredo. Os dados manipulados servem como base para a apresentação gráfica em um dashboard dinâmico, desenvolvido em Shiny (linguagem R). Por hora, proponho a análise quantitativa de métricas como visualizações, likes, dislikes e comentários. Bem como a análise textual das legendas geradas automaticamente pelo sistema do Youtube (quando habilitado pelo(s) autor(es) do canal).

Infelizmente o acesso ao texto completo dos comentários é limitado pela API do Youtube, portanto eles não foram analisados (motivos financeiros). A motivação para este estudo é meramente e unicamente por uma questão de curiosidade e pelo exercício da criatividade. Bem como uma forma divertida de se aperfeiçoar cada vez mais nas ferramentas da linguagem R (programação, métodos de exploração de dados, desenvolvimento de dashboards, etc.).

2.Metodologia

Primeiramente foi realizada a extração dos dados brutos. Para as métricas quantitativas, utilizei a API do Youtube (necessita de cadastro prévio) via pacote tuber do R. As legendas automáticas foram baixadas usando o pacote RCurl juntamente com a ferramenta online diycaptions ( http://diycaptions.com).

Os dados textuais foram processados pelas funções do pacote tm e visualizados usando a ferramenta wordcloud. No caso das métricas quantitativas do canal, os dados foram visualizados pela ferramenta plotly. A visualização foi realizada dentro de um dashboard desenvolvido em shiny.

Carregando as bibliotecas

library(rvest)
library(ggplot2)
library(dplyr)
library(tidyr)
library(lubridate)
library(plotly)
library(tuber)
library(stringr)
library(tm)
library(wordcloud)  
library(RCurl)
library(VennDiagram)
library(kableExtra)

2.1.Extraindo métricas do canal

Primeiramente precisamos fazer a autenticação da nossa conta para poder acessar os dados do Youtube. É preciso realizar um cadastro prévio no site e fazer a requisição de chave de acesso (https://developers.google.com/youtube/v3/).

client_id <- 'xxxxxxxx' 
key <- 'xxxxxxxx'

yt_oauth(client_id, key)
Agora podemos extrair os dados dos canal alvo, usando o ID dele. Caso não saiba onde encontrar esta informação, é aquela série de caracteres na URL (conforme destacado na figura).

all_vid_nerdologia <- get_all_channel_video_stats(channel_id= 'UClu474HMt895mVxZdlIHXEA', mine= FALSE)

Precisamos formatar ele adequadamente, formatando os tipos de variáveis bem como fazendo uma limpeza na variável dos títulos.

# Dates
date_split_list <- strsplit(x= as.character(all_vid_nerdologia$publication_date), split= 'T')
date_split_list <- lapply(date_split_list, function(x){x[1]}) %>% unlist()
all_vid_nerdologia['publication_date'] <- date_split_list
all_vid_nerdologia$publication_date <- as.Date(all_vid_nerdologia$publication_date)

# Variable class
all_vid_nerdologia$viewCount <-  as.numeric(all_vid_nerdologia$viewCount)
all_vid_nerdologia$likeCount <- as.numeric(all_vid_nerdologia$likeCount)
all_vid_nerdologia$dislikeCount <- as.numeric(all_vid_nerdologia$dislikeCount)
all_vid_nerdologia$favoriteCount <- as.numeric(all_vid_nerdologia$favoriteCount)
all_vid_nerdologia$commentCount <- as.numeric(all_vid_nerdologia$commentCount)

# Cleanning data title
all_vid_nerdologia$title <- gsub('\\s\\|\\s.*$', '', all_vid_nerdologia$title)

Top 10 vídeos com mais Likes

all_vid_nerdologia %>%
    arrange(desc(likeCount)) %>%
    head(10) %>%
    select(title, likeCount, dislikeCount, commentCount) %>%
    kable("html") %>% 
    kable_styling("striped", full_width= F) %>%
    column_spec(1:4, bold= T) %>%
    row_spec(1:10, bold= T, color= "white", background= "#3399ff")
title likeCount dislikeCount commentCount
Suicídio 114725 695 5853
Buraco Negro 113815 422 4076
Tempo 104740 303 3456
CAOS E EFEITO BORBOLETA 101124 423 2719
AS MARCAS TE MANIPULAM 101039 305 2441
QUAL O SOCO MAIS FORTE? 101037 595 6967
SER INVISÍVEL É POSSÍVEL? 100276 221 2531
LEVANTE ZUMBI 99738 294 3454
O que somos nós? 98443 308 4006
Mistérios do Fundo do Mar 97193 577 2506

Top 10 vídeos com mais Dislikes

all_vid_nerdologia %>%
    arrange(desc(dislikeCount)) %>%
    head(10) %>%
    select(title, dislikeCount, likeCount, commentCount) %>%
    kable("html") %>% 
    kable_styling("striped", full_width= F) %>%
    column_spec(1:4, bold= T) %>%
    row_spec(1:10, bold= T, color= "white", background= "#ff3333")
title dislikeCount likeCount commentCount
Sexismo 13984 93561 15642
Quem tem mais poder? 4093 91474 14424
Existe cura gay? 2787 83583 6110
Aquecimento Global 1854 49424 3921
Como funciona a Astrologia 1793 74924 5447
Maioridade Penal 1721 68294 3179
Por que o Merthiolate não arde mais? 1657 59323 2362
Motor perpétuo? Carro a água? 1629 52140 3355
Coxinhas vs. Petralhas 1312 59094 3632
Batman vs Superman 1175 66649 2337

Top 10 vídeos com mais comentários

all_vid_nerdologia %>%
    arrange(desc(commentCount)) %>%
    head(10) %>%
    select(title, commentCount, dislikeCount, likeCount) %>%
    kable("html") %>% 
    kable_styling("striped", full_width= F) %>%
    column_spec(1:4, bold= T) %>%
    row_spec(1:10, bold= T, color= "white", background= "#4d9900")
title commentCount dislikeCount likeCount
Sexismo 15642 13984 93561
Quem tem mais poder? 14424 4093 91474
QUAL O SOCO MAIS FORTE? 6967 595 101037
É só uma teoria 6120 690 92609
Existe cura gay? 6110 2787 83583
Suicídio 5853 695 114725
Como funciona a Astrologia 5447 1793 74924
Fomos à Lua? 5132 1120 64036
COMO MATAR O WOLVERINE 4979 488 89501
POSSESSÃO, ABDUÇÃO OU PARALISIA DO SONO? 4901 355 90767

Agora, deixarei salvo o objeto da tabela processada, para carregar dentro do meu dashboard.

save(file= 'all_vid_nerdologia.RData', all_vid_nerdologia)

2.1.Extraindo dados textuais das legendas e formatando

A função do pacote tuber que realiza a extração das legendas não está funcionando mais, por isso eu optei por um modo semi-rudimentar para fazer isso (porque demora um pouco :/). Tive que colocar um delay de 30 segundos em cada requisição pois se formos muito rápido acabamos tendo a conexão interrompinda.

url_request <- paste0('http://diycaptions.com/php/get-automatic-captions-as-txt.php?id=',  all_vid_nerdologia$id, '&language=asr')     

html_page <- list()

for(i in 1:length(url_request) ){
    
    html_page[i] <- getURL(url_request[i])
    
    Sys.sleep(30)
}

save(file= 'html_page.RObj', html_page)

Precisarei da minha lista de texto ordenado pela data quanto for retornar um index do plot

all_vid_nerdologia['index'] <- 1:nrow(all_vid_nerdologia)
all_vid_nerdologia <- all_vid_nerdologia %>% arrange(publication_date)
html_page_text_sorted <- html_page_text[all_vid_nerdologia$index]
save(file='html_page_text_sorted.RData', html_page_text_sorted)

Agora farei o pré-tratamento dos dados textuais e a limpeza propriamente dita

# Removing tags blocks
html_page_text <- lapply(html_page, function(x){
    
    split_1 <- strsplit(x[[1]], '<br><br>')[[1]][2]
    strsplit(split_1[[1]], '\t\t</div>')[[1]][1]
    
})

# Saving object for individual process
# save(file = 'html_page_text.RData', html_page_text)

# Generating corpus text for all avaiable data
text_df_total <- data.frame(doc_id= all_vid_nerdologia$id, text= unlist(html_page_text), stringsAsFactors= FALSE , drop=FALSE)

text_corpus_df <- Corpus(DataframeSource(text_df_total))

text_corpus_df_filtered <- text_corpus_df %>%
    tm_map(stripWhitespace)  %>%    
    tm_map(removePunctuation) %>%                              
    tm_map(removeNumbers)   %>%                               
    tm_map(removeWords, c(stopwords("portuguese"))) %>%  
    tm_map(removeNumbers) %>%  
    tm_map(stripWhitespace) %>%    
    tm_map(content_transformer(tolower))

# Creanting a term matrix
corpus_tf <- TermDocumentMatrix(text_corpus_df_filtered)

corpus_m <- as.matrix(corpus_tf)

corpus_m_sorted <- sort(rowSums(as.matrix(corpus_m)), decreasing= TRUE)

df_total <- data.frame(word= names(corpus_m_sorted), freq= as.numeric(corpus_m_sorted))

df_total$word <- as.character(df_total$word)

df_total <- df_total[which(nchar(df_total$word) > 4), ]

O gráfico de frequência total das palavras também deverá aparecer. Porém, lá na ferramenta irei adicionar mecanismos de controle de filtro.

wordcloud(df_total$word, df_total$freq, min.freq= 10, max.words= 150, random.order= FALSE, rot.per= 0.35, colors=brewer.pal(8, "Dark2"))

Farei também uma word cloud para os títulos dos vídeos postados

text_df_total <- data.frame(doc_id= all_vid_nerdologia$title, text= all_vid_nerdologia$title, stringsAsFactors= FALSE , drop=FALSE)

text_corpus_df <- Corpus(DataframeSource(text_df_total))

text_corpus_df_filtered <- text_corpus_df %>%
    tm_map(stripWhitespace)  %>%    
    tm_map(removePunctuation) %>%                              
    tm_map(removeNumbers)   %>%                               
    tm_map(removeWords, c(stopwords("portuguese"))) %>%  
    tm_map(removeNumbers) %>%  
    tm_map(stripWhitespace) %>%    
    tm_map(content_transformer(tolower))

# Creanting a term matrix
corpus_tf <- TermDocumentMatrix(text_corpus_df_filtered)

corpus_m <- as.matrix(corpus_tf)

corpus_m_sorted <- sort(rowSums(as.matrix(corpus_m)), decreasing= TRUE)

df_total <- data.frame(word= names(corpus_m_sorted), freq= as.numeric(corpus_m_sorted))

df_total$word <- as.character(df_total$word)

df_total_titulo <- df_total[which(nchar(df_total$word) > 2), ]

wordcloud(df_total_titulo$word, df_total_titulo$freq, min.freq= 2, max.words= 100, random.order= FALSE, rot.per= 0.35, colors=brewer.pal(8, "Dark2"))

O gráfico do dashboard deve ser similar a este, porém, responsivo com mais informações e opções de filtro.

all_vid_nerdologia %>%
    gather(key= type_counts, value= counts, 
           c(likeCount, dislikeCount, commentCount, viewCount)) %>%
    filter(str_detect('likeCount|dislikeCount|commentCount', type_counts)) %>%
    ggplot(aes(x= publication_date, y= as.numeric(counts), col= type_counts)) +
    geom_line(size=1, alpha=0.7) +
    geom_point(size=1) +
    scale_x_date() +
    theme_bw() +
    scale_color_manual(values = c('#440154FF', '#39568CFF', '#29AF7FFF'))

Criando um dashboard dinâmico

Para prover uma visualização diferenciada dos dados, optei por criar um dashboard dinâmico, reativo ao cursor do mouse e que também aceita diferentes parâmetros para gerar os resultados. Assim, podemos abranger um pouco mais do nosso aprendizado. :D

No dashboard, os dados da métrica e os dados textuais são carregados pré-processados. No caso, ele irá apenas realizar a apresentação dinâmica dos resultados.

Este é o código, também disponível no github (por favor, não fiquei com raiva pela desorganização).

library(shinydashboard)
library(highcharter)
library(ggplot2)
library(dplyr)
library(plotly)
library(tm)
library(wordcloud)  
library(VennDiagram)
library(kableExtra)

load('all_vid_nerdologia.RData')
load('html_page_text.RData')
load('html_page_text_sorted.RData')

ui <- dashboardPage(
    # Header ----
    dashboardHeader(title = "Youtube metrics"),
    
    # Sidebar content ----
    dashboardSidebar(collapsed = TRUE,
                     sidebarMenu(
                         menuItem("Dashboard", tabName = "dashboard", icon = icon("dashboard"))
                     )
    ),
    
    # Body content ----
    dashboardBody(
        
        includeCSS("www/custom.css"),
        tabItems(
            # First tab content
            tabItem(tabName= "dashboard",
                    
                    fluidRow(
                        column(width= 7,
                               tags$blockquote(("Metrics from video captions")),
                               plotlyOutput(outputId = 'metrics_total_plot', height= '400px')
                               
                        ),
                        column(width= 5,
                               wellPanel(
                                   plotOutput(outputId= 'cursor_wc_plot', height= '400px')
                               )
                        )
                    ),
                    
                    fluidRow(
                        column(3,
                               tags$blockquote(("Top 1 videos")),
                               valueBoxOutput("likebox", width = 15),
                               valueBoxOutput("dislikebox", width = 15),
                               valueBoxOutput("commentbox", width = 15)
                        ),
                        column(4,
                               tags$blockquote("Most frequent words in video titles", align="left"),
                               plotOutput(outputId= 'title_wc_plot', width=500, height=400)
                        ),
                        column(5,
                               tags$blockquote("Estimated number of words spoken in video", align="left"),
                               plotlyOutput(outputId = 'numb_spoken_words', height= '400px')   
                        )
                    )
                    
            )
        )
    )
)

server <- function(input, output){
    
    # Plot wordcloud total ----
    output$metrics_total_plot <- renderPlotly({
        
        all_vid_nerdologia %>%
            arrange(publication_date) %>%
            plot_ly(source = "source") %>%
            add_trace(x = ~publication_date, y = ~viewCount, 
                      name = 'View count', type = 'scatter', 
                      mode ="markers+lines", text = ~paste('Video: ', title),
                      line = list(color = '#440154FF ', width = 2)) %>%
            add_trace(x = ~publication_date, y = ~likeCount, 
                      name = 'Like count', type = 'scatter',
                      mode ="markers+lines", text = ~paste('Video: ', title),
                      line = list(color = '#39568CFF', width = 2)) %>%
            add_trace(x = ~publication_date, y = ~dislikeCount,
                      name = 'Dislike count', type = 'scatter', 
                      mode = "markers+lines", text = ~paste('Video: ', title),
                      line = list(color = '#29AF7FFF', width = 2)) %>%
            add_trace(x = ~publication_date, y = ~commentCount,
                      name = 'Comment count', type = 'scatter', 
                      mode = "markers+lines", text = ~paste('Video: ', title),
                      line = list(color = '#FDE725FF', width = 2)) %>%
            layout(xaxis = list(title = ""),
                   yaxis = list (title = "Metric's count"),
                   font =  list(size = 12),
                   hovermode = 'compare',
                   legend = list(orientation = 'h')) 
    })
    
    # Plot over cursor selected word cloud ----
    output$cursor_wc_plot <- renderPlot({
        
        eventdata <- event_data("plotly_hover", source = "source")
        
        validate(need(!is.null(eventdata), "Pass the mouse over the point :)"))
        
        cursor_id <- as.numeric(eventdata$pointNumber)[1] + 1
        
        all_vid_nerdologia <- all_vid_nerdologia %>%
            arrange(publication_date)
        
        text_df_cursor <- data.frame(doc_id= all_vid_nerdologia$id[ cursor_id ], 
                                     text= unlist(html_page_text_sorted[ cursor_id ]), 
                                     stringsAsFactors= FALSE , drop=FALSE)
        
        cursor_corpus_df <- Corpus(DataframeSource(text_df_cursor))
        
        cursor_corpus_df_filtered <- cursor_corpus_df %>%
            tm_map(stripWhitespace)  %>%    
            tm_map(removePunctuation) %>%                              
            tm_map(removeNumbers)   %>%                               
            tm_map(removeWords, c(stopwords("portuguese"))) %>%  
            tm_map(removeNumbers) %>%  
            tm_map(stripWhitespace) %>%    
            tm_map(content_transformer(tolower))
        
        # Creanting a term matrix
        corpus_tf <- TermDocumentMatrix(cursor_corpus_df_filtered)
        
        cursor_corpus_m <- as.matrix(corpus_tf)
        
        cursor_corpus_m_sorted <- sort(rowSums(as.matrix(cursor_corpus_m)), decreasing= TRUE)
        
        df_cursor_final <- data.frame(word= names(cursor_corpus_m_sorted), freq= as.numeric(cursor_corpus_m_sorted))
        
        df_cursor_final$word <- as.character(df_cursor_final$word)
        
        df_cursor_final <- df_cursor_final[which(nchar(df_cursor_final$word) > 3), ]
        
        wordcloud(df_cursor_final$word, df_cursor_final$freq, min.freq = 2, max.words= 50,
                  random.order= FALSE, colors=brewer.pal(8, "Dark2"), scale=c(4, 0.5))
    })
    
    # Metrics top videos box ----
    # Like box
    output$likebox <- renderValueBox({
        toplike <- all_vid_nerdologia %>%
            arrange(desc(likeCount)) %>%
            head(10)
        
        valueBox(value = tags$p(toplike$title[1], style = "font-size: 80%;"),
                 subtitle = paste0("Most liked video with: ", toplike$likeCount[1], " likes"), 
                 icon = icon("thumbs-up", lib = "glyphicon"),
                 color = "blue" 
        )
    })
    
    # Dislike box
    output$dislikebox <- renderValueBox({
        topdislike <- all_vid_nerdologia %>%
            arrange(desc(dislikeCount)) %>%
            head(10)
        
        valueBox(value = tags$p(topdislike$title[1], style = "font-size: 80%;"),
                 subtitle = paste0("Most disliked video with: ", topdislike$dislikeCount[1], " dislikes"),
                 icon = icon("thumbs-down", lib = "glyphicon"),
                 color = "red" 
        )
    })
    
    # Comments box
    output$commentbox <- renderValueBox({
        topcomment <- all_vid_nerdologia %>%
            arrange(desc(commentCount)) %>%
            head(10)
        
        valueBox(value = tags$p(topcomment$title[1], style = "font-size: 80%;"),
                 subtitle = paste0("Most Commented video with: ", topcomment$commentCount[1], " comments"),
                 icon = icon("comment", lib = "font-awesome"),
                 color = "orange" 
        )
    })
    
    # Word cloud topic's titles plot ----
    output$title_wc_plot <- renderPlot({
        
        title_df <- data.frame(doc_id= all_vid_nerdologia$title, text= all_vid_nerdologia$title, stringsAsFactors= FALSE , drop=FALSE)
        
        title_corpus <- Corpus(DataframeSource(title_df))
        
        title_corpus_filtered <- title_corpus %>%
            tm_map(stripWhitespace)  %>%    
            tm_map(removePunctuation) %>%                              
            tm_map(removeNumbers)   %>%                               
            tm_map(removeWords, c(stopwords("portuguese"))) %>%  
            tm_map(removeNumbers) %>%  
            tm_map(stripWhitespace) %>%    
            tm_map(content_transformer(tolower))
        
        # Creanting a term matrix
        corpus_tf <- TermDocumentMatrix(title_corpus_filtered)
        
        corpus_m <- as.matrix(corpus_tf)
        
        corpus_m_sorted <- sort(rowSums(as.matrix(corpus_m)), decreasing= TRUE)
        
        df_total <- data.frame(word= names(corpus_m_sorted), freq= as.numeric(corpus_m_sorted))
        
        df_total$word <- as.character(df_total$word)
        
        df_total_titulo <- df_total[which(nchar(df_total$word) > 2), ]
        
        wordcloud(df_total_titulo$word, df_total_titulo$freq, min.freq= 2,
                  max.words= 100, random.order= FALSE,
                  rot.per= 0.15, colors=brewer.pal(8, "Dark2"),
                  scale=c(5, .7))
        
    })
    
    # Plot number of spoken words
    output$numb_spoken_words <- renderPlotly({
        all_vid_nerdologia %>%
            arrange(publication_date) %>%
            mutate(n_words_spoken =  str_count(html_page_text_sorted, "\\S+")) %>%
            plot_ly() %>%
            add_trace(x = ~publication_date, y = ~n_words_spoken, 
                      name = 'Number of words spoken during video recording', type = 'scatter', 
                      mode = "markers+lines", text = ~paste('Video: ', title, '\n', 'Number of words: ', n_words_spoken),
                      line = list(color = '#004080', width = 4)) %>%
            layout(xaxis = list(title = "Publication date"),
                   yaxis = list (title = "Number of words"),
                   font =  list(size = 14),
                   hovermode = 'compare',
                   legend = list(orientation = 'h')) 
    })
    
}

shinyApp(ui, server)

Este é o resultado:

Em (I) temos um gráfico de linha que segue de acordo com o tempo do início até o presente momento do canal. Cada ponto se trata de um vídeo sendo o gráfico responsivo ao cursor do mouse, também com a legenda clicável. Toda vez que o usuário passar o cursor sobre um ponto, automaticamente será gerado um wordcloud (II) deste vídeo (no caso, o que o Youtuber falou). Em (III) temos os Top vídeos mais curtidos, odiados e comentados. O wordcloud em (IV) foi gerado a partir dos títulos dos vídeos do canal. Por fim, fiz uma contagem de palavras ditas ao longo do tempo (deveria ter plotado o tempo de vídeo….será?). Me parece que o número de views declina no decorrer do tempo ao passo que a quantidade de palavras ditas aumenta. D:. Pode ser uma relação errônea.

O dashboard online pode ser acessado neste Link :D

https://daviinada.shinyapps.io/youtubeapp/