Standard Bland Altman
Executive Summary

Executive Summary

Do measured weight kg and self reported weight kg agree well enough to be used interchangeably?

Pairs Analysed
182
Bias (Mean Difference)
0
Lower Limit of Agreement
-4.547
Upper Limit of Agreement
4.547
Within Limits
91.76%
Proportional Bias
detected
Correlation r
0.986
Across 182 paired measurements, measured weight kg and self reported weight kg show no average offset (bias = 0.00, 95% CI -0.34 to 0.34). The 95% limits of agreement run from -4.55 to 4.55: for a new measurement, the difference between the two methods is expected to fall in this range about 95% of the time. Here 91.76% of the observed differences fall inside (about 95% is expected by construction). Warning: the difference changes with the size of the measurement (slope -0.028, p = 0.025), so agreement varies with magnitude and the simple limits mislead — judge agreement separately at the low and high end of the range. The two methods correlate at r = 0.986; remember that correlation measures co-movement, not agreement. Whether these limits are acceptable depends on your clinical/technical tolerance: compare the limits of agreement against the largest disagreement you could tolerate in practice. The analysis describes how far apart the methods are — it cannot decide whether that is close enough for your use.
What this means

The short answer

Self-reported weight and measured weight show no average offset (bias = 0.00 kg), but the two methods can diverge by up to 4.55 kg for a typical measurement, and this disagreement changes with body weight. Whether self-report is acceptable depends on your tolerance for that gap.

The detail

Across 182 paired measurements, the bias is 0.00 kg (95% CI -0.34 to 0.34), and the 95% limits of agreement span -4.547 to 4.547 kg; 91.76% of observed differences fell inside these limits. However, proportional bias is detected (slope -0.028, p = 0.025): the disagreement shrinks as weight increases, so the simple limits mislead—agreement is tighter at higher weights and looser at lower weights. The correlation is 0.986, but correlation measures co-movement, not agreement; two methods can correlate near-perfectly while still disagreeing by several kilograms. Whether a ±4.55 kg gap is acceptable for your use is a tolerance call you must make.

What this can't tell you

The single pair of limits averages over disagreement that varies with magnitude. Consult the proportional_bias card for the magnitude-specific pattern, and consider regression-based limits or percentage differences for a weight-adjusted picture.

Overview

Analysis Overview

Bland-Altman agreement between measured weight kg and self reported weight kg across 182 paired measurements.

N Pairs182
Bias0
Loa Low-4.547
Loa High4.547
What this means

The short answer

Self-reported weight tracks measured weight very closely on average, with no systematic bias, but the two methods can differ by as much as 4.5 kg in either direction for a typical measurement. Whether that gap is acceptable depends on what you can tolerate in practice.

The detail

Across 182 paired measurements, the mean difference (measured minus self-reported) is 0.00 kg, with a 95% confidence interval of -0.34 to 0.34 kg—no evidence of a systematic offset in either direction. The 95% limits of agreement run from -4.547 to 4.547 kg, meaning a new pair of measurements is expected to differ by up to 4.55 kg about 95% of the time. In this dataset, 91.76% of the observed differences fell inside those limits, close to the 95% expected by construction. The analysis works on raw differences rather than correlation (which is 0.986) because two methods can move together almost perfectly while still disagreeing by a constant amount.

What this can't tell you

The agreement picture is incomplete without the proportional-bias result: the disagreement is not uniform across the measurement range (see proportional_bias card). The confidence intervals around the limits themselves are wide (lower: -5.13 to -3.97; upper: 3.97 to 5.13), reflecting the sample size of 182 pairs. Whether a ±4.55 kg difference is acceptable is a tolerance decision only you can make.

Data Preparation

Data Quality

Pair completeness and measurement checks.

Initial Rows182
Final Rows182
Rows Removed0
What this means

The short answer

All 182 rows formed complete pairs with no missing values; no data was removed during preparation. Both columns were verified to be on the same units (kilograms) before differences were computed.

The detail

The dataset loaded with 182 rows and retained all 182 rows as complete pairs. Both the measured weight kg and self-reported weight kg columns were coerced to numbers and checked for variation before the differences were analysed. The critical requirement—that both measurements be on the same units—was met; a mismatch would make the differences meaningless and the analysis cannot detect such a mismatch from the data alone.

What this can't tell you

