Data quality¶
Exercise data-quality-audit-annex.pdf · PDF
You ask the games table a simple question: which is the oldest game? You sort by release date and the answer comes back at once. A game from October 2014. Right behind it, one from April 2004.
2014 before 2004. The query did not fail. There was no red message. The table just handed you a wrong answer with a straight face. That is what dirty data does: it does not crash, it lies quietly. Before you clean anything, you need to know exactly where the dirt is. This page teaches you to look.
What you will learn
- Name the four kinds of dirty data: missing values, duplicates, wrong types or formats, and outliers
- Count the empty cells of any column with
COUNT(*) - COUNT(column) - Spot a number or a date that is really stored as text, and say what breaks because of it
- Tell an empty cell that means "we do not know" from one that means something
- Write a dirt inventory: column, kind of dirt, evidence, decision
- Avoid the trap: why "same name" is not a duplicate and "very big" is not an error
Open the chest before you smelt¶
Data quality is how far you can trust a table to answer your question: its values are present, correct, written in one consistent way, and not repeated.
In the crafting game we use all year, Unit 1 was farming and looting: you brought the materials home. Unit 2 is the furnace: preparing them. But nobody smelts blind. First you open the loot chest and look: empty slots, the same item twice, broken items, one sword with absurd stats.
A data audit is that look. You inspect a table, count what is wrong with it and write it down, without changing anything.
Our chest is games: 26,688 Steam games, one per row, collected from the statistics site Steam Spy and left dirty on purpose. You can download the file from the Resources page.
flowchart LR
A["<b>Audit</b><br>open the chest, count the dirt"] --> B["<b>Decide</b><br>fix, flag or leave"]
B --> C["<b>Clean</b><br>the furnace: the next lessons"]
C --> D["<b>Report</b><br>say what you did and what is left"]
Careful: today nobody fixes anything
The temptation is to start repairing the first problem you see. Resist it. A fix made before the audit is a guess, and you will not know what else you broke. The only product of an audit is a list.
Four kinds of dirt¶
Almost everything that can be wrong with a table falls into four groups. Each one has its own way of being found.
| Kind | In the chest | In a table | How to find it |
|---|---|---|---|
| Missing value | An empty slot | A NULL: no value at all |
COUNT(*) - COUNT(column) |
| Duplicate | The same item twice | The same real thing in two rows | GROUP BY + HAVING COUNT(*) > 1 |
| Wrong type or format | A sword stored as a picture of a sword | A date or a number stored as text; one thing spelled two ways | Look at the column type; sort it; LIKE |
| Outlier | An item with 9,999 damage | A value far from all the others. Maybe an error, maybe real | MIN, MAX, ORDER BY ... DESC |
The next four sections take them one at a time, on games. Every number below comes from running the query on the class table.
Missing values: the empty slots¶
A missing value is a cell with nothing in it. In SQL it is written
NULL, which means "no value", not zero and not an empty word.
You already know the tool from Unit 1. COUNT(*) counts rows. COUNT(column) counts only the rows where that column is not empty. The difference is the number of empty slots.
SELECT COUNT(*) AS total,
COUNT(price) AS with_price,
COUNT(*) - COUNT(price) AS no_price
FROM games;
| total | with_price | no_price |
|---|---|---|
| 26688 | 23593 | 3095 |
Do the same for every column, in one single SELECT, and you get the full picture:
| Column | Empty cells | Share of the rows |
|---|---|---|
metascore |
23,838 | 89.3% |
price |
3,095 | 11.6% |
developer |
152 | 0.6% |
publisher |
120 | 0.4% |
median_playtime |
12 | under 0.1% |
average_playtime |
9 | under 0.1% |
game |
3 | 0.01% |
number, release_date, owners |
0 | 0% |
Always write the percentage next to the count. "23,838 empty scores" sounds like a lot. "89.3% of the column is empty" tells you the column is almost not there.
Careful: an empty cell is not always an error
Dota 2 has no price. Is that a mistake? No: Dota 2 is free to play. That NULL carries information. Now look at metascore, the review score from the site Metacritic: 23,838 games have none, simply because no critic ever reviewed them. That NULL means "unknown".
Same empty slot, two different stories, and later two different treatments. An auditor writes down which story it is.
Missing values in disguise¶
Some empty slots do not look empty. Try these two:
SELECT COUNT(*) FROM games WHERE average_playtime = 0; -- 25757
SELECT COUNT(*) FROM games WHERE price = 0; -- 0
average_playtime is 0 in 25,757 rows: 96.5% of the table. Nobody believes that 96.5% of Steam games were played for zero minutes. That zero means "we have no data". It is a missing value wearing a costume.
And price = 0 finds nothing. Not one game costs exactly zero. So where are the free games? They are the 3,095 NULL ones.
| What you see | What it is | In games |
|---|---|---|
NULL |
No value at all | price IS NULL finds 3,095 rows |
0 |
A value: the number zero | price = 0 finds 0 rows |
0 used as "no data" |
A disguised missing value | average_playtime = 0 in 25,757 rows |
Wrong types and formats: text in disguise¶
A data type is the kind of value a column is allowed to hold: whole number, decimal number, text, date. The type decides what you can do with the column.
Ask the database what each column is made of:
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'games'
ORDER BY ordinal_position;
| column_name | data_type |
|---|---|
| number | bigint |
| game | text |
| release_date | text |
| price | double precision |
| owners | text |
| developer | text |
| publisher | text |
| average_playtime | double precision |
| median_playtime | double precision |
| metascore | double precision |
Two columns are text and should not be: release_date (a date) and owners (a quantity). This is what broke the query at the top of the page:
SELECT game, release_date FROM games ORDER BY release_date LIMIT 3;
| game | release_date |
|---|---|
| Hazard Ops | 8 Oct, 2014 |
| The Dark Legions | Apr 1, 2004 |
| Geneforge 3 | Apr 1, 2005 |
Text sorts like a dictionary, character by character. 8 comes before any letter, and Apr comes before Feb because A comes before F. A date stored as text cannot be sorted, filtered by year or put on a time axis.
It gets worse. A format is the way a value is written down. Most dates here look like Nov 16, 2004. Are they all like that?
SELECT release_date, COUNT(*) AS n
FROM games
WHERE release_date NOT LIKE '___ %, ____' -- 3 letters, space, day, comma, 4 digits
GROUP BY release_date
ORDER BY n DESC;
42 rows have another shape: 41 give only the month and the year (Oct 2010 three times, Jun 2009 twice...) and one is written the other way round, 8 Oct, 2014. One column, three ways of writing a date.
owners has the same disease. It holds only 13 different values, and all of them are ranges written as text, such as 10,000,000 .. 20,000,000. The most common is 0 .. 20,000, with 17,923 games. Sort it to find the most owned game and you get nonsense:
SELECT game, owners FROM games ORDER BY owners DESC LIMIT 3;
| game | owners |
|---|---|
| DOOM 3 | 500,000 .. 1,000,000 |
| Unreal Tournament 2004: Editor's Choice Edition | 500,000 .. 1,000,000 |
| Beyond Divinity | 500,000 .. 1,000,000 |
Dota 2, with 100,000,000 .. 200,000,000, loses. As text, 5 beats 1, so "500 thousand" beats "100 million".
Duplicates: the same item twice¶
A duplicate is the same real thing recorded in more than one row.
Is any game in the table twice? Group by name and keep the names that appear more than once:
SELECT game, COUNT(*) AS n
FROM games
WHERE game IS NOT NULL
GROUP BY game
HAVING COUNT(*) > 1
ORDER BY n DESC, game;
74 names come back, covering 149 rows. At the top, Dark Matter, three times. Before you call it a duplicate, look at it:
SELECT number, game, release_date, developer
FROM games WHERE game = 'Dark Matter';
| number | game | release_date | developer |
|---|---|---|---|
| 317 | Dark Matter | Oct 17, 2013 | InterWave Studios |
| 777 | Dark Matter | Feb 27, 2015 | Meridian4 |
| 770 | Dark Matter | May 4, 2018 | Barty Games |
Three developers, three dates. These are three different games that happen to share a name. Delete two of them and you have destroyed real data.
Careful: number is not an identifier
It is tempting to think the column number tells rows apart. Check it: SELECT COUNT(*), COUNT(DISTINCT number) FROM games; returns 26,688 rows but only 8,846 different numbers. The same number is reused by many games. An audit also tells you what you cannot rely on.
Outliers: the item with absurd stats¶
An outlier is a value far away from all the others. It may be a typing error. It may also be perfectly real.
Prices are in US dollars. Look at the edges of the column:
SELECT MIN(price), MAX(price), ROUND(AVG(price)::numeric, 2) AS mean FROM games;
| min | max | mean |
|---|---|---|
| 0.49 | 595.99 | 8.95 |
The average game costs 8.95 and the most expensive one costs 595.99. More than sixty times the average. A typo? Look before you decide:
SELECT game, price, developer
FROM games ORDER BY price DESC NULLS LAST LIMIT 4;
| game | price | developer |
|---|---|---|
| ADR-Labelling Game | 595.99 | Suomen Kuljetusturva Oy |
| GameMaker Studio 2 UWP | 399.99 | YoYo Games Ltd. |
| VEGAS Pro 16 Edit Steam Edition | 399 | MAGIX Software GmbH |
| Marvelous Designer 8 for Steam | 320 | CLO Virtual Fashion |
A game-making tool, a video editor, a 3D fashion design program. Steam also sells professional software, and software is expensive. 27 rows cost more than 100 dollars, and they are mostly products like these, not typing mistakes.
Careful: an outlier is not a mistake
Delete only what you can prove is wrong. 595.99 is a real price of a real product. An outlier is a question ("why is this one so different?"), and the answer is found by looking at the row, never by assuming.
The dirt inventory¶
A dirt inventory is the written result of an audit: one line per problem, each with its evidence and a decision.
Every line has four parts. The rule for the third one is strict: no number, no evidence. "There are NULLs" is not an audit. "3,095 NULL in price, 11.6% of the rows" is.
For the decision you have three verbs:
| Decision | Meaning | When |
|---|---|---|
| Fix | Repair the value | You know what the right value is |
| Flag | Leave it, and mark it so nobody forgets | You do not know the value and will not invent one |
| Leave | Keep it as it is and write it down as a limit | The value is real, or nothing can be done |
This is the inventory of games. It is also the map of the rest of the unit:
| Column | What we found | Decision | Lesson |
|---|---|---|---|
game |
3 rows without a name | Drop the rows: a game with no name cannot be identified | Types and formats |
release_date |
Text, in three shapes (42 rows differ) | Fix: convert to a real date | Types and formats |
owners |
Text. A range, not a number. 13 values | Fix: extract the lower bound as a number | Units and duplicates |
game |
74 names appear more than once | Investigate: same game, or same name? | Units and duplicates |
price |
Up to 595.99. Real: software sold on Steam | Leave the values; group them into bands | Categorisation |
price |
3,095 NULL (11.6%): free to play |
Fix with what we know: free means 0 | Imputation |
metascore |
23,838 NULL (89.3%) |
Flag, never invent a score | Imputation |
| playtime | 0 in 25,757 rows (96.5%) | Leave and write it down as a limit | The descriptive report |
Why should I care?
Imagine your boss asks for "the average score of Steam games". You can compute it in one line. But the inventory tells you the average is built on 11% of the table, and not a random 11%. Cleaning is not making the table pretty. It is knowing exactly what your numbers can and cannot say.
Try it: audit games yourself¶
Open SQL → SQL Lab, choose the database Dades classe and the schema public. Run one statement at a time: select it, then press Run. If you run a whole block, SQL Lab only shows the last result.
Step 1 — Size and types. Run SELECT COUNT(*) FROM games; and then the information_schema.columns query from this page. You should see 26,688 rows and two text columns that should not be text.
Step 2 — Missing values. Write one SELECT with a COUNT(*) - COUNT(column) for each of the ten columns. Your numbers should match the table in "Missing values". Then run the two disguise checks (average_playtime = 0 and price = 0).
Step 3 — Wrong formats. Run the NOT LIKE query and the two owners queries. You should be able to say how many dates have a different shape (42) and why DOOM 3 is not really the most owned game.
Step 4 — Duplicates. Run the HAVING COUNT(*) > 1 query, then look at the Dark Matter rows. Write one sentence: are they the same game?
Step 5 — Outliers. Run the MIN/MAX query and the top prices. Decide, with a reason, whether 595.99 is a typing error.
SQL Lab only shows 1,000 rows. Is my table broken?
No. SQL Lab limits what it displays, not what the table holds. COUNT(*) always counts every row. If a different total comes back from COUNT(*), the file was probably uploaded twice: a real duplicate problem, and a nice one to write in your inventory.
Key takeaways¶
- A data audit means looking and counting without changing anything. Its only product is a list.
- There are four kinds of dirt: missing values, duplicates, wrong types or formats, and outliers.
COUNT(*) - COUNT(column)gives the empty cells of a column. Always add the percentage.NULLand0are different things. ANULLcan mean "unknown" (metascore) or carry meaning (a free game), and a0can be a missing value in disguise (average_playtime).- A date or number stored as text sorts like a dictionary and cannot be calculated with.
- Same name is not a duplicate, and an outlier is not an error: look at the row before you decide.
- A dirt inventory has four parts per line: column, kind of dirt, evidence with a number, and a decision (fix, flag or leave).
Quick check¶
1. Which query counts the empty cells of the column price?
-
SELECT COUNT(price) FROM games -
SELECT COUNT(*) - COUNT(price) FROM games -
SELECT COUNT(*) FROM games WHERE price = 0 -
SELECT MIN(price) FROM games
COUNT(*) counts every row and COUNT(price) skips the rows where price is NULL, so the difference is the number of empty cells: 3,095. Searching for price = 0 finds nothing, because zero is a value and NULL is the absence of one.
2. Sorting games by release_date puts Apr 1, 2004 before any date in February. Why?
- Because April has fewer days than February in that year
- Because the table was uploaded in the wrong order
- Because the column is stored as text, and text sorts alphabetically
- Because
ORDER BYdoes not work on dates
release_date has the data type text, so the database compares it letter by letter: A comes before F. A real date column would sort by time. The type decides what you can do with a column, not how it looks.
3. Dota 2 has a NULL price and most games have a NULL metascore. What is the difference?
- None: both are errors that must be deleted
- The price is an outlier and the metascore is a duplicate
- The empty price means the game is free; the empty metascore means nobody reviewed the game
- The empty price is unknown; the empty metascore means the score is zero
The same empty cell can tell two stories. The price NULL carries information (free to play), so it can later be fixed with what we know. The metascore NULL means unknown, so it gets flagged, never invented.
4. Dark Matter appears three times in games, with three developers and three release dates. What should the audit say?
- It is a duplicate: keep the first row and delete the other two
- They are three different games that share a name: keep all three
- It is an outlier, because no other name appears three times
- The column
numberproves they are the same game
A duplicate is the same real thing in more than one row. Different developer and different date mean a different game. And number cannot help: it has only 8,846 different values for 26,688 rows, so it is not an identifier.
5. The most expensive row in games costs 595.99, and the mean price is 8.95. What is the right reaction?
- Delete the row: it is obviously a typing error
- Replace it with the mean price
- Look at the row first: it is real software sold on Steam, so it stays
- Ignore it: outliers never affect anything
An outlier is a question, not a verdict. The top prices belong to professional tools (a game engine, a video editor), which really do cost that much. You delete only what you can prove is wrong.
6. Which of these is a valid line of evidence in a dirt inventory?
- "The price column has some NULLs"
- "The dates look strange"
- "
COUNT(*) - COUNT(metascore)returns 23,838: 89.3% of the rows have no score" - "I fixed the owners column"
No number, no evidence. A valid line names the query and the figure it returned, with a percentage when it helps. And the last option breaks the first rule of an audit: nothing gets fixed yet.
7. You receive a table of a gym's members. The column visits_last_month is 0 for 95% of the members, and the gym is full every evening. Pick the two statements an auditor would write.
- The zeros are probably missing values in disguise: "no data", not "zero visits"
- The zeros are outliers and must be deleted
- The finding goes in the inventory with its count and percentage before anything is changed
- The column is clean, because it contains no
NULL
This is the average_playtime case again: a 0 that means "nobody measured it". A column with no NULL can still be almost empty. The auditor writes the evidence down first and decides later whether to fix, flag or leave.
Now you know what is dirty, and you have a plan. Time to light the furnace: next, Cleaning: types and formats, where release_date finally becomes a real date.