Dashboard

Netflix Movies and TV Shows

Dataset utilizado disponível no Kaggle

O dataset utilizado consiste em uma lista de todos os filmes e programas de TV disponíveis na Netflix, juntamente com detalhes como elenco, diretores, classificações, ano de lançamento, duração, etc. O dashboard implementado exibe algumas informações como a quantidade total de filmes, programas de TV, atores e atrizes, bem como comparações da distribuição dos filmes e shows entre os países, e comparações do crescimento entre filmes e programas de TV ao longo dos últimos anos.

Implementação

A base do dashboard foi construida usando o Shiny e o Shiny Dashboard. Para a manipulação dos dados foram utilizadas as bibliotecas dplyr, tidyr, fuzzyjoin, sf e rgdal. Para a plotagem dos gráficos foi utilizado a biblioteca ggplot2, e para a interatividade dos gráficos a biblioteca Plotly. Também foi utilizado o pacote do leaflet para a exibição de um mapa interativo, além de outros pacotes como ggthemes, reactable e RColorBrewer.

1. Layout

O layout foi definido utilizando o shinydashboard como base, utilizando o dashboardPage, dashboardHeader, dashboardSidebar e dashboardBody.

ui <- dashboardPage(
  skin = "black",
  dashboardHeader(title = "Netflix Movies and TV Shows", titleWidth = 300),
  dashboardSidebar(
    width = 300,
    sidebarMenu(
      # Itens do menu de navegação entre as páginas
      menuItem("Dashboard", tabName = "dashboard", icon = icon("dashboard")),
      menuItem("Explorador", tabName = "explorer", icon = icon("table")),
      menuItem("Mapa", tabName = "map", icon = icon("map")),
      
      # Controle exibido apenas quando estiver na aba do mapa
      uiOutput("mapControls")
    )
  ),
  dashboardBody(
    # ...
  )
)

1.1 Meta tags

Foi adicionado um arquivo CSS customizado para corrigir alguns estilos do dashboard.

Também foi adicionado um código JavaScript simples na página que, sempre que a aba atual é alterada, o Shiny é notificado para atualizar o input activeTab no lado do servidor com o nome da aba atual.

