├── dart_test.yaml ├── .gitignore ├── lib ├── stats.dart └── src │ ├── stats.g.dart │ ├── extension.dart │ ├── confidence_interval.g.dart │ ├── stats_sinks.dart │ ├── confidence_level.dart │ ├── confidence_interval.dart │ ├── t_score.dart │ └── stats.dart ├── test ├── ensure_build_test.dart ├── stats_test.dart ├── confidence_internal_test.dart └── t_score_test.dart ├── .github ├── dependabot.yml └── workflows │ ├── publish.yaml │ ├── no-response.yml │ └── ci.yml ├── pubspec.yaml ├── example ├── example.dart ├── streaming_example.dart └── confidence_internal_example.dart ├── analysis_options.yaml ├── LICENSE ├── README.md └── CHANGELOG.md /dart_test.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | presubmit-only: 3 | skip: "Should only be run during presubmit" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | pubspec.lock 5 | -------------------------------------------------------------------------------- /lib/stats.dart: -------------------------------------------------------------------------------- 1 | export 'src/confidence_interval.dart' show ConfidenceInterval; 2 | export 'src/confidence_level.dart' show ConfidenceLevel; 3 | export 'src/extension.dart'; 4 | export 'src/stats.dart'; 5 | -------------------------------------------------------------------------------- /test/ensure_build_test.dart: -------------------------------------------------------------------------------- 1 | @Tags(['presubmit-only']) 2 | library; 3 | 4 | import 'package:build_verify/build_verify.dart'; 5 | import 'package:test/scaffolding.dart'; 6 | 7 | void main() { 8 | test('ensure_build', expectBuildClean, timeout: const Timeout.factor(2)); 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: stats 2 | description: >- 3 | Calculate common statistical values for a set of numbers: max, min, mean, 4 | median, standard deviation, and standard error. 5 | version: 3.0.0 6 | repository: https://github.com/kevmoo/stats 7 | 8 | environment: 9 | sdk: ^3.7.0 10 | 11 | dependencies: 12 | json_annotation: ^4.9.0 13 | 14 | dev_dependencies: 15 | build_runner: ^2.2.1 16 | build_verify: ^3.0.0 17 | checks: ^0.3.0 18 | dart_flutter_team_lints: ^3.0.0 19 | json_serializable: ^6.6.0 20 | test: ^1.24.0 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'kevmoo' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:stats/stats.dart'; 4 | 5 | void main() { 6 | final input = [1, 2, 3, 4, 5, 6, 7, 8]; 7 | print('Input: $input'); 8 | final stats = Stats.fromData(input); 9 | final confidence = ConfidenceInterval.calculate( 10 | stats, 11 | ConfidenceLevel.percent95, 12 | ); 13 | 14 | print(_toJson(confidence)); 15 | // { 16 | // "stats": { 17 | // "count": 8, 18 | // "min": 1, 19 | // "max": 8, 20 | // "mean": 4.5, 21 | // "sumOfSquares": 42.0 22 | // }, 23 | // "marginOfError": 2.0481500799501973, 24 | // "tScore": 2.365, 25 | // "confidenceLevel": "percent95" 26 | // } 27 | } 28 | 29 | String _toJson(Object? value) => 30 | const JsonEncoder.withIndent(' ').convert(value); 31 | -------------------------------------------------------------------------------- /lib/src/stats.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'stats.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Stats _$StatsFromJson(Map json) => Stats( 10 | count: (json['count'] as num).toInt(), 11 | mean: (json['mean'] as num).toDouble(), 12 | min: json['min'] as num, 13 | max: json['max'] as num, 14 | sumOfSquares: (json['sumOfSquares'] as num).toDouble(), 15 | ); 16 | 17 | Map _$StatsToJson(Stats instance) => { 18 | 'count': instance.count, 19 | 'min': instance.min, 20 | 'max': instance.max, 21 | 'mean': instance.mean, 22 | 'sumOfSquares': instance.sumOfSquares, 23 | }; 24 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | strict-inference: true 7 | strict-raw-types: true 8 | 9 | linter: 10 | rules: 11 | - avoid_bool_literals_in_conditional_expressions 12 | - avoid_classes_with_only_static_members 13 | - avoid_private_typedef_functions 14 | - avoid_redundant_argument_values 15 | - avoid_unused_constructor_parameters 16 | - avoid_void_async 17 | - cancel_subscriptions 18 | - cascade_invocations 19 | - join_return_with_assignment 20 | - literal_only_boolean_expressions 21 | - missing_whitespace_between_adjacent_strings 22 | - no_adjacent_strings_in_list 23 | - no_runtimeType_toString 24 | - prefer_const_declarations 25 | - prefer_expression_function_bodies 26 | - prefer_final_locals 27 | - unnecessary_await_in_return 28 | - use_string_buffers 29 | -------------------------------------------------------------------------------- /example/streaming_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:stats/stats.dart'; 5 | 6 | void main() async { 7 | final controller = StreamController(sync: true); 8 | 9 | final rnd = Random(); 10 | 11 | void addRandomValue() { 12 | if (controller.isClosed) return; 13 | controller.add(rnd.nextInt(256)); 14 | Future.microtask(addRandomValue); 15 | } 16 | 17 | unawaited(Future.microtask(addRandomValue)); 18 | 19 | await for (var stats in controller.stream 20 | .transform(Stats.transformer) 21 | .take(1000)) { 22 | if (stats.count > 1) { 23 | final confidence = ConfidenceInterval.calculate( 24 | stats, 25 | ConfidenceLevel.percent99, 26 | ); 27 | print( 28 | [ 29 | stats.count, 30 | stats.mean.toStringAsPrecision(4), 31 | confidence.marginOfError.toStringAsPrecision(4), 32 | ].join('\t'), 33 | ); 34 | } 35 | } 36 | 37 | await controller.close(); 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kevin Moore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import '../stats.dart'; 4 | 5 | extension StatsExtensions on Iterable { 6 | Stats get stats => Stats.fromData(this); 7 | 8 | ConfidenceInterval confidenceInterval(ConfidenceLevel confidenceLevel) => 9 | ConfidenceInterval.calculate(stats, confidenceLevel); 10 | 11 | /// Returns the maximum of all values in `this`. 12 | T get max => reduce(math.max); 13 | 14 | /// Returns the minimum values in `this`. 15 | T get min => reduce(math.min); 16 | 17 | /// Returns the sum of all values in `this`. 18 | T get sum { 19 | var runningSum = T == int ? 0 : 0.0; 20 | for (var value in this) { 21 | runningSum += value; 22 | } 23 | 24 | return runningSum as T; 25 | } 26 | 27 | /// Returns the mean (average) of all values in `this`. 28 | /// 29 | /// `this` is only enumerated once. 30 | double get mean { 31 | var count = 0; 32 | num runningSum = 0; 33 | for (var value in this) { 34 | count++; 35 | runningSum += value; 36 | } 37 | 38 | if (count == 0) { 39 | throw ArgumentError( 40 | 'Cannot calculate the average of an empty collection.', 41 | ); 42 | } 43 | 44 | return runningSum / count; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/confidence_interval.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'confidence_interval.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ConfidenceInterval _$ConfidenceIntervalFromJson(Map json) => 10 | ConfidenceInterval( 11 | stats: Stats.fromJson(json['stats'] as Map), 12 | marginOfError: (json['marginOfError'] as num).toDouble(), 13 | tScore: (json['tScore'] as num).toDouble(), 14 | confidenceLevel: $enumDecode( 15 | _$ConfidenceLevelEnumMap, 16 | json['confidenceLevel'], 17 | ), 18 | ); 19 | 20 | Map _$ConfidenceIntervalToJson(ConfidenceInterval instance) => 21 | { 22 | 'stats': instance.stats, 23 | 'marginOfError': instance.marginOfError, 24 | 'tScore': instance.tScore, 25 | 'confidenceLevel': _$ConfidenceLevelEnumMap[instance.confidenceLevel]!, 26 | }; 27 | 28 | const _$ConfidenceLevelEnumMap = { 29 | ConfidenceLevel.percent90: 'percent90', 30 | ConfidenceLevel.percent95: 'percent95', 31 | ConfidenceLevel.percent99: 'percent99', 32 | }; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/kevmoo/stats/actions/workflows/ci.yml/badge.svg)](https://github.com/kevmoo/stats/actions/workflows/ci.yml) 2 | [![Pub Package](https://img.shields.io/pub/v/stats.svg)](https://pub.dev/packages/stats) 3 | [![package publisher](https://img.shields.io/pub/publisher/stats.svg)](https://pub.dev/packages/stats/publisher) 4 | 5 | Supports: 6 | 7 | - `Stats`: Typical statistical values over a `List` or `Stream` of `num`: 8 | `count`, `min`, `max`, `mean`, `sumOfSquares` 9 | - `ConfidenceInterval` over `Stats` with a given confidence level. 10 | 11 | ```dart 12 | import 'dart:convert'; 13 | 14 | import 'package:stats/stats.dart'; 15 | 16 | void main() { 17 | final input = [1, 2, 3, 4, 5, 6, 7, 8]; 18 | print('Input: $input'); 19 | final stats = Stats.fromData(input); 20 | final confidence = ConfidenceInterval.calculate( 21 | stats, 22 | ConfidenceLevel.percent95, 23 | ); 24 | 25 | print(_toJson(confidence)); 26 | // { 27 | // "stats": { 28 | // "count": 8, 29 | // "min": 1, 30 | // "max": 8, 31 | // "mean": 4.5, 32 | // "sumOfSquares": 42.0 33 | // }, 34 | // "marginOfError": 2.0481500799501973, 35 | // "tScore": 2.365, 36 | // "confidenceLevel": "percent95" 37 | // } 38 | } 39 | 40 | String _toJson(Object? value) => 41 | const JsonEncoder.withIndent(' ').convert(value); 42 | ``` 43 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'kevmoo' }} 21 | steps: 22 | - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /lib/src/stats_sinks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'stats.dart'; 4 | 5 | final class StatsConverter extends Converter { 6 | const StatsConverter(); 7 | 8 | @override 9 | Stats convert(num input) => Stats.fromData([input]); 10 | 11 | @override 12 | Sink startChunkedConversion(Sink sink) => ChunkedSink(sink); 13 | } 14 | 15 | final class ChunkedSink extends StatsSink { 16 | ChunkedSink(this._target); 17 | 18 | final Sink _target; 19 | bool _closed = false; 20 | 21 | @override 22 | void add(num value) { 23 | if (_closed) { 24 | throw StateError('Closed.'); 25 | } 26 | super.add(value); 27 | _target.add(emit()); 28 | } 29 | 30 | @override 31 | void close() { 32 | super.close(); 33 | _closed = true; 34 | } 35 | } 36 | 37 | final class StatsSink implements ChunkedConversionSink { 38 | int _count = 0; 39 | double _mean = 0; 40 | double _sumOfSquares = 0; 41 | late num _min, _max; 42 | 43 | @override 44 | void add(num value) { 45 | assert(value.isFinite); 46 | if (_count == 0) { 47 | _min = _max = value; 48 | } else { 49 | if (value < _min) { 50 | _min = value; 51 | } else if (value > _max) { 52 | _max = value; 53 | } 54 | } 55 | _count++; 56 | final delta = value - _mean; 57 | _mean += delta / _count; 58 | final delta2 = value - _mean; // Use the new mean for delta2 59 | _sumOfSquares += delta * delta2; 60 | } 61 | 62 | Stats emit() { 63 | if (_count == 0) { 64 | throw ArgumentError('Cannot be empty.', 'source'); 65 | } 66 | 67 | return Stats( 68 | count: _count, 69 | mean: _mean, 70 | min: _min, 71 | max: _max, 72 | sumOfSquares: _sumOfSquares, 73 | ); 74 | } 75 | 76 | @override 77 | void close() {} 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/confidence_level.dart: -------------------------------------------------------------------------------- 1 | /// Supported confidence levels for the simplified t-score lookup. 2 | enum ConfidenceLevel { 3 | percent90( 4 | value: 0.90, 5 | scores: { 6 | 1: 6.314, 2: 2.920, 3: 2.353, 4: 2.1318, 5: 2.015, // 7 | 6: 1.943, 7: 1.895, 8: 1.860, 9: 1.833, 10: 1.812, 8 | 11: 1.796, 12: 1.782, 13: 1.771, 14: 1.761, 15: 1.753, 9 | 16: 1.746, 17: 1.740, 18: 1.734, 19: 1.729, 20: 1.725, 10 | 29: 1.699, 11 | 30: 1.697, 12 | 60: 1.671, 13 | 120: 1.658, 14 | }, 15 | infinityValue: 1.645, 16 | ), 17 | percent95( 18 | value: .95, 19 | scores: { 20 | 1: 12.706, 2: 4.303, 3: 3.182, 4: 2.7764, 5: 2.571, // 21 | 6: 2.447, 7: 2.365, 8: 2.306, 9: 2.262, 10: 2.228, 22 | 11: 2.201, 12: 2.179, 13: 2.160, 14: 2.145, 15: 2.131, 23 | 16: 2.120, 17: 2.110, 18: 2.101, 19: 2.093, 20: 2.086, 24 | 29: 2.045, 25 | 30: 2.042, 26 | 60: 2.000, 27 | 120: 1.980, 28 | }, 29 | infinityValue: 1.960, 30 | ), 31 | percent99( 32 | value: 0.99, 33 | scores: { 34 | 1: 63.657, 2: 9.925, 3: 5.841, 4: 4.604, 5: 4.032, // 35 | 6: 3.707, 7: 3.499, 8: 3.355, 9: 3.250, 10: 3.169, 36 | 11: 3.106, 12: 3.055, 13: 3.012, 14: 2.977, 15: 2.947, 37 | 16: 2.921, 17: 2.896, 18: 2.878, 19: 2.861, 20: 2.845, 38 | 29: 2.756, 39 | 30: 2.750, 40 | 60: 2.660, 41 | 120: 2.617, 42 | }, 43 | infinityValue: 2.576, 44 | ); 45 | 46 | const ConfidenceLevel({ 47 | required this.value, 48 | required this.scores, 49 | required this.infinityValue, 50 | }); 51 | 52 | /// The confidence level as a percentage between `0` and `1`. 53 | final double value; 54 | 55 | /// A lookup of degrees of freedom to the t-score for this confidence level. 56 | final Map scores; 57 | 58 | /// The value used for very large degrees of freedom. 59 | final double infinityValue; 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 26 | - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | # Run tests on a matrix consisting of two dimensions: 40 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 41 | # 2. release channel: dev 42 | test: 43 | needs: analyze 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | # Add macos-latest and/or windows-latest if relevant for this package. 49 | os: [ubuntu-latest] 50 | sdk: [3.7, dev] 51 | steps: 52 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 53 | - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c 54 | with: 55 | sdk: ${{ matrix.sdk }} 56 | - id: install 57 | name: Install dependencies 58 | run: dart pub get 59 | - run: dart test -x presubmit-only -p vm,chrome --compiler dart2wasm,dart2js 60 | if: always() && steps.install.outcome == 'success' 61 | - run: dart test --run-skipped -t presubmit-only 62 | if: always() && steps.install.outcome == 'success' 63 | -------------------------------------------------------------------------------- /lib/src/confidence_interval.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'confidence_level.dart'; 4 | import 'stats.dart'; 5 | import 't_score.dart'; 6 | 7 | part 'confidence_interval.g.dart'; 8 | 9 | /// Represents the calculated confidence interval. 10 | @JsonSerializable() 11 | class ConfidenceInterval { 12 | ConfidenceInterval({ 13 | required this.stats, 14 | required this.marginOfError, 15 | required this.tScore, 16 | required this.confidenceLevel, 17 | }) : assert(marginOfError >= 0), 18 | assert(marginOfError.isFinite); 19 | 20 | factory ConfidenceInterval.fromJson(Map json) => 21 | _$ConfidenceIntervalFromJson(json); 22 | 23 | final Stats stats; 24 | final double marginOfError; 25 | final double tScore; 26 | final ConfidenceLevel confidenceLevel; 27 | 28 | double get lowerBound => stats.mean - marginOfError; 29 | double get upperBound => stats.mean + marginOfError; 30 | 31 | /// Calculates the [ConfidenceInterval] for [stats] given a [confidenceLevel]. 32 | static ConfidenceInterval calculate( 33 | Stats stats, 34 | ConfidenceLevel confidenceLevel, 35 | ) { 36 | if (stats.count < 2) { 37 | throw ArgumentError.value( 38 | stats, 39 | 'stats', 40 | 'At least two data points are required ' 41 | 'to calculate a confidence interval.', 42 | ); 43 | } 44 | final tScore = calcTScore(stats.count - 1, confidenceLevel); 45 | 46 | final se = stats.sampleValues.standardError; 47 | 48 | final marginOfError = tScore * se; 49 | 50 | return ConfidenceInterval( 51 | stats: stats, 52 | marginOfError: marginOfError, 53 | tScore: tScore, 54 | confidenceLevel: confidenceLevel, 55 | ); 56 | } 57 | 58 | Map toJson() => _$ConfidenceIntervalToJson(this); 59 | 60 | @override 61 | String toString() => ''' 62 | Confidence Level: ${(confidenceLevel.value * 100).toStringAsFixed(0)}% 63 | Margin of Error: ${marginOfError.toStringAsFixed(_toStringPrecision)} 64 | Confidence Interval: [${lowerBound.toStringAsFixed(_toStringPrecision)}, ${upperBound.toStringAsFixed(_toStringPrecision)}]'''; 65 | } 66 | 67 | const _toStringPrecision = 4; 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 2 | 3 | - Removed all deprecated members: 4 | - Removed `LightStats`. 5 | - `average` renamed to `mean`. 6 | - Removed `median` everywhere. 7 | - `Stats`: Replaced `standardDeviation` with `sumOfSquares` 8 | - `Stats`: removed type parameter. Added more complexity than it was worth. 9 | - `ConfidenceInterval` now builds *from* a `Stats` instance 10 | instead of extending it. 11 | - `Stats` can now be calculated from a `Stream`. 12 | - `Stats.transformer` allows calculating updated stats *on the fly*. 13 | 14 | ## 2.2.0 15 | 16 | - Added confidence internal logic: 17 | - `ConfidenceInterval` class and `ConfidenceLevel` enum. 18 | - Added `confidenceInterval` extension to `Iterable`. 19 | - `Stats`: added `besselCorrection` optional parameter to constructors. 20 | - Added `assert` calls to `LightStats` and `Stats` constructors. 21 | - Changed the type of `LightStats.average` and `Stats.standardDeviation` to 22 | `double`. 23 | - Deprecations: 24 | - `LightStats` will be removed. 25 | - `average` deprecated in favor of `mean`. 26 | - `median` deprecated everywhere. 27 | - Require at least Dart 3.7 28 | 29 | ## 2.1.0 30 | 31 | - Export `LightStats`. 32 | - Require at least Dart 3.0 33 | 34 | ## 2.0.0 35 | 36 | - Require at least Dart 2.12 37 | - Null safety 38 | 39 | ## 1.0.1 40 | 41 | - Allow `package:json_annotation` `v4.x`. 42 | 43 | ## 1.0.0 44 | 45 | - Added `LightStats` class and corresponding extension. 46 | - Unlike `Stats`, creating `LightStats` does not create and sort a `List` with 47 | the entire source contents. It is "cheaper" to use, especially with large 48 | inputs. 49 | - `Stats` 50 | - Now has a type argument ``. 51 | - implements `LightStats`. 52 | - Added `fromSortedList` factory. 53 | - `standardError` is now an on-demand calculated property instead of a 54 | field. 55 | - Renamed `mean` to `average`. 56 | - Added `Iterable` extensions: `lightStats`, `stats`, `min`, `max`, `sum` 57 | and `average`. 58 | - Require Dart SDK `>=2.7.0 <3.0.0`. 59 | 60 | ## 0.2.0+3 61 | 62 | - Support latest `package:json_annotation`. 63 | 64 | ## 0.2.0+2 65 | 66 | - Support latest `package:json_annotation`. 67 | 68 | ## 0.2.0+1 69 | 70 | - Fix readme. 71 | 72 | ## 0.2.0 73 | 74 | - Initial release. 75 | -------------------------------------------------------------------------------- /lib/src/t_score.dart: -------------------------------------------------------------------------------- 1 | import 'confidence_level.dart'; 2 | 3 | const biggestSpecifiedDF = 120; 4 | 5 | double calcTScore(int degreesOfFreedom, ConfidenceLevel confidenceLevel) { 6 | if (degreesOfFreedom < 1) { 7 | throw ArgumentError.value( 8 | degreesOfFreedom, 9 | 'degreesOfFreedom', 10 | 'Degrees of freedom must be at least 1.', 11 | ); 12 | } 13 | final scores = confidenceLevel.scores; 14 | 15 | // Check for exact match 16 | if (scores.containsKey(degreesOfFreedom)) { 17 | return scores[degreesOfFreedom]!; 18 | } 19 | 20 | if (degreesOfFreedom > biggestSpecifiedDF) { 21 | return confidenceLevel.infinityValue; 22 | } 23 | 24 | // Find bracketing degrees of freedom for interpolation 25 | int? lowerDf; 26 | int? upperDf; 27 | 28 | for (final dfKey in scores.keys) { 29 | if (dfKey < degreesOfFreedom) { 30 | lowerDf = dfKey; 31 | } else if (dfKey > degreesOfFreedom) { 32 | upperDf = dfKey; 33 | break; // Found the upper bracket 34 | } 35 | // If dfKey == degreesOfFreedom, it would have been caught by the exact 36 | // match check earlier 37 | } 38 | 39 | if (lowerDf != null && upperDf != null) { 40 | final lowerScore = scores[lowerDf]!; 41 | final upperScore = scores[upperDf]!; 42 | 43 | // Linear interpolation: 44 | // score = y1 + (x - x1) * (y2 - y1) / (x2 - x1) 45 | final tScore = 46 | lowerScore + 47 | (degreesOfFreedom - lowerDf) * 48 | (upperScore - lowerScore) / 49 | (upperDf - lowerDf); 50 | return tScore; 51 | } 52 | 53 | // If no suitable brackets found (e.g., df is larger than any specific df but 54 | // less than infinity threshold, or some other unexpected state). 55 | // This part of the logic might need refinement based on table structure. 56 | // Given the check for `degreesOfFreedom >= largestSpecificDf` earlier, this 57 | // path should ideally be for cases where df is within the range of sortedDfs 58 | // but bracketing failed. 59 | throw ArgumentError.value( 60 | degreesOfFreedom, 61 | 'degreesOfFreedom', 62 | 'Cannot interpolate t-score for df=$degreesOfFreedom at ' 63 | 'CL=${confidenceLevel.name}. ' 64 | 'Check table data or consider using a statistical library for more ' 65 | 'precision.', 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /example/confidence_internal_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:stats/stats.dart'; 2 | 3 | void main() { 4 | // Example benchmark data (e.g., response times in milliseconds) 5 | final benchmarkData = [ 6 | 120.5, 130.2, 115.8, 125.1, 140.0, // 7 | 110.3, 135.5, 122.9, 128.7, 132.0, 8 | 121.0, 129.5, 116.0, 124.8, 139.0, 9 | 111.0, 136.0, 123.0, 127.0, 131.0, 10 | 100.0, 11 | ]; 12 | 13 | print('--- Sample Data ---'); 14 | print('Number of items: ${benchmarkData.length}'); 15 | print('Raw Data: $benchmarkData\n'); 16 | 17 | final ci95 = benchmarkData.confidenceInterval(ConfidenceLevel.percent95); 18 | print('95% Confidence Interval:'); 19 | print(ci95); 20 | print('\n'); 21 | 22 | final ci99 = benchmarkData.confidenceInterval(ConfidenceLevel.percent99); 23 | print('99% Confidence Interval:'); 24 | print(ci99); 25 | print('\n'); 26 | 27 | // --- Example 2: Comparing two sets of benchmark data --- 28 | print('--- Comparing Two Benchmark Sets ---'); 29 | final oldVersionData = [ 30 | 150.0, 160.0, 145.0, 155.0, 170.0, // 31 | 140.0, 165.0, 152.0, 158.0, 162.0, 32 | ]; // n = 10 33 | print('Old Version Benchmark Data: $oldVersionData'); 34 | 35 | final newVersionData = [ 36 | 120.0, 130.0, 115.0, 125.0, 140.0, // 37 | 110.0, 135.0, 122.0, 128.0, 132.0, 38 | ]; // n = 10 39 | print('New Version Benchmark Data: $newVersionData\n'); 40 | 41 | final ciOld = oldVersionData.confidenceInterval(ConfidenceLevel.percent95); 42 | print('Old Version 95% CI:'); 43 | print(ciOld); 44 | print(''); 45 | 46 | final ciNew = newVersionData.confidenceInterval(ConfidenceLevel.percent95); 47 | print('New Version 95% CI:'); 48 | print(ciNew); 49 | print(''); 50 | 51 | print('--- Interpretation for Comparison ---'); 52 | if (ciOld.lowerBound > ciNew.upperBound || 53 | ciNew.lowerBound > ciOld.upperBound) { 54 | print( 55 | 'The confidence intervals DO NOT OVERLAP. ' 56 | 'This suggests a statistically significant difference.', 57 | ); 58 | if (ciOld.stats.mean > ciNew.stats.mean) { 59 | print('The New Version is statistically significantly faster/better.'); 60 | } else { 61 | print('The Old Version is statistically significantly faster/better.'); 62 | } 63 | } else { 64 | print( 65 | 'The confidence intervals OVERLAP. This suggests that the difference ' 66 | 'between the two versions is not statistically significant at the 95% ' 67 | 'confidence level.', 68 | ); 69 | print( 70 | 'Further runs or a more powerful statistical test ' 71 | '(like a t-test on the difference) might be needed to confirm a ' 72 | 'difference, or the difference might be negligible.', 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/stats_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:checks/checks.dart'; 4 | import 'package:stats/stats.dart'; 5 | import 'package:test/scaffolding.dart'; 6 | 7 | Stats _validateJson( 8 | Iterable values, 9 | num expectedSum, 10 | Map expectedJson, 11 | ) { 12 | final stats = values.stats; 13 | 14 | check(stats.toJson()).deepEquals(expectedJson); 15 | 16 | check(values.sum).equals(expectedSum); 17 | check(stats.mean).isCloseTo(values.mean, 0.0000001); 18 | check(stats.min).equals(values.min); 19 | check(stats.max).equals(values.max); 20 | return stats; 21 | } 22 | 23 | void main() { 24 | test('empty source is not allowed', () { 25 | check(() => [].stats).throws().which( 26 | (it) => it.has((p0) => p0.message, 'message').equals('Cannot be empty.'), 27 | ); 28 | }); 29 | 30 | group('empty', () { 31 | test('sum', () { 32 | check([].sum).equals(0); 33 | check([].sum).equals(0); 34 | check([].sum).equals(0); 35 | }); 36 | 37 | test('max', () { 38 | check(() => [].max).throws(); 39 | }); 40 | 41 | test('min', () { 42 | check(() => [].min).throws(); 43 | }); 44 | 45 | test('mean', () { 46 | check(() => [].mean).throws(); 47 | }); 48 | }); 49 | 50 | test('trivial', () { 51 | _validateJson( 52 | [0], 53 | 0, 54 | {'count': 1, 'mean': 0.0, 'max': 0, 'min': 0, 'sumOfSquares': 0.0}, 55 | ); 56 | }); 57 | 58 | test('two simple values', () { 59 | _validateJson( 60 | [0, 2], 61 | 2, 62 | {'count': 2, 'mean': 1.0, 'min': 0, 'max': 2, 'sumOfSquares': 2.0}, 63 | ); 64 | }); 65 | 66 | test('10 values', () { 67 | _validateJson(Iterable.generate(10, (i) => i), 45, { 68 | 'count': 10, 69 | 'mean': 4.5, 70 | 'min': 0, 71 | 'max': 9, 72 | 'sumOfSquares': 82.5, 73 | }); 74 | }); 75 | 76 | test('11 values', () { 77 | _validateJson(Iterable.generate(11, (i) => i), 55, { 78 | 'count': 11, 79 | 'mean': 5.0, 80 | 'min': 0, 81 | 'max': 10, 82 | 'sumOfSquares': 110.0, 83 | }); 84 | }); 85 | 86 | test('precision', () { 87 | final stats = 88 | _validateJson(Iterable.generate(100, math.sqrt), 661.4629471031477, { 89 | 'count': 100, 90 | 'mean': 6.614629471031478, 91 | 'max': 9.9498743710662, 92 | 'min': 0.0, 93 | 'sumOfSquares': 574.6676960961834, 94 | }); 95 | 96 | check(stats.withPrecision(4).toJson()).deepEquals({ 97 | 'count': 100, 98 | 'mean': 6.615, 99 | 'max': 9.95, 100 | 'min': 0.0, 101 | 'sumOfSquares': 574.7, 102 | }); 103 | 104 | check(stats.withPrecision(1).toJson()).deepEquals({ 105 | 'count': 100, 106 | 'mean': 7.0, 107 | 'max': 10.0, 108 | 'min': 0.0, 109 | 'sumOfSquares': 600.0, 110 | }); 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /test/confidence_internal_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:stats/stats.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | // Used https://www.statskingdom.com/confidence-interval-calculator.html 6 | // to calculate target values 7 | 8 | group('set 5', () { 9 | const threshold = 0.0003; 10 | const data = [1.0, 2.0, 3.0, 4.0, 5.0]; 11 | 12 | final expected = { 13 | ConfidenceLevel.percent90: { 14 | 'marginOfError': closeTo(1.507443319, threshold), 15 | 'tScore': 2.1318, 16 | 'confidenceLevel': 'percent90', 17 | }, 18 | ConfidenceLevel.percent95: { 19 | 'marginOfError': closeTo(1.963243161, threshold), 20 | 'tScore': 2.7764, 21 | 'confidenceLevel': 'percent95', 22 | }, 23 | ConfidenceLevel.percent99: { 24 | 'marginOfError': closeTo(3.255586705, threshold), 25 | 'tScore': 4.604, 26 | 'confidenceLevel': 'percent99', 27 | }, 28 | }; 29 | 30 | for (var entry in expected.entries) { 31 | test( 32 | 'calculate confidence interval ${entry.key} correctly for a sample', 33 | () { 34 | final interval = ConfidenceInterval.calculate(data.stats, entry.key); 35 | expect(interval.toJson()..remove('stats'), entry.value); 36 | }, 37 | ); 38 | } 39 | }); 40 | 41 | group('set of 100 - even', () { 42 | final data = List.generate(101, (index) => index - 50); 43 | 44 | // This is HIGHER because 100 is interpolated 45 | // Still less than 1% of the target value 46 | const threshold = 0.016; 47 | 48 | final expected = { 49 | ConfidenceLevel.percent90: { 50 | 'marginOfError': closeTo(4.8404, threshold), 51 | 'tScore': closeTo(1.6602, threshold), 52 | 'confidenceLevel': 'percent90', 53 | }, 54 | ConfidenceLevel.percent95: { 55 | 'marginOfError': closeTo(5.7842, threshold), 56 | 'tScore': closeTo(1.984, threshold), 57 | 'confidenceLevel': 'percent95', 58 | }, 59 | ConfidenceLevel.percent99: { 60 | 'marginOfError': closeTo(7.6557, threshold), 61 | 'tScore': closeTo(2.6259, threshold), 62 | 'confidenceLevel': 'percent99', 63 | }, 64 | }; 65 | 66 | for (var entry in expected.entries) { 67 | test( 68 | 'calculate confidence interval ${entry.key} correctly for a sample', 69 | () { 70 | final interval = ConfidenceInterval.calculate(data.stats, entry.key); 71 | expect(interval.toJson()..remove('stats'), entry.value); 72 | }, 73 | ); 74 | } 75 | }); 76 | 77 | const bigNumber = 1001; 78 | group('set of $bigNumber - clustered', () { 79 | final data = List.generate( 80 | bigNumber, 81 | (index) => index / (bigNumber - 1) - 0.5, 82 | ); 83 | 84 | const threshold = 0.00001; 85 | 86 | final expected = { 87 | ConfidenceLevel.percent90: { 88 | 'marginOfError': closeTo(0.0150317, threshold), 89 | 'tScore': closeTo(1.645, threshold), 90 | 'confidenceLevel': 'percent90', 91 | }, 92 | ConfidenceLevel.percent95: { 93 | 'marginOfError': closeTo(0.01791, threshold), 94 | 'tScore': 1.96, 95 | 'confidenceLevel': 'percent95', 96 | }, 97 | ConfidenceLevel.percent99: { 98 | 'marginOfError': closeTo(0.023539, threshold), 99 | 'tScore': 2.576, 100 | 'confidenceLevel': 'percent99', 101 | }, 102 | }; 103 | 104 | for (var entry in expected.entries) { 105 | test( 106 | 'calculate confidence interval ${entry.key} correctly for a sample', 107 | () { 108 | final interval = ConfidenceInterval.calculate(data.stats, entry.key); 109 | expect(interval.toJson()..remove('stats'), entry.value); 110 | }, 111 | ); 112 | } 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /test/t_score_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:stats/src/confidence_level.dart'; 2 | import 'package:stats/src/t_score.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('t-scores for confidence levels', () { 7 | for (var level in ConfidenceLevel.values) { 8 | test(level, () { 9 | expect( 10 | level.scores, 11 | hasLength(ConfidenceLevel.percent95.scores.length), 12 | ); 13 | expect( 14 | level.scores.keys, 15 | orderedEquals(ConfidenceLevel.percent95.scores.keys), 16 | ); 17 | expect(level.scores.keys.first, 1); 18 | expect(level.scores.keys.last, biggestSpecifiedDF); 19 | 20 | MapEntry? previous; 21 | for (var entry in level.scores.entries) { 22 | if (previous == null) { 23 | previous = entry; 24 | continue; 25 | } 26 | expect(entry.key, greaterThan(previous.key)); 27 | expect(entry.value, lessThan(previous.value)); 28 | previous = entry; 29 | } 30 | }); 31 | } 32 | }); 33 | 34 | group('Exact Matches', () { 35 | test('should return exact t-score for percent95, df=1', () { 36 | expect(calcTScore(1, ConfidenceLevel.percent95), 12.706); 37 | }); 38 | 39 | test('should return exact t-score for percent90, df=10', () { 40 | expect(calcTScore(10, ConfidenceLevel.percent90), 1.812); 41 | }); 42 | 43 | test('should return exact t-score for percent99, df=20', () { 44 | expect(calcTScore(20, ConfidenceLevel.percent99), 2.845); 45 | }); 46 | 47 | test('should return exact t-score for percent95, df=120', () { 48 | expect(calcTScore(120, ConfidenceLevel.percent95), 1.980); 49 | }); 50 | }); 51 | 52 | group('Infinity Key (Large DF)', () { 53 | test('should return infinity t-score for percent95, df=120 ' 54 | '(boundary of largest specific)', () { 55 | expect(calcTScore(121, ConfidenceLevel.percent95), 1.960); 56 | expect(calcTScore(200, ConfidenceLevel.percent95), 1.960); 57 | expect(calcTScore(1000000, ConfidenceLevel.percent95), 1.960); 58 | }); 59 | 60 | test('should return infinity t-score for percent90, df=500', () { 61 | expect(calcTScore(500, ConfidenceLevel.percent90), 1.645); 62 | }); 63 | 64 | test('should return infinity t-score for percent99, df=1000', () { 65 | expect(calcTScore(1000, ConfidenceLevel.percent99), 2.576); 66 | }); 67 | }); 68 | 69 | group('Interpolation', () { 70 | test('should interpolate for percent95, df=17 (between 16 and 18)', () { 71 | expect( 72 | calcTScore(17, ConfidenceLevel.percent95), 73 | closeTo(2.11, 0.000001), 74 | ); 75 | }); 76 | 77 | test('should interpolate for percent90, df=25 (between 20 and 29)', () { 78 | expect( 79 | calcTScore(25, ConfidenceLevel.percent90), 80 | closeTo(1.710555, 0.000001), 81 | ); 82 | }); 83 | 84 | test('should interpolate for percent99, df=40 (between 30 and 60)', () { 85 | expect( 86 | calcTScore(40, ConfidenceLevel.percent99), 87 | closeTo(2.720, 0.00001), 88 | ); 89 | }); 90 | 91 | test( 92 | 'should interpolate for percent95, df=2 (exact match, but tests loop)', 93 | () { 94 | // This will hit the exact match, but good to ensure loop logic is 95 | // sound 96 | expect(calcTScore(2, ConfidenceLevel.percent95), 4.303); 97 | }, 98 | ); 99 | }); 100 | 101 | test( 102 | 'should throw ArgumentError if df is below smallest tabulated value', 103 | () { 104 | expect( 105 | () => calcTScore(0, ConfidenceLevel.percent95), 106 | throwsA( 107 | isA().having( 108 | (e) => e.message, 109 | 'message', 110 | 'Degrees of freedom must be at least 1.', // Current error message 111 | ), 112 | ), 113 | ); 114 | }, 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/stats.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:math' as math; 4 | 5 | import 'package:json_annotation/json_annotation.dart'; 6 | 7 | import 'stats_sinks.dart'; 8 | 9 | part 'stats.g.dart'; 10 | 11 | @JsonSerializable() 12 | /// Represents important statistical values of a collection of numbers. 13 | class Stats { 14 | Stats({ 15 | required this.count, 16 | required this.mean, 17 | required this.min, 18 | required this.max, 19 | required this.sumOfSquares, 20 | }) : assert(count > 0), 21 | assert(sumOfSquares.isFinite), 22 | assert(sumOfSquares >= 0), 23 | assert(mean >= min), 24 | assert(mean <= max), 25 | assert(mean.isFinite), 26 | assert(min.isFinite), 27 | assert(max.isFinite); 28 | 29 | /// The number of items in the source collection. 30 | final int count; 31 | 32 | /// The minimum value in the source collection. 33 | final num min; 34 | 35 | /// The maximum value in the source collection. 36 | final num max; 37 | 38 | /// The mean (average) value in the source collection. 39 | final double mean; 40 | 41 | /// The sum of all of each value in the source collection squared. 42 | /// 43 | /// Important for many other statistical calculations. 44 | final double sumOfSquares; 45 | 46 | /// Represents three typical statistical values related to [Stats]. 47 | /// 48 | /// * `variance` is [Stats.sumOfSquares]`/`[Stats.count]. 49 | /// * `standardDeviation` is the square-root of `variance`. 50 | /// * `standardError` is `standardDeviation` divided by the square-root of 51 | /// `count`. 52 | ({double variance, double standardDeviation, double standardError}) 53 | get populationValues => _values(population: true); 54 | 55 | /// Represents three typical statistical values related to [Stats]. 56 | /// 57 | /// * `variance` is [Stats.sumOfSquares]`/(`[Stats.count]`-1)`. 58 | /// * `standardDeviation` is the square-root of `variance`. 59 | /// * `standardError` is `standardDeviation` divided by the square-root of 60 | /// `count`. 61 | ({double variance, double standardDeviation, double standardError}) 62 | get sampleValues => _values(population: false); 63 | 64 | /// Assumes all values in [source] are [num.isFinite]. 65 | factory Stats.fromData(Iterable source) { 66 | final state = StatsSink(); 67 | for (final value in source) { 68 | state.add(value); 69 | } 70 | return state.emit(); 71 | } 72 | 73 | /// Assumes all values in [source] are [num.isFinite]. 74 | static Future fromStream(Stream source) async { 75 | final state = StatsSink(); 76 | await for (final value in source) { 77 | state.add(value); 78 | } 79 | return state.emit(); 80 | } 81 | 82 | static const Converter transformer = StatsConverter(); 83 | 84 | Stats withPrecision(int precision) { 85 | double fixDouble(double input) => 86 | double.parse(input.toStringAsPrecision(precision)); 87 | 88 | num fix(num input) => input is int ? input : fixDouble(input as double); 89 | 90 | return Stats( 91 | count: count, 92 | mean: fixDouble(mean), 93 | min: fix(min), 94 | max: fix(max), 95 | sumOfSquares: fixDouble(sumOfSquares), 96 | ); 97 | } 98 | 99 | factory Stats.fromJson(Map json) => _$StatsFromJson(json); 100 | 101 | Map toJson() => _$StatsToJson(this); 102 | 103 | ({double variance, double standardDeviation, double standardError}) _values({ 104 | required bool population, 105 | }) { 106 | final variance = 107 | population 108 | ? sumOfSquares / count 109 | : count == 1 110 | ? double.nan 111 | : sumOfSquares / (count - 1); 112 | final standardDeviation = math.sqrt(variance); 113 | return ( 114 | variance: variance, 115 | standardDeviation: standardDeviation, 116 | standardError: standardDeviation / math.sqrt(count), 117 | ); 118 | } 119 | } 120 | --------------------------------------------------------------------------------