Pandas Series and DataFrames
Series from a list or a dict, how to create a DataFrame from a dict of lists, the inspection methods .head(), .tail(), .info(), and .describe(), checking a DataFrame's .shape, the difference between df['col'] and df.col, and the basics of .loc/.iloc selection.
What Is Pandas, and Why Is It Built on NumPy?
Pandas is a Python library for working with labeled, tabular data — the kind of data you'd normally put in a spreadsheet or a database table: rows and columns, where every column can have its own name and its own data type. It's the single most-used tool in the Python data science stack, and it sits directly on top of NumPy.
Every column inside a Pandas table is, under the hood, a NumPy array. That's not a coincidence — Pandas was built specifically to add row and column labels, mixed column types, and a huge library of data-wrangling methods (filtering, grouping, merging, reshaping) on top of the fast, vectorized foundation NumPy already provides. Anything you learned about vectorized math and boolean masking in Section 1 carries over directly.
pd: import pandas as pd. You'll see pd. in front of almost every function for the rest of this course, the same way np. prefixed every NumPy call in Section 1.Creating a Series
A Series is a one-dimensional, labeled array. Every value has an associated index label — by default, integers starting at 0, but you can supply your own.
import pandas as pd # From a plain list — Pandas assigns a default integer index (0, 1, 2, ...) prices = pd.Series([19.99, 24.50, 7.25, 42.00]) print(prices) # 0 19.99 # 1 24.50 # 2 7.25 # 3 42.00 # dtype: float64 # From a dict — the dict keys become the index labels automatically inventory = pd.Series({"apples": 50, "bananas": 30, "pears": 12}) print(inventory["bananas"]) # 30 — label-based lookup print(inventory.index) # Index(['apples', 'bananas', 'pears'], dtype='object') print(inventory.values) # array([50, 30, 12]) — the underlying NumPy array
.values hands you back the plain NumPy array sitting underneath a Series. Everything you know about vectorized math still applies — prices * 0.9 works exactly like it did on a NumPy array in Section 1, but now the result keeps its index labels too.Creating a DataFrame
The most common way to build a small DataFrame by hand is from a dict of lists — each key becomes a column name, and each list becomes that column's values. Every list must be the same length.
import pandas as pd data = { "name": ["Aiman", "Bilal", "Chen", "Deepa"], "age": [23, 31, 27, 35], "city": ["Lahore", "Karachi", "Beijing", "Delhi"], "score": [88.5, 92.0, 76.5, 99.0], } df = pd.DataFrame(data) print(df) # name age city score # 0 Aiman 23 Lahore 88.5 # 1 Bilal 31 Karachi 92.0 # 2 Chen 27 Beijing 76.5 # 3 Deepa 35 Delhi 99.0
| name | age | city | score | |
|---|---|---|---|---|
| 0 | Aiman | 23 | Lahore | 88.5 |
| 1 | Bilal | 31 | Karachi | 92.0 |
| 2 | Chen | 27 | Beijing | 76.5 |
| 3 | Deepa | 35 | Delhi | 99.0 |
df['age'] hands you back a Series, and why every column keeps its own dtype even though they all live in the same table.First Look: head, tail, info, describe, shape
Before doing anything else with a new DataFrame, get in the habit of running these five checks. They tell you what you're actually working with.
# df is the 4-row DataFrame from Section 3 df.head() # first 5 rows (fewer if the DataFrame is smaller) — default n=5 df.head(2) # first 2 rows only df.tail(2) # last 2 rows print(df.shape) # (4, 4) — (rows, columns), same idea as a NumPy array's .shape df.info() # column names, dtypes, non-null counts, and memory usage # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 4 entries, 0 to 3 # Data columns (total 4 columns): # # Column Non-Null Count Dtype # --- ------ -------------- ----- # 0 name 4 non-null object # 1 age 4 non-null int64 # 2 city 4 non-null object # 3 score 4 non-null float64 df.describe() # summary statistics for numeric columns only: count, mean, std, min, quartiles, max
.info() tells you about structure — dtypes and how many values are missing in each column. .describe() tells you about distribution — mean, spread, and range — and only looks at numeric columns by default. Run both, every time, on any dataset you haven't seen before.Selecting Columns and Rows: bracket, dot, loc, iloc
There are several ways to pull data out of a DataFrame, and each one has a slightly different job.
# --- Column selection --- print(df["name"]) # bracket notation — always works, even with spaces in the name print(df.name) # dot notation — same result, but breaks if the column name isn't a valid identifier print(df[["name", "score"]]) # double brackets = a DataFrame with multiple columns # --- .loc — select by LABEL --- print(df.loc[0]) # row with index label 0, as a Series print(df.loc[0, "city"]) # single value: row 0, column "city" print(df.loc[0:2, ["name", "age"]]) # rows 0 through 2 (INCLUSIVE), two columns # --- .iloc — select by INTEGER POSITION --- print(df.iloc[0]) # first row by position, regardless of its label print(df.iloc[0, 2]) # row at position 0, column at position 2 print(df.iloc[0:2]) # rows at positions 0 and 1 — stop is EXCLUSIVE, like Python slicing
.loc uses the labels you can see in the index/columns, and its slices include the endpoint. .iloc uses plain integer positions, counting from 0, and its slices exclude the endpoint — just like a Python list. When the index is the default 0,1,2,..., .loc[0:2] and .iloc[0:2] can look deceptively similar but return different numbers of rows.Lesson Summary
Let's recap everything you learned in this lesson:
Series is one labeled column; a DataFrame is a 2D table of Series sharing an index.pd.DataFrame(data)..head(), .tail(), .info(), .describe(), and .shape are your first-look toolkit for any new DataFrame..loc selects by label (inclusive slices); .iloc selects by integer position (exclusive slices).df.loc[0:2] versus df.iloc[0:2] on a default integer index?df.col_name (dot notation) fail when df['col_name'] works fine?Build and inspect a small DataFrame of your own.
Build a dict with three keys —
"product" (5 product name strings), "price" (5 floats), and "in_stock" (5 booleans) — and turn it into a DataFrame called catalog. Then: (1) print catalog.shape, (2) print catalog.head(3), (3) print just the "price" column using bracket notation, and (4) use .loc to print the "product" and "price" columns for the first two rows only.
Rules: Use a dict of lists with
pd.DataFrame() — don't build the rows one at a time.
💡 Show hints if you're stuck
- Build the dict:
data = {"product": [...], "price": [...], "in_stock": [...]} - Create the frame:
catalog = pd.DataFrame(data) - First two rows, two columns:
catalog.loc[0:1, ["product", "price"]]