Mathew K Analytics

Lesson 1 · Pandas Full Course

Learn Pandas Fast: Answer a Real Data Question in 20 Minutes | Pandas Course #1

In this lesson you'll be able to: Explain what pandas is and why analysts use it Load a real CSV file and inspect it in seconds Find and handle missing…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Pandas 01: Introduction to Pandas and its Role in Data Analysis#

In this lesson you'll be able to:

  • Explain what pandas is and why analysts use it
  • Load a real CSV file and inspect it in seconds
  • Find and handle missing values
  • Filter, group and chart data to answer a question Dataset: the Titanic passenger list (data/pandas/titanic.csv)

Warm-up: the same question in plain Python#

What share of passengers survived? No pandas yet.

import csv

with open("data/pandas/titanic.csv") as f:
    rows = list(csv.DictReader(f))
print(len(rows), "passengers")
print(rows[0]["Name"], "| age:", rows[0]["Age"])
891 passengers
Braund, Mr. Owen Harris | age: 22
survived = [int(r["Survived"]) for r in rows]
print(sum(survived) / len(survived))
0.3838383838383838

Load a real table in one line#

pandas reads the file, works out each column's type and labels it.

import pandas as pd

df = pd.read_csv("data/pandas/titanic.csv")
print(type(df))
print(df.shape)
<class 'pandas.DataFrame'>
(891, 12)
df.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
df["Survived"].mean().round(3)
np.float64(0.384)

Where pandas fits#

Files in, then pandas to clean and summarise, then charts and models. Under the hood, each column is a fast NumPy array.

fares = df["Fare"].to_numpy()
print(type(fares))
print(fares[:4])
<class 'numpy.ndarray'>
[ 7.25   71.2833  7.925  53.1   ]
print(df["Fare"].sum().round(2))
print((df["Fare"] * 2).head(3).tolist())
28693.95
[14.5, 142.5666, 15.85]

Common mistake: giving read_csv just the file name

name = "titanic.csv"
pd.read_csv(name)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[9], line 2
      1 name = "titanic.csv"
----> 2 pd.read_csv(name)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:872, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, skip_blank_lines, parse_dates, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, low_memory, memory_map, float_precision, storage_options, dtype_backend)
    860 kwds_defaults = _refine_defaults_read(
    861     dialect,
    862     delimiter,
   (...)
    868     dtype_backend=dtype_backend,
    869 )
    870 kwds.update(kwds_defaults)
--> 872 return _read(filepath_or_buffer, kwds)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:300, in _read(filepath_or_buffer, kwds)
    297 _validate_names(kwds.get("names", None))
    299 # Create the parser.
--> 300 parser = TextFileReader(filepath_or_buffer, **kwds)
    302 if chunksize or iterator:
    303     return parser

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1643, in TextFileReader.__init__(self, f, engine, **kwds)
   1640     self.options["has_index_names"] = kwds["has_index_names"]
   1642 self.handles: IOHandles | None = None
-> 1643 self._engine = self._make_engine(f, self.engine)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1907, in TextFileReader._make_engine(self, f, engine)
   1905     if "b" not in mode:
   1906         mode += "b"
-> 1907 self.handles = get_handle(
   1908     f,
   1909     mode,
   1910     encoding=self.options.get("encoding", None),
   1911     compression=self.options.get("compression", None),
   1912     memory_map=self.options.get("memory_map", False),
   1913     is_text=is_text,
   1914     errors=self.options.get("encoding_errors", "strict"),
   1915     storage_options=self.options.get("storage_options", None),
   1916 )
   1917 assert self.handles is not None
   1918 f = self.handles.handle

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\common.py:930, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    925 elif isinstance(handle, str):
    926     # Check whether the filename is to be opened in binary mode.
    927     # Binary mode does not support 'encoding' and 'newline'.
    928     if ioargs.encoding and "b" not in ioargs.mode:
    929         # Encoding
--> 930         handle = open(
    931             handle,
    932             ioargs.mode,
    933             encoding=ioargs.encoding,
    934             errors=errors,
    935             newline="",
    936         )
    937     else:
    938         # Binary mode
    939         handle = open(handle, ioargs.mode)

FileNotFoundError: [Errno 2] No such file or directory: 'titanic.csv'
df = pd.read_csv("data/pandas/titanic.csv")
df.shape
(891, 12)

Inspect: what is in this table?#

Check column names, types and value ranges before any analysis.

df.tail(3)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
888 889 0 3 Johnston, Miss. Catherine Helen "Carrie" female NaN 1 2 W./C. 6607 23.45 NaN S
889 890 1 1 Behr, Mr. Karl Howell male 26.0 0 0 111369 30.00 C148 C
890 891 0 3 Dooley, Mr. Patrick male 32.0 0 0 370376 7.75 NaN Q
df.columns.tolist()
['PassengerId',
 'Survived',
 'Pclass',
 'Name',
 'Sex',
 'Age',
 'SibSp',
 'Parch',
 'Ticket',
 'Fare',
 'Cabin',
 'Embarked']
