The lab kit of the course: https://github.com/hrhouma2/aiopsatlas-ml-data-diagnostics-labs-en
NorthPeak keeps its clean data in a SQLite file. Your colleagues ask questions in SQL. You prefer pandas. Today you answer six questions both ways and prove the answers match with assert. Each query adds one SQL word. By the end you have used COUNT, WHERE, ORDER BY, LIMIT, GROUP BY, AVG, SUM and JOIN.
cd aiopsatlas-ml-data-diagnostics-labs-en
.\.venv\Scripts\Activate.ps1cd aiopsatlas-ml-data-diagnostics-labs-en
source .venv/bin/activateCreate week02/six_queries.py. Start it with this block. The function sql runs one query and returns a DataFrame.
import sqlite3
import pandas as pd
pd.set_option("display.width", 120)
con = sqlite3.connect("data/diagnostics.sqlite")
machines = pd.read_csv("data/clean/machines.csv")
readings = pd.read_csv("data/clean/readings.csv")
incidents = pd.read_csv("data/clean/incidents.csv")
def sql(query):
return pd.read_sql_query(query, con)data/diagnostics.sqlite holds three tables. They are the clean CSV files, loaded as is.
| Table | Rows | Key columns |
|---|---|---|
machines | 40 | machine_id, machine_type, site, install_year, rated_power_kw |
readings | 14,600 | reading_id, machine_id, date, six sensor columns, fault_next_7d |
incidents | 155 | incident_id, machine_id, date, category, severity, downtime_hours, repair_cost_cad, technician, description |
machine_id appears in all three tables. It is the column you join on.
pd.read_sql_query("SELECT * FROM machines LIMIT 3", con) prints:
machine_id machine_type site install_year rated_power_kw
0 M001 pump Toronto 2022 45.0
1 M002 pump Montreal 2016 45.0
2 M003 pump Toronto 2013 45.0The simplest query: how many readings? In pandas, len().
q1 = sql("SELECT COUNT(*) AS n FROM readings")
print(q1)
p1 = len(readings)
print("pandas:", p1)
assert q1["n"][0] == p1 n
0 14600
pandas: 14600AS n names the result column. Without it the column is called COUNT(*), which is hard to type.
The hot days of M001: temperature above 60, oldest first, five rows.
q2 = sql("""
SELECT date, temperature_c
FROM readings
WHERE machine_id = 'M001' AND temperature_c > 60
ORDER BY date
LIMIT 5
""")
print(q2)
mask = (readings["machine_id"] == "M001") & (readings["temperature_c"] > 60)
p2 = readings.loc[mask, ["date", "temperature_c"]].sort_values("date").head(5)
print(p2.to_string(index=False))
assert q2["date"].tolist() == p2["date"].tolist() date temperature_c
0 2025-04-23 61.1
1 2025-05-02 62.7
2 2025-05-05 65.7
3 2025-05-07 63.0
4 2025-05-20 60.3The pandas side prints the same five rows. Notice the & between the two conditions, and the brackets around each one. In SQL it is AND with no brackets.
Machines per site. In pandas this is value_counts().
q3 = sql("SELECT site, COUNT(*) AS n FROM machines GROUP BY site ORDER BY n DESC")
print(q3)
p3 = machines["site"].value_counts()
print(p3)
assert dict(zip(q3["site"], q3["n"])) == p3.to_dict() site n
0 Montreal 20
1 Toronto 13
2 Quebec City 7The assert turns both results into dictionaries. Order does not matter in a dictionary, so the check is fair.
The five machines with the highest mean temperature.
q4 = sql("""
SELECT machine_id, ROUND(AVG(temperature_c), 1) AS mean_temp
FROM readings
GROUP BY machine_id
ORDER BY mean_temp DESC
LIMIT 5
""")
print(q4)
p4 = readings.groupby("machine_id")["temperature_c"].mean().round(1).sort_values(ascending=False).head(5)
print(p4)
assert q4["machine_id"].tolist() == p4.index.tolist() machine_id mean_temp
0 M012 61.3
1 M018 59.7
2 M019 59.6
3 M016 59.2
4 M011 58.8All five are compressors, M011 to M020. Compressors run hot by design.
Mean power by machine type. Power is in readings, type is in machines. The pandas twin of JOIN is merge.
q5 = sql("""
SELECT m.machine_type, ROUND(AVG(r.power_kw), 1) AS mean_power
FROM readings r
JOIN machines m ON r.machine_id = m.machine_id
GROUP BY m.machine_type
ORDER BY mean_power DESC
""")
print(q5)
merged = readings.merge(machines, on="machine_id")
p5 = merged.groupby("machine_type")["power_kw"].mean().round(1).sort_values(ascending=False)
print(p5)
assert q5["mean_power"].tolist() == p5.tolist() machine_type mean_power
0 compressor 45.8
1 chiller 36.9
2 pump 25.9
3 conveyor 12.6r and m are short names for the two tables. r.machine_id = m.machine_id says which rows go together. merged has 14,600 rows and the columns of both tables.
Incidents and total repair cost by category.
q6 = sql("""
SELECT category, COUNT(*) AS n, ROUND(SUM(repair_cost_cad), 2) AS cost
FROM incidents
GROUP BY category
ORDER BY n DESC
""")
print(q6)
p6 = (
incidents.groupby("category")
.agg(n=("incident_id", "count"), cost=("repair_cost_cad", "sum"))
.round(2)
.sort_values("n", ascending=False)
)
print(p6)
assert q6["n"].tolist() == p6["n"].tolist()
assert q6["cost"].tolist() == p6["cost"].tolist()
con.close()
print("All six queries give the same answer in SQL and in pandas.") category n cost
0 bearing_wear 48 60257.29
1 overheating 39 54811.67
2 leak 28 22957.77
3 electrical 23 31467.09
4 sensor_fault 17 10439.98
All six queries give the same answer in SQL and in pandas.Bearing wear is the most frequent category and the most expensive: 48 incidents, 60,257 dollars. Electrical is rarer but costs more per incident than leaks.
merge, and which column did you join on?AS n change in the result?assert of Query 3 convert both sides to dictionaries?JOIN. Both tables share machine_id, so ON r.machine_id = m.machine_id.n. Without it the column is called COUNT(*).value_counts() may order the sites differently. A dictionary compares site to count, whatever the order.Write a seventh query: the mean downtime_hours and the mean repair_cost_cad by severity, in SQL and in pandas. Then a harder one: the number of incidents per site, which needs a JOIN between incidents and machines. Montreal should have 66.
week02/exercise_2_solution.py in the kit"""Week 2, Exercise 2 - Six SQL queries, and the same six in pandas.
Run from the kit root, with the venv active:
python week02/exercise_2_solution.py
Every query runs twice: once in SQL on data/diagnostics.sqlite with sqlite3,
once in pandas on the CSV files of data/clean/. The script checks that the
two answers are the same.
"""
import sqlite3
import pandas as pd
pd.set_option("display.width", 120)
con = sqlite3.connect("data/diagnostics.sqlite")
machines = pd.read_csv("data/clean/machines.csv")
readings = pd.read_csv("data/clean/readings.csv")
incidents = pd.read_csv("data/clean/incidents.csv")
def sql(query):
"""Run one SELECT and return a DataFrame."""
return pd.read_sql_query(query, con)
# Query 1 - COUNT
print("== Query 1: how many readings? ==")
q1 = sql("SELECT COUNT(*) AS n FROM readings")
print(q1)
p1 = len(readings)
print("pandas:", p1)
assert q1["n"][0] == p1
print()
# Query 2 - WHERE + ORDER BY + LIMIT
print("== Query 2: hot days of M001 ==")
q2 = sql(
"""
SELECT date, temperature_c
FROM readings
WHERE machine_id = 'M001' AND temperature_c > 60
ORDER BY date
LIMIT 5
"""
)
print(q2)
mask = (readings["machine_id"] == "M001") & (readings["temperature_c"] > 60)
p2 = readings.loc[mask, ["date", "temperature_c"]].sort_values("date").head(5)
print(p2.to_string(index=False))
assert q2["date"].tolist() == p2["date"].tolist()
print()
# Query 3 - GROUP BY + COUNT
print("== Query 3: machines per site ==")
q3 = sql("SELECT site, COUNT(*) AS n FROM machines GROUP BY site ORDER BY n DESC")
print(q3)
p3 = machines["site"].value_counts()
print(p3)
assert dict(zip(q3["site"], q3["n"])) == p3.to_dict()
print()
# Query 4 - GROUP BY + AVG
print("== Query 4: the five hottest machines ==")
q4 = sql(
"""
SELECT machine_id, ROUND(AVG(temperature_c), 1) AS mean_temp
FROM readings
GROUP BY machine_id
ORDER BY mean_temp DESC
LIMIT 5
"""
)
print(q4)
p4 = readings.groupby("machine_id")["temperature_c"].mean().round(1).sort_values(ascending=False).head(5)
print(p4)
assert q4["machine_id"].tolist() == p4.index.tolist()
print()
# Query 5 - JOIN
print("== Query 5: mean power by machine type ==")
q5 = sql(
"""
SELECT m.machine_type, ROUND(AVG(r.power_kw), 1) AS mean_power
FROM readings r
JOIN machines m ON r.machine_id = m.machine_id
GROUP BY m.machine_type
ORDER BY mean_power DESC
"""
)
print(q5)
merged = readings.merge(machines, on="machine_id")
p5 = merged.groupby("machine_type")["power_kw"].mean().round(1).sort_values(ascending=False)
print(p5)
assert q5["mean_power"].tolist() == p5.tolist()
print()
# Query 6 - GROUP BY with two aggregates
print("== Query 6: incidents and cost by category ==")
q6 = sql(
"""
SELECT category, COUNT(*) AS n, ROUND(SUM(repair_cost_cad), 2) AS cost
FROM incidents
GROUP BY category
ORDER BY n DESC
"""
)
print(q6)
p6 = (
incidents.groupby("category")
.agg(n=("incident_id", "count"), cost=("repair_cost_cad", "sum"))
.round(2)
.sort_values("n", ascending=False)
)
print(p6)
assert q6["n"].tolist() == p6["n"].tolist()
assert q6["cost"].tolist() == p6["cost"].tolist()
print()
con.close()
print("All six queries give the same answer in SQL and in pandas.")Run it with python week02/exercise_2_solution.py. The last line of the output is the deliverable.
All systems — sqlite3.OperationalError: no such table: readings. The path is wrong, so SQLite created a new empty database. Delete the empty file it created and check you are in the kit root. sqlite3.connect never complains about a missing file.
All systems — sqlite3.OperationalError: no such column: chiller. You wrote text with double quotes. Use single quotes: 'chiller'.
All systems — AssertionError on Query 5. Your merge may have dropped rows. Print len(merged); it must be 14,600. Check that on="machine_id" is spelled right.
All systems — ValueError: The truth value of a Series is ambiguous. You wrote and between two conditions. pandas needs &, and brackets around each condition.
Windows only — sqlite3.OperationalError: unable to open database file. The file is open in another program, for example a database viewer. Close it and run again.
Linux and macOS only — sqlite3 module missing. Rare. Your Python was built without SQLite. Install python3 from your package manager, not from source, and rebuild the venv.