Methodological Replication of Power Simulations for the NCT04435626 Primary Endpoint (Heart Failure)

mathematical statistics
sap
survival analysis
trial design
sas
Outline the process for replicating power simulation results by applying the prespecified primary analysis methodology established in the NCT04435626 clinical protocol for finerenone in heart failure patients.
Author

Yupng L.

Published

January 25, 2026

Primary Objectives

  • To replicate the power simulation results for the primary endpoint using the prespecified primary analysis methodology described in NCT04435626 [Protocol title: A Multicenter, Randomized, Double-blind, Parallel-group, Placebo-controlled Study to Evaluate the Efficacy and Safety of Finerenone on Morbidity and Mortality in Participants With Heart Failure (NYHA II-IV) and Left Ventricular Ejection Fraction ≥ 40% (LVEF ≥ 40%)].

    • To validate power (target: >=90%) for detecting treatment effect on total composite events (not time to first) at two-sided \(\alpha = \ 0.05\). The key components (for this simulation study) for the attributes of the primary estimand to address the primary objective:

      • Variable: Number of unfavorable events including CV death and total (first and recurrent) heart failure events.

      • Population-level summary: Ratio of exposure-weighted composite event rates between finerenone and placebo. Exposure-weighted refers to patients being weighted according to their follow-up time in determining the rate.

  • Confirm expected event counts (with approximately 5500 randomized participants, it is expected to observe approximately 1310 first events and approximately 2375 total events)

  • Verify the observed 19% rate reduction (rate ratio = 0.81) using Andersen-Gill model.

Study Design Parameters

  • Total study duration: 42 months

  • Recruitment period: 24 months

  • Randomization ratio: 1:1 (finerenone:placebo)

  • Sample size: 5,500 participants (2,750 per arm)

  • Follow-up period: Up to 42 months

Event Rate Parameters (Placebo Arm)

  • CV death rate: 0.005125 events/month/participant (annualized: 6.15%)

  • HF event rate: 0.0182 events/month/participant (annualized: 21.84%)

  • Non-CV death rate: 0.0016 events/month/participant (annualized: 1.92%)

  • Expected first composite event rate: 12.5 per 100 patient-years

  • Expected CV death event rate: 4.6 per 100 patient-years

  • Expected CV death proportion: ~70% of all deaths (Non-CV deaths are simulated [by Bayer] as exponentially distributed censoring events with a rate of 0.0016 non-CV deaths/month per participant, leading to approximately 70% of all deaths being due to CV causes)

Recurrent Event Model

  • Distribution: Gamma frailty model (see sections below for mathematical formula)

  • Frailty variance: 5.0 (see sections below for random number generation)

  • Expected total-to-first event ratio: 1.8

    • Total-to-first event ratio = Total number of events / Number of first events.

Treatment Effects (Finerenone Arm)

  • CV death hazard ratio: 0.80

  • HF event rate ratio: 0.75

  • Primary composite rate ratio: 0.81 (19% reduction)

Dropout/Discontinuation

  • Annual discontinuation rate: 5% (both arms)

  • Post-discontinuation event rates: Finerenone participants revert to placebo rates; placebo participants unchanged

  • Observation status: Participants remain in follow-up after discontinuation

Random Number Generation

  • Set reproducible seeds for each simulation run

  • Use separate random number streams for:

    • Enrollment times

    • Treatment assignment

    • Event generation (CV death, HF events, non-CV death)

    • Frailty parameters

    • Discontinuation times

Participant-Level Data Generation Plan

Step 1: Enrollment

  • Generate enrollment times uniformly over 24 months

Step 2: Randomization

  • Assign treatment (1:1) using simple randomization (Note: if we have the assumptions of prevalences of each stratification factor, we can simulate a dummy randomization list using block randomization methodology. At the stage of design, we can ignore such stratification variables).

