Example: Survival Subgroup

Introduction

This example demonstrates how to create a basic forest plot using the forestly package for clinical trial subgroup analysis.

Setup

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

# Enable reactable CSS
embed_css()

Create Clinical Trial Data

Code
# Create example efficacy data for subgroup analysis
efficacy_data = pl.DataFrame({
    "subgroup": ["Overall", "Age <65", "Age >=65", "Male", "Female", 
                 "ECOG 0", "ECOG 1", "Prior Therapy", "No Prior Therapy"],
    "category": ["Overall", "Age", "Age", "Sex", "Sex", 
                 "Performance", "Performance", "History", "History"],
    "hazard_ratio": [0.72, 0.68, 0.81, 0.69, 0.75, 0.65, 0.78, 0.73, 0.71],
    "hr_ci_lower": [0.58, 0.51, 0.62, 0.52, 0.57, 0.48, 0.60, 0.55, 0.54],
    "hr_ci_upper": [0.89, 0.91, 1.05, 0.92, 0.98, 0.87, 1.01, 0.96, 0.93],
    "p_value": [0.003, 0.012, 0.089, 0.015, 0.042, 0.004, 0.061, 0.025, 0.011],
    "treatment_events": [45, 24, 21, 23, 22, 18, 27, 30, 15],
    "treatment_total": [150, 78, 72, 75, 75, 60, 90, 100, 50],
    "control_events": [62, 35, 27, 33, 29, 28, 34, 41, 21],
    "control_total": [148, 76, 72, 74, 74, 59, 89, 99, 49],
})

# Display the data
print(f"Data shape: {efficacy_data.shape}")
efficacy_data.head()
Data shape: (9, 10)
shape: (5, 10)
subgroup category hazard_ratio hr_ci_lower hr_ci_upper p_value treatment_events treatment_total control_events control_total
str str f64 f64 f64 f64 i64 i64 i64 i64
"Overall" "Overall" 0.72 0.58 0.89 0.003 45 150 62 148
"Age <65" "Age" 0.68 0.51 0.91 0.012 24 78 35 76
"Age >=65" "Age" 0.81 0.62 1.05 0.089 21 72 27 72
"Male" "Sex" 0.69 0.52 0.92 0.015 23 75 33 74
"Female" "Sex" 0.75 0.57 0.98 0.042 22 75 29 74

Create Forest Plot

Code
# Create the forest plot
forest = ForestPlot(
    data=efficacy_data,
    panels=[
        TextPanel(
            variables="subgroup",
            group_by="category", 
            title="Subgroup",
        ),
        
        # Treatment arm events
        TextPanel(
            variables=["treatment_events", "treatment_total"],
            title="Treatment",
            labels=["n", "N"],
        ),
        
        # Control arm events
        TextPanel(
            variables=["control_events", "control_total"],
            title="Control",
            labels=["n", "N"],
        ),
        
        # Forest plot sparkline
        SparklinePanel(
            variables="hazard_ratio",
            lower="hr_ci_lower",
            upper="hr_ci_upper",
            title="Hazard Ratio (95% CI)",
            reference_line=1.0,
            xlim=(0.4, 1.2),
            width=200,
            show_legend=False
        ),
        
        # P-values
        TextPanel(
            variables="p_value",
            title="P-value",
        )
    ],
)

Display Interactive Forest Plot

Code
# Generate and display the interactive Reactable
forest.to_reactable()

Interpretation

The forest plot shows:

  • Overall Effect: The treatment shows a significant benefit with HR = 0.72 (95% CI: 0.58-0.89, p=0.003)
  • Age Subgroups: Younger patients (<65) show greater benefit than older patients
  • Performance Status: ECOG 0 patients show the strongest treatment effect
  • Consistency: Treatment effect is consistent across most subgroups (all HRs < 1.0)

Export Options

Code
# Export to RTF for regulatory submission
forest.to_rtf("forest_plot_subgroup.rtf")

# Export to static plot
forest.to_plotnine().save("forest_plot_subgroup.png", dpi=300)

# Export processed data
forest.to_dataframe().write_csv("forest_plot_data.csv")