DolphinDB 无监督聚类实战:工况自动识别与设备群体分群
摘要
工业时序平台上跑的分析,绝大多数是有监督的:告警有阈值、评分有规则、寿命预测有历史故障当标签——问题定义清楚,答案对错可查。
但现场还有一类问题,压根没人打过标签:
"这台设备现在是正常运转、待机,还是在空转?"——没人时刻记录工况,但这个状态决定了能耗是否在白白流走。
"厂里这两百台设备,哪几台'性格'相同?"——想给同类设备建立对比基线,但"同类"没人定义过。
这类问题没法写规则(写规则的前提是你知道答案长什么样),只能靠无监督****聚类:让数据自己说话,把相似的聚到一起,结构浮出来之后人再来命名。说得更直白些:有监督回答"我问什么你答什么",无监督回答"我还不知道该问什么,先帮我看看数据里有什么结构"。
本文用 DolphinDB 的窗口特征 +
kmeans把无监督分析落地到两层场景:单台设备的工况自动分段(运行/待机/空转的时间线)和全厂设备的群体分群(同型组与离群设备发现)。核心观点是:聚类算法本身十分钟讲完,功夫全在特征指纹的设计和聚类结果的工程化使用上。
特别说明:本文基于个人在 IoT 场景下的设计与使用经验,涉及的方法与判断来自有限测试,不代表官方或专业结论。
一、没有标签的问题
先把有监督和无监督的分界说清楚,再谈怎么用。
1.1 两种问题的分界
| 有监督 | 无监督 | |
| 典型问题 | 振动超没超标?还能用几天? | 现在是什么工况?哪些设备相似? |
| 前提 | 有明确标准(阈值/历史标签) | 没人给过答案 |
| 输出 | 判定值 / 预测值 | 簇结构(人再命名) |
| 验证方式 | 对已知答案 | 对业务合理性(解读) |
分界很实用:能写出"什么算对"的问题用规则或监督模型;写不出的,才考虑聚类。这也是聚类容易被滥用的原因——它不需要标签,什么都不给也能跑出结果,但结果有没有意义,全看特征设计。
1.2 本文的两层场景
-
时间维度:工况自动分段。单台设备的时间线切成一段一段的"状态"(运行 / 待机 / 空转),没有人盯着的工况记录,从数据里自动长出来
-
设备维度:群体分群。全厂设备按运行"指纹"自动分成同型组,组内互为基线,组外即离群
两层用的是同一套工具箱(窗口特征 + 标准化 + kmeans),只是聚类的对象不同——一个是时间窗,一个是设备。
二、特征准备:给"状态"造指纹
聚类是按特征空间的距离聚的,特征长什么样,簇就是什么样。这是无监督分析里花心思最多的环节。
2.1 工况指纹:短窗特征
判断"这 15 分钟是什么工况",不看单个点,看一段窗口内的统计形态:
// 每 15 分钟一个窗口,算工况指纹
winFeat = select
deviceId,
bar(ts, 15m) as window,
avg(power) as p_mean, // 平均功率
std(power) as p_std, // 功率波动
sum(iif(power < 5.0, 1, 0)) \ count(*) as low_ratio, // 低功率占比(待机嫌疑)
sum(iif(power > 72.0, 1, 0)) \ count(*) as high_ratio // 满载占比(额定 80kW 的 90%)
from loadTable("dfs://iot", "power")
where ts between datetime(2026.03.01) : datetime(2026.03.31)
group by deviceId, bar(ts, 15m)
四个特征的设计意图:正常运转的特征是"中等功率、波动平稳";待机是"低功率、低占比变化";空转/间歇是"中低功率、波动大"。每个特征都要有物理含义——聚类完解读簇的时候,全靠这些物理含义说话。
2.2 标准化:聚类前的必做动作
四个特征的量纲完全不同(功率几十,占比零点几),直接聚类的话距离被大量纲的特征主导,小量纲特征形同虚设。标准化成 z-score,各特征等权参与:
// 特征标准化:减均值除标准差
feats = select
(p_mean - avg(p_mean)) \ std(p_mean) as z_pmean,
(p_std - avg(p_std)) \ std(p_std) as z_pstd,
(low_ratio - avg(low_ratio)) \ std(low_ratio) as z_low,
(high_ratio- avg(high_ratio))\ std(high_ratio) as z_high
from winFeat
2.3 设备指纹:长周期特征
设备分群用更长周期的"性格"统计——日均功率、波动、待机占比、负载率,一台设备一行:
// 设备指纹:3 月整月的运行性格
profile = select deviceId,
avg(power) as avg_power,
std(power) as power_std,
sum(iif(power < 5.0, 1, 0)) \ count(*) as idle_ratio,
avg(power) \ 80.0 as load_factor // 负载率(额定 80kW)
from loadTable("dfs://iot", "power")
where ts between datetime(2026.03.01) : datetime(2026.03.31)
group by deviceId
三、第一层:工况自动分段(时间维度)
3.1 聚类:kmeans 三步走
把标准化特征喂给 kmeans,聚成三类:
// 特征矩阵 + kmeans(k=3,固定随机种子保证可复现)
X = matrix(feats`z_pmean, feats`z_pstd, feats`z_low, feats`z_high)
kmodel = kmeans(X, 3, 10, 0.01, 42)
// 结果包含:每个样本的簇号 + 各簇质心
// 把簇号拼回窗口表
taggedWin = select deviceId, window, p_mean, p_std, low_ratio,
kmodel 中的簇号列 as cluster
from winFeat
到这里算法部分就结束了——十分钟的事。真正的功夫在下一步。
3.2 解读:簇号变工况名
kmeans 只会告诉你"簇 0/1/2",不会告诉你哪个是运行。簇的语义靠人解读:算每个簇在原始特征上的均值,对着物理含义命名。
// 各簇的原始特征均值——解读的依据
select cluster,
count(*) as win_count,
avg(p_mean) as avg_power,
avg(p_std) as avg_fluct,
avg(low_ratio) as avg_low
from taggedWin
group by cluster
假想解读结果:
| 簇 | 平均功率 | 波动 | 低功率占比 | 人工命名 |
| 0 | 68 kW | 3.1 | 0.02 | RUNNING(稳定运行) |
| 1 | 2.4 kW | 0.8 | 0.97 | IDLE(待机) |
| 2 | 35 kW | 19.5 | 0.41 | CYCLING(间歇/空转嫌疑) |
命名的纪律:命名写进一张映射表,后续所有分析引用映射表,而不是把"簇 2 = 空转"硬编码进查询——下次重训聚类,簇号会变,映射表一改全链路生效。
3.3 从分段到经营指标
工况时间线出来后,最有价值的一条统计是空转/间歇时长——设备开着电耗着,但没在干活:
// 空转时长与估算浪费电量(间歇段功率均值 × 时长)
select deviceId,
count(*) * 15 as cycling_minutes,
sum(p_mean) * 0.25 as wasted_kwh // 每窗 0.25 小时
from taggedWin
where cluster = 2
group by deviceId
order by wasted_kwh desc
按浪费电量排序的设备清单,就是现场治理的第一张工单。注意这里的"空转"是嫌疑清单不是定罪——间歇工况也可能是正常的工艺节拍(批次生产本来就是间歇的),清单交给工艺的人裁决。聚类给坐标,人给结论。
3.4 k 怎么选与工况漂移
两个绕不开的工程问题:
-
k 的选择:理论上可以算轮廓系数、画肘部图,但工程上更务实的做法是在 2~5 之间试,看哪个 k 的簇解读起来物理上最通顺。k 不是算出来的,是"解读得动"的那个
-
工况漂移:设备老化、季节变化都会让工况分布移动。指纹窗口特征要定期重算重训(比如每月),并把新旧工况命名做一次对照——簇号变了不要紧,映射表保证语义连续
四、第二层:设备群体分群(设备维度)
4.1 同型群的发现
设备指纹表(一台设备一行)同样标准化后聚类:
// 设备分群:按运行性格聚成同型组
Xp = matrix(stdFeats`z_avgpower, stdFeats`z_powerstd,
stdFeats`z_idleratio, stdFeats`z_loadfactor)
pModel = kmeans(Xp, 4, 10, 0.01, 42)
// 拼回指纹表:每台设备一个同型组号
groupedDev = select deviceId, avg_power, idle_ratio, load_factor,
簇号 as peerGroup
from profile
解读同型组比解读工况更容易——比如可能得到:"第 2 组 = 高负载连续运转的重型泵群"、"第 3 组 = 低负载高待机的备机群"。同型组是数据定义的,不是型号表定义的:同型号的设备可能因为安装位置、工艺位置不同而"性格"迥异,分群比型号表更贴近实际运行。
4.2 同型群的两个用法
用法一:组内基线。 能耗、待机率、负载率,都应该和同型组比而不是和全厂比——重型泵群和备机群比待机率没有意义。同型组的均值就是天然基线:
// 组内对比:偏离同型组均值的设备
select d.deviceId, d.peerGroup,
d.idle_ratio,
g.group_avg_idle,
d.idle_ratio \ g.group_avg_idle as ratio_to_group
from groupedDev as d
inner join (
select peerGroup, avg(idle_ratio) as group_avg_idle
from groupedDev group by peerGroup
) as g on d.peerGroup = g.peerGroup
where d.idle_ratio > g.group_avg_idle * 1.5
用法二:离群发现。 一台设备到它所属簇质心的距离就是离群得分——距离越远,"性格"越不合群。离群设备值得看两眼:可能是工况特殊(合理),也可能是故障早期(设备的"性格变化"先于参数越限):
// 离群得分 = 标准化特征到所属簇质心的欧氏距离
// score 大的设备 = 行为偏离同型群
4.3 新设备归群
新设备接入后跑一段时间,算出它的指纹,套用现有质心就近归群——不用重跑聚类,模型复用。归群后的新设备立刻享受组内基线的待遇,无需冷启动。
五、工程细节与坑
5.1 标准化不做,聚类白做
再说一遍因为它最重要:量纲差异会让距离计算完全被大量纲特征绑架。z-score 标准化是聚类的入场券,不是可选步骤。标准化的均值和标准差要用全量数据算(不是逐行各自算),并保存下来——新样本归群时要用同一套标准化参数。
5.2 聚类结果不稳定,纪律来兜底
kmeans 对初始质心敏感,重跑可能得到不同的簇号排列。两个纪律:固定随机种子保证可复现;簇语义走映射表而不是硬编码簇号,簇号怎么变都不影响下游。
5.3 特征比算法重要十倍
kmeans 换成别的聚类算法,结果差异通常远小于特征设计的差异。一个自检方法:解读簇的时候如果说不清"这一簇为什么聚在一起",问题几乎总在特征——要么物理含义不清,要么混进了无关特征(比如把设备编号当特征喂进去)。
5.4 聚类不取代规则
工况分段出来后,有人会问"那还写什么运行状态判断规则?"。答案是互补:规则适合明确的、稳定的判定(成本低、可解释、实时);聚类适合规则写不出来的模糊地带(发现结构、建基线组)。工程上常见组合:聚类离线发现结构 → 结构转成规则参数 → 规则上线跑实时。聚类是规则的上游,不是对手。
5.5 簇是快照,会过期
指纹来自一段时间的数据,设备会换工况、会老化、会大修。分群结果要有"生成日期",定期重训,重要决策(比如基线对比)别拿半年前的分群当依据。
六、写在最后
平台上的有监督分析——告警、评分、预测——都在回答已经定义好的问题。无监督分析是另一只眼睛:回答还没被定义的问题。
本文两层场景,一句话各自收拢:
-
工况自动分段:短窗指纹 + kmeans + 人工命名映射表,把没人记录的工况从数据里挖出来,空转嫌疑清单直接换成电费语言
-
设备群体分群:运行性格指纹聚成同型组,组内基线替代"全厂一刀切"的对比,离群设备是故障的早期线索
四个值得带走的工程判断:
-
特征设计决定一切:每个特征都要有物理含义,解读簇的时候全靠它
-
标准化是入场券:不做的聚类是大量纲特征的独角戏
-
簇语义走映射表:算法给簇号,人给名字,名字进配置不进代码
-
聚类给坐标,人给结论:空转是嫌疑不是定罪,离群是线索不是故障
最后回到那句分界:有监督回答"我问什么你答什么",无监督回答"我还不知道该问什么"。两者交替使用——聚类发现的结构,转成规则和标签,喂给下一轮有监督分析。数据平台的分析能力,就是这样一圈一圈长出来的。
参考链接:
DolphinDB 官网:DolphinDB 智能数据平台
DolphinDB 官方文档:DolphinDB 技术文档

浙公网安备 33010602011771号