Step 3: Generate Individual Frailty

  • Draw frailty parameter from Gamma distribution based on the two steps as follows:

    • Draw random number Y from \(Y \sim Gamma(a = 0.2,\lambda = 1),\) s.t. \(Var\lbrack Y\rbrack = 0.2 \times 1^{2} = 0.2\).

    • Transform Frailty by: \(Frailty = Y/0.2,\) s.t. \(Var\left\lbrack \frac{Y}{0.2} \right\rbrack = \frac{0.2}{{0.2}^{2}} = 5\)

  • Individual frailty multiplies baseline event rates.

    • Frailty represents unobserved heterogeneity in individual risk. Even after accounting for measured covariates (age, treatment, etc.), participants differ in their underlying susceptibility to events.

    • Conditional hazard for each individual participant \(j\) with covariates \(\mathbf{X}_{j}\) and frailty \(\alpha_{j}\) can be defined as: \(h_{j}\left( t\mid\alpha_{j} \right) = h_{0}(t) \cdot \alpha_{j} \cdot exp(\beta^{t}\mathbf{X}_{j})\)

    • Under exponential distribution, baseline hazard for CV death (for an example) can be generated by \(TTE_{CVD} \sim Exp(\lambda = 0.005125)\) without frailty. With individual frailty, it will be updated to \(TTE_{CVD} \sim Exp(Frailty \times \lambda)\).

      • Participant with \(Frailty = 0.5\) (low): Expected survival = 1/(0.005125 × 0.5) = 390 months.

      • Participant with \(Frailty = 2.0\) (high): Expected survival = 1/(0.005125 × 2.0) = 98 months

Step 4: Generate Event Times

For Placebo Arm:

  • CV deaths: Exponential with individual rate = 0.005125 × frailty

  • HF events: Recurrent events from gamma frailty process

    • Rate: λ_HF = 0.0182 × frailty

    • Generate multiple events per participant until censoring

    • Use renewal process (or another more complex method as an option: non-homogeneous Poisson process) with frailty. Bayer used a non-homogeneous Poisson process in their work per the study protocol, which assumes a constant event rate across time. See Appendix 1 for further information.

  • Non-CV deaths: Exponential with rate = 0.0016 (independent of frailty)

For Finerenone Arm:

  • CV deaths: Exponential with individual rate = 0.005125 × 0.80 × frailty

  • HF events: Gamma frailty process with rate = 0.0182 × 0.75 × frailty

  • Non-CV deaths: Same as placebo

  • Overall composite rate ratio: Approximately 0.81 (19% reduction)

Step 5: Generate Discontinuation Times

  • Generate discontinuation time: Exponential with 5% annual rate

  • Post-discontinuation for finerenone: Revert to placebo rates for future events

  • Post-discontinuation for placebo: Rates unchanged

  • Continue follow-up and event recording after discontinuation

Step 6: Apply Censoring

  • Terminal events: CV death, non-CV death

  • Administrative censoring: 42 months - enrollment time

  • After terminal event, no further HF events recorded

  • Record exact timing of all events and censoring

More notes for the idea of “Until Censoring” in this simulation study regarding the types of censoring:

  1. Death (terminal event)

    • CV death or non-CV death

    • No more HF events possible after death

  2. Administrative censoring

    • Study ends at 42 months (i.e. 3.5 years)

    • Participant enrolled at month 10 → max follow-up = 32 months

  3. Treatment discontinuation (not true censoring but maybe an intercurrent event)

    • Participant continues to be observed

    • But event rate changes (Per Bayer: with finerenone participants having the same risk of events as placebo participants after discontinuation and no change in event rate for discontinuing placebo participants)

E.g. censor_time = min(cv_death_time, noncv_death_time, max_followup);

Example:

  • Participant enrolled at month 5
  • Max follow-up = 42 - 5 = 37 months
  • During simulation: CV death at month 25
  • During simulation: Non-CV death at month 40 (doesn’t happen first)
    • Censoring time = 25 months (earliest event which is the CV death time)

    • Generate HF events only from 0 to 25 months

Primary Analysis Methodology

Andersen-Gill Model Specification:

\[ h_{ij}(t)\ = \ h_{0}(t)\ \times \ exp(\beta\ \times \ Treatment_{i}) \]

Where:

  • \(h_{ij}(t)\) = hazard for j-th event in participant i

  • \(h_{0}(t)\) = baseline hazard (not consider stratification)

  • \(\beta\) = log(rate ratio) for treatment effect

  • \(Treatment_{i}\) = indicator for finerenone (1) vs placebo (0)

Robust standard errors (sandwich estimator) will be used to account for correlations of event times within a participant. As shown by Lin et al. 2000, the Andersen Gill model with robust standard errors can be interpreted as a proportional rates model. After the authors of the paper, the model is also referred to as Lin, Wei, Yang and Ying (LWYY) model.

Search “LWYY model” on the internet for more details (Lin et al., 2000).

Summary Table of Key Methodologies Used in This Simulation Study

Category Methods
Survival Analysis Exponential distribution, Competing risks, Censoring
Recurrent Events Gamma frailty, Renewal process, Andersen-Gill model
Inference Robust sandwich estimator
Treatment Effects Hazard ratio, Rate ratio
Other Advanced Topics Counting processes, Martingales, Estimating equations

Appendix 1. Generating Multiple HF Events Per Participant

Unlike CV death (which happens at most once), HF hospitalizations can recur multiple times for the same participant. We need a method to generate:

  • Event 1 at time \(t₁\)

  • Event 2 at time \(t₂\)

  • Event 3 at time \(t₃\)

  • … until censoring

With RENEWAL PROCESS:

  • Each inter-event time is independently drawn from the same distribution

  • The process “renews” after each event

  • Time between events doesn’t depend on when previous events occurred.

Example: Participant \(j\) with \(\lambda_{j} = \ 0.03\) events/month, censoring at 30 months (using exponential distribution to simulate the time-to-event):

  1. Inter-event time 1: 5.2 months → Event 1 at time 5.2

  2. Inter-event time 2: 8.7 months → Event 2 at time 13.9 (5.2 + 8.7)

  3. Inter-event time 3: 12.1 months → Event 3 at time 26.0 (13.9 + 12.1)

  4. Inter-event time 4: 18.3 months → Would be at time 44.3 → CENSORED (exceeds 30)

Result: 3 HF events at times 5.2, 13.9, and 26.0 months.

With NON-HOMOGENEOUS PROCESS:

A Poisson process where the event rate can change over time: \(\lambda(t)\), e.g., after treatment discontinuation.

For example, treatment discontinuation due to lack of efficacy may create a non-homogeneous process:

  1. Before dropout (\(t\ < \ t_{dropout}\)): \(\lambda(t)\ = \ 0.0182\ \times \ 0.75\ \times \ Frailty\) (finerenone rate)

  2. After dropout (\(t\ \geq \ t_{dropout}\)): \(\lambda(t)\ = \ 0.0182\ \times \ Frailty\) (placebo rate)

The rate changes at time \(t_{dropout}\), making it non-homogeneous.

However, homogeneous process will keep a constant rate: \(\lambda(t)\ = \ \lambda_{j}\) for all t.

Method 1 to Generate Events using non-homo process: Thinning Algorithm

  1. Generate events from the maximum rate over the entire period

  2. Accept/reject each event based on \(\lambda(t)\) at that time

  3. More complex but handles arbitrary \(\lambda(t)\)

Method 2 to Generate Events using non-homo process: Piecewise Constant Rates (Simpler)

Divide time into intervals with constant rates:

Interval 1: \([0, t_{drop-out})\) → Rate = \(\lambda ₁\)

Interval 2: \([t_{drop-out}, T_{max}]\) → Rate =\(\ \lambda ₂\)

Generate events separately in each interval using renewal process.

SAS Program (Core Parts) for Reference

/* Generate events before dropout */

current_time = 0;

do while (current_time < min(time_to_dropout, censor_time));

inter_event_time = rand('exponential') / lambda_hf_on_treatment;

current_time = current_time + inter_event_time;

if current_time < min(time_to_dropout, censor_time) then do;

