Spaces:
Running
Running
| # 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(' | |
| <button id="share-button" | |
| style=" | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 5px 10px; | |
| font-size: 16px; | |
| font-weight: normal; | |
| color: #000; | |
| background-color: #fff; | |
| border: 1px solid #ddd; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| box-shadow: 0 1.5px 0 #000; | |
| "> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" | |
| stroke="currentColor" stroke-width="2" stroke-linecap="round" | |
| stroke-linejoin="round"> | |
| <circle cx="18" cy="5" r="3"></circle> | |
| <circle cx="6" cy="12" r="3"></circle> | |
| <circle cx="18" cy="19" r="3"></circle> | |
| <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line> | |
| <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line> | |
| </svg> | |
| <strong>Share</strong> | |
| </button> | |
| '), | |
| 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) | |