P4: Prediction levels

This protocol covers the three prediction tasks and levels (residue, domain, protein) and how each one shapes df_parts. Before you can ask CPP which physicochemical features separate your groups, you first have to decide what one example is. Is it a single residue (a cleavage site, a post-translational modification), one sub-region of a protein (a transmembrane domain), or a whole chain (a short peptide)? That choice is the prediction level, and in AAanalysis it is written directly into the load_dataset() name prefix: AA_* (residue), DOM_* (domain), SEQ_* (protein). The level is not a different algorithm: all three feed the same run(). What changes is the unit of comparison, and therefore how you build df_parts.

Choosing the prediction level fixes the unit of comparison (a window at residue level, a part set at domain level, or the whole chain at protein level), and that, in turn, fixes how the parts are named and which kind of feature you can read out. Pick the level from your biological question first; the df_parts construction then follows mechanically.

Like Protocol 1, this is a determinant-discovery / prediction setup: you contrast a test group (label=1) against a reference group (label=0) and read out the signature, the set of position-resolved physicochemical features that separates them. Getting df_parts right is the single most common bottleneck for new users, so this protocol shows the correct construction side by side for all three levels.

When to use it. Use this protocol when you have a labelled two-group task and are unsure whether it is residue, domain, or protein level (or when you are building df_parts for the first time and want a correct template per level). The level follows the biological question:

  • Residue level (AA_*): “What distinguishes a position from its neighbours?” The unit of comparison is a fixed-length window anchored at a residue or a scissile bond (e.g. protease cleavage, PTM sites).

  • Domain level (DOM_*): “What distinguishes one sub-region of a protein from the same region in another protein?” The unit is a part set derived from tmd_start / tmd_stop (e.g. gamma-secretase substrates compared over their transmembrane domain), CPP’s native ground.

  • Protein level (SEQ_*): “What distinguishes one whole chain from another?” The unit is the whole sequence as a single part (e.g. aggregation-prone vs. soluble peptides).

When not to use it. This protocol is about choosing a level and wiring df_parts: it is not where you sample a reference group. If your residue-level task has no ready-made negatives (true for most protease / PTM problems), you must first construct non-site windows; see the Next step. And if you have only one group, or want to redesign a sequence rather than contrast two sets, you are outside the determinant-discovery setup this protocol assumes.

Input. A df_seq whose format matches the level (see get_df_parts()), plus a binary label column (1 = test group, 0 = reference group). Remember that load_dataset(name=..., n=N) returns 2N rows (N per group); read group sizes from the label column, never from len(df_seq). The level is encoded in the dataset name prefix (AA_* / DOM_* / SEQ_*; see the data-loader tutorial for the full naming scheme).

Level

Dataset

df_seq format

Biological contrast (test=1 vs reference=0)

residue

AA_CASPASE3

s equence-based (9-residue windows around the scissile bond; even window = be tween-residues)

cleaved (1) vs. not cleaved (0)

domain

DOM_GSEC

p osition-based (tmd_start / tmd_stop, 1-based inclusive)

gamma-secretase substrate (1) vs . non-substrate (0)

protein

SEQ_AMYLO

s equence-based (short full peptides)

ag gregation-prone (1) vs. soluble (0)

These tables come from upstream. Domain- and protein-level tables are loaded directly with load_dataset() (or your own annotation), while residue-level windows are produced by the Construct sets & sampling protocol (AAWindowSampler), which is also where you build the reference group when no negatives exist.

import aaanalysis as aa

aa.options["verbose"] = False
aa.options["random_state"] = 42

sf = aa.SequenceFeature()

Run. The pipeline is identical at every level (get_df_parts -> run() -> TreeModel ranking), and only df_parts (and the matching split_kws) differ. We build the three df_parts first, then run CPP on each.

Residue level: a window as a single part. The 9-residue window is the unit of comparison, so we map the whole window onto one part. We set jmd_n_len=0 / jmd_c_len=0 so no flanking is split off, and select the single neutral part tmd, here just a placeholder name for “the window”, not a transmembrane domain. (At the residue level the principled naming follows the Schechter-Berger ... P2 . P1 | P1' . P2' ... convention around the scissile bond; today parts are chosen from the predefined family, so we keep the neutral tmd.)

