Install any skill in seconds. Free to start, no credit card required.
Get Started Free →End-to-end biomarker discovery workflow from expression data to validated biomarker panels. Covers feature selection with Boruta/LASSO, classifier training with nested CV, and SHAP interpretation. Use when building and validating diagnostic or prognostic biomarker signatures from omics data.
.claude/skills/bio-workflows-biomarker-pipeline/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 10% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 51% | 0% |
<!--
#
#
-->
Complete pipeline from expression data to validated biomarker panels with classifier.
Expression matrix + Metadata
|
v
[1. Data Preparation] -----> StandardScaler, train/test split
|
v
[2. Feature Selection] ----> Boruta or LASSO stability selection
|
v
[3. Model Training] -------> RandomForest/XGBoost with nested CV
|
v
[4. Model Interpretation] -> SHAP values, feature importance
|
v
[5. Validation] -----------> Hold-out test, bootstrap CI
|
v
Validated biomarker panel + classifierpythonimport pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler expr = pd.read_csv('expression.csv', index_col=0) meta = pd.read_csv('metadata.csv', index_col=0) X = expr.T # samples x genes y = meta.loc[X.index, 'condition'].values # test_size=0.2: Standard 80/20 split; use 0.3 for <100 samples X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) # Fit scaler on training only to prevent data leakage scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)
QC Checkpoint 1: Check class balance, sample counts per group
pythonfrom boruta import BorutaPy from sklearn.ensemble import RandomForestClassifier from sklearn.feature_selection import SelectKBest, f_classif # Pre-filter if >10k features if X_train_scaled.shape[1] > 10000: selector = SelectKBest(f_classif, k=5000) selector.fit(X_train_scaled, y_train) X_train_filt = X_train_scaled[:, selector.get_support()] feature_mask = selector.get_support() else: X_train_filt = X_train_scaled feature_mask = None # max_depth=5: Shallow trees for stable importances rf = RandomForestClassifier(n_estimators=100, max_depth=5, n_jobs=-1, random_state=42) # max_iter=100: Usually sufficient; 200 if many tentative boruta = BorutaPy(rf, n_estimators='auto', max_iter=100, random_state=42, verbose=0) boruta.fit(X_train_filt, y_train) selected_idx = boruta.support_ print(f'Selected {selected_idx.sum()} features')
pythonfrom sklearn.linear_model import LogisticRegressionCV import numpy as np # n_bootstrap=100: Quick; use 500 for publication n_bootstrap = 100 stability_scores = np.zeros(X_train_scaled.shape[1]) for i in range(n_bootstrap): idx = np.random.choice(len(y_train), size=len(y_train), replace=True) # Cs=10: 10 regularization values to search model = LogisticRegressionCV(penalty='l1', solver='saga', Cs=10, cv=3, random_state=i, max_iter=1000) model.fit(X_train_scaled[idx], y_train[idx]) stability_scores += (model.coef_[0] != 0).astype(int) stability_scores /= n_bootstrap # stability_threshold=0.6: Standard; 0.8 for strict selected_idx = stability_scores > 0.6 print(f'Selected {selected_idx.sum()} features (stability >0.6)')
QC Checkpoint 2:
pythonfrom sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.ensemble import RandomForestClassifier X_train_sel = X_train_scaled[:, selected_idx] X_test_sel = X_test_scaled[:, selected_idx] # outer_cv=5: Standard for performance estimation outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) # n_estimators=100: Sufficient for most omics clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1) cv_scores = cross_val_score(clf, X_train_sel, y_train, cv=outer_cv, scoring='roc_auc') print(f'Nested CV AUC: {cv_scores.mean():.3f} +/- {cv_scores.std():.3f}')
QC Checkpoint 3:
pythonimport shap import matplotlib.pyplot as plt clf.fit(X_train_sel, y_train) # SHAP v0.47+: call explainer directly explainer = shap.TreeExplainer(clf) shap_values = explainer(X_train_sel) # Beeswarm: shows importance AND direction shap.plots.beeswarm(shap_values, max_display=20, show=False) plt.tight_layout() plt.savefig('shap_beeswarm.png', dpi=150, bbox_inches='tight') plt.close() # Extract top features import numpy as np mean_shap = np.abs(shap_values.values).mean(axis=0) top_shap_idx = np.argsort(mean_shap)[-20:]
QC Checkpoint 4:
pythonfrom sklearn.metrics import roc_auc_score, classification_report import numpy as np y_prob = clf.predict_proba(X_test_sel)[:, 1] test_auc = roc_auc_score(y_test, y_prob) print(f'Hold-out test AUC: {test_auc:.3f}') # Bootstrap CI for AUC # n_bootstrap=1000: Standard for publication-quality CI n_bootstrap = 1000 boot_aucs = [] for i in range(n_bootstrap): idx = np.random.choice(len(y_test), size=len(y_test), replace=True) boot_aucs.append(roc_auc_score(y_test[idx], y_prob[idx])) ci_lower, ci_upper = np.percentile(boot_aucs, [2.5, 97.5]) print(f'95% CI: [{ci_lower:.3f}, {ci_upper:.3f}]') print(classification_report(y_test, clf.predict(X_test_sel)))
| Step | Parameter | Recommendation | |------|-----------|----------------| | Split | test_size | 0.2 (standard), 0.3 for small datasets | | Boruta | max_iter | 100 (sufficient), 200 if tentative features | | LASSO | n_bootstrap | 100 (quick), 500 for publication | | LASSO | stability_threshold | 0.6 (standard), 0.8 for strict | | Nested CV | outer_folds | 5 (standard), 10 for small datasets | | Nested CV | inner_folds | 3 (sufficient for tuning) | | RF | n_estimators | 100-500 | | XGBoost | learning_rate | 0.1 (conservative) |
| Issue | Likely Cause | Solution | |-------|--------------|----------| | No features selected | Too strict threshold | Lower stability threshold, increase iterations | | Too many features (>200) | Noisy data | Add pre-filtering, increase regularization | | Low CV AUC (<0.6) | No signal, low power | Check data quality, add samples | | High variance across folds | Small sample size | Use more folds, LOOCV | | SHAP features differ from selected | Model using different signal | Review feature correlations |
pythonimport pandas as pd import joblib # Save biomarker panel feature_names = X_train.columns[selected_idx].tolist() pd.DataFrame({'feature': feature_names}).to_csv('biomarker_panel.csv', index=False) # Save model and scaler for deployment joblib.dump(clf, 'biomarker_classifier.joblib') joblib.dump(scaler, 'feature_scaler.joblib')
<!-- AUTHOR_SIGNATURE: 9a7f3c2e-MD-BABU-MIA-2026-MSSM-SECURE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 24,472 | 25,859 | +6% | 1 | 1 | 0% | 5,599 | 8,086 | +44% | 0 | 0 | — |
case-02 | fail→pass | 27,673 | 34,404 | +24% | 1 | 1 | 0% | 6,219 | 6,814 | +10% | 0 | 0 | — |
case-03 | fail→pass | 12,045 | 7,707 | -36% | 1 | 1 | 0% | 2,247 | 3,863 | +72% | 0 | 0 | — |
case-04 | pass→pass | 16,042 | 9,261 | -42% | 1 | 1 | 0% | 2,682 | 4,375 | +63% | 0 | 0 | — |
case-05 | fail→pass | 15,858 | 10,099 | -36% | 1 | 1 | 0% | 2,853 | 4,516 | +58% | 0 | 0 | — |
case-06 | fail→pass | 14,991 | 11,047 | -26% | 1 | 1 | 0% | 2,981 | 4,491 | +51% | 0 | 0 | — |
case-07 | pass→pass | 12,614 | 10,014 | -21% | 1 | 1 | 0% | 2,363 | 4,308 | +82% | 0 | 0 | — |
case-08 | pass→pass | 13,044 | 4,285 | -67% | 1 | 1 | 0% | 2,322 | 3,233 | +39% | 0 | 0 | — |
case-09 | fail→fail | 11,978 | 8,280 | -31% | 1 | 1 | 0% | 2,557 | 4,087 | +60% | 0 | 0 | — |
case-10 | fail→fail | 20,247 | 10,734 | -47% | 1 | 1 | 0% | 3,935 | 4,853 | +23% | 0 | 0 | — |
case-11 | fail→fail | 14,569 | 9,126 | -37% | 1 | 1 | 0% | 3,025 | 4,254 | +41% | 0 | 0 | — |
case-12 | fail→pass | 13,747 | 6,299 | -54% | 1 | 1 | 0% | 2,448 | 3,626 | +48% | 0 | 0 | — |
case-13 | pass→pass | 11,383 | 6,358 | -44% | 1 | 1 | 0% | 1,995 | 3,597 | +80% | 0 | 0 | — |
case-14 | pass→pass | 31,010 | 10,036 | -68% | 1 | 1 | 0% | 2,745 | 4,329 | +58% | 0 | 0 | — |
case-15 | fail→pass | 15,588 | 12,261 | -21% | 1 | 1 | 0% | 2,698 | 4,791 | +78% | 0 | 0 | — |
case-16 | fail→pass | 15,777 | 13,925 | -12% | 1 | 1 | 0% | 2,556 | 5,182 | +103% | 0 | 0 | — |
case-17 | fail→pass | 11,884 | 2,562 | -78% | 1 | 1 | 0% | 2,241 | 2,827 | +26% | 0 | 0 | — |
case-18 | pass→pass | 15,318 | 5,366 | -65% | 1 | 1 | 0% | 2,340 | 3,537 | +51% | 0 | 0 | — |
case-19 | pass→pass | 5,254 | 2,774 | -47% | 1 | 1 | 0% | 996 | 2,890 | +190% | 0 | 0 | — |
case-20 | pass→pass | 12,592 | 9,657 | -23% | 1 | 1 | 0% | 2,718 | 4,597 | +69% | 0 | 0 | — |
case-21 | pass→pass | 15,860 | 11,196 | -29% | 1 | 1 | 0% | 3,329 | 4,984 | +50% | 0 | 0 | — |
case-22 | pass→pass | 13,335 | 9,154 | -31% | 1 | 1 | 0% | 2,612 | 4,263 | +63% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/26/2026 | +36% |
Other measured skills in the registry, with their headline benchmark lift.