Introduccion a ggplot2

Herramientas: ggplot2

Presentación

Este documento quiere servir de apoyo a las sesiones de Visual Analytics del Programa Big Data & Business Intelligence de la Universidad de Deusto. Como tal, es un documento en estado constante de revisión y ampliación.

Estos apuntes no pretenden ser una documentación exhaustiva de ggplot2; la idea es transmitir los entresijos básicos del funcionamiento de ggplot2.

ggplot2

ggplot2 es un paquete parte de Tidyverse para crear gráficos estáticos. En los siguientes ejemplos usaremos algún otro paquete del Tidyverse, por lo que cargaremos el metapaquete entero (o<tra opción sería cargar sólo los paquetes que vayamos a usar: ggplot2, dplyr…)

Layered Grammar of graphics

En 2010 Hadley Wickham publicó el artículo “A layered grammar of graphics”, basado en la Grammar of graphics de Wilkinson y en la que se basa ggplot.

Dicha gramática por capas consta de los siguientes elementos:

  • DATA: ggplot espera como entrada un conjunto de datos limpio (tidy data) como entrada. Además, dependiendo del tipo de gráfica que queramos crear, tendremos que crear algún resumen de datos agregados, o incluso reestructurar el conjunto de datos original. Podemos usar conjuntos de datos distintos para capas distintas.
  • GEOM: elemento gráfico al que se van a mapear las variables del conjunto de datos: líneas, puntos…
  • STATS: transformaciones estadísticas de los datos.
  • POSTION: ajustes para evitar que las marcas se superpongan.
  • COORDINATE: por defecto ggplot aplicará coordenadas cartesianas al gráfico, pero podemos modificar las escalas, así como el sistema de coordenadas (por ejemplo, a un sistema polar)
  • FACET: para poder crear small multiples.
  • THEMING: funciones y argumentos relacionados con aspectos gráficos a los datos propiamente dichos: tipografía, fondo, aspecto de los ejes…
ggplot(data = <DATA>) +
  <GEOM_FUNCTION>(
     mapping = aes(<MAPPINGS>),
     stat = <STAT>,
     position = <POSITION>
  ) +
  <COORDINATE_FUNCTION> +
  <FACET_FUNCTION>

Cosas que hace ggplot y cosas que no

ggplot es una herramienta muy versátil para crear todo tipo de gráficos estáticos.

Además es extensible, por lo que existe una comunidad de extensiones que amplian las funciones existentes en ggplot para facilitar crear determinados gráficos.

Sin embargo, ggplot no tiene:

  • Interactividad: filtros… Podemos usar el paquete ggvis (en desarrollo), o montar una app con Shiny.
  • Animaciones. aunque están desarrollando una Grammar of animated graphics con gganimate.

Funcionamiento básico

Carga inicial de datos

Cargamos el conjunto de datos diamonds que incluye ggplot2 y echamos un vistazo a su estructura (más información sobre las variables):

library(ggplot2)

package 'ggplot2' was built under R version 3.5.1

str(diamonds)

## Classes 'tbl_df', 'tbl' and 'data.frame':	53940 obs. of  10 variables:
##  $ carat  : num  0.23 0.21 0.23 0.29 0.31 0.24 0.24 0.26 0.22 0.23 ...
##  $ cut    : Ord.factor w/ 5 levels "Fair"<"Good"<..: 5 4 2 4 2 3 3 3 1 3 ...
##  $ color  : Ord.factor w/ 7 levels "D"<"E"<"F"<"G"<..: 2 2 2 6 7 7 6 5 2 5 ...
##  $ clarity: Ord.factor w/ 8 levels "I1"<"SI2"<"SI1"<..: 2 3 5 4 2 6 7 3 4 5 ...
##  $ depth  : num  61.5 59.8 56.9 62.4 63.3 62.8 62.3 61.9 65.1 59.4 ...
##  $ table  : num  55 61 65 58 58 57 57 55 61 61 ...
##  $ price  : int  326 326 327 334 335 336 336 337 337 338 ...
##  $ x      : num  3.95 3.89 4.05 4.2 4.34 3.94 3.95 4.07 3.87 4 ...
##  $ y      : num  3.98 3.84 4.07 4.23 4.35 3.96 3.98 4.11 3.78 4.05 ...
##  $ z      : num  2.43 2.31 2.31 2.63 2.75 2.48 2.47 2.53 2.49 2.39 ...

Función ggplot

ggplot(data = diamonds)

Esta función no da error, pero no vemos ninguna gráfica. Esto se debe a que no hemos indicado a ggplot qué tiene que hacer con los datos, cómo tiene que convertirlos en elementos gráficos.

