En estos laboratorios usaremos la librería tidyverse
, que es un paquete de varias librerías para manejo de datos, como dplyr
(manipular datos), ggplot2
(gráficos), y otras.
Vamos a trabajar con tidyverse
en vez de R Base, pero con ambos se puede lograr los mismos resultados.
# install.packages("tidyverse")
# cargamos la librería
library(tidyverse)
# le decimos a R el directorio en el que vamos a trabajar, también se puede hacer desde el explorador de archivos que ofrece RStudio
setwd('/home/mquezada/diplomado-2018/')
Cargar datos:
# `x <- 1` asigna 1 a la variable `x`
# acá vamos a guardar en la variable `data` la tabla que leemos desde el archivo CSV
# http://datos.gob.cl/dataset/9348
data <- read_csv('https://users.dcc.uchile.cl/~mquezada/diplomado-2018/accidentes-2010-2011.csv')
## Parsed with column specification:
## cols(
## Muestra = col_character(),
## `Descripcion Muestra` = col_character(),
## Año = col_integer(),
## Atropello = col_number(),
## Caída = col_number(),
## Colisión = col_number(),
## Choque = col_number(),
## Volcadura = col_number(),
## Otros = col_number(),
## Muertos = col_number(),
## `Lesionados Graves` = col_number(),
## `Lesionados Menos Graves` = col_number(),
## `Lesionados Leves` = col_number(),
## `(Incorporar Otra Columna Si es Necesario)` = col_character()
## )
Los mensajes de output muestran cómo se interpretó cada columna de los datos. Podemos ver que, por ejemplo, el Año fue interpretado como número entero.
La función read_csv
de tidyverse
entrega un tibble
, que es similar a un data.frame
de R. Una de sus gracias es que es amigable con la consola a la hora de imprimirlo, a diferencia de un data.frame
, para el cual generalmente se necesita usar head
para evitar mostrar toda la tabla en pantalla. head
muestra por defecto los primeros 6 valores de un vector (o las primeras 6 fila de una tabla).
En tidyverse
no es necesario usar head
:
# o `head(data)` si se trabaja con `data.frame` en vez de `tibble`
data
Vemos que el output nos muestra más información de la tabla, como los tipos de datos de cada columna (str(data)
o sapply(data, class)
), las dimensiones de la tabla (dim(data)
), y los nombres del resto de las columnas (colnames(data)
).
Vamos a definir unos conceptos a la hora de trabajar con datos:
En este caso, cada observación corresponde a una fila del dataset, cada atributo a una columna y cada valor a una única celda de la tabla. Por lo tanto, podemos hablar indistintamente de un atributo o de una columna, o de una fila y una observación.
Veamos algunos valores del dataset usando glimpse
(o str
)
glimpse(data)
## Observations: 716
## Variables: 14
## $ Muestra <chr> "Nacional", "Nacio...
## $ `Descripcion Muestra` <chr> "Nacional", "Nacio...
## $ Año <int> 2010, 2011, 2010, ...
## $ Atropello <dbl> 8247, 8339, 115, 1...
## $ Caída <dbl> 1478, 1488, 6, 13,...
## $ Colisión <dbl> 29127, 31487, 265,...
## $ Choque <dbl> 14558, 16312, 151,...
## $ Volcadura <dbl> 3629, 3985, 55, 68...
## $ Otros <dbl> 707, 1223, 6, 10, ...
## $ Muertos <dbl> 1595, 1573, 28, 33...
## $ `Lesionados Graves` <dbl> 6899, 6724, 87, 99...
## $ `Lesionados Menos Graves` <dbl> 4321, 4454, 70, 80...
## $ `Lesionados Leves` <dbl> 41744, 43034, 444,...
## $ `(Incorporar Otra Columna Si es Necesario)` <chr> NA, NA, NA, NA, NA...
Vemos que la mayoría de las columnas son numéricas, indicando la cantidad de atropellos, caídas, etc. Sin embargo, la última columna tiene NA
s. Estos son datos faltantes, de los cuales desconocemos su valor.
Podemos ver los distintos valores de una columna usando la función unique()
y $
para seleccionar una columna de los datos.
unique(data$Año)
## [1] 2010 2011
¿Cuántos valores distintos hay en la última columna? ¿Hay alguna fila para la cual haya algún dato en ésta?
# usamos `...` para nombrar variables o atributos con símbolos especiales, como espacios en blanco
unique(data$`(Incorporar Otra Columna Si es Necesario)`)
## [1] NA
Vemos que el único valor en esta columna es NA
, por lo que es seguro eliminar esta columna:
data <- data[, -14]
# o data <- select(data, -(`(Incorporar Otra Columna Si es Necesario)`))
¿Qué hacer cuando hay datos faltantes? Siempre depende del dominio de los datos. Hay al menos dos opciones:
En el segundo caso, se puede imputar un valor con algún valor fijo (ej. 0 o 1), o con un valor que depende de los otros (ej. el promedio, la mediana, etc.)
Con summary
podemos ver cómo se distribuyen los datos de cada columna.
summary(data)
## Muestra Descripcion Muestra Año Atropello
## Length:716 Length:716 Min. :2010 Min. : 0.00
## Class :character Class :character 1st Qu.:2010 1st Qu.: 2.00
## Mode :character Mode :character Median :2010 Median : 6.00
## Mean :2010 Mean : 69.49
## 3rd Qu.:2011 3rd Qu.: 31.25
## Max. :2011 Max. :8339.00
## Caída Colisión Choque Volcadura
## Min. : 0.00 Min. : 0.0 Min. : 0.0 Min. : 0.00
## 1st Qu.: 0.00 1st Qu.: 5.0 1st Qu.: 3.0 1st Qu.: 4.00
## Median : 0.00 Median : 18.0 Median : 11.0 Median : 8.00
## Mean : 12.43 Mean : 254.0 Mean : 129.3 Mean : 31.90
## 3rd Qu.: 4.00 3rd Qu.: 105.5 3rd Qu.: 50.0 3rd Qu.: 15.25
## Max. :1488.00 Max. :31487.0 Max. :16312.0 Max. :3985.00
## Otros Muertos Lesionados Graves
## Min. : 0.000 Min. : 0.00 Min. : 0.00
## 1st Qu.: 0.000 1st Qu.: 1.00 1st Qu.: 4.00
## Median : 1.000 Median : 3.00 Median : 10.00
## Mean : 8.087 Mean : 13.27 Mean : 57.08
## 3rd Qu.: 2.250 3rd Qu.: 7.00 3rd Qu.: 27.00
## Max. :1223.000 Max. :1595.00 Max. :6899.00
## Lesionados Menos Graves Lesionados Leves
## Min. : 0.00 Min. : 0.0
## 1st Qu.: 3.00 1st Qu.: 18.0
## Median : 7.00 Median : 48.0
## Mean : 36.77 Mean : 355.2
## 3rd Qu.: 18.00 3rd Qu.: 149.2
## Max. :4454.00 Max. :43034.0
Usando data.frames
en R Base, se pueden filtrar filas y columnas seleccionando atributos con $
y aplicando condiciones lógicas. Ej:
# en R Base: dataframe[<filtro filas>, <filtro columnas>] (si el <filtro> está en blanco, se seleccionan todas las filas/columnas)
# la notación $ sirve para seleccionar una columna de los datos (dataframe$columna). Esta operación retorna un vector
data[data$Año == 2010 & data$Muestra == "Nacional", ]
En tidyverse
(en particular, en dplyr
) tenemos la función filter
que además de hacer lo mismo de una forma más amigable, tiene otras ventajas que veremos después:
# equivalente al ejemplo anterior
# retorna un nuevo tibble
filter(data, Año == 2010, Muestra == 'Nacional')
En este caso, la coma representa un “AND” entre las dos condiciones. Podemos usar otro tipo de condiciones en los argumentos de filter
:
filter(data, Año == 2010, Muestra == 'Comunal', Muertos > 30 | `Lesionados Graves` > 110)
Los ejemplos anteriores filtran filas. Para filtrar columnas, usamos select
:
# selecciona la columnas "Año", "Atropello", y todas las columnas entre "Muertos" y "Lesionados Leves"
select(data, Año, Atropello, Muertos:`Lesionados Leves`)
# selecciona todas las columnas, *excepto* "Muertos" y "Otros"
# acá creamos un vector usando `combine`: c(Muertos, Otros) para enumerar más de una columna
select(data, -c(Muertos, Otros))
dplyr
¿Qué hacemos si queremos filtrar filas por algún valor de una columna, y seleccionar algunas de estas columnas? Por ejemplo, queremos todas las comunas que tuvieron más de 100 lesionados graves o más de 30 muertos en el año 2010, pero sólo queremos reportar las columnas relevantes.
Una forma de hacerlo es asignando los resultados a nuevas variables y usando estas nuevas variables con el siguiente filtro, por ejemplo:
data2 <- filter(data, Año == 2010, Muestra == 'Comunal', Muertos > 30 | `Lesionados Graves` > 100)
data3 <- select(data2, Año, Muestra, `Descripcion Muestra`, Muertos, `Lesionados Graves`)
data3
Una forma un poco más simple es usando un pipe o tubería en el cual va pasando la misma tabla y sus resultados por cada una de las funciones que estamos aplicando:
data %>%
filter(Año == 2010, Muestra == 'Comunal', Muertos > 30 | `Lesionados Graves` > 100) %>%
select(Año, Muestra, `Descripcion Muestra`, Muertos, `Lesionados Graves`)
Nota que no tuvimos que crear una variable auxiliar, ni tampoco tuvimos que pasársela a select
. El operador %>%
(pipe) permite pasar una variable por varias funciones y retornar el resultado a la siguiente función en el pipe.
En dplyr
existen más funciones que nos permiten manipular datos. Las funciones principales son las siguientes:
filter
para seleccionar filas por alguna condiciónselect
para escoger columnas por su nombrearrange
para reordenar filasmutate
para crear nuevas columnassummarize
para colapsar varios valores en uno soloTambién podemos agrupar filas y hacer agregaciones usando summarize
. Esto lo veremos más adelante.
En el siguiente ejemplo vamos a mostrar las regiones con la mayor tasa de muertes por cualquier tipo de accidente en el año 2010.
data %>%
filter(Año == 2010, Muestra == 'Regional') %>%
mutate(muertes_por_accidente = Muertos / (Atropello + Caída + Colisión + Choque + Volcadura + Otros)) %>%
select(`Descripcion Muestra`, muertes_por_accidente) %>%
arrange(desc(muertes_por_accidente))
group_by
y summarize
Total de afectados por muestra:
# Verificamos con esto que los datos al menos están bien agregados :-)
data %>%
group_by(Muestra) %>%
summarize(
leves = sum(`Lesionados Leves`),
menos_graves = sum(`Lesionados Menos Graves`),
graves = sum(`Lesionados Graves`),
muertos = sum(Muertos)
)
# podemos agregar por varias columnas
data %>%
filter(Muestra != 'Nacional') %>%
group_by(Muestra, Año) %>%
summarize(
promedio_muertos = mean(Muertos)
)
# valores a nivel de comuna
data %>%
filter(Año == 2010, Muestra == 'Comunal') %>%
summarize(
promedio_atropello = mean(Atropello),
mediana_atropello = median(Atropello),
promedio_caida = mean(Caída),
mediana_caida = median(Caída),
promedio_colision = mean(Colisión),
mediana_colision = median(Colisión)
) %>%
print(width = Inf)
## # A tibble: 1 x 6
## promedio_atropello mediana_atropello promedio_caida mediana_caida
## <dbl> <dbl> <dbl> <dbl>
## 1 24.1 5.5 4.32 0
## promedio_colision mediana_colision
## <dbl> <dbl>
## 1 85.2 15.5
Notemos que el promedio y la mediana son muy diferentes. ¿Por qué? ¿Qué quiere decir la mediana? ¿En qué casos nos sirve?
Otras estadísticas:
mean(Atropello, trim = x)
calcula el promedio, pero eliminando la fracción x
de los datos más grandes y más pequeños de Atropello
sd(Atropello)
, nos indica el grado de dispersión con respecto al promedio.quantile(Atropello)
, determina los cuantiles (por defecto, los cuartiles, pero se le puede indicar cualquier percentil).IQR(Atropello)
determina la diferencia entre el tercer y primer cuartil.ggplot2
¿En qué casos nos sirve usar el promedio? Necesitamos ver la distribución de los datos para saber cuál es una medida de la “tendencia central” apropiado:
comunas_2010 <- filter(data, Año == 2010, Muestra == 'Comunal')
ggplot(comunas_2010) +
geom_histogram(mapping = aes(x = Atropello), binwidth = 10)
Otra forma de observar la distribución de los datos de forma resumida es mediante un boxplot:
# acá el eje $x$ que se muestra en el gráfico no tiene sentido
ggplot(comunas_2010) +
geom_boxplot(mapping = aes(y = Atropello))
Podemos observar que la mayoría de los datos se concentra en mucho menos de 50 atropellos. El boxplot de ggplot2
se diferencia del boxplot ‘tradicional’ en que en ggplot2
la barra de los extremos se extiende hasta 1.5 * IQR
en vez de hasta el 90vo percentil (o noveno decil).
También podemos usar boxplot para comparar distribuciones:
ggplot(filter(data, Muestra == 'Comunal')) +
geom_boxplot(mapping = aes(x = factor(Año), y = Atropello, group=Año)) +
labs(x = "Año", y = "Comunas", title="Atropellos por Comuna en el 2010 y 2011")
Podemos observar que para el 2011 hay un outlier (una comuna con más de 400 atropellos).
Sirve para observar correlaciones entre dos variables.
ggplot(comunas_2010) +
geom_point(mapping = aes(x = Atropello, y = `Lesionados Leves`))
dollar <- read_csv("https://www.exchange-rates.org/HistoryExchangeRatesReportDownload.aspx?base_iso_code=USD&iso_code=CLP")
## Parsed with column specification:
## cols(
## Date = col_character(),
## Rate = col_double(),
## `ISO Code From` = col_character(),
## `ISO Code To` = col_character()
## )
dollar
# lubridate ofrece funciones para "parsear" (estructurar) fechas
# ej. `mdy()` (month-day-year) permite parsear fechas del estilo "12-31-1999" o "12/31/1999"
# https://lubridate.tidyverse.org/
library(lubridate)
##
## Attaching package: 'lubridate'
## The following object is masked from 'package:base':
##
## date
dollar <- dollar %>%
mutate(fecha = mdy(dollar$Date))
dollar
ggplot(dollar) +
geom_line(mapping = aes(x=fecha, y=Rate)) +
labs(x="Fecha", y="CLP", title="Valor del Dólar en Pesos Chilenos")
Un gráfico de línea es útil cuando queremos ver la tendencia de una variable que depende directamente de otra (usualmente, el valor de algo en el tiempo).
Útil cuando queremos visualizar un valor asociado a variables categóricas, ya sea un valor presente en los datos o un valor calculado (ej. frecuencia).
# coord_flip "da vuelta" el sistema de coordenadas
# por defecto, geom_bar usará la frecuencia en el mapping a `y`
# si usamos stat = "identity", podemos usar el valor que queramos en `y`
ggplot(filter(data, Muestra == 'Regional', Año == 2010)) +
geom_bar(mapping = aes(x = `Descripcion Muestra`, y = Muertos), stat = "identity") +
coord_flip()
Podemos reordenar las barras usando reorder
en el eje x.
# ordenamos los nombres de las regiones en base al valor del atributo que queremos mostrar en $y$, en este caso, por Muertos.
ggplot(filter(data, Muestra == 'Regional', Año == 2010)) +
geom_bar(mapping = aes(x = reorder(`Descripcion Muestra`, Muertos), y = Muertos), stat = "identity") +
coord_flip()
# `fill` es otro mapping: corresponde a diferenciar las barras por su color
# en este caso, usamos el Año para diferenciar las barras
# `position = "dodge"` mostrará las barras una al lado de la otra
ggplot(filter(data, Muestra == 'Regional')) +
geom_bar(mapping = aes(x = `Descripcion Muestra`, y = Muertos, fill = factor(Año)),
stat = "identity",
position = "dodge") +
coord_flip()
Vemos que la región del Bío-Bío aparece dos veces, y es porque para un año aparece como VII región y para otro como VIII región. Podemos corregir eso en los datos y volver a graficar:
# `replace` recibe tres parámetros:
# 1. el nombre de la columna C en la cual queremos reemplazar valores
# 2. la condición que las filas de C deben cumplir
# 3. el nuevo valor de las filas anteriores
data <- data %>%
mutate(`Descripcion Muestra`=replace(`Descripcion Muestra`, `Descripcion Muestra` == 'VII Región del Bio-Bio', 'VIII Región del Bio-Bio'))
ggplot(filter(data, Muestra == 'Regional')) +
geom_bar(mapping = aes(x = `Descripcion Muestra`, y = Muertos, fill = factor(Año)),
stat = "identity",
position = "dodge") +
coord_flip()
¿Por qué es mejor que usar un gráfico de torta (pie chart)?
Corresponde a la suma acumulada de un atributo y el % de observaciones que tienen al menos esa cantidad para ese atributo. Es muy útil para observar distribuciones muy sesgadas.
Por ejemplo, podemos ver que la mediana está en el punto en que y == 50%
.
# el operador .$ permite extraer una columna del data frame como un vector,
# en este caso, el vector solo tiene 1 valor, que es la mediana de atropellos
med <- data %>%
filter(Muestra == 'Comunal', Año == 2010) %>%
summarize(mediana = median(Atropello)) %>%
.$mediana
# añadimos una linea vertical con geom_vline, cuyo intercepto con $x$ es el valor de la mediana
# podemos ver que la mediana corresponde al 50% en este grafico
# cada % en el eje $y$ corresponde a los percentiles y en $x$ es el valor del percentil correspondiente
# podemos ver aprox el 90% de las comunas tuvo 60 atropellos o menos
ggplot(filter(data, Muestra == 'Comunal', Año == 2010)) +
stat_ecdf(mapping = aes(x = Atropello)) +
geom_vline(xintercept = med, color = "blue") +
scale_y_continuous(labels = scales::percent, breaks = seq(0, 1, 0.1)) +
scale_x_continuous(breaks = seq(0, 400, 20)) +
labs(x = "Cantidad de atropellos", y = "% de comunas con al menos X atropellos")
ggplot2
permite desplegar varios gráficos juntos, ya que cada gráfico corresponde a una “capa” en la visualización:
# mostrar los datos junto con sus boxplots
comunal <- filter(data, Muestra == 'Comunal')
# podemos poner el mapping al principio para evitar escribir el mismo cada vez
ggplot(comunal, mapping = aes(x = factor(Año), y = Choque)) +
geom_boxplot() +
geom_jitter(color = "red") +
coord_flip()
# podemos ver que la correlación no es muy fuerte
ggplot(filter(data, Muestra == 'Comunal', Año == 2010), mapping = aes(x = Colisión, y = Muertos)) +
geom_point() +
geom_smooth(method='lm')
También podemos agrupar gráficos por alguna variable usando facet_grid
.
# usamos una variable categórica o una que sabemos que tiene pocos valores
# la notación `~ Año` significa que vamos a agrupar los gráficos sólo por el atributo `Año`
ggplot(filter(data, Muestra == 'Regional')) +
geom_bar(mapping = aes(x = `Descripcion Muestra`, y = Atropello), stat = "identity") +
facet_grid(~ Año) +
coord_flip()
También existe facet_wrap
, que es similar y permite indicar la cantidad de filas y columnas de los gráficos.
# cambiamos las etiquetas por el formato Año-Mes-Día (%Y-%m-%d)
ggplot(dollar) +
geom_line(mapping = aes(x=fecha, y=Rate)) +
scale_x_date(date_labels = "%Y-%m-%d") +
labs(x="Fecha", y="CLP", title="Valor del Dólar en Pesos Chilenos")
# añadimos "cortes" (breaks) cada 1 mes y unidades de dinero en el eje y
ggplot(dollar) +
geom_line(mapping = aes(x=fecha, y=Rate)) +
scale_x_date(date_labels = "%Y-%m-%d", date_breaks = "1 month") +
scale_y_continuous(labels = scales::dollar) +
labs(x="Fecha", y="CLP", title="Valor del Dólar en Pesos Chilenos")
# usamos un grafico de densidad (lo mismo que un histograma pero que va de 0 a 1 en vez de 0 al maximo de frecuencia)
# cambiamos el eje y para mostrar porcentajes
ggplot(filter(data, Muestra == 'Comunal', Año == 2010)) +
geom_density(mapping = aes(x = Atropello)) +
scale_y_continuous(labels = scales::percent)
# por defecto geom_bar muestra las frecuencias de x
# podemos usar scale_y_log10 o scale_y_sqrt para cambiar la escala de y a logaritmica o raiz cuadrada, respectivamente
# ambas sirven para disminuir el efecto de puntos demasiado grandes (en este caso, frecuencias) y poder ver los valores mas pequeños
# log requiere que los valores sean > 0, ya que log(0) no está definido
ggplot(filter(data, Muestra == 'Comunal', Año == 2010)) +
geom_bar(mapping = aes(x = Atropello)) +
scale_y_sqrt()
Los datos que hemos usado hasta ahora tienen tres buenas propiedades:
data
Estas propiedades nos han ayudado a hacer todos los ejemplos de este tutorial de forma simple.
¿Cuál es el problema con los siguientes datos? ¿Cómo calcularía el IMC de cada persona en una nueva columna?
people <- tribble(
~nombre, ~llave, ~valor,
#-----------------|--------|------
"Phillip Woods", "weight", 80,
"Phillip Woods", "height", 186,
"Jessica Cordero", "weight", 52,
"Jessica Cordero", "height", 156
)
Para transformar estos datos en el formato tidy (“ordenado”), dplyr
provee de dos funciones: gather
y spread
:
people %>%
spread(key = llave, value = valor) %>%
mutate(imc = weight / (height / 100) ^ 2)
spread
toma los valores del atributo dado por key
y los separa en nuevos atributos cuyos valores están dados por value
.
Podemos tener el caso contrario, donde valores asociados a más de una observación están distribuidos en varios atributos:
library(readxl)
# ¿Cómo podemos ver la tendencia en el tiempo de cada región?
# http://datos.gob.cl/dataset/7161
femicidios <- read_excel('vcm-femicidiossegunregion2008-2012.xlsx')
femicidios
femicidios.tidy <- femicidios %>%
gather(`2008`:`2012`, key = "Año", value = "Casos", convert = TRUE)
ggplot(filter(femicidios.tidy, Región == 'Valparaíso')) +
geom_bar(mapping = aes(x = Año, y = Casos), stat = "identity")
gather
recibe como primer argumento las columnas desde las que queremos recolectar datos. key
indica el nombre del atributo en el cual se pondrán las columnas y value
el nombre del atributo donde se pondrán los valores asociados a cada columna. convert
le indica si debe tratar de inferir el tipo de dato de cada valor (en este caso, el año es numérico, si omitimos convert
, los dejará como caracteres).
Para ver la definición de una función en R, con ejemplos, se puede usar help
o ?
:
# help(gather)
?gather
También las referencias, abajo, contienen mucha información útil sobre cómo usar las herramientas que hemos visto en este tutorial.