fix(cluster_detail): always persist per-row cluster labels to EntityProfile for data row display
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user