online-cp 0.0.1__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.
- online_cp-0.0.1/LICENCE +8 -0
- online_cp-0.0.1/PKG-INFO +108 -0
- online_cp-0.0.1/README.md +92 -0
- online_cp-0.0.1/pyproject.toml +28 -0
- online_cp-0.0.1/setup.cfg +4 -0
- online_cp-0.0.1/src/online_cp/CPS.py +292 -0
- online_cp-0.0.1/src/online_cp/__init__.py +5 -0
- online_cp-0.0.1/src/online_cp/classifiers.py +327 -0
- online_cp-0.0.1/src/online_cp/kernels.py +136 -0
- online_cp-0.0.1/src/online_cp/martingale.py +114 -0
- online_cp-0.0.1/src/online_cp/regressors.py +1254 -0
- online_cp-0.0.1/src/online_cp.egg-info/PKG-INFO +108 -0
- online_cp-0.0.1/src/online_cp.egg-info/SOURCES.txt +14 -0
- online_cp-0.0.1/src/online_cp.egg-info/dependency_links.txt +1 -0
- online_cp-0.0.1/src/online_cp.egg-info/requires.txt +2 -0
- online_cp-0.0.1/src/online_cp.egg-info/top_level.txt +2 -0
online_cp-0.0.1/LICENCE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright 2024 Johan Hallberg Szabadváry
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
7
|
+
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
8
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
online_cp-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: online-cp
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An implementation of online conformal prediction
|
|
5
|
+
Author-email: Johan Hallberg Szabadvary <johan.hallberg.szabadvary@ju.se>
|
|
6
|
+
Project-URL: Homepage, https://github.com/egonmedhatten/OnlineConformalPrediction
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/egonmedhatten/OnlineConformalPrediction/issues
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENCE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
|
|
17
|
+
# online-cp -- Online Conformal Prediction
|
|
18
|
+
|
|
19
|
+
This project is an implementation of Online Conformal Prediction.
|
|
20
|
+
|
|
21
|
+
For now, take a look at [`example.ipynb`](example.ipynb) to see how to use the library.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
Let's create a dataset with noisy evaluations of the function $f(x_1, x_2) = x_1 + x_2$.
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
import numpy as np
|
|
29
|
+
N = 30
|
|
30
|
+
X = np.random.uniform(0, 1, (N, 2))
|
|
31
|
+
y = X.sum(axis=1) + np.random.normal(0, 0.1, N)
|
|
32
|
+
cp.learn_initial_training_set(X, y)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Import the library and create a regressor:
|
|
36
|
+
|
|
37
|
+
```py
|
|
38
|
+
from online_cp import ConformalRidgeRegressor
|
|
39
|
+
cp = ConformalRidgeRegressor()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Alternative 1: Learn the whole dataset online
|
|
43
|
+
```py
|
|
44
|
+
cp.learn_initial_training_set(X, y)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Predict an object (your output may not be exactly the same, as the dataset depends on the random seed).
|
|
48
|
+
```py
|
|
49
|
+
cp.predict(np.array([0.5, 0.5]), epsilon=0.1, bounds='both')
|
|
50
|
+
(0.8065748777057368, 1.2222461945130274)
|
|
51
|
+
```
|
|
52
|
+
The prediction set is the closed interval whose boundaries are indicated by the output.
|
|
53
|
+
|
|
54
|
+
Alternative 2: Learn the dataset sequentially online, and make predictions as we go. In order to output nontrivial prediction at significance level $\epsilon=0.1$, we need to have learned at least 20 examples.
|
|
55
|
+
```py
|
|
56
|
+
cp = ConformalRidgeRegressor()
|
|
57
|
+
for i, (obj, lab) in enumerate(zip(X, y)):
|
|
58
|
+
print(cp.predict(obj, epsilon=0.1, bounds='both'))
|
|
59
|
+
cp.learn_one(obj, lab)
|
|
60
|
+
```
|
|
61
|
+
The output will be ```(inf, inf)``` for the first 19 predictions, after which we will typically see meaningful prediction sets.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Future considerations
|
|
65
|
+
|
|
66
|
+
### Release minimal version
|
|
67
|
+
For use in projects, it may be good to have a released minimal version of OnlineConformalPrediction. Initially, it could include
|
|
68
|
+
* Conformalised Ridge Regression
|
|
69
|
+
* Plugin martingale
|
|
70
|
+
* Possibly Conformalised Nearest Neighbours Regression (but I will have to check it carefully for any bugs)
|
|
71
|
+
|
|
72
|
+
### Properties of CPs?
|
|
73
|
+
* Should we keep track of errors internally in the parent class?
|
|
74
|
+
* Should we store the average interval size?
|
|
75
|
+
* For classifiers; should we store the efficiency metrics?
|
|
76
|
+
|
|
77
|
+
### Linear regression
|
|
78
|
+
We will initally focus on regression, but online classification is actually easier. A simple class that uses e.g. scikit-learn classifiers to define nonconformity measure could be easily implemented.
|
|
79
|
+
|
|
80
|
+
There are at least three commonly used regularisations used in linear regression, all of which are compatible with the kernel trick.
|
|
81
|
+
* $L1$ (Lasso)
|
|
82
|
+
* $L2$ (Ridge)
|
|
83
|
+
* Linear combination of the above (Elastic net)
|
|
84
|
+
|
|
85
|
+
All of these can be conformalized, and at least Ridge can also be used in conformal predictive systems (CPS).
|
|
86
|
+
|
|
87
|
+
Another relatively simple regressor is the k-nearest neighbours algorithm, which is very flexible as it can use arbitrary distances. It is particularly interesting in the CPS setting. The distance can be measured in feature space as defined by a kernel.
|
|
88
|
+
|
|
89
|
+
Ridge and KNN are described in detail in Algorithmic Learning in a Random World. Lasso and Elastic net are conformalised in the paper Fast Exact Conformalization of Lasso using Piecewise Linear Homotopy, but I am unaware of any extention to CPS.
|
|
90
|
+
|
|
91
|
+
### Teaching schedule
|
|
92
|
+
Section 3.3 in Algorithmic Learning in a Radnom World deals with, so called, weak teachers. In the pure online mode, labels arrive immediately after a predition is made. This makes little sense in practice. The notion of a teaching schedule formalises this, and makes the relevant validity guarantees clear. There are three types of validity; weak, strong, and iterated logartihm validity.
|
|
93
|
+
|
|
94
|
+
There may be settings where the user wants to specify a teaching schedule beforehand, to guarantee some property of validity. It may also be the case that the teaching schedule is implied by the usage, and it would then be useful to know if the resulting prediciton sets are valid.
|
|
95
|
+
|
|
96
|
+
A teaching schedule also serves as documentation of what has been done, which could be useful in practice.
|
|
97
|
+
|
|
98
|
+
## Todo
|
|
99
|
+
* Should we add some scaler? Don't know if it is neccesary for Ridge
|
|
100
|
+
* Possibly add a class MimoConformalRidgeRegressor
|
|
101
|
+
* Add CPS version of ridge regressor?
|
|
102
|
+
* Possibly add a TeachingSchedule?
|
|
103
|
+
* Possibly add ACI, both for single, and MIMO CRR?
|
|
104
|
+
* Add references to papers and books to README
|
|
105
|
+
* Add k-NN regressor and CPS
|
|
106
|
+
|
|
107
|
+
# References
|
|
108
|
+
How to cite papers? I think I have seen it in some repos.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# online-cp -- Online Conformal Prediction
|
|
2
|
+
|
|
3
|
+
This project is an implementation of Online Conformal Prediction.
|
|
4
|
+
|
|
5
|
+
For now, take a look at [`example.ipynb`](example.ipynb) to see how to use the library.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
Let's create a dataset with noisy evaluations of the function $f(x_1, x_2) = x_1 + x_2$.
|
|
10
|
+
|
|
11
|
+
```py
|
|
12
|
+
import numpy as np
|
|
13
|
+
N = 30
|
|
14
|
+
X = np.random.uniform(0, 1, (N, 2))
|
|
15
|
+
y = X.sum(axis=1) + np.random.normal(0, 0.1, N)
|
|
16
|
+
cp.learn_initial_training_set(X, y)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Import the library and create a regressor:
|
|
20
|
+
|
|
21
|
+
```py
|
|
22
|
+
from online_cp import ConformalRidgeRegressor
|
|
23
|
+
cp = ConformalRidgeRegressor()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Alternative 1: Learn the whole dataset online
|
|
27
|
+
```py
|
|
28
|
+
cp.learn_initial_training_set(X, y)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Predict an object (your output may not be exactly the same, as the dataset depends on the random seed).
|
|
32
|
+
```py
|
|
33
|
+
cp.predict(np.array([0.5, 0.5]), epsilon=0.1, bounds='both')
|
|
34
|
+
(0.8065748777057368, 1.2222461945130274)
|
|
35
|
+
```
|
|
36
|
+
The prediction set is the closed interval whose boundaries are indicated by the output.
|
|
37
|
+
|
|
38
|
+
Alternative 2: Learn the dataset sequentially online, and make predictions as we go. In order to output nontrivial prediction at significance level $\epsilon=0.1$, we need to have learned at least 20 examples.
|
|
39
|
+
```py
|
|
40
|
+
cp = ConformalRidgeRegressor()
|
|
41
|
+
for i, (obj, lab) in enumerate(zip(X, y)):
|
|
42
|
+
print(cp.predict(obj, epsilon=0.1, bounds='both'))
|
|
43
|
+
cp.learn_one(obj, lab)
|
|
44
|
+
```
|
|
45
|
+
The output will be ```(inf, inf)``` for the first 19 predictions, after which we will typically see meaningful prediction sets.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Future considerations
|
|
49
|
+
|
|
50
|
+
### Release minimal version
|
|
51
|
+
For use in projects, it may be good to have a released minimal version of OnlineConformalPrediction. Initially, it could include
|
|
52
|
+
* Conformalised Ridge Regression
|
|
53
|
+
* Plugin martingale
|
|
54
|
+
* Possibly Conformalised Nearest Neighbours Regression (but I will have to check it carefully for any bugs)
|
|
55
|
+
|
|
56
|
+
### Properties of CPs?
|
|
57
|
+
* Should we keep track of errors internally in the parent class?
|
|
58
|
+
* Should we store the average interval size?
|
|
59
|
+
* For classifiers; should we store the efficiency metrics?
|
|
60
|
+
|
|
61
|
+
### Linear regression
|
|
62
|
+
We will initally focus on regression, but online classification is actually easier. A simple class that uses e.g. scikit-learn classifiers to define nonconformity measure could be easily implemented.
|
|
63
|
+
|
|
64
|
+
There are at least three commonly used regularisations used in linear regression, all of which are compatible with the kernel trick.
|
|
65
|
+
* $L1$ (Lasso)
|
|
66
|
+
* $L2$ (Ridge)
|
|
67
|
+
* Linear combination of the above (Elastic net)
|
|
68
|
+
|
|
69
|
+
All of these can be conformalized, and at least Ridge can also be used in conformal predictive systems (CPS).
|
|
70
|
+
|
|
71
|
+
Another relatively simple regressor is the k-nearest neighbours algorithm, which is very flexible as it can use arbitrary distances. It is particularly interesting in the CPS setting. The distance can be measured in feature space as defined by a kernel.
|
|
72
|
+
|
|
73
|
+
Ridge and KNN are described in detail in Algorithmic Learning in a Random World. Lasso and Elastic net are conformalised in the paper Fast Exact Conformalization of Lasso using Piecewise Linear Homotopy, but I am unaware of any extention to CPS.
|
|
74
|
+
|
|
75
|
+
### Teaching schedule
|
|
76
|
+
Section 3.3 in Algorithmic Learning in a Radnom World deals with, so called, weak teachers. In the pure online mode, labels arrive immediately after a predition is made. This makes little sense in practice. The notion of a teaching schedule formalises this, and makes the relevant validity guarantees clear. There are three types of validity; weak, strong, and iterated logartihm validity.
|
|
77
|
+
|
|
78
|
+
There may be settings where the user wants to specify a teaching schedule beforehand, to guarantee some property of validity. It may also be the case that the teaching schedule is implied by the usage, and it would then be useful to know if the resulting prediciton sets are valid.
|
|
79
|
+
|
|
80
|
+
A teaching schedule also serves as documentation of what has been done, which could be useful in practice.
|
|
81
|
+
|
|
82
|
+
## Todo
|
|
83
|
+
* Should we add some scaler? Don't know if it is neccesary for Ridge
|
|
84
|
+
* Possibly add a class MimoConformalRidgeRegressor
|
|
85
|
+
* Add CPS version of ridge regressor?
|
|
86
|
+
* Possibly add a TeachingSchedule?
|
|
87
|
+
* Possibly add ACI, both for single, and MIMO CRR?
|
|
88
|
+
* Add references to papers and books to README
|
|
89
|
+
* Add k-NN regressor and CPS
|
|
90
|
+
|
|
91
|
+
# References
|
|
92
|
+
How to cite papers? I think I have seen it in some repos.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "online-cp"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Johan Hallberg Szabadvary", email="johan.hallberg.szabadvary@ju.se" },
|
|
6
|
+
]
|
|
7
|
+
description = "An implementation of online conformal prediction"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
dependencies = [
|
|
10
|
+
'numpy',
|
|
11
|
+
'scipy',
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: BSD License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
"Homepage" = "https://github.com/egonmedhatten/OnlineConformalPrediction"
|
|
22
|
+
"Bug Tracker" = "https://github.com/egonmedhatten/OnlineConformalPrediction/issues"
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
pythonpath = ["src"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import time
|
|
3
|
+
import warnings
|
|
4
|
+
from scipy.spatial.distance import pdist, cdist, squareform
|
|
5
|
+
|
|
6
|
+
MACHINE_EPSILON = np.finfo(np.float64).eps
|
|
7
|
+
|
|
8
|
+
class ConformalPredictiveSystem:
|
|
9
|
+
'''
|
|
10
|
+
Parent class for conformal predictive systems. Unclear if some methods are common to all, so perhaps we don't need it.
|
|
11
|
+
'''
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NearestNeighboursPredictionMachine(ConformalPredictiveSystem):
|
|
15
|
+
|
|
16
|
+
def __init__(self, k, distance='euclidean', distance_func=None, warnings=True, verbose=0, rnd_state=None):
|
|
17
|
+
'''
|
|
18
|
+
Consider adding possibility to update self.k as the training set grows, e.g. by some heuristic or something.
|
|
19
|
+
Two rules of thumb are quite simple:
|
|
20
|
+
1. Choose k close to sqrt(n) where n is the training set size
|
|
21
|
+
2. If the data has large variance, choose k larger. If the variance is small, choose k smaller. This is less clear, however.
|
|
22
|
+
'''
|
|
23
|
+
|
|
24
|
+
self.k = k
|
|
25
|
+
|
|
26
|
+
self.distance = distance
|
|
27
|
+
if distance_func is None:
|
|
28
|
+
self.distance_func = self._standard_distance_func
|
|
29
|
+
else:
|
|
30
|
+
self.distance_func = distance_func
|
|
31
|
+
self.distance = 'custom'
|
|
32
|
+
|
|
33
|
+
self.X = None
|
|
34
|
+
self.y = None
|
|
35
|
+
self.D = None
|
|
36
|
+
|
|
37
|
+
# Should we raise warnings
|
|
38
|
+
self.warnings = warnings
|
|
39
|
+
|
|
40
|
+
self.verbose = verbose
|
|
41
|
+
self.rnd_gen = np.random.default_rng(rnd_state)
|
|
42
|
+
|
|
43
|
+
def _standard_distance_func(self, X, y=None):
|
|
44
|
+
'''
|
|
45
|
+
By default we use scipy to compute distances
|
|
46
|
+
'''
|
|
47
|
+
X = np.atleast_2d(X)
|
|
48
|
+
if y is None:
|
|
49
|
+
dists = squareform(pdist(X, metric=self.distance))
|
|
50
|
+
else:
|
|
51
|
+
y = np.atleast_2d(y)
|
|
52
|
+
dists = cdist(X, y, metric=self.distance)
|
|
53
|
+
return dists
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def learn_initial_training_set(self, X, y):
|
|
57
|
+
self.X = X
|
|
58
|
+
self.y = y
|
|
59
|
+
self.D = self.distance_func(X)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def update_distance_matrix(D, d):
|
|
64
|
+
return np.block([[D, d], [d.T, np.array([0])]])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def learn_one(self, x, y, precomputed=None):
|
|
68
|
+
'''
|
|
69
|
+
precomputed is a dictionary
|
|
70
|
+
{
|
|
71
|
+
'X': X,
|
|
72
|
+
'D': D,
|
|
73
|
+
}
|
|
74
|
+
'''
|
|
75
|
+
# Learn label y
|
|
76
|
+
if self.y is None:
|
|
77
|
+
self.y = np.array([y])
|
|
78
|
+
else:
|
|
79
|
+
self.y = np.append(self.y, y)
|
|
80
|
+
|
|
81
|
+
if precomputed is None:
|
|
82
|
+
# Learn object x
|
|
83
|
+
if self.X is None:
|
|
84
|
+
self.X = x.reshape(1,-1)
|
|
85
|
+
self.D = self.distance_func(self.X)
|
|
86
|
+
else:
|
|
87
|
+
d = self.distance_func(self.X, x)
|
|
88
|
+
self.D = self.update_distance_matrix(self.D, d)
|
|
89
|
+
self.X = np.append(self.X, x.reshape(1, -1), axis=0)
|
|
90
|
+
else:
|
|
91
|
+
self.X = precomputed['X']
|
|
92
|
+
self.D = precomputed['D']
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def predict_cpd(self, x, return_update=False, save_time=False):
|
|
96
|
+
|
|
97
|
+
'''
|
|
98
|
+
TODO Add possibility to return precomputed as we did with ConformalRegressor.
|
|
99
|
+
'''
|
|
100
|
+
tic = time.time()
|
|
101
|
+
# Temporarily update the distance matrix
|
|
102
|
+
if self.X is None:
|
|
103
|
+
X = x.reshape(1,-1)
|
|
104
|
+
D = self.distance_func(X)
|
|
105
|
+
y = np.array(-np.inf) # Initialise label as -inf
|
|
106
|
+
else:
|
|
107
|
+
d = self.distance_func(self.X, x)
|
|
108
|
+
D = self.update_distance_matrix(self.D, d)
|
|
109
|
+
X = np.append(self.X, x.reshape(1, -1), axis=0)
|
|
110
|
+
y = np.append(self.y, -np.inf) # Initialise label as -inf
|
|
111
|
+
toc_dist = time.time()-tic
|
|
112
|
+
|
|
113
|
+
tic = time.time()
|
|
114
|
+
# Find all neighbours and semi-neighbours
|
|
115
|
+
# NOTE I have a feeling that this could be stored in order to save time... Investigate later
|
|
116
|
+
k_nearest = D.argsort(axis=0)[1:self.k+1]
|
|
117
|
+
toc_sort = time.time() - tic
|
|
118
|
+
|
|
119
|
+
tic = time.time()
|
|
120
|
+
idx_all_neighbours_and_semi_neighbours = []
|
|
121
|
+
|
|
122
|
+
full_neighbours = []
|
|
123
|
+
single_neighbours = []
|
|
124
|
+
semi_neighbours = []
|
|
125
|
+
|
|
126
|
+
n = D.shape[0] - 1
|
|
127
|
+
k_nearest_of_n = k_nearest.T[n]
|
|
128
|
+
for i, col in enumerate(k_nearest.T):
|
|
129
|
+
if i in k_nearest_of_n and n in col:
|
|
130
|
+
# print(f'{i} is a full neighbour')
|
|
131
|
+
idx_all_neighbours_and_semi_neighbours.append(i)
|
|
132
|
+
full_neighbours.append(y[i])
|
|
133
|
+
if i in k_nearest_of_n and n not in col:
|
|
134
|
+
# print(f'{i} is a single neighbour')
|
|
135
|
+
idx_all_neighbours_and_semi_neighbours.append(i)
|
|
136
|
+
single_neighbours.append(y[i])
|
|
137
|
+
if i not in k_nearest_of_n and n in col:
|
|
138
|
+
# print(f'{i} is a semi-neighbour')
|
|
139
|
+
idx_all_neighbours_and_semi_neighbours.append(i)
|
|
140
|
+
semi_neighbours.append(y[i])
|
|
141
|
+
toc_find_neighbours = time.time() - tic
|
|
142
|
+
|
|
143
|
+
# Line 1
|
|
144
|
+
Kprime = len(idx_all_neighbours_and_semi_neighbours)
|
|
145
|
+
# Line 2 and 3
|
|
146
|
+
Y = np.zeros(shape=(Kprime+2,))
|
|
147
|
+
Y[0] = -np.inf
|
|
148
|
+
Y[-1] = np.inf
|
|
149
|
+
Y[1:-1] = y[idx_all_neighbours_and_semi_neighbours]
|
|
150
|
+
Y.sort()
|
|
151
|
+
|
|
152
|
+
# Line 4
|
|
153
|
+
Alpha = -np.inf * np.ones(n + 1) # Initialize at something unreasonable
|
|
154
|
+
N = -np.inf * np.ones(self.k + 1) # Initialize at something unreasonable
|
|
155
|
+
for i in range(n + 1):
|
|
156
|
+
J = k_nearest.T[i]
|
|
157
|
+
Alpha[i] = np.where(y[J] <= y[i])[0].shape[0]
|
|
158
|
+
for k in range(self.k + 1):
|
|
159
|
+
N[k] = np.where(Alpha == k)[0].shape[0]
|
|
160
|
+
|
|
161
|
+
# Line 5
|
|
162
|
+
L = -np.inf * np.ones(Kprime+1) # Initialize at something unreasonable
|
|
163
|
+
U = -np.inf * np.ones(Kprime+1) # Initialize at something unreasonable
|
|
164
|
+
L[0] = 0
|
|
165
|
+
U[0] = N[0]/(n+1)
|
|
166
|
+
|
|
167
|
+
tic = time.time()
|
|
168
|
+
# Line 6
|
|
169
|
+
for k in range(1, Kprime + 1):
|
|
170
|
+
# FIXME Something is wrong with this loop... Very difficult to tell what.
|
|
171
|
+
# Line 7
|
|
172
|
+
|
|
173
|
+
if Y[k] in full_neighbours or Y[k] in single_neighbours:
|
|
174
|
+
# Line 8
|
|
175
|
+
N[int(Alpha[-1])] -= 1
|
|
176
|
+
Alpha[n] += 1
|
|
177
|
+
N[int(Alpha[-1])] += 1
|
|
178
|
+
|
|
179
|
+
# Line 9
|
|
180
|
+
if Y[k] in full_neighbours or Y[k] in semi_neighbours:
|
|
181
|
+
# Line 10
|
|
182
|
+
N[int(Alpha[k])] -= 1
|
|
183
|
+
Alpha[k] -= 1
|
|
184
|
+
N[int(Alpha[k])] += 1
|
|
185
|
+
|
|
186
|
+
# Line 11
|
|
187
|
+
L[k] = N[:int(Alpha[-1])].sum() / (n + 1) if Alpha[-1] != 0 else 0
|
|
188
|
+
U[k] = N[:int(Alpha[-1]) + 1].sum() / (n + 1)
|
|
189
|
+
toc_loop = time.time() - tic
|
|
190
|
+
|
|
191
|
+
time_dict = {
|
|
192
|
+
'Compute distance matrix': toc_dist,
|
|
193
|
+
'Sort distance matrix': toc_sort,
|
|
194
|
+
'Find all neighbours and semi-neighbours': toc_find_neighbours,
|
|
195
|
+
'Loop': toc_loop
|
|
196
|
+
}
|
|
197
|
+
time_dict = time_dict if save_time else None
|
|
198
|
+
# Line 12
|
|
199
|
+
cps = KnnConformalPredictiveDistributionFunction(L, U, Y, time_dict)
|
|
200
|
+
|
|
201
|
+
if return_update:
|
|
202
|
+
return cps, {'X': X, 'D': D}
|
|
203
|
+
else:
|
|
204
|
+
return cps
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ConformalPredictiveDistributionFunction:
|
|
208
|
+
|
|
209
|
+
'''
|
|
210
|
+
NOTE
|
|
211
|
+
The CPD contains all the information needed to form a
|
|
212
|
+
prediction set. We can take quantiles and so on.
|
|
213
|
+
'''
|
|
214
|
+
|
|
215
|
+
def quantile(self, quantile, tau):
|
|
216
|
+
raise NotImplementedError('Parent class has not quantile function')
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def predict_set(self, tau, epsilon=0.1, bounds='both'):
|
|
220
|
+
'''
|
|
221
|
+
The convex hull of the epsilon/2 and 1-epsilon/2 quantiles make up
|
|
222
|
+
the prediction set Gamma(epsilon)
|
|
223
|
+
TODO Add things like bounds, so we can predict just upper or lower...
|
|
224
|
+
Make it possible to pass two epsilons to predict skewed intervals...
|
|
225
|
+
'''
|
|
226
|
+
q1 = epsilon/2
|
|
227
|
+
q2 = 1 - epsilon/2
|
|
228
|
+
if bounds=='both':
|
|
229
|
+
lower = self.quantile(q1, tau)
|
|
230
|
+
upper = self.quantile(q2, tau)
|
|
231
|
+
elif bounds=='lower':
|
|
232
|
+
lower = self.quantile(q1, tau)
|
|
233
|
+
upper = np.inf
|
|
234
|
+
elif bounds=='upper':
|
|
235
|
+
lower = -np.inf
|
|
236
|
+
upper = self.quantile(q2, tau)
|
|
237
|
+
else:
|
|
238
|
+
raise Exception
|
|
239
|
+
|
|
240
|
+
# print(f'Lower: {lower}')
|
|
241
|
+
# print(f'Upper: {upper}')
|
|
242
|
+
return lower, upper
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# These methods relate to when the cpd is used to predict sets
|
|
246
|
+
@staticmethod
|
|
247
|
+
def err(Gamma, y):
|
|
248
|
+
return int(not(Gamma[0] <= y <= Gamma[1]))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def width(Gamma):
|
|
253
|
+
return Gamma[1] - Gamma[0]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class KnnConformalPredictiveDistributionFunction(ConformalPredictiveDistributionFunction):
|
|
258
|
+
|
|
259
|
+
def __init__(self, L, U, Y, time_dict=None):
|
|
260
|
+
self.L = L
|
|
261
|
+
self.U = U
|
|
262
|
+
self.Y = Y
|
|
263
|
+
|
|
264
|
+
self.time_dict = time_dict
|
|
265
|
+
|
|
266
|
+
def __call__(self, y, tau=None):
|
|
267
|
+
Y = self.Y[:-1]
|
|
268
|
+
idx_eq = np.where(y == Y)[0]
|
|
269
|
+
if idx_eq.shape[0] > 0:
|
|
270
|
+
k = idx_eq.min()
|
|
271
|
+
interval = (self.L[k-1], self.U[k])
|
|
272
|
+
else:
|
|
273
|
+
k = np.where(Y <= y)[0].max()
|
|
274
|
+
interval = (self.L[k], self.U[k])
|
|
275
|
+
|
|
276
|
+
Pi0 = interval[0]
|
|
277
|
+
Pi1 = interval[1]
|
|
278
|
+
|
|
279
|
+
if tau is None:
|
|
280
|
+
return Pi0, Pi1
|
|
281
|
+
else:
|
|
282
|
+
return (1 - tau) * Pi0 + tau * Pi1
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def quantile(self, quantile, tau):
|
|
286
|
+
q = np.inf
|
|
287
|
+
for y in self.Y[::-1]:
|
|
288
|
+
if self.__call__(y, tau) >= quantile:
|
|
289
|
+
q = y
|
|
290
|
+
else:
|
|
291
|
+
return q
|
|
292
|
+
return q
|