Skip to content

MRG : add support for sample_weights in StandardScaler when X is sparse - #18682

Merged
glemaitre merged 11 commits into
scikit-learn:masterfrom
agramfort:std_scaler_sw_sparse
Oct 28, 2020
Merged

glemaitre merged 11 commits into
scikit-learn:masterfrom
agramfort:std_scaler_sw_sparse

Conversation

@agramfort

@agramfort agramfort commented Oct 25, 2020 •

Copy link
Copy Markdown
Member

cc @maikia
Towards #15601.

@agramfort agramfort changed the title WIP : add support for sample_weights in StandardScaler when X is sparse MRG : add support for sample_weights in StandardScaler when X is sparse Oct 25, 2020

@glemaitre glemaitre 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 a quick partial review. I need more time to have a better look.

Comment thread sklearn/preprocessing/_data.py
Comment thread sklearn/preprocessing/_data.py Outdated
Comment thread sklearn/preprocessing/_data.py Outdated
Comment thread sklearn/preprocessing/_data.py Outdated
Comment thread sklearn/utils/sparsefuncs.py Outdated
Comment thread sklearn/utils/sparsefuncs_fast.pyx Outdated
Comment thread sklearn/utils/sparsefuncs_fast.pyx Outdated
Comment thread sklearn/utils/sparsefuncs_fast.pyx Outdated
Comment thread sklearn/utils/sparsefuncs_fast.pyx Outdated
Comment thread sklearn/utils/sparsefuncs_fast.pyx Outdated

@ogrisel ogrisel 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.

Besides @glemaitre review and the comments below, this LGTM.

I think we should also add a test that checks that the results of fitting a scaler on sparse data with missing values and sample weights is the same as fitting a scaler with a dense version of this data. I don't think the current tests cover this comparison.

Comment thread sklearn/preprocessing/tests/test_data.py Outdated
Comment thread sklearn/preprocessing/tests/test_data.py Outdated
Comment thread sklearn/preprocessing/tests/test_data.py
Comment thread sklearn/utils/sparsefuncs.py
Comment thread sklearn/utils/sparsefuncs.py
Comment thread sklearn/utils/sparsefuncs_fast.pyx Outdated

@maikia maikia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM thanks @agramfort

Comment thread sklearn/preprocessing/_data.py Outdated
The number of samples processed by the estimator for each feature.
If there are not missing samples, the ``n_samples_seen`` will be an
integer, otherwise it will be an array.
integer, otherwise it will be an array. If sample_weights are

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
integer, otherwise it will be an array. If sample_weights are
integer, otherwise it will be an array of dtype=int. If sample_weights are

Comment thread sklearn/preprocessing/_data.py Outdated
If there are not missing samples, the ``n_samples_seen`` will be an
integer, otherwise it will be an array.
integer, otherwise it will be an array. If sample_weights are
used it will be an array that sums the weights seen so far.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
used it will be an array that sums the weights seen so far.
used it will be an array of dtype=float that sums the weights seen so far.

@maikia maikia left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would also update the dtype test (test_standard_scaler_dtype) to include sparse in test_data.py (it revealed some problems when we did it for dense).
This could work:

@pytest.mark.parametrize("sparse_constructor",
                         [None, sparse.csc_matrix, sparse.csr_matrix])
@pytest.mark.parametrize("add_sample_weight", [False, True])
def test_standard_scaler_dtype(add_sample_weight, sparse_constructor):
    # Ensure scaling does not affect dtype
    rng = np.random.RandomState(0)
    n_samples = 10
    n_features = 3
    if add_sample_weight:
        sample_weight = np.ones(n_samples)
    else:
        sample_weight = None
    for dtype in [np.float16, np.float32, np.float64]:
        with_mean = True
        X = rng.randn(n_samples, n_features).astype(dtype)
        if sparse_constructor is not None:
            X = sparse_constructor(X)
            with_mean = False

        scaler = StandardScaler(with_mean=with_mean)
        X_scaled = scaler.fit(X, sample_weight=sample_weight).transform(X)
        assert X.dtype == X_scaled.dtype
        assert scaler.mean_.dtype == np.float64
        assert scaler.scale_.dtype == np.float64

@agramfort

Copy link
Copy Markdown
Member Author

@maikia @ogrisel @glemaitre I think I addressed all comments here.

@ogrisel ogrisel 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.

LGTM (again :)

@glemaitre
glemaitre self-requested a review October 27, 2020 16:50

@glemaitre glemaitre 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.

Otherwise LGTM

Comment thread sklearn/preprocessing/_data.py Outdated
integer, otherwise it will be an array.
If there are no missing samples, the ``n_samples_seen`` will be an
integer, otherwise it will be an array of dtype int. If
sample_weights are used it will be a float (if no missing data)

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
sample_weights are used it will be a float (if no missing data)
`sample_weights` are used it will be a float (if no missing data)

Comment thread sklearn/preprocessing/_data.py Outdated
X = self._validate_data(X, accept_sparse=('csr', 'csc'),
estimator=self, dtype=FLOAT_DTYPES,
force_all_finite='allow-nan', reset=first_call)
n_samples, n_features = X.shape

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
n_samples, n_features = X.shape
n_features = X.shape[1]

Comment thread sklearn/preprocessing/_data.py Outdated
Comment on lines +782 to +783
elif (hasattr(self, 'n_samples_seen_') and
np.size(self.n_samples_seen_) == 1):

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
elif (hasattr(self, 'n_samples_seen_') and
np.size(self.n_samples_seen_) == 1):
elif np.size(self.n_samples_seen_) == 1:

Comment on lines +322 to +334
@pytest.mark.parametrize("sparse_constructor",
[None, sparse.csc_matrix, sparse.csr_matrix])
def test_standard_scaler_sample_weight(
Xw, X, sample_weight, sparse_constructor):

with_mean = True
if sparse_constructor is not None:
X = sparse_constructor(X)
Xw = sparse_constructor(Xw)
with_mean = False
else:
Xw = np.array(Xw)
X = np.array(X)

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
@pytest.mark.parametrize("sparse_constructor",
[None, sparse.csc_matrix, sparse.csr_matrix])
def test_standard_scaler_sample_weight(
Xw, X, sample_weight, sparse_constructor):
with_mean = True
if sparse_constructor is not None:
X = sparse_constructor(X)
Xw = sparse_constructor(Xw)
with_mean = False
else:
Xw = np.array(Xw)
X = np.array(X)
@pytest.mark.parametrize(
"array_constructor", ["array", "sparse_csr", "sparse_csc"]
)
def test_standard_scaler_sample_weight(
Xw, X, sample_weight, array_constructor):
with_mean = not array_constructor.startswith("sparse")
X, = _convert_container(X, array_constructor)
Xw = _convert_container(Xw, array_constructor)

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 need to add the following line sklearn.utils._testing._convert_container:

    elif constructor_name == 'sparse_csr':
        return sp.sparse.csr_matrix(container)
    elif constructor_name == 'sparse_csc':
        return sp.sparse.csc_matrix(container)

Axis along which the axis should be computed.

weights : ndarray of shape (n_samples,) or (n_features,), default=None
if axis is set to 0 shape is (n_samples,) or

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
if axis is set to 0 shape is (n_samples,) or
If axis is set to 0 shape is (n_samples,) or

Comment thread sklearn/utils/sparsefuncs.py Outdated
variances : float array with shape (n_features,)
Feature-wise variances

sum_weights : float array with shape (n_features,)

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
sum_weights : float array with shape (n_features,)
sum_weights : ndarray of shape (n_features,), dtype=floating

Comment thread sklearn/utils/sparsefuncs.py Outdated
Feature-wise variances

sum_weights : float array with shape (n_features,)
Returned if return_sum_weights is True.

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
Returned if return_sum_weights is True.
Returned if `return_sum_weights` is `True`.

Comment thread sklearn/utils/sparsefuncs.py Outdated

return_sum_weights : bool, default=False
If True, returns the sum of weights seen for each feature
if axis=0 or each sample if axis=1.

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
if axis=0 or each sample if axis=1.
if `axis=0` or each sample if `axis=1`.

@glemaitre
glemaitre merged commit 5263eea into scikit-learn:master Oct 28, 2020
@glemaitre

Copy link
Copy Markdown
Member

Thanks @agramfort @maikia

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.

4 participants