Repository: statsmodels/statsmodels

Issue:
BUG: Remove reset of seeding in the ML EM `fit` method of `Factor`
- [x] closes #7357
- [x] tests added / passed. 
- [x] code/documentation is well formatted.  
- [x] properly formatted commit message. See 
      [NumPy's guide](https://docs.scipy.org/doc/numpy-1.15.1/dev/gitwash/development_workflow.html#writing-the-commit-message). 

I removed the line of code in the `_fit_ml_em` method of `multivariate.Factor` that resets the RNG seed. Doing so broke 2 tests cases, but these were only broken due to precision error. These are fixed by simply increasing the number of iterations used to estimate the models in the test cases.


<details>


**Notes**:

* It is essential that you add a test when making code changes. Tests are not 
  needed for doc changes.
* When adding a new function, test values should usually be verified in another package (e.g., R/SAS/Stata).
* When fixing a bug, you must add a test that would produce the bug in master and
  then show that it is fixed with the new code.
* New code additions must be well formatted. Changes should pass flake8. If on Linux or OSX, you can
  verify you changes are well formatted by running 
  ```
  git diff upstream/master -u -- "*.py" | flake8 --diff --isolated
  ```
  assuming `flake8` is installed. This command is also available on Windows 
  using the Windows System for Linux once `flake8` is installed in the 
  local Linux environment. While passing this test is not required, it is good practice and it help 
  improve code quality in `statsmodels`.
* Docstring additions must render correctly, including escapes and LaTeX.

</details>



Discussion:
I think a more deterministic fix is something like

```
rs = np.random.RadnomState(1234)
rs.normal(...)
```

which should be identical to the current behaviour without touching the singleton state.  A better optional lets a user pass a RandomState which is passed to fit em when not None, and if None the code above is used.

Resolving patch:
diff --git a/statsmodels/multivariate/factor.py b/statsmodels/multivariate/factor.py
--- a/statsmodels/multivariate/factor.py
+++ b/statsmodels/multivariate/factor.py
@@ -433,12 +433,13 @@ def nscore(par):
 
         return FactorResults(self)
 
-    def _fit_ml_em(self, iter):
+    def _fit_ml_em(self, iter, random_state=None):
         """estimate Factor model using EM algorithm
         """
         # Starting values
-        np.random.seed(3427)
-        load = 0.1*np.random.normal(size=(self.k_endog, self.n_factor))
+        if random_state is None:
+            random_state = np.random.RandomState(3427)
+        load = 0.1 * random_state.standard_normal(size=(self.k_endog, self.n_factor))
         uniq = 0.5 * np.ones(self.k_endog)
 
         for k in range(iter):


Test patch:
diff --git a/statsmodels/multivariate/tests/test_ml_factor.py b/statsmodels/multivariate/tests/test_ml_factor.py
--- a/statsmodels/multivariate/tests/test_ml_factor.py
+++ b/statsmodels/multivariate/tests/test_ml_factor.py
@@ -2,7 +2,7 @@
 from statsmodels.multivariate.factor import Factor
 from numpy.testing import assert_allclose, assert_equal
 from scipy.optimize import approx_fprime
-
+import warnings
 
 # A small model for basic testing
 def _toy():
@@ -78,12 +78,29 @@ def test_exact_em():
             s = np.sqrt(np.diag(c))
             c /= np.outer(s, s)
             fa = Factor(corr=c, n_factor=n_factor, method='ml')
-            load_e, uniq_e = fa._fit_ml_em(200)
+            load_e, uniq_e = fa._fit_ml_em(2000)
             c_e = np.dot(load_e, load_e.T)
             c_e.flat[::c_e.shape[0]+1] += uniq_e
             assert_allclose(c_e, c, rtol=1e-4, atol=1e-4)
 
 
+def test_fit_ml_em_random_state():
+    # Ensure Factor._fit_ml_em doesn't change numpy's singleton random state
+    # see #7357
+
+    T = 10
+    epsilon = np.random.multivariate_normal(np.zeros(3), np.eye(3), size=T).T
+    initial = np.random.get_state()
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", message='Fitting did not converge')
+        Factor(endog=epsilon, n_factor=2, method='ml').fit()
+    final = np.random.get_state()
+
+    assert(initial[0] == final[0])
+    assert_equal(initial[1], final[1])
+    assert(initial[2:] == final[2:])
+
+
 def test_em():
 
     n_factor = 1