Data Checks#
EvalML provides data checks to help guide you in achieving the highest performing model. These utility functions help deal with problems such as overfitting, abnormal data, and missing data. These data checks can be found under evalml/data_checks
. Below we will cover examples for each available data check in EvalML, as well as the DefaultDataChecks
collection of data checks.
Missing Data#
Missing data or rows with NaN
values provide many challenges for machine learning pipelines. In the worst case, many algorithms simply will not run with missing data! EvalML pipelines contain imputation components to ensure that doesn’t happen. Imputation works by approximating missing values with existing values. However, if a column contains a high number of missing values, a large percentage of the column would be approximated by a small percentage.
This could potentially create a column without useful information for machine learning pipelines. By using NullDataCheck
, EvalML will alert you to this potential problem by returning the columns that pass the missing values threshold.
[1]:
import numpy as np
import pandas as pd
from evalml.data_checks import NullDataCheck
X = pd.DataFrame(
[[1, 2, 3], [0, 4, np.nan], [1, 4, np.nan], [9, 4, np.nan], [8, 6, np.nan]]
)
null_check = NullDataCheck(pct_null_col_threshold=0.8, pct_null_row_threshold=0.8)
messages = null_check.validate(X)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_clustering.py:35: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _pt_shuffle_rec(i, indexes, index_mask, partition_tree, M, pos):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_clustering.py:54: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def delta_minimization_order(all_masks, max_swap_size=100, num_passes=2):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_clustering.py:63: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _reverse_window(order, start, length):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_clustering.py:69: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _reverse_window_score_gain(masks, order, start, length):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_clustering.py:77: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _mask_delta_score(m1, m2):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/links.py:5: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def identity(x):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/links.py:10: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _identity_inverse(x):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/links.py:15: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def logit(x):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/links.py:20: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _logit_inverse(x):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_masked_model.py:363: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _build_fixed_single_output(averaged_outs, last_outs, outputs, batch_positions, varying_rows, num_varying_rows, link, linearizing_weights):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_masked_model.py:385: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _build_fixed_multi_output(averaged_outs, last_outs, outputs, batch_positions, varying_rows, num_varying_rows, link, linearizing_weights):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_masked_model.py:428: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _init_masks(cluster_matrix, M, indices_row_pos, indptr):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/utils/_masked_model.py:439: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _rec_fill_masks(cluster_matrix, indices_row_pos, indptr, indices, M, ind):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/maskers/_tabular.py:186: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _single_delta_mask(dind, masked_inputs, last_mask, data, x, noop_code):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/maskers/_tabular.py:197: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _delta_masking(masks, x, curr_delta_inds, varying_rows_out,
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/maskers/_image.py:175: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def _jit_build_partition_tree(xmin, xmax, ymin, ymax, zmin, zmax, total_ywidth, total_zwidth, M, clustering, q):
/home/docs/checkouts/readthedocs.org/user_builds/feature-labs-inc-evalml/envs/v0.77.0/lib/python3.8/site-packages/shap/explainers/_partition.py:676: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
def lower_credit(i, value, M, values, clustering):
The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
Warning: Column(s) '2' are 80.0% or more null
Abnormal Data#
EvalML provides a few data checks to check for abnormal data:
NoVarianceDataCheck
ClassImbalanceDataCheck
TargetLeakageDataCheck
InvalidTargetDataCheck
IDColumnsDataCheck
OutliersDataCheck
HighVarianceCVDataCheck
MulticollinearityDataCheck
UniquenessDataCheck
TargetDistributionDataCheck
DateTimeFormatDataCheck
TimeSeriesParametersDataCheck
TimeSeriesSplittingDataCheck
Zero Variance#
Data with zero variance indicates that all values are identical. If a feature has zero variance, it is not likely to be a useful feature. Similarly, if the target has zero variance, there is likely something wrong. NoVarianceDataCheck
checks if the target or any feature has only one unique value and alerts you to any such columns.
[2]:
from evalml.data_checks import NoVarianceDataCheck
X = pd.DataFrame({"no var col": [0, 0, 0], "good col": [0, 4, 1]})
y = pd.Series([1, 0, 1])
no_variance_data_check = NoVarianceDataCheck()
messages = no_variance_data_check.validate(X, y)
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
Warning: 'no var col' has 1 unique value.
Note that you can set NaN
to count as an unique value, but NoVarianceDataCheck
will still return a warning if there is only one unique non-NaN
value in a given column.
[3]:
from evalml.data_checks import NoVarianceDataCheck
X = pd.DataFrame(
{
"no var col": [0, 0, 0],
"no var col with nan": [1, np.nan, 1],
"good col": [0, 4, 1],
}
)
y = pd.Series([1, 0, 1])
no_variance_data_check = NoVarianceDataCheck(count_nan_as_value=True)
messages = no_variance_data_check.validate(X, y)
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
Warning: 'no var col' has 1 unique value.
Warning: 'no var col with nan' has two unique values including nulls. Consider encoding the nulls for this column to be useful for machine learning.
Class Imbalance#
For classification problems, the distribution of examples across each class can vary. For small variations, this is normal and expected. However, when the number of examples for each class label is disproportionately biased or skewed towards a particular class (or classes), it can be difficult for machine learning models to predict well. In addition, having a low number of examples for a given class could mean that one or more of the CV folds generated for the training data could only have few or no examples from that class. This may cause the model to only predict the majority class and ultimately resulting in a poor-performant model.
ClassImbalanceDataCheck
checks if the target labels are imbalanced beyond a specified threshold for a certain number of CV folds. It returns DataCheckError
messages for any classes that have less samples than double the number of CV folds specified (since that indicates the likelihood of having at little to no samples of that class in a given fold), and DataCheckWarning
messages for any classes that fall below the set threshold percentage.
[4]:
from evalml.data_checks import ClassImbalanceDataCheck
X = pd.DataFrame([[1, 2, 0, 1], [4, 1, 9, 0], [4, 4, 8, 3], [9, 2, 7, 1]])
y = pd.Series([0, 1, 1, 1, 1])
class_imbalance_check = ClassImbalanceDataCheck(threshold=0.25, num_cv_folds=4)
messages = class_imbalance_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: The following labels fall below 25% of the target: [0]
Warning: The following labels in the target have severe class imbalance because they fall under 25% of the target and have less than 100 samples: [0]
Error: The number of instances of these targets is less than 2 * the number of cross folds = 8 instances: [0, 1]
Target Leakage#
Target leakage, also known as data leakage, can occur when you train your model on a dataset that includes information that should not be available at the time of prediction. This causes the model to score suspiciously well, but perform poorly in production. TargetLeakageDataCheck
checks for features that could potentially be “leaking”
information by calculating the Pearson correlation coefficient between each feature and the target to warn users if there are features are highly correlated with the target. Currently, only numerical features are considered.
[5]:
from evalml.data_checks import TargetLeakageDataCheck
X = pd.DataFrame(
{"leak": [10, 42, 31, 51, 61], "x": [42, 54, 12, 64, 12], "y": [12, 5, 13, 74, 24]}
)
y = pd.Series([10, 42, 31, 51, 40])
target_leakage_check = TargetLeakageDataCheck(pct_corr_threshold=0.8)
messages = target_leakage_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
One or more pairs of columns did not share enough rows of non-null data to measure the relationship. The measurement for these columns will be NaN. Use 'extra_stats=True' to get the shared rows for each pair of columns.
One or more pairs of columns did not share enough rows of non-null data to measure the relationship. The measurement for these columns will be NaN. Use 'extra_stats=True' to get the shared rows for each pair of columns.
Invalid Target Data#
The InvalidTargetDataCheck
checks if the target data contains any missing or invalid values. Specifically:
if any of the target values are missing, a
DataCheckError
message is returnedif the specified problem type is a binary classification problem but there is more or less than two unique values in the target, a
DataCheckError
message is returnedif binary classification target classes are numeric values not equal to {0, 1}, a
DataCheckError
message is returned because it can cause unpredictable behavior when passed to pipelines
[6]:
from evalml.data_checks import InvalidTargetDataCheck
X = pd.DataFrame({})
y = pd.Series([0, 1, None, None])
invalid_target_check = InvalidTargetDataCheck("binary", "Log Loss Binary")
messages = invalid_target_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: Input target and features have different lengths
Warning: Input target and features have mismatched indices. Details will include the first 10 mismatched indices.
Error: 2 row(s) (50.0%) of target values are null
ID Columns#
ID columns in your dataset provide little to no benefit to a machine learning pipeline as the pipeline cannot extrapolate useful information from unique identifiers. Thus, IDColumnsDataCheck
reminds you if these columns exists. In the given example, ‘user_number’ and ‘revenue_id’ columns are both identified as potentially being unique identifiers that should be removed.
[7]:
from evalml.data_checks import IDColumnsDataCheck
X = pd.DataFrame(
[[0, 53, 6325, 5], [1, 90, 6325, 10], [2, 90, 18, 20]],
columns=["user_number", "cost", "revenue", "revenue_id"],
)
id_col_check = IDColumnsDataCheck(id_threshold=0.9)
messages = id_col_check.validate(X)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: Columns 'user_number', 'revenue_id' are 90.0% or more likely to be an ID column
Primary key columns however, can be useful. Primary key columns are typically the first column in the dataset, have all unique values, and are either named ID
or a name that ends with _id
. Though they are ignored from the modeling process, they can be used as an identifier to query on before or after the modeling process. IDColumnsDataCheck
will also remind you if it finds that the first column of the DataFrame is a primary key. In the given example, user_id
is identified as a
primary key, while revenue_id
was identified as a regular unique identifier.
[8]:
from evalml.data_checks import IDColumnsDataCheck
X = pd.DataFrame(
[[0, 53, 6325, 5], [1, 90, 6325, 10], [2, 90, 18, 20]],
columns=["user_id", "cost", "revenue", "revenue_id"],
)
id_col_check = IDColumnsDataCheck(id_threshold=0.9)
messages = id_col_check.validate(X)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: The first column 'user_id' is likely to be the primary key
Warning: Columns 'revenue_id' are 90.0% or more likely to be an ID column
Multicollinearity#
The MulticollinearityDataCheck
data check is used in to detect if are any set of features that are likely to be multicollinear. Multicollinear features affect the performance of a model, but more importantly, it may greatly impact model interpretation. EvalML uses mutual information to determine collinearity.
[9]:
from evalml.data_checks import MulticollinearityDataCheck
y = pd.Series([1, 0, 2, 3, 4])
X = pd.DataFrame(
{
"col_1": y,
"col_2": y * 3,
"col_3": ~y,
"col_4": y / 2,
"col_5": y + 1,
"not_collinear": [0, 1, 0, 0, 0],
}
)
multi_check = MulticollinearityDataCheck(threshold=0.95)
messages = multi_check.validate(X)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
One or more pairs of columns did not share enough rows of non-null data to measure the relationship. The measurement for these columns will be NaN. Use 'extra_stats=True' to get the shared rows for each pair of columns.
Uniqueness#
The UniquenessDataCheck
is used to detect columns with either too unique or not unique enough values. For regression type problems, the data is checked for a lower limit of uniqueness. For multiclass type problems, the data is checked for an upper limit.
[10]:
import pandas as pd
from evalml.data_checks import UniquenessDataCheck
X = pd.DataFrame(
{
"most_unique": [float(x) for x in range(10)], # [0,1,2,3,4,5,6,7,8,9]
"more_unique": [x % 5 for x in range(10)], # [0,1,2,3,4,0,1,2,3,4]
"unique": [x % 3 for x in range(10)], # [0,1,2,0,1,2,0,1,2,0]
"less_unique": [x % 2 for x in range(10)], # [0,1,0,1,0,1,0,1,0,1]
"not_unique": [float(1) for x in range(10)],
}
) # [1,1,1,1,1,1,1,1,1,1]
uniqueness_check = UniquenessDataCheck(problem_type="regression", threshold=0.5)
messages = uniqueness_check.validate(X)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: Input columns 'not_unique' for regression problem type are not unique enough.
Sparsity#
The SparsityDataCheck
is used to identify features that contain a sparsity of values.
[11]:
from evalml.data_checks import SparsityDataCheck
X = pd.DataFrame(
{
"most_sparse": [float(x) for x in range(10)], # [0,1,2,3,4,5,6,7,8,9]
"more_sparse": [x % 5 for x in range(10)], # [0,1,2,3,4,0,1,2,3,4]
"sparse": [x % 3 for x in range(10)], # [0,1,2,0,1,2,0,1,2,0]
"less_sparse": [x % 2 for x in range(10)], # [0,1,0,1,0,1,0,1,0,1]
"not_sparse": [float(1) for x in range(10)],
}
) # [1,1,1,1,1,1,1,1,1,1]
sparsity_check = SparsityDataCheck(
problem_type="multiclass", threshold=0.4, unique_count_threshold=3
)
messages = sparsity_check.validate(X)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: Input columns ('most_sparse', 'more_sparse', 'sparse') for multiclass problem type are too sparse.
Outliers#
Outliers are observations that differ significantly from other observations in the same sample. Many machine learning pipelines suffer in performance if outliers are not dropped from the training set as they are not representative of the data. OutliersDataCheck()
uses IQR to notify you if a sample can be considered an outlier.
Below we generate a random dataset with some outliers.
[12]:
data = np.tile(np.arange(10) * 0.01, (100, 10))
X = pd.DataFrame(data=data)
# generate some outliers in columns 3, 25, 55, and 72
X.iloc[0, 3] = -10000
X.iloc[3, 25] = 10000
X.iloc[5, 55] = 10000
X.iloc[10, 72] = -10000
We then utilize OutliersDataCheck()
to rediscover these outliers.
[13]:
from evalml.data_checks import OutliersDataCheck
outliers_check = OutliersDataCheck()
messages = outliers_check.validate(X)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: Column(s) '3', '25', '55', '72' are likely to have outlier data.
Target Distribution#
Target data can come in a variety of distributions, such as Gaussian or Lognormal. When we work with machine learning models, we feed data into an estimator that learns from the training data provided. Sometimes the data can be significantly spread out with a long tail or outliers, which could lead to a lognormal distribution. This can cause machine learning model performance to suffer.
To help the estimators better understand the underlying relationships in the data between the features and the target, we can use the TargetDistributionDataCheck
to identify such a distribution.
[14]:
from scipy.stats import lognorm
from evalml.data_checks import TargetDistributionDataCheck
data = np.tile(np.arange(10) * 0.01, (100, 10))
X = pd.DataFrame(data=data)
y = pd.Series(lognorm.rvs(s=0.4, loc=1, scale=1, size=100))
target_dist_check = TargetDistributionDataCheck()
messages = target_dist_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Warning: Target may have a lognormal distribution.
Datetime Format#
Datetime information is a necessary component of time series problems, but sometimes the data we deal with may contain flaws that make it impossible for time series models to work with them. For example, in order to identify a frequency in the datetime information there has to be equal interval spacing between data points i.e. January 1, 2021, January 3, 2021, January 5, 2021, …etc which are separated by two days. If instead there are random jumps in the datetime data i.e. January 1, 2021, January 3, 2021, January 12, 2021, then a frequency can’t be inferred. Another common issue with time series models are that they can’t handle datetime information that isn’t properly sorted. Datetime values that aren’t monotonically increasing (sorted in ascending order) will encounter this issue and their frequency cannot be inferred.
To make it easy to verify that the datetime column you’re working with is properly spaced and sorted, we can leverage the DatetimeFormatDataCheck
. When initializing the data check, pass in the name of the column that contains your datetime information (or pass in “index” if it’s found in either your X or y indices).
[15]:
from evalml.data_checks import DateTimeFormatDataCheck
X = pd.DataFrame(
pd.date_range("January 1, 2021", periods=8, freq="2D"), columns=["dates"]
)
y = pd.Series([1, 2, 4, 2, 1, 2, 3, 1])
# Replaces the last entry with January 16th instead of January 15th
# so that the data is no longer evenly spaced.
X.iloc[7] = "January 16, 2021"
datetime_format_check = DateTimeFormatDataCheck(datetime_column="dates")
messages = datetime_format_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
print("--------------------------------")
# Reverses the order of the index datetime values to be decreasing.
X = X[::-1]
messages = datetime_format_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Error: Column 'dates' has datetime values that do not align with the inferred frequency.
Error: A frequency was detected in column 'dates', but there are faulty datetime values that need to be addressed.
--------------------------------
Error: Datetime values must be sorted in ascending order.
Error: No frequency could be detected in column 'dates', possibly due to uneven intervals or too many duplicate/missing values.
Time Series Parameters#
In order to support time series problem types in AutoML, certain conditions have to be met. - The parameters gap
, max_delay
, forecast_horizon
, and time_index
have to be passed in to problem_configuration
. - The values of gap
, max_delay
, forecast_horizon
have to be appropriate for the size of the data.
For point 2 above, this means that the window size (as defined by gap
+ max_delay
+ forecast_horizon
) has to be less than the number of observations in the data divided by the number of splits + 1. For example, with 100 observations and 3 splits, the split size would be 25. This means that the window size has to be less than 25.
[16]:
from evalml.data_checks import TimeSeriesParametersDataCheck
X = pd.DataFrame(pd.date_range("1/1/21", periods=100), columns=["dates"])
y = pd.Series([i % 2 for i in range(100)])
problem_config = {
"gap": 1,
"max_delay": 23,
"forecast_horizon": 1,
"time_index": "dates",
}
# With 3 splits, the split size will be 25 (100/3+1)
# Since gap + max_delay + forecast_horizon is 25, this will
# throw an error for window size.
ts_params_data_check = TimeSeriesParametersDataCheck(
problem_configuration=problem_config, n_splits=3
)
messages = ts_params_data_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Time Series Splitting#
Due to the nature of time series data, splitting cannot involve shuffling and has to be done in a sequential manner. This means splitting the data into n_splits
+ 1 different sections and increasing the size of the training data by the split size every iteration while keeping the test size equal to the split size.
For every split in the data, the training and validation segments must contain target data that has an example of every class found in the entire target set for time series binary and time series multiclass problems. The reason for this is that many classification machine learning models run into issues if they’re trained on data that doesn’t contain an instance of a class but then the model is expected to be able to predict for it. For example, with 3 splits and a split size of 25, this means that every training/validation split: (0:25)/(25:50), (0:50)/(50:75), (0:75)/(75:100) must contain at least one instance of all unique target classes in the training and validation set. - At least one instance of both classes in a time series binary problem. - At least one instance of all classes in a time series multiclass problem.
[17]:
from evalml.data_checks import TimeSeriesSplittingDataCheck
X = None
y = pd.Series([0 if i < 50 else i % 2 for i in range(100)])
ts_splitting_check = TimeSeriesSplittingDataCheck("time series binary", 3)
messages = ts_splitting_check.validate(X, y)
errors = [message for message in messages if message["level"] == "error"]
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
for error in errors:
print("Error:", error["message"])
Error: Time Series Binary and Time Series Multiclass problem types require every training and validation split to have at least one instance of all the target classes. The following splits are invalid: [1, 2]
Data Check Messages#
Each data check’s validate
method returns a list of DataCheckMessage
objects indicating warnings or errors found; warnings are stored as a DataCheckWarning
object and errors are stored as a DataCheckError
object. You can filter the messages returned by a data check by checking for the type of message returned. Below,
NoVarianceDataCheck
returns a list containing a DataCheckWarning
and a DataCheckError
message. We can determine which is which by checking the type of each message.
[18]:
from evalml.data_checks import NoVarianceDataCheck, DataCheckWarning
X = pd.DataFrame(
{
"no var col": [0, 0, 0],
"no var col with nan": [1, np.nan, 1],
"good col": [0, 4, 1],
}
)
y = pd.Series([1, 0, 1])
no_variance_data_check = NoVarianceDataCheck(count_nan_as_value=True)
messages = no_variance_data_check.validate(X, y)
warnings = [message for message in messages if message["level"] == "warning"]
for warning in warnings:
print("Warning:", warning["message"])
Warning: 'no var col' has 1 unique value.
Warning: 'no var col with nan' has two unique values including nulls. Consider encoding the nulls for this column to be useful for machine learning.
Writing Your Own Data Check#
If you would prefer to write your own data check, you can do so by extending the DataCheck
class and implementing the validate(self, X, y)
class method. Below, we’ve created a new DataCheck
, ZeroVarianceDataCheck
, which is similar to NoVarianceDataCheck
defined in EvalML
. The validate(self, X, y)
method should return a dictionary with ‘warnings’ and ‘errors’ as keys mapping to list of warnings and errors, respectively.
[19]:
from evalml.data_checks import DataCheck
class ZeroVarianceDataCheck(DataCheck):
def validate(self, X, y):
messages = []
if not isinstance(X, pd.DataFrame):
X = pd.DataFrame(X)
warning_msg = "Column '{}' has zero variance"
messages.extend(
[
DataCheckError(warning_msg.format(column), self.name)
for column in X.columns
if len(X[column].unique()) == 1
]
)
return messages
Defining Collections of Data Checks#
For convenience, EvalML provides a DataChecks
class to represent a collection of data checks. We will go over DefaultDataChecks
(API reference), a collection defined to check for some of the most common data issues.
Default Data Checks#
DefaultDataChecks
is a collection of data checks defined to check for some of the most common data issues. They include:
NullDataCheck
IDColumnsDataCheck
TargetLeakageDataCheck
InvalidTargetDataCheck
TargetDistributionDataCheck
(for regression problem types)ClassImbalanceDataCheck
(for classification problem types)NoVarianceDataCheck
DateTimeFormatDataCheck
(for time series problem types)TimeSeriesParametersDataCheck
(for time series problem types)TimeSeriesSplittingDataCheck
(for time series classification problem types)
Writing Your Own Collection of Data Checks#
If you would prefer to create your own collection of data checks, you could either write your own data checks class by extending the DataChecks
class and setting the self.data_checks
attribute to the list of DataCheck
classes or objects, or you could pass that list of data checks to the constructor of the DataChecks
class. Below, we create two identical collections of data checks using the two different methods.
[20]:
# Create a subclass of `DataChecks`
from evalml.data_checks import (
DataChecks,
NullDataCheck,
InvalidTargetDataCheck,
NoVarianceDataCheck,
ClassImbalanceDataCheck,
TargetLeakageDataCheck,
)
from evalml.problem_types import ProblemTypes, handle_problem_types
class MyCustomDataChecks(DataChecks):
data_checks = [
NullDataCheck,
InvalidTargetDataCheck,
NoVarianceDataCheck,
TargetLeakageDataCheck,
]
def __init__(self, problem_type, objective):
"""
A collection of basic data checks.
Args:
problem_type (str): The problem type that is being validated. Can be regression, binary, or multiclass.
"""
if handle_problem_types(problem_type) == ProblemTypes.REGRESSION:
super().__init__(
self.data_checks,
data_check_params={
"InvalidTargetDataCheck": {
"problem_type": problem_type,
"objective": objective,
}
},
)
else:
super().__init__(
self.data_checks + [ClassImbalanceDataCheck],
data_check_params={
"InvalidTargetDataCheck": {
"problem_type": problem_type,
"objective": objective,
}
},
)
custom_data_checks = MyCustomDataChecks(
problem_type=ProblemTypes.REGRESSION, objective="R2"
)
for data_check in custom_data_checks.data_checks:
print(data_check.name)
NullDataCheck
InvalidTargetDataCheck
NoVarianceDataCheck
TargetLeakageDataCheck
[21]:
# Pass list of data checks to the `data_checks` parameter of DataChecks
same_custom_data_checks = DataChecks(
data_checks=[
NullDataCheck,
InvalidTargetDataCheck,
NoVarianceDataCheck,
TargetLeakageDataCheck,
],
data_check_params={
"InvalidTargetDataCheck": {
"problem_type": ProblemTypes.REGRESSION,
"objective": "R2",
}
},
)
for data_check in custom_data_checks.data_checks:
print(data_check.name)
NullDataCheck
InvalidTargetDataCheck
NoVarianceDataCheck
TargetLeakageDataCheck