df.info()
<class 'pandas.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    str    
 4   Sex          891 non-null    str    
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    str    
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    str    
 11  Embarked     889 non-null    str    
dtypes: float64(2), int64(5), str(5)
memory usage: 118.9 KB
df.describe().round(2)
PassengerId Survived Pclass Age SibSp Parch Fare
count 891.00 891.00 891.00 714.00 891.00 891.00 891.00
mean 446.00 0.38 2.31 29.70 0.52 0.38 32.20
std 257.35 0.49 0.84 14.53 1.10 0.81 49.69
min 1.00 0.00 1.00 0.42 0.00 0.00 0.00
25% 223.50 0.00 2.00 20.12 0.00 0.00 7.91
50% 446.00 0.00 3.00 28.00 0.00 0.00 14.45
75% 668.50 1.00 3.00 38.00 1.00 0.00 31.00
max 891.00 1.00 3.00 80.00 8.00 6.00 512.33

Quick check: is df["Age"] a table or something else?

ages = df["Age"]
print(type(ages))
ages.head(3)
<class 'pandas.Series'>
0    22.0
1    38.0
2    26.0
Name: Age, dtype: float64
print(df["Sex"].unique())
print(df["Ticket"].nunique(), "different tickets")
<ArrowStringArray>
['male', 'female']
Length: 2, dtype: str
681 different tickets
df["Pclass"].value_counts().sort_index()
Pclass
1    216
2    184
3    491
Name: count, dtype: int64
df["Embarked"].value_counts()
Embarked
S    644
C    168
Q     77
Name: count, dtype: int64

Clean: find and handle missing values#

Real data has gaps. Count them, then drop or fill with care.

df.isna().sum()
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64

Quick check: what share of all ages is missing?

df["Age"].isna().mean().round(3)
np.float64(0.199)
with_age = df.dropna(subset=["Age"])
print(df.shape, with_age.shape)
(891, 12) (714, 12)
top_port = df["Embarked"].mode()[0]
df["Embarked"] = df["Embarked"].fillna(top_port)
print(top_port, df["Embarked"].isna().sum())
S 0

Your turn: how many passengers have no recorded cabin?

df["Cabin"].isna().sum()
np.int64(687)

Ask questions: select and filter#

A condition gives True or False per row; keep the True ones.