The analysis cannot verify that the units are truly identical in real-world meaning (e.g., both recorded in kilograms and not mixed with pounds in either column). A spot-check of the raw data against collection protocols would strengthen confidence in the unit match.

Visualization

Bland-Altman Plot

Difference between measured weight kg and self reported weight kg plotted against their mean, with bias and limit lines.

What this means

The short answer

The cloud of differences is scattered around zero with no strong systematic offset, but it tilts downward as weight increases—disagreement is smaller at higher weights and larger at lower weights. About 92% of points fall inside the ±4.55 kg band, as expected.

The detail

Each of the 182 points plots the difference (measured minus self-reported) against the mean of the two readings. The bias line sits at 0.00 kg. The outer lines mark the 95% limits of agreement at -4.547 and 4.547 kg; 91.76% of points lie between them. The cloud tilts with magnitude (slope -0.028, p = 0.025): points at lower mean weights (around 40–50 kg) show wider scatter and a tendency toward larger negative differences, while points at higher mean weights (60+ kg) cluster tighter and closer to zero. This tilt means the flat limit lines are misleading—read agreement separately at the low and high end of the measured range.

What this can't tell you

The visual pattern confirms proportional bias but does not quantify how much agreement improves or worsens at specific weight ranges. The proportional_bias card provides the regression slope and p-value; consider regression-based limits for weight-adjusted bounds.

Data Table

Agreement Statistics

Bias, SD of differences, and limits of agreement — each with a 95% CI.

MeasureEstimateCI LowCI HighInterpretation
Bias (mean difference)0-0.3390.339On average measured weight kg reads higher than self reported weight kg — the CI includes zero, so no systematic offset is established.
SD of differences2.32The spread of the per-row disagreements between the two methods.
Lower limit of agreement-4.547-5.128-3.966For a new measurement, measured weight kg minus self reported weight kg is expected to stay above this about 97.5% of the time.
Upper limit of agreement4.5473.9665.128For a new measurement, measured weight kg minus self reported weight kg is expected to stay below this about 97.5% of the time.
What this means

The short answer

The bias is zero (no systematic offset), and the ±4.55 kg limits describe how far apart the methods can be. The confidence intervals around those limits are wide, reflecting the finite sample size, so the honest range of plausible limits is broader than the point estimates alone suggest.

The detail

The bias is 0.00 kg with a 95% CI of -0.34 to 0.34 kg, so no systematic offset is established. The SD of the differences is 2.32 kg. The lower limit of agreement is -4.547 kg (95% CI -5.128 to -3.966); the upper limit is 4.547 kg (95% CI 3.966 to 5.128). The width of these confidence intervals—roughly ±0.6 kg around each limit—reflects the 182 pairs available; with more data, those intervals would narrow and the limits would become more precise.

What this can't tell you

These limits are averages across all weights in the dataset. Because proportional bias is detected, the limits are wider than necessary at high weights and narrower than necessary at low weights. Whether ±4.55 kg is acceptable for your purpose is a tolerance call only you can make by comparing these limits against the largest disagreement you could tolerate in practice.

Data Table

Proportional Bias Check

Does the disagreement between the methods change with the size of the measurement?

TermEstimateStd ErrorP ValueInterpretation
Intercept1.870.84330.028The expected difference between the methods at a (hypothetical) measurement of zero.
Slope (difference vs magnitude)-0.02850.01260.025Significant (p = 0.025): the disagreement between measured weight kg and self reported weight kg changes with the size of the measurement — agreement varies with magnitude and the simple limits mislead.
What this means

The short answer

The disagreement between the two methods shrinks as weight increases: the slope is -0.028 kg per kilogram of body weight (p = 0.025), a statistically significant pattern. Self-report is relatively worse at lower weights and relatively better at higher weights.

The detail

The regression of the difference on the mean measurement yields a slope of -0.0285 (standard error 0.0126, p = 0.025). This negative slope means that for every kilogram increase in body weight, the difference between measured and self-reported weight decreases by about 0.028 kg on average. The intercept is 1.87 kg (p = 0.028), representing the expected difference at a hypothetical zero weight. The p-value of 0.025 crosses the conventional threshold, so proportional bias is established—agreement is not constant across the range.

What this can't tell you

The slope quantifies the trend but does not specify the limits at particular weight ranges. To know how much agreement improves or worsens at, say, 50 kg versus 80 kg, you would need regression-based limits of agreement or a weight-stratified analysis. Consider recomputing limits separately at the low and high quartiles of the measured range to see the practical magnitude of the variation.

