Source code for pertpy.tools._perturbation_space._comparison
from typing import TYPE_CHECKING
import numpy as np
from scipy.sparse import issparse
from scipy.sparse import vstack as sp_vstack
from sklearn.base import ClassifierMixin
from sklearn.linear_model import LogisticRegression
if TYPE_CHECKING:
from numpy.typing import NDArray
[docs]
class PerturbationComparison:
"""Comparison between real and simulated perturbations."""
[docs]
def compare_classification(
self,
real: np.ndarray,
simulated: np.ndarray,
control: np.ndarray,
clf: ClassifierMixin | None = None,
) -> float:
"""Compare classification accuracy between real and simulated perturbations.
Trains a classifier on the real perturbation data & the control data and reports a normalized
classification accuracy on the simulated perturbation.
Args:
real: Real perturbed data.
simulated: Simulated perturbed data.
control: Control data
clf: sklearn classifier to use, `sklearn.linear_model.LogisticRegression` if not provided.
"""
assert real.shape[1] == simulated.shape[1] == control.shape[1]
if clf is None:
clf = LogisticRegression()
n_x = real.shape[0]
data = sp_vstack((real, control)) if issparse(real) else np.vstack((real, control))
labels = np.concatenate([np.full(real.shape[0], "comp"), np.full(control.shape[0], "ctrl")])
clf.fit(data, labels)
norm_score = clf.score(simulated, np.full(simulated.shape[0], "comp")) / clf.score(real, labels[:n_x])
norm_score = min(1.0, norm_score)
return norm_score
[docs]
def compare_knn(
self,
real: np.ndarray,
simulated: np.ndarray,
control: np.ndarray | None = None,
use_simulated_for_knn: bool = False,
n_neighbors: int = 20,
random_state: int = 0,
n_jobs: int = 1,
) -> dict[str, float]:
"""Calculate proportions of real perturbed and control data points for simulated data.
Computes proportions of real perturbed, control and simulated (if `use_simulated_for_knn=True`)
data points for simulated data. If control (`C`) is not provided, builds the knn graph from
real perturbed + simulated perturbed.
Args:
real: Real perturbed data.
simulated: Simulated perturbed data.
control: Control data
use_simulated_for_knn: Include simulted perturbed data (`simulated`) into the knn graph.
Only valid when control (`control`) is provided.
n_neighbors: Number of neighbors to use in k-neighbor graph.
random_state: Random state used for k-neighbor graph construction.
n_jobs: Number of cores to use. Defaults to -1 (all).
"""
assert real.shape[1] == simulated.shape[1]
if control is not None:
assert real.shape[1] == control.shape[1]
n_y = simulated.shape[0]
if control is None:
index_data = sp_vstack((simulated, real)) if issparse(real) else np.vstack((simulated, real))
else:
datas = (simulated, real, control) if use_simulated_for_knn else (real, control)
index_data = sp_vstack(datas) if issparse(real) else np.vstack(datas)
y_in_index = use_simulated_for_knn or control is None
c_in_index = control is not None
label_groups = ["comp"]
labels: NDArray[np.str_] = np.full(index_data.shape[0], "comp")
if y_in_index:
labels[:n_y] = "siml"
label_groups.append("siml")
if c_in_index:
labels[-control.shape[0] :] = "ctrl"
label_groups.append("ctrl")
from pynndescent import NNDescent
index = NNDescent(
index_data,
n_neighbors=max(50, n_neighbors),
random_state=random_state,
n_jobs=n_jobs,
)
indices = index.query(simulated, k=n_neighbors)[0]
uq, uq_counts = np.unique(labels[indices], return_counts=True)
uq_counts_norm = uq_counts / uq_counts.sum()
counts = dict(zip(label_groups, [0.0] * len(label_groups), strict=False))
counts.update(zip(uq, uq_counts_norm, strict=False))
return counts