Code
import polars as pl
from reactable import embed_css
from forestly import ForestPlot, TextPanel, SparklinePanel, Config
# Enable reactable CSS
embed_css()
This example demonstrates creating a safety forest plot comparing adverse events between treatment and placebo, matching clinical trial reporting standards.
# Create safety data with three treatment groups
safety_data = pl.DataFrame({
# Adverse event terms
"ae_term": [
"Application site pruritus",
"Pruritus",
"Application site erythema",
"Rash",
"Erythema",
"Dizziness",
"Application site irritation",
"Sinus bradycardia",
"Blister",
"Application site dermatitis"
],
# Placebo (N=86)
"placebo_n": [6, 8, 3, 5, 9, 2, 3, 2, 0, 5],
"placebo_pct": [7.0, 9.3, 3.5, 5.8, 10.5, 2.3, 3.5, 2.3, 0.0, 5.8],
# Low dose treatment (N=84)
"low_dose_n": [22, 23, 12, 13, 15, 8, 9, 7, 5, 9],
"low_dose_pct": [26.2, 27.4, 14.3, 15.5, 17.9, 9.5, 10.7, 8.3, 6.0, 10.7],
# High dose treatment (N=84)
"high_dose_n": [22, 26, 15, 11, 14, 12, 9, 8, 1, 7],
"high_dose_pct": [26.2, 31.0, 17.9, 13.1, 16.7, 14.3, 10.7, 9.5, 1.2, 8.3],
# AE proportions for sparkline (all three groups)
"ae_prop_placebo": [7.0, 9.3, 3.5, 5.8, 10.5, 2.3, 3.5, 2.3, 0.0, 5.8],
"ae_prop_low": [26.2, 27.4, 14.3, 15.5, 17.9, 9.5, 10.7, 8.3, 6.0, 10.7],
"ae_prop_high": [26.2, 31.0, 17.9, 13.1, 16.7, 14.3, 10.7, 9.5, 1.2, 8.3],
# Risk difference vs placebo (Low dose)
"risk_diff_low": [19.2, 18.1, 10.8, 9.7, 7.4, 7.2, 7.2, 6.0, 6.0, 4.9],
"risk_diff_low_lower": [9.1, 7.7, 3.1, 1.5, -2.0, 0.5, -0.1, -0.1, 1.3, -2.7],
"risk_diff_low_upper": [29.3, 28.5, 18.5, 17.9, 16.8, 13.9, 14.5, 12.1, 10.7, 12.5],
# Risk difference vs placebo (High dose)
"risk_diff_high": [19.2, 21.7, 14.3, 7.3, 6.2, 12.0, 7.2, 7.2, 1.2, 2.5],
"risk_diff_high_lower": [9.1, 11.0, 6.2, -0.8, -3.2, 4.3, -0.1, 0.1, -2.1, -4.7],
"risk_diff_high_upper": [29.3, 32.4, 22.4, 15.4, 15.6, 19.7, 14.5, 14.3, 4.5, 9.7],
})
print(f"Safety data shape: {safety_data.shape}")
safety_data.head()
Safety data shape: (10, 16)
ae_term | placebo_n | placebo_pct | low_dose_n | low_dose_pct | high_dose_n | high_dose_pct | ae_prop_placebo | ae_prop_low | ae_prop_high | risk_diff_low | risk_diff_low_lower | risk_diff_low_upper | risk_diff_high | risk_diff_high_lower | risk_diff_high_upper |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
str | i64 | f64 | i64 | f64 | i64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 |
"Application site pruritus" | 6 | 7.0 | 22 | 26.2 | 22 | 26.2 | 7.0 | 26.2 | 26.2 | 19.2 | 9.1 | 29.3 | 19.2 | 9.1 | 29.3 |
"Pruritus" | 8 | 9.3 | 23 | 27.4 | 26 | 31.0 | 9.3 | 27.4 | 31.0 | 18.1 | 7.7 | 28.5 | 21.7 | 11.0 | 32.4 |
"Application site erythema" | 3 | 3.5 | 12 | 14.3 | 15 | 17.9 | 3.5 | 14.3 | 17.9 | 10.8 | 3.1 | 18.5 | 14.3 | 6.2 | 22.4 |
"Rash" | 5 | 5.8 | 13 | 15.5 | 11 | 13.1 | 5.8 | 15.5 | 13.1 | 9.7 | 1.5 | 17.9 | 7.3 | -0.8 | 15.4 |
"Erythema" | 9 | 10.5 | 15 | 17.9 | 14 | 16.7 | 10.5 | 17.9 | 16.7 | 7.4 | -2.0 | 16.8 | 6.2 | -3.2 | 15.6 |
# Define colors for the plot
colors = ["#8B4513", "#4682B4", "#2E7D32"] # Brown for placebo, blue for low dose, green for high dose
# Create safety forest plot with three treatment arms
safety_forest = ForestPlot(
data=safety_data,
panels=[
# Adverse Events column
TextPanel(
variables="ae_term",
title="Adverse Events",
align="left" # Left align for text column
),
# AE Proportion panel showing all three groups
SparklinePanel(
variables=["ae_prop_placebo", "ae_prop_low", "ae_prop_high"],
title="AE Proportion (%)",
labels=["Placebo", "Low Dose", "High Dose"],
reference_line=0,
width=400,
show_legend=True
),
# Risk Difference panel with confidence intervals for both doses vs Placebo
SparklinePanel(
variables=["risk_diff_low", "risk_diff_high"],
lower=["risk_diff_low_lower", "risk_diff_high_lower"],
upper=["risk_diff_low_upper", "risk_diff_high_upper"],
labels=["Low Dose", "High Dose"],
title="Risk Difference (%) + 95% CI\nvs. Placebo",
reference_line=0,
width=300,
x_label="Placebo <-- Favor --> Treatment",
),
# Placebo counts and percentages
TextPanel(
variables=["placebo_n", "placebo_pct"],
title="Placebo\n(N=86)",
labels=["n", "(%)"],
),
# Low dose counts and percentages
TextPanel(
variables=["low_dose_n", "low_dose_pct"],
title="Low Dose\n(N=84)",
labels=["n", "(%)"],
),
# High dose counts and percentages
TextPanel(
variables=["high_dose_n", "high_dose_pct"],
title="High Dose\n(N=84)",
)
],
config=Config(
title="Treatment-Emergent Adverse Events",
colors=colors
)
)
The safety forest plot reveals:
---
title: "Example: Adverse Events"
format:
html:
code-fold: true
code-tools: true
---
## Introduction
This example demonstrates creating a safety forest plot comparing adverse events between treatment and placebo, matching clinical trial reporting standards.
## Setup
```{python}
#| label: setup
#| message: false
import polars as pl
from reactable import embed_css
from forestly import ForestPlot, TextPanel, SparklinePanel, Config
# Enable reactable CSS
embed_css()
```
## Create Safety Data
```{python}
#| label: create-safety-data
# Create safety data with three treatment groups
safety_data = pl.DataFrame({
# Adverse event terms
"ae_term": [
"Application site pruritus",
"Pruritus",
"Application site erythema",
"Rash",
"Erythema",
"Dizziness",
"Application site irritation",
"Sinus bradycardia",
"Blister",
"Application site dermatitis"
],
# Placebo (N=86)
"placebo_n": [6, 8, 3, 5, 9, 2, 3, 2, 0, 5],
"placebo_pct": [7.0, 9.3, 3.5, 5.8, 10.5, 2.3, 3.5, 2.3, 0.0, 5.8],
# Low dose treatment (N=84)
"low_dose_n": [22, 23, 12, 13, 15, 8, 9, 7, 5, 9],
"low_dose_pct": [26.2, 27.4, 14.3, 15.5, 17.9, 9.5, 10.7, 8.3, 6.0, 10.7],
# High dose treatment (N=84)
"high_dose_n": [22, 26, 15, 11, 14, 12, 9, 8, 1, 7],
"high_dose_pct": [26.2, 31.0, 17.9, 13.1, 16.7, 14.3, 10.7, 9.5, 1.2, 8.3],
# AE proportions for sparkline (all three groups)
"ae_prop_placebo": [7.0, 9.3, 3.5, 5.8, 10.5, 2.3, 3.5, 2.3, 0.0, 5.8],
"ae_prop_low": [26.2, 27.4, 14.3, 15.5, 17.9, 9.5, 10.7, 8.3, 6.0, 10.7],
"ae_prop_high": [26.2, 31.0, 17.9, 13.1, 16.7, 14.3, 10.7, 9.5, 1.2, 8.3],
# Risk difference vs placebo (Low dose)
"risk_diff_low": [19.2, 18.1, 10.8, 9.7, 7.4, 7.2, 7.2, 6.0, 6.0, 4.9],
"risk_diff_low_lower": [9.1, 7.7, 3.1, 1.5, -2.0, 0.5, -0.1, -0.1, 1.3, -2.7],
"risk_diff_low_upper": [29.3, 28.5, 18.5, 17.9, 16.8, 13.9, 14.5, 12.1, 10.7, 12.5],
# Risk difference vs placebo (High dose)
"risk_diff_high": [19.2, 21.7, 14.3, 7.3, 6.2, 12.0, 7.2, 7.2, 1.2, 2.5],
"risk_diff_high_lower": [9.1, 11.0, 6.2, -0.8, -3.2, 4.3, -0.1, 0.1, -2.1, -4.7],
"risk_diff_high_upper": [29.3, 32.4, 22.4, 15.4, 15.6, 19.7, 14.5, 14.3, 4.5, 9.7],
})
print(f"Safety data shape: {safety_data.shape}")
safety_data.head()
```
## Create Safety Forest Plot
```{python}
#| label: create-safety-forest
# Define colors for the plot
colors = ["#8B4513", "#4682B4", "#2E7D32"] # Brown for placebo, blue for low dose, green for high dose
# Create safety forest plot with three treatment arms
safety_forest = ForestPlot(
data=safety_data,
panels=[
# Adverse Events column
TextPanel(
variables="ae_term",
title="Adverse Events",
align="left" # Left align for text column
),
# AE Proportion panel showing all three groups
SparklinePanel(
variables=["ae_prop_placebo", "ae_prop_low", "ae_prop_high"],
title="AE Proportion (%)",
labels=["Placebo", "Low Dose", "High Dose"],
reference_line=0,
width=400,
show_legend=True
),
# Risk Difference panel with confidence intervals for both doses vs Placebo
SparklinePanel(
variables=["risk_diff_low", "risk_diff_high"],
lower=["risk_diff_low_lower", "risk_diff_high_lower"],
upper=["risk_diff_low_upper", "risk_diff_high_upper"],
labels=["Low Dose", "High Dose"],
title="Risk Difference (%) + 95% CI\nvs. Placebo",
reference_line=0,
width=300,
x_label="Placebo <-- Favor --> Treatment",
),
# Placebo counts and percentages
TextPanel(
variables=["placebo_n", "placebo_pct"],
title="Placebo\n(N=86)",
labels=["n", "(%)"],
),
# Low dose counts and percentages
TextPanel(
variables=["low_dose_n", "low_dose_pct"],
title="Low Dose\n(N=84)",
labels=["n", "(%)"],
),
# High dose counts and percentages
TextPanel(
variables=["high_dose_n", "high_dose_pct"],
title="High Dose\n(N=84)",
)
],
config=Config(
title="Treatment-Emergent Adverse Events",
colors=colors
)
)
```
## Display Interactive Safety Forest Plot
```{python}
#| label: display-safety-forest
# Generate and display the interactive Reactable
safety_forest.to_reactable()
```
## Key Findings
The safety forest plot reveals:
1. **Application Site Reactions**: Most common AEs are application-site related (pruritus, erythema, irritation)
2. **Risk Differences**: All AEs show higher incidence in treatment group vs placebo
3. **Statistical Significance**: Events where CI doesn't cross 0 indicate significant differences
4. **Safety Profile**: Largest risk differences seen in pruritus and application site reactions
## Export Options
```{python}
#| label: export-options
#| eval: false
# Export to different formats
safety_forest.to_rtf("safety_forest_plot.rtf") # For regulatory submissions
safety_forest.to_dataframe() # Get processed data
```