Repository navigation
Conversation
Test CoverageTotal: 74.27% (20594 / 27729 lines)
|
|
/review |
| __all__ = ["conformal_quantile"] | ||
|
|
||
|
|
||
| def conformal_quantile(scores, alpha: float, presorted: bool = False) -> float: |
There was a problem hiding this comment.
[medium] conformal_quantile duplicates existing _query_quantile
The split-conformal quantile (ceil((1-alpha)(n+1))-th smallest score, +inf when the rank exceeds n) is already implemented in pyhealth/calib/predictionset/base_conformal/init.py:43 as _query_quantile, which the label, cluster and other conformal methods use. This new helper repeats the same formula. Its differences (presorted fast path, alpha outside [0,1] returning -inf, a 1e-9 float tolerance) are not used by the existing helper, but they are not offered as a reason for a separate function either. Fix: extend _query_quantile with the presorted and out-of-range alpha behavior, or call it from EnbPI and document why the new edge cases differ from the set predictors.
| # The loader shuffles, so sorting keeps the same bag while letting | ||
| # disk-backed datasets read contiguous runs. | ||
| loader = get_dataloader( | ||
| train_dataset.subset(np.sort(indices).tolist()), |
There was a problem hiding this comment.
[low] Subset deep-copies the full dataset once per bootstrap model
_train_models calls train_dataset.subset(...) for each of the n_bootstrap models. SampleDataset.subset (pyhealth/datasets/sample_dataset.py:857-868) runs copy.deepcopy(self) before replacing _data, so each bag copies the entire dataset. Calibration cost therefore grows with B times the dataset size before any training. Fix: build the bag datasets from one shared copy of the data, or pass the index list to a loader without copying.
| raise ValueError("x and x_rec must have the same shape.") | ||
|
|
||
| prediction_interval_metrics = ["miscoverage_pi", "interval_width"] | ||
| has_intervals = y_lower is not None and y_upper is not None |
There was a problem hiding this comment.
[low] Interval metrics silently skipped when only one bound is given
has_intervals requires both y_lower and y_upper. If a caller passes only one bound, miscoverage_pi and interval_width are dropped without an error, and the result simply lacks those keys. The docstring documents this, but the mistake is easy to miss. Fix: raise ValueError when exactly one of y_lower or y_upper is given.
| metrics: Optional[List[str]] = None, | ||
| y_lower: np.ndarray | None = None, | ||
| y_upper: np.ndarray | None = None, | ||
| ) -> Dict[str, float]: |
There was a problem hiding this comment.
[low] Return annotation no longer matches rolling metric output
The annotation is Dict[str, float], but rolling_miscoverage_pi@W returns a numpy array, as the docstring says. Fix: change the annotation to Dict[str, float | np.ndarray] or a similar union.
| y_true.append(as_numpy(batch[self.label_key])) | ||
| return np.concatenate(y_pred), np.concatenate(y_true) | ||
|
|
||
| def forward(self, **kwargs) -> dict[str, torch.Tensor]: |
There was a problem hiding this comment.
[low] Trainer.inference compatibility is claimed but not tested
The forward docstring says it works inside Trainer.inference. The tests call forward directly and never run it through Trainer.inference, which would check the loss and y_true handling and the output keys. Fix: add one test that runs Trainer.inference on a small calibrated EnbPI and checks the returned keys and shapes.
There was a problem hiding this comment.
AI Review
suggestion: approve
decision: human review
Thank you for this contribution. It adds EnbPI as the first time series conformal interval method in pyhealth.calib, with an IntervalPredictor interface, interval metrics reachable through regression_metrics_fn, docs, an example and thorough tests. The implementation looks correct and matches its description. The tests use hand-computed values, checkpoint round trips and an end-to-end coverage check.
Coverage meets the bar. The whole repository is at 74.27%, above the 60% bar. The new files are at 100%. regression.py is at 90.2%. base_classes.py is at 75%, slightly under 80%. Its uncovered lines are the abstract-style ... bodies of the new interface, so there is little to test there.
A maintainer will take a look before this is decided. The change adds IntervalPredictor, a new base class in pyhealth/calib/base_classes.py that ACI and AgACI are planned to subclass. It also extends the shared regression_metrics_fn with optional parameters. EnbPI's checkpoint format relies on PyTorch's extra state hooks. Existing calls look unaffected. The maintainer will still want to check the new base-class contract, since it is the core part of the codebase.
Optional suggestions:
conformal_quantileoverlaps with the existing_query_quantilehelper inbase_conformal. You could reuse or extend that helper, or note briefly why a separate one is needed. The differences are the presorted fast path, alpha outside [0, 1] and the float tolerance.SampleDataset.subsetmay deep-copy the dataset for each bootstrap model. Check whether that is costly for large datasets.regression_metrics_fnsilently skips interval metrics when only one ofy_lowerory_upperis passed. Raising an error would be clearer. Its return annotation also no longer matches the array-valued rolling metric.- A test that runs
forwardthroughTrainer.inferencewould back up the claim in the docs.
Reviewed commit d68b819
Adds EnbPI from Xu and Xie, "Conformal prediction interval for dynamic time-series", ICML 2021, as the first time series conformal method in
pyhealth.calib. It builds prediction intervals for time series regression and updates them online as labels are revealed, without assuming exchangeability. ACI and AgACI will follow on the same interface.Changes
pyhealth.calib.predictionset.EnbPItrains a bootstrap ensemble from amodel_fnfactory, fills a sliding window with out-of-bag absolute residuals, and returns the center plus or minus the conformal(1 - alpha)quantile of the window.reset()restores the calibration window, andstate_dict()carries the full calibration state for checkpoints.IntervalPredictorinpyhealth/calib/base_classes.pydeclarescalibrate,forwardandupdate, likeSetPredictor.pyhealth/metrics/prediction_interval.pyaddsmiscoverage_pi,interval_widthandrolling_miscoverage_pi@W, available throughregression_metrics_fn(..., y_lower=..., y_upper=...).examples/conformal_timeseries/enbpi_synthetic.pyand tests.Key decisions
phiover the out-of-bag predictions, as in the paper's experiments and MAPIE.center="quantile"reproduces Algorithm 1 line 12, whose(1 - alpha)quantile biases the point prediction upward.model_fnfactory, following MAPIE's clone-per-bootstrap convention, so the bootstrap bags always match the models. No calibrator in PyHealth trained models before, so there was no in-repo convention.IntervalPredictorfixes only the public interface. Each method implements its own online loop, because ACI and AgACI update in ways a shared loop would constrain.forwardis stateless and works insideTrainer.inference, andupdateslides the window.predict_sequentialis a convenience method for evaluation that runs the paper's loop over a labeled test stream.calibratecall trains a new ensemble on the data passed in, like other calibrators recompute from their data.reset()restores the calibration window without retraining, so evaluations can be rerun from the same start.state_dict()saves the whole predictor, including the bootstrap indices and the residual window, through PyTorch's extra state hooks, soload_state_dictandTrainercheckpoints restore calibrated inference and resume online updates. The hooks sit in a separate checkpointing section at the end ofEnbPI, apart from the algorithm.calibrateraises when no training point has an out-of-bag model, since every interval would be NaN. The paper does not address this case, and with the recommendedB = 20to30it essentially never happens.regressionmode with one stream in time order and absolute residuals, as in the ICML paper. Many series at once (timeseries_regression) and signed residuals are follow-ups.