# Every loader option by name; all but `name` and `n` sit at their default here.
df_res = aa.load_dataset(name="AA_CASPASE3", n=15,
                         random=False,                 # True would draw a different n per class on every call
                         non_canonical_aa="remove",    # "keep" retains X / U / B; "gap" replaces them with "-"
                         min_len=None, max_len=None,   # a sequence-length window; None keeps every entry
                         aa_window_size=9,             # the window width the AA_* datasets are cut to
                         verbose=False)
labels_res = df_res["label"].to_list()

df_parts_res = sf.get_df_parts(df_seq=df_res, list_parts=["tmd"], all_parts=False,
                               jmd_n_len=0, jmd_c_len=0,   # no flanking: the window is the whole part
                               tmd_len=None,               # only used for the anchor-based (sequence + pos) format
                               remove_entries_with_gaps=False, replace_non_canonical_aa=False)
aa.display_df(df=df_parts_res, n_rows=5, show_shape=True)
DataFrame shape: (30, 1)
  tmd
entry  
CASPASE3_1_pos126 QTLRDSMLK
CASPASE3_1_pos127 TLRDSMLKY
CASPASE3_2_pos116 VDETDSGAG
CASPASE3_2_pos117 DETDSGAGL
CASPASE3_2_pos223 VDAVDTGIS

Domain level: named parts from ``tmd_start`` / ``tmd_stop``. The position-based format lets get_df_parts slice each sequence into the named parts jmd_n / tmd / jmd_c (here the TMD is the domain of interest). jmd_n_len / jmd_c_len set how many flanking residues frame the domain.

df_dom = aa.load_dataset(name="DOM_GSEC", n=15)
labels_dom = df_dom["label"].to_list()

df_parts_dom = sf.get_df_parts(df_seq=df_dom, list_parts=["jmd_n", "tmd", "jmd_c"], all_parts=False,
                               jmd_n_len=10, jmd_c_len=10,   # residues framing the domain on each side
                               tmd_len=None,
                               remove_entries_with_gaps=False, replace_non_canonical_aa=False)
aa.display_df(df=df_parts_dom, n_rows=5, show_shape=True)
DataFrame shape: (30, 3)
  jmd_n tmd jmd_c
entry      
Q14802 NSPFYYDWHS LQVGGLICAGVLCAMGIIIVMSA KCKCKFGQKS
Q86UE4 LGLEPKRYPG WVILVGTGALGLLLLFLLGYGWA AACAGARKKR
Q969W9 FQSMEITELE FVQIIIIVVVMMVMVVVITCLLS HYKLSARSFI
P53801 RWGVCWVNFE ALIITMSVVGGTLLLGIAICCCC CCRRKRSRKP
Q8IUW5 NDTGNGHPEY IAYALVPVFFIMGLFGVLICHLL KKKGYRCTTE

Protein level: the whole chain as a single part. For whole-chain prediction the entire sequence is the part. As at the residue level we set jmd_n_len=0 / jmd_c_len=0 and select the single neutral part tmd, but here it spans the full peptide, not a window.

df_prot = aa.load_dataset(name="SEQ_AMYLO", n=15)
labels_prot = df_prot["label"].to_list()

df_parts_prot = sf.get_df_parts(df_seq=df_prot, list_parts=["tmd"], all_parts=False,
                                jmd_n_len=0, jmd_c_len=0,   # the single part spans the full peptide
                                tmd_len=None,
                                remove_entries_with_gaps=False, replace_non_canonical_aa=False)
aa.display_df(df=df_parts_prot, n_rows=5, show_shape=True)
DataFrame shape: (30, 1)
  tmd
entry  
AMYLO_1 AAAQAA
AMYLO_2 QSSYSS
AMYLO_3 QSYGQQ
AMYLO_4 QSYNPP
AMYLO_5 QSYSGY

Run CPP per level. split_kws must fit the part lengths (segments/patterns cannot exceed the shortest part). For the short residue window we use small positional splits; for the domain we size splits to the 10-residue JMDs; for the protein we use a single compositional Segment(1,1) whole-part average (position-agnostic, composition-like).

Notice how the only thing that changes between the three runs is df_parts (and the split_kws sized to it): the run() -> TreeModel half is copy-paste identical at every level. To keep the focus on the figure we show only the domain-level df_feat here, and visualise that same signature as a feature map in the Output section just below.

