File size: 2,011 Bytes
0fff343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
"""Univariate prefilter: narrow ~20k features to top-N for the GP shortlist.

Computed name-blind on the TRAIN split only — the feature labels (opaque
IDs) are never used as anything but row keys; only the numeric scores and
y go in. Pre-ranking once lets the permutation null re-score after every
label shuffle very cheaply.

Dispatches via an Objective so the same prefilter machinery works for
both the binary AUROC and the continuous correlation paths.
"""

from __future__ import annotations

import numpy as np
import pandas as pd

from engine.objectives import BinaryAUROCObjective, Objective


def precompute_ranks(M: pd.DataFrame) -> pd.DataFrame:
    """Rank each column of M (samples × features). Reuse across permutations."""
    return M.rank(axis=0)


def auroc_per_feature(X_ranks: pd.DataFrame, y_bool: np.ndarray) -> np.ndarray:
    """Vectorised per-feature AUROC against a binary y.

    Mann-Whitney U / (n_pos * n_neg) — equivalent to scoring each column as
    a single-feature classifier of y. Kept exported because the permutation
    null in `engine/permutation.py` uses it directly for the binary path.
    """
    y_bool = np.asarray(y_bool, dtype=bool)
    n_pos = int(y_bool.sum())
    n_neg = int(len(y_bool) - n_pos)
    if n_pos == 0 or n_neg == 0:
        return np.full(X_ranks.shape[1], np.nan)
    sum_ranks_pos = X_ranks.iloc[y_bool].sum(axis=0).values
    U = sum_ranks_pos - n_pos * (n_pos + 1) / 2
    return U / (n_pos * n_neg)


def top_n_features(
    M_train: pd.DataFrame,
    y_train: np.ndarray,
    n: int = 2000,
    *,
    objective: Objective | None = None,
) -> tuple[list[str], pd.Series]:
    """Top-N feature IDs by the objective's univariate ranking, descending."""
    obj = objective or BinaryAUROCObjective()
    ranks = precompute_ranks(M_train)
    raw = obj.prefilter_score_per_feature(ranks, y_train)
    scores = pd.Series(raw, index=M_train.columns, name="prefilter_score")
    top = scores.nlargest(n).index.tolist()
    return top, scores