Loading Data That Fights Back
Encodings, dtypes, sentinel values and the missing-value list you did not know about
Week 1 read a CSV in one line and it worked. That is the exception. Real extracts arrive with the wrong encoding, dates as text, numbers with currency symbols, and a sentinel value someone chose in 1998 to mean “unknown”. Loading is where most silent errors enter.
pandas has opinions about what missing means
You met this in week 1 without knowing it. read_csv converts a list of strings to NaN automatically, and that list is longer than you would guess.
from pandas.io.parsers.readers import STR_NA_VALUES
print(sorted(STR_NA_VALUES))
'None' is on that list
A category legitimately called None: a customer with no internet service, a product with no discount, is read as a missing value. The column silently gains hundreds of gaps that were never in the file. This is a real defect that appeared while building this course's dataset, and it is why the category is spelled No internet.
Two arguments control it. keep_default_na=False switches the whole list off; na_values= adds your own sentinels.
import io as _io
raw = 'service,code\nNone,1\nDSL,-999\nFibre,2\n'
default = pd.read_csv(_io.StringIO(raw))
print('default:')
print(default)
explicit = pd.read_csv(_io.StringIO(raw), keep_default_na=False,
na_values=['-999'])
print('\nkeeping None as text, treating -999 as missing:')
print(explicit)
service code
0 NaN 1
1 DSL -999
2 Fibre 2
keeping None as text, treating -999 as missing:
service code
0 None 1.0
1 DSL NaN
2 Fibre 2.0
Declare the types you expect
Letting pandas guess the dtype of every column is convenient and occasionally wrong. An identifier of digits becomes an integer and loses its leading zeros; a postcode becomes a float. Declare what matters.
import io as _io
raw = 'account,postcode\n007831,01234\n004120,00987\n'
print('guessed:')
print(pd.read_csv(_io.StringIO(raw)))
print('\ndeclared:')
print(pd.read_csv(_io.StringIO(raw), dtype={'account': str, 'postcode': str}))
account postcode
0 7831 1234
1 4120 987
declared:
account postcode
0 007831 01234
1 004120 00987
The leading zeros are gone in the first version and there is no way to get them back afterwards. Loss at load time is unrecoverable, which is why loading deserves more care than it usually gets.
Parse dates at load time
df = pd.read_csv('customers.csv', parse_dates=['signup_date'])
print(df['signup_date'].dtype)
print(df['signup_date'].min(), 'to', df['signup_date'].max())
2019-01-01 00:00:00 to 2024-12-01 00:00:00
When the format is ambiguous
03/04/2024 is the third of April in Britain and the fourth of March in America, and pandas will pick one. If your source is not ISO format, pass the format explicitly: pd.to_datetime(col, format='%d/%m/%Y'). Guessing wrong produces dates that are valid, plausible and wrong for eleven months of the year.
Memory, and when it starts to matter
df = pd.read_csv('customers.csv')
before = df.memory_usage(deep=True).sum()
for col in ['contract', 'internet_service', 'payment_method']:
df[col] = df[col].astype('category')
after = df.memory_usage(deep=True).sum()
print('before %6.1f KB' % (before / 1024))
print('after %6.1f KB' % (after / 1024))
print('saved %5.1f%%' % (100 * (1 - after / before)))
after 782.4 KB
saved 40.8%
A category column stores each distinct value once and an integer code per row. With three distinct values across three thousand rows the saving is large, and it grows with the row count. On a dataset that fits comfortably in memory this is housekeeping; on one that does not, it is the difference between working and not.
Day 1 takeaway
Loading is not a formality. pandas silently converts a list of strings to missing values, guesses dtypes in ways that destroy leading zeros, and picks a date format when yours is ambiguous. Declaredtype, na_values and parse_dates deliberately, and convert low-cardinality text to category.