File size: 6,133 Bytes
f54e1d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
from modules.datafact_generator.util import DataFact, DataFactGenerator
import numpy as np
from sklearn.linear_model import LinearRegression
from scipy.special import expit

class TrendFact(DataFact):
    def __init__(self):
        super().__init__()
        self.type = "trend"
        self.types = [
            "increase", "decrease", "stable", "increase_then_decrease", "decrease_then_increase"
        ]

class TrendFactGenerator(DataFactGenerator):
    def __init__(self, data):
        super().__init__(data)

    def extract_trend_facts(self) -> list[TrendFact]:
        trend_facts: list[TrendFact] = []

        if not self.is_temporal:
            return []
        
        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 len(y_list) <= 1:
                continue

            trend_fact = self._extract_single_trend(group_value, indices, y_list)
            trend_facts.append(trend_fact)

        return trend_facts

    def _extract_single_trend(self, group_value: str, indices: list[int], y_list: list) -> TrendFact:
        """ 处理单个 group """

        # 分别计算单调上升,单调下降的分数,然后分别以每个点为转折点,计算先升后降,先降后升分数,取最高
        # 分数计算方式使用线性回归后的斜率

        trend_fact = TrendFact()

        def generate_score(y, slope_threshold=0.05, slope_scale=1.5):
            """
            生成某一段的 trend 分数
            划分为 decrease, stable, increase 三类
            """
            y = np.array(y)
            x = np.arange(len(y)).reshape(-1, 1)

            model = LinearRegression().fit(x, y)
            slope = model.coef_[0]
            abs_slope = abs(slope)

            y_mean = np.mean(y)

            abs_slope = abs_slope / y_mean * (len(y)-1)

            if abs_slope < slope_threshold:
                subtype = "stable"
                score = 1 - expit(slope_scale * (abs_slope / slope_threshold))  # 越靠近 0 越高
            else:
                subtype = "increase" if slope > 0 else "decrease"
                score = expit(slope_scale * (abs_slope - slope_threshold))  # 越远离阈值越高

            return score, subtype
            
        # 计算单调上升,单调下降的分数
        mono_score, mono_subtype = generate_score(y_list)

        # 分别遍历每个点作为临界点,分别计算两部分分数,汇总为先升后降和先降后升的分数
        # 我们希望如果两段比较均分,那么分数应该相对较高;如果两段很不均匀,分数应该很低
        # 熵很好
        max_poly_score = 0
        max_poly_subtype = ""
        best_split_idx = -1
        for idx in range(2, len(y_list)-2):
            first_y_list, second_y_list = [y_list[i] for i in range(0, idx)], [y_list[i] for i in range(idx, len(y_list))]

            first_score, first_subtype = generate_score(first_y_list)
            second_score, second_subtype = generate_score(second_y_list)

            # 趋势一样不考虑
            if first_subtype == second_subtype:
                continue

            # 有 stable 不考虑
            if first_subtype == "stable" or second_subtype == "stable":
                continue

            if first_score == "increase":
                poly_subtype = "increase_then_decrease"
            else:
                poly_subtype = "decrease_then_increase"

            first_ratio = len(first_y_list) / len(y_list)
            second_ratio = len(second_y_list) / len(y_list)

            # 熵
            poly_score = - first_ratio * np.log2(first_ratio) * first_score - second_ratio * np.log2(second_ratio) * second_score

            if poly_score > max_poly_score:
                max_poly_score = poly_score
                max_poly_subtype = poly_subtype
                best_split_idx = idx

        score = 0
        subtype = ""
        if mono_score >= max_poly_score:
            score = mono_score
            subtype = mono_subtype
        else:
            score = max_poly_score
            subtype = max_poly_subtype

        def generate_annotation_and_reason():
            annotation, reason = "", ""

            if subtype == "increase":
                annotation = f"The {self.y_column} of {group_value} shows an increasing trend."
                reason = "The overall data exhibits a consistent upward movement."

            elif subtype == "decrease":
                annotation = f"The {self.y_column} of {group_value} shows a decreasing trend."
                reason = "The overall data exhibits a consistent downward movement."

            elif subtype == "stable":
                annotation = f"The {self.y_column} of {group_value} remains stable over time."
                reason = "The slope of the data is close to zero, indicating minimal variation."

            elif subtype == "increase_then_decrease" and best_split_idx is not None:
                annotation = f"The {self.y_column} of {group_value} increases first and then decreases."
                reason = (
                    f"From index 0 to {best_split_idx}, the data increases; "
                    f"from index {best_split_idx} to {len(y_list)-1}, it decreases."
                )

            elif subtype == "decrease_then_increase" and best_split_idx is not None:
                annotation = f"The {self.y_column} of {group_value} decreases first and then increases."
                reason = (
                    f"From index 0 to {best_split_idx}, the data decreases; "
                    f"from index {best_split_idx} to {len(y_list)-1}, it increases."
                )
            
            return annotation, reason
        
        annotation, reason = generate_annotation_and_reason()

        data_points = [self.tabular_data[indices[-1]]] # 把最后一个元素作为 data point

        trend_fact.set_value(
            subtype, data_points, score, annotation, reason
        )

        return trend_fact