Studio Matrx Monthly · Volume 1 · Issue 3 · August 2026
Amogh N P
 In loving memory of Amogh N P — Architect · Designer · Visionary 
Cleaning and Transforming DataLesson 4.2
PSD for Architecture, Planning & Urban Design/Module 4 · Working with Design Data

Lesson 4.2 · Working with Design Data

Cleaning and Transforming Data

Real files are messy - blanks, wrong types, duplicates. This is how you filter, fix and reshape them into data you can actually trust and total

13 min Interactive lessonFree · open lessonByAmogh N P· Architect & interior designer
The hook

Nobody sends you clean data. The gap between a messy export and a schedule you can trust is a handful of pandas moves.

The room schedule has a blank area on row 40. Two rooms are named Kitchen and kitchen. A quantity column came in as text because one cell said TBC. This is not a broken file - this is every real file.

Cleaning and transforming is the craft that turns that mess into data you can total and chart. In this lesson you filter out what you do not need, sort to see what matters, decide what to do about the gaps, build new columns from the ones you have, and - the payoff move - use groupby to collapse hundreds of line items into the handful of totals a deliverable actually reports. It is the least glamorous and most valuable half of working with data.

Filter & sort -> decide about NaN -> compute columns -> groupby to total. Order matters. Write it as a recipe.

Filter and sort - see what matters

The first thing you usually do to a fresh table is narrow it and order it. You met the boolean filter last lesson; in real cleaning you often combine conditions, and the two rules to remember are that pandas uses & for and and | for or, and each condition needs its own parentheses:

python
import pandas as pd

df = pd.read_csv("ffe_list.csv")

joinery = df[(df["category"] == "joinery") & (df["qty"] > 0)]
flagged = df[(df["status"] == "TBC") | (df["cost"].isna())]

Forget a set of parentheses and Python raises an error, which is annoying but honest - it is telling you the condition was ambiguous. Once you have the rows you want, sorting puts the interesting ones on top:

python
df = df.sort_values("area_sqm", ascending=False)   # largest rooms first
df = df.sort_values(["floor", "area_sqm"])          # by floor, then area

sort_values takes one column or a list of columns, and ascending flips the direction. Sorting does not change the data, only the order you view it in, which makes it a safe, fast way to sanity-check a table: sort by area and the impossibly large or clearly-zero rooms jump to the ends where you can spot the errors. Filtering and sorting together are how you go from a wall of rows to the specific, ordered slice a question needs - the same two moves whether the table has twelve rows or twelve thousand.

CLEANING PIPELINERAW (messy)Kitchen 12 45kitchen __ 45HALL 8 TBChall 8 30Study __ 60blanks, case, textstr.lower().str.strip()dropna(subset=[qty])cost = qty * rategroupby(item).sum()CLEAN (usable)item total_costhall 240kitchen 540study ...one row per itemOrder matters: standardise labels before grouping; compute cost after dropping incomplete rows.Written as steps, the whole clean-up becomes a recipe you re-run when the data changes.
Zoom
A cleaning pipeline as before and after. A raw export arrives with blank cells, mixed-case labels and a quantity typed as text; a short chain of pandas steps - standardise, drop or fill gaps, compute, group - hands out a clean, summarised table you can trust and total.

Missing values - the decision you cannot skip

Real tables have holes: a cost not yet quoted, an area left blank, a supplier unknown. pandas marks these as NaN (not a number), and the important thing is that a missing value is a decision, not just a nuisance - you have to choose what it means for your calculation. First, find them:

python
df.isna().sum()          # count of missing values per column

That one line is the fastest audit of a file's completeness you will find. Then you choose. Sometimes the right move is to drop the incomplete rows; sometimes it is to fill the gaps with a sensible default:

python
quoted = df.dropna(subset=["cost"])         # keep only rows that have a cost
df["cost"] = df["cost"].fillna(0)           # or treat missing cost as zero
df["supplier"] = df["supplier"].fillna("TBC")

Which is correct depends entirely on the question. If you are totalling committed cost, dropping unquoted items is honest; if you are showing a worst-case budget, filling with an estimate is better; filling a missing cost with zero is fine for a placeholder but dangerous in a total, because it silently understates the number. There is no universal right answer, and that is the point: the value of doing this in code is that your choice is written down and repeatable, so anyone (including future you) can see exactly how the gaps were handled rather than guessing at a spreadsheet full of blanks.

