• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

INGEOTEC / CompStats / 13591488881

28 Feb 2025 03:40PM UTC coverage: 97.73% (-0.3%) from 98.038%
13591488881

push

github

mgraffg
Comparison in performance plot

27 of 32 new or added lines in 2 files covered. (84.38%)

1421 of 1454 relevant lines covered (97.73%)

2.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.44
/CompStats/interface.py
1
# Copyright 2025 Sergio Nava Muñoz and Mario Graff Guerrero
2

3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6

7
#     http://www.apache.org/licenses/LICENSE-2.0
8

9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
from dataclasses import dataclass
3✔
15
from sklearn.metrics import balanced_accuracy_score
3✔
16
from sklearn.base import clone
3✔
17
import pandas as pd
3✔
18
import numpy as np
3✔
19
from CompStats.bootstrap import StatisticSamples
3✔
20
from CompStats.utils import progress_bar
3✔
21
from CompStats import measurements
3✔
22
from CompStats.measurements import SE
3✔
23
from CompStats.utils import dataframe
3✔
24

25

26
class Perf(object):
3✔
27
    """Perf is an entry point to CompStats
28

29
    :param y_true: True measurement or could be a pandas.DataFrame where column label 'y' corresponds to the true measurement.
30
    :type y_true: numpy.ndarray or pandas.DataFrame
31
    :param score_func: Function to measure the performance, it is assumed that the best algorithm has the highest value.
32
    :type score_func: Function where the first argument is :math:`y` and the second is :math:`\\hat{y}.`
33
    :param error_func: Function to measure the performance where the best algorithm has the lowest value.
34
    :type error_func: Function where the first argument is :math:`y` and the second is :math:`\\hat{y}.` 
35
    :param y_pred: Predictions, the algorithms will be identified with alg-k where k=1 is the first argument included in :py:attr:`args.`
36
    :type y_pred: numpy.ndarray
37
    :param kwargs: Predictions, the algorithms will be identified using the keyword
38
    :type kwargs: numpy.ndarray
39
    :param n_jobs: Number of jobs to compute the statistic, default=-1 corresponding to use all threads.
40
    :type n_jobs: int
41
    :param num_samples: Number of bootstrap samples, default=500.
42
    :type num_samples: int
43
    :param use_tqdm: Whether to use tqdm.tqdm to visualize the progress, default=True.
44
    :type use_tqdm: bool
45

46

47
    >>> from sklearn.svm import LinearSVC
48
    >>> from sklearn.linear_model import LogisticRegression
49
    >>> from sklearn.ensemble import RandomForestClassifier
50
    >>> from sklearn.datasets import load_iris
51
    >>> from sklearn.model_selection import train_test_split
52
    >>> from sklearn.base import clone
53
    >>> from CompStats.interface import Perf
54
    >>> X, y = load_iris(return_X_y=True)
55
    >>> _ = train_test_split(X, y, test_size=0.3)
56
    >>> X_train, X_val, y_train, y_val = _
57
    >>> m = LinearSVC().fit(X_train, y_train)
58
    >>> hy = m.predict(X_val)
59
    >>> ens = RandomForestClassifier().fit(X_train, y_train)
60
    >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
61
    >>> perf
62
    <Perf>
63
    Statistic with its standard error (se)
64
    statistic (se)
65
    0.9792 (0.0221) <= alg-1
66
    0.9744 (0.0246) <= forest
67
    
68
    If an algorithm's prediction is missing, this can be included by calling the instance, as can be seen in the following instruction. Note that the algorithm's name can also be given with the keyword :py:attr:`name.`
69

70
    >>> lr = LogisticRegression().fit(X_train, y_train)
71
    >>> perf(lr.predict(X_val), name='Log. Reg.')
72
    <Perf>
73
    Statistic with its standard error (se)
74
    statistic (se)
75
    1.0000 (0.0000) <= Log. Reg.
76
    0.9792 (0.0221) <= alg-1
77
    0.9744 (0.0246) <= forest
78
    
79
    The performance function used to compare the algorithms can be changed, and the same bootstrap samples would be used if the instance were cloned. Consequently, the values are computed using the same samples, as can be seen in the following example.
80

81
    >>> perf_error = clone(perf)
82
    >>> perf_error.error_func = lambda y, hy: (y != hy).mean()
83
    >>> perf_error
84
    <Perf>
85
    Statistic with its standard error (se)
86
    statistic (se)
87
    0.0000 (0.0000) <= Log. Reg.
88
    0.0222 (0.0237) <= alg-1
89
    0.0222 (0.0215) <= forest
90

91
    """
