← Methods repository
concept

Missing Data Pattern Table

A diagnostic artifact that cross-tabulates the rate and co-occurrence structure of missing values by variable, treatment arm, calendar time, site, and outcome status to characterize the missingness pattern and inform a defensible mechanism judgment (MCAR / MAR / MNAR) before any analysis decision.

Descriptive_Epidemiologymissing-datamissingness-patternmcar-mar-mnarmultiple-imputationcomplete-case-analysisdata-quality-assessmentdescriptive-epidemiologysensitivity-analysis
Methods reference only. Use primary source citations and local policy before applying this in a study protocol, regulatory submission, payer dossier, or clinical decision.

In plain language

A missing data pattern table is a simple diagnostic you build before any analysis to see exactly which variables have gaps, how many gaps, and whether those gaps are spread evenly or clustered in ways that matter. You cross-tabulate the percent missing for each variable by treatment group, site, and time period, then examine which variables tend to be missing together on the same record. The pattern tells you whether gaps appear random or are tied to something measurable in your data, which in turn tells you which statistical method is appropriate to handle them.

A missing data pattern table is the descriptive backbone of any principled missing-data strategy in real-world evidence. It is not an estimator and not an imputation; it is the audit that turns "we have missingness" into a specific, testable claim about which variables are missing, how much, for whom, and together with what. Two distinct objects travel under this name and both belong in the table: (1) the per-variable missingness summary — the proportion missing for each analysis variable, ideally stratified by the factors that drive selection in routinely collected data (treatment arm, calendar quarter, care site/plan, and outcome status); and (2) the monotone-vs-non-monotone pattern matrix — the set of distinct missingness patterns across variables (the binary observed/missing footprint per record) and how many records share each, which reveals whether missingness is monotone (dropout-like, amenable to simpler methods) or arbitrary (requiring chained-equations imputation or pattern-mixture modeling).

Core conceptual distinction

(table vs mechanism). The pattern table answers a different question than the mechanism. The table is purely observable: counts and cross-tabs you can compute from the data. The mechanism — Rubin's taxonomy of MCAR (missingness independent of all data), MAR (missingness depends only on observed data), and MNAR (missingness depends on the unobserved value itself) — is partly unverifiable and must be argued, not measured. The table constrains that argument: if HbA1c is missing far more often in one treatment arm or in Medicare Advantage enrollees, MCAR is empirically implausible (Little's test on the pattern matrix can formalize this), and you are forced to choose between MAR-justified methods (multiple imputation, IPCW) conditional on the observed drivers, or MNAR-justified sensitivity analysis (pattern-mixture, delta-adjustment). The estimand is unchanged by the table, but the credibility of the identifying assumption required to estimate it is exactly what the table is built to interrogate.

Pros, cons, and trade-offs

(specific and comparative). - vs jumping straight to complete-case analysis: The pattern table exposes whether complete-case is even admissible. Complete-case is unbiased only under MCAR (or, for a regression coefficient, when missingness is independent of the outcome given covariates); the table is what lets you reject MCAR. Cost: it is descriptive overhead that does not itself fix anything. Prefer the table before any deletion-based analysis — it is cheap insurance against a silently biased default. - vs jumping straight to multiple imputation (MI): MI is valid under MAR, but MAR-given-what? The pattern table tells you which observed variables drive missingness and therefore which auxiliary variables the imputation model must include to make MAR plausible. Skipping the table risks an imputation model that omits the very predictors that make missingness ignorable, reintroducing bias while projecting false confidence (Hughes et al.). Prefer the table to specify the imputation model, not as a substitute for it. - vs a single overall "% missing" figure: The stratified table is the entire value-add. A 5% overall HbA1c-missing rate that is 1% in one arm and 20% in the other is a confounded-missingness alarm; the marginal 5% hides it. Cost: more programming and more cells to interpret. Always prefer the stratified, arm-by-time-by-site version for any comparative analysis.

When to use

(decision rules). Always, and first — before specifying complete-case, MI, IPCW, or pattern-mixture analyses, and before finalizing the statistical analysis plan. It is mandatory documentation for regulatory- and HTA-grade RWE: ICH E9(R1), FDA RWE guidance, and ISPOR good-practice all expect missingness to be characterized and the handling strategy pre-specified and justified against it. Run it on every analysis variable, every confounder feeding a propensity score, and every component of a composite endpoint or cost vector.

