Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Backend/dummy_data/supplier_to_warehouse_recp.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"Supplier": [

"Supplier": [
{"supplier_id": "SUP1", "supplier_name": "Supplier A"},
{"supplier_id": "SUP2", "supplier_name": "Supplier B"}
],
Expand Down
46 changes: 23 additions & 23 deletions Backend/dummy_data/warehouse_to_customer.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
{
"Supplier": [
{"supplier_id": "SUP1", "supplier_name": "Supplier A"},
{"supplier_id": "SUP2", "supplier_name": "Supplier B"}
],
"Product": [
{"product_id": "PROD1", "rfid_tag": "RFID123", "product_name": "Laptop", "status": "available", "supplier_id": "SUP1"},
{"product_id": "PROD2", "rfid_tag": "RFID124", "product_name": "Phone", "status": "available", "supplier_id": "SUP1"},
{"product_id": "PROD3", "rfid_tag": "RFID125", "product_name": "Tablet", "status": "sold", "supplier_id": "SUP2"}
],
"Shelf": [
{"shelf_id": "SHELF1", "shelf_location": "Aisle 1, Shelf 2"},
{"shelf_id": "SHELF2", "shelf_location": "Aisle 3, Shelf 1"}
],
"ShelfInventory": [
{"shelf_inventory_id": "SHELF_INV1", "shelf_id": "SHELF1", "product_id": "PROD1", "added_timestamp": "2025-03-03T17:00:00", "removed_timestamp": "2025-03-05T12:30:00"},
{"shelf_inventory_id": "SHELF_INV2", "shelf_id": "SHELF1", "product_id": "PROD2", "added_timestamp": "2025-03-03T18:00:00", "removed_timestamp": null},
{"shelf_inventory_id": "SHELF_INV3", "shelf_id": "SHELF2", "product_id": "PROD3", "added_timestamp": "2025-03-04T10:00:00", "removed_timestamp": "2025-03-05T12:00:00"}
],
"Sale": [
{"sale_id": "SALE1", "product_id": "PROD1", "sale_timestamp": "2025-03-05T12:30:00"},
{"sale_id": "SALE2", "product_id": "PROD3", "sale_timestamp": "2025-03-05T12:00:00"}
]
}
"Supplier": [
{"supplier_id": "SUP1", "supplier_name": "Supplier A"},
{"supplier_id": "SUP2", "supplier_name": "Supplier B"}
],
"Product": [
{"product_id": "PROD1", "rfid_tag": "RFID123", "product_name": "Laptop", "status": "available", "supplier_id": "SUP1"},
{"product_id": "PROD2", "rfid_tag": "RFID124", "product_name": "Phone", "status": "available", "supplier_id": "SUP1"},
{"product_id": "PROD3", "rfid_tag": "RFID125", "product_name": "Tablet", "status": "sold", "supplier_id": "SUP2"}
],
"Shelf": [
{"shelf_id": "SHELF1", "shelf_location": "Aisle 1, Shelf 2"},
{"shelf_id": "SHELF2", "shelf_location": "Aisle flags3, Shelf 1"}
],
"ShelfInventory": [
{"shelf_inventory_id": "SHELF_INV1", "shelf_id": "SHELF1", "product_id": "PROD1", "added_timestamp": "2025-03-03T17:00:00", "removed_timestamp": "2025-03-05T12:30:00"},
{"shelf_inventory_id": "SHELF_INV2", "shelf_id": "SHELF1", "product_id": "PROD2", "added_timestamp": "2025-03-03T18:00:00", "removed_timestamp": "2025-03-05T12:15:00"},
{"shelf_inventory_id": "SHELF_INV3", "shelf_id": "SHELF2", "product_id": "PROD3", "added_timestamp": "2025-03-04T10:00:00", "removed_timestamp": "2025-03-05T12:00:00"}
],
"Sale": [
{"sale_id": "SALE1", "product_id": "PROD1", "sale_timestamp": "2025-03-05T12:30:00"},
{"sale_id": "SALE2", "product_id": "PROD2", "sale_timestamp": "2025-03-05T12:15:00"}
]
}
3 changes: 0 additions & 3 deletions Backend/src/Db/database_management.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from mako.parsetree import Expression
from sqlalchemy import False_
from sqlalchemy.sql.functions import mode
from sqlmodel.ext.asyncio.session import AsyncSession
import logging

Expand Down
Empty file added Backend/src/ml/__init__.py
Empty file.
30 changes: 30 additions & 0 deletions Backend/src/ml/anomaly_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Backend/src/ml/anomaly_detection.py
import pandas as pd
from sklearn.ensemble import IsolationForest
from .data_fetcher import fetch_recent_data
from Backend.src.Db.database_management import DatabaseManagement

async def detect_anomalies(db: DatabaseManagement, interval_seconds: int = 60, contamination: float = 0.1):
"""Detect anomalies in recent inventory and sales data using Isolation Forest."""
time_window_minutes = interval_seconds / 60

df = await fetch_recent_data(db, time_window_minutes)

if df.empty:
return df

features = ["inventory_count", "inventory_change", "sales_count"]
X = df[features].values

iso_forest = IsolationForest(
contamination=contamination,
random_state=42,
n_estimators=100
)
iso_forest.fit(X)

df["anomaly_label"] = iso_forest.predict(X)
df["anomaly_score"] = iso_forest.decision_function(X)
df["is_anomaly"] = df["anomaly_label"].apply(lambda x: True if x == -1 else False)

