Copula models in Python using sympy
Table of Contents
Figure 1: Frank copula probability density function visualisation
Introduction
A copula is a cumulative distribution function with uniform marginals 1. Copulas are a popular statistical tool in risk management because they provide a method for capturing and characterising co-dependency between arbitrary random variables. Copula allow marginal distributions to be estimated independently, with dependency defined on-top of them.
SymPy is a Python library for symbolic mathematics 2. In this article, I use it to simplify differentiation i.e. I use SymPy just for its autodiff capabilities.
In this post, I share some Archimedean copula I have written in Python. The CDF and PDF are computed and visualised for bivariate models.
Tail dependence
The tail dependence of two random variables is a measure of their co-movements in the tails (extremes) of the distributions. Two random variables can be uncorrelated but still exhibit tail dependence. This is especially true in stock returns during moments of panic.
It is a stylized fact of stock returns that they commonly exhibit tail dependence. 3
Upper tail dependence
Upper tail dependence refers to co-movements in the upper tails of two random variables \(X_1\) and \(X_2\).
Lower tail dependence
Lower tail dependence refers to co-movements in the lower tails of two random variables \(X_1\) and \(X_2\). Share prices tend to fall together at the same time rather than rise together. This is due to the aforementioned panic selling phenomenon.
Base copula
An Archimedean copula is a type of copula function that is constructed using a so-called generator function. The generator function is a mathematical function that satisfies a set of conditions, including being continuous, strictly decreasing, and convex. Some examples of Archimedean generator functions include the Gumbel, Clayton, and Frank functions.
The following class implements a base Archimedean copula. It must be instantiated with a theta value \(\theta\) and a copula formula:
class Archimedean: def __init__(self, theta, copula): self._theta = theta self._c = copula def cdf(self, input_1, input_2): u1, u2, theta = sp.symbols("u1 u2 theta") phi1 = self._c.generator(u1, theta) phi2 = self._c.generator(u2, theta) expr = self._c.inverse(phi1 + phi2, theta) return expr.subs({"u1": input_1, "u2": input_2, "theta": self._theta}) def pdf(self, input_1, input_2): u1, u2 = sp.symbols("u1 u2") expr = sp.diff(self.cdf(u1, u2), u1, u2) return expr.subs({"u1": input_1, "u2": input_2}) def conditional_probability(self, input_1, input_2, wrt): u1, u2 = sp.symbols("u1 u2") if wrt == 1: expr = sp.diff(self.cdf(u1, u2), u1) elif wrt == 2: expr = sp.diff(self.cdf(u1, u2), u2) else: raise Exception("with respect to must be 1 or 2") return expr.subs({"u1": input_1, "u2": input_2}) def reflected(self, input_1, input_2): u1, u2 = sp.symbols("u1 u2") expr = u1 + u2 - 1 + self.cdf(1 - u1, 1 - u2) return expr.subs({"u1": input_1, "u2": input_2})
Clayton
The Clayton copula is an Archimedean copula that emphasises lower tail dependency. The density function appears to be more spiky for larger values of theta 4.
class Clayton: def generator(self, t, theta): return ((t ** -theta) - 1) / theta def inverse(self, t, theta): return ((theta * t) + 1) ** -(1 / theta)
Figure 2: Clayton PDF (1)
Figure 3: Clayton PDF (2)
Frank
The Frank copula is an Archimedean copula that emphasises both upper and lower tail dependency. The function is very smooth when theta is small e.g. when \(\theta \le 1\), and appears to stiffen as theta grows towards infinity 5.
class Frank: def generator(self, t, theta): return -sp.log(((e ** -(theta * t)) - 1) / ((e ** -theta) - 1)) def inverse(self, t, theta): return -(1 / theta) * sp.log((((e ** -theta) - 1) / (e ** t)) + 1)
Figure 4: Frank CDF (1)
Figure 5: Frank CDF (2)
Figure 6: Frank PDF (1)
Figure 7: Frank PDF (2)
Figure 8: Frank PDF (3)
N14
The N14 copula is an Archimedean copula that puts emphasis on both lower and upper tail dependency; the emphasis is greater for upper dependency. Similar to Clayton: the density function appears to get spiky for larger theta 6
class N14: def generator(self, t, theta): return ((t ** (-1 / theta)) - 1) ** theta def inverse(self, t, theta): return (t ** (1 / theta) + 1) ** -theta
Figure 9: N14 PDF