hprmn commited on
Commit
f58a96b
·
1 Parent(s): 14f0b4c
Files changed (2) hide show
  1. .python-version +1 -0
  2. src/streamlit_app.py +1069 -61
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12.4-peta
src/streamlit_app.py CHANGED
@@ -447,7 +447,17 @@ def analyze_network_connectivity(G, line_segments=None):
447
  return analysis
448
 
449
 
450
- def create_network_visualization(G, nodes, centrality_measures, show_labels=False):
 
 
 
 
 
 
 
 
 
 
451
  """Buat visualisasi jaringan menggunakan Plotly dengan error handling"""
452
  try:
453
  if G.number_of_nodes() == 0:
@@ -484,30 +494,134 @@ def create_network_visualization(G, nodes, centrality_measures, show_labels=Fals
484
  # Fallback ke spring layout jika tidak ada koordinat
485
  pos = nx.spring_layout(G, k=1, iterations=50)
486
 
487
- # Siapkan data untuk edges
488
- edge_x = []
489
- edge_y = []
490
- edge_info = []
491
 
 
 
 
 
 
 
 
492
  for edge in G.edges(data=True):
493
- if edge[0] in pos and edge[1] in pos:
494
- x0, y0 = pos[edge[0]]
495
- x1, y1 = pos[edge[1]]
496
- edge_x.extend([x0, x1, None])
497
- edge_y.extend([y0, y1, None])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
  weight = edge[2].get("weight", 0)
500
- edge_info.append(f"Weight: {weight:.2f}m")
501
-
502
- # Trace untuk edges dengan styling yang lebih baik
503
- edge_trace = go.Scatter(
504
- x=edge_x,
505
- y=edge_y,
506
- line=dict(width=0.8, color="rgba(125,125,125,0.8)"),
507
- hoverinfo="none",
508
- mode="lines",
509
- name="Saluran Listrik",
510
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
 
512
  # Siapkan data untuk nodes
513
  node_x = []
@@ -515,6 +629,7 @@ def create_network_visualization(G, nodes, centrality_measures, show_labels=Fals
515
  node_text = []
516
  node_color = []
517
  node_size = []
 
518
 
519
  # Gunakan degree centrality untuk pewarnaan dan ukuran
520
  degree_cent = centrality_measures.get("degree", {})
@@ -524,14 +639,51 @@ def create_network_visualization(G, nodes, centrality_measures, show_labels=Fals
524
  x, y = pos[node]
525
  node_x.append(x)
526
  node_y.append(y)
 
527
 
528
- # Informasi node
529
  adjacencies = list(G.neighbors(node))
530
- node_info = f"Node: {node}<br>"
531
- node_info += f"Koneksi: {len(adjacencies)}<br>"
532
- node_info += f"Degree Centrality: {degree_cent.get(node, 0):.3f}<br>"
533
- node_info += f'Betweenness: {centrality_measures.get("betweenness", {}).get(node, 0):.3f}<br>'
534
- node_info += f'Closeness: {centrality_measures.get("closeness", {}).get(node, 0):.3f}'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
  node_text.append(node_info)
537
  node_color.append(degree_cent.get(node, 0))
@@ -541,15 +693,17 @@ def create_network_visualization(G, nodes, centrality_measures, show_labels=Fals
541
  size_multiplier = 20
542
  node_size.append(base_size + degree_cent.get(node, 0) * size_multiplier)
543
 
 
 
544
  # Trace untuk nodes dengan styling yang lebih menarik
545
  node_trace = go.Scatter(
546
  x=node_x,
547
  y=node_y,
548
  mode="markers+text" if show_labels else "markers",
549
  hoverinfo="text",
550
- text=[str(i) for i in range(len(node_x))] if show_labels else [],
551
  textposition="middle center",
552
- textfont=dict(size=8, color="white"),
553
  hovertext=node_text,
554
  marker=dict(
555
  showscale=True,
@@ -570,9 +724,11 @@ def create_network_visualization(G, nodes, centrality_measures, show_labels=Fals
570
  name="Node/Junction",
571
  )
572
 
573
- # Buat figure
 
 
574
  fig = go.Figure(
575
- data=[edge_trace, node_trace],
576
  layout=go.Layout(
577
  title=dict(
578
  text="Visualisasi Graf Jaringan Listrik DIY",
@@ -584,7 +740,7 @@ def create_network_visualization(G, nodes, centrality_measures, show_labels=Fals
584
  margin=dict(b=40, l=40, r=60, t=80),
585
  annotations=[
586
  dict(
587
- text="Node berukuran dan berwarna berdasarkan Degree Centrality.<br>Node yang lebih besar dan gelap = lebih penting dalam jaringan",
588
  showarrow=False,
589
  xref="paper",
590
  yref="paper",
@@ -680,6 +836,139 @@ def create_centrality_comparison(centrality_measures):
680
  return go.Figure()
681
 
682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  def create_map_visualization(gdf_original):
684
  """Buat visualisasi peta menggunakan Folium dengan error handling"""
685
  try:
@@ -867,7 +1156,45 @@ def main():
867
  st.sidebar.success(f"✅ Data berhasil diunduh: {len(gdf)} features")
868
 
869
  # Konfigurasi visualisasi
870
- show_labels = st.sidebar.checkbox("Tampilkan Label Node", value=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
 
872
  # Add performance settings
873
  with st.sidebar.expander("⚙️ Pengaturan Performa"):
@@ -1093,15 +1420,237 @@ def main():
1093
  f"ℹ️ Menampilkan {max_nodes_viz} node dengan degree tertinggi dari total {G.number_of_nodes()} nodes"
1094
  )
1095
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
  # Visualisasi graf
1097
  try:
1098
  network_fig = create_network_visualization(
1099
- G_viz, st.session_state["nodes"], centrality_measures, show_labels
 
 
 
 
 
 
 
 
1100
  )
1101
  st.plotly_chart(network_fig, use_container_width=True)
1102
  except Exception as e:
1103
  st.error(f"Error creating network visualization: {str(e)}")
1104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1105
  # Informasi graf
1106
  st.markdown("### 🔍 Interpretasi Graf")
1107
  st.markdown(
@@ -1110,6 +1659,7 @@ def main():
1110
  - **Edge (Sisi)**: Merepresentasikan saluran listrik yang menghubungkan antar titik
1111
  - **Warna Node**: Intensitas warna menunjukkan tingkat kepentingan berdasarkan Degree Centrality
1112
  - **Node dengan warna lebih gelap**: Memiliki lebih banyak koneksi (lebih kritis)
 
1113
  """
1114
  )
1115
 
@@ -1126,6 +1676,166 @@ def main():
1126
  except Exception as e:
1127
  st.error(f"Error creating centrality comparison: {str(e)}")
1128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1129
  # Identifikasi node kritis
1130
  st.markdown("### 🎯 Identifikasi Node Kritis")
1131
 
@@ -1142,18 +1852,69 @@ def main():
1142
  for i, (node, centrality) in enumerate(top_nodes, 1):
1143
  st.write(f"{i}. Node {node}: {centrality:.4f}")
1144
 
1145
- # Rekomendasi
1146
- st.markdown("### 💡 Rekomendasi")
1147
- if top_nodes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1148
  st.markdown(
1149
  f"""
1150
  **Node Paling Kritis:** Node {top_nodes[0][0]} (Degree Centrality: {top_nodes[0][1]:.4f})
1151
-
1152
- **Rekomendasi Kebijakan:**
1153
- 1. **Prioritas Pemeliharaan**: Fokuskan pemeliharaan pada node dengan centrality tinggi
1154
- 2. **Redundansi**: Pertimbangkan jalur alternatif untuk node kritis
1155
- 3. **Monitoring**: Pasang sistem monitoring khusus pada node dengan degree centrality > 0.1
1156
- 4. **Investasi**: Alokasikan investasi infrastruktur pada area dengan connectivity rendah
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1157
  """
1158
  )
1159
 
@@ -1164,24 +1925,125 @@ def main():
1164
  )
1165
 
1166
  if mst.number_of_nodes() > 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
1167
  col1, col2 = st.columns(2)
1168
  with col1:
1169
- total_weight = sum(
1170
- [data["weight"] for _, _, data in mst.edges(data=True)]
1171
- )
1172
- st.metric("Total Bobot MST", f"{total_weight:.2f}m")
1173
- st.metric("Jumlah Edge", mst.number_of_edges())
1174
  with col2:
1175
- original_weight = sum(
1176
- [data["weight"] for _, _, data in G.edges(data=True)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1177
  )
1178
- efficiency = (
1179
- ((original_weight - total_weight) / original_weight * 100)
1180
- if original_weight > 0
1181
- else 0
 
 
 
 
1182
  )
1183
- st.metric("Efisiensi", f"{efficiency:.2f}%")
1184
- st.metric("Penghematan", f"{original_weight - total_weight:.2f}m")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1185
 
1186
  # Visualisasi MST
1187
  try:
@@ -1199,12 +2061,158 @@ def main():
1199
  except Exception as e:
1200
  st.error(f"Error creating MST visualization: {str(e)}")
1201
 
1202
- st.markdown("### 🔧 Interpretasi MST")
1203
- st.markdown(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1204
  """
1205
- - MST menunjukkan jaringan dengan biaya minimum yang tetap menghubungkan semua node
1206
- - Berguna untuk perencanaan infrastruktur baru atau optimasi jaringan existing
1207
- - Edge yang tidak termasuk dalam MST bisa dianggap sebagai redundansi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1208
  """
1209
  )
1210
  else:
 
447
  return analysis
448
 
449
 
450
+ def create_network_visualization(
451
+ G,
452
+ nodes,
453
+ centrality_measures,
454
+ show_labels=False,
455
+ show_edge_details=False,
456
+ label_size=10,
457
+ label_color="white",
458
+ edge_offset=0.02,
459
+ show_edge_colors=True,
460
+ ):
461
  """Buat visualisasi jaringan menggunakan Plotly dengan error handling"""
462
  try:
463
  if G.number_of_nodes() == 0:
 
494
  # Fallback ke spring layout jika tidak ada koordinat
495
  pos = nx.spring_layout(G, k=1, iterations=50)
496
 
497
+ # Siapkan data untuk edges dengan multiple edges terpisah
498
+ edge_traces = [] # List untuk menyimpan multiple traces
 
 
499
 
500
+ # Hitung statistik edge untuk normalisasi
501
+ edge_weights = [data.get("weight", 0) for _, _, data in G.edges(data=True)]
502
+ max_weight = max(edge_weights) if edge_weights else 1
503
+ min_weight = min(edge_weights) if edge_weights else 0
504
+
505
+ # Group edges berdasarkan pasangan node untuk mendeteksi multiple edges
506
+ edge_groups = {}
507
  for edge in G.edges(data=True):
508
+ node_pair = tuple(sorted([edge[0], edge[1]]))
509
+ if node_pair not in edge_groups:
510
+ edge_groups[node_pair] = []
511
+ edge_groups[node_pair].append(edge)
512
+
513
+ # Fungsi untuk membuat offset untuk multiple edges
514
+ def calculate_edge_offset(
515
+ x0, y0, x1, y1, offset_distance, edge_index, total_edges
516
+ ):
517
+ """Hitung offset untuk edge paralel"""
518
+ if total_edges == 1:
519
+ return x0, y0, x1, y1
520
+
521
+ # Hitung vektor perpendicular
522
+ dx = x1 - x0
523
+ dy = y1 - y0
524
+ length = (dx**2 + dy**2) ** 0.5
525
+
526
+ if length == 0:
527
+ return x0, y0, x1, y1
528
+
529
+ # Vektor unit perpendicular
530
+ perp_x = -dy / length
531
+ perp_y = dx / length
532
+
533
+ # Hitung offset untuk edge ini
534
+ if total_edges % 2 == 1:
535
+ # Odd number: center edge at 0, others at ±offset
536
+ center_index = total_edges // 2
537
+ offset = (edge_index - center_index) * offset_distance
538
+ else:
539
+ # Even number: no center edge
540
+ offset = (edge_index - (total_edges - 1) / 2) * offset_distance
541
+
542
+ # Apply offset
543
+ offset_x0 = x0 + perp_x * offset
544
+ offset_y0 = y0 + perp_y * offset
545
+ offset_x1 = x1 + perp_x * offset
546
+ offset_y1 = y1 + perp_y * offset
547
+
548
+ return offset_x0, offset_y0, offset_x1, offset_y1
549
+
550
+ # Proses setiap group edge
551
+ for node_pair, edges in edge_groups.items():
552
+ if edges[0][0] not in pos or edges[0][1] not in pos:
553
+ continue
554
+
555
+ x0, y0 = pos[edges[0][0]]
556
+ x1, y1 = pos[edges[0][1]]
557
+
558
+ total_edges = len(edges)
559
+ offset_distance = edge_offset # Gunakan parameter yang dapat diatur
560
+
561
+ for edge_index, edge in enumerate(edges):
562
+ # Hitung posisi dengan offset
563
+ offset_x0, offset_y0, offset_x1, offset_y1 = calculate_edge_offset(
564
+ x0, y0, x1, y1, offset_distance, edge_index, total_edges
565
+ )
566
 
567
  weight = edge[2].get("weight", 0)
568
+ line_name = edge[2].get("nama", f"Edge_{edge[0]}_{edge[1]}")
569
+ line_id = edge[2].get("line_id", f"ID_{edge[0]}_{edge[1]}")
570
+
571
+ # Info detail untuk hover
572
+ if show_edge_details:
573
+ edge_info = (
574
+ f"Edge: {edge[0]} ↔ {edge[1]}<br>"
575
+ f"Nama: {line_name}<br>"
576
+ f"ID: {line_id}<br>"
577
+ f"Panjang: {weight:.2f}m ({weight/1000:.3f}km)<br>"
578
+ f"Saluran {edge_index + 1} dari {total_edges}"
579
+ )
580
+ else:
581
+ edge_info = f"Edge: {edge[0]} ↔ {edge[1]}<br>Panjang: {weight:.2f}m"
582
+
583
+ # Warna berdasarkan jumlah edge paralel
584
+ if total_edges > 1 and show_edge_colors:
585
+ # Multiple edges: gunakan warna berbeda jika diaktifkan
586
+ colors = ["red", "blue", "green", "orange", "purple", "brown"]
587
+ color = colors[edge_index % len(colors)]
588
+ edge_color = color
589
+ edge_width = 2.0 # Lebih tebal untuk multiple edges
590
+ elif total_edges > 1:
591
+ # Multiple edges tanpa warna berbeda
592
+ edge_color = "rgba(255,100,100,0.8)" # Merah muda untuk multiple
593
+ edge_width = 2.0
594
+ else:
595
+ # Single edge: warna berdasarkan panjang
596
+ if max_weight > min_weight:
597
+ normalized_weight = (weight - min_weight) / (
598
+ max_weight - min_weight
599
+ )
600
+ red_component = int(255 * (1 - normalized_weight))
601
+ blue_component = int(255 * normalized_weight)
602
+ edge_color = (
603
+ f"rgba({red_component}, 100, {blue_component}, 0.7)"
604
+ )
605
+ else:
606
+ edge_color = "rgba(125,125,125,0.8)"
607
+ edge_width = 1.2
608
+
609
+ # Buat trace untuk edge ini
610
+ edge_trace = go.Scatter(
611
+ x=[offset_x0, offset_x1, None],
612
+ y=[offset_y0, offset_y1, None],
613
+ line=dict(width=edge_width, color=edge_color),
614
+ hoverinfo="text" if show_edge_details else "none",
615
+ hovertext=edge_info if show_edge_details else None,
616
+ mode="lines",
617
+ name=(
618
+ f"Saluran {edge_index + 1}"
619
+ if total_edges > 1
620
+ else "Saluran Listrik"
621
+ ),
622
+ showlegend=False,
623
+ )
624
+ edge_traces.append(edge_trace)
625
 
626
  # Siapkan data untuk nodes
627
  node_x = []
 
629
  node_text = []
630
  node_color = []
631
  node_size = []
632
+ node_ids = [] # Pindahkan ke sini untuk sinkronisasi
633
 
634
  # Gunakan degree centrality untuk pewarnaan dan ukuran
635
  degree_cent = centrality_measures.get("degree", {})
 
639
  x, y = pos[node]
640
  node_x.append(x)
641
  node_y.append(y)
642
+ node_ids.append(str(node)) # Tambahkan ID node sesuai urutan
643
 
644
+ # Informasi node dengan detail koneksi
645
  adjacencies = list(G.neighbors(node))
646
+ node_degree = G.degree(node)
647
+
648
+ # Hitung total edge secara manual untuk verifikasi
649
+ total_edges_manual = 0
650
+ connection_details = []
651
+ for neighbor in adjacencies:
652
+ # Hitung berapa banyak edge antara node ini dan neighbor
653
+ edge_count = G.number_of_edges(node, neighbor)
654
+ total_edges_manual += edge_count
655
+ if edge_count > 1:
656
+ connection_details.append(
657
+ f"→ Node {neighbor} ({edge_count} saluran)"
658
+ )
659
+ else:
660
+ connection_details.append(f"→ Node {neighbor}")
661
+
662
+ node_info = f"🔵 Node: {node}<br>"
663
+ node_info += f"📊 Degree (NetworkX): {node_degree}<br>"
664
+ node_info += f"🔢 Total Edge Manual: {total_edges_manual}<br>"
665
+ node_info += f"👥 Tetangga: {len(adjacencies)}<br>"
666
+
667
+ # Tampilkan peringatan jika ada ketidaksesuaian
668
+ if node_degree != total_edges_manual:
669
+ node_info += f"⚠️ INCONSISTENCY DETECTED!<br>"
670
+
671
+ if show_edge_details and connection_details:
672
+ node_info += f"🔗 Detail Koneksi:<br>"
673
+ node_info += "<br>".join(
674
+ connection_details[:5]
675
+ ) # Batasi 5 koneksi pertama
676
+ if len(connection_details) > 5:
677
+ node_info += (
678
+ f"<br>... dan {len(connection_details) - 5} lainnya"
679
+ )
680
+ node_info += "<br><br>"
681
+
682
+ node_info += f"📈 Sentralitas:<br>"
683
+ node_info += f"• Degree: {degree_cent.get(node, 0):.4f}<br>"
684
+ node_info += f'• Betweenness: {centrality_measures.get("betweenness", {}).get(node, 0):.4f}<br>'
685
+ node_info += f'• Closeness: {centrality_measures.get("closeness", {}).get(node, 0):.4f}<br>'
686
+ node_info += f'• Eigenvector: {centrality_measures.get("eigenvector", {}).get(node, 0):.4f}'
687
 
688
  node_text.append(node_info)
689
  node_color.append(degree_cent.get(node, 0))
 
693
  size_multiplier = 20
694
  node_size.append(base_size + degree_cent.get(node, 0) * size_multiplier)
695
 
696
+ # node_ids sudah dibuat di loop sebelumnya, tidak perlu duplikasi
697
+
698
  # Trace untuk nodes dengan styling yang lebih menarik
699
  node_trace = go.Scatter(
700
  x=node_x,
701
  y=node_y,
702
  mode="markers+text" if show_labels else "markers",
703
  hoverinfo="text",
704
+ text=node_ids if show_labels else [],
705
  textposition="middle center",
706
+ textfont=dict(size=label_size, color=label_color, family="Arial Black"),
707
  hovertext=node_text,
708
  marker=dict(
709
  showscale=True,
 
724
  name="Node/Junction",
725
  )
726
 
727
+ # Buat figure dengan multiple edge traces
728
+ all_traces = edge_traces + [node_trace]
729
+
730
  fig = go.Figure(
731
+ data=all_traces,
732
  layout=go.Layout(
733
  title=dict(
734
  text="Visualisasi Graf Jaringan Listrik DIY",
 
740
  margin=dict(b=40, l=40, r=60, t=80),
741
  annotations=[
742
  dict(
743
+ text="Node berukuran dan berwarna berdasarkan Degree Centrality.<br>Saluran paralel ditampilkan dengan garis terpisah dan warna berbeda.<br>Node yang lebih besar dan gelap = lebih penting dalam jaringan",
744
  showarrow=False,
745
  xref="paper",
746
  yref="paper",
 
836
  return go.Figure()
837
 
838
 
839
+ def create_centrality_matrix(centrality_measures):
840
+ """Buat matriks sentralitas untuk semua node dengan error handling"""
841
+ try:
842
+ if not centrality_measures or not centrality_measures.get("degree"):
843
+ return pd.DataFrame()
844
+
845
+ # Ambil semua node
846
+ nodes = list(centrality_measures["degree"].keys())
847
+
848
+ # Buat DataFrame dengan semua ukuran sentralitas
849
+ centrality_data = {
850
+ "Node": nodes,
851
+ "Degree Centrality": [
852
+ centrality_measures["degree"].get(node, 0) for node in nodes
853
+ ],
854
+ "Closeness Centrality": [
855
+ centrality_measures["closeness"].get(node, 0) for node in nodes
856
+ ],
857
+ "Betweenness Centrality": [
858
+ centrality_measures["betweenness"].get(node, 0) for node in nodes
859
+ ],
860
+ "Eigenvector Centrality": [
861
+ centrality_measures["eigenvector"].get(node, 0) for node in nodes
862
+ ],
863
+ }
864
+
865
+ df = pd.DataFrame(centrality_data)
866
+
867
+ # Urutkan berdasarkan Degree Centrality (descending)
868
+ df = df.sort_values("Degree Centrality", ascending=False).reset_index(drop=True)
869
+
870
+ return df
871
+
872
+ except Exception as e:
873
+ st.error(f"Error creating centrality matrix: {str(e)}")
874
+ return pd.DataFrame()
875
+
876
+
877
+ def create_node_connection_details(G, top_n=20):
878
+ """Buat tabel detail koneksi untuk node-node teratas"""
879
+ try:
880
+ if G.number_of_nodes() == 0:
881
+ return pd.DataFrame()
882
+
883
+ # Ambil node dengan degree tertinggi
884
+ node_degrees = dict(G.degree())
885
+ top_nodes = sorted(node_degrees.items(), key=lambda x: x[1], reverse=True)[
886
+ :top_n
887
+ ]
888
+
889
+ connection_data = []
890
+
891
+ # Deteksi self-loops dalam graf
892
+ self_loops = list(nx.selfloop_edges(G))
893
+ has_self_loops = len(self_loops) > 0
894
+
895
+ for node, degree in top_nodes:
896
+ neighbors = list(G.neighbors(node))
897
+ actual_neighbors = [n for n in neighbors if n != node] # Exclude self-loop
898
+
899
+ # Hitung detail koneksi
900
+ connection_details = []
901
+ total_edges = 0
902
+
903
+ # Hitung edges ke tetangga sebenarnya
904
+ for neighbor in actual_neighbors:
905
+ edge_count = G.number_of_edges(node, neighbor)
906
+ total_edges += edge_count
907
+ if edge_count > 1:
908
+ connection_details.append(f"Node {neighbor} ({edge_count}x)")
909
+ else:
910
+ connection_details.append(f"Node {neighbor}")
911
+
912
+ # Tambahkan self-loop jika ada
913
+ if G.has_edge(node, node):
914
+ self_edge_count = G.number_of_edges(node, node)
915
+ total_edges += self_edge_count
916
+ connection_details.append(
917
+ f"Node {node} (SELF-LOOP: {self_edge_count}x)"
918
+ )
919
+
920
+ # Batasi tampilan koneksi
921
+ if len(connection_details) > 8:
922
+ display_connections = (
923
+ ", ".join(connection_details[:8])
924
+ + f", ... (+{len(connection_details)-8})"
925
+ )
926
+ else:
927
+ display_connections = ", ".join(connection_details)
928
+
929
+ # Bandingkan degree NetworkX dengan perhitungan manual
930
+ degree_nx = G.degree(node)
931
+
932
+ # Cek self-loop untuk node ini
933
+ has_self_loop = G.has_edge(node, node)
934
+ self_loop_count = (
935
+ 1 if has_self_loop else 0
936
+ ) # Self-loop dihitung 1x sesuai teori graf
937
+
938
+ # Total edges termasuk self-loop
939
+ total_edges_with_self = total_edges + self_loop_count
940
+ is_consistent = degree_nx == total_edges_with_self
941
+
942
+ # Status dengan informasi self-loop
943
+ if is_consistent:
944
+ status = "✅ OK" + (" (with self-loop)" if has_self_loop else "")
945
+ elif has_self_loop:
946
+ status = f"⚠️ SELF-LOOP (+{self_loop_count})"
947
+ else:
948
+ status = "⚠️ INCONSISTENT"
949
+
950
+ connection_data.append(
951
+ {
952
+ "Node": node,
953
+ "Degree (NetworkX)": degree_nx,
954
+ "Total Edges (Manual)": total_edges,
955
+ "Self-Loop": "Yes" if has_self_loop else "No",
956
+ "Jumlah Tetangga": len(actual_neighbors),
957
+ "Detail Koneksi": display_connections,
958
+ "Rasio Edge/Tetangga": (
959
+ f"{total_edges/len(neighbors):.2f}" if neighbors else "0"
960
+ ),
961
+ "Status": status,
962
+ }
963
+ )
964
+
965
+ return pd.DataFrame(connection_data)
966
+
967
+ except Exception as e:
968
+ st.error(f"Error creating connection details: {str(e)}")
969
+ return pd.DataFrame()
970
+
971
+
972
  def create_map_visualization(gdf_original):
973
  """Buat visualisasi peta menggunakan Folium dengan error handling"""
974
  try:
 
1156
  st.sidebar.success(f"✅ Data berhasil diunduh: {len(gdf)} features")
1157
 
1158
  # Konfigurasi visualisasi
1159
+ st.sidebar.markdown("### 🎨 Pengaturan Visualisasi")
1160
+ show_labels = st.sidebar.checkbox(
1161
+ "Tampilkan Label Node",
1162
+ value=True,
1163
+ help="Menampilkan ID node pada visualisasi graf",
1164
+ )
1165
+ show_edge_details = st.sidebar.checkbox(
1166
+ "Tampilkan Detail Edge",
1167
+ value=False,
1168
+ help="Menampilkan informasi detail tentang saluran listrik",
1169
+ )
1170
+
1171
+ # Pengaturan tampilan label
1172
+ with st.sidebar.expander("🏷️ Pengaturan Label Node"):
1173
+ label_size = st.slider(
1174
+ "Ukuran Label", 6, 16, 10, help="Ukuran font untuk label node"
1175
+ )
1176
+ label_color = st.selectbox(
1177
+ "Warna Label",
1178
+ ["white", "black", "red", "blue", "green"],
1179
+ index=0,
1180
+ help="Warna teks label node",
1181
+ )
1182
+
1183
+ # Pengaturan edge paralel
1184
+ with st.sidebar.expander("🔗 Pengaturan Saluran Paralel"):
1185
+ edge_offset = st.slider(
1186
+ "Jarak Antar Saluran Paralel",
1187
+ 0.01,
1188
+ 0.05,
1189
+ 0.02,
1190
+ 0.005,
1191
+ help="Mengatur jarak visual antar saluran paralel",
1192
+ )
1193
+ show_edge_colors = st.checkbox(
1194
+ "Warna Berbeda untuk Saluran Paralel",
1195
+ value=True,
1196
+ help="Memberikan warna berbeda untuk setiap saluran paralel",
1197
+ )
1198
 
1199
  # Add performance settings
1200
  with st.sidebar.expander("⚙️ Pengaturan Performa"):
 
1420
  f"ℹ️ Menampilkan {max_nodes_viz} node dengan degree tertinggi dari total {G.number_of_nodes()} nodes"
1421
  )
1422
 
1423
+ # Kontrol visualisasi tambahan
1424
+ col1, col2, col3 = st.columns(3)
1425
+ with col1:
1426
+ if show_edge_details:
1427
+ st.info(
1428
+ "ℹ️ Mode Detail Edge: Hover pada garis untuk melihat detail saluran"
1429
+ )
1430
+ with col2:
1431
+ if show_labels:
1432
+ st.info("ℹ️ Mode Label: ID node ditampilkan pada graf")
1433
+ with col3:
1434
+ # Fitur pencarian node
1435
+ search_node = st.text_input(
1436
+ "🔍 Cari Node:",
1437
+ placeholder="Masukkan ID node (contoh: 13, 83, 154)",
1438
+ help="Masukkan ID node untuk mencari informasi detail",
1439
+ )
1440
+
1441
+ if search_node:
1442
+ try:
1443
+ node_id = int(search_node)
1444
+ if node_id in G.nodes():
1445
+ neighbors = list(G.neighbors(node_id))
1446
+ degree_nx = G.degree(node_id)
1447
+
1448
+ # Hitung manual untuk debugging (exclude self-loop dari neighbors)
1449
+ total_edges_manual = 0
1450
+ edge_details = []
1451
+ actual_neighbors = [
1452
+ n for n in neighbors if n != node_id
1453
+ ] # Exclude self
1454
+
1455
+ for neighbor in actual_neighbors:
1456
+ edge_count = G.number_of_edges(node_id, neighbor)
1457
+ total_edges_manual += edge_count
1458
+ edge_details.append(f"→ {neighbor} ({edge_count} edge)")
1459
+
1460
+ # Tambahkan self-loop secara terpisah jika ada
1461
+ if G.has_edge(node_id, node_id):
1462
+ self_edge_count = G.number_of_edges(node_id, node_id)
1463
+ edge_details.append(
1464
+ f"→ {node_id} (SELF-LOOP: {self_edge_count} edge)"
1465
+ )
1466
+ total_edges_manual += self_edge_count
1467
+
1468
+ st.success(f"✅ Node {node_id} ditemukan!")
1469
+ st.write(f"• **Degree (NetworkX)**: {degree_nx}")
1470
+ st.write(
1471
+ f"• **Total Edges (Manual)**: {total_edges_manual}"
1472
+ )
1473
+ st.write(
1474
+ f"• **Jumlah Tetangga Sebenarnya**: {len(actual_neighbors)}"
1475
+ )
1476
+ st.write(
1477
+ f"• **Neighbors dari NetworkX**: {len(neighbors)} (mungkin termasuk self)"
1478
+ )
1479
+
1480
+ # Debugging mendalam untuk edge
1481
+ has_self_loop_search = G.has_edge(node_id, node_id)
1482
+ self_loop_adjustment = 1 if has_self_loop_search else 0
1483
+ expected_degree = total_edges_manual
1484
+
1485
+ # Debug: Lihat semua edge yang terhubung ke node ini
1486
+ st.write("**🔍 Debug - Semua Edge yang Terhubung:**")
1487
+ all_edges = []
1488
+
1489
+ # Metode 1: Dari G.edges()
1490
+ for edge in G.edges(node_id, data=True):
1491
+ all_edges.append(
1492
+ f"Edge: {edge[0]} → {edge[1]} (data: {edge[2]})"
1493
+ )
1494
+
1495
+ # Metode 2: Cek degree calculation NetworkX
1496
+ degree_dict = dict(G.degree([node_id]))
1497
+ st.write(f"• NetworkX degree calculation: {degree_dict}")
1498
+
1499
+ # Metode 3: Manual count semua edges
1500
+ manual_degree = 0
1501
+ for neighbor in G.neighbors(node_id):
1502
+ edge_count = G.number_of_edges(node_id, neighbor)
1503
+ manual_degree += edge_count
1504
+ st.write(f"• To {neighbor}: {edge_count} edge(s)")
1505
+
1506
+ st.write(f"• **Manual degree total**: {manual_degree}")
1507
+ st.write(f"• **NetworkX degree**: {degree_nx}")
1508
+ st.write(f"• **Difference**: {degree_nx - manual_degree}")
1509
+
1510
+ if all_edges:
1511
+ st.write("**All edges from G.edges():**")
1512
+ for edge in all_edges:
1513
+ st.write(f" {edge}")
1514
+
1515
+ if degree_nx != manual_degree:
1516
+ st.error("⚠️ **NETWORKX BUG DETECTED!**")
1517
+ st.write("**Analysis:**")
1518
+ st.write(f"- Manual count (CORRECT): {manual_degree}")
1519
+ st.write(f"- NetworkX degree (WRONG): {degree_nx}")
1520
+ st.write(f"- Difference: +{degree_nx - manual_degree}")
1521
+ st.write(
1522
+ f"- Self-loop present: {'Yes' if has_self_loop_search else 'No'}"
1523
+ )
1524
+ st.write("**Root Cause:**")
1525
+ st.write(
1526
+ "- NetworkX internal bug with self-loop counting"
1527
+ )
1528
+ st.write("- Graf construction issue")
1529
+ st.write("- Use manual count as the correct value")
1530
+
1531
+ st.success(
1532
+ f"✅ **CORRECTED**: Node {node_id} has {manual_degree} connections"
1533
+ )
1534
+ elif has_self_loop_search:
1535
+ st.info(
1536
+ "ℹ️ **Self-loop detected** (counted as +1 degree)"
1537
+ )
1538
+ else:
1539
+ st.success("✅ **All calculations consistent!**")
1540
+
1541
+ st.write("**Detail Koneksi:**")
1542
+ for detail in edge_details[:8]:
1543
+ st.write(f" {detail}")
1544
+ if len(edge_details) > 8:
1545
+ st.write(f" ... dan {len(edge_details) - 8} lainnya")
1546
+
1547
+ else:
1548
+ st.warning(f"❌ Node {node_id} tidak ditemukan dalam graf")
1549
+ # Tampilkan beberapa node yang tersedia untuk referensi
1550
+ available_nodes = sorted(list(G.nodes()))[:10]
1551
+ st.info(f"💡 Contoh node yang tersedia: {available_nodes}")
1552
+ except ValueError:
1553
+ st.warning("⚠️ Masukkan angka yang valid")
1554
+
1555
+ # Info tambahan tentang node
1556
+ st.markdown("### 📋 Informasi Node")
1557
+ col1, col2, col3 = st.columns(3)
1558
+ with col1:
1559
+ total_nodes = G.number_of_nodes()
1560
+ st.metric("Total Node", total_nodes)
1561
+ with col2:
1562
+ if total_nodes > 0:
1563
+ min_node = min(G.nodes())
1564
+ max_node = max(G.nodes())
1565
+ st.metric("Range Node ID", f"{min_node} - {max_node}")
1566
+ with col3:
1567
+ # Tampilkan beberapa node dengan degree tertinggi
1568
+ if G.number_of_nodes() > 0:
1569
+ top_degree_nodes = sorted(
1570
+ G.degree(), key=lambda x: x[1], reverse=True
1571
+ )[:3]
1572
+ top_nodes_str = ", ".join(
1573
+ [str(node) for node, _ in top_degree_nodes]
1574
+ )
1575
+ st.metric("Top 3 Node (Degree)", top_nodes_str)
1576
+
1577
  # Visualisasi graf
1578
  try:
1579
  network_fig = create_network_visualization(
1580
+ G_viz,
1581
+ st.session_state["nodes"],
1582
+ centrality_measures,
1583
+ show_labels,
1584
+ show_edge_details,
1585
+ label_size,
1586
+ label_color,
1587
+ edge_offset,
1588
+ show_edge_colors,
1589
  )
1590
  st.plotly_chart(network_fig, use_container_width=True)
1591
  except Exception as e:
1592
  st.error(f"Error creating network visualization: {str(e)}")
1593
 
1594
+ # Detail Koneksi Node
1595
+ st.markdown("### 🔗 Detail Koneksi Node")
1596
+
1597
+ try:
1598
+ connection_df = create_node_connection_details(G_viz, top_n=20)
1599
+ if not connection_df.empty:
1600
+ st.markdown(
1601
+ """
1602
+ **Penjelasan Kolom:**
1603
+ - **Total Edges**: Jumlah total saluran yang terhubung ke node
1604
+ - **Jumlah Tetangga**: Jumlah node lain yang terhubung langsung
1605
+ - **Rasio Edge/Tetangga**: Rata-rata saluran per tetangga (>1 = ada saluran paralel)
1606
+ """
1607
+ )
1608
+
1609
+ # Highlight nodes dengan multiple edges
1610
+ def highlight_multiple_edges(df):
1611
+ def color_ratio(val):
1612
+ try:
1613
+ ratio = float(val)
1614
+ if ratio > 1.5:
1615
+ return "background-color: #ffcccc; font-weight: bold" # Merah muda untuk rasio tinggi
1616
+ elif ratio > 1.0:
1617
+ return "background-color: #fff2cc" # Kuning untuk rasio sedang
1618
+ else:
1619
+ return ""
1620
+ except:
1621
+ return ""
1622
+
1623
+ return df.style.applymap(
1624
+ color_ratio, subset=["Rasio Edge/Tetangga"]
1625
+ )
1626
+
1627
+ st.dataframe(
1628
+ highlight_multiple_edges(connection_df),
1629
+ use_container_width=True,
1630
+ height=400,
1631
+ )
1632
+
1633
+ # Analisis tambahan
1634
+ high_ratio_nodes = connection_df[
1635
+ connection_df["Rasio Edge/Tetangga"].astype(float) > 1.0
1636
+ ]
1637
+ if not high_ratio_nodes.empty:
1638
+ st.markdown("### 🔍 Analisis Saluran Paralel")
1639
+ st.info(
1640
+ f"Ditemukan {len(high_ratio_nodes)} node dengan saluran paralel (rasio > 1.0)"
1641
+ )
1642
+
1643
+ for _, row in high_ratio_nodes.head(5).iterrows():
1644
+ st.write(
1645
+ f"• **Node {row['Node']}**: {row['Total Edges']} saluran ke {row['Jumlah Tetangga']} tetangga (rasio: {row['Rasio Edge/Tetangga']})"
1646
+ )
1647
+
1648
+ else:
1649
+ st.warning("Tidak ada data koneksi untuk ditampilkan")
1650
+
1651
+ except Exception as e:
1652
+ st.error(f"Error creating connection details: {str(e)}")
1653
+
1654
  # Informasi graf
1655
  st.markdown("### 🔍 Interpretasi Graf")
1656
  st.markdown(
 
1659
  - **Edge (Sisi)**: Merepresentasikan saluran listrik yang menghubungkan antar titik
1660
  - **Warna Node**: Intensitas warna menunjukkan tingkat kepentingan berdasarkan Degree Centrality
1661
  - **Node dengan warna lebih gelap**: Memiliki lebih banyak koneksi (lebih kritis)
1662
+ - **Saluran Paralel**: Node dengan rasio Edge/Tetangga > 1 memiliki multiple saluran ke tetangga yang sama
1663
  """
1664
  )
1665
 
 
1676
  except Exception as e:
1677
  st.error(f"Error creating centrality comparison: {str(e)}")
1678
 
1679
+ # Matriks Sentralitas
1680
+ st.markdown("### 📊 Matriks Nilai Sentralitas")
1681
+
1682
+ try:
1683
+ centrality_df = create_centrality_matrix(centrality_measures)
1684
+ if not centrality_df.empty:
1685
+ # Tampilkan statistik ringkas
1686
+ col1, col2, col3, col4 = st.columns(4)
1687
+
1688
+ with col1:
1689
+ st.metric("Total Node", len(centrality_df))
1690
+ with col2:
1691
+ st.metric(
1692
+ "Max Degree Centrality",
1693
+ f"{centrality_df['Degree Centrality'].max():.4f}",
1694
+ )
1695
+ with col3:
1696
+ st.metric(
1697
+ "Max Betweenness",
1698
+ f"{centrality_df['Betweenness Centrality'].max():.4f}",
1699
+ )
1700
+ with col4:
1701
+ st.metric(
1702
+ "Max Closeness",
1703
+ f"{centrality_df['Closeness Centrality'].max():.4f}",
1704
+ )
1705
+
1706
+ # Opsi untuk menampilkan semua data atau hanya top N
1707
+ display_option = st.radio(
1708
+ "Pilih tampilan data:",
1709
+ ["Top 20 Node", "Top 50 Node", "Semua Node"],
1710
+ horizontal=True,
1711
+ )
1712
+
1713
+ if display_option == "Top 20 Node":
1714
+ display_df = centrality_df.head(20)
1715
+ elif display_option == "Top 50 Node":
1716
+ display_df = centrality_df.head(50)
1717
+ else:
1718
+ display_df = centrality_df
1719
+
1720
+ # Tampilkan tabel dengan styling dan color coding
1721
+ def highlight_values(df):
1722
+ """Apply color coding to centrality values"""
1723
+ styled_df = df.style
1724
+
1725
+ # Color coding untuk setiap kolom centrality
1726
+ centrality_cols = [
1727
+ "Degree Centrality",
1728
+ "Closeness Centrality",
1729
+ "Betweenness Centrality",
1730
+ "Eigenvector Centrality",
1731
+ ]
1732
+
1733
+ # Color mapping untuk setiap kolom dengan warna berbeda
1734
+ color_maps = {
1735
+ "Degree Centrality": "Reds", # Merah
1736
+ "Closeness Centrality": "Blues", # Biru
1737
+ "Betweenness Centrality": "Greens", # Hijau
1738
+ "Eigenvector Centrality": "Purples", # Ungu
1739
+ }
1740
+
1741
+ for col in centrality_cols:
1742
+ if col in df.columns:
1743
+ # Gradient color berbeda untuk setiap kolom
1744
+ styled_df = styled_df.background_gradient(
1745
+ subset=[col],
1746
+ cmap=color_maps[col],
1747
+ vmin=0,
1748
+ vmax=df[col].max(),
1749
+ )
1750
+
1751
+ # Format angka dengan 6 desimal
1752
+ format_dict = {}
1753
+ for col in centrality_cols:
1754
+ if col in df.columns:
1755
+ format_dict[col] = "{:.6f}"
1756
+
1757
+ styled_df = styled_df.format(format_dict)
1758
+
1759
+ # Styling tambahan
1760
+ styled_df = styled_df.set_properties(
1761
+ **{"font-weight": "bold", "text-align": "center"},
1762
+ subset=["Node"],
1763
+ )
1764
+
1765
+ # Highlight top 5 nodes dengan border tebal
1766
+ top_5_indices = df.head(5).index
1767
+ styled_df = styled_df.set_properties(
1768
+ **{"border": "3px solid #ff6b6b", "font-weight": "bold"},
1769
+ subset=pd.IndexSlice[top_5_indices, :],
1770
+ )
1771
+
1772
+ return styled_df
1773
+
1774
+ # Tampilkan legend untuk color coding
1775
+ st.markdown(
1776
+ """
1777
+ **📋 Keterangan Visualisasi (Warna per Kolom):**
1778
+ - 🟥 **Degree Centrality**: Gradasi Merah (putih → merah gelap)
1779
+ - 🟦 **Closeness Centrality**: Gradasi Biru (putih → biru gelap)
1780
+ - 🟩 **Betweenness Centrality**: Gradasi Hijau (putih → hijau gelap)
1781
+ - 🟪 **Eigenvector Centrality**: Gradasi Ungu (putih → ungu gelap)
1782
+ - 🔴 **Border Merah Tebal**: Top 5 node paling penting
1783
+
1784
+ *Semakin gelap warna = semakin tinggi nilai sentralitas*
1785
+ """
1786
+ )
1787
+
1788
+ # Tampilkan tabel dengan styling
1789
+ st.dataframe(
1790
+ highlight_values(display_df),
1791
+ use_container_width=True,
1792
+ height=400,
1793
+ )
1794
+
1795
+ # Informasi tambahan tentang interpretasi
1796
+ with st.expander("ℹ️ Cara Membaca Matriks Sentralitas"):
1797
+ st.markdown(
1798
+ """
1799
+ **Interpretasi Nilai Sentralitas:**
1800
+
1801
+ 1. **Degree Centrality (0-1)**:
1802
+ - Mengukur jumlah koneksi langsung
1803
+ - Nilai tinggi = node dengan banyak koneksi
1804
+
1805
+ 2. **Closeness Centrality (0-1)**:
1806
+ - Mengukur kedekatan ke semua node lain
1807
+ - Nilai tinggi = node yang mudah dijangkau dari mana saja
1808
+
1809
+ 3. **Betweenness Centrality (0-1)**:
1810
+ - Mengukur seberapa sering node berada di jalur terpendek
1811
+ - Nilai tinggi = node yang berperan sebagai jembatan penting
1812
+
1813
+ 4. **Eigenvector Centrality (0-1)**:
1814
+ - Mengukur pengaruh berdasarkan kualitas koneksi
1815
+ - Nilai tinggi = node yang terhubung ke node-node penting lainnya
1816
+
1817
+ **Tips Analisis:**
1818
+ - Node dengan nilai tinggi di semua kategori = **Super Critical**
1819
+ - Node dengan Betweenness tinggi = **Bottleneck** potensial
1820
+ - Node dengan Degree tinggi tapi Eigenvector rendah = **Hub** lokal
1821
+ """
1822
+ )
1823
+
1824
+ # Tombol download CSV
1825
+ csv = centrality_df.to_csv(index=False)
1826
+ st.download_button(
1827
+ label="📥 Download Matriks Sentralitas (CSV)",
1828
+ data=csv,
1829
+ file_name="centrality_matrix.csv",
1830
+ mime="text/csv",
1831
+ )
1832
+
1833
+ else:
1834
+ st.warning("Tidak ada data sentralitas untuk ditampilkan")
1835
+
1836
+ except Exception as e:
1837
+ st.error(f"Error creating centrality matrix: {str(e)}")
1838
+
1839
  # Identifikasi node kritis
1840
  st.markdown("### 🎯 Identifikasi Node Kritis")
1841
 
 
1852
  for i, (node, centrality) in enumerate(top_nodes, 1):
1853
  st.write(f"{i}. Node {node}: {centrality:.4f}")
1854
 
1855
+ # Rekomendasi berdasarkan analisis statistik
1856
+ st.markdown("### 💡 Rekomendasi Berbasis Data")
1857
+ if top_nodes and centrality_measures.get("degree"):
1858
+ # Analisis statistik degree centrality
1859
+ degree_values = list(centrality_measures["degree"].values())
1860
+ mean_degree = np.mean(degree_values)
1861
+ std_degree = np.std(degree_values)
1862
+ q75 = np.percentile(degree_values, 75)
1863
+ q90 = np.percentile(degree_values, 90)
1864
+ q95 = np.percentile(degree_values, 95)
1865
+
1866
+ # Threshold berdasarkan statistik
1867
+ critical_threshold = q90 # Top 10%
1868
+ high_priority_threshold = q75 # Top 25%
1869
+
1870
+ # Hitung jumlah node per kategori
1871
+ critical_nodes = [
1872
+ node
1873
+ for node, cent in centrality_measures["degree"].items()
1874
+ if cent >= critical_threshold
1875
+ ]
1876
+ high_priority_nodes = [
1877
+ node
1878
+ for node, cent in centrality_measures["degree"].items()
1879
+ if cent >= high_priority_threshold and cent < critical_threshold
1880
+ ]
1881
+
1882
+ st.markdown("#### 📊 Analisis Statistik Degree Centrality")
1883
+ col1, col2, col3, col4 = st.columns(4)
1884
+ with col1:
1885
+ st.metric("Mean", f"{mean_degree:.4f}")
1886
+ with col2:
1887
+ st.metric("Std Dev", f"{std_degree:.4f}")
1888
+ with col3:
1889
+ st.metric("75th Percentile", f"{q75:.4f}")
1890
+ with col4:
1891
+ st.metric("90th Percentile", f"{q90:.4f}")
1892
+
1893
  st.markdown(
1894
  f"""
1895
  **Node Paling Kritis:** Node {top_nodes[0][0]} (Degree Centrality: {top_nodes[0][1]:.4f})
1896
+
1897
+ **Rekomendasi Kebijakan Berbasis Data:**
1898
+
1899
+ 1. **🔴 Monitoring Kritis** (≥ {critical_threshold:.4f} - Top 10%):
1900
+ - **{len(critical_nodes)} node** memerlukan monitoring 24/7
1901
+ - Sistem backup dan redundansi wajib
1902
+ - Maintenance preventif bulanan
1903
+
1904
+ 2. **🟡 Monitoring Prioritas** ({high_priority_threshold:.4f} - {critical_threshold:.4f} - Top 25%):
1905
+ - **{len(high_priority_nodes)} node** monitoring reguler
1906
+ - Maintenance preventif triwulanan
1907
+ - Rencana contingency tersedia
1908
+
1909
+ 3. **🟢 Monitoring Standar** (< {high_priority_threshold:.4f}):
1910
+ - Monitoring rutin sesuai jadwal normal
1911
+ - Maintenance tahunan
1912
+
1913
+ **Basis Ilmiah:**
1914
+ - Threshold berdasarkan distribusi statistik data aktual
1915
+ - Top 10% (90th percentile) untuk monitoring kritis
1916
+ - Top 25% (75th percentile) untuk monitoring prioritas
1917
+ - Menggunakan analisis risiko berbasis data, bukan nilai arbitrary
1918
  """
1919
  )
1920
 
 
1925
  )
1926
 
1927
  if mst.number_of_nodes() > 0:
1928
+ # Perhitungan dasar
1929
+ total_weight_mst = sum(
1930
+ [data["weight"] for _, _, data in mst.edges(data=True)]
1931
+ )
1932
+ original_weight = sum(
1933
+ [data["weight"] for _, _, data in G.edges(data=True)]
1934
+ )
1935
+ savings = original_weight - total_weight_mst
1936
+ efficiency = (
1937
+ (savings / original_weight * 100) if original_weight > 0 else 0
1938
+ )
1939
+
1940
+ # Tampilkan metrics
1941
  col1, col2 = st.columns(2)
1942
  with col1:
1943
+ st.metric("Total Bobot MST", f"{total_weight_mst:.2f}m")
1944
+ st.metric("Jumlah Edge MST", mst.number_of_edges())
 
 
 
1945
  with col2:
1946
+ st.metric("Efisiensi", f"{efficiency:.2f}%")
1947
+ st.metric("Penghematan", f"{savings:.2f}m")
1948
+
1949
+ # Proses Perhitungan Detail
1950
+ st.markdown("### 🧮 Proses Perhitungan Efisiensi dan Penghematan")
1951
+
1952
+ with st.expander("📊 Detail Perhitungan Langkah demi Langkah"):
1953
+ st.markdown("#### 1️⃣ **Perhitungan Total Bobot Jaringan Asli**")
1954
+ st.code(
1955
+ f"""
1956
+ # Formula: Σ(weight_i) untuk semua edge dalam graf asli
1957
+ original_edges = {G.number_of_edges()} edge
1958
+ original_weight = Σ(weight_i) = {original_weight:.2f} meter
1959
+ """
1960
+ )
1961
+
1962
+ st.markdown("#### 2️⃣ **Perhitungan Total Bobot MST**")
1963
+ st.code(
1964
+ f"""
1965
+ # Formula: Σ(weight_i) untuk edge dalam MST
1966
+ mst_edges = {mst.number_of_edges()} edge
1967
+ mst_weight = Σ(weight_i) = {total_weight_mst:.2f} meter
1968
+ """
1969
  )
1970
+
1971
+ st.markdown("#### 3️⃣ **Perhitungan Penghematan Absolut**")
1972
+ st.code(
1973
+ f"""
1974
+ # Formula: Penghematan = Total_Asli - Total_MST
1975
+ savings = {original_weight:.2f} - {total_weight_mst:.2f}
1976
+ savings = {savings:.2f} meter
1977
+ """
1978
  )
1979
+
1980
+ st.markdown("#### 4️⃣ **Perhitungan Efisiensi Relatif**")
1981
+ st.code(
1982
+ f"""
1983
+ # Formula: Efisiensi = (Penghematan / Total_Asli) × 100%
1984
+ efficiency = ({savings:.2f} / {original_weight:.2f}) × 100%
1985
+ efficiency = {efficiency:.2f}%
1986
+ """
1987
+ )
1988
+
1989
+ st.markdown("#### 5️⃣ **Interpretasi Hasil**")
1990
+ if efficiency > 50:
1991
+ interpretation = (
1992
+ "🔴 **Sangat Tinggi** - Jaringan asli sangat tidak efisien"
1993
+ )
1994
+ recommendation = "Pertimbangkan restrukturisasi besar-besaran"
1995
+ elif efficiency > 30:
1996
+ interpretation = (
1997
+ "🟡 **Tinggi** - Ada potensi optimasi signifikan"
1998
+ )
1999
+ recommendation = "Evaluasi edge redundan untuk penghematan"
2000
+ elif efficiency > 10:
2001
+ interpretation = "🟢 **Sedang** - Jaringan cukup efisien"
2002
+ recommendation = "Optimasi minor pada area tertentu"
2003
+ else:
2004
+ interpretation = "✅ **Rendah** - Jaringan sudah sangat efisien"
2005
+ recommendation = "Pertahankan struktur existing"
2006
+
2007
+ st.markdown(
2008
+ f"""
2009
+ **Tingkat Efisiensi:** {interpretation}
2010
+ **Rekomendasi:** {recommendation}
2011
+
2012
+ **Penjelasan:**
2013
+ - **Efisiensi {efficiency:.2f}%** berarti MST dapat menghemat {efficiency:.2f}% dari total panjang kabel
2014
+ - **Penghematan {savings:.2f}m** setara dengan {savings/1000:.3f} km kabel
2015
+ - **Edge yang dihilangkan:** {G.number_of_edges() - mst.number_of_edges()} edge (redundan)
2016
+ """
2017
+ )
2018
+
2019
+ # Analisis Biaya (opsional)
2020
+ st.markdown("### 💰 Analisis Biaya (Estimasi)")
2021
+ col1, col2 = st.columns(2)
2022
+
2023
+ with col1:
2024
+ cost_per_meter = st.number_input(
2025
+ "Biaya per meter (Rp)",
2026
+ min_value=0,
2027
+ value=500000,
2028
+ step=50000,
2029
+ help="Estimasi biaya instalasi kabel per meter",
2030
+ )
2031
+
2032
+ with col2:
2033
+ if cost_per_meter > 0:
2034
+ total_cost_original = original_weight * cost_per_meter
2035
+ total_cost_mst = total_weight_mst * cost_per_meter
2036
+ cost_savings = total_cost_original - total_cost_mst
2037
+
2038
+ st.metric(
2039
+ "Biaya Jaringan Asli", f"Rp {total_cost_original:,.0f}"
2040
+ )
2041
+ st.metric("Biaya MST", f"Rp {total_cost_mst:,.0f}")
2042
+ st.metric("Penghematan Biaya", f"Rp {cost_savings:,.0f}")
2043
+
2044
+ st.success(
2045
+ f"💡 **Insight**: Dengan MST, dapat menghemat **Rp {cost_savings:,.0f}** ({efficiency:.1f}%) dari biaya konstruksi!"
2046
+ )
2047
 
2048
  # Visualisasi MST
2049
  try:
 
2061
  except Exception as e:
2062
  st.error(f"Error creating MST visualization: {str(e)}")
2063
 
2064
+ st.markdown("### 🔧 Interpretasi MST dan Analisis Redundansi")
2065
+
2066
+ # Analisis Edge Redundan
2067
+ st.markdown("#### 🔍 Analisis Edge Redundan")
2068
+
2069
+ # Identifikasi edge redundan
2070
+ mst_edges = set(mst.edges())
2071
+ original_edges = set(G.edges())
2072
+ redundant_edges = []
2073
+
2074
+ for edge in original_edges:
2075
+ # Cek kedua arah karena edge tidak berarah
2076
+ if edge not in mst_edges and (edge[1], edge[0]) not in mst_edges:
2077
+ edge_data = G.get_edge_data(edge[0], edge[1])
2078
+ if edge_data:
2079
+ redundant_edges.append(
2080
+ (edge[0], edge[1], edge_data["weight"])
2081
+ )
2082
+
2083
+ # Hitung total bobot edge redundan
2084
+ total_redundant_weight = sum(
2085
+ [weight for _, _, weight in redundant_edges]
2086
+ )
2087
+ redundant_percentage = (
2088
+ (total_redundant_weight / original_weight * 100)
2089
+ if original_weight > 0
2090
+ else 0
2091
+ )
2092
+
2093
+ col1, col2, col3 = st.columns(3)
2094
+ with col1:
2095
+ st.metric("Edge Redundan", f"{len(redundant_edges)}")
2096
+ with col2:
2097
+ st.metric("Bobot Redundan", f"{total_redundant_weight:.2f}m")
2098
+ with col3:
2099
+ st.metric("% Redundansi", f"{redundant_percentage:.2f}%")
2100
+
2101
+ # Detail perhitungan redundansi
2102
+ with st.expander("🧮 Perhitungan Analisis Redundansi"):
2103
+ st.markdown("#### 1️⃣ **Identifikasi Edge Redundan**")
2104
+ st.code(
2105
+ f"""
2106
+ # Edge dalam jaringan asli: {len(original_edges)}
2107
+ # Edge dalam MST: {len(mst_edges)}
2108
+ # Edge redundan = Edge_asli - Edge_MST
2109
+ redundant_edges = {len(redundant_edges)}
2110
  """
2111
+ )
2112
+
2113
+ st.markdown("#### 2️⃣ **Perhitungan Bobot Redundan**")
2114
+ st.code(
2115
+ f"""
2116
+ # Formula: Σ(weight_i) untuk edge yang tidak ada dalam MST
2117
+ total_redundant_weight = Σ(weight_redundant_i)
2118
+ total_redundant_weight = {total_redundant_weight:.2f} meter
2119
+ """
2120
+ )
2121
+
2122
+ st.markdown("#### 3️⃣ **Persentase Redundansi**")
2123
+ st.code(
2124
+ f"""
2125
+ # Formula: (Bobot_Redundan / Bobot_Total_Asli) × 100%
2126
+ redundancy_percentage = ({total_redundant_weight:.2f} / {original_weight:.2f}) × 100%
2127
+ redundancy_percentage = {redundant_percentage:.2f}%
2128
+ """
2129
+ )
2130
+
2131
+ st.markdown("#### 4️⃣ **Verifikasi Konsistensi**")
2132
+ st.code(
2133
+ f"""
2134
+ # Verifikasi: MST_weight + Redundant_weight = Original_weight
2135
+ {total_weight_mst:.2f} + {total_redundant_weight:.2f} = {total_weight_mst + total_redundant_weight:.2f}
2136
+ Original weight: {original_weight:.2f}
2137
+ Difference: {abs(original_weight - (total_weight_mst + total_redundant_weight)):.2f}m
2138
+ """
2139
+ )
2140
+
2141
+ # Tampilkan beberapa edge redundan terbesar
2142
+ if redundant_edges:
2143
+ st.markdown("#### 5️⃣ **Top 10 Edge Redundan Terpanjang**")
2144
+ redundant_sorted = sorted(
2145
+ redundant_edges, key=lambda x: x[2], reverse=True
2146
+ )[:10]
2147
+ redundant_df = pd.DataFrame(
2148
+ redundant_sorted,
2149
+ columns=["Node A", "Node B", "Panjang (m)"],
2150
+ )
2151
+ redundant_df["Panjang (km)"] = (
2152
+ redundant_df["Panjang (m)"] / 1000
2153
+ )
2154
+ st.dataframe(redundant_df, use_container_width=True)
2155
+
2156
+ # Interpretasi berdasarkan tingkat redundansi
2157
+ st.markdown("#### 📊 Interpretasi Tingkat Redundansi")
2158
+
2159
+ if redundant_percentage > 40:
2160
+ redundancy_level = "🔴 **Sangat Tinggi**"
2161
+ redundancy_meaning = "Jaringan memiliki banyak jalur alternatif"
2162
+ redundancy_action = "Pertimbangkan untuk mengurangi edge redundan pada fase konstruksi baru"
2163
+ elif redundant_percentage > 25:
2164
+ redundancy_level = "🟡 **Tinggi**"
2165
+ redundancy_meaning = (
2166
+ "Jaringan memiliki redundansi yang baik untuk keandalan"
2167
+ )
2168
+ redundancy_action = (
2169
+ "Evaluasi cost-benefit antara redundansi dan efisiensi"
2170
+ )
2171
+ elif redundant_percentage > 10:
2172
+ redundancy_level = "🟢 **Sedang**"
2173
+ redundancy_meaning = "Tingkat redundansi optimal untuk keseimbangan efisiensi-keandalan"
2174
+ redundancy_action = "Pertahankan tingkat redundansi saat ini"
2175
+ else:
2176
+ redundancy_level = "⚠️ **Rendah**"
2177
+ redundancy_meaning = (
2178
+ "Jaringan mendekati struktur minimal (seperti MST)"
2179
+ )
2180
+ redundancy_action = (
2181
+ "Pertimbangkan menambah redundansi untuk meningkatkan keandalan"
2182
+ )
2183
+
2184
+ st.markdown(
2185
+ f"""
2186
+ **Tingkat Redundansi:** {redundancy_level} ({redundant_percentage:.1f}%)
2187
+
2188
+ **Makna:** {redundancy_meaning}
2189
+
2190
+ **Rekomendasi:** {redundancy_action}
2191
+
2192
+ **Analisis Teknis:**
2193
+ - **Edge redundan:** {len(redundant_edges)} dari {len(original_edges)} total edge
2194
+ - **Bobot redundan:** {total_redundant_weight:.2f}m ({total_redundant_weight/1000:.3f} km)
2195
+ - **Fungsi redundansi:** Menyediakan jalur alternatif jika terjadi gangguan
2196
+ - **Trade-off:** Redundansi ↑ = Keandalan ↑, Efisiensi ↓
2197
+ """
2198
+ )
2199
+
2200
+ # Kesimpulan MST
2201
+ st.markdown("#### 🎯 Kesimpulan MST Analysis")
2202
+ st.markdown(
2203
+ f"""
2204
+ **Ringkasan Analisis:**
2205
+
2206
+ 1. **Efisiensi Jaringan:** {efficiency:.1f}% - MST dapat menghemat {efficiency:.1f}% dari total panjang kabel
2207
+ 2. **Redundansi Jaringan:** {redundant_percentage:.1f}% - {redundant_percentage:.1f}% dari jaringan bersifat redundan
2208
+ 3. **Optimasi Potensial:** {len(redundant_edges)} edge dapat dievaluasi untuk penghematan
2209
+ 4. **Keseimbangan:** Pertimbangkan trade-off antara efisiensi (MST) dan keandalan (redundansi)
2210
+
2211
+ **Aplikasi Praktis:**
2212
+ - **Perencanaan Baru:** Gunakan MST sebagai baseline minimum
2213
+ - **Optimasi Existing:** Evaluasi edge redundan untuk cost reduction
2214
+ - **Maintenance:** Prioritaskan edge MST untuk pemeliharaan kritis
2215
+ - **Expansion:** Tambahkan edge di luar MST untuk meningkatkan redundansi
2216
  """
2217
  )
2218
  else: