Source code for atomworks.ml.datasets.pandas_dataset
"""Pandas DataFrame-based dataset implementation."""
import logging
from collections.abc import Callable
from os import PathLike
from pathlib import Path
from typing import Any
import numpy as np
import pandas as pd
import pyarrow as pa
from .base import MolecularDataset
from .metadata import ArrowMetadataIndex, MetadataIndex, MetadataIndexProtocol
logger = logging.getLogger("datasets")
[docs]
class PandasDataset(MolecularDataset):
"""Dataset for tabular data stored as pandas DataFrames.
Delegates all metadata, filtering, and ID-mapping logic to
:py:class:`~atomworks.ml.datasets.metadata.MetadataIndex`.
"""
def __init__(
self,
*,
data: pd.DataFrame | PathLike,
name: str,
id_column: str = "example_id",
filters: list[str] | None = None,
columns_to_load: list[str] | None = None,
# MolecularDataset parameters
transform: Callable | None = None,
loader: Callable | None = None,
save_failed_examples_to_dir: str | Path | None = None,
load_kwargs: dict | tuple | None = None,
# Metadata options
memory_map: bool = False,
metadata: MetadataIndexProtocol | None = None,
):
"""Initialize PandasDataset.
Args:
data: Either a pandas DataFrame or path to a CSV/Parquet file containing
the tabular data. Each row represents one example.
name: Descriptive name for this dataset. Used for debugging and some
downstream functions when using nested datasets.
id_column: Column name to use as the DataFrame index for
example ID lookups.
filters: Optional list of pandas query strings to filter the data.
Applied in order during initialization.
columns_to_load: Optional list of column names to load when reading
from a file. If None, all columns are loaded. Can dramatically reduce
memory usage and load time if loading from a columnar format like Parquet.
transform: Transform pipeline to apply to loaded data.
loader: Optional function to process raw DataFrame rows into Transform-ready format.
save_failed_examples_to_dir: Optional directory path where failed examples
will be saved for debugging. Includes RNG state and error information.
load_kwargs: Additional keyword arguments passed to pandas' read functions
(read_csv, read_parquet) when loading from file.
memory_map: If ``True``, use :class:`ArrowMetadataIndex` for memory-mapped
metadata storage instead of in-memory pandas. Reduces heap usage for
large datasets.
metadata: Optional pre-built metadata index. If provided, used directly
instead of constructing a new index. Takes precedence over ``memory_map``.
Examples:
Load from DataFrame:
>>> df = pd.DataFrame({"path": [...], "label": [...]})
>>> dataset = PandasDataset(data=df, name="my_dataset")
Load from file with memory-mapped metadata:
>>> dataset = PandasDataset(data="data.parquet", name="big_dataset", memory_map=True)
"""
super().__init__(
name=name,
transform=transform,
loader=loader,
save_failed_examples_to_dir=save_failed_examples_to_dir,
)
if metadata is not None:
self._metadata = metadata
else:
# Normalize load_kwargs to dict
_load_kwargs = dict(load_kwargs) if isinstance(load_kwargs, tuple) else load_kwargs
_index_cls = ArrowMetadataIndex if memory_map else MetadataIndex
self._metadata = _index_cls(
data=data,
name=name,
id_column=id_column,
filters=filters,
columns_to_load=columns_to_load,
load_kwargs=_load_kwargs,
)
@property
def metadata(self) -> MetadataIndexProtocol:
"""The metadata index."""
return self._metadata
@property
def data(self) -> pd.DataFrame | pa.Table:
"""The underlying metadata table.
Returns a :class:`pd.DataFrame` for :class:`MetadataIndex` or a
:class:`pa.Table` for :class:`ArrowMetadataIndex`.
Raises:
AttributeError: If the metadata backend has no ``data`` attribute
(e.g. :class:`SequentialMetadataIndex`).
"""
return self._metadata.data
def __getitem__(self, idx: int) -> Any:
"""Get an example by index, applying specified loader and Transforms.
Args:
idx: The index of the example to retrieve.
Returns:
Transformed data from the row.
"""
raw_data = self.metadata.get_row(idx)
example_id = self.metadata.get_example_id(idx)
data = self._apply_loader(raw_data)
return self._apply_transform(data, example_id=example_id, idx=idx)
def __len__(self) -> int:
"""Return the number of rows in the dataset."""
return len(self.metadata)
def __contains__(self, example_id: str) -> bool:
"""Check if the dataset contains the example ID."""
return example_id in self.metadata
[docs]
def id_to_idx(self, example_id: str | list[str]) -> int | list[int]:
"""Convert an example ID to the corresponding local index."""
return self.metadata.id_to_idx(example_id)
[docs]
def idx_to_id(self, idx: int | list[int]) -> str | np.ndarray:
"""Convert a local index to the corresponding example ID."""
return self.metadata.idx_to_id(idx)