| | import streamlit as st |
| | import pandas as pd |
| | import numpy as np |
| | import plotly.express as px |
| | import plotly.graph_objects as go |
| | from datetime import datetime, timedelta |
| | from io import StringIO, BytesIO |
| | import base64 |
| | import json |
| | from dataclasses import dataclass, asdict |
| | from typing import Optional, Dict, List |
| | import hashlib |
| |
|
| | |
| | @dataclass |
| | class Config: |
| | PAGE_TITLE = "ESG Compliance Intelligence" |
| | PAGE_ICON = "π±" |
| | REQUIRED_COLUMNS = ['timestamp', 'ph_level', 'wastewater_lmin', 'co2_emission_kg', 'energy_kwh', 'location', 'status'] |
| | PH_RANGE = (6.0, 8.5) |
| | WASTEWATER_LIMIT = 60.0 |
| | COMPLIANCE_THRESHOLD = 80 |
| |
|
| | |
| | @dataclass |
| | class ComplianceReport: |
| | report_id: str |
| | timestamp: str |
| | location: str |
| | ph_level: float |
| | wastewater_volume: float |
| | co2_total: float |
| | energy_consumption: float |
| | compliance_status: str |
| | generated_by: str = "ESG Compliance Engine" |
| | |
| | @classmethod |
| | def from_dataframe_row(cls, row): |
| | return cls( |
| | report_id=f"PSA-ENV-{datetime.now().strftime('%Y%m%d%H%M')}", |
| | timestamp=datetime.now().isoformat(), |
| | location=row['location'], |
| | ph_level=float(row['ph_level']), |
| | wastewater_volume=float(row['wastewater_lmin']), |
| | co2_total=float(row['co2_emission_kg']), |
| | energy_consumption=float(row['energy_kwh']), |
| | compliance_status=row['status'].upper() |
| | ) |
| |
|
| | |
| | class DataValidator: |
| | @staticmethod |
| | def validate_csv_structure(df: pd.DataFrame) -> None: |
| | missing_cols = [col for col in Config.REQUIRED_COLUMNS if col not in df.columns] |
| | if missing_cols: |
| | raise ValueError(f"Missing required columns: {', '.join(missing_cols)}") |
| | |
| | @staticmethod |
| | def validate_data_types(df: pd.DataFrame) -> pd.DataFrame: |
| | try: |
| | df['timestamp'] = pd.to_datetime(df['timestamp']) |
| | df['ph_level'] = pd.to_numeric(df['ph_level']) |
| | df['wastewater_lmin'] = pd.to_numeric(df['wastewater_lmin']) |
| | df['co2_emission_kg'] = pd.to_numeric(df['co2_emission_kg']) |
| | df['energy_kwh'] = pd.to_numeric(df['energy_kwh']) |
| | return df |
| | except Exception as e: |
| | raise ValueError(f"Data type conversion failed: {str(e)}") |
| | |
| | @staticmethod |
| | def validate_ranges(df: pd.DataFrame) -> None: |
| | if not df['ph_level'].between(0, 14).all(): |
| | raise ValueError("pH values must be between 0 and 14") |
| | if (df[['wastewater_lmin', 'co2_emission_kg', 'energy_kwh']] < 0).any().any(): |
| | raise ValueError("Environmental measurements cannot be negative") |
| |
|
| | |
| | class DataManager: |
| | @staticmethod |
| | def get_sample_data() -> str: |
| | return """timestamp,ph_level,wastewater_lmin,co2_emission_kg,energy_kwh,chemical_usage_kg,location,status |
| | 2024-08-27 14:30,7.4,48.5,14.2,92,2.3,Statoil Platform Alpha,compliant |
| | 2024-08-27 14:25,7.2,45.1,12.8,88,2.1,Statoil Platform Alpha,compliant |
| | 2024-08-27 14:20,7.6,52.3,15.7,95,2.5,Statoil Platform Alpha,warning |
| | 2024-08-27 14:15,7.8,55.2,16.1,98,2.8,Statoil Platform Alpha,violation |
| | 2024-08-27 14:10,7.3,47.8,13.5,89,2.2,Statoil Platform Alpha,compliant""" |
| | |
| | @staticmethod |
| | @st.cache_data |
| | def load_and_validate_data(uploaded_file) -> pd.DataFrame: |
| | try: |
| | if uploaded_file: |
| | df = pd.read_csv(uploaded_file) |
| | else: |
| | df = pd.read_csv(StringIO(DataManager.get_sample_data())) |
| | |
| | DataValidator.validate_csv_structure(df) |
| | df = DataValidator.validate_data_types(df) |
| | DataValidator.validate_ranges(df) |
| | return df |
| | |
| | except Exception as e: |
| | st.error(f"Data loading error: {str(e)}") |
| | return pd.DataFrame() |
| | |
| | @staticmethod |
| | def get_carbon_footprint_data() -> pd.DataFrame: |
| | return pd.DataFrame({ |
| | 'source': ['Fuel Consumption', 'Electricity', 'Chemicals', 'Transport'], |
| | 'co2_kg': [145.2, 67.8, 89.1, 19.6], |
| | 'percentage': [45, 21, 28, 6] |
| | }) |
| |
|
| | |
| | class UIComponents: |
| | @staticmethod |
| | def apply_styling(): |
| | st.markdown(""" |
| | <style> |
| | .main-header { |
| | background: linear-gradient(135deg, #1e40af 0%, #059669 100%); |
| | padding: 2rem; border-radius: 15px; margin-bottom: 2rem; |
| | color: white; text-align: center; box-shadow: 0 8px 32px rgba(0,0,0,0.1); |
| | } |
| | .metric-card { |
| | background: white; padding: 1.5rem; border-radius: 12px; |
| | border: 2px solid #e5e7eb; box-shadow: 0 4px 20px rgba(0,0,0,0.08); |
| | text-align: center; transition: transform 0.2s ease; |
| | } |
| | .metric-card:hover { transform: translateY(-2px); } |
| | .alert-danger { |
| | background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); |
| | border-left: 4px solid #dc2626; padding: 1rem; border-radius: 8px; margin: 1rem 0; |
| | } |
| | .alert-success { |
| | background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); |
| | border-left: 4px solid #059669; padding: 1rem; border-radius: 8px; margin: 1rem 0; |
| | } |
| | .alert-warning { |
| | background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); |
| | border-left: 4px solid #f59e0b; padding: 1rem; border-radius: 8px; margin: 1rem 0; |
| | } |
| | .chart-container { |
| | background: white; padding: 1.5rem; border-radius: 12px; |
| | box-shadow: 0 4px 20px rgba(0,0,0,0.08); margin: 1rem 0; border: 1px solid #f1f5f9; |
| | } |
| | </style> |
| | """, unsafe_allow_html=True) |
| | |
| | @staticmethod |
| | def render_header(): |
| | st.markdown(""" |
| | <div class="main-header"> |
| | <h1>π± ESG Compliance Intelligence</h1> |
| | <p>Norwegian Petroleum Safety Authority Real-time Monitoring</p> |
| | </div> |
| | """, unsafe_allow_html=True) |
| | |
| | @staticmethod |
| | def render_alert(status: str, location: str): |
| | alerts = { |
| | 'violation': ('alert-danger', 'π¨', 'CRITICAL ALERT', 'Environmental violation detected'), |
| | 'warning': ('alert-warning', 'β οΈ', 'WARNING', 'Parameters approaching limits'), |
| | 'compliant': ('alert-success', 'β
', 'COMPLIANT', 'All systems within regulations') |
| | } |
| | |
| | css_class, icon, title, message = alerts.get(status, alerts['compliant']) |
| | st.markdown(f""" |
| | <div class="{css_class}"> |
| | {icon} <strong>{title}:</strong> {message} at {location} |
| | </div> |
| | """, unsafe_allow_html=True) |
| |
|
| | |
| | class ChartGenerator: |
| | @staticmethod |
| | def create_environmental_trend(df: pd.DataFrame): |
| | fig = px.line(df, x='timestamp', y=['ph_level', 'wastewater_lmin'], |
| | title="Environmental Parameters Over Time") |
| | fig.update_layout(height=400, showlegend=True) |
| | return fig |
| | |
| | @staticmethod |
| | def create_carbon_breakdown(carbon_df: pd.DataFrame): |
| | fig = px.bar(carbon_df, x='source', y='co2_kg', |
| | title="COβ Emissions by Source", |
| | color='co2_kg', color_continuous_scale='Greens') |
| | fig.update_layout(height=400) |
| | return fig |
| | |
| | @staticmethod |
| | def create_compliance_trend(df: pd.DataFrame): |
| | compliance_map = {'compliant': 100, 'warning': 70, 'violation': 30} |
| | df['compliance_score'] = df['status'].map(compliance_map) |
| | |
| | fig = px.area(df, x='timestamp', y='compliance_score', |
| | title="Compliance Score Trend", |
| | color_discrete_sequence=['#059669']) |
| | fig.add_hline(y=Config.COMPLIANCE_THRESHOLD, line_dash="dash", |
| | line_color="red", annotation_text="PSA Threshold") |
| | fig.update_layout(height=300) |
| | return fig |
| |
|
| | |
| | class ReportGenerator: |
| | @staticmethod |
| | def generate_html_report(report: ComplianceReport) -> str: |
| | return f""" |
| | <!DOCTYPE html> |
| | <html> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <title>PSA Environmental Compliance Report</title> |
| | <style> |
| | body {{ font-family: Arial, sans-serif; margin: 40px; }} |
| | .header {{ background: #1e40af; color: white; padding: 20px; border-radius: 8px; text-align: center; }} |
| | .section {{ margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 8px; }} |
| | .compliant {{ color: #059669; font-weight: bold; }} |
| | .warning {{ color: #f59e0b; font-weight: bold; }} |
| | .violation {{ color: #dc2626; font-weight: bold; }} |
| | table {{ width: 100%; border-collapse: collapse; margin: 10px 0; }} |
| | th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }} |
| | th {{ background-color: #f8fafc; }} |
| | </style> |
| | </head> |
| | <body> |
| | <div class="header"> |
| | <h1>Norwegian Petroleum Safety Authority</h1> |
| | <h2>Environmental Compliance Report</h2> |
| | <p>Report ID: {report.report_id}</p> |
| | </div> |
| | |
| | <div class="section"> |
| | <h3>Summary</h3> |
| | <p><strong>Generated:</strong> {report.timestamp}</p> |
| | <p><strong>Location:</strong> {report.location}</p> |
| | <p><strong>Status:</strong> <span class="{report.compliance_status.lower()}">{report.compliance_status}</span></p> |
| | </div> |
| | |
| | <div class="section"> |
| | <h3>Environmental Measurements</h3> |
| | <table> |
| | <tr><th>Parameter</th><th>Value</th><th>Unit</th><th>Status</th></tr> |
| | <tr><td>pH Level</td><td>{report.ph_level:.1f}</td><td>pH</td><td>β Monitored</td></tr> |
| | <tr><td>Wastewater</td><td>{report.wastewater_volume:.1f}</td><td>L/min</td><td>β Monitored</td></tr> |
| | <tr><td>COβ Emissions</td><td>{report.co2_total:.1f}</td><td>kg</td><td>β Tracked</td></tr> |
| | <tr><td>Energy</td><td>{report.energy_consumption:.1f}</td><td>kWh</td><td>β Tracked</td></tr> |
| | </table> |
| | </div> |
| | |
| | <div class="section"> |
| | <h3>Certification</h3> |
| | <p>Report generated by {report.generated_by}</p> |
| | <p><strong>Hash:</strong> {hashlib.md5(str(report.report_id).encode()).hexdigest()[:8]}</p> |
| | </div> |
| | </body> |
| | </html> |
| | """ |
| | |
| | @staticmethod |
| | def create_excel_export(df: pd.DataFrame, carbon_df: pd.DataFrame) -> bytes: |
| | output = BytesIO() |
| | with pd.ExcelWriter(output, engine='openpyxl') as writer: |
| | df.to_excel(writer, sheet_name='Environmental_Data', index=False) |
| | carbon_df.to_excel(writer, sheet_name='Carbon_Footprint', index=False) |
| | |
| | summary = pd.DataFrame({ |
| | 'Metric': ['Latest pH', 'Latest COβ', 'Latest Energy', 'Status'], |
| | 'Value': [df.iloc[-1]['ph_level'], df.iloc[-1]['co2_emission_kg'], |
| | df.iloc[-1]['energy_kwh'], df.iloc[-1]['status'].upper()] |
| | }) |
| | summary.to_excel(writer, sheet_name='Summary', index=False) |
| | |
| | return output.getvalue() |
| |
|
| | |
| | class ESGDashboard: |
| | def __init__(self): |
| | st.set_page_config( |
| | page_title=Config.PAGE_TITLE, |
| | page_icon=Config.PAGE_ICON, |
| | layout="wide" |
| | ) |
| | UIComponents.apply_styling() |
| | |
| | def render_sidebar(self) -> Optional[pd.DataFrame]: |
| | with st.sidebar: |
| | st.header("βοΈ Controls") |
| | |
| | uploaded_file = st.file_uploader("Upload CSV Data", type=['csv']) |
| | |
| | if st.button("π Refresh Data"): |
| | st.cache_data.clear() |
| | st.rerun() |
| | |
| | st.markdown("---") |
| | st.markdown("**Required CSV Format:**") |
| | st.code("timestamp,ph_level,wastewater_lmin,co2_emission_kg,energy_kwh,location,status") |
| | |
| | return uploaded_file |
| | |
| | def render_metrics(self, df: pd.DataFrame): |
| | latest = df.iloc[-1] |
| | col1, col2, col3, col4 = st.columns(4) |
| | |
| | status_icons = {"compliant": "β
", "warning": "β οΈ", "violation": "π¨"} |
| | |
| | with col1: |
| | st.metric("Status", f"{status_icons[latest['status']]} {latest['status'].upper()}") |
| | with col2: |
| | ph_delta = latest['ph_level'] - 7.0 |
| | st.metric("pH Level", f"{latest['ph_level']:.1f}", f"{ph_delta:+.1f}") |
| | with col3: |
| | st.metric("COβ Emissions", f"{latest['co2_emission_kg']:.1f} kg", "-2.3 kg") |
| | with col4: |
| | st.metric("Energy Usage", f"{latest['energy_kwh']:.0f} kWh", "+5.2 kWh") |
| | |
| | def render_charts(self, df: pd.DataFrame, carbon_df: pd.DataFrame): |
| | col1, col2 = st.columns(2) |
| | |
| | with col1: |
| | st.markdown('<div class="chart-container">', unsafe_allow_html=True) |
| | st.subheader("π§ Environmental Monitoring") |
| | fig_env = ChartGenerator.create_environmental_trend(df) |
| | st.plotly_chart(fig_env, use_container_width=True) |
| | st.markdown('</div>', unsafe_allow_html=True) |
| | |
| | with col2: |
| | st.markdown('<div class="chart-container">', unsafe_allow_html=True) |
| | st.subheader("π Carbon Footprint") |
| | fig_carbon = ChartGenerator.create_carbon_breakdown(carbon_df) |
| | st.plotly_chart(fig_carbon, use_container_width=True) |
| | st.markdown('</div>', unsafe_allow_html=True) |
| | |
| | st.markdown('<div class="chart-container">', unsafe_allow_html=True) |
| | st.subheader("π Compliance Trend") |
| | fig_compliance = ChartGenerator.create_compliance_trend(df) |
| | st.plotly_chart(fig_compliance, use_container_width=True) |
| | st.markdown('</div>', unsafe_allow_html=True) |
| | |
| | def render_export_section(self, df: pd.DataFrame, carbon_df: pd.DataFrame): |
| | st.markdown('<div class="chart-container">', unsafe_allow_html=True) |
| | st.subheader("β¬οΈ Export Reports") |
| | |
| | col1, col2, col3 = st.columns(3) |
| | |
| | with col1: |
| | if st.button("π Generate PDF Report", type="primary"): |
| | report = ComplianceReport.from_dataframe_row(df.iloc[-1]) |
| | html_content = ReportGenerator.generate_html_report(report) |
| | |
| | st.download_button( |
| | label="β¬οΈ Download Report", |
| | data=html_content, |
| | file_name=f"PSA_Report_{datetime.now().strftime('%Y%m%d_%H%M')}.html", |
| | mime="text/html" |
| | ) |
| | st.success("β
Report ready for download!") |
| | |
| | with col2: |
| | if st.button("π Export Excel"): |
| | excel_data = ReportGenerator.create_excel_export(df, carbon_df) |
| | |
| | st.download_button( |
| | label="β¬οΈ Download Excel", |
| | data=excel_data, |
| | file_name=f"ESG_Data_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx", |
| | mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
| | ) |
| | st.success("β
Excel file ready!") |
| | |
| | with col3: |
| | if st.button("π€ Submit to PSA"): |
| | submission_id = f"PSA-SUB-{datetime.now().strftime('%Y%m%d%H%M')}" |
| | st.success(f"β
Submitted!\nID: {submission_id}") |
| | |
| | st.markdown('</div>', unsafe_allow_html=True) |
| | |
| | def run(self): |
| | UIComponents.render_header() |
| | |
| | uploaded_file = self.render_sidebar() |
| | |
| | |
| | df = DataManager.load_and_validate_data(uploaded_file) |
| | if df.empty: |
| | st.stop() |
| | |
| | carbon_df = DataManager.get_carbon_footprint_data() |
| | |
| | |
| | latest_status = df.iloc[-1]['status'] |
| | latest_location = df.iloc[-1]['location'] |
| | UIComponents.render_alert(latest_status, latest_location) |
| | |
| | |
| | self.render_metrics(df) |
| | self.render_charts(df, carbon_df) |
| | self.render_export_section(df, carbon_df) |
| |
|
| | |
| | def main(): |
| | dashboard = ESGDashboard() |
| | dashboard.run() |
| |
|
| | if __name__ == "__main__": |
| | main() |