Mathew K Analytics

Lesson 12 · Pandas Full Course

Rename Columns in Pandas the Clean Way (One Line for Every Column) | Pandas Course #12

In this lesson you'll be able to: Rename chosen columns safely with rename(columns=...) Clean every column name at once with string methods Add prefixes 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 12: Renaming Columns and Indexes#

In this lesson you'll be able to:

  • Rename chosen columns safely with rename(columns=...)
  • Clean every column name at once with string methods
  • Add prefixes and suffixes, and replace all names with set_axis
  • Rename index labels and name the index itself
  • Turn a grouped result into a clean, report-ready table Dataset: Superstore sales, 8399 orders (data/pandas/superstore.csv)

Warm-up: an index from lesson 11#

Lesson 11 set and reset indexes. Same skill, sales data.

import pandas as pd
df = pd.read_csv("data/pandas/superstore.csv")
print(df.shape)
print(df.columns.tolist())
(8399, 21)
['Row ID', 'Order ID', 'Order Date', 'Order Priority', 'Order Quantity', 'Sales', 'Discount', 'Ship Mode', 'Profit', 'Unit Price', 'Shipping Cost', 'Customer Name', 'Province', 'Region', 'Customer Segment', 'Product Category', 'Product Sub-Category', 'Product Name', 'Product Container', 'Product Base Margin', 'Ship Date']
by_row = df.set_index("Row ID")
by_row.loc[1, ["Customer Name", "Sales", "Profit"]]
Customer Name    Muhammed MacIntyre
Sales                        261.54
Profit                      -213.25
Name: 1, dtype: object

Rename chosen columns with rename#

Problem: names with spaces and hyphens are awkward in code.

df.query("`Order Quantity` > 45").shape
(849, 21)
new_names = {"Product Sub-Category": "subcategory",
             "Customer Segment": "segment"}
short = df.rename(columns=new_names)
short[["segment", "subcategory"]].head(3)
segment subcategory
0 Small Business Storage & Organization
1 Consumer Appliances
2 Consumer Binders and Binder Accessories

Quick check: did rename change the original df?

print("segment" in df.columns)
print("Customer Segment" in df.columns)
False
True

Common mistake: a typo in the old name

oops = df.rename(columns={"sales": "revenue"})
oops["revenue"].head(3)
---------------------------------------------------------------------------
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: 'revenue'

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

KeyError                                  Traceback (most recent call last)
Cell In[7], line 2
      1 oops = df.rename(columns={"sales": "revenue"})
----> 2 oops["revenue"].head(3)

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: 'revenue'
fixed = df.rename(columns={"Sales": "revenue"}, errors="raise")
fixed["revenue"].head(3)
0      261.54
1    10123.02
2      244.57
Name: revenue, dtype: float64
df = df.rename(columns={"Product Base Margin": "base_margin"})
print(df.columns[-2:].tolist())
['base_margin', 'Ship Date']

Your turn#

Rename Order Priority to priority and Ship Mode to ship, then count the ship values.

quick = df.rename(columns={"Order Priority": "priority",
                           "Ship Mode": "ship"})
quick["ship"].value_counts()
ship
Regular Air       6270
Delivery Truck    1146
Express Air        983
Name: count, dtype: int64

Clean every name at once with string methods#

Problem: 21 names to fix. Typing a dictionary for each is slow.

cols = df.columns.str.lower()
cols = cols.str.replace(" ", "_").str.replace("-", "_")
print(cols[:8].tolist())
['row_id', 'order_id', 'order_date', 'order_priority', 'order_quantity', 'sales', 'discount', 'ship_mode']
df.columns = cols
df.product_sub_category.head(3)
0            Storage & Organization
1                        Appliances
2    Binders and Binder Accessories
Name: product_sub_category, dtype: str
print(df.columns.is_unique, len(df.columns))
True 21
raw = pd.DataFrame(columns=[" Sales ", "Unit Price", "QTY"])
print(raw.rename(columns=str.lower).columns.tolist())
tidy = raw.columns.str.strip().str.lower().str.replace(" ", "_")
print(tidy.tolist())
[' sales ', 'unit price', 'qty']
['sales', 'unit_price', 'qty']
prod = df.filter(like="product_").columns
print(prod.str.removeprefix("product_").tolist())
['category', 'sub_category', 'name', 'container']

