Mathew K Analytics

Lesson 5 · Pandas Full Course

Pandas DataFrame Basics: Add, Drop, Rename and Summarise Columns | Pandas Course #5

In this lesson you'll be able to: Read a table's key attributes: shape, size, columns, index, dtypes Add new columns with fast, whole-column arithmetic Drop…

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 05: Basic DataFrame Operations and Attributes#

In this lesson you'll be able to:

  • Read a table's key attributes: shape, size, columns, index, dtypes
  • Add new columns with fast, whole-column arithmetic
  • Drop and rename columns without losing your original data
  • Summarise down columns or across rows with the axis argument
  • Change column types safely, missing values included Dataset: the Titanic passenger list (data/pandas/titanic.csv)

Warm-up: load and inspect#

Lesson 04 loaded and inspected a file. Same moves, new dataset.

import pandas as pd
df = pd.read_csv("data/pandas/titanic.csv")
cols = ["Name", "Sex", "Age", "Pclass", "Fare"]
df[cols].head(3)
Name Sex Age Pclass Fare
0 Braund, Mr. Owen Harris male 22.0 3 7.2500
1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 71.2833
2 Heikkinen, Miss. Laina female 26.0 3 7.9250
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

Attributes: the facts a table knows about itself#

Problem: answer 'how big, what columns, what types?' instantly.

print(df.shape)
print(df.size)
print(df.ndim)
(891, 12)
10692
2
print(df.columns[:4])
print("Fare" in df.columns)
print(df.index)
Index(['PassengerId', 'Survived', 'Pclass', 'Name'], dtype='str')
True
RangeIndex(start=0, stop=891, step=1)
df.dtypes.value_counts()
int64      5
str        5
float64    2
Name: count, dtype: int64
df.drop(columns="Name").head(3).T
0 1 2
PassengerId 1 2 3
Survived 0 1 1
Pclass 3 1 3
Sex male female female
Age 22.0 38.0 26.0
SibSp 1 1 0
Parch 0 0 0
Ticket A/5 21171 PC 17599 STON/O2. 3101282
Fare 7.25 71.2833 7.925
Cabin NaN C85 NaN
Embarked S C S
arr = df[["Age", "Fare"]].to_numpy()
print(type(arr), arr.shape)
print(arr[:2])
<class 'numpy.ndarray'> (891, 2)
[[22.      7.25  ]
 [38.     71.2833]]

Quick check: did anyone on board have an age above 80?

over_80 = df[df["Age"] > 80]
print(over_80.empty)
print(df["Age"].max())
True
80.0

Common mistake: dot access with the wrong capital letter

df.fare.head()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_26028\1009979465.py in ?()
----> 1 df.fare.head()

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py in ?(self, name)
   6190             and name not in self._accessors
   6191             and self._info_axis._can_hold_identifiers_and_holds_name(name)
   6192         ):
   6193             return self[name]
-> 6194         return object.__getattribute__(self, name)

AttributeError: 'DataFrame' object has no attribute 'fare'
df["Fare"].head()
0     7.2500
1    71.2833
2     7.9250
3    53.1000
4     8.0500
Name: Fare, dtype: float64

Create new columns with whole-column arithmetic#

Problem: derive new facts, like family size, for all 891 rows at once.

df["family_size"] = df["SibSp"] + df["Parch"] + 1
df[["SibSp", "Parch", "family_size"]].head()
SibSp Parch family_size
0 1 0 2
1 1 0 2
2 0 0 1
3 1 0 2
4 0 0 1
df["fare_pp"] = df["Fare"] / df["family_size"]
df[["Fare", "family_size", "fare_pp"]].head().round(2)
Fare family_size fare_pp
0 7.25 2 3.62
1 71.28 2 35.64
2 7.92 1 7.92
3 53.10 2 26.55
4 8.05 1 8.05
df["is_child"] = df["Age"] < 18
df["is_child"].value_counts()
is_child
False    778
True     113
Name: count, dtype: int64
avg_fare = df["Fare"].mean()
above = df["Fare"] > avg_fare
print(round(avg_fare, 2), round(above.mean(), 3))
32.2 0.237
df["ship"] = "Titanic"
print(df["ship"].nunique(), df.shape)
1 (891, 16)

Common mistake: a list that doesn't match the number of rows

