Skip to content

Commit 292163d

Browse files
authored
NEW: add filter-ids action (#370)
<!-- PR guidance and examples: https://github.com/rachis-org/governance/blob/main/pr_template_guidelines.md --> <!-- rachis Generative AI Policy: https://github.com/rachis-org/governance/blob/main/ai_policy.md --> # Description <!-- REQUIRED: Briefly describe this PR, or link the issue it closes. --> - Adds filter-id action that can filter any feature table with metadata by samples or features. The logic for metadata filtering is the same as in the actions filter-samples and filter-features. - There is an additional parameter where you can specify ids directly without a metadata file. - This action was added because the `Unconstrained` table needs actions for id filtering but the data of the `Unconstrained` table is not suited for the additional filters of filter-samples and filter-features that only work with count tables. - `Unconstrained` was added as a type to the split, transpose and rename-ids actions. # AI Disclosure <!-- REQUIRED: Check exactly one option. --> - [ ] NO AI USED. - [x] AI USED. # AI Usage Details <!-- REQUIRED if 'AI USED' is checked. This section can be deleted if 'NO AI USED' is checked. --> Codex
2 parents 56388b3 + bc5373a commit 292163d

7 files changed

Lines changed: 457 additions & 53 deletions

File tree

‎q2_feature_table/__init__.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
tabulate_sample_frequencies,
1414
tabulate_feature_frequencies, summarize)
1515
from ._merge import (merge, merge_seqs, merge_taxa, overlap_methods)
16-
from ._filter import (filter_samples, filter_features, filter_seqs,
16+
from ._filter import (filter_samples, filter_features, filter_ids, filter_seqs,
1717
filter_features_conditionally)
1818
from ._split import split
1919
from ._core_features import core_features
@@ -30,9 +30,9 @@
3030

3131
__all__ = ['rarefy', 'presence_absence', 'relative_frequency', 'transpose',
3232
'_summarize', 'merge', 'merge_seqs', 'filter_samples',
33-
'filter_features', 'merge_taxa', 'tabulate_seqs', 'overlap_methods',
34-
'core_features', 'group', 'heatmap', 'heatmap_choices',
35-
'filter_seqs', 'subsample_ids', 'rename_ids',
33+
'filter_features', 'filter_ids', 'merge_taxa', 'tabulate_seqs',
34+
'overlap_methods', 'core_features', 'group', 'heatmap',
35+
'heatmap_choices', 'filter_seqs', 'subsample_ids', 'rename_ids',
3636
'filter_features_conditionally', 'split',
3737
'tabulate_feature_frequencies', 'tabulate_sample_frequencies',
3838
'summarize', 'normalize', 'multiply_tables', '_multiply_tables',

‎q2_feature_table/_examples.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,41 @@ def feature_table_summarize(use):
461461
feature_freqs.assert_output_type('ImmutableMetadata')
462462
sample_freqs.assert_output_type('ImmutableMetadata')
463463
viz.assert_output_type('Visualization')
464+
465+
466+
def feature_table_filter_ids_samples_by_metadata_and_where(use):
467+
feature_table = use.init_artifact_from_url(
468+
"feature_table", moving_pics_ft_url
469+
)
470+
sample_metadata = use.init_metadata_from_url(
471+
"sample_metadata", moving_pics_md_url
472+
)
473+
474+
filtered_table, = use.action(
475+
use.UsageAction(plugin_id="feature_table", action_id="filter_ids"),
476+
use.UsageInputs(
477+
table=feature_table,
478+
axis="sample",
479+
metadata=sample_metadata,
480+
where="[subject]='subject-1'",
481+
),
482+
use.UsageOutputNames(filtered_table="filtered_table"),
483+
)
484+
485+
filtered_table.assert_output_type("FeatureTable[Frequency]")
486+
487+
488+
def feature_table_filter_ids_features_by_ids(use):
489+
feature_table = use.init_artifact("feature_table", ft4_factory)
490+
491+
filtered_table, = use.action(
492+
use.UsageAction(plugin_id="feature_table", action_id="filter_ids"),
493+
use.UsageInputs(
494+
table=feature_table,
495+
axis="feature",
496+
ids=["F1", "F3"],
497+
),
498+
use.UsageOutputNames(filtered_table="filtered_table"),
499+
)
500+
501+
filtered_table.assert_output_type("FeatureTable[Frequency]")

‎q2_feature_table/_filter.py‎

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ def check_relative_frequency(table: biom.Table) -> bool:
3030
def _get_biom_filter_function(ids_to_keep, min_frequency, max_frequency,
3131
min_nonzero, max_nonzero):
3232
ids_to_keep = set(ids_to_keep)
33+
if min_frequency is None:
34+
min_frequency = -np.inf
3335
if max_frequency is None:
3436
max_frequency = np.inf
37+
if min_nonzero is None:
38+
min_nonzero = 0
3539
if max_nonzero is None:
3640
max_nonzero = np.inf
3741

@@ -48,21 +52,28 @@ def f(data_vector, id_, metadata):
4852
def _filter_table(table, min_frequency, max_frequency, min_nonzero,
4953
max_nonzero, metadata, where, axis, exclude_ids=False,
5054
filter_opposite_axis=True,
51-
allow_empty_table=False):
52-
if min_frequency == 0 and max_frequency is None and min_nonzero == 0 and\
53-
max_nonzero is None and metadata is None and where is None and\
54-
exclude_ids is False:
55+
allow_empty_table=False, ids=None):
56+
if (min_frequency in (None, 0) and max_frequency is None and
57+
min_nonzero in (None, 0) and max_nonzero is None and ids is None
58+
and metadata is None and where is None and exclude_ids is False):
5559
raise ValueError("No filtering was requested.")
5660
if metadata is None and where is not None:
5761
raise ValueError("Metadata must be provided if 'where' is "
5862
"specified.")
59-
if metadata is None and exclude_ids is True:
60-
raise ValueError("Metadata must be provided if 'exclude_ids' "
61-
"is True.")
62-
if metadata is not None:
63-
ids_to_keep = metadata.get_ids(where=where)
63+
if ids is None and metadata is None and exclude_ids is True:
64+
raise ValueError("Either 'ids' or metadata must be provided if "
65+
"'exclude_ids' is True.")
66+
if ids is not None:
67+
missing_ids = set(ids) - set(table.ids(axis=axis))
68+
if missing_ids:
69+
raise ValueError("The following IDs are not in the table: %s" %
70+
", ".join(sorted(missing_ids)))
71+
if ids is None and metadata is None:
72+
ids_to_keep = set(table.ids(axis=axis))
6473
else:
65-
ids_to_keep = table.ids(axis=axis)
74+
ids_to_keep = set(ids or [])
75+
if metadata is not None:
76+
ids_to_keep.update(metadata.get_ids(where=where))
6677
if exclude_ids is True:
6778
ids_to_keep = set(table.ids(axis=axis)) - set(ids_to_keep)
6879

@@ -82,7 +93,9 @@ def _filter_table(table, min_frequency, max_frequency, min_nonzero,
8293
def filter_samples(table: biom.Table, min_frequency: int = 0,
8394
max_frequency: int = None, min_features: int = 0,
8495
max_features: int = None,
85-
metadata: qiime2.Metadata = None, where: str = None,
96+
ids: list[str] = None,
97+
metadata: qiime2.Metadata = None,
98+
where: str = None,
8699
exclude_ids: bool = False,
87100
filter_empty_features: bool = True,
88101
allow_empty_table: bool = False)\
@@ -92,15 +105,17 @@ def filter_samples(table: biom.Table, min_frequency: int = 0,
92105
max_nonzero=max_features, metadata=metadata,
93106
where=where, axis='sample', exclude_ids=exclude_ids,
94107
filter_opposite_axis=filter_empty_features,
95-
allow_empty_table=allow_empty_table
108+
allow_empty_table=allow_empty_table, ids=ids
96109
)
97110
return table
98111

99112

100113
def filter_features(table: biom.Table, min_frequency: int = 0,
101114
max_frequency: int | str = 'None', min_samples: int = 0,
102115
max_samples: int = None,
103-
metadata: qiime2.Metadata = None, where: str = None,
116+
ids: list[str] = None,
117+
metadata: qiime2.Metadata = None,
118+
where: str = None,
104119
exclude_ids: bool = False,
105120
filter_empty_samples: bool = True,
106121
allow_empty_table: bool = False)\
@@ -115,7 +130,7 @@ def filter_features(table: biom.Table, min_frequency: int = 0,
115130
max_nonzero=max_samples, metadata=metadata,
116131
where=where, axis='observation', exclude_ids=exclude_ids,
117132
filter_opposite_axis=filter_empty_samples,
118-
allow_empty_table=allow_empty_table)
133+
allow_empty_table=allow_empty_table, ids=ids)
119134

120135
if is_relative_frequency and not table.is_empty():
121136
relative_frequency(table)
@@ -178,3 +193,34 @@ def _filter_f(values, id_, metadata):
178193
_validate_nonempty_table(new_table)
179194

180195
return new_table
196+
197+
198+
def filter_ids(
199+
table: biom.Table,
200+
axis: str,
201+
ids: list[str] = None,
202+
metadata: qiime2.Metadata = None,
203+
where: str = None,
204+
exclude_ids: bool = False,
205+
allow_empty_table: bool = False
206+
) -> biom.Table:
207+
"""
208+
Filter sample or feature IDs from a table.
209+
210+
IDs may be supplied directly with ``ids`` and/or selected from
211+
``metadata`` using a SQLite ``where`` clause. When both are supplied,
212+
their IDs are combined. Direct IDs must all occur on the requested axis.
213+
``where`` may be used only with ``metadata``; without a WHERE clause, all
214+
metadata IDs are selected. IDs on the opposite axis are retained.
215+
"""
216+
axis_map = {"sample": "sample", "feature": "observation"}
217+
biom_axis = axis_map[axis]
218+
219+
_filter_table(
220+
table=table, min_frequency=None, max_frequency=None,
221+
min_nonzero=None, max_nonzero=None, metadata=metadata, where=where,
222+
axis=biom_axis, exclude_ids=exclude_ids,
223+
filter_opposite_axis=False,
224+
allow_empty_table=allow_empty_table, ids=ids)
225+
226+
return table

‎q2_feature_table/plugin_setup.py‎

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
Visualization)
1313

