Tutorials › Biostatistics › R Markdown for Clinical Study Reports

Clinical Reporting & Reproducibility

R Markdown for Clinical Study Reports

A practical guide to using R Markdown to build reproducible Clinical Study Reports, including report structure, YAML configuration, R code chunks, clinical tables and figures, parameterized reporting, Word and PDF output, references, quality control, traceability, and production workflows.

Intermediate 22 min read

What You'll Learn

  • What R Markdown is and why it is useful for clinical reporting
  • How Markdown, R code, tables, figures, and narrative text work together
  • How to structure an R Markdown CSR and configure YAML output
  • How to create clinical tables, figures, and dynamically generated text
  • How parameterized reports can support repeated study outputs
  • How to approach reproducibility, QC, traceability, and production

Introduction

A Clinical Study Report contains a mixture of narrative text, statistical tables, figures, patient-level listings, metadata, references, and supporting documentation. Traditionally, these components may be produced by several different tools and assembled manually into a document.

That workflow can work, but it creates opportunities for inconsistency. A table may be regenerated after the narrative has been written. A figure may use a different analysis dataset than the corresponding table. A treatment label may be updated in one location but not another.

R Markdown offers a different approach: the narrative, analysis code, tables, figures, and report structure can be maintained in a single reproducible source document.

Key idea: R Markdown is not simply a convenient way to write a document. In a well-designed clinical reporting workflow, it can act as the connection between the analysis code, the derived results, and the reporting layer. The resulting document can be regenerated when the underlying data or analysis changes.

What Is R Markdown?

R Markdown combines three major components:

  • Markdown: lightweight syntax for headings, paragraphs, lists, tables, links, and emphasis.
  • R code: executable code embedded in the document.
  • Output formatting: a rendering system that converts the source into HTML, PDF, Word, or other formats.

A minimal R Markdown document looks like this:

---
title: "Clinical Study Report"
output: word_document
---

# Introduction

This is the introduction to the report.

```{r}
mean(c(10, 20, 30))
```

When the document is rendered, the Markdown becomes formatted narrative text and the R code is executed. The result is incorporated into the final report.

Why R Markdown Is Attractive for Clinical Reporting

The central advantage is the relationship between source and output.

Suppose a study has 428 treated patients. A table reports the number of patients with treatment-emergent adverse events, and the narrative says: "Treatment-emergent adverse events occurred in 312 patients."

If both the table and narrative are generated from the same analysis objects, the risk of disagreement is substantially reduced.

The basic relationship is:

$$ \text{Source data} \rightarrow \text{Analysis dataset} \rightarrow \text{R code} \rightarrow \text{Results} \rightarrow \text{CSR} $$

This does not eliminate the need for review or independent QC. It changes where the information comes from and makes the derivation more transparent.

R Markdown Is Not a Replacement for the SAP

An important distinction is that R Markdown is a reporting and reproducibility technology, not a statistical analysis plan.

The SAP should continue to define the statistical methods, analysis populations, estimands where applicable, handling of missing data, derived variables, multiplicity procedures, and other prespecified analysis decisions.

R Markdown should implement the approved analysis and reporting specification; it should not silently invent statistical methodology.

Good principle: The report-generating code should implement the approved statistical specification. If the programming team discovers that the intended analysis cannot be implemented as written, the issue should be resolved through the appropriate study governance process rather than hidden inside the report.

The Anatomy of an R Markdown CSR

A practical R Markdown CSR can be organized into several layers.

1
Metadata: title, study identifier, reporting date, output format, and parameters.
2
Setup: package loading, global options, helper functions, and configuration.
3
Data: controlled loading of analysis datasets and supporting metadata.
4
Analysis: derivations and statistical calculations required by the report.
5
Reporting objects: tables, figures, summary statistics, and narrative values.
6
CSR narrative: sections, subsections, interpretation, and conclusions.
7
References and appendices: citations, supplementary outputs, and supporting material.

A Clinical Reporting Example

Consider a simplified efficacy section containing a treatment comparison. Instead of typing the number of responders manually, the report can derive it from an analysis dataset.

responders <- sum(adrs$BOR == "PR", na.rm = TRUE)

response_rate <- responders / nrow(adrs) * 100

The narrative can then use the calculated value:

The objective response rate was `r sprintf("%.1f", response_rate)`%.

If the underlying data change and the report is rerun, the value in the narrative changes with the analysis.

Illustrative CSR Output

Objective Response

Among treated patients with evaluable tumor assessments, the objective response rate was 42.6%.

Responsen%
Complete Response82.0
Partial Response16440.6
Stable Disease15137.4
Progressive Disease8120.0

The numbers in this preview are illustrative. In a production report, the table and narrative should be generated from the study-specific analysis objects and independently checked.

The YAML Header

Every R Markdown document normally begins with a YAML header enclosed by three dashes.

---
title: "Clinical Study Report"
author: "Statistical Programming"
date: "`r Sys.Date()`"
output:
  word_document:
    reference_docx: "csr-template.docx"
---

YAML controls document-level settings before the body of the report is processed.

For clinical reporting, useful YAML fields include:

FieldPurpose
titleReport title displayed by the rendering system
authorDocument author or reporting function
dateReport generation or document date
outputTarget format such as Word, PDF, or HTML
paramsInputs used to parameterize the report
bibliographyReference database for citations

Choosing the Output Format

R Markdown can produce several output formats. For CSR work, the practical choice often depends on the organization's document workflow.

FormatTypical UseImportant Consideration
WordCollaborative document review and editingTemplate fidelity, styles, page breaks, and tracked changes workflow
PDFFixed-layout review or archival presentationLaTeX dependencies and exact pagination
HTMLInteractive internal review and developmentNot necessarily the final controlled document format
Practical approach: HTML is often convenient during development because it renders quickly and supports rich diagnostics. Word or PDF may then be used for the controlled deliverable, depending on the organization's validated reporting process.