hf_event_time = current_time;

output;

end;

end;

/* Generate events after dropout (if applicable) */

if time_to_dropout < censor_time then do;

current_time = time_to_dropout;

lambda_hf_post_dropout = &hf_event_rate * frailty; /* Revert to placebo rate */

do while (current_time < censor_time);

inter_event_time = rand('exponential') / lambda_hf_post_dropout;

current_time = current_time + inter_event_time;

if current_time < censor_time then do;

hf_event_time = current_time;

output;

end;

end;

end;

Appendix 2. Reference SAS Program for Power Simulation

/**************************************************************************
 * purpose: generate simulation datasets for phase 3 heart failure trial
 *          primary endpoint: total (first and recurrent) cv death and hf events.
 *          tried to replicate the power simulation by NCT04435626.
 * analysis: andersen-gill model with robust standard errors.        
 * note: stratification factors (region, lvef) not included (simplify the simulation)
 * author: yupng.l (2026)

 * This post presents an independent replication of the primary endpoint power simulation 
 * using the primary analysis methodology described in NCT04435626. The implementation 
 * relies exclusively on publicly available information and reflects my own interpretation. 
 * This work is not affiliated with or endorsed by the study sponsor or investigators. 
 * The views expressed here are my own.
 **************************************************************************/
dm log "clear";
dm output "clear";

proc datasets lib=work memtype=data kill;
run;

quit;

title1;
footnote1;
%let n_sim = 1000;        /* number of simulations */
%let n_participants = 5500;    /* total sample size */
%let recruitment_months = 24;  /* recruitment period */
%let total_months = 42;    /* total study duration */

/* parameters */
%let cv_death_rate = 0.005125;    /* cv death rate per month */
%let hf_event_rate = 0.0182;      /* hf event rate per month */
%let noncv_death_rate = 0.0016;   /* non-cv death rate per month */
%let frailty_variance = 5.0;      /* gamma frailty variance */
%let hr_cv_death = 0.80;          /* hazard ratio for cv death */
%let rr_hf_event = 0.75;          /* rate ratio for hf events */
%let annual_dropout = 0.05;       /* annual discontinuation rate */

/**************************************************************************
 * macro: generate one simulation dataset
 **************************************************************************/
