39 · Series: 1D labeled arrays
A pandas Series is a NumPy array with an index attached — the labels unlock alignment-aware arithmetic, label-based access, and the convenience methods that make exploratory data analysis fast.
A `pd.Series` is a NumPy ndarray plus an **index** — an array of labels attached to each value. The index unlocks two things NumPy can't do: label-based access (`s.loc['alice']` instead of `s[0]`) and **index-aligned arithmetic** (adding two Series aligns them by label before computing, inserting `NaN` for any label that appears in only one Series). That automatic alignment is what prevents the silent 'off-by-one row' bugs that plague plain-array data pipelines.
Without this:
Without understanding Series indexing, combining two datasets by row position instead of by label is a subtle and devastating bug — model predictions aligned to the wrong samples, class labels attached to the wrong features. The `.loc` / `.iloc` distinction is also the gateway to understanding DataFrame indexing, and every Pandas debugging session eventually comes back to 'which one should I use here?'
A Series is the fundamental 1D data structure in pandas. Think of it as a NumPy array with an extra layer: every value has a label (the index). The label can be a string, an integer, a date — anything hashable. This label layer is what makes pandas data structures different from bare NumPy arrays.
When you do arithmetic on two NumPy arrays, NumPy aligns them by position — element 0 pairs with element 0. When you do arithmetic on two Series, pandas aligns them by label — the value labelled 'alice' in the first Series pairs with the value labelled 'alice' in the second. If a label appears in only one Series, the result for that label is NaN. This design prevents an entire class of silent bugs.
Series have two indexing APIs:
.loc[label]— label-based lookup. Use the label as written in the index..iloc[i]— integer position-based lookup. Always 0-based, always by position, regardless of what the index contains.
Using .loc on an integer-indexed Series looks the same as .iloc, which is a common trap. When in doubt, be explicit: use .loc when you mean 'find by label' and .iloc when you mean 'find by position'.
Python (in browser)
`pd.Series(dict)` creates a Series with the dict keys as the index. `.idxmax()` returns the **label** of the maximum value — not the position — which is exactly what you need when the index contains meaningful names like model identifiers or feature names.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`.loc['alice']` always finds Alice regardless of her position in the index. `.iloc[0]` always finds position 0 regardless of the label. When the index is `['carol', 'alice', 'bob']`, `.iloc[0]` returns Carol's score — a silent bug if you expected Alice. Always use the right accessor for the intent.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
Index alignment is pandas' superpower and its biggest footgun. The automatic `NaN` insertion tells you exactly where data is missing — it never silently pairs the wrong rows. In a production ML pipeline, unexpected `NaN` after arithmetic is almost always a sign of a key mismatch between two datasets.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`s.isna().sum()` is the first command to run on any column of a new dataset — it tells you how much data is missing. `.fillna(s.mean())` is the simplest imputation strategy (mean imputation). `.dropna()` is appropriate when you have enough data to discard rows; `.fillna()` is better when every row is precious.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`.value_counts()` is the go-to command for checking **class distribution** before training a classifier. An imbalanced distribution (e.g., 950 'cat' vs 50 'dog') tells you to apply class weighting or oversampling before fitting — discovering this after training is expensive.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
A Series `s` has index `['b', 'a', 'c']` and values `[10, 20, 30]`. What does `s.iloc[0]` return?
- A `pd.Series` is a NumPy ndarray plus an index. The index enables label-based access (`.loc`) and alignment-aware arithmetic.
- `.loc[label]` looks up by label; `.iloc[i]` looks up by integer position. They give the same result only when the index is the default `RangeIndex`.
- Adding two Series aligns them by label first — missing pairs become `NaN`. Check `.isna().sum()` after any merge or arithmetic to catch unexpected gaps.
- `s.fillna(value)` replaces `NaN` with a constant or computed value; `s.dropna()` removes them. `s.value_counts()` is the fastest class-distribution check.
Every column of a DataFrame is a Series. `model.feature_importances_` from a scikit-learn tree is often a Series with feature names as the index — `.sort_values(ascending=False)` immediately shows which features matter most. `.value_counts()` is the standard first check on the target column to detect class imbalance before training.
If you remove it: Without understanding the `.loc` / `.iloc` distinction, combining predictions with ground-truth labels by row position is an easy bug — especially after sorting or filtering a DataFrame, which changes positional order without changing labels. Label-based alignment is the safety net that makes pandas data pipelines reproducible.