Generating a randomized file subset¶
When you have a large folder of recordings, two kinds of bias can creep into an analysis:
- Selection bias — which files you choose to analyze.
- Scoring bias — how you analyze a file once you know its condition.
This notebook addresses the first: it draws a deterministic, per-condition randomized subset from a dataset and writes it to a manifest CSV. You can then open that CSV in CloudScope with Load CSV and turn on blinded analysis mode to address the second.
We demonstrate on the reproducible velocity-sample-data sample. To use your own data, replace ensure_sample(...) with AcqImageList("/path/to/your/data") — everything else is the same.
How this fits in CloudScope¶
The randomized-manifest API lives on AcqImageList in the acqstore backend, so it works identically from a notebook or a script:
to_randomized_manifest_master_csv(...)writes all files in a deterministic randomized order within each group (the master list).to_randomized_manifest_csv(...)takes the firstn_per_groupfiles of each group from a master to form a balanced subset.AcqImageList.from_manifest_csv(...)loads a manifest back into a list.
Splitting master from subset means you can enlarge a study later (bump n_per_group) and keep the earlier files, in the same order, without re-randomizing.
from pathlib import Path
import tempfile
import pandas as pd
from acqstore.acq_image import AcqImageList
from acqstore.sample_data import ensure_sample, VELOCITY_SAMPLE_DATA
Load the dataset¶
We load the sample folder and group by the grandparent column, which holds the experimental condition for this dataset (for example 14d Saline). Every dataset exposes the same manifest columns; pick whichever categorical column names your condition — common choices are grandparent, genotype, or condition.
sample_folder = ensure_sample(VELOCITY_SAMPLE_DATA)
acq_list = AcqImageList(str(sample_folder))
GROUPBY = "grandparent" # condition column for this dataset
rows = acq_list.get_schema_rows()
group_counts = pd.Series([r[GROUPBY] for r in rows]).value_counts()
print(f"{len(rows)} files across {group_counts.size} groups:")
print(group_counts.to_string())
15 files across 3 groups: 14d Saline 8 28d AngII 4 28d Saline 3
Step 1 — write the master manifest¶
The master manifest lists every file, shuffled deterministically within each group. We pass a random_seed so the ordering is reproducible, and root_path=sample_folder so the _rel_path column is written relative to the dataset root (this is what lets the manifest reload from anywhere).
We write the CSVs into a temporary working directory to keep the sample cache clean; in practice you would save them next to your dataset or in your project folder.
work_dir = Path(tempfile.mkdtemp(prefix="cloudscope-manifest-"))
master_csv = work_dir / "master_manifest.csv"
acq_list.to_randomized_manifest_master_csv(
master_csv,
groupby_column=GROUPBY,
random_seed=42,
root_path=sample_folder,
)
master_df = pd.read_csv(master_csv)
print(f"master manifest: {len(master_df)} rows -> {master_csv}")
master_df
master manifest: 15 rows -> /var/folders/76/6bdl7smj72g6tynz3985xxl40000gn/T/cloudscope-manifest-cz45r1tr/master_manifest.csv
| _rel_path | _group | _random_order | _source_index | |
|---|---|---|---|---|
| 0 | 14d Saline/20251014/20251014_A98_0005.oir | 14d Saline | 0 | 3 |
| 1 | 14d Saline/20251014/20251014_A98_0006.oir | 14d Saline | 1 | 4 |
| 2 | 14d Saline/20251015/20251015_A99_0003.oir | 14d Saline | 2 | 6 |
| 3 | 14d Saline/20251015/20251015_A99_0004.oir | 14d Saline | 3 | 7 |
| 4 | 14d Saline/20251014/20251014_A98_0004.oir | 14d Saline | 4 | 2 |
| 5 | 14d Saline/20251015/20251015_A99_0002.oir | 14d Saline | 5 | 5 |
| 6 | 14d Saline/20251014/20251014_A98_0002.oir | 14d Saline | 6 | 0 |
| 7 | 14d Saline/20251014/20251014_A98_0003.oir | 14d Saline | 7 | 1 |
| 8 | 28d AngII/20250717/20250717_A87_0005.oir | 28d AngII | 0 | 9 |
| 9 | 28d AngII/20260102/20260102_A118_0003.oir | 28d AngII | 1 | 11 |
| 10 | 28d AngII/20260102/20260102_A118_0002.oir | 28d AngII | 2 | 10 |
| 11 | 28d AngII/20250717/20250717_A87_0004.oir | 28d AngII | 3 | 8 |
| 12 | 28d Saline/20251029/20251029_A105_0002.oir | 28d Saline | 0 | 12 |
| 13 | 28d Saline/20251029/20251029_A105_0003.oir | 28d Saline | 1 | 13 |
| 14 | 28d Saline/20251030/20251030_A106_0002.oir | 28d Saline | 2 | 14 |
The _group column is the condition, _random_order is the shuffled position within that group, and _source_index records the file's position in the original list. Because the seed is fixed, re-running this cell always produces the same order.
Step 2 — draw a balanced subset¶
The subset takes the first n_per_group files of each group from the master. With n_per_group=3 and three groups we get a balanced set of nine files. If some groups have fewer than n_per_group files, pass allow_unbalanced=True to take as many as are available instead of raising.
subset_csv = work_dir / "subset_manifest.csv"
acq_list.to_randomized_manifest_csv(
subset_csv,
master_csv_path=master_csv,
n_per_group=3,
)
subset_df = pd.read_csv(subset_csv)
print("files per group in subset:")
print(subset_df["_group"].value_counts().to_string())
subset_df
files per group in subset: _group 14d Saline 3 28d AngII 3 28d Saline 3
| _rel_path | _group | _random_order | _source_index | |
|---|---|---|---|---|
| 0 | 14d Saline/20251014/20251014_A98_0005.oir | 14d Saline | 0 | 3 |
| 1 | 14d Saline/20251014/20251014_A98_0006.oir | 14d Saline | 1 | 4 |
| 2 | 14d Saline/20251015/20251015_A99_0003.oir | 14d Saline | 2 | 6 |
| 3 | 28d AngII/20250717/20250717_A87_0005.oir | 28d AngII | 0 | 9 |
| 4 | 28d AngII/20260102/20260102_A118_0003.oir | 28d AngII | 1 | 11 |
| 5 | 28d AngII/20260102/20260102_A118_0002.oir | 28d AngII | 2 | 10 |
| 6 | 28d Saline/20251029/20251029_A105_0002.oir | 28d Saline | 0 | 12 |
| 7 | 28d Saline/20251029/20251029_A105_0003.oir | 28d Saline | 1 | 13 |
| 8 | 28d Saline/20251030/20251030_A106_0002.oir | 28d Saline | 2 | 14 |
Step 3 — load the subset back¶
from_manifest_csv returns a structured LoadResult with the loaded AcqImageList and any load warnings. We pass the same root_path used when writing so the relative paths resolve. Here we skip eager image and CSV loading for speed.
result = AcqImageList.from_manifest_csv(
subset_csv,
root_path=sample_folder,
load_images=False,
load_analysis_csv=False,
)
print(f"loaded {len(result.acq_image_list.get_files())} files, "
f"{len(result.warnings)} warnings")
for acq in result.acq_image_list.get_files():
print(" ", acq.name)
loaded 9 files, 0 warnings 20251014_A98_0005.oir 20251014_A98_0006.oir 20251015_A99_0003.oir 20250717_A87_0005.oir 20260102_A118_0003.oir 20260102_A118_0002.oir 20251029_A105_0002.oir 20251029_A105_0003.oir 20251030_A106_0002.oir
Use it in the CloudScope GUI¶
- Save the subset manifest CSV somewhere you can find it (above we used a temporary folder; use a real path for your own studies).
- In CloudScope, open the history menu and choose Load CSV, then select the subset manifest.
- Open the Config panel and turn on blinded mode before scoring or running analyses. See Blinded analysis mode.
You are now analyzing an unbiased subset without seeing file names or conditions.
Adapting to your own data¶
- Point at your folder:
acq_list = AcqImageList("/path/to/your/data"). - Choose the grouping column: set
GROUPBYto the categorical manifest column that names your condition (for examplegenotype,condition, orgrandparent). Inspectacq_list.get_schema_rows()[0]to see the available columns. - Balance vs. availability: raise
n_per_groupfor larger studies, and passallow_unbalanced=Truewhen groups are uneven. - Reproducibility: keep the
random_seedfixed to make the sampling auditable and repeatable.
The master/subset split lets a study grow over time: keep the master manifest under version control, and regenerate larger subsets from it as you collect more data.