trexselector 0.6.0__py3-none-any.whl
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.
Potentially problematic release.
This version of trexselector might be problematic. Click here for more details.
- tests/__init__.py +0 -0
- tests/conftest.py +46 -0
- tests/test_trexselector/__init__.py +3 -0
- tests/test_trexselector/test_add_dummies.py +60 -0
- tests/test_trexselector/test_fdp.py +81 -0
- tests/test_trexselector/test_tpp.py +81 -0
- tests/test_trexselector/test_trex.py +103 -0
- trexselector/__init__.py +19 -0
- trexselector/add_dummies.py +142 -0
- trexselector/fdp.py +145 -0
- trexselector/gauss_data.py +49 -0
- trexselector/lm_dummy.py +149 -0
- trexselector/phi_prime_fun.py +58 -0
- trexselector/random_experiments.py +229 -0
- trexselector/screen_trex.py +228 -0
- trexselector/select_var_fun.py +69 -0
- trexselector/select_var_fun_DA_BT.py +77 -0
- trexselector/trex.py +611 -0
- trexselector-0.6.0.dist-info/METADATA +84 -0
- trexselector-0.6.0.dist-info/RECORD +22 -0
- trexselector-0.6.0.dist-info/WHEEL +5 -0
- trexselector-0.6.0.dist-info/top_level.txt +2 -0
tests/__init__.py
ADDED
|
File without changes
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pytest configuration file.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import pytest
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
# Add the package root to the Python path
|
|
11
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def gaussian_data():
|
|
15
|
+
"""
|
|
16
|
+
Fixture providing Gaussian test data similar to the R package's Gauss_data.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
tuple: X, y, beta where:
|
|
20
|
+
X is the predictor matrix (n x p)
|
|
21
|
+
y is the response vector (n)
|
|
22
|
+
beta is the true coefficient vector (p)
|
|
23
|
+
"""
|
|
24
|
+
# Set seed for reproducibility
|
|
25
|
+
np.random.seed(789)
|
|
26
|
+
|
|
27
|
+
n, p = 50, 100
|
|
28
|
+
|
|
29
|
+
# Generate predictor matrix
|
|
30
|
+
X = np.random.normal(0, 1, size=(n, p))
|
|
31
|
+
|
|
32
|
+
# Scale X
|
|
33
|
+
X = (X - X.mean(axis=0)) / X.std(axis=0, ddof=1)
|
|
34
|
+
|
|
35
|
+
# Generate true coefficient vector (3 non-zero coefficients)
|
|
36
|
+
beta = np.zeros(p)
|
|
37
|
+
beta[:3] = 3.0
|
|
38
|
+
|
|
39
|
+
# Generate response vector
|
|
40
|
+
eps = np.random.normal(0, 1, size=n)
|
|
41
|
+
y = X @ beta + eps
|
|
42
|
+
|
|
43
|
+
# Center y
|
|
44
|
+
y = y - y.mean()
|
|
45
|
+
|
|
46
|
+
return X, y, beta
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the add_dummies function.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import numpy as np
|
|
7
|
+
from trexselector import add_dummies
|
|
8
|
+
|
|
9
|
+
def test_add_dummies_input_validation(gaussian_data):
|
|
10
|
+
"""Test error control for inputs X and num_dummies."""
|
|
11
|
+
X, y, _ = gaussian_data
|
|
12
|
+
|
|
13
|
+
# Tests for X
|
|
14
|
+
# Test X must be a 2D array
|
|
15
|
+
with pytest.raises(ValueError, match="'X' must be a 2D numpy array"):
|
|
16
|
+
add_dummies(X=X[:, 0], num_dummies=10)
|
|
17
|
+
|
|
18
|
+
# Create X with NaNs
|
|
19
|
+
X_with_nan = X.copy()
|
|
20
|
+
X_with_nan[0, 0] = np.nan
|
|
21
|
+
|
|
22
|
+
with pytest.raises(ValueError, match="'X' contains NaNs"):
|
|
23
|
+
add_dummies(X=X_with_nan, num_dummies=10)
|
|
24
|
+
|
|
25
|
+
# Tests for num_dummies
|
|
26
|
+
# Test num_dummies must be an integer
|
|
27
|
+
with pytest.raises(ValueError, match="'num_dummies' must be an integer larger or equal to 1"):
|
|
28
|
+
add_dummies(X=X, num_dummies=0.5)
|
|
29
|
+
|
|
30
|
+
# Test num_dummies must be >= 1
|
|
31
|
+
with pytest.raises(ValueError, match="'num_dummies' must be an integer larger or equal to 1"):
|
|
32
|
+
add_dummies(X=X, num_dummies=0)
|
|
33
|
+
|
|
34
|
+
# Test num_dummies must not be negative
|
|
35
|
+
with pytest.raises(ValueError, match="'num_dummies' must be an integer larger or equal to 1"):
|
|
36
|
+
add_dummies(X=X, num_dummies=-10)
|
|
37
|
+
|
|
38
|
+
def test_add_dummies_output_shape(gaussian_data):
|
|
39
|
+
"""Test that the output shape of add_dummies is correct."""
|
|
40
|
+
X, _, _ = gaussian_data
|
|
41
|
+
|
|
42
|
+
# Create a smaller test matrix
|
|
43
|
+
n, p = 20, 5
|
|
44
|
+
np.random.seed(42)
|
|
45
|
+
X_small = np.random.randn(n, p)
|
|
46
|
+
|
|
47
|
+
# Test with different numbers of dummies
|
|
48
|
+
for num_dummies in [1, 2, 3]:
|
|
49
|
+
X_Dummy = add_dummies(X=X_small, num_dummies=num_dummies)
|
|
50
|
+
|
|
51
|
+
# Check shape
|
|
52
|
+
assert X_Dummy.shape == (n, p + num_dummies), f"Expected shape {(n, p + num_dummies)}, got {X_Dummy.shape}"
|
|
53
|
+
|
|
54
|
+
# Check that the first p columns are identical to X
|
|
55
|
+
np.testing.assert_allclose(X_Dummy[:, :p], X_small)
|
|
56
|
+
|
|
57
|
+
# Check that the dummy variables are standardized with more tolerance
|
|
58
|
+
for j in range(p, p + num_dummies):
|
|
59
|
+
assert abs(X_Dummy[:, j].mean()) < 1e-10, f"Dummy column {j} is not centered"
|
|
60
|
+
assert abs(X_Dummy[:, j].std() - 1.0) < 1e-10, f"Dummy column {j} is not standardized"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the FDP (False Discovery Proportion) function.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import numpy as np
|
|
7
|
+
from trexselector import FDP, trex
|
|
8
|
+
|
|
9
|
+
def test_fdp_input_validation_beta_hat(gaussian_data):
|
|
10
|
+
"""Test error control for input beta_hat."""
|
|
11
|
+
X, y, beta = gaussian_data
|
|
12
|
+
|
|
13
|
+
# Run T-Rex to get beta_hat
|
|
14
|
+
np.random.seed(1234)
|
|
15
|
+
res = trex(X, y, verbose=False)
|
|
16
|
+
beta_hat = np.zeros(beta.shape[0])
|
|
17
|
+
beta_hat[res["selected_var"]] = 1
|
|
18
|
+
|
|
19
|
+
# Create beta_hat with NAs
|
|
20
|
+
beta_hat_with_nan = beta_hat.copy()
|
|
21
|
+
beta_hat_with_nan[np.random.choice(len(beta_hat), size=10)] = np.nan
|
|
22
|
+
|
|
23
|
+
# Tests
|
|
24
|
+
# Test beta_hat must be 1D array
|
|
25
|
+
with pytest.raises(ValueError, match="'beta_hat' must be a 1D numpy array"):
|
|
26
|
+
FDP(beta_hat=np.column_stack([beta_hat, beta_hat]), beta=beta)
|
|
27
|
+
|
|
28
|
+
# Test beta_hat must be numeric
|
|
29
|
+
with pytest.raises(ValueError, match="'beta_hat' must be a 1D numpy array"):
|
|
30
|
+
FDP(beta_hat=beta_hat.astype(str), beta=beta)
|
|
31
|
+
|
|
32
|
+
# Test beta_hat cannot contain NaNs
|
|
33
|
+
with pytest.raises(ValueError, match="'beta_hat' contains NaNs"):
|
|
34
|
+
FDP(beta_hat=beta_hat_with_nan, beta=beta)
|
|
35
|
+
|
|
36
|
+
def test_fdp_input_validation_beta(gaussian_data):
|
|
37
|
+
"""Test error control for input beta."""
|
|
38
|
+
X, y, beta = gaussian_data
|
|
39
|
+
|
|
40
|
+
# Run T-Rex to get beta_hat
|
|
41
|
+
np.random.seed(1234)
|
|
42
|
+
res = trex(X, y, verbose=False)
|
|
43
|
+
beta_hat = np.zeros(beta.shape[0])
|
|
44
|
+
beta_hat[res["selected_var"]] = 1
|
|
45
|
+
|
|
46
|
+
# Create beta with NAs
|
|
47
|
+
beta_with_nan = beta.copy()
|
|
48
|
+
beta_with_nan[np.random.choice(len(beta), size=10)] = np.nan
|
|
49
|
+
|
|
50
|
+
# Tests
|
|
51
|
+
# Test beta must be 1D array
|
|
52
|
+
with pytest.raises(ValueError, match="'beta' must be a 1D numpy array"):
|
|
53
|
+
FDP(beta_hat=beta_hat, beta=np.column_stack([beta, beta]))
|
|
54
|
+
|
|
55
|
+
# Test beta must be numeric
|
|
56
|
+
with pytest.raises(ValueError, match="'beta' must be a 1D numpy array"):
|
|
57
|
+
FDP(beta_hat=beta_hat, beta=beta.astype(str))
|
|
58
|
+
|
|
59
|
+
# Test beta cannot contain NaNs
|
|
60
|
+
with pytest.raises(ValueError, match="'beta' contains NaNs"):
|
|
61
|
+
FDP(beta_hat=beta_hat, beta=beta_with_nan)
|
|
62
|
+
|
|
63
|
+
# Test length of beta_hat must match length of beta
|
|
64
|
+
with pytest.raises(ValueError, match="Shapes of 'beta_hat' and 'beta' must match"):
|
|
65
|
+
FDP(beta_hat=np.concatenate([beta_hat, beta_hat]), beta=beta)
|
|
66
|
+
|
|
67
|
+
def test_fdp_value_range(gaussian_data):
|
|
68
|
+
"""Test that the value of FDP is in the interval [0, 1]."""
|
|
69
|
+
X, y, beta = gaussian_data
|
|
70
|
+
|
|
71
|
+
# Run T-Rex to get beta_hat
|
|
72
|
+
np.random.seed(1234)
|
|
73
|
+
res = trex(X, y, verbose=False)
|
|
74
|
+
beta_hat = np.zeros(beta.shape[0])
|
|
75
|
+
beta_hat[res["selected_var"]] = 1
|
|
76
|
+
|
|
77
|
+
# Compute FDP
|
|
78
|
+
fdp = FDP(beta_hat=beta_hat, beta=beta)
|
|
79
|
+
|
|
80
|
+
# Test FDP is in [0, 1]
|
|
81
|
+
assert 0 <= fdp <= 1, f"FDP value {fdp} is outside the expected range [0, 1]"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the TPP (True Positive Proportion) function.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import numpy as np
|
|
7
|
+
from trexselector import TPP, trex
|
|
8
|
+
|
|
9
|
+
def test_tpp_input_validation_beta_hat(gaussian_data):
|
|
10
|
+
"""Test error control for input beta_hat."""
|
|
11
|
+
X, y, beta = gaussian_data
|
|
12
|
+
|
|
13
|
+
# Run T-Rex to get beta_hat
|
|
14
|
+
np.random.seed(1234)
|
|
15
|
+
res = trex(X, y, verbose=False)
|
|
16
|
+
beta_hat = np.zeros(beta.shape[0])
|
|
17
|
+
beta_hat[res["selected_var"]] = 1
|
|
18
|
+
|
|
19
|
+
# Create beta_hat with NAs
|
|
20
|
+
beta_hat_with_nan = beta_hat.copy()
|
|
21
|
+
beta_hat_with_nan[np.random.choice(len(beta_hat), size=10)] = np.nan
|
|
22
|
+
|
|
23
|
+
# Tests
|
|
24
|
+
# Test beta_hat must be 1D array
|
|
25
|
+
with pytest.raises(ValueError, match="'beta_hat' must be a 1D numpy array"):
|
|
26
|
+
TPP(beta_hat=np.column_stack([beta_hat, beta_hat]), beta=beta)
|
|
27
|
+
|
|
28
|
+
# Test beta_hat must be numeric
|
|
29
|
+
with pytest.raises(ValueError, match="'beta_hat' must be a 1D numpy array"):
|
|
30
|
+
TPP(beta_hat=beta_hat.astype(str), beta=beta)
|
|
31
|
+
|
|
32
|
+
# Test beta_hat cannot contain NaNs
|
|
33
|
+
with pytest.raises(ValueError, match="'beta_hat' contains NaNs"):
|
|
34
|
+
TPP(beta_hat=beta_hat_with_nan, beta=beta)
|
|
35
|
+
|
|
36
|
+
def test_tpp_input_validation_beta(gaussian_data):
|
|
37
|
+
"""Test error control for input beta."""
|
|
38
|
+
X, y, beta = gaussian_data
|
|
39
|
+
|
|
40
|
+
# Run T-Rex to get beta_hat
|
|
41
|
+
np.random.seed(1234)
|
|
42
|
+
res = trex(X, y, verbose=False)
|
|
43
|
+
beta_hat = np.zeros(beta.shape[0])
|
|
44
|
+
beta_hat[res["selected_var"]] = 1
|
|
45
|
+
|
|
46
|
+
# Create beta with NAs
|
|
47
|
+
beta_with_nan = beta.copy()
|
|
48
|
+
beta_with_nan[np.random.choice(len(beta), size=10)] = np.nan
|
|
49
|
+
|
|
50
|
+
# Tests
|
|
51
|
+
# Test beta must be 1D array
|
|
52
|
+
with pytest.raises(ValueError, match="'beta' must be a 1D numpy array"):
|
|
53
|
+
TPP(beta_hat=beta_hat, beta=np.column_stack([beta, beta]))
|
|
54
|
+
|
|
55
|
+
# Test beta must be numeric
|
|
56
|
+
with pytest.raises(ValueError, match="'beta' must be a 1D numpy array"):
|
|
57
|
+
TPP(beta_hat=beta_hat, beta=beta.astype(str))
|
|
58
|
+
|
|
59
|
+
# Test beta cannot contain NaNs
|
|
60
|
+
with pytest.raises(ValueError, match="'beta' contains NaNs"):
|
|
61
|
+
TPP(beta_hat=beta_hat, beta=beta_with_nan)
|
|
62
|
+
|
|
63
|
+
# Test length of beta_hat must match length of beta
|
|
64
|
+
with pytest.raises(ValueError, match="Shapes of 'beta_hat' and 'beta' must match"):
|
|
65
|
+
TPP(beta_hat=np.concatenate([beta_hat, beta_hat]), beta=beta)
|
|
66
|
+
|
|
67
|
+
def test_tpp_value_range(gaussian_data):
|
|
68
|
+
"""Test that the value of TPP is in the interval [0, 1]."""
|
|
69
|
+
X, y, beta = gaussian_data
|
|
70
|
+
|
|
71
|
+
# Run T-Rex to get beta_hat
|
|
72
|
+
np.random.seed(1234)
|
|
73
|
+
res = trex(X, y, verbose=False)
|
|
74
|
+
beta_hat = np.zeros(beta.shape[0])
|
|
75
|
+
beta_hat[res["selected_var"]] = 1
|
|
76
|
+
|
|
77
|
+
# Compute TPP
|
|
78
|
+
tpp = TPP(beta_hat=beta_hat, beta=beta)
|
|
79
|
+
|
|
80
|
+
# Test TPP is in [0, 1]
|
|
81
|
+
assert 0 <= tpp <= 1, f"TPP value {tpp} is outside the expected range [0, 1]"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the main trex function.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import numpy as np
|
|
7
|
+
from trexselector import trex
|
|
8
|
+
|
|
9
|
+
def test_trex_input_validation_method_type(gaussian_data):
|
|
10
|
+
"""Test error control for inputs method and type."""
|
|
11
|
+
X, y, _ = gaussian_data
|
|
12
|
+
|
|
13
|
+
# Test method must be one of the allowed values
|
|
14
|
+
with pytest.raises(ValueError, match="'method' must be one of"):
|
|
15
|
+
trex(X=X, y=y, method="test")
|
|
16
|
+
|
|
17
|
+
# Test type must be one of the allowed values
|
|
18
|
+
with pytest.raises(ValueError, match="'type' must be one of"):
|
|
19
|
+
trex(X=X, y=y, type="test")
|
|
20
|
+
|
|
21
|
+
def test_trex_input_validation_X(gaussian_data):
|
|
22
|
+
"""Test error control for input X."""
|
|
23
|
+
X, y, _ = gaussian_data
|
|
24
|
+
|
|
25
|
+
# Test X must be a 2D array
|
|
26
|
+
with pytest.raises(ValueError, match="'X' must be a 2D numpy array"):
|
|
27
|
+
trex(X=X[:, 0], y=y)
|
|
28
|
+
|
|
29
|
+
# Create X with NaNs
|
|
30
|
+
X_with_nan = X.copy()
|
|
31
|
+
X_with_nan[0, 0] = np.nan
|
|
32
|
+
|
|
33
|
+
with pytest.raises(ValueError, match="'X' contains NaNs"):
|
|
34
|
+
trex(X=X_with_nan, y=y)
|
|
35
|
+
|
|
36
|
+
# Test X with non-numeric values - this will either raise when creating the X array
|
|
37
|
+
# or during standardization, so we just check for any ValueError
|
|
38
|
+
with pytest.raises(Exception):
|
|
39
|
+
trex(X=X.astype(str), y=y)
|
|
40
|
+
|
|
41
|
+
def test_trex_input_validation_y(gaussian_data):
|
|
42
|
+
"""Test error control for input y."""
|
|
43
|
+
X, y, _ = gaussian_data
|
|
44
|
+
|
|
45
|
+
# Test y must be a 1D array
|
|
46
|
+
with pytest.raises(ValueError, match="'y' must be a 1D numpy array"):
|
|
47
|
+
trex(X=X, y=np.column_stack([y, y]))
|
|
48
|
+
|
|
49
|
+
# Create y with NaNs
|
|
50
|
+
y_with_nan = y.copy()
|
|
51
|
+
y_with_nan[0] = np.nan
|
|
52
|
+
|
|
53
|
+
# Check for a ValueError with a message containing "NaN" (case insensitive)
|
|
54
|
+
with pytest.raises(ValueError, match="NaN"):
|
|
55
|
+
trex(X=X, y=y_with_nan)
|
|
56
|
+
|
|
57
|
+
# Test X and y must have compatible shapes
|
|
58
|
+
with pytest.raises(ValueError, match="Number of rows in X must match length of y"):
|
|
59
|
+
trex(X=X, y=y[:-1])
|
|
60
|
+
|
|
61
|
+
def test_trex_input_validation_parameters(gaussian_data):
|
|
62
|
+
"""Test error control for other parameters."""
|
|
63
|
+
X, y, _ = gaussian_data
|
|
64
|
+
|
|
65
|
+
# Test tFDR must be between 0 and 1
|
|
66
|
+
with pytest.raises(ValueError, match="'tFDR' must be a number between 0 and 1"):
|
|
67
|
+
trex(X=X, y=y, tFDR=-0.1)
|
|
68
|
+
|
|
69
|
+
with pytest.raises(ValueError, match="'tFDR' must be a number between 0 and 1"):
|
|
70
|
+
trex(X=X, y=y, tFDR=1.1)
|
|
71
|
+
|
|
72
|
+
# Test K must be >= 2
|
|
73
|
+
with pytest.raises(ValueError, match="The number of random experiments 'K' must be an integer larger or equal to 2"):
|
|
74
|
+
trex(X=X, y=y, K=1)
|
|
75
|
+
|
|
76
|
+
# Test max_num_dummies must be >= 1
|
|
77
|
+
with pytest.raises(ValueError, match="'max_num_dummies' must be an integer larger or equal to 1"):
|
|
78
|
+
trex(X=X, y=y, max_num_dummies=0)
|
|
79
|
+
|
|
80
|
+
def test_trex_basic_functionality(gaussian_data):
|
|
81
|
+
"""Test that trex produces the expected output."""
|
|
82
|
+
X, y, beta = gaussian_data
|
|
83
|
+
|
|
84
|
+
# Run trex with minimal parameters
|
|
85
|
+
np.random.seed(1234)
|
|
86
|
+
res = trex(X=X, y=y, tFDR=0.2, K=5, max_num_dummies=1, verbose=False)
|
|
87
|
+
|
|
88
|
+
# Check that the output contains the expected keys
|
|
89
|
+
expected_keys = [
|
|
90
|
+
"selected_var", "tFDR", "T_stop", "num_dummies", "V", "v_thresh"
|
|
91
|
+
]
|
|
92
|
+
for key in expected_keys:
|
|
93
|
+
assert key in res, f"Expected key {key} not found in result"
|
|
94
|
+
|
|
95
|
+
# Check that selected_var is a numpy array
|
|
96
|
+
assert isinstance(res["selected_var"], np.ndarray), "selected_var should be a numpy array"
|
|
97
|
+
|
|
98
|
+
# Check that T_stop is a positive integer
|
|
99
|
+
assert isinstance(res["T_stop"], int), "T_stop should be an integer"
|
|
100
|
+
assert res["T_stop"] > 0, "T_stop should be positive"
|
|
101
|
+
|
|
102
|
+
# Check that the number of dummies is as expected
|
|
103
|
+
assert res["num_dummies"] == X.shape[1], "num_dummies should be p in the first iteration"
|
trexselector/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TRexSelector: T-Rex Selector for High-Dimensional Variable Selection & FDR Control
|
|
3
|
+
|
|
4
|
+
This package performs fast variable selection in high-dimensional settings while
|
|
5
|
+
controlling the false discovery rate (FDR) at a user-defined target level.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .trex import trex
|
|
9
|
+
from .random_experiments import random_experiments
|
|
10
|
+
from .lm_dummy import lm_dummy
|
|
11
|
+
from .screen_trex import screen_trex
|
|
12
|
+
from .add_dummies import add_dummies, add_dummies_GVS
|
|
13
|
+
from .fdp import FDP, TPP, fdp_hat
|
|
14
|
+
from .select_var_fun import select_var_fun
|
|
15
|
+
from .select_var_fun_DA_BT import select_var_fun_DA_BT
|
|
16
|
+
from .phi_prime_fun import Phi_prime_fun
|
|
17
|
+
from .gauss_data import generate_gaussian_data
|
|
18
|
+
|
|
19
|
+
__version__ = '0.6.0'
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions for adding dummy variables to the predictor matrix
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.cluster.hierarchy import linkage, fcluster
|
|
7
|
+
from scipy.spatial.distance import pdist
|
|
8
|
+
|
|
9
|
+
def add_dummies(X, num_dummies):
|
|
10
|
+
"""
|
|
11
|
+
Add random dummy variables to the predictor matrix.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
X : ndarray, shape (n, p)
|
|
16
|
+
Predictor matrix.
|
|
17
|
+
num_dummies : int
|
|
18
|
+
Number of dummies to append to the predictor matrix.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
ndarray, shape (n, p + num_dummies)
|
|
23
|
+
Predictor matrix with appended dummies.
|
|
24
|
+
"""
|
|
25
|
+
# Error control
|
|
26
|
+
if not isinstance(X, np.ndarray) or X.ndim != 2:
|
|
27
|
+
raise ValueError("'X' must be a 2D numpy array.")
|
|
28
|
+
|
|
29
|
+
if not np.issubdtype(X.dtype, np.number):
|
|
30
|
+
raise ValueError("'X' only allows numerical values.")
|
|
31
|
+
|
|
32
|
+
if np.any(np.isnan(X)):
|
|
33
|
+
raise ValueError("'X' contains NaNs. Please remove or impute them before proceeding.")
|
|
34
|
+
|
|
35
|
+
if not isinstance(num_dummies, int) or num_dummies < 1:
|
|
36
|
+
raise ValueError("'num_dummies' must be an integer larger or equal to 1.")
|
|
37
|
+
|
|
38
|
+
# Number of rows and columns in X
|
|
39
|
+
n, p = X.shape
|
|
40
|
+
|
|
41
|
+
# Generate dummy variables
|
|
42
|
+
np.random.seed(0) # For reproducibility
|
|
43
|
+
dummies = np.random.normal(0, 1, size=(n, num_dummies))
|
|
44
|
+
|
|
45
|
+
# Scale dummies more carefully to ensure exact standardization
|
|
46
|
+
dummies = dummies - dummies.mean(axis=0)
|
|
47
|
+
stds = dummies.std(axis=0, ddof=1)
|
|
48
|
+
dummies = dummies / stds
|
|
49
|
+
|
|
50
|
+
# Verify standardization
|
|
51
|
+
assert np.allclose(dummies.mean(axis=0), 0, atol=1e-10)
|
|
52
|
+
assert np.allclose(dummies.std(axis=0, ddof=1), 1, atol=1e-10)
|
|
53
|
+
|
|
54
|
+
# Append dummies to X
|
|
55
|
+
X_Dummy = np.hstack((X, dummies))
|
|
56
|
+
|
|
57
|
+
return X_Dummy
|
|
58
|
+
|
|
59
|
+
def add_dummies_GVS(X, num_dummies, corr_max=0.5):
|
|
60
|
+
"""
|
|
61
|
+
Add dummy variables with correlation constraints for group variable selection.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
X : ndarray, shape (n, p)
|
|
66
|
+
Predictor matrix.
|
|
67
|
+
num_dummies : int
|
|
68
|
+
Number of dummies to append to the predictor matrix.
|
|
69
|
+
corr_max : float, default=0.5
|
|
70
|
+
Maximum allowed correlation between any two predictors from different clusters.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
dict
|
|
75
|
+
A dictionary containing:
|
|
76
|
+
- X_Dummy: Predictor matrix with appended dummies
|
|
77
|
+
- max_clusters: Maximum number of clusters
|
|
78
|
+
- cluster_sizes: Size of each cluster
|
|
79
|
+
- IEN_cl_id_vectors: Cluster identity vectors for Informed Elastic Net (IEN)
|
|
80
|
+
"""
|
|
81
|
+
# Error control
|
|
82
|
+
if not isinstance(X, np.ndarray) or X.ndim != 2:
|
|
83
|
+
raise ValueError("'X' must be a 2D numpy array.")
|
|
84
|
+
|
|
85
|
+
if not isinstance(num_dummies, int) or num_dummies < 1:
|
|
86
|
+
raise ValueError("'num_dummies' must be an integer larger or equal to 1.")
|
|
87
|
+
|
|
88
|
+
if not isinstance(corr_max, float) or corr_max < 0 or corr_max > 1:
|
|
89
|
+
raise ValueError("'corr_max' must be a float between 0 and 1.")
|
|
90
|
+
|
|
91
|
+
# Number of rows and columns in X
|
|
92
|
+
n, p = X.shape
|
|
93
|
+
|
|
94
|
+
if num_dummies % p != 0:
|
|
95
|
+
raise ValueError("'num_dummies' must be a positive integer multiple of the number of original predictors in X.")
|
|
96
|
+
|
|
97
|
+
# Number of dummies per predictor
|
|
98
|
+
num_dummies_per_pred = num_dummies // p
|
|
99
|
+
|
|
100
|
+
# Compute correlation matrix
|
|
101
|
+
corr_mat = np.corrcoef(X, rowvar=False)
|
|
102
|
+
|
|
103
|
+
# Compute distance matrix: 1 - |corr_mat|
|
|
104
|
+
dist_mat = 1 - np.abs(corr_mat)
|
|
105
|
+
|
|
106
|
+
# Hierarchical clustering
|
|
107
|
+
Z = linkage(pdist(X.T), method='single')
|
|
108
|
+
|
|
109
|
+
# Determine clusters: cut tree at height 1 - corr_max
|
|
110
|
+
clusters = fcluster(Z, 1 - corr_max, criterion='distance') - 1 # 0-based indexing
|
|
111
|
+
|
|
112
|
+
# Number of clusters
|
|
113
|
+
max_clusters = len(np.unique(clusters))
|
|
114
|
+
|
|
115
|
+
# Cluster sizes
|
|
116
|
+
unique_clusters, cluster_sizes = np.unique(clusters, return_counts=True)
|
|
117
|
+
|
|
118
|
+
# Create IEN cluster identity vectors
|
|
119
|
+
IEN_cl_id_vectors = np.zeros((p, max_clusters))
|
|
120
|
+
for j in range(p):
|
|
121
|
+
IEN_cl_id_vectors[j, clusters[j]] = 1
|
|
122
|
+
|
|
123
|
+
# Initialize X_Dummy
|
|
124
|
+
X_Dummy = np.copy(X)
|
|
125
|
+
|
|
126
|
+
# Add dummies for each original predictor
|
|
127
|
+
for j in range(p):
|
|
128
|
+
# Generate dummies for this predictor
|
|
129
|
+
x_dummies = np.random.normal(0, 1, size=(n, num_dummies_per_pred))
|
|
130
|
+
|
|
131
|
+
# Scale dummies
|
|
132
|
+
x_dummies = (x_dummies - x_dummies.mean(axis=0)) / x_dummies.std(axis=0, ddof=1)
|
|
133
|
+
|
|
134
|
+
# Append to X_Dummy
|
|
135
|
+
X_Dummy = np.hstack((X_Dummy, x_dummies))
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"X_Dummy": X_Dummy,
|
|
139
|
+
"max_clusters": max_clusters,
|
|
140
|
+
"cluster_sizes": cluster_sizes,
|
|
141
|
+
"IEN_cl_id_vectors": IEN_cl_id_vectors
|
|
142
|
+
}
|
trexselector/fdp.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions for computing false discovery proportion (FDP) and true positive proportion (TPP)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
def FDP(beta_hat, beta, eps=np.finfo(float).eps):
|
|
8
|
+
"""
|
|
9
|
+
Compute the false discovery proportion (FDP).
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
beta_hat : ndarray
|
|
14
|
+
Estimated coefficient vector.
|
|
15
|
+
beta : ndarray
|
|
16
|
+
True coefficient vector.
|
|
17
|
+
eps : float, default=machine epsilon
|
|
18
|
+
Numerical zero.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
float
|
|
23
|
+
False discovery proportion.
|
|
24
|
+
"""
|
|
25
|
+
# Error control
|
|
26
|
+
if not isinstance(beta_hat, np.ndarray) or beta_hat.ndim != 1:
|
|
27
|
+
raise ValueError("'beta_hat' must be a 1D numpy array.")
|
|
28
|
+
|
|
29
|
+
if not np.issubdtype(beta_hat.dtype, np.number):
|
|
30
|
+
raise ValueError("'beta_hat' must be a 1D numpy array of numeric values.")
|
|
31
|
+
|
|
32
|
+
if np.any(np.isnan(beta_hat)):
|
|
33
|
+
raise ValueError("'beta_hat' contains NaNs. Please remove or impute them before proceeding.")
|
|
34
|
+
|
|
35
|
+
if not isinstance(beta, np.ndarray) or beta.ndim != 1:
|
|
36
|
+
raise ValueError("'beta' must be a 1D numpy array.")
|
|
37
|
+
|
|
38
|
+
if not np.issubdtype(beta.dtype, np.number):
|
|
39
|
+
raise ValueError("'beta' must be a 1D numpy array of numeric values.")
|
|
40
|
+
|
|
41
|
+
if np.any(np.isnan(beta)):
|
|
42
|
+
raise ValueError("'beta' contains NaNs. Please remove or impute them before proceeding.")
|
|
43
|
+
|
|
44
|
+
if beta_hat.shape != beta.shape:
|
|
45
|
+
raise ValueError("Shapes of 'beta_hat' and 'beta' must match.")
|
|
46
|
+
|
|
47
|
+
# Number of estimated variables
|
|
48
|
+
R = np.sum(np.abs(beta_hat) > eps)
|
|
49
|
+
|
|
50
|
+
# If no variable is selected, the FDP is defined as 0
|
|
51
|
+
if R == 0:
|
|
52
|
+
return 0.0
|
|
53
|
+
|
|
54
|
+
# False discoveries
|
|
55
|
+
V = np.sum((np.abs(beta_hat) > eps) & (np.abs(beta) <= eps))
|
|
56
|
+
|
|
57
|
+
# False discovery proportion
|
|
58
|
+
return V / R
|
|
59
|
+
|
|
60
|
+
def TPP(beta_hat, beta, eps=np.finfo(float).eps):
|
|
61
|
+
"""
|
|
62
|
+
Compute the true positive proportion (TPP).
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
beta_hat : ndarray
|
|
67
|
+
Estimated coefficient vector.
|
|
68
|
+
beta : ndarray
|
|
69
|
+
True coefficient vector.
|
|
70
|
+
eps : float, default=machine epsilon
|
|
71
|
+
Numerical zero.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
float
|
|
76
|
+
True positive proportion.
|
|
77
|
+
"""
|
|
78
|
+
# Error control
|
|
79
|
+
if not isinstance(beta_hat, np.ndarray) or beta_hat.ndim != 1:
|
|
80
|
+
raise ValueError("'beta_hat' must be a 1D numpy array.")
|
|
81
|
+
|
|
82
|
+
if not np.issubdtype(beta_hat.dtype, np.number):
|
|
83
|
+
raise ValueError("'beta_hat' must be a 1D numpy array of numeric values.")
|
|
84
|
+
|
|
85
|
+
if np.any(np.isnan(beta_hat)):
|
|
86
|
+
raise ValueError("'beta_hat' contains NaNs. Please remove or impute them before proceeding.")
|
|
87
|
+
|
|
88
|
+
if not isinstance(beta, np.ndarray) or beta.ndim != 1:
|
|
89
|
+
raise ValueError("'beta' must be a 1D numpy array.")
|
|
90
|
+
|
|
91
|
+
if not np.issubdtype(beta.dtype, np.number):
|
|
92
|
+
raise ValueError("'beta' must be a 1D numpy array of numeric values.")
|
|
93
|
+
|
|
94
|
+
if np.any(np.isnan(beta)):
|
|
95
|
+
raise ValueError("'beta' contains NaNs. Please remove or impute them before proceeding.")
|
|
96
|
+
|
|
97
|
+
if beta_hat.shape != beta.shape:
|
|
98
|
+
raise ValueError("Shapes of 'beta_hat' and 'beta' must match.")
|
|
99
|
+
|
|
100
|
+
# Number of true non-zero coefficients
|
|
101
|
+
S = np.sum(np.abs(beta) > eps)
|
|
102
|
+
|
|
103
|
+
# If there are no true non-zero coefficients, the TPP is defined as 0
|
|
104
|
+
if S == 0:
|
|
105
|
+
return 0.0
|
|
106
|
+
|
|
107
|
+
# True positives
|
|
108
|
+
T = np.sum((np.abs(beta_hat) > eps) & (np.abs(beta) > eps))
|
|
109
|
+
|
|
110
|
+
# True positive proportion
|
|
111
|
+
return T / S
|
|
112
|
+
|
|
113
|
+
def fdp_hat(V, Phi, Phi_prime):
|
|
114
|
+
"""
|
|
115
|
+
Compute the estimated FDP for a set of voting thresholds.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
V : ndarray
|
|
120
|
+
Voting thresholds.
|
|
121
|
+
Phi : ndarray
|
|
122
|
+
Vector of relative occurrences.
|
|
123
|
+
Phi_prime : ndarray
|
|
124
|
+
Vector of expected relative occurrences.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
ndarray
|
|
129
|
+
Estimated FDP for each voting threshold.
|
|
130
|
+
"""
|
|
131
|
+
p = len(Phi)
|
|
132
|
+
V_len = len(V)
|
|
133
|
+
|
|
134
|
+
# Initialize estimated FDP
|
|
135
|
+
FDP_hat = np.zeros(V_len)
|
|
136
|
+
|
|
137
|
+
# Compute estimated FDP for each voting threshold
|
|
138
|
+
for i, v in enumerate(V):
|
|
139
|
+
R = np.sum(Phi >= v)
|
|
140
|
+
if R == 0:
|
|
141
|
+
FDP_hat[i] = 0.0
|
|
142
|
+
else:
|
|
143
|
+
FDP_hat[i] = np.sum(Phi_prime * (Phi >= v)) / R
|
|
144
|
+
|
|
145
|
+
return FDP_hat
|