Standard Classification Drivers
Executive Summary

Executive Summary

What drives Churned toward 'Yes' across 800 observations

Observations
800
AUC
0.855
Pseudo R Squared
0.317
Accuracy
0.776
Significant Terms
4
Top Driver
Monthly Price
Across 800 observations (36.8% 'Yes'), Monthly Price is the strongest driver of Churned: each additional unit of Monthly Price multiplies the odds of Yes by 1.064 (p = 3.25e-34). Tenure Months ranks second. The model's discrimination is strong (AUC = 0.855), and 4 driver terms are statistically significant at p < 0.05. 3 of 4 mapped driver columns have at least one significant term.
Interpretation

Monthly Price emerges as the strongest driver of churn: each additional unit multiplies the odds of 'Yes' by 1.064 (p = 3.25e-34). Tenure Months ranks second, with longer tenure reliably lowering churn odds. The model discriminates strongly between churners and non-churners (AUC = 0.855) and all 4 driver terms reach statistical significance at p < 0.05. Three of four mapped driver columns contribute at least one significant effect. This concentration of signal across drivers, combined with strong discrimination, indicates that price, tenure, and plan type are reliable levers for understanding and potentially reducing churn risk.

Overview

Analysis Overview

Logistic regression of Churned on 4 driver columns across 800 observations.

N Observations800
Auc0.855
Pseudo R20.317
N Drivers4
Success Rate0.368
Top DriverMonthly Price
Interpretation

This analysis applies logistic regression to identify what drives churn across 800 observations, where 36.8% of customers churned. Four drivers—Monthly Price, Tenure Months, Plan Type, and Engagement Score—are modeled as independent contributors to the odds of churn. Each driver's effect is quantified as an odds ratio: values above 1 raise the odds of churn; values below 1 lower them. The model achieves strong discrimination (AUC = 0.855) and McFadden pseudo-R² = 0.317, indicating meaningful but incomplete predictive power. Odds ratios describe statistical association between drivers and churn, not causal mechanisms. This framework isolates each driver's contribution while holding others fixed, enabling targeted intervention prioritization.

Data Preparation

Data Quality

Outcome binarization, driver typing, and exclusions.

Initial Rows800
Final Rows800
Rows Removed0
Success Rate0.368
Interpretation

All 800 rows were retained with no exclusions. The outcome, Churned, was binarized with 'Yes' (36.8% of rows) as the success class and 'No' as the reference. Numeric drivers (Monthly Price, Tenure Months, Engagement Score) underwent median imputation for missing values; categorical drivers (Plan Type) were encoded with blanks treated as 'Missing' and rare levels grouped into 'Other' beyond 12 categories. No drivers were excluded as constant or identifier-like. This preprocessing ensures complete case coverage and consistent encoding across the logistic model, enabling reliable odds ratio estimation for all four mapped drivers.

Data Table

Odds Ratios

Each term's effect on the odds of 'Yes', with 95% confidence intervals.

TermOdds RatioCI LowCI HighP ValueSignificance
Intercept0.1060.03340.32860.0001***
Monthly Price1.0641.0541.0763.25e-34***
Tenure Months0.9560.94680.96498.36e-21***
Plan Type: Premium0.20970.12990.33347.72e-11***
Plan Type: Standard0.39860.25540.61680***
Engagement Score1.0010.98231.0190.956
Interpretation

Monthly Price has an odds ratio of 1.064 (95% CI: 1.054–1.076, p = 3.25e-34), meaning each unit increase multiplies churn odds by 1.064. Tenure Months shows 0.956 (95% CI: 0.947–0.965, p = 8.36e-21), so each additional month lowers churn odds by a factor of 0.956. Premium plan customers have 0.2097 times the odds of churners on the reference plan (p = 7.72e-11), and Standard plan customers have 0.3986 times those odds (p < 0.001). Engagement Score (odds ratio 1.001, 95% CI: 0.982–1.019, p = 0.956) shows no reliable effect—its confidence interval straddles 1. Four of five terms are statistically significant; Engagement Score alone lacks reliable association with churn.

Visualization

Driver Ranking

Relative statistical influence of each driver, scaled 0-100.

Interpretation

