regression.dart 6.2 KB

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