# Residue level: positional splits over the short window
split_kws_res = sf.get_split_kws(split_types=["Segment", "Pattern"], n_split_min=1,
                                 n_split_max=4, steps_pattern=[1, 2], n_min=2, n_max=3, len_max=9,
                                 steps_periodicpattern=None,   # only used by the PeriodicPattern split type
                                 strategy=None)                # "compositional" / "positional" presets
cpp = aa.CPP(df_parts=df_parts_res, split_kws=split_kws_res)
df_feat_res = cpp.run(labels=labels_res, n_filter=20, n_jobs=1)  # n_jobs=1 avoids a spawn issue on Python 3.14/macOS

X_res = sf.feature_matrix(features=df_feat_res["feature"], df_parts=df_parts_res)
tm = aa.TreeModel().fit(X_res, labels=labels_res)
df_feat_res = tm.add_feat_importance(df_feat=df_feat_res)
# Domain level: splits sized to the named parts (positional + compositional).
# This is the one call listing every public parameter by name, grouped, as a reference
# for the two levels above and below; all unnamed defaults are shown at their default value.
split_kws_dom = sf.get_split_kws(split_types=None,            # None uses Segment + Pattern + PeriodicPattern
                                 n_split_min=1, n_split_max=10, steps_pattern=[3, 4],
                                 n_min=2, n_max=4, len_max=10, steps_periodicpattern=None, strategy=None)
cpp = aa.CPP(df_parts=df_parts_dom, split_kws=split_kws_dom,
             df_scales=None, df_cat=None,      # None loads the bundled AAontology scales and categories
             accept_gaps=False, verbose=False, random_state=42,
             bootstrap=False, bootstrap_kws=None)   # True would add a feature-stability column
df_feat_dom = cpp.run(labels=labels_dom, label_test=1, label_ref=0,
                      # filtering: how many features survive, and how redundant they may be
                      n_filter=20, n_pre_filter=None, pct_pre_filter=5,
                      max_std_test=0.2, max_overlap=0.5, max_cor=0.5,
                      check_cat=True, parametric=False, redundancy="legacy",
                      # part geometry the feature positions are mapped onto
                      start=1, tmd_len=20, jmd_n_len=10, jmd_c_len=10,
                      # execution (n_jobs=1 avoids a spawn issue on Python 3.14/macOS)
                      n_jobs=1, vectorized=True, n_batches=None, n_sample_batches=None,
                      return_stats=False)

X_dom = sf.feature_matrix(features=df_feat_dom["feature"], df_parts=df_parts_dom,
                          df_scales=None, accept_gaps=False, n_jobs=1, batch=False,
                          df_seq=None, df_parts_kws=None)   # the df_seq route builds df_parts on the fly
tm = aa.TreeModel(list_model_classes=None, list_model_kwargs=None,   # None uses the default tree ensemble
                  is_preselected=None, verbose=False, random_state=42)
tm = tm.fit(X=X_dom, labels=labels_dom, n_rounds=5, use_rfe=False,
            n_cv=5, n_feat_min=25, n_feat_max=75, metric="accuracy", step=None)  # only used when use_rfe=True
df_feat_dom = tm.add_feat_importance(df_feat=df_feat_dom, drop=False, sort=False)
aa.display_df(df=df_feat_dom[["feature", "mean_dif", "abs_auc", "feat_importance"]], n_rows=5)
  feature mean_dif abs_auc feat_importance
1 TMD-Pattern(C,4,8)-ROBB760103 0.176000 0.467000 8.380000
2 TMD-Pattern(C,4,8)-MUNV940101 -0.134000 0.456000 6.685000
3 TMD-Pattern(C,4,8)-RACS820114 -0.191000 0.440000 6.238000
4 TMD-Pattern(C,4,8)-RACS820113 -0.262000 0.436000 4.690000
5 TMD-Pattern(C,4,8)-NAGK730103 -0.282000 0.431000 5.885000
# Protein level: compositional Segment(1,1) over the whole chain
split_kws_prot = sf.get_split_kws(split_types="Segment", n_split_min=1, n_split_max=1)
cpp = aa.CPP(df_parts=df_parts_prot, split_kws=split_kws_prot)
df_feat_prot = cpp.run(labels=labels_prot, n_filter=15, n_jobs=1)  # n_jobs=1 avoids a spawn issue on Python 3.14/macOS

X_prot = sf.feature_matrix(features=df_feat_prot["feature"], df_parts=df_parts_prot)
tm = aa.TreeModel().fit(X_prot, labels=labels_prot)
df_feat_prot = tm.add_feat_importance(df_feat=df_feat_prot)

