EduFlow

Section 2 · pandas Foundations

Adding and Changing Columns

18 min4 min readPython exercise

Lesson at a glance

Create calculated columns with vectorised arithmetic, build conditional columns with .loc and np.where, map codes to labels, and change values safely under pandas Copy-on-Write rules.

  1. 1Calculated columns
  2. 2Conditional columns
  3. 3Translating codes with map
  4. 4Fixing a column’s type with astype
  5. 5Changing values: the one rule

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Cartwheel’s order_items table has a quantity, a unit price and a discount on each of its 3,991 lines, but no revenue. Revenue is something you calculate, and how you calculate it has to be written down where everyone can see it. That is what a computed column does: it turns a business rule into one line of code applied to every row.

Calculated columns

Assigning to a new column name creates it:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
items["gross"] = items["quantity"] * items["unit_price"]
items["revenue"] = items["gross"] * (1 - items["discount"])
items["discount_amount"] = items["gross"] - items["revenue"]
print(items.head().to_string())
print(round(items["revenue"].sum(), 2), round(items["discount_amount"].sum(), 2))

Each line works on whole columns, row by row, with no loop. The result: 471,940.08 dollars of revenue across all lines, after 11,658.52 dollars of discounts. (That total still includes cancelled orders; you will remove them in Section 4 once you can join order_items to orders.)

assign() does the same but returns a new DataFrame instead of changing the existing one, which lets you chain steps:

Python
import pandas as pd

items = (
    pd.read_csv("order_items.csv")
    .assign(revenue=lambda d: d["quantity"] * d["unit_price"] * (1 - d["discount"]))
    .assign(is_discounted=lambda d: d["discount"] > 0)
)
print(items[["revenue", "is_discounted"]].head(3))
print(items["is_discounted"].sum(), "discounted lines")

lambda d: ... is a tiny unnamed function that receives the DataFrame at that point in the chain. You need it when a step uses a column created earlier in the same chain. Both styles are fine; plain assignment is easier to read when you are starting out, and chains shine in the cleaning function you will write in Lesson 3.5.

Conditional columns

A two-way label is a job for NumPy’s where, which pandas is built on:

Python
import numpy as np
import pandas as pd

items = pd.read_csv("order_items.csv")
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
items["line_size"] = np.where(items["revenue"] >= 100, "large", "small")
print(items["line_size"].value_counts())

Read it as “where revenue is at least 100, large; elsewhere, small”. For three or more categories, start with a default and overwrite subsets with .loc:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
items["discount_band"] = "none"
items.loc[items["discount"] > 0, "discount_band"] = "small"
items.loc[items["discount"] > 0.10, "discount_band"] = "big"
print(items["discount_band"].value_counts())

Order matters exactly as it did with if/elif in Lesson 1.4, but reversed: later assignments overwrite earlier ones, so go from the broadest condition to the narrowest. Discounts of 5% and 10% end up small; 15% and 25% end up big.

Translating codes with map

Columns full of codes (mobile_app, ids, status flags) often need readable labels. map() with a dictionary translates each value:

Python
import pandas as pd

orders = pd.read_csv("orders.csv")
labels = {"web": "Website", "mobile_app": "Mobile app", "marketplace": "Marketplace"}
orders["channel_label"] = orders["channel"].map(labels)
print(orders["channel_label"].value_counts())
print(orders["channel_label"].isna().sum(), "unmapped")

Any value missing from the dictionary becomes NaN, silently. The last line is the check that catches it: zero unmapped values means every channel had a label.

Fixing a column’s type with astype

Sometimes the values are right but the type is not. products.csv stores is_active as 0 and 1, which pandas reads as integers. Summing works, but a reader of your code cannot tell a flag from a count, and filters read awkwardly. astype() converts a whole column:

Python
import pandas as pd

products = pd.read_csv("products.csv")
products["is_active"] = products["is_active"].astype(bool)
print(products["is_active"].dtype, products["is_active"].sum(), "active products")
print(products.loc[~products["is_active"], "name"].tolist())

Three products are discontinued, and ~products["is_active"] reads as “not active”. Use astype when every value can be converted cleanly; Section 3 shows what to do when some cannot, such as totals with dollar signs.

Changing values: the one rule

Here is the rule that saves you hours of confusion: to change values in a DataFrame, assign to it in a single step, with a new column or with .loc[rows, column]. Never select and then assign in two steps:

Python
# Wrong: chained assignment. Under Copy-on-Write this never updates items.
items["discount"][items["discount"] > 0.2] = 0.2

# Right: one .loc call names the rows and the column together.
items.loc[items["discount"] > 0.2, "discount"] = 0.2

The first form selects the discount column, which produces a new object, and then assigns into that object. Whether items itself changed used to depend on pandas internals, and pandas 2 prints a ChainedAssignmentError warning or a SettingWithCopyWarning when it spots the pattern. With Copy-on-Write (the default from pandas 3.0, available in pandas 2 through pd.options.mode.copy_on_write = True), the rule becomes simple and strict: every selection behaves like a copy, so chained assignment never updates the original. Write it the .loc way and your code behaves identically on every version.

That completes your pandas foundations: load, inspect, select, filter, sort and compute. Section 3 points these tools at the messy export, where blanks, typos, money strings and ambiguous dates are waiting.

Key takeaways

  • df["new"] = expression adds a column computed for every row at once; assign() does the same and returns a new DataFrame.
  • Change existing values with one .loc[row_mask, "col"] = value call, never with chained brackets like df["col"][mask] = value.
  • np.where(condition, a, b) builds a two-way column; .loc assignments in sequence or np.select handle more categories.
  • map() with a dictionary translates codes into labels, and values missing from the dictionary become NaN.
  • Under Copy-on-Write, every selection behaves like a copy, so the only way to change a DataFrame is to assign to it directly.

Further reading

Check your understanding

3 questions

Question 1 of 3

You want to cap every discount above 0.2 at 0.2. Which line does it correctly in pandas 2 and 3?

Question 2 of 3

orders["channel_label"] = orders["channel"].map({"web": "Website", "mobile_app": "App"}). What happens to marketplace orders?

Question 3 of 3

What does np.where(items["quantity"] >= 3, "bulk", "regular") produce?

Practice

Python exercise

Add revenue and labels to order lines

Add three columns to items: revenue (quantity × unit price × (1 − discount)), discount_band ("none" when the discount is 0, "small" when it is above 0 and at most 0.10, "big" when it is above 0.10), and is_bulk (a boolean, True when quantity is 4 or more). Then set total_revenue to the sum of revenue, rounded to 2 decimals. Keep all 3,991 rows.

Data files: order_items.csv

import pandas as pd

items = pd.read_csv("order_items.csv")

# 1. Column "revenue": quantity * unit_price * (1 - discount)

# 2. Column "discount_band": "none" when discount is 0,
#    "small" when it is above 0 and at most 0.10, "big" when above 0.10

# 3. Column "is_bulk": True when quantity is 4 or more

# 4. Total revenue of all lines, rounded to 2 decimals
total_revenue = None
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

(no output)

Checks

  • revenue exists and line 4 (index 3) is 270.90
  • Discounts are applied in revenue
  • discount_band splits lines into none 3,367 / small 231 / big 393
  • is_bulk is a boolean column with 126 bulk lines
  • total_revenue is 471,940.08
  • items still has all 3,991 rows