Создание geom / stat с нуля
Давний тайник, первый раз постер!
Я только недавно начал работать с R, и сейчас я пытаюсь улучшить свои навыки визуализации. То, что я хочу сделать, - это создать коробочные графики со средними бриллиантами в виде слоя сверху (см. Рисунок в ссылке ниже). Я не нашел ни одной функции, которая бы это делала, поэтому, думаю, мне нужно создать ее самому.
Я надеялся создать geom или stat, которые позволили бы что-то вроде этого работать:
ggplot(data, aes(...))) +
geom_boxplot(...) +
geom_meanDiamonds(...)
Я понятия не имею, с чего начать, чтобы построить эту новую функцию. Я знаю, какие значения необходимы для средних алмазов (среднее значение и доверительный интервал), но я не знаю, как построить геом / статистику, которая берет данные из ggplot()
, вычисляет среднее значение и CI для каждой группы и наносит средний бриллиант на вершину каждого коробочного графика.
Я искал подробные описания того, как создавать функции такого типа с нуля, однако я не нашел ничего, что действительно начиналось бы снизу. Я был бы очень признателен, если бы кто-нибудь мог указать мне на несколько полезных руководств.
Спасибо!
1 ответ
В настоящее время я учусь писать geoms самостоятельно, так что это будет довольно длинная и бессвязная статья, поскольку я прохожу свои мыслительные процессы, распутывая аспекты Geom (создавая многоугольники и отрезки) из аспектов Stats (вычисляя, где эти многоугольники & сегменты должны быть) из Geom.
Отказ от ответственности: я не знаком с этим видом заговора, и Google не бросил много авторитетных руководств. Мое понимание того, как доверительный интервал рассчитывается / используется здесь, может быть неверным.
Шаг 0. Понять взаимосвязь между geom / stat и функцией layer.
geom_boxplot
а также stat_boxplot
являются примерами функций слоя. Если вы введете их в консоль R, вы увидите, что они (относительно) короткие и не содержат фактического кода для расчета коробок / усов коробчатого графика. Вместо, geom_boxplot
содержит строку, которая говорит geom = GeomBoxplot
, в то время как stat_boxplot
содержит строку, которая говорит stat = StatBoxplot
(воспроизведено ниже).
> stat_boxplot
function (mapping = NULL, data = NULL, geom = "boxplot", position = "dodge2",
..., coef = 1.5, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE)
{
layer(data = data, mapping = mapping, stat = StatBoxplot,
geom = geom, position = position, show.legend = show.legend,
inherit.aes = inherit.aes, params = list(na.rm = na.rm,
coef = coef, ...))
}
GeomBoxplot
а также StatBoxplot
являются объектами ggproto Они там, где происходит волшебство.
Шаг 1. Признать, что ggproto()
"s _inherit
Параметр ваш друг.
Не изобретай велосипед. Поскольку мы хотим создать что-то, что хорошо сочетается с блокпостом, мы можем взять ссылку из Geom / Stat, использованного для этого, и изменить только то, что необходимо.
StatMeanDiamonds <- ggproto(
`_class` = "StatMeanDiamonds",
`_inherit` = StatBoxplot,
... # add functions here to override those defined in StatBoxplot
)
GeomMeanDiamonds <- ggproto(
`_class` = "GeomMeanDiamonds",
`_inherit` = GeomBoxplot,
... # as above
)
Шаг 2. Модифицируйте стат.
В StatBoxplot определены 3 функции: setup_data
, setup_params
, а также compute_group
, Вы можете обратиться к коду на Github (ссылка выше) для деталей или просмотреть их, введя, например, StatBoxplot$compute_group
,
compute_group
Функция вычисляет значения ymin / lower / middle / upper / ymax для всех значений y, связанных с каждой группой (то есть с каждым уникальным значением x), которые используются для построения графика на диаграмме. Мы можем переопределить его одним, который вычисляет доверительный интервал и средние значения вместо этого:
# ci is added as a parameter, to allow the user to specify different confidence intervals
compute_group_new <- function(data, scales, width = NULL,
ci = 0.95, na.rm = FALSE){
a <- mean(data$y)
s <- sd(data$y)
n <- sum(!is.na(data$y))
error <- qt(ci + (1-ci)/2, df = n-1) * s / sqrt(n)
stats <- c("lower" = a - error, "mean" = a, "upper" = a + error)
if(length(unique(data$x)) > 1) width <- diff(range(data$x)) * 0.9
df <- as.data.frame(as.list(stats))
df$x <- if(is.factor(data$x)) data$x[1] else mean(range(data$x))
df$width <- width
df
}
(Необязательно) StatBoxplot предоставляет пользователю возможность включить weight
в качестве эстетического отображения. Мы также можем учесть это, заменив:
a <- mean(data$y)
s <- sd(data$y)
n <- sum(!is.na(data$y))
с:
if(!is.null(data$weight)) {
a <- Hmisc::wtd.mean(data$y, weights = data$weight)
s <- sqrt(Hmisc::wtd.var(data$y, weights = data$weight))
n <- sum(data$weight[!is.na(data$y) & !is.na(data$weight)])
} else {
a <- mean(data$y)
s <- sd(data$y)
n <- sum(!is.na(data$y))
}
Нет необходимости менять другие функции в StatBoxplot. Таким образом, мы можем определить StatMeanDiamonds следующим образом:
StatMeanDiamonds <- ggproto(
`_class` = "StatMeanDiamonds",
`_inherit` = StatBoxplot,
compute_group = compute_group_new
)
Шаг 3. Измените Geom.
GeomBoxplot имеет 3 функции: setup_data
, draw_group
, а также draw_key
, Он также включает в себя определения default_aes()
а также required_aes()
,
Так как мы изменили исходный источник данных (данные, созданные StatMeanDiamonds, содержат вычисленные столбцы "нижний" / "средний" / "верхний", в то время как данные, полученные StatBoxplot, содержали бы вычисленные столбцы "ymin" / "нижний" / "middle" / "upper" / "ymax"), убедитесь, что ниже по течению setup_data
функция также влияет. (В этом случае, GeomBoxplot$setup_data
не ссылается на затронутые столбцы, поэтому никаких изменений здесь не требуется)
draw_group
Функция принимает данные, созданные StatMeanDiamonds и устанавливается setup_data
и создает несколько фреймов данных. "common" содержит эстетические отображения, общие для всех геомов. "diamond.df" для отображений, которые вносят вклад в многоугольник алмазов, и "plot.df" для отображений, которые вносят вклад в горизонтальный отрезок в среднем. Кадры данных затем передаются в draw_panel
функции GeomPolygon и GeomSegment, соответственно, для создания фактических полигонов / отрезков.
draw_group_new = function(data, panel_params, coord,
varwidth = FALSE){
common <- data.frame(colour = data$colour,
size = data$size,
linetype = data$linetype,
fill = alpha(data$fill, data$alpha),
group = data$group,
stringsAsFactors = FALSE)
diamond.df <- data.frame(x = c(data$x, data$xmax, data$x, data$xmin),
y = c(data$upper, data$mean, data$lower, data$mean),
alpha = data$alpha,
common,
stringsAsFactors = FALSE)
segment.df <- data.frame(x = data$xmin, xend = data$xmax,
y = data$mean, yend = data$mean,
alpha = NA,
common,
stringsAsFactors = FALSE)
ggplot2:::ggname("geom_meanDiamonds",
grid::grobTree(
GeomPolygon$draw_panel(diamond.df, panel_params, coord),
GeomSegment$draw_panel(segment.df, panel_params, coord)
))
}
draw_key
Функция используется для создания легенды для этого слоя, в случае необходимости. Поскольку GeomMeanDiamonds наследуется от GeomBoxplot, по умолчанию draw_key = draw_key_boxplot
и нам не нужно это менять. Если оставить его без изменений, код не будет нарушен. Тем не менее, я думаю, что более простая легенда, такая как draw_key_polygon
предлагает менее загроможденный взгляд.
GeomBoxplot-х default_aes
технические характеристики выглядят хорошо. Но нам нужно изменить required_aes
поскольку данные, которые мы ожидаем получить от StatMeanDiamonds, отличаются ("нижний" / "средний" / "верхний" вместо "ymin" / "нижний" / "средний" / "верхний" / "ymax").
Теперь мы готовы определить GeomMeanDiamonds:
GeomMeanDiamonds <- ggproto(
"GeomMeanDiamonds",
GeomBoxplot,
draw_group = draw_group_new,
draw_key = draw_key_polygon,
required_aes = c("x", "lower", "upper", "mean")
)
Шаг 4. Определите функции слоя.
Это скучная часть. Я скопировал с geom_boxplot
/ stat_boxplot
непосредственно, удалив все ссылки на выбросы в geom_meanDiamonds
меняется на geom = GeomMeanDiamonds
/ stat = StatMeanDiamonds
и добавление ci = 0.95
в stat_meanDiamonds
,
geom_meanDiamonds <- function(mapping = NULL, data = NULL,
stat = "meanDiamonds", position = "dodge2",
..., varwidth = FALSE, na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE){
if (is.character(position)) {
if (varwidth == TRUE) position <- position_dodge2(preserve = "single")
} else {
if (identical(position$preserve, "total") & varwidth == TRUE) {
warning("Can't preserve total widths when varwidth = TRUE.", call. = FALSE)
position$preserve <- "single"
}
}
layer(data = data, mapping = mapping, stat = stat,
geom = GeomMeanDiamonds, position = position,
show.legend = show.legend, inherit.aes = inherit.aes,
params = list(varwidth = varwidth, na.rm = na.rm, ...))
}
stat_meanDiamonds <- function(mapping = NULL, data = NULL,
geom = "meanDiamonds", position = "dodge2",
..., ci = 0.95,
na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) {
layer(data = data, mapping = mapping, stat = StatMeanDiamonds,
geom = geom, position = position, show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ci = ci, ...))
}
Шаг 5. Проверьте вывод.
# basic
ggplot(iris,
aes(Species, Sepal.Length)) +
geom_boxplot() +
geom_meanDiamonds()
# with additional parameters, to see if they break anything
ggplot(iris,
aes(Species, Sepal.Length)) +
geom_boxplot(width = 0.8) +
geom_meanDiamonds(aes(fill = Species),
color = "red", alpha = 0.5, size = 1,
ci = 0.99, width = 0.3)