h2ofm 0.42.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
h2ofm/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ # Copyright 2026 H2O.ai, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from importlib.metadata import version
5
+
6
+ from . import timeh2o
7
+ from .timeh2o import TimeH2O
8
+
9
+ __version__ = version("h2ofm")
10
+ __all__ = ["TimeH2O", "timeh2o", "__version__"]
h2ofm/py.typed ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ # Copyright 2026 H2O.ai, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from ._api import Forecast, TimeH2O
5
+
6
+ __all__ = ["Forecast", "TimeH2O"]
h2ofm/timeh2o/_api.py ADDED
@@ -0,0 +1,630 @@
1
+ # Copyright 2026 H2O.ai, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import math
7
+ from collections.abc import Sequence
8
+ from dataclasses import dataclass
9
+ from numbers import Integral, Real
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+ import torch
15
+ from pandas.api import types as ptypes
16
+
17
+ from ._checkpoints import load_checkpoint, locate_checkpoint
18
+ from ._data.preprocess import from_arrays
19
+ from ._model.config import TimeH2OConfig
20
+ from ._pipeline.df_utils import (
21
+ ITEM,
22
+ ROW_ORDER_FREQ,
23
+ STEP,
24
+ TIME,
25
+ datetimes_to_integer_times,
26
+ infer_freq_from_df,
27
+ is_integer_time_column,
28
+ prepare_frames,
29
+ )
30
+ from ._pipeline.pipeline import TimeH2OPipeline
31
+ from ._pipeline.postprocess import clip_frame, clip_targets, nonnegative_targets
32
+ from ._pipeline.tta import blend_items, reflect_arrays
33
+ from ._pipeline.utils import extrapolate_quantiles, validate_quantile_levels
34
+ from ._warnings import warn
35
+
36
+ Array = np.ndarray | torch.Tensor
37
+ DEFAULT_QUANTILES = tuple(i / 10 for i in range(1, 10))
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Forecast:
42
+ """The forecast of one input item, as CPU NumPy arrays.
43
+
44
+ A 1D history gives ``point`` shaped (horizon,) and ``quantiles`` shaped
45
+ (horizon, levels); a 2D (targets, history) item keeps its target axis,
46
+ (targets, horizon) and (targets, horizon, levels). ``point`` holds medians,
47
+ not means. The last quantile axis follows ``quantile_levels``, in the
48
+ order they were requested.
49
+ """
50
+
51
+ point: np.ndarray
52
+ quantiles: np.ndarray
53
+ quantile_levels: tuple[float, ...]
54
+
55
+ @property
56
+ def median(self) -> np.ndarray:
57
+ """The point forecast under its precise name; the same array as
58
+ ``point``."""
59
+ return self.point
60
+
61
+ def quantile(self, level: float) -> np.ndarray:
62
+ """The forecast at one requested quantile level, shaped like
63
+ ``point``. Raises KeyError for a level that was not requested."""
64
+ if isinstance(level, bool) or not isinstance(level, Real):
65
+ raise TypeError(
66
+ f"quantile level must be a number, e.g. quantile(0.9); got {level!r}"
67
+ )
68
+ for index, requested in enumerate(self.quantile_levels):
69
+ if math.isclose(requested, level, rel_tol=0.0, abs_tol=1e-9):
70
+ return self.quantiles[..., index]
71
+ raise KeyError(
72
+ f"quantile level {level} was not requested; "
73
+ f"available: {list(self.quantile_levels)}"
74
+ )
75
+
76
+
77
+ def _positive_int(value, name):
78
+ if isinstance(value, bool) or not isinstance(value, Integral) or value < 1:
79
+ raise ValueError(f"{name} must be a positive integer")
80
+ return int(value)
81
+
82
+
83
+ def _flag(value, name):
84
+ if not isinstance(value, bool):
85
+ raise ValueError(f"{name} must be a boolean")
86
+ return value
87
+
88
+
89
+ def _real_tensor(item, what: str) -> torch.Tensor:
90
+ if isinstance(item, torch.Tensor):
91
+ if item.is_complex():
92
+ raise ValueError(f"{what} must contain real numeric values")
93
+ return item.detach().to(device="cpu", dtype=torch.float32).clone()
94
+ array = np.asarray(item)
95
+ if array.dtype.kind not in "biuf":
96
+ raise ValueError(f"{what} must contain real numeric values")
97
+ return torch.tensor(np.array(array, dtype=np.float32, copy=True))
98
+
99
+
100
+ def _series(item) -> tuple[torch.Tensor, bool]:
101
+ """One series as a (rows, history) tensor, and whether it was 1D."""
102
+ if isinstance(item, pd.DataFrame):
103
+ raise TypeError("use forecast_df() for dataframes")
104
+ if isinstance(item, (str, bytes)):
105
+ raise TypeError("A series must be an array, tensor or sequence of numbers")
106
+ item = _real_tensor(item, "Series")
107
+ if item.ndim not in (1, 2) or any(size == 0 for size in item.shape):
108
+ raise ValueError(
109
+ "A series must have nonempty shape (history,) or (targets, history); "
110
+ "use forecast_many() for several series"
111
+ )
112
+ if torch.isinf(item).any():
113
+ raise ValueError(
114
+ "Series may contain NaN for missing observations, but not infinity"
115
+ )
116
+ return item.reshape(-1, item.shape[-1]), item.ndim == 1
117
+
118
+
119
+ def _batch(contexts) -> tuple[list[torch.Tensor], list[bool]]:
120
+ """Each series of a batch: a sequence, or an array iterated along axis 0."""
121
+ if isinstance(contexts, pd.DataFrame):
122
+ raise TypeError("use forecast_df() for dataframes")
123
+ if isinstance(contexts, (np.ndarray, torch.Tensor)):
124
+ if contexts.ndim not in (2, 3):
125
+ raise ValueError(
126
+ "An array of contexts must be 2D (series, history) or 3D "
127
+ "(series, targets, history); use forecast() for one series"
128
+ )
129
+ items = list(contexts)
130
+ elif isinstance(contexts, Sequence) and not isinstance(contexts, (str, bytes)):
131
+ if contexts and all(isinstance(v, Real) for v in contexts):
132
+ raise TypeError(
133
+ "contexts is a single series of numbers; use forecast() for one "
134
+ "series, or wrap it in a list"
135
+ )
136
+ items = list(contexts)
137
+ else:
138
+ raise TypeError("contexts must be a sequence of series")
139
+ if not items:
140
+ raise ValueError("contexts must contain at least one series")
141
+ series = [_series(item) for item in items]
142
+ return [t for t, _ in series], [u for _, u in series]
143
+
144
+
145
+ _ROW_TYPES = (
146
+ np.ndarray,
147
+ torch.Tensor,
148
+ pd.Series,
149
+ pd.Index,
150
+ pd.Categorical,
151
+ list,
152
+ tuple,
153
+ )
154
+
155
+
156
+ def _covariate_row(values, name: str) -> torch.Tensor | np.ndarray:
157
+ """Convert non-boolean numeric rows to float32; keep other dtypes as labels."""
158
+ if isinstance(values, torch.Tensor):
159
+ values = values.detach().cpu()
160
+ if values.is_complex():
161
+ raise ValueError(f"{name} must contain real numbers or labels")
162
+ values = (values if values.dtype == torch.bool else values.float()).numpy()
163
+ if isinstance(values, np.ndarray):
164
+ if values.dtype.kind == "c":
165
+ raise ValueError(f"{name} must contain real numbers or labels")
166
+ if values.dtype.kind not in "iuf":
167
+ return values.astype(object)
168
+ row = torch.from_numpy(np.array(values, dtype=np.float32))
169
+ else:
170
+ series = values if isinstance(values, pd.Series) else pd.Series(values)
171
+ if ptypes.is_complex_dtype(series):
172
+ raise ValueError(f"{name} must contain real numbers or labels")
173
+ if not ptypes.is_numeric_dtype(series) or ptypes.is_bool_dtype(series):
174
+ return series.to_numpy(dtype=object)
175
+ row = torch.from_numpy(
176
+ series.to_numpy(dtype=np.float32, na_value=np.nan, copy=True)
177
+ )
178
+ if torch.isinf(row).any():
179
+ raise ValueError(f"{name} may contain NaN, but not infinity")
180
+ return row
181
+
182
+
183
+ def _covariate_items(covariates, items, name, extra) -> list[list | None]:
184
+ """Parse covariate rows per item, spanning history plus extra steps."""
185
+ span = "history + horizon" if extra else "history"
186
+ if covariates is None:
187
+ return [None] * len(items)
188
+ if isinstance(covariates, (str, bytes)) or not isinstance(
189
+ covariates, (Sequence, np.ndarray, torch.Tensor)
190
+ ):
191
+ raise TypeError(f"{name} must be a sequence with one entry per item")
192
+ if len(covariates) != len(items):
193
+ raise ValueError(f"{name} has {len(covariates)} entries for {len(items)} items")
194
+ result = []
195
+ for index, (entry, item) in enumerate(zip(covariates, items, strict=True)):
196
+ if entry is None:
197
+ result.append(None)
198
+ continue
199
+ length = item.shape[-1] + extra
200
+ if isinstance(entry, (np.ndarray, torch.Tensor)) and entry.ndim == 2:
201
+ parts, shape = list(entry), tuple(entry.shape)
202
+ elif isinstance(entry, (np.ndarray, torch.Tensor)):
203
+ parts, shape = ([entry] if entry.ndim == 1 else None), tuple(entry.shape)
204
+ elif isinstance(entry, (pd.Series, pd.Index, pd.Categorical)):
205
+ parts, shape = [entry], (len(entry),)
206
+ elif isinstance(entry, Sequence) and not isinstance(entry, (str, bytes)):
207
+ rows = bool(entry) and all(isinstance(r, _ROW_TYPES) for r in entry)
208
+ parts = list(entry) if rows else [entry]
209
+ shape = (len(parts), len(parts[0])) if rows else (len(entry),)
210
+ else:
211
+ raise TypeError(f"{name}[{index}] must be an array or a sequence of rows")
212
+ if (
213
+ parts is None
214
+ or shape[-1:] != (length,)
215
+ or any(len(row) != length for row in parts)
216
+ ):
217
+ lengths = [len(row) for row in parts or ()]
218
+ got = f"rows of lengths {lengths}" if len(set(lengths)) > 1 else shape
219
+ raise ValueError(
220
+ f"{name}[{index}] must have shape ({span},) or "
221
+ f"(covariates, {span}) with {span} = {length}; got {got}"
222
+ )
223
+ rows = [_covariate_row(row, name) for row in parts]
224
+ result.append(rows or None)
225
+ return result
226
+
227
+
228
+ def _warn_past_covariates_ignored(what: str) -> None:
229
+ warn(
230
+ f"This checkpoint does not use past covariates; {what} are validated "
231
+ "and ignored. Pass covariates known through the horizon as future "
232
+ "covariates instead."
233
+ )
234
+
235
+
236
+ class TimeH2O:
237
+ """A loaded TimeH2O checkpoint.
238
+
239
+ Construct with :meth:`from_pretrained` and reuse the instance; nothing is
240
+ cached globally. :meth:`forecast` takes one series, :meth:`forecast_many`
241
+ several and :meth:`forecast_df` a long-format dataframe.
242
+ """
243
+
244
+ def __init__(self, pipeline: TimeH2OPipeline, batch_size: int = 256):
245
+ self._pipeline = pipeline
246
+ self._batch_size = _positive_int(batch_size, "batch_size")
247
+
248
+ @classmethod
249
+ def from_pretrained(
250
+ cls,
251
+ pretrained_model_name_or_path: str | Path,
252
+ *,
253
+ device: str | torch.device = "auto",
254
+ revision: str | None = None,
255
+ token: str | bool | None = None,
256
+ cache_dir: str | Path | None = None,
257
+ local_files_only: bool = False,
258
+ batch_size: int = 256,
259
+ ) -> TimeH2O:
260
+ """Load a checkpoint: a directory with config.json and model.safetensors.
261
+
262
+ ``pretrained_model_name_or_path`` is that directory, the
263
+ model.safetensors file inside it, or a Hugging Face model ID such as
264
+ ``"h2oai/timeh2o-v1-tiny"``, as in transformers. A path that
265
+ exists locally takes precedence over a model ID. From the Hub only config.json and
266
+ model.safetensors are fetched, into the Hugging Face cache;
267
+ ``revision``, ``token``, ``cache_dir`` and ``local_files_only`` apply
268
+ to that download only. config.json defines the architecture; every
269
+ weight must match it. Pickles and remote code are never loaded.
270
+ 'auto' selects CUDA when available, otherwise CPU. All inference uses
271
+ float32 weights. ``batch_size`` caps the rows (targets and covariates
272
+ of whole series; targets only without cross-variate attention) the
273
+ model runs at once; it changes speed and memory, not forecasts.
274
+ """
275
+ path = locate_checkpoint(
276
+ pretrained_model_name_or_path,
277
+ revision=revision,
278
+ token=token,
279
+ cache_dir=cache_dir,
280
+ local_files_only=local_files_only,
281
+ )
282
+ return cls(TimeH2OPipeline(load_checkpoint(path, device)), batch_size)
283
+
284
+ @property
285
+ def config(self) -> TimeH2OConfig:
286
+ """The checkpoint's validated config.json."""
287
+ return self._pipeline.model.config
288
+
289
+ @property
290
+ def device(self) -> torch.device:
291
+ return self._pipeline.model.device
292
+
293
+ @property
294
+ def batch_size(self) -> int:
295
+ """The most rows run at once; set in :meth:`from_pretrained`."""
296
+ return self._batch_size
297
+
298
+ def _options(self, horizon, quantile_levels, tta, clip, context, ar):
299
+ horizon = _positive_int(horizon, "horizon")
300
+ flip = _flag(tta, "flip_equivariant") and not self.config.flip_equivariant
301
+ _flag(clip, "clip_nonnegative")
302
+ if ar is not None and not isinstance(ar, bool):
303
+ raise ValueError("autoregressive must be a boolean or None")
304
+ levels = [float(q) for q in quantile_levels]
305
+ validate_quantile_levels(levels)
306
+ if len(set(levels)) != len(levels):
307
+ raise ValueError("quantile_levels must not contain duplicates")
308
+ options = dict(
309
+ batch_size=self._batch_size,
310
+ context_length=(
311
+ None if context is None else _positive_int(context, "context_length")
312
+ ),
313
+ autoregressive=ar,
314
+ )
315
+ return horizon, levels, flip, options
316
+
317
+ @torch.no_grad()
318
+ def forecast(
319
+ self,
320
+ context: Array | Sequence[float],
321
+ horizon: int,
322
+ *,
323
+ past_covariates: Array | Sequence[float] | None = None,
324
+ future_covariates: Array | Sequence[float] | None = None,
325
+ quantile_levels: Sequence[float] = DEFAULT_QUANTILES,
326
+ flip_equivariant: bool = False,
327
+ clip_nonnegative: bool = False,
328
+ context_length: int | None = None,
329
+ autoregressive: bool | None = None,
330
+ ) -> Forecast:
331
+ """Forecast one series ``horizon`` steps ahead.
332
+
333
+ ``context`` is a 1D (history,) series or a 2D (targets, history)
334
+ multi-target series. ``past_covariates`` and
335
+ ``future_covariates`` are that series' covariates, shaped as described
336
+ in :meth:`forecast_many`. Returns a :class:`Forecast`; every option
337
+ behaves as in :meth:`forecast_many`.
338
+ """
339
+ target, univariate = _series(context)
340
+ return self._forecast(
341
+ [target],
342
+ [univariate],
343
+ [past_covariates],
344
+ [future_covariates],
345
+ horizon,
346
+ quantile_levels,
347
+ flip_equivariant,
348
+ clip_nonnegative,
349
+ context_length,
350
+ autoregressive,
351
+ )[0]
352
+
353
+ @torch.no_grad()
354
+ def forecast_many(
355
+ self,
356
+ contexts: Sequence[Array | Sequence[float]] | Array,
357
+ horizon: int,
358
+ *,
359
+ past_covariates: Sequence[Array | Sequence[float] | None] | None = None,
360
+ future_covariates: Sequence[Array | Sequence[float] | None] | None = None,
361
+ quantile_levels: Sequence[float] = DEFAULT_QUANTILES,
362
+ flip_equivariant: bool = False,
363
+ clip_nonnegative: bool = False,
364
+ context_length: int | None = None,
365
+ autoregressive: bool | None = None,
366
+ ) -> list[Forecast]:
367
+ """Forecast several series ``horizon`` steps ahead.
368
+
369
+ ``contexts`` is a sequence of series, each a 1D (history,) series or a
370
+ 2D (targets, history) multi-target series;
371
+ histories may differ in length. A 2D array is a batch of 1D series and
372
+ a 3D array a batch of 2D series. NaN marks a missing observation; an
373
+ all-missing target is a zero-valued cold start. Returns one
374
+ :class:`Forecast` per series, in input order.
375
+
376
+ ``past_covariates`` and ``future_covariates`` hold one entry per
377
+ series: None, or its covariates, with NaN for a missing value. Past
378
+ covariates are observed over the history only, shaped (history,) or
379
+ (covariates, history). The timeh2o-v1 checkpoints do not use them:
380
+ they are validated, then ignored. Future covariates are
381
+ observed over the history and known through the horizon, shaped
382
+ (history + horizon,) or (covariates, history + horizon). An entry
383
+ may also be a sequence of such rows. A row with a numeric,
384
+ non-boolean dtype is numeric; any other row (strings, objects,
385
+ booleans, categoricals) holds labels, encoded as :meth:`forecast_df`
386
+ encodes a categorical column, with None or NaN as a missing label.
387
+
388
+ ``flip_equivariant=True``: flipping a history upside down (covariates
389
+ included) flips its forecast. Free for checkpoints that are
390
+ flip-equivariant by design (``config.flip_equivariant``); others
391
+ average with a flipped forecast, which roughly doubles inference time.
392
+ ``clip_nonnegative=True`` clamps forecasts at zero for targets with a
393
+ nonnegative history; it runs last, after the flip blend and quantile
394
+ extrapolation.
395
+
396
+ ``quantile_levels`` outside the trained 0.1..0.9 grid use
397
+ Gaussian-tail extrapolation. ``context_length`` may shorten the
398
+ checkpoint's context.
399
+ ``autoregressive=True`` decodes horizons beyond the checkpoint's
400
+ max_horizon in blocks, False in a single pass; None follows the
401
+ checkpoint's saved default.
402
+ """
403
+ targets, univariate = _batch(contexts)
404
+ size = len(targets)
405
+ return self._forecast(
406
+ targets,
407
+ univariate,
408
+ [None] * size if past_covariates is None else past_covariates,
409
+ [None] * size if future_covariates is None else future_covariates,
410
+ horizon,
411
+ quantile_levels,
412
+ flip_equivariant,
413
+ clip_nonnegative,
414
+ context_length,
415
+ autoregressive,
416
+ )
417
+
418
+ def _forecast(
419
+ self,
420
+ targets,
421
+ univariate,
422
+ past_covariates,
423
+ future_covariates,
424
+ horizon,
425
+ quantile_levels,
426
+ flip_equivariant,
427
+ clip_nonnegative,
428
+ context_length,
429
+ autoregressive,
430
+ ) -> list[Forecast]:
431
+ """Forecast parsed series; the covariates hold one entry per series."""
432
+ horizon, levels, flip, options = self._options(
433
+ horizon,
434
+ quantile_levels,
435
+ flip_equivariant,
436
+ clip_nonnegative,
437
+ context_length,
438
+ autoregressive,
439
+ )
440
+ past = _covariate_items(past_covariates, targets, "past_covariates", 0)
441
+ if any(entry is not None for entry in past):
442
+ _warn_past_covariates_ignored("past_covariates")
443
+ covariates = _covariate_items(
444
+ future_covariates, targets, "future_covariates", horizon
445
+ )
446
+ predictions = self._predict_arrays(targets, covariates, horizon, options)
447
+ if flip:
448
+ reflected = [
449
+ reflect_arrays(target, known)
450
+ for target, known in zip(targets, covariates, strict=True)
451
+ ]
452
+ predictions = blend_items(
453
+ predictions,
454
+ self._predict_arrays(
455
+ [target for target, _, _ in reflected],
456
+ [known for _, known, _ in reflected],
457
+ horizon,
458
+ options,
459
+ ),
460
+ [means for _, _, means in reflected],
461
+ )
462
+ native = self._pipeline.quantiles
463
+ # One call for all items: quantile maps act row by row.
464
+ extrapolated = extrapolate_quantiles(
465
+ levels, native, torch.cat(predictions).transpose(1, 2)
466
+ ).numpy()
467
+ results, start = [], 0
468
+ for prediction, target, squeeze in zip(
469
+ predictions, targets, univariate, strict=True
470
+ ):
471
+ point = prediction[:, native.index(0.5)].numpy()
472
+ stop = start + prediction.shape[0]
473
+ quantiles = extrapolated[start:stop].copy()
474
+ start = stop
475
+ if clip_nonnegative:
476
+ clamp = nonnegative_targets(target)
477
+ point = clip_targets(point, clamp)
478
+ quantiles = clip_targets(quantiles, clamp)
479
+ if squeeze:
480
+ point, quantiles = point[0], quantiles[0]
481
+ results.append(Forecast(point, quantiles, tuple(levels)))
482
+ return results
483
+
484
+ def _predict_arrays(self, targets, covariates, horizon, options):
485
+ """Return (targets, trained quantiles, horizon) forecasts per series."""
486
+ items = from_arrays(targets, covariates, horizon)
487
+ flat = self._pipeline.predict(
488
+ [item for parts in items for item in parts], horizon, **options
489
+ )
490
+ joined, cursor = [], 0
491
+ for parts in items:
492
+ joined.append(torch.cat(flat[cursor : cursor + len(parts)]))
493
+ cursor += len(parts)
494
+ return joined
495
+
496
+ @torch.no_grad()
497
+ def forecast_df(
498
+ self,
499
+ context: pd.DataFrame,
500
+ horizon: int | pd.DataFrame,
501
+ *,
502
+ target: str | list[str] = "target",
503
+ id_column: str | None = None,
504
+ time_column: str | None = None,
505
+ freq: str | int | None = None,
506
+ quantile_levels: Sequence[float] = DEFAULT_QUANTILES,
507
+ flip_equivariant: bool = False,
508
+ clip_nonnegative: bool = False,
509
+ context_length: int | None = None,
510
+ autoregressive: bool | None = None,
511
+ ) -> pd.DataFrame:
512
+ """Forecast from historical rows, ``horizon`` steps or rows ahead.
513
+
514
+ ``context`` contains only history, including any missing observations.
515
+ ``horizon`` is the number of steps to forecast per item, or a frame of
516
+ horizon rows. In a horizon frame, target columns must be absent or
517
+ entirely missing; every covariate column it has is future-known, even
518
+ when all its horizon values are missing, while covariates absent from
519
+ it are past-only and ignored by v1. It must have the same items as
520
+ ``context`` and an equal number of rows per item, which sets the
521
+ horizon. With an integer ``horizon``, every covariate is past-only.
522
+
523
+ ``target`` names one target column or a list of target columns. Omit
524
+ ``id_column`` for one series, and ``time_column`` to use per-item row order.
525
+ The time column may contain datetimes, datetime strings, or integer
526
+ values representing time steps; integer times are never converted to
527
+ dates. Supplied future times must continue history at its inferred
528
+ frequency, or ``freq`` when supplied: a pandas offset alias for
529
+ datetimes, the integer step between time values for integer times.
530
+ Inputs are never mutated.
531
+
532
+ Returns ``id_column`` (when given), ``time_column`` or ``step`` (1 to
533
+ ``horizon``), then ``target_name``, ``predictions`` (the median), and one
534
+ column per requested quantile, named like ``"0.1"``. Augmentation and
535
+ decoding options behave as in :meth:`forecast`; categorical and boolean
536
+ covariates remain unchanged by the flip blend.
537
+ """
538
+ horizon_df = horizon if isinstance(horizon, pd.DataFrame) else None
539
+ if horizon_df is None:
540
+ if not isinstance(horizon, Real):
541
+ raise TypeError(
542
+ "horizon must be a positive integer or a pandas dataframe"
543
+ )
544
+ horizon = _positive_int(horizon, "horizon")
545
+ targets = [target] if isinstance(target, str) else target
546
+ history, future, frame_horizon, items = prepare_frames(
547
+ context, horizon_df, id_column, time_column, targets
548
+ )
549
+ dropped = [
550
+ c
551
+ for c in context.columns
552
+ if c not in (id_column, time_column, *targets) and c not in history
553
+ ]
554
+ if dropped:
555
+ _warn_past_covariates_ignored(f"past-only columns {dropped}")
556
+ if horizon_df is not None:
557
+ horizon = frame_horizon
558
+ horizon, levels, flip, options = self._options(
559
+ horizon,
560
+ quantile_levels,
561
+ flip_equivariant,
562
+ clip_nonnegative,
563
+ context_length,
564
+ autoregressive,
565
+ )
566
+ integer_time = time_column is not None and is_integer_time_column(
567
+ context[time_column]
568
+ )
569
+ if freq is not None and time_column is None:
570
+ raise ValueError("freq needs a time_column")
571
+ if isinstance(freq, bool) or (
572
+ integer_time and not isinstance(freq, int | None)
573
+ ):
574
+ raise ValueError("freq must be an integer step for an integer time column")
575
+ if isinstance(freq, int):
576
+ if not integer_time:
577
+ raise ValueError("an integer freq needs an integer time column")
578
+ freq = f"{_positive_int(freq, 'freq')}s"
579
+ output_names = {str(q) for q in levels}
580
+ if any(
581
+ isinstance(c, str) and c in output_names for c in (id_column, time_column)
582
+ ):
583
+ raise ValueError("key columns must not use quantile output names")
584
+ if time_column is None:
585
+ freq = ROW_ORDER_FREQ
586
+ elif freq is None:
587
+ freq = infer_freq_from_df(history, items, ITEM, TIME)
588
+ grid = self._pipeline.quantiles if flip else levels
589
+ predictions = self._pipeline.predict_df(
590
+ history,
591
+ future_df=future,
592
+ id_column=ITEM,
593
+ time_column=TIME,
594
+ target=targets,
595
+ horizon=horizon,
596
+ quantile_levels=grid,
597
+ freq=freq,
598
+ tta=flip,
599
+ **options,
600
+ )
601
+ if flip:
602
+ values = torch.from_numpy(predictions[[str(q) for q in grid]].to_numpy())
603
+ requested = extrapolate_quantiles(levels, grid, values).numpy()
604
+ for i, level in enumerate(levels):
605
+ predictions[str(level)] = requested[:, i]
606
+ if clip_nonnegative:
607
+ predictions = clip_frame(predictions, history, ITEM, targets, levels)
608
+ if time_column is None:
609
+ steps = predictions.groupby([ITEM, "target_name"], sort=False).cumcount()
610
+ predictions[STEP] = steps + 1
611
+ elif integer_time:
612
+ predictions[TIME] = datetimes_to_integer_times(predictions[TIME])
613
+ if id_column is not None:
614
+ predictions[ITEM] = items[predictions[ITEM].to_numpy()]
615
+ renames = {ITEM: id_column, TIME: time_column}
616
+ predictions = predictions.rename(
617
+ columns={k: v for k, v in renames.items() if v is not None}
618
+ )
619
+ keys = [
620
+ *([] if id_column is None else [id_column]),
621
+ STEP if time_column is None else time_column,
622
+ ]
623
+ return predictions[
624
+ [
625
+ *keys,
626
+ "target_name",
627
+ "predictions",
628
+ *(str(q) for q in levels),
629
+ ]
630
+ ]