# setwd("~/Dropbox/OptimizingSI/Analysis/ono") # install.packages("~/Documents/strategize-software/strategize", repos = NULL, type = "source", force = FALSE) # ============================================================================= # app_ono.R # Async, navigation‑friendly Shiny demo for strategize‑Ono # --------------------------------------------------------------------------- # * Heavy strategize jobs run in a background R session via future/promises. # * UI stays responsive; you can browse old results while a new run crunches. # * STARTUP‑SAFE and INPUT‑SAFE: # • req(input$case_type) prevents length‑zero error. # • Reactive inputs are captured (isolated) *before* the future() call, # fixing “Can't access reactive value outside reactive consumer.” # ============================================================================= options(error = NULL) library(shiny) library(ggplot2) library(strategize) library(dplyr) # ---- Async helpers ---------------------------------------------------------- library(promises) library(future) ; plan(multisession) # 1 worker per core library(shinyjs) # ============================================================================= # Custom plotting function (unchanged) # ============================================================================= plot_factor <- function(pi_star_list, pi_star_se_list, factor_name, zStar = 1.96, n_strategies = 1L) { probs <- lapply(pi_star_list, function(x) x[[factor_name]]) ses <- lapply(pi_star_se_list, function(x) x[[factor_name]]) levels <- names(probs[[1]]) df <- do.call(rbind, lapply(seq_len(n_strategies), function(i) { data.frame( Strategy = if (n_strategies == 1) "Optimal" else c("Democrat", "Republican")[i], Level = levels, Probability = probs[[i]] ) })) df$Level_num <- as.numeric(as.factor(df$Level)) df$x_dodged <- if (n_strategies == 1) df$Level_num else df$Level_num + ifelse(df$Strategy == "Democrat", -0.05, 0.05) ggplot(df, aes(x = x_dodged, y = Probability, color = Strategy)) + geom_segment(aes(x = x_dodged, xend = x_dodged, y = 0, yend = Probability), size = 0.3) + geom_point(size = 2.5) + geom_text(aes(label = sprintf("%.2f", Probability)), vjust = -0.7, size = 3) + scale_x_continuous(breaks = unique(df$Level_num), labels = unique(df$Level), limits = c(min(df$x_dodged) - 0.20, max(df$x_dodged) + 0.20)) + labs(title = "Optimal Distribution for:", subtitle = sprintf("*%s*", gsub(factor_name, pattern = "\\.", replace = " ")), x = "Level", y = "Probability") + theme_minimal(base_size = 18) + theme(legend.position = "none", legend.title = element_blank(), panel.grid.major = element_blank(), panel.grid.minor = element_blank(), axis.line = element_line(color = "black", size = 0.5), axis.text.x = element_text(angle = 45, hjust = 1, margin = margin(r = 10))) + scale_color_manual(values = c(Democrat = "#89cff0", Republican = "red", Optimal = "black")) } # ============================================================================= # UI (identical to previous async version—only shinyjs::useShinyjs() added) # ============================================================================= ui <- fluidPage( useShinyjs(), titlePanel("Exploring strategize with the candidate choice conjoint data"), tags$p( style = "text-align: left; margin-top: -10px;", tags$a(href = "https://strategizelab.org/", target = "_blank", title = "strategizelab.org", style = "color: #337ab7; text-decoration: none;", "strategizelab.org ", icon("external-link", style = "font-size: 12px;")) ), # ---- Share button (unchanged) -------------------------------------------- tags$div( style = "text-align: left; margin: 0.5em 0 0.5em 0em;", HTML(' '), tags$script( HTML(" (function() { const shareBtn = document.getElementById('share-button'); function toast() { const n = document.createElement('div'); n.innerText = 'Copied to clipboard'; Object.assign(n.style, { position:'fixed',bottom:'20px',right:'20px', background:'rgba(0,0,0,0.8)',color:'#fff', padding:'8px 12px',borderRadius:'4px',zIndex:9999}); document.body.appendChild(n); setTimeout(()=>n.remove(),2000); } shareBtn.addEventListener('click', ()=>{ const url = window.location.href; if (navigator.share) { navigator.share({title:document.title||'Link',url}) .catch(()=>{}); } else if (navigator.clipboard) { navigator.clipboard.writeText(url).then(toast); } else { const ta = document.createElement('textarea'); ta.value=url; document.body.appendChild(ta); ta.select(); try{document.execCommand('copy'); toast();} catch(e){alert('Copy this link:\\n'+url);} ta.remove(); } }); })();") ) ), sidebarLayout( sidebarPanel( h4("Analysis Options"), radioButtons("case_type", "Case Type:", choices = c("Average", "Adversarial"), selected = "Average"), conditionalPanel( condition = "input.case_type == 'Average'", selectInput("respondent_group", "Respondent Group:", choices = c("All", "Democrat", "Independent", "Republican"), selected = "Democrat") ), numericInput("lambda_input", "Lambda (regularization):", value = 0.01, min = 1e-6, max = 10, step = 0.01), actionButton("compute", "Compute Results", class = "btn-primary"), div(id = "status_text", style = "margin-top:6px; font-style:italic; color:#555;"), hr(), h4("Visualization"), selectInput("factor", "Select Factor to Display:", choices = NULL), br(), selectInput("previousResults", "View Previous Results:", choices = NULL), hr(), h5("Instructions:"), p("1. Select a case type and, for Average case, a respondent group."), p("2. Specify the single lambda to be used by strategize."), p("3. Click 'Compute Results' to generate optimal strategies."), p("4. Choose a factor to view its distribution."), p("5. Use 'View Previous Results' to toggle among past computations.") ), mainPanel( tabsetPanel( tabPanel("Optimal Strategy Plot", plotOutput("strategy_plot", height = "600px")), tabPanel("Q Value", verbatimTextOutput("q_value"), p("Q represents the estimated outcome under the optimal strategy,", "with 95% confidence interval.")), tabPanel("About", h3("About this page"), p("This page app explores the ", a("strategize R package", href = "https://github.com/cjerzak/strategize-software/", target = "_blank"), " using Ono forced conjoint experimental data.", "It computes optimal strategies for Average (optimizing for a respondent", "group) and Adversarial (optimizing for both parties in competition) cases", "on the fly."), p(strong("Average Case:"), "Optimizes candidate characteristics for a", "selected respondent group."), p(strong("Adversarial Case:"), "Finds equilibrium strategies for Democrats", "and Republicans."), p(strong("More information:"), a("strategizelab.org", href = "https://strategizelab.org", target = "_blank")) ) ), br(), wellPanel( h4("Currently Selected Computation:"), verbatimTextOutput("selection_summary") ) ) ) ) # ============================================================================= # SERVER # ============================================================================= server <- function(input, output, session) { # ---- Data load (unchanged) ----------------------------------------------- load("Processed_OnoData.RData") Primary2016 <- read.csv("PrimaryCandidates2016 - Sheet1.csv") # ---- Reactive stores ------------------------------------------------------ cachedResults <- reactiveValues(data = list()) runningFlags <- reactiveValues(active = list()) # ---- Factor dropdown updater --------------------------------------------- observe({ req(input$case_type) if (input$case_type == "Average") { factors <- setdiff(colnames(FACTOR_MAT_FULL), "Office") } else { factors <- setdiff(colnames(FACTOR_MAT_FULL), c("Office", "Party.affiliation", "Party.competition")) } updateSelectInput(session, "factor", choices = factors, selected = factors[1]) }) # =========================================================================== # Compute Results button # =========================================================================== observeEvent(input$compute, { ## ---- CAPTURE reactive inputs ------------------------------------------ case_type <- isolate(input$case_type) respondent_group <- isolate(input$respondent_group) my_lambda <- isolate(input$lambda_input) label <- if (case_type == "Average") { paste0("Case=Average, Group=", respondent_group, ", Lambda=", my_lambda) } else { paste0("Case=Adversarial, Lambda=", my_lambda) } runningFlags$active[[label]] <- TRUE cachedResults$data[[label]] <- NULL updateSelectInput(session, "previousResults", choices = names(cachedResults$data), selected = label) shinyjs::html("status_text", "") shinyjs::html("status_text", "submitting…") # Immediately show “submitting…” shinyjs::delay(2000, shinyjs::html("status_text", "submitted")) # Two‑second later switch to “submitted” shinyjs::disable("compute") showNotification(sprintf("Job '%s' submitted …", label), type = "message", duration = 3) ## ---- FUTURE ----------------------------------------------------------- future({ strategize_start <- Sys.time() # --------------- shared hyper‑params ---------------------------------- params <- list( nSGD = 1000L, batch_size = 50L, penalty_type = "KL", nFolds = 3L, use_optax = TRUE, compute_se = FALSE, conf_level = 0.95, conda_env = "strategize", conda_env_required = TRUE ) if (case_type == "Average") { # ---------- Average case -------------------------------------------- indices <- if (respondent_group == "All") { which(my_data$Office == "President") } else { which(my_data_FULL$R_Partisanship == respondent_group & my_data$Office == "President") } FACTOR_MAT <- FACTOR_MAT_FULL[indices, !colnames(FACTOR_MAT_FULL) %in% c("Office", "Party.affiliation", "Party.competition")] Yobs <- Yobs_FULL[indices] X <- X_FULL[indices, ] pair_id <- pair_id_FULL[indices] assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)] Qoptimized <- strategize( Y = Yobs, W = FACTOR_MAT, X = X, pair_id = pair_id, p_list = assignmentProbList[colnames(FACTOR_MAT)], lambda = my_lambda, diff = TRUE, adversarial = FALSE, use_regularization = TRUE, K = 1L, nSGD = params$nSGD, penalty_type = params$penalty_type, folds = params$nFolds, use_optax = params$use_optax, compute_se = params$compute_se, conf_level = params$conf_level, conda_env = params$conda_env, conda_env_required = params$conda_env_required ) Qoptimized$n_strategies <- 1L } else { # ---------- Adversarial case ---------------------------------------- DROP <- c("Office", "Party.affiliation", "Party.competition") FACTOR_MAT <- FACTOR_MAT_FULL[, !colnames(FACTOR_MAT_FULL) %in% DROP] assignmentProbList <- assignmentProbList_FULL[!names(assignmentProbList_FULL) %in% DROP] # Build Primary slates FactorOptions <- apply(FACTOR_MAT, 2, table) prior_alpha <- 10 Primary_D <- Primary2016[Primary2016$Party == "Democratic", colnames(FACTOR_MAT)] Primary_R <- Primary2016[Primary2016$Party == "Republican", colnames(FACTOR_MAT)] slate_fun <- function(df) { lapply(colnames(df), function(col) { post <- FactorOptions[[col]]; post[] <- prior_alpha emp <- table(df[[col]]); emp <- emp[names(emp) != "Unclear"] post[names(emp)] <- post[names(emp)] + emp prop.table(post) }) |> setNames(colnames(df)) } slate_list <- list(Democratic = slate_fun(Primary_D), Republican = slate_fun(Primary_R)) indices <- which(my_data$R_Partisanship %in% c("Republican", "Democrat") & my_data$Office == "President") FACTOR_MAT <- FACTOR_MAT_FULL[indices, !colnames(FACTOR_MAT_FULL) %in% c("Office", "Party.competition", "Party.affiliation")] Yobs <- Yobs_FULL[indices] my_data_red <- my_data_FULL[indices, ] pair_id <- pair_id_FULL[indices] cluster_var <- cluster_var_FULL[indices] my_data_red$Party.affiliation_clean <- ifelse(my_data_red$Party.affiliation == "Republican Party", "Republican", ifelse(my_data_red$Party.affiliation == "Democratic Party","Democrat","Independent")) assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)] slate_list$Democratic <- slate_list$Democratic[names(assignmentProbList)] slate_list$Republican <- slate_list$Republican[names(assignmentProbList)] Qoptimized <- strategize( Y = Yobs, W = FACTOR_MAT, X = NULL, p_list = assignmentProbList, slate_list = slate_list, varcov_cluster_variable = cluster_var, competing_group_variable_respondent = my_data_red$R_Partisanship, competing_group_variable_candidate = my_data_red$Party.affiliation_clean, competing_group_competition_variable_candidate = my_data_red$Party.competition, pair_id = pair_id, respondent_id = my_data_red$respondentIndex, respondent_task_id = my_data_red$task, profile_order = my_data_red$profile, lambda = my_lambda, diff = TRUE, use_regularization = TRUE, force_gaussian = FALSE, adversarial = TRUE, K = 1L, nMonte_adversarial = 20L, nSGD = params$nSGD, penalty_type = params$penalty_type, learning_rate_max = 0.001, use_optax = params$use_optax, compute_se = params$compute_se, conf_level = params$conf_level, conda_env = params$conda_env, conda_env_required = params$conda_env_required ) Qoptimized$n_strategies <- 2L } Qoptimized$runtime_seconds <- as.numeric(difftime(Sys.time(), strategize_start, units = "secs")) Qoptimized[c("pi_star_point", "pi_star_se", "Q_point", "Q_se", "n_strategies", "runtime_seconds")] }) %...>% # success handler (function(res) { cachedResults$data[[label]] <- res runningFlags$active[[label]] <- FALSE updateSelectInput(session, "previousResults", choices = names(cachedResults$data), selected = label) shinyjs::html("status_text", "complete!") shinyjs::enable("compute") showNotification(sprintf("Job '%s' finished (%.1f s).", label, res$runtime_seconds), type = "message", duration = 6) }) %...!% # error handler (function(err) { runningFlags$active[[label]] <- FALSE cachedResults$data[[label]] <- NULL shinyjs::html("status_text", "error – see log") shinyjs::enable("compute") showNotification(paste("Error in", label, ":", err$message), type = "error", duration = 8) }) NULL # return value of observeEvent }) # ---- Helper: fetch selected result or show waiting msg ------------------- selectedResult <- reactive({ lbl <- input$previousResults ; req(lbl) if (isTRUE(runningFlags$active[[lbl]])) validate("Computation is still running – please wait…") res <- cachedResults$data[[lbl]] validate(need(!is.null(res), "No finished result selected.")) res }) # ---- Outputs ------------------------------------------------------------- output$strategy_plot <- renderPlot({ res <- selectedResult() plot_factor(res$pi_star_point, res$pi_star_se, factor_name = input$factor, n_strategies = res$n_strategies) }) output$q_value <- renderText({ res <- selectedResult() q_pt <- res$Q_point; q_se <- res$Q_se txt <- if (length(q_se) && q_se > 0) sprintf("Estimated Q Value: %.3f ± %.3f", q_pt, 1.96*q_se) else sprintf("Estimated Q Value: %.3f", q_pt) sprintf("%s (Runtime: %.2f s)", txt, res$runtime_seconds) }) output$selection_summary <- renderText({ input$previousResults }) } # ============================================================================= # Run the app # ============================================================================= shinyApp(ui, server)