bowdbeg commited on
Commit
e391132
1 Parent(s): aafacb6
Files changed (3) hide show
  1. README.md +27 -10
  2. matching_series.py +113 -27
  3. plot_cuc.py +24 -0
README.md CHANGED
@@ -28,30 +28,47 @@ At minium, the metric requires the original time-series and the generated time-s
28
  >>> metric = evaluate.load("bowdbeg/matching_series")
29
  >>> results = metric.compute(references=references, predictions=predictions, batch_size=1000)
30
  >>> print(results)
31
- {'matching_mse': 0.15873331613053895, 'harmonic_mean': 0.15623569099681772, 'covered_mse': 0.15381544718035087, 'index_mse': 0.16636189201532087, 'matching_mse_features': [0.13739837269222452, 0.1395309409295018, 0.13677679887355126, 0.14408421162706211, 0.1430115910456261, 0.13726657544044085, 0.14274372684301717, 0.13504614539190338, 0.13853582796877975, 0.14482307626368343], 'harmonic_mean_features': [0.1309991815519093, 0.13157175020534279, 0.12735134531950718, 0.1327483317911355, 0.1336402851605765, 0.12878380179856022, 0.1344831997941457, 0.12782689483798823, 0.12909420446395195, 0.13417435670997752], 'covered_mse_features': [0.12516953618356524, 0.12447158260731798, 0.11914118322950448, 0.12306606276504639, 0.1254216201001874, 0.12128844181049621, 0.12712643943219143, 0.12134032531607968, 0.12085741660832867, 0.12498436126166071], 'index_mse_features': [0.16968036010688156, 0.1624888691672768, 0.15926142198600082, 0.17250634507748022, 0.16713668302081525, 0.16663213728264645, 0.1596766027744231, 0.16251306560725656, 0.17160303243460656, 0.17212040269582168], 'macro_matching_mse': 0.13992172670757905, 'macro_covered_mse': 0.12328669693143782, 'macro_harmonic_mean': 0.13106733516330948, 'macro_index_mse': 0.1663618920153209}
32
  ```
33
 
34
  ### Inputs
35
  - **predictions**: (list of list of list of float or numpy.ndarray): The generated time-series. The shape of the array should be `(num_generation, seq_len, num_features)`.
36
  - **references**: (list of list of list of float or numpy.ndarray): The original time-series. The shape of the array should be `(num_reference, seq_len, num_features)`.
37
  - **batch_size**: (int, optional): The batch size for computing the metric. This affects quadratically. Default is None.
 
 
38
 
39
  ### Output Values
40
 
41
  Let prediction instances be $P = \{p_1, p_2, \ldots, p_n\}$ and reference instances be $R = \{r_1, r_2, \ldots, r_m\}$.
42
 
43
- - **matching_mse**: (float): Average of the MSE between the generated instance and the reference instance with the lowest MSE. Intuitively, This is similar to precision in classification. In the equation, $\frac{1}{n} \sum_{i=1}^{n} \min_{j} \mathrm{MSE}(p_i, r_j)$.
44
- - **covered_mse**: (float): Average of the MSE between the reference instance and the with the lowest MSE. Intuitively, This is similar to recall in classification. In the equation, $\frac{1}{m} \sum_{j=1}^{m} \min_{i} \mathrm{MSE}(p_i, r_j)$.
45
- - **harmonic_mean**: (float): Harmonic mean of the matching_mse and covered_mse. This is similar to F1-score in classification.
46
  - **index_mse**: (float): Average of the MSE between the generated instance and the reference instance with the same index. In the equation, $\frac{1}{n} \sum_{i=1}^{n} \mathrm{MSE}(p_i, r_i)$.
47
- - **matching_mse_features**: (list of float): matching_mse computed individually for each feature.
48
- - **covered_mse_features**: (list of float): covered_mse computed individually for each feature.
49
- - **harmonic_mean_features**: (list of float): harmonic_mean computed individually for each feature.
50
  - **index_mse_features**: (list of float): index_mse computed individually for each feature.
51
- - **macro_matching_mse**: (float): Average of the matching_mse_features.
52
- - **macro_covered_mse**: (float): Average of the covered_mse_features.
53
- - **macro_harmonic_mean**: (float): Average of the harmonic_mean_features.
54
  - **macro_index_mse**: (float): Average of the index_mse_features.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  #### Values from Popular Papers
57
  <!-- *Give examples, preferrably with links to leaderboards or publications, to papers that have reported this metric, along with the values they have reported.* -->
 
28
  >>> metric = evaluate.load("bowdbeg/matching_series")
29
  >>> results = metric.compute(references=references, predictions=predictions, batch_size=1000)
30
  >>> print(results)
31
+ {'precision_mse': 0.15642462680824154, 'f1_mse': 0.15423970232736145, 'recall_mse': 0.15211497466247828, 'index_mse': 0.1650527529752939, 'precision_mse_features': [0.14161461272391063, 0.13959801451122986, 0.13494790079336152, 0.13812467072775822, 0.13502155933085397, 0.13773603530687478, 0.13782869677371534, 0.13880373566781345, 0.1347356979110729, 0.1380613227954152], 'f1_mse_features': [0.13200523240237663, 0.1321561699583367, 0.12686344486378406, 0.12979789457435542, 0.12768556637792927, 0.1316950291866994, 0.12937893459231917, 0.13052145628415104, 0.12571029554640592, 0.12686388502130683], 'recall_mse_features': [0.12361708937664843, 0.1254676048318782, 0.11969288602958734, 0.12241798787954035, 0.12110565263179066, 0.12616166677071738, 0.12190537193383513, 0.1231719120998892, 0.1178181328089802, 0.11734651764610313], 'index_mse_features': [0.16728853331521837, 0.1673468681819004, 0.16940025907048203, 0.16828093040638223, 0.17486439883284577, 0.15779474562305962, 0.16255301663470148, 0.16224400164732194, 0.1531092505944622, 0.167645525446565], 'macro_precision_mse': 0.1376472246542006, 'macro_recall_mse': 0.121870482200897, 'macro_f1_mse': 0.12926779088076645, 'macro_index_mse': 0.1650527529752939, 'matching_precision': 0.09, 'matching_recall': 1.0, 'matching_f1': 0.1651376146788991, 'matching_precision_features': [0.1, 0.1, 0.1, 0.1, 0.09, 0.09, 0.1, 0.1, 0.1, 0.1], 'matching_recall_features': [1.0, 1.0, 1.0, 0.7, 0.9, 1.0, 0.9, 1.0, 0.9, 0.8], 'matching_f1_features': [0.18181818181818182, 0.18181818181818182, 0.18181818181818182, 0.175, 0.16363636363636364, 0.1651376146788991, 0.18, 0.18181818181818182, 0.18, 0.17777777777777778], 'macro_matching_precision': 0.098, 'macro_matching_recall': 0.92, 'macro_matching_f1': 0.1768824483365768, 'cuc': 0.1364, 'coverages': [0.10000000000000002, 0.16666666666666666, 0.3, 0.5333333333333333, 0.9], 'macro_cuc': 0.13874, 'macro_coverages': [0.10000000000000002, 0.18000000000000002, 0.31, 0.48, 0.98], 'cuc_features': [0.1428, 0.13580000000000003, 0.15250000000000002, 0.14579999999999999, 0.12990000000000002, 0.1364, 0.1459, 0.12330000000000002, 0.13580000000000003, 0.13920000000000002], 'coverages_features': [[0.10000000000000002, 0.16666666666666666, 0.3666666666666667, 0.5, 1.0], [0.10000000000000002, 0.16666666666666666, 0.26666666666666666, 0.43333333333333335, 1.0], [0.10000000000000002, 0.20000000000000004, 0.3666666666666667, 0.6, 1.0], [0.10000000000000002, 0.16666666666666666, 0.3333333333333333, 0.5333333333333333, 1.0], [0.10000000000000002, 0.20000000000000004, 0.26666666666666666, 0.4666666666666666, 0.9], [0.10000000000000002, 0.16666666666666666, 0.30000000000000004, 0.5333333333333333, 0.9], [0.10000000000000002, 0.20000000000000004, 0.3333333333333333, 0.5333333333333333, 1.0], [0.10000000000000002, 0.20000000000000004, 0.3, 0.3, 1.0], [0.10000000000000002, 0.16666666666666666, 0.26666666666666666, 0.4333333333333333, 1.0], [0.10000000000000002, 0.16666666666666666, 0.30000000000000004, 0.4666666666666666, 1.0]]}
32
  ```