# ...
dashboardBody(
  # META TAGS
  tags$head(
    tags$link(rel = "stylesheet", type = "text/css", href = "custom.css"),
    
    # Script simples usando jQuery para atualizar a aba ativa no input do Shiny
    tags$script(HTML('
      $(document).ready(function(){
        $("a[data-toggle=tab]").on("show.bs.tab", function(e){
          Shiny.setInputValue("activeTab", $(this).attr("data-value"));
        });
      });
    '))
  ),
  # ...
)

1.2 Abas

Foi utilizado o tabItems do shinydashboard para definir o conteúdo de cada aba, que são controladas através da interação com os itens do menu lateral (menuItem).

tabItems(
      # Dashboard ----
      tabItem(
        tabName = "dashboard",
        # ...
      ),
      
      # Explorer ----
      tabItem(
        tabName = "explorer",
        # ...
      ),
      
      # Map ----
      tabItem(
        tabName = "map"
        # ...
      )
)

1.2.1 Aba Dashboard

Na aba Dashboard foi utilizada a seguinte estrutura:

  1. Caixas de valor: Elemento simples para exibir valores numéricos ou de texto, com um ícone.
    • Quantidade total de títulos
    • Quantidade total de filmes
    • Quantidade total de programas de TV
    • Quantidade total de atores e atrizes
    • Quantidade total de paises
    • Quantidade total de diretores
  2. Filmes vs Show de TV (Gráfico): Comparativo do crescimento de filmes e programas de TV ao longo dos últimos anos.
  3. Top 10 atores/atrizes (Gráfico): Os 10 atores/atrizes que mais aparecem em filmes diferentes.

Captura de tela da aba dashboard

# Dashboard ----
tabItem(
  tabName = "dashboard",
  
  # VALUE BOXES
  fluidRow(
    valueBoxOutput("totalCount"),
    valueBoxOutput("movieCount"),
    valueBoxOutput("tvShowCount"),
    valueBoxOutput("castCount"),
    valueBoxOutput("countriesCount"),
    valueBoxOutput("directorCount"),
  ),
  
  # CHARTS
  fluidRow(
    box(
      width = 12,
      title = "Filmes vs Programas de TV",
      plotlyOutput("moviesVsTvShowPlot")
    ),
    box(
      width = 12,
      title = "Top 10 atores/atrizes",
      plotOutput("top10CastPlot")
    )
  )
)

1.2.2 Aba Explorador

Na aba Explorador foi utilizado o pacote reactable para exibir uma tabela que permite explorar todo o dataset, com filtro, ordenação, paginação e busca. Captura de tela da aba Explorador

# Explorer ----
tabItem(
  tabName = "explorer",
  fluidRow(
    box(
      width = 12,
      reactableOutput("explorerTable")
    )
  )
)

1.2.3 Aba Mapa

Na aba Mapa foi utilizado o pacote leaflet para exibir um mapa interativo que mostra a quantidade de lançamentos por região. Além disso, foi adicionado um sliderInput no painel lateral, que aparece somente na aba Mapa e serve para alterar a opacidade dos polígonos do mapa. Captura de tela da aba Mapa

# Map ----
tabItem(
  tabName = "map",
  fluidRow(
    box(
      title = "Países com maior número de lançamentos",
      width = 12,
      leafletOutput("map", height = 'calc(100vh - 200px)')
    )
  )
)



2. Manipulação dos dados


2.1 Dashboard


2.1.1 Total de lançamentos

Conta todos os títulos distintos e salva na coluna n, extrai apenas a coluna n e formata o valor.

nf_titles %>%
    summarize(n = n_distinct(show_id)) %>%
    pull(n) %>%
    format(big.mark = ".", decimal.mark = ",")

2.1.2 Total de filmes

Filtra todos que possuem o valor Movie na coluna type, conta os lançamentos distintos e salva na coluna n, extrai a propriedade n e formata o valor.

nf_titles %>%
    filter(type == "Movie") %>%
    summarize(n = n_distinct(show_id)) %>%
    pull(n) %>%
    format(big.mark = ".", decimal.mark = ",")

2.1.3 Total de programas de TV

Filtra todos que possuem o valor TV Show na coluna type, conta os lançamentos distintos e salva na coluna n, extrai a propriedade n e formata o valor.

nf_titles %>%
    filter(type == "TV Show") %>%
    summarize(n = n_distinct(show_id)) %>%
    pull(n) %>%
    format(big.mark = ".", decimal.mark = ",")

2.1.4 Total de atores/atrizes

Separa a coluna de atores/atrizes, converte uma coluna com vários atores/atrizes separados por vírgula em várias linhas com apenas um ator por linha.

nf_cast = nf_titles %>%
  separate_rows(cast, sep = ",") %>%
  mutate(cast = str_trim(cast, side = "both")) %>%
  filter(cast != "")

Conta todos os atores/atrizes distintos e salva na coluna n, extrai apenas a coluna n e formata o valor.

nf_cast %>%
      summarise(n = n_distinct(cast)) %>%
      pull(n) %>%
      format(big.mark = ".", decimal.mark = ",")

2.1.5 Total de países

Separa a coluna de países, converte uma coluna com vários países separados por vírgula em várias linhas com apenas um país na coluna.

nf_countries = nf_titles %>%
  separate_rows(country, sep = ",") %>%
  mutate(country = str_trim(country, side = "both")) %>%
  filter(country != "")

Conta todos os países distintos e salva na coluna n, extrai apenas a coluna n e formata o valor.

nf_countries %>%
    summarise(n = n_distinct(country)) %>%
    pull(n) %>%
    format(big.mark = ".", decimal.mark = ",")

2.1.6 Total de diretores

Separa a coluna de diretores, conta todos os diretores distintos, salva na coluna n, extrai apenas a coluna n e formata o valor.

nf_titles %>%
      separate_rows(director, sep = ",") %>%
      mutate(director = str_trim(director, side = "both")) %>%
      filter(director != "") %>%
      summarise(n = n_distinct(director)) %>%
      pull(n) %>%
      format(big.mark = ".", decimal.mark = ",")

2.1.7 Filmes vs Programas de TV

Agrupa por ano de lançamento e tipo, conta o número distinto de lançamentos de cada grupo, ordena pelo ano de lançamento e constrói o gráfico. O x é o ano de lançamento, o y é a quantidade de lançamentos no respectivo ano. O tipo é utilizado para representar o estilo da linha e a cor.

É utilizado o renderPlotly e o ggplotly para tornar o gráfico criado com o ggplot interativo.

output$moviesVsTvShowPlot <- renderPlotly({
  ggplotly(
    tooltip = c("text"),
    nf_titles %>%
      group_by(release_year, type) %>%
      summarize(n = n_distinct(show_id)) %>%
      arrange(release_year) %>%
      ggplot(aes(
        x = release_year,
        y = n,
        text = paste(
          "<b>", release_year, "</b>",
          "<br>Tipo: ", type,
          "<br>Títulos lançados: ", n
        )
      )) +
      geom_line(
        aes(
          group = 1,
          color = type,
          linetype = type,
        ),
        size = 1
      ) +
      scale_color_manual(values = c("darkred", "steelblue")) +
      labs(color = "Tipo", linetype = NULL, x = "Ano de lançamento", y = "Quantidade de lançamentos")
  )
})

2.1.8 Top 10 atores/atrizes

Agrupa pelos atores/atrizes, o dataset em que a coluna de atores/atrizes já foi separada, conta os lançamentos distintos, ordena pela contagem em ordem decrescente e pega apenas os 10 primeiros.

output$top10CastPlot <- renderPlot({
  nf_cast %>%
    group_by(cast) %>%
    summarise(n = n_distinct(show_id)) %>%
    arrange(desc(n)) %>%
    head(10) %>%
    ggplot(aes(
      x = reorder(cast, n),
      y = n
    )) +
    geom_bar(stat = "identity") +
    coord_flip() +
    labs(x = "Atores/Atrizes", y = "Lançamentos")
})

2.2 Explorador

output$explorerTable <- renderReactable({
  reactable(nf_titles, defaultSorted = c("date_added", "title"), defaultSortOrder = "desc", filterable = TRUE, searchable = TRUE)
})

2.3 Mapa


2.3.1 Controle de opacidade

Sempre que a aba ativa for map o input de controle de opacidade é renderizado.

output$mapControls = renderUI({
  if (!is.null(input$activeTab) && input$activeTab == "map") {
    fluidRow(
      column(
        width = 12,
        sliderInput(
          inputId = "mapOpcaitySlider",
          label = h3("Opacidade"),
          min = 0,
          max = 100,
          value = 75,
          post = '%'
        )
      ),
    )
  }
})

2.3.2 Mapa

Primeiro é criado um dataset com a contagem de títulos lançados agrupados por país.

nf_countries_count <- nf_countries %>%
    group_by(country) %>%
    summarise(n = n_distinct(show_id))

Após isso, criamos um novo dataset através da intersecção da contagem de lançamentos por pais, com a geometria do mapa para cada país.

Obs.: É preciso usar um join com regex pois pode haver diferenças entre os nomes do GeoJSON e do dataset, como por exemplo: United States e United States of America

# É preciso converter para um objeto SF usando `st_as_sf` para que o leaflet encontre as geometrias dos países 
countries_intersection <- st_as_sf(
  world_country %>%
    regex_inner_join(nf_countries_count, by = c(name = "country"), ignore_case = TRUE)
)

É criado uma paleta de cores que será usada para pintar as regiões do mapa de acordo com a quantidade de lançamentos da região. Também é criado um modelo para as labels do tooltip ao passar o mouse sobre uma região.

pal <- colorBin("Reds", nf_countries_count$n, bins = c(0, 10, 100, 200, 300, 400, 600, 800, 1000, 2000, 3000, Inf))
  
labels <- sprintf(
  "<strong>%s</strong><br/>%d lançamentos</sup>",
  countries_intersection$name, countries_intersection$n
) %>% lapply(htmltools::HTML)

Por último, criamos um mapa do leaflet:

# Obtém o valor da opacidade do input em forma de porcentagem
opacity <- input[["mapOpcaitySlider"]] / 100

countries_intersection %>%
  leaflet() %>%
  addTiles() %>%
  addPolygons(
    fillColor = ~pal(n), # Obtém a cor da paleta de cores conforme a quantidade de títulos da região
    weight = 2,
    opacity = opacity,
    fillOpacity = opacity,
    color = "white",
    dashArray = "3",
    highlightOptions = highlightOptions(
      weight = 4,
      color = "#222",
      dashArray = "",
      fillOpacity = 0.7,
      bringToFront = TRUE
    ),
    label = labels,
    labelOptions = labelOptions(
      style = list("font-weight" = "normal", padding = "3px 8px"),
      textsize = "15px",
      direction = "auto"
    )
  ) %>%
  addLegend(pal = pal, values = ~n, opacity = 0.7, title = NULL,
            position = "bottomright")