regression.dart 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. // tools/regression.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:rational/rational.dart';
  4. import '../utils/math_utils.dart';
  5. import 'dart:math';
  6. import 'package:fl_chart/fl_chart.dart';
  7. class RegressionPage extends StatefulWidget {
  8. @override
  9. _RegressionPageState createState() => _RegressionPageState();
  10. }
  11. class _RegressionPageState extends State<RegressionPage> {
  12. final TextEditingController xController = TextEditingController();
  13. final TextEditingController yController = TextEditingController();
  14. String result = '';
  15. List<FlSpot> scatterSpots = [];
  16. List<FlSpot> regressionLineSpots = [];
  17. bool showRegressionLine = false;
  18. @override
  19. Widget build(BuildContext context) {
  20. return Scaffold(
  21. appBar: AppBar(title: Text('线性回归')),
  22. body: SingleChildScrollView( // 添加可滚动容器
  23. padding: EdgeInsets.all(16), // 将padding移到这里
  24. child: Column(
  25. children: [
  26. _buildInputField('x值(用空格分隔)', xController),
  27. _buildInputField('y值(用空格分隔)', yController),
  28. SizedBox(height: 20),
  29. ElevatedButton(
  30. onPressed: _calculate,
  31. child: Text('计算'),
  32. ),
  33. SizedBox(height: 20),
  34. // 回归直线显示选项
  35. Row(
  36. children: [
  37. Checkbox(
  38. value: showRegressionLine,
  39. onChanged: (value) {
  40. setState(() {
  41. showRegressionLine = value!;
  42. });
  43. },
  44. ),
  45. Text('显示回归直线'),
  46. ],
  47. ),
  48. SizedBox(height: 20),
  49. if (result.isNotEmpty)
  50. Card(
  51. child: Padding(
  52. padding: EdgeInsets.all(16),
  53. child: Text(
  54. result,
  55. style: TextStyle(fontSize: 14, fontFamily: 'Monospace'),
  56. ),
  57. ),
  58. ),
  59. SizedBox(height: 20),
  60. // 散点图
  61. if (scatterSpots.isNotEmpty) _buildScatterChart(),
  62. ],
  63. ),
  64. ),
  65. );
  66. }
  67. Widget _buildInputField(String label, TextEditingController controller) {
  68. return Padding(
  69. padding: EdgeInsets.symmetric(vertical: 8),
  70. child: TextField(
  71. controller: controller,
  72. keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true),
  73. decoration: InputDecoration(
  74. labelText: label,
  75. border: OutlineInputBorder(),
  76. hintText: '用空格分隔多个值',
  77. ),
  78. ),
  79. );
  80. }
  81. Widget _buildScatterChart() {
  82. return Container(
  83. height: 300,
  84. padding: EdgeInsets.all(16),
  85. child: Card(
  86. elevation: 4,
  87. child: Padding(
  88. padding: EdgeInsets.all(16),
  89. child: LineChart(
  90. LineChartData(
  91. gridData: FlGridData(show: true),
  92. titlesData: FlTitlesData(
  93. bottomTitles: AxisTitles(
  94. sideTitles: SideTitles(showTitles: true),
  95. ),
  96. leftTitles: AxisTitles(
  97. sideTitles: SideTitles(showTitles: true),
  98. ),
  99. topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  100. rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  101. ),
  102. borderData: FlBorderData(show: true),
  103. minX: scatterSpots.map((spot) => spot.x).reduce(min) - 1,
  104. maxX: scatterSpots.map((spot) => spot.x).reduce(max) + 1,
  105. minY: scatterSpots.map((spot) => spot.y).reduce(min) - 1,
  106. maxY: scatterSpots.map((spot) => spot.y).reduce(max) + 1,
  107. lineBarsData: [
  108. // 散点图
  109. LineChartBarData(
  110. spots: scatterSpots,
  111. isCurved: false,
  112. dotData: FlDotData(
  113. show: true,
  114. getDotPainter: (spot, percent, barData, index) {
  115. return FlDotCirclePainter(
  116. radius: 4,
  117. color: Colors.blue,
  118. strokeWidth: 2,
  119. strokeColor: Colors.blue,
  120. );
  121. },
  122. ),
  123. color: Colors.transparent, // 隐藏连接线
  124. belowBarData: BarAreaData(show: false),
  125. ),
  126. // 回归直线(可选)
  127. if (showRegressionLine && regressionLineSpots.isNotEmpty)
  128. LineChartBarData(
  129. spots: regressionLineSpots,
  130. isCurved: false,
  131. dotData: FlDotData(show: false),
  132. color: Colors.red,
  133. barWidth: 2,
  134. belowBarData: BarAreaData(show: false),
  135. ),
  136. ],
  137. ),
  138. ),
  139. ),
  140. ),
  141. );
  142. }
  143. void _calculate() {
  144. try {
  145. final xList = xController.text.split(' ').where((s) => s.isNotEmpty).map((s) => double.parse(s)).toList();
  146. final yList = yController.text.split(' ').where((s) => s.isNotEmpty).map((s) => double.parse(s)).toList();
  147. if (xList.isEmpty || yList.isEmpty) {
  148. setState(() {
  149. result = '错误: 请输入数据';
  150. });
  151. return;
  152. }
  153. if (xList.length != yList.length) {
  154. setState(() {
  155. result = '错误: x和y的数据长度不一致';
  156. });
  157. return;
  158. }
  159. final n = xList.length;
  160. double sx = 0, sy = 0, sx2 = 0, sy2 = 0, sxy = 0;
  161. for (int i = 0; i < n; i++) {
  162. sx += xList[i];
  163. sy += yList[i];
  164. sx2 += xList[i] * xList[i];
  165. sy2 += yList[i] * yList[i];
  166. sxy += xList[i] * yList[i];
  167. }
  168. final xBar = sx / n;
  169. final yBar = sy / n;
  170. final sigmaX = sx2 - n * xBar * xBar;
  171. final sigmaY = sy2 - n * yBar * yBar;
  172. final sigmaXY = sxy - n * xBar * yBar;
  173. final r = sigmaXY / sqrt(sigmaX * sigmaY);
  174. final b = sigmaXY / sigmaX;
  175. final a = yBar - b * xBar;
  176. double se2 = 0;
  177. for (int i = 0; i < n; i++) {
  178. se2 += pow(yList[i] - b * xList[i] - a, 2);
  179. }
  180. final R2 = 1 - se2 / sigmaY;
  181. // 准备散点数据
  182. scatterSpots = [];
  183. for (int i = 0; i < n; i++) {
  184. scatterSpots.add(FlSpot(xList[i], yList[i]));
  185. }
  186. // 准备回归直线数据(两个端点)
  187. regressionLineSpots = [];
  188. if (xList.isNotEmpty) {
  189. double minX = xList.reduce(min);
  190. double maxX = xList.reduce(max);
  191. regressionLineSpots.add(FlSpot(minX, a + b * minX));
  192. regressionLineSpots.add(FlSpot(maxX, a + b * maxX));
  193. }
  194. setState(() {
  195. result = '''
  196. x_bar = ${xBar.toStringAsFixed(4)}
  197. y_bar = ${yBar.toStringAsFixed(4)}
  198. Σ(x-x̄)² = ${sigmaX.toStringAsFixed(4)}
  199. Σ(y-ȳ)² = ${sigmaY.toStringAsFixed(4)}
  200. Σ(x-x̄)(y-ȳ) = ${sigmaXY.toStringAsFixed(4)}
  201. 相关系数 r = ${r.toStringAsFixed(4)}
  202. 斜率 b = ${b.toStringAsFixed(4)}
  203. 截距 a = ${a.toStringAsFixed(4)}
  204. 残差平方和 = ${se2.toStringAsFixed(4)}
  205. 决定系数 R² = ${R2.toStringAsFixed(4)}
  206. ''';
  207. });
  208. } catch (e) {
  209. setState(() {
  210. result = '错误: $e';
  211. scatterSpots = [];
  212. regressionLineSpots = [];
  213. });
  214. }
  215. }
  216. @override
  217. void dispose() {
  218. xController.dispose();
  219. yController.dispose();
  220. super.dispose();
  221. }
  222. }