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
Before you can fit any model, the data must be in the shape scikit-learn expects. That shape is rigid and the library will not guess when it is wrong. Understanding the rules up front saves time that would otherwise go to debugging shape errors.
Every estimator in the library receives its data the same way. A features matrix and a target vector. The features matrix is always two-dimensional. The target vector is always one-dimensional. These constraints hold whether you have five rows or five million.
The data can come from a CSV, a database, a pandas DataFrame or one of the bundled toy datasets. The source does not matter. What matters is the final array shape that reaches fit. Getting it right is mechanical once you know the two rules.
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've 150 flowers and four readings of each, X.shape is (150, 4). The rule is universal across the library. It's the reason a table of data maps onto it so right. Rows are readings, columns are variables. This 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. This May be ints or strings. y.shape is (150,). Note the trailing comma. This Is what a one-dimensional shape looks like in NumPy.
The library won't guess when the shapes are wrong. 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. Those are fully 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 just the right shape. Almost each 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.
The default gives you a Bunch, and two arguments give you the shapes you usually want instead.
example_02.pyscikit-learn
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
print("return_X_y gives arrays:", type(X).__name__, X.shape, y.shape)
df = load_iris(as_frame=True).frame
print()
print("as_frame gives a DataFrame:", df.shape)
print(df.head(3))
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
from sklearn.datasets import make_classification, make_regression
Xc, yc = make_classification(n_samples=200, n_features=5, n_informative=3,
n_classes=2, random_state=0)
print("classification:", Xc.shape, "classes:", sorted(set(yc)))
Xr, yr = make_regression(n_samples=200, n_features=3, noise=10.0, random_state=0)
print("regression :", Xr.shape, "y range: %.1f to %.1f" % (yr.min(), yr.max()))
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
import numpy as np
rows = [
{"rooms": 2, "area": 55, "price": 180},
{"rooms": 3, "area": 78, "price": 260},
{"rooms": 4, "area": 96, "price": 310},
]
X = np.array([[r["rooms"], r["area"]] for r in rows])
y = np.array([r["price"] for r in rows])
print("X:", X.shape)
print(X)
print("y:", y.shape, y)
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
from sklearn.linear_model import LinearRegression
import numpy as np
X = np.array([[1], [2], [3]])
y = np.array([10, 20]) # one short
try:
LinearRegression().fit(X, y)
except ValueError as e:
print("ValueError:", e)
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
from sklearn.datasets import load_wine
bunch = load_wine()
print("a Bunch is a dict you can also use with dots")
print("keys:", sorted(k for k in bunch.keys() if k != "DESCR"))
print()
print("bunch.data is bunch['data']:", bunch.data is bunch["data"])
print("samples per class:", [int((bunch.target == c).sum()) for c in range(3)])
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're tiny, they need no network. They're what almost each example in the docs uses. Their size is the point. A model fits in milliseconds. You can try a thing and see the result right away.
fetch_* downloads a larger, more realistic dataset the first time and caches it. fetch_california_housing, fetch_20newsgroups, fetch_openml for any of it on OpenML. These are the ones to use when the toy datasets stop being lifelike. But they need a network. This Is why this track stays with the bundled ones.
make_* makes data with the structure you asked for. make_classification, make_regression, make_blobs, make_moons. These are the most underrated of the three. They let you make just the situation you want to study. A dataset with two helpful features and eight useless ones, or classes that are on purpose not linearly separable, or a regression with a known amount of noise. When you're testing whether a technique does what you think, made data with known properties beats real data whose properties you're guessing at.
The Bunch, and the two ways past it
The load_* and fetch_* functions return a Bunch. This Is a dict that also allows field access. data.target and data["target"] are the same object. This Is why examples use both spellings alike.
A Bunch carries more than the arrays. feature_names gives the column names, target_names maps the int labels back to a thing readable. DESCR holds a full description of the dataset. Where it came from, what each column means. How many samples there are. Printing DESCR is the fastest way to understand an unseen bundled dataset, and it's routinely skipped.
return_X_y=True skips the Bunch and returns the two arrays as a tuple. This 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. data.data turns a DataFrame with named columns. This matters more than it first appears. A DataFrame carries its column names into the estimator. This Is what lets a fitted model report feature_names_in_ and what lets a ColumnTransformer select columns by name rather than by spot.
Turning your own data into X and y
Real data rarely arrives in the right shape, and the conversion is often one comprehension.
From a list of dicts, 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 each row. This Is just what a list comprehension ensures and a hand-written loop doesn't.
From a pandas DataFrame it's shorter still: X = df[["rooms", "area"]] and y = df["price"]. Selecting with a list of column names gives a DataFrame. This Is 2-D and so a valid X. Selecting with a single name gives a Series. This Is 1-D and so a valid y. Getting those two confused. df["rooms"] where df[["rooms"]] was meant. Makes the 1-D error message. The doubled brackets are the fix.
The one rule that lives each source: decide the column order once. Keep it. A model fitted on columns in one order and given new data in another won't complain. It will give confident nonsense. Column three is column three what it used to mean.
The two errors, and what they are telling you
"Expected 2D array, got 1D array instead." You passed a thing with one dim 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's a single sample. Choosing between those two is choosing what your data means. -1 tells NumPy to work that dim out from the length.
"Found input variables with inconsistent numbers of samples: [3, 2]."X and y disagree about how many rows there are. 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. This Makes them among the more helpful errors you'll meet. Reading them before changing any of it's faster than guessing. The shape they report is often enough on its own to spot which of the two objects is wrong.
Sparse matrices, and when one appears
Some transformers don't return a normal array. OneHotEncoder and the text vectorisers return a sparse mat. This Stores only the non-zero entries and their spots.
The reason is size. One-hot encoding a column with ten thousand distinct values makes ten thousand columns, almost all of them zero on any given row. Stored densely that is huge and almost fully wasted. Stored sparsely it's a list of the few spots that aren't zero.
You'll 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. Not each method can be written to work on that form.
.toarray() converts to a dense array. Is the right move only when you're certain the result fits in memory. Which is just the case the sparse form existed to avoid. sparse_output=False on the encoder is the better fix when the number of columns is truly small. Most of the time the correct answer is to leave it sparse. 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. A column measured in millions next to one measured in fractions is the situation that makes scaling needed. Whether any of it's missing. np.isnan(X).sum() costs nothing and most estimators refuse to fit with NaN present. And how the target is spread. For a classifier, the count per class, because that single number decides whether fit is a real metric at all.
None of these need plotting or a lengthy exploration. Four lines before the first fit catch the majority of problems that would or else 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% fit. A reader who hasn't counted will believe it.
Feature names, and why they are worth keeping
Passing NumPy arrays works, and passing DataFrames gives you a thing arrays can't: the model remembers what the columns were called.
A fitted estimator that was given a DataFrame gains feature_names_in_. A few report results against those names rather than against spots. feature_importances_ on a tree, coef_ on a linear model. The output of get_feature_names_out() on a transformer are all far easier to read when there's 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. Scikit-learn raises rather than silently using the wrong column. Which is the failure mode that arrays can't protect you from at all.
The cost is that a DataFrame is slower than an array and that some ops convert back to arrays anyway, losing the names partway through a pipeline. That is why get_feature_names_out() exists on transformers. It rebuilds the names on the other side of a step that dropped them, including the invented names that one-hot encoding makes.
The habit worth adopting: use DataFrames at the boundary where data enters. Stop worrying about whether the middle of the pipeline is arrays or frames.
Should I use the bundled datasets for any of it real? No. They're for learning the mechanics. Iris in given is small, clean and nearly separable. This Makes almost each 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 with Loading and Shaping Data
Print the description. Add print(load_iris().DESCR[:800]) to the first editor. It shows the columns, the classes and where the data came from.
Change the made data. In the third editor, set n_informative=1 and see that four of the five features are noise by build. handy when testing whether a model can skip them.
Trigger the shape error on purpose. Pass data.data[0] as X and read what it suggests. One sample needs reshape(1, -1), not reshape(-1, 1).
Compare the two forms. Fit any of it on as_frame=True data, then check model.feature_names_in_. With plain arrays that field doesn't exist.
Questions people ask about Loading and Shaping Data
What does return_X_y=True do? It makes loaders such as load_iris return a plain (X, y) tuple instead of a Bunch object, which is shorter when you only need the arrays.
How do I get my own CSV into X and y? Read it with pandas, pick the target column as y, and keep the remaining columns as X. Check the shapes before fitting.
Why does scikit-learn complain about a 1-D array? Features must be 2-D with one row per sample. A single column needs reshape(-1, 1), and a single sample needs reshape(1, -1).
Is a sparse matrix a problem? Usually not. Many estimators accept sparse input directly, and it saves memory when most entries are zero, as in text counts.
Should I keep feature names? Yes. A DataFrame input lets the fitted model remember column names, which makes coefficients and importances readable later.
Can the data contain strings or NaN? Not for most estimators. Strings must be encoded and missing values imputed first, or the call raises an error.
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 any of it: 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
Load a dataset into a feature matrix and target vector with the exact shapes scikit-learn expects before any model can be fitted.
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.