Skip to content
Open
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
67 changes: 67 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Tests

on:
push:
branches: [master, main, new_version]
pull_request:
# Runs the unpinned job below, so a new release of a dependency is found on a
# schedule instead of on an unrelated push.
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
test:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.12']

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
# Constrained so that a dependency release cannot turn this red without a commit
# here. The pins only apply to CI; setup.py stays open for anyone installing the
# package. See ci/constraints-py*.txt for how to move them forward.
run: |
python -m pip install --upgrade pip
pip install -e .[test] -c ci/constraints-py${{ matrix.python-version }}.txt
- name: Run tests
run: pytest -v --cov=cell2cell --cov-report=term-missing

latest-dependencies:
# The same suite with nothing pinned, to find out when the ecosystem moves ahead of
# the constraints. A failure here means a dependency needs attention, not that the
# commit is broken, which is why it does not run on pushes or pull requests.
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.12']

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install the newest dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[test]
pip list
- name: Run tests
run: pytest -v
2 changes: 1 addition & 1 deletion cell2cell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
from cell2cell import tensor
from cell2cell import utils

__version__ = "0.8.4"
__version__ = "0.9.0"
13 changes: 12 additions & 1 deletion cell2cell/analysis/cell2cell_pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pandas as pd
import scanpy
import numpy as np
from natsort import natsorted
from tqdm import tqdm

from cell2cell.core import interaction_space as ispace
Expand Down Expand Up @@ -462,6 +463,11 @@ class SingleCellInteractions:
of a given gene.
- 'average' : Computes the average gene expression among the single cells
composing a cell type for a given gene.
- 'trimean' : Computes the Tukey's trimean of the gene expression among the
single cells composing a cell type for a given gene. It is a weighted
average of the median and the first and third quartiles
(0.5 * Q2 + 0.25 * (Q1 + Q3)), so it is more robust to outliers than
the average while still accounting for the spread of the distribution.

barcode_col : str, default='barcodes'
Column-name for the single cells in the metadata.
Expand Down Expand Up @@ -609,6 +615,11 @@ class SingleCellInteractions:
of a given gene.
- 'average' : Computes the average gene expression among the single cells
composing a cell type for a given gene.
- 'trimean' : Computes the Tukey's trimean of the gene expression among the
single cells composing a cell type for a given gene. It is a weighted
average of the median and the first and third quartiles
(0.5 * Q2 + 0.25 * (Q1 + Q3)), so it is more robust to outliers than
the average while still accounting for the spread of the distribution.

