|
| 1 | +import sqlite3 |
| 2 | +from pathlib import Path |
| 3 | + |
| 4 | +import pandas as pd |
| 5 | +import plotly.express as px |
| 6 | +import streamlit as st |
| 7 | + |
| 8 | +from day17_CONFIG_settings import ( |
| 9 | + DAY17_DB_PATH, |
| 10 | + DAY17_DEADLINE_THRESHOLDS_DAYS, |
| 11 | + DAY17_JURISDICTIONS, |
| 12 | +) |
| 13 | + |
| 14 | +BASE_DIR = Path(__file__).resolve().parent |
| 15 | +QUERY_DIR = BASE_DIR / "queries" |
| 16 | + |
| 17 | + |
| 18 | +def day17_get_db_path() -> Path: |
| 19 | + if DAY17_DB_PATH.is_absolute(): |
| 20 | + return DAY17_DB_PATH |
| 21 | + return (BASE_DIR / DAY17_DB_PATH).resolve() |
| 22 | + |
| 23 | + |
| 24 | +def day17_read_sql(query_filename: str) -> str: |
| 25 | + return (QUERY_DIR / query_filename).read_text() |
| 26 | + |
| 27 | + |
| 28 | +def day17_query_df(query_filename: str, params: dict | None = None) -> pd.DataFrame: |
| 29 | + db_path = day17_get_db_path() |
| 30 | + with sqlite3.connect(db_path) as conn: |
| 31 | + return pd.read_sql_query(day17_read_sql(query_filename), conn, params=params or {}) |
| 32 | + |
| 33 | + |
| 34 | +def day17_get_date_bounds() -> tuple[pd.Timestamp, pd.Timestamp]: |
| 35 | + db_path = day17_get_db_path() |
| 36 | + with sqlite3.connect(db_path) as conn: |
| 37 | + row = conn.execute( |
| 38 | + """ |
| 39 | + SELECT MIN(d.full_date), MAX(d.full_date) |
| 40 | + FROM fct_holdings h |
| 41 | + JOIN dim_date d ON h.date_key = d.date_key |
| 42 | + """ |
| 43 | + ).fetchone() |
| 44 | + return pd.to_datetime(row[0]), pd.to_datetime(row[1]) |
| 45 | + |
| 46 | + |
| 47 | +def day17_get_latest_date() -> pd.Timestamp: |
| 48 | + db_path = day17_get_db_path() |
| 49 | + with sqlite3.connect(db_path) as conn: |
| 50 | + row = conn.execute( |
| 51 | + """ |
| 52 | + SELECT MAX(d.full_date) |
| 53 | + FROM fct_holdings h |
| 54 | + JOIN dim_date d ON h.date_key = d.date_key |
| 55 | + """ |
| 56 | + ).fetchone() |
| 57 | + return pd.to_datetime(row[0]) |
| 58 | + |
| 59 | + |
| 60 | +st.set_page_config(page_title="Day 17 Legal Analytics", layout="wide") |
| 61 | + |
| 62 | +st.title("Day 17 - Multi-Jurisdictional Asset Compliance Dashboard") |
| 63 | + |
| 64 | +# Validate database path early |
| 65 | +if not day17_get_db_path().exists(): |
| 66 | + st.error(f"Database not found at {day17_get_db_path()}") |
| 67 | + st.stop() |
| 68 | + |
| 69 | +min_date, max_date = day17_get_date_bounds() |
| 70 | +latest_date = day17_get_latest_date() |
| 71 | + |
| 72 | +with st.sidebar: |
| 73 | + st.header("Filters") |
| 74 | + selected_jurisdictions = st.multiselect( |
| 75 | + "Jurisdictions", |
| 76 | + options=DAY17_JURISDICTIONS, |
| 77 | + default=DAY17_JURISDICTIONS, |
| 78 | + ) |
| 79 | + point_in_time_date = st.date_input( |
| 80 | + "Point-in-time date", |
| 81 | + value=latest_date.date(), |
| 82 | + min_value=min_date.date(), |
| 83 | + max_value=max_date.date(), |
| 84 | + ) |
| 85 | + |
| 86 | +st.caption( |
| 87 | + "Decision focus: Identify assets requiring regulatory attention this quarter based on " |
| 88 | + "classification changes, jurisdictional compliance status, and upcoming deadlines." |
| 89 | +) |
| 90 | + |
| 91 | +portfolio_df = day17_query_df("day17_QUERY_portfolio_overview.sql") |
| 92 | + |
| 93 | +if selected_jurisdictions: |
| 94 | + portfolio_df = portfolio_df[portfolio_df["jurisdiction"].isin(selected_jurisdictions)] |
| 95 | + |
| 96 | +# Section 1 - Portfolio Overview |
| 97 | +st.subheader("Portfolio Overview") |
| 98 | +col1, col2, col3 = st.columns(3) |
| 99 | +with col1: |
| 100 | + st.metric("Total Assets", portfolio_df["asset_id"].nunique()) |
| 101 | +with col2: |
| 102 | + st.metric("Total Market Value", f"${portfolio_df['market_value'].sum():,.0f}") |
| 103 | +with col3: |
| 104 | + st.metric("Latest Snapshot Date", latest_date.date().isoformat()) |
| 105 | + |
| 106 | +col4, col5 = st.columns(2) |
| 107 | +with col4: |
| 108 | + assets_by_jurisdiction = ( |
| 109 | + portfolio_df.groupby("jurisdiction")["asset_id"].nunique().reset_index() |
| 110 | + ) |
| 111 | + fig_jur = px.bar( |
| 112 | + assets_by_jurisdiction, |
| 113 | + x="jurisdiction", |
| 114 | + y="asset_id", |
| 115 | + title="Assets by Jurisdiction", |
| 116 | + labels={"asset_id": "Asset Count"}, |
| 117 | + ) |
| 118 | + st.plotly_chart(fig_jur, use_container_width=True) |
| 119 | + |
| 120 | +with col5: |
| 121 | + assets_by_class = ( |
| 122 | + portfolio_df.groupby("asset_class")["asset_id"].nunique().reset_index() |
| 123 | + ) |
| 124 | + fig_class = px.bar( |
| 125 | + assets_by_class.sort_values("asset_id", ascending=False), |
| 126 | + x="asset_class", |
| 127 | + y="asset_id", |
| 128 | + title="Assets by Class", |
| 129 | + labels={"asset_id": "Asset Count"}, |
| 130 | + ) |
| 131 | + st.plotly_chart(fig_class, use_container_width=True) |
| 132 | + |
| 133 | +# Section 2 - Historical Tracking (Primary Visual) |
| 134 | +st.subheader("Asset Classification Timeline (SCD Type 2)") |
| 135 | +st.caption("Timeline uses synthetic history for Equipment/IP/Certification assets (change date = latest holdings date - 60 days).") |
| 136 | +scd_df = day17_query_df("day17_QUERY_scd_timeline.sql") |
| 137 | +scd_df["valid_from"] = pd.to_datetime(scd_df["valid_from"]) |
| 138 | +scd_df["valid_to"] = pd.to_datetime(scd_df["valid_to"]).fillna(latest_date) |
| 139 | + |
| 140 | +fig_timeline = px.timeline( |
| 141 | + scd_df, |
| 142 | + x_start="valid_from", |
| 143 | + x_end="valid_to", |
| 144 | + y="asset_id", |
| 145 | + color="asset_class", |
| 146 | + hover_data=["asset_name", "asset_type", "version_status"], |
| 147 | + title="Asset Classification Changes Over Time", |
| 148 | +) |
| 149 | +fig_timeline.update_yaxes(autorange="reversed") |
| 150 | +fig_timeline.update_layout(height=500) |
| 151 | +st.plotly_chart(fig_timeline, use_container_width=True) |
| 152 | + |
| 153 | +st.subheader("Classification Changes This Quarter") |
| 154 | +changes_df = day17_query_df("day17_QUERY_changes_this_quarter.sql") |
| 155 | +st.dataframe(changes_df, use_container_width=True) |
| 156 | + |
| 157 | +# Section 3 - Compliance Status |
| 158 | +st.subheader("Compliance Status by Jurisdiction") |
| 159 | +compliance_df = day17_query_df("day17_QUERY_compliance_by_jurisdiction.sql") |
| 160 | +if selected_jurisdictions: |
| 161 | + compliance_df = compliance_df[ |
| 162 | + compliance_df["jurisdiction"].isin(selected_jurisdictions) |
| 163 | + ] |
| 164 | + |
| 165 | +fig_compliance = px.bar( |
| 166 | + compliance_df, |
| 167 | + x="jurisdiction", |
| 168 | + y="asset_count", |
| 169 | + color="compliance_status", |
| 170 | + barmode="group", |
| 171 | + title="Compliance Status by Jurisdiction", |
| 172 | + labels={"asset_count": "Asset Count"}, |
| 173 | +) |
| 174 | +st.plotly_chart(fig_compliance, use_container_width=True) |
| 175 | + |
| 176 | +# Section 4 - Risk and Deadlines |
| 177 | +st.subheader("Upcoming Compliance Deadlines") |
| 178 | +deadlines_df = day17_query_df("day17_QUERY_upcoming_deadlines.sql") |
| 179 | +if selected_jurisdictions: |
| 180 | + deadlines_df = deadlines_df[deadlines_df["jurisdiction"].isin(selected_jurisdictions)] |
| 181 | + |
| 182 | +critical_days = DAY17_DEADLINE_THRESHOLDS_DAYS["critical"] |
| 183 | +warning_days = DAY17_DEADLINE_THRESHOLDS_DAYS["warning"] |
| 184 | + |
| 185 | +def day17_urgency_label(days: int) -> str: |
| 186 | + if days <= critical_days: |
| 187 | + return "Critical" |
| 188 | + if days <= warning_days: |
| 189 | + return "Warning" |
| 190 | + return "OK" |
| 191 | + |
| 192 | + |
| 193 | +deadlines_df["urgency"] = deadlines_df["days_to_deadline"].apply(day17_urgency_label) |
| 194 | + |
| 195 | +st.dataframe( |
| 196 | + deadlines_df.sort_values(["urgency", "days_to_deadline"]), |
| 197 | + use_container_width=True, |
| 198 | +) |
| 199 | + |
| 200 | +st.subheader("Point-in-Time Portfolio Composition") |
| 201 | +point_df = day17_query_df( |
| 202 | + "day17_QUERY_point_in_time.sql", |
| 203 | + params={"as_of_date": point_in_time_date.isoformat()}, |
| 204 | +) |
| 205 | +st.dataframe(point_df, use_container_width=True) |
0 commit comments