def _find_matching_child(node, context): for child in node["children"]: property_name = child["decision_rule"]["property"] operand = child["decision_rule"]["operand"] operator = child["decision_rule"]["operator"] context_value = context.get(property_name) # If there is no context value: if context_value is None: raise CraftAiDecisionError( """Unable to take decision, property '{}' is missing from the given context.""" .format(property_name)) if (not isinstance(operator, six.string_types) or not operator in OPERATORS.values()): raise CraftAiDecisionError( """Invalid decision tree format, {} is not a valid""" """ decision operator.""".format(operator)) # To be compared, continuous parameters should not be strings if TYPES["continuous"] in operator: context_value = float(context_value) operand = float(operand) if OPERATORS_FUNCTION[operator](context_value, operand): return child return {}
def _distribution(node): # If it is a leaf if not (node.get("children") is not None and len(node.get("children"))): prediction = node["prediction"] value_distribution = prediction["distribution"] nb_samples = prediction["nb_samples"] # It is a classification problem if isinstance(value_distribution, list): return [value_distribution, nb_samples] # It is a regression problem predicted_value = prediction.get("value") if predicted_value is not None: return [predicted_value, nb_samples] raise CraftAiDecisionError( """Unable to take decision: the decision tree has no valid""" """ predicted value for the given context.""" ) # If it is not a leaf, we recurse into the children and store # the distributions/means and sizes of each child branch. def recurse(_child): return InterpreterV2._distribution(_child) values_sizes = map(recurse, node.get("children")) values, sizes = zip(*values_sizes) if isinstance(values[0], list): return InterpreterV2.compute_mean_distributions(values, sizes) return InterpreterV2.compute_mean_values(values, sizes)
def decide(tree, args): bare_tree, configuration, tree_version = Interpreter._parse_tree(tree) if configuration != {}: time = None if len(args) == 1 else args[1] context_result = Interpreter._rebuild_context( configuration, args[0], time) context = context_result["context"] else: context = Interpreter.join_decide_args(args) # Convert timezones as integers into standard +/hh:mm format # This should only happen when no time generated value is required context = Interpreter._convert_timezones_to_standard_format( configuration, context) if semver.match(tree_version, ">=1.0.0") and semver.match( tree_version, "<2.0.0"): decision = InterpreterV1.decide(configuration, bare_tree, context) elif semver.match(tree_version, ">=2.0.0") and semver.match( tree_version, "<3.0.0"): decision = InterpreterV2.decide(configuration, bare_tree, context) else: raise CraftAiDecisionError( """Invalid decision tree format, "{}" is currently not a valid version.""" .format(tree_version)) decision["context"] = context return decision
def _check_context(configuration, context): # Extract the required properties (i.e. those that are not the output) expected_properties = [ p for p in configuration["context"] if not p in configuration["output"] ] # Retrieve the missing properties missing_properties = [ p for p in expected_properties if not p in context ] # Validate the values bad_properties = [ p for p in expected_properties if p in context and not _VALUE_VALIDATORS[configuration["context"] [p]["type"]](context[p]) ] if missing_properties or bad_properties: missing_properties_messages = [ "expected property '{}' is not defined".format(p) for p in missing_properties ] bad_properties_messages = [ "'{}' is not a valid value for property '{}' of type '{}'". format(context[p], p, configuration["context"][p]["type"]) for p in bad_properties ] raise CraftAiDecisionError( """Unable to take decision, the given context is not valid: {}.""" .format(", ".join(missing_properties_messages + bad_properties_messages)))
def _rebuild_context(configuration, state, time=None): # Model should come from _parse_tree and is assumed to be checked upon # already output = configuration["output"] context = configuration["context"] # We should not use the output key(s) to compare against configuration_ctx = { key: context[key] for (key, value) in context.items() if (key not in output) } # Check if we need the time object to_generate = [] for prop in configuration_ctx.items(): prop_name = prop[0] prop_attributes = prop[1] if prop_attributes["type"] in [ "time_of_day", "day_of_week", "day_of_month", "month_of_year", "timezone" ]: # is_generated is at True, we must generate the time for the associated context property case_1 = "is_generated" in list( prop_attributes.keys()) and prop[1]["is_generated"] # is_generated is not given, by default at True, so we must generate it as well case_2 = "is_generated" not in list(prop_attributes.keys()) if case_1 or case_2: to_generate.append(prop_name) # Raise an exception if a time object is not provided but needed if to_generate and not isinstance(time, Time): # Check for missings (not provided and need to be generated) missings = [] for prop in to_generate: if prop not in list(state.keys()): missings.append(prop_name) # Raise an error if some need to be generated but not provided and no Time object if missings: raise CraftAiDecisionError( """you must provide a Time object to decide() because""" """ context properties {} need to be generated.""".format( missings)) else: to_generate = [] # Generate context properties which need to if to_generate: for prop in to_generate: state[prop] = time.to_dict()[configuration_ctx[prop]["type"]] # Rebuild the context with generated and non-generated values context = { feature: state.get(feature) for feature, properties in configuration_ctx.items() } return context
def join_decide_args(args): joined_args = {} for arg in args: if isinstance(arg, Time): joined_args.update(arg.to_dict()) try: joined_args.update(arg) except TypeError: raise CraftAiDecisionError( """Invalid context args, the given objects aren't dicts""" """ or Time instances.""") return joined_args
def _parse_tree(tree_object): # Checking definition of tree_object if not isinstance(tree_object, dict): raise CraftAiDecisionError( "Invalid decision tree format, the given json is not an object." ) # Checking version existence tree_version = tree_object.get("_version") if not tree_version: raise CraftAiDecisionError( """Invalid decision tree format, unable to find the version""" """ informations.""") # Checking version and tree validity according to version if re.compile(r"\d+.\d+.\d+").match(tree_version) is None: raise CraftAiDecisionError( """Invalid decision tree format, "{}" is not a valid version.""" .format(tree_version)) elif semver.match(tree_version, ">=1.0.0") and semver.match( tree_version, "<3.0.0"): if tree_object.get("configuration") is None: raise CraftAiDecisionError( """Invalid decision tree format, no configuration found""") if tree_object.get("trees") is None: raise CraftAiDecisionError( """Invalid decision tree format, no tree found.""") bare_tree = tree_object.get("trees") configuration = tree_object.get("configuration") else: raise CraftAiDecisionError( """Invalid decision tree format, {} is not a supported""" """ version.""".format(tree_version)) return bare_tree, configuration, tree_version
def _find_matching_child(node, context, deactivate_missing_values=True): for child in node["children"]: property_name = child["decision_rule"]["property"] operand = child["decision_rule"]["operand"] operator = child["decision_rule"]["operator"] context_value = context.get(property_name) # If there is no context value: if context_value is None: if deactivate_missing_values: raise CraftAiDecisionError( """Unable to take decision, property '{}' is missing from the given context.""". format(property_name) ) if (not isinstance(operator, six.string_types) or not operator in OPERATORS.values()): raise CraftAiDecisionError( """Invalid decision tree format, {} is not a valid""" """ decision operator.""".format(operator) ) if OPERATORS_FUNCTION[operator](context_value, operand): return child return {}
def _check_context(configuration, context): # Extract the required properties (i.e. those that are not the output) expected_properties = [ p for p in configuration["context"] if not p in configuration["output"] ] # Retrieve the missing properties missing_properties = [ p for p in expected_properties if not p in context or context[p] is None ] # Validate the values bad_properties = [ p for p in expected_properties if not InterpreterV1.validate_property_value( configuration, context, p) ] if missing_properties or bad_properties: missing_properties = sorted(missing_properties) missing_properties_messages = [ "expected property '{}' is not defined".format(p) for p in missing_properties ] bad_properties = sorted(bad_properties) bad_properties_messages = [ "'{}' is not a valid value for property '{}' of type '{}'". format(context[p], p, configuration["context"][p]["type"]) for p in bad_properties ] errors = missing_properties_messages + bad_properties_messages # deal with missing properties if errors: message = "Unable to take decision, the given context is not valid: " + errors.pop( 0) for error in errors: message = "".join((message, ", ", error)) message = message + "." raise CraftAiDecisionError(message)