- Add attendance-grade-correlation-card and data-access-correlation, correlation-compute - Add attendance-trend-chart and trend-compute for trend analysis - Add attendance-warnings-card and warning-compute for attendance warnings - Add attendance-report-print for printable reports - Add class-comparison-card for class attendance comparison - Add notifications and services directory
211 lines
6.0 KiB
TypeScript
211 lines
6.0 KiB
TypeScript
/**
|
||
* L-9 考勤与成绩关联分析:纯函数实现。
|
||
*
|
||
* 与 IO 解耦,便于单测。包含:
|
||
* - Pearson 相关系数计算
|
||
* - 风险等级分类
|
||
* - 相关系数解释
|
||
* - 班级汇总聚合
|
||
*
|
||
* 参考 warning-compute.ts / trend-compute.ts 的纯函数模式。
|
||
*/
|
||
|
||
import type {
|
||
AttendanceGradeCorrelationItem,
|
||
AttendanceGradeCorrelationSummary,
|
||
AttendanceGradeRiskLevel,
|
||
} from "./types"
|
||
|
||
/** 风险阈值:出勤率(百分比)。 */
|
||
export const RISK_ATTENDANCE_HIGH_THRESHOLD = 80
|
||
export const RISK_ATTENDANCE_MEDIUM_THRESHOLD = 90
|
||
|
||
/** 风险阈值:成绩(0-100 归一化分数)。 */
|
||
export const RISK_SCORE_HIGH_THRESHOLD = 60
|
||
export const RISK_SCORE_MEDIUM_THRESHOLD = 75
|
||
|
||
/** Pearson 相关系数解释阈值(绝对值)。 */
|
||
const CORRELATION_STRONG_THRESHOLD = 0.7
|
||
const CORRELATION_WEAK_THRESHOLD = 0.3
|
||
|
||
/** 相关系数解释类型(与 types.ts 中保持一致)。 */
|
||
export type CorrelationInterpretation =
|
||
| "strong_negative"
|
||
| "weak_negative"
|
||
| "negligible"
|
||
| "weak_positive"
|
||
| "strong_positive"
|
||
| "insufficient_data"
|
||
|
||
/**
|
||
* 计算 Pearson 相关系数。
|
||
*
|
||
* @param x 自变量数组(如出勤率)
|
||
* @param y 因变量数组(如平均成绩)
|
||
* @returns r ∈ [-1, +1];数据不足(<2 个点)或方差为 0 时返回 null
|
||
*/
|
||
export function computePearsonCorrelation(
|
||
x: readonly number[],
|
||
y: readonly number[]
|
||
): number | null {
|
||
if (x.length !== y.length) return null
|
||
if (x.length < 2) return null
|
||
|
||
const n = x.length
|
||
let sumX = 0
|
||
let sumY = 0
|
||
let sumXY = 0
|
||
let sumX2 = 0
|
||
let sumY2 = 0
|
||
|
||
for (let i = 0; i < n; i++) {
|
||
const xi = x[i]
|
||
const yi = y[i]
|
||
if (!Number.isFinite(xi) || !Number.isFinite(yi)) return null
|
||
sumX += xi
|
||
sumY += yi
|
||
sumXY += xi * yi
|
||
sumX2 += xi * xi
|
||
sumY2 += yi * yi
|
||
}
|
||
|
||
const numerator = n * sumXY - sumX * sumY
|
||
const denominator = Math.sqrt(
|
||
(n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)
|
||
)
|
||
|
||
if (denominator === 0) return null
|
||
// 限制到 [-1, 1] 防止浮点误差溢出
|
||
return Math.max(-1, Math.min(1, numerator / denominator))
|
||
}
|
||
|
||
/**
|
||
* 根据出勤率和平均成绩判定风险等级。
|
||
* - high:低出勤(<80%)且低分(<60%)
|
||
* - medium:中低出勤(<90%)且中低分(<75%),但未达 high
|
||
* - low:其余(正常)
|
||
*/
|
||
export function classifyRiskLevel(
|
||
attendanceRate: number,
|
||
averageScore: number
|
||
): AttendanceGradeRiskLevel {
|
||
const isLowAttendance = attendanceRate < RISK_ATTENDANCE_HIGH_THRESHOLD
|
||
const isMediumAttendance =
|
||
attendanceRate < RISK_ATTENDANCE_MEDIUM_THRESHOLD && !isLowAttendance
|
||
const isLowScore = averageScore < RISK_SCORE_HIGH_THRESHOLD
|
||
const isMediumScore =
|
||
averageScore < RISK_SCORE_MEDIUM_THRESHOLD && !isLowScore
|
||
|
||
if (isLowAttendance && isLowScore) return "high"
|
||
if ((isLowAttendance || isMediumAttendance) && (isLowScore || isMediumScore))
|
||
return "medium"
|
||
return "low"
|
||
}
|
||
|
||
/**
|
||
* 解释 Pearson 相关系数。
|
||
* - |r| >= 0.7:强相关
|
||
* - 0.3 <= |r| < 0.7:弱相关
|
||
* - |r| < 0.3:可忽略
|
||
*/
|
||
export function interpretCorrelation(
|
||
r: number | null
|
||
): CorrelationInterpretation {
|
||
if (r === null) return "insufficient_data"
|
||
const abs = Math.abs(r)
|
||
if (abs >= CORRELATION_STRONG_THRESHOLD) {
|
||
return r > 0 ? "strong_positive" : "strong_negative"
|
||
}
|
||
if (abs >= CORRELATION_WEAK_THRESHOLD) {
|
||
return r > 0 ? "weak_positive" : "weak_negative"
|
||
}
|
||
return "negligible"
|
||
}
|
||
|
||
/** 风险等级排序权重(用于降序排列:high > medium > low)。 */
|
||
const RISK_ORDER: Record<AttendanceGradeRiskLevel, number> = {
|
||
high: 0,
|
||
medium: 1,
|
||
low: 2,
|
||
}
|
||
|
||
/**
|
||
* 聚合班级考勤-成绩关联汇总(纯函数)。
|
||
*
|
||
* 输入:每个学生的原始数据(studentId, studentName, attendanceRate, averageScore, 计数)。
|
||
* 输出:含 Pearson 相关系数、风险分级、排序后的 items。
|
||
*
|
||
* 排除无考勤或无成绩记录的学生(视为数据不全,不参与关联分析)。
|
||
*/
|
||
export function computeCorrelationSummary(
|
||
classId: string,
|
||
className: string,
|
||
startDate: string,
|
||
endDate: string,
|
||
rawItems: ReadonlyArray<{
|
||
studentId: string
|
||
studentName: string
|
||
attendanceRate: number
|
||
attendanceRecordCount: number
|
||
absentCount: number
|
||
averageScore: number
|
||
gradeRecordCount: number
|
||
}>
|
||
): AttendanceGradeCorrelationSummary {
|
||
// 过滤掉无考勤或无成绩记录的学生
|
||
const validItems = rawItems.filter(
|
||
(it) => it.attendanceRecordCount > 0 && it.gradeRecordCount > 0
|
||
)
|
||
|
||
const items: AttendanceGradeCorrelationItem[] = validItems.map((it) => ({
|
||
studentId: it.studentId,
|
||
studentName: it.studentName,
|
||
attendanceRate: round2(it.attendanceRate),
|
||
attendanceRecordCount: it.attendanceRecordCount,
|
||
absentCount: it.absentCount,
|
||
averageScore: round2(it.averageScore),
|
||
gradeRecordCount: it.gradeRecordCount,
|
||
riskLevel: classifyRiskLevel(it.attendanceRate, it.averageScore),
|
||
}))
|
||
|
||
// 排序:风险等级降序 → 出勤率升序(高风险学生排在最前)
|
||
items.sort((a, b) => {
|
||
const riskDiff = RISK_ORDER[a.riskLevel] - RISK_ORDER[b.riskLevel]
|
||
if (riskDiff !== 0) return riskDiff
|
||
return a.attendanceRate - b.attendanceRate
|
||
})
|
||
|
||
// Pearson 相关系数:x=出勤率,y=平均成绩
|
||
const correlation = computePearsonCorrelation(
|
||
items.map((it) => it.attendanceRate),
|
||
items.map((it) => it.averageScore)
|
||
)
|
||
|
||
const riskCounts = {
|
||
high: items.filter((it) => it.riskLevel === "high").length,
|
||
medium: items.filter((it) => it.riskLevel === "medium").length,
|
||
low: items.filter((it) => it.riskLevel === "low").length,
|
||
}
|
||
|
||
return {
|
||
classId,
|
||
className,
|
||
startDate,
|
||
endDate,
|
||
items,
|
||
correlation: correlation !== null ? round4(correlation) : null,
|
||
correlationInterpretation: interpretCorrelation(correlation),
|
||
riskCounts,
|
||
}
|
||
}
|
||
|
||
/** 保留两位小数。 */
|
||
function round2(n: number): number {
|
||
return Math.round(n * 100) / 100
|
||
}
|
||
|
||
/** 保留四位小数(相关系数精度)。 */
|
||
function round4(n: number): number {
|
||
return Math.round(n * 10000) / 10000
|
||
}
|