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,456 @@
1
+ """Base Bivariate Copula Estimator."""
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
+ from abc import ABC, abstractmethod
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+ import plotly.graph_objects as go
13
+ import sklearn.utils as sku
14
+ import sklearn.utils.validation as skv
15
+
16
+ from skfolio.distribution._base import BaseDistribution
17
+ from skfolio.distribution.copula._utils import (
18
+ empirical_tail_concentration,
19
+ plot_tail_concentration,
20
+ )
21
+
22
+ UNIFORM_MARGINAL_EPSILON = 1e-9
23
+ _RHO_BOUNDS = (-0.999, 0.999)
24
+
25
+
26
+ class BaseBivariateCopula(BaseDistribution, ABC):
27
+ """Base class for Bivariate Copula Estimators.
28
+
29
+ This abstract class defines the interface for bivariate copula models, including
30
+ methods for fitting, sampling, scoring, and computing partial derivatives.
31
+
32
+ Parameters
33
+ ----------
34
+ random_state : int, RandomState instance or None, default=None
35
+ Seed or random state to ensure reproducibility.
36
+ """
37
+
38
+ # Used for AIC and BIC
39
+ _n_params: int
40
+
41
+ def __init__(self, random_state: int | None = None):
42
+ super().__init__(random_state=random_state)
43
+
44
+ def _validate_X(self, X: npt.ArrayLike, reset: bool) -> np.ndarray:
45
+ """Validate the input data.
46
+
47
+ Parameters
48
+ ----------
49
+ X : array-like of shape (n_observations, 2)
50
+ An array of bivariate inputs `(u, v)` where each row represents a
51
+ bivariate observation. Both `u` and `v` must be in the interval `[0, 1]`.
52
+
53
+ reset : bool, default=True
54
+ Whether to reset the `n_features_in_` attribute.
55
+ If False, the input will be checked for consistency with data
56
+ provided when reset was last True.
57
+
58
+ Returns
59
+ -------
60
+ validated_X: ndarray of shape (n_observations, 2)
61
+ The validated data array.
62
+
63
+ Raises
64
+ ------
65
+ ValueError
66
+ If input data is invalid (e.g., not in `[0, 1]` or incorrect shape).
67
+ """
68
+ X = skv.validate_data(self, X, dtype=np.float64, reset=reset)
69
+ if X.shape[1] != 2:
70
+ raise ValueError("X must contains two columns for Bivariate Copula")
71
+ if not np.all((X >= 0) & (X <= 1)):
72
+ raise ValueError(
73
+ "X must be in the interval `[0, 1]`, usually reprinting uniform "
74
+ "distributions obtained from marginals CDF transformation"
75
+ )
76
+
77
+ # Handle potential numerical issues by ensuring X doesn't contain exact 0 or 1.
78
+ X = np.clip(X, UNIFORM_MARGINAL_EPSILON, 1 - UNIFORM_MARGINAL_EPSILON)
79
+ return X
80
+
81
+ @property
82
+ def n_params(self) -> int:
83
+ """Number of model parameters."""
84
+ return self._n_params
85
+
86
+ @property
87
+ @abstractmethod
88
+ def lower_tail_dependence(self) -> float:
89
+ """Theoretical lower tail dependence coefficient."""
90
+ pass
91
+
92
+ @property
93
+ @abstractmethod
94
+ def upper_tail_dependence(self) -> float:
95
+ """Theoretical upper tail dependence coefficient."""
96
+ pass
97
+
98
+ @property
99
+ @abstractmethod
100
+ def fitted_repr(self) -> str:
101
+ """String representation of the fitted copula."""
102
+ pass
103
+
104
+ @abstractmethod
105
+ def fit(self, X: npt.ArrayLike, y=None) -> "BaseBivariateCopula":
106
+ """Fit the copula model.
107
+
108
+ Parameters
109
+ ----------
110
+ X : array-like of shape (n_observations, 2)
111
+ An array of bivariate inputs `(u, v)` where each row represents a
112
+ bivariate observation. Both `u` and `v` must be in the interval [0, 1],
113
+ having been transformed to uniform marginals.
114
+
115
+ y : None
116
+ Ignored. Provided for compatibility with scikit-learn's API.
117
+
118
+ Returns
119
+ -------
120
+ self : BaseBivariateCopula
121
+ Returns the instance itself.
122
+ """
123
+ pass
124
+
125
+ @abstractmethod
126
+ def cdf(self, X: npt.ArrayLike) -> np.ndarray:
127
+ """Compute the CDF of the bivariate copula.
128
+
129
+ Parameters
130
+ ----------
131
+ X : array-like of shape (n_observations, 2)
132
+ An array of bivariate inputs `(u, v)` where each row represents a
133
+ bivariate observation. Both `u` and `v` must be in the interval `[0, 1]`,
134
+ having been transformed to uniform marginals.
135
+
136
+ Returns
137
+ -------
138
+ cdf : ndarray of shape (n_observations,)
139
+ CDF values for each observation in X.
140
+ """
141
+ pass
142
+
143
+ @abstractmethod
144
+ def partial_derivative(
145
+ self, X: npt.ArrayLike, first_margin: bool = False
146
+ ) -> np.ndarray:
147
+ r"""Compute the h-function (partial derivative) for the bivariate copula
148
+ with respect to a specified margin.
149
+
150
+ The h-function with respect to the second margin represents the conditional
151
+ distribution function of :math:`u` given :math:`v`:
152
+
153
+ .. math::
154
+ h(u \mid v) = \frac{\partial C(u,v)}{\partial v}
155
+
156
+ Parameters
157
+ ----------
158
+ X : array-like of shape (n_observations, 2)
159
+ An array of bivariate inputs `(u, v)` where each row represents a
160
+ bivariate observation. Both `u` and `v` must be in the interval `[0, 1]`,
161
+ having been transformed to uniform marginals.
162
+
163
+ first_margin : bool, default=False
164
+ If True, compute the partial derivative with respect to the first
165
+ margin `u`; otherwise, compute the partial derivative with respect to the
166
+ second margin `v`.
167
+
168
+ Returns
169
+ -------
170
+ p : ndarray of shape (n_observations,)
171
+ h-function values :math:`h(u \mid v) \;=\; p` for each observation in X.
172
+ """
173
+ pass
174
+
175
+ @abstractmethod
176
+ def inverse_partial_derivative(
177
+ self, X: npt.ArrayLike, first_margin: bool = False
178
+ ) -> np.ndarray:
179
+ r"""Compute the inverse of the bivariate copula's partial derivative, commonly
180
+ known as the inverse h-function [1]_.
181
+
182
+ Let :math:`C(u, v)` be a bivariate copula. The h-function with respect to the
183
+ second margin is defined by
184
+
185
+ .. math::
186
+ h(u \mid v) \;=\; \frac{\partial\,C(u, v)}{\partial\,v},
187
+
188
+ which is the conditional distribution of :math:`U` given :math:`V = v`.
189
+ The **inverse h-function**, denoted :math:`h^{-1}(p \mid v)`, is the unique
190
+ value :math:`u \in [0,1]` such that
191
+
192
+ .. math::
193
+ h(u \mid v) \;=\; p,
194
+ \quad \text{where } p \in [0,1].
195
+
196
+ In practical terms, given :math:`(p, v)` in :math:`[0, 1]^2`,
197
+ :math:`h^{-1}(p \mid v)` solves for the :math:`u` satisfying
198
+ :math:`p = \partial C(u, v)/\partial v`.
199
+
200
+ Parameters
201
+ ----------
202
+ X : array-like of shape (n_observations, 2)
203
+ An array of bivariate inputs `(p, v)`, each in the interval `[0, 1]`.
204
+ - The first column `p` corresponds to the value of the h-function.
205
+ - The second column `v` is the conditioning variable.
206
+
207
+ first_margin : bool, default=False
208
+ If True, compute the inverse partial derivative with respect to the first
209
+ margin `u`; otherwise, compute the inverse partial derivative with respect
210
+ to the second margin `v`.
211
+
212
+ Returns
213
+ -------
214
+ u : ndarray of shape (n_observations,)
215
+ A 1D-array of length `n_observations`, where each element is the computed
216
+ :math:`u = h^{-1}(p \mid v)` for the corresponding pair in `X`.
217
+
218
+ References
219
+ ----------
220
+ .. [1] "Multivariate Models and Dependence Concepts", Joe, H. (1997)
221
+ .. [2] "An Introduction to Copulas", Nelsen, R. B. (2006)
222
+ """
223
+ pass
224
+
225
+ @abstractmethod
226
+ def score_samples(self, X: npt.ArrayLike) -> np.ndarray:
227
+ """Compute the log-likelihood of each sample (log-pdf) under the model.
228
+
229
+ Parameters
230
+ ----------
231
+ X : array-like of shape (n_observations, 2)
232
+ An array of bivariate inputs `(u, v)` where each row represents a
233
+ bivariate observation. Both `u` and `v` must be in the interval `[0, 1]`,
234
+ having been transformed to uniform marginals.
235
+
236
+ Returns
237
+ -------
238
+ density : ndarray of shape (n_observations,)
239
+ The log-likelihood of each sample under the fitted copula.
240
+ """
241
+ pass
242
+
243
+ def sample(self, n_samples: int = 1):
244
+ """Generate random samples from the bivariate copula using the inverse
245
+ Rosenblatt transform.
246
+
247
+ Parameters
248
+ ----------
249
+ n_samples : int, default=1
250
+ Number of samples to generate.
251
+
252
+ Returns
253
+ -------
254
+ X : array-like of shape (n_samples, 2)
255
+ An array of bivariate inputs `(u, v)` where each row represents a
256
+ bivariate observation. Both `u` and `v` are uniform marginals in the
257
+ interval `[0, 1]`.
258
+ """
259
+ skv.check_is_fitted(self)
260
+ rng = sku.check_random_state(self.random_state)
261
+
262
+ # Generate independent Uniform(0, 1) samples
263
+ X = rng.random(size=(n_samples, 2))
264
+
265
+ # Apply the inverse Rosenblatt transform on the first variable.
266
+ X[:, 1] = self.inverse_partial_derivative(X, first_margin=True)
267
+ return X
268
+
269
+ def tail_concentration(self, quantiles: np.ndarray) -> np.ndarray:
270
+ """
271
+ Compute the tail concentration function for a set of quantiles.
272
+
273
+ The tail concentration function is defined as follows:
274
+ - For quantiles q ≤ 0.5:
275
+ C(q) = P(U ≤ q, V ≤ q) / q
276
+
277
+ - For quantiles q > 0.5:
278
+ C(q) = (1 - 2q + P(U ≤ q, V ≤ q)) / (1 - q)
279
+
280
+ where U and V are the pseudo-observations of the first and second variables,
281
+ respectively. This function returns the concentration values for each q
282
+ provided.
283
+
284
+ Parameters
285
+ ----------
286
+ quantiles : ndarray of shape (n_quantiles,)
287
+ A 1D array of quantile levels (values between 0 and 1) at which to compute
288
+ the tail concentration.
289
+
290
+ Returns
291
+ -------
292
+ concentration : ndarray of shape (n_quantiles,)
293
+ The computed tail concentration values corresponding to each quantile.
294
+
295
+ References
296
+ ----------
297
+ .. [1] "Quantitative Risk Management: Concepts, Techniques, and Tools",
298
+ McNeil, Frey, Embrechts (2005)
299
+
300
+ Raises
301
+ ------
302
+ ValueError
303
+ If any value in `quantiles` is not in the interval [0, 1].
304
+ """
305
+ quantiles = np.asarray(quantiles)
306
+ if not np.all((quantiles >= 0) & (quantiles <= 1)):
307
+ raise ValueError("quantiles must be between 0.0 and 1.0.")
308
+ X = np.stack((quantiles, quantiles)).T
309
+ cdf = self.cdf(X)
310
+ concentration = np.where(
311
+ quantiles <= 0.5,
312
+ cdf / quantiles,
313
+ (1.0 - 2 * quantiles + cdf) / (1.0 - quantiles),
314
+ )
315
+ return concentration
316
+
317
+ def plot_tail_concentration(
318
+ self, X: npt.ArrayLike | None = None, title: str | None = None
319
+ ) -> go.Figure:
320
+ """
321
+ Plot the tail concentration function.
322
+
323
+ This method computes the tail concentration function at 100 evenly spaced
324
+ quantile levels between 0.005 and 0.995.
325
+ The plot displays the concentration values on the y-axis and the quantile levels
326
+ on the x-axis.
327
+
328
+ The tail concentration is defined as:
329
+ - Lower tail: λ_L(q) = P(U₂ ≤ q | U₁ ≤ q)
330
+ - Upper tail: λ_U(q) = P(U₂ ≥ q | U₁ ≥ q)
331
+
332
+ where U₁ and U₂ are the pseudo-observations of the first and second variables,
333
+ respectively.
334
+
335
+ Parameters
336
+ ----------
337
+ X : array-like of shape (n_samples, 2), optional
338
+ If provided, it is used to plot the empirical tail concentration for
339
+ comparison versus the model tail concentration.
340
+
341
+ title : str, optional
342
+ The title for the plot. If not provided, a default title based on the fitted
343
+ copula's representation is used.
344
+
345
+ Returns
346
+ -------
347
+ fig : go.Figure
348
+ A Plotly figure object containing the tail concentration curve.
349
+
350
+ References
351
+ ----------
352
+ .. [1] "Quantitative Risk Management: Concepts, Techniques, and Tools",
353
+ McNeil, Frey, Embrechts (2005)
354
+ """
355
+ if title is None:
356
+ title = f"Tail Concentration of Bivariate {self.__class__.__name__}"
357
+ if X is not None:
358
+ title += " vs Empirical"
359
+
360
+ quantiles = np.linspace(5e-3, 1.0 - 5e-3, num=100)
361
+ concentration = self.tail_concentration(quantiles)
362
+
363
+ tail_concentration_dict = {self.__class__.__name__: concentration}
364
+ if X is not None:
365
+ tail_concentration_dict["Empirical"] = empirical_tail_concentration(
366
+ X, quantiles=quantiles
367
+ )
368
+
369
+ fig = plot_tail_concentration(
370
+ tail_concentration_dict=tail_concentration_dict,
371
+ quantiles=quantiles,
372
+ title=title,
373
+ smoothing=1.3,
374
+ )
375
+ return fig
376
+
377
+ def plot_pdf_2d(self, title: str | None = None) -> go.Figure:
378
+ """
379
+ Plot a 2D contour of the estimated probability density function (PDF).
380
+
381
+ This method generates a grid over [0, 1]^2, computes the PDF, and displays a
382
+ contour plot of the PDF.
383
+ Contour levels are limited to the 97th quantile to avoid extreme densities.
384
+
385
+ Parameters
386
+ ----------
387
+ title : str, optional
388
+ The title for the plot. If not provided, a default title based on the fitted
389
+ copula's representation is used.
390
+
391
+ Returns
392
+ -------
393
+ fig : go.Figure
394
+ A Plotly figure object containing the 2D contour plot of the PDF.
395
+ """
396
+ skv.check_is_fitted(self)
397
+
398
+ if title is None:
399
+ title = f"PDF of the Bivariate {self.__class__.__name__}"
400
+
401
+ u = np.linspace(0.01, 0.99, 100)
402
+ U, V = np.meshgrid(u, u)
403
+ grid_points = np.column_stack((U.ravel(), V.ravel()))
404
+ pdfs = np.exp(self.score_samples(grid_points)).reshape(U.shape)
405
+ # After the 97th quantile, the pdf gets too dense, and it dilutes the plot.
406
+ end = round(np.quantile(pdfs, 0.97), 1)
407
+ fig = go.Figure(
408
+ data=go.Contour(
409
+ x=u,
410
+ y=u,
411
+ z=pdfs,
412
+ colorscale="Magma",
413
+ contours=dict(start=0, end=end, size=0.2),
414
+ line=dict(width=0),
415
+ colorbar=dict(title="PDF"),
416
+ )
417
+ )
418
+ fig.update_layout(
419
+ title=title,
420
+ xaxis_title="u",
421
+ yaxis_title="v",
422
+ )
423
+ return fig
424
+
425
+ def plot_pdf_3d(self, title: str | None = None) -> go.Figure:
426
+ """
427
+ Plot a 3D surface of the estimated probability density function (PDF).
428
+
429
+ This method generates a grid over [0, 1]^2, computes the PDF, and displays a
430
+ 3D surface plot of the PDF using Plotly.
431
+
432
+ Parameters
433
+ ----------
434
+ title : str, optional
435
+ The title for the plot. If not provided, a default title based on the fitted
436
+ copula's representation is used.
437
+
438
+ Returns
439
+ -------
440
+ fig : go.Figure
441
+ A Plotly figure object containing a 3D surface plot of the PDF.
442
+ """
443
+ skv.check_is_fitted(self)
444
+
445
+ if title is None:
446
+ title = f"PDF of the Bivariate {self.__class__.__name__}"
447
+
448
+ u = np.linspace(0.03, 0.97, 100)
449
+ U, V = np.meshgrid(u, u)
450
+ grid_points = np.column_stack((U.ravel(), V.ravel()))
451
+ pdfs = np.exp(self.score_samples(grid_points)).reshape(U.shape)
452
+ fig = go.Figure(data=[go.Surface(x=U, y=V, z=pdfs, colorscale="Magma")])
453
+ fig.update_layout(
454
+ title=title, scene=dict(xaxis_title="u", yaxis_title="v", zaxis_title="PDF")
455
+ )
456
+ return fig