whylabs/whylogs-python

View on GitHub
python/examples/advanced/Metric_Constraints_with_Condition_Count_Metrics.ipynb

Summary

Maintainability
Test Coverage
{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Creating Metric Constraints on Condition Count Metrics"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "whylogs profiles contain summarized information about our data. This means that it's a __lossy__ process, and once we get the profiles, we don't have access anymore to the complete set of data.\n",
    "\n",
    "This makes some types of constraints impossible to be created from standard metrics itself. For example, suppose you need to check every row of a column to check that there are no textual information that matches a credit card number or email information. Or maybe you're interested in ensuring that there are no even numbers in a certain column. How do we do that if we don't have access to the complete data?\n",
    "\n",
    "The answer is that you need to define a __Condition Count Metric__ to be tracked __before__ logging your data. This metric will count the number of times the values of a given column meets a user-defined condition. When the profile is generated, you'll have that information to check against the constraints you'll create.\n",
    "\n",
    "In this example, you'll learn how to:\n",
    "- Define additional Condition Count Metrics\n",
    "- Define actions to be triggered whenever those conditions are met during the logging process.\n",
    "- Use the Condition Count Metrics to create constraints against said conditions\n",
    "\n",
    "If you want more information on Condition Count Metrics, you can see [this example](https://nbviewer.org/github/whylabs/whylogs/blob/mainline/python/examples/advanced/Condition_Count_Metrics.ipynb) and also the documentation for [Data Validation](https://whylogs.readthedocs.io/en/stable/features/data_validation.html)\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Installing whylogs\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Note: you may need to restart the kernel to use updated packages.\n",
    "%pip install whylogs"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Context\n",
    "\n",
    "Let's assume we have a DataFrame for which we wish to log standard metrics through whylogs' default logging process. But additionally, we want specific information on two columns:\n",
    "\n",
    "- `url`: Regex pattern validation: the values in this column should always start with `https:://www.mydomain.com/profile`\n",
    "- `subscription_date`: Date Format validation: the values in this column should be a string with a date format of `%Y-%m-%d`\n",
    "\n",
    "In addition, we consider these cases to be critical, so we wish to make certain actions whenever the condition fails. In this example we will:\n",
    "\n",
    "- Send an alert in Slack whenever `subscription_date` fails the condition\n",
    "- Send an alert in Slack and pull a symbolic Andon Cord whenever `url` is not from the domain we expect\n",
    "\n",
    "Let's first create a simple DataFrame to demonstrate:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "data = {\n",
    "        \"name\": [\"Alice\", \"Bob\", \"Charles\"],\n",
    "        \"age\": [31,0,25],\n",
    "        \"url\": [\"https://www.mydomain.com/profile/123\", \"www.wrongdomain.com\", \"http://mydomain.com/unsecure\"],\n",
    "        \"subscription_date\": [\"2021-12-28\",\"2019-29-11\",\"04/08/2021\"],\n",
    "    }\n",
    "\n",
    "df = pd.DataFrame(data)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this case, both `url` and `subscription_date` has 2 values out of 3 that are not what we expect."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Defining the Relations\n",
    "\n",
    "Let's first define the relations that will actually check whether the value passes our constraint. For the date format validation, we'll use the __datetime__ module in a user defined function. As for the Regex pattern matching, we will use whylogs' `Predicates` along with regular expressions, which allows us to build simple relations intuitively."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime\n",
    "from typing import Any\n",
    "from whylogs.core.relations import Predicate\n",
    "\n",
    "\n",
    "def date_format(x: Any) -> bool:\n",
    "    date_format = '%Y-%m-%d'\n",
    "    try:\n",
    "        datetime.datetime.strptime(x, date_format)\n",
    "        return True\n",
    "    except ValueError:\n",
    "        return False\n",
    "\n",
    "# matches accept a regex expression\n",
    "matches_domain_url = Predicate().matches(\"^https:\\/\\/www.mydomain.com\\/profile\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Defining the Actions\n",
    "\n",
    "Next, we need to define the actions that will be triggered whenever the conditions fail.\n",
    "\n",
    "We will define two placeholder functions that, in a real scenario, would execute the defined actions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any\n",
    "\n",
    "def pull_andon_cord(validator_name, condition_name: str, value: Any):\n",
    "    print(\"Validator: {}\\n    Condition name {} failed for value {}\".format(validator_name, condition_name, value))\n",
    "    print(\"    Pulling andon cord....\")\n",
    "    # Do something here to respond to the constraint violation\n",
    "    return\n",
    "\n",
    "def send_slack_alert(validator_name, condition_name: str, value: Any):\n",
    "    print(\"Validator: {}\\n    Condition name {} failed for value {}\".format(validator_name, condition_name, value))\n",
    "    print(\"    Sending slack alert....\")\n",
    "    # Do something here to respond to the constraint violation\n",
    "    return"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conditions = Relations + Actions\n",
    "\n",
    "Conditions are defined by the combination of a relation and a set of actions. Now that we have both relations and actions, we can create two sets of conditions - in this example, each set contain a single condition, but we could have multiple."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from whylogs.core.metrics.condition_count_metric import Condition\n",
    "\n",
    "has_date_format = {\n",
    "    \"Y-m-d format\": Condition(date_format, actions=[send_slack_alert]),\n",
    "}\n",
    "\n",
    "regex_conditions = {\"url_matches_domain\": Condition(matches_domain_url, actions=[pull_andon_cord,send_slack_alert])}\n",
    "\n",
    "ints_conditions = {\n",
    "    \"integer_zeros\": Condition(Predicate().equals(0)),\n",
    "}"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Passing the conditions to the Logger\n",
    "\n",
    "Now, we need to let the logger aware of our Conditions. This can be done by creating a custom schema object that will be passed to `why.log()`.\n",
    "\n",
    "To create the schema object, we will use the __Declarative Schema__, which is an auxiliary class that will enable us to create a schema in a simple way.\n",
    "\n",
    "In this case, we want our schema to start with the default behavior (standard metrics for the default datatypes). Then, we want to add two condition count metrics based on the conditions we defined earlier and the name of the column we want to bind those conditions to. We can do so by calling the schema's `add_condition_count_metric` method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from whylogs.core.resolvers import STANDARD_RESOLVER\n",
    "from whylogs.core.specialized_resolvers import ConditionCountMetricSpec\n",
    "from whylogs.core.schema import DeclarativeSchema\n",
    "\n",
    "schema = DeclarativeSchema(STANDARD_RESOLVER)\n",
    "\n",
    "schema.add_resolver_spec(column_name=\"subscription_date\", metrics=[ConditionCountMetricSpec(has_date_format)])\n",
    "schema.add_resolver_spec(column_name=\"url\", metrics=[ConditionCountMetricSpec(regex_conditions)])\n",
    "schema.add_resolver_spec(column_name=\"age\", metrics=[ConditionCountMetricSpec(ints_conditions)])\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, let's pass the schema to why.log() and start logging our data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Validator: condition_count\n",
      "    Condition name url_matches_domain failed for value www.wrongdomain.com\n",
      "    Pulling andon cord....\n",
      "Validator: condition_count\n",
      "    Condition name url_matches_domain failed for value www.wrongdomain.com\n",
      "    Sending slack alert....\n",
      "Validator: condition_count\n",
      "    Condition name url_matches_domain failed for value http://mydomain.com/unsecure\n",
      "    Pulling andon cord....\n",
      "Validator: condition_count\n",
      "    Condition name url_matches_domain failed for value http://mydomain.com/unsecure\n",
      "    Sending slack alert....\n",
      "Validator: condition_count\n",
      "    Condition name Y-m-d format failed for value 2019-29-11\n",
      "    Sending slack alert....\n",
      "Validator: condition_count\n",
      "    Condition name Y-m-d format failed for value 04/08/2021\n",
      "    Sending slack alert....\n"
     ]
    }
   ],
   "source": [
    "import whylogs as why\n",
    "profile_view = why.log(df, schema=schema).profile().view()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that during the logging process, our actions were triggered whenever the condition failed. We can see the name of the failed condition and the specific value that triggered it.\n",
    "\n",
    "We see the actions were triggered, but we also expect the Condition Count Metrics to be generated. Let's see if this is the case:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>cardinality/est</th>\n",
       "      <th>cardinality/lower_1</th>\n",
       "      <th>cardinality/upper_1</th>\n",
       "      <th>condition_count/integer_zeros</th>\n",
       "      <th>condition_count/total</th>\n",
       "      <th>counts/inf</th>\n",
       "      <th>counts/n</th>\n",
       "      <th>counts/nan</th>\n",
       "      <th>counts/null</th>\n",
       "      <th>distribution/max</th>\n",
       "      <th>distribution/mean</th>\n",
       "      <th>distribution/median</th>\n",
       "      <th>distribution/min</th>\n",
       "      <th>distribution/n</th>\n",
       "      <th>distribution/q_01</th>\n",
       "      <th>distribution/q_05</th>\n",
       "      <th>distribution/q_10</th>\n",
       "      <th>distribution/q_25</th>\n",
       "      <th>distribution/q_75</th>\n",
       "      <th>distribution/q_90</th>\n",
       "      <th>distribution/q_95</th>\n",
       "      <th>distribution/q_99</th>\n",
       "      <th>distribution/stddev</th>\n",
       "      <th>frequent_items/frequent_strings</th>\n",
       "      <th>ints/max</th>\n",
       "      <th>ints/min</th>\n",
       "      <th>type</th>\n",
       "      <th>types/boolean</th>\n",
       "      <th>types/fractional</th>\n",
       "      <th>types/integral</th>\n",
       "      <th>types/object</th>\n",
       "      <th>types/string</th>\n",
       "      <th>types/tensor</th>\n",
       "      <th>condition_count/Y-m-d format</th>\n",
       "      <th>condition_count/url_matches_domain</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>column</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>age</th>\n",
       "      <td>3.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3.00015</td>\n",
       "      <td>1.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>31.0</td>\n",
       "      <td>18.666667</td>\n",
       "      <td>25.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>3</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>31.0</td>\n",
       "      <td>31.0</td>\n",
       "      <td>31.0</td>\n",
       "      <td>31.0</td>\n",
       "      <td>16.441817</td>\n",
       "      <td>[FrequentItem(value='25', est=1, upper=1, lowe...</td>\n",
       "      <td>31.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>SummaryType.COLUMN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>name</th>\n",
       "      <td>3.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3.00015</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>[FrequentItem(value='Alice', est=1, upper=1, l...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>SummaryType.COLUMN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>subscription_date</th>\n",
       "      <td>3.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3.00015</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>[FrequentItem(value='2019-29-11', est=1, upper...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>SummaryType.COLUMN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>url</th>\n",
       "      <td>3.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3.00015</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>[FrequentItem(value='www.wrongdomain.com', est...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>SummaryType.COLUMN</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                   cardinality/est  ...  condition_count/url_matches_domain\n",
       "column                              ...                                    \n",
       "age                            3.0  ...                                 NaN\n",
       "name                           3.0  ...                                 NaN\n",
       "subscription_date              3.0  ...                                 NaN\n",
       "url                            3.0  ...                                 1.0\n",
       "\n",
       "[4 rows x 35 columns]"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "profile_view.to_pandas()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "At the far right of our summary dataframe, you can find the Condition Count Metrics: the `Y-m-d format` condition was met only once of a total of 3. The same happens for the `url_matches_domain`. Note that for columns where the condition was not defined, a `NaN` is displayed."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Creating Metric Constraints based on Condition Count Metrics"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So far, we created Condition Count Metrics for both of the desired conditions. During the logging process, the set of actions defined for each of the conditions were triggered whenever the conditions failed to be met.\n",
    "\n",
    "Now, we wish to create Metric Constraints on top of the Condition Count Metrics, so we can generate a Constraints Report. This can be done by using the `condition_meets` helper constraint. You only need to specify the column name and the name of the condition you want to check:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[ReportResult(name='subscription_date meets condition Y-m-d format', passed=0, failed=1, summary=None),\n",
       " ReportResult(name='url never meets condition url_matches_domain', passed=0, failed=1, summary=None),\n",
       " ReportResult(name='age.integer_zeros lower than or equal to 1', passed=1, failed=0, summary=None)]"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from whylogs.core.constraints.factories import condition_meets, condition_never_meets, condition_count_below\n",
    "from whylogs.core.constraints import ConstraintsBuilder\n",
    "\n",
    "builder = ConstraintsBuilder(dataset_profile_view=profile_view)\n",
    "\n",
    "builder.add_constraint(condition_meets(column_name=\"subscription_date\", condition_name=\"Y-m-d format\"))\n",
    "builder.add_constraint(condition_never_meets(column_name=\"url\", condition_name=\"url_matches_domain\"))\n",
    "builder.add_constraint(condition_count_below(column_name=\"age\", condition_name=\"integer_zeros\", max_count=1))\n",
    "\n",
    "constraints = builder.build()\n",
    "constraints.generate_constraints_report()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `condition_meets` constraint will fail if the said condition is not met at least once. In other words, if `condition_count/condition_name` is smaller than `condition_count/total`.\n",
    "\n",
    "The `condition_never_meets` constraint will fail if the said condition is met at least once. In other words, if `condition_count/condition_name` is greater than 0.\n",
    "\n",
    "The `condition_count_below` constraint will fail if the said condition is met more than a specified number of times."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Visualizing the Report"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can visualize the Constraints Report as usual by calling `NotebookProfileVisualizer`'s `constraints_report`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div></div><iframe srcdoc=\"&lt;!DOCTYPE html&gt;\n",
       "&lt;html lang=&quot;en&quot;&gt;\n",
       "  &lt;head&gt;\n",
       "    &lt;meta charset=&quot;UTF-8&quot; /&gt;\n",
       "    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot; /&gt;\n",
       "    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;\n",
       "    &lt;meta name=&quot;description&quot; content=&quot;&quot; /&gt;\n",
       "    &lt;meta name=&quot;author&quot; content=&quot;&quot; /&gt;\n",
       "\n",
       "    &lt;title&gt;Profile Visualizer | whylogs&lt;/title&gt;\n",
       "\n",
       "    &lt;link rel=&quot;icon&quot; href=&quot;images/whylabs-favicon.png&quot; type=&quot;image/png&quot; sizes=&quot;16x16&quot; /&gt;\n",
       "    &lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.googleapis.com&quot; /&gt;\n",
       "    &lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.gstatic.com&quot; crossorigin /&gt;\n",
       "    &lt;link href=&quot;https://fonts.googleapis.com/css2?family=Asap:wght@400;500;600;700&amp;display=swap&quot; rel=&quot;stylesheet&quot; /&gt;\n",
       "    &lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.gstatic.com&quot; /&gt;\n",
       "    &lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css&quot; /&gt;\n",
       "\n",
       "    &lt;script\n",
       "      src=&quot;https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.7/handlebars.min.js&quot;\n",
       "      integrity=&quot;sha512-RNLkV3d+aLtfcpEyFG8jRbnWHxUqVZozacROI4J2F1sTaDqo1dPQYs01OMi1t1w9Y2FdbSCDSQ2ZVdAC8bzgAg==&quot;\n",
       "      crossorigin=&quot;anonymous&quot;\n",
       "      referrerpolicy=&quot;no-referrer&quot;\n",
       "    &gt;&lt;/script&gt;\n",
       "\n",
       "    &lt;style type=&quot;text/css&quot;&gt;\n",
       "\n",
       "      /* Screen on smaller screens */\n",
       "      .no-responsive {\n",
       "        display: none;\n",
       "        position: fixed;\n",
       "        top: 0;\n",
       "        left: 0;\n",
       "        z-index: 1031;\n",
       "        width: 100vw;\n",
       "        height: 100vh;\n",
       "        background-color: var(--tealBackground);\n",
       "        display: flex;\n",
       "        align-items: center;\n",
       "        justify-content: center;\n",
       "      }\n",
       "\n",
       "      @media screen and (min-width: 1000px) {\n",
       "        .desktop-content {\n",
       "          display: block;\n",
       "        }\n",
       "        .no-responsive {\n",
       "          display: none;\n",
       "        }\n",
       "      }\n",
       "\n",
       "      .no-responsive__content {\n",
       "        max-width: 600px;\n",
       "        width: 100%;\n",
       "        padding: 0 24px;\n",
       "      }\n",
       "\n",
       "      .no-responsive__title {\n",
       "        font-size: 96px;\n",
       "        font-weight: 300;\n",
       "        color: var(--brandSecondary900);\n",
       "        line-height: 1.167;\n",
       "      }\n",
       "\n",
       "      .no-responsive__text {\n",
       "        margin: 0;\n",
       "        font-size: 16px;\n",
       "        font-weight: 400;\n",
       "        color: var(--brandSecondary900);\n",
       "        line-height: 1.5;\n",
       "      }\n",
       "\n",
       "      .header-title {\n",
       "        font-size: 26px;\n",
       "        font-weight: 700;\n",
       "        color: #444444;\n",
       "      }\n",
       "\n",
       "      .tooltip-full-number {\n",
       "        position: relative;\n",
       "        display: inline-block;\n",
       "      }\n",
       "\n",
       "      .tooltip-full-number .tooltiptext {\n",
       "        visibility: hidden;\n",
       "        background: black;\n",
       "        color: white;\n",
       "        border: 1px solid black;\n",
       "        text-align: start;\n",
       "        padding: 3px;\n",
       "        position: absolute;\n",
       "        z-index: 1;\n",
       "        top: 0;\n",
       "        left: 100%;\n",
       "        margin-left: 5px;\n",
       "        opacity: 0;\n",
       "        transition: opacity 0.5s;\n",
       "        font-size: 13px;\n",
       "        font-weight: normal;\n",
       "        line-height: 100%;\n",
       "      }\n",
       "\n",
       "      .tooltip-full-number:hover .tooltiptext {\n",
       "        visibility: visible;\n",
       "        opacity: 1;\n",
       "      }\n",
       "\n",
       "\n",
       "      .wl-compare-profile {\n",
       "        position: relative;\n",
       "        left: 0;\n",
       "        padding: 30px;\n",
       "        margin-bottom: 20px;\n",
       "        background: var(--white);;\n",
       "        border-bottom: 1px solid #CED4DA;\n",
       "      }\n",
       "\n",
       "      .alert-list {\n",
       "        padding: 30px;\n",
       "        padding-top: 0;\n",
       "      }\n",
       "\n",
       "    .drift-detection {\n",
       "       justify-content: space-between;\n",
       "       align-items: center;\n",
       "     }\n",
       "\n",
       "     .drift-detection-info-circle {\n",
       "       width: 15px;\n",
       "       height: 15px;\n",
       "       border-radius: 50px;\n",
       "       display: inline-block;\n",
       "       margin-right: 8px;\n",
       "     }\n",
       "\n",
       "     .drift-detection-info-drifts-item {\n",
       "         padding-right: 20px;\n",
       "     }\n",
       "\n",
       "     .drift-detection-info-title {\n",
       "       font-family: Arial;\n",
       "       font-weight: bold;\n",
       "       font-size: 22px;\n",
       "       line-height: 130%;\n",
       "       color: #313B3D;\n",
       "     }\n",
       "\n",
       "     .drift-detection-info-drifts-item-count {\n",
       "       font-family: Arial;\n",
       "       font-weight: bold;\n",
       "       font-size: 14px;\n",
       "       line-height: 16px;\n",
       "       color: #000000;\n",
       "       padding-right: 8px;\n",
       "     }\n",
       "\n",
       "     .drift-detection-info-drifts-item-name {\n",
       "       font-family: Arial;\n",
       "       font-style: normal;\n",
       "       font-weight: normal;\n",
       "       font-size: 12px;\n",
       "       line-height: 14px;\n",
       "       color: #000000;\n",
       "     }\n",
       "\n",
       "     .drift-detection-search-input {\n",
       "       display: flex;\n",
       "       align-items: center;\n",
       "       background: rgba(255, 255, 255, 0.7);\n",
       "       border: 1px solid #DBE5E7;\n",
       "       box-sizing: border-box;\n",
       "       border-radius: 4px;\n",
       "       width: 170px;\n",
       "       padding-left: 10px;\n",
       "     }\n",
       "\n",
       "     .drift-detection-search-input img{\n",
       "       margin-right: 5px;\n",
       "     }\n",
       "\n",
       "     .drift-detection-search-input input::placeholder {\n",
       "       font-family: Arial;\n",
       "       font-weight: normal;\n",
       "       font-size: 13px;\n",
       "       line-height: 16px;\n",
       "       color: #313B3D;\n",
       "     }\n",
       "\n",
       "     .dropdown-container {\n",
       "       position: absolute;\n",
       "       right: 30px;\n",
       "       top: 80px;\n",
       "       z-index: 999;\n",
       "       background: #FFFFFF;\n",
       "       border: 1px solid #DBE5E7;\n",
       "       box-sizing: border-box;\n",
       "       box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.05);\n",
       "       border-radius: 4px;\n",
       "       padding: 10px !important;\n",
       "       border: none !important;\n",
       "     }\n",
       "\n",
       "     .filter-options-title {\n",
       "       width: 240px;\n",
       "     }\n",
       "\n",
       "     .filter-options-title p {\n",
       "       margin: 0;\n",
       "     }\n",
       "\n",
       "     .form-check-input:checked {\n",
       "       background-color: #0E7384;\n",
       "       border-color: #0E7384;\n",
       "     }\n",
       "\n",
       "     .form-check-input[type=checkbox] {\n",
       "       border-radius: 2px;\n",
       "     }\n",
       "\n",
       "     .search-input{\n",
       "       padding-top: 0 !important;\n",
       "       padding-bottom: 0 !important;\n",
       "     }\n",
       "\n",
       "     .search-input input{\n",
       "       border: none;\n",
       "       background: none;\n",
       "       outline: none;\n",
       "       height: 40px;\n",
       "       width: 100%;\n",
       "       font-size: 14px;\n",
       "     }\n",
       "\n",
       "     .search-input img{\n",
       "       height: 19px;\n",
       "       pointer-events: none;\n",
       "     }\n",
       "\n",
       "     input::placeholder {\n",
       "       color: var(--secondaryLight1000);\n",
       "     }\n",
       "\n",
       "      .statistics {\n",
       "        width: 100%;\n",
       "      }\n",
       "\n",
       "     .close-filter-button {\n",
       "       display: flex;\n",
       "       justify-content: center;\n",
       "       align-items: center;\n",
       "       background: rgba(255, 255, 255, 0.7);\n",
       "       border: 1px solid #369BAC;\n",
       "       box-sizing: border-box;\n",
       "       border-radius: 4px;\n",
       "       width: 40px;\n",
       "       height: 40px;\n",
       "       cursor: pointer;\n",
       "       margin-left: 10px;\n",
       "     }\n",
       "\n",
       "      .statistic-number-title {\n",
       "        font-family: Arial;\n",
       "        font-weight: normal;\n",
       "        font-size: 14px;\n",
       "        line-height: 20px;\n",
       "        color: #6C757D;\n",
       "      }\n",
       "\n",
       "      .statistic-number {\n",
       "        font-family: Arial;\n",
       "        font-weight: bold;\n",
       "        font-size: 20px;\n",
       "        line-height: 140%;\n",
       "        display: flex;\n",
       "        align-items: center;\n",
       "        color: #4F595B;\n",
       "      }\n",
       "\n",
       "      .full-summary-statistics-wrap {\n",
       "        padding: 20px;\n",
       "      }\n",
       "\n",
       "      .statistics {\n",
       "        width: 100%;\n",
       "      }\n",
       "\n",
       "      .statistics-list {\n",
       "        width: 100% ;\n",
       "      }\n",
       "\n",
       "      mark {\n",
       "        padding: 5px;\n",
       "        border-radius: 4px;\n",
       "        font-family: Arial;\n",
       "        font-weight: normal;\n",
       "        font-size: 14px;\n",
       "        line-height: 140%;\n",
       "      }\n",
       "\n",
       "      .blue-mark {\n",
       "        background-color:  #369BAC1A;\n",
       "        color: #369BAC;\n",
       "      }\n",
       "\n",
       "      .red-mark {\n",
       "        background-color: #FFEFEE;\n",
       "        color: #F5473C;\n",
       "      }\n",
       "\n",
       "      .alert-tag {\n",
       "        height: 27px;\n",
       "        padding: 5px;\n",
       "        border-radius: 4px;\n",
       "        font-family: Arial;\n",
       "        font-weight: bold;\n",
       "        font-size: 12px;\n",
       "        line-height: 140%;\n",
       "        color: #FFFFFF;\n",
       "      }\n",
       "\n",
       "      .turquoise-background-color {\n",
       "        background-color: #1DBB42;\n",
       "      }\n",
       "\n",
       "      .bordeaux-background-color {\n",
       "        background-color: #C6462A;\n",
       "      }\n",
       "\n",
       "      .border-solid-gray {\n",
       "        border: 1px solid #CED4DA;\n",
       "        border-radius: 4px;\n",
       "      }\n",
       "\n",
       "      .display-flex {\n",
       "        display: flex;\n",
       "      }\n",
       "\n",
       "      .justify-content-space-between {\n",
       "        justify-content: space-between;\n",
       "      }\n",
       "\n",
       "      .justify-content-center {\n",
       "        justify-content: center;\n",
       "      }\n",
       "\n",
       "      .align-items-center {\n",
       "        align-items: center;\n",
       "      }\n",
       "\n",
       "      .align-items-flex-start {\n",
       "        align-items: flex-start;\n",
       "      }\n",
       "\n",
       "      .padding-right-30 {\n",
       "        padding-right: 30px;\n",
       "      }\n",
       "\n",
       "      .notif-circle-container{\n",
       "        position: absolute;\n",
       "        top: 25px;\n",
       "        right: 25px;\n",
       "        padding: 5.3px;\n",
       "        border-radius: 50%;\n",
       "        background-color: white;\n",
       "        cursor: pointer;\n",
       "      }\n",
       "\n",
       "      .notif-circle {\n",
       "        position: absolute;\n",
       "        top: 2px;\n",
       "        right: 2px;\n",
       "        padding: 3.3px;\n",
       "        border-radius: 50%;\n",
       "        background-color: #F2994A;\n",
       "      }\n",
       "\n",
       "      .alert-list-text {\n",
       "        width: 70%\n",
       "      }\n",
       "\n",
       "     @media screen and (min-width: 500px) {\n",
       "       .desktop-content {\n",
       "         display: block;\n",
       "       }\n",
       "       .no-responsive {\n",
       "         display: none;\n",
       "       }\n",
       "     }\n",
       "    &lt;/style&gt;\n",
       "  &lt;/head&gt;\n",
       "\n",
       "  &lt;body id=&quot;generated-html&quot;&gt;&lt;/body&gt;\n",
       "\n",
       "  &lt;script id=&quot;entry-template&quot; type=&quot;text/x-handlebars-template&quot;&gt;\n",
       "    \n",
       "      &lt;div class=&quot;desktop-content&quot;&gt;\n",
       "        &lt;div class=&quot;full-summary-statistics-wrap&quot;&gt;\n",
       "          &lt;div class=&quot;full-summary-statistics&quot;&gt;\n",
       "            &lt;div&gt;\n",
       "              &lt;div class=&quot;display-flex justify-content-center&quot;&gt;\n",
       "                &lt;div class=&quot;statistics-list border-solid-gray&quot;&gt;\n",
       "                  &lt;div&gt;\n",
       "                    &lt;div class=&quot;wl-compare-profile&quot; id=&quot;compare-profile&quot;&gt;\n",
       "                        &lt;div class=&quot;drift-detection-wrap&quot;&gt;\n",
       "                          &lt;div class=&quot;drift-detection display-flex align-items-flex-start&quot;&gt;\n",
       "                            &lt;div class=&quot;drift-detection-info flex-direction-colum&quot;&gt;\n",
       "                              &lt;div class=&quot;drift-detection-info-title-wrap display-flex&quot;&gt;\n",
       "                                &lt;p class=&quot;drift-detection-info-title&quot;&gt;\n",
       "                                  Constraints Report\n",
       "                                &lt;/p&gt;\n",
       "                              &lt;/div&gt;\n",
       "\n",
       "                              &lt;!-- &lt;/div&gt; --&gt;\n",
       "                            &lt;/div&gt;\n",
       "                            &lt;div class=&quot;drift-detection-search-input-wrap display-flex&quot;&gt;\n",
       "                              &lt;div class=&quot;drift-detection-search-input search-input&quot;&gt;\n",
       "                                &lt;input type=&quot;text&quot; id=&quot;wl__feature-search&quot; placeholder=&quot;Quick search...&quot;/&gt;\n",
       "                                &lt;img src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGdSURBVHgBpZK7TkJBEIZnZoVocdTYyQNALxpNKPQBMEaNJsbCRMKhl8ISWwt6AaksCF5iTHgAGhJD4AHkAaAzGiwUsjvOQnO4SYh/cXbPzO43OxcEj9Zy92EiFSXNIfvPyE1kKFfdoxJMENpP6DrvLC0vJoEwCgwto7DWcxoIIHBYbA3NmKwnDltjAeuZhyul1DaTTlfPB6Nt5Z53DOgky4P875+nlctY2+unjZviLklkJhi5bPUa3y/7qJuQUM7PinMy7CdQc1Gh16vnBxPzrMROmlKQEgKNASAHLQCmSIGpS75O+O5pdQAgVXaIqTkNwDDXHmcnW3VmHZoGMLoTsOt88+NrAMCIZdu+iLTyTwKRa1Md6YKfOgXbzO7K8sWku5u5RxcRV5EpPezrzcHGbXEXWaUkgkweZ/UC9YrK3zqggFw5FBZfm8EUavHj7AjAKpIvBDrGn+pNnlcyhYgqbcC41idr1gvB4SdZkDbzQa21gwv0Vj07aPTtL07XdDOyDXohCDNoHIRmAVRie20f+RKybRDQDvxHkXy/7b/DrayncLbMwQAAAABJRU5ErkJggg==&quot;/&gt;\n",
       "                              &lt;/div&gt;\n",
       "                              &lt;div class=&quot;wl__dropdown_arrow-icon&quot;&gt;\n",
       "                                &lt;div onclick=&quot;openFilter()&quot; class=&quot;close-filter-button&quot;&gt;\n",
       "                                &lt;div class=&quot;display-flex close-filter-icon d-none&quot;&gt;\n",
       "                                  &lt;img src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADFSURBVHgBfY+xDcIwEEXvnLQBZQkYAEhDwwKpEEK0CCZgAEjJCEmgjYSAygxAHTZgFRSOsyUjY5mcZFnn/+78PwBXf3+MoKWUPuYjVBPFnTwpr9t/oNJfcTfXsAhRAlDqDhhQIPYgpAqNMDqcUqSAYZT1epr9gAHt6uXshvYme4DYHQJNDKh0dD0m5WXB10Y3Fqjtuh7fROn3oREDWxfeMLyRsMnc0OgDzdduaA0Pi3Plgr7Q2kaAePeBqh6rueSNBVt6fgCwBV1JLF3rlAAAAABJRU5ErkJggg==&quot;/&gt;\n",
       "                                &lt;/div&gt;\n",
       "                                &lt;div class=&quot;display-flex filter-icon&quot;&gt;\n",
       "                                  &lt;img src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAPCAYAAADtc08vAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACSSURBVHgBrZLBCYAwDEWTUjw7igcdwI1cxkNXUJzBEVzFAbSVKoKmaVrEB6GHJv/w+AACtRk7P9IONv1QOYUl96k0zv61m2tjARoLtSDI3EFsgIJ4uoXrMLazO72CRG2mzg/8BSdVlEjhpGZJjAWdAZJECpWalEhJSs1pHuUlMad5FFai1Lwg4Ckx1TxKIPFL8w55mEWd8VjPGAAAAABJRU5ErkJggg==&quot;/&gt;\n",
       "                                &lt;/div&gt;\n",
       "                                &lt;/div&gt;\n",
       "                              &lt;span class=&quot;notif-circle-container&quot;&gt;\n",
       "                                &lt;span class=&quot;notif-circle&quot;&gt;&lt;/span&gt;\n",
       "                              &lt;/span&gt;\n",
       "                            &lt;/div&gt;\n",
       "                            &lt;/div&gt;\n",
       "                          &lt;/div&gt;\n",
       "                        &lt;/div&gt;\n",
       "                        &lt;div class=&quot;dropdown-container flex-direction-colum mb-2 d-none&quot; id=&quot;dropdown-container&quot;&gt;\n",
       "                          &lt;div class=&quot;filter-options&quot;&gt;\n",
       "                            &lt;div class=&quot;filter-options-title space-between dropdown&quot;&gt;\n",
       "                              &lt;p&gt;Select a view:&lt;/p&gt;\n",
       "                            &lt;/div&gt;\n",
       "                            &lt;div class=&quot;form-check mb-1 mt-2&quot;&gt;\n",
       "                              &lt;input\n",
       "                                class=&quot;form-check-input wl__feature-filter-input&quot;\n",
       "                                type=&quot;checkbox&quot;\n",
       "                                name=&quot;checkbox&quot;\n",
       "                                value=&quot;Discrete&quot;\n",
       "                                id=&quot;inferredDiscrete&quot;\n",
       "                                checked\n",
       "                              /&gt;\n",
       "                              &lt;label class=&quot;form-check-label&quot; for=&quot;inferredDiscrete&quot;&gt;\n",
       "                                Failed constraints (&lt;span class=&quot;wl__feature-count--discrete&quot;&gt;&lt;/span&gt;)\n",
       "                              &lt;/label&gt;\n",
       "                            &lt;/div&gt;\n",
       "                            &lt;div class=&quot;form-check mb-1&quot;&gt;\n",
       "                              &lt;input\n",
       "                                class=&quot;form-check-input wl__feature-filter-input&quot;\n",
       "                                type=&quot;checkbox&quot;\n",
       "                                name=&quot;checkbox&quot;\n",
       "                                value=&quot;Non-discrete&quot;\n",
       "                                id=&quot;inferredNonDiscrete&quot;\n",
       "                                checked\n",
       "                              /&gt;\n",
       "                              &lt;label class=&quot;form-check-label&quot; for=&quot;inferredNonDiscrete&quot;&gt;\n",
       "                                Passed constraints (&lt;span\n",
       "                                  class=&quot;wl__feature-count--non-discrete&quot;\n",
       "                                &gt;&lt;/span&gt;)\n",
       "                              &lt;/label&gt;\n",
       "                            &lt;/div&gt;\n",
       "                            &lt;div class=&quot;form-check mb-1&quot;&gt;\n",
       "                              &lt;input\n",
       "                                class=&quot;form-check-input wl__feature-filter-input&quot;\n",
       "                                type=&quot;checkbox&quot;\n",
       "                                name=&quot;checkbox&quot;\n",
       "                                value=&quot;Unknown&quot;\n",
       "                                id=&quot;inferredUnknown&quot;\n",
       "                                checked\n",
       "                              /&gt;\n",
       "                              &lt;label class=&quot;form-check-label&quot; for=&quot;inferredUnknown&quot;&gt;\n",
       "                                All constraints (&lt;span class=&quot;wl__feature-count--unknown&quot;&gt;&lt;/span&gt;)\n",
       "                              &lt;/label&gt;\n",
       "                            &lt;/div&gt;\n",
       "                          &lt;/div&gt;\n",
       "                        &lt;/div&gt;\n",
       "                      &lt;/div&gt;\n",
       "                    &lt;div class=&quot;alert-list&quot; id=&quot;alert-list&quot;&gt;\n",
       "                      {{{alertLIst this}}}\n",
       "                    &lt;/div&gt;\n",
       "                  &lt;/div&gt;\n",
       "                &lt;/div&gt;\n",
       "              &lt;/div&gt;\n",
       "            &lt;/div&gt;\n",
       "          &lt;/div&gt;\n",
       "        &lt;/div&gt;\n",
       "      &lt;/div&gt;\n",
       "      &lt;div class=&quot;no-responsive&quot;&gt;\n",
       "        &lt;div class=&quot;no-responsive__content&quot;&gt;\n",
       "          &lt;h1 class=&quot;no-responsive__title&quot;&gt;Hold on! :)&lt;/h1&gt;\n",
       "          &lt;p class=&quot;no-responsive__text&quot;&gt;\n",
       "            It looks like your current screen size or device is not yet supported by the WhyLabs Sandbox. The Sandbox is\n",
       "            best experienced on a desktop computer. Please try maximizing this window or switching to another device. We\n",
       "            are working on adding support for a larger variety of devices.\n",
       "          &lt;/p&gt;\n",
       "        &lt;/div&gt;\n",
       "      &lt;/div&gt;\n",
       "    \n",
       "  &lt;/script&gt;\n",
       "\n",
       "  &lt;script src=&quot;https://code.jquery.com/jquery-3.6.0.min.js&quot; integrity=&quot;sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt;\n",
       "\n",
       "  &lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js&quot; integrity=&quot;sha512-cd6CHE+XWDQ33ElJqsi0MdNte3S+bQY819f7p3NUHgwQQLXSKjE4cPZTeGNI+vaxZynk1wVU3hoHmow3m089wA==&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/script&gt;\n",
       "\n",
       "  &lt;script&gt;\n",
       "    function registerHandlebarHelperFunctions() {\n",
       "      //helper fun\n",
       "      function formatLabelDate(timestamp) {\n",
       "        const date = new Date(timestamp);\n",
       "        const format = d3.timeFormat(&quot;%Y-%m-%d %I:%M:%S %p %Z&quot;);\n",
       "        return format(date);\n",
       "      }\n",
       "\n",
       "      function fixNumberTo(number, decimals = 3) {\n",
       "        return parseFloat(number).toFixed(decimals);\n",
       "      }\n",
       "\n",
       "      const randomNumbers = (range) =&gt; Math.floor(Math.random() * range)\n",
       "\n",
       "      const findFetureWithNumberSummary = (column) =&gt; {\n",
       "        const fetureIndex = Object.values(column.columns)\n",
       "              .findIndex((feture) =&gt; feture.numberSummary)\n",
       "\n",
       "        return Object.keys(column.columns)[fetureIndex]\n",
       "      }\n",
       "\n",
       "      const alertListItemStatus = (status, passedItem, failedItem) =&gt; {\n",
       "        if (status) {\n",
       "          return passedItem\n",
       "        } else {\n",
       "          return failedItem\n",
       "        }\n",
       "      }\n",
       "\n",
       "      const alertListElement = (name, text, status, summary) =&gt; {\n",
       "        if (summary == null){\n",
       "          return (\n",
       "          `&lt;div\n",
       "             data-inferred-type=${alertListItemStatus(status, &quot;passed&quot;, &quot;failed&quot;)}\n",
       "             class=&quot;alert-list-item display-flex justify-content-space-between align-items-center mb-2&quot;\n",
       "           &gt;\n",
       "            &lt;div class=&quot;alert-list-text&quot;&gt;\n",
       "              ${\n",
       "                name &amp;&amp;\n",
       "                alertListItemStatus(\n",
       "                  status,\n",
       "                  `&lt;mark class=&quot;blue-mark&quot;&gt;${name}&lt;/mark&gt;`,\n",
       "                  `&lt;mark class=&quot;red-mark&quot;&gt;${name}&lt;/mark&gt;`\n",
       "                )\n",
       "              }\n",
       "              ${text}\n",
       "            &lt;/div&gt;\n",
       "              ${\n",
       "                alertListItemStatus(\n",
       "                  status,\n",
       "                  `\n",
       "                  &lt;div class=&quot;turquoise-background-color alert-tag&quot;&gt;Passed&lt;/div&gt;\n",
       "                  `\n",
       "                  ,\n",
       "                  `\n",
       "                  &lt;div class=&quot;bordeaux-background-color alert-tag&quot;&gt;Failed&lt;/div&gt;\n",
       "                  `\n",
       "                )\n",
       "              }\n",
       "              &lt;/div&gt;`\n",
       "        )\n",
       "\n",
       "        }\n",
       "        return (\n",
       "          `&lt;div\n",
       "             data-inferred-type=${alertListItemStatus(status, &quot;passed&quot;, &quot;failed&quot;)}\n",
       "             class=&quot;alert-list-item display-flex justify-content-space-between align-items-center mb-2&quot;\n",
       "           &gt;\n",
       "            &lt;div class=&quot;alert-list-text&quot;&gt;\n",
       "              ${\n",
       "                name &amp;&amp;\n",
       "                alertListItemStatus(\n",
       "                  status,\n",
       "                  `&lt;mark class=&quot;blue-mark&quot;&gt;${name}&lt;/mark&gt;`,\n",
       "                  `&lt;mark class=&quot;red-mark&quot;&gt;${name}&lt;/mark&gt;`\n",
       "                )\n",
       "              }\n",
       "              ${text}\n",
       "            &lt;/div&gt;\n",
       "              ${\n",
       "                alertListItemStatus(\n",
       "                  status,\n",
       "                  `\n",
       "                  &lt;div class=&quot;tooltip-full-number&quot;&gt;\n",
       "                    &lt;div class=&quot;turquoise-background-color alert-tag&quot;&gt;Passed\n",
       "                      &lt;span class=&quot;tooltiptext&quot;&gt;\n",
       "                          &lt;pre class=&quot;mb-1&quot;&gt; ${JSON.stringify(summary, null, 2)} &lt;/pre&gt;\n",
       "                        &lt;/span&gt;\n",
       "                    &lt;/div&gt;\n",
       "                  &lt;/div&gt;`\n",
       "\n",
       "                  ,\n",
       "                  `\n",
       "                  &lt;div class=&quot;tooltip-full-number&quot;&gt;\n",
       "                    &lt;div class=&quot;bordeaux-background-color alert-tag&quot;&gt;Failed\n",
       "                      &lt;span class=&quot;tooltiptext&quot;&gt;\n",
       "                        &lt;pre class=&quot;mb-1&quot;&gt; ${JSON.stringify(summary, null, 2)} &lt;/pre&gt;\n",
       "                        &lt;/span&gt;\n",
       "                    &lt;/div&gt;\n",
       "                  &lt;/div&gt;`\n",
       "                )\n",
       "              }\n",
       "              &lt;/div&gt;`\n",
       "        )\n",
       "      }\n",
       "\n",
       "      let failedConstraints = 0;\n",
       "\n",
       "      Handlebars.registerHelper(&quot;getProfileTimeStamp&quot;, function (column) {\n",
       "        return formatLabelDate(+column.properties.dataTimestamp)\n",
       "      });\n",
       "\n",
       "      Handlebars.registerHelper(&quot;getProfileName&quot;, function (column) {\n",
       "        return column.properties.tags.name\n",
       "      });\n",
       "\n",
       "\n",
       "      Handlebars.registerHelper(&quot;alertLIst&quot;, function (column) {\n",
       "        let alertListItem = column.map((value) =&gt; {\n",
       "          if (value[1][0]) {\n",
       "            let alertListValue = value[1].map((cstr)=&gt;{\n",
       "              return alertListElement(value[0],cstr[0],cstr[cstr.length - 1] === 0 || (failedConstraints++, false), value[3])\n",
       "            })\n",
       "            return alertListValue.join(&#x27; &#x27;)\n",
       "          } else {\n",
       "            return alertListElement(&#x27;&#x27;, value[0], value[2] === 0 ||  (failedConstraints++, false), value[3])\n",
       "          }\n",
       "        })\n",
       "         $(document).ready(() =&gt; {\n",
       "           $(&quot;.wl__feature-count--discrete&quot;).append(failedConstraints)\n",
       "           $(&quot;.wl__feature-count--non-discrete&quot;).append(column.length - failedConstraints)\n",
       "           $(&quot;.wl__feature-count--unknown&quot;).append(column.length)\n",
       "         })\n",
       "        return alertListItem.join(&#x27; &#x27;)\n",
       "      });\n",
       "\n",
       "    }\n",
       "\n",
       "    function openFilter() {\n",
       "      const $filterOptions = $(&quot;.dropdown-container&quot;);\n",
       "      const filterClass = $filterOptions.attr(&quot;class&quot;);\n",
       "\n",
       "      if (filterClass.indexOf(&quot;d-none&quot;) &gt; 0) {\n",
       "        $filterOptions.removeClass(&quot;d-none&quot;);\n",
       "        $(&quot;.filter-icon&quot;).addClass(&quot;d-none&quot;)\n",
       "        $(&quot;.close-filter-icon&quot;).removeClass(&quot;d-none&quot;)\n",
       "      } else {\n",
       "        $filterOptions.addClass(&quot;d-none&quot;);\n",
       "        $(&quot;.close-filter-icon&quot;).addClass(&quot;d-none&quot;)\n",
       "        $(&quot;.filter-icon&quot;).removeClass(&quot;d-none&quot;)\n",
       "      }\n",
       "    }\n",
       "\n",
       "    function initHandlebarsTemplate() {\n",
       "      // Replace this context with JSON from .py file\n",
       "      const context = [[&quot;subscription_date meets condition Y-m-d format&quot;, 0, 1, {&quot;metric&quot;: &quot;condition_count&quot;, &quot;total&quot;: 3, &quot;Y-m-d format&quot;: 1}], [&quot;url never meets condition url_matches_domain&quot;, 0, 1, {&quot;metric&quot;: &quot;condition_count&quot;, &quot;total&quot;: 3, &quot;url_matches_domain&quot;: 1}], [&quot;age.integer_zeros lower than or equal to 1&quot;, 1, 0, {&quot;metric&quot;: &quot;condition_count&quot;, &quot;total&quot;: 3, &quot;integer_zeros&quot;: 1}]];\n",
       "      // Config handlebars and pass data to HBS template\n",
       "      const source = document.getElementById(&quot;entry-template&quot;).innerHTML;\n",
       "      const template = Handlebars.compile(source);\n",
       "      const html = template(context);\n",
       "      const target = document.getElementById(&quot;generated-html&quot;);\n",
       "      target.innerHTML = html;\n",
       "    }\n",
       "\n",
       "    function initWebsiteScripts() {\n",
       "      const $featureSearch = document.getElementById(&quot;wl__feature-search&quot;);\n",
       "      const $alertList = document.getElementById(&quot;alert-list&quot;);\n",
       "      const $discrete = document.getElementById(&quot;inferredDiscrete&quot;);\n",
       "      const $nonDiscrete = document.getElementById(&quot;inferredNonDiscrete&quot;);\n",
       "      const $unknown = document.getElementById(&quot;inferredUnknown&quot;);\n",
       "\n",
       "      const activeTypes = {\n",
       "        passed: true,\n",
       "        failed: true\n",
       "      };\n",
       "\n",
       "      let searchString = &quot;&quot;;\n",
       "\n",
       "      function debounce(func, wait, immediate) {\n",
       "        let timeout;\n",
       "\n",
       "        return function () {\n",
       "          const context = this;\n",
       "          const args = arguments;\n",
       "          const later = function () {\n",
       "            timeout = null;\n",
       "            if (!immediate) func.apply(context, args);\n",
       "          };\n",
       "\n",
       "          const callNow = immediate &amp;&amp; !timeout;\n",
       "          clearTimeout(timeout);\n",
       "          timeout = setTimeout(later, wait);\n",
       "          if (callNow) func.apply(context, args);\n",
       "        };\n",
       "      }\n",
       "\n",
       "      function filterNotification() {\n",
       "        const $notifCircleContainer = $(&quot;.notif-circle-container&quot;)\n",
       "        const $boxes = $(&#x27;.wl_filter-options&gt;.form-check&gt;input[name=checkbox]:checked&#x27;);\n",
       "        const item = Object.values($boxes).find(function(value) { return $(value)[0] === undefined});\n",
       "        if (item === undefined) {\n",
       "          $notifCircleContainer.removeClass(&quot;d-none&quot;)\n",
       "        } else {\n",
       "          $notifCircleContainer.addClass(&quot;d-none&quot;)\n",
       "        }\n",
       "      }\n",
       "\n",
       "      function handleSearch() {\n",
       "        const tableBodyChildren = $alertList.children;\n",
       "\n",
       "        for (let i = 0; i &lt; tableBodyChildren.length; i++) {\n",
       "          const type = tableBodyChildren[i].dataset.inferredType.toLowerCase();\n",
       "          const name = $(tableBodyChildren[i].children[0]).html().toLowerCase();\n",
       "          if (activeTypes[type] &amp;&amp; name.includes(searchString)) {\n",
       "            tableBodyChildren[i].style.display = &quot;&quot;;\n",
       "          } else {\n",
       "            tableBodyChildren[i].style.display = &quot;none&quot;;\n",
       "          }\n",
       "        }\n",
       "      }\n",
       "\n",
       "      const checkedBoxes = () =&gt; {\n",
       "        if ($(&#x27;.form-check-input:checked&#x27;).length === $(&quot;.form-check-input&quot;).length - 1) {\n",
       "          $($(&quot;.form-check-input&quot;)[$(&quot;.form-check-input&quot;).length - 1]).prop( &quot;checked&quot;, true );\n",
       "        }\n",
       "      }\n",
       "\n",
       "      $featureSearch.addEventListener(\n",
       "        &quot;keyup&quot;,\n",
       "        debounce((event) =&gt; {\n",
       "          searchString = event.target.value.toLowerCase();\n",
       "          handleSearch();\n",
       "        }, 100),\n",
       "      );\n",
       "\n",
       "      $discrete.addEventListener(&quot;change&quot;, (event) =&gt; {\n",
       "        const currentCheckbox = $(event.currentTarget);\n",
       "\n",
       "        if (event.currentTarget.checked) {\n",
       "          activeTypes[&quot;failed&quot;] = true;\n",
       "          checkedBoxes();\n",
       "        } else {\n",
       "          activeTypes[&quot;failed&quot;] = false;\n",
       "          $($(&quot;.form-check-input&quot;)[$(&quot;.form-check-input&quot;).length - 1]).prop( &quot;checked&quot;, false );\n",
       "        }\n",
       "        handleSearch();\n",
       "      });\n",
       "\n",
       "      $nonDiscrete.addEventListener(&quot;change&quot;, (event) =&gt; {\n",
       "        const currentCheckbox = $(event.currentTarget);\n",
       "\n",
       "        if (event.currentTarget.checked) {\n",
       "          activeTypes[&quot;passed&quot;] = true;\n",
       "          checkedBoxes();\n",
       "        } else {\n",
       "          activeTypes[&quot;passed&quot;] = false;\n",
       "          $($(&quot;.form-check-input&quot;)[$(&quot;.form-check-input&quot;).length - 1]).prop( &quot;checked&quot;, false );\n",
       "        }\n",
       "        handleSearch();\n",
       "      });\n",
       "\n",
       "      $unknown.addEventListener(&quot;change&quot;, (event) =&gt; {\n",
       "        const currentCheckbox = $(event.currentTarget);\n",
       "\n",
       "        if (event.currentTarget.checked) {\n",
       "          $(&quot;.form-check-input&quot;).prop( &quot;checked&quot;, true );\n",
       "          activeTypes[&quot;passed&quot;] = true;\n",
       "          activeTypes[&quot;failed&quot;] = true;\n",
       "          checkedBoxes();\n",
       "        } else {\n",
       "          $(&quot;.form-check-input&quot;).prop( &quot;checked&quot;, false );\n",
       "          activeTypes[&quot;passed&quot;] = false;\n",
       "          activeTypes[&quot;failed&quot;] = false;\n",
       "        }\n",
       "        handleSearch();\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function checkedBoxes() {\n",
       "      const $boxes = $(&#x27;input[name=checkbox]:checked&#x27;);\n",
       "      const $notifCircleContainer = $(&quot;.notif-circle-container&quot;)\n",
       "\n",
       "      if ($boxes.length) {\n",
       "        $notifCircleContainer.removeClass(&quot;d-none&quot;)\n",
       "      }\n",
       "    }\n",
       "\n",
       "    function openFilter() {\n",
       "      const $filterOptions = $(&quot;.dropdown-container&quot;);\n",
       "      const $notifCircleContainer = $(&quot;.notif-circle-container&quot;)\n",
       "      const filterClass = $filterOptions.attr(&quot;class&quot;);\n",
       "\n",
       "      if (filterClass.indexOf(&quot;d-none&quot;) &gt; 0) {\n",
       "        $notifCircleContainer.addClass(&quot;d-none&quot;)\n",
       "        $filterOptions.removeClass(&quot;d-none&quot;);\n",
       "        $(&quot;.filter-icon&quot;).addClass(&quot;d-none&quot;)\n",
       "        $(&quot;.close-filter-icon&quot;).removeClass(&quot;d-none&quot;)\n",
       "      } else {\n",
       "        $filterOptions.addClass(&quot;d-none&quot;);\n",
       "        $(&quot;.close-filter-icon&quot;).addClass(&quot;d-none&quot;)\n",
       "        $(&quot;.filter-icon&quot;).removeClass(&quot;d-none&quot;)\n",
       "        checkedBoxes()\n",
       "      }\n",
       "    }\n",
       "\n",
       "    // Invoke functions -- keep in mind invokation order\n",
       "    registerHandlebarHelperFunctions();\n",
       "    initHandlebarsTemplate();\n",
       "    initWebsiteScripts();\n",
       "  &lt;/script&gt;\n",
       "&lt;/html&gt;\n",
       "\" width=100% height=300\n",
       "        frameBorder=0></iframe>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from whylogs.viz import NotebookProfileVisualizer\n",
    "\n",
    "visualization = NotebookProfileVisualizer()\n",
    "visualization.constraints_report(constraints, cell_height=300)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By hovering on the status, you can view the number of times the condition failed, and the total number of times the condition was checked."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "5dd5901cadfd4b29c2aaf95ecd29c0c3b10829ad94dcfe59437dbee391154aea"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}