Metadata-Version: 2.5
Name: lagkit
Version: 0.1.0
Summary: Sparse Bayesian regression on lagged functional features, with leakage-free grouped cross-validation.
Project-URL: Homepage, https://github.com/ProjeetBhaumik/lagkit
Project-URL: Issues, https://github.com/ProjeetBhaumik/lagkit/issues
Author: Projeet Bhaumik
License: MIT License
        
        Copyright (c) 2026 Projeet Bhaumik
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: bayesian,cross-validation,functional-data-analysis,horseshoe,quantitative-finance,time-lagged-features,time-series,wearables
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Requires-Dist: arviz>=0.17
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: patsy>=0.5
Requires-Dist: pymc>=5.10
Requires-Dist: pyyaml>=6.0
Requires-Dist: scikit-learn>=1.3
Provides-Extra: dev
Requires-Dist: matplotlib>=3.7; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Provides-Extra: plots
Requires-Dist: matplotlib>=3.7; extra == 'plots'
Description-Content-Type: text/markdown

# lagkit

**Sparse Bayesian regression on lagged functional features, with leakage-free grouped cross-validation.**

For data where each observation carries a *function sampled on an ordered axis* — a history of lagged measurements, a term structure, a spectrum — and two questions matter at once:

1. How well can the outcome be predicted?
2. Which positions on the axis carried the signal?

Treating each lag-feature pair as an independent covariate answers neither. `lagkit` smooths across the axis, shrinks selectively, validates without leaking, and maps coefficients back onto the axis.

```bash
pip install lagkit
```

Requires Python ≥ 3.10.

---

## Usage

```python
from lagkit import LagSpec, LaggedFeatureUnion, GroupedHorseshoe, critical_windows

spec  = LagSpec(max_lag=72)                     # feature_1h ... feature_72h
union = LaggedFeatureUnion(["cough", "breathingRate"], spec)

X_train = union.fit_transform(train_df)         # 144 lag columns -> ~6 scores
model   = GroupedHorseshoe(groups=union.groups_).fit(X_train, y_train)

y_hat = model.predict(union.transform(test_df))

curves  = union.coefficients_to_lag_domain(model.beta_draws_)
windows = critical_windows(curves["cough"], spec.lags)
```

`windows` is a per-lag table: posterior mean effect, 95% band, and flags for positions where the posterior puts ≥95% of its mass on one side of zero.

---

## Components

| Component | Purpose |
|---|---|
| [`LagSpec`](src/lagkit/lags.py) | Addresses wide `feature_24h` tables. Set `template` to `{feature}_lag{lag}` and downstream code is unaffected. |
| [`WidthPreservingKNNImputer`](src/lagkit/impute.py) | `KNNImputer` silently drops all-missing columns. For a trajectory about to be projected onto a fixed basis, a missing position must be restored and filled from its neighbours, not removed. |
| [`SplineFPCA`](src/lagkit/fpca.py) | B-spline projection then PCA: many positions to a few latent scores, enforcing smoothness across the axis. |
| [`LaggedFeatureUnion`](src/lagkit/fpca.py) | One `SplineFPCA` per feature, concatenated, tracking which columns belong to which feature. |
| [`GroupedHorseshoe`](src/lagkit/horseshoe.py) | Regularised horseshoe with a shrinkage scale per group, so a feature switches on or off as a whole. Global scale calibrated from the expected number of relevant coefficients. |
| [`GatedResidualExpert`](src/lagkit/residual.py) | Gates boosted-tree corrections by local support, so test points far from training data are not confidently "corrected". |
| [`crossval`](src/lagkit/crossval.py) | Outer leave-one-group-out, inner CV for tuning, cross-fitted residuals. |
| [`critical_windows`](src/lagkit/windows.py) | Posterior effect curves to credible intervals over the axis. |
| [`ThresholdGrader`](src/lagkit/grading.py) | Continuous prediction to ordinal grade, with class probabilities from the posterior. GOLD staging ships as a preset. |

---

## Cross-validation

The CV helpers take a callback that rebuilds the model from index subsets, rather than a pre-built design matrix:

```python
def fit_predict(fit_idx, val_idx, fold_index):
    ...  # refit imputation, splines, PCA, scaling AND the model on fit_idx alone
    return predictions_for_val_idx

oof = cross_fitted_predictions(len(train_df), fit_predict, n_splits=5)
```

Fitting the representation once outside the loop leaks validation rows into the imputer, the PCA and the scalers, inflating every score that follows. `run_experiment` applies three layers automatically:

- **Outer leave-one-group-out** — no group informs its own prediction.
- **Inner CV within the outer training set** — the held-out group plays no part in hyperparameter selection.
- **Cross-fitted residuals** — the residual stage trains on out-of-fold residuals, not in-sample ones.

A group is whatever must not be split: a subject, a site, a contiguous time block.

---

## Experiment runner

```python
from lagkit import ExperimentConfig
from lagkit.experiment import run_experiment

config = ExperimentConfig(
    path="cohort.csv",
    target="FEV1",
    group_col="subject_id",
    features=["cough", "breathingRate", "ie_ratio"],
    sparse_features=["cough"],          # nearest-lag carry, not interpolation
    max_lag=72,
    use_residual_expert=True,
    out_dir="outputs",
)

result = run_experiment(config)
result.save()
```

From the shell:

```bash
lagkit template > config.yaml
```

```bash
lagkit run config.yaml --set target="FVC obs" --out-dir results/fvc
```

`run_experiment` is a convenience layer; every primitive composes by hand.

---

## Results

`result.summary` reports three things together, because point accuracy alone is a poor summary when the number of groups is small:

- **accuracy** — `r2`, `rmse`, `mae`, `pearson_r`, plus `*_backbone_only` so the residual stage has to justify itself;
- **interval honesty** — `coverage95_predictive` against the nominal 0.95;
- **calibration** — `pred_on_true_slope`. Well below 1 means predictions are compressed towards the mean; this can coexist with a respectable R².

Convergence is reported per fold. `divergences_total > 0` or `rhat_max > 1.01` means the posterior is not trustworthy regardless of predictive accuracy.

---

## Applicability

The axis need not be time and the group need not be a person. Two worked examples:

- [`examples/quickstart.py`](examples/quickstart.py) — lagged sensor features across a cohort; the axis is hours, the group is a subject.
- [`examples/vix_term_structure.py`](examples/vix_term_structure.py) — an implied-volatility term structure; the axis is option tenor, the group is a contiguous block of trading days.

The same applies wherever observations are curves over an ordered index: spectra over wavelength, load profiles over hour of day, dose-response over concentration.

No domain-specific API is included, and none should be added. A domain helper inside the library means it has been forked in practice and belongs to its caller.

---

## Development

```bash
pip install -e ".[dev]"
```

```bash
pytest -m "not slow"
```

The `slow` marker covers tests that run MCMC. Run the full suite with plain `pytest`.

## Citation

Developed for an honours project on predicting spirometry from wearable respiratory sensing. If you use it, please cite the repository.

## License

MIT
