| | |
| | """ |
| | Demand Data Validation Visualization Module |
| | |
| | Provides Streamlit visualization for demand data validation. |
| | Shows which products are included/excluded from optimization and why. |
| | """ |
| |
|
| | import pandas as pd |
| | import streamlit as st |
| | from typing import Dict |
| | from src.config.constants import LineType |
| | from src.demand_filtering import DemandFilter |
| |
|
| |
|
| | |
| | LEVEL_NAMES = { |
| | 'prepack': 'prepack', |
| | 'subkit': 'subkit', |
| | 'master': { |
| | 'standalone': 'standalone_master', |
| | 'with_hierarchy': 'master_with_hierarchy' |
| | }, |
| | 'unclassified': 'no_hierarchy_data' |
| | } |
| |
|
| |
|
| | class DemandValidationViz: |
| | """ |
| | Simple visualization wrapper for demand filtering results. |
| | All filtering logic is in DemandFilter - this just displays the results. |
| | """ |
| | |
| | def __init__(self): |
| | self.filter_instance = DemandFilter() |
| | self.speed_data = None |
| | |
| | def load_data(self): |
| | """Load all data needed for visualization""" |
| | try: |
| | from src.config import optimization_config |
| | from src.preprocess import extract |
| | self.speed_data = extract.read_package_speed_data() |
| | return self.filter_instance.load_data() |
| | except Exception as e: |
| | error_msg = f"Error loading data: {str(e)}" |
| | print(error_msg) |
| | if st: |
| | st.error(error_msg) |
| | return False |
| | |
| | def validate_all_products(self) -> pd.DataFrame: |
| | """ |
| | Create DataFrame with validation results for all products. |
| | Main visualization method - converts filtering results to displayable format. |
| | """ |
| | |
| | analysis = self.filter_instance.get_complete_product_analysis() |
| | product_details = analysis['product_details'] |
| | |
| | results = [] |
| | for product_id, details in product_details.items(): |
| | |
| | speed = self.speed_data.get(product_id) if self.speed_data else None |
| | production_hours = (details['demand'] / speed) if speed and speed > 0 else None |
| | |
| | |
| | line_type_id = details['line_assignment'] |
| | line_name = LineType.get_name(line_type_id) if line_type_id is not None else "no_assignment" |
| | |
| | |
| | ptype = details['product_type'] |
| | if ptype == 'unclassified': |
| | level_name = LEVEL_NAMES['unclassified'] |
| | elif ptype == 'master': |
| | level_name = LEVEL_NAMES['master']['standalone' if details['is_standalone_master'] else 'with_hierarchy'] |
| | else: |
| | level_name = LEVEL_NAMES.get(ptype, f"level_{ptype}") |
| | |
| | |
| | if not details['is_included_in_optimization']: |
| | validation_status = f"π« Excluded: {', '.join(details['exclusion_reasons'])}" |
| | else: |
| | issues = [] |
| | if speed is None: |
| | issues.append("missing_speed_data (will use default)") |
| | if not details['has_hierarchy']: |
| | issues.append("no_hierarchy_data") |
| | validation_status = f"β οΈ Data Issues: {', '.join(issues)}" if issues else "β
Ready for optimization" |
| |
|
| |
|
| |
|
| | if details['has_too_high_demand']: |
| | issues.append("too_high_demand") |
| | validation_status = f"β οΈ Data Issues: {', '.join(issues)}" if issues else "β
Ready for optimization" |
| | results.append({ |
| | 'Product ID': product_id, |
| | 'Demand': details['demand'], |
| | 'Product Type': ptype.title(), |
| | 'Level': level_name, |
| | 'Is Standalone Master': "Yes" if details['is_standalone_master'] else "No", |
| | 'Line Type ID': line_type_id if line_type_id else "N/A", |
| | 'Line Type': line_name, |
| | 'UNICEF Staff': details['unicef_staff'], |
| | 'Humanizer Staff': details['humanizer_staff'], |
| | 'Total Staff': details['total_staff'], |
| | 'Production Speed (units/hour)': f"{speed:.1f}" if speed else "N/A", |
| | 'Production Hours Needed': f"{production_hours:.1f}" if production_hours else "N/A", |
| | 'Has Line Assignment': "β
" if details['has_line_assignment'] else "β", |
| | 'Has Staffing Data': "β
" if details['has_staffing'] else "β", |
| | 'Has Speed Data': "β
" if speed is not None else "β (will use default)", |
| | 'Has Hierarchy Data': "β
" if details['has_hierarchy'] else "β", |
| | 'Excluded from Optimization': not details['is_included_in_optimization'], |
| | 'Exclusion Reasons': ', '.join(details['exclusion_reasons']) if details['exclusion_reasons'] else '', |
| | 'Data Quality Issues': ', '.join(issues) if details['is_included_in_optimization'] and 'issues' in locals() and issues else '', |
| | 'Has Too High Demand': "β
" if details['has_too_high_demand'] else "β", |
| | 'Validation Status': validation_status |
| | }) |
| | |
| | df = pd.DataFrame(results) |
| | df = df.sort_values(['Excluded from Optimization', 'Demand'], ascending=[False, False]) |
| | return df |
| | |
| | def get_summary_statistics(self, df: pd.DataFrame) -> Dict: |
| | """Calculate summary statistics from validation results""" |
| | analysis = self.filter_instance.get_complete_product_analysis() |
| | included_df = df[df['Excluded from Optimization'] == False] |
| | |
| | return { |
| | 'total_products': analysis['total_products'], |
| | 'total_demand': analysis['total_demand'], |
| | 'included_products': analysis['included_count'], |
| | 'excluded_products': analysis['excluded_count'], |
| | 'included_demand': analysis['included_demand'], |
| | 'excluded_demand': analysis['excluded_demand'], |
| | 'type_counts': df['Product Type'].value_counts().to_dict(), |
| | 'no_line_assignment': len(included_df[included_df['Has Line Assignment'] == "β"]), |
| | 'no_staffing': len(included_df[included_df['Has Staffing Data'] == "β"]), |
| | 'no_speed': len(included_df[included_df['Has Speed Data'].str.contains("β")]), |
| | 'no_hierarchy': len(included_df[included_df['Has Hierarchy Data'] == "β"]), |
| | 'standalone_masters': analysis['standalone_masters_count'], |
| | 'total_unicef_needed': sum(p['unicef_staff'] for p in analysis['product_details'].values()), |
| | 'total_humanizer_needed': sum(p['humanizer_staff'] for p in analysis['product_details'].values()), |
| | 'excluded_with_too_high_demand': analysis['excluded_with_too_high_demand_count'] |
| | } |
| |
|
| |
|
| | def display_demand_validation(): |
| | """ |
| | Display demand validation analysis in Streamlit. |
| | Main entry point for the validation page. |
| | """ |
| | st.header("π Demand Data Validation") |
| | st.markdown("Analysis showing which products are included/excluded from optimization and data quality status.") |
| | |
| | |
| | validator = DemandValidationViz() |
| | with st.spinner("Loading and analyzing data..."): |
| | if not validator.load_data(): |
| | st.error("Failed to load data for validation.") |
| | return |
| | validation_df = validator.validate_all_products() |
| | stats = validator.get_summary_statistics(validation_df) |
| | |
| | |
| | st.subheader("π Summary Statistics") |
| | col1, col2, col3, col4 = st.columns(4) |
| | col1.metric("Total Products", stats['total_products']) |
| | col1.metric("Included in Optimization", stats['included_products'], delta="Ready") |
| | col2.metric("Total Demand", f"{stats['total_demand']:,}") |
| | col2.metric("Excluded from Optimization", stats['excluded_products'], delta="Omitted") |
| | col3.metric("Included Demand", f"{stats['included_demand']:,}", delta="Will be optimized") |
| | col3.metric("UNICEF Staff Needed", stats['total_unicef_needed']) |
| | col4.metric("Excluded Demand", f"{stats['excluded_demand']:,}", delta="Omitted") |
| | col4.metric("Humanizer Staff Needed", stats['total_humanizer_needed']) |
| | |
| | |
| | st.subheader("π Product Type Distribution") |
| | if stats['type_counts']: |
| | col1, col2 = st.columns(2) |
| | with col1: |
| | type_df = pd.DataFrame(list(stats['type_counts'].items()), columns=['Product Type', 'Count']) |
| | st.bar_chart(type_df.set_index('Product Type')) |
| | with col2: |
| | for ptype, count in stats['type_counts'].items(): |
| | percentage = (count / stats['total_products']) * 100 |
| | st.write(f"**{ptype}:** {count} products ({percentage:.1f}%)") |
| | |
| | |
| | st.subheader("β οΈ Data Quality Issues (Included Products)") |
| | st.write("Issues affecting products that **will be** included in optimization:") |
| | col1, col2, col3, col4 = st.columns(4) |
| | col1.metric("No Line Assignment", stats['no_line_assignment'], |
| | delta=None if stats['no_line_assignment'] == 0 else "Issue") |
| | col2.metric("No Staffing Data", stats['no_staffing'], |
| | delta=None if stats['no_staffing'] == 0 else "Issue") |
| | col3.metric("No Speed Data", stats['no_speed'], |
| | delta=None if stats['no_speed'] == 0 else "Will use default") |
| | col4.metric("No Hierarchy Data", stats['no_hierarchy'], |
| | delta=None if stats['no_hierarchy'] == 0 else "Issue") |
| | col5.metric("Excluded: Too High Demand", stats['excluded_with_too_high_demand'], |
| | delta=None if stats['excluded_with_too_high_demand'] == 0 else "Excluded") |
| | |
| | included_df = validation_df[validation_df['Excluded from Optimization'] == False].copy() |
| | excluded_df = validation_df[validation_df['Excluded from Optimization'] == True].copy() |
| | |
| | st.subheader("β
Products Included in Optimization") |
| | st.write(f"**{len(included_df)} products** with total demand of **{included_df['Demand'].sum():,} units**") |
| | |
| | if len(included_df) > 0: |
| | |
| | col1, col2 = st.columns(2) |
| | type_filter = col1.selectbox("Filter by type", ["All"] + list(included_df['Product Type'].unique()), key="inc_filter") |
| | min_demand = col2.number_input("Minimum demand", min_value=0, value=0, key="inc_demand") |
| | |
| | |
| | filtered = included_df.copy() |
| | if type_filter != "All": |
| | filtered = filtered[filtered['Product Type'] == type_filter] |
| | if min_demand > 0: |
| | filtered = filtered[filtered['Demand'] >= min_demand] |
| | |
| | |
| | display_cols = ['Product ID', 'Demand', 'Product Type', 'Line Type', 'UNICEF Staff', |
| | 'Humanizer Staff', 'Production Speed (units/hour)', 'Data Quality Issues', 'Validation Status'] |
| | st.dataframe(filtered[display_cols], use_container_width=True, height=300) |
| | else: |
| | st.warning("No products are included in optimization!") |
| | |
| | |
| | st.subheader("π« Products Excluded from Optimization") |
| | st.write(f"**{len(excluded_df)} products** with total demand of **{excluded_df['Demand'].sum():,} units**") |
| | st.info("Excluded due to: missing line assignments, zero staffing, or non-standalone masters") |
| | |
| | if len(excluded_df) > 0: |
| | |
| | st.write("**Exclusion reasons:**") |
| | for reason, count in excluded_df['Exclusion Reasons'].value_counts().items(): |
| | st.write(f"β’ {reason}: {count} products") |
| | |
| | |
| | display_cols = ['Product ID', 'Demand', 'Product Type', 'Exclusion Reasons', |
| | 'UNICEF Staff', 'Humanizer Staff', 'Line Type'] |
| | st.dataframe(excluded_df[display_cols], use_container_width=True, height=200) |
| | |
| | |
| | if st.button("π₯ Export Validation Results to CSV"): |
| | st.download_button("Download CSV", validation_df.to_csv(index=False), |
| | file_name="demand_validation_results.csv", mime="text/csv") |
| | |
| | |
| | st.subheader("π‘ Recommendations") |
| | |
| | if stats['excluded_products'] > 0: |
| | st.warning(f"**{stats['excluded_products']} products** ({stats['excluded_demand']:,} units) excluded from optimization") |
| | |
| | |
| | if stats['no_line_assignment'] > 0: |
| | st.info(f"**Line Assignment**: {stats['no_line_assignment']} included products missing line assignments") |
| | if stats['no_staffing'] > 0: |
| | st.info(f"**Staffing Data**: {stats['no_staffing']} included products missing staffing requirements") |
| | if stats['no_speed'] > 0: |
| | st.info(f"**Speed Data**: {stats['no_speed']} included products missing speed data (will use default 106.7 units/hour)") |
| | if stats['no_hierarchy'] > 0: |
| | st.info(f"**Hierarchy Data**: {stats['no_hierarchy']} included products not in kit hierarchy") |
| | |
| | |
| | if stats['included_products'] > 0: |
| | st.success(f"β
**{stats['included_products']} products** with {stats['included_demand']:,} units demand ready for optimization!") |
| | if stats['no_speed'] == 0 and stats['no_hierarchy'] == 0: |
| | st.info("π All included products have complete data!") |
| | else: |
| | st.error("β No products passed filtering. Review exclusion reasons and check data configuration.") |
| |
|
| |
|
| | if __name__ == "__main__": |
| | |
| | display_demand_validation() |
| |
|