fix(cluster_detail): show raw data rows instead of entity profiles

This commit is contained in:
PM-pinou
2026-07-24 11:58:31 +08:00
parent d2e34ca43d
commit 244967bad5
2 changed files with 67 additions and 40 deletions
+37 -14
View File
@@ -161,7 +161,9 @@ def _get_globe_flows(run, max_rows=500):
def cluster_detail(request, display_id, cluster_label): 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) run = get_object_or_404(AnalysisRun, display_id=display_id)
try: try:
cluster_label = int(cluster_label) cluster_label = int(cluster_label)
@@ -169,29 +171,50 @@ def cluster_detail(request, display_id, cluster_label):
raise Http404("Invalid cluster label") raise Http404("Invalid cluster label")
cluster = get_object_or_404(ClusterResult, run=run, cluster_label=cluster_label) cluster = get_object_or_404(ClusterResult, run=run, cluster_label=cluster_label)
features = cluster.features.all().order_by('-distinguishing_score')[:50] 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 = '' nl_summary = ''
feats_list = [{'feature_name': f.feature_name, 'score': f.distinguishing_score, feats_list = [{'feature_name': f.feature_name, 'score': f.distinguishing_score,
'mean': f.mean, 'std': f.std} for f in features[:10]] 'mean': f.mean, 'std': f.std} for f in features[:10]]
if feats_list: if feats_list:
nl_summary = nl_describe.describe_cluster(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', { return render(request, 'analysis/cluster_detail.html', {
'run': run, 'run': run,
'cluster': cluster, 'cluster': cluster,
'features': features, 'features': features,
'entities': entities, 'row_data': row_data,
'entity_data_json': json.dumps(entity_data),
'nl_summary': nl_summary, 'nl_summary': nl_summary,
}) })
+30 -26
View File
@@ -3,14 +3,12 @@
{% block title %}簇 #{{ cluster.cluster_label }} — 运行 #{{ run.display_id }}{% endblock %} {% block title %}簇 #{{ cluster.cluster_label }} — 运行 #{{ run.display_id }}{% endblock %}
{% block content %} {% block content %}
<style> <style>
.entity-card { border:1px solid #e0e0e0; border-radius:6px; padding:0.6rem; margin-bottom:0.4rem; background:#fafafa; } .feat-table { width:100%; border-collapse:collapse; font-size:0.8rem; }
.entity-card .val { font-weight:600; font-size:0.85rem; } .feat-table th { background:#f5f5f5; padding:0.3rem 0.5rem; text-align:left; font-weight:600; border-bottom:2px solid #ddd; white-space:nowrap; }
.entity-card .feat { font-size:0.75rem; color:#666; display:inline-block; margin-right:0.5rem; } .feat-table td { padding:0.25rem 0.5rem; border-bottom:1px solid #eee; }
.feat-table td code { font-size:0.72rem; }
.feat-positive { color:#2e7d32; } .feat-positive { color:#2e7d32; }
.feat-negative { color:#c62828; } .feat-negative { color:#c62828; }
.feat-table { width:100%; border-collapse:collapse; font-size:0.8rem; }
.feat-table th { background:#f5f5f5; padding:0.3rem 0.5rem; text-align:left; font-weight:600; border-bottom:2px solid #ddd; }
.feat-table td { padding:0.25rem 0.5rem; border-bottom:1px solid #eee; }
</style> </style>
<div class="card"> <div class="card">
@@ -93,28 +91,34 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
<!-- ── Entity List ── --> <!-- ── Raw Data Rows ── -->
<div class="card"> <div class="card">
<h3>👤 行列表 <span style="font-weight:400;font-size:0.8rem;color:#888;">{{ entities|length }} 条</span></h3> <h3>📋 数据行 <span style="font-weight:400;font-size:0.8rem;color:#888;">{{ row_data|length }} 条</span></h3>
<div id="entityList"> {% if row_data %}
{% for e in entities %} <div style="overflow-x:auto;">
<div class="entity-card"> <table class="feat-table" style="font-size:0.75rem;">
<div style="display:flex;justify-content:space-between;align-items:center;"> <thead>
<span class="val">{{ e.entity_value }}</span> <tr>
<a href="{% url 'analysis:entity_profile' e.id %}" class="btn btn-sm" style="font-size:0.75rem;padding:0.2rem 0.6rem;background:#4361ee;color:#fff;text-decoration:none;border-radius:4px;">画像</a> <th>#</th>
</div> {% for k, v in row_data.0.items %}
{% if e.feature_json %} <th>{{ k }}</th>
<div style="margin-top:0.3rem;">
{% for k, v in e.feature_json.items|slice:":8" %}
<span class="feat"><strong>{{ k }}</strong>: {% if v is None %}-{% else %}{{ v|floatformat:2 }}{% endif %}</span>
{% endfor %} {% endfor %}
{% if e.feature_json.items|length > 8 %}<span class="feat" style="color:#999;">+{{ e.feature_json.items|length|add:"-8" }} more</span>{% endif %} </tr>
</div> </thead>
{% endif %} <tbody>
</div> {% for row in row_data %}
{% empty %} <tr>
<div class="empty-state"><p>该簇无数据行</p></div> <td>{{ forloop.counter }}</td>
{% endfor %} {% for k, v in row.items %}
<td><code>{{ v|default:"-" }}</code></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% else %}
<div class="empty-state"><p>该簇无数据行</p></div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}