Skip to content

catdap2

pycatdap.catdap2

CATDAP-02: Optimal explanatory variable subset search.

Finds the best subset of explanatory variables for a response variable, with support for continuous variable binning (pooling).

Catdap2Result dataclass

Catdap2Result(
    base_aic: float,
    aic: DataFrame,
    aic_order: list[str],
    subsets: list[SubsetResult],
    intervals: Mapping[str, list[float]],
    tway_tables: Mapping[str, DataFrame],
)

Result container for :func:catdap2.

Attributes:

Name Type Description
base_aic float

AIC of the null model (no explanatory variables).

aic DataFrame

Single-variable ΔAIC values with columns ['variable', 'aic'].

aic_order list[str]

Variables sorted by ΔAIC ascending (best first).

subsets list[SubsetResult]

Best variable subsets grouped by size.

intervals Mapping[str, list[float]]

Pooling boundaries for continuous variables. Empty for categorical-only analyses.

tway_tables Mapping[str, DataFrame]

Two-way tables for each single explanatory variable.

to_plotly_json

to_plotly_json() -> dict[str, object]

Return a Plotly-Figure-compatible JSON spec for this result.

Produces a horizontal bar chart of single-variable ΔAIC values, suitable for direct consumption by react-plotly.js or any Plotly renderer.

Returns:

Type Description
dict

Plotly figure spec with data and layout keys.

Notes

Implements the contract from BLUEPRINT.md §5.7 / DP-4 (LizyStudio integration). The subset-trajectory visualization will be added in v0.3+ (Issue #12 / #20).

Source code in src/pycatdap/catdap2.py
def to_plotly_json(self) -> dict[str, object]:
    """Return a Plotly-Figure-compatible JSON spec for this result.

    Produces a horizontal bar chart of single-variable ΔAIC values,
    suitable for direct consumption by ``react-plotly.js`` or any
    Plotly renderer.

    Returns
    -------
    dict
        Plotly figure spec with ``data`` and ``layout`` keys.

    Notes
    -----
    Implements the contract from BLUEPRINT.md §5.7 / DP-4 (LizyStudio
    integration). The subset-trajectory visualization will be added
    in v0.3+ (Issue #12 / #20).
    """
    variables = [str(v) for v in self.aic["variable"].tolist()]
    values = [float(v) for v in self.aic["aic"].to_numpy()]
    colors = ["#2ca02c" if v < 0 else "#d62728" for v in values]
    return {
        "data": [
            {
                "type": "bar",
                "orientation": "h",
                "x": values,
                "y": variables,
                "marker": {"color": colors},
                "name": "ΔAIC",
            }
        ],
        "layout": {
            "title": f"CATDAP-02 Single-Variable AIC (base={self.base_aic:.2f})",
            "xaxis": {"title": "ΔAIC", "zeroline": True},
            "yaxis": {"title": "Variable", "automargin": True},
        },
    }

catdap2

catdap2(
    data: DataFrame,
    pool: list[int] | None = None,
    response_name: str | None = None,
    accuracy: list[float] | None = None,
    nvar: int | None = None,
) -> Catdap2Result

Search for optimal explanatory variable subsets using AIC.

Parameters:

Name Type Description Default
data DataFrame

Input data with categorical and/or continuous variables. Not modified.

required
pool list[int] or None

Pooling method per column: 0 = equal pooling, 1 = unequal pooling (default), 2 = categorical (no pooling). If None, all columns treated as categorical.

None
response_name str or None

Response variable column name. If None, uses the first column.

None
accuracy list[float] or None

Minimum bin width per column for pooling. If None, auto-detected.

None
nvar int or None

Number of top variables to retain for multi-variable search.

None

Returns:

Type Description
Catdap2Result

Raises:

Type Description
ValueError

If inputs are invalid.

Source code in src/pycatdap/catdap2.py
def catdap2(
    data: pd.DataFrame,
    pool: list[int] | None = None,
    response_name: str | None = None,
    accuracy: list[float] | None = None,
    nvar: int | None = None,
) -> Catdap2Result:
    """Search for optimal explanatory variable subsets using AIC.

    Parameters
    ----------
    data : DataFrame
        Input data with categorical and/or continuous variables.
        Not modified.
    pool : list[int] or None
        Pooling method per column:
        ``0`` = equal pooling, ``1`` = unequal pooling (default),
        ``2`` = categorical (no pooling).
        If ``None``, all columns treated as categorical.
    response_name : str or None
        Response variable column name.  If ``None``, uses the first column.
    accuracy : list[float] or None
        Minimum bin width per column for pooling.
        If ``None``, auto-detected.
    nvar : int or None
        Number of top variables to retain for multi-variable search.

    Returns
    -------
    Catdap2Result

    Raises
    ------
    ValueError
        If inputs are invalid.
    """
    if response_name is None:
        if data.empty:
            msg = "data must not be empty"
            raise ValueError(msg)
        response_name = str(data.columns[0])

    if pool is None:
        pool = [2] * len(data.columns)

    _validate_catdap2_input(data, pool, response_name)

    # Prepare data (apply pooling)
    prepared, intervals = _prepare_data(data, pool, response_name, accuracy)

    # Compute base AIC
    resp_col = prepared[response_name]
    marg_e: npt.NDArray[np.float64] = np.asarray(
        resp_col.value_counts().to_numpy(), dtype=np.float64
    )
    n_total = len(resp_col)
    base_aic = compute_base_aic(marg_e, n_total)

    # Identify explanatory variables
    expl_cols = [c for c in prepared.columns if c != response_name]

    # Single-variable AIC
    aic_records: list[dict[str, object]] = []
    tway_tables: dict[str, pd.DataFrame] = {}

    for col in expl_cols:
        cross, me, mf, n = build_crosstab(prepared, response_name, col)
        delta = compute_delta_aic(cross, me, mf, n)
        aic_records.append({"variable": col, "aic": delta})

        subset = prepared[[response_name, col]].dropna()
        tway_tables[col] = pd.crosstab(subset[response_name], subset[col])

    aic_df = pd.DataFrame(aic_records)
    if not aic_df.empty:
        aic_df = aic_df.sort_values("aic").reset_index(drop=True)

    aic_order = list(aic_df["variable"]) if not aic_df.empty else []

    # Subset search
    subsets = search_best_subset(prepared, response_name, expl_cols, nvar=nvar)

    return Catdap2Result(
        base_aic=base_aic,
        aic=aic_df.copy(),
        aic_order=aic_order,
        subsets=subsets,
        intervals=types.MappingProxyType(intervals),
        tway_tables=types.MappingProxyType(tway_tables),
    )