IV Meetup ML, Belo Horizonte

Neste tutorial você aprenderá a usar o R e suas principais bibliotecas de manipulação e visualização de dados para fazer Análise Exploratória de Dados (EDA - Exploratory Data Analysis).

Base de Dados NYC Flights

A base de Dados NYC Flights contém informações sobre todos os vôos que saíram da cidade de Nova Iorque no ano de 2014: 253316 vôos no total.

library("readr")

## Import dataset
flights <- readr::read_csv("./data/flights.csv.zip")
head(flights, n = 5)
dep_time dep_delay arr_time arr_delay origin dest distance
2014-01-01 09:14:00 14 2014-01-01 15:13:00 13 JFK LAX 2475
2014-01-01 11:57:00 -3 2014-01-01 18:00:00 13 JFK LAX 2475
2014-01-01 19:02:00 2 2014-01-02 00:53:00 9 JFK LAX 2475
2014-01-01 07:22:00 -8 2014-01-01 09:59:00 -26 LGA PBI 1035
2014-01-01 13:47:00 2 2014-01-01 19:37:00 1 JFK LAX 2475

A base de dados disponibiliza as seguintes informações: horário de saída e chegada dos vôos, atraso em minutos na saída e na chegada dos vôos, aeroportos de origem e destino, e distância total percorrida em Km. Quais insights podemos extrair desses dados?

A melhor forma de começar consiste em enumerar as perguntas que queremos responder. Exemplo:

  1. Quantos vôos saíram atrasados de cada aeroporto?
  2. O aeroporto de destino tem influência nos atrasos de saída dos vôos?
  3. Qual o aeroporto mais disciplinado com os horários de saída?
  4. O tempo de atraso na chegada está correlacionado com o tempo de atraso na saída?
  5. O tempo de atraso na chegada está correlacionado com a distância do vôo?
  6. Qual o comportamento dos atrasos ao longo dos meses do ano?
  7. Existe algum dia da semana ou horário do dia com maior incidência de atrasos para determinado aeroporto?

Atraso médio dos vôos saindo de EWR

Vamos utilizar o pacote dplyr para manipulação dos dados. Resolveremos problemas complexos quebrando em pequenas partes. Com os verbos básicos do dplyr conseguiremos fazer diversos tipos de análise:

Geralmente, uma análise será composta por uma combinação sequencial de chamadas aos métodos do dplyr. Existem 3 maneiras distintas de combinar as chamadas.

Chamada usando variáveis intermediárias

É uma maneira confusa, pois é muito difícil atribuir nomes coerentes a cada uma das variáveis intermediárias.

library("dplyr")

delays1 <- dplyr::group_by(flights, origin, dest)
delays2 <- dplyr::summarise(delays1, delay = mean(dep_delay), num = n())
delays3 <- dplyr::filter(delays2, origin == "EWR")
delays4 <- dplyr::top_n(delays3, n = 10, wt = delay)
delays  <- dplyr::arrange(delays4, desc(delay))
head(delays, n = 5)
origin dest delay count
EWR AVP 83.00000 1
EWR CAE 36.09524 21
EWR TYS 32.81513 238
EWR TUL 28.89552 201
EWR SAT 26.71220 205

Chamada aninhada

É sem dúvidas a maneira mais difícil de ler o código.

delays <- dplyr::arrange(
   dplyr::top_n(
      dplyr::filter(
         dplyr::summarise(
            dplyr::group_by(
               flights, origin, dest
            ), delay = mean(dep_delay), num = n()
         ), origin == "EWR"
      ), n = 10, wt = delay
   ), desc(delay)
)
head(delays, n = 5)
origin dest delay count
EWR AVP 83.00000 1
EWR CAE 36.09524 21
EWR TYS 32.81513 238
EWR TUL 28.89552 201
EWR SAT 26.71220 205

Chamada usando operador de pipeline

É a maneira mais limpa de ler a sequência de chamadas e não requer nomes intermediários. Para isso usamos o operador %>% (pipe operator). A análise com dplyr é de fato um pipeline de processamento dos dados.

library("magrittr")

delays <- flights %>% 
   dplyr::group_by(origin, dest) %>% 
   dplyr::summarise(delay = mean(dep_delay), num = n()) %>% 
   dplyr::filter(origin == "EWR") %>% 
   dplyr::top_n(n = 10, wt = delay) %>% 
   dplyr::arrange(desc(delay))
