Source code for emflow.data.dataset

"""Dataset: a named collection of :class:`Field` s (+ optional asset collection).

A dataset can be built in code (fields passed directly) or loaded lazily from a
Rebase-compatible Hugging Face repo via :meth:`Dataset.from_manifest`, where the
repo's ``rebase.yaml`` declares each field's parquet file and availability rule::

    name: heftcom2024
    description: Hybrid Energy Forecasting and Trading Competition 2024
    fields:
      wind_power:
        path: data/wind_power.parquet
        kind: actual
        availability_lag: 1h
      dwd_nwp:
        path: data/dwd_nwp.parquet
        kind: forecast            # parquet already has (issue_time, valid_time) index
"""

from __future__ import annotations

from dataclasses import dataclass, field as _dc_field
import typing as t

import pandas as pd

from .field import Field

if t.TYPE_CHECKING:  # pragma: no cover
    import energydatamodel as edm


[docs] @dataclass class Dataset: """Named fields + optional :mod:`energydatamodel` asset collection. ``fields`` maps name -> :class:`Field`. The legacy ``data`` dict-of-frames view is kept as a convenience (``dataset.data["wind_power"]`` returns the raw frame), but evaluation code must go through a :class:`~emflow.data.feed.DataFeed`, never through ``data``. """ name: str description: t.Optional[str] = None collection: t.Optional["edm.Collection"] = None fields: t.Dict[str, Field] = _dc_field(default_factory=dict) def __post_init__(self): for key, f in list(self.fields.items()): if not isinstance(f, Field): # Accept raw frames for convenience; default availability. self.fields[key] = Field(name=key, frame=f) elif f.name != key: raise ValueError(f"fields[{key!r}] holds Field named {f.name!r}") # -- access ---------------------------------------------------------------
[docs] def field(self, name: str) -> Field: try: return self.fields[name] except KeyError: raise KeyError( f"dataset {self.name!r} has no field {name!r}; " f"available: {sorted(self.fields)}" ) from None
[docs] def add(self, field: Field) -> "Dataset": self.fields[field.name] = field return self
@property def data(self) -> t.Dict[str, pd.DataFrame]: """Raw frames by field name (convenience view โ€” not leak-safe).""" return {name: f.frame for name, f in self.fields.items()} @property def list_data(self): return list(self.fields.keys()) # -- manifest loading -------------------------------------------------------
[docs] @classmethod def from_manifest(cls, repo: str, token: t.Optional[str] = None) -> "Dataset": """Load a dataset from a Rebase-compatible repo's ``rebase.yaml``. ``repo`` is an ``rb://dataset/<owner>/<name>`` URI (or bare ``<owner>/<name>``, treated as a dataset repo). Field frames are read with pandas via fsspec, so ``rb://`` caching and ``HF_TOKEN`` handling apply. """ import fsspec import yaml if not repo.startswith("rb://"): repo = f"rb://dataset/{repo}" repo = repo.rstrip("/") storage = {"token": token} if token else {} with fsspec.open(f"{repo}/rebase.yaml", "r", **storage) as fh: manifest = yaml.safe_load(fh) fields = {} for name, spec in (manifest.get("fields") or {}).items(): frame = pd.read_parquet(f"{repo}/{spec['path']}", storage_options=storage or None) kind = spec.get("kind", "actual") if kind == "forecast" and not isinstance(frame.index, pd.MultiIndex): frame = frame.set_index([spec["issue_col"], spec["valid_col"]]) fields[name] = Field( name=name, frame=frame, kind=kind, availability_lag=spec.get("availability_lag", "0h"), knowledge_col=spec.get("knowledge_col"), description=spec.get("description"), ) return cls(name=manifest.get("name", repo.rsplit("/", 1)[-1]), description=manifest.get("description"), fields=fields)
def __repr__(self): return f"Dataset({self.name!r}, fields={sorted(self.fields)})"