Monthly Price dominates with importance score 100, raising churn odds at p = 3.25e-34. Tenure Months follows at 76.7, lowering odds significantly (p = 8.36e-21). Plan Type ranks third at 53.3, with categorical effects (p = 7.72e-11). Engagement Score trails at 0.4 and is not statistically significant (p = 0.956). This ranking reflects the magnitude of each driver's z-statistic relative to Monthly Price, providing a scale-free measure of statistical influence. For churn reduction, focus first on pricing strategy and tenure retention, then on plan positioning. Engagement Score carries negligible predictive weight and may warrant exclusion from future operational models.

Visualization

Predicted Probabilities

How confidently the model scores each row toward 'Yes'.

Interpretation

The model predicts an average probability of churn of 0.6 for actual churners and 0.232 for non-churners—a separation gap of 0.368. This moderate separation, visible as two distinct humps in the predicted probability distribution, confirms that the four drivers contain real signal for distinguishing churn risk. The humped structure (rather than a single central lump) indicates the drivers successfully stratify customers into higher- and lower-risk cohorts. However, the overlap between distributions reflects residual uncertainty: some customers with low-risk profiles still churn, and some high-risk profiles do not. This pattern aligns with the model's 77.6% accuracy at the 0.5 threshold, suggesting practical utility for risk segmentation despite incomplete predictive certainty.

Data Table

Model Quality

Discrimination, fit, and 0.5-threshold classification metrics.

MetricValueInterpretation
AUC0.855Probability a random 'Yes' row is scored above a random 'No' row — strong discrimination.
McFadden pseudo-R²0.317Improvement in fit over an intercept-only model (0 = none, 1 = perfect).
Accuracy (0.5 threshold)0.776Share of rows classified correctly when predicting 'Yes' above probability 0.5.
Sensitivity0.643Share of actual 'Yes' rows the model catches.
Specificity0.854Share of actual 'No' rows the model correctly clears.
Baseline accuracy0.633Accuracy from always predicting the majority class ('No').
Interpretation

The model achieves AUC = 0.855, indicating strong discrimination—a random churner is correctly ranked above a random non-churner 85.5% of the time. McFadden pseudo-R² = 0.317 shows the four drivers improve fit substantially over an intercept-only baseline. At the 0.5 probability threshold, accuracy reaches 77.6%—a 14.4 percentage point lift over the 63.3% baseline from always predicting 'No'. Sensitivity (64.3%) reveals the model catches roughly two-thirds of actual churners, while specificity (85.4%) confirms it correctly clears most non-churners. This profile—strong discrimination, higher specificity than sensitivity—suits risk identification: the model is conservative (few false alarms) but misses some true churners, appropriate for targeted retention campaigns where precision matters.

Methodology

Methodology

Statistical methodology and diagnostics for Logistic Regression — What Drives Yes vs No

Statistical Method

Logistic Regression — What Drives Yes vs No

Standard-library analysis: which factors drive a binary outcome — churned or retained, converted or not, defaulted or paid? Logistic regression turns each driver into an odds ratio you can act on, ranks the drivers by statistical influence, shows how cleanly the model separates the two groups, and reports honest quality metrics (AUC, accuracy, sensitivity, specificity). Works on any dataset: map a yes/no outcome and 1-8 candidate drivers.

Data
N = 800 observations
Assumptions
  • The outcome is binary (exactly two levels)
  • Driver effects are linear on the log-odds scale
  • Observations are independent
Limitations
  • Association, not causation — odds ratios describe co-movement, not intervention effects
  • Perfectly separating drivers make coefficients unstable (flagged in the narrative, not hidden)
  • Rare outcomes (few yes or few no rows) widen confidence intervals
Software & Citation
MCP Analytics · mcpanalytics.ai
Code Appendix

Analysis Code

Complete R source code for this analysis

Logistic Regression — What Drives Yes vs No

Fits a logistic regression explaining a binary outcome (churned, converted, defaulted) from one or more driver columns chosen by the user. Reports each driver's effect as an odds ratio with a 95% confidence interval, ranks drivers by statistical influence, shows the predicted-probability distribution, and summarizes model quality with AUC, McFadden pseudo-R², and confusion metrics.

Why This Method?

Logistic regression is the workhorse for yes/no outcomes: each driver's effect becomes an odds ratio ("one unit of X multiplies the odds by Y"), significance tests separate signal from noise, and AUC gives an honest read on how well the drivers actually distinguish the two classes.

What This Analysis Covers

  • Odds ratios with 95% confidence intervals and significance
  • Driver ranking by statistical influence (|z|)
  • Predicted-probability distribution (class separation)
  • Model quality: AUC, pseudo-R², accuracy, sensitivity, specificity

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {outcome, driver_1..driver_N}. All narrative is derived from the user's own column names and computed values.

suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))

Step 1: Row accounting + semantic column discovery

initial_rows <- nrow(df)
  if (!"outcome" %in% names(df)) {
    stop("column_mapping must map an &#x27;outcome' column (the yes/no value to explain)")
  }
  driver_cols <- grep("^driver_[0-9]+$", names(df), value = TRUE)
  driver_cols <- driver_cols[order(as.integer(sub("^driver_", "", driver_cols)))]
  if (length(driver_cols) == 0) {
    stop("column_mapping must map at least one driver column(driver_1)")
  }

  outcome_name <- humanize_semantic("outcome", col_map)
  driver_names <- setNames(humanize_semantic(driver_cols, col_map), driver_cols)

Step 2: Binarize the outcome (robust 0/1, logical, two-level string)

v_raw <- df$outcome
  if (is.logical(v_raw)) {
    df <- df[!is.na(v_raw), , drop = FALSE]
    y <- as.integer(df$outcome)
    success_label <- "TRUE"
    failure_label <- "FALSE"
  } else {
    vc <- trimws(as.character(v_raw))
    keep <- !is.na(v_raw) & !is.na(vc) & vc != ""
    df <- df[keep, , drop = FALSE]
    vc <- vc[keep]
    lv <- sort(unique(vc))
    if (length(lv) < 2) {
      stop(sprintf(
        "The outcome column(&#x27;%s') has only one value ('%s') — a yes/no outcome needs both classes present.",
        outcome_name, if (length(lv) == 1) lv[1] else "empty"))
    }
    if (length(lv) > 2) {
      stop(sprintf(
        "The outcome column(&#x27;%s') has %d distinct values (%s%s) — a yes/no outcome must have exactly two, such as churned/retained, yes/no, or 0/1.",
        outcome_name, length(lv), paste(head(lv, 5), collapse = ", "),
        if (length(lv) > 5) ", …" else ""))
    }

Which level is the "success" (event)? Match common positive labels case-insensitively; otherwise take the alphabetically-last level.

pat <- "^(yes|true|1|churned|converted|clicked|purchased|default|fraud)$"
    hits <- lv[grepl(pat, tolower(lv))]
    success_label <- if (length(hits) == 1) hits else lv[2]
    failure_label <- setdiff(lv, success_label)[1]
    y <- as.integer(vc == success_label)
  }
  df$outcome <- y
  if (nrow(df) < 30) {
    stop(sprintf(
      "Only %d rows have a usable value in the outcome column(&#x27;%s'). At least 30 are required.",
      nrow(df), outcome_name))
  }
  if (sum(y == 1) == 0 || sum(y == 0) == 0) {
    stop(sprintf(
      "After cleaning, the outcome column(&#x27;%s') contains only one class — both outcomes must be present.",
      outcome_name))
  }
  success_rate <- mean(y)

Step 3: Type each driver — numeric if >=95% of values convert, else factor

dropped_drivers <- character(0)
  for (dc in driver_cols) {
    v <- df[[dc]]
    if (!is.numeric(v)) {
      conv <- suppressWarnings(as.numeric(as.character(v)))
      n_orig <- sum(!is.na(v) & as.character(v) != "")
      if (n_orig > 0 && sum(!is.na(conv)) >= 0.95 * n_orig) {
        df[[dc]] <- conv
      }
    }
    v <- df[[dc]]
    if (is.numeric(v)) {

Numeric: impute NA with median

med <- median(v, na.rm = TRUE)
      if (is.na(med)) { dropped_drivers <- c(dropped_drivers, dc); next }
      v[is.na(v)] <- med
      df[[dc]] <- v
    } else {

Categorical: blank/NA -> "Missing"; lump beyond 12 levels into "Other"

v <- as.character(v)
      v[is.na(v) | trimws(v) == ""] <- "Missing"
      tab <- sort(table(v), decreasing = TRUE)
      if (length(tab) > 12) {
        keep <- names(tab)[1:12]
        v[!(v %in% keep)] <- "Other"
      }
      if (length(unique(v)) > nrow(df) / 2) {

Near-unique text column (an ID, not a driver) — exclude

dropped_drivers <- c(dropped_drivers, dc)
        next
      }
      df[[dc]] <- factor(v)
    }
  }

Step 4: Drop zero-variance drivers

