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

INGEOTEC / CompStats / 21356668509

26 Jan 2026 11:51AM UTC coverage: 97.805% (-0.05%) from 97.859%
21356668509

push

github

mgraffg
Confidence interval

9 of 10 new or added lines in 3 files covered. (90.0%)

1515 of 1549 relevant lines covered (97.81%)

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, CI
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
                 name:str=None,
94
                 score_func=balanced_accuracy_score,
95
                 error_func=None,
96
                 num_samples: int=500,
97
                 n_jobs: int=-1,
98
                 use_tqdm=True,
99
                 **kwargs):
100
        assert (score_func is None) ^ (error_func is None)
3✔
101
        self.score_func = score_func
3✔
102
        self.error_func = error_func
3✔
103
        algs = {}
3✔
104
        if name is not None:
3✔
105
            if isinstance(name, str):
3✔
106
                name = [name]
3✔
107
        else:
108
            name = [f'alg-{k+1}' for k, _ in enumerate(y_pred)]
3✔
109
        for key, v in zip(name, y_pred):
3✔
110
            algs[key] = np.asanyarray(v)
3✔
111
        algs.update(**kwargs)
3✔
112
        self.predictions = algs
3✔
113
        self.y_true = y_true
3✔
114
        self.num_samples = num_samples
3✔
115
        self.n_jobs = n_jobs
3✔
116
        self.use_tqdm = use_tqdm
3✔
117
        self.sorting_func = np.linalg.norm
3✔
118
        self._init()
3✔
119

120
    def _init(self):
3✔
121
        """Compute the bootstrap statistic"""
122

123
        bib = True if self.score_func is not None else False
3✔
124
        if hasattr(self, '_statistic_samples'):
3✔
125
            _ = self.statistic_samples
×
126
            _.BiB = bib
×
127
        else:
128
            _ = StatisticSamples(statistic=self.statistic_func,
3✔
129
                                 n_jobs=self.n_jobs,
130
                                 num_samples=self.num_samples,
131
                                 BiB=bib)
132
            _.samples(N=self.y_true.shape[0])
3✔
133
        self.statistic_samples = _
3✔
134

135
    def get_params(self):
3✔
136
        """Parameters"""
137

138
        return dict(y_true=self.y_true,
3✔
139
                    score_func=self.score_func,
140
                    error_func=self.error_func,
141
                    num_samples=self.num_samples,
142
                    n_jobs=self.n_jobs)
143

144
    def __sklearn_clone__(self):
3✔
145
        klass = self.__class__
3✔
146
        params = self.get_params()
3✔
147
        ins = klass(**params)
3✔
148
        ins.predictions = dict(self.predictions)
3✔
149
        ins._statistic_samples._samples = self.statistic_samples._samples
3✔
150
        ins.sorting_func = self.sorting_func
3✔
151
        return ins
3✔
152

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

168
    def __str__(self):
3✔
169
        """Prediction statistics with standard error in parenthesis"""
170
        if not isinstance(self.statistic, dict):
3✔
171
            return self.__repr__()
3✔
172

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

187
    def __call__(self, y_pred, name=None):
3✔
188
        """Add predictions"""
189
        if name is None:
3✔
190
            k = len(self.predictions) + 1
3✔
191
            if k == 0:
3✔
192
                k = 1
×
193
            name = f'alg-{k}'
3✔
194
        self.best = None
3✔
195
        self.statistic = None
3✔
196
        self.predictions[name] = np.asanyarray(y_pred)
3✔
197
        samples = self._statistic_samples
3✔
198
        calls = samples.calls
3✔
199
        if name in calls:
3✔
200
            del calls[name]
3✔
201
        return self
3✔
202

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

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

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

274
    @best.setter
3✔
275
    def best(self, value):
3✔
276
        self._best = value
3✔
277

278
    @property
3✔
279
    def sorting_func(self):
3✔
280
        """Rank systems when multiple performances are used"""
281
        return self._sorting_func
3✔
282

283
    @sorting_func.setter
3✔
284
    def sorting_func(self, value):
3✔
285
        self._sorting_func = value
3✔
286

287
    @property
3✔
288
    def statistic(self):