When NOT to use — and when it is actively misleading or dangerous

(decision rules). The table itself is never the wrong thing to compute; the danger is misreading it. (1) Do not treat a passing Little MCAR test as license for complete-case. The test has low power, ignores MNAR entirely, and a non-significant result in a small or sparse cell pattern is uninformative — acting on it is the classic trap. (2) Do not stratify only by baseline variables when the missingness is outcome-driven. If a lab is ordered because the patient is deteriorating, the value is MNAR and no amount of observed-covariate stratification rescues MAR; the table must include outcome status precisely to surface this, and if it does, MI alone is misleading and you need MNAR sensitivity analysis. (3) Do not confuse structural/legitimate missingness with informative missingness. A pregnancy field "missing" for men, or a lab not drawn because it was clinically unnecessary, are not the same as a deteriorating-patient lab gap; collapsing them in one table and imputing across them fabricates data. (4) Do not let a clean-looking table mask differential ascertainment masquerading as data presence — a zero "missing" rate for diagnoses in claims does not mean the disease is absent; it means a code was or was not submitted, which is an exposure/outcome-misclassification problem the missingness table cannot see.

Data-source operational depth

(real failure modes and workarounds). - Claims (FFS vs Medicare Advantage): Most "missingness" in claims is really non-observation of person-time, and it is structurally differential. Medicare Advantage encounter data are notoriously incomplete relative to fee-for-service claims, so any variable derived from utilization (comorbidity flags, prior therapy, baseline cost) is differentially missing for MA-only person-time — and MA enrollment correlates with health and region, making this MNAR with respect to the very confounders you need. Workaround: restrict to enrollees with the relevant benefit (A/B/D or commercial medical+pharmacy) across the lookback, flag MA-only spans explicitly, and stratify the pattern table by plan type so the differential is visible rather than averaged away. Lab results are absent from most medical claims entirely (you get the CPT for the test, not the value), so an HbA1c "value" column is ~100% missing in claims-only data. - EHR: Missingness is encounter-driven and informative. A field is populated only if a visit occurred and the clinician charted it; sicker, more-engaged patients accrue more data, so completeness correlates with the outcome. External-care leakage means a patient treated elsewhere looks "missing" when they are merely unobserved in this system. Workaround: stratify by site and by within-system utilization, treat loss to follow-up as potentially informative (IPCW rather than naive complete-case), and use linked claims to distinguish "not done" from "done elsewhere." - Registry: Pre-specified data dictionaries make missingness more interpretable, but completeness varies by site and over the enrollment period, and adjudicated fields may be missing precisely for the hardest-to-adjudicate (often most severe) cases. Stratify by site and accrual era. - Linked claims–EHR–vital records: The richest substrate but linkage itself induces missingness — the unlinkable subset is a selected population, and date discrepancies between order, fill, and service dates create apparent missingness in time-windowed variables. Report the linkage denominator as the first row of the pattern table.

Worked claims/EHR example