33
 
34
  ### Inputs
35
  - **predictions**: (list of list of list of float or numpy.ndarray): The generated time-series. The shape of the array should be `(num_generation, seq_len, num_features)`.
36
  - **references**: (list of list of list of float or numpy.ndarray): The original time-series. The shape of the array should be `(num_reference, seq_len, num_features)`.
37
  - **batch_size**: (int, optional): The batch size for computing the metric. This affects quadratically. Default is None.
38
+ - **cuc_n_calculation**: (int, optional): The number of samples to compute the coverage because sampling exists. Default is 3.
39
+ - **cuc_n_samples**: (list of int, optional): The number of samples to compute the coverage. Default is $[2^i \text{for} i \leq \log_2 n] + [n]$.
40
 
41
  ### Output Values
42
 
43
  Let prediction instances be $P = \{p_1, p_2, \ldots, p_n\}$ and reference instances be $R = \{r_1, r_2, \ldots, r_m\}$.
44
 
45
+ - **precision_mse**: (float): Average of the MSE between the generated instance and the reference instance with the lowest MSE. Intuitively, this is similar to precision in classification. In the equation, $\frac{1}{n} \sum_{i=1}^{n} \min_{j} \mathrm{MSE}(p_i, r_j)$.
46
+ - **recall_mse**: (float): Average of the MSE between the reference instance and the with the lowest MSE. Intuitively, this is similar to recall in classification. In the equation, $\frac{1}{m} \sum_{j=1}^{m} \min_{i} \mathrm{MSE}(p_i, r_j)$.
47
+ - **f1_mse**: (float): Harmonic mean of the precision_mse and recall_mse. This is similar to F1-score in classification.
48
  - **index_mse**: (float): Average of the MSE between the generated instance and the reference instance with the same index. In the equation, $\frac{1}{n} \sum_{i=1}^{n} \mathrm{MSE}(p_i, r_i)$.