for (dc in setdiff(driver_cols, dropped_drivers)) {
    v <- df[[dc]]
    zero_var <- if (is.numeric(v)) {
      isTRUE(var(v, na.rm = TRUE) == 0) || is.na(var(v, na.rm = TRUE))
    } else {
      length(unique(v)) <= 1
    }
    if (zero_var) dropped_drivers <- c(dropped_drivers, dc)
  }
  model_drivers <- setdiff(driver_cols, dropped_drivers)
  if (length(model_drivers) == 0) {
    stop("No usable driver columns remained after cleaning(all were constant, empty, or identifier-like).")
  }

  df_clean <- df[, c(model_drivers, "outcome"), drop = FALSE]
  final_rows <- nrow(df_clean)
  rows_removed <- initial_rows - final_rows

Step 5: Guard — need clearly more rows than model terms

n_terms_est <- sum(sapply(model_drivers, function(dc) {
    if (is.factor(df_clean[[dc]])) nlevels(df_clean[[dc]]) - 1 else 1
  }))
  while (n_terms_est >= final_rows - 5 && length(model_drivers) > 1) {
    drop_dc <- model_drivers[length(model_drivers)]
    dropped_drivers <- c(dropped_drivers, drop_dc)
    model_drivers <- model_drivers[-length(model_drivers)]
    df_clean <- df_clean[, c(model_drivers, "outcome"), drop = FALSE]
    n_terms_est <- sum(sapply(model_drivers, function(dc) {
      if (is.factor(df_clean[[dc]])) nlevels(df_clean[[dc]]) - 1 else 1
    }))
  }

Step 6: Fit logistic regression, capturing convergence warnings

glm_warnings <- character(0)
  model <- withCallingHandlers(
    glm(outcome ~ ., data = df_clean, family = binomial()),
    warning = function(w) {
      glm_warnings <<- c(glm_warnings, conditionMessage(w))
      invokeRestart("muffleWarning")
    }
  )
  pred <- as.numeric(fitted(model))
  yv <- df_clean$outcome

Separation / convergence diagnostics — note in narrative, never crash

sep_frac <- mean(pred < 1e-3 | pred > 1 - 1e-3)
  separation_flag <- sep_frac > 0.99
  nonconv_flag <- any(grepl("did not converge|fitted probabilities numerically 0 or 1",
                            glm_warnings))
  fit_note <- if (separation_flag) {
    paste0("Caution: one or more drivers separate the two outcome classes ",
           "almost perfectly(", round(100 * sep_frac, 1), "% of predicted ",
           "probabilities are at 0 or 1). Coefficients and odds ratios for ",
           "the separating driver are unstable and their confidence ",
           "intervals unreliable — treat the ranking, not the magnitudes, ",
           "as the finding.")
  } else if (nonconv_flag) {
    paste0("Caution: the model fit raised convergence warnings(often a ",
           "sign of near-perfect separation or sparse categories). ",
           "Estimates near the extremes should be read with care.")
  } else ""

Step 7: Odds ratios — broom::tidy + exp(); CIs fall back to NA

tidy_df <- tryCatch(
    suppressWarnings(suppressMessages(
      broom::tidy(model, conf.int = TRUE, conf.level = 0.95))),
    error = function(e) {
      td <- broom::tidy(model)
      td$conf.low <- NA_real_
      td$conf.high <- NA_real_
      td
    }
  )
  odds_ratios_df <- data.frame(
    term         = humanize_term(tidy_df$term, model_drivers, col_map),
    odds_ratio   = signif(exp(tidy_df$estimate), 4),
    ci_low       = signif(exp(tidy_df$conf.low), 4),
    ci_high      = signif(exp(tidy_df$conf.high), 4),
    p_value      = signif(tidy_df$p.value, 3),
    significance = ifelse(is.na(tidy_df$p.value), "",
                   ifelse(tidy_df$p.value < 0.001, "***",
                   ifelse(tidy_df$p.value < 0.01,  "**",
                   ifelse(tidy_df$p.value < 0.05,  "*",
                   ifelse(tidy_df$p.value < 0.1,   ".", ""))))),
    stringsAsFactors = FALSE
  )
  n_sig <- sum(tidy_df$p.value < 0.05 & tidy_df$term != "(Intercept)", na.rm = TRUE)

Step 8: Driver importance — max |z| across a driver's terms, 0-100.

