142 lines
5.9 KiB
HTML
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>
|