在这里插入图片描述

在接口耗时、请求大小、任务执行时长等场景中,我们经常需要回答这样的问题:

这批数据的整体分布是偏集中还是偏长尾?
典型请求“中位数”表现如何?有没有 P95/P99 被少数极慢请求拖高?
有没有明显的离群值(outlier)需要单独关注?

本案例基于 Kotlin Multiplatform(KMP)与 OpenHarmony,实现了一个分位数与箱线图五数概括分析器

  • 对输入的数值序列进行排序与插值计算;
  • 输出 Min / Q1 / Median / Q3 / Max 五数概括;
  • 计算常见分位数 P90 / P95 / P99;
  • 基于箱线图 IQR 规则检测上下侧离群值;
  • 通过 ArkTS 单页面展示完整分析报告,帮助你快速理解分布形态与长尾风险。

一、问题背景与典型场景

在 AIOps 与性能分析中,平均值往往是不够的。典型场景包括:

  1. 接口耗时分布分析
    SLO 可能要求“95% 请求耗时低于 200ms”,此时 P95 比平均值更关键;极少数 P99/P99.9 请求是否超时,决定体验的稳定性。

  2. 请求体大小与流量分布
    需要知道大部分请求体是否集中在一个区间,还是存在少数“超大请求”拖高整体资源消耗。

  3. 批处理任务耗时诊断
    一批任务的执行时间差异很大,需要判断是“整体偏慢”还是“少数极慢任务”导致整体指标异常。

  4. 数据质量与异常值检测
    对指标做箱线图分析,可以快速标记超出正常范围的异常值,为后续告警与根因分析提供线索。

这些问题可以抽象为:

给定数值样本序列 \( x_0, x_1, \dots, x_{n-1} \),
计算五数概括 (Min, Q1, Median, Q3, Max) 以及若干分位数 (P90, P95, P99),
并根据 IQR 规则检测异常离群值。


二、Kotlin 分位数与箱线图分析引擎

1. 输入格式设计

为与前几个案例保持一致,本案例继续使用简单的文本配置输入:

series=120,95,180,210,160,300,240

也支持直接裸写一行或多行数值序列,例如:

120,95,180,210,160,300,240
80,90,110

解析逻辑会尝试从每一行中按 , / 空格 / ; / 换行拆分并转换为 Double


2. Kotlin 分析主入口

App.kt 中,我们定义了对外暴露的分析函数,并通过 @JsExport 让 OpenHarmony 端可以直接调用:

@JsExport
fun percentileBoxplotAnalyzer(inputData: String): String {
    val sanitized = inputData.trim()
    if (sanitized.isEmpty()) {
        return "❌ 输入为空,请按 series=120,95,180,... 形式提供数据"
    }

    val lines = sanitized.lines()
        .map { it.trim() }
        .filter { it.isNotEmpty() }

    val values = mutableListOf<Double>()
    for (line in lines) {
        if (line.startsWith("series=", ignoreCase = true)) {
            val parsed = line.substringAfter("=")
                .split(",", " ", ";", "\n")
                .mapNotNull { it.trim().takeIf { s -> s.isNotEmpty() }?.toDoubleOrNull() }
            values += parsed
        } else {
            // 也允许直接裸序列行
            val parsed = line.split(",", " ", ";", "\n")
                .mapNotNull { it.trim().takeIf { s -> s.isNotEmpty() }?.toDoubleOrNull() }
            values += parsed
        }
    }

    if (values.isEmpty()) {
        return "❌ 未解析到任何数值,请检查 series=120,95,180,... 的格式是否正确"
    }

    val sorted = values.sorted()
    val n = sorted.size

    fun percentile(p: Double): Double {
        if (n == 1) return sorted[0]
        val rank = p * (n - 1)
        val low = rank.toInt()
        val high = min(low + 1, n - 1)
        val frac = rank - low
        return sorted[low] * (1 - frac) + sorted[high] * frac
    }

    val minValue = sorted.first()
    val maxValue = sorted.last()
    val median = percentile(0.5)
    val q1 = percentile(0.25)
    val q3 = percentile(0.75)
    val p90 = percentile(0.9)
    val p95 = percentile(0.95)
    val p99 = percentile(0.99)
    val iqr = q3 - q1
    val lowerFence = q1 - 1.5 * iqr
    val upperFence = q3 + 1.5 * iqr

    val lowerOutliers = sorted.filter { it < lowerFence }
    val upperOutliers = sorted.filter { it > upperFence }

    val builder = StringBuilder()
    builder.appendLine("📦 分位数与箱线图五数概括分析报告")
    builder.appendLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
    builder.appendLine("样本数量: $n")
    builder.appendLine("最小值 (Min): $minValue")
    builder.appendLine("Q1 (25% 分位): ${round2(q1)}")
    builder.appendLine("中位数 (P50): ${round2(median)}")
    builder.appendLine("Q3 (75% 分位): ${round2(q3)}")
    builder.appendLine("最大值 (Max): $maxValue")
    builder.appendLine("IQR (四分位距): ${round2(iqr)}")
    builder.appendLine()

    builder.appendLine("🎯 典型分位数")
    builder.appendLine("P90: ${round2(p90)}")
    builder.appendLine("P95: ${round2(p95)}")
    builder.appendLine("P99: ${round2(p99)}")
    builder.appendLine()

    builder.appendLine("🚩 箱线图离群值检测")
    builder.appendLine("下边界阈值 (Q1 - 1.5 * IQR): ${round2(lowerFence)}")
    builder.appendLine("上边界阈值 (Q3 + 1.5 * IQR): ${round2(upperFence)}")
    builder.appendLine()

    if (lowerOutliers.isEmpty() && upperOutliers.isEmpty()) {
        builder.appendLine("✅ 未检测到明显离群值,分布相对集中")
    } else {
        if (lowerOutliers.isNotEmpty()) {
            builder.appendLine("⬇ 下侧离群值 (${lowerOutliers.size} 个): ${lowerOutliers.joinToString()}")
        }
        if (upperOutliers.isNotEmpty()) {
            builder.appendLine("⬆ 上侧离群值 (${upperOutliers.size} 个): ${upperOutliers.joinToString()}")
        }
    }

    builder.appendLine()
    builder.appendLine("🧠 工程化解读")
    builder.appendLine("- 通过五数概括 (Min/Q1/Median/Q3/Max) 可以快速把握整体分布轮廓;")
    builder.appendLine("- P90/P95/P99 有助于评估接口 SLO 是否容易被长尾拖累;")
    builder.appendLine("- 利用箱线图离群值规则,可以快速标出“极慢请求”或“异常数据点”,用于告警与优化。")

    return builder.toString().trim()
}