Quick check: how many original names contained a space?

head = pd.read_csv("data/pandas/superstore.csv", nrows=0)
print(head.columns.str.contains(" ").sum())
16
nice = df.columns.str.replace("_", " ").str.title()
print(nice[:4].tolist())
['Row Id', 'Order Id', 'Order Date', 'Order Priority']

Common mistake: a new list of the wrong length

money = df[["sales", "profit", "discount"]].copy()
money.columns = ["revenue", "profit"]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[18], line 2
      1 money = df[["sales", "profit", "discount"]].copy()
----> 2 money.columns = ["revenue", "profit"]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:6208, in NDFrame.__setattr__(self, name, value)
   6206 try:
   6207     object.__getattribute__(self, name)
-> 6208     return object.__setattr__(self, name, value)
   6209 except AttributeError:
   6210     pass

File pandas/_libs/properties.pyx:69, in pandas._libs.properties.AxisProperty.__set__()

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py:754, in NDFrame._set_axis(self, axis, labels)
    749 """
    750 This is called from the cython code when we set the `index` attribute
    751 directly, e.g. `series.index = [1, 2, 3]`.
    752 """
    753 labels = ensure_index(labels)
--> 754 self._mgr.set_axis(axis, labels)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\managers.py:273, in BaseBlockManager.set_axis(self, axis, new_labels)
    271 def set_axis(self, axis: AxisInt, new_labels: Index) -> None:
    272     # Caller is responsible for ensuring we have an Index object.
--> 273     self._validate_set_axis(axis, new_labels)
    274     self.axes[axis] = new_labels

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\managers.py:288, in BaseBlockManager._validate_set_axis(self, axis, new_labels)
    285     pass
    287 elif new_len != old_len:
--> 288     raise ValueError(
    289         f"Length mismatch: Expected axis has {old_len} elements, new "
    290         f"values have {new_len} elements"
    291     )

ValueError: Length mismatch: Expected axis has 3 elements, new values have 2 elements
money.columns = ["revenue", "profit", "discount"]
money.head(3)
revenue profit discount
0 261.54 -213.25 0.04
1 10123.02 457.81 0.07
2 244.57 46.71 0.01

Your turn#

Make every column name upper case, then show the first 4 names.

upper = df.rename(columns=str.upper)
print(upper.columns[:4].tolist())
['ROW_ID', 'ORDER_ID', 'ORDER_DATE', 'ORDER_PRIORITY']

Add prefixes and suffixes, or replace every name#

Problem: mark units or sources on many columns at once.

cad = df[["sales", "profit", "shipping_cost"]].add_suffix("_cad")
print(cad.columns.tolist())
['sales_cad', 'profit_cad', 'shipping_cost_cad']
tagged = df[["sales", "profit"]].add_prefix("store_")
tagged.head(3)
store_sales store_profit
0 261.54 -213.25
1 10123.02 457.81
2 244.57 46.71
labels = ["Order", "Sales ($)", "Profit ($)"]
report = df[["order_id", "sales", "profit"]].set_axis(labels,
                                                axis=1)
report.head(3)
Order Sales ($) Profit ($)
0 3 261.54 -213.25
1 293 10123.02 457.81
2 293 244.57 46.71

Your turn#

Keep order_quantity and unit_price, renamed to qty and price.

pairs = {"order_quantity": "qty", "unit_price": "price"}
small = df[list(pairs)].rename(columns=pairs)
small.head(3)
qty price
0 6 38.94
1 49 208.16
2 27 8.69

Rename index labels and name the index#

Problem: a typo in the data shows up as a row label.

