HarmonyOS问卷引擎连载07:分组匿名统计与小样本隐藏【共创稿事节】
课程评价问卷的提交链路已经能够检查动态必填、评分取值和权重规则。新的问题出现在结果页:某门课程的A班收到了十二份有效评价,B班只有三份。如果两个班级都显示精确平均分、各星级人数甚至逐题分布,人数较少的班级很容易通过已知信息猜出个别填写者的评价。简单地把姓名字段从结果表删掉,仍不能解决小样本统计的识别风险。
上一节交付的是“这份答案能否参与评分”的摘要,本节沿用其最终有效提交记录,构建只面向聚合结果的统计层。最基础的显示规则是:某组有效样本数未达到业务设定的阈值时,公开页面隐藏该组均值与分布。阈值在示例中取五份,它是产品展示策略,不是系统权限;管理员和面向公众的报告应使用不同的数据投影,界面隐藏字段也不能替代服务端访问控制。

统计入口只接受有效提交
前六篇依次完成了条件跳题、依赖拓扑、旧草稿迁移、动态必填、缺答定位和评分规则拦截。这个顺序非常重要:统计不应重新猜测某道题当时是否可见,也不应直接遍历尚未提交的草稿。评分服务先根据发布版本核定答案,产出固定的分数和提交状态;统计层接收的是已经通过业务校验的记录。
目录在原工程基础上增加model/ScoreRecord.ets、service/GroupAggregator.ets、service/ResultProjection.ets以及pages/ReportPage.ets。前者描述数据契约,聚合器做纯计算,结果投影决定哪些字段可以进入展示页面。这样教学负责人调整最小样本数、增加统计分组,或者为平板改造列表布局时,原始提交记录与算法不会被页面逻辑意外修改。
对课程评价而言,分组维度可以是班级、课程或教学环节,但每次统计必须先限定同一个问卷版本、同一个有效时间窗口。混合不同版本的评分规则会让平均分失去可比性;若新版将某道满意度题的权重改动,不能仅凭“满分都是五分”就把旧版和新版结果并列计算。本文选择固定的surveyVersion=3作为统计口径,分数组件使用上一节已经核准的normalizedScore。
数据模型同时保存提交身份和版本
同一份结果可能因为网络重试重复到达汇总层,所以需要稳定的提交标识。若后端允许撤回或更正提交,聚合前应按submissionId选择修订号最大的记录,然后再过滤状态与分数。只按数组长度计数,会让重复请求看起来像新增了受访者。下面的数据结构刻意把组ID与提交身份分开,UI拿到的结果模型中不携带填写者的姓名、联系方式或单题正文。
// model/ScoreRecord.ets
export interface ScoreRecord {
submissionId: string;
revision: number;
groupId: string;
surveyVersion: number;
status: 'ACCEPTED' | 'REJECTED';
normalizedScore: number;
}
export interface GroupReport {
groupId: string;
validCount: number;
visible: boolean;
mean: number | null;
histogram: number[];
}
export function isAccepted(record: ScoreRecord, version: number): boolean {
return record.status === 'ACCEPTED' &&
record.surveyVersion === version &&
record.groupId.length > 0 &&
Number.isFinite(record.normalizedScore) &&
record.normalizedScore >= 1 && record.normalizedScore <= 5;
}
normalizedScore在本例中约定为一至五分的已核准数值。与题目评分相关的计算、无效选项拦截和权重校验仍属于上一篇的提交层,统计层不能悄悄修正明显越界的分数,否则同一份提交在不同页面上可能出现两种结果。统计层遇到不符合契约的记录,应把它放入待核对范围,并记录对应提交ID,而不是把异常值当作零分拉低平均数。
同一组内可能出现多次改动。为了避免用户的旧分数和新分数都进入汇总,先按提交ID收敛,再取最近一次修订。聚合的顺序固定为去重、选版本、核定有效性、分组、计算、投影。阈值判定必须放在有效样本过滤之后,不能先用总提交数决定是否显示结果。
用单次聚合计算均值与分布
统计页需要总评分和星级分布两个结果,但不应为每个指标各遍历整张提交表。聚合时将符合口径的分数按组累加,再得到均值和直方图。为避免人为误判,下面把样本阈值写成参数;UI与服务端都应使用同一份配置。分布按四舍五入至一到五星的示例口径计数,平均分仍使用原始核准得分计算,两种口径在报告说明中分别标注。
// service/GroupAggregator.ets
import { ScoreRecord, GroupReport, isAccepted } from '../model/ScoreRecord';
export function aggregateGroups(input: ScoreRecord[],
version: number, minCount: number): GroupReport[] {
if (!Number.isInteger(minCount) || minCount < 2) {
throw new Error('样本阈值配置有误');
}
const latest = new Map<string, ScoreRecord>();
for (const record of input) {
const old = latest.get(record.submissionId);
if (!old || record.revision > old.revision) {
latest.set(record.submissionId, record);
}
}
const buckets = new Map<string, number[]>();
for (const record of latest.values()) {
if (!isAccepted(record, version)) {
continue;
}
const values = buckets.get(record.groupId) ?? [];
values.push(record.normalizedScore);
buckets.set(record.groupId, values);
}
const reports: GroupReport[] = [];
for (const [groupId, values] of buckets.entries()) {
const visible = values.length >= minCount;
const sum = values.reduce((total: number, n: number) => total + n, 0);
const histogram: number[] = [0, 0, 0, 0, 0];
if (visible) {
for (const value of values) {
const star = Math.max(1, Math.min(5, Math.round(value)));
histogram[star - 1] += 1;
}
}
reports.push({ groupId, validCount: values.length, visible,
mean: visible ? sum / values.length : null, histogram });
}
return reports.sort((a: GroupReport, b: GroupReport) =>
a.groupId.localeCompare(b.groupId));
}
用十二份有效记录计算A班,假设均值为4.2,报告可展示平均分与五档分布。B班只有三份,即使这三份分数都有效,公开报告也应把均值和直方图置为不可展示状态。这里保留validCount给管理端核查;对普通查看者,可以进一步只显示“样本不足”,而不暴露精确人数。隐藏是数据投影的一部分,不应仅靠把文字颜色调浅或遮盖图表实现。
同一班级如果没有符合条件的提交,聚合器不会自然产生该组记录。页面如果需要保留完整的班级目录,可以用课程主数据与聚合结果做左连接,对缺失组生成validCount=0的空报告。不要把零份样本当作均值为零,也不要把图表中的五个空柱解释成“每个评分档位都无人选择”:这类状态属于无数据,而不是观众给了低分。
服务端投影与页面状态分层
业务端应保留一个独立的结果投影函数,面向公开报告只输出允许展示的字段。这样在页面缓存、导出或分享统计摘要时,都会取到同一份受控数据。管理端可以查看有效记录数帮助排查,但也需要明确角色授权,避免通过不断调整班级筛选条件反推出单份反馈。下面给出一个简洁的投影对象,保留canShow而不把所有内部细节传给界面。
// service/ResultProjection.ets
import { GroupReport } from '../model/ScoreRecord';
export interface PublicGroupRow {
groupId: string;
label: string;
canShow: boolean;
meanText: string;
histogram: number[];
}
export function toPublicRows(reports: GroupReport[]): PublicGroupRow[] {
return reports.map((report: GroupReport) => ({
groupId: report.groupId,
label: report.visible ? '已满足展示条件' : '样本不足',
canShow: report.visible,
meanText: report.visible && report.mean !== null
? report.mean.toFixed(1) : '—',
histogram: report.visible ? report.histogram.slice() : []
}));
}
小样本阈值不是一项可以任意放宽的页面开关。多维交叉分组,例如“某班级+某课程+某个学习小组”,可能使原本满足要求的十二人群组被切成多个三人组。还要防止通过“全班结果减去某小组结果”的差分查询推算人数过少的单元。实际运营中应限制细分层级,统一统计周期,对敏感分组采用更高阈值,并为导出功能设置独立的访问控制与审计。
ArkUI 列表只消费聚合行
页面不应接触原始提交记录,而应从受控的统计服务取得PublicGroupRow[]。用@State保存可显示行,以固定groupId作为ForEach键,动态更新统计周期时列表的身份仍稳定。对于可展示组渲染均值,对于不足阈值的组显示占位文案;不能在后者的调试信息或可复制文本中残留被隐藏的分数。下例省略了图表绘制,只保留状态分支和主要字段。
// pages/ReportPage.ets 的页面主体
import { PublicGroupRow } from '../service/ResultProjection';
@Entry
@Component
struct ReportPage {
@State rows: PublicGroupRow[] = [];
@State loading: boolean = false;
@State errorText: string = '';
build() {
Column({ space: 12 }) {
Text('课程评价统计').fontSize(22).fontWeight(FontWeight.Bold)
if (this.loading) {
Text('正在载入汇总结果')
}
if (this.errorText.length > 0) {
Text(this.errorText).fontColor('#B33434')
}
List({ space: 12 }) {
ForEach(this.rows, (row: PublicGroupRow) => {
ListItem() {
Column({ space: 8 }) {
Text(row.groupId).fontSize(19)
Text(row.canShow ? `平均分 ${row.meanText}` : row.label)
.fontColor(row.canShow ? '#166AC6' : '#916B29')
}.padding(16).width('100%')
}
}, (row: PublicGroupRow) => row.groupId)
}.layoutWeight(1)
}.padding(16).height('100%')
}
}


管理端可以同时看到A班十二份有效记录和B班三份有效记录,以核对阈值是否正确应用;面向填写者的统计页面则显示A班均值与分布,对B班仅给出“样本不足”。这样的权限差异应由数据接口返回的投影模型决定,不能让普通查看者只要更改页面状态便看到被保护的字段。
回归路径从规则版本到统计投影
排查“分组人数比提交页多”时,先核对submissionId去重是否在有效性过滤之前执行,随后检查被更正记录的修订号与最终状态。排查“平均分突然变化”时,先确认查询窗口和问卷版本是否被混合,再比较上一节输出的核准评分规则版本。排查“隐藏组仍有柱状图”时,重点查看接口响应里是否已经剔除直方图,而不是只看页面是否隐藏组件。
可以准备包含重复提交、撤回更正、规则版本不一致、评分越界与小样本组的输入集合,逐项核对计算结果。对A班构造十二条有效评分,B班构造三条有效评分,按阈值五计算时,A班可以展示,B班返回空分布。再把B班增加到五条,检查统计投影是否由隐藏切换为显示。公开接口的监控字段使用组数、阈值和统计窗口即可,避免记录原始评论全文或单份评分。
官方资料可参考HarmonyOS文档中心、ArkTS列表与分组列表和https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-watch@Watch状态观察。示例的数据聚合、最小展示阈值及公共投影均属于应用业务逻辑,ArkUI承担列表呈现与状态响应;选用不同的SDK时,应在对应版本的文档中复核接口可用情况。
到这里,问卷的完整链路从条件题的答案有效性延伸到了结果可见性:提交前确认每条记录的评分合法,提交后在统一版本与口径下聚合,再依据样本阈值生成受控的统计页面。下一篇可以围绕跨设备结果页布局与统计快照缓存继续完善,避免大屏与手机端分别计算出不一致的报告。
更多推荐

所有评论(0)