cjerzak commited on
Commit
36df8ac
·
verified ·
1 Parent(s): 0067c72

Create warmup/app_v1.R

Browse files
Files changed (1) hide show
  1. warmup/app_v1.R +527 -0
warmup/app_v1.R ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # setwd("~/Dropbox/OptimizingSI/Analysis/ono")
2
+ # install.packages( "~/Documents/strategize-software/strategize", repos = NULL, type = "source",force = F)
3
+ # Script: app_ono.R
4
+
5
+ options(error = NULL)
6
+ library(shiny)
7
+ library(ggplot2)
8
+ library(strategize)
9
+ library(dplyr)
10
+
11
+ # Custom plotting function for optimal strategy distributions
12
+ plot_factor <- function(pi_star_list,
13
+ pi_star_se_list,
14
+ factor_name,
15
+ zStar = 1.96,
16
+ n_strategies = 1L) {
17
+ probs <- lapply(pi_star_list, function(x) x[[factor_name]])
18
+ ses <- lapply(pi_star_se_list, function(x) x[[factor_name]])
19
+ levels <- names(probs[[1]])
20
+
21
+ # Create data frame for plotting
22
+ df <- do.call(rbind, lapply(1:n_strategies, function(i) {
23
+ data.frame(
24
+ Strategy = if (n_strategies == 1) "Optimal" else c("Democrat", "Republican")[i],
25
+ Level = levels,
26
+ Probability = probs[[i]]
27
+ #SE = ses[[i]]
28
+ )
29
+ }))
30
+
31
+ # Manual dodging: Create numeric x-positions with offsets
32
+ df$Level_num <- as.numeric(as.factor(df$Level)) # Convert Level to numeric (1, 2, ...)
33
+ if (n_strategies == 1) {
34
+ df$x_dodged <- df$Level_num # No dodging for single strategy
35
+ } else {
36
+ # Apply ±offset for Democrat/Republican
37
+ df$x_dodged <- df$Level_num +
38
+ ifelse(df$Strategy == "Democrat",
39
+ -0.05, 0.05)
40
+ }
41
+
42
+ # Plot with ggplot2
43
+ p <- ggplot(df, aes(x = x_dodged,
44
+ y = Probability,
45
+ color = Strategy)) +
46
+ # Segment from y=0 to y=Probability
47
+ geom_segment(
48
+ aes(x = x_dodged, xend = x_dodged,
49
+ y = 0, yend = Probability),
50
+ size = 0.3
51
+ ) +
52
+ # Point at the probability
53
+ geom_point(
54
+ size = 2.5
55
+ ) +
56
+ # Text label above the point
57
+ geom_text(
58
+ aes(x = x_dodged,
59
+ label = sprintf("%.2f", Probability)),
60
+ vjust = -0.7,
61
+ size = 3
62
+ ) +
63
+ # Set x-axis with original Level labels
64
+ scale_x_continuous(
65
+ breaks = unique(df$Level_num),
66
+ labels = unique(df$Level),
67
+ limits = c(min(df$x_dodged)-0.20,
68
+ max(df$x_dodged)+0.20)
69
+ ) +
70
+ # Labels
71
+ labs(
72
+ title = "Optimal Distribution for:",
73
+ subtitle = sprintf("*%s*", gsub(factor_name,
74
+ pattern = "\\.",
75
+ replace = " ")),
76
+ x = "Level",
77
+ y = "Probability"
78
+ ) +
79
+ # Apply Tufte's minimalistic theme
80
+ theme_minimal(base_size = 18,
81
+ base_line_size = 0) +
82
+ theme(
83
+ legend.position = "none",
84
+ legend.title = element_blank(),
85
+ panel.grid.major = element_blank(),
86
+ panel.grid.minor = element_blank(),
87
+ axis.line = element_line(color = "black", size = 0.5),
88
+ axis.text.x = element_text(angle = 45,
89
+ hjust = 1,
90
+ margin = margin(r = 10)) # Add right margin
91
+ ) +
92
+ # Manual color scale for different strategies
93
+ scale_color_manual(values = c("Democrat" = "#89cff0",
94
+ "Republican" = "red",
95
+ "Optimal" = "black"))
96
+
97
+ return(p)
98
+ }
99
+
100
+ # UI Definition
101
+ ui <- fluidPage(
102
+ titlePanel("Exploring strategize with the candidate choice conjoint data"),
103
+
104
+ tags$p(
105
+ style = "text-align: left; margin-top: -10px;",
106
+ tags$a(
107
+ href = "https://strategizelab.org/",
108
+ target = "_blank",
109
+ title = "strategizelab.org",
110
+ style = "color: #337ab7; text-decoration: none;",
111
+ "strategizelab.org ",
112
+ icon("external-link", style = "font-size: 12px;")
113
+ )
114
+ ),
115
+
116
+ # ---- Minimal "Share" button HTML + JS inlined ----
117
+ tags$div(
118
+ style = "text-align: left; margin: 0.5em 0 0.5em 0em;",
119
+ HTML('
120
+ <button id="share-button"
121
+ style="
122
+ display: inline-flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ gap: 8px;
126
+ padding: 5px 10px;
127
+ font-size: 16px;
128
+ font-weight: normal;
129
+ color: #000;
130
+ background-color: #fff;
131
+ border: 1px solid #ddd;
132
+ border-radius: 6px;
133
+ cursor: pointer;
134
+ box-shadow: 0 1.5px 0 #000;
135
+ ">
136
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
137
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
138
+ <circle cx="18" cy="5" r="3"></circle>
139
+ <circle cx="6" cy="12" r="3"></circle>
140
+ <circle cx="18" cy="19" r="3"></circle>
141
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
142
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
143
+ </svg>
144
+ <strong>Share</strong>
145
+ </button>
146
+ '),
147
+ tags$script(
148
+ HTML("
149
+ (function() {
150
+ const shareBtn = document.getElementById('share-button');
151
+ // Reusable helper function to show a small “Copied!” message
152
+ function showCopyNotification() {
153
+ const notification = document.createElement('div');
154
+ notification.innerText = 'Copied to clipboard';
155
+ notification.style.position = 'fixed';
156
+ notification.style.bottom = '20px';
157
+ notification.style.right = '20px';
158
+ notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
159
+ notification.style.color = '#fff';
160
+ notification.style.padding = '8px 12px';
161
+ notification.style.borderRadius = '4px';
162
+ notification.style.zIndex = '9999';
163
+ document.body.appendChild(notification);
164
+ setTimeout(() => { notification.remove(); }, 2000);
165
+ }
166
+ shareBtn.addEventListener('click', function() {
167
+ const currentURL = window.location.href;
168
+ const pageTitle = document.title || 'Check this out!';
169
+ // If browser supports Web Share API
170
+ if (navigator.share) {
171
+ navigator.share({
172
+ title: pageTitle,
173
+ text: '',
174
+ url: currentURL
175
+ })
176
+ .catch((error) => {
177
+ console.log('Sharing failed', error);
178
+ });
179
+ } else {
180
+ // Fallback: Copy URL
181
+ if (navigator.clipboard && navigator.clipboard.writeText) {
182
+ navigator.clipboard.writeText(currentURL).then(() => {
183
+ showCopyNotification();
184
+ }, (err) => {
185
+ console.error('Could not copy text: ', err);
186
+ });
187
+ } else {
188
+ // Double fallback for older browsers
189
+ const textArea = document.createElement('textarea');
190
+ textArea.value = currentURL;
191
+ document.body.appendChild(textArea);
192
+ textArea.select();
193
+ try {
194
+ document.execCommand('copy');
195
+ showCopyNotification();
196
+ } catch (err) {
197
+ alert('Please copy this link:\\n' + currentURL);
198
+ }
199
+ document.body.removeChild(textArea);
200
+ }
201
+ }
202
+ });
203
+ })();
204
+ ")
205
+ )
206
+ ),
207
+ # ---- End: Minimal Share button snippet ----
208
+
209
+ sidebarLayout(
210
+ sidebarPanel(
211
+ h4("Analysis Options"),
212
+ radioButtons("case_type", "Case Type:",
213
+ choices = c("Average", "Adversarial"),
214
+ selected = "Average"),
215
+ conditionalPanel(
216
+ condition = "input.case_type == 'Average'",
217
+ selectInput("respondent_group", "Respondent Group:",
218
+ choices = c("All", "Democrat", "Independent", "Republican"),
219
+ selected = "All")
220
+ ),
221
+ numericInput("lambda_input", "Lambda (regularization):",
222
+ value = 0.01, min = 1e-6, max = 10, step = 0.01),
223
+ actionButton("compute", "Compute Results", class = "btn-primary"),
224
+ hr(),
225
+ h4("Visualization"),
226
+ selectInput("factor", "Select Factor to Display:",
227
+ choices = NULL),
228
+ br(),
229
+ selectInput("previousResults", "View Previous Results:",
230
+ choices = NULL),
231
+ hr(),
232
+ h5("Instructions:"),
233
+ p("1. Select a case type and, for Average case, a respondent group."),
234
+ p("2. Specify the single lambda to be used by strategize."),
235
+ p("3. Click 'Compute Results' to generate optimal strategies."),
236
+ p("4. Choose a factor to view its distribution."),
237
+ p("5. Use 'View Previous Results' to toggle among past computations.")
238
+ ),
239
+
240
+ mainPanel(
241
+ tabsetPanel(
242
+ tabPanel("Optimal Strategy Plot",
243
+ plotOutput("strategy_plot", height = "600px")),
244
+ tabPanel("Q Value",
245
+ verbatimTextOutput("q_value"),
246
+ p("Q represents the estimated outcome
247
+ under the optimal strategy, with 95% confidence interval.")),
248
+ tabPanel("About",
249
+ h3("About this page"),
250
+ p("This page app explores the ",
251
+ a("strategize R package", href = "https://github.com/cjerzak/strategize-software/", target = "_blank"),
252
+ " using Ono forced conjoint experimental data.
253
+ It computes optimal strategies for Average (optimizing for a respondent group)
254
+ and Adversarial (optimizing for both parties in competition) cases on the fly."),
255
+ p(strong("Average Case:"),
256
+ "Optimizes candidate characteristics for a selected respondent group."),
257
+ p(strong("Adversarial Case:"),
258
+ "Finds equilibrium strategies for Democrats and Republicans."),
259
+ p(strong("More information:"),
260
+ a("strategizelab.org", href = "https://strategizelab.org", target = "_blank"))
261
+ )
262
+ ),
263
+ br(),
264
+ wellPanel(
265
+ h4("Currently Selected Computation:"),
266
+ verbatimTextOutput("selection_summary")
267
+ )
268
+ )
269
+ )
270
+ )
271
+
272
+ # Server Definition
273
+ server <- function(input, output, session) {
274
+ # Load data
275
+ load("Processed_OnoData.RData")
276
+ Primary2016 <- read.csv("PrimaryCandidates2016 - Sheet1.csv")
277
+
278
+ # Prepare a storage structure for caching multiple results
279
+ cachedResults <- reactiveValues(data = list())
280
+
281
+ # Dynamic update of factor choices
282
+ observe({
283
+ if (input$case_type == "Average") {
284
+ factors <- colnames(FACTOR_MAT_FULL)[!colnames(FACTOR_MAT_FULL) %in% c("Office")]
285
+ } else {
286
+ factors <- colnames(FACTOR_MAT_FULL)[!colnames(FACTOR_MAT_FULL) %in% c("Office", "Party.affiliation", "Party.competition")]
287
+ }
288
+ updateSelectInput(session, "factor", choices = factors, selected = factors[1])
289
+ })
290
+
291
+ # Observe "Compute Results" button to generate a new result and cache it
292
+ observeEvent(input$compute, {
293
+ withProgress(message = "Computing optimal strategies...", value = 0, {
294
+ incProgress(0.2, detail = "Preparing data...")
295
+
296
+ # Common hyperparameters
297
+ params <- list(
298
+ nSGD = 1000L,
299
+ batch_size = 50L,
300
+ penalty_type = "KL",
301
+ nFolds = 3L,
302
+ use_optax = TRUE,
303
+ compute_se = FALSE, # Set to FALSE for quicker results
304
+ conf_level = 0.95,
305
+ conda_env = "strategize",
306
+ conda_env_required = TRUE
307
+ )
308
+
309
+ # Grab the single user-chosen lambda
310
+ my_lambda <- input$lambda_input
311
+
312
+ # We'll define a label to track the result uniquely
313
+ # Include the case type, group (if Average), and lambda in the label
314
+ if (input$case_type == "Average") {
315
+ label <- paste("Case=Average, Group=", input$respondent_group, ", Lambda=", my_lambda, sep="")
316
+ } else {
317
+ label <- paste("Case=Adversarial, Lambda=", my_lambda, sep="")
318
+ }
319
+
320
+ strategize_start <- Sys.time() # Timing strategize start
321
+ if (input$case_type == "Average") {
322
+ # Subset data for Average case
323
+ if (input$respondent_group == "All") {
324
+ indices <- which(my_data$Office == "President")
325
+ } else {
326
+ indices <- which(
327
+ my_data_FULL$R_Partisanship == input$respondent_group &
328
+ my_data$Office == "President"
329
+ )
330
+ }
331
+
332
+ FACTOR_MAT <- FACTOR_MAT_FULL[indices,
333
+ !colnames(FACTOR_MAT_FULL) %in%
334
+ c("Office","Party.affiliation","Party.competition")]
335
+ Yobs <- Yobs_FULL[indices]
336
+ X <- X_FULL[indices, ]
337
+ log_pr_w <- log_pr_w_FULL[indices]
338
+ pair_id <- pair_id_FULL[indices]
339
+ assignmentProbList <- assignmentProbList_FULL[names(FACTOR_MAT)]
340
+
341
+ incProgress(0.4,
342
+ detail = "Running strategize...")
343
+
344
+ # Compute with strategize
345
+ Qoptimized <- strategize(
346
+ Y = Yobs,
347
+ W = FACTOR_MAT,
348
+ X = X,
349
+ pair_id = pair_id,
350
+
351
+ p_list = assignmentProbList[colnames(FACTOR_MAT)],
352
+ lambda = my_lambda,
353
+ diff = TRUE,
354
+ adversarial = FALSE,
355
+ use_regularization = TRUE,
356
+ K = 1L,
357
+ nSGD = params$nSGD,
358
+ penalty_type = params$penalty_type,
359
+ folds = params$nFolds,
360
+ use_optax = params$use_optax,
361
+ compute_se = params$compute_se,
362
+ conf_level = params$conf_level,
363
+ conda_env = params$conda_env,
364
+ conda_env_required = params$conda_env_required
365
+ )
366
+ Qoptimized$n_strategies <- 1L
367
+ }
368
+ if (input$case_type == "Adversarial"){
369
+ # Adversarial case
370
+
371
+ DROP_FACTORS <- c("Office", "Party.affiliation", "Party.competition")
372
+ FACTOR_MAT <- FACTOR_MAT_FULL[, !colnames(FACTOR_MAT_FULL) %in% DROP_FACTORS]
373
+ Yobs <- Yobs_FULL
374
+ X <- X_FULL
375
+ log_pr_w <- log_pr_w_FULL
376
+ assignmentProbList <- assignmentProbList_FULL[!names(assignmentProbList_FULL) %in% DROP_FACTORS]
377
+
378
+ incProgress(0.3, detail = "Preparing slate data...")
379
+
380
+ FactorOptions <- apply(FACTOR_MAT, 2, table)
381
+ prior_alpha <- 10
382
+ Primary_D <- Primary2016[Primary2016$Party == "Democratic", colnames(FACTOR_MAT)]
383
+ Primary_R <- Primary2016[Primary2016$Party == "Republican", colnames(FACTOR_MAT)]
384
+
385
+ Primary_D_slate <- lapply(colnames(Primary_D), function(col) {
386
+ posterior_alpha <- FactorOptions[[col]]; posterior_alpha[] <- prior_alpha
387
+ Empirical_ <- table(Primary_D[[col]])
388
+ Empirical_ <- Empirical_[names(Empirical_) != "Unclear"]
389
+ posterior_alpha[names(Empirical_)] <- posterior_alpha[names(Empirical_)] + Empirical_
390
+ prop.table(posterior_alpha)
391
+ })
392
+ names(Primary_D_slate) <- colnames(Primary_D)
393
+
394
+ Primary_R_slate <- lapply(colnames(Primary_R), function(col) {
395
+ posterior_alpha <- FactorOptions[[col]]; posterior_alpha[] <- prior_alpha
396
+ Empirical_ <- table(Primary_R[[col]])
397
+ Empirical_ <- Empirical_[names(Empirical_) != "Unclear"]
398
+ posterior_alpha[names(Empirical_)] <- posterior_alpha[names(Empirical_)] + Empirical_
399
+ prop.table(posterior_alpha)
400
+ })
401
+ names(Primary_R_slate) <- colnames(Primary_R)
402
+
403
+ slate_list <- list("Democratic" = Primary_D_slate, "Republican" = Primary_R_slate)
404
+
405
+ indices <- which(
406
+ my_data$R_Partisanship %in% c("Republican","Democrat") &
407
+ my_data$Office == "President"
408
+ )
409
+ FACTOR_MAT <- FACTOR_MAT_FULL[indices,
410
+ !colnames(FACTOR_MAT_FULL) %in% c("Office","Party.competition","Party.affiliation")]
411
+ Yobs <- Yobs_FULL[indices]
412
+ my_data_red <- my_data_FULL[indices,]
413
+ pair_id <- pair_id_FULL[indices]
414
+ cluster_var <- cluster_var_FULL[indices]
415
+ my_data_red$Party.affiliation_clean <- ifelse(
416
+ my_data_red$Party.affiliation == "Republican Party",
417
+ yes = "Republican",
418
+ no = ifelse(my_data_red$Party.affiliation == "Democratic Party","Democrat","Independent")
419
+ )
420
+
421
+ assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)]
422
+ slate_list$Democratic <- slate_list$Democratic[names(assignmentProbList)]
423
+ slate_list$Republican <- slate_list$Republican[names(assignmentProbList)]
424
+
425
+ incProgress(0.4, detail = "Running strategize...")
426
+
427
+ Qoptimized <- strategize(
428
+ Y = Yobs,
429
+ W = FACTOR_MAT,
430
+ X = NULL,
431
+ p_list = assignmentProbList,
432
+ slate_list = slate_list,
433
+ varcov_cluster_variable = cluster_var,
434
+ competing_group_variable_respondent = my_data_red$R_Partisanship,
435
+ competing_group_variable_candidate = my_data_red$Party.affiliation_clean,
436
+ competing_group_competition_variable_candidate = my_data_red$Party.competition,
437
+ pair_id = pair_id,
438
+ respondent_id = my_data_red$respondentIndex,
439
+ respondent_task_id = my_data_red$task,
440
+ profile_order = my_data_red$profile,
441
+
442
+ lambda = my_lambda,
443
+ diff = TRUE,
444
+ use_regularization = TRUE,
445
+ force_gaussian = FALSE,
446
+ adversarial = TRUE,
447
+ K = 1L,
448
+ nMonte_adversarial = 20L,
449
+ nSGD = params$nSGD,
450
+ penalty_type = params$penalty_type,
451
+ learning_rate_max = 0.001,
452
+ use_optax = params$use_optax,
453
+ compute_se = params$compute_se,
454
+ conf_level = params$conf_level,
455
+ conda_env = params$conda_env,
456
+ conda_env_required = params$conda_env_required
457
+ )
458
+ # check correlation between strategies to diagnose optimization issues
459
+ # plot(unlist(Qoptimized$pi_star_point$Democrat), unlist(Qoptimized$pi_star_point$Republican))
460
+ Qoptimized$n_strategies <- 2L
461
+ }
462
+ Qoptimized$runtime_seconds <- as.numeric(difftime(Sys.time(),
463
+ strategize_start,
464
+ units = "secs"))
465
+
466
+ Qoptimized <- Qoptimized[c("pi_star_point",
467
+ "pi_star_se",
468
+ "Q_point",
469
+ "Q_se",
470
+ "n_strategies",
471
+ "runtime_seconds")]
472
+
473
+ incProgress(0.8, detail = "Finalizing results...")
474
+
475
+ # Store in the reactiveValues cache
476
+ cachedResults$data[[label]] <- Qoptimized
477
+
478
+ # Update the choice list for previous results
479
+ updateSelectInput(session, "previousResults",
480
+ choices = names(cachedResults$data),
481
+ selected = label)
482
+ })
483
+ })
484
+
485
+ # Reactive to pick the result the user wants to display
486
+ selectedResult <- reactive({
487
+ validate(
488
+ need(input$previousResults != "", "No result computed or selected yet.")
489
+ )
490
+ cachedResults$data[[input$previousResults]]
491
+ })
492
+
493
+ # Render strategy plot
494
+ output$strategy_plot <- renderPlot({
495
+ req(selectedResult())
496
+ factor_name <- input$factor
497
+ pi_star_list <- selectedResult()$pi_star_point
498
+ pi_star_se_list <- selectedResult()$pi_star_se
499
+ n_strategies <- selectedResult()$n_strategies
500
+ plot_factor(pi_star_list = pi_star_list,
501
+ pi_star_se_list = pi_star_se_list,
502
+ factor_name = factor_name,
503
+ n_strategies = n_strategies)
504
+ })
505
+
506
+ # Render Q value
507
+ output$q_value <- renderText({
508
+ req(selectedResult())
509
+ q_point <- selectedResult()$Q_point
510
+ q_se <- selectedResult()$Q_se
511
+ show_se <- length(q_se) > 0
512
+ if(show_se){ show_se <- q_se > 0 }
513
+ if(!show_se){ render_text <- paste("Estimated Q Value:", sprintf("%.3f", q_point)) }
514
+ if(show_se){ render_text <- paste("Estimated Q Value:", sprintf("%.3f ± %.3f", q_point, 1.96 * q_se)) }
515
+ sprintf("%s (Runtime: %.3f s)",
516
+ render_text,
517
+ selectedResult()$runtime_seconds)
518
+ })
519
+
520
+ # Show which set of parameters (label) is currently selected
521
+ output$selection_summary <- renderText({
522
+ input$previousResults
523
+ })
524
+ }
525
+
526
+ # Run the app
527
+ shinyApp(ui, server)