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

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +288 -342
app.R CHANGED
@@ -1,119 +1,104 @@
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('
@@ -133,8 +118,9 @@ ui <- fluidPage(
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>
@@ -146,65 +132,34 @@ ui <- fluidPage(
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(
@@ -218,16 +173,14 @@ ui <- fluidPage(
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."),
@@ -243,21 +196,25 @@ ui <- fluidPage(
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(),
@@ -269,161 +226,157 @@ ui <- fluidPage(
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,
@@ -432,97 +385,90 @@ server <- function(input, output, session) {
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)
528
-
 
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(xend = x_dodged, yend = Probability), size = 0.3) +
58
+ geom_point(size = 2.5) +
59
+ geom_text(aes(label = sprintf("%.2f", Probability)),
60
+ vjust = -0.7, size = 3) +
61
+ scale_x_continuous(breaks = unique(df$Level_num),
62
+ labels = unique(df$Level),
63
+ limits = c(min(df$x_dodged) - 0.20,
64
+ max(df$x_dodged) + 0.20)) +
65
+ labs(title = "Optimal Distribution for:",
66
+ subtitle = sprintf("*%s*",
67
+ gsub(factor_name, pattern = "\\.", replace = " ")),
68
+ x = "Level",
69
+ y = "Probability") +
70
+ theme_minimal(base_size = 18) +
71
+ theme(legend.position = "none",
72
+ legend.title = element_blank(),
73
+ panel.grid.major = element_blank(),
74
+ panel.grid.minor = element_blank(),
75
+ axis.line = element_line(color = "black", size = 0.5),
76
+ axis.text.x = element_text(angle = 45, hjust = 1,
77
+ margin = margin(r = 10))) +
78
+ scale_color_manual(values = c(Democrat = "#89cff0",
79
+ Republican = "red",
80
+ Optimal = "black"))
81
  }
82
 
83
+ # =============================================================================
84
+ # UI (identical to previous async version—only shinyjs::useShinyjs() added)
85
+ # =============================================================================
86
  ui <- fluidPage(
87
+ useShinyjs(),
88
+
89
  titlePanel("Exploring strategize with the candidate choice conjoint data"),
90
 
91
  tags$p(
92
  style = "text-align: left; margin-top: -10px;",
93
+ tags$a(href = "https://strategizelab.org/",
94
+ target = "_blank",
95
+ title = "strategizelab.org",
96
+ style = "color: #337ab7; text-decoration: none;",
97
+ "strategizelab.org ",
98
+ icon("external-link", style = "font-size: 12px;"))
 
 
99
  ),
100
 
101
+ # ---- Share button (unchanged) --------------------------------------------
102
  tags$div(
103
  style = "text-align: left; margin: 0.5em 0 0.5em 0em;",
104
  HTML('
 
118
  cursor: pointer;
119
  box-shadow: 0 1.5px 0 #000;
120
  ">
121
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
122
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
123
+ stroke-linejoin="round">
124
  <circle cx="18" cy="5" r="3"></circle>
125
  <circle cx="6" cy="12" r="3"></circle>
126
  <circle cx="18" cy="19" r="3"></circle>
 
132
  '),
133
  tags$script(
134
  HTML("
135
+ (function() {
136
+ const shareBtn = document.getElementById('share-button');
137
+ function toast() {
138
+ const n = document.createElement('div');
139
+ n.innerText = 'Copied to clipboard';
140
+ Object.assign(n.style, {
141
+ position:'fixed',bottom:'20px',right:'20px',
142
+ background:'rgba(0,0,0,0.8)',color:'#fff',
143
+ padding:'8px 12px',borderRadius:'4px',zIndex:9999});
144
+ document.body.appendChild(n); setTimeout(()=>n.remove(),2000);
145
+ }
146
+ shareBtn.addEventListener('click', ()=>{
147
+ const url = window.location.href;
148
+ if (navigator.share) {
149
+ navigator.share({title:document.title||'Link',url})
150
+ .catch(()=>{});
151
+ } else if (navigator.clipboard) {
152
+ navigator.clipboard.writeText(url).then(toast);
153
+ } else {
154
+ const ta = document.createElement('textarea');
155
+ ta.value=url; document.body.appendChild(ta); ta.select();
156
+ try{document.execCommand('copy'); toast();}
157
+ catch(e){alert('Copy this link:\\n'+url);} ta.remove();
158
  }
159
+ });
160
+ })();")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  )
162
+ ),
 
163
 
164
  sidebarLayout(
165
  sidebarPanel(
 
173
  choices = c("All", "Democrat", "Independent", "Republican"),
174
  selected = "All")
175
  ),
176
+ numericInput("lambda_input", "Lambda (regularization):",
177
  value = 0.01, min = 1e-6, max = 10, step = 0.01),
178
  actionButton("compute", "Compute Results", class = "btn-primary"),
179
  hr(),
180
  h4("Visualization"),
181
+ selectInput("factor", "Select Factor to Display:", choices = NULL),
 
182
  br(),
183
+ selectInput("previousResults", "View Previous Results:", choices = NULL),
 
184
  hr(),
185
  h5("Instructions:"),
186
  p("1. Select a case type and, for Average case, a respondent group."),
 
196
  plotOutput("strategy_plot", height = "600px")),
197
  tabPanel("Q Value",
198
  verbatimTextOutput("q_value"),
199
+ p("Q represents the estimated outcome under the optimal strategy,",
200
+ "with 95% confidence interval.")),
201
  tabPanel("About",
202
  h3("About this page"),
203
  p("This page app explores the ",
204
+ a("strategize R package",
205
+ href = "https://github.com/cjerzak/strategize-software/",
206
+ target = "_blank"),
207
+ " using Ono forced conjoint experimental data.",
208
+ "It computes optimal strategies for Average (optimizing for a respondent",
209
+ "group) and Adversarial (optimizing for both parties in competition) cases",
210
+ "on the fly."),
211
+ p(strong("Average Case:"), "Optimizes candidate characteristics for a",
212
+ "selected respondent group."),
213
+ p(strong("Adversarial Case:"), "Finds equilibrium strategies for Democrats",
214
+ "and Republicans."),
215
  p(strong("More information:"),
216
+ a("strategizelab.org", href = "https://strategizelab.org",
217
+ target = "_blank"))
218
  )
219
  ),
220
  br(),
 
226
  )
227
  )
228
 
229
+ # =============================================================================
230
+ # SERVER
231
+ # =============================================================================
232
  server <- function(input, output, session) {
233
+
234
+ # ---- Data load (unchanged) -----------------------------------------------
235
  load("Processed_OnoData.RData")
236
  Primary2016 <- read.csv("PrimaryCandidates2016 - Sheet1.csv")
237
 
238
+ # ---- Reactive stores ------------------------------------------------------
239
  cachedResults <- reactiveValues(data = list())
240
+ runningFlags <- reactiveValues(active = list())
241
 
242
+ # ---- Factor dropdown updater ---------------------------------------------
243
  observe({
244
+ req(input$case_type)
245
  if (input$case_type == "Average") {
246
+ factors <- setdiff(colnames(FACTOR_MAT_FULL), "Office")
247
  } else {
248
+ factors <- setdiff(colnames(FACTOR_MAT_FULL),
249
+ c("Office", "Party.affiliation", "Party.competition"))
250
  }
251
+ updateSelectInput(session, "factor",
252
+ choices = factors,
253
+ selected = factors[1])
254
  })
255
 
256
+ # ===========================================================================
257
+ # Compute Results button
258
+ # ===========================================================================
259
  observeEvent(input$compute, {
260
+
261
+ ## ---- CAPTURE reactive inputs ------------------------------------------
262
+ case_type <- isolate(input$case_type)
263
+ respondent_group <- isolate(input$respondent_group)
264
+ my_lambda <- isolate(input$lambda_input)
265
+
266
+ label <- if (case_type == "Average") {
267
+ paste0("Case=Average, Group=", respondent_group,
268
+ ", Lambda=", my_lambda)
269
+ } else {
270
+ paste0("Case=Adversarial, Lambda=", my_lambda)
271
+ }
272
+
273
+ runningFlags$active[[label]] <- TRUE
274
+ cachedResults$data[[label]] <- NULL
275
+ updateSelectInput(session, "previousResults",
276
+ choices = names(cachedResults$data),
277
+ selected = label)
278
+ shinyjs::disable("compute")
279
+ showNotification(sprintf("Job '%s' submitted …", label),
280
+ type = "message", duration = 3)
281
+
282
+ ## ---- FUTURE -----------------------------------------------------------
283
+ future({
284
+
285
+ strategize_start <- Sys.time()
286
 
287
+ # --------------- shared hyper‑params ----------------------------------
288
  params <- list(
289
+ nSGD = 1000L,
290
+ batch_size = 50L,
291
  penalty_type = "KL",
292
+ nFolds = 3L,
293
+ use_optax = TRUE,
294
+ compute_se = FALSE,
295
+ conf_level = 0.95,
296
+ conda_env = "strategize",
297
  conda_env_required = TRUE
298
  )
299
 
300
+ if (case_type == "Average") {
301
+ # ---------- Average case --------------------------------------------
302
+ indices <- if (respondent_group == "All") {
303
+ which(my_data$Office == "President")
 
 
 
 
 
 
 
 
 
 
 
 
304
  } else {
305
+ which(my_data_FULL$R_Partisanship == respondent_group &
306
+ my_data$Office == "President")
 
 
307
  }
308
 
309
+ FACTOR_MAT <- FACTOR_MAT_FULL[indices,
310
+ !colnames(FACTOR_MAT_FULL) %in%
311
+ c("Office", "Party.affiliation", "Party.competition")]
312
+ Yobs <- Yobs_FULL[indices]
313
+ X <- X_FULL[indices, ]
314
+ pair_id <- pair_id_FULL[indices]
315
+ assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)]
 
 
 
 
316
 
 
317
  Qoptimized <- strategize(
318
  Y = Yobs,
319
  W = FACTOR_MAT,
320
  X = X,
321
  pair_id = pair_id,
322
+ p_list = assignmentProbList[colnames(FACTOR_MAT)],
323
+ lambda = my_lambda,
324
+ diff = TRUE,
325
+ adversarial = FALSE,
326
+ use_regularization = TRUE,
327
+ K = 1L,
328
+ nSGD = params$nSGD,
 
329
  penalty_type = params$penalty_type,
330
+ folds = params$nFolds,
331
  use_optax = params$use_optax,
332
  compute_se = params$compute_se,
333
  conf_level = params$conf_level,
334
+ conda_env = params$conda_env,
335
  conda_env_required = params$conda_env_required
336
  )
337
  Qoptimized$n_strategies <- 1L
 
 
 
 
 
 
 
 
 
 
338
 
339
+ } else {
340
+ # ---------- Adversarial case ----------------------------------------
341
+ DROP <- c("Office", "Party.affiliation", "Party.competition")
342
+ FACTOR_MAT <- FACTOR_MAT_FULL[, !colnames(FACTOR_MAT_FULL) %in% DROP]
343
+ assignmentProbList <- assignmentProbList_FULL[!names(assignmentProbList_FULL) %in% DROP]
344
 
345
+ # Build Primary slates
346
  FactorOptions <- apply(FACTOR_MAT, 2, table)
347
+ prior_alpha <- 10
348
+ Primary_D <- Primary2016[Primary2016$Party == "Democratic",
349
+ colnames(FACTOR_MAT)]
350
+ Primary_R <- Primary2016[Primary2016$Party == "Republican",
351
+ colnames(FACTOR_MAT)]
352
+ slate_fun <- function(df) {
353
+ lapply(colnames(df), function(col) {
354
+ post <- FactorOptions[[col]]; post[] <- prior_alpha
355
+ emp <- table(df[[col]]); emp <- emp[names(emp) != "Unclear"]
356
+ post[names(emp)] <- post[names(emp)] + emp
357
+ prop.table(post)
358
+ }) |> setNames(colnames(df))
359
+ }
360
+ slate_list <- list(Democratic = slate_fun(Primary_D),
361
+ Republican = slate_fun(Primary_R))
 
 
 
 
 
 
 
 
362
 
363
+ indices <- which(my_data$R_Partisanship %in% c("Republican", "Democrat") &
364
+ my_data$Office == "President")
365
+ FACTOR_MAT <- FACTOR_MAT_FULL[indices,
366
+ !colnames(FACTOR_MAT_FULL) %in%
367
+ c("Office", "Party.competition", "Party.affiliation")]
368
+ Yobs <- Yobs_FULL[indices]
369
+ my_data_red <- my_data_FULL[indices, ]
370
+ pair_id <- pair_id_FULL[indices]
 
371
  cluster_var <- cluster_var_FULL[indices]
372
+ my_data_red$Party.affiliation_clean <-
373
+ ifelse(my_data_red$Party.affiliation == "Republican Party", "Republican",
374
+ ifelse(my_data_red$Party.affiliation == "Democratic Party","Democrat","Independent"))
 
 
375
 
376
  assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)]
377
  slate_list$Democratic <- slate_list$Democratic[names(assignmentProbList)]
378
  slate_list$Republican <- slate_list$Republican[names(assignmentProbList)]
379
 
 
 
380
  Qoptimized <- strategize(
381
  Y = Yobs,
382
  W = FACTOR_MAT,
 
385
  slate_list = slate_list,
386
  varcov_cluster_variable = cluster_var,
387
  competing_group_variable_respondent = my_data_red$R_Partisanship,
388
+ competing_group_variable_candidate = my_data_red$Party.affiliation_clean,
389
+ competing_group_competition_variable_candidate =
390
+ my_data_red$Party.competition,
391
+ pair_id = pair_id,
392
+ respondent_id = my_data_red$respondentIndex,
393
  respondent_task_id = my_data_red$task,
394
+ profile_order = my_data_red$profile,
395
+ lambda = my_lambda,
396
+ diff = TRUE,
 
397
  use_regularization = TRUE,
398
+ force_gaussian = FALSE,
399
+ adversarial = TRUE,
400
+ K = 1L,
401
  nMonte_adversarial = 20L,
402
+ nSGD = params$nSGD,
403
  penalty_type = params$penalty_type,
404
  learning_rate_max = 0.001,
405
+ use_optax = params$use_optax,
406
  compute_se = params$compute_se,
407
  conf_level = params$conf_level,
408
+ conda_env = params$conda_env,
409
  conda_env_required = params$conda_env_required
410
  )
 
 
411
  Qoptimized$n_strategies <- 2L
412
  }
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
+ Qoptimized$runtime_seconds <-
415
+ as.numeric(difftime(Sys.time(), strategize_start, units = "secs"))
416
+ Qoptimized[c("pi_star_point", "pi_star_se", "Q_point",
417
+ "Q_se", "n_strategies", "runtime_seconds")]
418
+ }) %...>% # success handler
419
+ (function(res) {
420
+ cachedResults$data[[label]] <- res
421
+ runningFlags$active[[label]] <- FALSE
422
+ updateSelectInput(session, "previousResults",
423
+ choices = names(cachedResults$data),
424
+ selected = label)
425
+ shinyjs::enable("compute")
426
+ showNotification(sprintf("Job '%s' finished (%.1f s).",
427
+ label, res$runtime_seconds),
428
+ type = "message", duration = 6)
429
+ }) %...!% # error handler
430
+ (function(err) {
431
+ runningFlags$active[[label]] <- FALSE
432
+ cachedResults$data[[label]] <- NULL
433
+ shinyjs::enable("compute")
434
+ showNotification(paste("Error in", label, ":", err$message),
435
+ type = "error", duration = 8)
436
+ })
437
+
438
+ NULL # return value of observeEvent
439
  })
440
 
441
+ # ---- Helper: fetch selected result or show waiting msg -------------------
442
  selectedResult <- reactive({
443
+ lbl <- input$previousResults ; req(lbl)
444
+ if (isTRUE(runningFlags$active[[lbl]]))
445
+ validate("Computation is still running – please wait…")
446
+ res <- cachedResults$data[[lbl]]
447
+ validate(need(!is.null(res), "No finished result selected."))
448
+ res
449
  })
450
 
451
+ # ---- Outputs -------------------------------------------------------------
452
  output$strategy_plot <- renderPlot({
453
+ res <- selectedResult()
454
+ plot_factor(res$pi_star_point, res$pi_star_se,
455
+ factor_name = input$factor,
456
+ n_strategies = res$n_strategies)
 
 
 
 
 
457
  })
458
 
 
459
  output$q_value <- renderText({
460
+ res <- selectedResult()
461
+ q_pt <- res$Q_point; q_se <- res$Q_se
462
+ txt <- if (length(q_se) && q_se > 0)
463
+ sprintf("Estimated Q Value: %.3f ± %.3f", q_pt, 1.96*q_se)
464
+ else sprintf("Estimated Q Value: %.3f", q_pt)
465
+ sprintf("%s (Runtime: %.2f s)", txt, res$runtime_seconds)
 
 
 
 
466
  })
467
 
468
+ output$selection_summary <- renderText({ input$previousResults })
 
 
 
469
  }
470
 
471
+ # =============================================================================
472
+ # Run the app
473
+ # =============================================================================
474
  shinyApp(ui, server)