action-rules 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.
File without changes
@@ -0,0 +1,357 @@
1
+ import copy
2
+ import itertools
3
+ from collections import defaultdict
4
+
5
+ import pandas as pd
6
+
7
+
8
+ # Reduce candidates to required minimum number of stable or flexible attributes.
9
+ def reduce_candidates_min_attributes(K, actionable_attributes, stable_items_binding, min_stable_attributes,
10
+ flexible_items_binding, min_flexible_attributes):
11
+ # Reduce by min stable and flexible
12
+ number_of_stable_attributes = len(stable_items_binding) - (min_stable_attributes - K)
13
+ if K > min_stable_attributes:
14
+ number_of_flexible_attributes = len(flexible_items_binding) - (
15
+ min_flexible_attributes - actionable_attributes - 1)
16
+ else:
17
+ number_of_flexible_attributes = 0
18
+ reduced_stable_items_binding = {k: stable_items_binding[k] for k in
19
+ list(stable_items_binding.keys())[:number_of_stable_attributes]}
20
+ reduced_flexible_items_binding = {k: flexible_items_binding[k] for k in
21
+ list(flexible_items_binding.keys())[:number_of_flexible_attributes]}
22
+ return reduced_stable_items_binding, reduced_flexible_items_binding
23
+
24
+
25
+ # Check if itemset is in the stop list.
26
+ def in_stop_list(ar_prefix, stop_list):
27
+ if ar_prefix[-2:] in stop_list:
28
+ return True
29
+ if ar_prefix[1:] in stop_list:
30
+ stop_list.append(ar_prefix)
31
+ return True
32
+ return False
33
+
34
+
35
+ # Generate candidates and count their support
36
+ # Node derived from stable attribute must simultaneously have sufficient support for the desired and undesired classes in the consequent.
37
+ # Group of nodes representing flexible attributes must simultaneously have required min. support for the desired and undesired classes.
38
+ def generate_candidates(ar_prefix, itemset_prefix, stable_items_binding, flexible_items_binding, undesired_mask,
39
+ desired_mask, actionable_attributes=0, item=0, stop_list=[], frames=None, undesired_state=0,
40
+ desired_state=1, stop_list_itemset=[], classification_rules=[], verbose=False):
41
+ K = len(itemset_prefix) + 1
42
+ reduced_stable_items_binding, reduced_flexible_items_binding = reduce_candidates_min_attributes(K,
43
+ actionable_attributes,
44
+ stable_items_binding,
45
+ min_stable_attributes,
46
+ flexible_items_binding,
47
+ min_flexible_attributes)
48
+
49
+ if undesired_mask is None:
50
+ undesired_frame = frames[undesired_state]
51
+ desired_frame = frames[desired_state]
52
+ else:
53
+ undesired_frame = frames[undesired_state].multiply(undesired_mask, axis="index")
54
+ desired_frame = frames[desired_state].multiply(desired_mask, axis="index")
55
+
56
+ stable_candidates = copy.deepcopy(stable_items_binding)
57
+ flexible_candidates = copy.deepcopy(flexible_items_binding)
58
+
59
+ new_branches = []
60
+
61
+ for attribute, items in reduced_stable_items_binding.items():
62
+ for item in items:
63
+
64
+ new_ar_prefix = ar_prefix + (item,)
65
+ if in_stop_list(new_ar_prefix, stop_list):
66
+ continue
67
+
68
+ undesired_support = undesired_frame[item].sum()
69
+ desired_support = desired_frame[item].sum()
70
+
71
+ if verbose:
72
+ print('SUPPORT')
73
+ print(itemset_prefix + (item,))
74
+ print((undesired_support, desired_support))
75
+
76
+ if undesired_support < min_undesired_support or desired_support < min_desired_support:
77
+ stable_candidates[attribute].remove(item)
78
+ stop_list.append(new_ar_prefix)
79
+ else:
80
+ new_branches.append({'ar_prefix': new_ar_prefix,
81
+ 'itemset_prefix': new_ar_prefix,
82
+ 'item': item,
83
+ 'undesired_mask': undesired_frame[item],
84
+ 'desired_mask': desired_frame[item],
85
+ 'actionable_attributes': 0,
86
+ })
87
+
88
+ for attribute, items in reduced_flexible_items_binding.items():
89
+
90
+ new_ar_prefix = ar_prefix + (attribute,)
91
+ if in_stop_list(new_ar_prefix, stop_list):
92
+ continue
93
+
94
+ undesired_states = []
95
+ desired_states = []
96
+ undesired_count = 0
97
+ desired_count = 0
98
+ for item in items:
99
+
100
+ if in_stop_list(itemset_prefix + (item,), stop_list_itemset):
101
+ continue
102
+
103
+ undesired_support = undesired_frame[item].sum()
104
+ desired_support = desired_frame[item].sum()
105
+
106
+ if verbose:
107
+ print('SUPPORT')
108
+ print(itemset_prefix + (item,))
109
+ print((undesired_support, desired_support))
110
+
111
+ # is undesired
112
+ if desired_support + undesired_support == 0:
113
+ undesired_conf = 0
114
+ else:
115
+ undesired_conf = undesired_support / (desired_support + undesired_support)
116
+ if undesired_support >= min_undesired_support:
117
+ undesired_count += 1
118
+ if undesired_conf >= min_undesired_confidence:
119
+ undesired_states.append({'item': item, 'support': undesired_support, 'confidence': undesired_conf})
120
+ # is desired
121
+ if desired_support + undesired_support == 0:
122
+ desired_conf = 0
123
+ else:
124
+ desired_conf = desired_support / (desired_support + undesired_support)
125
+ if desired_support >= min_desired_support:
126
+ desired_count += 1
127
+ if desired_conf >= min_desired_confidence:
128
+ desired_states.append({'item': item, 'support': desired_support, 'confidence': desired_conf})
129
+ if desired_support < min_desired_support and undesired_support < min_undesired_support:
130
+ flexible_candidates[attribute].remove(item)
131
+ stop_list_itemset.append(itemset_prefix + (item,))
132
+
133
+ if actionable_attributes == 0 and (undesired_count == 0 or desired_count == 0): # just for first flexible level
134
+ del flexible_candidates[attribute]
135
+ stop_list.append(ar_prefix + (attribute,))
136
+ else:
137
+ for item in items:
138
+ new_branches.append({'ar_prefix': new_ar_prefix,
139
+ 'itemset_prefix': itemset_prefix + (item,),
140
+ 'item': item,
141
+ 'undesired_mask': undesired_frame[item],
142
+ 'desired_mask': desired_frame[item],
143
+ 'actionable_attributes': actionable_attributes + 1,
144
+ })
145
+
146
+ if actionable_attributes + 1 >= min_flexible_attributes:
147
+ for undesired_item in undesired_states:
148
+ new_itemset_prefix = itemset_prefix + (undesired_item['item'],)
149
+ classification_rules[new_ar_prefix]['undesired'].append({
150
+ 'itemset': new_itemset_prefix,
151
+ 'support': undesired_item['support'],
152
+ 'confidence': undesired_item['confidence'],
153
+ 'target': desired_change_in_target[0]
154
+ })
155
+ for desired_item in desired_states:
156
+ new_itemset_prefix = itemset_prefix + (desired_item['item'],)
157
+ classification_rules[new_ar_prefix]['desired'].append({
158
+ 'itemset': new_itemset_prefix,
159
+ 'support': desired_item['support'],
160
+ 'confidence': desired_item['confidence'],
161
+ 'target': desired_change_in_target[1]
162
+ })
163
+
164
+ for new_branch in new_branches:
165
+ adding = False
166
+ new_stable = {}
167
+ new_flexible = {}
168
+
169
+ for attribute, items in stable_candidates.items():
170
+ for item in items:
171
+ if adding:
172
+ if attribute not in new_stable:
173
+ new_stable[attribute] = []
174
+ new_stable[attribute].append(item)
175
+ if item == new_branch['item']:
176
+ adding = True
177
+
178
+ for attribute, items in flexible_candidates.items():
179
+ for item in items:
180
+ if adding:
181
+ if attribute not in new_flexible:
182
+ new_flexible[attribute] = []
183
+ new_flexible[attribute].append(item)
184
+ if item == new_branch['item']:
185
+ adding = True
186
+
187
+ new_branch['stable_items_binding'] = new_stable
188
+ new_branch['flexible_items_binding'] = new_flexible
189
+
190
+ return new_branches
191
+
192
+
193
+ # Generate action rules from classification rules
194
+ def generate_action_rules(classification_rules, action_rules):
195
+ for attribute_prefix, rules in classification_rules.items():
196
+ for desired_rule in rules['desired']:
197
+ for undesired_rule in rules['undesired']:
198
+ action_rules.append({'undesired': undesired_rule, 'desired': desired_rule})
199
+
200
+
201
+ # Prune tree
202
+ def prune_tree(K, classification_rules, stop_list):
203
+ for attribute_prefix, rules in classification_rules.items():
204
+ if K == len(attribute_prefix):
205
+ if len(rules['desired']) < 0 or len(rules['undesired']) < 0:
206
+ stop_list.append(attribute_prefix)
207
+ del classification_rules[attribute_prefix]
208
+
209
+
210
+ # Get the dictionaries of attributes and their values
211
+ def get_bindings(data, stable_attributes, flexible_attributes, target):
212
+ stable_items_binding = defaultdict(lambda: [])
213
+ flexible_items_binding = defaultdict(lambda: [])
214
+ target_items_binding = defaultdict(lambda: [])
215
+
216
+ for col in data.columns:
217
+ is_continue = False
218
+ # stable
219
+ for attribute in stable_attributes:
220
+ if col.startswith(attribute + '_<item>_'):
221
+ stable_items_binding[attribute].append(col)
222
+ is_continue = True
223
+ break
224
+ if is_continue is True:
225
+ continue
226
+ # flexible
227
+ for attribute in flexible_attributes:
228
+ if col.startswith(attribute + '_<item>_'):
229
+ flexible_items_binding[attribute].append(col)
230
+ is_continue = True
231
+ break
232
+ if is_continue is True:
233
+ continue
234
+ # target
235
+ if col.startswith(target + '_<item>_'):
236
+ target_items_binding[target].append(col)
237
+ return stable_items_binding, flexible_items_binding, target_items_binding
238
+
239
+
240
+ # Create default stop list
241
+ def get_stop_list(stable_items_binding, flexible_items_binding):
242
+ stop_list = []
243
+ for items in stable_items_binding.values():
244
+ for stop_couple in itertools.product(items, repeat=2):
245
+ stop_list.append(tuple(stop_couple))
246
+ for item in flexible_items_binding.keys():
247
+ stop_list.append(tuple([item, item]))
248
+ return stop_list
249
+
250
+
251
+ # Split data to desired and undesired
252
+ def get_split_tables(data, target_items_binding, target):
253
+ frames = {}
254
+ for item in target_items_binding[target]:
255
+ mask = data[item] == 1
256
+ frames[item] = data[mask]
257
+ return frames
258
+
259
+
260
+ # Action-Apriori main function
261
+ def action_apriori(data, stable_attributes, flexible_attributes, target, desired_change_in_target_l,
262
+ min_stable_attributes_l, min_flexible_attributes_l, min_undesired_support_l,
263
+ min_undesired_confidence_l, min_desired_support_l, min_desired_confidence_l, verbose=False):
264
+ # Global variables
265
+ global min_stable_attributes
266
+ global min_flexible_attributes
267
+ global min_undesired_support
268
+ global min_desired_support
269
+ global min_undesired_confidence
270
+ global min_desired_confidence
271
+ global desired_change_in_target
272
+ min_stable_attributes = min_stable_attributes_l
273
+ min_flexible_attributes = min_flexible_attributes_l
274
+ min_undesired_support = min_undesired_support_l
275
+ min_desired_support = min_desired_support_l
276
+ min_undesired_confidence = min_undesired_confidence_l
277
+ min_desired_confidence = min_desired_confidence_l
278
+ desired_change_in_target = desired_change_in_target_l
279
+
280
+ data = pd.get_dummies(data, sparse=False, columns=data.columns, prefix_sep='_<item>_')
281
+ stable_items_binding, flexible_items_binding, target_items_binding = get_bindings(data, stable_attributes,
282
+ flexible_attributes, target)
283
+ stop_list = get_stop_list(stable_items_binding, flexible_items_binding)
284
+ frames = get_split_tables(data, target_items_binding, target)
285
+ undesired_state = target + '_<item>_' + str(desired_change_in_target[0])
286
+ desired_state = target + '_<item>_' + str(desired_change_in_target[1])
287
+ action_rules = []
288
+ classification_rules = defaultdict(lambda: {'desired': [], 'undesired': []})
289
+ stop_list_itemset = []
290
+
291
+ candidates_queue = [{
292
+ 'ar_prefix': tuple(),
293
+ 'itemset_prefix': tuple(),
294
+ 'stable_items_binding': stable_items_binding,
295
+ 'flexible_items_binding': flexible_items_binding,
296
+ 'undesired_mask': None,
297
+ 'desired_mask': None,
298
+ 'actionable_attributes': 0
299
+ }]
300
+ K = 0
301
+ while len(candidates_queue) > 0:
302
+ candidate = candidates_queue.pop(0)
303
+ if len(candidate['ar_prefix']) > K:
304
+ K += 1
305
+ prune_tree(K, classification_rules, stop_list)
306
+ new_candidates = generate_candidates(**candidate, stop_list=stop_list, frames=frames,
307
+ undesired_state=undesired_state, desired_state=desired_state,
308
+ stop_list_itemset=stop_list_itemset,
309
+ classification_rules=classification_rules, verbose=verbose)
310
+ candidates_queue += new_candidates
311
+ generate_action_rules(classification_rules, action_rules)
312
+ return action_rules
313
+
314
+
315
+ def get_ar_notation(ar_dict, target):
316
+ rule = '['
317
+ for i, item in enumerate(ar_dict['undesired']['itemset']):
318
+ if i > 0:
319
+ rule += ' ∧ '
320
+ rule += '('
321
+ if item == ar_dict['desired']['itemset'][i]:
322
+ val = item.split('_<item>_')
323
+ rule += str(val[0]) + ': ' + str(val[1])
324
+ else:
325
+ val = item.split('_<item>_')
326
+ val_desired = ar_dict['desired']['itemset'][i].split('_<item>_')
327
+ rule += str(val[0]) + ': ' + str(val[1]) + ' → ' + str(val_desired[1])
328
+ rule += ')'
329
+ rule += '] ⇒ [' + str(target) + ': ' + str(ar_dict['undesired']['target']) + ' → ' + str(
330
+ ar_dict['desired']['target']) + ']'
331
+ rule += ', support of undesired part: ' + str(
332
+ ar_dict['undesired']['support']) + ', confidence of undesired part: ' + str(ar_dict['undesired']['confidence'])
333
+ rule += ', support of desired part: ' + str(ar_dict['desired']['support']) + ', confidence of desired part: ' + str(
334
+ ar_dict['desired']['confidence'])
335
+ return rule
336
+
337
+
338
+ def get_export_notation(action_rules, target):
339
+ rules = []
340
+ for ar_dict in action_rules:
341
+ rule = {'stable': [], 'flexible': []}
342
+ for i, item in enumerate(ar_dict['undesired']['itemset']):
343
+ if item == ar_dict['desired']['itemset'][i]:
344
+ val = item.split('_<item>_')
345
+ rule['stable'].append({'attribute': val[0], 'value': val[1]})
346
+ else:
347
+ val = item.split('_<item>_')
348
+ val_desired = ar_dict['desired']['itemset'][i].split('_<item>_')
349
+ rule['flexible'].append({'attribute': val[0], 'undesired': val[1], 'desired': val_desired[1]})
350
+ rule['target'] = {'attribute': target, 'undesired': ar_dict['undesired']['target'],
351
+ 'desired': ar_dict['desired']['target']}
352
+ rule['support of undesired part'] = ar_dict['undesired']['support']
353
+ rule['confidence of undesired part'] = ar_dict['undesired']['confidence']
354
+ rule['support of desired part'] = ar_dict['desired']['support']
355
+ rule['confidence of desired part'] = ar_dict['desired']['confidence']
356
+ rules.append(rule)
357
+ return rules
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lukas Sykora
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.1
2
+ Name: action-rules
3
+ Version: 0.0.1
4
+ Summary: Action Rules Mining Tool.
5
+ Home-page: https://github.com/lukassykora/actionrules
6
+ Author: Lukas Sykora
7
+ Author-email: lukas.sykora@vse.cz
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.txt
12
+ Requires-Dist: pandas (~=2.2.2)
13
+
14
+ # Action-Apriori (Apriori Modified for Action Rules Mining)
15
+
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
+
18
+
19
+ ## Installation
20
+ The Action-Apriori script needs the following libraries:
21
+ - itertools (built-in module in python)
22
+ - copy (built-in module in python)
23
+ - collections (built-in module in python)
24
+ - pandas (1.3.4)
25
+
26
+ The tested Python version is: 3.9.7
27
+
28
+ The action_apriori function can be called:
29
+
30
+ ```python
31
+ import action_rules as ar
32
+ import pandas as pd
33
+ # Data
34
+ transactions = {'Sex': ['M', 'F', 'M', 'M', 'F', 'M', 'F'],
35
+ 'Age': ['Y', 'Y', 'O', 'Y', 'Y', 'O', 'Y'],
36
+ 'Class': [1, 1, 2, 2, 1, 1, 2],
37
+ 'Embarked': ['S', 'C', 'S', 'C', 'S', 'C', 'C'],
38
+ 'Survived': [1, 1, 0, 0, 1, 1, 0],
39
+ }
40
+ data = pd.DataFrame.from_dict(transactions)
41
+ # Parameters
42
+ stable_attributes = ['Sex','Age']
43
+ flexible_attributes = ['Class','Embarked']
44
+ target = 'Survived'
45
+ wanted_change_in_target = [0, 1]
46
+ min_stable_attributes = 2
47
+ min_flexible_attributes = 1 #min 1
48
+ min_unwanted_support = 1
49
+ min_unwanted_confidence = 0.5 #min 0.5
50
+ min_wanted_support = 2
51
+ min_wanted_confidence = 0.5 #min 0.5
52
+ # Action Rules Mining
53
+ action_rules = ar.action_apriori(
54
+ data,
55
+ stable_attributes,
56
+ flexible_attributes,
57
+ target,
58
+ wanted_change_in_target,
59
+ min_stable_attributes ,
60
+ min_flexible_attributes,
61
+ min_unwanted_support,
62
+ min_unwanted_confidence,
63
+ min_wanted_support,
64
+ min_wanted_confidence,
65
+ True) #verbose
66
+ # Print rules
67
+ for action_rule in action_rules:
68
+ print(action_rule)
69
+ # Print rules with action rules notation
70
+ for action_rule in action_rules:
71
+ print(ar.get_ar_notation(action_rule, target))
72
+ # Print rules with export notation
73
+ print(ar.get_export_notation(action_rules, target))
74
+ ```
75
+
76
+ The output: ation rule with notation:
77
+
78
+ ```python
79
+ [(Sex: F) ∧ (Age: Y) ∧ (Class: 2 → 1)] ⇒ [Survived: 0 → 1], support of undesired part: 1, confidence of undesired part: 1.0, support of desired part: 2, confidence of desired part: 1.0
80
+ ```
81
+
82
+
83
+
84
+
@@ -0,0 +1,7 @@
1
+ action_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ action_rules/action_rules.py,sha256=BSVQ5CiQp3TimnqGLp_lQTskX9kmafG9gCwxJkHyyqw,16926
3
+ action_rules-0.0.1.dist-info/LICENSE.txt,sha256=gEr-nYwCX9aGVHiZSsOVYQb4TFlbKfQVmmd0-Gun2t4,1088
4
+ action_rules-0.0.1.dist-info/METADATA,sha256=9vL4OiGIp9pp6geT2ErXVI3wmTGg_WypwXT0zNkFiK0,2419
5
+ action_rules-0.0.1.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92
6
+ action_rules-0.0.1.dist-info/top_level.txt,sha256=4uSeKPOWLOUAFeSknZThyYBY1Ra9pQb_wNWm8GRCxcE,13
7
+ action_rules-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.36.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ action_rules