|
|
|
|
|
import networkx as nx
|
|
from pyvis.network import Network
|
|
import logging
|
|
from pathlib import Path
|
|
import pandas as pd
|
|
import random
|
|
|
|
|
|
from src.data_management import storage
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
|
|
OUTPUT_DIR = Path("output/graphs")
|
|
DEFAULT_GRAPH_FILENAME = "concept_network"
|
|
|
|
DEFAULT_ANALYSIS_FILENAME = storage.NETWORK_ANALYSIS_FILENAME
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_COLORS = [
|
|
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
|
|
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
|
|
]
|
|
|
|
def get_color_for_community(community_id, colors=DEFAULT_COLORS):
|
|
""" Verilen community ID için paletten bir renk döndürür. """
|
|
if community_id < 0 or community_id is None or pd.isna(community_id):
|
|
return "#CCCCCC"
|
|
return colors[int(community_id) % len(colors)]
|
|
|
|
def scale_value(value, min_val=0, max_val=1, new_min=10, new_max=50):
|
|
""" Bir değeri belirli bir aralığa ölçekler (örn: merkeziyet -> düğüm boyutu). """
|
|
if max_val == min_val or value is None or pd.isna(value):
|
|
return new_min
|
|
|
|
scaled = ((value - min_val) / (max_val - min_val)) * (new_max - new_min) + new_min
|
|
return max(new_min, min(scaled, new_max))
|
|
|
|
|
|
def visualize_network(graph: nx.Graph | None = None,
|
|
graph_filename: str = DEFAULT_GRAPH_FILENAME,
|
|
analysis_filename: str = DEFAULT_ANALYSIS_FILENAME,
|
|
output_filename: str = "concept_network_visualization.html",
|
|
show_buttons: bool = True,
|
|
physics_solver: str = 'barnesHut',
|
|
size_metric: str = 'degree_centrality',
|
|
color_metric: str = 'community_id',
|
|
height: str = "800px",
|
|
width: str = "100%"
|
|
) -> str | None:
|
|
"""
|
|
Ağ grafını Pyvis ile görselleştirir. Düğüm boyutu ve rengi için ağ
|
|
analizi metriklerini kullanır.
|
|
"""
|
|
if graph is None:
|
|
logging.info(f"Graf sağlanmadı, '{graph_filename}.pkl' dosyasından yükleniyor...")
|
|
graph = storage.load_network(graph_filename)
|
|
|
|
if graph is None or not isinstance(graph, nx.Graph) or graph.number_of_nodes() == 0:
|
|
logging.error("Görselleştirilecek geçerli veya boş olmayan bir graf bulunamadı.")
|
|
return None
|
|
|
|
|
|
logging.info(f"Ağ analizi sonuçları '{analysis_filename}.parquet' dosyasından yükleniyor...")
|
|
analysis_df = storage.load_dataframe(analysis_filename, [])
|
|
metrics_dict = {}
|
|
min_size_val, max_size_val = 0, 1
|
|
|
|
if analysis_df is not None and not analysis_df.empty and 'concept_id' in analysis_df.columns:
|
|
|
|
required_metrics = [size_metric, color_metric]
|
|
for metric in required_metrics:
|
|
if metric not in analysis_df.columns:
|
|
logging.warning(f"Analiz sonuçlarında '{metric}' sütunu bulunamadı. Varsayılan değerler kullanılacak.")
|
|
analysis_df[metric] = None
|
|
|
|
|
|
if size_metric in analysis_df.columns and analysis_df[size_metric].notna().any():
|
|
min_size_val = analysis_df[size_metric].min()
|
|
max_size_val = analysis_df[size_metric].max()
|
|
|
|
|
|
metrics_dict = analysis_df.set_index('concept_id').to_dict('index')
|
|
logging.info("Ağ analizi metrikleri yüklendi.")
|
|
else:
|
|
logging.warning("Ağ analizi sonuçları yüklenemedi veya boş. Varsayılan düğüm boyutları/renkleri kullanılacak.")
|
|
|
|
|
|
logging.info(f"'{output_filename}' için Pyvis ağı oluşturuluyor...")
|
|
net = Network(notebook=False, height=height, width=width, heading='ChronoSense Konsept Ağı (Metriklerle)', cdn_resources='remote')
|
|
net.barnes_hut(gravity=-8000, central_gravity=0.1, spring_length=150, spring_strength=0.005, damping=0.09)
|
|
|
|
|
|
for node, attrs in graph.nodes(data=True):
|
|
node_label = attrs.get('name', str(node))
|
|
node_metrics = metrics_dict.get(node, {})
|
|
|
|
|
|
size_val = node_metrics.get(size_metric)
|
|
node_size = scale_value(size_val, min_size_val, max_size_val, new_min=10, new_max=40)
|
|
|
|
|
|
color_val = node_metrics.get(color_metric)
|
|
node_color = get_color_for_community(color_val)
|
|
|
|
|
|
node_title = f"ID: {node}<br>Name: {attrs.get('name', 'N/A')}"
|
|
node_title += f"<br>{size_metric}: {size_val:.3f}" if pd.notna(size_val) else ""
|
|
node_title += f"<br>{color_metric}: {int(color_val)}" if pd.notna(color_val) else ""
|
|
|
|
net.add_node(node, label=node_label, title=node_title, size=node_size, color=node_color)
|
|
|
|
|
|
for source, target, attrs in graph.edges(data=True):
|
|
edge_title = f"Type: {attrs.get('type', 'N/A')}"
|
|
edge_value = 0.5 ; edge_color = "#DDDDDD"
|
|
|
|
edge_type = attrs.get('type')
|
|
weight = attrs.get('weight', 0)
|
|
|
|
if edge_type == 'extracted':
|
|
edge_title += f"<br>Relation: {attrs.get('relation_type', 'N/A')}"
|
|
edge_value = max(0.6, weight)
|
|
edge_color = "#FF6347"
|
|
elif edge_type == 'similarity':
|
|
sim_score = attrs.get('similarity', weight)
|
|
edge_title += f"<br>Similarity: {sim_score:.3f}"
|
|
edge_value = sim_score
|
|
edge_color = "#4682B4"
|
|
elif edge_type == 'combined':
|
|
edge_title += f"<br>Relation: {attrs.get('relation_type', 'N/A')}"
|
|
sim_score = attrs.get('similarity', weight)
|
|
edge_title += f"<br>Similarity: {sim_score:.3f}"
|
|
edge_value = max(0.6, sim_score)
|
|
edge_color = "#9370DB"
|
|
|
|
net.add_edge(source, target, title=edge_title, value=max(0.1, edge_value), color=edge_color)
|
|
|
|
if show_buttons:
|
|
net.show_buttons(filter_=['physics', 'nodes', 'edges'])
|
|
|
|
try:
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
output_path = OUTPUT_DIR / output_filename
|
|
net.save_graph(str(output_path))
|
|
logging.info(f"Ağ görselleştirmesi başarıyla '{output_path}' olarak kaydedildi.")
|
|
return str(output_path)
|
|
except Exception as e:
|
|
logging.exception(f"Ağ görselleştirmesi kaydedilirken hata oluştu: {e}")
|
|
return None |