diff --git a/blog-navbar.html b/blog-navbar.html new file mode 100644 index 00000000..3a0e4036 --- /dev/null +++ b/blog-navbar.html @@ -0,0 +1,73 @@ + diff --git a/examples/index.qmd b/examples/index.qmd new file mode 100644 index 00000000..b724783c --- /dev/null +++ b/examples/index.qmd @@ -0,0 +1,13 @@ +--- +title: "Python Examples" +listing: + contents: posts + feed: true + sort: "date desc" + type: default + categories: true + sort-ui: false + filter-ui: false +page-layout: full +title-block-banner: true +--- diff --git a/examples/posts/calibration_curve/index.qmd b/examples/posts/calibration_curve/index.qmd new file mode 100644 index 00000000..b9911f9f --- /dev/null +++ b/examples/posts/calibration_curve/index.qmd @@ -0,0 +1,103 @@ +--- +title: "Calibration Curves for Multiple Models" +author: "Uriah Finkel" +date: "2024-05-15" +description: "An example of creating a calibration curve using rtichoke and scikit-learn." +categories: [calibration, scikit-learn] +draft: false +--- + +The following example is inspired by the [scikit-learn documentation displaying a calibration curve](https://scikit-learn.org/stable/auto_examples/calibration/plot_calibration_curve.html). + +## Load data and fit models + +```{python} +from sklearn.datasets import make_classification +from sklearn.model_selection import train_test_split +from sklearn.linear_model import LogisticRegression +from sklearn.naive_bayes import GaussianNB +from sklearn.calibration import CalibratedClassifierCV +import numpy as np +from sklearn.svm import LinearSVC + +X, y = make_classification( + n_samples=10_000, n_features=20, n_informative=2, n_redundant=10, random_state=42 +) + +X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.99, random_state=42 +) + +lr = LogisticRegression(C=1.0) +gnb = GaussianNB() +gnb_isotonic = CalibratedClassifierCV(gnb, cv=2, method="isotonic") +gnb_sigmoid = CalibratedClassifierCV(gnb, cv=2, method="sigmoid") + +lr.fit(X_train, y_train) +gnb.fit(X_train, y_train) +gnb_isotonic.fit(X_train, y_train) +gnb_sigmoid.fit(X_train, y_train) + +y_proba_lr = lr.predict_proba(X_test)[:, 1] +y_proba_gnb = gnb.predict_proba(X_test)[:, 1] +y_proba_gnb_isotonic = gnb_isotonic.predict_proba(X_test)[:, 1] +y_proba_gnb_sigmoid = gnb_sigmoid.predict_proba(X_test)[:, 1] + +class NaivelyCalibratedLinearSVC(LinearSVC): + def fit(self, X, y): + super().fit(X, y) + df = self.decision_function(X) + self.df_min_ = df.min() + self.df_max_ = df.max() + + def predict_proba(self, X): + df = self.decision_function(X) + calibrated_df = (df - self.df_min_) / (self.df_max_ - self.df_min_) + proba_pos_class = np.clip(calibrated_df, 0, 1) + proba_neg_class = 1 - proba_pos_class + return np.c_[proba_neg_class, proba_pos_class] + +svc = NaivelyCalibratedLinearSVC(max_iter=10_000) +svc_isotonic = CalibratedClassifierCV(svc, cv=2, method="isotonic") +svc_sigmoid = CalibratedClassifierCV(svc, cv=2, method="sigmoid") + +svc.fit(X_train, y_train) +svc_isotonic.fit(X_train, y_train) +svc_sigmoid.fit(X_train, y_train) + +y_proba_svc = svc.predict_proba(X_test)[:, 1] +y_proba_svc_isotonic = svc_isotonic.predict_proba(X_test)[:, 1] +y_proba_svc_sigmoid = svc_sigmoid.predict_proba(X_test)[:, 1] +``` + +## Gaussian Naive Bayes + +```{python} +from rtichoke import create_calibration_curve + +create_calibration_curve( + probs={ + "Logistic": y_proba_lr, + "Naive Bayes": y_proba_gnb, + "Naive Bayes + Isotonic": y_proba_gnb_isotonic, + "Naive Bayes + Sigmoid": y_proba_gnb_sigmoid, + }, + reals=y_test +).show(config={"displayModeBar": False, "displaylogo": False}) +``` + +## Linear SVC + +```{python} +create_calibration_curve( + probs={ + "Logistic": y_proba_lr, + "SVC": y_proba_svc, + "SVC + Isotonic": y_proba_svc_isotonic, + "SVC + Sigmoid": y_proba_svc_sigmoid, + }, + reals=y_test +).show(config={"displayModeBar": False, "displaylogo": False}) +``` + +This reproduces the core comparison from the scikit-learn example while keeping the rtichoke rendering as the Python example itself. diff --git a/examples/posts/dca_with_fixed_time_horizons/index.qmd b/examples/posts/dca_with_fixed_time_horizons/index.qmd new file mode 100644 index 00000000..5fe618a3 --- /dev/null +++ b/examples/posts/dca_with_fixed_time_horizons/index.qmd @@ -0,0 +1,62 @@ +--- +title: "Decision Curves with Fixed Time Horizons" +author: "Uriah Finkel" +date: "2025-12-15" +categories: [Decision, Time-Horizons] +--- + +`rtichoke` for Python introduces support for fixed time horizons, allowing flexible specification of the prediction horizon and automatically updating performance plots accordingly. + +## The most under-discussed design choice in prediction models: The Time Horizon + +Prediction models require a well-defined fixed time horizon: The end of follow-up over which the outcome probability is defined. A probability of dying within 1 week, 1 year, or 100 years represents fundamentally different clinical questions and very different implied decision contexts. + +It is therefore important to explore the sensitivity of model performance to the choice of time horizon: Shorter horizons typically yield fewer observed events, resulting in smaller gaps between the two baseline strategies "treat all" (Everyone is considered Predicted Positive) and "treat none" (everyone is considered Predicted Negative). + +In contrast, longer horizons may introduce ambiguity through censoring (loss to follow-up) or competing events (events that preclude the primary outcome). + +## Pragmatic approach: Performance Sensitivity Analysis with rtichoke + +You do not need to develop a new or more complex model to overcome these problems, first you need to ensure if there's a problem at all and for which time horizons: + +You can reuse predictions trained for a specific horizon and evaluate their robustness across alternative fixed time horizons: This allows you to assess how performance changes as the effective follow-up window varies without retraining the model. + +## Load data and fit a Cox Regression + +```{python} +import pandas as pd +import lifelines + +df_time_to_cancer_dx = pd.read_csv( + "https://raw.githubusercontent.com/ddsjoberg/dca-tutorial/main/data/df_time_to_cancer_dx.csv" +) + +cph = lifelines.CoxPHFitter() +cph.fit( + df=df_time_to_cancer_dx, + duration_col="ttcancer", + event_col="cancer", + formula="age + famhistory + marker", +) + +cph_pred_vals = cph.predict_survival_function( + df_time_to_cancer_dx[["age", "famhistory", "marker"]], times=[1.5] +) + +df_time_to_cancer_dx["pr_failure18"] = [1 - val for val in cph_pred_vals.iloc[0, :]] +``` + +## Decision Curve with Multiple Fixed Time Horizons + +The `fixed_time_horizons` argument allows you to explicitly define the set of follow-up horizons to evaluate. + +```{python} +from rtichoke import create_decision_curve_times + +create_decision_curve_times( + probs={"full": df_time_to_cancer_dx["pr_failure18"]}, + reals=df_time_to_cancer_dx["cancer"], + times=df_time_to_cancer_dx["ttcancer"], + fixed_time_horizons=[0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0] +) +``` diff --git a/great-docs.yml b/great-docs.yml index 8dcb1d78..28ac2c23 100644 --- a/great-docs.yml +++ b/great-docs.yml @@ -1,49 +1,98 @@ -display_name: rtichoke -parser: numpy -repo: https://github.com/uriahf/rtichoke_python -user_guide: user_guide -homepage: user_guide +display_name: rtichoke +parser: numpy +repo: https://github.com/uriahf/rtichoke_python +user_guide: user_guide +homepage: user_guide site_url: https://uriahf.github.io/rtichoke_python/ +include_in_header: + - text: | + site: css: site.css +sections: + - title: Examples + dir: examples + type: blog + navbar_after: User Guide + source: - enabled: true - branch: main - placement: usage - -reference: - - title: Performance Data - desc: Prepare classification and time-to-event data for visualization. - contents: - - prepare_performance_data - - prepare_binned_classification_data - - prepare_performance_data_times - - prepare_binned_classification_data_times - - - title: Discrimination - desc: ROC, precision-recall, gains, and lift visualizations. - contents: - - create_roc_curve - - create_roc_curve_times - - plot_roc_curve - - create_precision_recall_curve - - create_precision_recall_curve_times - - plot_precision_recall_curve - - create_gains_curve - - create_gains_curve_times - - plot_gains_curve - - create_lift_curve - - create_lift_curve_times - - plot_lift_curve - - - title: Calibration - desc: Calibration visualizations for classification and time-to-event models. - contents: - - create_calibration_curve - - create_calibration_curve_times - + enabled: true + branch: main + placement: usage + +reference: + - title: Performance Data + desc: Prepare classification and time-to-event data for visualization. + contents: + - prepare_performance_data + - prepare_binned_classification_data + - prepare_performance_data_times + - prepare_binned_classification_data_times + + - title: Discrimination + desc: ROC, precision-recall, gains, and lift visualizations. + contents: + - create_roc_curve + - create_roc_curve_times + - plot_roc_curve + - create_precision_recall_curve + - create_precision_recall_curve_times + - plot_precision_recall_curve + - create_gains_curve + - create_gains_curve_times + - plot_gains_curve + - create_lift_curve + - create_lift_curve_times + - plot_lift_curve + + - title: Calibration + desc: Calibration visualizations for classification and time-to-event models. + contents: + - create_calibration_curve + - create_calibration_curve_times + - title: Utility desc: Decision-curve analysis for classification and time-to-event models. contents: diff --git a/site.css b/site.css index 14644969..760c2a79 100644 --- a/site.css +++ b/site.css @@ -14,171 +14,59 @@ --rtichoke-soft: #fef0ec; --rtichoke-primary: #c54b29; --rtichoke-accent: #ce3d15; - --rtichoke-border: #f0cfc3; - --rtichoke-text: #281d19; + --rtichoke-border: #f4d3c7; + --rtichoke-text: #1f1a17; --rtichoke-muted: #725f57; --rtichoke-shadow: 0 10px 28px rgba(96, 46, 27, 0.07); } -body { - background: var(--rtichoke-bg); - color: var(--rtichoke-text); - font-family: "Commissioner", system-ui, sans-serif; - font-weight: 400; - letter-spacing: 0.005em; -} - -h1, -h2, -h3, -h4, -h5, -h6, -.navbar-brand, -.quarto-title-block .title, -.sidebar-title { - color: var(--rtichoke-primary); - font-family: "Fraunces9pt-Light", "Fraunces", Georgia, serif; - font-weight: 700; - letter-spacing: -0.015em; -} - -a { - color: var(--rtichoke-accent); - text-decoration-color: rgba(206, 61, 21, 0.35); - text-decoration-thickness: 0.1em; - text-underline-offset: 0.14em; -} - -a:hover, -a:focus-visible { - color: var(--rtichoke-primary); - text-decoration-color: currentColor; -} - -.navbar, -.navbar-dark, -.navbar-light { - background: rgba(255, 247, 245, 0.96) !important; - border-bottom: 1px solid var(--rtichoke-border); - box-shadow: 0 5px 18px rgba(96, 46, 27, 0.05); - backdrop-filter: blur(12px); -} - -.navbar-brand, -.navbar .nav-link, -.navbar .navbar-title { - color: var(--rtichoke-primary) !important; -} - -.navbar .nav-link:hover, -.navbar .nav-link:focus, -.navbar .nav-link.active { - color: var(--rtichoke-accent) !important; -} - -#quarto-sidebar, -.sidebar, -.quarto-secondary-nav { - background: var(--rtichoke-bg); - border-color: var(--rtichoke-border); -} - -.sidebar-item a, -.sidebar-navigation a { - border-radius: 0.55rem; -} - -.sidebar-item a:hover, -.sidebar-item a.active, -.sidebar-navigation a:hover, -.sidebar-navigation a.active { - background: var(--rtichoke-soft); - color: var(--rtichoke-accent) !important; -} - -.quarto-title-block, -.gd-group-card, -.gd-api-card, -.card, -.callout { - background: var(--rtichoke-surface); - border-color: var(--rtichoke-border) !important; - border-radius: 0.9rem; - box-shadow: var(--rtichoke-shadow); -} - -.quarto-title-block { - padding: 1.25rem 1.4rem; -} - -blockquote { - background: var(--rtichoke-soft); - border-left: 4px solid var(--rtichoke-primary); - border-radius: 0 0.7rem 0.7rem 0; - color: #523d35; - padding: 0.85rem 1.1rem; -} - -div.sourceCode, -pre, -code:not(.sourceCode) { - border-color: var(--rtichoke-border) !important; -} - -div.sourceCode, -pre { - background: #fffdfc; - border-radius: 0.75rem; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); -} - -code:not(.sourceCode) { - background: var(--rtichoke-soft); - color: #8f321b; - border-radius: 0.35rem; -} - -.table, -table { - --bs-table-bg: transparent; - --bs-table-striped-bg: rgba(254, 240, 236, 0.72); - border-color: var(--rtichoke-border); -} - -.badge, -.btn-primary { - background-color: var(--rtichoke-primary) !important; - border-color: var(--rtichoke-primary) !important; -} - -.btn-outline-primary { - color: var(--rtichoke-primary); - border-color: var(--rtichoke-primary); -} - -.btn-outline-primary:hover { - background-color: var(--rtichoke-primary); - color: #fff; -} - -.page-footer, -footer.footer { - background: var(--rtichoke-surface); - border-top: 1px solid var(--rtichoke-border); - color: var(--rtichoke-muted); -} +body { background: var(--rtichoke-bg); color: var(--rtichoke-text); font-family: "Commissioner", system-ui, sans-serif; font-weight: 400; letter-spacing: 0.005em; } +h1, h2, h3, h4, h5, h6, .navbar-brand, .quarto-title-block .title, .sidebar-title, .listing-title { color: var(--rtichoke-primary); font-family: "Fraunces9pt-Light", "Fraunces", Georgia, serif; font-weight: 700; letter-spacing: -0.015em; } +a { color: var(--rtichoke-accent); text-decoration-color: rgba(206, 61, 21, 0.35); text-decoration-thickness: 0.1em; text-underline-offset: 0.14em; } +a:hover, a:focus-visible { color: var(--rtichoke-primary); text-decoration-color: currentColor; } +.navbar, .navbar-dark, .navbar-light { background: var(--rtichoke-bg) !important; border-bottom: 1px solid var(--rtichoke-border); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.04); } +.navbar-brand, .navbar .nav-link, .navbar .navbar-title { color: var(--rtichoke-primary) !important; } +.navbar .nav-link:hover, .navbar .nav-link:focus, .navbar .nav-link.active { color: var(--rtichoke-accent) !important; } +#quarto-sidebar, .sidebar, .quarto-secondary-nav { background: var(--rtichoke-bg); border-color: var(--rtichoke-border); } +.sidebar-item a, .sidebar-navigation a { border-radius: 0.55rem; } +.sidebar-item a:hover, .sidebar-item a.active, .sidebar-navigation a:hover, .sidebar-navigation a.active { background: var(--rtichoke-soft); color: var(--rtichoke-accent) !important; } +.quarto-title-block, .gd-group-card, .gd-api-card, .card, .callout { background: var(--rtichoke-surface); border-color: var(--rtichoke-border) !important; border-radius: 0.9rem; box-shadow: var(--rtichoke-shadow); } +.quarto-title-block { padding: 1.25rem 1.4rem; } + +/* The Examples area should feel exactly like the shared rtichoke blog shell. */ +.rtichoke-blog-shell #quarto-search, +.rtichoke-blog-shell .gd-github-widget, +.rtichoke-blog-shell .gd-version-badge, +.rtichoke-blog-shell .gd-dark-mode-toggle, +.rtichoke-blog-shell .gd-keyboard-nav, +.rtichoke-blog-shell .navbar .quarto-navbar-tools { display: none !important; } +.rtichoke-blog-shell .navbar-brand { font-family: "Fraunces", "Fraunces9pt-Light", serif; font-weight: 600; } +.rtichoke-blog-shell .navbar-nav .nav-link { font-family: "Fraunces", "Fraunces9pt-Light", serif; font-weight: 600; } +.rtichoke-blog-shell .quarto-title-block { background: transparent; border: 0; box-shadow: none; padding: 0; } +.rtichoke-blog-shell .quarto-listing-filter, +.rtichoke-blog-shell .quarto-listing .quarto-listing-filter, +.rtichoke-blog-shell .quarto-listing .listing-actions, +.rtichoke-blog-shell .quarto-listing .listing-search, +.rtichoke-blog-shell .quarto-listing .listing-tools, +.rtichoke-blog-shell .quarto-listing .listing-toolbar { display: none !important; } + +.quarto-listing { display: grid; gap: 1.5rem; } +.quarto-listing .listing-item { background: #ffffff; border: 1px solid var(--rtichoke-border); border-radius: 18px; padding: 1.4rem 1.6rem; box-shadow: 0 14px 30px rgba(0, 0, 0, 0.05); transition: transform 0.15s ease, box-shadow 0.15s ease; } +.quarto-listing .listing-item:hover { transform: translateY(-2px); box-shadow: 0 18px 38px rgba(0, 0, 0, 0.08); } +.listing-description { color: #4b3a32; margin-bottom: 0.75rem; } +.listing-categories .listing-category, .quarto-category { background: var(--rtichoke-soft); color: var(--rtichoke-accent); border: 1px solid var(--rtichoke-border); padding: 0.25rem 0.55rem; border-radius: 999px; font-size: 0.85rem; font-weight: 600; } +.listing-date { color: #7b685f; font-weight: 600; letter-spacing: 0.02em; } + +blockquote { background: var(--rtichoke-soft); border-left: 4px solid var(--rtichoke-primary); border-radius: 0 0.7rem 0.7rem 0; color: #523d35; padding: 0.85rem 1.1rem; } +div.sourceCode, pre, code:not(.sourceCode) { border-color: var(--rtichoke-border) !important; } +div.sourceCode, pre { background: #fffdfc; border-radius: 0.75rem; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); } +code:not(.sourceCode) { background: var(--rtichoke-soft); color: #8f321b; border-radius: 0.35rem; } +.table, table { --bs-table-bg: transparent; --bs-table-striped-bg: rgba(254, 240, 236, 0.72); border-color: var(--rtichoke-border); } +.badge, .btn-primary { background-color: var(--rtichoke-primary) !important; border-color: var(--rtichoke-primary) !important; } +.btn-outline-primary { color: var(--rtichoke-primary); border-color: var(--rtichoke-primary); } +.btn-outline-primary:hover { background-color: var(--rtichoke-primary); color: #fff; } +.page-footer, footer.footer { background: var(--rtichoke-surface); border-top: 1px solid var(--rtichoke-border); color: var(--rtichoke-muted); } @media (prefers-color-scheme: dark) { - body.quarto-dark { - --rtichoke-bg: #211815; - --rtichoke-surface: #2d211d; - --rtichoke-soft: #3a2721; - --rtichoke-primary: #f29a7f; - --rtichoke-accent: #ffad91; - --rtichoke-border: #684235; - --rtichoke-text: #fff4f0; - --rtichoke-muted: #d5b9af; - --rtichoke-shadow: none; - } + body.quarto-dark { --rtichoke-bg: #211815; --rtichoke-surface: #2d211d; --rtichoke-soft: #3a2721; --rtichoke-primary: #f29a7f; --rtichoke-accent: #ffad91; --rtichoke-border: #684235; --rtichoke-text: #fff4f0; --rtichoke-muted: #d5b9af; --rtichoke-shadow: none; } }