| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- // 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<RegressionPage> createState() => _RegressionPageState();
- }
- class _RegressionPageState extends State<RegressionPage> {
- 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 = <String>[
- '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 = <FlSpot>[];
- 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 = <FlSpot>[];
- 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 <ToolOutput>[
- 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<FlSpot> scatter, List<FlSpot> line) {
- if (scatter.isEmpty && line.isEmpty) {
- return const SizedBox(
- height: 200,
- child: Center(child: Text('没有可绘制的数据')),
- );
- }
- // 坐标范围只是给图留边距,不是工具的结果(原来的实现也这么做)。
- final xs = <double>[...scatter.map((s) => s.x), ...line.map((s) => s.x)];
- final ys = <double>[...scatter.map((s) => s.y), ...line.map((s) => s.y)];
- double lo(Iterable<double> v) => v.reduce((a, b) => a < b ? a : b);
- double hi(Iterable<double> 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),
- ),
- ],
- ),
- ),
- );
- }
|