Exercise 3 — Clean the raw readings

Guided practice5 min
Time
30-40 min
You need
the kit, the venv active, Exercise 1 done
Deliverable
the line All checks passed. and the file data/clean/readings_from_raw.csv

The lab kit of the course: https://github.com/hrhouma2/aiopsatlas-ml-data-diagnostics-labs-en

Goal

In Exercise 1 you counted the defects of readings_raw.csv. Today you fix them, one at a time. After each fix you compare your table with data/clean/readings.csv, the truth. An assert stops the script the moment something is wrong. At the end your file has 14,600 rows and no missing value in the sensor columns. It matches the truth everywhere the raw data still had information.

Setup: the commands (PowerShell, then bash)
powershell
cd aiopsatlas-ml-data-diagnostics-labs-en
.\.venv\Scripts\Activate.ps1
bash
cd aiopsatlas-ml-data-diagnostics-labs-en
source .venv/bin/activate

Create week02/clean_raw.py and start it with this block. truth is the clean file. You compare against it after every step. numpy is the library pandas is built on. Here we only use np.nan, its marker for a missing value.

python
import numpy as np
import pandas as pd

raw = pd.read_csv("data/raw/readings_raw.csv")
truth = pd.read_csv("data/clean/readings.csv")
The data you will touch

Two files with the same rows in a different order. The raw file has no fault_next_7d.

text
data/raw/readings_raw.csv   14,750 rows, 9 columns, shuffled, dirty
data/clean/readings.csv     14,600 rows, 10 columns, sorted by reading_id, clean

The defect table of Exercise 1 is your checklist:

text
duplicated rows                 150
missing temperature_c           291
missing vibration_mm_s          219
missing pressure_bar            146
temperature_c == 999.0           15
load_pct < 0                     12
vibration_mm_s > 30              10

Step 1 — Drop the duplicates

Remove the 150 copies. The row count must be 14,600.

python
readings = raw.drop_duplicates()
print("rows:", len(raw), "->", len(readings))
assert len(readings) == 14600
text
rows: 14750 -> 14600

If the assert fails, the script stops here with AssertionError. That is the point. A silent error later is worse than a loud one now.

Step 2 — Same rows as the truth

Sort by reading_id and reset the index. Then check the columns nobody damaged: reading_id, ambient_c, power_kw.

python
readings = readings.sort_values("reading_id").reset_index(drop=True)
assert (readings["reading_id"] == truth["reading_id"]).all()
assert (readings["ambient_c"] == truth["ambient_c"]).all()
assert (readings["power_kw"] == truth["power_kw"]).all()
print("reading_id, ambient_c and power_kw: identical to the clean file")
text
reading_id, ambient_c and power_kw: identical to the clean file

reset_index(drop=True) matters. Without it, row 0 of readings is not row 0 of truth, and every comparison is off.

Step 3 — Negative loads

Twelve loads have the wrong sign. The absolute value gives the true value back.

python
print("load_pct < 0 before:", (readings["load_pct"] < 0).sum())
readings["load_pct"] = readings["load_pct"].abs()
print("load_pct < 0 after :", (readings["load_pct"] < 0).sum())
assert (readings["load_pct"] == truth["load_pct"]).all()
print("load_pct: identical to the clean file")
text
load_pct < 0 before: 12
load_pct < 0 after : 0
load_pct: identical to the clean file

The assert proves the fix. All 14,600 loads now equal the truth. A sign error is the best kind of defect: the information was still there.

Step 4 — Vibration spikes

Ten vibrations were multiplied by 25 by a gateway bug. Divide them back. Compare only where a value exists, because 219 are still missing.

python
spike = readings["vibration_mm_s"] > 30
print("vibration > 30 before:", spike.sum())
readings.loc[spike, "vibration_mm_s"] = (readings.loc[spike, "vibration_mm_s"] / 25).round(2)
print("vibration > 30 after :", (readings["vibration_mm_s"] > 30).sum())
known = readings["vibration_mm_s"].notna()
assert (readings.loc[known, "vibration_mm_s"] == truth.loc[known, "vibration_mm_s"]).all()
print("vibration_mm_s: identical to the clean file where a value exists")
text
vibration > 30 before: 10
vibration > 30 after : 0
vibration_mm_s: identical to the clean file where a value exists

