From ce53c81ee3c8b5545e7ec17125634d2082ae62ba Mon Sep 17 00:00:00 2001 From: Karan Desai Date: Fri, 10 Mar 2017 02:33:08 +0530 Subject: [PATCH 01/64] ENH Add verbose option and corresponding tests for Pipeline. --- doc/modules/pipeline.rst | 13 ++-- sklearn/pipeline.py | 131 ++++++++++++++++++++++++++++----- sklearn/tests/test_pipeline.py | 58 ++++++++++++++- 3 files changed, 177 insertions(+), 25 deletions(-) diff --git a/doc/modules/pipeline.rst b/doc/modules/pipeline.rst index 232b3ed72bbda..425fe6ed66dc0 100644 --- a/doc/modules/pipeline.rst +++ b/doc/modules/pipeline.rst @@ -47,7 +47,7 @@ is an estimator object:: >>> pipe # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS Pipeline(memory=None, steps=[('reduce_dim', PCA(copy=True,...)), - ('clf', SVC(C=1.0,...))]) + ('clf', SVC(C=1.0,...))], verbose=False) The utility function :func:`make_pipeline` is a shorthand for constructing pipelines; @@ -62,7 +62,7 @@ filling in the names automatically:: steps=[('binarizer', Binarizer(copy=True, threshold=0.0)), ('multinomialnb', MultinomialNB(alpha=1.0, class_prior=None, - fit_prior=True))]) + fit_prior=True))], verbose=False) The estimators of a pipeline are stored as a list in the ``steps`` attribute:: @@ -82,7 +82,8 @@ Parameters of the estimators in the pipeline can be accessed using the >>> pipe.set_params(clf__C=10) # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS Pipeline(memory=None, steps=[('reduce_dim', PCA(copy=True, iterated_power='auto',...)), - ('clf', SVC(C=10, cache_size=200, class_weight=None,...))]) + ('clf', SVC(C=10, cache_size=200, class_weight=None,...))], + verbose=False) Attributes of named_steps map to keys, enabling tab completion in interactive environments:: @@ -160,7 +161,7 @@ object:: >>> pipe # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS Pipeline(..., steps=[('reduce_dim', PCA(copy=True,...)), - ('clf', SVC(C=1.0,...))]) + ('clf', SVC(C=1.0,...))], verbose=False) >>> # Clear the cache directory when you don't need it anymore >>> rmtree(cachedir) @@ -177,7 +178,7 @@ object:: >>> pipe.fit(digits.data, digits.target) ... # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS Pipeline(memory=None, - steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))]) + steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))], verbose=False) >>> # The pca instance can be inspected directly >>> print(pca1.components_) # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS [[ -1.77484909e-19 ... 4.07058917e-18]] @@ -199,7 +200,7 @@ object:: >>> cached_pipe.fit(digits.data, digits.target) ... # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS Pipeline(memory=..., - steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))]) + steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))], verbose=False) >>> print(cached_pipe.named_steps['reduce_dim'].components_) ... # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS [[ -1.77484909e-19 ... 4.07058917e-18]] diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 66da9dffeb066..9cda48aa081eb 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -10,6 +10,8 @@ # License: BSD from collections import defaultdict +from abc import ABCMeta, abstractmethod +import time import numpy as np from scipy import sparse @@ -17,16 +19,73 @@ from .base import clone, TransformerMixin from .externals.joblib import Parallel, delayed from .externals import six -from .utils.metaestimators import if_delegate_has_method +from .utils.metaestimators import if_delegate_has_method, _BaseComposition from .utils import Bunch from .utils.validation import check_memory -from .utils.metaestimators import _BaseComposition __all__ = ['Pipeline', 'FeatureUnion'] -class Pipeline(_BaseComposition): +class _BasePipeline(six.with_metaclass(ABCMeta, _BaseComposition)): + """Handles parameter management for classifiers composed of named steps. + """ + + @abstractmethod + def __init__(self): + pass + + def _replace_step(self, steps_attr, name, new_val): + # assumes `name` is a valid step name + new_steps = getattr(self, steps_attr)[:] + for i, (step_name, _) in enumerate(new_steps): + if step_name == name: + new_steps[i] = (name, new_val) + break + setattr(self, steps_attr, new_steps) + + def _get_params(self, steps_attr, deep=True): + out = super(_BasePipeline, self).get_params(deep=False) + if not deep: + return out + steps = getattr(self, steps_attr) + out.update(steps) + for name, estimator in steps: + if estimator is None: + continue + for key, value in six.iteritems(estimator.get_params(deep=True)): + out['%s__%s' % (name, key)] = value + return out + + def _set_params(self, steps_attr, **params): + # Ensure strict ordering of parameter setting: + # 1. All steps + if steps_attr in params: + setattr(self, steps_attr, params.pop(steps_attr)) + # 2. Step replacement + step_names, _ = zip(*getattr(self, steps_attr)) + for name in list(six.iterkeys(params)): + if '__' not in name and name in step_names: + self._replace_step(steps_attr, name, params.pop(name)) + # 3. Step parameters and other initilisation arguments + super(_BasePipeline, self).set_params(**params) + return self + + def _validate_names(self, names): + if len(set(names)) != len(names): + raise ValueError('Names provided are not unique: ' + '{0!r}'.format(list(names))) + invalid_names = set(names).intersection(self.get_params(deep=False)) + if invalid_names: + raise ValueError('Step names conflict with constructor arguments: ' + '{0!r}'.format(sorted(invalid_names))) + invalid_names = [name for name in names if '__' in name] + if invalid_names: + raise ValueError('Step names must not contain __: got ' + '{0!r}'.format(invalid_names)) + + +class Pipeline(_BasePipeline): """Pipeline of transforms with a final estimator. Sequentially apply a list of transforms and a final estimator. @@ -62,6 +121,9 @@ class Pipeline(_BaseComposition): inspect estimators within the pipeline. Caching the transformers is advantageous when fitting is time consuming. + verbose : boolean, optional + Verbosity mode. + Attributes ---------- named_steps : bunch object, a dictionary with attribute access @@ -89,7 +151,7 @@ class Pipeline(_BaseComposition): ... # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE Pipeline(memory=None, steps=[('anova', SelectKBest(...)), - ('svc', SVC(...))]) + ('svc', SVC(...))], verbose=False) >>> prediction = anova_svm.predict(X) >>> anova_svm.score(X, y) # doctest: +ELLIPSIS 0.829... @@ -109,11 +171,12 @@ class Pipeline(_BaseComposition): # BaseEstimator interface - def __init__(self, steps, memory=None): + def __init__(self, steps, memory=None, verbose=False): # shallow copy of steps self.steps = list(steps) self._validate_steps() self.memory = memory + self.verbose = verbose def get_params(self, deep=True): """Get parameters for this estimator. @@ -168,6 +231,14 @@ def _validate_steps(self): "'%s' (type %s) doesn't" % (estimator, type(estimator))) + def _print_final_step(self, start_time, time_elapsed_so_far): + time_elapsed = time.time() - start_time + time_elapsed_so_far += time_elapsed + print('[Pipeline] (step %d of %d) %s ... %.5fs' % + (len(self.steps), len(self.steps), self.steps[-1][0], + time_elapsed_so_far)) + print('[Pipeline] Total time elapsed: %.5fs' % time_elapsed_so_far) + @property def _estimator_type(self): return self.steps[-1][1]._estimator_type @@ -196,7 +267,10 @@ def _fit(self, X, y=None, **fit_params): step, param = pname.split('__', 1) fit_params_steps[step][param] = pval Xt = X + # Keep a record of time elapsed + time_elapsed_so_far = 0 for step_idx, (name, transformer) in enumerate(self.steps[:-1]): + step_start_time = time.time() if transformer is None: pass else: @@ -214,9 +288,16 @@ def _fit(self, X, y=None, **fit_params): # transformer. This is necessary when loading the transformer # from the cache. self.steps[step_idx] = (name, fitted_transformer) + + step_time_elapsed = time.time() - step_start_time + time_elapsed_so_far += step_time_elapsed + # Logging time elapsed for current step to stdout + if self.verbose: + print('[Pipeline] (step %d of %d) %s ... %.5fs' % + (step_idx + 1, len(self.steps), name, step_time_elapsed)) if self._final_estimator is None: - return Xt, {} - return Xt, fit_params_steps[self.steps[-1][0]] + return Xt, {}, time_elapsed_so_far + return Xt, fit_params_steps[self.steps[-1][0]], time_elapsed_so_far def fit(self, X, y=None, **fit_params): """Fit the model @@ -244,9 +325,12 @@ def fit(self, X, y=None, **fit_params): self : Pipeline This estimator """ - Xt, fit_params = self._fit(X, y, **fit_params) + Xt, fit_params, time_elapsed_so_far = self._fit(X, y, **fit_params) + final_step_start_time = time.time() if self._final_estimator is not None: self._final_estimator.fit(Xt, y, **fit_params) + if self.verbose: + self._print_final_step(final_step_start_time, time_elapsed_so_far) return self def fit_transform(self, X, y=None, **fit_params): @@ -277,13 +361,21 @@ def fit_transform(self, X, y=None, **fit_params): Transformed samples """ last_step = self._final_estimator - Xt, fit_params = self._fit(X, y, **fit_params) - if hasattr(last_step, 'fit_transform'): - return last_step.fit_transform(Xt, y, **fit_params) - elif last_step is None: + Xt, fit_params, time_elapsed_so_far = self._fit(X, y, **fit_params) + final_step_start_time = time.time() + if last_step is None: + if self.verbose: + print('[Pipeline] Step %s is NoneType.' % self.steps[-1][0]) + print('[Pipeline] Total time elapsed: %.3fs' % + time_elapsed_so_far) return Xt + elif hasattr(last_step, 'fit_transform'): + Xt = last_step.fit_transform(Xt, y, **fit_params) else: - return last_step.fit(Xt, y, **fit_params).transform(Xt) + Xt = last_step.fit(Xt, y, **fit_params).transform(Xt) + if self.verbose: + self._print_final_step(final_step_start_time, time_elapsed_so_far) + return Xt @if_delegate_has_method(delegate='_final_estimator') def predict(self, X): @@ -332,8 +424,12 @@ def fit_predict(self, X, y=None, **fit_params): ------- y_pred : array-like """ - Xt, fit_params = self._fit(X, y, **fit_params) - return self.steps[-1][-1].fit_predict(Xt, y, **fit_params) + Xt, fit_params, time_elapsed_so_far = self._fit(X, y, **fit_params) + final_step_start_time = time.time() + y_pred = self.steps[-1][-1].fit_predict(Xt, y, **fit_params) + if self.verbose: + self._print_final_step(final_step_start_time, time_elapsed_so_far) + return y_pred @if_delegate_has_method(delegate='_final_estimator') def predict_proba(self, X): @@ -547,7 +643,7 @@ def make_pipeline(*steps, **kwargs): Pipeline(memory=None, steps=[('standardscaler', StandardScaler(copy=True, with_mean=True, with_std=True)), - ('gaussiannb', GaussianNB(priors=None))]) + ('gaussiannb', GaussianNB(priors=None))], verbose=False) Returns ------- @@ -584,7 +680,7 @@ def _fit_transform_one(transformer, weight, X, y, return res * weight, transformer -class FeatureUnion(_BaseComposition, TransformerMixin): +class FeatureUnion(_BasePipeline, TransformerMixin): """Concatenates results of multiple transformer objects. This estimator applies a list of transformer objects in parallel to the @@ -612,6 +708,7 @@ class FeatureUnion(_BaseComposition, TransformerMixin): Keys are transformer names, values the weights. """ + def __init__(self, transformer_list, n_jobs=1, transformer_weights=None): self.transformer_list = list(transformer_list) self.n_jobs = n_jobs diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 1165370885d36..ed54482cd26c8 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -67,12 +67,12 @@ def set_params(self, **params): class NoInvTransf(NoTrans): - def transform(self, X): + def transform(self, X, y=None): return X class Transf(NoInvTransf): - def transform(self, X): + def transform(self, X, y=None): return X def inverse_transform(self, X): @@ -573,6 +573,7 @@ def make(): 'memory': None, 'm2__mult': 2, 'last__mult': 5, + 'verbose': False }) pipeline.set_params(m2=None) @@ -968,3 +969,56 @@ def test_make_pipeline_memory(): assert_true(pipeline.memory is None) shutil.rmtree(cachedir) + +def check_pipeline_verbosity_fit_predict(pipe_method): + # Test that the verbosity of pipeline is proper + from sklearn.externals.six.moves import cStringIO as StringIO + import sys + old_stdout = sys.stdout + sys.stdout = StringIO() + pipe_method(X=None, y=None, clf__should_succeed=True) + verbose_output = sys.stdout + sys.stdout = old_stdout + + # check output + verbose_output.seek(0) + lines = verbose_output.readlines() + assert_true('[Pipeline] (step 1 of 2) transf ... ' in lines[0]) + assert_true('[Pipeline] (step 2 of 2) clf ... ' in lines[1]) + assert_true('[Pipeline] Total time elapsed: ' in lines[2]) + + +def test_pipeline_fit_verbosity(): + pipe = Pipeline([('transf', Transf()), ('clf', FitParamT())], verbose=True) + yield check_pipeline_verbosity_fit_predict, pipe.fit + yield check_pipeline_verbosity_fit_predict, pipe.fit_predict + + +def check_pipeline_verbosity_fit_transform(pipe_method, last_was_none=False): + # Test that the verbosity of pipeline is proper + from sklearn.externals.six.moves import cStringIO as StringIO + import sys + old_stdout = sys.stdout + sys.stdout = StringIO() + pipe_method(X=[[1, 2, 3], [4, 5, 6]], y=[[7], [8]]) + verbose_output = sys.stdout + sys.stdout = old_stdout + + # check output + verbose_output.seek(0) + lines = verbose_output.readlines() + assert_true('[Pipeline] (step 1 of 2) mult1 ... ' in lines[0]) + if last_was_none: + assert_true('[Pipeline] Step mult2 is NoneType.' in lines[1]) + else: + assert_true('[Pipeline] (step 2 of 2) mult2 ... ' in lines[1]) + assert_true('[Pipeline] Total time elapsed: ' in lines[2]) + + +def test_pipeline_verbosity_fit_transform(): + pipe = Pipeline([('mult1', Mult(mult=1)), ('mult2', Mult(mult=2))], + verbose=True) + yield check_pipeline_verbosity_fit_transform, pipe.fit_transform + pipe = Pipeline([('mult1', Mult(mult=1)), ('mult2', None)], + verbose=True) + yield check_pipeline_verbosity_fit_transform, pipe.fit_transform, True From c7bbb94b716f93aae9abc6c16dc4a9c12f066603 Mon Sep 17 00:00:00 2001 From: Karan Desai Date: Fri, 10 Mar 2017 17:11:55 +0530 Subject: [PATCH 02/64] ENH Add verbose option and corresponding tests for FeatureUnion. --- doc/modules/pipeline.rst | 4 +- sklearn/pipeline.py | 70 +++++++++++++++++++++++++--------- sklearn/tests/test_pipeline.py | 25 ++++++++++++ 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/doc/modules/pipeline.rst b/doc/modules/pipeline.rst index 425fe6ed66dc0..456d2f6142847 100644 --- a/doc/modules/pipeline.rst +++ b/doc/modules/pipeline.rst @@ -254,7 +254,7 @@ and ``value`` is an estimator object:: FeatureUnion(n_jobs=1, transformer_list=[('linear_pca', PCA(copy=True,...)), ('kernel_pca', KernelPCA(alpha=1.0,...))], - transformer_weights=None) + transformer_weights=None, verbose=False) Like pipelines, feature unions have a shorthand constructor called @@ -269,7 +269,7 @@ and ignored by setting to ``None``:: FeatureUnion(n_jobs=1, transformer_list=[('linear_pca', PCA(copy=True,...)), ('kernel_pca', None)], - transformer_weights=None) + transformer_weights=None, verbose=False) .. topic:: Examples: diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 9cda48aa081eb..5d799ebe03686 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -656,8 +656,16 @@ def make_pipeline(*steps, **kwargs): return Pipeline(_name_estimators(steps), memory=memory) -def _fit_one_transformer(transformer, X, y): - return transformer.fit(X, y) +def _fit_one_transformer(trans, X, y, verbose=False, idx=None, + total_steps=None, name=None): + # idx, total_steps and name are not required when verbosity is disabled + step_start_time = time.time() + trans = trans.fit(X, y) + step_time_elapsed = time.time() - step_start_time + if verbose: + print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % + (idx + 1, total_steps, name, step_time_elapsed)) + return trans def _transform_one(transformer, weight, X): @@ -668,16 +676,22 @@ def _transform_one(transformer, weight, X): return res * weight -def _fit_transform_one(transformer, weight, X, y, - **fit_params): - if hasattr(transformer, 'fit_transform'): - res = transformer.fit_transform(X, y, **fit_params) +def _fit_transform_one(trans, weight, X, y, verbose=False, idx=None, + total_steps=None, name=None, **fit_params): + # idx, total_steps and name are not required when verbosity is disabled + step_start_time = time.time() + if hasattr(trans, 'fit_transform'): + res = trans.fit_transform(X, y, **fit_params) else: - res = transformer.fit(X, y, **fit_params).transform(X) + res = trans.fit(X, y, **fit_params).transform(X) + step_time_elapsed = time.time() - step_start_time + if verbose: + print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % + (idx + 1, total_steps, name, step_time_elapsed)) # if we have a weight for this transformer, multiply output if weight is None: - return res, transformer - return res * weight, transformer + return res, trans + return res * weight, trans class FeatureUnion(_BasePipeline, TransformerMixin): @@ -707,12 +721,17 @@ class FeatureUnion(_BasePipeline, TransformerMixin): Multiplicative weights for features per transformer. Keys are transformer names, values the weights. + verbose : boolean, optional + Verbosity mode. + """ - def __init__(self, transformer_list, n_jobs=1, transformer_weights=None): - self.transformer_list = list(transformer_list) + def __init__(self, transformer_list, n_jobs=1, transformer_weights=None, + verbose=False): + self.transformer_list = tosequence(transformer_list) self.n_jobs = n_jobs self.transformer_weights = transformer_weights + self.verbose = verbose self._validate_transformers() def get_params(self, deep=True): @@ -802,9 +821,18 @@ def fit(self, X, y=None): This estimator """ self._validate_transformers() + all_transformers = [(name, trans, weight) for name, trans, weight in + self._iter()] + total_steps = len(all_transformers) + # Keep a record of time elapsed + start_time = time.time() transformers = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_one_transformer)(trans, X, y) - for _, trans, _ in self._iter()) + delayed(_fit_one_transformer)(trans, X, y, self.verbose, idx, + total_steps, name) + for idx, (name, trans, _) in enumerate(all_transformers)) + time_elapsed = time.time() - start_time + if self.verbose: + print('[FeatureUnion] Total time elapsed: %.5fs' % time_elapsed) self._update_transformer_list(transformers) return self @@ -826,10 +854,18 @@ def fit_transform(self, X, y=None, **fit_params): sum of n_components (output dimension) over transformers. """ self._validate_transformers() + all_transformers = [(name, trans, weight) for name, trans, weight in + self._iter()] + total_steps = len(all_transformers) + # Keep a record of time elapsed + start_time = time.time() result = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_transform_one)(trans, weight, X, y, - **fit_params) - for name, trans, weight in self._iter()) + delayed(_fit_transform_one)(trans, weight, X, y, self.verbose, + idx, total_steps, name) + for idx, (name, trans, weight) in enumerate(all_transformers)) + time_elapsed = time.time() - start_time + if self.verbose: + print('[FeatureUnion] Total time elapsed: %.5fs' % time_elapsed) if not result: # All transformers are None @@ -908,7 +944,7 @@ def make_union(*transformers, **kwargs): TruncatedSVD(algorithm='randomized', n_components=2, n_iter=5, random_state=None, tol=0.0))], - transformer_weights=None) + transformer_weights=None, verbose=False) """ n_jobs = kwargs.pop('n_jobs', 1) if kwargs: diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index ed54482cd26c8..69907919071c1 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1022,3 +1022,28 @@ def test_pipeline_verbosity_fit_transform(): pipe = Pipeline([('mult1', Mult(mult=1)), ('mult2', None)], verbose=True) yield check_pipeline_verbosity_fit_transform, pipe.fit_transform, True + + +def check_feature_union_verbosity(feature_union_method): + # Test that the verbosity of feature union is proper + from sklearn.externals.six.moves import cStringIO as StringIO + import sys + old_stdout = sys.stdout + sys.stdout = StringIO() + feature_union_method(X=[[1, 2, 3], [4, 5, 6]], y=[[7], [8]]) + verbose_output = sys.stdout + sys.stdout = old_stdout + + # check output + verbose_output.seek(0) + lines = verbose_output.readlines() + assert_true('[FeatureUnion] (step 1 of 2) mult1 ... ' in lines[0]) + assert_true('[FeatureUnion] (step 2 of 2) mult2 ... ' in lines[1]) + assert_true('[FeatureUnion] Total time elapsed: ' in lines[2]) + + +def test_feature_union_verbosity(): + union = FeatureUnion([('mult1', Mult(mult=1)), ('mult2', Mult(mult=2))], + verbose=True) + yield check_feature_union_verbosity, union.fit + yield check_feature_union_verbosity, union.fit_transform From d1353fb67a6ad0074ace386a067e54d492a882fd Mon Sep 17 00:00:00 2001 From: Karan Desai Date: Fri, 10 Mar 2017 20:14:41 +0530 Subject: [PATCH 03/64] ENH Add _pretty_print method to print from Pipeline properly. - Each line printed by Pipeline and FeatureUnion, when their verbosity mode is on, will be 70 characters long. --- sklearn/pipeline.py | 70 ++++++++++++++++++++++++---------- sklearn/tests/test_pipeline.py | 14 +++---- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 5d799ebe03686..4053c580c4656 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -27,6 +27,22 @@ __all__ = ['Pipeline', 'FeatureUnion'] +def _pretty_print(step_info): + """Helper method to print the information about execution of a particular + step of Pipeline / FeatureUnion (if verbosity is enabled). It receives a + string having information about current step and prints it in such a way + that its length is 70 characters. + + Parameters + ---------- + step_info : str + String of form '[ClassName] (step x of y) step_name ... time_elapsed' + + """ + name, elapsed = step_info.split('...') + print('%s%s%s' % (name, '.' * (70 - len(name + elapsed)), elapsed)) + + class _BasePipeline(six.with_metaclass(ABCMeta, _BaseComposition)): """Handles parameter management for classifiers composed of named steps. """ @@ -231,13 +247,12 @@ def _validate_steps(self): "'%s' (type %s) doesn't" % (estimator, type(estimator))) - def _print_final_step(self, start_time, time_elapsed_so_far): - time_elapsed = time.time() - start_time - time_elapsed_so_far += time_elapsed - print('[Pipeline] (step %d of %d) %s ... %.5fs' % - (len(self.steps), len(self.steps), self.steps[-1][0], - time_elapsed_so_far)) - print('[Pipeline] Total time elapsed: %.5fs' % time_elapsed_so_far) + def _print_final_step(self, final_step_time_elapsed, time_elapsed_so_far): + _pretty_print('[Pipeline] (step %d of %d) %s ... %.5fs' % + (len(self.steps), len(self.steps), self.steps[-1][0], + final_step_time_elapsed)) + _pretty_print('[Pipeline] Total time elapsed: ... %.5fs' % + time_elapsed_so_far) @property def _estimator_type(self): @@ -293,8 +308,9 @@ def _fit(self, X, y=None, **fit_params): time_elapsed_so_far += step_time_elapsed # Logging time elapsed for current step to stdout if self.verbose: - print('[Pipeline] (step %d of %d) %s ... %.5fs' % - (step_idx + 1, len(self.steps), name, step_time_elapsed)) + _pretty_print('[Pipeline] (step %d of %d) %s ... %.5fs' % + (step_idx + 1, len(self.steps), name, + step_time_elapsed)) if self._final_estimator is None: return Xt, {}, time_elapsed_so_far return Xt, fit_params_steps[self.steps[-1][0]], time_elapsed_so_far @@ -329,8 +345,11 @@ def fit(self, X, y=None, **fit_params): final_step_start_time = time.time() if self._final_estimator is not None: self._final_estimator.fit(Xt, y, **fit_params) + final_step_time_elapsed = time.time() - final_step_start_time + time_elapsed_so_far += final_step_time_elapsed if self.verbose: - self._print_final_step(final_step_start_time, time_elapsed_so_far) + self._print_final_step(final_step_time_elapsed, + time_elapsed_so_far) return self def fit_transform(self, X, y=None, **fit_params): @@ -365,16 +384,20 @@ def fit_transform(self, X, y=None, **fit_params): final_step_start_time = time.time() if last_step is None: if self.verbose: - print('[Pipeline] Step %s is NoneType.' % self.steps[-1][0]) - print('[Pipeline] Total time elapsed: %.3fs' % - time_elapsed_so_far) + _pretty_print('[Pipeline] Step %s is NoneType ...' % + self.steps[-1][0]) + _pretty_print('[Pipeline] Total time elapsed: ... %.5fs' % + time_elapsed_so_far) return Xt elif hasattr(last_step, 'fit_transform'): Xt = last_step.fit_transform(Xt, y, **fit_params) else: Xt = last_step.fit(Xt, y, **fit_params).transform(Xt) + final_step_time_elapsed = time.time() - final_step_start_time + time_elapsed_so_far += final_step_time_elapsed if self.verbose: - self._print_final_step(final_step_start_time, time_elapsed_so_far) + self._print_final_step(final_step_time_elapsed, + time_elapsed_so_far) return Xt @if_delegate_has_method(delegate='_final_estimator') @@ -427,8 +450,11 @@ def fit_predict(self, X, y=None, **fit_params): Xt, fit_params, time_elapsed_so_far = self._fit(X, y, **fit_params) final_step_start_time = time.time() y_pred = self.steps[-1][-1].fit_predict(Xt, y, **fit_params) + final_step_time_elapsed = time.time() - final_step_start_time + time_elapsed_so_far += final_step_time_elapsed if self.verbose: - self._print_final_step(final_step_start_time, time_elapsed_so_far) + self._print_final_step(final_step_time_elapsed, + time_elapsed_so_far) return y_pred @if_delegate_has_method(delegate='_final_estimator') @@ -663,8 +689,8 @@ def _fit_one_transformer(trans, X, y, verbose=False, idx=None, trans = trans.fit(X, y) step_time_elapsed = time.time() - step_start_time if verbose: - print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % - (idx + 1, total_steps, name, step_time_elapsed)) + _pretty_print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % + (idx + 1, total_steps, name, step_time_elapsed)) return trans @@ -686,8 +712,8 @@ def _fit_transform_one(trans, weight, X, y, verbose=False, idx=None, res = trans.fit(X, y, **fit_params).transform(X) step_time_elapsed = time.time() - step_start_time if verbose: - print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % - (idx + 1, total_steps, name, step_time_elapsed)) + _pretty_print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % + (idx + 1, total_steps, name, step_time_elapsed)) # if we have a weight for this transformer, multiply output if weight is None: return res, trans @@ -832,7 +858,8 @@ def fit(self, X, y=None): for idx, (name, trans, _) in enumerate(all_transformers)) time_elapsed = time.time() - start_time if self.verbose: - print('[FeatureUnion] Total time elapsed: %.5fs' % time_elapsed) + _pretty_print( + '[FeatureUnion] Total time elapsed: ... %.5fs' % time_elapsed) self._update_transformer_list(transformers) return self @@ -865,7 +892,8 @@ def fit_transform(self, X, y=None, **fit_params): for idx, (name, trans, weight) in enumerate(all_transformers)) time_elapsed = time.time() - start_time if self.verbose: - print('[FeatureUnion] Total time elapsed: %.5fs' % time_elapsed) + _pretty_print( + '[FeatureUnion] Total time elapsed: ... %.5fs' % time_elapsed) if not result: # All transformers are None diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 69907919071c1..bfbd5760ef9a8 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -983,8 +983,8 @@ def check_pipeline_verbosity_fit_predict(pipe_method): # check output verbose_output.seek(0) lines = verbose_output.readlines() - assert_true('[Pipeline] (step 1 of 2) transf ... ' in lines[0]) - assert_true('[Pipeline] (step 2 of 2) clf ... ' in lines[1]) + assert_true('[Pipeline] (step 1 of 2) transf ...' in lines[0]) + assert_true('[Pipeline] (step 2 of 2) clf ...' in lines[1]) assert_true('[Pipeline] Total time elapsed: ' in lines[2]) @@ -1007,11 +1007,11 @@ def check_pipeline_verbosity_fit_transform(pipe_method, last_was_none=False): # check output verbose_output.seek(0) lines = verbose_output.readlines() - assert_true('[Pipeline] (step 1 of 2) mult1 ... ' in lines[0]) + assert_true('[Pipeline] (step 1 of 2) mult1 ...' in lines[0]) if last_was_none: - assert_true('[Pipeline] Step mult2 is NoneType.' in lines[1]) + assert_true('[Pipeline] Step mult2 is NoneType ...' in lines[1]) else: - assert_true('[Pipeline] (step 2 of 2) mult2 ... ' in lines[1]) + assert_true('[Pipeline] (step 2 of 2) mult2 ...' in lines[1]) assert_true('[Pipeline] Total time elapsed: ' in lines[2]) @@ -1037,8 +1037,8 @@ def check_feature_union_verbosity(feature_union_method): # check output verbose_output.seek(0) lines = verbose_output.readlines() - assert_true('[FeatureUnion] (step 1 of 2) mult1 ... ' in lines[0]) - assert_true('[FeatureUnion] (step 2 of 2) mult2 ... ' in lines[1]) + assert_true('[FeatureUnion] (step 1 of 2) mult1 ...' in lines[0]) + assert_true('[FeatureUnion] (step 2 of 2) mult2 ...' in lines[1]) assert_true('[FeatureUnion] Total time elapsed: ' in lines[2]) From a981bdc2533bbd93d870994b0e68fd796c7acbdd Mon Sep 17 00:00:00 2001 From: Karan Desai Date: Fri, 10 Mar 2017 20:18:24 +0530 Subject: [PATCH 04/64] Add a changelog entry about verbosity of Pipeline. --- doc/whats_new.rst | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 0ca707ce2cbbf..bc49a821db437 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -415,6 +415,113 @@ Miscellaneous - :class:`dummy.DummyClassifier` and :class:`dummy.DummyRegressor` now accept non-finite features. :issue:`8931` by :user:`Attractadore`. + - Added optional parameter ``verbose`` in :class:`pipeline.Pipeline` and + :class:`pipeline.FeatureUnion` for showing progress and timing of each + step. :issue:`8568` by :user:`Karan Desai `. + + - Update Sphinx-Gallery from 0.1.4 to 0.1.7 for resolving links in + documentation build with Sphinx>1.5 :issue:`8010`, :issue:`7986` + :user:`Oscar Najera ` + + - :class:`multioutput.MultiOutputRegressor` and :class:`multioutput.MultiOutputClassifier` + now support online learning using `partial_fit`. + issue: `8053` by :user:`Peng Yu `. + - :class:`pipeline.Pipeline` allows to cache transformers + within a pipeline by using the ``memory`` constructor parameter. + By :issue:`7990` by :user:`Guillaume Lemaitre `. + + - :class:`decomposition.PCA`, :class:`decomposition.IncrementalPCA` and + :class:`decomposition.TruncatedSVD` now expose the singular values + from the underlying SVD. They are stored in the attribute + ``singular_values_``, like in :class:`decomposition.IncrementalPCA`. + + - :class:`cluster.MiniBatchKMeans` and :class:`cluster.KMeans` + now uses significantly less memory when assigning data points to their + nearest cluster center. :issue:`7721` by :user:`Jon Crall `. + + - Added ``classes_`` attribute to :class:`model_selection.GridSearchCV`, + :class:`model_selection.RandomizedSearchCV`, :class:`grid_search.GridSearchCV`, + and :class:`grid_search.RandomizedSearchCV` that matches the ``classes_`` + attribute of ``best_estimator_``. :issue:`7661` and :issue:`8295` + by :user:`Alyssa Batula `, :user:`Dylan Werner-Meier `, + and :user:`Stephen Hoover `. + + - The ``min_weight_fraction_leaf`` constraint in tree construction is now + more efficient, taking a fast path to declare a node a leaf if its weight + is less than 2 * the minimum. Note that the constructed tree will be + different from previous versions where ``min_weight_fraction_leaf`` is + used. :issue:`7441` by :user:`Nelson Liu `. + + - Added ``average`` parameter to perform weights averaging in + :class:`linear_model.PassiveAggressiveClassifier`. :issue:`4939` + by :user:`Andrea Esuli `. + + - Custom metrics for the :mod:`sklearn.neighbors` binary trees now have + fewer constraints: they must take two 1d-arrays and return a float. + :issue:`6288` by `Jake Vanderplas`_. + + - :class:`ensemble.GradientBoostingClassifier` and :class:`ensemble.GradientBoostingRegressor` + now support sparse input for prediction. + :issue:`6101` by :user:`Ibraim Ganiev `. + + - Added ``shuffle`` and ``random_state`` parameters to shuffle training + data before taking prefixes of it based on training sizes in + :func:`model_selection.learning_curve`. + :issue:`7506` by :user:`Narine Kokhlikyan `. + + - Added ``norm_order`` parameter to :class:`feature_selection.SelectFromModel` + to enable selection of the norm order when ``coef_`` is more than 1D + + - Added ``sample_weight`` parameter to :meth:`pipeline.Pipeline.score`. + :issue:`7723` by :user:`Mikhail Korobov `. + + - ``check_estimator`` now attempts to ensure that methods transform, predict, etc. + do not set attributes on the estimator. + :issue:`7533` by :user:`Ekaterina Krivich `. + + - For sparse matrices, :func:`preprocessing.normalize` with ``return_norm=True`` + will now raise a ``NotImplementedError`` with 'l1' or 'l2' norm and with + norm 'max' the norms returned will be the same as for dense matrices. + :issue:`7771` by `Ang Lu `_. + + - :class:`sklearn.linear_model.RANSACRegressor` no longer throws an error + when calling ``fit`` if no inliers are found in its first iteration. + Furthermore, causes of skipped iterations are tracked in newly added + attributes, ``n_skips_*``. + :issue:`7914` by :user:`Michael Horrell `. + + - :func:`model_selection.cross_val_predict` now returns output of the + correct shape for all values of the argument ``method``. + :issue:`7863` by :user:`Aman Dalmia `. + + - Fix a bug where :class:`sklearn.feature_selection.SelectFdr` did not + exactly implement Benjamini-Hochberg procedure. It formerly may have + selected fewer features than it should. + :issue:`7490` by :user:`Peng Meng `. + + - Added ability to set ``n_jobs`` parameter to :func:`pipeline.make_union`. + A ``TypeError`` will be raised for any other kwargs. :issue:`8028` + by :user:`Alexander Booth `. + + - Added type checking to the ``accept_sparse`` parameter in + :mod:`sklearn.utils.validation` methods. This parameter now accepts only + boolean, string, or list/tuple of strings. ``accept_sparse=None`` is deprecated + and should be replaced by ``accept_sparse=False``. + :issue:`7880` by :user:`Josh Karnofsky `. + + - :class:`model_selection.GridSearchCV`, :class:`model_selection.RandomizedSearchCV` + and :func:`model_selection.cross_val_score` now allow estimators with callable + kernels which were previously prohibited. :issue:`8005` by `Andreas Müller`_ . + + + - Added ability to use sparse matrices in :func:`feature_selection.f_regression` + with ``center=True``. :issue:`8065` by :user:`Daniel LeJeune `. + + - Add ``sample_weight`` parameter to :func:`metrics.cohen_kappa_score` by + Victor Poughon. + + - In :class:`gaussian_process.GaussianProcessRegressor`, method ``predict`` + is a lot faster with ``return_std=True`` by :user:`Hadrien Bertrand `. Bug fixes ......... @@ -5754,3 +5861,5 @@ David Huard, Dave Morrill, Ed Schofield, Travis Oliphant, Pearu Peterson. .. _Neeraj Gangwar: http://neerajgangwar.in .. _Arthur Mensch: https://amensch.fr + +.. _Karan Desai: https://www.github.com/karandesai-96 From 3bddca19703d775e2389fd31b39acadad35cfc63 Mon Sep 17 00:00:00 2001 From: petrushev Date: Fri, 1 Sep 2017 12:52:43 +0200 Subject: [PATCH 05/64] Fix bug in pipeline transformer list in-place assignment; Fix error message bug --- sklearn/pipeline.py | 6 +++--- sklearn/tests/test_pipeline.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 4053c580c4656..765d8da7fbf41 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -20,7 +20,7 @@ from .externals.joblib import Parallel, delayed from .externals import six from .utils.metaestimators import if_delegate_has_method, _BaseComposition -from .utils import Bunch +from .utils import Bunch, tosequence from .utils.validation import check_memory @@ -934,10 +934,10 @@ def transform(self, X): def _update_transformer_list(self, transformers): transformers = iter(transformers) - self.transformer_list[:] = [ + self.transformer_list = tosequence([ (name, None if old is None else next(transformers)) for name, old in self.transformer_list - ] + ]) def make_union(*transformers, **kwargs): diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index bfbd5760ef9a8..6ccb3a2360ecc 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -834,9 +834,9 @@ def test_step_name_validation(): # we validate in construction (despite scikit-learn convention) bad_steps3 = [('a', Mult(2)), (param, Mult(3))] for bad_steps, message in [ - (bad_steps1, "Estimator names must not contain __: got ['a__q']"), + (bad_steps1, "Step names must not contain __: got ['a__q']"), (bad_steps2, "Names provided are not unique: ['a', 'a']"), - (bad_steps3, "Estimator names conflict with constructor " + (bad_steps3, "Step names conflict with constructor " "arguments: ['%s']" % param), ]: # three ways to make invalid: From ba083b8264d9201755da4a01e77bfb9e5f209f5d Mon Sep 17 00:00:00 2001 From: petrushev Date: Fri, 1 Sep 2017 13:15:53 +0200 Subject: [PATCH 06/64] Extend `make_pipeline` to support the `verbose` kwarg as well --- sklearn/pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 765d8da7fbf41..2949d383dce8e 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -676,10 +676,11 @@ def make_pipeline(*steps, **kwargs): p : Pipeline """ memory = kwargs.pop('memory', None) + verbose = kwargs.pop('verbose', False) if kwargs: raise TypeError('Unknown keyword arguments: "{}"' .format(list(kwargs.keys())[0])) - return Pipeline(_name_estimators(steps), memory=memory) + return Pipeline(_name_estimators(steps), memory=memory, verbose=verbose) def _fit_one_transformer(trans, X, y, verbose=False, idx=None, From 5857d5e9b5b1cca9d6083c8bd4246165459291e5 Mon Sep 17 00:00:00 2001 From: petrushev Date: Fri, 1 Sep 2017 13:40:35 +0200 Subject: [PATCH 07/64] Fix `whats new` to reflect the Pipeline verbose change --- doc/whats_new.rst | 114 +++------------------------------------------- 1 file changed, 7 insertions(+), 107 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index bc49a821db437..d8d5c43004dce 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -48,6 +48,13 @@ Model evaluation and meta-estimators - A scorer based on :func:`metrics.brier_score_loss` is also available. :issue:`9521` by :user:`Hanmin Qin `. +Miscellaneous + +- Added optional parameter ``verbose`` in :class:`pipeline.Pipeline` and + :class:`pipeline.FeatureUnion` for showing progress and timing of each + step. :issue:`8568` by :user:`Karan Desai `. + + Bug fixes ......... @@ -415,113 +422,6 @@ Miscellaneous - :class:`dummy.DummyClassifier` and :class:`dummy.DummyRegressor` now accept non-finite features. :issue:`8931` by :user:`Attractadore`. - - Added optional parameter ``verbose`` in :class:`pipeline.Pipeline` and - :class:`pipeline.FeatureUnion` for showing progress and timing of each - step. :issue:`8568` by :user:`Karan Desai `. - - - Update Sphinx-Gallery from 0.1.4 to 0.1.7 for resolving links in - documentation build with Sphinx>1.5 :issue:`8010`, :issue:`7986` - :user:`Oscar Najera ` - - - :class:`multioutput.MultiOutputRegressor` and :class:`multioutput.MultiOutputClassifier` - now support online learning using `partial_fit`. - issue: `8053` by :user:`Peng Yu `. - - :class:`pipeline.Pipeline` allows to cache transformers - within a pipeline by using the ``memory`` constructor parameter. - By :issue:`7990` by :user:`Guillaume Lemaitre `. - - - :class:`decomposition.PCA`, :class:`decomposition.IncrementalPCA` and - :class:`decomposition.TruncatedSVD` now expose the singular values - from the underlying SVD. They are stored in the attribute - ``singular_values_``, like in :class:`decomposition.IncrementalPCA`. - - - :class:`cluster.MiniBatchKMeans` and :class:`cluster.KMeans` - now uses significantly less memory when assigning data points to their - nearest cluster center. :issue:`7721` by :user:`Jon Crall `. - - - Added ``classes_`` attribute to :class:`model_selection.GridSearchCV`, - :class:`model_selection.RandomizedSearchCV`, :class:`grid_search.GridSearchCV`, - and :class:`grid_search.RandomizedSearchCV` that matches the ``classes_`` - attribute of ``best_estimator_``. :issue:`7661` and :issue:`8295` - by :user:`Alyssa Batula `, :user:`Dylan Werner-Meier `, - and :user:`Stephen Hoover `. - - - The ``min_weight_fraction_leaf`` constraint in tree construction is now - more efficient, taking a fast path to declare a node a leaf if its weight - is less than 2 * the minimum. Note that the constructed tree will be - different from previous versions where ``min_weight_fraction_leaf`` is - used. :issue:`7441` by :user:`Nelson Liu `. - - - Added ``average`` parameter to perform weights averaging in - :class:`linear_model.PassiveAggressiveClassifier`. :issue:`4939` - by :user:`Andrea Esuli `. - - - Custom metrics for the :mod:`sklearn.neighbors` binary trees now have - fewer constraints: they must take two 1d-arrays and return a float. - :issue:`6288` by `Jake Vanderplas`_. - - - :class:`ensemble.GradientBoostingClassifier` and :class:`ensemble.GradientBoostingRegressor` - now support sparse input for prediction. - :issue:`6101` by :user:`Ibraim Ganiev `. - - - Added ``shuffle`` and ``random_state`` parameters to shuffle training - data before taking prefixes of it based on training sizes in - :func:`model_selection.learning_curve`. - :issue:`7506` by :user:`Narine Kokhlikyan `. - - - Added ``norm_order`` parameter to :class:`feature_selection.SelectFromModel` - to enable selection of the norm order when ``coef_`` is more than 1D - - - Added ``sample_weight`` parameter to :meth:`pipeline.Pipeline.score`. - :issue:`7723` by :user:`Mikhail Korobov `. - - - ``check_estimator`` now attempts to ensure that methods transform, predict, etc. - do not set attributes on the estimator. - :issue:`7533` by :user:`Ekaterina Krivich `. - - - For sparse matrices, :func:`preprocessing.normalize` with ``return_norm=True`` - will now raise a ``NotImplementedError`` with 'l1' or 'l2' norm and with - norm 'max' the norms returned will be the same as for dense matrices. - :issue:`7771` by `Ang Lu `_. - - - :class:`sklearn.linear_model.RANSACRegressor` no longer throws an error - when calling ``fit`` if no inliers are found in its first iteration. - Furthermore, causes of skipped iterations are tracked in newly added - attributes, ``n_skips_*``. - :issue:`7914` by :user:`Michael Horrell `. - - - :func:`model_selection.cross_val_predict` now returns output of the - correct shape for all values of the argument ``method``. - :issue:`7863` by :user:`Aman Dalmia `. - - - Fix a bug where :class:`sklearn.feature_selection.SelectFdr` did not - exactly implement Benjamini-Hochberg procedure. It formerly may have - selected fewer features than it should. - :issue:`7490` by :user:`Peng Meng `. - - - Added ability to set ``n_jobs`` parameter to :func:`pipeline.make_union`. - A ``TypeError`` will be raised for any other kwargs. :issue:`8028` - by :user:`Alexander Booth `. - - - Added type checking to the ``accept_sparse`` parameter in - :mod:`sklearn.utils.validation` methods. This parameter now accepts only - boolean, string, or list/tuple of strings. ``accept_sparse=None`` is deprecated - and should be replaced by ``accept_sparse=False``. - :issue:`7880` by :user:`Josh Karnofsky `. - - - :class:`model_selection.GridSearchCV`, :class:`model_selection.RandomizedSearchCV` - and :func:`model_selection.cross_val_score` now allow estimators with callable - kernels which were previously prohibited. :issue:`8005` by `Andreas Müller`_ . - - - - Added ability to use sparse matrices in :func:`feature_selection.f_regression` - with ``center=True``. :issue:`8065` by :user:`Daniel LeJeune `. - - - Add ``sample_weight`` parameter to :func:`metrics.cohen_kappa_score` by - Victor Poughon. - - - In :class:`gaussian_process.GaussianProcessRegressor`, method ``predict`` - is a lot faster with ``return_std=True`` by :user:`Hadrien Bertrand `. Bug fixes ......... From 72f4d0ed39742117bfda1c1bff0fcca8619f2a24 Mon Sep 17 00:00:00 2001 From: petrushev Date: Fri, 1 Sep 2017 13:59:09 +0200 Subject: [PATCH 08/64] Remove `_BasePipeline` resulting from a bad rebase --- sklearn/pipeline.py | 63 ++-------------------------------- sklearn/tests/test_pipeline.py | 4 +-- 2 files changed, 4 insertions(+), 63 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 2949d383dce8e..d05f8ee1991a7 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -10,7 +10,6 @@ # License: BSD from collections import defaultdict -from abc import ABCMeta, abstractmethod import time import numpy as np @@ -43,65 +42,7 @@ def _pretty_print(step_info): print('%s%s%s' % (name, '.' * (70 - len(name + elapsed)), elapsed)) -class _BasePipeline(six.with_metaclass(ABCMeta, _BaseComposition)): - """Handles parameter management for classifiers composed of named steps. - """ - - @abstractmethod - def __init__(self): - pass - - def _replace_step(self, steps_attr, name, new_val): - # assumes `name` is a valid step name - new_steps = getattr(self, steps_attr)[:] - for i, (step_name, _) in enumerate(new_steps): - if step_name == name: - new_steps[i] = (name, new_val) - break - setattr(self, steps_attr, new_steps) - - def _get_params(self, steps_attr, deep=True): - out = super(_BasePipeline, self).get_params(deep=False) - if not deep: - return out - steps = getattr(self, steps_attr) - out.update(steps) - for name, estimator in steps: - if estimator is None: - continue - for key, value in six.iteritems(estimator.get_params(deep=True)): - out['%s__%s' % (name, key)] = value - return out - - def _set_params(self, steps_attr, **params): - # Ensure strict ordering of parameter setting: - # 1. All steps - if steps_attr in params: - setattr(self, steps_attr, params.pop(steps_attr)) - # 2. Step replacement - step_names, _ = zip(*getattr(self, steps_attr)) - for name in list(six.iterkeys(params)): - if '__' not in name and name in step_names: - self._replace_step(steps_attr, name, params.pop(name)) - # 3. Step parameters and other initilisation arguments - super(_BasePipeline, self).set_params(**params) - return self - - def _validate_names(self, names): - if len(set(names)) != len(names): - raise ValueError('Names provided are not unique: ' - '{0!r}'.format(list(names))) - invalid_names = set(names).intersection(self.get_params(deep=False)) - if invalid_names: - raise ValueError('Step names conflict with constructor arguments: ' - '{0!r}'.format(sorted(invalid_names))) - invalid_names = [name for name in names if '__' in name] - if invalid_names: - raise ValueError('Step names must not contain __: got ' - '{0!r}'.format(invalid_names)) - - -class Pipeline(_BasePipeline): +class Pipeline(_BaseComposition): """Pipeline of transforms with a final estimator. Sequentially apply a list of transforms and a final estimator. @@ -721,7 +662,7 @@ def _fit_transform_one(trans, weight, X, y, verbose=False, idx=None, return res * weight, trans -class FeatureUnion(_BasePipeline, TransformerMixin): +class FeatureUnion(_BaseComposition, TransformerMixin): """Concatenates results of multiple transformer objects. This estimator applies a list of transformer objects in parallel to the diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 6ccb3a2360ecc..bfbd5760ef9a8 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -834,9 +834,9 @@ def test_step_name_validation(): # we validate in construction (despite scikit-learn convention) bad_steps3 = [('a', Mult(2)), (param, Mult(3))] for bad_steps, message in [ - (bad_steps1, "Step names must not contain __: got ['a__q']"), + (bad_steps1, "Estimator names must not contain __: got ['a__q']"), (bad_steps2, "Names provided are not unique: ['a', 'a']"), - (bad_steps3, "Step names conflict with constructor " + (bad_steps3, "Estimator names conflict with constructor " "arguments: ['%s']" % param), ]: # three ways to make invalid: From 1b7245e52e2c192ab67ea6eb4c5883ab5e226265 Mon Sep 17 00:00:00 2001 From: petrushev Date: Fri, 1 Sep 2017 14:03:17 +0200 Subject: [PATCH 09/64] Revert `FeatureUnion.transformer_list` to list instead of sequence --- sklearn/pipeline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index d05f8ee1991a7..91ec6b377074f 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -19,7 +19,7 @@ from .externals.joblib import Parallel, delayed from .externals import six from .utils.metaestimators import if_delegate_has_method, _BaseComposition -from .utils import Bunch, tosequence +from .utils import Bunch from .utils.validation import check_memory @@ -696,7 +696,7 @@ class FeatureUnion(_BaseComposition, TransformerMixin): def __init__(self, transformer_list, n_jobs=1, transformer_weights=None, verbose=False): - self.transformer_list = tosequence(transformer_list) + self.transformer_list = list(transformer_list) self.n_jobs = n_jobs self.transformer_weights = transformer_weights self.verbose = verbose @@ -876,10 +876,10 @@ def transform(self, X): def _update_transformer_list(self, transformers): transformers = iter(transformers) - self.transformer_list = tosequence([ + self.transformer_list[:] = [ (name, None if old is None else next(transformers)) for name, old in self.transformer_list - ]) + ] def make_union(*transformers, **kwargs): From 3938adc38d086aaf3aafc8700b840d7a47f5a1c3 Mon Sep 17 00:00:00 2001 From: petrushev Date: Fri, 1 Sep 2017 14:19:36 +0200 Subject: [PATCH 10/64] Extend `FeatureUnion` to support the `verbose` kwarg as well --- doc/whats_new.rst | 5 +++++ sklearn/pipeline.py | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index d8d5c43004dce..6e54350b10d43 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -54,6 +54,11 @@ Miscellaneous :class:`pipeline.FeatureUnion` for showing progress and timing of each step. :issue:`8568` by :user:`Karan Desai `. +- Added optional parameter ``verbose`` in functions `pipeline.make_pipeline` + and `pipeline.make_union` to extend the same functionality as the + corresponding classes. :issue:`9668` by + :user:`Baze Petrushev `. + Bug fixes ......... diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 91ec6b377074f..3046c5fa5009b 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -601,6 +601,9 @@ def make_pipeline(*steps, **kwargs): inspect estimators within the pipeline. Caching the transformers is advantageous when fitting is time consuming. + verbose : boolean, optional + Verbosity mode. + Examples -------- >>> from sklearn.naive_bayes import GaussianNB @@ -896,6 +899,9 @@ def make_union(*transformers, **kwargs): n_jobs : int, optional Number of jobs to run in parallel (default 1). + verbose : boolean, optional + Verbosity mode. + Returns ------- f : FeatureUnion @@ -917,9 +923,11 @@ def make_union(*transformers, **kwargs): transformer_weights=None, verbose=False) """ n_jobs = kwargs.pop('n_jobs', 1) + verbose = kwargs.pop('verbose', False) if kwargs: # We do not currently support `transformer_weights` as we may want to # change its type spec in make_union raise TypeError('Unknown keyword arguments: "{}"' .format(list(kwargs.keys())[0])) - return FeatureUnion(_name_estimators(transformers), n_jobs=n_jobs) + return FeatureUnion(_name_estimators(transformers), n_jobs=n_jobs, + verbose=verbose) From efb4aac6a1ce790bcb16dadf4cd5be5c1464be29 Mon Sep 17 00:00:00 2001 From: petrushev Date: Sun, 3 Sep 2017 22:25:18 +0200 Subject: [PATCH 11/64] Minor tidy-up --- doc/whats_new.rst | 3 +-- sklearn/pipeline.py | 40 +++++++++++++++++----------------- sklearn/tests/test_pipeline.py | 4 ++-- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 6e54350b10d43..981e7ae95788c 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -57,7 +57,7 @@ Miscellaneous - Added optional parameter ``verbose`` in functions `pipeline.make_pipeline` and `pipeline.make_union` to extend the same functionality as the corresponding classes. :issue:`9668` by - :user:`Baze Petrushev `. + :user:`Baze Petrushev ` and :user:`Karan Desai `. Bug fixes @@ -5766,5 +5766,4 @@ David Huard, Dave Morrill, Ed Schofield, Travis Oliphant, Pearu Peterson. .. _Neeraj Gangwar: http://neerajgangwar.in .. _Arthur Mensch: https://amensch.fr - .. _Karan Desai: https://www.github.com/karandesai-96 diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 3046c5fa5009b..b5975e002b568 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -18,10 +18,11 @@ from .base import clone, TransformerMixin from .externals.joblib import Parallel, delayed from .externals import six -from .utils.metaestimators import if_delegate_has_method, _BaseComposition +from .utils.metaestimators import if_delegate_has_method from .utils import Bunch from .utils.validation import check_memory +from .utils.metaestimators import _BaseComposition __all__ = ['Pipeline', 'FeatureUnion'] @@ -627,16 +628,16 @@ def make_pipeline(*steps, **kwargs): return Pipeline(_name_estimators(steps), memory=memory, verbose=verbose) -def _fit_one_transformer(trans, X, y, verbose=False, idx=None, +def _fit_one_transformer(transformer, X, y, verbose=False, idx=None, total_steps=None, name=None): # idx, total_steps and name are not required when verbosity is disabled step_start_time = time.time() - trans = trans.fit(X, y) + transformer = transformer.fit(X, y) step_time_elapsed = time.time() - step_start_time if verbose: _pretty_print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % (idx + 1, total_steps, name, step_time_elapsed)) - return trans + return transformer def _transform_one(transformer, weight, X): @@ -647,22 +648,22 @@ def _transform_one(transformer, weight, X): return res * weight -def _fit_transform_one(trans, weight, X, y, verbose=False, idx=None, +def _fit_transform_one(transformer, weight, X, y, verbose=False, idx=None, total_steps=None, name=None, **fit_params): # idx, total_steps and name are not required when verbosity is disabled step_start_time = time.time() - if hasattr(trans, 'fit_transform'): - res = trans.fit_transform(X, y, **fit_params) + if hasattr(transformer, 'fit_transform'): + res = transformer.fit_transform(X, y, **fit_params) else: - res = trans.fit(X, y, **fit_params).transform(X) + res = transformer.fit(X, y, **fit_params).transform(X) step_time_elapsed = time.time() - step_start_time if verbose: _pretty_print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % (idx + 1, total_steps, name, step_time_elapsed)) # if we have a weight for this transformer, multiply output if weight is None: - return res, trans - return res * weight, trans + return res, transformer + return res * weight, transformer class FeatureUnion(_BaseComposition, TransformerMixin): @@ -792,15 +793,14 @@ def fit(self, X, y=None): This estimator """ self._validate_transformers() - all_transformers = [(name, trans, weight) for name, trans, weight in - self._iter()] + all_transformers = list(self._iter()) total_steps = len(all_transformers) # Keep a record of time elapsed start_time = time.time() transformers = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_one_transformer)(trans, X, y, self.verbose, idx, - total_steps, name) - for idx, (name, trans, _) in enumerate(all_transformers)) + delayed(_fit_one_transformer)(transformer, X, y, self.verbose, + idx, total_steps, name) + for idx, (name, transformer, _) in enumerate(all_transformers)) time_elapsed = time.time() - start_time if self.verbose: _pretty_print( @@ -826,15 +826,15 @@ def fit_transform(self, X, y=None, **fit_params): sum of n_components (output dimension) over transformers. """ self._validate_transformers() - all_transformers = [(name, trans, weight) for name, trans, weight in - self._iter()] + all_transformers = list(self._iter()) total_steps = len(all_transformers) # Keep a record of time elapsed start_time = time.time() result = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_transform_one)(trans, weight, X, y, self.verbose, - idx, total_steps, name) - for idx, (name, trans, weight) in enumerate(all_transformers)) + delayed(_fit_transform_one)(transformer, weight, X, y, + self.verbose, idx, total_steps, name, + **fit_params) + for idx, (name, transformer, weight) in enumerate(all_transformers)) time_elapsed = time.time() - start_time if self.verbose: _pretty_print( diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index bfbd5760ef9a8..45454345cb9f1 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -67,12 +67,12 @@ def set_params(self, **params): class NoInvTransf(NoTrans): - def transform(self, X, y=None): + def transform(self, X): return X class Transf(NoInvTransf): - def transform(self, X, y=None): + def transform(self, X): return X def inverse_transform(self, X): From 2e5b6cc62b76a5ce752ea39c4b39594ab883b6b0 Mon Sep 17 00:00:00 2001 From: petrushev Date: Wed, 6 Sep 2017 00:18:03 +0200 Subject: [PATCH 12/64] Add `message_with_time` helper --- sklearn/cross_validation.py | 6 +-- sklearn/model_selection/_validation.py | 8 ++-- sklearn/pipeline.py | 59 ++++++++++---------------- sklearn/tests/test_pipeline.py | 27 +++++++----- sklearn/utils/__init__.py | 22 +++++++++- 5 files changed, 66 insertions(+), 56 deletions(-) diff --git a/sklearn/cross_validation.py b/sklearn/cross_validation.py index 7646459da3936..5544890d95e8d 100644 --- a/sklearn/cross_validation.py +++ b/sklearn/cross_validation.py @@ -22,7 +22,8 @@ import scipy.sparse as sp from .base import is_classifier, clone -from .utils import indexable, check_random_state, safe_indexing +from .utils import (indexable, check_random_state, safe_indexing, + message_with_time) from .utils.validation import (_is_arraylike, _num_samples, column_or_1d) from .utils.multiclass import type_of_target @@ -1700,8 +1701,7 @@ def _fit_and_score(estimator, X, y, scorer, train, test, verbose, if verbose > 2: msg += ", score=%f" % test_score if verbose > 1: - end_msg = "%s -%s" % (msg, logger.short_format_time(scoring_time)) - print("[CV] %s %s" % ((64 - len(end_msg)) * '.', end_msg)) + print(message_with_time('CV', msg, scoring_time)) ret = [train_score] if return_train_score else [] ret.extend([test_score, _num_samples(X_test), scoring_time]) diff --git a/sklearn/model_selection/_validation.py b/sklearn/model_selection/_validation.py index 773f70fb7dba2..a5a3a50dae443 100644 --- a/sklearn/model_selection/_validation.py +++ b/sklearn/model_selection/_validation.py @@ -20,10 +20,11 @@ import scipy.sparse as sp from ..base import is_classifier, clone -from ..utils import indexable, check_random_state, safe_indexing +from ..utils import (indexable, check_random_state, safe_indexing, + message_with_time) from ..utils.validation import _is_arraylike, _num_samples from ..utils.metaestimators import _safe_split -from ..externals.joblib import Parallel, delayed, logger +from ..externals.joblib import Parallel, delayed from ..externals.six.moves import zip from ..metrics.scorer import check_scoring, _check_multimetric_scoring from ..exceptions import FitFailedWarning @@ -480,8 +481,7 @@ def _fit_and_score(estimator, X, y, scorer, train, test, verbose, msg += ", score=%s" % test_scores if verbose > 1: total_time = score_time + fit_time - end_msg = "%s, total=%s" % (msg, logger.short_format_time(total_time)) - print("[CV] %s %s" % ((64 - len(end_msg)) * '.', end_msg)) + print(message_with_time('CV', msg, total_time)) ret = [train_scores, test_scores] if return_train_score else [test_scores] diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index b5975e002b568..1a4e4d24c6e62 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -19,7 +19,7 @@ from .externals.joblib import Parallel, delayed from .externals import six from .utils.metaestimators import if_delegate_has_method -from .utils import Bunch +from .utils import Bunch, message_with_time from .utils.validation import check_memory from .utils.metaestimators import _BaseComposition @@ -27,22 +27,6 @@ __all__ = ['Pipeline', 'FeatureUnion'] -def _pretty_print(step_info): - """Helper method to print the information about execution of a particular - step of Pipeline / FeatureUnion (if verbosity is enabled). It receives a - string having information about current step and prints it in such a way - that its length is 70 characters. - - Parameters - ---------- - step_info : str - String of form '[ClassName] (step x of y) step_name ... time_elapsed' - - """ - name, elapsed = step_info.split('...') - print('%s%s%s' % (name, '.' * (70 - len(name + elapsed)), elapsed)) - - class Pipeline(_BaseComposition): """Pipeline of transforms with a final estimator. @@ -190,11 +174,11 @@ def _validate_steps(self): % (estimator, type(estimator))) def _print_final_step(self, final_step_time_elapsed, time_elapsed_so_far): - _pretty_print('[Pipeline] (step %d of %d) %s ... %.5fs' % - (len(self.steps), len(self.steps), self.steps[-1][0], - final_step_time_elapsed)) - _pretty_print('[Pipeline] Total time elapsed: ... %.5fs' % - time_elapsed_so_far) + message = '(step %d of %d) %s' % ( + len(self.steps), len(self.steps), self.steps[-1][0]) + print(message_with_time('Pipeline', message, final_step_time_elapsed)) + print(message_with_time( + 'Pipeline', 'Total time elapsed:', time_elapsed_so_far)) @property def _estimator_type(self): @@ -250,9 +234,9 @@ def _fit(self, X, y=None, **fit_params): time_elapsed_so_far += step_time_elapsed # Logging time elapsed for current step to stdout if self.verbose: - _pretty_print('[Pipeline] (step %d of %d) %s ... %.5fs' % - (step_idx + 1, len(self.steps), name, - step_time_elapsed)) + message = '(step %d of %d) %s' % ( + step_idx + 1, len(self.steps), name) + print(message_with_time('Pipeline', message, step_time_elapsed)) if self._final_estimator is None: return Xt, {}, time_elapsed_so_far return Xt, fit_params_steps[self.steps[-1][0]], time_elapsed_so_far @@ -326,10 +310,10 @@ def fit_transform(self, X, y=None, **fit_params): final_step_start_time = time.time() if last_step is None: if self.verbose: - _pretty_print('[Pipeline] Step %s is NoneType ...' % - self.steps[-1][0]) - _pretty_print('[Pipeline] Total time elapsed: ... %.5fs' % - time_elapsed_so_far) + message = 'Step %s is NoneType' % (self.steps[-1][0],) + print(message_with_time('Pipeline', message, 0)) + print(message_with_time( + 'Pipeline', 'Total time elapsed', time_elapsed_so_far)) return Xt elif hasattr(last_step, 'fit_transform'): Xt = last_step.fit_transform(Xt, y, **fit_params) @@ -635,8 +619,8 @@ def _fit_one_transformer(transformer, X, y, verbose=False, idx=None, transformer = transformer.fit(X, y) step_time_elapsed = time.time() - step_start_time if verbose: - _pretty_print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % - (idx + 1, total_steps, name, step_time_elapsed)) + message = '(step %d of %d) %s' % (idx + 1, total_steps, name) + print(message_with_time('FeatureUnion', message, step_time_elapsed)) return transformer @@ -658,8 +642,8 @@ def _fit_transform_one(transformer, weight, X, y, verbose=False, idx=None, res = transformer.fit(X, y, **fit_params).transform(X) step_time_elapsed = time.time() - step_start_time if verbose: - _pretty_print('[FeatureUnion] (step %d of %d) %s ... %.5fs' % - (idx + 1, total_steps, name, step_time_elapsed)) + message = '(step %d of %d) %s' % (idx + 1, total_steps, name) + print(message_with_time('FeatureUnion', message, step_time_elapsed)) # if we have a weight for this transformer, multiply output if weight is None: return res, transformer @@ -803,8 +787,9 @@ def fit(self, X, y=None): for idx, (name, transformer, _) in enumerate(all_transformers)) time_elapsed = time.time() - start_time if self.verbose: - _pretty_print( - '[FeatureUnion] Total time elapsed: ... %.5fs' % time_elapsed) + print(message_with_time( + 'FeatureUnion', 'Total time elapsed:', time_elapsed)) + self._update_transformer_list(transformers) return self @@ -837,8 +822,8 @@ def fit_transform(self, X, y=None, **fit_params): for idx, (name, transformer, weight) in enumerate(all_transformers)) time_elapsed = time.time() - start_time if self.verbose: - _pretty_print( - '[FeatureUnion] Total time elapsed: ... %.5fs' % time_elapsed) + print(message_with_time( + 'FeatureUnion', 'Total time elapsed:', time_elapsed)) if not result: # All transformers are None diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 45454345cb9f1..76d3fe66634fe 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -983,10 +983,11 @@ def check_pipeline_verbosity_fit_predict(pipe_method): # check output verbose_output.seek(0) lines = verbose_output.readlines() - assert_true('[Pipeline] (step 1 of 2) transf ...' in lines[0]) - assert_true('[Pipeline] (step 2 of 2) clf ...' in lines[1]) - assert_true('[Pipeline] Total time elapsed: ' in lines[2]) - + assert_true('(step 1 of 2) transf' in lines[0]) + assert_true('(step 2 of 2) clf' in lines[1]) + assert_true('Total time elapsed' in lines[2]) + for line in lines: + assert line.startswith('[Pipeline]') def test_pipeline_fit_verbosity(): pipe = Pipeline([('transf', Transf()), ('clf', FitParamT())], verbose=True) @@ -1007,12 +1008,13 @@ def check_pipeline_verbosity_fit_transform(pipe_method, last_was_none=False): # check output verbose_output.seek(0) lines = verbose_output.readlines() - assert_true('[Pipeline] (step 1 of 2) mult1 ...' in lines[0]) + assert_true('(step 1 of 2) mult1' in lines[0]) + assert_true(lines[0].startswith('[Pipeline]')) if last_was_none: - assert_true('[Pipeline] Step mult2 is NoneType ...' in lines[1]) + assert_true('Step mult2 is NoneType' in lines[1]) else: - assert_true('[Pipeline] (step 2 of 2) mult2 ...' in lines[1]) - assert_true('[Pipeline] Total time elapsed: ' in lines[2]) + assert_true('(step 2 of 2) mult2' in lines[1]) + assert_true('Total time elapsed' in lines[2]) def test_pipeline_verbosity_fit_transform(): @@ -1037,9 +1039,12 @@ def check_feature_union_verbosity(feature_union_method): # check output verbose_output.seek(0) lines = verbose_output.readlines() - assert_true('[FeatureUnion] (step 1 of 2) mult1 ...' in lines[0]) - assert_true('[FeatureUnion] (step 2 of 2) mult2 ...' in lines[1]) - assert_true('[FeatureUnion] Total time elapsed: ' in lines[2]) + assert_true('(step 1 of 2) mult1' in lines[0]) + assert_true('(step 2 of 2) mult2' in lines[1]) + assert_true('Total time elapsed' in lines[2]) + assert_true(lines[0].startswith('[FeatureUnion]')) + assert_true(lines[1].startswith('[FeatureUnion]')) + assert_true(lines[2].startswith('[FeatureUnion]')) def test_feature_union_verbosity(): diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 4b2665cdd4f77..f4c2a1791663f 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -14,7 +14,7 @@ check_consistent_length, check_X_y, indexable, check_symmetric) from .class_weight import compute_class_weight, compute_sample_weight -from ..externals.joblib import cpu_count +from ..externals.joblib import cpu_count, logger from ..exceptions import DataConversionWarning from .deprecation import deprecated @@ -506,3 +506,23 @@ def indices_to_mask(indices, mask_length): mask[indices] = True return mask + + +def message_with_time(source, message, time_): + """Create one line message for logging purposes + + Parameters + ---------- + source: str + String indicating the source or the reference of the message + + message: str + Short message + + time_: int + Time in seconds + """ + start_message = '[%s]' % (source,) + end_message = "%s, total=%s" % (message, logger.short_format_time(time_)) + dots_len = (68 - len(start_message) - len(end_message)) + return ("%s %s %s" % (start_message, dots_len * '.', end_message)) From 45f346b0932f3d90236943605a6d548f10c284c5 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Tue, 9 Jan 2018 22:26:35 +1100 Subject: [PATCH 13/64] Rewrite verbose pipeline logic --- doc/whats_new/v0.20.rst | 3 +- sklearn/cross_validation.py | 6 +- sklearn/pipeline.py | 200 +++++++++++++-------------------- sklearn/tests/test_pipeline.py | 120 +++++++------------- sklearn/utils/__init__.py | 37 +++++- 5 files changed, 151 insertions(+), 215 deletions(-) diff --git a/doc/whats_new/v0.20.rst b/doc/whats_new/v0.20.rst index cf87b546a6a57..5601c2474a436 100644 --- a/doc/whats_new/v0.20.rst +++ b/doc/whats_new/v0.20.rst @@ -142,7 +142,8 @@ Model evaluation and meta-estimators - Added optional parameter ``verbose`` in :class:`pipeline.Pipeline` and :class:`pipeline.FeatureUnion` and corresponding ``make_`` helpers for showing progress and timing of each step. :issue:`9668` by :user:`Baze - Petrushev ` and :user:`Karan Desai `. + Petrushev `, :user:`Karan Desai ` and `Joel + Nothman`_. Metrics diff --git a/sklearn/cross_validation.py b/sklearn/cross_validation.py index 2d22fcdd98234..3198c39139210 100644 --- a/sklearn/cross_validation.py +++ b/sklearn/cross_validation.py @@ -22,8 +22,7 @@ import scipy.sparse as sp from .base import is_classifier, clone -from .utils import (indexable, check_random_state, safe_indexing, - message_with_time) +from .utils import indexable, check_random_state, safe_indexing from .utils.validation import (_is_arraylike, _num_samples, column_or_1d) from .utils.multiclass import type_of_target @@ -1701,7 +1700,8 @@ def _fit_and_score(estimator, X, y, scorer, train, test, verbose, if verbose > 2: msg += ", score=%f" % test_score if verbose > 1: - print(message_with_time('CV', msg, scoring_time)) + end_msg = "%s -%s" % (msg, logger.short_format_time(scoring_time)) + print("[CV] %s %s" % ((64 - len(end_msg)) * '.', end_msg)) ret = [train_score] if return_train_score else [] ret.extend([test_score, _num_samples(X_test), scoring_time]) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 5785d68fb45a6..0c5c87ef5c572 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -10,7 +10,6 @@ # License: BSD from collections import defaultdict -import time import numpy as np from scipy import sparse @@ -19,7 +18,7 @@ from .externals.joblib import Parallel, delayed from .externals import six from .utils.metaestimators import if_delegate_has_method -from .utils import Bunch, message_with_time +from .utils import Bunch, log_elapsed from .utils.validation import check_memory from .utils.metaestimators import _BaseComposition @@ -172,13 +171,6 @@ def _validate_steps(self): "'%s' (type %s) doesn't" % (estimator, type(estimator))) - def _print_final_step(self, final_step_time_elapsed, time_elapsed_so_far): - message = '(step %d of %d) %s' % ( - len(self.steps), len(self.steps), self.steps[-1][0]) - print(message_with_time('Pipeline', message, final_step_time_elapsed)) - print(message_with_time( - 'Pipeline', 'Total time elapsed:', time_elapsed_so_far)) - @property def _estimator_type(self): return self.steps[-1][1]._estimator_type @@ -192,6 +184,18 @@ def named_steps(self): def _final_estimator(self): return self.steps[-1][1] + def _log_message(self, step_idx): + if not self.verbose: + return + if step_idx < 0: + step_idx = len(self.steps) + step_idx + n_steps = len([est for _, est in self.steps + if est is not None]) + step_num = len([est for _, est in self.steps[:step_idx + 1] + if est is not None]) + return '(step %d of %d) Fitting %s' % (step_num, n_steps, + self.steps[step_idx][0]) + # Estimator interface def _fit(self, X, y=None, **fit_params): @@ -209,38 +213,29 @@ def _fit(self, X, y=None, **fit_params): step, param = pname.split('__', 1) fit_params_steps[step][param] = pval Xt = X - # Keep a record of time elapsed - time_elapsed_so_far = 0 for step_idx, (name, transformer) in enumerate(self.steps[:-1]): - step_start_time = time.time() if transformer is None: - pass + continue + + if hasattr(memory, 'cachedir') and memory.cachedir is None: + # we do not clone when caching is disabled to preserve + # backward compatibility + cloned_transformer = transformer else: - if hasattr(memory, 'cachedir') and memory.cachedir is None: - # we do not clone when caching is disabled to preserve - # backward compatibility - cloned_transformer = transformer - else: - cloned_transformer = clone(transformer) - # Fit or load from cache the current transfomer - Xt, fitted_transformer = fit_transform_one_cached( - cloned_transformer, None, Xt, y, - **fit_params_steps[name]) - # Replace the transformer of the step with the fitted - # transformer. This is necessary when loading the transformer - # from the cache. - self.steps[step_idx] = (name, fitted_transformer) - - step_time_elapsed = time.time() - step_start_time - time_elapsed_so_far += step_time_elapsed - # Logging time elapsed for current step to stdout - if self.verbose: - message = '(step %d of %d) %s' % ( - step_idx + 1, len(self.steps), name) - print(message_with_time('Pipeline', message, step_time_elapsed)) + cloned_transformer = clone(transformer) + # Fit or load from cache the current transfomer + Xt, fitted_transformer = fit_transform_one_cached( + True, 'Pipeline', self._log_message(step_idx), + cloned_transformer, None, Xt, y, + **fit_params_steps[name]) + # Replace the transformer of the step with the fitted + # transformer. This is necessary when loading the transformer + # from the cache. + self.steps[step_idx] = (name, fitted_transformer) + if self._final_estimator is None: - return Xt, {}, time_elapsed_so_far - return Xt, fit_params_steps[self.steps[-1][0]], time_elapsed_so_far + return Xt, {} + return Xt, fit_params_steps[self.steps[-1][0]] def fit(self, X, y=None, **fit_params): """Fit the model @@ -268,15 +263,10 @@ def fit(self, X, y=None, **fit_params): self : Pipeline This estimator """ - Xt, fit_params, time_elapsed_so_far = self._fit(X, y, **fit_params) - final_step_start_time = time.time() + Xt, fit_params = self._fit(X, y, **fit_params) if self._final_estimator is not None: - self._final_estimator.fit(Xt, y, **fit_params) - final_step_time_elapsed = time.time() - final_step_start_time - time_elapsed_so_far += final_step_time_elapsed - if self.verbose: - self._print_final_step(final_step_time_elapsed, - time_elapsed_so_far) + with log_elapsed('Pipeline', self._log_message(-1)): + self._final_estimator.fit(Xt, y, **fit_params) return self def fit_transform(self, X, y=None, **fit_params): @@ -307,24 +297,14 @@ def fit_transform(self, X, y=None, **fit_params): Transformed samples """ last_step = self._final_estimator - Xt, fit_params, time_elapsed_so_far = self._fit(X, y, **fit_params) - final_step_start_time = time.time() + Xt, fit_params = self._fit(X, y, **fit_params) if last_step is None: - if self.verbose: - message = 'Step %s is NoneType' % (self.steps[-1][0],) - print(message_with_time('Pipeline', message, 0)) - print(message_with_time( - 'Pipeline', 'Total time elapsed', time_elapsed_so_far)) return Xt - elif hasattr(last_step, 'fit_transform'): - Xt = last_step.fit_transform(Xt, y, **fit_params) - else: - Xt = last_step.fit(Xt, y, **fit_params).transform(Xt) - final_step_time_elapsed = time.time() - final_step_start_time - time_elapsed_so_far += final_step_time_elapsed - if self.verbose: - self._print_final_step(final_step_time_elapsed, - time_elapsed_so_far) + with log_elapsed('Pipeline', self._log_message(-1)): + if hasattr(last_step, 'fit_transform'): + Xt = last_step.fit_transform(Xt, y, **fit_params) + else: + Xt = last_step.fit(Xt, y, **fit_params).transform(Xt) return Xt @if_delegate_has_method(delegate='_final_estimator') @@ -374,14 +354,9 @@ def fit_predict(self, X, y=None, **fit_params): ------- y_pred : array-like """ - Xt, fit_params, time_elapsed_so_far = self._fit(X, y, **fit_params) - final_step_start_time = time.time() - y_pred = self.steps[-1][-1].fit_predict(Xt, y, **fit_params) - final_step_time_elapsed = time.time() - final_step_start_time - time_elapsed_so_far += final_step_time_elapsed - if self.verbose: - self._print_final_step(final_step_time_elapsed, - time_elapsed_so_far) + Xt, fit_params = self._fit(X, y, **fit_params) + with log_elapsed('Pipeline', self._log_message(-1)): + y_pred = self.steps[-1][-1].fit_predict(Xt, y, **fit_params) return y_pred @if_delegate_has_method(delegate='_final_estimator') @@ -589,8 +564,9 @@ def make_pipeline(*steps, **kwargs): inspect estimators within the pipeline. Caching the transformers is advantageous when fitting is time consuming. - verbose : boolean, optional - Verbosity mode. + verbose : boolean, default=False + Verbosity mode. When enabled, the time elapsed while fitting each step + will be printed as it is completed. Examples -------- @@ -617,18 +593,6 @@ def make_pipeline(*steps, **kwargs): return Pipeline(_name_estimators(steps), memory=memory, verbose=verbose) -def _fit_one_transformer(transformer, X, y, verbose=False, idx=None, - total_steps=None, name=None): - # idx, total_steps and name are not required when verbosity is disabled - step_start_time = time.time() - transformer = transformer.fit(X, y) - step_time_elapsed = time.time() - step_start_time - if verbose: - message = '(step %d of %d) %s' % (idx + 1, total_steps, name) - print(message_with_time('FeatureUnion', message, step_time_elapsed)) - return transformer - - def _transform_one(transformer, weight, X): res = transformer.transform(X) # if we have a weight for this transformer, multiply output @@ -637,19 +601,17 @@ def _transform_one(transformer, weight, X): return res * weight -def _fit_transform_one(transformer, weight, X, y, verbose=False, idx=None, - total_steps=None, name=None, **fit_params): - # idx, total_steps and name are not required when verbosity is disabled - step_start_time = time.time() - if hasattr(transformer, 'fit_transform'): - res = transformer.fit_transform(X, y, **fit_params) - else: - res = transformer.fit(X, y, **fit_params).transform(X) - step_time_elapsed = time.time() - step_start_time - if verbose: - message = '(step %d of %d) %s' % (idx + 1, total_steps, name) - print(message_with_time('FeatureUnion', message, step_time_elapsed)) - # if we have a weight for this transformer, multiply output +def _fit_transform_one(is_transform, clsname, message, + transformer, weight, + X, y, **fit_params): + with log_elapsed(clsname, message): + if not is_transform: + return None, transformer.fit(X, y, **fit_params) + elif hasattr(transformer, 'fit_transform'): + res = transformer.fit_transform(X, y, **fit_params) + else: + res = transformer.fit(X, y, **fit_params).transform(X) + if weight is None: return res, transformer return res * weight, transformer @@ -781,22 +743,8 @@ def fit(self, X, y=None): self : FeatureUnion This estimator """ - self.transformer_list = list(self.transformer_list) - self._validate_transformers() - all_transformers = list(self._iter()) - total_steps = len(all_transformers) - # Keep a record of time elapsed - start_time = time.time() - transformers = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_one_transformer)(transformer, X, y, self.verbose, - idx, total_steps, name) - for idx, (name, transformer, _) in enumerate(all_transformers)) - time_elapsed = time.time() - start_time - if self.verbose: - print(message_with_time( - 'FeatureUnion', 'Total time elapsed:', time_elapsed)) - - self._update_transformer_list(transformers) + fit_params = {} # XXX: this should probably be added to method + self._fit_transform(X, y, fit_params, is_transform=False) return self def fit_transform(self, X, y=None, **fit_params): @@ -816,26 +764,30 @@ def fit_transform(self, X, y=None, **fit_params): hstack of results of transformers. sum_n_components is the sum of n_components (output dimension) over transformers. """ + return self._fit_transform(X, y, fit_params, is_transform=True) + + def _fit_transform(self, X, y, fit_params, is_transform): + self.transformer_list = list(self.transformer_list) self._validate_transformers() - all_transformers = list(self._iter()) - total_steps = len(all_transformers) - # Keep a record of time elapsed - start_time = time.time() + transformers = list(self._iter()) result = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_transform_one)(transformer, weight, X, y, - self.verbose, idx, total_steps, name, - **fit_params) - for idx, (name, transformer, weight) in enumerate(all_transformers)) - time_elapsed = time.time() - start_time - if self.verbose: - print(message_with_time( - 'FeatureUnion', 'Total time elapsed:', time_elapsed)) + delayed(_fit_transform_one)( + is_transform, 'FeatureUnion', + ('(step %d of %d) Fitting %s' % (idx + 1, len(transformers), + name) + if self.verbose else None), + transformer, weight, X, y, **fit_params) + for idx, (name, transformer, weight) + in enumerate(transformers)) if not result: # All transformers are None return np.zeros((X.shape[0], 0)) Xs, transformers = zip(*result) self._update_transformer_list(transformers) + if not is_transform: + return + if any(sparse.issparse(f) for f in Xs): Xs = sparse.hstack(Xs).tocsr() else: diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index ceed114d5e84e..c2e163451e711 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -5,9 +5,11 @@ from tempfile import mkdtemp import shutil import time +import re import numpy as np from scipy import sparse +import pytest from sklearn.externals.six.moves import zip from sklearn.utils.testing import assert_raises @@ -986,85 +988,39 @@ def test_make_pipeline_memory(): shutil.rmtree(cachedir) -def check_pipeline_verbosity_fit_predict(pipe_method): - # Test that the verbosity of pipeline is proper - from sklearn.externals.six.moves import cStringIO as StringIO - import sys - old_stdout = sys.stdout - sys.stdout = StringIO() - pipe_method(X=None, y=None, clf__should_succeed=True) - verbose_output = sys.stdout - sys.stdout = old_stdout - - # check output - verbose_output.seek(0) - lines = verbose_output.readlines() - assert_true('(step 1 of 2) transf' in lines[0]) - assert_true('(step 2 of 2) clf' in lines[1]) - assert_true('Total time elapsed' in lines[2]) - for line in lines: - assert line.startswith('[Pipeline]') - -def test_pipeline_fit_verbosity(): - pipe = Pipeline([('transf', Transf()), ('clf', FitParamT())], verbose=True) - yield check_pipeline_verbosity_fit_predict, pipe.fit - yield check_pipeline_verbosity_fit_predict, pipe.fit_predict - - -def check_pipeline_verbosity_fit_transform(pipe_method, last_was_none=False): - # Test that the verbosity of pipeline is proper - from sklearn.externals.six.moves import cStringIO as StringIO - import sys - old_stdout = sys.stdout - sys.stdout = StringIO() - pipe_method(X=[[1, 2, 3], [4, 5, 6]], y=[[7], [8]]) - verbose_output = sys.stdout - sys.stdout = old_stdout - - # check output - verbose_output.seek(0) - lines = verbose_output.readlines() - assert_true('(step 1 of 2) mult1' in lines[0]) - assert_true(lines[0].startswith('[Pipeline]')) - if last_was_none: - assert_true('Step mult2 is NoneType' in lines[1]) - else: - assert_true('(step 2 of 2) mult2' in lines[1]) - assert_true('Total time elapsed' in lines[2]) - - -def test_pipeline_verbosity_fit_transform(): - pipe = Pipeline([('mult1', Mult(mult=1)), ('mult2', Mult(mult=2))], - verbose=True) - yield check_pipeline_verbosity_fit_transform, pipe.fit_transform - pipe = Pipeline([('mult1', Mult(mult=1)), ('mult2', None)], - verbose=True) - yield check_pipeline_verbosity_fit_transform, pipe.fit_transform, True - - -def check_feature_union_verbosity(feature_union_method): - # Test that the verbosity of feature union is proper - from sklearn.externals.six.moves import cStringIO as StringIO - import sys - old_stdout = sys.stdout - sys.stdout = StringIO() - feature_union_method(X=[[1, 2, 3], [4, 5, 6]], y=[[7], [8]]) - verbose_output = sys.stdout - sys.stdout = old_stdout - - # check output - verbose_output.seek(0) - lines = verbose_output.readlines() - assert_true('(step 1 of 2) mult1' in lines[0]) - assert_true('(step 2 of 2) mult2' in lines[1]) - assert_true('Total time elapsed' in lines[2]) - assert_true(lines[0].startswith('[FeatureUnion]')) - assert_true(lines[1].startswith('[FeatureUnion]')) - assert_true(lines[2].startswith('[FeatureUnion]')) - - -def test_feature_union_verbosity(): - union = FeatureUnion([('mult1', Mult(mult=1)), ('mult2', Mult(mult=2))], - verbose=True) - yield check_feature_union_verbosity, union.fit - yield check_feature_union_verbosity, union.fit_transform + +@pytest.mark.parametrize(['est', 'pattern'], [ + (Pipeline([('transf', Transf()), ('clf', FitParamT())]), + r'''\[Pipeline\].*\(step 1 of 2\) Fitting transf.* elapsed=.*\n''' + r'''\[Pipeline\].*\(step 2 of 2\) Fitting clf.* elapsed=.*\n$'''), + (Pipeline([('transf', Transf()), ('clf', None)]), + r'''\[Pipeline\].*\(step 1 of 1\) Fitting transf.* elapsed=.*\n$'''), + (Pipeline([('transf', None), ('mult', Mult())]), + r'''\[Pipeline\].*\(step 1 of 1\) Fitting mult.* elapsed=.*\n$'''), + (FeatureUnion([('mult1', Mult()), ('mult2', Mult())]), + r'''\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* elapsed=.*\n''' + r'''\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* elapsed=.*\n$'''), + (FeatureUnion([('mult1', None), ('mult2', Mult()), ('mult3', None)]), + r'''\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* elapsed=.*\n$'''), +]) +@pytest.mark.parametrize('method', ['fit', 'fit_transform', 'fit_predict']) +def test_verbose(est, method, pattern, capsys): + try: + func = getattr(est, method) + except AttributeError: + return + # XXX: getattr(Pipeline(...), 'fit_transform') is always True + if (method == 'fit_transform' and hasattr(est, 'steps') and + type(est.steps[-1][1]).__name__ == 'FitParamT'): + return + + X = [[1, 2, 3], [4, 5, 6]] + y = [[7], [8]] + + est.set_params(verbose=False) + func(X, y) + assert not capsys.readouterr().out, 'Got output for verbose=False' + + est.set_params(verbose=True) + func(X, y) + assert re.match(pattern, capsys.readouterr().out) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index d715bdad2b018..2e3ca57353bbc 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -2,6 +2,8 @@ The :mod:`sklearn.utils` module includes various utilities. """ from collections import Sequence +from contextlib import contextmanager +import time as _time import numpy as np from scipy.sparse import issparse @@ -508,21 +510,46 @@ def indices_to_mask(indices, mask_length): return mask -def message_with_time(source, message, time_): +def message_with_time(source, message, time): """Create one line message for logging purposes Parameters ---------- - source: str + source : str String indicating the source or the reference of the message - message: str + message : str Short message - time_: int + time : int Time in seconds """ start_message = '[%s]' % (source,) - end_message = "%s, total=%s" % (message, logger.short_format_time(time_)) + end_message = "%s, elapsed=%s" % (message, logger.short_format_time(time)) dots_len = (68 - len(start_message) - len(end_message)) return ("%s %s %s" % (start_message, dots_len * '.', end_message)) + + +@contextmanager +def log_elapsed(source, message): + """Log elapsed time to stdout when the context is exited + + Parameters + ---------- + source : str + String indicating the source or the reference of the message + + message : str or None + Short message. If None, nothing will be printed + + Returns + ------- + context_manager + Prints elapsed time upon exit if verbose + """ + if message is None: + yield + else: + start = _time.time() + yield + print(message_with_time(source, message, _time.time() - start)) From 3950fb008caba650e9d9444504f6d2b5ba22d580 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Tue, 9 Jan 2018 22:52:02 +1100 Subject: [PATCH 14/64] Test logging utils --- sklearn/utils/tests/test_utils.py | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index fa93bf34fe6bc..e02ca27e192d8 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -1,6 +1,9 @@ from itertools import chain, product import warnings +import string +import time +import pytest import numpy as np import scipy.sparse as sp from scipy.linalg import pinv2 @@ -18,6 +21,7 @@ from sklearn.utils import safe_indexing from sklearn.utils import shuffle from sklearn.utils import gen_even_slices +from sklearn.utils import message_with_time, log_elapsed from sklearn.utils.extmath import pinvh from sklearn.utils.arpack import eigsh from sklearn.utils.mocking import MockDataFrame @@ -274,3 +278,58 @@ def test_gen_even_slices(): slices = gen_even_slices(10, -1) assert_raises_regex(ValueError, "gen_even_slices got n_packs=-1, must be" " >=1", next, slices) + + +@pytest.mark.parametrize( + ['source', 'message', 'is_long'], + [ + ('ABC', string.ascii_lowercase, False), + ('ABCDEF', string.ascii_lowercase, False), + ('ABC', string.ascii_lowercase * 3, True), + ('ABC' * 10, string.ascii_lowercase, True), + ('ABC', string.ascii_lowercase + u'\u1048', False), + ]) +@pytest.mark.parametrize( + ['time', 'time_str'], + [ + (0.2, ' 0.2s'), + (20, ' 20.0s'), + (2000, '33.3min'), + (20000, '333.3min'), + ]) +def test_message_with_time(source, message, is_long, time, time_str): + out = message_with_time(source, message, time) + if is_long: + assert len(out) > 70 + else: + assert len(out) == 70 + + assert out.startswith('[' + source + '] ') + out = out[len(source) + 3:] + + assert out.endswith(time_str) + out = out[:-len(time_str)] + assert out.endswith(', elapsed=') + out = out[:-len(', elapsed=')] + assert out.endswith(message) + out = out[:-len(message)] + assert out.endswith(' ') + out = out[:-1] + + if is_long: + assert not out + else: + assert list(set(out)) == ['.'] + + +@pytest.mark.parametrize( + ['message', 'expected'], + [ + ('hello', message_with_time('ABC', 'hello', 0.1) + '\n'), + ('', message_with_time('ABC', '', 0.1) + '\n'), + (None, ''), + ]) +def test_log_elapsed(message, expected, capsys): + with log_elapsed('ABC', message): + time.sleep(0.1) + assert capsys.readouterr().out == expected From 6807457f1e3499f9af0694273999041af51de9a4 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Tue, 9 Jan 2018 22:54:51 +1100 Subject: [PATCH 15/64] Param docstring --- sklearn/pipeline.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 0c5c87ef5c572..5af4e11fc0b6e 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -63,7 +63,8 @@ class Pipeline(_BaseComposition): transformers is advantageous when fitting is time consuming. verbose : boolean, optional - Verbosity mode. + Verbosity mode. When enabled, the time elapsed while fitting each step + will be printed as it is completed. Attributes ---------- @@ -565,8 +566,8 @@ def make_pipeline(*steps, **kwargs): transformers is advantageous when fitting is time consuming. verbose : boolean, default=False - Verbosity mode. When enabled, the time elapsed while fitting each step - will be printed as it is completed. + Verbosity mode. When enabled, the time elapsed while fitting each + transformer will be printed as it is completed. Examples -------- From badf235ad1f2664ec4cc95143ec16755e34b114f Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Tue, 16 Jan 2018 17:21:13 +1100 Subject: [PATCH 16/64] pytest<3.3 does return namedtuples --- sklearn/tests/test_pipeline.py | 4 ++-- sklearn/utils/tests/test_utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index c2e163451e711..da7936d4c1cd1 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1019,8 +1019,8 @@ def test_verbose(est, method, pattern, capsys): est.set_params(verbose=False) func(X, y) - assert not capsys.readouterr().out, 'Got output for verbose=False' + assert not capsys.readouterr()[0], 'Got output for verbose=False' est.set_params(verbose=True) func(X, y) - assert re.match(pattern, capsys.readouterr().out) + assert re.match(pattern, capsys.readouterr()[0]) diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index e02ca27e192d8..391a5166d2427 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -332,4 +332,4 @@ def test_message_with_time(source, message, is_long, time, time_str): def test_log_elapsed(message, expected, capsys): with log_elapsed('ABC', message): time.sleep(0.1) - assert capsys.readouterr().out == expected + assert capsys.readouterr()[0] == expected From 7de5ed8c757c5132f6ae9c1b582d3f4cd1273e37 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Tue, 16 Jan 2018 23:12:27 +1100 Subject: [PATCH 17/64] Don't forcefully adjust timing on Windows as joblib does --- sklearn/utils/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 2e3ca57353bbc..fdee22321afb0 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -525,7 +525,13 @@ def message_with_time(source, message, time): Time in seconds """ start_message = '[%s]' % (source,) - end_message = "%s, elapsed=%s" % (message, logger.short_format_time(time)) + + # from joblib.logger.short_format_time without the Windows -.1s adjustment + if time > 60: + time_str = "%4.1fmin" % (time / 60) + else: + time_str = " %5.1fs" % (time) + end_message = "%s, elapsed=%s" % (message, time_str) dots_len = (68 - len(start_message) - len(end_message)) return ("%s %s %s" % (start_message, dots_len * '.', end_message)) From 5dce7c1445b71ce74d708b72b78a7e3dfe32af51 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Mon, 12 Feb 2018 10:02:47 +1100 Subject: [PATCH 18/64] Need some __future__ --- sklearn/utils/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index fdee22321afb0..0b3d89a738a1e 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -1,6 +1,8 @@ """ The :mod:`sklearn.utils` module includes various utilities. """ +from __future__ import division, print_function + from collections import Sequence from contextlib import contextmanager import time as _time From b8c56d8254d971da1893467b4de24875f7dd5b62 Mon Sep 17 00:00:00 2001 From: Joel Nothman Date: Tue, 13 Feb 2018 23:06:15 +1100 Subject: [PATCH 19/64] Rm unused import --- sklearn/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 0b3d89a738a1e..71e033016f994 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -18,7 +18,7 @@ check_consistent_length, check_X_y, indexable, check_symmetric) from .class_weight import compute_class_weight, compute_sample_weight -from ..externals.joblib import cpu_count, logger +from ..externals.joblib import cpu_count from ..exceptions import DataConversionWarning from .deprecation import deprecated From 6997c8f57dd01bab062fbda377dad4f32bb3c5e6 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Tue, 26 Jun 2018 23:15:09 -0400 Subject: [PATCH 20/64] ENH: Adapts ColumnTransformer to pipeline funcs --- sklearn/compose/_column_transformer.py | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index aeedd4e3eaf4f..8e667bb7a0977 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -7,6 +7,7 @@ # Joris Van den Bossche # License: BSD from itertools import chain +from functools import partial import numpy as np from scipy import sparse @@ -15,7 +16,7 @@ from ..externals.joblib import Parallel, delayed from ..externals import six from ..pipeline import ( - _fit_one_transformer, _fit_transform_one, _transform_one, _name_estimators) + _fit_transform_one, _transform_one, _name_estimators) from ..preprocessing import FunctionTransformer from ..utils import Bunch from ..utils.metaestimators import _BaseComposition @@ -344,8 +345,9 @@ def _fit_transform(self, X, y, func, fitted=False): """ try: return Parallel(n_jobs=self.n_jobs)( - delayed(func)(clone(trans) if not fitted else trans, - X_sel, y, weight) + delayed(func)( + transformer=clone(trans) if not fitted else trans, + X=X_sel, y=y, weight=weight) for _, trans, X_sel, weight in self._iter( X=X, fitted=fitted, replace_strings=True)) except ValueError as e: @@ -375,7 +377,16 @@ def fit(self, X, y=None): self._validate_remainder(X) self._validate_transformers() - transformers = self._fit_transform(X, y, _fit_one_transformer) + fit_one_transformer = partial( + _fit_transform_one, + is_transform=False, + clsname="ColumnTransformer", + message=None + ) + + # fit_one_transformer returns (None, transformer) tuples + transformer_tuples = self._fit_transform(X, y, fit_one_transformer) + transformers = [t[1] for t in transformer_tuples] self._update_fitted_transformers(transformers) return self @@ -404,7 +415,13 @@ def fit_transform(self, X, y=None): self._validate_remainder(X) self._validate_transformers() - result = self._fit_transform(X, y, _fit_transform_one) + fit_transform_one = partial( + _fit_transform_one, + is_transform=True, + clsname="ColumnTransformer", + message=None) + + result = self._fit_transform(X, y, fit_transform_one) if not result: # All transformers are None From fb8c1234e7314c5311f57261823fb2ed9980b2cd Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 27 Jun 2018 09:06:46 -0400 Subject: [PATCH 21/64] ENH: Adds verbose to ColumnTransformer --- sklearn/compose/_column_transformer.py | 40 ++++++++--- .../compose/tests/test_column_transformer.py | 71 +++++++++++++++++-- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 8e667bb7a0977..cab2efe0e28d9 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -90,6 +90,9 @@ class ColumnTransformer(_BaseComposition, TransformerMixin): transformer is multiplied by these weights. Keys are transformer names, values the weights. + verbose : boolean, optional + Verbosity mode. + Attributes ---------- transformers_ : list @@ -141,11 +144,12 @@ class ColumnTransformer(_BaseComposition, TransformerMixin): """ def __init__(self, transformers, remainder='passthrough', n_jobs=1, - transformer_weights=None): + transformer_weights=None, verbose=False): self.transformers = transformers self.remainder = remainder self.n_jobs = n_jobs self.transformer_weights = transformer_weights + self.verbose = verbose @property def _transformers(self): @@ -335,6 +339,11 @@ def _validate_output(self, result): "The output of the '{0}' transformer should be 2D (scipy " "matrix, array, or pandas DataFrame).".format(name)) + def _log_message(self, name, idx, total): + if not self.verbose: + return None + return '(%d of %d) Fitting %s' % (idx, total, name) + def _fit_transform(self, X, y, func, fitted=False): """ Private function to fit and/or transform on demand. @@ -343,13 +352,19 @@ def _fit_transform(self, X, y, func, fitted=False): on the passed function. ``fitted=True`` ensures the fitted transformers are used. """ + transformers = list(self._iter( + X=X, fitted=fitted, replace_strings=True)) try: return Parallel(n_jobs=self.n_jobs)( delayed(func)( transformer=clone(trans) if not fitted else trans, - X=X_sel, y=y, weight=weight) - for _, trans, X_sel, weight in self._iter( - X=X, fitted=fitted, replace_strings=True)) + X=X_sel, + y=y, + weight=weight, + message=self._log_message( + name, idx, len(transformers))) + for idx, (name, trans, X_sel, + weight) in enumerate(transformers, 1)) except ValueError as e: if "Expected 2D array, got 1D array instead" in str(e): raise ValueError(_ERR_MSG_1DCOLUMN) @@ -380,8 +395,7 @@ def fit(self, X, y=None): fit_one_transformer = partial( _fit_transform_one, is_transform=False, - clsname="ColumnTransformer", - message=None + clsname='ColumnTransformer' ) # fit_one_transformer returns (None, transformer) tuples @@ -418,8 +432,7 @@ def fit_transform(self, X, y=None): fit_transform_one = partial( _fit_transform_one, is_transform=True, - clsname="ColumnTransformer", - message=None) + clsname='ColumnTransformer') result = self._fit_transform(X, y, fit_transform_one) @@ -453,7 +466,16 @@ def transform(self, X): """ check_is_fitted(self, 'transformers_') - Xs = self._fit_transform(X, None, _transform_one, fitted=True) + try: + Xs = Parallel(n_jobs=self.n_jobs)( + delayed(_transform_one)(trans, X_sel, None, weight) + for _, trans, X_sel, weight in self._iter( + X=X, fitted=True, replace_strings=True)) + except ValueError as e: + if "Expected 2D array, got 1D array instead" in str(e): + raise ValueError(_ERR_MSG_1DCOLUMN) + else: + raise self._validate_output(Xs) if not Xs: diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index f1f7f9a24474e..1164a7b8a6fc3 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -1,6 +1,7 @@ """ Test the ColumnTransformer. """ +import re import numpy as np from scipy import sparse @@ -403,7 +404,8 @@ def test_column_transformer_get_set_params(): 'trans2__with_mean': True, 'trans2__with_std': True, 'transformers': ct.transformers, - 'transformer_weights': None} + 'transformer_weights': None, + 'verbose': False} assert_dict_equal(ct.get_params(), exp) @@ -419,7 +421,8 @@ def test_column_transformer_get_set_params(): 'trans2__with_mean': True, 'trans2__with_std': True, 'transformers': ct.transformers, - 'transformer_weights': None} + 'transformer_weights': None, + 'verbose': False} assert_dict_equal(ct.get_params(), exp) @@ -743,7 +746,8 @@ def test_column_transformer_get_set_params_with_remainder(): 'trans1__with_mean': True, 'trans1__with_std': True, 'transformers': ct.transformers, - 'transformer_weights': None} + 'transformer_weights': None, + 'verbose': False} assert ct.get_params() == exp @@ -758,7 +762,8 @@ def test_column_transformer_get_set_params_with_remainder(): 'remainder__with_std': False, 'trans1': 'passthrough', 'transformers': ct.transformers, - 'transformer_weights': None} + 'transformer_weights': None, + 'verbose': False} assert ct.get_params() == exp @@ -777,3 +782,61 @@ def test_column_transformer_no_estimators(): assert len(ct.transformers_) == 1 assert ct.transformers_[-1][0] == 'remainder' assert ct.transformers_[-1][2] == [0, 1, 2] + + +@pytest.mark.parametrize(['est', 'pattern'], [ + (ColumnTransformer( + [('trans1', Trans(), [0]), + ('trans2', Trans(), [1])], + remainder=DoubleTrans()), + (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* elapsed=.*\n$')), + (ColumnTransformer( + [('trans1', Trans(), [0]), + ('trans2', Trans(), [1])], + remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* elapsed=.*\n$')), + (ColumnTransformer( + [('trans1', Trans(), [0]), + ('trans2', 'drop', [1])], + remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* elapsed=.*\n$')), + (ColumnTransformer( + [('trans1', Trans(), [0]), + ('trans2', 'passthrough', [1])], + remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* elapsed=.*\n$')), + (ColumnTransformer( + [('trans1', Trans(), [0])], + remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* elapsed=.*\n$')), + (ColumnTransformer( + [('trans1', Trans(), [0]), + ('trans2', Trans(), [1])], + remainder='drop'), + (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* elapsed=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Fitting trans2.* elapsed=.*\n$')), + (ColumnTransformer( + [('trans1', Trans(), [0])], + remainder='drop'), + (r'\[ColumnTransformer\].*\(1 of 1\) Fitting trans1.* elapsed=.*\n$')) +]) +@pytest.mark.parametrize('method', ['fit', 'fit_transform']) +def test_column_transformer_verbose(est, pattern, method, capsys): + X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T + + func = getattr(est, method) + est.set_params(verbose=False) + func(X_array) + assert not capsys.readouterr()[0], 'Got output for verbose=False' + + est.set_params(verbose=True) + func(X_array) + assert re.match(pattern, capsys.readouterr()[0]) From b3d5c8ea14d50cfdaabe2f51b3660f173a3bb2c3 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 27 Jun 2018 12:55:00 -0400 Subject: [PATCH 22/64] DOC: Fix doctest --- sklearn/compose/_column_transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index cab2efe0e28d9..b1f7bf3c7e301 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -695,7 +695,7 @@ def make_column_transformer(*transformers, **kwargs): ['numerical_column']), ('onehotencoder', OneHotEncoder(...), - ['categorical_column'])]) + ['categorical_column'])], verbose=False) """ n_jobs = kwargs.pop('n_jobs', 1) From 3f658bd8f46f0dce431636ca10b0af5fc1e8bd63 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 27 Jun 2018 22:03:12 -0400 Subject: [PATCH 23/64] TST: Adds test for _ERR_MSG_1DCOLUMN --- .../compose/tests/test_column_transformer.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index 1164a7b8a6fc3..4f9bf787b9d31 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -22,6 +22,7 @@ from sklearn.exceptions import NotFittedError from sklearn.preprocessing import StandardScaler, Normalizer from sklearn.feature_extraction import DictVectorizer +from sklearn.compose._column_transformer import _ERR_MSG_1DCOLUMN class Trans(BaseEstimator): @@ -840,3 +841,31 @@ def test_column_transformer_verbose(est, pattern, method, capsys): est.set_params(verbose=True) func(X_array) assert re.match(pattern, capsys.readouterr()[0]) + + +@pytest.mark.parametrize('method', ['fit', 'fit_transform', 'transform']) +def test_column_transformer_raises_expected_2D_array(method): + X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T + + class TransRaiseExpected2D(BaseEstimator): + def __init__(self, error_method): + self.error_method = error_method + self.__setattr__(error_method, self._raise_error) + + def _raise_error(self, X, y=None): + raise ValueError("Expected 2D array, got 1D array instead") + + def fit(self, X, y=None): + return self + + def transform(self, X, y=None): + return X + + ct = ColumnTransformer([('trans1', TransRaiseExpected2D(method), 0)]) + + # fit needs to be called first to test transform + if method == 'transform': + ct.fit(X_array) + + assert_raise_message(ValueError, _ERR_MSG_1DCOLUMN, + getattr(ct, method), X_array) From 02edbc410fe2e5a227d9dd25e045911e9cc95f76 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Thu, 19 Jul 2018 19:39:47 -0400 Subject: [PATCH 24/64] RFC: Simplify transform --- sklearn/compose/_column_transformer.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index a9d7215500f81..234c544acab56 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -467,17 +467,7 @@ def transform(self, X): """ check_is_fitted(self, 'transformers_') - - try: - Xs = Parallel(n_jobs=self.n_jobs)( - delayed(_transform_one)(trans, X_sel, None, weight) - for _, trans, X_sel, weight in self._iter( - X=X, fitted=True, replace_strings=True)) - except ValueError as e: - if "Expected 2D array, got 1D array instead" in str(e): - raise ValueError(_ERR_MSG_1DCOLUMN) - else: - raise + Xs = self._fit_transform(X, None, _transform_one, fitted=True) self._validate_output(Xs) if not Xs: From 8df40b61342140d205cfb2bcb636fc155b7706a2 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Thu, 19 Jul 2018 19:40:27 -0400 Subject: [PATCH 25/64] RFC: Removes unrelated test --- .../compose/tests/test_column_transformer.py | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index 0c958fc2eced1..b11ebd6c30d60 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -858,34 +858,6 @@ def test_column_transformer_verbose(est, pattern, method, capsys): assert re.match(pattern, capsys.readouterr()[0]) -@pytest.mark.parametrize('method', ['fit', 'fit_transform', 'transform']) -def test_column_transformer_raises_expected_2D_array(method): - X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T - - class TransRaiseExpected2D(BaseEstimator): - def __init__(self, error_method): - self.error_method = error_method - self.__setattr__(error_method, self._raise_error) - - def _raise_error(self, X, y=None): - raise ValueError("Expected 2D array, got 1D array instead") - - def fit(self, X, y=None): - return self - - def transform(self, X, y=None): - return X - - ct = ColumnTransformer([('trans1', TransRaiseExpected2D(method), 0)]) - - # fit needs to be called first to test transform - if method == 'transform': - ct.fit(X_array) - - assert_raise_message(ValueError, _ERR_MSG_1DCOLUMN, - getattr(ct, method), X_array) - - def test_column_transformer_callable_specifier(): # assert that function gets the full array / dataframe X_array = np.array([[0, 1, 2], [2, 4, 6]]).T From 5ab53c957d7abb2a3a06894aa37db383cbb5a0ef Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Thu, 26 Jul 2018 10:24:51 -0400 Subject: [PATCH 26/64] STY: Fix --- sklearn/compose/tests/test_column_transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index 2a080b00fb678..269a06eedc6d4 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -915,7 +915,7 @@ def test_column_transformer_verbose(est, pattern, method, capsys): est.set_params(verbose=True) func(X_array) assert re.match(pattern, capsys.readouterr()[0]) - + def test_column_transformer_no_estimators_set_params(): ct = ColumnTransformer([]).set_params(n_jobs=2) From ed781381649fdfefa01c2f62837745a1c5c195c9 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Thu, 26 Jul 2018 11:33:01 -0400 Subject: [PATCH 27/64] MRG: Fix --- sklearn/pipeline.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index b4bf6b73292ca..0ffc1c740ad25 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -221,12 +221,7 @@ def _fit(self, X, y=None, **fit_params): Xt = X for step_idx, (name, transformer) in enumerate(self.steps[:-1]): if transformer is None: - continue - - if hasattr(memory, 'cachedir') and memory.cachedir is None: - # we do not clone when caching is disabled to preserve - # backward compatibility - cloned_transformer = transformer + pass else: if hasattr(memory, 'location'): # joblib >= 0.12 @@ -246,15 +241,15 @@ def _fit(self, X, y=None, **fit_params): cloned_transformer = clone(transformer) else: cloned_transformer = clone(transformer) - # Fit or load from cache the current transfomer - Xt, fitted_transformer = fit_transform_one_cached( - True, 'Pipeline', self._log_message(step_idx), - cloned_transformer, None, Xt, y, - **fit_params_steps[name]) - # Replace the transformer of the step with the fitted - # transformer. This is necessary when loading the transformer - # from the cache. - self.steps[step_idx] = (name, fitted_transformer) + # Fit or load from cache the current transfomer + Xt, fitted_transformer = fit_transform_one_cached( + True, 'Pipeline', self._log_message(step_idx), + cloned_transformer, None, Xt, y, + **fit_params_steps[name]) + # Replace the transformer of the step with the fitted + # transformer. This is necessary when loading the transformer + # from the cache. + self.steps[step_idx] = (name, fitted_transformer) if self._final_estimator is None: return Xt, {} return Xt, fit_params_steps[self.steps[-1][0]] From ebae161dea64910db16f2131d715f06bfabc72b1 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Tue, 21 Aug 2018 22:15:10 -0400 Subject: [PATCH 28/64] DOC: Adds docstring --- sklearn/pipeline.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index b27b882b8791d..1c0c364513c58 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -631,6 +631,16 @@ def _transform_one(transformer, X, y, weight, **fit_params): def _fit_transform_one(is_transform, clsname, message, transformer, weight, X, y, **fit_params): + """ + Fits ``transformer`` to ``X`` and ``y``. + + If ``is_transform`` is ``True``, then ``X``, ``y`` will be transformed. The + transformed result is returned with the fitted transformer. If ``weight`` + is not ``None``, the result will be multipled by ``weight``. + + If ``is_transform`` is ``False``, then a tuple of + (``None``, fitted_transformer) will be returned. + """ with log_elapsed(clsname, message): if not is_transform: return None, transformer.fit(X, y, **fit_params) From ab5a9717648cfc6afa92536263bb2e07fe533d8a Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 22 Aug 2018 08:21:10 -0400 Subject: [PATCH 29/64] BUG: n_jobs defaults to None --- sklearn/compose/_column_transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 823b278ab3b60..85bc083774ff8 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -164,7 +164,7 @@ def __init__(self, transformers, remainder='drop', sparse_threshold=0.3, - n_jobs=1, + n_jobs=None, transformer_weights=None, verbose=False): self.transformers = transformers From 6d87aac181c88d81883b7758a8527fe0d868463c Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 22 Aug 2018 10:35:16 -0400 Subject: [PATCH 30/64] BUG: Defaults n_jobs to None --- sklearn/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 1c0c364513c58..e3497864e4fd5 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -706,7 +706,7 @@ class FeatureUnion(_BaseComposition, TransformerMixin): def __init__(self, transformer_list, - n_jobs=1, + n_jobs=None, transformer_weights=None, verbose=False): self.transformer_list = transformer_list @@ -927,7 +927,7 @@ def make_union(*transformers, **kwargs): random_state=None, tol=0.0))], transformer_weights=None, verbose=False) """ - n_jobs = kwargs.pop('n_jobs', 1) + n_jobs = kwargs.pop('n_jobs', None) verbose = kwargs.pop('verbose', False) if kwargs: # We do not currently support `transformer_weights` as we may want to From 6e501b2755580c59bc3d8c42b429197c04d9f095 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Mon, 8 Oct 2018 09:40:11 -0400 Subject: [PATCH 31/64] STY: Removes formatting diffs --- sklearn/compose/_column_transformer.py | 47 ++++++++++++-------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 646681def3897..2d818d6f428b7 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -188,10 +188,9 @@ def _transformers(self): @_transformers.setter def _transformers(self, value): - self.transformers = [(name, trans, col) - for ((name, trans), - (_, _, - col)) in zip(value, self.transformers)] + self.transformers = [ + (name, trans, col) for ((name, trans), (_, _, col)) + in zip(value, self.transformers)] def get_params(self, deep=True): """Get parameters for this estimator. @@ -249,8 +248,7 @@ def _iter(self, fitted=False, replace_strings=False): # skip in case of 'drop' if trans == 'passthrough': trans = FunctionTransformer( - validate=False, - accept_sparse=True, + validate=False, accept_sparse=True, check_inverse=False) elif trans == 'drop': continue @@ -272,12 +270,12 @@ def _validate_transformers(self): for t in transformers: if t in ('drop', 'passthrough'): continue - if (not (hasattr(t, "fit") or hasattr(t, "fit_transform")) - or not hasattr(t, "transform")): - raise TypeError( - "All estimators should implement fit and " - "transform, or can be 'drop' or 'passthrough' " - "specifiers. '%s' (type %s) doesn't." % (t, type(t))) + if (not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not + hasattr(t, "transform")): + raise TypeError("All estimators should implement fit and " + "transform, or can be 'drop' or 'passthrough' " + "specifiers. '%s' (type %s) doesn't." % + (t, type(t))) def _validate_column_callables(self, X): """ @@ -323,8 +321,8 @@ def named_transformers_(self): """ # Use Bunch object to improve autocomplete - return Bunch(**dict([(name, trans) - for name, trans, _ in self.transformers_])) + return Bunch(**dict([(name, trans) for name, trans, _ + in self.transformers_])) def get_feature_names(self): """Get feature names from all transformers. @@ -345,10 +343,10 @@ def get_feature_names(self): "a 'passthrough' transformer.") elif not hasattr(trans, 'get_feature_names'): raise AttributeError("Transformer %s (type %s) does not " - "provide get_feature_names." % - (str(name), type(trans).__name__)) - feature_names.extend( - [name + "__" + f for f in trans.get_feature_names()]) + "provide get_feature_names." + % (str(name), type(trans).__name__)) + feature_names.extend([name + "__" + f for f in + trans.get_feature_names()]) return feature_names def _update_fitted_transformers(self, transformers): @@ -484,9 +482,8 @@ def fit_transform(self, X, y=None): self.sparse_output_ = True elif any(sparse.issparse(X) for X in Xs): nnz = sum(X.nnz if sparse.issparse(X) else X.size for X in Xs) - total = sum( - X.shape[0] * X.shape[1] if sparse.issparse(X) else X.size - for X in Xs) + total = sum(X.shape[0] * X.shape[1] if sparse.issparse(X) + else X.size for X in Xs) density = nnz / total self.sparse_output_ = density < self.sparse_threshold else: @@ -580,8 +577,8 @@ def _check_key_type(key, superclass): if isinstance(key, superclass): return True if isinstance(key, slice): - return (isinstance(key.start, (superclass, type(None))) - and isinstance(key.stop, (superclass, type(None)))) + return (isinstance(key.start, (superclass, type(None))) and + isinstance(key.stop, (superclass, type(None)))) if isinstance(key, list): return all(isinstance(x, superclass) for x in key) if hasattr(key, 'dtype'): @@ -794,8 +791,8 @@ def make_column_transformer(*transformers, **kwargs): remainder = kwargs.pop('remainder', 'drop') sparse_threshold = kwargs.pop('sparse_threshold', 0.3) if kwargs: - raise TypeError('Unknown keyword arguments: "{}"'.format( - list(kwargs.keys())[0])) + raise TypeError('Unknown keyword arguments: "{}"' + .format(list(kwargs.keys())[0])) transformer_list = _get_transformer_list(transformers) return ColumnTransformer(transformer_list, n_jobs=n_jobs, remainder=remainder, From 4924d1ae845cc9af5ed9f04ae836ce5f707b92a7 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Mon, 8 Oct 2018 10:05:47 -0400 Subject: [PATCH 32/64] DOC: Rewords comment --- sklearn/tests/test_pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 4921270a74be4..68b62f615503d 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1054,7 +1054,8 @@ def test_verbose(est, method, pattern, capsys): func = getattr(est, method) except AttributeError: return - # XXX: getattr(Pipeline(...), 'fit_transform') is always True + # XXX: skips test when method is `fit_transform` and when the last step is + # `FitParamT` since `FitParamT` does not define a `transform` method if (method == 'fit_transform' and hasattr(est, 'steps') and type(est.steps[-1][1]).__name__ == 'FitParamT'): return From 87629ecff02b733675e918495d541c0dd4d182b3 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Mon, 8 Oct 2018 10:38:13 -0400 Subject: [PATCH 33/64] STY: Removes formatting --- sklearn/compose/_column_transformer.py | 2 ++ sklearn/pipeline.py | 28 +++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 2d818d6f428b7..dd22769dfc966 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -23,8 +23,10 @@ from ..utils.metaestimators import _BaseComposition from ..utils.validation import check_array, check_is_fitted + __all__ = ['ColumnTransformer', 'make_column_transformer'] + _ERR_MSG_1DCOLUMN = ("1D data passed to a transformer that expects 2D data. " "Try to specify the column selection as a list of one " "item instead of a scalar.") diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 971d101f5d3ce..0f921acb7ee2c 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -173,9 +173,9 @@ def _validate_steps(self): # We allow last estimator to be None as an identity transformation if estimator is not None and not hasattr(estimator, "fit"): - raise TypeError( - "Last step of Pipeline should implement fit. " - "'%s' (type %s) doesn't" % (estimator, type(estimator))) + raise TypeError("Last step of Pipeline should implement fit. " + "'%s' (type %s) doesn't" + % (estimator, type(estimator))) @property def _estimator_type(self): @@ -212,8 +212,8 @@ def _fit(self, X, y=None, **fit_params): fit_transform_one_cached = memory.cache(_fit_transform_one) - fit_params_steps = dict( - (name, {}) for name, step in self.steps if step is not None) + fit_params_steps = dict((name, {}) for name, step in self.steps + if step is not None) for pname, pval in six.iteritems(fit_params): step, param = pname.split('__', 1) fit_params_steps[step][param] = pval @@ -753,11 +753,11 @@ def _validate_transformers(self): for t in transformers: if t is None or t == 'drop': continue - if (not (hasattr(t, "fit") or hasattr(t, "fit_transform")) - or not hasattr(t, "transform")): - raise TypeError( - "All estimators should implement fit and " - "transform. '%s' (type %s) doesn't" % (t, type(t))) + if (not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not + hasattr(t, "transform")): + raise TypeError("All estimators should implement fit and " + "transform. '%s' (type %s) doesn't" % + (t, type(t))) def _iter(self): """ @@ -781,10 +781,10 @@ def get_feature_names(self): for name, trans, weight in self._iter(): if not hasattr(trans, 'get_feature_names'): raise AttributeError("Transformer %s (type %s) does not " - "provide get_feature_names." % - (str(name), type(trans).__name__)) - feature_names.extend( - [name + "__" + f for f in trans.get_feature_names()]) + "provide get_feature_names." + % (str(name), type(trans).__name__)) + feature_names.extend([name + "__" + f for f in + trans.get_feature_names()]) return feature_names def fit(self, X, y=None): From c9d3f37c7e804841a0eeb2a61419e8d2d028f24f Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Mon, 8 Oct 2018 10:38:39 -0400 Subject: [PATCH 34/64] BUG: Fixes --- sklearn/compose/_column_transformer.py | 2 +- sklearn/model_selection/_validation.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index dd22769dfc966..64cc95a42b6e9 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -401,7 +401,7 @@ def _fit_transform(self, X, y, func, fitted=False): ``fitted=True`` ensures the fitted transformers are used. """ transformers = list( - self._iter(X=X, fitted=fitted, replace_strings=True)) + self._iter(fitted=fitted, replace_strings=True)) try: return Parallel(n_jobs=self.n_jobs)( delayed(func)( diff --git a/sklearn/model_selection/_validation.py b/sklearn/model_selection/_validation.py index 4238bb53b71a9..1725720eda87a 100644 --- a/sklearn/model_selection/_validation.py +++ b/sklearn/model_selection/_validation.py @@ -26,7 +26,6 @@ from ..utils.validation import _is_arraylike, _num_samples from ..utils.metaestimators import _safe_split from ..utils import Parallel, delayed -from ..utils._joblib import logger from ..externals.six.moves import zip from ..metrics.scorer import check_scoring, _check_multimetric_scoring from ..exceptions import FitFailedWarning From 5e1dc7953c16370dc40e17d08c4fbbd6bd2934a8 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Mon, 8 Oct 2018 10:51:28 -0400 Subject: [PATCH 35/64] RFC: Clearly states the max length to be 70 --- sklearn/utils/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index cd962fe910ac3..114858a563e5d 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -546,16 +546,16 @@ def message_with_time(source, message, time): time : int Time in seconds """ - start_message = '[%s]' % (source,) + start_message = "[%s] " % (source,) # from joblib.logger.short_format_time without the Windows -.1s adjustment if time > 60: time_str = "%4.1fmin" % (time / 60) else: time_str = " %5.1fs" % (time) - end_message = "%s, elapsed=%s" % (message, time_str) - dots_len = (68 - len(start_message) - len(end_message)) - return ("%s %s %s" % (start_message, dots_len * '.', end_message)) + end_message = " %s, elapsed=%s" % (message, time_str) + dots_len = (70 - len(start_message) - len(end_message)) + return "%s%s%s" % (start_message, dots_len * '.', end_message) @contextmanager From d50eebee38a42a9e9be920e20d080e8da207acdb Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Mon, 8 Oct 2018 11:06:23 -0400 Subject: [PATCH 36/64] DOC: Fix --- doc/modules/compose.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/compose.rst b/doc/modules/compose.rst index f9e49b9e4de28..2d5bc83f04e12 100644 --- a/doc/modules/compose.rst +++ b/doc/modules/compose.rst @@ -363,7 +363,7 @@ and ignored by setting to ``'drop'``:: ... # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS FeatureUnion(n_jobs=None, transformer_list=[('linear_pca', PCA(copy=True,...)), - ('kernel_pca', None)], + ('kernel_pca', 'drop')], transformer_weights=None, verbose=False) .. topic:: Examples: From b7d629d1b66c859da86fcdab243f9403c90d0b7d Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Mon, 8 Oct 2018 11:07:14 -0400 Subject: [PATCH 37/64] BUG: Fix --- sklearn/tests/test_pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 68b62f615503d..6f3468c153da8 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -10,7 +10,6 @@ import pytest import numpy as np from scipy import sparse -import pytest from sklearn.externals.six.moves import zip from sklearn.utils.testing import assert_raises From 4e6bc321c424d1d13b70cb6deae2d5ed60bf4628 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 17 Oct 2018 21:44:47 -0400 Subject: [PATCH 38/64] RFC: Removes import --- sklearn/utils/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 114858a563e5d..87b5a3404c356 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -3,7 +3,6 @@ """ from __future__ import division, print_function -from collections import Sequence from contextlib import contextmanager import time as _time import numbers From 8550d726b257bb19de5c3ffabd583b2faed451c4 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 12:40:27 -0400 Subject: [PATCH 39/64] ENH: Address comments --- .../compose/tests/test_column_transformer.py | 2 +- sklearn/pipeline.py | 27 +++++++++++++------ sklearn/tests/test_pipeline.py | 2 +- sklearn/utils/__init__.py | 5 ++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index e30e82c14ad3b..ddf6e5d825ad2 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -1010,7 +1010,7 @@ def test_column_transformer_verbose(est, pattern, method, capsys): func = getattr(est, method) est.set_params(verbose=False) func(X_array) - assert not capsys.readouterr()[0], 'Got output for verbose=False' + assert not capsys.readouterr().out, 'Got output for verbose=False' est.set_params(verbose=True) func(X_array) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 0f921acb7ee2c..0bb7bfe4de9fb 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -165,8 +165,8 @@ def _validate_steps(self): for t in transformers: if t is None: continue - if (not (hasattr(t, "fit") or hasattr(t, "fit_transform")) - or not hasattr(t, "transform")): + if (not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not + hasattr(t, "transform")): raise TypeError("All intermediate steps should be " "transformers and implement fit and transform." " '%s' (type %s) doesn't" % (t, type(t))) @@ -193,8 +193,6 @@ def _final_estimator(self): def _log_message(self, step_idx): if not self.verbose: return - if step_idx < 0: - step_idx = len(self.steps) + step_idx n_steps = len([est for _, est in self.steps if est is not None]) step_num = len( [est for _, est in self.steps[:step_idx + 1] if est is not None]) @@ -616,8 +614,8 @@ def make_pipeline(*steps, **kwargs): memory = kwargs.pop('memory', None) verbose = kwargs.pop('verbose', False) if kwargs: - raise TypeError('Unknown keyword arguments: "{}"'.format( - list(kwargs.keys())[0])) + raise TypeError('Unknown keyword arguments: "{}"' + .format(list(kwargs.keys())[0])) return Pipeline(_name_estimators(steps), memory=memory, verbose=verbose) @@ -629,8 +627,14 @@ def _transform_one(transformer, X, y, weight, **fit_params): return res * weight -def _fit_transform_one(is_transform, clsname, message, transformer, weight, X, - y, **fit_params): +def _fit_transform_one(transformer, + weight, + X, + y, + is_transform=None, + clsname=None, + message=None, + **fit_params): """ Fits ``transformer`` to ``X`` and ``y``. @@ -640,7 +644,14 @@ def _fit_transform_one(is_transform, clsname, message, transformer, weight, X, If ``is_transform`` is ``False``, then a tuple of (``None``, fitted_transformer) will be returned. + + If ``is_transform`` is ``None``, then it is set to true + when ``transformer`` is a ``TransformerMixin``. """ + if clsname is None: + clsname = transformer.__class__.__name__ + if is_transform is None: + is_transform = isinstance(transformer, TransformerMixin) with log_elapsed(clsname, message): if not is_transform: return None, transformer.fit(X, y, **fit_params) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 6f3468c153da8..8cac67b200831 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1064,7 +1064,7 @@ def test_verbose(est, method, pattern, capsys): est.set_params(verbose=False) func(X, y) - assert not capsys.readouterr()[0], 'Got output for verbose=False' + assert not capsys.readouterr().out, 'Got output for verbose=False' est.set_params(verbose=True) func(X, y) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 87b5a3404c356..d74f42798401e 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -547,7 +547,8 @@ def message_with_time(source, message, time): """ start_message = "[%s] " % (source,) - # from joblib.logger.short_format_time without the Windows -.1s adjustment + # adapted from joblib.logger.short_format_time without the Windows -.1s + # adjustment if time > 60: time_str = "%4.1fmin" % (time / 60) else: @@ -558,7 +559,7 @@ def message_with_time(source, message, time): @contextmanager -def log_elapsed(source, message): +def log_elapsed(source, message=None): """Log elapsed time to stdout when the context is exited Parameters From dae9a411eac627d2e9bdafb030d0d271a06b29c6 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 12:59:04 -0400 Subject: [PATCH 40/64] ENH: Uses timeit --- sklearn/utils/__init__.py | 8 +++++--- sklearn/utils/tests/test_utils.py | 9 +++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index d74f42798401e..04f9b75b488c1 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -4,7 +4,7 @@ from __future__ import division, print_function from contextlib import contextmanager -import time as _time +import timeit import numbers import platform import struct @@ -578,9 +578,11 @@ def log_elapsed(source, message=None): if message is None: yield else: - start = _time.time() + start = timeit.default_timer() yield - print(message_with_time(source, message, _time.time() - start)) + print( + message_with_time(source, message, + timeit.default_timer() - start)) def get_chunk_n_rows(row_bytes, max_n_rows=None, diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index a438ad74caa3e..72320a14bb1da 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -1,7 +1,7 @@ from itertools import chain, product import warnings import string -import time +import timeit import pytest import numpy as np @@ -314,10 +314,11 @@ def test_message_with_time(source, message, is_long, time, time_str): ('', message_with_time('ABC', '', 0.1) + '\n'), (None, ''), ]) -def test_log_elapsed(message, expected, capsys): +def test_log_elapsed(message, expected, capsys, monkeypatch): + monkeypatch.setattr(timeit, 'default_timer', lambda: 0) with log_elapsed('ABC', message): - time.sleep(0.1) - assert capsys.readouterr()[0] == expected + monkeypatch.setattr(timeit, 'default_timer', lambda: 0.1) + assert capsys.readouterr().out == expected @pytest.mark.parametrize("value, result", [(float("nan"), True), From a778069865eaf9bd9c281f02ac15d8cf46a06453 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 13:41:30 -0400 Subject: [PATCH 41/64] BUG: Fix --- sklearn/pipeline.py | 26 ++++++++++++++++++-------- sklearn/tests/test_pipeline.py | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 0bb7bfe4de9fb..377f81c29f519 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -193,6 +193,8 @@ def _final_estimator(self): def _log_message(self, step_idx): if not self.verbose: return + if step_idx < 0: + step_idx = len(self.steps) + step_idx n_steps = len([est for _, est in self.steps if est is not None]) step_num = len( [est for _, est in self.steps[:step_idx + 1] if est is not None]) @@ -240,8 +242,10 @@ def _fit(self, X, y=None, **fit_params): cloned_transformer = clone(transformer) # Fit or load from cache the current transfomer Xt, fitted_transformer = fit_transform_one_cached( - True, 'Pipeline', self._log_message(step_idx), - cloned_transformer, None, Xt, y, **fit_params_steps[name]) + cloned_transformer, None, Xt, y, + is_transform=True, clsname='Pipeline', + message=self._log_message(step_idx), + **fit_params_steps[name]) # Replace the transformer of the step with the fitted # transformer. This is necessary when loading the transformer # from the cache. @@ -837,16 +841,22 @@ def fit_transform(self, X, y=None, **fit_params): """ return self._fit_transform(X, y, fit_params, is_transform=True) + def _log_message(self, name, idx, total): + if not self.verbose: + return None + return '(step %d of %d) Fitting %s' % (idx, total, name) + def _fit_transform(self, X, y, fit_params, is_transform): self.transformer_list = list(self.transformer_list) self._validate_transformers() transformers = list(self._iter()) - result = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_transform_one)(is_transform, 'FeatureUnion', ( - '(step %d of %d) Fitting %s' % - (idx + 1, len(transformers), name) if self.verbose else None - ), transformer, weight, X, y, **fit_params) - for idx, (name, transformer, weight) in enumerate(transformers)) + result = Parallel(n_jobs=self.n_jobs)(delayed(_fit_transform_one)( + transformer, weight, X, y, + is_transform=is_transform, + clsname='FeatureUnion', + message=self._log_message(name, idx, len(transformers)), + **fit_params) for idx, (name, transformer, + weight) in enumerate(transformers, 1)) if not result: # All transformers are None diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 8cac67b200831..b78e04b3a9349 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1068,4 +1068,4 @@ def test_verbose(est, method, pattern, capsys): est.set_params(verbose=True) func(X, y) - assert re.match(pattern, capsys.readouterr()[0]) + assert re.match(pattern, capsys.readouterr().out) From c9ab7b94e4db1fd5c06689fdd8b0bcf70c0748c9 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 14:32:34 -0400 Subject: [PATCH 42/64] RFC: Does not handle wrapping --- sklearn/pipeline.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 377f81c29f519..7940cbd6ddb0c 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -193,8 +193,6 @@ def _final_estimator(self): def _log_message(self, step_idx): if not self.verbose: return - if step_idx < 0: - step_idx = len(self.steps) + step_idx n_steps = len([est for _, est in self.steps if est is not None]) step_num = len( [est for _, est in self.steps[:step_idx + 1] if est is not None]) @@ -282,7 +280,7 @@ def fit(self, X, y=None, **fit_params): """ Xt, fit_params = self._fit(X, y, **fit_params) if self._final_estimator is not None: - with log_elapsed('Pipeline', self._log_message(-1)): + with log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): self._final_estimator.fit(Xt, y, **fit_params) return self @@ -317,7 +315,7 @@ def fit_transform(self, X, y=None, **fit_params): Xt, fit_params = self._fit(X, y, **fit_params) if last_step is None: return Xt - with log_elapsed('Pipeline', self._log_message(-1)): + with log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): if hasattr(last_step, 'fit_transform'): Xt = last_step.fit_transform(Xt, y, **fit_params) else: @@ -380,7 +378,7 @@ def fit_predict(self, X, y=None, **fit_params): y_pred : array-like """ Xt, fit_params = self._fit(X, y, **fit_params) - with log_elapsed('Pipeline', self._log_message(-1)): + with log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): y_pred = self.steps[-1][-1].fit_predict(Xt, y, **fit_params) return y_pred From dc917def6455bbffe11be2d97ae8e478d78e260b Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 14:41:04 -0400 Subject: [PATCH 43/64] RFC: Removes partial --- sklearn/compose/_column_transformer.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 2b1110a7d4829..a80db839429c6 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -9,7 +9,6 @@ from __future__ import division from itertools import chain -from functools import partial import numpy as np from scipy import sparse @@ -405,9 +404,11 @@ def _fit_transform(self, X, y, func, fitted=False): return Parallel(n_jobs=self.n_jobs)( delayed(func)( transformer=clone(trans) if not fitted else trans, + weight=weight, X=_get_column(X, column), y=y, - weight=weight, + is_transform=True, + clsname='ColumnTransformer', message=self._log_message(name, idx, len(transformers))) for idx, (name, trans, column, weight) in enumerate( self._iter(fitted=fitted, replace_strings=True), 1)) @@ -466,10 +467,7 @@ def fit_transform(self, X, y=None): self._validate_column_callables(X) self._validate_remainder(X) - fit_transform_one = partial( - _fit_transform_one, is_transform=True, clsname='ColumnTransformer') - - result = self._fit_transform(X, y, fit_transform_one) + result = self._fit_transform(X, y, _fit_transform_one) if not result: self._update_fitted_transformers([]) From 795e1d599f01c6dd34729df00c0e34edd16732dd Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 14:41:17 -0400 Subject: [PATCH 44/64] DOC: Moves what_new message up --- doc/whats_new/v0.20.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/whats_new/v0.20.rst b/doc/whats_new/v0.20.rst index 375fc2c33dba5..86a0745e7dd2a 100644 --- a/doc/whats_new/v0.20.rst +++ b/doc/whats_new/v0.20.rst @@ -75,6 +75,16 @@ Changelog avoid pickling errors caused by the serialization of their methods. :issue:`12171` by :user:`Thomas Moreau `. +:mod:`sklearn.pipeline` +....................... + +- |Feature| Added optional parameter ``verbose`` in :class:`pipeline.Pipeline`, + :class:`compose.ColumnTransformer` and :class:`pipeline.FeatureUnion` + and corresponding ``make_`` helpers for showing progress and timing of + each step. :issue:`9668` by :user:`Baze Petrushev `, + :user:`Karan Desai `, `Joel Nothman`_, and + :user:`thomasjpfan`. + :mod:`sklearn.preprocessing` ........................ @@ -968,13 +978,6 @@ Support for Python 3.3 has been officially dropped. - |API| :class:`pipeline.FeatureUnion` now supports ``'drop'`` as a transformer to drop features. :issue:`11144` by :user:`thomasjpfan`. -- |Feature| Added optional parameter ``verbose`` in :class:`pipeline.Pipeline`, - :class:`compose.ColumnTransformer` and :class:`pipeline.FeatureUnion` - and corresponding ``make_`` helpers for showing progress and timing of - each step. :issue:`9668` by :user:`Baze Petrushev `, - :user:`Karan Desai `, `Joel Nothman`_, and - :user:`thomasjpfan`. - :mod:`sklearn.preprocessing` ............................ From 1f240fa827479f6ae2b6cb310ea228802c12f44e Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 14:45:16 -0400 Subject: [PATCH 45/64] RFC: Renames to message_clsname --- sklearn/compose/_column_transformer.py | 2 +- sklearn/pipeline.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index a80db839429c6..03b65e2f85ca7 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -408,7 +408,7 @@ def _fit_transform(self, X, y, func, fitted=False): X=_get_column(X, column), y=y, is_transform=True, - clsname='ColumnTransformer', + message_clsname='ColumnTransformer', message=self._log_message(name, idx, len(transformers))) for idx, (name, trans, column, weight) in enumerate( self._iter(fitted=fitted, replace_strings=True), 1)) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 7940cbd6ddb0c..190422a3cdd68 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -241,7 +241,7 @@ def _fit(self, X, y=None, **fit_params): # Fit or load from cache the current transfomer Xt, fitted_transformer = fit_transform_one_cached( cloned_transformer, None, Xt, y, - is_transform=True, clsname='Pipeline', + is_transform=True, message_clsname='Pipeline', message=self._log_message(step_idx), **fit_params_steps[name]) # Replace the transformer of the step with the fitted @@ -634,7 +634,7 @@ def _fit_transform_one(transformer, X, y, is_transform=None, - clsname=None, + message_clsname=None, message=None, **fit_params): """ @@ -649,12 +649,14 @@ def _fit_transform_one(transformer, If ``is_transform`` is ``None``, then it is set to true when ``transformer`` is a ``TransformerMixin``. + + If ``message_clsname`` is ``None``, the ``transformer`` class name is used. """ - if clsname is None: - clsname = transformer.__class__.__name__ + if message_clsname is None: + message_clsname = transformer.__class__.__name__ if is_transform is None: is_transform = isinstance(transformer, TransformerMixin) - with log_elapsed(clsname, message): + with log_elapsed(message_clsname, message): if not is_transform: return None, transformer.fit(X, y, **fit_params) elif hasattr(transformer, 'fit_transform'): @@ -851,7 +853,7 @@ def _fit_transform(self, X, y, fit_params, is_transform): result = Parallel(n_jobs=self.n_jobs)(delayed(_fit_transform_one)( transformer, weight, X, y, is_transform=is_transform, - clsname='FeatureUnion', + message_clsname='FeatureUnion', message=self._log_message(name, idx, len(transformers)), **fit_params) for idx, (name, transformer, weight) in enumerate(transformers, 1)) From 4edff5d6180a1ca4f0e92eb388a08b9963f9c20d Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 14:51:02 -0400 Subject: [PATCH 46/64] RFC: Convert back to original order --- sklearn/compose/_column_transformer.py | 2 +- sklearn/pipeline.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 03b65e2f85ca7..1d746692efab0 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -404,9 +404,9 @@ def _fit_transform(self, X, y, func, fitted=False): return Parallel(n_jobs=self.n_jobs)( delayed(func)( transformer=clone(trans) if not fitted else trans, - weight=weight, X=_get_column(X, column), y=y, + weight=weight, is_transform=True, message_clsname='ColumnTransformer', message=self._log_message(name, idx, len(transformers))) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 190422a3cdd68..9017c872495b9 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -240,7 +240,7 @@ def _fit(self, X, y=None, **fit_params): cloned_transformer = clone(transformer) # Fit or load from cache the current transfomer Xt, fitted_transformer = fit_transform_one_cached( - cloned_transformer, None, Xt, y, + cloned_transformer, Xt, y, None, is_transform=True, message_clsname='Pipeline', message=self._log_message(step_idx), **fit_params_steps[name]) @@ -630,9 +630,9 @@ def _transform_one(transformer, X, y, weight, **fit_params): def _fit_transform_one(transformer, - weight, X, y, + weight, is_transform=None, message_clsname=None, message=None, @@ -851,7 +851,7 @@ def _fit_transform(self, X, y, fit_params, is_transform): self._validate_transformers() transformers = list(self._iter()) result = Parallel(n_jobs=self.n_jobs)(delayed(_fit_transform_one)( - transformer, weight, X, y, + transformer, X, y, weight, is_transform=is_transform, message_clsname='FeatureUnion', message=self._log_message(name, idx, len(transformers)), From b1f83310842aaf09503edaab4c271d9a4fada506 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 15:05:11 -0400 Subject: [PATCH 47/64] RFC: Rename to return_transform --- sklearn/compose/_column_transformer.py | 2 +- sklearn/pipeline.py | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 1d746692efab0..ce590b55aa8c9 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -407,7 +407,7 @@ def _fit_transform(self, X, y, func, fitted=False): X=_get_column(X, column), y=y, weight=weight, - is_transform=True, + return_transform=True, message_clsname='ColumnTransformer', message=self._log_message(name, idx, len(transformers))) for idx, (name, trans, column, weight) in enumerate( diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 9017c872495b9..2d582cd797505 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -241,7 +241,7 @@ def _fit(self, X, y=None, **fit_params): # Fit or load from cache the current transfomer Xt, fitted_transformer = fit_transform_one_cached( cloned_transformer, Xt, y, None, - is_transform=True, message_clsname='Pipeline', + return_transform=True, message_clsname='Pipeline', message=self._log_message(step_idx), **fit_params_steps[name]) # Replace the transformer of the step with the fitted @@ -633,31 +633,26 @@ def _fit_transform_one(transformer, X, y, weight, - is_transform=None, + return_transform=False, message_clsname=None, message=None, **fit_params): """ Fits ``transformer`` to ``X`` and ``y``. - If ``is_transform`` is ``True``, then ``X``, ``y`` will be transformed. The + If ``return_transform`` is ``True``, then ``X``, ``y`` will be transformed. The transformed result is returned with the fitted transformer. If ``weight`` is not ``None``, the result will be multipled by ``weight``. - If ``is_transform`` is ``False``, then a tuple of + If ``return_transform`` is ``False``, then a tuple of (``None``, fitted_transformer) will be returned. - If ``is_transform`` is ``None``, then it is set to true - when ``transformer`` is a ``TransformerMixin``. - If ``message_clsname`` is ``None``, the ``transformer`` class name is used. """ if message_clsname is None: message_clsname = transformer.__class__.__name__ - if is_transform is None: - is_transform = isinstance(transformer, TransformerMixin) with log_elapsed(message_clsname, message): - if not is_transform: + if not return_transform: return None, transformer.fit(X, y, **fit_params) elif hasattr(transformer, 'fit_transform'): res = transformer.fit_transform(X, y, **fit_params) @@ -852,7 +847,7 @@ def _fit_transform(self, X, y, fit_params, is_transform): transformers = list(self._iter()) result = Parallel(n_jobs=self.n_jobs)(delayed(_fit_transform_one)( transformer, X, y, weight, - is_transform=is_transform, + return_transform=is_transform, message_clsname='FeatureUnion', message=self._log_message(name, idx, len(transformers)), **fit_params) for idx, (name, transformer, From e349e7adb9a79032b45287dd85122650e903bc4a Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 15:16:08 -0400 Subject: [PATCH 48/64] RFC: Filters parameter_grid for test_verbose --- sklearn/tests/test_pipeline.py | 49 +++++++++++++++++----------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index b78e04b3a9349..cb076301ee380 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -6,6 +6,7 @@ import shutil import time import re +import itertools import pytest import numpy as np @@ -1033,31 +1034,31 @@ def test_make_pipeline_memory(): shutil.rmtree(cachedir) -@pytest.mark.parametrize(['est', 'pattern'], [ - (Pipeline([('transf', Transf()), ('clf', FitParamT())]), - r'''\[Pipeline\].*\(step 1 of 2\) Fitting transf.* elapsed=.*\n''' - r'''\[Pipeline\].*\(step 2 of 2\) Fitting clf.* elapsed=.*\n$'''), - (Pipeline([('transf', Transf()), ('clf', None)]), - r'''\[Pipeline\].*\(step 1 of 1\) Fitting transf.* elapsed=.*\n$'''), - (Pipeline([('transf', None), ('mult', Mult())]), - r'''\[Pipeline\].*\(step 1 of 1\) Fitting mult.* elapsed=.*\n$'''), - (FeatureUnion([('mult1', Mult()), ('mult2', Mult())]), - r'''\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* elapsed=.*\n''' - r'''\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* elapsed=.*\n$'''), - (FeatureUnion([('mult1', None), ('mult2', Mult()), ('mult3', None)]), - r'''\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* elapsed=.*\n$'''), -]) -@pytest.mark.parametrize('method', ['fit', 'fit_transform', 'fit_predict']) +parameter_grid_test_verbose = ((est, pattern, method) for ( + est, pattern +), method in itertools.product( + [(Pipeline([('transf', Transf()), ('clf', FitParamT())]), + r'''\[Pipeline\].*\(step 1 of 2\) Fitting transf.* elapsed=.*\n''' + r'''\[Pipeline\].*\(step 2 of 2\) Fitting clf.* elapsed=.*\n$'''), + (Pipeline([('transf', Transf()), ('clf', None)]), + r'''\[Pipeline\].*\(step 1 of 1\) Fitting transf.* elapsed=.*\n$'''), + (Pipeline([('transf', None), ('mult', Mult())]), + r'''\[Pipeline\].*\(step 1 of 1\) Fitting mult.* elapsed=.*\n$'''), + (FeatureUnion([('mult1', Mult()), ('mult2', Mult())]), + r'''\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* elapsed=.*\n''' + r'''\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* elapsed=.*\n$'''), + (FeatureUnion([('mult1', None), ('mult2', Mult()), ('mult3', None)]), + r'''\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* elapsed=.*\n$''' + )], ['fit', 'fit_transform', 'fit_predict']) + if hasattr(est, method) and not ( + method == 'fit_transform' and hasattr(est, 'steps') and + type(est.steps[-1][1]).__name__ == 'FitParamT') +) + + +@pytest.mark.parametrize('est, pattern, method', parameter_grid_test_verbose) def test_verbose(est, method, pattern, capsys): - try: - func = getattr(est, method) - except AttributeError: - return - # XXX: skips test when method is `fit_transform` and when the last step is - # `FitParamT` since `FitParamT` does not define a `transform` method - if (method == 'fit_transform' and hasattr(est, 'steps') and - type(est.steps[-1][1]).__name__ == 'FitParamT'): - return + func = getattr(est, method) X = [[1, 2, 3], [4, 5, 6]] y = [[7], [8]] From e9edcd2949f575dfc88f9e1fa3c0a9b7c4813546 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 24 Oct 2018 15:20:31 -0400 Subject: [PATCH 49/64] RFC: Uses empty string as default --- sklearn/pipeline.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 2d582cd797505..8e51039d0aef2 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -634,23 +634,19 @@ def _fit_transform_one(transformer, y, weight, return_transform=False, - message_clsname=None, + message_clsname='', message=None, **fit_params): """ Fits ``transformer`` to ``X`` and ``y``. - If ``return_transform`` is ``True``, then ``X``, ``y`` will be transformed. The - transformed result is returned with the fitted transformer. If ``weight`` - is not ``None``, the result will be multipled by ``weight``. + If ``return_transform`` is ``True``, then ``X``, ``y`` will be transformed. + The transformed result is returned with the fitted transformer. If + ``weight`` is not ``None``, the result will be multipled by ``weight``. If ``return_transform`` is ``False``, then a tuple of (``None``, fitted_transformer) will be returned. - - If ``message_clsname`` is ``None``, the ``transformer`` class name is used. """ - if message_clsname is None: - message_clsname = transformer.__class__.__name__ with log_elapsed(message_clsname, message): if not return_transform: return None, transformer.fit(X, y, **fit_params) From f7b288140b0908a27755a6be1bad9573cf32ffa1 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Fri, 4 Jan 2019 09:23:27 -0500 Subject: [PATCH 50/64] TST: Adds verbose to pprint tests --- sklearn/utils/tests/test_pprint.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sklearn/utils/tests/test_pprint.py b/sklearn/utils/tests/test_pprint.py index 77e36c959d4b8..5fe24b1fc6041 100644 --- a/sklearn/utils/tests/test_pprint.py +++ b/sklearn/utils/tests/test_pprint.py @@ -75,7 +75,8 @@ def test_pipeline(): multi_class='warn', n_jobs=None, penalty='l2', random_state=None, solver='warn', tol=0.0001, verbose=0, - warm_start=False))])""" + warm_start=False))], + verbose=False)""" expected = expected[1:] # remove first \n assert pipeline.__repr__() == expected @@ -184,7 +185,8 @@ def test_gridsearch_pipeline(): multi_class='ovr', penalty='l2', random_state=None, tol=0.0001, - verbose=0))]), + verbose=0))], + verbose=False), iid='warn', n_jobs=1, param_grid=[{'classify__C': [1, 10, 100, 1000], 'reduce_dim': [PCA(copy=True, iterated_power=7, From 52f876ea0305c05699248449def499839e5a7079 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Fri, 4 Jan 2019 09:30:14 -0500 Subject: [PATCH 51/64] TST: Replace total with elapsed --- sklearn/model_selection/tests/test_validation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sklearn/model_selection/tests/test_validation.py b/sklearn/model_selection/tests/test_validation.py index d143b07998bc3..2264f95695735 100644 --- a/sklearn/model_selection/tests/test_validation.py +++ b/sklearn/model_selection/tests/test_validation.py @@ -190,7 +190,7 @@ def fit(self, X, Y=None, sample_weight=None, class_prior=None, raise ValueError('X cannot be d') if sample_weight is not None: assert sample_weight.shape[0] == X.shape[0], ( - 'MockClassifier extra fit_param ' + 'MockClassifier extra fit_param ' 'sample_weight.shape[0] is {0}, should be {1}' .format(sample_weight.shape[0], X.shape[0])) if class_prior is not None: @@ -1556,12 +1556,12 @@ def three_params_scorer(i, j, k): @pytest.mark.parametrize("return_train_score, scorer, expected", [ (False, three_params_scorer, - "[CV] .................................... , score=3.421, total= 0.0s"), + "[CV] .................................. , score=3.421, elapsed= 0.0s"), (True, three_params_scorer, - "[CV] ................ , score=(train=3.421, test=3.421), total= 0.0s"), + "[CV] .............. , score=(train=3.421, test=3.421), elapsed= 0.0s"), (True, {'sc1': three_params_scorer, 'sc2': three_params_scorer}, "[CV] , sc1=(train=3.421, test=3.421)" - ", sc2=(train=3.421, test=3.421), total= 0.0s") + ", sc2=(train=3.421, test=3.421), elapsed= 0.0s") ]) def test_fit_and_score_verbosity(capsys, return_train_score, scorer, expected): X, y = make_classification(n_samples=30, random_state=0) From 6804511e105a12c0e782ed6eeb422852371288f3 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Fri, 4 Jan 2019 10:25:56 -0500 Subject: [PATCH 52/64] ENH: Uses total instead of elapsed --- .../compose/tests/test_column_transformer.py | 32 +++++++++---------- .../model_selection/tests/test_validation.py | 6 ++-- sklearn/utils/__init__.py | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index b792b9bd3f13c..30b24744a3cd0 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -994,44 +994,44 @@ def test_column_transformer_no_estimators(): [('trans1', Trans(), [0]), ('trans2', Trans(), [1])], remainder=DoubleTrans()), - (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* elapsed=.*\n$')), + (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* total=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* total=.*\n$')), (ColumnTransformer( [('trans1', Trans(), [0]), ('trans2', Trans(), [1])], remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* elapsed=.*\n$')), + (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* total=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* total=.*\n$')), (ColumnTransformer( [('trans1', Trans(), [0]), ('trans2', 'drop', [1])], remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* elapsed=.*\n$')), + (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* total=.*\n$')), (ColumnTransformer( [('trans1', Trans(), [0]), ('trans2', 'passthrough', [1])], remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* elapsed=.*\n$')), + (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* total=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* total=.*\n$')), (ColumnTransformer( [('trans1', Trans(), [0])], remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* elapsed=.*\n$')), + (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* total=.*\n$')), (ColumnTransformer( [('trans1', Trans(), [0]), ('trans2', Trans(), [1])], remainder='drop'), - (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* elapsed=.*\n' - r'\[ColumnTransformer\].*\(2 of 2\) Fitting trans2.* elapsed=.*\n$')), + (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Fitting trans2.* total=.*\n$')), (ColumnTransformer( [('trans1', Trans(), [0])], remainder='drop'), - (r'\[ColumnTransformer\].*\(1 of 1\) Fitting trans1.* elapsed=.*\n$')) + (r'\[ColumnTransformer\].*\(1 of 1\) Fitting trans1.* total=.*\n$')) ]) @pytest.mark.parametrize('method', ['fit', 'fit_transform']) def test_column_transformer_verbose(est, pattern, method, capsys): diff --git a/sklearn/model_selection/tests/test_validation.py b/sklearn/model_selection/tests/test_validation.py index 2264f95695735..915545fa77b74 100644 --- a/sklearn/model_selection/tests/test_validation.py +++ b/sklearn/model_selection/tests/test_validation.py @@ -1556,12 +1556,12 @@ def three_params_scorer(i, j, k): @pytest.mark.parametrize("return_train_score, scorer, expected", [ (False, three_params_scorer, - "[CV] .................................. , score=3.421, elapsed= 0.0s"), + "[CV] .................................... , score=3.421, total= 0.0s"), (True, three_params_scorer, - "[CV] .............. , score=(train=3.421, test=3.421), elapsed= 0.0s"), + "[CV] ................ , score=(train=3.421, test=3.421), total= 0.0s"), (True, {'sc1': three_params_scorer, 'sc2': three_params_scorer}, "[CV] , sc1=(train=3.421, test=3.421)" - ", sc2=(train=3.421, test=3.421), elapsed= 0.0s") + ", sc2=(train=3.421, test=3.421), total= 0.0s") ]) def test_fit_and_score_verbosity(capsys, return_train_score, scorer, expected): X, y = make_classification(n_samples=30, random_state=0) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index eddf10f1d16b3..c5547ef285402 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -593,7 +593,7 @@ def message_with_time(source, message, time): time_str = "%4.1fmin" % (time / 60) else: time_str = " %5.1fs" % (time) - end_message = " %s, elapsed=%s" % (message, time_str) + end_message = " %s, total=%s" % (message, time_str) dots_len = (70 - len(start_message) - len(end_message)) return "%s%s%s" % (start_message, dots_len * '.', end_message) From bde21f591daea55d64c29dc78572d4803eff5124 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Fri, 4 Jan 2019 12:18:06 -0500 Subject: [PATCH 53/64] TST: Fix --- sklearn/tests/test_pipeline.py | 14 +++++++------- sklearn/utils/tests/test_utils.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index ce52308302aca..0dc868452132d 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1057,17 +1057,17 @@ def test_make_pipeline_memory(): est, pattern ), method in itertools.product( [(Pipeline([('transf', Transf()), ('clf', FitParamT())]), - r'''\[Pipeline\].*\(step 1 of 2\) Fitting transf.* elapsed=.*\n''' - r'''\[Pipeline\].*\(step 2 of 2\) Fitting clf.* elapsed=.*\n$'''), + r'''\[Pipeline\].*\(step 1 of 2\) Fitting transf.* total=.*\n''' + r'''\[Pipeline\].*\(step 2 of 2\) Fitting clf.* total=.*\n$'''), (Pipeline([('transf', Transf()), ('clf', None)]), - r'''\[Pipeline\].*\(step 1 of 1\) Fitting transf.* elapsed=.*\n$'''), + r'''\[Pipeline\].*\(step 1 of 1\) Fitting transf.* total=.*\n$'''), (Pipeline([('transf', None), ('mult', Mult())]), - r'''\[Pipeline\].*\(step 1 of 1\) Fitting mult.* elapsed=.*\n$'''), + r'''\[Pipeline\].*\(step 1 of 1\) Fitting mult.* total=.*\n$'''), (FeatureUnion([('mult1', Mult()), ('mult2', Mult())]), - r'''\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* elapsed=.*\n''' - r'''\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* elapsed=.*\n$'''), + r'''\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* total=.*\n''' + r'''\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* total=.*\n$'''), (FeatureUnion([('mult1', None), ('mult2', Mult()), ('mult3', None)]), - r'''\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* elapsed=.*\n$''' + r'''\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* total=.*\n$''' )], ['fit', 'fit_transform', 'fit_predict']) if hasattr(est, method) and not ( method == 'fit_transform' and hasattr(est, 'steps') and diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index ed913c83a4faa..c659e671d7f85 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -294,8 +294,8 @@ def test_message_with_time(source, message, is_long, time, time_str): assert out.endswith(time_str) out = out[:-len(time_str)] - assert out.endswith(', elapsed=') - out = out[:-len(', elapsed=')] + assert out.endswith(', total=') + out = out[:-len(', total=')] assert out.endswith(message) out = out[:-len(message)] assert out.endswith(' ') From 8b17a8ce712a31549df435e8139dc93c25660c94 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Tue, 12 Mar 2019 10:52:50 -0400 Subject: [PATCH 54/64] CLN Address comments --- sklearn/model_selection/_validation.py | 4 ++-- sklearn/pipeline.py | 12 ++++++------ sklearn/utils/__init__.py | 8 ++++---- sklearn/utils/tests/test_utils.py | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sklearn/model_selection/_validation.py b/sklearn/model_selection/_validation.py index ed4e946d7711e..95fa6538128cf 100644 --- a/sklearn/model_selection/_validation.py +++ b/sklearn/model_selection/_validation.py @@ -20,7 +20,7 @@ from ..base import is_classifier, clone from ..utils import (indexable, check_random_state, safe_indexing, - message_with_time) + _message_with_time) from ..utils.validation import _is_arraylike, _num_samples from ..utils.metaestimators import _safe_split from ..utils._joblib import Parallel, delayed @@ -566,7 +566,7 @@ def _fit_and_score(estimator, X, y, scorer, train, test, verbose, if verbose > 1: total_time = score_time + fit_time - print(message_with_time('CV', msg, total_time)) + print(_message_with_time('CV', msg, total_time)) ret = [train_scores, test_scores] if return_train_score else [test_scores] diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index f4803fc3479ba..84de43f55ad04 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -18,7 +18,7 @@ from .base import clone, TransformerMixin from .utils._joblib import Parallel, delayed from .utils.metaestimators import if_delegate_has_method -from .utils import Bunch, log_elapsed +from .utils import Bunch, _log_elapsed from .utils.validation import check_memory from .utils.metaestimators import _BaseComposition @@ -117,7 +117,7 @@ class Pipeline(_BaseComposition): >>> # Indexing can also be used to extract a sub-pipeline. >>> sub_pipeline = anova_svm[:1] >>> sub_pipeline # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE - Pipeline(memory=None, steps=[('anova', ...)]) + Pipeline(memory=None, steps=[('anova', ...)], verbose=False) >>> coef = anova_svm[-1].coef_ >>> anova_svm['svc'] is anova_svm[-1] True @@ -325,7 +325,7 @@ def fit(self, X, y=None, **fit_params): """ Xt, fit_params = self._fit(X, y, **fit_params) if self._final_estimator != 'passthrough': - with log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): + with _log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): self._final_estimator.fit(Xt, y, **fit_params) return self @@ -360,7 +360,7 @@ def fit_transform(self, X, y=None, **fit_params): Xt, fit_params = self._fit(X, y, **fit_params) if last_step == 'passthrough': return Xt - with log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): + with _log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): if hasattr(last_step, 'fit_transform'): Xt = last_step.fit_transform(Xt, y, **fit_params) else: @@ -422,7 +422,7 @@ def fit_predict(self, X, y=None, **fit_params): y_pred : array-like """ Xt, fit_params = self._fit(X, y, **fit_params) - with log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): + with _log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): y_pred = self.steps[-1][-1].fit_predict(Xt, y, **fit_params) return y_pred @@ -689,7 +689,7 @@ def _fit_transform_one(transformer, If ``return_transform`` is ``False``, then a tuple of (``None``, fitted_transformer) will be returned. """ - with log_elapsed(message_clsname, message): + with _log_elapsed(message_clsname, message): if not return_transform: return None, transformer.fit(X, y, **fit_params) elif hasattr(transformer, 'fit_transform'): diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 2eb9ff5470397..6ad65bcc49031 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -569,7 +569,7 @@ def indices_to_mask(indices, mask_length): return mask -def message_with_time(source, message, time): +def _message_with_time(source, message, time): """Create one line message for logging purposes Parameters @@ -597,7 +597,7 @@ def message_with_time(source, message, time): @contextmanager -def log_elapsed(source, message=None): +def _log_elapsed(source, message=None): """Log elapsed time to stdout when the context is exited Parameters @@ -619,8 +619,8 @@ def log_elapsed(source, message=None): start = timeit.default_timer() yield print( - message_with_time(source, message, - timeit.default_timer() - start)) + _message_with_time(source, message, + timeit.default_timer() - start)) def get_chunk_n_rows(row_bytes, max_n_rows=None, diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index 24adf5a07d755..7642114715d3c 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -19,7 +19,7 @@ from sklearn.utils import safe_indexing from sklearn.utils import shuffle from sklearn.utils import gen_even_slices -from sklearn.utils import message_with_time, log_elapsed +from sklearn.utils import _message_with_time, _log_elapsed from sklearn.utils import get_chunk_n_rows from sklearn.utils import is_scalar_nan from sklearn.utils.mocking import MockDataFrame @@ -283,7 +283,7 @@ def check_warning(*args, **kw): (20000, '333.3min'), ]) def test_message_with_time(source, message, is_long, time, time_str): - out = message_with_time(source, message, time) + out = _message_with_time(source, message, time) if is_long: assert len(out) > 70 else: @@ -310,13 +310,13 @@ def test_message_with_time(source, message, is_long, time, time_str): @pytest.mark.parametrize( ['message', 'expected'], [ - ('hello', message_with_time('ABC', 'hello', 0.1) + '\n'), - ('', message_with_time('ABC', '', 0.1) + '\n'), + ('hello', _message_with_time('ABC', 'hello', 0.1) + '\n'), + ('', _message_with_time('ABC', '', 0.1) + '\n'), (None, ''), ]) def test_log_elapsed(message, expected, capsys, monkeypatch): monkeypatch.setattr(timeit, 'default_timer', lambda: 0) - with log_elapsed('ABC', message): + with _log_elapsed('ABC', message): monkeypatch.setattr(timeit, 'default_timer', lambda: 0.1) assert capsys.readouterr().out == expected From 495ecd780da30a17433ab6f69b1f85e5805e643c Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Tue, 12 Mar 2019 10:55:29 -0400 Subject: [PATCH 55/64] CLN Address comments --- sklearn/pipeline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 84de43f55ad04..2153dbb42f3be 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -849,8 +849,7 @@ def fit(self, X, y=None): self : FeatureUnion This estimator """ - fit_params = {} # XXX: this should probably be added to method - self._fit_transform(X, y, fit_params, is_transform=False) + self._fit_transform(X, y, {}, is_transform=False) return self def fit_transform(self, X, y=None, **fit_params): From 2cb6bba2aa45e6745540b69097c5bd2d513cc981 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Tue, 12 Mar 2019 15:40:20 -0400 Subject: [PATCH 56/64] BLD Trigger CI From 64fc3eb37b24736e889e57f7bdf93743a3ebbe64 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Tue, 12 Mar 2019 20:08:38 -0400 Subject: [PATCH 57/64] BUG Fix doctest error --- doc/modules/compose.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/modules/compose.rst b/doc/modules/compose.rst index f49ac9204b2bc..9e654310ef485 100644 --- a/doc/modules/compose.rst +++ b/doc/modules/compose.rst @@ -107,9 +107,9 @@ permitted). This is convenient for performing only some of the transformations (or their inverse): >>> pipe[:1] # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS - Pipeline(memory=None, steps=[('reduce_dim', PCA(copy=True, ...))]) + Pipeline(memory=None, steps=[('reduce_dim', PCA(copy=True, ...))],...) >>> pipe[-1:] # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS - Pipeline(memory=None, steps=[('clf', SVC(C=1.0, ...))]) + Pipeline(memory=None, steps=[('clf', SVC(C=1.0, ...))],...) Nested parameters ................. @@ -221,7 +221,7 @@ object:: >>> pipe.fit(digits.data, digits.target) ... # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS Pipeline(memory=None, - steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))], + steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))], verbose=False) >>> # The pca instance can be inspected directly >>> print(pca1.components_) # doctest: +NORMALIZE_WHITESPACE, +ELLIPSIS From a5e7a94bb9207e26209afc9bc0ad6c8144acf394 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Tue, 12 Mar 2019 20:51:03 -0400 Subject: [PATCH 58/64] STY Flake8 --- sklearn/pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 2153dbb42f3be..4ba5e15555162 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -325,7 +325,8 @@ def fit(self, X, y=None, **fit_params): """ Xt, fit_params = self._fit(X, y, **fit_params) if self._final_estimator != 'passthrough': - with _log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): + with _log_elapsed('Pipeline', + self._log_message(len(self.steps)-1)): self._final_estimator.fit(Xt, y, **fit_params) return self From 3a42ba564b07aa3a79430e5fb36723e483ce1a68 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Fri, 12 Apr 2019 11:27:41 -0400 Subject: [PATCH 59/64] CLN Address comments --- doc/whats_new/v0.20.rst | 10 --- doc/whats_new/v0.21.rst | 7 ++ sklearn/compose/_column_transformer.py | 14 ++-- sklearn/pipeline.py | 94 +++++++++++++++----------- sklearn/tests/test_pipeline.py | 5 +- sklearn/utils/__init__.py | 4 +- 6 files changed, 74 insertions(+), 60 deletions(-) diff --git a/doc/whats_new/v0.20.rst b/doc/whats_new/v0.20.rst index 0944781477ddc..e5309aeddd575 100644 --- a/doc/whats_new/v0.20.rst +++ b/doc/whats_new/v0.20.rst @@ -332,16 +332,6 @@ Changelog avoid pickling errors caused by the serialization of their methods. :issue:`12171` by :user:`Thomas Moreau `. -:mod:`sklearn.pipeline` -....................... - -- |Feature| Added optional parameter ``verbose`` in :class:`pipeline.Pipeline`, - :class:`compose.ColumnTransformer` and :class:`pipeline.FeatureUnion` - and corresponding ``make_`` helpers for showing progress and timing of - each step. :issue:`9668` by :user:`Baze Petrushev `, - :user:`Karan Desai `, `Joel Nothman`_, and - :user:`thomasjpfan`. - :mod:`sklearn.preprocessing` ............................. diff --git a/doc/whats_new/v0.21.rst b/doc/whats_new/v0.21.rst index b2df99d9a131b..deb391359e237 100644 --- a/doc/whats_new/v0.21.rst +++ b/doc/whats_new/v0.21.rst @@ -500,6 +500,13 @@ Support for Python 3.4 and below has been officially dropped. therefore ``len(pipeline)`` returns the number of steps in the pipeline. :issue:`13439` by :user:`Lakshya KD `. +- |Feature| Added optional parameter ``verbose`` in :class:`pipeline.Pipeline`, + :class:`compose.ColumnTransformer` and :class:`pipeline.FeatureUnion` + and corresponding ``make_`` helpers for showing progress and timing of + each step. :issue:`11364` by :user:`Baze Petrushev `, + :user:`Karan Desai `, `Joel Nothman`_, and + :user:`Thomas Fan `. + :mod:`sklearn.preprocessing` ............................ diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index c743c593d7357..1ae08f0f15049 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -101,8 +101,9 @@ class ColumnTransformer(_BaseComposition, TransformerMixin): transformer is multiplied by these weights. Keys are transformer names, values the weights. - verbose : boolean, optional - Verbosity mode. + verbose : boolean, optional(default=False) + Verbosity mode. When enabled, the time elapsed while fitting each step + will be printed as it is completed. Attributes ---------- @@ -408,7 +409,6 @@ def _fit_transform(self, X, y, func, fitted=False): X=_get_column(X, column), y=y, weight=weight, - return_transform=True, message_clsname='ColumnTransformer', message=self._log_message(name, idx, len(transformers))) for idx, (name, trans, column, weight) in enumerate( @@ -797,6 +797,10 @@ def make_column_transformer(*transformers, **kwargs): ``-1`` means using all processors. See :term:`Glossary ` for more details. + verbose : boolean, optional(default=False) + Verbosity mode. When enabled, the time elapsed while fitting each step + will be printed as it is completed. + Returns ------- ct : ColumnTransformer @@ -830,10 +834,12 @@ def make_column_transformer(*transformers, **kwargs): n_jobs = kwargs.pop('n_jobs', None) remainder = kwargs.pop('remainder', 'drop') sparse_threshold = kwargs.pop('sparse_threshold', 0.3) + verbose = kwargs.pop('verbose', False) if kwargs: raise TypeError('Unknown keyword arguments: "{}"' .format(list(kwargs.keys())[0])) transformer_list = _get_transformer_list(transformers) return ColumnTransformer(transformer_list, n_jobs=n_jobs, remainder=remainder, - sparse_threshold=sparse_threshold) + sparse_threshold=sparse_threshold, + verbose=verbose) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 836b84595be56..5009183bb6aed 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -299,7 +299,7 @@ def _fit(self, X, y=None, **fit_params): # Fit or load from cache the current transfomer Xt, fitted_transformer = fit_transform_one_cached( cloned_transformer, Xt, y, None, - return_transform=True, message_clsname='Pipeline', + message_clsname='Pipeline', message=self._log_message(step_idx), **fit_params_steps[name]) # Replace the transformer of the step with the fitted @@ -643,9 +643,9 @@ def make_pipeline(*steps, **kwargs): inspect estimators within the pipeline. Caching the transformers is advantageous when fitting is time consuming. - verbose : boolean, default=False - Verbosity mode. When enabled, the time elapsed while fitting each - transformer will be printed as it is completed. + verbose : boolean, optional(default=False) + Verbosity mode. When enabled, the time elapsed while fitting each step + will be printed as it is completed. See also -------- @@ -689,24 +689,16 @@ def _fit_transform_one(transformer, X, y, weight, - return_transform=False, message_clsname='', message=None, **fit_params): """ - Fits ``transformer`` to ``X`` and ``y``. - - If ``return_transform`` is ``True``, then ``X``, ``y`` will be transformed. - The transformed result is returned with the fitted transformer. If - ``weight`` is not ``None``, the result will be multipled by ``weight``. - - If ``return_transform`` is ``False``, then a tuple of - (``None``, fitted_transformer) will be returned. + Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned + with the fitted transformer. If ``weight`` is not ``None``, the result will + be multipled by ``weight``. """ with _log_elapsed(message_clsname, message): - if not return_transform: - return None, transformer.fit(X, y, **fit_params) - elif hasattr(transformer, 'fit_transform'): + if hasattr(transformer, 'fit_transform'): res = transformer.fit_transform(X, y, **fit_params) else: res = transformer.fit(X, y, **fit_params).transform(X) @@ -716,6 +708,20 @@ def _fit_transform_one(transformer, return res * weight, transformer +def _fit_one(transformer, + X, + y, + weight, + message_clsname='', + message=None, + **fit_params): + """ + Fits ``transformer`` to ``X`` and ``y``. + """ + with _log_elapsed(message_clsname, message): + return transformer.fit(X, y, **fit_params) + + class FeatureUnion(_BaseComposition, TransformerMixin): """Concatenates results of multiple transformer objects. @@ -746,8 +752,9 @@ class FeatureUnion(_BaseComposition, TransformerMixin): Multiplicative weights for features per transformer. Keys are transformer names, values the weights. - verbose : boolean, optional - Verbosity mode. + verbose : boolean, optional(default=False) + Verbosity mode. When enabled, the time elapsed while fitting each step + will be printed as it is completed. See also -------- @@ -863,7 +870,12 @@ def fit(self, X, y=None): self : FeatureUnion This estimator """ - self._fit_transform(X, y, {}, is_transform=False) + transformers = self._parallel_func(X, y, {}, _fit_one) + if not transformers: + # All transformers are None + return self + + self._update_transformer_list(transformers) return self def fit_transform(self, X, y=None, **fit_params): @@ -883,39 +895,38 @@ def fit_transform(self, X, y=None, **fit_params): hstack of results of transformers. sum_n_components is the sum of n_components (output dimension) over transformers. """ - return self._fit_transform(X, y, fit_params, is_transform=True) + results = self._parallel_func(X, y, fit_params, _fit_transform_one) + if not results: + # All transformers are None + return np.zeros((X.shape[0], 0)) + + Xs, transformers = zip(*results) + self._update_transformer_list(transformers) + + if any(sparse.issparse(f) for f in Xs): + Xs = sparse.hstack(Xs).tocsr() + else: + Xs = np.hstack(Xs) + return Xs def _log_message(self, name, idx, total): if not self.verbose: return None return '(step %d of %d) Fitting %s' % (idx, total, name) - def _fit_transform(self, X, y, fit_params, is_transform): + def _parallel_func(self, X, y, fit_params, func): + """Runs func in parallel on X and y""" self.transformer_list = list(self.transformer_list) self._validate_transformers() transformers = list(self._iter()) - result = Parallel(n_jobs=self.n_jobs)(delayed(_fit_transform_one)( + + return Parallel(n_jobs=self.n_jobs)(delayed(func)( transformer, X, y, weight, - return_transform=is_transform, message_clsname='FeatureUnion', message=self._log_message(name, idx, len(transformers)), **fit_params) for idx, (name, transformer, weight) in enumerate(transformers, 1)) - if not result: - # All transformers are None - return np.zeros((X.shape[0], 0)) - Xs, transformers = zip(*result) - self._update_transformer_list(transformers) - if not is_transform: - return - - if any(sparse.issparse(f) for f in Xs): - Xs = sparse.hstack(Xs).tocsr() - else: - Xs = np.hstack(Xs) - return Xs - def transform(self, X): """Transform X separately by each transformer, concatenate results. @@ -966,8 +977,9 @@ def make_union(*transformers, **kwargs): ``-1`` means using all processors. See :term:`Glossary ` for more details. - verbose : boolean, optional - Verbosity mode. + verbose : boolean, optional(default=False) + Verbosity mode. When enabled, the time elapsed while fitting each step + will be printed as it is completed. Returns ------- @@ -999,7 +1011,7 @@ def make_union(*transformers, **kwargs): if kwargs: # We do not currently support `transformer_weights` as we may want to # change its type spec in make_union - raise TypeError('Unknown keyword arguments: "{}"'.format( - list(kwargs.keys())[0])) + raise TypeError('Unknown keyword arguments: "{}"' + .format(list(kwargs.keys())[0])) return FeatureUnion( _name_estimators(transformers), n_jobs=n_jobs, verbose=verbose) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 671e905f0eec4..0e74a38a6a5db 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1084,9 +1084,8 @@ def test_pipeline_param_error(): clf.fit([[0], [0]], [0, 1], sample_weight=[1, 1]) -parameter_grid_test_verbose = ((est, pattern, method) for ( - est, pattern -), method in itertools.product( +parameter_grid_test_verbose = ((est, pattern, method) for + (est, pattern), method in itertools.product( [(Pipeline([('transf', Transf()), ('clf', FitParamT())]), r'''\[Pipeline\].*\(step 1 of 2\) Fitting transf.* total=.*\n''' r'''\[Pipeline\].*\(step 2 of 2\) Fitting clf.* total=.*\n$'''), diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 6ad65bcc49031..11e5c6fb3fd0f 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -583,14 +583,14 @@ def _message_with_time(source, message, time): time : int Time in seconds """ - start_message = "[%s] " % (source,) + start_message = "[%s] " % source # adapted from joblib.logger.short_format_time without the Windows -.1s # adjustment if time > 60: time_str = "%4.1fmin" % (time / 60) else: - time_str = " %5.1fs" % (time) + time_str = " %5.1fs" % time end_message = " %s, total=%s" % (message, time_str) dots_len = (70 - len(start_message) - len(end_message)) return "%s%s%s" % (start_message, dots_len * '.', end_message) From 0c6ed63e6505e728ddc5a3b4041a467c27c25788 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Fri, 12 Apr 2019 21:31:00 -0400 Subject: [PATCH 60/64] DOC Clears up verbose doc --- sklearn/compose/_column_transformer.py | 8 ++++---- sklearn/pipeline.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 1ae08f0f15049..558ac30522abf 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -102,8 +102,8 @@ class ColumnTransformer(_BaseComposition, TransformerMixin): values the weights. verbose : boolean, optional(default=False) - Verbosity mode. When enabled, the time elapsed while fitting each step - will be printed as it is completed. + If True, the time elapsed while fitting each transformer will be + printed as it is completed. Attributes ---------- @@ -798,8 +798,8 @@ def make_column_transformer(*transformers, **kwargs): for more details. verbose : boolean, optional(default=False) - Verbosity mode. When enabled, the time elapsed while fitting each step - will be printed as it is completed. + If True, the time elapsed while fitting each transformer will be + printed as it is completed. Returns ------- diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 5009183bb6aed..f574b76c2b03c 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -63,8 +63,8 @@ class Pipeline(_BaseComposition): transformers is advantageous when fitting is time consuming. verbose : boolean, optional - Verbosity mode. When enabled, the time elapsed while fitting each step - will be printed as it is completed. + If True, the time elapsed while fitting each step will be printed as it + is completed. Attributes ---------- @@ -643,9 +643,9 @@ def make_pipeline(*steps, **kwargs): inspect estimators within the pipeline. Caching the transformers is advantageous when fitting is time consuming. - verbose : boolean, optional(default=False) - Verbosity mode. When enabled, the time elapsed while fitting each step - will be printed as it is completed. + verbose : boolean, optional + If True, the time elapsed while fitting each step will be printed as it + is completed. See also -------- @@ -753,8 +753,8 @@ class FeatureUnion(_BaseComposition, TransformerMixin): Keys are transformer names, values the weights. verbose : boolean, optional(default=False) - Verbosity mode. When enabled, the time elapsed while fitting each step - will be printed as it is completed. + If True, the time elapsed while fitting each transformer will be + printed as it is completed. See also -------- @@ -978,8 +978,8 @@ def make_union(*transformers, **kwargs): for more details. verbose : boolean, optional(default=False) - Verbosity mode. When enabled, the time elapsed while fitting each step - will be printed as it is completed. + If True, the time elapsed while fitting each transformer will be + printed as it is completed. Returns ------- From 37e1f646a579f56a8d6b91609b2b5609efae288a Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Sun, 14 Apr 2019 14:11:35 -0400 Subject: [PATCH 61/64] ENH Proper handling of None and passthrough --- sklearn/pipeline.py | 60 ++++++++++++++++++++----------- sklearn/tests/test_pipeline.py | 37 +++++++++++++------ sklearn/utils/__init__.py | 2 +- sklearn/utils/tests/test_utils.py | 6 ++-- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index f574b76c2b03c..b1c7a7df4d94d 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -18,7 +18,7 @@ from .base import clone, TransformerMixin from .utils._joblib import Parallel, delayed from .utils.metaestimators import if_delegate_has_method -from .utils import Bunch, _log_elapsed +from .utils import Bunch, _print_elapsed_time from .utils.validation import check_memory from .utils.metaestimators import _BaseComposition @@ -192,16 +192,21 @@ def _validate_steps(self): "or be the string 'passthrough'. " "'%s' (type %s) doesn't" % (estimator, type(estimator))) - def _iter(self, with_final=True): + def _iter(self, with_final=True, filter_passthrough=True): """ - Generate (name, trans) tuples excluding 'passthrough' transformers + Generate (idx, (name, trans)) tuples from self.steps + + When filter_passthrough is True, 'passthrough' and None transformers + are filtered out. """ stop = len(self.steps) if not with_final: stop -= 1 for idx, (name, trans) in enumerate(islice(self.steps, 0, stop)): - if trans is not None and trans != 'passthrough': + if not filter_passthrough: + yield idx, name, trans + elif trans is not None and trans != 'passthrough': yield idx, name, trans def __len__(self): @@ -246,12 +251,17 @@ def _final_estimator(self): def _log_message(self, step_idx): if not self.verbose: - return - n_steps = len([est for _, est in self.steps if est is not None]) - step_num = len( - [est for _, est in self.steps[:step_idx + 1] if est is not None]) - return '(step %d of %d) Fitting %s' % (step_num, n_steps, - self.steps[step_idx][0]) + return None + name, step = self.steps[step_idx] + + if step is None or step == 'passthrough': + return '(step %d of %d) Passing %s' % (step_idx + 1, + len(self.steps), + name) + else: + return '(step %d of %d) Fitting %s' % (step_idx + 1, + len(self.steps), + name) # Estimator interface @@ -277,7 +287,15 @@ def _fit(self, X, y=None, **fit_params): step, param = pname.split('__', 1) fit_params_steps[step][param] = pval Xt = X - for step_idx, name, transformer in self._iter(with_final=False): + for (step_idx, + name, + transformer) in self._iter(with_final=False, + filter_passthrough=False): + if (transformer is None or transformer == 'passthrough'): + with _print_elapsed_time('Pipeline', + self._log_message(step_idx)): + continue + if hasattr(memory, 'location'): # joblib >= 0.12 if memory.location is None: @@ -337,9 +355,9 @@ def fit(self, X, y=None, **fit_params): This estimator """ Xt, fit_params = self._fit(X, y, **fit_params) - if self._final_estimator != 'passthrough': - with _log_elapsed('Pipeline', - self._log_message(len(self.steps)-1)): + with _print_elapsed_time('Pipeline', + self._log_message(len(self.steps) - 1)): + if self._final_estimator != 'passthrough': self._final_estimator.fit(Xt, y, **fit_params) return self @@ -372,9 +390,10 @@ def fit_transform(self, X, y=None, **fit_params): """ last_step = self._final_estimator Xt, fit_params = self._fit(X, y, **fit_params) - if last_step == 'passthrough': - return Xt - with _log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): + with _print_elapsed_time('Pipeline', + self._log_message(len(self.steps) - 1)): + if last_step == 'passthrough': + return Xt if hasattr(last_step, 'fit_transform'): Xt = last_step.fit_transform(Xt, y, **fit_params) else: @@ -436,7 +455,8 @@ def fit_predict(self, X, y=None, **fit_params): y_pred : array-like """ Xt, fit_params = self._fit(X, y, **fit_params) - with _log_elapsed('Pipeline', self._log_message(len(self.steps)-1)): + with _print_elapsed_time('Pipeline', + self._log_message(len(self.steps) - 1)): y_pred = self.steps[-1][-1].fit_predict(Xt, y, **fit_params) return y_pred @@ -697,7 +717,7 @@ def _fit_transform_one(transformer, with the fitted transformer. If ``weight`` is not ``None``, the result will be multipled by ``weight``. """ - with _log_elapsed(message_clsname, message): + with _print_elapsed_time(message_clsname, message): if hasattr(transformer, 'fit_transform'): res = transformer.fit_transform(X, y, **fit_params) else: @@ -718,7 +738,7 @@ def _fit_one(transformer, """ Fits ``transformer`` to ``X`` and ``y``. """ - with _log_elapsed(message_clsname, message): + with _print_elapsed_time(message_clsname, message): return transformer.fit(X, y, **fit_params) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 0e74a38a6a5db..836cea45fbdf5 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1086,22 +1086,39 @@ def test_pipeline_param_error(): parameter_grid_test_verbose = ((est, pattern, method) for (est, pattern), method in itertools.product( - [(Pipeline([('transf', Transf()), ('clf', FitParamT())]), - r'''\[Pipeline\].*\(step 1 of 2\) Fitting transf.* total=.*\n''' - r'''\[Pipeline\].*\(step 2 of 2\) Fitting clf.* total=.*\n$'''), + [ + (Pipeline([('transf', Transf()), ('clf', FitParamT())]), + r'\[Pipeline\].*\(step 1 of 2\) Fitting transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Fitting clf.* total=.*\n$'), + (Pipeline([('transf', Transf()), ('noop', None), + ('clf', FitParamT())]), + r'\[Pipeline\].*\(step 1 of 3\) Fitting transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 3\) Passing noop.* total=.*\n' + r'\[Pipeline\].*\(step 3 of 3\) Fitting clf.* total=.*\n$'), + (Pipeline([('transf', Transf()), ('noop', 'passthrough'), + ('clf', FitParamT())]), + r'\[Pipeline\].*\(step 1 of 3\) Fitting transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 3\) Passing noop.* total=.*\n' + r'\[Pipeline\].*\(step 3 of 3\) Fitting clf.* total=.*\n$'), (Pipeline([('transf', Transf()), ('clf', None)]), - r'''\[Pipeline\].*\(step 1 of 1\) Fitting transf.* total=.*\n$'''), + r'\[Pipeline\].*\(step 1 of 2\) Fitting transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Passing clf.* total=.*\n$'), (Pipeline([('transf', None), ('mult', Mult())]), - r'''\[Pipeline\].*\(step 1 of 1\) Fitting mult.* total=.*\n$'''), + r'\[Pipeline\].*\(step 1 of 2\) Passing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Fitting mult.* total=.*\n$'), + (Pipeline([('transf', 'passthrough'), ('mult', Mult())]), + r'\[Pipeline\].*\(step 1 of 2\) Passing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Fitting mult.* total=.*\n$'), (FeatureUnion([('mult1', Mult()), ('mult2', Mult())]), - r'''\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* total=.*\n''' - r'''\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* total=.*\n$'''), + r'\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* total=.*\n' + r'\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* total=.*\n$'), (FeatureUnion([('mult1', None), ('mult2', Mult()), ('mult3', None)]), - r'''\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* total=.*\n$''' - )], ['fit', 'fit_transform', 'fit_predict']) + r'\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* total=.*\n$' + ) + ], ['fit', 'fit_transform', 'fit_predict']) if hasattr(est, method) and not ( method == 'fit_transform' and hasattr(est, 'steps') and - type(est.steps[-1][1]).__name__ == 'FitParamT') + isinstance(est.steps[-1][1], FitParamT)) ) diff --git a/sklearn/utils/__init__.py b/sklearn/utils/__init__.py index 11e5c6fb3fd0f..8213fdcde904b 100644 --- a/sklearn/utils/__init__.py +++ b/sklearn/utils/__init__.py @@ -597,7 +597,7 @@ def _message_with_time(source, message, time): @contextmanager -def _log_elapsed(source, message=None): +def _print_elapsed_time(source, message=None): """Log elapsed time to stdout when the context is exited Parameters diff --git a/sklearn/utils/tests/test_utils.py b/sklearn/utils/tests/test_utils.py index 7642114715d3c..233d3c87efb28 100644 --- a/sklearn/utils/tests/test_utils.py +++ b/sklearn/utils/tests/test_utils.py @@ -19,7 +19,7 @@ from sklearn.utils import safe_indexing from sklearn.utils import shuffle from sklearn.utils import gen_even_slices -from sklearn.utils import _message_with_time, _log_elapsed +from sklearn.utils import _message_with_time, _print_elapsed_time from sklearn.utils import get_chunk_n_rows from sklearn.utils import is_scalar_nan from sklearn.utils.mocking import MockDataFrame @@ -314,9 +314,9 @@ def test_message_with_time(source, message, is_long, time, time_str): ('', _message_with_time('ABC', '', 0.1) + '\n'), (None, ''), ]) -def test_log_elapsed(message, expected, capsys, monkeypatch): +def test_print_elapsed_time(message, expected, capsys, monkeypatch): monkeypatch.setattr(timeit, 'default_timer', lambda: 0) - with _log_elapsed('ABC', message): + with _print_elapsed_time('ABC', message): monkeypatch.setattr(timeit, 'default_timer', lambda: 0.1) assert capsys.readouterr().out == expected From 94ed049111af0869765cc4e75299caf63382ad61 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Sun, 14 Apr 2019 14:15:19 -0400 Subject: [PATCH 62/64] STY --- sklearn/tests/test_pipeline.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 836cea45fbdf5..4a76d67b957dc 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1113,9 +1113,8 @@ def test_pipeline_param_error(): r'\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* total=.*\n' r'\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* total=.*\n$'), (FeatureUnion([('mult1', None), ('mult2', Mult()), ('mult3', None)]), - r'\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* total=.*\n$' - ) - ], ['fit', 'fit_transform', 'fit_predict']) + r'\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* total=.*\n$') + ], ['fit', 'fit_transform', 'fit_predict']) if hasattr(est, method) and not ( method == 'fit_transform' and hasattr(est, 'steps') and isinstance(est.steps[-1][1], FitParamT)) From 94e3bd56430a4702a18a07f49bce98abee561b6d Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Wed, 17 Apr 2019 20:04:11 -0400 Subject: [PATCH 63/64] CLN Uses the word processing --- examples/compose/plot_column_transformer.py | 2 +- sklearn/compose/_column_transformer.py | 2 +- .../compose/tests/test_column_transformer.py | 80 +++++++++---------- sklearn/pipeline.py | 13 +-- sklearn/tests/test_pipeline.py | 34 ++++---- 5 files changed, 59 insertions(+), 72 deletions(-) diff --git a/examples/compose/plot_column_transformer.py b/examples/compose/plot_column_transformer.py index 115c84b1a5968..02599a12396d6 100644 --- a/examples/compose/plot_column_transformer.py +++ b/examples/compose/plot_column_transformer.py @@ -117,7 +117,7 @@ def transform(self, posts): # Use a SVC classifier on the combined features ('svc', LinearSVC()), -]) +], verbose=True) # limit the list of categories to make running this example faster. categories = ['alt.atheism', 'talk.religion.misc'] diff --git a/sklearn/compose/_column_transformer.py b/sklearn/compose/_column_transformer.py index 558ac30522abf..a59e7962bbbb4 100644 --- a/sklearn/compose/_column_transformer.py +++ b/sklearn/compose/_column_transformer.py @@ -390,7 +390,7 @@ def _validate_output(self, result): def _log_message(self, name, idx, total): if not self.verbose: return None - return '(%d of %d) Fitting %s' % (idx, total, name) + return '(%d of %d) Processing %s' % (idx, total, name) def _fit_transform(self, X, y, func, fitted=False): """ diff --git a/sklearn/compose/tests/test_column_transformer.py b/sklearn/compose/tests/test_column_transformer.py index 9db87c90a89dc..c150492eae209 100644 --- a/sklearn/compose/tests/test_column_transformer.py +++ b/sklearn/compose/tests/test_column_transformer.py @@ -986,50 +986,42 @@ def test_column_transformer_no_estimators(): assert ct.transformers_[-1][2] == [0, 1, 2] -@pytest.mark.parametrize(['est', 'pattern'], [ - (ColumnTransformer( - [('trans1', Trans(), [0]), - ('trans2', Trans(), [1])], - remainder=DoubleTrans()), - (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* total=.*\n' - r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* total=.*\n' - r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* total=.*\n$')), - (ColumnTransformer( - [('trans1', Trans(), [0]), - ('trans2', Trans(), [1])], - remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* total=.*\n' - r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* total=.*\n' - r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* total=.*\n$')), - (ColumnTransformer( - [('trans1', Trans(), [0]), - ('trans2', 'drop', [1])], - remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* total=.*\n' - r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* total=.*\n$')), - (ColumnTransformer( - [('trans1', Trans(), [0]), - ('trans2', 'passthrough', [1])], - remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 3\) Fitting trans1.* total=.*\n' - r'\[ColumnTransformer\].*\(2 of 3\) Fitting trans2.* total=.*\n' - r'\[ColumnTransformer\].*\(3 of 3\) Fitting remainder.* total=.*\n$')), - (ColumnTransformer( - [('trans1', Trans(), [0])], - remainder='passthrough'), - (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* total=.*\n' - r'\[ColumnTransformer\].*\(2 of 2\) Fitting remainder.* total=.*\n$')), - (ColumnTransformer( - [('trans1', Trans(), [0]), - ('trans2', Trans(), [1])], - remainder='drop'), - (r'\[ColumnTransformer\].*\(1 of 2\) Fitting trans1.* total=.*\n' - r'\[ColumnTransformer\].*\(2 of 2\) Fitting trans2.* total=.*\n$')), - (ColumnTransformer( - [('trans1', Trans(), [0])], - remainder='drop'), - (r'\[ColumnTransformer\].*\(1 of 1\) Fitting trans1.* total=.*\n$')) -]) +@pytest.mark.parametrize( + ['est', 'pattern'], + [(ColumnTransformer([('trans1', Trans(), [0]), ('trans2', Trans(), [1])], + remainder=DoubleTrans()), + (r'\[ColumnTransformer\].*\(1 of 3\) Processing trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Processing trans2.* total=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Processing remainder.* total=.*\n$' + )), + (ColumnTransformer([('trans1', Trans(), [0]), ('trans2', Trans(), [1])], + remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 3\) Processing trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Processing trans2.* total=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Processing remainder.* total=.*\n$' + )), + (ColumnTransformer([('trans1', Trans(), [0]), ('trans2', 'drop', [1])], + remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 2\) Processing trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Processing remainder.* total=.*\n$' + )), + (ColumnTransformer([('trans1', Trans(), [0]), + ('trans2', 'passthrough', [1])], + remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 3\) Processing trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 3\) Processing trans2.* total=.*\n' + r'\[ColumnTransformer\].*\(3 of 3\) Processing remainder.* total=.*\n$' + )), + (ColumnTransformer([('trans1', Trans(), [0])], remainder='passthrough'), + (r'\[ColumnTransformer\].*\(1 of 2\) Processing trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Processing remainder.* total=.*\n$' + )), + (ColumnTransformer([('trans1', Trans(), [0]), ('trans2', Trans(), [1])], + remainder='drop'), + (r'\[ColumnTransformer\].*\(1 of 2\) Processing trans1.* total=.*\n' + r'\[ColumnTransformer\].*\(2 of 2\) Processing trans2.* total=.*\n$')), + (ColumnTransformer([('trans1', Trans(), [0])], remainder='drop'), + (r'\[ColumnTransformer\].*\(1 of 1\) Processing trans1.* total=.*\n$'))]) @pytest.mark.parametrize('method', ['fit', 'fit_transform']) def test_column_transformer_verbose(est, pattern, method, capsys): X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index b1c7a7df4d94d..70bef40d05743 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -254,14 +254,9 @@ def _log_message(self, step_idx): return None name, step = self.steps[step_idx] - if step is None or step == 'passthrough': - return '(step %d of %d) Passing %s' % (step_idx + 1, - len(self.steps), - name) - else: - return '(step %d of %d) Fitting %s' % (step_idx + 1, - len(self.steps), - name) + return '(step %d of %d) Processing %s' % (step_idx + 1, + len(self.steps), + name) # Estimator interface @@ -932,7 +927,7 @@ def fit_transform(self, X, y=None, **fit_params): def _log_message(self, name, idx, total): if not self.verbose: return None - return '(step %d of %d) Fitting %s' % (idx, total, name) + return '(step %d of %d) Processing %s' % (idx, total, name) def _parallel_func(self, X, y, fit_params, func): """Runs func in parallel on X and y""" diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py index 4a76d67b957dc..7728c24496cbe 100644 --- a/sklearn/tests/test_pipeline.py +++ b/sklearn/tests/test_pipeline.py @@ -1088,32 +1088,32 @@ def test_pipeline_param_error(): (est, pattern), method in itertools.product( [ (Pipeline([('transf', Transf()), ('clf', FitParamT())]), - r'\[Pipeline\].*\(step 1 of 2\) Fitting transf.* total=.*\n' - r'\[Pipeline\].*\(step 2 of 2\) Fitting clf.* total=.*\n$'), + r'\[Pipeline\].*\(step 1 of 2\) Processing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Processing clf.* total=.*\n$'), (Pipeline([('transf', Transf()), ('noop', None), ('clf', FitParamT())]), - r'\[Pipeline\].*\(step 1 of 3\) Fitting transf.* total=.*\n' - r'\[Pipeline\].*\(step 2 of 3\) Passing noop.* total=.*\n' - r'\[Pipeline\].*\(step 3 of 3\) Fitting clf.* total=.*\n$'), + r'\[Pipeline\].*\(step 1 of 3\) Processing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 3\) Processing noop.* total=.*\n' + r'\[Pipeline\].*\(step 3 of 3\) Processing clf.* total=.*\n$'), (Pipeline([('transf', Transf()), ('noop', 'passthrough'), ('clf', FitParamT())]), - r'\[Pipeline\].*\(step 1 of 3\) Fitting transf.* total=.*\n' - r'\[Pipeline\].*\(step 2 of 3\) Passing noop.* total=.*\n' - r'\[Pipeline\].*\(step 3 of 3\) Fitting clf.* total=.*\n$'), + r'\[Pipeline\].*\(step 1 of 3\) Processing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 3\) Processing noop.* total=.*\n' + r'\[Pipeline\].*\(step 3 of 3\) Processing clf.* total=.*\n$'), (Pipeline([('transf', Transf()), ('clf', None)]), - r'\[Pipeline\].*\(step 1 of 2\) Fitting transf.* total=.*\n' - r'\[Pipeline\].*\(step 2 of 2\) Passing clf.* total=.*\n$'), + r'\[Pipeline\].*\(step 1 of 2\) Processing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Processing clf.* total=.*\n$'), (Pipeline([('transf', None), ('mult', Mult())]), - r'\[Pipeline\].*\(step 1 of 2\) Passing transf.* total=.*\n' - r'\[Pipeline\].*\(step 2 of 2\) Fitting mult.* total=.*\n$'), + r'\[Pipeline\].*\(step 1 of 2\) Processing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Processing mult.* total=.*\n$'), (Pipeline([('transf', 'passthrough'), ('mult', Mult())]), - r'\[Pipeline\].*\(step 1 of 2\) Passing transf.* total=.*\n' - r'\[Pipeline\].*\(step 2 of 2\) Fitting mult.* total=.*\n$'), + r'\[Pipeline\].*\(step 1 of 2\) Processing transf.* total=.*\n' + r'\[Pipeline\].*\(step 2 of 2\) Processing mult.* total=.*\n$'), (FeatureUnion([('mult1', Mult()), ('mult2', Mult())]), - r'\[FeatureUnion\].*\(step 1 of 2\) Fitting mult1.* total=.*\n' - r'\[FeatureUnion\].*\(step 2 of 2\) Fitting mult2.* total=.*\n$'), + r'\[FeatureUnion\].*\(step 1 of 2\) Processing mult1.* total=.*\n' + r'\[FeatureUnion\].*\(step 2 of 2\) Processing mult2.* total=.*\n$'), (FeatureUnion([('mult1', None), ('mult2', Mult()), ('mult3', None)]), - r'\[FeatureUnion\].*\(step 1 of 1\) Fitting mult2.* total=.*\n$') + r'\[FeatureUnion\].*\(step 1 of 1\) Processing mult2.* total=.*\n$') ], ['fit', 'fit_transform', 'fit_predict']) if hasattr(est, method) and not ( method == 'fit_transform' and hasattr(est, 'steps') and From 76754543658771ced13942e843fe58e36f6ae324 Mon Sep 17 00:00:00 2001 From: Thomas Fan Date: Sat, 20 Apr 2019 08:42:14 -0400 Subject: [PATCH 64/64] CLN Address comments --- sklearn/pipeline.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py index 70bef40d05743..1fcdadaabb6c0 100644 --- a/sklearn/pipeline.py +++ b/sklearn/pipeline.py @@ -390,10 +390,9 @@ def fit_transform(self, X, y=None, **fit_params): if last_step == 'passthrough': return Xt if hasattr(last_step, 'fit_transform'): - Xt = last_step.fit_transform(Xt, y, **fit_params) + return last_step.fit_transform(Xt, y, **fit_params) else: - Xt = last_step.fit(Xt, y, **fit_params).transform(Xt) - return Xt + return last_step.fit(Xt, y, **fit_params).transform(Xt) @if_delegate_has_method(delegate='_final_estimator') def predict(self, X, **predict_params): @@ -710,7 +709,7 @@ def _fit_transform_one(transformer, """ Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned with the fitted transformer. If ``weight`` is not ``None``, the result will - be multipled by ``weight``. + be multiplied by ``weight``. """ with _print_elapsed_time(message_clsname, message): if hasattr(transformer, 'fit_transform'):