treesumstats 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- README.md +122 -0
- treesumstats/__init__.py +125 -0
- treesumstats/basic_sumstats.py +36 -0
- treesumstats/branch_sumstats.py +180 -0
- treesumstats/calculator.py +62 -0
- treesumstats/event_time_sumstats.py +68 -0
- treesumstats/ltt_sumstats.py +348 -0
- treesumstats/resolution_sumstats.py +100 -0
- treesumstats/subtree_sumstats.py +285 -0
- treesumstats/sumstats.py +111 -0
- treesumstats/topology_sumstats.py +335 -0
- treesumstats/transmission_chain_sumstats.py +95 -0
- treesumstats/tree_manager.py +308 -0
- treesumstats-0.1.dist-info/LICENSE +674 -0
- treesumstats-0.1.dist-info/METADATA +144 -0
- treesumstats-0.1.dist-info/RECORD +19 -0
- treesumstats-0.1.dist-info/WHEEL +5 -0
- treesumstats-0.1.dist-info/entry_points.txt +2 -0
- treesumstats-0.1.dist-info/top_level.txt +1 -0
README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# treesumstats
|
|
2
|
+
|
|
3
|
+
Representing phylogenetic trees (or forests of such trees) as summary statistics.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
[//]: # ([](https://doi.org/10.1093/sysbio/syad059))
|
|
8
|
+
[//]: # ([](https://github.com/evolbioinfo/treesumstats/releases))
|
|
9
|
+
[](https://pypi.org/project/treesumstats/)
|
|
10
|
+
[](https://pypi.org/project/treesumstats/)
|
|
11
|
+
[](https://hub.docker.com/r/evolbioinfo/treesumstats/tags)
|
|
12
|
+
|
|
13
|
+
## Implemented summary statistics
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Input data
|
|
18
|
+
One needs to supply a time-scaled phylogenetic tree in newick format.
|
|
19
|
+
If the file contains several trees (one tree per line), it will be considered as a forest.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
There are 4 alternative ways to run __treesumstats__ on your computer:
|
|
24
|
+
with [docker](https://www.docker.com/community-edition),
|
|
25
|
+
[apptainer](https://apptainer.org/),
|
|
26
|
+
in Python3, or via command line (requires installation with Python3).
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Run in python3 or command-line (for linux systems, recommended Ubuntu 21 or newer versions)
|
|
31
|
+
|
|
32
|
+
You could either install python (version 3.9 or higher) system-wide and then install treesumstats via pip:
|
|
33
|
+
```bash
|
|
34
|
+
sudo apt install -y python3 python3-pip python3-setuptools python3-distutils
|
|
35
|
+
pip3 install treesumstats
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
or alternatively, you could install python (version 3.9 or higher) and treesumstats via [conda](https://conda.io/docs/) (make sure that conda is installed first).
|
|
39
|
+
Here we will create a conda environment called _phyloenv_:
|
|
40
|
+
```bash
|
|
41
|
+
conda create --name phyloenv python=3.9
|
|
42
|
+
conda activate phyloenv
|
|
43
|
+
pip install treesumstats
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#### Basic usage in a command line
|
|
48
|
+
If you installed __treesumstats__ in a conda environment (here named _phyloenv_), do not forget to first activate it, e.g.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
conda activate phyloenv
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Run the following command to calculate the summary statistics for an input tree _tree.nwk_ and save them to an output tab-delimited file _stats.tab_.
|
|
55
|
+
(The non-mandatory option _--add_descriptions_ would add a column with statistics descriptions to the output table.)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
treesumstats_encode --nwk tree.nwk --tab stats.tab --add_descriptions
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Help
|
|
62
|
+
|
|
63
|
+
To see detailed options, run:
|
|
64
|
+
```bash
|
|
65
|
+
treesumstats_encode --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
### Run with docker
|
|
70
|
+
|
|
71
|
+
#### Basic usage
|
|
72
|
+
Once [docker](https://www.docker.com/community-edition) is installed,
|
|
73
|
+
|
|
74
|
+
run the following command to calculate the summary statistics for an input tree _tree.nwk_ and save them to an output tab-delimited file _stats.tab_.
|
|
75
|
+
(The non-mandatory option _--add_descriptions_ would add a column with statistics descriptions to the output table.)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
docker run -v <path_to_the_folder_containing_the_tree>:/data:rw -t evolbioinfo/treesumstats --nwk /data/tree.nwk --tab /data/stats.tab --add_descriptions
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
This will produce a tab-delimited file _stats.tab_ in the <path_to_the_folder_containing_the_tree> folder,
|
|
84
|
+
|
|
85
|
+
containing the summary statistic names in the _statistic_ column, their values in the _value_ column and their descriptions in the _description_ column.
|
|
86
|
+
|
|
87
|
+
#### Help
|
|
88
|
+
|
|
89
|
+
To see advanced options, run
|
|
90
|
+
```bash
|
|
91
|
+
docker run -t evolbioinfo/treesumstats -h
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
### Run with apptainer
|
|
97
|
+
|
|
98
|
+
#### Basic usage
|
|
99
|
+
Once [apptainer](https://apptainer.org/docs/user/latest/quick_start.html#installation) is installed,
|
|
100
|
+
|
|
101
|
+
run the following command to calculate the summary statistics for an input tree _tree.nwk_ and save them to an output tab-delimited file _stats.tab_.
|
|
102
|
+
(The non-mandatory option _--add_descriptions_ would add a column with statistics descriptions to the output table.)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
apptainer run docker://evolbioinfo/treesumstats --nwk tree.nwk --tab stats.tab --add_descriptions
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
This will produce a tab-delimited file _stats.tab_,
|
|
111
|
+
|
|
112
|
+
containing the summary statistic names in the _statistic_ column, their values in the _value_ column and their descriptions in the _description_ column.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#### Help
|
|
116
|
+
|
|
117
|
+
To see advanced options, run
|
|
118
|
+
```bash
|
|
119
|
+
apptainer run docker://evolbioinfo/treesumstats -h
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
treesumstats/__init__.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
MED = 'median'
|
|
7
|
+
|
|
8
|
+
VAR = 'var'
|
|
9
|
+
|
|
10
|
+
MEAN = 'mean'
|
|
11
|
+
|
|
12
|
+
MAX = 'max'
|
|
13
|
+
|
|
14
|
+
MIN = 'min'
|
|
15
|
+
|
|
16
|
+
EPSILON = 1e-6
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
BASIC_ARRAY_METRICS = [MEAN, MIN, MAX, VAR, MED]
|
|
20
|
+
|
|
21
|
+
BASIC_METRIC2HELP = {VAR: 'variance',
|
|
22
|
+
MIN: 'minimum',
|
|
23
|
+
MEAN: 'average',
|
|
24
|
+
MED: 'median',
|
|
25
|
+
MAX: 'maximum'}
|
|
26
|
+
|
|
27
|
+
BASIC_METRIC2FUN = {VAR: np.var,
|
|
28
|
+
MIN: np.min,
|
|
29
|
+
MEAN: np.mean,
|
|
30
|
+
MED: np.median,
|
|
31
|
+
MAX: np.max}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
NORMALIZED = 'normalized'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FeatureCalculator(ABC):
|
|
38
|
+
"""Abstract base class for feature calculators."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def feature_names(self):
|
|
42
|
+
"""Return a list of feature names this class can compute."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def set_forest(self, forest, **kwargs):
|
|
47
|
+
"""Sets the forest on which the features are to be calculated"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def calculate(self, forest, feature_name, **kwargs):
|
|
52
|
+
"""Compute a specific feature or features."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def help(self, feature_name, *args, **kwargs):
|
|
57
|
+
"""Shows the definition of the specific feature."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FeatureRegistry:
|
|
62
|
+
"""Registry for mapping feature names to their respective calculator classes."""
|
|
63
|
+
|
|
64
|
+
_registry = {}
|
|
65
|
+
_calculators = []
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def register(cls, calculator):
|
|
69
|
+
"""Register a feature calculator class and map its features."""
|
|
70
|
+
cls._calculators.append(calculator)
|
|
71
|
+
for feature in calculator.feature_names():
|
|
72
|
+
cls._registry[feature] = calculator
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def get_calculator(cls, feature_name):
|
|
76
|
+
"""Retrieve the appropriate calculator class for a given feature."""
|
|
77
|
+
return cls._registry.get(feature_name)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def list_features(cls):
|
|
81
|
+
return list(cls._registry.keys())
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def list_calculators(cls):
|
|
85
|
+
return list(cls._calculators)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FeatureManager:
|
|
89
|
+
"""Manages feature computation dynamically based on user input."""
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def compute_features(cls, forest, *feature_list, **kwargs):
|
|
93
|
+
"""Compute and return requested features."""
|
|
94
|
+
|
|
95
|
+
for calc in FeatureRegistry.list_calculators():
|
|
96
|
+
calc.set_forest(forest, **kwargs)
|
|
97
|
+
|
|
98
|
+
if not feature_list:
|
|
99
|
+
feature_list = FeatureManager.available_features()
|
|
100
|
+
|
|
101
|
+
for feature in feature_list:
|
|
102
|
+
calculator = FeatureRegistry.get_calculator(feature)
|
|
103
|
+
if calculator:
|
|
104
|
+
yield calculator.calculate(feature, **kwargs)
|
|
105
|
+
else:
|
|
106
|
+
warnings.warn(f'Did not find a Feature Calculator for feature {feature}')
|
|
107
|
+
yield None
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def available_features(cls):
|
|
111
|
+
return FeatureRegistry.list_features()
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def help(cls, feature_name, *args, **kwargs):
|
|
115
|
+
"""Returns feature description"""
|
|
116
|
+
|
|
117
|
+
calculator = FeatureRegistry.get_calculator(feature_name)
|
|
118
|
+
if calculator:
|
|
119
|
+
return calculator.help(feature_name, **kwargs)
|
|
120
|
+
else:
|
|
121
|
+
warnings.warn(f'Did not find a Feature Calculator for feature {feature_name}')
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from treesumstats import FeatureCalculator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BasicFeatureCalculator(FeatureCalculator):
|
|
5
|
+
"""Computes tree height-related summary statistics."""
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self._forest = None
|
|
9
|
+
|
|
10
|
+
def feature_names(self):
|
|
11
|
+
return ['n_trees', 'n_tips', 'n_inodes', 'len_forest']
|
|
12
|
+
|
|
13
|
+
def set_forest(self, forest, **kwargs):
|
|
14
|
+
self._forest = forest
|
|
15
|
+
|
|
16
|
+
def calculate(self, feature_name, **kwargs):
|
|
17
|
+
if 'n_trees' == feature_name:
|
|
18
|
+
return len(self._forest)
|
|
19
|
+
if 'n_tips' == feature_name:
|
|
20
|
+
return sum(len(tree) for tree in self._forest)
|
|
21
|
+
if 'n_inodes' == feature_name:
|
|
22
|
+
return sum(1 for tree in self._forest for n in tree.traverse() if not n.is_leaf())
|
|
23
|
+
if 'len_forest' == feature_name:
|
|
24
|
+
return sum(_.dist for tree in self._forest for _ in tree.traverse())
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def help(self, feature_name, *args, **kwargs):
|
|
28
|
+
if 'n_trees' == feature_name:
|
|
29
|
+
return 'number of trees in the forest.'
|
|
30
|
+
if 'n_tips' == feature_name:
|
|
31
|
+
return 'number of tips in the forest.'
|
|
32
|
+
if 'n_inodes' == feature_name:
|
|
33
|
+
return 'number of internal nodes in the forest.'
|
|
34
|
+
if 'len_forest' == feature_name:
|
|
35
|
+
return 'sum of forest branch lengths.'
|
|
36
|
+
return None
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from treesumstats import BASIC_METRIC2HELP, BASIC_METRIC2FUN, FeatureCalculator, MEAN, MED, VAR, EPSILON
|
|
2
|
+
from treesumstats.tree_manager import annotate_forest_with_time, TIME
|
|
3
|
+
|
|
4
|
+
BRLEN = 'brlen'
|
|
5
|
+
|
|
6
|
+
class BranchFeatureCalculator(FeatureCalculator):
|
|
7
|
+
"""Computes tree branch-related summary statistics."""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._edists_top = None
|
|
11
|
+
self._idists_top = None
|
|
12
|
+
self._edists_bottom = None
|
|
13
|
+
self._idists_bottom = None
|
|
14
|
+
self._edists_middle = None
|
|
15
|
+
self._idists_middle = None
|
|
16
|
+
self._max_time = None
|
|
17
|
+
self._idists = None
|
|
18
|
+
self._edists = None
|
|
19
|
+
self._forest = None
|
|
20
|
+
|
|
21
|
+
def feature_names(self):
|
|
22
|
+
return [f'{BRLEN}_inode_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
23
|
+
+ [f'{BRLEN}_tip_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
24
|
+
+ [f'{BRLEN}_inode_top_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
25
|
+
+ [f'{BRLEN}_tip_top_{flavour}' for flavour in[MEAN, MED, VAR]]\
|
|
26
|
+
+ [f'{BRLEN}_inode_middle_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
27
|
+
+ [f'{BRLEN}_tip_middle_{flavour}' for flavour in[MEAN, MED, VAR]] \
|
|
28
|
+
+ [f'{BRLEN}_inode_bottom_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
29
|
+
+ [f'{BRLEN}_tip_bottom_{flavour}' for flavour in[MEAN, MED, VAR]] \
|
|
30
|
+
+ [f'frac_{BRLEN}_inode_{flavour}_by_{BRLEN}_tip_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
31
|
+
+ [f'frac_{BRLEN}_inode_top_{flavour}_by_{BRLEN}_tip_top_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
32
|
+
+ [f'frac_{BRLEN}_inode_middle_{flavour}_by_{BRLEN}_tip_middle_{flavour}' for flavour in [MEAN, MED, VAR]] \
|
|
33
|
+
+ [f'frac_{BRLEN}_inode_bottom_{flavour}_by_{BRLEN}_tip_bottom_{flavour}' for flavour in [MEAN, MED, VAR]]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_forest(self, forest, **kwargs):
|
|
37
|
+
self._forest = forest
|
|
38
|
+
annotate_forest_with_time(self._forest)
|
|
39
|
+
self._idists = None
|
|
40
|
+
self._edists = None
|
|
41
|
+
self._edists_top = None
|
|
42
|
+
self._idists_top = None
|
|
43
|
+
self._edists_middle = None
|
|
44
|
+
self._idists_middle = None
|
|
45
|
+
self._edists_bottom = None
|
|
46
|
+
self._idists_bottom = None
|
|
47
|
+
self._max_time = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def max_time(self):
|
|
52
|
+
if self._max_time is None:
|
|
53
|
+
self._max_time = max(getattr(_, TIME) for tree in self._forest for _ in tree)
|
|
54
|
+
return self._max_time
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def idists(self):
|
|
58
|
+
if self._idists is None:
|
|
59
|
+
self._idists, self._edists = self._get_branch_lengths()
|
|
60
|
+
return self._idists
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def edists(self):
|
|
64
|
+
if self._edists is None:
|
|
65
|
+
self._idists, self._edists = self._get_branch_lengths()
|
|
66
|
+
return self._edists
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def idists_top(self):
|
|
70
|
+
if self._idists_top is None:
|
|
71
|
+
|
|
72
|
+
def time_filter(nd):
|
|
73
|
+
return getattr(nd, TIME) <= self.max_time / 3
|
|
74
|
+
|
|
75
|
+
self._idists_top, self._edists_top = self._get_branch_lengths(time_filter)
|
|
76
|
+
return self._idists_top
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def edists_top(self):
|
|
80
|
+
if self._edists_top is None:
|
|
81
|
+
|
|
82
|
+
def time_filter(nd):
|
|
83
|
+
return getattr(nd, TIME) <= self.max_time / 3
|
|
84
|
+
|
|
85
|
+
self._idists_top, self._edists_top = self._get_branch_lengths(time_filter)
|
|
86
|
+
return self._edists_top
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def idists_middle(self):
|
|
90
|
+
if self._idists_middle is None:
|
|
91
|
+
|
|
92
|
+
def time_filter(nd):
|
|
93
|
+
return (getattr(nd, TIME) <= 2 * self.max_time / 3) \
|
|
94
|
+
and (self.max_time / 3 < (getattr(nd, TIME) - nd.dist))
|
|
95
|
+
|
|
96
|
+
self._idists_middle, self._edists_middle = self._get_branch_lengths(time_filter)
|
|
97
|
+
return self._idists_middle
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def edists_middle(self):
|
|
101
|
+
if self._edists_middle is None:
|
|
102
|
+
|
|
103
|
+
def time_filter(nd):
|
|
104
|
+
return (getattr(nd, TIME) <= 2 * self.max_time / 3) \
|
|
105
|
+
and (self.max_time / 3 < (getattr(nd, TIME) - nd.dist))
|
|
106
|
+
|
|
107
|
+
self._idists_middle, self._edists_middle = self._get_branch_lengths(time_filter)
|
|
108
|
+
return self._edists_middle
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def idists_bottom(self):
|
|
112
|
+
if self._idists_bottom is None:
|
|
113
|
+
|
|
114
|
+
def time_filter(nd):
|
|
115
|
+
return 2 * self.max_time / 3 < (getattr(nd, TIME) - nd.dist)
|
|
116
|
+
|
|
117
|
+
self._idists_bottom, self._edists_bottom = self._get_branch_lengths(time_filter)
|
|
118
|
+
return self._idists_bottom
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def edists_bottom(self):
|
|
122
|
+
if self._edists is None:
|
|
123
|
+
|
|
124
|
+
def time_filter(nd):
|
|
125
|
+
return 2 * self.max_time / 3 < (getattr(nd, TIME) - nd.dist)
|
|
126
|
+
|
|
127
|
+
self._idists_bottom, self._edists_bottom = self._get_branch_lengths(time_filter)
|
|
128
|
+
return self._edists_bottom
|
|
129
|
+
|
|
130
|
+
def _get_branch_lengths(self, node_filter=lambda nd: True):
|
|
131
|
+
internal_dists, external_dists = [], []
|
|
132
|
+
|
|
133
|
+
for tree in self._forest:
|
|
134
|
+
for n in tree.traverse():
|
|
135
|
+
if not node_filter(n):
|
|
136
|
+
continue
|
|
137
|
+
if n.is_leaf():
|
|
138
|
+
external_dists.append(n.dist)
|
|
139
|
+
elif not n.is_root():
|
|
140
|
+
internal_dists.append(n.dist)
|
|
141
|
+
return internal_dists, external_dists
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def calculate(self, feature_name, **kwargs):
|
|
145
|
+
if feature_name.startswith(BRLEN):
|
|
146
|
+
is_internal = 'inode' in feature_name
|
|
147
|
+
flavour = feature_name.split('_')[-1]
|
|
148
|
+
if is_internal:
|
|
149
|
+
data = self.idists_top if 'top' in feature_name \
|
|
150
|
+
else (self.idists_middle if 'middle' in feature_name \
|
|
151
|
+
else (self.idists_bottom if 'bottom' in feature_name else self.idists))
|
|
152
|
+
else:
|
|
153
|
+
data = self.edists_top if 'top' in feature_name \
|
|
154
|
+
else (self.edists_middle if 'middle' in feature_name \
|
|
155
|
+
else (self.edists_bottom if 'bottom' in feature_name else self.edists))
|
|
156
|
+
return BASIC_METRIC2FUN[flavour](data) if len(data) else 0
|
|
157
|
+
if feature_name.startswith('frac'):
|
|
158
|
+
feature_name_1, feature_name_2 = feature_name[len('frac_'): ].split('_by_')
|
|
159
|
+
val1, val2 = self.calculate(feature_name_1), self.calculate(feature_name_2)
|
|
160
|
+
return val1 / max(val2, EPSILON)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def help(self, feature_name, *args, **kwargs):
|
|
164
|
+
if feature_name.startswith(BRLEN):
|
|
165
|
+
is_internal = 'inode' in feature_name
|
|
166
|
+
flavour = feature_name.split('_')[-1]
|
|
167
|
+
res = f'{BASIC_METRIC2HELP[flavour]} ' \
|
|
168
|
+
f'of the lengths of the forest {'non-root internal nodes' if is_internal else 'tips'}.'
|
|
169
|
+
part = 'top' if 'top' in feature_name \
|
|
170
|
+
else ('middle' if 'middle' in feature_name else ('bottom' if 'bottom' in feature_name else ''))
|
|
171
|
+
if part:
|
|
172
|
+
res += f' Only branches starting and finishing in the {part} third of the forest are considered. ' \
|
|
173
|
+
f'(The total time between the earliest forest tree start and the last sampled tip '\
|
|
174
|
+
'is split into three equal parts.)'
|
|
175
|
+
return res
|
|
176
|
+
if feature_name.startswith('frac'):
|
|
177
|
+
feature_name_1, feature_name_2 = feature_name[len('frac_'):].split('_by_')
|
|
178
|
+
return f'fraction of two values. The dividend is {self.help(feature_name_1).split('.')[0]}. '\
|
|
179
|
+
f'The divisor is {self.help(feature_name_1)}'
|
|
180
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pandas.core.computation.ops import isnumeric
|
|
2
|
+
|
|
3
|
+
from treesumstats.basic_sumstats import BasicFeatureCalculator
|
|
4
|
+
from treesumstats.branch_sumstats import BranchFeatureCalculator
|
|
5
|
+
from treesumstats.event_time_sumstats import EventTimeFeatureCalculator
|
|
6
|
+
from treesumstats.ltt_sumstats import LTTFeatureCalculator
|
|
7
|
+
from treesumstats.subtree_sumstats import SubtreeFeatureCalculator
|
|
8
|
+
from treesumstats.topology_sumstats import TopologyFeatureCalculator
|
|
9
|
+
from treesumstats import FeatureRegistry, FeatureManager
|
|
10
|
+
from treesumstats.resolution_sumstats import ResolutionFeatureCalculator
|
|
11
|
+
from treesumstats.transmission_chain_sumstats import TransmissionChainFeatureCalculator
|
|
12
|
+
from treesumstats.tree_manager import read_forest
|
|
13
|
+
|
|
14
|
+
FeatureRegistry.register(ResolutionFeatureCalculator())
|
|
15
|
+
FeatureRegistry.register(TopologyFeatureCalculator())
|
|
16
|
+
FeatureRegistry.register(EventTimeFeatureCalculator())
|
|
17
|
+
FeatureRegistry.register(BasicFeatureCalculator())
|
|
18
|
+
FeatureRegistry.register(BranchFeatureCalculator())
|
|
19
|
+
FeatureRegistry.register(TransmissionChainFeatureCalculator())
|
|
20
|
+
FeatureRegistry.register(LTTFeatureCalculator())
|
|
21
|
+
FeatureRegistry.register(SubtreeFeatureCalculator())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
"""
|
|
27
|
+
Entry point for tree encoding with command-line arguments.
|
|
28
|
+
:return: void
|
|
29
|
+
"""
|
|
30
|
+
import argparse
|
|
31
|
+
|
|
32
|
+
parser = argparse.ArgumentParser(description="Calculate summary statistics for an input tree/forest.")
|
|
33
|
+
parser.add_argument('--nwk', type=str, required=True, help="input tree/forest files in newick format")
|
|
34
|
+
parser.add_argument('--tab', type=str, required=True, help="output summary statistic table, in tab-delimited format: the first column will contain summary statistic name, and the second its value.")
|
|
35
|
+
parser.add_argument('--statistics', type=str, nargs='*', help='an optional list of summary statistics that should be calculated. If not given all the available statistics will be calculated.')
|
|
36
|
+
parser.add_argument('--add_descriptions', action='store_true', help='whether to add a column "description" with the description of each calculated statistic.')
|
|
37
|
+
params = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
forest = read_forest(params.nwk)
|
|
40
|
+
|
|
41
|
+
features = params.statistics if params.statistics else FeatureManager.available_features()
|
|
42
|
+
|
|
43
|
+
def format_value(value):
|
|
44
|
+
try:
|
|
45
|
+
value = float(value)
|
|
46
|
+
if int(value) == value:
|
|
47
|
+
return f'{value:.0f}'
|
|
48
|
+
if value > 1e-6:
|
|
49
|
+
return f'{value:.6f}'
|
|
50
|
+
return f'{value:.g}'
|
|
51
|
+
except:
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
with open(params.tab, 'w+') as f:
|
|
56
|
+
f.write(f'statistic\tvalue{'\tdescription' if params.add_definitions else ''}\n')
|
|
57
|
+
for feature_name, value in zip(features, FeatureManager.compute_features(forest)):
|
|
58
|
+
f.write(f'{feature_name}\t{format_value(value)}{f"\t{FeatureManager.help(feature_name)}" if params.add_definitions else ""}\n')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if '__main__' == __name__:
|
|
62
|
+
main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from treesumstats import BASIC_METRIC2HELP, BASIC_ARRAY_METRICS, BASIC_METRIC2FUN, FeatureCalculator, NORMALIZED
|
|
4
|
+
from treesumstats.tree_manager import annotate_forest_with_time, TIME
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventTimeFeatureCalculator(FeatureCalculator):
|
|
8
|
+
"""Computes tree event time-related summary statistics."""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._max_time = None
|
|
12
|
+
self._tip_times = None
|
|
13
|
+
self._forest = None
|
|
14
|
+
self._inode_times = None
|
|
15
|
+
|
|
16
|
+
def feature_names(self):
|
|
17
|
+
return [f'{TIME}_tip_{flavour}' for flavour in BASIC_ARRAY_METRICS] \
|
|
18
|
+
+ [f'{TIME}_inode_{flavour}' for flavour in BASIC_ARRAY_METRICS] \
|
|
19
|
+
+ [f'{TIME}_tip_{NORMALIZED}_{flavour}' for flavour in BASIC_ARRAY_METRICS] \
|
|
20
|
+
+ [f'{TIME}_inode_{NORMALIZED}_{flavour}' for flavour in BASIC_ARRAY_METRICS]
|
|
21
|
+
|
|
22
|
+
def set_forest(self, forest, **kwargs):
|
|
23
|
+
self._forest = forest
|
|
24
|
+
annotate_forest_with_time(self._forest)
|
|
25
|
+
self._tip_times = None
|
|
26
|
+
self._inode_times = None
|
|
27
|
+
self._max_time = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def tip_times(self):
|
|
31
|
+
if self._tip_times is None:
|
|
32
|
+
self._tip_times = np.array([getattr(t, TIME) for tree in self._forest for t in tree])
|
|
33
|
+
return self._tip_times
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def max_time(self):
|
|
37
|
+
if self._max_time is None:
|
|
38
|
+
self._max_time = np.max(self.tip_times)
|
|
39
|
+
return self._max_time
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def inode_times(self):
|
|
43
|
+
if self._inode_times is None:
|
|
44
|
+
self._inode_times \
|
|
45
|
+
= np.array([getattr(n, TIME) for tree in self._forest for n in tree.traverse() if not n.is_leaf()])
|
|
46
|
+
return self._inode_times
|
|
47
|
+
|
|
48
|
+
def calculate(self, feature_name, **kwargs):
|
|
49
|
+
if feature_name.startswith(TIME):
|
|
50
|
+
is_internal = 'inode' in feature_name
|
|
51
|
+
is_normalized = NORMALIZED in feature_name
|
|
52
|
+
flavour = feature_name.split('_')[-1]
|
|
53
|
+
return BASIC_METRIC2FUN[flavour]((self.inode_times if is_internal else self.tip_times)
|
|
54
|
+
/ (self.max_time if is_normalized else 1))
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def help(self, feature_name, *args, **kwargs):
|
|
59
|
+
if feature_name.startswith(TIME):
|
|
60
|
+
is_internal = 'inode' in feature_name
|
|
61
|
+
node = 'internal node' if is_internal else 'tip'
|
|
62
|
+
is_normalized = NORMALIZED in feature_name
|
|
63
|
+
flavour = feature_name.split('_')[-1]
|
|
64
|
+
res = f'{BASIC_METRIC2HELP[flavour]} of the sampling times of forest {node}s.'
|
|
65
|
+
if is_normalized:
|
|
66
|
+
res += ' We normalize the sampling times by dividing them by the last tip\'s sampling time.'
|
|
67
|
+
return res
|
|
68
|
+
return None
|