Source code for ba.qca.calibrate

"""Calibration: transform categorical/numerical data to binary for QCA."""

from __future__ import annotations

from typing import Any, Callable

import numpy as np
import pandas as pd


[docs] def calibrate( df: pd.DataFrame, thresholds: dict[str, float | str | Callable], ) -> pd.DataFrame: """Binarize columns using explicit thresholds. Each key in ``thresholds`` is a column name; the value specifies the binarization rule: - **float/int**: ``value >= threshold`` → 1, else 0. - ``'any_present'``: any truthy / non-null / non-zero → 1. - ``'median'``: ``value >= median`` → 1. - **callable**: applied per-value, must return bool-like. Columns not in ``thresholds`` are passed through unchanged. >>> import pandas as pd >>> df = pd.DataFrame({'age': [25, 35, 45], 'ill': ['no', 'yes', 'no']}) >>> calibrate(df, {'age': 30, 'ill': 'any_present'}).values.tolist() [[0, 0], [1, 1], [1, 0]] """ result = df.copy() for col, rule in thresholds.items(): if col not in result.columns: raise KeyError( f"Column '{col}' not found in DataFrame. " f"Available: {list(result.columns)}" ) result[col] = _apply_rule(result[col], rule) return result
def _apply_rule(series: pd.Series, rule: Any) -> pd.Series: if callable(rule) and not isinstance(rule, str): return series.map(rule).astype(int) if isinstance(rule, str): rule_lower = rule.lower().strip() if rule_lower == "any_present": return _any_present(series) if rule_lower == "median": med = series.median() return (series >= med).astype(int) raise ValueError( f"Unknown string rule '{rule}'. Use 'any_present', 'median', " f"a numeric threshold, or a callable." ) # Numeric threshold return (series >= rule).astype(int) def _any_present(series: pd.Series) -> pd.Series: """Truthy/non-null/non-zero/non-'no'/non-'none' → 1.""" def _is_present(val): if pd.isna(val): return 0 if isinstance(val, str): return int(val.lower().strip() not in ("", "no", "none", "false", "0", "n/a", "na")) return int(bool(val)) return series.map(_is_present).astype(int)