92
    def __init__(self, y_true, *y_pred,
3✔
93
                 score_func=balanced_accuracy_score,
94
                 error_func=None,
95
                 num_samples: int=500,
96
                 n_jobs: int=-1,
97
                 use_tqdm=True,
98
                 **kwargs):
99
        assert (score_func is None) ^ (error_func is None)
3✔
100
        self.score_func = score_func
3✔
101
        self.error_func = error_func
3✔
102
        algs = {}
3✔
103
        for k, v in enumerate(y_pred):
3✔
104
            algs[f'alg-{k+1}'] = np.asanyarray(v)
3✔
105
        algs.update(**kwargs)
3✔
106
        self.predictions = algs
3✔
107
        self.y_true = y_true
3✔
108
        self.num_samples = num_samples
3✔
109
        self.n_jobs = n_jobs
3✔
110
        self.use_tqdm = use_tqdm
3✔
111
        self.sorting_func = np.linalg.norm
3✔
112
        self._init()
3✔
113

114
    def _init(self):
3✔
115
        """Compute the bootstrap statistic"""
116

117
        bib = True if self.score_func is not None else False
3✔
118
        if hasattr(self, '_statistic_samples'):
3✔
119
            _ = self.statistic_samples
×
120
            _.BiB = bib
×
121
        else:
122
            _ = StatisticSamples(statistic=self.statistic_func,
3✔
123
                                 n_jobs=self.n_jobs,
124
                                 num_samples=self.num_samples,
125
                                 BiB=bib)
126
            _.samples(N=self.y_true.shape[0])
3✔
127
        self.statistic_samples = _
3✔
128

129
    def get_params(self):
3✔
130
        """Parameters"""
131

132
        return dict(y_true=self.y_true,
3✔
133
                    score_func=self.score_func,
134
                    error_func=self.error_func,
135
                    num_samples=self.num_samples,
136
                    n_jobs=self.n_jobs)
137

138
    def __sklearn_clone__(self):
3✔
139
        klass = self.__class__
3✔
140
        params = self.get_params()
3✔
141
        ins = klass(**params)
3✔
142
        ins.predictions = dict(self.predictions)
3✔
143
        ins._statistic_samples._samples = self.statistic_samples._samples
3✔
144
        ins.sorting_func = self.sorting_func
3✔
145
        return ins
3✔
146

147
    def __repr__(self):
3✔
148
        """Prediction statistics with standard error in parenthesis"""
149
        arg = 'score_func' if self.error_func is None else 'error_func'
3✔
150
        func_name = self.statistic_func.__name__
3✔
151
        statistic = self.statistic
3✔
152
        if isinstance(statistic, dict):
3✔
153
            return f"<{self.__class__.__name__}({arg}={func_name})>\n{self}"
×
154
        elif isinstance(statistic, float):
3✔
155
            return f"<{self.__class__.__name__}({arg}={func_name}, statistic={statistic:0.4f}, se={self.se:0.4f})>"
3✔
156
        desc = [f'{k:0.4f}' for k in statistic]
3✔
157
        desc = ', '.join(desc)
3✔
158
        desc_se = [f'{k:0.4f}' for k in self.se]
3✔
159
        desc_se = ', '.join(desc_se)
3✔
160
        return f"<{self.__class__.__name__}({arg}={func_name}, statistic=[{desc}], se=[{desc_se}])>"
3✔
161

162
    def __str__(self):
3✔
163
        """Prediction statistics with standard error in parenthesis"""
164
        if not isinstance(self.statistic, dict):
3✔
165
            return self.__repr__()