3✔
289
        """Statistic
290

291
        >>> from sklearn.svm import LinearSVC
292
        >>> from sklearn.ensemble import RandomForestClassifier
293
        >>> from sklearn.datasets import load_iris
294
        >>> from sklearn.model_selection import train_test_split
295
        >>> from CompStats.interface import Perf
296
        >>> X, y = load_iris(return_X_y=True)
297
        >>> _ = train_test_split(X, y, test_size=0.3)
298
        >>> X_train, X_val, y_train, y_val = _
299
        >>> m = LinearSVC().fit(X_train, y_train)
300
        >>> hy = m.predict(X_val)
301
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
302
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
303
        >>> perf.statistic
304
        {'alg-1': 1.0, 'forest': 0.9500891265597148}     
305
        """
306
        if hasattr(self, '_statistic') and self._statistic is not None:
3✔
307
            return self._statistic
3✔
308
        BiB = True if self.score_func is not None else False
3✔
309
        data = sorted([(k, self.statistic_func(self.y_true, v))
3✔
310
                       for k, v in self.predictions.items()],
311
                      key=lambda x: self.sorting_func(x[1]),
312
                      reverse=BiB)
313
        if len(data) == 1:
3✔
314
            self._statistic = data[0][1]
3✔
315
        else:
316
            self._statistic = dict(data)
3✔
317
        return self._statistic
3✔
318

319
    @statistic.setter
3✔
320
    def statistic(self, value):
3✔
321
        """statistic setter"""
322
        self._statistic = value
3✔
323

324
    @property
3✔
325
    def se(self):
3✔
326
        """Standard Error
327
    
328
        >>> from sklearn.svm import LinearSVC
329
        >>> from sklearn.ensemble import RandomForestClassifier
330
        >>> from sklearn.datasets import load_iris
331
        >>> from sklearn.model_selection import train_test_split
332
        >>> from CompStats.interface import Perf
333
        >>> X, y = load_iris(return_X_y=True)
334
        >>> _ = train_test_split(X, y, test_size=0.3)
335
        >>> X_train, X_val, y_train, y_val = _
336
        >>> m = LinearSVC().fit(X_train, y_train)
337
        >>> hy = m.predict(X_val)
338
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
339
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
340
        >>> perf.se
341
        {'alg-1': 0.0, 'forest': 0.026945730782184187}
342
        """
343

344
        output = SE(self.statistic_samples)
3✔
345
        if len(output) == 1:
3✔
346
            return list(output.values())[0]
3✔
347
        return output
3✔
348

349
    @property
3✔
350
    def ci(self):
3✔
351
        """Confidence interval
352
    
353
        >>> from sklearn.svm import LinearSVC
354
        >>> from sklearn.datasets import load_iris
355
        >>> from sklearn.model_selection import train_test_split
356
        >>> from CompStats.interface import Perf
357
        >>> X, y = load_iris(return_X_y=True)
358
        >>> _ = train_test_split(X, y, test_size=0.3)
359
        >>> X_train, X_val, y_train, y_val = _
360
        >>> m = LinearSVC().fit(X_train, y_train)
361
        >>> hy = m.predict(X_val)
362
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
363
        >>> perf = Perf(y_val, hy, name='LinearSVC')
364
        >>> perf.ci
365
        (np.float64(0.9333333333333332), np.float64(1.0))
366
        """
367

368
        output = CI(self.statistic_samples)
3✔
369
        if len(output) == 1:
3✔
370
            return list(output.values())[0]
3✔
NEW
371
        return output
×
372

373
    def plot(self, value_name:str=None,
3✔
374
             var_name:str='Performance',
375
             alg_legend:str='Algorithm',
376
             perf_names:list=None,
377
             CI:float=0.05,
378
             kind:str='point', linestyle:str='none',
379
             col_wrap:int=3, capsize:float=0.2,
380
             comparison:bool=True,
381
             right:bool=True,
382
             comp_legend:str='Comparison',
383
             winner_legend:str='Best',
384
             tie_legend:str='Equivalent',
385
             loser_legend:str='Different',
386
             **kwargs):
