Standard Count Regression
Executive Summary

Executive Summary

What is associated with cnt across 731 observations

Observations
731
Model Used
Negative binomial
Dispersion
400.86
Pseudo R Squared
0.474
Significant Drivers
4
Top Driver
yr
A negative binomial regression on 731 observations explains cnt from 6 drivers. yr is the strongest driver: each additional unit multiplies the expected cnt by 1.65 — a 65% increase (very strong evidence). The counts are overdispersed (dispersion = 400.86, well above the 1.0 a plain Poisson model assumes), so the negative binomial model is the headline — its significance tests are honest where Poisson's would be overconfident. 4 of 6 drivers are statistically significant at p < 0.05, and the model accounts for 47.4% of the deviance in cnt.
What this means

The short answer

Year (yr) is the strongest driver: each additional year multiplies expected daily rentals by 1.65—a 65% increase with very strong evidence. Four of six drivers are statistically significant, and the overdispersion statistic (400.86) confirms that a negative binomial model is the only honest choice for this count data.

The detail

Negative binomial regression on 731 observations explains cnt from 6 drivers. The rate ratio for yr is 1.654 (95% CI: 1.569–1.744, p < 0.001). Dispersion = 400.86, well above 1.0, so Poisson would be overconfident. Significant drivers at p < 0.05: yr, season, weathersit, and holiday. The model accounts for 47.4% of deviance in cnt (pseudo R² = 0.474).

What this can't tell you

The model does not separate confounding or mediation among the six drivers. A stronger year effect could reflect unmeasured factors (marketing, infrastructure) rather than time itself. Isolating causal pathways would require instrumental variables or a longer time series to test for specific policy changes.

Overview

Analysis Overview

Negative binomial regression of cnt on 6 drivers across 731 observations.

N Observations731
Model UsedNegative binomial
Dispersion400.86
Pseudo R20.474
N Drivers6
Top Driveryr
What this means

The short answer

Daily bike rental counts are best explained by a negative binomial model rather than ordinary linear regression because counts are whole numbers with a natural floor at zero and variable spread — properties a count-specific model handles correctly. The model accounts for 47.4% of the variation in rental counts across 731 days.

The detail

Rental counts range from 0 to 8,714 across 731 observations. The data exhibits overdispersion (dispersion = 400.86), meaning variance grows faster than a simple Poisson model expects. A negative binomial regression was fitted with 6 drivers: season, yr, holiday, weekday, workingday, and weathersit. This model respects the count structure — non-negative integers with a floor at zero — and avoids invalid predictions and confidence intervals that linear regression would produce. The pseudo R² of 0.474 indicates the drivers explain a substantial but incomplete share of rental variation.

What this can't tell you

The model accounts for observed drivers but does not capture all sources of daily variation; 52.6% of deviance remains unexplained. Unmodeled factors — demand shocks, special events, or maintenance — may drive remaining volatility. A finer-grained temporal export (hourly or weekly aggregates) might reveal cyclical patterns not visible at daily resolution.

Data Preparation

Data Quality

Count validation, driver typing, and exclusions.

Initial Rows731
Final Rows731
Rows Removed0
What this means

The short answer

All 731 rows passed validation as valid count data. Numeric drivers were completed with median imputation; categorical drivers were cleaned by grouping rare levels. No rows were excluded, so the full dataset entered the model.

The detail

Count column validation confirmed all 731 rows as non-negative integers, the requirement for honest count modeling. Numeric drivers with missing values were imputed using the column median. Categorical drivers were recoded: rare levels grouped into 'Other' and blanks labeled 'Missing'. The outcome cnt ranges from 0 to 8,714 with a median of 4,548 and mean of 4504.35. All six mapped drivers (season, yr, holiday, weekday, workingday, weathersit) were usable; none were excluded.

What this can't tell you

The preprocessing report does not disclose which specific drivers had missing values or how many. A row-level export showing the original missingness pattern would clarify whether imputation affected the strength of any particular driver's association.