Output. Each level returns its own df_feat signature, one row per selected feature, with the feature id reading as Part-Split-Scale. The downstream columns are identical across levels: mean_dif (signed test minus reference difference), abs_auc (separation strength, the adjusted AUC* in [0, 0.5] where 0 = no separation and 0.5 = perfect separation), and feat_importance (tree-based rank). The only thing that changes is how the feature id reads, because the parts differ; everything downstream stays the same.

A note on the numbers. We deliberately use tiny n=15-per-group fixtures so the protocol runs in seconds. On so few sequences it is trivial for top-ranked features to separate the two groups perfectly, so you will see abs_auc sitting at or near 0.5 (especially at the residue level, where the short windows overfit most easily). Treat these values as illustrative of what the column means, not as a real effect size; on a full dataset the same ranking shows a graded spread of abs_auc below 0.5.

The fastest way to see a signature is the feature map (the same plot as in Protocol 1). Below is the domain-level (gamma-secretase) signature: the y-axis groups features by AAontology category, the heatmap cells show mean_dif (red = higher in substrates, blue = higher in non-substrates), and the bars on the right show feat_importance.

import matplotlib.pyplot as plt

aa.plot_settings(font_scale=0.65, weight_bold=False)
cpp_plot = aa.CPPPlot()                 # feature_map is an INSTANCE method
cpp_plot.feature_map(df_feat=df_feat_dom, name_test="substrate", name_ref="non-substrate")
plt.tight_layout()
plt.show()
../_images/protocol4_prediction_tasks_1_output_15_0.png

The feature ids read differently per level because the parts differ:

Level

df_parts columns

Example feature

Reads as

residue

tmd (the window)

TMD-Pattern (C,5,6,8)-...

which positions in the window, which property

domain

jmd_n, tmd, jmd_c

TMD-Patte rn(C,4,8)-...

which sub-region of the domain, which property

protein

tmd (whole chain)

TMD-Seg ment(1,1)-...

a whole-chain composition property (no position)

Visualise any level’s signature with aa.CPPPlot().feature_map(df_feat=...) exactly as above: the plot needs the feat_importance column we added with add_feat_importance() (see P1: CPP signature for the full feature_map parameter set). The table states the difference between the levels; the figure below shows it, by drawing all three units of comparison on one and the same protein.

The three levels, shown on one protein. The table above states that the feature id reads differently per level; the figure below shows what actually changes. All three units of comparison are drawn on the same chain, BTC (entry P35070), a gamma-secretase substrate from DOM_GSEC that carries an annotated TMD, so the residue window, the named domain parts and the whole protein are all defined on it. The property is held fixed too: one AAontology hydrophobicity scale (ARGP820101) at every level. The top panel draws the unit of comparison (every fourteenth window only, for legibility); the bottom panel draws the value each level hands to CPP, which is the Segment(1,1) mean of that scale over its unit.

import numpy as np
import pandas as pd

# One protein, three levels. BTC (entry P35070) comes from the same DOM_GSEC table used above.
df_one = df_dom[df_dom["entry"] == "P35070"]
seq = df_one["sequence"].iloc[0]
tmd_start, tmd_stop = int(df_one["tmd_start"].iloc[0]), int(df_one["tmd_stop"].iloc[0])
win_len, jmd_len, scale_id = 9, 10, "ARGP820101"   # AAontology: Polarity / Hydrophobicity

df_scales = aa.load_scales(name="scales", just_aaindex=False, unclassified_out=False,
                           top60_n=None,             # a top60_n subset is only used for scale benchmarking
                           top_explain_n=None, top_explain_min_th=None)


def scale_mean(df_parts, parts):
    """Mean of one scale over each part: exactly the Segment(1,1) feature CPP builds."""
    features = [f"{p.upper()}-Segment(1,1)-{scale_id}" for p in parts]
    X = sf.feature_matrix(features=features, df_parts=df_parts, df_scales=df_scales,
                          accept_gaps=False, n_jobs=1, batch=False, df_seq=None, df_parts_kws=None)
    return np.asarray(X).ravel()


# Residue level: one 9-residue window per position, each window a unit of its own.
windows = [seq[i:i + win_len] for i in range(len(seq) - win_len + 1)]
df_parts_win = sf.get_df_parts_from_windows(dict_parts={"tmd": windows})
x_res = scale_mean(df_parts_win, ["tmd"])
pos_res = np.arange(len(windows)) + win_len // 2 + 1