A subtle trap lives here. A column that looks numeric can secretly be text if even one cell holds something like TBC or n/a, and pandas will refuse to do arithmetic on it or will treat the whole column as strings. When that happens, pd.to_numeric(df["cost"], errors="coerce") is the reliable fix: it converts every value it can into a real number and turns the ones it cannot into NaN, which you then handle with the same drop-or-fill decision. So the honest workflow is often two steps - first coerce the column to numbers so the missing and the malformed both show up as NaN, then decide what those gaps mean. Doing it in that order means a stray TBC never silently poisons a total.

NaN = a decision, not just a gap. Drop it, fill it, or estimate it - but choose on purpose, and write it down.

New columns - compute what you actually need

Most useful numbers are not in the file - they are one step away from it. A BOQ has quantity and rate but you need cost; a room list has length and width but you need area. Because pandas works on whole columns, you build a new column by writing the arithmetic once, over the entire Series:

python
df["cost"] = df["qty"] * df["rate"]
df["area_sqm"] = df["length_m"] * df["width_m"]
df["cost_per_sqm"] = df["cost"] / df["area_sqm"]

Each line multiplies or divides two columns element-by-element and stores the result as a brand-new column on the left. No loop, no dragging a formula down - one statement covers every row. When the logic is conditional rather than arithmetic, a common tool is numpy.where, which chooses per row:

python
import numpy as np
df["size_class"] = np.where(df["area_sqm"] > 15, "large", "small")

That reads almost like English: where the area is over 15, label it large, otherwise small. You can also standardise text while you are here - df["room"] = df["room"].str.lower().str.strip() collapses Kitchen, kitchen and kitchen into one consistent label, which matters enormously the moment you group. Computed columns are where a raw export becomes an analysis: you are no longer just holding the data someone gave you, you are deriving the quantities your deliverable is actually about.

Two more everyday transforms round this out. Renaming columns with df.rename(columns={"Area (SqM)": "area_sqm"}) gives you clean, code-friendly names to work with instead of whatever the source file happened to use, which matters because you will type these names constantly. And dropping duplicate rows with df.drop_duplicates() is the quiet fix for a table where the same line item was pasted in twice - a common and dangerous error in a BOQ, because a duplicated row inflates a total silently. None of these moves are glamorous, but together - rename, coerce types, fill or drop gaps, compute, de-duplicate - they are the difference between a number you can put your name to and one you are quietly hoping is right.

GROUPBY: SPLIT - APPLY - COMBINE1. one tableG living 241 study 12G kitchen 131 bed 15G hall 9by floor2. split into groupsfloor Gliving 24kitchen 13hall 9floor 1study 12bed 15.sum()3. apply + combinefloorarea_sqmG46127df.groupby("floor")["area_sqm"].sum() - hundreds of rooms collapse to one total per floor.
Zoom
The split-apply-combine idea behind groupby. Split the table into groups by a key column (here, floor), apply a calculation to each group (sum the area), and combine the results into one small summary table. Hundreds of line items become the handful of totals a deliverable reports.

groupby - the move that makes totals

Here is the technique that earns its keep. A schedule has hundreds of line items, but a deliverable reports summaries: total area per floor, cost per category, count of items per supplier. `groupby` does exactly this - it splits the table into groups, applies a calculation to each, and combines the results back into a small table. The pattern is called split-apply-combine:

python
by_floor = df.groupby("floor")["area_sqm"].sum()
cost_by_cat = df.groupby("category")["cost"].sum().sort_values(ascending=False)
counts = df.groupby("supplier").size()

Read the first line as: group the rows by their floor value, take the area_sqm column, and sum it within each group. Out comes a tiny Series - one total per floor - from a table of hundreds of rooms. Swap sum for mean, max, count or size and you get the average area, the largest, the number of items. You can group by several columns at once (groupby(["floor", "category"])) and aggregate several columns together with .agg. This one method replaces the pivot table you would build by hand in a spreadsheet, and unlike the pivot table it re-runs itself the instant the source data changes. If you take one transforming move from this lesson into your work, make it groupby - it is the bridge from raw line items to the numbers a client actually reads.

One thing that trips up newcomers is what groupby hands back. Grouping by a single key and aggregating one column gives you a Series indexed by the group - the floors or trades become the index labels. That is often exactly what you want, but if you need it as a normal table again - to write it to Excel or chart it - .reset_index() turns the group labels back into an ordinary column, and .to_frame() turns a Series into a one-column DataFrame. When you want several summaries at once, .agg takes a dictionary: df.groupby("trade").agg({"cost": "sum", "qty": "sum", "item": "count"}) gives total cost, total quantity and a line count per trade in a single, readable call - the whole summary block of a BOQ in one statement.