head(delays, n = 5)
origin dest delay count
EWR AVP 83.00000 1
EWR CAE 36.09524 21
EWR TYS 32.81513 238
EWR TUL 28.89552 201
EWR SAT 26.71220 205

Visualização dos atrasos em EWR

Para visualização dos dados utilizaremos o pacote ggplot2, que segue os conceitos contidos no livro The Grammar of Graphics. Os gráficos são construídos também em etapas, adicionando camadas (layers). Fica mais fácil de entender exemplificando:

library("tidyr")
library("ggplot2")
library("viridis")

dt <- delays %>% 
   dplyr::mutate(delay = round(delay, 2)) %>% 
   tidyr::unite(col = "flight", origin, dest, sep = "-")

g <- ggplot(aes(x = flight, y = delay), data = dt) +
   geom_bar(stat = "identity") +
   labs(x = "Flight", y = "Delay (min)", title = "Average Delays", 
        subtitle = "Flights from EWR")
plot(g)

Podemos deixar esse gráfico de barras muito mais apresentável:

g <- ggplot(aes(x = reorder(flight, delay), y = delay, 
                fill = reorder(flight, delay)), data = dt) +
   geom_bar(width = 0.7, stat = "identity", color = "white", size = 0.5) +
   coord_flip() +
   geom_text(aes(label = delay), nudge_y = -4, color = "white", size = 4, 
             fontface = "bold") +
   viridis::scale_fill_viridis(direction = -1, discrete = TRUE, guide = "none") +
   labs(x = "Flight", y = "Delay (min)", title = "Average Delays", 
        subtitle = "Flights from EWR")
plot(g)

Distribuição dos atrasos

A análise anterior nos deu uma ideia da média dos atrasos. Mas como esses atrasos variam em torno da média? Para responder a essa pergunta, vamos utilizar um recusos gráfico conhecido como boxplot, que resume a tendência central e a dispersão dos dados.

Primeiramente, vamos escolher os 10 destinos mais frequentes saindo de EWR e remover os valores extremos dos atrasos, para podermos visualizar melhor os dados:

dt <- flights %>% 
   dplyr::filter(origin == "EWR") %>% 
   dplyr::group_by(dest) %>% 
   dplyr::summarise(num = n()) %>% 
   dplyr::top_n(n = 10, wt = num)

dt <- flights %>% 
   dplyr::filter(origin == "EWR", dest %in% dt$dest) %>%
   dplyr::filter(dep_delay > 0) %>% 
   dplyr::filter(dep_delay > quantile(dep_delay, 0.1), 
          dep_delay < quantile(dep_delay, 0.9)) %>% 
   dplyr::select(origin, dest, dep_delay)

Agora nosso boxplot:

g <- ggplot(aes(x = dest, y = dep_delay), data = dt) +
   geom_boxplot(outlier.shape = NA) +
   labs(x = "Destination", y = "Delay (min)", title = "Distribution of Delays", 
        subtitle = "Flights from EWR")
plot(g)

Assim como fizemos para o gráfico de barras, vamos tornar o boxplot mais apresentável:

g <- ggplot(aes(x = dest, y = dep_delay, fill = dest), data = dt) +
   geom_jitter(width = 0.2, color = "gray40", alpha = 0.6, size = 1) +
   geom_boxplot(outlier.shape = NA, alpha = 0.6, size = 0.6) +
   viridis::scale_fill_viridis(discrete = TRUE, guide = "none") +
   viridis::scale_color_viridis(discrete = TRUE, guide = "none") +
   labs(x = "Destination", y = "Delay (min)", title = "Distribution of Delays", 
        subtitle = "Flights from EWR")
plot(g)

Acesse o site R Graph Gallery e busque inspiração por meio de vários exemplos de visualização de dados usando ggplot2 e alguns outros recursos gráficos do R.

Atraso médio do vôo JFK-LAX ao longo do ano

Vamos agora utilizar o pacote lubridate para extrair informação das variáveis de data. Escolhemos o vôo JFK-LAX para exemplificar, pois ele é o mais frequente:

