Skip to content

Add condition-specific mutation names to get_mutations_df #192

Description

@jaredgalloway

Add condition-specific mutation names to get_mutations_df

Summary

Add mutation_{condition} columns to Model.get_mutations_df() that re-express each mutation relative to the wildtype sequence of each condition. Currently, all mutations are named relative to the reference condition only, which makes downstream analyses difficult when users need to know what each mutation means in a non-reference condition's native coordinates.

Background

In multidms, all mutations are encoded relative to the reference condition's wildtype sequence. For example, if the reference condition has G at site 3 and condition b has P at site 3, then:

  • Mutation G3R in reference coordinates corresponds to P3R in condition b coordinates
  • Mutation G3P in reference coordinates is an identity (wildtype) for condition b

The current get_mutations_df() output uses reference-relative names exclusively. This is correct for the model internals, but users analyzing specific conditions need to map these back to the condition's native sequence — e.g., to compare predicted mutational effects with independently measured data expressed in that condition's coordinates, or to find Usher tree founder sequences for each homolog.

Motivated by: https://github.com/matsengrp/data-central/pull/84#issuecomment-3974093633

Proposed Approach

Add a mutation_{condition} column for each condition to the DataFrame returned by get_mutations_df(). The mapping uses the existing Data.site_map property, which already stores the wildtype amino acid at each site for every condition.

Key design decisions:

  • Per-condition columns in wide format — Keeps the existing wide-format output structure consistent (parallels beta_{condition}, shift_{condition}, predicted_func_score_{condition} columns).
  • Identity mutations are included (e.g., P3P) — When a reference mutation targets the wildtype residue of a non-reference condition, show the identity mutation rather than dropping the row. Users can filter these trivially.
  • Implemented at Model.get_mutations_df() level — The mutation-to-condition mapping logic belongs in the model output, not in Data, since it's a presentation concern.

User Interface / API

Programmatic API

model.fit()
mut_df = model.get_mutations_df()

# New columns alongside existing ones:
# mutation_a, mutation_b  (one per condition)
# Example row: mutation='G3R', mutation_a='G3R', mutation_b='P3R'
# Example row: mutation='G3P', mutation_a='G3P', mutation_b='P3P'

# Reference condition column is always identical to the index
assert (mut_df["mutation_a"] == mut_df.index).all()  # if 'a' is reference

# Filter out identity mutations for a specific condition
non_identity = mut_df[
    mut_df["mutation_b"].str[0] != mut_df["mutation_b"].str[-1]
]

Proposed Changes

1. Model.get_mutations_df() in multidms/model.py

File: multidms/model.py (existing)

Add mutation_{condition} columns after beta/shift columns and before predicted functional score columns. The mapping logic:

# For each condition, remap mutation names using site_map
site_map = self._data.site_map
for condition in self._data.conditions:
    def remap_mutation(mut_str):
        wt, site, aa = self._data._mutparser.parse_mut(mut_str)
        cond_wt = site_map.loc[site, condition]
        return f"{cond_wt}{site}{aa}"
    mutations_df[f"mutation_{condition}"] = [
        remap_mutation(m) for m in self._data.mutations
    ]

For reference-sequence conditions, mutation_{condition} will be identical to the index (since the wildtype is the same). For non-reference conditions, the wildtype character in the mutation string is replaced with the condition's actual wildtype at that site.

2. Column ordering

Place mutation_{condition} columns immediately after the index, before beta_{condition} columns:

mutation (index) | wts | sites | muts | times_seen_* |
mutation_a | mutation_b | beta_a | beta_b | shift_b |
predicted_func_score_a | predicted_func_score_b

Implementation Details

Mutation Remapping Algorithm

For a mutation string like G3R:

  1. Parse into (wt='G', site='3', mut='R') using Data._mutparser.parse_mut()
  2. Look up site_map.loc['3', condition] to get the condition's wildtype at that site
  3. Construct f"{cond_wt}{site}{mut}" → e.g., P3R if condition wildtype is P

Edge cases:

  • Reference condition: cond_wt == wt always, so output matches input
  • Identity mutation: When mut == cond_wt (e.g., G3P where condition has P at site 3), output is P3P. Included by design.
  • Sites with identical wildtype across conditions: Output matches input for those sites

Data Dependencies

The mapping relies only on:

  • Data.site_map — wildtype amino acids per site per condition (already computed during Data.__init__)
  • Data._mutparser.parse_mut() — existing mutation string parser

No new dependencies required.

Testing Strategy

  • Doctest in get_mutations_df: Add a doctest showing mutation_{condition} columns for a minimal two-condition example with at least one non-identical site.
  • Unit test edge cases:
    • Reference condition column equals the mutation index
    • Non-reference condition correctly remaps wildtype character
    • Identity mutations (e.g., P3P) appear when expected
    • All-identical-wildtype conditions produce identical mutation names
  • Integration: Verify that the existing tests/test_data.py and doctests continue to pass unchanged.

Documentation Updates

  • Update get_mutations_df docstring to document new columns
  • Add example in docstring showing the new columns

Dependencies

None — uses only existing Data properties.

Alternatives Considered

Alternative 1: Long-format output with a single conditional_mutation column

Would require restructuring get_mutations_df() from wide to long format.

Why not chosen: Would break existing downstream code that expects the wide format. The wide format is consistent with how beta_, shift_, and predicted_func_score_ columns are already structured.

Alternative 2: Implement as a standalone Data method

Add Data.mutations_for_condition(condition) that returns remapped mutation names.

Why not chosen: Users expect this information alongside the model output (betas, shifts, predictions). A separate method would require manual joining. However, a utility on Data could be added later if needed independently of the model.

Alternative 3: Exclude identity mutations (show empty string / NaN)

Why not chosen: User preference is to include them. This also avoids ambiguity about what an empty value means (unseen vs. identity).

Success Criteria

  • get_mutations_df() returns mutation_{condition} columns for every condition
  • Reference condition column values match the mutation index
  • Non-reference condition columns correctly use that condition's wildtype amino acid
  • Identity mutations like P3P are included
  • All existing tests and doctests pass
  • New doctest demonstrates the feature

Future Work

  • A standalone Data.remap_mutation(mutation, condition) utility for one-off lookups
  • Optional filtering of identity mutations via parameter
  • Per-condition CSV export method for external tool compatibility

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions