The idea
Raw data rarely has the exact columns your analysis needs. You'll add calculated columns (margin, VAT, a "big order" flag), rename cryptic export names (TGL_TRX → order_date), and drop columns nobody uses. These operations are simple, but doing them cleanly (in one place, with clear names, without surprising side effects) is what makes an analysis easy to follow.
import pandas as pd
import numpy as np
sales = pd.read_csv("sales.csv")
lines = sales[["order_id", "product", "category", "quantity", "unit_price", "revenue", "status"]].copy()
.copy() makes lines an independent DataFrame, so adding columns to it never affects sales (and avoids the warning from the last module).
Adding a calculated column
Assign to a new column name. The right-hand side is usually vectorised maths on other columns:
lines["vat"] = (lines["revenue"] * 0.11).round().astype(int)
lines["revenue_incl_vat"] = lines["revenue"] + lines["vat"]
print(lines[["product", "revenue", "vat", "revenue_incl_vat"]].head(3))
product revenue vat revenue_incl_vat
0 Desk Organizer 89000 9790 98790
1 Arabica Gayo 250g 285000 31350 316350
2 Pour-over Kettle 960000 105600 1065600
.round() rounds, and .astype(int) converts the floats back to whole numbers. A single value works too: lines["currency"] = "IDR" fills every row with it.
Conditional columns
For a two-way choice, np.where(condition, value_if_true, value_if_false) from NumPy is the standard tool:
lines["size"] = np.where(lines["revenue"] >= 300000, "large", "small")
print(lines["size"].value_counts())
size
small 52
large 19
Name: count, dtype: int64
For several bands, pd.cut turns numbers into labelled ranges:
lines["price_band"] = pd.cut(
lines["unit_price"],
bins=[0, 60000, 150000, float("inf")],
labels=["budget", "mid", "premium"],
)
print(lines[["product", "unit_price", "price_band"]].head(4))
product unit_price price_band
0 Desk Organizer 89000 mid
1 Arabica Gayo 250g 95000 mid
2 Pour-over Kettle 320000 premium
3 Scented Candle 75000 mid
bins are the edges: 0 to 60,000 is "budget", 60,000 to 150,000 "mid", and anything above "premium". Each bin includes its right edge (so 60,000 counts as budget).
For arbitrary mappings, .map() with a dictionary translates values (module 12 covers it fully):
category_owner = {"Coffee": "Maya", "Kitchen": "Maya", "Stationery": "Agus", "Electronics": "Agus", "Home": "Maya"}
lines["owner"] = lines["category"].map(category_owner)
assign: add columns without changing the original
df.assign(...) returns a new DataFrame with extra columns. It doesn't touch df, which makes it perfect for chaining steps together (module 20) and for keeping the original intact:
enriched = lines.assign(
margin_rate=0.4,
est_margin=lambda d: (d["revenue"] * d["margin_rate"]).round().astype(int),
)
print(enriched[["revenue", "margin_rate", "est_margin"]].head(2))
print("est_margin" in lines.columns)
revenue margin_rate est_margin
0 89000 0.4 35600
1 285000 0.4 114000
False
The lambda d: ... form receives the DataFrame as built so far, so est_margin can use margin_rate, which was created a moment earlier in the same assign.
Renaming columns
Exports from other systems often come with column names like TGL_TRX or NM PRODUK, which mean something to whoever built that system but are awkward to type and hard to read in your code. Renaming them once, right after loading, makes everything after that easier. Pass rename a dictionary that maps each old name to its new name:
export = pd.DataFrame({"TGL_TRX": ["01/07/2024"], "NM PRODUK": ["Kopi Susu"], "Qty": [2]})
export = export.rename(columns={"TGL_TRX": "order_date", "NM PRODUK": "product", "Qty": "quantity"})
print(list(export.columns))
['order_date', 'product', 'quantity']
To clean every name at once (lowercase, underscores instead of spaces), transform the column labels directly:
raw = pd.DataFrame(columns=["Order ID", " Customer Name ", "Total (Rp)"])
raw.columns = (
raw.columns.str.strip()
.str.lower()
.str.replace(r"[^a-z0-9]+", "_", regex=True)
.str.strip("_")
)
print(list(raw.columns))
['order_id', 'customer_name', 'total_rp']
Consistent snake_case column names mean you never have to remember whether it was "Total (Rp)" or "total". Many analysts make this the first line after every read_csv.
Dropping columns
Helper columns you only needed for a calculation, like the VAT columns from earlier, can clutter the final table. drop removes the columns you name:
slim = lines.drop(columns=["vat", "revenue_incl_vat"])
print(slim.shape, lines.shape)
(71, 10) (71, 12)
drop returns a new frame. errors="ignore" stops it complaining if a column doesn't exist. Selecting the columns you keep (lines[[...]]) is often clearer than listing what to drop, because the result's shape is visible in the code.
Reordering columns
Column order matters more than it seems. The person reading your table looks at the left-hand columns first, so the most important ones (the order id, the product, the revenue) should come first. There's no special reorder method; you just select the columns in the order you want:
first = ["order_id", "product", "revenue"]
ordered = lines[first + [c for c in lines.columns if c not in first]]
print(list(ordered.columns)[:5])
['order_id', 'product', 'revenue', 'category', 'quantity']
Why this matters for clean code
Give derived columns names that say exactly what they contain (revenue_incl_vat, not rev2), standardise column names once at load time, and prefer assign when you want each step to produce a new, clearly named result. Avoid quietly modifying a shared DataFrame from several places: when a column appears "from nowhere", the next reader has to hunt for where it was created.
Recap
df["new"] = expressionadds a column; vectorised maths runs on whole columns.np.where(cond, a, b)for two-way choices;pd.cut(col, bins, labels)for ranges;.map(dict)for lookups.df.assign(name=value_or_lambda)returns a new frame; lambdas see columns created earlier in the same call.df.rename(columns={old: new}); clean all names viadf.columns.str....df.drop(columns=[...])returns a new frame; selecting the columns to keep is often clearer.- Reorder by selecting columns in the order you want.