Data Table

Methods & Disclosure

How the bias, limits, and checks are computed, and what they can and cannot decide.

ItemDetail
Difference conventionEvery row's difference is measured weight kg minus self reported weight kg; the magnitude axis is the mean of the two readings.
Bias and its 95% CIBias = mean difference = 0.00; 95% CI -0.34 to 0.34 (t distribution, 181 degrees of freedom).
Limits of agreementBias plus or minus 1.96 times the SD of the differences (2.32): -4.55 to 4.55.
CIs for the limitsClassical Bland-Altman standard error for a limit, SD x sqrt(1/n + 1.96^2/(2(n-1))) = 0.29, giving lower limit CI -5.13 to -3.97 and upper limit CI 3.97 to 5.13.
Proportional-bias checkOrdinary regression of the difference on the mean of the two methods; slope -0.028 (p = 0.025).
Coverage check91.76% of the 182 observed differences fall inside the limits (about 95% is expected by construction).
Correlation vs agreementPearson r between measured weight kg and self reported weight kg is 0.986. Correlation measures whether the readings rise and fall together; agreement asks how far apart they are. High correlation is not agreement.
AcceptabilityWhether these limits are acceptable depends on your clinical/technical tolerance: compare the limits of agreement against the largest disagreement you could tolerate in practice. The analysis describes how far apart the methods are — it cannot decide whether that is close enough for your use.
What this means

The short answer

The analysis follows the Bland-Altman method: each pair's difference is plotted against its mean, the bias and limits are computed from the mean and SD of those differences, and a regression checks whether disagreement varies with weight. The methods are classical; the key finding is that agreement is not uniform across the range.

The detail

Bias is the mean difference (measured minus self-reported), computed as 0.00 with a t-based 95% CI of -0.34 to 0.34 (df = 181). Limits of agreement are bias ± 1.96 × SD of differences (2.32), yielding -4.55 to 4.55 kg. Each limit's 95% CI uses the classical Bland-Altman standard error (0.29), giving lower limit CI -5.13 to -3.97 and upper limit CI 3.97 to 5.13. Proportional-bias check: ordinary regression of difference on mean, slope -0.028 (p = 0.025). Coverage: 91.76% of 182 differences fall inside the limits. Pearson r between methods is 0.986—reported to show that high correlation does not equal agreement; the methods track together but can still diverge systematically.

What this can't tell you

The analysis does not decide whether ±4.55 kg is acceptable—that is your tolerance call. It describes how far apart the methods are, not whether that distance is close enough for your use.

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

Method Agreement — Bland-Altman

Do two measurement methods agree well enough to be used interchangeably? Each row is one subject or sample measured by both methods on the same units. The analysis works on the per-row difference between the methods: the bias (mean difference) with a 95% confidence interval, the 95% limits of agreement (bias ± 1.96·SD of the differences) each with its own confidence interval, a proportional-bias check (regression of the difference on the magnitude), and the share of points inside the limits.

Why This Method?

Correlation cannot answer an agreement question: two methods can correlate almost perfectly while one reads systematically higher than the other. Bland-Altman analysis instead describes how far apart the two methods are expected to be for a typical measurement — a bias plus a range — which is the quantity a practitioner can actually judge against a tolerance.

What This Analysis Covers

  • The Bland-Altman scatter (difference vs mean, with bias and limit lines)
  • Bias, SD of differences, and limits of agreement, each with 95% CIs
  • A proportional-bias regression (does the disagreement grow with magnitude?)
  • The observed share of points inside the limits, and the correlation r

explicitly contrasted with agreement

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {method_1, method_2}. 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
  #   n / n_dropped        $ complete pairs used + incomplete rows dropped
  #   m1_h / m2_h          $ humanized user names for the two methods
  #   bias / sd_d          $ mean difference (method_1 - method_2) + its SD
  #   bias_lo / bias_hi    $ 95% CI for the bias
  #   loa_low / loa_high   $ limits of agreement = bias -/+ 1.96*sd_d
  #   loa_low_lo/loa_low_hi/loa_high_lo/loa_high_hi  $ 95% CIs for each limit
  #   pct_within           $ observed % of points inside the limits
  #   r_methods            $ Pearson r between the two methods
  #   slope/slope_se/slope_p/intercept  $ regression of difference on mean
  #   prop_bias            $ TRUE when the slope is significant at p<0.05
  #   bias_sig             $ TRUE when the bias CI excludes zero
  #   high_r               $ TRUE when r_methods >= 0.8
  #   contrast_fires       $ high_r AND bias_sig — the correlation-vs-agreement teaching point
  #   dir_word             $ "higher"/"lower" consistent with the sign of bias
  #   ba_points_df         $ mean_value, difference (<=1000 sampled)
  #   agreement_df         $ measure, estimate, ci_low, ci_high, interpretation
  #   prop_bias_df         $ term, estimate, std_error, p_value, interpretation
  #   methods_df           $ item, detail
  #   tolerance_note       $ the "acceptability is the user's call" sentence
  #   metrics / json_output
  # === /SHARED EXPORTS ===

