Repository: qiyunzhu/woltka

Issue:
Woltka classify errors with tiny samples
I encountered an issue where samples with very few counts through `woltka classify`, which result in empty tables, trigger an error due to a call to numpy.vectorize. It would be great if this scenario produced an empty table instead of raising an error. The full traceback is provided below.

## Traceback

```python
Number of features before collapsing: 3.
Reading mapping file: orf-to-protein.map.xz... Done.
Traceback (most recent call last):
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/bin/woltka", line 10, in <module>
    sys.exit(cli())
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/woltka/cli.py", line 234, in collapse_cmd
    collapse_wf(**kwargs)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/woltka/tools.py", line 270, in collapse_wf
    round_table(table, digits or None)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/woltka/table.py", line 447, in round_table
    round_biom(table, digits)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/woltka/biom.py", line 195, in round_biom
    tmd.data = np.vectorize(f)(tmd.data).astype('float64')
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/numpy/lib/_function_base_impl.py", line 2470, in __ca
ll__
    return self._call_as_normal(*args, **kwargs)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/numpy/lib/_function_base_impl.py", line 2463, in _cal
l_as_normal
    return self._vectorize_call(func=func, args=vargs)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/numpy/lib/_function_base_impl.py", line 2548, in _vec
torize_call
    ufunc, otypes = self._get_ufunc_and_otypes(func=func, args=args)
  File "/scratch/conda-envs/7002191ebcbec84d5c12599c4ceeb789_/lib/python3.10/site-packages/numpy/lib/_function_base_impl.py", line 2504, in _get
_ufunc_and_otypes
    raise ValueError('cannot call `vectorize` on size 0 inputs '
ValueError: cannot call `vectorize` on size 0 inputs unless `otypes` is set
```

## Minimal example

Outside of woltka classify, I was able to trace this to Numpy erroring with an empty matrix [here](https://github.com/qiyunzhu/woltka/blob/main/woltka/biom.py#L195).

## minimal example

python
```
from woltka.biom import round_biom
import biom
x = biom.Table(data=[], observation_ids=[], sample_ids=[])
round_biom(x)
```

Hints from issue comments:

Solution patch:
diff --git a/woltka/biom.py b/woltka/biom.py
index 21fae89..55b12ad 100644
--- a/woltka/biom.py
+++ b/woltka/biom.py
@@ -192,7 +192,8 @@ def round_biom(table: biom.Table, digits=0):
             return round(num, digits)
 
     tmd = table.matrix_data
-    tmd.data = np.vectorize(f)(tmd.data).astype('float64')
+    if tmd.nnz:
+        tmd.data = np.vectorize(f)(tmd.data).astype('float64')
     tmd.eliminate_zeros()
     table.remove_empty(axis='observation')
 


Test patch:
diff --git a/woltka/tests/test_biom.py b/woltka/tests/test_biom.py
index 7911da3..d8983a9 100644
--- a/woltka/tests/test_biom.py
+++ b/woltka/tests/test_biom.py
@@ -206,6 +206,30 @@ class BiomTests(TestCase):
             'S2': {'G1': 1.55, 'G2': 0.17, 'G3': 1.5}})))
         self.assertBIOMEqual(obs, exp)
 
+    def test_round_biom_empty_table(self):
+        # https://github.com/qiyunzhu/woltka/issues/216
+        exp = Table(data=[], observation_ids=[], sample_ids=[])
+        round_biom(exp)
+        obs = Table(data=[], observation_ids=[], sample_ids=[])
+        self.assertBIOMEqual(obs, exp)
+
+    def test_round_biom_empty_table_with_labels(self):
+        # https://github.com/qiyunzhu/woltka/issues/216
+        observation_ids = ['G1', 'G2', 'G3']
+        sample_ids = ['S1', 'S2']
+        data = np.zeros((len(observation_ids), len(sample_ids)))
+
+        obs = Table(data, observation_ids=observation_ids,
+                    sample_ids=sample_ids)
+        round_biom(obs)
+
+        data = np.zeros((len(observation_ids), len(sample_ids)))
+        exp = Table(data, observation_ids=observation_ids,
+                    sample_ids=sample_ids)
+        exp.remove_empty(axis='observation')
+
+        self.assertBIOMEqual(obs, exp)
+
     def test_biom_add_metacol(self):
         obs = Table(*map(np.array, prep_table({
             'S1': {'G1': 4, 'G2': 5, 'G3': 8, 'G4': 0, 'G5': 3},