Source code for andrey.data.dataset

"""The DGP product types: a sampled dataset and its quality report.

``andrey.data`` returns a :class:`CausalDataset` -- observational ``data`` paired with the
ground-truth causal ``graph`` (an Andrey :class:`~andrey.core.structure.GraphStructure`), the drawn
structural ``params``, a ``provenance`` record sufficient to regenerate it, and a ``report`` of
honesty/QA diagnostics. All three container types are frozen ``kw_only`` dataclasses, matching the
core ``Structure`` style, so fields grow without ossifying a positional contract.
"""

from __future__ import annotations

from dataclasses import dataclass, field

import numpy as np

from andrey.core.structure import LATENT, GraphStructure


@dataclass(frozen=True, kw_only=True)
class QAReport:
    """Honesty / quality diagnostics for a sampled dataset (all ``None`` until computed).

    Populated by ``andrey.data.qa``; the field names are a stable contract downstream consumers
    bind to.

    Parameters
    ----------
    varsortability : float or None, default=None
        Fraction of directed paths ordered by increasing marginal variance (Reisach et al. 2021);
        ``0.5`` is uninformative. ``None`` until computed.
    r2sortability : float or None, default=None
        The R^2-based companion (Reisach et al. 2023); ``None`` when uncomputed or undefined
        (for example, ``n <= d``, where the correlation matrix is singular).
    faithfulness_accept_rate : float or None, default=None
        Accept rate of the optional faithfulness screen; ``None`` when no screen ran.
    scale : str or None, default=None
        The scale mode used (``"standardize"``, ``"unitless"``, ``"rescale"``, or ``"raw"``).
    """

    varsortability: float | None = None
    r2sortability: float | None = None
    faithfulness_accept_rate: float | None = None
    scale: str | None = None


@dataclass(frozen=True, kw_only=True)
class SCMParams:
    """The drawn structural parameters, aligned to the truth graph's edge list.

    Part of the return contract so weighted metrics and parity comparison read the exact realized
    coefficients rather than regenerating them.

    Parameters
    ----------
    edges : np.ndarray of shape (m, 2)
        Directed edges ``(parent, child)`` in node-label space; row ``k`` pairs with ``weights[k]``.
    weights : np.ndarray of shape (m,) or (m, 3, n_features)
        Structural coefficient of each edge (empty for parameter-free functionals). Under the
        location-scale functional, the weight of each edge in the location. Under the
        random-features functional, the frequencies, phases, and amplitudes of each edge's
        function.
    noise_scales : np.ndarray of shape (d,)
        Per-node exogenous-noise standard deviation. Under the location-scale functional the noise
        of a node is this constant times a scale that depends on its parents. A measurement
        column's entry is the standard deviation of its measurement noise.
    functional : str
        Functional-form name (for example, ``"linear"``).
    noise : str
        Noise-family name (for example, ``"gaussian"``).
    scale_weights : np.ndarray of shape (m,) or (0,), default=empty
        Under the location-scale functional, the weight of each edge in the scale, row ``k``
        pairing with ``edges[k]``; empty for every other functional.

    Notes
    -----
    The edges are those of the structural causal model. The edge from a variable into its
    measurement column is in the dataset's ``graph`` and in ``measurements.parents``, not here.
    """

    edges: np.ndarray
    weights: np.ndarray
    noise_scales: np.ndarray
    functional: str
    noise: str
    scale_weights: np.ndarray = field(default_factory=lambda: np.empty(0))