regions = df.groupby("region")[["sales", "profit"]].sum()
regions.round(0)
sales profit
region
Atlantic 2014248.0 238961.0
Northwest Territories 800847.0 100653.0
Nunavut 116376.0 2841.0
Ontario 3063212.0 346869.0
Prarie 2837305.0 321160.0
Quebec 1510195.0 140427.0
West 3597549.0 297009.0
Yukon 975867.0 73849.0
regions = regions.rename(index={"Prarie": "Prairie"})
print(regions.index.tolist())
['Atlantic', 'Northwest Territories', 'Nunavut', 'Ontario', 'Prairie', 'Quebec', 'West', 'Yukon']
regions.loc["Prairie"].round(0)
sales     2837305.0
profit     321160.0
Name: Prairie, dtype: float64
print(regions.index.name)
regions = regions.rename_axis("Region")
print(regions.index.name)
region
Region
print(regions.rename(index=str.upper).index[:3].tolist())
['ATLANTIC', 'NORTHWEST TERRITORIES', 'NUNAVUT']
regions.rename_axis(columns="Measure").head(2).round(0)
Measure sales profit
Region
Atlantic 2014248.0 238961.0
Northwest Territories 800847.0 100653.0

Quick check: did renaming the label fix Prarie in df?

print((df["region"] == "Prarie").sum())
1706
df["region"] = df["region"].replace({"Prarie": "Prairie"})
print((df["region"] == "Prairie").sum())
1706

Common mistake: renaming a row label with columns=

