names=RETAINED_DATA_COLUMNS, index_col=False, skipinitialspace=True) anonymous_df = ola.anonymize(df, k=k, info_loss=prec_loss, generalization_rules=generalization_rules, max_sup=0.05) out_file_path = 'output/out_ola_k_{}_adult-prep.data'.format(k) # Export dataset anonymous_df[0].fillna('*', inplace=True) anonymous_df[0].to_csv(ola_abs_output_path, index=False, header=False) dataset = pandas.read_csv(ola_abs_output_path, names=RETAINED_DATA_COLUMNS, index_col=False, skipinitialspace=True) GROUPS = dataset.groupby(QUASI_ATTRIBUTES) total_time = time.time() - start_time eval_results(R_initial, GROUPS, ola_abs_output_path, total_time, other_algo=True, k=k) sys.stdout.close()
def m3ar_modified_algo(D, R_initial, output_file_name='m3ar_modified.data', k=DESIRED_K): start_time = time.time() # First construct R care R_care = construct_r_care(R_initial) print('R CARE LENGTH =', len(R_care)) # pprint_rule_set(R_care) rule_budgets = [rule.budget for rule in R_care] print('R CARE BUDGETS AT INITIAL:') print(rule_budgets) print( '===============================================================================' ) # Build groups from the dataset then split G into 2 sets of groups: safe groups SG and unsafe groups UG # Sort groups in UG and SG by length ascendingly GROUPS, SG, UG, _, _ = build_groups(D, R_care=R_care, k=k) print('THERE ARE {} SAFE GROUPS AND {} UNSAFE GROUPS'.format( len(SG), len(UG))) # cal_number_of_free_tuples(GROUPS, k) free_tuples = generate_free_tuples(SG, k) SelG = None loop_iteration = 0 # STAGE 1: Pick free tuples into unsafe groups while len(UG) > 0 and len( free_tuples) > 0 and all_rules_having_positive_budgets(R_care): loop_iteration += 1 if SelG is None: # MODIFICATION 1: UG was already sorted by group length. Process the UG groups one by one, start with the unsafe group with the longest length # The longer the group length, the more chance it will soon become a safe group SelG = UG.pop(0) # print('LOOP ITERATION {}. Pop the unsafe group with the longest length. SelG index: {}. SelG length: {}'.format(loop_iteration, SelG.index, group_length(SelG))) # print('Rules budget now are:') # print([rule.budget for rule in R_care]) no_tuples_needed_to_become_a_safe_group = k - group_length(SelG) no_tuples_picked = 0 free_tuples_to_remove = [] for data_tuple in free_tuples: source_group = find_group(data_tuple.group_index, GROUPS) R_affected = construct_r_affected_by_a_migration( source_group, SelG) if all_rules_having_positive_budgets(R_affected): for rule in R_affected: rule.budget -= 1 # Perform a migration of this free tuple to the destination group convert_quasi_attributes(data_tuple, SelG) SelG.received_tuples.append(data_tuple) # print('Data tuple picked is from group {}'.format(data_tuple.group_index)) source_group.origin_tuples.remove(data_tuple) free_tuples_to_remove.append(data_tuple) no_tuples_picked += 1 if no_tuples_picked == no_tuples_needed_to_become_a_safe_group: # Pick enough, break to process the next unsafe group add_group(SelG, SG) remove_group(SelG, UG) # print('Number of safe groups now is: {}. Number of free tuples is: {}'.format(len(SG), len(free_tuples))) SelG = None break for t in free_tuples_to_remove: free_tuples.remove(t) # If look up in all free tuples but cannot find enough tuples to make this group safe, return them to source group if no_tuples_picked < no_tuples_needed_to_become_a_safe_group: while len(SelG.received_tuples) > 0: data_tuple = SelG.received_tuples[0] SelG.received_tuples.remove(data_tuple) source_group = find_group(data_tuple.group_index, GROUPS) convert_quasi_attributes(data_tuple, source_group) source_group.origin_tuples.append(data_tuple) R_affected = construct_r_affected_by_a_migration( source_group, SelG) free_tuples.append(data_tuple) add_group(SelG, UG) remove_group(SelG, SG) for rule in R_affected: rule.budget += 1 SelG = None print('TOTAL LOOPS: {}'.format(loop_iteration)) print('AFTER STAGE 1') print('NUMBER OF SAFE GROUPS AND UNSAFE GROUPS: {}, {}'.format( len(SG), len(UG))) print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format( sum( group_length(group) for group in GROUPS if group_length(group) >= k))) print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format( sum( group_length(group) for group in GROUPS if group_length(group) < k))) print('NUMBER OF FREE TUPLES: {}'.format(len(free_tuples))) rule_budgets = [rule.budget for rule in R_care] print('R CARE BUDGETS AFTER STAGE 1:') print(rule_budgets) # STAGE 2: PROCESS ONE BY ONE IN THE UNSAFE GROUP, START WITH GROUP WITH SHORT LENGTH # UG = sorted(UG, key=lambda gr: group_length(gr)) # UG_SMALL_DISPERSED, UG_BIG_DISPERSED = [], [] # for unsafe_group in UG: # if group_length(unsafe_group) <= k / 2: # With small group disperse them # st = time.time() # dst_group, R_affected = find_group_to_move_dispersing(unsafe_group, SG) # print('Find group to move dispersing step takes', time.time() - st, 'seconds', 'Number of safe groups:', len(SG)) # if dst_group: # for rule in R_affected: # rule.budget -= 1 # print('DISPERSE SMALL GROUP', unsafe_group.index, '- LENGTH BEFORE:', group_length(unsafe_group)) # for data_tuple in unsafe_group.origin_tuples: # convert_quasi_attributes(data_tuple, dst_group) # dst_group.received_tuples.append(data_tuple) # for data_tuple in unsafe_group.received_tuples: # convert_quasi_attributes(data_tuple, dst_group) # dst_group.received_tuples.append(data_tuple) # unsafe_group.origin_tuples = [] # unsafe_group.received_tuples = [] # print('DISPERSE SMALL GROUP', unsafe_group.index, '- LENGTH AFTER:', group_length(unsafe_group)) # # remove_group(unsafe_group, UG) # UG_SMALL_DISPERSED.append(unsafe_group) # else: # print('Cannot find any group for group {} to disperse to'.format(unsafe_group.index)) # else: # no_tuples_needed_to_become_a_safe_group = k - group_length(unsafe_group) # picked_tuples = free_tuples[:no_tuples_needed_to_become_a_safe_group] # if len(picked_tuples) < no_tuples_needed_to_become_a_safe_group: # This means the free tuples set is now empty # continue # for data_tuple in picked_tuples: # source_group = find_group(data_tuple.group_index, GROUPS) # # Migrate these free tuples to this unsafe group to make it safe # R_affected = construct_r_affected_by_a_migration(source_group, unsafe_group) # for rule in R_affected: # rule.budget -= 1 # free_tuples.remove(data_tuple) # source_group.origin_tuples.remove(data_tuple) # convert_quasi_attributes(data_tuple, unsafe_group) # print('UNSAFE BIG GROUP {} RECEIVED {} FREE TUPLES TO BECOME SAFE'.format(unsafe_group.index, no_tuples_needed_to_become_a_safe_group)) # unsafe_group.received_tuples.extend(picked_tuples) # if is_safe_group(unsafe_group, k): # Now becomes safe # add_group(unsafe_group, SG) # # remove_group(unsafe_group, UG) # UG_BIG_DISPERSED.append(unsafe_group) # for g_sm in UG_SMALL_DISPERSED: # print('Small group dispersed', g_sm.index, group_length(g_sm)) # remove_group(g_sm, UG) # for g_big in UG_BIG_DISPERSED: # print('Big group dispersed', g_big.index, group_length(g_big)) # remove_group(g_big, UG) # print('AFTER STAGE 2') # print('NUMBER OF UNSAFE GROUPS AND SAFE GROUPS: {}, {}'.format(len([ug for ug in UG if group_length(ug) > 0]), len(SG))) # print('NUMBER OF UNSAFE GROUPS WITH LENGTH <= K/2 DISPERSED (MOVED TO A SAFE GROUP): {}'.format(len(UG_SMALL_DISPERSED))) # print('NUMBER OF UNSAFE GROUPS WITH LENGTH > K/2 DISPERSED (RECEIVED FREE TUPLES): {}'.format(len(UG_BIG_DISPERSED))) # print('TOTAL NUMBER OF SAFE GROUPS: {}'.format(len([group for group in GROUPS if group_length(group) >= k]))) # print('TOTAL NUMBER OF UNSAFE GROUPS: {}'.format(len([group for group in GROUPS if 0 < group_length(group) < k]))) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) >= k))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) < k))) # print('NUMBER OF FREE TUPLES: {}'.format(len(free_tuples))) # # print('UG_SMALL_DISPERSED', [g.index for g in UG_SMALL_DISPERSED]) # # print('UG_BIG_DISPERSED', [g.index for g in UG_BIG_DISPERSED]) # rule_budgets = [rule.budget for rule in R_care] # print('R CARE BUDGETS:') # print(rule_budgets) # # STAGE 3: PROCESS ONE BY ONE IN THE REMAINING UNSAFE GROUP, START WITH GROUP WITH SHORT LENGTH # for unsafe_group in UG: # # first_tuple_of_this_unsafe_group = unsafe_group.origin_tuples[0] # dst_group, R_affected = find_group_to_move_dispersing(unsafe_group, SG) # if dst_group: # for rule in R_affected: # rule.budget -= 1 # for data_tuple in unsafe_group.origin_tuples: # convert_quasi_attributes(data_tuple, dst_group) # dst_group.received_tuples.append(data_tuple) # for data_tuple in unsafe_group.received_tuples: # convert_quasi_attributes(data_tuple, dst_group) # dst_group.received_tuples.append(data_tuple) # unsafe_group.origin_tuples = [] # unsafe_group.received_tuples = [] # else: # print('Cannot find any group for group {} to disperse to'.format(unsafe_group.index)) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) >= k))) # # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS 2: {}'.format(sum(group_length(group) for group in SG))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) < k))) # # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS 2: {}'.format(sum(group_length(group) for group in UG))) UG_SMALL, UG_BIG = [], [] for g in UG: if group_length(g) <= k / 2: UG_SMALL.append(g) else: UG_BIG.append(g) # SIMILAR TO M3AR UM = [] # Set of groups that cannot migrate member with other groups SelG = None loop_iteration = 0 UG.sort(key=lambda group: group_length(group)) while (len(UG) > 0) or (SelG): loop_iteration += 1 print( 'START LOOP ITERATION {}. UG length: {}. SG length: {}. UM length: {}' .format(loop_iteration, len(UG), len(SG), len(UM))) if SelG is None: # Randomly pick a group SelG from unsafe groups set # print('LOOP ITERATION {}. SelG is None, randomly pick one'.format(loop_iteration)) SelG = UG.pop(0) # remove_group(SelG, UG) if group_length(SelG) <= k / 2: remove_group(SelG, UG_SMALL) else: remove_group(SelG, UG_BIG) # print('LOOP ITERATION {}. SelG: {} ({})'.format(loop_iteration, SelG.index, group_length(SelG))) # Find the most appropriate group g in UG and SG to perform migration with SelG if group_length(SelG) <= k / 2: remaining_groups = UG_BIG + UG_SMALL + SG else: remaining_groups = UG_SMALL + UG_BIG + SG result_find_migration = find_group_to_migrate(R_care, SelG, remaining_groups, k) # If cannot find such a group, add it to the unmigrant UM set if result_find_migration is None: # print('LOOP ITERATION {}: NO RESULT FOR MIGRATION'.format(loop_iteration)) add_group(SelG, UM) SelG = None else: # If we can find a migration operation # print('LOOP ITERATION {}: PREPARE MIGRATION GROUP {} ({}) => GROUP {} ({}): {} TUPLES'.format(loop_iteration, result_find_migration.group_i.index, group_length(result_find_migration.group_i), result_find_migration.group_j.index, group_length(result_find_migration.group_j), result_find_migration.no_migrant_tuples)) # g is the other group in the migration operation g = result_find_migration.group_i if result_find_migration.group_i.index != SelG.index else result_find_migration.group_j # Perform a migration operation do_migration(result_find_migration.group_i, result_find_migration.group_j, result_find_migration.no_migrant_tuples) # print('LOOP ITERATION {}: AFTER MIGRATION: SelG: {} ({}). g: {} ({})'.format(loop_iteration, SelG.index, group_length(SelG), g.index, group_length(g))) for rule in result_find_migration.R_affected: rule.budget -= 1 if group_length(SelG) == 0: remove_group(SelG, UG) remove_group(SelG, UG_SMALL) remove_group(SelG, UG_BIG) if group_length(g) == 0: remove_group(g, UG) remove_group(g, UG_SMALL) remove_group(g, UG_BIG) if group_length(SelG) == 0 and group_length(g) == 0: SelG = None # print('LOOP ITERATION {}: BOTH GROUPS SelG AND g NOW HAVE 0 TUPLES'.format(loop_iteration)) # print('LOOP ITERATION {} FINISHES IN {} SECONDS'.format(loop_iteration, time.time() - st)) continue # Check if now we have a safe group in the pair (SelG, g) or not. # If there is one, collect it and add it to the safe group if is_safe_group(SelG, k): add_group(SelG, SG) remove_group(SelG, UG) remove_group(SelG, UG_SMALL) remove_group(SelG, UG_BIG) # print('LOOP ITERATION {}: MOVE GROUP {} TO SG'.format(loop_iteration, SelG.index)) if is_safe_group(g, k): add_group(g, SG) # print('LOOP ITERATION {}: MOVE GROUP {} TO SG'.format(loop_iteration, g.index)) remove_group(g, UG) remove_group(g, UG_SMALL) remove_group(g, UG_BIG) # print('LOOP ITERATION {}: CHECK SAFE - SelG: {} ({},{}) - g: {} ({},{})'.format(loop_iteration, SelG.index, is_safe_group(SelG, k), 'Empty' if group_length(SelG) == 0 else '', g.index, is_safe_group(g, k), 'Empty' if group_length(g) == 0 else '')) # Handle which group next? If there is any unsafe group in the pair, continue with it if is_unsafe_group(SelG, k): pass # Keep handling SelG elif is_unsafe_group(g, k): # Continue with g SelG = g remove_group(g, UG) else: # The next iteration we will choose another group to process SelG = None # print('LOOP ITERATION {} FINISHES IN {} SECONDS'.format(loop_iteration, time.time() - st)) print('TOTAL LOOPS: {}. UG length: {}. SG length: {}. UM length: {}\n'. format(loop_iteration, len(UG), len(SG), len(UM))) print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format( sum( group_length(group) for group in GROUPS if group_length(group) >= k))) print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS 2: {}'.format( sum(group_length(group) for group in SG))) print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format( sum( group_length(group) for group in GROUPS if group_length(group) < k and group not in UM))) print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS 2: {}'.format( sum(group_length(group) for group in UG))) print('TOTAL NUMBER OF TUPLES IN UM: {}'.format( sum(group_length(group) for group in UM))) print('=======================================') print('=======================================') print('=======================================') print('START TO DISPERSE', len(UM), 'UM GROUPS') print('NUMBER OF UM GROUPS WITH LENGTH > 0:', sum(1 for group in UM if group_length(group) > 0)) while len(UM) > 0: # Disperse g_um = UM.pop(0) if group_length(g_um) > 0: # Just consider group with length > 0 disperse(R_care, g_um, GROUPS, SG, UM) # print('AFTER DISPERSING: NUMBER OF UM GROUPS WITH LENGTH > 0:', sum(1 for group in UM if group_length(group) > 0)) # print('FINAL RESULTS: UG length: {}. SG length: {}. UM length: {}\n'.format(len(UG), len(SG), len(UM))) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) >= k))) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS 2: {}'.format(sum(group_length(group) for group in SG))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) < k))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS 2: {}'.format(sum(group_length(group) for group in UG))) # print('TOTAL NUMBER OF TUPLES IN UM: {}'.format(sum(group_length(group) for group in UM))) total_time = time.time() - start_time eval_results(R_initial, GROUPS, output_file_name, total_time, k=k)
def mod_m3ar_algo(D, R_initial, output_file_name, k=DESIRED_K): start_time = time.time() R_care = construct_r_care(R_initial) # List of cared rules print('LENGTH OF R_care', len(R_care)) print('===============================================================================') # Build groups from the dataset then split G into 2 sets of groups: safe groups SG and unsafe groups UG GROUPS, SG, UG, UG_SMALL, UG_BIG = build_groups(D, R_care=R_care, k=k) print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format(sum(group_length(group) for group in SG))) print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format(sum(group_length(group) for group in UG))) UM = [] # Set of groups that cannot migrate member with other groups print('K =', k) print('NUMBER OF UNSAFE GROUPS AND SAFE GROUPS:', len(UG), len(SG)) SelG = None loop_iteration = 0 UG.sort(key=lambda group: group_length(group)) while (len(UG) > 0) or (SelG): st = time.time() loop_iteration += 1 # print('START LOOP ITERATION {}. UG length: {}. SG length: {}. UM length: {}'.format(loop_iteration, len(UG), len(SG), len(UM))) if SelG is None: # Randomly pick a group SelG from unsafe groups set # print('LOOP ITERATION {}. SelG is None, randomly pick one'.format(loop_iteration)) SelG = UG.pop(0) # remove_group(SelG, UG) remove_unsafe_group(SelG, UG, UG_SMALL, UG_BIG) # print('LOOP ITERATION {}. SelG: {} ({})'.format(loop_iteration, SelG.index, group_length(SelG))) # Find the most appropriate group g in UG and SG to perform migration with SelG if group_length(SelG) <= k/2: remaining_groups = UG_BIG + UG_SMALL + SG else: remaining_groups = UG_SMALL + UG_BIG + SG result_find_migration = find_group_to_migrate(R_care, SelG, remaining_groups, k) # If cannot find such a group, add it to the unmigrant UM set if result_find_migration is None: # print('LOOP ITERATION {}: NO RESULT FOR MIGRATION'.format(loop_iteration)) add_group(SelG, UM) SelG = None else: # If we can find a migration operation # print('LOOP ITERATION {}: PREPARE MIGRATION GROUP {} ({}) => GROUP {} ({}): {} TUPLES'.format(loop_iteration, result_find_migration.group_i.index, group_length(result_find_migration.group_i), result_find_migration.group_j.index, group_length(result_find_migration.group_j), result_find_migration.no_migrant_tuples)) # g is the other group in the migration operation g = result_find_migration.group_i if result_find_migration.group_i.index != SelG.index else result_find_migration.group_j # Perform a migration operation do_migration(result_find_migration.group_i, result_find_migration.group_j, result_find_migration.no_migrant_tuples) # print('LOOP ITERATION {}: AFTER MIGRATION: SelG: {} ({}). g: {} ({})'.format(loop_iteration, SelG.index, group_length(SelG), g.index, group_length(g))) for rule in result_find_migration.R_affected: rule.budget -= 1 if group_length(SelG) == 0: remove_unsafe_group(SelG, UG, UG_SMALL, UG_BIG) if group_length(g) == 0: remove_unsafe_group(g, UG, UG_SMALL, UG_BIG) if group_length(SelG) == 0 and group_length(g) == 0: SelG = None # print('LOOP ITERATION {}: BOTH GROUPS SelG AND g NOW HAVE 0 TUPLES'.format(loop_iteration)) # print('LOOP ITERATION {} FINISHES IN {} SECONDS'.format(loop_iteration, time.time() - st)) continue # Check if now we have a safe group in the pair (SelG, g) or not. # If there is one, collect it and add it to the safe group if is_safe_group(SelG, k): add_group(SelG, SG) remove_unsafe_group(SelG, UG, UG_SMALL, UG_BIG) # print('LOOP ITERATION {}: MOVE GROUP {} TO SG'.format(loop_iteration, SelG.index)) if is_safe_group(g, k): add_group(g, SG) # print('LOOP ITERATION {}: MOVE GROUP {} TO SG'.format(loop_iteration, g.index)) remove_unsafe_group(g, UG, UG_SMALL, UG_BIG) # print('LOOP ITERATION {}: CHECK SAFE - SelG: {} ({},{}) - g: {} ({},{})'.format(loop_iteration, SelG.index, is_safe_group(SelG, k), 'Empty' if group_length(SelG) == 0 else '', g.index, is_safe_group(g, k), 'Empty' if group_length(g) == 0 else '')) # Handle which group next? If there is any unsafe group in the pair, continue with it if is_unsafe_group(SelG, k): pass # Keep handling SelG elif is_unsafe_group(g, k): # Continue with g SelG = g remove_group(g, UG) remove_unsafe_group(g, UG, UG_SMALL, UG_BIG) else: # The next iteration we will choose another group to process SelG = None # print('LOOP ITERATION {} FINISHES IN {} SECONDS'.format(loop_iteration, time.time() - st)) # print('TOTAL LOOPS: {}. UG length: {}. SG length: {}. UM length: {}\n'.format(loop_iteration, len(UG), len(SG), len(UM))) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) >= k))) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS 2: {}'.format(sum(group_length(group) for group in SG))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) < k and group not in UM))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS 2: {}'.format(sum(group_length(group) for group in UG))) # print('TOTAL NUMBER OF TUPLES IN UM: {}'.format(sum(group_length(group) for group in UM))) # print('=======================================') # print('=======================================') # print('=======================================') # print('START TO DISPERSE', len(UM), 'UM GROUPS') # print('NUMBER OF UM GROUPS WITH LENGTH > 0:', sum(1 for group in UM if group_length(group) > 0)) while len(UM) > 0: # Disperse g_um = UM.pop(0) if group_length(g_um) > 0: # Just consider group with length > 0 disperse(R_care, g_um, GROUPS, SG, UM) # print('AFTER DISPERSING: NUMBER OF UM GROUPS WITH LENGTH > 0:', sum(1 for group in UM if group_length(group) > 0)) # print('FINAL RESULTS: UG length: {}. SG length: {}. UM length: {}\n'.format(len(UG), len(SG), len(UM))) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) >= k))) # print('TOTAL NUMBER OF TUPLES IN SAFE GROUPS 2: {}'.format(sum(group_length(group) for group in SG))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS: {}'.format(sum(group_length(group) for group in GROUPS if group_length(group) < k))) # print('TOTAL NUMBER OF TUPLES IN UNSAFE GROUPS 2: {}'.format(sum(group_length(group) for group in UG))) # print('TOTAL NUMBER OF TUPLES IN UM: {}'.format(sum(group_length(group) for group in UM))) total_time = time.time() - start_time eval_results(R_initial, GROUPS, output_file_name, total_time, k=k)