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):
"""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,
})
+29 -25
View File
@@ -3,14 +3,12 @@
{% block title %}簇 #{{ cluster.cluster_label }} — 运行 #{{ run.display_id }}{% endblock %}
{% block content %}
<style>
.entity-card { border:1px solid #e0e0e0; border-radius:6px; padding:0.6rem; margin-bottom:0.4rem; background:#fafafa; }
.entity-card .val { font-weight:600; font-size:0.85rem; }
.entity-card .feat { font-size:0.75rem; color:#666; display:inline-block; margin-right:0.5rem; }
.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; white-space:nowrap; }
.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-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>
<div class="card">
@@ -93,28 +91,34 @@
{% endwith %}
{% endif %}
<!-- ── Entity List ── -->
<!-- ── Raw Data Rows ── -->
<div class="card">
<h3>👤 行列表 <span style="font-weight:400;font-size:0.8rem;color:#888;">{{ entities|length }} 条</span></h3>
<div id="entityList">
{% for e in entities %}
<div class="entity-card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span class="val">{{ e.entity_value }}</span>
<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>
</div>
{% if e.feature_json %}
<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>
<h3>📋 数据行 <span style="font-weight:400;font-size:0.8rem;color:#888;">{{ row_data|length }} 条</span></h3>
{% if row_data %}
<div style="overflow-x:auto;">
<table class="feat-table" style="font-size:0.75rem;">
<thead>
<tr>
<th>#</th>
{% for k, v in row_data.0.items %}
<th>{{ k }}</th>
{% 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>
</thead>
<tbody>
{% for row in row_data %}
<tr>
<td>{{ forloop.counter }}</td>
{% for k, v in row.items %}
<td><code>{{ v|default:"-" }}</code></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state"><p>该簇无数据行</p></div>
{% endif %}
</div>
{% empty %}
<div class="empty-state"><p>该簇无数据行</p></div>
{% endfor %}
</div>
</div>
{% endblock %}