wrong = regions.rename(columns={"Yukon": "YT"}, errors="raise")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_25048\3258849768.py in ?()
----> 1 wrong = regions.rename(columns={"Yukon": "YT"}, errors="raise")

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py in ?(self, mapper, index, columns, axis, copy, inplace, level, errors)
   6481         2  2  5
   6482         4  3  6
   6483         """
   6484         self._check_copy_deprecation(copy)
-> 6485         return super()._rename(
   6486             mapper=mapper,
   6487             index=index,
   6488             columns=columns,

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py in ?(self, mapper, index, columns, axis, inplace, level, errors)
   1047                         label
   1048                         for index, label in enumerate(replacements)
   1049                         if indexer[index] == -1
   1050                     ]
-> 1051                     raise KeyError(f"{missing_labels} not found in axis")
   1052 
   1053             new_index = ax._transform_index(f, level=level)
   1054             result._set_axis_nocheck(new_index, axis=axis_no, inplace=True)

KeyError: "['Yukon'] not found in axis"
regions.rename(index={"Yukon": "YT"}).tail(2).round(0)
sales profit
Region
West 3597549.0 297009.0
YT 975867.0 73849.0

Your turn#

Shorten Northwest Territories to NWT and Nunavut to NU in regions.

short_reg = {"Northwest Territories": "NWT", "Nunavut": "NU"}
print(regions.rename(index=short_reg).index.tolist())
['Atlantic', 'NWT', 'NU', 'Ontario', 'Prairie', 'Quebec', 'West', 'Yukon']

Flatten and rename grouped column names#

Problem: agg with a list makes two-level column names.

cat = df.groupby("product_category")["profit"].agg(["sum", "mean"])
cat = cat.rename(columns={"sum": "total", "mean": "average"})
cat.round(1)
total average
product_category
Furniture 117433.0 68.1
Office Supplies 518021.4 112.4
Technology 886313.5 429.2
two = df.groupby("product_category").agg({"sales": ["sum", "max"]})
print(two.columns.tolist())
[('sales', 'sum'), ('sales', 'max')]
two.columns = ["_".join(pair) for pair in two.columns]
two.round(0)
sales_sum sales_max
product_category
Furniture 5178591.0 29345.0
Office Supplies 3752762.0 25410.0
Technology 5984248.0 89061.0

Quick check: which category has the biggest single sale?

print(two["sales_max"].idxmax())
print(two["sales_max"].max().round(2))
Technology
89061.05
named = df.groupby("product_category").agg(
    total=("profit", "sum"),
    orders=("order_id", "count"))
named.round(1)
total orders
product_category
Furniture 117433.0 1724
Office Supplies 518021.4 4610
Technology 886313.5 2065
modes = df["ship_mode"].value_counts()
modes.rename("orders").rename_axis("mode")
mode
Regular Air       6270
Delivery Truck    1146
Express Air        983
Name: orders, dtype: int64

Your turn#

Profit by customer_segment as sum and mean, renamed to total_profit and avg_profit, index named Segment.

seg = df.groupby("customer_segment")["profit"].agg(["sum", "mean"])
seg = seg.rename(columns={"sum": "total_profit",
                          "mean": "avg_profit"})
seg.rename_axis("Segment").round(1)
total_profit avg_profit
Segment
Consumer 287959.9 174.6
Corporate 599746.0 195.0
Home Office 318354.0 156.7
Small Business 315708.0 192.3

Mini-project: a report-ready profit table by region#

Plan: summarise, add margin, give friendly names, save, re-read.

rep = df.groupby("region")[["sales", "profit"]].sum()
rep["margin"] = rep["profit"] / rep["sales"] * 100
rep = rep.sort_values("margin", ascending=False)
rep.round(1)
sales profit margin
region
Northwest Territories 800847.3 100653.1 12.6
Atlantic 2014248.2 238960.7 11.9
Ontario 3063212.5 346868.5 11.3
Prairie 2837304.6 321160.1 11.3
Quebec 1510195.1 140426.6 9.3
West 3597549.3 297008.6 8.3
Yukon 975867.4 73849.2 7.6
Nunavut 116376.5 2841.1 2.4
friendly = {"sales": "Sales ($)", "profit": "Profit ($)",
            "margin": "Margin (%)"}
final = rep.rename(columns=friendly).rename_axis("Region")
final.round(1)
Sales ($) Profit ($) Margin (%)
Region
Northwest Territories 800847.3 100653.1 12.6
Atlantic 2014248.2 238960.7 11.9
Ontario 3063212.5 346868.5 11.3
Prairie 2837304.6 321160.1 11.3
Quebec 1510195.1 140426.6 9.3
West 3597549.3 297008.6 8.3
Yukon 975867.4 73849.2 7.6
Nunavut 116376.5 2841.1 2.4
import os
os.makedirs("output/pandas", exist_ok=True)
path = "output/pandas/l12_region_report.csv"
final.round(1).to_csv(path)
back = pd.read_csv(path, index_col="Region")
print(back.columns.tolist())
['Sales ($)', 'Profit ($)', 'Margin (%)']
import matplotlib.pyplot as plt
ax = back["Margin (%)"].sort_values().plot(kind="barh",
                                           figsize=(7, 4))
ax.set_title("Profit margin by region")
ax.set_xlabel("Margin (% of sales)")
ax.set_ylabel("Region")
plt.show()
No description has been provided for this image
total = back["Profit ($)"].sum() / back["Sales ($)"].sum() * 100
print(round(total, 1))
10.2
back.loc[["Ontario", "West"]]
Sales ($) Profit ($) Margin (%)
Region
Ontario 3063212.5 346868.5 11.3
West 3597549.3 297008.6 8.3
west = back.loc["West"]
gap = (total - west["Margin (%)"]) / 100
print(round(west["Sales ($)"] * gap))
68444
best = back["Margin (%)"].idxmax()
top_sales = back["Sales ($)"].idxmax()
print(best, back.loc[best, "Margin (%)"])
print(top_sales, back.loc[top_sales, "Margin (%)"])
Northwest Territories 12.6
West 8.3

Recap#

  • rename(columns={old: new}) returns a new table; assign it back
  • errors="raise" turns silent typos into errors
  • df.columns.str methods clean every name at once
  • add_prefix, add_suffix and set_axis tag or replace all names
  • rename(index=...) and rename_axis fix row labels and the index name

Challenges#

  1. Rename ship_mode and order_priority to shorter names, with errors="raise".
  2. Build profit by customer segment and name the index "Segment".
  3. Add the prefix "ship_" to the shipping columns only.

What's next#

Lesson 13: Sorting and Ranking Data Sort by several columns, handle ties, and rank within groups.

Found this useful?

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