Añadir argumentos a ggplot

ggplot(data = diamonds, mapping = aes(x = cut, y = price))

En este caso, además de indicar el conjunto de datos con el que trabajar, mapeamos las variables cut y price a variables gráficas (posición en los ejes x e y respectivamente). Este mapeo se realiza gracias a la función aes(), abreviación de aesthetics (lista de argumentos aes disponibles en ggplot2).

Sin embargo, seguimos sin ver ninguna gráfica (aunque ya aparecen elementos auxiliares, como los ejes, que se han podido computar porque hemos indicado las variables para dichos ejes). Esto se debe a que todavía no hemos indicado qué tipo de marca gráfica queremos utilizar para crearla.

Añadir geoms a ggplot

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_point()

Para añadir marcas gráficas con funciones geom (u otras funciones de ggplot) tenemos que añadirlas con el signo + al final de cada función.

Los argumentos estéticos están relacionados y dependen de la función geom que vayamos a usar. Por ejemplo, podemos mapear el argumento estético shape a una variable si usamosgeom_point, pero no funciona con geom_line

Modificar los argumentos por defecto

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_point(shape=21)

Vincular correctamente variables a aesthetics

Importante: si usamos un argumento estético dentro de la función aes, hay que vincularlo a una variable (normalmente un factor), por lo que el aspecto de ese argumento variará dependiendo del valor de la variable; por ejemplo, si tenemos una variable con los valores menor de edad|mayor de edad y la vinculamos dentro de aes a colour, ggplot asignará un color a menor de edad y otro a mayor de edad

Sin embargo, si asignamos ese mismo parámetro fuera de aes, ggplot asignará un único valor a todas las observaciones.

ggplot(data = diamonds, mapping = aes(x = cut, y = price, colour = color)) +
  geom_point()

En el ejemplo anterior la variable color está correctamente vinculada con el aesthetics colour; en el ejemplo siguiente, sin embargo, la vinculación no es correcta:

ggplot(data = diamonds, mapping = aes(x = cut, y = price), colour = color) +
  geom_point()

Usemos otros geoms

Dependiendo del tamaño que le demos al gráfico (especialmente si le damos poca altura), puede llegar a parecer que tenemos líneas uniformes en los distintos niveles de la variable cut, pero lo que tenemos son 53940 puntos que se superponen; estamos ante un claro ejemplo de overploting. Aunque la gramática de esta gráfica es correcta, la gráfica resultante no resulta significativa, por lo que para poder conocer mejor la relación entre las variables cut y price deberíamos optar por otra visualización.

Como en este caso nos interesa ver cuál es la distribución de los valores de la variable price por cut, podemos utilizar algunas de las gráficas diseñadas para tal efecto. En este caso, bastaría con cambiar la función geom (lista con todos los geoms disponibles en ggplot)

Gráfica tipo boxplot

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_boxplot()

Gráfica tipo violin

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_violin()

Todas las funciones geom tienen unas serie de parámetros marcados por defecto que podemos modificar, tal y como veremos más adelante.

