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