<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://lms.onnocenter.or.id/wiki/index.php?action=history&amp;feed=atom&amp;title=PM%3A_Bottleneck_Detection</id>
	<title>PM: Bottleneck Detection - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://lms.onnocenter.or.id/wiki/index.php?action=history&amp;feed=atom&amp;title=PM%3A_Bottleneck_Detection"/>
	<link rel="alternate" type="text/html" href="https://lms.onnocenter.or.id/wiki/index.php?title=PM:_Bottleneck_Detection&amp;action=history"/>
	<updated>2026-04-20T18:16:22Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.45.1</generator>
	<entry>
		<id>https://lms.onnocenter.or.id/wiki/index.php?title=PM:_Bottleneck_Detection&amp;diff=72788&amp;oldid=prev</id>
		<title>Unknown user at 08:52, 13 September 2025</title>
		<link rel="alternate" type="text/html" href="https://lms.onnocenter.or.id/wiki/index.php?title=PM:_Bottleneck_Detection&amp;diff=72788&amp;oldid=prev"/>
		<updated>2025-09-13T08:52:07Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;a href=&quot;https://lms.onnocenter.or.id/wiki/index.php?title=PM:_Bottleneck_Detection&amp;amp;diff=72788&amp;amp;oldid=72781&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>Unknown user</name></author>
	</entry>
	<entry>
		<id>https://lms.onnocenter.or.id/wiki/index.php?title=PM:_Bottleneck_Detection&amp;diff=72781&amp;oldid=prev</id>
		<title>Unknown user: Created page with &quot;Mantap. Aku siapkan **script siap-pakai** untuk mendeteksi bottleneck dari **file XES terlampir** (`/mnt/data/BPIC_2012_A.xes`) memakai **PM4Py**. Script menghitung:  * **Serv...&quot;</title>
		<link rel="alternate" type="text/html" href="https://lms.onnocenter.or.id/wiki/index.php?title=PM:_Bottleneck_Detection&amp;diff=72781&amp;oldid=prev"/>
		<updated>2025-09-13T00:00:01Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;Mantap. Aku siapkan **script siap-pakai** untuk mendeteksi bottleneck dari **file XES terlampir** (`/mnt/data/BPIC_2012_A.xes`) memakai **PM4Py**. Script menghitung:  * **Serv...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Mantap. Aku siapkan **script siap-pakai** untuk mendeteksi bottleneck dari **file XES terlampir** (`/mnt/data/BPIC_2012_A.xes`) memakai **PM4Py**.&lt;br /&gt;
