1. Lighthouse

Level 4 · Series or DataFrame?

The request

From Priya Shah, Head of Data

“Quick check before you start building reports, because this trips up everyone in their first week.”

Your goal

Choose the statement that is true.

Which of these is true about `orders["status"]` and `orders[["status"]]`?

Pick one answer, then check it.

The lesson

Why this distinction matters

pandas has two main objects, and almost every confusing error in your first weeks comes from having one when you expected the other.

A Series is one column: values plus an index. It has one dtype and a name (the column name). Series have methods for working on values, such as .value_counts(), .unique(), .str.lower(), .dt.year, .map() and .between().

A DataFrame is a table of one or more columns that share an index. DataFrames have methods for working on tables, such as .merge(), .groupby() with several columns, .sort_values(by=...), .drop_duplicates(subset=...) and .to_csv().

The rule

orders["status"]             # Series
orders[["status"]]           # DataFrame, 1 column
orders[["status", "channel"]]   # DataFrame, 2 columns

The inner brackets create a Python list. Whenever you select with a list, you get a DataFrame, even if the list holds a single name.

Checking what you have

When in doubt, ask Python:

type(orders["status"])       # pandas.core.series.Series
type(orders[["status"]])     # pandas.core.frame.DataFrame
orders[["status"]].shape     # (12000, 1)
orders["status"].shape       # (12000,)   one dimension only

The shape is a good tell: a Series has a one-element shape, a DataFrame always has two.

A useful mental model

Think of a DataFrame as a dictionary of Series that all share the same index. Asking for one key (orders["status"]) hands you that one Series. Asking with a list of keys hands you a smaller dictionary, which is still a DataFrame. This also explains why adding a column looks like adding a dictionary key: orders["is_web"] = orders["channel"] == "web".

Converting between them

orders[["status"]].squeeze()        # one-column DataFrame to Series
orders["status"].to_frame()         # Series to one-column DataFrame

Where it bites

  • orders[["status"]].value_counts() works but returns counts indexed by a one-level MultiIndex, which prints and merges differently from orders["status"].value_counts().
  • .str and .dt accessors exist only on Series.
  • Assigning a new column needs a Series (or a list or a single value) on the right-hand side, not a DataFrame.
  • Many aggregation results are Series. orders.groupby("channel")["shipping_fee"].mean() is a Series indexed by channel; add .reset_index() to turn it into a two-column table.

Getting this right also makes task statements in this game easy to follow: when a level asks for a Series, return one column’s worth of values; when it asks for a DataFrame with named columns, return a table with exactly those columns.