Asimismo, las funciones geomtienen asociada una función stat por defecto, que se encarga de realizar los cálculos pertinentes para obtener las variables necesarias para dibujar el geomcorrespondiente (por ejemplo, en el caso de geom_boxplot, la función stat_boxplot calcula los valores ymin, lower, middle… (ver referencia online).

Ejemplos con varias capas

Tal y como indica el concepto layered, ggplot nos permite ir añadiendo capas de elementos gráficos una sobre otra. Cada función geom genera una nueva capa gráfica, que se van apilando una sobre la otra según aparecen en el código.

Siguiendo con el ejemplo anterior, las gráficas boxplot o violin no muestran todas las observaciones, sino que están diseñadas para ofrecer distintos estadísticos. Así, podemos combinar una capa que contenga una marca gráfica para cada una de las observaciones con una capa que ofrezca resúmenes estadísticos.

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_violin() +
  geom_point(alpha = 0.2, position = "jitter", shape = 21)

El parámetro position = "jitter" distribuye los puntos aleatoriamente en el eje x, sin que esa posición sea significativa. Es una opción para intentar evitar el problema de overploting ya mencionado.

El parámetro shape = 21 cambia el tipo de punto por defecto (circulo relleno de color) por un círculo en el que sólo se dibuja el perímetro. Esta es otra opción para intentar lidiar con el problema del overploting.

Partiendo de los mismos datos, si alteramos el orden de las capas y modificamos algunos parámetros gráficos podemos clarificar la gráfica y hacerla más fácil de interpretar.

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_point(alpha = 0.2, position = "jitter", shape = 21) +
  geom_violin(color = "red", alpha  = 0.2, size = 1)

Gráficas básicas

Histogramas y gráficas de barras

Histograma (una variable cuantitativa)

ggplot(data = diamonds, mapping = aes(x = price)) +
  geom_histogram()

stat_bin() using bins = 30. Pick better value with binwidth.

geom_histogram utiliza por defecto stat_bin para dibujar la altura de las barras. Podemos modificar el tamaño de los bins, indicando un número fijo de bins o bien su anchura (binwidth).

También podemos cambiar el estadístico utilizado para marcar la altura de las barras, y utilizar la densidad del total en lugar del conteo.

ggplot(data = diamonds, mapping = aes(x = price, y = ..density..)) +
  geom_histogram()

stat_bin() using bins = 30. Pick better value with binwidth.

Diagrama de barras (cuentas o proporciones de un factor)

ggplot(data = diamonds, mapping = aes(x = cut)) +
  geom_bar()

geom_bar utiliza la cuenta de valores para calcular la altura de cada barra. En el caso de que queramos vincular la altura a otra variable cuantitativa, tenemos que indicar dicha variable en aes, y cambiar el stat por defecto. En el ejemplo, creamos una nueva variable que es la media de precio para cada corte (fun.y = mean), y cambiamos el stata summary:

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_bar(stat="summary", fun.y=mean)

Diagrama de barras (una variable cuantitativa por una variable cualitativa)

Finalmente, podemos crear un conjunto de datos que ya contenga los cálculos que necesitamos antes de crear la gráfica, e utilizar dicho conjunto de datos en ggplot:

library(dplyr)
package 'dplyr' was built under R version 3.5.1

The following objects are masked from 'package:stats':
     filter, lag

The following objects are masked from 'package:base':
     intersect, setdiff, setequal, union

mediaprecio_corte <- diamonds %>%
  group_by(cut) %>%
  summarize(mediaPrecio = mean(price))

mediaprecio_corte

## # A tibble: 5 x 2
##   cut       mediaPrecio
##   <ord>           <dbl>
## 1 Fair            4359.
## 2 Good            3929.
## 3 Very Good       3982.
## 4 Premium         4584.
## 5 Ideal           3458.

ggplot(data = mediaprecio_corte, mapping = aes(x = cut, y = mediaPrecio)) +
  geom_col()

Gráficas de dispersión (scaterplot)

ggplot(data = diamonds, mapping = aes(x = carat, y = depth)) +
  geom_point()

ggplot(data = diamonds, mapping = aes(x = carat, y = depth)) +
  geom_point(alpha = 0.5, shape = 21)

Podemos mapear otras variables a un scaterplot: categóricas a color o forma, cuantitativas a tamaño (normalmente nos referimos a este tipo de gráfica como bubble chart).

ggplot(data = diamonds, mapping = aes(x = x, y = table, color = cut, size = z)) +
  geom_point(alpha = 0.2)

Gráficas de líneas

geom_freqpoly hace los mismos cáculos que geom_histogram, pero en lugar de dibujar barras dibuja líneas.

ggplot(data = diamonds, mapping = aes(x = price)) +
  geom_freqpoly()

stat_bin() using bins = 30. Pick better value with binwidth.

Podemos dividir la línea a partir de los niveles de un factor (variable cualitativa).

ggplot(data = diamonds, mapping = aes(x = price, group = cut)) +
  geom_freqpoly()

stat_bin() using bins = 30. Pick better value with binwidth.

O para facilitar la identificación de cada nivel, utilizar una variable gráfica como color o linetype (o incluso mapear la misma variable del conjunto de datos a dos variables gráficas, para reforzar la identificación de cada nivel).

ggplot(data = diamonds, mapping = aes(x = price, color = cut, linetype = cut)) +
  geom_freqpoly()

stat_bin() using bins = 30. Pick better value with binwidth.

Gráficas avanzadas

Facetas o small multiples

Una posible forma de evitar el problema del overplotting es subdividir la gráfica. La función facet_wrappermite indicar una faceta, es decir, un vector de tipo carácter (normalmente, una variable categórica del conjunto de datos).

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_point() +
  facet_wrap(~ color)

Si queremos cruzar dos variables categóricas, tenemos que suar la función facet_grid:

ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
  geom_point() +
  facet_grid(clarity ~ color)

Alterar la estructura de datos

Para poder crear algunas gráfica será necesario que reestructuremos el conjunto de datos para poder obtener el resultado deseado.

Heatmap

En este ejemplo, vamos a crear un heatmap que mostrará los nivels de la variable categórica cuten el eje y, y una serie de variables cuantitativas en el eje x. Como solo podemos mapear una variable a cada eje, para poder mostrar más de una en el eje x tendremos que “fundir” las variables y sus valores en pares clave-valor, con la función ‘gather’.

Una vez modificada la estructura, podemos crear el heatmap usando el geom_tile.

library(tidyr)

package 'tidyr' was built under R version 3.5.1

diamonds_Heatmap <- diamonds %>%
  group_by(cut) %>%
  summarise(depthPercent = mean(depth),
            table = mean(table),
            length = mean (x),
            width = mean(y),
            depth = mean(z)) %>%
            gather(variable, valor, -cut)

diamonds_Heatmap

## # A tibble: 25 x 3
##    cut       variable     valor
##    <ord>     <chr>        <dbl>
##  1 Fair      depthPercent  64.0
##  2 Good      depthPercent  62.4
##  3 Very Good depthPercent  61.8
##  4 Premium   depthPercent  61.3
##  5 Ideal     depthPercent  61.7
##  6 Fair      table         59.1
##  7 Good      table         58.7
##  8 Very Good table         58.0
##  9 Premium   table         58.7
## 10 Ideal     table         56.0
## # ... with 15 more rows

ggplot(data = diamonds_Heatmap, mapping = aes(x = variable, y = cut)) +
  geom_tile(aes(fill = valor), color = "white")

ggplot(data = diamonds_Heatmap, mapping = aes(x = variable, y = cut, label = sprintf("%0.2f", round(valor, digits = 2)))) +
  geom_tile(aes(fill = valor), color = "white") +
  geom_label()

Sobre la gráfica: Uno de los problemas principales de esta técnica consiste en que cada variable puede tener su propia escala; e, incluso teniendo una misma escala, los rangos de los valores pueden ser muy dispares. Para que la gráfica resulte más significativa tendríamos que normalizar los valores de las distintas variables (que ahora están bajo una única variable)

Jugando con el sistema de coordenadas

Coordenadas polares: Pie chart y radar chart

library(tidyr)
diamonds_Pie <- diamonds %>%
  group_by(cut) %>%
  summarise(TotalPrice = sum(price))

diamonds_Pie

## # A tibble: 5 x 2
##   cut       TotalPrice
##   <ord>          <int>
## 1 Fair         7017600
## 2 Good        19275009
## 3 Very Good   48107623
## 4 Premium     63221498
## 5 Ideal       74513487

ggplot(data = diamonds_Pie, mapping = aes(x = "", y = TotalPrice, fill = cut)) +
  geom_bar(width = 1, stat = "identity") +
  coord_polar(theta = "y")

library(tidyr)
diamonds_Radar <- diamonds %>%
  group_by(cut) %>%
  summarise(depthPercent = mean(depth),
            table = mean(table),
            length = mean (x),
            width = mean(y),
            depth = mean(z)) %>%
            gather(variable, valor, -cut)

diamonds_Radar

## # A tibble: 25 x 3
##    cut       variable     valor
##    <ord>     <chr>        <dbl>
##  1 Fair      depthPercent  64.0
##  2 Good      depthPercent  62.4
##  3 Very Good depthPercent  61.8
##  4 Premium   depthPercent  61.3
##  5 Ideal     depthPercent  61.7
##  6 Fair      table         59.1
##  7 Good      table         58.7
##  8 Very Good table         58.0
##  9 Premium   table         58.7
## 10 Ideal     table         56.0
## # ... with 15 more rows

ggplot(data = diamonds_Radar, mapping = aes(x = variable, y = valor)) +
  geom_polygon(mapping = aes(group = cut, color = cut), fill = NA) +
  coord_polar()

Coordenadas paralelas

ggplot(data = diamonds_Radar, mapping = aes(x = variable, y = valor)) +
  geom_line(mapping = aes(group = cut, color = cut))

Sobre la gráfica: Uno de los problemas principales de las gráficas de coordenadas paralelas consiste en que cada variable puede tener su propia escala; e, incluso teniendo una misma escala, los rangos de los valores pueden ser muy dispares. En la gráfica superior tenemos este segundo problema: las variables depth, length y width tienen un rango de valores similares, y también las variables depthPercent y table. Podemos normalizar los valores.
diamonds_Radar_filtrado <- diamonds_Radar %>%
  filter(variable %in% c("depth","length","width"))

ggplot(data = diamonds_Radar_filtrado, mapping = aes(x = variable, y = valor)) +
  geom_line(mapping = aes(group = cut, color = cut))

Recursos de interés

Bibliografía


Más posts