# 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 )