prod-fs 1.0.5__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.
- prod_fs-1.0.5/PKG-INFO +96 -0
- prod_fs-1.0.5/README.md +83 -0
- prod_fs-1.0.5/pyproject.toml +28 -0
- prod_fs-1.0.5/src/prod_fs/__init__.py +1 -0
- prod_fs-1.0.5/src/prod_fs/prodfs.py +546 -0
prod_fs-1.0.5/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: prod-fs
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: ProD: A visualizable filter-feature selection method based on prodding the class probability densities for overlapping
|
|
5
|
+
Author: RenZhen95
|
|
6
|
+
Author-email: RenZhen95 <j-liaw@hotmail.com>
|
|
7
|
+
Requires-Dist: joblib>=1.5.3
|
|
8
|
+
Requires-Dist: matplotlib>=3.10.8
|
|
9
|
+
Requires-Dist: numpy<2.0.0
|
|
10
|
+
Requires-Dist: scipy>=1.16.3
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
y<p align="center">
|
|
15
|
+
<img src="https://github.com/RenZhen95/prod-fs/blob/main/docs/artwork/logo.svg" width="300">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
**ProD**, a visualizable filter-feature selection method based on "prodding" the class <ins>Pro</ins>bability <ins>D</ins>ensities for overlapping.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
ProD can be installed from PyPI:
|
|
22
|
+
<pre>
|
|
23
|
+
pip install prod-fs
|
|
24
|
+
</pre>
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
```python
|
|
28
|
+
from prodfs import ProD
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import matplotlib.pyplot as plt
|
|
32
|
+
from sklearn.datasets import make_classification
|
|
33
|
+
|
|
34
|
+
# Create random classification dataset
|
|
35
|
+
X, y = make_classification(
|
|
36
|
+
n_samples=300, n_features=50, n_classes=3, n_informative=5,
|
|
37
|
+
shuffle=False
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Initialize ProD object
|
|
41
|
+
prodRanker = ProD()
|
|
42
|
+
|
|
43
|
+
# Carry out feature selection
|
|
44
|
+
prodRanker.fit(X, y)
|
|
45
|
+
|
|
46
|
+
# Get top 10 features
|
|
47
|
+
top10Features = prodRanker.get_topnFeatures(10)
|
|
48
|
+
|
|
49
|
+
# Visualize the top feature's ability to segregate PDEs
|
|
50
|
+
fig, axs = plt.subplots(1, 2, sharey=True)
|
|
51
|
+
|
|
52
|
+
# Top ranked feature
|
|
53
|
+
prodRanker.plot_overlapAreas(top10Features[0], legend="intersection", _ax=axs[0])
|
|
54
|
+
axs[0].set_title("Most relevant feature", loc="left")
|
|
55
|
+
|
|
56
|
+
# Last ranked feature
|
|
57
|
+
prodRanker.plot_overlapAreas(49, legend="intersection", _ax=axs[1])
|
|
58
|
+
axs[1].set_title("Least relevant feature", loc="left")
|
|
59
|
+
|
|
60
|
+
axs[0].set_ylabel(r"Probability Density, $\hat{P}$")
|
|
61
|
+
for i in range(2):
|
|
62
|
+
axs[i].set_xlim(-0.5, 1.5)
|
|
63
|
+
axs[i].set_xticks(np.arange(-0.5, 2.0, 0.5))
|
|
64
|
+
```
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="https://github.com/RenZhen95/prod-fs/blob/main/docs/artwork/example_plot.svg" width="550">
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
Check out the notebooks provided as tutorials and examples of some specific use cases.
|
|
70
|
+
|
|
71
|
+
## Citation
|
|
72
|
+
For now, cite the followinng abstract
|
|
73
|
+
> J.C. Liaw, F. Geu Flores. A novel univariate feature selection filter-measure based on the reduction of class overlapping. 94th Annual Meeting of the International Association of Applied Mathematics and Mechanics - GAMM, Magdeburg, Deutschland, 18.-22. March 2024, Oral Presentation S25.01-4
|
|
74
|
+
|
|
75
|
+
Available at <a href="https://jahrestagung.gamm.org/wp-content/uploads/2024/03/BookOfAbstracts-2.pdf#page=365" target="_blank">Book of Abstracts of the 94th Annual Meeting of the International Association of Applied Mathematics and Mechanics, p363</a>
|
|
76
|
+
|
|
77
|
+
The other feature selection methods that were compared to in our paper is as listed below:
|
|
78
|
+
1. LH-RELIEF: Feature weight estimation for gene selection: a local hyperlinear learning approach
|
|
79
|
+
DOI: https://doi.org/10.1186/1471-2105-15-70
|
|
80
|
+
|
|
81
|
+
2. I-RELIEF: Iterative RELIEF for Feature Weighting: Algorithms, Theories, and Applications
|
|
82
|
+
DOI: https://doi.org/10.1109/TPAMI.2007.1093
|
|
83
|
+
|
|
84
|
+
3. RELIEF-F: Estimating attributes: Analysis and extensions of RELIEF
|
|
85
|
+
DOI: https://doi.org/10.1007/3-540-57868-4_57
|
|
86
|
+
|
|
87
|
+
4. MultiSURF: Benchmarking relief-based feature selection methods for bioinformatics data mining
|
|
88
|
+
DOI: https://doi.org/10.1016/j.jbi.2018.07.015
|
|
89
|
+
|
|
90
|
+
5. Random Forests
|
|
91
|
+
DOI: https://doi.org/10.1023/A:1010933404324
|
|
92
|
+
|
|
93
|
+
6. ANOVA F-statistic: Statistical Methods for Research Workers
|
|
94
|
+
|
|
95
|
+
7. Mutual Information: Estimating mutual information
|
|
96
|
+
DOI: https://doi.org/10.1103/PhysRevE.69.066138
|
prod_fs-1.0.5/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
y<p align="center">
|
|
2
|
+
<img src="https://github.com/RenZhen95/prod-fs/blob/main/docs/artwork/logo.svg" width="300">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
**ProD**, a visualizable filter-feature selection method based on "prodding" the class <ins>Pro</ins>bability <ins>D</ins>ensities for overlapping.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
ProD can be installed from PyPI:
|
|
9
|
+
<pre>
|
|
10
|
+
pip install prod-fs
|
|
11
|
+
</pre>
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
```python
|
|
15
|
+
from prodfs import ProD
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
from sklearn.datasets import make_classification
|
|
20
|
+
|
|
21
|
+
# Create random classification dataset
|
|
22
|
+
X, y = make_classification(
|
|
23
|
+
n_samples=300, n_features=50, n_classes=3, n_informative=5,
|
|
24
|
+
shuffle=False
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Initialize ProD object
|
|
28
|
+
prodRanker = ProD()
|
|
29
|
+
|
|
30
|
+
# Carry out feature selection
|
|
31
|
+
prodRanker.fit(X, y)
|
|
32
|
+
|
|
33
|
+
# Get top 10 features
|
|
34
|
+
top10Features = prodRanker.get_topnFeatures(10)
|
|
35
|
+
|
|
36
|
+
# Visualize the top feature's ability to segregate PDEs
|
|
37
|
+
fig, axs = plt.subplots(1, 2, sharey=True)
|
|
38
|
+
|
|
39
|
+
# Top ranked feature
|
|
40
|
+
prodRanker.plot_overlapAreas(top10Features[0], legend="intersection", _ax=axs[0])
|
|
41
|
+
axs[0].set_title("Most relevant feature", loc="left")
|
|
42
|
+
|
|
43
|
+
# Last ranked feature
|
|
44
|
+
prodRanker.plot_overlapAreas(49, legend="intersection", _ax=axs[1])
|
|
45
|
+
axs[1].set_title("Least relevant feature", loc="left")
|
|
46
|
+
|
|
47
|
+
axs[0].set_ylabel(r"Probability Density, $\hat{P}$")
|
|
48
|
+
for i in range(2):
|
|
49
|
+
axs[i].set_xlim(-0.5, 1.5)
|
|
50
|
+
axs[i].set_xticks(np.arange(-0.5, 2.0, 0.5))
|
|
51
|
+
```
|
|
52
|
+
<p align="center">
|
|
53
|
+
<img src="https://github.com/RenZhen95/prod-fs/blob/main/docs/artwork/example_plot.svg" width="550">
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
Check out the notebooks provided as tutorials and examples of some specific use cases.
|
|
57
|
+
|
|
58
|
+
## Citation
|
|
59
|
+
For now, cite the followinng abstract
|
|
60
|
+
> J.C. Liaw, F. Geu Flores. A novel univariate feature selection filter-measure based on the reduction of class overlapping. 94th Annual Meeting of the International Association of Applied Mathematics and Mechanics - GAMM, Magdeburg, Deutschland, 18.-22. March 2024, Oral Presentation S25.01-4
|
|
61
|
+
|
|
62
|
+
Available at <a href="https://jahrestagung.gamm.org/wp-content/uploads/2024/03/BookOfAbstracts-2.pdf#page=365" target="_blank">Book of Abstracts of the 94th Annual Meeting of the International Association of Applied Mathematics and Mechanics, p363</a>
|
|
63
|
+
|
|
64
|
+
The other feature selection methods that were compared to in our paper is as listed below:
|
|
65
|
+
1. LH-RELIEF: Feature weight estimation for gene selection: a local hyperlinear learning approach
|
|
66
|
+
DOI: https://doi.org/10.1186/1471-2105-15-70
|
|
67
|
+
|
|
68
|
+
2. I-RELIEF: Iterative RELIEF for Feature Weighting: Algorithms, Theories, and Applications
|
|
69
|
+
DOI: https://doi.org/10.1109/TPAMI.2007.1093
|
|
70
|
+
|
|
71
|
+
3. RELIEF-F: Estimating attributes: Analysis and extensions of RELIEF
|
|
72
|
+
DOI: https://doi.org/10.1007/3-540-57868-4_57
|
|
73
|
+
|
|
74
|
+
4. MultiSURF: Benchmarking relief-based feature selection methods for bioinformatics data mining
|
|
75
|
+
DOI: https://doi.org/10.1016/j.jbi.2018.07.015
|
|
76
|
+
|
|
77
|
+
5. Random Forests
|
|
78
|
+
DOI: https://doi.org/10.1023/A:1010933404324
|
|
79
|
+
|
|
80
|
+
6. ANOVA F-statistic: Statistical Methods for Research Workers
|
|
81
|
+
|
|
82
|
+
7. Mutual Information: Estimating mutual information
|
|
83
|
+
DOI: https://doi.org/10.1103/PhysRevE.69.066138
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "prod-fs"
|
|
3
|
+
version = "1.0.5"
|
|
4
|
+
description = "ProD: A visualizable filter-feature selection method based on prodding the class probability densities for overlapping"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "RenZhen95", email = "j-liaw@hotmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"joblib>=1.5.3",
|
|
12
|
+
"matplotlib>=3.10.8",
|
|
13
|
+
"numpy<2.0.0",
|
|
14
|
+
"scipy>=1.16.3",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
prod-fs = "prodfs:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.9.11,<0.10.0"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"pandas>=2.3.3",
|
|
27
|
+
"scikit-learn>=1.8.0",
|
|
28
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .prodfs import ProD
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from itertools import combinations
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from scipy.stats import gaussian_kde
|
|
6
|
+
from joblib import Parallel, delayed
|
|
7
|
+
|
|
8
|
+
class ProD():
|
|
9
|
+
def __init__(
|
|
10
|
+
self, integration_method="trapz", delta=500,
|
|
11
|
+
bw_method="scott", k=2, n_jobs=1,
|
|
12
|
+
lower_end=-1.5, upper_end=2.5,
|
|
13
|
+
averaging_method="mean", mode="release"
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
integration_method : str
|
|
19
|
+
- Integration method.
|
|
20
|
+
|
|
21
|
+
Available options include 'numpy.trapz' (default) and 'sum'.
|
|
22
|
+
|
|
23
|
+
delta : int
|
|
24
|
+
- Number of cells in the x-grid
|
|
25
|
+
|
|
26
|
+
bw_method : str, scalar or callable
|
|
27
|
+
- The method used to calculate the estimator bandwith. This can be
|
|
28
|
+
'scott' and 'silverman', a scalar constant or a callable. For
|
|
29
|
+
more details, see scipy.stats.gaussian_kde documentation.
|
|
30
|
+
|
|
31
|
+
k : intpairwise
|
|
32
|
+
- Compute the mean intersection area between (number of classes)
|
|
33
|
+
choose k combinations of intersection areas.
|
|
34
|
+
|
|
35
|
+
n_jobs : int
|
|
36
|
+
- Number of processors to use. -1 to use all available processors.
|
|
37
|
+
|
|
38
|
+
lower_end : float
|
|
39
|
+
- Lower end of the grid to evaluate the KDEs.
|
|
40
|
+
|
|
41
|
+
upper_end : float
|
|
42
|
+
- Upper end of the grid to evaluate the KDEs.
|
|
43
|
+
|
|
44
|
+
averaging_method : str
|
|
45
|
+
- Method of averaging the combinations of intersection areas.
|
|
46
|
+
Available options include:
|
|
47
|
+
1. "mean" : The mean of all the combinations of intersection areas
|
|
48
|
+
2. "weighted" : A weighted mean of all the combinations of intersection areas
|
|
49
|
+
|
|
50
|
+
mode : str ("release", "development")
|
|
51
|
+
- Option implemented during development to return constructed kernels
|
|
52
|
+
PDEs.
|
|
53
|
+
"""
|
|
54
|
+
self.integration_method = integration_method
|
|
55
|
+
self.delta = delta
|
|
56
|
+
self.bw_method = bw_method
|
|
57
|
+
self.k = k
|
|
58
|
+
self.n_jobs = n_jobs
|
|
59
|
+
self.mode = mode
|
|
60
|
+
self.averaging_method = averaging_method
|
|
61
|
+
|
|
62
|
+
# Initializing the x-axis grid
|
|
63
|
+
if lower_end > 0.0:
|
|
64
|
+
raise ValueError("Parameter lower_end must be less than 0.0!")
|
|
65
|
+
else:
|
|
66
|
+
self.leftEnd = lower_end
|
|
67
|
+
|
|
68
|
+
if upper_end < 1.0:
|
|
69
|
+
raise ValueError("Parameter upper_end must be greater than 1.0!")
|
|
70
|
+
else:
|
|
71
|
+
self.rightEnd = upper_end
|
|
72
|
+
|
|
73
|
+
def fit(self, X, y):
|
|
74
|
+
"""
|
|
75
|
+
Get the intersection areas of the PDE of class-segregated groups.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
X : np.array
|
|
80
|
+
- Dataset with the shape: (n_samples, n_features)
|
|
81
|
+
|
|
82
|
+
y : np.array
|
|
83
|
+
- Class vector
|
|
84
|
+
"""
|
|
85
|
+
self.y = y
|
|
86
|
+
|
|
87
|
+
# Min-max normalization of dataset
|
|
88
|
+
X_sub = X - X.min(axis=0)
|
|
89
|
+
self.X = X_sub / X_sub.max(axis=0)
|
|
90
|
+
|
|
91
|
+
# Grouping the samples according to unique y label
|
|
92
|
+
self.y_segregatedGroup, self.y_segregatedGroup_sd, self.y_segregatedGroup_mean = self.segregateX_y()
|
|
93
|
+
|
|
94
|
+
# Initializing a list of available classes
|
|
95
|
+
self.yLabels = list(self.y_segregatedGroup.keys())
|
|
96
|
+
self.yLabels.sort()
|
|
97
|
+
|
|
98
|
+
# Check to make sure user does not enter an invalid parameter 'n'
|
|
99
|
+
if self.k > len(self.yLabels):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Parameter k must be between 2 and number of class ({len(self.yLabels)})!"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if self.k == 1:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Parameter k must be between 2 and number of class ({len(self.yLabels)})!"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Initializing the default grid
|
|
110
|
+
mid_grid= np.linspace(0.0, 1.0, int(self.delta))
|
|
111
|
+
self.grid_width = mid_grid[1] - mid_grid[0]
|
|
112
|
+
|
|
113
|
+
if self.leftEnd != 0.0:
|
|
114
|
+
leftGrid = np.arange(self.leftEnd, 0.0, self.grid_width)
|
|
115
|
+
self.XGrid = np.concatenate((leftGrid, mid_grid))
|
|
116
|
+
else:
|
|
117
|
+
self.XGrid = mid_grid
|
|
118
|
+
|
|
119
|
+
if self.rightEnd != 1.0:
|
|
120
|
+
rightGrid = np.arange(1.0, self.rightEnd, self.grid_width)
|
|
121
|
+
self.XGrid = np.concatenate((self.XGrid, rightGrid))
|
|
122
|
+
|
|
123
|
+
# Do not allow user to use ProD, when class has only one sample
|
|
124
|
+
yToRemove = []
|
|
125
|
+
for y in self.y_segregatedGroup.keys():
|
|
126
|
+
if self.y_segregatedGroup[y].shape[0] == 1:
|
|
127
|
+
yToRemove.append(y)
|
|
128
|
+
print(
|
|
129
|
+
f"---\ny={y} sub-dataset has only 1 sample and will be " +
|
|
130
|
+
"excluded ... "
|
|
131
|
+
)
|
|
132
|
+
# - removing class populations with only one sample
|
|
133
|
+
if len(yToRemove) != 0:
|
|
134
|
+
for y in yToRemove:
|
|
135
|
+
self.y_segregatedGroup.pop(y, None)
|
|
136
|
+
# - abort if all remaining samples belong to only one single class
|
|
137
|
+
if len(self.y_segregatedGroup) == 1:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"There's only one target label, " +
|
|
140
|
+
f"y={self.y_segregatedGroup.keys()}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Construct kernel density estimator per class for every feature
|
|
144
|
+
print(
|
|
145
|
+
"Constructing and evaluating the KDEs per class for every feature ... "
|
|
146
|
+
)
|
|
147
|
+
self.pdes = []
|
|
148
|
+
self.grids = []
|
|
149
|
+
|
|
150
|
+
delayed_calls = (
|
|
151
|
+
delayed(
|
|
152
|
+
self.construct_kernel
|
|
153
|
+
)(feat_idx) for feat_idx in range(self.X.shape[1])
|
|
154
|
+
)
|
|
155
|
+
res = Parallel(n_jobs=self.n_jobs, verbose=0)(delayed_calls)
|
|
156
|
+
if self.mode == "development":
|
|
157
|
+
self.feature_kernels = []
|
|
158
|
+
for item in res:
|
|
159
|
+
self.feature_kernels.append(item[0])
|
|
160
|
+
self.pdes.append(item[1])
|
|
161
|
+
self.grids.append(item[2])
|
|
162
|
+
elif self.mode == "release":
|
|
163
|
+
for item in res:
|
|
164
|
+
self.pdes.append(item[0])
|
|
165
|
+
self.grids.append(item[1])
|
|
166
|
+
|
|
167
|
+
print(" - Kernels constructed!")
|
|
168
|
+
|
|
169
|
+
# Compute intersection areas
|
|
170
|
+
_combinations = combinations(self.yLabels, self.k)
|
|
171
|
+
c1 = []
|
|
172
|
+
cStack = []
|
|
173
|
+
print("Computing intersection areas ...")
|
|
174
|
+
for c in _combinations:
|
|
175
|
+
c1.append(c)
|
|
176
|
+
delayed_calls_intersectionArea = (
|
|
177
|
+
delayed(
|
|
178
|
+
self.compute_intersectionArea
|
|
179
|
+
)(feat_idx, c) for feat_idx in range(self.X.shape[1])
|
|
180
|
+
)
|
|
181
|
+
c_intersection = Parallel(
|
|
182
|
+
n_jobs=self.n_jobs, backend="threading", verbose=0
|
|
183
|
+
)(delayed_calls_intersectionArea)
|
|
184
|
+
|
|
185
|
+
cStack.append(c_intersection)
|
|
186
|
+
|
|
187
|
+
cStack = np.array(cStack)
|
|
188
|
+
|
|
189
|
+
if self.averaging_method == "mean":
|
|
190
|
+
print(" - averaging_method: 'mean'")
|
|
191
|
+
self.intersectionAreas = np.mean(cStack, axis=0)
|
|
192
|
+
|
|
193
|
+
elif self.averaging_method == "weighted":
|
|
194
|
+
print(" - averaging_method: 'weighted'")
|
|
195
|
+
nSamples_total = self.X.shape[0]
|
|
196
|
+
nSamples_perClass = defaultdict()
|
|
197
|
+
for _class in self.y_segregatedGroup.keys():
|
|
198
|
+
nSamples_perClass[_class] = self.y_segregatedGroup[_class].shape[0]
|
|
199
|
+
|
|
200
|
+
_weights = np.zeros(len(c1))
|
|
201
|
+
for _wi, _c in enumerate(c1):
|
|
202
|
+
_weight = (
|
|
203
|
+
nSamples_perClass[_c[0]] + nSamples_perClass[_c[1]]
|
|
204
|
+
) / nSamples_total
|
|
205
|
+
_weights[_wi] = _weight
|
|
206
|
+
|
|
207
|
+
# Normalized such that the sum of all the weights are 1
|
|
208
|
+
_norm_weights = _weights / _weights.sum()
|
|
209
|
+
|
|
210
|
+
intAreas = np.zeros(self.X.shape[1])
|
|
211
|
+
for _ci in range(len(c1)):
|
|
212
|
+
intAreas += cStack[_ci,:] * _norm_weights[_ci]
|
|
213
|
+
|
|
214
|
+
self.intersectionAreas = intAreas
|
|
215
|
+
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"Acceptable options for the parameter 'averaging_method' are: " +
|
|
219
|
+
"('mean', 'weighted')"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Get feature importances as expressed in terms of reciprocal of
|
|
223
|
+
# computed intersection areas
|
|
224
|
+
self.feature_importances_ = 1/self.intersectionAreas
|
|
225
|
+
|
|
226
|
+
def compute_intersectionArea(self, feat_idx, _combinations):
|
|
227
|
+
"""
|
|
228
|
+
Compute intersection area between estimated PDEs.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
feat_idx : int
|
|
233
|
+
- Index of the desired feature in the given dataset, X.
|
|
234
|
+
|
|
235
|
+
pairwise : tuple
|
|
236
|
+
- Pair of indices indicating which pair of classes to compare.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
OA : float
|
|
241
|
+
- Computed intersection area of the PDEs.
|
|
242
|
+
"""
|
|
243
|
+
yStack = []
|
|
244
|
+
|
|
245
|
+
for c in _combinations:
|
|
246
|
+
yStack.append(self.pdes[feat_idx][c])
|
|
247
|
+
|
|
248
|
+
yIntersection = np.amin(yStack, axis=0)
|
|
249
|
+
|
|
250
|
+
if self.integration_method == "sum":
|
|
251
|
+
OA = (yIntersection.sum())/delta
|
|
252
|
+
elif self.integration_method == "trapz":
|
|
253
|
+
OA = np.trapz(yIntersection, self.grids[feat_idx])
|
|
254
|
+
else:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
"Possible options for <integration_method>: " +
|
|
257
|
+
"('trapz', 'sum')"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return OA
|
|
261
|
+
|
|
262
|
+
def construct_kernel(self, feat_idx):
|
|
263
|
+
"""
|
|
264
|
+
Construct the kernel density estimator of all the class-segregated groups
|
|
265
|
+
for a given feature.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
feat_idx : int
|
|
270
|
+
- Index of the desired feature in the given dataset, X.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
pdes : dict
|
|
275
|
+
- dict[class]: Evaluated KDEs along grid.
|
|
276
|
+
|
|
277
|
+
_grid : np.array
|
|
278
|
+
- Grid to evaluated KDE needed for trapezoidal integration.
|
|
279
|
+
|
|
280
|
+
If self.mode=="development":
|
|
281
|
+
kdes : dict
|
|
282
|
+
- dict[class]: Constructed KDE.
|
|
283
|
+
|
|
284
|
+
"""
|
|
285
|
+
kernels = defaultdict(); pdes = defaultdict()
|
|
286
|
+
|
|
287
|
+
special_means = np.array(())
|
|
288
|
+
|
|
289
|
+
# To account for cases where all samples in the population are
|
|
290
|
+
# centered at the local mean
|
|
291
|
+
for y in self.yLabels:
|
|
292
|
+
if self.y_segregatedGroup_sd[y][feat_idx] <= 2*self.grid_width:
|
|
293
|
+
# Fit mean into the grid
|
|
294
|
+
special_means = np.append(
|
|
295
|
+
special_means, self.y_segregatedGroup_mean[y][feat_idx]
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
special_means = np.sort(special_means)
|
|
299
|
+
|
|
300
|
+
if len(special_means) > 0:
|
|
301
|
+
lefthalf = np.where(self.XGrid < special_means[0])[0]
|
|
302
|
+
righthalf = np.where(self.XGrid > special_means[-1])[0]
|
|
303
|
+
|
|
304
|
+
_grid = self.XGrid[lefthalf]
|
|
305
|
+
for _mean in special_means:
|
|
306
|
+
_grid = np.append(_grid, _mean)
|
|
307
|
+
_grid = np.concatenate((_grid, self.XGrid[righthalf]))
|
|
308
|
+
|
|
309
|
+
else:
|
|
310
|
+
_grid = self.XGrid
|
|
311
|
+
|
|
312
|
+
for y in self.yLabels:
|
|
313
|
+
kernel = gaussian_kde(
|
|
314
|
+
self.y_segregatedGroup[y][:,feat_idx], self.bw_method
|
|
315
|
+
)
|
|
316
|
+
kernels[y] = kernel
|
|
317
|
+
|
|
318
|
+
pde = np.reshape(kernel(_grid).T, len(_grid))
|
|
319
|
+
pdes[y] = pde
|
|
320
|
+
|
|
321
|
+
if self.mode == "development":
|
|
322
|
+
return kernels, pdes, _grid
|
|
323
|
+
elif self.mode == "release":
|
|
324
|
+
return pdes, _grid
|
|
325
|
+
|
|
326
|
+
def segregateX_y(self):
|
|
327
|
+
"""
|
|
328
|
+
Routine to segregate X samples into unique y groups
|
|
329
|
+
"""
|
|
330
|
+
unique_y = list(set(self.y))
|
|
331
|
+
|
|
332
|
+
_subX = defaultdict()
|
|
333
|
+
_subX_sd = defaultdict()
|
|
334
|
+
_subX_mean = defaultdict()
|
|
335
|
+
for uy in unique_y:
|
|
336
|
+
_subX[uy] = self.X[np.where(self.y==uy)[0], :]
|
|
337
|
+
|
|
338
|
+
# Add 'zero' to last element to allow for scipy to carry out
|
|
339
|
+
# a Cholesky Decomposition on the variance matrix
|
|
340
|
+
_subX[uy][-1] += 1e-15
|
|
341
|
+
|
|
342
|
+
# Compute standard deviation per population per feature
|
|
343
|
+
_subX_sd[uy] = _subX[uy].std(axis=0)
|
|
344
|
+
|
|
345
|
+
# Compute mean per population per feature
|
|
346
|
+
_subX_mean[uy] = _subX[uy].mean(axis=0)
|
|
347
|
+
|
|
348
|
+
return _subX, _subX_sd, _subX_mean
|
|
349
|
+
|
|
350
|
+
def get_topnFeatures(self, n):
|
|
351
|
+
"""
|
|
352
|
+
Returns the indices of the top n features (smaller intersection areas
|
|
353
|
+
are more important).
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
n : int
|
|
358
|
+
- Desired number of top features
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
inds_topFeatures : list
|
|
363
|
+
- List of top n features, starting from the most to least important
|
|
364
|
+
features.
|
|
365
|
+
"""
|
|
366
|
+
return sorted(
|
|
367
|
+
range(len(self.intersectionAreas)),
|
|
368
|
+
key=lambda i: self.intersectionAreas[i],
|
|
369
|
+
reverse=False
|
|
370
|
+
)[:n]
|
|
371
|
+
|
|
372
|
+
def plot_overlapAreas(
|
|
373
|
+
self, feat_idx, feat_names=None, _combinations=None,
|
|
374
|
+
intersection_area=True, show_samples=False, legend=False,
|
|
375
|
+
legend_fIndex=None, _ax=None
|
|
376
|
+
):
|
|
377
|
+
"""
|
|
378
|
+
Function to plot intersection areas for a given feature.
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
feat_idx : int
|
|
383
|
+
- Index of feature to plot according to input X.
|
|
384
|
+
|
|
385
|
+
feat_names : list or None
|
|
386
|
+
- List of feature names in order of features according to input X.
|
|
387
|
+
If None, integer indices will be used.
|
|
388
|
+
|
|
389
|
+
_combinations : tuple or None
|
|
390
|
+
- Tuple of which classes to consider when plotting the
|
|
391
|
+
intersection area. If None, then plots intersection area
|
|
392
|
+
between all KDEs (k=number of class).
|
|
393
|
+
|
|
394
|
+
intersection_area : bool
|
|
395
|
+
- If True, plot shaded intersection area (k=total number of classes)
|
|
396
|
+
|
|
397
|
+
show_samples : bool
|
|
398
|
+
- If True, show samples that make up the KDEs as short vertical lines.
|
|
399
|
+
|
|
400
|
+
legend : bool, 'intersection', 'class'
|
|
401
|
+
- If true, legend would include all the class PDEs and computed
|
|
402
|
+
intersection areas. If 'intersection', only includes
|
|
403
|
+
intersection area, and 'class' only includes the classes
|
|
404
|
+
|
|
405
|
+
legend_fIndex : str or Noney
|
|
406
|
+
- Replaces the ``feat_idx`` parameter with user-defined choice.
|
|
407
|
+
|
|
408
|
+
_ax : matplotlib.axes.Axes
|
|
409
|
+
- Matplotlib's Axes object, if None, one will be created internally.
|
|
410
|
+
"""
|
|
411
|
+
if _ax is None:
|
|
412
|
+
fig, _ax = plt.subplots(1,1)
|
|
413
|
+
_ax_passed = False
|
|
414
|
+
else:
|
|
415
|
+
_ax_passed = True
|
|
416
|
+
|
|
417
|
+
linecolors = []
|
|
418
|
+
|
|
419
|
+
if _combinations is None:
|
|
420
|
+
pdes_perFeature = self.pdes[feat_idx]
|
|
421
|
+
else:
|
|
422
|
+
pdes_perFeature = defaultdict()
|
|
423
|
+
for y in _combinations:
|
|
424
|
+
if not y in self.yLabels:
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f"The class {y} was not found in the original class array y" +
|
|
427
|
+
f"\nTypes of classes: {self.yLabels}"
|
|
428
|
+
)
|
|
429
|
+
else:
|
|
430
|
+
pdes_perFeature[y] = self.pdes[feat_idx][y]
|
|
431
|
+
|
|
432
|
+
# Initialize yStack
|
|
433
|
+
yStack = []
|
|
434
|
+
for y, p_y in pdes_perFeature.items():
|
|
435
|
+
yStack.append(p_y)
|
|
436
|
+
|
|
437
|
+
# First plot all KDEs regardless of user input
|
|
438
|
+
if isinstance(legend, str):
|
|
439
|
+
if legend == "intersection":
|
|
440
|
+
class_legendlabels = False
|
|
441
|
+
area_legendlabel = True
|
|
442
|
+
show_legend = True
|
|
443
|
+
elif legend == "class":
|
|
444
|
+
class_legendlabels = True
|
|
445
|
+
area_legendlabel = False
|
|
446
|
+
show_legend = True
|
|
447
|
+
else:
|
|
448
|
+
raise ValueError(
|
|
449
|
+
"Possible values for 'legend' are 'intersection', " +
|
|
450
|
+
"'class' or boolean"
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
if legend:
|
|
454
|
+
class_legendlabels = True
|
|
455
|
+
area_legendlabel = True
|
|
456
|
+
show_legend = True
|
|
457
|
+
else:
|
|
458
|
+
class_legendlabels = False
|
|
459
|
+
area_legendlabel = False
|
|
460
|
+
show_legend = False
|
|
461
|
+
|
|
462
|
+
for y, p_y in self.pdes[feat_idx].items():
|
|
463
|
+
if class_legendlabels:
|
|
464
|
+
p = _ax.plot(self.grids[feat_idx], p_y, alpha=0.7, label=f"Class {y}")
|
|
465
|
+
else:
|
|
466
|
+
p = _ax.plot(self.grids[feat_idx], p_y, alpha=0.7)
|
|
467
|
+
|
|
468
|
+
# Get line colors
|
|
469
|
+
linecolors.append(p[0].get_color())
|
|
470
|
+
|
|
471
|
+
# Plotting the data samples
|
|
472
|
+
if show_samples:
|
|
473
|
+
yMax = _ax.get_ylim()[1]
|
|
474
|
+
for i, k in enumerate(self.pdes[feat_idx].keys()):
|
|
475
|
+
_ax.vlines(
|
|
476
|
+
self.y_segregatedGroup[k][feat_idx], 0.0, 0.03*yMax,
|
|
477
|
+
color=linecolors[i], alpha=0.7
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Getting the smallest probabilities of all the estimates at every
|
|
481
|
+
# grid point
|
|
482
|
+
yIntersection = np.amin(yStack, axis=0)
|
|
483
|
+
|
|
484
|
+
# Get OA
|
|
485
|
+
if _combinations is None:
|
|
486
|
+
OA = self.compute_intersectionArea(feat_idx, self.yLabels)
|
|
487
|
+
else:
|
|
488
|
+
OA = self.compute_intersectionArea(feat_idx, _combinations)
|
|
489
|
+
|
|
490
|
+
if show_legend:
|
|
491
|
+
if area_legendlabel:
|
|
492
|
+
if not legend_fIndex is None:
|
|
493
|
+
idxlegend = str(legend_fIndex)
|
|
494
|
+
else:
|
|
495
|
+
idxlegend = str(feat_idx)
|
|
496
|
+
|
|
497
|
+
if _combinations is None:
|
|
498
|
+
_label_underscript = idxlegend
|
|
499
|
+
else:
|
|
500
|
+
_label_underscript = str(_combinations)
|
|
501
|
+
_label_underscript = _label_underscript.replace(' ', '')
|
|
502
|
+
_label_underscript = f"{idxlegend}, {_label_underscript}"
|
|
503
|
+
|
|
504
|
+
_label = r"$A_{" + _label_underscript + r"}=$"
|
|
505
|
+
_label += str(round(OA, 3))
|
|
506
|
+
|
|
507
|
+
if intersection_area:
|
|
508
|
+
fill_poly = _ax.fill_between(
|
|
509
|
+
self.grids[feat_idx], 0, yIntersection, label=_label,
|
|
510
|
+
color="lightgray", edgecolor="lavender"
|
|
511
|
+
)
|
|
512
|
+
else:
|
|
513
|
+
if intersection_area:
|
|
514
|
+
fill_poly = _ax.fill_between(
|
|
515
|
+
self.grids[feat_idx], 0, yIntersection,
|
|
516
|
+
color="lightgray", edgecolor="lavender"
|
|
517
|
+
)
|
|
518
|
+
_ax.legend()
|
|
519
|
+
else:
|
|
520
|
+
if intersection_area:
|
|
521
|
+
fill_poly = _ax.fill_between(
|
|
522
|
+
self.grids[feat_idx], 0, yIntersection,
|
|
523
|
+
color="lightgray", edgecolor="lavender"
|
|
524
|
+
)
|
|
525
|
+
if intersection_area:
|
|
526
|
+
fill_poly.set_hatch('xxx')
|
|
527
|
+
|
|
528
|
+
if not feat_names is None:
|
|
529
|
+
_ax.set_xlabel(feat_names[feat_idx], fontsize='large')
|
|
530
|
+
else:
|
|
531
|
+
_ax.set_xlabel(f"Feature {feat_idx}", fontsize='large')
|
|
532
|
+
|
|
533
|
+
xrange = self.grids[feat_idx].max() - self.grids[feat_idx].min()
|
|
534
|
+
_ax.set_xlim(
|
|
535
|
+
(
|
|
536
|
+
self.grids[feat_idx].min()-(xrange*0.05),
|
|
537
|
+
self.grids[feat_idx].max()+(xrange*0.05)
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
_ax.set_xticks(
|
|
541
|
+
np.arange(
|
|
542
|
+
self.grids[feat_idx].min()-(xrange*0.05),
|
|
543
|
+
self.grids[feat_idx].max()+(xrange*0.05),
|
|
544
|
+
0.5
|
|
545
|
+
)
|
|
546
|
+
)
|