Skip to content

reasons

Reason-code mapping layer (spec section 8).

Deterministic post-processor from a local decomposition to ranked adverse-action reasons: Group A (derogatory information, driven by the I channel) versus Group B (insufficient information, driven by D + M). The mapping table ships configurable; legally operative wording stays with the lender's compliance function.

ReasonCode dataclass

ReasonCode(
    feature: str,
    group: str,
    subtype: str,
    contribution: float,
    text: str,
)

One ranked adverse-action reason.

map_reasons

map_reasons(
    instance_channels: DataFrame,
    *,
    dictionary: Mapping[str, str] | None = None,
    orientation: str = "higher_is_better",
    k: int = 4,
    theta_abs: float = 0.05,
    theta_rel: float = 0.5
) -> list[ReasonCode]

Map one instance's channels to ranked, group-tagged reason codes.

Parameters:

Name Type Description Default
instance_channels DataFrame

Frame with columns feature, I, D, M for one instance.

required
dictionary Mapping[str, str] | None

Optional feature -> approved reason text mapping; features not in the dictionary fall back to subtype templates.

None
orientation str

"higher_is_better" (default): negative contributions worsen the score. "lower_is_better" flips the sign convention.

'higher_is_better'
k int

Maximum number of reasons emitted.

4
theta_abs float

Group B is emitted only when |D + M| >= theta_abs and |D + M| >= theta_rel * |f| (spec 8.3, noise suppression).

0.05
theta_rel float

Group B is emitted only when |D + M| >= theta_abs and |D + M| >= theta_rel * |f| (spec 8.3, noise suppression).

0.05
Source code in src/triadxai/reasons.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def map_reasons(
    instance_channels: pd.DataFrame,
    *,
    dictionary: Mapping[str, str] | None = None,
    orientation: str = "higher_is_better",
    k: int = 4,
    theta_abs: float = 0.05,
    theta_rel: float = 0.5,
) -> list[ReasonCode]:
    """Map one instance's channels to ranked, group-tagged reason codes.

    Parameters
    ----------
    instance_channels
        Frame with columns ``feature``, ``I``, ``D``, ``M`` for one instance.
    dictionary
        Optional feature -> approved reason text mapping; features not in the
        dictionary fall back to subtype templates.
    orientation
        ``"higher_is_better"`` (default): negative contributions worsen the
        score. ``"lower_is_better"`` flips the sign convention.
    k
        Maximum number of reasons emitted.
    theta_abs, theta_rel
        Group B is emitted only when ``|D + M| >= theta_abs`` and
        ``|D + M| >= theta_rel * |f|`` (spec 8.3, noise suppression).
    """
    if orientation not in ("higher_is_better", "lower_is_better"):
        raise ValueError(
            f"unknown orientation {orientation!r}; "
            "expected 'higher_is_better' or 'lower_is_better'"
        )
    sign = 1.0 if orientation == "higher_is_better" else -1.0
    candidates: list[tuple[str, str, float, str]] = []
    for row in instance_channels.itertuples(index=False):
        info = sign * row.I
        epistemic = sign * (row.D + row.M)
        total = info + epistemic
        if info < 0.0:
            candidates.append(("A", "derogatory", info, row.feature))
        if (
            epistemic < 0.0
            and abs(epistemic) >= theta_abs
            and abs(epistemic) >= theta_rel * abs(total)
        ):
            subtype = "insufficient_missing" if abs(row.M) >= abs(row.D) else "insufficient_density"
            candidates.append(("B", subtype, epistemic, row.feature))
    candidates.sort(key=lambda item: abs(item[2]), reverse=True)
    codes = []
    for group, subtype, contribution, feature in candidates[:k]:
        text = (dictionary or {}).get(feature) or _DEFAULT_TEMPLATES[subtype].format(
            feature=feature
        )
        codes.append(
            ReasonCode(
                feature=feature,
                group=group,
                subtype=subtype,
                contribution=float(contribution),
                text=text,
            )
        )
    return codes