Script menghitung:&lt;br /&gt;
&lt;br /&gt;
* **Service time** per aktivitas dari pasangan `lifecycle:start`–`complete`&lt;br /&gt;
* **Waiting time** antar-aktivitas di dalam tiap case&lt;br /&gt;
* Ringkasan metrik (mean/median/p95/count) + **bottleneck score** (gabungan z-score service &amp;amp; waiting)&lt;br /&gt;
* Ekspor ke **CSV** dan tampilkan **TOP-N bottleneck**&lt;br /&gt;
&lt;br /&gt;
&amp;gt; **Install (sekali saja)**&lt;br /&gt;
&amp;gt;&lt;br /&gt;
&amp;gt; ```bash&lt;br /&gt;
&amp;gt; pip install pm4py pandas numpy&lt;br /&gt;
&amp;gt; ```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 1) Script utama — `bottleneck_pm4py.py`&lt;br /&gt;
&lt;br /&gt;
```python&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&lt;br /&gt;
import argparse&lt;br /&gt;
import sys&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
import numpy as np&lt;br /&gt;
import pandas as pd&lt;br /&gt;
&lt;br /&gt;
from pm4py.objects.log.importer.xes import importer as xes_importer&lt;br /&gt;
from pm4py import convert_to_dataframe&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def pair_start_complete(df: pd.DataFrame) -&amp;gt; pd.DataFrame:&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Pasangkan start/complete per (case, activity) -&amp;gt; satu baris per eksekusi aktivitas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if &amp;quot;lifecycle:transition&amp;quot; not in df.columns:&lt;br /&gt;
        return pd.DataFrame(columns=[&amp;quot;case&amp;quot;, &amp;quot;activity&amp;quot;, &amp;quot;start_time&amp;quot;, &amp;quot;complete_time&amp;quot;, &amp;quot;service_sec&amp;quot;])&lt;br /&gt;
&lt;br /&gt;
    dfl = df.dropna(subset=[&amp;quot;lifecycle:transition&amp;quot;]).copy()&lt;br /&gt;
    dfl[&amp;quot;transition&amp;quot;] = dfl[&amp;quot;lifecycle:transition&amp;quot;].str.lower()&lt;br /&gt;
    dfl = dfl[dfl[&amp;quot;transition&amp;quot;].isin([&amp;quot;start&amp;quot;, &amp;quot;complete&amp;quot;])]&lt;br /&gt;
    if dfl.empty:&lt;br /&gt;
        return pd.DataFrame(columns=[&amp;quot;case&amp;quot;, &amp;quot;activity&amp;quot;, &amp;quot;start_time&amp;quot;, &amp;quot;complete_time&amp;quot;, &amp;quot;service_sec&amp;quot;])&lt;br /&gt;
&lt;br /&gt;
    dfl = dfl.sort_values([&amp;quot;case:concept:name&amp;quot;, &amp;quot;concept:name&amp;quot;, &amp;quot;time:timestamp&amp;quot;, &amp;quot;transition&amp;quot;]).copy()&lt;br /&gt;
    dfl[&amp;quot;start_rank&amp;quot;] = dfl[&amp;quot;transition&amp;quot;].eq(&amp;quot;start&amp;quot;).groupby(&lt;br /&gt;
        [dfl[&amp;quot;case:concept:name&amp;quot;], dfl[&amp;quot;concept:name&amp;quot;]]&lt;br /&gt;
    ).cumsum()&lt;br /&gt;
    dfl[&amp;quot;complete_rank&amp;quot;] = dfl[&amp;quot;transition&amp;quot;].eq(&amp;quot;complete&amp;quot;).groupby(&lt;br /&gt;
        [dfl[&amp;quot;case:concept:name&amp;quot;], dfl[&amp;quot;concept:name&amp;quot;]]&lt;br /&gt;
    ).cumsum()&lt;br /&gt;
&lt;br /&gt;
    starts = dfl[dfl[&amp;quot;transition&amp;quot;] == &amp;quot;start&amp;quot;].rename(columns={&amp;quot;time:timestamp&amp;quot;: &amp;quot;start_time&amp;quot;})&lt;br /&gt;
    comps  = dfl[dfl[&amp;quot;transition&amp;quot;] == &amp;quot;complete&amp;quot;].rename(columns={&amp;quot;time:timestamp&amp;quot;: &amp;quot;complete_time&amp;quot;})&lt;br /&gt;
&lt;br /&gt;
    merged = pd.merge(&lt;br /&gt;
        starts[[&amp;quot;case:concept:name&amp;quot;, &amp;quot;concept:name&amp;quot;, &amp;quot;start_rank&amp;quot;, &amp;quot;start_time&amp;quot;]],&lt;br /&gt;
        comps[[&amp;quot;case:concept:name&amp;quot;, &amp;quot;concept:name&amp;quot;, &amp;quot;complete_rank&amp;quot;, &amp;quot;complete_time&amp;quot;]],&lt;br /&gt;
        left_on=[&amp;quot;case:concept:name&amp;quot;, &amp;quot;concept:name&amp;quot;, &amp;quot;start_rank&amp;quot;],&lt;br /&gt;
        right_on=[&amp;quot;case:concept:name&amp;quot;, &amp;quot;concept:name&amp;quot;, &amp;quot;complete_rank&amp;quot;],&lt;br /&gt;
        how=&amp;quot;inner&amp;quot;,&lt;br /&gt;
    ).rename(columns={&amp;quot;case:concept:name&amp;quot;: &amp;quot;case&amp;quot;, &amp;quot;concept:name&amp;quot;: &amp;quot;activity&amp;quot;})&lt;br /&gt;
&lt;br /&gt;
    merged[&amp;quot;service_sec&amp;quot;] = (merged[&amp;quot;complete_time&amp;quot;] - merged[&amp;quot;start_time&amp;quot;]).dt.total_seconds()&lt;br /&gt;
    merged = merged[(merged[&amp;quot;service_sec&amp;quot;] &amp;gt;= 0) &amp;amp; np.isfinite(merged[&amp;quot;service_sec&amp;quot;])]&lt;br /&gt;
    return merged[[&amp;quot;case&amp;quot;, &amp;quot;activity&amp;quot;, &amp;quot;start_time&amp;quot;, &amp;quot;complete_time&amp;quot;, &amp;quot;service_sec&amp;quot;]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def compute_waiting_times(df: pd.DataFrame, exec_df: pd.DataFrame) -&amp;gt; pd.DataFrame:&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Hitung waiting time antar aktivitas di tiap case.&lt;br /&gt;
    - Jika ada start/complete: tunggu = start(curr) - complete(prev)&lt;br /&gt;
    - Jika tidak ada lifecycle: tunggu = time(curr) - time(prev)&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    has_lifecycle = &amp;quot;lifecycle:transition&amp;quot; in df.columns and \&lt;br /&gt;
        df[&amp;quot;lifecycle:transition&amp;quot;].str.lower().isin([&amp;quot;start&amp;quot;, &amp;quot;complete&amp;quot;]).any()&lt;br /&gt;
&lt;br /&gt;
    rows = []&lt;br /&gt;
    if has_lifecycle and not exec_df.empty:&lt;br /&gt;
        per_case = exec_df.sort_values([&amp;quot;case&amp;quot;, &amp;quot;start_time&amp;quot;])&lt;br /&gt;
        for case, g in per_case.groupby(&amp;quot;case&amp;quot;):&lt;br /&gt;
            g = g.sort_values(&amp;quot;start_time&amp;quot;)&lt;br /&gt;
            prev_complete, prev_act = None, None&lt;br /&gt;
            for _, r in g.iterrows():&lt;br /&gt;
                if prev_complete is not None:&lt;br /&gt;
                    wt = (r[&amp;quot;start_time&amp;quot;] - prev_complete).total_seconds()&lt;br /&gt;
                    if wt &amp;gt;= 0:&lt;br /&gt;
                        rows.append({&amp;quot;case&amp;quot;: case, &amp;quot;from_activity&amp;quot;: prev_act,&lt;br /&gt;
                                     &amp;quot;to_activity&amp;quot;: r[&amp;quot;activity&amp;quot;], &amp;quot;waiting_sec&amp;quot;: wt})&lt;br /&gt;
                prev_complete, prev_act = r[&amp;quot;complete_time&amp;quot;], r[&amp;quot;activity&amp;quot;]&lt;br /&gt;
    else:&lt;br /&gt;
        # fallback tanpa lifecycle&lt;br /&gt;
        df2 = df.sort_values([&amp;quot;case:concept:name&amp;quot;, &amp;quot;time:timestamp&amp;quot;])&lt;br /&gt;
        for case, g in df2.groupby(&amp;quot;case:concept:name&amp;quot;):&lt;br /&gt;
            g = g.sort_values(&amp;quot;time:timestamp&amp;quot;)&lt;br /&gt;
            prev_time, prev_act = None, None&lt;br /&gt;
            for _, r in g.iterrows():&lt;br /&gt;
                if prev_time is not None:&lt;br /&gt;
                    wt = (r[&amp;quot;time:timestamp&amp;quot;] - prev_time).total_seconds()&lt;br /&gt;
                    if wt &amp;gt;= 0:&lt;br /&gt;
                        rows.append({&amp;quot;case&amp;quot;: case, &amp;quot;from_activity&amp;quot;: prev_act,&lt;br /&gt;
                                     &amp;quot;to_activity&amp;quot;: r[&amp;quot;concept:name&amp;quot;], &amp;quot;waiting_sec&amp;quot;: wt})&lt;br /&gt;
                prev_time, prev_act = r[&amp;quot;time:timestamp&amp;quot;], r[&amp;quot;concept:name&amp;quot;]&lt;br /&gt;
&lt;br /&gt;
    if not rows:&lt;br /&gt;
        return pd.DataFrame(columns=[&amp;quot;case&amp;quot;, &amp;quot;from_activity&amp;quot;, &amp;quot;to_activity&amp;quot;, &amp;quot;waiting_sec&amp;quot;])&lt;br /&gt;
    w = pd.DataFrame(rows)&lt;br /&gt;
    return w[[&amp;quot;case&amp;quot;, &amp;quot;from_activity&amp;quot;, &amp;quot;to_activity&amp;quot;, &amp;quot;waiting_sec&amp;quot;]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def zscore(series: pd.Series) -&amp;gt; pd.Series:&lt;br /&gt;
    mu = np.nanmean(series)&lt;br /&gt;
    sd = np.nanstd(series, ddof=0)&lt;br /&gt;
    if sd == 0 or np.isnan(sd):&lt;br /&gt;
        return pd.Series(np.zeros(len(series)), index=series.index)&lt;br /&gt;
    return (series - mu) / sd&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    ap = argparse.ArgumentParser(description=&amp;quot;Bottleneck Detection from XES using PM4Py&amp;quot;)&lt;br /&gt;
    ap.add_argument(&amp;quot;xes_path&amp;quot;, type=str, help=&amp;quot;Path ke file .xes&amp;quot;)&lt;br /&gt;
    ap.add_argument(&amp;quot;--top&amp;quot;, type=int, default=10, help=&amp;quot;Top-N bottleneck yang ditampilkan (default 10)&amp;quot;)&lt;br /&gt;
    ap.add_argument(&amp;quot;--out&amp;quot;, type=str, default=&amp;quot;bottlenecks_summary.csv&amp;quot;, help=&amp;quot;Output CSV ringkasan&amp;quot;)&lt;br /&gt;
    args = ap.parse_args()&lt;br /&gt;
&lt;br /&gt;
    xes_path = Path(args.xes_path)&lt;br /&gt;
    if not xes_path.exists():&lt;br /&gt;
        print(f&amp;quot;[ERROR] File tidak ditemukan: {xes_path}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
    # 1) Load XES -&amp;gt; DataFrame&lt;br /&gt;
    log = xes_importer.apply(str(xes_path))&lt;br /&gt;
    df = convert_to_dataframe(log)&lt;br /&gt;
&lt;br /&gt;
    for c in [&amp;quot;case:concept:name&amp;quot;, &amp;quot;concept:name&amp;quot;, &amp;quot;time:timestamp&amp;quot;]:&lt;br /&gt;
        if c not in df.columns:&lt;br /&gt;
            print(f&amp;quot;[ERROR] Kolom wajib hilang di event log: {c}&amp;quot;, file=sys.stderr)&lt;br /&gt;
            sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
    df[&amp;quot;time:timestamp&amp;quot;] = pd.to_datetime(df[&amp;quot;time:timestamp&amp;quot;], errors=&amp;quot;coerce&amp;quot;)&lt;br /&gt;
    df = df.dropna(subset=[&amp;quot;time:timestamp&amp;quot;]).copy()&lt;br /&gt;
&lt;br /&gt;
    # 2) Service time per aktivitas&lt;br /&gt;
    exec_df = pair_start_complete(df)&lt;br /&gt;
&lt;br /&gt;
    # 3) Waiting time antar aktivitas&lt;br /&gt;
    wait_df = compute_waiting_times(df, exec_df)&lt;br /&gt;
&lt;br /&gt;
    # 4) Agregasi per aktivitas&lt;br /&gt;
    if not exec_df.empty:&lt;br /&gt;
        service_stats = exec_df.groupby(&amp;quot;activity&amp;quot;)[&amp;quot;service_sec&amp;quot;].agg(&lt;br /&gt;
            service_mean_sec=&amp;quot;mean&amp;quot;,&lt;br /&gt;
            service_median_sec=&amp;quot;median&amp;quot;,&lt;br /&gt;
            service_p95_sec=lambda x: np.nanpercentile(x, 95),&lt;br /&gt;
            service_count=&amp;quot;count&amp;quot;,&lt;br /&gt;
        ).reset_index()&lt;br /&gt;
    else:&lt;br /&gt;
        service_stats = pd.DataFrame(columns=[&amp;quot;activity&amp;quot;,&amp;quot;service_mean_sec&amp;quot;,&amp;quot;service_median_sec&amp;quot;,&amp;quot;service_p95_sec&amp;quot;,&amp;quot;service_count&amp;quot;])&lt;br /&gt;
&lt;br /&gt;
    if not wait_df.empty:&lt;br /&gt;
        wait_stats = wait_df.groupby(&amp;quot;to_activity&amp;quot;)[&amp;quot;waiting_sec&amp;quot;].agg(&lt;br /&gt;
            wait_mean_sec=&amp;quot;mean&amp;quot;,&lt;br /&gt;
            wait_median_sec=&amp;quot;median&amp;quot;,&lt;br /&gt;
            wait_p95_sec=lambda x: np.nanpercentile(x, 95),&lt;br /&gt;
            wait_count=&amp;quot;count&amp;quot;,&lt;br /&gt;
        ).reset_index().rename(columns={&amp;quot;to_activity&amp;quot;: &amp;quot;activity&amp;quot;})&lt;br /&gt;
    else:&lt;br /&gt;
        wait_stats = pd.DataFrame(columns=[&amp;quot;activity&amp;quot;,&amp;quot;wait_mean_sec&amp;quot;,&amp;quot;wait_median_sec&amp;quot;,&amp;quot;wait_p95_sec&amp;quot;,&amp;quot;wait_count&amp;quot;])&lt;br /&gt;
&lt;br /&gt;
    summary = pd.merge(service_stats, wait_stats, on=&amp;quot;activity&amp;quot;, how=&amp;quot;outer&amp;quot;).fillna(0)&lt;br /&gt;
&lt;br /&gt;
    # 5) Skor bottleneck (gabungan z-score)&lt;br /&gt;
    summary[&amp;quot;z_service&amp;quot;]      = zscore(summary[&amp;quot;service_mean_sec&amp;quot;])&lt;br /&gt;
    summary[&amp;quot;z_wait&amp;quot;]         = zscore(summary[&amp;quot;wait_mean_sec&amp;quot;])&lt;br /&gt;
    summary[&amp;quot;z_service_p95&amp;quot;]  = zscore(summary[&amp;quot;service_p95_sec&amp;quot;])&lt;br /&gt;
    summary[&amp;quot;z_wait_p95&amp;quot;]     = zscore(summary[&amp;quot;wait_p95_sec&amp;quot;])&lt;br /&gt;
&lt;br /&gt;
    summary[&amp;quot;bottleneck_score&amp;quot;] = (&lt;br /&gt;
        0.4 * summary[&amp;quot;z_service&amp;quot;] +&lt;br /&gt;
        0.4 * summary[&amp;quot;z_wait&amp;quot;] +&lt;br /&gt;
        0.1 * summary[&amp;quot;z_service_p95&amp;quot;] +&lt;br /&gt;
        0.1 * summary[&amp;quot;z_wait_p95&amp;quot;]&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    summary_sorted = summary.sort_values(&amp;quot;bottleneck_score&amp;quot;, ascending=False)&lt;br /&gt;
    summary_sorted.to_csv(args.out, index=False)&lt;br /&gt;
&lt;br /&gt;
    # 6) Tampilkan Top-N ringkas&lt;br /&gt;
    n = min(args.top, len(summary_sorted))&lt;br /&gt;
    print(f&amp;quot;\n=== TOP {n} BOTTLENECK ACTIVITIES ===&amp;quot;)&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        print(&amp;quot;Tidak ada aktivitas terdeteksi. Cek struktur log Anda.&amp;quot;)&lt;br /&gt;
        sys.exit(0)&lt;br /&gt;
&lt;br /&gt;
    for _, r in summary_sorted.head(n).iterrows():&lt;br /&gt;
        print(&lt;br /&gt;
            f&amp;quot;- {r[&amp;#039;activity&amp;#039;]}: score={r[&amp;#039;bottleneck_score&amp;#039;]:.3f} | &amp;quot;&lt;br /&gt;
            f&amp;quot;svc_mean={r[&amp;#039;service_mean_sec&amp;#039;]:.1f}s (p95={r[&amp;#039;service_p95_sec&amp;#039;]:.1f}s, n={int(r[&amp;#039;service_count&amp;#039;])}) | &amp;quot;&lt;br /&gt;
            f&amp;quot;wait_mean={r[&amp;#039;wait_mean_sec&amp;#039;]:.1f}s (p95={r[&amp;#039;wait_p95_sec&amp;#039;]:.1f}s, n={int(r[&amp;#039;wait_count&amp;#039;])})&amp;quot;&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
    print(f&amp;quot;\nCSV ringkasan disimpan ke: {args.out}&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Prioritaskan aktivitas dengan score &amp;amp; p95 tinggi, terutama jika wait_mean besar (antrian).&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
**Jalankan ke file terlampir** (path sesuai unggahan Anda):&lt;br /&gt;
&lt;br /&gt;
```bash&lt;br /&gt;
python bottleneck_pm4py.py /mnt/data/BPIC_2012_A.xes --top 15 --out bottlenecks_BPICA.csv&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 2) (Opsional) Cross-check cepat pakai **Performance DFG** PM4Py&lt;br /&gt;
&lt;br /&gt;
Ini alternatif ringkas untuk melihat **mean waiting time** antar-aktivitas (edge) dan **sojourn time** (di node). Cocok untuk sanity check bottleneck transisi.&lt;br /&gt;
&lt;br /&gt;
```python&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
# perf_dfg_quickcheck.py&lt;br /&gt;
from pm4py.objects.log.importer.xes import importer as xes_importer&lt;br /&gt;
from pm4py.algo.discovery.dfg import algorithm as dfg_discovery&lt;br /&gt;
from pm4py.statistics.sojourn_time.log import get as soj_get&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
xes = sys.argv[1]&lt;br /&gt;
log = xes_importer.apply(xes)&lt;br /&gt;
&lt;br /&gt;
# Mean performance DFG (edge durations)&lt;br /&gt;
perf_dfg = dfg_discovery.apply(log, variant=dfg_discovery.Variants.PERFORMANCE)&lt;br /&gt;
df_edges = pd.DataFrame(&lt;br /&gt;
    [{&amp;quot;from&amp;quot;: a, &amp;quot;to&amp;quot;: b, &amp;quot;mean_sec&amp;quot;: v} for (a, b), v in perf_dfg.items()]&lt;br /&gt;
).sort_values(&amp;quot;mean_sec&amp;quot;, ascending=False)&lt;br /&gt;
&lt;br /&gt;
# Sojourn time per activity (durasi berada di node)&lt;br /&gt;
soj = soj_get.apply(log)  # returns dict {activity: mean_seconds}&lt;br /&gt;
df_nodes = pd.DataFrame(&lt;br /&gt;
    [{&amp;quot;activity&amp;quot;: k, &amp;quot;sojourn_mean_sec&amp;quot;: v} for k, v in soj.items()]&lt;br /&gt;
).sort_values(&amp;quot;sojourn_mean_sec&amp;quot;, ascending=False)&lt;br /&gt;
&lt;br /&gt;
df_edges.to_csv(&amp;quot;perf_dfg_edges.csv&amp;quot;, index=False)&lt;br /&gt;
df_nodes.to_csv(&amp;quot;sojourn_nodes.csv&amp;quot;, index=False)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Top 10 edges by mean_sec:&amp;quot;)&lt;br /&gt;
print(df_edges.head(10))&lt;br /&gt;
print(&amp;quot;\nTop 10 activities by sojourn_mean_sec:&amp;quot;)&lt;br /&gt;
print(df_nodes.head(10))&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
**Jalankan:**&lt;br /&gt;
&lt;br /&gt;
```bash&lt;br /&gt;
python perf_dfg_quickcheck.py /mnt/data/BPIC_2012_A.xes&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### Catatan penting&lt;br /&gt;
&lt;br /&gt;
* **Akurasi service time** bergantung pada hadirnya pasangan `lifecycle:start/complete`. Jika dataset hanya punya `complete`, fokuskan interpretasi pada **waiting antar event** (edge) dan **sojourn** (node).&lt;br /&gt;
* **p95** membantu mengungkap **ekor panjang** (spikes jarang tapi berat) yang sering jadi bottleneck meski mean tidak terlalu tinggi.&lt;br /&gt;
* Untuk investigasi mendalam, gabungkan hasil `bottlenecks_summary.csv` dengan **variasi per resource**, **per channel**, atau **per case attribute** (mis. `org:resource`, `org:role`, `application type`, dll.) lalu lakukan **groupby** tambahan.&lt;br /&gt;
&lt;br /&gt;
Kalau mau, saya bisa lanjutkan dengan **visualisasi** (bar chart Top-N) atau **filter per resource**—tinggal sebutkan atribut yang ingin dianalisis.&lt;/div&gt;</summary>
		<author><name>Unknown user</name></author>
	</entry>
</feed>