How to use ggplot to plot probability densities? - r

I am looking for the ggplot way to plot a probability density function (or any function). I used to use the old plot() function in R to do this. For example, to plot a beta distribution with alpha=1 and beta=1 (uniform):
x <- seq(0,1,length=100)
db <- dbeta(x, 1, 1)
plot(x, db, type='l')
How can I do it in ggplot?

ggplot2 has a stat_function() function to superimpose a function on a plot in much the same way as curve() does. I struggled a little bit to get this to work without generating the data until I realised how to use the variables produced by the statistic --- here ..y... The following is similar to what you would get with curve(dbeta(x, shape1 = 2, shape2 = 2), col = "red"):
require(ggplot2)
x <- seq(0, 1, len = 100)
p <- qplot(x, geom = "blank")
stat <- stat_function(aes(x = x, y = ..y..), fun = dbeta, colour="red", n = 100,
args = list(shape1 = 2, shape2 = 2))
p + stat

library(ggplot2)
x <- seq(0,1,length=100)
db <- dbeta(x, 1, 1)
You can use the qplot function within ggplot2 to make a quick plot
qplot(x, db, geom="line")
or you can add a geom_line layer to a ggplot
ggplot() + geom_line(aes(x,db))

Related

Using base R, how to create a "joy plot" (aka ridgeline plots), with many distributions on top of each other with vertical offset?

The type of plot I am trying to achieve in R seems to have been known as either as moving distribution, as joy plot or as ridgeline plot:
There is already a question in Stackoverflow whose recorded answer explains how to do it using ggplot: How to reproduce this moving distribution plot with R?
However, for learning purposes, I am trying to achieve the same using only base R plots (no lattice, no ggplot, no any plotting package).
In order to get started, I generated the following fake data to play with:
set.seed(2020)
shapes <- c(0.1, 0.5, 1, 2, 4, 5, 6)
dat <- lapply(shapes, function(x) rbeta(1000, x, x))
names(dat) <- letters[1:length(shapes)]
Then using mfrow I can achieve this:
par(mfrow=c(length(shapes), 1))
par(mar=c(1, 5, 1, 1))
for(i in 1:length(shapes))
{
values <- density(dat[[names(dat)[i]]])
plot(NA,
xlim=c(min(values$x), max(values$x)),
ylim=c(min(values$y), max(values$y)),
axes=FALSE,
main="",
xlab="",
ylab=letters[i])
polygon(values, col="light blue")
}
The result I get is:
Clearly, using mfrow (or even layout) here is not flexible enough and also does allow for the overlaps between the distributions.
Then, the question: how can I reproduce that type of plot using only base R plotting functions?
Here's a base R solution. First, we calculate all the density values and then manually offset off the y axis
vals <- Map(function(x, g, i) {
with(density(x), data.frame(x,y=y+(i-1), g))
}, dat, names(dat), seq_along(dat))
Then, to plot, we calculate the overall range, draw an empty plot, and the draw the densities (in reverse so they stack)
xrange <- range(unlist(lapply(vals, function(d) range(d$x))))
yrange <- range(unlist(lapply(vals, function(d) range(d$y))))
plot(0,0, type="n", xlim=xrange, ylim=yrange, yaxt="n", ylab="", xlab="Value")
for(d in rev(vals)) {
with(d, polygon(x, y, col="light blue"))
}
axis(2, at=seq_along(dat)-1, names(dat))
d = lapply(dat, function(x){
tmp = density(x)
data.frame(x = tmp$x, y = tmp$y)
})
d = lapply(seq_along(d), function(i){
tmp = d[[i]]
tmp$grp = names(d)[i]
tmp
})
d = do.call(rbind, d)
grp = unique(d$grp)
n = length(grp)
spcx = 5
spcy = 3
rx = range(d$x)
ry = range(d$y)
rx[2] = rx[2] + n/spcx
ry[2] = ry[2] + n/spcy
graphics.off()
plot(1, type = "n", xlim = rx, ylim = ry, axes = FALSE, ann = FALSE)
lapply(seq_along(grp), function(i){
x = grp[i]
abline(h = (n - i)/spcy, col = "grey")
axis(2, at = (n - i)/spcy, labels = grp[i])
polygon(d$x[d$grp == x] + (n - i)/spcx,
d$y[d$grp == x] + (n - i)/spcy,
col = rgb(0.5, 0.5, 0.5, 0.5))
})

Multiple Layers in ggplot2

