import json
from .encoder_decoder import EncDec
from .util import vector2dict
from typing import Callable
import operator
[docs]def json2expression(obj):
return Expression(obj['att'], obj['op'], obj['thr'])
[docs]def json2rule(obj):
premises = [json2expression(p) for p in obj['premise']]
cons = obj['cons']
return Rule(premises, cons)
[docs]class Expression(object):
"""
Logical expression representing a condition in a rule.
An Expression represents a single condition (premise) in a decision rule,
consisting of a variable name, a comparison operator, and a threshold value.
For example: "age > 30" or "income <= 50000".
Expressions are used to build Rule objects that explain black box predictions.
Attributes:
variable (str): Name of the feature/variable in the condition
operator (Callable): Comparison operator from the operator module
(e.g., operator.gt, operator.lt, operator.eq, operator.ge, operator.le)
value (float or int): Threshold value for the comparison
Example:
>>> import operator
>>> from lore_sa.rule import Expression
>>>
>>> # Create an expression: age > 30
>>> expr = Expression('age', operator.gt, 30)
>>> print(expr) # Output: age > 30
>>>
>>> # Create an expression: income <= 50000
>>> expr2 = Expression('income', operator.le, 50000)
>>> print(expr2) # Output: income <= 50000
"""
[docs] def __init__(self, variable: str, operator: Callable, value):
"""
Initialize a logical expression.
Args:
variable (str): Name of the variable/feature that the expression refers to
operator (Callable): Logical comparison operator. Use one from the operator
module: operator.gt (>), operator.lt (<), operator.eq (=),
operator.ge (>=), operator.le (<=), operator.ne (!=)
value (float or int): Numerical threshold value to compare against
Raises:
ValueError: If an unsupported operator is provided
"""
self.variable = variable
self.operator = operator
self.value = value
[docs] def operator2string(self):
"""
Convert the logical operator to its string representation.
Returns:
str: String representation of the operator (e.g., '>', '<', '=', '>=', '<=', '!=')
Raises:
ValueError: If the operator is not one of the recognized comparison operators
Example:
>>> import operator
>>> expr = Expression('age', operator.gt, 30)
>>> expr.operator2string() # Returns '>'
"""
operator_strings = {operator.gt: '>', operator.lt: '<', operator.ne: '!=',
operator.eq: '=', operator.ge: '>=', operator.le: '<='}
if self.operator not in operator_strings:
raise ValueError(
"logical operator not recognized. Use one of [operator.gt,operator.lt,operator.eq, operator.gte, operator.lte]")
return operator_strings[self.operator]
def __str__(self):
"""
Return a human-readable string representation of the expression.
Returns:
str: String in the format "variable operator value" (e.g., "age > 30")
"""
return "%s %s %s" % (self.variable, self.operator2string(), self.value)
def __eq__(self, other):
return (self.variable == other.variable and
self.operator == other.operator and
abs(self.value - other.value) < 1e-6)
[docs] def to_dict(self):
"""
Convert the expression to a dictionary representation.
Returns:
dict: Dictionary with keys 'att' (attribute/variable name),
'op' (operator as string), and 'thr' (threshold value)
Example:
>>> expr = Expression('age', operator.gt, 30)
>>> expr.to_dict()
{'att': 'age', 'op': '>', 'thr': 30}
"""
return {
'att': self.variable,
'op': self.operator2string(),
'thr': self.value
}
[docs]class Rule(object):
"""
Decision rule with premises (conditions) and consequences (predictions).
A Rule represents an if-then decision rule extracted from an interpretable model.
It consists of:
- Premises: A list of Expression objects that form the "if" part (conditions)
- Consequences: An Expression representing the "then" part (predicted class)
Rules are the primary output of LORE explanations, describing when and why
a black box model makes specific predictions.
Attributes:
premises (list): List of Expression objects representing the conditions
consequences (Expression): Expression representing the predicted outcome
encoder (EncDec): Encoder/decoder for handling feature transformations
Example:
>>> # Rule: IF age > 30 AND income <= 50000 THEN class = 0
>>> premises = [
... Expression('age', operator.gt, 30),
... Expression('income', operator.le, 50000)
... ]
>>> consequence = Expression('class', operator.eq, 0)
>>> rule = Rule(premises, consequence, encoder)
>>> print(rule) # Output: { age > 30, income <= 50000 } --> { class = 0 }
"""
[docs] def __init__(self, premises: list, consequences: Expression, encoder: EncDec):
"""
Initialize a decision rule.
Args:
premises (list): List of Expression objects representing the rule's conditions.
These are combined with AND logic.
consequences (Expression): Expression representing the rule's prediction/outcome
encoder (EncDec): Encoder/decoder object used to decode categorical features
back to their original representation
Note:
The encoder is used to decode one-hot encoded categorical features back to
their original categorical values, making the rule more interpretable.
"""
self.encoder = encoder
self.premises = [self.decode_rule(p) for p in premises]
self.consequences = self.decode_rule(consequences)
def _pstr(self):
return '{ %s }' % (', '.join([str(p) for p in self.premises]))
def _cstr(self):
return '{ %s }' % self.consequences
def __str__(self):
str_out = 'premises:\n' + '%s \n' % ("\n".join([str(e) for e in self.premises]))
str_out += 'consequence: %s' % (str(self.consequences))
return str_out
def __eq__(self, other):
return self.premises == other.premises and self.consequences == other.cons
def __len__(self):
return len(self.premises)
def __hash__(self):
return hash(str(self))
def to_dict(self):
premises = [{'attr': e.variable, 'val': e.value, 'op': e.operator2string()}
for e in self.premises]
return {
'premises': premises,
'consequence': {
'attr': self.consequences.variable,
'val': self.consequences.value,
'op': self.consequences.operator2string()
}
}
def decode_rule(self, rule: Expression):
if 'categorical' not in self.encoder.dataset_descriptor.keys() or self.encoder.dataset_descriptor['categorical'] == {}:
return rule
if rule.variable.split('=')[0] in self.encoder.dataset_descriptor['categorical'].keys():
decoded_label = rule.variable.split("=")[0]
decoded_value = rule.variable.split("=")[1]
rule.variable = decoded_label
if rule.value:
rule.operator = operator.eq
else:
rule.operator = operator.ne
rule.value = decoded_value
return rule
else:
return rule
def is_covered(self, x, feature_names):
xd = vector2dict(x, feature_names)
for p in self.premises:
if p.operator == operator.le and xd[p.variable] > p.value:
return False
elif p.operator == operator.gt and xd[p.variable] <= p.value:
return False
return True
class ExpressionEncoder(json.JSONEncoder):
""" Special json encoder for Condition types """
def default(self, obj):
if isinstance(obj, Expression):
json_obj = {
'att': obj.variable,
'op': obj.operator2string(),
'thr': obj.value,
}
return json_obj
return json.JSONEncoder.default(self, obj)
[docs]class RuleEncoder(json.JSONEncoder):
""" Special json encoder for Rule types """
[docs] def default(self, obj):
if isinstance(obj, Rule):
ce = ExpressionEncoder()
json_obj = {
'premise': [ce.default(p) for p in obj.premises],
'cons': obj.consequences,
}
return json_obj
return json.JSONEncoder.default(self, obj)