3✔
166

167
        se = self.se
3✔
168
        output = ["Statistic with its standard error (se)"]
3✔
169
        output.append("statistic (se)")
3✔
170
        for key, value in self.statistic.items():
3✔
171
            if isinstance(value, float):
3✔
172
                desc = f'{value:0.4f} ({se[key]:0.4f}) <= {key}'
3✔
173
            else:
174
                desc = [f'{v:0.4f} ({k:0.4f})'
3✔
175
                        for v, k in zip(value, se[key])]
176
                desc = ', '.join(desc)
3✔
177
                desc = f'{desc} <= {key}'
3✔
178
            output.append(desc)
3✔
179
        return "\n".join(output)
3✔
180

181
    def __call__(self, y_pred, name=None):
3✔
182
        """Add predictions"""
183
        if name is None:
3✔
184
            k = len(self.predictions) + 1
3✔
185
            if k == 0:
3✔
186
                k = 1
×
187
            name = f'alg-{k}'
3✔
188
        self.best = None
3✔
189
        self.predictions[name] = np.asanyarray(y_pred)
3✔
190
        samples = self._statistic_samples
3✔
191
        calls = samples.calls
3✔
192
        if name in calls:
3✔
193
            del calls[name]
3✔
194
        return self
3✔
195

196
    def difference(self, wrt: str=None):
3✔
197
        """Compute the difference w.r.t any algorithm by default is the best
198

199
        >>> from sklearn.svm import LinearSVC
200
        >>> from sklearn.ensemble import RandomForestClassifier
201
        >>> from sklearn.datasets import load_iris
202
        >>> from sklearn.model_selection import train_test_split
203
        >>> from sklearn.base import clone
204
        >>> from CompStats.interface import Perf
205
        >>> X, y = load_iris(return_X_y=True)
206
        >>> _ = train_test_split(X, y, test_size=0.3)
207
        >>> X_train, X_val, y_train, y_val = _
208
        >>> m = LinearSVC().fit(X_train, y_train)
209
        >>> hy = m.predict(X_val)
210
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
211
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
212
        >>> perf.difference()
213
        <Difference>
214
        difference p-values w.r.t alg-1
215
        forest 0.06        
216
        """
217
        if wrt is None:
3✔
218
            wrt = self.best
3✔
219
        if isinstance(wrt, str):
3✔
220
            base = self.statistic_samples.calls[wrt]
3✔
221
        else:
222
            base = np.array([self.statistic_samples.calls[key][:, col]
3✔
223
                            for col, key in enumerate(wrt)]).T       
224
        sign = 1 if self.statistic_samples.BiB else -1
3✔
225
        diff = dict()
3✔
226
        for k, v in self.statistic_samples.calls.items():
3✔
227
            if base.ndim == 1 and k == wrt:
3✔
228
                continue
3✔
229
            diff[k] = sign * (base - v)
3✔
230
        diff_ins = Difference(statistic_samples=clone(self.statistic_samples),
3✔
231
                              statistic=self.statistic)
232
        diff_ins.sorting_func = self.sorting_func
3✔
233
        diff_ins.statistic_samples.calls = diff
3✔
234
        diff_ins.statistic_samples.info['best'] = self.best
3✔
235
        diff_ins.best = self.best
3✔
236
        return diff_ins
3✔
237

238
    @property
3✔
239
    def best(self):
3✔
240
        """System with best performance"""
241
        if hasattr(self, '_best') and self._best is not None:
3✔
242
            return self._best
3✔
243
        if not isinstance(self.statistic, dict):
3✔
244
            key, value = list(self.statistic_samples.calls.items())[0]
3✔
245
            if value.ndim == 1:
3✔
246
                self._best = key
3✔
247
            else:
248
                self._best = np.array([key] * value.shape[1])
3✔
249
            return self._best
3✔
250
        BiB = bool(self.statistic_samples.BiB)
3✔
251
        keys = np.array(list(self.statistic.keys()))
3✔
252
        data = np.asanyarray([self.statistic[k]
3✔
253
                              for k in keys])        
