Visualizations and Diagrams in Pharmacoepidemiology and RWE
The disciplined use of diagrams and statistical graphics — causal DAGs, study-design timelines, attrition flows, propensity-score overlap and standardized-difference (Love) plots, Kaplan-Meier/cumulative-incidence curves, forest plots, and treatment-pattern Sankeys — to specify causal assumptions and time anchors, diagnose positivity and confounding control, and transparently report effect estimates across the RWE study lifecycle.
In plain language
In a pharmacoepidemiology study, charts and diagrams do real scientific work — they are not decoration. Each visualization in the standard toolkit answers a different question: Did we build the study population fairly? Do the two treatment groups look alike before we compare outcomes? What happened to patients over time? Picking the right chart for each question, and avoiding the wrong one, is what separates a transparent study report from a misleading one.
Visualizations in pharmacoepidemiology are not decoration; they are decision-support and transparency instruments that make assumptions, time anchors, data fitness, and diagnostics legible to statisticians, clinicians, regulators, and payers. A small canonical set recurs across the study lifecycle: the directed acyclic graph (DAG) to declare the causal structure and the minimal sufficient adjustment set; the study-design timeline (calendar time vs. patient event time, with eligibility, washout, time zero, covariate window, follow-up, and censoring) to forestall immortal-time and adjustment-for-the-future errors; the attrition/CONSORT flow to expose selection; the propensity-score overlap plot and standardized-mean-difference ("Love") plot to certify positivity and balance; the Kaplan-Meier or cumulative-incidence curve with a number-at-risk table; the forest plot for primary, subgroup, and sensitivity estimates; and the Sankey for treatment patterns and lines of therapy. Each pairs with exact text and tables (N, point estimate + CI, code lists, windows, trimming rules) so the figure is auditable, not impressionistic.
Core conceptual distinction
A figure encodes one of three distinct epistemic jobs, and conflating them is the most common failure. (1) Assumption-declaring figures (DAGs, design timelines) state what the analyst believes about causal structure and time and are not derived from the data — a DAG cannot be "validated" by the dataset it motivates. (2) Diagnostic figures (PS-overlap, Love plot, Schoenfeld-residual plot, attrition flow) are computed from the data to test whether a design assumption holds — positivity, covariate balance, proportional hazards, unbiased selection. (3) Inferential/reporting figures (KM/CIF, forest plot) display the estimated quantity and must therefore be pinned to a pre-specified estimand: a Kaplan-Meier curve reports 1 minus the marginal survival treating competing events as censoring, whereas a cumulative-incidence (Aalen-Johansen) curve reports the actual probability of the event in the presence of competing risks. In elderly or oncology cohorts those two curves diverge sharply, and choosing 1-KM when death competes overstates the cause-specific cumulative risk. The graphic must match the model (cause-specific Cox vs. Fine-Gray subdistribution) and the question, not the other way around.
Pros, cons, and trade-offs
- vs. text/table-only reporting (e.g., a bare STaRT-RWE table): Figures communicate temporal and causal structure that prose hides — a reviewer sees immortal time in a timeline diagram instantly but may miss it in a methods paragraph. Cost: figures can oversimplify (a DAG omits unmeasured nodes by drafting choice) and are misread without the accompanying numbers; they require tooling and iteration. Prefer figures for any comparative design, but never ship a figure without its paired table. - vs. interactive dashboards (Shiny/Streamlit): Static, code-generated SVG/PNG plus portable Mermaid are version-controllable, reproducible, embeddable in protocols and publications, and need no server. Cost: no live subgroup/sensitivity filtering. Prefer static for protocol, publication, and regulatory submission; supply code so reviewers can regenerate or extend interactively. - DAG (DAGitty/dagitty) vs. ad-hoc box-and-arrow diagrams: A machine-readable DAG yields the adjustment set algorithmically and flags colliders/M-bias; a hand-drawn diagram invites conditioning on a collider or a mediator. Cost: a DAG forces you to commit to a structure you may be unsure of. Prefer a formal DAG whenever adjustment decisions are contested. - Kaplan-Meier vs. cumulative-incidence function: KM is correct only when the competing-risk hazard is null or truly independent; CIF is correct under competing events. Prefer CIF (with cause-specific or Fine-Gray models) in elderly, oncology, and end-stage-disease cohorts, and report at-risk and competing-event counts alongside.
When to use
Specify the DAG and the design timeline in the protocol/SAP before any data pull; produce the attrition funnel, PS-overlap, and Love plot as gating diagnostics before fitting the outcome model; produce KM/CIF and forest plots only for the pre-registered estimand. Use a Sankey when the deliverable is treatment sequencing, switching, or lines of therapy. Pre-register each figure ("a Love plot of |SMD| pre/post weighting with a 0.1 reference line will be reported") so the figure set is a commitment, not a post-hoc choice.
When NOT to use — and when it is actively misleading or dangerous
- A forest plot that pools across data sources or payers with materially different capture hides heterogeneity; if Medicare Advantage encounter completeness differs from fee-for-service claims, a single pooled point estimate is a weighted average of incommensurable measurements. Facet or stratify instead. - A Kaplan-Meier curve where death competes with the event (e.g., re-hospitalization in heart-failure elders) is actively misleading — 1-KM overstates risk; switch to CIF. - A Love plot used as proof of no confounding. Balance on measured covariates says nothing about unmeasured confounding; a beautiful Love plot can coexist with severe residual bias. Pair with negative-control diagnostics and an E-value, never present balance as exchangeability. - A DAG presented as data-derived truth. It is an assumption; over-simplified DAGs that omit a real confounder or draw a confounder as a collider will license the wrong adjustment set. - A PS-overlap plot read only at the mean. Near-positivity lives in the tails; a histogram that looks fine centrally can hide regions where one arm has near-zero density, which matching silently discards while the estimand quietly shifts. - An attrition flow that reports only totals, not per-arm exclusions. Differential exclusion by arm is exactly the selection bias the figure exists to surface.
Data-source operational depth
- Claims (FFS vs. MA vs. commercial): Attrition funnels, PS/Love plots, KM/CIF, and forests should be faceted by payer because measurement differs. Medicare Advantage encounter data lack the fee-for-service claim stream, so MA-only person-time can masquerade as "no event" — restrict survival and attrition visuals to enrollees with the relevant benefit (A/B/D or commercial pharmacy) and exclude MA-only spans. MA risk-adjustment activity (HCC coding, health-risk assessments, chart review) inflates diagnosis frequency, so a Love plot can look better simply because a covariate is coded more intensely in one arm — a coding artifact, not real balance. Differential competing risks are common: if one drug is preferentially used in sicker elderly patients, death competes more in that arm, and a KM (vs. CIF) contrast is biased by the competing event, not the exposure. - EHR: Visit-driven capture means the time axis of a design timeline must show observation windows explicitly; a patient who leaves the system is differentially lost, so attrition and KM curves must treat loss to follow-up as potentially informative. Labs, vitals, and NLP-derived problem-list nodes enrich DAGs and sharpen outcome adjudication relative to claims. - Registry: Adjudicated outcomes and stage/severity make KM/CIF and DAG severity-nodes reliable; pharmacy exposure is usually incomplete — link to claims for fills and to a death index to firm up the censoring shown in survival visuals. - Linked claims–EHR–vital records: The ideal substrate for trustworthy survival and competing-risk figures (severity + completeness + mortality), but linkage selection (only the linkable subset) and order/fill/service date discrepancies must be reconciled before the time-zero shown on the design timeline is assigned. A subtle trap is immortal time in procedure studies: if time zero is set at diagnosis but the index procedure occurs later, the design timeline will reveal guaranteed event-free ("immortal") person-time between the two — anchoring the timeline figure at the procedure date (or using a landmark) makes the bias visible and removable.
Worked claims example
Question: incident heart-failure hospitalization with a second-generation sulfonylurea vs. a DPP-4 inhibitor among type-2-diabetes adults in a commercial + Medicare FFS database. The figure set, in order: (1) Database-feasibility attrition funnel — source diabetes population (e.g., N=412,000) → ≥2 diabetes diagnoses (N=355,000) → 365 days continuous A/B/D or commercial medical+pharmacy enrollment before first study fill (N=180,000) → no sulfonylurea/DPP-4 fill in the 365-day washout, i.e., incident users (N=64,000) → analytic cohort after age ≥18 and exclusion of MA-only person-time (N=58,400), with the per-arm count and exclusion reason at each box so differential drop-out is visible. (2) Study-design timeline anchoring time zero at the first qualifying fill, with the 365-day baseline covariate window strictly before time zero and follow-up censored at disenrollment, death, end of data, and (as-treated) discontinuation = last `days_supply` end + grace period. (3) PS-overlap density by arm to confirm positivity, then a Love plot of |SMD| before vs. after 1:1 matching/overlap weighting with a 0.1 reference line — read in the tails, not just at the mean. (4) A cumulative-incidence (Aalen-Johansen) curve for HF hospitalization with all-cause death as a competing event (not 1-KM, because death competes heavily in this elderly-skewed cohort), with a number-at-risk and number-of-deaths table. (5) A forest plot of the cause-specific HR and the Fine-Gray subdistribution HR for the primary analysis plus sensitivity analyses (washout 180 vs. 365 days, grace period, negative-control outcome), faceted FFS vs. commercial so payer heterogeneity in capture is explicit rather than averaged away.
Worked example
Scenario
A research team is writing a report on a study comparing two diabetes pills — a sulfonylurea and a DPP-4 inhibitor — to see which is associated with fewer heart-failure hospitalizations. Before submitting the report, the team needs to choose the right visualization for each section: (1) showing how they built the study group, (2) showing that the two drug groups are comparable, (3) showing how outcomes unfolded over time, and (4) showing results across subgroups and sensitivity analyses. The table below maps each communication need to the correct visualization and flags which chart would be wrong.
Dataset
Study communication needs mapped to the right pharmacoepidemiology visualization
| Section of report | Communication need | Correct visualization | Wrong choice and why |
|---|---|---|---|
| Cohort selection | Show how the 412,000 diabetes patients were filtered down to 58,400 in the analytic cohort | Attrition funnel — one box per filter with patient counts and exclusion reasons, reported separately for each drug arm | A single summary table — hides whether one drug arm lost more patients than the other at any step |
| Study design | Show when covariates were measured, when follow-up started, and what events end follow-up | Study-design timeline (Gantt-style) anchored at the first qualifying prescription fill | A paragraph of methods text — a reviewer cannot spot immortal time or a misplaced measurement window in prose as easily as in a diagram |
| Covariate balance | Show that the two drug groups look similar on age, kidney disease, prior hospitalizations, etc., before and after statistical weighting | Love plot — one dot per variable, two series (before and after weighting), vertical reference line at 0.1 | Reporting only the p-values from t-tests — p-values depend on sample size and do not directly measure how different the groups are |
| Time-to-event outcomes | Show the probability of heart-failure hospitalization over two years when some patients also die before being hospitalized | Cumulative-incidence curve (Aalen-Johansen method) with a table of patients still at risk and patients who died | Standard Kaplan-Meier curve — in an older cohort where death competes with hospitalization, 1-minus-KM overstates how many patients would eventually be hospitalized |
| Subgroup and sensitivity results | Show the main effect estimate plus results for age groups, sex, kidney-disease status, and two alternate analysis specifications | Forest plot — one row per analysis, box at the point estimate, horizontal line for the uncertainty interval, vertical null-effect reference line | A single number in the abstract — one result cannot communicate whether the finding holds across patient subgroups or changes under different analytic assumptions |
Steps
Start at the beginning of the study lifecycle, not the end. Draw the study-design timeline in the protocol before any data are pulled — it forces the team to declare exactly where time zero sits and prevents the exposure window from accidentally overlapping the outcome window.
Build the attrition funnel next, during cohort construction. Report exclusion counts for each drug arm separately so that differential drop-out (one arm losing more patients than the other) is visible and can be explained.
Before fitting any outcome model, produce the Love plot as a diagnostic checkpoint. If dots remain past the 0.1 line after weighting, the statistical adjustment is incomplete and the outcome analysis should wait.
When selecting between a Kaplan-Meier curve and a cumulative-incidence curve, ask one question: can patients die or permanently switch treatment before experiencing the event of interest? For older patients with diabetes, the answer is yes — use the cumulative-incidence curve.
Assemble the forest plot last, after all pre-specified sensitivity analyses are run. Each row should correspond to an analysis that was declared in the study plan before data were analyzed, not added after seeing the primary result.
Result
The recommended visualization set for this study is: (1) per-arm attrition funnel, (2) study-design timeline anchored at the index prescription fill, (3) Love plot with a 0.1 reference line, (4) cumulative-incidence curve with a competing-death table, and (5) forest plot with subgroup and sensitivity rows. Every chart answers a specific question; no chart is interchangeable with another. The logic is: match the picture to the epistemic job — assumption-declaring, diagnostic, or inferential — and choose within each job based on the clinical context (here, competing death in an older cohort mandates the cumulative-incidence curve over Kaplan-Meier).
Runnable example
python implementation
Highest-yield diagnostic figure: a propensity-score overlap density plus a standardized-mean-difference (Love) plot from an already-fit PS. Required inputs (one row per analytic-cohort member, after ACNU/cohort construction): df : person_id, arm...
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
COVARS = ["age", "female", "ckd", "prior_hf", "insulin", "n_hosp_baseline"] # baseline-window covariates
def std_mean_diff(df, var, weights=None):
t = df["arm"] == 1
x1, x0 = df.loc[t, var], df.loc[~t, var]
if weights is None:
m1, m0 = x1.mean(), x0.mean()
v1, v0 = x1.var(ddof=1), x0.var(ddof=1)
else:
w1, w0 = weights[t], weights[~t]
m1 = np.average(x1, weights=w1); m0 = np.average(x0, weights=w0)
v1 = np.average((x1 - m1) ** 2, weights=w1)
v0 = np.average((x0 - m0) ** 2, weights=w0)
pooled = np.sqrt((v1 + v0) / 2.0)
return 0.0 if pooled == 0 else (m1 - m0) / pooled
def overlap_and_love(df: pd.DataFrame, out_overlap="overlap.png", out_love="love.png"):
# --- Positivity / overlap: PS densities by arm ---
fig, ax = plt.subplots(figsize=(6, 4))
for a, lab in [(1, "Study"), (0, "Comparator")]:
ax.hist(df.loc[df["arm"] == a, "ps"], bins=40, density=True, alpha=0.5, label=lab)
ax.set_xlabel("Estimated propensity score"); ax.set_ylabel("Density")
ax.set_title("PS overlap (inspect the tails for near-positivity)"); ax.legend()
fig.tight_layout(); fig.savefig(out_overlap, dpi=150); plt.close(fig)
# --- Balance: |SMD| before vs after IPTW, with the 0.1 decision line ---
rows = [(v, abs(std_mean_diff(df, v)), abs(std_mean_diff(df, v, df["iptw"]))) for v in COVARS]
bal = pd.DataFrame(rows, columns=["covariate", "before", "after"]).sort_values("before")
y = np.arange(len(bal))
fig, ax = plt.subplots(figsize=(6, 0.45 * len(bal) + 1))
ax.scatter(bal["before"], y, marker="o", label="Unweighted")
ax.scatter(bal["after"], y, marker="x", label="IPTW")
ax.axvline(0.1, ls="--", color="grey") # |SMD| < 0.1 balance threshold
ax.set_yticks(y); ax.set_yticklabels(bal["covariate"])
ax.set_xlabel("|Standardized mean difference|")
ax.set_title("Covariate balance (Love plot)"); ax.legend()
fig.tight_layout(); fig.savefig(out_love, dpi=150); plt.close(fig)
return balpython implementation
Reporting figure under competing risks: a cumulative-incidence (Aalen-Johansen) curve, the correct alternative to 1-Kaplan-Meier when death competes with the event. Required input (one row per analytic-cohort member): df : person_id, arm, time (days from...
import matplotlib.pyplot as plt
from lifelines import AalenJohansenFitter
def plot_cif(df, event_of_interest=1, out="cif.png"):
fig, ax = plt.subplots(figsize=(6, 4))
for a, lab in [(1, "Study"), (0, "Comparator")]:
sub = df[df["arm"] == a]
ajf = AalenJohansenFitter()
ajf.fit(sub["time"], sub["status"], event_of_interest=event_of_interest)
ajf.plot(ax=ax, label=f"{lab} (event {event_of_interest})")
ax.set_xlabel("Days since time zero")
ax.set_ylabel("Cumulative incidence")
ax.set_title("Cumulative incidence with competing death (Aalen-Johansen)")
ax.legend()
fig.tight_layout(); fig.savefig(out, dpi=150); plt.close(fig)