20

I am looking for a program (in R or SAS or standalone, if free or low cost) that will do power analysis for ordinal logistic regression.

mdewey
  • 17,806
Peter Flom
  • 119,535
  • 36
  • 175
  • 383
  • I agree, It would help to know exactly how many ordinal categories are being modeled here (it looks like maybe 3 to me), what beta0 and beta2 are (thresholds?), and how the -1/2, 1/4, and 1/4 were chosen. – Craig L-S Jul 16 '21 at 00:09

3 Answers3

36

I prefer to do power analyses beyond the basics by simulation. With precanned packages, I am never quite sure what assumptions are being made.

Simulating for power is quite straight forward (and affordable) using R.

  1. decide what you think your data should look like and how you will analyze it
  2. write a function or set of expressions that will simulate the data for a given relationship and sample size and do the analysis (a function is preferable in that you can make the sample size and parameters into arguments to make it easier to try different values). The function or code should return the p-value or other test statistic.
  3. use the replicate function to run the code from above a bunch of times (I usually start at about 100 times to get a feel for how long it takes and to get the right general area, then up it to 1,000 and sometimes 10,000 or 100,000 for the final values that I will use). The proportion of times that you rejected the null hypothesis is the power.
  4. redo the above for another set of conditions.

Here is a simple example with ordinal regression:

library(rms)

tmpfun <- function(n, beta0, beta1, beta2) {
    x <- runif(n, 0, 10)
    eta1 <- beta0 + beta1*x
    eta2 <- eta1 + beta2
    p1 <- exp(eta1)/(1+exp(eta1))
    p2 <- exp(eta2)/(1+exp(eta2))
    tmp <- runif(n)
    y <- (tmp < p1) + (tmp < p2)
    fit <- lrm(y~x)
    fit$stats[5]
}

out <- replicate(1000, tmpfun(100, -1/2, 1/4, 1/4))
mean( out < 0.05 )
Greg Snow
  • 51,722
  • 7
    +1, this is a very robust, universal approach. I have often used it. I'd like to suggest another feature: You can generate data for the max $N$ you would consider, then fit the model for proportions of those data by successively fitting the 1st n of them at regular intervals up to $N$ (eg, n=100, 120, 140, 160, 180, & 200). Instead of saving a p-value from each generated dataset, you can save a row of p-values. Averaging over each column gives you a quick and dirty sense of how power is changing w/ $N$, and helps you hone in on an appropriate value quickly. – gung - Reinstate Monica Feb 08 '12 at 02:57
  • 2
    @gung: yours comment make sense, would you mind adding your codes so that less experience people in R could also benefit from it? thanks –  Jul 02 '12 at 14:06
  • Could you please give me a reference of a book of simulation in R. – ABC Feb 21 '15 at 09:48
  • 1
    I am looking at this again and I have a couple questions: 1) Why is x uniform on 1:10? 2) How would you generalize it to more than 1 independent variable? – Peter Flom Feb 22 '15 at 15:48
  • @harry, I don't know of any books on simulation in R. Some of the general books on R may have a section that introduces simulation. – Greg Snow Feb 23 '15 at 18:02
  • 2
    @PeterFlom, x had to be something, so I chose (arbitrarily) to have it uniform between 0 and 10, it could have also been normal, gamma, etc. Best would be to choose something that is similar to what we expect the real x variables to look like. To use more than 1 predictor variable, generate them independently (or from a multivariate normal, copula, etc.) then just include them all in the eta1 piece, e.g. eta1 <- beta0 + beta1*x1 + beta2*x2 + beta3*x3. – Greg Snow Feb 23 '15 at 18:07
  • What is the benefit of replicating ? How does replicate work inside the function ? How does replicate modify ? It seems to me I am getting 1000 independent fit$stats[5] and taking the last fit$stats[5] . – ABC Jul 08 '15 at 11:15
  • 1
    @ABC, not replicating would only give you a single decision, you need replications to determine how often the test rejects (the definition of power). replicate is not in the function and does not modify. The function returns the p-value (what is in fit$stats[5]) for one iteration, replicate runs the function 1,000 times (or whatever number you specify) and returns the 1,000 p-values, the mean function then computes the proportion of tests that would reject the null at $\alpha=0.05$. – Greg Snow Jul 08 '15 at 16:55
  • 1
    What are the beta0 ... beta1 values? are they regression coefficents that have come from step 1? where you decide what you think your data should look like – lukeg Feb 27 '16 at 17:50
  • @lukeg, yes the beta values are "True" regression coefficients that you need to assume some values for. You can do the simulation for different sets of values to see how they change the power. Reasonable sets of values need to be based on step 1 which is based on the underlying science. – Greg Snow Feb 29 '16 at 19:04
  • @GregSnow Thanks for this. If beta0...beta2 are your coefficients - presumably the intercept beta0 + 2 independent variables' coefficient - why does beta2 appear in the eta2 calculation in your answer but not in eta1 as it does in your example comment above from Feb 23, 2015? Or is it that the answer is for univariate regression and beta2 must've represented something different than I thought? – Hack-R Sep 27 '21 at 16:41
  • 3
    @Hack-R, the above code is for ordinal logistic regression, or proportional odds logistic regression, where there are 3 ordered levels in the response variable, e.g. low, medium, and high. So beta_0 and beta_1 together create eta1 which translates to the probability of being in the medium or high group (anything above low), then beta_2 is added to give eta2 which is the probability of being in the high group. I probably should have come up with a better name for beta_2 to convey this better. – Greg Snow Sep 27 '21 at 22:08
  • @GregSnow. You have one coefficient beta0 for intercept in the eta1, is beta2 in eta 2 the second intercept (for the top category)? Also, is it possible to implement the same code for categorical variables? In the model with 2 variables x1 and x2 each of them with 2 levels and interactions you would have beta1x1i + beta2x1j + beta3x2i + beta4x2j + beta5x1jx2j is there a way to ensure that when x1i is 1 then x1j will be 0? Or to develop 4 separate models with x1 and x2 equal 0 0, 0 1, 1 0, 1 1, and run your code for each of these 1000 times and the average mean result? – Milo May 04 '23 at 23:29
  • @Milo, Beta2 is the difference between the 1st and 2nd intercepts, so the intercept for the top group is Beta1 + Beta2. This is a simple example, you can expand it to as many predictors as you want, categorical predictors should be turned into indicator variables just like in analysis. – Greg Snow May 05 '23 at 18:51
  • @GregSnow. thank you. In a model with dummy variable with 2 levels one will always be 0 so only one beta either beta1 or beta2 contributes.If I turn a categorical variable into a dummy one and use runif() to generate xi and x j for each level in beta1x1i + beta2x1j, won't it be a problem that both xi and xj may be different than 0? – Milo May 05 '23 at 20:08
  • 1
    @Milo, It is simpler to generate a categorical predictor using the sample function, then logic statements in the calculations, e.g.: x1 <- sample(c('a', 'b', 'c'), n, replace=TRUE); eta <- beta0 + beta1*(x1=='b') + beta2*(x1=='c') – Greg Snow May 05 '23 at 22:30
