Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/actions/setup_nox/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ inputs:
cache_number:
description: "A manually specified cache number. Useful for triggering new caches."
required: True
default: 0
default: "0"
poetry_version:
description: "Which poetry version to use."
required: True
Expand All @@ -20,6 +20,18 @@ runs:
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: "Install nox"
run: pipx install nox
shell: bash
Expand Down
12 changes: 5 additions & 7 deletions .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
with:
cache_number: ${{ env.CACHE_NUMBER }}
poetry_version: ${{ env.POETRY_VERSION }}
- name: Check for Dependency Vulnerabilities
- name: Lint code
run: nox -s lint

type:
Expand All @@ -60,7 +60,7 @@ jobs:
with:
cache_number: ${{ env.CACHE_NUMBER }}
poetry_version: ${{ env.POETRY_VERSION }}
- name: Check for Dependency Vulnerabilities
- name: Type check
run: nox -s type

security:
Expand Down Expand Up @@ -103,7 +103,7 @@ jobs:
run: nox -s tests
- name: Upload to codecov
if: matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
Expand All @@ -114,7 +114,7 @@ jobs:
release:
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/main' }}
needs: [lint, test, security]
needs: [lint, type, test, security]
outputs:
released: ${{ steps.release.outputs.released }}
version: ${{ steps.release.outputs.version }}
Expand All @@ -129,7 +129,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: '3.14'

- name: Install dependencies
run: |
Expand All @@ -149,8 +149,6 @@ jobs:
semantic-release version
poetry build
semantic-release publish
# Mark that a release was created
echo "released=true" >> $GITHUB_OUTPUT

- name: Upload distribution artifacts
uses: actions/upload-artifact@v4
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
# CHANGELOG


## v3.6.0 (2026-06-14)

### Bug Fixes

- **data_handling.py**: Fix deprecated pandas `groupby(axis=...)` API for pandas 2.0 compatibility

- **data_handling.py**: Replace `np.NAN` with `np.nan` for NumPy 2.0 compatibility

### Build System

- **jaccard.py**: Vendor `boolean-jaccard` module locally; removes the external dependency which was
unmaintained and capped at Python < 3.11

- **pyproject.toml**: Drop Python 3.8 and 3.9 support; add Python 3.11, 3.12, and 3.13 support

- **pyproject.toml**: Bump pandas to `^2.0` and numpy to `^2.0`

- **pyproject.toml**: Update dev tooling — flake8, pytest, coverage, black, Sphinx; replace `safety`
with `pip-audit`

### Continuous Integration

- **noxfile.py**: Update test matrix from Python 3.9/3.10 to 3.10–3.13; update default session
Python to 3.13

- **noxfile.py**: Replace `safety` with `pip-audit` in security session; update `poetry export`
flags from `--dev` / `--no-dev` to `--with dev` / `--only main` for Poetry v1.2+ compatibility

- **cicd.yaml**: Add Python 3.11–3.14 to CI setup; upgrade codecov action to v4; gate release job
on type check passing; fix CI step display names


## v3.5.3 (2025-11-09)

### Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
project = "LTA"
author = "Ryan B Patterson-Cross"
copyright = "2021, IMS-MRL Bioinformatics and Biostatistic Core"
version = "3.5.2"
version = "3.5.3"
extensions = [
"sphinx_rtd_theme",
"sphinx.ext.autodoc",
Expand Down
3 changes: 2 additions & 1 deletion lta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
The version number,
specified in the form 'major.minor.patch'
"""
__version__ = "3.5.2"

__version__ = "3.5.3"
1 change: 1 addition & 0 deletions lta/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
"""Provide entry point for CLI."""

import logging
from datetime import datetime
from pathlib import Path
Expand Down
1 change: 1 addition & 0 deletions lta/commands/run.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
"""A simple sample function for the CLI."""

import logging

import configargparse
Expand Down
52 changes: 36 additions & 16 deletions lta/helpers/data_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
and, thus, testable -
by removing it from the harder to test context of the object.
"""

import logging
from pathlib import Path
from typing import Any, List, Literal, Optional, Tuple
Expand Down Expand Up @@ -109,11 +110,17 @@ def not_zero(
pd.DataFrame
The processed data.
"""
df = (
(df == 0)
.groupby(axis=axis, level=[compartment, level])
.agg(lambda x: x.sum() <= (thresh * len(x)))
)
bool_df = df == 0
if axis == "index":
df = bool_df.groupby(level=[compartment, level]).agg(
lambda x: x.sum() <= (thresh * len(x))
)
else:
df = (
bool_df.T.groupby(level=[compartment, level])
.agg(lambda x: x.sum() <= (thresh * len(x)))
.T
)
if axis == "index":
df = df.loc[:, df.any(axis=axis)]
if axis == "columns":
Expand Down Expand Up @@ -168,7 +175,10 @@ def enfc(
if not order:
logging.debug("Order not passed. Defaulting to ('experimental', 'control')")
order = ("experimental", "control")
mean = df.groupby(axis=axis, level=level).mean()
if axis == "index":
mean = df.groupby(level=level).mean()
else:
mean = df.T.groupby(level=level).mean().T
logging.debug(f"Grouping/filtering on {axis}.")
# Replace inf (x/0) with NaN
# Replace 0 (0/x) with NaN
Expand All @@ -177,20 +187,30 @@ def enfc(
logfc = np.log10(
mean.loc[order[0], :]
.div(mean.loc[order[1], :])
.replace([np.inf, -np.inf, 0], np.NAN)
.replace([np.inf, -np.inf, 0], np.nan)
)
else:
logfc = np.log10(
mean.loc[:, order[0]]
.div(mean.loc[:, order[1]])
.replace([np.inf, -np.inf, 0], np.NAN)
.replace([np.inf, -np.inf, 0], np.nan)
)
if axis == "index":
error = (
df.groupby(level=level)
.std(numeric_only=True)
.pow(2)
.sum(axis=axis)
.div(2)
.pow(0.5)
)
else:
error = (
df.T.groupby(level=level)
.std(numeric_only=True)
.T.pow(2)
.sum(axis=axis)
.div(2)
.pow(0.5)
)
error = (
df.groupby(axis=axis, level=level)
.std(numeric_only=True)
.pow(2)
.sum(axis=axis)
.div(2)
.pow(0.5)
)
return logfc.div(error)
112 changes: 112 additions & 0 deletions lta/helpers/jaccard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
"""Jaccard similarities and their p-values.

Vendored from boolean-jaccard 0.1.1 (https://github.com/rbpatt2019/boolean-jaccard),
which is unmaintained and capped at Python <3.11.
Original: python port of the R jaccard package by N. Chung.
"""

import logging
from typing import Optional

import numpy as np
import pandas as pd

logger = logging.getLogger(__name__)


def similarity(
x: np.ndarray,
y: np.ndarray,
center: bool = False,
px: Optional[float] = None,
py: Optional[float] = None,
) -> float:
"""Calculate Jaccard similarity."""
if x.ndim != 1 or y.ndim != 1:
logging.error(
f"All vectors must be 1-d. ndims: {[x.ndim, y.ndim]}.", stack_info=True
)
raise IndexError
if x.shape != y.shape: # type: ignore[operator]
logging.error(
f"All vectors must have the same length. shape: {[x.shape, y.shape]}",
stack_info=True,
)
raise IndexError
if x.dtype != bool or y.dtype != bool:
logging.error(
f"All vectors must be boolean. dtypes: {[x.dtype, y.dtype]}",
stack_info=True,
)
raise TypeError

if px is None:
px = float(x.mean())
if py is None:
py = float(y.mean())

intersect = (x & y).sum()
union = x.sum() + y.sum() - intersect

denominator = px + py - (px * py)
if denominator == 0:
return np.nan

if union == 0:
j = (px * py) / denominator
else:
j = intersect / union

if center:
return j - ((px * py) / denominator)
return j


def distance(
x: np.ndarray, y: np.ndarray, px: Optional[float] = None, py: Optional[float] = None
) -> float:
"""Calculate Jaccard distance (1 - similarity)."""
if px is None:
px = float(x.mean())
if py is None:
py = float(y.mean())
return 1 - similarity(x, y, center=False, px=px, py=py)


def bootstrap(
x: np.ndarray,
y: np.ndarray,
px: Optional[float] = None,
py: Optional[float] = None,
n: int = 1000,
seed: int = 42,
) -> pd.Series:
"""Bootstrap p-value for Jaccard similarity."""
j = similarity(x, y, center=False, px=px, py=py)
if px is None:
px = float(x.mean())
if py is None:
py = float(y.mean())
if px == 1 or py == 1 or len(x) == x.sum() or len(y) == y.sum():
logger.warning("Bootstrap is degenerate as at least one vector is all 1.")
return pd.Series([j, 1], index=["J-sim", "p-val"])
if px == 0 or py == 0 or x.sum() == 0 or y.sum() == 0:
logger.warning("Bootstrap is degenerate as at least one vector is all 0.")
return pd.Series([j, 1], index=["J-sim", "p-val"])

j_obs = similarity(x, y, center=True, px=px, py=py)

rng = np.random.default_rng(seed)
vals = (
similarity(
rng.choice(x, size=len(x), replace=True, shuffle=False),
rng.choice(y, size=len(y), replace=True, shuffle=False),
center=True,
)
for _ in range(n)
)
j_null = np.fromiter(vals, dtype=np.float32, count=n)
np.abs(j_null, dtype=np.float32, out=j_null)
p_val = (j_null >= np.abs(j_obs)).sum() / n
return pd.Series([j, p_val], index=["J-sim", "p-val"])
3 changes: 2 additions & 1 deletion lta/helpers/pipeline.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
"""A dataclass that allows for an object oriented pipeline."""

import itertools
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Dict

import pandas as pd
from jaccard import jaccard as jac

import lta.helpers.data_handling as dh
from lta.helpers import jaccard as jac
from lta.helpers import utils

logger = logging.getLogger(__name__)
Expand Down
1 change: 1 addition & 0 deletions lta/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
lta_parser : argparse.ArgumentParser
The argument parser for the root command.
"""

from pathlib import Path

import configargparse
Expand Down
Loading
Loading