Word Output and Corporate Templates

A major challenge in clinical reporting is that the desired output usually has a predefined appearance: title pages, headers, footers, fonts, margins, table styles, figure placement, and page numbering.

For Word output, a reference document can provide the formatting framework. For example:

---
title: "Clinical Study Report"
output:
  word_document:
    reference_docx: "csr-reference-template.docx"
---

The important concept is that the R Markdown source should contain the content and generation logic, while the reference document can provide much of the Word-specific styling.

Markdown Headings Become Report Sections

A CSR can be structured with ordinary Markdown headings.

# 9. Efficacy Evaluation

## 9.1 Primary Efficacy Endpoint

### 9.1.1 Analysis Population

The primary efficacy endpoint was analyzed in the intent-to-treat population.

The exact numbering strategy should match the organization's report template. Automatic numbering is generally preferable to manually typing numbers when the document architecture permits it.

R Code Chunks

R code is placed inside fenced code blocks marked with {r}.

```{r}
summary(adsl$AGE)
```

A code chunk can calculate an object without necessarily printing it:

```{r}
n_treated <- nrow(adsl)
```

That object can then be used later:

The treated population consisted of `r n_treated` patients.

This is one of the most powerful features of R Markdown for clinical reporting: values can flow from analysis code directly into narrative text.

Chunk Options

Chunk options control how code and results are handled.

```{r, echo=FALSE, message=FALSE, warning=FALSE}
table(adsl$SEX)
```
OptionMeaning
echo=FALSERun the code but do not display the source code in the report
include=FALSERun the code without displaying code or results
message=FALSESuppress package or function messages in the rendered report
warning=FALSESuppress warnings in the rendered report
fig.widthControl figure width
fig.heightControl figure height
fig.capProvide a figure caption
QC caution: Suppressing warnings can improve the appearance of a final report, but it should not be used to hide programming problems. Warnings should be reviewed during development and QC even when they are not printed in the final CSR.

Global Setup

A common pattern is to place package loading and global options in an initial setup chunk.

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  echo = FALSE,
  message = FALSE,
  warning = FALSE,
  fig.align = "center"
)

library(dplyr)
library(ggplot2)
library(gt)
```

The setup chunk is normally executed before the report body. It can also define reusable functions and study-level configuration.

Do Not Put Everything in One Giant Chunk

A common beginner mistake is to put all data preparation, analysis, table creation, and plotting into one enormous R chunk.

That makes debugging and review difficult.

A better structure separates major tasks:

```{r data}
# Load analysis datasets
```

```{r derivations}
# Create reporting variables
```

```{r table-demographics}
# Create Table 14.1.3
```

```{r figure-response}
# Create Figure 14.2.1
```

The report then becomes easier to inspect because the computational structure is visible even when the code itself is hidden from the final output.

Loading Clinical Analysis Data

In a production environment, analysis data should be loaded from a controlled location rather than manually copied into the R Markdown file.

```{r data}
adsl <- readRDS("data/adsl.rds")
adae <- readRDS("data/adae.rds")
adlb <- readRDS("data/adlb.rds")
adrs <- readRDS("data/adrs.rds")
```

The precise storage format and access mechanism will depend on the study environment. The important principle is that the report should have a documented relationship to the analysis data it uses.

Separate Data Preparation From Reporting

For larger CSRs, it is often better to move substantial derivation logic into reusable R scripts or functions rather than placing all transformations in the R Markdown document.

For example:

source("R/derive_demographics.R")
source("R/derive_adverse_events.R")
source("R/derive_efficacy.R")

The R Markdown document then becomes primarily the reporting layer.

A
R scripts/functions: perform reusable derivations and statistical programming.
B
Analysis objects: contain the validated results needed by the report.
C
R Markdown: assembles narrative, tables, figures, and references.
D
Rendered document: becomes the human-readable reporting artifact.

Generating Clinical Tables

Tables are central to a CSR. R Markdown can embed tables generated by packages such as gt, flextable, or other reporting libraries.

A simple example using a data frame is:

demog_summary <- data.frame(
  Characteristic = c("Age, years", "Male", "Female"),
  Value = c("58.4", "214 (51.4%)", "202 (48.6%)")
)

demog_summary

For more controlled formatting, a table-generation package can be used.

library(gt)

demog_summary |>
  gt() |>
  tab_header(
    title = "Table 14.1.3",
    subtitle = "Demographic Characteristics"
  )

Clinical Table Design

A production clinical table generally needs more than correct numbers. Consider:

  • Title and table number
  • Population definition
  • Treatment-group labels
  • Column headers and units
  • Appropriate decimal precision
  • Missing-value representation
  • Footnotes
  • Abbreviations
  • Page-breaking behavior
  • Traceability to source data and analysis programming

Generating Figures

Figures can be created directly from R code.

```{r response-figure, fig.cap="Figure 14.2.1. Individual patient tumor response"}
ggplot(adrs, aes(x = AVAL, y = PCHG, group = USUBJID)) +
  geom_line() +
  geom_hline(yintercept = 0) +
  labs(
    x = "Time",
    y = "Percent Change from Baseline"
  )
```

The figure is rendered as part of the document rather than copied manually into it.

Figure Captions Matter

A figure caption should provide enough context for the reader to understand what is being displayed.

For example:

Illustrative caption: Figure 14.2.1. Individual patient percent change from baseline in the sum of diameters of target lesions over time. Each line represents an individual patient in the response-evaluable population. The figure is illustrative and does not represent an actual study dataset.

In a production CSR, the exact wording should be controlled by the study specification.

Dynamic Narrative Text

Dynamic inline R expressions are especially useful when a sentence contains a value that already exists in the analysis object.

The safety population included `r n_safety` patients.

More complex text can be generated with sprintf():

The median age was `r sprintf("%.1f", median_age)` years
(range, `r min_age` to `r max_age`).

This reduces the need to manually edit numbers in the narrative after every analysis rerun.

Dynamic Text Should Be Controlled

Dynamic reporting does not mean every sentence should be generated programmatically.

A good balance is:

Static narrative

Use for clinical interpretation, methodological explanations, and standard report language.

Dynamic values

Use for counts, percentages, confidence intervals, medians, ranges, p-values, and other analysis-derived values.

The goal is to automate values that are objectively derived while retaining human control over scientific interpretation.

Confidence Intervals in Narrative

Suppose an analysis object contains a treatment effect and confidence interval:

estimate <- 0.82
lower <- 0.61
upper <- 1.11

sprintf(
  "The hazard ratio was %.2f (95%% CI %.2f, %.2f).",
  estimate, lower, upper
)

The resulting sentence can be inserted into the report.

Formatting is part of reporting correctness. A confidence interval of 0.61 to 1.11 should not accidentally become 0.6 to 1.1 merely because a generic printing function was used. Decimal precision should follow the table and reporting specification.

Parameterized Reports

One of the most useful R Markdown features for clinical reporting is parameterization.

A parameterized report can accept inputs such as:

  • Study identifier
  • Treatment group
  • Analysis population
  • Reporting cutoff date
  • Output language
  • Endpoint
  • Subgroup

For example:

---
title: "Clinical Study Report"
output: word_document
params:
  study: "ABC-001"
  cutoff_date: "2026-06-30"
  population: "Safety"
---

The parameters can then be accessed with:

study_id <- params$study
cutoff <- as.Date(params$cutoff_date)
population <- params$population

Why Parameterization Matters

Imagine that the same report structure is needed for several treatment groups. Without parameterization, the programmer might maintain multiple copies of the same R Markdown document.

With parameterization, one template can be rendered repeatedly with different inputs.

1
Define the report template.
2
Define controlled parameters.
3
Pass parameters to the rendering process.
4
Generate the report.
5
Validate that the parameter values were applied correctly.

Rendering a Parameterized Report

rmarkdown::render(
  "csr.Rmd",
  params = list(
    study = "ABC-001",
    cutoff_date = "2026-06-30",
    population = "Safety"
  ),
  output_file = "CSR_Safety_ABC-001.docx"
)

The exact production command may be incorporated into a controlled script, pipeline, or reporting framework.

Tables and Figures Should Share Analysis Objects

A common source of inconsistency is calculating the same quantity independently for a table, a figure, and a narrative.

For example, if three pieces of the report need the number of patients with treatment-emergent adverse events, calculate it once:

n_teae <- sum(adae$TRTEMFL == "Y", na.rm = TRUE)

Then use the same object throughout the report.

The safety population included `r n_safety` patients.

Treatment-emergent adverse events were reported in
`r n_teae` patients.

The same object can feed a table.

Single source of truth: When multiple report elements depend on the same analysis result, prefer a shared analysis object rather than repeating the derivation independently.

Cross-References

Clinical reports often refer to other tables and figures:

"Results are summarized in Table 14.1.3."

"Individual patient trajectories are shown in Figure 14.2.1."

Where the document system supports it, automated cross-references are preferable to manually typed references because tables and figures can move during document revision.

References and Citations

R Markdown can use citation databases such as BibTeX files.

---
bibliography: references.bib
---

A citation can then be inserted using citation syntax supported by the selected output system.

RECIST 1.1 provides standardized criteria for tumor response assessment [@eisenhauer2009].

The exact citation workflow depends on the output format and document requirements.

Clinical Study Report References

Typical references may include:

  • Protocol and protocol amendments
  • Statistical Analysis Plan
  • RECIST or other disease-specific response criteria
  • Regulatory guidance
  • ICH guidance
  • Published methodological literature
  • Relevant clinical practice or disease guidelines

The reporting system should distinguish between references cited in the narrative and controlled study documents maintained elsewhere in the study documentation system.

Reproducibility

A report is reproducible when another authorized user can understand and execute the documented process sufficiently to recreate the intended output.

For clinical reporting, reproducibility involves more than saving the R Markdown file.

ComponentExample
SourceAnalysis datasets and metadata
CodeR scripts, functions, and R Markdown
EnvironmentR version and package versions
ConfigurationParameters, templates, and options
OutputRendered Word/PDF/HTML document
QC evidenceChecks, review records, and discrepancy resolution

Package Version Management

R packages evolve. A report that renders today may not necessarily render identically after package updates.

Therefore, production workflows should control the software environment appropriately.

Tools such as renv can record package versions for an R project.

renv::init()
renv::snapshot()
renv::restore()

The specific environment-management approach should follow organizational standards and any applicable validation requirements.

Important: Version control is not only about source-code history. In a reproducible reporting environment, the versions of R, packages, templates, helper functions, and relevant dependencies can all affect the generated output.

Project Structure

A larger CSR project can benefit from a predictable directory structure.

clinical-report/
├── csr.Rmd
├── README.md
├── renv.lock
├── data/
│   ├── adsl.rds
│   ├── adae.rds
│   └── adrs.rds
├── R/
│   ├── functions.R
│   ├── derive_safety.R
│   └── derive_efficacy.R
├── tables/
├── figures/
├── templates/
│   └── csr-reference.docx
├── references/
│   └── references.bib
└── output/

The exact structure can vary. The important concept is separation of source code, data, templates, generated outputs, and supporting resources.

Use Version Control

Git or another controlled versioning system can provide a history of changes to:

  • R Markdown source
  • R scripts
  • Helper functions
  • Report specifications
  • Templates
  • Configuration files

A version-control history can be especially useful when a reviewer asks: "What changed between the previous report and this version?"

Git Does Not Replace Formal QC

Version control answers questions about what changed. It does not by itself establish that the changed result is correct.

A robust workflow therefore combines version control with programming QC, review, validation checks, and controlled documentation.

QC of Dynamically Generated Narrative

Dynamic narrative requires particular attention because a sentence can be grammatically correct while still being scientifically wrong.

For example:

The response rate was 61.2%.

The programmer should verify:

  • Which analysis population produced 61.2%?
  • Which numerator was used?
  • Which denominator was used?
  • Was the correct treatment group selected?
  • Was the correct cutoff applied?
  • Was the correct response definition used?
Automation does not equal correctness. R Markdown can guarantee that a sentence is regenerated consistently from an object. It cannot guarantee that the object itself was derived correctly.

Independent QC

For important clinical outputs, independent programming or review may be required.

Possible QC strategies include:

  • Independent derivation of key results
  • Comparison against validated TFL outputs
  • Row-by-row or cell-by-cell comparison for selected tables
  • Independent review of figures
  • Review of dynamic narrative values
  • Structural document checks
  • Review of warnings and errors during rendering

Comparing an R Markdown CSR With Traditional TFL Assembly

ActivityTraditional WorkflowR Markdown Workflow
Table calculationSeparate programming outputCan be generated during rendering
Figure generationSeparate graphics processCan be generated from the same analysis objects
Narrative valuesOften entered manuallyCan be inserted dynamically
Document assemblyManual or semi-automatedRendered from source
ReproductionRequires coordinating outputsCan rerun the report pipeline
TraceabilityDepends on external documentationCan be linked directly to code and objects

This does not mean traditional TFL workflows are obsolete. Many regulated organizations continue to use validated TFL-generation and document-assembly systems. R Markdown can complement or replace selected parts of those workflows when the organization's processes permit it.

R Markdown and TFL Specifications

A strong reporting workflow starts with a specification, not with formatting code.

For each table or figure, define:

  • Output identifier
  • Title
  • Population
  • Analysis dataset
  • Variables
  • Derivations
  • Statistical methods
  • Display conventions
  • Rounding rules
  • Footnotes
  • QC expectations

The R Markdown document should then implement the specification.

Rounding Rules

Rounding deserves special attention in clinical reports.

For example, a proportion might be calculated from:

$$ \frac{37}{83}\times100 = 44.5783\ldots\% $$

The displayed value might be 44.6%, depending on the reporting specification.

pct <- 37 / 83 * 100
sprintf("%.1f%%", pct)

The calculation should generally be performed using the unrounded value and rounded only for presentation.

Do Not Round Intermediate Calculations Unnecessarily

Suppose a confidence interval, percentage, or derived parameter is based on a calculation chain.

Premature rounding can introduce discrepancies between independently calculated results.

A good general principle is:

Calculate at full required precision; round at the reporting boundary.

Missing Values

Clinical datasets contain missing values for many legitimate reasons.

R functions can behave differently when missing values are present.

mean(adsl$AGE, na.rm = TRUE)

But na.rm = TRUE should not be added mechanically. The appropriate handling of missing values is an analysis decision, not merely a programming convenience.

Dates in Clinical Reports

Dates are another area where automated reporting can prevent transcription errors.

cutoff_date <- as.Date("2026-06-30")

format(
  cutoff_date,
  "%d %B %Y"
)

The result can be incorporated into a report:

Data cutoff date: `r format(cutoff_date, "%d %B %Y")`

The date format should follow the organization's reporting standard.

Patient Counts and Denominators

One of the most common clinical-reporting errors is using the wrong denominator.

For example, the denominator for a safety summary may differ from the denominator for an efficacy endpoint.

PopulationPossible Use
RandomizedDisposition or randomized population summaries
Intent-to-treatEfficacy analyses where specified
SafetyTreatment-emergent safety summaries
Per-protocolSpecific sensitivity analyses when prespecified
Response-evaluableSelected tumor-response analyses

R Markdown makes denominator calculations reproducible, but the programmer must still use the denominator defined by the analysis specification.

Subgroup Reports

Parameterized reporting becomes especially useful for subgroup analyses.

params:
  subgroup_variable: "SEX"
  subgroup_level: "F"

The analysis code can then filter to the requested subgroup.

analysis_data <- adsl |>
  filter(SEX == params$subgroup_level)

This can support repeated analyses while reducing duplicated source files.

Be Careful With Parameter Explosion

Parameterization is powerful, but a report with dozens of loosely controlled parameters can become difficult to understand and validate.

Prefer a small set of meaningful, documented parameters.

For example:

params:
  study_id: "ABC-001"
  cutoff_date: "2026-06-30"
  population: "SAF"
  treatment: "Treatment A"

Each parameter should have a clear purpose and should be checked before analysis begins.

Input Validation

A parameterized report should fail early when invalid inputs are supplied.

allowed_populations <- c("ITT", "SAF", "PP")

if (!params$population %in% allowed_populations) {
  stop("Invalid analysis population.")
}

Failing early is preferable to silently producing a report with an unintended population.

Study Cutoff Dates

Clinical reports are often based on a data cutoff date.

A controlled cutoff can be applied before generating report outputs:

analysis_data <- analysis_data |>
  filter(ADT <= params$cutoff_date)

The exact cutoff logic depends on the analysis specification. In some cases, different datasets or endpoints may use different rules.

Never assume that a single date filter is sufficient. Disposition, exposure, adverse events, laboratory data, survival endpoints, and efficacy assessments can have different analysis conventions.

Handling Errors During Rendering

A production report should not be considered successful merely because a document file was created.

A robust process should detect:

  • R errors
  • Warnings requiring investigation
  • Missing input files
  • Empty analysis populations
  • Unexpected factor levels
  • Missing treatment groups
  • Unexpected table dimensions
  • Missing figures
  • Incorrect parameter values

For example:

if (nrow(adsl) == 0) {
  stop("Analysis dataset contains no observations.")
}

Assertions and Automated Checks

Automated checks can catch obvious problems before a report reaches manual review.

stopifnot(
  all(adsl$USUBJID != ""),
  all(!is.na(adsl$TRT01P))
)

More sophisticated checks can verify expected treatment groups, patient counts, ranges, and table structures.

Example: Checking a Key Table

expected_treatments <- c(
  "Placebo",
  "Treatment A",
  "Treatment B"
)

observed_treatments <- unique(adsl$TRT01P)

if (!all(expected_treatments %in% observed_treatments)) {
  stop("One or more expected treatment groups are missing.")
}

Such checks should be tailored to the study rather than copied blindly.

Traceability

Traceability means being able to understand how a reported result relates back to its source.

For example:

1
CSR sentence: "42.6% achieved an objective response."
2
Reporting object: orr_summary.
3
Analysis code: response derivation and denominator logic.
4
Analysis dataset: response and subject-level records.
5
Source data and documented derivation rules.

R Markdown can make this relationship easier to inspect because the reporting document contains executable code and can reference the objects used to create the result.

Traceability Is Not Automatically Created

Embedding code in a document does not automatically establish complete clinical traceability.

A production environment may still need:

  • Dataset metadata
  • Variable-level specifications
  • Derivation documentation
  • Programming specifications
  • Version history
  • QC records
  • Reviewer sign-off
Think of R Markdown as one layer of traceability. It can connect the reporting output to executable analysis code, but complete clinical-programming traceability extends beyond a single document.

Reproducible Figures

A manually edited figure can become disconnected from the code that generated it.

With R Markdown:

```{r figure}
ggplot(efficacy_data, aes(x = TIME, y = PCHG, group = USUBJID)) +
  geom_line()
```

the figure is recreated from the data whenever the report is rendered.

This is particularly valuable when a data cutoff changes or a programming defect is corrected.

Tables Should Be Recreated, Not Manually Edited

If a table is generated by R and then manually modified in Word, the final document may no longer correspond exactly to the source code.

Manual editing may sometimes be unavoidable for document production, but important numerical content should ideally remain under programmatic control.

When Manual Editing Is Appropriate

Not every aspect of a CSR should necessarily be generated by code.

Manual authoring can be appropriate for:

  • Clinical interpretation
  • Medical writing
  • Contextual discussion
  • Explanatory narrative
  • Document-level editorial decisions

The key distinction is between scientific interpretation and derived numerical content.

Common Mistake: Copying Numbers Into Narrative

Consider:

The safety population included 428 patients.

If "428" is typed manually, a later data update can leave the narrative incorrect.

A dynamic version is:

The safety population included `r n_safety` patients.

This is safer because the displayed value is linked to the analysis object.

Common Mistake: Recalculating the Same Statistic Three Ways

Suppose the response rate appears in:

  • A summary table
  • A figure annotation
  • A narrative paragraph

If each location contains independent calculation code, discrepancies become possible.

Instead:

orr <- calculate_orr(adrs)

orr_rate <- orr$rate
orr_ci   <- orr$ci

Then all report elements use orr_rate and orr_ci.

Common Mistake: Hiding All Warnings

It can be tempting to start every report with:

knitr::opts_chunk$set(
  warning = FALSE,
  message = FALSE
)

That may be appropriate for final presentation, but it can conceal important diagnostic information during development.

A better workflow is:

  1. Render with warnings visible during development.
  2. Investigate unexpected warnings.
  3. Resolve or document legitimate warnings.
  4. Suppress non-actionable messages only in the final presentation.

Common Mistake: Using the Report as the Analysis Program

A 500-page CSR should not necessarily contain every piece of analysis logic required to derive every result from raw data.

A better architecture separates:

  • Data preparation
  • Statistical analysis
  • Reporting

The R Markdown document should remain understandable as a reporting artifact.

Common Mistake: Hard-Coding Study-Specific Values

Avoid unnecessary statements such as:

treatment_label <- "Treatment A"
n <- 428
cutoff <- "30 June 2026"

when those values should be obtained from controlled metadata or parameters.

Hard-coded values can be especially risky when a template is reused across studies.

Clinical Tables and Statistical Precision

Different statistics often require different display rules.

StatisticIllustrative Display
Patient count428
Percentage51.4%
Mean58.4
Standard deviation12.7
Median57.0
Confidence interval(0.61, 1.11)
P-value0.034

These are illustrative only. The study's reporting specification should define the actual formatting rules.

Conditional Text

R Markdown can also generate different wording depending on results.

if (p_value < 0.05) {
  "The treatment difference was statistically significant."
} else {
  "The treatment difference was not statistically significant."
}

This capability should be used carefully. Automated language should reflect the statistical specification and should not introduce interpretations that go beyond it.

Conditional Sections

The same principle can be used for optional report sections.

if (params$include_biomarker) {
  # Generate biomarker section
}

This can be useful for report templates shared across related analyses, but conditional logic should remain transparent and tested.

Patient-Level Listings

Listings can also be generated from R Markdown, particularly for exploratory or internal reporting.

However, very large listings can introduce document-size and rendering challenges.

For large regulatory appendices, a dedicated listing-generation process may remain more appropriate depending on the organization's standards.

Large Clinical Study Reports

A very large CSR can contain thousands of pages when appendices and listings are included.

At that scale, consider separating the reporting system into components:

  • Main CSR narrative
  • Statistical tables
  • Figures
  • Patient listings
  • Appendices

The main R Markdown document can reference or assemble controlled outputs rather than performing every task in one rendering operation.

Performance

Rendering a large report repeatedly can be slow.

Useful strategies include:

  • Precomputing expensive analysis objects
  • Using reusable data objects
  • Reducing repeated database or file reads
  • Separating development from production rendering
  • Using caching carefully where appropriate

Caching can improve speed but introduces additional state that must be understood and controlled.

Knitr Caching

```{r expensive-analysis, cache=TRUE}
model <- run_expensive_model(data)
summary(model)
```

If the underlying data change without the cache being invalidated appropriately, the report can contain stale results.

Cache carefully. A faster report is not useful if it renders results from an outdated calculation. Production workflows should have an explicit strategy for cache invalidation or avoid caching where reproducibility could be compromised.

Document Metadata

A controlled CSR may require metadata such as:

MetadataExample
Study IDABC-001
Report versionFinal 1.0
Data cutoff30 June 2026
Analysis populationSafety
Software environmentControlled R environment
Generation date10 September 2026

These values should be generated from controlled metadata where practical.

Example CSR YAML Template

---
title: "Clinical Study Report"
subtitle: "Study ABC-001"
author: "Statistical Programming"
date: "`r format(Sys.Date(), '%d %B %Y')`"

params:
  study_id: "ABC-001"
  cutoff_date: "2026-06-30"
  population: "Safety"

output:
  word_document:
    reference_docx: "templates/csr-reference.docx"
---

Example Setup Chunk

```{r setup, include=FALSE}