Data Table

Rate Ratios

Each driver's multiplicative effect on the expected cnt with 95% confidence intervals and plain-language significance.

TermRate RatioCI LowCI HighP ValueSignificance
Baseline (intercept)267223912988< 0.001very strong evidence
season1.2271.1981.256< 0.001very strong evidence
yr1.6541.5691.744< 0.001very strong evidence
holiday0.8430.7150.9930.0412significant
weekday1.0120.9991.0260.0713marginal
workingday1.0540.9941.1170.0814marginal
weathersit0.7680.7320.806< 0.001very strong evidence
What this means

The short answer

Year and season both raise daily rentals with very strong evidence; bad weather and holidays lower them. Year is the dominant lever: each additional year multiplies expected rentals by 1.654. Weekday and workingday effects are marginal and not reliably different from zero.

The detail

Rate ratios and 95% confidence intervals (negative binomial, n = 731):

  • yr: 1.654 (1.569–1.744, p < 0.001, very strong)
  • season: 1.227 (1.198–1.256, p < 0.001, very strong)
  • weathersit: 0.768 (0.732–0.806, p < 0.001, very strong)
  • holiday: 0.843 (0.715–0.993, p = 0.0412, significant)
  • weekday: 1.012 (0.999–1.026, p = 0.0713, marginal)
  • workingday: 1.054 (0.994–1.117, p = 0.0814, marginal)

The baseline expected count (all numeric drivers at zero, all categories at reference) is 2,672.409 rentals.

What this can't tell you

The confidence intervals for weekday and workingday include 1.0, so these effects cannot be separated from noise. The model does not reveal whether year's effect reflects growing demand, expanded supply, or seasonal shifts that are not fully captured by the season variable.

Visualization

Driver Ranking

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

What this means

The short answer

Year dominates the ranking (importance 100), followed by season (90.2). Weather conditions rank third (57), and the remaining three drivers (holiday, weekday, workingday) trail far behind with importance scores below 11, reflecting marginal or weak evidence.

The detail

