Team Ai
Apppublic

DrMostafa/Process_Mining

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
app.py377 linesDownload Raw Back to root
1import streamlit as st2import shutil3import importlib4import pandas as pd5 6# ----------------------------7# Config8# ----------------------------9st.set_page_config(page_title="Mini Process Miner", layout="wide")10DEBUG = True  # set to False to hide the env checks from users11 12# Optional: quick environment/dependency check13if DEBUG:14    st.write("Python OK. Checking deps…")15    st.write("pm4py import:", bool(importlib.util.find_spec("pm4py")))16    st.write("graphviz (pip) import:", bool(importlib.util.find_spec("graphviz")))17    st.write("dot in PATH:", shutil.which("dot"))18 19# ----------------------------20# Page setup21# ----------------------------22st.title("Mini Process Miner (vibe-coded)")23 24# Uploader with clear instructions25uploaded = st.file_uploader(26    "Upload your event log (CSV)",27    type=["csv"],28    help="Use EXACT headers (lowercase): required → case_id, activity, timestamp; optional → column1, column2, column3."29)30 31st.caption(32    "**Required columns:** case_id, activity, timestamp  •  "33    "**Optional:** column1, column2, column3 (e.g., resource, team, location)  •  "34    "Need a sample dataset? [Download a test CSV here](https://drive.google.com/drive/folders/1q0iqn5_FFz4EttLDl0zR09RQ3z4JsdDR)  •  "35    "**Disclaimer:** This demo tool offers no guarantees regarding data security or accuracy; use at your own risk.  •  "36    "Created by Dennis Arrindell, powered by [PM4Py](https://pm4py.fit.fraunhofer.de/), and 100% vibe-coded with ChatGPT."37)38 39 40# ----------------------------41# Helpers42# ----------------------------43def ensure_parsed(df: pd.DataFrame) -> pd.DataFrame:44    """Normalize columns and parse timestamp."""45    df = df.copy()46    df.columns = [c.strip().lower() for c in df.columns]47    df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")48    df = df.dropna(subset=["timestamp"])49    return df50 51def compute_ordered(df: pd.DataFrame) -> pd.DataFrame:52    return df.sort_values(["case_id", "timestamp"])53 54def apply_case_level_exclusion(df: pd.DataFrame, activities_to_drop: list) -> pd.DataFrame:55    """Remove entire cases that contain any of the selected activities."""56    if not activities_to_drop:57        return df58    cases_with_forbidden = df.loc[df["activity"].isin(activities_to_drop), "case_id"].unique()59    return df.loc[~df["case_id"].isin(cases_with_forbidden)].copy()60 61def apply_event_level_exclusion(df: pd.DataFrame, activities_to_remove: list) -> pd.DataFrame:62    """Remove only those activity events, keep the rest of the case."""63    if not activities_to_remove:64        return df65    out = df.loc[~df["activity"].isin(activities_to_remove)].copy()66    valid_cases = out["case_id"].value_counts()67    keep_cases = valid_cases[valid_cases > 0].index68    return out.loc[out["case_id"].isin(keep_cases)].copy()69 70def apply_activity_threshold(df: pd.DataFrame, min_freq: int) -> pd.DataFrame:71    """Drop events whose activity total frequency < min_freq."""72    if min_freq <= 1 or df.empty:73        return df74    counts = df["activity"].value_counts()75    keep_acts = counts[counts >= min_freq].index76    return df.loc[df["activity"].isin(keep_acts)].copy()77 78def build_edges(ordered_df: pd.DataFrame) -> pd.DataFrame:79    """Build directly-follows edges with counts."""80    if ordered_df.empty:81        return pd.DataFrame(columns=["edge", "count"])82    tmp = ordered_df.copy()83    tmp["next_activity"] = tmp.groupby("case_id")["activity"].shift(-1)84    edges = tmp.dropna(subset=["next_activity"])[["activity", "next_activity"]]85    if edges.empty:86        return pd.DataFrame(columns=["edge", "count"])87    edges["edge"] = edges["activity"] + " → " + edges["next_activity"]88    edge_counts = edges["edge"].value_counts().rename_axis("edge").reset_index(name="count")89    return edge_counts90 91def apply_optional_column_includes(df: pd.DataFrame, colname: str, selected: list) -> pd.DataFrame:92    """If selections provided for a column, keep only rows where column ∈ selected."""93    if colname in df.columns and selected:94        return df[df[colname].astype(str).isin([str(x) for x in selected])]95    return df96 97# ----------------------------98# Main99# ----------------------------100if uploaded:101    raw_df = pd.read_csv(uploaded)102 103    # Validate columns early (we normalize to lowercase)104    required = {"case_id", "activity", "timestamp"}105    if not required.issubset(set([c.strip().lower() for c in raw_df.columns])):106        st.error("CSV must include required columns: case_id, activity, timestamp. Optional: column1, column2, column3.")107        st.stop()108 109    df = ensure_parsed(raw_df)110 111    # ----------------------------112    # Sidebar filters (case/event + optional column1/2/3) FIRST113    # ----------------------------114    st.sidebar.header("Filters")115 116    # Optional extra columns (exact names after normalization): column1, column2, column3117    extra_cols_present = [c for c in ["column1", "column2", "column3"] if c in df.columns]118 119    # Case-level exclusion120    all_activities = sorted(df["activity"].astype(str).unique().tolist())121    case_exclude = st.sidebar.multiselect(122        "Remove all CASES containing these activities",123        options=all_activities,124        help="If a case contains one of these activities, the entire case is removed."125    )126 127    # Event-level exclusion128    event_exclude = st.sidebar.multiselect(129        "Remove only EVENTS with these activities (keep cases)",130        options=all_activities,131        help="Events with these activities are dropped, but the case remains if other events exist."132    )133 134    # Optional include filters for extra columns135    if extra_cols_present:136        st.sidebar.markdown("---")137        st.sidebar.subheader("Optional column filters")138        selections = {}139        for col in extra_cols_present:140            options = sorted(df[col].dropna().astype(str).unique().tolist())141            selections[col] = st.sidebar.multiselect(142                f"Include only {col} values",143                options=options,144                help=f"Leave empty to include all {col} values."145            )146    else:147        selections = {}148 149    # Apply case/event filters150    df_filt = apply_case_level_exclusion(df, case_exclude)151    df_filt = apply_event_level_exclusion(df_filt, event_exclude)152 153    # Apply optional column includes154    for col, sel in selections.items():155        df_filt = apply_optional_column_includes(df_filt, col, sel)156 157    if df_filt.empty:158        st.warning("All data filtered out. Adjust filters to see results.")159        st.stop()160 161    ordered = compute_ordered(df_filt)162 163    # ----------------------------164    # Sidebar sliders (activity & connection thresholds)165    # ----------------------------166    act_counts_for_slider = ordered["activity"].value_counts()167    max_act_allowed = int(act_counts_for_slider.max()) if not act_counts_for_slider.empty else 1168    if max_act_allowed < 1:169        max_act_allowed = 1170 171    apply_act_thresh_to_model = st.sidebar.checkbox(172        "Apply activity frequency threshold to the model",173        value=True,174        help="If enabled, activities below the threshold are removed before discovery/visualization."175    )176    min_act = st.sidebar.slider(177        "Min activity frequency to KEEP",178        min_value=1, max_value=max_act_allowed, value=1,179        help="Drops activities whose total frequency is below this value (if enabled above)."180    )181 182    # Create df_model after activity slider decision183    if apply_act_thresh_to_model:184        df_model = apply_activity_threshold(ordered, min_act)185    else:186        df_model = ordered187 188    df_model = compute_ordered(df_model)189    if df_model.empty:190        st.warning("All events dropped by the activity frequency threshold. Lower the threshold.")191        st.stop()192 193    # Connection frequency slider (visual-only)194    edge_counts_for_slider = build_edges(df_model)195    max_edge_allowed = int(edge_counts_for_slider["count"].max()) if not edge_counts_for_slider.empty else 1196    if max_edge_allowed < 1:197        max_edge_allowed = 1198    min_edge = st.sidebar.slider(199        "Min connection frequency to SHOW",200        min_value=1, max_value=max_edge_allowed, value=1,201        help="Hides low-frequency connections in the Connections/DFG views (visual-only)."202    )203 204    st.sidebar.markdown("---")205    st.sidebar.caption("Activity threshold may modify the model; connection threshold only affects visuals.")206 207    # ----------------------------208    # Metrics209    # ----------------------------210    total_cases = df_model["case_id"].nunique()211    total_events = len(df_model)212    unique_acts = df_model["activity"].nunique()213    c1, c2, c3 = st.columns(3)214    c1.metric("Total cases", total_cases)215    c2.metric("Total events", total_events)216    c3.metric("Unique activities", unique_acts)217 218    # ----------------------------219    # Activity frequency (reflects min_act)220    # ----------------------------221    st.subheader("Activity frequency")222    act_counts = df_model["activity"].value_counts().rename_axis("activity").reset_index(name="count")223    st.dataframe(act_counts[act_counts["count"] >= min_act], use_container_width=True)224    st.bar_chart(act_counts.set_index("activity")["count"])225 226    # ----------------------------227    # Variants (quick & dirty)228    # ----------------------------229    try:230        variants = (231            df_model.groupby("case_id")["activity"]232            .apply(lambda s: " → ".join(s))233            .value_counts()234        )235        st.subheader("Top variants (quick & dirty)")236        st.dataframe(237            variants.rename("count").reset_index().rename(columns={"index": "variant"}).head(20),238            use_container_width=True239        )240    except Exception:241        st.info("Could not compute variants; check your timestamp and activity values.")242 243    # ----------------------------244    # Connections (transitions) — respects min_edge (visual-only)245    # ----------------------------246    st.subheader("Connections (transitions)")247    edge_counts = build_edges(df_model)248    if edge_counts.empty:249        st.info("No transitions found after current filters.")250    else:251        st.dataframe(edge_counts[edge_counts["count"] >= min_edge], use_container_width=True)252 253    # ----------------------------254    # PM4Py visualizations (clean, frequency, performance, DFG)255    # ----------------------------256    st.subheader("Discovered Process Map")257    try:258        # Lazy imports so app still loads without pm4py259        from pm4py.objects.log.util import dataframe_utils260        from pm4py.objects.conversion.log import converter as log_converter261        from pm4py.algo.discovery.inductive import algorithm as inductive_miner262        from pm4py.visualization.petri_net import visualizer as pn_visualizer263        from pm4py.visualization.process_tree import visualizer as pt_visualizer264        from pm4py.objects.conversion.process_tree import converter as pt_converter265        from pm4py.objects.process_tree import obj as pt_obj266        from pm4py.algo.discovery.dfg import algorithm as dfg_discovery267        from pm4py.visualization.dfg import visualizer as dfg_visualization268 269        # Prepare dataframe for PM4Py270        pm_df = df_model.rename(columns={271            "case_id": "case:concept:name",272            "activity": "concept:name",273            "timestamp": "time:timestamp"274        }).copy()275        pm_df["time:timestamp"] = pd.to_datetime(pm_df["time:timestamp"], errors="coerce")276        pm_df = pm_df.dropna(subset=["time:timestamp"])277        pm_df = dataframe_utils.convert_timestamp_columns_in_df(pm_df)278 279        # Convert to event log280        event_log = log_converter.apply(pm_df)281 282        # Discover model283        model = inductive_miner.apply(event_log)284        if isinstance(model, pt_obj.ProcessTree):285            tree = model286            net, im, fm = pt_converter.apply(tree)287            tree_gviz = pt_visualizer.apply(tree)288        else:289            net, im, fm = model290            tree_gviz = None291 292        tabs = st.tabs(["Clean Petri Net", "Frequency", "Performance", "DFG (with numbers)"])293 294        # --- Clean Petri net ---295        with tabs[0]:296            gviz_pn = pn_visualizer.apply(net, im, fm)297            st.graphviz_chart(gviz_pn.source, use_container_width=True)298            if tree_gviz is not None:299                st.caption("Process Tree (discovered)")300                st.graphviz_chart(tree_gviz.source, use_container_width=True)301 302        # --- Frequency-decorated Petri net ---303        with tabs[1]:304            try:305                gviz_freq = pn_visualizer.apply(306                    net, im, fm,307                    variant=pn_visualizer.Variants.FREQUENCY,308                    log=event_log309                )310                st.graphviz_chart(gviz_freq.source, use_container_width=True)311                st.caption("Numbers reflect frequencies from the filtered log.")312            except Exception as e:313                st.info(f"Frequency decoration not available: {e}")314 315        # --- Performance-decorated Petri net ---316        with tabs[2]:317            try:318                gviz_perf = pn_visualizer.apply(319                    net, im, fm,320                    variant=pn_visualizer.Variants.PERFORMANCE,321                    log=event_log322                )323                st.graphviz_chart(gviz_perf.source, use_container_width=True)324                st.caption("Numbers reflect performance (e.g., average durations) computed from timestamps.")325            except Exception as e:326                st.info(f"Performance decoration not available: {e}")327 328        # --- DFG with numbers (respects min_edge visually) ---329        with tabs[3]:330            try:331                dfg_freq = dfg_discovery.apply(event_log)  # {(a,b): count}332                dfg_freq_filtered = {k: v for k, v in dfg_freq.items() if v >= min_edge}333                dfg_freq_gviz = dfg_visualization.apply(334                    dfg_freq_filtered if dfg_freq_filtered else dfg_freq,335                    log=event_log,336                    variant=dfg_visualization.Variants.FREQUENCY337                )338                st.graphviz_chart(dfg_freq_gviz.source, use_container_width=True)339                st.caption("DFG (Frequency): edge labels show counts. Low-frequency edges hidden per slider.")340 341                dfg_perf_gviz = dfg_visualization.apply(342                    dfg_freq_filtered if dfg_freq_filtered else dfg_freq,343                    log=event_log,344                    variant=dfg_visualization.Variants.PERFORMANCE345                )346                st.graphviz_chart(dfg_perf_gviz.source, use_container_width=True)347                st.caption("DFG (Performance): edge labels show avg durations. Low-frequency edges hidden per slider.")348            except Exception as e:349                st.info(f"DFG visualization not available: {e}")350 351    except ModuleNotFoundError:352        st.error("PM4Py not found. Please ensure pm4py and graphviz are installed.")353    except Exception as e:354        st.warning(f"Could not render process map: {e}")355 356    # ----------------------------357    # Credits358    # ----------------------------359    st.markdown("---")360    with st.expander("Credits", expanded=False):361        st.markdown(362            """363**Credits**  364Created by **Dennis Arrindell** — creator of the best selling online course about Process Mining on Udemy.365 366100% Vibe coded using ChatGPT367 368Inspired by the pioneering work of **Wil van der Aalst**, the “godfather of process mining.”  369 370Powered by the **PM4Py** process mining library, created by **Sebastiaan J. van Zelst** and contributors: https://pm4py.fit.fraunhofer.de/371 372Built with Python and other open-source libraries (pandas, Streamlit, Graphviz, etc.).  373 374Full technical information, installation steps, and source code available in the **GitHub repository**.375            """376        )377