Metadata-Version: 2.5
Name: floodgb
Version: 0.3.0
Summary: A flood model
Author-email: Mike Kittridge <mullenkamp1@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: flood,water
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Requires-Dist: booklet>=0.12
Requires-Dist: matplotlib
Requires-Dist: numpy>=1.24
Requires-Dist: orjson
Requires-Dist: pandas>=2.2
Requires-Dist: pyarrow
Requires-Dist: scikit-learn>=1.3.1
Requires-Dist: scipy
Requires-Dist: xarray
Requires-Dist: zstandard
Provides-Extra: dev
Requires-Dist: coverage[toml]>=6.5; extra == 'dev'
Requires-Dist: h5netcdf; extra == 'dev'
Requires-Dist: h5py; extra == 'dev'
Requires-Dist: matplotlib; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: spyder-kernels==2.5.2; extra == 'dev'
Description-Content-Type: text/x-rst

FloodGB
=======

Multi-horizon river flow forecasting: gradient boosting over lagged rainfall and antecedent flow.

FloodGB is **lag engineering plus multi-horizon orchestration** around any scikit-learn regressor,
not a model of its own. You supply the estimator; it builds the lagged feature matrix, trains one
model per forecast lead hour, and fills the forecast window from the last observation forward.

It is built for **floods**. Evaluation is weighted toward flood peaks by design, because
whole-record error is dominated by baseflow that antecedent flow already tracks — and that is not
the part a flood forecast exists to get right.

Quick start
-----------

.. code-block:: python

    from floodgb import FloodGB
    from sklearn.ensemble import HistGradientBoostingRegressor
    from sklearn.pipeline import make_pipeline

    lags = {
        rain_id:  (1, 48, 'sum'),    # DRIVER: one column per lag in [start, end]
        river_id: (72, 24, 'mean'),  # TARGET: (fore_len, target_period, agg)
    }                                # fore_len is the number of models trained

    m = FloodGB()
    m.set_params([rain_id, river_id], river_id, 'h', lags)
    m.set_model(make_pipeline(HistGradientBoostingRegressor(
        loss='squared_error', max_iter=100, learning_rate=0.05, early_stopping=False)))
    m.train_models([precip, flow], break_time='2015-07-31',
                   variables=['precipitation', 'streamflow'])
    all_results, hf_results, max_results, predictions = m.test_models()
    m.export('model.blt')

    # later, operationally
    forecast = FloodGB('model.blt').predict([precip, flow],
                                            variables=['precipitation', 'streamflow'])

Input is a list of ``xarray.Dataset`` with ``time`` and ``station_id``; name the measurement with
``variables=``. Datasets are flattened to one wide frame and merged by station id, so rainfall and
flow arrive separately.

Choosing which gauges to use
----------------------------

``floodgb.selection`` answers the operational question: how few rain gauges can the model depend
on? Every extra gauge is another telemetry feed that can fail during the storm the model exists to
forecast.

.. code-block:: python

    from floodgb import evaluate_subsets

    res = evaluate_subsets(model, [precip, flow], river_id, candidate_ids, lags, 'h',
                           time_steps=[6, 24, 72], break_times=['2011-07-01', '2014-07-01'],
                           variables=['precipitation', 'streamflow'])
    res.frontier()     # best subset at each size, per lead, with ties shown

It fits a model for **every subset** of the candidates and scores them all on one shared evaluation
set. That is deliberately more expensive than ranking gauges by importance — and necessary, because
no per-gauge score can express *"these three are watching the same storm"*. Rank and truncate, and
you reliably select near-duplicates of one signal while discarding the independent gauges carrying
the rest of it.

Three things it handles that a hand-rolled sweep usually does not:

* **Fabricated zeros.** Resampling rainfall with ``sum`` turns every gap — including a gauge's
  entire pre-record span — into *measured zero rainfall*, not missing data. Those rows are removed.
* **Comparability.** Every subset is scored on the same rows, asserted at runtime.
* **The evaluation window is computed, never hardcoded.** A recorded window is only correct for the
  station set it was computed over, and station sets change.

Read the frontier **per lead**, and pass **several** ``break_times``: on real records the winning
subset moves with the forecast horizon, and with the train/test split.

Requirements
------------

Python ≥ 3.11, pandas ≥ 2.2 (tested to 3.0), scikit-learn ≥ 1.3.1. Exported artifacts embed the
training frame and hold pickled estimators, so pin scikit-learn in any environment that loads them.

License
-------

Apache-2.0.