GROUPBY: SPLIT - APPLY - COMBINE1. one tableG living 241 study 12G kitchen 131 bed 15G hall 9by floor2. split into groupsfloor Gliving 24kitchen 13hall 9floor 1study 12bed 15.sum()3. apply + combinefloorarea_sqmG46127df.groupby("floor")["area_sqm"].sum() - hundreds of rooms collapse to one total per floor.
Zoom
The split-apply-combine idea behind groupby. Split the table into groups by a key column (here, floor), apply a calculation to each group (sum the area), and combine the results into one small summary table. Hundreds of line items become the handful of totals a deliverable reports.

Chaining it into a pipeline

In practice these moves are not separate - you chain them into a short pipeline that takes the raw file in at the top and hands clean, summarised data out the bottom. Written plainly, one step per line, it stays readable:

python
import pandas as pd

df = pd.read_csv("boq_raw.csv")
df["item"] = df["item"].str.lower().str.strip()   # standardise labels
df = df.dropna(subset=["qty", "rate"])            # drop incomplete lines
df["cost"] = df["qty"] * df["rate"]               # compute cost
totals = df.groupby("trade")["cost"].sum()        # summarise by trade
print(totals)

Five lines take a messy export to a cost-by-trade summary. The order matters - you standardise labels before grouping so RCC and rcc do not become two trades, and you compute cost after dropping the rows that cannot have one. Because it is written down, this is not a one-time clean-up; it is a recipe. When the quantities change next week you run the same five lines and get the updated totals, with every cleaning decision applied identically. That reproducibility is the real prize of doing this in code rather than by hand: not that any single clean is faster, but that you never have to remember how you did it, because the script is the memory.

CLEANING PIPELINERAW (messy)Kitchen 12 45kitchen __ 45HALL 8 TBChall 8 30Study __ 60blanks, case, textstr.lower().str.strip()dropna(subset=[qty])cost = qty * rategroupby(item).sum()CLEAN (usable)item total_costhall 240kitchen 540study ...one row per itemOrder matters: standardise labels before grouping; compute cost after dropping incomplete rows.Written as steps, the whole clean-up becomes a recipe you re-run when the data changes.
Zoom
A cleaning pipeline as before and after. A raw export arrives with blank cells, mixed-case labels and a quantity typed as text; a short chain of pandas steps - standardise, drop or fill gaps, compute, group - hands out a clean, summarised table you can trust and total.

Read -> standardise -> drop/fill -> compute -> group. One readable step per line = a re-runnable recipe.

Moves & functions you met in this lesson

Boolean filtering with & and |

Keep rows matching combined conditions - and with &, or with |, each in parentheses

The parentheses are mandatory in pandas; forgetting them raises an error rather than guessing your intent.

dropna / fillna

Remove rows with missing values, or fill the gaps with a chosen default

Which is right depends on the question. Filling a missing cost with zero is safe as a placeholder but dangerous in a total.

Computed column

Build a new column from existing ones - df[cost] = df[qty] * df[rate]

Whole-column arithmetic in one line, applied to every row at once. No loop, no dragging a formula.

groupby (split-apply-combine)

Split rows into groups, aggregate each, combine into a summary

The pivot-table replacement. sum, mean, count, size and .agg cover most schedule and BOQ summaries.

Hands-on workshop

Workshop - turn a messy BOQ into trade totals

You will take a deliberately messy quantities file and run it through a small cleaning pipeline, ending with a cost-by-trade summary you could paste into a report - the everyday shape of design data work.

Python 3 with pandas and numpy installed (pip install pandas numpy), plus any spreadsheet to build the messy CSV.

Given & goal
Goal: clean a messy table and summarise it with groupby
Inputs: a boq.csv with columns trade, item, qty, rate - include some blanks, mixed-case trades, and one qty left empty
Time: ~40 minutes
  1. 1Make boq.csv with about ten rows across three trades (e.g. rcc, RCC, masonry, Masonry, plaster), leaving one qty blank and one rate blank on purpose.
  2. 2Load it and audit it: read it with pd.read_csv, then run df.info() and df.isna().sum() to see the types and the gaps before touching anything.
  3. 3Standardise the trade labels with df["trade"] = df["trade"].str.lower().str.strip() so RCC and rcc become one trade.
  4. 4Handle the gaps: drop rows that cannot have a cost with df = df.dropna(subset=["qty", "rate"]), and note in a comment why you dropped rather than filled.
  5. 5Add the computed column df["cost"] = df["qty"] * df["rate"], then build the summary totals = df.groupby("trade")["cost"].sum().sort_values(ascending=False).
  6. 6Print totals, then change one rate in the source file and re-run the whole script to watch the summary update itself.

You’ll walk away with
A script that reads a messy boq.csv and prints a sorted cost-by-trade summary, with a comment at each step saying what it cleaned and why.

The worked example

Three altitudes on the same idea

Read the band that fits you — or all three.

For the architectAutomate busywork & build custom tools

Area statements and code checks are cleaning problems in disguise. Before you can total gross floor area by use or flag rooms below a light-and-ventilation minimum, the export needs its blanks resolved, its room names standardised and its areas computed from length times width. groupby then gives you area-by-floor and area-by-use in two lines - the tables a planning submission wants - and the script re-runs cleanly every time the model changes.

For the interior designerScripts for data, schedules & layouts

Your specification and quantity work lives and dies on consistent labels. When a supplier column mixes Ltd, ltd and limited, or a cost is left blank pending a quote, your totals are quietly wrong until you fix it. str.lower(), fillna and a computed cost = qty * rate column clean that up honestly, and groupby by supplier or by room turns a 300-line FF&E workbook into the per-room and per-supplier subtotals your client actually reviews.

For the studentA hireable computational skill

Data cleaning is the skill every job posting assumes and no course quite teaches - so learn it deliberately. Real datasets in a portfolio, a thesis or a studio project arrive messy, and the person who can standardise, fill and groupby them looks instantly capable. These are the exact moves the GIS and data-analysis courses in this Academy build on, so getting dropna, fillna, computed columns and split-apply-combine into your hands now pays off across everything that follows.

Misconception check

If the file looks fine when I open it, it is clean and I can start calculating.

Looking fine and being clean are different things, and the difference is exactly what burns people. A column can look like numbers but be stored as text because one cell said TBC, so your sums silently fail or refuse to run. Two rooms named Kitchen and kitchen look identical but count as separate groups. A blank cell reads as empty to your eye but as NaN to pandas, which may quietly drop it or poison an average. This is why info() and isna().sum() come before any calculation - they surface the problems your eye skims over. Clean data is not data that looks tidy; it is data whose types are right, whose labels are consistent, and whose gaps you have decided about on purpose.
Try it

Do it yourself

Think about the order of the moves and what each one decides.

  1. 1Why must each condition in a combined boolean filter be wrapped in its own parentheses?
  2. 2Give one situation where dropping missing rows is right, and one where filling them is better.
  3. 3Write the line that adds a cost column equal to qty times rate.
  4. 4In your own words, what are the three steps of split-apply-combine?
  5. 5Why should you standardise text labels before you group by them, not after?
Take this with you

The one line to carry out

Cleaning is the craft between loading and using data: filter and sort to see it, decide about missing values on purpose, compute the columns you actually need, and use groupby to collapse line items into totals. Chain those steps into a written pipeline and the clean-up becomes a recipe you re-run for free.
Take it further
References & further reading

Peer-reviewed journals & authoritative standards

  1. 01Data cleansingWikipedia, 2026.
  2. 02pandas - Python Data Analysis Librarypandas.pydata.org, 2026.
  3. 03pandas (software)Wikipedia, 2026.
  4. 04NumPynumpy.org, 2026.
Related lessons
Recap
Real files are messy, and the value is in the transform. You narrow and order with boolean filters and sort_values, and you audit gaps with isna().sum() before deciding to dropna or fillna - a choice, not a nuisance. Computed columns derive the quantities your deliverable is really about, and groupby's split-apply-combine turns hundreds of rows into the handful of totals a client reads. Chained into a step-per-line pipeline, the whole clean-up is reproducible - a recipe that re-runs identically when the data changes.
Carry forward →

You can now clean and summarise any table. Next we point that skill straight at the paperwork of practice - reading a real room schedule or BOQ, computing areas, quantities and costs, and writing the result back out to an Excel or CSV file people can open.

A

The author

Amogh N P

Architect, interior designer, and creative polymath. Studio Matrx began in his notebooks — his vision of design made honest, useful, and open to everyone. Its Academy is written and taught in his memory, and free, forever.

More about Amogh →