dt <- flights %>% 
   dplyr::filter(dep_delay > 0) %>% 
   tidyr::unite("flight", origin, dest, sep = "-") %>% 
   dplyr::group_by(flight) %>% 
   dplyr::summarise(num = n()) %>% 
   dplyr::ungroup() %>% 
   dplyr::arrange(desc(num))
head(dt, n = 5)
flight num
JFK-LAX 3489
LGA-ATL 2752
JFK-SFO 2733
LGA-ORD 2614
EWR-SFO 2559

Vamos criar a variável dep_month, contendo o mês do embarque de cada vôo:

dt <- flights %>% 
   na.omit() %>% 
   dplyr::filter(dep_delay > 0) %>% 
   tidyr::unite("flight", origin, dest, sep = "-") %>% 
   dplyr::filter(flight == "JFK-LAX") %>% 
   dplyr::mutate(dep_month = lubridate::month(dep_time)) %>% 
   dplyr::group_by(dep_month) %>% 
   dplyr::summarize(delay_mean = median(dep_delay), delay_sd = IQR(dep_delay), 
                    count = n()) %>% 
   dplyr::ungroup()

Agora vamos plotar o atraso médio ao longo do ano:

g <- ggplot(aes(x = dep_month, y = delay_mean), data = dt) +
   geom_ribbon(aes(ymin = delay_mean - delay_sd, ymax = delay_mean + delay_sd), 
               alpha = 0.2) +
   geom_line(size = 1, color = "#482878FF", alpha = 0.6) +
   geom_point(size = 3, color = "#482878FF", alpha = 0.8) +
   labs(x = "Month", y = "Delay (min)", title = "Delay Trends", 
        subtitle = "Flight from JFK to LAX") +
   theme_bw()
plot(g)

Número de atrasos por dia da semana

Será que o dia da semana tem influência nos atrasos? Vamos utilizar um outro tipo de recurso gráfico, chamado heatmap, para visualizar a taxa de atrasos por dia da semana ao longo do ano. Vamos escolher novamente o vôo JFK-LAX para exemplificar.

dt <- flights %>% 
   na.omit() %>% 
   tidyr::unite("flight", origin, dest, sep = "-") %>% 
   dplyr::filter(flight == "JFK-LAX") %>% 
   dplyr::mutate(dep_weekday = lubridate::wday(dep_time, label = TRUE, 
                                               abbr = TRUE)) %>%
   dplyr::mutate(dep_month = lubridate::month(dep_time, label = TRUE, 
                                              abbr = TRUE)) %>% 
   dplyr::group_by(dep_month, dep_weekday) %>% 
   dplyr::summarise(rate = sum(dep_delay > 0) / n()) %>% 
   dplyr::ungroup()
head(dt, n = 5)
dep_month dep_weekday rate
1 Dom 0.5877193
1 Seg 0.3865546
1 Ter 0.4363636
1 Qua 0.6056338
1 Qui 0.5751634

O heatmap permite visualizar 3 variáveis usando apenas 2 dimensões:

g <- ggplot(dt, aes(x = dep_month, y = dep_weekday, fill = rate)) +
   geom_tile(color = "gray93", size = 1, alpha = 0.8) +
   geom_text(aes(label = round(rate, 2)), color = "white") +
   viridis::scale_fill_viridis(direction = -1, name = "Rate") +
   guides(fill = guide_colorbar(barwidth = 0.5, barheight = 10)) +
   theme_gray(base_family = "Helvetica", base_size = 14) +
   labs(x = "Month", y = "Weekday")
plot(g)

Localização dos Aeroportos

Vamos combinar os dados dos vôos com uma nova base de dados contendo informações detalhadas de cada aeroporto.

airports <- readr::read_csv("./data/airports.csv.zip")
head(airports, n = 5)
faa name lat lon alt tz dst tzone
04G Lansdowne Airport 41.13047 -80.61958 1044 -5 A America/New_York
06A Moton Field Municipal Airport 32.46057 -85.68003 264 -6 A America/Chicago
06C Schaumburg Regional 41.98934 -88.10124 801 -6 A America/Chicago
06N Randall Airport 41.43191 -74.39156 523 -5 A America/New_York
09J Jekyll Island Airport 31.07447 -81.42778 11 -5 A America/New_York

O pacote ggmap permite inserir mapas como camadas do ggplot2. Vamos utilizá-lo para plotar a localização dos aeroportos de destino:

