fit.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. // 函数拟合工具 - 数据拟合模块
  2. // 页面加载完成后执行初始化
  3. document.addEventListener('DOMContentLoaded', function() {
  4. // 初始化数据拟合功能
  5. initDataFitting();
  6. // 初始化多项式次数控制
  7. const regressionType = document.getElementById('regression-type');
  8. const polynomialDegreeContainer = document.getElementById('polynomial-degree-container');
  9. regressionType.addEventListener('change', function() {
  10. if (this.value === 'polynomial') {
  11. polynomialDegreeContainer.style.display = 'inline-block';
  12. } else {
  13. polynomialDegreeContainer.style.display = 'none';
  14. }
  15. });
  16. });
  17. /**
  18. * 初始化数据拟合功能
  19. */
  20. function initDataFitting() {
  21. const fitButton = document.getElementById('fit-data');
  22. const fitType = document.getElementById('fit-type');
  23. fitButton.addEventListener('click', function() {
  24. // 获取数据点
  25. const fitTable = document.getElementById('fit-excel-table');
  26. const dataPoints = window.dataTable.getDataPointsFromExcelTable(fitTable);
  27. // 检查数据点是否足够
  28. if (dataPoints.length < 2) {
  29. alert('请至少输入两个有效的数据点');
  30. return;
  31. }
  32. // 根据选择的函数类型进行拟合
  33. const fitResult = fitData(dataPoints, fitType.value);
  34. // 显示拟合结果
  35. displayFitResult(fitResult, dataPoints);
  36. });
  37. }
  38. // 使用utils.js中的getDataPointsFromTable函数
  39. /**
  40. * 拟合数据
  41. * @param {Array} dataPoints - 数据点数组
  42. * @param {string} fitType - 拟合函数类型
  43. * @returns {Object} 拟合结果
  44. */
  45. function fitData(dataPoints, fitType) {
  46. // 提取x和y值数组
  47. const xValues = dataPoints.map(point => point.x);
  48. const yValues = dataPoints.map(point => point.y);
  49. let result = {
  50. fitType: fitType,
  51. equation: '',
  52. parameters: [],
  53. r2: 0,
  54. functionExpr: ''
  55. };
  56. try {
  57. switch (fitType) {
  58. case 'linear':
  59. result = fitLinear(xValues, yValues);
  60. break;
  61. case 'quadratic':
  62. result = fitPolynomial(xValues, yValues, 2);
  63. break;
  64. case 'cubic':
  65. result = fitPolynomial(xValues, yValues, 3);
  66. break;
  67. case 'exponential':
  68. result = fitExponential(xValues, yValues);
  69. break;
  70. case 'logarithmic':
  71. result = fitLogarithmic(xValues, yValues);
  72. break;
  73. case 'power':
  74. result = fitPower(xValues, yValues);
  75. break;
  76. default:
  77. throw new Error('不支持的拟合类型');
  78. }
  79. return result;
  80. } catch (error) {
  81. alert('拟合计算错误: ' + error.message);
  82. return null;
  83. }
  84. }
  85. /**
  86. * 线性拟合 (y = ax + b)
  87. * @param {Array} xValues - x值数组
  88. * @param {Array} yValues - y值数组
  89. * @returns {Object} 拟合结果
  90. */
  91. function fitLinear(xValues, yValues) {
  92. // 使用最小二乘法计算线性回归参数
  93. const n = xValues.length;
  94. // 计算各项和
  95. let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
  96. for (let i = 0; i < n; i++) {
  97. sumX += xValues[i];
  98. sumY += yValues[i];
  99. sumXY += xValues[i] * yValues[i];
  100. sumX2 += xValues[i] * xValues[i];
  101. }
  102. // 计算斜率和截距
  103. const a = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
  104. const b = (sumY - a * sumX) / n;
  105. // 计算拟合优度 R²
  106. const r2 = calculateRSquared(xValues, yValues, x => a * x + b);
  107. // 格式化方程
  108. const equation = `y = ${a.toFixed(4)}x ${b >= 0 ? '+ ' + b.toFixed(4) : '- ' + Math.abs(b).toFixed(4)}`;
  109. const functionExpr = `${a}*x ${b >= 0 ? '+ ' + b : '- ' + Math.abs(b)}`;
  110. return {
  111. fitType: 'linear',
  112. equation: equation,
  113. parameters: [a, b],
  114. r2: r2,
  115. functionExpr: functionExpr
  116. };
  117. }
  118. /**
  119. * 多项式拟合
  120. * @param {Array} xValues - x值数组
  121. * @param {Array} yValues - y值数组
  122. * @param {number} degree - 多项式次数
  123. * @returns {Object} 拟合结果
  124. */
  125. function fitPolynomial(xValues, yValues, degree) {
  126. // 使用math.js的多项式回归
  127. const coeffs = polyfit(xValues, yValues, degree);
  128. // 构建方程字符串
  129. let equation = 'y = ';
  130. let functionExpr = '';
  131. for (let i = 0; i <= degree; i++) {
  132. const power = degree - i;
  133. const coeff = coeffs[i];
  134. if (i > 0 && coeff >= 0) {
  135. equation += ' + ';
  136. functionExpr += ' + ';
  137. } else if (i > 0 && coeff < 0) {
  138. equation += ' - ';
  139. functionExpr += ' - ';
  140. }
  141. if (power === 0) {
  142. equation += `${Math.abs(coeff).toFixed(4)}`;
  143. functionExpr += `${Math.abs(coeff)}`;
  144. } else if (power === 1) {
  145. equation += `${Math.abs(coeff).toFixed(4)}x`;
  146. functionExpr += `${Math.abs(coeff)}*x`;
  147. } else {
  148. equation += `${Math.abs(coeff).toFixed(4)}x^${power}`;
  149. functionExpr += `${Math.abs(coeff)}*x^${power}`;
  150. }
  151. }
  152. // 计算拟合优度 R²
  153. const r2 = calculateRSquared(xValues, yValues, x => {
  154. let result = 0;
  155. for (let i = 0; i <= degree; i++) {
  156. result += coeffs[i] * Math.pow(x, degree - i);
  157. }
  158. return result;
  159. });
  160. // 计算RMSE
  161. const rmse = calculateRMSE(xValues, yValues, x => {
  162. let result = 0;
  163. for (let i = 0; i <= degree; i++) {
  164. result += coeffs[i] * Math.pow(x, degree - i);
  165. }
  166. return result;
  167. });
  168. return {
  169. fitType: degree === 2 ? 'quadratic' : 'cubic',
  170. equation: equation,
  171. parameters: coeffs,
  172. r2: r2,
  173. rmse: rmse,
  174. functionExpr: functionExpr
  175. };
  176. }
  177. /**
  178. * 指数拟合 (y = a*e^(bx))
  179. * @param {Array} xValues - x值数组
  180. * @param {Array} yValues - y值数组
  181. * @returns {Object} 拟合结果
  182. */
  183. function fitExponential(xValues, yValues) {
  184. // 检查y值是否都为正
  185. for (let i = 0; i < yValues.length; i++) {
  186. if (yValues[i] <= 0) {
  187. throw new Error('指数拟合要求所有y值必须为正数');
  188. }
  189. }
  190. // 对y取对数,转换为线性问题: ln(y) = ln(a) + bx
  191. const lnY = yValues.map(y => Math.log(y));
  192. // 使用线性拟合
  193. const linearFit = fitLinear(xValues, lnY);
  194. // 转换回指数参数
  195. const a = Math.exp(linearFit.parameters[1]);
  196. const b = linearFit.parameters[0];
  197. // 计算拟合优度 R²
  198. const r2 = calculateRSquared(xValues, yValues, x => a * Math.exp(b * x));
  199. // 格式化方程
  200. const equation = `y = ${a.toFixed(4)}e^(${b.toFixed(4)}x)`;
  201. const functionExpr = `${a}*e^(${b}*x)`;
  202. return {
  203. fitType: 'exponential',
  204. equation: equation,
  205. parameters: [a, b],
  206. r2: r2,
  207. functionExpr: functionExpr
  208. };
  209. }
  210. /**
  211. * 对数拟合 (y = a*ln(x) + b)
  212. * @param {Array} xValues - x值数组
  213. * @param {Array} yValues - y值数组
  214. * @returns {Object} 拟合结果
  215. */
  216. function fitLogarithmic(xValues, yValues) {
  217. // 检查x值是否都为正
  218. for (let i = 0; i < xValues.length; i++) {
  219. if (xValues[i] <= 0) {
  220. throw new Error('对数拟合要求所有x值必须为正数');
  221. }
  222. }
  223. // 对x取对数,转换为线性问题: y = a*ln(x) + b
  224. const lnX = xValues.map(x => Math.log(x));
  225. // 使用线性拟合
  226. const linearFit = fitLinear(lnX, yValues);
  227. // 获取参数
  228. const a = linearFit.parameters[0];
  229. const b = linearFit.parameters[1];
  230. // 计算拟合优度 R²
  231. const r2 = calculateRSquared(xValues, yValues, x => a * Math.log(x) + b);
  232. // 格式化方程
  233. const equation = `y = ${a.toFixed(4)}ln(x) ${b >= 0 ? '+ ' + b.toFixed(4) : '- ' + Math.abs(b).toFixed(4)}`;
  234. const functionExpr = `${a}*ln(x) ${b >= 0 ? '+ ' + b : '- ' + Math.abs(b)}`;
  235. return {
  236. fitType: 'logarithmic',
  237. equation: equation,
  238. parameters: [a, b],
  239. r2: r2,
  240. functionExpr: functionExpr
  241. };
  242. }
  243. /**
  244. * 幂函数拟合 (y = a*x^b)
  245. * @param {Array} xValues - x值数组
  246. * @param {Array} yValues - y值数组
  247. * @returns {Object} 拟合结果
  248. */
  249. function fitPower(xValues, yValues) {
  250. // 检查x和y值是否都为正
  251. for (let i = 0; i < xValues.length; i++) {
  252. if (xValues[i] <= 0 || yValues[i] <= 0) {
  253. throw new Error('幂函数拟合要求所有x和y值必须为正数');
  254. }
  255. }
  256. // 对x和y取对数,转换为线性问题: ln(y) = ln(a) + b*ln(x)
  257. const lnX = xValues.map(x => Math.log(x));
  258. const lnY = yValues.map(y => Math.log(y));
  259. // 使用线性拟合
  260. const linearFit = fitLinear(lnX, lnY);
  261. // 转换回幂函数参数
  262. const a = Math.exp(linearFit.parameters[1]);
  263. const b = linearFit.parameters[0];
  264. // 计算拟合优度 R²
  265. const r2 = calculateRSquared(xValues, yValues, x => a * Math.pow(x, b));
  266. // 格式化方程
  267. const equation = `y = ${a.toFixed(4)}x^${b.toFixed(4)}`;
  268. const functionExpr = `${a}*x^${b}`;
  269. return {
  270. fitType: 'power',
  271. equation: equation,
  272. parameters: [a, b],
  273. r2: r2,
  274. functionExpr: functionExpr
  275. };
  276. }
  277. // 使用utils.js中的calculateRSquared和calculateRMSE函数
  278. /**
  279. * 显示拟合结果
  280. * @param {Object} fitResult - 拟合结果
  281. * @param {Array} dataPoints - 数据点
  282. */
  283. function displayFitResult(fitResult, dataPoints) {
  284. if (!fitResult) return;
  285. // 显示拟合方程(使用MathJax渲染)
  286. const equationResult = document.getElementById('equation-result');
  287. // 根据拟合类型生成LaTeX格式的方程
  288. let latexEquation = '';
  289. switch(fitResult.fitType) {
  290. case 'linear':
  291. latexEquation = `y = ${formatNumber(fitResult.parameters[0])}x ${fitResult.parameters[1] >= 0 ? '+ ' + formatNumber(fitResult.parameters[1]) : '- ' + formatNumber(Math.abs(fitResult.parameters[1]))}`;
  292. break;
  293. case 'quadratic':
  294. 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]))}`;
  295. break;
  296. case 'cubic':
  297. 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]))}`;
  298. break;
  299. case 'exponential':
  300. latexEquation = `y = ${formatNumber(fitResult.parameters[0])}e^{${formatNumber(fitResult.parameters[1])}x}`;
  301. break;
  302. case 'logarithmic':
  303. latexEquation = `y = ${formatNumber(fitResult.parameters[0])}\\ln(x) ${fitResult.parameters[1] >= 0 ? '+ ' + formatNumber(fitResult.parameters[1]) : '- ' + formatNumber(Math.abs(fitResult.parameters[1]))}`;
  304. break;
  305. case 'power':
  306. latexEquation = `y = ${formatNumber(fitResult.parameters[0])}x^{${formatNumber(fitResult.parameters[1])}}`;
  307. break;
  308. default:
  309. latexEquation = fitResult.equation;
  310. }
  311. // 使用MathJax渲染方程
  312. equationResult.innerHTML = `\\[${latexEquation}\\]`;
  313. if (window.MathJax) {
  314. MathJax.typeset([equationResult]);
  315. }
  316. // 使用generateStatsHTML生成统计信息
  317. const statsResult = document.getElementById('stats-result');
  318. // 计算RMSE(如果fitResult中没有)
  319. let rmse = 0;
  320. if (fitResult.rmse) {
  321. rmse = fitResult.rmse;
  322. } else {
  323. // 提取x和y值数组
  324. const xValues = dataPoints.map(point => point.x);
  325. const yValues = dataPoints.map(point => point.y);
  326. // 根据拟合类型创建预测函数
  327. let predictFn;
  328. switch(fitResult.fitType) {
  329. case 'linear':
  330. predictFn = x => fitResult.parameters[0] * x + fitResult.parameters[1];
  331. break;
  332. case 'quadratic':
  333. predictFn = x => fitResult.parameters[0] * x * x + fitResult.parameters[1] * x + fitResult.parameters[2];
  334. break;
  335. case 'cubic':
  336. predictFn = x => fitResult.parameters[0] * x * x * x + fitResult.parameters[1] * x * x + fitResult.parameters[2] * x + fitResult.parameters[3];
  337. break;
  338. case 'exponential':
  339. predictFn = x => fitResult.parameters[0] * Math.exp(fitResult.parameters[1] * x);
  340. break;
  341. case 'logarithmic':
  342. predictFn = x => fitResult.parameters[0] * Math.log(x) + fitResult.parameters[1];
  343. break;
  344. case 'power':
  345. predictFn = x => fitResult.parameters[0] * Math.pow(x, fitResult.parameters[1]);
  346. break;
  347. default:
  348. predictFn = x => 0;
  349. }
  350. rmse = calculateRMSE(xValues, yValues, predictFn);
  351. }
  352. // 生成统计HTML
  353. statsResult.innerHTML = generateStatsHTML({
  354. coefficients: fitResult.parameters,
  355. rSquared: fitResult.r2,
  356. rmse: rmse,
  357. formula: fitResult.fitType,
  358. dataPoints: dataPoints.length
  359. });
  360. // 绘制拟合曲线和数据点
  361. plotFitResult(fitResult, dataPoints);
  362. }
  363. /**
  364. * 绘制拟合结果
  365. * @param {Object} fitResult - 拟合结果
  366. * @param {Array} dataPoints - 数据点
  367. */
  368. function plotFitResult(fitResult, dataPoints) {
  369. // 提取数据点的x和y值
  370. const xData = dataPoints.map(point => point.x);
  371. const yData = dataPoints.map(point => point.y);
  372. // 找出x的最小值和最大值
  373. const xMin = Math.min(...xData);
  374. const xMax = Math.max(...xData);
  375. // 为了平滑曲线,生成更多的点
  376. const xRange = xMax - xMin;
  377. const xStart = xMin - xRange * 0.1;
  378. const xEnd = xMax + xRange * 0.1;
  379. const step = xRange / 100;
  380. // 生成拟合曲线的点
  381. const xFit = [];
  382. const yFit = [];
  383. try {
  384. // 编译拟合函数表达式
  385. const compiledFunction = math.compile(fitResult.functionExpr);
  386. for (let x = xStart; x <= xEnd; x += step) {
  387. try {
  388. // 跳过对数和幂函数的负值
  389. if ((fitResult.fitType === 'logarithmic' || fitResult.fitType === 'power') && x <= 0) {
  390. continue;
  391. }
  392. const y = compiledFunction.evaluate({x: x});
  393. if (!isNaN(y) && isFinite(y)) {
  394. xFit.push(x);
  395. yFit.push(y);
  396. }
  397. } catch (error) {
  398. continue;
  399. }
  400. }
  401. // 创建数据点散点图
  402. const dataTrace = {
  403. x: xData,
  404. y: yData,
  405. mode: 'markers',
  406. type: 'scatter',
  407. name: '数据点',
  408. marker: {
  409. size: 8,
  410. color: 'rgba(255, 0, 0, 0.7)'
  411. }
  412. };
  413. // 创建拟合曲线
  414. const fitTrace = {
  415. x: xFit,
  416. y: yFit,
  417. mode: 'lines',
  418. type: 'scatter',
  419. name: '拟合曲线',
  420. line: {
  421. color: 'blue',
  422. width: 2
  423. }
  424. };
  425. // 绘图布局
  426. const layout = {
  427. title: '数据拟合结果',
  428. xaxis: {
  429. title: 'X'
  430. },
  431. yaxis: {
  432. title: 'Y'
  433. },
  434. legend: {
  435. x: 0,
  436. y: 1
  437. }
  438. };
  439. // 绘制图表
  440. Plotly.newPlot('plot-area', [dataTrace, fitTrace], layout);
  441. } catch (error) {
  442. console.error('绘制拟合结果错误:', error);
  443. alert('绘制拟合结果错误: ' + error.message);
  444. }
  445. }
  446. /**
  447. * 多项式拟合函数
  448. * @param {Array} x - x值数组
  449. * @param {Array} y - y值数组
  450. * @param {number} degree - 多项式次数
  451. * @returns {Array} 多项式系数数组
  452. */
  453. function polyfit(x, y, degree) {
  454. // 构建范德蒙德矩阵
  455. const X = [];
  456. const n = x.length;
  457. for (let i = 0; i < n; i++) {
  458. X[i] = [];
  459. for (let j = 0; j <= degree; j++) {
  460. X[i][j] = Math.pow(x[i], degree - j);
  461. }
  462. }
  463. // 使用math.js的最小二乘法求解
  464. try {
  465. // 转换为math.js矩阵
  466. const Xmatrix = math.matrix(X);
  467. const Ymatrix = math.matrix(y);
  468. // 计算 (X^T * X)^-1 * X^T * Y
  469. const Xt = math.transpose(Xmatrix);
  470. const XtX = math.multiply(Xt, Xmatrix);
  471. const XtXinv = math.inv(XtX);
  472. const XtY = math.multiply(Xt, Ymatrix);
  473. const coeffs = math.multiply(XtXinv, XtY);
  474. // 转换回普通数组
  475. return Array.from(coeffs.valueOf());
  476. } catch (error) {
  477. console.error('多项式拟合计算错误:', error);
  478. throw new Error('多项式拟合计算失败');
  479. }
  480. }