TFL Gallery / f-derm-time-to-event
AEF01 · Figure
Time to Dermatologic Event by Treatment Group
The study's only figure: Kaplan-Meier curves for time to a first dermatologic event, by treatment group, with medians, confidence limits and a log-rank test. Xanomeline is delivered transdermally, so this is the tolerability question the delivery route raises.
- Slug
- f-derm-time-to-event
- Regulatory ID
- AEF01
- Type
- Figure
- E3 position
- §14.2
- Status
- evidenced
- Current iteration
- v002
- Evidence
- 20/20 passing
- Requirements
- DSP-ALL-001 DSP-ALL-002 DSP-INTXT-001 DSP-INTXT-002 DSP-INTXT-003 DSP-TTE-001 DSP-TTE-002 DSP-TTE-003 STD-GATE-001 STD-GATE-002 STD-GATE-003 STD-GATE-004 STD-GATE-005 STD-MODEL-001 STD-MODEL-002 STD-MODEL-003 STD-MODEL-004 STD-MODEL-005 STD-SRC-001 STD-SRC-002
- Used in
- Clinical Study Report Figure 14.2.14
- Post-Text Display Package Figure 14.2.14
- Abbreviated Clinical Study Report (ICH E3) Figure 14.2.14
- Study model
- Safety Analysis Set SAFFL · grouped by TRT01A
- Specification
- 2 iterations
- Environment
- R 4.3.3 Darwin 23.6.0
- Bound by
- No text block binds it
- Values
- No value is sourced from it
- adsl254 rows · 70c7278
- adtte254 rows · 82a036b
- analysis.yamlwhat to compute
- display.yamlhow to show it
- custom.Rthe display's own method
- study modelSafety Analysis Set
- prepare_data()regenerate('f-derm-time-to-event')
- build_ard()one row per statistic
- render_display()every variant
- render_rtf()the same cells
- ard.jsonv002 · 51 rows · dae95ac
- table.htmlthe rendered display
- table.rtfsha256 f13a708
- manifest.jsonwho, why, every hash
Generated by the same pipeline run that wrote the ARD and the HTML above, from the same rendered cells; the short hash is the manifest's record of the file.
| Time to Dermatologic Event by Treatment Group | |||
| Study CDISCPILOT01 — Safety Analysis Set | |||
| Placebo (N=86) | Xanomeline Low Dose (N=84) | Xanomeline High Dose (N=84) | |
|---|---|---|---|
| Subjects with a dermatologic event, n (%) | |||
| Censored, n (%) | |||
| Median time to event, days (95% CI) | |||
| Dermatologic events are the adverse events the study flagged as its first customised query, CQ01NAM = 'DERMATOLOGIC EVENTS'; ADTTE carries one record per subject for the first such treatment-emergent event. | |||
| Subjects without a dermatologic event are censored at their study completion date (CNSR = 1); the event time is the day of the first event (CNSR = 0). | |||
| Survival probability is the Kaplan-Meier product-limit estimate of remaining free of a dermatologic event. Tick marks on a curve are censored subjects, and the counts beneath it are the subjects still at risk at each of those days. | |||
| The median and its 95% confidence limits use the linear transformation, reproducing the limits in the reference report; NE means the curve never reached 0.5 and the median is not estimable. | |||
| The test annotated on the figure is the log-rank test of equality across the three treatment groups. Its chi-square and degrees of freedom are stated alongside the p-value so the p-value can be checked rather than taken on trust; the reference report states only that the difference was significant. A p-value smaller than the precision this display declares is reported at that boundary — '<0.0001' — rather than rounded to '0.0000', which would assert a probability of zero. The unrounded value is retained in the analysis results dataset. | |||
| The curve, the tick marks, the numbers at risk and the annotated test are drawn from the same committed analysis results dataset as the table beneath them, so no number on this page is a second calculation of another. | |||
| Source: adtte, adsl (CDISC pilot ADaM package, vendored from phuse-org/phuse-scripts). Data cut-off: 2014-07-01. | |||
| open.csr display f-derm-time-to-event (post_text variant); generated from the committed ARD. | |||
Rendered from outputs/f-derm-time-to-event/v002/table.html.
| analysis | group1 | group1_level | group2 | group2_level | variable | variable_level | context | stat_name | stat_label | warning | error | stat |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | time | Time (days) | — | — | 0, 1, 2, 3, 7, 8, 9, 12, 13, 14, 16, 21, 24, 26, 27, 28, 31, 32, 33, 35, 42, 43, 46, 48, 56, 57, 63, 64, 67, 70, 77, 79, 83, 90, 97, 110, 116, 135, 139, 142, 148, 162, 175, 177, 181, 182, 183, 184, 185, 188, 189, 191, 195, 196, 197, 198 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | surv | Survival probability | — | — | 1, 0.988372093023256, 0.976744186046512, 0.953488372093023, 0.941860465116279, 0.941860465116279, 0.930087209302326, 0.930087209302326, 0.930087209302326, 0.930087209302326, 0.917849219706242, 0.905611230110159, 0.881135250917993, 0.868897261321909, 0.856659271725826, 0.844421282129743, 0.844421282129743, 0.844421282129743, 0.831817979411389, 0.819214676693034, 0.80661137397468, 0.794008071256325, 0.794008071256325, 0.781201489461869, 0.768394907667412, 0.768394907667412, 0.768394907667412, 0.75514671960418, 0.75514671960418, 0.741416779247741, 0.713438787578015, 0.699449791743152, 0.685460795908289, 0.671471800073426, 0.657482804238563, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | n_risk | Number at risk | — | — | 86, 86, 85, 84, 82, 81, 80, 79, 78, 77, 76, 75, 74, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 56, 55, 53, 51, 50, 49, 48, 47, 46, 45, 44, 42, 41, 40, 39, 37, 35, 31, 27, 14, 11, 10, 9, 7, 5, 4, 2, 1 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | n_event_at | Events at time | — | — | 0, 1, 1, 2, 1, 0, 1, 0, 0, 0, 1, 1, 2, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | censor_time | Censoring time | — | — | 8, 12, 13, 14, 31, 32, 46, 57, 63, 64, 67, 70, 116, 135, 139, 142, 148, 162, 175, 177, 181, 182, 183, 184, 185, 188, 189, 191, 195, 196, 197, 198 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | censor_surv | Survival at censoring | — | — | 0.941860465116279, 0.930087209302326, 0.930087209302326, 0.930087209302326, 0.844421282129743, 0.844421282129743, 0.794008071256325, 0.768394907667412, 0.768394907667412, 0.75514671960418, 0.75514671960418, 0.741416779247741, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.6434938084037, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248, 0.626102083852248 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | risk_time | Times at which subjects at risk are counted (days) | — | — | 0, 50, 100, 150, 200 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival | risk_n | Subjects at risk | — | — | 86, 61, 47, 40, 0 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | N | Number of subjects | — | — | 86 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | n | Subjects with an event | — | — | 29 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | p | Proportion with an event | — | — | 0.337209302325581 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | n_censor | Subjects censored | — | — | 57 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | p_censor | Proportion censored | — | — | 0.662790697674419 |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | median | Median time to event | — | — | — |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | median_lcl | Median 95% LCL | — | — | — |
| km | TRT01A | Placebo | — | — | AVAL | TTDE | survival_summary | median_ucl | Median 95% UCL | — | — | — |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | time | Time (days) | — | — | 0, 1, 2, 3, 5, 8, 11, 12, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 39, 41, 43, 46, 48, 49, 51, 57, 60, 64, 68, 69, 71, 77, 80, 97, 100, 107, 112, 119, 121, 126, 167, 180, 181, 184, 187, 190 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | surv | Survival probability | — | — | 1, 0.988095238095238, 0.952380952380952, 0.928571428571429, 0.916666666666667, 0.916666666666667, 0.916666666666667, 0.879504504504505, 0.879504504504505, 0.816682754182754, 0.778989703989704, 0.753861003861004, 0.728732303732304, 0.728732303732304, 0.703162749215381, 0.677593194698458, 0.664808417439996, 0.652023640181535, 0.638983167377904, 0.599861748967012, 0.573780803359751, 0.560437063746733, 0.533749584520698, 0.520405844907681, 0.507062105294663, 0.493718365681646, 0.480374626068628, 0.467030886455611, 0.467030886455611, 0.452878435350895, 0.438269453565383, 0.423156713787266, 0.391811772025246, 0.376139301144237, 0.376139301144237, 0.359785418485791, 0.327077653168901, 0.310723770510456, 0.310723770510456, 0.293461338815431, 0.293461338815431, 0.275120005139467, 0.256778671463502, 0.238437337787538, 0.220096004111573, 0.201754670435609, 0.183413336759644, 0.16507200308368, 0.146730669407716, 0.146730669407716, 0.125769145206613, 0.125769145206613, 0.125769145206613, 0.125769145206613, 0.125769145206613, 0.125769145206613, 0.125769145206613 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | n_risk | Number at risk | — | — | 84, 84, 83, 80, 78, 76, 75, 74, 71, 70, 65, 62, 60, 58, 57, 55, 53, 52, 50, 49, 46, 43, 42, 40, 39, 38, 37, 36, 34, 33, 31, 29, 27, 25, 24, 23, 22, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | n_event_at | Events at time | — | — | 0, 1, 3, 2, 1, 0, 0, 3, 0, 5, 3, 2, 2, 0, 2, 2, 1, 1, 1, 3, 2, 1, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 2, 1, 0, 1, 2, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | censor_time | Censoring time | — | — | 5, 8, 11, 14, 20, 24, 28, 35, 37, 39, 41, 43, 49, 64, 69, 121, 167, 180, 181, 184, 187, 190 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | censor_surv | Survival at censoring | — | — | 0.916666666666667, 0.916666666666667, 0.916666666666667, 0.879504504504505, 0.728732303732304, 0.652023640181535, 0.573780803359751, 0.467030886455611, 0.467030886455611, 0.452878435350895, 0.438269453565383, 0.423156713787266, 0.376139301144237, 0.310723770510456, 0.293461338815431, 0.146730669407716, 0.125769145206613, 0.125769145206613, 0.125769145206613, 0.125769145206613, 0.125769145206613, 0.125769145206613 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | risk_time | Times at which subjects at risk are counted (days) | — | — | 0, 50, 100, 150, 200 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival | risk_n | Subjects at risk | — | — | 84, 23, 12, 6, 0 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | N | Number of subjects | — | — | 84 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | n | Subjects with an event | — | — | 62 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | p | Proportion with an event | — | — | 0.738095238095238 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | n_censor | Subjects censored | — | — | 22 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | p_censor | Proportion censored | — | — | 0.261904761904762 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | median | Median time to event | — | — | 33 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | median_lcl | Median 95% LCL | — | — | 27 |
| km | TRT01A | Xanomeline Low Dose | — | — | AVAL | TTDE | survival_summary | median_ucl | Median 95% UCL | — | — | 48 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | time | Time (days) | — | — | 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 16, 18, 19, 20, 22, 23, 24, 25, 29, 30, 31, 36, 37, 39, 42, 43, 44, 46, 47, 50, 51, 52, 55, 58, 61, 63, 64, 68, 69, 70, 71, 89, 94, 96, 167, 180, 188, 189 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | surv | Survival probability | — | — | 1, 0.964285714285714, 0.904017857142857, 0.855803571428571, 0.831696428571429, 0.807589285714286, 0.807589285714286, 0.795164835164835, 0.795164835164835, 0.795164835164835, 0.768659340659341, 0.742153846153846, 0.72866013986014, 0.701163530808814, 0.687415226283151, 0.673386344114107, 0.645328579776019, 0.631299697606975, 0.616951977206817, 0.602604256806658, 0.5882565364065, 0.559561095606183, 0.53011051162691, 0.514964497008998, 0.499818482391086, 0.484672467773175, 0.469526453155263, 0.454380438537351, 0.454380438537351, 0.423043856569258, 0.37423110388819, 0.357960186327834, 0.341689268767478, 0.292876516086409, 0.276605598526053, 0.260334680965697, 0.242979035567984, 0.225623390170271, 0.225623390170271, 0.206821440989415, 0.206821440989415, 0.206821440989415, 0.18384128087948, 0.160861120769545, 0.13788096065961, 0.114900800549675, 0.09192064043974, 0.09192064043974, 0.09192064043974, 0.09192064043974, 0.09192064043974 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | n_risk | Number at risk | — | — | 84, 84, 80, 75, 71, 69, 67, 65, 64, 61, 60, 58, 55, 53, 51, 49, 48, 46, 44, 43, 42, 41, 38, 35, 34, 33, 32, 31, 30, 29, 26, 23, 22, 21, 18, 17, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | n_event_at | Events at time | — | — | 0, 3, 5, 4, 2, 2, 0, 1, 0, 0, 2, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 0, 2, 3, 1, 1, 3, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | censor_time | Censoring time | — | — | 1, 6, 8, 11, 14, 15, 18, 22, 29, 30, 43, 44, 55, 63, 68, 69, 167, 180, 188, 189 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | censor_surv | Survival at censoring | — | — | 0.964285714285714, 0.807589285714286, 0.795164835164835, 0.795164835164835, 0.742153846153846, 0.72866013986014, 0.687415226283151, 0.631299697606975, 0.559561095606183, 0.53011051162691, 0.454380438537351, 0.423043856569258, 0.260334680965697, 0.225623390170271, 0.206821440989415, 0.206821440989415, 0.09192064043974, 0.09192064043974, 0.09192064043974, 0.09192064043974 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | risk_time | Times at which subjects at risk are counted (days) | — | — | 0, 50, 100, 150, 200 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival | risk_n | Subjects at risk | — | — | 84, 22, 4, 4, 0 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | N | Number of subjects | — | — | 84 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | n | Subjects with an event | — | — | 61 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | p | Proportion with an event | — | — | 0.726190476190476 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | n_censor | Subjects censored | — | — | 23 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | p_censor | Proportion censored | — | — | 0.273809523809524 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | median | Median time to event | — | — | 36 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | median_lcl | Median 95% LCL | — | — | 24 |
| km | TRT01A | Xanomeline High Dose | — | — | AVAL | TTDE | survival_summary | median_ucl | Median 95% UCL | — | — | 46 |
| km | — | — | — | — | AVAL | TTDE | survival_test | chisq | Log-rank chi-square | — | — | 60.2695567390281 |
| km | — | — | — | — | AVAL | TTDE | survival_test | df | Degrees of freedom | — | — | 2 |
| km | — | — | — | — | AVAL | TTDE | survival_test | p_value | Log-rank p-value | — | — | 8.17771631386364e-14 |
51 rows, one per computed statistic.
- Schema
- opencsr/ard/v1
- Generated
- 2026-09-02T15:23:21Z
- Spec hash
- sha256:dd67fc12f07b9385d20a849745d6d24fa38adb36470696468d5de7b4fe62d0c0
- Display hash
- sha256:a900fd7b08f074adb3842866f5e8cea6910d922cfd091d544e844d86fd3fdd68
- ARD file
- outputs/f-derm-time-to-event/v002/ard.json
- ARD hash
- sha256:dae95acbcabfedd187568e36989787120773f69897bcb9b67b4e17b32febc2a2
Source datasets
| Dataset | Rows | Package | Hash |
|---|---|---|---|
| adsl | 254 | phuse-org/phuse-scripts:data/adam 398a6d33ced9359ffb58c46650a6d488811176b1 | sha256:70c7278e83e571a7d4a94fd864c98bf8ca243c7c2cc9eec8b99a40fa7bc98e35 |
| adtte | 254 | phuse-org/phuse-scripts:data/adam 398a6d33ced9359ffb58c46650a6d488811176b1 | sha256:82a036bcf2e2e0a50abd055347a9a41fc6ff9f3895d17c38b13db5512fa8b0ab |
Environment
R 4.3.3 · Darwin 23.6.0 · cards 0.6.1 · cardx 0.2.5 · gtsummary 2.3.0 · gt 1.0.0 · dplyr 1.1.4 · pharmaverseadam 1.1.0 · jsonlite 2.0.0 · yaml 2.3.10 · digest 0.6.37
id: f-derm-time-to-event
title: "Time to Dermatologic Event by Treatment Group"
regulatory_id: AEF01
type: figure
# ADTTE is the study's own time-to-event dataset, and only the CDISC pilot's own
# ADaM package ships it. `sources: phuse` takes the whole study from that
# packaging so the denominators, the arms and the event times come from one
# coherent submission rather than two — the pilot's ADSL puts 86 / 84 / 84
# subjects in the three arms, which is what the reference report reports.
sources: phuse
dataset: adtte
analysis_set: safety
denominator: adsl
group:
- TRT01A
total: false
analyses:
- name: km
method: figure
custom: km_time_to_event
label: "Kaplan-Meier estimates of time to first dermatologic event"
# define.xml, Figure_14-1: ParamCD "TTDE", AnalysisVariables AVAL and CNSR,
# `time aval*cnsr(1)` — CNSR = 1 is the censoring value.
param: TTDE
time_var: AVAL
censor_var: CNSR
censor_value: 1
# The grid for the numbers-at-risk strip. Declared here so the counts the
# figure prints are committed statistics in the ARD, checkable against it,
# rather than arithmetic the renderer does on its way to the page.
risk_times: [0, 50, 100, 150, 200]
id: f-derm-time-to-event
title: "Time to Dermatologic Event by Treatment Group"
study: CDISCPILOT01
population_label: "Safety Analysis Set"
cutoff: "2014-07-01"
source: "Source: adtte, adsl (CDISC pilot ADaM package, vendored from phuse-org/phuse-scripts). Data cut-off: 2014-07-01."
footnotes:
- "Dermatologic events are the adverse events the study flagged as its first customised query, CQ01NAM = 'DERMATOLOGIC EVENTS'; ADTTE carries one record per subject for the first such treatment-emergent event."
- "Subjects without a dermatologic event are censored at their study completion date (CNSR = 1); the event time is the day of the first event (CNSR = 0)."
- "Survival probability is the Kaplan-Meier product-limit estimate of remaining free of a dermatologic event. Tick marks on a curve are censored subjects, and the counts beneath it are the subjects still at risk at each of those days."
- "The median and its 95% confidence limits use the linear transformation, reproducing the limits in the reference report; NE means the curve never reached 0.5 and the median is not estimable."
- "The test annotated on the figure is the log-rank test of equality across the three treatment groups. Its chi-square and degrees of freedom are stated alongside the p-value so the p-value can be checked rather than taken on trust; the reference report states only that the difference was significant. A p-value smaller than the precision this display declares is reported at that boundary — '<0.0001' — rather than rounded to '0.0000', which would assert a probability of zero. The unrounded value is retained in the analysis results dataset."
- "The curve, the tick marks, the numbers at risk and the annotated test are drawn from the same committed analysis results dataset as the table beneath them, so no number on this page is a second calculation of another."
columns:
order: [Placebo, "Xanomeline Low Dose", "Xanomeline High Dose"]
format:
digits:
p: 0
n_censor: 0
p_censor: 0
median: 0
median_lcl: 0
median_ucl: 0
chisq: 2
df: 0
p_value: 4
# `p_censor` is a proportion this display's custom.R computes, so it is
# declared here rather than pre-scaled in the ARD (contract §3).
proportions: [p, prop, p_censor]
median_ci: "{median} ({median_lcl}, {median_ucl})"
rows:
- { analysis: km, pattern: n_pct, label: "Subjects with a dermatologic event, n (%)" }
- { analysis: km, pattern: "{n_censor} ({p_censor}%)", label: "Censored, n (%)" }
- { analysis: km, pattern: median_ci, label: "Median time to event, days (95% CI)",
na_text: "NE" }
figure:
analysis: km
plot_title: "Product-Limit Survival Function Estimates"
width: 900
height: 460
x_axis:
label: "Time to Dermatologic Event or End of Study (days)"
min: 0
max: 200
ticks: [0, 50, 100, 150, 200]
y_axis:
label: "Survival Probability"
min: 0
max: 1
ticks: [0, 0.2, 0.4, 0.6, 0.8, 1.0]
digits: 1
stats:
time: time
value: surv
censor_time: censor_time
censor_value: censor_surv
# Series are named and ordered here; their colours and dash patterns come from
# the engine's palette, which is colour-vision-safe and has a dark-scheme
# counterpart. A hex value written here would be presentation masquerading as
# part of the display's definition.
series:
- level: Placebo
- level: "Xanomeline Low Dose"
- level: "Xanomeline High Dose"
at_risk:
times: risk_time
counts: risk_n
label: "Number at risk"
annotation:
bindings:
chisq: "km:chisq"
df: "km:df"
p: "km:p_value"
template: "Log-rank chi-square {chisq} on {df} df, p {p}"
variants:
post_text: {}
# Kaplan-Meier statistics for Figure 14-1 of the CDISCPILOT01 clinical study
# report, "Time to Dermatologic Event by Treatment Group".
#
# The analysis is not invented here. It is stated twice in the study's own
# submission package, and this file implements exactly what those two documents
# say and nothing more:
#
# define.xml, Analysis Results Metadata, ResultDisplay "Figure_14-1"
# Documentation ....... "Kaplan-Meier estimates and log-rank analysis of
# time to first dermatological adverse event, safety
# population"
# AnalysisDataset ..... ADTTE where SAFFL = "Y"
# ParameterList ....... PARAMCD = "TTDE"
# AnalysisVariables ... ADTTE.AVAL, ADTTE.CNSR
# ProgrammingCode ..... proc lifetest data = adtte (where=(saffl="Y")) plots=s;
# id usubjid;
# strata trtan;
# time aval*cnsr(1);
# test trtan;
# run;
#
# Statistical analysis plan, section 11.2
# "The time to the first dermatological event will be compared across the
# treatment groups using Kaplan-Meier methods. Graphical displays of the
# survival curves will be presented."
#
# Two decisions follow from the SAS code and are worth stating, because both are
# reproductions of a 2006 result rather than modern defaults:
#
# `time aval*cnsr(1)` makes CNSR = 1 the censoring value, so an event is
# CNSR = 0. ADTTE's define.xml derivation confirms it: "if ADAE.TRTEMFL = 'Y'
# then CNSR = 0, else CNSR = 1".
#
# The median's confidence limits use the linear ("plain") transformation,
# which is what PROC LIFETEST produced in the SAS release that generated the
# report. It is not survfit()'s default. Chosen because it reproduces the
# published limits exactly (27-48 and 24-46 days); the log-log transformation
# does not (it gives 23-46 for the high-dose arm). Recorded here so the choice
# is legible rather than buried in an argument.
#
# `test trtan` — the one-degree-of-freedom rank test for association with dose —
# is deliberately NOT computed. See the display's report; both tests the SAS
# step produces are p < 0.0001, and the published figure annotates the
# stratified log-rank, which is what `strata trtan` yields and what survdiff()
# reproduces.
# `custom.R` is sourced into a bare environment, so the package's own helpers
# are not in scope; this display needs one of them.
`%||%` <- function(x, y) if (is.null(x)) y else x
#' Kaplan-Meier estimates and the stratified log-rank test
#'
#' @param data ADTTE, already restricted to the analysis set by build_ard().
#' @param spec The analysis entry; `spec$group` names the stratification
#' variable, `spec$param` the PARAMCD to analyse, and `spec$risk_times` the
#' grid at which subjects at risk are counted for the strip under the curve.
#' @param denom The subject-level denominator dataset (unused: ADTTE is already
#' one record per subject per parameter, so the risk set is the denominator).
#' @return A cards-shaped data frame.
km_time_to_event <- function(data, spec, denom) {
if (!requireNamespace("survival", quietly = TRUE)) {
stop("The `survival` package is required by km_time_to_event().", call. = FALSE)
}
group <- spec$group
if (length(group) != 1) {
stop("km_time_to_event() needs exactly one grouping variable.", call. = FALSE)
}
param <- spec$param %||% "TTDE"
if (!is.null(data$PARAMCD)) {
data <- data[!is.na(data$PARAMCD) & data$PARAMCD == param, , drop = FALSE]
}
if (!nrow(data)) {
stop("km_time_to_event(): no records for PARAMCD '", param, "'.", call. = FALSE)
}
dup <- anyDuplicated(data$USUBJID)
if (dup) {
stop(
"km_time_to_event(): ADTTE has more than one '", param, "' record for ",
data$USUBJID[dup], "; the risk set would count a subject twice.",
call. = FALSE
)
}
# `time aval*cnsr(1)`: censored when CNSR is the censoring value, event
# otherwise. Read from the spec so the mapping is declared, not assumed.
censor_value <- as.numeric(spec$censor_value %||% 1)
time <- as.numeric(data[[spec$time_var %||% "AVAL"]])
cnsr <- as.numeric(data[[spec$censor_var %||% "CNSR"]])
if (anyNA(time) || anyNA(cnsr)) {
stop("km_time_to_event(): AVAL or CNSR is missing for at least one subject.", call. = FALSE)
}
event <- as.integer(cnsr != censor_value)
risk_times <- as.numeric(spec$risk_times %||% numeric(0))
strata <- data[[group]]
levels <- if (is.factor(strata)) levels(droplevels(strata)) else sort(unique(as.character(strata)))
strata <- factor(as.character(strata), levels = levels)
df <- data.frame(.time = time, .event = event, .strata = strata, stringsAsFactors = FALSE)
surv <- survival::Surv(df$.time, df$.event)
# conf.type = "plain": see the header note on reproducing the published limits.
fit <- survival::survfit(surv ~ .strata, data = df, conf.type = "plain")
tbl <- summary(fit)$table
if (is.null(dim(tbl))) tbl <- t(as.matrix(tbl))
rownames(tbl) <- levels
pieces <- list()
for (lv in levels) {
keep <- df$.strata == lv
n_sub <- sum(keep)
n_evt <- sum(df$.event[keep])
n_cns <- n_sub - n_evt
part <- if (length(levels) == 1) fit else fit[match(lv, levels)]
# The step function, as coordinates: (0, 1) then one point per distinct
# time survfit reports, which includes censoring-only times so the curve
# runs to the end of follow-up rather than stopping at the last event.
step_time <- c(0, as.numeric(part$time))
step_surv <- c(1, as.numeric(part$surv))
cens <- part$n.censor > 0
row <- function(stat_name, stat_label, value, context) {
out <- data.frame(
group1 = group, group1_level = lv,
group2 = NA_character_, group2_level = NA_character_,
variable = spec$time_var %||% "AVAL", variable_level = param,
context = context, stat_name = stat_name, stat_label = stat_label,
warning = NA_character_, error = NA_character_,
stringsAsFactors = FALSE
)
out$stat <- list(value)
out
}
# The numbers-at-risk strip prints committed statistics, not a second
# calculation: the count at each declared time is the size of the risk set,
# the subjects whose observed time has not yet passed. It travels in the ARD
# so a reader can check the strip against `ard.json` rather than against the
# renderer's arithmetic.
risk_n <- vapply(risk_times, function(t) sum(df$.time[keep] >= t), numeric(1))
pieces <- c(pieces, list(
row("time", "Time (days)", step_time, "survival"),
row("surv", "Survival probability", step_surv, "survival"),
row("n_risk", "Number at risk", c(n_sub, as.numeric(part$n.risk)), "survival"),
row("n_event_at", "Events at time", c(0, as.numeric(part$n.event)), "survival"),
row("censor_time", "Censoring time", as.numeric(part$time[cens]), "survival"),
row("censor_surv", "Survival at censoring", as.numeric(part$surv[cens]), "survival"),
row("risk_time", "Times at which subjects at risk are counted (days)", risk_times, "survival"),
row("risk_n", "Subjects at risk", risk_n, "survival"),
row("N", "Number of subjects", n_sub, "survival_summary"),
row("n", "Subjects with an event", n_evt, "survival_summary"),
row("p", "Proportion with an event", n_evt / n_sub, "survival_summary"),
row("n_censor", "Subjects censored", n_cns, "survival_summary"),
row("p_censor", "Proportion censored", n_cns / n_sub, "survival_summary"),
row("median", "Median time to event", unname(tbl[lv, "median"]), "survival_summary"),
row("median_lcl", "Median 95% LCL", unname(tbl[lv, "0.95LCL"]), "survival_summary"),
row("median_ucl", "Median 95% UCL", unname(tbl[lv, "0.95UCL"]), "survival_summary")
))
}
# `strata trtan` in PROC LIFETEST: the test of equality over strata. survdiff()
# with rho = 0 is the log-rank form, on (number of strata - 1) df.
if (length(levels) > 1) {
sd <- survival::survdiff(surv ~ .strata, data = df, rho = 0)
dfree <- length(sd$n) - 1
stat_row <- function(stat_name, stat_label, value) {
out <- data.frame(
group1 = NA_character_, group1_level = NA_character_,
group2 = NA_character_, group2_level = NA_character_,
variable = spec$time_var %||% "AVAL", variable_level = param,
context = "survival_test", stat_name = stat_name, stat_label = stat_label,
warning = NA_character_, error = NA_character_,
stringsAsFactors = FALSE
)
out$stat <- list(value)
out
}
pieces <- c(pieces, list(
stat_row("chisq", "Log-rank chi-square", unname(sd$chisq)),
stat_row("df", "Degrees of freedom", dfree),
# Named `p_value`, never `p`: the renderer scales any statistic called `p`
# to a percentage, and a p-value multiplied by 100 would publish silently.
stat_row("p_value", "Log-rank p-value", stats::pchisq(sd$chisq, dfree, lower.tail = FALSE))
))
}
out <- do.call(rbind, pieces)
rownames(out) <- NULL
out
}
v002current
One study (D0032 R2, #59): regenerated so the ARD carries its population record for the treatment-consistency gate. Spec and lane unchanged; every statistic identical.
2026-09-02T15:23:21Z · @obotclaw
v001
Initial generation of Figure 14-1: Kaplan-Meier estimates of time to first dermatologic event, from the study own analysis results metadata (define.xml ResultDisplay Figure_14-1, PROC LIFETEST) and SAP section 11.2.
2026-08-27T05:00:35Z · @jwildfire
Every iteration is kept: outputs/f-derm-time-to-event/vNNN/ holds the spec snapshot, the ARD, the rendered output, and a manifest recording who regenerated it, why, and from which commit.