return df
75 changes: 75 additions & 0 deletions Backend/src/ml/data_fetcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Backend/src/ml/data_fetcher.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from sqlalchemy.sql import text
from Backend.src.Db.database_management import DatabaseManagement
from Backend.src.Db.models import Product, ShelfInventory, Sale, Shelf

async def fetch_recent_data(db: DatabaseManagement, time_window_minutes: int = 60) -> pd.DataFrame:
"""Fetch recent shelf inventory and sales data from the database for anomaly detection."""
cutoff_time = datetime.now() - timedelta(minutes=time_window_minutes)

# Fetch Shelf Inventory
shelf_inv_query = text("""
SELECT shelf_inventory_id, product_id, shelf_id, added_timestamp, removed_timestamp
FROM shelf_inventory
WHERE added_timestamp >= :cutoff OR (removed_timestamp IS NOT NULL AND removed_timestamp >= :cutoff)
ORDER BY added_timestamp, removed_timestamp
""").bindparams(cutoff=cutoff_time)
shelf_inv_results = await db.session.exec(shelf_inv_query)
shelf_inv_df = pd.DataFrame(shelf_inv_results.mappings().all()) if shelf_inv_results else pd.DataFrame()

# Fetch Sales
sales_query = text("""
SELECT product_id, sale_timestamp, COUNT(*) as sales_count
FROM sale
WHERE sale_timestamp >= :cutoff
GROUP BY product_id, sale_timestamp
""").bindparams(cutoff=cutoff_time)
sales_results = await db.session.exec(sales_query)
sales_df = pd.DataFrame(sales_results.mappings().all()) if sales_results else pd.DataFrame()

# Fetch Products
products = await db.search(Product, all_results=True)
products_df = pd.DataFrame([p.dict() for p in products]) if products else pd.DataFrame()

# Fetch Shelves
shelves = await db.search(Shelf, all_results=True)
shelves_df = pd.DataFrame([s.dict() for s in shelves]) if shelves else pd.DataFrame()

# Preprocess Shelf Inventory Data
if not shelf_inv_df.empty:
# Calculate stock change: +1 for added, -1 for removed
shelf_inv_df["stock_change"] = np.where(
shelf_inv_df["added_timestamp"].notnull() & shelf_inv_df["removed_timestamp"].isnull(), 1,
np.where(shelf_inv_df["removed_timestamp"].notnull(), -1, 0)
)
shelf_inv_df["timestamp"] = shelf_inv_df.apply(
lambda row: row["removed_timestamp"] if pd.notnull(row["removed_timestamp"]) else row["added_timestamp"], axis=1
)

# Aggregate stock changes by product and timestamp
shelf_inv_df = shelf_inv_df.groupby(["product_id", "shelf_id", "timestamp"])["stock_change"].sum().reset_index()
shelf_inv_df["inventory_count"] = shelf_inv_df.groupby("product_id")["stock_change"].cumsum().fillna(0)
shelf_inv_df["inventory_change"] = shelf_inv_df.groupby("product_id")["inventory_count"].diff().fillna(shelf_inv_df["stock_change"])

# Merge with product and shelf info
shelf_inv_df = shelf_inv_df.merge(products_df[["product_id", "product_name"]], on="product_id", how="left")
shelf_inv_df = shelf_inv_df.merge(shelves_df[["shelf_id", "shelf_location"]], on="shelf_id", how="left")

# Merge with sales data
if not sales_df.empty:
shelf_inv_df = shelf_inv_df.merge(
sales_df[["sale_timestamp", "product_id", "sales_count"]],
left_on=["timestamp", "product_id"],
right_on=["sale_timestamp", "product_id"],
how="left"
)
shelf_inv_df["sales_count"] = shelf_inv_df["sales_count"].fillna(0)

# Define expected columns
expected_columns = ["timestamp", "product_id", "product_name", "shelf_location", "inventory_count", "inventory_change", "sales_count"]
if shelf_inv_df.empty:
return pd.DataFrame(columns=expected_columns)
return shelf_inv_df[expected_columns]
8 changes: 8 additions & 0 deletions Backend/src/ml/db_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Backend/src/ml/db_init.py
from sqlalchemy.ext.asyncio import AsyncEngine
from sqlmodel import SQLModel

async def initialize_database(engine: AsyncEngine):
"""Create tables if they don’t exist."""
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
27 changes: 27 additions & 0 deletions Backend/src/ml/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Backend/src/ml/test_main.py
import asyncio
import pandas as pd
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.orm import sessionmaker
from Backend.src.Db.db import async_engine, get_session # Use actual database engine
from Backend.src.Db.database_management import DatabaseManagement
from .anomaly_detection import detect_anomalies

async def main():
# Use the actual database engine (no need to initialize tables here, handled in src/main.py)
async for session in get_session():
db = DatabaseManagement(session)

# Run anomaly detection with a 48-hour window
result_df = await detect_anomalies(db, interval_seconds=172800) # 48 hours

# Filter anomalies and select desired columns
if not result_df.empty:
anomalies = result_df[result_df["is_anomaly"]][["product_id", "product_name", "shelf_location"]]
print(anomalies if not anomalies.empty else "No anomalies detected.")
else:
print("No data returned from anomaly detection.")

if __name__ == "__main__":
asyncio.run(main())
Loading