Showing posts with label inverting CDF. Show all posts
Showing posts with label inverting CDF. Show all posts

Wednesday, July 23, 2014

Making random draws from an arbitrarily defined pdf

I recently found myself in need of a function to sample randomly from an arbitrarily defined probability density function. An excellent post by Quantitations shows how to accomplish this using some of Rs fairly sophisticated functional approximation tools such as integrate and uniroot. The only problem with this excellent post was that the machine cost was enormous with samples of 1000 draws taking 5 seconds on my machine with repeated samples of 100,000+ draws (which I was after) clearly being unworkable.

Thus I decided to take my own crack at it. First let us review the basics of drawing random variables from non-uniform distributions. The standard method I think most algorithms use works as follows:

Assumptions
1. You can draw pseudo-random uniform variable u
2. You can integrate the pdf to construct a cdf
$$p = F(x) = \int_{-\infty}^\infty f(x) dx$$
3. You can invert the cdf in order to solve for p
$$G(F(x))=F^{-1}(F(x))=F^{-1}(p)=x$$

The method thus relies upon the somewhat simple method of calculating x by drawing u and plugging into G (the inverse of F).

Now let us assume that we are in a bit of a bind. We can neither integrate f(x) nor invert F(x). The previously mentioned post by Quantitations demonstrates how to do the operation in this case by directly approximating the cdf followed by approximating the inverse. This process is computationally intensive for simple functions and extremely time consuming for complex functions.

In contrast I approximate the cdf by drawing x values and associated probabilities from the pdf along a user specified range. I create a matrix of bins over which the approximate cdf is divided into. After that I apply some function (mean or median) over the range of x which corresponds to each bin providing an x point value for each cdf bin value.  The gacdf returns a list of results from this process.

The ricdf function then takes the list of returns and is able to draw values from the approximated inverse cdf. Once the gicdf has completed its operation, ricdf is able to generate variables nearly as fast as that of standard non-uniform random variables.

As a matter of comparison, I define the funciton f as the pdf of the normal (dnorm) in R and draw from it 1000 time. Using rnorm, ricdf [defined in this post], and samplepdf [defined at Quantitations] I graphically see how the three draws compare below.
Three random sampling procedures for the random normal.
On the graph, Black type=0, is rnorm. Dark blue type=1, is ricdf. Light blue, type=2 is samplepdf. Redrawing the graph several times, visually I could not tell the difference between the three methods.

Comparing run times of the three methods over ten repetitions rnorm used no seconds, ricdf (with ficdf) used .8 seconds, and samplepdf used 5. Because ricdf and gicdf are separate functions with gicdf setting up the table to draw from, we only need to set it up once per pdf so ricf can be benchmarked separately from gicfd. Comparing 1000 replications of rnorm and ricdf, rnorm took .1 seconds and ricdf took .11 seconds. I did not compare with samplepdf which I expected would take 5200 seconds (87 minutes).

For fun I have generated several other strange 'proportional' pdfs.  I say proportional because strictly speaking pdfs are required to integrate to 1. However, gicdf forces the probabilities to sum to 1 across the range making the formal requirement not necessary.

I define a piecewise pdf


And sample from it.


I also define a multimodal  pdf



And sample from it.

Find the R code below or the gist on github.

gicdf <- function(fun, 
                 min=-3.5, 
                 max=3.5, 
                 bins=1000, 
                 nqratio=10, 
                 grouping=mean, 
                 ...) {
  # Generate an inverse CDF of an arbitrary function
  fun <- match.fun(fun)
  grouping <- match.fun(grouping)
 
  # Number of points to draw
  nq=nqratio*bins
 
  # Draw indexes
  qdraw <- seq(min, max,length.out=nq)
 
  # Calculate proportional probability of each draw
  pdraw <- fun(qdraw,...)
 
  # Rescale probability sum to 1, rescale
  pdraw <- pdraw/sum(pdraw)
 
  # Calculate the cumulative probability at each qdraw
  cpdraw <- cumsum(pdraw)
 
  # Caculate the cumulative probability at each bin
  pbin <- (1:bins)/bins
  xbin <- NA*(1:bins)
 
  for (i in 1:bins) {
    xbin[i] <- grouping(qdraw[cpdraw<pbin[i]&cpdraw>0], na.rm = TRUE)
    cpdraw[cpdraw<pbin[i]] <- 2
  }
 
  (draw.set <- list(digits=floor(log10(bins)), xbin=xbin, pbin=pbin))
}
 