df["deck"] = ["A", "B", "C"]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[17], line 1
----> 1 df["deck"] = ["A", "B", "C"]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4672, in DataFrame.__setitem__(self, key, value)
   4669     self._setitem_array([key], value)
   4670 else:
   4671     # set column
-> 4672     self._set_item(key, value)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4874, in DataFrame._set_item(self, key, value)
   4864 def _set_item(self, key, value) -> None:
   4865     """
   4866     Add series to DataFrame in specified column.
   4867 
   (...)
   4872     ensure homogeneity.
   4873     """
-> 4874     value, refs = self._sanitize_column(value)
   4876     if (
   4877         key in self.columns
   4878         and value.ndim == 1
   4879         and not isinstance(value.dtype, ExtensionDtype)
   4880     ):
   4881         # broadcast across multiple columns if necessary
   4882         if not self.columns.is_unique or isinstance(self.columns, MultiIndex):

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:5756, in DataFrame._sanitize_column(self, value)
   5753     return _reindex_for_setitem(value, self.index)
   5755 if is_list_like(value):
-> 5756     com.require_length_match(value, self.index)
   5757 return sanitize_array(value, self.index, copy=True, allow_2d=True), None

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\common.py:601, in require_length_match(data, index)
    597 """
    598 Check the length of data matches the length of the index.
    599 """
    600 if len(data) != len(index):
--> 601     raise ValueError(
    602         "Length of values "
    603         f"({len(data)}) "
    604         "does not match length of index "
    605         f"({len(index)})"
    606     )

ValueError: Length of values (3) does not match length of index (891)
df["deck"] = df["Cabin"].str[0]
df["deck"].value_counts().head(4)
deck
C    59
B    47
D    33
E    32
Name: count, dtype: int64
months = df.assign(age_months=df["Age"] * 12)
print("age_months" in months.columns)
print("age_months" in df.columns)
True
False

Your turn#

How many passengers travelled with no family at all? Use family_size.

alone = df["family_size"] == 1
print(alone.sum())
print(round(alone.mean() * 100, 1))
537
60.3

Drop and rename columns safely#

Problem: trim and tidy a table without destroying the original.

slim = df.drop(columns=["Ticket", "Cabin", "ship"])
print(df.shape, slim.shape)
(891, 17) (891, 14)

Common mistake: dropping a column without saying it's a column

df.drop("Cabin")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[22], line 1
----> 1 df.drop("Cabin")

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:6302, in DataFrame.drop(self, labels, axis, index, columns, level, inplace, errors)
   6144 def drop(
   6145     self,
   6146     labels: IndexLabel | ListLike = None,
   (...)
   6153     errors: IgnoreRaise = "raise",
   6154 ) -> DataFrame | None:
   6155     """
   6156     Drop specified labels from rows or columns.
   6157 
   (...)
   6300             weight  1.0     0.8
   6301     """
-> 6302     return super().drop(
   6303         labels=labels,
   6304         axis=axis,
   6305         index=index,
   6306         columns=columns,
   6307         level=level,
   6308         inplace=inplace,
   6309         errors=errors,
   6310     )

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:4632, in NDFrame.drop(self, labels, axis, index, columns, level, inplace, errors)
   4630 for axis, labels in axes.items():
   4631     if labels is not None:
-> 4632         obj = obj._drop_axis(labels, axis, level=level, errors=errors)
   4634 if inplace:
   4635     self._update_inplace(obj)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:4674, in NDFrame._drop_axis(self, labels, axis, level, errors, only_slice)
   4672         new_axis = axis.drop(labels, level=level, errors=errors)
   4673     else:
-> 4674         new_axis = axis.drop(labels, errors=errors)
   4675     indexer = axis.get_indexer(new_axis)
   4677 # Case for non-unique axis
   4678 else:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:7268, in Index.drop(self, labels, errors)
   7266 if mask.any():
   7267     if errors != "ignore":
-> 7268         raise KeyError(f"{labels[mask].tolist()} not found in axis")
   7269     indexer = indexer[~mask]
   7270 return self.delete(indexer)

KeyError: "['Cabin'] not found in axis"
print(df.drop(columns="Cabin").shape)
print(df.drop(index=[0, 1]).shape)
(891, 16)
(889, 17)
names = {"Pclass": "pclass", "SibSp": "sib_sp"}
slim = slim.rename(columns=names)
print(slim.columns.tolist())
['PassengerId', 'Survived', 'pclass', 'Name', 'Sex', 'Age', 'sib_sp', 'Parch', 'Fare', 'Embarked', 'family_size', 'fare_pp', 'is_child', 'deck']

