Population-aware, welfare-aware feature selection.
PopFS selects a small subset of features that performs well across a set of
known subpopulations, not just on their pooled average. The objective is a
generalized-mean (power) welfare over per-population utilities, so the user
can choose anything from utilitarian (alpha=1, arithmetic mean) to
Rawlsian (alpha=-inf, worst-case).
The package ships four selectors that share a common interface and a common evaluator, so you can benchmark them head-to-head on any dataset with known subgroup structure:
| Selector | Idea |
|---|---|
PopFS |
Full pipeline: SIS + multitask group-lasso screen + welfare-aware Gauss-Newton greedy + swap refinement |
PopFSScreen |
Screening stage only (SIS + MT-lasso, no greedy refinement) |
Lasso |
Pooled L1 baseline (sklearn Lasso / L1-LogisticRegression) |
XGBoost |
Pooled tree-importance baseline (falls back to ExtraTrees if xgboost is not installed) |
All four return a SelectionResult and can be scored with
popfs.evaluate to produce per-population + welfare-aggregate metrics.
pip install .
# or with the tree-baseline extra (installs xgboost):
pip install ".[xgboost]"Requires Python ≥ 3.9. Core dependencies: numpy, pandas, scipy,
scikit-learn. xgboost is optional; if absent, the XGBoost selector
transparently falls back to sklearn.ensemble.ExtraTrees.
import numpy as np
from popfs import Population, PopFS, evaluate, fit_population_teachers
# 1. Assemble a list of Populations, one per subgroup.
pops = [
Population(X_train=..., X_val=..., X_test=...,
y_train=..., y_val=..., y_test=...,
name="group_A", task="regression"),
Population(X_train=..., X_val=..., X_test=...,
y_train=..., y_val=..., y_test=...,
name="group_B", task="regression"),
]
# 2. Fit per-population teachers (shared across selectors + evaluate()).
fit_population_teachers(pops, teacher_kind="hgb", seed=0)
# 3. Run PopFS.
selector = PopFS(k=8, welfare_alpha=1.0, # utilitarian
p0=100, d_screen=30, top_l=10,
selection_objective="teacher", seed=0)
result = selector.fit(pops, feature_names=[...])
# 4. Score any subset — even one selected by another method.
ev = evaluate(result.selected_indices, pops, welfare_alpha=1.0)
print(ev.summary["teacher_power_welfare"], ev.summary["label_power_welfare"])alpha |
Welfare interpretation |
|---|---|
+∞ |
Best-population utility (max) |
1.0 |
Arithmetic mean (utilitarian) — the default |
0.0 |
Geometric mean (Nash bargaining) |
-1.0, etc. |
Increasingly Rawlsian; downweights populations with low utility |
-∞ |
Worst-population utility (min, Rawlsian max-min) |
alpha=-np.inf is a common choice when you care specifically about the
worst-off subpopulation.
from popfs import (
Population, # dataclass: per-subgroup train/val/test arrays
SelectionResult, # return type of every selector's .fit()
EvaluationResult, # return type of evaluate()
PopFS, PopFSScreen, # our selectors
Lasso, XGBoost, # pooled baselines
evaluate, # method-agnostic scorer
power_welfare, # generalized-mean aggregator
power_welfare_gain,
fit_population_teachers,# pseudo-labeler used before selection
__version__,
)- Regression (
task="regression"): continuousy. Teacher = a full-feature Ridge/HGB fit; loss = MSE. - Classification (
task="classification", binary):y ∈ {0, 1}. Teacher = full-feature HGB probability estimate; loss = log loss.