Source code for copul.family.core.copula_plotting_mixin

from matplotlib import rcParams
import matplotlib.pyplot as plt
import numpy as np
import sympy as sp
from copul.family.copula_graphs import CopulaGraphs


[docs] class CopulaPlottingMixin: """ Mixin class for copula sampling methods. This class provides methods for sampling from copulas using different techniques. """
[docs] def scatter_plot( self, n=1_000, approximate=False, figsize=(10, 8), alpha=0.6, colormap="viridis", samples=None, ): """ Create a scatter plot of random variates from the copula. Parameters ---------- n : int, optional The number of samples to generate (default is 1,000). approximate : bool, optional Whether to use explicit sampling from the conditional distributions or approximate sampling with a checkerboard copula figsize : tuple, optional Figure size as (width, height) in inches. alpha : float, optional Transparency of points (0 to 1). colormap : str, optional Colormap to use for 3D plots. samples : np.ndarray, optional Pre-generated samples to plot. If provided, `n` is ignored. Returns ------- None """ if self.dim == 2: if samples is None: # Generate samples data_ = self.rvs(n, approximate=approximate) else: # Use provided samples data_ = samples plt.figure(figsize=figsize) plt.scatter(data_[:, 0], data_[:, 1], s=rcParams["lines.markersize"] ** 2) title = CopulaGraphs(self).get_copula_title() plt.title(title) plt.xlabel("u") plt.ylabel("v") plt.grid(True) plt.xlim(0, 1) plt.ylim(0, 1) plt.show() plt.close() elif self.dim == 3: # Generate samples if samples is None: data = self.rvs(n, approximate=approximate) else: data = samples # Create 3D figure and axes fig = plt.figure(figsize=figsize) ax = fig.add_subplot(111, projection="3d") # Create color mapping based on the third dimension for better visualization colors = data[:, 2] # Plot the 3D scatter points scatter = ax.scatter( data[:, 0], # x-coordinates (first margin) data[:, 1], # y-coordinates (second margin) data[:, 2], # z-coordinates (third margin) c=colors, # color by third dimension cmap=colormap, s=rcParams["lines.markersize"] ** 2, alpha=alpha, ) # Add a color bar to show the mapping cbar = fig.colorbar(scatter, ax=ax, pad=0.1) cbar.set_label("w value") # Set title and labels title = CopulaGraphs(self).get_copula_title() ax.set_title(title) ax.set_xlabel("u") ax.set_ylabel("v") ax.set_zlabel("w") # Set axis limits to the copula domain [0,1]³ ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.set_zlim(0, 1) # Add gridlines ax.grid(True) # Add a view angle that shows the 3D structure well ax.view_init(elev=30, azim=45) # Instead of tight_layout, adjust margins manually for 3D plots fig.subplots_adjust(top=0.9, bottom=0.1, left=0.1, right=0.9) plt.show() plt.close() else: # For higher dimensions, display scatter plot matrix if samples is None: data = self.rvs(n, approximate=approximate) else: data = samples # Create scatter plot matrix fig, axs = plt.subplots( self.dim, self.dim, figsize=(3 * self.dim, 3 * self.dim), ) # Get copula title title = CopulaGraphs(self).get_copula_title() fig.suptitle(title, fontsize=16) # Variable names var_names = [f"u{i + 1}" for i in range(self.dim)] # Fill the scatter plot matrix for i in range(self.dim): for j in range(self.dim): if i == j: # Histogram on the diagonal axs[i, j].hist(data[:, i], bins=20, alpha=0.7) else: # Scatter plot on off-diagonal axs[i, j].scatter( data[:, j], data[:, i], s=rcParams["lines.markersize"], alpha=0.5, ) # Set labels only on the outer plots if i == self.dim - 1: axs[i, j].set_xlabel(var_names[j]) if j == 0: axs[i, j].set_ylabel(var_names[i]) # Set limits axs[i, j].set_xlim(0, 1) axs[i, j].set_ylim(0, 1) plt.tight_layout(rect=[0, 0, 1, 0.96]) # Make room for the title plt.show() plt.close()
[docs] def save_plot( self, filename, n=1_000, approximate=False, figsize=(12, 10), alpha=0.7, colormap="viridis", style="default", point_size=None, dpi=300, grid_alpha=0.2, contour_levels=10, add_contours=True, add_marginals=True, format="png", transparent=False, ): """ Create and save a scatter plot of random variates from the copula. Parameters ---------- filename : str Path where the plot will be saved. n : int, optional The number of samples to generate (default is 1,000). approximate : bool, optional Whether to use explicit sampling or approximate sampling. figsize : tuple, optional Figure size as (width, height) in inches. alpha : float, optional Transparency of points (0 to 1). colormap : str, optional Colormap to use for plots. style : str, optional Matplotlib style to use. point_size : int, optional Size of scatter points. dpi : int, optional Resolution of the saved figure. grid_alpha : float, optional Transparency of grid lines (0 to 1). contour_levels : int, optional Number of contour levels to add. add_contours : bool, optional Whether to add density contours to 2D plots. add_marginals : bool, optional Whether to add marginal distributions to 2D plots. format : str, optional File format to save the plot (e.g., 'png', 'pdf', 'svg'). transparent : bool, optional Whether to save with a transparent background. Returns ------- None """ # Create the figure fig = self.scatter_plot( n=n, approximate=approximate, figsize=figsize, alpha=alpha, colormap=colormap, style=style, point_size=point_size, dpi=dpi, grid_alpha=grid_alpha, contour_levels=contour_levels, add_contours=add_contours, add_marginals=add_marginals, ) # Ensure the filename has the correct extension if not filename.lower().endswith(f".{format.lower()}"): filename = f"{filename}.{format.lower()}" # Save the figure fig.savefig( filename, dpi=dpi, bbox_inches="tight", transparent=transparent, format=format, ) plt.close(fig) print(f"Plot saved to: {filename}")
[docs] def plot_density( self, grid_size=50, figsize=(10, 8), colormap="viridis", style="default", dpi=120, add_contours=True, contour_levels=10, contour_colors="black", contour_alpha=0.6, add_colorbar=True, ): """ Create a density plot of the copula. Parameters ---------- grid_size : int, optional Number of grid points in each dimension. figsize : tuple, optional Figure size as (width, height) in inches. colormap : str, optional Colormap to use for the density plot. style : str, optional Matplotlib style to use. dpi : int, optional Resolution of the figure. add_contours : bool, optional Whether to add contour lines to the density plot. contour_levels : int, optional Number of contour levels to add. contour_colors : str or list, optional Color(s) for contour lines. contour_alpha : float, optional Transparency of contour lines (0 to 1). add_colorbar : bool, optional Whether to add a colorbar to the plot. Returns ------- fig : matplotlib.figure.Figure The created figure object. """ # Set the style if style != "default": plt.style.use(style) # Get title title = CopulaGraphs(self).get_copula_title() if self.dim == 2: # Create 2D grid x = np.linspace(0.001, 0.999, grid_size) y = np.linspace(0.001, 0.999, grid_size) X, Y = np.meshgrid(x, y) # Calculate the copula density positions = np.vstack([X.ravel(), Y.ravel()]).T try: density_values = np.array([self.pdf(pos) for pos in positions]) Z = density_values.reshape(X.shape) # Create figure fig, ax = plt.subplots(figsize=figsize, dpi=dpi) # Plot the density as a filled contour contourf = ax.contourf(X, Y, Z, levels=50, cmap=colormap, alpha=0.9) # Add contour lines if requested if add_contours: ax.contour( X, Y, Z, levels=contour_levels, colors=contour_colors, alpha=contour_alpha, ) # Add colorbar if requested if add_colorbar: cbar = fig.colorbar(contourf, ax=ax) cbar.set_label("Density") # Set title and labels ax.set_title(title + " Density", fontsize=14) ax.set_xlabel("u", fontsize=12) ax.set_ylabel("v", fontsize=12) # Set axis limits ax.set_xlim(0, 1) ax.set_ylim(0, 1) # Add grid ax.grid(True, alpha=0.2) plt.tight_layout() return fig except (AttributeError, NotImplementedError): print("Density function not available for this copula.") return None else: print("Density plots are currently only supported for 2D copulas.") return None
[docs] def create_comparison_plot( self, other_copula, n=1_000, figsize=(15, 7), colormap="viridis", style="default", dpi=120, ): """ Create a comparison plot between this copula and another one. Parameters ---------- other_copula : Copula Another copula to compare with. n : int, optional Number of samples to generate from each copula. figsize : tuple, optional Figure size as (width, height) in inches. colormap : str, optional Colormap to use for the plots. style : str, optional Matplotlib style to use. dpi : int, optional Resolution of the figure. Returns ------- fig : matplotlib.figure.Figure The created figure object. """ # Set the style if style != "default": plt.style.use(style) # Check dimensions if self.dim != other_copula.dim: print("Copulas must have the same dimension for comparison.") return None # Generate samples data1 = self.rvs(n) data2 = other_copula.rvs(n) # Get titles title1 = CopulaGraphs(self).get_copula_title() title2 = CopulaGraphs(other_copula).get_copula_title() if self.dim == 2: # Create figure with two subplots fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize, dpi=dpi) # Plot first copula scatter1 = ax1.scatter( data1[:, 0], data1[:, 1], s=rcParams["lines.markersize"] * 1.5, alpha=0.7, c=data1[:, 1], cmap=colormap, edgecolor="none", ) ax1.set_title(title1, fontsize=12) ax1.set_xlabel("u", fontsize=11) ax1.set_ylabel("v", fontsize=11) ax1.grid(True, alpha=0.2) ax1.set_xlim(0, 1) ax1.set_ylim(0, 1) # Plot second copula scatter2 = ax2.scatter( data2[:, 0], data2[:, 1], s=rcParams["lines.markersize"] * 1.5, alpha=0.7, c=data2[:, 1], cmap=colormap, edgecolor="none", ) ax2.set_title(title2, fontsize=12) ax2.set_xlabel("u", fontsize=11) ax2.set_ylabel("v", fontsize=11) ax2.grid(True, alpha=0.2) ax2.set_xlim(0, 1) ax2.set_ylim(0, 1) # Add colorbars fig.colorbar(scatter1, ax=ax1, pad=0.01) fig.colorbar(scatter2, ax=ax2, pad=0.01) # Add comparison title fig.suptitle(f"Comparison: {title1} vs {title2}", fontsize=14, y=0.98) plt.tight_layout(rect=[0, 0, 1, 0.95]) return fig else: print("Comparison plots are currently optimized for 2D copulas.") # Simply return the regular scatter plot matrix for higher dimensions return self.scatter_plot( n=n, figsize=figsize, colormap=colormap, style=style, dpi=dpi )
[docs] def plot_c_over_u(self, *, plot_type="3d", log_z=False, **kwargs): """ Plot the ratio C(u,v) / u on (0,1)². Works whether ``cdf`` is a SymPy expression/wrapper *or* a numeric Python function (e.g. in ``ShuffleOfMin``). Parameters ---------- plot_type : {"3d", "contour", "functions"}, optional - "3d" : surface plot (default) - "contour" : filled contour plot - "functions": 9 one-dimensional slices v = 0.1,…,0.9 log_z : bool, optional Log–colour scale for the contour plot. **kwargs : forwarded to the internal plotting routine. """ # ------------------------------------------------------------------ # Build a callable / SymPy expression for C(u,v)/u # ------------------------------------------------------------------ ratio_obj = None if hasattr(self.cdf, "func"): # SymPy wrapper ratio_obj = sp.simplify(self.cdf.func / self.u) elif isinstance(self.cdf, sp.Expr): # bare SymPy Expr ratio_obj = sp.simplify(self.cdf / self.u) else: # numeric callable def ratio_obj(u, v): return self.cdf(u, v) / u title = kwargs.pop( "title", f"{CopulaGraphs(self).get_copula_title()} – C(u,v)/u", ) zlabel = kwargs.pop("zlabel", r"$C(u,v)/u$") if plot_type == "3d": return self._plot3d(ratio_obj, title=title, zlabel=zlabel, **kwargs) if plot_type == "contour": return self._plot_contour( ratio_obj, title=title, zlabel=zlabel, log_z=log_z, **kwargs ) if plot_type == "functions": return self._plot_functions( ratio_obj, title=title, zlabel=zlabel, xlabel="u", **kwargs ) raise ValueError("plot_type must be '3d', 'contour', or 'functions'.")
[docs] def plot_c_over_v(self, *, plot_type="3d", log_z=False, **kwargs): """ Plot the ratio C(u,v) / v on (0,1)². The interface is identical to ``plot_c_over_u``. """ # ------------------------------------------------------------------ # Build a callable / SymPy expression for C(u,v)/v # ------------------------------------------------------------------ ratio_obj = None if hasattr(self.cdf, "func"): # SymPy wrapper ratio_obj = sp.simplify(self.cdf.func / self.v) elif isinstance(self.cdf, sp.Expr): # bare SymPy Expr ratio_obj = sp.simplify(self.cdf / self.v) else: # numeric callable def ratio_obj(u, v): return self.cdf(u, v) / v title = kwargs.pop( "title", f"{CopulaGraphs(self).get_copula_title()} – C(u,v)/v", ) zlabel = kwargs.pop("zlabel", r"$C(u,v)/v$") if plot_type == "3d": return self._plot3d(ratio_obj, title=title, zlabel=zlabel, **kwargs) if plot_type == "contour": return self._plot_contour( ratio_obj, title=title, zlabel=zlabel, log_z=log_z, **kwargs ) if plot_type == "functions": return self._plot_functions( ratio_obj, title=title, zlabel=zlabel, xlabel="u", **kwargs ) raise ValueError("plot_type must be '3d', 'contour', or 'functions'.")