// tools/regression.dart import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'tool_kit.dart'; /// 线性回归 —— 十个统计量、散点和回归直线全部由 Symja 算。 /// /// Dart 只做两件不涉及计算的事:数个数(校验 x、y 长度一致)和排版。回归 /// 系数、相关系数、残差等一次 `N[With[...], 10]` 全部算好;画图要的双精度 /// 散点由 `{N[x], N[y]}` 给出,回归直线两端点再单独问一次。图上**始终**画 /// 回归直线,原来的「显示回归直线」复选框已去掉。 class RegressionPage extends StatefulWidget { const RegressionPage({super.key}); @override State createState() => _RegressionPageState(); } class _RegressionPageState extends State { final _x = ToolField('x值', hint: '用空格分隔多个值'); final _y = ToolField('y值', hint: '用空格分隔多个值'); @override Widget build(BuildContext context) => ToolScaffold( title: '线性回归', fields: [_x, _y], compute: (session) async { if (_x.count == 0 || _y.count == 0) { throw Exception('请输入 x 和 y 的数据'); } if (_x.count != _y.count) { throw Exception('x 和 y 的个数必须一致(现在是 ${_x.count} 和 ${_y.count})'); } final x = _x.list; final y = _y.list; // 十个量一条表达式算齐(隐式乘号一律写出来)。 final statsExpr = 'N[With[{x=$x, y=$y}, ' 'Module[{xb, yb, sxx, syy, sxy, b, aa, se2}, ' 'xb=Mean[x]; yb=Mean[y]; ' 'sxx=Total[(x-xb)^2]; syy=Total[(y-yb)^2]; ' 'sxy=Total[(x-xb)*(y-yb)]; ' 'b=sxy/sxx; aa=yb-b*xb; se2=Total[(y-(aa+b*x))^2]; ' '{xb, yb, sxx, syy, sxy, sxy/Sqrt[sxx*syy], b, aa, se2, 1-se2/syy}]], 10]'; final stats = parseSymjaList((await session.compute(statsExpr)).text) .map((e) => _clean('$e')) .toList(); if (stats.length < 10) throw Exception('回归计算没有返回预期结果'); // 守卫:Σ(x−x̄)² 为 0 说明 x 全相同,拟合无意义。 if (asNumber(stats[2]) == 0) { throw Exception('x 的取值不能全部相同(Σ(x−x̄)² = 0,无法拟合)'); } const labels = [ 'x 均值 x̄', 'y 均值 ȳ', 'Σ(x−x̄)²', 'Σ(y−ȳ)²', 'Σ(x−x̄)(y−ȳ)', '相关系数 r', '斜率 b', '截距 a', '残差平方和', '决定系数 R²', ]; // 散点坐标:N[x]、N[y](一次调用拿两组)。 final pts = parseSymjaList((await session.compute('{N[$x], N[$y]}')).text); final scatter = []; if (pts.length >= 2 && pts[0] is List && pts[1] is List) { final xs = pts[0] as List; final ys = pts[1] as List; for (var i = 0; i < xs.length && i < ys.length; i++) { final xv = asNumber(xs[i]); final yv = asNumber(ys[i]); if (xv != null && yv != null) scatter.add(FlSpot(xv, yv)); } } // 回归直线两端点(在 x 的最小/最大值处取值)。 final endsExpr = 'N[With[{x=$x, y=$y}, ' 'Module[{xb, yb, sxy, sxx, b, aa}, ' 'xb=Mean[x]; yb=Mean[y]; sxx=Total[(x-xb)^2]; ' 'sxy=Total[(x-xb)*(y-yb)]; b=sxy/sxx; aa=yb-b*xb; ' '{Min[x], Max[x], aa+b*Min[x], aa+b*Max[x]}]], 10]'; final ends = parseSymjaList((await session.compute(endsExpr)).text) .map(asNumber) .toList(); final line = []; if (ends.length >= 4 && ends[0] != null && ends[1] != null && ends[2] != null && ends[3] != null) { line.add(FlSpot(ends[0]!, ends[2]!)); line.add(FlSpot(ends[1]!, ends[3]!)); } return [ for (var i = 0; i < 10; i++) ToolText(labels[i], stats[i]), ToolChart('散点与回归直线', _scatterWithLine(scatter, line)), ]; }, ); } /// 去掉 Symja 近似值尾巴上的精度标记(形如 `0.9943767127`` 或 `0.9943767127`10.`)。 String _clean(String s) => s.split('`').first.trim(); /// 散点 + 回归直线(图不参与复制,所以没有 tex/文本)。 Widget _scatterWithLine(List scatter, List line) { if (scatter.isEmpty && line.isEmpty) { return const SizedBox( height: 200, child: Center(child: Text('没有可绘制的数据')), ); } // 坐标范围只是给图留边距,不是工具的结果(原来的实现也这么做)。 final xs = [...scatter.map((s) => s.x), ...line.map((s) => s.x)]; final ys = [...scatter.map((s) => s.y), ...line.map((s) => s.y)]; double lo(Iterable v) => v.reduce((a, b) => a < b ? a : b); double hi(Iterable v) => v.reduce((a, b) => a > b ? a : b); return SizedBox( height: 280, child: LineChart( LineChartData( gridData: FlGridData(show: true), titlesData: FlTitlesData( topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: true), minX: lo(xs) - 1, maxX: hi(xs) + 1, minY: lo(ys) - 1, maxY: hi(ys) + 1, lineBarsData: [ if (scatter.isNotEmpty) LineChartBarData( spots: scatter, isCurved: false, barWidth: 0, color: Colors.transparent, // 只显示点,不连折线 dotData: FlDotData( show: true, getDotPainter: (spot, percent, bar, index) => FlDotCirclePainter( radius: 4, color: Colors.blue, strokeWidth: 2, strokeColor: Colors.blue, ), ), ), if (line.isNotEmpty) LineChartBarData( spots: line, isCurved: false, barWidth: 2, color: Colors.red, dotData: FlDotData(show: false), ), ], ), ), ); }