%macro generate_one_sim(sim_id=);
    /*** step 1: generate baseline participant data ***/
    data participants_&sim_id;
        call streaminit(123+&sim_id);  /* set seed for reproducibility */

        do participant_id = 1 to &n_participants;
            /* enrollment time (uniform over recruitment period) */
            enroll_time = rand('uniform') * &recruitment_months;

            /* randomization 1:1 */
            treatment = rand('bernoulli', 0.5);  /* 0=placebo, 1=finerenone */

            /* generate individual frailty from gamma() distribution */
            /* alpha used as a temporary variable */
            /* where theta = frailty_variance */
            alpha = 1 / &frailty_variance;
            frailty = rand('gamma', alpha) / alpha;

            /* maximum follow-up time for this participant */
            max_followup = &total_months - enroll_time;

            /* treatment-specific parameters */
            if treatment = 0 then
                do;
                    /* placebo */
                    lambda_cv = &cv_death_rate * frailty;
                    lambda_hf = &hf_event_rate * frailty;
                end;
            else
                do;
                    /* finerenone */
                    lambda_cv = &cv_death_rate * &hr_cv_death * frailty;
                    lambda_hf = &hf_event_rate * &rr_hf_event * frailty;
                end;

            lambda_noncv = &noncv_death_rate;  /* independent of frailty */

            /* generate discontinuation time */
            dropout_rate = &annual_dropout / 12;  /* monthly rate */
            time_to_dropout = rand('exponential') / dropout_rate;
            output;
        end;

        drop alpha;
    run;

    /*** step 2: generate cv deaths ***/
    data cv_deaths_&sim_id;
        set participants_&sim_id;

        /* generate time to cv death */
        time_to_cv_death = rand('exponential') / lambda_cv;

        /* adjust if occurs after dropout (finerenone reverts to placebo) */
        if treatment = 1 and time_to_dropout < time_to_cv_death then
            do;
                /* generate new cv death time from dropout using placebo rate */
                lambda_cv_placebo = &cv_death_rate * frailty;
                time_to_cv_death = time_to_dropout + 
                    rand('exponential') / lambda_cv_placebo;
            end;

        /* apply administrative censoring */
        if time_to_cv_death > max_followup then
            do;
                cv_death = 0;
                cv_death_time = max_followup;
            end;
        else
            do;
                cv_death = 1;
                cv_death_time = time_to_cv_death;
            end;

        keep participant_id treatment frailty enroll_time max_followup 
            time_to_dropout lambda_hf cv_death cv_death_time;
    run;

    /*** step 3: generate non-cv deaths ***/
    data noncv_deaths_&sim_id;
        set cv_deaths_&sim_id;

        /* generate time to non-cv death */
        time_to_noncv_death = rand('exponential') / &noncv_death_rate;

        /* apply administrative censoring */
        if time_to_noncv_death > max_followup then
            do;
                noncv_death = 0;
                noncv_death_time = max_followup;
            end;
        else
            do;
                noncv_death = 1;
                noncv_death_time = time_to_noncv_death;
            end;

        keep participant_id treatment frailty enroll_time max_followup 
            time_to_dropout lambda_hf 
            cv_death cv_death_time noncv_death noncv_death_time;
    run;

    /*** step 4: generate recurrent hf events ***/
    data hf_events_&sim_id;
        set noncv_deaths_&sim_id;

        /* determine censoring time (earliest of death or admin censoring) */
        censor_time = min(cv_death_time, noncv_death_time, max_followup);

        /* generate recurrent hf events using renewal process */
        current_time = 0;
        event_num = 0;
        current_lambda_hf = lambda_hf;

        do while (current_time < censor_time);
            /* generate next hf event time */
            inter_event_time = rand('exponential') / current_lambda_hf;
            current_time = current_time + inter_event_time;

            /* check if event occurs before censoring */
            if current_time < censor_time then
                do;
                    event_num + 1;
                    hf_event_time = current_time;

                    /* check if dropout occurred and adjust rate if needed */
                    if treatment = 1 and time_to_dropout < current_time 
                        and time_to_dropout < hf_event_time then
                        do;
                            /* revert to placebo rate after dropout */
                            current_lambda_hf = &hf_event_rate * frailty;
                        end;

                    output;
                end;
            else leave;
        end;

        /* keep variables needed for analysis */
        keep participant_id treatment event_num hf_event_time censor_time;
    run;

    /*** step 5: create counting process dataset for andersen-gill ***/
    /* first, add cv death as an event */
    data cv_death_events_&sim_id;
        set noncv_deaths_&sim_id;
        where cv_death = 1;
        event_num = 0;  /* assign cv death as event 0 for sorting */
        event_time = cv_death_time;
        event_type = 'cv_death';
        keep participant_id treatment event_num event_time event_type;
    run;

    /* add hf events */
    data hf_event_list_&sim_id;
        set hf_events_&sim_id;
        event_time = hf_event_time;
        event_type = 'hf_event';
        keep participant_id treatment event_num event_time event_type;
    run;

    /* combine all events and sort */
    data all_events_&sim_id;
        set cv_death_events_&sim_id hf_event_list_&sim_id;
    run;

    proc sort data=all_events_&sim_id;
        by participant_id event_time;
    run;

    /* create counting process format */
    data counting_process_&sim_id;
        set all_events_&sim_id;
        by participant_id;

        /* start time is previous event time (or 0 for first event) */
        retain prev_event_time;

        if first.participant_id then
            do;
                start_time = 0;
                prev_event_time = 0;
            end;
        else start_time = prev_event_time;
        stop_time = event_time;
        event = 1;  /* all rows represent events */
        prev_event_time = event_time;

        /* add censoring information for last observation */
        if last.participant_id then
            last_event = 1;
        else last_event = 0;
        sim_id = &sim_id;
        keep sim_id participant_id treatment start_time stop_time event 
            event_type last_event;
    run;

    /* add censored observations (no events) */
    data participants_no_events_&sim_id;
        merge noncv_deaths_&sim_id(in=a) 
            all_events_&sim_id(in=b keep=participant_id);
        by participant_id;

        if a and not b;  /* participants with no events */
        sim_id = &sim_id;
        start_time = 0;
        stop_time = min(cv_death_time, noncv_death_time, max_followup);
        event = 0;
        event_type = 'censored';
        last_event = 1;
        keep sim_id participant_id treatment start_time stop_time event 
            event_type last_event;
    run;

    /* combine events and censored observations */
    data final_dataset_&sim_id;
        set counting_process_&sim_id participants_no_events_&sim_id;
    run;

    proc sort data=final_dataset_&sim_id;
        by sim_id participant_id start_time;
    run;

    /*** step 6: create summary statistics dataset ***/
    proc sql;
        create table sim_summary_&sim_id as
            select 
                &sim_id as sim_id,
                count(distinct participant_id) as n_participants,
                sum
            (case 
                when treatment=0 then 1 
                else 0 
            end)
        as n_placebo,
            sum
        (case 
            when treatment=1 then 1 
            else 0 
        end)
    as n_finerenone,
        sum
    (case 
        when event=1 then 1 
        else 0 
    end)
