diagnosis_rule_op.py 11 KB
Newer Older
1
2
3
4
5
6
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""A module for data diagnosis rule ops."""

from typing import Dict, Callable
7
import re
8
9
10
11
12
13
14
15
16
17
18
19

import pandas as pd

from superbench.benchmarks.context import Enum
from superbench.common.utils import logger


class DiagnosisRuleType(Enum):
    """The Enum class representing different rule ops."""

    VARIANCE = 'variance'
    VALUE = 'value'
20
    MULTI_RULES = 'multi_rules'
21
    FAILURE_CHECK = 'failure_check'
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


class RuleOp:
    """RuleOp class to maintain all rule functions."""

    functions: Dict[DiagnosisRuleType, Callable] = dict()

    @classmethod
    def add_rule_func(cls, rule_type):
        """Add rule fuction.

        Args:
            rule_type (DiagnosisRuleType): The type of rule function.

        Return:
            decorator (Callable): return the decorator to add the rule function.
        """
        def decorator(func):
            cls.functions[rule_type] = func
            return func

        return decorator

    @classmethod
    def get_rule_func(cls, rule_type):
        """Get rule fuction by rule_type.

        Args:
            rule_type (DiagnosisRuleType): The type of rule function.

        Return:
            func (Callable): rule function, None means invalid rule type.
        """
        if rule_type in cls.functions:
            return cls.functions[rule_type]

        return None

60
61
62
63
64
65
66
67
68
    @staticmethod
    def check_criterion_with_a_value(rule):
        """Check if the criterion is valid with a numeric variable and return bool type.

        Args:
            rule (dict): rule including function, criteria, metrics with their baseline values and categories
        """
        # parse criteria and check if valid
        if not isinstance(eval(rule['criteria'])(0), bool):
69
            logger.log_and_raise(exception=ValueError, msg='invalid criteria format')
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

    @staticmethod
    def miss_test(metric, rule, data_row, details, categories):
        """Check if the metric in the rule missed test and if so add details and categories.

        Args:
            metric (str): the name of the metric
            data_row (pd.Series): raw data of the metrics
            rule (dict): rule including function, criteria, metrics with their baseline values and categories
            details (list): details about violated rules and related data
            categories (set): categories of violated rules

        Returns:
            bool: if the metric in the rule missed test, return True, otherwise return False
        """
        # metric not in raw_data or the value is none, miss test
        if metric not in data_row or pd.isna(data_row[metric]):
87
            RuleOp.add_categories_and_details(metric + '_miss', 'FailedTest', details, categories)
88
89
90
91
92
93
94
95
96
97
98
99
100
101
            return True
        return False

    @staticmethod
    def add_categories_and_details(detail, category, details, categories):
        """Add details and categories.

        Args:
            detail (str): violated rule and related data
            category (str): category of violated rule
            details (list): list of details about violated rules and related data
            categories (set): set of categories of violated rules
        """
        details.append(detail)
102
103
        if category:
            categories.add(category)
104

105
106
107
108
109
110
    @staticmethod
    def variance(data_row, rule, summary_data_row, details, categories):
        """Rule op function of variance.

        Each metric in the rule will calculate the variance (val - baseline / baseline),
        and use criteria in the rule to determine whether metric's variance meet the criteria,
111
        if any metric meet the criteria, the rule is not passed.
112
113
114
115
116

        Args:
            data_row (pd.Series): raw data of the metrics
            rule (dict): rule including function, criteria, metrics with their baseline values and categories
            summary_data_row (pd.Series): results of the metrics processed after the function
117
            details (list): details about violated rules and related data
118
119
120
            categories (set): categories of violated rules

        Returns:
121
            number: the number of the metrics that violate the rule if the rule is not passed, otherwise 0
122
        """
123
124
        violated_metric_num = 0
        RuleOp.check_criterion_with_a_value(rule)
125
126
127
        # every metric should pass the rule
        for metric in rule['metrics']:
            # metric not in raw_data or the value is none, miss test
128
            if not RuleOp.miss_test(metric, rule, data_row, details, categories):
129
                violate_metric = False
130
131
132
                # check if metric pass the rule
                val = data_row[metric]
                baseline = rule['metrics'][metric]
133
134
135
136
                if baseline is None or baseline == 0:
                    logger.log_and_raise(
                        exception=ValueError, msg='invalid baseline 0 or baseline not found in variance rule'
                    )
137
138
139
140
141
                var = (val - baseline) / baseline
                summary_data_row[metric] = var
                violate_metric = eval(rule['criteria'])(var)
                # add issued details and categories
                if violate_metric:
142
                    violated_metric_num += 1
143
144
145
                    info = '(B/L: {:.4f} VAL: {:.4f} VAR: {:.2f}% Rule:{})'.format(
                        baseline, val, var * 100, rule['criteria']
                    )
146
147
148
149
                    if 'store' not in rule or not rule['store']:
                        RuleOp.add_categories_and_details(metric + info, rule['categories'], details, categories)
                    else:
                        RuleOp.add_categories_and_details(metric + info, None, details, categories)
150
        return violated_metric_num
151
152
153
154
155
156
157

    @staticmethod
    def value(data_row, rule, summary_data_row, details, categories):
        """Rule op function of value.

        Each metric in the rule will use criteria in the rule
        to determine whether metric's value meet the criteria,
