Skip to content

Support AA abundance plot - #25

Open
mzueva wants to merge 1 commit into
mainfrom
mzueva/MILAB-5490-aa-plot
Open

Support AA abundance plot#25
mzueva wants to merge 1 commit into
mainfrom
mzueva/MILAB-5490-aa-plot

Conversation

@mzueva

@mzueva mzueva commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

This PR adds a new export to the redefine-clonotype block that allow to build per position AA abundance plot. 2 new columns are exported with the following specs:

New PColumns Spec
{
      "ref": {
        "__isRef": true,
        "blockId": "",
        "name": "cdr3AaPositions.count"
      },
      "obj": {
        "annotations": {
          "pl7.app/label": "AA Count by position",
          "pl7.app/table/visibility": "hidden",
          "pl7.app/trace": ""
        },
        "axesSpec": [
          {
            "annotations": {
              "pl7.app/label": "Sample"
            },
            "domain": {
              "pl7.app/blockId": ""
            },
            "name": "pl7.app/sampleId",
            "type": "String"
          },
          {
            "annotations": {
              "pl7.app/label": "Clonotype ID",
              "pl7.app/segmentedBy": "[\"pl7.app/vdj/clonotypingRunId\"]",
              "pl7.app/table/fontFamily": "monospace",
              "pl7.app/table/orderPriority": "110000",
              "pl7.app/table/visibility": "default"
            },
            "domain": {
              "pl7.app/redefined-by": "",
              "pl7.app/vdj/chain": "IGHeavy",
              "pl7.app/vdj/clonotypeKey/structure": "[[\"pl7.app/vdj/sequence\",[\"pl7.app/alphabet\",\"aminoacid\"],[\"pl7.app/vdj/chain\",\"IGHeavy\"],[\"pl7.app/vdj/feature\",\"VDJRegionInFrame\"]]]",
              "pl7.app/vdj/clonotypingRunId": ""
            },
            "name": "pl7.app/vdj/clonotypeKey",
            "type": "String"
          },
          {
            "annotations": {
              "pl7.app/label": "Chain"
            },
            "name": "pl7.app/vdj/chain",
            "type": "String"
          },
          {
            "annotations": {
              "pl7.app/label": "IMGT CDR3 position"
            },
            "domain": {
              "pl7.app/vdj/feature": "CDR3",
              "pl7.app/vdj/numberingSchema": "imgt"
            },
            "name": "pl7.app/vdj/numberingPosition",
            "type": "String"
          }
        ],
        "kind": "PColumn",
        "name": "pl7.app/count",
        "valueType": "Long"
      }
    },
    {
      "ref": {
        "__isRef": true,
        "blockId": "",
        "name": "cdr3AaPositions.aminoacid"
      },
      "obj": {
        "annotations": {
          "pl7.app/discreteFilters": "true",
          "pl7.app/label": "IMGT CDR3 amino acid",
          "pl7.app/table/visibility": "hidden",
          "pl7.app/trace": ""
        },
        "axesSpec": [
          {
            "annotations": {
              "pl7.app/label": "Sample"
            },
            "domain": {
              "pl7.app/blockId": ""
            },
            "name": "pl7.app/sampleId",
            "type": "String"
          },
          {
            "annotations": {
              "pl7.app/label": "Clonotype ID",
              "pl7.app/segmentedBy": "[\"pl7.app/vdj/clonotypingRunId\"]",
              "pl7.app/table/fontFamily": "monospace",
              "pl7.app/table/orderPriority": "110000",
              "pl7.app/table/visibility": "default"
            },
            "domain": {
              "pl7.app/redefined-by": "",
              "pl7.app/vdj/chain": "IGHeavy",
              "pl7.app/vdj/clonotypeKey/structure": "[[\"pl7.app/vdj/sequence\",[\"pl7.app/alphabet\",\"aminoacid\"],[\"pl7.app/vdj/chain\",\"IGHeavy\"],[\"pl7.app/vdj/feature\",\"VDJRegionInFrame\"]]]",
              "pl7.app/vdj/clonotypingRunId": ""
            },
            "name": "pl7.app/vdj/clonotypeKey",
            "type": "String"
          },
          {
            "annotations": {
              "pl7.app/label": "Chain"
            },
            "name": "pl7.app/vdj/chain",
            "type": "String"
          },
          {
            "annotations": {
              "pl7.app/label": "IMGT CDR3 position"
            },
            "domain": {
              "pl7.app/vdj/feature": "CDR3",
              "pl7.app/vdj/numberingSchema": "imgt"
            },
            "name": "pl7.app/vdj/numberingPosition",
            "type": "String"
          }
        ],
        "domain": {
          "pl7.app/alphabet": "aminoacid"
        },
        "kind": "PColumn",
        "name": "pl7.app/aminoacid",
        "valueType": "String"
      }
    }