254
        if isinstance(self.statistic[keys[0]], np.ndarray):
3✔
255
            if BiB:
3✔
256
                best = data.argmax(axis=0)
3✔
257
            else:
258
                best = data.argmin(axis=0)
×
259
        else:
260
            if BiB:
3✔
261
                best = data.argmax()
3✔
262
            else:
263
                best = data.argmin()
×
264
        self._best = keys[best]
3✔
265
        return self._best
3✔
266
    
267
    @best.setter
3✔
268
    def best(self, value):
3✔
269
        self._best = value
3✔
270

271
    @property
3✔
272
    def sorting_func(self):
3✔
273
        """Rank systems when multiple performances are used"""
274
        return self._sorting_func
3✔
275
    
276
    @sorting_func.setter
3✔
277
    def sorting_func(self, value):
3✔
278
        self._sorting_func = value
3✔
279

280
    @property
3✔
281
    def statistic(self):
3✔
282
        """Statistic
283

284
        >>> from sklearn.svm import LinearSVC
285
        >>> from sklearn.ensemble import RandomForestClassifier
286
        >>> from sklearn.datasets import load_iris
287
        >>> from sklearn.model_selection import train_test_split
288
        >>> from CompStats.interface import Perf
289
        >>> X, y = load_iris(return_X_y=True)
290
        >>> _ = train_test_split(X, y, test_size=0.3)
291
        >>> X_train, X_val, y_train, y_val = _
292
        >>> m = LinearSVC().fit(X_train, y_train)
293
        >>> hy = m.predict(X_val)
294
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
295
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
296
        >>> perf.statistic
297
        {'alg-1': 1.0, 'forest': 0.9500891265597148}     
298
        """
299

300
        data = sorted([(k, self.statistic_func(self.y_true, v))
3✔
301
                       for k, v in self.predictions.items()],
302
                      key=lambda x: self.sorting_func(x[1]), 
303
                      reverse=self.statistic_samples.BiB)
304
        if len(data) == 1:
3✔
305
            return data[0][1]
3✔
306
        return dict(data)
3✔
307

308
    @property
3✔
309
    def se(self):
3✔
310
        """Standard Error
311
    
312
        >>> from sklearn.svm import LinearSVC
313
        >>> from sklearn.ensemble import RandomForestClassifier
314
        >>> from sklearn.datasets import load_iris
315
        >>> from sklearn.model_selection import train_test_split
316
        >>> from CompStats.interface import Perf
317
        >>> X, y = load_iris(return_X_y=True)
318
        >>> _ = train_test_split(X, y, test_size=0.3)
319
        >>> X_train, X_val, y_train, y_val = _
320
        >>> m = LinearSVC().fit(X_train, y_train)
321
        >>> hy = m.predict(X_val)
322
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
323
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
324
        >>> perf.se
325
        {'alg-1': 0.0, 'forest': 0.026945730782184187}
326
        """
327

328
        output = SE(self.statistic_samples)
3✔
329
        if len(output) == 1:
3✔
330
            return list(output.values())[0]
3✔
331
        return output
3✔
332

333
    def plot(self, value_name:str=None,
3✔
334
             var_name:str='Performance',
335
             alg_legend:str='Algorithm',
336
             perf_names:list=None,
337
             CI:float=0.05,
338
             kind:str='point', linestyle:str='none',
339
             col_wrap:int=3, capsize:float=0.2,
340
             comparison:bool=True,
341
             right:bool=True,
342
             comp_legend:str='Comparison',
343
             winner_legend:str='Best',
344
             tie_legend:str='Equivalent',
345
             loser_legend:str='Different',
346
             **kwargs):
347
        """plot with seaborn
348

349
        >>> from sklearn.svm import LinearSVC
350
        >>> from sklearn.ensemble import RandomForestClassifier
351
        >>> from sklearn.datasets import load_iris
352
        >>> from sklearn.model_selection import train_test_split
353
        >>> from CompStats.interface import Perf
354
        >>> X, y = load_iris(return_X_y=True)
355
        >>> _ = train_test_split(X, y, test_size=0.3)
356
        >>> X_train, X_val, y_train, y_val = _
357
        >>> m = LinearSVC().fit(X_train, y_train)
358
        >>> hy = m.predict(X_val)
359
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
360
        >>> perf = Perf(y_val, hy, score_func=None,
361
                        error_func=lambda y, hy: (y != hy).mean(),
362
                        forest=ens.predict(X_val))
363
        >>> perf.plot()
364
        """
