The lab kit of the course: https://github.com/hrhouma2/aiopsatlas-ml-data-diagnostics-labs-en
The maintenance app of NorthPeak exports incidents as JSON documents. Each document nests the machine inside the incident and carries a list of tags. You read the file with Python's json module, then with DuckDB as if it were a table. Both give the same counts. At the end you put a small cache in front of a slow function and watch the second call become instant.
cd aiopsatlas-ml-data-diagnostics-labs-en
.\.venv\Scripts\Activate.ps1cd aiopsatlas-ml-data-diagnostics-labs-en
source .venv/bin/activateCreate week03/json_two_ways.py and start it with these imports. DuckDB is already installed in the venv.
import json
import time
from collections import Counter
import duckdbdata/json/incidents.json: one list of 155 objects. The second document:
{
"_id": "INC-0002",
"date": "2025-05-05",
"machine": {"id": "M001", "type": "pump", "site": "Toronto"},
"category": "bearing_wear",
"severity": "low",
"downtime_hours": 2.9,
"repair_cost_cad": 582.47,
"technician": "M. Haddad",
"description": "Bearing temperature rising on M001. Operator reports a rattling sound near the drive shaft.",
"tags": ["bearing", "low", "pump"]
}_id — the incident id, INC-0001 to INC-0155.machine — a nested object: id, type, site.category, severity — the same values as in the CSV.tags — a list of three words: the first word of the category, the severity, the machine type.json.load reads the file into a Python list. Each item is a dictionary.
with open("data/json/incidents.json", encoding="utf-8") as f:
docs = json.load(f)
print("documents:", len(docs))
first = docs[0]
print("keys:", list(first.keys()))
print("first machine:", first["machine"])
print("site of the first incident:", first["machine"]["site"])
print("tags:", first["tags"])documents: 155
keys: ['_id', 'date', 'machine', 'category', 'severity', 'downtime_hours', 'repair_cost_cad', 'technician', 'description', 'tags']
first machine: {'id': 'M001', 'type': 'pump', 'site': 'Toronto'}
site of the first incident: Toronto
tags: ['high', 'overheating', 'pump']first["machine"] is itself a dictionary. first["machine"]["site"] goes one level deeper. No join: the site travels with the incident.
Counter counts values. Feed it the site of every document.
by_site = Counter(doc["machine"]["site"] for doc in docs)
for site, n in by_site.most_common():
print(f"{site:<12} {n}")Montreal 66
Toronto 56
Quebec City 33The same 66, 56, 33 as the SQL join of Exercise 1. Same data, different shape.
"high" in doc["tags"] is True when the list contains that word. Combine two conditions with and.
high = [doc for doc in docs if "high" in doc["tags"]]
print("tag 'high' :", len(high))
high_pump = [doc["_id"] for doc in docs if "high" in doc["tags"] and "pump" in doc["tags"]]
print("tags 'high' + 'pump':", len(high_pump), high_pump)tag 'high' : 23
tags 'high' + 'pump': 4 ['INC-0001', 'INC-0010', 'INC-0021', 'INC-0025']23 high-severity incidents in the year. Four of them on pumps. Those four ids are your first deliverable.
read_json_auto opens the file and guesses the types. Ask for the count, then the types of three columns.
print(duckdb.sql("SELECT COUNT(*) AS n FROM read_json_auto('data/json/incidents.json')"))
print(duckdb.sql("""
SELECT column_name, column_type
FROM (DESCRIBE SELECT * FROM read_json_auto('data/json/incidents.json'))
WHERE column_name IN ('machine', 'tags', 'date')
"""))┌───────┐
│ n │
│ int64 │
├───────┤
│ 155 │
└───────┘
┌─────────────┬──────────────────────────────────────────────────┐
│ column_name │ column_type │
│ varchar │ varchar │
├─────────────┼──────────────────────────────────────────────────┤
│ date │ DATE │
│ machine │ STRUCT(id VARCHAR, "type" VARCHAR, site VARCHAR) │
│ tags │ VARCHAR[] │
└─────────────┴──────────────────────────────────────────────────┘DuckDB understood the nesting. machine is a STRUCT with three fields. tags is VARCHAR[], a list of text. And date became a real DATE, which pandas did not do for the CSV.
A nested field is reached with a dot: machine.site. A list is tested with list_contains.
print(duckdb.sql("""
SELECT machine.site AS site, COUNT(*) AS n
FROM read_json_auto('data/json/incidents.json')
GROUP BY machine.site
ORDER BY n DESC
"""))
print(duckdb.sql("""
SELECT _id, machine.id AS machine_id, tags
FROM read_json_auto('data/json/incidents.json')
WHERE list_contains(tags, 'high') AND list_contains(tags, 'pump')
ORDER BY _id
"""))┌─────────────┬───────┐
│ site │ n │
│ varchar │ int64 │
├─────────────┼───────┤
│ Montreal │ 66 │
│ Toronto │ 56 │
│ Quebec City │ 33 │
└─────────────┴───────┘
┌──────────┬────────────┬───────────────────────────┐
│ _id │ machine_id │ tags │
│ varchar │ varchar │ varchar[] │
├──────────┼────────────┼───────────────────────────┤
│ INC-0001 │ M001 │ [high, overheating, pump] │
│ INC-0010 │ M003 │ [bearing, high, pump] │
│ INC-0021 │ M007 │ [bearing, high, pump] │
│ INC-0025 │ M008 │ [electrical, high, pump] │
└──────────┴────────────┴───────────────────────────┘Same 66, 56, 33. Same four ids. Python loops and SQL agree, as they must.
Write a function that is slow on purpose: it sleeps half a second and re-reads the file. Put a dictionary in front of it. Call it twice.
cache = {}
def count_by_site(site):
if site in cache:
return cache[site], "from cache"
time.sleep(0.5)
with open("data/json/incidents.json", encoding="utf-8") as f:
n = sum(1 for doc in json.load(f) if doc["machine"]["site"] == site)
cache[site] = n
return n, "computed"
for _ in range(2):
start = time.perf_counter()
n, how = count_by_site("Toronto")
print(f"Toronto -> {n} ({how}, {time.perf_counter() - start:.3f} s)")
print("cache keys:", list(cache.keys()))Toronto -> 56 (computed, 0.502 s)
Toronto -> 56 (from cache, 0.000 s)
cache keys: ['Toronto']Your exact times will differ a little. The pattern will not: half a second, then nothing. The key is the site; the value is the count. That is a key-value store in five lines. Lesson 5 explains where this idea goes.
high, and how many of those also carry pump?tags, and which SQL function tests it?doc["machine"]["site"] in Python; machine.site in DuckDB.INC-0001, INC-0010, INC-0021, INC-0025.VARCHAR[], a list of text. list_contains(tags, 'high') tests it.56 under the key Toronto in the dictionary. The second call found the key and skipped the sleep and the file read.In DuckDB, count how many documents carry each tag. You need unnest(tags) in a subquery, then GROUP BY. The tag low should appear 73 times and bearing 48 times. Then compute the mean downtime_hours by machine.type. Chillers should be the highest, around 10.5 hours.
week03/exercise_2_solution.py in the kit"""Week 3, Exercise 2 - JSON documents with the json module and DuckDB.
Run from the kit root, with the venv active:
python week03/exercise_2_solution.py
Reads data/json/incidents.json two ways:
1. json.load: a list of 155 dicts, nested access with ["machine"]["site"];
2. a count by site with a loop;
3. a tag filter in Python: "high" and "pump";
4. DuckDB read_json_auto: the same file as a table, nested columns;
5. DuckDB: the same count by site and the same tag filter;
6. a small dict cache in front of a slow function.
"""
import json
import time
from collections import Counter
import duckdb
# Step 1 - load the documents
print("== Step 1: json.load ==")
with open("data/json/incidents.json", encoding="utf-8") as f:
docs = json.load(f)
print("documents:", len(docs))
first = docs[0]
print("keys:", list(first.keys()))
print("first machine:", first["machine"])
print("site of the first incident:", first["machine"]["site"])
print("tags:", first["tags"])
print()
# Step 2 - count by site, nested field
print("== Step 2: incidents by site ==")
by_site = Counter(doc["machine"]["site"] for doc in docs)
for site, n in by_site.most_common():
print(f"{site:<12} {n}")
print()
# Step 3 - tag filter in Python
print("== Step 3: tag filter ==")
high = [doc for doc in docs if "high" in doc["tags"]]
print("tag 'high' :", len(high))
high_pump = [doc["_id"] for doc in docs if "high" in doc["tags"] and "pump" in doc["tags"]]
print("tags 'high' + 'pump':", len(high_pump), high_pump)
print()
# Step 4 - DuckDB reads the same file as a table
print("== Step 4: DuckDB read_json_auto ==")
print(duckdb.sql("SELECT COUNT(*) AS n FROM read_json_auto('data/json/incidents.json')"))
print(
duckdb.sql(
"""
SELECT column_name, column_type
FROM (DESCRIBE SELECT * FROM read_json_auto('data/json/incidents.json'))
WHERE column_name IN ('machine', 'tags', 'date')
"""
)
)
# Step 5 - the same two questions in SQL
print("== Step 5: DuckDB, nested field and list ==")
print(
duckdb.sql(
"""
SELECT machine.site AS site, COUNT(*) AS n
FROM read_json_auto('data/json/incidents.json')
GROUP BY machine.site
ORDER BY n DESC
"""
)
)
print(
duckdb.sql(
"""
SELECT _id, machine.id AS machine_id, tags
FROM read_json_auto('data/json/incidents.json')
WHERE list_contains(tags, 'high') AND list_contains(tags, 'pump')
ORDER BY _id
"""
)
)
# Step 6 - a dict as a cache
print("== Step 6: a dict cache ==")
cache = {}
def count_by_site(site):
"""Slow on purpose: reads the whole file every time."""
if site in cache:
return cache[site], "from cache"
time.sleep(0.5)
with open("data/json/incidents.json", encoding="utf-8") as f:
n = sum(1 for doc in json.load(f) if doc["machine"]["site"] == site)
cache[site] = n
return n, "computed"
for _ in range(2):
start = time.perf_counter()
n, how = count_by_site("Toronto")
print(f"Toronto -> {n} ({how}, {time.perf_counter() - start:.3f} s)")
print("cache keys:", list(cache.keys()))Run it with python week03/exercise_2_solution.py. The Step 6 block at the end shows the two timing lines.
All systems — json.decoder.JSONDecodeError. The file is cut or was edited by hand. Run python data/make_dataset.py again.
All systems — TypeError: list indices must be integers. You wrote docs["machine"]. docs is the list; docs[0] is the first document.
All systems — KeyError: 'site'. You wrote doc["site"]. The site is nested: doc["machine"]["site"].
All systems — duckdb.BinderException: ... UNNEST not supported here. In the bonus, unnest must be in a subquery: SELECT tag, COUNT(*) FROM (SELECT unnest(tags) AS tag FROM ...) GROUP BY tag.
Windows only — the DuckDB table borders print as ? characters. The console encoding is not UTF-8. Run $env:PYTHONIOENCODING = "utf-8" once in PowerShell, then run again.
Linux and macOS only — ModuleNotFoundError: No module named 'duckdb'. The venv is not active, or pip install -r requirements.txt was interrupted. Activate and run the install again.