diff --git a/analysis/views/clustering.py b/analysis/views/clustering.py index 2978de3..ec7ffd0 100644 --- a/analysis/views/clustering.py +++ b/analysis/views/clustering.py @@ -161,7 +161,9 @@ def _get_globe_flows(run, max_rows=500): def cluster_detail(request, display_id, cluster_label): - """Detailed view of a single cluster with entity list, SVD features, and NL summary.""" + """Detailed view of a single cluster with raw data rows, SVD features, and NL summary.""" + import json + import polars as pl run = get_object_or_404(AnalysisRun, display_id=display_id) try: cluster_label = int(cluster_label) @@ -169,29 +171,50 @@ def cluster_detail(request, display_id, cluster_label): raise Http404("Invalid cluster label") cluster = get_object_or_404(ClusterResult, run=run, cluster_label=cluster_label) features = cluster.features.all().order_by('-distinguishing_score')[:50] - entities = cluster.entities.all()[:100] + + # Load raw data rows for this cluster from EP row indices + row_data = [] + try: + from analysis.data_loader import load_csv_directory, load_from_db + lf = None + if run.sqlite_table: + lf = load_from_db(run.sqlite_table) + if lf is None and run.csv_glob: + lf, _, _, _, _ = load_csv_directory(run.csv_glob) + if lf is not None: + # Get row indices assigned to this cluster from EntityProfile + eps = cluster.entities.all().order_by('entity_value')[:100] + indices = [] + for ep in eps: + try: + idx = int(ep.entity_value.replace('row_', '')) + indices.append(idx) + except (ValueError, AttributeError): + continue + if indices: + df = lf.collect(streaming=True) + df_filtered = df[indices] + # Convert to dict list for template (limit columns for display) + cols = df_filtered.columns[:20] # show first 20 columns + for row_idx, row in enumerate(df_filtered.select(cols).iter_rows(named=True)): + entry = {'index': indices[row_idx] if row_idx < len(indices) else row_idx} + for k, v in row.items(): + entry[k] = v + row_data.append(entry) + except Exception as exc: + logger.warning(f'Failed to load raw data for cluster detail: {exc}') + nl_summary = '' feats_list = [{'feature_name': f.feature_name, 'score': f.distinguishing_score, 'mean': f.mean, 'std': f.std} for f in features[:10]] if feats_list: nl_summary = nl_describe.describe_cluster(feats_list) - # Build entity data with feature_json values - entity_data = [] - for e in entities: - entry = {'id': e.id, 'value': e.entity_value, - 'embedding_x': e.embedding_x, 'embedding_y': e.embedding_y} - if e.feature_json: - entry['features'] = {k: round(float(v), 4) if isinstance(v, (int, float)) else str(v) - for k, v in e.feature_json.items()} - entity_data.append(entry) - return render(request, 'analysis/cluster_detail.html', { 'run': run, 'cluster': cluster, 'features': features, - 'entities': entities, - 'entity_data_json': json.dumps(entity_data), + 'row_data': row_data, 'nl_summary': nl_summary, }) diff --git a/templates/analysis/cluster_detail.html b/templates/analysis/cluster_detail.html index 643295c..28572cd 100644 --- a/templates/analysis/cluster_detail.html +++ b/templates/analysis/cluster_detail.html @@ -3,14 +3,12 @@ {% block title %}簇 #{{ cluster.cluster_label }} — 运行 #{{ run.display_id }}{% endblock %} {% block content %}
@@ -93,28 +91,34 @@ {% endwith %} {% endif %} - +
-

👤 行列表 {{ entities|length }} 条

-
- {% for e in entities %} -
-
- {{ e.entity_value }} - 画像 -
- {% if e.feature_json %} -
- {% for k, v in e.feature_json.items|slice:":8" %} - {{ k }}: {% if v is None %}-{% else %}{{ v|floatformat:2 }}{% endif %} +

📋 数据行 {{ row_data|length }} 条

+ {% if row_data %} +
+ + + + + {% for k, v in row_data.0.items %} + {% endfor %} - {% if e.feature_json.items|length > 8 %}+{{ e.feature_json.items|length|add:"-8" }} more{% endif %} - - {% endif %} - - {% empty %} -

该簇无数据行

- {% endfor %} + + + + {% for row in row_data %} + + + {% for k, v in row.items %} + + {% endfor %} + + {% endfor %} + +
#{{ k }}
{{ forloop.counter }}{{ v|default:"-" }}
+ {% else %} +

该簇无数据行

+ {% endif %}
{% endblock %} \ No newline at end of file