// tools/hypergeometric_distribution.dart import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'tool_kit.dart'; /// 超几何分布。 /// /// 期望 EX = n·M/N /// 方差 DX = n·M·(N-M)·(N-n) / (N²·(N-1)) /// P(X = k) = C(M,k)·C(N-M,n-k) / C(N,n),k = max(0, n-(N-M)) … min(n, M) /// /// Symja 里 `N` 是内置名(数值近似),所以表达式里的变量一律换成 nn/mm/kk。 class HypergeometricDistributionPage extends StatefulWidget { const HypergeometricDistributionPage({super.key}); @override State createState() => _HypergeometricDistributionPageState(); } class _HypergeometricDistributionPageState extends State { final _total = ToolField('总数 N', hint: '整数,如 10'); final _good = ToolField('满足条件数 M', hint: '整数,如 4'); final _draw = ToolField('抽取数 n', hint: '整数,如 3'); @override Widget build(BuildContext context) => ToolScaffold( title: '超几何分布', fields: [_total, _good, _draw], compute: (session) async { final total = int.tryParse(_total.text); final good = int.tryParse(_good.text); final draw = int.tryParse(_draw.text); if (total == null || good == null || draw == null) { throw Exception('总数 N、满足条件数 M、抽取数 n 都必须是整数'); } if (total <= 1) { throw Exception('总数 N 必须大于 1(方差公式要除以 N-1)'); } if (good < 0 || good > total) { throw Exception('满足条件数 M 必须在 0 到 N 之间(现在是 $good,N = $total)'); } if (draw < 0 || draw > total) { throw Exception('抽取数 n 必须在 0 到 N 之间(现在是 $draw,N = $total)'); } // nn=N,mm=M,kk=n(避开内置名 N)。 final wrap = 'With[{nn=$total,mm=$good,kk=$draw},'; final outputs = [ ...await exactAndApprox(session, '期望 EX', '$wrap kk*mm/nn]'), ...await exactAndApprox( session, '方差 DX', '$wrap kk*mm*(nn-mm)*(nn-kk)/(nn^2*(nn-1))]', ), ]; // 表格里的 k 从 max(0, n-(N-M)) 起,是个偏移量。让 Symja 单独算一次, // 拿到起点后才能把第 j 项标成真实的 k。 final startRes = await session.compute('$wrap Max[0,kk-(nn-mm)]]'); final start = int.tryParse(startRes.text.trim()); if (start == null) { throw Exception('无法确定 k 的起始值:${startRes.text}'); } final exact = await session.compute( '$wrap Rationalize[Table[Binomial[mm,j]*Binomial[nn-mm,kk-j]/' 'Binomial[nn,kk], {j, Max[0,kk-(nn-mm)], Min[kk,mm]}]]]', ); final numeric = await session.compute( '$wrap N[Table[Binomial[mm,j]*Binomial[nn-mm,kk-j]/Binomial[nn,kk], ' '{j, Max[0,kk-(nn-mm)], Min[kk,mm]}], ${session.approximateDigits}]]', ); final probs = parseSymjaList(exact.text); final nums = parseSymjaList(numeric.text); for (var i = 0; i < probs.length; i++) { outputs.add( ToolText( 'P(X = ${start + i})', withApprox('${probs[i]}', i < nums.length ? '${nums[i]}' : ''), ), ); } final values = nums .map(asNumber) .whereType() .toList(growable: false); if (values.length == probs.length) { outputs.add(ToolChart('概率分布', _barChart(start, values))); } return outputs; }, ); /// 柱状图:最高的那一段(可能并排几个)涂红,左边蓝、右边绿 —— 和原来一样的规则。 Widget _barChart(int start, List probs) { var maxProb = 0.0; for (final v in probs) { if (v > maxProb) maxProb = v; } var firstMax = -1, lastMax = -1; for (var i = 0; i < probs.length; i++) { if (probs[i] == maxProb) { if (firstMax < 0) firstMax = i; lastMax = i; } } Color colorAt(int i) { if (i >= firstMax && i <= lastMax) return Colors.red; return i < firstMax ? Colors.blue.shade300 : Colors.green.shade300; } return SizedBox( height: 300, child: BarChart( BarChartData( barTouchData: BarTouchData(enabled: false), alignment: BarChartAlignment.spaceAround, maxY: maxProb == 0 ? 1.0 : maxProb * 1.1, borderData: FlBorderData(show: true), gridData: FlGridData(show: true), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (value, meta) => Padding( padding: const EdgeInsets.only(top: 8), child: Text( '${value.toInt()}', style: const TextStyle(fontSize: 10), ), ), ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) => value % 0.1 == 0 ? Text( value.toStringAsFixed(1), style: const TextStyle(fontSize: 10), ) : const Text(''), ), ), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), barGroups: [ for (var i = 0; i < probs.length; i++) BarChartGroupData( x: start + i, barRods: [ BarChartRodData( toY: probs[i], color: colorAt(i), width: 16, borderRadius: BorderRadius.circular(4), ), ], ), ], ), ), ); } }