387
        """plot with seaborn
388

389
        >>> from sklearn.svm import LinearSVC
390
        >>> from sklearn.ensemble import RandomForestClassifier
391
        >>> from sklearn.datasets import load_iris
392
        >>> from sklearn.model_selection import train_test_split
393
        >>> from CompStats.interface import Perf
394
        >>> X, y = load_iris(return_X_y=True)
395
        >>> _ = train_test_split(X, y, test_size=0.3)
396
        >>> X_train, X_val, y_train, y_val = _
397
        >>> m = LinearSVC().fit(X_train, y_train)
398
        >>> hy = m.predict(X_val)
399
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
400
        >>> perf = Perf(y_val, hy, score_func=None,
401
                        error_func=lambda y, hy: (y != hy).mean(),
402
                        forest=ens.predict(X_val))
403
        >>> perf.plot()
404
        """
405
        import seaborn as sns
3✔
406
        if value_name is None:
3✔
407
            if self.score_func is not None:
3✔
408
                value_name = 'Score'
3✔
409
            else:
410
                value_name = 'Error'
×
411
        if not isinstance(self.statistic, dict):
3✔
412
            comparison = False
3✔
413
        best = self.best
3✔
414
        if isinstance(best, np.ndarray):
3✔
415
            if best.shape[0] < col_wrap:
3✔
416
                col_wrap = best.shape[0]
3✔
417
        df = self.dataframe(value_name=value_name, var_name=var_name,
3✔
418
                            alg_legend=alg_legend, perf_names=perf_names,
419
                            comparison=comparison, alpha=CI, right=right,
420
                            comp_legend=comp_legend, 
421
                            winner_legend=winner_legend,
422
                            tie_legend=tie_legend,
423
                            loser_legend=loser_legend)
424
        if var_name not in df.columns:
3✔
425
            var_name = None
3✔
426
            col_wrap = None
3✔
427
        ci = lambda x: measurements.CI(x, alpha=CI)
3✔
428
        if comparison:
3✔
429
            kwargs.update(dict(hue=comp_legend))
3✔
430
        f_grid = sns.catplot(df, x=value_name, errorbar=ci,
3✔
431
                             y=alg_legend, col=var_name,
432
                             kind=kind, linestyle=linestyle,
433
                             col_wrap=col_wrap, capsize=capsize, **kwargs)
434
        return f_grid
3✔
435

436
    def dataframe(self, comparison:bool=False,
3✔
437
                  right:bool=True,
438
                  alpha:float=0.05,
439
                  value_name:str='Score',
440
                  var_name:str='Performance',
441
                  alg_legend:str='Algorithm',
442
                  comp_legend:str='Comparison',
443
                  winner_legend:str='Best',
444
                  tie_legend:str='Equivalent',
445
                  loser_legend:str='Different',
446
                  perf_names:str=None):
447
        """Dataframe"""
448
        if perf_names is None and isinstance(self.best, np.ndarray):
3✔
449
            func_name = self.statistic_func.__name__
3✔
450
            perf_names = [f'{func_name}({i})'
3✔
451
                          for i, k in enumerate(self.best)]
452
        df = dataframe(self, value_name=value_name,
3✔
453
                       var_name=var_name,
454
                       alg_legend=alg_legend,
455
                       perf_names=perf_names)
456
        if not comparison:
3✔
457
            return df
3✔
458
        df[comp_legend] = tie_legend
3✔
459
        diff = self.difference()
3✔
460
        best = self.best
3✔
461
        if isinstance(best, str):
3✔
462
            for name, p in diff.p_value(right=right).items():
×
463
                if p >= alpha:
×
464
                    continue
×
465
                df.loc[df[alg_legend] == name, comp_legend] = loser_legend
×
466
            df.loc[df[alg_legend] == best, comp_legend] = winner_legend
×
467
        else:
468
            p_values = diff.p_value(right=right)
3✔
469
            systems = list(p_values.keys())
3✔
470
            p_values = np.array([p_values[k] for k in systems])
3✔
471
            for name, p_value, winner in zip(perf_names,
3✔
472
                                             p_values.T,
473
                                             best):
474
                mask = df[var_name] == name
3✔
475
                for alg, p in zip(systems, p_value):
3✔
476
                    if p >= alpha and winner != alg:
3✔
477
                        continue
3✔
478
                    _ = mask & (df[alg_legend] == alg)
3✔
479
                    if winner == alg:
