|
| 1 | +from typing import Optional |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +import pandas as pd |
| 5 | +import whylogs as why |
| 6 | +from flytekitplugins.whylogs.renderer import WhylogsConstraintsRenderer, WhylogsSummaryDriftRenderer |
| 7 | +from whylogs.core.constraints import ConstraintsBuilder, MetricConstraint, MetricsSelector |
| 8 | + |
| 9 | +import flytekit |
| 10 | +from flytekit import task, workflow |
| 11 | + |
| 12 | + |
| 13 | +@task |
| 14 | +def make_data(n_rows: int) -> pd.DataFrame: |
| 15 | + data = { |
| 16 | + "sepal_length": np.random.random_sample(n_rows), |
| 17 | + "sepal_width": np.random.random_sample(n_rows), |
| 18 | + "petal_length": np.random.random_sample(n_rows), |
| 19 | + "petal_width": np.random.random_sample(n_rows), |
| 20 | + "species": np.random.choice(["virginica", "setosa", "versicolor"], n_rows), |
| 21 | + "species_id": np.random.choice([1, 2, 3], n_rows), |
| 22 | + } |
| 23 | + return pd.DataFrame(data) |
| 24 | + |
| 25 | + |
| 26 | +@task |
| 27 | +def run_constraints(df: pd.DataFrame, min_value: Optional[float] = 0.0, max_value: Optional[float] = 4.0) -> bool: |
| 28 | + # This API constraints workflow is very flexible but a bit cumbersome. |
| 29 | + # It will be simplified in the future, so for now we'll stick with injecting |
| 30 | + # a Constraints object to the renderer. |
| 31 | + profile_view = why.log(df).view() |
| 32 | + builder = ConstraintsBuilder(profile_view) |
| 33 | + num_constraint = MetricConstraint( |
| 34 | + name=f"numbers between {min_value} and {max_value} only", |
| 35 | + condition=lambda x: x.min > min_value and x.max < max_value, |
| 36 | + metric_selector=MetricsSelector(metric_name="distribution", column_name="sepal_length"), |
| 37 | + ) |
| 38 | + |
| 39 | + builder.add_constraint(num_constraint) |
| 40 | + constraints = builder.build() |
| 41 | + |
| 42 | + renderer = WhylogsConstraintsRenderer() |
| 43 | + flytekit.Deck("constraints", renderer.to_html(constraints=constraints)) |
| 44 | + |
| 45 | + return constraints.validate() |
| 46 | + |
| 47 | + |
| 48 | +@workflow |
| 49 | +def whylogs_renderers_workflow(min_value: float, max_value: float) -> bool: |
| 50 | + df = make_data(n_rows=10) |
| 51 | + validated = run_constraints(df=df, min_value=min_value, max_value=max_value) |
| 52 | + return validated |
| 53 | + |
| 54 | + |
| 55 | +def test_constraints_passing(): |
| 56 | + validated = whylogs_renderers_workflow(min_value=0.0, max_value=1.0) |
| 57 | + assert validated is True |
| 58 | + |
| 59 | + |
| 60 | +def test_constraints_failing(): |
| 61 | + validated = whylogs_renderers_workflow(min_value=-1.0, max_value=0.0) |
| 62 | + assert validated is False |
| 63 | + |
| 64 | + |
| 65 | +def test_summary_drift_report_is_written(): |
| 66 | + renderer = WhylogsSummaryDriftRenderer() |
| 67 | + new_data = make_data(n_rows=10) |
| 68 | + reference_data = make_data(n_rows=100) |
| 69 | + |
| 70 | + report = renderer.to_html(target_data=new_data, reference_data=reference_data) |
| 71 | + assert report is not None |
| 72 | + assert isinstance(report, str) |
| 73 | + assert "Profile Summary" in report |
0 commit comments