158
        if any metric meet the criteria, the rule is not passed.
159
160
161
162
163

        Args:
            data_row (pd.Series): raw data of the metrics
            rule (dict): rule including function, criteria, metrics with their baseline values and categories
            summary_data_row (pd.Series): results of the metrics processed after the function
164
            details (list): details about violated rules and related data
165
166
167
            categories (set): categories of violated rules

        Returns:
168
            number: the number of the metrics that violate the rule if the rule is not passed, otherwise 0
169
        """
170
        violated_metric_num = 0
171
        # parse criteria and check if valid
172
        RuleOp.check_criterion_with_a_value(rule)
173
174
175
        # every metric should pass the rule
        for metric in rule['metrics']:
            # metric not in raw_data or the value is none, miss test
176
            if not RuleOp.miss_test(metric, rule, data_row, details, categories):
177
                violate_metric = False
178
179
180
181
182
183
                # check if metric pass the rule
                val = data_row[metric]
                summary_data_row[metric] = val
                violate_metric = eval(rule['criteria'])(val)
                # add issued details and categories
                if violate_metric:
184
                    violated_metric_num += 1
185
                    info = '(VAL: {:.4f} Rule:{})'.format(val, rule['criteria'])
186
187
188
189
                    if 'store' not in rule or not rule['store']:
                        RuleOp.add_categories_and_details(metric + info, rule['categories'], details, categories)
                    else:
                        RuleOp.add_categories_and_details(metric + info, None, details, categories)
190
191
192
        return violated_metric_num

    @staticmethod
193
    def multi_rules(rule, details, categories, store_values):
194
195
196
197
198
199
200
201
202
        """Rule op function of multi_rules.

        The criteria in this rule will use the combined results of multiple previous rules and their metrics
        which has been stored in advance to determine whether this rule is passed.

        Args:
            rule (dict): rule including function, criteria, metrics with their baseline values and categories
            details (list): details about violated rules and related data
            categories (set): categories of violated rules
203
204
            store_values (dict): including the number of the metrics that violate the rule, and the values of
            the metrics for the rules with 'store' True
205
206
207
        Returns:
            number: 0 if the rule is passed, otherwise 1
        """
208
209
210
211
212
213
214
215
216
217
218
219
220
221
        try:
            violated = eval(rule['criteria'])(store_values)
            if not isinstance(violated, bool):
                logger.log_and_raise(exception=ValueError, msg='invalid criteria format')
            if violated:
                info = '{}:{}'.format(rule['name'], rule['criteria'])
                RuleOp.add_categories_and_details(info, rule['categories'], details, categories)
            return 1 if violated else 0
        # the key defined in criteria is not found
        except KeyError as e:
            logger.log_and_raise(exception=KeyError, msg='invalid criteria format - {}'.format(str(e)))
        # miss/failed test
        except Exception:
            return 0
222

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
    @staticmethod
    def failure_check(data_row, rule, summary_data_row, details, categories, raw_rule):
        """Rule op function of failure_check.

        Args:
            data_row (pd.Series): raw data of the metrics
            rule (dict): rule including function, criteria, metrics with their baseline values and categories
            summary_data_row (pd.Series): results of the metrics processed after the function
            details (list): details about violated rules and related data
            categories (set): categories of violated rules
            raw_rule (dict): raw rule read from rule file

        Returns:
            number: the number of the metrics that violate the rule if the rule is not passed, otherwise 0
        """
        violated_metric_num = 0
        for metric_regex in raw_rule['metrics']:
            match = False
            for metric in rule['metrics']:
                if re.search(metric_regex, metric):
                    match = True
                    # metric not in raw_data or the value is none, miss test
                    if metric not in data_row or pd.isna(data_row[metric]):
                        violated_metric_num += 1
                    break
            # metric_regex written in rules is not matched by any metric, miss test
            if not match:
                violated_metric_num += 1
                RuleOp.add_categories_and_details(metric_regex + '_miss', rule['categories'], details, categories)
        # return code != 0, failed test
        violated_metric_num += RuleOp.value(data_row, rule, summary_data_row, details, categories)
        return violated_metric_num

256
257
258

RuleOp.add_rule_func(DiagnosisRuleType.VARIANCE)(RuleOp.variance)
RuleOp.add_rule_func(DiagnosisRuleType.VALUE)(RuleOp.value)
259
RuleOp.add_rule_func(DiagnosisRuleType.MULTI_RULES)(RuleOp.multi_rules)
260
RuleOp.add_rule_func(DiagnosisRuleType.FAILURE_CHECK)(RuleOp.failure_check)