Spaces:
Running
Running
| # 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 | |
| ) |