pyFOCI 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/deploy-gh-pages.yml +1 -1
  2. {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/lint.yml +1 -1
  3. {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/python-app.yml +7 -9
  4. {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/release.yml +1 -1
  5. {pyfoci-0.3.0 → pyfoci-0.3.2}/CHANGELOG.md +19 -0
  6. {pyfoci-0.3.0/pyFOCI.egg-info → pyfoci-0.3.2}/PKG-INFO +5 -3
  7. {pyfoci-0.3.0 → pyfoci-0.3.2}/dev-readme.txt +8 -3
  8. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/index.rst +1 -1
  9. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/user_guide.rst +1 -1
  10. pyfoci-0.3.2/examples/plot_FOCISelector_bike_sharing.py +191 -0
  11. pyfoci-0.3.2/examples/plot_FOCISelector_comparison.py +204 -0
  12. {pyfoci-0.3.0 → pyfoci-0.3.2}/pixi.lock +5345 -1637
  13. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/_foci.py +64 -20
  14. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/_version.py +3 -3
  15. pyfoci-0.3.2/pyFOCI/tests/test_foci.py +410 -0
  16. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/discovery.py +5 -1
  17. {pyfoci-0.3.0 → pyfoci-0.3.2/pyFOCI.egg-info}/PKG-INFO +5 -3
  18. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/SOURCES.txt +3 -1
  19. pyfoci-0.3.2/pyFOCI.egg-info/requires.txt +2 -0
  20. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/scm_file_list.json +3 -1
  21. pyfoci-0.3.2/pyFOCI.egg-info/scm_version.json +8 -0
  22. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyproject.toml +18 -5
  23. pyfoci-0.3.0/pyFOCI/tests/test_foci.py +0 -204
  24. pyfoci-0.3.0/pyFOCI.egg-info/requires.txt +0 -2
  25. pyfoci-0.3.0/pyFOCI.egg-info/scm_version.json +0 -8
  26. {pyfoci-0.3.0 → pyfoci-0.3.2}/.codecov.yml +0 -0
  27. {pyfoci-0.3.0 → pyfoci-0.3.2}/.coveragerc +0 -0
  28. {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/dependabot.yml +0 -0
  29. {pyfoci-0.3.0 → pyfoci-0.3.2}/.gitignore +0 -0
  30. {pyfoci-0.3.0 → pyfoci-0.3.2}/.pre-commit-config.yaml +0 -0
  31. {pyfoci-0.3.0 → pyfoci-0.3.2}/CITATION.cff +0 -0
  32. {pyfoci-0.3.0 → pyfoci-0.3.2}/LICENSE +0 -0
  33. {pyfoci-0.3.0 → pyfoci-0.3.2}/README.md +0 -0
  34. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/Makefile +0 -0
  35. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/css/project-template.css +0 -0
  36. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_api.svg +0 -0
  37. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_examples.svg +0 -0
  38. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_getting_started.svg +0 -0
  39. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_user_guide.svg +0 -0
  40. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/logo.png +0 -0
  41. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/js/copybutton.js +0 -0
  42. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/class.rst +0 -0
  43. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/function.rst +0 -0
  44. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/numpydoc_docstring.py +0 -0
  45. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/sidebar-search-bs.html +0 -0
  46. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/api.rst +0 -0
  47. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/conf.py +0 -0
  48. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/make.bat +0 -0
  49. {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/quick_start.rst +0 -0
  50. {pyfoci-0.3.0 → pyfoci-0.3.2}/examples/README.txt +0 -0
  51. /pyfoci-0.3.0/examples/plot_FOCISelector.py → /pyfoci-0.3.2/examples/plot_FOCISelector_progression.py +0 -0
  52. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/__init__.py +0 -0
  53. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/tests/__init__.py +0 -0
  54. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/tests/test_common.py +0 -0
  55. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/__init__.py +0 -0
  56. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/tests/__init__.py +0 -0
  57. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/tests/test_discovery.py +0 -0
  58. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/dependency_links.txt +0 -0
  59. {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/top_level.txt +0 -0
  60. {pyfoci-0.3.0 → pyfoci-0.3.2}/setup.cfg +0 -0
@@ -19,7 +19,7 @@ jobs:
19
19
  - uses: actions/checkout@v7
20
20
  with:
21
21
  fetch-depth: 0
22
- - uses: prefix-dev/setup-pixi@v0.9.6
22
+ - uses: prefix-dev/setup-pixi@v0.10.0
23
23
  with:
24
24
  pixi-version: v0.68.1
25
25
  environments: doc
@@ -13,7 +13,7 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - uses: actions/checkout@v7
16
- - uses: prefix-dev/setup-pixi@v0.9.6
16
+ - uses: prefix-dev/setup-pixi@v0.10.0
17
17
  with:
18
18
  pixi-version: v0.68.1
19
19
  environments: lint
@@ -13,23 +13,21 @@ jobs:
13
13
  strategy:
14
14
  matrix:
15
15
  os: [windows-latest, ubuntu-latest, macos-latest]
16
+ environment: [latest]
17
+ include:
18
+ - os: ubuntu-latest
19
+ environment: oldest
16
20
  runs-on: ${{ matrix.os }}
17
21
  steps:
18
22
  - uses: actions/checkout@v7
19
- - uses: prefix-dev/setup-pixi@v0.9.6
23
+ - uses: prefix-dev/setup-pixi@v0.10.0
20
24
  with:
21
25
  pixi-version: v0.68.1
22
- environments: test
26
+ environments: ${{ matrix.environment }}
23
27
  frozen: true
24
28
 
25
- - name: Clear pixi cache and rebuild environment
26
- shell: bash
27
- run: |
28
- rm -rf .pixi
29
- pixi install
30
-
31
29
  - name: Run tests
32
- run: pixi run -e test test
30
+ run: pixi run -e ${{ matrix.environment }} test
33
31
 
34
32
  - name: Upload coverage reports to Codecov
35
33
  if: matrix.os == 'ubuntu-latest'
@@ -15,7 +15,7 @@ jobs:
15
15
  uses: actions/checkout@v7
16
16
 
17
17
  - name: Install Pixi
18
- uses: prefix-dev/setup-pixi@v0.9.6
18
+ uses: prefix-dev/setup-pixi@v0.10.0
19
19
  with:
20
20
  pixi-version: v0.68.1
21
21
 
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.2]
9
+
10
+ ### Added
11
+ - The documentation now contains an example comparing FOCI and Lasso on a small non-linear real-world dataset.
12
+
13
+ ### Changed
14
+ - Loosened the restriction to scikit-learn 1.8.
15
+
16
+ ## [0.3.1] - 2026-07-03
17
+
18
+ ### Added
19
+ - The documentation now contains an example comparing FOCI with some scikit-learn feature selectors
20
+ on a small artificial redundant nonlinear dataset.
21
+
22
+ ### Changed
23
+ - The default nearest neighbors strategy `nn_strategy="grouping"` is now
24
+ faster than quadratic in the number of unique rows.
25
+ - Removed the restriction to Python < 3.13.
26
+
8
27
  ## [0.3.0] - 2026-06-26
9
28
 
10
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyFOCI
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: A template for scikit-learn compatible packages.
5
5
  Author-email: Robert Pollak <robert.pollak@jku.at>
6
6
  License-Expression: BSD-3-Clause
@@ -8,14 +8,16 @@ Project-URL: Homepage, https://github.com/m3dm-jku/pyFOCI
8
8
  Project-URL: Issues, https://github.com/m3dm-jku/pyFOCI/issues
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
11
13
  Classifier: Operating System :: POSIX
12
14
  Classifier: Operating System :: Unix
13
15
  Classifier: Operating System :: MacOS
14
16
  Classifier: Operating System :: Microsoft :: Windows
15
- Requires-Python: <3.13,>=3.12
17
+ Requires-Python: >=3.12
16
18
  Description-Content-Type: text/markdown
17
19
  License-File: LICENSE
18
- Requires-Dist: scikit-learn==1.8
20
+ Requires-Dist: scikit-learn>=1.6
19
21
  Requires-Dist: numpy
20
22
  Dynamic: license-file
21
23
 
@@ -3,9 +3,12 @@ Testing and merging:
3
3
 
4
4
  1. Locally:
5
5
 
6
+ # On new Python release:
7
+ pixi update
8
+
6
9
  pixi run -e lint lint
7
10
 
8
- pixi run -e test test
11
+ pixi run -e latest test
9
12
  # Also examine the resulting coverage.xml.
10
13
 
11
14
  pixi run -e doc build-doc
@@ -22,6 +25,8 @@ Push dev branch to GitHub.
22
25
 
23
26
  Create pull request to trigger CI tests.
24
27
 
28
+ Update the [Unreleased] changelog section, if relevant
29
+
25
30
  # Merge locally, as a workaround for https://github.com/orgs/community/discussions/5524,
26
31
  # "PR's "Rebase and Merge" should not alter commits if the head branch is already on top of the main one".
27
32
  printf 'Fast-forwarding main to dev: '; git merge-base --is-ancestor main dev && git branch -f main dev && echo OK || echo ERROR
@@ -30,7 +35,7 @@ git push origin main:main
30
35
  # By this, The GitHub repo setting "Automatically delete head branches"
31
36
  # deletes the dev branch in the pull request, for easier creation of the next pull request from dev.
32
37
 
33
- Update the [Unreleased] changelog section
38
+ Check the formatting of https://github.com/m3dm-jku/pyFOCI/blob/main/CHANGELOG.md
34
39
 
35
40
 
36
41
  Releasing:
@@ -42,5 +47,5 @@ Update the changelog, commit with "Release 0.1.2".
42
47
 
43
48
  Tag the commit as "v0.1.2" and push it, including the tag.
44
49
 
45
- GitHub actions will then update the online docs and publish this release on GitHub and PyPI.
50
+ Check the GitHub and PyPI release pages.
46
51
 
@@ -9,7 +9,7 @@
9
9
  pyFOCI documentation
10
10
  #############################################
11
11
 
12
- **Date**: |today| **Version**: |version|
12
+ **Date**: |today| **Version**: |release|
13
13
 
14
14
  **Useful links**:
15
15
  `Package Repository <https://pypi.org/project/pyFOCI>`__ |
@@ -33,7 +33,7 @@ FOCI performs **hierarchical forward selection**. The selection process, as desc
33
33
  How our Implementation Works
34
34
  ----------------------------
35
35
 
36
- We have added a ``max_features`` parameter, and a :math:`T_n` threshold ``min_delta``.
36
+ We have added a ``max_features`` parameter, and a :math:`T_n` threshold ``min_delta`` to extend the stopping criteria.
37
37
 
38
38
  Also, the selection process, modified as in the `FOCI R reference implementation <https://cran.r-project.org/package=FOCI>`_
39
39
  and via **Fuchs, S. (2024)** , works as follows:
@@ -0,0 +1,191 @@
1
+ """
2
+ ===================================
3
+ FOCI vs Lasso on real-world dataset
4
+ ===================================
5
+
6
+ This example compares feature subsets selected by Feature Ordering by Conditional
7
+ Independence (FOCI), Lasso, and a simple univariate mutual-information selector
8
+ (as baseline) on a real-world regression task: hourly bike rental demand from the
9
+ UCI/OpenML Bike Sharing Demand dataset.
10
+
11
+ Bike demand depends on calendar and weather variables and shows strong nonlinear
12
+ and cyclical effects (e.g. time-of-day and weekday patterns), making it a useful
13
+ sanity check for nonlinear feature selection.
14
+
15
+ We use a fixed chronological train/test split, a fixed five-feature budget for all
16
+ methods, and the same nonlinear downstream regressor for evaluation. Lasso's
17
+ regularization strength is selected by cross-validation.
18
+ """
19
+
20
+ import time
21
+ from functools import partial
22
+
23
+ import matplotlib.pyplot as plt
24
+ import numpy as np
25
+ from sklearn.datasets import fetch_openml
26
+ from sklearn.ensemble import HistGradientBoostingRegressor
27
+ from sklearn.feature_selection import (
28
+ SelectFromModel,
29
+ SelectKBest,
30
+ mutual_info_regression,
31
+ )
32
+ from sklearn.linear_model import LassoCV
33
+ from sklearn.metrics import mean_absolute_error, r2_score
34
+
35
+ from pyFOCI import FOCISelector
36
+
37
+ # -------------------------------------------------------------------------
38
+ # Config
39
+ # -------------------------------------------------------------------------
40
+ K_FEATURES = 5
41
+ STRIDE = 2 # for runtime reasons
42
+ TRAIN_FRACTION = 0.75
43
+ RANDOM_STATE = 0
44
+
45
+ # -------------------------------------------------------------------------
46
+ # 1. Load a real-world dataset
47
+ # -------------------------------------------------------------------------
48
+ bike = fetch_openml(
49
+ "Bike_Sharing_Demand",
50
+ version=2,
51
+ as_frame=True,
52
+ parser="pandas",
53
+ )
54
+ df = bike.frame.copy()
55
+
56
+ # FOCISelector and Lasso work on numeric arrays. The OpenML copy contains a few
57
+ # low-cardinality categorical columns, which we encode as deterministic integer
58
+ # codes. We intentionally do not add sinusoidal or one-hot feature engineering:
59
+ # the point is to compare feature selectors on a compact raw-feature view.
60
+ for col in df.select_dtypes(["category", "object"]).columns:
61
+ df[col] = df[col].astype("category").cat.codes
62
+
63
+ # Runtime-only deterministic thinning. The original rows are hourly, so this
64
+ # keeps every second hour while preserving temporal order.
65
+ df = df.iloc[::STRIDE].reset_index(drop=True)
66
+
67
+ X_frame = df.drop(columns=["count"])
68
+ y = df["count"].to_numpy(dtype=float)
69
+ feature_names = X_frame.columns.to_numpy()
70
+ X = X_frame.to_numpy(dtype=float)
71
+
72
+ split = int(TRAIN_FRACTION * len(df))
73
+ X_train, X_test = X[:split], X[split:]
74
+ y_train, y_test = y[:split], y[split:]
75
+
76
+ # -------------------------------------------------------------------------
77
+ # 2. Feature selection benchmark
78
+ # -------------------------------------------------------------------------
79
+ selectors = [
80
+ (
81
+ "Mutual Info",
82
+ SelectKBest(
83
+ partial(mutual_info_regression, random_state=RANDOM_STATE),
84
+ k=K_FEATURES,
85
+ ),
86
+ ),
87
+ (
88
+ "LassoCV top-k",
89
+ SelectFromModel(
90
+ LassoCV(cv=5, random_state=RANDOM_STATE, max_iter=20000, n_jobs=-1),
91
+ threshold=-np.inf,
92
+ max_features=K_FEATURES,
93
+ ),
94
+ ),
95
+ (
96
+ "FOCI",
97
+ FOCISelector(
98
+ max_features=K_FEATURES,
99
+ min_delta=None,
100
+ random_state=RANDOM_STATE,
101
+ ),
102
+ ),
103
+ ]
104
+
105
+ results = []
106
+ w = 91
107
+ print("\n" + "=" * w)
108
+ print(
109
+ "Feature selector comparison on Bike Sharing Demand "
110
+ f"(n={len(df)}, p={X.shape[1]}, k={K_FEATURES})"
111
+ )
112
+ print("=" * w)
113
+ header = f"{'Method':<14} | {'Time (s)':<8} | {'Test R²':<8} | {'MAE':<8} | Features"
114
+ print(header)
115
+ print("-" * w)
116
+
117
+ for name, selector in selectors:
118
+ t0 = time.time()
119
+ selector.fit(X_train, y_train)
120
+ elapsed = time.time() - t0
121
+
122
+ support = selector.get_support()
123
+ selected_idx = np.flatnonzero(support)
124
+ selected_names = feature_names[selected_idx].tolist()
125
+
126
+ # Same downstream model for all selectors: the comparison is about which
127
+ # raw features are chosen, not about changing predictors between methods.
128
+ predictor = HistGradientBoostingRegressor(
129
+ max_iter=200,
130
+ learning_rate=0.05,
131
+ max_leaf_nodes=31,
132
+ random_state=RANDOM_STATE,
133
+ )
134
+ predictor.fit(X_train[:, selected_idx], y_train)
135
+ y_pred = predictor.predict(X_test[:, selected_idx])
136
+
137
+ test_r2 = r2_score(y_test, y_pred)
138
+ test_mae = mean_absolute_error(y_test, y_pred)
139
+
140
+ results.append(
141
+ {
142
+ "name": name,
143
+ "time": elapsed,
144
+ "r2": test_r2,
145
+ "mae": test_mae,
146
+ "features": selected_names,
147
+ }
148
+ )
149
+
150
+ print(
151
+ f"{name:<14} | {elapsed:8.3f} | {test_r2:8.4f} | "
152
+ f"{test_mae:8.2f} | {selected_names}"
153
+ )
154
+
155
+ print("=" * w + "\n")
156
+
157
+ # -------------------------------------------------------------------------
158
+ # 3. Plotting results
159
+ # -------------------------------------------------------------------------
160
+ fig, ax = plt.subplots(1, 1, figsize=(7, 5))
161
+
162
+ names = [result["name"] for result in results]
163
+ r2_scores = [result["r2"] for result in results]
164
+ runtimes = [result["time"] for result in results]
165
+
166
+ x_pos = np.arange(len(names))
167
+ width = 0.35
168
+
169
+ ax_time = ax.twinx()
170
+ ax.bar(x_pos - width / 2, r2_scores, width, label="Test R²", color="tab:purple")
171
+ ax_time.bar(
172
+ x_pos + width / 2,
173
+ runtimes,
174
+ width,
175
+ label="Runtime (s)",
176
+ color="tab:orange",
177
+ alpha=0.7,
178
+ )
179
+
180
+ ax.set_title("Bike Sharing Demand: selected-feature utility vs runtime")
181
+ ax.set_xticks(x_pos)
182
+ ax.set_xticklabels(names, rotation=20, ha="right")
183
+ ax.set_ylabel("Test R²", color="tab:purple", fontweight="bold")
184
+ ax.tick_params(axis="y", labelcolor="tab:purple")
185
+ ax.set_ylim(0, 1.05)
186
+
187
+ ax_time.set_ylabel("Runtime (seconds)", color="tab:orange", fontweight="bold")
188
+ ax_time.tick_params(axis="y", labelcolor="tab:orange")
189
+
190
+ fig.tight_layout()
191
+ plt.show()
@@ -0,0 +1,204 @@
1
+ """
2
+ ===================================
3
+ FOCI vs others on synthetic dataset
4
+ ===================================
5
+
6
+ This example creates a small additive nonlinear synthetic dataset with redundant
7
+ distractor features to demonstrate how Feature Ordering by Conditional Independence
8
+ (FOCI) isolates complementary nonlinear signals, in comparison with some basic
9
+ scikit-learn feature selectors.
10
+
11
+ Univariate feature selectors (SelectKBest) evaluate features marginally, ranking
12
+ redundant collinear features equally high. Lasso is a (sparse) linear method,
13
+ therefore does not work well on strongly nonlinear data, similar to Recursive Feature
14
+ Elimination (RFE) with a linear model, which also cannot deal well with collinear
15
+ features. Tree-based RFE works better, but is much slower and still dilutes split
16
+ importances across collinear groups. A kernel-based method (SequentialFeatureSelector
17
+ with Support Vector Regression) works well, but takes even longer.
18
+ """
19
+
20
+ import time
21
+
22
+ import matplotlib.pyplot as plt
23
+ import numpy as np
24
+ from sklearn.ensemble import RandomForestRegressor
25
+ from sklearn.feature_selection import (
26
+ RFE,
27
+ SelectFromModel,
28
+ SelectKBest,
29
+ SequentialFeatureSelector,
30
+ f_regression,
31
+ mutual_info_regression,
32
+ )
33
+ from sklearn.linear_model import Lasso, LinearRegression
34
+ from sklearn.metrics import r2_score
35
+ from sklearn.model_selection import train_test_split
36
+ from sklearn.pipeline import make_pipeline
37
+ from sklearn.preprocessing import StandardScaler
38
+ from sklearn.svm import SVR
39
+
40
+ from pyFOCI import FOCISelector
41
+
42
+ # -------------------------------------------------------------------------
43
+ # Config
44
+ # -------------------------------------------------------------------------
45
+ K_FEATURES = 5
46
+ N_DISTRACTORS = 4
47
+
48
+ # -------------------------------------------------------------------------
49
+ # 1. Generate small nonlinear dataset with collinearity
50
+ # -------------------------------------------------------------------------
51
+ random_state = np.random.RandomState(0)
52
+ n, p = 600, 25
53
+ X = random_state.normal(size=(n, p))
54
+
55
+ # Create a redundant collinear distractor group for x0
56
+ x0_distractors = range(4, 4 + N_DISTRACTORS)
57
+
58
+ X[:, x0_distractors] = X[:, 0:1] + 0.01 * random_state.normal(size=(n, N_DISTRACTORS))
59
+
60
+ # True underlying signals, all even.
61
+ y = (
62
+ 2 * (X[:, 0] ** 2 - 1)
63
+ + 2 * (X[:, 1] ** 2 - 1)
64
+ + 3 * np.exp(-X[:, 2] ** 2)
65
+ + 2 * np.cos(2.0 * X[:, 3])
66
+ + 0.1 * random_state.normal(size=n)
67
+ )
68
+
69
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
70
+
71
+ # -------------------------------------------------------------------------
72
+ # 2. Benchmark comparison
73
+ # -------------------------------------------------------------------------
74
+ signal_groups = [
75
+ {0, *x0_distractors},
76
+ {1},
77
+ {2},
78
+ {3},
79
+ ]
80
+ signal_group_names = ["x0", "x1", "x2", "x3"]
81
+
82
+ # Used for downstream evaluation and as the estimator for tree-based RFE
83
+ rf = RandomForestRegressor(n_estimators=100, random_state=0, n_jobs=-1)
84
+
85
+ svr_rbf = make_pipeline(StandardScaler(), SVR(kernel="rbf", C=10.0, gamma="scale"))
86
+
87
+ selectors = [
88
+ ("F-reg", SelectKBest(f_regression, k=K_FEATURES)),
89
+ ("Mutual Info", SelectKBest(mutual_info_regression, k=K_FEATURES)),
90
+ (
91
+ "Lasso (L1)",
92
+ SelectFromModel(
93
+ Lasso(alpha=0.01, random_state=0, max_iter=5000),
94
+ max_features=K_FEATURES,
95
+ ),
96
+ ),
97
+ (
98
+ "RFE (LinReg)",
99
+ RFE(estimator=LinearRegression(), n_features_to_select=K_FEATURES),
100
+ ),
101
+ ("RFE (RF)", RFE(estimator=rf, n_features_to_select=K_FEATURES)),
102
+ (
103
+ "SFS (SVR-RBF)",
104
+ SequentialFeatureSelector(
105
+ estimator=svr_rbf,
106
+ n_features_to_select=K_FEATURES,
107
+ direction="forward",
108
+ n_jobs=-1,
109
+ ),
110
+ ),
111
+ ("FOCI", FOCISelector(max_features=K_FEATURES, random_state=0)),
112
+ ]
113
+
114
+ results = []
115
+ w = 80
116
+ print("\n" + "=" * w)
117
+ print(
118
+ "Feature Selector comparison on small additive nonlinear data "
119
+ f"(n={n}, p={p}, k={K_FEATURES})"
120
+ )
121
+ print("=" * w)
122
+ header = (
123
+ f"{'Method':<13} | {'Time (s)':<8} | {'Signal Cov':<10} | "
124
+ f"{'Test R²':<8} | {'Sel Groups':<12} | {'Sel Indices'}"
125
+ )
126
+ print(header)
127
+ print("-" * w)
128
+
129
+ for name, sel in selectors:
130
+ t0 = time.time()
131
+ sel.fit(X_train, y_train)
132
+ dt = time.time() - t0
133
+
134
+ support = sel.get_support()
135
+ sorted_idx = [int(i) for i in np.where(support)[0]]
136
+
137
+ selected_set = set(sorted_idx)
138
+ selected_groups = [
139
+ group_name
140
+ for group_name, group in zip(signal_group_names, signal_groups)
141
+ if (selected_set & group)
142
+ ]
143
+ selected_groups_str = ",".join(selected_groups) if selected_groups else "-"
144
+
145
+ captured_groups = len(selected_groups)
146
+ signal_coverage = captured_groups / len(signal_groups)
147
+
148
+ if sorted_idx:
149
+ rf.fit(X_train[:, sorted_idx], y_train)
150
+ y_pred = rf.predict(X_test[:, sorted_idx])
151
+ test_r2 = r2_score(y_test, y_pred)
152
+ else:
153
+ test_r2 = 0.0
154
+
155
+ results.append(
156
+ {
157
+ "name": name,
158
+ "time": dt,
159
+ "r2": test_r2,
160
+ }
161
+ )
162
+
163
+ print(
164
+ f"{name:<13} | {dt:8.3f} | {signal_coverage:10.2f} | "
165
+ f"{test_r2:8.4f} | {selected_groups_str:<12} | {sorted_idx}"
166
+ )
167
+
168
+ print("=" * w + "\n")
169
+
170
+ # -------------------------------------------------------------------------
171
+ # 3. Plotting Results
172
+ # -------------------------------------------------------------------------
173
+ fig, ax = plt.subplots(1, 1, figsize=(7, 5))
174
+
175
+ names = [r["name"] for r in results]
176
+ r2_scores = [r["r2"] for r in results]
177
+ runtimes = [r["time"] for r in results]
178
+
179
+ x_pos = np.arange(len(names))
180
+ width = 0.35
181
+
182
+ ax_time = ax.twinx()
183
+ ax.bar(x_pos - width / 2, r2_scores, width, label="Test R² Score", color="tab:purple")
184
+ ax_time.bar(
185
+ x_pos + width / 2,
186
+ runtimes,
187
+ width,
188
+ label="Runtime (s)",
189
+ color="tab:orange",
190
+ alpha=0.7,
191
+ )
192
+
193
+ ax.set_title("Selected-feature utility vs Runtime")
194
+ ax.set_xticks(x_pos)
195
+ ax.set_xticklabels(names, rotation=20, ha="right")
196
+ ax.set_ylabel("Test R² Score", color="tab:purple", fontweight="bold")
197
+ ax.tick_params(axis="y", labelcolor="tab:purple")
198
+ ax.set_ylim(0, 1.05)
199
+
200
+ ax_time.set_ylabel("Runtime (seconds)", color="tab:orange", fontweight="bold")
201
+ ax_time.tick_params(axis="y", labelcolor="tab:orange")
202
+
203
+ fig.tight_layout()
204
+ plt.show()