Mathew K Analytics

Lesson 6 · Pandas Full Course

Explore a New Dataset in Pandas Step by Step (Hands-on Project) | Pandas Course #6

In this lesson you'll be able to: Build a DataFrame from your own lists and extend it Run a first-look checklist on an unfamiliar dataset Add, filter and…

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 06: Hands-on - Creating and Exploring DataFrames#

In this lesson you'll be able to:

  • Build a DataFrame from your own lists and extend it
  • Run a first-look checklist on an unfamiliar dataset
  • Add, filter and sort columns to answer plain questions
  • Turn a hunch into a checked finding with real numbers Dataset: restaurant tips (data/pandas/tips.csv)

Warm-up: attributes and a new column#

Lesson 05 used attributes and column arithmetic. Try them on tips.

import pandas as pd
tips = pd.read_csv("data/pandas/tips.csv")
print(tips.shape, tips.size)
print(tips.columns.tolist())
(244, 7) 1708
['total_bill', 'tip', 'sex', 'smoker', 'day', 'time', 'size']
tips["bill_after_tip"] = tips["total_bill"] + tips["tip"]
tips[["total_bill", "tip", "bill_after_tip"]].head(3)
total_bill tip bill_after_tip
0 16.99 1.01 18.00
1 10.34 1.66 12.00
2 21.01 3.50 24.51

Step 1: build a DataFrame from your own lists#

Problem: copy a few bills into a small table you can analyse.

first = tips.head(4)
log = pd.DataFrame({
    "day": first["day"].to_list(),
    "size": first["size"].to_list(),
    "tip": first["tip"].to_list(),
})
log
day size tip
0 Sun 2 1.01
1 Sun 3 1.66
2 Sun 3 3.50
3 Sun 2 3.31
log["per_person"] = (log["tip"] / log["size"]).round(2)
log
day size tip per_person
0 Sun 2 1.01 0.50
1 Sun 3 1.66 0.55
2 Sun 3 3.50 1.17
3 Sun 2 3.31 1.66

Your turn#

Add a column group that is True when 3 or more people sat together.

log["group"] = log["size"] >= 3
log[["size", "tip", "group"]]
size tip group
0 2 1.01 False
1 3 1.66 True
2 3 3.50 True
3 2 3.31 False
extra = tips.loc[[4], ["day", "size"]]
log = pd.concat([log, extra], ignore_index=True)
log
day size tip per_person group
0 Sun 2 1.01 0.50 False
1 Sun 3 1.66 0.55 True
2 Sun 3 3.50 1.17 True
3 Sun 2 3.31 1.66 False
4 Sun 4 NaN NaN NaN

Common mistake: a column name with the wrong capital letter

log["Size"].sum()
---------------------------------------------------------------------------
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: 'Size'

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

KeyError                                  Traceback (most recent call last)
Cell In[8], line 1
----> 1 log["Size"].sum()

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: 'Size'
print(log.columns.tolist())
print(log["size"].sum())
['day', 'size', 'tip', 'per_person', 'group']
14

Step 2: a first-look checklist#

Problem: understand an unfamiliar file before trusting it.

  • Size, peek, types, gaps, and one random sample
