EducationalTestingService / factor_analyzer

A Python module to perform exploratory & confirmatory factor analyses.
GNU General Public License v2.0
235 stars 68 forks source link

Fix to scipy.minimize changes #124

Closed ikeuchi-screen closed 1 year ago

ikeuchi-screen commented 1 year ago

Motivation

Fix for changes in scipy-1.11.0

The scipy.optimize.minimize function was changed in scipy-1.11.0.

SciPy 1.11.0 Release Notes https://scipy.github.io/devdocs/release/1.11.0-notes.html#expired-deprecations

The scipy.optimize.minimize function now raises an error for x0 with x0.ndim > 1.

How to reproduce the error

Executing the following code will result in an error.

Confirmatory factor analysis example.

import pandas as pd
from factor_analyzer import (ConfirmatoryFactorAnalyzer, ModelSpecificationParser)

df_features = pd.read_csv('tests/data/test11.csv')
model_dict = {"F1": ["V1", "V2", "V3", "V4"], "F2": ["V5", "V6", "V7", "V8"]}
model_spec = ModelSpecificationParser.parse_model_specification_from_dict(df_features, model_dict)
cfa = ConfirmatoryFactorAnalyzer(model_spec, disp=False)
cfa.fit(df_features.values)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [1], line 8
      6 model_spec = ModelSpecificationParser.parse_model_specification_from_dict(df_features, model_dict)
      7 cfa = ConfirmatoryFactorAnalyzer(model_spec, disp=False)
----> 8 cfa.fit(df_features.values)

File ~\factor_analyzer\confirmatory_factor_analyzer.py:742, in ConfirmatoryFactorAnalyzer.fit(self, X, y)
    737     assert len(self.bounds) == len(x0), error_msg
    739 # fit the actual model using L-BFGS algorithm;
    740 # the constraints are set inside the objective function,
    741 # so that we can avoid using linear programming methods (e.g. SLSQP)
--> 742 res = minimize(
    743     self._objective,
    744     x0,
    745     method="L-BFGS-B",
    746     options={"maxiter": self.max_iter, "disp": self.disp},
    747     bounds=self.bounds,
    748     args=(cov_mtx, self.model.loadings),
    749 )
    751 # if the optimizer failed to converge, print the message
    752 if not res.success:

File ~\AppData\Roaming\Python\Python39\site-packages\scipy\optimize\_minimize.py:533, in minimize(fun, x0, args, method, jac, hess, hessp, bounds, constraints, tol, callback, options)
    530 x0 = np.atleast_1d(np.asarray(x0))
    532 if x0.ndim != 1:
--> 533     raise ValueError("'x0' must only have one dimension.")
    535 if x0.dtype.kind in np.typecodes["AllInteger"]:
    536     x0 = np.asarray(x0, dtype=float)

ValueError: 'x0' must only have one dimension.

Expected Behavior

https://github.com/EducationalTestingService/factor_analyzer#examples

Description of the changes

The error can be avoided by setting x0 in the minimize function to 1d-vector.

res = minimize(
    self._objective,
    x0.flatten(),
    method="L-BFGS-B",
    options={"maxiter": self.max_iter, "disp": self.disp},
    bounds=self.bounds,
    args=(cov_mtx, self.model.loadings),
)
desilinguist commented 1 year ago

@ikeuchi-screen I think I know why the code coverage is not being computed. I am going to use this PR to also fix the coverage issue in a bit. Thanks for your patience!