# Domain level: the three named parts around the annotated TMD, one value each.
df_parts_one = sf.get_df_parts(df_seq=df_one, list_parts=["jmd_n", "tmd", "jmd_c"], all_parts=False,
                               jmd_n_len=jmd_len, jmd_c_len=jmd_len, tmd_len=None,
                               remove_entries_with_gaps=False, replace_non_canonical_aa=False)
x_dom = scale_mean(df_parts_one, ["jmd_n", "tmd", "jmd_c"])
spans_dom = [(tmd_start - jmd_len, tmd_start - 1), (tmd_start, tmd_stop), (tmd_stop + 1, tmd_stop + jmd_len)]

# Protein level: the whole chain as a single part, one value for the entire protein.
df_parts_chain = sf.get_df_parts(df_seq=df_one[["entry", "sequence"]], list_parts=["tmd"], all_parts=False,
                                 jmd_n_len=0, jmd_c_len=0, tmd_len=None,
                                 remove_entries_with_gaps=False, replace_non_canonical_aa=False)
x_prot = float(scale_mean(df_parts_chain, ["tmd"])[0])

aa.plot_settings(font_scale=0.9, weight_bold=False)
c_prot, c_dom, c_res = aa.plot_get_clist(n_colors=3)   # gray, blue, red
c_dom_light = "#c5d2e8"

fig, axes = plt.subplots(2, 1, figsize=(10, 5.6), sharex=True, gridspec_kw={"height_ratios": [1.1, 1.6]})

