sampling-mining-workflows-dsl 0.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.
- sampling_mining_workflows_dsl/ __init__.py +0 -0
- sampling_mining_workflows_dsl/CompleteWorkflow.py +23 -0
- sampling_mining_workflows_dsl/Workflow.py +242 -0
- sampling_mining_workflows_dsl/WorkflowBuilder.py +11 -0
- sampling_mining_workflows_dsl/analysis/ChiSquareAnalysis.py +37 -0
- sampling_mining_workflows_dsl/analysis/CochranTest.py +44 -0
- sampling_mining_workflows_dsl/analysis/CochranWorkflowAnalysis.py +37 -0
- sampling_mining_workflows_dsl/analysis/CoverageTest.py +60 -0
- sampling_mining_workflows_dsl/analysis/DistributionWorkflowAnalysis.py +57 -0
- sampling_mining_workflows_dsl/analysis/HistAnalysis.py +201 -0
- sampling_mining_workflows_dsl/analysis/HistWorkflowAnalysis.py +72 -0
- sampling_mining_workflows_dsl/analysis/KSWorkflowAnalysis.py +217 -0
- sampling_mining_workflows_dsl/analysis/WorkflowAnalysis.py +7 -0
- sampling_mining_workflows_dsl/analysis/YamaneTest.py +16 -0
- sampling_mining_workflows_dsl/analysis/YamaneWorkflowAnalysis.py +27 -0
- sampling_mining_workflows_dsl/analysis/kolmogorov_smirnov.py +31 -0
- sampling_mining_workflows_dsl/constraint/BoolComparator.py +23 -0
- sampling_mining_workflows_dsl/constraint/BoolConstraint.py +50 -0
- sampling_mining_workflows_dsl/constraint/BoolConstraintString.py +66 -0
- sampling_mining_workflows_dsl/constraint/Comparator.py +16 -0
- sampling_mining_workflows_dsl/constraint/Constraint.py +23 -0
- sampling_mining_workflows_dsl/constraint/NaturalComparator.py +17 -0
- sampling_mining_workflows_dsl/element/Element.py +45 -0
- sampling_mining_workflows_dsl/element/Loader.py +17 -0
- sampling_mining_workflows_dsl/element/Repository.py +28 -0
- sampling_mining_workflows_dsl/element/Set.py +210 -0
- sampling_mining_workflows_dsl/element/Writer.py +9 -0
- sampling_mining_workflows_dsl/element/loader/CsvLoader.py +67 -0
- sampling_mining_workflows_dsl/element/loader/JsonLoader.py +84 -0
- sampling_mining_workflows_dsl/element/loader/LoaderFactory.py +14 -0
- sampling_mining_workflows_dsl/element/writer/CsvWriter.py +46 -0
- sampling_mining_workflows_dsl/element/writer/JsonWriter.py +58 -0
- sampling_mining_workflows_dsl/element/writer/WriterFactory.py +7 -0
- sampling_mining_workflows_dsl/exec_visualizer/WorkflowVisualizer.py +189 -0
- sampling_mining_workflows_dsl/github_seart/loader.py +17 -0
- sampling_mining_workflows_dsl/github_seart/metadata.py +106 -0
- sampling_mining_workflows_dsl/metadata/Metadata.py +117 -0
- sampling_mining_workflows_dsl/metadata/MetadataBoolean.py +14 -0
- sampling_mining_workflows_dsl/metadata/MetadataDate.py +75 -0
- sampling_mining_workflows_dsl/metadata/MetadataDict.py +33 -0
- sampling_mining_workflows_dsl/metadata/MetadataList.py +32 -0
- sampling_mining_workflows_dsl/metadata/MetadataNumber.py +29 -0
- sampling_mining_workflows_dsl/metadata/MetadataString.py +13 -0
- sampling_mining_workflows_dsl/metadata/MetadataValue.py +24 -0
- sampling_mining_workflows_dsl/operator/Operator.py +165 -0
- sampling_mining_workflows_dsl/operator/OperatorBuilder.py +191 -0
- sampling_mining_workflows_dsl/operator/OperatorFactory.py +76 -0
- sampling_mining_workflows_dsl/operator/clustering/GroupingOperator.py +36 -0
- sampling_mining_workflows_dsl/operator/clustering/SubWorkflowOperatorBuilder.py +76 -0
- sampling_mining_workflows_dsl/operator/selection/SelectionOperator.py +5 -0
- sampling_mining_workflows_dsl/operator/selection/filter/FilterOperator.py +30 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/SamplingOperator.py +11 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/automatic/AutomaticSamplingOperator.py +9 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/automatic/RandomSelectionOperator.py +24 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/automatic/RandomSelectionPartitionOperator.py +62 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/automatic/SystematicRandomSelectionOperator.py +14 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/automatic/SystematicSelectionOperator.py +31 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/manual/InteractiveManualSamplingOperator.py +40 -0
- sampling_mining_workflows_dsl/operator/selection/sampling/manual/ManualSamplingOperator.py +28 -0
- sampling_mining_workflows_dsl/operator/set_algebra/ExternalSetOperator.py +33 -0
- sampling_mining_workflows_dsl/operator/set_algebra/InternalSetOperator.py +47 -0
- sampling_mining_workflows_dsl/operator/set_algebra/SetOperator.py +30 -0
- sampling_mining_workflows_dsl/operator/set_algebra/external_set_operator/DifferenceOperator.py +17 -0
- sampling_mining_workflows_dsl/operator/set_algebra/external_set_operator/IntersectionOperator.py +19 -0
- sampling_mining_workflows_dsl/operator/set_algebra/external_set_operator/UnionOperator.py +17 -0
- sampling_mining_workflows_dsl/operator/set_algebra/internal_set_operator/DifferenceOperator.py +17 -0
- sampling_mining_workflows_dsl/operator/set_algebra/internal_set_operator/IntersectionOperator.py +18 -0
- sampling_mining_workflows_dsl/operator/set_algebra/internal_set_operator/UnionOperator.py +17 -0
- sampling_mining_workflows_dsl/operator/set_algebra/set_operator/DifferenceOperator.py +15 -0
- sampling_mining_workflows_dsl/operator/set_algebra/set_operator/IntersectionOperator.py +18 -0
- sampling_mining_workflows_dsl/operator/set_algebra/set_operator/UnionOperator.py +17 -0
- sampling_mining_workflows_dsl/test/ __init__.py +0 -0
- sampling_mining_workflows_dsl/test/Workflow_simple.py +38 -0
- sampling_mining_workflows_dsl/test/input.json +401 -0
- sampling_mining_workflows_dsl/toolbox.py +42 -0
- sampling_mining_workflows_dsl-0.0.1.dist-info/METADATA +236 -0
- sampling_mining_workflows_dsl-0.0.1.dist-info/RECORD +79 -0
- sampling_mining_workflows_dsl-0.0.1.dist-info/WHEEL +4 -0
- sampling_mining_workflows_dsl-0.0.1.dist-info/licenses/LICENSE.txt +674 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import matplotlib.ticker as ticker
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from sampling_mining_workflows_dsl.element.Set import Set
|
|
10
|
+
from sampling_mining_workflows_dsl.metadata.Metadata import Metadata
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sampling_mining_workflows_dsl.metadata import MetadataValue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HistAnalysis:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
save_path: str,
|
|
20
|
+
metadata: Metadata,
|
|
21
|
+
top_x: int = -1,
|
|
22
|
+
category: bool = True,
|
|
23
|
+
sort: bool = False,
|
|
24
|
+
show: bool = False,
|
|
25
|
+
log_y: bool = False,
|
|
26
|
+
fixed_bins: int = None,
|
|
27
|
+
max_x_bound: float = None,
|
|
28
|
+
x_label: str = None,
|
|
29
|
+
fig_size=(10,6),
|
|
30
|
+
):
|
|
31
|
+
self.metadata = metadata
|
|
32
|
+
# Wether data should be treated as categorical data or continous
|
|
33
|
+
self.category = category
|
|
34
|
+
self.top_x = top_x
|
|
35
|
+
self.sort = sort
|
|
36
|
+
self.save_path = save_path
|
|
37
|
+
os.makedirs(self.save_path, exist_ok=True)
|
|
38
|
+
self.show = show
|
|
39
|
+
self.log_y = log_y
|
|
40
|
+
self.fixed_bins = fixed_bins
|
|
41
|
+
self.max_x_bound = max_x_bound
|
|
42
|
+
self.x_label = x_label
|
|
43
|
+
self.fig_size = fig_size
|
|
44
|
+
|
|
45
|
+
def analyze(self, s: Set, file_name: str, op_info: str):
|
|
46
|
+
# From Set to List of Metadata values
|
|
47
|
+
try:
|
|
48
|
+
metadata_values = []
|
|
49
|
+
for element in s.get_elements():
|
|
50
|
+
if not isinstance(element, Set):
|
|
51
|
+
metadata_value: MetadataValue = element.get_metadata_value(
|
|
52
|
+
self.metadata
|
|
53
|
+
)
|
|
54
|
+
if self.metadata.type is list:
|
|
55
|
+
metadata_values.extend(metadata_value.get_value())
|
|
56
|
+
else:
|
|
57
|
+
metadata_values.append(metadata_value.get_value())
|
|
58
|
+
|
|
59
|
+
if self.top_x > 0:
|
|
60
|
+
# Count all and find top_x
|
|
61
|
+
counter = Counter(metadata_values)
|
|
62
|
+
most_common = dict(counter.most_common(self.top_x))
|
|
63
|
+
top_values = set(most_common.keys())
|
|
64
|
+
|
|
65
|
+
# Replace non-top values with 'Other'
|
|
66
|
+
metadata_values = [
|
|
67
|
+
val if val in top_values else "Other" for val in metadata_values
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
fig, ax = self.create_histogram(metadata_values, op_info)
|
|
71
|
+
if self.show:
|
|
72
|
+
self.show_histogram()
|
|
73
|
+
self.save_histogram(fig, file_name)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Error analyzing {self.metadata.name}: {e}")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
def create_histogram(self, data: list, op_info: str):
|
|
79
|
+
df = pd.DataFrame(data, columns=["value"])
|
|
80
|
+
series = df["value"]
|
|
81
|
+
|
|
82
|
+
# Apply max x bound filter if specified
|
|
83
|
+
if self.max_x_bound is not None and not self.category:
|
|
84
|
+
# Only filter for continuous data
|
|
85
|
+
filtered_data = [x for x in data if x <= self.max_x_bound]
|
|
86
|
+
df = pd.DataFrame(filtered_data, columns=["value"])
|
|
87
|
+
series = df["value"]
|
|
88
|
+
data = filtered_data
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Set style and create figure with better styling
|
|
92
|
+
plt.style.use('default') # Reset to default style
|
|
93
|
+
fig, ax = plt.subplots(figsize=self.fig_size)
|
|
94
|
+
fig.patch.set_facecolor('white')
|
|
95
|
+
|
|
96
|
+
if not self.category:
|
|
97
|
+
unique_values = series.nunique()
|
|
98
|
+
# Determine number of bins
|
|
99
|
+
if self.fixed_bins is not None:
|
|
100
|
+
bins = self.fixed_bins
|
|
101
|
+
else:
|
|
102
|
+
bins = min(10, unique_values)
|
|
103
|
+
|
|
104
|
+
# Create histogram with better styling
|
|
105
|
+
n, bins_edges, patches = ax.hist(
|
|
106
|
+
x=data,
|
|
107
|
+
bins=bins,
|
|
108
|
+
color="#000000", # Dark blue color
|
|
109
|
+
alpha=0.85,
|
|
110
|
+
edgecolor="#C9CCD3", # Very dark blue border
|
|
111
|
+
linewidth=1.2
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if self.log_y:
|
|
115
|
+
ax.set_yscale('log')
|
|
116
|
+
|
|
117
|
+
# Set x-axis limit if max_x_bound is specified
|
|
118
|
+
if self.max_x_bound is not None:
|
|
119
|
+
ax.set_xlim(right=self.max_x_bound)
|
|
120
|
+
else:
|
|
121
|
+
if self.sort:
|
|
122
|
+
value_counts = series.value_counts(ascending=False, sort=True)
|
|
123
|
+
else:
|
|
124
|
+
value_counts = series.value_counts().sort_index(ascending=True)
|
|
125
|
+
|
|
126
|
+
# Create bar chart with better styling
|
|
127
|
+
value_counts.plot(
|
|
128
|
+
kind="bar",
|
|
129
|
+
ax=ax,
|
|
130
|
+
color="#000000", # Dark blue color
|
|
131
|
+
alpha=0.85,
|
|
132
|
+
edgecolor='#C9CCD3', # Very dark blue border
|
|
133
|
+
linewidth=1.2
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Set custom x-label or default
|
|
138
|
+
x_axis_label = self.x_label if self.x_label else "Category"
|
|
139
|
+
ax.set_xlabel(x_axis_label, fontsize=16, fontweight='bold')
|
|
140
|
+
|
|
141
|
+
# Rotate x-axis labels for better readability
|
|
142
|
+
plt.xticks(rotation=45, ha='right', fontsize=14)
|
|
143
|
+
|
|
144
|
+
# Enhanced styling
|
|
145
|
+
ax.set_title(op_info, fontsize=18, fontweight='bold', pad=20)
|
|
146
|
+
|
|
147
|
+
# Y-axis label with log scale indication
|
|
148
|
+
y_label = "Frequency"
|
|
149
|
+
if self.log_y:
|
|
150
|
+
y_label += " (Log Scale)"
|
|
151
|
+
ax.set_ylabel(y_label, fontsize=16, fontweight='bold')
|
|
152
|
+
|
|
153
|
+
# Set custom x-label for continuous data if provided
|
|
154
|
+
if not self.category and self.x_label:
|
|
155
|
+
ax.set_xlabel(self.x_label, fontsize=16, fontweight='bold')
|
|
156
|
+
|
|
157
|
+
# Grid styling
|
|
158
|
+
ax.grid(True, alpha=0.3, linestyle='-', linewidth=0.5)
|
|
159
|
+
ax.set_axisbelow(True)
|
|
160
|
+
|
|
161
|
+
# Spine styling
|
|
162
|
+
for spine in ax.spines.values():
|
|
163
|
+
spine.set_color('#333333')
|
|
164
|
+
spine.set_linewidth(1)
|
|
165
|
+
|
|
166
|
+
# Make axes more visible with intermediate ticks
|
|
167
|
+
ax.tick_params(axis='both', which='major', labelsize=14, colors='#333333')
|
|
168
|
+
ax.tick_params(axis='both', which='minor', labelsize=12, colors='#666666')
|
|
169
|
+
|
|
170
|
+
# Add minor ticks for Y axis for better readability
|
|
171
|
+
ax.yaxis.set_minor_locator(ticker.AutoMinorLocator())
|
|
172
|
+
|
|
173
|
+
# If log scale, use log minor locator for Y axis
|
|
174
|
+
if self.log_y:
|
|
175
|
+
ax.yaxis.set_minor_locator(ticker.LogLocator(base=10.0, subs='auto', numticks=4))
|
|
176
|
+
|
|
177
|
+
# Format axis numbers with separators
|
|
178
|
+
# Format x-axis with space separators for thousands
|
|
179
|
+
if not self.category:
|
|
180
|
+
|
|
181
|
+
ax.xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f"{x:,.0f}"))
|
|
182
|
+
# Format y-axis with space separators for thousands
|
|
183
|
+
ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f"{x:,.0f}"))
|
|
184
|
+
|
|
185
|
+
plt.tight_layout()
|
|
186
|
+
return fig, ax
|
|
187
|
+
|
|
188
|
+
def save_histogram(self, fig, file_name: str):
|
|
189
|
+
|
|
190
|
+
if file_name:
|
|
191
|
+
# Create folder if needed
|
|
192
|
+
os.makedirs(self.save_path, exist_ok=True)
|
|
193
|
+
file_path = os.path.join(self.save_path, file_name)
|
|
194
|
+
fig.savefig(file_path)
|
|
195
|
+
else:
|
|
196
|
+
print("No save path provided, displaying histogram instead.")
|
|
197
|
+
self.show_histogram()
|
|
198
|
+
plt.close(fig)
|
|
199
|
+
|
|
200
|
+
def show_histogram(self):
|
|
201
|
+
plt.show()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from sampling_mining_workflows_dsl.analysis.HistAnalysis import HistAnalysis
|
|
4
|
+
from sampling_mining_workflows_dsl.analysis.WorkflowAnalysis import WorkflowAnalysis
|
|
5
|
+
from sampling_mining_workflows_dsl.metadata.Metadata import Metadata
|
|
6
|
+
from sampling_mining_workflows_dsl.operator.clustering.GroupingOperator import GroupingOperator
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HistWorkflowAnalysis(WorkflowAnalysis):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
metadata: Metadata[T],
|
|
15
|
+
top_x: int = -1,
|
|
16
|
+
category: bool = True,
|
|
17
|
+
sort: bool = False,
|
|
18
|
+
output_path: str = "hist_analysis/",
|
|
19
|
+
log_y: bool = False,
|
|
20
|
+
fixed_bins: int = None,
|
|
21
|
+
max_x_bound: float = None,
|
|
22
|
+
x_label: str = None,
|
|
23
|
+
fig_size=(10,6)
|
|
24
|
+
):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.category = category
|
|
27
|
+
self.sort = sort
|
|
28
|
+
self.metadata = metadata
|
|
29
|
+
self.file_path = output_path
|
|
30
|
+
self.top_x = top_x
|
|
31
|
+
self.log_y = log_y
|
|
32
|
+
self.fixed_bins = fixed_bins
|
|
33
|
+
self.max_x_bound = max_x_bound
|
|
34
|
+
self.x_label = x_label
|
|
35
|
+
self.fig_size = fig_size
|
|
36
|
+
|
|
37
|
+
def analyze(
|
|
38
|
+
self, workflow, workflow_name: str = "main workflow", op_number: int = 1
|
|
39
|
+
):
|
|
40
|
+
op = workflow.get_root()
|
|
41
|
+
analysis = HistAnalysis(
|
|
42
|
+
self.file_path, self.metadata, self.top_x, self.category, self.sort,
|
|
43
|
+
show=False, log_y=self.log_y, fixed_bins=self.fixed_bins,
|
|
44
|
+
max_x_bound=self.max_x_bound, x_label=self.x_label,fig_size=self.fig_size
|
|
45
|
+
)
|
|
46
|
+
analysis.analyze(
|
|
47
|
+
op.get_input(),
|
|
48
|
+
f"{self.metadata.name}_{workflow_name}_op{op_number}_input.svg",
|
|
49
|
+
f"",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
while op is not None:
|
|
53
|
+
if isinstance(op, GroupingOperator):
|
|
54
|
+
analysis.analyze(
|
|
55
|
+
op.get_merged_output(),
|
|
56
|
+
f"{self.metadata.name}_{workflow_name}_op{op_number}_output.svg",
|
|
57
|
+
f"",
|
|
58
|
+
)
|
|
59
|
+
for i, internal_w in enumerate(op.get_workflows(), start=1):
|
|
60
|
+
# Recursively analyze subworkflows
|
|
61
|
+
subworkflow_name = f"subworkflow {i}"
|
|
62
|
+
self.analyze(
|
|
63
|
+
internal_w, subworkflow_name, 1
|
|
64
|
+
) # Reset operator numbering for subworkflows
|
|
65
|
+
else:
|
|
66
|
+
analysis.analyze(
|
|
67
|
+
op.get_output(),
|
|
68
|
+
f"{self.metadata.name}_{workflow_name}_op{op_number}_output.svg",
|
|
69
|
+
f"",
|
|
70
|
+
)
|
|
71
|
+
op = op.get_next_operator()
|
|
72
|
+
op_number += 1
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from sampling_mining_workflows_dsl.Workflow import Workflow
|
|
5
|
+
from sampling_mining_workflows_dsl.analysis.HistAnalysis import HistAnalysis
|
|
6
|
+
from sampling_mining_workflows_dsl.analysis.WorkflowAnalysis import WorkflowAnalysis
|
|
7
|
+
from sampling_mining_workflows_dsl.analysis.kolmogorov_smirnov import kolmogorov_smirnov
|
|
8
|
+
from sampling_mining_workflows_dsl.metadata.Metadata import Metadata
|
|
9
|
+
from sampling_mining_workflows_dsl.operator.clustering.GroupingOperator import (
|
|
10
|
+
GroupingOperator,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KSWorkflowAnalysis(WorkflowAnalysis):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
metadata: Metadata[T],
|
|
20
|
+
output_path: str = "analysis",
|
|
21
|
+
):
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
self.metadata = metadata
|
|
25
|
+
self.file_path = output_path+"/ks_analysis.txt"
|
|
26
|
+
|
|
27
|
+
def analyze(
|
|
28
|
+
self, workflow
|
|
29
|
+
):
|
|
30
|
+
import os
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
|
|
33
|
+
# Create all directory if it doesn't exist
|
|
34
|
+
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
|
|
35
|
+
print(f"KS Analysis results will be saved in {self.file_path}")
|
|
36
|
+
|
|
37
|
+
# Store pairs that pass the test
|
|
38
|
+
passing_pairs = []
|
|
39
|
+
failing_pairs = []
|
|
40
|
+
|
|
41
|
+
# Open file for writing
|
|
42
|
+
with open(self.file_path, 'w', encoding='utf-8') as f:
|
|
43
|
+
# Write header
|
|
44
|
+
f.write(f"Kolmogorov-Smirnov Analysis Results\n")
|
|
45
|
+
f.write(f"{'='*50}\n")
|
|
46
|
+
f.write(f"Metadata: {self.metadata.name}\n")
|
|
47
|
+
f.write(f"{'='*50}\n\n")
|
|
48
|
+
|
|
49
|
+
#compute for all pair
|
|
50
|
+
tuples = self.get_all_set_from_workflow(workflow)
|
|
51
|
+
|
|
52
|
+
if len(tuples) < 2:
|
|
53
|
+
f.write("Warning: Less than 2 sets found. Cannot perform pairwise analysis.\n")
|
|
54
|
+
print("Warning: Less than 2 sets found. Cannot perform pairwise analysis.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
comparison_count = 0
|
|
58
|
+
for i in range(0, len(tuples)):
|
|
59
|
+
for j in range(i, len(tuples)):
|
|
60
|
+
set_1, op_1 = tuples[i]
|
|
61
|
+
set_2, op_2 = tuples[j]
|
|
62
|
+
|
|
63
|
+
comparison_count += 1
|
|
64
|
+
|
|
65
|
+
# Write comparison header
|
|
66
|
+
op_1_name = op_1.__class__.__name__ if op_1 else 'Initial set'
|
|
67
|
+
op_1_name = f"Set #{i}"+op_1_name
|
|
68
|
+
op_2_name = op_2.__class__.__name__ if op_2 else 'Initial Input'
|
|
69
|
+
op_2_name = f"Set #{j}"+op_2_name
|
|
70
|
+
f.write(f"Comparison #{comparison_count}\n")
|
|
71
|
+
f.write(f"Set 1: {op_1_name} (size: {set_1.size()})\n")
|
|
72
|
+
f.write(f"Set 2: {op_2_name} (size: {set_2.size()})\n")
|
|
73
|
+
f.write(f"{'-'*30}\n")
|
|
74
|
+
|
|
75
|
+
# Perform KS analysis
|
|
76
|
+
print(f"Analyzing sets {i} from {op_1_name} and {j} from {op_2_name}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
ks_result = kolmogorov_smirnov(self.metadata).analyze(set_1, set_2)
|
|
80
|
+
|
|
81
|
+
# Extract results from KstestResult object
|
|
82
|
+
statistic = ks_result.statistic if hasattr(ks_result, 'statistic') else 'N/A'
|
|
83
|
+
p_value = ks_result.pvalue if hasattr(ks_result, 'pvalue') else 'N/A'
|
|
84
|
+
|
|
85
|
+
# Create interpretation
|
|
86
|
+
if p_value != 'N/A':
|
|
87
|
+
if p_value > 0.05:
|
|
88
|
+
interpretation = "No significant difference (p > 0.05)"
|
|
89
|
+
else:
|
|
90
|
+
interpretation = "Significant difference detected (p ≤ 0.05)"
|
|
91
|
+
else:
|
|
92
|
+
interpretation = "Unable to determine"
|
|
93
|
+
|
|
94
|
+
# Check if test passes (typically p_value > 0.05 means no significant difference)
|
|
95
|
+
test_passed = p_value != 'N/A' and float(p_value) > 0.05
|
|
96
|
+
|
|
97
|
+
pair_info = {
|
|
98
|
+
'comparison': comparison_count,
|
|
99
|
+
'set_1': op_1_name,
|
|
100
|
+
'set_2': op_2_name,
|
|
101
|
+
'statistic': statistic,
|
|
102
|
+
'p_value': p_value,
|
|
103
|
+
'interpretation': interpretation
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if test_passed:
|
|
107
|
+
passing_pairs.append(pair_info)
|
|
108
|
+
f.write(f"✓ TEST PASSED - ")
|
|
109
|
+
else:
|
|
110
|
+
failing_pairs.append(pair_info)
|
|
111
|
+
f.write(f"✗ TEST FAILED - ")
|
|
112
|
+
|
|
113
|
+
# Write results to file
|
|
114
|
+
f.write(f"KS Statistic: {statistic}\n")
|
|
115
|
+
f.write(f"P-value: {p_value}\n")
|
|
116
|
+
f.write(f"Interpretation: {interpretation}\n")
|
|
117
|
+
|
|
118
|
+
# Print to console as well
|
|
119
|
+
status = "PASSED" if test_passed else "FAILED"
|
|
120
|
+
print(f" [{status}] KS Statistic: {statistic}, P-value: {p_value}")
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
error_msg = f"Error during analysis: {str(e)}"
|
|
124
|
+
f.write(f"{error_msg}\n")
|
|
125
|
+
print(f" {error_msg}")
|
|
126
|
+
failing_pairs.append({
|
|
127
|
+
'comparison': comparison_count,
|
|
128
|
+
'set_1': op_1_name,
|
|
129
|
+
'set_2': op_2_name,
|
|
130
|
+
'error': str(e)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
f.write(f"\n")
|
|
134
|
+
|
|
135
|
+
# Write summary section
|
|
136
|
+
f.write(f"\n{'='*50}\n")
|
|
137
|
+
f.write(f"SUMMARY\n")
|
|
138
|
+
f.write(f"{'='*50}\n")
|
|
139
|
+
f.write(f"Total comparisons: {comparison_count}\n")
|
|
140
|
+
f.write(f"Tests passed: {len(passing_pairs)}\n")
|
|
141
|
+
f.write(f"Tests failed: {len(failing_pairs)}\n")
|
|
142
|
+
f.write(f"\nPassing pairs (no significant difference):\n")
|
|
143
|
+
f.write(f"{'-'*30}\n")
|
|
144
|
+
|
|
145
|
+
if passing_pairs:
|
|
146
|
+
for pair in passing_pairs:
|
|
147
|
+
f.write(f"• {pair['set_1']} vs {pair['set_2']} (p-value: {pair['p_value']})\n")
|
|
148
|
+
else:
|
|
149
|
+
f.write("None\n")
|
|
150
|
+
|
|
151
|
+
f.write(f"\nFailing pairs (significant difference detected):\n")
|
|
152
|
+
f.write(f"{'-'*30}\n")
|
|
153
|
+
|
|
154
|
+
if failing_pairs:
|
|
155
|
+
for pair in failing_pairs:
|
|
156
|
+
if 'error' in pair:
|
|
157
|
+
f.write(f"• {pair['set_1']} vs {pair['set_2']} (Error: {pair['error']})\n")
|
|
158
|
+
else:
|
|
159
|
+
f.write(f"• {pair['set_1']} vs {pair['set_2']} (p-value: {pair['p_value']})\n")
|
|
160
|
+
else:
|
|
161
|
+
f.write("None\n")
|
|
162
|
+
|
|
163
|
+
f.write(f"\nAnalysis completed at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
164
|
+
|
|
165
|
+
# Print summary to console
|
|
166
|
+
print(f"\n{'='*50}")
|
|
167
|
+
print(f"KS ANALYSIS SUMMARY")
|
|
168
|
+
print(f"{'='*50}")
|
|
169
|
+
print(f"Total comparisons: {comparison_count}")
|
|
170
|
+
print(f"Tests passed: {len(passing_pairs)}")
|
|
171
|
+
print(f"Tests failed: {len(failing_pairs)}")
|
|
172
|
+
|
|
173
|
+
if passing_pairs:
|
|
174
|
+
print(f"\nPassing pairs (no significant difference):")
|
|
175
|
+
for pair in passing_pairs:
|
|
176
|
+
print(f" ✓ {pair['set_1']} vs {pair['set_2']} (p-value: {pair['p_value']})")
|
|
177
|
+
|
|
178
|
+
if failing_pairs:
|
|
179
|
+
print(f"\nFailing pairs (significant difference detected):")
|
|
180
|
+
for pair in failing_pairs:
|
|
181
|
+
if 'error' in pair:
|
|
182
|
+
print(f" ✗ {pair['set_1']} vs {pair['set_2']} (Error)")
|
|
183
|
+
else:
|
|
184
|
+
print(f" ✗ {pair['set_1']} vs {pair['set_2']} (p-value: {pair['p_value']})")
|
|
185
|
+
|
|
186
|
+
print(f"\nResults saved to: {self.file_path}")
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
'passing_pairs': passing_pairs,
|
|
190
|
+
'failing_pairs': failing_pairs,
|
|
191
|
+
'total_comparisons': comparison_count
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_all_set_from_workflow(self, workflow: Workflow, index=0):
|
|
199
|
+
sets = {}
|
|
200
|
+
if index==0:
|
|
201
|
+
sets[index] = (workflow._input,None)
|
|
202
|
+
index += 1
|
|
203
|
+
|
|
204
|
+
op = workflow.get_root()
|
|
205
|
+
while op is not None:
|
|
206
|
+
if not isinstance(op, GroupingOperator):
|
|
207
|
+
if hasattr(op, "_output") and op._output is not None:
|
|
208
|
+
sets[index] = (op._output,op)
|
|
209
|
+
index += 1
|
|
210
|
+
else:
|
|
211
|
+
for internal_w in op.get_workflows():
|
|
212
|
+
grouping_sets = self.get_all_set_from_workflow(internal_w, index)
|
|
213
|
+
index = index + len(grouping_sets)
|
|
214
|
+
sets.update(grouping_sets)
|
|
215
|
+
op = op.get_next_operator()
|
|
216
|
+
|
|
217
|
+
return sets
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class YamaneTest:
|
|
5
|
+
def __init__(self, population_size: int, margin_of_error: float):
|
|
6
|
+
self.population_size = population_size
|
|
7
|
+
self.margin_of_error = margin_of_error
|
|
8
|
+
|
|
9
|
+
def calculate_required_sample_size(self) -> int:
|
|
10
|
+
return math.ceil(
|
|
11
|
+
self.population_size / (1 + self.population_size * self.margin_of_error**2)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def is_representative(self, given_sample_size: int) -> bool:
|
|
15
|
+
required_sample_size = self.calculate_required_sample_size()
|
|
16
|
+
return given_sample_size >= required_sample_size
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from sampling_mining_workflows_dsl.analysis.WorkflowAnalysis import WorkflowAnalysis
|
|
2
|
+
from sampling_mining_workflows_dsl.analysis.YamaneTest import YamaneTest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class YamaneWorkflowAnalysis(WorkflowAnalysis):
|
|
6
|
+
def __init__(self, margin_of_error: float = 0.05):
|
|
7
|
+
super().__init__()
|
|
8
|
+
self.margin_of_error = margin_of_error
|
|
9
|
+
|
|
10
|
+
def analyze(self, workflow):
|
|
11
|
+
pop_size = workflow.get_workflow_input().size()
|
|
12
|
+
yamane_test = YamaneTest(pop_size, self.margin_of_error)
|
|
13
|
+
|
|
14
|
+
required_sample_size = yamane_test.calculate_required_sample_size()
|
|
15
|
+
actual_sample_size = workflow.get_workflow_output().size()
|
|
16
|
+
|
|
17
|
+
print("Yamane's Test Analysis:")
|
|
18
|
+
print("Population Size (input size):", pop_size)
|
|
19
|
+
print("Required Sample Size:", required_sample_size)
|
|
20
|
+
print("Actual Sample Size:", actual_sample_size)
|
|
21
|
+
|
|
22
|
+
if yamane_test.is_representative(actual_sample_size):
|
|
23
|
+
print("The sample is representative based on Yamane's test.")
|
|
24
|
+
else:
|
|
25
|
+
print("The sample is not representative based on Yamane's test.")
|
|
26
|
+
|
|
27
|
+
print("------------------------------------------")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
from scipy.stats import ks_2samp
|
|
3
|
+
|
|
4
|
+
from sampling_mining_workflows_dsl.element.Repository import Repository
|
|
5
|
+
from sampling_mining_workflows_dsl.element.Set import Set
|
|
6
|
+
from sampling_mining_workflows_dsl.metadata.Metadata import Metadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class kolmogorov_smirnov:
|
|
10
|
+
def __init__(self, metadata: Metadata[int]):
|
|
11
|
+
self.metadata = metadata
|
|
12
|
+
|
|
13
|
+
def analyze(self, a: Set, sample: Set) -> float:
|
|
14
|
+
first_list = self.extract_list(a)
|
|
15
|
+
second_list = self.extract_list(sample)
|
|
16
|
+
|
|
17
|
+
# Perform the Kolmogorov-Smirnov test
|
|
18
|
+
res = ks_2samp(first_list, second_list)
|
|
19
|
+
return res
|
|
20
|
+
|
|
21
|
+
def extract_list(self, s: Set) -> list[int]:
|
|
22
|
+
metadata_values = []
|
|
23
|
+
|
|
24
|
+
for element in s.get_elements():
|
|
25
|
+
if isinstance(element, Repository):
|
|
26
|
+
repo: Repository = element
|
|
27
|
+
metadata_value = repo.get_metadata_value(self.metadata)
|
|
28
|
+
if metadata_value:
|
|
29
|
+
metadata_values.append(metadata_value.get_value())
|
|
30
|
+
|
|
31
|
+
return metadata_values
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from sampling_mining_workflows_dsl.constraint.Comparator import Comparator
|
|
5
|
+
from sampling_mining_workflows_dsl.element.Element import Element
|
|
6
|
+
from sampling_mining_workflows_dsl.metadata.Metadata import Metadata
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BoolComparator[T](Comparator[T]):
|
|
12
|
+
def __init__(
|
|
13
|
+
self, targeted_metadata: Metadata[T], comparator: Callable[[T, T], T] = None
|
|
14
|
+
):
|
|
15
|
+
super().__init__(targeted_metadata)
|
|
16
|
+
self.comparator = comparator
|
|
17
|
+
|
|
18
|
+
def compare(self, a: Element, b: Element) -> Element:
|
|
19
|
+
a_value = a.get_metadata_value(self.targeted_metadata).get_value()
|
|
20
|
+
b_value = b.get_metadata_value(self.targeted_metadata).get_value()
|
|
21
|
+
result = self.comparator(a_value, b_value)
|
|
22
|
+
|
|
23
|
+
return a if result == a_value else b
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from sampling_mining_workflows_dsl.constraint.Constraint import Constraint
|
|
5
|
+
from sampling_mining_workflows_dsl.element.Element import Element
|
|
6
|
+
from sampling_mining_workflows_dsl.metadata.Metadata import Metadata
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BoolConstraint(Constraint[T]):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
workflow,
|
|
15
|
+
constraint: Callable[[tuple[T, ...]], bool],
|
|
16
|
+
*targeted_metadatas: tuple[Metadata[T], ...],
|
|
17
|
+
):
|
|
18
|
+
super().__init__(workflow, *targeted_metadatas)
|
|
19
|
+
self.constraint = constraint
|
|
20
|
+
self.or_constraint: Constraint | None = None
|
|
21
|
+
self.and_constraint: Constraint | None = None
|
|
22
|
+
|
|
23
|
+
def is_satisfied(self, element: Element) -> bool:
|
|
24
|
+
if self.or_constraint is not None and self.and_constraint is not None:
|
|
25
|
+
raise RuntimeError("Both 'and' & 'or' constraints are defined")
|
|
26
|
+
|
|
27
|
+
value_objs = [
|
|
28
|
+
element.get_metadata_value(target_metadata).get_value()
|
|
29
|
+
for target_metadata in self.targeted_metadatas
|
|
30
|
+
]
|
|
31
|
+
# TODO add type check
|
|
32
|
+
# if not isinstance(value_objs, self.targeted_metadatas.type):
|
|
33
|
+
# raise RuntimeError(f"Unexpected metadata type: {type(value_objs)}")
|
|
34
|
+
|
|
35
|
+
constraint_result = self.constraint(*value_objs)
|
|
36
|
+
|
|
37
|
+
if self.or_constraint is not None:
|
|
38
|
+
return constraint_result or self.or_constraint.is_satisfied(element)
|
|
39
|
+
if self.and_constraint is not None:
|
|
40
|
+
return constraint_result and self.and_constraint.is_satisfied(element)
|
|
41
|
+
|
|
42
|
+
return constraint_result
|
|
43
|
+
|
|
44
|
+
def or_(self, other: "BoolConstraint") -> "BoolConstraint":
|
|
45
|
+
self.or_constraint = other
|
|
46
|
+
return other
|
|
47
|
+
|
|
48
|
+
def and_(self, other: "BoolConstraint") -> "BoolConstraint":
|
|
49
|
+
self.and_constraint = other
|
|
50
|
+
return other
|