known is a mask of the rows that have a value. NaN == NaN is False in pandas, so you cannot compare missing cells. You skip them.

Step 5 — The placeholder becomes missing

999.0 carries no information. Turn it into NaN. The count of missing temperatures goes up. That is correct: those fifteen values were always missing, just hidden.

python
print("missing temperature before:", readings["temperature_c"].isna().sum())
readings["temperature_c"] = readings["temperature_c"].replace(999.0, np.nan)
print("missing temperature after :", readings["temperature_c"].isna().sum())
known = readings["temperature_c"].notna()
assert (readings.loc[known, "temperature_c"] == truth.loc[known, "temperature_c"]).all()
print("temperature_c: identical to the clean file where a value exists")
text
missing temperature before: 291
missing temperature after : 306
temperature_c: identical to the clean file where a value exists

291 plus 15 is 306. Every temperature that exists now equals the truth.

Step 6 — Fill the holes with the machine median

Fill each missing value with the median of the same machine. groupby("machine_id")[col].transform("median") gives that median on every row. Then measure how far your filled values are from the truth.

python
for col in ["temperature_c", "vibration_mm_s", "pressure_bar"]:
    missing = readings[col].isna()
    median = readings.groupby("machine_id")[col].transform("median")
    readings[col] = readings[col].fillna(median)
    error = (readings.loc[missing, col] - truth.loc[missing, col]).abs().mean()
    print(f"{col:<15} filled {missing.sum():>3} values, mean error vs truth {error:.2f}")
assert readings[["temperature_c", "vibration_mm_s", "pressure_bar"]].isna().sum().sum() == 0
print("no missing value left")
text
temperature_c   filled 306 values, mean error vs truth 10.31
vibration_mm_s  filled 219 values, mean error vs truth 0.61
pressure_bar    filled 146 values, mean error vs truth 0.31
no missing value left

Read the errors. A filled temperature is off by 10 degrees on average. Temperature changes with the season, and a yearly median ignores that. Vibration and pressure are much closer. Filling is a guess. Now you know how good the guess is.

Step 7 — Save and check the file

Write the result and read it back. The shape must be (14600, 9).

python
out = "data/clean/readings_from_raw.csv"
readings.to_csv(out, index=False)
check = pd.read_csv(out)
print(out, check.shape)
assert check.shape == (14600, 9)
print("All checks passed.")
text
data/clean/readings_from_raw.csv (14600, 9)
All checks passed.

index=False keeps the row numbers out of the file. Without it you get an extra unnamed column on the next read_csv.

Check yourself

  1. Why did the number of missing temperatures rise from 291 to 306 in Step 5?
  2. Which two defects could be fixed exactly, and why?
  3. Why is the mean error 10.31 degrees for temperature but only 0.31 bar for pressure?
  4. What would happen to the comparisons if you forgot reset_index(drop=True) in Step 2?
Answers
  1. The fifteen 999.0 placeholders became NaN. 291 plus 15 is 306. They were always missing, just written as a number.
  2. Negative loads (abs) and vibration spikes (divide by 25). The true value was still inside the wrong value. A 999.0 or an empty cell holds nothing.
  3. Temperature goes from 32.6 in January to 60.5 in July, so a yearly median is often far off. Pressure barely changes over the year.
  4. The rows of readings would keep their shuffled index. Row 0 would not be reading 1, and every == would compare different readings.

Bonus (optional)

Replace the machine median with a better guess for temperature_c: the median of the same machine in the same month. You need a month column from pd.to_datetime(readings["date"]).dt.month and a groupby(["machine_id", "month"]). Measure the mean error again. It should drop well below 10 degrees.

Full solutionweek02/exercise_3_solution.py in the kit
python
"""Week 2, Exercise 3 - Clean the raw readings and prove it.

Run from the kit root, with the venv active:

    python week02/exercise_3_solution.py

Turns data/raw/readings_raw.csv back into a clean table, one defect at a
time, and checks every step against data/clean/readings.csv with assert:
  1. drop the 150 duplicated rows              -> 14,600 rows
  2. sort by reading_id, same ids as the clean file
  3. negative load_pct -> absolute value        -> equal to the clean file
  4. vibration > 30 -> divide by 25             -> equal to the clean file
  5. temperature 999.0 -> missing               -> 306 missing temperatures
  6. fill missing values with the machine median -> no missing value left
  7. save data/clean/readings_from_raw.csv
"""