Step 1: Resolve mapped columns (humanized for all prose)

initial_rows <- nrow(df)
  m1_h <- humanize_semantic("method_1", col_map)
  m2_h <- humanize_semantic("method_2", col_map)
  if (!("method_1" %in% names(df)) || !("method_2" %in% names(df))) {
    stop(sprintf("Bland-Altman agreement needs both &#x27;%s' (the first method) and '%s' (the second method) mapped.",
                 m1_h, m2_h))
  }

Step 2: Coerce both measurements to numeric (95% rule)

coerce_num <- function(v, label_h) {
    if (is.numeric(v)) return(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 column &#x27;%s' does not look numeric — fewer than 95%% of its values parse as numbers. Pick a numeric column.",
                   label_h))
    }
    conv
  }
  v1 <- coerce_num(df$method_1, m1_h)
  v2 <- coerce_num(df$method_2, m2_h)

Step 3: Keep row-wise complete pairs; require enough of them

keep <- !is.na(v1) & !is.na(v2)
  n_dropped <- sum(!keep)
  v1 <- v1[keep]; v2 <- v2[keep]
  n <- length(v1)
  final_rows <- n
  rows_removed <- initial_rows - final_rows
  if (n < 10) {
    stop(sprintf("Only %d complete %s / %s pairs remained after dropping incomplete rows — at least 10 are needed for a Bland-Altman agreement analysis.",
                 n, m1_h, m2_h))
  }

Step 4: Guard degenerate inputs (constant columns, identical differences)

if (isTRUE(stats::var(v1) == 0)) {
    stop(sprintf("The column &#x27;%s' is constant — every value is identical — so method agreement cannot be assessed. Map a measurement that varies.", m1_h))
  }
  if (isTRUE(stats::var(v2) == 0)) {
    stop(sprintf("The column &#x27;%s' is constant — every value is identical — so method agreement cannot be assessed. Map a measurement that varies.", m2_h))
  }

Step 5: Differences, bias, and its 95% CI (classical formulas)

d  <- v1 - v2
  mn <- (v1 + v2) / 2
  bias <- mean(d)
  sd_d <- stats::sd(d)
  if (is.na(sd_d) || sd_d == 0) {
    stop(sprintf("&#x27;%s' and '%s' differ by exactly the same amount on every row, so the limits of agreement collapse to a point — there is no variation in the differences to analyse.",
                 m1_h, m2_h))
  }
  tq <- stats::qt(0.975, n - 1)
  se_bias <- sd_d / sqrt(n)
  bias_lo <- bias - tq * se_bias
  bias_hi <- bias + tq * se_bias

Step 6: Limits of agreement, each with its own 95% CI

loa_low  <- bias - 1.96 * sd_d
  loa_high <- bias + 1.96 * sd_d
  se_loa <- sd_d * sqrt(1 / n + 1.96^2 / (2 * (n - 1)))
  loa_low_lo  <- loa_low  - tq * se_loa
  loa_low_hi  <- loa_low  + tq * se_loa
  loa_high_lo <- loa_high - tq * se_loa
  loa_high_hi <- loa_high + tq * se_loa

Step 7: Observed coverage of the limits (should be near 95%)

n_within <- sum(d >= loa_low & d <= loa_high)
  pct_within <- 100 * n_within / n

Step 8: Correlation between the methods (to contrast with agreement)

r_methods <- suppressWarnings(tryCatch(stats::cor(v1, v2), error = function(e) NA_real_))

Step 9: Proportional-bias check — regression of difference on mean

