Skip to content

ProteinAnalysis.protein_scale crashes for window 1 and silently biases even windows #5340

Description

@ChenziqiAdam

ProteinAnalysis.protein_scale does not document an odd-window or minimum window restriction, but two valid-looking window choices seem to be mishandled:

  1. window=1 raises ZeroDivisionError while the natural one-residue profile is simply the selected scale value for each residue.
  2. An even window double-counts subsequence[window // 2]. The returned profile therefore depends on sequence direction even though all positional weights are described as symmetric about the window center.

Reproduction code:

from Bio.SeqUtils.ProtParam import ProteinAnalysis

scale = {aa: i + 1.0 for i, aa in enumerate("ACDEFGHIK")}

analysis = ProteinAnalysis("ACDEFGHIK")
analysis.protein_scale(scale, 1, edge=1.0)
# ZeroDivisionError: float division by zero

forward = analysis.protein_scale(scale, 4, edge=0.4)
reverse = ProteinAnalysis("KIHGFEDCA").protein_scale(
    {aa: scale[aa] for aa in scale}, 4, edge=0.4
)[::-1]

print(max(abs(a - b) for a, b in zip(forward, reverse)))
# 0.2941176470588238

Activity

  1. peterjc commented on Oct 2, 2026

    @peterjc
    Member

    I'm guessing this was flagged with automated tools, rather than a biological use case?

    Perhaps it would be simplest to document and impose limits on the window size.

  2. added theissue type on Oct 2, 2026
  3. ChenziqiAdam commented on Oct 11, 2026

    @ChenziqiAdam
    ContributorAuthor

    Sorry for the late reply. There is a real biological use case that hits it directly: the Hopp–Woods hydrophilicity scale ProtParamData.hw, is defined for a hexapeptide window, i.e. an even window of 6.

    from Bio.SeqUtils.ProtParam import ProteinAnalysis
    from Bio.SeqUtils import ProtParamData
    
    seq = "MKTAYIAKQRQISFVKSHFSRQDEEKLL"
    prof = ProteinAnalysis(seq).protein_scale(ProtParamData.hw, 6)
    plain = [sum(ProtParamData.hw[c] for c in seq[i:i+6]) / 6 for i in range(len(seq) - 5)]
    rev = ProteinAnalysis(seq[::-1]).protein_scale(ProtParamData.hw, 6)[::-1]
    
    print(max(abs(a - b) for a, b in zip(prof, plain)))  # 0.45
    print(max(abs(a - b) for a, b in zip(prof, rev)))    # 0.64

    With the default edge=1.0 the profile should be the plain 6-residue average, but the middle residue is counted twice. The first two values are -0.543 and -0.686, where the plain mean gives -0.550 and -0.417. Reversing the sequence also changes the profile.

    So for this scale, an even window gives a silently wrong result rather than an error.

  4. peterjc commented on Oct 12, 2026

    @peterjc
    Member

    That's a good use-case then, thanks @ChenziqiAdam. Does the proposal on #5363 work for you?

  5. ChenziqiAdam commented on Oct 13, 2026

    @ChenziqiAdam
    ContributorAuthor

    LGTM

  6. added a commit that references this issue on Nov 12, 2026
    14d1623
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions