cjerzak's picture
Create app_v2.R
2e34193 verified
# 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)