Open a Kobo export without losing data

Lesson 3 of 12 · about 25 minutes · Foundations

Download the dataset No email required. Use it for anything.

The problem

You have an export from Kobo. You open it, the shape looks about right, and you start working. Three hours later a total is wrong and you trace it back to the first line of code you wrote.

Reading the file is not a formality. It is where most cleaning errors are made, because pandas makes a series of reasonable guesses and never mentions them.

What you will have at the end

The survey loaded, an honest list of what every column actually contains, and the habit of looking before you touch anything.

The dataset

Download household-survey-messy.csv from the link above: 600 households from a fictional survey across six districts, plus a few duplicates. It is generated, not real, and it is deliberately broken in the exact ways real exports are broken.

The steps

Load it and look

import pandas as pd

df = pd.read_csv("household-survey-messy.csv")
print(df.shape)
print(df.columns.tolist())
(618, 16)

618 rows for 600 households. Hold that thought — we come back to it in a later lesson.

Look at what pandas decided

This is the step people skip.

print(df.dtypes)

Two of these are worth stopping on:

Column What pandas chose Why
household_size float64 The column is whole numbers, but blanks force it to a float, so you get 4.0
monthly_income_ugx str Some values are text like 450,000, so the whole column stays text

Neither is a bug. Both are pandas doing something sensible with what it was given, and both will surprise you later if you did not look now.

Read it as it actually is

For a first pass, stop pandas guessing at all:

raw = pd.read_csv(
    "household-survey-messy.csv",
    dtype=str,              # every column as text
    keep_default_na=False,  # do not turn "NA" into a blank
)

keep_default_na=False is the important one. By default pandas treats NA, N/A, null and a dozen other strings as missing — which quietly erases the difference between a blank cell and someone having typed NA. Those are not the same thing, and the next lesson but one is entirely about why.

Now count what is really in a column:

print(raw["consent"].value_counts())

Eleven different spellings of yes and no. That is a real export.

Where it breaks

The file with two header rows

If someone ticked "include labels" when exporting, the file has the field names on row one and the question text on row two:

lab = pd.read_csv("household-survey-with-labels.csv")
print(lab.shape)              # (619, 16) - one row too many
print(lab.iloc[0]["district"])  # 'District'  <- a question label, not data

Every column is now text, because the first "value" in each is a sentence. Skip the label row:

lab = pd.read_csv("household-survey-with-labels.csv", skiprows=[1])

Note skiprows=[1] — a list, meaning "skip row index 1". Passing skiprows=1 without the brackets skips the header instead and loses your column names.

The invisible character at the start of the file

Exports often carry a byte order mark: three invisible bytes before the first column name. The symptom is a KeyError on a column you can plainly see.

pandas 3 strips it for you, so you may never meet it there. Other tools do not:

import csv

with open("household-survey-messy.csv", encoding="utf-8") as f:
    print(next(csv.reader(f))[0])      # '_uuid'  <- not '_uuid'

with open("household-survey-messy.csv", encoding="utf-8-sig") as f:
    print(next(csv.reader(f))[0])      # '_uuid'

utf-8-sig means "UTF-8, and discard the mark if it is there". It is safe on files that do not have one, so when you are reading a file you did not create, use it by default.

The ID that became a number

Not in this dataset, but it will happen to you: an identifier like 0012345 read as a number loses its leading zero, and 3.1E5 becomes 310000.0. If a column is an identifier, say so:

df = pd.read_csv("export.csv", dtype={"household_id": str})

Your turn

Load the messy file twice — once with defaults, once with dtype=str and keep_default_na=False — and compare monthly_income_ugx between them.

Count how many cells differ. Those are the values pandas decided were missing on your behalf. One of them is a household that answered.

Next

Select, filter, rename — the four operations that do most of the work.