entropy25 commited on
Commit
41c0f80
Β·
verified Β·
1 Parent(s): c23e463

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +451 -5
app.py CHANGED
@@ -513,16 +513,16 @@ def create_pdf_charts(df, stats):
513
  try:
514
  daily_data = df.groupby('date')['weight_kg'].sum().reset_index()
515
  if len(daily_data) > 0:
516
- fig_trend = px.line(daily_data, x='date', y='weight_kg', title="Daily Production Trend",
517
- labels={'date': 'Date', 'weight_kg': 'Weight (kg)'})
518
  charts['trend'] = save_plotly_as_image(fig_trend, "trend.png")
519
  except:
520
  pass
521
 
522
  if len(materials) > 0 and len(values) > 0:
523
  try:
524
- fig_bar = px.bar(x=labels, y=values, title="Production by Material Type",
525
- labels={'x': 'Material Type', 'y': 'Weight (kg)'})
526
  charts['bar'] = save_plotly_as_image(fig_bar, "materials.png")
527
  except:
528
  pass
@@ -603,4 +603,450 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
603
  Total output achieved: <b>{total_production:,.0f} kg</b> with an average
604
  daily production of <b>{daily_avg:,.0f} kg</b>.
605
  <br/><br/>
606
- <b>Key Highlights:</b><br/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  try:
514
  daily_data = df.groupby('date')['weight_kg'].sum().reset_index()
515
  if len(daily_data) > 0:
516
+ fig_trend = px.line(daily_data, x='date', y='weight_kg', title="Daily Production Trend")
517
+ fig_trend.update_layout(xaxis_title="Date", yaxis_title="Weight (kg)")
518
  charts['trend'] = save_plotly_as_image(fig_trend, "trend.png")
519
  except:
520
  pass
521
 
522
  if len(materials) > 0 and len(values) > 0:
523
  try:
524
+ fig_bar = px.bar(x=labels, y=values, title="Production by Material Type")
525
+ fig_bar.update_layout(xaxis_title="Material Type", yaxis_title="Weight (kg)")
526
  charts['bar'] = save_plotly_as_image(fig_bar, "materials.png")
527
  except:
528
  pass
 
603
  Total output achieved: <b>{total_production:,.0f} kg</b> with an average
604
  daily production of <b>{daily_avg:,.0f} kg</b>.
605
  <br/><br/>