Aliased (perfectly collinear) drivers have NA/absent statistics — no estimable independent effect; exclude from the ranking instead of crashing on which.max(all-NA) -> integer(0) (LAT-1445 guard).

term_raw <- tidy_df$term[tidy_df$term != "(Intercept)"]
  term_z   <- tidy_df$statistic[tidy_df$term != "(Intercept)"]
  term_est <- tidy_df$estimate[tidy_df$term != "(Intercept)"]
  term_p   <- tidy_df$p.value[tidy_df$term != "(Intercept)"]
  imp_rows <- lapply(model_drivers, function(dc) {
    idx <- which(startsWith(term_raw, dc))
    idx <- idx[!is.na(term_z[idx])]
    if (length(idx) == 0) return(NULL)
    best <- idx[which.max(abs(term_z[idx]))]
    data.frame(
      semantic  = dc,
      driver    = driver_names[[dc]],
      abs_z     = abs(term_z[best]),
      direction = if (is.numeric(df_clean[[dc]])) {
        if (term_est[best] > 0) "raises the odds" else "lowers the odds"
      } else "categorical",
      p_value   = signif(term_p[best], 3),
      top_term  = humanize_term(term_raw[best], model_drivers, col_map),
      top_or    = signif(exp(term_est[best]), 4),
      stringsAsFactors = FALSE
    )
  })
  importance_df <- do.call(rbind, Filter(Negate(is.null), imp_rows))
  if (!is.null(importance_df) && nrow(importance_df) > 0) {
    importance_df <- importance_df[order(-importance_df$abs_z), , drop = FALSE]
    max_z <- max(importance_df$abs_z, na.rm = TRUE)
    importance_df$importance_score <- if (is.finite(max_z) && max_z > 0) {
      round(100 * importance_df$abs_z / max_z, 1)
    } else 0
    rownames(importance_df) <- NULL
  } else {
    importance_df <- data.frame(semantic = character(0), driver = character(0),
                                abs_z = numeric(0), direction = character(0),
                                p_value = numeric(0), top_term = character(0),
                                top_or = numeric(0), importance_score = numeric(0),
                                stringsAsFactors = FALSE)
  }
  top_driver_name <- if (nrow(importance_df) > 0) importance_df$driver[1] else "Unknown"

Driver COLUMNS (not terms) with at least one significant term at p<0.05 — business readers think in drivers, so the tldr reports this alongside n_sig.

n_sig_drivers <- sum(vapply(model_drivers, function(dc) {
    idx <- which(startsWith(term_raw, dc))
    any(term_p[idx] < 0.05, na.rm = TRUE)
  }, logical(1)))

Plain-English odds statement for the strongest driver

top_driver_phrase <- if (nrow(importance_df) > 0) {
    top <- importance_df[1, ]
    if (top$direction == "categorical") {
      lvl <- sub("^.*: ", "", top$top_term)
      paste0("the group &#x27;", lvl, "' has ", top$top_or,
             "x the odds of ", success_label, " versus its reference group")
    } else {
      paste0("each additional unit of ", top$driver,
             " multiplies the odds of ", success_label, " by ", top$top_or)
    }
  } else "no driver had an estimable effect"

Step 9: Model quality — pseudo-R², rank-based AUC, confusion at 0.5