[docs] @dataclass(frozen=True, kw_only=True) class Missingness: """The missing values drawn over a dataset, with the parents of each missingness indicator. The missingness indicator ``R_i`` of column ``i`` is 1 where its value is missing. The m-graph adds every indicator to the causal graph, with an edge into it from each of its parents (Mohan, Pearl, and Tian 2013); :mod:`andrey.data.missingness` draws them. Parameters ---------- mask : np.ndarray of shape (n, d) ``True`` where a value is missing; the data hold ``NaN`` there. parents : dict of int to list of int Each column that can go missing, mapped to the columns its indicator depends on, in ascending order: ``[]`` under ``"mcar"``, always-observed columns under ``"mar"``, other columns that can go missing under ``"mnar"``, and ``[i]`` for column ``i`` under ``"self_masking"``. A column it does not list is always observed. mechanism : str ``"mcar"``, ``"mar"``, ``"mnar"``, or ``"self_masking"``. form : str or None How an indicator depends on its parents, ``"logistic"`` or ``"quantile"``; ``None`` under ``"mcar"``. rate : float The expected share of missing values in each column that can go missing. References ---------- Mohan, K., Pearl, J., and Tian, J. (2013). Graphical models for inference with missing data. Advances in Neural Information Processing Systems 26. """ mask: np.ndarray parents: dict[int, list[int]] mechanism: str form: str | None rate: float
[docs] @dataclass(frozen=True, kw_only=True) class Measurements: """The measurement columns added to a dataset, each a variable plus independent noise. :func:`andrey.data.measurement.noisy_children` describes them and ``measurements=`` on :meth:`~andrey.data.SCM.sample` draws them. Parameters ---------- parents : dict of int to int Each measurement column mapped to the column it measures, both as columns of the returned data, in ascending order of the measurement columns. In the true graph the measured column is the measurement column's only parent. noise : str The family of the measurement noise (for example, ``"gaussian"``). noise_scale : float The standard deviation of the measurement noise, as a multiple of the standard deviation of the measured column. """ parents: dict[int, int] noise: str noise_scale: float
@dataclass(frozen=True, kw_only=True) class CausalDataset: """Simulated data paired with its ground-truth causal graph. Parameters ---------- data : np.ndarray of shape (n, n_obs) The sample, ``float64`` by default (see ``SCM.sample(dtype=...)``): observational rows, each environment's rows when ``env`` is set, or one row per time step for a time-series model. Columns are the **observed** variables, indexed to match the observed nodes of ``graph`` (observed-first: for a causally-sufficient SCM ``n_obs == graph.n_nodes`` and column ``j`` is node ``j``). graph : GraphStructure or TemporalStructure Ground-truth causal graph. For an i.i.d. model, a ``GraphStructure`` over all ``d`` nodes (``kind="dag"``); latent confounders, when present, are flagged via ``graph.node_types`` and dropped from ``data``. For a time-series model, a ``TemporalStructure``, one graph per lag. report : QAReport Honesty / QA diagnostics (fields are ``None`` until computed). provenance : dict Regeneration record: root entropy, per-component seed layout, SCM config, package versions. params : SCMParams The drawn structural coefficients and noise scales. env : np.ndarray of shape (n,) or None, default=None The environment of each row, ``0`` to ``K - 1``, its rows together in that order, when the data were drawn under interventions; ``None`` otherwise. targets : dict of int to list of int, or None, default=None Each environment's intervened columns, after the column shuffle: ``{0: [], 1: [3]}`` when environment 0 has no intervention and environment 1 intervenes on column 3. ``None`` when ``env`` is ``None``. missing : Missingness or None, default=None The missing values drawn by ``missing=`` on :meth:`~andrey.data.SCM.sample`, their mask and the parents of each missingness indicator; ``None`` when the data are complete. measurements : Measurements or None, default=None The measurement columns added by ``measurements=`` on :meth:`~andrey.data.SCM.sample`, each with the column it measures; ``None`` without them. They are columns of ``data``, shuffled with the others when the sample shuffles its columns, and nodes of ``graph``, each a child of the column it measures. Notes ----- For an i.i.d. model, use :attr:`observed_graph` -- never ``graph`` directly -- when scoring a discovery estimate, so the graph's node set matches ``data``'s columns for latent SCMs. """ data: np.ndarray graph: GraphStructure report: QAReport provenance: dict params: SCMParams env: np.ndarray | None = None targets: dict[int, list[int]] | None = None missing: Missingness | None = None measurements: Measurements | None = None @property def observed_graph(self) -> GraphStructure: """The truth graph restricted to observed nodes, aligned to ``data``'s columns. Equal to ``graph`` for a causally-sufficient SCM. For a latent SCM it is the subgraph induced on the observed nodes (indices ``0 .. n_obs-1``, observed-first) -- a DAG over exactly ``data``'s columns, measurement columns included, so ``shd(estimate, observed_graph)`` is well-shaped. This induced DAG drops the confounding paths through latents; the confounding-aware observed truth is the latent-projected MAG (``andrey.data.latent.marginal``). """ types = self.graph.node_types if types is None or not np.any(types == LATENT): return self.graph from .graphs import dag_truth n_obs = self.data.shape[1] e = self.params.edges e = e[(e[:, 0] < n_obs) & (e[:, 1] < n_obs)] if self.measurements is not None: measured = [(parent, column) for column, parent in self.measurements.parents.items()] e = np.concatenate([e, np.array(measured, dtype=e.dtype).reshape(-1, 2)]) return dag_truth(e[:, 0], e[:, 1], n_obs)