fix(cluster_detail): show raw data rows instead of entity profiles
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="empty-state"><p>该簇无数据行</p></div>
|
||||
{% endfor %}
|
||||
</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>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user