365
        import seaborn as sns
3✔
366
        if value_name is None:
3✔
367
            if self.score_func is not None:
3✔
368
                value_name = 'Score'
3✔
369
            else:
370
                value_name = 'Error'
×
371
        if not isinstance(self.statistic, dict):
3✔
372
            comparison = False
3✔
373
        df = self.dataframe(value_name=value_name, var_name=var_name,
3✔
374
                            alg_legend=alg_legend, perf_names=perf_names,
375
                            comparison=comparison, alpha=CI, right=right,
376
                            comp_legend=comp_legend, 
377
                            winner_legend=winner_legend,
378
                            tie_legend=tie_legend,
379
                            loser_legend=loser_legend)
380
        if var_name not in df.columns:
3✔
381
            var_name = None
3✔
382
            col_wrap = None
3✔
383
        ci = lambda x: measurements.CI(x, alpha=CI)
3✔
384
        if comparison:
3✔
385
            kwargs.update(dict(hue=comp_legend))
3✔
386
        f_grid = sns.catplot(df, x=value_name, errorbar=ci,
3✔
387
                             y=alg_legend, col=var_name,
388
                             kind=kind, linestyle=linestyle,
389
                             col_wrap=col_wrap, capsize=capsize, **kwargs)
390
        return f_grid
3✔
391

392
    def dataframe(self, comparison:bool=False,
3✔
393
                  right:bool=True,
394
                  alpha:float=0.05,
395
                  value_name:str='Score',
396
                  var_name:str='Performance',
397
                  alg_legend:str='Algorithm',
398
                  comp_legend:str='Comparison',
399
                  winner_legend:str='Best',
400
                  tie_legend:str='Equivalent',
401
                  loser_legend:str='Different',
402
                  perf_names:str=None):
403
        """Dataframe"""
404
        if perf_names is None and isinstance(self.best, np.ndarray):
3✔
405
            func_name = self.statistic_func.__name__
3✔
406
            perf_names = [f'{func_name}({i})'
3✔
407
                          for i, k in enumerate(self.best)]
408
        df = dataframe(self, value_name=value_name,
3✔
409
                       var_name=var_name,
410
                       alg_legend=alg_legend,
411
                       perf_names=perf_names)
412
        if not comparison:
3✔
413
            return df
3✔
414
        df[comp_legend] = tie_legend
3✔
415
        diff = self.difference()
3✔
416
        best = self.best
3✔
417
        if isinstance(best, str):
3✔
NEW
418
            for name, p in diff.p_value(right=right).items():
×
NEW
419
                if p >= alpha:
×
NEW
420
                    continue
×
NEW
421
                df.loc[df[alg_legend] == name, comp_legend] = loser_legend
×
NEW
422
            df.loc[df[alg_legend] == best, comp_legend] = winner_legend
×
423
        else:
424
            p_values = diff.p_value(right=right)
3✔
425
            systems = list(p_values.keys())
3✔
426
            p_values = np.array([p_values[k] for k in systems])
3✔
427
            for name, p_value, winner in zip(perf_names,
3✔
428
                                             p_values.T,
429
                                             best):
430
                mask = df[var_name] == name
3✔
431
                for alg, p in zip(systems, p_value):
3✔
432
                    if p >= alpha and winner != alg:
3✔
433
                        continue
3✔
434
                    _ = mask & (df[alg_legend] == alg)
3✔
435
                    if winner == alg:
3✔
436
                        df.loc[_, comp_legend] = winner_legend
3✔
437
                    else:
438
                        df.loc[_, comp_legend] = loser_legend
3✔
439
        return df
3✔
440

441
    @property
3✔
442
    def n_jobs(self):