# Panel A: the unit of comparison at each level, on the same chain.
ax = axes[0]
ax.broken_barh([(1, len(seq))], (-0.22, 0.44), color=c_prot, zorder=2)
ax.broken_barh([(1, len(seq))], (0.93, 0.14), color="0.9", zorder=1)
ax.broken_barh([(s, e - s + 1) for s, e in [spans_dom[0], spans_dom[2]]], (0.78, 0.44), color=c_dom_light, zorder=2)
ax.broken_barh([(spans_dom[1][0], spans_dom[1][1] - spans_dom[1][0] + 1)], (0.78, 0.44), color=c_dom, zorder=2)
ax.broken_barh([(1, len(seq))], (1.93, 0.14), color="0.9", zorder=1)
ax.broken_barh([(p - win_len // 2, win_len) for p in pos_res[::14]], (1.78, 0.44),
               color=c_res, edgecolor="white", linewidth=0.8, zorder=2)
ax.set_yticks([2, 1, 0])
ax.set_yticklabels([f"Residue\n({len(windows)} windows)", "Domain\n(3 parts)", "Protein\n(1 chain)"],
                   size=aa.plot_gcfs() - 2)
ax.set_ylim(-0.8, 2.8)
ax.set_title("The unit of comparison on one protein (BTC, a gamma-secretase substrate)", size=aa.plot_gcfs())
ax.spines[["top", "right", "left"]].set_visible(False)
ax.tick_params(axis="y", length=0)

# Panel B: the same scale, aggregated by each level's unit.
ax = axes[1]
ax.axvspan(tmd_start, tmd_stop, color=c_dom, alpha=0.12, lw=0)
ax.plot(pos_res, x_res, color=c_res, lw=1.4, zorder=3)
for (start_pos, stop_pos), value in zip(spans_dom, x_dom):
    ax.hlines(value, start_pos, stop_pos, color=c_dom, lw=5, zorder=4)
ax.hlines(x_prot, 1, len(seq), color=c_prot, lw=3, zorder=2)
ax.set_xlim(0, len(seq) + 1)
ax.set_xlabel("Residue position")
ax.set_ylabel(f"Hydrophobicity\n({scale_id})")
ax.spines[["top", "right"]].set_visible(False)
aa.plot_legend(ax=ax, dict_color={f"Residue: {win_len}-residue window": c_res,
                                  "Domain: named part": c_dom,
                                  "Protein: whole chain": c_prot},
               loc="upper left", loc_out=True, y=-0.38, x=0, n_cols=3, fontsize=aa.plot_gcfs() - 2)
plt.tight_layout()
plt.show()
../_images/protocol4_prediction_tasks_2_output_18_0.png
# The same picture as numbers: what one unit covers, and how many of them this protein has.
df_levels = pd.DataFrame({
    "level": ["residue (AA_*)", "domain (DOM_*)", "protein (SEQ_*)"],
    "unit of comparison": [f"{win_len}-residue window", "named part", "whole chain"],
    "units on this protein": [len(windows), len(x_dom), len(df_parts_chain)],
    "hydrophobicity": [f"{x_res.min():.2f} to {x_res.max():.2f}",
                       ", ".join(f"{v:.2f}" for v in x_dom), f"{x_prot:.2f}"],
    "one value covers": [f"{win_len} residues", f"{jmd_len} to {tmd_stop - tmd_start + 1} residues",
                         f"{len(seq)} residues"],
})
aa.display_df(df=df_levels, n_rows=10, show_shape=True, char_limit=40)
DataFrame shape: (3, 5)
  level unit of comparison units on this protein hydrophobicity one value covers
1 residue (AA_*) 9-residue window 170 0.11 to 0.64 9 residues
2 domain (DOM_*) named part 3 0.33, 0.54, 0.37 10 to 23 residues
3 protein (SEQ_*) whole chain 1 0.35 178 residues

Notice how one protein and one property give three different read-outs. At residue level the scale stays resolved: the overlapping windows trace a profile, so a feature can say at which position the property is high, which is why residue signatures are positional. At domain level that profile collapses into three numbers, one per named part, and the TMD part sits far above both flanking JMDs: a feature can still say which sub-region, but no longer which residue. At protein level everything collapses into a single number for the chain, which lands below the TMD it contains, because the hydrophobic domain is averaged together with the soluble rest of the protein: position is gone, so protein-level signatures can only be compositional. Choosing a level is choosing how much of that resolution you keep, and the table puts the same three read-outs side by side as numbers.

How to interpret. Read each output column as follows:

Output

Non-expert reading

abs_auc (closer to 0.5)

stronger group-separating property; abs_auc is the adjusted AUC* in [0, 0.5], with 0 = no separation and 0.5 = perfect separation

positive mean_dif

property is higher in the test group (label=1)

negative mean_dif

property is higher in the reference group (label=0)

a Segment(1,1) feature (protein level)

a compositional signal (it does not depend on position)

a Pattern(...) / multi-segment feature (residue/domain)

a positional signal (it depends on where in the part)

The level constrains the kind of feature you can read, which is exactly what the three-level figure above makes visible: the residue profile keeps position, the three domain parts keep sub-region, and the single whole-chain value keeps neither.

  • Residue signatures are purely positional: e.g. CASPASE3 cleavage depends on which flanking position (P2, P3, P4 …) carries which property around the scissile bond.

  • Protein signatures are purely compositional: amyloid aggregation depends on overall composition, not position, so only Segment(1,1) features appear.

  • Domain signatures mix both: the gamma-secretase TMD shows positional sub-region effects and whole-part composition.

Key takeaways

  • The prediction level you pick fixes the unit of comparison and how parts are named: it is a modelling decision, not an algorithm switch; all three levels call the same run().

  • The level constrains the kind of signature: residue is purely positional, protein is purely compositional, domain mixes both. The figure shows why, on one protein: the same scale keeps its position profile over windows, collapses to three part means over the domain, and collapses to a single number over the chain.

  • Always read the signature (coherent blocks of related features), never a single cell, and on tiny fixtures read abs_auc as illustrative, not as a real effect size.

Common mistakes.

  • Calling ``CPP(df_seq=…)``: CPP takes df_parts; always build them with get_df_parts() first (true at every level).

  • Forgetting that “part” means different things per level: a window (residue), a named sub-region (domain), or the whole chain (protein). Name and interpret it accordingly.

  • Using positional splits at the protein level: whole-chain composition is best captured by the compositional Segment(1,1); multi-segment / Pattern splits over-fragment a position-agnostic signal.

  • Letting ``split_kws`` exceed the shortest part: segments and patterns cannot be longer than the smallest part (e.g. a 10-residue JMD or a 9-residue window), or CPP raises a “too short sequences” error. Size n_split_max / len_max to the parts.

  • Confusing residue with domain: both can use a single tmd part, but residue windows are short, fixed-length, and anchored at a site, while domain parts come from tmd_start/tmd_stop and carry flanking JMDs (jmd_n_len/jmd_c_len).

Next step. Continue with P5: Engineer features to turn the chosen prediction level and its df_parts into an engineered CPP feature set.