diff --git a/examples/factory_graph_dashboard/app.py b/examples/factory_graph_dashboard/app.py new file mode 100644 index 000000000..73c1f563f --- /dev/null +++ b/examples/factory_graph_dashboard/app.py @@ -0,0 +1,267 @@ +import streamlit as st +from neo4j import GraphDatabase +import pandas as pd +import plotly.express as px + +URI = "bolt://localhost:7687" +USER = "neo4j" +PASSWORD = "password123" + +driver = GraphDatabase.driver(URI, auth=(USER, PASSWORD)) + +st.set_page_config( + page_title="Factory Graph Dashboard", + layout="wide" +) + +st.title(" Factory Production Knowledge Graph Dashboard") + + +def run_query(query): + with driver.session() as session: + result = session.run(query) + return [dict(record) for record in result] + + +# ------------------------------- +# KPI SECTION +# ------------------------------- + +st.header("Factory KPIs") + +capacity_query = """ +MATCH (c:Capacity) +RETURN +sum(c.total_capacity) as capacity, +sum(c.total_planned) as planned, +sum(c.deficit) as deficit +""" + +capacity_data = run_query(capacity_query)[0] + +col1, col2, col3 = st.columns(3) + +col1.metric("Total Capacity Hours", capacity_data["capacity"]) +col2.metric("Total Planned Hours", capacity_data["planned"]) +col3.metric("Net Deficit", capacity_data["deficit"]) +# ------------------------------- +# EXECUTIVE SUMMARY +# ------------------------------- + +st.header("Executive Summary") + +summary_query = """ +MATCH (p:Project)-[r:GOES_THROUGH]->(s:Station) +WHERE r.actual_hours > r.planned_hours +RETURN +count(DISTINCT p) AS overloaded_projects, +max(r.actual_hours - r.planned_hours) AS max_overrun +""" + +summary = run_query(summary_query)[0] + +c1, c2 = st.columns(2) + +c1.metric( + "Overloaded Projects", + summary["overloaded_projects"] +) + +c2.metric( + "Max Overrun Hours", + summary["max_overrun"] +) + +# ------------------------------- +# BOTTLENECK ANALYSIS +# ------------------------------- + +st.header("Station Bottleneck Analysis") + +bottleneck_query = """ +MATCH (p:Project)-[r:GOES_THROUGH]->(s:Station) +RETURN +s.name as station, +sum(r.planned_hours) as planned, +sum(r.actual_hours) as actual +ORDER BY actual DESC +""" + +bottleneck = pd.DataFrame(run_query(bottleneck_query)) + +fig = px.bar( + bottleneck, + x="station", + y=["planned", "actual"], + barmode="group", + title="Planned vs Actual Hours by Station" +) + +st.plotly_chart(fig, use_container_width=True) + + +# ------------------------------- +# WORKER COVERAGE +# ------------------------------- + +st.header("Worker Coverage") + +worker_query = """ +MATCH (w:Worker)-[:CAN_COVER]->(s:Station) +RETURN +w.name as worker, +collect(s.code) as stations +""" + +workers = run_query(worker_query) + +for worker in workers: + st.write(f"**{worker['worker']}** → {', '.join(worker['stations'])}") + +# ------------------------------- +# WORKER COVERAGE HEATMAP +# ------------------------------- + +st.header("Worker Coverage Heatmap") + +coverage_query = """ +MATCH (w:Worker)-[:CAN_COVER]->(s:Station) +RETURN +s.code AS station, +count(w) AS worker_count +ORDER BY station +""" + +coverage_df = pd.DataFrame(run_query(coverage_query)) + +if not coverage_df.empty: + + fig = px.bar( + coverage_df, + x="station", + y="worker_count", + title="Certified Worker Coverage by Station" + ) + + st.plotly_chart(fig, use_container_width=True) + + weak = coverage_df[ + coverage_df["worker_count"] <= 1 + ] + + if not weak.empty: + st.error("Single Point Failure Stations") + st.dataframe(weak) + +# ------------------------------- +# PROJECT OVERLOAD +# ------------------------------- + +st.header("Overloaded Projects") + +overload_query = """ +MATCH (p:Project)-[r:GOES_THROUGH]->(s:Station) +WHERE r.actual_hours > r.planned_hours * 1.1 +RETURN +p.name as project, +s.name as station, +r.planned_hours as planned, +r.actual_hours as actual +""" + +overloaded = pd.DataFrame(run_query(overload_query)) + +st.dataframe(overloaded, use_container_width=True) +if not overloaded.empty: + + sample = overloaded.iloc[0] + + st.info( + f""" +Why flagged? + +Project: {sample['project']} +Station: {sample['station']} + +Planned Hours: {sample['planned']} +Actual Hours: {sample['actual']} + +Reason: +Actual effort exceeded planned threshold by >10%. +Operational overload detected. +""" + ) + + + +# ------------------------------- +# RISK ANALYSIS +# ------------------------------- + +st.header("Operational Risk Analysis") + +if not overloaded.empty: + + overloaded["variance_percent"] = ( + (overloaded["actual"] - overloaded["planned"]) + / overloaded["planned"] + ) * 100 + + def classify_risk(v): + if v >= 30: + return "HIGH" + elif v >= 15: + return "MEDIUM" + return "LOW" + + overloaded["risk_level"] = overloaded["variance_percent"].apply(classify_risk) + + st.dataframe( + overloaded[ + ["project", "station", "planned", "actual", "variance_percent", "risk_level"] + ], + use_container_width=True + ) + + high_risk_count = len( + overloaded[overloaded["risk_level"] == "HIGH"] + ) + + medium_risk_count = len( + overloaded[overloaded["risk_level"] == "MEDIUM"] + ) + + low_risk_count = len( + overloaded[overloaded["risk_level"] == "LOW"] + ) + + c1, c2, c3 = st.columns(3) + + c1.error(f"HIGH RISK: {high_risk_count}") + c2.warning(f"MEDIUM RISK: {medium_risk_count}") + c3.success(f"LOW RISK: {low_risk_count}") + + +# ------------------------------- +# STATION SEARCH TOOL +# ------------------------------- + +st.header("Station Search") + +station_code = st.text_input("Enter Station Code") + +if station_code: + query = f""" + MATCH (w:Worker)-[:CAN_COVER]->(s:Station {{code: '{station_code}'}}) + RETURN w.name as worker, w.role as role + """ + + results = pd.DataFrame(run_query(query)) + + if not results.empty: + st.dataframe(results) + else: + st.warning("No workers found") + + +driver.close() diff --git a/examples/factory_graph_dashboard/data/factory_capacity.csv b/examples/factory_graph_dashboard/data/factory_capacity.csv new file mode 100644 index 000000000..795ff52f0 --- /dev/null +++ b/examples/factory_graph_dashboard/data/factory_capacity.csv @@ -0,0 +1,9 @@ +week,own_staff_count,hired_staff_count,own_hours,hired_hours,overtime_hours,total_capacity,total_planned,deficit +w1,10,2,400,80,0,480,612,-132 +w2,10,2,400,80,40,520,645,-125 +w3,10,2,400,80,0,480,398,82 +w4,10,2,400,80,20,500,550,-50 +w5,10,2,400,80,30,510,480,30 +w6,9,2,360,80,0,440,520,-80 +w7,10,2,400,80,40,520,600,-80 +w8,10,2,400,80,20,500,470,30 \ No newline at end of file diff --git a/examples/factory_graph_dashboard/data/factory_production.csv b/examples/factory_graph_dashboard/data/factory_production.csv new file mode 100644 index 000000000..ca6ce43e1 --- /dev/null +++ b/examples/factory_graph_dashboard/data/factory_production.csv @@ -0,0 +1,69 @@ +project_id,project_number,project_name,product_type,unit,quantity,unit_factor,station_code,station_name,etapp,bop,week,planned_hours,actual_hours,completed_units +P01,4501,Stålverket Borås,IQB,meter,600,1.77,011,FS IQB,ET1,BOP1,w1,48.0,45.2,28 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,012,Förmontering IQB,ET1,BOP1,w1,32.0,35.5,25 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,013,Montering IQB,ET1,BOP1,w1,28.0,26.0,22 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,014,Svets o montage IQB,ET1,BOP1,w1,35.0,38.2,20 +P01,4501,Stålverket Borås,SB,styck,40,4.0,018,SB B/F-hall,ET1,BOP1,w1,16.0,14.5,4 +P01,4501,Stålverket Borås,SP,styck,180,2.0,019,SP B/F-hall,ET1,BOP1,w1,12.0,13.0,7 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,011,FS IQB,ET1,BOP1,w2,48.0,50.0,32 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,012,Förmontering IQB,ET1,BOP1,w2,32.0,30.0,28 +P01,4501,Stålverket Borås,IQP,styck,90,2.80,015,Montering IQP,ET1,BOP2,w2,25.0,28.0,9 +P01,4501,Stålverket Borås,SR,styck,8,45.0,021,SR B/F-hall,ET1,BOP2,w2,40.0,42.0,1 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,011,FS IQB,ET1,BOP1,w1,30.0,28.0,20 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,012,Förmontering IQB,ET1,BOP1,w1,22.0,24.5,18 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,013,Montering IQB,ET1,BOP1,w1,18.0,17.0,16 +P02,4502,Kontorshus Mölndal,IQP,styck,70,2.70,015,Montering IQP,ET1,BOP1,w1,19.0,21.0,7 +P02,4502,Kontorshus Mölndal,SD,styck,30,3.00,018,SB B/F-hall,ET1,BOP1,w1,9.0,8.5,3 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,011,FS IQB,ET1,BOP1,w2,30.0,32.0,24 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,014,Svets o montage IQB,ET1,BOP1,w2,25.0,23.0,20 +P02,4502,Kontorshus Mölndal,SP,styck,120,1.75,019,SP B/F-hall,ET1,BOP2,w2,14.0,15.5,8 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,011,FS IQB,ET1,BOP1,w1,72.0,70.0,40 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,012,Förmontering IQB,ET1,BOP1,w1,48.0,52.0,35 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,013,Montering IQB,ET1,BOP1,w1,38.0,36.5,30 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,014,Svets o montage IQB,ET1,BOP1,w1,42.0,48.0,28 +P03,4503,Lagerhall Jönköping,SB,styck,60,6.00,018,SB B/F-hall,ET1,BOP1,w1,36.0,38.0,6 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,011,FS IQB,ET1,BOP1,w2,72.0,75.0,45 +P03,4503,Lagerhall Jönköping,IQP,styck,110,2.90,015,Montering IQP,ET1,BOP2,w2,32.0,30.0,11 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,016,Gjutning,ET1,BOP2,w2,28.0,35.0,8 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,017,Målning,ET1,BOP2,w3,24.0,22.0,20 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,011,FS IQB,ET1,BOP1,w1,38.0,36.0,24 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,012,Förmontering IQB,ET1,BOP1,w1,25.0,27.0,20 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,013,Montering IQB,ET1,BOP1,w1,20.0,19.0,18 +P04,4504,Parkering Helsingborg,IQP,styck,55,2.85,015,Montering IQP,ET1,BOP1,w1,16.0,18.0,6 +P04,4504,Parkering Helsingborg,SB,styck,25,7.50,018,SB B/F-hall,ET1,BOP1,w1,19.0,22.0,3 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,011,FS IQB,ET1,BOP1,w2,38.0,40.0,28 +P04,4504,Parkering Helsingborg,SP,styck,100,2.00,019,SP B/F-hall,ET1,BOP2,w2,12.0,11.0,6 +P04,4504,Parkering Helsingborg,SR,styck,12,120.0,021,SR B/F-hall,ET1,BOP2,w2,60.0,65.0,1 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,011,FS IQB,ET2,BOP3,w1,95.0,90.0,50 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,012,Förmontering IQB,ET2,BOP3,w1,65.0,68.0,42 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,013,Montering IQB,ET2,BOP3,w1,50.0,48.0,38 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,014,Svets o montage IQB,ET2,BOP3,w1,58.0,62.0,35 +P05,4505,Sjukhus Linköping ET2,IQP,styck,150,2.88,015,Montering IQP,ET2,BOP3,w1,30.0,33.0,10 +P05,4505,Sjukhus Linköping ET2,SB,styck,50,5.00,018,SB B/F-hall,ET2,BOP3,w1,25.0,28.0,5 +P05,4505,Sjukhus Linköping ET2,SD,styck,45,2.75,018,SB B/F-hall,ET2,BOP3,w1,12.0,11.5,4 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,011,FS IQB,ET2,BOP3,w2,95.0,98.0,55 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,016,Gjutning,ET2,BOP3,w2,35.0,40.0,12 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,017,Målning,ET2,BOP3,w2,28.0,26.0,25 +P05,4505,Sjukhus Linköping ET2,SR,styck,20,274.0,021,SR B/F-hall,ET2,BOP3,w3,120.0,115.0,2 +P06,4506,Skola Uppsala,IQB,meter,500,1.60,011,FS IQB,ET1,BOP1,w2,40.0,38.0,26 +P06,4506,Skola Uppsala,IQB,meter,500,1.60,012,Förmontering IQB,ET1,BOP1,w2,28.0,30.0,22 +P06,4506,Skola Uppsala,IQB,meter,500,1.60,013,Montering IQB,ET1,BOP1,w2,22.0,20.0,18 +P06,4506,Skola Uppsala,IQP,styck,80,2.75,015,Montering IQP,ET1,BOP1,w2,22.0,24.0,8 +P06,4506,Skola Uppsala,SB,styck,35,4.50,018,SB B/F-hall,ET1,BOP1,w2,16.0,18.0,4 +P06,4506,Skola Uppsala,SP,styck,140,1.50,019,SP B/F-hall,ET1,BOP2,w3,14.0,12.0,10 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,011,FS IQB,ET1,BOP1,w1,45.0,42.0,22 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,012,Förmontering IQB,ET1,BOP1,w1,30.0,33.0,18 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,014,Svets o montage IQB,ET1,BOP1,w1,35.0,32.0,16 +P07,4507,Idrottshall Västerås,SB,styck,45,3.50,018,SB B/F-hall,ET1,BOP1,w1,16.0,18.0,5 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,011,FS IQB,ET1,BOP1,w2,45.0,48.0,26 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,016,Gjutning,ET1,BOP2,w2,20.0,22.0,5 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,017,Målning,ET1,BOP2,w3,18.0,16.0,15 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,011,FS IQB,ET1,BOP1,w1,65.0,62.0,36 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,012,Förmontering IQB,ET1,BOP1,w1,42.0,45.0,30 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,013,Montering IQB,ET1,BOP1,w1,35.0,38.0,25 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,014,Svets o montage IQB,ET1,BOP1,w1,40.0,44.0,22 +P08,4508,Bro E6 Halmstad,SP,styck,200,2.50,019,SP B/F-hall,ET1,BOP1,w1,20.0,18.0,8 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,011,FS IQB,ET1,BOP1,w2,65.0,68.0,42 +P08,4508,Bro E6 Halmstad,IQP,styck,95,2.93,015,Montering IQP,ET1,BOP2,w2,28.0,30.0,10 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,016,Gjutning,ET1,BOP2,w3,22.0,25.0,8 +P08,4508,Bro E6 Halmstad,SR,styck,15,180.0,021,SR B/F-hall,ET1,BOP2,w3,90.0,85.0,2 \ No newline at end of file diff --git a/examples/factory_graph_dashboard/data/factory_workers.csv b/examples/factory_graph_dashboard/data/factory_workers.csv new file mode 100644 index 000000000..3110285cc --- /dev/null +++ b/examples/factory_graph_dashboard/data/factory_workers.csv @@ -0,0 +1,15 @@ +worker_id,name,role,primary_station,can_cover_stations,certifications,hours_per_week,type +W01,Erik Lindberg,Operator,011,"011,012","MIG/MAG,TIG,ISO 9606",40,permanent +W02,Anna Berg,Operator,011,"011,014","MIG/MAG,TIG",40,permanent +W03,Lars Jensen,Operator,012,"012,013","Surface treatment,CE marking",40,permanent +W04,Maria Stone,Operator,013,"013","Blasting,Surface protection",40,permanent +W05,Johan Peters,Operator,014,"014,015","Hydraulics,Mechanics,Crane",40,permanent +W06,Karen Nilsen,Inspector,015,"015","SIS,SS-EN 1090,NDT",40,permanent +W07,Per Hansen,Operator,016,"016,017","Casting,Formwork",40,permanent +W08,Sofia Arden,Operator,017,"017","Surface treatment,Spray painting",40,permanent +W09,Magnus Stone,Operator,018,"018,019","Sheet metal,Assembly",40,permanent +W10,Elin Frank,Operator,019,"019,018","Assembly,Welding",32,permanent +W11,Victor Elm,Foreman,all,"011,012,013,014,015,016,017,018,019,021","Leadership,CE,ISO 9001",45,permanent +W12,Lena Dale,Quality Manager,015,"015","ISO 9001,SS-EN 1090,Audit",40,permanent +W13,Ahmed Hassan,Operator,011,"011","MIG/MAG",40,hired +W14,Petra Steen,Operator,012,"012,013","Surface treatment",40,hired \ No newline at end of file diff --git a/examples/factory_graph_dashboard/graph_queries.py b/examples/factory_graph_dashboard/graph_queries.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/factory_graph_dashboard/ingest_data.py b/examples/factory_graph_dashboard/ingest_data.py new file mode 100644 index 000000000..ea3e8c1d0 --- /dev/null +++ b/examples/factory_graph_dashboard/ingest_data.py @@ -0,0 +1,131 @@ +from neo4j import GraphDatabase +import pandas as pd + +URI = "bolt://localhost:7687" +USER = "neo4j" +PASSWORD = "password123" + +driver = GraphDatabase.driver(URI, auth=(USER, PASSWORD)) + +production = pd.read_csv("data/factory_production.csv") +workers = pd.read_csv("data/factory_workers.csv") +capacity = pd.read_csv("data/factory_capacity.csv") + + +def clear_db(tx): + tx.run("MATCH (n) DETACH DELETE n") + + +def create_production(tx, row): + tx.run(""" + MERGE (p:Project { + id: $project_id, + number: $project_number, + name: $project_name + }) + + MERGE (prod:Product { + type: $product_type, + unit: $unit + }) + + MERGE (s:Station { + code: $station_code, + name: $station_name + }) + + MERGE (w:Week { + name: $week + }) + + MERGE (p)-[:USES_PRODUCT { + quantity: $quantity, + unit_factor: $unit_factor + }]->(prod) + + MERGE (p)-[:GOES_THROUGH { + planned_hours: $planned_hours, + actual_hours: $actual_hours, + completed_units: $completed_units, + etapp: $etapp, + bop: $bop + }]->(s) + + MERGE (s)-[:ACTIVE_IN]->(w) + """, **row) + + +def create_worker(tx, row): + tx.run(""" + MERGE (worker:Worker { + id: $worker_id, + name: $name, + role: $role, + type: $type, + hours_per_week: $hours_per_week + }) + + MERGE (primary:Station { + code: $primary_station + }) + + MERGE (worker)-[:PRIMARY_AT]->(primary) + """, **row) + + cover_list = str(row["can_cover_stations"]).split(",") + + for station in cover_list: + tx.run(""" + MATCH (worker:Worker {id: $worker_id}) + MERGE (s:Station {code: $station}) + MERGE (worker)-[:CAN_COVER]->(s) + """, worker_id=row["worker_id"], station=station.strip()) + + certs = str(row["certifications"]).split(",") + + for cert in certs: + tx.run(""" + MATCH (worker:Worker {id: $worker_id}) + MERGE (c:Certification {name: $cert}) + MERGE (worker)-[:HAS_CERTIFICATION]->(c) + """, worker_id=row["worker_id"], cert=cert.strip()) + + +def create_capacity(tx, row): + tx.run(""" + MERGE (w:Week {name: $week}) + + MERGE (cap:Capacity { + week: $week, + own_staff_count: $own_staff_count, + hired_staff_count: $hired_staff_count, + own_hours: $own_hours, + hired_hours: $hired_hours, + overtime_hours: $overtime_hours, + total_capacity: $total_capacity, + total_planned: $total_planned, + deficit: $deficit + }) + + MERGE (w)-[:HAS_CAPACITY]->(cap) + """, **row) + + +with driver.session() as session: + print("Clearing database...") + session.execute_write(clear_db) + + print("Loading production data...") + for _, row in production.iterrows(): + session.execute_write(create_production, row.to_dict()) + + print("Loading workers...") + for _, row in workers.iterrows(): + session.execute_write(create_worker, row.to_dict()) + + print("Loading capacity...") + for _, row in capacity.iterrows(): + session.execute_write(create_capacity, row.to_dict()) + +print("Data loaded successfully.") +driver.close() diff --git a/examples/factory_graph_dashboard/requirements.txt b/examples/factory_graph_dashboard/requirements.txt new file mode 100644 index 000000000..f3180f5e5 --- /dev/null +++ b/examples/factory_graph_dashboard/requirements.txt @@ -0,0 +1,4 @@ +streamlit +pandas +neo4j +plotly diff --git a/examples/factory_graph_dashboard/screenshots/dashboard-kpi-overview.png b/examples/factory_graph_dashboard/screenshots/dashboard-kpi-overview.png new file mode 100644 index 000000000..b49dd50fc Binary files /dev/null and b/examples/factory_graph_dashboard/screenshots/dashboard-kpi-overview.png differ diff --git a/examples/factory_graph_dashboard/screenshots/neo4j-graph-schema.png b/examples/factory_graph_dashboard/screenshots/neo4j-graph-schema.png new file mode 100644 index 000000000..97db358fd Binary files /dev/null and b/examples/factory_graph_dashboard/screenshots/neo4j-graph-schema.png differ diff --git a/examples/factory_graph_dashboard/screenshots/operational-risk-analysis.png b/examples/factory_graph_dashboard/screenshots/operational-risk-analysis.png new file mode 100644 index 000000000..6231bf2c0 Binary files /dev/null and b/examples/factory_graph_dashboard/screenshots/operational-risk-analysis.png differ diff --git a/examples/factory_graph_dashboard/screenshots/overloaded-projects-explainability.png b/examples/factory_graph_dashboard/screenshots/overloaded-projects-explainability.png new file mode 100644 index 000000000..f03e7d70e Binary files /dev/null and b/examples/factory_graph_dashboard/screenshots/overloaded-projects-explainability.png differ diff --git a/examples/factory_graph_dashboard/screenshots/planned-vs-actual-stations.png b/examples/factory_graph_dashboard/screenshots/planned-vs-actual-stations.png new file mode 100644 index 000000000..162657616 Binary files /dev/null and b/examples/factory_graph_dashboard/screenshots/planned-vs-actual-stations.png differ diff --git a/examples/factory_graph_dashboard/screenshots/station-search-016.png b/examples/factory_graph_dashboard/screenshots/station-search-016.png new file mode 100644 index 000000000..35436f742 Binary files /dev/null and b/examples/factory_graph_dashboard/screenshots/station-search-016.png differ diff --git a/examples/factory_graph_dashboard/screenshots/worker-coverage-risk.png b/examples/factory_graph_dashboard/screenshots/worker-coverage-risk.png new file mode 100644 index 000000000..db9980873 Binary files /dev/null and b/examples/factory_graph_dashboard/screenshots/worker-coverage-risk.png differ diff --git a/submissions/srishtigusainn/level6/README.md b/submissions/srishtigusainn/level6/README.md new file mode 100644 index 000000000..e4cfc71a3 --- /dev/null +++ b/submissions/srishtigusainn/level6/README.md @@ -0,0 +1,216 @@ +# Level 6 — Factory Production Knowledge Graph Dashboard + +## Overview + +This project transforms factory production planning data into an interactive knowledge graph dashboard using Neo4j and Streamlit. + +The dataset models a Swedish steel fabrication company's production workflow across: + +- 8 construction projects +- 9 production stations +- 13+ workers +- weekly capacity planning +- operational bottleneck analysis + +Instead of relying on spreadsheets, this implementation uses graph relationships to model production dependencies, staffing coverage, overload risk, and operational intelligence. + +--- + +## Architecture + +### Tech Stack + +- Neo4j Desktop (graph database) +- Cypher (graph queries) +- Python 3 +- Streamlit +- Pandas +- Plotly + +--- + +## Graph Schema + +### Node Types + +- `Project` +- `Product` +- `Station` +- `Worker` +- `Week` +- `Capacity` +- `Certification` + +### Relationships + +- `(:Project)-[:USES_PRODUCT]->(:Product)` +- `(:Project)-[:GOES_THROUGH {planned_hours, actual_hours, completed_units}]->(:Station)` +- `(:Worker)-[:PRIMARY_AT]->(:Station)` +- `(:Worker)-[:CAN_COVER]->(:Station)` +- `(:Worker)-[:HAS_CERTIFICATION]->(:Certification)` +- `(:Project)-[:ACTIVE_IN]->(:Week)` +- `(:Capacity)-[:HAS_CAPACITY]->(:Week)` + +--- + +## Features Implemented + +### 1. Executive KPI Dashboard + +Displays: + +- Total factory capacity +- Total planned production hours +- Net deficit +- overloaded project count +- max operational overrun + +--- + +### 2. Planned vs Actual Station Analytics + +Interactive comparison of: + +- planned production hours +- actual production hours +- station-level bottleneck identification + +--- + +### 3. Worker Coverage Intelligence + +Graph-powered staffing analysis showing: + +- workers by station +- cross-station coverage +- single-point failure detection +- staffing risk visibility + +--- + +### 4. Overload Detection with Explainability + +Projects exceeding planning thresholds are flagged. + +Each alert explains: + +- affected project +- station +- planned hours +- actual hours +- variance reason + +This adds explainability rather than black-box alerting. + +--- + +### 5. Operational Risk Classification + +Risk levels: + +- LOW +- MEDIUM +- HIGH + +Calculated from variance percentage between planned vs actual execution. + +--- + +### 6. Station Search Tool + +Interactive lookup for: + +- station code +- available certified workers +- staffing fallback coverage + +Example: +Station 016 (Gjutning) + +--- + +## Screenshots + +### Neo4j Knowledge Graph + +![Neo4j Graph](../../examples/factory_graph_dashboard/screenshots/neo4j-graph-schema.png) + +--- + +### KPI Dashboard + +![KPI Dashboard](../../examples/factory_graph_dashboard/screenshots/dashboard-kpi-overview.png) + +--- + +### Planned vs Actual Analysis + +![Planned vs Actual](../../examples/factory_graph_dashboard/screenshots/planned-vs-actual-stations.png) + +--- + +### Worker Coverage Risk + +![Worker Coverage](../../examples/factory_graph_dashboard/screenshots/worker-coverage-risk.png) + +--- + +### Explainability Layer + +![Explainability](../../examples/factory_graph_dashboard/screenshots/overloaded-projects-explainability.png) + +--- + +### Operational Risk Dashboard + +![Risk Dashboard](../../examples/factory_graph_dashboard/screenshots/operational-risk-analysis.png) + +--- + +### Station Search + +![Station Search](../../examples/factory_graph_dashboard/screenshots/station-search-016.png) + +--- + +## Business Value + +This replaces spreadsheet-driven operational tracking with: + +- graph-based dependency modeling +- explainable overload alerts +- workforce resilience analysis +- operational decision intelligence +- searchable production knowledge graph + +--- + +## Run Instructions + +### Install dependencies + +```bash +pip install streamlit pandas plotly neo4j +``` + +### Start Neo4j + +Run local Neo4j Desktop instance. + +### Load graph + +```bash +python3 ingest_data.py +``` + +### Launch dashboard + +```bash +streamlit run app.py +``` + +--- + +## Submission + +Level 6 Knowledge Graph Dashboard