regression.dart 7.4 KB

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