49
+ - **precision_mse_features**: (list of float): precision_mse computed individually for each feature.
50
+ - **recall_mse_features**: (list of float): recall_mse computed individually for each feature.
51
+ - **f1_mse_features**: (list of float): f1_mse computed individually for each feature.
52
  - **index_mse_features**: (list of float): index_mse computed individually for each feature.
53
+ - **macro_precision_mse**: (float): Average of the precision_mse_features.
54
+ - **macro_recall_mse**: (float): Average of the recall_mse_features.
55
+ - **macro_f1_mse**: (float): Average of the f1_mse_features.
56
  - **macro_index_mse**: (float): Average of the index_mse_features.
57
+ - **matching_precision**: (float): Precision of the matching instances. In the equation, $\frac{ | \{i | \min_{i} \mathrm{MSE}(p_i, r_j)\} | }{m}$.
58
+ - **matching_recall**: (float): Recall of the matching instances. In the equation, $\frac{ | \{j | \min_{j} \mathrm{MSE}(p_i, r_j)\} | }{n}$.
59
+ - **matching_f1**: (float): F1-score of the matching instances.
60
+ - **matching_precision_features**: (list of float): matching_precision computed individually for each feature.
61
+ - **matching_recall_features**: (list of float): matching_recall computed individually for each feature.
62
+ - **matching_f1_features**: (list of float): matching_f1 computed individually for each feature.
63
+ - **macro_matching_precision**: (float): Average of the matching_precision_features.
64
+ - **macro_matching_recall**: (float): Average of the matching_recall_features.
65
+ - **macro_matching_f1**: (float): Average of the matching_f1_features.
66
+ - **coverages**: (list of float): Coverage of the matching instances computed on the sampled generated data in cuc_n_samples. In the equation, $[\frac{ | \{ j | \min_{j} \mathrm{MSE}(p_i, r_j) \text{where}~p_i \in \mathrm{sample}(P, \mathrm{n\_sample}) \} | }{m} \text{for}~\mathrm{n\_sample} \in \mathrm{cuc\_n\_samples} ]$.
67
+ - **cuc**: (float): Coverage of the matching instances. In the equation, $\frac{ | \{i | \min_{i} \mathrm{MSE}(p_i, r_j) < \mathrm{threshold}\} | }{n}$.
68
+ - **coverages_features**: (list of list of float): coverages computed individually for each feature.
69
+ - **cuc_features**: (list of float): cuc computed individually for each feature.
70
+ - **macro_coverages**: (list of float): Average of the coverages_features.
71
+ - **macro_cuc**: (float): Average of the cuc_features.
72
 
