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.
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:
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.
The Anatomy of an R Markdown CSR
A practical R Markdown CSR can be organized into several layers.
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.
Objective Response
Among treated patients with evaluable tumor assessments, the objective response rate was 42.6%.
| Response | n | % |
|---|---|---|
| Complete Response | 8 | 2.0 |
| Partial Response | 164 | 40.6 |
| Stable Disease | 151 | 37.4 |
| Progressive Disease | 81 | 20.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:
| Field | Purpose |
|---|---|
title | Report title displayed by the rendering system |
author | Document author or reporting function |
date | Report generation or document date |
output | Target format such as Word, PDF, or HTML |
params | Inputs used to parameterize the report |
bibliography | Reference 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.
| Format | Typical Use | Important Consideration |
|---|---|---|
| Word | Collaborative document review and editing | Template fidelity, styles, page breaks, and tracked changes workflow |
| Fixed-layout review or archival presentation | LaTeX dependencies and exact pagination | |
| HTML | Interactive internal review and development | Not necessarily the final controlled document format |
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)
```
| Option | Meaning |
|---|---|
echo=FALSE | Run the code but do not display the source code in the report |
include=FALSE | Run the code without displaying code or results |
message=FALSE | Suppress package or function messages in the rendered report |
warning=FALSE | Suppress warnings in the rendered report |
fig.width | Control figure width |
fig.height | Control figure height |
fig.cap | Provide a figure caption |
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.
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:
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.
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.
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.
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.
| Component | Example |
|---|---|
| Source | Analysis datasets and metadata |
| Code | R scripts, functions, and R Markdown |
| Environment | R version and package versions |
| Configuration | Parameters, templates, and options |
| Output | Rendered Word/PDF/HTML document |
| QC evidence | Checks, 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.
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?
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
| Activity | Traditional Workflow | R Markdown Workflow |
|---|---|---|
| Table calculation | Separate programming output | Can be generated during rendering |
| Figure generation | Separate graphics process | Can be generated from the same analysis objects |
| Narrative values | Often entered manually | Can be inserted dynamically |
| Document assembly | Manual or semi-automated | Rendered from source |
| Reproduction | Requires coordinating outputs | Can rerun the report pipeline |
| Traceability | Depends on external documentation | Can 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:
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:
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.
| Population | Possible Use |
|---|---|
| Randomized | Disposition or randomized population summaries |
| Intent-to-treat | Efficacy analyses where specified |
| Safety | Treatment-emergent safety summaries |
| Per-protocol | Specific sensitivity analyses when prespecified |
| Response-evaluable | Selected 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.
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:
orr_summary.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
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:
- Render with warnings visible during development.
- Investigate unexpected warnings.
- Resolve or document legitimate warnings.
- 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.
| Statistic | Illustrative Display |
|---|---|
| Patient count | 428 |
| Percentage | 51.4% |
| Mean | 58.4 |
| Standard deviation | 12.7 |
| Median | 57.0 |
| Confidence interval | (0.61, 1.11) |
| P-value | 0.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.
Document Metadata
A controlled CSR may require metadata such as:
| Metadata | Example |
|---|---|
| Study ID | ABC-001 |
| Report version | Final 1.0 |
| Data cutoff | 30 June 2026 |
| Analysis population | Safety |
| Software environment | Controlled R environment |
| Generation date | 10 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.
Development Workflow
A practical development cycle is:
- Build and validate analysis objects.
- Create the R Markdown structure.
- Develop tables and figures.
- Add dynamic narrative values.
- Render frequently during development.
- Review warnings and errors.
- Apply the document template.
- Perform programming QC.
- Perform statistical and clinical review.
- Generate the controlled final output.
Production Workflow
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
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:
| Dataset | Example Reporting Use |
|---|---|
| ADSL | Disposition, demographics, baseline characteristics, treatment exposure |
| ADAE | Adverse-event summaries |
| ADLB | Laboratory analyses |
| ADVS | Vital-sign analyses |
| ADRS | Tumor-response analyses |
| ADTTE | Time-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.
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?
| Area | Example QC |
|---|---|
| Inputs | Correct dataset versions and cutoff date |
| Populations | Patient counts and flags |
| Tables | Independent numerical comparison |
| Figures | Visual and data-level comparison |
| Narrative | Dynamic values and interpretation |
| References | Correct citations and numbering |
| Formatting | Headers, footers, page breaks, titles, footnotes |
| Output | No missing figures, truncated tables, or rendering errors |
End-to-End QC Checklist
R Markdown vs. Quarto
R Markdown remains widely used, but modern R reporting workflows may also use Quarto.
| Feature | R Markdown | Quarto |
|---|---|---|
| R code execution | Yes | Yes |
| Markdown-based authoring | Yes | Yes |
| Parameterized reports | Yes | Yes |
| Word/PDF/HTML output | Yes | Yes |
| Existing clinical reporting ecosystem | Extensive | Growing |
| Migration consideration | Existing projects may remain on R Markdown | Useful 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
- Keep analysis and reporting layers distinct.
- Use one source of truth for shared numerical results.
- Parameterize repeated report structures.
- Control software and package versions.
- Keep warnings visible during development.
- Validate inputs before rendering.
- Use reusable formatting functions.
- Avoid manually editing generated numerical results.
- Maintain version control for source files.
- Document the relationship between datasets, code, outputs, and QC.
- Design the report template before scaling up production.
- Keep human clinical interpretation under appropriate review.
Practical CSR Architecture
A mature R Markdown reporting system might look like this:
ADSL, ADAE, ADRS, ADLB, ADTTE, and other study-specific datasets.
Formatting, population filtering, statistical calculations, and report utilities.
Study-specific derivations and statistical analyses.
Validated summaries, tables, figures, and narrative values.
Document structure, narrative, dynamic values, and output assembly.
Controlled Word/PDF/HTML presentation.
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.
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:
That relationship can make clinical reporting more consistent, easier to regenerate, and easier to trace.
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.