Newer
Older
# ------------------------------------------------------------------------------
# Desc: This app performs a two proportion test via randomization. The
# resampling test mimics Fisher's Exact Test.
# Published Location:
# Email: fairbour@msu.edu, manskisc@msu.edu
#
# For questions or concerns, please email the authors. This work is licensed
# under a Creative Commons Attribution-ShareAlike 4.0 International License
# (https://creativecommons.org/licenses/by-sa/4.0/).
# ------------------------------------------------------------------------------
# loading packages
library(shiny)
library(ggplot2)
library(dplyr)
library(BHH2)
library(gridExtra)
library(shinyjs)
enableBookmarking(store = "server")
# Sources objects, functions, etc, from TwoProportionSource.R
# This file contains the html code for the editable table,
# the decimalcount function, the dotplot_locs function, and
# custom ggplot2 themes.
# colors for plots
hist.fill.color <- "grey70" # histogram bar fill color
hist.outline.color <- "black" # histogram bar outline color
dot.fill.color <- "grey70" # dotplot dot fill color
cutoff.color <- "#F05133" # color for cutoff values
# defines the presets
Presets <- list()
# Presets`preset name` <- c(Top Left, Bottom Left, Top Right, Bottom Right,
# Column A Name, Column B name, Row A name, Row B name)
Presets$`Dolphin Therapy` <- c(13, 5, 7, 15, "Improved", "Did not improve",
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
Presets$`Duct Tape Therapy` <- c(22, 15, 4, 10, "Wart Gone", "Wart Remains",
"Duct Tape", "Cryotherapy")
Presets$`Gender Discrimination` <- c(21, 14, 3, 10, "Promotion", "No Promotion",
"Male", "Female")
Presets$`Opportunity Cost` <- c(56, 41, 19, 34, "buy DVD", "not buy DVD",
"control", "treatment")
Presets$`Avandia` <- c(2593, 5386, 65000, 154592, "Yes", "No", "Rosiglitazone",
"Pioglitazone")
Presets$`Custom` <- c(0, 0, 0, 0, "Column A", "Column B", "Row 1", "Row 2")
ui <- function(request) {
fluidPage(useShinyjs(),
titlePanel("Two Proportion Resampling Test"),
sidebarLayout(
sidebarPanel(
tabsetPanel(
tabPanel("Test",
tags$div(class="header", checked = NA,
tags$p("Enter your data into the table
below, or choose one of the data
presets. Press the Shuffle button
to simulate the results under an
independence null model."
)
),
hr(),
selectInput(inputId = "plot",
label = "Plot Type",
choices = c("Dotplot", "Histogram")),
selectInput(inputId = "presets",
label = "Presets",
selected = "Custom", # selects the initial preset
choices = names(Presets)),
html_table,
actionButton(inputId = "Reset", label = "Reset"),
numericInput(inputId = "numsamp",
label = "Shuffle how many times?",
value = 100, min = 1, max = 5000),
tags$div(class="header", checked = NA,
tags$p("Enter a value from 1 to 5000")),
actionButton("Replicate", "Shuffle")
),
tabPanel("Information",
tags$div(class="header", checked = NA,
tags$p("Enter the value of your observed difference of proportions in the text
under the graph. Selecting 'greater/less than' will highlight the
samples that are greater/less than your value."),
tags$p("Selecting 'beyond' will highlight the samples that are further away
from 0 than your value."),
hr(),
hr(),
tags$p("Written by Scott Manski"),
tags$p("This work is licensed under a "),
tags$a(href="http://creativecommons.org/licenses/by-sa/4.0/", "Creative Commons Attribution-ShareAlike 4.0 International License"),
hr(),
bookmarkButton()
)
))),
mainPanel(
plotOutput("RandomPlot"),
checkboxInput("Show.Observed", "Show observed difference", FALSE),
textOutput("Observed.Diff"),
fluidRow(
column(textOutput("count.samples"), width = 3),
column(selectInput("inequality", NULL, c("greater than", "less than", "beyond")), width = 3),
column(textInput("cutoff", NULL), width = 4),
htmlOutput("counts"))
)
)
)}
server <- function(input, output, session) {
# initialize values for use in server
values <- reactiveValues()
values$props <- vector()
values$table <- matrix(rep(NA, 9), ncol=3)
# observe any changes in table
observe({
values$table[1, 1] <- as.numeric(input$TL) # top left position
values$table[1, 2] <- as.numeric(input$TR) # top right position
values$table[2, 1] <- as.numeric(input$BL) # bottom left position
values$table[2, 2] <- as.numeric(input$BR) # bottom right position
values$table[1, 3] <- as.numeric(input$TL) + as.numeric(input$TR) #top sum
values$table[2, 3] <- as.numeric(input$BL) + as.numeric(input$BR) # bottom sum
values$table[3, 1] <- as.numeric(input$TL) + as.numeric(input$BL) # left sum
values$table[3, 2] <- as.numeric(input$TR) + as.numeric(input$BR) # right sum
values$table[3, 3] <- as.numeric(input$TL) + as.numeric(input$TR) + # total sum
as.numeric(input$BL) + as.numeric(input$BR)
# calculates the limits for the plots based on the standard deviation
# the standard deviation is calculated based on the Hypergeometric distribution
values$x.lim <- 6*sqrt(values$table[3, 1]*values$table[1, 3]/values$table[3, 3]*
values$table[2, 3]/values$table[3, 3]*values$table[3, 2]/(values$table[3,3]-1)/values$table[1, 3]^2 +
values$table[3, 2]*values$table[2, 3]/values$table[3, 3]*
values$table[1, 3]/values$table[3, 3]*values$table[3, 1]/(values$table[3,3]-1)/values$table[2, 3]^2)
})
# output for values of table if there is a change
output$TRT <- renderText({
values$table[1, 3]
})
output$TRB <- renderText({
values$table[2, 3]
})
output$TBL <- renderText({
values$table[3, 1]
})
output$TBR <- renderText({
values$table[3, 2]
})
output$Total <- renderText({
values$table[3, 3]
})
# these will update each time the user clicks the Replicate button
observeEvent(input$Replicate || input$Show.Observed, {
values$observed <- values$table[1 ,1]/values$table[1, 3] -
values$table[2 ,1]/values$table[2, 3]
})
# reset the values if "Reset" is pressed
observeEvent(input$Reset, {
values$props <- vector()
enable("Replicate")
})
# checks to see if the current table matches a preset, otherwise the preset is "Custom"
observeEvent(c(input$TL, input$TR, input$BL, input$BR, input$C1N,
input$C2N, input$R1N, input$R2N), {
# combine the current table inputs into a vector
current <- c(input$TL, input$BL, input$TR, input$BR, input$C1N, input$C2N,
input$R1N, input$R2N)
# loops through each preset and determines the number of cells that match the current table
preset <- unlist(lapply(names(Presets), function(i) {
sum(Presets[[i]] == current)
}))
# if all cells match, change the SelectInput value to that preset,
# otherwise change the value to "Custom"
if (sum(preset == 8) > 0) {
updateSelectInput(session, "presets", selected = names(Presets)[which(preset == 8)])
} else {
updateSelectInput(session, "presets", selected = "Custom")
}
}, ignoreInit = TRUE)
# disable or enable the "Shuffle" button
# the "Shuffle" button in enabled when the number of shuffles is less than
# 5,000 and the total number of shuffles is less than 20,000
observeEvent(input$numsamp, {
if (is.numeric(input$numsamp)){
if (input$numsamp > 5000){
disable("Replicate")
} else if (length(values$props) <= 20000){
enable("Replicate")
}
}
})
# update the values when shuffle is pressed
observeEvent(input$Replicate, {
new.vals <- rhyper(input$numsamp, values$table[1, 3],
values$table[2, 3], values$table[3, 1])
new.vals <- new.vals/values$table[1, 3] - (values$table[3, 1]-new.vals)/
values$table[2, 3]
values$props <- c(values$props, new.vals)
if (length(values$props) >= 20000){
disable("Replicate")
} else {
enable("Replicate")
}
}, ignoreInit = TRUE)
# when "Beyond" is selected, this function is used to calculate the probability
# for each possible outcome.
two_sided_values <- function() {
m <- values$table[1, 3]
n <- values$table[2, 3]
k <- values$table[3, 1]
support <- c(max(0, k - n):min(k, m))
x <- dhyper(support, m, n, k)
names(x) <- support
x
}
# update the counts for the cutoff if there are any changes
update_counts <- eventReactive(c(input$cutoff, input$Replicate, input$Reset,
input$inequality, input$presets), {
if (!is.na(as.numeric(input$cutoff))){
# the error is used to handle rounded values of input$cutoff
num.decimals <- decimalcount(as.character(input$cutoff))
error <- ifelse(num.decimals <= 1, 0, 0.1^num.decimals/2)
# for "greater than", finds the number and proportion of values greater than
# input$cutoff - error. For "less than", finds the number and proportion of
# values less than input$cutoff + error. For beyond, the number and proportion
# of values is calculated by adding up all points such that the probability of
# obtaining that point is less than or equal to that of input$cutoff see
# https://en.wikipedia.org/wiki/Fisher%27s_exact_test, the second to last paragraph
# in the Example section)
if (input$inequality == "greater than"){
values$prob <- sum(values$props >= as.numeric(input$cutoff)-error)/
length(values$props)
values$count <- sum(values$props >= as.numeric(input$cutoff)-error)
} else if (input$inequality == "less than") {
values$prob <- sum(values$props <= as.numeric(input$cutoff)+error)/
length(values$props)
values$count <- sum(as.numeric(values$props) <=
as.numeric(input$cutoff)+error)
} else {
x <- two_sided_values()
cutoff <- x[which(names(x) == values$table[1, 1])]
vals <- as.numeric(names(x)[which(x <= cutoff)])
vals <- vals/values$table[1, 3] - (values$table[3, 1]-vals)/
values$table[2, 3]
values$prob <- length(which(values$props %in% vals))/
length(values$props)
values$count <- length(which(values$props %in% vals))
}
}
})
# creates the desired plot
output$RandomPlot <- renderPlot({
if (length(values$props) != 0 & !is.na(values$table[3, 1])){ # after reset, values$props is empty
DF <- values$table
if (input$plot == "Dotplot"){ # plot == TRUE is dotplot, FALSE is histogram
# n is the number of columns for the dotplot
# large datasets will have n <- 1
if (DF[3, 1] > 1000){
n <- 1
} else {
n <- 4
}
# gets the dotplot locations for the dotplot
df <- dotplot_locs(values$props, n, input$cutoff, cutoff.color,
dot.fill.color, input$inequality)
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
df <- df[df$x < values$x.lim & df$x > -values$x.lim, ]
myplot <- ggplot(df) +
geom_point(aes(x ,y, colour = fill.color),
size=min(n, 50/length(values$props)^0.5)) +
scale_colour_manual(name = "fill.color",
values = levels(df$fill.color)) +
scale_y_continuous(limits = c(0, max(n*7.5,max(df$y)))) +
scale_x_continuous(limits = c(-values$x.lim, values$x.lim)) +
labs(x = "Shuffled Difference in Proportions", y = "Count") +
plaintheme + axistheme
} else {
df <- data.frame("x" = values$props[values$props < values$x.lim &
values$props > -values$x.lim])
unique.vals <- sort(unique(as.numeric(as.character(df$x))))
# a histogram is created to determine the bars that need to be colored
myplot <- ggplot(df, aes(x=x)) + geom_histogram(binwidth = max(diff(unique.vals))) +
scale_x_continuous(limits = c(-values$x.lim, values$x.lim))
names.counts <- ggplot_build(myplot)$data[[1]]$x
# color is determined if input$cutoff is specified
if (!is.na(as.numeric(input$cutoff))){
num.decimals <- decimalcount(as.character(input$cutoff))
error <- ifelse(num.decimals <= 2, 0, 0.1^num.decimals/2)
if (input$inequality == "greater than"){
to.color <- which(names.counts >= as.numeric(input$cutoff)-error)
} else if (input$inequality == "less than"){
to.color <- which(names.counts <= as.numeric(input$cutoff)+error)
} else{
to.color <- c(which(names.counts <= -1*abs(as.numeric(input$cutoff))+error),
which(names.counts >= abs(as.numeric(input$cutoff))-error))
}
} else {
to.color <- NA
}
fill.color <- rep(hist.fill.color, length(names.counts))
fill.color[to.color] <- cutoff.color
# the histogram is plotted
myplot <- ggplot(df, aes(x=x)) +
geom_histogram(binwidth = max(diff(unique.vals)), fill = fill.color,
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
labs(x = "Shuffled Difference in Proportions", y = "Count") +
scale_x_continuous(limits = c(-values$x.lim, values$x.lim)) +
plaintheme + axistheme
}
myplot
}
})
# calculate the observed difference when checkbox is TRUE
output$Observed.Diff <- renderText({
if (input$Show.Observed){
DF <- data()
values$observed = values$table[1, 1]/values$table[1, 3] - values$table[2, 1]/values$table[2, 3]
paste("Observed Difference:", round(values$observed, 6))
}
})
# text for sample counts
output$count.samples <- renderText({
"Count Samples"
})
# output for counts when cutoff is specified
output$counts <- renderText({
update_counts()
if (!is.null(values$prob)){
if (is.na(values$prob)){
" "
} else if (!is.na(as.numeric(input$cutoff))){
paste("<font color=", cutoff.color, "><b>",values$count, "/",
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
length(values$props), " (", round(values$prob, 4), ")",
"</b></font>", sep = "")
} else if (nchar(input$cutoff)!=0){
"<font color=\"#FF0000\"><b>Invalid Cutoff!</b></font>"
} else {
" "
}
}
})
# changes table if a different preset is selected
observeEvent(input$presets, {
values$props <- vector()
enable("Replicate")
if (input$presets != "Custom") {
preset_index <- which(names(Presets) == input$presets)
preset <- Presets[[preset_index]]
DF <- data.frame("X1" = as.numeric(preset[1:2]),
"X2" = as.numeric(preset[3:4]))
DF[3, ] <- apply(DF, 2, sum)
DF[, 3] <- apply(DF, 1, sum)
values$table <- DF
values$table.names <- preset[5:8]
updateTextInput(session, "TL", value = values$table[1,1])
updateTextInput(session, "TR", value = values$table[1,2])
updateTextInput(session, "BL", value = values$table[2,1])
updateTextInput(session, "BR", value = values$table[2,2])
updateTextInput(session, "C1N", value = values$table.names[1])
updateTextInput(session, "C2N", value = values$table.names[2])
updateTextInput(session, "R1N", value = values$table.names[3])
updateTextInput(session, "R2N", value = values$table.names[4])
}
})
## Bookmarking ##
# to remove bookmarking, remove bookmarkButton() from the ui
# when the bookmark button is pressed, the current values of props and table are saved
onBookmark(function(state) {
state$values$props <- values$props
state$values$table <- values$table
})
# when opening a bookmarked page, props is restored
onRestored(function(state) {
values$props <- state$values$props
})
# when opening a bookmarked page, table is restored
onRestore(function(state) {
values$table <- state$values$table
})
}
shinyApp(ui = ui, server = server, options = list(height = 1080), enableBookmarking = "server")