# Draw from acdf
ricdf <- function(N, draw.set) {
  digits <- draw.set$digits
  pdraws <- ceiling(runif(N)*10^digits)/10^digits
  draw.set$xbin[match(pdraws,draw.set$pbin)]
}
 
# Define an arbitrary pdf f: to compare lets start with normal pdf
f <- function(x) dnorm(x)
 
library(ggplot2)
 
# set the number to sample
nsamples <- 1000
 
# Draw from rnorm
sample0 <- cbind(draw=rnorm(nsamples), type=0)
 
# Draw inverted cdf distribution information for range between -4 and 4
# using mean approach
sample1 <- cbind(draw=ricdf(nsamples, gicdf(f,min=-4,max=4)), type=1)
 
# samplepdf and endsign defined on Quantitations blog
# begin Quantitations code
endsign <- function(f, sign = 1) {
  b <- sign
  while (sign * f(b) < 0) b <- 10 * b
  return(b)
}
samplepdf <- function(n, pdf, ..., spdf.lower = -Inf, spdf.upper = Inf) {
  vpdf <- function(v) sapply(v, pdf, ...)  # vectorize
  cdf <- function(x) integrate(vpdf, spdf.lower, x)$value
  invcdf <- function(u) {
    subcdf <- function(t) cdf(t) - u
    if (spdf.lower == -Inf) 
      spdf.lower <- endsign(subcdf, -1)
    if (spdf.upper == Inf) 
      spdf.upper <- endsign(subcdf)
    return(uniroot(subcdf, lower=spdf.lower, upper=spdf.upper)$root)
  }
  sapply(runif(n), invcdf)
}
# end Quantitations code
sample2 <- cbind(draw=samplepdf(nsamples, f), type=2)
 
# join the three samples together
samples <- as.data.frame(rbind(sample0, sample1, sample2))
 
ggplot(samples, aes(x=draw, color=type))+
  geom_density(aes(group=type, size=-type))
 
library(rbenchmark)
# Let us do some speed comparisons
benchmark(rnorm(nsamples), 
          ricdf(nsamples, gicdf(f,min=-4,max=4)),
          samplepdf(nsamples, f),
          replications=10)
# test replications elapsed relative user.self
#2 ricdf(nsamples, gicdf(f, min = -4, max = 4))   10    7.97
#1                              rnorm(nsamples)   10    0.00
#3                       samplepdf(nsamples, f)   10   52.39
 
# This sets the entire process of generating and drawing from ricdf as about 7 times 
# faster than samplepdf. However, if we seperate the gicdf function from the 
# ricdf function which only need be run each time a new pdf is input, then we can
# considerably increase run speeds.
 
myicdf <- gicdf(f,min=-4,max=4)
# Sets up the initial icdf to draw from. 
 
benchmark(rnorm(nsamples), ricdf(nsamples, myicdf), replications=1000)
# elapsed time for rnorm = .10 seconds and ricdf = .11
# thus drawing from ricdf can be extremely fast
 
# Now let's look at an unusual proportional pdf distribution
f <- function(x) {
  p <- rep(0, length(x))
  p[x>0&x<1] <- x[x>0&x<1]^2
  p[x>2&x<3] <- 3-x[x>2&x<3]^.5
  p[x>4&x<5] <- x[x>4&x<5]
  p
}
 
x <- seq(-1,6,.01)
plot(x,f(x), type='l', 
     xlab='Proportional Probability',
     main='Proportional pdf')
# A key thing to note is that the pdf does not need to integrate to 1.
# The gicdf function will rescale it to ensure it does.
# However, it should all be positive
 
# I define bins as equal to 10,000 when normally they are equal to 1000
myicdf <- gicdf(f,min=-1,max=6, bins=1000)
samples <- data.frame(draws=ricdf(100000,myicdf))
 
ggplot(samples, aes(x=draws))+
  geom_histogram(binwidth=.1)+
  aes(y = ..density..)
 
 
# Okay here is one more probability
f <- function(x) dnorm(x)*abs(1+x^4)
 
x <- seq(-5,5,.01)
plot(x,f(x), type='l', 
     xlab='Proportional Probability',
     main='Proportional pdf2')
 
 