I want to overlay a plot of an empirical cdf with a cdf of a normal distribution. I can only get the code to work without using ggplot.
rnd_nv1 <- rnorm(1000, 1.5, 0.5)
plot(ecdf(rnd_nv1))
lines(seq(0, 3, by=.1), pnorm(seq(0, 3, by=.1), 1.5, 0.5), col=2)
For ggplot to work I would need a single data frame, for example joining rnd_vn1 and pnorm(seq(0, 3, by=.1), 1.5, 0.5), col=2). This is a problem, because the function rnorm gives me just the function values without values on the domain. I don't even know how rnorm creates these, if I view the table I just see function values. But then again, magically, the plot of rnd_nv1 works.
The following plots the two lines but they overlap, since they are almost equal.
set.seed(1856)
x <- seq(0, 3, by = 0.1)
rnd_nv1 <- rnorm(1000, 1.5, 0.5)
dat <- data.frame(x = x, ecdf = ecdf(rnd_nv1)(x), norm = pnorm(x, 1.5, 0.5))
library(ggplot2)
long <- reshape2::melt(dat, id.vars = "x")
ggplot(long, aes(x = x, y = value, colour = variable)) +
geom_line()

Plot two densities in the same figure in R

I have two densities
N(µ = 1, σ2 = 1) and
N(µ = −3.5, σ2 = 3/4). I know I am have to use plot() and lines() but I am not sure how to convert the densities into functions. I am not even sure if that's what I have to do.
Any help would be appreciated. Thank you
You can use the dnorm() function, along with a sequence of numbers generated with seq() to get values to plot a pdf:
Get 5000 values between -10 and 10
x<-seq(-10,10,length=5000)
Calculate values - notice that dnorm() uses standard deviation and not the variance, so you need to take the square root of 0.75.
y<-dnorm(x,mean=0, sd=1)
z<-dnorm(x, mean = -3.5, sd = sqrt(0.75))
Plot first density in red with plot():
plot(x, y, type="l" , ylim = c(0,1), xlim = c(-8,8), col = "red")
Plot second one on top of first using the lines() function in blue:
lines(x,z, type = "l", col = "blue")
This code will plot you two densities in the same figure ;)
library(tidyverse)
seq(-10, 10, by = 0.1) %>%
tibble(x = .) %>%
mutate(D1 = dnorm(x, 1, 1),
D2 = dnorm(x, -3.5, 3/4)) %>%
gather(-x, key = Distribution, value = Value) %>%
ggplot(aes(x, Value)) +
geom_line(aes(color = Distribution))

r : ecdf over histogram