df[["Name", "Age", "Fare"]].head(3)
Name Age Fare
0 Braund, Mr. Owen Harris 22.0 7.2500
1 Cumings, Mrs. John Bradley (Florence Briggs Th... 38.0 71.2833
2 Heikkinen, Miss. Laina 26.0 7.9250
kids = df[df["Age"] < 18]
print(len(kids), "children")
kids[["Name", "Age", "Pclass"]].head(3)
113 children
Name Age Pclass
7 Palsson, Master. Gosta Leonard 2.0 3
9 Nasser, Mrs. Nicholas (Adele Achem) 14.0 2
10 Sandstrom, Miss. Marguerite Rut 4.0 3
top = df.sort_values("Fare", ascending=False)
top[["Name", "Pclass", "Fare"]].head(3)
Name Pclass Fare
679 Cardeza, Mr. Thomas Drake Martinez 1 512.3292
258 Ward, Miss. Anna 1 512.3292
737 Lesurer, Mr. Gustave J 1 512.3292
ports = ["C", "Q"]
not_s = df[df["Embarked"].isin(ports)]
print(len(not_s), "boarded at C or Q")
245 boarded at C or Q

Your turn: how many passengers paid a fare above 100?

rich = df[df["Fare"] > 100]
print(len(rich), "passengers paid over 100")
print(rich["Pclass"].unique())
53 passengers paid over 100
[1]

Common mistake: combining conditions with and

df[(df["Sex"] == "female") and (df["Pclass"] == 1)]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_24588\1707758690.py in ?()
----> 1 df[(df["Sex"] == "female") and (df["Pclass"] == 1)]

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py in ?(self)
   1499     @final
   1500     def __bool__(self) -> NoReturn:
-> 1501         raise ValueError(
   1502             f"The truth value of a {type(self).__name__} is ambiguous. "
   1503             "Use a.empty, a.bool(), a.item(), a.any() or a.all()."
   1504         )

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
first_women = df[(df["Sex"] == "female") & (df["Pclass"] == 1)]
print(len(first_women), "women in first class")
first_women["Survived"].mean().round(2)
94 women in first class
np.float64(0.97)

Summarise: group and compare#

groupby splits rows into groups, then summarises each one.

df.groupby("Sex")["Survived"].mean().round(2)
Sex
female    0.74
male      0.19
Name: Survived, dtype: float64
df.groupby("Pclass")["Fare"].mean().round(2)
Pclass
1    84.15
2    20.66
3    13.68
Name: Fare, dtype: float64

Quick check: which class had the best survival rate?

df.groupby("Pclass")["Survived"].mean().round(2)
Pclass
1    0.63
2    0.47
3    0.24
Name: Survived, dtype: float64
by_port = df.groupby("Embarked")["Survived"].mean()
by_port.sort_values(ascending=False).round(2)
Embarked
C    0.55
Q    0.39
S    0.34
Name: Survived, dtype: float64
df.groupby("Embarked")["Fare"].agg(["mean", "median"]).round(2)
mean median
Embarked
C 59.95 29.70
Q 13.28 7.75
S 27.24 13.00

Your turn: what was the median age in each class?

df.groupby("Pclass")["Age"].median()
Pclass
1    37.0
2    29.0
3    24.0
Name: Age, dtype: float64
table = df.pivot_table(values="Survived", index="Pclass",
                       columns="Sex", aggfunc="mean")
table.round(2)
Sex female male
Pclass
1 0.97 0.37
2 0.92 0.16
3 0.50 0.14

Visualise: let a chart tell the story#

pandas hands its numbers straight to matplotlib for plotting.

import matplotlib.pyplot as plt

rates = df.groupby("Pclass")["Survived"].mean()
ax = rates.plot(kind="bar", color="steelblue", rot=0)
ax.set_title("Titanic survival rate by class")
ax.set_xlabel("Passenger class")
ax.set_ylabel("Share who survived")
plt.show()
No description has been provided for this image
ax = df.boxplot(column="Age", by="Pclass", grid=False)
ax.set_title("Age by passenger class")
plt.suptitle("")
ax.set_xlabel("Passenger class")
ax.set_ylabel("Age in years")
plt.show()
No description has been provided for this image

Common mistake: a column name with the wrong capital letter

df["age"].mean()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3641, in Index.get_loc(self, key)
   3640 try:
-> 3641     return self._engine.get_loc(casted_key)
   3642 except KeyError as err:

File pandas/_libs/index.pyx:168, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:197, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7668, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7676, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'age'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[40], line 1
----> 1 df["age"].mean()

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4378, in DataFrame.__getitem__(self, key)
   4376 if self.columns.nlevels > 1:
   4377     return self._getitem_multilevel(key)
-> 4378 indexer = self.columns.get_loc(key)
   4379 if is_integer(indexer):
   4380     indexer = [indexer]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3648, in Index.get_loc(self, key)
   3643     if isinstance(casted_key, slice) or (
   3644         isinstance(casted_key, abc.Iterable)
   3645         and any(isinstance(x, slice) for x in casted_key)
   3646     ):
   3647         raise InvalidIndexError(key) from err
-> 3648     raise KeyError(key) from err
   3649 except TypeError:
   3650     # If we have a listlike key, _check_indexing_error will raise
   3651     #  InvalidIndexError. Otherwise we fall through and re-raise
   3652     #  the TypeError.
   3653     self._check_indexing_error(key)

KeyError: 'age'
print([c for c in df.columns if c.lower() == "age"])
df["Age"].mean().round(1)
['Age']
np.float64(29.7)

Mini-project: did women and children go first?#

Load, flag, group and compare, all with today's tools.

df["Group"] = "Man"
df.loc[df["Sex"] == "female", "Group"] = "Woman"
df.loc[df["Age"] < 16, "Group"] = "Child"
df["Group"].value_counts()
Group
Man      537
Woman    271
Child     83
Name: count, dtype: int64
summary = df.groupby("Group")["Survived"].agg(["count", "mean"])
summary.round(2).sort_values("mean", ascending=False)
count mean
Group
Woman 271 0.76
Child 83 0.59
Man 537 0.16
by_class = df.pivot_table(values="Survived", index="Group",
                          columns="Pclass", aggfunc="mean")
by_class.round(2)
Pclass 1 2 3
Group
Child 0.83 1.00 0.43
Man 0.35 0.08 0.12
Woman 0.98 0.91 0.49
pd.crosstab(df["Group"], df["Pclass"])
Pclass 1 2 3
Group
Child 6 19 58
Man 119 99 319
Woman 91 66 114
ax = by_class.T.plot(kind="bar", rot=0)
ax.set_title("Survival by group and class")
ax.set_xlabel("Passenger class")
ax.set_ylabel("Share who survived")
plt.show()
No description has been provided for this image
import os

os.makedirs("output/pandas", exist_ok=True)
path = "output/pandas/l01_group_survival.csv"
by_class.round(2).to_csv(path)
pd.read_csv(path, index_col=0)
1 2 3
Group
Child 0.83 1.00 0.43
Man 0.35 0.08 0.12
Woman 0.98 0.91 0.49

Finding: women and children did go first, but mostly in first and second class. In third class, half of the women and children died.

Recap#

  • pandas turns files into DataFrames: labelled rows and columns
  • head, info and describe inspect a table in seconds
  • isna, dropna and fillna find and handle missing values
  • Filter with conditions joined by &, never and
  • groupby, pivot_table and plot turn data into answers

Challenges#

  1. What was the survival rate at each port in Embarked?
  2. Find the oldest passenger who survived. Which class were they in?
  3. Chart the average fare for the Man, Woman and Child groups.

What's next#

Lesson 02: Installing Pandas and Setting Up the Environment. Check versions, fix paths and set up a project you can rerun.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.