3✔
480
                        df.loc[_, comp_legend] = winner_legend
3✔
481
                    else:
482
                        df.loc[_, comp_legend] = loser_legend
3✔
483
        return df
3✔
484

485
    @property
3✔
486
    def n_jobs(self):
3✔
487
        """Number of jobs to compute the statistics"""
488
        return self._n_jobs
3✔
489

490
    @n_jobs.setter
3✔
491
    def n_jobs(self, value):
3✔
492
        self._n_jobs = value
3✔
493

494
    @property
3✔
495
    def statistic_func(self):
3✔
496
        """Statistic function"""
497
        if self.score_func is not None:
3✔
498
            return self.score_func
3✔
499
        return self.error_func
3✔
500

501
    @property
3✔
502
    def statistic_samples(self):
3✔
503
        """Statistic Samples"""
504

505
        samples = self._statistic_samples
3✔
506
        algs = set(samples.calls.keys())
3✔
507
        algs = set(self.predictions.keys()) - algs
3✔
508
        if len(algs):
3✔
509
            for key in progress_bar(algs, use_tqdm=self.use_tqdm):
3✔
510
                samples(self.y_true, self.predictions[key], name=key)
3✔
511
        return self._statistic_samples
3✔
512

513
    @statistic_samples.setter
3✔
514
    def statistic_samples(self, value):
3✔
515
        self._statistic_samples = value
3✔
516

517
    @property
3✔
518
    def num_samples(self):
3✔
519
        """Number of bootstrap samples"""
520
        return self._num_samples
3✔
521

522
    @num_samples.setter
3✔
523
    def num_samples(self, value):
3✔
524
        self._num_samples = value
3✔
525

526
    @property
3✔
527
    def predictions(self):
3✔
528
        """Predictions"""
529
        return self._predictions
3✔
530

531
    @predictions.setter
3✔
532
    def predictions(self, value):
3✔
533
        self._predictions = value
3✔
534

535
    @property
3✔
536
    def y_true(self):
3✔
537
        """True output, gold standard o :math:`y`"""
538

539
        return self._y_true
3✔
540

541
    @y_true.setter
3✔
542
    def y_true(self, value):
3✔
543
        if isinstance(value, pd.DataFrame):
3✔
544
            self._y_true = value['y'].to_numpy()
3✔
545
            algs = {}
3✔
546
            for c in value.columns:
3✔
547
                if c == 'y':
3✔
548
                    continue
3✔
549
                algs[c] = value[c].to_numpy()
3✔
550
            self.predictions.update(algs)
3✔
551
            return
3✔
552
        self._y_true = np.asanyarray(value)
3✔
553

554
    @property
3✔
555
    def score_func(self):
3✔
556
        """Score function"""
557
        return self._score_func
3✔
558

559
    @score_func.setter
3✔
560
    def score_func(self, value):
3✔
561
        self._score_func = value
3✔
562
        if value is not None:
3✔
563
            self.error_func = None
3✔
564
            if hasattr(self, '_statistic_samples'):
3✔
565
                self._statistic_samples.statistic = value
×
566
                self._statistic_samples.BiB = True
×
567

568
    @property
3✔
569
    def error_func(self):
3✔
570
        """Error function"""
571
        return self._error_func
3✔
572

573
    @error_func.setter
3✔
574
    def error_func(self, value):
3✔
575
        self._error_func = value
3✔
576
        if value is not None:
3✔
577
            self.score_func = None
3✔
578
            if hasattr(self, '_statistic_samples'):
3✔
579
                self._statistic_samples.statistic = value
3✔
580
                self._statistic_samples.BiB = False
3✔
581

582

583
@dataclass
3✔
584
class Difference:
3✔
585
    """Difference
586
    
587
    >>> from sklearn.svm import LinearSVC
588
    >>> from sklearn.ensemble import RandomForestClassifier
589
    >>> from sklearn.datasets import load_iris
590
    >>> from sklearn.model_selection import train_test_split
591
    >>> from sklearn.base import clone
592
    >>> from CompStats.interface import Perf
593
    >>> X, y = load_iris(return_X_y=True)
594
    >>> _ = train_test_split(X, y, test_size=0.3)
595
    >>> X_train, X_val, y_train, y_val = _
596
    >>> m = LinearSVC().fit(X_train, y_train)
597
    >>> hy = m.predict(X_val)
598
    >>> ens = RandomForestClassifier().fit(X_train, y_train)
599
    >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
600
    >>> diff = perf.difference()
601
    >>> diff
602
    <Difference>
603
    difference p-values w.r.t alg-1
604
    0.0780 <= forest
605
    """
