esg / app.py
entropy25's picture
Update app.py
b8f5958 verified
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
# Configuration
@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
# Data Models
@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()
)
# Data Validation
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")
# Data Management
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]
})
# UI Components
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)
# Visualization
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
# Report Generation
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()
# Main Application
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()
# Load and validate data
df = DataManager.load_and_validate_data(uploaded_file)
if df.empty:
st.stop()
carbon_df = DataManager.get_carbon_footprint_data()
# Render alert
latest_status = df.iloc[-1]['status']
latest_location = df.iloc[-1]['location']
UIComponents.render_alert(latest_status, latest_location)
# Render dashboard components
self.render_metrics(df)
self.render_charts(df, carbon_df)
self.render_export_section(df, carbon_df)
# Application Entry Point
def main():
dashboard = ESGDashboard()
dashboard.run()
if __name__ == "__main__":
main()