Skip to content

Overflow during the estimation of empirical accuracy #106

Description

@fc-jian

Issue:
At line 268 of alignparse/consensus.py, the binomial coefficients are precomputed for subsequent likelihood calculations. However, for large values of n and k, directly computing them using scipy.special.binom results in inf values, which can cause errors during likelihood optimization.

Proposed solution:
To address this, we can estimate the logarithm of the binomial coefficients using Stirling’s approximation instead of computing them directly. The subsequent likelihood calculation can then be divided into two cases:

  • When n = u: the binomial coefficients remain small, so direct computation can be retained.
  • When n ≠ u: the binomial coefficients can become extremely large, but since the additional p ** n term is absent, the computation can be performed entirely in log space.

Example implementation:

def log_comb(n, k):
    """
    Robustly compute log[C(n, k)].
    For small/moderate n, use scipy.special.binom.
    For large n, use Stirling approximation.
    """
    try:
        # Try direct computation
        val = scipy.special.binom(n, k)
        # if finite and not huge
        if numpy.isfinite(val) and val > 0 and val < numpy.inf:
            return numpy.log(val)
        else:
            raise OverflowError
    except Exception:
        # Stirling approximation for large n,k
        # Use: log C(n,k) = n*log(n) - k*log(k) - (n-k)*log(n-k)
        #       + 0.5*log(n / (2π k (n-k)))
        log2pi = numpy.log(2 * numpy.pi)
        term = (
            n * numpy.log(n)
            - k * numpy.log(k)
            - (n - k) * numpy.log(n - k)
            + 0.5 * (numpy.log(n) - numpy.log(k) - numpy.log(n - k) - log2pi)
        )
        return term

class _LnL_error_rate:
    def __init__(self, df, *, n_col, u_col, count_col):
        """See main class docstring."""
        _df = df.assign(
            n=lambda x: x[n_col],
            u=lambda x: x[u_col],
            count=lambda x: x[count_col],
        )
        self._df_un = _df[['n', 'u', 'count']].query("n == u").assign(
            binom=lambda x: scipy.special.binom(x["n"], x["u"] - 1)
        )
        self._df_other = _df[['n', 'u', 'count']].query("n != u").assign(
            log_binom=lambda x: [log_comb(n, k) for n, k in zip(x["n"], x["u"] - 1)]
        )

    def lnlik(self, eps):
        """Log likelihood for error rate `eps`."""
        return sum(
            self._df_other["count"]
            * (
                self._df_other["log_binom"]
                + (self._df_other["n"] - self._df_other["u"] + 1) * numpy.log(1 - eps)
                + (self._df_other["u"] - 1) * numpy.log(eps)
            )
        ) + sum(
            self._df_un["count"]
            * numpy.log(
                self._df_un["binom"]
                * (1 - eps) ** (self._df_un["n"] - self._df_un["u"] + 1)
                * eps ** (self._df_un["u"] - 1)
                + eps ** self._df_un["n"]
            )
        )

    def neg_lnlik(self, eps):
        """Negative log likelihood for error rate `epsilon`."""
        return -self.lnlik(eps)

    def maxlik_eps(self):
        """Maximum likelihood value of error rate `epsilon`."""
        res = scipy.optimize.minimize_scalar(
            self.neg_lnlik, bounds=(1e-8, 1 - 1e-8), method="bounded"
        )
        if not res.success:
            raise RuntimeError(f"optimization failed:\n{res}")
        return res.x

Would you be open to this approach? If it seems reasonable, I’d be happy to open a PR to address this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions