Example: Adverse Events

Introduction

This example demonstrates creating a safety forest plot comparing adverse events between treatment and placebo, matching clinical trial reporting standards.

Setup

Code
import polars as pl
from reactable import embed_css
from forestly import ForestPlot, TextPanel, SparklinePanel, Config

# Enable reactable CSS
embed_css()

Create Safety Data

Code
# 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)
shape: (5, 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

Create Safety Forest Plot

Code
# 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

Code
# 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

Code
# Export to different formats
safety_forest.to_rtf("safety_forest_plot.rtf")  # For regulatory submissions
safety_forest.to_dataframe()  # Get processed data