2025, Dec 05 13:00

How to Fit SciPy's Negative Binomial with a Real-Valued n: Patch Shape Info to Bypass Integer Parameter Errors

Fit SciPy's Negative Binomial with continuous n using stats.fit. Fix the integer-parameter ValueError by patching shape info and run MLE via differential evolution.

Fitting a Negative Binomial to discrete data in SciPy looks straightforward until you try to estimate a real-valued n. The pmf handles non-integer n just fine, but the high-level fitting API rejects it with a domain error. If you’ve hit a ValueError about integer parameters and empty bounds, you’re running into a mismatch between the distribution’s parameter metadata and what you actually want to optimize.

Reproducing the error

The following minimal example attempts to fit stats.nbinom with a non-integer n using scipy.stats.fit and a differential evolution optimizer. The optimizer itself is fine; the complaint comes from how the distribution is configured for fitting.

from scipy import stats
from scipy.optimize import differential_evolution
import numpy as np

prng = np.random.default_rng()

def de_wrapper(objfun, box, *, integrality):
    return differential_evolution(
        objfun,
        box,
        strategy='best2bin',
        rng=prng,
        integrality=[False, False, False]
    )

observed = [0, 0, 0, 1, 2, 4, 11]
param_bounds = [(1.5, 1.55), (0, 1)]

fit_out = stats.fit(stats.nbinom, observed, param_bounds, optimizer=de_wrapper)
print(fit_out.params)

Running this produces:

ValueError: There are no integer values for `n` on the interval defined by the user-provided bounds and the domain of the distribution.

What actually goes wrong

The error isn’t from the optimizer; it’s triggered before optimization because stats.fit consults the distribution’s parameter shape metadata. In SciPy’s Negative Binomial, n is treated as an integer parameter. With bounds set to (1.5, 1.55), there is no integer inside that interval, so the fit is rejected immediately. That’s why you see the message about “no integer values for n” within the provided bounds.

At the same time, the pmf of stats.nbinom accepts non-integer n and will evaluate correctly for such inputs. This inconsistency means the distribution can be used with real n at the pmf level, but the default fitting pipeline enforces integrality for n.

Make n continuous by patching the distribution

To allow real-valued n during fitting, you can patch the distribution definition so that n is treated as continuous in its shape information. Once that constraint is lifted, stats.fit proceeds as expected with the optimizer of your choice.

from scipy import stats
from scipy.optimize import differential_evolution
import numpy as np
from scipy.stats._discrete_distns import nbinom_gen, _ShapeInfo


class NBinomReal(nbinom_gen):
    def _shape_info(self):
        # declare 'n' as non-integer, keep its domain and 'p' bounds
        return [
            _ShapeInfo("n", False, (0, np.inf), (True, False)),
            _ShapeInfo("p", False, (0, 1), (True, True))
        ]


def evo_driver(objfun, box, *, integrality):
    return differential_evolution(
        objfun,
        box,
        strategy='best2bin',
        integrality=[False, False, False]
    )

samples = [0, 0, 0, 1, 2, 4, 11]
search_space = [(0.1, 1.55), (0, 1)]
patched_dist = NBinomReal(name='nbinom')

fitted = stats.fit(patched_dist, samples, search_space, optimizer=evo_driver)
print(fitted)

This keeps the fitting workflow intact while allowing n to vary over positive reals. The domain for p remains between 0 and 1. The loc parameter is still available in the fit and is unconstrained unless you specify otherwise.

Why this detail matters

In some modeling tasks, including disease modeling, n carries a direct interpretation and does not need to be an integer. You might, for example, be interested in dispersion while letting n move continuously. The high-level API’s default integrality constraint blocks that use case; removing it aligns the fit with how the pmf already behaves and enables practical maximum-likelihood estimation for real-valued n.

Takeaway

If you see a ValueError about integer n when fitting the Negative Binomial with scipy.stats.fit, it comes from the distribution’s shape configuration rather than the optimizer. When you need real-valued n, patch the distribution so that n is treated as continuous in its shape info, then rerun the fit with your chosen optimizer and appropriate bounds. You’ll retain the convenience of the stats.fit interface while unlocking the parameterization you actually need.