knitr::opts_chunk$set(
  echo = FALSE,
  message = FALSE,
  warning = FALSE,
  fig.align = "center",
  out.width = "90%"
)

library(dplyr)
library(ggplot2)
library(gt)

study_id <- params$study_id
cutoff_date <- as.Date(params$cutoff_date)

adsl <- readRDS("data/adsl.rds")
adae <- readRDS("data/adae.rds")

if (nrow(adsl) == 0) {
  stop("ADSL contains no observations.")
}
```

Example Demographics Summary

n_safety <- nrow(adsl)

median_age <- median(
  adsl$AGE,
  na.rm = TRUE
)

age_range <- range(
  adsl$AGE,
  na.rm = TRUE
)

The narrative can then be written as:

The safety population consisted of `r n_safety` patients.
The median age was `r sprintf("%.1f", median_age)` years
(range, `r age_range[1]` to `r age_range[2]`).

Example Adverse-Event Summary

teae_summary <- adae |>
  filter(TRTEMFL == "Y") |>
  distinct(USUBJID) |>
  summarise(
    n = n()
  )

The resulting object can be used in the narrative:

Treatment-emergent adverse events were reported in
`r teae_summary$n` patients.

Example Figure Workflow

response_plot <- ggplot(
  adrs,
  aes(
    x = ADY,
    y = PCHG,
    group = USUBJID
  )
) +
  geom_line(alpha = 0.45) +
  geom_hline(
    yintercept = 0,
    linewidth = 0.6
  ) +
  labs(
    x = "Days from First Dose",
    y = "Percent Change from Baseline"
  ) +
  theme_classic()

response_plot

Keeping the plot in an object can be useful when the same figure needs to be displayed, tested, or exported elsewhere.

Tables, Figures, and Narrative as One System

The strongest R Markdown workflows treat the CSR as an integrated reporting system rather than as a collection of unrelated outputs.

1
Analysis data are loaded from controlled sources.
2
Derivations produce reusable analysis objects.
3
Tables and figures consume those objects.
4
Narrative uses the same objects for dynamic values.
5
The complete document is rendered from source.
6
QC compares the rendered report against independent expectations.

Development Workflow

A practical development cycle is:

  1. Build and validate analysis objects.
  2. Create the R Markdown structure.
  3. Develop tables and figures.
  4. Add dynamic narrative values.
  5. Render frequently during development.
  6. Review warnings and errors.
  7. Apply the document template.
  8. Perform programming QC.
  9. Perform statistical and clinical review.
  10. Generate the controlled final output.

Production Workflow

A
Freeze inputs: identify the approved datasets, metadata, cutoff, and analysis version.
B
Prepare environment: use the controlled R and package environment.
C
Run checks: validate inputs, parameters, populations, and expected structures.
D
Render: generate the complete report.
E
QC: compare key numerical and graphical outputs with independent results.
F
Review: conduct statistical, clinical, and editorial review.
G
Archive: retain the appropriate source, environment, output, and QC evidence according to procedure.

Regulated-Environment Considerations

Using R Markdown in a regulated clinical environment requires more than learning the syntax.

Organizations may need procedures addressing:

  • Software qualification or validation
  • Change control
  • Access control
  • Version management
  • Audit trails
  • Electronic records and signatures where applicable
  • Source-data and analysis-data controls
  • Programming QC
  • Document approval
Important distinction: R Markdown itself is a technology. Whether and how it can be used for a specific regulated deliverable depends on the organization's validated processes, systems, procedures, and applicable requirements.

R Markdown and ICH E3

The Clinical Study Report structure should remain aligned with the applicable CSR expectations and the study's reporting plan.

R Markdown can help generate sections of a report, but it does not determine what those sections should contain.

For example, the reporting framework may include sections covering:

  • Study objectives
  • Investigational plan
  • Study patients
  • Efficacy evaluation
  • Safety evaluation
  • Discussion and conclusions
  • Tables and figures
  • References
  • Appendices

The report template should therefore be designed around the required study structure rather than around the capabilities of the software.

R Markdown and CDISC Analysis Data

R Markdown can consume ADaM datasets such as:

DatasetExample Reporting Use
ADSLDisposition, demographics, baseline characteristics, treatment exposure
ADAEAdverse-event summaries
ADLBLaboratory analyses
ADVSVital-sign analyses
ADRSTumor-response analyses
ADTTETime-to-event endpoints

The precise dataset names and structures depend on the study and standards implementation.

ADSL as a Common Anchor

In many analysis workflows, the subject-level analysis dataset is used as a common population anchor.

For example:

safety_ids <- adsl |>
  filter(SAFFL == "Y") |>
  select(USUBJID)

Other analysis datasets can then be evaluated against that population when appropriate.

Do Not Assume One Dataset Defines Every Population

A common programming error is to use the subject-level population for every endpoint without considering endpoint-specific analysis rules.

The report code should explicitly implement the population definition specified for each analysis.

Clinical Reporting With dplyr

The dplyr package is useful for preparing reporting objects.

ae_summary <- adae |>
  filter(TRTEMFL == "Y") |>
  group_by(TRT01P) |>
  summarise(
    n_patients = n_distinct(USUBJID),
    .groups = "drop"
  )

This object can then feed a table or narrative.

Clinical Reporting With ggplot2

The same principle applies to figures.

ggplot(
  response_data,
  aes(
    x = ADY,
    y = PCHG,
    group = USUBJID
  )
) +
  geom_line(alpha = 0.5) +
  facet_wrap(~TRT01P)

The advantage is that the figure is linked directly to the reporting data.

Clinical Reporting With gt

The gt package provides a convenient framework for formatting tables.

table1 <- summary_data |>
  gt() |>
  tab_header(
    title = "Table 14.1.1",
    subtitle = "Demographic Characteristics"
  )

Other packages may be preferable depending on whether the final output is HTML, Word, PDF, or a specialized production format.

Document-Level CSS

HTML output can be styled with CSS.

---
output:
  html_document:
    css: "styles.css"
---

This can be useful for internal review dashboards and web-based reporting. For Word or PDF CSRs, document templates and output-specific formatting mechanisms are usually more important.

Page Breaks and Section Control

Long clinical reports require careful control of pagination.

Page breaks may be implemented differently depending on the output format. For example, Word-specific content may use format-specific methods or template styles.

Do not assume that an R Markdown document has identical pagination in Word, PDF, and HTML. Each output format has a different rendering engine and document model.

Tables That Span Multiple Pages

Clinical tables can be long enough to span several pages.

The production table system should support:

  • Repeating headers
  • Controlled row heights
  • Footnotes
  • Continuation labels
  • Page breaks
  • Landscape orientation when necessary

These requirements often determine which table-generation package and output format are most appropriate.

Landscape Tables

Some tables are too wide for portrait pages.

A clinical reporting framework may therefore need section-level orientation changes or a specialized Word/PDF template.

The important point is to design the document architecture before generating hundreds of pages of output.

Report Generation Functions

Reusable functions can simplify the reporting layer.

make_percent <- function(x, digits = 1) {
  sprintf(
    paste0("%.", digits, "f%%"),
    x
  )
}

Then:

make_percent(42.638)

returns a consistently formatted value.

Centralize Formatting Rules

Instead of scattering formatting rules throughout the report, define them in reusable functions where practical.

fmt_n <- function(x) {
  format(x, big.mark = ",", scientific = FALSE)
}

fmt_pct <- function(x) {
  sprintf("%.1f%%", x)
}

This makes changes easier and reduces inconsistent formatting.

Validation of Formatting Functions

Formatting functions should themselves be tested.

stopifnot(fmt_pct(42.638) == "42.6%")
stopifnot(fmt_n(12345) == "12,345")

Small tests can prevent formatting changes from propagating unexpectedly through a large report.

Reproducible Session Information

It can be useful to record the software environment used to generate a report.

sessionInfo()

A production report may not need the complete session information in the reader-facing CSR, but it can be retained as part of the supporting documentation where appropriate.

Report Logs

A production rendering process should ideally generate logs that allow the team to determine:

  • When the report was generated
  • Which parameters were used
  • Whether errors occurred
  • Whether warnings occurred
  • Which software environment was used

The exact logging approach depends on the reporting platform.

Automated Rendering

Once the report is stable, rendering can be automated with a script.

rmarkdown::render(
  input = "csr.Rmd",
  output_format = "word_document",
  output_file = "CSR_ABC-001.docx"
)

A wrapper script can perform input checks before calling the renderer.

A Safer Production Wrapper

required_files <- c(
  "data/adsl.rds",
  "data/adae.rds",
  "data/adrs.rds"
)

missing_files <- required_files[
  !file.exists(required_files)
]

if (length(missing_files) > 0) {
  stop(
    paste(
      "Missing required files:",
      paste(missing_files, collapse = ", ")
    )
  )
}

rmarkdown::render(
  "csr.Rmd",
  output_file = "CSR_ABC-001.docx"
)

Testing Before Rendering the Full CSR

A 600-page report can take substantially longer to render than a small test document.

During development, it can be useful to:

  • Test individual analysis objects.
  • Render selected sections.
  • Use a small development dataset where appropriate.
  • Validate tables before full document generation.
  • Validate figures separately.

The final production render should still use the controlled production inputs.

What Should Be QC'd?

AreaExample QC
InputsCorrect dataset versions and cutoff date
PopulationsPatient counts and flags
TablesIndependent numerical comparison
FiguresVisual and data-level comparison
NarrativeDynamic values and interpretation
ReferencesCorrect citations and numbering
FormattingHeaders, footers, page breaks, titles, footnotes
OutputNo missing figures, truncated tables, or rendering errors

End-to-End QC Checklist

1
Confirm the analysis dataset versions.
2
Confirm the data cutoff date.
3
Confirm the analysis populations.
4
Confirm parameter values.
5
Run automated input and structural checks.
6
Render the report with the controlled environment.
7
Review rendering logs and warnings.
8
Independently verify key tables and figures.
9
Review dynamic narrative values.
10
Perform clinical and statistical review.
11
Check document formatting and pagination.
12
Archive the appropriate source, output, environment, and QC evidence.

R Markdown vs. Quarto

R Markdown remains widely used, but modern R reporting workflows may also use Quarto.

FeatureR MarkdownQuarto
R code executionYesYes
Markdown-based authoringYesYes
Parameterized reportsYesYes
Word/PDF/HTML outputYesYes
Existing clinical reporting ecosystemExtensiveGrowing
Migration considerationExisting projects may remain on R MarkdownUseful for new projects depending on organizational standards

For an existing clinical programming organization, the best technology is not necessarily the newest one. The decision should consider validated workflows, team expertise, templates, dependencies, and maintenance requirements.

When R Markdown Is a Good Fit

  • Reproducible internal clinical reporting
  • Automated statistical summaries
  • Interactive analysis review
  • Study-specific reports generated from common templates
  • Reports where narrative values should remain synchronized with analysis
  • Research and development workflows

When a Different Tool May Be Better

  • Very large listing-heavy regulatory submissions
  • Organizations with established validated TFL production systems
  • Documents requiring highly specialized pagination
  • Workflows dominated by manual medical-writing collaboration
  • Systems where R is not an approved or supported production technology

The choice should be made based on the reporting requirements rather than on software preference alone.

Best Practices

  1. Keep analysis and reporting layers distinct.
  2. Use one source of truth for shared numerical results.
  3. Parameterize repeated report structures.
  4. Control software and package versions.
  5. Keep warnings visible during development.
  6. Validate inputs before rendering.
  7. Use reusable formatting functions.
  8. Avoid manually editing generated numerical results.
  9. Maintain version control for source files.
  10. Document the relationship between datasets, code, outputs, and QC.
  11. Design the report template before scaling up production.
  12. Keep human clinical interpretation under appropriate review.

Practical CSR Architecture

A mature R Markdown reporting system might look like this:

1
Controlled analysis datasets
ADSL, ADAE, ADRS, ADLB, ADTTE, and other study-specific datasets.
2
Reusable R functions
Formatting, population filtering, statistical calculations, and report utilities.
3
Analysis programs
Study-specific derivations and statistical analyses.
4
Reporting objects
Validated summaries, tables, figures, and narrative values.
5
R Markdown CSR
Document structure, narrative, dynamic values, and output assembly.
6
Template
Controlled Word/PDF/HTML presentation.
7
QC and review
Independent verification, statistical review, clinical review, and document review.

A Minimal but Useful CSR Skeleton

---
title: "Clinical Study Report"
output:
  word_document:
    reference_docx: "templates/csr.docx"

params:
  study_id: "ABC-001"
  cutoff_date: "2026-06-30"
---

```{r setup, include=FALSE}
library(dplyr)
library(ggplot2)

