The estimator API
Four methods, two kinds of number, and one very informative underscore
fitpredicttransformscoreget_paramstrailing underscoreWatch it happen
Play it through, or step back and forth yourself.
model = LogisticRegression(C=1.0, max_iter=1000)
model.fit(X_train, y_train)model.predict(X_test)model.score(X_test, y_test)fit(X, y)learnpredict(X)applytransform(X)apply, for preprocessorsscore(X, y)one numberget_params() / set_params()the hyperparametersscikit-learn's real achievement isn't any one algorithm — it's that all of them wear the same interface. Learn these four methods and you can drive a model you've never heard of.
The idea
scikit-learn's real achievement isn't any particular algorithm — it's that two hundred of them wear the same interface. Learn it once and you can drive a model you've never heard of, which is why this short lesson pays for itself repeatedly.
Construct, then fit
model = LogisticRegression(C=1.0, max_iter=1000) # learns nothing
model.fit(X_train, y_train) # learns everythingThe constructor only records your choices. Nothing is computed, no data is touched — you can build an estimator with no data in the room at all. All the work happens in fit, which reads the data and writes what it found onto the object.
Two properties of fit worth knowing. It returns the object itself, so model.fit(X, y).predict(X_test) chains. And it replaces rather than accumulates: fit a second time and the first fit is gone. (A handful of estimators offer partial_fit for genuine incremental learning; the default is a clean slate.)
The trailing underscore
This is the convention worth internalising, because it is load-bearing across the whole library. An attribute ending in _ was learned from the data. Before fit, it doesn't exist:
model = LogisticRegression()
hasattr(model, "coef_") # False
model.coef_ # AttributeError: not fitted yet
model.fit(X_train, y_train)
hasattr(model, "coef_") # True
model.coef_ # [[1.018, 0.173, 0.076, -0.125, 0.995]]
model.classes_ # [0 1]
model.n_features_in_ # 5It holds everywhere, and it tells you where to look when you want to know what a model learned:
StandardScaler mean_, scale_
SimpleImputer statistics_
OneHotEncoder categories_
DecisionTreeClassifier tree_, feature_importances_
KMeans cluster_centers_, labels_
PCA components_, explained_variance_It's also how NotFittedError works internally — scikit-learn checks for the underscored attributes. When you see that error, you called predict before fit, or on a copy that was never fitted.
The two kinds of number
Every model has numbers in it, and they come from two completely different places. Confusing them is the single most common source of muddle for people starting out.
| Hyperparameters | Parameters |
|---|---|
| You choose them | The data chooses them |
| Before fitting, in the constructor | During fit |
| No underscore | Trailing underscore |
C=1.0, max_depth=4 | coef_, tree_ |
Module 6 is entirely about choosing hyperparameters well; lesson 25 is about reading parameters to understand what the model learned. Keeping the two straight now saves a lot of confusion later.
model.get_params() # everything you chose
model.set_params(C=0.1) # change one, without rebuildingget_params isn't just introspection — it's the mechanism GridSearchCV uses to try different settings, and how clone() makes a fresh unfitted copy. Because every estimator implements it, tools can manipulate models they know nothing about.
predict, transform, score
predict(X) applies what was learned to new rows and changes nothing. Classifiers also offer predict_proba(X), which gives the probabilities behind those labels — lesson 7 is about why you usually want those instead.
transform(X) is what preprocessors have where models have predict. Same contract: fit learns (the column medians, the category list), transform applies. And:
X_train_s = scaler.fit_transform(X_train) # fit AND transform — training only
X_test_s = scaler.transform(X_test) # transform ONLY — never fit hereThat asymmetry is not a style preference. Fitting the scaler on the test set lets the test set's means influence the training data — the test set stops being unseen. Lesson 13 makes it impossible to get wrong by putting everything in a Pipeline.
score(X, y) gives one number: accuracy for classifiers, R² for regressors. It's convenient, and it's a trap, because it picks a metric for you and the default is very often the wrong one. The whole of module 2 is about that.
Why it's worth caring about the interface
Because it composes. A Pipeline is an estimator; so is a ColumnTransformer, and a GridSearchCV. Each of them implements fit/predict, so they nest inside each other freely and every tool in the library accepts them. Swapping LogisticRegression for RandomForestClassifier inside a tuned pipeline is a one-word change — and that is a direct consequence of these four method names.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
trycalling model.predict(...) before fit and reading the NotFittedError carefully.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Build a LogisticRegression and do not fit it. Return whether it has a coef_ yet, as a bool.
Fit a SimpleImputer(strategy="median") on X[NUMERIC] and return what it learned — statistics_, rounded to 2 decimal places, as a list.
Sort the two kinds apart. Return a list of only the hyperparameters from ["C", "coef_", "max_iter", "classes_", "n_features_in_"] — the ones you set rather than the ones the data set.
Show that fit returns the object itself — which is what makes model.fit(X, y).predict(X) work. Return model.fit(...) is model as a bool.
