indico_toolkit 6.14.0__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.
Files changed (83) hide show
  1. indico_toolkit/__init__.py +24 -0
  2. indico_toolkit/association/__init__.py +15 -0
  3. indico_toolkit/association/association.py +65 -0
  4. indico_toolkit/association/extracted_tokens.py +106 -0
  5. indico_toolkit/association/line_items.py +189 -0
  6. indico_toolkit/association/positioning.py +301 -0
  7. indico_toolkit/association/splitting.py +41 -0
  8. indico_toolkit/auto_populate/__init__.py +3 -0
  9. indico_toolkit/auto_populate/populator.py +362 -0
  10. indico_toolkit/auto_populate/types.py +66 -0
  11. indico_toolkit/auto_review/__init__.py +6 -0
  12. indico_toolkit/auto_review/auto_review_functions.py +123 -0
  13. indico_toolkit/auto_review/auto_reviewer.py +62 -0
  14. indico_toolkit/client.py +35 -0
  15. indico_toolkit/errors.py +32 -0
  16. indico_toolkit/etloutput/__init__.py +210 -0
  17. indico_toolkit/etloutput/cell.py +47 -0
  18. indico_toolkit/etloutput/errors.py +16 -0
  19. indico_toolkit/etloutput/etloutput.py +110 -0
  20. indico_toolkit/etloutput/range.py +30 -0
  21. indico_toolkit/etloutput/table.py +44 -0
  22. indico_toolkit/etloutput/token.py +25 -0
  23. indico_toolkit/indico_wrapper/__init__.py +15 -0
  24. indico_toolkit/indico_wrapper/dataset.py +114 -0
  25. indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
  26. indico_toolkit/indico_wrapper/download.py +193 -0
  27. indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
  28. indico_toolkit/indico_wrapper/modelop.py +200 -0
  29. indico_toolkit/indico_wrapper/reviewer.py +111 -0
  30. indico_toolkit/indico_wrapper/workflow.py +231 -0
  31. indico_toolkit/metrics/__init__.py +11 -0
  32. indico_toolkit/metrics/compare_ground_truth.py +123 -0
  33. indico_toolkit/metrics/compare_models.py +180 -0
  34. indico_toolkit/metrics/metrics.py +373 -0
  35. indico_toolkit/metrics/plotting.py +102 -0
  36. indico_toolkit/ocr/__init__.py +9 -0
  37. indico_toolkit/ocr/customocr_object.py +42 -0
  38. indico_toolkit/ocr/ondoc_object.py +93 -0
  39. indico_toolkit/ocr/standard_object.py +54 -0
  40. indico_toolkit/pipelines/__init__.py +3 -0
  41. indico_toolkit/pipelines/file_processing.py +157 -0
  42. indico_toolkit/polling/__init__.py +8 -0
  43. indico_toolkit/polling/autoreview.py +204 -0
  44. indico_toolkit/polling/downstream.py +145 -0
  45. indico_toolkit/polling/queries.py +68 -0
  46. indico_toolkit/py.typed +0 -0
  47. indico_toolkit/results/__init__.py +104 -0
  48. indico_toolkit/results/document.py +78 -0
  49. indico_toolkit/results/errors.py +4 -0
  50. indico_toolkit/results/model.py +60 -0
  51. indico_toolkit/results/normalization.py +159 -0
  52. indico_toolkit/results/predictionlist.py +395 -0
  53. indico_toolkit/results/predictions/__init__.py +88 -0
  54. indico_toolkit/results/predictions/box.py +58 -0
  55. indico_toolkit/results/predictions/citation.py +47 -0
  56. indico_toolkit/results/predictions/classification.py +50 -0
  57. indico_toolkit/results/predictions/documentextraction.py +160 -0
  58. indico_toolkit/results/predictions/extraction.py +35 -0
  59. indico_toolkit/results/predictions/formextraction.py +121 -0
  60. indico_toolkit/results/predictions/group.py +29 -0
  61. indico_toolkit/results/predictions/prediction.py +41 -0
  62. indico_toolkit/results/predictions/span.py +43 -0
  63. indico_toolkit/results/predictions/summarization.py +103 -0
  64. indico_toolkit/results/predictions/unbundling.py +51 -0
  65. indico_toolkit/results/result.py +152 -0
  66. indico_toolkit/results/review.py +32 -0
  67. indico_toolkit/results/utilities.py +73 -0
  68. indico_toolkit/retry.py +94 -0
  69. indico_toolkit/snapshots/__init__.py +3 -0
  70. indico_toolkit/snapshots/snapshot.py +340 -0
  71. indico_toolkit/structure/__init__.py +3 -0
  72. indico_toolkit/structure/create_structure.py +232 -0
  73. indico_toolkit/structure/queries.py +119 -0
  74. indico_toolkit/structure/utils.py +16 -0
  75. indico_toolkit/types/__init__.py +12 -0
  76. indico_toolkit/types/classification.py +92 -0
  77. indico_toolkit/types/extractions.py +227 -0
  78. indico_toolkit/types/predictions.py +28 -0
  79. indico_toolkit/types/workflow_object.py +133 -0
  80. indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
  81. indico_toolkit-6.14.0.dist-info/METADATA +144 -0
  82. indico_toolkit-6.14.0.dist-info/RECORD +83 -0
  83. indico_toolkit-6.14.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,231 @@
1
+ import io
2
+ from os import PathLike
3
+ from typing import Dict, List, Union
4
+
5
+ from indico import IndicoClient
6
+ from indico.queries import (
7
+ GetSubmission,
8
+ GetWorkflow,
9
+ JobStatus,
10
+ ListSubmissions,
11
+ Submission,
12
+ SubmissionFilter,
13
+ SubmitReview,
14
+ UpdateSubmission,
15
+ UpdateWorkflowSettings,
16
+ WaitForSubmissions,
17
+ WorkflowSubmission,
18
+ )
19
+ from indico.queries.submission import SubmissionResult
20
+ from indico.types import Workflow
21
+
22
+ from ..errors import ToolkitStatusError
23
+ from ..ocr import OnDoc
24
+ from ..types import WorkflowResult
25
+ from .indico_wrapper import IndicoWrapper
26
+
27
+ COMPLETE_FILTER = SubmissionFilter(status="COMPLETE", retrieved=False)
28
+ PENDING_REVIEW_FILTER = SubmissionFilter(status="PENDING_REVIEW", retrieved=False)
29
+
30
+
31
+ class Workflow(IndicoWrapper):
32
+ """
33
+ Class to support Workflow-related API calls
34
+ """
35
+
36
+ def __init__(self, client: IndicoClient):
37
+ self.client = client
38
+
39
+ def get_workflow(self, workflow_id: int) -> Workflow:
40
+ """
41
+ Args:
42
+ workflow_id (int): Workflow id to query for
43
+ """
44
+ return self.client.call(GetWorkflow(workflow_id))
45
+
46
+ def submit_documents_to_workflow(
47
+ self,
48
+ workflow_id: int,
49
+ *,
50
+ files: Union[List[Union[str, PathLike]], None] = None,
51
+ streams: Union[Dict[str, io.BufferedIOBase], None] = None,
52
+ ) -> List[int]:
53
+ """
54
+ Args:
55
+ workflow_id (int): Workflow to submit to
56
+ pdf_filepaths (List[str]): Path to local documents you would like to submit
57
+ streams (Dict[str, io.BufferedIOBase]): List of filename keys mapped to
58
+ streams for upload.
59
+ Returns:
60
+ List[int]: List of unique and persistent identifier for each submission.
61
+ """
62
+ return self.client.call(
63
+ WorkflowSubmission(workflow_id=workflow_id, files=files, streams=streams)
64
+ )
65
+
66
+ def get_ondoc_ocr_from_etl_url(self, etl_url: str) -> OnDoc:
67
+ """
68
+ Get ondocument OCR object from workflow result etl output
69
+
70
+ Args:
71
+ etl_url (str): url from "etl_output" key of workflow result json
72
+
73
+ Returns:
74
+ OnDoc: 'ondocument' OCR object
75
+ """
76
+ ocr_result = []
77
+ etl_response = self.get_storage_object(etl_url)
78
+ for page in etl_response["pages"]:
79
+ page_ocr = self.get_storage_object(page["page_info"])
80
+ ocr_result.append(page_ocr)
81
+ return OnDoc(ocr_result)
82
+
83
+ def get_file_bytes(self, file_url: str) -> bytes:
84
+ return self.get_storage_object(file_url)
85
+
86
+ def get_img_bytes_from_etl_url(self, etl_url: str) -> List[bytes]:
87
+ """
88
+ Get image bytes for each page from workflow result etl output
89
+
90
+ Args:
91
+ etl_url (str): url from "etl_output" key of workflow result json
92
+
93
+ Returns:
94
+ image_bytes
95
+ """
96
+ image_bytes = []
97
+ etl_response = self.get_storage_object(etl_url)
98
+ for page in etl_response["pages"]:
99
+ page_bytes = self.get_storage_object(page["image"])
100
+ image_bytes.append(page_bytes)
101
+ return image_bytes
102
+
103
+ def mark_submission_as_retreived(self, submission_id: int):
104
+ self.client.call(UpdateSubmission(submission_id, retrieved=True))
105
+
106
+ def get_complete_submission_objects(
107
+ self, workflow_id: int, submission_ids: List[int] = None
108
+ ) -> List[Submission]:
109
+ return self._get_list_of_submissions(
110
+ workflow_id, COMPLETE_FILTER, submission_ids
111
+ )
112
+
113
+ def get_submission_object(self, submission_id: int) -> Submission:
114
+ return self.client.call(GetSubmission(submission_id))
115
+
116
+ def get_submission_results_from_ids(
117
+ self,
118
+ submission_ids: List[int],
119
+ timeout: int = 180,
120
+ return_raw_json: bool = False,
121
+ raise_exception_for_failed: bool = False,
122
+ return_failed_results: bool = True,
123
+ ignore_deleted_submissions: bool = False,
124
+ ) -> List[WorkflowResult]:
125
+ """
126
+ Wait for submission to pass through workflow models and get result. If Review is
127
+ enabled, result may be retrieved prior to human review.
128
+
129
+ Args:
130
+ submission_ids (List[int]): Ids of submission predictions to retrieve
131
+ timeout (int): seconds permitted for each submission prior to timing out
132
+ return_raw_json: (bool) = If True return raw json result, otherwise return
133
+ WorkflowResult object.
134
+ raise_exception_for_failed (bool): if True, ToolkitStatusError raised for
135
+ failed submissions
136
+ return_failed_results (bool): if True, return objects for failed submissions
137
+ ignore_deleted_submissions (bool): if True, ignore deleted submissions
138
+
139
+ Returns:
140
+ List[WorkflowResult]: workflow result objects
141
+ """
142
+ results = []
143
+ self.wait_for_submissions_to_process(submission_ids, timeout)
144
+ for subid in submission_ids:
145
+ submission: Submission = self.get_submission_object(subid)
146
+ if submission.status == "FAILED":
147
+ message = f"FAILURE, Submission: {subid}. {submission.errors}"
148
+ if raise_exception_for_failed:
149
+ raise ToolkitStatusError(message)
150
+ elif not return_failed_results:
151
+ print(message)
152
+ continue
153
+ if submission.deleted and ignore_deleted_submissions:
154
+ continue
155
+ result = self._create_result(submission)
156
+ # Add path to original input file to result
157
+ result["input_file"] = submission.input_file
158
+ result["filename"] = submission.input_filename
159
+ if return_raw_json:
160
+ results.append(result)
161
+ else:
162
+ results.append(WorkflowResult(result))
163
+ return results
164
+
165
+ def _create_result(self, submission: Union[Submission, int]):
166
+ """
167
+ Assumes you already checked that the submission result is COMPLETE
168
+ """
169
+ job = self.client.call(SubmissionResult(submission, wait=True))
170
+ return self.get_storage_object(job.result)
171
+
172
+ def submit_submission_review(
173
+ self,
174
+ submission_id: int,
175
+ updated_predictions: dict,
176
+ wait: bool = True,
177
+ force_complete: bool = False,
178
+ ):
179
+ job = self.client.call(
180
+ SubmitReview(
181
+ submission_id,
182
+ changes=updated_predictions,
183
+ force_complete=force_complete,
184
+ )
185
+ )
186
+ if wait:
187
+ job = self.client.call(JobStatus(job.id, wait=True))
188
+ return job
189
+
190
+ def update_workflow_settings(
191
+ self,
192
+ workflow_id: int,
193
+ enable_review: bool = False,
194
+ enable_auto_review: bool = False,
195
+ ) -> None:
196
+ self.client.call(
197
+ UpdateWorkflowSettings(
198
+ workflow_id,
199
+ enable_review=enable_review,
200
+ enable_auto_review=enable_auto_review,
201
+ )
202
+ )
203
+
204
+ def wait_for_submissions_to_process(
205
+ self, submission_ids: List[int], timeout_seconds: int = 180
206
+ ) -> None:
207
+ """
208
+ Wait for submissions to reach a terminal status of "COMPLETE",
209
+ "PENDING_AUTO_REVIEW", "FAILED", or "PENDING_REVIEW"
210
+ """
211
+ self.client.call(WaitForSubmissions(submission_ids, timeout_seconds))
212
+
213
+ def _get_list_of_submissions(
214
+ self,
215
+ workflow_id: int,
216
+ submission_filter: SubmissionFilter,
217
+ submission_ids: List[int] = None,
218
+ ) -> List[Submission]:
219
+ return self.client.call(
220
+ ListSubmissions(
221
+ workflow_ids=[workflow_id],
222
+ submission_ids=submission_ids,
223
+ filters=submission_filter,
224
+ )
225
+ )
226
+
227
+ def _error_handle(self, message: str, ignore_exceptions: bool):
228
+ if ignore_exceptions:
229
+ print(f"Ignoring exception and continuing: {message}")
230
+ else:
231
+ raise RuntimeError(message)
@@ -0,0 +1,11 @@
1
+ from .compare_ground_truth import CompareGroundTruth
2
+ from .compare_models import CompareModels
3
+ from .metrics import ExtractionMetrics
4
+ from .plotting import Plotting
5
+
6
+ __all__ = (
7
+ "CompareGroundTruth",
8
+ "CompareModels",
9
+ "ExtractionMetrics",
10
+ "Plotting",
11
+ )
@@ -0,0 +1,123 @@
1
+ from typing import List
2
+
3
+ from ..association.association import sequences_exact, sequences_overlap
4
+ from ..errors import ToolkitInputError
5
+ from ..types.extractions import Extractions
6
+
7
+
8
+ class CompareGroundTruth:
9
+ """
10
+ Compare a set of ground truths against a set of model predictions on a per document
11
+ basis.
12
+ """
13
+
14
+ def __init__(self, ground_truth: List[dict], predictions: List[dict]):
15
+ self.gt_by_label: dict = Extractions(ground_truth).to_dict_by_label
16
+ self.preds_by_label: dict = Extractions(predictions).to_dict_by_label
17
+ self.labels: List[str] = list(
18
+ set(list(self.gt_by_label) + list(self.preds_by_label))
19
+ )
20
+ self.all_label_metrics: dict = None
21
+ self.overall_metrics: dict = None
22
+
23
+ def set_all_label_metrics(self, span_type: str = "overlap") -> None:
24
+ """
25
+ The "all_label_metrics" dict includes each label as a key and each label's
26
+ metrics as the corresponding value.
27
+ """
28
+ if span_type in ["exact", "overlap"]:
29
+ self.all_label_metrics = {
30
+ label: self._get_base_metrics(label, span_type) for label in self.labels
31
+ }
32
+ else:
33
+ raise ToolkitInputError("Invalid span type entered.")
34
+
35
+ def set_overall_metrics(self) -> None:
36
+ """
37
+ The "overall_metrics" dict includes the metrics for the entire document.
38
+ (Key: metric type; value: metric value)
39
+ """
40
+ if self.all_label_metrics is None:
41
+ self.set_all_label_metrics()
42
+
43
+ metrics_types = [
44
+ "true_positives",
45
+ "false_positives",
46
+ "false_negatives",
47
+ ]
48
+
49
+ overall_metrics = {}
50
+
51
+ for metric in metrics_types:
52
+ total_amt = 0
53
+ for label in self.all_label_metrics:
54
+ total_amt += self.all_label_metrics[label][metric]
55
+ overall_metrics[metric] = total_amt
56
+
57
+ overall_metrics["precision"] = self._get_precision(
58
+ overall_metrics["true_positives"],
59
+ overall_metrics["false_positives"],
60
+ )
61
+ overall_metrics["recall"] = self._get_recall(
62
+ overall_metrics["true_positives"],
63
+ overall_metrics["false_negatives"],
64
+ )
65
+
66
+ self.overall_metrics = overall_metrics
67
+
68
+ def _get_precision(self, true_p: int, false_p: int) -> float:
69
+ try:
70
+ precision = true_p / (true_p + false_p)
71
+ except ZeroDivisionError:
72
+ precision = 0
73
+ return precision
74
+
75
+ def _get_recall(self, true_p: int, false_n: int) -> float:
76
+ try:
77
+ recall = true_p / (true_p + false_n)
78
+ except ZeroDivisionError:
79
+ recall = 0
80
+ return recall
81
+
82
+ def _get_base_metrics(self, label: str, span_type: str) -> dict:
83
+ """
84
+ With the current overlap span type calculation, if 2 separate predictions
85
+ each overlap with a single ground truth, each pred is counted as a true
86
+ positive. (i.e. There isn't a break out of the loop once a TP is found.)
87
+ """
88
+ # TODO potentially build in choice on the "multiple true positives" per ground
89
+ # truth prediction by adding conditional
90
+ true_pos = 0
91
+ false_neg = 0
92
+ false_pos = 0
93
+ span_types = {"overlap": sequences_overlap, "exact": sequences_exact}
94
+ span_type_func = span_types[span_type]
95
+
96
+ if label not in self.preds_by_label:
97
+ false_neg = len(self.gt_by_label[label])
98
+ elif label not in self.gt_by_label:
99
+ false_pos = len(self.preds_by_label[label])
100
+ else:
101
+ for model_pred in self.preds_by_label[label]:
102
+ match_found = False
103
+ for gt_pred in self.gt_by_label[label]:
104
+ if span_type_func(model_pred, gt_pred):
105
+ true_pos += 1
106
+ match_found = True
107
+ if not match_found:
108
+ false_pos += 1
109
+ for gt_pred in self.gt_by_label[label]:
110
+ match_found = False
111
+ for model_pred in self.preds_by_label[label]:
112
+ if span_type_func(model_pred, gt_pred):
113
+ match_found = True
114
+ if not match_found:
115
+ false_neg += 1
116
+
117
+ return {
118
+ "true_positives": true_pos,
119
+ "false_negatives": false_neg,
120
+ "false_positives": false_pos,
121
+ "precision": self._get_precision(true_pos, false_pos),
122
+ "recall": self._get_recall(true_pos, false_neg),
123
+ }
@@ -0,0 +1,180 @@
1
+ from collections import namedtuple
2
+ from typing import List, Set, Tuple
3
+
4
+ from indico import IndicoClient
5
+
6
+ from ..errors import ToolkitInputError
7
+ from .metrics import ExtractionMetrics
8
+ from .plotting import Plotting
9
+
10
+ try:
11
+ import pandas as pd
12
+
13
+ _PANDAS_INSTALLED = True
14
+ except ImportError as error:
15
+ _PANDAS_INSTALLED = False
16
+ _IMPORT_ERROR = error
17
+
18
+ ModelIds = namedtuple("ModelIds", "group_id id")
19
+
20
+
21
+ class CompareModels(ExtractionMetrics):
22
+ def __init__(
23
+ self,
24
+ client: IndicoClient,
25
+ model_group_1: int,
26
+ model_id_1: int,
27
+ model_group_2: int,
28
+ model_id_2: int,
29
+ ):
30
+ """
31
+ Compare the metrics for two extraction models
32
+
33
+ Args:
34
+ client (IndicoClient): instantiated indico client
35
+ model_group_1 (int): model group of first model
36
+ model_id_1 (int): id of first model
37
+ model_group_2 (int): model group of second model
38
+ model_id_2 (int): id of second model
39
+ """
40
+ if not _PANDAS_INSTALLED:
41
+ raise RuntimeError(
42
+ "comparing metrics requires additional dependencies: "
43
+ "`pip install indico-toolkit[metrics]`"
44
+ ) from _IMPORT_ERROR
45
+
46
+ self.client = client
47
+ self.models = [
48
+ ModelIds(model_group_1, model_id_1),
49
+ ModelIds(model_group_2, model_id_2),
50
+ ]
51
+ self.non_overlapping_fields: Set[str] = None
52
+ self.overlapping_fields: Set[str] = None
53
+ self.df: pd.DataFrame = None
54
+
55
+ def get_data(self, span_type: str = "overlap") -> "pd.DataFrame":
56
+ """
57
+ Gathers metrics for both models into a dataframe, setting it to self.df
58
+ Args:
59
+ span_type (str): options include 'superset', 'exact', 'overlap' or 'token'
60
+ """
61
+ dfs = []
62
+ for model in self.models:
63
+ self.get_metrics(model.group_id)
64
+ df = self.get_metrics_df(span_type, model.id)
65
+ df.drop("model_id", axis=1, inplace=True)
66
+ dfs.append(df)
67
+ self._set_labelset_info(dfs)
68
+ if len(self.overlapping_fields) == 0:
69
+ raise ToolkitInputError(
70
+ "There are no shared labels between the models you provided: "
71
+ f"{self.non_overlapping_fields}"
72
+ )
73
+ self.df = pd.merge(
74
+ dfs[0], dfs[1], on="field_name", suffixes=self._model_suffixes
75
+ )
76
+
77
+ def get_metric_differences(
78
+ self, metric: str = "f1Score", include_difference: bool = True
79
+ ):
80
+ """
81
+ Get a dataframe focused on one metrics, by default sorted by a column of value
82
+ differences
83
+
84
+ Args:
85
+ metric (str, optional): possible values are 'precision', 'recall',
86
+ 'f1Score', 'falsePositives', 'falseNegatives', 'truePositives'.
87
+ Defaults to "f1Score".
88
+ include_difference (bool): include a column of the most recent model ID
89
+ minus the older model ID
90
+ """
91
+ metric_cols = self._get_metric_col_names(metric)
92
+ cols_to_keep = ["field_name", *metric_cols]
93
+ diff_df = self.df[cols_to_keep].copy()
94
+ if include_difference:
95
+ diff_df["difference"] = diff_df[metric_cols[0]] - diff_df[metric_cols[1]]
96
+ diff_df = diff_df.sort_values(by=["difference"], ascending=False)
97
+ return diff_df
98
+
99
+ def bar_plot(
100
+ self,
101
+ output_path: str,
102
+ metric: str = "f1Score",
103
+ plot_title: str = "",
104
+ bar_colors: List[str] = ["salmon", "darkblue"],
105
+ ):
106
+ """
107
+ Write an html bar plot to disc. Will also open the plot automatically in your
108
+ browser, where you will interactive functionality and the ability to download a
109
+ copy as a PNG as well.
110
+
111
+ Args:
112
+ output_path (str): where you want to write plot, e.g. "./myplot.html"
113
+ metric (str, optional): possible values are 'precision', 'recall',
114
+ 'f1Score', 'falsePositives', 'falseNegatives', 'truePositives'.
115
+ Defaults to "f1Score".
116
+ plot_title (str, optional): Title of the plot. Defaults to "".
117
+ bar_colors (List[str], optional): length two list with the colors for your
118
+ plot, can be css color names, rgb, or hex name .
119
+ Defaults to ["#EEE8AA", "#98FB98"].
120
+ """
121
+ metric_cols = self._get_metric_col_names(metric, order_descending=False)
122
+ plotting = Plotting()
123
+ for i in range(2):
124
+ plotting.add_barplot_data(
125
+ self.df["field_name"],
126
+ self.df[metric_cols[i]],
127
+ name=self._id_from_col_name(metric_cols[i]),
128
+ color=bar_colors[i],
129
+ )
130
+ plotting.define_layout(
131
+ yaxis_title=metric, legend_title="Model ID", plot_title=plot_title
132
+ )
133
+ plotting.plot(output_path)
134
+
135
+ def to_csv(
136
+ self,
137
+ output_path: str,
138
+ ):
139
+ """
140
+ Write a CSV to disc of all model metrics
141
+ Args:
142
+ output_path (str): path to write CSV on your system, e.g. "./my_metrics.csv"
143
+ """
144
+ self.df.to_csv(output_path, index=False)
145
+
146
+ def get_data_df(self):
147
+ raise NotImplementedError(
148
+ "This method is not used by this child class. Use 'df' attribute"
149
+ )
150
+
151
+ @staticmethod
152
+ def _id_from_col_name(col_name: str):
153
+ return col_name.split("_")[-1]
154
+
155
+ def _get_metric_col_names(self, metric: str, order_descending: bool = True):
156
+ ordered = sorted(
157
+ self._model_suffixes,
158
+ key=lambda x: int(x.replace("_", "")),
159
+ reverse=order_descending,
160
+ )
161
+ return [f"{metric}{suffix}" for suffix in ordered]
162
+
163
+ @staticmethod
164
+ def labelsets_overlap(labelset1: set, labelset2: set):
165
+ return labelset1.intersection(labelset2)
166
+
167
+ @staticmethod
168
+ def labelset_differences(labelset1: set, labelset2: set):
169
+ return labelset1.symmetric_difference(labelset2)
170
+
171
+ @property
172
+ def _model_suffixes(self):
173
+ return f"_{self.models[0].id}", f"_{self.models[1].id}"
174
+
175
+ def _set_labelset_info(self, df_list: "List[pd.DataFrame]") -> Tuple[set]:
176
+ labelsets = [set(i["field_name"]) for i in df_list]
177
+ self.overlapping_fields = self.labelsets_overlap(labelsets[0], labelsets[1])
178
+ self.non_overlapping_fields = self.labelset_differences(
179
+ labelsets[0], labelsets[1]
180
+ )