3✔
443
        """Number of jobs to compute the statistics"""
444
        return self._n_jobs
3✔
445

446
    @n_jobs.setter
3✔
447
    def n_jobs(self, value):
3✔
448
        self._n_jobs = value
3✔
449

450
    @property
3✔
451
    def statistic_func(self):
3✔
452
        """Statistic function"""
453
        if self.score_func is not None:
3✔
454
            return self.score_func
3✔
455
        return self.error_func
3✔
456

457
    @property
3✔
458
    def statistic_samples(self):
3✔
459
        """Statistic Samples"""
460

461
        samples = self._statistic_samples
3✔
462
        algs = set(samples.calls.keys())
3✔
463
        algs = set(self.predictions.keys()) - algs
3✔
464
        if len(algs):
3✔
465
            for key in progress_bar(algs, use_tqdm=self.use_tqdm):
3✔
466
                samples(self.y_true, self.predictions[key], name=key)
3✔
467
        return self._statistic_samples
3✔
468

469
    @statistic_samples.setter
3✔
470
    def statistic_samples(self, value):
3✔
471
        self._statistic_samples = value
3✔
472

473
    @property
3✔
474
    def num_samples(self):
3✔
475
        """Number of bootstrap samples"""
476
        return self._num_samples
3✔
477

478
    @num_samples.setter
3✔
479
    def num_samples(self, value):
3✔
480
        self._num_samples = value
3✔
481

482
    @property
3✔
483
    def predictions(self):
3✔
484
        """Predictions"""
485
        return self._predictions
3✔
486

487
    @predictions.setter
3✔
488
    def predictions(self, value):
3✔
489
        self._predictions = value
3✔
490

491
    @property
3✔
492
    def y_true(self):
3✔
493
        """True output, gold standard o :math:`y`"""
494

495
        return self._y_true
3✔
496

497
    @y_true.setter
3✔
498
    def y_true(self, value):
3✔
499
        if isinstance(value, pd.DataFrame):
3✔
500
            self._y_true = value['y'].to_numpy()
3✔
501
            algs = {}
3✔
502
            for c in value.columns:
3✔
503
                if c == 'y':
3✔
504
                    continue
3✔
505
                algs[c] = value[c].to_numpy()
3✔
506
            self.predictions.update(algs)
3✔
507
            return
3✔
508
        self._y_true = value
3✔
509

510
    @property
3✔
511
    def score_func(self):
3✔
512
        """Score function"""
513
        return self._score_func
3✔
514

515
    @score_func.setter
3✔
516
    def score_func(self, value):
3✔
517
        self._score_func = value
3✔
518
        if value is not None:
3✔
519
            self.error_func = None
3✔
520
            if hasattr(self, '_statistic_samples'):
3✔
521
                self._statistic_samples.statistic = value
×
522
                self._statistic_samples.BiB = True
×
523

524
    @property
3✔
525
    def error_func(self):
3✔
526
        """Error function"""
527
        return self._error_func
3✔
528

529
    @error_func.setter
3✔
530
    def error_func(self, value):
3✔
531
        self._error_func = value
3✔
532
        if value is not None:
3✔
533
            self.score_func = None
3✔
534
            if hasattr(self, '_statistic_samples'):
3✔
535
                self._statistic_samples.statistic = value
3✔
536
                self._statistic_samples.BiB = False
3✔
537

538

539
@dataclass
3✔
540
class Difference:
3✔
541
    """Difference
542
    
543
    >>> from sklearn.svm import LinearSVC
544
    >>> from sklearn.ensemble import RandomForestClassifier
545
    >>> from sklearn.datasets import load_iris
546
    >>> from sklearn.model_selection import train_test_split
547
    >>> from sklearn.base import clone
548
    >>> from CompStats.interface import Perf
549
    >>> X, y = load_iris(return_X_y=True)
550
    >>> _ = train_test_split(X, y, test_size=0.3)
551
    >>> X_train, X_val, y_train, y_val = _
552
    >>> m = LinearSVC().fit(X_train, y_train)
553
    >>> hy = m.predict(X_val)
554
    >>> ens = RandomForestClassifier().fit(X_train, y_train)
555
    >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
556
    >>> diff = perf.difference()
557
    >>> diff
558
    <Difference>
559
    difference p-values w.r.t alg-1
560
    0.0780 <= forest
561
    """
