') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Enforce instance key to be dtype int by melonora · Pull Request #444 · scverse/spatialdata · GitHub
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
2 changes: 1 addition & 1 deletion src/spatialdata/datasets.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,7 +240,7 @@ def _points_blobs(
arr = rng.integers(padding, length - padding, size=(n_points, 2)).astype(np.int64)
# randomly assign some values from v to the points
points_assignment0 = rng.integers(0, 10, size=arr.shape[0]).astype(np.int64)
genes = rng.choice(["a", "b"], size=arr.shape[0])
genes = rng.choice(["gene_a", "gene_b"], size=arr.shape[0])
annotation = pd.DataFrame(
{
"genes": genes,
Expand Down
18 changes: 18 additions & 0 deletions src/spatialdata/models/models.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@
from multiscale_spatial_image.multiscale_spatial_image import MultiscaleSpatialImage
from multiscale_spatial_image.to_multiscale.to_multiscale import Methods
from pandas import CategoricalDtype
from pandas.errors import IntCastingNaNError
from shapely._geometry import GeometryType
from shapely.geometry import MultiPolygon, Point, Polygon
from shapely.geometry.collection import GeometryCollection
Expand DownExpand Up@@ -857,6 +858,23 @@ def parse(
adata.obs[region_key] = pd.Categorical(adata.obs[region_key])
if instance_key is None:
raise ValueError("`instance_key` must be provided.")
if adata.obs[instance_key].dtype != int:
try:
warnings.warn(
f"Converting `{cls.INSTANCE_KEY}: {instance_key}` to integer dtype.", UserWarning, stacklevel=2
)
adata.obs[instance_key] = adata.obs[instance_key].astype(int)
except IntCastingNaNError as exc:
raise ValueError("Values within table.obs[] must be able to be coerced to int dtype.") from exc

grouped = adata.obs.groupby(region_key)
grouped_size = grouped.size()
grouped_nunique = grouped.nunique()
not_unique = grouped_size[grouped_size != grouped_nunique[instance_key]].index.tolist()
if not_unique:
raise ValueError(
f"Instance key column for region(s) `{', '.join(not_unique)}` does not contain only unique integers"
)

attr = {"region": region, "region_key": region_key, "instance_key": instance_key}
adata.uns[cls.ATTRS_KEY] = attr
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -247,7 +247,7 @@ def _get_shapes() -> dict[str, GeoDataFrame]:
points["radius"] = rng.normal(size=(len(points), 1))

out["poly"] = ShapesModel.parse(poly)
out["poly"].index = ["a", "b", "c", "d", "e"]
out["poly"].index = [0, 1, 2, 3, 4]
out["multipoly"] = ShapesModel.parse(multipoly)
out["circles"] = ShapesModel.parse(points)

Expand Down
2 changes: 1 addition & 1 deletion tests/core/operations/test_spatialdata_operations.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@ def test_subset(full_sdata: SpatialData) -> None:

adata = AnnData(
shape=(10, 0),
obs={"region": ["circles"] * 5 + ["poly"] * 5, "instance_id": [0, 1, 2, 3, 4, "a", "b", "c", "d", "e"]},
obs={"region": ["circles"] * 5 + ["poly"] * 5, "instance_id": [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]},
)
del full_sdata.table
sdata_table = TableModel.parse(adata, region=["circles", "poly"], region_key="region", instance_key="instance_id")
Expand Down
28 changes: 27 additions & 1 deletion tests/models/test_models.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import re
import tempfile
from copy import deepcopy
from functools import partial
Expand DownExpand Up@@ -305,7 +306,7 @@ def test_table_model(
region: str | np.ndarray,
) -> None:
region_key = "reg"
obs = pd.DataFrame(RNG.integers(0, 100, size=(10, 3)), columns=["A", "B", "C"])
obs = pd.DataFrame(RNG.choice(np.arange(0, 100), size=(10, 3), replace=False), columns=["A", "B", "C"])
obs[region_key] = region
adata = AnnData(RNG.normal(size=(10, 2)), obs=obs)
table = model.parse(adata, region=region, region_key=region_key, instance_key="A")
Expand All@@ -319,6 +320,31 @@ def test_table_model(
assert TableModel.REGION_KEY_KEY in table.uns[TableModel.ATTRS_KEY]
assert table.uns[TableModel.ATTRS_KEY][TableModel.REGION_KEY] == region

obs["A"] = obs["A"].astype(str)
adata = AnnData(RNG.normal(size=(10, 2)), obs=obs)
with pytest.warns(UserWarning, match="Converting"):
model.parse(adata, region=region, region_key=region_key, instance_key="A")

obs["A"] = pd.Series(len([chr(ord("a") + i) for i in range(10)]))
adata = AnnData(RNG.normal(size=(10, 2)), obs=obs)
with pytest.raises(ValueError, match="Values within"):
model.parse(adata, region=region, region_key=region_key, instance_key="A")

@pytest.mark.parametrize("model", [TableModel])
@pytest.mark.parametrize("region", [["sample_1"] * 5 + ["sample_2"] * 5])
def test_table_instance_key_values_not_unique(self, model: TableModel, region: str | np.ndarray):
region_key = "region"
obs = pd.DataFrame(RNG.integers(0, 100, size=(10, 3)), columns=["A", "B", "C"])
obs[region_key] = region
obs["A"] = [1] * 5 + list(range(5))
adata = AnnData(RNG.normal(size=(10, 2)), obs=obs)
with pytest.raises(ValueError, match=re.escape("Instance key column for region(s) `sample_1`")):
model.parse(adata, region=region, region_key=region_key, instance_key="A")

adata.obs["A"] = [1] * 10
with pytest.raises(ValueError, match=re.escape("Instance key column for region(s) `sample_1, sample_2`")):
model.parse(adata, region=region, region_key=region_key, instance_key="A")


def test_get_schema():
images = _get_images()
Expand Down