diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4dcca68..8091d709 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -129,6 +129,15 @@ notes — with cross-references and examples — live in
axes' tick labels and raised `ValueError: not enough values to unpack (expected 2, got 0)`.
Rendering is unchanged: the three `pytest-mpl` baselines are byte-identical before and after,
and a caller-supplied `ax` is still drawn into, so multi-panel composition is unaffected.
+- `load_dataset(random=True)` is reproducible: the new `random_state` parameter seeds the
+ balanced per-class sampling, and `options['random_state']` overrides it like everywhere else
+ in the package. The function had no seed at all, so the first line of most workflows drew a
+ different sample on every call and no benchmark, tutorial or bug report using `random=True`
+ could be reproduced. The per-class draws share one `numpy.random.RandomState`, whose stream is
+ stable across numpy versions, so a given seed reproduces the same frame in another process.
+ Everything else is untouched: `random=False` (the deterministic head-of-class selection) and
+ an unseeded `random=True` are byte-identical to before, verified over all 14 bundled datasets
+ (#582).
### Changed
- `CPP.run(n_batches=...)` (scale-axis batching) now returns output identical to the
diff --git a/aaanalysis/data_handling/_load_dataset.py b/aaanalysis/data_handling/_load_dataset.py
index 2db2895a..965fddeb 100644
--- a/aaanalysis/data_handling/_load_dataset.py
+++ b/aaanalysis/data_handling/_load_dataset.py
@@ -66,6 +66,20 @@ def post_check_df_seq(df_seq: pd.DataFrame, n: Optional[int] = None, name: Optio
# Helper functions
+def _get_sampling_rng(random_state: Optional[int] = None) -> Optional[np.random.RandomState]:
+ """Get random number generator for the balanced class sampling
+
+ A single generator is shared by the per-class draws, so each class continues the same
+ stream instead of repeating it. ``np.random.RandomState`` is used (not ``default_rng``)
+ because its stream is guaranteed stable across numpy versions, which is what makes a
+ given seed reproduce the same sample in another process or environment. ``None`` keeps
+ the truly random default (pandas then draws from the global numpy random state).
+ """
+ if random_state is None:
+ return None
+ return np.random.RandomState(random_state)
+
+
def _is_aa_level(name: str) -> bool:
return name.split("_")[0] == "AA"
@@ -147,6 +161,7 @@ def load_dataset(name: str = "Overview",
max_len: Optional[int] = None,
aa_window_size: Union[int, None] = 9,
verbose: bool = False,
+ random_state: Optional[int] = None,
) -> DataFrame:
"""
Load protein benchmarking datasets.
@@ -178,6 +193,7 @@ def load_dataset(name: str = "Overview",
Number of proteins per class, selected by index. If ``None``, the whole dataset will be returned.
random : bool, default=False
If ``True``, ``n`` randomly selected proteins per class will be chosen.
+ Set ``random_state`` to make that selection reproducible.
non_canonical_aa : {'remove', 'keep', 'gap'}, default='remove'
Options for handling non-canonical amino acids:
@@ -200,6 +216,12 @@ def load_dataset(name: str = "Overview",
If ``True``, report how many entries each removal step (``min_len``,
``max_len``, and ``non_canonical_aa='remove'``) dropped. Does not change
the returned data.
+ random_state : int, optional
+ The seed used by the random number generator. If a positive integer, results of stochastic processes are
+ consistent, enabling reproducibility. If ``None``, stochastic processes will be truly random. Only used
+ together with ``random=True``; overridden by ``options['random_state']`` when set.
+
+ .. versionadded:: 1.2.0
Returns
-------
@@ -266,6 +288,7 @@ def load_dataset(name: str = "Overview",
is_cs_dataset = _is_cleavage_site_dataset(name=name)
check_aa_window_size(aa_window_size=aa_window_size, is_cs_dataset=is_cs_dataset)
verbose = ut.check_verbose(verbose)
+ random_state = ut.check_random_state(random_state=random_state)
# Load overview table
if name == "Overview":
@@ -311,7 +334,8 @@ def load_dataset(name: str = "Overview",
if n is not None:
labels = set(df_seq[ut.COL_LABEL])
if random:
- df_seq = pd.concat([df_seq[df_seq[ut.COL_LABEL] == l].sample(n) for l in labels])
+ rng = _get_sampling_rng(random_state=random_state)
+ df_seq = pd.concat([df_seq[df_seq[ut.COL_LABEL] == l].sample(n=n, random_state=rng) for l in labels])
else:
df_seq = pd.concat([df_seq[df_seq[ut.COL_LABEL] == l].head(n) for l in labels])
post_check_df_seq(df_seq=df_seq, n=n, name=name)
diff --git a/examples/data_handling/load_dataset.ipynb b/examples/data_handling/load_dataset.ipynb
index d564faa0..f12ba550 100644
--- a/examples/data_handling/load_dataset.ipynb
+++ b/examples/data_handling/load_dataset.ipynb
@@ -21,10 +21,10 @@
},
"collapsed": false,
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:36.989110Z",
- "iopub.status.busy": "2026-09-14T21:28:36.988989Z",
- "iopub.status.idle": "2026-09-14T21:28:38.529658Z",
- "shell.execute_reply": "2026-09-14T21:28:38.529357Z"
+ "iopub.execute_input": "2026-09-18T22:09:31.956656Z",
+ "iopub.status.busy": "2026-09-18T22:09:31.956267Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.365636Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.365453Z"
}
},
"outputs": [
@@ -39,238 +39,238 @@
"data": {
"text/html": [
"\n",
- "
\n",
+ "\n",
" \n",
" \n",
" | | \n",
- " Level | \n",
- " Dataset | \n",
- " # Sequences | \n",
- " Avg length | \n",
- " # Amino acids | \n",
- " # Positives | \n",
- " # Negatives | \n",
- " Predictor | \n",
- " Description | \n",
- " Reference | \n",
- " Label | \n",
+ " Level | \n",
+ " Dataset | \n",
+ " # Sequences | \n",
+ " Avg length | \n",
+ " # Amino acids | \n",
+ " # Positives | \n",
+ " # Negatives | \n",
+ " Predictor | \n",
+ " Description | \n",
+ " Reference | \n",
+ " Label | \n",
"
\n",
" \n",
" \n",
" \n",
- " | 1 | \n",
- " Amino acid | \n",
- " AA_CASPASE3 | \n",
- " 233 | \n",
- " 796.587983 | \n",
- " 185605 | \n",
- " 705 | \n",
- " 184900 | \n",
- " PROSPERous | \n",
- " Prediction of c...3 cleavage site | \n",
- " Song et al., 2018 | \n",
- " 1 (adjacent to ... cleavage site) | \n",
- "
\n",
- " \n",
- " | 2 | \n",
- " Amino acid | \n",
- " AA_FURIN | \n",
- " 71 | \n",
- " 831.028169 | \n",
- " 59003 | \n",
- " 163 | \n",
- " 58840 | \n",
- " PROSPERous | \n",
- " Prediction of f...n cleavage site | \n",
- " Song et al., 2018 | \n",
- " 1 (adjacent to ... cleavage site) | \n",
- "
\n",
- " \n",
- " | 3 | \n",
- " Amino acid | \n",
- " AA_LDR | \n",
- " 342 | \n",
- " 345.754386 | \n",
- " 118248 | \n",
- " 35469 | \n",
- " 82779 | \n",
- " IDP-Seq2Seq | \n",
- " Prediction of l...d regions (LDR) | \n",
- " Tang et al., 2020 | \n",
- " 1 (disordered), 0 (ordered) | \n",
- "
\n",
- " \n",
- " | 4 | \n",
- " Amino acid | \n",
- " AA_MMP2 | \n",
- " 573 | \n",
- " 546.205934 | \n",
- " 312976 | \n",
- " 2416 | \n",
- " 310560 | \n",
- " PROSPERous | \n",
- " Prediction of M...) cleavage site | \n",
- " Song et al., 2018 | \n",
- " 1 (adjacent to ... cleavage site) | \n",
- "
\n",
- " \n",
- " | 5 | \n",
- " Amino acid | \n",
- " AA_RNABIND | \n",
- " 221 | \n",
- " 248.873303 | \n",
- " 55001 | \n",
- " 6492 | \n",
- " 48509 | \n",
- " GMKSVM-RU | \n",
- " Prediction of R...(RBP60 dataset) | \n",
- " Yang et al., 2021 | \n",
- " 1 (binding), 0 (non-binding) | \n",
- "
\n",
- " \n",
- " | 6 | \n",
- " Amino acid | \n",
- " AA_SA | \n",
- " 233 | \n",
- " 796.587983 | \n",
- " 185605 | \n",
- " 101082 | \n",
- " 84523 | \n",
- " PROSPERous | \n",
- " Prediction of s...PASE3 data set) | \n",
- " Song et al., 2018 | \n",
- " 1 (exposed/acce...non-accessible) | \n",
- "
\n",
- " \n",
- " | 7 | \n",
- " Sequence | \n",
- " SEQ_AMYLO | \n",
- " 1414 | \n",
- " 6.000000 | \n",
- " 8484 | \n",
- " 511 | \n",
- " 903 | \n",
- " ReRF-Pred | \n",
- " Prediction of a...ognenic regions | \n",
- " Teng et al. 2021 | \n",
- " 1 (amyloidogeni...-amyloidogenic) | \n",
- "
\n",
- " \n",
- " | 8 | \n",
- " Sequence | \n",
- " SEQ_CAPSID | \n",
- " 7935 | \n",
- " 424.030246 | \n",
- " 3364680 | \n",
- " 3864 | \n",
- " 4071 | \n",
- " VIRALpro | \n",
- " Prediction of capdsid proteins | \n",
- " Galiez et al., 2016 | \n",
- " 1 (capsid prote...capsid protein) | \n",
- "
\n",
- " \n",
- " | 9 | \n",
- " Sequence | \n",
- " SEQ_DISULFIDE | \n",
- " 2547 | \n",
- " 241.252454 | \n",
- " 614470 | \n",
- " 897 | \n",
- " 1650 | \n",
- " Dipro | \n",
- " Prediction of d...es in sequences | \n",
- " Cheng et al., 2006 | \n",
- " 1 (sequence wit...ithout SS bond) | \n",
- "
\n",
- " \n",
- " | 10 | \n",
- " Sequence | \n",
- " SEQ_LOCATION | \n",
- " 1835 | \n",
- " 399.126975 | \n",
- " 732398 | \n",
- " 1045 | \n",
- " 790 | \n",
- " nan | \n",
- " Prediction of s...lasma membrane) | \n",
- " Shen et al., 2019 | \n",
- " 1 (protein in c...asma membrane) | \n",
- "
\n",
- " \n",
- " | 11 | \n",
- " Sequence | \n",
- " SEQ_SOLUBLE | \n",
- " 17408 | \n",
- " 254.611041 | \n",
- " 4432269 | \n",
- " 8704 | \n",
- " 8704 | \n",
- " SOLpro | \n",
- " Prediction of s...oluble proteins | \n",
- " Magnan et al., 2009 | \n",
- " 1 (soluble), 0 (insoluble) | \n",
- "
\n",
- " \n",
- " | 12 | \n",
- " Sequence | \n",
- " SEQ_TAIL | \n",
- " 6668 | \n",
- " 400.673365 | \n",
- " 2671690 | \n",
- " 2574 | \n",
- " 4094 | \n",
- " VIRALpro | \n",
- " Prediction of tail proteins | \n",
- " Galiez et al., 2016 | \n",
- " 1 (tail protein...n-tail protein) | \n",
- "
\n",
- " \n",
- " | 13 | \n",
- " Domain | \n",
- " DOM_GSEC | \n",
- " 126 | \n",
- " 737.809524 | \n",
- " 92964 | \n",
- " 63 | \n",
- " 63 | \n",
- " nan | \n",
- " Prediction of g...tase substrates | \n",
- " Breimann et al, 2024c | \n",
- " 1 (substrate), ...(non-substrate) | \n",
- "
\n",
- " \n",
- " | 14 | \n",
- " Domain | \n",
- " DOM_GSEC_PU | \n",
- " 694 | \n",
- " 712.570605 | \n",
- " 494524 | \n",
- " 63 | \n",
- " 0 | \n",
- " nan | \n",
- " Prediction of g...es (PU dataset) | \n",
- " Breimann et al, 2024c | \n",
- " 1 (substrate), ...bstrate status) | \n",
+ " 1 | \n",
+ " Amino acid | \n",
+ " AA_CASPASE3 | \n",
+ " 233 | \n",
+ " 796.587983 | \n",
+ " 185605 | \n",
+ " 705 | \n",
+ " 184900 | \n",
+ " PROSPERous | \n",
+ " Prediction of c...3 cleavage site | \n",
+ " Song et al., 2018 | \n",
+ " 1 (adjacent to ... cleavage site) | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " Amino acid | \n",
+ " AA_FURIN | \n",
+ " 71 | \n",
+ " 831.028169 | \n",
+ " 59003 | \n",
+ " 163 | \n",
+ " 58840 | \n",
+ " PROSPERous | \n",
+ " Prediction of f...n cleavage site | \n",
+ " Song et al., 2018 | \n",
+ " 1 (adjacent to ... cleavage site) | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " Amino acid | \n",
+ " AA_LDR | \n",
+ " 342 | \n",
+ " 345.754386 | \n",
+ " 118248 | \n",
+ " 35469 | \n",
+ " 82779 | \n",
+ " IDP-Seq2Seq | \n",
+ " Prediction of l...d regions (LDR) | \n",
+ " Tang et al., 2020 | \n",
+ " 1 (disordered), 0 (ordered) | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " Amino acid | \n",
+ " AA_MMP2 | \n",
+ " 573 | \n",
+ " 546.205934 | \n",
+ " 312976 | \n",
+ " 2416 | \n",
+ " 310560 | \n",
+ " PROSPERous | \n",
+ " Prediction of M...) cleavage site | \n",
+ " Song et al., 2018 | \n",
+ " 1 (adjacent to ... cleavage site) | \n",
+ "
\n",
+ " \n",
+ " | 5 | \n",
+ " Amino acid | \n",
+ " AA_RNABIND | \n",
+ " 221 | \n",
+ " 248.873303 | \n",
+ " 55001 | \n",
+ " 6492 | \n",
+ " 48509 | \n",
+ " GMKSVM-RU | \n",
+ " Prediction of R...(RBP60 dataset) | \n",
+ " Yang et al., 2021 | \n",
+ " 1 (binding), 0 (non-binding) | \n",
+ "
\n",
+ " \n",
+ " | 6 | \n",
+ " Amino acid | \n",
+ " AA_SA | \n",
+ " 233 | \n",
+ " 796.587983 | \n",
+ " 185605 | \n",
+ " 101082 | \n",
+ " 84523 | \n",
+ " PROSPERous | \n",
+ " Prediction of s...PASE3 data set) | \n",
+ " Song et al., 2018 | \n",
+ " 1 (exposed/acce...non-accessible) | \n",
+ "
\n",
+ " \n",
+ " | 7 | \n",
+ " Sequence | \n",
+ " SEQ_AMYLO | \n",
+ " 1414 | \n",
+ " 6.000000 | \n",
+ " 8484 | \n",
+ " 511 | \n",
+ " 903 | \n",
+ " ReRF-Pred | \n",
+ " Prediction of a...ognenic regions | \n",
+ " Teng et al. 2021 | \n",
+ " 1 (amyloidogeni...-amyloidogenic) | \n",
+ "
\n",
+ " \n",
+ " | 8 | \n",
+ " Sequence | \n",
+ " SEQ_CAPSID | \n",
+ " 7935 | \n",
+ " 424.030246 | \n",
+ " 3364680 | \n",
+ " 3864 | \n",
+ " 4071 | \n",
+ " VIRALpro | \n",
+ " Prediction of capdsid proteins | \n",
+ " Galiez et al., 2016 | \n",
+ " 1 (capsid prote...capsid protein) | \n",
+ "
\n",
+ " \n",
+ " | 9 | \n",
+ " Sequence | \n",
+ " SEQ_DISULFIDE | \n",
+ " 2547 | \n",
+ " 241.252454 | \n",
+ " 614470 | \n",
+ " 897 | \n",
+ " 1650 | \n",
+ " Dipro | \n",
+ " Prediction of d...es in sequences | \n",
+ " Cheng et al., 2006 | \n",
+ " 1 (sequence wit...ithout SS bond) | \n",
+ "
\n",
+ " \n",
+ " | 10 | \n",
+ " Sequence | \n",
+ " SEQ_LOCATION | \n",
+ " 1835 | \n",
+ " 399.126975 | \n",
+ " 732398 | \n",
+ " 1045 | \n",
+ " 790 | \n",
+ " nan | \n",
+ " Prediction of s...lasma membrane) | \n",
+ " Shen et al., 2019 | \n",
+ " 1 (protein in c...asma membrane) | \n",
+ "
\n",
+ " \n",
+ " | 11 | \n",
+ " Sequence | \n",
+ " SEQ_SOLUBLE | \n",
+ " 17408 | \n",
+ " 254.611041 | \n",
+ " 4432269 | \n",
+ " 8704 | \n",
+ " 8704 | \n",
+ " SOLpro | \n",
+ " Prediction of s...oluble proteins | \n",
+ " Magnan et al., 2009 | \n",
+ " 1 (soluble), 0 (insoluble) | \n",
+ "
\n",
+ " \n",
+ " | 12 | \n",
+ " Sequence | \n",
+ " SEQ_TAIL | \n",
+ " 6668 | \n",
+ " 400.673365 | \n",
+ " 2671690 | \n",
+ " 2574 | \n",
+ " 4094 | \n",
+ " VIRALpro | \n",
+ " Prediction of tail proteins | \n",
+ " Galiez et al., 2016 | \n",
+ " 1 (tail protein...n-tail protein) | \n",
+ "
\n",
+ " \n",
+ " | 13 | \n",
+ " Domain | \n",
+ " DOM_GSEC | \n",
+ " 126 | \n",
+ " 737.809524 | \n",
+ " 92964 | \n",
+ " 63 | \n",
+ " 63 | \n",
+ " nan | \n",
+ " Prediction of g...tase substrates | \n",
+ " Breimann et al, 2024c | \n",
+ " 1 (substrate), ...(non-substrate) | \n",
+ "
\n",
+ " \n",
+ " | 14 | \n",
+ " Domain | \n",
+ " DOM_GSEC_PU | \n",
+ " 694 | \n",
+ " 712.570605 | \n",
+ " 494524 | \n",
+ " 63 | \n",
+ " 0 | \n",
+ " nan | \n",
+ " Prediction of g...es (PU dataset) | \n",
+ " Breimann et al, 2024c | \n",
+ " 1 (substrate), ...bstrate status) | \n",
"
\n",
" \n",
"
\n"
@@ -310,10 +310,10 @@
},
"collapsed": false,
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:38.531289Z",
- "iopub.status.busy": "2026-09-14T21:28:38.531122Z",
- "iopub.status.idle": "2026-09-14T21:28:38.618552Z",
- "shell.execute_reply": "2026-09-14T21:28:38.618269Z"
+ "iopub.execute_input": "2026-09-18T22:09:33.366642Z",
+ "iopub.status.busy": "2026-09-18T22:09:33.366559Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.424844Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.424620Z"
}
},
"outputs": [
@@ -321,63 +321,63 @@
"data": {
"text/html": [
"\n",
- "\n",
+ "\n",
" \n",
" \n",
" | | \n",
- " entry | \n",
- " gene | \n",
- " sequence | \n",
- " label | \n",
+ " entry | \n",
+ " gene | \n",
+ " sequence | \n",
+ " label | \n",
"
\n",
" \n",
" \n",
" \n",
- " | 1 | \n",
- " CAPSID_1 | \n",
- " name_1 | \n",
- " MVTHNVKINKHVTRR...DTPRIPATKLDEENV | \n",
- " 0 | \n",
+ " 1 | \n",
+ " CAPSID_1 | \n",
+ " name_1 | \n",
+ " MVTHNVKINKHVTRR...DTPRIPATKLDEENV | \n",
+ " 0 | \n",
"
\n",
" \n",
- " | 2 | \n",
- " CAPSID_2 | \n",
- " name_2 | \n",
- " MKKRQKKMTLSNFTD...AMLEAVINARHFGEE | \n",
- " 0 | \n",
+ " 2 | \n",
+ " CAPSID_2 | \n",
+ " name_2 | \n",
+ " MKKRQKKMTLSNFTD...AMLEAVINARHFGEE | \n",
+ " 0 | \n",
"
\n",
" \n",
- " | 3 | \n",
- " CAPSID_4072 | \n",
- " name_4072 | \n",
- " MALTTNDVITEDFVR...AWKAIFPEAAVKVDA | \n",
- " 1 | \n",
+ " 3 | \n",
+ " CAPSID_4072 | \n",
+ " name_4072 | \n",
+ " MALTTNDVITEDFVR...AWKAIFPEAAVKVDA | \n",
+ " 1 | \n",
"
\n",
" \n",
- " | 4 | \n",
- " CAPSID_4073 | \n",
- " name_4073 | \n",
- " MGELTDNGVQLAKAQ...TCTNPAAHAKIRDLK | \n",
- " 1 | \n",
+ " 4 | \n",
+ " CAPSID_4073 | \n",
+ " name_4073 | \n",
+ " MGELTDNGVQLAKAQ...TCTNPAAHAKIRDLK | \n",
+ " 1 | \n",
"
\n",
" \n",
"
\n"
@@ -402,7 +402,7 @@
"collapsed": false
},
"source": [
- "The sampling can be performed randomly by setting ``random=True``: "
+ "The sampling can be performed randomly by setting ``random=True``. Provide a fixed seed via ``random_state`` to make the random selection reproducible (the global ``aa.options['random_state']`` setting overrides it):"
]
},
{
@@ -416,10 +416,10 @@
},
"collapsed": false,
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:38.619732Z",
- "iopub.status.busy": "2026-09-14T21:28:38.619656Z",
- "iopub.status.idle": "2026-09-14T21:28:38.662218Z",
- "shell.execute_reply": "2026-09-14T21:28:38.661947Z"
+ "iopub.execute_input": "2026-09-18T22:09:33.425747Z",
+ "iopub.status.busy": "2026-09-18T22:09:33.425685Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.469856Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.469644Z"
}
},
"outputs": [
@@ -427,63 +427,63 @@
"data": {
"text/html": [
"\n",
- "\n",
+ "\n",
" \n",
" \n",
" | | \n",
- " entry | \n",
- " gene | \n",
- " sequence | \n",
- " label | \n",
+ " entry | \n",
+ " gene | \n",
+ " sequence | \n",
+ " label | \n",
"
\n",
" \n",
" \n",
" \n",
- " | 1 | \n",
- " CAPSID_2024 | \n",
- " name_2024 | \n",
- " MSKAGEIYYDIKIRE...NNRIIRTNRAQGVIL | \n",
- " 0 | \n",
+ " 1 | \n",
+ " CAPSID_2839 | \n",
+ " name_2839 | \n",
+ " MDTADIVWVEESVSA...KFVYPFDDKMSFLFA | \n",
+ " 0 | \n",
"
\n",
" \n",
- " | 2 | \n",
- " CAPSID_2227 | \n",
- " name_2227 | \n",
- " MADIPPDPPALNTTP...PAACPYPCGHTFLRP | \n",
- " 0 | \n",
+ " 2 | \n",
+ " CAPSID_151 | \n",
+ " name_151 | \n",
+ " MPDTTTDPRPAPPAP...ADKRTHLLNRAPGHS | \n",
+ " 0 | \n",
"
\n",
" \n",
- " | 3 | \n",
- " CAPSID_6439 | \n",
- " name_6439 | \n",
- " MLELEFEDVPNNIGS...SSAAREFLSKFGIRM | \n",
- " 1 | \n",
+ " 3 | \n",
+ " CAPSID_6241 | \n",
+ " name_6241 | \n",
+ " MRLFLRDFFMSMYTT...MLLADPDEFVSVQLA | \n",
+ " 1 | \n",
"
\n",
" \n",
- " | 4 | \n",
- " CAPSID_5062 | \n",
- " name_5062 | \n",
- " AEGDDPAKAAFNSLQ...TIGIKLFKKFTSKAS | \n",
- " 1 | \n",
+ " 4 | \n",
+ " CAPSID_6362 | \n",
+ " name_6362 | \n",
+ " MSRLGPALDEFRSFK...PDSQSTGGDLSVIDS | \n",
+ " 1 | \n",
"
\n",
" \n",
"
\n"
@@ -497,7 +497,7 @@
}
],
"source": [
- "df_seq = aa.load_dataset(name=\"SEQ_CAPSID\", n=2, random=True)\n",
+ "df_seq = aa.load_dataset(name=\"SEQ_CAPSID\", n=2, random=True, random_state=42)\n",
"aa.display_df(df=df_seq)"
]
},
@@ -522,10 +522,10 @@
},
"collapsed": false,
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:38.663641Z",
- "iopub.status.busy": "2026-09-14T21:28:38.663557Z",
- "iopub.status.idle": "2026-09-14T21:28:38.679470Z",
- "shell.execute_reply": "2026-09-14T21:28:38.679231Z"
+ "iopub.execute_input": "2026-09-18T22:09:33.470766Z",
+ "iopub.status.busy": "2026-09-18T22:09:33.470705Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.484700Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.484488Z"
}
},
"outputs": [
@@ -564,10 +564,10 @@
},
"collapsed": false,
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:38.680525Z",
- "iopub.status.busy": "2026-09-14T21:28:38.680438Z",
- "iopub.status.idle": "2026-09-14T21:28:38.686082Z",
- "shell.execute_reply": "2026-09-14T21:28:38.685824Z"
+ "iopub.execute_input": "2026-09-18T22:09:33.485632Z",
+ "iopub.status.busy": "2026-09-18T22:09:33.485568Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.490371Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.490186Z"
}
},
"outputs": [
@@ -598,10 +598,10 @@
"id": "85ac404f",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:38.687503Z",
- "iopub.status.busy": "2026-09-14T21:28:38.687414Z",
- "iopub.status.idle": "2026-09-14T21:28:38.694645Z",
- "shell.execute_reply": "2026-09-14T21:28:38.694339Z"
+ "iopub.execute_input": "2026-09-18T22:09:33.491200Z",
+ "iopub.status.busy": "2026-09-18T22:09:33.491145Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.496338Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.496148Z"
}
},
"outputs": [
@@ -652,10 +652,10 @@
},
"collapsed": false,
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:38.696133Z",
- "iopub.status.busy": "2026-09-14T21:28:38.696013Z",
- "iopub.status.idle": "2026-09-14T21:28:38.803865Z",
- "shell.execute_reply": "2026-09-14T21:28:38.803572Z"
+ "iopub.execute_input": "2026-09-18T22:09:33.497148Z",
+ "iopub.status.busy": "2026-09-18T22:09:33.497097Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.599518Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.599303Z"
}
},
"outputs": [
@@ -663,58 +663,58 @@
"data": {
"text/html": [
"\n",
- "\n",
+ "\n",
" \n",
" \n",
" | | \n",
- " entry | \n",
- " sequence | \n",
- " label | \n",
+ " entry | \n",
+ " sequence | \n",
+ " label | \n",
"
\n",
" \n",
" \n",
" \n",
- " | 1 | \n",
- " CASPASE3_1_pos2 | \n",
- " MSLFD | \n",
- " 0 | \n",
+ " 1 | \n",
+ " CASPASE3_1_pos2 | \n",
+ " MSLFD | \n",
+ " 0 | \n",
"
\n",
" \n",
- " | 2 | \n",
- " CASPASE3_1_pos3 | \n",
- " SLFDL | \n",
- " 0 | \n",
+ " 2 | \n",
+ " CASPASE3_1_pos3 | \n",
+ " SLFDL | \n",
+ " 0 | \n",
"
\n",
" \n",
- " | 3 | \n",
- " CASPASE3_1_pos126 | \n",
- " LRDSM | \n",
- " 1 | \n",
+ " 3 | \n",
+ " CASPASE3_1_pos126 | \n",
+ " LRDSM | \n",
+ " 1 | \n",
"
\n",
" \n",
- " | 4 | \n",
- " CASPASE3_1_pos127 | \n",
- " RDSML | \n",
- " 1 | \n",
+ " 4 | \n",
+ " CASPASE3_1_pos127 | \n",
+ " RDSML | \n",
+ " 1 | \n",
"
\n",
" \n",
"
\n"
@@ -753,10 +753,10 @@
},
"collapsed": false,
"execution": {
- "iopub.execute_input": "2026-09-14T21:28:38.805071Z",
- "iopub.status.busy": "2026-09-14T21:28:38.804978Z",
- "iopub.status.idle": "2026-09-14T21:28:38.819941Z",
- "shell.execute_reply": "2026-09-14T21:28:38.819703Z"
+ "iopub.execute_input": "2026-09-18T22:09:33.600439Z",
+ "iopub.status.busy": "2026-09-18T22:09:33.600377Z",
+ "iopub.status.idle": "2026-09-18T22:09:33.612642Z",
+ "shell.execute_reply": "2026-09-18T22:09:33.612448Z"
}
},
"outputs": [
@@ -764,112 +764,112 @@
"data": {
"text/html": [
"\n",
- "\n",
+ "\n",
" \n",
" \n",
" | | \n",
- " entry | \n",
- " gene | \n",
- " sequence | \n",
- " label | \n",
- " tmd_start | \n",
- " tmd_stop | \n",
- " jmd_n | \n",
- " tmd | \n",
- " jmd_c | \n",
+ " entry | \n",
+ " gene | \n",
+ " sequence | \n",
+ " label | \n",
+ " tmd_start | \n",
+ " tmd_stop | \n",
+ " jmd_n | \n",
+ " tmd | \n",
+ " jmd_c | \n",
"
\n",
" \n",
" \n",
" \n",
- " | 1 | \n",
- " P05067 | \n",
- " APP | \n",
- " MLPGLALLLLAAWTA...GYENPTYKFFEQMQN | \n",
- " 1 | \n",
- " 701 | \n",
- " 723 | \n",
- " FAEDVGSNKG | \n",
- " AIIGLMVGGVVIATVIVITLVML | \n",
- " KKKQYTSIHH | \n",
- "
\n",
- " \n",
- " | 2 | \n",
- " P14925 | \n",
- " Pam | \n",
- " MAGRARSGLLLLLLG...EEEYSAPLPKPAPSS | \n",
- " 1 | \n",
- " 868 | \n",
- " 890 | \n",
- " KLSTEPGSGV | \n",
- " SVVLITTLLVIPVLVLLAIVMFI | \n",
- " RWKKSRAFGD | \n",
- "
\n",
- " \n",
- " | 3 | \n",
- " P70180 | \n",
- " Npr3 | \n",
- " MRSLLLFTFSACVLL...RELREDSIRSHFSVA | \n",
- " 1 | \n",
- " 477 | \n",
- " 499 | \n",
- " PCKSSGGLEE | \n",
- " SAVTGIVVGALLGAGLLMAFYFF | \n",
- " RKKYRITIER | \n",
- "
\n",
- " \n",
- " | 4 | \n",
- " P12821 | \n",
- " ACE | \n",
- " MGAASGRRGPGLLLP...SHGPQFGSEVELRHS | \n",
- " 2 | \n",
- " 1257 | \n",
- " 1276 | \n",
- " GLDLDAQQAR | \n",
- " VGQWLLLFLGIALLVATLGL | \n",
- " SQRLFSIRHR | \n",
- "
\n",
- " \n",
- " | 5 | \n",
- " P36896 | \n",
- " ACVR1B | \n",
- " MAESAGASSFFPLVV...KKTLSQLSVQEDVKI | \n",
- " 2 | \n",
- " 127 | \n",
- " 149 | \n",
- " EHPSMWGPVE | \n",
- " LVGIIAGPVFLLFLIIIIVFLVI | \n",
- " NYHQRVYHNR | \n",
- "
\n",
- " \n",
- " | 6 | \n",
- " Q8NER5 | \n",
- " ACVR1C | \n",
- " MTRALCSALRQALLL...KKTISQLCVKEDCKA | \n",
- " 2 | \n",
- " 114 | \n",
- " 136 | \n",
- " PNAPKLGPME | \n",
- " LAIIITVPVCLLSIAAMLTVWAC | \n",
- " QGRQCSYRKK | \n",
+ " 1 | \n",
+ " P05067 | \n",
+ " APP | \n",
+ " MLPGLALLLLAAWTA...GYENPTYKFFEQMQN | \n",
+ " 1 | \n",
+ " 701 | \n",
+ " 723 | \n",
+ " FAEDVGSNKG | \n",
+ " AIIGLMVGGVVIATVIVITLVML | \n",
+ " KKKQYTSIHH | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " P14925 | \n",
+ " Pam | \n",
+ " MAGRARSGLLLLLLG...EEEYSAPLPKPAPSS | \n",
+ " 1 | \n",
+ " 868 | \n",
+ " 890 | \n",
+ " KLSTEPGSGV | \n",
+ " SVVLITTLLVIPVLVLLAIVMFI | \n",
+ " RWKKSRAFGD | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " P70180 | \n",
+ " Npr3 | \n",
+ " MRSLLLFTFSACVLL...RELREDSIRSHFSVA | \n",
+ " 1 | \n",
+ " 477 | \n",
+ " 499 | \n",
+ " PCKSSGGLEE | \n",
+ " SAVTGIVVGALLGAGLLMAFYFF | \n",
+ " RKKYRITIER | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " P12821 | \n",
+ " ACE | \n",
+ " MGAASGRRGPGLLLP...SHGPQFGSEVELRHS | \n",
+ " 2 | \n",
+ " 1257 | \n",
+ " 1276 | \n",
+ " GLDLDAQQAR | \n",
+ " VGQWLLLFLGIALLVATLGL | \n",
+ " SQRLFSIRHR | \n",
+ "
\n",
+ " \n",
+ " | 5 | \n",
+ " P36896 | \n",
+ " ACVR1B | \n",
+ " MAESAGASSFFPLVV...KKTLSQLSVQEDVKI | \n",
+ " 2 | \n",
+ " 127 | \n",
+ " 149 | \n",
+ " EHPSMWGPVE | \n",
+ " LVGIIAGPVFLLFLIIIIVFLVI | \n",
+ " NYHQRVYHNR | \n",
+ "
\n",
+ " \n",
+ " | 6 | \n",
+ " Q8NER5 | \n",
+ " ACVR1C | \n",
+ " MTRALCSALRQALLL...KKTISQLCVKEDCKA | \n",
+ " 2 | \n",
+ " 114 | \n",
+ " 136 | \n",
+ " PNAPKLGPME | \n",
+ " LAIIITVPVCLLSIAAMLTVWAC | \n",
+ " QGRQCSYRKK | \n",
"
\n",
" \n",
"
\n"
@@ -904,7 +904,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.14.0"
+ "version": "3.13.11"
}
},
"nbformat": 4,
diff --git a/tests/unit/data_handling_tests/test_load_dataset.py b/tests/unit/data_handling_tests/test_load_dataset.py
index 641e5df8..d920d9fe 100644
--- a/tests/unit/data_handling_tests/test_load_dataset.py
+++ b/tests/unit/data_handling_tests/test_load_dataset.py
@@ -1,6 +1,12 @@
"""
This is a script for testing the aa.load_dataset function.
"""
+import hashlib
+import os
+import subprocess
+import sys
+from pathlib import Path
+
from hypothesis import given, example, settings
import hypothesis.strategies as some
import numpy as np
@@ -382,3 +388,157 @@ def test_resolves_sample_by_gene_end_to_end(self):
sf = aa.SequenceFeature(verbose=False)
df_parts = sf.get_df_parts(df_seq=df)
assert resolve_sample_entry(df_seq=df, df_parts=df_parts, sample="APP") == "P05067"
+
+
+# Int-labelled datasets, whose class blocks are concatenated in a process-independent
+# order, so a seeded frame can be compared byte-for-byte across processes.
+LIST_SEEDED_NAMES = ["DOM_GSEC", "SEQ_CAPSID", "SEQ_LOCATION"]
+
+
+def _digest(df_seq):
+ """Byte-level digest of a frame (values, index, shape, and dtypes)."""
+ meta = f"{df_seq.shape}|{[f'{c}:{t}' for c, t in df_seq.dtypes.astype(str).items()]}"
+ return hashlib.sha256(df_seq.to_csv(index=True).encode("utf-8") + meta.encode("utf-8")).hexdigest()
+
+
+class TestLoadDatasetRandomState:
+ """Test the 'random_state' parameter of load_dataset (issue #582)."""
+
+ # Reproducibility under an explicit seed
+ def test_same_seed_returns_identical_frame(self):
+ """The same seed reproduces the sample exactly."""
+ df_1 = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=42)
+ df_2 = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=42)
+ pd.testing.assert_frame_equal(df_1, df_2)
+
+ @pytest.mark.parametrize("name", LIST_SEEDED_NAMES)
+ def test_same_seed_identical_for_every_level(self, name):
+ """A fixed seed is reproducible on the sequence and domain levels alike."""
+ df_1 = aa.load_dataset(name=name, n=3, random=True, random_state=0)
+ df_2 = aa.load_dataset(name=name, n=3, random=True, random_state=0)
+ assert _digest(df_1) == _digest(df_2)
+
+ @settings(max_examples=10, deadline=None)
+ @given(random_state=some.integers(min_value=0, max_value=10000))
+ def test_any_seed_is_reproducible(self, random_state):
+ """Any valid seed reproduces its own sample (property-based)."""
+ kwargs = dict(name="SEQ_CAPSID", n=3, random=True, random_state=random_state)
+ pd.testing.assert_frame_equal(aa.load_dataset(**kwargs), aa.load_dataset(**kwargs))
+
+ def test_seed_zero_is_honoured(self):
+ """random_state=0 is a valid seed, not treated as 'unset'."""
+ df_1 = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=0)
+ df_2 = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=0)
+ pd.testing.assert_frame_equal(df_1, df_2)
+
+ def test_different_seeds_give_different_samples(self):
+ """Different seeds draw different samples (not a constant selection)."""
+ digests = {_digest(aa.load_dataset(name="SEQ_LOCATION", n=5, random=True, random_state=rs))
+ for rs in [0, 1, 2, 3, 42]}
+ assert len(digests) > 1
+
+ def test_seeded_sample_is_balanced(self):
+ """A seeded random selection still returns n entries per class."""
+ df_seq = aa.load_dataset(name="SEQ_LOCATION", n=5, random=True, random_state=42)
+ assert len(df_seq) == 5 * 2
+ assert set(df_seq[ut.COL_LABEL]) == {0, 1}
+
+ def test_columns_and_dtypes_unchanged_by_seed(self):
+ """Seeding changes which rows are drawn, never the columns or their dtypes."""
+ df_unseeded = aa.load_dataset(name="DOM_GSEC", n=5, random=True)
+ df_seeded = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=42)
+ assert list(df_unseeded.columns) == list(df_seeded.columns)
+ assert df_unseeded.dtypes.astype(str).to_dict() == df_seeded.dtypes.astype(str).to_dict()
+
+ def test_reproducible_across_processes(self):
+ """A seeded frame is identical in a fresh interpreter (KPI: across processes)."""
+ df_seq = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=42)
+ code = (
+ "import hashlib, warnings; warnings.simplefilter('ignore');"
+ "import aaanalysis as aa;"
+ "df = aa.load_dataset(name='DOM_GSEC', n=5, random=True, random_state=42);"
+ "meta = f\"{df.shape}|{[f'{c}:{t}' for c, t in df.dtypes.astype(str).items()]}\";"
+ "print(hashlib.sha256(df.to_csv(index=True).encode('utf-8') + meta.encode('utf-8')).hexdigest())"
+ )
+ env = dict(os.environ, MPLBACKEND="Agg",
+ PYTHONPATH=str(Path(aa.__file__).resolve().parents[1]))
+ out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, env=env)
+ assert out.returncode == 0, out.stderr
+ assert out.stdout.strip().splitlines()[-1] == _digest(df_seq)
+
+ # Global option override
+ def test_option_random_state_is_honoured(self):
+ """options['random_state'] seeds the sampling with no explicit argument."""
+ aa.options["random_state"] = 42
+ df_1 = aa.load_dataset(name="DOM_GSEC", n=5, random=True)
+ df_2 = aa.load_dataset(name="DOM_GSEC", n=5, random=True)
+ pd.testing.assert_frame_equal(df_1, df_2)
+
+ def test_option_random_state_matches_explicit_seed(self):
+ """The option resolves to the same sample as passing that seed directly."""
+ df_explicit = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=42)
+ aa.options["random_state"] = 42
+ df_option = aa.load_dataset(name="DOM_GSEC", n=5, random=True)
+ pd.testing.assert_frame_equal(df_explicit, df_option)
+
+ def test_option_random_state_overrides_argument(self):
+ """options['random_state'] wins over the per-call argument (documented contract)."""
+ df_explicit = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=42)
+ aa.options["random_state"] = 42
+ df_option = aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=7)
+ pd.testing.assert_frame_equal(df_explicit, df_option)
+
+ # random=False is unaffected
+ @pytest.mark.parametrize("name", LIST_SEEDED_NAMES + ["AA_CASPASE3", "DOM_GSEC_PU"])
+ def test_random_false_ignores_seed(self, name):
+ """random=False returns the head-of-class selection, seed or no seed."""
+ df_plain = aa.load_dataset(name=name, n=3)
+ assert _digest(aa.load_dataset(name=name, n=3, random_state=42)) == _digest(df_plain)
+ assert _digest(aa.load_dataset(name=name, n=3, random_state=7)) == _digest(df_plain)
+
+ def test_random_false_ignores_option(self):
+ """A global random_state does not touch the deterministic selection either."""
+ df_plain = aa.load_dataset(name="DOM_GSEC", n=5)
+ aa.options["random_state"] = 42
+ pd.testing.assert_frame_equal(aa.load_dataset(name="DOM_GSEC", n=5), df_plain)
+
+ def test_full_dataset_ignores_seed(self):
+ """Without 'n' there is no sampling, so the seed cannot change the output."""
+ df_plain = aa.load_dataset(name="SEQ_CAPSID")
+ assert _digest(aa.load_dataset(name="SEQ_CAPSID", random_state=42)) == _digest(df_plain)
+
+ # Invalid input
+ def test_invalid_random_state_negative(self):
+ """A negative seed is rejected."""
+ with pytest.raises(ValueError):
+ aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=-1)
+
+ @settings(max_examples=10, deadline=None)
+ @given(random_state=some.integers(max_value=-1))
+ def test_invalid_random_state_negative_property(self, random_state):
+ """Every negative seed is rejected (property-based)."""
+ with pytest.raises(ValueError):
+ aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=random_state)
+
+ def test_invalid_random_state_float(self):
+ """A non-integer seed is rejected."""
+ with pytest.raises(ValueError):
+ aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state=1.5)
+
+ def test_invalid_random_state_str(self):
+ """A string seed is rejected with a message naming the parameter."""
+ with pytest.raises(ValueError, match="random_state"):
+ aa.load_dataset(name="DOM_GSEC", n=5, random=True, random_state="42")
+
+ def test_invalid_random_state_rejected_without_random(self):
+ """The seed is validated even when random=False (fail fast, not silently)."""
+ with pytest.raises(ValueError):
+ aa.load_dataset(name="DOM_GSEC", n=5, random_state=-5)
+
+ def test_option_random_state_read_per_call(self):
+ """The option is resolved on every call, so changing it changes the next sample."""
+ aa.options["random_state"] = 42
+ df_1 = aa.load_dataset(name="SEQ_LOCATION", n=5, random=True)
+ aa.options["random_state"] = 7
+ df_2 = aa.load_dataset(name="SEQ_LOCATION", n=5, random=True)
+ assert _digest(df_1) != _digest(df_2)