这里采用了较为常见的“线性插值分位数”实现方式:

  • 先对样本 sorted(),得到有序序列;
  • 对于目标分位数 \( p \),计算秩值 \( r = p \cdot (n-1) \),在相邻两个点之间做线性插值;
  • 同一实现复用到 P50 / P90 / P95 / P99 以及 Q1/Q3 上,保证行为一致。

离群值检测则基于经典箱线图规则:

  • 计算 IQR = Q3 - Q1;
  • 下边界 = Q1 - 1.5 * IQR,上边界 = Q3 + 1.5 * IQR;
  • 小于下边界或大于上边界的样本视为“潜在离群值”,分别归类为下侧/上侧离群。

三、OpenHarmony 侧调用与 UI 展示思路

在 ArkTS 页面中,可以像之前案例一样从 hellokjs 导入该分析函数:

import { percentileBoxplotAnalyzer } from './hellokjs'

页面只需要维护一个样本输入状态,例如:

  • seriesInput: "120,95,180,210,160,300,240"

调用时自动加上 series= 前缀(如果用户没有写):

const seriesLine = this.seriesInput.includes('series=') ? this.seriesInput : `series=${this.seriesInput}`
const payload = seriesLine
this.result = percentileBoxplotAnalyzer(payload)

展示区域可以使用等宽字体 Text 直接呈现 Kotlin 返回的多行报告,在 UI 上重点突出:

  • 五数概括部分(Min/Q1/Median/Q3/Max);
  • P90/P95/P99 行;
  • “箱线图离群值检测”下的告警行(使用颜色或图标增强可读性)。

如果需要进一步可视化,可以在 ArkTS 侧根据这些数值画出简化版“水平箱线图”,例如用若干 Row + Flex 模拟刻度条。


四、复杂度与工程实践建议

当前实现的主要开销来自排序:

  • 排序时间复杂度:\( O(n \log n) \);
  • 之后的分位数和 IQR 计算都是 \( O(1) \) 或 \( O(n) \) 级别;
  • 整体复杂度约为 \( O(n \log n) \),在大多数终端侧场景下足够使用。

在工程实践中,可以考虑以下扩展:

  1. 在线分位数估计
    对于极大规模或流式数据,可以引入 P² 算法或 t-digest 等近似分位数算法,在不保存全部样本的前提下估计 P95/P99。

  2. 预聚合 + 分位数分析
    先按接口 / 服务 / 租户等维度聚合统计,再对每个维度的样本做分位数 / 箱线图分析,构建多维度 SLO 健康度看板。

  3. 与 TopK / 阈值 / 滑动窗口联动
    将本案例与前几个案例组合:

    • 对“滑动窗口峰值序列”做分位数分析;
    • 对“TopK 峰值样本”的局部分布做箱线图概览;
    • 在阈值附近窗口中单独计算 P95/P99 评估风险。
  4. 端侧离线诊断
    利用 KMP + OpenHarmony 的跨端特性,将这套分位数分析逻辑下沉到终端设备,在离线或弱网环境中依然能完成基本的性能分布诊断。

通过这个分位数与箱线图五数概括案例,你可以把统计学中的基础工具直接落地到接口耗时分析、容量规划与异常检测等工程场景中,
在保持实现简洁的前提下,大幅提升对“长尾”和“离群值”的洞察能力。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