562

563
    statistic_samples:StatisticSamples=None
3✔
564
    statistic:dict=None
3✔
565
    best:str=None
3✔
566

567
    @property
3✔
568
    def sorting_func(self):
3✔
569
        """Rank systems when multiple performances are used"""
570
        return self._sorting_func
3✔
571
    
572
    @sorting_func.setter
3✔
573
    def sorting_func(self, value):
3✔
574
        self._sorting_func = value    
3✔
575

576
    def __repr__(self):
3✔
577
        """p-value"""
578
        return f"<{self.__class__.__name__}>\n{self}"
×
579

580
    def __str__(self):
3✔
581
        """p-value"""
582
        if isinstance(self.best, str):
3✔
583
            best = f' w.r.t {self.best}'
3✔
584
        else:
585
            best = ''
3✔
586
        output = [f"difference p-values {best}"]
3✔
587
        best = self.best
3✔
588
        if isinstance(best, np.ndarray):
3✔
589
            desc = ', '.join(best)
3✔
590
            output.append(f'{desc} <= Best')
3✔
591
        for key, value in self.p_value().items():
3✔
592
            if isinstance(value, float):
3✔
593
                output.append(f'{value:0.4f} <= {key}')
3✔
594
            else:
595
                desc = [f'{v:0.4f}' for v in value]
3✔
596
                desc = ', '.join(desc)
3✔
597
                desc = f'{desc} <= {key}'
3✔
598
                output.append(desc)
3✔
599
        return "\n".join(output)
3✔
600

601
    def _delta_best(self):
3✔
602
        """Compute multiple delta"""
603
        if isinstance(self.best, str):
3✔
604
            return self.statistic[self.best]
3✔
605
        keys = np.unique(self.best)
3✔
606
        statistic = np.array([self.statistic[k]
3✔
607
                              for k in keys])
608
        m = {v: k for k, v in enumerate(keys)}
3✔
609
        best = np.array([m[x] for x in self.best])
3✔
610
        return statistic[best, np.arange(best.shape[0])]
3✔
611

612
    def p_value(self, right:bool=True):
3✔
613
        """Compute p_value of the differences
614

615
        :param right: Estimate the p-value using :math:`\\text{sample} \\geq 2\\delta`
616
        :type right: bool  
617
        
618
        >>> from sklearn.svm import LinearSVC
619
        >>> from sklearn.ensemble import RandomForestClassifier
620
        >>> from sklearn.datasets import load_iris
621
        >>> from sklearn.model_selection import train_test_split
622
        >>> from sklearn.base import clone
623
        >>> from CompStats.interface import Perf
624
        >>> X, y = load_iris(return_X_y=True)
625
        >>> _ = train_test_split(X, y, test_size=0.3)
626
        >>> X_train, X_val, y_train, y_val = _
627
        >>> m = LinearSVC().fit(X_train, y_train)
628
        >>> hy = m.predict(X_val)
629
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
630
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
631
        >>> diff = perf.difference()
632
        >>> diff.p_value()
633
        {'forest': np.float64(0.3)}
634
        """
635
        values = []
3✔
636
        sign = 1 if self.statistic_samples.BiB else -1
3✔
637
        delta_best = self._delta_best()
3✔
638
        for k, v in self.statistic_samples.calls.items():
3✔
639
            delta = 2 * sign * (delta_best - self.statistic[k])
3✔
640
            if not isinstance(delta_best, np.ndarray):
3✔
641
                if right:
3✔
642
                    values.append((k, (v >= delta).mean()))
3✔
643
                else:
644
                    values.append((k, (v <= 0).mean()))
×
645
            else:
646
                if right:
3✔
647
                    values.append((k, (v >= delta).mean(axis=0)))
3✔
648
                else:
649
                    values.append((k, (v <= 0).mean(axis=0)))
