Skip to content

Commit 57ded56

Browse files
committed
Release 0.1
1 parent adf911f commit 57ded56

16 files changed

Lines changed: 742 additions & 247 deletions

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
We happily welcome contributions to *PROJECT NAME*. We use GitHub Issues to track community reported issues and GitHub Pull Requests for accepting changes.
1+
We happily welcome contributions to *Databricks Labs - dataframe-rules-engine*.
2+
We use GitHub Issues to track community reported issues and GitHub Pull Requests for accepting changes.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[Project Name]
1+
dataframe-rules-engine
22

33
Copyright (2019) Databricks, Inc.
44

NOTICE

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
[Project Name]
2-
3-
Copyright (2018) Databricks, Inc.
4-
5-
6-
This Software includes software developed at Databricks (https://www.databricks.com/) and its use is subject to the included LICENSE file.
1+
dataframe-rules-engine
72

3+
Copyright (2018) Databricks, Inc.
84

9-
Additionally, this Software contains code from the following open source projects:
10-
11-
[Project Name - License]
5+
This Software includes software developed at Databricks (https://www.databricks.com/) and its use is subject to the included LICENSE file.

README.md

Lines changed: 174 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,183 @@
1-
# PROJECT NAME
2-
Standard Project Template for Databricks Labs Projects
1+
# Rules Engine
2+
Simplified Validation for Production Workloads
33

44
## Project Description
5-
Short description of project's purpose
5+
As pipelines move from bronze to gold, it's very common that some level of governance be performed in
6+
Silver or at various places in the pipeline. The need for business rule validation is very common.
7+
Databricks recognizes this and, as such, is building Delta Pipelines with Expectations.
8+
Upon release of Delta Pipelines, the need for this package will be re-evaluated and the code base will
9+
be adjusted appropriately. This is serves an immediate need and Delta Expectations is expected to be a more,
10+
full-fledged and robust example of this functionality.
611

7-
## Project Support
8-
Please note that all projects in the /databrickslabs github account are provided for your exploration only, and are not formally supported by Databricks with Service Level Agreements (SLAs). They are provided AS-IS and we do not make any guarantees of any kind. Please do not submit a support ticket relating to any issues arising from the use of these projects.
12+
Introducing Databricks Labs - dataframe-rules-engine, a simple solution for validating data in dataframes before you
13+
move the data to production and/or in-line (coming soon).
914

10-
Any issues discovered through the use of this project should be filed as GitHub Issues on the Repo. They will be reviewed as time permits, but there are no formal SLAs for support.
15+
![Alt Text](images/Rules_arch.png)
16+
## Using The Rules Engine In Your Project
17+
* Pull the latest release from the releases
18+
* Add it as a dependency (will be in Maven eventually)
19+
* Reference it in your imports
1120

21+
## Getting Started
22+
A list of usage examples is available in the `demo` folder of this repo in [html](demo/Rules_Engine_Examples.html)
23+
and as a [Databricks Notebook DBC](demo/Rules_Engine_Examples.dbc).
1224

13-
## Building the Project
14-
Instructions for how to build the project
25+
The process simple:
26+
* Define Rules
27+
* Build a RuleSet from your Dataframe using your Rules you built
28+
```scala
29+
import com.databricks.labs.validation.utils.Structures._
30+
import com.databricks.labs.validation._
31+
```
32+
33+
As of version 0.1 There are three primary rule types
34+
* Boundary Rules
35+
* Categorical Rules (Strings and Numerical)
36+
* Date Rules (in progress)
37+
38+
Rules can be composed of:
39+
* simple column references `col("my_column_name")`
40+
* complex columns `col("Revenue") - col("Cost")`
41+
* aggregate columns `min("ColumnName")`
42+
43+
Rules can be applied to simple DataFrames or grouped Dataframes. To use a grouped dataframe simply pass
44+
your dataframe into the RuleSet and pass one or more columns in as `by` columns. This will apply the rule
45+
at the group level which can be helpful at times.
46+
47+
### Simple Rule
48+
`val validateRetailPrice = Rule("Retail_Price_Validation", col("retail_price"), Bounds(0.0, 6.99))`
49+
50+
### List of Rules
51+
NOTE: While validations can be performed on aggregate cols (whether the DF is grouped or not) aggregate columns
52+
only return a single value - as such the failed count will be set to 1 for failures so for aggregate columns
53+
the `Invalid_Count` is rendered somewhat useless. Better granularity can be seen in the report when not using
54+
aggregates.
55+
```scala
56+
val specializedRules = Array(
57+
// Example of aggregate column
58+
Rule("Reasonable_sku_counts", count(col("sku")), Bounds(lower = 20.0, upper = 200.0)),
59+
// Example of calculated column from optimized UDF
60+
Rule("Max_allowed_discount",
61+
max(getDiscountPercentage(col("retail_price"), col("scan_price"))),
62+
Bounds(upper = 90.0)),
63+
// Example distinct values rule
64+
Rule("Unique_Skus", countDistinct("sku"), Bounds(upper = 1.0))
65+
)
66+
```
67+
68+
### MinMax Rules
69+
It's very common to build rules to validate min and max allowable values so there's a helper function
70+
to speed up this process. It really only makes sense to use minmax when specifying both an upper and a lower bound
71+
in the Bounds object. Using this method in the example below will only require three lines of code instead of the 6
72+
if each rule was built manually
73+
```scala
74+
val minMaxPriceDefs = Array(
75+
MinMaxRuleDef("MinMax_Sku_Price", col("retail_price"), Bounds(0.0, 29.99)),
76+
MinMaxRuleDef("MinMax_Scan_Price", col("scan_price"), Bounds(0.0, 29.99)),
77+
MinMaxRuleDef("MinMax_Cost", col("cost"), Bounds(0.0, 12.0))
78+
)
79+
80+
// Generate the array of Rules from the minmax generator
81+
val minMaxPriceRules = RuleSet.generateMinMaxRules(minMaxPriceDefs: _*)
82+
```
83+
OR -- simply add the list of minmax rules or simple individual rule definitions
84+
to an existing RuleSet (if not using builder pattern)
85+
```scala
86+
val someRuleSet = RuleSet(df)
87+
someRuleSet.addMinMaxRules(minMaxPriceDefs: _*)
88+
someRuleSet.addMinMaxRules("Retail_Price_Validation", col("retail_price"), Bounds(0.0, 6.99))
89+
```
90+
91+
### Categorical Rules
92+
There are two types of categorical rules which are used to validate against a pre-defined list of valid
93+
values. Currently (as of 0.1) accepted categorical types are String, Double, Int, Long
94+
```scala
95+
val catNumerics = Array(
96+
Rule("Valid_Stores", col("store_id"), Lookups.validStoreIDs),
97+
Rule("Valid_Skus", col("sku"), Lookups.validSkus)
98+
)
99+
100+
val catStrings = Array(
101+
Rule("Valid_Regions", col("region"), Lookups.validRegions)
102+
)
103+
```
15104

16-
## Deploying / Installing the Project
17-
Instructions for how to deploy the project, or install it
105+
### Validation
106+
Now that you have some rules built up... it's time to build the ruleset and validate it. As mentioned above,
107+
the dataframe can be simple or groupBy column[s] can be passed in (as string) to perform validation at the
108+
grouped level.
109+
```scala
110+
val (rulesReport, passed) = RuleSet(df)
111+
.add(specializedRules)
112+
.add(minMaxPriceRules)
113+
.add(catNumerics)
114+
.add(catStrings)
115+
.validate()
18116

19-
## Releasing the Project
20-
Instructions for how to release a version of the project
117+
val (rulesReport, passed) = RuleSet(df, Array("store_id"))
118+
.add(specializedRules)
119+
.add(minMaxPriceRules)
120+
.add(catNumerics)
121+
.add(catStrings)
122+
.validate()
123+
```
124+
The validation returns two items, a boolean (true/false) as to whether all rules passed or not. If a single rule
125+
fails the `passed` value above will return false. The `rulesReport` is a summary of which rules failed and,
126+
if the input column was not an aggregate column, the number of failed records. An image of the report is below.
127+
![Alt Text](images/rulesReport.png)
21128

22-
## Using the Project
23-
Simple examples on how to use the project
129+
## Next Steps
130+
Clearly, this is just a start. This is a small package and, as such, a GREAT place to start if you've never
131+
contributed to a project before. Please feel free to fork the repo and/or submit PRs. I'd love to see what
132+
you come up with. If you're not much of a developer or don't have the time you can still contribute! Please
133+
post your ideas in the issues and label them appropriately (i.e. bug/enhancement) and someone will review it
134+
and add it as soon as possible.
135+
136+
Some ideas of great adds are:
137+
* Add a Python wrapper
138+
* Refactor Rule and/or Validator to implement an Abstract class or trait
139+
* There's a clear opportunity to abstract away some of the redundancy between rule types.
140+
* Implement a fast runner
141+
* Optimize performance by failing fast for big data. Smart sampling could be implemented to review subsets
142+
of columns/records and look for failures to enable a faster failure.
143+
* Implement tests
144+
* Yeah, I know...I should have done this on day 0...but...time is always an issue. I plan to come back and add
145+
tests but if you'd like to add tests, that's a great way to learn code base (especially one this small)
146+
* Implement the date time rule (or somet other custom rule)
147+
* The date time rule has already been scaffolded, it just needs to be built out
148+
* What kind of complex rules does your business require that isn't possible here
149+
* Add a quarantine pattern
150+
* Enable a configuration to a Ruleset to identify records that didn't pass the validations and add
151+
them to a predefined quarantine zone.
152+
* Add logic to attempt to auto-handle certain types of failures based on common business patterns
153+
* When Delta Pipelines feature is release, simplify this package by wrapping the logic with pipelines.
154+
155+
156+
## Legal Information
157+
This software is provided as-is and is not officially supported by Databricks through customer technical support channels.
158+
Support, questions, and feature requests can be submitted through the Issues page of this repo.
159+
Please see the [legal agreement](LICENSE.txt) and understand that issues with the use of this code will
160+
not be answered or investigated by Databricks Support.
161+
162+
## Core Contribution team
163+
* Lead Developer: [Daniel Tomes](https://www.linkedin.com/in/tomes/), Practice Leader, Databricks
164+
* Developer: <b> your name here </b> Contribute to the project
165+
166+
167+
## Project Support
168+
Please note that all projects in the /databrickslabs github account are provided for your exploration only,
169+
and are not formally supported by Databricks with Service Level Agreements (SLAs).
170+
They are provided AS-IS and we do not make any guarantees of any kind.
171+
Please do not submit a support ticket relating to any issues arising from the use of these projects.
172+
173+
Any issues discovered through the use of this project should be filed as GitHub Issues on the Repo.
174+
They will be reviewed as time permits, but there are no formal SLAs for support.
175+
176+
177+
## Building the Project
178+
To build the project: <br>
179+
```
180+
cd Downloads
181+
git pull repo
182+
sbt clean package
183+
```

build.sbt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name := "Rules_Engine"
1+
name := "dataframe-rules-engine"
22

33
organization := "com.databricks"
44

@@ -8,7 +8,6 @@ scalaVersion := "2.11.12"
88
scalacOptions ++= Seq("-Xmax-classfile-name", "78")
99

1010
libraryDependencies += "org.apache.spark" %% "spark-core" % "2.4.0"
11-
libraryDependencies += "org.apache.spark" %% "spark-mllib" % "2.4.0"
1211
libraryDependencies += "org.apache.spark" %% "spark-sql" % "2.4.0"
1312

1413
lazy val commonSettings = Seq(

demo/Example.scala

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.databricks.labs.validation
2+
3+
import com.databricks.labs.validation.utils.{Lookups, SparkSessionWrapper}
4+
import com.databricks.labs.validation.utils.Structures._
5+
import org.apache.spark.sql.Column
6+
import org.apache.spark.sql.functions._
7+
8+
object Example extends App with SparkSessionWrapper {
9+
import spark.implicits._
10+
11+
/**
12+
* Validation example
13+
* Passing pre-built array of rules into a RuleSet and validating a non-grouped dataframe
14+
*/
15+
16+
/**
17+
* Example of a proper UDF to simplify rules logic. Simplification UDFs should take in zero or many
18+
* columns and return one column
19+
* @param retailPrice column 1
20+
* @param scanPrice column 2
21+
* @return result column of applied logic
22+
*/
23+
def getDiscountPercentage(retailPrice: Column, scanPrice: Column): Column = {
24+
(retailPrice - scanPrice) / retailPrice
25+
}
26+
27+
// Example of creating array of custom rules
28+
val specializedRules = Array(
29+
Rule("Reasonable_sku_counts", count(col("sku")), Bounds(lower = 20.0, upper = 200.0)),
30+
Rule("Max_allowed_discount",
31+
max(getDiscountPercentage(col("retail_price"), col("scan_price"))),
32+
Bounds(upper = 90.0)),
33+
Rule("Retail_Price_Validation", col("retail_price"), Bounds(0.0, 6.99)),
34+
Rule("Unique_Skus", countDistinct("sku"), Bounds(upper = 1.0))
35+
)
36+
37+
// It's common to generate many min/max boundaries. These can be generated easily
38+
// The generator function can easily be extended or overridden to satisfy more complex requirements
39+
val minMaxPriceDefs = Array(
40+
MinMaxRuleDef("MinMax_Sku_Price", col("retail_price"), Bounds(0.0, 29.99)),
41+
MinMaxRuleDef("MinMax_Scan_Price", col("scan_price"), Bounds(0.0, 29.99)),
42+
MinMaxRuleDef("MinMax_Cost", col("cost"), Bounds(0.0, 12.0))
43+
)
44+
45+
val minMaxPriceRules = RuleSet.generateMinMaxRules(minMaxPriceDefs: _*)
46+
val someRuleSet = RuleSet(df)
47+
someRuleSet.addMinMaxRules(minMaxPriceDefs: _*)
48+
someRuleSet.addMinMaxRules("Retail_Price_Validation", col("retail_price"), Bounds(0.0, 6.99))
49+
50+
51+
val catNumerics = Array(
52+
Rule("Valid_Stores", col("store_id"), Lookups.validStoreIDs),
53+
Rule("Valid_Skus", col("sku"), Lookups.validSkus)
54+
)
55+
56+
val catStrings = Array(
57+
Rule("Valid_Regions", col("region"), Lookups.validRegions)
58+
)
59+
60+
//TODO - validate datetime
61+
// Test, example data frame
62+
val df = sc.parallelize(Seq(
63+
("Northwest", 1001, 123456, 9.32, 8.99, 4.23, "2020-02-01 00:00:00.000"),
64+
("Northwest", 1001, 123256, 19.99, 16.49, 12.99, "2020-02-01"),
65+
("Northwest", 1001, 123456, 0.99, 0.99, 0.10, "2020-02-01"),
66+
("Northwest", 1001, 123456, 0.98, 0.90, 0.10, "2020-02-01"), // non_distinct sku
67+
("Northwst", 1001, 123456, 0.99, 0.99, 0.10, "2020-02-01"), // Misspelled Region
68+
("Northwest", 1002, 122987, 9.99, 9.49, 6.49, "2021-02-01"), // Invalid Date/Timestamp
69+
("Northwest", 1002, 173544, 1.29, 0.99, 1.23, "2020-02-01"),
70+
("Northwest", 1002, 168212, 3.29, 1.99, 1.23, "2020-02-01"),
71+
("Northwest", 1002, 365423, 1.29, 0.99, 1.23, "2020-02-01"),
72+
("Northwest", 1002, 3897615, 14.99, 129.99, 1.23, "2020-02-01"),
73+
("Northwest", 1003, 163212, 3.29, 1.99, 1.23, "2020-02-01") // Invalid numeric store_id groupby test
74+
)).toDF("region", "store_id", "sku", "retail_price", "scan_price", "cost", "create_ts")
75+
.withColumn("create_ts", 'create_ts.cast("timestamp"))
76+
.withColumn("create_dt", 'create_ts.cast("date"))
77+
78+
// Doing the validation
79+
// The validate method will return the rules report dataframe which breaks down which rules passed and which
80+
// rules failed and how/why. The second return value returns a boolean to determine whether or not all tests passed
81+
// val (rulesReport, passed) = RuleSet(df, Array("store_id"))
82+
val (rulesReport, passed) = RuleSet(df)
83+
.add(specializedRules)
84+
.add(minMaxPriceRules)
85+
.add(catNumerics)
86+
.add(catStrings)
87+
.validate(2)
88+
89+
rulesReport.show(200, false)
90+
// rulesReport.printSchema()
91+
92+
93+
}

demo/Rules_Engine_Examples.dbc

5.08 KB
Binary file not shown.

demo/Rules_Engine_Examples.html

Lines changed: 42 additions & 0 deletions
Large diffs are not rendered by default.

images/Rules_arch.png

168 KB
Loading

images/rulesReport.png

65.8 KB
Loading

0 commit comments

Comments
 (0)