regression.dart 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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 = 'N[With[{x=$x, y=$y}, '
  34. 'Module[{xb, yb, sxx, syy, sxy, b, aa, se2}, '
  35. 'xb=Mean[x]; yb=Mean[y]; '
  36. 'sxx=Total[(x-xb)^2]; syy=Total[(y-yb)^2]; '
  37. 'sxy=Total[(x-xb)*(y-yb)]; '
  38. 'b=sxy/sxx; aa=yb-b*xb; se2=Total[(y-(aa+b*x))^2]; '
  39. '{xb, yb, sxx, syy, sxy, sxy/Sqrt[sxx*syy], b, aa, se2, 1-se2/syy}]], 10]';
  40. final stats = parseSymjaList((await session.compute(statsExpr)).text)
  41. .map((e) => _clean('$e'))
  42. .toList();
  43. if (stats.length < 10) throw Exception('回归计算没有返回预期结果');
  44. // 守卫:Σ(x−x̄)² 为 0 说明 x 全相同,拟合无意义。
  45. if (asNumber(stats[2]) == 0) {
  46. throw Exception('x 的取值不能全部相同(Σ(x−x̄)² = 0,无法拟合)');
  47. }
  48. const labels = <String>[
  49. 'x 均值 x̄',
  50. 'y 均值 ȳ',
  51. 'Σ(x−x̄)²',
  52. 'Σ(y−ȳ)²',
  53. 'Σ(x−x̄)(y−ȳ)',
  54. '相关系数 r',
  55. '斜率 b',
  56. '截距 a',
  57. '残差平方和',
  58. '决定系数 R²',
  59. ];
  60. // 散点坐标:N[x]、N[y](一次调用拿两组)。
  61. final pts = parseSymjaList((await session.compute('{N[$x], N[$y]}')).text);
  62. final scatter = <FlSpot>[];
  63. if (pts.length >= 2 && pts[0] is List && pts[1] is List) {
  64. final xs = pts[0] as List;
  65. final ys = pts[1] as List;
  66. for (var i = 0; i < xs.length && i < ys.length; i++) {
  67. final xv = asNumber(xs[i]);
  68. final yv = asNumber(ys[i]);
  69. if (xv != null && yv != null) scatter.add(FlSpot(xv, yv));
  70. }
  71. }
  72. // 回归直线两端点(在 x 的最小/最大值处取值)。
  73. final endsExpr = 'N[With[{x=$x, y=$y}, '
  74. 'Module[{xb, yb, sxy, sxx, b, aa}, '
  75. 'xb=Mean[x]; yb=Mean[y]; sxx=Total[(x-xb)^2]; '
  76. 'sxy=Total[(x-xb)*(y-yb)]; b=sxy/sxx; aa=yb-b*xb; '
  77. '{Min[x], Max[x], aa+b*Min[x], aa+b*Max[x]}]], 10]';
  78. final ends = parseSymjaList((await session.compute(endsExpr)).text)
  79. .map(asNumber)
  80. .toList();
  81. final line = <FlSpot>[];
  82. if (ends.length >= 4 &&
  83. ends[0] != null &&
  84. ends[1] != null &&
  85. ends[2] != null &&
  86. ends[3] != null) {
  87. line.add(FlSpot(ends[0]!, ends[2]!));
  88. line.add(FlSpot(ends[1]!, ends[3]!));
  89. }
  90. return <ToolOutput>[
  91. for (var i = 0; i < 10; i++) ToolText(labels[i], stats[i]),
  92. ToolChart('散点与回归直线', _scatterWithLine(scatter, line)),
  93. ];
  94. },
  95. );
  96. }
  97. /// 去掉 Symja 近似值尾巴上的精度标记(形如 `0.9943767127`` 或 `0.9943767127`10.`)。
  98. String _clean(String s) => s.split('`').first.trim();
  99. /// 散点 + 回归直线(图不参与复制,所以没有 tex/文本)。
  100. Widget _scatterWithLine(List<FlSpot> scatter, List<FlSpot> line) {
  101. if (scatter.isEmpty && line.isEmpty) {
  102. return const SizedBox(
  103. height: 200,
  104. child: Center(child: Text('没有可绘制的数据')),
  105. );
  106. }
  107. // 坐标范围只是给图留边距,不是工具的结果(原来的实现也这么做)。
  108. final xs = <double>[...scatter.map((s) => s.x), ...line.map((s) => s.x)];
  109. final ys = <double>[...scatter.map((s) => s.y), ...line.map((s) => s.y)];
  110. double lo(Iterable<double> v) => v.reduce((a, b) => a < b ? a : b);
  111. double hi(Iterable<double> v) => v.reduce((a, b) => a > b ? a : b);
  112. return SizedBox(
  113. height: 280,
  114. child: LineChart(
  115. LineChartData(
  116. gridData: FlGridData(show: true),
  117. titlesData: FlTitlesData(
  118. topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  119. rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  120. ),
  121. borderData: FlBorderData(show: true),
  122. minX: lo(xs) - 1,
  123. maxX: hi(xs) + 1,
  124. minY: lo(ys) - 1,
  125. maxY: hi(ys) + 1,
  126. lineBarsData: [
  127. if (scatter.isNotEmpty)
  128. LineChartBarData(
  129. spots: scatter,
  130. isCurved: false,
  131. barWidth: 0,
  132. color: Colors.transparent, // 只显示点,不连折线
  133. dotData: FlDotData(
  134. show: true,
  135. getDotPainter: (spot, percent, bar, index) =>
  136. FlDotCirclePainter(
  137. radius: 4,
  138. color: Colors.blue,
  139. strokeWidth: 2,
  140. strokeColor: Colors.blue,
  141. ),
  142. ),
  143. ),
  144. if (line.isNotEmpty)
  145. LineChartBarData(
  146. spots: line,
  147. isCurved: false,
  148. barWidth: 2,
  149. color: Colors.red,
  150. dotData: FlDotData(show: false),
  151. ),
  152. ],
  153. ),
  154. ),
  155. );
  156. }