as total_events,
    sum
(case 
    when event=1 and treatment=0 then 1 
    else 0 
end)
as events_placebo,
sum
(case 
when event=1 and treatment=1 then 1 
else 0 
end)
as events_finerenone,
sum
(case 
when event=1 and event_type='cv_death' then 1 
else 0 
end)
as cv_deaths,
sum
(case 
when event=1 and event_type='hf_event' then 1 
else 0 
end)
as hf_events,
sum(stop_time - start_time) as total_person_months,
sum
(case 
when treatment=0 then (stop_time - start_time) 
else 0 
end)
as person_months_placebo,
sum
(case 
when treatment=1 then (stop_time - start_time) 
else 0 
end)
as person_months_finerenone
from final_dataset_&sim_id;
    quit;

    /* count first events per participant */
    proc sql;
        create table first_events_&sim_id as
            select 
                participant_id,
                treatment,
                min(event_time) as time_to_first_event
            from (
                /* combine cv deaths and hf events */
            select participant_id, treatment, cv_death_time as event_time
                from noncv_deaths_&sim_id
                    where cv_death = 1

                        union

                    select participant_id, treatment, hf_event_time as event_time
                        from hf_events_&sim_id
                            )
                        group by participant_id, treatment;
    quit;

    /* count first events by treatment arm */
    proc sql;
        create table first_event_summary_&sim_id as
            select 
                &sim_id as sim_id,
                count(*) as n_first_events_total,
                sum
            (case 
                when treatment=0 then 1 
                else 0 
            end)
        as n_first_events_placebo,
            sum
        (case 
            when treatment=1 then 1 
            else 0 
        end)
    as n_first_events_finerenone
        from first_events_&sim_id;
    quit;

    /* count total events (already in counting_process dataset) */
    proc sql;
        create table total_event_summary_&sim_id as
            select 
                &sim_id as sim_id,
                sum(event) as n_total_events,
                sum
            (case 
                when treatment=0 and event=1 then 1 
                else 0 
            end)
        as n_total_events_placebo,
            sum
        (case 
            when treatment=1 and event=1 then 1 
            else 0 
        end)
    as n_total_events_finerenone
        from final_dataset_&sim_id;
    quit;

    /* calculate the ratio */
    proc sql;
        create table event_ratio_&sim_id as
            select 
                a.sim_id,
                a.n_first_events_total,
                a.n_first_events_placebo,
                a.n_first_events_finerenone,
                b.n_total_events,
                b.n_total_events_placebo,
                b.n_total_events_finerenone,
                b.n_total_events / a.n_first_events_total as total_to_first_ratio,
                b.n_total_events_placebo / a.n_first_events_placebo as ratio_placebo,
                b.n_total_events_finerenone / a.n_first_events_finerenone as ratio_finerenone
            from first_event_summary_&sim_id as a,
                total_event_summary_&sim_id as b
            where a.sim_id = b.sim_id;
    quit;

    /* calculate rates per 100 person-years */
    data sim_summary_&sim_id;
        set sim_summary_&sim_id;
        person_years_total = total_person_months / 12;
        person_years_placebo = person_months_placebo / 12;
        person_years_finerenone = person_months_finerenone / 12;
        rate_total = (total_events / person_years_total) * 100;
        rate_placebo = (events_placebo / person_years_placebo) * 100;
        rate_finerenone = (events_finerenone / person_years_finerenone) * 100;
        rate_ratio = rate_finerenone / rate_placebo;
        drop total_person_months person_months_placebo person_months_finerenone;
    run;

