Repository: statsmodels/statsmodels

Issue:
BUG/ENH: Kendall’s tau of FrankCopula wrong for small theta
For the Frank Copula, if the theta parameter tends to 0, Kendall's tau should also approach 0 as this is the limit case for independence.

However, I get:
```
from statsmodels.distributions.copula.archimedean import FrankCopula
c=FrankCopula()
[c.tau(th) for th in [1, 0.5, 0.1, 0.05, 0.01, 1e-3, 1e-4, 1e-5]]
[0.110018, 0.05541, 0.01106, 0.00536, -0.003657, -0.4767260, -47.6837, -4768.3714]
```
tau is defined to 
![grafik](https://user-images.githubusercontent.com/67651262/217354975-4d9602a9-8f07-4006-9681-9f31d5f55e5f.png)
where
![grafik](https://user-images.githubusercontent.com/67651262/217355224-86ca9148-b6ef-4664-8c1a-50791ec78e02.png)
is the [Debye function](https://en.wikipedia.org/wiki/Debye_function) of order one.


It seems that the result for small theta depends critically on the numeric precision of the Debye value which is calculated like this:

https://github.com/statsmodels/statsmodels/blob/91317661337ec92a71353ecdf8eae530d61b3a7c/statsmodels/distributions/copula/archimedean.py#L25

So, I am asking why np.float32 is used which has a value of "just" 1.1920929e-07 for me.

It seems even possible to set the left value to 0, even if it is 0/0 there. (without warnings)
This will keep results positive until 1e-5 after all.

Maybe there are more accurate implementations. Also, it seems that debye function is somehow related to the polylogarithm.


Discussion:
I don't know (or remember) where the epsilon comes from, most likely copied from somewhere.
It looks appropriate if the integrated values are in the range of 1.0

We never checked cases that go close to a boundary of the parameter space, AFAIR

Putting the alpha inside the integrand might help for small alpha

It looks like that goes to 1 

integral_0^0.000001   t/(e^t - 1) / 0.000001 dt 
-> 1

https://www.wolframalpha.com/input?i=integral_0%5E0.000001+++t%2F%28e%5Et+-+1%29+%2F+0.000001+dt+

graph using debye in the form the special function
plot shows it increases to 1 as alpha -> 0

https://www.wolframalpha.com/input?i=%28-1%2F2x%5E2%2Bln%281-e%5Ex%29x%2BLi_2%28e%5Ex%29-zeta%282%29%29+%2F+x

assuming I copy pasted correctly
I got the Taylor series expansion around 0 for both Debye and tau.
I still need to make a clean function, but should be able to fix this soon.

Resolving patch:
diff --git a/statsmodels/distributions/copula/archimedean.py b/statsmodels/distributions/copula/archimedean.py
--- a/statsmodels/distributions/copula/archimedean.py
+++ b/statsmodels/distributions/copula/archimedean.py
@@ -17,7 +17,8 @@
 
 
 def _debye(alpha):
-    EPSILON = np.finfo(np.float32).eps
+    # EPSILON = np.finfo(np.float32).eps
+    EPSILON = np.finfo(np.float64).eps * 100
 
     def integrand(t):
         return t / (np.exp(t) - 1)
@@ -26,6 +27,51 @@ def integrand(t):
     return debye_value
 
 
+def _debyem1_expansion(x):
+    """Debye function minus 1, Taylor series approximation around zero
+
+    function is not used
+    """
+    x = np.asarray(x)
+    # Expansion derived using Wolfram alpha
+    dm1 = (-x/4 + x**2/36 - x**4/3600 + x**6/211680 - x**8/10886400 +
+           x**10/526901760 - x**12 * 691/16999766784000)
+    return dm1
+
+
+def tau_frank(theta):
+    """Kendall's tau for Frank Copula
+
+    This uses Taylor series expansion for theta <= 1.
+
+    Parameters
+    ----------
+    theta : float
+        Parameter of the Frank copula. (not vectorized)
+
+    Returns
+    -------
+    tau : float, tau for given theta
+    """
+
+    if theta <= 1:
+        tau = _tau_frank_expansion(theta)
+    else:
+        debye_value = _debye(theta)
+        tau = 1 + 4 * (debye_value - 1) / theta
+
+    return tau
+
+
+def _tau_frank_expansion(x):
+    x = np.asarray(x)
+    # expansion derived using wolfram alpha
+    # agrees better with R copula for x<=1, maybe even for larger theta
+    tau = (x/9 - x**3/900 + x**5/52920 - x**7/2721600 + x**9/131725440 -
+           x**11 * 691/4249941696000)
+    return tau
+
+
 class ArchimedeanCopula(Copula):
     """Base class for Archimedean copulas
 
@@ -330,8 +376,8 @@ def tau(self, theta=None):
         # Joe 2014 p. 166
         if theta is None:
             theta = self.theta
-        debye_value = _debye(theta)
-        return 1 + 4 * (debye_value - 1) / theta
+
+        return tau_frank(theta)
 
     def theta_from_tau(self, tau):
         MIN_FLOAT_LOG = np.log(sys.float_info.min)
@@ -340,7 +386,10 @@ def theta_from_tau(self, tau):
         def _theta_from_tau(alpha):
             return self.tau(theta=alpha) - tau
 
-        result = optimize.least_squares(_theta_from_tau, 1, bounds=(
+        # avoid start=1, because break in tau approximation method
+        start = 0.5 if tau < 0.11 else 2
+
+        result = optimize.least_squares(_theta_from_tau, start, bounds=(
             MIN_FLOAT_LOG, MAX_FLOAT_LOG))
         theta = result.x[0]
         return theta


Test patch:
diff --git a/statsmodels/distributions/copula/tests/test_copula.py b/statsmodels/distributions/copula/tests/test_copula.py
--- a/statsmodels/distributions/copula/tests/test_copula.py
+++ b/statsmodels/distributions/copula/tests/test_copula.py
@@ -21,6 +21,7 @@
     ClaytonCopula,
     FrankCopula,
     GumbelCopula,
+    _debyem1_expansion,
 )
 from statsmodels.distributions.copula.copulas import CopulaDistribution
 import statsmodels.distributions.copula.depfunc_ev as trev
@@ -379,6 +380,23 @@ def test_basic(self):
         ppfc = cop.ppfcond_2g1(cdfc, [0.6], args=args)
         assert_allclose(ppfc, u[1], rtol=1e-13)
 
+    def test_tau(self):
+        copula = FrankCopula(k_dim=2)
+
+        theta = [2, 1, 1e-2, 1e-4, 1e-5, 1e-6]
+        # > tau(frankCopula(param = 2, dim = 2))
+        tau_r = [0.2138945692196201, 0.110018536448993, 0.001111110000028503,
+                 1.111110992013664e-05, 1.111104651951855e-06,
+                 1.108825244955369e-07]
+
+        tau_cop = [copula.tau(th) for th in theta]
+        assert_allclose(tau_cop[:-1], tau_r[:-1], rtol=1e-5)
+        # relative precision at very small tau is not very high
+
+        # check debye function
+        taud = 1 + 4 * _debyem1_expansion(theta) / theta
+        assert_allclose(taud, tau_cop, rtol=1e-5)
+
 
 # The reference results are coming from the R package Copula.
 # See ``copula_r_tests.rst`` for more details.