tips = tips.drop(columns="bill_after_tip")
tips.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4
tips.info()
<class 'pandas.DataFrame'>
RangeIndex: 244 entries, 0 to 243
Data columns (total 7 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   total_bill  244 non-null    float64
 1   tip         244 non-null    float64
 2   sex         244 non-null    str    
 3   smoker      244 non-null    str    
 4   day         244 non-null    str    
 5   time        244 non-null    str    
 6   size        244 non-null    int64  
dtypes: float64(2), int64(1), str(4)
memory usage: 17.3 KB
print(tips.isna().sum().sum())
0
tips.nunique()
total_bill    229
tip           123
sex             2
smoker          2
day             4
time            2
size            6
dtype: int64
tips.sample(5, random_state=42)
total_bill tip sex smoker day time size
24 19.82 3.18 Male No Sat Dinner 2
6 8.77 2.00 Male No Sun Dinner 2
153 24.55 2.00 Male No Sun Dinner 4
211 25.89 5.16 Male Yes Sat Dinner 4
198 13.00 2.00 Female Yes Thur Lunch 2

Quick check: which day has the most bills in this data?

tips["day"].value_counts()
day
Sat     87
Sun     76
Thur    62
Fri     19
Name: count, dtype: int64
tips["time"].value_counts()
time
Dinner    176
Lunch      68
Name: count, dtype: int64

Step 3: summarise the numbers and categories#

Problem: describe a typical bill, tip and party in a few lines.

tips.describe().round(2)
total_bill tip size
count 244.00 244.00 244.00
mean 19.79 3.00 2.57
std 8.90 1.38 0.95
min 3.07 1.00 1.00
25% 13.35 2.00 2.00
50% 17.80 2.90 2.00
75% 24.13 3.56 3.00
max 50.81 10.00 6.00
tips.describe(exclude="number")
sex smoker day time
count 244 244 244 244
unique 2 2 4 2
top Male No Sat Dinner
freq 157 151 87 176
tips["size"].value_counts().sort_index()
size
1      4
2    156
3     38
4     37
5      5
6      4
Name: count, dtype: int64
tips["smoker"].value_counts(normalize=True).round(3)
smoker
No     0.619
Yes    0.381
Name: proportion, dtype: float64

Your turn#

What is the median tip, and the largest tip ever left?

print(tips["tip"].median())
print(tips["tip"].max())
2.9
10.0
import matplotlib.pyplot as plt
ax = tips["tip"].plot(kind="hist", bins=20, figsize=(7, 4))
ax.set_title("Tips left on 244 restaurant bills")
ax.set_xlabel("Tip (dollars)")
ax.set_ylabel("Number of bills")
plt.show()
No description has been provided for this image

Step 4: add columns that answer questions#

Problem: a $3 tip on $10 is not the same as $3 on $50.

tips["tip_pct"] = tips["tip"] / tips["total_bill"] * 100
tips[["total_bill", "tip", "tip_pct"]].head().round(1)
total_bill tip tip_pct
0 17.0 1.0 5.9
1 10.3 1.7 16.1
2 21.0 3.5 16.7
3 23.7 3.3 14.0
4 24.6 3.6 14.7
tips["per_person"] = tips["total_bill"] / tips["size"]
print(round(tips["per_person"].median(), 2))
print(round(tips["tip_pct"].median(), 2))
7.26
15.48
tips["big_party"] = tips["size"] >= 3
tips["big_party"].value_counts()
big_party
False    160
True      84
Name: count, dtype: int64

Your turn#

Which 3 bills had the highest spend per person?

pp_cols = ["total_bill", "size", "per_person"]
tips.nlargest(3, "per_person")[pp_cols].round(2)
total_bill size per_person
184 40.55 2 20.27
179 34.63 2 17.32
170 50.81 3 16.94

Quick check: what share of bills tipped 20 percent or more?

generous = tips["tip_pct"] >= 20
print(generous.sum(), round(generous.mean(), 3))
39 0.16

Quick check: which single bill had the highest tip percentage?

cols = ["total_bill", "tip", "size", "day", "tip_pct"]
tips.nlargest(3, "tip_pct")[cols].round(1)
total_bill tip size day tip_pct
172 7.2 5.2 2 Sun 71.0
178 9.6 4.0 2 Sun 41.7
67 3.1 1.0 1 Sat 32.6

Step 5: filter and sort to find specific bills#

Problem: answer 'which bills?' questions, with more than one condition.

dinner = tips[tips["time"] == "Dinner"]
print(len(dinner), round(dinner["tip_pct"].median(), 2))
176 15.54

Common mistake: using and to combine two conditions

tips[(tips["day"] == "Sat") and (tips["smoker"] == "Yes")]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_12428\1221388770.py in ?()
----> 1 tips[(tips["day"] == "Sat") and (tips["smoker"] == "Yes")]

~\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().
sat_smoke = (tips["day"] == "Sat") & (tips["smoker"] == "Yes")
print(sat_smoke.sum())
42
fri_sun = (tips["day"] == "Fri") | (tips["day"] == "Sun")
print(fri_sun.sum())
95
by_bill = tips.sort_values("total_bill", ascending=False)
by_bill[cols].head(3).round(1)
total_bill tip size day tip_pct
170 50.8 10.0 3 Sat 19.7
212 48.3 9.0 4 Sat 18.6
59 48.3 6.7 4 Sat 13.9
two_keys = tips.sort_values(["day", "tip_pct"],
                            ascending=[True, False])
two_keys[cols].head(3).round(1)
total_bill tip size day tip_pct
93 16.3 4.3 2 Fri 26.3
221 13.4 3.5 2 Fri 25.9
222 8.6 1.9 1 Fri 22.4

Your turn#

Show the 3 lunch bills with the highest tip percentage.

lunch = tips[tips["time"] == "Lunch"]
lunch.nlargest(3, "tip_pct")[cols].round(1)
total_bill tip size day tip_pct
149 7.5 2.0 2 Thur 26.6
221 13.4 3.5 2 Fri 25.9
194 16.6 4.0 2 Thur 24.1

Step 6: chart a relationship#

Problem: see at a glance whether two columns move together.

ax = tips.plot(kind="scatter", x="total_bill", y="tip",
                figsize=(7, 4), alpha=0.6)
ax.set_title("Tip against bill for 244 tables")
ax.set_xlabel("Total bill (dollars)")
ax.set_ylabel("Tip (dollars)")
plt.show()
No description has been provided for this image

Quick check: do bigger parties leave a smaller tip percentage? Guess first.

Common mistake: passing a column name to corr

tips["total_bill"].corr("tip")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_12428\1045464817.py in ?()
----> 1 tips["total_bill"].corr("tip")

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\series.py in ?(self, other, method, min_periods)
   2762         >>> s1 = pd.Series([0.45, 0.45])
   2763         >>> s1.corr(s1)
   2764         nan
   2765         """  # noqa: E501
-> 2766         this, other = self.align(other, join="inner")
   2767         if len(this) == 0:
   2768             return np.nan
   2769 

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py in ?(self, other, join, axis, level, copy, fill_value)
   9836                 level=level,
   9837                 fill_value=fill_value,
   9838             )
   9839         else:  # pragma: no cover
-> 9840             raise TypeError(f"unsupported type: {type(other)}")
   9841 
   9842         right = cast(NDFrameT, _right)
   9843         if self.ndim == 1 or axis == 0:

TypeError: unsupported type: <class 'str'>
print(round(tips["total_bill"].corr(tips["tip"]), 3))
print(round(tips["size"].corr(tips["tip_pct"]), 3))
0.676
-0.143

Your turn#

Draw a bar chart of the number of bills on each day.

counts = tips["day"].value_counts()
ax = counts.plot(kind="bar", rot=0, figsize=(7, 4))
ax.set_title("Bills per day")
ax.set_xlabel("Day")
ax.set_ylabel("Number of bills")
plt.show()
No description has been provided for this image

Mini-project: do bigger parties tip less generously?#

Plan: compare small and big parties, check each size, then test outliers.

big = tips["big_party"]
print(round(tips.loc[big, "tip"].median(), 2),
      round(tips.loc[~big, "tip"].median(), 2))
3.5 2.39
big_mean = tips.loc[big, "tip_pct"].mean()
small_mean = tips.loc[~big, "tip_pct"].mean()
print(round(big_mean, 2), round(small_mean, 2))
14.9 16.7
by_size = tips.groupby("size")["tip_pct"].agg(["count", "median"])
by_size.round(1)
count median
size
1 4 20.3
2 156 15.6
3 38 15.9
4 37 14.7
5 5 12.1
6 4 16.3
big_med = tips.loc[big, "tip_pct"].median()
small_med = tips.loc[~big, "tip_pct"].median()
print(round(big_med, 2), round(small_med, 2))
14.95 15.67
normal = tips["tip_pct"] < 40
t_big = tips.loc[big & normal, "tip_pct"].mean()
t_small = tips.loc[~big & normal, "tip_pct"].mean()
print((~normal).sum(), round(t_big, 2), round(t_small, 2))
2 14.9 16.2
summary = pd.DataFrame({"mean": [small_mean, big_mean],
                        "median": [small_med, big_med]},
                       index=["1-2 people", "3+ people"])
ax = summary.plot(kind="bar", rot=0, figsize=(7, 4))
ax.set_title("Tip percentage by party size")
ax.set_xlabel("Party")
ax.set_ylabel("Tip (% of bill)")
plt.show()
No description has been provided for this image
gap_mean = small_mean - big_mean
gap_med = small_med - big_med
print(f"Gap: {gap_mean:.1f} pts by mean, {gap_med:.1f} by median")
Gap: 1.8 pts by mean, 0.7 by median

Recap#

  • Build small tables from a dict of lists; extend them with new columns and concat
  • First look: shape, head, info, gaps, a seeded sample
  • Ratios like tip_pct make unequal rows comparable
  • Combine conditions with & and |, never and / or
  • Check means against medians before trusting a gap

Challenges#

  1. Do smokers tip a different percentage? Compare means and medians.
  2. Add a weekend column (Sat or Sun) and compare tip_pct.
  3. Build a DataFrame from the last 3 bills and add tip per person.

What's next#

Lesson 07: Importing and Exporting Data (CSV, Excel, JSON, SQL) Save your tables to files and load data from many formats.

Found this useful?

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