Skip to content

[MRG] varimax rotation for factor analysis - #11064

Merged
agramfort merged 25 commits into
scikit-learn:masterfrom
jona-sassenhagen:varimax
Jul 5, 2020
Merged

agramfort merged 25 commits into
scikit-learn:masterfrom
jona-sassenhagen:varimax

Conversation

@jona-sassenhagen

@jona-sassenhagen jona-sassenhagen commented May 3, 2018 •

Copy link
Copy Markdown
Contributor

Reference Issues/PRs

closes #2688

What does this implement/fix? Explain your changes.

As discussed in #2688, sklearn's Factor Analysis does not support rotations. This makes it essentially useless for e.g. psychologists.

This PR implements the varimax and quartimax rotations.

@jona-sassenhagen jona-sassenhagen changed the title WIP varimax rotation for factor analysis [WIP] varimax rotation for factor analysis May 3, 2018
Comment thread examples/decomposition/plot_varimax_fa.py
@sklearn-lgtm

Copy link
Copy Markdown

This pull request introduces 1 alert when merging 99991b7 into 593bf65 - view on lgtm.com

new alerts:

  • 1 for Syntax error

Comment posted by lgtm.com

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

Well, it's green ...

@jnothman

@jnothman jnothman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just looking at the basics, thanks


rotation : None | 'varimax' | 'orthomax'
If not None, apply the indicated rotation. Currently, varimax and
orthomax are implemented.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a reference or some discussion in the user guide? At least there should be a link to the example from the user guide

Comment thread sklearn/decomposition/tests/test_factor_analysis.py
# test rotation
n_comps = 2
for method in ("varimax", 'quartimax'):
fa_var = FactorAnalysis(n_components=n_comps, rotation=method)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please assert that the solution varies with the parameter

@jona-sassenhagen

jona-sassenhagen commented May 6, 2018 •

Copy link
Copy Markdown
Contributor Author

CIs on 9f582f0 can all be canceled, I pushed quite preemptively.

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

@jnothman better?

fa_var = FactorAnalysis(n_components=n_comps, rotation=method)
results.append(fa_var.fit_transform(X))
for a, b in ((0, 1), (0, 2), (1, 2)):
assert_raises(AssertionError, assert_array_almost_equal,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think assert not np.allclose would be clearer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ... only here, or also in 112? 84 also uses assert_array_almost_equal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use assert_allclose in those cases, but here you are raising an error in the case that the assertion fails, which is especially difficult to read.

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

@jnothman now? :)

@jona-sassenhagen jona-sassenhagen changed the title [WIP] varimax rotation for factor analysis [MRG] varimax rotation for factor analysis Jun 2, 2018
@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

Anything else? It seems ready to me.

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

@jnothman can I bother you about this one again? :)

@jnothman

jnothman commented Jun 7, 2018 via email

Copy link
Copy Markdown
Member

fa_var = FactorAnalysis(n_components=n_comps, rotation=method)
results.append(fa_var.fit_transform(X))
for a, b in ((0, 1), (0, 2), (1, 2)):
assert_raises(AssertionError, assert_array_almost_equal,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use assert_allclose in those cases, but here you are raising an error in the case that the assertion fails, which is especially difficult to read.

Comment thread doc/modules/decomposition.rst Outdated
:align: center
:scale: 75%

Factor Analysis is often followed by a rotation of the factors, usually to improve

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note the parameter name to control this

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

To be honest, I didn't sit down with the original paper for this one, I think I looked at a few implementations online and just made sure it matched the R output ... E.g.,

https://en.wikipedia.org/wiki/Talk:Varimax_rotation

@jnothman

jnothman commented Jul 29, 2018 via email

Copy link
Copy Markdown
Member

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

Ok, sure. Thanks.

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

Is now the time? :)

@jnothman jnothman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide references somewhere in the documentation (user guide and/or docstring)


for _ in range(itermax):
comp_rot = np.dot(components, rotation_matrix)
tmp = np.diag((comp_rot ** 2).sum(axis=0)) / nrow * gamma

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do all this computation unnecessarily in the quartimax case?

Can't we just use if here?

def _ortho_rotation(self, components, method='varimax', eps=1e-6,
itermax=100):
"""Return rotated components."""
if (method == 'varimax'):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parentheses should be removed here and below.

Can just use gamma = method == 'varimax', even.

ConvergenceWarning)

self.components_ = W
if self.rotation is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well do this check inside the method

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, replace the previous line to self.components_ = self._rotate(W). I think that would be much clearer to the reader.

