Standard Meta Analysis
Executive Summary

Executive Summary

What 20 studies of 'Effect Size' say once pooled — and how much they disagree.

Studies Pooled
20
Fixed-Effect Estimate
0.5031
Random-Effects Estimate
0.506
Random-Effects 95% CI
0.427 to 0.585
I-squared (%)
11.5
Tau-squared
0.0036
Cochran Q p-value
0.312
Prediction Interval
0.354 to 0.658
Small-Study Effect
not detected
Egger p-value
0.468
Pooling 20 studies of 'Effect Size', the random-effects estimate is 0.506 (95% CI 0.427 to 0.585) — a positive effect, and the interval excludes zero, so the pooled effect is distinguishable from no effect. The fixed-effect model agrees closely (0.503), which is what limited between-study variance implies. Heterogeneity is low (I-squared 11.47%, p = 0.312), so the studies are consistent with estimating a common effect and the pooled number can be read as an estimate of it. Split by 'Region', there is no evidence the subgroups differ (p = 0.652). Egger's test finds no significant funnel asymmetry (p = 0.468). The test uses all 20 studies. Even at this size it detects only one specific pattern — estimates that vary systematically with their own standard error — and asymmetry has causes other than publication bias, including real differences between small and large studies.
Suggested Interpretation

The short answer

The pooled effect across 20 studies is 0.506 (95% CI 0.427 to 0.585), a positive effect that excludes zero. Heterogeneity is low, so this can be read as an estimate of a common effect rather than an average across different effects. No evidence of regional differences (p = 0.652) and no significant funnel asymmetry (p = 0.468).

The detail

Random-effects estimate 0.506, fixed-effect 0.5031 — they agree closely because tau-squared is 0.0036. I-squared 11.5%, p = 0.312 (Cochran Q) supports the assumption of a common effect. The prediction interval (0.354 to 0.658) shows where future studies might fall. Egger's test for small-study effects: intercept p = 0.468, not significant. Subgroup analysis by Region: between-subgroup Q = 0.203, p = 0.652 — no evidence of differential effects by region. With 20 studies, Egger's test detects only one specific pattern (estimates varying with their own standard error) and cannot distinguish publication bias from real differences between small and large studies.

What this can't tell you

Whether the asymmetry test would detect small-study effects at smaller sample sizes, or whether regional differences exist in the underlying population rather than in this sample.

Overview

Analysis Overview

Fixed-effect and random-effects pooling of 20 studies on 'Effect Size'.

N Studies20
Fixed Effect0.5031
Random Effects0.506
I Squared11.5
Suggested Interpretation

The short answer

Two models are fitted because the assumption about whether studies estimate the same effect or different ones must be decided by the data, not by preference. Here, heterogeneity is low (I-squared 11.47%), so both models agree closely: fixed-effect 0.5031, random-effects 0.506. The low heterogeneity supports treating the pooled number as an estimate of one shared effect rather than an average across genuinely different study effects.

The detail

The fixed-effect model assumes every study estimates the same quantity and weights each by 1 divided by its variance. The random-effects model adds a between-study variance component (tau-squared = 0.0036) and allows true effects to differ. Standard errors came from the reported standard error in 'Standard Error' for all 20 studies. Cochran's Q = 21.461 on 19 degrees of freedom (p = 0.312) yields I-squared of 11.47%, which is low by convention. The two pooled estimates differ by only 0.003, reflecting the small tau-squared.

What this can't tell you

Q has low power at 20 studies, so the non-significant p-value is weak evidence of homogeneity rather than proof of it. Neither model corrects for bias shared by the studies themselves.

Data Preparation

Data Quality

Which rows became studies, and which could not.

Initial Rows20
Final Rows20
Rows Removed0
Suggested Interpretation

The short answer

All 20 rows loaded were usable as studies—none were removed. A study requires both a numeric effect estimate and a strictly positive standard error; zero or negative standard errors would give infinite weight and silently decide the whole result. Standard errors came from the reported standard error in 'Standard Error', and blank or duplicate study labels were handled so every study appears separately on the forest plot.

The detail

20 initial rows, 20 final rows, 0 rows removed. The requirement for usable data is a numeric effect AND a strictly positive standard error in 'Standard Error'. Blank study labels were replaced with a positional name and duplicate labels were suffixed to ensure each study is distinct on the forest plot.

What this can't tell you

This step does not assess whether the studies measured the same construct or whether the reported standard errors are correct.

Visualization

Forest Plot

Every study's effect and 95% interval, with both pooled estimates.

Suggested Interpretation

The short answer

All 20 studies cluster tightly around the pooled estimates (0.503 fixed-effect, 0.506 random-effects), with markers scattering about as much as their own confidence intervals allow—the visual signature of low heterogeneity. Trial 16 (2022) is most precise and carries 16.37% of the random-effects weight; Trial 05 (2020) is least precise at 1.29%.

The detail

Studies are ordered by effect size from highest (Trial 11 at 1.0826) to lowest (Trial 08 at 0.160). The widest interval is Trial 05's (2020) and the narrowest is Trial 16's (2022). The narrow intervals dominate the weighting: Trial 16 carries 16.37%, Trial 15 carries 8.73%, and Trial 10 carries 10.71%, while Trial 05 carries only 1.29%. The scatter of markers around the pooled rows is consistent with their own interval widths, which is what low heterogeneity looks like. Several intervals cross zero (Trial 13, Trial 18, Trial 07), meaning those studies alone could not distinguish the effect from nothing.

What this can't tell you

The forest plot shows scatter but does not test for systematic patterns; heterogeneity statistics provide that test. Visual inspection of a forest plot is unreliable for detecting asymmetry.

Data Table

Pooled Estimates

Fixed-effect and random-effects results side by side.

