fit, predict, transform

Three method names that every model in the library shares - which is why swapping one model for another is a one-line change.

Overview

The problem it solves

Every machine-learning library has to answer the same question: how does a program hand data to an algorithm and get an answer back. scikit-learn answered it once, in 2007, and then made every algorithm in the library answer it the same way.

The result is that fit, predict and transform are almost the whole API. A linear regression, a random forest, a support-vector machine and a k-means clustering all expose the same three methods, so the code around a model does not change when the model does.

Worth knowing

fit learns from data, predict produces a target, transform produces new features. Every object in the library is some combination of those three.
An attribute ending in _ was learned during fit. One without was set by you.
X is 2-D with one row per sample and one column per feature; y is 1-D with one entry per sample.
A single feature still needs two dimensions - reshape(-1, 1) is the usual fix.
fit returns the estimator, which is why Model().fit(X, y) chains.
Calling predict before fit raises NotFittedError rather than guessing.

fit, predict, transform: A Practical Guide

scikit-learn has one interface and about two hundred algorithms behind it. Learning the interface is most of learning the library.

Every model is the same three methods

The library's whole design is that you learn the interface once, not once per algorithm.

example_01.pyscikit-learn
Output

The trailing underscore means learned

Attributes ending in an underscore did not exist until fit ran. It is a naming convention, and it is the whole story of what fitting does.

example_02.pyscikit-learn
Output

fit returns the estimator, so it chains

Every fit returns self. That is why you will see .fit(X, y) attached to the constructor on one line.

example_03.pyscikit-learn
Output

Transformers use transform instead of predict

A model predicts a target. A transformer changes the features. The fit half is identical.

example_04.pyscikit-learn
Output

Swapping the model is one line

Same data, same calls, different algorithm - which is the payoff for having one interface.

example_05.pyscikit-learn
Output

X is always 2-D, y is always 1-D

The single most common error message on this track comes from getting this wrong, and it tells you exactly what it wanted.

example_06.pyscikit-learn
Output

fit learns, predict answers

fit(X, y) shows the estimator the data and the answers, and it stores what it worked out on the object. predict(X) takes new data with no answers and produces them.

The split matters more than it looks. Fitting and predicting are separate calls because they happen at different times on different data — you fit once on data you have, and predict many times on data you did not have when you fitted.

The underscore convention

scikit-learn marks learned attributes with a trailing underscore, and the convention is worth taking seriously because it divides an estimator into two halves.

Attributes without an underscore are hyperparameters: settings you chose, passed to the constructor, present before any data was seen. n_neighbors, max_depth, fit_intercept.

Attributes with a trailing underscore are learned: they did not exist before fit and they came from the data. coef_, intercept_, feature_importances_, classes_.

Reading an estimator therefore tells you what it was told and what it worked out, and the two never get confused. It also means hasattr(model, "coef_") is a reliable test for "has this been fitted", which is exactly what the library's own check_is_fitted does.

Transformers change features rather than predicting

A transformer is the other half of the library. Instead of predict it has transform, and instead of producing a target it produces a new version of X.

StandardScaler is the canonical one: fit computes the mean and standard deviation of each column, transform subtracts and divides. OneHotEncoder, SimpleImputer and PCA are all the same shape.

fit_transform does both in one call and is the one you will write most often on training data. It is not merely shorthand — for some transformers it is faster than doing the two separately, which is why it exists as its own method.

The rule that matters: fit on training data only, transform everything. A scaler fitted on the test set has learned from data the model is supposed to have never seen, and the score you get afterwards is not a score.

The shapes

X is two-dimensional: rows are samples, columns are features. y is one-dimensional: one value per row of X.

That holds even when there is one feature, which is where beginners meet their first error. A list of six numbers is six samples of nothing, not one sample of six features, and scikit-learn refuses to guess which you meant. reshape(-1, 1) turns a flat array into a column, and the error message says so explicitly.

The two kinds of estimator

Everything in the library is one of two things, and telling them apart tells you which method to call.

A predictor ends in a target. LinearRegression, LogisticRegression, RandomForestClassifier, KMeans — you fit them and then predict. Classifiers additionally offer predict_proba, which returns the probability of each class rather than the winning one.

A transformer ends in new features. StandardScaler, OneHotEncoder, SimpleImputer, PCA — you fit them and then transform. There is no target to produce, so there is no predict.

The two compose: a chain of transformers followed by one predictor is the shape of essentially every real scikit-learn program, and Pipeline exists to hold exactly that chain and give it the same fit/predict interface as a single estimator. That is why the API is worth learning first — a whole pipeline is used the same way as the simplest model in the library.

score, and what it means for each

Every predictor has a score(X, y) method, and the number it returns is not the same quantity for all of them.

For a classifier, score is accuracy: the proportion of predictions that were right. For a regressor, it is R²: how much of the variation in the target the model accounts for, where 1.0 is perfect and 0.0 is no better than always guessing the mean. R² can be negative, which means the model is worse than that guess.

Both are conveniences rather than recommendations. Accuracy is misleading whenever the classes are imbalanced, and R² says nothing about whether the errors are large in the units you care about. Later modules replace both with metrics that answer a specific question, and the metrics module holds several dozen of them.