import numpy as np
import pandas as pd

pd.set_option("display.width", 120)

raw = pd.read_csv("data/raw/readings_raw.csv")
truth = pd.read_csv("data/clean/readings.csv")

# Step 1 - duplicates
print("== Step 1: drop duplicates ==")
readings = raw.drop_duplicates()
print("rows:", len(raw), "->", len(readings))
assert len(readings) == 14600
print()

# Step 2 - same rows as the clean file
print("== Step 2: sort and compare ids ==")
readings = readings.sort_values("reading_id").reset_index(drop=True)
assert (readings["reading_id"] == truth["reading_id"]).all()
assert (readings["ambient_c"] == truth["ambient_c"]).all()
assert (readings["power_kw"] == truth["power_kw"]).all()
print("reading_id, ambient_c and power_kw: identical to the clean file")
print()

# Step 3 - negative loads
print("== Step 3: negative loads ==")
print("load_pct < 0 before:", (readings["load_pct"] < 0).sum())
readings["load_pct"] = readings["load_pct"].abs()
print("load_pct < 0 after :", (readings["load_pct"] < 0).sum())
assert (readings["load_pct"] == truth["load_pct"]).all()
print("load_pct: identical to the clean file")
print()

# Step 4 - vibration spikes
print("== Step 4: vibration spikes ==")
spike = readings["vibration_mm_s"] > 30
print("vibration > 30 before:", spike.sum())
readings.loc[spike, "vibration_mm_s"] = (readings.loc[spike, "vibration_mm_s"] / 25).round(2)
print("vibration > 30 after :", (readings["vibration_mm_s"] > 30).sum())
known = readings["vibration_mm_s"].notna()
assert (readings.loc[known, "vibration_mm_s"] == truth.loc[known, "vibration_mm_s"]).all()
print("vibration_mm_s: identical to the clean file where a value exists")
print()

# Step 5 - the 999.0 placeholder
print("== Step 5: 999.0 becomes missing ==")
print("missing temperature before:", readings["temperature_c"].isna().sum())
readings["temperature_c"] = readings["temperature_c"].replace(999.0, np.nan)
print("missing temperature after :", readings["temperature_c"].isna().sum())
known = readings["temperature_c"].notna()
assert (readings.loc[known, "temperature_c"] == truth.loc[known, "temperature_c"]).all()
print("temperature_c: identical to the clean file where a value exists")
print()

# Step 6 - fill missing values with the median of the same machine
print("== Step 6: fill with the machine median ==")
for col in ["temperature_c", "vibration_mm_s", "pressure_bar"]:
    missing = readings[col].isna()
    median = readings.groupby("machine_id")[col].transform("median")
    readings[col] = readings[col].fillna(median)
    error = (readings.loc[missing, col] - truth.loc[missing, col]).abs().mean()
    print(f"{col:<15} filled {missing.sum():>3} values, mean error vs truth {error:.2f}")
assert readings[["temperature_c", "vibration_mm_s", "pressure_bar"]].isna().sum().sum() == 0
print("no missing value left")
print()

# Step 7 - save
print("== Step 7: save ==")
out = "data/clean/readings_from_raw.csv"
readings.to_csv(out, index=False)
check = pd.read_csv(out)
print(out, check.shape)
assert check.shape == (14600, 9)
print("All checks passed.")

Run it with python week02/exercise_3_solution.py. The output ends with All checks passed.

Stuck? Common errors

All systems — AssertionError in Step 2 on reading_id. You forgot sort_values("reading_id") or reset_index(drop=True). Both are needed.

All systems — AssertionError in Step 4 or 5. You compared rows that contain NaN. Build the known mask with .notna() and compare only those rows.

All systems — SettingWithCopyWarning. You assigned into a filtered copy. Use readings.loc[mask, "col"] = ... as in Step 4, not readings[mask]["col"] = ....

All systems — the mean error prints nan. The missing mask was computed after fillna, so it is all False. Compute missing before you fill.

Windows only — PermissionError when saving. The CSV is open in Excel. Close it and run again.

Linux and macOS only — the file is saved but ls data/clean does not show it. You ran the script from another folder, so the relative path pointed elsewhere. Run from the kit root.