Files
ZhiQiXiaoYuan/utils/gpaPredictor.js
ChuXun eaab9a762a 1
2025-10-19 20:28:31 +08:00

277 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* GPA预测算法 - 多项式回归
*/
/**
* 多项式回归预测
* @param {Array} data - 历史GPA数据 [{semester: '2023-1', gpa: 3.5}, ...]
* @param {Number} degree - 多项式阶数默认2(二次)
* @param {Number} future - 预测未来几个学期
* @returns {Object} 预测结果
*/
function polynomialRegression(data, degree = 2, future = 3) {
if (!data || data.length < 2) {
return {
predictions: [],
trend: 0,
confidence: 0
};
}
// 提取数据点
const x = data.map((_, index) => index); // 学期编号 [0, 1, 2, ...]
const y = data.map(item => item.gpa); // GPA值
// 构建范德蒙德矩阵并求解系数
const coefficients = fitPolynomial(x, y, degree);
// 预测未来学期
const predictions = [];
const startIndex = data.length;
for (let i = 0; i < future; i++) {
const xValue = startIndex + i;
const yValue = evaluatePolynomial(coefficients, xValue);
// 限制GPA在合理范围内 [0, 4.0]
const predictedGPA = Math.max(0, Math.min(4.0, yValue));
predictions.push({
semester: generateSemesterName(data.length + i + 1),
gpa: Number(predictedGPA.toFixed(2)),
isPrediction: true
});
}
// 计算趋势(与当前学期对比)
const currentGPA = y[y.length - 1];
const nextGPA = predictions[0].gpa;
const trend = ((nextGPA - currentGPA) / currentGPA * 100).toFixed(1);
// 计算置信度基于R²
const rSquared = calculateRSquared(x, y, coefficients);
const confidence = Math.round(rSquared * 100);
return {
predictions,
trend: Number(trend),
confidence,
coefficients
};
}
/**
* 拟合多项式 - 使用最小二乘法
*/
function fitPolynomial(x, y, degree) {
const n = x.length;
const m = degree + 1;
// 构建设计矩阵 X 和目标向量 Y
const X = [];
for (let i = 0; i < n; i++) {
const row = [];
for (let j = 0; j <= degree; j++) {
row.push(Math.pow(x[i], j));
}
X.push(row);
}
// 计算 X^T * X
const XTX = multiplyMatrices(transpose(X), X);
// 计算 X^T * Y
const XTY = multiplyMatrixVector(transpose(X), y);
// 求解方程 (X^T * X) * coefficients = X^T * Y
const coefficients = solveLinearSystem(XTX, XTY);
return coefficients;
}
/**
* 计算多项式值
*/
function evaluatePolynomial(coefficients, x) {
let result = 0;
for (let i = 0; i < coefficients.length; i++) {
result += coefficients[i] * Math.pow(x, i);
}
return result;
}
/**
* 计算R²决定系数
*/
function calculateRSquared(x, y, coefficients) {
const n = x.length;
const yMean = y.reduce((sum, val) => sum + val, 0) / n;
let ssTotal = 0;
let ssResidual = 0;
for (let i = 0; i < n; i++) {
const yPred = evaluatePolynomial(coefficients, x[i]);
ssTotal += Math.pow(y[i] - yMean, 2);
ssResidual += Math.pow(y[i] - yPred, 2);
}
return 1 - (ssResidual / ssTotal);
}
/**
* 矩阵转置
*/
function transpose(matrix) {
const rows = matrix.length;
const cols = matrix[0].length;
const result = [];
for (let j = 0; j < cols; j++) {
const row = [];
for (let i = 0; i < rows; i++) {
row.push(matrix[i][j]);
}
result.push(row);
}
return result;
}
/**
* 矩阵乘法
*/
function multiplyMatrices(a, b) {
const rowsA = a.length;
const colsA = a[0].length;
const colsB = b[0].length;
const result = [];
for (let i = 0; i < rowsA; i++) {
const row = [];
for (let j = 0; j < colsB; j++) {
let sum = 0;
for (let k = 0; k < colsA; k++) {
sum += a[i][k] * b[k][j];
}
row.push(sum);
}
result.push(row);
}
return result;
}
/**
* 矩阵向量乘法
*/
function multiplyMatrixVector(matrix, vector) {
const rows = matrix.length;
const result = [];
for (let i = 0; i < rows; i++) {
let sum = 0;
for (let j = 0; j < vector.length; j++) {
sum += matrix[i][j] * vector[j];
}
result.push(sum);
}
return result;
}
/**
* 求解线性方程组 - 高斯消元法
*/
function solveLinearSystem(A, b) {
const n = A.length;
const augmented = A.map((row, i) => [...row, b[i]]);
// 前向消元
for (let i = 0; i < n; i++) {
// 选主元
let maxRow = i;
for (let k = i + 1; k < n; k++) {
if (Math.abs(augmented[k][i]) > Math.abs(augmented[maxRow][i])) {
maxRow = k;
}
}
// 交换行
[augmented[i], augmented[maxRow]] = [augmented[maxRow], augmented[i]];
// 消元
for (let k = i + 1; k < n; k++) {
const factor = augmented[k][i] / augmented[i][i];
for (let j = i; j <= n; j++) {
augmented[k][j] -= factor * augmented[i][j];
}
}
}
// 回代
const x = new Array(n);
for (let i = n - 1; i >= 0; i--) {
x[i] = augmented[i][n];
for (let j = i + 1; j < n; j++) {
x[i] -= augmented[i][j] * x[j];
}
x[i] /= augmented[i][i];
}
return x;
}
/**
* 生成学期名称
*/
function generateSemesterName(index) {
const year = 2023 + Math.floor(index / 2);
const term = index % 2 === 1 ? '1' : '2';
return `${year}-${term}`;
}
/**
* 计算GPA根据分数
* @param {Number} score - 分数
* @returns {Number} GPA绩点
*/
function calculateGPA(score) {
if (score >= 60) {
return (score / 10 - 5);
}
return 0;
}
/**
* 批量计算GPA
*/
function batchCalculateGPA(courses) {
return courses.map(course => ({
...course,
gpa: calculateGPA(course.score)
}));
}
/**
* 计算平均GPA
*/
function calculateAverageGPA(courses) {
if (!courses || courses.length === 0) return 0;
const totalCredits = courses.reduce((sum, c) => sum + (c.credit || 1), 0);
const weightedSum = courses.reduce((sum, c) => {
const gpa = c.gpa || calculateGPA(c.score);
return sum + gpa * (c.credit || 1);
}, 0);
return (weightedSum / totalCredits).toFixed(2);
}
module.exports = {
polynomialRegression,
calculateGPA,
batchCalculateGPA,
calculateAverageGPA
};