Quick check: after all that dropping, does df still have Cabin?

print("Cabin" in df.columns)
print("Cabin" in slim.columns)
True
False

Your turn#

Make a table people with Name, Sex and Age, all names in lower case.

people = df[["Name", "Sex", "Age"]]
people = people.rename(columns=str.lower)
people.head(3)
name sex age
0 Braund, Mr. Owen Harris male 22.0
1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0
2 Heikkinen, Miss. Laina female 26.0
order = ["Name", "Sex", "Age", "pclass", "fare_pp"]
slim[order].head(3).round(2)
Name Sex Age pclass fare_pp
0 Braund, Mr. Owen Harris male 22.0 3 3.62
1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 35.64
2 Heikkinen, Miss. Laina female 26.0 3 7.92

Summarise down columns or across rows#

Problem: one number per column, or one number per passenger.

fare = df["Fare"]
print(round(fare.sum(), 2), round(fare.mean(), 2))
print(fare.median(), fare.max())
28693.95 32.2
14.4542 512.3292

Common mistake: asking for the mean of a whole mixed table

df.mean()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[29], line 1
----> 1 df.mean()

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\util\_decorators.py:336, in deprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper(*args, **kwargs)
    330 if len(args) > num_allow_args:
    331     warnings.warn(
    332         msg.format(arguments=_format_argument_list(allow_args)),
    333         klass,
    334         stacklevel=find_stack_level(),
    335     )