%mend generate_one_sim;

/**************************************************************************
 * main execution: generate multiple simulations
 **************************************************************************/
%macro run_all_simulations;
    %do sim = 1 %to &n_sim;
        %if %sysfunc(mod(&sim, 100)) = 0 %then
            %do;
                %put note: completed &sim of &n_sim simulations;
            %end;

        %generate_one_sim(sim_id=&sim);
    %end;

    /* combine all simulation datasets */
    data all_simulations;
        set final_dataset_1 - final_dataset_&n_sim;
    run;

    /* combine all summary statistics */
    data all_summaries;
        set sim_summary_1 - sim_summary_&n_sim;
    run;

    data all_event_ratios;
        set event_ratio_1 - event_ratio_&n_sim;
    run;

    proc means data=all_event_ratios mean std min max p25 p50 p75;
        var n_first_events_total 
            n_total_events 
            total_to_first_ratio;
        title "summary of event counts across all simulations";
    run;

    /* clean up temp simulation datasets */
    proc datasets library=work nolist;
        delete participants_: cv_deaths_: noncv_deaths_: hf_events_: 
            cv_death_events_: hf_event_list_: all_events_: 
            counting_process_: participants_no_events_: final_dataset_: 
            sim_summary_:;
    quit;

%mend run_all_simulations;

/**************************************************************************
 * execute simulation 
 **************************************************************************/
%run_all_simulations;

/**************************************************************************
 * after running simulations, analyze with andersen-gill model
 **************************************************************************/
%macro analyze_sim;
    /* fit andersen-gill model with robust standard errors */
    proc sort data=all_simulations;
        by sim_id participant_id start_time;
    run;

    /* template sas program from bayer sap (published) */
    proc phreg data=all_simulations covs(aggregate);
        by sim_id;
        class participant_id;
        model (start_time, stop_time)*event(0) = treatment / ties=efron rl;
        id participant_id;  /* cluster variable for robust se */
        ods output parameterestimates=parms;
    run;

    /* extract results */
    data results;
        set parms;
        where parameter = 'treatment';
        rate_ratio = exp(estimate);
        p_value = probchisq;
        significant = (p_value < 0.05);
        keep sim_id rate_ratio p_value significant;
    run;

%mend analyze_sim;

/**************************************************************************
 * calculate power across all simulations
 **************************************************************************/
%macro calculate_power;
    /* analyze all simulations */
    %analyze_sim();

    /* combine results */
    data all_results;
        set results;
    run;

    /* calculate power */
    proc means data=all_results mean;
        var rate_ratio significant;
        output out=power_results 
            mean=(rate_ratio) =  mean_rr
            mean(significant)=power 
            n(significant)=n_sims;
    run;

    data power_results;
        set power_results;
        power_pct = power * 100;
    run;

    proc print data=power_results;
        title "power analysis results";
        var power_pct mean_rr n_sims;
    run;

    /* clean up */
    proc datasets library=work nolist;
        delete parms_: results_:;
    quit;

%mend calculate_power;

/* execute power calculation */
%calculate_power;