Chapter 2 · DataFrames
Section 2.2 · Chapter 2 · Learning Statistics with Python
DataFrames
Prof. Xuhu Wan
ISOM, HKUST Business School · 2026 Edition
Real data arrive with holes and repeats. You will audit the notebook’s original grid file (one column is a fifth empty, another 99.99 %), decide between filling and dropping — surgically — discover why one November hour appears twice every year, and then carve out exactly the rows and columns a model needs, predicting each row count before you run.
AEP_hourly.csv is the notebook’s energy(Clean=False): 178 258 hours, 12 utilities, lots of NaN. We keep four utilities and 2011–2012.
What does load.isna().sum().sum() / load.size measure?
Four different diseases in one frame: AEP and COMED each missing one hour, FE missing 3 624 (the first five months of 2011), NI missing all but one of 17 540. df.isna().sum() is the first line you run on any new dataset.
Zoom in on AEP’s single hole (04:00 on 6 December 2012). Four options — one of them is a crime in time series. Decide which before running.
Which fill is look-ahead bias when the rows are ordered in time?
fillna(0)ffill() — carry the last value forwardbfill() — pull the next value backwardfillna(median)dropna() removes any row with any NaN. With NI 99.99 % empty, predict how many rows survive.
How many rows does load.dropna() keep?
Rule of thumb from the lecture
When not too many values are missing: fillna(median) for numerical columns, fillna(mode) for categorical ones. Filling five months of FE with one number keeps the rows but shrinks the spread — a fifth of the column now sits exactly at the centre. When most of a column is missing (NI), drop the column: a filled column is a fiction.
Build clean from load in two moves: drop the hopeless NI column (drop(columns=…)), then drop the rows that still have a NaN. Result: no NaN, three columns, more than 13 000 rows.
The notebook asks you to fill COMED with its mean; here COMED has one hole and FE has 3 624, so fill FE.
Fill the NaN in load["FE"] with the column mean, into fe_mean. It must contain no NaN and its mean must equal the pre-fill mean (why is that automatic?).
Switch to the smaller pjm_hourly.csv (the notebook’s cleaned new.csv, 2014–2017). It has no NaN — but some timestamps appear twice. Predict how many, then look at which hours they are.
Every one is 02:00 on the first Sunday of November — the hour that repeats when US clocks fall back from daylight-saving time. Two different loads, one label. Nothing is “wrong” with the file; the calendar did it.
drop_duplicates(subset=…) decides which copy survives. Predict the row counts.
hours has 4 duplicated timestamps. How many rows does keep=False remove?
Using the boolean mask form hours[~hours.index.duplicated(keep=…)], remove both copies of every duplicated hour, into nodup. Check: 8 rows fewer than hours.
A mask built from one column selects rows of the entire frame, all columns intact. Two conditions need & / | and parentheses.
What happens with hours[hours.AEP > 15000 & hours.AEP < 16000]?
& binds before >; you need parentheses.query() writes the filter as a string: and / or instead of & / |, and @name splices in a Python variable. Predict whether it matches the mask.
hours.query(“AEP > 15000 and AEP < 16000”) returns…
SyntaxError — you cannot use and in pandasbandisin tests each value against a list. Combine with day_name() to split weekends from weekdays.
~ negates a boolean mask. The weekday/weekend gap (about 1 400 MW, 15 240 vs 13 795) is the industrial load that switches off on Saturday — a fact you will exploit with groupby in §2.3.
A DataFrame is a dict of Series sharing one index. Pull one column out and ask what it is.
What does hours[“AEP”] return?
Series.loc label slices include the end label; .iloc position slices stop before the end position. What two numbers print?
3 3
loc[mask, columns] does both selections in one call.
Select the last two columns of hours, keeping only the rows where the last column (DEOK) is above its own mean, into sel.
For time series you never shuffle: the first 80 % of hours train, the last 20 % test.
Split hours by position: train = first 80 % of rows, test = the remaining rows, in time order.
isna().sum() per column is the first line on any dataset: in 2011–2012 the real file had 1 / 1 / 3 624 / 17 539 holes in four columns.ffill looks back, bfill looks forward — a leak in any time-ordered frame. fillna(median) keeps rows but shrinks the spread; dropna() kept no rows because of NI — use subset=, thresh=, or drop the column.keep="first"/"last" drops one copy, keep=False drops both.query("… and …") reads naturally and @var injects variables; isin handles membership.[] gives a Series, [[]] a DataFrame; loc slices are right-inclusive, iloc right-exclusive; loc[mask, cols] filters and selects at once.Next: §2.3 — split the frame into groups, join two frames on a key, reshape long ↔︎ wide, and glue frames together.
Prof. Xuhu Wan · HKUST ISOM · Learning Statistics with Python