"Rotate the factor analysis solution."
implemented = {"varimax", "quartimax"}
if method in implemented:
self.components_ = self._ortho_rotation(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems strange to put this in a separate function. Instead:

if method is None:
    return
if method not in implemented:
    raise ...
do the rotation
self.components_ = ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it simpler to test, you could make this a module-level private function or a staticmethod, taking components_ in and returning it optionally modified.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sure. I'd for now keep it as a separate function to make it easier to refactor if somebody decides to add oblique or other rotations ..?

np.dot(components.T, comp_rot ** 3 - np.dot(comp_rot, tmp)))
rotation_matrix = np.dot(u, v)
var_new = np.sum(s)
if var != 0 and (var_new < var * (1 + eps)):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parentheses around < expression are redundant

np.eye(X.shape[1]), 12)

# test rotation
n_comps = 2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call this n_components, please

self.components_ = self._ortho_rotation(
self.components_.T, method=method, eps=tol)[:n_components]
else:
raise NotImplementedError("'method' must be in %s, not %s"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we would use a ValueError. Subtle, yes, but NotImplementedError is usually for a function call with argument types where the operation is not yet supported, for instance.

for method in (None, "varimax", 'quartimax'):
fa_var = FactorAnalysis(n_components=n_comps, rotation=method)
results.append(fa_var.fit_transform(X))
for a, b in ((0, 1), (0, 2), (1, 2)):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make the outer collection a list to improve readability [(0, 1), (0, 2) ...]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it would be much more readable to use itertools.combinations([None, 'varimax', 'quartimax'], 2) and have stored the results by name

Comment thread sklearn/decomposition/tests/test_factor_analysis.py
Comment thread sklearn/decomposition/tests/test_factor_analysis.py

@jnothman jnothman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix up these changes and hopefully we can get another reviewer in towards merging this.


print(__doc__)

# #############################################################################

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# #############################################################################
##############################################################################

ax.set_title("Iris feature correlation matrix")
plt.tight_layout()

# #############################################################################

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# #############################################################################
##############################################################################

Varimax PR: address review comments

@jnothman jnothman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing those up


print(__doc__)

##############################################################################

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are now using the following for section dividers:

Suggested change
##############################################################################
# %%

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@jnothman jnothman closed this May 7, 2020
@jnothman jnothman reopened this May 7, 2020
@ericpre

ericpre commented Jun 10, 2020

Copy link
Copy Markdown
Contributor

@amueller and @agramfort, are you happy with this PR?

@agramfort agramfort left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a versionadded added to docstring and update to what's new page. Besides LGTM !

`"The varimax criterion for analytic rotation in factor analysis"
<https://link.springer.com/article/10.1007%2FBF02289233>`_
H. F. Kaiser, 1958

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs versionadded

@jnothman

jnothman commented Jul 5, 2020

Copy link
Copy Markdown
Member

@agramfort was that a formal approval?

@agramfort agramfort left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for MRG provided CIs turn green. thx @jona-sassenhagen

@ericpre

ericpre commented Jul 5, 2020

Copy link
Copy Markdown
Contributor

The CI failure seems to be related to setting up the environment, would updating the branch with upstream fix it? Last time, this branch was merged with upstream was 12 days ago.

@thomasjpfan

Copy link
Copy Markdown
Member

The CI failure seems to be related to setting up the environment, would updating the branch with upstream fix it?

Yes merging with master should fix it. I have done to merge to see if it works.

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

We're green!

@agramfort
agramfort merged commit 5e193ec into scikit-learn:master Jul 5, 2020
@agramfort

Copy link
Copy Markdown
Member

Thx @jona-sassenhagen

@jnothman

jnothman commented Jul 6, 2020 via email

Copy link
Copy Markdown
Member

@jona-sassenhagen

Copy link
Copy Markdown
Contributor Author

Thanks everyone.

@jona-sassenhagen
jona-sassenhagen deleted the varimax branch July 12, 2020 10:57
glemaitre pushed a commit to glemaitre/scikit-learn that referenced this pull request Jul 17, 2020
* init varimax

* fix py2 and add example text

* fix print again

* test, docs

* fix test

* fix doctstring

* np.allclose for test

* add refrence

* address comments

* update

* Fix linting failure.

* Fix deprecation warning.

* a bit of cleanup

* Address @jnothman's review comments.

* Update section divider

* Add note in what's new page and versionadded to docstring.

Co-authored-by: Eric Prestat <[email protected]>
Co-authored-by: Thomas J. Fan <[email protected]>
jayzed82 pushed a commit to jayzed82/scikit-learn that referenced this pull request Oct 22, 2020
* init varimax

* fix py2 and add example text

* fix print again

* test, docs

* fix test

* fix doctstring

* np.allclose for test

* add refrence

* address comments

* update

* Fix linting failure.

* Fix deprecation warning.

* a bit of cleanup

* Address @jnothman's review comments.

* Update section divider

* Add note in what's new page and versionadded to docstring.

Co-authored-by: Eric Prestat <[email protected]>
Co-authored-by: Thomas J. Fan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Varimax rotation for Factor Analysis and PCA

9 participants