Statistical influence ranking (scaled 0–100 from each driver's strongest z-statistic relative to yr):

  • yr: 100 (positive, very strong evidence)
  • season: 90.2 (positive, very strong evidence)
  • weathersit: 57 (negative, very strong evidence)
  • holiday: 10.9 (negative, significant)
  • weekday: 9.7 (positive, marginal)
  • workingday: 9.3 (positive, marginal)

Importance is a scale-free ranking; the gap between yr (100) and season (90.2) reflects the relative strength of their z-statistics. Drivers marked marginal are indistinguishable from noise.

What this can't tell you

The ranking does not account for collinearity among drivers. Weekday, workingday, and season may share information, so the true independent contribution of weekday is uncertain. A variance inflation factor or sensitivity analysis would clarify whether these weak effects are suppressed by overlap with season.

Visualization

Count Distribution

How the observed cnt values are distributed.

What this means

The short answer

Daily rental counts range from 0 to 8,714 with a median of 4,548 and mean of 4504.35. The distribution is fairly symmetric for count data, with no zero-count days. This shape—whole numbers, a floor at zero—is precisely what the count model is built for; linear regression would mishandle both the effects and the significance.

The detail

All 731 observations are non-negative integers. The distribution is centered near the median (4,548) with the mean (4504.35) close behind, indicating minimal skew. No rows have cnt = 0. The right tail extends to 8,714. This structure—bounded below, integer-valued, variance growing with the mean—is the signature of count data and the reason ordinary linear regression produces invalid confidence intervals and impossible negative predictions here.

What this can't tell you

The distribution does not reveal whether counts cluster by day-of-week or season, or whether the observed variance is driven by a few high-demand days or spread evenly. A stratified breakdown (counts by season, by weekday) would show whether the drivers in the model are capturing the major sources of heterogeneity.

Data Table

Model Quality

Overdispersion verdict, model choice, and fit statistics for the negative binomial model.

MetricValueInterpretation
Model usedNegative binomialChosen because dispersion = 400.86 — well above the 1.0 a Poisson model assumes, so a negative binomial model that absorbs the extra variance is the headline.
Overdispersion (Pearson chi-squared / df)400.86Above 1.5: counts vary more than a plain Poisson model allows, so Poisson p-values would be overconfident.
Pseudo R-squared (explained deviance)0.474The drivers account for 47.4% of the deviance in cnt.
Predicted vs observed correlation0.73Predicted counts track observed counts closely.
Observations modeled731Rows with a valid cnt value used in the model.
What this means

The short answer

The overdispersion statistic (400.86) is far above 1.0, confirming that counts vary much more than a plain Poisson model allows. The negative binomial model is the only honest choice. It explains 47.4% of the deviance and predicts observed counts with a correlation of 0.73.

The detail

Overdispersion (Pearson χ² / df) = 400.86, well above the 1.5 threshold that signals model misfit. A Poisson model would assume dispersion = 1.0 and produce overconfident p-values. The negative binomial absorbs this extra variance, so its significance tests are reliable. Pseudo R² = 0.474 means the six drivers account for 47.4% of deviance in cnt. Predicted vs. observed correlation = 0.73. All 731 rows with valid cnt entered the model.

What this can't tell you

The row-level correlation of 0.73 reflects individual-day noise inherent in count data; this does not impair the precision of rate ratios, which measure average multiplicative effects across the full sample. The model's fit would improve with additional drivers (e.g., events, weather detail), but the current six-driver effects are reliable regardless.

Rate this report Was this the answer you needed?
The exact source that produced this report — yours to keep, read, and re-run.
Download PDF
How this was computed method · R source · citation
The code that did it

Count Outcomes — Poisson & Negative Binomial

Models an event count (orders, tickets, defects, visits) on one or more driver columns chosen by the user. Fits a Poisson regression, tests for overdispersion via the Pearson dispersion statistic, and upgrades to a negative binomial model (quasi-Poisson fallback) when the data demands it. Reports every effect as a rate ratio with confidence intervals and significance, ranks drivers by statistical influence, and surfaces model quality.

Why This Method?

Counts are non-negative integers, usually right-skewed with many zeros, and their variance grows with the mean — every one of which violates ordinary linear regression. A log-link count model respects all of it, and its exponentiated coefficients read directly as business English: "each unit of X multiplies the expected count by Y."

What This Analysis Covers

  • Count-column validation (non-negative whole numbers) and distribution
  • Automatic overdispersion test and model selection (Poisson vs NB)
  • Rate ratio table with 95% confidence intervals and significance
  • Driver ranking by statistical influence
  • Model quality: dispersion, pseudo R-squared, predicted-vs-actual fit

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {count, 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 (!"count" %in% names(df)) {
    stop("column_mapping must map a &#x27;count' column (the event count 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)")
  }

  count_name <- humanize_semantic("count", col_map)
  driver_names <- setNames(humanize_semantic(driver_cols, col_map), driver_cols)

Step 2: Validate the count column — non-negative near-integers

A count model is only honest on true event counts. Require >= 95% of non-blank values to be numeric, non-negative, and whole (tolerance 1e-6); otherwise stop with a humanized, actionable message.

raw_chr <- as.character(df$count)
  nonblank <- !is.na(df$count) & trimws(raw_chr) != ""
  n_nonblank <- sum(nonblank)
  if (n_nonblank < 30) {
    stop(sprintf(
      "Only %d rows have a value in the count column(&#x27;%s'). At least 30 are required for a count regression.",
      n_nonblank, count_name))
  }
  conv <- suppressWarnings(as.numeric(raw_chr))
  ok <- nonblank & !is.na(conv) & conv >= 0 & abs(conv - round(conv)) <= 1e-6
  if (sum(ok) < 0.95 * n_nonblank) {
    stop(sprintf(
      paste0("The column mapped as the count(&#x27;%s') must contain non-negative whole ",
             "numbers — events per row, like orders, tickets, or visits. %s of %s ",
             "values are fractional, negative, or non-numeric, so a count model ",
             "cannot be fit. If this outcome is a continuous measure(revenue, ",
             "duration, a rate), use the linear regression tool instead."),
      count_name,
      format(n_nonblank - sum(ok), big.mark = ","),
      format(n_nonblank, big.mark = ",")))
  }
  df$count <- round(conv)
  df$count[!ok] <- NA_real_
  df <- df[!is.na(df$count), , drop = FALSE]
  if (nrow(df) < 30) {
    stop(sprintf(
      "Only %d rows have a usable value in the count column(&#x27;%s') after validation. At least 30 are required.",
      nrow(df), count_name))
  }

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_d <- suppressWarnings(as.numeric(as.character(v)))
      n_orig <- sum(!is.na(v) & as.character(v) != "")
      if (n_orig > 0 && sum(!is.na(conv_d)) >= 0.95 * n_orig) {
        df[[dc]] <- conv_d
      }
    }
    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"
      if (length(unique(v)) > nrow(df) / 2) {

Near-unique text column (an ID, not a driver) — exclude BEFORE lumping, or the lump step disguises identifiers as 12 categories.

dropped_drivers <- c(dropped_drivers, dc)
        next
      }
      tab <- sort(table(v), decreasing = TRUE)
      if (length(tab) > 12) {
        keep <- names(tab)[1:12]
        v[!(v %in% keep)] <- "Other"
      }
      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, "count"), 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, "count"), 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 Poisson; test overdispersion; upgrade if needed

