Where practice data comes from, what shape the library insists on, and the two error messages you will meet before anything else works.
Overview
The shape the library insists on
scikit-learn takes two things: a two-dimensional X and a one-dimensional y.
X has one row per sample and one column per feature. If you have 150 flowers and four measurements of each, X.shape is (150, 4). The convention is universal across the library, and it is the reason a table of data maps onto it so directly — rows are observations, columns are variables, which is how a spreadsheet is already arranged.
y has one entry per row of X. For a regression it holds numbers; for a classification it holds labels, which may be integers or strings. y.shape is (150,) — note the trailing comma, which is what a one-dimensional shape looks like in NumPy.
The library will not guess when the shapes are wrong, and that refusal is worth appreciating rather than resenting. A list of six numbers could be six samples of one feature or one sample of six features, and those are entirely different problems. Rather than pick one, scikit-learn raises and tells you what it received.
Worth knowing
X is 2-D - rows are samples, columns are features. y is 1-D, one entry per row of X.
load_* functions ship data with the library; fetch_* download it; make_* generate it.
return_X_y=True skips the Bunch and hands back the two arrays directly.
as_frame=True gives a DataFrame with the feature names attached, which is what keeps names alive through a pipeline.
"Expected 2D array, got 1D array instead" means a single feature needs reshape(-1, 1).
"Found input variables with inconsistent numbers of samples" means X and y have different row counts - the message prints both.
Loading and Shaping Data: A Practical Guide
Before any model can be fitted, the data has to be two objects of exactly the right shape. Almost every first error on this track is one of the two this page ends with.
A dataset that ships with the library
Seven small datasets come with scikit-learn, so nothing has to be downloaded before you can try something.
example_01.pyscikit-learn
Output
Three ways to take the same data out
The default gives you a Bunch, and two arguments give you the shapes you usually want instead.
example_02.pyscikit-learn
Output
Data you make up, with the properties you want
The make_* functions generate data with a known structure, which is how you test an idea without hunting for a dataset.
example_03.pyscikit-learn
Output
Your own data, arranged the way the library wants
One row per sample, one column per feature - and the target separate.
example_04.pyscikit-learn
Output
X and y must agree on the number of rows
The second error message everyone meets, and it counts both for you.
example_05.pyscikit-learn
Output
A Bunch is a dictionary with attribute access
Which is why you will see both bunch.data and bunch['data'] in examples, meaning the same thing.
example_06.pyscikit-learn
Output
Where practice data comes from
Three families of function, distinguished by their prefix.
load_* returns a small dataset that ships inside the installed package. load_iris, load_wine, load_digits, load_diabetes, load_breast_cancer and a couple more. They are tiny, they need no network, and they are what almost every example in the documentation uses. Their size is the point: a model fits in milliseconds, so you can try something and see the result immediately.
fetch_* downloads a larger, more realistic dataset the first time and caches it. fetch_california_housing, fetch_20newsgroups, fetch_openml for anything on OpenML. These are the ones to use when the toy datasets stop being convincing — but they need a network, which is why this track stays with the bundled ones.
make_* generates data with the structure you asked for. make_classification, make_regression, make_blobs, make_moons. These are the most underrated of the three, because they let you construct exactly the situation you want to study: a dataset with two informative features and eight useless ones, or classes that are deliberately not linearly separable, or a regression with a known amount of noise. When you are testing whether a technique does what you think, generated data with known properties beats real data whose properties you are guessing at.
The Bunch, and the two ways past it
The load_* and fetch_* functions return a Bunch, which is a dictionary that also allows attribute access. data.target and data["target"] are the same object, which is why examples use both spellings interchangeably.
A Bunch carries more than the arrays. feature_names gives the column names, target_names maps the integer labels back to something readable, and DESCR holds a full description of the dataset — where it came from, what each column means, and how many samples there are. Printing DESCR is the fastest way to understand an unfamiliar bundled dataset, and it is routinely ignored.
return_X_y=True skips the Bunch and returns the two arrays as a tuple, which is what you want when you already know the dataset and just need the data. It makes the common line short: X, y = load_iris(return_X_y=True).
as_frame=True returns pandas objects instead of NumPy arrays: data.frame is a DataFrame with the target as a column, and data.data becomes a DataFrame with named columns. This matters more than it first appears, because a DataFrame carries its column names into the estimator, which is what lets a fitted model report feature_names_in_ and what lets a ColumnTransformer select columns by name rather than by position.
Turning your own data into X and y
Real data rarely arrives in the right shape, and the conversion is usually one comprehension.
From a list of dictionaries, build X by pulling the feature keys in a fixed order and y by pulling the target key. The order matters and must be the same for every row, which is exactly what a list comprehension guarantees and a hand-written loop does not.
From a pandas DataFrame it is shorter still: X = df[["rooms", "area"]] and y = df["price"]. Selecting with a list of column names gives a DataFrame, which is 2-D and therefore a valid X; selecting with a single name gives a Series, which is 1-D and therefore a valid y. Getting those two confused — df["rooms"] where df[["rooms"]] was meant — produces the 1-D error message, and the doubled brackets are the fix.
The one rule that survives every source: decide the column order once, and keep it. A model fitted on columns in one order and given new data in another will not complain. It will produce confident nonsense, because column three is column three whatever it used to mean.
The two errors, and what they are telling you
"Expected 2D array, got 1D array instead." You passed something with one dimension where X was wanted. The message goes on to suggest reshape(-1, 1) if the data has a single feature and reshape(1, -1) if it is a single sample, and choosing between those two is choosing what your data means. -1 tells NumPy to work that dimension out from the length.
"Found input variables with inconsistent numbers of samples: [3, 2]."X and y disagree about how many rows there are, and the numbers in brackets are the two counts in order. This one almost always means an upstream filter was applied to one and not the other — dropping rows with missing targets from y but not from X, say — and the fix belongs there rather than at the call that raised.
Both messages name the shapes involved, which makes them among the more helpful errors you will meet. Reading them before changing anything is faster than guessing, and the shape they report is usually enough on its own to identify which of the two objects is wrong.
Sparse matrices, and when one appears
Some transformers do not return a normal array. OneHotEncoder and the text vectorisers return a sparse matrix, which stores only the non-zero entries and their positions.
The reason is size. One-hot encoding a column with ten thousand distinct values produces ten thousand columns, almost all of them zero on any given row. Stored densely that is enormous and almost entirely wasted; stored sparsely it is a list of the few positions that are not zero.
You will notice it in three ways. Printing one shows a summary rather than the numbers. Indexing behaves differently from a NumPy array. And some estimators accept it happily while others raise, because not every algorithm can be written to work on that representation.
.toarray() converts to a dense array, and is the right move only when you are certain the result fits in memory — which is exactly the case the sparse representation existed to avoid. sparse_output=False on the encoder is the better fix when the number of columns is genuinely small. Most of the time the correct answer is to leave it sparse, because the estimators that matter for high-dimensional data all handle it.
Looking at the data before fitting anything
The step that gets skipped, and the one that catches the problems a model will silently absorb.
Four questions are worth answering before any fit. How many rows and columns, from X.shape — a model with more features than samples behaves quite differently from one with the reverse. What range each feature covers, because a column measured in millions next to one measured in fractions is the situation that makes scaling necessary. Whether anything is missing, since np.isnan(X).sum() costs nothing and most estimators refuse to fit with NaN present. And how the target is distributed — for a classifier, the count per class, because that single number decides whether accuracy is a meaningful metric at all.
None of these require plotting or a lengthy exploration. Four lines before the first fit catch the majority of problems that would otherwise appear later as an inexplicable score.
The one that matters most is the class balance. A dataset that is 99% one class will let almost any classifier report 99% accuracy, and a reader who has not counted will believe it.
Feature names, and why they are worth keeping
Passing NumPy arrays works, and passing DataFrames gives you something arrays cannot: the model remembers what the columns were called.
A fitted estimator that was given a DataFrame gains feature_names_in_, and several report results against those names rather than against positions. feature_importances_ on a tree, coef_ on a linear model, and the output of get_feature_names_out() on a transformer are all far easier to read when there is a name attached to each number.
It also adds a safety check. Fit on a DataFrame and then predict on one whose columns are in a different order, and scikit-learn raises rather than silently using the wrong column — which is the failure mode that arrays cannot protect you from at all.
The cost is that a DataFrame is slower than an array and that some operations convert back to arrays anyway, losing the names partway through a pipeline. That is why get_feature_names_out() exists on transformers: it reconstructs the names on the other side of a step that dropped them, including the invented names that one-hot encoding produces.
The habit worth adopting: use DataFrames at the boundary where data enters, and stop worrying about whether the middle of the pipeline is arrays or frames.
Should I use the bundled datasets for anything real? No. They are for learning the mechanics. Iris in particular is small, clean and nearly separable, which makes almost every method look good on it.
Can I pass a Python list instead of an array? Yes, for X and y both - scikit-learn converts them. Arrays and DataFrames are preferable because they carry a dtype and, for frames, the column names.
Things to try
Print the description. Add print(load_iris().DESCR[:800]) to the first editor. It explains the columns, the classes and where the data came from.
Change the generated data. In the third editor, set n_informative=1 and see that four of the five features are noise by construction — useful when testing whether a model can ignore them.
Trigger the shape error deliberately. Pass data.data[0] as X and read what it suggests. One sample needs reshape(1, -1), not reshape(-1, 1).
Compare the two representations. Fit anything on as_frame=True data, then check model.feature_names_in_. With plain arrays that attribute does not exist.
Where this leaves you
Two objects, two shapes, and three prefixes for finding data to practise on. The next module takes that data and does the one thing that has to happen before any score means anything: splitting it.
Check yourself
0 of 4
Answer without scrolling back up.
What shape must X have?
Rows are samples and columns are features, which is why a single feature still needs reshape(-1, 1).
What does return_X_y=True change?
It skips the Bunch wrapper, which is what makes `X, y = load_iris(return_X_y=True)` a one-liner.
Which prefix generates synthetic data with properties you choose?
make_classification, make_regression and make_blobs construct data with a known structure, which is ideal for testing whether a technique behaves as expected.
"Found input variables with inconsistent numbers of samples" means what?
The numbers in the brackets are the two row counts. It usually means a filter was applied to one of the two and not the other.
Cheat sheet
Loading and Shaping Data
Before any model can be fitted, the data has to be two objects of exactly the right shape. Almost every first error on this track is one of the two this page ends with.
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.