adsl <- readRDS("data/adsl.rds")
adae <- readRDS("data/adae.rds")
adrs <- readRDS("data/adrs.rds")

n_safety <- sum(adsl$SAFFL == "Y", na.rm = TRUE)
```

# 1. Introduction

This Clinical Study Report summarizes Study `r params$study_id`.

# 2. Study Patients

The safety population included `r n_safety` patients.

# 3. Safety Evaluation

## 3.1 Adverse Events

```{r}
# Generate safety summary table
```

# 4. Efficacy Evaluation

## 4.1 Primary Endpoint

```{r}
# Generate primary efficacy analysis
```

## 4.2 Figures

```{r}
# Generate efficacy figure
```

# 5. Conclusions

The study results are interpreted in the context of the prespecified
statistical analysis plan and clinical study objectives.

From Prototype to Production

The path from a small R Markdown report to a production clinical reporting system usually involves several stages.

1
Prototype: prove that the analysis and report can be generated.
2
Standardize: create reusable functions and templates.
3
Parameterize: remove unnecessary duplication.
4
Validate: establish appropriate checks and QC.
5
Control: manage versions, environments, and templates.
6
Operationalize: integrate the process into the organization's reporting workflow.

The Most Important Concept

The biggest benefit of R Markdown for clinical reporting is not the ability to write a report using Markdown syntax.

It is the ability to connect analysis, results, and narrative within a reproducible workflow.

A manually written sentence can become stale. A manually pasted table can become disconnected from its source. A manually inserted figure can be difficult to reproduce.

A well-designed R Markdown report instead creates a direct relationship:

$$ \text{Analysis} \rightarrow \text{Reporting Objects} \rightarrow \text{Tables + Figures + Narrative} \rightarrow \text{CSR} $$

That relationship can make clinical reporting more consistent, easier to regenerate, and easier to trace.

Bottom line: R Markdown can provide a powerful reproducible framework for Clinical Study Reports by bringing executable R code, narrative text, tables, figures, and document generation into one source workflow. The strongest implementations separate analysis from presentation, use shared analysis objects for numerical results, parameterize repeated reports, control software environments, validate inputs, and apply independent QC. R Markdown does not replace the SAP, statistical programming specifications, clinical review, or regulated processes; it provides a technology layer that can connect those activities to the final reporting document.

References

Allaire, J.J., Xie, Y., McPherson, J., et al. rmarkdown: Dynamic Documents for R. R package documentation and project resources.
Xie, Y. (2015). Dynamic Documents with R and knitr. Chapman and Hall/CRC.
Xie, Y., Allaire, J.J., and Grolemund, G. (2018). R Markdown: The Definitive Guide. Chapman and Hall/CRC.
International Council for Harmonisation. ICH E3: Structure and Content of Clinical Study Reports.
International Council for Harmonisation. ICH E6: Good Clinical Practice.
R Core Team. R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing.
Posit. R Markdown documentation.