WB_Analyzer / dashboard.py
bakyt92's picture
updated dashboard.py
63bd99c
"""
Dashboard visualization components for Wildberries Analytics
Creates interactive charts and visualizations using Plotly
"""
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
import logging
from wildberries_client import validate_wb_data, aggregate_wb_data
logger = logging.getLogger(__name__)
def create_sales_dashboard(sales_data: pd.DataFrame, period: str = "week", daily_revenue_data: pd.DataFrame = None) -> go.Figure:
"""
Create comprehensive sales dashboard with multiple visualizations
Args:
sales_data: DataFrame with sales data
period: Analysis period ("week" or "month")
daily_revenue_data: Pre-calculated daily revenue data (optional)
Returns:
Plotly figure with sales dashboard
"""
if sales_data.empty:
return create_empty_chart("No sales data available")
# Create subplot layout with better spacing
fig = make_subplots(
rows=3, cols=1,
subplot_titles=[
"πŸ“ˆ Daily Revenue Trend",
"πŸ† Top Products by Revenue",
"πŸ“Š Sales by Category"
],
specs=[[{"secondary_y": True}],
[{"type": "bar"}],
[{"type": "pie"}]],
vertical_spacing=0.15,
row_heights=[0.4, 0.35, 0.25]
)
try:
# 1. Daily Revenue Trend (Top Left) - Use pre-calculated data if available
if daily_revenue_data is not None and not daily_revenue_data.empty:
# Use the pre-calculated daily revenue data to ensure consistency
daily_revenue = daily_revenue_data.copy()
elif 'sale_date' in sales_data.columns and 'total_price' in sales_data.columns:
# Fallback to calculating if not provided
daily_revenue = sales_data.groupby(sales_data['sale_date'].dt.date).agg({
'total_price': 'sum',
'quantity': 'sum'
}).reset_index()
else:
daily_revenue = pd.DataFrame()
if not daily_revenue.empty:
# Revenue line
fig.add_trace(
go.Scatter(
x=daily_revenue['sale_date'],
y=daily_revenue['total_price'],
mode='lines+markers',
name='Revenue (β‚½)',
line=dict(color='#2E86AB', width=3),
marker=dict(size=8),
hovertemplate='<b>%{x}</b><br>Revenue: β‚½%{y:,.0f}<extra></extra>'
),
row=1, col=1
)
# Quantity bars on secondary y-axis
fig.add_trace(
go.Bar(
x=daily_revenue['sale_date'],
y=daily_revenue['quantity'],
name='Quantity',
marker_color='rgba(46, 134, 171, 0.3)',
yaxis='y2',
hovertemplate='<b>%{x}</b><br>Quantity: %{y}<extra></extra>'
),
row=1, col=1, secondary_y=True
)
# 2. Top Products by Revenue (Second Row)
if 'product_name' in sales_data.columns and 'total_price' in sales_data.columns:
top_products = sales_data.groupby('product_name')['total_price'].sum().nlargest(8).reset_index()
fig.add_trace(
go.Bar(
x=top_products['total_price'],
y=top_products['product_name'],
orientation='h',
name='Top Products',
marker_color='#A23B72',
hovertemplate='<b>%{y}</b><br>Revenue: β‚½%{x:,.0f}<extra></extra>'
),
row=2, col=1
)
# 3. Sales by Category (Third Row)
if 'category' in sales_data.columns and 'total_price' in sales_data.columns:
category_sales = sales_data.groupby('category')['total_price'].sum().reset_index()
fig.add_trace(
go.Pie(
labels=category_sales['category'],
values=category_sales['total_price'],
name='Categories',
hovertemplate='<b>%{label}</b><br>Revenue: β‚½%{value:,.0f}<br>Percent: %{percent}<extra></extra>',
marker_colors=px.colors.qualitative.Set3
),
row=3, col=1
)
except Exception as e:
logger.error(f"Error creating sales dashboard: {str(e)}")
return create_empty_chart(f"Error creating dashboard: {str(e)}")
# Update layout
fig.update_layout(
title=f"πŸ“Š Sales Analytics Dashboard - Last {period.title()}",
title_x=0.5,
showlegend=False,
height=1100,
font=dict(size=12),
template="plotly_white",
margin=dict(t=120, b=80, l=80, r=80)
)
# Update axes labels
fig.update_xaxes(title_text="Date", row=1, col=1)
fig.update_yaxes(title_text="Revenue (β‚½)", row=1, col=1)
fig.update_yaxes(title_text="Quantity", secondary_y=True, row=1, col=1)
fig.update_xaxes(title_text="Revenue (β‚½)", row=2, col=1)
fig.update_yaxes(title_text="Products", row=2, col=1)
return fig
def create_inventory_dashboard(forecast_data: pd.DataFrame) -> go.Figure:
"""
Create inventory risk analysis dashboard
Args:
forecast_data: DataFrame with forecast results
Returns:
Plotly figure with inventory dashboard
"""
if forecast_data.empty:
return create_empty_chart("No inventory data available")
# Create subplot layout
fig = make_subplots(
rows=3, cols=1,
subplot_titles=[
"🚨 Risk Level Distribution",
"⏰ Days Until Stockout",
"πŸ“¦ Current Stock Levels"
],
specs=[[{"type": "pie"}],
[{"type": "bar"}],
[{"type": "bar"}]],
vertical_spacing=0.15,
row_heights=[0.35, 0.35, 0.30]
)
try:
# 1. Risk Level Distribution (Top Left)
if 'risk_level' in forecast_data.columns:
risk_counts = forecast_data['risk_level'].value_counts()
colors = {
'πŸ”΄ Critical': '#FF4444',
'🟑 Warning': '#FFAA00',
'🟒 Safe': '#44AA44',
'❌ Error': '#888888'
}
fig.add_trace(
go.Pie(
labels=risk_counts.index,
values=risk_counts.values,
name='Risk Levels',
marker_colors=[colors.get(label, '#CCCCCC') for label in risk_counts.index],
hovertemplate='<b>%{label}</b><br>Count: %{value}<br>Percent: %{percent}<extra></extra>'
),
row=1, col=1
)
# 2. Days Until Stockout (Top Right)
if 'days_until_stockout' in forecast_data.columns and 'product_name' in forecast_data.columns:
# Sort by days until stockout and take top 15 for readability
sorted_data = forecast_data.nsmallest(15, 'days_until_stockout')
# Color bars based on risk level
bar_colors = []
for days in sorted_data['days_until_stockout']:
if days < 7:
bar_colors.append('#FF4444')
elif days < 14:
bar_colors.append('#FFAA00')
else:
bar_colors.append('#44AA44')
fig.add_trace(
go.Bar(
x=sorted_data['days_until_stockout'],
y=sorted_data['product_name'],
orientation='h',
name='Days Until Stockout',
marker_color=bar_colors,
hovertemplate='<b>%{y}</b><br>Days: %{x:.1f}<extra></extra>'
),
row=2, col=1
)
# 3. Current Stock Levels (Third Row)
if 'current_stock' in forecast_data.columns and 'product_name' in forecast_data.columns:
# Take top 10 products by stock level for better visibility
stock_data = forecast_data.nlargest(10, 'current_stock')
fig.add_trace(
go.Bar(
x=stock_data['product_name'],
y=stock_data['current_stock'],
name='Current Stock',
marker_color='#2E86AB',
hovertemplate='<b>%{x}</b><br>Stock: %{y}<extra></extra>'
),
row=3, col=1
)
except Exception as e:
logger.error(f"Error creating inventory dashboard: {str(e)}")
return create_empty_chart(f"Error creating dashboard: {str(e)}")
# Update layout
fig.update_layout(
title="πŸ“¦ Inventory Risk Analysis Dashboard",
title_x=0.5,
showlegend=False,
height=1100,
font=dict(size=12),
template="plotly_white",
margin=dict(t=120, b=80, l=80, r=80)
)
# Update axes
fig.update_xaxes(title_text="Days", row=2, col=1)
fig.update_yaxes(title_text="Products", row=2, col=1)
fig.update_xaxes(title_text="Products", row=3, col=1, tickangle=45)
fig.update_yaxes(title_text="Stock Quantity", row=3, col=1)
return fig
def create_trend_chart(data: pd.DataFrame,
date_col: str = 'sale_date',
value_col: str = 'total_price',
title: str = "Trend Analysis") -> go.Figure:
"""
Create a trend analysis chart
Args:
data: DataFrame with time series data
date_col: Name of date column
value_col: Name of value column
title: Chart title
Returns:
Plotly figure with trend chart
"""
if data.empty:
return create_empty_chart("No data available for trend analysis")
try:
# Group by date
if date_col in data.columns and value_col in data.columns:
daily_data = data.groupby(data[date_col].dt.date)[value_col].sum().reset_index()
fig = go.Figure()
# Add main trend line
fig.add_trace(
go.Scatter(
x=daily_data[date_col],
y=daily_data[value_col],
mode='lines+markers',
name='Daily Values',
line=dict(color='#2E86AB', width=3),
marker=dict(size=8),
hovertemplate='<b>%{x}</b><br>Value: %{y:,.0f}<extra></extra>'
)
)
# Add moving average if enough data points
if len(daily_data) >= 7:
daily_data['ma7'] = daily_data[value_col].rolling(window=7, center=True).mean()
fig.add_trace(
go.Scatter(
x=daily_data[date_col],
y=daily_data['ma7'],
mode='lines',
name='7-day Moving Average',
line=dict(color='#A23B72', width=2, dash='dash'),
hovertemplate='<b>%{x}</b><br>7-day MA: %{y:,.0f}<extra></extra>'
)
)
fig.update_layout(
title=title,
title_x=0.5,
xaxis_title="Date",
yaxis_title="Value",
template="plotly_white",
height=400
)
return fig
else:
return create_empty_chart(f"Required columns not found: {date_col}, {value_col}")
except Exception as e:
logger.error(f"Error creating trend chart: {str(e)}")
return create_empty_chart(f"Error creating chart: {str(e)}")
def create_comparison_chart(data: pd.DataFrame,
category_col: str = 'category',
value_col: str = 'total_price',
title: str = "Category Comparison") -> go.Figure:
"""
Create a comparison chart for categories
Args:
data: DataFrame with categorical data
category_col: Name of category column
value_col: Name of value column
title: Chart title
Returns:
Plotly figure with comparison chart
"""
if data.empty:
return create_empty_chart("No data available for comparison")
try:
if category_col in data.columns and value_col in data.columns:
category_data = data.groupby(category_col)[value_col].sum().sort_values(ascending=True)
fig = go.Figure()
fig.add_trace(
go.Bar(
x=category_data.values,
y=category_data.index,
orientation='h',
marker_color=px.colors.qualitative.Set3,
hovertemplate='<b>%{y}</b><br>Value: %{x:,.0f}<extra></extra>'
)
)
fig.update_layout(
title=title,
title_x=0.5,
xaxis_title="Value",
yaxis_title="Category",
template="plotly_white",
height=400
)
return fig
else:
return create_empty_chart(f"Required columns not found: {category_col}, {value_col}")
except Exception as e:
logger.error(f"Error creating comparison chart: {str(e)}")
return create_empty_chart(f"Error creating chart: {str(e)}")
def create_empty_chart(message: str = "No data available") -> go.Figure:
"""
Create an empty chart with a message
Args:
message: Message to display
Returns:
Plotly figure with message
"""
fig = go.Figure()
fig.add_annotation(
x=0.5,
y=0.5,
xref="paper",
yref="paper",
text=message,
showarrow=False,
font=dict(size=16, color="gray"),
xanchor="center",
yanchor="middle"
)
fig.update_layout(
template="plotly_white",
height=400,
xaxis=dict(showgrid=False, showticklabels=False, zeroline=False),
yaxis=dict(showgrid=False, showticklabels=False, zeroline=False)
)
return fig
def create_kpi_cards(data: pd.DataFrame) -> Dict[str, Any]:
"""
Calculate KPIs for dashboard cards
Args:
data: DataFrame with sales/inventory data
Returns:
Dictionary with KPI values
"""
if data.empty:
return {
"total_revenue": 0,
"total_orders": 0,
"avg_order_value": 0,
"top_product": "N/A"
}
kpis = {}
try:
# Revenue metrics
if 'total_price' in data.columns:
kpis['total_revenue'] = data['total_price'].sum()
kpis['avg_order_value'] = data['total_price'].mean()
else:
kpis['total_revenue'] = 0
kpis['avg_order_value'] = 0
# Order count (net orders: sales minus returns)
if 'is_return' in data.columns:
sales_count = (~data['is_return']).sum()
returns_count = data['is_return'].sum()
kpis['total_orders'] = sales_count - returns_count
kpis['sales_count'] = sales_count
kpis['returns_count'] = returns_count
else:
kpis['total_orders'] = len(data)
# Top product
if 'product_name' in data.columns and 'total_price' in data.columns:
top_product = data.groupby('product_name')['total_price'].sum().idxmax()
kpis['top_product'] = top_product
else:
kpis['top_product'] = "N/A"
# Growth calculations (if date column available)
if 'sale_date' in data.columns:
# Calculate week-over-week growth
current_week = data[data['sale_date'] >= (datetime.now() - timedelta(days=7))]
previous_week = data[
(data['sale_date'] >= (datetime.now() - timedelta(days=14))) &
(data['sale_date'] < (datetime.now() - timedelta(days=7)))
]
current_revenue = current_week['total_price'].sum() if 'total_price' in data.columns else 0
previous_revenue = previous_week['total_price'].sum() if 'total_price' in data.columns else 0
if previous_revenue > 0:
growth_rate = ((current_revenue - previous_revenue) / previous_revenue) * 100
kpis['revenue_growth'] = round(growth_rate, 1)
else:
kpis['revenue_growth'] = 0
except Exception as e:
logger.error(f"Error calculating KPIs: {str(e)}")
kpis = {
"total_revenue": 0,
"total_orders": 0,
"avg_order_value": 0,
"top_product": "Error calculating",
"revenue_growth": 0
}
return kpis
def format_metric(value: float, metric_type: str = "currency") -> str:
"""
Format metrics for display
Args:
value: Numeric value to format
metric_type: Type of metric ("currency", "number", "percent")
Returns:
Formatted string
"""
if pd.isna(value) or value is None:
return "N/A"
try:
if metric_type == "currency":
return f"β‚½{value:,.0f}"
elif metric_type == "percent":
return f"{value:+.1f}%"
elif metric_type == "number":
return f"{value:,.0f}"
else:
return str(value)
except:
return "N/A"
def create_wb_kpi_cards(data: pd.DataFrame) -> Dict[str, Any]:
"""Enhanced KPIs specifically for Wildberries data"""
kpis = create_kpi_cards(data) # Use existing function
try:
# Add Wildberries-specific metrics
if 'sales_commission' in data.columns:
total_commission = data['sales_commission'].sum()
kpis['total_commission'] = total_commission
if 'total_price' in data.columns and data['total_price'].sum() > 0:
commission_rate = (total_commission / data['total_price'].sum() * 100)
kpis['avg_commission_rate'] = commission_rate
else:
kpis['avg_commission_rate'] = 0
if 'amount_for_pay' in data.columns:
kpis['total_payout'] = data['amount_for_pay'].sum()
# Calculate net profit (amount_for_pay is what seller receives)
if 'total_price' in data.columns:
gross_revenue = data['total_price'].sum()
net_revenue = data['amount_for_pay'].sum()
kpis['platform_fees'] = gross_revenue - net_revenue
kpis['net_margin_percent'] = (net_revenue / gross_revenue * 100) if gross_revenue > 0 else 0
if 'office_name' in data.columns:
office_sales = data.groupby('office_name')['total_price'].sum()
if not office_sales.empty:
kpis['top_office'] = office_sales.idxmax()
kpis['top_office_revenue'] = office_sales.max()
if 'delivery_cost' in data.columns:
kpis['total_delivery_cost'] = data['delivery_cost'].sum()
# Calculate sales velocity (average daily sales)
if 'sale_date' in data.columns and not data.empty:
date_range = (data['sale_date'].max() - data['sale_date'].min()).days
if date_range > 0:
kpis['daily_sales_velocity'] = len(data) / date_range
else:
kpis['daily_sales_velocity'] = len(data)
except Exception as e:
logger.error(f"Error calculating Wildberries KPIs: {str(e)}")
return kpis
def create_commission_analysis_chart(sales_data: pd.DataFrame) -> go.Figure:
"""Create commission analysis visualization for Wildberries data"""
if sales_data.empty or 'sales_commission' not in sales_data.columns:
return create_empty_chart("No commission data available")
fig = make_subplots(
rows=2, cols=2,
subplot_titles=[
"Commission by Product",
"Commission Rate Trend",
"Revenue vs Commission",
"Top Commission Products"
],
specs=[[{"type": "bar"}, {"type": "scatter"}],
[{"type": "scatter"}, {"type": "bar"}]],
vertical_spacing=0.1,
horizontal_spacing=0.1
)
try:
# 1. Commission by Product (Top 10)
if 'product_name' in sales_data.columns:
product_commission = sales_data.groupby('product_name')['sales_commission'].sum().nlargest(10)
fig.add_trace(
go.Bar(
x=product_commission.index,
y=product_commission.values,
name='Commission',
marker_color='#FF6B6B',
hovertemplate='<b>%{x}</b><br>Commission: β‚½%{y:,.0f}<extra></extra>'
),
row=1, col=1
)
# 2. Daily commission trend
if 'sale_date' in sales_data.columns:
daily_commission = sales_data.groupby(sales_data['sale_date'].dt.date)['sales_commission'].sum()
fig.add_trace(
go.Scatter(
x=daily_commission.index,
y=daily_commission.values,
mode='lines+markers',
name='Daily Commission',
line=dict(color='#4ECDC4', width=2),
hovertemplate='<b>%{x}</b><br>Commission: β‚½%{y:,.0f}<extra></extra>'
),
row=1, col=2
)
# 3. Revenue vs Commission scatter
if 'total_price' in sales_data.columns and 'product_name' in sales_data.columns:
product_data = sales_data.groupby('product_name').agg({
'total_price': 'sum',
'sales_commission': 'sum'
}).reset_index()
fig.add_trace(
go.Scatter(
x=product_data['total_price'],
y=product_data['sales_commission'],
mode='markers',
name='Revenue vs Commission',
marker=dict(
size=8,
color='#45B7D1',
opacity=0.7
),
text=product_data['product_name'],
hovertemplate='<b>%{text}</b><br>Revenue: β‚½%{x:,.0f}<br>Commission: β‚½%{y:,.0f}<extra></extra>'
),
row=2, col=1
)
# 4. Commission rate by product
if 'product_name' in sales_data.columns and 'total_price' in sales_data.columns:
product_stats = sales_data.groupby('product_name').agg({
'total_price': 'sum',
'sales_commission': 'sum'
})
product_stats['commission_rate'] = (product_stats['sales_commission'] / product_stats['total_price'] * 100)
top_rates = product_stats['commission_rate'].nlargest(10)
fig.add_trace(
go.Bar(
x=top_rates.index,
y=top_rates.values,
name='Commission Rate (%)',
marker_color='#96CEB4',
hovertemplate='<b>%{x}</b><br>Commission Rate: %{y:.1f}%<extra></extra>'
),
row=2, col=2
)
except Exception as e:
logger.error(f"Error creating commission analysis: {str(e)}")
return create_empty_chart(f"Error creating commission analysis: {str(e)}")
# Update layout
fig.update_layout(
title="πŸ’° Commission Analysis Dashboard",
title_x=0.5,
showlegend=False,
height=800,
font=dict(size=10),
template="plotly_white"
)
# Update axes
fig.update_xaxes(title_text="Products", row=1, col=1)
fig.update_yaxes(title_text="Commission (β‚½)", row=1, col=1)
fig.update_xaxes(title_text="Date", row=1, col=2)
fig.update_yaxes(title_text="Commission (β‚½)", row=1, col=2)
fig.update_xaxes(title_text="Revenue (β‚½)", row=2, col=1)
fig.update_yaxes(title_text="Commission (β‚½)", row=2, col=1)
fig.update_xaxes(title_text="Products", row=2, col=2)
fig.update_yaxes(title_text="Commission Rate (%)", row=2, col=2)
return fig
def validate_and_process_wb_data(weekly_data: pd.DataFrame, monthly_data: pd.DataFrame) -> Dict[str, Any]:
"""Validate and process Wildberries data with enhanced error handling"""
# Run validation
validation_results = validate_wb_data(weekly_data, monthly_data)
# Process data for better performance if large datasets
processed_data = {
"weekly": weekly_data,
"monthly": monthly_data,
"validation": validation_results
}
# Aggregate data if datasets are large (>1000 records)
if len(weekly_data) > 1000:
processed_data["weekly_aggregated"] = aggregate_wb_data(weekly_data, 'daily')
logger.info(f"Aggregated weekly data from {len(weekly_data)} to {len(processed_data['weekly_aggregated'])} records")
if len(monthly_data) > 1000:
processed_data["monthly_aggregated"] = aggregate_wb_data(monthly_data, 'daily')
logger.info(f"Aggregated monthly data from {len(monthly_data)} to {len(processed_data['monthly_aggregated'])} records")
return processed_data