Question: comparative HbA1c control (mean HbA1c at 12 months) for GLP-1 RA vs basal insulin initiators in a linked commercial-claims + EHR diabetes cohort. (1) For each analysis variable — baseline HbA1c, baseline eGFR, BMI, the 12-month outcome HbA1c, and each propensity-score confounder — compute the proportion of `person_id` with a non-missing value. (2) Cross-tabulate the 12-month HbA1c missingness by `arm` × calendar quarter of `index_date` × care `site_id` × `plan_type` (FFS vs MA-equivalent commercial PPO vs HMO). (3) Suppose the table shows outcome HbA1c missing in 18% of the basal-insulin arm but 9% of the GLP-1 arm, concentrated in HMO sites in 2020-Q2 (a COVID lab-access shock) — a textbook MAR-on-observed-factors (arm, site, time) plus a suspected MNAR component (insulin initiators with poor control may skip labs). (4) Build the monotone-vs-arbitrary pattern matrix: if a record missing the 12-month HbA1c is also missing the 12-month eGFR (both lab-draw-dependent), the pattern is co-clustered, signaling a shared visit-attendance mechanism to encode as an auxiliary "had a 12-month encounter" predictor. (5) Decision: complete-case is rejected (Little's test significant; rates differ by arm); specify multiple imputation by chained equations including arm, site, quarter, the encounter-attendance flag, and the co-missing labs as predictors, and add a tipping-point/delta-adjusted MNAR sensitivity analysis shifting imputed values in the insulin arm to test robustness. The pattern table is what justified every one of these choices in the SAP.

Interpreting the output

Consider the GLP-1 vs. basal insulin cohort above, where the 12-month HbA1c outcome is missing in 9% of GLP-1 arm patients and 19% of basal insulin patients — a 10 percentage-point differential between arms.

(1) Formal statistical interpretation. The observed arm-differential of 10 percentage points in outcome missingness is direct evidence against the missing completely at random (MCAR) assumption, which requires that the probability of missingness be unrelated to both observed and unobserved variables. Little's MCAR test further quantifies this departure. The co-clustering of outcome and eGFR missingness within the same patients — the monotone pattern — indicates a shared visit-attendance mechanism rather than independent item non-response; this structural feature must be encoded in any imputation model as an auxiliary predictor. Under MCAR, complete-case analysis would be valid; the pattern table rules that out. Under missing at random (MAR) conditional on arm, site, and time, multiple imputation with those variables as predictors is valid; the suspected MNAR component requires an additional sensitivity analysis.

(2) Practical interpretation for a decision-maker. The pattern table is the evidentiary basis for every subsequent missing-data decision in the SAP. A 10-point arm differential means the complete-case sample is differentially depleted in insulin initiators — likely the sicker ones — biasing any unadjusted comparison. Documenting the pattern before analysis forces transparency: reviewers can see exactly which variables are missing, by how much, and in which groups, so the choice of imputation method is grounded in data rather than assumption.

Worked example

Scenario

You are studying a diabetes drug comparison: GLP-1 agonist versus basal insulin, using a linked claims and EHR dataset of 200 patients. Before you run any models, your statistician asks you to build a missing data pattern table for four key analysis variables: baseline HbA1c, baseline eGFR, BMI, and the 12-month outcome HbA1c. The table below is what you would produce. You then read the pattern to decide how to handle the missingness.

Dataset

Per-variable missingness summary table: rows are analysis variables, columns show total patients with a value, count missing, percent missing, and the missingness pattern type across all four variables. Two treatment arms are shown side by side. N = 100 per arm (200 total).

variableglp1_n_observedglp1_n_missingglp1_pct_missinginsulin_n_observedinsulin_n_missinginsulin_pct_missingpattern_type
baseline_hba1c9733%9644%arbitrary
baseline_egfr9555%9466%arbitrary
bmi9288%901010%arbitrary
outcome_hba1c_12m9199%811919%monotone

Steps

  • Read the last two numeric columns first: outcome HbA1c at 12 months is missing in 9 of 100 GLP-1 patients (9%) but 19 of 100 insulin patients (19%). A two-fold difference between arms is the first alarm that missingness may not be random.

  • Check whether the gap can be explained by observed factors you already have: if the 19 missing insulin records are concentrated at one care site or in the first quarter of 2020 (a COVID lab-access shock), missingness is tied to observable variables, which supports a MAR argument.

  • Read the pattern_type column: baseline variables show an arbitrary pattern, meaning gaps on baseline HbA1c do not predict gaps on baseline eGFR on the same record. The outcome variable shows a monotone pattern, meaning patients who miss their 12-month HbA1c also tend to miss their 12-month eGFR, suggesting a shared cause such as not having a 12-month clinic visit at all.

  • Combine these two reads: the outcome has both a large arm difference (differential, so not MCAR) and a monotone co-missing structure (shared visit-attendance mechanism). MCAR is rejected. The question is now MAR versus MNAR.

  • Ask the clinical question to distinguish MAR from MNAR: are insulin patients more likely to skip their 12-month lab because their control is poor and they are avoiding their doctor? If yes, the missing value itself (poor HbA1c) predicts being missing, which is MNAR.

  • Arithmetic check: overall outcome missingness = (9 + 19) / 200 = 28 / 200 = 14%. The arm-specific rates are 9% and 19%, which average to 14% only when the arms are equal in size, as they are here: (9% + 19%) / 2 = 14%. This confirms the arm rates are internally consistent.

Result

Dominant pattern: outcome HbA1c is missing in 9/100 = 9% of GLP-1 patients and 19/100 = 19% of insulin patients, a 10 percentage-point arm difference that rules out MCAR. The monotone co-missing structure for outcome labs points to a shared visit-attendance mechanism. The implied handling: (1) complete-case is rejected because missingness is differential by arm; (2) multiple imputation is warranted and must include arm, site, and calendar quarter as predictors to make a MAR assumption plausible; (3) a sensitivity analysis shifting imputed insulin-arm values by a delta (MNAR tipping-point) should be pre-specified to test whether conclusions hold if the missing patients had worse-than-imputed control.

Runnable example

python implementation

Builds a missing data pattern table from a one-row-per-patient analysis frame. Required input (post data-management): df : person_id (unique), arm in {'STUDY','COMPARATOR'}, index_date (datetime), site_id, plan_type ('FFS'/'MA_EQUIV'), plus the analysis...

import pandas as pd
import numpy as np

ANALYSIS_VARS = ["base_hba1c", "base_egfr", "bmi", "out_hba1c_12m"]

def missingness_by_stratum(df: pd.DataFrame, var: str) -> pd.DataFrame:
    # % missing for one variable by arm x calendar quarter of index_date.
    q = df["index_date"].dt.to_period("Q").astype(str)
    out = (df.assign(_miss=df[var].isna(), _qtr=q)
             .groupby(["arm", "_qtr"])
             .agg(n=("person_id", "size"), n_missing=("_miss", "sum")))
    out["pct_missing"] = (out["n_missing"] / out["n"]).round(3)
    return out.reset_index()

def variable_summary(df: pd.DataFrame, vars_: list[str]) -> pd.DataFrame:
    # Overall and by-arm missingness for every analysis variable (the headline table).
    rows = []
    for v in vars_:
        overall = df[v].isna().mean()
        by_arm = df.groupby("arm")[v].apply(lambda s: s.isna().mean())
        rows.append({"variable": v, "pct_missing_overall": round(overall, 3),
                     **{f"pct_missing_{a}": round(p, 3) for a, p in by_arm.items()}})
    return pd.DataFrame(rows)

def pattern_matrix(df: pd.DataFrame, vars_: list[str]) -> pd.DataFrame:
    # Distinct observed(1)/missing(0) footprints across vars_ and how many records share each.
    # A single dominant footprint => near-complete; staircase of footprints => monotone (dropout-like);
    # many scattered footprints => arbitrary pattern -> chained-equations imputation required.
    obs = df[vars_].notna().astype(int)
    pat = (obs.groupby(vars_).size()
              .reset_index(name="n_records")
              .sort_values("n_records", ascending=False))
    pat["n_missing_vars"] = (len(vars_) - pat[vars_].sum(axis=1)).astype(int)
    return pat

var_table = variable_summary(df, ANALYSIS_VARS)
outcome_by_stratum = missingness_by_stratum(df, "out_hba1c_12m")
patterns = pattern_matrix(df, ANALYSIS_VARS)
r implementation

Missing data pattern table in R. Input `df` is one row per patient with: person_id, arm, index_date (Date), site_id, plan_type, and the analysis variables, with missing encoded as NA (not sentinel codes). naniar gives the per-variable summary;...

library(dplyr)
library(naniar)
library(mice)

analysis_vars <- c("base_hba1c", "base_egfr", "bmi", "out_hba1c_12m")

# (1) Per-variable missingness, overall and stratified by arm x calendar quarter.
var_summary <- naniar::miss_var_summary(df %>% select(all_of(analysis_vars)))

stratified <- df %>%
  mutate(qtr = paste0(lubridate::year(index_date), "Q",
                      lubridate::quarter(index_date))) %>%
  group_by(arm, qtr) %>%
  summarise(n = n(),
            pct_missing_outcome = mean(is.na(out_hba1c_12m)),
            .groups = "drop")

# (2) Pattern matrix: rows = distinct observed/missing footprints, last col = count missing,
#     row counts = records sharing the footprint. A staircase indicates monotone missingness.
pattern <- mice::md.pattern(df %>% select(all_of(analysis_vars)), plot = FALSE)