From d4d5eda7e6f52f00de03c4f7cdfaa87dea073eff Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 13 Aug 2026 15:21:14 +0300 Subject: [PATCH 1/4] Add guides navigation for validation documentation --- great-docs.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/great-docs.yml b/great-docs.yml index 2c40814f..a8293201 100644 --- a/great-docs.yml +++ b/great-docs.yml @@ -14,7 +14,18 @@ include_in_header: + - text: | + site: css: site.css @@ -85,3 +107,4 @@ reference: - create_decision_curve - create_decision_curve_times - plot_decision_curve + From 76332189856b18bdca3f6b7a9becdc054a8a0dec Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 13 Aug 2026 15:21:19 +0300 Subject: [PATCH 2/4] Style the reusable horizon explorer --- site.css | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/site.css b/site.css index 14644969..29d4039f 100644 --- a/site.css +++ b/site.css @@ -182,3 +182,94 @@ footer.footer { --rtichoke-shadow: none; } } +uriahtalks-horizon-explorer { + color: inherit; + background: transparent; + container-type: inline-size; + display: block; + margin: 1rem 0 1.5rem; +} + +uriahtalks-horizon-explorer .uriah-horizon-heading { + font-size: 1.1em; + font-weight: 650; + margin-bottom: .65rem; +} + +uriahtalks-horizon-explorer .uriah-horizon-control { + display: grid; + gap: .3rem; + max-width: 34rem; +} + +uriahtalks-horizon-explorer input[type="range"] { + accent-color: var(--uriah-horizon-accent); + width: 100%; +} + +uriahtalks-horizon-explorer .uriah-horizon-chart { + background: transparent; + overflow-x: auto; + width: 100%; +} + +uriahtalks-horizon-explorer .uriah-horizon-svg { + background: transparent; + display: block; + min-width: 620px; + width: 100%; +} + +uriahtalks-horizon-explorer .uriah-axis { + stroke: currentColor; + stroke-width: 1; +} + +uriahtalks-horizon-explorer .uriah-tick, +uriahtalks-horizon-explorer .uriah-axis-label, +uriahtalks-horizon-explorer .uriah-row-label { + fill: currentColor; + font: 14px Commissioner, system-ui, sans-serif; +} + +uriahtalks-horizon-explorer .uriah-tick, +uriahtalks-horizon-explorer .uriah-axis-label { + text-anchor: middle; +} + +uriahtalks-horizon-explorer .uriah-row-label { + dominant-baseline: middle; + text-anchor: end; +} + +uriahtalks-horizon-explorer .uriah-followup-line { + stroke: color-mix(in srgb, currentColor 38%, transparent); + stroke-width: 2; +} + +uriahtalks-horizon-explorer .uriah-horizon-line { + stroke: var(--uriah-horizon-accent); + stroke-dasharray: 7 6; + stroke-width: 4; +} + +uriahtalks-horizon-explorer .uriah-emoji-marker { + cursor: help; + dominant-baseline: central; + font: 30px "Segoe UI Emoji", "Apple Color Emoji", sans-serif; + text-anchor: middle; +} + +uriahtalks-horizon-explorer .uriah-horizon-key { + display: flex; + flex-wrap: wrap; + font-size: .9em; + gap: .45rem 1.1rem; +} + +@media (max-width: 650px) { + uriahtalks-horizon-explorer .uriah-horizon-svg { + width: 720px; + } +} + From ae7b15d1fc18f546d68fe6be6491ce02f9f5a57e Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 13 Aug 2026 15:21:25 +0300 Subject: [PATCH 3/4] Add reusable fixed-horizon explorer --- assets/uriahtalks-horizon-explorer.js | 152 ++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 assets/uriahtalks-horizon-explorer.js diff --git a/assets/uriahtalks-horizon-explorer.js b/assets/uriahtalks-horizon-explorer.js new file mode 100644 index 00000000..575f47e9 --- /dev/null +++ b/assets/uriahtalks-horizon-explorer.js @@ -0,0 +1,152 @@ +// Great Docs-compatible renderer for the uriahtalks horizon explorer. +class UriahtalksHorizonExplorer extends HTMLElement { + connectedCallback() { + if (this.dataset.ready === "true") return; + this.dataset.ready = "true"; + + const competingAsCensored = + this.hasAttribute("competing-as-censored"); + const min = Number(this.getAttribute("min") || 5); + const max = Number(this.getAttribute("max") || 50); + const step = Number(this.getAttribute("step") || 5); + const initial = Number(this.getAttribute("horizon") || 30); + const title = this.getAttribute("heading") || "Explore the horizon"; + const accent = this.getAttribute("accent-color") || "#7a9a01"; + + this.style.setProperty("--uriah-horizon-accent", accent); + this.innerHTML = ` +
${title}
+ + +
+ `; + + const observations = [ + [1, .482, "primary"], [2, .194, "primary"], + [3, .998, "primary"], [4, .372, "primary"], + [5, .696, "none"], [6, .284, "competing"], + [7, .784, "primary"], [8, .920, "competing"], + [9, .630, "none"], [10, .086, "primary"], + ].map(([id, fraction, outcome]) => ({ + id, outcome, time: fraction * max, + })); + + const states = { + primary: ["🤢", "Primary event"], + competing: ["💀", "Competing event"], + censored: ["🤬", "Censored"], + nonEvent: ["🤨", "Non-event through horizon"], + }; + const input = this.querySelector("input"); + const output = this.querySelector("output"); + const chart = this.querySelector(".uriah-horizon-chart"); + const key = this.querySelector(".uriah-horizon-key"); + const ns = "http://www.w3.org/2000/svg"; + + const classify = (observation, horizon) => { + if (observation.time > horizon) return states.nonEvent; + if (observation.outcome === "primary") return states.primary; + if (observation.outcome === "competing") { + return competingAsCensored ? states.censored : states.competing; + } + return states.censored; + }; + + const render = () => { + const horizon = Number(input.value); + output.value = horizon; + chart.replaceChildren(); + const width = 900, height = 390; + const margin = { top: 22, right: 28, bottom: 48, left: 58 }; + const innerWidth = width - margin.left - margin.right; + const rowHeight = + (height - margin.top - margin.bottom) / observations.length; + const x = value => margin.left + (value / max) * innerWidth; + const svg = document.createElementNS(ns, "svg"); + svg.setAttribute("viewBox", `0 0 ${width} ${height}`); + svg.setAttribute("class", "uriah-horizon-svg"); + + const tickStep = Math.max(step, max / 5); + for (let tick = 0; tick <= max; tick += tickStep) { + const line = document.createElementNS(ns, "line"); + line.setAttribute("x1", x(tick)); + line.setAttribute("x2", x(tick)); + line.setAttribute("y1", height - margin.bottom); + line.setAttribute("y2", height - margin.bottom + 6); + line.setAttribute("class", "uriah-axis"); + svg.appendChild(line); + const text = document.createElementNS(ns, "text"); + text.setAttribute("x", x(tick)); + text.setAttribute("y", height - margin.bottom + 24); + text.setAttribute("class", "uriah-tick"); + text.textContent = Number(tick.toFixed(2)); + svg.appendChild(text); + } + + const axisLabel = document.createElementNS(ns, "text"); + axisLabel.setAttribute("x", margin.left + innerWidth / 2); + axisLabel.setAttribute("y", height - 5); + axisLabel.setAttribute("class", "uriah-axis-label"); + axisLabel.textContent = "Follow-up time"; + svg.appendChild(axisLabel); + + const horizonLine = document.createElementNS(ns, "line"); + horizonLine.setAttribute("x1", x(horizon)); + horizonLine.setAttribute("x2", x(horizon)); + horizonLine.setAttribute("y1", margin.top - 8); + horizonLine.setAttribute("y2", height - margin.bottom); + horizonLine.setAttribute("class", "uriah-horizon-line"); + svg.appendChild(horizonLine); + + observations.forEach((observation, index) => { + const y = margin.top + rowHeight * (index + .5); + const displayTime = Math.min(observation.time, horizon); + const state = classify(observation, horizon); + const label = document.createElementNS(ns, "text"); + label.setAttribute("x", margin.left - 14); + label.setAttribute("y", y); + label.setAttribute("class", "uriah-row-label"); + label.textContent = observation.id; + svg.appendChild(label); + const followup = document.createElementNS(ns, "line"); + followup.setAttribute("x1", x(0)); + followup.setAttribute("x2", x(displayTime)); + followup.setAttribute("y1", y); + followup.setAttribute("y2", y); + followup.setAttribute("class", "uriah-followup-line"); + svg.appendChild(followup); + const marker = document.createElementNS(ns, "text"); + marker.setAttribute("x", x(displayTime)); + marker.setAttribute("y", y); + marker.setAttribute("class", "uriah-emoji-marker"); + marker.textContent = state[0]; + const tooltip = document.createElementNS(ns, "title"); + tooltip.textContent = + `Observation ${observation.id}: ${state[1]}; observed time ${observation.time.toFixed(2)}`; + marker.appendChild(tooltip); + svg.appendChild(marker); + }); + + chart.appendChild(svg); + const active = competingAsCensored + ? [states.primary, states.censored, states.nonEvent] + : [states.primary, states.competing, states.censored, states.nonEvent]; + key.replaceChildren(...active.map(state => { + const item = document.createElement("span"); + item.innerHTML = ` ${state[1]}`; + return item; + })); + }; + + input.addEventListener("input", render); + render(); + } +} + +customElements.define("uriahtalks-horizon-explorer", UriahtalksHorizonExplorer); + From 84c02e2cfa5451973fdc2dad7fe38847349c152a Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Thu, 13 Aug 2026 15:21:33 +0300 Subject: [PATCH 4/4] Document fixed time horizons before validation --- user_guide/02-fixed-time-horizons.qmd | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 user_guide/02-fixed-time-horizons.qmd diff --git a/user_guide/02-fixed-time-horizons.qmd b/user_guide/02-fixed-time-horizons.qmd new file mode 100644 index 00000000..1268bd5e --- /dev/null +++ b/user_guide/02-fixed-time-horizons.qmd @@ -0,0 +1,54 @@ +--- +title: "Fixed Time Horizons" +guide-section: "Before You Validate" +--- + +A time-to-event prediction must specify when the outcome is evaluated. Use +`fixed_time_horizons` to declare those times in the same unit as `times` and +the prediction model: + +```python +fixed_time_horizons = [5.0, 10.0] # years +``` + +Choose clinically meaningful horizons before inspecting performance. Changing +the horizon changes the outcome being validated. + +## Update administrative censoring + +At a fixed horizon: + +- an event observed after the horizon is a 🤨 non-event *at that horizon*; +- event-free follow-up ending before the horizon is 🤬 censored; +- a primary event observed by the horizon remains 🤢; and +- a competing event observed by the horizon remains 💀. + +## Explore the horizon + +Move the slider to see how the selected horizon changes each observation. The +vertical line marks the horizon; information after it is not used. + + + + +The symbols are: + +- 🤬 censored before the horizon; +- 🤢 primary event; +- 🤨 non-event through the horizon; and +- 💀 competing event. + +## Pass horizons to `rtichoke` + +```python +performance_data = rk.prepare_performance_data_times( + probs=probs, + reals=reals, + times=times, + fixed_time_horizons=[5.0, 10.0], +) +``` +