Executive Summary
Net Promoter Score from Likelihood to Recommend
The short answer
Overall NPS is +15 (95% CI +9 to +21). By segment, Pro plan scores +30 (95% CI +21 to +39) while Free plan scores +0 (95% CI -9 to +9)—a gap of +30 points that is statistically significant and real, not attributable to chance.
The detail
Across 600 responses, the promoter/passive/detractor split is 40% / 35% / 25%. The Pro plan NPS of +30 versus Free plan NPS of +0 yields a z-statistic of 4.7 with p < 0.0001, well below the conventional 0.05 threshold. The confidence intervals do not overlap (Pro: +21 to +39; Free: -9 to +9), confirming the difference is genuine. Both segments exceed the 30-response minimum for valid comparison.
What this can't tell you
The analysis does not explain why Pro plan respondents are more likely to recommend; it measures the difference but not its source. Segment size and composition details are not provided here.
Analysis Overview
Net Promoter Score from 600 responses to Likelihood to Recommend.
The short answer
The Net Promoter Score across 600 responses is +15 on the standard -100 to +100 scale, with a 95% confidence interval of +9 to +21. This reflects the standard NPS method: promoters (9–10 rating) minus detractors (0–6), each expressed as a percentage. The confidence interval reflects sampling variability and anchors the precision of the estimate.
The detail
NPS is calculated as the percentage of promoters (40%) minus the percentage of detractors (25%), yielding +15 points. The 95% confidence interval (+9 to +21) is derived from the classical multinomial variance formula and accounts for the sample size of 600 responses. The interval width of 12.7 points indicates moderate precision; the true population NPS is reasonably likely to fall within this range. Two customer segments are present, each with its own NPS and interval, enabling direct comparison.
What this can't tell you
The overall NPS does not reveal which specific product features or service elements drive the score, nor does it distinguish satisfaction drivers within the detractor or promoter groups.
Data Quality
Row cleaning and scale validation applied before scoring.
The short answer
Of 616 rows loaded, 600 valid 0-10 responses were retained. Twelve responses with blank or non-numeric scores and 4 responses with scores outside the 0-10 range were dropped, leaving no data quality issues in the final dataset.
The detail
Initial dataset: 616 rows. Dropped: 12 rows with blank or non-numeric Likelihood to Recommend values; 4 rows with values outside 0-10. Final usable responses: 600. Non-integer scores: 0 rows required rounding. The 0-10 scale validation step confirmed no responses fell outside the valid range after cleaning.
What this can't tell you
Dropping out-of-range responses is appropriate for NPS, but the analysis does not assess whether those 4 invalid responses came from a particular segment or time period, which could reveal data collection issues.
Score Distribution
How the 600 responses spread across the 0-10 scale.
The short answer
Responses cluster at opposite ends of the scale: the most common score is 9, with 40% of all responses in the promoter zone (9–10), while 25% fall in the detractor zone (0–6). The distribution is polarized rather than centered, indicating two distinct customer experiences coexist.
The detail
Score 9 is the single most frequent response. The 9–10 band contains 240 responses (40%), the 7–8 band (passives) contains 210 responses (35%), and the 0–6 band (detractors) contains 150 responses (25%). Within detractors, 10.3% score 3 or below, representing a clearly dissatisfied subgroup. Scores 7 and 8 each account for over 100 responses (107 and 103, respectively), anchoring the passive segment.
What this can't tell you
The bimodal pattern shows satisfaction is split, but the data does not identify which customer cohorts or use cases fall into each mode, nor does it explain the drivers of the two distinct clusters.
Promoters, Passives & Detractors
The NPS composition of each Customer Segment segment.
The short answer
Pro plan has a healthier composition (50% promoters, 20% detractors) than Free plan (30% promoters, 30% detractors). The Free plan's equal split between promoters and detractors is its drag on NPS; the Pro plan's promoter advantage is its strength.
The detail
Overall: 40% promoters, 35% passives, 25% detractors. Pro plan: 50% promoters, 30% passives, 20% detractors (NPS +30). Free plan: 30% promoters, 40% passives, 30% detractors (NPS +0). Free plan's detractor share equals its promoter share, yielding a neutral NPS. Pro plan's 50% promoter rate exceeds its 20% detractor rate by 30 percentage points, directly driving the +30 NPS advantage. Protecting the Pro plan's promoter base and reducing Free plan detractors are both critical.
What this can't tell you
The analysis does not show which specific features or experiences separate promoters from detractors within each plan.
NPS by Segment
NPS, sample size, and 95% CI per Customer Segment segment.
| Segment | N | Nps | CI | Flag |
|---|---|---|---|---|
| Pro plan | 300 | 30 | +21 to +39 | |
| Free plan | 300 | 0 | -9 to +9 |
The short answer
Pro plan's NPS of +30 is statistically significantly higher than Free plan's NPS of +0. The 30-point gap is real, not random noise: the confidence intervals do not overlap, and a formal z-test confirms the difference (z = 4.7, p < 0.0001).
The detail
Pro plan: NPS +30, 95% CI +21 to +39, n=300. Free plan: NPS +0, 95% CI −9 to +9, n=300. The gap of 30 points is statistically significant (z = 4.7, p < 0.0001). Non-overlapping confidence intervals confirm the difference is genuine: Pro plan's lower bound (+21) exceeds Free plan's upper bound (+9). Both segments exceed the 30-response minimum for the z-test.
What this can't tell you
The analysis does not determine whether the difference reflects product quality, user expectations, or selection bias (different customer types choosing each plan).
Method & Fine Print
Formulas, validation rules, and this dataset's actual margin of error.
| Item | Detail |
|---|---|
| Score categories | Promoters score 9-10, passives 7-8, detractors 0-6 on the 0-10 likelihood-to-recommend scale. |
| NPS formula | NPS = % promoters minus % detractors, stated as a point score from -100 to +100. |
| Confidence interval | Classical multinomial variance formula: Var(NPS) = (p_promoter + p_detractor - (p_promoter - p_detractor)^2) / n on the proportion scale; SE in points = 100 x sqrt(Var); 95% CI = NPS plus or minus 1.96 x SE. |
| This dataset | n = 600 valid responses; SE = 3.2 points; the 95% interval spans 13 points (+9 to +21). |
| Segment comparison | Two-proportion-style z-test on the NPS difference between the best and worst segment, run only when both have at least 30 responses; otherwise the comparison is reported as underpowered. Gaps with overlapping 95% confidence intervals are never called differences. |
| Scale validation | Values outside 0-10 are dropped and reported. A column whose values sit entirely within 1-5 stops the analysis — a 1-5 rating scale cannot be honestly rescaled to NPS. |
| Small-sample policy | The confidence interval width in points is always stated alongside the score, so a small-sample NPS reads as the rough estimate it is. |
The short answer
NPS is % promoters minus % detractors, in points. The 95% confidence interval uses the classical multinomial variance formula to account for sampling error. Segment comparisons use a two-proportion z-test only when both segments have at least 30 responses; overlapping intervals mean no difference is claimed.
The detail
NPS formula: % promoters (9-10) − % detractors (0-6), stated as a point score from −100 to +100. Confidence interval: Var(NPS) = (p_promoter + p_detractor − (p_promoter − p_detractor)²) / n; on this dataset, SE = 3.2 points; 95% CI = NPS ± 1.96 × SE. Segment comparison: two-proportion z-test applied only when both segments have ≥30 responses; gaps with overlapping 95% confidence intervals are not called differences. Scale validation: values outside 0-10 are dropped and reported. This dataset: n=600, SE=3.2 points, interval width=13 points (+9 to +21).
What this can't tell you
The multinomial variance formula assumes independent responses and does not account for clustering (e.g., multiple responses per customer or organization).
Methodology
Statistical methodology and diagnostics for NPS Analysis
Statistical Method
Standard-library analysis: Net Promoter Score from a 0-10 likelihood-to-recommend column. Promoter / passive / detractor shares, the NPS point score with a proper 95% confidence interval, the full 0-10 score distribution, and per-segment NPS with a statistically honest best-vs-worst comparison. Works on any survey export: map the 0-10 score column, optionally a segment column.
- The score column holds responses on the standard 0-10 likelihood-to-recommend scale
- Responses are independent (one row per respondent)
- The sample is representative of the population you want the NPS to describe
- NPS on small samples is noisy — the confidence interval, stated in points, is part of the answer
- Segment gaps with overlapping confidence intervals are not called differences
- A 1-5 rating scale cannot be converted to NPS; the analysis stops rather than silently rescaling
- NPS says how likely people claim they are to recommend — not why; pair it with verbatim feedback
Analysis Code
Complete R source code for this analysis
NPS Analysis — Net Promoter Score with Honest Uncertainty
Computes the Net Promoter Score from a 0-10 likelihood-to-recommend column: promoter / passive / detractor shares, the NPS point score with a 95% confidence interval from the classical multinomial variance formula, the full 0-10 score distribution, and per-segment NPS with a statistically honest best-vs-worst comparison.
Why This Method?
NPS is usually reported as a bare number, which hides how noisy it is on small samples. Stating the confidence interval in points, and refusing to call overlapping-interval segment gaps "differences", turns the same arithmetic into a trustworthy answer.
What This Analysis Covers
- NPS = % promoters (9-10) minus % detractors (0-6), in points
- 95% CI via Var(NPS) = (p9 + p0_6 - (p9 - p0_6)^2) / n
- The raw 0-10 distribution and the promoter/passive/detractor mix
- Per-segment NPS with CIs and a gated best-vs-worst z-test
Standard Library
Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {score, segment}. 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))Core Analysis Pipeline
compute_shared <- function(df, params, col_map = list()) {
# === SHARED EXPORTS ===
# initial_rows/final_rows/rows_removed $ row accounting
# score_h / segment_h $ humanized user names for the mapped columns
# has_segment $ TRUE when a usable segment column is mapped
# n_na_score / n_out_of_range / n_non_integer $ cleaning counts
# lumped_levels / segment_constant $ segment cleaning notes
# overall $ list(n, prom_pct, pass_pct, det_pct, nps, se_pts,
# ci_low, ci_high, ci_half) — whole-sample NPS
# seg_stats $ list of the same per segment (NULL if no segment)
# segment_nps_df $ segment, n, nps, ci, flag (falls back to Overall)
# score_distribution_df $ score ("0".."10" in numeric order), count
# nps_mix_df $ segment_label, share_pct, nps_category
# methods_df $ item, detail — formulas + this dataset's numbers
# gap / gap_verdict / gap_text $ best-vs-worst segment comparison
# wide_ci / wide_note $ small-sample honesty (CI width in points)
# fmt_pts / fmt_ci / fmt_p $ formatters (signed points, no e-notation)
# metrics / json_output
# === /SHARED EXPORTS ===Step 0: Formatters — signed point scores, no bare asterisks, no e-notation
fmt_pts <- function(x) sprintf("%+d", as.integer(round(x)))
fmt_p <- function(p) {
if (is.na(p)) "p not computed"
else if (p < 0.0001) "p < 0.0001"
else sprintf("p = %.4f", p)
}Step 1: Resolve mapped columns (humanized for all prose)
initial_rows <- nrow(df)
score_h <- humanize_semantic("score", col_map)
has_segment <- "segment" %in% names(df)
segment_h <- if (has_segment) humanize_semantic("segment", col_map) else "segment"
if (!("score" %in% names(df))) {
stop(sprintf("NPS analysis needs the score column '%s' mapped — the 0-10 likelihood-to-recommend response.", score_h))
}Step 2: Coerce the score to numeric (95% rule)
v <- df$score
if (!is.numeric(v)) {
ch <- as.character(v)
non_blank <- !is.na(ch) & trimws(ch) != ""
conv <- suppressWarnings(as.numeric(ch))
if (sum(non_blank) == 0 ||
sum(!is.na(conv[non_blank])) < 0.95 * sum(non_blank)) {
stop(sprintf("The score column '%s' does not look numeric — fewer than 95%% of its values parse as numbers. NPS needs the raw 0-10 responses.", score_h))
}
v <- conv
}
seg_raw <- if (has_segment) as.character(df$segment) else NULL
keep <- !is.na(v)
n_na_score <- sum(!keep)
v <- v[keep]
if (has_segment) seg_raw <- seg_raw[keep]
if (length(v) == 0) {
stop(sprintf("No rows with a usable numeric value in '%s' remained after cleaning.", score_h))
}Step 3: Scale validation — HARD. 1-5 scales stop; outside 0-10 drops.
A column whose values sit entirely in 1..5 is a 1-5 rating scale, not the 0-10 NPS question. Rescaling would fabricate promoter/detractor categories that were never asked, so the analysis stops instead.
if (min(v) >= 1 && max(v) <= 5) {
stop(sprintf(
paste0("The score column '%s' only contains values from %s to %s — it looks like a 1-5 rating scale. ",
"NPS is defined on the 0-10 likelihood-to-recommend scale(detractors 0-6, passives 7-8, promoters 9-10), ",
"and a 1-5 scale cannot be honestly rescaled onto it — the categories would be invented, not measured. ",
"This analysis stops rather than silently rescale your data. ",
"For 1-5 ratings, use a distribution or group-comparison analysis instead, or field the question on a 0-10 scale."),
score_h,
format(round(min(v), 2), scientific = FALSE),
format(round(max(v), 2), scientific = FALSE)
))
}
in_range <- v >= 0 & v <= 10
n_out_of_range <- sum(!in_range)
if (n_out_of_range > 0.5 * length(v)) {
stop(sprintf("More than half of the values in '%s' fall outside 0-10 — this does not look like the 0-10 NPS question. Map the raw 0-10 likelihood-to-recommend column.", score_h))
}
v <- v[in_range]
if (has_segment) seg_raw <- seg_raw[in_range]
n_non_integer <- sum(v != round(v))
v <- round(v)
final_rows <- length(v)
rows_removed <- initial_rows - final_rows
if (final_rows < 10) {
stop(sprintf("Only %d usable 0-10 responses remained in '%s' — at least 10 are needed to compute an NPS.", final_rows, score_h))
}Step 4: Segment cleaning — blanks to Missing, lump beyond 8 levels
lumped_levels <- character(0)
segment_constant <- FALSE
if (has_segment) {
seg_raw[is.na(seg_raw) | trimws(seg_raw) == ""] <- "Missing"
tab <- sort(table(seg_raw), decreasing = TRUE)
if (length(tab) > 8) {
keep_lv <- names(tab)[1:8]
lumped_levels <- setdiff(names(tab), keep_lv)
seg_raw[seg_raw %in% lumped_levels] <- "Other"
}
if (length(unique(seg_raw)) < 2) {
segment_constant <- TRUE
has_segment <- FALSE
}
}Step 5: NPS with the classical multinomial variance formula
Var(NPS proportion) = (p9 + p0_6 - (p9 - p0_6)^2) / n, where p9 is the promoter share and p0_6 the detractor share. SE in points = 100 x sqrt(Var).
nps_stats <- function(x) {
n <- length(x)
p_prom <- mean(x >= 9)
p_det <- mean(x <= 6)
p_pass <- 1 - p_prom - p_det
nps <- 100 * (p_prom - p_det)
var_prop <- max(0, (p_prom + p_det - (p_prom - p_det)^2) / n)
se_pts <- 100 * sqrt(var_prop)
list(n = n,
prom_pct = 100 * p_prom, pass_pct = 100 * p_pass, det_pct = 100 * p_det,
nps = nps, se_pts = se_pts,
ci_low = nps - 1.96 * se_pts, ci_high = nps + 1.96 * se_pts,
ci_half = 1.96 * se_pts)
}
fmt_ci <- function(s) paste0(fmt_pts(s$ci_low), " to ", fmt_pts(s$ci_high))
overall <- nps_stats(v)Step 6: Per-segment NPS + the gated best-vs-worst comparison
seg_stats <- NULL
gap <- NULL
gap_verdict <- "none" # none | underpowered | overlap | different | not_significant
gap_text <- ""
if (has_segment) {
levs <- names(sort(table(seg_raw), decreasing = TRUE))
seg_stats <- lapply(levs, function(l) c(list(segment = l), nps_stats(v[seg_raw == l])))NA-safe best/worst pick (LAT-1445 class: never which.max over possible NAs)
nps_vals <- sapply(seg_stats, function(s) s$nps)
ok <- which(!is.na(nps_vals))
best <- seg_stats[[ok[which.max(nps_vals[ok])]]]
worst <- seg_stats[[ok[which.min(nps_vals[ok])]]]
gap <- list(best = best, worst = worst, diff = best$nps - worst$nps,
z = NA_real_, p = NA_real_,
cis_overlap = best$ci_low <= worst$ci_high)
if (best$segment != worst$segment) {
if (best$n >= 30 && worst$n >= 30) {Two-proportion-style z-test on the NPS difference (both n >= 30)
se_diff <- sqrt(best$se_pts^2 + worst$se_pts^2)
if (se_diff > 0) {
gap$z <- gap$diff / se_diff
gap$p <- 2 * stats::pnorm(-abs(gap$z))
}
if (gap$cis_overlap) {
gap_verdict <- "overlap"
gap_text <- paste0(
"The widest gap is ", fmt_pts(gap$diff), " points, between ", best$segment,
" (NPS ", fmt_pts(best$nps), ", 95% CI ", fmt_ci(best), ") and ", worst$segment,
" (NPS ", fmt_pts(worst$nps), ", 95% CI ", fmt_ci(worst),
"), but their 95% confidence intervals overlap — this gap must not be called a real difference",
if (!is.na(gap$p) && gap$p < 0.05) " despite the nominal test result" else "", ". ",
"Treat the segments as statistically indistinguishable on this sample."
)
} else if (!is.na(gap$p) && gap$p < 0.05) {
gap_verdict <- "different"
gap_text <- paste0(
best$segment, " (NPS ", fmt_pts(best$nps), ", 95% CI ", fmt_ci(best),
") is genuinely ahead of ", worst$segment, " (NPS ", fmt_pts(worst$nps),
", 95% CI ", fmt_ci(worst), "): a gap of ", fmt_pts(gap$diff),
" points that is statistically significant(z = ",
format(round(gap$z, 1), scientific = FALSE), ", ", fmt_p(gap$p),
"), with non-overlapping confidence intervals."
)
} else {
gap_verdict <- "not_significant"
gap_text <- paste0(
"The gap of ", fmt_pts(gap$diff), " points between ", best$segment,
" and ", worst$segment, " is not statistically reliable(",
fmt_p(gap$p), ") — treat it as noise on this sample."
)
}
} else {
gap_verdict <- "underpowered"
small <- if (best$n < 30 && worst$n < 30) paste0(best$segment, " (n = ", best$n, ") and ", worst$segment, " (n = ", worst$n, ") both have")
else if (best$n < 30) paste0(best$segment, " (n = ", best$n, ") has")
else paste0(worst$segment, " (n = ", worst$n, ") has")
gap_text <- paste0(
"The observed gap between ", best$segment, " (NPS ", fmt_pts(best$nps),
") and ", worst$segment, " (NPS ", fmt_pts(worst$nps), ") is ",
fmt_pts(gap$diff), " points, but the comparison is underpowered: ",
small, " fewer than 30 responses, so no test was run and this gap should not be read as a real difference."
)
}
}
}Step 7: Chart + table datasets (semantic column names)
cnt <- as.integer(table(factor(v, levels = 0:10)))
score_distribution_df <- data.frame(
score = as.character(0:10),
count = cnt,
stringsAsFactors = FALSE
)
cat_labels <- c("Promoters(9-10)", "Passives(7-8)", "Detractors(0-6)")
mix_source <- if (has_segment) seg_stats else list(c(list(segment = "Overall"), overall))
nps_mix_df <- do.call(rbind, lapply(mix_source, function(s) data.frame(
segment_label = s$segment,
nps_category = cat_labels,
share_pct = round(c(s$prom_pct, s$pass_pct, s$det_pct), 1),
stringsAsFactors = FALSE
)))
rownames(nps_mix_df) <- NULL
table_source <- if (has_segment) seg_stats[order(-sapply(seg_stats, function(s) s$nps))]
else list(c(list(segment = "Overall"), overall))
segment_nps_df <- do.call(rbind, lapply(table_source, function(s) data.frame(
segment = s$segment,
n = s$n,
nps = round(s$nps, 1),
ci = paste0(fmt_pts(s$ci_low), " to ", fmt_pts(s$ci_high)),
flag = if (s$n < 30) "low sample(fewer than 30)" else "",
stringsAsFactors = FALSE
)))
rownames(segment_nps_df) <- NULLStep 8: Honesty notes computed from the data
wide_ci <- overall$ci_half >= 15
wide_note <- if (wide_ci) paste0(
"Caution: with only ", format(overall$n, big.mark = ","), " responses the NPS point score is noisy — ",
"the 95% confidence interval spans ", as.integer(round(overall$ci_high - overall$ci_low)),
" points(", fmt_ci(overall), "), so ", fmt_pts(overall$nps),
" is a wide, rough estimate rather than a precise score."
) else ""
methods_df <- data.frame(
item = c(
"Score categories",
"NPS formula",
"Confidence interval",
"This dataset",
"Segment comparison",
"Scale validation",
"Small-sample policy"
),
detail = c(
"Promoters score 9-10, passives 7-8, detractors 0-6 on the 0-10 likelihood-to-recommend scale.",
"NPS = % promoters minus % detractors, stated as a point score from -100 to +100.",
"Classical multinomial variance formula: Var(NPS) = (p_promoter + p_detractor - (p_promoter - p_detractor)^2) / n on the proportion scale; SE in points = 100 x sqrt(Var); 95% CI = NPS plus or minus 1.96 x SE.",
paste0("n = ", format(overall$n, big.mark = ","), " valid responses; SE = ",
format(round(overall$se_pts, 1), scientific = FALSE), " points; the 95% interval spans ",
as.integer(round(overall$ci_high - overall$ci_low)), " points(", fmt_ci(overall), ")."),
"Two-proportion-style z-test on the NPS difference between the best and worst segment, run only when both have at least 30 responses; otherwise the comparison is reported as underpowered. Gaps with overlapping 95% confidence intervals are never called differences.",
"Values outside 0-10 are dropped and reported. A column whose values sit entirely within 1-5 stops the analysis — a 1-5 rating scale cannot be honestly rescaled to NPS.",
"The confidence interval width in points is always stated alongside the score, so a small-sample NPS reads as the rough estimate it is."
),
stringsAsFactors = FALSE
)
metrics <- list(
`NPS` = fmt_pts(overall$nps),
`95% CI` = fmt_ci(overall),
`Promoters %` = round(overall$prom_pct, 1),
`Passives %` = round(overall$pass_pct, 1),
`Detractors %` = round(overall$det_pct, 1),
`Responses` = overall$n
)
json_output <- list(
answer = paste0(
"NPS from ", format(overall$n, big.mark = ","), " responses to ", score_h, ": ",
fmt_pts(overall$nps), " (95% CI ", fmt_ci(overall), "), from ",
round(overall$prom_pct, 1), "% promoters, ", round(overall$pass_pct, 1),
"% passives, and ", round(overall$det_pct, 1), "% detractors. ",
if (nzchar(gap_text)) gap_text
else if (segment_constant) paste0("The mapped ", segment_h, " column has a single value, so no segment comparison was possible.")
else "No segment column was mapped, so the score is reported overall only.",
if (nzchar(wide_note)) paste0(" ", wide_note) else ""
),
cards = lapply(
c("tldr", "overview", "preprocessing", "score_distribution",
"nps_mix", "segment_nps", "methods"),
function(cid) list(id = cid, metrics = metrics)
)
)
list(
initial_rows = initial_rows, final_rows = final_rows,
rows_removed = rows_removed,
score_h = score_h, segment_h = segment_h, has_segment = has_segment,
n_na_score = n_na_score, n_out_of_range = n_out_of_range,
n_non_integer = n_non_integer,
lumped_levels = lumped_levels, segment_constant = segment_constant,
overall = overall, seg_stats = seg_stats,
segment_nps_df = segment_nps_df,
score_distribution_df = score_distribution_df,
nps_mix_df = nps_mix_df, methods_df = methods_df,
gap = gap, gap_verdict = gap_verdict, gap_text = gap_text,
wide_ci = wide_ci, wide_note = wide_note,
fmt_pts = fmt_pts, fmt_ci = fmt_ci, fmt_p = fmt_p,
metrics = metrics, json_output = json_output
)
}