CoolFace
Apppublic

TwinklData/Community_Collections_App

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
app.py475 linesDownload Raw Back to root
1################################2# CONFIGURATION3################################4 5import streamlit as st6import pandas as pd7import hashlib8import joblib9from io import BytesIO10from umap import UMAP11from hdbscan import HDBSCAN12 13import os14 15from streamlit_extras.metric_cards import style_metric_cards16from streamlit_extras.add_vertical_space import add_vertical_space17 18# ---- FUNCTIONS ----19 20from src.extract_usage import extract_usage21from src.necessity_index import compute_necessity, index_scaler, qcut_labels22from src.column_detection import detect_freeform_col, detect_id_col, detect_school_type_col23from src.shortlist import shortlist_applications24from src.twinkl_originals import find_book_candidates25from src.preprocess_text import normalise_text 26import src.models.topic_modeling_pipeline as topic_modeling_pipeline27from src.px_charts import plot_histogram, plot_topic_countplot28from typing import Tuple29 30style_metric_cards(box_shadow=False, border_left_color='#E7F4FF',background_color='#E7F4FF', border_size_px=0, border_radius_px=6)31 32##################################33# CACHED PROCESSING FUNCTIONS34##################################35 36# -----------------------------------------------------------------------------37# Heavy processing (IO + NLP) is cached to avoid re‑executing when the UI state38# changes. The function only re‑runs if the **file contents** change.39# -----------------------------------------------------------------------------40 41@st.cache_resource42def load_heartfelt_predictor():43    model_path = os.path.join("src", "models", "heartfelt_pipeline.joblib")44    return joblib.load(model_path)45 46@st.cache_resource47def load_embeddings_model():48    return topic_modeling_pipeline.load_embedding_model('all-MiniLM-L12-v2')49 50@st.cache_data(show_spinner=True)51def load_and_process(raw_csv: bytes) -> Tuple[pd.DataFrame, str]:52    """53    Load CSV from raw bytes, detect freeform column, compute necessity scores,54    and extract usage items. Returns processed DataFrame and freeform column name.55    """56    # Read Uploaded Data 57    df_orig = pd.read_csv(BytesIO(raw_csv))58 59    # Detect freeform column60    freeform_col = detect_freeform_col(df_orig)61    id_col = detect_id_col(df_orig)62    school_type_col = detect_school_type_col(df_orig)63    print(id_col)64 65    df_orig = df_orig[df_orig[freeform_col].notna()]66 67    #Word Count68    df_orig['word_count'] = df_orig[freeform_col].fillna('').str.split().str.len()69 70    # Compute Necessity Scores71    scored = df_orig.join(df_orig[freeform_col].apply(compute_necessity))72    scored['necessity_index'] = index_scaler(scored['necessity_index'].values)73    scored['priority'] = qcut_labels(scored['necessity_index'])74 75    # Find Twinkl Originals Candidates76    scored['book_candidates'] = find_book_candidates(scored, freeform_col, school_type_col)77 78    # Label Heartfelt Applications79    scored['clean_text'] = scored[freeform_col].map(normalise_text)80    model = load_heartfelt_predictor()81    scored['is_heartfelt'] = model.predict(scored['clean_text'].astype(str))82 83 84    85    # Usage Extraction86    docs = df_orig[freeform_col].to_list()87    scored['usage'] = extract_usage(docs)88 89    return scored, freeform_col, id_col90 91# -----------------------------------------------------------------------------92# Derivative computations that rely only on the processed DataFrame are also93# cached. These are lightweight but still benefit from caching because this94# function might be called multiple times during widget interaction.95# -----------------------------------------------------------------------------96 97 98@st.cache_data(show_spinner=True)99def compute_shortlist(df: pd.DataFrame) -> pd.DataFrame:100    """Pre‑compute shortlist_score for all rows (used for both modes)."""101    return shortlist_applications(df, k=len(df))102 103@st.cache_resource(show_spinner=True)104def run_topic_modeling():105 106    try:107 108        # ------- 1. Tokenize texts into sentences -------109        nlp = topic_modeling_pipeline.load_spacy_model(model_name='en_core_web_sm')110 111        sentences = []112        mappings = []113 114        for idx, application_text in df[freeform_col].dropna().items():115            for sentence in topic_modeling_pipeline.spacy_sent_tokenize(application_text):116                sentences.append(sentence)117                mappings.append(idx)118 119 120        # -------- 2. Generate embeddings -------121 122        embeddings_model = load_embeddings_model()123        embeddings = embeddings_model.encode(sentences, show_progress_bar=True)124 125        # -------- 3. Topic Modeling --------126 127        umap_model = UMAP(n_neighbors=7, n_components=5, min_dist=0.0, metric='cosine', random_state=42)128        hdbscan_model = HDBSCAN(min_cluster_size=10, metric='euclidean', cluster_selection_method='eom', prediction_data=True)129 130        # --------- 4. Perform Topic Modeling ---------131 132        topic_model, topics, probs = topic_modeling_pipeline.bertopic_model(sentences, embeddings, embeddings_model, umap_model, hdbscan_model)133 134        topic_modeling_pipeline.ai_labels_to_custom_name(topic_model) 135 136        return topic_model, topics, probs, mappings137 138    except Exception as e:139        st.error(f"Topic modeling failed: {e}")140        st.code(traceback.format_exc())  # Shows the full error in a nice code box141        return None, None, None, None142 143 144 145################################146# MAIN APP SCRIPT147################################148 149st.title("🪷 Community Collections Helper")150 151uploaded_file = st.file_uploader("Upload grant applications file for analysis", type='csv', label_visibility='collapsed')152 153 154# ========== FINGERPRINTING CURRENT FILE ==========155# This helps avoid reruns of certain functions as156# long as the file stays the same157 158if uploaded_file is not None:159    raw = uploaded_file.read()160    file_hash = hashlib.md5(raw).hexdigest()161    st.session_state["current_file_hash"] = file_hash162else:163    raw = None164    st.session_state.pop("current_file_hash", None)165 166if raw is None:167    st.stop()168 169## ====== DATA PROCESSING ======170 171df, freeform_col, id_col = load_and_process(raw) # from cached function172topic_model, topics, probs, mappings = run_topic_modeling() # from cached function173 174if topic_model is not None:175    label_map = (topic_model176                 .get_topic_info()177                 .set_index("Topic")["CustomName"]178                 .to_dict())179    df = topic_modeling_pipeline.attach_topics(df, mappings, topics, label_map, col="topics")180else:181    st.warning("Topics could not be generated; continuing without them.")182 183 184topics_df = topic_model.get_topic_info()185topics_df = topics_df[topics_df['Topic'] > -1]186topics_df.drop(columns=['Name', 'OpenAI'], inplace=True)187cols_to_move = ['Topic','CustomName']188topics_df = topics_df[cols_to_move + [col for col in topics_df.columns if col not in cols_to_move]]189topics_df.rename(columns={'CustomName':'Topic Name', 'Topic':'Topic Nr.'}, inplace=True)190 191 192 193book_candidates_df = df[df['book_candidates'] == True]194 195###############################196#         SIDE PANNEL         #197###############################198 199with st.sidebar:200    st.title("Shortlist Mode")201 202 203    quantile_map = {"strict": 0.75, "generous": 0.5}204    mode = st.segmented_control(205            "Select one option",206            options=["strict", "generous"],207            default="strict",208            )209    210    scored_full = compute_shortlist(df)211    threshold_score = scored_full["shortlist_score"].quantile(quantile_map[mode])212    auto_short_df = scored_full[scored_full["shortlist_score"] >= threshold_score]213 214    st.title("Filters")215 216    ## --- Dataframe To Filter ---217    options = ['All applications', 'Not shortlisted'] 218    selected_view = st.pills('Choose data to filter', options, default='Not shortlisted')219    st.write("")220 221    ## --- Necessity Index Filtering ---222    min_idx = float(df['necessity_index'].min())223    max_idx = float(df['necessity_index'].max())224    filter_range = st.slider(225        "Necessity Index Range", min_value=min_idx, max_value=max_idx, value=(min_idx, max_idx)226    )227    228    def filter_all_applications(df, auto_short_df, filter_range):229        return df[df['necessity_index'].between(filter_range[0], filter_range[1])]230 231    def filter_not_shortlisted(df, auto_short_df, filter_range):232        return df[233            (~df.index.isin(auto_short_df.index)) &234            (df['necessity_index'].between(filter_range[0], filter_range[1]))235        ]236 237    filter_map = {238            'All applications': filter_all_applications,239            'Not shortlisted': filter_not_shortlisted,240            }241 242    filtered_df = filter_map[selected_view](df, auto_short_df, filter_range)243 244    ## -------- Topic Filtering -------245 246    topic_options = sorted(topics_df['Topic Name'].unique())247    selected_topics = st.multiselect("Filter by Topic(s)", options=topic_options, default=[])248 249    if selected_topics:250        selected_set = set(selected_topics)251        filtered_df = filtered_df[252            filtered_df['topics'].apply(lambda topic_list: selected_set.issubset(set(topic_list)))253        ]254 255    st.markdown(f"**Total Applications:** {len(df)}")256    st.markdown(f"**Filtered Applications:** {len(filtered_df)}")257 258    manual_keys = [k for k in st.session_state.keys() if k.startswith("shortlist_")]259    manually_shortlisted = [int(k.split("_")[1]) for k in manual_keys if st.session_state[k]]260 261    st.markdown(f"**Manually Shortlisted:** {len(manually_shortlisted)}")262    if manually_shortlisted:263        csv = df.loc[manually_shortlisted].to_csv(index=False).encode("utf-8")264        st.download_button(265            "Download Manual Shortlist",266            data=csv,267            file_name="manual_shortlist.csv",268            mime="text/csv",269            icon="⬇️",270        )271 272 273    add_vertical_space(4)274    st.divider()275    st.badge("Version 1.0.0", icon=':material/category:',color='violet')276    st.markdown("""277    :grey[Made with 🩷  by the AI Innovation Team278    Contact: lynn.perez@twinkl.com]279    """)280 281 282 283## ====== CREATE TAB SECTIONS =======284tab1, tab2 = st.tabs(["Shortlist Manager","Insights"])285 286 287##################################################288#              SHORTLIST MANAGER TAB             # 289##################################################290 291with tab1:292    293    ## =========== AUTOMATIC SHORTLIST =========294 295    st.header("Automatic Shortlist")296 297    csv_auto = auto_short_df.to_csv(index=False).encode("utf-8")298    all_processed_data = df.to_csv(index=False).encode("utf-8")299    book_candidates = book_candidates_df.to_csv(index=False).encode("utf-8")300    topic_descriptions_csv = topics_df.to_csv(index=False).encode("utf-8")301 302 303    csv_options = {304        "Shortlist": (csv_auto, "shortlist.csv"),305        "All Processed Data": (all_processed_data, "all_processed.csv"),306        "Book Candidates": (book_candidates, "book_candidates.csv"),307        "Topic Descriptions": (topic_descriptions_csv, "topic_descriptions.csv"),308    }309 310    choice = st.selectbox("Select a file for download", list(csv_options.keys()))311 312    csv_data, file_name = csv_options[choice]313 314 315    st.download_button(316        label=f"Download {choice}",317        data=csv_data,318        file_name=file_name,319        mime="text/csv",320        help="This button will download the selected file from above",321        icon="⬇️"322 323    )324 325    326    st.write("")327    total_col, shortlistCounter_col, mode_col = st.columns(3)328 329    total_col.metric("Applications Submitted", len(df))330    shortlistCounter_col.metric("Shorlist Length",  len(auto_short_df))331    mode_col.metric("Mode", mode)332 333    shorltist_cols_to_show = [334            id_col,335            freeform_col,336            'book_candidates',337            'usage',338            'necessity_index',339            'urgency_score',340            'severity_score',341            'vulnerability_score',342            'shortlist_score',343            'is_heartfelt',344            'topics',345            ]346 347    st.dataframe(auto_short_df.loc[:, shorltist_cols_to_show], hide_index=True)348 349    ## ====== APPLICATIONS REVIEW =======350 351    add_vertical_space(2)352    st.header("Manual Filtering")353    st.info("Use the **side panel** filters to more easily sort through applications that you'd like to review.", icon=':material/info:')354 355    st.write("")356    if len(filtered_df) > 0:357        st.markdown("#### Filtered Applications")358        for idx, row in filtered_df.iterrows():359            with st.expander(f"Application {int(row[id_col])}"):360                st.write("")361                col1, col2, col3, col4 = st.columns(4)362                col1.metric("Necessity", f"{row['necessity_index']:.1f}")363                col2.metric("Urgency", f"{int(row['urgency_score'])}")364                col3.metric("Severity", f"{int(row['severity_score'])}")365                col4.metric("Vulnerability", f"{int(row['vulnerability_score'])}")366 367                st.markdown("##### Excerpt")368                st.write(row[freeform_col])369 370                # HTML for clean usage items 371                usage_items = [item for item in row['usage'] if item and item.lower() != 'none']372                if usage_items:373                    st.markdown("##### Usage")374                    pills_html = "".join(375                            f"<span style='display:inline-block;background-color:#E7F4FF;color:#125E9E;border-radius:20px;padding:4px 10px;margin:2px;font-size:0.95rem;'>{item}</span>"376                        for item in usage_items377                    )378                    st.html(pills_html)379                else:380                    st.caption("*No usage found*")381 382                topic_items = [item for item in row['topics'] if item and item.lower() != 'none']383                if topic_items:384                    st.markdown("##### Topics")385                    topic_boxes_html= "".join(386                    f"<span style='display:inline-block;background-color:#ECE0FC;color:#6741B9;border-radius:5px;padding:4px 10px;margin:2px;font-size:0.95rem;'>{item}</span>"387                    for item in topic_items388                    )389                    st.html(topic_boxes_html)390                else:391                    st.caption("_No topics assigned for this application_")392 393 394                st.checkbox(395                    "Add to shortlist",396                    key=f"shortlist_{idx}"397                )398 399    else:400        st.markdown(401            """402            <br>403            <div style="text-align: center; font-size: 1.2em">404                🍂 <span style="color: grey;">No applications matched these filters...</span>405            </div>406            """,407            unsafe_allow_html=True,408        )409 410 411#########################################412#              INSIGHTS TAB             #413#########################################414 415with tab2:416 417 418    ## =========== DATA OVERVIEW ==========419 420    st.header("General Insights")421    add_vertical_space(1)422 423    col1, col2, col3 = st.columns(3)424    col1.metric("Applications Submitted", len(df))425    col2.metric("Median N.I", df['necessity_index'].median().round(2))426    col3.metric("Avg. Word Count", f"{df['word_count'].mean().round(1)}")427 428    ## --- NI Distribution Plot ---429    ni_distribution_plt = plot_histogram(df, col_to_plot='necessity_index', bins=50, title='Necessity Index Histogram')430    st.plotly_chart(ni_distribution_plt)431 432 433    ## ============= TOPIC MODELING ============434 435    st.header("Topic Modeling")436    add_vertical_space(1)437    438    ## ------- Display Topics Dataframe ------439 440    with st.popover("How are topic extracted?", icon="🌱"):441 442        st.write("""443        **About Topic Modeling**444 445        We use BERTopic to :primary[**dynamically**] extract the most common topics from the natural language data.446 447        BERTopic is a machine learning technique that allows us to group documents (in this case, sentences within application letters) based on their semantic similarity and other patterns such as word frequency and placement.448 449        The table you see below shows you the extracted topics, alongside their top 10 extracted keywords and a small sample of real texts from the applications that demonstrate where the topics came from.450 451        **Table Info**452        - **Topic Nr.:** The 'id' of the topic.453        - **Topic Name:** This is an AI-generated label based on a few samples of application responses alongside their corresponding keywords.454        - **Representation:** Top 10 keywords that best represent a topic455        - **Representative Docs**: Sample sentences contributing to the topic456        """)457    st.dataframe(topics_df, hide_index=True)458 459    ## -------- Plot Topics Chart ----------460 461    topic_count_plot = plot_topic_countplot(topics_df, topic_id_col='Topic Nr.', topic_name_col='Topic Name', representation_col='Representation', height=500, title='Topic Frequency Chart')462    st.plotly_chart(topic_count_plot, use_container_width=True)463 464    ## --------- User Updates -----------465 466    if st.session_state.get("topic_toast_shown_for") != st.session_state["current_file_hash"]:467        st.toast(468        """469        **Topic modeling is ready!** View the results on the _Insights_ tab470        """,471        icon='🎉'472        )473 474        st.session_state["topic_toast_shown_for"] = st.session_state["current_file_hash"]475