regression.dart 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. // tools/regression.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:fl_chart/fl_chart.dart';
  4. import 'tool_kit.dart';
  5. /// 线性回归 —— 十个统计量、散点和回归直线全部由 Symja 算。
  6. ///
  7. /// Dart 只做两件不涉及计算的事:数个数(校验 x、y 长度一致)和排版。回归
  8. /// 系数、相关系数、残差等一次 `N[With[...], 10]` 全部算好;画图要的双精度
  9. /// 散点由 `{N[x], N[y]}` 给出,回归直线两端点再单独问一次。图上**始终**画
  10. /// 回归直线,原来的「显示回归直线」复选框已去掉。
  11. class RegressionPage extends StatefulWidget {
  12. const RegressionPage({super.key});
  13. @override
  14. State<RegressionPage> createState() => _RegressionPageState();
  15. }
  16. class _RegressionPageState extends State<RegressionPage> {
  17. final _x = ToolField('x值', hint: '用空格分隔多个值');
  18. final _y = ToolField('y值', hint: '用空格分隔多个值');
  19. @override
  20. Widget build(BuildContext context) => ToolScaffold(
  21. title: '线性回归',
  22. fields: [_x, _y],
  23. compute: (session) async {
  24. if (_x.count == 0 || _y.count == 0) {
  25. throw Exception('请输入 x 和 y 的数据');
  26. }
  27. if (_x.count != _y.count) {
  28. throw Exception('x 和 y 的个数必须一致(现在是 ${_x.count} 和 ${_y.count})');
  29. }
  30. final x = _x.list;
  31. final y = _y.list;
  32. // 十个量一条表达式算齐(隐式乘号一律写出来)。
  33. final statsExpr =
  34. 'N[With[{x=$x, y=$y}, '
  35. 'Module[{xb, yb, sxx, syy, sxy, b, aa, se2}, '
  36. 'xb=Mean[x]; yb=Mean[y]; '
  37. 'sxx=Total[(x-xb)^2]; syy=Total[(y-yb)^2]; '
  38. 'sxy=Total[(x-xb)*(y-yb)]; '
  39. 'b=sxy/sxx; aa=yb-b*xb; se2=Total[(y-(aa+b*x))^2]; '
  40. '{xb, yb, sxx, syy, sxy, sxy/Sqrt[sxx*syy], b, aa, se2, 1-se2/syy}]], 10]';
  41. final stats = parseSymjaList((await session.compute(statsExpr)).text)
  42. .map((e) => _clean('$e'))
  43. .toList();
  44. if (stats.length < 10) throw Exception('回归计算没有返回预期结果');
  45. // 守卫:Σ(x−x̄)² 为 0 说明 x 全相同,拟合无意义。
  46. if (asNumber(stats[2]) == 0) {
  47. throw Exception('x 的取值不能全部相同(Σ(x−x̄)² = 0,无法拟合)');
  48. }
  49. const labels = <String>[
  50. 'x 均值 x̄',
  51. 'y 均值 ȳ',
  52. 'Σ(x−x̄)²',
  53. 'Σ(y−ȳ)²',
  54. 'Σ(x−x̄)(y−ȳ)',
  55. '相关系数 r',
  56. '斜率 b',
  57. '截距 a',
  58. '残差平方和',
  59. '决定系数 R²',
  60. ];
  61. // 散点坐标:N[x]、N[y](一次调用拿两组)。
  62. final pts = parseSymjaList(
  63. (await session.compute('{N[$x], N[$y]}')).text,
  64. );
  65. final scatter = <FlSpot>[];
  66. if (pts.length >= 2 && pts[0] is List && pts[1] is List) {
  67. final xs = pts[0] as List;
  68. final ys = pts[1] as List;
  69. for (var i = 0; i < xs.length && i < ys.length; i++) {
  70. final xv = asNumber(xs[i]);
  71. final yv = asNumber(ys[i]);
  72. if (xv != null && yv != null) scatter.add(FlSpot(xv, yv));
  73. }
  74. }
  75. // 回归直线两端点(在 x 的最小/最大值处取值)。
  76. final endsExpr =
  77. 'N[With[{x=$x, y=$y}, '
  78. 'Module[{xb, yb, sxy, sxx, b, aa}, '
  79. 'xb=Mean[x]; yb=Mean[y]; sxx=Total[(x-xb)^2]; '
  80. 'sxy=Total[(x-xb)*(y-yb)]; b=sxy/sxx; aa=yb-b*xb; '
  81. '{Min[x], Max[x], aa+b*Min[x], aa+b*Max[x]}]], 10]';
  82. final ends = parseSymjaList((await session.compute(endsExpr)).text)
  83. .map(asNumber)
  84. .toList();
  85. final line = <FlSpot>[];
  86. if (ends.length >= 4 &&
  87. ends[0] != null &&
  88. ends[1] != null &&
  89. ends[2] != null &&
  90. ends[3] != null) {
  91. line.add(FlSpot(ends[0]!, ends[2]!));
  92. line.add(FlSpot(ends[1]!, ends[3]!));
  93. }
  94. return <ToolOutput>[
  95. for (var i = 0; i < 10; i++) ToolText(labels[i], stats[i]),
  96. ToolChart('散点与回归直线', _scatterWithLine(scatter, line)),
  97. ];
  98. },
  99. );
  100. }
  101. /// 去掉 Symja 近似值尾巴上的精度标记(形如 `0.9943767127`` 或 `0.9943767127`10.`)。
  102. String _clean(String s) => s.split('`').first.trim();
  103. /// 散点 + 回归直线(图不参与复制,所以没有 tex/文本)。
  104. Widget _scatterWithLine(List<FlSpot> scatter, List<FlSpot> line) {
  105. if (scatter.isEmpty && line.isEmpty) {
  106. return const SizedBox(height: 200, child: Center(child: Text('没有可绘制的数据')));
  107. }
  108. // 坐标范围只是给图留边距,不是工具的结果(原来的实现也这么做)。
  109. final xs = <double>[...scatter.map((s) => s.x), ...line.map((s) => s.x)];
  110. final ys = <double>[...scatter.map((s) => s.y), ...line.map((s) => s.y)];
  111. double lo(Iterable<double> v) => v.reduce((a, b) => a < b ? a : b);
  112. double hi(Iterable<double> v) => v.reduce((a, b) => a > b ? a : b);
  113. return SizedBox(
  114. height: 280,
  115. child: LineChart(
  116. LineChartData(
  117. gridData: FlGridData(show: true),
  118. titlesData: FlTitlesData(
  119. topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  120. rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  121. ),
  122. borderData: FlBorderData(show: true),
  123. minX: lo(xs) - 1,
  124. maxX: hi(xs) + 1,
  125. minY: lo(ys) - 1,
  126. maxY: hi(ys) + 1,
  127. lineBarsData: [
  128. if (scatter.isNotEmpty)
  129. LineChartBarData(
  130. spots: scatter,
  131. isCurved: false,
  132. barWidth: 0,
  133. color: Colors.transparent, // 只显示点,不连折线
  134. dotData: FlDotData(
  135. show: true,
  136. getDotPainter: (spot, percent, bar, index) =>
  137. FlDotCirclePainter(
  138. radius: 4,
  139. color: Colors.blue,
  140. strokeWidth: 2,
  141. strokeColor: Colors.blue,
  142. ),
  143. ),
  144. ),
  145. if (line.isNotEmpty)
  146. LineChartBarData(
  147. spots: line,
  148. isCurved: false,
  149. barWidth: 2,
  150. color: Colors.red,
  151. dotData: FlDotData(show: false),
  152. ),
  153. ],
  154. ),
  155. ),
  156. );
  157. }