Files
tianxuan/templates/analysis/cluster_detail.html
T

142 lines
5.9 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}簇 #{{ cluster.cluster_label }} — 运行 #{{ run.display_id }}{% endblock %}
{% block content %}
<style>
.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; }
</style>
<div class="card">
<a href="{% url 'analysis:cluster_overview' run.display_id %}" style="color:#4361ee;text-decoration:none;font-size:0.9rem;">← 聚类概览</a>
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;margin-top:0.5rem;">
<div>
<h2 style="margin:0;">簇 #{{ cluster.cluster_label }}
{% if cluster.cluster_label == -1 %}<span class="badge badge-warning">噪声</span>{% endif %}
</h2>
<p style="margin:0.25rem 0 0;color:#666;font-size:0.9rem;">
{{ cluster.size }} 个行
{% if cluster.proportion %} — {{ cluster.proportion|floatformat:1 }}% 占总{% endif %}
{% if cluster.silhouette_score is not None %} — 轮廓系数: {{ cluster.silhouette_score|floatformat:4 }}{% endif %}
</p>
</div>
</div>
{% if nl_summary %}
<div style="background:#f8f9fa;border-left:3px solid #4361ee;padding:0.6rem;margin:0.75rem 0;border-radius:0 4px 4px 0;font-size:0.85rem;line-height:1.6;color:#333;">{{ nl_summary }}</div>
{% endif %}
</div>
<!-- ── Feature Analysis ── -->
<div class="card">
<h3>📊 特征分析 <span style="font-weight:400;font-size:0.8rem;color:#888;">{{ features|length }} 个特征</span></h3>
{% if features %}
<table class="feat-table">
<thead>
<tr>
<th>#</th>
<th>特征名</th>
<th>区分度</th>
<th>均值</th>
<th>标准差</th>
<th>中位数</th>
<th>P25</th>
<th>P75</th>
</tr>
</thead>
<tbody>
{% for f in features %}
<tr>
<td>{{ forloop.counter }}</td>
<td><code>{{ f.feature_name }}</code></td>
<td><span class="{% if f.distinguishing_score > 0 %}feat-positive{% else %}feat-negative{% endif %}">{{ f.distinguishing_score|floatformat:3 }}</span></td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.mean|floatformat:3|default:"-" }}</td>
<td>{{ f.std|floatformat:3|default:"-" }}</td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.median|floatformat:3|default:"-" }}</td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.p25|floatformat:3|default:"-" }}</td>
<td class="ts-val" data-col="{{ f.feature_name }}">{{ f.p75|floatformat:3|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state"><p>该簇无特征数据</p></div>
{% endif %}
</div>
<!-- ── SVD Singular Values ── -->
{% if cluster.cluster_label != -1 %}
{# SVD-extracted features (from _run_clustering_pipeline cluster_svd_extract) have higher distinguishing scores #}
{% with svd_features=features|slice:":5" %}
{% if svd_features %}
<div class="card">
<h3>🔬 SVD 关键特征 <span style="font-weight:400;font-size:0.8rem;color:#888;">Top 5 按区分度</span></h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;">
{% for f in svd_features %}
<div style="background:#f8f9fa;border-radius:6px;padding:0.5rem;border:1px solid #e0e0e0;">
<div style="font-weight:600;font-size:0.8rem;">{{ f.feature_name }}</div>
<div style="font-size:0.75rem;color:#666;margin-top:0.2rem;">
Score: <span class="{% if f.distinguishing_score > 0 %}feat-positive{% else %}feat-negative{% endif %}">{{ f.distinguishing_score|floatformat:2 }}</span>
| 均值: {{ f.mean|floatformat:2 }}
| 标准差: {{ f.std|floatformat:2 }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{% endif %}
<!-- ── Raw Data Rows ── -->
<div class="card">
<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 %}
</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 %}
<script>
(function(){
var tsCols=["timestamp","time","epoch","ts","8dbd","created_at","updated_at","event_time","datetime"];
document.querySelectorAll(".ts-val").forEach(function(el){
var col=el.dataset.col,isTs=false;
for(var i=0;i<tsCols.length;i++){if(col.toLowerCase().indexOf(tsCols[i])>=0){isTs=true;break;}}
if(!isTs)return;
var raw=parseFloat(el.textContent);
if(isNaN(raw)||raw<1e8||raw>2e12)return;
var d=new Date(raw*1000);
if(d.getFullYear()<2000||d.getFullYear()>2100)return;
el.textContent=d.toISOString().replace("T"," ").substring(0,16);
el.title="epoch: "+raw;
});
})();
</script>