in R, with ecdf I can plot a empirical cumulative distribution function
plot(ecdf(mydata))
and with hist I can plot a histogram of my data
hist(mydata)
How I can plot the histogram and the ecdf in the same plot?
EDIT
I try make something like that
https://mathematica.stackexchange.com/questions/18723/how-do-i-overlay-a-histogram-with-a-plot-of-cdf
Also a bit late, here's another solution that extends #Christoph 's Solution with a second y-Axis.
par(mar = c(5,5,2,5))
set.seed(15)
dt <- rnorm(500, 50, 10)
h <- hist(
dt,
breaks = seq(0, 100, 1),
xlim = c(0,100))
par(new = T)
ec <- ecdf(dt)
plot(x = h$mids, y=ec(h$mids)*max(h$counts), col = rgb(0,0,0,alpha=0), axes=F, xlab=NA, ylab=NA)
lines(x = h$mids, y=ec(h$mids)*max(h$counts), col ='red')
axis(4, at=seq(from = 0, to = max(h$counts), length.out = 11), labels=seq(0, 1, 0.1), col = 'red', col.axis = 'red')
mtext(side = 4, line = 3, 'Cumulative Density', col = 'red')
The trick is the following: You don't add a line to your plot, but plot another plot on top, that's why we need par(new = T). Then you have to add the y-axis later on (otherwise it will be plotted over the y-axis on the left).
Credits go here (#tim_yates Answer) and there.
There are two ways to go about this. One is to ignore the different scales and use relative frequency in your histogram. This results in a harder to read histogram. The second way is to alter the scale of one or the other element.
I suspect this question will soon become interesting to you, particularly #hadley 's answer.
ggplot2 single scale
Here is a solution in ggplot2. I am not sure you will be satisfied with the outcome though because the CDF and histograms (count or relative) are on quite different visual scales. Note this solution has the data in a dataframe called mydata with the desired variable in x.
library(ggplot2)
set.seed(27272)
mydata <- data.frame(x= rexp(333, rate=4) + rnorm(333))
ggplot(mydata, aes(x)) +
stat_ecdf(color="red") +
geom_bar(aes(y = (..count..)/sum(..count..)))
base R multi scale
Here I will rescale the empirical CDF so that instead of a max value of 1, its maximum value is whatever bin has the highest relative frequency.
h <- hist(mydata$x, freq=F)
ec <- ecdf(mydata$x)
lines(x = knots(ec),
y=(1:length(mydata$x))/length(mydata$x) * max(h$density),
col ='red')
you can try a ggplot approach with a second axis
set.seed(15)
a <- rnorm(500, 50, 10)
# calculate ecdf with binsize 30
binsize=30
df <- tibble(x=seq(min(a), max(a), diff(range(a))/binsize)) %>%
bind_cols(Ecdf=with(.,ecdf(a)(x))) %>%
mutate(Ecdf_scaled=Ecdf*max(a))
# plot
ggplot() +
geom_histogram(aes(a), bins = binsize) +
geom_line(data = df, aes(x=x, y=Ecdf_scaled), color=2, size = 2) +
scale_y_continuous(name = "Density",sec.axis = sec_axis(trans = ~./max(a), name = "Ecdf"))
Edit
Since the scaling was wrong I added a second solution, calculatin everything in advance:
binsize=30
a_range= floor(range(a)) +c(0,1)
b <- seq(a_range[1], a_range[2], round(diff(a_range)/binsize)) %>% floor()
df_hist <- tibble(a) %>%
mutate(gr = cut(a,b, labels = floor(b[-1]), include.lowest = T, right = T)) %>%
count(gr) %>%
mutate(gr = as.character(gr) %>% as.numeric())
# calculate ecdf with binsize 30
df <- tibble(x=b) %>%
bind_cols(Ecdf=with(.,ecdf(a)(x))) %>%
mutate(Ecdf_scaled=Ecdf*max(df_hist$n))
ggplot(df_hist, aes(gr, n)) +
geom_col(width = 2, color = "white") +
geom_line(data = df, aes(x=x, y=Ecdf*max(df_hist$n)), color=2, size = 2) +
scale_y_continuous(name = "Density",sec.axis = sec_axis(trans = ~./max(df_hist$n), name = "Ecdf"))
As already pointed out, this is problematic because the plots you want to merge have such different y-scales. You can try
set.seed(15)
mydata<-runif(50)
hist(mydata, freq=F)
lines(ecdf(mydata))
to get
Although a bit late... Another version which is working with preset bins:
set.seed(15)
dt <- rnorm(500, 50, 10)
h <- hist(
dt,
breaks = seq(0, 100, 1),
xlim = c(0,100))
ec <- ecdf(dt)
lines(x = h$mids, y=ec(h$mids)*max(h$counts), col ='red')
lines(x = c(0,100), y=c(1,1)*max(h$counts), col ='red', lty = 3) # indicates 100%
lines(x = c(which.min(abs(ec(h$mids) - 0.9)), which.min(abs(ec(h$mids) - 0.9))), # indicates where 90% is reached
y = c(0, max(h$counts)), col ='black', lty = 3)
(Only the second y-axis is not working yet...)
In addition to previous answers, I wanted to have ggplot do the tedious calculation (in contrast to #Roman's solution, which was kindly enough updated upon my request), i.e., calculate and draw the histogram and calculate and overlay the ECDF. I came up with the following (pseudo code):
# 1. Prepare the plot
plot <- ggplot() + geom_hist(...)
# 2. Get the max value of Y axis as calculated in the previous step
maxPlotY <- max(ggplot_build(plot)$data[[1]]$y)
# 3. Overlay scaled ECDF and add secondary axis
plot +
stat_ecdf(aes(y=..y..*maxPlotY)) +
scale_y_continuous(name = "Density", sec.axis = sec_axis(trans = ~./maxPlotY, name = "ECDF"))
This way you don't need to calculate everything beforehand and feed the results to ggpplot. Just lay back and let it do everything for you!

Plotting two Poisson processes on one plot

I have two Poisson processes:
n <- 100
x <- seq(0, 10, length = 1000)
y1 <- cumsum(rpois(1000, 1 / n))
y2 <- -cumsum(rpois(1000, 1 / n))
I would like to plot them in one plot and expect that y1 lies above x-axis and y2 lies below x-axis. I tried the following code:
plot(x, y1)
par(new = TRUE)
plot(x, y2, col = "red",
axes = FALSE,
xlab = '', ylab = '',
xlim = c(0, 10), ylim = c(min(y2), max(y1)))
but it did not work. Can someone please tell me how to fix this? (I am working with R for my code)
Many thanks in advance
How about
plot(x,y1, ylim=range(y1,y2), type="l")
lines(x, y2, col="red")
I would suggest trying to avoid multiple calls to plot with par(new=TRUE). That is usually very messy. Here we use lines() to add to an existing plot. The only catch is that the x and y limits won't change based on the new data, so we use ylim in the first plot() call to set a range appropriate for all the data.
Or if you don't want to worry about limits (like MrFlick mentioned) or the number of lines, you could also tide up your data and using melt and ggplot
df <- data.frame(x, y1, y2)
library(reshape2)
library(ggplot2)
mdf <- melt(df, "x")
ggplot(mdf, aes(x, value, color = variable)) +
geom_line()

Resources