3✔
650
        values.sort(key=lambda x: self.sorting_func(x[1]))
3✔
651
        return dict(values)
3✔
652

653
    def dataframe(self, value_name:str='Score',
3✔
654
                  var_name:str='Best',
655
                  alg_legend:str='Algorithm',
656
                  sig_legend:str='Significant',
657
                  perf_names:str=None,
658
                  right:bool=True,
659
                  alpha:float=0.05):
660
        """Dataframe"""
661
        if perf_names is None and isinstance(self.best, np.ndarray):
3✔
662
            perf_names = [f'{alg}({k})'
3✔
663
                          for k, alg in enumerate(self.best)]
664
        df = dataframe(self, value_name=value_name,
3✔
665
                       var_name=var_name,
666
                       alg_legend=alg_legend,
667
                       perf_names=perf_names)
668
        df[sig_legend] = False
3✔
669
        if isinstance(self.best, str):
3✔
670
            for name, p in self.p_value(right=right).items():
3✔
671
                if p >= alpha:
3✔
672
                    continue
3✔
673
                df.loc[df[alg_legend] == name, sig_legend] = True
3✔
674
        else:
675
            p_values = self.p_value(right=right)
3✔
676
            systems = list(p_values.keys())
3✔
677
            p_values = np.array([p_values[k] for k in systems])
3✔
678
            for name, p_value in zip(perf_names, p_values.T):
3✔
679
                mask = df[var_name] == name
3✔
680
                for alg, p in zip(systems, p_value):
3✔
681
                    if p >= alpha:
3✔
682
                        continue
3✔
683
                    _ = mask & (df[alg_legend] == alg)
3✔
684
                    df.loc[_, sig_legend] = True
3✔
685
        return df
3✔
686

687
    def plot(self, value_name:str='Difference',
3✔
688
             var_name:str='Best',
689
             alg_legend:str='Algorithm',
690
             sig_legend:str='Significant',
691
             perf_names:list=None,
692
             alpha:float=0.05,
693
             right:bool=True,
694
             kind:str='point', linestyle:str='none',
695
             col_wrap:int=3, capsize:float=0.2,
696
             set_refline:bool=True,
697
             **kwargs):
698
        """Plot
699

700
        >>> from sklearn.svm import LinearSVC
701
        >>> from sklearn.ensemble import RandomForestClassifier
702
        >>> from sklearn.datasets import load_iris
703
        >>> from sklearn.model_selection import train_test_split
704
        >>> from sklearn.base import clone
705
        >>> from CompStats.interface import Perf
706
        >>> X, y = load_iris(return_X_y=True)
707
        >>> _ = train_test_split(X, y, test_size=0.3)
708
        >>> X_train, X_val, y_train, y_val = _
709
        >>> m = LinearSVC().fit(X_train, y_train)
710
        >>> hy = m.predict(X_val)
711
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
712
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
713
        >>> diff = perf.difference()
714
        >>> diff.plot()
715
        """
716
        import seaborn as sns
3✔
717
        df = self.dataframe(value_name=value_name,
3✔
718
                            var_name=var_name,
719
                            alg_legend=alg_legend,
720
                            sig_legend=sig_legend,
721
                            perf_names=perf_names,
722
                            alpha=alpha, right=right)
723
        title = var_name         
3✔
724
        if var_name not in df.columns:
3✔
725
            var_name = None
3✔
726
            col_wrap = None
3✔
727
        ci = lambda x: measurements.CI(x, alpha=2*alpha)
3✔
728
        f_grid = sns.catplot(df, x=value_name, errorbar=ci,
3✔
729
                             y=alg_legend, col=var_name,
730
                             kind=kind, linestyle=linestyle,
731
                             col_wrap=col_wrap, capsize=capsize,
732
                             hue=sig_legend,
733
                             **kwargs)
734
        if set_refline:
3✔
735
            f_grid.refline(x=0)
3✔
736
        if isinstance(self.best, str):
3✔
737
            f_grid.facet_axis(0, 0).set_title(f'{title} = {self.best}')
3✔
738
        return f_grid
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc