| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- // 函数拟合工具 - 数据拟合模块
- // 页面加载完成后执行初始化
- document.addEventListener('DOMContentLoaded', function() {
- // 初始化数据拟合功能
- initDataFitting();
- // 初始化多项式次数控制
- const regressionType = document.getElementById('regression-type');
- const polynomialDegreeContainer = document.getElementById('polynomial-degree-container');
- regressionType.addEventListener('change', function() {
- if (this.value === 'polynomial') {
- polynomialDegreeContainer.style.display = 'inline-block';
- } else {
- polynomialDegreeContainer.style.display = 'none';
- }
- });
- });
- /**
- * 初始化数据拟合功能
- */
- function initDataFitting() {
- const fitButton = document.getElementById('fit-data');
- const fitType = document.getElementById('fit-type');
- fitButton.addEventListener('click', function() {
- // 获取数据点
- const fitTable = document.getElementById('fit-excel-table');
- const dataPoints = window.dataTable.getDataPointsFromExcelTable(fitTable);
- // 检查数据点是否足够
- if (dataPoints.length < 2) {
- alert('请至少输入两个有效的数据点');
- return;
- }
- // 根据选择的函数类型进行拟合
- const fitResult = fitData(dataPoints, fitType.value);
- // 显示拟合结果
- displayFitResult(fitResult, dataPoints);
- });
- }
- // 使用utils.js中的getDataPointsFromTable函数
- /**
- * 拟合数据
- * @param {Array} dataPoints - 数据点数组
- * @param {string} fitType - 拟合函数类型
- * @returns {Object} 拟合结果
- */
- function fitData(dataPoints, fitType) {
- // 提取x和y值数组
- const xValues = dataPoints.map(point => point.x);
- const yValues = dataPoints.map(point => point.y);
- let result = {
- fitType: fitType,
- equation: '',
- parameters: [],
- r2: 0,
- functionExpr: ''
- };
- try {
- switch (fitType) {
- case 'linear':
- result = fitLinear(xValues, yValues);
- break;
- case 'quadratic':
- result = fitPolynomial(xValues, yValues, 2);
- break;
- case 'cubic':
- result = fitPolynomial(xValues, yValues, 3);
- break;
- case 'exponential':
- result = fitExponential(xValues, yValues);
- break;
- case 'logarithmic':
- result = fitLogarithmic(xValues, yValues);
- break;
- case 'power':
- result = fitPower(xValues, yValues);
- break;
- default:
- throw new Error('不支持的拟合类型');
- }
- return result;
- } catch (error) {
- alert('拟合计算错误: ' + error.message);
- return null;
- }
- }
- /**
- * 线性拟合 (y = ax + b)
- * @param {Array} xValues - x值数组
- * @param {Array} yValues - y值数组
- * @returns {Object} 拟合结果
- */
- function fitLinear(xValues, yValues) {
- // 使用最小二乘法计算线性回归参数
- const n = xValues.length;
- // 计算各项和
- let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
- for (let i = 0; i < n; i++) {
- sumX += xValues[i];
- sumY += yValues[i];
- sumXY += xValues[i] * yValues[i];
- sumX2 += xValues[i] * xValues[i];
- }
- // 计算斜率和截距
- const a = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
- const b = (sumY - a * sumX) / n;
- // 计算拟合优度 R²
- const r2 = calculateRSquared(xValues, yValues, x => a * x + b);
- // 格式化方程
- const equation = `y = ${a.toFixed(4)}x ${b >= 0 ? '+ ' + b.toFixed(4) : '- ' + Math.abs(b).toFixed(4)}`;
- const functionExpr = `${a}*x ${b >= 0 ? '+ ' + b : '- ' + Math.abs(b)}`;
- return {
- fitType: 'linear',
- equation: equation,
- parameters: [a, b],
- r2: r2,
- functionExpr: functionExpr
- };
- }
- /**
- * 多项式拟合
- * @param {Array} xValues - x值数组
- * @param {Array} yValues - y值数组
- * @param {number} degree - 多项式次数
- * @returns {Object} 拟合结果
- */
- function fitPolynomial(xValues, yValues, degree) {
- // 使用math.js的多项式回归
- const coeffs = polyfit(xValues, yValues, degree);
- // 构建方程字符串
- let equation = 'y = ';
- let functionExpr = '';
- for (let i = 0; i <= degree; i++) {
- const power = degree - i;
- const coeff = coeffs[i];
- if (i > 0 && coeff >= 0) {
- equation += ' + ';
- functionExpr += ' + ';
- } else if (i > 0 && coeff < 0) {
- equation += ' - ';
- functionExpr += ' - ';
- }
- if (power === 0) {
- equation += `${Math.abs(coeff).toFixed(4)}`;
- functionExpr += `${Math.abs(coeff)}`;
- } else if (power === 1) {
- equation += `${Math.abs(coeff).toFixed(4)}x`;
- functionExpr += `${Math.abs(coeff)}*x`;
- } else {
- equation += `${Math.abs(coeff).toFixed(4)}x^${power}`;
- functionExpr += `${Math.abs(coeff)}*x^${power}`;
- }
- }
- // 计算拟合优度 R²
- const r2 = calculateRSquared(xValues, yValues, x => {
- let result = 0;
- for (let i = 0; i <= degree; i++) {
- result += coeffs[i] * Math.pow(x, degree - i);
- }
- return result;
- });
- // 计算RMSE
- const rmse = calculateRMSE(xValues, yValues, x => {
- let result = 0;
- for (let i = 0; i <= degree; i++) {
- result += coeffs[i] * Math.pow(x, degree - i);
- }
- return result;
- });
- return {
- fitType: degree === 2 ? 'quadratic' : 'cubic',
- equation: equation,
- parameters: coeffs,
- r2: r2,
- rmse: rmse,
- functionExpr: functionExpr
- };
- }
- /**
- * 指数拟合 (y = a*e^(bx))
- * @param {Array} xValues - x值数组
- * @param {Array} yValues - y值数组
- * @returns {Object} 拟合结果
- */
- function fitExponential(xValues, yValues) {
- // 检查y值是否都为正
- for (let i = 0; i < yValues.length; i++) {
- if (yValues[i] <= 0) {
- throw new Error('指数拟合要求所有y值必须为正数');
- }
- }
- // 对y取对数,转换为线性问题: ln(y) = ln(a) + bx
- const lnY = yValues.map(y => Math.log(y));
- // 使用线性拟合
- const linearFit = fitLinear(xValues, lnY);
- // 转换回指数参数
- const a = Math.exp(linearFit.parameters[1]);
- const b = linearFit.parameters[0];
- // 计算拟合优度 R²
- const r2 = calculateRSquared(xValues, yValues, x => a * Math.exp(b * x));
- // 格式化方程
- const equation = `y = ${a.toFixed(4)}e^(${b.toFixed(4)}x)`;
- const functionExpr = `${a}*e^(${b}*x)`;
- return {
- fitType: 'exponential',
- equation: equation,
- parameters: [a, b],
- r2: r2,
- functionExpr: functionExpr
- };
- }
- /**
- * 对数拟合 (y = a*ln(x) + b)
- * @param {Array} xValues - x值数组
- * @param {Array} yValues - y值数组
- * @returns {Object} 拟合结果
- */
- function fitLogarithmic(xValues, yValues) {
- // 检查x值是否都为正
- for (let i = 0; i < xValues.length; i++) {
- if (xValues[i] <= 0) {
- throw new Error('对数拟合要求所有x值必须为正数');
- }
- }
- // 对x取对数,转换为线性问题: y = a*ln(x) + b
- const lnX = xValues.map(x => Math.log(x));
- // 使用线性拟合
- const linearFit = fitLinear(lnX, yValues);
- // 获取参数
- const a = linearFit.parameters[0];
- const b = linearFit.parameters[1];
- // 计算拟合优度 R²
- const r2 = calculateRSquared(xValues, yValues, x => a * Math.log(x) + b);
- // 格式化方程
- const equation = `y = ${a.toFixed(4)}ln(x) ${b >= 0 ? '+ ' + b.toFixed(4) : '- ' + Math.abs(b).toFixed(4)}`;
- const functionExpr = `${a}*ln(x) ${b >= 0 ? '+ ' + b : '- ' + Math.abs(b)}`;
- return {
- fitType: 'logarithmic',
- equation: equation,
- parameters: [a, b],
- r2: r2,
- functionExpr: functionExpr
- };
- }
- /**
- * 幂函数拟合 (y = a*x^b)
- * @param {Array} xValues - x值数组
- * @param {Array} yValues - y值数组
- * @returns {Object} 拟合结果
- */
- function fitPower(xValues, yValues) {
- // 检查x和y值是否都为正
- for (let i = 0; i < xValues.length; i++) {
- if (xValues[i] <= 0 || yValues[i] <= 0) {
- throw new Error('幂函数拟合要求所有x和y值必须为正数');
- }
- }
- // 对x和y取对数,转换为线性问题: ln(y) = ln(a) + b*ln(x)
- const lnX = xValues.map(x => Math.log(x));
- const lnY = yValues.map(y => Math.log(y));
- // 使用线性拟合
- const linearFit = fitLinear(lnX, lnY);
- // 转换回幂函数参数
- const a = Math.exp(linearFit.parameters[1]);
- const b = linearFit.parameters[0];
- // 计算拟合优度 R²
- const r2 = calculateRSquared(xValues, yValues, x => a * Math.pow(x, b));
- // 格式化方程
- const equation = `y = ${a.toFixed(4)}x^${b.toFixed(4)}`;
- const functionExpr = `${a}*x^${b}`;
- return {
- fitType: 'power',
- equation: equation,
- parameters: [a, b],
- r2: r2,
- functionExpr: functionExpr
- };
- }
- // 使用utils.js中的calculateRSquared和calculateRMSE函数
- /**
- * 显示拟合结果
- * @param {Object} fitResult - 拟合结果
- * @param {Array} dataPoints - 数据点
- */
- function displayFitResult(fitResult, dataPoints) {
- if (!fitResult) return;
- // 显示拟合方程(使用MathJax渲染)
- const equationResult = document.getElementById('equation-result');
- // 根据拟合类型生成LaTeX格式的方程
- let latexEquation = '';
- switch(fitResult.fitType) {
- case 'linear':
- latexEquation = `y = ${formatNumber(fitResult.parameters[0])}x ${fitResult.parameters[1] >= 0 ? '+ ' + formatNumber(fitResult.parameters[1]) : '- ' + formatNumber(Math.abs(fitResult.parameters[1]))}`;
- break;
- case 'quadratic':
- latexEquation = `y = ${formatNumber(fitResult.parameters[0])}x^2 ${fitResult.parameters[1] >= 0 ? '+ ' + formatNumber(fitResult.parameters[1]) : '- ' + formatNumber(Math.abs(fitResult.parameters[1]))}x ${fitResult.parameters[2] >= 0 ? '+ ' + formatNumber(fitResult.parameters[2]) : '- ' + formatNumber(Math.abs(fitResult.parameters[2]))}`;
- break;
- case 'cubic':
- latexEquation = `y = ${formatNumber(fitResult.parameters[0])}x^3 ${fitResult.parameters[1] >= 0 ? '+ ' + formatNumber(fitResult.parameters[1]) : '- ' + formatNumber(Math.abs(fitResult.parameters[1]))}x^2 ${fitResult.parameters[2] >= 0 ? '+ ' + formatNumber(fitResult.parameters[2]) : '- ' + formatNumber(Math.abs(fitResult.parameters[2]))}x ${fitResult.parameters[3] >= 0 ? '+ ' + formatNumber(fitResult.parameters[3]) : '- ' + formatNumber(Math.abs(fitResult.parameters[3]))}`;
- break;
- case 'exponential':
- latexEquation = `y = ${formatNumber(fitResult.parameters[0])}e^{${formatNumber(fitResult.parameters[1])}x}`;
- break;
- case 'logarithmic':
- latexEquation = `y = ${formatNumber(fitResult.parameters[0])}\\ln(x) ${fitResult.parameters[1] >= 0 ? '+ ' + formatNumber(fitResult.parameters[1]) : '- ' + formatNumber(Math.abs(fitResult.parameters[1]))}`;
- break;
- case 'power':
- latexEquation = `y = ${formatNumber(fitResult.parameters[0])}x^{${formatNumber(fitResult.parameters[1])}}`;
- break;
- default:
- latexEquation = fitResult.equation;
- }
- // 使用MathJax渲染方程
- equationResult.innerHTML = `\\[${latexEquation}\\]`;
- if (window.MathJax) {
- MathJax.typeset([equationResult]);
- }
- // 使用generateStatsHTML生成统计信息
- const statsResult = document.getElementById('stats-result');
- // 计算RMSE(如果fitResult中没有)
- let rmse = 0;
- if (fitResult.rmse) {
- rmse = fitResult.rmse;
- } else {
- // 提取x和y值数组
- const xValues = dataPoints.map(point => point.x);
- const yValues = dataPoints.map(point => point.y);
- // 根据拟合类型创建预测函数
- let predictFn;
- switch(fitResult.fitType) {
- case 'linear':
- predictFn = x => fitResult.parameters[0] * x + fitResult.parameters[1];
- break;
- case 'quadratic':
- predictFn = x => fitResult.parameters[0] * x * x + fitResult.parameters[1] * x + fitResult.parameters[2];
- break;
- case 'cubic':
- predictFn = x => fitResult.parameters[0] * x * x * x + fitResult.parameters[1] * x * x + fitResult.parameters[2] * x + fitResult.parameters[3];
- break;
- case 'exponential':
- predictFn = x => fitResult.parameters[0] * Math.exp(fitResult.parameters[1] * x);
- break;
- case 'logarithmic':
- predictFn = x => fitResult.parameters[0] * Math.log(x) + fitResult.parameters[1];
- break;
- case 'power':
- predictFn = x => fitResult.parameters[0] * Math.pow(x, fitResult.parameters[1]);
- break;
- default:
- predictFn = x => 0;
- }
- rmse = calculateRMSE(xValues, yValues, predictFn);
- }
- // 生成统计HTML
- statsResult.innerHTML = generateStatsHTML({
- coefficients: fitResult.parameters,
- rSquared: fitResult.r2,
- rmse: rmse,
- formula: fitResult.fitType,
- dataPoints: dataPoints.length
- });
- // 绘制拟合曲线和数据点
- plotFitResult(fitResult, dataPoints);
- }
- /**
- * 绘制拟合结果
- * @param {Object} fitResult - 拟合结果
- * @param {Array} dataPoints - 数据点
- */
- function plotFitResult(fitResult, dataPoints) {
- // 提取数据点的x和y值
- const xData = dataPoints.map(point => point.x);
- const yData = dataPoints.map(point => point.y);
- // 找出x的最小值和最大值
- const xMin = Math.min(...xData);
- const xMax = Math.max(...xData);
- // 为了平滑曲线,生成更多的点
- const xRange = xMax - xMin;
- const xStart = xMin - xRange * 0.1;
- const xEnd = xMax + xRange * 0.1;
- const step = xRange / 100;
- // 生成拟合曲线的点
- const xFit = [];
- const yFit = [];
- try {
- // 编译拟合函数表达式
- const compiledFunction = math.compile(fitResult.functionExpr);
- for (let x = xStart; x <= xEnd; x += step) {
- try {
- // 跳过对数和幂函数的负值
- if ((fitResult.fitType === 'logarithmic' || fitResult.fitType === 'power') && x <= 0) {
- continue;
- }
- const y = compiledFunction.evaluate({x: x});
- if (!isNaN(y) && isFinite(y)) {
- xFit.push(x);
- yFit.push(y);
- }
- } catch (error) {
- continue;
- }
- }
- // 创建数据点散点图
- const dataTrace = {
- x: xData,
- y: yData,
- mode: 'markers',
- type: 'scatter',
- name: '数据点',
- marker: {
- size: 8,
- color: 'rgba(255, 0, 0, 0.7)'
- }
- };
- // 创建拟合曲线
- const fitTrace = {
- x: xFit,
- y: yFit,
- mode: 'lines',
- type: 'scatter',
- name: '拟合曲线',
- line: {
- color: 'blue',
- width: 2
- }
- };
- // 绘图布局
- const layout = {
- title: '数据拟合结果',
- xaxis: {
- title: 'X'
- },
- yaxis: {
- title: 'Y'
- },
- legend: {
- x: 0,
- y: 1
- }
- };
- // 绘制图表
- Plotly.newPlot('plot-area', [dataTrace, fitTrace], layout);
- } catch (error) {
- console.error('绘制拟合结果错误:', error);
- alert('绘制拟合结果错误: ' + error.message);
- }
- }
- /**
- * 多项式拟合函数
- * @param {Array} x - x值数组
- * @param {Array} y - y值数组
- * @param {number} degree - 多项式次数
- * @returns {Array} 多项式系数数组
- */
- function polyfit(x, y, degree) {
- // 构建范德蒙德矩阵
- const X = [];
- const n = x.length;
- for (let i = 0; i < n; i++) {
- X[i] = [];
- for (let j = 0; j <= degree; j++) {
- X[i][j] = Math.pow(x[i], degree - j);
- }
- }
- // 使用math.js的最小二乘法求解
- try {
- // 转换为math.js矩阵
- const Xmatrix = math.matrix(X);
- const Ymatrix = math.matrix(y);
- // 计算 (X^T * X)^-1 * X^T * Y
- const Xt = math.transpose(Xmatrix);
- const XtX = math.multiply(Xt, Xmatrix);
- const XtXinv = math.inv(XtX);
- const XtY = math.multiply(Xt, Ymatrix);
- const coeffs = math.multiply(XtXinv, XtY);
- // 转换回普通数组
- return Array.from(coeffs.valueOf());
- } catch (error) {
- console.error('多项式拟合计算错误:', error);
- throw new Error('多项式拟合计算失败');
- }
- }
|