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 ADDED
@@ -0,0 +1,122 @@
1
+ # treesumstats
2
+
3
+ Representing phylogenetic trees (or forests of such trees) as summary statistics.
4
+
5
+
6
+
7
+ [//]: # ([![DOI:10.1093/sysbio/syad059](https://zenodo.org/badge/DOI/10.1093/sysbio/syad059.svg)](https://doi.org/10.1093/sysbio/syad059))
8
+ [//]: # ([![GitHub release](https://img.shields.io/github/v/release/evolbioinfo/treesumstats.svg)](https://github.com/evolbioinfo/treesumstats/releases))
9
+ [![PyPI version](https://badge.fury.io/py/treesumstats.svg)](https://pypi.org/project/treesumstats/)
10
+ [![PyPI downloads](https://shields.io/pypi/dm/treesumstats)](https://pypi.org/project/treesumstats/)
11
+ [![Docker pulls](https://img.shields.io/docker/pulls/evolbioinfo/treesumstats)](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
+
@@ -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