Python · Foundations · 6 min read

How a DataFrame works

Before the first line of pandas: what a DataFrame really is, why every column has a type, and why one expression on a column replaces a loop over thousands of rows.

The idea

A DataFrame is a table that lives in Python's memory. It has rows going down, columns going across, and a label on each row called the index. Harbor & Pine's orders, order_items and customers are all DataFrames, loaded for you before the first level of the pandas chapters.

Figure 1

The parts of a DataFrame: index, columns, values

orders
indexorder_idstatuschannelshipping_fee
01001completedweb0.00
11002completedweb0.00
21003completedmarketplace4.99
31004completedmarketplace4.99
orders['channel'] (a Series)
indexchannel
0web
1web
2marketplace
3marketplace

Step 1 of 4 This is the start of the orders DataFrame. It looks like a table because it is one: rows going down, columns going across.

  1. This is the start of the orders DataFrame. It looks like a table because it is one: rows going down, columns going across.
  2. The first column is not data. It is the index: a label for each row, here the numbers 0, 1, 2, 3. pandas adds it for you and uses it to keep track of rows.
  3. Each other column has a name and holds one kind of value from top to bottom. shipping_fee is all numbers, channel is all text.
  4. Pick one column with orders['channel'] and you get a Series: a single column that carries the same index along.

The part to hold on to is how the columns work. A DataFrame is not a grid of independent cells, like a spreadsheet. It is a set of columns, and each column is a typed array: one name, one kind of value, one value per row. Everything you will do in pandas is an operation on whole columns: add two of them, compare one with a number, group by one, sum another.

Why it exists

Plain Python gives you lists. A list of 30,000 prices works, but to double them you write a loop, and a loop that runs through 30,000 items in Python is slow and noisy. To keep a second list of quantities lined up with the prices, you have to remember that position 4,711 in one list means position 4,711 in the other. Nothing enforces it.

A DataFrame fixes both problems. The columns are stored side by side and always have the same length, so row 4,711 is the same row in every column. And the operations work on the whole column at once, in fast compiled code, so a question about 30,000 order lines takes a blink instead of a coffee.

How it works under the hood

Rows, columns, index

orders.shape gives (12000, 7): 12,000 rows and 7 columns. The index is separate from the 7 columns. By default it is the numbers 0 to 11999, and its job is to label rows so pandas can line things up. When you filter or sort, rows keep their original labels, which is why a result can start at row 3,418 and not at 0. reset_index() renumbers them when you want that.

Typed columns

Figure 2

Every column has one dtype

orders.dtypes
columndtypeexample
order_idint641001
statusobjectcompleted
shipping_feefloat644.99
order_dateobject2023-01-01 20:51:19

Step 1 of 4 orders.info() or orders.dtypes lists the dtype of each column: the kind of value it holds.

  1. orders.info() or orders.dtypes lists the dtype of each column: the kind of value it holds.
  2. order_id is int64, whole numbers. shipping_fee is float64, numbers with decimals. Both support arithmetic.
  3. status is object, which for now means text. You can compare it with 'completed' but you cannot add it up.
  4. order_date is also object: the file stores dates as text, so pandas does not treat them as dates yet. A wrong dtype is the usual reason a calculation fails or sorts oddly.

A column holds one dtype, and the dtype decides what you can do. Numbers can be added and averaged. Text can be matched and cut up. Dates can be subtracted. If a number arrives as text ("$1,249.00" in a supplier file, say), no arithmetic works until you convert it. That is why the pandas chapters start with info(): it shows each column's dtype and how many values are not missing. A column like coupon_code has gaps, and pandas shows those as NaN.

Vectorised operations

An operation on a column is applied to every value, row by row, and gives back a column.

Figure 3

One expression on a column, no loop

order_items
quantityunit_pricediscount_pct
931.990.0
311.990.0
411.990.0
1149.000.0
quantity * unit_price
line_total
287.91
35.97
47.96
149.00

Step 1 of 3 Four order lines from order_items. We want the money for each line: quantity times price.

  1. Four order lines from order_items. We want the money for each line: quantity times price.
  2. order_items['quantity'] * order_items['unit_price'] multiplies the two columns row by row: the first with the first, the second with the second, and so on.
  3. The answer is a whole new column with one value per row. There was no for, no counter and no first row handled specially.

Three things follow. The result has the same length and the same index as the inputs, so it lines up with the table it came from. You can store it as a new column: order_items['line_total'] = order_items['quantity'] * order_items['unit_price']. And it is fast, because pandas hands the whole column to compiled code instead of running Python once per row. The same goes for a comparison: orders['shipping_fee'] > 0 gives a column of True and False, one per order. That is the basis of filtering.

A DataFrame, a SQL table and a spreadsheet

All three hold rows and columns, and they differ in how you work with them.

  • Spreadsheet. Every cell is its own thing, and a formula is written once per cell. The types are loose: one cell can hold a number and the next one text. You see the data and edit it by hand.
  • SQL table. Columns are typed and rows have no order unless you ask for one. The data lives in a database; you send a query and receive a result. A change is a statement such as UPDATE.
  • DataFrame. Columns are typed like SQL, but the table sits in your session's memory, and it has an index that keeps row order. You run one operation after another on it, and each step returns a new DataFrame or Series that you can name and keep.

The same job in each: "total shipping fee per channel" is a SUM formula per group in a spreadsheet, a GROUP BY in SQL, and orders.groupby('channel')['shipping_fee'].sum() in pandas. The reading is the same; only the vocabulary changes.

Common misconceptions

"I need a loop to do something to every row"

You almost never do. Loops are the habit from plain Python, and they work on a DataFrame, but they are slow and long. orders['shipping_fee'] * 2 is one line and doubles all 12,000 fees. Reach for a loop only when a row's result truly depends on the one before it. If you catch yourself writing for i in range(len(df)), look for the column operation first.

"The index is just the first column"

The index is not one of the 7 columns of orders. It labels rows and does not count in shape. Because it travels with the data, a filtered result keeps the old labels: the third row of the result may carry the label 2,041. Use .iloc for "the third row" and .loc for "the row labelled 2,041", which chapter 5 covers.

"A DataFrame behaves like a spreadsheet: each cell is separate"

A cell in pandas is never edited in isolation by a formula that sits in it. Operations act on columns, and a column has one dtype. That is why one stray text value such as "n/a" in a price column turns the whole column into object, and then sum() cannot add it. Fix the value or convert the column, and the arithmetic comes back.

Check yourself

One question. Answering it ticks this lesson off on the map; it never locks a level.

In orders, the column shipping_fee holds numbers and status holds text. What happens with orders['shipping_fee'] * 2?

Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.