73
  #### Values from Popular Papers
74
  <!-- *Give examples, preferrably with links to leaderboards or publications, to papers that have reported this metric, along with the values they have reported.* -->
matching_series.py CHANGED
@@ -13,8 +13,9 @@
13
  # limitations under the License.
14
  """TODO: Add a description here."""
15
 
 
16
  import statistics
17
- from typing import Optional, Union
18
 
19
  import datasets
20
  import evaluate
@@ -127,9 +128,11 @@ class matching_series(evaluate.Metric):
127
 
128
  def _compute(
129
  self,
130
- predictions: Union[list, np.ndarray],
131
- references: Union[list, np.ndarray],
132
  batch_size: Optional[int] = None,
 
 
133
  ):
134
  """
135
  Compute the scores of the module given the predictions and references
@@ -139,6 +142,8 @@ class matching_series(evaluate.Metric):
139
  references: list of reference
140
  shape: (num_reference, num_timesteps, num_features)
141
  batch_size: batch size to use for the computation. If None, the whole dataset is processed at once.
 
 
142
  Returns:
143
  """
144
  predictions = np.array(predictions)
@@ -175,47 +180,128 @@ class matching_series(evaluate.Metric):
175
 
176
  # matching mse
177
  # shape: (num_generation,)
178
- matching_mse = mse_mean[np.arange(len(best_match)), best_match].mean()
179
 
180
  # best match for each reference time series
181
  # shape: (num_reference,)
182
  best_match_inv = np.argmin(mse_mean, axis=0)
183
- covered_mse = mse_mean[best_match_inv, np.arange(len(best_match_inv))].mean()
184
 
185
- harmonic_mean = 2 / (1 / matching_mse + 1 / covered_mse)
 
 
 
 
 
186
 
187
  # take matching for each feature and compute metrics for them
188
- matching_mse_features = []
189
- covered_mse_features = []
190
- harmonic_mean_features = []
 
 
 
191
  index_mse_features = []
 
 
192
  for f in range(predictions.shape[-1]):
193
  mse_f = mse[:, :, f]
194
  index_mse_f = mse_f.diagonal(axis1=0, axis2=1).mean()
195
  best_match_f = np.argmin(mse_f, axis=-1)
196
- matching_mse_f = mse_f[np.arange(len(best_match_f)), best_match_f].mean()
197
  best_match_inv_f = np.argmin(mse_f, axis=0)
198
- covered_mse_f = mse_f[best_match_inv_f, np.arange(len(best_match_inv_f))].mean()
199
- harmonic_mean_f = 2 / (1 / matching_mse_f + 1 / covered_mse_f)
200
- matching_mse_features.append(matching_mse_f)
201
- covered_mse_features.append(covered_mse_f)
202
- harmonic_mean_features.append(harmonic_mean_f)
203
  index_mse_features.append(index_mse_f)
204
- macro_matching_mse = statistics.mean(matching_mse_features)
205
- macro_covered_mse = statistics.mean(covered_mse_features)
206
- macro_harmonic_mean = statistics.mean(harmonic_mean_features)
 
 
 
 
 
 
 
 
 
 
 
 
207
  macro_index_mse = statistics.mean(index_mse_features)
 
 
 
 
 
 
 
 
 
 
 
