GrocyGenieModel / model.py
shahriar031's picture
Update model.py
664fb2f verified
Raw
History Blame Contribute Delete
15.8 kB
from supabase_client import supabase
import os
import random
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from dotenv import load_dotenv
from supabase import create_client, Client
from sklearn.preprocessing import LabelEncoder, MinMaxScaler
import tensorflow as tf
from tensorflow.keras.models import Model, load_model
from tensorflow.keras.layers import Input, LSTM, Dense
import uuid
import joblib
from pathlib import Path
# ===============================
# 3. CONSTANTS & ENCODING
# ===============================
MODEL_PATH = "/tmp/global_model.h5"
DATA_PATH = "/tmp/initial_data.csv"
SCALER_PATH = "/tmp/scaler.pkl"
REGIONS = ['urban', 'rural']
SEASONS = ['winter', 'spring', 'summer', 'autumn']
EVENTS = ['normal', 'fasting', 'guests', 'sickness', 'travel', 'meal_off']
REGION_MULTIPLIER = {'urban': 1.0, 'rural': 1.1}
SEASON_MULTIPLIER = {'winter': 1.1, 'spring': 1.0, 'summer': 0.9, 'autumn': 1.0}
EVENT_MULTIPLIER = {'normal': 1.0, 'fasting': 0.7, 'guests': 1.3, 'sickness': 0.5, 'travel': 0.3, 'meal_off': 0.2}
BASE_CONSUMPTION = {
'rice': {'adult_male': 0.3, 'adult_female': 0.25, 'child': 0.15},
'milk': {'adult_male': 0.2, 'adult_female': 0.18, 'child': 0.3},
'potato': {'adult_male': 0.25, 'adult_female': 0.2, 'child': 0.15},
'onion': {'adult_male': 0.1, 'adult_female': 0.1, 'child': 0.05}
}
le_region = LabelEncoder().fit(REGIONS)
le_season = LabelEncoder().fit(SEASONS)
le_event = LabelEncoder().fit(EVENTS)
le_product = None
scaler = None
# ===============================
# 4. UTILITIES & DATA FUNCTIONS
# ===============================
def get_season(date):
month = date.month
if month in [12, 1, 2]: return 'winter'
elif month in [3, 4, 5]: return 'spring'
elif month in [6, 7, 8]: return 'summer'
return 'autumn'
def generate_family():
return {
'adult_male': random.randint(1, 3),
'adult_female': random.randint(1, 3),
'child': random.randint(0, 3)
}, random.choice(REGIONS)
def calculate_base_consumption(fam, region, season, event, product):
base = BASE_CONSUMPTION.get(product, {'adult_male': 0.1, 'adult_female': 0.1, 'child': 0.05})
total = sum(base[k]*fam.get(k, 0) for k in base)
total *= REGION_MULTIPLIER.get(region, 1.0)
total *= SEASON_MULTIPLIER.get(season, 1.0)
total *= EVENT_MULTIPLIER.get(event, 1.0)
total *= np.random.normal(1, 0.05)
return max(total, 0.01)
def generate_data(products, families=3, days=180):
data = []
for _ in range(families):
fam, region = generate_family()
start = datetime.today() - timedelta(days=days)
for d in range(days):
date = start + timedelta(days=d)
season = get_season(date)
event = random.choices(EVENTS, weights=[70,5,5,5,5,10], k=1)[0]
for prod in products:
cons = calculate_base_consumption(fam, region, season, event, prod)
stock = random.uniform(1.0, 10.0)
pred_days = stock / cons if cons > 0 else 1
act_days = pred_days * np.random.normal(1, 0.1)
finish_error = int(act_days - pred_days)
data.append({
'date': date.strftime('%Y-%m-%d'),
'product': prod, 'region': region, 'season': season, 'event': event,
'adult_male': fam['adult_male'], 'adult_female': fam['adult_female'], 'child': fam['child'],
'consumption': cons,
'stock_quantity': stock,
'finish_error': finish_error, 'finish_days': act_days
})
return pd.DataFrame(data)
def is_new_product(product, existing_df):
return product not in existing_df['product'].unique()
def generate_and_append_new_product(product, csv_path=DATA_PATH):
print(f"Generating synthetic data for new product: {product}")
new_df = generate_data([product], families=3, days=180)
if os.path.exists(csv_path):
existing = pd.read_csv(csv_path)
combined = pd.concat([existing, new_df], ignore_index=True)
else:
combined = new_df
combined.to_csv(csv_path, index=False)
print(f"Product '{product}' added to dataset.")
def create_user_if_not_exists(user_id, user_name="test_user", email="test@example.com"):
"""Create a user if they don't exist in the database"""
try:
response = supabase.table("users").select("user_id").eq("user_id", user_id).execute()
if response.data:
print(f"User {user_id} already exists")
return True
new_user = {
"user_id": user_id,
"user_name": user_name,
"email": email,
"region": "urban",
"adult_male": 1,
"adult_female": 1,
"child": 0
}
response = supabase.table("users").insert(new_user).execute()
print(f"Created new user: {user_id}")
return True
except Exception as e:
print(f"Error creating user: {e}")
return False
def fetch_feedback_for_user(user_id):
response = supabase.table("feedback_data").select("*").eq("user_id", user_id).execute()
return pd.DataFrame(response.data) if response.data else pd.DataFrame()
def insert_feedback(user_id, df):
if not create_user_if_not_exists(user_id):
print("Failed to create user, cannot insert feedback")
return
df['user_id'] = user_id
response = supabase.table("feedback_data").insert(df.to_dict(orient="records")).execute()
print("Feedback inserted successfully")
def get_product_id(product_name):
res = supabase.table("products").select("product_id").eq("product_name", product_name).execute()
if res.data:
return res.data[0]['product_id']
new_id = str(uuid.uuid4())
supabase.table("products").insert({
'product_id': new_id,
'product_name': product_name,
'unit': 'unit'
}).execute()
return new_id
def store_predictions(user_id, predictions, user_input):
rows = []
for product, result in predictions.items():
product_id = get_product_id(product)
row = {
'user_id': user_id,
'product_id': product_id,
'prediction_date': datetime.today().date().isoformat(),
'predicted_consumption': result['predicted_consumption'],
'predicted_finish_days': result['predicted_finish_days'],
'predicted_finish_date': result['predicted_finish_date'],
'predicted_error': result['predicted_finish_error'],
'stock_quantity': user_input['stock'][product],
}
rows.append(row)
supabase.table("prediction_outputs").insert(rows).execute()
print("✅ Predictions stored successfully")
# ===============================
# 5. MODELING
# ===============================
def prepare_data(df, products):
global le_product, scaler
le_product = LabelEncoder().fit(products)
df['region_enc'] = le_region.transform(df['region'])
df['season_enc'] = le_season.transform(df['season'])
df['event_enc'] = le_event.transform(df['event'])
df['product_enc'] = le_product.transform(df['product'])
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
if scaler is None:
scaler_local = MinMaxScaler()
df[['adult_male','adult_female','child','consumption','stock_quantity']] = scaler_local.fit_transform(df[['adult_male','adult_female','child','consumption','stock_quantity']])
else:
scaler_local = scaler
df[['adult_male','adult_female','child','consumption','stock_quantity']] = scaler_local.transform(df[['adult_male','adult_female','child','consumption','stock_quantity']])
X, y1, y2, y3 = [], [], [], []
seq_len = 7
for p in df['product_enc'].unique():
sub = df[df['product_enc'] == p].reset_index(drop=True)
feats = sub[['adult_male','adult_female','child','consumption','stock_quantity','region_enc','season_enc','event_enc','product_enc']].values
c = sub['consumption'].values
err = sub['finish_error'].values
days = sub['finish_days'].values
for i in range(len(sub) - seq_len):
X.append(feats[i:i + seq_len])
y1.append(c[i + seq_len])
y2.append(err[i + seq_len])
y3.append(days[i + seq_len])
return np.array(X), np.array(y1), np.array(y2), np.array(y3), scaler_local
def build_model(input_shape):
inp = Input(shape=input_shape) # (7, 9)
x = LSTM(64)(inp)
x = Dense(32, activation='relu')(x)
out1 = Dense(1, name='daily_consumption_output')(x)
out2 = Dense(1, name='finish_error_output')(x)
out3 = Dense(1, name='finish_days_output')(x)
model = Model(inputs=inp, outputs=[out1, out2, out3])
model.compile(optimizer='adam', loss=tf.keras.losses.MeanSquaredError())
return model
def load_or_train_model():
global scaler
model_path = MODEL_PATH
if not os.path.exists(DATA_PATH):
pd.DataFrame(columns=[
'date', 'product', 'region', 'season', 'event',
'adult_male', 'adult_female', 'child',
'consumption', 'stock_quantity', 'finish_error', 'finish_days'
]).to_csv(DATA_PATH, index=False)
df = pd.read_csv(DATA_PATH)
if df.empty:
df = generate_data(list(BASE_CONSUMPTION.keys()))
df.to_csv(DATA_PATH, index=False)
products = df['product'].unique().tolist()
X, y1, y2, y3, scaler_obj = prepare_data(df, products)
scaler = scaler_obj
if os.path.exists(model_path):
os.remove(model_path)
model = build_model((X.shape[1], X.shape[2]))
model.fit(
X,
{
'daily_consumption_output': y1,
'finish_error_output': y2,
'finish_days_output': y3
},
epochs=10,
batch_size=32,
validation_split=0.1
)
model.save(model_path)
joblib.dump(scaler, SCALER_PATH)
print("✅ Model trained and saved.")
print("✅ Scaler saved to scaler.pkl.")
return model
def retrain_model_with_feedback(user_id):
base_df = pd.read_csv(DATA_PATH)
feedback_df = fetch_feedback_for_user(user_id)
if feedback_df.empty:
print("No feedback found for user:", user_id)
return {"message": f"No feedback found for user {user_id}"}
combined_df = pd.concat([base_df, feedback_df], ignore_index=True)
X, y1, y2, y3, scaler_obj = prepare_data(combined_df, combined_df['product'].unique().tolist())
model = build_model((X.shape[1], X.shape[2]))
model.fit(
X,
{'daily_consumption_output': y1,
'finish_error_output': y2,
'finish_days_output': y3},
epochs=10,
batch_size=32,
validation_split=0.1,
)
model.save(MODEL_PATH)
joblib.dump(scaler_obj, SCALER_PATH)
print("Retrained model saved.")
return {"message": f"Retraining complete for user {user_id}"}
# ===============================
# 6. PREDICTION
# ===============================
def predict_user_input(user_input):
global le_product, scaler, ml_model
initial_df = pd.read_csv(DATA_PATH)
predictions = {}
for product in user_input['stock'].keys():
if is_new_product(product, initial_df):
generate_and_append_new_product(product)
initial_df = pd.read_csv(DATA_PATH)
products = initial_df['product'].unique().tolist()
le_product = LabelEncoder().fit(products)
product_enc = le_product.transform([product])[0]
vec = []
for _ in range(7):
base = calculate_base_consumption(
user_input['family'],
user_input['region'],
user_input['season'],
user_input['event'],
product
)
stock_val = user_input['stock'][product]
raw = [
user_input['family']['adult_male'],
user_input['family']['adult_female'],
user_input['family']['child'],
base,
stock_val
]
df_input = pd.DataFrame(
[raw],
columns=['adult_male', 'adult_female', 'child', 'consumption', 'stock_quantity']
)
raw_scaled = scaler.transform(df_input)[0]
region_enc = le_region.transform([user_input['region']])[0]
season_enc = le_season.transform([user_input['season']])[0]
event_enc = le_event.transform([user_input['event']])[0]
features = list(raw_scaled) + [region_enc, season_enc, event_enc, product_enc]
vec.append(features)
vec = np.array(vec)[np.newaxis, :, :] # shape = (1, 7, 9)
y1, y2, y3 = ml_model.predict(vec, verbose=0)
# -------- INVERSE TRANSFORM START --------
stock_val = user_input['stock'][product]
df_temp = pd.DataFrame([[
user_input['family']['adult_male'],
user_input['family']['adult_female'],
user_input['family']['child'],
0, # placeholder
stock_val
]], columns=['adult_male', 'adult_female', 'child', 'consumption', 'stock_quantity'])
scaled_temp = scaler.transform(df_temp)
scaled_temp[0][3] = y1[0][0] # Replace scaled 'consumption' with predicted
unscaled = scaler.inverse_transform(scaled_temp)[0]
daily = float(unscaled[3]) # actual predicted consumption (inverse-transformed)
# -------- INVERSE TRANSFORM END --------
error = float(y2[0][0])
days = float(y3[0][0])
finish_date = datetime.today() + timedelta(days=days)
predictions[product] = {
'predicted_consumption': round(daily, 3),
'predicted_finish_days': round(days, 2),
'predicted_finish_date': finish_date.strftime('%Y-%m-%d'),
'predicted_finish_error': round(error, 2)
}
return predictions
# ===============================
# 8. GLOBAL LOADING FOR FASTAPI
# ===============================
if os.path.exists(MODEL_PATH):
ml_model = tf.keras.models.load_model(MODEL_PATH)
print("✅ Loaded trained model for prediction.")
else:
ml_model = load_or_train_model()
scaler = joblib.load(SCALER_PATH)
print("✅ Loaded scaler.")
# ===============================
# 7. EXAMPLE RUN
# ===============================
if __name__ == "__main__":
model = load_or_train_model()
user_input = {
"family": {"adult_male": 2, "adult_female": 2, "child": 1},
"region": "urban",
"season": "summer",
"event": "normal",
"stock": {"rice": 5, "milk": 3, "chicken": 4} # 'chicken' is new
}
results = predict_user_input(user_input)
print(pd.DataFrame(results).T)
feedback = pd.DataFrame([{
'date': datetime.today().strftime('%Y-%m-%d'),
'product': k,
'region': user_input['region'],
'season': user_input['season'],
'event': user_input['event'],
'adult_male': user_input['family']['adult_male'],
'adult_female': user_input['family']['adult_female'],
'child': user_input['family']['child'],
'consumption': v['predicted_consumption'],
'finish_error': v['predicted_finish_error'],
'finish_days': v['predicted_finish_days'],
'stock_quantity': user_input['stock'][k]
} for k, v in results.items()])
user_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, "user001"))
try:
insert_feedback(user_uuid, feedback)
store_predictions(user_uuid, results, user_input)
print("Data insertion completed successfully!")
except Exception as e:
print(f"Error during data insertion: {e}")