myicdf <- gicdf(f,min=-5,max=5, bins=1000)
samples <- data.frame(draws=ricdf(100000,myicdf))
 
ggplot(samples, aes(x=draws))+
  geom_histogram(binwidth=.2)+
  aes(y = ..density..)
 
# I will leave you to define your own pdfs to draw from.
# If you end up using this method, please cite!
Created by Pretty R at inside-R.org




Saturday, November 3, 2012

Draw Multinomial Random Variables

Original Code

* It is a little tricky to draw multinomial random variables (joint binomial distribution) in Stata because Stata does not have a canned inverse CDF function for the binomial that inverts P values to x values.

* Fortunately in my last post I programmed an inverseCDF command (http://www.econometricsbysimulation.com/2012/11/inverting-binomial-distribution-in.html).

* I will load in the program for later use:

cap program drop CDFinv
program define CDFinv

  * Let's define the arguments of the program.
  * N is the total number of trials.
  * P is the CDF value that we would like inverted
  * p is the probability of one outcome being a success.
  * newvar is the name of the new variable to be generated.
  args newvar P N p

  * The challenging thing about trying to invert a CDF for a count distribution is that the CDF is a probability mass function with discontinuous regions.
  * However, the particularly nice thing about the binomial distribution is the the pool of outcomes is well defined. (1,2,3..N)
  * Thus we can simply count how many times the "steps" of the CDF fall below the target CDF level.
  cap drop `newvar'
  qui gen `newvar'=0
    label var `newvar' "Inverse CDF of binomial"

  forv k=1(1)`N' {
    * Add 1 to the variable value every time the CDF(n,k-1,p) is less than the target CDF value P.
qui replace `newvar'=`newvar'+1 if binomial(`N', `k'-1, `p')<(`P')
  }

end