ccc_permutation_pvalues : pandas.DataFrame
Contains the P-values of the permutation analysis on the
Expand Down Expand Up @@ -935,7 +946,7 @@ def initialize_interaction_space(rnaseq_data, ppi_data, cutoff_setup, analysis_s
if excluded_cells is None:
excluded_cells = []

included_cells = sorted(list((set(rnaseq_data.columns) - set(excluded_cells))))
included_cells = natsorted(set(rnaseq_data.columns) - set(excluded_cells))

interaction_space = ispace.InteractionSpace(rnaseq_data=rnaseq_data[included_cells],
ppi_data=ppi_data,
Expand Down
31 changes: 22 additions & 9 deletions cell2cell/analysis/tensor_downstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import numpy as np
import pandas as pd

from natsort import natsorted

from cell2cell.stats import gini_coefficient


Expand Down Expand Up @@ -97,7 +99,7 @@ def get_factor_specific_ccc_networks(result, sender_label='Sender Cells', receiv
else:
raise ValueError('result is not of a valid type. It must be an InteractionTensor or a dict.')

factors = sorted(list(set(result[sender_label].columns) & set(result[receiver_label].columns)))
factors = natsorted(set(result[sender_label].columns) & set(result[receiver_label].columns))

networks = dict()
for f in factors:
Expand All @@ -113,7 +115,7 @@ def flatten_factor_ccc_networks(networks, orderby='senders'):
'''
Flattens all adjacency matrices in the factor-specific
cell-cell communication networks. It generates a matrix
where rows are factors and columns are cell-cell pairs.
where rows are cell-cell pairs and columns are factors.

Parameters
----------
Expand All @@ -131,11 +133,19 @@ def flatten_factor_ccc_networks(networks, orderby='senders'):
Returns
-------
flatten_networks : pandas.DataFrame
A dataframe wherein rows contains a factor-specific network. Columns are
the directed cell-cell pairs.
A dataframe wherein each column contains a factor-specific network. Rows are
the directed cell-cell pairs, named as '<sender> --> <receiver>'. Cells keep
the order they have in the tensor dimensions.
'''
senders = sorted(set.intersection(*[set(v.index) for v in networks.values()]))
receivers = sorted(set.intersection(*[set(v.columns) for v in networks.values()]))
net_list = list(networks.values())
common_senders = set.intersection(*[set(v.index) for v in net_list])
common_receivers = set.intersection(*[set(v.columns) for v in net_list])

# Keep the order of the elements in the tensor dimensions instead of sorting them.
# Sorting the names here without reordering the data assigns loadings to the wrong
# cell-cell pair whenever the tensor elements are not alphabetically sorted.
senders = [s for s in net_list[0].index if s in common_senders]
receivers = [r for r in net_list[0].columns if r in common_receivers]

if orderby == 'senders':
cell_pairs = [s + ' --> ' + r for s in senders for r in receivers]
Expand All @@ -146,7 +156,10 @@ def flatten_factor_ccc_networks(networks, orderby='senders'):
else:
raise ValueError("`orderby` must be either 'senders' or 'receivers'.")

data = np.asarray([v.values.flatten(flatten_order) for v in networks.values()]).T
# Reindexing guarantees that every network is flattened in the same order the
# cell-cell pair names were built from.
data = np.asarray([v.reindex(index=senders, columns=receivers).values.flatten(flatten_order)
for v in net_list]).T
flatten_networks = pd.DataFrame(data=data,
index=cell_pairs,
columns=list(networks.keys())
Expand Down Expand Up @@ -191,7 +204,7 @@ def compute_gini_coefficients(result, sender_label='Sender Cells', receiver_labe
else:
raise ValueError('result is not of a valid type. It must be an InteractionTensor or a dict.')

factors = sorted(list(set(result[sender_label].columns) & set(result[receiver_label].columns)))
factors = natsorted(set(result[sender_label].columns) & set(result[receiver_label].columns))

ginis = []
for f in factors:
Expand Down Expand Up @@ -276,7 +289,7 @@ def get_lr_by_cell_pairs(result, lr_label, sender_label, receiver_label, order_c
assert receiver_label in result.keys(), 'The specified dimension ' + receiver_label + ' is not present in the `result` input'

# Sort factors
sorted_factors = sorted(result[lr_label].columns, key=lambda x: int(x.split(' ')[1]))
sorted_factors = natsorted(result[lr_label].columns)

# Get CCI network per factor
networks = get_factor_specific_ccc_networks(result=result,
Expand Down
8 changes: 4 additions & 4 deletions cell2cell/clustering/cluster_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ def compute_linkage(distance_matrix, method='ward', optimal_ordering=True):
Z : numpy.ndarray
The hierarchical clustering encoded as a linkage matrix.
'''
if (type(distance_matrix) is pd.core.frame.DataFrame):
data = distance_matrix.values
else:
data = distance_matrix.copy()
# `np.array` accepts both dataframes and arrays, and always copies, so the diagonal
# can be zeroed below. The array behind `DataFrame.values` is read-only under the
# copy-on-write of pandas >= 3.0.
data = np.array(distance_matrix, dtype=float)
if ~(data.transpose() == data).all():
raise ValueError('The matrix is not symmetric')

Expand Down
12 changes: 8 additions & 4 deletions cell2cell/core/interaction_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from __future__ import absolute_import

from cell2cell.preprocessing import integrate_data, cutoffs, get_genes_from_complexes, add_complexes_to_expression
from cell2cell.preprocessing import (integrate_data, cutoffs, get_genes_from_complexes, add_complexes_to_expression,
zero_diagonal)
from cell2cell.core import cell, cci_scores, communication_scores

import itertools
Expand Down Expand Up @@ -64,7 +65,9 @@ def generate_pairs(cells, cci_type, self_interaction=True, remove_duplicates=Tru
else:
raise NotImplementedError("CCI type has to be directed or undirected")
if remove_duplicates:
pairs = list(set(pairs)) # Remove duplicates
# `dict.fromkeys` removes duplicates while keeping the order given by the
# list of cells, so the resulting pairs are reproducible across runs.
pairs = list(dict.fromkeys(pairs))
return pairs


Expand Down Expand Up @@ -510,14 +513,15 @@ def compute_pairwise_cci_scores(self, cci_score=None, use_ppi_score=False, verbo
# )

# Generate distance matrix
if ~(cci_score in ['count', 'icellnet']):
if cci_score not in ['count', 'icellnet']:
self.distance_matrix = self.interaction_elements['cci_matrix'].apply(lambda x: 1 - x)
else:
#self.distance_matrix = self.interaction_elements['cci_matrix'].div(self.interaction_elements['cci_matrix'].max().max()).apply(lambda x: 1 - x)
# Regularized distance
mean = np.nanmean(self.interaction_elements['cci_matrix'])
self.distance_matrix = self.interaction_elements['cci_matrix'].div(self.interaction_elements['cci_matrix'] + mean).apply(lambda x: 1 - x)
np.fill_diagonal(self.distance_matrix.values, 0.0) # Make diagonal zero (delete autocrine-interactions)
# Make diagonal zero (delete autocrine-interactions)
self.distance_matrix = zero_diagonal(self.distance_matrix)

def pair_communication_score(self, cell1, cell2, communication_score='expression_thresholding',
use_ppi_score=False, verbose=True):
Expand Down
4 changes: 3 additions & 1 deletion cell2cell/datasets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
from cell2cell.datasets.heuristic_data import (HeuristicGOTerms)
from cell2cell.datasets.random_data import (generate_random_rnaseq, generate_random_ppi, generate_random_cci_scores,
generate_random_metadata)
from cell2cell.datasets.toy_data import (generate_toy_distance, generate_toy_rnaseq, generate_toy_ppi, generate_toy_metadata)
from cell2cell.datasets.toy_data import (generate_toy_distance, generate_toy_rnaseq, generate_toy_ppi, generate_toy_metadata,
generate_toy_contexts, generate_toy_single_cells, generate_toy_coordinates,
generate_toy_spatial_adata, generate_toy_liana_output)
Loading
Loading