Skip to content

SummaryCollection

py3r.behaviour.summary.summary_collection.SummaryCollection

SummaryCollection(summary_dict: dict[str, Summary])

Bases: BaseCollection, SummaryCollectionPlotMixin

collection of Summary objects (e.g. for grouping individuals) note: type-hints refer to Summary, but factory methods allow for other classes these are intended ONLY for subclasses of Summary, and this is enforced

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> # add a simple boolean feature to each Features for summaries to consume
>>> for f in fc.values():
...     s = pd.Series([True, False] * (len(f.tracking.data)//2 + 1))[:len(f.tracking.data)]
...     s.index = f.tracking.data.index
...     f.store(s, 'flag', meta={})
>>> sc = SummaryCollection.from_features_collection(fc)
>>> list(sorted(sc.keys()))
['A', 'B']

each instance-attribute

each: Summary

each_forcebatch instance-attribute

each_forcebatch: Summary

summary_dict property

summary_dict

is_grouped property

is_grouped

True if this collection is a grouped view.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
>>> coll.is_grouped
False

groupby_tags property

groupby_tags

The tag names used to form this grouped view (or None if flat).

group_keys property

group_keys

Keys for the groups in a grouped view. Empty list if not grouped.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     coll['A'].add_tag('group','G1'); coll['B'].add_tag('group','G2')
>>> g = coll.groupby('group')
>>> sorted(g.group_keys)
[('G1',), ('G2',)]

from_features_collection classmethod

from_features_collection(
    features_collection: FeaturesCollection,
    summary_cls=Summary,
)

creates a SummaryCollection from a FeaturesCollection (flat or grouped)

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> # add numeric scalar per Features via a quick summary to test to_df later
>>> for f in fc.values():
...     import numpy as np, pandas as pd
...     s = pd.Series(range(len(f.tracking.data)), index=f.tracking.data.index)
...     f.store(s, 'counter', meta={})
>>> sc = SummaryCollection.from_features_collection(fc)
>>> isinstance(sc['A'], Summary) and isinstance(sc['B'], Summary)
True

from_list classmethod

from_list(summary_list: list[Summary])

creates a SummaryCollection from a list of Summary objects, keyed by handle

Examples:

>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking import Tracking
>>> from py3r.behaviour.features.features import Features
>>> from py3r.behaviour.summary.summary import Summary
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...     t1 = Tracking.from_dlc(str(p), handle='A', fps=30)
...     t2 = Tracking.from_dlc(str(p), handle='B', fps=30)
>>> f1, f2 = Features(t1), Features(t2)
>>> # store simple scalar summaries
>>> s1, s2 = Summary(f1), Summary(f2)
>>> s1.store(1, 'count'); s2.store(2, 'count')
>>> sc = SummaryCollection.from_list([s1, s2])
>>> list(sorted(sc.keys()))
['A', 'B']

to_df

to_df(
    include_tags: bool = False,
    tag_prefix: str = "tag_",
    series: Literal["ignore", "separate"] = "ignore",
) -> (
    pd.DataFrame
    | tuple[pd.DataFrame, dict[str, pd.DataFrame]]
)

Collate values from each Summary.data into tabular output.

  • Index: handles of the Summary objects
  • Scalar columns: keys from each Summary.data with scalar values
  • If include_tags is True, include tag columns with the given prefix
  • If series='ignore' (default), Series entries are skipped
  • If series='separate', return (scalars_df, series_tables) where series_tables is {metric_name: dataframe} and each dataframe has one row per handle and one column per Series index value.

Examples:

>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking import Tracking
>>> from py3r.behaviour.features.features import Features
>>> from py3r.behaviour.summary.summary import Summary
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...     t1 = Tracking.from_dlc(str(p), handle='A', fps=30)
...     t2 = Tracking.from_dlc(str(p), handle='B', fps=30)
>>> s1, s2 = Summary(Features(t1)), Summary(Features(t2))
>>> s1.store(1.0, 'score'); s2.store(2.0, 'score')
>>> s1.features.tracking.add_tag('group', 'G1'); s2.features.tracking.add_tag('group', 'G2')
>>> sc = SummaryCollection.from_list([s1, s2])
>>> df = sc.to_df(include_tags=True)
>>> set(df.columns) >= {'score', 'tag_group'}
True
>>> s1.store(pd.Series([1.0, 2.0], index=['A', 'B']), 'speed_by_state')
>>> s2.store(pd.Series([3.0, 4.0], index=['A', 'B']), 'speed_by_state')
>>> scalars, series_tables = sc.to_df(series='separate')
>>> isinstance(scalars, pd.DataFrame) and 'speed_by_state' in series_tables
True

make_bin

make_bin(startframe, endframe)

returns a new SummaryCollection with binned summaries

Examples:

>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking import Tracking
>>> from py3r.behaviour.features.features import Features
>>> from py3r.behaviour.summary.summary import Summary
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...     t = Tracking.from_dlc(str(p), handle='A', fps=30)
>>> s = Summary(Features(t))
>>> sc = SummaryCollection.from_list([s])
>>> b = sc.make_bin(0, 2)
>>> isinstance(b, SummaryCollection)
True

make_bins

make_bins(numbins)

returns a list of SummaryCollection, one per bin

Examples:

>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking import Tracking
>>> from py3r.behaviour.features.features import Features
>>> from py3r.behaviour.summary.summary import Summary
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...     t = Tracking.from_dlc(str(p), handle='A', fps=30)
>>> sc = SummaryCollection.from_list([Summary(Features(t))])
>>> bins = sc.make_bins(3)
>>> len(bins) == 3 and all(isinstance(b, SummaryCollection) for b in bins)
True

store

store(
    results_dict,
    name: str = None,
    meta: dict = None,
    overwrite: bool = False,
)

Store SummaryResult objects returned by batch methods.

  • Flat collection: results_dict is {handle: SummaryResult}
  • Grouped collection: results_dict is {group_key: {handle: SummaryResult}}

Examples:

>>> import pandas as pd, tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> # add a boolean column for summaries
>>> for f in fc.values():
...     m = pd.Series([True, False] * (len(f.tracking.data)//2 + 1))[:len(f.tracking.data)]
...     m.index = f.tracking.data.index
...     f.store(m, 'mask', meta={})
>>> sc = SummaryCollection.from_features_collection(fc)
>>> rd = {h: s.time_true('mask') for h, s in sc.items()}
>>> sc.store(rd, name='t_mask')
>>> all('t_mask' in s.data for s in sc.values())
True

Returns:

Type Description
str

The resolved stored metric name. If auto-naming would resolve to multiple different names across leaves, raises ValueError.

stored_info

stored_info() -> pd.DataFrame

Summarize stored summary metrics across the collection's leaf Summary objects.

Returns a DataFrame indexed by summary with columns: - attached_to: number of recordings containing the summary key - missing_from: number of recordings not containing the summary key - type: value datatype name when consistent, or a list of datatype names when mixed across recordings.

bfa

bfa(
    column: str,
    all_states=None,
    numshuffles: int = 1000,
    pairs: list[tuple[str, str]] | None = None,
    random_state: int | None = 0,
)

Behaviour Flow Analysis between groups for a grouped SummaryCollection.

Requires the collection to be grouped (via groupby). Computes transition matrices per Summary within each group, then computes Manhattan distances between group means and surrogate distributions via shuffling.

If pairs is provided, only those group pairs are analyzed; otherwise all unique pairs in self.group_keys are evaluated.

Parameters:

Name Type Description Default

random_state

int | None

Optional seed for deterministic surrogate shuffling. None keeps non-deterministic behavior.

0

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> # inject simple 2-state labels and tags to build groups
>>> for i, (h, f) in enumerate(fc.items()):
...     pat = ['A','A','B','B','A'] * (len(f.tracking.data)//5 + 1)
...     states = pd.Series(pat[:len(f.tracking.data)], index=f.tracking.data.index)
...     f.store(states, 'state', meta={})
...     f.tracking.add_tag('group', f'G{i+1}')
>>> gfc = fc.groupby('group')
>>> sc = SummaryCollection.from_features_collection(gfc)
>>> # compute all pairs
>>> res = sc.bfa('state', all_states=['A','B'], numshuffles=2)
>>> isinstance(res, dict) and 'observed' in next(iter(res.values()))
True
>>> # compute only specific pair(s)
>>> res2 = sc.bfa('state', all_states=['A','B'], numshuffles=2, pairs=[('G1','G2')])
>>> list(res2.keys()) == ['G1_vs_G2']
True

bfa_stats staticmethod

bfa_stats(
    bfa_results: dict[str, dict[str, float]],
) -> dict[str, dict[str, float]]

Compute simple statistics (percentile, zscore, right_tail_p) from bfa results.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> for i, (h, f) in enumerate(fc.items()):
...     pat = ['A','A','B','B','A'] * (len(f.tracking.data)//5 + 1)
...     states = pd.Series(pat[:len(f.tracking.data)], index=f.tracking.data.index)
...     f.store(states, 'state', meta={})
...     f.tracking.add_tag('group', f'G{i+1}')
>>> sc = SummaryCollection.from_features_collection(fc.groupby('group'))
>>> bfa_out = sc.bfa('state', all_states=['A','B'], numshuffles=2)
>>> stats = SummaryCollection.bfa_stats(bfa_out)
>>> set(next(iter(stats.values())).keys()) >= {'percentile','zscore','right_tail_p'}
True

plot_bfa_results staticmethod

plot_bfa_results(
    results: dict[str, dict[str, float]],
    compares: str | list[str] | None = None,
    add_stats: bool = True,
    stats: dict[str, dict[str, float]] | None = None,
    bins: int = 50,
    figsize: tuple[float, float] = (4, 3),
    save_dir: str | None = None,
    show: bool = True,
    compare: str | None = None,
)

Plot one or more BFA result comparisons as separate single-panel figures.

  • If compares is None and results contain a single comparison, that one is plotted.
  • If compares is a string, only that comparison is plotted.
  • If compares is a list of strings, each comparison is plotted separately.
  • If add_stats is True and stats not provided, statistics will be computed via SummaryCollection.bfa_stats(results) and annotated on each plot.

Returns (fig, ax) for a single comparison, or a dict {compare: (fig, ax)} for multiple.

Examples:

>>> import tempfile, shutil, os
>>> from pathlib import Path
>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> # add simple 2-state labels and tags to build two groups
>>> for i, (h, f) in enumerate(fc.items()):
...     pat = ['A','A','B','B','A'] * (len(f.tracking.data)//5 + 1)
...     states = pd.Series(pat[:len(f.tracking.data)], index=f.tracking.data.index)
...     f.store(states, 'state', meta={})
...     f.tracking.add_tag('group', f'G{i+1}')
>>> sc = SummaryCollection.from_features_collection(fc.groupby('group'))
>>> bfa_out = sc.bfa('state', all_states=['A','B'], numshuffles=5)
>>> # plot a single comparison and save it
>>> with tempfile.TemporaryDirectory() as outdir:
...     fig, ax = SummaryCollection.plot_bfa_results(
...         bfa_out, compare='G1_vs_G2', show=False, save_dir=outdir)
...     os.path.exists(os.path.join(outdir, 'G1_vs_G2.png'))
True

plot_transition_umap

plot_transition_umap(
    column: str,
    all_states=None,
    groups: list[str] | list[list[str]] | None = None,
    n_neighbors: int = 15,
    min_dist: float = 0.1,
    random_state: int = 0,
    figsize: tuple[float, float] = (4.5, 4),
    show: bool = True,
    save_dir: str | None = None,
)

Plot a simple UMAP embedding of per-subject transition matrices for selected groups.

Parameters:

Name Type Description Default

column

str

Name of the categorical column used to compute transition matrices.

required

all_states

Optional explicit state ordering for transition matrices.

None

groups

list[str] | list[list[str]] | None
  • Optional list of group keys (strings) to include; defaults to all.
  • Or a list of lists for sequential groups, e.g. [['control_pre','control_45min','control_90min'], ['treatment_pre','treatment_45min','treatment_90min']]. Each sequence is plotted with a monochrome gradient.
None

n_neighbors

int

UMAP hyperparameters.

15

min_dist

int

UMAP hyperparameters.

15

random_state

int

UMAP hyperparameters.

15

figsize

tuple[float, float]

Matplotlib options.

(4.5, 4)

show

tuple[float, float]

Matplotlib options.

(4.5, 4)

Returns:

Type Description
(fig, ax): Matplotlib figure and axis.

Examples:

>>> # xdoctest: +REQUIRES(module: umap)
>>> import tempfile, shutil, os, pandas as pd
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> for i, (h, f) in enumerate(fc.items()):
...     pat = ['A','A','B','B','A'] * (len(f.tracking.data)//5 + 1)
...     states = pd.Series(pat[:len(f.tracking.data)], index=f.tracking.data.index)
...     f.store(states, 'state', meta={})
...     f.tracking.add_tag('group', f'G{i+1}')
>>> sc = SummaryCollection.from_features_collection(fc.groupby('group'))
>>> with tempfile.TemporaryDirectory() as outdir:
...     fig, ax = sc.plot_transition_umap(
...         column='state', all_states=['A','B'], groups=['G1','G2'],
...         show=False, save_dir=outdir)
...     os.path.exists(os.path.join(outdir, 'transition_umap.png'))
True

plot_chord

plot_chord(
    column: str,
    all_states: list[str | int] | None = None,
    *,
    fromkey: str | None = None,
    plot_individual: bool = False,
    show: bool = True,
    save_dir: str | None = None,
    cmap: str | list | None = None,
    **kwargs,
)

Plot chord diagrams of state transitions using a minimal pattern.

  • If not grouped:
  • plot_individual=False: sum over the collection and plot a single chord.
  • plot_individual=True: plot one chord per recording.
  • If grouped:
  • plot_individual=False: sum within each group and plot one chord per group.
  • plot_individual=True: plot one chord per recording per group.

Parameters:

Name Type Description Default

column

str

Name of the categorical column used to compute transitions.

required

all_states

list[str | int] | None

Optional explicit state ordering for transition matrices. Required when fromkey is not provided.

None

fromkey

str | None

Optional key in each Summary.data containing a precomputed transition DataFrame. If provided, this key is used directly instead of computing transitions from column.

None

plot_individual

bool

If True, plot per recording; otherwise plot summed aggregate.

False

show

bool

If True, display figures.

True

save_dir

str | None

Optional directory to save figures; created if missing.

None

kwargs

Additional keyword arguments to pass to pycirclize.Circos.chord_diagram.

{}

Returns:

Name Type Description
object
  • flat & plot_individual=False: single fig
  • flat & plot_individual=True: dict {handle: fig}
  • grouped & plot_individual=False: dict {group: fig}
  • grouped & plot_individual=True: dict {group: {handle: fig}}

Examples:

>>> # xdoctest: +REQUIRES(module: pycirclize)
>>> import tempfile, os, shutil
>>> import pandas as pd
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     # create two recordings from the sample csv
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
...     # build features and inject a simple 3-state sequence
...     fc = FeaturesCollection.from_tracking_collection(tc)
...     for _, f in fc.items():
...         pat = ['0','1','2','1','0'] * (len(f.tracking.data)//5 + 1)
...         seq = pd.Series(pat[:len(f.tracking.data)], index=f.tracking.data.index)
...         f.store(seq, 'state', meta={})
...     sc = SummaryCollection.from_features_collection(fc)
...     # plot flat aggregate and save it
...     with tempfile.TemporaryDirectory() as outdir:
...         _ = sc.plot_chord(
...             'state', all_states=['0','1','2'], show=False, save_dir=outdir)
...         os.path.exists(os.path.join(outdir, 'chord_state.png'))
True

prepare_plot

prepare_plot(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    merge_by: str | None = "metric",
    ax=None,
    figsize=None,
)

Prepare a tidy DataFrame and seaborn kwargs without drawing anything.

This is the single entry point for all plot data preparation. The convenience sns* methods call this internally; power users can call it directly for full control over the seaborn call.

Parameters:

Name Type Description Default

metric

str or BatchResult or list[str | BatchResult]

Metric to prepare. Lists are merged into a single plot-ready metric.

required

group_order

dict[str, list] | None

{tag_name: [value, ...]} controlling within-tag value ordering.

None

sort_by

list[str] | str | None

Override spatial sort priority (which tag is the primary x-axis sort dimension). Colours are unaffected — they always follow the groupby(tags=...) order. Accepts a single tag name or a list. See :meth:_sns_plot_common for details.

None

merge_by

('metric', 'component')

Used only when metric is a list with more than one item. Controls whether merged labels are arranged as metric::component ("metric"), component::metric ("component"), or kept as flat merged labels without two-level axis formatting (None).

"metric"

ax

Axes

Axes to plot on. If None, a new figure is created with auto-calculated size.

None

figsize

tuple[float, float]

Override the automatic figure size.

None

Returns:

Type Description
PlotSpec

A namespace with the following attributes:

  • fig — the :class:~matplotlib.figure.Figure
  • ax — the :class:~matplotlib.axes.Axes
  • df — tidy long-form :class:~pandas.DataFrame
  • sns_kwargsdict ready to unpack into any seaborn categorical plot function (contains data, x, y, hue, order, hue_order, palette, dodge, ax)
  • metric_name — raw metric name string
  • ylabel — auto-detected y-axis label (or "Value")
  • hide_legendbool hint for legend handling
  • created_figbool whether the figure was created here
  • n_componentsint number of unique components
  • n_groupsint number of unique groups (1 if ungrouped)
  • filename_prefixstr | None handle slug for auto-filenames

Examples:

Basic power-user workflow::

import seaborn as sns

spec = sc_grouped.prepare_plot(
    "total_distance",
    group_order=GROUP_ORDER,
    sort_by="timepoint",
)

# Full seaborn control — override anything you like
sns.boxplot(**spec.sns_kwargs, width=0.6)
spec.ax.set_title("My custom title")
spec.fig.savefig("custom.png", dpi=300)

Composing multiple layers::

spec = sc_grouped.prepare_plot(metric, group_order=ORDER)
sns.barplot(**spec.sns_kwargs, errorbar=None, alpha=0.4)
sns.stripplot(**spec.sns_kwargs, size=4, jitter=True)

snsstrip

snsstrip(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    annotate=None,
    ax=None,
    show: bool = True,
    savedir: str | None = None,
    filename: str | None = None,
    title: str | None = None,
    **kwargs,
)

Strip plot (jittered scatter) using seaborn.

Parameters:

Name Type Description Default

metric

str or BatchResult

Either a key from Summary.data, or a BatchResult from a batch method.

required

group_order

dict[str, list] | None

Control group display order. See :meth:_sns_plot_common.

None

sort_by

list[str] | str | None

Override spatial sort priority. See :meth:_sns_plot_common.

None

ax

Axes

Axes to plot on. If None, creates new figure.

None

show

bool

Display the plot. Default True.

True

savedir

str | None

Directory to save figure.

None

filename

str | None

Custom filename.

None

title

str | None

Plot title.

None

**kwargs

Passed to seaborn.stripplot (e.g., jitter, alpha, size, palette). Also accepts random_state for deterministic jitter placement.

{}

Returns:

Type Description
tuple[Figure, Axes, DataFrame]

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> for f in fc.values():
...     idx = f.tracking.data.index[:30]
...     f.store(pd.Series(([True, False] * 15)[:len(idx)], index=idx),
...             'active', meta={})
>>> sc = SummaryCollection.from_features_collection(fc)
>>> fig, ax, df = sc.snsstrip(sc.each.time_in_state('active'), show=False)
>>> isinstance(df, pd.DataFrame)
True

snsswarm

snsswarm(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    annotate=None,
    ax=None,
    show: bool = True,
    savedir: str | None = None,
    filename: str | None = None,
    title: str | None = None,
    **kwargs,
)

Swarm plot (non-overlapping scatter) using seaborn.

Parameters:

Name Type Description Default

metric

str or BatchResult

Either a key from Summary.data, or a BatchResult from a batch method.

required

group_order

dict[str, list] | None

Control group display order. See :meth:_sns_plot_common.

None

sort_by

list[str] | str | None

Override spatial sort priority. See :meth:_sns_plot_common.

None

ax

Save/display options.

None

show

Save/display options.

None

savedir

Save/display options.

None

filename

Save/display options.

None

title

Save/display options.

None

**kwargs

Passed to seaborn.swarmplot (e.g., size, palette).

{}

Returns:

Type Description
tuple[Figure, Axes, DataFrame]

snsbar

snsbar(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    annotate=None,
    ax=None,
    show: bool = True,
    savedir: str | None = None,
    filename: str | None = None,
    title: str | None = None,
    **kwargs,
)

Bar plot with error bars using seaborn.

Parameters:

Name Type Description Default

metric

str or BatchResult

Either a key from Summary.data, or a BatchResult from a batch method.

required

group_order

dict[str, list] | None

Control group display order. See :meth:_sns_plot_common.

None

sort_by

list[str] | str | None

Override spatial sort priority. See :meth:_sns_plot_common.

None

ax

Save/display options.

None

show

Save/display options.

None

savedir

Save/display options.

None

filename

Save/display options.

None

title

Save/display options.

None

**kwargs

Passed to seaborn.barplot (e.g., errorbar, palette, saturation).

{}

Returns:

Type Description
tuple[Figure, Axes, DataFrame]

snsbox

snsbox(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    annotate=None,
    ax=None,
    show: bool = True,
    savedir: str | None = None,
    filename: str | None = None,
    title: str | None = None,
    **kwargs,
)

Box plot using seaborn.

Parameters:

Name Type Description Default

metric

str or BatchResult

Either a key from Summary.data, or a BatchResult from a batch method.

required

group_order

dict[str, list] | None

Control group display order. See :meth:_sns_plot_common.

None

sort_by

list[str] | str | None

Override spatial sort priority. See :meth:_sns_plot_common.

None

ax

Save/display options.

None

show

Save/display options.

None

savedir

Save/display options.

None

filename

Save/display options.

None

title

Save/display options.

None

**kwargs

Passed to seaborn.boxplot (e.g., width, palette, fliersize).

{}

Returns:

Type Description
tuple[Figure, Axes, DataFrame]

snsviolin

snsviolin(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    annotate=None,
    ax=None,
    show: bool = True,
    savedir: str | None = None,
    filename: str | None = None,
    title: str | None = None,
    **kwargs,
)

Violin plot using seaborn.

Parameters:

Name Type Description Default

metric

str or BatchResult

Either a key from Summary.data, or a BatchResult from a batch method.

required

group_order

dict[str, list] | None

Control group display order. See :meth:_sns_plot_common.

None

sort_by

list[str] | str | None

Override spatial sort priority. See :meth:_sns_plot_common.

None

ax

Save/display options.

None

show

Save/display options.

None

savedir

Save/display options.

None

filename

Save/display options.

None

title

Save/display options.

None

**kwargs

Passed to seaborn.violinplot (e.g., inner, split, palette).

{}

Returns:

Type Description
tuple[Figure, Axes, DataFrame]

snspoint

snspoint(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    annotate=None,
    ax=None,
    show: bool = True,
    savedir: str | None = None,
    filename: str | None = None,
    title: str | None = None,
    **kwargs,
)

Point plot (mean + CI) using seaborn.

Parameters:

Name Type Description Default

metric

str or BatchResult

Either a key from Summary.data, or a BatchResult from a batch method.

required

group_order

dict[str, list] | None

Control group display order. See :meth:_sns_plot_common.

None

sort_by

list[str] | str | None

Override spatial sort priority. See :meth:_sns_plot_common.

None

ax

Save/display options.

None

show

Save/display options.

None

savedir

Save/display options.

None

filename

Save/display options.

None

title

Save/display options.

None

**kwargs

Passed to seaborn.pointplot (e.g., errorbar, markers, linestyles).

{}

Returns:

Type Description
tuple[Figure, Axes, DataFrame]

snssuperplot

snssuperplot(
    metric,
    *,
    group_order: dict | None = None,
    sort_by: list | str | None = None,
    annotate=None,
    ax=None,
    show: bool = True,
    savedir: str | None = None,
    filename: str | None = None,
    title: str | None = None,
    ylabel: str | None = None,
    bar_kwargs: dict | None = None,
    strip_kwargs: dict | None = None,
    **kwargs,
)

Superplot: bar plot (mean) with strip plot (individual dots) overlay.

This is the "publication-ready" visualization showing mean bars with individual data points scattered on top, commonly used in scientific papers. The dots are constrained within the bar width by default.

Parameters:

Name Type Description Default

metric

str or BatchResult

Either a key from Summary.data, or a BatchResult from a batch method.

required

group_order

dict[str, list] | None

Control group display order. See :meth:_sns_plot_common.

None

sort_by

list[str] | str | None

Override spatial sort priority. See :meth:_sns_plot_common.

None

annotate

str or dict or None

Statistical annotations. See :meth:_sns_plot_common.

None

ax

Axes

Axes to plot on. If None, creates new figure.

None

show

bool

Display the plot. Default True.

True

savedir

str | None

Directory to save figure.

None

filename

str | None

Custom filename.

None

title

str | None

Plot title.

None

ylabel

str | None

Y-axis label. Auto-detected from metric when None.

None

bar_kwargs

dict | None

Extra kwargs for barplot (e.g., errorbar, capsize, saturation).

None

strip_kwargs

dict | None

Extra kwargs for stripplot (e.g., alpha, size, jitter).

None

**kwargs

Common kwargs passed to both plots (e.g., palette, dodge). Also accepts random_state for deterministic jitter placement.

{}

Returns:

Type Description
tuple[Figure, Axes, DataFrame]

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> import pandas as pd
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> from py3r.behaviour.features.features_collection import FeaturesCollection
>>> from py3r.behaviour.summary.summary_collection import SummaryCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...     tc = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
>>> fc = FeaturesCollection.from_tracking_collection(tc)
>>> for f in fc.values():
...     n = len(f.tracking.data)
...     states = pd.Series((['A', 'B', 'A'] * (n // 3 + 1))[:n],
...                        index=f.tracking.data.index)
...     f.store(states, 'zone', meta={})
>>> sc = SummaryCollection.from_features_collection(fc)
>>> fig, ax, df = sc.snssuperplot(sc.each.time_in_state('zone'), show=False)
>>> isinstance(df, pd.DataFrame)
True

values

values()

Values iterator (elements or sub-collections).

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
>>> len(list(coll.values())) == 2
True

items

items()

Items iterator (handle, element).

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
>>> sorted([h for h, _ in coll.items()])
['A', 'B']

keys

keys()

Keys iterator (handles or group keys).

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
>>> list(sorted(coll.keys()))
['A', 'B']

merge classmethod

merge(collections, *, copy=False)

Merge multiple collections into a single flat collection containing all leaf elements from each input.

Each input collection is flattened before merging, so grouped inputs are supported. The result is always a new flat collection. Leaves are shared by reference unless copy=True.

Parameters:

Name Type Description Default

collections

list[BaseCollection]

Two or more collections of the same concrete type. Every element across all collections must have a unique handle.

required

copy

bool

If True, each leaf is copied (via its .copy() method) so that the merged collection is fully independent of the originals.

False

Returns:

Type Description
BaseCollection

A new flat collection containing all leaves.

Raises:

Type Description
ValueError

If collections is empty, or if any handles are duplicated.

TypeError

If any input is not an instance of the calling class.

Warns:

Type Description
UserWarning

If the tag key sets differ across input collections (the merged collection will have mixed tag coverage).

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv'); _ = shutil.copy(p, d / 'B.csv')
...         _ = shutil.copy(p, d / 'C.csv'); _ = shutil.copy(p, d / 'D.csv')
...     c1 = TrackingCollection.from_dlc({'A': str(d/'A.csv'), 'B': str(d/'B.csv')}, fps=30)
...     c2 = TrackingCollection.from_dlc({'C': str(d/'C.csv'), 'D': str(d/'D.csv')}, fps=30)
>>> merged = TrackingCollection.merge([c1, c2])
>>> sorted(merged.keys())
['A', 'B', 'C', 'D']
>>> len(merged)
4

groupby

groupby(tags)

Group the collection by one or more existing tag names. Returns a grouped view (this same collection type) whose values are sub-collections keyed by a tuple of tag values in the order provided.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     coll['A'].add_tag('group','G1'); coll['B'].add_tag('group','G2')
>>> g = coll.groupby('group')
>>> g.is_grouped
True
>>> sorted(g.group_keys)
[('G1',), ('G2',)]

flatten

flatten()

Flatten a MultipleCollection to a flat Collection. If already flat, return self.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     coll['A'].add_tag('group','G1'); coll['B'].add_tag('group','G1')
...     g = coll.groupby('group')
>>> flat = g.flatten()
>>> flat.is_grouped
False
>>> sorted(flat.keys())
['A', 'B']

get_group

get_group(key)

Get a sub-collection by group key from a grouped view.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     coll['A'].add_tag('group','G1'); coll['B'].add_tag('group','G2')
>>> g = coll.groupby('group')
>>> sub = g.get_group(('G1',))
>>> list(sub.keys())
['A']

regroup

regroup()

Recompute the same grouping using the current tags and the original grouping tag order. If not grouped, returns self.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     coll['A'].add_tag('group','G1'); coll['B'].add_tag('group','G1')
...     g = coll.groupby('group')
...     coll['B'].add_tag('group','G2', overwrite=True)  # change tag
>>> g2 = g.regroup()
>>> sorted(g2.group_keys)
[('G1',), ('G2',)]

tags_info

tags_info(
    *, include_value_counts: bool = False
) -> pd.DataFrame

Summarize tag presence across the collection's leaf objects. Works for flat and grouped collections. If include_value_counts is True, include a column 'value_counts' with a dict of value->count for each tag. Returns a pandas.DataFrame with columns: ['tag', 'attached_to', 'missing_from', 'unique_values', ('value_counts')]

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     coll['A'].add_tag('genotype', 'WT')
...     coll['B'].add_tag('timepoint', 'T1')
>>> info = coll.tags_info(include_value_counts=True)
>>> int(info.loc['genotype','attached_to'])
1
>>> int(info.loc['genotype','missing_from'])
1
>>> int(info.loc['genotype','unique_values'])
1
>>> info.loc['genotype','value_counts']
{'WT': 1}
>>> int(info.loc['timepoint','attached_to'])
1

map_leaves

map_leaves(fn)

Apply a function to every leaf element and return a new collection of the same type. Preserves grouping shape and groupby metadata when grouped.

fn: callable(Element) -> ElementLike

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
>>> sub = coll.map_leaves(lambda t: t.loc[0:1])
>>> all(len(t.data) == 2 for t in sub.values())
True

copy

copy()

Creates a copy of the BaseCollection. Raises NotImplementedError if any leaf does not implement copy().

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking import Tracking
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         _ = shutil.copy(p, d / 'A.csv')
...         _ = shutil.copy(p, d / 'B.csv')
...     coll = TrackingCollection.from_folder(
...         str(d), tracking_loader=Tracking.from_dlc, fps=30
...     )
>>> coll_copy = coll.copy()
>>> sorted(coll_copy.keys())
['A', 'B']

save

save(
    dirpath: str,
    *,
    overwrite: bool = False,
    data_format: str = "parquet",
) -> None

Save this collection to a directory. Preserves grouping and delegates to leaf objects' save(dirpath, data_format, overwrite=True).

Examples:

>>> import tempfile, shutil, os
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     out = d / 'coll'
...     coll.save(str(out), overwrite=True, data_format='csv')
...     # collection-level manifest at top-level
...     assert os.path.exists(os.path.join(str(out), 'manifest.json'))
...     # element-level manifests under elements/<handle>/
...     el_manifest = os.path.join(str(out), 'elements', 'A', 'manifest.json')
...     assert os.path.exists(el_manifest)

load classmethod

load(dirpath: str)

Load a collection previously saved with save(). Uses the class's _element_type.load to reconstruct leaves.

Examples:

>>> import tempfile, shutil
>>> from pathlib import Path
>>> from py3r.behaviour.util.docdata import data_path
>>> from py3r.behaviour.tracking.tracking_collection import TrackingCollection
>>> with tempfile.TemporaryDirectory() as d:
...     d = Path(d)
...     with data_path('py3r.behaviour.tracking._data', 'dlc_single.csv') as p:
...         a = d / 'A.csv'; b = d / 'B.csv'
...         _ = shutil.copy(p, a); _ = shutil.copy(p, b)
...     coll = TrackingCollection.from_dlc({'A': str(a), 'B': str(b)}, fps=30)
...     out = d / 'coll'
...     coll.save(str(out), overwrite=True, data_format='csv')
...     coll2 = TrackingCollection.load(str(out))
>>> list(sorted(coll2.keys()))
['A', 'B']