borders <- function(database = "world", regions = ".", fill = NA,
colour = "grey50", ...) {
df <- map_data(database, regions)
geom_polygon(
aes_(~long, ~lat, group = ~group),
data = df, fill = fill, colour = colour, ...,
inherit.aes = FALSE, show.legend = FALSE
)
}
18 Programar con ggplot2
You are reading the work-in-progress third edition of the ggplot2 book. This chapter is currently a dumping ground for ideas, and we don’t recommend reading it.
18.1 Introductción
Un requisito importante para un buen análisis de datos es la flexibilidad. Si sus datos cambian o descubre algo que le hace reconsiderar sus suposiciones básicas, debe poder cambiar fácilmente muchos gráficos a la vez. El principal inhibidor de la flexibilidad es la duplicación de código. Si tienes la misma frase argumental repetida una y otra vez, tendrás que hacer el mismo cambio en muchos lugares diferentes. ¡A menudo la sola idea de hacer todos esos cambios es agotadora! Este capítulo le ayudará a superar ese problema mostrándole cómo programar con ggplot2.
Para que su código sea más flexible, debe reducir el código duplicado escribiendo funciones. Cuando notes que estás haciendo lo mismo una y otra vez, piensa en cómo podrías generalizarlo y convertirlo en una función. Si no está muy familiarizado con cómo funcionan las funciones en R, es posible que desee mejorar sus conocimientos en https://adv-r.hadley.nz/functions.html.
En este capítulo mostraremos cómo escribir funciones que crean:
- Un único componente ggplot2.
- Múltiples componentes de ggplot2.
- Una trama completa.
Y luego terminaremos con una breve ilustración de cómo se pueden aplicar técnicas de programación funcional a objetos ggplot2.
También puede que le resulten útiles los paquetes cowplot y ggthemes. Además de proporcionar componentes reutilizables que le ayudarán directamente, también puede leer el código fuente de los paquetes para descubrir cómo funcionan.
18.2 Componentes individuales
Cada componente de un gráfico ggplot es un objeto. La mayoría de las veces usted crea el componente y lo agrega inmediatamente a un gráfico, pero no es necesario. En su lugar, puede guardar cualquier componente en una variable (dándole un nombre) y luego agregarlo a varios gráficos:
bestfit <- geom_smooth(
method = "lm",
se = FALSE,
colour = alpha("steelblue", 0.5),
linewidth = 2
)
ggplot(mpg, aes(cty, hwy)) +
geom_point() +
bestfit
#> `geom_smooth()` using formula = 'y ~ x'
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
bestfit
#> `geom_smooth()` using formula = 'y ~ x'
Esta es una excelente manera de reducir los tipos simples de duplicación (¡es mucho mejor que copiar y pegar!), pero requiere que el componente sea exactamente el mismo cada vez. Si necesita más flexibilidad, puede envolver estos fragmentos reutilizables en una función. Por ejemplo, podríamos extender nuestro objeto bestfit
a una función más general para agregar líneas de mejor ajuste a un gráfico. El siguiente código crea un geom_lm()
con tres parámetros: el modelo formula
, la línea colour
y el linewidth
:
geom_lm <- function(formula = y ~ x, colour = alpha("steelblue", 0.5),
linewidth = 2, ...) {
geom_smooth(formula = formula, se = FALSE, method = "lm", colour = colour,
linewidth = linewidth, ...)
}
ggplot(mpg, aes(displ, 1 / hwy)) +
geom_point() +
geom_lm()
ggplot(mpg, aes(displ, 1 / hwy)) +
geom_point() +
geom_lm(y ~ poly(x, 2), linewidth = 1, colour = "red")
Preste mucha atención al uso de “...
”. Cuando se incluye en la definición de función “...
” permite que una función acepte argumentos adicionales arbitrarios. Dentro de la función, puedes usar “...
” para pasar esos argumentos a otra función. Aquí pasamos “...
” a geom_smooth()
para que el usuario aún pueda modificar todos los demás argumentos que no hemos anulado explícitamente. Cuando escribes tus propias funciones de componentes, es una buena idea usar siempre “...
” de esta manera.
Finalmente, tenga en cuenta que sólo puede agregar componentes a un gráfico; no puede modificar ni eliminar objetos existentes.
18.2.1 Ejercicios
Cree un objeto que represente un histograma rosa con 100 contenedores.
Cree un objeto que represente una escala de relleno con la paleta Blues ColorBrewer.
Lea el código fuente de
theme_grey()
. ¿Cuáles son sus argumentos? ¿Como funciona?Cree
scale_colour_wesanderson()
. Debería tener un parámetro para elegir la paleta del paquete wesanderson y crear una escala continua o discreta.
18.3 Múltiples componentes
No siempre es posible lograr tus objetivos con un solo componente. Afortunadamente, ggplot2 tiene una manera conveniente de agregar múltiples componentes a un gráfico en un solo paso con una lista. La siguiente función agrega dos capas: una para mostrar la media y otra para mostrar su intervalo de confianza del 95%:
geom_mean <- function() {
list(
stat_summary(fun = "mean", geom = "bar", fill = "grey70"),
stat_summary(fun.data = "mean_cl_normal", geom = "errorbar", width = 0.4)
)
}
ggplot(mpg, aes(class, cty)) + geom_mean()
ggplot(mpg, aes(drv, cty)) + geom_mean()
Si la lista contiene elementos NULL
, se ignoran. Esto facilita la adición condicional de componentes:
geom_mean <- function(se = TRUE) {
list(
stat_summary(fun = "mean", geom = "bar", fill = "grey70"),
if (se)
stat_summary(fun.data = "mean_cl_normal", geom = "errorbar", width = 0.4)
)
}
ggplot(mpg, aes(drv, cty)) + geom_mean()
ggplot(mpg, aes(drv, cty)) + geom_mean(se = FALSE)
18.3.1 Componentes de la trama
No estás limitado sólo a agregar capas de esta manera. También puede incluir cualquiera de los siguientes tipos de objetos en la lista:
Un marco de datos, que anulará el conjunto de datos predeterminado asociado con el gráfico. (Si agrega un marco de datos por sí solo, necesitará usar
%+%
, pero esto no es necesario si el marco de datos está en una lista).Un objeto
aes()
, que se combinará con el mapeo estético predeterminado existente.Escalas, que anulan las escalas existentes, con una advertencia si ya han sido configuradas por el usuario.
Sistemas de coordenadas y especificación de facetas, que anulan la configuración existente.
Componentes del tema, que anulan los componentes especificados.
18.3.2 Anotación
A menudo resulta útil agregar anotaciones estándar a un gráfico. En este caso, su función también establecerá los datos en la función de capa, en lugar de heredarlos del gráfico. Hay otras dos opciones que debes configurar al hacer esto. Estos garantizan que la capa sea autónoma:
inherit.aes = FALSE
evita que la capa herede la estética del trazado principal. Esto garantiza que su anotación funcione independientemente de lo que haya en el gráfico.show.legend = FALSE
garantiza que su anotación no aparecerá en la leyenda.
Un ejemplo de esta técnica es la función borders()
integrada en ggplot2. Está diseñado para agregar bordes de mapas desde uno de los conjuntos de datos del paquete de mapas:
18.3.3 Argumentos adicionales
Si desea pasar argumentos adicionales a los componentes de su función, ...
no sirve: no hay forma de dirigir diferentes argumentos a diferentes componentes. En su lugar, deberá pensar en cómo desea que funcione su función, equilibrando los beneficios de tener una función que lo haga todo con el costo de tener una función compleja que sea más difícil de entender.
Para comenzar, aquí hay un enfoque que utiliza modifyList()
y do.call()
:
geom_mean <- function(..., bar.params = list(), errorbar.params = list()) {
params <- list(...)
bar.params <- modifyList(params, bar.params)
errorbar.params <- modifyList(params, errorbar.params)
bar <- do.call("stat_summary", modifyList(
list(fun = "mean", geom = "bar", fill = "grey70"),
bar.params)
)
errorbar <- do.call("stat_summary", modifyList(
list(fun.data = "mean_cl_normal", geom = "errorbar", width = 0.4),
errorbar.params)
)
list(bar, errorbar)
}
ggplot(mpg, aes(class, cty)) +
geom_mean(
colour = "steelblue",
errorbar.params = list(width = 0.5, linewidth = 1)
)
ggplot(mpg, aes(class, cty)) +
geom_mean(
bar.params = list(fill = "steelblue"),
errorbar.params = list(colour = "blue")
)
Si necesita un comportamiento más complejo, podría ser más fácil crear una geom o estadística personalizada. Puede obtener más información sobre esto en la viñeta extensible de ggplot2 incluida con el paquete. Léelo corriendo vignette("extending-ggplot2")
.
18.3.4 Ejercicios
Para aprovechar al máximo el espacio, muchos ejemplos de este libro ocultan las etiquetas y la leyenda de los ejes. Acabamos de copiar y pegar el mismo código en varios lugares, pero tendría más sentido crear una función reutilizable. ¿Cómo sería esa función?
Extienda la función
borders()
para agregar tambiéncoord_quickmap()
al gráfico.Revise su propio código. ¿Qué combinaciones de geoms o escalas usas todo el tiempo? ¿Cómo podrías extraer el patrón en una función reutilizable?
18.4 Funciones de trazado
La creación de pequeños componentes reutilizables está más en línea con el espíritu de ggplot2: puedes recombinarlos de manera flexible para crear cualquier trama que desees. Pero a veces estás creando la misma trama una y otra vez y no necesitas esa flexibilidad. En lugar de crear componentes, es posible que desee escribir una función que tome datos y parámetros y devuelva un gráfico completo.
Por ejemplo, podrías resumir el código completo necesario para hacer un gráfico circular:
piechart <- function(data, mapping) {
ggplot(data, mapping) +
geom_bar(width = 1) +
coord_polar(theta = "y") +
xlab(NULL) +
ylab(NULL)
}
piechart(mpg, aes(factor(1), fill = class))
Esto es mucho menos flexible que el enfoque basado en componentes, pero igualmente es mucho más conciso. Tenga en cuenta que tuvimos cuidado de devolver el objeto de la trama, en lugar de imprimirlo. Eso hace posible agregar otros componentes de ggplot2.
Puede adoptar un enfoque similar para dibujar gráficos de coordenadas paralelas (PCP). Los PCP requieren una transformación de los datos, por lo que recomendamos escribir dos funciones: una que haga la transformación y otra que genere el gráfico. Mantener estas dos piezas separadas hace la vida mucho más fácil si luego deseas reutilizar la misma transformación para una visualización diferente.
pcp_data <- function(df) {
is_numeric <- vapply(df, is.numeric, logical(1))
# Cambiar la escala de columnas numéricas
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
df[is_numeric] <- lapply(df[is_numeric], rescale01)
# Agregar identificador de fila
df$.row <- rownames(df)
# Trate los números como variables de valor (como medidas)
# gather_ es la versión estadandar de evaluación de gather
# y es más fácil de utilizar para programar
tidyr::gather_(df, "variable", "value", names(df)[is_numeric])
}
pcp <- function(df, ...) {
df <- pcp_data(df)
ggplot(df, aes(variable, value, group = .row)) + geom_line(...)
}
pcp(mpg)
#> Warning: `gather_()` was deprecated in tidyr 1.2.0.
#> ℹ Please use `gather()` instead.
pcp(mpg, aes(colour = drv))
#> Warning: `gather_()` was deprecated in tidyr 1.2.0.
#> ℹ Please use `gather()` instead.
18.4.1 Refiriéndose indirectamente a variables.
La función piechart()
anterior es un poco poco atractiva porque requiere que el usuario conozca la especificación exacta aes()
que genera un gráfico circular. Sería más conveniente si el usuario pudiera simplemente especificar el nombre de la variable a trazar. Para hacer eso necesitarás aprender un poco más sobre cómo funciona aes()
.
aes()
usa evaluación ordenada: en lugar de mirar los valores de sus argumentos, mira sus expresiones. Esto dificulta la programación porque cuando desea que se refiera a una variable proporcionada en un argumento, utiliza el nombre del argumento:
my_function <- function(x_var) {
aes(x = x_var)
}
my_function(abc)
#> Aesthetic mapping:
#> * `x` -> `x_var`
Esto lo resolvemos utilizando la técnica estándar de programación con tidy-evaluación: abrazar. Abrazar le dice a gglot2 que mire “dentro” del argumento y use su valor, no su nombre literal:
my_function <- function(x_var) {
aes(x = {{ x_var }})
}
my_function(abc)
#> Aesthetic mapping:
#> * `x` -> `abc`
Esto facilita la actualización de nuestra función de gráfico circular:
piechart <- function(data, var) {
ggplot(data, aes(factor(1), fill = {{ var }})) +
geom_bar(width = 1) +
coord_polar(theta = "y") +
xlab(NULL) +
ylab(NULL)
}
mpg |> piechart(class)
18.4.2 Ejercicios
Cree una función
distribution()
especialmente diseñada para visualizar distribuciones continuas. Permita que el usuario proporcione un conjunto de datos y el nombre de una variable para visualizar. Permítales elegir entre histogramas, polígonos de frecuencia y gráficos de densidad. ¿Qué otros argumentos le gustaría incluir?¿Qué argumentos adicionales debería tomar
pcp()
? ¿Cuáles son las desventajas de cómo se usa...
en el código actual?
18.5 Programación funcional
Dado que los objetos ggplot2 son simplemente objetos R normales, puedes ponerlos en una lista. Esto significa que puede aplicar todas las excelentes herramientas de programación funcional de R. Por ejemplo, si quisieras agregar diferentes geoms al mismo gráfico base, podrías ponerlas en una lista y usar lapply()
.
geoms <- list(
geom_point(),
geom_boxplot(aes(group = cut_width(displ, 1))),
list(geom_point(), geom_smooth())
)
p <- ggplot(mpg, aes(displ, hwy))
lapply(geoms, function(g) p + g)
#> [[1]]
#>
#> [[2]]
#>
#> [[3]]
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Si no está familiarizado con la programación funcional, lea https://adv-r.hadley.nz/fp.html y piense en cómo podría aplicar las técnicas a su código de trazado duplicado.
18.5.1 Ejercicios
-
¿Cómo podrías agregar una capa
geom_point()
a cada elemento de la siguiente lista?plots <- list( ggplot(mpg, aes(displ, hwy)), ggplot(diamonds, aes(carat, price)), ggplot(faithfuld, aes(waiting, eruptions, size = density)) )
-
¿Qué hace la siguiente función? ¿Cuál es un mejor nombre para ello?