A lightweight rule engine that allows declarative specification of business rules.
Project description
simple-rule-engine
A lightweight yet powerful rule engine that allows declarative specification of business rules and saves tons of repeated development work.
Key Features
- Ability to declaratively author both Scoring and Decision Rules.
- Ability to version control rule declarations thus enabling auditing of rule changes over a period of time.
- Ability to author chained rules. Evaluation of one rule can refer to the result of another rule, thus enabling modular, hierarchical rules.
Table of Contents
Why Rule Engine?
Decision making has always been at the heart of any business. In certain industries (such as Lending), some of the decisions made are so dynamic & at a flux that programming these decisions by hand is counter-productive.
Take the example of the decision of giving someone a loan. It primarily involves ascertaining two fundamental factors:
- Ability to repay the loan.
- Intent to repay the loan.
When you start assessing a borrower based on above, you typically get all facts required to make a decision (such as Bureau score, bank statements etc.) and you will pass these facts through a decision matrix to arrive at
- A composite score on a scale that gives an indication of whether the borrower will repay the loan (intent)
- A recommendation of how much loan should be given to the borrower. (ability)
The aforementioned decisions involve evaluation of multiple parameters. You simply cannot write a program to solve such complex scoring or decision problems:
- The evaluations and/or scores will always change over a period of time to adjust to business needs
- The rules will also change based on the nature of the business product.
The simple-rule-engine solves such dynamic decision making problems by abstracting the scoring or decision making into a framework and providing a standard rule template to author the rules.
The rules can be authored through a separate UI and stored as rule template in a database. The rule engine then can be treated as a service and just by passing all the facts (a.k.a inputs), we get the the corresponding decisions or scores (output).
The caller of the rule engine is agnostic of the business logic behiond rule evaluation.
Benefits
- Declarative authoring of rules. This can be done by a business analyst, rather than a developer. The developer just focuses on extracting the facts that are required to be passed into the engine.
- Versioning of rules to enable the auditing of the rule changes over a period of time.
Concepts
The simple-rule-engine is composed of two parts:
- A Rule template which enables one to declaratively specify a rule, which can either be a Decision (or) a Score. The rule template is uniquely identified by a name.
- A parser engine which when passed with a rule name & facts, parses the rule template against the facts given and provides an output. The output can either be a score (numeric) or a decision (anything).
The simple-rule-engine allows the rules to be “chained”. I.e. you can build a small portion of the score as an independent rule and “use” this rule in another rule.
Rule Declaration Language
- At the heart of simple-rule-engine is the rule declaration language.
- The rule declaration language supports two types of rules: Decision rule or a Score rule.
Score rule:
- A Score rule is composed of one or many rule sets.
- Each rule-set computes a sub-score and is applied a weight.
- The total score then would be the sum of all the individual scores of all the rule sets belonging to a rule.
- A rule set is composed of one or many rule rows.
- You can ‘roughly’ think of each Rule Row as a Conditional evaluation of the facts (a.k.a antecedent) & a score based on these conditions (a.k.a consequent).
Decision rule:
- A Decision rule is always composed of only one rule set.
- A rule set is composed of one or many rule rows.
- You can ‘roughly’ think of each Rule Row as a Conditional evaluation of the facts (a.k.a antecedent) & a decision based on these conditions (a.k.a consequent).
- A decision rule always arrives at a single decision at the end of parsing.
- The decision can be anything (a numeric, a string such as YES/NO or even a JSON)
- Once a Rule Row evaluates to True, the corresponding decision is returned immediately.
Antecedent and Consequent
- An antecedent at the core is an evaluator. It evaluates one or many facts through an operator.
- For evaluating numeric facts, a numeric operator is used. It can be one of (<=, <, >, >=, ==, <>, between, is_none)
- For evaluating string facts, a string operator is used. It can be one of (in_list, contains, is_none, equals)
- You can mix evaluation of more than one fact & combine the result with an “and” or “or” condition.
- You can perform complex evaluations involving multiple facts combining AND and OR conditions recursively in the antecedent. See Examples.
- The system allows a total recursion depth of 5 to allow complex evaluations.
- A rule can be an antecedent. This allows one rule to use another rule as part of evaulation.
Examples
A simple decision tree involving facts
Decision matrix
Bureau Score | Marital Status | Business Ownership | Decision |
---|---|---|---|
between 650 and 800 | in [Married, Unspecified] | in [Owned by Self, Owned by Family] | GO |
Rule specification
from simpleruleengine.conditional.when_all import WhenAll
from simpleruleengine.expression.expression import Expression
from simpleruleengine.operator.between import Between
from simpleruleengine.operator.string_in import In
from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
from simpleruleengine.token.numeric_token import NumericToken
from simpleruleengine.token.string_token import StringToken
if __name__ == "__main__":
cibil_score_between_650_800 = Expression(
NumericToken("cibil_score"),
Between(floor=650, ceiling=800)
)
marital_status_in_married_unspecified = Expression(
StringToken("marital_status"),
In("Married", "Unspecified")
)
business_owned_by_self_family = Expression(
StringToken("business_ownership"),
In("Owned by Self", "Owned by Family")
)
rule_row_decision_go = RuleRowDecision(
WhenAll(
cibil_score_between_650_800,
marital_status_in_married_unspecified,
business_owned_by_self_family
),
"GO"
)
rule_set_decision = RuleSetDecision(rule_row_decision_go)
fact = dict(
cibil_score=700,
marital_status="Married",
business_ownership="Owned by Self"
)
assert rule_set_decision.evaluate(fact) == "GO"
A complex decision tree involving multiple AND and OR conditions
Decision matrix
Applicant Age | Applicant Ownership | Business Ownership | Decision |
---|---|---|---|
>=35 | in [Owned by Self, Owned by Family] | in [Owned by Self, Owned by Family] | GO |
>=35 | in [Owned by Self, Owned by Family] | in [Rented] | GO |
>=35 | in [Rented] | in [Owned by Self, Owned by Family] | GO |
>=35 | in [Rented] | in [Rented] | NO GO |
<35 | in [Rented] | in [Rented] | NO GO |
<35 | in [Owned by Self, Owned by Family] | in [Rented] | NO GO |
<35 | in [Rented] | in [Owned by Self, Owned by Family] | NO GO |
<35 | in [Owned by Self, Owned by Family] | in [Owned by Self, Owned by Family] | GO |
- when the applicant age is >=35, either of applicant ownership or business ownership must be Owned.
- When the applicant age is <35, both the applicant ownership and business ownership must be Owned.
Rule specification
from simpleruleengine.conditional.when_all import WhenAll
from simpleruleengine.conditional.when_any import WhenAny
from simpleruleengine.expression.expression import Expression
from simpleruleengine.operator.greater_than_equal import Gte
from simpleruleengine.operator.string_in import In
from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
from simpleruleengine.token.numeric_token import NumericToken
from simpleruleengine.token.string_token import StringToken
if __name__ == "__main__":
applicant_age_gte_35 = Expression(
NumericToken("applicant_age"),
Gte(35)
)
business_owned_by_self_family = Expression(
StringToken("business_ownership"),
In("Owned by Self", "Owned by Family")
)
applicant_owned_by_self_family = Expression(
StringToken("applicant_ownership"),
In("Owned by Self", "Owned by Family")
)
rule_row_decision_go = RuleRowDecision(
WhenAll(
applicant_age_gte_35,
WhenAny(
business_owned_by_self_family,
applicant_owned_by_self_family
)
),
"GO"
)
rule_set_decision = RuleSetDecision(rule_row_decision_go)
fact_go = dict(
applicant_age=42,
applicant_ownership="Not Owned",
business_ownership="Owned by Self"
)
assert rule_set_decision.evaluate(fact_go) == "GO"
fact_no_go_1 = dict(
applicant_age=42,
applicant_ownership="Not Owned",
business_ownership="Not Owned"
)
assert rule_set_decision.evaluate(fact_no_go_1) != "GO"
fact_no_go_2 = dict(
applicant_age=25,
applicant_ownership="Owned by Self",
business_ownership="Owned by Self"
)
assert rule_set_decision.evaluate(fact_no_go_2) != "GO"
A scoring rule involving multiple parameters
Rule set Name | Weightage |
---|---|
no_of_running_bl_pl | 0.5 |
last_loan_drawn_in_months | 0.5 |
no_of_running_bl_pl
Condition | Score |
---|---|
no_of_running_bl_pl >= 7 | -100 |
no_of_running_bl_pl >= 4 | -40 |
no_of_running_bl_pl >= 2 | 30 |
no_of_running_bl_pl >= 0 | 100 |
no_of_running_bl_pl is none | 100 |
last_loan_drawn_in_months
Condition | Score |
---|---|
last_loan_drawn_in_months == 0 | 30 |
last_loan_drawn_in_months <3 | -30 |
last_loan_drawn_in_months <= 12 | 40 |
last_loan_drawn_in_months >12 | 100 |
last_loan_drawn_in_months is none | 100 |
Rule Specification
from simpleruleengine.conditional.when_all import WhenAll
from simpleruleengine.operator.greater_than_equal import Gte
from simpleruleengine.operator.greater_than import Gt
from simpleruleengine.operator.equal import Eq
from simpleruleengine.operator.less_than import Lt
from simpleruleengine.operator.less_than_equal import Lte
from simpleruleengine.rulerow.rule_row_score import RuleRowScore
from simpleruleengine.ruleset.rule_set_score import RuleSetScore
from simpleruleengine.rule.rule_score import RuleScore
from simpleruleengine.token.numeric_token import NumericToken
from simpleruleengine.expression.expression import Expression
if __name__ == "__main__":
no_run_bl_pl_gte_7_score_minus_100 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(7))),
-100
)
no_run_bl_pl_gte_4_score_minus_40 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(4))),
-40
)
no_run_bl_pl_gte_2_score_30 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(2))),
30
)
no_run_bl_pl_gte_0_score_100 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(0))),
100
)
no_of_run_bl_pl_rule_set = RuleSetScore(
no_run_bl_pl_gte_7_score_minus_100,
no_run_bl_pl_gte_4_score_minus_40,
no_run_bl_pl_gte_2_score_30,
no_run_bl_pl_gte_0_score_100,
weight=0.5
)
fact_no_run_bl_pl_2 = dict(no_of_running_bl_pl=2)
assert no_of_run_bl_pl_rule_set.evaluate(fact_no_run_bl_pl_2) == 15.0
last_loan_drawn_in_months_eq_0_score_30 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Eq(0))),
30
)
last_loan_drawn_in_months_lt_3_score_minus_30 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Lt(3))),
-30
)
last_loan_drawn_in_months_lte_12_score_40 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Lte(12))),
40
)
last_loan_drawn_in_months_gt_12_score_100 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Gt(12))),
100
)
last_loan_drawn_in_months_rule_set = RuleSetScore(
last_loan_drawn_in_months_eq_0_score_30,
last_loan_drawn_in_months_lt_3_score_minus_30,
last_loan_drawn_in_months_lte_12_score_40,
last_loan_drawn_in_months_gt_12_score_100,
weight=0.5
)
fact_last_loan_drawn_in_months_lte_12 = dict(last_loan_drawn_in_months=6)
assert last_loan_drawn_in_months_rule_set.evaluate(
fact_last_loan_drawn_in_months_lte_12) == 20.0
fact_rule_score = dict(last_loan_drawn_in_months=6, no_of_running_bl_pl=2)
rule_score = RuleScore(
no_of_run_bl_pl_rule_set,
last_loan_drawn_in_months_rule_set
)
assert rule_score.execute(fact_rule_score) == 35.0
no_run_bl_pl_gte_7_score_minus_100 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(7))), -100)
no_run_bl_pl_gte_4_score_minus_40 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(4))), -40)
no_run_bl_pl_gte_2_score_30 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(2))), 30)
no_run_bl_pl_gte_0_score_100 = RuleRowScore(
WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(0))), 100)
no_of_run_bl_pl_rule_set = RuleSetScore(
no_run_bl_pl_gte_7_score_minus_100,
no_run_bl_pl_gte_4_score_minus_40,
no_run_bl_pl_gte_2_score_30,
no_run_bl_pl_gte_0_score_100,
weight=0.5
)
fact_no_run_bl_pl_2 = dict(no_of_running_bl_pl=2)
assert no_of_run_bl_pl_rule_set.evaluate(fact_no_run_bl_pl_2) == 15.0
last_loan_drawn_in_months_eq_0_score_30 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Eq(0))),
30
)
last_loan_drawn_in_months_lt_3_score_minus_30 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Lt(3))),
-30
)
last_loan_drawn_in_months_lte_12_score_40 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Lte(12))),
40
)
last_loan_drawn_in_months_gt_12_score_100 = RuleRowScore(
WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Gt(12))),
100
)
last_loan_drawn_in_months_rule_set = RuleSetScore(
last_loan_drawn_in_months_eq_0_score_30,
last_loan_drawn_in_months_lt_3_score_minus_30,
last_loan_drawn_in_months_lte_12_score_40,
last_loan_drawn_in_months_gt_12_score_100,
weight=0.5
)
fact_last_loan_drawn_in_months_lte_12 = dict(last_loan_drawn_in_months=6)
assert last_loan_drawn_in_months_rule_set.evaluate(
fact_last_loan_drawn_in_months_lte_12) == 20.0
fact_rule_score = dict(last_loan_drawn_in_months=6, no_of_running_bl_pl=2)
rule_score = RuleScore(
no_of_run_bl_pl_rule_set,
last_loan_drawn_in_months_rule_set
)
assert rule_score.execute(fact_rule_score) == 35.0
A nested rule that involves another rule for evaulation
Rule
- If cibil score is between 650 and 800, score is 100
- If cibil score is less than 650, score is 0
- Decide GO if pet in [dog, cat] and cibil score is greater than 0
Rule Specification
from simpleruleengine.conditional.when_all import WhenAll
from simpleruleengine.expression.expression import Expression
from simpleruleengine.operator.between import Between
from simpleruleengine.operator.greater_than import Gt
from simpleruleengine.operator.greater_than_equal import Gte
from simpleruleengine.operator.string_in import In
from simpleruleengine.operator.less_than_equal import Lte
from simpleruleengine.operator.less_than import Lt
from simpleruleengine.operator.string_not_in import NotIn
from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
from simpleruleengine.rulerow.rule_row_score import RuleRowScore
from simpleruleengine.ruleset.rule_set_score import RuleSetScore
from simpleruleengine.token.numeric_token import NumericToken
from simpleruleengine.token.string_token import StringToken
from simpleruleengine.token.rule_token import RuleToken
from simpleruleengine.rule.rule_score import RuleScore
if __name__ == "__main__":
cibil_score_between_650_800 = Expression(
NumericToken("cibil_score"),
Between(floor=650, ceiling=800)
)
cibil_score_lt_650 = Expression(
NumericToken("cibil_score"),
Lt(650)
)
rule_row_between_650_800 = RuleRowScore(
antecedent=WhenAll(cibil_score_between_650_800),
consequent=100
)
rule_row_between_lt_650 = RuleRowScore(
antecedent=WhenAll(cibil_score_lt_650),
consequent=0
)
rule_set_cibil_score = RuleSetScore(
rule_row_between_lt_650,
rule_row_between_650_800,
weight=1
)
rule_cibil_score = RuleScore(rule_set_cibil_score)
fact = dict(cibil_score=350)
assert rule_cibil_score.execute(fact) == 0
expression_pet_in_dog_cat = Expression(
StringToken(name="pet"),
In("dog", "cat")
)
expression_cibil_score_gt_0 = Expression(
RuleToken(name="cibil_rule", rule=rule_cibil_score),
Gt(0)
)
when_all_cibil_and_pet = WhenAll(
expression_cibil_score_gt_0,
expression_pet_in_dog_cat
)
rule_row_go_cibil_and_pet = RuleRowDecision(
antecedent=when_all_cibil_and_pet,
consequent="GO"
)
rule_set_decision_cibil_score_and_pet = RuleSetDecision(
rule_row_go_cibil_and_pet)
fact = dict(cibil_score=350, pet="dog")
assert rule_set_decision_cibil_score_and_pet.evaluate(fact) != "GO"
fact = dict(cibil_score=725, pet="dog")
assert rule_set_decision_cibil_score_and_pet.evaluate(fact) == "GO"
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for simpleruleengine-1.0.0-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 698377b95622bdf4e62f48a9ae14667d9f00502c4a3e10a44f0f21dc4402143d |
|
MD5 | 2aecb78b120bf80fb8c36516616152b3 |
|
BLAKE2b-256 | 2f74fccdaa2b9f66755f74cf1983eac174ab56432fd0de6c0072add1568b04f9 |