Skip to content

Add bivariate copula implementation#137

Open
chrischu12 wants to merge 91 commits intosimon-hirsch:mainfrom
chrischu12:bivariate_copula
Open

Add bivariate copula implementation#137
chrischu12 wants to merge 91 commits intosimon-hirsch:mainfrom
chrischu12:bivariate_copula

Conversation

@chrischu12
Copy link

No description provided.

Copy link
Owner

@simon-hirsch simon-hirsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss the base class for the copulas, because now all distributions need to implement the param_link parameter. This should be solved by a CopulaMixin class that implements the necessary methods.

@simon-hirsch simon-hirsch requested review from BerriJ and Copilot July 3, 2025 09:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for bivariate copulas by introducing new link functions and multivariate copula distributions, and integrates them into the online multivariate distribution regression estimator.

  • Introduce FisherZLink and TauToPar in copulalinks.py
  • Extend base Distribution to accept param_links and update existing distributions
  • Add BiCopNormal and MarginalCopula classes for Gaussian copula modeling
  • Integrate copula-based parameter handling into online_mvdistreg

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ondil/link/copulalinks.py New Fisher Z and Kendall‐tau link functions
src/ondil/link/init.py Export copula link functions
src/ondil/base/distribution.py Add support for param_links in base Distribution
src/ondil/distributions/normal.py Update constructor signature for param_links
src/ondil/distributions/mv_t_low_rank.py Pass param_links through to superclass
src/ondil/distributions/mv_normal_chol.py Pass param_links through to superclass
src/ondil/distributions/mv_marg_cop.py New MarginalCopula distribution
src/ondil/distributions/bicop_normal.py New BiCopNormal Gaussian copula distribution
src/ondil/distributions/init.py Register new copula distributions
src/ondil/estimators/online_mvdistreg.py Integrate copula option and branching logic into fitting routine
Comments suppressed due to low confidence (3)

src/ondil/distributions/normal.py:47

  • The param_links parameter should be typed as dict[int, LinkFunction] instead of LinkFunction, and default to an empty dict. Update the annotation to param_links: dict[int, LinkFunction] = {}.
        param_links: LinkFunction = {},

src/ondil/distributions/bicop_normal.py:33

  • The constructor uses a singular param_link but the base class expects param_links: dict[int, LinkFunction]. Consider renaming to param_links and accepting a mapping keyed by parameter index.
        param_link: LinkFunction = TauToPar(),

src/ondil/estimators/online_mvdistreg.py:690

  • The code previously scaled X and stored it in X_scaled but now passes the raw X to _outer_fit, bypassing the scaling step. It should likely be X_scaled here.
        self._outer_fit(X=X, y=y, theta=theta)

@simon-hirsch simon-hirsch mentioned this pull request Jul 3, 2025
@simon-hirsch simon-hirsch changed the base branch from mvdistreg to main September 11, 2025 14:09
@simon-hirsch
Copy link
Owner

@chrischu12 I have changed the base branch as we #134 is merged now.

@simon-hirsch simon-hirsch requested a review from Copilot October 8, 2025 07:53
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 12 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

# If no regularization is allowed, we just return the theta
return theta

def _make_initial_eta(self, theta: np.ndarray):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be on the copula class?

theta[a] = self.distribution.set_initial_guess(y, theta[a], p)
theta = self._handle_path_regularization(theta=theta, p=p, a=a)
# if (inner_iteration == 0) and (outer_iteration == 0) & (a == 0):
# theta[a] = self.distribution.set_initial_guess(y, theta[a], p)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be commented as it is necessary for the multivariate case. Properly switch between copula and MV case in the fitting

dl2_link = self.distribution.cube_to_flat(dl2_link, param=p)
dl2_link = dl2_link[:, k]
else:
eta = self._make_initial_eta(theta)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated above

# If the likelihood is at some point decreasing, we're breaking
# Hence we need to store previous iteration values:
if (inner_iteration > 0) | (outer_iteration > 0):
if (inner_iteration == 0) and (outer_iteration == 0):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated below.

eta_elem, param=p, k=k, d=self.dim_
)

if issubclass(self.distribution.__class__, CopulaMixin):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this block should be as eta_to_theta_element on the copula class and then we don't need to import and check for specific copulas on the main estimator class.

param=p,
).reshape(-1, 1)

if isinstance(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this block. Should also be a function on the copula class (the same as above), eta_to_theta_element

old_likelihood = self._current_likelihood[a]

# If the LL is decreasing, we're resetting to the previous iteration
if ((outer_iteration > 0) | (inner_iteration > 1)) & decreasing:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why change this to > 0

Comment on lines +1650 to +1654
if issubclass(self.distribution.__class__, CopulaMixin) and p == 0:
out[a][p] = np.tanh(out[a][p] / 2) * (1 - 1e-8)
out[a][p] = self.distribution.param_link_inverse(
out[a][p] * (1 - 1e-8), param=0
) * (1 - 1e-8)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if issubclass(self.distribution.class, CopulaMixin) and p == 0:
out[a][p] = np.tanh(out[a][p] / 2) * (1 - 1e-8)
out[a][p] = self.distribution.param_link_inverse(
out[a][p] * (1 - 1e-8), param=0
) * (1 - 1e-8)

This pattern is duplicated a few times across the code. Can this be moved in the param_link_inverse for the copula?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants