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.
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/deploy-gh-pages.yml +1 -1
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/lint.yml +1 -1
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/python-app.yml +7 -9
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/workflows/release.yml +1 -1
- {pyfoci-0.3.0 → pyfoci-0.3.2}/CHANGELOG.md +19 -0
- {pyfoci-0.3.0/pyFOCI.egg-info → pyfoci-0.3.2}/PKG-INFO +5 -3
- {pyfoci-0.3.0 → pyfoci-0.3.2}/dev-readme.txt +8 -3
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/index.rst +1 -1
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/user_guide.rst +1 -1
- pyfoci-0.3.2/examples/plot_FOCISelector_bike_sharing.py +191 -0
- pyfoci-0.3.2/examples/plot_FOCISelector_comparison.py +204 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pixi.lock +5345 -1637
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/_foci.py +64 -20
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/_version.py +3 -3
- pyfoci-0.3.2/pyFOCI/tests/test_foci.py +410 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/discovery.py +5 -1
- {pyfoci-0.3.0 → pyfoci-0.3.2/pyFOCI.egg-info}/PKG-INFO +5 -3
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/SOURCES.txt +3 -1
- pyfoci-0.3.2/pyFOCI.egg-info/requires.txt +2 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/scm_file_list.json +3 -1
- pyfoci-0.3.2/pyFOCI.egg-info/scm_version.json +8 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyproject.toml +18 -5
- pyfoci-0.3.0/pyFOCI/tests/test_foci.py +0 -204
- pyfoci-0.3.0/pyFOCI.egg-info/requires.txt +0 -2
- pyfoci-0.3.0/pyFOCI.egg-info/scm_version.json +0 -8
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.codecov.yml +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.coveragerc +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.github/dependabot.yml +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.gitignore +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/.pre-commit-config.yaml +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/CITATION.cff +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/LICENSE +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/README.md +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/Makefile +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/css/project-template.css +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_api.svg +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_examples.svg +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_getting_started.svg +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/index_user_guide.svg +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/img/logo.png +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_static/js/copybutton.js +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/class.rst +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/function.rst +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/numpydoc_docstring.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/_templates/sidebar-search-bs.html +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/api.rst +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/conf.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/make.bat +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/doc/quick_start.rst +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/examples/README.txt +0 -0
- /pyfoci-0.3.0/examples/plot_FOCISelector.py → /pyfoci-0.3.2/examples/plot_FOCISelector_progression.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/__init__.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/tests/__init__.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/tests/test_common.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/__init__.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/tests/__init__.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI/utils/tests/test_discovery.py +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/dependency_links.txt +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/pyFOCI.egg-info/top_level.txt +0 -0
- {pyfoci-0.3.0 → pyfoci-0.3.2}/setup.cfg +0 -0
|
@@ -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.
|
|
23
|
+
- uses: prefix-dev/setup-pixi@v0.10.0
|
|
20
24
|
with:
|
|
21
25
|
pixi-version: v0.68.1
|
|
22
|
-
environments:
|
|
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
|
|
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'
|
|
@@ -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.
|
|
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:
|
|
17
|
+
Requires-Python: >=3.12
|
|
16
18
|
Description-Content-Type: text/markdown
|
|
17
19
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: scikit-learn
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
Check the GitHub and PyPI release pages.
|
|
46
51
|
|
|
@@ -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()
|