slope <- NA_real_; slope_se <- NA_real_; slope_p <- NA_real_
  intercept <- NA_real_; intercept_se <- NA_real_; intercept_p <- NA_real_
  if (isTRUE(stats::var(mn) > 0)) {
    fit <- tryCatch(stats::lm(d ~ mn), error = function(e) NULL)
    if (!is.null(fit)) {
      cf <- tryCatch(summary(fit)$coefficients, error = function(e) NULL)
      if (!is.null(cf) && nrow(cf) == 2) {
        intercept    <- cf[1, 1]; intercept_se <- cf[1, 2]; intercept_p <- cf[1, 4]
        slope        <- cf[2, 1]; slope_se     <- cf[2, 2]; slope_p     <- cf[2, 4]
      }
    }
  }
  prop_bias <- !is.na(slope_p) && slope_p < 0.05

Step 10: Verdict flags — all direction/consistency language computed

bias_sig <- !is.na(bias_lo) && !is.na(bias_hi) && (bias_lo > 0 || bias_hi < 0)
  high_r <- !is.na(r_methods) && r_methods >= 0.8
  contrast_fires <- high_r && bias_sig
  dir_word <- if (bias > 0) "higher" else if (bias < 0) "lower" else "the same on average"
  tolerance_note <- paste0(
    "Whether these limits are acceptable depends on your clinical/technical tolerance: ",
    "compare the limits of agreement against the largest disagreement you could tolerate ",
    "in practice. The analysis describes how far apart the methods are — it cannot decide ",
    "whether that is close enough for your use."
  )

Step 11: Bland-Altman scatter dataset — <=1000 sampled, fixed seed

set.seed(42)
  sidx <- if (n > 1000) sample(n, 1000) else seq_len(n)
  ba_points_df <- data.frame(
    mean_value = round(mn[sidx], 4),
    difference = round(d[sidx], 4),
    stringsAsFactors = FALSE
  )
  ba_points_df <- ba_points_df[order(ba_points_df$mean_value), , drop = FALSE]
  rownames(ba_points_df) <- NULL

Step 12: Agreement table — bias, SD, limits, all with CIs

agreement_df <- data.frame(
    measure = c("Bias(mean difference)", "SD of differences",
                "Lower limit of agreement", "Upper limit of agreement"),
    estimate = round(c(bias, sd_d, loa_low, loa_high), 3),
    ci_low  = round(c(bias_lo, NA_real_, loa_low_lo,  loa_high_lo), 3),
    ci_high = round(c(bias_hi, NA_real_, loa_low_hi,  loa_high_hi), 3),
    interpretation = c(
      sprintf("On average %s reads %s %s than %s%s.",
              m1_h,
              if (bias == 0) "" else r2(abs(bias)),
              if (bias >= 0) "higher" else "lower", m2_h,
              if (bias_sig) " — the CI excludes zero, a real systematic offset"
              else " — the CI includes zero, so no systematic offset is established"),
      "The spread of the per-row disagreements between the two methods.",
      sprintf("For a new measurement, %s minus %s is expected to stay above this about 97.5%% of the time.", m1_h, m2_h),
      sprintf("For a new measurement, %s minus %s is expected to stay below this about 97.5%% of the time.", m1_h, m2_h)
    ),
    stringsAsFactors = FALSE
  )

Step 13: Proportional-bias table

prop_bias_df <- data.frame(
    term = c("Intercept", "Slope(difference vs magnitude)"),
    estimate  = round(c(intercept, slope), 4),
    std_error = round(c(intercept_se, slope_se), 4),
    p_value   = c(fmt_p(intercept_p), fmt_p(slope_p)),
    interpretation = c(
      "The expected difference between the methods at a(hypothetical) measurement of zero.",
      if (prop_bias)
        sprintf("Significant(%s): the disagreement between %s and %s changes with the size of the measurement — agreement varies with magnitude and the simple limits mislead.",
                fmt_pp(slope_p), m1_h, m2_h)
      else if (!is.na(slope_p))
        sprintf("Not significant(%s): no evidence that the disagreement grows or shrinks with the size of the measurement.",
                fmt_pp(slope_p))
      else
        "The regression could not be computed(no variation in the measurement magnitude)."
    ),
    stringsAsFactors = FALSE
  )

Step 14: Methods table