ModelEstimateStd ErrorCI LowCI HighP ValueInterpretation
Fixed-effect (inverse-variance)0.50310.03610.43230.5739< 0.001Assumes every study estimates the SAME effect and all variation is sampling error. Weights are 1 divided by the study variance, so precise studies dominate. The heterogeneity statistics are consistent with that assumption here.
Random-effects (DerSimonian-Laird)0.5060.04040.42690.5852< 0.001Allows the true effect to differ across studies by tau-squared = 0.0036, which flattens the weights and widens the interval. The two models agree closely, which is what low heterogeneity implies.
Suggested Interpretation

The short answer

Both models yield a positive effect with a 95% confidence interval that excludes zero (fixed-effect p < 0.001, random-effects p < 0.001). They agree closely—differing by only 0.003—because tau-squared is small (0.0036), so the choice between models does not change the conclusion.

The detail

Fixed-effect estimate: 0.5031 (standard error 0.0361, 95% CI 0.4323 to 0.5739). Random-effects estimate: 0.506 (standard error 0.0404, 95% CI 0.4269 to 0.5852). The fixed-effect interval is 0.142 wide; the random-effects interval is 0.158 wide. The fixed-effect model assumes every study estimates the same effect and weights by 1 divided by the study variance; precise studies dominate. The random-effects model allows true effects to differ and flattens the weights. The two models agree closely here because tau-squared = 0.0036 is low.

What this can't tell you

Neither model corrects for bias shared by all the studies. If the studies carry a systematic flaw, both pooled estimates inherit it, and a narrow confidence interval around a biased average is still misleading.

Data Table

Heterogeneity

How much the studies disagree, and what that does to the pooled estimate.

StatisticValueMeaning
Cochran's Q21.461Total weighted dispersion of the study estimates around the fixed-effect pooled value.
Degrees of freedom19Number of studies minus one (20 studies pooled).
Q p-value0.312Probability of dispersion this large if every study shared one true effect. Q is known to have low power at small study counts, so a non-significant Q with 20 studies does not establish homogeneity.
I-squared (%)11.47Share of the total variation that exceeds sampling error: 11.47% here, which is low by the conventional bands.
H-squared1.130Ratio of total variation to sampling variation. One means no excess heterogeneity.
Tau-squared0.0036Estimated variance of the true effects across studies (DerSimonian-Laird). Zero means no detectable between-study variance.
Tau0.060The standard deviation of the true effects, on the same scale as the effect itself.
95% prediction interval0.354 to 0.658The range a NEW comparable study's true effect would fall in about 95% of the time. This is the honest width of the finding, and it is always wider than the confidence interval around the average.
Suggested Interpretation

The short answer

Heterogeneity is low (I-squared 11.47%), meaning the studies scatter around the pooled estimate by about as much as their own sampling error alone would predict. Cochran's Q = 21.461 (p = 0.312) is consistent with one shared true effect, so the pooled number can be read as an estimate of it rather than an average across genuinely different effects.

The detail

Cochran's Q = 21.461 on 19 degrees of freedom (p = 0.312); I-squared = 11.47%; tau-squared = 0.0036; tau = 0.060. The prediction interval (0.354 to 0.658) is the range a new comparable study would land in about 95% of the time, and it is always wider than the confidence interval around the pooled average. H-squared = 1.130 (ratio of total variation to sampling variation; one means no excess heterogeneity).

What this can't tell you

Q has low power at 20 studies, so a non-significant p-value is weak evidence of homogeneity rather than proof of it. I-squared itself is estimated with uncertainty that is not reported here. The prediction interval assumes the next study comes from the same distribution of true effects.

Data Table

Subgroup Pooling

Does the effect hold across the groups, or only in some of them?

SubgroupStudiesEstimateCI LowCI HighI SquaredInterpretation
North100.50160.40890.5943010 study/studies pooled to 0.502 (95% CI 0.409 to 0.594); within-group I-squared 0.00%.
South100.54150.39490.688130.210 study/studies pooled to 0.541 (95% CI 0.395 to 0.688); within-group I-squared 30.20%.
Suggested Interpretation

The short answer

The two regions (North and South) show no detectable difference in effect size. North yields 0.5016 (95% CI 0.4089 to 0.5943) from 10 studies, and South yields 0.5415 (95% CI 0.3949 to 0.6881) from 10 studies. Between-group Q = 0.203 (p = 0.652) supports pooling them together.

The detail

North: 10 studies pooled to random-effects estimate 0.5016 with within-group I-squared 0.00%. South: 10 studies pooled to random-effects estimate 0.5415 with within-group I-squared 30.20%. Between-subgroup Q = 0.203 on 1 degree of freedom (p = 0.652). The South subgroup shows moderate within-group heterogeneity (I-squared 30.20%), while North shows none (I-squared 0.00%).

What this can't tell you

Subgroup tests across studies have low power; a null result here is weak evidence of sameness, not proof that regions truly behave identically. The comparison is observational across studies, so it locates where the effect differs but cannot explain why.

Visualization

Funnel Plot

Effect against precision — the shape that publication bias distorts.

Suggested Interpretation

The short answer

