277 lines
5.9 KiB
JavaScript
277 lines
5.9 KiB
JavaScript
/**
|
||
* 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
|
||
};
|