What score is genuinely useful for is a quick comparison on the same data with the same model type, and for the cross-validation helpers, which call it by default when you do not name a metric.

Hyperparameters are set at construction

The constructor takes the settings, and there are usually many of them with sensible defaults. RandomForestClassifier() works, and so does RandomForestClassifier(n_estimators=500, max_depth=8, random_state=0).

Two methods make those settings inspectable. get_params() returns every hyperparameter as a dictionary, including the defaults you did not pass, which is the fastest way to find out what an estimator can be configured with. set_params(**kw) changes them on an existing object.

Those two are not conveniences either — they are what makes automated tuning possible. GridSearchCV works by calling set_params with each combination in turn, which is why it can tune any estimator in the library, including ones written after it, and any step inside a pipeline.

The corollary is worth remembering: an estimator is fully described by its hyperparameters plus its fitted attributes. Nothing else is hidden on it.

random_state, and reproducibility

Anything in the library that makes a random choice takes a random_state. Splitting data, initialising k-means, sampling features in a random forest, shuffling folds — all of them.

Passing an integer makes the run reproducible: the same data and the same seed give the same result, every time, on any machine. Leaving it out gives a different answer on each run, which makes two scores incomparable and a bug impossible to reproduce.

The habit worth forming early is to pass random_state=0 to everything that accepts it while you are learning or debugging, and to think carefully before removing it. The one time you genuinely want it left out is when you are deliberately measuring how much the result varies between runs — which is a real question, and one that a single seeded number cannot answer.

When an estimator has not been fitted

Calling predict before fit raises NotFittedError rather than returning nonsense, and the message names the estimator and tells you to call fit.

That check exists because the alternative is worse. An unfitted model has no coefficients, and a library that quietly returned zeros or None would produce a program that runs, produces numbers, and is entirely meaningless.

It is also why the underscore convention matters practically rather than only stylistically: check_is_fitted works by looking for attributes ending in an underscore. An estimator you write yourself gets the same behaviour for free by following the same convention.

Why one interface was the right decision

It is worth appreciating how unusual this is. Most machine-learning code before scikit-learn had a shape per algorithm: one library wanted a matrix and a separate label vector, another wanted them combined, a third wanted a configuration file. Comparing two algorithms meant rewriting the code around them, which meant people compared far fewer than they should have.

Fixing the interface changed what is cheap. Swapping a model becomes one line, so trying five is a loop rather than a project. Cross-validation can be written once and work with anything, because it only needs fit, predict and score. A pipeline can hold arbitrary steps, because every step honours fit and transform. A tuner can search any estimator, because get_params and set_params are universal.

None of that required the algorithms to have anything in common mathematically. A decision tree and a linear model share no theory at all; they share three method names, and that turned out to be enough to build the entire ecosystem of helpers on top.

The practical consequence for someone learning: time spent on the API is not overhead before the interesting part. It *is* the part that transfers. The estimators are individually simple to use once the shape is familiar, and the modules that follow spend most of their words on when each one is appropriate and how to tell whether it worked, rather than on how to call it.

What the library deliberately does not do

Knowing the boundary saves looking for things that are not there.

scikit-learn does not do deep learning. There is a small MLPClassifier, useful for a demonstration and not for real work; anything serious belongs in PyTorch or a similar library. It does not do GPUs. It does not do sequence models, text generation, or anything with the word "neural" beyond that one estimator.

It does not handle data loading, cleaning or plotting. Data arrives as arrays or DataFrames that pandas produced, and results are plotted with matplotlib. Those are separate libraries on purpose, and the boundary is clean: scikit-learn takes numeric arrays and returns numeric arrays.

It also does not do statistical inference. A linear model gives you coefficients and no p-values, no confidence intervals and no hypothesis tests, because the library is built around prediction rather than explanation. statsmodels is the library for that question, and reaching for it is the right answer rather than a workaround.

Is the API stable? Remarkably so. Code written against fit/predict a decade ago still runs, which is unusual in this field and is a large part of why the library is worth learning properly.

Things to try

  1. Run the first editor. hasattr(model, "coef_") is False before fit and True after. That is the whole of what fitting does, visible in one line.
  2. Change the data. Make the relationship non-linear — y = [1, 4, 9, 16, 25, 36] — and watch coef_ become a compromise rather than an exact fit.
  3. Swap the model. In the fifth editor, add from sklearn.svm import SVR and put SVR() in the list. Nothing else changes.
  4. Break the shape. In the last editor, read the error properly. It names the shape it got and the shape it wanted.

Where this leaves you

Three method names, one shape convention and one naming convention cover the surface of the entire library. Everything from here is a choice of estimator and, far more importantly, whether the number it reports can be believed.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What does a trailing underscore on an attribute mean?

  2. What shape must X be?

  3. What does a transformer have instead of predict?

  4. Why does fit return the estimator?

Cheat sheet

fit, predict, transform

Every machine-learning library has to answer the same question: how does a program hand data to an algorithm and get an answer back. scikit-learn answered it once, in 2007, and then made every algorithm in the library answer it the same way.

SCIKIT-LEARN · vizlearn.in/sklearn/what_is_scikit_learn.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.