Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1cf722f
fix: score detection f1 as 0 when no real bout is matched
BenoitJT-GIRARD Oct 29, 2026
a250f41
fix: read predictions by position in intra-bout continuity
BenoitJT-GIRARD Oct 29, 2026
9ca7b63
fix: average intra-bout continuity over found real bouts only
BenoitJT-GIRARD Oct 29, 2026
94e36b4
feat: add missed_bouts option to score missed real bouts as zero
BenoitJT-GIRARD Oct 29, 2026
1921e13
feat: add pooled aggregation of bout counts and terms over files
BenoitJT-GIRARD Oct 29, 2026
2ce6419
fix: check input lengths and warn when columns are dropped
BenoitJT-GIRARD Oct 29, 2026
4eea918
test: add golden values for the ten recordings scored together
BenoitJT-GIRARD Oct 29, 2026
a30bb89
test: make the mutation and version tests able to fail
BenoitJT-GIRARD Oct 29, 2026
5958086
test: correct the tags and legends of the fixture comparison
BenoitJT-GIRARD Oct 29, 2026
b8ac631
ci: run the parity tests and stop persisting checkout credentials
BenoitJT-GIRARD Oct 29, 2026
aece8c2
ci: publish only after full ci and when the tag matches the version
BenoitJT-GIRARD Oct 29, 2026
065fcfe
chore: align ruff versions, lint the quickstart and add mypy
BenoitJT-GIRARD Oct 29, 2026
f2570da
build: trim runtime dependencies, complete metadata and slim the sdist
BenoitJT-GIRARD Oct 29, 2026
c7831c7
docs: describe the metrics, options and errors as computed
BenoitJT-GIRARD Oct 29, 2026
732af15
docs: add a citation file for the software and the article
BenoitJT-GIRARD Oct 29, 2026
c077461
docs: add the 1.1.0 changelog entry and complete the 0.2.0 one
BenoitJT-GIRARD Oct 29, 2026
ea78c3d
docs: update the release, security, setup and contributing guides
BenoitJT-GIRARD Oct 29, 2026
aeb4a54
docs: make help texts and docstrings match the code
BenoitJT-GIRARD Oct 29, 2026
6fa260c
docs: correct the tutorials and store the current notebook outputs
BenoitJT-GIRARD Oct 29, 2026
52df89e
chore: set the version to 1.1.0
BenoitJT-GIRARD Oct 29, 2026
0e898e8
fix: give the matlab errors ids and drop the toolbox dependency
BenoitJT-GIRARD Oct 30, 2026
29e9505
docs: re-run the calms21 tutorial with 1.1.0
BenoitJT-GIRARD Oct 30, 2026
e27a12a
build: extend the package keywords
BenoitJT-GIRARD Oct 30, 2026
8bd327e
chore: set the release date of 1.1.0
BenoitJT-GIRARD Oct 30, 2026
155984b
build: upgrade locked packages with security advisories
BenoitJT-GIRARD Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_call: # Run by publish.yml before it publishes

permissions:
contents: read
Expand All @@ -15,6 +16,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
Expand All @@ -25,10 +28,29 @@ jobs:
run: uv sync --locked --dev

- name: Run ruff check
run: uv run ruff check src/ tests/
run: uv run ruff check src/ tests/ demo/quickstart.py

- name: Run ruff format check
run: uv run ruff format --check src/ tests/
run: uv run ruff format --check src/ tests/ demo/quickstart.py

typecheck:
name: Type check (mypy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.11"

- name: Install dependencies
run: uv sync --locked --dev

- name: Run mypy
run: uv run mypy src/

test:
name: Test (Python ${{ matrix.python-version }})
Expand All @@ -40,6 +62,8 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
Expand All @@ -50,4 +74,4 @@ jobs:
run: uv sync --locked --dev

- name: Run tests
run: uv run pytest tests/ --ignore=tests/test_parity.py -v
run: uv run pytest tests/ -v
36 changes: 30 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,37 @@ on:
tags:
- 'v*'

permissions:
contents: read

jobs:
check-version:
name: Check tag against package version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Compare the tag with the version in pyproject.toml
env:
TAG: ${{ github.ref_name }}
run: |
version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
if [ "$TAG" != "v$version" ]; then
echo "::error::Tag $TAG does not match the package version $version (expected v$version)"
exit 1
fi
echo "Tag $TAG matches the package version $version"

ci:
name: CI
needs: check-version
uses: ./.github/workflows/ci.yml # Every job of ci.yml, test matrix included

build-and-publish:
name: Build and publish to PyPI
needs: [check-version, ci]
runs-on: ubuntu-latest
environment: pypi
permissions:
Expand All @@ -16,18 +44,14 @@ jobs:

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: "3.11"

- name: Install dependencies
run: uv sync --locked --dev

- name: Run tests before publishing
run: uv run pytest tests/ --ignore=tests/test_parity.py --cov=banos --cov-fail-under=80

- name: Build package
run: uv build

Expand Down
5 changes: 3 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
rev: v0.15.7 # Same ruff version as uv.lock (and CI)
hooks:
- id: ruff
- id: ruff-check
args: [--fix]
- id: ruff-format
types_or: [python, pyi] # Notebooks are not reformatted
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
Expand Down
141 changes: 141 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,140 @@

All notable changes to this project will be documented in this file.

## [1.1.0] — 2026-10-30

Two metrics are corrected (detection F1 and IC), two options are added (`missed_bouts` and
`aggregation`), and invalid input now raises an error or a warning. With the default
options and valid input, precision, recall, SO and TP are unchanged.

### Numerical changes
- **Detection F1 is 0, not NaN, when no real bout is matched.** F1 is now 0 when TP = 0
and FP + FN > 0 (a recording with real bouts and nothing predicted, or only false
bouts), the value of 2 TP / (2 TP + FP + FN). It stays NaN only when TP = FP = FN = 0;
every other value is unchanged. Such a recording now stays in the means instead of
leaving them. Precision and recall are unchanged (NaN on a zero denominator). This
brings back the 0 of v0.1.5, which v0.2.0 had turned into NaN.
- **IC averages over the found real bouts only.** A real bout with no predicted frame
inside it holds no switch and scored IC 1.0: missing a bout raised IC, and a model that
predicted nothing scored IC 1.0. IC is now the mean of `1 - switches / (bout length - 1)`
over the real bouts with at least one predicted frame, one-frame bouts still left out,
and NaN when no such bout is found.
- **IC reads the prediction by position.** IC read the prediction by index label, so
DataFrames indexed other than 0..n-1 (frames numbered from 1, reversed, float or string
labels, concatenated recordings) got a wrong IC or an exception. Input with the default
index is unchanged.
- **Golden values.** The fixtures move to `golden_python_v110.json` and
`golden_matlab_v110.json`; the v0.2.0 files are kept as archives. 41 of the 300
per-recording values change: 2 F1 values (Recording_8 mount F1 from NaN to 0,
Recording_8 overall F1 from 0.6062 to 0.4546) and 39 IC values, in every recording
(Recording_8 mount IC from 1.0 to NaN: both its real bouts are missed). The other 259
are unchanged. `python tests/fixtures/compare_fixtures.py --mode 3` shows them.
- **Bundled data**, the 10 recordings scored together with the default options
(`banos.score(data)`, four behaviors), 1.0.0 → 1.1.0: F1 mount 0.8780 → 0.7902, overall
0.7746 → 0.7553; IC attack 0.9956 → 0.9949, investigation 0.9918 → 0.9875, mount
0.9969 → 0.9942, other 0.9945 → 0.9927, overall 0.9947 → 0.9923. Precision, recall, SO
and TP are unchanged.
- **A model that predicts nothing**, on the same data, per file: F1 attack 1.0 → 0.5,
mount 1.0 → 0.4, investigation and other NaN → 0, overall 1.0 → 0.225; IC investigation
and other 1.0 → NaN. The 0.5 and 0.4 that remain are the share of recordings without
the behavior (absent-behavior rule, unchanged). Pooled, its F1 is 0 for every behavior.
- The MATLAB code gives the same numbers (parity tests within 1e-6).

### Added
- `missed_bouts` option of `banos.score()`: `"exclude"` (default) leaves a missed real
bout out of SO, TP and IC; `"zero"` adds a term equal to 0 to SO, to TP and, for a bout
longer than one frame, to IC. Precision, recall and F1 do not depend on it.
- `aggregation` option of `banos.score()`: `"per_file"` (default) scores each recording,
then averages; `"pooled"` sums the TP, FP and FN counts of all recordings and averages
all their SO, TP and IC terms, for each behavior, without the absent-behavior rule, and
`overall_metrics` is the mean over the behaviors. The result has the same form in both.
- Functions `calculate_so_terms`, `calculate_tp_terms` and `calculate_ic_terms`
(`banos.core`), `compute_behavior_components`, `calculate_components_for_each_file` and
`pool_components` (`banos`). `calculate_so`, `calculate_tp_metric`, `calculate_ic`,
`compute_behavior_metrics` and `calculate_banos_for_each_file` take the keyword
`missed_bouts`.
- MATLAB: the `'missed_bouts'` and `'aggregation'` options of `BANOS_score`;
`BANOSparseOptions` and `BANOSpoolComponents`; the terms as second output of
`BANOScalculateSO`, `BANOScalculateTP` and `BANOScalculateIC`, and the components as
second output of `BANOScomputeBehaviorMetrics` and `BANOScalculateBANOSForEachFile`.
- Golden values of the 10 recordings scored together, for each combination of the two
options (`all_recordings` in the v110 fixtures), checked by the regression and parity
tests.
- `py.typed`: type checkers read the annotations of the package.
- `CITATION.cff`, with the article to cite.
- Mode 3 of `tests/fixtures/compare_fixtures.py` (v0.2.0 against v1.1.0), and a test that
runs the script and compares its output with `reports/fixture_comparison.html`.

### Changed
- **Input errors.** `ValueError` when the prediction and the ground truth of a recording
have different numbers of frames (a shorter prediction raised `KeyError` when a real
bout longer than one frame reached past its last frame, and was scored otherwise; a
longer one was scored) or when a recording has no frame (it scored 1.0 on all six
metrics).
- **Warnings.** A `UserWarning` names the recording and the column for each column
dropped (values other than 0 and 1, or missing or invalid on the other side) and for
each column whose NaN values are replaced by 0; both were silent. The warnings point at
the line that called `banos.score()` or `preprocess_data()`. Valid input gives no
warning.
- **Options checked on entry.** `banos.score()` and the pipeline functions raise
`ValueError` for an unknown `matching`, `missed_bouts` or `aggregation` value even when
nothing would be scored; an unknown `matching` used to pass unnoticed when no behavior
had real bouts.
- **MATLAB.** `BANOSpreprocessData` raises and warns in the same way (identifiers
`BANOS:lengthMismatch`, `BANOS:noFrame`, `BANOS:nanReplaced`, `BANOS:columnDropped`).
Options are stricter: an unknown option name, which was ignored, and an unknown
`'matching'` value, which was treated as greedy, raise an error (`BANOS:unknownOption`,
`BANOS:missingOptionValue`, `BANOS:invalidOption`). The other errors of `BANOS_score`
get identifiers too: `BANOS:optimalNotImplemented`, `BANOS:missingGroundTruth` and
`BANOS:invalidData`.
- **Imports.** `calculate_so`, `calculate_tp_metric` and `calculate_ic` are no longer
importable from `banos.pipeline`, which only used them; import them from `banos.core`.
- **Packaging.** matplotlib and seaborn move from the runtime dependencies to the
`notebook` extra: `pip install banos` installs numpy, pandas and scipy only. The `dev`
extra is removed; the `dev` dependency group, which `uv sync` installs, stays. Authors
and maintainers are set, the license is an SPDX expression (`License-Expression: MIT`,
with `License-File`) and the license classifier is removed. The build backend is
`hatchling>=1.27,<2`. The sdist holds the package, its tests with the data they read,
`demo/quickstart.py`, README, LICENSE and CHANGELOG (about 70 kB instead of 1.2 MB).
- **CI.** The test matrix runs the parity tests; a `Type check (mypy)` job runs mypy on
`src/`; checkouts do not keep credentials. The publish workflow checks that the tag is
`v` followed by the version in `pyproject.toml` and runs the whole CI before building
and publishing.
- **Development tools.** Pre-commit runs ruff 0.15.7, the version of the lock and CI. CI
also lints `demo/quickstart.py`. mypy, pandas-stubs and scipy-stubs join the dev group;
pytest-html leaves it.
- **Tests.** `tests/test_comparison_report.py`, which was always skipped, is removed:
`test_parity_group_metrics` checks the same values, and now also fails when Python and
MATLAB report different behaviors.

### Fixed
- The `score()` docstring and the `BANOS_score` help said that a metric is NaN when the
ground truth has no bout; per file, such a behavior scores 1.0 or 0.0.
- README: the IC formula (the denominator is the bout length minus 1), SO and TP (averaged
over every overlapping pair, not over matched pairs), the return value for several
recordings, the dependencies, and links that did not work on the PyPI page.
- Tutorials: the text said that the prediction shifted by 15 frames still detects both
bouts (DA 1.0); it shares no frame with them, so DA is 0 (it printed NaN before this
release).
- The CalMS21 tutorial printed the absolute local paths of its model and data cache; it
prints them relative to the repository. Its download failed: data.caltech.edu refuses
the default user agent of urllib (HTTP 403), so the requests now send a User-Agent
that names the tutorial and the repository. Its stored outputs are those of 1.1.0.
- `compare_fixtures.py` swapped the `VALUE->NULL` and `NULL->VALUE` tags and misdescribed
the v0.1.5 to v0.2.0 changes in its legend; it no longer fails on a Windows console.
- `test_preprocess_data_does_not_mutate_caller_dict` compared a dict's id with itself and
could not fail; it now compares the caller's data before and after. `test_score_version`
compares `banos.__version__` with the version in `pyproject.toml`.
- Type errors reported by mypy: `is_logical_column` returns a Python `bool`, and
`drop_non_logical_columns` is annotated with the column labels it returns.
- The MATLAB code averaged with `nanmean`, a function of the Statistics and Machine
Learning Toolbox, so scoring several recordings needed that toolbox. It now uses
`mean(..., 'omitnan')` of base MATLAB, which gives the same values.
- `SECURITY.md` said that BANOS reads CSV files (it reads no file: it takes DataFrames or
MATLAB cell arrays) and that `uv.lock` pins the dependencies users get (it pins the
development and CI environment). `SETUP.md` said to allow all actions (the repository
allows selected actions, pinned by SHA).

## [1.0.0] — 2025-02-27

First stable release. Metric computation and the golden fixtures are unchanged from 0.3.0.
Expand Down Expand Up @@ -36,10 +170,17 @@ No change to metric computation: `src/banos/` and the golden fixtures are unchan
- **Absent-behavior scoring**: when GT has no bouts, metrics are no longer `NaN`/`0`:
- Machine also has no bouts → all metrics = **1.0** (correct absence, rewarded)
- Machine predicts bouts that don't exist in GT → all metrics = **0.0** (false detection, penalized)
- **Undefined metrics are NaN instead of 0** (missing from this entry until 1.1.0): for a
behavior with real bouts in a recording and no predicted bout matching any of them (so
none overlapping them), F1, SO and TP became NaN instead of 0, and so did precision when
nothing was predicted. The averages leave NaN out, so such a recording no longer
counted in these means. 1.1.0 scores this F1 as 0 again.
- Python minimum version raised to 3.9

### Bug Fixes
- Fixed IC boundary: last transition within GT bout was not counted (`range(gt_end-1)` → `range(gt_end)`)
- **One-frame real bouts are left out of IC** (missing from this entry until 1.1.0): such a
bout has no frame pair to compare; v0.1.5 gave it an IC of 0, whatever was predicted.
- Fixed variable shadowing of `tp` count and `tp` metric in `compute_behavior_metrics`
- Fixed Matlab `BANOSmatchHeaders`: now matches by column name, not column index
- Fixed `aggregate_metrics`: NaN values now excluded from averages (nanmean behavior)
Expand Down
56 changes: 56 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
cff-version: 1.2.0
message: "If you use BANOS, please cite the article given as preferred citation."
title: BANOS
abstract: "Behavior ANnOtation Score: ethological metrics for evaluating behavior annotation quality."
type: software
version: 1.1.0
date-released: 2026-10-30
license: MIT
repository-code: "https://github.com/BelloneLab/BANOS"
authors:
- given-names: "Benoît J. T."
family-names: Girard
keywords:
- behavior
- annotation
- ethology
- video
- scoring
- metrics
- computational ethology
- animal behavior
- behavioral neuroscience
- action segmentation
- bout detection
- evaluation metrics
- inter-annotator agreement
- rodent
preferred-citation:
type: article
title: "From eye to AI: Studying rodent social behavior in the era of machine learning"
authors:
- given-names: Giuseppe
family-names: Chindemi
- given-names: Camilla
family-names: Bellone
- given-names: Benoit
family-names: Girard
journal: "Neuroscience & Biobehavioral Reviews"
volume: 188
start: 106766
year: 2026
doi: 10.1016/j.neubiorev.2026.106766
references:
- type: article
title: "From eye to AI: studying rodent social behavior in the era of machine Learning"
authors:
- given-names: Giuseppe
family-names: Chindemi
- given-names: Camilla
family-names: Bellone
- given-names: Benoit
family-names: Girard
journal: "arXiv preprint arXiv:2508.04255"
year: 2025
doi: 10.48550/arXiv.2508.04255
url: "https://arxiv.org/abs/2508.04255"
18 changes: 14 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ Contact: open a [GitHub Issue](https://github.com/BelloneLab/BANOS/issues).

## How to Report a Bug

Open a [GitHub Issue](https://github.com/BelloneLab/BANOS/issues) using the bug report
template. Please include:
Open a [GitHub Issue](https://github.com/BelloneLab/BANOS/issues). Please include:

- BANOS version (`pip show banos` or check `pyproject.toml`)
- BANOS version (`pip show banos`, or `banos.__version__`)
- Python version (or MATLAB version, if applicable)
- A minimal reproducible example (small CSV + code that demonstrates the wrong output)

Expand All @@ -41,8 +40,19 @@ template. Please include:

Code style:
- Python: follow existing style enforced by `ruff` (lint + format)
- Use type hints consistent with the existing codebase
- Use type hints consistent with the existing codebase; CI runs `mypy` on `src/`
- Tests are required for bug fixes (see `tests/`)
- A fix that changes a metric changes both implementations, Python (`src/banos/`) and
MATLAB (`matlab/BANOS/`), and regenerates the golden fixtures (see
[`RELEASE.md`](RELEASE.md)); the parity tests compare the two

Before opening the PR, run the checks of CI (see [`SETUP.md`](SETUP.md)):
```bash
uv run ruff check src/ tests/ demo/quickstart.py
uv run ruff format --check src/ tests/ demo/quickstart.py
uv run mypy src/
uv run pytest
```

## What We Accept

Expand Down
Loading
Loading