regression.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. // 函数拟合工具 - 回归分析模块
  2. // 页面加载完成后执行初始化
  3. document.addEventListener('DOMContentLoaded', function() {
  4. // 初始化回归分析功能
  5. initRegressionAnalysis();
  6. // 初始化回归分析表格
  7. initRegressionTable();
  8. // 初始化输入模式切换
  9. initInputModeToggle();
  10. });
  11. /**
  12. * 初始化回归分析功能
  13. */
  14. function initRegressionAnalysis() {
  15. const runButton = document.getElementById('run-regression');
  16. const regressionType = document.getElementById('regression-type');
  17. const polynomialDegree = document.getElementById('polynomial-degree');
  18. const regressionData = document.getElementById('regression-data');
  19. const tableInputMode = document.getElementById('table-input-mode');
  20. const csvInputMode = document.getElementById('csv-input-mode');
  21. // 监听回归类型变化,显示/隐藏多项式次数选择
  22. regressionType.addEventListener('change', function() {
  23. const degreeContainer = document.getElementById('polynomial-degree-container');
  24. if (this.value === 'polynomial') {
  25. degreeContainer.style.display = 'block';
  26. } else {
  27. degreeContainer.style.display = 'none';
  28. }
  29. });
  30. runButton.addEventListener('click', function() {
  31. let data;
  32. // 根据当前激活的输入模式获取数据
  33. if (tableInputMode.classList.contains('active')) {
  34. // 从表格获取数据
  35. data = getDataFromTable();
  36. } else if (csvInputMode.classList.contains('active')) {
  37. // 从CSV文本框获取数据
  38. data = parseCSVData(regressionData.value);
  39. }
  40. // 检查数据是否有效
  41. if (!data || data.X.length === 0) {
  42. alert('请输入有效的数据');
  43. return;
  44. }
  45. // 检查数据点是否足够
  46. if (data.X.length < 3) {
  47. alert('请至少输入三个有效的数据点');
  48. return;
  49. }
  50. // 根据选择的回归类型进行分析
  51. let result;
  52. if (regressionType.value === 'linear') {
  53. result = runMultipleLinearRegression(data.X, data.y);
  54. } else if (regressionType.value === 'polynomial') {
  55. const degree = parseInt(polynomialDegree.value);
  56. result = runPolynomialRegression(data.X, data.y, degree);
  57. }
  58. // 显示回归结果
  59. displayRegressionResult(result, data);
  60. });
  61. }
  62. /**
  63. * 解析CSV格式的数据
  64. * @param {string} csvText - CSV格式的文本数据
  65. * @returns {Object} 解析后的数据对象,包含X(自变量矩阵)和y(因变量向量)
  66. */
  67. function parseCSVData(csvText) {
  68. if (!csvText.trim()) {
  69. return null;
  70. }
  71. try {
  72. // 按行分割
  73. const lines = csvText.trim().split('\n');
  74. // 检查是否有足够的行
  75. if (lines.length < 2) {
  76. throw new Error('数据行数不足');
  77. }
  78. // 解析数据
  79. const X = [];
  80. const y = [];
  81. for (let i = 0; i < lines.length; i++) {
  82. const values = lines[i].split(',').map(val => parseFloat(val.trim()));
  83. // 检查是否所有值都是有效数字
  84. if (values.some(isNaN)) {
  85. continue; // 跳过包含非数字的行
  86. }
  87. // 最后一个值作为因变量y,其余作为自变量X
  88. if (values.length >= 2) {
  89. X.push(values.slice(0, -1));
  90. y.push(values[values.length - 1]);
  91. }
  92. }
  93. return { X, y };
  94. } catch (error) {
  95. console.error('解析CSV数据错误:', error);
  96. alert('解析CSV数据错误: ' + error.message);
  97. return null;
  98. }
  99. }
  100. /**
  101. * 从表格中获取回归数据
  102. * @returns {Object} 数据对象,包含X(自变量矩阵)和y(因变量向量)
  103. */
  104. function getDataFromTable() {
  105. try {
  106. const table = document.getElementById('regression-excel-table');
  107. if (!table) {
  108. throw new Error('找不到回归分析表格');
  109. }
  110. const rows = table.querySelectorAll('.excel-row');
  111. if (rows.length === 0) {
  112. throw new Error('表格中没有数据行');
  113. }
  114. const X = [];
  115. const y = [];
  116. rows.forEach(row => {
  117. const cells = row.querySelectorAll('.excel-cell');
  118. if (cells.length < 2) {
  119. return; // 跳过不完整的行
  120. }
  121. // 获取X值(除最后一列外的所有列)
  122. const xValues = [];
  123. let allValid = true;
  124. // 遍历除最后一列外的所有列作为X值
  125. for (let i = 0; i < cells.length - 1; i++) {
  126. const value = parseFloat(cells[i].textContent.trim());
  127. if (isNaN(value)) {
  128. allValid = false;
  129. break;
  130. }
  131. xValues.push(value);
  132. }
  133. // 获取Y值(最后一列)
  134. const yValue = parseFloat(cells[cells.length - 1].textContent.trim());
  135. if (isNaN(yValue)) {
  136. allValid = false;
  137. }
  138. // 如果所有值都有效,则添加到数据集
  139. if (allValid) {
  140. X.push(xValues);
  141. y.push(yValue);
  142. }
  143. });
  144. if (X.length === 0) {
  145. throw new Error('没有找到有效的数据点');
  146. }
  147. return { X, y };
  148. } catch (error) {
  149. console.error('从表格获取数据错误:', error);
  150. alert('从表格获取数据错误: ' + error.message);
  151. return null;
  152. }
  153. }
  154. /**
  155. * 初始化回归分析表格
  156. */
  157. function initRegressionTable() {
  158. // 获取表格元素
  159. const regressionTable = document.getElementById('regression-table');
  160. const tableBody = regressionTable.querySelector('tbody');
  161. // 绑定添加行按钮事件
  162. const addRowBtn = document.getElementById('add-regression-row');
  163. if (addRowBtn) {
  164. addRowBtn.addEventListener('click', function() {
  165. addRegressionTableRow(tableBody);
  166. updateRegressionRowNumbers();
  167. });
  168. }
  169. // 绑定添加变量按钮事件
  170. const addColumnBtn = document.getElementById('add-regression-column');
  171. if (addColumnBtn) {
  172. addColumnBtn.addEventListener('click', function() {
  173. addRegressionTableColumn(regressionTable);
  174. });
  175. }
  176. // 绑定已有的删除按钮事件
  177. const removeButtons = regressionTable.querySelectorAll('.remove-row');
  178. removeButtons.forEach(button => {
  179. button.addEventListener('click', function() {
  180. const row = this.closest('tr');
  181. if (row) {
  182. tableBody.removeChild(row);
  183. updateRegressionRowNumbers();
  184. }
  185. });
  186. });
  187. }
  188. /**
  189. * 添加回归表格行
  190. * @param {HTMLElement} tableBody - 表格体元素
  191. */
  192. function addRegressionTableRow(tableBody) {
  193. const newRow = document.createElement('tr');
  194. const headerRow = document.querySelector('#regression-table thead tr');
  195. const numColumns = headerRow.children.length;
  196. // 创建序号单元格
  197. const indexCell = document.createElement('td');
  198. indexCell.className = 'row-index';
  199. indexCell.textContent = tableBody.children.length + 1;
  200. newRow.appendChild(indexCell);
  201. // 创建X输入单元格(根据当前表头数量)
  202. for (let i = 1; i < numColumns - 2; i++) { // 减去序号、Y和操作列
  203. const xCell = document.createElement('td');
  204. const xInput = document.createElement('input');
  205. xInput.type = 'number';
  206. xInput.step = 'any';
  207. xInput.className = `x${i}-value`;
  208. xCell.appendChild(xInput);
  209. newRow.appendChild(xCell);
  210. }
  211. // 创建Y输入单元格
  212. const yCell = document.createElement('td');
  213. const yInput = document.createElement('input');
  214. yInput.type = 'number';
  215. yInput.step = 'any';
  216. yInput.className = 'y-value';
  217. yCell.appendChild(yInput);
  218. newRow.appendChild(yCell);
  219. // 创建操作单元格(删除按钮)
  220. const actionCell = document.createElement('td');
  221. const removeBtn = document.createElement('button');
  222. removeBtn.textContent = '删除';
  223. removeBtn.className = 'remove-row';
  224. removeBtn.addEventListener('click', function() {
  225. tableBody.removeChild(newRow);
  226. updateRegressionRowNumbers();
  227. });
  228. actionCell.appendChild(removeBtn);
  229. newRow.appendChild(actionCell);
  230. // 将行添加到表格
  231. tableBody.appendChild(newRow);
  232. }
  233. /**
  234. * 添加回归表格列(变量)
  235. * @param {HTMLElement} table - 表格元素
  236. */
  237. function addRegressionTableColumn(table) {
  238. const headerRow = table.querySelector('thead tr');
  239. const bodyRows = table.querySelectorAll('tbody tr');
  240. // 计算新变量的索引(减去序号、Y和操作列)
  241. const varIndex = headerRow.children.length - 2;
  242. // 添加表头
  243. const newHeader = document.createElement('th');
  244. newHeader.className = 'sortable';
  245. newHeader.textContent = `X${varIndex}`;
  246. // 在Y列之前插入新列
  247. const yHeader = headerRow.children[headerRow.children.length - 2];
  248. headerRow.insertBefore(newHeader, yHeader);
  249. // 为每一行添加新的输入单元格
  250. bodyRows.forEach(row => {
  251. const newCell = document.createElement('td');
  252. const newInput = document.createElement('input');
  253. newInput.type = 'number';
  254. newInput.step = 'any';
  255. newInput.className = `x${varIndex}-value`;
  256. newCell.appendChild(newInput);
  257. // 在Y单元格之前插入新单元格
  258. const yCell = row.children[row.children.length - 2];
  259. row.insertBefore(newCell, yCell);
  260. });
  261. }
  262. /**
  263. * 更新回归表格行号
  264. */
  265. function updateRegressionRowNumbers() {
  266. const rows = document.querySelectorAll('#regression-table tbody tr');
  267. rows.forEach((row, index) => {
  268. const indexCell = row.querySelector('td:first-child');
  269. if (indexCell) {
  270. indexCell.textContent = index + 1;
  271. }
  272. });
  273. }
  274. /**
  275. * 初始化输入模式切换
  276. */
  277. function initInputModeToggle() {
  278. const tableModeBtn = document.getElementById('table-mode-btn');
  279. const csvModeBtn = document.getElementById('csv-mode-btn');
  280. const tableInputMode = document.getElementById('table-input-mode');
  281. const csvInputMode = document.getElementById('csv-input-mode');
  282. tableModeBtn.addEventListener('click', function() {
  283. // 切换按钮状态
  284. tableModeBtn.classList.add('active');
  285. csvModeBtn.classList.remove('active');
  286. // 切换输入模式
  287. tableInputMode.classList.add('active');
  288. csvInputMode.classList.remove('active');
  289. });
  290. csvModeBtn.addEventListener('click', function() {
  291. // 切换按钮状态
  292. csvModeBtn.classList.add('active');
  293. tableModeBtn.classList.remove('active');
  294. // 切换输入模式
  295. csvInputMode.classList.add('active');
  296. tableInputMode.classList.remove('active');
  297. });
  298. }
  299. /**
  300. * 运行多元线性回归
  301. * @param {Array} X - 自变量矩阵,每行是一个数据点,每列是一个特征
  302. * @param {Array} y - 因变量向量
  303. * @returns {Object} 回归结果
  304. */
  305. function runMultipleLinearRegression(X, y) {
  306. try {
  307. // 添加常数项(截距)
  308. const Xwith1 = X.map(row => [1, ...row]);
  309. // 转换为math.js矩阵
  310. const Xmatrix = math.matrix(Xwith1);
  311. const ymatrix = math.matrix(y);
  312. // 计算回归系数: β = (X'X)^(-1)X'y
  313. const Xt = math.transpose(Xmatrix);
  314. const XtX = math.multiply(Xt, Xmatrix);
  315. const XtXinv = math.inv(XtX);
  316. const Xty = math.multiply(Xt, ymatrix);
  317. const beta = math.multiply(XtXinv, Xty);
  318. // 计算拟合值
  319. const yFit = math.multiply(Xmatrix, beta);
  320. // 计算R²和调整后的R²
  321. const stats = calculateRegressionStats(y, yFit.valueOf(), Xwith1[0].length - 1);
  322. // 构建回归方程
  323. const equation = buildLinearRegressionEquation(beta.valueOf(), X[0].length);
  324. return {
  325. type: 'linear',
  326. equation: equation,
  327. coefficients: beta.valueOf(),
  328. r2: stats.r2,
  329. adjustedR2: stats.adjustedR2,
  330. numVars: X[0].length
  331. };
  332. } catch (error) {
  333. console.error('多元线性回归计算错误:', error);
  334. alert('多元线性回归计算错误: ' + error.message);
  335. return null;
  336. }
  337. }
  338. /**
  339. * 运行多项式回归
  340. * @param {Array} X - 自变量矩阵,每行是一个数据点,每列是一个特征
  341. * @param {Array} y - 因变量向量
  342. * @param {number} degree - 多项式次数
  343. * @returns {Object} 回归结果
  344. */
  345. function runPolynomialRegression(X, y, degree) {
  346. try {
  347. // 检查是否只有一个自变量
  348. if (X[0].length > 1) {
  349. throw new Error('多项式回归目前只支持一个自变量');
  350. }
  351. // 提取单个自变量
  352. const x = X.map(row => row[0]);
  353. // 创建多项式特征矩阵
  354. const polyX = [];
  355. for (let i = 0; i < x.length; i++) {
  356. const row = [1]; // 常数项
  357. for (let j = 1; j <= degree; j++) {
  358. row.push(Math.pow(x[i], j));
  359. }
  360. polyX.push(row);
  361. }
  362. // 转换为math.js矩阵
  363. const Xmatrix = math.matrix(polyX);
  364. const ymatrix = math.matrix(y);
  365. // 计算回归系数: β = (X'X)^(-1)X'y
  366. const Xt = math.transpose(Xmatrix);
  367. const XtX = math.multiply(Xt, Xmatrix);
  368. const XtXinv = math.inv(XtX);
  369. const Xty = math.multiply(Xt, ymatrix);
  370. const beta = math.multiply(XtXinv, Xty);
  371. // 计算拟合值
  372. const yFit = math.multiply(Xmatrix, beta);
  373. // 计算R²和调整后的R²
  374. const stats = calculateRegressionStats(y, yFit.valueOf(), degree);
  375. // 构建回归方程
  376. const equation = buildPolynomialRegressionEquation(beta.valueOf(), degree);
  377. return {
  378. type: 'polynomial',
  379. equation: equation,
  380. coefficients: beta.valueOf(),
  381. r2: stats.r2,
  382. adjustedR2: stats.adjustedR2,
  383. degree: degree
  384. };
  385. } catch (error) {
  386. console.error('多项式回归计算错误:', error);
  387. alert('多项式回归计算错误: ' + error.message);
  388. return null;
  389. }
  390. }
  391. /**
  392. * 计算回归统计量
  393. * @param {Array} yActual - 实际y值
  394. * @param {Array} yFit - 拟合y值
  395. * @param {number} numVars - 自变量数量
  396. * @returns {Object} 统计量对象
  397. */
  398. function calculateRegressionStats(yActual, yFit, numVars) {
  399. const n = yActual.length;
  400. // 计算y的平均值
  401. let yMean = 0;
  402. for (let i = 0; i < n; i++) {
  403. yMean += yActual[i];
  404. }
  405. yMean /= n;
  406. // 计算总平方和 (SST) 和残差平方和 (SSE)
  407. let sst = 0;
  408. let sse = 0;
  409. for (let i = 0; i < n; i++) {
  410. sst += Math.pow(yActual[i] - yMean, 2);
  411. sse += Math.pow(yActual[i] - yFit[i], 2);
  412. }
  413. // 计算R²
  414. const r2 = 1 - (sse / sst);
  415. // 计算调整后的R²
  416. const adjustedR2 = 1 - ((1 - r2) * (n - 1) / (n - numVars - 1));
  417. return { r2, adjustedR2 };
  418. }
  419. /**
  420. * 构建线性回归方程字符串
  421. * @param {Array} coefficients - 回归系数
  422. * @param {number} numVars - 自变量数量
  423. * @returns {string} 回归方程字符串
  424. */
  425. function buildLinearRegressionEquation(coefficients, numVars) {
  426. let equation = 'y = ';
  427. // 添加截距
  428. equation += coefficients[0].toFixed(4);
  429. // 添加各个自变量的系数
  430. for (let i = 0; i < numVars; i++) {
  431. const coeff = coefficients[i + 1];
  432. if (coeff >= 0) {
  433. equation += ' + ' + coeff.toFixed(4) + 'x' + (i + 1);
  434. } else {
  435. equation += ' - ' + Math.abs(coeff).toFixed(4) + 'x' + (i + 1);
  436. }
  437. }
  438. return equation;
  439. }
  440. /**
  441. * 构建多项式回归方程字符串
  442. * @param {Array} coefficients - 回归系数
  443. * @param {number} degree - 多项式次数
  444. * @returns {string} 回归方程字符串
  445. */
  446. function buildPolynomialRegressionEquation(coefficients, degree) {
  447. let equation = 'y = ';
  448. // 添加截距
  449. equation += coefficients[0].toFixed(4);
  450. // 添加各次项的系数
  451. for (let i = 1; i <= degree; i++) {
  452. const coeff = coefficients[i];
  453. if (coeff >= 0) {
  454. equation += ' + ' + coeff.toFixed(4);
  455. } else {
  456. equation += ' - ' + Math.abs(coeff).toFixed(4);
  457. }
  458. if (i === 1) {
  459. equation += 'x';
  460. } else {
  461. equation += 'x^' + i;
  462. }
  463. }
  464. return equation;
  465. }
  466. /**
  467. * 显示回归分析结果
  468. * @param {Object} result - 回归结果
  469. * @param {Object} data - 原始数据
  470. */
  471. function displayRegressionResult(result, data) {
  472. if (!result) return;
  473. // 显示回归方程
  474. const equationResult = document.getElementById('equation-result');
  475. equationResult.textContent = result.equation;
  476. // 显示统计信息
  477. const statsResult = document.getElementById('stats-result');
  478. statsResult.innerHTML = `
  479. 拟合优度 R² = ${(result.r2 * 100).toFixed(2)}%<br>
  480. 调整后的 R² = ${(result.adjustedR2 * 100).toFixed(2)}%<br>
  481. 数据点数量: ${data.X.length}<br>
  482. 变量数量: ${result.numVars || result.degree}
  483. `;
  484. // 绘制回归结果
  485. if (data.X[0].length === 1) {
  486. // 只有一个自变量时才绘制图表
  487. plotRegressionResult(result, data);
  488. } else {
  489. // 多变量回归不绘制图表
  490. Plotly.purge('plot-area');
  491. const plotArea = document.getElementById('plot-area');
  492. plotArea.innerHTML = '<div class="no-plot-message">多变量回归无法在二维图表中显示</div>';
  493. }
  494. }
  495. /**
  496. * 绘制回归分析结果
  497. * @param {Object} result - 回归结果
  498. * @param {Object} data - 原始数据
  499. */
  500. function plotRegressionResult(result, data) {
  501. // 提取数据点的x和y值
  502. const xData = data.X.map(row => row[0]);
  503. const yData = data.y;
  504. // 找出x的最小值和最大值
  505. const xMin = Math.min(...xData);
  506. const xMax = Math.max(...xData);
  507. // 为了平滑曲线,生成更多的点
  508. const xRange = xMax - xMin;
  509. const xStart = xMin - xRange * 0.1;
  510. const xEnd = xMax + xRange * 0.1;
  511. const step = xRange / 100;
  512. // 生成回归曲线的点
  513. const xFit = [];
  514. const yFit = [];
  515. for (let x = xStart; x <= xEnd; x += step) {
  516. let y;
  517. if (result.type === 'linear') {
  518. // 线性回归: y = b0 + b1*x
  519. y = result.coefficients[0] + result.coefficients[1] * x;
  520. } else if (result.type === 'polynomial') {
  521. // 多项式回归: y = b0 + b1*x + b2*x^2 + ... + bn*x^n
  522. y = result.coefficients[0];
  523. for (let i = 1; i <= result.degree; i++) {
  524. y += result.coefficients[i] * Math.pow(x, i);
  525. }
  526. }
  527. if (!isNaN(y) && isFinite(y)) {
  528. xFit.push(x);
  529. yFit.push(y);
  530. }
  531. }
  532. // 创建数据点散点图
  533. const dataTrace = {
  534. x: xData,
  535. y: yData,
  536. mode: 'markers',
  537. type: 'scatter',
  538. name: '数据点',
  539. marker: {
  540. size: 8,
  541. color: 'rgba(255, 0, 0, 0.7)'
  542. }
  543. };
  544. // 创建回归曲线
  545. const fitTrace = {
  546. x: xFit,
  547. y: yFit,
  548. mode: 'lines',
  549. type: 'scatter',
  550. name: '回归曲线',
  551. line: {
  552. color: 'blue',
  553. width: 2
  554. }
  555. };
  556. // 绘图布局
  557. const layout = {
  558. title: '回归分析结果',
  559. xaxis: {
  560. title: 'X'
  561. },
  562. yaxis: {
  563. title: 'Y'
  564. },
  565. legend: {
  566. x: 0,
  567. y: 1
  568. }
  569. };
  570. // 绘制图表
  571. Plotly.newPlot('plot-area', [dataTrace, fitTrace], layout);
  572. }