Dispersion = Pearson chi-squared / residual df. Equidispersed Poisson data sits near 1. Above 1.5 we refit as negative binomial (MASS ships with base R distributions but availability is verified defensively); if MASS is unavailable or glm.nb fails to converge, quasi-Poisson keeps the point estimates and corrects the standard errors.

fit_pois <- glm(count ~ ., family = poisson(), data = df_clean)
  df_res <- df.residual(fit_pois)
  dispersion <- if (df_res > 0) {
    sum(residuals(fit_pois, type = "pearson")^2) / df_res
  } else NA_real_
  overdispersed <- is.finite(dispersion) && dispersion > 1.5

  model <- fit_pois
  model_label <- "Poisson"
  model_reason <- paste0(
    "dispersion = ", round(dispersion, 2),
    ", close enough to the 1.0 a Poisson model assumes")
  if (overdispersed) {
    nb_fit <- NULL
    if (requireNamespace("MASS", quietly = TRUE)) {
      nb_fit <- tryCatch(
        suppressWarnings(MASS::glm.nb(count ~ ., data = df_clean)),
        error = function(e) NULL)
    }
    if (!is.null(nb_fit)) {
      model <- nb_fit
      model_label <- "Negative binomial"
      model_reason <- paste0(
        "dispersion = ", round(dispersion, 2),
        " — well above the 1.0 a Poisson model assumes, so a negative ",
        "binomial model that absorbs the extra variance is the headline")
    } else {
      model <- glm(count ~ ., family = quasipoisson(), data = df_clean)
      model_label <- "Quasi-Poisson"
      model_reason <- paste0(
        "dispersion = ", round(dispersion, 2),
        " — well above the 1.0 a Poisson model assumes; a negative binomial ",
        "fit was unavailable, so quasi-Poisson corrects the standard errors")
    }
  }
  pseudo_r2 <- 1 - model$deviance / model$null.deviance
  if (!is.finite(pseudo_r2)) pseudo_r2 <- NA_real_

Step 7: Rate ratios — exp(coef) with 95% CIs (Wald, tryCatch-guarded)

