skfolio 0.7.0__py3-none-any.whl → 0.8.1__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.
Files changed (114) hide show
  1. skfolio/__init__.py +2 -2
  2. skfolio/cluster/__init__.py +1 -1
  3. skfolio/cluster/_hierarchical.py +1 -1
  4. skfolio/datasets/__init__.py +1 -1
  5. skfolio/datasets/_base.py +2 -2
  6. skfolio/datasets/data/__init__.py +1 -0
  7. skfolio/distance/__init__.py +1 -1
  8. skfolio/distance/_base.py +2 -2
  9. skfolio/distance/_distance.py +4 -4
  10. skfolio/distribution/__init__.py +56 -0
  11. skfolio/distribution/_base.py +203 -0
  12. skfolio/distribution/copula/__init__.py +35 -0
  13. skfolio/distribution/copula/_base.py +456 -0
  14. skfolio/distribution/copula/_clayton.py +539 -0
  15. skfolio/distribution/copula/_gaussian.py +407 -0
  16. skfolio/distribution/copula/_gumbel.py +560 -0
  17. skfolio/distribution/copula/_independent.py +196 -0
  18. skfolio/distribution/copula/_joe.py +609 -0
  19. skfolio/distribution/copula/_selection.py +111 -0
  20. skfolio/distribution/copula/_student_t.py +486 -0
  21. skfolio/distribution/copula/_utils.py +509 -0
  22. skfolio/distribution/multivariate/__init__.py +11 -0
  23. skfolio/distribution/multivariate/_base.py +241 -0
  24. skfolio/distribution/multivariate/_utils.py +632 -0
  25. skfolio/distribution/multivariate/_vine_copula.py +1254 -0
  26. skfolio/distribution/univariate/__init__.py +19 -0
  27. skfolio/distribution/univariate/_base.py +308 -0
  28. skfolio/distribution/univariate/_gaussian.py +136 -0
  29. skfolio/distribution/univariate/_johnson_su.py +152 -0
  30. skfolio/distribution/univariate/_normal_inverse_gaussian.py +153 -0
  31. skfolio/distribution/univariate/_selection.py +85 -0
  32. skfolio/distribution/univariate/_student_t.py +144 -0
  33. skfolio/exceptions.py +6 -6
  34. skfolio/measures/__init__.py +1 -1
  35. skfolio/measures/_enums.py +7 -7
  36. skfolio/measures/_measures.py +4 -7
  37. skfolio/metrics/__init__.py +2 -0
  38. skfolio/metrics/_scorer.py +4 -4
  39. skfolio/model_selection/__init__.py +2 -2
  40. skfolio/model_selection/_combinatorial.py +15 -12
  41. skfolio/model_selection/_validation.py +2 -2
  42. skfolio/model_selection/_walk_forward.py +3 -3
  43. skfolio/moments/covariance/_base.py +1 -1
  44. skfolio/moments/covariance/_denoise_covariance.py +1 -1
  45. skfolio/moments/covariance/_detone_covariance.py +1 -1
  46. skfolio/moments/covariance/_empirical_covariance.py +1 -1
  47. skfolio/moments/covariance/_ew_covariance.py +1 -1
  48. skfolio/moments/covariance/_gerber_covariance.py +1 -1
  49. skfolio/moments/covariance/_graphical_lasso_cv.py +1 -1
  50. skfolio/moments/covariance/_implied_covariance.py +2 -7
  51. skfolio/moments/covariance/_ledoit_wolf.py +1 -1
  52. skfolio/moments/covariance/_oas.py +1 -1
  53. skfolio/moments/covariance/_shrunk_covariance.py +1 -1
  54. skfolio/moments/expected_returns/_base.py +1 -1
  55. skfolio/moments/expected_returns/_empirical_mu.py +1 -1
  56. skfolio/moments/expected_returns/_equilibrium_mu.py +1 -1
  57. skfolio/moments/expected_returns/_ew_mu.py +1 -1
  58. skfolio/moments/expected_returns/_shrunk_mu.py +2 -2
  59. skfolio/optimization/__init__.py +2 -0
  60. skfolio/optimization/_base.py +2 -2
  61. skfolio/optimization/cluster/__init__.py +2 -0
  62. skfolio/optimization/cluster/_nco.py +7 -7
  63. skfolio/optimization/cluster/hierarchical/__init__.py +2 -0
  64. skfolio/optimization/cluster/hierarchical/_base.py +1 -2
  65. skfolio/optimization/cluster/hierarchical/_herc.py +2 -2
  66. skfolio/optimization/cluster/hierarchical/_hrp.py +2 -2
  67. skfolio/optimization/convex/__init__.py +2 -0
  68. skfolio/optimization/convex/_base.py +8 -8
  69. skfolio/optimization/convex/_distributionally_robust.py +4 -4
  70. skfolio/optimization/convex/_maximum_diversification.py +5 -5
  71. skfolio/optimization/convex/_mean_risk.py +5 -6
  72. skfolio/optimization/convex/_risk_budgeting.py +3 -3
  73. skfolio/optimization/ensemble/__init__.py +2 -0
  74. skfolio/optimization/ensemble/_base.py +2 -2
  75. skfolio/optimization/ensemble/_stacking.py +1 -1
  76. skfolio/optimization/naive/__init__.py +2 -0
  77. skfolio/optimization/naive/_naive.py +1 -1
  78. skfolio/population/__init__.py +2 -0
  79. skfolio/population/_population.py +35 -9
  80. skfolio/portfolio/_base.py +42 -8
  81. skfolio/portfolio/_multi_period_portfolio.py +3 -2
  82. skfolio/portfolio/_portfolio.py +4 -4
  83. skfolio/pre_selection/__init__.py +2 -0
  84. skfolio/pre_selection/_drop_correlated.py +2 -2
  85. skfolio/pre_selection/_select_complete.py +25 -26
  86. skfolio/pre_selection/_select_k_extremes.py +2 -2
  87. skfolio/pre_selection/_select_non_dominated.py +2 -2
  88. skfolio/pre_selection/_select_non_expiring.py +2 -2
  89. skfolio/preprocessing/__init__.py +2 -0
  90. skfolio/preprocessing/_returns.py +2 -2
  91. skfolio/prior/__init__.py +4 -0
  92. skfolio/prior/_base.py +2 -2
  93. skfolio/prior/_black_litterman.py +5 -3
  94. skfolio/prior/_empirical.py +3 -1
  95. skfolio/prior/_factor_model.py +8 -4
  96. skfolio/prior/_synthetic_data.py +239 -0
  97. skfolio/synthetic_returns/__init__.py +1 -0
  98. skfolio/typing.py +1 -1
  99. skfolio/uncertainty_set/__init__.py +2 -0
  100. skfolio/uncertainty_set/_base.py +2 -2
  101. skfolio/uncertainty_set/_bootstrap.py +1 -1
  102. skfolio/uncertainty_set/_empirical.py +1 -1
  103. skfolio/utils/__init__.py +1 -0
  104. skfolio/utils/bootstrap.py +2 -2
  105. skfolio/utils/equations.py +13 -10
  106. skfolio/utils/sorting.py +2 -2
  107. skfolio/utils/stats.py +7 -7
  108. skfolio/utils/tools.py +76 -12
  109. {skfolio-0.7.0.dist-info → skfolio-0.8.1.dist-info}/METADATA +99 -24
  110. skfolio-0.8.1.dist-info/RECORD +120 -0
  111. {skfolio-0.7.0.dist-info → skfolio-0.8.1.dist-info}/WHEEL +1 -1
  112. skfolio-0.7.0.dist-info/RECORD +0 -95
  113. {skfolio-0.7.0.dist-info → skfolio-0.8.1.dist-info/licenses}/LICENSE +0 -0
  114. {skfolio-0.7.0.dist-info → skfolio-0.8.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,560 @@
1
+ """Bivariate Gumbel Copula Estimation."""
2
+
3
+ # Copyright (c) 2025
4
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
5
+ # Credits: Matteo Manzi, Vincent Maladière, Carlo Nicolini
6
+ # SPDX-License-Identifier: BSD-3-Clause
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import scipy.stats as st
11
+ import sklearn.utils.validation as skv
12
+
13
+ from skfolio.distribution.copula._base import BaseBivariateCopula
14
+ from skfolio.distribution.copula._utils import (
15
+ CopulaRotation,
16
+ _apply_copula_rotation,
17
+ _apply_margin_swap,
18
+ _apply_rotation_cdf,
19
+ _apply_rotation_partial_derivatives,
20
+ _select_rotation_itau,
21
+ _select_theta_and_rotation_mle,
22
+ )
23
+
24
+ # Gumbel copula with a theta of 1.0 is just the independence copula, so we chose a lower
25
+ # bound of 1.0001. After 80, the copula is already imposing extremely strong upper tail
26
+ # dependence and increasing it will make it impractical.
27
+ _THETA_BOUNDS = (1.0001, 80.0)
28
+
29
+
30
+ class GumbelCopula(BaseBivariateCopula):
31
+ r"""Bivariate Gumbel Copula Estimation.
32
+
33
+ The Gumbel copula is an Archimedean copula characterized by strong upper tail
34
+ dependence and little to no lower tail dependence.
35
+
36
+ In its unrotated form, it is used for modeling extreme co-movements in the upper
37
+ tail (i.e. simultaneous extreme gains).
38
+
39
+ Rotations allow the copula to be adapted for different types of tail dependence:
40
+ - A 180° rotation captures extreme co-movements in the lower tail (i.e.
41
+ simultaneous extreme losses).
42
+
43
+ - A 90° rotation captures scenarios where one variable exhibits extreme losses
44
+ while the other shows extreme gains.
45
+
46
+ - A 270° rotation captures the opposite scenario, where one variable experiences
47
+ extreme gains while the other suffers extreme losses.
48
+
49
+ Gumbel copula generally exhibits weaker upper tail dependence than the Joe copula.
50
+
51
+ It is defined by:
52
+
53
+ .. math::
54
+ C_{\theta}(u, v) = \exp\Bigl(-\Bigl[(-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr]^{1/\theta}\Bigr)
55
+
56
+ where :math:`\theta \ge 1` is the dependence parameter. When :math:`\theta = 1`,
57
+ the Gumbel copula reduces to the independence copula. Larger values of
58
+ :math:`\theta` result in stronger upper-tail dependence.
59
+
60
+ .. note::
61
+
62
+ Rotations are needed for Archimedean copulas (e.g., Joe, Gumbel, Gumbel)
63
+ because their parameters only model positive dependence, and they exhibit
64
+ asymmetric tail behavior. To model negative dependence, one uses rotations
65
+ to "flip" the copula's tail dependence.
66
+
67
+ Parameters
68
+ ----------
69
+ itau : bool, default=True
70
+ If True, :math:`\theta` is estimated using the Kendall's tau inversion method;
71
+ otherwise, the Maximum Likelihood Estimation (MLE) method is used. The MLE is
72
+ slower but more accurate.
73
+
74
+ kendall_tau : float, optional
75
+ If `itau` is True and `kendall_tau` is provided, this value is used;
76
+ otherwise, it is computed.
77
+
78
+ tolerance : float, default=1e-4
79
+ Convergence tolerance for the MLE optimization.
80
+
81
+ random_state : int, RandomState instance or None, default=None
82
+ Seed or random state to ensure reproducibility.
83
+
84
+ Attributes
85
+ ----------
86
+ theta_ : float
87
+ Fitted theta coefficient :math:`\theta` > 1.
88
+
89
+ rotation_ : CopulaRotation
90
+ Fitted rotation of the copula.
91
+
92
+ Examples
93
+ --------
94
+ >>> from skfolio.datasets import load_sp500_dataset
95
+ >>> from skfolio.preprocessing import prices_to_returns
96
+ >>> from skfolio.distribution import GumbelCopula, compute_pseudo_observations
97
+ >>>
98
+ >>> # Load historical prices and convert them to returns
99
+ >>> prices = load_sp500_dataset()
100
+ >>> X = prices_to_returns(prices)
101
+ >>> X = X[["AAPL", "JPM"]]
102
+ >>>
103
+ >>> # Convert returns to pseudo observation in the interval [0,1]
104
+ >>> X = compute_pseudo_observations(X)
105
+ >>>
106
+ >>> # Initialize the Copula estimator
107
+ >>> model = GumbelCopula()
108
+ >>>
109
+ >>> # Fit the model to the data.
110
+ >>> model.fit(X)
111
+ >>>
112
+ >>> # Display the fitted parameter and tail dependence coefficients
113
+ >>> print(model.fitted_repr)
114
+ GumbelCopula(theta=1.27, rot=180°)
115
+ >>> print(model.lower_tail_dependence)
116
+ 0.2735
117
+ >>> print(model.upper_tail_dependence)
118
+ 0.0
119
+ >>>
120
+ >>> # Compute the log-likelihood, total log-likelihood, CDF, Partial Derivative,
121
+ >>> # Inverse Partial Derivative, AIC, and BIC
122
+ >>> log_likelihood = model.score_samples(X)
123
+ >>> score = model.score(X)
124
+ >>> cdf = model.cdf(X)
125
+ >>> p = model.partial_derivative(X)
126
+ >>> u = model.inverse_partial_derivative(X)
127
+ >>> aic = model.aic(X)
128
+ >>> bic = model.bic(X)
129
+ >>>
130
+ >>> # Generate 5 new samples
131
+ >>> samples = model.sample(n_samples=5)
132
+ >>>
133
+ >>> # Plot the tail concentration function.
134
+ >>> fig = model.plot_tail_concentration()
135
+ >>> fig.show()
136
+ >>>
137
+ >>> # Plot a 2D contour of the estimated PDF.
138
+ >>> fig = model.plot_pdf_2d()
139
+ >>> fig.show()
140
+ >>>
141
+ >>> # Plot a 3D surface of the estimated PDF.
142
+ >>> fig = model.plot_pdf_3d()
143
+ >>> fig.show()
144
+
145
+ References
146
+ ----------
147
+ .. [1] "An Introduction to Copulas (2nd ed.)",
148
+ Nelsen (2006)
149
+
150
+ .. [2] "Multivariate Models and Dependence Concepts",
151
+ Joe, Chapman & Hall (1997)
152
+
153
+ .. [3] "Quantitative Risk Management: Concepts, Techniques and Tools",
154
+ McNeil, Frey & Embrechts (2005)
155
+
156
+ .. [4] "The t Copula and Related Copulas",
157
+ Demarta & McNeil (2005)
158
+
159
+ .. [5] "Copula Methods in Finance",
160
+ Cherubini, Luciano & Vecchiato (2004)
161
+ """
162
+
163
+ theta_: float
164
+ rotation_: CopulaRotation
165
+ _n_params = 1
166
+
167
+ def __init__(
168
+ self,
169
+ itau: bool = True,
170
+ kendall_tau: float | None = None,
171
+ tolerance: float = 1e-4,
172
+ random_state: int | None = None,
173
+ ):
174
+ super().__init__(random_state=random_state)
175
+ self.itau = itau
176
+ self.kendall_tau = kendall_tau
177
+ self.tolerance = tolerance
178
+
179
+ def fit(self, X: npt.ArrayLike, y=None) -> "GumbelCopula":
180
+ r"""Fit the Bivariate Gumbel Copula.
181
+
182
+ If `itau` is True, estimates :math:`\theta` using Kendall's tau inversion.
183
+ Otherwise, uses MLE by maximizing the log-likelihood.
184
+
185
+ Parameters
186
+ ----------
187
+ X : array-like of shape (n_observations, 2)
188
+ An array of bivariate inputs `(u, v)` where each row represents a
189
+ bivariate observation. Both `u` and `v` must be in the interval [0, 1],
190
+ having been transformed to uniform marginals.
191
+
192
+ y : None
193
+ Ignored. Provided for compatibility with scikit-learn's API.
194
+
195
+ Returns
196
+ -------
197
+ self : object
198
+ Returns the instance itself.
199
+ """
200
+ X = self._validate_X(X, reset=True)
201
+
202
+ if self.itau:
203
+ if self.kendall_tau is None:
204
+ kendall_tau = st.kendalltau(X[:, 0], X[:, 1]).statistic
205
+ else:
206
+ kendall_tau = self.kendall_tau
207
+
208
+ # For Gumbel, the theoretical relationship is: tau = 1 - 1/theta.
209
+ abs_kendall_tau = min(abs(kendall_tau), 0.9999)
210
+
211
+ self.theta_ = np.clip(
212
+ 1.0 / (1.0 - abs_kendall_tau),
213
+ a_min=_THETA_BOUNDS[0],
214
+ a_max=_THETA_BOUNDS[1],
215
+ )
216
+ self.rotation_ = _select_rotation_itau(
217
+ func=_neg_log_likelihood, X=X, theta=self.theta_
218
+ )
219
+
220
+ else:
221
+ self.theta_, self.rotation_ = _select_theta_and_rotation_mle(
222
+ _neg_log_likelihood, X=X, bounds=_THETA_BOUNDS, tolerance=self.tolerance
223
+ )
224
+
225
+ return self
226
+
227
+ def cdf(self, X: npt.ArrayLike) -> np.ndarray:
228
+ r"""Compute the CDF of the bivariate Gumbel copula.
229
+
230
+ .. math::
231
+ C(u,v) = \exp\Bigl(-\Bigl[(-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr]^{1/\theta}\Bigr).
232
+
233
+ Parameters
234
+ ----------
235
+ X : array-like of shape (n_observations, 2)
236
+ An array of bivariate inputs `(u, v)` where each row represents a
237
+ bivariate observation. Both `u` and `v` must be in the interval [0, 1],
238
+ having been transformed to uniform marginals.
239
+
240
+ Returns
241
+ -------
242
+ cdf : ndarray of shape (n_observations,)
243
+ CDF values for each observation in X.
244
+ """
245
+ skv.check_is_fitted(self)
246
+ X = self._validate_X(X, reset=False)
247
+ cdf = _apply_rotation_cdf(
248
+ func=_base_cdf, X=X, rotation=self.rotation_, theta=self.theta_
249
+ )
250
+ return cdf
251
+
252
+ def partial_derivative(
253
+ self, X: npt.ArrayLike, first_margin: bool = False
254
+ ) -> np.ndarray:
255
+ r"""Compute the h-function (partial derivative) for the bivariate Gumbel copula
256
+ with respect to a specified margin.
257
+
258
+ The h-function with respect to the second margin represents the conditional
259
+ distribution function of :math:`u` given :math:`v`:
260
+
261
+ .. math:: \begin{aligned}
262
+ h(u \mid v)
263
+ &= \frac{\partial C(u,v)}{\partial v}\\[6pt]
264
+ &= C(u,v)\,\Bigl[(-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr]^{\frac{1}{\theta}-1}
265
+ \,(-\ln v)^{\theta-1}\,\frac{1}{v}.
266
+ \end{aligned}
267
+
268
+ Parameters
269
+ ----------
270
+ X : array-like of shape (n_observations, 2)
271
+ An array of bivariate inputs `(u, v)` where each row represents a
272
+ bivariate observation. Both `u` and `v` must be in the interval [0, 1],
273
+ having been transformed to uniform marginals.
274
+
275
+ first_margin : bool, default=False
276
+ If True, compute the partial derivative with respect to the first
277
+ margin `u`; otherwise, compute the partial derivative with respect to the
278
+ second margin `v`.
279
+
280
+ Returns
281
+ -------
282
+ p : ndarray of shape (n_observations,)
283
+ h-function values :math:`h(u \mid v)` for each observation in X.
284
+ """
285
+ skv.check_is_fitted(self)
286
+ X = self._validate_X(X, reset=False)
287
+ p = _apply_rotation_partial_derivatives(
288
+ func=_base_partial_derivative, # Computation remains unchanged
289
+ X=X,
290
+ rotation=self.rotation_,
291
+ first_margin=first_margin,
292
+ theta=self.theta_,
293
+ )
294
+ return p
295
+
296
+ def inverse_partial_derivative(
297
+ self, X: npt.ArrayLike, first_margin: bool = False
298
+ ) -> np.ndarray:
299
+ r"""Compute the inverse of the bivariate copula's partial derivative, commonly
300
+ known as the inverse h-function.
301
+
302
+ Let :math:`C(u, v)` be a bivariate copula. The h-function with respect to the
303
+ second margin is defined by
304
+
305
+ .. math::
306
+ h(u \mid v) \;=\; \frac{\partial\,C(u, v)}{\partial\,v},
307
+
308
+ which is the conditional distribution of :math:`U` given :math:`V = v`.
309
+ The **inverse h-function**, denoted :math:`h^{-1}(p \mid v)`, is the unique
310
+ value :math:`u \in [0,1]` such that
311
+
312
+ .. math::
313
+ h(u \mid v) \;=\; p,
314
+ \quad \text{where } p \in [0,1].
315
+
316
+ In practical terms, given :math:`(p, v)` in :math:`[0, 1]^2`,
317
+ :math:`h^{-1}(p \mid v)` solves for the :math:`u` satisfying
318
+ :math:`p = \partial C(u, v)/\partial v`.
319
+
320
+ Parameters
321
+ ----------
322
+ X : array-like of shape (n_observations, 2)
323
+ An array of bivariate inputs `(p, v)`, each in the interval [0, 1].
324
+ - The first column `p` corresponds to the value of the h-function.
325
+ - The second column `v` is the conditioning variable.
326
+
327
+ first_margin : bool, default=False
328
+ If True, compute the inverse partial derivative with respect to the first
329
+ margin `u`; otherwise, compute the inverse partial derivative with respect
330
+ to the second margin `v`.
331
+
332
+ Returns
333
+ -------
334
+ u : ndarray of shape (n_observations,)
335
+ A 1D-array where each element is the computed :math:`u = h^{-1}(p \mid v)`
336
+ for the corresponding pair in `X`.
337
+ """
338
+ skv.check_is_fitted(self)
339
+ X = self._validate_X(X, reset=False)
340
+ u = _apply_rotation_partial_derivatives(
341
+ func=_base_inverse_partial_derivative, # Computation remains unchanged
342
+ X=X,
343
+ rotation=self.rotation_,
344
+ first_margin=first_margin,
345
+ theta=self.theta_,
346
+ )
347
+ return u
348
+
349
+ def score_samples(self, X: npt.ArrayLike) -> np.ndarray:
350
+ r"""Compute the log-likelihood of each sample (log-pdf) under the model.
351
+
352
+ For Gumbel, the PDF is given by:
353
+
354
+ .. math::
355
+ c(u,v) = \exp\Bigl(-\Bigl((-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr)^{1/\theta}\Bigr)
356
+ \cdot \left[\Bigl((-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr)^{1/\theta-1}
357
+ \left\{ \frac{(-\ln u)^{\theta}}{u}+\frac{(-\ln v)^{\theta}}{v}\right\}\right].
358
+
359
+ Parameters
360
+ ----------
361
+ X : array-like of shape (n_observations, 2)
362
+ An array of bivariate inputs `(u, v)` where each row represents a
363
+ bivariate observation. Both `u` and `v` must be in the interval [0, 1],
364
+ having been transformed to uniform marginals.
365
+
366
+ Returns
367
+ -------
368
+ density : ndarray of shape (n_observations,)
369
+ The log-likelihood of each sample under the fitted copula.
370
+ """
371
+ skv.check_is_fitted(self)
372
+ X = self._validate_X(X, reset=False)
373
+ X = _apply_copula_rotation(X, rotation=self.rotation_)
374
+ log_density = _base_sample_scores(X=X, theta=self.theta_)
375
+ return log_density
376
+
377
+ @property
378
+ def lower_tail_dependence(self) -> float:
379
+ """Theoretical lower tail dependence coefficient."""
380
+ skv.check_is_fitted(self)
381
+ if self.rotation_ == CopulaRotation.R180:
382
+ return 2.0 - np.power(2.0, 1.0 / self.theta_)
383
+ return 0
384
+
385
+ @property
386
+ def upper_tail_dependence(self) -> float:
387
+ """Theoretical upper tail dependence coefficient."""
388
+ skv.check_is_fitted(self)
389
+ if self.rotation_ == CopulaRotation.R0:
390
+ return 2.0 - np.power(2.0, 1.0 / self.theta_)
391
+ return 0
392
+
393
+ @property
394
+ def fitted_repr(self) -> str:
395
+ """String representation of the fitted copula."""
396
+ return (
397
+ f"{self.__class__.__name__}(theta={self.theta_:0.2f}, rot={self.rotation_})"
398
+ )
399
+
400
+
401
+ def _neg_log_likelihood(theta: float, X: np.ndarray) -> float:
402
+ """Negative log-likelihood function for the Gumbel copula.
403
+
404
+ Parameters
405
+ ----------
406
+ X : array-like of shape (n_observations, 2)
407
+ An array of bivariate inputs `(u, v)` where each row represents a
408
+ bivariate observation. Both `u` and `v` must be in the interval [0, 1],
409
+ having been transformed to uniform marginals.
410
+
411
+ theta : float
412
+ The dependence parameter (must be greater than 1).
413
+
414
+ Returns
415
+ -------
416
+ value : float
417
+ The negative log-likelihood value.
418
+ """
419
+ return -np.sum(_base_sample_scores(X=X, theta=theta))
420
+
421
+
422
+ def _base_sample_scores(X: np.ndarray, theta: float) -> np.ndarray:
423
+ r"""Compute the log-likelihood of each sample (log-pdf) under the bivariate Gumbel
424
+ copula.
425
+
426
+ Parameters
427
+ ----------
428
+ X : array-like of shape (n_observations, 2)
429
+ Bivariate samples `(u, v)`, with each component in [0,1].
430
+
431
+ theta : float
432
+ The dependence parameter (must be greater than 1).
433
+
434
+ Returns
435
+ -------
436
+ logpdf : ndarray of shape (n_observations,)
437
+ Log-likelihood values for each observation.
438
+ """
439
+ if theta <= 1:
440
+ raise ValueError("Theta must be greater than 1 for the Gumbel copula.")
441
+ Z = -np.log(X)
442
+ s = np.power(np.power(Z, theta).sum(axis=1), 1 / theta)
443
+ s = np.clip(s, a_min=1e-10, a_max=None)
444
+ log_density = (
445
+ -s
446
+ + np.log(s + theta - 1)
447
+ + (1 - 2 * theta) * np.log(s)
448
+ + (theta - 1) * np.log(Z.prod(axis=1))
449
+ + Z.sum(axis=1)
450
+ )
451
+ return log_density
452
+
453
+
454
+ def _base_cdf(X: np.ndarray, theta: float) -> np.ndarray:
455
+ r"""Bivariate Gumbel CDF (unrotated).
456
+
457
+ .. math::
458
+ C(u,v) = \exp\Bigl(-\Bigl[(-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr]^{1/\theta}\Bigr).
459
+ """
460
+ cdf = np.exp(-np.power(np.power(-np.log(X), theta).sum(axis=1), 1.0 / theta))
461
+ return cdf
462
+
463
+
464
+ def _base_partial_derivative(
465
+ X: np.ndarray, first_margin: bool, theta: float
466
+ ) -> np.ndarray:
467
+ r"""
468
+ Compute the partial derivative (h-function) for the unrotated Gumbel copula.
469
+
470
+ For Gumbel, the copula is defined as:
471
+
472
+ .. math::
473
+ C(u,v)=\exp\Bigl(-\Bigl[(-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr]^{1/\theta}\Bigr).
474
+
475
+ The partial derivative with respect to v is:
476
+
477
+ .. math::
478
+ \frac{\partial C(u,v)}{\partial v}
479
+ = C(u,v)\,\Bigl[(-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr]^{\frac{1}{\theta}-1}
480
+ \,(-\ln v)^{\theta-1}\,\frac{1}{v}.
481
+
482
+ Parameters
483
+ ----------
484
+ X : array-like of shape (n_observations, 2)
485
+ An array of bivariate inputs `(u, v)` with values in [0, 1].
486
+
487
+ first_margin : bool, default=False
488
+ If True, compute with respect to u (by swapping margins); otherwise,
489
+ compute with respect to v.
490
+
491
+ theta : float
492
+ The dependence parameter (must be > 1).
493
+
494
+ Returns
495
+ -------
496
+ p : ndarray of shape (n_observations,)
497
+ The computed h-function values.
498
+ """
499
+ X = _apply_margin_swap(X, first_margin=first_margin)
500
+ _, v = X.T
501
+ x, y = -np.log(X).T
502
+ p = (
503
+ np.exp(-np.power(np.power(x, theta) + np.power(y, theta), 1.0 / theta))
504
+ * np.power(np.power(x / y, theta) + 1.0, 1.0 / theta - 1.0)
505
+ / v
506
+ )
507
+ return p
508
+
509
+
510
+ def _base_inverse_partial_derivative(
511
+ X: np.ndarray, first_margin: bool, theta: float
512
+ ) -> np.ndarray:
513
+ r"""
514
+ Compute the inverse partial derivative for the unrotated Gumbel copula,
515
+ i.e. solve for u in h(u|v)=p.
516
+
517
+ In other words, given
518
+ - p, the value of the h-function, and
519
+ - v, the conditioning variable,
520
+ solve:
521
+
522
+ .. math::
523
+ p = C(u,v)\,\Bigl[(-\ln u)^{\theta}+(-\ln v)^{\theta}\Bigr]^{\frac{1}{\theta}-1}\,
524
+ (-\ln v)^{\theta-1}\,\frac{1}{v},
525
+
526
+ for u ∈ [0,1]. Since no closed-form solution exists, we use a numerical method.
527
+
528
+ Parameters
529
+ ----------
530
+ X : array-like of shape (n_observations, 2)
531
+ An array with first column p (h-function values) and second column v
532
+ (conditioning variable).
533
+
534
+ first_margin : bool, default=False
535
+ If True, treat the first margin as the conditioning variable.
536
+
537
+ theta : float
538
+ The dependence parameter (must be > 1).
539
+
540
+ Returns
541
+ -------
542
+ u : ndarray of shape (n_observations,)
543
+ A 1D-array where each element is the solution u ∈ [0,1] such that h(u|v)=p.
544
+ """
545
+ X = _apply_margin_swap(X, first_margin=first_margin)
546
+ p, v = -np.log(X).T
547
+ s = v + p + np.log(v) * (theta - 1.0)
548
+ # Initial guess
549
+ x = v.copy()
550
+ max_iters = 50
551
+ tol = 1e-8
552
+ for _ in range(max_iters):
553
+ x_new = x * (s - (theta - 1.0) * (np.log(x) - 1.0)) / (theta + x - 1.0)
554
+ x_new = np.clip(x_new, x, None)
555
+ diff = np.max(np.abs(x_new - x))
556
+ x = x_new
557
+ if diff < tol:
558
+ break
559
+ u = np.exp(-np.power(np.power(x, theta) - np.power(v, theta), 1.0 / theta))
560
+ return u