-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdashboard.py
More file actions
143 lines (126 loc) · 6.05 KB
/
dashboard.py
File metadata and controls
143 lines (126 loc) · 6.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""Phase 6 - Streamlit dashboard for the SFDR 2.0 classification engine.
Run: streamlit run dashboard.py
The dashboard reads the FROZEN engine (Phases 2-5) -- no logic lives here.
Three interactive controls wire to the levers identified in Phase 5:
- PAB-replication toggle (the dominant structural lever)
- Year slider (PAB pathway ratchets -7% p.a.)
- Taxonomy multiplier (the deeming-provision tipping point)
"""
import copy
import streamlit as st
import pandas as pd
from sfdr2.data import SAMPLE_PORTFOLIO
from sfdr2.classify import classify
from sfdr2 import sensitivity as S
st.set_page_config(page_title="SFDR 2.0 Classification Engine", layout="wide")
st.title("SFDR 2.0 Portfolio Classification Engine")
st.caption(
"Interactive implementation of the EU SFDR 2.0 product-category logic "
"(Sustainable / Transition / ESG Basics) with Paris-Aligned Benchmark "
"exclusions and the 70% threshold."
)
# ---------------------------------------------------------------------------
# Sidebar -- the three levers from Phase 5
# ---------------------------------------------------------------------------
st.sidebar.header("Product structure")
pab_repl = st.sidebar.checkbox(
"Product replicates a PAB benchmark",
value=False,
help="Activates the PAB-replication deeming provision. Sustainable / "
"Transition categories are deemed to meet the 70% floor.",
)
year = st.sidebar.slider(
"Reporting year",
min_value=2024, max_value=2035, value=2026, step=1,
help="PAB pathway: 50% intensity cut vs parent, then 7% p.a. self-decarbonisation.",
)
st.sidebar.header("Data assumption")
tax_mult = st.sidebar.slider(
"Taxonomy-aligned revenue multiplier",
min_value=0.5, max_value=3.0, value=1.0, step=0.05,
help="Scales every issuer's Taxonomy-aligned revenue. Use to test the "
"tipping point of the 15% deeming provision.",
)
# Apply the Taxonomy multiplier to a copy of the portfolio
H = copy.deepcopy(SAMPLE_PORTFOLIO)
for h in H:
if h.get("taxonomy_aligned_rev_pct") is not None:
h["taxonomy_aligned_rev_pct"] *= tax_mult
# ---------------------------------------------------------------------------
# Headline classification
# ---------------------------------------------------------------------------
result = classify(H, pab_replication=pab_repl, year=year)
c1, c2, c3 = st.columns([1.2, 1, 1])
with c1:
color = {"Sustainable": "#1f7a3c", "Transition": "#c47d00",
"ESG Basics": "#3a5a8c", "Non-classifiable": "#a02020"}.get(result.label, "#444")
st.markdown(
f"<div style='padding:18px;border-radius:8px;background:{color};color:white'>"
f"<div style='font-size:13px;opacity:0.85'>Classification</div>"
f"<div style='font-size:32px;font-weight:600'>{result.label}</div>"
f"<div style='font-size:13px;opacity:0.9;margin-top:4px'>"
f"via {result.carrying_path}</div></div>",
unsafe_allow_html=True,
)
with c2:
st.metric("PAB-replication", "Yes" if pab_repl else "No")
st.metric("Pathway aligned", "Yes" if result.pathway_aligned else "No")
with c3:
st.metric("Reporting year", year)
st.metric("Taxonomy multiplier", f"{tax_mult:.2f}x")
if result.pathway_warning:
st.warning(result.pathway_warning)
st.divider()
# ---------------------------------------------------------------------------
# Decision-tree audit trail
# ---------------------------------------------------------------------------
st.subheader("Decision-tree audit trail")
st.caption("Each node is one regulatory test. The 'decisive' marker shows the "
"node that carried the classification.")
audit_rows = [
{"#": i + 1, "Step": n.step, "Test": n.test, "Value": n.value,
"vs": n.threshold, "Pass": "Yes" if n.passed else "No",
"Decisive": "<-- decisive" if n.decisive else ""}
for i, n in enumerate(result.audit)
]
st.dataframe(pd.DataFrame(audit_rows), use_container_width=True, hide_index=True)
# ---------------------------------------------------------------------------
# Issuer-level screening (Phase 2 detail)
# ---------------------------------------------------------------------------
st.subheader("Issuer-level PAB screening")
st.caption("Synthetic sample. None = data gap (conservative: never qualifying, "
"but still in the denominator).")
df = pd.DataFrame(H)
display_cols = ["issuer", "sector", "weight_pct",
"coal_rev_pct", "oil_rev_pct", "gas_rev_pct",
"power_gen_rev_pct", "power_carbon_intensity_gco2_kwh",
"scope12_intensity_tco2_per_eur_m",
"taxonomy_aligned_rev_pct", "taxonomy_aligned_capex_pct",
"controversial_weapons", "tobacco_producer", "ungc_oecd_violation"]
st.dataframe(df[display_cols], use_container_width=True, hide_index=True)
# ---------------------------------------------------------------------------
# Phase 5 -- cost-of-compliance numbers
# ---------------------------------------------------------------------------
st.divider()
st.subheader("Cost-of-compliance diagnostics")
col_a, col_b = st.columns(2)
with col_a:
st.markdown("**Exclusion-gate attribution**")
ea = S.exclusion_attribution(H)
attr_rows = [{"Screen": k, "Weight lost (%)": round(v["weight"], 1),
"Names": ", ".join(v["names"])}
for k, v in sorted(ea["by_screen"].items(),
key=lambda kv: -kv[1]["weight"])]
st.dataframe(pd.DataFrame(attr_rows), use_container_width=True, hide_index=True)
st.caption(f"Total excluded weight: {ea['total_excluded_weight_pct']:.1f}% | "
f"Data gap: {ea['gap_weight_pct']:.1f}%")
with col_b:
st.markdown("**Decarbonisation residual**")
dr = S.decarb_residual(H)
st.metric("Post-exclusion WACI (tCO2e/EURm)", dr["post_excl_waci"])
st.metric("1.5C pathway target",
dr["pathway_target"],
delta=f"{dr['absolute_gap']:+.1f} gap",
delta_color="inverse")
st.caption(f"Further intensity cut required: **{dr['further_cut_required_pct']:.1f}%** "
"(screening is not decarbonisation)")