tidy_df <- broom::tidy(model)
  ci <- tryCatch(suppressMessages(confint.default(model)),
                 error = function(e) NULL)
  if (!is.null(ci) && all(tidy_df$term %in% rownames(ci))) {
    ci_lo_raw <- ci[tidy_df$term, 1]
    ci_hi_raw <- ci[tidy_df$term, 2]
  } else {
    ci_lo_raw <- tidy_df$estimate - 1.96 * tidy_df$std.error
    ci_hi_raw <- tidy_df$estimate + 1.96 * tidy_df$std.error
  }
  safe_exp <- function(x) {
    out <- exp(x)
    out[!is.finite(out)] <- NA_real_
    round(out, 3)
  }
  rate_ratios_df <- data.frame(
    term         = humanize_term(tidy_df$term, model_drivers, col_map),
    rate_ratio   = safe_exp(tidy_df$estimate),
    ci_low       = safe_exp(ci_lo_raw),
    ci_high      = safe_exp(ci_hi_raw),
    p_value      = format_p(tidy_df$p.value),
    significance = sig_word(tidy_df$p.value),
    stringsAsFactors = FALSE
  )
  # broom/coef carry term rownames; reset so the renderer adds no "Row" column.
  rownames(rate_ratios_df) <- NULL

Step 8: Driver importance — max |z| per driver, NA-filtered, 0-100

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))

Aliased (perfectly collinear) driver terms carry NA statistics — no estimable independent effect. Filter NA BEFORE which.max so an all-NA driver is excluded instead of crashing (LAT-1445 class).

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) "positive" else "negative"
      } else "categorical",
      p_raw      = term_p[best],
      best_term  = term_raw[best],
      best_est   = term_est[best],
      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
    importance_df$significance <- sig_word(importance_df$p_raw)
    rownames(importance_df) <- NULL
  } else {
    importance_df <- data.frame(
      semantic = character(0), driver = character(0), abs_z = numeric(0),
      direction = character(0), p_raw = numeric(0), best_term = character(0),
      best_est = numeric(0), importance_score = numeric(0),
      significance = character(0), stringsAsFactors = FALSE)
  }
  top_driver_name <- if (nrow(importance_df) > 0) importance_df$driver[1] else "Unknown"
  top_term_rr     <- if (nrow(importance_df) > 0) round(exp(importance_df$best_est[1]), 2) else NA_real_
  top_term_p      <- if (nrow(importance_df) > 0) importance_df$p_raw[1] else NA_real_
  top_is_numeric  <- if (nrow(importance_df) > 0) importance_df$direction[1] != "categorical" else FALSE
  top_term_label  <- if (nrow(importance_df) > 0) {
    humanize_term(importance_df$best_term[1], model_drivers, col_map)
  } else "Unknown"

Step 9: Count distribution — <=1500 sample + zero share

set.seed(42)
  didx <- if (final_rows > 1500) sample(final_rows, 1500) else seq_len(final_rows)
  count_distribution_df <- data.frame(
    observed_count = as.integer(df_clean$count[didx]),
    stringsAsFactors = FALSE
  )
  pct_zero   <- round(100 * mean(df_clean$count == 0), 1)
  count_mean <- mean(df_clean$count)
  count_med  <- median(df_clean$count)
  count_max  <- max(df_clean$count)

Step 10: Predicted vs actual counts — fit check on the response scale

predicted <- as.numeric(fitted(model))
  pred_actual_cor <- suppressWarnings(
    cor(predicted, df_clean$count, use = "complete.obs"))

Step 11: Significant drivers + KPI metrics

n_sig <- sum(importance_df$p_raw < 0.05, na.rm = TRUE)
  metrics <- list(
    `Observations`        = final_rows,
    `Model Used`          = model_label,
    `Dispersion`          = round(dispersion, 2),
    `Pseudo R Squared`    = round(pseudo_r2, 3),
    `Significant Drivers` = as.integer(n_sig),
    `Top Driver`          = top_driver_name
  )

Step 12: Model quality table — all values as strings (no e-notation)

