Spaces:
Sleeping
Sleeping
update
Browse files- .python-version +1 -0
- 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 489 |
-
edge_y = []
|
| 490 |
-
edge_info = []
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
for edge in G.edges(data=True):
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
weight = edge[2].get("weight", 0)
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 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 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 551 |
textposition="middle center",
|
| 552 |
-
textfont=dict(size=
|
| 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=
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1170 |
-
|
| 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 |
-
|
| 1176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1177 |
)
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1182 |
)
|
| 1183 |
-
|
| 1184 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
"""
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|