Repository: statsmodels/statsmodels

Issue:
Added fit params for the poisson model inside the NB
I added the minimum amount of parameter to fit the Poisson inside the NB fit. They are inherent from the NB paramters.

This solve many potential problems with the convergence, including #3572

thx


Discussion:

[![Coverage Status](https://coveralls.io/builds/10771239/badge)](https://coveralls.io/builds/10771239)

Coverage decreased (-0.007%) to 90.29% when pulling **78112d604973573158fc034d8d411a1b260c2a90 on denadai2:master** into **c9068817b9b1bea0777430f81b6d088d9782468a on statsmodels:master**.

some test failures in test_sandwich_cov.TestNegbinCluExposure

needs decision for 0.9

I kind of like the solution, but I'm not sure yet whether I prefer an alternative with explicit keywords, or some kind of fit_difficult solution.
related #3921 should fix llnull convergence problems

about the test failure in `test_sandwich_cov.TestNegbinCluExposure`
The test fails on scipy < 0.18, AFAICS, which is the same problem with bfgs that I had in #3921
The dataset for test case is ships.csv in discrete.tests.results

In the test case the optimization option are not changed in the fit call. However, Poisson defaults to "newton" and NegativeBinomial defaults to "bfgs". After the change in this PR, `method="bfgs"` in NB gets transmitted to Poisson which doesn't seem to work in the older scipy.
AFAIR, since scipy 0.18, bfgs has worked well again, after fixing to allow smaller stepsizes.

A related possibility: If the preliminary estimator finds a good start_params for the main model, even if that optimization is difficult, the main model estimation might be easier and would work with a more efficient optimizer. In that case we would want a robust optimizer like "nm" in the preliminary and "newton" or similar in the main estimation.

A change needed if we do something like in this PR: we need to transmit other keywords, at least the option for "minimize", so we can, e.g., use dogleg for both. e.g.
`fit_kwds = dict(method="minimize", min_method='dogleg')`

Another issue, that I don't remember whether I tried for Poisson and Negbin is to start from the null model estimate if constant is included in the model, i.e. use mean for constant and zeros for slope coefficients.

adding another issue here:
the preliminary estimator does not `skip_hessian` (merged in #1952). The hessian is not needed and is an unused, possibly expensive computation.
trying out the example (without offset)

solution with an attribute for fit options

```
model_nb.optim_kwds_prelim = dict(method='bfgs')
res_nb_d = model_nb.fit()

Optimization terminated successfully.
         Current function value: 6.452032
         Iterations: 14
         Function evaluations: 15
         Gradient evaluations: 15
```

or putting it in fit `**kwargs`:
```
res_nb_prelim = model_nb.fit(optim_kwds_prelim=dict(method='bfgs'))

Optimization terminated successfully.
         Current function value: 6.452032
         Iterations: 14
         Function evaluations: 15
         Gradient evaluations: 15
```

I like the fit keyword better, but given the current `**kwargs` we cannot add it as explicit keyword, and I don't like to put it before the regular fit keywords.

just a few "nm" iterations in preliminary Poisson are enough in this case.
"nm" does not converge in 50 iterations, `warn_convergence=False` (as used in llnull) avoids printing the warning

```
res_nb_prelim = model_nb.fit(optim_kwds_prelim=dict(method='nm', maxiter=50, warn_convergence=False))`\

Optimization terminated successfully.
         Current function value: 6.452032
         Iterations: 14
         Function evaluations: 15
         Gradient evaluations: 15
```

with only two explanatory variables, `skip_hessian` does not make any difference
bfgs as prelim is faster than "nm" with maxiter=50, in this case, and with scipy 0.18.1

```
In [37]:
%timeit res_nb_prelim = model_nb.fit(disp=0, optim_kwds_prelim=dict(method='nm', maxiter=50, warn_convergence=False, skip_hessian=True))
10 loops, best of 3: 24.2 ms per loop

In [35]:
%timeit res_nb_prelim = model_nb.fit(disp=0, optim_kwds_prelim=dict(method='nm', maxiter=50, warn_convergence=False))
10 loops, best of 3: 25.5 ms per loop

In [38]:
%timeit res_nb_prelim = model_nb.fit(disp=0, optim_kwds_prelim=dict(method='bfgs', maxiter=50, warn_convergence=False, skip_hessian=True))
100 loops, best of 3: 14.8 ms per loop
```

​

Resolving patch:
diff --git a/statsmodels/discrete/discrete_model.py b/statsmodels/discrete/discrete_model.py
--- a/statsmodels/discrete/discrete_model.py
+++ b/statsmodels/discrete/discrete_model.py
@@ -1022,6 +1022,12 @@ def _get_start_params_null(self):
 
     def fit(self, start_params=None, method='newton', maxiter=35,
             full_output=1, disp=1, callback=None, **kwargs):
+
+        if start_params is None and self.data.const_idx is not None:
+            # k_params or k_exog not available?
+            start_params = 0.001 * np.ones(self.exog.shape[1])
+            start_params[self.data.const_idx] = self._get_start_params_null()[0]
+
         cntfit = super(CountModel, self).fit(start_params=start_params,
                 method=method, maxiter=maxiter, full_output=full_output,
                 disp=disp, callback=callback, **kwargs)
@@ -1317,19 +1323,25 @@ def loglikeobs(self, params):
     def _get_start_params_null(self):
         offset = getattr(self, "offset", 0)
         exposure = getattr(self, "exposure", 0)
-        q = self.parameterization
 
         const = (self.endog / np.exp(offset + exposure)).mean()
         params = [np.log(const)]
         mu = const * np.exp(offset + exposure)
         resid = self.endog - mu
-        a = ((np.abs(resid) / np.sqrt(mu) - 1) * mu**(-q)).mean()
+        a = self._estimate_dispersion(mu, resid, df_resid=resid.shape[0] - 1)
         params.append(a)
 
         return np.array(params)
 
     _get_start_params_null.__doc__ = _get_start_params_null_docs
 
+    def _estimate_dispersion(self, mu, resid, df_resid=None):
+        q = self.parameterization
+        if df_resid is None:
+            df_resid = resid.shape[0]
+        a = ((np.abs(resid) / np.sqrt(mu) - 1) * mu**(-q)).sum() / df_resid
+        return a
+
     def fit(self, start_params=None, method='bfgs', maxiter=35,
             full_output=1, disp=1, callback=None, use_transparams = False,
             cov_type='nonrobust', cov_kwds=None, use_t=None, **kwargs):
@@ -1355,9 +1367,15 @@ def fit(self, start_params=None, method='bfgs', maxiter=35,
             offset = getattr(self, "offset", 0) + getattr(self, "exposure", 0)
             if np.size(offset) == 1 and offset == 0:
                 offset = None
+            optim_kwds_prelim = {'disp': 0, 'skip_hessian': True,
+                                 'warn_convergence': False}
+            optim_kwds_prelim.update(kwargs.get('optim_kwds_prelim', {}))
             mod_poi = Poisson(self.endog, self.exog, offset=offset)
-            start_params = mod_poi.fit(disp=0, method=method).params
-            start_params = np.append(start_params, 0.1)
+            res_poi = mod_poi.fit(**optim_kwds_prelim)
+            start_params = res_poi.params
+            a = self._estimate_dispersion(res_poi.predict(), res_poi.resid,
+                                          df_resid=res_poi.df_resid)
+            start_params = np.append(start_params, max(-0.1, a))
 
         if callback is None:
             # work around perfect separation callback #3895
@@ -2671,16 +2689,22 @@ def _get_start_params_null(self):
         params = [np.log(const)]
         mu = const * np.exp(offset + exposure)
         resid = self.endog - mu
-        if self.loglike_method == 'nb2':
-            #params.append(np.linalg.pinv(mu[:,None]).dot(resid**2 / mu - 1))
-            a = ((resid**2 / mu - 1) / mu).mean()
-            params.append(a)
-        else: #self.loglike_method == 'nb1':
-            params.append((resid**2 / mu - 1).mean())
+        a = self._estimate_dispersion(mu, resid, df_resid=resid.shape[0] - 1)
+        params.append(a)
         return np.array(params)
 
     _get_start_params_null.__doc__ = _get_start_params_null_docs
 
+    def _estimate_dispersion(self, mu, resid, df_resid=None):
+        if df_resid is None:
+            df_resid = resid.shape[0]
+        if self.loglike_method == 'nb2':
+            #params.append(np.linalg.pinv(mu[:,None]).dot(resid**2 / mu - 1))
+            a = ((resid**2 / mu - 1) / mu).sum() / df_resid
+        else: #self.loglike_method == 'nb1':
+            a = (resid**2 / mu - 1).sum() / df_resid
+        return a
+
     def fit(self, start_params=None, method='bfgs', maxiter=35,
             full_output=1, disp=1, callback=None,
             cov_type='nonrobust', cov_kwds=None, use_t=None, **kwargs):
@@ -2700,10 +2724,16 @@ def fit(self, start_params=None, method='bfgs', maxiter=35,
             offset = getattr(self, "offset", 0) + getattr(self, "exposure", 0)
             if np.size(offset) == 1 and offset == 0:
                 offset = None
+            optim_kwds_prelim = {'disp': 0, 'skip_hessian': True,
+                                 'warn_convergence': False}
+            optim_kwds_prelim.update(kwargs.get('optim_kwds_prelim', {}))
             mod_poi = Poisson(self.endog, self.exog, offset=offset)
-            start_params = mod_poi.fit(disp=0).params
+            res_poi = mod_poi.fit(**optim_kwds_prelim)
+            start_params = res_poi.params
             if self.loglike_method.startswith('nb'):
-                start_params = np.append(start_params, 0.1)
+                a = self._estimate_dispersion(res_poi.predict(), res_poi.resid,
+                                              df_resid=res_poi.df_resid)
+                start_params = np.append(start_params, max(0.05, a))
         else:
             if self._transparams is True:
                 # transform user provided start_params dispersion, see #3918
@@ -3020,13 +3050,20 @@ def _get_start_params_null(self):
         params = [np.log(const)]
         mu = const * np.exp(offset + exposure)
         resid = self.endog - mu
-        a = ((resid**2 / mu - 1) * mu**(-q)).mean()
+        a = self._estimate_dispersion(mu, resid, df_resid=resid.shape[0] - 1)
         params.append(a)
 
         return np.array(params)
 
     _get_start_params_null.__doc__ = _get_start_params_null_docs
 
+    def _estimate_dispersion(self, mu, resid, df_resid=None):
+        q = self.parameterization - 1
+        if df_resid is None:
+            df_resid = resid.shape[0]
+        a = ((resid**2 / mu - 1) * mu**(-q)).sum() / df_resid
+        return a
+
     def fit(self, start_params=None, method='bfgs', maxiter=35,
             full_output=1, disp=1, callback=None, use_transparams = False,
             cov_type='nonrobust', cov_kwds=None, use_t=None, **kwargs):
@@ -3052,9 +3089,16 @@ def fit(self, start_params=None, method='bfgs', maxiter=35,
             offset = getattr(self, "offset", 0) + getattr(self, "exposure", 0)
             if np.size(offset) == 1 and offset == 0:
                 offset = None
+
+            optim_kwds_prelim = {'disp': 0, 'skip_hessian': True,
+                                 'warn_convergence': False}
+            optim_kwds_prelim.update(kwargs.get('optim_kwds_prelim', {}))
             mod_poi = Poisson(self.endog, self.exog, offset=offset)
-            start_params = mod_poi.fit(disp=0).params
-            start_params = np.append(start_params, 0.1)
+            res_poi = mod_poi.fit(**optim_kwds_prelim)
+            start_params = res_poi.params
+            a = self._estimate_dispersion(res_poi.predict(), res_poi.resid,
+                                          df_resid=res_poi.df_resid)
+            start_params = np.append(start_params, max(0.05, a))
 
         if callback is None:
             # work around perfect separation callback #3895


Test patch:
diff --git a/statsmodels/discrete/tests/results/sm3533.csv b/statsmodels/discrete/tests/results/sm3533.csv
new file mode 100644
--- /dev/null
+++ b/statsmodels/discrete/tests/results/sm3533.csv
@@ -0,0 +1,271 @@
+pp,num
+10.171795655804711,1.0
+10.501472042414694,1742.0
+11.282291429896768,1.0
+11.43695113687264,0.0
+10.995897311894359,3071.0
+11.259593288117415,4829.0
+11.7913901878724,3.0
+10.89529383688643,284.0
+11.399319941347034,3558.0
+11.142513744919452,2907.0
+10.016101696905263,1.0
+11.134749595299702,1.0
+11.460178482911612,618.0
+10.332962113104644,0.0
+13.182255495711443,260.0
+9.873234509365107,101.0
+9.662243674190131,1.0
+11.31757945705352,3196.0
+11.17871067839023,1132.0
+10.605321890243529,1072.0
+10.849667129269148,17.0
+11.14135440850962,1.0
+12.194571763193121,1.0
+10.761280150015972,1264.0
+10.472968802094007,790.0
+10.488993841578681,2017.0
+9.914625296946584,1104.0
+10.889845245523059,976.0
+10.948980318873577,3127.0
+11.5754588521697,116.0
+10.83891402271876,12.0
+10.42611328041787,0.0
+11.966132569063815,1474.0
+10.193915850655934,1012.0
+10.237635661893249,970.0
+10.39714667138021,871.0
+10.809020627757324,0.0
+11.02875800051622,2867.0
+12.114171149556503,2.0
+10.268304262166483,1.0
+10.917304189499522,0.0
+11.818717556594327,820.0
+12.398893062575521,1150.0
+10.273843483530724,816.0
+11.12056412623904,0.0
+11.612707449517059,118.0
+11.074513521713284,1941.0
+10.742962032791647,435.0
+10.568055201212758,1483.0
+11.111820836139026,1577.0
+11.492804386974006,4605.0
+10.800656625202697,721.0
+10.848151918598122,2060.0
+10.753638481905739,0.0
+10.98918344927101,1.0
+11.165153627640535,0.0
+11.564863051756848,4734.0
+10.276877027171807,856.0
+11.286689637494428,0.0
+11.107180317654745,5218.0
+9.957833682276242,851.0
+10.990769224861854,1675.0
+11.62666478024292,1.0
+11.369701536831892,3140.0
+10.345605978292328,1932.0
+9.621655004873238,0.0
+11.749853571255056,2.0
+10.609723697637792,0.0
+11.24217916922465,533.0
+11.260443212190522,547.0
+9.775199532234597,1.0
+11.973282717164158,1.0
+12.39948249930955,2.0
+10.518051376717915,1950.0
+11.476168115244377,3200.0
+10.263013428702846,2267.0
+11.29761366527052,1582.0
+9.479527461791719,0.0
+10.918011460821951,123.0
+9.609384912281424,609.0
+11.414948733073855,0.0
+9.331406899955626,185.0
+9.940494278165223,1.0
+10.645520130825885,0.0
+10.053156777054832,316.0
+10.980399414224898,2991.0
+11.445620453959314,3494.0
+9.727227587120769,0.0
+10.004011390302427,698.0
+10.316986965672614,689.0
+11.364135391385066,3924.0
+11.841192703900717,6241.0
+10.513551992534897,1286.0
+9.898977397058243,0.0
+10.418315157681416,1508.0
+10.824208456689599,1043.0
+11.128820593983395,3372.0
+10.79605919778969,0.0
+9.409027282856401,590.0
+10.931122859815252,3334.0
+11.247317946836759,3475.0
+11.396245570629663,4236.0
+8.016977746762262,0.0
+10.919840790366809,0.0
+12.205947637717022,183.0
+11.233489301372298,4567.0
+8.931155429778348,506.0
+9.478763169489152,374.0
+11.830112940179575,7285.0
+10.812169410206767,1221.0
+12.048991843752006,817.0
+10.485004604424653,0.0
+10.240673819890999,1848.0
+11.230960352847676,0.0
+11.175254510173374,1.0
+10.923651694875096,2894.0
+9.52704712346858,0.0
+9.567385247481795,623.0
+10.956247856355338,1580.0
+11.406642301581638,4668.0
+11.760785043234838,4730.0
+9.625887815425859,632.0
+11.016167098415673,3149.0
+10.381087258403491,0.0
+9.664976524090797,1154.0
+10.981641999301841,2852.0
+11.553347379469145,1.0
+9.939337205429826,182.0
+9.708566960165658,696.0
+11.426495736971841,1767.0
+10.59518368082876,0.0
+11.332841403116465,1.0
+10.784876232401018,2281.0
+10.889080441396876,2319.0
+9.916601191681512,0.0
+11.043417712757153,0.0
+11.000965865163035,2943.0
+10.604454082148205,0.0
+10.928202103359991,0.0
+11.039154749155248,1694.0
+10.195410619245164,0.0
+10.955636966793897,1316.0
+9.478533767844102,0.0
+11.178752582806263,2963.0
+9.909121651153221,1182.0
+8.13622555490846,0.0
+11.495036405634488,0.0
+11.576651155850774,0.0
+9.272563715156315,0.0
+10.416071629316981,1693.0
+10.684760072732194,158.0
+10.020336819032421,1401.0
+11.86824749413235,0.0
+11.622792809600409,36.0
+10.139665782120794,960.0
+11.001649739923852,1739.0
+12.139216774744108,1.0
+12.239226918062332,814.0
+10.273187304197549,1105.0
+9.959584291887998,957.0
+11.551164927623898,2.0
+8.84779106484485,0.0
+10.341226221194589,0.0
+10.71005270013163,1283.0
+10.088015007879244,0.0
+11.293338082983785,0.0
+11.80365289560797,8247.0
+11.117138373413335,9719.0
+10.265105260864017,1111.0
+10.234229114448356,924.0
+9.612065160468624,544.0
+9.53870842969938,1.0
+10.012610979554482,1887.0
+10.467579023926712,0.0
+10.746734361340112,1745.0
+11.110266416192525,2441.0
+10.659257357575182,3468.0
+11.344991902770955,4480.0
+9.4053313318708,1.0
+10.856187356294447,1246.0
+10.25558704504304,0.0
+12.144888492205176,1.0
+11.123063000089472,2129.0
+10.157780872842554,923.0
+8.025843344150903,0.0
+10.638592082856581,0.0
+10.121578360213686,409.0
+10.24817594392868,1.0
+9.396155818024393,0.0
+11.514204646468608,4214.0
+11.835421922338627,1329.0
+10.73832958879877,1353.0
+9.962840778235641,1005.0
+10.051303927230347,606.0
+10.000069650382372,0.0
+11.571656530433991,5698.0
+10.620912625555597,2552.0
+9.616205400105674,725.0
+10.985292722887856,2581.0
+10.38294634055262,1291.0
+11.943187245295023,1.0
+10.98999352120919,0.0
+10.109810426119994,0.0
+10.386068010861816,1315.0
+10.61837176273886,0.0
+9.970772299341725,0.0
+9.070618428801046,0.0
+9.890706218171395,0.0
+11.041192341010333,4193.0
+10.499738295850376,2794.0
+10.908741089458378,2333.0
+8.284251797621916,0.0
+10.991173701070684,0.0
+11.153561193744727,2027.0
+10.300685246225866,1465.0
+12.054313270918852,5.0
+11.84448565723042,17.0
+11.485667314199365,4407.0
+9.452658902676433,677.0
+11.163481457539843,0.0
+9.658609509075912,0.0
+9.107310471656639,662.0
+9.1689974084418,0.0
+10.319694757176194,1072.0
+11.541736414824339,1.0
+11.538485993495504,4382.0
+10.50457426724686,1.0
+10.39714667138021,1374.0
+9.383957488139991,0.0
+10.443512690706546,0.0
+10.632773779711947,0.0
+8.975630184290507,0.0
+10.734111457653192,0.0
+11.062018331415482,1723.0
+10.151089542682712,1626.0
+11.594026074406777,2463.0
+11.375684342656722,2843.0
+10.50840443170166,57.0
+10.42812732013549,1.0
+10.995763139986401,0.0
+11.72499997991353,0.0
+11.408375731650665,1.0
+10.535211354858648,1.0
+11.449048058581468,1.0
+11.475389731441124,6662.0
+9.775370049755686,683.0
+9.464207095933233,679.0
+10.63904746172768,0.0
+10.693080631364326,458.0
+10.528757310692988,0.0
+10.263013428702846,1.0
+9.91788343281986,0.0
+11.757352898921226,267.0
+9.950132560359172,0.0
+9.502711433914095,0.0
+9.875345246589848,0.0
+12.047276190783021,40.0
+11.327860147505211,2933.0
+10.897257709680655,3857.0
+10.211413331464952,0.0
+9.911505324129712,851.0
+10.739240454183802,0.0
+11.608817293765796,980.0
+9.610792938260802,1446.0
+10.604057118911712,3130.0
+11.5332083690719,4090.0
+11.55856782284893,1338.0
+11.438697485615931,0.0
+11.709864661587565,1.0
+10.720377750501907,3485.0
diff --git a/statsmodels/discrete/tests/test_count_model.py b/statsmodels/discrete/tests/test_count_model.py
--- a/statsmodels/discrete/tests/test_count_model.py
+++ b/statsmodels/discrete/tests/test_count_model.py
@@ -340,8 +340,12 @@ def setup_class(cls):
         cls.endog = data.endog
         exog = sm.add_constant(data.exog[:,1], prepend=False)
         exog_infl = sm.add_constant(data.exog[:,0], prepend=False)
+        # cheating for now, parameters are not well identified in this dataset
+        # see https://github.com/statsmodels/statsmodels/pull/3928#issuecomment-331724022
+        sp = np.array([1.88, -10.28, -0.20, 1.14, 1.34])
         cls.res1 = sm.ZeroInflatedNegativeBinomialP(data.endog, exog,
-            exog_infl=exog_infl, p=2).fit(method='nm', maxiter=500)
+            exog_infl=exog_infl, p=2).fit(start_params=sp, method='nm',
+                                          xtol=1e-6, maxiter=5000)
         # for llnull test
         cls.res1._results._attach_nullmodel = True
         cls.init_keys = ['exog_infl', 'exposure', 'inflation', 'offset', 'p']
diff --git a/statsmodels/discrete/tests/test_diagnostic.py b/statsmodels/discrete/tests/test_diagnostic.py
--- a/statsmodels/discrete/tests/test_diagnostic.py
+++ b/statsmodels/discrete/tests/test_diagnostic.py
@@ -55,7 +55,7 @@ def test_count(self):
 
         # regression test
         tzi3_1 = (0.79863597832443878, 0.67077736750318928, 2, 2)
-        assert_allclose(tzi3, tzi3_1)
+        assert_allclose(tzi3, tzi3_1, rtol=5e-4)
 
     def test_probs(self):
         nobs = self.nobs
@@ -65,7 +65,7 @@ def test_probs(self):
         tzi = dia.test_chisquare_prob(self.res, probs[:, :2])
         # regression numbers
         tzi1 = (0.387770845, 0.5334734738)
-        assert_allclose(tzi[:2], tzi1)
+        assert_allclose(tzi[:2], tzi1, rtol=5e-5)
 
         # smoke test for plot
 
diff --git a/statsmodels/discrete/tests/test_discrete.py b/statsmodels/discrete/tests/test_discrete.py
--- a/statsmodels/discrete/tests/test_discrete.py
+++ b/statsmodels/discrete/tests/test_discrete.py
@@ -14,6 +14,7 @@
 import warnings
 
 import numpy as np
+import pandas as pd
 from numpy.testing import (assert_, assert_raises, assert_almost_equal,
                            assert_equal, assert_array_equal, assert_allclose,
                            assert_array_less)
@@ -1744,7 +1745,7 @@ def setup_class(cls):
         cls.endog = sm.distributions.genpoisson_p.rvs(mu_true,
             cls.expected_params[-1], 1, size=len(mu_true))
         model_gp = sm.GeneralizedPoisson(cls.endog, exog, p=1)
-        cls.res = model_gp.fit(method='nm', maxiter=5000, maxfun=5000)
+        cls.res = model_gp.fit(method='nm', xtol=1e-6, maxiter=5000, maxfun=5000)
 
     def test_basic(self):
         res = self.res
@@ -1763,7 +1764,7 @@ def test_newton(self):
         res = self.res
         res2 = res.model.fit(start_params=res.params, method='newton')
         assert_allclose(res.model.score(res.params),
-                        np.zeros(len(res2.params)), atol=5e-3)
+                        np.zeros(len(res2.params)), atol=0.01)
         assert_allclose(res.model.score(res2.params),
                         np.zeros(len(res2.params)), atol=1e-10)
         assert_allclose(res.params, res2.params, atol=1e-4)
@@ -2222,6 +2223,52 @@ def test_null_options():
     assert_equal(res._cache['llnull'],  999)
 
 
+def test_optim_kwds_prelim():
+    # test that fit options for preliminary fit is correctly transmitted
+
+    cur_dir = os.path.dirname(os.path.abspath(__file__))
+    filepath = os.path.join(cur_dir, "results", "sm3533.csv")
+    df = pd.read_csv(filepath)
+
+    features = ['pp']
+    X = (df[features] - df[features].mean())/df[features].std()
+    y = df['num'].values
+    exog = sm.add_constant(X[features].copy())
+    # offset=np.log(df['population'].values + 1)
+    # offset currently not used
+    offset = None
+
+    # we use "nm", "bfgs" does not work for Poisson/exp with older scipy
+    optim_kwds_prelim = dict(method='nm', maxiter=5000)
+    model = Poisson(y, exog, offset=offset) #
+    res_poi = model.fit(disp=0, **optim_kwds_prelim)
+
+    model = NegativeBinomial(y, exog, offset=offset)
+    res = model.fit(disp=0, optim_kwds_prelim=optim_kwds_prelim)
+
+    assert_allclose(res.mle_settings['start_params'][:-1], res_poi.params, rtol=1e-4)
+    assert_equal(res.mle_settings['optim_kwds_prelim'], optim_kwds_prelim)
+    assert_allclose(res.predict().mean(), y.mean(), rtol=0.1)
+
+    # NBP22 and GPP p=1.5 also fail on older scipy with bfgs, use nm instead
+    optim_kwds_prelim = dict(method='nm', maxiter=5000)
+    model = NegativeBinomialP(y, exog, offset=offset, p=2)
+    res = model.fit(disp=0, optim_kwds_prelim=optim_kwds_prelim)
+
+    assert_allclose(res.mle_settings['start_params'][:-1], res_poi.params, rtol=1e-4)
+    assert_equal(res.mle_settings['optim_kwds_prelim'], optim_kwds_prelim)
+    assert_allclose(res.predict().mean(), y.mean(), rtol=0.1)
+
+    # GPP with p=1.5 converges correctly,
+    # GPP fails when p=2 even with good start_params
+    model = GeneralizedPoisson(y, exog, offset=offset, p=1.5)
+    res = model.fit(disp=0, maxiter=200, optim_kwds_prelim=optim_kwds_prelim )
+
+    assert_allclose(res.mle_settings['start_params'][:-1], res_poi.params, rtol=1e-4)
+    assert_equal(res.mle_settings['optim_kwds_prelim'], optim_kwds_prelim)
+    # rough check that convergence makes sense
+    assert_allclose(res.predict().mean(), y.mean(), rtol=0.1)
+
 if __name__ == "__main__":
     import pytest
     pytest.main([__file__, '-vvs', '-x', '--pdb'])
diff --git a/statsmodels/discrete/tests/test_sandwich_cov.py b/statsmodels/discrete/tests/test_sandwich_cov.py
--- a/statsmodels/discrete/tests/test_sandwich_cov.py
+++ b/statsmodels/discrete/tests/test_sandwich_cov.py
@@ -374,7 +374,7 @@ class TestNegbinClu(CheckCountRobustMixin):
     def setup_class(cls):
         cls.res2 = results_st.results_negbin_clu
         mod = smd.NegativeBinomial(endog, exog)
-        cls.res1 = mod.fit(disp=False)
+        cls.res1 = mod.fit(disp=False, gtol=1e-7)
         cls.get_robust_clu()
 
 
@@ -408,7 +408,7 @@ class TestNegbinCluGeneric(CheckCountRobustMixin):
     def setup_class(cls):
         cls.res2 = results_st.results_negbin_clu
         mod = smd.NegativeBinomial(endog, exog)
-        cls.res1 = res1 = mod.fit(disp=False)
+        cls.res1 = res1 = mod.fit(disp=False, gtol=1e-7)
 
         get_robustcov_results(cls.res1._results, 'cluster',
                                                   groups=group,
@@ -437,7 +437,7 @@ def setup_class(cls):
                                                 use_correction=True,
                                                 df_correction=True),  #TODO has no effect
                                   use_t=False, #True,
-                                  )
+                                  gtol=1e-7)
         cls.bse_rob = cls.res1.bse
 
         nobs, k_vars = mod.exog.shape