\n",
999 | "\n",
1012 | "
\n",
1013 | " \n",
1014 | " \n",
1015 | " | \n",
1016 | " 1 | \n",
1017 | " 2 | \n",
1018 | " 3 | \n",
1019 | " 4 | \n",
1020 | " 5 | \n",
1021 | " 6 | \n",
1022 | " 7 | \n",
1023 | " 8 | \n",
1024 | " 9 | \n",
1025 | " 10 | \n",
1026 | "
\n",
1027 | " \n",
1028 | " \n",
1029 | " \n",
1030 | " | Normal | \n",
1031 | " 0.143916 | \n",
1032 | " 0.065354 | \n",
1033 | " 0.072771 | \n",
1034 | " 0.072959 | \n",
1035 | " 0.072172 | \n",
1036 | " 0.072553 | \n",
1037 | " 0.072384 | \n",
1038 | " 0.072357 | \n",
1039 | " 0.073190 | \n",
1040 | " 0.065104 | \n",
1041 | "
\n",
1042 | " \n",
1043 | " | MB-Percentile | \n",
1044 | " 0.138397 | \n",
1045 | " 0.067852 | \n",
1046 | " 0.075306 | \n",
1047 | " 0.075343 | \n",
1048 | " 0.074802 | \n",
1049 | " 0.075333 | \n",
1050 | " 0.075051 | \n",
1051 | " 0.075192 | \n",
1052 | " 0.075521 | \n",
1053 | " 0.067160 | \n",
1054 | "
\n",
1055 | " \n",
1056 | " | MB-Pivotal | \n",
1057 | " 0.138397 | \n",
1058 | " 0.067852 | \n",
1059 | " 0.075306 | \n",
1060 | " 0.075343 | \n",
1061 | " 0.074802 | \n",
1062 | " 0.075333 | \n",
1063 | " 0.075051 | \n",
1064 | " 0.075192 | \n",
1065 | " 0.075521 | \n",
1066 | " 0.067160 | \n",
1067 | "
\n",
1068 | " \n",
1069 | " | MB-Normal | \n",
1070 | " 0.141345 | \n",
1071 | " 0.068888 | \n",
1072 | " 0.076665 | \n",
1073 | " 0.076569 | \n",
1074 | " 0.075947 | \n",
1075 | " 0.076416 | \n",
1076 | " 0.076144 | \n",
1077 | " 0.076233 | \n",
1078 | " 0.076892 | \n",
1079 | " 0.068287 | \n",
1080 | "
\n",
1081 | " \n",
1082 | "
\n",
1083 | "
"
1084 | ],
1085 | "text/plain": [
1086 | " 1 2 3 4 5 6 \\\n",
1087 | "Normal 0.143916 0.065354 0.072771 0.072959 0.072172 0.072553 \n",
1088 | "MB-Percentile 0.138397 0.067852 0.075306 0.075343 0.074802 0.075333 \n",
1089 | "MB-Pivotal 0.138397 0.067852 0.075306 0.075343 0.074802 0.075333 \n",
1090 | "MB-Normal 0.141345 0.068888 0.076665 0.076569 0.075947 0.076416 \n",
1091 | "\n",
1092 | " 7 8 9 10 \n",
1093 | "Normal 0.072384 0.072357 0.073190 0.065104 \n",
1094 | "MB-Percentile 0.075051 0.075192 0.075521 0.067160 \n",
1095 | "MB-Pivotal 0.075051 0.075192 0.075521 0.067160 \n",
1096 | "MB-Normal 0.076144 0.076233 0.076892 0.068287 "
1097 | ]
1098 | },
1099 | "execution_count": 10,
1100 | "metadata": {},
1101 | "output_type": "execute_result"
1102 | }
1103 | ],
1104 | "source": [
1105 | "width"
1106 | ]
1107 | }
1108 | ],
1109 | "metadata": {
1110 | "kernelspec": {
1111 | "display_name": "Python 3 (ipykernel)",
1112 | "language": "python",
1113 | "name": "python3"
1114 | },
1115 | "language_info": {
1116 | "codemirror_mode": {
1117 | "name": "ipython",
1118 | "version": 3
1119 | },
1120 | "file_extension": ".py",
1121 | "mimetype": "text/x-python",
1122 | "name": "python",
1123 | "nbconvert_exporter": "python",
1124 | "pygments_lexer": "ipython3",
1125 | "version": "3.11.7"
1126 | }
1127 | },
1128 | "nbformat": 4,
1129 | "nbformat_minor": 5
1130 | }
1131 |
--------------------------------------------------------------------------------
/conquer/joint.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import numpy.random as rgt
3 |
4 | from scipy.stats import norm
5 | from scipy.optimize import minimize
6 | from scipy.sparse import csc_matrix
7 |
8 | from sklearn.metrics import pairwise_kernels as PK
9 | from sklearn.kernel_ridge import KernelRidge as KR
10 |
11 | from qpsolvers import Problem, solve_problem
12 | from cvxopt import solvers, matrix
13 | solvers.options['show_progress'] = False
14 |
15 | import torch
16 | import torch.nn as nn
17 | import torch.optim as optim
18 | import matplotlib.pyplot as plt
19 |
20 | from torch.utils.data import DataLoader, TensorDataset
21 | from torch.utils.data.dataset import random_split
22 | from torch.distributions.normal import Normal
23 | from enum import Enum
24 |
25 | from conquer.linear import low_dim
26 |
27 |
28 |
29 | ###############################################################################
30 | ######################### Linear QuantES Regression ###########################
31 | ###############################################################################
32 | class LR(low_dim):
33 | '''
34 | Joint Linear Quantile and Expected Shortfall Regression
35 | '''
36 | def _FZ_loss(self, x, tau, G1=False, G2_type=1):
37 | '''
38 | Fissler and Ziegel's Joint Loss Function
39 |
40 | Args:
41 | G1 : logical flag for the specification function G1 in FZ's loss;
42 | G1(x)=0 if G1=False, and G1(x)=x and G1=True.
43 | G2_type : an integer (from 1 to 5) that indicates the type.
44 | of the specification function G2 in FZ's loss.
45 |
46 | Returns:
47 | FZ loss function value.
48 | '''
49 |
50 | X = self.X
51 | if G2_type in {1, 2, 3}:
52 | Ymax = np.max(self.Y)
53 | Y = self.Y - Ymax
54 | else:
55 | Y = self.Y
56 | dim = X.shape[1]
57 | Yq = X @ x[:dim]
58 | Ye = X @ x[dim : 2*dim]
59 | f0, f1, _ = G2(G2_type)
60 | loss = f1(Ye) * (Ye - Yq - (Y - Yq) * (Y<= Yq) / tau) - f0(Ye)
61 | if G1:
62 | return np.mean((tau - (Y<=Yq)) * (Y-Yq) + loss)
63 | else:
64 | return np.mean(loss)
65 |
66 |
67 | def joint_fit(self, tau=0.5, G1=False, G2_type=1,
68 | standardize=True, refit=True, tol=None,
69 | options={'maxiter': None, 'maxfev': None, 'disp': False,
70 | 'return_all': False, 'initial_simplex': None,
71 | 'xatol': 0.0001, 'fatol': 0.0001, 'adaptive': False}):
72 | '''
73 | Joint Quantile & Expected Shortfall Regression via FZ Loss Minimization
74 |
75 | Refs:
76 | Higher Order Elicitability and Osband's Principle
77 | by Tobias Fissler and Johanna F. Ziegel
78 | Ann. Statist. 44(4): 1680-1707, 2016
79 |
80 | A Joint Quantile and Expected Shortfall Regression Framework
81 | by Timo Dimitriadis and Sebastian Bayer
82 | Electron. J. Stat. 13(1): 1823-1871, 2019
83 |
84 | Args:
85 | tau : quantile level; default is 0.5.
86 | G1 : logical flag for the specification function G1 in FZ's loss;
87 | G1(x)=0 if G1=False, and G1(x)=x and G1=True.
88 | G2_type : an integer (from 1 to 5) that indicates the type of the specification function G2 in FZ's loss.
89 | standardize : logical flag for x variable standardization prior to fitting the quantile model;
90 | default is TRUE.
91 | refit : logical flag for refitting joint regression if the optimization is terminated early;
92 | default is TRUE.
93 | tol : tolerance for termination.
94 | options : a dictionary of solver options;
95 | see https://docs.scipy.org/doc/scipy/reference/optimize.minimize-neldermead.html
96 |
97 | Returns:
98 | 'coef_q' : quantile regression coefficient estimate.
99 | 'coef_e' : expected shortfall regression coefficient estimate.
100 | 'nit' : total number of iterations.
101 | 'nfev' : total number of function evaluations.
102 | 'message' : a message that describes the cause of the termination.
103 | '''
104 |
105 | dim = self.X.shape[1]
106 | Ymax = np.max(self.Y)
107 | ### warm start with QR + truncated least squares
108 | qrfit = low_dim(self.X[:, self.itcp:], self.Y, intercept=True)\
109 | .fit(tau=tau, standardize=standardize)
110 | coef_q = qrfit['beta']
111 | tail = self.Y <= self.X @ coef_q
112 | tail_X = self.X[tail,:]
113 | tail_Y = self.Y[tail]
114 | coef_e = np.linalg.solve(tail_X.T @ tail_X, tail_X.T @ tail_Y)
115 | if G2_type in {1, 2, 3}:
116 | coef_q[0] -= Ymax
117 | coef_e[0] -= Ymax
118 | x0 = np.r_[(coef_q, coef_e)]
119 |
120 | ### joint quantile and ES fit
121 | fun = lambda x : self._FZ_loss(x, tau, G1, G2_type)
122 | esfit = minimize(fun, x0, method='Nelder-Mead',
123 | tol=tol, options=options)
124 | nit, nfev = esfit['nit'], esfit['nfev']
125 |
126 | ### refit if convergence criterion is not met
127 | while refit and not esfit['success']:
128 | esfit = minimize(fun, esfit['x'], method='Nelder-Mead',
129 | tol=tol, options=options)
130 | nit += esfit['nit']
131 | nfev += esfit['nfev']
132 |
133 | coef_q, coef_e = esfit['x'][:dim], esfit['x'][dim : 2*dim]
134 | if G2_type in {1, 2, 3}:
135 | coef_q[0] += Ymax
136 | coef_e[0] += Ymax
137 |
138 | return {'coef_q': coef_q, 'coef_e': coef_e,
139 | 'nit': nit, 'nfev': nfev,
140 | 'success': esfit['success'],
141 | 'message': esfit['message']}
142 |
143 |
144 | def twostep_fit(self, tau=0.5, h=None, kernel='Laplacian',
145 | loss='L2', robust=None, G2_type=1,
146 | standardize=True, tol=None, options=None,
147 | ci=False, level=0.95):
148 | '''
149 | Two-Step Procedure for Joint QuantES Regression
150 |
151 | Refs:
152 | Higher Order Elicitability and Osband's Principle
153 | by Tobias Fissler and Johanna F. Ziegel
154 | Ann. Statist. 44(4): 1680-1707, 2016
155 |
156 | Effciently Weighted Estimation of Tail and Interquantile Expectations
157 | by Sander Barendse
158 | SSRN Preprint, 2020
159 |
160 | Robust Estimation and Inference
161 | for Expected Shortfall Regression with Many Regressors
162 | by Xuming He, Kean Ming Tan and Wen-Xin Zhou
163 | J. R. Stat. Soc. B. 85(4): 1223-1246, 2023
164 |
165 | Inference for Joint Quantile and Expected Shortfall Regression
166 | by Xiang Peng and Huixia Judy Wang
167 | Stat 12(1) e619, 2023
168 |
169 | Args:
170 | tau : quantile level; default is 0.5.
171 | h : bandwidth; the default value is computed by self.bandwidth(tau).
172 | kernel : a character string representing one of the built-in smoothing kernels;
173 | default is "Laplacian".
174 | loss : the loss function used in stage two. There are three options.
175 | 1. 'L2': squared/L2 loss;
176 | 2. 'Huber': Huber loss;
177 | 3. 'FZ': Fissler and Ziegel's joint loss.
178 | robust : robustification parameter in the Huber loss;
179 | if robust=None, it will be automatically determined in a data-driven way;
180 | G2_type : an integer (from 1 to 5) that indicates the type of the specification function G2 in FZ's loss.
181 | standardize : logical flag for x variable standardization prior to fitting the quantile model;
182 | default is TRUE.
183 | tol : tolerance for termination.
184 | options : a dictionary of solver options;
185 | see https://docs.scipy.org/doc/scipy/reference/optimize.minimize-bfgs.html.
186 | ci : logical flag for computing normal-based confidence intervals.
187 | level : confidence level between 0 and 1.
188 |
189 | Returns:
190 | 'coef_q' : quantile regression coefficient estimate.
191 | 'res_q' : a vector of fitted quantile regression residuals.
192 | 'coef_e' : expected shortfall regression coefficient estimate.
193 | 'robust' : robustification parameter in the Huber loss.
194 | 'ci' : coordinates-wise (100*level)% confidence intervals.
195 | '''
196 |
197 | if loss in {'L2', 'Huber', 'TrunL2', 'TrunHuber'}:
198 | qrfit = self.fit(tau=tau, h=h, kernel=kernel,
199 | standardize=standardize)
200 | nres_q = np.minimum(qrfit['res'], 0)
201 |
202 | if loss == 'L2':
203 | adj = np.linalg.solve(self.X.T@self.X, self.X.T@nres_q / tau)
204 | coef_e = qrfit['beta'] + adj
205 | robust = None
206 | elif loss == 'Huber':
207 | Z = nres_q + tau*(self.Y - qrfit['res'])
208 | X0 = self.X[:, self.itcp:]
209 | if robust == None:
210 | esfit = low_dim(tau*X0, Z, intercept=self.itcp).adaHuber()
211 | coef_e = esfit['beta']
212 | robust = esfit['robust']
213 | if self.itcp: coef_e[0] /= tau
214 | elif robust > 0:
215 | esfit = low_dim(tau*X0, Z, intercept=self.itcp)\
216 | .als(robust=robust, standardize=standardize, scale=False)
217 | coef_e = esfit['beta']
218 | robust = esfit['robust']
219 | if self.itcp: coef_e[0] /= tau
220 | else:
221 | raise ValueError("robustification parameter must be positive")
222 | elif loss == 'FZ':
223 | if G2_type in np.arange(1,4):
224 | Ymax = np.max(self.Y)
225 | Y = self.Y - Ymax
226 | else:
227 | Y = self.Y
228 | qrfit = low_dim(self.X[:, self.itcp:], Y, intercept=True)\
229 | .fit(tau=tau, h=h, kernel=kernel, standardize=standardize)
230 | adj = np.minimum(qrfit['res'], 0)/tau + Y - qrfit['res']
231 | f0, f1, f2 = G2(G2_type)
232 |
233 | fun = lambda z : np.mean(f1(self.X @ z) * (self.X @ z - adj) \
234 | - f0(self.X @ z))
235 | grad = lambda z : self.X.T@(f2(self.X@z)*(self.X@z - adj))/self.n
236 | esfit = minimize(fun, qrfit['beta'], method='BFGS',
237 | jac=grad, tol=tol, options=options)
238 | coef_e = esfit['x']
239 | robust = None
240 | if G2_type in np.arange(1,4):
241 | coef_e[0] += Ymax
242 | qrfit['beta'][0] += Ymax
243 | elif loss == 'TrunL2':
244 | tail = self.Y <= self.X @ qrfit['beta']
245 | tail_X = self.X[tail,:]
246 | tail_Y = self.Y[tail]
247 | coef_e = np.linalg.solve(tail_X.T @ tail_X,
248 | tail_X.T @ tail_Y)
249 | robust = None
250 | elif loss == 'TrunHuber':
251 | tail = self.Y <= self.X @ qrfit['beta']
252 | esfit = LR(self.X[tail, self.itcp:], self.Y[tail],\
253 | intercept=self.itcp).adaHuber()
254 | coef_e = esfit['beta']
255 | robust = esfit['robust']
256 |
257 | if loss in {'L2', 'Huber'} and ci:
258 | res_e = nres_q + tau * self.X@(qrfit['beta'] - coef_e)
259 | n, p = self.X[:,self.itcp:].shape
260 | X0 = np.c_[np.ones(n,), self.X[:,self.itcp:] - self.mX]
261 | if loss == 'L2':
262 | weight = res_e ** 2
263 | else:
264 | weight = np.minimum(res_e ** 2, robust ** 2)
265 |
266 | inv_sig = np.linalg.inv(X0.T @ X0 / n)
267 | acov = inv_sig @ ((X0.T * weight) @ X0 / n) @ inv_sig
268 | radius = norm.ppf(1/2 + level/2) * np.sqrt(np.diag(acov)/n) / tau
269 | ci = np.c_[coef_e - radius, coef_e + radius]
270 |
271 | return {'coef_q': qrfit['beta'], 'res_q': qrfit['res'],
272 | 'coef_e': coef_e,
273 | 'loss': loss, 'robust': robust,
274 | 'ci': ci, 'level': level}
275 |
276 |
277 | def boot_es(self, tau=0.5, h=None, kernel='Laplacian',
278 | loss='L2', robust=None, standardize=True,
279 | B=200, level=0.95):
280 |
281 | fit = self.twostep_fit(tau, h, kernel, loss,
282 | robust, standardize=standardize)
283 | boot_coef = np.zeros((self.X.shape[1], B))
284 | for b in range(B):
285 | idx = rgt.choice(np.arange(self.n), size=self.n)
286 | boot = LR(self.X[idx,self.itcp:], self.Y[idx],
287 | intercept=self.itcp)
288 | if loss == 'L2':
289 | bfit = boot.twostep_fit(tau, h, kernel, loss='L2',
290 | standardize=standardize)
291 | else:
292 | bfit = boot.twostep_fit(tau, h, kernel, loss,
293 | robust=fit['robust'],
294 | standardize=standardize)
295 | boot_coef[:,b] = bfit['coef_e']
296 |
297 | left = np.quantile(boot_coef, 1/2-level/2, axis=1)
298 | right = np.quantile(boot_coef, 1/2+level/2, axis=1)
299 | piv_ci = np.c_[2*fit['coef_e'] - right, 2*fit['coef_e'] - left]
300 | per_ci = np.c_[left, right]
301 |
302 | return {'coef_q': fit['coef_q'],
303 | 'coef_e': fit['coef_e'],
304 | 'boot_coef_e': boot_coef,
305 | 'loss': loss, 'robust': fit['robust'],
306 | 'piv_ci': piv_ci, 'per_ci': per_ci, 'level': level}
307 |
308 |
309 | def nc_fit(self, tau=0.5, h=None, kernel='Laplacian',
310 | loss='L2', robust=None, standardize=True,
311 | ci=False, level=0.95):
312 | '''
313 | Non-Crossing Joint Quantile & Expected Shortfall Regression
314 |
315 | Refs:
316 | Robust Estimation and Inference
317 | for Expected Shortfall Regression with Many Regressors
318 | by Xuming He, Kean Ming Tan and Wen-Xin Zhou
319 | J. R. Stat. Soc. B. 85(4): 1223-1246, 2023
320 | '''
321 |
322 | qrfit = self.fit(tau=tau, h=h, kernel=kernel, standardize=standardize)
323 | nres_q = np.minimum(qrfit['res'], 0)
324 | fitted_q = self.Y - qrfit['res']
325 | Z = nres_q/tau + fitted_q
326 |
327 | P = matrix(self.X.T @ self.X / self.n)
328 | q = matrix(-self.X.T @ Z / self.n)
329 | G = matrix(self.X)
330 | hh = matrix(fitted_q)
331 | l, c = 0, robust
332 |
333 | if loss == 'L2':
334 | esfit = solvers.qp(P, q, G, hh,
335 | initvals={'x': matrix(qrfit['beta'])})
336 | coef_e = np.array(esfit['x']).reshape(self.X.shape[1],)
337 | else:
338 | rel = (self.X.shape[1] + np.log(self.n)) / self.n
339 | esfit = self.twostep_fit(tau, h, kernel, loss,
340 | robust, standardize=standardize)
341 | coef_e = esfit['coef_e']
342 | res = np.abs(Z - self.X @ coef_e)
343 | c = robust
344 |
345 | if robust == None:
346 | c = find_root(lambda t : np.mean(np.minimum((res/t)**2, 1)) - rel,
347 | np.min(res)+self.params['tol'], np.sqrt(res @ res))
348 |
349 | sol_diff = 1
350 | while l < self.params['max_iter'] \
351 | and sol_diff > self.params['tol']:
352 | wt = np.where(res > c, res/c, 1)
353 | P = matrix( (self.X.T / wt ) @ self.X / self.n)
354 | q = matrix( -self.X.T @ (Z / wt) / self.n)
355 | esfit = solvers.qp(P, q, G, hh, initvals={'x': matrix(coef_e)})
356 | tmp = np.array(esfit['x']).reshape(self.X.shape[1],)
357 | sol_diff = np.max(np.abs(tmp - coef_e))
358 | res = np.abs(Z - self.X @ tmp)
359 | if robust == None:
360 | c = find_root(lambda t : np.mean(np.minimum((res/t)**2, 1)) - rel,
361 | np.min(res)+self.params['tol'], np.sqrt(res @ res))
362 | coef_e = tmp
363 | l += 1
364 | c *= tau
365 |
366 | if ci:
367 | res_e = nres_q + tau * (fitted_q - self.X @ coef_e)
368 | X0 = np.c_[np.ones(self.n,), self.X[:,self.itcp:] - self.mX]
369 | if loss == 'L2': weight = res_e ** 2
370 | else: weight = np.minimum(res_e ** 2, c ** 2)
371 |
372 | inv_sig = np.linalg.inv(X0.T @ X0 / self.n)
373 | acov = inv_sig @ ((X0.T * weight) @ X0 / self.n) @ inv_sig
374 | radius = norm.ppf(1/2 + level/2) * np.sqrt(np.diag(acov)/self.n) / tau
375 | ci = np.c_[coef_e - radius, coef_e + radius]
376 |
377 | return {'coef_q': qrfit['beta'],
378 | 'res_q': qrfit['res'],
379 | 'coef_e': coef_e, 'nit': l,
380 | 'loss': loss, 'robust': c,
381 | 'ci': ci, 'level': level}
382 |
383 |
384 |
385 | ###############################################################################
386 | ########################## Kernel Ridge Regression ############################
387 | ###############################################################################
388 | class KRR:
389 | '''
390 | Kernel Ridge Regression
391 |
392 | Methods:
393 | __init__(): Initialize the KRR object
394 | qt(): Fit (smoothed) quantile kernel ridge regression
395 | es(): Fit (robust) expected shortfall kernel ridge regression
396 | qt_seq(): Fit a sequence of quantile kernel ridge regressions
397 | qt_predict(): Compute predicted quantile at test data
398 | es_predict(): Compute predicted expected shortfall at test data
399 | qt_loss(): Check or smoothed check loss
400 | qt_grad(): Compute the (sub)gradient of the (smoothed) check loss
401 | bw(): Compute the bandwidth (smoothing parameter)
402 | genK(): Generate the kernel matrix for test data
403 |
404 | Attributes:
405 | params (dict): a dictionary of kernel parameters;
406 | gamma (float), default is 1;
407 | coef0 (float), default is 1;
408 | degree (int), default is 3.
409 | rbf : exp(-gamma*||x-y||_2^2)
410 | polynomial : (gamma*