* This post also draws on a previous post in which I show how to transform Stata multivariate normal random draws into into any correlated random variable (http://www.econometricsbysimulation.com/2012/09/drawing-jointly-distributed-non-normal.html).

set more off

clear
set obs 10000

* For instance let's draw four variables.
* 1. a chi2 with 5 degrees of freedom
* 2. a poisson k = 5
* 3. a uniform variable with min = -5 and max = 5
* 4. a random f distribution draw with 5 and 5 degrees for numerator and denominator degrees of freedom.

* First we will specify the correlation matrix.
* The only constraint as far as I know is that the covariance matrix has to be PSD.
* This in practicality limits the possible correlations between variables since cross terms tend to cause vialations more likely in the PSD requirement.

matrix c = (  1, .9, .3,  .2 \ ///
             .9,  1, .2,  .1 \ ///
             .3, .2,  1,  .5 \ ///
             .2, .1, .5,   1 )

* If we do not specify a mean or covariance matrix then the default draws are standard normals which is what we want for simplicity.
drawnorm x1 x2 x3 x4, corr(c)

corr x?
spearman x?

* Now all we need to do is turn our normal draws into uniform draws.
* Note: if x~N(0,1) and THETA is the CDF of the normal then y=CDF(x)~uniform
* So for any new distribution with CDF ALPHA and inverse INVALPHA the variable z=INVALPHA(y) ~ alpha.

gen y1 = normal(x1)
gen y2 = normal(x2)
gen y3 = normal(x3)
gen y4 = normal(x4)

corr y?
spearman y?

sum
* Looking good. All of the y variables are uniformly distributed now while still maintaining most of the correlation and all of the rankcorrelation.

* The key final step is to take the inverse CDF of the uniforms for a series of binomial draws.

* The function CDFinv is programmed so that the:
*          1st argument is the new variable name to be created
*          2nd is the cumulative P value to be inverted
*          3rd Number of total draws
*          4th probability of success on each individual draw

* Let's imagine you would like to simulate quarterly data on verterans.

CDFinv z1 y1 4 .05
  label var z1 "Hospitalization from mental illness"

CDFinv z2 y2 4 .15
  label var z2 "Hospitalization from accident"

CDFinv z3 y3 4 .2
  label var z3 "Seeks treatment for PTSD"

CDFinv z4 y4 1 .2
  label var z4 "Unemployed"

corr z?
spearman z?

* Note: Using an inverse CDF function is highly non-linear.  Thus the correlations are significantly weakened since only a linear transformation of the initial values would maintain the same correlation values.
* To adjust the final correlations, experiment with initial values until you find outcomes that approximate the level of correlation desired.
* It is worth noting that there is an upper limit to the amount of correlation possible as a result of this transformation.

Friday, November 2, 2012

Inverting a Binomial Distribution in Stata - harder than it sounds

Original Code

* I was recently trying to figure out how to invert the CDF of a binomial distribution in Stata and realized that there is not built in command.  There is an invbinomial command but it is inverting a different value than I expect.

* The description reads:
/* returns the inverse of the cumulative binomial; that is,
                         it returns the probability of success on one trial
                         such that the probability of observing floor(k) or
                         fewer successes in floor(n) trials is p.
*/

* I might be confused but this is not the definition that I think of as an inverse CDF.

* Sure it takes a cumulative probability and returns a parameter but normally I think of a invCDF as taking a cumulative probability and parameters and returning a value.  For example invnormal takes the p value and returns the z value that corresponds to that p value.  Likewise, invgamma take an "a" and "p" value and returns an x value.

* invbinomial(n,k,p)

clear
set obs 10
* We will set the number of trials to be 10.

gen inv=invbinomial(10,_n-1,.5)

gen pdf=binomialp(10,_n-1,.5)

* The standard CDF can be calculated as follows from the pdf:
gen cdf=sum(pdf)

* Instead the CDF seems to be giving us invCDF(P,k)=theta, which is certaily useful for some purposes but not what I want.

* So, I will attempt to write my own CDFinverse.  I don't know how to write functions in Stata so instead I will write a small program that may do what I want.

cap program drop CDFinv
program define CDFinv

  * Let's define the arguments of the program.
  * N is the total number of trials.
  * P is the CDF value that we would like inverted
  * p is the probability of one outcome being a success.
  * newvar is the name of the new variable to be generated.
  args newvar P N p

  * The challenging thing about trying to invert a CDF for a count distribution is that the CDF is a probability mass function with discontinuous regions.
  * However, the particularly nice thing about the binomial distribution is the the pool of outcomes is well defined. (1,2,3..N)
  * Thus we can simply count how many times the "steps" of the CDF fall below the target CDF level.
  cap drop `newvar'
  qui gen `newvar'=0
    label var `newvar' "Inverse CDF of binomial"

  forv k=1(1)`N' {
    * Add 1 to the variable value every time the CDF(n,k-1,p) is less than the target CDF value P.
qui replace `newvar'=`newvar'+1 if binomial(`N', `k'-1, `p')<(`P')
  }

end

* Using the first cdf generated by the summing of the pdf, this command seems to be working pretty well.
CDFinv cdfinv cdf 10 .5
sum cdfinv

* Though I am not sure why it jumps from 1 to 3.  I suspect this is the result of rounding error.

* The true test is if I were to plug a uniform distribution into it if it would give me the same distribution as the random binomial distribution generator.

* Let's start with a bigger data set:
clear
set obs 100000

* To establish our benchmark.
gen bin_benchmark = rbinomial(10,.75)
  label var bin_benchmark "Random Binomial Generated with rbinomial"

* Now let's generate our uniform
gen P = runiform()

CDFinv bin_gen P 10 .75
  label var bin_gen "Binomial Generated with CDFinv command"
sum bin*
* Both the means and standard deviations are very close.

* If the invCDF worked properly then P=CDF(invCDF(P))
gen P2 = binomial(10, bin_gen, .75)
  label var P2 "CDF(invCDF(P))"

cor P P2
* Close but not perfect.  Let's look at the differences graphically

* We can get a step graph to attempt to see the difference.
sort P
twoway (connected P2 bin_gen, msize(zero) color(red)) ///
       (connected P bin_gen, msize(zero)),     ///
  scheme(sj) title("CDF (N=10,p=.75)")



* Okay, this is does not look not bad.

* To further confirm that things are working properly we might try comparing the two histograms.
hist bin_benchmark, nodraw name(Benchmark, replace)
hist bin_gen, nodraw name(Generated, replace)

graph combine Benchmark Generated, title("Both Methods Produce Nearly Identical Results(N=10,p=.75)")
* I cannot tell the difference between the two distributions.  Thus I am finally satisfied.