funcast 0.1.0__py3-none-any.whl
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.
- funcast/__init__.py +19 -0
- funcast/basis.py +102 -0
- funcast/model.py +347 -0
- funcast/selection.py +73 -0
- funcast-0.1.0.dist-info/METADATA +80 -0
- funcast-0.1.0.dist-info/RECORD +8 -0
- funcast-0.1.0.dist-info/WHEEL +4 -0
- funcast-0.1.0.dist-info/licenses/LICENSE +9 -0
funcast/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FunCast — Prévision fonctionnelle avec covariates.
|
|
3
|
+
|
|
4
|
+
Référence : Sezgin et al. (2025), "FunCast: a forecasting model for
|
|
5
|
+
functional data using covariates", Statistics and Computing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from funcast.basis import bspline_basis, fourier_basis, get_basis
|
|
9
|
+
from funcast.model import FunCast
|
|
10
|
+
from funcast.selection import select_h_rrss
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FunCast",
|
|
15
|
+
"get_basis",
|
|
16
|
+
"bspline_basis",
|
|
17
|
+
"fourier_basis",
|
|
18
|
+
"select_h_rrss",
|
|
19
|
+
]
|
funcast/basis.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functional basis for FunCast.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.interpolate import BSpline
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def bspline_basis(t: np.ndarray, n_basis: int, degree: int = 3) -> np.ndarray:
|
|
10
|
+
"""
|
|
11
|
+
Create a cubic B-spline basis matrix.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
t : np.ndarray
|
|
16
|
+
Evaluation points.
|
|
17
|
+
n_basis : int
|
|
18
|
+
Number of basis functions.
|
|
19
|
+
degree : int
|
|
20
|
+
Spline degrees. Default is 3.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
B : np.ndarray
|
|
25
|
+
Basis matrix.
|
|
26
|
+
"""
|
|
27
|
+
n_basis = max(n_basis, degree + 1)
|
|
28
|
+
|
|
29
|
+
t_min, t_max = t.min(), t.max()
|
|
30
|
+
n_inner = n_basis - degree - 1
|
|
31
|
+
|
|
32
|
+
if n_inner > 0:
|
|
33
|
+
inner_knots = np.linspace(t_min, t_max, n_inner + 2)[1:-1]
|
|
34
|
+
else:
|
|
35
|
+
inner_knots = np.array([])
|
|
36
|
+
|
|
37
|
+
knots = np.concatenate(
|
|
38
|
+
[np.repeat(t_min, degree + 1), inner_knots, np.repeat(t_max, degree + 1)]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
B = np.zeros((len(t), n_basis))
|
|
42
|
+
for j in range(n_basis):
|
|
43
|
+
coef = np.zeros(n_basis)
|
|
44
|
+
coef[j] = 1.0
|
|
45
|
+
spl = BSpline(knots, coef, degree)
|
|
46
|
+
B[:, j] = spl(t)
|
|
47
|
+
return B
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fourier_basis(t: np.ndarray, n_basis: int) -> np.ndarray:
|
|
51
|
+
"""
|
|
52
|
+
Create a Fourier basis.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
t : np.ndarray
|
|
57
|
+
Evaluation points.
|
|
58
|
+
n_basis : int
|
|
59
|
+
Number of basis functions.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
B : np.ndarray
|
|
64
|
+
Basis matrix.
|
|
65
|
+
"""
|
|
66
|
+
T = t.max() - t.min()
|
|
67
|
+
B = np.ones((len(t), n_basis))
|
|
68
|
+
for k in range(1, n_basis):
|
|
69
|
+
freq = (k + 1) // 2
|
|
70
|
+
if k % 2 == 1:
|
|
71
|
+
B[:, k] = np.cos(2 * np.pi * freq * t / T)
|
|
72
|
+
else:
|
|
73
|
+
B[:, k] = np.sin(2 * np.pi * freq * t / T)
|
|
74
|
+
return B
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_basis(t: np.ndarray, n_basis: int, basis_type: str = "bspline") -> np.ndarray:
|
|
78
|
+
"""
|
|
79
|
+
Create a basis of functions.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
t : np.ndarray
|
|
84
|
+
Evaluation points.
|
|
85
|
+
n_basis : int
|
|
86
|
+
Number of basis functions.
|
|
87
|
+
basis_type : str
|
|
88
|
+
Type of function basis, "bspline" or "fourier". Default is "bspline".
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
B : np.ndarray
|
|
93
|
+
Basis matrix.
|
|
94
|
+
"""
|
|
95
|
+
if basis_type == "bspline":
|
|
96
|
+
return bspline_basis(t, n_basis)
|
|
97
|
+
elif basis_type == "fourier":
|
|
98
|
+
return fourier_basis(t, n_basis)
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"unknown basis_type : '{basis_type}'. Choose 'bspline' or 'fourier'."
|
|
102
|
+
)
|
funcast/model.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FunCast model implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.linalg import lstsq
|
|
7
|
+
from sklearn.base import BaseEstimator
|
|
8
|
+
|
|
9
|
+
from funcast.basis import get_basis
|
|
10
|
+
from funcast.selection import select_h_rrss
|
|
11
|
+
|
|
12
|
+
if hasattr(np, "trapezoid"):
|
|
13
|
+
_trapezoid = np.trapezoid # type: ignore[attr-defined]
|
|
14
|
+
else:
|
|
15
|
+
_trapezoid = np.trapz # type: ignore[attr-defined]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FunCast(BaseEstimator):
|
|
19
|
+
"""
|
|
20
|
+
FunCast model.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
K : int, optionnal
|
|
25
|
+
Number of basis functions for the future of Y. Default is 10.
|
|
26
|
+
s : float, optionnal
|
|
27
|
+
Smoothing coefficient. Default is 0.5.
|
|
28
|
+
basis_type : str
|
|
29
|
+
Type of function basis, "bspline" or "fourier". Default is "bspline".
|
|
30
|
+
auto_h : bool, optionnal
|
|
31
|
+
If True, h_l is optimized with RRSS. Default is True.
|
|
32
|
+
h_list : list, optionnal
|
|
33
|
+
Values of h_l if auto_h is False. Default is None.
|
|
34
|
+
degree : int, optionnal
|
|
35
|
+
B-splines degree. Default is 3.
|
|
36
|
+
rcond : float or None, optionnal
|
|
37
|
+
Pseudo-inverse threshold. Default is None.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
K: int = 10,
|
|
43
|
+
s: float = 0.5,
|
|
44
|
+
basis_type: str = "bspline",
|
|
45
|
+
auto_h: bool = True,
|
|
46
|
+
h_list: list[int] | None = None,
|
|
47
|
+
degree: int = 3,
|
|
48
|
+
rcond: float | None = None,
|
|
49
|
+
):
|
|
50
|
+
self.K = K
|
|
51
|
+
self.s = s
|
|
52
|
+
self.basis_type = basis_type
|
|
53
|
+
self.auto_h = auto_h
|
|
54
|
+
self.h_list = h_list
|
|
55
|
+
self.degree = degree
|
|
56
|
+
self.rcond = rcond
|
|
57
|
+
|
|
58
|
+
def _compute_h(
|
|
59
|
+
self,
|
|
60
|
+
covariates_past: list[np.ndarray],
|
|
61
|
+
t_past: np.ndarray,
|
|
62
|
+
) -> list[int]:
|
|
63
|
+
"""
|
|
64
|
+
Compute h_l for each covariate.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
covariates_past : list of array-like
|
|
69
|
+
Past of covariates.
|
|
70
|
+
t_past : array-like
|
|
71
|
+
Past timestamps.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
h_values : list
|
|
76
|
+
Values of h_l.
|
|
77
|
+
"""
|
|
78
|
+
h_values = []
|
|
79
|
+
for ell, X in enumerate(covariates_past):
|
|
80
|
+
if self.auto_h:
|
|
81
|
+
h = select_h_rrss(
|
|
82
|
+
X,
|
|
83
|
+
t_past,
|
|
84
|
+
basis_type=self.basis_type,
|
|
85
|
+
degree=self.degree,
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
if self.h_list is None or len(self.h_list) <= ell:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"h_list required and of length p+1 if auto_h=False."
|
|
91
|
+
)
|
|
92
|
+
h = max(self.h_list[ell], self.degree + 1)
|
|
93
|
+
h_values.append(h)
|
|
94
|
+
return h_values
|
|
95
|
+
|
|
96
|
+
def _compute_q(self, h_values: list[int]) -> list[int]:
|
|
97
|
+
"""
|
|
98
|
+
Compute q_l.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
h_values : list
|
|
103
|
+
Values of h_l.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
list
|
|
108
|
+
Values of q_l.
|
|
109
|
+
"""
|
|
110
|
+
min_q = self.degree + 1
|
|
111
|
+
return [max(min_q, round((1 - self.s) * h)) for h in h_values]
|
|
112
|
+
|
|
113
|
+
def _project_covariates(
|
|
114
|
+
self,
|
|
115
|
+
covariates_past: list[np.ndarray],
|
|
116
|
+
t_past: np.ndarray,
|
|
117
|
+
h_values: list[int],
|
|
118
|
+
) -> tuple[list[np.ndarray], list[np.ndarray]]:
|
|
119
|
+
"""
|
|
120
|
+
Project each covariate on theta basis.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
covariates_past : list of array-like
|
|
125
|
+
Past of covariates.
|
|
126
|
+
t_past : array-like
|
|
127
|
+
Past timestamps.
|
|
128
|
+
h_values : list
|
|
129
|
+
Values of h_l.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
tuple
|
|
134
|
+
Expansion coefficients of the covariates.
|
|
135
|
+
"""
|
|
136
|
+
C_list, theta_list = [], []
|
|
137
|
+
for X, h in zip(covariates_past, h_values):
|
|
138
|
+
theta = get_basis(t_past, h, self.basis_type)
|
|
139
|
+
thetaT_theta = theta.T @ theta
|
|
140
|
+
C, _, _, _ = lstsq(thetaT_theta, (X @ theta).T, cond=self.rcond)
|
|
141
|
+
C_list.append(C.T)
|
|
142
|
+
theta_list.append(theta)
|
|
143
|
+
return C_list, theta_list
|
|
144
|
+
|
|
145
|
+
def _compute_J_matrices(
|
|
146
|
+
self,
|
|
147
|
+
theta_list: list[np.ndarray],
|
|
148
|
+
t_past: np.ndarray,
|
|
149
|
+
q_values: list[int],
|
|
150
|
+
) -> list[np.ndarray]:
|
|
151
|
+
"""
|
|
152
|
+
Compute the inner product matrix.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
theta_list : list of array-like
|
|
157
|
+
Basis for covariates.
|
|
158
|
+
t_past : array-like
|
|
159
|
+
Past timestamps.
|
|
160
|
+
q_values : list
|
|
161
|
+
Values of q_l.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
J_list : list
|
|
166
|
+
Inner product matrix.
|
|
167
|
+
"""
|
|
168
|
+
J_list = []
|
|
169
|
+
for theta, q_ell in zip(theta_list, q_values):
|
|
170
|
+
B_ell = get_basis(t_past, q_ell, self.basis_type)
|
|
171
|
+
integrand = theta[:, :, np.newaxis] * B_ell[:, np.newaxis, :]
|
|
172
|
+
J = _trapezoid(integrand, x=t_past, axis=0)
|
|
173
|
+
J_list.append(np.asarray(J))
|
|
174
|
+
return J_list
|
|
175
|
+
|
|
176
|
+
def _build_design_matrix(
|
|
177
|
+
self,
|
|
178
|
+
C_list: list[np.ndarray],
|
|
179
|
+
J_list: list[np.ndarray],
|
|
180
|
+
t_future: np.ndarray,
|
|
181
|
+
q_values: list[int],
|
|
182
|
+
) -> np.ndarray:
|
|
183
|
+
"""
|
|
184
|
+
Build the design matrix.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
C_list : list of array-like
|
|
189
|
+
Expansion coefficients of the covariates.
|
|
190
|
+
J_list : list
|
|
191
|
+
Inner product matrix.
|
|
192
|
+
t_future : array-like
|
|
193
|
+
Future timestamps.
|
|
194
|
+
q_values : list
|
|
195
|
+
Values of q_l.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
X_design : array-like
|
|
200
|
+
Design matrix.
|
|
201
|
+
"""
|
|
202
|
+
n = C_list[0].shape[0]
|
|
203
|
+
m2 = len(t_future)
|
|
204
|
+
K = self.K
|
|
205
|
+
q_total = sum(q_values)
|
|
206
|
+
psi = get_basis(t_future, K, self.basis_type)
|
|
207
|
+
C_full = np.concatenate(C_list, axis=1)
|
|
208
|
+
X_design = np.zeros((n * m2, K * q_total))
|
|
209
|
+
for j, t_j in enumerate(t_future):
|
|
210
|
+
psi_t = psi[j, :]
|
|
211
|
+
xi_parts = []
|
|
212
|
+
for ell, (J, q_ell) in enumerate(zip(J_list, q_values)):
|
|
213
|
+
h_ell = J.shape[0]
|
|
214
|
+
c_start = sum(C_list[e].shape[1] for e in range(ell))
|
|
215
|
+
c_end = c_start + h_ell
|
|
216
|
+
c_i_ell = C_full[:, c_start:c_end]
|
|
217
|
+
cJ = c_i_ell @ J
|
|
218
|
+
block = np.einsum("k,nq->nkq", psi_t, cJ).reshape(n, K * q_ell)
|
|
219
|
+
xi_parts.append(block)
|
|
220
|
+
xi_j = np.concatenate(xi_parts, axis=1)
|
|
221
|
+
X_design[j::m2, :] = xi_j
|
|
222
|
+
return X_design
|
|
223
|
+
|
|
224
|
+
def fit(
|
|
225
|
+
self,
|
|
226
|
+
Y_past: np.ndarray,
|
|
227
|
+
Y_future: np.ndarray,
|
|
228
|
+
t_past: np.ndarray,
|
|
229
|
+
t_future: np.ndarray,
|
|
230
|
+
covariates_past: list[np.ndarray] | None = None,
|
|
231
|
+
) -> "FunCast":
|
|
232
|
+
"""
|
|
233
|
+
Fit FunCast.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
Y_past : array-like
|
|
238
|
+
Past of Y.
|
|
239
|
+
Y_future : array-like
|
|
240
|
+
Future of Y.
|
|
241
|
+
t_past : array-like
|
|
242
|
+
Past timestamps.
|
|
243
|
+
t_future : array-like
|
|
244
|
+
Future timestamps.
|
|
245
|
+
covariates_past : list of array-like
|
|
246
|
+
Past of covariates.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
self
|
|
251
|
+
"""
|
|
252
|
+
all_covariates = [Y_past] + (covariates_past or [])
|
|
253
|
+
|
|
254
|
+
self.h_values_ = self._compute_h(all_covariates, t_past)
|
|
255
|
+
self.q_values_ = self._compute_q(self.h_values_)
|
|
256
|
+
self.C_list_, self.theta_list_ = self._project_covariates(
|
|
257
|
+
all_covariates, t_past, self.h_values_
|
|
258
|
+
)
|
|
259
|
+
self.J_list_ = self._compute_J_matrices(
|
|
260
|
+
self.theta_list_, t_past, self.q_values_
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
X_design = self._build_design_matrix(
|
|
264
|
+
self.C_list_, self.J_list_, t_future, self.q_values_
|
|
265
|
+
)
|
|
266
|
+
y = Y_future.ravel()
|
|
267
|
+
self.b_hat_, _, _, _ = lstsq(X_design, y, cond=self.rcond)
|
|
268
|
+
|
|
269
|
+
self.t_past_ = t_past
|
|
270
|
+
self.t_future_ = t_future
|
|
271
|
+
self.m2_ = Y_future.shape[1]
|
|
272
|
+
self.n_covariates_ = len(all_covariates)
|
|
273
|
+
|
|
274
|
+
return self
|
|
275
|
+
|
|
276
|
+
def predict(
|
|
277
|
+
self,
|
|
278
|
+
Y_past_new: np.ndarray,
|
|
279
|
+
covariates_past_new: list[np.ndarray] | None = None,
|
|
280
|
+
) -> np.ndarray:
|
|
281
|
+
"""
|
|
282
|
+
Predict the future of Y.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
Y_past_new : array-like
|
|
287
|
+
Past of Y for inference.
|
|
288
|
+
covariates_past_new : list of array-like
|
|
289
|
+
Past of covariates for inference.
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
Y_pred : array-like
|
|
294
|
+
Prediction of Y.
|
|
295
|
+
"""
|
|
296
|
+
all_new = [Y_past_new] + (covariates_past_new or [])
|
|
297
|
+
|
|
298
|
+
C_new_list = []
|
|
299
|
+
for X_new, theta, h in zip(all_new, self.theta_list_, self.h_values_):
|
|
300
|
+
thetaT_theta = theta.T @ theta
|
|
301
|
+
C_new, _, _, _ = lstsq(thetaT_theta, (X_new @ theta).T, cond=self.rcond)
|
|
302
|
+
C_new_list.append(np.asarray(C_new).T) # ← np.asarray() clarifie le type
|
|
303
|
+
|
|
304
|
+
X_new_design = self._build_design_matrix(
|
|
305
|
+
C_new_list, self.J_list_, self.t_future_, self.q_values_
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
n_new = Y_past_new.shape[0]
|
|
309
|
+
y_pred = X_new_design @ self.b_hat_
|
|
310
|
+
return np.asarray(y_pred).reshape(n_new, self.m2_)
|
|
311
|
+
|
|
312
|
+
def score(
|
|
313
|
+
self,
|
|
314
|
+
Y_past: np.ndarray,
|
|
315
|
+
Y_future: np.ndarray,
|
|
316
|
+
covariates_past: list[np.ndarray] | None = None,
|
|
317
|
+
metric: str = "rmse",
|
|
318
|
+
) -> float:
|
|
319
|
+
"""
|
|
320
|
+
Compute RMSE and SMAPE.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
Y_past : array-like
|
|
325
|
+
Past of Y.
|
|
326
|
+
Y_future : array-like
|
|
327
|
+
Future of Y.
|
|
328
|
+
covariates_past : list of array-like or None, optionnal
|
|
329
|
+
Past of covariates. Default is None.
|
|
330
|
+
metric : string, optionnal
|
|
331
|
+
Evaluation metric, "rmse" or "smape". Default is "rmse".
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
float
|
|
336
|
+
Metric value.
|
|
337
|
+
"""
|
|
338
|
+
Y_pred = self.predict(Y_past, covariates_past)
|
|
339
|
+
|
|
340
|
+
if metric == "rmse":
|
|
341
|
+
return float(np.sqrt(np.mean((Y_future - Y_pred) ** 2)))
|
|
342
|
+
elif metric == "smape":
|
|
343
|
+
denom = np.abs(Y_future) + np.abs(Y_pred)
|
|
344
|
+
denom = np.where(denom == 0, 1e-8, denom)
|
|
345
|
+
return float(100 * np.mean(np.abs(Y_future - Y_pred) / denom))
|
|
346
|
+
else:
|
|
347
|
+
raise ValueError(f"metric inconnu : '{metric}'. Choisir 'rmse' ou 'smape'.")
|
funcast/selection.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automatic selection of h_l with RRSS criterion.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.linalg import lstsq
|
|
9
|
+
|
|
10
|
+
from funcast.basis import get_basis
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def select_h_rrss(
|
|
14
|
+
X: np.ndarray,
|
|
15
|
+
t: np.ndarray,
|
|
16
|
+
h_candidates: list[int] | None = None,
|
|
17
|
+
basis_type: str = "bspline",
|
|
18
|
+
degree: int = 3,
|
|
19
|
+
) -> int:
|
|
20
|
+
"""
|
|
21
|
+
Selects h_l with RRSS criterion.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
X : array-like
|
|
26
|
+
Covariate realization.
|
|
27
|
+
t : array-like
|
|
28
|
+
Temporal grid.
|
|
29
|
+
h_candidates : list or None, optionnal
|
|
30
|
+
Candidates for h_l values. Default is None.
|
|
31
|
+
basis_type : str
|
|
32
|
+
Type of function basis. Default is "bspline"
|
|
33
|
+
degree : int
|
|
34
|
+
Spline degrees. Default is 3.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
h_opt : int
|
|
39
|
+
h_l value that minimizes RRSS.
|
|
40
|
+
"""
|
|
41
|
+
n, m1 = X.shape
|
|
42
|
+
min_h = degree + 1
|
|
43
|
+
|
|
44
|
+
if h_candidates is None:
|
|
45
|
+
max_h = max(min_h + 1, min(m1 // 2, 30))
|
|
46
|
+
h_candidates = list(range(min_h, max_h + 1))
|
|
47
|
+
else:
|
|
48
|
+
h_candidates = [h for h in h_candidates if h >= min_h]
|
|
49
|
+
|
|
50
|
+
if not h_candidates:
|
|
51
|
+
warnings.warn(f"No valid candidate for h_l. Set h={min_h}.")
|
|
52
|
+
return min_h
|
|
53
|
+
|
|
54
|
+
best_h, best_rrss = h_candidates[0], np.inf
|
|
55
|
+
|
|
56
|
+
for h in h_candidates:
|
|
57
|
+
denom = m1 - h
|
|
58
|
+
if denom <= 0:
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
theta = get_basis(t, h, basis_type)
|
|
62
|
+
thetaT_theta = theta.T @ theta
|
|
63
|
+
C, _, _, _ = lstsq(thetaT_theta, (X @ theta).T)
|
|
64
|
+
X_hat = (theta @ C).T
|
|
65
|
+
residuals = X - X_hat
|
|
66
|
+
rrss = np.sqrt(np.sum(residuals**2) / (n * denom))
|
|
67
|
+
if rrss < best_rrss:
|
|
68
|
+
best_rrss = rrss
|
|
69
|
+
best_h = h
|
|
70
|
+
except np.linalg.LinAlgError:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
return best_h
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: funcast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Functional data forecasting with covariates (FunCast model)
|
|
5
|
+
Project-URL: Homepage, https://github.com/SelmanSzgn/funcast
|
|
6
|
+
Project-URL: Repository, https://github.com/SelmanSzgn/funcast
|
|
7
|
+
Project-URL: Documentation, https://funcast.readthedocs.io
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/SelmanSzgn/funcast/issues
|
|
9
|
+
Author-email: Selman Sezgin <selman.szg@gmail.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2025 Selman Sezgin
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Keywords: B-spline,forecasting,functional data analysis,statistics,time series
|
|
21
|
+
Classifier: Development Status :: 3 - Alpha
|
|
22
|
+
Classifier: Intended Audience :: Science/Research
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Programming Language :: Python :: 3
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
29
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Requires-Dist: numpy>=1.24
|
|
32
|
+
Requires-Dist: scikit-learn>=1.3
|
|
33
|
+
Requires-Dist: scipy>=1.10
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
39
|
+
Provides-Extra: docs
|
|
40
|
+
Requires-Dist: myst-parser>=2.0; extra == 'docs'
|
|
41
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.25; extra == 'docs'
|
|
42
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
|
|
43
|
+
Requires-Dist: sphinx>=7.0; extra == 'docs'
|
|
44
|
+
Provides-Extra: examples
|
|
45
|
+
Requires-Dist: jupyter>=1.0; extra == 'examples'
|
|
46
|
+
Requires-Dist: matplotlib>=3.7; extra == 'examples'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
Python implementation of the FunCast functional data forecasting model.
|
|
50
|
+
|
|
51
|
+
## Paper reference
|
|
52
|
+
|
|
53
|
+
Sezgin et al. (2025), *"Funcast: a forecasting model for functional data using covariates"*, under review at Journal of Statistical Planning and Inference (JSPI).
|
|
54
|
+
|
|
55
|
+
Authors : Selman Sezgin (a, b), Julien Jacques (a), Kahina Mokrani (b) and Sylvain Allio (b)
|
|
56
|
+
|
|
57
|
+
(a) ERIC, Université Lumière Lyon 2, Lyon, France
|
|
58
|
+
|
|
59
|
+
(b) Orange Research, Belfort, France
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
``
|
|
63
|
+
pip install funcast
|
|
64
|
+
``
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
```
|
|
68
|
+
import numpy as np
|
|
69
|
+
from funcast import FunCast
|
|
70
|
+
|
|
71
|
+
n, m1, m2 = 50, 100, 20
|
|
72
|
+
t_past = np.linspace(0, 1, m1)
|
|
73
|
+
t_future = np.linspace(1, 1.2, m2)
|
|
74
|
+
Y_past = np.random.randn(n, m1)
|
|
75
|
+
Y_future = np.random.randn(n, m2)
|
|
76
|
+
|
|
77
|
+
model = FunCast(K=8, s=0.5)
|
|
78
|
+
model.fit(Y_past, Y_future, t_past, t_future)
|
|
79
|
+
Y_pred = model.predict(Y_past)
|
|
80
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
funcast/__init__.py,sha256=yieC42oHrighMK0HsqFAoZ5_cwHemzVeDyR1_7bz6Ds,490
|
|
2
|
+
funcast/basis.py,sha256=7Dt4n4h64eC82uIBg3MrBWhTSDRiIokMMHD79WW5obA,2446
|
|
3
|
+
funcast/model.py,sha256=cC8hNRiW-I0YdL_3NoFP3oUujjx_2bpUP_PQ5SCPk8o,10421
|
|
4
|
+
funcast/selection.py,sha256=oATDIddwwRnosfEYOBIpD9BLq1U8WjZ5BYtQuqVi9EQ,1854
|
|
5
|
+
funcast-0.1.0.dist-info/METADATA,sha256=w7a3Iknx8DRxfTTd7MIktskT_YxIxwG9mjYvUGAPv6w,3667
|
|
6
|
+
funcast-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
funcast-0.1.0.dist-info/licenses/LICENSE,sha256=y_187Ur1UaN3ShFLycik_BY8GE_uTgR4HzuhfYBmImM,1077
|
|
8
|
+
funcast-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Selman Sezgin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|