--> 336 return func(*args, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:14475, in DataFrame.mean(self, axis, skipna, numeric_only, **kwargs)
  14394 @deprecate_nonkeyword_arguments(Pandas4Warning, allowed_args=["self"], name="mean")
  14395 def mean(
  14396     self,
   (...)
  14400     **kwargs,
  14401 ) -> Series | Any:
  14402     """
  14403     Return the mean of the values over the requested axis.
  14404 
   (...)
  14473     dtype: float64
  14474     """
> 14475     result = super().mean(
  14476         axis=axis, skipna=skipna, numeric_only=numeric_only, **kwargs
  14477     )
  14478     if isinstance(result, Series):
  14479         result = result.__finalize__(self, method="mean")

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:11819, in NDFrame.mean(self, axis, skipna, numeric_only, **kwargs)
  11811 def mean(
  11812     self,
  11813     *,
   (...)
  11817     **kwargs,
  11818 ) -> Series | float:
> 11819     return self._stat_function(
  11820         "mean", nanops.nanmean, axis, skipna, numeric_only, **kwargs
  11821     )

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:11773, in NDFrame._stat_function(self, name, func, axis, skipna, numeric_only, **kwargs)
  11769 nv.validate_func(name, (), kwargs)
  11771 validate_bool_kwarg(skipna, "skipna", none_allowed=False)
> 11773 return self._reduce(
  11774     func, name=name, axis=axis, skipna=skipna, numeric_only=numeric_only
  11775 )

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:13635, in DataFrame._reduce(self, op, name, axis, skipna, numeric_only, filter_type, **kwds)
  13631     df = df.T
  13633 # After possibly _get_data and transposing, we are now in the
  13634 #  simple case where we can use BlockManager.reduce
> 13635 res = df._mgr.reduce(blk_func)
  13636 out = df._constructor_from_mgr(res, axes=res.axes).iloc[0]
  13637 out.name = None

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\managers.py:1694, in BlockManager.reduce(self, func)
   1691 # If 2D, we assume that we're operating column-wise
   1692 assert self.ndim == 2
-> 1694 res_blocks = [blk.reduce(func) for blk in self.blocks]
   1695 index = default_index(1)  # placeholder
   1696 # shallow copy self.items not needed because DataFrame._reduce does a getitem

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\blocks.py:358, in Block.reduce(self, func)
    352 @final
    353 def reduce(self, func) -> Block:
    354     # We will apply the function and reshape the result into a single-row
    355     #  Block with the same mgr_locs; squeezing will be done at a higher level
    356     assert self.ndim == 2
--> 358     result = func(self.values)
    360     if self.values.ndim == 1:
    361         res_values = result

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:13563, in DataFrame._reduce.<locals>.blk_func(values, axis)
  13561     if not is_1d_only_ea_dtype(values.dtype):
  13562         return values._reduce(name, axis=1, skipna=skipna, **kwds)
> 13563     return values._reduce(name, skipna=skipna, keepdims=True, **kwds)
  13564 else:
  13565     return op(values, axis=axis, skipna=skipna, **kwds)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\arrays\string_arrow.py:564, in ArrowStringArray._reduce(self, name, skipna, keepdims, **kwargs)
    562     result = self._reduce_calc(name, skipna=skipna, keepdims=keepdims, **kwargs)
    563 else:
--> 564     raise TypeError(f"Cannot perform reduction '{name}' with string dtype")
    566 if name in ("argmin", "argmax") and isinstance(result, pa.Array):
    567     return self._convert_int_result(result)

TypeError: Cannot perform reduction 'mean' with string dtype
df[["Age", "Fare", "family_size"]].mean().round(2)
Age            29.7
Fare           32.2
family_size     1.9
dtype: float64
df[["Age", "Fare"]].agg(["min", "max"])
Age Fare
min 0.42 0.0000
max 80.00 512.3292
rel = df[["SibSp", "Parch"]].sum(axis=1)
print(rel.head().tolist())
print((rel + 1).equals(df["family_size"]))
[1, 1, 0, 1, 0]
True
top = df["Fare"].idxmax()
print(top)
df.loc[top, ["Name", "Pclass", "Fare", "family_size"]]
258
Name           Ward, Miss. Anna
Pclass                        1
Fare                   512.3292
family_size                   1
Name: 258, dtype: object

Quick check: which class carried the most passengers?

counts = df["Pclass"].value_counts().sort_index()
print(counts)
print(df["Pclass"].nunique())
Pclass
1    216
2    184
3    491
Name: count, dtype: int64
3

Your turn#

What share of passengers survived, and what was the median age?

print(round(df["Survived"].mean(), 3))
print(df["Age"].median())
0.384
28.0

Change types with astype#

Problem: the right type saves memory and prevents silly mistakes.

print(df["Survived"].dtype)
survived = df["Survived"].astype(bool)
print(survived.head(4).tolist())
int64
[False, True, True, True]
before = df["Embarked"].memory_usage(deep=True)
df["Embarked"] = df["Embarked"].astype("category")
after = df["Embarked"].memory_usage(deep=True)
print(before, after)
8261 1051

Common mistake: converting a column with gaps to int

df["Age"].astype(int)
---------------------------------------------------------------------------
IntCastingNaNError                        Traceback (most recent call last)
Cell In[38], line 1
----> 1 df["Age"].astype(int)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:6529, in NDFrame.astype(self, dtype, copy, errors)
   6525     results = [ser.astype(dtype, errors=errors) for _, ser in self.items()]
   6527 else:
   6528     # else, only a single dtype is given
-> 6529     new_data = self._mgr.astype(dtype=dtype, errors=errors)
   6530     res = self._constructor_from_mgr(new_data, axes=new_data.axes)
   6531     return res.__finalize__(self, method="astype")

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\managers.py:627, in BaseBlockManager.astype(self, dtype, errors)
    626 def astype(self, dtype, errors: str = "raise") -> Self:
--> 627     return self.apply("astype", dtype=dtype, errors=errors)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\managers.py:445, in BaseBlockManager.apply(self, f, align_keys, **kwargs)
    443         applied = b.apply(f, **kwargs)
    444     else:
--> 445         applied = getattr(b, f)(**kwargs)
    446     result_blocks = extend_blocks(applied, result_blocks)
    448 out = type(self).from_blocks(result_blocks, [ax.view() for ax in self.axes])

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\blocks.py:607, in Block.astype(self, dtype, errors, squeeze)
    604         raise ValueError("Can not squeeze with more than one column.")
    605     values = values[0, :]  # type: ignore[call-overload]
--> 607 new_values = astype_array_safe(values, dtype, errors=errors)
    609 new_values = maybe_coerce_values(new_values)
    611 refs = None

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\dtypes\astype.py:240, in astype_array_safe(values, dtype, copy, errors)
    237     dtype = dtype.numpy_dtype
    239 try:
--> 240     new_values = astype_array(values, dtype, copy=copy)
    241 except (ValueError, TypeError):
    242     # e.g. _astype_nansafe can fail on object-dtype of strings
    243     #  trying to convert to float
    244     if errors == "ignore":

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\dtypes\astype.py:185, in astype_array(values, dtype, copy)
    182     values = values.astype(dtype, copy=copy)
    184 else:
--> 185     values = _astype_nansafe(values, dtype, copy=copy)
    187 # in pandas we don't store numpy str dtypes, so convert to object
    188 if isinstance(dtype, np.dtype) and issubclass(values.dtype.type, str):

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\dtypes\astype.py:102, in _astype_nansafe(arr, dtype, copy, skipna)
     97     return lib.ensure_string_array(
     98         arr, skipna=skipna, convert_na_value=False
     99     ).reshape(shape)
    101 elif np.issubdtype(arr.dtype, np.floating) and dtype.kind in "iu":
--> 102     return _astype_float_to_int_nansafe(arr, dtype, copy)
    104 elif arr.dtype == object:
    105     # if we have a datetime/timedelta array of objects
    106     # then coerce to datetime64[ns] and use DatetimeArray.astype
    108     if lib.is_np_dtype(dtype, "M"):

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\dtypes\astype.py:146, in _astype_float_to_int_nansafe(values, dtype, copy)
    142 """
    143 astype with a check preventing converting NaN to a meaningless integer value.
    144 """
    145 if not np.isfinite(values).all():
--> 146     raise IntCastingNaNError(
    147         "Cannot convert non-finite values (NA or inf) to integer."
    148         "Replace or remove non-finite values or cast to an integer type"
    149         "that supports these values (e.g. 'Int64')"
    150     )
    151 if dtype.kind == "u":
    152     # GH#45151
    153     if not (values >= 0).all():

IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')
age = df["Age"].round().astype("Int64")
print(age.dtype, age.isna().sum())
print(age.head().tolist())
Int64 177
[22, 38, 26, 35, 35]
first = df[df["Pclass"] == 1].copy()
first["Fare"] = first["Fare"].round()
print(first["Fare"].iloc[0], df.loc[1, "Fare"])
71.0 71.2833

Mini-project: did paying more per person help?#

Plan: split at the median fare per person, compare, then check within class.

cut = df["fare_pp"].median()
df["high_pp"] = df["fare_pp"] > cut
print(cut, df["high_pp"].sum())
8.3 445
high = df.loc[df["high_pp"], "Survived"].mean()
low = df.loc[~df["high_pp"], "Survived"].mean()
print(round(high, 3), round(low, 3))
0.508 0.26
third = df[df["Pclass"] == 3].copy()
third_cut = third["fare_pp"].median()
hi3 = third["fare_pp"] > third_cut
rate_hi = third.loc[hi3, "Survived"].mean()
rate_lo = third.loc[~hi3, "Survived"].mean()
print(round(rate_hi, 3), round(rate_lo, 3))
0.198 0.28
male_hi = (third.loc[hi3, "Sex"] == "male").mean()
male_lo = (third.loc[~hi3, "Sex"] == "male").mean()
print(round(male_hi, 2), round(male_lo, 2))
0.81 0.61
import matplotlib.pyplot as plt
rates = pd.Series([high, low, rate_hi, rate_lo],
                  index=["All: top half", "All: bottom half",
                         "3rd: top half", "3rd: bottom half"])
ax = rates.plot(kind="bar", rot=0, figsize=(8, 4))
ax.set_title("Survival by fare per person: everyone vs 3rd class")
ax.set_xlabel("Group")
ax.set_ylabel("Share who survived")
plt.show()
No description has been provided for this image
gap_all = (high - low) * 100
gap_3rd = (rate_hi - rate_lo) * 100
print(f"All: {gap_all:+.0f} pts, third class: {gap_3rd:+.0f} pts")
All: +25 pts, third class: -8 pts

Recap#

  • Attributes (shape, size, columns, dtypes, T) need no brackets
  • Whole-column arithmetic builds new columns without loops
  • drop and rename return new tables; name columns= explicitly
  • axis=0 summarises down columns, axis=1 across rows
  • astype changes types; use Int64 when values are missing

Challenges#

  1. Add a column fare_x2 and check its mean is double the fare mean.
  2. Rename Survived to survived, then drop PassengerId.
  3. Repeat the mini-project inside first class. Does the gap hold?

What's next#

Lesson 06: Hands-on - Creating and Exploring DataFrames Use everything so far to explore a brand new dataset from scratch.

Found this useful?

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