EduFlow

Section 2 · pandas Foundations

Series and DataFrames

16 min3 min readPython exercise

Lesson at a glance

Build pandas Series and DataFrames from Python lists and dictionaries, run calculations on whole columns at once, and predict how pandas lines values up by their index labels.

  1. 1Series: a column with labels
  2. 2Calculations on every value at once
  3. 3Alignment: pandas matches labels, not positions
  4. 4DataFrame: a table of Series

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

In the last section’s exercise, month names lived in one list and revenues in another, and you had to find the best month by its position and then look up the name in the other list. Sort one list and forget the other, and every month gets the wrong revenue without any error. pandas exists to make that kind of mistake hard: values carry their labels with them.

Series: a column with labels

A Series is a one-dimensional array of values plus an index: one label per value.

Python
import pandas as pd

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
revenue = pd.Series(
    [16167, 14906, 18208, 18817, 17428, 13933,
     22968, 32042, 25514, 30339, 75334, 41153],
    index=months,
    name="revenue_2025",
)
print(revenue.head(3))
print(revenue["Nov"], revenue.idxmax(), revenue.sum())

import pandas as pd loads the library under its universal nickname; every pandas tutorial and Stack Overflow answer uses pd. revenue["Nov"] looks a value up by label, and idxmax() returns the label of the largest value, which is the whole parallel-lists dance from Section 1 in one call.

The last line of the printed Series reads Name: revenue_2025, dtype: int64. The dtype is the type of every value in the column: int64 for whole numbers, float64 for decimals, object (or str in pandas 3) for text, bool, and datetime64 for dates. One column, one type; that rule is what makes pandas fast.

Calculations on every value at once

Arithmetic and comparisons on a Series apply to each value and return a new Series with the same index. This is called vectorised code: you describe the operation on the whole column and pandas does the loop in fast compiled code.

Python
import pandas as pd

revenue = pd.Series([30339, 75334, 41153], index=["Oct", "Nov", "Dec"])
print(revenue * 1.05)              # every month, 5% higher
print(revenue / revenue.sum())     # each month's share of Q4
print(revenue > 40000)             # a Series of True/False

The True/False Series on the last line looks like a curiosity now. In Lesson 2.4 it becomes the main way you filter rows.

Alignment: pandas matches labels, not positions

When you combine two Series, pandas lines them up by index label before it calculates. Here is Q4 2025 against Q4 2024, with the months deliberately written in a different order and one month missing:

Python
import pandas as pd

q4_2025 = pd.Series([30339, 75334, 41153], index=["Oct", "Nov", "Dec"])
q4_2024 = pd.Series([29177, 9360], index=["Nov", "Oct"])
print(q4_2025 - q4_2024)

October is subtracted from October and November from November, regardless of order. December exists only in 2025, so its result is NaN (“not a number”), pandas’ marker for a missing value. Notice the result became float64: in classic NumPy-backed columns, NaN is a float, so an integer column that gains a missing value turns into floats.

Left Series has Oct, Nov, Dec. Right Series has Nov and Oct in a different order. Arrows connect matching labels. The result has Oct and Nov computed, and Dec is NaN because it has no partner. q4_2025 Oct 30339 Nov 75334 Dec 41153 q4_2024 Nov 29177 Oct 9360 difference Oct 20979 Nov 46157 Dec NaN labels are matched first; a label with no partner gives NaN
Two Series combine by matching index labels

DataFrame: a table of Series

A DataFrame is several Series side by side, sharing one index. The easiest way to build a small one is the dictionary-of-lists shape you met in Lesson 1.3, one key per column:

Python
import pandas as pd

products = pd.DataFrame({
    "name": ["Cast Iron Skillet 26cm", "Burr Coffee Grinder", "Standing Desk 140cm", "Cork Yoga Mat"],
    "unit_price": [39, 129, 449, 72],
    "unit_cost": [17.45, 76.99, 204.98, 41.52],
})
print(products)
print(products.shape)       # (rows, columns)
print(products.dtypes)

Three attributes describe any DataFrame, and you will print them constantly. shape is the (rows, columns) pair, columns lists the column names, and index holds the row labels. None of them has parentheses, because they are stored facts about the table rather than actions; head() and sum() do work, so they are called with parentheses. Mixing the two up gives either a method printed as <bound method ...> or a TypeError: 'tuple' object is not callable, both of which you can now recognise.

pandas gave the rows a default index, 0 to 3. Each column is a Series you can pull out with square brackets, and column arithmetic lines up row by row because the columns share that index:

Python
import pandas as pd

products = pd.DataFrame({
    "name": ["Cast Iron Skillet 26cm", "Burr Coffee Grinder", "Standing Desk 140cm", "Cork Yoga Mat"],
    "unit_price": [39, 129, 449, 72],
    "unit_cost": [17.45, 76.99, 204.98, 41.52],
})
margin = products["unit_price"] - products["unit_cost"]
print(margin)
print(type(products["unit_price"]))

The skillet earns 21.55 dollars per unit and the standing desk 244.02. Four rows or four million, the code is the same, and that is the real promise of pandas: you think in columns, not in cells.

Building tables by hand is for learning. Next you load Cartwheel’s real CSV files and learn the inspection routine to run before you trust any of them.

Key takeaways

  • A Series is a column of values with an index of labels; a DataFrame is a table of Series that share one index.
  • Arithmetic on a Series applies to every value at once, so you write prices * 1.05 instead of a loop.
  • Operations between two Series line up by index label, not by position; labels present in only one side produce NaN.
  • df["col"] gives one column as a Series, and df.shape, df.columns, df.index and df.dtypes describe the table.

Further reading

Check your understanding

4 questions

Question 1 of 4

What is growth["Oct"]?

Python
y2024 = pd.Series([9360, 29177], index=["Oct", "Nov"])
y2025 = pd.Series([75334, 30339], index=["Nov", "Oct"])
growth = y2025 - y2024
Question 2 of 4

products is a DataFrame with 48 rows and 7 columns. What does products.shape return?

Question 3 of 4

Which line raises every product’s price by 5% without a loop?

Question 4 of 4

In the last exercise of Section 1 you kept month names and revenues in two parallel lists. What problem does a Series fix?

Practice

Python exercise

Your first Series and DataFrame

Build revenue, a Series of values labelled by months. Set best_month to the label of its largest value and share to each month’s share of the yearly total (a Series that sums to 1). Then build products, a DataFrame with the columns name, unit_price and unit_cost holding the three products listed in the comments, in that order, and margin, a Series of unit_price minus unit_cost.

import pandas as pd

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
values = [16167, 14906, 18208, 18817, 17428, 13933,
          22968, 32042, 25514, 30339, 75334, 41153]

# 1. A Series of the values, labelled by month
revenue = None

# 2. Label of the best month
best_month = None

# 3. Each month's share of the year (a Series that sums to 1)
share = None

# 4. A DataFrame with columns name, unit_price, unit_cost for these products
#    Cast Iron Skillet 26cm  39  17.45
#    Burr Coffee Grinder    129  76.99
#    2-Person Dome Tent     149  83.09
products = None

# 5. A Series: unit_price minus unit_cost for each product
margin = None
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

(no output)

Checks

  • revenue is a Series labelled Jan to Dec
  • best_month is "Nov"
  • share sums to 1, with November at about 23%
  • products is a 3 × 3 DataFrame with the right columns
  • margin holds 21.55, 52.01 and 65.91