606

607
    statistic_samples:StatisticSamples=None
3✔
608
    statistic:dict=None
3✔
609
    best:str=None
3✔
610

611
    @property
3✔
612
    def sorting_func(self):
3✔
613
        """Rank systems when multiple performances are used"""
614
        return self._sorting_func
3✔
615
    
616
    @sorting_func.setter
3✔
617
    def sorting_func(self, value):
3✔
618
        self._sorting_func = value    
3✔
619

620
    def __repr__(self):
3✔
621
        """p-value"""
622
        return f"<{self.__class__.__name__}>\n{self}"
×
623

624
    def __str__(self):
3✔
625
        """p-value"""
626
        if isinstance(self.best, str):
3✔
627
            best = f' w.r.t {self.best}'
3✔
628
        else:
629
            best = ''
3✔
630
        output = [f"difference p-values {best}"]
3✔
631
        best = self.best
3✔
632
        if isinstance(best, np.ndarray):
3✔
633
            desc = ', '.join(best)
3✔
634
            output.append(f'{desc} <= Best')
3✔
635
        for key, value in self.p_value().items():
3✔
636
            if isinstance(value, float):
3✔
637
                output.append(f'{value:0.4f} <= {key}')
3✔
638
            else:
639
                desc = [f'{v:0.4f}' for v in value]
3✔
640
                desc = ', '.join(desc)
3✔
641
                desc = f'{desc} <= {key}'
3✔
642
                output.append(desc)
3✔
643
        return "\n".join(output)
3✔
644

645
    def _delta_best(self):
3✔
646
        """Compute multiple delta"""
647
        if isinstance(self.best, str):
3✔
648
            return self.statistic[self.best]
3✔
649
        keys = np.unique(self.best)
3✔
650
        statistic = np.array([self.statistic[k]
3✔
651
                              for k in keys])
652
        m = {v: k for k, v in enumerate(keys)}
3✔
653
        best = np.array([m[x] for x in self.best])
3✔
654
        return statistic[best, np.arange(best.shape[0])]
3✔
655

656
    def p_value(self, right:bool=True):
3✔
657
        """Compute p_value of the differences
658

659
        :param right: Estimate the p-value using :math:`\\text{sample} \\geq 2\\delta`
660
        :type right: bool  
661
        
662
        >>> from sklearn.svm import LinearSVC
663
        >>> from sklearn.ensemble import RandomForestClassifier
664
        >>> from sklearn.datasets import load_iris
665
        >>> from sklearn.model_selection import train_test_split
666
        >>> from sklearn.base import clone
667
        >>> from CompStats.interface import Perf
668
        >>> X, y = load_iris(return_X_y=True)
669
        >>> _ = train_test_split(X, y, test_size=0.3)
670
        >>> X_train, X_val, y_train, y_val = _
671
        >>> m = LinearSVC().fit(X_train, y_train)
672
        >>> hy = m.predict(X_val)
673
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
674
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
675
        >>> diff = perf.difference()
676
        >>> diff.p_value()
677
        {'forest': np.float64(0.3)}
678
        """
679
        values = []
3✔
680
        sign = 1 if self.statistic_samples.BiB else -1
3✔
681
        delta_best = self._delta_best()
3✔
682
        for k, v in self.statistic_samples.calls.items():
3✔
683
            delta = 2 * sign * (delta_best - self.statistic[k])
3✔
684
            if not isinstance(delta_best, np.ndarray):
3✔
685
                if right:
3✔
686
                    values.append((k, (v >= delta).mean()))
3✔
687
                else:
688
                    values.append((k, (v <= 0).mean()))
×
689
            else:
690
                if right:
3✔
691
                    values.append((k, (v >= delta).mean(axis=0)))
3✔
692
                else:
693
                    values.append((k, (v <= 0).mean(axis=0)))
