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 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
@@ -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
+ )