Reputation: 4155
I am trying to create a scatterplot with binned x-axis for binary data. When I use geom_point
with binary y, the plot is pretty useless (see figure 1). As shown in figure 2, I want to bin the data based on the values of the x-axis and then plot the avg x and avg y within each bin using geom_point
(mapping the the number of obs in each bin to the size of the point). I can do this by aggregating the data but I was wondering whether ggplot can do it directly. I played around with stat_bindot
etc. but wasn't able to find a solution. Any ideas? Below is some code.
Thanks!
# simulate data
n=1000
y=rbinom(n,1,0.5)
x=runif(n)
data=data.frame(x,y)
# figure 1 - geom_point with binary data, pretty useless!
ggplot(data,aes(x=x,y=y)) + geom_point() + ylim(0,1)
# let's create an aggregated dataset with bins
bin=cut(data$x,seq(0,1,0.05))
# I am sure the aggregation can be done in a better way...
data.bin=aggregate(data,list(bin),function(x) { return(c(mean(x),length(x)))})
# figure 2 - geom_point with binned x-axis, much nicer!
ggplot(data.bin,aes(x=x[,1],y=y[,1],size=x[,2])) + geom_point() + ylim(0,1)
Figures 1 and 2:
Upvotes: 2
Views: 5858
Reputation: 6801
I wrote a new Stat function for this purpose.
It takes nbins
, bin_var
, bin_fun
and summary_fun
as arguments, with defaults for all four.
nbins
depends on the number of data points.bin_var
is "x". You can also set it to "y". This specifies the variable that is fed to bin_fun
.bin_fun
is the binning function. By default, it's seq_cut
which I wrote for the purpose. You can also write your own binning function. It just has to take data and nbins as arguments.summary_fun
is the summary function that is used to aggregate the bins. By default, it's mean
. You can also specify aggregating functions for x and y separately with fun.x
and fun.y
.ymin
and ymax
as aesthetics, you can also specify fun.ymin
and fun.ymax
.Note that if you specify aes(group = your_bins), bin_fun
is ignored and the grouping variable is used instead. Note also that it will create a count variable which can be accessed as ..count..
.
In your case, you use it like this:
p <- ggplot(data, aes(x, y)) +
geom_point(aes(size = ..count..), stat = "binner") +
ylim(0, 1)
Not very useful in this case (although this demonstrates homoskedasticity and that the variance is around 0.25 as befits the assumption of Bern(0.5) variates) but just for the example:
p + geom_linerange(stat = "binner",
fun.ymin = function(y) mean(y) - var(y) / 2,
fun.ymax = function(y) mean(y) + var(y) / 2)
The code:
library(proto)
stat_binner <- function (mapping = NULL, data = NULL, geom = "point", position = "identity", ...) {
StatBinner$new(mapping = mapping, data = data, geom = geom, position = position, ...)
}
StatBinner <- proto(ggplot2:::Stat, {
objname <- "binner"
default_geom <- function(.) GeomPoint
required_aes <- c("x", "y")
calculate_groups <- function(., data, scales, bin_var = "x", nbins = NULL, bin_fun = seq_cut, summary_fun = mean,
fun.data = NULL, fun.y = NULL, fun.ymax = NULL, fun.ymin = NULL,
fun.x = NULL, fun.xmax = NULL, fun.xmin = NULL, na.rm = FALSE, ...) {
data <- remove_missing(data, na.rm, c("x", "y"), name = "stat_binner")
# Same rules as binnedplot in arm package
n <- nrow(data)
if (is.null(nbins)) {
nbins <- if (n >= 100) floor(sqrt(n))
else if (n > 10 & n < 100) 10
else floor(n/2)
}
if (length(unique(data$group)) == 1) {
data$group <- bin_fun(data[[bin_var]], nbins)
}
if (!missing(fun.data)) {
# User supplied function that takes complete data frame as input
fun.data <- match.fun(fun.data)
fun <- function(df, ...) {
fun.data(df$y, ...)
}
} else {
if (!is.null(summary_fun)) {
if (!is.null(fun.x)) message("fun.x overriden by summary_fun")
if (!is.null(fun.y)) message("fun.y overriden by summary_fun")
fun.x <- fun.y <- summary_fun
}
# User supplied individual vector functions
fs_x <- compact(list(xmin = fun.x, x = fun.x, xmax = fun.xmax))
fs_y <- compact(list(ymin = fun.ymin, y = fun.y, ymax = fun.ymax))
fun <- function(df, ...) {
res_x <- llply(fs_x, function(f) do.call(f, list(df$x, ...)))
res_y <- llply(fs_y, function(f) do.call(f, list(df$y, ...)))
names(res_y) <- names(fs_y)
names(res_x) <- names(fs_x)
as.data.frame(c(res_y, res_x))
}
}
summarise_by_x_and_y(data, fun, ...)
}
})
summarise_by_x_and_y <- function(data, summary, ...) {
summary <- ddply(data, "group", summary, ...)
count <- ddply(data, "group", summarize, count = length(y))
unique <- ddply(data, "group", ggplot2:::uniquecols)
unique$y <- NULL
unique$x <- NULL
res <- merge(merge(summary, unique, by = "group"), count, by = "group")
# Necessary for, eg, colour aesthetics
other_cols <- setdiff(names(data), c(names(summary), names(unique)))
if (length(other_cols) > 0) {
other <- ddply(data[, c(other_cols, "group")], "group", numcolwise(mean))
res <- merge(res, other, by = "group")
}
res
}
seq_cut <- function(x, nbins) {
bins <- seq(min(x), max(x), length.out = nbins)
findInterval(x, bins, rightmost.closed = TRUE)
}
Upvotes: 5
Reputation: 58845
As @Kohske said, there is no direct way to do that in ggplot2
; you have to pre-summarize the data and pass that to ggplot
. Your approach works, but I would have done it slightly differently, using the plyr
package instead of aggregate
.
library("plyr")
data$bin <- cut(data$x,seq(0,1,0.05))
data.bin <- ddply(data, "bin", function(DF) {
data.frame(mean=numcolwise(mean)(DF), length=numcolwise(length)(DF))
})
ggplot(data.bin,aes(x=mean.x,y=mean.y,size=length.x)) + geom_point() +
ylim(0,1)
The advantage, in my opinion, is that you get a simple data frame with better names this way, rather than a data frame where some columns are matrices. But that is probably a matter of personal style than correctness.
Upvotes: 3