3✔
694
        values.sort(key=lambda x: self.sorting_func(x[1]))
3✔
695
        return dict(values)
3✔
696

697
    def dataframe(self, value_name:str='Score',
3✔
698
                  var_name:str='Best',
699
                  alg_legend:str='Algorithm',
700
                  sig_legend:str='Significant',
701
                  perf_names:str=None,
702
                  right:bool=True,
703
                  alpha:float=0.05):
704
        """Dataframe"""
705
        if perf_names is None and isinstance(self.best, np.ndarray):
3✔
706
            perf_names = [f'{alg}({k})'
3✔
707
                          for k, alg in enumerate(self.best)]
708
        df = dataframe(self, value_name=value_name,
3✔
709
                       var_name=var_name,
710
                       alg_legend=alg_legend,
711
                       perf_names=perf_names)
712
        df[sig_legend] = False
3✔
713
        if isinstance(self.best, str):
3✔
714
            for name, p in self.p_value(right=right).items():
3✔
715
                if p >= alpha:
3✔
716
                    continue
3✔
717
                df.loc[df[alg_legend] == name, sig_legend] = True
3✔
718
        else:
719
            p_values = self.p_value(right=right)
3✔
720
            systems = list(p_values.keys())
3✔
721
            p_values = np.array([p_values[k] for k in systems])
3✔
722
            for name, p_value in zip(perf_names, p_values.T):
3✔
723
                mask = df[var_name] == name
3✔
724
                for alg, p in zip(systems, p_value):
3✔
725
                    if p >= alpha:
3✔
726
                        continue
3✔
727
                    _ = mask & (df[alg_legend] == alg)
3✔
728
                    df.loc[_, sig_legend] = True
3✔
729
        return df
3✔
730

731
    def plot(self, value_name:str='Difference',
3✔
732
             var_name:str='Best',
733
             alg_legend:str='Algorithm',
734
             sig_legend:str='Significant',
735
             perf_names:list=None,
736
             alpha:float=0.05,
737
             right:bool=True,
738
             kind:str='point', linestyle:str='none',
739
             col_wrap:int=3, capsize:float=0.2,
740
             set_refline:bool=True,
741
             **kwargs):
742
        """Plot
743

744
        >>> from sklearn.svm import LinearSVC
745
        >>> from sklearn.ensemble import RandomForestClassifier
746
        >>> from sklearn.datasets import load_iris
747
        >>> from sklearn.model_selection import train_test_split
748
        >>> from sklearn.base import clone
749
        >>> from CompStats.interface import Perf
750
        >>> X, y = load_iris(return_X_y=True)
751
        >>> _ = train_test_split(X, y, test_size=0.3)
752
        >>> X_train, X_val, y_train, y_val = _
753
        >>> m = LinearSVC().fit(X_train, y_train)
754
        >>> hy = m.predict(X_val)
755
        >>> ens = RandomForestClassifier().fit(X_train, y_train)
756
        >>> perf = Perf(y_val, hy, forest=ens.predict(X_val))
757
        >>> diff = perf.difference()
758
        >>> diff.plot()
759
        """
760
        import seaborn as sns
3✔
761
        df = self.dataframe(value_name=value_name,
3✔
762
                            var_name=var_name,
763
                            alg_legend=alg_legend,
764
                            sig_legend=sig_legend,
765
                            perf_names=perf_names,
766
                            alpha=alpha, right=right)
767
        title = var_name         
3✔
768
        if var_name not in df.columns:
3✔
769
            var_name = None
3✔
770
            col_wrap = None
3✔
771
        ci = lambda x: measurements.CI(x, alpha=2*alpha)
3✔
772
        f_grid = sns.catplot(df, x=value_name, errorbar=ci,
3✔
773
                             y=alg_legend, col=var_name,
774
                             kind=kind, linestyle=linestyle,
775
                             col_wrap=col_wrap, capsize=capsize,
776
                             hue=sig_legend,
777
                             **kwargs)
778
        if set_refline:
3✔
779
            f_grid.refline(x=0)
3✔
780
        if isinstance(self.best, str):
3✔
781
            f_grid.facet_axis(0, 0).set_title(f'{title} = {self.best}')
3✔
782
        return f_grid
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc