Open the pipeline, save it, extend it with steps of your own and tune it by name
list(full.named_steps)
full[-1] # indexing, like a list
full.named_steps['columntransformer'] \
.named_transformers_['num'][0].statistics_
full[:-1].transform(X_test.head(2)).shape
['columntransformer', 'kneighborsregressor']
KNeighborsRegressor(n_neighbors=10)
[-118.5 34.2 29. 2129. 437. 1169. 411. 3.5]
(2, 13)
named_transformers_ opens a branch of the ColumnTransformer: the eight training medians the imputer will use (437 bedrooms).full[:-1] is the preprocessing only, exactly what the model receives.print(full) gives the text diagram; in Jupyter the pipeline is drawn as a clickable diagram.
joblib.dump(full, 'housing-knn.joblib')
loaded = joblib.load('housing-knn.joblib')
np.array_equal(loaded.predict(X_test), full.predict(X_test)) # True
k-NN (k-nearest neighbors) keeps all 16,512 training districts to find neighbors; most models store far less.
The loaded pipeline takes raw rows and returns the same predictions, to the last digit.
log = FunctionTransformer(np.log1p, feature_names_out='one-to-one') # nothing to learn
| skewness of the four count columns | |
|---|---|
| before | 3.85, 3.32, 5.17, 3.26 |
| after \(\log(1+x)\) | −1.09, −1.0, −1.04, −1.06 |
fit does nothing, so it cannot leak. 'one-to-one' keeps get_feature_names_out working.A clipper: learn the 1st and 99th percentiles of each column on the training rows, pull every value outside them back to the bound.
__init__ only stores the hyperparameters.fit learns and returns self; learned state ends with _ (lower_, upper_).transform applies what was learned, after checking it was fit.TransformerMixin gives fit_transform; OneToOneFeatureMixin the feature names.total_rooms: 162.2 to 11,248.0
class Clipper(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
def __init__(self, low=1, high=99):
self.low = low # hyperparameters: stored, nothing else
self.high = high
def fit(self, X, y=None):
X = validate_data(self, X) # remembers the column names and count
self.lower_ = np.percentile(X, self.low, axis=0) # learned state ends with "_"
self.upper_ = np.percentile(X, self.high, axis=0)
return self
def transform(self, X):
check_is_fitted(self)
X = validate_data(self, X, reset=False) # same columns as in fit
return np.clip(X, self.lower_, self.upper_)
It goes in the numeric branch, between the imputer and the scaler: make_pipeline(SimpleImputer(strategy='median'), Clipper(), StandardScaler()).
total_rooms multiplied by 1,000 (a unit error). What happens to its prediction, with and without the clipper?| true value | $65,600 |
| correct row | $90,030 |
| typo, no clipper | $251,020 |
| typo, clipper | $145,690 |
Still wrong, and no transformer can fix bad input. But the damage is bounded, and the bound was learned from training data only.
full.get_params() # 46 parameters, e.g. columntransformer__num__simpleimputer__strategy
full.set_params(kneighborsregressor__n_neighbors=20)
step__parameter.GridSearchCV and these names.named_steps, indexing, named_transformers_; every learned value can be read.FunctionTransformer. Something to learn: a class whose fit stores attributes ending in _.step__parameter; a search refits the whole pipeline.preprocess, model, prep, X_train and X_test from steps 1 to 5.set_output); new raw passengers; save, load, compare; a transformer of your own: the title.Pace: steps 6 and 7 and the first simulation in 12 min; step 8 in 5 min; step 9 and the second in 13 min.