pandas DataFrames
=================

Install the optional dependencies with ``pip install pipewhy[pandas]``. The
adapter accepts a local, in-memory pandas DataFrame and an explicit schema.
It supports signed ``int64`` and nullable ``Int64``, unsigned ``uint64`` and
nullable ``UInt64``, ``float64`` and nullable ``Float64``, ``bool`` and nullable
``boolean``, Python-backed ``StringDtype``, and UTC-aware ``datetime64[us, UTC]``
columns declared as ``datetime-us-utc``. IANA zoneinfo timestamps at microsecond
precision use ``datetime-us-zoneinfo`` with an explicit ``timezone_key``; daylight
saving folds are preserved. Other timezone objects and timestamp precision
combinations are rejected until qualified. Native NumPy-backed
dtypes cannot declare nullable fields; extension arrays use ``pandas.NA`` as
the null marker. A floating-point NaN is a value when its extension mask does
not mark it missing. NumPy dtype metadata is rejected until its semantics are
declared. Boolean storage must use canonical 0 or 1 bytes; the
bounded staging budget includes its conversion buffer. Unsupported types,
duplicate or mismatched columns, and
nulls in required fields are rejected. The DataFrame index is excluded.

.. code-block:: python

   from pathlib import Path
   import pandas as pd
   from pipewhy.adapters.pandas import PandasSource
   from pipewhy.cli.capture import scan_source
   from pipewhy.formats import PandasField, PandasLimits, PandasSchema, pandas_source_version
   from pipewhy.snapshots import LocalSnapshotStore

   frame = pd.DataFrame({
       "id": pd.array([1, 2], dtype="Int64"),
       "name": pd.array(["Ada", pd.NA], dtype=pd.StringDtype(storage="python", na_value=pd.NA)),
   })
   schema = PandasSchema((PandasField(0, "id", "Int64"), PandasField(1, "name", "string-python", True)))
   limits = PandasLimits(1000, 10, 1024, 1000000, 100, 10000)
   source = PandasSource("people", frame, schema, limits)
   version = pandas_source_version(frame, schema=schema, limits=limits)
   snapshot = scan_source(source, LocalSnapshotStore(Path("snapshots")), version)

The content version includes the schema and ordered records. Capture stages the
records and checks the source again before publishing, detecting accidental
changes between observations. It does not provide an atomic transaction against
concurrent mutation; callers should keep the DataFrame unchanged during capture.
The staged-byte limit bounds encoded records and their offsets, not the memory
already owned by the DataFrame. Strict normalization is the default; select
``profile="logical-v1"`` explicitly for logical comparisons. Snapshots can be
reopened without the original DataFrame. This adapter does not access remote
storage or distributed DataFrames.

Series
------

A named ``pandas.Series`` uses a distinct schema and source so its identity does
not collide with a one-column DataFrame. The field name must match the Series
name; its index is excluded under the same explicit policy. For example::

   from pipewhy.adapters.pandas import PandasSeriesSource
   from pipewhy.formats import PandasSeriesSchema, pandas_series_source_version

   amounts = pd.Series(pd.array([1, pd.NA], dtype="Int64"), name="amount")
   series_schema = PandasSeriesSchema(PandasField(0, "amount", "Int64", True))
   series_source = PandasSeriesSource("amounts", amounts, series_schema, limits)
   series_version = pandas_series_source_version(amounts, schema=series_schema, limits=limits)
   series_snapshot = scan_source(series_source, LocalSnapshotStore(Path("snapshots")), series_version)

The same dtype restrictions, resource budgets, mutation caveat and offline
snapshot behavior apply.

String categoricals
-------------------

A ``category-string`` field requires an explicit tuple of unique categories and
an explicit ``ordered`` boolean. Categories must use pandas' Python-backed
``StringDtype`` with ``pandas.NA`` as its missing marker. The complete category
list and ordering are part of the schema identity, including unused categories.
A categorical missing code is recorded as null when the field permits nulls.
Other category value types and category storage are rejected. Category labels
count towards the configured field and staging limits.

.. code-block:: python

   categories = ("red", "blue")
   category_index = pd.Index(pd.array(categories, dtype=pd.StringDtype(storage="python", na_value=pd.NA)))
   dtype = pd.CategoricalDtype(categories=category_index, ordered=True)
   colors = pd.DataFrame({"color": pd.Categorical(["blue", None, "red"], dtype=dtype)})
   schema = PandasSchema((PandasField(0, "color", "category-string", True, categories, True),))
