Skip to content

Commit 8790bdb

Browse files
authored
Implement get/set for row/column data and coercions betwee SE/RSE (#103)
1 parent e796c2b commit 8790bdb

8 files changed

Lines changed: 172 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# Changelog
22

3-
## Version 0.6.0 - 0.6.2
3+
## Version 0.6.0 - 0.6.3
44

55
- Classes now extend `BiocObject` from biocutils, which provides a metadata field.
66
- `valdiate` is renamed to `_validate` for consistency with other classes and packages.
77
- Update github actions to run from 3.10 - 3.14.
8+
- Coerce: RSE to/from SE and vice-versa.
9+
- Implement getters and setters to extract columns from either row/column data (See [issue](https://github.com/BiocPy/SummarizedExperiment/issues/99)).
810

911
## Version 0.5.4 - 0.5.5
1012

src/summarizedexperiment/RangedSummarizedExperiment.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -942,18 +942,55 @@ def combine_columns(self, *other) -> RangedSummarizedExperiment:
942942
######>> to se <<#####
943943
######################
944944

945+
@classmethod
946+
def from_summarizedexperiment(cls, se: SummarizedExperiment) -> RangedSummarizedExperiment:
947+
return cls(
948+
assays=se.get_assays(),
949+
row_data=se.get_row_data(),
950+
column_data=se.get_column_data(),
951+
row_names=se.get_row_names(),
952+
column_names=se.get_column_names(),
953+
metadata=se.get_metadata(),
954+
)
955+
956+
@classmethod
957+
def from_se(cls, se: SummarizedExperiment) -> RangedSummarizedExperiment:
958+
"""Alias for :py:meth:`~to_summarizedexperiment`."""
959+
return RangedSummarizedExperiment.from_summarizedexperiment(se)
960+
945961
def to_summarizedexperiment(self):
946962
"""Coerce to :py:class:`summarizedexperiment.SummarizedExperiment.SummarizedExperiment`"""
947963

948-
warn("SE does not containg a slot for ranges, dropping ranges.", UserWarning)
964+
warn("SE does not containg a slot for ranges, merging ranges with row_data.", UserWarning)
965+
966+
new_row_data = self._rows
967+
if self._row_ranges is not None:
968+
try:
969+
import copy
970+
971+
if isinstance(self._row_ranges, CompressedGenomicRangesList):
972+
df = self._row_ranges.get_unlist_data().to_pandas()
973+
else:
974+
df = self._row_ranges.to_pandas()
975+
976+
range_bf = biocframe.BiocFrame.from_pandas(df)
977+
if new_row_data is None or new_row_data.shape[0] == 0:
978+
new_row_data = range_bf
979+
else:
980+
new_row_data = copy.copy(new_row_data)
981+
for col in range_bf.column_names:
982+
new_row_data[col] = range_bf[col]
983+
except Exception as e:
984+
warn(f"Failed to transfer row_ranges to row_data: {e}", UserWarning)
949985

950986
return SummarizedExperiment(
951987
assays=self._assays,
952-
row_data=self._rows,
988+
row_data=new_row_data,
953989
column_data=self._cols,
954990
row_names=self._row_names,
955991
column_names=self._column_names,
956992
metadata=self._metadata,
993+
_validate=False,
957994
)
958995

959996
def to_se(self):

src/summarizedexperiment/SummarizedExperiment.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,33 @@ def combine_columns(self, *other) -> SummarizedExperiment:
128128
######>> to rse <<#####
129129
#######################
130130

131-
def to_rangedsummarizedexperiment(self):
132-
"""Coerce to :py:class:`summarizedexperiment.RangedSummarizedExperiment.RangedSummarizedExperiment`"""
131+
def to_rangedsummarizedexperiment(self, row_ranges=None):
132+
"""Coerce to :py:class:`summarizedexperiment.RangedSummarizedExperiment.RangedSummarizedExperiment`.
133+
134+
Args:
135+
row_ranges:
136+
Genomic features, must be the same length as the number of rows of
137+
the matrices in assays.
138+
139+
If ``row_ranges`` is not provided, this method will attempt to
140+
extract range information (e.g., 'seqnames', 'starts') from
141+
``row_data`` to construct the :py:class:`~genomicranges.GenomicRanges.GenomicRanges`.
142+
"""
133143

134144
from .RangedSummarizedExperiment import RangedSummarizedExperiment
135145

146+
if row_ranges is None:
147+
try:
148+
rd_cols = self.row_data.get_column_names()
149+
if "seqnames" in rd_cols and "starts" in rd_cols:
150+
df = self.row_data.to_pandas()
151+
row_ranges = GenomicRanges.from_pandas(df)
152+
except Exception as _:
153+
pass
154+
136155
return RangedSummarizedExperiment(
137156
assays=self._assays,
157+
row_ranges=row_ranges,
138158
row_data=self._rows,
139159
column_data=self._cols,
140160
row_names=self._row_names,
@@ -143,9 +163,9 @@ def to_rangedsummarizedexperiment(self):
143163
_validate=False,
144164
)
145165

146-
def to_rse(self):
166+
def to_rse(self, row_ranges=None):
147167
"""Alias for :py:meth:`~to_rangedsummarizedexperiment`."""
148-
return self.to_rangedsummarizedexperiment()
168+
return self.to_rangedsummarizedexperiment(row_ranges=row_ranges)
149169

150170

151171
############################

src/summarizedexperiment/base.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,3 +1197,40 @@ def to_anndata(self):
11971197
)
11981198

11991199
return obj
1200+
1201+
######################################
1202+
######>> col_data accessors <<########
1203+
######################################
1204+
1205+
def get_column_data_column(self, column: str) -> Any:
1206+
"""Access a column from the ``column_data``.
1207+
1208+
Args:
1209+
column:
1210+
Name of the column to retrieve.
1211+
1212+
Returns:
1213+
The content of the column.
1214+
"""
1215+
return self._cols.get_column(column)
1216+
1217+
def set_column_data_column(self, column: str, value: Any, in_place: bool = False) -> BaseSE:
1218+
"""Set or replace a column in ``column_data``.
1219+
1220+
Args:
1221+
column:
1222+
Name of the column to set.
1223+
1224+
value:
1225+
Values for the column. Must match the number of samples (columns).
1226+
1227+
in_place:
1228+
Whether to modify the ``BaseSE`` in place.
1229+
1230+
Returns:
1231+
A modified ``BaseSE`` object, either as a copy of the original
1232+
or as a reference to the (in-place-modified) original.
1233+
"""
1234+
output = self._define_output(in_place)
1235+
output._cols = output._cols.set_column(column, value, in_place=False)
1236+
return output

tests/test_RSE.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66
import pandas as pd
77
import pytest
8+
from summarizedexperiment.SummarizedExperiment import SummarizedExperiment
89
from summarizedexperiment.RangedSummarizedExperiment import RangedSummarizedExperiment
910

1011
__author__ = "jkanche"
@@ -132,3 +133,16 @@ def test_RSE_empty():
132133

133134
assert tse.row_names is None
134135
assert tse.col_names is None
136+
137+
def test_RSE_from_SE():
138+
tse = SummarizedExperiment(
139+
assays={"counts": counts}, row_data=df_gr, column_data=col_data
140+
)
141+
142+
assert tse is not None
143+
assert isinstance(tse, SummarizedExperiment)
144+
assert tse.shape == (200, 6)
145+
146+
rse = RangedSummarizedExperiment.from_se(tse)
147+
assert isinstance(rse, RangedSummarizedExperiment)
148+
assert rse.shape == (200, 6)

tests/test_RSE_methods.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,19 @@ def test_RSE_to_se():
231231
assert se is not None
232232
assert isinstance(se, SummarizedExperiment)
233233
assert rse.shape == se.shape
234+
235+
def test_RSE_to_se_with_ranges():
236+
tse = RangedSummarizedExperiment(
237+
assays={"counts": counts}, row_ranges=gr, column_data=col_data
238+
)
239+
240+
se = tse.to_summarizedexperiment()
241+
242+
assert se is not None
243+
assert isinstance(se, SummarizedExperiment)
244+
assert se.shape == tse.shape
245+
246+
assert se.row_data is not None
247+
assert "seqnames" in se.row_data.column_names
248+
assert "starts" in se.row_data.column_names
249+
assert "ends" in se.row_data.column_names

tests/test_SE.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy as np
55
import pandas as pd
66
from summarizedexperiment.SummarizedExperiment import SummarizedExperiment
7+
from summarizedexperiment.RangedSummarizedExperiment import RangedSummarizedExperiment
78
import delayedarray
89
from anndata import AnnData
910

tests/test_SE_methods.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,41 @@ def test_SE_to_rse():
149149
assert rse is not None
150150
assert isinstance(rse, RangedSummarizedExperiment)
151151
assert rse.shape == tse.shape
152+
153+
def test_SE_to_rse_parse_ranges():
154+
rd = BiocFrame({
155+
"seqnames": ["chr1"] * nrows,
156+
"starts": range(nrows),
157+
"ends": range(nrows),
158+
"strand": ["+"] * nrows
159+
})
160+
161+
tse = SummarizedExperiment(
162+
assays={"counts": counts}, row_data=rd, column_data=col_data
163+
)
164+
165+
rse = tse.to_rangedsummarizedexperiment()
166+
167+
assert rse is not None
168+
assert isinstance(rse, RangedSummarizedExperiment)
169+
assert rse.shape == tse.shape
170+
171+
assert rse.row_ranges is not None
172+
assert len(rse.row_ranges) == nrows
173+
assert np.allclose(rse.row_ranges.start, np.array(range(nrows)))
174+
175+
def test_SE_coldata_accessors():
176+
tse = SummarizedExperiment(
177+
assays={"counts": counts}, row_data=row_data, column_data=col_data
178+
)
179+
180+
assert tse.get_column_data_column("treatment") is not None
181+
assert len(tse.get_column_data_column("treatment")) == 6
182+
183+
new_tse = tse.set_column_data_column("stuff", [1, 2, 3, 4, 5, 6])
184+
assert new_tse.shape == tse.shape
185+
assert "stuff" in new_tse.col_data.column_names
186+
assert "stuff" not in tse.col_data.column_names
187+
188+
tse.set_column_data_column("stuff", [1, 2, 3, 4, 5, 6], in_place=True)
189+
assert "stuff" in tse.col_data.column_names

0 commit comments

Comments
 (0)