1414
from q2_types.feature_table import (
15-
FeatureTable, Frequency, RelativeFrequency, PresenceAbsence, Composition)
15+
FeatureTable, Frequency, RelativeFrequency, PresenceAbsence, Composition,
16+
Unconstrained)
1617
from q2_types.feature_data import (
1718
FeatureData, Sequence, Taxonomy, AlignedSequence, SequenceCharacteristics,
1819
LinkedSequence
@@ -35,6 +36,26 @@
3536
T1 = TypeMatch([Frequency, RelativeFrequency, PresenceAbsence, Composition])
3637
T2 = TypeMatch([Sequence, AlignedSequence, LinkedSequence])
3738
T3 = TypeMatch([Sequence, LinkedSequence])
39+
T4 = TypeMatch([Frequency, RelativeFrequency, PresenceAbsence, Composition,
40+
Unconstrained])
41+
42+
_filter_parameter_descriptions = {
43+
'ids': ('IDs to retain or, with `exclude_ids`, discard. When supplied '
44+
'with `metadata`, IDs from both sources are selected.'),
45+
'metadata': ('Metadata used with the `where` parameter when selecting '
46+
'IDs to retain, or with `exclude_ids` when selecting IDs '
47+
'to discard.'),
48+
'where': ('SQLite WHERE clause specifying metadata criteria that must be '
49+
'met for IDs to be included in the filtered feature table. If '
50+
'not provided, all IDs in `metadata` that are also in the '
51+
'feature table will be retained.'),
52+
'exclude_ids': ('If true, the IDs selected by `ids`, `metadata`, or '
53+
'`where` will be excluded from the filtered table '
54+
'instead of being retained.'),
55+
'allow_empty_table': ('If true, the filtered table may be empty. Default '
56+
'behavior is to raise an error if the filtered '
57+
'table is empty.')
58+
}
3859

3960
plugin.methods.register_function(
4061
function=q2_feature_table.rarefy,
@@ -136,10 +157,10 @@
136157

137158
plugin.methods.register_function(
138159
function=q2_feature_table.transpose,
139-
inputs={'table': FeatureTable[T1]},
160+
inputs={'table': FeatureTable[T4]},
140161
parameters={},
141162
outputs=[('transposed_feature_table',
142-
FeatureTable[T1])],
163+
FeatureTable[T4])],
143164
input_descriptions={
144165
'table': 'The feature table to be transposed.'
145166
},
@@ -283,15 +304,15 @@
283304
plugin.methods.register_function(
284305
function=q2_feature_table.rename_ids,
285306
inputs={
286-
'table': FeatureTable[T1],
307+
'table': FeatureTable[T4],
287308
},
288309
parameters={
289310
'metadata': MetadataColumn[Categorical],
290311
'strict': Bool,
291312
'axis': Str % Choices({'sample', 'feature'})
292313
},
293314
outputs=[
294-
('renamed_table', FeatureTable[T1])
315+
('renamed_table', FeatureTable[T4])
295316
],
296317
input_descriptions={
297318
'table': 'The table to be renamed',
@@ -323,6 +344,7 @@
323344
'max_frequency': Int,
324345
'min_features': Int,
325346
'max_features': Int,
347+
'ids': List[Str],
326348
'metadata': Metadata,
327349
'where': Str,
328350
'exclude_ids': Bool,
@@ -345,27 +367,15 @@
345367
'have to be retained. If no value is provided '
346368
'this will default to infinity (i.e., no maximum '
347369
'feature filter will be applied).'),
348-
'metadata': 'Sample metadata used with `where` parameter when '
349-
'selecting samples to retain, or with `exclude_ids` '
350-
'when selecting samples to discard.',
351-
'where': 'SQLite WHERE clause specifying sample metadata criteria '
352-
'that must be met to be included in the filtered feature '
353-
'table. If not provided, all samples in `metadata` that are '
354-
'also in the feature table will be retained.',
355-
'exclude_ids': 'If true, the samples selected by `metadata` or '
356-
'`where` parameters will be excluded from the filtered '
357-
'table instead of being retained.',
370+
**_filter_parameter_descriptions,
358371
'filter_empty_features': 'If true, features which are not present in '
359372
'any retained samples are dropped.',
360-
'allow_empty_table': 'If true, the filtered table may be empty. '
361-
'Default behavior is to raise an error if the '
362-
'filtered table is empty.'
363373
},
364374
output_descriptions={
365375
'filtered_table': 'The resulting feature table filtered by sample.'
366376
},
367377
name="Filter samples from table",
368-
description="Filter samples from table based on frequency and/or "
378+
description="Filter samples from table based on frequency, IDs, and/or "
369379
"metadata. Any features with a frequency of zero after sample "
370380
"filtering will also be removed.",
371381
examples={
@@ -440,6 +450,7 @@
440450
'max_frequency': p_filter_features_max_frequency,
441451
'min_samples': Int,
442452
'max_samples': Int,
453+
'ids': List[Str],
443454
'metadata': Metadata,
444455
'where': Str,
445456
'exclude_ids': Bool,
@@ -465,27 +476,15 @@
465476
'be observed in to be retained. If no value is '
466477
'provided this will default to infinity (i.e., no '
467478
'maximum sample filter will be applied).'),
468-
'metadata': 'Feature metadata used with `where` parameter when '
469-
'selecting features to retain, or with `exclude_ids` '
470-
'when selecting features to discard.',
471-
'where': 'SQLite WHERE clause specifying feature metadata criteria '
472-
'that must be met to be included in the filtered feature '
473-
'table. If not provided, all features in `metadata` that are '
474-
'also in the feature table will be retained.',
475-
'exclude_ids': 'If true, the features selected by `metadata` or '
476-
'`where` parameters will be excluded from the filtered '
477-
'table instead of being retained.',
479+
**_filter_parameter_descriptions,
478480
'filter_empty_samples': 'If true, drop any samples where none of the '
479481
'retained features are present.',
480-
'allow_empty_table': 'If true, the filtered table may be empty. '
481-
'Default behavior is to raise an error if the '
482-
'filtered table is empty.'
483482
},
484483
output_descriptions={
485484
'filtered_table': 'The resulting feature table filtered by feature.'
486485
},
487486
name="Filter features from table",
488-
description="Filter features from table based on frequency and/or "
487+
description="Filter features from table based on frequency, IDs, and/or "
489488
"metadata. Any samples with a frequency of zero after feature "
490489
"filtering will also be removed.",
491490
examples={
@@ -662,13 +661,13 @@
662661

663662
plugin.methods.register_function(
664663
function=q2_feature_table.split,
665-
inputs={'table': FeatureTable[T1]},
664+
inputs={'table': FeatureTable[T4]},
666665
parameters={
667666
'metadata': MetadataColumn[Categorical],
668667
'filter_empty_features': Bool
669668
},
670669
outputs=[
671-
('tables', Collection[FeatureTable[T1]])
670+
('tables', Collection[FeatureTable[T4]])
672671
],
673672
input_descriptions={
674673
'table': 'The table to split.'
@@ -961,3 +960,42 @@
961960
),
962961
citations=[],
963962
)
963+
964+
plugin.methods.register_function(
965+
function=q2_feature_table.filter_ids,
966+
inputs={"table": FeatureTable[T4]},
967+
parameters={"axis": Str % Choices(["sample", "feature"]),
968+
"ids": List[Str],
969+
"metadata": Metadata,
970+
"where": Str,
971+
"exclude_ids": Bool,
972+
"allow_empty_table": Bool},
973+
outputs=[("filtered_table", FeatureTable[T4])],
974+
input_descriptions={
975+
"table": (
976+
"The feature table from which IDs should be filtered."
977+
)
978+
},
979+
parameter_descriptions={
980+
"axis": (
981+
"The axis to filter. Select 'sample' to filter sample IDs or "
982+
"'feature' to filter feature IDs."
983+
),
984+
**_filter_parameter_descriptions,
985+
},
986+
output_descriptions={
987+
"filtered_table": (
988+
"The feature table filtered by selected sample or feature IDs."
989+
)
990+
},
991+
name="Filter IDs from table",
992+
description=(
993+
"Filter samples or features from a table using an ID list or "
994+
"metadata selected with a SQLite WHERE clause."
995+
),
996+
examples={
997+
"filter_samples_by_metadata_and_where":
998+
ex.feature_table_filter_ids_samples_by_metadata_and_where,
999+
"filter_features_by_ids": ex.feature_table_filter_ids_features_by_ids,
1000+
},
1001+
)

0 commit comments

Comments
 (0)