Export is added only if some of the numbering schemes is selected, column names vary depending on the selected numbering scheme, e.g. IMGT CDR3 position, Kabat CDR3 position, Chothia CDR3 position.

To view the plot using Graph Maker please use these settings: add "Stacked Bar Chart" and set up the data mapping:
image

Note 1: new export requires fix in clonotype browser, here is the PR: platforma-open/clonotype-browser#17

Note 2: plot is also added to cdr3-spectratype block in this PR platforma-open/cdr3-spectratype#37

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for per-position amino acid export to facilitate AA abundance plotting. The changes include adding a toggle in the UI, updating the block model, and modifying the anarci-numbering script and Tengo workflow to generate and process the position data. Feedback was provided regarding the Python implementation's memory efficiency and a data type mismatch in the count column, suggesting a refactor using the polars library.

Comment on lines +366 to +388
if args.positions_tsv:
chain_domain = {"H": "IGHeavy", "KL": "IGLight"}
pos_rows: List[List[str]] = []
unique_keys = list(dict.fromkeys(keys))
for key in unique_keys:
for chain in chains:
anarci_rows, pos_labels = anarci_by_chain[chain]
residues = anarci_rows.get(key) if anarci_rows and pos_labels else None
if residues is None:
continue
ranges = REGION_RANGES[args.scheme][chain]
cdr3_start, cdr3_end = ranges["CDR3"]
for pos_label, residue in zip(pos_labels, residues):
num = position_number(pos_label)
if num is None or num < cdr3_start or num > cdr3_end:
continue
residue = (residue or "").strip()
if residue in {"", "-", "."}:
continue
pos_rows.append([key, chain_domain[chain], pos_label, residue, "1"])
pl.DataFrame(
pos_rows, schema=["clonotypeKey", "chain", "position", "aminoacid", "count"], orient="row"
).write_csv(args.positions_tsv, separator="\t")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for generating the per-position CDR3 amino acid data builds a list of lists in memory, which can be inefficient and consume a large amount of memory for datasets with many clonotypes. Additionally, the count column is being written as a string "1", but the downstream workflow expects a numeric type (Long).

I suggest refactoring this section to use a more idiomatic polars approach. This will improve performance and memory efficiency by leveraging polars' lazy evaluation and optimized backend operations. The suggested change also corrects the data type of the count column.

    if args.positions_tsv:
        chain_domain = {"H": "IGHeavy", "KL": "IGLight"}
        all_pos_dfs = []
        for chain in chains:
            anarci_rows, pos_labels = anarci_by_chain[chain]
            if not anarci_rows or not pos_labels:
                continue

            ranges = REGION_RANGES[args.scheme][chain]
            cdr3_start, cdr3_end = ranges["CDR3"]

            df = pl.DataFrame(
                list(anarci_rows.items()),
                schema=["clonotypeKey", "residues"],
            )
            df = df.with_columns(pl.lit(pos_labels).alias("pos_labels"))
            df = df.explode(["residues", "pos_labels"])
            df = df.rename({"residues": "aminoacid", "pos_labels": "position"})

            df = df.with_columns(
                pl.col("position").str.extract(r"^(\d+)", 1).cast(pl.Int64).alias("pos_num")
            )
            df = df.filter(
                (pl.col("pos_num") >= cdr3_start)
                & (pl.col("pos_num") <= cdr3_end)
                & (~pl.col("aminoacid").str.strip().is_in(["", "-", "."]))
            )

            df = df.with_columns(
                pl.lit(chain_domain[chain]).alias("chain"),
                pl.lit(1, dtype=pl.Int64).alias("count"),
            )

            all_pos_dfs.append(df.select("clonotypeKey", "chain", "position", "aminoacid", "count"))

        if all_pos_dfs:
            final_df = pl.concat(all_pos_dfs)
            final_df.write_csv(args.positions_tsv, separator="\t")
        else:
            pl.DataFrame(
                schema={
                    "clonotypeKey": pl.Utf8,
                    "chain": pl.Utf8,
                    "position": pl.Utf8,
                    "aminoacid": pl.Utf8,
                    "count": pl.Int64,
                }
            ).write_csv(args.positions_tsv, separator="\t")

@mzueva
mzueva force-pushed the mzueva/MILAB-5490-aa-plot branch from 2c9b947 to 2e7cfc6 Compare April 14, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant