Lesson 4.3Lesson 4.3 · Working with Design Data
Schedules, BOQs and Spreadsheets
The paperwork of practice, scripted - read a room schedule or bill of quantities, compute the areas, quantities and costs, and write the finished sheet back to Excel
The schedule, the BOQ, the FF&E sheet - the documents that eat your evenings are exactly the ones a script produces best.
Every project runs on spreadsheets that someone maintains by hand: a room schedule with areas, a bill of quantities with rates and costs, a finishes list with subtotals. They are recomputed and re-issued every revision, and the recomputing is where the errors and the late nights live.
This lesson closes the loop. You read the real file in, use everything from the last two lessons to compute and summarise it, and then - the new move - write the finished result back out to Excel or CSV, formatted, with subtotals, ready for a colleague to open. When the quantities change next week, you do not redo the sheet; you re-run the script. This is the moment pandas stops being a tutorial and becomes a tool that produces your actual deliverables.
read_excel (verify!) -> compute + groupby -> to_excel(index=False). ExcelWriter for many tabs. The script IS the schedule.
Reading the real thing - a room schedule
Start from a file that already exists: a room schedule someone keeps in Excel, with a room name, a length and a width per row. Reading it is one line, but a real workbook has quirks - the data is on a named sheet, maybe with a title row above the headers - so read_excel gives you arguments to cope:
import pandas as pd
rooms = pd.read_excel(
"project.xlsx",
sheet_name="Rooms", # the sheet, by name
header=0, # which row holds the column names
)
print(rooms.head())
print(rooms.info())The head() and info() reflex matters even more here than with a clean CSV, because Excel is where mixed types hide - a length column with one cell reading approx 3.5 comes in as text, and every area you compute from it will fail silently. info() shows you that length_m is object (text) rather than float64 (number) before you trust it. If a column that should be numeric is not, pd.to_numeric(rooms["length_m"], errors="coerce") converts what it can and marks the rest as missing, so you can find and fix the offending cells. Reading is easy; reading and verifying is the professional habit, because a schedule built on a mistyped column is worse than no schedule at all - it looks authoritative and is wrong.
Real workbooks bring a few other quirks worth naming so they do not surprise you. Merged cells - common in hand-built schedules where someone merged the header across two columns - come into pandas as a value in the top-left cell and NaN in the rest, so a merged title row usually needs a skiprows or a manual clean. Trailing empty rows and a totals row at the bottom will be read as data, so it is common to slice them off or filter them out after loading. And the column names themselves often arrive with trailing spaces or inconsistent capitalisation - Area (SqM) versus area_sqm - which is why renaming to clean, predictable names right after the read pays for itself across the rest of the script. None of this is hard; it is just the reality of files made by people, and a minute spent tidying the structure up front saves an hour of confusion later.
Computing the numbers a schedule reports
With clean columns in hand, the calculations are the whole-column arithmetic from the last lesson, now doing real work. A room schedule wants area per room and a running total; a BOQ wants cost per line and a grand total:
rooms["area_sqm"] = rooms["length_m"] * rooms["width_m"]
rooms["area_sqm"] = rooms["area_sqm"].round(2)
total_area = rooms["area_sqm"].sum()
print(f"Total area: {total_area:.2f} sqm")Each line operates on the entire column, so it does not matter whether the schedule has eight rooms or eight hundred. round(2) keeps the areas to a sensible two decimals - a small courtesy that stops a schedule showing 23.999999 square metres. For a bill of quantities the same shape gives you cost, and groupby gives you the subtotals a BOQ is organised around:
boq = pd.read_excel("project.xlsx", sheet_name="BOQ")
boq["amount"] = (boq["qty"] * boq["rate"]).round(2)
by_trade = boq.groupby("trade")["amount"].sum().round(2)
grand_total = boq["amount"].sum().round(2)That is a complete quantity roll-up: a per-line amount, a subtotal per trade, and a grand total - the three numbers every bill of quantities exists to state. The point is not that any one calculation is hard; it is that all of them are now written down, so the schedule and the BOQ are computed the same way every time, and a rate change propagates through every dependent number the instant you re-run.
This is also where a designer's judgement stays firmly in charge. The script computes carpet area, but which walls and voids to deduct, whether balconies count at half, how to treat a double-height space - those are decisions you encode, and encoding them in code makes them explicit and reviewable rather than buried in a spreadsheet formula nobody remembers writing. A useful habit is to keep the raw source columns and add your computed ones alongside, never overwriting the inputs, so anyone can trace a final number back to where it came from. When a QS or an authority queries a figure, you can point at the exact line of code that produced it - a level of traceability a hand-built spreadsheet rarely offers, and one that quietly builds trust in your numbers.
Writing it back - the sheet your team opens
A calculation nobody can open is not a deliverable. The move that makes this lesson practical is writing your finished DataFrame back out to a file colleagues use. pandas does it with one method each way:
rooms.to_csv("room_schedule_out.csv", index=False)
rooms.to_excel("room_schedule_out.xlsx", sheet_name="Rooms", index=False)index=False is the argument to remember: without it, pandas writes its 0,1,2 row index as an extra unlabelled column that confuses everyone who opens the sheet. To write several tables into one workbook - the schedule on one tab, the trade subtotals on another - use an ExcelWriter:
with pd.ExcelWriter("deliverable.xlsx") as writer:
rooms.to_excel(writer, sheet_name="Rooms", index=False)
by_trade.to_frame("amount").to_excel(writer, sheet_name="By trade")
boq.to_excel(writer, sheet_name="BOQ", index=False)The with block opens the file, writes each tab, and closes it cleanly. Underneath, pandas uses openpyxl to speak Excel's format - the helper you installed in lesson 4.1 - and for finer control (bold headers, column widths, number formats) you can reach into openpyxl directly on the same workbook. But for most deliverables, to_excel with sensible sheet names and index=False produces a clean, openable file. The full arc - read the source, compute, summarise, write the result - is now a single script, and that script is your schedule.
A couple of practical cautions make writing files safe. First, to_excel overwrites the target file without asking, so never write back onto the source workbook you read from - always write to a new, clearly-named output file so a bad run cannot destroy your input. Second, if the output file is already open in Excel when your script runs, the write will fail with a permission error; close it first. And when you want the styled, branded look a practice expects, the cleanest pattern is to keep a formatted template workbook - headers, fonts, column widths and a logo already set - and have the script fill only the data cells, leaving the presentation untouched. That separation keeps the calculation logic in pandas and the visual polish in a file a non-coder on your team can maintain.
index=False or you ship a stray 0,1,2 column. ExcelWriter puts many tables in one workbook.
The end-to-end script - your deliverable as code
Put the three moves together and you have a small program that turns a source file into a finished, multi-tab workbook. Written one clear step at a time, it reads like the process it automates:
import pandas as pd
# 1. read
rooms = pd.read_excel("project.xlsx", sheet_name="Rooms")
rooms["length_m"] = pd.to_numeric(rooms["length_m"], errors="coerce")
rooms = rooms.dropna(subset=["length_m", "width_m"])
# 2. compute
rooms["area_sqm"] = (rooms["length_m"] * rooms["width_m"]).round(2)
by_floor = rooms.groupby("floor")["area_sqm"].sum().round(2)
# 3. write
with pd.ExcelWriter("schedule_out.xlsx") as writer:
rooms.to_excel(writer, sheet_name="Rooms", index=False)
by_floor.to_frame("area_sqm").to_excel(writer, sheet_name="By floor")
print(f"Wrote {len(rooms)} rooms, total {rooms['area_sqm'].sum():.2f} sqm")Run it and a clean workbook appears with a Rooms tab and a By-floor summary. Nothing here is new - it is read, clean, compute, group and write, the moves of this whole module assembled into one useful thing. What changed is the stance: you are no longer poking at data to learn, you are producing a document. The honest caveat is that not every sheet is worth scripting - a one-off, ten-row table is faster to type in Excel, and you should. The script earns its place when the sheet is large, recomputed often, or error-prone by hand. When it does, this pattern turns an evening of careful, fragile spreadsheet work into a command you run in a second, the same correct way every time.
There is a compounding benefit that only shows up over a project's life. The first time you script a schedule it may take longer than doing it by hand - you are debugging column names and chasing a mistyped cell. But a building project revises its schedules and quantities many times, and every revision after the first is nearly free: change the source, re-run, re-issue. Across a project that might mean twenty regenerations, the script wins many times over, and it wins on correctness as much as speed, because the twentieth revision is computed by exactly the same logic as the first, with no fresh chance for a dragged formula to break. The setup cost is paid once; the correctness is banked on every revision that follows, which is precisely the kind of steady, unglamorous return that makes scripting worth learning for real project work rather than for its own sake. That is the real argument for scripting your paperwork: not that it is clever, but that it makes a repeated, error-prone job both faster and more trustworthy every time it comes round again.
pd.read_excel arguments
sheet_name, header, skiprows - cope with real workbooks that are not one clean sheet
Always follow with head() and info(). Excel is where numbers hide as text; verify types before you compute.
pd.to_numeric(errors=coerce)
Convert a text column to numbers, marking un-convertible cells as missing
The honest fix for a length or cost column that came in as text because of one stray value.
to_csv / to_excel (index=False)
Write a DataFrame back out to a file colleagues can open
index=False stops pandas writing its 0,1,2 row index as a mystery extra column.
pd.ExcelWriter + openpyxl
Write several tables into one multi-tab workbook; the engine that speaks .xlsx
Use a with-block so the file is closed cleanly. Reach into openpyxl for bold headers and number formats.
Workshop - script a room schedule end to end
You will build the full read-compute-write arc: take an Excel room list, compute areas and a per-floor summary, and write a clean two-tab workbook - a deliverable produced entirely by script.
Python 3 with pandas and openpyxl installed (pip install pandas openpyxl), and Excel or a spreadsheet app to make and check the workbook.
Goal: read an Excel schedule, compute areas and subtotals, write a finished workbook Inputs: a project.xlsx with a Rooms sheet holding room, floor, length_m, width_m Time: ~45 minutes
- 1Make project.xlsx with a Rooms sheet of about eight rooms across two floors, with lengthm and widthm columns - deliberately type one length as text like 3.5m to create a mixed-type column.
- 2Read it with pd.readexcel("project.xlsx", sheetname="Rooms") and run info() - notice length_m is object type, not float, because of the text cell.
- 3Fix it with rooms["lengthm"] = pd.tonumeric(rooms["length_m"], errors="coerce"), then drop the now-missing row with dropna, and confirm the type is float64.
- 4Compute rooms["areasqm"] = (rooms["lengthm"] * rooms["widthm"]).round(2) and a summary byfloor = rooms.groupby("floor")["area_sqm"].sum().round(2).
- 5Write both to one workbook with an ExcelWriter block: the rooms table to a Rooms tab with index=False, and byfloor.toframe("area_sqm") to a By-floor tab.
- 6Open the output in Excel to confirm it is clean, then change a width in the source and re-run to watch the deliverable regenerate itself.
You’ll walk away with
A script that reads project.xlsx, verifies and cleans the numeric columns, computes areas and a per-floor total, and writes a two-tab schedule_out.xlsx that opens cleanly in Excel.
Three altitudes on the same idea
Read the band that fits you — or all three.
The area statement and the tender BOQ are your highest-value scripting targets. Read the model export, compute gross and carpet areas, group by floor and by use for the statutory statement, and write a clean workbook the authority and the QS can both open. Because the logic is written down, a late design change does not mean re-checking a spreadsheet by hand at midnight - it means re-running one command and re-issuing, with every subtotal guaranteed consistent.
Your FF&E budget is a read-compute-write loop waiting to be scripted. Pull the specification workbook in, compute line amounts from quantity and unit rate, group by room and by category for the subtotals a client reviews, and write it back to a tidy multi-tab Excel file with index=False so it opens clean. When a supplier revises rates, you change the source and re-run rather than re-totalling three hundred lines and hoping you caught them all.
Being able to say you scripted a real BOQ or area statement is a genuinely strong portfolio line. Estimation, quantity surveying and BIM-data roles all value someone who can read a messy project spreadsheet, compute the numbers reliably and write a clean output. Practise on any real sheet you can find - a studio cost plan, a materials list - because the read, compute with groupby, write-to-Excel arc here is exactly the workflow those jobs run on every day.
“Writing a schedule with code will lose all my Excel formatting, so it is not worth it for a real deliverable.”
Do it yourself
Trace the read-compute-write arc in your head before you run it.
- 1Which argument to to_excel stops pandas writing its row index as an extra column, and why does it matter?
- 2A cost column came in as text. Which function converts it to numbers and marks the bad cells as missing?
- 3What does an ExcelWriter with-block let you do that a single to_excel call does not?
- 4Name the three stages of the end-to-end script and one thing that happens in each.
- 5Give one case where scripting a schedule is not worth it, and say why.
The one line to carry out
Peer-reviewed journals & authoritative standards
- 01pandas - Python Data Analysis Library — pandas.pydata.org, 2026.
- 02Comma-separated values — Wikipedia, 2026.
- 03pandas (software) — Wikipedia, 2026.
- 04The Python Standard Library — docs.python.org, 2026.
You can now produce the numbers and the sheets. The last step of the module is to make those numbers legible at a glance - turning areas, costs and quantities into bar, line and pie charts with matplotlib, and saving them as images for a report or a board.
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 →