from modules.datafact_generator.util import DataFact, DataFactGenerator from modules.datafact_generator.value_fact import ValueFact from statistics import mean, stdev from scipy.special import expit class DifferenceFact(DataFact): """ 单个 difference fact """ def __init__(self): super().__init__() self.type = "difference" self.types = [ # 不同 group 之间 value 比较 "maximum_large", "maximum_small", "minimum_large", "minimum_small", "average_large", "average_small", # temporal "sudden_increase", # 相邻时间相差很多 "sudden_decrease", # categorical "sudden_change" # 按照值排序后,相邻的相差很多 ] class DifferenceFactGenerator(DataFactGenerator): def __init__(self, data: dict, value_facts: list[ValueFact]): super().__init__(data) self.value_facts = value_facts self.max_facts: list[ValueFact] = [] self.min_facts: list[ValueFact] = [] self.avg_facts: list[ValueFact] = [] for fact in self.value_facts: if fact.subtype == "max": self.max_facts.append(fact) if fact.subtype == "min": self.min_facts.append(fact) if fact.subtype == "avg": self.avg_facts.append(fact) def extract_difference_facts(self) -> list[DifferenceFact]: proportion_facts: list[DifferenceFact] = [] # 不同 group 之间 value 比较 if len(self.max_facts) > 1: max_large_fact, max_small_fact = self._extract_max_based_facts(self.max_facts) min_large_fact, min_small_fact = self._extract_min_based_facts(self.min_facts) avg_large_fact, avg_small_fact = self._extract_avg_based_facts(self.avg_facts) proportion_facts.extend([max_large_fact, max_small_fact, min_large_fact, min_small_fact, avg_large_fact, avg_small_fact]) # sudden for group_value in self.grouped_data.keys(): indices = self.grouped_data[group_value]["indices"] y_list = self.grouped_data[group_value]["y_list"] if self.is_temporal: increase_difference_fact, decrease_difference_fact = self._extract_temporal_sudden(group_value, indices, y_list) proportion_facts.append(increase_difference_fact) proportion_facts.append(decrease_difference_fact) else: difference_fact = self._extract_categorical_sudden(group_value, indices, y_list) proportion_facts.append(difference_fact) return proportion_facts def _extract_facts_base(self, fact_type: str, facts: list[ValueFact]): """ 模版函数 """ max_differencen_fact, min_difference_fact = DifferenceFact(), DifferenceFact() max_subtype, min_subtype = f"{fact_type}_large", f"{fact_type}_small" # value fact 的处理中, max, min, avg 统一地会把那个值放在 data_points[*][y_column] 中 max_val = max(facts, key=lambda x: x.data_points[0][self.y_column]).data_points[0][self.y_column] min_val = min(facts, key=lambda x: x.data_points[0][self.y_column]).data_points[0][self.y_column] all_val = [fact.data_points[0][self.y_column] for fact in facts] max_facts: list[ValueFact] = [] min_facts: list[ValueFact] = [] max_data_points, min_data_points = [], [] for fact in facts: if fact.data_points[0][self.y_column] == max_val: max_facts.append(fact) max_data_points.extend(fact.data_points) if fact.data_points[0][self.y_column] == min_val: min_facts.append(fact) min_data_points.extend(fact.data_points) def generate_score(): """ 统一使用单边 z 检验 """ mu = mean(all_val) sigma = stdev(all_val) if sigma == 0: max_score = 1.0 if max_val > mu else 0.0 min_score = 1.0 if min_val < mu else 0.0 return max_score, min_score k = 2.0 z0 = 0.8 z = (max_val - mu) / sigma max_score = expit(k * (z - z0)) z = (mu - min_val) / sigma min_score = expit(k * (z - z0)) return max_score, min_score def generate_annotation_and_reason(): max_annotation, max_reason = "", "" min_annotation, min_reason = "", "" max_group_value_str = ", ".join([max_data_point[self.group_column] for max_data_point in max_data_points]) min_group_value_str = ", ".join([min_data_point[self.group_column] for min_data_point in min_data_points]) if len(max_data_points) == 1: max_annotation = f"The {fact_type} value of {max_group_value_str} is the largest in all groups." max_reason = ( f"The {fact_type} value of {self.y_column} of {max_group_value_str} has a value of {max_val}, " f"which is larger than all other {self.group_column}." ) else: max_annotation = f"The {fact_type} value of {max_group_value_str} are all the largest in all groups." max_reason = ( f"The {fact_type} value of {self.y_column} of {max_group_value_str} all have a value of {max_val}, " f"which is larger than all other {self.group_column}." ) if len(min_data_points) == 1: min_annotation = f"The {fact_type} value of {min_group_value_str} is the smallest in all groups." min_reason = ( f"The {fact_type} value of {self.y_column} of {min_group_value_str} has a value of {min_val}, " f"which is smaller than all other {self.group_column}." ) else: min_annotation = f"The {fact_type} value of {min_group_value_str} are all the smallest in all groups." min_reason = ( f"The {fact_type} value of {self.y_column} of {min_group_value_str} all have a value of {max_val}, " f"which is smaller than all other {self.group_column}." ) return max_annotation, max_reason, min_annotation, min_reason max_score, min_score = generate_score() max_annotation, max_reason, min_annotation, min_reason = generate_annotation_and_reason() max_differencen_fact.set_value( max_subtype, max_data_points, max_score, max_annotation, max_reason ) min_difference_fact.set_value( min_subtype, min_data_points, min_score, min_annotation, min_reason ) return max_differencen_fact, min_difference_fact def _extract_max_based_facts(self, max_facts: list[ValueFact]): return self._extract_facts_base("maximum", max_facts) def _extract_min_based_facts(self, min_facts: list[ValueFact]): return self._extract_facts_base("minimum", min_facts) def _extract_avg_based_facts(self, avg_facts: list[ValueFact]): return self._extract_facts_base("average", avg_facts) def _extract_temporal_sudden(self, group_value: str, indices: list[int], y_list: list): """ 选择一个 group 中最显著的上升 / 下降 """ # 找到相邻值中绝对值相差最大的 max_diff_idx_increase = [] max_diff_increase = 0 max_diff_idx_decrease = [] max_diff_decrease = 0 for idx in range(len(y_list)-1): diff = abs(y_list[idx] - y_list[idx+1]) if y_list[idx] < y_list[idx+1]: # sudden increase if diff > max_diff_increase: max_diff_increase = diff max_diff_idx_increase = [idx] elif diff == max_diff_increase: max_diff_idx_increase.append(idx) else: # sudden decrease if diff < max_diff_decrease: max_diff_decrease = diff max_diff_idx_decrease = [idx] elif diff == max_diff_decrease: max_diff_idx_decrease.append(idx) increase_difference_fact, decrease_difference_fact = DifferenceFact(), DifferenceFact() increase_subtype, decrease_subtype = "sudden_increase", "sudden_decrease" before_increase_data_points = [self.tabular_data[indices[i]] for i in max_diff_idx_increase] before_decrease_data_points = [self.tabular_data[indices[i]] for i in max_diff_idx_decrease] after_increase_data_points = [self.tabular_data[indices[i+1]] for i in max_diff_idx_increase] after_decrease_data_points = [self.tabular_data[indices[i+1]] for i in max_diff_idx_decrease] def generate_score(): max_val = max(y_list) min_val = min(y_list) k = 4.0 z0 = 0.1 increase_ratio = max_diff_increase / (max_val - min_val) decrease_ratio = max_diff_decrease / (max_val - min_val) increase_score = expit(k * (increase_ratio - z0)) if max_diff_increase else 0.0 decrease_score = expit(k * (decrease_ratio - z0)) if max_diff_decrease else 0.0 return increase_score, decrease_score def generate_annotation_and_reason(): increase_annotation, increase_reason = "", "" decrease_annotation, decrease_reason = "", "" increase_positions = [data_point.get(self.x_column) for data_point in after_increase_data_points] increase_positions_str = ", ".join(increase_positions) after_increase_values = [str(data_point.get(self.y_column)) for data_point in after_increase_data_points] after_increase_values_str = ", ".join(after_increase_values) before_increase_values = [str(data_point.get(self.y_column)) for data_point in before_increase_data_points] before_increase_values_str = ", ".join(before_increase_values) decrease_positions = [data_point.get(self.x_column) for data_point in after_decrease_data_points] decrease_positions_str = ", ".join(decrease_positions) after_decrease_values = [str(data_point.get(self.y_column)) for data_point in after_decrease_data_points] after_decrease_values_str = ", ".join(after_decrease_values) before_decrease_values = [str(data_point.get(self.y_column)) for data_point in before_decrease_data_points] before_decrease_values_str = ", ".join(before_decrease_values) if len(max_diff_idx_increase) == 1: increase_annotation = f"The {group_value} shows a sudden increase at {increase_positions_str}." increase_reason = ( f"The {self.y_column} for {group_value} at {increase_positions_str} is {after_increase_values_str}, " f"which is significantly higher than the previous value of {before_increase_values_str}." ) else: increase_annotation = f"The {group_value} exhibits sudden increases at multiple points: {increase_positions_str}." increase_reason = ( f"At these positions, the {self.y_column} values for {group_value} are {after_increase_values_str}, " f"which are significantly higher than the preceding values of {before_increase_values_str}." ) if len(max_diff_idx_decrease) == 1: decrease_annotation = f"The {group_value} shows a sudden decrease at {decrease_positions_str}." decrease_reason = ( f"The {self.y_column} for {group_value} at {decrease_positions_str} is {after_decrease_values_str}, " f"which is significantly lower than the previous value of {before_decrease_values_str}." ) else: decrease_annotation = f"The {group_value} exhibits sudden decreases at multiple points: {decrease_positions_str}." decrease_reason = ( f"At these positions, the {self.y_column} values for {group_value} are {after_decrease_values_str}, " f"which are significantly lower than the preceding values of {before_decrease_values_str}." ) return increase_annotation, increase_reason, decrease_annotation, decrease_reason increase_score, decrease_score = generate_score() increase_annotation, increase_reason, decrease_annotation, decrease_reason = generate_annotation_and_reason() increase_difference_fact.set_value( increase_subtype, after_increase_data_points, increase_score, increase_annotation, increase_reason ) decrease_difference_fact.set_value( decrease_subtype, after_decrease_data_points, decrease_score, decrease_annotation, decrease_reason ) return increase_difference_fact, decrease_difference_fact def _extract_categorical_sudden(self, group_value: str, indices: list[int], y_list: list): difference_fact = DifferenceFact() subtype = "sudden_change" sorted_pairs = sorted(zip(y_list, indices), key=lambda x: x[0]) sorted_y_list, sorted_indices = zip(*sorted_pairs) y_list = list(sorted_y_list) indices = list(sorted_indices) max_diff_idx = [] max_diff = 0 for idx in range(len(y_list)-1): diff = y_list[idx+1] - y_list[idx] if diff > max_diff: max_diff = diff max_diff_idx = [idx] elif diff == max_diff: max_diff_idx.append(idx) before_change_data_points = [self.tabular_data[indices[i]] for i in max_diff_idx] after_change_data_points = [self.tabular_data[indices[i+1]] for i in max_diff_idx] def generate_score(): max_val = max(y_list) min_val = min(y_list) k = 4.0 z0 = 0.1 ratio = max_diff / (max_val - min_val) score = expit(k * (ratio - z0)) if max_diff else 0 return score def generate_annotation_and_reason(): annotation, reason = "", "" before_change_positions = [data_point.get(self.x_column) for data_point in before_change_data_points] after_change_positions = [data_point.get(self.x_column) for data_point in after_change_data_points] before_change_positions_str = ", ".join(before_change_positions) after_change_positions_str = ", ".join(after_change_positions) after_change_values = [str(data_point.get(self.y_column)) for data_point in after_change_data_points] after_change_values_str = ", ".join(after_change_values) before_change_values = [str(data_point.get(self.y_column)) for data_point in before_change_data_points] before_change_values_str = ", ".join(before_change_values) if len(max_diff_idx) == 1: annotation = ( f"The {group_value} shows a sudden jump in {self.y_column} from {before_change_positions_str} " f"to {after_change_positions_str}." ) reason = ( f"The {self.y_column} for {group_value} differs significantly between {before_change_values_str} at " f"{before_change_positions_str} and {after_change_values_str} at {after_change_positions_str}." ) else: annotation = ( f"The {group_value} exhibits multiple sudden jumps in {self.y_column}, transitioning from " f"{before_change_positions_str} to {after_change_positions_str}." ) reason = ( f"Across these points, the {self.y_column} for {group_value} differs significantly between " f"{before_change_values_str} at {before_change_positions_str} and " f"{after_change_values_str} at {after_change_positions_str}." ) return annotation, reason score = generate_score() annotation, reason = generate_annotation_and_reason() difference_fact.set_value( subtype, after_change_data_points, score, annotation, reason ) return difference_fact