model_quality_df <- data.frame(
    metric = c("Model used",
               "Overdispersion(Pearson chi-squared / df)",
               "Pseudo R-squared(explained deviance)",
               "Predicted vs observed correlation",
               "Observations modeled"),
    value = c(model_label,
              formatC(dispersion, digits = 2, format = "f"),
              formatC(pseudo_r2, digits = 3, format = "f"),
              formatC(pred_actual_cor, digits = 2, format = "f"),
              format(final_rows, big.mark = ",")),
    interpretation = c(
      paste0("Chosen because ", model_reason, "."),
      if (overdispersed) {
        "Above 1.5: counts vary more than a plain Poisson model allows, so Poisson p-values would be overconfident."
      } else {
        "Near 1: the variance tracks the mean, so the plain Poisson model is adequate."
      },
      paste0("The drivers account for ", round(100 * pseudo_r2, 1),
             "% of the deviance in ", count_name, "."),
      if (is.finite(pred_actual_cor) && pred_actual_cor >= 0.7) {
        "Predicted counts track observed counts closely."
      } else if (is.finite(pred_actual_cor) && pred_actual_cor >= 0.4) {
        "Predicted counts capture the broad pattern; substantial row-level noise remains."
      } else {
        "Predicted counts explain only part of the row-level variation — counts are individually noisy."
      },
      paste0("Rows with a valid ", count_name, " value used in the model.")),
    stringsAsFactors = FALSE
  )

Step 13: json_output machine channel

n_terms_fit <- nrow(rate_ratios_df) - 1
  json_output <- list(
    answer = paste0(
      model_label, " regression of ", count_name, " on ",
      length(model_drivers),
      if (length(model_drivers) == 1) " driver" else " drivers",
      " across ", format(final_rows, big.mark = ","), " rows ",
      "(overdispersion statistic = ", round(dispersion, 2),
      if (overdispersed) ", so plain Poisson was rejected" else "", "). ",
      top_driver_name, " is the strongest driver: ",
      if (top_is_numeric) {
        paste0("each additional unit multiplies the expected ", count_name,
               " by ", top_term_rr)
      } else {
        paste0(top_term_label, " multiplies the expected ", count_name,
               " by ", top_term_rr, " versus the baseline")
      },
      " (", sig_word(top_term_p), "). ",
      n_sig, " of ", nrow(importance_df),
      if (nrow(importance_df) == 1) " driver is" else " drivers are",
      " significant at p < 0.05; pseudo R-squared = ", round(pseudo_r2, 3), "."
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "rate_ratios_table",
        "driver_importance", "count_distribution", "model_quality"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows          = initial_rows,
    final_rows            = final_rows,
    rows_removed          = rows_removed,
    count_name            = count_name,
    driver_names          = driver_names,
    model_drivers         = model_drivers,
    dropped_drivers       = dropped_drivers,
    df_clean              = df_clean,
    model                 = model,
    model_label           = model_label,
    model_reason          = model_reason,
    dispersion            = dispersion,
    overdispersed         = overdispersed,
    pseudo_r2             = pseudo_r2,
    rate_ratios_df        = rate_ratios_df,
    importance_df         = importance_df,
    top_driver_name       = top_driver_name,
    top_term_rr           = top_term_rr,
    top_term_p            = top_term_p,
    top_is_numeric        = top_is_numeric,
    top_term_label        = top_term_label,
    count_distribution_df = count_distribution_df,
    pct_zero              = pct_zero,
    count_mean            = count_mean,
    count_med             = count_med,
    count_max             = count_max,
    pred_actual_cor       = pred_actual_cor,
    model_quality_df      = model_quality_df,
    n_sig                 = n_sig,
    metrics               = metrics,
    json_output           = json_output
  )
}

Compute shared resources

shared <- compute_shared(df, params, col_map)

Finalize (do not modify)

Your data has more stories to tell.Run any analysis on your own data — validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No SignupSign Up Free

Cite this analysis

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