Skip to main content

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

  1. Ability to declaratively author both Scoring and Decision Rules.
  2. Ability to version control rule declarations thus enabling auditing of rule changes over a period of time.
  3. 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).

Score Rule Concept

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.

Decision Rule Concept

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

simpleruleengine-2.0.0.tar.gz (16.3 kB view hashes)

Uploaded Source

Built Distribution

simpleruleengine-2.0.0-py3-none-any.whl (27.3 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page