library("ggmap")

dest <- airports %>% 
   dplyr::filter(faa %in% unique(flights$dest))

map <- ggmap::get_map("United States", zoom = 4, maptype = "terrain", 
                      scale = 2, color = "bw")

g <- ggmap::ggmap(map, extent = "device", darken = 0.1) +
   stat_density2d(data = dest, aes(x = lon, y = lat, fill = ..level.., 
                                   alpha = ..level..), 
                  size = 0.01, bins = 10, geom = "polygon") + 
   geom_density2d(data = dest, aes(x = lon, y = lat, color = ..level..), 
                  size = 0.5, bins = 15, alpha = 0.8) + 
   geom_point(aes(lon, lat), data = dest, size = 2, alpha = 0.8, color = "black") +
   scale_color_gradientn(colors = heat.colors(100)[80:5], guide = "none") + 
   scale_fill_gradientn(colors = heat.colors(100)[80:5], guide = "none") + 
   scale_alpha(range = c(0.1, 0.4), guide = "none")
plot(g)

Agora, vamos plotar os trajetos dos vôos que saíram de EWR:

dt <- flights %>% 
   dplyr::filter(origin == "EWR") %>% 
   dplyr::rename(faa = dest) %>% 
   dplyr::left_join(airports, by = "faa") %>% 
   na.omit() %>% 
   dplyr::rename(dest = faa, dest_lat = lat, dest_lon = lon)
dt <- dt %>% 
   dplyr::select(origin, dest, dest_lat, dest_lon, dep_delay) %>% 
   dplyr::rename(faa = origin) %>% 
   dplyr::left_join(airports, by = "faa") %>% 
   na.omit() %>% 
   dplyr::rename(origin = faa, orig_lat = lat, orig_lon = lon) %>% 
   dplyr::select(starts_with("orig"), starts_with("dest"), dep_delay)
dt <- dt %>%
   dplyr::filter(dep_delay > 0) %>% 
   dplyr::group_by(origin, dest) %>% 
   dplyr::summarise_each(funs(median))

map <- ggmap::get_map("United States", zoom = 4, maptype = "terrain", 
                      scale = 2, color = "color")

g <- ggmap::ggmap(map, extent = "device", darken = 0.5) +
   geom_curve(aes(x = orig_lon, y = orig_lat, xend = dest_lon, yend = dest_lat, 
                  color = dep_delay), data = dt, size = 0.5, curvature = 0.5) +
   geom_point(aes(x = dest_lon, y = dest_lat), data = dt, color = "red") +
   geom_text(data = dt, aes(x = dest_lon, y = dest_lat, label = dest), col = "black", 
             size = 3, nudge_x = .5, nudge_y = .5, fontface = "bold") +
   coord_cartesian() +
   viridis::scale_color_viridis(name = "Avg Delay", option = "B", direction = -1) +
   guides(color = guide_colorbar(barwidth = 15, barheight = 0.5)) +
   theme(legend.position = "bottom")
plot(g)

Como usar as ferramentas em produção?

O pacote dplyr, além de data.frames, aceita como entrada tabelas de diferentes bancos relacionais. Exemplo:

db <- dplyr::src_postgres(
   dbname = Sys.getenv("POSTGRES_DBNAME"),
   port = Sys.getenv("POSTGRES_PORT"),
   host = Sys.getenv("POSTGRES_HOST"),
   user = Sys.getenv("POSTGRES_USER"),
   password = Sys.getenv("POSTGRES_PASS")
)
my.tbl <- dplyr::tbl(db, "tbl_name")
my.tbl <- dplyr::tbl(db, dplyr::sql("SELECT * FROM tbl_name LIMIT 50"))
dt <- my.tbl %>%
   dplyr::filter() %>% 
   dplyr::mutate() %>% 
   dplyr::select() %>% 
   dplyr::collect()

É possível também manipular Spark data frames usando dplyr, por meio do pacote sparklyr. Exemplo:

library("sparklyr")
library("dplyr")

sc <- spark_connect(master="local")
flights <- copy_to(sc, flights, "flights")
dt <- flights %>% 
   dplyr::filter() %>% 
   dplyr::group_by() %>% 
   dplyr::summarise() %>% 
   dplyr::collect()