208
  return {
209
- "matching_mse": matching_mse,
210
- "harmonic_mean": harmonic_mean,
211
- "covered_mse": covered_mse,
212
  "index_mse": index_mse,
213
- "matching_mse_features": matching_mse_features,
214
- "harmonic_mean_features": harmonic_mean_features,
215
- "covered_mse_features": covered_mse_features,
216
  "index_mse_features": index_mse_features,
217
- "macro_matching_mse": macro_matching_mse,
218
- "macro_covered_mse": macro_covered_mse,
219
- "macro_harmonic_mean": macro_harmonic_mean,
220
  "macro_index_mse": macro_index_mse,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  # limitations under the License.
14
  """TODO: Add a description here."""
15
 
16
+ import math
17
  import statistics
18
+ from typing import List, Optional, Union
19
 
20
  import datasets
21
  import evaluate
 
128
 
129
  def _compute(
130
  self,
131
+ predictions: Union[List, np.ndarray],
132
+ references: Union[List, np.ndarray],
133
  batch_size: Optional[int] = None,
134
+ cuc_n_calculation: int = 3,
135
+ cuc_n_samples: Union[List[int], str] = "auto",
136
  ):
137
  """
138
  Compute the scores of the module given the predictions and references
 
142
  references: list of reference
143
  shape: (num_reference, num_timesteps, num_features)
144
  batch_size: batch size to use for the computation. If None, the whole dataset is processed at once.
145
+ cuc_n_calculation: number of Coverage Under Curve calculate times
146
+ cuc_n_samples: number of samples to use for Coverage Under Curve calculation. If "auto", it uses the number of samples of the predictions.
147
  Returns:
148
  """
149
  predictions = np.array(predictions)
 
180
 
181
  # matching mse
182
  # shape: (num_generation,)
183
+ precision_mse = mse_mean[np.arange(len(best_match)), best_match].mean()
184
 
185
  # best match for each reference time series
186
  # shape: (num_reference,)
187
  best_match_inv = np.argmin(mse_mean, axis=0)
188
+ recall_mse = mse_mean[best_match_inv, np.arange(len(best_match_inv))].mean()
189
 
190
+ f1_mse = 2 / (1 / precision_mse + 1 / recall_mse)
191
+
192
+ # matching precision, recall and f1
193
+ matching_precision = np.unique(best_match).size / len(best_match_inv)
194
+ matching_recall = np.unique(best_match_inv).size / len(best_match)
195
+ matching_f1 = 2 / (1 / matching_precision + 1 / matching_recall)
196
 
197
  # take matching for each feature and compute metrics for them
198
+ precision_mse_features = []
199
+ recall_mse_features = []
200
+ f1_mse_features = []
201
+ matching_precision_features = []
202
+ matching_recall_features = []
203
+ matching_f1_features = []
204
  index_mse_features = []
205
+ coverages_features = []
206
+ cuc_features = []
207
  for f in range(predictions.shape[-1]):
208
  mse_f = mse[:, :, f]
209
  index_mse_f = mse_f.diagonal(axis1=0, axis2=1).mean()
210
  best_match_f = np.argmin(mse_f, axis=-1)
211
+ precision_mse_f = mse_f[np.arange(len(best_match_f)), best_match_f].mean()
212
  best_match_inv_f = np.argmin(mse_f, axis=0)
213
+ recall_mse_f = mse_f[best_match_inv_f, np.arange(len(best_match_inv_f))].mean()
214
+ f1_mse_f = 2 / (1 / precision_mse_f + 1 / recall_mse_f)
215
+ precision_mse_features.append(precision_mse_f)
216
+ recall_mse_features.append(recall_mse_f)
217
+ f1_mse_features.append(f1_mse_f)
218
  index_mse_features.append(index_mse_f)
219
+
220
+ matching_precision_f = np.unique(best_match_f).size / len(best_match_f)
221
+ matching_recall_f = np.unique(best_match_inv_f).size / len(best_match_inv_f)
222
+ matching_f1_f = 2 / (1 / matching_precision_f + 1 / matching_recall_f)
223
+ matching_precision_features.append(matching_precision_f)
224
+ matching_recall_features.append(matching_recall_f)
225
+ matching_f1_features.append(matching_f1_f)
226
+
227
+ coverages_f, cuc_f = self.compute_cuc(best_match_f, len(references), cuc_n_calculation, cuc_n_samples)
228
+ coverages_features.append(coverages_f)
229
+ cuc_features.append(cuc_f)
230
+
231
+ macro_precision_mse = statistics.mean(precision_mse_features)
232
+ macro_recall_mse = statistics.mean(recall_mse_features)
233
+ macro_f1_mse = statistics.mean(f1_mse_features)
234
  macro_index_mse = statistics.mean(index_mse_features)
235
+
236
+ macro_matching_precision = statistics.mean(matching_precision_features)
237
+ macro_matching_recall = statistics.mean(matching_recall_features)
238
+ macro_matching_f1 = statistics.mean(matching_f1_features)
239
+
240
+ # cuc
241
+ coverages, cuc = self.compute_cuc(best_match, len(references), cuc_n_calculation, cuc_n_samples)
242
+
243
+ macro_cuc = statistics.mean(cuc_features)
244
+ macro_coverages = [statistics.mean(c) for c in zip(*coverages_features)]
245
+
246
  return {
247
+ "precision_mse": precision_mse,
248
+ "f1_mse": f1_mse,
249
+ "recall_mse": recall_mse,
250
  "index_mse": index_mse,
251
+ "precision_mse_features": precision_mse_features,
252
+ "f1_mse_features": f1_mse_features,
253
+ "recall_mse_features": recall_mse_features,
254
  "index_mse_features": index_mse_features,
255
+ "macro_precision_mse": macro_precision_mse,
256
+ "macro_recall_mse": macro_recall_mse,
257
+ "macro_f1_mse": macro_f1_mse,
258
  "macro_index_mse": macro_index_mse,
259
+ "matching_precision": matching_precision,
260
+ "matching_recall": matching_recall,
261
+ "matching_f1": matching_f1,
262
+ "matching_precision_features": matching_precision_features,
263
+ "matching_recall_features": matching_recall_features,
264
+ "matching_f1_features": matching_f1_features,
265
+ "macro_matching_precision": macro_matching_precision,
266
+ "macro_matching_recall": macro_matching_recall,
267
+ "macro_matching_f1": macro_matching_f1,
268
+ "cuc": cuc,
269
+ "coverages": coverages,
270
+ "macro_cuc": macro_cuc,
271
+ "macro_coverages": macro_coverages,
272
+ "cuc_features": cuc_features,
273
+ "coverages_features": coverages_features,
274
  }
275
+
276
+ def compute_cuc(
277
+ self,
278
+ match: np.ndarray,
279
+ n_reference: int,
280
+ n_calculation: int,
281
+ n_samples: Union[List[int], str],
282
+ ):
283
+ """
284
+ Compute Coverage Under Curve
285
+ Args:
286
+ match: best match for each generated time series
287
+ n_reference: number of reference time series
288
+ n_calculation: number of Coverage Under Curve calculate times
289
+ n_samples: number of samples to use for Coverage Under Curve calculation. If "auto", it uses the number of samples of the predictions.
290
+ Returns:
291
+ """
292
+ n_generaiton = len(match)
293
+ if n_samples == "auto":
294
+ exp = int(math.log2(n_generaiton))
295
+ n_samples = [int(2**i) for i in range(exp)]
296
+ n_samples.append(n_generaiton)
297
+ assert isinstance(n_samples, list) and all(isinstance(n, int) for n in n_samples)
298
+
299
+ coverages = []
300
+ for n_sample in n_samples:
301
+ coverage = 0
302
+ for _ in range(n_calculation):
303
+ sample = np.random.choice(match, size=n_sample, replace=False) # type: ignore
304
+ coverage += len(np.unique(sample)) / n_reference
305
+ coverages.append(coverage / n_calculation)
306
+ cuc = np.trapz(coverages, n_samples) / len(n_samples) / max(n_samples)
307
+ return coverages, cuc
plot_cuc.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from argparse import ArgumentParser
3
+
4
+ import matplotlib.pyplot as plt
5
+
6
+ parser = ArgumentParser()
7
+ parser.add_argument("input", type=str, help="Input file of json data, output of matching_series")
8
+ parser.add_argument("output", type=str, help="Output file of the plot")
9
+ args = parser.parse_args()
10
+
11
+ with open(args.input, "r") as f:
12
+ data = json.load(f)
13
+
14
+ coverages = data["coverages"]
15
+ x = [2**i for i in range(len(coverages))]
16
+ y = coverages
17
+
18
+ fig, ax = plt.subplots()
19
+
20
+ ax.plot(x, y, "o-")
21
+ ax.set_xscale("log", base=2)
22
+ ax.set_xlabel("Number of generations")
23
+ ax.set_ylabel("Coverage")
24
+ plt.savefig(args.output)