ll_full <- suppressWarnings(as.numeric(logLik(model)))
  null_model <- suppressWarnings(glm(outcome ~ 1, data = df_clean, family = binomial()))
  ll_null <- suppressWarnings(as.numeric(logLik(null_model)))
  mcfadden <- if (is.finite(ll_full) && is.finite(ll_null) && ll_null != 0) {
    1 - ll_full / ll_null
  } else NA_real_

  n1 <- sum(yv == 1)
  n0 <- sum(yv == 0)
  auc <- if (n1 > 0 && n0 > 0) {
    r <- rank(pred)
    (sum(r[yv == 1]) - n1 * (n1 + 1) / 2) / (as.numeric(n1) * as.numeric(n0))
  } else NA_real_

  pred_class <- as.integer(pred >= 0.5)
  tp <- sum(pred_class == 1 & yv == 1)
  tn <- sum(pred_class == 0 & yv == 0)
  fp <- sum(pred_class == 1 & yv == 0)
  fn <- sum(pred_class == 0 & yv == 1)
  accuracy    <- (tp + tn) / length(yv)
  sensitivity <- if ((tp + fn) > 0) tp / (tp + fn) else NA_real_
  specificity <- if ((tn + fp) > 0) tn / (tn + fp) else NA_real_
  baseline_acc <- max(success_rate, 1 - success_rate)

  band <- auc_band(auc)
  model_quality_df <- data.frame(
    metric = c("AUC", "McFadden pseudo-R²", "Accuracy(0.5 threshold)",
               "Sensitivity", "Specificity", "Baseline accuracy"),
    value = round(c(auc, mcfadden, accuracy, sensitivity, specificity,
                    baseline_acc), 3),
    interpretation = c(
      paste0("Probability a random &#x27;", success_label, "' row is scored above a random '",
             failure_label, "&#x27; row — ", band, " discrimination."),
      "Improvement in fit over an intercept-only model(0 = none, 1 = perfect).",
      paste0("Share of rows classified correctly when predicting &#x27;", success_label,
             "&#x27; above probability 0.5."),
      paste0("Share of actual &#x27;", success_label, "' rows the model catches."),
      paste0("Share of actual &#x27;", failure_label, "' rows the model correctly clears."),
      paste0("Accuracy from always predicting the majority class(&#x27;",
             if (success_rate >= 0.5) success_label else failure_label, "&#x27;).")
    ),
    stringsAsFactors = FALSE
  )

Step 10: Predicted-probability distribution — <=2000 sample

set.seed(42)
  sidx <- if (length(pred) > 2000) sample(length(pred), 2000) else seq_along(pred)
  probability_distribution_df <- data.frame(
    predicted_probability = round(pred[sidx], 4),
    stringsAsFactors = FALSE
  )
  mean_p1 <- mean(pred[yv == 1])
  mean_p0 <- mean(pred[yv == 0])

Step 11: KPI metrics (user-facing keys)

metrics <- list(
    `Observations`        = final_rows,
    `AUC`                 = round(auc, 3),
    `Pseudo R Squared`    = round(mcfadden, 3),
    `Accuracy`            = round(accuracy, 3),
    `Significant Terms`   = as.integer(n_sig),
    `Top Driver`          = top_driver_name
  )

Step 12: json_output machine channel

json_output <- list(
    answer = paste0(
      "Logistic regression of ", outcome_name, " (success = &#x27;", success_label,
      "&#x27;, ", round(100 * success_rate, 1), "% of rows) on ",
      length(model_drivers), " driver(s) across ",
      format(final_rows, big.mark = ","), " rows: ", top_driver_name,
      " is the strongest driver — ", top_driver_phrase, " (p = ",
      importance_df$p_value[1] %||% NA, "). AUC = ", round(auc, 3), " (",
      band, "), McFadden pseudo-R² = ", round(mcfadden, 3), "; ", n_sig,
      if (n_sig == 1) " driver term" else " driver terms",
      " significant at p<0.05.",
      if (nchar(fit_note) > 0) paste0(" ", fit_note) else ""
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "odds_ratios_table",
        "driver_importance", "probability_distribution", "model_quality"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows  = initial_rows,
    final_rows    = final_rows,
    rows_removed  = rows_removed,
    outcome_name  = outcome_name,
    success_label = success_label,
    failure_label = failure_label,
    success_rate  = success_rate,
    driver_names  = driver_names,
    model_drivers = model_drivers,
    dropped_drivers = dropped_drivers,
    df_clean      = df_clean,
    model         = model,
    fit_note      = fit_note,
    odds_ratios_df = odds_ratios_df,
    importance_df = importance_df,
    top_driver_name = top_driver_name,
    top_driver_phrase = top_driver_phrase,
    probability_distribution_df = probability_distribution_df,
    mean_p1       = mean_p1,
    mean_p0       = mean_p0,
    model_quality_df = model_quality_df,
    auc           = auc,
    mcfadden      = mcfadden,
    accuracy      = accuracy,
    sensitivity   = sensitivity,
    specificity   = specificity,
    baseline_acc  = baseline_acc,
    n_sig         = n_sig,
    n_sig_drivers = n_sig_drivers,
    metrics       = metrics,
    json_output   = json_output
  )
}
Your data has more stories to tell. Run any analysis on your own data — 60+ validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No Signup Sign Up Free

Report an Issue

Tell us what's wrong. You'll get a free re-run of this analysis so you can try again with different parameters. If the re-run still doesn't meet your expectations, we'll refund your credits.

Want to run this analysis on your own data? Upload CSV — Free Analysis See Pricing