From 31defbc6949d476cb866afac1d0d46132f89871e Mon Sep 17 00:00:00 2001 From: Henry Day-Hall Date: Tue, 7 Nov 2023 20:48:59 +0100 Subject: [PATCH 1/2] More scans and more prints --- jet_tools/CompareClusters.py | 4 ++++ jet_tools/ParallelFormJets.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/jet_tools/CompareClusters.py b/jet_tools/CompareClusters.py index 196ed08..262c6fe 100644 --- a/jet_tools/CompareClusters.py +++ b/jet_tools/CompareClusters.py @@ -1682,12 +1682,15 @@ def project_2d(show_params, best_slice, all_cols, variable_cols, score_cols, mult_col = "AveSeperateJets" # if we are taking the best slice remove all other table rows if best_slice: + print(f"Value of best_slice={best_slice}") bg_values = table[bg_col].values sg_values = table[sg_col].values combined = np.sqrt(0.53 * np.square(bg_values) + np.square(sg_values)) if isinstance(best_slice, bool): + print("Using the best slice as per the combined score") best_idx = np.nanargmin(combined.tolist()) elif isinstance(best_slice, dict): + print("Using a set of hyperparameters as a best slice") # then it is the values of the best slice mask = filter_matching(all_cols, table, approx=best_slice) possible = mask.index[mask].tolist() @@ -1698,6 +1701,7 @@ def project_2d(show_params, best_slice, all_cols, variable_cols, score_cols, distances += abs(table.loc[i, v] - best_slice[v]) best_idx = possible[np.argmin(distances)] elif isinstance(best_slice, int): + print(f"Using index {best_slice} as the best slice") best_idx = best_slice else: raise NotImplementedError diff --git a/jet_tools/ParallelFormJets.py b/jet_tools/ParallelFormJets.py index 2c1f890..1fe9610 100644 --- a/jet_tools/ParallelFormJets.py +++ b/jet_tools/ParallelFormJets.py @@ -1214,7 +1214,8 @@ def run_list(eventWise_path, end_time, class_list, param_list, name_list): 'WeightExponent': 1.} fix_CALEv2_3 = {'SeedGenerator': 'PtCenter', - 'SeedIncrement': 3} + 'SeedIncrement': 3, + 'ToAffinity': 'exp'} fix_CALEv2_4 = {'SeedGenerator': 'PtCenter', 'SeedIncrement': 3, @@ -1241,6 +1242,10 @@ def run_list(eventWise_path, end_time, class_list, param_list, name_list): 'Cutoff': [0.02, 0.04, 0.06, 0.08, 0.1], 'WeightExponent': [0., 1.]} +scan_CALEv2_7 = {'Sigma': [0.2, 0.3, 0.4, 0.5], + 'Cutoff': [0.0005, 0.001, 0.005, 0.01, 0.015], + 'WeightExponent': [0., 1.]} + def tabulate_fragments(eventWise_paths): """ Makes the assumption that jets with the same name are the same jet""" if isinstance(eventWise_paths, Components.EventWise): From ab2cdd7d8dcf26e820f3383624f90090c6957773 Mon Sep 17 00:00:00 2001 From: Henry Day-Hall Date: Wed, 8 Nov 2023 14:28:37 +0100 Subject: [PATCH 2/2] Fixed bugs in Compare clusters that impeeded selecting best slice --- jet_tools/CompareClusters.py | 255 +++++++++++++++++++---------------- 1 file changed, 141 insertions(+), 114 deletions(-) diff --git a/jet_tools/CompareClusters.py b/jet_tools/CompareClusters.py index 923b8a9..540a97c 100644 --- a/jet_tools/CompareClusters.py +++ b/jet_tools/CompareClusters.py @@ -351,11 +351,7 @@ def event_detectables(tag_leaves, jets, input_idx, source_idx, # convert to source_idxs jet_inputs = jet_inputs[jet_inputs < len(source_idx)] jet_inputs = set(source_idx[jet_inputs]) - try: - tag_in_jet = jet_inputs.intersection(tag_leaves) - except: - import ipdb; ipdb.set_trace() - tag_in_jet = jet_inputs.intersection(tag_leaves) + tag_in_jet = jet_inputs.intersection(tag_leaves) bg_in_jet = list(jet_inputs - tag_in_jet) tag_in_jet = list(tag_in_jet) tag_mass2 = np.sum(energy[tag_in_jet])**2 -\ @@ -500,11 +496,7 @@ def per_event_detectables(eventWise, jet_name, jet_idxs): continue # then skip # if we get here, there are detectable particles from the tags eventWise.selected_event = event_n - #TODO remove - try: - assert len(event_tags) == len(eventWise.DetectableTag_Rapidity) - except: - import ipdb; ipdb.set_trace() + assert len(event_tags) == len(eventWise.DetectableTag_Rapidity) energy = eventWise.Energy px = eventWise.Px py = eventWise.Py @@ -609,17 +601,8 @@ def get_detectable_comparisons(eventWise, jet_name, jet_idxs, append=False): _, _, _, percent_found, seperate_jets, \ = per_event_detectables(eventWise, jet_name, jet_idxs) content = {} - try: - rapidity_distance = ak.from_iter(rapidity_in) - \ - eventWise.DetectableTag_Rapidity - except: - print(rapidity_in[0]) - print(eventWise.DetectableTag_Rapidity[0]) - print(len(rapidity_in)) - print(len(eventWise.DetectableTag_Rapidity)) - import ipdb; ipdb.set_trace() - rapidity_distance = ak.from_iter(rapidity_in) - \ - eventWise.DetectableTag_Rapidity + rapidity_distance = ak.from_iter(rapidity_in) - \ + eventWise.DetectableTag_Rapidity pt_distance = ak.from_iter(pt_in) - eventWise.DetectableTag_PT phi_distance = ak.from_iter(phi_in) - eventWise.DetectableTag_Phi content[jet_name + "_DistanceRapidity"] = np.abs(rapidity_distance) @@ -946,7 +929,9 @@ def filter_table(*args): # print(f"Length after quality filter = {len(args[-1])}") # remove spectral mean mask = filter_matching(args[0], table, exact={'jet_class': 'SpectralMean'}) - table = table[~mask] + table['mask'] = ~mask + table = table[table['mask']] + table.drop('mask', axis=1, inplace=True) args = [*args[:-1], table] return args @@ -980,34 +965,31 @@ def nan_filter_table(all_cols, variable_cols, score_cols, table): and each column is a hyperparameter """ # make a mask marking the location of nan - nan_mask = [] - for row in table: - nan_mask.append([]) - for x in row: - try: - # by calling any on the is_nan - # we throw a ValueError if x is actually a jaggedArray - nan_mask[-1].append(np.any(np.isnan(x))) - except TypeError: - nan_mask[-1].append(False) - nan_mask = np.array(nan_mask) + n_rows = len(table.jet_name.values) + nan_mask = np.zeros((n_rows, len(all_cols)), dtype=bool) + for i, col in enumerate(all_cols): + nan_mask = np.isnan(table[col].values) if np.any(nan_mask): + keep_rows = np.ones(n_rows, dtype=bool) # drop any rows where any score_cols are nan score_nan = nan_mask[:, [all_cols.index(name) for name in score_cols]] all_nan = np.all(score_nan, axis=1) - table = table[~all_nan] - nan_mask = nan_mask[~all_nan] + keep_rows[all_nan] = False + table['keep_rows'] = keep_rows + table = table[table['keep_rows']] # then drop any cols where all values are np.nan drop_cols = np.fromiter((np.all(nan_mask[:, i]) for i, name in enumerate(all_cols)), dtype=bool) + drop_col_names = ['keep_rows'] for i in np.where(drop_cols)[0][::-1]: name = all_cols.pop(i) + drop_col_names.append(name) if name in variable_cols: del variable_cols[variable_cols.index(name)] elif name in score_cols: del score_cols[score_cols.index(name)] - table = ak.from_iter([row[~drop_cols] for row in table]) + table.drop(columns=drop_col_names) return all_cols, variable_cols, score_cols, table @@ -1041,11 +1023,9 @@ def quality_filter_table(all_cols, variable_cols, score_cols, table): """ # AveDistanceSignal is positive, smaller is better - signal_gap = table[:, all_cols.index("AveDistanceSignal")] - table = table[signal_gap < 33.] + table = table[table.AveDistanceSignal < 33.] # AveDistanceBG is positive, smaller is better - background_gap = table[:, all_cols.index("AveDistanceBG")] - table = table[background_gap < 33.] + table = table[table.AveDistanceBG < 33.] return all_cols, variable_cols, score_cols, table @@ -1137,10 +1117,10 @@ def filter_matching(all_cols, table, exact=None, approx=None): mask = pd.Series([True]*table.shape[0]) if exact is not None: for name, value in exact.items(): - mask &= (table[name] == TypeTools.restring(value)) + mask &= (table[name].values == TypeTools.restring(value)) if approx is not None: for name, value in approx.items(): - mask &= table[name].apply(TypeTools.soft_equality, args=(value,)) + mask &= table[name].values.apply(TypeTools.soft_equality, args=(value,)) return mask @@ -1178,18 +1158,10 @@ def plot_mass_gaps(eventWise_paths, jet_name=None, plt.title("Cluster methods") all_cols, variable_cols, score_cols, table = \ filter_table(*tabulate_scores(eventWise_paths)) - signal_gap = np.fromiter( - (row[all_cols.index("SeperateAveDistanceSignal")] - for row in table), dtype=float) - background_gap = np.fromiter( - (row[all_cols.index("SeperateAveDistanceBG")] - for row in table), dtype=float) - percent_found = np.fromiter( - (row[all_cols.index("AvePercentFound")] - for row in table), dtype=float) - seperate_jets = np.fromiter( - (row[all_cols.index("AveSeperateJets")] - for row in table), dtype=float) + signal_gap = table.SeperateAveDistanceSignal.values + background_gap = table.SeperateAveDistanceBG.values + percent_found = table.AvePercentFound.values + seperate_jets = table.AveSeperateJets.values if highlight_fn is not None: highlight = highlight_fn(all_cols, variable_cols, score_cols, table) @@ -1218,7 +1190,10 @@ def plot_mass_gaps(eventWise_paths, jet_name=None, mask *= (signal_gap < 5.)*(background_gap < 5.) signal_gap, background_gap, seperate_jets, percent_found, highlight = signal_gap[mask], background_gap[mask], seperate_jets[mask], percent_found[mask], highlight[mask] if labels: - jet_names = [row[all_cols.index("jet_name")]+'_'+row[all_cols.index("eventWise_name")].replace('.awkd', '').replace('iridis_', '') for row in table[mask]] + jet_names = table.jet_name.values + jet_names = [j_name+'_'+ew_name.replace('.awkd', '').replace('iridis_', '') + for j_name, ew_name in + zip(table.jet_name.values[mask], table.eventWise_name.values[mask])] PlottingTools.label_scatter(signal_gap, background_gap, jet_names, ax=ax) max_colour = max(1.5, np.max(seperate_jets)) @@ -1234,9 +1209,6 @@ def plot_mass_gaps(eventWise_paths, jet_name=None, def plot_class_bests(eventWise_paths, save_prefix=None): all_cols, variable_cols, score_cols, table = filter_table(*tabulate_scores(eventWise_paths)) variable_cols = ['jet_class'] + variable_cols - class_col = all_cols.index("jet_class") - jet_col = all_cols.index("jet_name") - eventWise_col = all_cols.index("eventWise_name") inverted_names = ["AveBGMassRatio"] + [name for name in all_cols if "Distance" in name or "Quality" in name] output1 = [["jet class"] + score_cols] @@ -1248,36 +1220,40 @@ def plot_class_bests(eventWise_paths, save_prefix=None): background_col, signal_col, percent_col, seperate_col = [all_cols.index(name) for name in ["AveDistanceBG", "AveDistanceSignal", "AvePercentFound", "AveSeperateJets"]] - meta_score = np.fromiter((-(row[background_col]+row[signal_col]**2)/(row[percent_col] + row[seperate_col]) - for row in table), dtype=float) - #meta_score = np.fromiter((-row[all_cols.index("AveDistancePT")] - # for row in table), dtype=float) + aveDistanceBG = table.AveDistanceBG.values + aveDistanceSignal = table.AveDistanceSignal.values + avePercentFound = table.AvePercentFound.values + aveSeperateJets = table.AveSeperateJets.values + meta_score = np.fromiter((-aveDistanceBG+aveDistanceSignal**2)/(avePercentFound + aveSeperateJets), + dtype=float) + cols_to_remove = ['meta_score'] + table['meta_score'] = meta_score output3 = [["file name", "jet name"] + score_cols] - for class_name in set(table[:, class_col]): - row_indices, rows = zip(*[(i, row) for i, row in enumerate(table) - if row[class_col] == class_name]) - best_meta = np.argmax(meta_score[list(row_indices)]) - output3.append([rows[best_meta][eventWise_col][7:], rows[best_meta][jet_col]]) + for class_name in set(table[class_col].values): + class_table = table[table.jet_class == class_name] + row_indices_for_class = np.where(table.jet_class.values == class_name)[0] + best_meta = np.argmax(class_table.meta_score.values) + output3.append([class_table.eventWise_name.values[best_meta][7:], class_table.jet_name.values[best_meta]]) jet_names = [class_name] file_names = ["~ in ~"] scores = ["~ score ~"] numeric_scores.append([]) row_nums.append([]) for score_name in score_cols: - score_col = all_cols.index(score_name) - output3[-1].append(f"{rows[best_meta][score_col]:.3g}") - class_scores = [row[score_col] for row in rows] + value = class_table[score_name].values[best_meta] + output3[-1].append(f"{value:.3g}") + class_scores = class_table[score_col].values if score_name in inverted_names: best = np.nanargmin(class_scores) numeric_scores[-1].append(-class_scores[best]) else: best = np.nanargmax(class_scores) numeric_scores[-1].append(class_scores[best]) - row_nums[-1].append(row_indices[best]) + row_nums[-1].append(row_indices_for_class[best]) scores.append(f"{class_scores[best]:.3g}") - best_row = rows[best] - jet_names.append(best_row[jet_col]) - file_names.append(best_row[eventWise_col][7:]) + best_row = [class_table[col].values[best] for col in all_cols] + jet_names.append(class_table.jet_name.values[best]) + file_names.append(class_table.file_name.values[best][7:]) output1 += [jet_names, file_names, scores] # now identify and label the best numeric_scores = np.array(numeric_scores) @@ -1292,10 +1268,10 @@ def plot_class_bests(eventWise_paths, save_prefix=None): # header output1[output_row][col+1] += "*best*" # add the best to the secodn table - jet_row = table[row_nums[best][col]] - jet_scores = [f"{jet_row[all_cols.index(name)]:.4g}" for name in score_cols] + best_row = row_nums[best] + jet_scores = [f"{table[name].values[best_row]:.4g}" for name in score_cols] jet_scores[col] += "*best*" - output2.append([jet_row[all_cols.index("jet_name")]] + jet_scores) + output2.append([table.jet_name.values[best_row]] + jet_scores) fig, (ax1, ax2, ax3) = plt.subplots(3, 1) PlottingTools.hide_axis(ax1) PlottingTools.hide_axis(ax2) @@ -1325,12 +1301,11 @@ def plot_grid(all_cols, plot_column_names, plot_row_names, table): # colour each cluster by jet_class and AvePercentFound class_colour_dict = {'Spectral': 'Blues', 'IterativeCone': 'Oranges', 'GeneralisedKT': 'Reds'} - colours = np.zeros((len(table), 4)) + colours = np.zeros((len(table.jet_name.values), 4)) for name in class_colour_dict: cmap = matplotlib.cm.get_cmap(class_colour_dict[name]) - rows = [c==name for c in table[:, all_cols.index('jet_class')]] - values = np.fromiter(table[rows, all_cols.index('AvePercentFound')], - dtype=float) + rows = [c==name for c in table.jet_class.values] + values = np.fromiter(table.AvePercentFound.values[rows], dtype=float) # rescale to make better use of the colour range if len(values) > 0: max_val = np.nanmax(values) @@ -1341,14 +1316,14 @@ def plot_grid(all_cols, plot_column_names, plot_row_names, table): #markers = np.random.choice(['v', 's', '*', 'D', 'P', 'X'], len(table)) plotting_params = dict(c=colours) #, marker=markers) for col_n, col_name in enumerate(plot_column_names): - x_positions = table[:, all_cols.index(col_name)] + x_positions = table[col_name].values # this function will decided what kind of scale and create it if col_name in impure_cols: x_positions, x_scale_positions, x_scale_labels = make_scale(x_positions) else: x_positions = x_positions.tolist() # awkward arrays don't play well for row_n, row_name in enumerate(plot_row_names): - y_positions = table[:, all_cols.index(row_name)] + y_positions = table[row_name].values if row_name in impure_cols: y_positions, y_scale_positions, y_scale_labels = make_scale(y_positions) else: @@ -1672,18 +1647,16 @@ def project_2d(show_params, best_slice, all_cols, variable_cols, score_cols, values of show params for each index of the slice """ - variable_idxs = [all_cols.index(name) for name in variable_cols] - num_configurations = len(table) + num_configurations = len(table.jet_name.values) if show_params is None: first = InputTools.list_complete("Chose first parameter; ", variable_cols) second = InputTools.list_complete("Chose second parameter; ", variable_cols) show_params = [first.strip(), second.strip()] assert len(show_params) == 2 - show_indices = [all_cols.index(s) for s in show_params] distinct_values = [] for s in show_params: - values = table[s] - distinct_values.append(np.array(TypeTools.generic_sort(values.unique()))) + values = table[s].values + distinct_values.append(np.array(TypeTools.generic_sort(set(values)))) # SepertateAveDistanceBG/Signal is positive, smaller is better bg_col = "SeperateAveDistanceBG" @@ -1706,8 +1679,8 @@ def project_2d(show_params, best_slice, all_cols, variable_cols, score_cols, distances = np.zeros_like(possible, dtype=float) for i in possible: for v in variable_cols: - if table.loc[i, v] != best_slice[v]: - distances += abs(table.loc[i, v] - best_slice[v]) + if table[v].values != best_slice[v]: + distances += abs(table[v].values[i] - best_slice[v]) best_idx = possible[np.argmin(distances)] elif isinstance(best_slice, int): print(f"Using index {best_slice} as the best slice") @@ -1717,30 +1690,29 @@ def project_2d(show_params, best_slice, all_cols, variable_cols, score_cols, if print_best: print(f"best score; {combined[best_idx]}") print(f"best row={best_idx};", end=' ') - print({v: table.loc[best_idx, v] for v in ["jet_name", "eventWise_name"] + variable_cols}) + print({v: table[v].values[best_idx] for v in ["jet_name", "eventWise_name"] + variable_cols}) print() print_best = False - close_filter = pd.Series([True] * len(table), index=table.index) - for i in variable_cols: - if i not in show_params: - close_filter &= TypeTools.soft_equality(table[i], table.loc[best_idx, i]) - if not close_filter.loc[best_idx]: - raise ValueError(f"Failed to find the {i} that match {table.loc[best_idx, i]}") - table = table[close_filter] + close_filter = np.array([True] * len(table.jet_name.values)) + for v in variable_cols: + if v not in show_params: + close_filter &= TypeTools.soft_equality(table[v].values, table[v].values[best_idx]) + if not close_filter[best_idx]: + raise ValueError(f"Failed to find the {v} that match {table[v].values[best_idx]}") # make the images image_sg = np.full(tuple(len(val) for val in distinct_values), np.nan) image_bg = np.full(tuple(len(val) for val in distinct_values), np.nan) image_mult = np.full(tuple(len(val) for val in distinct_values), np.nan) - x_values = table[show_params[0]].values - y_values = table[show_params[1]].values + x_values = table[show_params[0]].values[close_filter] + y_values = table[show_params[1]].values[close_filter] for x, x_val in enumerate(distinct_values[0]): matches_x = TypeTools.soft_equality(x_values, x_val) for y, y_val in enumerate(distinct_values[1]): matches_y = TypeTools.soft_equality(y_values, y_val) matches = matches_x & matches_y - image_bg[x, y] = np.nanmean(table[matches][bg_col].values) - image_sg[x, y] = np.nanmean(table[matches][sg_col].values) - image_mult[x, y] = np.nanmean(table[matches][mult_col].values) + image_bg[x, y] = np.nanmean(table[bg_col].values[close_filter][matches]) + image_sg[x, y] = np.nanmean(table[sg_col].values[close_filter][matches]) + image_mult[x, y] = np.nanmean(table[mult_col].values[close_filter][matches]) combined = np.sqrt(0.53 * image_bg**2 + image_sg**2) show_mappings = [{v: i for i, v in enumerate(dis)} for dis in distinct_values] @@ -1854,6 +1826,8 @@ def tabulate_matching_scores(eventWise, jet_name_base, jet_PS_mask=None): """ all_cols, variable_cols, score_cols, table = tabulate_scores(eventWise) + all_cols, variable_cols, score_cols, table =\ + merge_jets_by_params(all_cols, variable_cols, score_cols, table) name_col = "jet_name" if jet_PS_mask is None: def test(name): @@ -1873,6 +1847,59 @@ def test(name): return all_cols, variable_cols, score_cols, table +def merge_jets_by_params(all_cols, variable_cols, score_cols, table): + print("Starting jet merge") + new_table = [] + distinct_values = [] + other_cols = [name for name in all_cols if name not in variable_cols + score_cols] + old_table_length = len(table.jet_name.values) + recorded_row = np.zeros(old_table_length, dtype=bool) + close_filter = np.ones(old_table_length, dtype=bool) + for name in variable_cols: + values = table[name].values + distinct_values.append(np.array(TypeTools.generic_sort(set(values)))) + import itertools + for i in range(old_table_length): + if recorded_row[i]: + continue + close_filter[:] = True + variable_values = [] + for name in variable_cols: + value = table[name].values[i] + variable_values.append(value) + close_filter &= TypeTools.soft_equality(table[name].values, value) + recorded_row[close_filter] = True # we will record these + assert np.any(close_filter) + # we now have a close filter + other_values = [] + for name in other_cols: + found = sorted(set(table[name].values[close_filter])) + if len(found) == 1: + other_values.append(found[0]) + else: + composed = '_'.join([f"{f}" for f in found]) + other_values.append(composed) + score_values = [np.nanmean(table[name].values[close_filter]) + for name in score_cols] + row = [] + for name in all_cols: + if name in other_cols: + row.append(other_values[other_cols.index(name)]) + elif name in variable_cols: + row.append(variable_values[variable_cols.index(name)]) + elif name in score_cols: + row.append(score_values[score_cols.index(name)]) + new_table.append(row) + # Rearrange columns to the desired order + new_table = pd.DataFrame(new_table) + # Check if table is empty + if not table.empty: + new_table.columns = all_cols + print("Merged table created") + + return all_cols, variable_cols, score_cols, new_table + + def plot_2d(eventWise, jet_name_base, show_params=None, best_slice=True, ax_arr=None): """ Plot a 2d slice of the loss values @@ -1958,7 +1985,7 @@ def plot_scan_triangles(eventWise, jet_name_base, jet_PS_mask=None, if show_names is None: show_names = [] for name in variable_cols: - values = table[name] + values = table[name].values values = {str(v) for v in values} if len(values) > 1: show_names.append(name) @@ -2062,15 +2089,15 @@ def plot_scan_triangles(eventWise, jet_name_base, jet_PS_mask=None, print_best=print_best) comb_min = min(comb_min, np.nanmin(combined)) comb_max = max(comb_max, np.nanmax(combined)) - np.clip(image_sg, sig_bg_kwargs['vmin'], sig_bg_kwargs['vmax'], - out=image_sg) - np.clip(image_bg, sig_bg_kwargs['vmin'], sig_bg_kwargs['vmax'], - out=image_bg) - np.clip(image_mult, multiplicity_kwargs['vmin'], - multiplicity_kwargs['vmax'], - out=image_mult) - np.clip(combined, combined_kwargs['vmin'], combined_kwargs['vmax'], - out=combined) + #np.clip(image_sg, sig_bg_kwargs['vmin'], sig_bg_kwargs['vmax'], + # out=image_sg) + #np.clip(image_bg, sig_bg_kwargs['vmin'], sig_bg_kwargs['vmax'], + # out=image_bg) + #np.clip(image_mult, multiplicity_kwargs['vmin'], + # multiplicity_kwargs['vmax'], + # out=image_mult) + #np.clip(combined, combined_kwargs['vmin'], combined_kwargs['vmax'], + # out=combined) # only give ticks/labels on the diagonals v1_mapping, v2_mapping = show_mappings #v1_name, v2_name = show_here @@ -2177,7 +2204,7 @@ def plot_scan_triangle(eventWise, jet_name_base, show_names=None, if show_names is None: show_names = [] for name in variable_cols: - values = table[:, all_cols.index(name)] + values = table[name].values values = {str(v) for v in values} if len(values) > 1: show_names.append(name)