Truck_Analysis / app.py
ans123's picture
Update app.py
621d3d6 verified
import pandas as pd
import numpy as np
import gradio as gr
import matplotlib.pyplot as plt
import seaborn as sns
import io
import base64
# --- Global Variables to store processed data ---
global_df = None
global_brand_resale = None
global_brand_resale_mean = 0
global_fair_market_value_mean = 0
global_purchase_amount_mean = 0
global_monthly_payment_mean = 0
global_ownership_types = []
# === Truck ID Cleaner ===
def clean_truck_id(val):
"""
Cleans and standardizes truck IDs by removing "SPOT-" prefix and stripping whitespace.
Handles NaN values by returning None.
"""
if pd.isna(val):
return None
return str(val).strip().replace("SPOT-", "")
# === Load and Prepare Data ===
def load_and_clean_data():
"""
Loads data from various Excel files, performs initial cleaning,
and converts relevant columns to appropriate data types (numeric, datetime).
Includes error handling for missing files.
"""
try:
# Load files
finance = pd.read_excel("truck-finance.xlsx")
maintenance = pd.read_excel("maintenancepo-truck.xlsx")
distance = pd.read_excel("vehicle-distance-traveled.xlsx")
odometer = pd.read_excel("truck-odometer-data-week.xlsx")
stub = pd.read_excel("stub-data.xlsx")
paper = pd.read_excel("truck-paper.xlsx")
# --- Explicitly convert relevant columns to numeric and datetime ---
# Coerce errors will turn unparseable values into NaN
finance['fair_market_value'] = pd.to_numeric(finance['fair_market_value'], errors='coerce')
finance['purchase_amount'] = pd.to_numeric(finance['purchase_amount'], errors='coerce')
finance['monthly_payment'] = pd.to_numeric(finance['monthly_payment'], errors='coerce')
maintenance['amount'] = pd.to_numeric(maintenance['amount'], errors='coerce')
distance['date'] = pd.to_datetime(distance['date'], errors='coerce')
distance['distance'] = pd.to_numeric(distance['distance'], errors='coerce')
odometer['pay_date'] = pd.to_datetime(odometer['pay_date'], errors='coerce')
odometer['odometer'] = pd.to_numeric(odometer['odometer'], errors='coerce')
paper['truck_price'] = pd.to_numeric(paper['truck_price'], errors='coerce')
print("Finance columns after loading:", finance.columns)
print("Maintenance columns after loading:", maintenance.columns)
print("Stub columns after loading:", stub.columns)
print("Distance columns after loading:", distance.columns)
print("Odometer columns after loading:", odometer.columns)
# --- Clean & Standardize IDs ---
finance["truck_id"] = finance["unit_id"].apply(clean_truck_id)
maintenance["truck_id"] = maintenance["unit_id"].apply(clean_truck_id)
stub["truck_id"] = stub["TRUCK"].apply(clean_truck_id)
odometer["truck_id"] = odometer["unit_id"].apply(clean_truck_id)
distance["truck_id"] = distance["unit_id"].apply(clean_truck_id)
return finance, maintenance, distance, odometer, stub, paper
except FileNotFoundError as e:
print(f"Error: One or more input files not found. Please ensure all Excel files are in the same directory as the script. Missing file: {e.filename}")
raise gr.Error(f"Required file not found: {e.filename}. Please upload all necessary Excel files.")
except Exception as e:
print(f"An unexpected error occurred during data loading: {e}")
raise gr.Error(f"An error occurred during data loading: {e}")
# === Initial Data Processing (called once at app startup) ===
def initial_data_processing():
"""
Loads, cleans, merges, and prepares all data for the Gradio app.
Populates global variables used by prediction and plotting functions.
"""
global global_df, global_brand_resale, global_brand_resale_mean, \
global_fair_market_value_mean, global_purchase_amount_mean, \
global_monthly_payment_mean, global_ownership_types
try:
finance, maintenance, distance, odometer, stub, paper = load_and_clean_data()
# --- Maintenance Summary ---
maintenance_summary = maintenance.groupby("truck_id").agg(
total_repairs=("amount", "sum"),
shop_visits=("truck_id", "count")
).reset_index()
# --- Stub Usage ---
stub_summary = stub.groupby("truck_id").agg(
usage_records=("truck_id", "count")
).reset_index()
# --- 10-Week Distance Summary ---
latest = distance['date'].max()
last10 = distance[distance['date'].notna() & (distance['date'] >= (latest - pd.Timedelta(weeks=10)))]
distance_summary = last10.groupby("truck_id").agg(
last_10w_miles=('distance', 'sum')
).reset_index()
# --- Odometer Summary ---
odometer_cleaned = odometer[odometer['pay_date'].notna() & odometer['odometer'].notna()]
odo_summary = odometer_cleaned.sort_values(['truck_id', 'pay_date']).groupby("truck_id").agg(
odo_start=('odometer', 'first'),
odo_end=('odometer', 'last')
).reset_index()
odo_summary["odo_diff"] = odo_summary["odo_end"] - odo_summary["odo_start"]
# --- Resale Values (avg per make) ---
paper['truck_brand'] = paper['truck_brand'].str.upper()
global_brand_resale = paper.groupby("truck_brand").agg(
avg_resale_value=('truck_price', 'mean')
).reset_index()
global_brand_resale_mean = global_brand_resale['avg_resale_value'].mean()
# --- Merge All Sources ---
df = finance.merge(maintenance_summary, on="truck_id", how="left")
df = df.merge(stub_summary, on="truck_id", how="left")
df = df.merge(distance_summary, on="truck_id", how="left")
df = df.merge(odo_summary[['truck_id', 'odo_diff']], on="truck_id", how="left")
df['make'] = df['make'].str.upper()
df = df.merge(global_brand_resale, left_on='make', right_on='truck_brand', how='left')
df.drop(columns=['truck_brand'], inplace=True)
# --- Standardize 'ownership_type' ---
df['ownership_type'] = df['ownership_type'].astype(str).str.strip().str.upper()
global_ownership_types = df['ownership_type'].unique().tolist()
# Ensure 'NAN' is handled if it appears due to missing ownership types
if 'NAN' in global_ownership_types:
global_ownership_types.remove('NAN')
global_ownership_types.sort() # Sort for better display in dropdown
# Calculate means for imputation, handling potential NaN means if column is all NaN
global_fair_market_value_mean = df['fair_market_value'].mean()
global_purchase_amount_mean = df['purchase_amount'].mean()
global_monthly_payment_mean = df['monthly_payment'].mean()
# --- Handle NaNs for decision-making columns ---
df["total_repairs"] = df["total_repairs"].fillna(0)
df["shop_visits"] = df["shop_visits"].fillna(0)
df["usage_records"] = df["usage_records"].fillna(0)
df["last_10w_miles"] = df["last_10w_miles"].fillna(0)
df["odo_diff"] = df["odo_diff"].fillna(0).apply(lambda x: 0 if x < 0 else x)
df["avg_resale_value"] = df["avg_resale_value"].fillna(global_brand_resale_mean if not pd.isna(global_brand_resale_mean) else 0)
df["fair_market_value"] = df["fair_market_value"].fillna(global_fair_market_value_mean if not pd.isna(global_fair_market_value_mean) else 0)
df["purchase_amount"] = df["purchase_amount"].fillna(global_purchase_amount_mean if not pd.isna(global_purchase_amount_mean) else 0)
df["monthly_payment"] = df["monthly_payment"].fillna(global_monthly_payment_mean if not pd.isna(global_monthly_payment_mean) else 0)
# --- Add CPM ---
# Replace odo_diff = 0 with 1 for CPM calculation to avoid division by zero and get non-zero CPM
df['odo_diff_for_cpm'] = df['odo_diff'].replace(0, 1)
df["CPM"] = df["total_repairs"] / df["odo_diff_for_cpm"]
df["CPM"] = df["CPM"].replace([np.inf, -np.inf], np.nan)
df["CPM"] = df["CPM"].fillna(0)
# --- Apply decision logic to the full dataset for plotting the breakdown ---
def make_decision_for_df(row):
# This is the same logic as before, applied to the full DataFrame
# 1. Scrap:
if (row['total_repairs'] > 8000 and
row['last_10w_miles'] < 500 and
row['odo_diff'] > 70000 and
row['CPM'] > 0.2 and
row['purchase_amount'] < 20000):
return "Scrap"
# 2. Sell:
elif (row['total_repairs'] > 5000 and
row['last_10w_miles'] < 1000 and
row['fair_market_value'] > row['purchase_amount'] and
row['odo_diff'] > 50000):
return "Sell"
# 3. Lease:
elif (row['ownership_type'] == 'OPERATING LEASE' and
row['monthly_payment'] > 600 and
row['purchase_amount'] < 30000 and
row['fair_market_value'] > 28000 and
row['odo_diff'] < 40000):
return "Lease"
# 4. Keep:
elif (row['total_repairs'] < 3000 and
row['last_10w_miles'] > 2000 and
row['fair_market_value'] < row['purchase_amount'] and
row['odo_diff'] < 30000):
return "Keep"
# 5. Analyze: Default
else:
return "Analyze"
df["Decision"] = df.apply(make_decision_for_df, axis=1)
global_df = df.copy() # Make a copy to avoid SettingWithCopyWarning if modified later
print("Initial data processing complete. Data loaded for Gradio app.")
except gr.Error as e:
print(f"Gradio Error during initial data processing: {e}")
# If an error occurs, ensure global_df is an empty DataFrame to prevent further errors
global_df = pd.DataFrame()
except Exception as e:
print(f"Unexpected error during initial data processing: {e}")
global_df = pd.DataFrame()
# === Decision Prediction Function for Gradio Interface ===
def predict_decision(total_repairs, last_10w_miles, odo_diff, cpm, purchase_amount, fair_market_value, monthly_payment, ownership_type_str, make):
"""
Predicts the decision for a single truck based on user inputs.
Uses globally pre-calculated means for missing values if inputs are None.
"""
# Handle potentially None inputs from Gradio and ensure numeric types
total_repairs = float(total_repairs) if total_repairs is not None else 0.0
last_10w_miles = float(last_10w_miles) if last_10w_miles is not None else 0.0
odo_diff = float(odo_diff) if odo_diff is not None else 0.0
cpm = float(cpm) if cpm is not None else 0.0
# Use global means for financial values if user input is None, and ensure they are float
purchase_amount = float(purchase_amount) if purchase_amount is not None else (global_purchase_amount_mean if not pd.isna(global_purchase_amount_mean) else 0.0)
fair_market_value = float(fair_market_value) if fair_market_value is not None else (global_fair_market_value_mean if not pd.isna(global_fair_market_value_mean) else 0.0)
monthly_payment = float(monthly_payment) if monthly_payment is not None else (global_monthly_payment_mean if not pd.isna(global_monthly_payment_mean) else 0.0)
ownership_type_str = ownership_type_str.strip().upper() if ownership_type_str is not None else "UNKNOWN"
make = make.strip().upper() if make is not None else "UNKNOWN"
# For avg_resale_value, try to get it from the pre-calculated global_brand_resale, else use global mean
avg_resale_value = 0.0 # Default if global_brand_resale is not loaded
if global_brand_resale is not None:
avg_resale_value_lookup = global_brand_resale.loc[global_brand_resale['truck_brand'] == make, 'avg_resale_value'].values
if len(avg_resale_value_lookup) > 0:
avg_resale_value = avg_resale_value_lookup[0]
else:
avg_resale_value = global_brand_resale_mean if not pd.isna(global_brand_resale_mean) else 0.0
# Apply the same logic as make_decision, but directly with the input variables
# 1. Scrap:
if (total_repairs > 8000 and
last_10w_miles < 500 and
odo_diff > 70000 and
cpm > 0.2 and
purchase_amount < 20000):
return "Scrap"
# 2. Sell:
elif (total_repairs > 5000 and
last_10w_miles < 1000 and
fair_market_value > purchase_amount and
odo_diff > 50000):
return "Sell"
# 3. Lease:
elif (ownership_type_str == 'OPERATING LEASE' and
monthly_payment > 600 and
purchase_amount < 30000 and
fair_market_value > 28000 and
odo_diff < 40000):
return "Lease"
# 4. Keep:
elif (total_repairs < 3000 and
last_10w_miles > 2000 and
fair_market_value < purchase_amount and
odo_diff < 30000):
return "Keep"
# 5. Analyze: Default
else:
return "Analyze"
# === Plot Generation Function for Gradio Interface ===
def generate_plots():
"""
Generates various plots from the processed global_df and returns them as base64 encoded images.
"""
if global_df is None or global_df.empty:
# Return a list of None values for the images if data is not loaded
return [None, None, None, None]
plot_buffers = [] # Store image bytes here
# Plot 1: Decision Breakdown
try:
plt.figure(figsize=(8, 6))
# Add hue and set legend=False to resolve FutureWarning
sns.countplot(data=global_df, x='Decision', hue='Decision', palette='viridis', order=global_df['Decision'].value_counts().index, legend=False)
plt.title('Decision Breakdown for the Fleet')
plt.xlabel('Decision')
plt.ylabel('Number of Trucks')
plt.grid(axis='y', linestyle='--', alpha=0.7)
buf = io.BytesIO()
plt.savefig(buf, format='png')
plt.close()
# Encode to base64 and prepend data URI
plot_buffers.append(base64.b64encode(buf.getvalue()).decode('utf-8'))
except Exception as e:
print(f"Error generating Decision Breakdown plot: {e}")
plot_buffers.append(None) # Append None if plot generation fails
# Plot 2: Total Repairs by Ownership Type
try:
plt.figure(figsize=(12, 7))
# Filter out NaN/None ownership types if any remain for plotting robustness
plot_df = global_df[global_df['ownership_type'].notna() & (global_df['ownership_type'] != 'NAN')]
if not plot_df.empty:
# Add hue and set legend=False to resolve FutureWarning
sns.boxplot(data=plot_df, x='ownership_type', y='total_repairs', hue='ownership_type', palette='coolwarm', legend=False)
plt.title('Total Repairs by Ownership Type')
plt.xlabel('Ownership Type')
plt.ylabel('Total Repairs ($)')
plt.xticks(rotation=45, ha='right')
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png')
plt.close()
# Encode to base64 and prepend data URI
plot_buffers.append(base64.b64encode(buf.getvalue()).decode('utf-8'))
else:
plot_buffers.append(None)
except Exception as e:
print(f"Error generating Total Repairs plot: {e}")
plot_buffers.append(None)
# Plot 3: Last 10 Weeks Miles Distribution
try:
plt.figure(figsize=(10, 6))
sns.histplot(data=global_df, x='last_10w_miles', bins=30, kde=True, color='skyblue')
plt.title('Distribution of Last 10 Weeks Miles')
plt.xlabel('Last 10 Weeks Miles')
plt.ylabel('Number of Trucks')
plt.grid(axis='y', linestyle='--', alpha=0.7)
buf = io.BytesIO()
plt.savefig(buf, format='png')
plt.close()
# Encode to base64 and prepend data URI
plot_buffers.append(base64.b64encode(buf.getvalue()).decode('utf-8'))
except Exception as e:
print(f"Error generating Miles Distribution plot: {e}")
plot_buffers.append(None)
# Plot 4: Fair Market Value vs. Purchase Amount
try:
plt.figure(figsize=(10, 7))
# Ensure columns are numeric and handle potential NaNs for plotting
plot_df = global_df.dropna(subset=['purchase_amount', 'fair_market_value', 'Decision'])
if not plot_df.empty:
sns.scatterplot(data=plot_df, x='purchase_amount', y='fair_market_value', hue='Decision', palette='deep', alpha=0.7)
plt.title('Fair Market Value vs. Purchase Amount by Decision')
plt.xlabel('Purchase Amount ($)')
plt.ylabel('Fair Market Value ($)')
plt.grid(linestyle='--', alpha=0.7)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png')
plt.close()
# Encode to base64 and prepend data URI
plot_buffers.append(base64.b64encode(buf.getvalue()).decode('utf-8'))
else:
plot_buffers.append(None)
except Exception as e:
print(f"Error generating FMV vs Purchase plot: {e}")
plot_buffers.append(None)
return plot_buffers
# --- Initial Data Loading and Processing Call ---
initial_data_processing()
# --- Gradio Interface Definition ---
# Define inputs for the Decision Predictor tab
# Use the dynamically populated global_ownership_types for the dropdown choices
decision_inputs = [
gr.Number(label="Total Repairs ($)", value=0.0),
gr.Number(label="Last 10 Weeks Miles", value=0.0),
gr.Number(label="Odometer Difference (odo_diff)", value=0.0),
gr.Number(label="Cost Per Mile (CPM)", value=0.0),
gr.Number(label="Purchase Amount ($)", value=0.0),
gr.Number(label="Fair Market Value ($)", value=0.0),
gr.Number(label="Monthly Payment ($)", value=0.0),
gr.Dropdown(label="Ownership Type", choices=global_ownership_types, value=global_ownership_types[0] if global_ownership_types else "OWNER OPERATOR OWNED"),
gr.Textbox(label="Make (e.g., FORD)", value="FORD")
]
# Create the Gradio Interface
with gr.Blocks() as demo:
gr.Markdown("# Truck Evaluation Application")
gr.Markdown("Use this app to predict truck decisions and visualize fleet data.")
with gr.Tab("Decision Predictor"):
gr.Markdown("## Predict Truck Decision")
gr.Markdown("Enter the details for a single truck to get a decision.")
with gr.Row():
for input_comp in decision_inputs:
input_comp.render()
predict_button = gr.Button("Get Decision")
decision_output = gr.Textbox(label="Decision", interactive=False)
predict_button.click(
fn=predict_decision,
inputs=decision_inputs,
outputs=decision_output
)
with gr.Tab("Data Visualizations"):
gr.Markdown("## Fleet Data Visualizations")
gr.Markdown("Explore insights from your truck data.")
plot_button = gr.Button("Generate Plots")
# Output components for plots
# These are just placeholders; the generate_plots function will return the actual image bytes
plot_outputs_components = [
gr.Image(label="Decision Breakdown", interactive=False, visible=True),
gr.Image(label="Total Repairs by Ownership Type", interactive=False, visible=True),
gr.Image(label="Last 10 Weeks Miles Distribution", interactive=False, visible=True),
gr.Image(label="Fair Market Value vs. Purchase Amount", interactive=False, visible=True)
]
plot_button.click(
fn=generate_plots,
inputs=[],
outputs=plot_outputs_components
)
# Launch the Gradio app
if __name__ == "__main__":
demo.launch()