fix(cluster_detail): always persist per-row cluster labels to EntityProfile for data row display

This commit is contained in:
PM-pinou
2026-07-24 11:56:02 +08:00
parent b135b18779
commit d2e34ca43d
+40 -28
View File
@@ -359,6 +359,36 @@ def _run_clustering_pipeline(run, store, ds_id, feature_columns, algorithm,
except Exception as svd_err: except Exception as svd_err:
logger.warning(f'Cluster SVD extraction skipped: {svd_err}') logger.warning(f'Cluster SVD extraction skipped: {svd_err}')
# ── Step 2c: Save per-row cluster labels to EntityProfile ──
# Always persist row→cluster mapping so cluster_detail can show data rows
# even when UMAP is skipped or fails.
try:
cluster_entry_save = store.get_cluster_result(cluster_id)
labels_list_save = cluster_entry_save.get('labels', []) if cluster_entry_save else []
if labels_list_save:
from analysis.models import EntityProfile as EP_save
from analysis.models import ClusterResult as CR_save
# Delete stale EP records for this run first
EP_save.objects.filter(run=run).delete()
cr_cache_save = {}
row_profiles = []
for r_idx in range(len(labels_list_save)):
lbl = labels_list_save[r_idx]
ck = f'{run.id}_{lbl}'
if ck not in cr_cache_save:
cr_cache_save[ck] = CR_save.objects.filter(run=run, cluster_label=lbl).first()
row_profiles.append(EP_save(
entity_value=f'row_{r_idx}', run=run, cluster_label=lbl,
cluster=cr_cache_save[ck],
embedding_x=None, embedding_y=None, embedding_z=None,
))
if row_profiles:
EP_save.objects.bulk_create(row_profiles, ignore_conflicts=True, batch_size=1000)
run.entity_count = len(row_profiles)
run.save(update_fields=['entity_count'])
except Exception as save_err:
logger.warning(f'Row labels save skipped: {save_err}')
# ── Step 3: UMAP-3D embedding (2D fallback) ─────────────────────── # ── Step 3: UMAP-3D embedding (2D fallback) ───────────────────────
if run_umap: if run_umap:
try: try:
@@ -438,41 +468,23 @@ def _run_clustering_pipeline(run, store, ds_id, feature_columns, algorithm,
labels_list = cluster_entry.get('labels', []) if cluster_entry else [] labels_list = cluster_entry.get('labels', []) if cluster_entry else []
from analysis.models import EntityProfile as EP from analysis.models import EntityProfile as EP
from analysis.models import ClusterResult as CR
profiles = [] # Update existing EP records with UMAP coordinates
cr_cache = {}
for i in range(len(df_umap)): for i in range(len(df_umap)):
# Use row index as identifier — no entity column
ev = f'row_{i}' ev = f'row_{i}'
lbl = labels_list[i] if i < len(labels_list) else -1
cache_key = f'{run.id}_{lbl}'
if cache_key not in cr_cache:
cr_cache[cache_key] = CR.objects.filter(
run=run, cluster_label=lbl).first()
has_z = umap_components >= 3 and coords.shape[1] >= 3 has_z = umap_components >= 3 and coords.shape[1] >= 3
profiles.append(EP( update_fields = {
entity_value=ev, run=run, cluster_label=lbl, 'embedding_x': float(coords[i, 0]) if i < len(coords) else None,
cluster=cr_cache[cache_key], 'embedding_y': float(coords[i, 1]) if i < len(coords) else None,
embedding_x=float(coords[i, 0]) if i < len(coords) else None, }
embedding_y=float(coords[i, 1]) if i < len(coords) else None, if has_z:
embedding_z=float(coords[i, 2]) if has_z update_fields['embedding_z'] = float(coords[i, 2]) if i < len(coords) else None
and i < len(coords) else None, EP.objects.filter(run=run, entity_value=ev).update(**update_fields)
)) run.entity_count = len(df_umap)
if profiles: run.save(update_fields=['entity_count'])
EP.objects.bulk_create(profiles, ignore_conflicts=True, batch_size=1000)
run.entity_count = len(profiles)
run.save(update_fields=['entity_count'])
except Exception as umap_err: except Exception as umap_err:
logger.warning(f'UMAP embedding skipped: {umap_err}') logger.warning(f'UMAP embedding skipped: {umap_err}')
# Fallback: if entity_count not set (UMAP skipped), derive from cluster sizes
if run.entity_count is None:
from django.db.models import Sum
total = run.clusters.aggregate(total=Sum('size'))['total'] or 0
run.entity_count = total
run.save(update_fields=['entity_count'])
# ── Done ── # ── Done ──
run.status = 'completed' run.status = 'completed'
run.progress_pct = 100 run.progress_pct = 100