chi_square.dart 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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. intro: const Text('请输入 2×2 列联表数据:'),
  27. compute: (session) async {
  28. final a = _need(_a);
  29. final b = _need(_b);
  30. final c = _need(_c);
  31. final d = _need(_d);
  32. final bind = 'a=$a,b=$b,c=$c,d=$d';
  33. final k2 =
  34. 'With[{$bind}, Module[{nn=a+b+c+d}, '
  35. 'nn*(a*d-b*c)^2/((a+b)*(c+d)*(a+c)*(b+d))]]';
  36. // 各组比例(给图表,不复制):组1 = a/(a+b)、b/(a+b),组2 = c/(c+d)、d/(c+d)
  37. final prop = 'With[{$bind}, N[{a/(a+b), b/(a+b), c/(c+d), d/(c+d)}]]';
  38. final p = parseSymjaList((await session.compute(prop)).text);
  39. final n0 = p.isNotEmpty ? asNumber(p[0]) ?? 0.0 : 0.0;
  40. final n1 = p.length > 1 ? asNumber(p[1]) ?? 0.0 : 0.0;
  41. final n2 = p.length > 2 ? asNumber(p[2]) ?? 0.0 : 0.0;
  42. final n3 = p.length > 3 ? asNumber(p[3]) ?? 0.0 : 0.0;
  43. return [
  44. ...await exactAndApprox(session, 'K²', k2),
  45. ToolChart('各组比例', _barChart(n0, n1, n2, n3)),
  46. ];
  47. },
  48. );
  49. /// 等高堆积条形图:两组各按 类别A / 类别B 的比例堆积,纵轴百分比。
  50. Widget _barChart(double a1, double b1, double a2, double b2) => Column(
  51. mainAxisSize: MainAxisSize.min,
  52. children: [
  53. SizedBox(
  54. height: 220,
  55. child: BarChart(
  56. BarChartData(
  57. alignment: BarChartAlignment.spaceAround,
  58. maxY: 1.0,
  59. barTouchData: BarTouchData(enabled: false),
  60. titlesData: FlTitlesData(
  61. show: true,
  62. bottomTitles: AxisTitles(
  63. sideTitles: SideTitles(
  64. showTitles: true,
  65. getTitlesWidget: (value, meta) =>
  66. Text(value.toInt() == 0 ? '组1' : '组2'),
  67. ),
  68. ),
  69. leftTitles: AxisTitles(
  70. sideTitles: SideTitles(
  71. showTitles: true,
  72. reservedSize: 40,
  73. getTitlesWidget: (value, meta) =>
  74. Text('${(value * 100).toInt()}%'),
  75. ),
  76. ),
  77. topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
  78. rightTitles: AxisTitles(
  79. sideTitles: SideTitles(showTitles: false),
  80. ),
  81. ),
  82. gridData: FlGridData(
  83. show: true,
  84. drawVerticalLine: false,
  85. getDrawingHorizontalLine: (value) => FlLine(
  86. color: Colors.grey.withValues(alpha: 0.3),
  87. strokeWidth: 1,
  88. ),
  89. ),
  90. borderData: FlBorderData(show: false),
  91. barGroups: [
  92. BarChartGroupData(
  93. x: 0,
  94. groupVertically: true,
  95. barsSpace: 4,
  96. barRods: [
  97. BarChartRodData(
  98. fromY: 0,
  99. toY: a1,
  100. color: Colors.blue,
  101. width: 30,
  102. ),
  103. BarChartRodData(
  104. fromY: a1,
  105. toY: a1 + b1,
  106. color: Colors.red,
  107. width: 30,
  108. ),
  109. ],
  110. ),
  111. BarChartGroupData(
  112. x: 1,
  113. groupVertically: true,
  114. barsSpace: 4,
  115. barRods: [
  116. BarChartRodData(
  117. fromY: 0,
  118. toY: a2,
  119. color: Colors.blue,
  120. width: 30,
  121. ),
  122. BarChartRodData(
  123. fromY: a2,
  124. toY: a2 + b2,
  125. color: Colors.red,
  126. width: 30,
  127. ),
  128. ],
  129. ),
  130. ],
  131. ),
  132. ),
  133. ),
  134. const SizedBox(height: 10),
  135. Row(
  136. mainAxisAlignment: MainAxisAlignment.center,
  137. children: [
  138. _legend(Colors.blue, '类别A'),
  139. const SizedBox(width: 20),
  140. _legend(Colors.red, '类别B'),
  141. ],
  142. ),
  143. ],
  144. );
  145. Widget _legend(Color color, String text) => Row(
  146. mainAxisSize: MainAxisSize.min,
  147. children: [
  148. Container(width: 16, height: 16, color: color),
  149. const SizedBox(width: 8),
  150. Text(text),
  151. ],
  152. );
  153. /// 取一个标量输入框:空着或填了多个值都直接报中文错。
  154. String _need(ToolField f) {
  155. if (f.count == 0) throw Exception('请填写「${f.label}」');
  156. if (f.count > 1) throw Exception('「${f.label}」只能填一个数');
  157. return f.text;
  158. }
  159. }