Source code for dataset.dataset_object
from __future__ import annotations
from abc import ABC, abstractmethod
from copy import deepcopy
from pathlib import Path
import pandas as pd
import yaml
[docs]
class DatasetObject(ABC):
"""Base class for a tabular dataset and its feature metadata.
A dataset is created in a *mutable* state holding a raw, non-encoded,
non-scaled dataframe (including the target column). Preprocessing steps
mutate it through :meth:`snapshot`/:meth:`update`. Once :meth:`freeze` is
called the object becomes *immutable* and the read interface
(:meth:`get`, :meth:`ordered_features`, :meth:`__len__`,
:meth:`__getitem__`) becomes available while the mutation interface is
locked. This freeze/mutable state machine prevents recourse methods and
evaluations from accidentally mutating finalized data.
Attributes
----------
target_column : str
Name of the label column inside the dataframe.
raw_feature_type : dict[str, str]
Maps each feature name to its semantic type (e.g. ``"continuous"``,
``"categorical"``).
raw_feature_mutability : dict[str, bool]
Maps each feature name to whether it may be changed by a method.
raw_feature_actionability : dict[str, str]
Maps each feature name to its allowed direction of change.
"""
_rawdf: pd.DataFrame
_freeze: bool = False
target_column: str
raw_feature_type: dict[str, str]
raw_feature_mutability: dict[str, bool]
raw_feature_actionability: dict[str, str]
@abstractmethod
def __init__(self, path: str, **kwargs):
"""Load the raw dataframe and feature metadata from ``path``.
Parameters
----------
path : str
Directory containing the offline data file and ``<name>.yaml``
metadata for this dataset.
**kwargs
Implementation-specific options.
"""
raise NotImplementedError
@abstractmethod
def _read_df(self, path: str) -> pd.DataFrame:
raise NotImplementedError
def _read_attrs(self, path: str) -> dict[str, object]:
attrs_path = Path(path) / f"{Path(path).name}.yaml"
with attrs_path.open("r", encoding="utf-8") as file:
return yaml.safe_load(file) or {}
def _ensure_mutable(self) -> None:
if self._freeze:
raise RuntimeError("Dataset is frozen; snapshot()/update() are unavailable")
def _ensure_frozen(self) -> None:
if not self._freeze:
raise RuntimeError(
"Dataset is mutable; get()/ordered_features()/__len__()/__getitem__() are unavailable"
)
[docs]
def snapshot(self) -> pd.DataFrame:
"""Return a deep copy of the current raw dataframe.
Only available while the dataset is mutable. Preprocessing steps should
edit this copy and write it back with :meth:`update`.
Returns
-------
pandas.DataFrame
A deep copy of the underlying dataframe.
Raises
------
RuntimeError
If the dataset is frozen.
"""
self._ensure_mutable()
return self._rawdf.copy(deep=True)
[docs]
def update(self, flag: str, value: object, df: pd.DataFrame | None = None) -> bool:
"""Set an attribute (and optionally swap the dataframe) on a mutable dataset.
Parameters
----------
flag : str
Name of the attribute to set. Commonly a boolean preprocessing flag
(to guard against double application) or a metadata key.
value : object
Value to store; deep-copied before assignment.
df : pandas.DataFrame, optional
If provided, replaces the underlying dataframe (deep-copied).
Returns
-------
bool
``True`` on success.
Raises
------
RuntimeError
If the dataset is frozen.
ValueError
If ``flag`` is ``None``.
"""
self._ensure_mutable()
if flag is None:
raise ValueError("flag must not be None")
if df is not None:
self._rawdf = df.copy(deep=True)
setattr(self, flag, deepcopy(value))
return True
[docs]
def attr(self, flag: str) -> object:
"""Return a deep copy of a public attribute (flag or metadata).
Parameters
----------
flag : str
Public attribute name. Names starting with ``_`` are forbidden.
Returns
-------
object
Deep copy of the stored attribute value.
Raises
------
AttributeError
If ``flag`` is protected or unknown.
"""
if flag.startswith("_"):
raise AttributeError(f"Access to protected member '{flag}' is forbidden")
if not hasattr(self, flag):
raise AttributeError(f"Unknown dataset attribute: {flag}")
return deepcopy(getattr(self, flag))
[docs]
def freeze(self):
"""Lock the dataset, enabling the read interface and disabling mutation."""
self._freeze = True
[docs]
def get(self, target: bool = False) -> pd.DataFrame:
"""Return the feature columns, or the target column.
Only available once the dataset is frozen.
Parameters
----------
target : bool, default False
If ``False`` return all feature columns (target excluded). If
``True`` return a single-column dataframe with the target.
Returns
-------
pandas.DataFrame
A deep copy of the requested columns.
Raises
------
RuntimeError
If the dataset is not frozen.
KeyError
If the target column is missing.
"""
self._ensure_frozen()
target_column = self.target_column
if target_column not in self._rawdf.columns:
raise KeyError(f"Unknown target column: {target_column}")
if target:
return self._rawdf.loc[:, [target_column]].copy(deep=True)
return self._rawdf.loc[:, self._rawdf.columns != target_column].copy(deep=True)
[docs]
def ordered_features(self) -> list[str]:
"""Return all column names (features and target) in dataframe order.
Returns
-------
list[str]
Column names. Only available once frozen.
"""
self._ensure_frozen()
return list(self._rawdf.columns)
[docs]
def __len__(self) -> int:
"""Return the number of rows (frozen datasets only)."""
self._ensure_frozen()
return int(self._rawdf.shape[0])
[docs]
def __getitem__(self, key) -> pd.DataFrame:
"""Index a frozen dataset by row (int/slice) or column name (str).
Parameters
----------
key : int or slice or str
``int``/``slice`` select rows; ``str`` selects a single column.
Returns
-------
pandas.DataFrame
A deep copy of the selected rows or column.
"""
self._ensure_frozen()
if isinstance(key, int):
return self._rawdf.iloc[[key]].copy(deep=True)
if isinstance(key, slice):
return self._rawdf.iloc[key].copy(deep=True)
if isinstance(key, str):
if key not in self._rawdf.columns:
raise KeyError(f"Unknown feature name: {key}")
return self._rawdf.loc[:, [key]].copy(deep=True)
raise TypeError(
"DatasetObject only supports int/slice row indexing or str column indexing"
)
[docs]
def clone(self) -> DatasetObject:
"""Return a deep, unfrozen copy of this dataset.
The clone is always mutable regardless of this object's freeze state, so
further preprocessing can be applied to it.
Returns
-------
DatasetObject
A deep copy with ``_freeze`` reset to ``False``.
"""
clone = self.__class__.__new__(self.__class__)
clone.__dict__ = deepcopy(self.__dict__)
clone._freeze = False
return clone