Chapter 4: Tables

Read, inspect, select and clean a table.

Before a model sees a single , somebody reads a table: a CSV of training runs, an export of labels, a log. Chapter 1 read one by hand and got strings back. pandas reads it in one call into a : named columns, each a with its own , all sharing one that labels the rows.

reads a CSV, and reads Parquet, a binary format that stores each column’s . Then look before you change anything. shows the first rows, each column’s and how many values it really has, and a summary of the numbers.

import pandas as pd
df = pd.read_csv("data/runs.csv", index_col="run")
df.head(3)
Output:
    model    lr  epochs  val_loss device
run                                     
r1    cnn  0.10    10.0      0.42    gpu
r2    cnn  0.01    10.0      0.35    gpu
r3    mlp  0.10     NaN      0.61    cpu
df.info()
Output:
<class 'pandas.DataFrame'>
Index: 8 entries, r1 to r8
Data columns (total 5 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   model     8 non-null      str    
 1   lr        8 non-null      float64
 2   epochs    6 non-null      float64
 3   val_loss  7 non-null      float64
 4   device    7 non-null      str    
dtypes: float64(3), str(2)
memory usage: 446.0 bytes
df.describe()
Output:
             lr     epochs  val_loss
count  8.000000   6.000000  7.000000
mean   0.053750  14.166667  0.455714
std    0.041726   6.645801  0.155334
min    0.010000   5.000000  0.290000
25%    0.010000  10.000000  0.340000
50%    0.050000  15.000000  0.420000
75%    0.100000  20.000000  0.545000
max    0.100000  20.000000  0.710000

Here is that file of eight runs, cleaned a line at a time. takes a name in brackets; picks rows and columns by label and by position; a keeps the rows where a test is True. Then , and repair the gaps and the types, and adds a column. counts, and turns the result into an for a model.

Table Example — read_csv Step 1 of 13
import pandas as pddf = pd.read_csv('data/runs.csv', index_col='run')df.dtypesdf.isna()df['val_loss']df.loc['r3':'r5', ['model', 'epochs']]df.iloc[[0, -1], :2]df[df['val_loss'] < 0.4]df = df.dropna(subset=['val_loss'])df = df.fillna({'epochs': 10, 'device': 'cpu'})df = df.astype({'epochs': 'int64'})df = df.assign(steps=df['epochs'] * 250)df['model'].value_counts()X = df[['lr', 'epochs']].to_numpy()

df 8 rows × 5 columns

runmodellrepochsval_lossdevice
r1cnn0.110.00.42gpu
r2cnn0.0110.00.35gpu
r3mlp0.1NaN0.61cpu
r4mlp0.0520.0NaNcpu
r5cnn0.0520.00.33gpu
r6rnn0.15.00.71NaN
r7mlp0.0120.00.48cpu
r8cnn0.01NaN0.29gpu

Eight runs. The run column became the index, and every blank field came in as NaN.

Two of those lines are easy to mix up. reads labels, and a label includes its end, so 'r3':'r5' is three rows. reads positions and leaves the end out, as NumPy does. After the has a gap where r4 was, so position 3 now holds r5.

This is where people slip. A reads as , and comparing with <, >= or even == gives False. So df[df['val_loss'] < 0.4] and its opposite, >= 0.4, both leave r4 out, and nothing warns you. Count the gaps with before you filter, and check that the rows you kept and the rows you dropped add up.

import pandas as pd
df = pd.read_csv("data/runs.csv", index_col="run")
len(df)
Output:
8
len(df[df["val_loss"] < 0.4]), len(df[df["val_loss"] >= 0.4])
Output:
(3, 4)
df["val_loss"].isna().sum()
Output:
np.int64(1)
nan = float("nan")
nan < 0.4, nan >= 0.4, nan == nan
Output:
(False, False, False)

pandas 3 made the rule: anything you take from a behaves as a separate . That is the opposite of NumPy, where a is a of the original. Changing a you selected never changes the table, and neither does a like df['epochs']['r3'] = 10, which pandas warns about. To change a table, change the table itself, in one step.

import warnings
import pandas as pd
df = pd.read_csv("data/runs.csv", index_col="run")
losses = df["val_loss"]
losses.iloc[0] = 0.0
losses.iloc[0], df.loc["r1", "val_loss"]
Output:
(np.float64(0.0), np.float64(0.42))
with warnings.catch_warnings(record=True) as caught:
    df["epochs"]["r3"] = 10
[w.category.__name__ for w in caught]
Output:
['ChainedAssignmentError']
df.loc["r3", "epochs"]
Output:
np.float64(nan)
df.loc["r3", "epochs"] = 10
df.loc["r3", "epochs"]
Output:
np.float64(10.0)

A CSV stores only text, so every read guesses the again, and a float32 column comes back float64. gives back the that to_parquet wrote.

import pandas as pd
df = pd.read_csv("data/runs.csv", index_col="run")
small = df.astype({"lr": "float32"})
small.to_csv("runs.csv")
small.to_parquet("runs.parquet")
pd.read_csv("runs.csv", index_col="run")["lr"].dtype
Output:
dtype('float64')
pd.read_parquet("runs.parquet")["lr"].dtype
Output:
dtype('float32')

The habit this chapter adds is to look after every step. Check the with , count the gaps with , and compare the number of rows before and after each filter. A table that lost a row without a word trains a model that is wrong without anyone noticing.

Homework

The packet has three parts: five one-line exercises with , , , and ; cleaning a messy CSV into the table the test expects; and two bugs to fix, a filter that loses the runs with no accuracy and a that changes nothing.

Chapter 4 workbook

Unzip, follow the README once to set up, then run pytest until it's green.