No significant funnel asymmetry is detected (Egger's test p = 0.468). The 20 studies scatter across a range of effects and precisions with no obvious gap in one bottom corner — the classic signature of missing small studies. However, reading a funnel plot by eye is unreliable.

The detail

Precise studies (high precision values, top of the plot) cluster around the pooled effect; imprecise studies (low precision, bottom) scatter more widely. Trial 20 (2017) reports the highest effect (1.0826) with low precision (2.9762); Trial 08 (2023) reports the lowest (0.16) with moderate precision (3.9841). Egger's regression of standardised effect on precision yields an intercept of 0.414, p = 0.468 — not significant. The test uses all 20 studies and detects only one specific pattern: systematic variation of estimates with their own standard error. Asymmetry has causes beyond publication bias, including real differences between small and large studies.

What this can't tell you

Whether the test has sufficient power to detect small-study effects at 20 studies, or whether asymmetry patterns not captured by Egger's regression (e.g. outcome reporting bias) are present.

Data Table

Small-Study Effects

Egger's regression test for funnel asymmetry, with its power stated honestly.

QuantityValueMeaning
Egger intercept (bias)0.414Regression of each study's standardised effect on its precision. An intercept away from zero means small (imprecise) studies report systematically different effects from large ones.
Standard error of the intercept0.559How well determined that intercept is; small study counts make it large.
t statistic0.741The intercept divided by its standard error.
p-value0.468Two-sided test that the intercept is zero. The test uses all 20 studies. Even at this size it detects only one specific pattern — estimates that vary systematically with their own standard error — and asymmetry has causes other than publication bias, including real differences between small and large studies.
Studies in the test20Every one of the 20 pooled studies enters the regression.
Bias-adjusted slope0.443The slope estimates what the pooled effect would be for a study of infinite precision. It is a crude correction, not a bias-free estimate.
Suggested Interpretation

The short answer

Egger's regression finds no significant asymmetry (intercept 0.414, p = 0.468), meaning small and large studies report effects consistent with a common underlying value. However, the test at 20 studies has limited power and detects only one specific pattern: estimates that vary systematically with their own standard error.

The detail

Egger intercept: 0.414 (standard error 0.559, t = 0.741, p = 0.468). The test uses all 20 studies. The bias-adjusted slope is 0.443. Asymmetry has causes other than publication bias, including real differences between small and large studies. A symmetric funnel is consistent with a literature where entire classes of null results are missing uniformly.

What this can't tell you

At 20 studies the test has limited power to detect small-study effects. A non-significant result does not prove small-study effects are absent. The test cannot detect studies that were never written, and it captures only one specific form of asymmetry.

Data Table

Methods & Disclosure

Every formula, and the specific things pooling cannot fix.

ItemDetail
Input shapeOne row per study: the effect estimate in 'Effect Size', the study label in 'Study Label', and its precision. 20 of 20 rows were usable.
Precision sourceStandard errors came from the reported standard error in 'Standard Error'.
Fixed-effect modelInverse-variance weights (1 divided by the squared standard error): pooled estimate 0.503, standard error 0.036, 95% CI 0.432 to 0.574.
Random-effects modelDerSimonian-Laird moment estimator for tau-squared (0.0036), weights 1 divided by (variance plus tau-squared): pooled estimate 0.506, standard error 0.040, 95% CI 0.427 to 0.585.
HeterogeneityCochran's Q = 21.461 on 19 degrees of freedom (p = 0.312); I-squared = 11.47%; H-squared = 1.130.
Prediction intervalRandom-effects estimate plus or minus the 97.5th percentile of a t distribution on 18 degrees of freedom times the square root of (tau-squared plus the squared standard error of the pooled estimate).
Subgroup testBetween-subgroup Q on the 'Region' column = 0.203 on 1 degrees of freedom (p = 0.652), comparing the random-effects estimate of each subgroup.
Small-study effectsEgger's regression of the standardised effect on precision; intercept 0.414 (p = 0.468). The test uses all 20 studies. Even at this size it detects only one specific pattern — estimates that vary systematically with their own standard error — and asymmetry has causes other than publication bias, including real differences between small and large studies.
Null valueThe pooled effect is tested against a null value of zero. That is correct for mean differences, standardised mean differences and log-transformed ratios. Ratio measures such as odds ratios or risk ratios must be log-transformed BEFORE they are pooled, otherwise both the weighting and this test are wrong.
What pooling cannot fixPooling averages the studies it is given. It cannot repair selective reporting, cannot detect that the studies measured subtly different things, and cannot turn observational comparisons into causal ones — a pooled association is still an association.
Suggested Interpretation

The short answer

Inverse-variance weights pool the fixed-effect model; DerSimonian-Laird moment estimation adds between-study variance for the random-effects model. Cochran's Q tests heterogeneity; Egger's regression tests for small-study effects. All 20 rows were usable. Standard errors came from the reported 'Standard Error' column.

The detail

Fixed-effect weights: 1 divided by squared standard error. Pooled estimate 0.503, SE 0.036, 95% CI 0.432 to 0.574. Random-effects tau-squared (DerSimonian-Laird): 0.0036; weights: 1 divided by (variance + tau-squared); pooled estimate 0.506, SE 0.040, 95% CI 0.427 to 0.585. Heterogeneity: Cochran's Q = 21.461 on 19 df, p = 0.312; I-squared = 11.47%; H-squared = 1.130. Prediction interval: random-effects estimate ± t₉₈.₇₅(18 df) × √(tau-squared + SE²). Subgroup Q (Region): 0.203 on 1 df, p = 0.652. Egger's intercept: 0.414, p = 0.468. The analysis tests against a null of zero, valid only for differences or log-transformed ratios — raw ratio measures must be log-transformed before pooling.

What this can't tell you

Whether the studies measured the same construct, or whether observational associations are causal.

Methodology

Methodology

Statistical methodology and diagnostics for Meta-Analysis & Forest Plot

Statistical Method

Meta-Analysis & Forest Plot

Standard-library analysis: pool effect sizes across studies. Map one row per study — a study label, the effect estimate, and either its standard error or its 95% confidence interval — and get the forest plot, the fixed-effect (inverse-variance) and random-effects (DerSimonian-Laird) pooled estimates side by side, the heterogeneity diagnostics that decide which of the two means anything (Cochran's Q, I-squared, tau-squared, plus a prediction interval for the next study), optional subgroup pooling with a between-subgroup Q test, and a funnel plot with Egger's regression for small-study effects — reported with an explicit statement of how little power that test has at your study count.

Data
N = 20 observations
Assumptions
  • Each row is one study reporting one effect estimate, and the studies are independent of each other
  • Effects are on a scale where zero means no effect — mean differences, standardised mean differences, or LOG-transformed ratios
  • The standard errors are correct as reported; the analysis weights by them and cannot check them
  • When the interval is used instead of a standard error, it is a 95% normal-approximation interval
  • The studies estimate comparable quantities — the analysis can measure disagreement but cannot tell you the studies measured different constructs
Limitations
  • Pooling averages the studies it is given: it cannot repair selective reporting, poor study conduct, or an incomparable mix of designs
  • Raw odds ratios or risk ratios must be log-transformed before pooling — pooling them untransformed breaks both the weighting and the test against zero
  • The DerSimonian-Laird tau-squared is known to be downward-biased with few studies, which makes random-effects intervals slightly too narrow there
  • Egger's test has very little power below about 10 studies, and funnel asymmetry has causes other than publication bias
Software & Citation
MCP Analytics · mcpanalytics.ai
Code Appendix

Analysis Code

Complete R source code for this analysis

Meta-Analysis & Forest Plot

Pools per-study effect sizes into one estimate. Each row is one study carrying an effect estimate and its standard error (or a 95% confidence interval the analysis inverts into a standard error). The analysis fits BOTH the fixed-effect (inverse-variance) and the random-effects (DerSimonian-Laird) model, reports the heterogeneity diagnostics that decide which of the two means anything (Cochran's Q, I-squared, tau-squared), draws the forest plot, and checks the funnel for small-study effects with Egger's regression.

Why This Method?

A single study is one draw from a noisy process. Pooling weights each study by its precision, so the combined estimate is tighter than any one study — but only if the studies are estimating the same thing. The heterogeneity diagnostics are what tell you whether that premise holds, which is why they are reported next to the pooled number rather than buried underneath it.

What This Analysis Covers

  • Forest plot: every study's effect and confidence interval, plus both pooled estimates
  • Fixed-effect (inverse-variance) and random-effects (DerSimonian-Laird) models
  • Heterogeneity: Cochran's Q, I-squared, tau-squared, and a prediction interval
  • Subgroup pooling with a between-subgroup Q test when a grouping column is mapped
  • Funnel plot and Egger's regression for small-study effects, with an honest

statement of how little power that test has at the study count in hand

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {study, effect, std_error | ci_low + ci_high, subgroup}. 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
  #   k                     $ studies pooled
  #   study_h/effect_h/se_h/sub_h $ humanized user column names
  #   se_source             $ where each study's standard error came from
  #   drop_df               $ data.frame(reason, studies) — excluded rows
  #   fit                   $ pool_iv() result for ALL studies
  #   pi_lo/pi_hi           $ 95% prediction interval for a new study
  #   het_word/het_band     $ computed heterogeneity language (Cochrane bands)
  #   re_is_average         $ TRUE when I-squared >= 50 — the RE estimate then
  #                           describes a DISTRIBUTION, not one true effect
  #   egger_*               $ Egger regression intercept/se/t/p + power verdict
  #   forest_df             $ study + effect + ci_low + ci_high + std_error + weight_pct
  #   funnel_df             $ effect + precision (+ study label)
  #   models_df             $ the two pooled models side by side
  #   het_df                $ heterogeneity statistics table
  #   subgroup_df           $ per-subgroup pooling (or one "All studies" row)
  #   egger_df              $ small-study-effect table
  #   methods_df            $ full disclosure table
  #   metrics / json_output
  # === /SHARED EXPORTS ===

Step 1: Resolve the mapped columns (humanized for every sentence)

initial_rows <- nrow(df)
  study_h  <- humanize_semantic("study", col_map)
  effect_h <- humanize_semantic("effect", col_map)
  se_h     <- humanize_semantic("std_error", col_map)
  lo_h     <- humanize_semantic("ci_low", col_map)
  hi_h     <- humanize_semantic("ci_high", col_map)
  sub_h    <- humanize_semantic("subgroup", col_map)

  has <- function(nm) nm %in% names(df)
  if (!has("effect")) {
    stop(sprintf("Meta-analysis needs a column of per-study effect estimates mapped as the effect(&#x27;%s' was not found in the data).", effect_h))
  }

Step 2: Coerce the effect column (95% rule — refuse, never guess)

coerce_strict <- function(v, label_h, what) {
    if (is.numeric(v)) return(as.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 column &#x27;%s' mapped as %s does not look numeric — fewer than 95%% of its values parse as numbers. Map a numeric column.",
                   label_h, what))
    }
    conv
  }
  y_all <- coerce_strict(df$effect, effect_h, "the effect estimate")

Step 3: Standard errors — reported, or inverted from the interval

z95 <- stats::qnorm(0.975)
  has_se <- has("std_error")
  has_ci <- has("ci_low") && has("ci_high")
  se_from_ci <- NULL
  if (has_ci) {
    lo_v <- coerce_strict(df$ci_low,  lo_h, "the lower confidence limit")
    hi_v <- coerce_strict(df$ci_high, hi_h, "the upper confidence limit")
    se_from_ci <- (hi_v - lo_v) / (2 * z95)
    se_from_ci[!is.na(se_from_ci) & se_from_ci <= 0] <- NA_real_
  }
  n_filled <- 0L
  if (has_se) {
    se_all <- coerce_strict(df$std_error, se_h, "the standard error")
    if (!is.null(se_from_ci)) {
      fill <- is.na(se_all) & !is.na(se_from_ci)
      n_filled <- sum(fill)
      se_all[fill] <- se_from_ci[fill]
    }
    se_source <- if (n_filled > 0)
      sprintf("the reported standard error in &#x27;%s', with %d study standard error(s) recovered from the '%s' to '%s' interval",
              se_h, n_filled, lo_h, hi_h)
    else
      sprintf("the reported standard error in &#x27;%s'", se_h)
  } else if (!is.null(se_from_ci)) {
    se_all <- se_from_ci
    se_source <- sprintf("the reported 95%% confidence interval(&#x27;%s' to '%s'), inverted as half-width divided by %s",
                         lo_h, hi_h, r2(z95))
  } else {
    stop(sprintf("Meta-analysis needs each study&#x27;s precision: map either a standard-error column, or both confidence-limit columns, alongside '%s'.",
                 effect_h))
  }

Step 4: Study labels — never dropped, blanks filled, duplicates suffixed

labels_all <- if (has("study")) trimws(as.character(df$study)) else rep(NA_character_, initial_rows)
  auto_lab <- paste0("Study ", seq_len(initial_rows))
  blank_lab <- is.na(labels_all) | labels_all == ""
  labels_all[blank_lab] <- auto_lab[blank_lab]
  labels_all <- make.unique(labels_all, sep = " #")

Step 5: Usable rows — an effect AND a strictly positive standard error

bad_effect <- !is.finite(y_all)
  bad_se_missing <- !bad_effect & !is.finite(se_all)
  bad_se_nonpos <- !bad_effect & is.finite(se_all) & se_all <= 0
  keep <- !bad_effect & !bad_se_missing & !bad_se_nonpos

  drop_rows <- list()
  if (sum(bad_effect) > 0)
    drop_rows[[length(drop_rows) + 1]] <- data.frame(
      reason = sprintf("No usable %s value", effect_h),
      studies = sum(bad_effect), stringsAsFactors = FALSE)
  if (sum(bad_se_missing) > 0)
    drop_rows[[length(drop_rows) + 1]] <- data.frame(
      reason = "No usable standard error(and none recoverable from a confidence interval)",
      studies = sum(bad_se_missing), stringsAsFactors = FALSE)
  if (sum(bad_se_nonpos) > 0)
    drop_rows[[length(drop_rows) + 1]] <- data.frame(
      reason = "Standard error is zero or negative, so the study would carry infinite weight",
      studies = sum(bad_se_nonpos), stringsAsFactors = FALSE)
  drop_df <- if (length(drop_rows) > 0) do.call(rbind, drop_rows) else
    data.frame(reason = "None — every row was usable",
               studies = 0L, stringsAsFactors = FALSE)
  rownames(drop_df) <- NULL

  y <- y_all[keep]; se <- se_all[keep]; labs <- labels_all[keep]
  k <- length(y)
  final_rows <- k
  rows_removed <- initial_rows - final_rows
  n_dropped <- rows_removed

  if (k < 3) {
    stop(sprintf("Meta-analysis needs at least 3 studies with both a usable &#x27;%s' value and a usable standard error; only %d of the %d rows qualified (studies identified by '%s'). Pooling two studies produces a number, but Cochran's Q, I-squared and any funnel check are meaningless at that size.",
                 effect_h, k, initial_rows, study_h))
  }

Step 6: Pool — fixed-effect and DerSimonian-Laird random-effects

fit <- pool_iv(y, se)

Step 7: Prediction interval for the effect in a NEW study

pi_lo <- NA_real_; pi_hi <- NA_real_
  if (k >= 3) {
    tq <- stats::qt(0.975, df = k - 2)
    spread <- sqrt(fit$tau2 + fit$se_re^2)
    pi_lo <- fit$theta_re - tq * spread
    pi_hi <- fit$theta_re + tq * spread
  }

Step 8: Heterogeneity language — Cochrane bands, computed

het_band <- if (fit$i2 < 25) "low" else if (fit$i2 < 50) "moderate" else
    if (fit$i2 < 75) "substantial" else "considerable"
  re_is_average <- fit$i2 >= 50
  het_word <- sprintf("I-squared of %s%% (%s heterogeneity)", r2(fit$i2), het_band)
  models_agree <- abs(fit$theta_fe - fit$theta_re) < 0.05 * max(1e-9, abs(fit$theta_re))

The single most important honest sentence this module can produce.

het_caveat <- if (re_is_average) {
    sprintf("Because %s of the variation across studies is more than sampling error can explain, the random-effects estimate of %s is the AVERAGE of a distribution of genuinely different study effects — it is not an estimate of one true effect that every study shares. The 95%% prediction interval(%s to %s) is the honest range for what a new comparable study would find, and it is much wider than the confidence interval around the average.",
            paste0(r2(fit$i2), "%"), r3(fit$theta_re), r3(pi_lo), r3(pi_hi))
  } else {
    sprintf("Heterogeneity is %s(I-squared %s%%, %s), so the studies are consistent with estimating a common effect and the pooled number can be read as an estimate of it.",
            het_band, r2(fit$i2), fmt_pp(fit$p_q))
  }

Step 9: Egger's regression for small-study effects (funnel asymmetry)

precision <- 1 / se
  egger_b0 <- NA_real_; egger_b0_se <- NA_real_; egger_t <- NA_real_
  egger_p <- NA_real_; egger_slope <- NA_real_
  egger_ok <- k >= 3 && isTRUE(stats::var(precision) > 0)
  if (egger_ok) {
    snd <- y / se
    efit <- tryCatch(stats::lm(snd ~ precision), error = function(e) NULL)
    if (!is.null(efit)) {
      ecf <- tryCatch(summary(efit)$coefficients, error = function(e) NULL)
      if (!is.null(ecf) && nrow(ecf) == 2) {
        egger_b0 <- ecf[1, 1]; egger_b0_se <- ecf[1, 2]
        egger_t  <- ecf[1, 3]; egger_p     <- ecf[1, 4]
        egger_slope <- ecf[2, 1]
      }
    }
  }
  egger_sig <- !is.na(egger_p) && egger_p < 0.05
  egger_underpowered <- k < 10
  egger_verdict <- if (is.na(egger_p)) "not testable" else
    if (egger_sig) "detected" else "not detected"

The power statement always names the actual study count — a null Egger result on a handful of studies is not evidence of no small-study effect.

egger_power_note <- if (is.na(egger_p)) {
    sprintf("Egger&#x27;s test could not be computed: all %d studies report the same precision, so the funnel has no spread to regress against.", k)
  } else if (egger_underpowered) {
    sprintf("With only %d studies, Egger&#x27;s test has almost no power — the standard guidance is not to interpret funnel asymmetry below 10 studies. A p-value of %s here is close to uninformative, and the absence of a significant result is NOT evidence that small-study effects are absent.",
            k, fmt_p(egger_p))
  } else {
    sprintf("The test uses all %d studies. Even at this size it detects only one specific pattern — estimates that vary systematically with their own standard error — and asymmetry has causes other than publication bias, including real differences between small and large studies.",
            k)
  }

Step 10: Subgroup pooling (only when a grouping column is mapped)

sub_ok <- has("subgroup")
  sub_levels_note <- ""
  q_bet <- NA_real_; q_bet_df <- NA_integer_; q_bet_p <- NA_real_
  n_groups <- 0L
  if (sub_ok) {
    g_all <- trimws(as.character(df$subgroup))[keep]
    g_all[is.na(g_all) | g_all == ""] <- "Missing"
    tab <- sort(table(g_all), decreasing = TRUE)

Lump past 8 levels, then lump anything with fewer than 2 studies.

keep_lv <- names(tab)[seq_len(min(8, length(tab)))]
    g <- ifelse(g_all %in% keep_lv, g_all, "Other")
    tab2 <- table(g)
    sparse <- names(tab2)[tab2 < 2]
    if (length(sparse) > 0) g <- ifelse(g %in% sparse, "Other(fewer than 2 studies)", g)
    lumped <- sum(!(g_all %in% keep_lv)) + sum(g == "Other(fewer than 2 studies)")
    if (lumped > 0) {
      sub_levels_note <- sprintf(" %d study/studies fell into levels of &#x27;%s' that were too small or too numerous to pool separately and were grouped together.",
                                 lumped, sub_h)
    }
    gl <- names(sort(table(g), decreasing = TRUE))
    gl <- gl[sapply(gl, function(x) sum(g == x) >= 2)]
    n_groups <- length(gl)
    if (n_groups >= 1) {
      rows <- lapply(gl, function(lv) {
        idx <- which(g == lv)
        f <- pool_iv(y[idx], se[idx])
        data.frame(
          subgroup = lv, studies = length(idx),
          estimate = round(f$theta_re, 4),
          ci_low = round(f$re_lo, 4), ci_high = round(f$re_hi, 4),
          i_squared = round(f$i2, 1),
          se_g = f$se_re,
          stringsAsFactors = FALSE)
      })
      subgroup_df <- do.call(rbind, rows)
    } else {
      subgroup_df <- NULL
    }
    if (n_groups >= 2) {
      wg <- 1 / (subgroup_df$se_g^2)
      theta_bar <- sum(wg * subgroup_df$estimate) / sum(wg)
      q_bet <- sum(wg * (subgroup_df$estimate - theta_bar)^2)
      q_bet_df <- n_groups - 1L
      q_bet_p <- stats::pchisq(q_bet, df = q_bet_df, lower.tail = FALSE)
    }
  } else {
    subgroup_df <- NULL
  }
  if (is.null(subgroup_df)) {
    subgroup_df <- data.frame(
      subgroup = "All studies", studies = k,
      estimate = round(fit$theta_re, 4),
      ci_low = round(fit$re_lo, 4), ci_high = round(fit$re_hi, 4),
      i_squared = round(fit$i2, 1), se_g = fit$se_re,
      stringsAsFactors = FALSE)
    n_groups <- 1L
  }
  subgroup_df$interpretation <- vapply(seq_len(nrow(subgroup_df)), function(i) {
    r <- subgroup_df[i, ]
    sprintf("%d study/studies pooled to %s(95%% CI %s to %s); within-group I-squared %s%%.",
            r$studies, r3(r$estimate), r3(r$ci_low), r3(r$ci_high), r2(r$i_squared))
  }, character(1))
  subgroup_df$se_g <- NULL
  rownames(subgroup_df) <- NULL
  sub_differs <- !is.na(q_bet_p) && q_bet_p < 0.05

Step 11: Forest dataset — studies (capped) plus both pooled rows

max_plot <- 50L
  ord <- order(-y)
  plot_idx <- ord
  forest_note <- ""
  if (k > max_plot) {

Keep the most precise studies for a readable plot; the pooled estimates below still use every study.

keep_pl <- order(se)[seq_len(max_plot)]
    plot_idx <- keep_pl[order(-y[keep_pl])]
    forest_note <- sprintf(" The plot shows the %d most precise of the %d studies for readability; both pooled estimates use all %d.",
                           max_plot, k, k)
  }
  w_pct <- 100 * fit$w_re / sum(fit$w_re)
  forest_df <- data.frame(
    study = labs[plot_idx],
    effect = round(y[plot_idx], 4),
    ci_low = round(y[plot_idx] - z95 * se[plot_idx], 4),
    ci_high = round(y[plot_idx] + z95 * se[plot_idx], 4),
    std_error = round(se[plot_idx], 4),
    weight_pct = round(w_pct[plot_idx], 2),
    stringsAsFactors = FALSE)
  pooled_rows <- data.frame(
    study = c(sprintf("Pooled — fixed-effect(k = %d)", k),
              sprintf("Pooled — random-effects(k = %d)", k)),
    effect = round(c(fit$theta_fe, fit$theta_re), 4),
    ci_low = round(c(fit$fe_lo, fit$re_lo), 4),
    ci_high = round(c(fit$fe_hi, fit$re_hi), 4),
    std_error = round(c(fit$se_fe, fit$se_re), 4),
    weight_pct = c(NA_real_, NA_real_),
    stringsAsFactors = FALSE)
  forest_df <- rbind(forest_df, pooled_rows)
  rownames(forest_df) <- NULL

Step 12: Funnel dataset — effect against precision (1 / standard error)

set.seed(42)
  fidx <- if (k > 1000) sample(k, 1000) else seq_len(k)
  funnel_df <- data.frame(
    effect = round(y[fidx], 4),
    precision = round(precision[fidx], 4),
    study = labs[fidx],
    stringsAsFactors = FALSE)
  funnel_df <- funnel_df[order(funnel_df$effect), , drop = FALSE]
  rownames(funnel_df) <- NULL

Step 13: Model comparison table

models_df <- data.frame(
    model = c("Fixed-effect(inverse-variance)", "Random-effects(DerSimonian-Laird)"),
    estimate = round(c(fit$theta_fe, fit$theta_re), 4),
    std_error = round(c(fit$se_fe, fit$se_re), 4),
    ci_low = round(c(fit$fe_lo, fit$re_lo), 4),
    ci_high = round(c(fit$fe_hi, fit$re_hi), 4),
    p_value = c(fmt_p(fit$fe_p), fmt_p(fit$re_p)),
    interpretation = c(
      sprintf("Assumes every study estimates the SAME effect and all variation is sampling error. Weights are 1 divided by the study variance, so precise studies dominate. %s",
              if (re_is_average)
                "That assumption is contradicted here by the heterogeneity, so this number understates the real uncertainty."
              else
                "The heterogeneity statistics are consistent with that assumption here."),
      sprintf("Allows the true effect to differ across studies by tau-squared = %s, which flattens the weights and widens the interval. %s",
              r4(fit$tau2),
              if (fit$tau2 <= 0)
                "Here tau-squared came out at zero, so this model collapses onto the fixed-effect one and the two rows agree by construction."
              else if (re_is_average)
                "This is the estimate to quote, but as an average across differing effects, not as one true effect."
              else
                "The two models agree closely, which is what low heterogeneity implies.")
    ),
    stringsAsFactors = FALSE)

Step 14: Heterogeneity table

het_df <- data.frame(
    statistic = c("Cochran&#x27;s Q", "Degrees of freedom", "Q p-value",
                  "I-squared(%)", "H-squared", "Tau-squared", "Tau",
                  "95% prediction interval"),
    value = c(r3(fit$q), as.character(fit$df), fmt_p(fit$p_q),
              r2(fit$i2), r3(fit$h2), r4(fit$tau2), r3(fit$tau),
              if (is.na(pi_lo)) "not available" else paste0(r3(pi_lo), " to ", r3(pi_hi))),
    meaning = c(
      "Total weighted dispersion of the study estimates around the fixed-effect pooled value.",
      sprintf("Number of studies minus one(%d studies pooled).", k),
      sprintf("Probability of dispersion this large if every study shared one true effect. Q is known to have low power at small study counts, so a non-significant Q with %d studies does not establish homogeneity.", k),
      sprintf("Share of the total variation that exceeds sampling error: %s%% here, which is %s by the conventional bands.", r2(fit$i2), het_band),
      "Ratio of total variation to sampling variation. One means no excess heterogeneity.",
      "Estimated variance of the true effects across studies(DerSimonian-Laird). Zero means no detectable between-study variance.",
      "The standard deviation of the true effects, on the same scale as the effect itself.",
      "The range a NEW comparable study&#x27;s true effect would fall in about 95% of the time. This is the honest width of the finding, and it is always wider than the confidence interval around the average."
    ),
    stringsAsFactors = FALSE)

Step 15: Small-study-effect table

egger_df <- data.frame(
    quantity = c("Egger intercept(bias)", "Standard error of the intercept",
                 "t statistic", "p-value", "Studies in the test",
                 "Bias-adjusted slope"),
    value = c(r3(egger_b0), r3(egger_b0_se), r3(egger_t), fmt_p(egger_p),
              as.character(k), r3(egger_slope)),
    meaning = c(
      "Regression of each study&#x27;s standardised effect on its precision. An intercept away from zero means small (imprecise) studies report systematically different effects from large ones.",
      "How well determined that intercept is; small study counts make it large.",
      "The intercept divided by its standard error.",
      sprintf("Two-sided test that the intercept is zero. %s", egger_power_note),
      sprintf("Every one of the %d pooled studies enters the regression.", k),
      "The slope estimates what the pooled effect would be for a study of infinite precision. It is a crude correction, not a bias-free estimate."
    ),
    stringsAsFactors = FALSE)

Step 16: Methods disclosure

methods_df <- data.frame(
    item = c("Input shape", "Precision source", "Fixed-effect model",
             "Random-effects model", "Heterogeneity", "Prediction interval",
             "Subgroup test", "Small-study effects", "Null value",
             "What pooling cannot fix"),
    detail = c(
      sprintf("One row per study: the effect estimate in &#x27;%s', the study label in '%s', and its precision. %d of %d rows were usable.",
              effect_h, study_h, k, initial_rows),
      sprintf("Standard errors came from %s.", se_source),
      sprintf("Inverse-variance weights(1 divided by the squared standard error): pooled estimate %s, standard error %s, 95%% CI %s to %s.",
              r3(fit$theta_fe), r3(fit$se_fe), r3(fit$fe_lo), r3(fit$fe_hi)),
      sprintf("DerSimonian-Laird moment estimator for tau-squared(%s), weights 1 divided by(variance plus tau-squared): pooled estimate %s, standard error %s, 95%% CI %s to %s.",
              r4(fit$tau2), r3(fit$theta_re), r3(fit$se_re), r3(fit$re_lo), r3(fit$re_hi)),
      sprintf("Cochran&#x27;s Q = %s on %d degrees of freedom (%s); I-squared = %s%%; H-squared = %s.",
              r3(fit$q), fit$df, fmt_pp(fit$p_q), r2(fit$i2), r3(fit$h2)),
      sprintf("Random-effects estimate plus or minus the 97.5th percentile of a t distribution on %d degrees of freedom times the square root of(tau-squared plus the squared standard error of the pooled estimate).",
              max(1, k - 2)),
      if (!is.na(q_bet))
        sprintf("Between-subgroup Q on the &#x27;%s' column = %s on %d degrees of freedom (%s), comparing the random-effects estimate of each subgroup.",
                sub_h, r3(q_bet), q_bet_df, fmt_pp(q_bet_p))
      else
        "No between-subgroup test was run: a grouping column with at least two levels of two or more studies each was not mapped.",
      sprintf("Egger&#x27;s regression of the standardised effect on precision; intercept %s (%s). %s",
              r3(egger_b0), fmt_pp(egger_p), egger_power_note),
      "The pooled effect is tested against a null value of zero. That is correct for mean differences, standardised mean differences and log-transformed ratios. Ratio measures such as odds ratios or risk ratios must be log-transformed BEFORE they are pooled, otherwise both the weighting and this test are wrong.",
      "Pooling averages the studies it is given. It cannot repair selective reporting, cannot detect that the studies measured subtly different things, and cannot turn observational comparisons into causal ones — a pooled association is still an association."
    ),
    stringsAsFactors = FALSE)

Step 17: Metrics + one-paragraph computed answer

dir_word <- if (fit$theta_re > 0) "positive" else if (fit$theta_re < 0) "negative" else "exactly zero"
  re_sig <- (fit$re_lo > 0) || (fit$re_hi < 0)
  sig_clause <- if (re_sig)
    "the interval excludes zero, so the pooled effect is distinguishable from no effect"
  else
    "the interval includes zero, so the pooled effect is not distinguishable from no effect"

  metrics <- list(
    `Studies Pooled`            = k,
    `Fixed-Effect Estimate`     = round(fit$theta_fe, 4),
    `Random-Effects Estimate`   = round(fit$theta_re, 4),
    `Random-Effects 95% CI`     = paste0(r3(fit$re_lo), " to ", r3(fit$re_hi)),
    `I-squared(%)`             = round(fit$i2, 1),
    `Tau-squared`               = round(fit$tau2, 4),
    `Cochran Q p-value`         = fmt_p(fit$p_q),
    `Prediction Interval`       = if (is.na(pi_lo)) "not available" else
      paste0(r3(pi_lo), " to ", r3(pi_hi)),
    `Small-Study Effect`        = egger_verdict,
    `Egger p-value`             = fmt_p(egger_p)
  )

  sub_clause <- if (sub_differs) {
    sprintf(" Pooling by &#x27;%s' shows the subgroups do NOT share one effect (between-group Q = %s on %d degrees of freedom, %s), so the single pooled number averages across real differences.",
            sub_h, r3(q_bet), q_bet_df, fmt_pp(q_bet_p))
  } else if (!is.na(q_bet_p)) {
    sprintf(" Pooling by &#x27;%s' gives no evidence that the subgroups differ (between-group Q = %s, %s).",
            sub_h, r3(q_bet), fmt_pp(q_bet_p))
  } else ""

  egger_clause <- if (egger_sig) {
    sprintf(" Egger&#x27;s regression finds funnel asymmetry (intercept %s, %s): smaller studies report systematically %s effects, which is what publication bias looks like — though real differences between small and large studies produce the same pattern.",
            r3(egger_b0), fmt_pp(egger_p),
            if (!is.na(egger_b0) && egger_b0 > 0) "larger" else "smaller")
  } else if (!is.na(egger_p)) {
    sprintf(" Egger&#x27;s regression finds no significant funnel asymmetry (intercept %s, %s), but %s",
            r3(egger_b0), fmt_pp(egger_p),
            if (egger_underpowered)
              sprintf("with only %d studies that test is close to powerless and cannot rule small-study effects out.", k)
            else
              "asymmetry tests only detect one specific pattern and cannot rule publication bias out.")
  } else ""

  json_output <- list(
    answer = paste0(
      "Pooling ", k, " studies on &#x27;", effect_h, "': the random-effects (DerSimonian-Laird) estimate is ",
      r3(fit$theta_re), " (95% CI ", r3(fit$re_lo), " to ", r3(fit$re_hi), "), and ", sig_clause,
      ". The fixed-effect estimate is ", r3(fit$theta_fe), " (95% CI ", r3(fit$fe_lo), " to ", r3(fit$fe_hi),
      "). Heterogeneity: Cochran&#x27;s Q = ", r3(fit$q), " on ", fit$df, " degrees of freedom (", fmt_pp(fit$p_q),
      "), I-squared = ", r2(fit$i2), "% (", het_band, "), tau-squared = ", r4(fit$tau2), ". ",
      het_caveat, sub_clause, egger_clause
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "forest_plot", "pooled_models",
        "heterogeneity", "subgroup_pooling", "funnel_plot",
        "small_study_effects", "methods"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows = initial_rows, final_rows = final_rows,
    rows_removed = rows_removed, n_dropped = n_dropped, k = k,
    study_h = study_h, effect_h = effect_h, se_h = se_h, sub_h = sub_h,
    se_source = se_source, n_filled = n_filled,
    has_subgroup = sub_ok, sub_levels_note = sub_levels_note,
    drop_df = drop_df, fit = fit, pi_lo = pi_lo, pi_hi = pi_hi,
    het_band = het_band, het_word = het_word, het_caveat = het_caveat,
    re_is_average = re_is_average, models_agree = models_agree,
    dir_word = dir_word, re_sig = re_sig, sig_clause = sig_clause,
    egger_b0 = egger_b0, egger_b0_se = egger_b0_se, egger_t = egger_t,
    egger_p = egger_p, egger_slope = egger_slope, egger_sig = egger_sig,
    egger_underpowered = egger_underpowered, egger_verdict = egger_verdict,
    egger_power_note = egger_power_note,
    q_bet = q_bet, q_bet_df = q_bet_df, q_bet_p = q_bet_p,
    n_groups = n_groups, sub_differs = sub_differs,
    forest_df = forest_df, forest_note = forest_note,
    funnel_df = funnel_df, models_df = models_df, het_df = het_df,
    subgroup_df = subgroup_df, egger_df = egger_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 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