chi_square.dart 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. // tools/chi_square.dart
  2. import 'package:fl_chart/fl_chart.dart';
  3. import 'package:flutter/material.dart';
  4. import 'tool_kit.dart';
  5. /// 卡方检验(2×2 列联表)。
  6. ///
  7. /// K² = n(ad − bc)² / ((a+b)(c+d)(a+c)(b+d)),n = a+b+c+d
  8. ///
  9. /// 精确值给分数、近似值给小数,都从 Symja 拿。图表里的比例数据同样走 Symja
  10. /// (`N[...]`),Dart 只负责把数字喂给 fl_chart —— 图表不参与复制。
  11. class ChiSquarePage extends StatefulWidget {
  12. const ChiSquarePage({super.key});
  13. @override
  14. State<ChiSquarePage> createState() => _ChiSquarePageState();
  15. }
  16. class _ChiSquarePageState extends State<ChiSquarePage> {
  17. // 2×2 列联表就照列联表的样子排:a b 一行、c d 一行
  18. final _a = ToolField('a', hint: '第一行左');
  19. final _b = ToolField('b', hint: '第一行右', rowWithPrevious: true);
  20. final _c = ToolField('c', hint: '第二行左');
  21. final _d = ToolField('d', hint: '第二行右', rowWithPrevious: true);
  22. @override
  23. Widget build(BuildContext context) => ToolScaffold(
  24. title: '卡方检验',
  25. fields: [_a, _b, _c, _d],
  26. compute: (session) async {
  27. final a = _need(_a);
  28. final b = _need(_b);
  29. final c = _need(_c);
  30. final d = _need(_d);
  31. final bind = 'a=$a,b=$b,c=$c,d=$d';
  32. final k2 =
  33. 'With[{$bind}, Module[{nn=a+b+c+d}, '
  34. 'nn*(a*d-b*c)^2/((a+b)*(c+d)*(a+c)*(b+d))]]';
  35. // 各组比例(给图表,不复制):组1 = a/(a+b)、b/(a+b),组2 = c/(c+d)、d/(c+d)
  36. final prop = 'With[{$bind}, N[{a/(a+b), b/(a+b), c/(c+d), d/(c+d)}]]';
  37. final p = parseSymjaList((await session.compute(prop)).text);
  38. final n0 = p.isNotEmpty ? asNumber(p[0]) ?? 0.0 : 0.0;
  39. final n1 = p.length > 1 ? asNumber(p[1]) ?? 0.0 : 0.0;
  40. final n2 = p.length > 2 ? asNumber(p[2]) ?? 0.0 : 0.0;
  41. final n3 = p.length > 3 ? asNumber(p[3]) ?? 0.0 : 0.0;
  42. return [
  43. ...await exactAndApprox(session, 'K²', k2),
  44. ToolChart('各组比例', _barChart(n0, n1, n2, n3)),
  45. ];
  46. },
  47. );
  48. /// 等高堆积条形图:两组各按 类别A / 类别B 的比例堆积,纵轴百分比。
  49. Widget _barChart(double a1, double b1, double a2, double b2) => Column(
  50. mainAxisSize: MainAxisSize.min,
  51. children: [
  52. SizedBox(
  53. height: 220,
  54. child: BarChart(
  55. BarChartData(
  56. alignment: BarChartAlignment.spaceAround,
  57. maxY: 1.0,
  58. barTouchData: BarTouchData(enabled: false),
  59. titlesData: FlTitlesData(
  60. show: true,
  61. bottomTitles: AxisTitles(
  62. sideTitles: SideTitles(
  63. showTitles: true,
  64. getTitlesWidget: (value, meta) =>
  65. Text(value.toInt() == 0 ? '组1' : '组2'),
  66. ),
  67. ),
  68. leftTitles: AxisTitles(
  69. sideTitles: SideTitles(
  70. showTitles: true,
  71. reservedSize: 40,
  72. getTitlesWidget: (value, meta) =>
  73. Text('${(value * 100).toInt()}%'),
  74. ),
  75. ),
  76. topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  77. rightTitles: AxisTitles(
  78. sideTitles: SideTitles(showTitles: false),
  79. ),
  80. ),
  81. gridData: FlGridData(
  82. show: true,
  83. drawVerticalLine: false,
  84. getDrawingHorizontalLine: (value) => FlLine(
  85. color: Colors.grey.withValues(alpha: 0.3),
  86. strokeWidth: 1,
  87. ),
  88. ),
  89. borderData: FlBorderData(show: false),
  90. barGroups: [
  91. BarChartGroupData(
  92. x: 0,
  93. groupVertically: true,
  94. barsSpace: 4,
  95. barRods: [
  96. BarChartRodData(
  97. fromY: 0,
  98. toY: a1,
  99. color: Colors.blue,
  100. width: 30,
  101. ),
  102. BarChartRodData(
  103. fromY: a1,
  104. toY: a1 + b1,
  105. color: Colors.red,
  106. width: 30,
  107. ),
  108. ],
  109. ),
  110. BarChartGroupData(
  111. x: 1,
  112. groupVertically: true,
  113. barsSpace: 4,
  114. barRods: [
  115. BarChartRodData(
  116. fromY: 0,
  117. toY: a2,
  118. color: Colors.blue,
  119. width: 30,
  120. ),
  121. BarChartRodData(
  122. fromY: a2,
  123. toY: a2 + b2,
  124. color: Colors.red,
  125. width: 30,
  126. ),
  127. ],
  128. ),
  129. ],
  130. ),
  131. ),
  132. ),
  133. const SizedBox(height: 10),
  134. Row(
  135. mainAxisAlignment: MainAxisAlignment.center,
  136. children: [
  137. _legend(Colors.blue, '类别A'),
  138. const SizedBox(width: 20),
  139. _legend(Colors.red, '类别B'),
  140. ],
  141. ),
  142. ],
  143. );
  144. Widget _legend(Color color, String text) => Row(
  145. mainAxisSize: MainAxisSize.min,
  146. children: [
  147. Container(width: 16, height: 16, color: color),
  148. const SizedBox(width: 8),
  149. Text(text),
  150. ],
  151. );
  152. /// 取一个标量输入框:空着或填了多个值都直接报中文错。
  153. String _need(ToolField f) {
  154. if (f.count == 0) throw Exception('请填写「${f.label}」');
  155. if (f.count > 1) throw Exception('「${f.label}」只能填一个数');
  156. return f.text;
  157. }
  158. }