The lab kit of the course: https://github.com/hrhouma2/aiopsatlas-ml-data-diagnostics-labs-en
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.
cd aiopsatlas-ml-data-diagnostics-labs-en
.\.venv\Scripts\Activate.ps1cd aiopsatlas-ml-data-diagnostics-labs-en
source .venv/bin/activateCreate 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.
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")Two files with the same rows in a different order. The raw file has no fault_next_7d.
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, cleanThe defect table of Exercise 1 is your checklist:
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 10Remove the 150 copies. The row count must be 14,600.
readings = raw.drop_duplicates()
print("rows:", len(raw), "->", len(readings))
assert len(readings) == 14600rows: 14750 -> 14600If the assert fails, the script stops here with AssertionError. That is the point. A silent error later is worse than a loud one now.
Sort by reading_id and reset the index. Then check the columns nobody damaged: reading_id, ambient_c, power_kw.
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")reading_id, ambient_c and power_kw: identical to the clean filereset_index(drop=True) matters. Without it, row 0 of readings is not row 0 of truth, and every comparison is off.
Twelve loads have the wrong sign. The absolute value gives the true value back.
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")load_pct < 0 before: 12
load_pct < 0 after : 0
load_pct: identical to the clean fileThe 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.
Ten vibrations were multiplied by 25 by a gateway bug. Divide them back. Compare only where a value exists, because 219 are still missing.
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")vibration > 30 before: 10
vibration > 30 after : 0
vibration_mm_s: identical to the clean file where a value existsknown 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.
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.
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")missing temperature before: 291
missing temperature after : 306
temperature_c: identical to the clean file where a value exists291 plus 15 is 306. Every temperature that exists now equals the truth.
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.
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")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 leftRead 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.
Write the result and read it back. The shape must be (14600, 9).
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.")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.
reset_index(drop=True) in Step 2?999.0 placeholders became NaN. 291 plus 15 is 306. They were always missing, just written as a number.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.readings would keep their shuffled index. Row 0 would not be reading 1, and every == would compare different readings.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.
week02/exercise_3_solution.py in the kit"""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.
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.