cjerzak commited on
Commit
2e34193
·
verified ·
1 Parent(s): f8aef6c

Create app_v2.R

Browse files
Files changed (1) hide show
  1. warmup/app_v2.R +482 -0
warmup/app_v2.R ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # setwd("~/Dropbox/OptimizingSI/Analysis/ono")
2
+ # install.packages("~/Documents/strategize-software/strategize", repos = NULL, type = "source", force = FALSE)
3
+
4
+ # =============================================================================
5
+ # app_ono.R
6
+ # Async, navigation‑friendly Shiny demo for strategize‑Ono
7
+ # ---------------------------------------------------------------------------
8
+ # * Heavy strategize jobs run in a background R session via future/promises.
9
+ # * UI stays responsive; you can browse old results while a new run crunches.
10
+ # * STARTUP‑SAFE and INPUT‑SAFE:
11
+ # • req(input$case_type) prevents length‑zero error.
12
+ # • Reactive inputs are captured (isolated) *before* the future() call,
13
+ # fixing “Can't access reactive value outside reactive consumer.”
14
+ # =============================================================================
15
+
16
+ options(error = NULL)
17
+
18
+ library(shiny)
19
+ library(ggplot2)
20
+ library(strategize)
21
+ library(dplyr)
22
+
23
+ # ---- Async helpers ----------------------------------------------------------
24
+ library(promises)
25
+ library(future) ; plan(multisession) # 1 worker per core
26
+ library(shinyjs)
27
+
28
+ # =============================================================================
29
+ # Custom plotting function (unchanged)
30
+ # =============================================================================
31
+ plot_factor <- function(pi_star_list,
32
+ pi_star_se_list,
33
+ factor_name,
34
+ zStar = 1.96,
35
+ n_strategies = 1L) {
36
+
37
+ probs <- lapply(pi_star_list, function(x) x[[factor_name]])
38
+ ses <- lapply(pi_star_se_list, function(x) x[[factor_name]])
39
+ levels <- names(probs[[1]])
40
+
41
+ df <- do.call(rbind, lapply(seq_len(n_strategies), function(i) {
42
+ data.frame(
43
+ Strategy = if (n_strategies == 1) "Optimal"
44
+ else c("Democrat", "Republican")[i],
45
+ Level = levels,
46
+ Probability = probs[[i]]
47
+ )
48
+ }))
49
+
50
+ df$Level_num <- as.numeric(as.factor(df$Level))
51
+ df$x_dodged <- if (n_strategies == 1)
52
+ df$Level_num
53
+ else
54
+ df$Level_num + ifelse(df$Strategy == "Democrat", -0.05, 0.05)
55
+
56
+ ggplot(df, aes(x = x_dodged, y = Probability, color = Strategy)) +
57
+ geom_segment(aes(x = x_dodged, xend = x_dodged,
58
+ y = 0, yend = Probability), size = 0.3) +
59
+ geom_point(size = 2.5) +
60
+ geom_text(aes(label = sprintf("%.2f", Probability)),
61
+ vjust = -0.7, size = 3) +
62
+ scale_x_continuous(breaks = unique(df$Level_num),
63
+ labels = unique(df$Level),
64
+ limits = c(min(df$x_dodged) - 0.20,
65
+ max(df$x_dodged) + 0.20)) +
66
+ labs(title = "Optimal Distribution for:",
67
+ subtitle = sprintf("*%s*",
68
+ gsub(factor_name, pattern = "\\.", replace = " ")),
69
+ x = "Level",
70
+ y = "Probability") +
71
+ theme_minimal(base_size = 18) +
72
+ theme(legend.position = "none",
73
+ legend.title = element_blank(),
74
+ panel.grid.major = element_blank(),
75
+ panel.grid.minor = element_blank(),
76
+ axis.line = element_line(color = "black", size = 0.5),
77
+ axis.text.x = element_text(angle = 45, hjust = 1,
78
+ margin = margin(r = 10))) +
79
+ scale_color_manual(values = c(Democrat = "#89cff0",
80
+ Republican = "red",
81
+ Optimal = "black"))
82
+ }
83
+
84
+ # =============================================================================
85
+ # UI (identical to previous async version—only shinyjs::useShinyjs() added)
86
+ # =============================================================================
87
+ ui <- fluidPage(
88
+ useShinyjs(),
89
+
90
+ titlePanel("Exploring strategize with the candidate choice conjoint data"),
91
+
92
+ tags$p(
93
+ style = "text-align: left; margin-top: -10px;",
94
+ tags$a(href = "https://strategizelab.org/",
95
+ target = "_blank",
96
+ title = "strategizelab.org",
97
+ style = "color: #337ab7; text-decoration: none;",
98
+ "strategizelab.org ",
99
+ icon("external-link", style = "font-size: 12px;"))
100
+ ),
101
+
102
+ # ---- Share button (unchanged) --------------------------------------------
103
+ tags$div(
104
+ style = "text-align: left; margin: 0.5em 0 0.5em 0em;",
105
+ HTML('
106
+ <button id="share-button"
107
+ style="
108
+ display: inline-flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ gap: 8px;
112
+ padding: 5px 10px;
113
+ font-size: 16px;
114
+ font-weight: normal;
115
+ color: #000;
116
+ background-color: #fff;
117
+ border: 1px solid #ddd;
118
+ border-radius: 6px;
119
+ cursor: pointer;
120
+ box-shadow: 0 1.5px 0 #000;
121
+ ">
122
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
123
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
124
+ stroke-linejoin="round">
125
+ <circle cx="18" cy="5" r="3"></circle>
126
+ <circle cx="6" cy="12" r="3"></circle>
127
+ <circle cx="18" cy="19" r="3"></circle>
128
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
129
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
130
+ </svg>
131
+ <strong>Share</strong>
132
+ </button>
133
+ '),
134
+ tags$script(
135
+ HTML("
136
+ (function() {
137
+ const shareBtn = document.getElementById('share-button');
138
+ function toast() {
139
+ const n = document.createElement('div');
140
+ n.innerText = 'Copied to clipboard';
141
+ Object.assign(n.style, {
142
+ position:'fixed',bottom:'20px',right:'20px',
143
+ background:'rgba(0,0,0,0.8)',color:'#fff',
144
+ padding:'8px 12px',borderRadius:'4px',zIndex:9999});
145
+ document.body.appendChild(n); setTimeout(()=>n.remove(),2000);
146
+ }
147
+ shareBtn.addEventListener('click', ()=>{
148
+ const url = window.location.href;
149
+ if (navigator.share) {
150
+ navigator.share({title:document.title||'Link',url})
151
+ .catch(()=>{});
152
+ } else if (navigator.clipboard) {
153
+ navigator.clipboard.writeText(url).then(toast);
154
+ } else {
155
+ const ta = document.createElement('textarea');
156
+ ta.value=url; document.body.appendChild(ta); ta.select();
157
+ try{document.execCommand('copy'); toast();}
158
+ catch(e){alert('Copy this link:\\n'+url);} ta.remove();
159
+ }
160
+ });
161
+ })();")
162
+ )
163
+ ),
164
+
165
+ sidebarLayout(
166
+ sidebarPanel(
167
+ h4("Analysis Options"),
168
+ radioButtons("case_type", "Case Type:",
169
+ choices = c("Average", "Adversarial"),
170
+ selected = "Average"),
171
+ conditionalPanel(
172
+ condition = "input.case_type == 'Average'",
173
+ selectInput("respondent_group", "Respondent Group:",
174
+ choices = c("All", "Democrat", "Independent", "Republican"),
175
+ selected = "Democrat")
176
+ ),
177
+ numericInput("lambda_input", "Lambda (regularization):",
178
+ value = 0.01, min = 1e-6, max = 10, step = 0.01),
179
+ actionButton("compute", "Compute Results", class = "btn-primary"),
180
+ div(id = "status_text",
181
+ style = "margin-top:6px; font-style:italic; color:#555;"),
182
+ hr(),
183
+ h4("Visualization"),
184
+ selectInput("factor", "Select Factor to Display:", choices = NULL),
185
+ br(),
186
+ selectInput("previousResults", "View Previous Results:", choices = NULL),
187
+ hr(),
188
+ h5("Instructions:"),
189
+ p("1. Select a case type and, for Average case, a respondent group."),
190
+ p("2. Specify the single lambda to be used by strategize."),
191
+ p("3. Click 'Compute Results' to generate optimal strategies."),
192
+ p("4. Choose a factor to view its distribution."),
193
+ p("5. Use 'View Previous Results' to toggle among past computations.")
194
+ ),
195
+
196
+ mainPanel(
197
+ tabsetPanel(
198
+ tabPanel("Optimal Strategy Plot",
199
+ plotOutput("strategy_plot", height = "600px")),
200
+ tabPanel("Q Value",
201
+ verbatimTextOutput("q_value"),
202
+ p("Q represents the estimated outcome under the optimal strategy,",
203
+ "with 95% confidence interval.")),
204
+ tabPanel("About",
205
+ h3("About this page"),
206
+ p("This page app explores the ",
207
+ a("strategize R package",
208
+ href = "https://github.com/cjerzak/strategize-software/",
209
+ target = "_blank"),
210
+ " using Ono forced conjoint experimental data.",
211
+ "It computes optimal strategies for Average (optimizing for a respondent",
212
+ "group) and Adversarial (optimizing for both parties in competition) cases",
213
+ "on the fly."),
214
+ p(strong("Average Case:"), "Optimizes candidate characteristics for a",
215
+ "selected respondent group."),
216
+ p(strong("Adversarial Case:"), "Finds equilibrium strategies for Democrats",
217
+ "and Republicans."),
218
+ p(strong("More information:"),
219
+ a("strategizelab.org", href = "https://strategizelab.org",
220
+ target = "_blank"))
221
+ )
222
+ ),
223
+ br(),
224
+ wellPanel(
225
+ h4("Currently Selected Computation:"),
226
+ verbatimTextOutput("selection_summary")
227
+ )
228
+ )
229
+ )
230
+ )
231
+
232
+ # =============================================================================
233
+ # SERVER
234
+ # =============================================================================
235
+ server <- function(input, output, session) {
236
+
237
+ # ---- Data load (unchanged) -----------------------------------------------
238
+ load("Processed_OnoData.RData")
239
+ Primary2016 <- read.csv("PrimaryCandidates2016 - Sheet1.csv")
240
+
241
+ # ---- Reactive stores ------------------------------------------------------
242
+ cachedResults <- reactiveValues(data = list())
243
+ runningFlags <- reactiveValues(active = list())
244
+
245
+ # ---- Factor dropdown updater ---------------------------------------------
246
+ observe({
247
+ req(input$case_type)
248
+ if (input$case_type == "Average") {
249
+ factors <- setdiff(colnames(FACTOR_MAT_FULL), "Office")
250
+ } else {
251
+ factors <- setdiff(colnames(FACTOR_MAT_FULL),
252
+ c("Office", "Party.affiliation", "Party.competition"))
253
+ }
254
+ updateSelectInput(session, "factor",
255
+ choices = factors,
256
+ selected = factors[1])
257
+ })
258
+
259
+ # ===========================================================================
260
+ # Compute Results button
261
+ # ===========================================================================
262
+ observeEvent(input$compute, {
263
+
264
+ ## ---- CAPTURE reactive inputs ------------------------------------------
265
+ case_type <- isolate(input$case_type)
266
+ respondent_group <- isolate(input$respondent_group)
267
+ my_lambda <- isolate(input$lambda_input)
268
+
269
+ label <- if (case_type == "Average") {
270
+ paste0("Case=Average, Group=", respondent_group,
271
+ ", Lambda=", my_lambda)
272
+ } else {
273
+ paste0("Case=Adversarial, Lambda=", my_lambda)
274
+ }
275
+
276
+ runningFlags$active[[label]] <- TRUE
277
+ cachedResults$data[[label]] <- NULL
278
+ updateSelectInput(session, "previousResults",
279
+ choices = names(cachedResults$data),
280
+ selected = label)
281
+ shinyjs::html("status_text", "")
282
+ shinyjs::html("status_text", "submitting…") # Immediately show “submitting…”
283
+ shinyjs::delay(2000, shinyjs::html("status_text", "submitted")) # Two‑second later switch to “submitted”
284
+ shinyjs::disable("compute")
285
+ showNotification(sprintf("Job '%s' submitted …", label),
286
+ type = "message", duration = 3)
287
+
288
+ ## ---- FUTURE -----------------------------------------------------------
289
+ future({
290
+
291
+ strategize_start <- Sys.time()
292
+
293
+ # --------------- shared hyper‑params ----------------------------------
294
+ params <- list(
295
+ nSGD = 1000L,
296
+ batch_size = 50L,
297
+ penalty_type = "KL",
298
+ nFolds = 3L,
299
+ use_optax = TRUE,
300
+ compute_se = FALSE,
301
+ conf_level = 0.95,
302
+ conda_env = "strategize",
303
+ conda_env_required = TRUE
304
+ )
305
+
306
+ if (case_type == "Average") {
307
+ # ---------- Average case --------------------------------------------
308
+ indices <- if (respondent_group == "All") {
309
+ which(my_data$Office == "President")
310
+ } else {
311
+ which(my_data_FULL$R_Partisanship == respondent_group &
312
+ my_data$Office == "President")
313
+ }
314
+
315
+ FACTOR_MAT <- FACTOR_MAT_FULL[indices,
316
+ !colnames(FACTOR_MAT_FULL) %in%
317
+ c("Office", "Party.affiliation", "Party.competition")]
318
+ Yobs <- Yobs_FULL[indices]
319
+ X <- X_FULL[indices, ]
320
+ pair_id <- pair_id_FULL[indices]
321
+ assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)]
322
+
323
+ Qoptimized <- strategize(
324
+ Y = Yobs,
325
+ W = FACTOR_MAT,
326
+ X = X,
327
+ pair_id = pair_id,
328
+ p_list = assignmentProbList[colnames(FACTOR_MAT)],
329
+ lambda = my_lambda,
330
+ diff = TRUE,
331
+ adversarial = FALSE,
332
+ use_regularization = TRUE,
333
+ K = 1L,
334
+ nSGD = params$nSGD,
335
+ penalty_type = params$penalty_type,
336
+ folds = params$nFolds,
337
+ use_optax = params$use_optax,
338
+ compute_se = params$compute_se,
339
+ conf_level = params$conf_level,
340
+ conda_env = params$conda_env,
341
+ conda_env_required = params$conda_env_required
342
+ )
343
+ Qoptimized$n_strategies <- 1L
344
+
345
+ } else {
346
+ # ---------- Adversarial case ----------------------------------------
347
+ DROP <- c("Office", "Party.affiliation", "Party.competition")
348
+ FACTOR_MAT <- FACTOR_MAT_FULL[, !colnames(FACTOR_MAT_FULL) %in% DROP]
349
+ assignmentProbList <- assignmentProbList_FULL[!names(assignmentProbList_FULL) %in% DROP]
350
+
351
+ # Build Primary slates
352
+ FactorOptions <- apply(FACTOR_MAT, 2, table)
353
+ prior_alpha <- 10
354
+ Primary_D <- Primary2016[Primary2016$Party == "Democratic",
355
+ colnames(FACTOR_MAT)]
356
+ Primary_R <- Primary2016[Primary2016$Party == "Republican",
357
+ colnames(FACTOR_MAT)]
358
+ slate_fun <- function(df) {
359
+ lapply(colnames(df), function(col) {
360
+ post <- FactorOptions[[col]]; post[] <- prior_alpha
361
+ emp <- table(df[[col]]); emp <- emp[names(emp) != "Unclear"]
362
+ post[names(emp)] <- post[names(emp)] + emp
363
+ prop.table(post)
364
+ }) |> setNames(colnames(df))
365
+ }
366
+ slate_list <- list(Democratic = slate_fun(Primary_D),
367
+ Republican = slate_fun(Primary_R))
368
+
369
+ indices <- which(my_data$R_Partisanship %in% c("Republican", "Democrat") &
370
+ my_data$Office == "President")
371
+ FACTOR_MAT <- FACTOR_MAT_FULL[indices,
372
+ !colnames(FACTOR_MAT_FULL) %in%
373
+ c("Office", "Party.competition", "Party.affiliation")]
374
+ Yobs <- Yobs_FULL[indices]
375
+ my_data_red <- my_data_FULL[indices, ]
376
+ pair_id <- pair_id_FULL[indices]
377
+ cluster_var <- cluster_var_FULL[indices]
378
+ my_data_red$Party.affiliation_clean <-
379
+ ifelse(my_data_red$Party.affiliation == "Republican Party", "Republican",
380
+ ifelse(my_data_red$Party.affiliation == "Democratic Party","Democrat","Independent"))
381
+
382
+ assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)]
383
+ slate_list$Democratic <- slate_list$Democratic[names(assignmentProbList)]
384
+ slate_list$Republican <- slate_list$Republican[names(assignmentProbList)]
385
+
386
+ Qoptimized <- strategize(
387
+ Y = Yobs,
388
+ W = FACTOR_MAT,
389
+ X = NULL,
390
+ p_list = assignmentProbList,
391
+ slate_list = slate_list,
392
+ varcov_cluster_variable = cluster_var,
393
+ competing_group_variable_respondent = my_data_red$R_Partisanship,
394
+ competing_group_variable_candidate = my_data_red$Party.affiliation_clean,
395
+ competing_group_competition_variable_candidate =
396
+ my_data_red$Party.competition,
397
+ pair_id = pair_id,
398
+ respondent_id = my_data_red$respondentIndex,
399
+ respondent_task_id = my_data_red$task,
400
+ profile_order = my_data_red$profile,
401
+ lambda = my_lambda,
402
+ diff = TRUE,
403
+ use_regularization = TRUE,
404
+ force_gaussian = FALSE,
405
+ adversarial = TRUE,
406
+ K = 1L,
407
+ nMonte_adversarial = 20L,
408
+ nSGD = params$nSGD,
409
+ penalty_type = params$penalty_type,
410
+ learning_rate_max = 0.001,
411
+ use_optax = params$use_optax,
412
+ compute_se = params$compute_se,
413
+ conf_level = params$conf_level,
414
+ conda_env = params$conda_env,
415
+ conda_env_required = params$conda_env_required
416
+ )
417
+ Qoptimized$n_strategies <- 2L
418
+ }
419
+
420
+ Qoptimized$runtime_seconds <-
421
+ as.numeric(difftime(Sys.time(), strategize_start, units = "secs"))
422
+ Qoptimized[c("pi_star_point", "pi_star_se", "Q_point",
423
+ "Q_se", "n_strategies", "runtime_seconds")]
424
+ }) %...>% # success handler
425
+ (function(res) {
426
+ cachedResults$data[[label]] <- res
427
+ runningFlags$active[[label]] <- FALSE
428
+ updateSelectInput(session, "previousResults",
429
+ choices = names(cachedResults$data),
430
+ selected = label)
431
+ shinyjs::html("status_text", "complete!")
432
+ shinyjs::enable("compute")
433
+ showNotification(sprintf("Job '%s' finished (%.1f s).",
434
+ label, res$runtime_seconds),
435
+ type = "message", duration = 6)
436
+ }) %...!% # error handler
437
+ (function(err) {
438
+ runningFlags$active[[label]] <- FALSE
439
+ cachedResults$data[[label]] <- NULL
440
+ shinyjs::html("status_text", "error – see log")
441
+ shinyjs::enable("compute")
442
+ showNotification(paste("Error in", label, ":", err$message),
443
+ type = "error", duration = 8)
444
+ })
445
+
446
+ NULL # return value of observeEvent
447
+ })
448
+
449
+ # ---- Helper: fetch selected result or show waiting msg -------------------
450
+ selectedResult <- reactive({
451
+ lbl <- input$previousResults ; req(lbl)
452
+ if (isTRUE(runningFlags$active[[lbl]]))
453
+ validate("Computation is still running – please wait…")
454
+ res <- cachedResults$data[[lbl]]
455
+ validate(need(!is.null(res), "No finished result selected."))
456
+ res
457
+ })
458
+
459
+ # ---- Outputs -------------------------------------------------------------
460
+ output$strategy_plot <- renderPlot({
461
+ res <- selectedResult()
462
+ plot_factor(res$pi_star_point, res$pi_star_se,
463
+ factor_name = input$factor,
464
+ n_strategies = res$n_strategies)
465
+ })
466
+
467
+ output$q_value <- renderText({
468
+ res <- selectedResult()
469
+ q_pt <- res$Q_point; q_se <- res$Q_se
470
+ txt <- if (length(q_se) && q_se > 0)
471
+ sprintf("Estimated Q Value: %.3f ± %.3f", q_pt, 1.96*q_se)
472
+ else sprintf("Estimated Q Value: %.3f", q_pt)
473
+ sprintf("%s (Runtime: %.2f s)", txt, res$runtime_seconds)
474
+ })
475
+
476
+ output$selection_summary <- renderText({ input$previousResults })
477
+ }
478
+
479
+ # =============================================================================
480
+ # Run the app
481
+ # =============================================================================
482
+ shinyApp(ui, server)