import os import json import csv from datetime import datetime import numpy as np import pandas as pd import faiss import streamlit as st import altair as alt from sentence_transformers import SentenceTransformer # ========================================================= # PAGE CONFIG # ========================================================= st.set_page_config( page_title="IGPA Legislation Explorer", layout="wide", initial_sidebar_state="expanded" ) # ========================================================= # PASSWORD GATE # ========================================================= APP_PASSWORD = "bills" if "authed" not in st.session_state: st.session_state.authed = False if not st.session_state.authed: st.markdown( """ """, unsafe_allow_html=True, ) st.markdown('
IGPA Legislation Explorer
', unsafe_allow_html=True) st.markdown( """
πŸ” Quick riddle to enter:

β€œWhat do lawmakers pass to create laws?”

Answer format: all lowercase, one word
""", unsafe_allow_html=True, ) _, center_col, _ = st.columns([1, 1.2, 1]) with center_col: pw = st.text_input("Your answer", type="password") if st.button("Enter", use_container_width=True): if not APP_PASSWORD: st.error("Server misconfigured: APP_PASSWORD not set in Space secrets.") elif pw.strip().lower() == APP_PASSWORD: st.session_state.authed = True st.rerun() else: st.error("Incorrect password.") st.stop() # ========================================================= # THEME-SAFE STYLES # ========================================================= st.markdown( """ """, unsafe_allow_html=True ) # ========================================================= # CONFIG # ========================================================= DB_DIR = "." FEEDBACK_CSV = os.path.join(DB_DIR, "impact_feedback.csv") DEFAULT_TOP_K = 10 IMPACT_ORDER = [ "Not Impactful", "Slightly Impactful", "Moderately Impactful", "Very Impactful" ] DATE_COL = "status_date" SUMMARY_COL = "Llama Summary" LINK_COL = "ftp_url" CATEGORY_COL = "category_std" SUBCATEGORY_COL = "subcategory_std" POLICY_COL = "policy_domain_final" BENEFICIARY_CATEGORY_COL = "intended_beneficiaries_category" BENEFICIARY_SUBCATEGORY_COL = "intended_beneficiaries_subcategory" BENEFICIARY_KEYWORD_COL = "intended_beneficiaries_keyword" BENEFICIARY_COL = "intended_beneficiaries_final" STRATEGY_COL = "legislative_strategy_std" GOAL_COL = "legislative_goal_std" INTENT_COL = "intent_std" INCREASING_COL = "increasing_aspects_std" DECREASING_COL = "decreasing_aspects_std" MOTIVATION_COL = "motivation_std" IMPACT_COL_PREFERRED = "impact_rating_std" IMPACT_COL_FALLBACK = "Impact Rating" # ========================================================= # SIDEBAR TOP ACTION # ========================================================= with st.sidebar: if st.button("Logout", use_container_width=True): st.session_state.authed = False st.rerun() # ========================================================= # LOAD VECTOR DB # ========================================================= @st.cache_resource def load_vector_db(db_dir: str = DB_DIR): with open(os.path.join(db_dir, "config.json"), "r", encoding="utf-8") as f: cfg = json.load(f) index = faiss.read_index(os.path.join(db_dir, "faiss_index.bin")) meta = pd.read_parquet(os.path.join(db_dir, "metadata.parquet")) meta = meta.reset_index(drop=True) if "vec_id" not in meta.columns: meta = meta.reset_index().rename(columns={"index": "vec_id"}) model = SentenceTransformer(cfg["embedding_model_name"]) return index, meta, model, cfg index, meta_df, embed_model, cfg = load_vector_db() IMPACT_COL = IMPACT_COL_PREFERRED if IMPACT_COL_PREFERRED in meta_df.columns else IMPACT_COL_FALLBACK if DATE_COL in meta_df.columns: meta_df[DATE_COL] = pd.to_datetime(meta_df[DATE_COL], errors="coerce") # ========================================================= # HELPERS # ========================================================= def impact_to_score(x): if pd.isna(x): return np.nan x = str(x).strip().lower() mapping = { "not impactful": 0, "slightly impactful": 1, "moderately impactful": 2, "very impactful": 3 } return mapping.get(x, np.nan) if "impact_rating_score" not in meta_df.columns and IMPACT_COL in meta_df.columns: meta_df["impact_rating_score"] = meta_df[IMPACT_COL].apply(impact_to_score) DEFAULT_FILTERS = { "categories": [], "subcategories": [], "beneficiary_categories": [], "beneficiary_subcategories": [], "policy_domains": [], "impact_selected": [], "bill_statuses": [], "date_range": ( meta_df[DATE_COL].min().date() if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].min()) else datetime.utcnow().date(), meta_df[DATE_COL].max().date() if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].max()) else datetime.utcnow().date(), ), "num_search_results": DEFAULT_TOP_K, } for key, value in DEFAULT_FILTERS.items(): if key not in st.session_state: st.session_state[key] = value if "search_results" not in st.session_state: st.session_state.search_results = None if "current_query" not in st.session_state: st.session_state.current_query = "" if "history" not in st.session_state: st.session_state.history = [] def embed_query(query: str): return embed_model.encode( [query], normalize_embeddings=True, convert_to_numpy=True ).astype("float32") def append_feedback_row( bill_id, predicted_impact, user_response, corrected_impact=None, comment=None, path=FEEDBACK_CSV, ): try: file_exists = os.path.isfile(path) with open(path, "a", newline="", encoding="utf-8") as f: writer = csv.writer(f) if not file_exists: writer.writerow( [ "timestamp", "bill_id", "predicted_impact", "user_response", "corrected_impact", "comment", ] ) writer.writerow( [ datetime.utcnow().isoformat(), bill_id, predicted_impact, user_response, corrected_impact if corrected_impact else "", comment if comment else "", ] ) except Exception as e: st.error(f"Failed to save feedback: {str(e)}") def get_multiselect_options(df, col_name): if col_name not in df.columns: return [] vals = df[col_name].dropna().astype(str).str.strip() return sorted([v for v in vals.unique().tolist() if v]) def get_dependent_subcategories(df, category_col, subcategory_col, selected_categories): if subcategory_col not in df.columns: return [] sub_df = df.copy() if selected_categories and category_col in df.columns: sub_df = sub_df[sub_df[category_col].isin(selected_categories)] vals = sub_df[subcategory_col].dropna().astype(str).str.strip() return sorted([v for v in vals.unique().tolist() if v]) def get_beneficiary_categories(df): if BENEFICIARY_CATEGORY_COL not in df.columns: return [] vals = df[BENEFICIARY_CATEGORY_COL].dropna().astype(str).str.strip() return sorted([v for v in vals.unique().tolist() if v]) def get_beneficiary_subcategories(df, selected_categories): return get_dependent_subcategories( df, BENEFICIARY_CATEGORY_COL, BENEFICIARY_SUBCATEGORY_COL, selected_categories ) def get_categories(df): if CATEGORY_COL not in df.columns: return [] vals = df[CATEGORY_COL].dropna().astype(str).str.strip() return sorted([v for v in vals.unique().tolist() if v]) def get_subcategories(df, selected_categories): return get_dependent_subcategories( df, CATEGORY_COL, SUBCATEGORY_COL, selected_categories ) def build_filter_mask(df): mask = pd.Series(True, index=df.index) selected_categories = st.session_state.get("categories", []) if selected_categories and CATEGORY_COL in df.columns: mask &= df[CATEGORY_COL].isin(selected_categories) selected_subcategories = st.session_state.get("subcategories", []) if selected_subcategories and SUBCATEGORY_COL in df.columns: mask &= df[SUBCATEGORY_COL].isin(selected_subcategories) selected_beneficiary_categories = st.session_state.get("beneficiary_categories", []) if selected_beneficiary_categories and BENEFICIARY_CATEGORY_COL in df.columns: mask &= df[BENEFICIARY_CATEGORY_COL].isin(selected_beneficiary_categories) selected_beneficiary_subcategories = st.session_state.get("beneficiary_subcategories", []) if selected_beneficiary_subcategories and BENEFICIARY_SUBCATEGORY_COL in df.columns: mask &= df[BENEFICIARY_SUBCATEGORY_COL].isin(selected_beneficiary_subcategories) selected_policy_domains = st.session_state.get("policy_domains", []) if selected_policy_domains and POLICY_COL in df.columns: mask &= df[POLICY_COL].isin(selected_policy_domains) selected_impact = st.session_state.get("impact_selected", []) if selected_impact and IMPACT_COL in df.columns: mask &= df[IMPACT_COL].isin(selected_impact) selected_statuses = st.session_state.get("bill_statuses", []) if selected_statuses and "status_desc" in df.columns: mask &= df["status_desc"].isin(selected_statuses) if "date_range" in st.session_state and st.session_state.date_range and DATE_COL in df.columns: dr = st.session_state.date_range if isinstance(dr, (tuple, list)) and len(dr) == 2: start, end = dr else: start = end = dr start = pd.to_datetime(start) end = pd.to_datetime(end) mask &= df[DATE_COL].between(start, end) return mask def reset_filters(): for key, value in DEFAULT_FILTERS.items(): st.session_state[key] = value st.session_state.search_results = None st.session_state.current_query = "" st.rerun() def get_first_available(row, cols, default=""): for c in cols: if c in row.index: val = row.get(c) if pd.notna(val) and str(val).strip(): return val return default # ========================================================= # SIDEBAR FILTERS # ========================================================= with st.sidebar: st.header("Filters") if st.button("Reset Filters", use_container_width=True): reset_filters() st.caption("Tip: Apply filters here before reviewing bills and charts.") st.markdown("### Category & Subcategory") has_category_cols = CATEGORY_COL in meta_df.columns and SUBCATEGORY_COL in meta_df.columns if has_category_cols: category_options = get_categories(meta_df) st.multiselect( "Category", options=category_options, key="categories" ) subcategory_options = get_subcategories( meta_df, st.session_state.categories ) current_subcats = st.session_state.get("subcategories", []) valid_current_subcats = [x for x in current_subcats if x in subcategory_options] if current_subcats != valid_current_subcats: st.session_state["subcategories"] = valid_current_subcats st.multiselect( "Subcategory", options=subcategory_options, key="subcategories" ) else: st.info("Category and Subcategory filters are unavailable because 'category_std' and 'subcategory_std' are not present in the dataset.") st.markdown("### Intended Beneficiaries") beneficiary_category_options = get_beneficiary_categories(meta_df) st.multiselect( "Intended Beneficiary Category", options=beneficiary_category_options, key="beneficiary_categories" ) beneficiary_subcategory_options = get_beneficiary_subcategories( meta_df, st.session_state.beneficiary_categories ) current_beneficiary_subcats = st.session_state.get("beneficiary_subcategories", []) valid_beneficiary_subcats = [x for x in current_beneficiary_subcats if x in beneficiary_subcategory_options] if current_beneficiary_subcats != valid_beneficiary_subcats: st.session_state["beneficiary_subcategories"] = valid_beneficiary_subcats st.multiselect( "Intended Beneficiary Subcategory", options=beneficiary_subcategory_options, key="beneficiary_subcategories" ) st.markdown("### Policy, Impact, Status") st.multiselect( "Policy Area", options=get_multiselect_options(meta_df, POLICY_COL), key="policy_domains" ) st.multiselect( "Impact Rating (STD)", options=IMPACT_ORDER, key="impact_selected" ) st.multiselect( "Bill Status", options=get_multiselect_options(meta_df, "status_desc"), key="bill_statuses" ) st.markdown("### Time Filter") min_date = ( meta_df[DATE_COL].min().date() if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].min()) else datetime.utcnow().date() ) max_date = ( meta_df[DATE_COL].max().date() if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].max()) else datetime.utcnow().date() ) st.date_input( "Status Date Range", value=st.session_state.get("date_range", (min_date, max_date)), min_value=min_date, max_value=max_date, key="date_range" ) if os.path.exists(FEEDBACK_CSV): try: df_feedback = pd.read_csv(FEEDBACK_CSV) st.info(f"Feedback records: {len(df_feedback)}") with open(FEEDBACK_CSV, "rb") as f: st.download_button( label="Download impact_feedback.csv", data=f.read(), file_name="impact_feedback.csv", mime="text/csv" ) except Exception: st.info("Feedback CSV ready") with st.expander("Search History"): for i, item in enumerate(reversed(st.session_state.history[-5:]), 1): st.write(f"{i}. {item.get('query', '')}") # ========================================================= # FILTERED DATA # ========================================================= filtered_df = meta_df[build_filter_mask(meta_df)].copy() # ========================================================= # TABS # ========================================================= tab_search, tab_trends = st.tabs(["Search & Results", "Trends & Insights"]) # ========================================================= # SEARCH TAB # ========================================================= with tab_search: st.markdown('
IGPA Legislation Explorer
', unsafe_allow_html=True) st.markdown( '
Search, filter, and analyze legislative bills by impact, policy area, category, subcategory, and intended beneficiaries.
', unsafe_allow_html=True ) col1, col2, col3, col4 = st.columns(4) total_bills = len(filtered_df) policy_count = filtered_df[POLICY_COL].nunique() if POLICY_COL in filtered_df.columns else 0 beneficiary_count = filtered_df[BENEFICIARY_COL].nunique() if BENEFICIARY_COL in filtered_df.columns else 0 very_impactful = (filtered_df[IMPACT_COL] == "Very Impactful").sum() if IMPACT_COL in filtered_df.columns else 0 with col1: st.markdown(f"""
Total Bills
{total_bills}
""", unsafe_allow_html=True) with col2: st.markdown(f"""
Policy Domains
{policy_count}
""", unsafe_allow_html=True) with col3: st.markdown(f"""
Beneficiary Groups
{beneficiary_count}
""", unsafe_allow_html=True) with col4: st.markdown(f"""
Very Impactful Bills
{very_impactful}
""", unsafe_allow_html=True) st.markdown('
Most Impacted Beneficiary Keywords
', unsafe_allow_html=True) if BENEFICIARY_KEYWORD_COL in filtered_df.columns and "impact_rating_score" in filtered_df.columns: impact_df = ( filtered_df.dropna(subset=[BENEFICIARY_KEYWORD_COL, "impact_rating_score"]) .groupby(BENEFICIARY_KEYWORD_COL) .agg( avg_impact=("impact_rating_score", "mean"), bills=("bill_id", "count"), top_bills=("title", lambda x: "; ".join(x.head(5))) ) .reset_index() .sort_values(["avg_impact", "bills"], ascending=[False, False]) .head(15) ) if not impact_df.empty: chart = ( alt.Chart(impact_df) .mark_bar() .encode( x=alt.X(f"{BENEFICIARY_KEYWORD_COL}:N", sort="-y", title="Beneficiary Keyword"), y=alt.Y("avg_impact:Q", title="Average Impact Score"), color=alt.Color( "avg_impact:Q", scale=alt.Scale(domain=[0, 3], range=["#fde68a", "#dc2626"]), legend=alt.Legend(title="Average Impact") ), tooltip=[ alt.Tooltip(f"{BENEFICIARY_KEYWORD_COL}:N", title="Beneficiary Keyword"), alt.Tooltip("avg_impact:Q", format=".2f", title="Average Impact"), alt.Tooltip("bills:Q", title="Number of Bills"), alt.Tooltip("top_bills:N", title="Top Bills") ] ) .properties(height=350) ) st.altair_chart(chart, use_container_width=True) else: st.info("No beneficiary keyword impact data available for the current filters.") else: st.info("Beneficiary keyword or impact score columns are not available.") table_header_col1 = st.columns([1])[0] with table_header_col1: st.markdown('
Bills Matching Selected Filters
', unsafe_allow_html=True) st.caption( "These rows reflect the active toolbar filters. ILGA columns are source legislative fields. " "Llama columns are AI-generated summaries or standardized outputs." ) display_cols = { "bill_number": "Bill Number (ILGA)", "title": "Title (ILGA)", "description": "Description (ILGA)", CATEGORY_COL: "Category", SUBCATEGORY_COL: "Subcategory", BENEFICIARY_CATEGORY_COL: "Intended Beneficiary Category", BENEFICIARY_SUBCATEGORY_COL: "Intended Beneficiary Subcategory", SUMMARY_COL: "Llama Summary", "Potential Impact": "Potential Impact (Llama)", IMPACT_COL: "Impact Rating (STD)", "status_desc": "Status (ILGA)", LINK_COL: "Bill Link (ILGA)" } available_cols = [c for c in display_cols if c in filtered_df.columns] filter_bill_df = ( filtered_df[available_cols] .rename(columns=display_cols) .copy() ) st.dataframe( filter_bill_df, use_container_width=True, column_config={ "Bill Link (ILGA)": st.column_config.LinkColumn( label="Bill Link (ILGA)", display_text="Open Bill" ) } if "Bill Link (ILGA)" in filter_bill_df.columns else None ) csv = filter_bill_df.to_csv(index=False).encode("utf-8") st.download_button( label="Download as CSV", data=csv, file_name="filtered_bills.csv", mime="text/csv" ) search_header_col1, search_header_col2 = st.columns([6, 2]) with search_header_col1: st.markdown('
Search Bills
', unsafe_allow_html=True) with search_header_col2: st.slider( "Number of Results – Search Query Results", 5, 50, st.session_state.get("num_search_results", DEFAULT_TOP_K), 5, key="num_search_results" ) search_col, button_col = st.columns([6, 1]) with search_col: query = st.text_input( "Ask a question about legislation", value=st.session_state.current_query, placeholder="Example: bills related to higher education funding, healthcare workers, or workforce policy" ) with button_col: st.markdown("
", unsafe_allow_html=True) search_clicked = st.button("Search", use_container_width=True) if search_clicked and query.strip(): st.session_state.current_query = query st.session_state.history.append({"query": query}) q_vec = embed_query(query) top_k = st.session_state.num_search_results n_search = min(len(meta_df), top_k * 5) scores, ids = index.search(q_vec, n_search) ids, scores = ids[0], scores[0] allowed = set(filtered_df.index) kept = [(i, s) for i, s in zip(ids, scores) if i in allowed][:top_k] if not kept: st.warning("No results found for this query under the current filters.") st.session_state.search_results = None else: results = meta_df.loc[[i for i, _ in kept]].copy() results["similarity"] = [s for _, s in kept] st.session_state.search_results = results if st.session_state.search_results is not None: results = st.session_state.search_results st.markdown('
Filtered Results Table
', unsafe_allow_html=True) st.caption("Search results are ranked by semantic similarity after applying the active toolbar filters.") st.caption("ILGA fields come from source legislation data. Llama fields are AI-generated summaries or standardized outputs.") review_cols = [ "bill_number", "title", "description", CATEGORY_COL, SUBCATEGORY_COL, BENEFICIARY_CATEGORY_COL, BENEFICIARY_SUBCATEGORY_COL, SUMMARY_COL, GOAL_COL, "Potential Impact", INCREASING_COL, DECREASING_COL, IMPACT_COL, "similarity", LINK_COL ] review_df = results[[c for c in review_cols if c in results.columns]].copy() review_df.rename( columns={ "bill_number": "Bill Number", "title": "Title", "description": "Description from Legiscan", CATEGORY_COL: "Category", SUBCATEGORY_COL: "Subcategory", BENEFICIARY_CATEGORY_COL: "Main Category (LLaMA Generated Response)", BENEFICIARY_SUBCATEGORY_COL: "Beneficiary Subcategory", SUMMARY_COL: "LLaMA Summary", GOAL_COL: "Legislative Goal", "Potential Impact": "Potential Impact (LLaMA)", INCREASING_COL: "Increasing Aspects (STD)", DECREASING_COL: "Decreasing Aspects (STD)", IMPACT_COL: "Impact Rating (STD)", "similarity": "Similarity Score", LINK_COL: "Bill URL (ILGA)" }, inplace=True ) st.dataframe( review_df, use_container_width=True, column_config={ "Bill URL (ILGA)": st.column_config.LinkColumn( "Bill URL (ILGA)", display_text="Open bill" ) } if "Bill URL (ILGA)" in review_df.columns else None ) csv_data = review_df.to_csv(index=False).encode("utf-8") st.download_button( label="Download CSV", data=csv_data, file_name="search_results.csv", mime="text/csv" ) st.markdown("---") st.markdown('
Top Matching Bills
', unsafe_allow_html=True) for idx, row in results.iterrows(): bill_number_val = get_first_available(row, ["bill_number"]) title_val = get_first_available(row, ["title"]) description_val = get_first_available(row, ["description"]) category_val = get_first_available(row, [CATEGORY_COL]) subcategory_val = get_first_available(row, [SUBCATEGORY_COL]) summary_val = get_first_available(row, [SUMMARY_COL]) st.markdown(f"**Bill Number** \n{bill_number_val if bill_number_val else 'N/A'}") st.markdown(f"**Title** \n{title_val if title_val else 'N/A'}") if description_val: st.markdown(f"**Description from Legiscan** \n{description_val}") if category_val: st.markdown(f"**Category** \n{category_val}") if subcategory_val: st.markdown(f"**Subcategory** \n{subcategory_val}") if pd.notna(row.get(LINK_COL)): st.markdown(f"[Open Full Bill]({row.get(LINK_COL)})") if summary_val: with st.expander("Summary from LLaMA", expanded=True): st.write(summary_val) detail_map = { "Status": "status_desc", "Legislative Goal": "Legislative Goal", "Key Provisions": "Key Provisions", "Increasing Aspects": "Increasing Aspects", "Decreasing Aspects": "Decreasing Aspects", "Category & Subcategory": "Category & Subcategory", "Ideological Alignment": "Ideological Alignment", "Potential Impact": "Potential Impact", "Original Law": "Original Law", "committee": "committee", "last_action_date": "last_action_date", "last_action": "last_action", "ILGA State Link": "state_link", "Legiscan URL": "url", "ftp_url": "ftp_url", } with st.expander("More Details"): for label, col_name in detail_map.items(): if col_name in results.columns: val = row.get(col_name) if pd.notna(val) and str(val).strip(): if label in ["ILGA State Link", "Legiscan URL", "ftp_url"]: st.markdown(f"**{label}:** [Open Link]({val})") else: st.write(f"**{label}:** {val}") with st.expander("Impact Rating Accuracy", expanded=False): st.markdown("**Is this impact rating accurate?**") predicted_impact = row.get(IMPACT_COL, "") bill_id_safe = str(row.get("bill_id", idx)) feedback_submitted = st.session_state.get(f"feedback_done_{bill_id_safe}", False) if feedback_submitted: st.success("Thank you for your feedback.") st.caption(f"Bill: {row.get('bill_number', 'N/A')} | Saved to impact_feedback.csv") else: col_yes, col_no = st.columns(2) with col_yes: if st.button("Yes - Accurate", key=f"yes_{bill_id_safe}", use_container_width=True): append_feedback_row( bill_id=bill_id_safe, predicted_impact=predicted_impact, user_response="Yes", corrected_impact=None, comment=None, ) st.session_state[f"feedback_done_{bill_id_safe}"] = True st.rerun() with col_no: if st.button("No - Incorrect", key=f"no_{bill_id_safe}", use_container_width=True): st.session_state[f"show_corrected_{bill_id_safe}"] = True st.rerun() if st.session_state.get(f"show_corrected_{bill_id_safe}", False): corrected_value = st.selectbox( "Correct impact rating", IMPACT_ORDER, key=f"corrected_{bill_id_safe}", ) comment = st.text_area( "Optional correction note", max_chars=250, key=f"comment_{bill_id_safe}", placeholder="Add a short note explaining why the impact rating should change" ) col_submit, col_cancel = st.columns([3, 1]) with col_submit: if st.button("Submit Feedback", key=f"submit_{bill_id_safe}", type="primary"): append_feedback_row( bill_id=bill_id_safe, predicted_impact=predicted_impact, user_response="No", corrected_impact=corrected_value, comment=comment, ) st.session_state[f"feedback_done_{bill_id_safe}"] = True st.session_state[f"show_corrected_{bill_id_safe}"] = False st.rerun() with col_cancel: if st.button("Cancel", key=f"cancel_{bill_id_safe}"): st.session_state[f"show_corrected_{bill_id_safe}"] = False st.rerun() st.markdown("---") # ========================================================= # TRENDS TAB # ========================================================= with tab_trends: top_policy = ( filtered_df[POLICY_COL].value_counts().head(1) if POLICY_COL in filtered_df.columns else pd.Series(dtype=int) ) top_beneficiaries = ( filtered_df[BENEFICIARY_COL].value_counts().head(1) if BENEFICIARY_COL in filtered_df.columns else pd.Series(dtype=int) ) strategy_impact = ( filtered_df[filtered_df[IMPACT_COL].notna()] .groupby(STRATEGY_COL)[IMPACT_COL] .apply(lambda x: (x == "Very Impactful").sum()) if STRATEGY_COL in filtered_df.columns and IMPACT_COL in filtered_df.columns else pd.Series(dtype=int) ) avg_impact_ben = ( filtered_df.dropna(subset=["impact_rating_score"]) .groupby(BENEFICIARY_COL)["impact_rating_score"] .mean() .sort_values(ascending=False) if BENEFICIARY_COL in filtered_df.columns and "impact_rating_score" in filtered_df.columns else pd.Series(dtype=float) ) total_bills = len(filtered_df) total_high_impact = ( (filtered_df[IMPACT_COL] == "Very Impactful").sum() if IMPACT_COL in filtered_df.columns else 0 ) st.markdown("### Key Insights") st.write(f"**Total Bills Considered:** {total_bills}") st.write(f"**Total Very Impactful Bills:** {total_high_impact}") st.write( f"**Most Active Policy Domain:** {top_policy.index[0]} ({top_policy.iloc[0]} bills)" if not top_policy.empty else "No data" ) st.write( f"**Most Benefited Group:** {top_beneficiaries.index[0]} ({top_beneficiaries.iloc[0]} bills)" if not top_beneficiaries.empty else "No data" ) st.write( f"**Strategy Producing Most Very Impactful Bills:** {strategy_impact.idxmax()}" if not strategy_impact.empty else "N/A" ) st.write( f"**Highest Average Impact (Beneficiary):** {avg_impact_ben.index[0]} ({avg_impact_ben.iloc[0]:.2f})" if not avg_impact_ben.empty else "N/A" ) st.markdown("---") col1, col2 = st.columns(2) with col1: st.markdown("### Policy Domain Activity") if POLICY_COL in filtered_df.columns: policy_agg = ( filtered_df.groupby(POLICY_COL) .agg( Count=("bill_id", "count"), avg_impact=("impact_rating_score", "mean") if "impact_rating_score" in filtered_df.columns else ("bill_id", "count"), top_bills=("title", lambda x: "; ".join(x.head(5))), top_beneficiaries=( BENEFICIARY_COL, lambda x: ", ".join(x.value_counts().head(3).index) ) if BENEFICIARY_COL in filtered_df.columns else ("title", lambda x: ""), recent_date=( DATE_COL, lambda x: x.max().strftime("%Y-%m-%d") if pd.notna(x.max()) else "" ) if DATE_COL in filtered_df.columns else ("title", lambda x: ""), bill_numbers=("bill_number", lambda x: ", ".join(map(str, x.head(5)))) ) .reset_index() .rename(columns={POLICY_COL: "Policy Domain"}) ) policy_chart = ( alt.Chart(policy_agg) .mark_bar() .encode( x=alt.X("Policy Domain:N", sort="-y", title="Policy Domain"), y=alt.Y("Count:Q", title="Number of Bills"), color=alt.Color( "avg_impact:Q", title="Average Impact", scale=alt.Scale(scheme="orangered") ), tooltip=[ alt.Tooltip("Policy Domain:N"), alt.Tooltip("Count:Q", title="Number of Bills"), alt.Tooltip("avg_impact:Q", format=".2f", title="Average Impact"), alt.Tooltip("top_bills:N", title="Top Bills"), alt.Tooltip("top_beneficiaries:N", title="Top Beneficiaries"), alt.Tooltip("recent_date:N", title="Most Recent Bill"), alt.Tooltip("bill_numbers:N", title="Bill Numbers") ] ) .properties(height=400) ) st.altair_chart(policy_chart, use_container_width=True) else: st.write("No policy domain data available.") with col2: st.markdown("### Impact Distribution") if IMPACT_COL in filtered_df.columns: impact_hover = ( filtered_df[filtered_df[IMPACT_COL].notna()] .groupby(IMPACT_COL) .agg( Count=("bill_id", "count"), top_beneficiaries=( BENEFICIARY_KEYWORD_COL, lambda x: ", ".join(x.dropna().astype(str).value_counts().head(3).index) ) if BENEFICIARY_KEYWORD_COL in filtered_df.columns else ("bill_id", lambda x: ""), top_motivation=( MOTIVATION_COL, lambda x: ", ".join(x.dropna().astype(str).value_counts().head(3).index) ) if MOTIVATION_COL in filtered_df.columns else ("bill_id", lambda x: "") ) .reindex(IMPACT_ORDER) .reset_index() ) impact_chart = ( alt.Chart(impact_hover) .mark_bar() .encode( x=alt.X(f"{IMPACT_COL}:N", sort=IMPACT_ORDER, title="Impact Level"), y=alt.Y("Count:Q"), color=alt.Color("Count:Q", scale=alt.Scale(scheme="reds"), legend=None), tooltip=[ alt.Tooltip(f"{IMPACT_COL}:N", title="Impact Level"), alt.Tooltip("Count:Q", title="Count"), alt.Tooltip("top_beneficiaries:N", title="Top Beneficiaries"), alt.Tooltip("top_motivation:N", title="Top Motivation") ] ) .properties(height=300) ) st.altair_chart(impact_chart, use_container_width=True) else: st.write("No impact rating data available.") st.markdown("### Legislative Strategy: Very Impactful Bills") if STRATEGY_COL in filtered_df.columns and IMPACT_COL in filtered_df.columns: strategy_high_impact = ( filtered_df[filtered_df[IMPACT_COL].notna()] .groupby(STRATEGY_COL) .agg( Very_Impactful_Bills=(IMPACT_COL, lambda x: (x == "Very Impactful").sum()), top_bills=("title", lambda x: "; ".join(x.head(5))), top_beneficiaries=( BENEFICIARY_COL, lambda x: ", ".join(x.value_counts().head(3).index) ) if BENEFICIARY_COL in filtered_df.columns else ("title", lambda x: ""), recent_date=( DATE_COL, lambda x: x.max().strftime("%Y-%m-%d") if pd.notna(x.max()) else "" ) if DATE_COL in filtered_df.columns else ("title", lambda x: "") ) .reset_index() .rename(columns={STRATEGY_COL: "Strategy"}) ) strategy_chart = ( alt.Chart(strategy_high_impact) .mark_bar() .encode( x=alt.X("Strategy:N", sort="-y", title="Strategy"), y=alt.Y("Very_Impactful_Bills:Q", title="Very Impactful Bills"), color=alt.Color("Very_Impactful_Bills:Q", scale=alt.Scale(scheme="orangered")), tooltip=[ alt.Tooltip("Strategy:N"), alt.Tooltip("Very_Impactful_Bills:Q"), alt.Tooltip("top_bills:N", title="Top Bills"), alt.Tooltip("top_beneficiaries:N", title="Top Beneficiaries"), alt.Tooltip("recent_date:N", title="Most Recent Bill") ] ) .properties(height=400) ) st.altair_chart(strategy_chart, use_container_width=True) else: st.write("No legislative strategy data available for selected filters.") st.markdown("### Beneficiary Coverage & Average Impact") if BENEFICIARY_COL in filtered_df.columns and "impact_rating_score" in filtered_df.columns: ben_df = ( filtered_df.dropna(subset=[BENEFICIARY_COL, "impact_rating_score"]) .groupby(BENEFICIARY_COL) .agg( total_bills=("bill_id", "count"), avg_impact=("impact_rating_score", "mean"), top_bills=("title", lambda x: "; ".join(x.head(5))), recent_date=( DATE_COL, lambda x: x.max().strftime("%Y-%m-%d") if pd.notna(x.max()) else "" ) if DATE_COL in filtered_df.columns else ("title", lambda x: ""), bill_numbers=("bill_number", lambda x: ", ".join(map(str, x.head(5)))) ) .reset_index() ) if not ben_df.empty: ben_chart = ( alt.Chart(ben_df) .mark_rect() .encode( x=alt.X("total_bills:Q", title="Number of Bills"), y=alt.Y(f"{BENEFICIARY_COL}:N", sort="-x", title="Beneficiary Group"), color=alt.Color( "avg_impact:Q", scale=alt.Scale(domain=[0, 3], range=["#fde68a", "#dc2626"]), legend=alt.Legend(title="Average Impact Score") ), tooltip=[ alt.Tooltip(f"{BENEFICIARY_COL}:N", title="Beneficiary"), alt.Tooltip("total_bills:Q", title="Number of Bills"), alt.Tooltip("avg_impact:Q", format=".2f", title="Average Impact"), alt.Tooltip("top_bills:N", title="Top Bills"), alt.Tooltip("recent_date:N", title="Most Recent Bill"), alt.Tooltip("bill_numbers:N", title="Bill Numbers") ] ) .properties(height=400) ) st.altair_chart(ben_chart, use_container_width=True) else: st.write("No beneficiary impact data available for selected filters.") else: st.write("No beneficiary data available for selected filters.")