Repository: statsmodels/statsmodels

Issue:
got multiple values for argument 'constraints', 'bounds'
#### Describe the bug
error happens when provide "bounds" or "constraints" option for scipy minimize

#### Code Sample, a copy-pastable example if possible

```python
~/anaconda3/lib/python3.7/site-packages/statsmodels/base/optimizer.py in _fit_minimize(f, score, start_params, fargs, kwargs, disp, maxiter, callback, retall, full_output, hess)
    273 
    274     res = optimize.minimize(f, start_params, args=fargs, method=kwargs['min_method'],
--> 275                             jac=score, hess=hess, callback=callback, options=options)
    276 
    277     xopt    = res.x

~/anaconda3/lib/python3.7/site-packages/scipy/optimize/_minimize.py in minimize(fun, x0, args, method, jac, hess, hessp, bounds, constraints, tol, callback, options)
    616     elif meth == 'slsqp':
    617         return _minimize_slsqp(fun, x0, args, jac, bounds,
--> 618                                constraints, callback=callback, **options)
    619     elif meth == 'trust-constr':
    620         return _minimize_trustregion_constr(fun, x0, args, jac, hess, hessp,

TypeError: _minimize_slsqp() got multiple values for argument 'constraints'

```
<details>

This happens when a keyword argument is specified that overwrites a positional argument. 
as scipy minimize already has bounds, constraints in the signature, and the options pass the bunds and constraints again. 

It should handled the same way as jac and hess, thank you
</details>


If the issue has not been resolved, please file it in the issue tracker.

#### Expected Output

Run without give error

#### Output of ``import statsmodels.api as sm; sm.show_versions()``

<details>
statsmodels
===========

Installed: 0.11.1 
</details>



Discussion:
Which model?
Can you make up a simple example that we can run?

This was never checked.

aside: standard asymptotic theory for inference doesn't apply if the parameter can be at the boundary of the parameter space, i.e. if there are possibly binding inequality constraints.



according to the docs: [optimizer](https://www.statsmodels.org/stable/optimization.html)
this should affect glm and all other models which can apply scipy optimizer .

The problem is due to the mismatch of the base/optimizer.py and the scipy's minimize function signature. scipy's minimize function has default named args for bounds and constraints.

statsmodels' optimizer put bounds, and constraints into the "options" and then pass to scipy's optimizer "options", but scipy has already has named args in its arg list, so cause the bunds and constraints be passed multiple times, 

i verified by modified the optimizer.py 

get the bounds and constraints from the kwargs and not put them into the options.
optimize.minimize(..., bounds=bounds, constraints=constraints, callback=callback, options=options)

then it will work, 

Thanks for this great package 

Resolving patch:
diff --git a/statsmodels/base/optimizer.py b/statsmodels/base/optimizer.py
--- a/statsmodels/base/optimizer.py
+++ b/statsmodels/base/optimizer.py
@@ -258,7 +258,7 @@ def _fit_minimize(f, score, start_params, fargs, kwargs, disp=True,
     kwargs.setdefault('min_method', 'BFGS')
 
     # prepare options dict for minimize
-    filter_opts = ['extra_fit_funcs', 'niter', 'min_method', 'tol']
+    filter_opts = ['extra_fit_funcs', 'niter', 'min_method', 'tol', 'bounds', 'constraints']
     options = dict((k,v) for k,v in kwargs.items() if k not in filter_opts)
     options['disp']    = disp
     options['maxiter'] = maxiter
@@ -271,8 +271,23 @@ def _fit_minimize(f, score, start_params, fargs, kwargs, disp=True,
     if kwargs['min_method'] in no_jac:
         score = None
 
+    # Use bounds/constraints only if they're allowed by the method
+    has_bounds = ['L-BFGS-B', 'TNC', 'SLSQP', 'trust-constr']
+    has_constraints = ['COBYLA', 'SLSQP' , 'trust-constr']
+
+    if 'bounds' in kwargs.keys() and kwargs['min_method'] in has_bounds:
+        bounds = kwargs['bounds']
+    else:
+        bounds = None
+
+    if 'constraints' in kwargs.keys() and kwargs['min_method'] in has_constraints:
+        constraints = kwargs['constraints']
+    else:
+        constraints = ()
+
     res = optimize.minimize(f, start_params, args=fargs, method=kwargs['min_method'],
-                            jac=score, hess=hess, callback=callback, options=options)
+                            jac=score, hess=hess, bounds=bounds, constraints=constraints,
+                            callback=callback, options=options)
 
     xopt    = res.x
     retvals = None


Test patch:
diff --git a/statsmodels/base/tests/test_optimize.py b/statsmodels/base/tests/test_optimize.py
--- a/statsmodels/base/tests/test_optimize.py
+++ b/statsmodels/base/tests/test_optimize.py
@@ -1,10 +1,12 @@
 import pytest
 from numpy.testing import assert_
+from numpy.testing import assert_almost_equal
 
 from statsmodels.base.optimizer import (_fit_newton, _fit_nm,
                                         _fit_bfgs, _fit_cg,
                                         _fit_ncg, _fit_powell,
-                                        _fit_lbfgs, _fit_basinhopping)
+                                        _fit_lbfgs, _fit_basinhopping,
+                                        _fit_minimize)
 fit_funcs = {
     'newton': _fit_newton,
     'nm': _fit_nm,  # Nelder-Mead
@@ -14,6 +16,7 @@
     'powell': _fit_powell,
     'lbfgs': _fit_lbfgs,
     'basinhopping': _fit_basinhopping,
+    'minimize': _fit_minimize,
 }
 
 
@@ -28,6 +31,18 @@ def dummy_score(x):
 def dummy_hess(x):
     return [[2.]]
 
+def dummy_bounds_constraint_func(x):
+    return (x[0] - 1)**2 + (x[1] - 2.5)**2
+
+
+def dummy_bounds():
+    return ((0, None), (0, None))
+
+def dummy_constraints():
+    cons = ({'type': 'ineq', 'fun': lambda x:  x[0] - 2 * x[1] + 2},
+         {'type': 'ineq', 'fun': lambda x: -x[0] - 2 * x[1] + 6},
+         {'type': 'ineq', 'fun': lambda x: -x[0] + 2 * x[1] + 2})
+    return cons
 
 @pytest.mark.smoke
 def test_full_output_false(reset_randomstate):
@@ -54,7 +69,6 @@ def test_full_output_false(reset_randomstate):
         else:
             assert_(len(xopt) == 1)
 
-
 def test_full_output(reset_randomstate):
     for method in fit_funcs:
         func = fit_funcs[method]
@@ -75,3 +89,10 @@ def test_full_output(reset_randomstate):
             assert_(xopt.shape == () and xopt.size == 1)
         else:
             assert_(len(xopt) == 1)
+
+def test_minimize_scipy_slsqp():
+    func = fit_funcs['minimize']
+    xopt, _ = func(dummy_bounds_constraint_func, None, (2,0), (),
+    {'min_method':'SLSQP','bounds':dummy_bounds(), 'constraints':dummy_constraints()},
+    hess=None, full_output=False, disp=0)
+    assert_almost_equal(xopt, [1.4 ,1.7], 4)