sdtoolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdtoolkit/__init__.py +31 -0
- sdtoolkit/__main__.py +5 -0
- sdtoolkit/cli.py +93 -0
- sdtoolkit/core.py +199 -0
- sdtoolkit-0.1.0.dist-info/METADATA +247 -0
- sdtoolkit-0.1.0.dist-info/RECORD +10 -0
- sdtoolkit-0.1.0.dist-info/WHEEL +5 -0
- sdtoolkit-0.1.0.dist-info/entry_points.txt +2 -0
- sdtoolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- sdtoolkit-0.1.0.dist-info/top_level.txt +1 -0
sdtoolkit/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""sdtoolkit: dependency-free standard deviation, variance and RSD."""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
StatisticsError,
|
|
5
|
+
Summary,
|
|
6
|
+
coefficient_of_variation,
|
|
7
|
+
describe,
|
|
8
|
+
mean,
|
|
9
|
+
population_stdev,
|
|
10
|
+
population_variance,
|
|
11
|
+
relative_std_dev,
|
|
12
|
+
sample_stdev,
|
|
13
|
+
sample_variance,
|
|
14
|
+
sum_of_squares,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"StatisticsError",
|
|
21
|
+
"Summary",
|
|
22
|
+
"coefficient_of_variation",
|
|
23
|
+
"describe",
|
|
24
|
+
"mean",
|
|
25
|
+
"population_stdev",
|
|
26
|
+
"population_variance",
|
|
27
|
+
"relative_std_dev",
|
|
28
|
+
"sample_stdev",
|
|
29
|
+
"sample_variance",
|
|
30
|
+
"sum_of_squares",
|
|
31
|
+
]
|
sdtoolkit/__main__.py
ADDED
sdtoolkit/cli.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Command-line interface: ``python -m sdtoolkit 2 4 4 4 5 5 7 9``."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import csv
|
|
5
|
+
import sys
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from .core import StatisticsError, describe
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_number(text: str, where: str) -> float:
|
|
12
|
+
try:
|
|
13
|
+
return float(text)
|
|
14
|
+
except ValueError:
|
|
15
|
+
raise StatisticsError(f"{where}: {text!r} is not a number") from None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _read_column(path: str, column: str) -> List[float]:
|
|
19
|
+
with open(path, newline="", encoding="utf-8") as f:
|
|
20
|
+
# Lines starting with '#' are comments.
|
|
21
|
+
reader = csv.DictReader(line for line in f if not line.startswith("#"))
|
|
22
|
+
if reader.fieldnames is None or column not in reader.fieldnames:
|
|
23
|
+
raise StatisticsError(
|
|
24
|
+
f"column {column!r} not found; available: {reader.fieldnames}"
|
|
25
|
+
)
|
|
26
|
+
values = []
|
|
27
|
+
for i, row in enumerate(reader, start=1):
|
|
28
|
+
cell = (row[column] or "").strip()
|
|
29
|
+
if not cell:
|
|
30
|
+
raise StatisticsError(
|
|
31
|
+
f"{path}: data row {i}: empty cell in {column!r} "
|
|
32
|
+
"(missing values are not skipped)"
|
|
33
|
+
)
|
|
34
|
+
values.append(_parse_number(cell, f"{path}: data row {i}"))
|
|
35
|
+
return values
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fmt(x: float) -> str:
|
|
39
|
+
return f"{x:.10g}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def format_summary(s) -> str:
|
|
43
|
+
kind = "population" if s.population else "sample"
|
|
44
|
+
denom = "N" if s.population else "n - 1"
|
|
45
|
+
rows = [
|
|
46
|
+
("n", str(s.n)),
|
|
47
|
+
("mean", _fmt(s.mean)),
|
|
48
|
+
("min", _fmt(s.minimum)),
|
|
49
|
+
("max", _fmt(s.maximum)),
|
|
50
|
+
("range", _fmt(s.range)),
|
|
51
|
+
("sum of squares", _fmt(s.sum_of_squares)),
|
|
52
|
+
(f"{kind} variance", _fmt(s.variance)),
|
|
53
|
+
(f"{kind} std dev", _fmt(s.stdev)),
|
|
54
|
+
("RSD (%)", "undefined (mean is 0)" if s.rsd_percent is None else _fmt(s.rsd_percent)),
|
|
55
|
+
]
|
|
56
|
+
width = max(len(k) for k, _ in rows)
|
|
57
|
+
lines = [f"{k:<{width}} {v}" for k, v in rows]
|
|
58
|
+
lines.append(f"(divisor: {denom})")
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
63
|
+
parser = argparse.ArgumentParser(
|
|
64
|
+
prog="sdtoolkit",
|
|
65
|
+
description="Mean, variance, standard deviation and RSD of a dataset.",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument("values", nargs="*", help="numbers to summarise")
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--population", action="store_true",
|
|
70
|
+
help="treat data as the whole population (divide by N, not n - 1)",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument("--csv", metavar="FILE", help="read values from a CSV file")
|
|
73
|
+
parser.add_argument("--column", metavar="NAME", help="CSV column to use (with --csv)")
|
|
74
|
+
args = parser.parse_args(argv)
|
|
75
|
+
|
|
76
|
+
if args.csv and args.values:
|
|
77
|
+
parser.error("give either values or --csv, not both")
|
|
78
|
+
if bool(args.csv) != bool(args.column):
|
|
79
|
+
parser.error("--csv and --column must be used together")
|
|
80
|
+
if not args.csv and not args.values:
|
|
81
|
+
parser.error("no values given")
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
if args.csv:
|
|
85
|
+
data = _read_column(args.csv, args.column)
|
|
86
|
+
else:
|
|
87
|
+
data = [_parse_number(v, f"argument {i + 1}") for i, v in enumerate(args.values)]
|
|
88
|
+
summary = describe(data, population=args.population)
|
|
89
|
+
except (StatisticsError, OSError) as e:
|
|
90
|
+
print(f"error: {e}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
print(format_summary(summary))
|
|
93
|
+
return 0
|
sdtoolkit/core.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Core dispersion statistics.
|
|
2
|
+
|
|
3
|
+
Every function takes an iterable of numbers. Inputs are validated and
|
|
4
|
+
converted to ``float`` first:
|
|
5
|
+
|
|
6
|
+
* ``int``, ``float`` and other real numbers are accepted.
|
|
7
|
+
* ``decimal.Decimal`` and ``fractions.Fraction`` are accepted and converted
|
|
8
|
+
to ``float``. Results are floats; exact decimal/rational precision is not
|
|
9
|
+
preserved.
|
|
10
|
+
* ``bool``, ``None``, strings, NaN and infinities are rejected with a
|
|
11
|
+
:class:`StatisticsError`. Missing values are never skipped silently: clean
|
|
12
|
+
your data before calling these functions.
|
|
13
|
+
|
|
14
|
+
Variance is computed with a two-pass algorithm: the mean first, then the
|
|
15
|
+
squared deviations from it, summed with :func:`math.fsum`. A small correction
|
|
16
|
+
term (the sum of deviations, which is zero in exact arithmetic) absorbs the
|
|
17
|
+
rounding error left in the mean. The shortcut ``sum(x**2) - n*mean**2`` is
|
|
18
|
+
never used because it loses most of its significant digits when the data
|
|
19
|
+
sits far from zero.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
import numbers
|
|
24
|
+
from dataclasses import dataclass, asdict
|
|
25
|
+
from decimal import Decimal
|
|
26
|
+
from typing import Iterable, List, Optional
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"StatisticsError",
|
|
30
|
+
"Summary",
|
|
31
|
+
"mean",
|
|
32
|
+
"sum_of_squares",
|
|
33
|
+
"population_variance",
|
|
34
|
+
"sample_variance",
|
|
35
|
+
"population_stdev",
|
|
36
|
+
"sample_stdev",
|
|
37
|
+
"relative_std_dev",
|
|
38
|
+
"coefficient_of_variation",
|
|
39
|
+
"describe",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StatisticsError(ValueError):
|
|
44
|
+
"""Raised for empty, too-short or invalid input."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _to_floats(data: Iterable) -> List[float]:
|
|
48
|
+
if isinstance(data, (str, bytes)):
|
|
49
|
+
raise StatisticsError("expected an iterable of numbers, got a string")
|
|
50
|
+
values = []
|
|
51
|
+
for i, x in enumerate(data):
|
|
52
|
+
if isinstance(x, bool):
|
|
53
|
+
raise StatisticsError(f"item {i}: booleans are not numbers ({x!r})")
|
|
54
|
+
if x is None:
|
|
55
|
+
raise StatisticsError(
|
|
56
|
+
f"item {i}: None (missing value); remove or fill it before computing"
|
|
57
|
+
)
|
|
58
|
+
if isinstance(x, str):
|
|
59
|
+
raise StatisticsError(
|
|
60
|
+
f"item {i}: strings are not accepted ({x!r}); convert with float() first"
|
|
61
|
+
)
|
|
62
|
+
if not isinstance(x, (numbers.Real, Decimal)):
|
|
63
|
+
raise StatisticsError(f"item {i}: {x!r} is not a real number")
|
|
64
|
+
try:
|
|
65
|
+
f = float(x)
|
|
66
|
+
except OverflowError:
|
|
67
|
+
raise StatisticsError(f"item {i}: {x!r} is too large for a float") from None
|
|
68
|
+
if math.isnan(f):
|
|
69
|
+
raise StatisticsError(f"item {i}: NaN is not allowed")
|
|
70
|
+
if math.isinf(f):
|
|
71
|
+
raise StatisticsError(f"item {i}: infinity is not allowed")
|
|
72
|
+
values.append(f)
|
|
73
|
+
if not values:
|
|
74
|
+
raise StatisticsError("no data points: at least one value is required")
|
|
75
|
+
return values
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _mean_and_ss(values: List[float]):
|
|
79
|
+
n = len(values)
|
|
80
|
+
m = math.fsum(values) / n
|
|
81
|
+
deviations = [x - m for x in values]
|
|
82
|
+
# Corrected two-pass: sum(d) is 0 in exact arithmetic; subtracting
|
|
83
|
+
# sum(d)**2 / n removes the error caused by rounding in m.
|
|
84
|
+
ss = math.fsum(d * d for d in deviations) - math.fsum(deviations) ** 2 / n
|
|
85
|
+
return m, max(ss, 0.0)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _need_two(n: int) -> None:
|
|
89
|
+
if n < 2:
|
|
90
|
+
raise StatisticsError(
|
|
91
|
+
f"sample statistics need at least 2 data points, got {n}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def mean(data: Iterable) -> float:
|
|
96
|
+
"""Arithmetic mean."""
|
|
97
|
+
values = _to_floats(data)
|
|
98
|
+
return math.fsum(values) / len(values)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def sum_of_squares(data: Iterable) -> float:
|
|
102
|
+
"""Sum of squared deviations from the mean, SS = sum((x - mean)**2)."""
|
|
103
|
+
return _mean_and_ss(_to_floats(data))[1]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def population_variance(data: Iterable) -> float:
|
|
107
|
+
"""Population variance, SS / N."""
|
|
108
|
+
values = _to_floats(data)
|
|
109
|
+
return _mean_and_ss(values)[1] / len(values)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def sample_variance(data: Iterable) -> float:
|
|
113
|
+
"""Sample variance with Bessel's correction, SS / (n - 1). Needs n >= 2."""
|
|
114
|
+
values = _to_floats(data)
|
|
115
|
+
_need_two(len(values))
|
|
116
|
+
return _mean_and_ss(values)[1] / (len(values) - 1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def population_stdev(data: Iterable) -> float:
|
|
120
|
+
"""Population standard deviation, sqrt(SS / N)."""
|
|
121
|
+
return math.sqrt(population_variance(data))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def sample_stdev(data: Iterable) -> float:
|
|
125
|
+
"""Sample standard deviation, sqrt(SS / (n - 1)). Needs n >= 2."""
|
|
126
|
+
return math.sqrt(sample_variance(data))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def coefficient_of_variation(data: Iterable, population: bool = False) -> float:
|
|
130
|
+
"""Standard deviation divided by |mean|, as a ratio (0.05 means 5 %).
|
|
131
|
+
|
|
132
|
+
Uses the sample standard deviation unless ``population=True``.
|
|
133
|
+
Raises StatisticsError when the mean is zero.
|
|
134
|
+
"""
|
|
135
|
+
values = _to_floats(data)
|
|
136
|
+
n = len(values)
|
|
137
|
+
if not population:
|
|
138
|
+
_need_two(n)
|
|
139
|
+
m, ss = _mean_and_ss(values)
|
|
140
|
+
if m == 0:
|
|
141
|
+
raise StatisticsError("mean is zero; relative dispersion is undefined")
|
|
142
|
+
sd = math.sqrt(ss / (n if population else n - 1))
|
|
143
|
+
return sd / abs(m)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def relative_std_dev(data: Iterable, population: bool = False) -> float:
|
|
147
|
+
"""Relative standard deviation in percent: 100 * SD / |mean|.
|
|
148
|
+
|
|
149
|
+
Uses the sample standard deviation unless ``population=True``.
|
|
150
|
+
Raises StatisticsError when the mean is zero.
|
|
151
|
+
"""
|
|
152
|
+
return 100.0 * coefficient_of_variation(data, population=population)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class Summary:
|
|
157
|
+
"""Result of :func:`describe`. ``rsd_percent`` is None when the mean is 0."""
|
|
158
|
+
|
|
159
|
+
n: int
|
|
160
|
+
mean: float
|
|
161
|
+
minimum: float
|
|
162
|
+
maximum: float
|
|
163
|
+
range: float
|
|
164
|
+
sum_of_squares: float
|
|
165
|
+
variance: float
|
|
166
|
+
stdev: float
|
|
167
|
+
rsd_percent: Optional[float]
|
|
168
|
+
population: bool
|
|
169
|
+
|
|
170
|
+
def as_dict(self) -> dict:
|
|
171
|
+
return asdict(self)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def describe(data: Iterable, population: bool = False) -> Summary:
|
|
175
|
+
"""Summary of location and spread.
|
|
176
|
+
|
|
177
|
+
``population=False`` (default) uses n - 1 in the denominator and needs
|
|
178
|
+
at least two values; ``population=True`` uses N.
|
|
179
|
+
"""
|
|
180
|
+
values = _to_floats(data)
|
|
181
|
+
n = len(values)
|
|
182
|
+
if not population:
|
|
183
|
+
_need_two(n)
|
|
184
|
+
m, ss = _mean_and_ss(values)
|
|
185
|
+
var = ss / (n if population else n - 1)
|
|
186
|
+
sd = math.sqrt(var)
|
|
187
|
+
lo, hi = min(values), max(values)
|
|
188
|
+
return Summary(
|
|
189
|
+
n=n,
|
|
190
|
+
mean=m,
|
|
191
|
+
minimum=lo,
|
|
192
|
+
maximum=hi,
|
|
193
|
+
range=hi - lo,
|
|
194
|
+
sum_of_squares=ss,
|
|
195
|
+
variance=var,
|
|
196
|
+
stdev=sd,
|
|
197
|
+
rsd_percent=None if m == 0 else 100.0 * (sd / abs(m)),
|
|
198
|
+
population=population,
|
|
199
|
+
)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sdtoolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dependency-free standard deviation, variance and relative standard deviation for Python
|
|
5
|
+
Author: Ramesh-xyz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://standarddeviationcalculator.net/
|
|
8
|
+
Project-URL: Documentation, https://ramesh-xyz.github.io/statistics-calculator-toolkit/python.html
|
|
9
|
+
Project-URL: Source, https://github.com/Ramesh-xyz/statistics-calculator-toolkit
|
|
10
|
+
Project-URL: Issues, https://github.com/Ramesh-xyz/statistics-calculator-toolkit/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/Ramesh-xyz/statistics-calculator-toolkit/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: standard deviation,variance,coefficient of variation,statistics
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# sdtoolkit for Python
|
|
22
|
+
|
|
23
|
+
Part of [statistics-calculator-toolkit](https://github.com/Ramesh-xyz/statistics-calculator-toolkit), which ships the same API for
|
|
24
|
+
[several languages](https://ramesh-xyz.github.io/statistics-calculator-toolkit/).
|
|
25
|
+
|
|
26
|
+
`sdtoolkit` is a small, dependency-free Python package for standard
|
|
27
|
+
deviation, variance, sum of squares, relative standard deviation (RSD) and
|
|
28
|
+
coefficient of variation, with a command-line interface. It uses only the
|
|
29
|
+
standard library and supports Python 3.9 and later.
|
|
30
|
+
|
|
31
|
+
## Problems it solves
|
|
32
|
+
|
|
33
|
+
- **Picking the wrong formula.** Population and sample versions have
|
|
34
|
+
separate, explicitly named functions, and the CLI prints which divisor it
|
|
35
|
+
used.
|
|
36
|
+
- **Silent precision loss.** The shortcut `Σx² − (Σx)²/n` can return zero or
|
|
37
|
+
even a negative variance for data far from zero. sdtoolkit uses a two-pass
|
|
38
|
+
algorithm with `math.fsum`.
|
|
39
|
+
- **Garbage in, number out.** `None`, `NaN`, infinities, booleans and strings
|
|
40
|
+
raise a clear error instead of producing a plausible-looking result. Missing
|
|
41
|
+
values are never skipped silently.
|
|
42
|
+
- **Undefined RSD.** A zero mean raises an error instead of returning
|
|
43
|
+
infinity or dividing by zero.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
| function | returns |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `mean(data)` | arithmetic mean |
|
|
50
|
+
| `sum_of_squares(data)` | SS = Σ(x − x̄)² |
|
|
51
|
+
| `population_variance(data)` | SS / N |
|
|
52
|
+
| `sample_variance(data)` | SS / (n − 1) |
|
|
53
|
+
| `population_stdev(data)` | √(SS / N) |
|
|
54
|
+
| `sample_stdev(data)` | √(SS / (n − 1)) |
|
|
55
|
+
| `relative_std_dev(data, population=False)` | 100 × SD / \|mean\|, in percent |
|
|
56
|
+
| `coefficient_of_variation(data, population=False)` | SD / \|mean\|, as a ratio |
|
|
57
|
+
| `describe(data, population=False)` | `Summary` dataclass with all of the above plus min, max, range |
|
|
58
|
+
|
|
59
|
+
All errors are raised as `sdtoolkit.StatisticsError`, a subclass of
|
|
60
|
+
`ValueError`.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pip install sdtoolkit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or from a clone, without installing:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
PYTHONPATH=src python -m sdtoolkit 1 2 3
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
### Python
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
>>> import sdtoolkit as sd
|
|
80
|
+
>>> data = [2, 4, 4, 4, 5, 5, 7, 9]
|
|
81
|
+
>>> sd.mean(data)
|
|
82
|
+
5.0
|
|
83
|
+
>>> sd.sum_of_squares(data)
|
|
84
|
+
32.0
|
|
85
|
+
>>> sd.population_variance(data), sd.population_stdev(data)
|
|
86
|
+
(4.0, 2.0)
|
|
87
|
+
>>> sd.sample_variance(data), sd.sample_stdev(data)
|
|
88
|
+
(4.571428571428571, 2.138089935299395)
|
|
89
|
+
>>> sd.relative_std_dev(data)
|
|
90
|
+
42.7617987059879
|
|
91
|
+
>>> sd.coefficient_of_variation(data, population=True)
|
|
92
|
+
0.4
|
|
93
|
+
>>> sd.describe(data)
|
|
94
|
+
Summary(n=8, mean=5.0, minimum=2.0, maximum=9.0, range=7.0, sum_of_squares=32.0, variance=4.571428571428571, stdev=2.138089935299395, rsd_percent=42.7617987059879, population=False)
|
|
95
|
+
>>> sd.describe(data, population=True).as_dict()["stdev"]
|
|
96
|
+
2.0
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Command line
|
|
100
|
+
|
|
101
|
+
```console
|
|
102
|
+
$ python -m sdtoolkit 2 4 4 4 5 5 7 9
|
|
103
|
+
n 8
|
|
104
|
+
mean 5
|
|
105
|
+
min 2
|
|
106
|
+
max 9
|
|
107
|
+
range 7
|
|
108
|
+
sum of squares 32
|
|
109
|
+
sample variance 4.571428571
|
|
110
|
+
sample std dev 2.138089935
|
|
111
|
+
RSD (%) 42.76179871
|
|
112
|
+
(divisor: n - 1)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```console
|
|
116
|
+
$ python -m sdtoolkit --population 2 4 4 4 5 5 7 9
|
|
117
|
+
n 8
|
|
118
|
+
mean 5
|
|
119
|
+
min 2
|
|
120
|
+
max 9
|
|
121
|
+
range 7
|
|
122
|
+
sum of squares 32
|
|
123
|
+
population variance 4
|
|
124
|
+
population std dev 2
|
|
125
|
+
RSD (%) 40
|
|
126
|
+
(divisor: N)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Read one column of a CSV file (lines starting with `#` are treated as
|
|
130
|
+
comments):
|
|
131
|
+
|
|
132
|
+
```console
|
|
133
|
+
$ python -m sdtoolkit --csv examples/sample-datasets.csv --column lab_replicate_mg_l
|
|
134
|
+
n 10
|
|
135
|
+
mean 12.405
|
|
136
|
+
min 12.36
|
|
137
|
+
max 12.45
|
|
138
|
+
range 0.09
|
|
139
|
+
sum of squares 0.00825
|
|
140
|
+
sample variance 0.0009166666667
|
|
141
|
+
sample std dev 0.03027650354
|
|
142
|
+
RSD (%) 0.244066937
|
|
143
|
+
(divisor: n - 1)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Errors go to stderr with exit status 1:
|
|
147
|
+
|
|
148
|
+
```console
|
|
149
|
+
$ python -m sdtoolkit 1 2 x
|
|
150
|
+
error: argument 3: 'x' is not a number
|
|
151
|
+
$ python -m sdtoolkit 5
|
|
152
|
+
error: sample statistics need at least 2 data points, got 1
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
After `pip install .` the same CLI is also available as `sdtoolkit`.
|
|
156
|
+
|
|
157
|
+
## Population vs sample
|
|
158
|
+
|
|
159
|
+
Use the population functions when your data are the entire group you are
|
|
160
|
+
describing. Use the sample functions when your data are drawn from a larger
|
|
161
|
+
group you want to draw conclusions about. The sample formulas divide by
|
|
162
|
+
n − 1 (Bessel's correction), which makes the sample variance an unbiased
|
|
163
|
+
estimate of the population variance, and they need at least two values.
|
|
164
|
+
|
|
165
|
+
`describe`, `relative_std_dev`, `coefficient_of_variation` and the CLI
|
|
166
|
+
default to the sample formulas. Details:
|
|
167
|
+
[Sample vs population](https://ramesh-xyz.github.io/statistics-calculator-toolkit/sample-vs-population.html).
|
|
168
|
+
|
|
169
|
+
## Formulas
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
x̄ = Σxᵢ / n
|
|
173
|
+
SS = Σ (xᵢ − x̄)²
|
|
174
|
+
σ² = SS / N σ = √σ²
|
|
175
|
+
s² = SS / (n − 1) s = √s²
|
|
176
|
+
CV = s / |x̄|
|
|
177
|
+
RSD = 100 × s / |x̄| %
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A step-by-step walk-through of these formulas is on the
|
|
181
|
+
[standard deviation formula reference](https://standarddeviationcalculator.net/standard-deviation-formula/).
|
|
182
|
+
|
|
183
|
+
Further reading:
|
|
184
|
+
|
|
185
|
+
- [Standard deviation](https://ramesh-xyz.github.io/statistics-calculator-toolkit/standard-deviation.html)
|
|
186
|
+
- [Variance](https://ramesh-xyz.github.io/statistics-calculator-toolkit/variance.html)
|
|
187
|
+
- [Sample vs population](https://ramesh-xyz.github.io/statistics-calculator-toolkit/sample-vs-population.html)
|
|
188
|
+
- [Relative standard deviation](https://ramesh-xyz.github.io/statistics-calculator-toolkit/relative-standard-deviation.html)
|
|
189
|
+
- [Outliers](https://ramesh-xyz.github.io/statistics-calculator-toolkit/outliers.html)
|
|
190
|
+
|
|
191
|
+
## Example datasets
|
|
192
|
+
|
|
193
|
+
[`examples/sample-datasets.csv`](https://github.com/Ramesh-xyz/statistics-calculator-toolkit/blob/main/examples/sample-datasets.csv) has three
|
|
194
|
+
small columns: lab replicate readings, exam scores and daily temperatures.
|
|
195
|
+
They are **illustrative values made up for teaching, not real research
|
|
196
|
+
data.** [`examples/worked-examples.md`](https://github.com/Ramesh-xyz/statistics-calculator-toolkit/blob/main/examples/worked-examples.md) works
|
|
197
|
+
through each one by hand and checks the result with the code.
|
|
198
|
+
|
|
199
|
+
## Accuracy and edge cases
|
|
200
|
+
|
|
201
|
+
- **Algorithm.** Two passes: the mean, then the squared deviations from it,
|
|
202
|
+
each summed with `math.fsum`. A correction term removes the rounding error
|
|
203
|
+
left in the mean. The test suite compares results with Python's
|
|
204
|
+
`statistics.pvariance`, `variance`, `pstdev` and `stdev` (which use exact
|
|
205
|
+
rational arithmetic) to a relative tolerance of 1e-12, including data
|
|
206
|
+
offset by 10⁹ and 10¹².
|
|
207
|
+
- **Accepted input.** Any iterable of `int`, `float`, `decimal.Decimal` or
|
|
208
|
+
`fractions.Fraction`. Decimals and fractions are converted to `float`, so
|
|
209
|
+
their extra precision is not kept and results are always `float`.
|
|
210
|
+
- **Rejected input.** `bool`, `None`, strings, NaN, ±infinity, complex
|
|
211
|
+
numbers, and integers too large for a float raise `StatisticsError` naming
|
|
212
|
+
the offending item.
|
|
213
|
+
- **Missing values are not skipped.** `None` raises, and an empty CSV cell
|
|
214
|
+
raises with its row number. Remove or impute missing data yourself so the
|
|
215
|
+
choice is visible in your code.
|
|
216
|
+
- **Empty input** raises. **Sample statistics with one value** raise.
|
|
217
|
+
Population variance of one value is 0.
|
|
218
|
+
- **Zero mean.** `relative_std_dev` and `coefficient_of_variation` raise;
|
|
219
|
+
`describe` sets `rsd_percent` to `None`. RSD is only meaningful for
|
|
220
|
+
ratio-scale data; see
|
|
221
|
+
[Relative standard deviation](https://ramesh-xyz.github.io/statistics-calculator-toolkit/relative-standard-deviation.html).
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
>>> sd.mean([1, None, 3])
|
|
225
|
+
Traceback (most recent call last):
|
|
226
|
+
...
|
|
227
|
+
sdtoolkit.core.StatisticsError: item 1: None (missing value); remove or fill it before computing
|
|
228
|
+
>>> sd.sample_stdev([1e9 + 4, 1e9 + 7, 1e9 + 13, 1e9 + 16]) ** 2
|
|
229
|
+
30.0
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Tests
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
PYTHONPATH=src python -m unittest discover -s tests
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The suite uses only `unittest`. GitHub Actions runs it on Python 3.9 to 3.13
|
|
239
|
+
([workflow](https://github.com/Ramesh-xyz/statistics-calculator-toolkit/blob/main/.github/workflows/test-python.yml)).
|
|
240
|
+
|
|
241
|
+
## Contributing
|
|
242
|
+
|
|
243
|
+
See [CONTRIBUTING.md](https://github.com/Ramesh-xyz/statistics-calculator-toolkit/blob/main/CONTRIBUTING.md).
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT. See [LICENSE](https://github.com/Ramesh-xyz/statistics-calculator-toolkit/blob/main/LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sdtoolkit/__init__.py,sha256=tOeKKMqYckvf58uHn-PrgDe2iOavGwOycaGEGmtgfW8,586
|
|
2
|
+
sdtoolkit/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
3
|
+
sdtoolkit/cli.py,sha256=3oh9HJllzk3YSrHCjI-dWt7Onq_HT1Xv1Ju5-buWMbk,3336
|
|
4
|
+
sdtoolkit/core.py,sha256=sihuX4gvQlg83hrExRovTfwTWmFKpx7Lwv13XUn9dyo,6330
|
|
5
|
+
sdtoolkit-0.1.0.dist-info/licenses/LICENSE,sha256=FEPoA8cBEIhb-W6J-y_YzTMn8TjdKX9Euh1KvSwb-mY,1067
|
|
6
|
+
sdtoolkit-0.1.0.dist-info/METADATA,sha256=XxCM8hU211f12cIAbC8umt-GtFJ4nLGxcC6DvD9QzzI,9101
|
|
7
|
+
sdtoolkit-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
sdtoolkit-0.1.0.dist-info/entry_points.txt,sha256=z1B6dnaSLOFcFAvsBKvA8p7igpn2kPsCe5eAyY4vhqM,49
|
|
9
|
+
sdtoolkit-0.1.0.dist-info/top_level.txt,sha256=BJAOxl62Hfo46HWNk_oRIoK2IBjYagzpoTsGv6ykmrk,10
|
|
10
|
+
sdtoolkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ramesh-xyz
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sdtoolkit
|