Metadata-Version: 2.4
Name: psienced
Version: 0.1.0
Summary: Small in-house helpers for experimental data: robust stats, outliers, units, sig figs, and uncertainty propagation.
Author: Vignesh S K
License-Expression: MIT
License-File: LICENSE
Keywords: outliers,science,significant-figures,statistics,uncertainty,units
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: numpy>=1.21
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == 'test'
Description-Content-Type: text/markdown

# psienced

Small in-house helpers for experimental data — the pieces that otherwise get
rewritten at the top of every analysis script.

- **Robust statistics** — MAD, standard error, CV, t-based confidence intervals,
  one-call summaries.
- **Outlier detection** — MAD, Tukey/IQR and z-score, with masking that keeps
  plate positions intact.
- **Rescaling** — z-score, robust scale, min–max, percent-of-control, log.
- **Units** — case-sensitive conversion across seven dimensions, plus the
  `C1V1 = C2V2` dilution arithmetic.
- **Significant figures** — rounding and value ± uncertainty reporting.
- **Uncertainty propagation** — a `Measurement` type that carries error bars
  through ordinary Python arithmetic.

Everything is NaN-tolerant by default, because missing wells and failed reads
are the normal state of bench data rather than an error condition.

## Install

```bash
pip install -e .
```

The only runtime dependency is NumPy. For the test suite:

```bash
pip install -e ".[test]"
```

## Use

```python
import psienced as ps

readings = [10.1, 9.8, 10.0, 10.3, 45.0, float("nan"), 9.9, 10.2]

ps.outliers_mad(readings)          # -> [F, F, F, F, True, F, F, F]
clean = ps.mask_outliers(readings) # spike becomes NaN, positions preserved

ps.summary(clean)["mean"]          # 10.05  (the 45.0 never reaches it)
ps.confidence_interval(clean)      # (9.85, 10.25)
```

### Outliers

`outliers_mad` is the default for a reason. The classic mean/SD z-score is
included for compatibility with existing protocols, but on the small `n` typical
of replicate sets a single extreme value inflates the SD enough to hide itself:

```python
spiked = [10.0, 10.1, 9.9, 10.2, 9.8, 10.05, 45.0]

ps.outliers_mad(spiked)[-1]      # True  — caught
ps.outliers_zscore(spiked)[-1]   # False — masked by its own effect on the SD
```

One caveat in the other direction: if more than half the values are identical
the MAD is exactly zero and `outliers_mad` flags nothing at all, even against an
obvious spike. `outliers_iqr` still catches that case.

Use `mask_outliers` (NaN in place) when position carries meaning — plate wells,
time points, paired conditions — and `drop_outliers` when it does not.
`outlier_report` returns the indices and values for a QC log.

### Units

Names are case-sensitive on purpose: `mm` is millimetres, `mM` is millimolar,
and folding those together is precisely the mistake this module exists to catch.
Both micro signs (U+00B5 and U+03BC) are accepted, since which one a file
contains depends on the instrument that wrote it.

```python
ps.convert(1.0, "mM", "uM")     # 1000.0
ps.convert(37.0, "C", "F")      # 98.6   (affine, not scaled)
ps.convert(1.0, "mm", "mM")     # ValueError: different physical dimensions

ps.dilution_volume(c1=100.0, c2=5.0, v2=20.0)   # 1.0 mL of stock
ps.molarity_to_mass(0.1, molar_mass=58.44)      # 5.844 g/L NaCl
```

### Uncertainty

```python
absorbance  = ps.Measurement.from_replicates([0.412, 0.408, 0.415])
extinction  = ps.Measurement(6220.0, 30.0)
path_length = ps.Measurement(1.0, 0.005)

concentration = absorbance / (extinction * path_length)
print(concentration.format(unit="M"))       # '0.0000662 +/- 0.0000006 M'
```

Rendering is fixed-point, so results far from unity read better after rescaling.
Multiplying by a plain number scales the error bar with the value:

```python
print((concentration * 1e6).format(unit="uM"))   # '66.2 +/- 0.6 uM'
```

Two assumptions are built in, and both matter:

- **Independence.** Operands are treated as uncorrelated, so `m - m` reports a
  nonzero uncertainty even though the answer is exactly zero. Simplify
  algebraically before wrapping.
- **Linearity.** The usual first-order approximation, which degrades once the
  relative uncertainty passes roughly 10%. Reach for Monte Carlo beyond that.

### Reporting

```python
ps.format_measurement(10.14159, 0.523, unit="mM")   # '10.1 +/- 0.5 mM'
ps.format_sig(1.5, 4)                               # '1.500'  (keeps the zeros)
```

## Layout

| Module | Contents |
| --- | --- |
| `psienced.stats` | `mad`, `sem`, `cv`, `robust_zscore`, `confidence_interval`, `summary` |
| `psienced.outliers` | `outliers_mad`, `outliers_iqr`, `outliers_zscore`, `flag_outliers`, `mask_outliers`, `drop_outliers`, `outlier_report` |
| `psienced.normalize` | `zscore`, `robust_scale`, `minmax`, `percent_of_control`, `log_transform` |
| `psienced.units` | `convert`, `known_units`, `dilution_volume`, `dilution_factor`, `molarity_to_mass`, `mass_to_molarity` |
| `psienced.sigfig` | `round_sig`, `format_sig`, `round_to_uncertainty`, `format_measurement` |
| `psienced.propagation` | `Measurement`, `sqrt`, `log`, `log10`, `exp` |

Every name is re-exported at the top level, so `import psienced as ps` is enough.

## Conventions

- **NaN is ignored, never silently dropped.** `summary` reports `n` and
  `n_missing` separately, and no detector flags a NaN as an outlier — a missing
  well is an absence, not an anomaly.
- **Sample statistics use `ddof=1`.** A set of replicates is a sample from a
  process, not a population. Override per call where that is wrong.
- **Shape is preserved.** Transforms and masks return arrays shaped like their
  input, so results can be written back over the raw data.
- **Errors are raised, not guessed at.** Mismatched dimensions, impossible
  dilutions and negative uncertainties fail loudly.

## Tests

```bash
pytest
```

The suite covers each module plus doctests, and checks the t-distribution
implementation against published critical-value tables.

## License

MIT — see [LICENSE](LICENSE).