methods_df <- data.frame(
    item = c("Difference convention", "Bias and its 95% CI", "Limits of agreement",
             "CIs for the limits", "Proportional-bias check", "Coverage check",
             "Correlation vs agreement", "Acceptability"),
    detail = c(
      sprintf("Every row&#x27;s difference is %s minus %s; the magnitude axis is the mean of the two readings.", m1_h, m2_h),
      sprintf("Bias = mean difference = %s; 95%% CI %s to %s(t distribution, %d degrees of freedom).",
              r2(bias), r2(bias_lo), r2(bias_hi), n - 1),
      sprintf("Bias plus or minus 1.96 times the SD of the differences(%s): %s to %s.",
              r2(sd_d), r2(loa_low), r2(loa_high)),
      sprintf("Classical Bland-Altman standard error for a limit, SD x sqrt(1/n + 1.96^2/(2(n-1))) = %s, giving lower limit CI %s to %s and upper limit CI %s to %s.",
              r2(se_loa), r2(loa_low_lo), r2(loa_low_hi), r2(loa_high_lo), r2(loa_high_hi)),
      sprintf("Ordinary regression of the difference on the mean of the two methods; slope %s(%s).",
              r3(slope), fmt_pp(slope_p)),
      sprintf("%s%% of the %s observed differences fall inside the limits(about 95%% is expected by construction).",
              r2(pct_within), format(n, big.mark = ",")),
      sprintf("Pearson r between %s and %s is %s. Correlation measures whether the readings rise and fall together; agreement asks how far apart they are. High correlation is not agreement.",
              m1_h, m2_h, r3(r_methods)),
      tolerance_note
    ),
    stringsAsFactors = FALSE
  )

  metrics <- list(
    `Pairs Analysed`            = n,
    `Bias(Mean Difference)`    = round(bias, 3),
    `Lower Limit of Agreement`  = round(loa_low, 3),
    `Upper Limit of Agreement`  = round(loa_high, 3),
    `Within Limits`             = paste0(r2(pct_within), "%"),
    `Proportional Bias`         = if (prop_bias) "detected" else "not detected",
    `Correlation r`             = if (is.na(r_methods)) NA_real_ else round(r_methods, 3)
  )

  contrast_clause <- if (contrast_fires) {
    paste0(" The two methods correlate strongly(r = ", r3(r_methods),
           "), yet they do not agree: the bias of ", r2(bias),
           " is a real systematic offset — high correlation is not agreement.")
  } else ""
  prop_clause <- if (prop_bias) {
    paste0(" Warning: the difference changes with the size of the measurement(slope ",
           r3(slope), ", ", fmt_pp(slope_p),
           "), so agreement varies with magnitude and the simple limits mislead.")
  } else ""

  json_output <- list(
    answer = paste0(
      "Bland-Altman agreement of ", m1_h, " versus ", m2_h, " across ",
      format(n, big.mark = ","), " paired measurements: the bias(", m1_h,
      " minus ", m2_h, ") is ", r2(bias), " (95% CI ", r2(bias_lo), " to ",
      r2(bias_hi), "), with 95% limits of agreement from ", r2(loa_low),
      " to ", r2(loa_high), "; ", r2(pct_within),
      "% of observed differences fall inside the limits. Proportional bias ",
      if (prop_bias) "was detected" else "was not detected", " (slope ",
      fmt_pp(slope_p), ").", contrast_clause, prop_clause,
      " Whether these limits are acceptable depends on your clinical/technical tolerance."
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "bland_altman_plot",
        "agreement_table", "proportional_bias", "methods"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows = initial_rows, final_rows = final_rows,
    rows_removed = rows_removed, n = n, n_dropped = n_dropped,
    m1_h = m1_h, m2_h = m2_h,
    bias = bias, sd_d = sd_d, bias_lo = bias_lo, bias_hi = bias_hi,
    loa_low = loa_low, loa_high = loa_high,
    loa_low_lo = loa_low_lo, loa_low_hi = loa_low_hi,
    loa_high_lo = loa_high_lo, loa_high_hi = loa_high_hi,
    pct_within = pct_within, r_methods = r_methods,
    slope = slope, slope_se = slope_se, slope_p = slope_p,
    intercept = intercept,
    prop_bias = prop_bias, bias_sig = bias_sig, high_r = high_r,
    contrast_fires = contrast_fires, dir_word = dir_word,
    tolerance_note = tolerance_note,
    ba_points_df = ba_points_df, agreement_df = agreement_df,
    prop_bias_df = prop_bias_df, methods_df = methods_df,
    metrics = metrics, json_output = json_output
  )
}
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