4

Besides Snow's excellent example, I believe you can also do a power simulation by resampling from an existing dataset which has your effect. Not quite a bootstrap, since you're not sampling-with-replacement the same n, but the same idea.

So here's an example: I ran a little self-experiment which turned in a positive point-estimate but because it was little, was not nearly statistically-significant in the ordinal logistic regression. With that point-estimate, how big a n would I need? For various possible n, I many times generated a dataset & ran the ordinal logistic regression & saw how small the p-value was:

library(boot)
library(rms)
npt <- read.csv("http://www.gwern.net/docs/nootropics/2013-gwern-noopept.csv")
newNoopeptPower <- function(dt, indices) {
    d <- dt[sample(nrow(dt), n, replace=TRUE), ] # new dataset, possibly larger than the original
    lmodel <- lrm(MP ~ Noopept + Magtein, data = d)
    return(anova(lmodel)[7])
}
alpha <- 0.05
for (n in seq(from = 300, to = 600, by = 30)) {
   bs <- boot(data=npt, statistic=newNoopeptPower, R=10000, parallel="multicore", ncpus=4)
   print(c(n, sum(bs$t<=alpha)/length(bs$t)))
}

With the output (for me):

[1] 300.0000   0.1823
[1] 330.0000   0.1925
[1] 360.0000   0.2083
[1] 390.0000   0.2143
[1] 420.0000   0.2318
[1] 450.0000   0.2462
[1] 480.000   0.258
[1] 510.0000   0.2825
[1] 540.0000   0.2855
[1] 570.0000   0.3184
[1] 600.0000   0.3175

In this case, at n=600 the power was 32%. Not very encouraging.

(If my simulation approach is wrong, please someone tell me. I'm going off a few medical papers discussing power simulation for planning clinical trials, but I'm not at all certain about my precise implementation.)

gwern
  • 425
3

I would add one other thing to Snow's answer (and this applies to any power analysis via simulation) - pay attention to whether you are looking for a 1 or 2 tailed test. Popular programs like G*Power default to 1-tailed test, and if you are trying to see if your simulations match them (always a good idea when you are learning how to do this), you will want to check that first.

To make Snow's run a 1-tailed test, I would add a parameter called "tail" to the function inputs, and put something like this in the function itself:

 #two-tail test
  if (tail==2) fit$stats[5]

  #one-tail test
  if (tail==1){
    if (fit$coefficients[5]>0) {
          fit$stats[5]/2
    } else 1

The 1-tailed version basically checks to see that the coefficient is positive, and then cuts the p-value in half.