WB_Analyzer / app.py
bakyt92's picture
update of utils.py
9c6c702
# Wildberries Analytics Dashboard
# Updated for Hugging Face Spaces deployment
import gradio as gr
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
import json
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
# Import custom modules
from wildberries_client import WildberriesAPI
from forecasting import InventoryForecaster
from dashboard import (
create_sales_dashboard,
create_inventory_dashboard,
create_wb_kpi_cards,
create_commission_analysis_chart,
validate_and_process_wb_data
)
from config import get_config
import utils
# Initialize configuration
config = get_config()
def initialize_wb_client(api_token=None):
"""Initialize Wildberries API client with error handling"""
try:
# Only use token provided through Gradio interface
if not api_token or api_token.strip() == "":
return None
client = WildberriesAPI(api_token.strip())
return client
except Exception as e:
gr.Error(f"Failed to initialize Wildberries client: {str(e)}")
return None
def get_sales_data(period, api_token=None, start_date=None, end_date=None):
"""Fetch sales data with fallback to demo data"""
wb_client = initialize_wb_client(api_token)
if wb_client is None:
# Use demo data when API is not available
return utils.load_demo_sales_data(period)
try:
if period == "week":
date_from = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
elif period == "month":
date_from = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
else:
date_from = start_date if start_date else (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
data = wb_client.get_sales(date_from)
return data
except Exception as e:
gr.Warning(f"API error: {str(e)}. Using demo data.")
return utils.load_demo_sales_data(period)
def analyze_sales_performance(period, api_token):
"""Analyze sales performance for the specified period with enhanced Wildberries metrics"""
try:
data = get_sales_data(period, api_token)
if data.empty:
return "No sales data available for the selected period.", None, None, pd.DataFrame()
# Calculate daily revenue data ONCE to ensure consistency between chart and table
daily_revenue_data = None
daily_revenue_table = pd.DataFrame()
if 'sale_date' in data.columns and 'total_price' in data.columns:
# Create aggregation dictionary based on available columns
agg_dict = {
'total_price': 'sum',
'quantity': 'sum'
}
# Add optional columns if they exist
if 'sales_commission' in data.columns:
agg_dict['sales_commission'] = 'sum'
if 'amount_for_pay' in data.columns:
agg_dict['amount_for_pay'] = 'sum'
daily_revenue_data = data.groupby(data['sale_date'].dt.date).agg(agg_dict).reset_index()
# Create the daily revenue breakdown table
daily_revenue_table = pd.DataFrame({
'Date': daily_revenue_data['sale_date'].astype(str),
'Revenue (₽)': daily_revenue_data['total_price'].round(2),
'Orders': daily_revenue_data['quantity'].astype(int),
'Commission (₽)': (daily_revenue_data['sales_commission'].round(2) if 'sales_commission' in daily_revenue_data.columns else 0),
'Net Revenue (₽)': (daily_revenue_data['amount_for_pay'].round(2) if 'amount_for_pay' in daily_revenue_data.columns else daily_revenue_data['total_price'].round(2))
})
# Sort by date descending (most recent first)
daily_revenue_table = daily_revenue_table.sort_values('Date', ascending=False)
# Get enhanced KPIs for Wildberries data
kpis = create_wb_kpi_cards(data)
# Create sales summary with Wildberries-specific metrics
mode_indicator = "Demo Mode" if not api_token or api_token.strip() == "" else "Live Data"
summary = f"""
## Sales Performance - Last {period.capitalize()} ({mode_indicator})
### 📊 Core Metrics
- **Total Revenue**: ₽{kpis.get('total_revenue', 0):,.2f}
- **Total Orders**: {kpis.get('total_orders', 0):,}
- **Average Order Value**: ₽{kpis.get('avg_order_value', 0):.2f}
- **Best Seller**: {kpis.get('top_product', 'N/A')}
### 💰 Wildberries Metrics
- **Total Commission**: ₽{kpis.get('total_commission', 0):,.2f}
- **Commission Rate**: {kpis.get('avg_commission_rate', 0):.1f}%
- **Net Revenue (After Fees)**: ₽{kpis.get('total_payout', 0):,.2f}
- **Platform Fees**: ₽{kpis.get('platform_fees', 0):,.2f}
- **Net Margin**: {kpis.get('net_margin_percent', 0):.1f}%
### 🚚 Operations
- **Top Office**: {kpis.get('top_office', 'N/A')}
- **Total Delivery Cost**: ₽{kpis.get('total_delivery_cost', 0):,.2f}
- **Daily Sales Velocity**: {kpis.get('daily_sales_velocity', 0):.1f} orders/day
"""
# Create main sales visualization with pre-calculated daily data
main_chart = create_sales_dashboard(data, period, daily_revenue_data)
# Create commission analysis if commission data is available
commission_chart = None
if 'sales_commission' in data.columns:
commission_chart = create_commission_analysis_chart(data)
return summary, main_chart, commission_chart, daily_revenue_table
except Exception as e:
error_msg = f"Error analyzing sales: {str(e)}"
gr.Error(error_msg)
return error_msg, None, None, pd.DataFrame()
def calculate_stockout_forecast(method, api_token):
"""Calculate days until stockout for products"""
try:
# Get current inventory (demo data if API unavailable)
wb_client = initialize_wb_client(api_token)
# Determine if we're in demo mode
use_demo_mode = not api_token or api_token.strip() == ""
if wb_client and not use_demo_mode:
try:
inventory_data = wb_client.get_stocks()
# If API returns empty data, fall back to demo
if inventory_data.empty:
inventory_data = utils.load_demo_inventory_data()
use_demo_mode = True
except Exception:
inventory_data = utils.load_demo_inventory_data()
use_demo_mode = True
else:
inventory_data = utils.load_demo_inventory_data()
use_demo_mode = True
# Get sales data for forecasting - ensure consistency with inventory data source
if use_demo_mode:
sales_data = utils.load_demo_sales_data("month")
else:
sales_data = get_sales_data("month", api_token)
# If API sales data is empty, fall back to demo
if sales_data.empty:
sales_data = utils.load_demo_sales_data("month")
# Initialize forecaster
forecaster = InventoryForecaster()
# Calculate forecasts
forecasts = []
for _, item in inventory_data.iterrows():
product_sales = sales_data[sales_data['product_id'] == item['product_id']]
# If no sales data for this product, use average daily sales of 1
if product_sales.empty:
avg_daily_sales = 1
max_daily_sales = 1
else:
avg_daily_sales = product_sales['quantity'].mean()
max_daily_sales = product_sales['quantity'].max()
if method == "simple":
days_left = forecaster.simple_division_method(
item['current_stock'],
avg_daily_sales
)
elif method == "safety_stock":
days_left = forecaster.safety_stock_method(
item['current_stock'],
avg_daily_sales,
max_daily_sales,
avg_lead_time=7,
max_lead_time=14
)
elif method == "weighted":
if not product_sales.empty:
days_left = forecaster.weighted_average_method(
item['current_stock'],
product_sales
)
else:
days_left = forecaster.simple_division_method(
item['current_stock'],
avg_daily_sales
)
else:
days_left = forecaster.seasonal_adjustment_method(
item['current_stock'],
avg_daily_sales,
seasonal_factor=1.0
)
# Risk categorization
if days_left < 7:
risk_level = "🔴 Critical"
elif days_left < 14:
risk_level = "🟡 Warning"
else:
risk_level = "🟢 Safe"
forecasts.append({
'Product': item['product_name'],
'Current Stock': item['current_stock'],
'Avg Daily Sales': round(avg_daily_sales, 2),
'Days Until Stockout': round(days_left, 1),
'Risk Level': risk_level
})
# Create results DataFrame
results_df = pd.DataFrame(forecasts)
if results_df.empty:
# Return all 3 required values for Gradio
return "No inventory data available.", pd.DataFrame(), None
# Sort by days until stockout
results_df = results_df.sort_values('Days Until Stockout')
# Create summary
critical_items = len(results_df[results_df['Days Until Stockout'] < 7])
warning_items = len(results_df[results_df['Days Until Stockout'].between(7, 14)])
mode_indicator = "Demo Mode" if use_demo_mode else "Live Data"
summary = f"""
## Inventory Forecast - {method.replace('_', ' ').title()} Method ({mode_indicator})
- **Critical Items** (< 7 days): {critical_items}
- **Warning Items** (7-14 days): {warning_items}
- **Safe Items** (> 14 days): {len(results_df) - critical_items - warning_items}
"""
# Create visualization
chart = create_inventory_dashboard(results_df)
return summary, results_df, chart
except Exception as e:
error_msg = f"Error calculating forecast: {str(e)}"
gr.Error(error_msg)
# Return all 3 required values for Gradio
return error_msg, pd.DataFrame(), None
def update_status(api_token):
"""Update API status based on token"""
if not api_token or api_token.strip() == "":
return "🔴 Demo Mode - Enter API token above for live data"
else:
return "🟢 API Token Configured - Ready for live data"
# Create Gradio interface
def create_interface():
"""Create the main Gradio interface"""
with gr.Blocks(
title="Wildberries Analytics Dashboard",
theme=gr.themes.Soft(),
css="""
footer {visibility: hidden}
.plot-container {min-height: 1150px !important}
.gradio-plot {min-height: 1150px !important}
"""
) as demo:
gr.Markdown("""
# 🛍️ Wildberries Analytics Dashboard
Monitor your marketplace performance and predict inventory needs with AI-powered analytics.
**Features:**
- 📊 Sales performance analysis with automatic return detection
- 📦 Inventory forecasting with AI-powered predictions
- ⚠️ Stockout risk alerts and notifications
- 📈 Interactive dashboards with commission analysis
""")
# API Token Configuration
with gr.Row():
with gr.Column(scale=3):
api_token_input = gr.Textbox(
value="",
label="🔑 Wildberries API Token (Required for Real Data)",
placeholder="Paste your Wildberries API token here to access live data - leave empty for demo mode",
type="password",
info="💡 Get your token from your Wildberries seller account → Settings → API"
)
with gr.Column(scale=1):
api_status = gr.Textbox(
value="🔴 Demo Mode - Enter API token above for live data",
label="Status",
interactive=False
)
# Update status when token changes
api_token_input.change(
fn=update_status,
inputs=[api_token_input],
outputs=[api_status]
)
with gr.Tabs():
with gr.TabItem("📊 Sales Analytics"):
with gr.Row():
with gr.Column(scale=1):
period_selector = gr.Radio(
choices=["week", "month"],
value="week",
label="Analysis Period"
)
analyze_btn = gr.Button("📈 Analyze Sales", variant="primary")
with gr.Column(scale=3):
sales_summary = gr.Markdown("Enter your API token above and select a period, then click 'Analyze Sales' to get started.")
# Sales dashboards
with gr.Row():
sales_chart = gr.Plot(label="Sales Performance Dashboard")
with gr.Row():
commission_chart = gr.Plot(label="Commission Analysis Dashboard", visible=False)
# Daily Revenue Table
with gr.Row():
daily_revenue_table = gr.DataFrame(
label="📅 Daily Revenue Breakdown",
headers=["Date", "Revenue (₽)", "Orders", "Commission (₽)", "Net Revenue (₽)"],
datatype=["str", "number", "number", "number", "number"],
interactive=False
)
# Event handlers
analyze_btn.click(
fn=analyze_sales_performance,
inputs=[period_selector, api_token_input],
outputs=[sales_summary, sales_chart, commission_chart, daily_revenue_table]
)
# Inventory Forecasting Tab
with gr.TabItem("📦 Inventory Forecasting"):
with gr.Row():
with gr.Column(scale=1):
forecast_method = gr.Dropdown(
choices=[
("Simple Division", "simple"),
("Safety Stock", "safety_stock"),
("Weighted Average", "weighted"),
("Seasonal Adjustment", "seasonal")
],
value="simple",
label="Forecasting Method"
)
forecast_btn = gr.Button("🔮 Calculate Forecast", variant="primary")
with gr.Column(scale=3):
forecast_summary = gr.Markdown("Enter your API token above and select a forecasting method, then click 'Calculate Forecast'.")
forecast_table = gr.DataFrame(
headers=["Product", "Current Stock", "Avg Daily Sales", "Days Until Stockout", "Risk Level"],
label="Inventory Forecast Results"
)
forecast_chart = gr.Plot(label="Inventory Risk Analysis")
# Event handlers
forecast_btn.click(
fn=calculate_stockout_forecast,
inputs=[forecast_method, api_token_input],
outputs=[forecast_summary, forecast_table, forecast_chart]
)
# Data Validation Tab
with gr.TabItem("🔍 Data Validation"):
with gr.Row():
with gr.Column(scale=1):
validation_btn = gr.Button("🔍 Validate Data Consistency", variant="primary")
with gr.Column(scale=3):
validation_results = gr.Markdown("Click 'Validate Data Consistency' to check for data quality issues.")
# Event handlers
def validate_wb_data_interface(api_token):
"""Interface function for data validation"""
try:
weekly_data = get_sales_data("week", api_token)
monthly_data = get_sales_data("month", api_token)
processed_data = validate_and_process_wb_data(weekly_data, monthly_data)
validation = processed_data["validation"]
mode_indicator = "Demo Mode" if not api_token or api_token.strip() == "" else "Live Data"
results_md = f"""
## Data Validation Results ({mode_indicator})
**Status**: {"✅ " + validation["status"].upper() if validation["status"] == "valid" else "❌ " + validation["status"].upper()}
### 📊 Data Summary
- **Weekly Records**: {len(weekly_data):,}
- **Monthly Records**: {len(monthly_data):,}
### ⚠️ Warnings ({len(validation["warnings"])})
"""
for warning in validation["warnings"]:
results_md += f"- {warning}\n"
if validation["errors"]:
results_md += f"\n### ❌ Errors ({len(validation['errors'])})\n"
for error in validation["errors"]:
results_md += f"- {error}\n"
if not validation["warnings"] and not validation["errors"]:
results_md += "\n✅ **No data quality issues detected!**"
return results_md
except Exception as e:
return f"❌ **Validation Error**: {str(e)}"
validation_btn.click(
fn=validate_wb_data_interface,
inputs=[api_token_input],
outputs=[validation_results]
)
# Documentation Tab
with gr.TabItem("📖 Documentation"):
gr.Markdown("""
## How to Use This Dashboard
### 🔧 Setup
1. **API Token**: Enter your Wildberries API token in the field above (required for real data)
2. **Get Token**: Login to your Wildberries seller account → Settings → API → Generate Token
3. **Permissions**: Ensure your token has access to Analytics and Statistics APIs
4. **Demo Mode**: Leave token field empty to explore with sample data
### 📊 Sales Analytics
- **Week Analysis**: Shows sales data for the last 7 days
- **Month Analysis**: Shows sales data for the last 30 days
- **Enhanced Metrics**: Commission analysis, net revenue, platform fees
- **Commission Dashboard**: Detailed commission breakdown by products
- **Pagination**: Automatically handles large datasets (80,000+ records)
### 📦 Inventory Forecasting
Choose from multiple forecasting methods:
- **Simple Division**: Current stock ÷ average daily sales
- **Safety Stock**: Includes buffer for demand variability
- **Weighted Average**: Recent sales weighted more heavily
- **Seasonal Adjustment**: Accounts for seasonal demand patterns
### 🚨 Risk Levels
- 🔴 **Critical** (< 7 days): Immediate action required
- 🟡 **Warning** (7-14 days): Monitor closely
- 🟢 **Safe** (> 14 days): Adequate stock levels
### 🔍 Data Validation
- **Consistency Checks**: Automatic validation of data quality
- **Duplicate Detection**: Identifies duplicate sales records
- **Data Aggregation**: Performance optimization for large datasets
### 🔗 API Information
This dashboard uses the [Wildberries API](https://dev.wildberries.ru/en/openapi/api-information):
- **Sales Endpoint**: `/api/v1/supplier/sales` (with automatic pagination)
- **Stocks Endpoint**: `/api/v1/supplier/stocks`
- **Rate Limits**: 300 requests/minute (respected automatically)
- **Data Retention**: Sales data available for 90 days
### 🛠️ Technical Details
- **Framework**: Gradio + FastMCP
- **Deployment**: Hugging Face Spaces
- **Data Processing**: Pandas + NumPy
- **Visualization**: Plotly
""")
gr.Markdown("""
---
💡 **Note**: This dashboard starts in demo mode with sample data. To access your real Wildberries data, enter your API token in the field above.
🔒 **Security**: Your token is only used during this session and is never stored or logged.
""")
return demo
# Launch the application
if __name__ == "__main__":
demo = create_interface()
demo.launch(
share=True, # Set to False for Hugging Face Spaces
server_name="0.0.0.0", # Required for Spaces
server_port=7860, # Default Gradio port
show_error=True,
#mcp_server=True
)