The lab kit of the course: https://github.com/hrhouma2/aiopsatlas-ml-data-diagnostics-labs-en
The reliability team asks: "Which sites have the most bearing incidents?" In the knowledge graph this is a path of three hops: category, incident, machine, site. You have no graph database. You have two CSV files and pandas. Each hop is one merge. You walk the path, answer the question, then find the most connected machine of the plant.
cd aiopsatlas-ml-data-diagnostics-labs-en
.\.venv\Scripts\Activate.ps1cd aiopsatlas-ml-data-diagnostics-labs-en
source .venv/bin/activateCreate week03/walk_the_graph.py and start it with:
import pandas as pd
pd.set_option("display.width", 120)data/graph/nodes.csv, 207 rows: node_id, label, name.
node_id,label,name
M001,Machine,M001
Montreal,Site,Montreal
pump,MachineType,pump
bearing_wear,FaultCategory,bearing_wear
INC-0001,Incident,INC-0001data/graph/edges.csv, 390 rows: source, relation, target. Read each row as a sentence.
source,relation,target
M001,LOCATED_AT,Toronto
M001,IS_A,pump
INC-0001,HAPPENED_ON,M001
INC-0001,HAS_CATEGORY,overheating| Relation | From | To | Count |
|---|---|---|---|
LOCATED_AT | Machine | Site | 40 |
IS_A | Machine | MachineType | 40 |
HAPPENED_ON | Incident | Machine | 155 |
HAS_CATEGORY | Incident | FaultCategory | 155 |
Load both files. Count nodes by label and edges by relation.
nodes = pd.read_csv("data/graph/nodes.csv")
edges = pd.read_csv("data/graph/edges.csv")
print("nodes:", len(nodes), "| edges:", len(edges))
print(nodes["label"].value_counts())
print(edges["relation"].value_counts())nodes: 207 | edges: 390
label
Incident 155
Machine 40
FaultCategory 5
MachineType 4
Site 3
relation
HAPPENED_ON 155
HAS_CATEGORY 155
LOCATED_AT 40
IS_A 40155 + 40 + 5 + 4 + 3 is 207. 155 + 155 + 40 + 40 is 390. The totals add up.
Look at M001 from both ends. Edges where it is the source, then edges where it is the target.
print(edges[edges["source"] == "M001"])
print(edges[edges["target"] == "M001"]) source relation target
0 M001 LOCATED_AT Toronto
40 M001 IS_A pump
source relation target
80 INC-0001 HAPPENED_ON M001
81 INC-0002 HAPPENED_ON M001
82 INC-0003 HAPPENED_ON M001
83 INC-0004 HAPPENED_ON M001
84 INC-0005 HAPPENED_ON M001As a source, M001 has two edges: its site and its type. As a target, five incidents point at it. Direction matters. If you only looked at source, you would think M001 never broke.
Split edges into four tables, one per relation. Rename source and target to say what they are. The function does it once for all four.
def relation(name, source_name, target_name):
part = edges[edges["relation"] == name]
return part.rename(columns={"source": source_name, "target": target_name})[[source_name, target_name]]
located_at = relation("LOCATED_AT", "machine", "site")
is_a = relation("IS_A", "machine", "machine_type")
happened_on = relation("HAPPENED_ON", "incident", "machine")
has_category = relation("HAS_CATEGORY", "incident", "category")
print(happened_on.head(3))
print(has_category.head(3)) incident machine
80 INC-0001 M001
81 INC-0002 M001
82 INC-0003 M001
incident category
235 INC-0001 overheating
236 INC-0002 bearing_wear
237 INC-0003 bearing_wearNow happened_on and has_category share a column, incident. happened_on and located_at share machine. Shared columns are where the merges will happen.
Merge happened_on with has_category on incident. Each row is now a path of two edges. Filter on M001.
two_hop = happened_on.merge(has_category, on="incident")
print(two_hop[two_hop["machine"] == "M001"]) incident machine category
0 INC-0001 M001 overheating
1 INC-0002 M001 bearing_wear
2 INC-0003 M001 bearing_wear
3 INC-0004 M001 leak
4 INC-0005 M001 electricalRead a row as a path: M001 had INC-0002, which has category bearing_wear. Five incidents, four different categories, two of them bearing wear.
Merge two_hop with located_at on machine. Then keep bearing_wear and count by site. This is the deliverable.
three_hop = two_hop.merge(located_at, on="machine")
print("rows:", len(three_hop))
bearing = three_hop[three_hop["category"] == "bearing_wear"]
print(bearing["site"].value_counts())
print(pd.crosstab(three_hop["site"], three_hop["category"]))rows: 155
site
Toronto 22
Montreal 18
Quebec City 8
category bearing_wear electrical leak overheating sensor_fault
site
Montreal 18 8 14 18 8
Quebec City 8 5 6 11 3
Toronto 22 10 8 10 6Still 155 rows: one per incident, the path did not lose or add any. Toronto has 22 bearing incidents with 13 machines. Montreal has 18 with 20 machines. Toronto's pumps are the ones to look at. The crosstab gives the whole picture in one table.
The degree of a node is the number of edges that touch it. Count every appearance in source and target, then keep the machines.
degree = pd.concat([edges["source"], edges["target"]]).value_counts()
machine_degree = degree[degree.index.isin(located_at["machine"])]
print(machine_degree.head(3))
top = machine_degree.index[0]
print(f"{top}: {int(happened_on['machine'].eq(top).sum())} incidents + LOCATED_AT + IS_A = {int(machine_degree.iloc[0])} edges")M017 10
M020 9
M030 9
M017: 8 incidents + LOCATED_AT + IS_A = 10 edgesM017 is a compressor in Quebec City, installed in 2014. Eight incidents in one year, plus its two fixed edges. The oldest machines break the most. You will use that in Week 4 when you build features.
three_hop have, and why must it be that number?edges[edges["source"] == "M001"] show no incident?M017, and how is it made up?INC-0001 HAPPENED_ON M001. M001 is the target of those edges, not the source.HAPPENED_ON edges pointing at it, plus LOCATED_AT and IS_A.Add a fourth hop with is_a and build the crosstab of machine_type by category. Which type has the most leak incidents? Compressors should lead with 11. Then find the category that never happened on a conveyor in Montreal, if there is one.
week03/exercise_3_solution.py in the kit"""Week 3, Exercise 3 - Graph traversal with pandas merges.
Run from the kit root, with the venv active:
python week03/exercise_3_solution.py
Walks the NorthPeak knowledge graph (data/graph/nodes.csv, edges.csv) with
nothing but pandas merges:
1. load nodes and edges, count them by label and by relation;
2. the neighbours of one machine, M001;
3. one hop per relation: four small tables;
4. a 2-hop path machine -> incident -> category, for M001;
5. a 3-hop path category <- incident -> machine -> site:
which sites have the most bearing_wear incidents;
6. the most connected node (degree).
"""
import pandas as pd
pd.set_option("display.width", 120)
# Step 1 - load
print("== Step 1: nodes and edges ==")
nodes = pd.read_csv("data/graph/nodes.csv")
edges = pd.read_csv("data/graph/edges.csv")
print("nodes:", len(nodes), "| edges:", len(edges))
print(nodes["label"].value_counts())
print(edges["relation"].value_counts())
print()
# Step 2 - neighbours of M001
print("== Step 2: neighbours of M001 ==")
print(edges[edges["source"] == "M001"])
print(edges[edges["target"] == "M001"])
print()
# Step 3 - one table per relation
print("== Step 3: one table per relation ==")
def relation(name, source_name, target_name):
"""Keep one relation and rename source/target to readable names."""
part = edges[edges["relation"] == name]
return part.rename(columns={"source": source_name, "target": target_name})[[source_name, target_name]]
located_at = relation("LOCATED_AT", "machine", "site")
is_a = relation("IS_A", "machine", "machine_type")
happened_on = relation("HAPPENED_ON", "incident", "machine")
has_category = relation("HAS_CATEGORY", "incident", "category")
print(happened_on.head(3))
print(has_category.head(3))
print()
# Step 4 - 2 hops: machine -> incident -> category
print("== Step 4: 2-hop path for M001 ==")
two_hop = happened_on.merge(has_category, on="incident")
print(two_hop[two_hop["machine"] == "M001"])
print()
# Step 5 - 3 hops: category <- incident -> machine -> site
print("== Step 5: bearing_wear incidents by site ==")
three_hop = two_hop.merge(located_at, on="machine")
print("rows:", len(three_hop))
bearing = three_hop[three_hop["category"] == "bearing_wear"]
print(bearing["site"].value_counts())
print()
print(pd.crosstab(three_hop["site"], three_hop["category"]))
print()
# Step 6 - degree
print("== Step 6: the most connected nodes ==")
degree = pd.concat([edges["source"], edges["target"]]).value_counts()
machine_degree = degree[degree.index.isin(located_at["machine"])]
print(machine_degree.head(3))
top = machine_degree.index[0]
print(f"{top}: {int(happened_on['machine'].eq(top).sum())} incidents + LOCATED_AT + IS_A = {int(machine_degree.iloc[0])} edges")Run it with python week03/exercise_3_solution.py. Step 5 prints the deliverable table; Step 6 prints M017.
All systems — rows: 0 after a merge. The shared column has different names on the two sides, for example machine and machine_id. Print .columns of both tables and check the on= value.
All systems — rows: 6200 or more after a merge. You merged on a column that is not a key, or you merged edges with itself without filtering the relation first. Build the four small tables of Step 3 first.
All systems — KeyError: 'incident'. The rename did not apply, or you selected the columns before renaming. Follow the order of the relation function: filter, rename, select.
All systems — value_counts() shows all five categories instead of three sites. You counted category instead of site, or forgot the bearing_wear filter.
Windows only — the crosstab wraps over several lines. Add pd.set_option("display.width", 120) at the top of the script.
Linux and macOS only — python: command not found. Use python3 to create the venv. Once active, python works.