606
+ <b>Key Highlights:</b><br/>
607
+ β€’ Total production: {total_production:,.0f} kg<br/>
608
+ β€’ Daily average: {daily_avg:,.0f} kg<br/>
609
+ β€’ Materials tracked: {len([k for k in stats.keys() if k != '_total_'])}<br/>
610
+ β€’ Data quality: {len(df):,} records processed
611
+ </para>
612
+ """
613
+ elements.append(Paragraph(exec_summary, styles['Normal']))
614
+ elements.append(Spacer(1, 20))
615
+
616
+ elements.append(Paragraph("Production Summary", styles['Heading3']))
617
+
618
+ summary_data = [['Material Type', 'Total (kg)', 'Share (%)', 'Daily Avg (kg)']]
619
+
620
+ for material, info in stats.items():
621
+ if material != '_total_':
622
+ summary_data.append([
623
+ material.replace('_', ' ').title(),
624
+ f"{info['total']:,.0f}",
625
+ f"{info['percentage']:.1f}%",
626
+ f"{info['daily_avg']:,.0f}"
627
+ ])
628
+
629
+ summary_table = Table(summary_data, colWidths=[2*inch, 1.5*inch, 1*inch, 1.5*inch])
630
+ summary_table.setStyle(TableStyle([
631
+ ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
632
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
633
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
634
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
635
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
636
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
637
+ ]))
638
+
639
+ elements.append(summary_table)
640
+ elements.append(PageBreak())
641
+
642
+ elements.append(Paragraph("Production Analysis Charts", subtitle_style))
643
+
644
+ try:
645
+ charts = create_pdf_charts(df, stats)
646
+ except:
647
+ charts = {}
648
+
649
+ charts_added = False
650
+
651
+ chart_insights = {
652
+ 'pie': "Material distribution shows production allocation across different materials. Balanced distribution indicates diversified production capabilities.",
653
+ 'trend': "Production trend reveals operational patterns and seasonal variations. Consistent trends suggest stable operational efficiency.",
654
+ 'bar': "Material comparison highlights performance differences and production capacities. Top performers indicate optimization opportunities.",
655
+ 'shift': "Shift analysis reveals operational efficiency differences between day and night operations. Balance indicates effective resource utilization."
656
+ }
657
+
658
+ for chart_type, chart_title in [
659
+ ('pie', "Production Distribution"),
660
+ ('trend', "Production Trend"),
661
+ ('bar', "Material Comparison"),
662
+ ('shift', "Shift Analysis")
663
+ ]:
664
+ chart_path = charts.get(chart_type)
665
+ if chart_path and os.path.exists(chart_path):
666
+ try:
667
+ elements.append(Paragraph(chart_title, styles['Heading3']))
668
+ elements.append(Image(chart_path, width=6*inch, height=3*inch))
669
+
670
+ insight_text = f"<i>Analysis: {chart_insights.get(chart_type, 'Chart analysis not available.')}</i>"
671
+ elements.append(Paragraph(insight_text, ai_style))
672
+ elements.append(Spacer(1, 20))
673
+ charts_added = True
674
+ except Exception as e:
675
+ pass
676
+
677
+ if not charts_added:
678
+ elements.append(Paragraph("Charts Generation Failed", styles['Heading3']))
679
+ elements.append(Paragraph("Production Data Summary:", styles['Normal']))
680
+ for material, info in stats.items():
681
+ if material != '_total_':
682
+ summary_text = f"β€’ {material.replace('_', ' ').title()}: {info['total']:,.0f} kg ({info['percentage']:.1f}%)"
683
+ elements.append(Paragraph(summary_text, styles['Normal']))
684
+ elements.append(Spacer(1, 20))
685
+
686
+ elements.append(PageBreak())
687
+ elements.append(Paragraph("Quality Control Analysis", subtitle_style))
688
+
689
+ quality_data = [['Material', 'Outliers', 'Normal Range (kg)', 'Status']]
690
+
691
+ for material, info in outliers.items():
692
+ if info['count'] == 0:
693
+ status = "GOOD"
694
+ elif info['count'] <= 3:
695
+ status = "MONITOR"
696
+ else:
697
+ status = "ATTENTION"
698
+
699
+ quality_data.append([
700
+ material.replace('_', ' ').title(),
701
+ str(info['count']),
702
+ info['range'],
703
+ status
704
+ ])
705
+
706
+ quality_table = Table(quality_data, colWidths=[2*inch, 1*inch, 2*inch, 1.5*inch])
707
+ quality_table.setStyle(TableStyle([
708
+ ('BACKGROUND', (0, 0), (-1, 0), colors.darkred),
709
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
710
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
711
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
712
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
713
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
714
+ ]))
715
+
716
+ elements.append(quality_table)
717
+
718
+ if model:
719
+ elements.append(PageBreak())
720
+ elements.append(Paragraph("AI Intelligent Analysis", subtitle_style))
721
+
722
+ try:
723
+ ai_analysis = generate_ai_summary(model, df, stats, outliers)
724
+ except:
725
+ ai_analysis = "AI analysis temporarily unavailable."
726
+
727
+ ai_paragraphs = ai_analysis.split('\n\n')
728
+ for paragraph in ai_paragraphs:
729
+ if paragraph.strip():
730
+ formatted_text = paragraph.replace('**', '<b>', 1).replace('**', '</b>', 1)
731
+ formatted_text = formatted_text.replace('β€’', ' β€’')
732
+ elements.append(Paragraph(formatted_text, styles['Normal']))
733
+ elements.append(Spacer(1, 8))
734
+ else:
735
+ elements.append(PageBreak())
736
+ elements.append(Paragraph("AI Analysis", subtitle_style))
737
+ elements.append(Paragraph("AI analysis unavailable - API key not configured. Please configure Google AI API key to enable intelligent insights.", styles['Normal']))
738
+
739
+ elements.append(Spacer(1, 30))
740
+ footer_text = f"""
741
+ <para alignment="center">
742
+ <i>This report was generated by Production Monitor System<br/>
743
+ Nilsen Service &amp; Consulting AS - Production Analytics Division<br/>
744
+ Report contains {len(df):,} data records across {stats['_total_']['work_days']} working days</i>
745
+ </para>
746
+ """
747
+ elements.append(Paragraph(footer_text, styles['Normal']))
748
+
749
+ doc.build(elements)
750
+ buffer.seek(0)
751
+ return buffer
752
+
753
+ def create_csv_export(df, stats):
754
+ summary_df = pd.DataFrame([
755
+ {
756
+ 'Material': material.replace('_', ' ').title(),
757
+ 'Total_kg': info['total'],
758
+ 'Percentage': info['percentage'],
759
+ 'Daily_Average_kg': info['daily_avg'],
760
+ 'Work_Days': info['work_days'],
761
+ 'Records_Count': info['records']
762
+ }
763
+ for material, info in stats.items() if material != '_total_'
764
+ ])
765
+
766
+ return summary_df
767
+
768
+ def add_export_section(df, stats, outliers, model):
769
+ st.markdown('<div class="section-header">πŸ“„ Export Reports</div>', unsafe_allow_html=True)
770
+
771
+ if 'export_ready' not in st.session_state:
772
+ st.session_state.export_ready = False
773
+ if 'pdf_buffer' not in st.session_state:
774
+ st.session_state.pdf_buffer = None
775
+ if 'csv_data' not in st.session_state:
776
+ st.session_state.csv_data = None
777
+
778
+ col1, col2, col3 = st.columns(3)
779
+
780
+ with col1:
781
+ if st.button("πŸ“Š Generate PDF Report with AI", key="generate_pdf_btn", type="primary"):
782
+ try:
783
+ with st.spinner("Generating PDF with AI analysis..."):
784
+ st.session_state.pdf_buffer = create_enhanced_pdf_report(df, stats, outliers, model)
785
+ st.session_state.export_ready = True
786
+ st.success("βœ… PDF report with AI analysis generated successfully!")
787
+
788
+ except Exception as e:
789
+ st.error(f"❌ PDF generation failed: {str(e)}")
790
+ st.session_state.export_ready = False
791
+
792
+ if st.session_state.export_ready and st.session_state.pdf_buffer:
793
+ st.download_button(
794
+ label="πŸ’Ύ Download PDF Report",
795
+ data=st.session_state.pdf_buffer,
796
+ file_name=f"production_report_ai_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
797
+ mime="application/pdf",
798
+ key="download_pdf_btn"
799
+ )
800
+
801
+ with col2:
802
+ if st.button("πŸ“ˆ Generate CSV Summary", key="generate_csv_btn"):
803
+ try:
804
+ st.session_state.csv_data = create_csv_export(df, stats)
805
+ st.success("βœ… CSV summary generated successfully!")
806
+ except Exception as e:
807
+ st.error(f"❌ CSV generation failed: {str(e)}")
808
+
809
+ if st.session_state.csv_data is not None:
810
+ csv_string = st.session_state.csv_data.to_csv(index=False)
811
+ st.download_button(
812
+ label="πŸ’Ύ Download CSV Summary",
813
+ data=csv_string,
814
+ file_name=f"production_summary_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
815
+ mime="text/csv",
816
+ key="download_csv_btn"
817
+ )
818
+
819
+ with col3:
820
+ csv_string = df.to_csv(index=False)
821
+ st.download_button(
822
+ label="πŸ“‹ Download Raw Data",
823
+ data=csv_string,
824
+ file_name=f"raw_production_data_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
825
+ mime="text/csv",
826
+ key="download_raw_btn"
827
+ )
828
+
829
+ if st.session_state.export_ready or st.session_state.csv_data is not None:
830
+ st.markdown("---")
831
+ if st.button("πŸ”„ Reset Export Cache", key="reset_export_btn"):
832
+ st.session_state.export_ready = False
833
+ st.session_state.pdf_buffer = None
834
+ st.session_state.csv_data = None
835
+ st.rerun()
836
+
837
+ def main():
838
+ load_css()
839
+
840
+ st.markdown("""
841
+ <div class="main-header">
842
+ <div class="main-title">🏭 Production Monitor</div>
843
+ <div class="main-subtitle">Nilsen Service & Consulting AS | Real-time Production Analytics with AI Insights</div>
844
+ </div>
845
+ """, unsafe_allow_html=True)
846
+
847
+ model = init_ai()
848
+
849
+ if 'current_df' not in st.session_state:
850
+ st.session_state.current_df = None
851
+ if 'current_stats' not in st.session_state:
852
+ st.session_state.current_stats = None
853
+
854
+ with st.sidebar:
855
+ st.markdown("### πŸ“Š Data Source")
856
+
857
+ uploaded_file = st.file_uploader("Upload Production Data", type=['csv'])
858
+
859
+ st.markdown("---")
860
+ st.markdown("### πŸ“Š Quick Load")
861
+
862
+ col1, col2 = st.columns(2)
863
+ with col1:
864
+ if st.button("πŸ“Š 2024 Data", type="primary", key="load_2024"):
865
+ st.session_state.load_preset = "2024"
866
+ with col2:
867
+ if st.button("πŸ“Š 2025 Data", type="primary", key="load_2025"):
868
+ st.session_state.load_preset = "2025"
869
+
870
+ st.markdown("---")
871
+ st.markdown("""
872
+ **Expected TSV format:**
873
+ - `date`: MM/DD/YYYY
874
+ - `weight_kg`: Production weight
875
+ - `material_type`: Material category
876
+ - `shift`: day/night (optional)
877
+ """)
878
+
879
+ if model:
880
+ st.success("πŸ€– AI Assistant Ready")
881
+ else:
882
+ st.warning("⚠️ AI Assistant Unavailable")
883
+
884
+ df = st.session_state.current_df
885
+ stats = st.session_state.current_stats
886
+
887
+ if uploaded_file:
888
+ try:
889
+ df = load_data(uploaded_file)
890
+ stats = get_material_stats(df)
891
+ st.session_state.current_df = df
892
+ st.session_state.current_stats = stats
893
+ st.success("βœ… Data uploaded successfully!")
894
+ except Exception as e:
895
+ st.error(f"❌ Error loading uploaded file: {str(e)}")
896
+
897
+ elif 'load_preset' in st.session_state:
898
+ year = st.session_state.load_preset
899
+ try:
900
+ with st.spinner(f"Loading {year} data..."):
901
+ df = load_preset_data(year)
902
+ if df is not None:
903
+ stats = get_material_stats(df)
904
+ st.session_state.current_df = df
905
+ st.session_state.current_stats = stats
906
+ st.success(f"βœ… {year} data loaded successfully!")
907
+ except Exception as e:
908
+ st.error(f"❌ Error loading {year} data: {str(e)}")
909
+ finally:
910
+ del st.session_state.load_preset
911
+
912
+ if df is not None and stats is not None:
913
+ st.markdown('<div class="section-header">πŸ“‹ Material Overview</div>', unsafe_allow_html=True)
914
+ materials = [k for k in stats.keys() if k != '_total_']
915
+
916
+ cols = st.columns(4)
917
+ for i, material in enumerate(materials[:3]):
918
+ info = stats[material]
919
+ with cols[i]:
920
+ st.metric(
921
+ label=material.replace('_', ' ').title(),
922
+ value=f"{info['total']:,.0f} kg",
923
+ delta=f"{info['percentage']:.1f}% of total"
924
+ )
925
+ st.caption(f"Daily avg: {info['daily_avg']:,.0f} kg")
926
+
927
+ if len(materials) >= 3:
928
+ total_info = stats['_total_']
929
+ with cols[3]:
930
+ st.metric(
931
+ label="Total Production",
932
+ value=f"{total_info['total']:,.0f} kg",
933
+ delta="100% of total"
934
+ )
935
+ st.caption(f"Daily avg: {total_info['daily_avg']:,.0f} kg")
936
+
937
+ st.markdown('<div class="section-header">πŸ“Š Production Trends</div>', unsafe_allow_html=True)
938
+
939
+ col1, col2 = st.columns([3, 1])
940
+ with col2:
941
+ time_view = st.selectbox("Time Period", ["daily", "weekly", "monthly"], key="time_view_select")
942
+
943
+ with col1:
944
+ with st.container():
945
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
946
+ total_chart = create_total_production_chart(df, time_view)
947
+ st.plotly_chart(total_chart, use_container_width=True)
948
+ st.markdown('</div>', unsafe_allow_html=True)
949
+
950
+ st.markdown('<div class="section-header">🏷️ Materials Analysis</div>', unsafe_allow_html=True)
951
+
952
+ col1, col2 = st.columns([3, 1])
953
+ with col2:
954
+ selected_materials = st.multiselect(
955
+ "Select Materials",
956
+ options=materials,
957
+ default=materials,
958
+ key="materials_select"
959
+ )
960
+
961
+ with col1:
962
+ if selected_materials:
963
+ with st.container():
964
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
965
+ materials_chart = create_materials_trend_chart(df, time_view, selected_materials)
966
+ st.plotly_chart(materials_chart, use_container_width=True)
967
+ st.markdown('</div>', unsafe_allow_html=True)
968
+
969
+ if 'shift' in df.columns:
970
+ st.markdown('<div class="section-header">πŸŒ“ Shift Analysis</div>', unsafe_allow_html=True)
971
+
972
+ with st.container():
973
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
974
+ shift_chart = create_shift_trend_chart(df, time_view)
975
+ st.plotly_chart(shift_chart, use_container_width=True)
976
+ st.markdown('</div>', unsafe_allow_html=True)
977
+
978
+ st.markdown('<div class="section-header">⚠️ Quality Check</div>', unsafe_allow_html=True)
979
+ outliers = detect_outliers(df)
980
+
981
+ cols = st.columns(len(outliers))
982
+ for i, (material, info) in enumerate(outliers.items()):
983
+ with cols[i]:
984
+ if info['count'] > 0:
985
+ if len(info['dates']) <= 5:
986
+ dates_str = ", ".join(info['dates'])
987
+ else:
988
+ dates_str = f"{', '.join(info['dates'][:3])}, +{len(info['dates'])-3} more"
989
+
990
+ st.markdown(f'<div class="alert-warning"><strong>{material.title()}</strong><br>{info["count"]} outliers detected<br>Normal range: {info["range"]}<br><small>Dates: {dates_str}</small></div>', unsafe_allow_html=True)
991
+ else:
992
+ st.markdown(f'<div class="alert-success"><strong>{material.title()}</strong><br>All values normal</div>', unsafe_allow_html=True)
993
+
994
+ add_export_section(df, stats, outliers, model)
995
+
996
+ if model:
997
+ st.markdown('<div class="section-header">πŸ€– AI Insights</div>', unsafe_allow_html=True)
998
+
999
+ quick_questions = [
1000
+ "How does production distribution on weekdays compare to weekends?",
1001
+ "Which material exhibits the most volatility in our dataset?",
1002
+ "To improve stability, which material or shift needs immediate attention?"
1003
+ ]
1004
+
1005
+ cols = st.columns(len(quick_questions))
1006
+ for i, q in enumerate(quick_questions):
1007
+ with cols[i]:
1008
+ if st.button(q, key=f"ai_q_{i}"):
1009
+ with st.spinner("Analyzing..."):
1010
+ answer = query_ai(model, stats, q, df)
1011
+ st.info(answer)
1012
+
1013
+ custom_question = st.text_input("Ask about your production data:", key="custom_ai_question")
1014
+ if custom_question and st.button("Ask AI", key="ask_ai_btn"):
1015
+ with st.spinner("Analyzing..."):
1016
+ answer = query_ai(model, stats, custom_question, df)
1017
+ st.success(f"**Q:** {custom_question}")
1018
+ st.write(f"**A:** {answer}")
1019
+
1020
+ else:
1021
+ st.markdown('<div class="section-header">πŸ“– How to Use This Platform</div>', unsafe_allow_html=True)
1022
+
1023
+ col1, col2 = st.columns(2)
1024
+
1025
+ with col1:
1026
+ st.markdown("""
1027
+ ### πŸš€ **Quick Start**
1028
+ 1. Upload your TSV data in the sidebar
1029
+ 2. Or click Quick Load buttons for preset data
1030
+ 3. View production by material type
1031
+ 4. Analyze trends (daily/weekly/monthly)
1032
+ 5. Check anomalies in Quality Check
1033
+ 6. Export reports (PDF with AI, CSV)
1034
+ 7. Ask the AI assistant for insights
1035
+ """)
1036
+
1037
+ with col2:
1038
+ st.markdown("""
1039
+ ### πŸ“Š **Key Features**
1040
+ - Real-time interactive charts
1041
+ - One-click preset data loading
1042
+ - Time-period comparisons
1043
+ - Shift performance analysis
1044
+ - Outlier detection with dates
1045
+ - AI-powered PDF reports
1046
+ - Intelligent recommendations
1047
+ """)
1048
+
1049
+ st.info("πŸ“ Ready to start? Upload your production data or use Quick Load buttons to begin analysis!")
1050
+
1051
+ if __name__ == "__main__":
1052
+ main()