Repository: alteryx/woodwork
Pull request #1549 (2022-10-25 16:39:54)

Add Boolean Inference

codecov[bot]: # [Codecov](https://codecov.io/gh/alteryx/woodwork/pull/1549?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) Report
> Merging [#1549](https://codecov.io/gh/alteryx/woodwork/pull/1549?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) (d31c5e6) into [main](https://codecov.io/gh/alteryx/woodwork/commit/2e8e5e9905db4f4433344242d67da935c0af3b00?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) (2e8e5e9) will **decrease** coverage by `81.73%`.
> The diff coverage is `4.62%`.

> :exclamation: Current head d31c5e6 differs from pull request most recent head 3d71009. Consider uploading reports for the commit 3d71009 to get more accurate results

```diff
@@             Coverage Diff             @@
##             main    #1549       +/-   ##
===========================================
- Coverage   98.91%   17.18%   -81.74%     
===========================================
  Files          91       91               
  Lines       10813    10892       +79     
===========================================
- Hits        10696     1872     -8824     
- Misses        117     9020     +8903     
```


| [Impacted Files](https://codecov.io/gh/alteryx/woodwork/pull/1549?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) | Coverage Δ | |
|---|---|---|
| [woodwork/config.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvY29uZmlnLnB5) | `53.33% <ø> (-46.67%)` | :arrow_down: |
| [woodwork/tests/accessor/test\_serialization.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvYWNjZXNzb3IvdGVzdF9zZXJpYWxpemF0aW9uLnB5) | `21.29% <ø> (-78.71%)` | :arrow_down: |
| [woodwork/tests/accessor/test\_statistics.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvYWNjZXNzb3IvdGVzdF9zdGF0aXN0aWNzLnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [woodwork/tests/accessor/test\_table\_accessor.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvYWNjZXNzb3IvdGVzdF90YWJsZV9hY2Nlc3Nvci5weQ==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [woodwork/tests/logical\_types/test\_logical\_types.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvbG9naWNhbF90eXBlcy90ZXN0X2xvZ2ljYWxfdHlwZXMucHk=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [woodwork/tests/testing\_utils/\_\_init\_\_.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvdGVzdGluZ191dGlscy9fX2luaXRfXy5weQ==) | `35.71% <0.00%> (-64.29%)` | :arrow_down: |
| [woodwork/tests/type\_system/conftest.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvdHlwZV9zeXN0ZW0vY29uZnRlc3QucHk=) | `0.00% <ø> (-100.00%)` | :arrow_down: |
| [woodwork/tests/type\_system/test\_ltype\_inference.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvdHlwZV9zeXN0ZW0vdGVzdF9sdHlwZV9pbmZlcmVuY2UucHk=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [woodwork/tests/utils/test\_read\_file.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvdXRpbHMvdGVzdF9yZWFkX2ZpbGUucHk=) | `0.00% <ø> (-100.00%)` | :arrow_down: |
| [woodwork/tests/conftest.py](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvY29uZnRlc3QucHk=) | `57.36% <5.55%> (-42.64%)` | :arrow_down: |
| ... and [67 more](https://codecov.io/gh/alteryx/woodwork/pull/1549/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx)

bchen1116: No big change in runtime. Perf test report is attached
[3109.html.zip](https://github.com/alteryx/woodwork/files/9881846/3109.html.zip)

bchen1116: Leaving this unmerged for the 0.20.0 release because it introduces a lot of breaking changes. Will aim to get in for 0.21.0 release.
ParthivNaresh: > Leaving this unmerged for the 0.20.0 release because it introduces a lot of breaking changes. Will aim to get in for 0.21.0 release.

Can we also add all documented breaking changes to the release notes?
bchen1116: Changing to `npartitions=-1`, as per @thehomebrewnerd's suggestion, leads to slightly better times on the perf tests.
[3160.html.zip](https://github.com/alteryx/woodwork/files/9922790/3160.html.zip)

Will keep this change over `.compute()`

Patch:
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 929135a..bb2118b 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -6,6 +6,7 @@ Release Notes
 Future Release
 ==============
     * Enhancements
+        * Improved ``Boolean`` and ``BooleanNullable`` inference to detect common string representations of boolean values (:pr:`1549`)
     * Fixes
         * Resolve FutureWarning in `_get_box_plot_info_for_column` (:pr:`1563`)
     * Changes
@@ -14,7 +15,11 @@ Future Release
     * Testing Changes
 
     Thanks to the following people for contributing to this release:
-    :user:`gsheni`, :user:`sbadithe`
+    :user:`gsheni`, :user:`sbadithe`, :user:`bchen1116`
+
+Breaking Changes
+++++++++++++++++
+    * :pr:`1549` will automatically infer more values as ``Boolean`` or ``BooleanNullable``, including, but not limited to, ``[0, 1]``, ``['yes', 'no']``, and ``["True", "False"]``.
 
 v0.20.0 October 31, 2022
 ========================
diff --git a/woodwork/config.py b/woodwork/config.py
index 842893d..b5233fb 100644
--- a/woodwork/config.py
+++ b/woodwork/config.py
@@ -51,6 +51,14 @@ CONFIG_DEFAULTS = {
     "frequence_inference_window_length": 15,
     "frequence_inference_threshold": 0.9,
     "correlation_metrics": ["mutual_info", "pearson", "spearman", "max", "all"],
+    "boolean_inference_strings": [
+        ["yes", "no"],
+        ["y", "n"],
+        ["1", "0"],
+        ["true", "false"],
+        ["t", "f"],
+    ],
+    "boolean_inference_ints": [1, 0],
 }
 
 
diff --git a/woodwork/logical_types.py b/woodwork/logical_types.py
index 4c1442f..bb6d765 100644
--- a/woodwork/logical_types.py
+++ b/woodwork/logical_types.py
@@ -194,6 +194,10 @@ class AgeNullable(LogicalType):
 class Boolean(LogicalType):
     """Represents Logical Types that contain binary values indicating true/false.
 
+    Args:
+        cast_nulls_as (bool): If provided, null values in the column will be cast to this default bool, otherwise will raise an error if None.
+            Defaults to None.
+
     Examples:
         .. code-block:: python
 
@@ -203,6 +207,34 @@ class Boolean(LogicalType):
 
     primary_dtype = "bool"
 
+    def __init__(self, cast_nulls_as=None):
+        if cast_nulls_as and not isinstance(cast_nulls_as, bool):
+            raise ValueError(
+                f"Parameter `cast_nulls_as` must be either True or False, recieved {cast_nulls_as}",
+            )
+        self.cast_nulls_as = cast_nulls_as
+
+    def transform(self, series, null_invalid_values=False):
+        """Validates Boolean values by checking for valid boolean equivalents.
+
+        Args:
+            series (series): Series of boolean values
+
+        Returns:
+            Series: Returns column transformed into boolean type
+        """
+        is_dask = _is_dask_series(series)
+        if (is_dask and series.isna().any().compute()) or (
+            not is_dask and series.isna().any()
+        ):
+            if self.cast_nulls_as is None:
+                raise ValueError(
+                    "Expected no null values in this Boolean column. If you want to keep the nulls, use BooleanNullable type. Otherwise, cast these nulls to a boolean value with the `cast_null_as` parameter.",
+                )
+            series.fillna(self.cast_nulls_as, inplace=True)
+        series = _coerce_boolean(series, True)
+        return super().transform(series)
+
 
 class BooleanNullable(LogicalType):
     """Represents Logical Types that contain binary values indicating true/false.
@@ -219,8 +251,7 @@ class BooleanNullable(LogicalType):
 
     def transform(self, series, null_invalid_values=False):
         series = _replace_nans(series, self.primary_dtype)
-        if null_invalid_values:
-            series = _coerce_boolean(series)
+        series = _coerce_boolean(series, null_invalid_values)
         return super().transform(series)
 
 
@@ -928,11 +959,24 @@ def _coerce_numeric(series):
     return series
 
 
-def _coerce_boolean(series):
+def _coerce_boolean(series, null_invalid_values=False):
     if not pd.api.types.is_bool_dtype(series):
-        valid = {"True": True, "False": False}
-        series = _coerce_string(series)
-        series = series.map(valid)
+        series = _coerce_string(series).str.lower()
+        return _transform_boolean(series, null_invalid_values)
+    return series
+
+
+def _transform_boolean(series, null_invalid_values):
+    boolean_inference_list = config.get_option("boolean_inference_strings")
+    boolean_inference_list.append(config.get_option("boolean_inference_ints"))
+    valid = {}
+    for booleans in boolean_inference_list:
+        valid[booleans[0]] = True
+        valid[booleans[1]] = False
+    if null_invalid_values:
+        series = series.apply(lambda x: valid.get(x, np.nan))
+    else:
+        series = series.apply(lambda x: valid.get(x, x))
     return series
 
 
diff --git a/woodwork/type_sys/inference_functions.py b/woodwork/type_sys/inference_functions.py
index 6904d44..20a33f9 100644
--- a/woodwork/type_sys/inference_functions.py
+++ b/woodwork/type_sys/inference_functions.py
@@ -7,6 +7,7 @@ from pandas.api import types as pdtypes
 
 import woodwork as ww
 from woodwork import data
+from woodwork.config import config
 from woodwork.type_sys.utils import _is_categorical_series, col_is_datetime
 
 Tokens = Iterable[str]
@@ -93,8 +94,18 @@ def boolean_nullable_func(series):
                 {False},
             ]:
                 return True
+            series_lower = set(str(s).lower() for s in set(series_no_null))
+            if series_lower in [
+                set(boolean_list)
+                for boolean_list in config.get_option("boolean_inference_strings")
+            ]:
+                return True
         except TypeError:  # Necessary to check for non-hashable values because of object dtype consideration
             return False
+    elif pdtypes.is_integer_dtype(series.dtype):
+        series_unique = set(series)
+        if series_unique == set(config.get_option("boolean_inference_ints")):
+            return True
     return False
 
 
diff --git a/woodwork/type_sys/type_system.py b/woodwork/type_sys/type_system.py
index acfe56c..0d13c06 100644
--- a/woodwork/type_sys/type_system.py
+++ b/woodwork/type_sys/type_system.py
@@ -298,7 +298,7 @@ class TypeSystem(object):
             series = series.head(INFERENCE_SAMPLE_SIZE)
         else:
             if _is_dask_series(series):
-                series = series.head(INFERENCE_SAMPLE_SIZE)
+                series = series.head(INFERENCE_SAMPLE_SIZE, npartitions=-1)
             elif _is_spark_series(series):
                 series = series.head(INFERENCE_SAMPLE_SIZE).to_pandas()
             else:


Test patch:
diff --git a/woodwork/tests/accessor/test_serialization.py b/woodwork/tests/accessor/test_serialization.py
index ab2c9e6..019e62b 100644
--- a/woodwork/tests/accessor/test_serialization.py
+++ b/woodwork/tests/accessor/test_serialization.py
@@ -224,7 +224,10 @@ def test_to_dictionary(sample_df):
                 "name": "boolean",
                 "ordinal": 11,
                 "use_standard_tags": True,
-                "logical_type": {"parameters": {}, "type": "Boolean"},
+                "logical_type": {
+                    "parameters": {"cast_nulls_as": None},
+                    "type": "Boolean",
+                },
                 "physical_type": {"type": "bool"},
                 "semantic_tags": [],
                 "description": None,
diff --git a/woodwork/tests/accessor/test_statistics.py b/woodwork/tests/accessor/test_statistics.py
index cf82805..bef7c3f 100644
--- a/woodwork/tests/accessor/test_statistics.py
+++ b/woodwork/tests/accessor/test_statistics.py
@@ -1187,7 +1187,7 @@ def test_describe_with_improper_tags(describe_df):
     df = describe_df.copy()[["boolean_col", "natural_language_col"]]
 
     logical_types = {
-        "boolean_col": Boolean,
+        "boolean_col": BooleanNullable,
         "natural_language_col": NaturalLanguage,
     }
     semantic_tags = {
@@ -1199,7 +1199,7 @@ def test_describe_with_improper_tags(describe_df):
     stats_df = df.ww.describe()
 
     # Make sure boolean stats were computed with improper 'category' tag
-    assert isinstance(stats_df["boolean_col"]["logical_type"], Boolean)
+    assert isinstance(stats_df["boolean_col"]["logical_type"], BooleanNullable)
     assert stats_df["boolean_col"]["semantic_tags"] == {"category"}
     # Make sure numeric stats were not computed with improper 'numeric' tag
     assert stats_df["natural_language_col"]["semantic_tags"] == {"numeric"}
@@ -1441,7 +1441,7 @@ def test_value_counts(categorical_df):
     logical_types = {
         "ints": IntegerNullable,
         "categories1": Categorical,
-        "bools": Boolean,
+        "bools": BooleanNullable,
         "categories2": Categorical,
         "categories3": Categorical,
     }
diff --git a/woodwork/tests/accessor/test_table_accessor.py b/woodwork/tests/accessor/test_table_accessor.py
index 319eb5f..bbba750 100644
--- a/woodwork/tests/accessor/test_table_accessor.py
+++ b/woodwork/tests/accessor/test_table_accessor.py
@@ -2979,11 +2979,8 @@ def test_ltype_conversions_nullable_types():
     assert isinstance(df.ww.logical_types["int"], Integer)
 
     # Test invalid conversions
-    error_msg = (
-        "Error converting datatype for bool_null from type boolean to type bool. "
-        "Please confirm the underlying data is consistent with logical type Boolean."
-    )
-    with pytest.raises(TypeConversionError, match=error_msg):
+    error_msg = "Expected no null values in this Boolean column. If you want to keep the nulls, use BooleanNullable type. Otherwise, cast these nulls to a boolean value"
+    with pytest.raises(ValueError, match=error_msg):
         df.ww.set_types({"bool_null": "Boolean"})
 
     error_msg = (
diff --git a/woodwork/tests/conftest.py b/woodwork/tests/conftest.py
index 47a7d05..98565af 100644
--- a/woodwork/tests/conftest.py
+++ b/woodwork/tests/conftest.py
@@ -97,7 +97,7 @@ def sample_df_pandas():
 @pytest.fixture()
 def sample_df_dask(sample_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(sample_df_pandas, npartitions=2)
+    return dd.from_pandas(sample_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -227,7 +227,7 @@ def sample_unsorted_df_pandas():
 @pytest.fixture()
 def sample_unsorted_df_dask(sample_unsorted_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(sample_unsorted_df_pandas, npartitions=2)
+    return dd.from_pandas(sample_unsorted_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -254,7 +254,7 @@ def sample_series_pandas():
 @pytest.fixture()
 def sample_series_dask(sample_series_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(sample_series_pandas, npartitions=2)
+    return dd.from_pandas(sample_series_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -285,7 +285,7 @@ def sample_datetime_series_pandas():
 @pytest.fixture()
 def sample_datetime_series_dask(sample_datetime_series_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(sample_datetime_series_pandas, npartitions=2)
+    return dd.from_pandas(sample_datetime_series_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -328,7 +328,7 @@ def time_index_df_pandas():
 @pytest.fixture()
 def time_index_df_dask(time_index_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(time_index_df_pandas, npartitions=2)
+    return dd.from_pandas(time_index_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -358,7 +358,7 @@ def numeric_time_index_df_pandas():
 @pytest.fixture()
 def numeric_time_index_df_dask(numeric_time_index_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(numeric_time_index_df_pandas, npartitions=2)
+    return dd.from_pandas(numeric_time_index_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -459,7 +459,7 @@ def describe_df_pandas():
 @pytest.fixture()
 def describe_df_dask(describe_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(describe_df_pandas, npartitions=2)
+    return dd.from_pandas(describe_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -501,7 +501,7 @@ def df_same_mi_pandas():
 @pytest.fixture()
 def df_same_mi_dask(df_same_mi_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(df_same_mi_pandas, npartitions=2)
+    return dd.from_pandas(df_same_mi_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -599,7 +599,7 @@ def categorical_df_pandas():
 @pytest.fixture()
 def categorical_df_dask(categorical_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(categorical_df_pandas, npartitions=2)
+    return dd.from_pandas(categorical_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -623,7 +623,7 @@ def empty_df_pandas():
 @pytest.fixture()
 def empty_df_dask(empty_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(empty_df_pandas, npartitions=2)
+    return dd.from_pandas(empty_df_pandas, npartitions=1)
 
 
 # Cannot have an empty Spark DataFrame
@@ -680,7 +680,7 @@ def latlong_df_pandas():
 @pytest.fixture()
 def latlong_df_dask(latlong_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(latlong_df_pandas, npartitions=2)
+    return dd.from_pandas(latlong_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -706,7 +706,7 @@ def empty_latlong_df_pandas():
 @pytest.fixture()
 def empty_latlong_df_dask(empty_latlong_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(empty_latlong_df_pandas, npartitions=2)
+    return dd.from_pandas(empty_latlong_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -785,7 +785,7 @@ def whitespace_df_pandas():
 @pytest.fixture()
 def whitespace_df_dask(whitespace_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(whitespace_df_pandas, npartitions=2)
+    return dd.from_pandas(whitespace_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -814,7 +814,7 @@ def falsy_names_df_pandas():
 @pytest.fixture()
 def falsy_names_df_dask(falsy_names_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(falsy_names_df_pandas, npartitions=2)
+    return dd.from_pandas(falsy_names_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -902,7 +902,7 @@ def serialize_df_pandas():
 @pytest.fixture()
 def serialize_df_dask(serialize_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(serialize_df_pandas, npartitions=2)
+    return dd.from_pandas(serialize_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -957,7 +957,7 @@ def outliers_df_pandas():
 @pytest.fixture()
 def outliers_df_dask(outliers_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(outliers_df_pandas, npartitions=2)
+    return dd.from_pandas(outliers_df_pandas, npartitions=1)
 
 
 @pytest.fixture()
@@ -1042,7 +1042,7 @@ def timezones_df_pandas():
 @pytest.fixture()
 def timezones_df_dask(timezones_df_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(timezones_df_pandas, npartitions=2)
+    return dd.from_pandas(timezones_df_pandas, npartitions=1)
 
 
 @pytest.fixture(params=["timezones_df_pandas", "timezones_df_dask"])
@@ -1058,7 +1058,7 @@ def postal_code_numeric_series_pandas():
 @pytest.fixture()
 def postal_code_numeric_series_dask(postal_code_numeric_series_pandas):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(postal_code_numeric_series_pandas, npartitions=2)
+    return dd.from_pandas(postal_code_numeric_series_pandas, npartitions=1)
 
 
 @pytest.fixture()
diff --git a/woodwork/tests/logical_types/test_logical_types.py b/woodwork/tests/logical_types/test_logical_types.py
index 715271b..a3dafe4 100644
--- a/woodwork/tests/logical_types/test_logical_types.py
+++ b/woodwork/tests/logical_types/test_logical_types.py
@@ -932,3 +932,130 @@ def test_datetime_pivot_point_no_format_provided():
     df_expected = pd.DataFrame({"dates": expected_values}, dtype="datetime64[ns]")
     df.ww.init(logical_types={"dates": Datetime})
     pd.testing.assert_frame_equal(df, df_expected)
+
+
+@pytest.mark.parametrize("df_type", ["pandas", "dask", "spark"])
+def test_boolean_other_values(df_type):
+    df = pd.DataFrame(
+        {
+            "bool1": [0, 1, 0, 1, 1, 0],
+            "bool2": ["t", "f", "t", "f", "t", "t"],
+            "bool3": ["T", "F", "T", "F", "F", "f"],
+            "bool4": ["true", "false", "false", "false", "true", "true"],
+            "bool5": ["True", "False", "False", "True", "false", "TRUE"],
+            "bool6": ["1", "0", "1", "1", "1", "0"],
+            "bool7": ["YES", "NO", "YES", "yes", "no", "no"],
+            "bool8": ["N", "N", "n", "y", "Y", "y"],
+        },
+    )
+    if df_type == "spark":
+        ps = pytest.importorskip(
+            "pyspark.pandas",
+            reason="Pyspark pandas not installed, skipping",
+        )
+        df = ps.from_pandas(df)
+    elif df_type == "dask":
+        dd = pytest.importorskip(
+            "dask.dataframe",
+            reason="Dask not installed, skipping",
+        )
+        df = dd.from_pandas(df, npartitions=1)
+    df.ww.init()
+    assert all([str(dtype) == "Boolean" for dtype in df.ww.logical_types.values()])
+
+
+def test_boolean_cast_nulls_as():
+    Boolean(cast_nulls_as=None)
+    Boolean(cast_nulls_as=False)
+    Boolean(cast_nulls_as=True)
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            f"Parameter `cast_nulls_as` must be either True or False, recieved {1}",
+        ),
+    ):
+        Boolean(cast_nulls_as=1)
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            f"Parameter `cast_nulls_as` must be either True or False, recieved {'True'}",
+        ),
+    ):
+        Boolean(cast_nulls_as="True")
+
+
+@pytest.mark.parametrize("cast_null", [None, True, False])
+@pytest.mark.parametrize("null", [None, pd.NA, np.nan])
+@pytest.mark.parametrize(
+    "series",
+    (
+        [True, True, False, False],
+        ["Yes", "yes", "NO", "no"],
+        ["1", "1", "0", "0"],
+        ["True", "true", "false", "FALSE"],
+    ),
+)
+def test_boolean_with_null_error(series, null, cast_null):
+    df = pd.DataFrame({"bool_with_null": series + [null]})
+    if cast_null is None:
+        with pytest.raises(
+            ValueError,
+            match="Expected no null values in this Boolean column. If you want to keep the nulls, use BooleanNullable type. Otherwise, cast these nulls to a boolean value",
+        ):
+            df.ww.init(
+                logical_types={"bool_with_null": Boolean(cast_nulls_as=cast_null)},
+            )
+    else:
+        df.ww.init(logical_types={"bool_with_null": Boolean(cast_nulls_as=cast_null)})
+        assert list(df["bool_with_null"].values) == [
+            True,
+            True,
+            False,
+            False,
+            cast_null,
+        ]
+
+
+@pytest.mark.parametrize("df_type", ["pandas", "dask", "spark"])
+@pytest.mark.parametrize("null", [None, pd.NA, np.nan])
+def test_boolean_nullable_other_values_dont_cast(null, df_type):
+    df = pd.DataFrame(
+        {
+            "bool1": ["N", "N", "n", null, "Y", "y"],
+            "bool2": ["t", "f", "t", null, "f", "t"],
+            "bool3": ["T", "F", "T", "F", "F", null],
+            "bool4": ["true", "false", "false", "false", "true", null],
+            "bool5": ["True", "False", "False", "True", null, "TRUE"],
+            "bool6": ["1", "0", "1", null, "1", "0"],
+            "bool7": ["YES", "NO", "YES", "yes", null, "no"],
+        },
+    )
+    if df_type == "spark":
+        ps = pytest.importorskip(
+            "pyspark.pandas",
+            reason="Pyspark pandas not installed, skipping",
+        )
+        df = ps.from_pandas(df)
+    elif df_type == "dask":
+        dd = pytest.importorskip(
+            "dask.dataframe",
+            reason="Dask not installed, skipping",
+        )
+        df = dd.from_pandas(df, npartitions=1)
+    df.ww.init()
+    assert all(
+        [str(dtype) == "BooleanNullable" for dtype in df.ww.logical_types.values()],
+    )
+
+
+def test_boolean_mixed_string():
+    df = pd.DataFrame(
+        {
+            "a": ["yes", "y", "n", "n"],
+            "b": ["True", "t", "False", "False"],
+            "c": ["0", "y", "0", "y"],
+        },
+    )
+
+    df.ww.init()
+    assert all([str(dtype) != "Boolean" for dtype in df.ww.logical_types.values()])
diff --git a/woodwork/tests/testing_utils/__init__.py b/woodwork/tests/testing_utils/__init__.py
index 6e5405c..d4360c1 100644
--- a/woodwork/tests/testing_utils/__init__.py
+++ b/woodwork/tests/testing_utils/__init__.py
@@ -15,7 +15,7 @@ from woodwork.tests.testing_utils.table_utils import (
 
 def pd_to_dask(series):
     dd = pytest.importorskip("dask.dataframe", reason="Dask not installed, skipping")
-    return dd.from_pandas(series, npartitions=2)
+    return dd.from_pandas(series, npartitions=1)
 
 
 def pd_to_spark(series):
diff --git a/woodwork/tests/type_system/conftest.py b/woodwork/tests/type_system/conftest.py
index 4c003f4..0bf2d6c 100644
--- a/woodwork/tests/type_system/conftest.py
+++ b/woodwork/tests/type_system/conftest.py
@@ -63,6 +63,17 @@ def pandas_bools():
     return [
         pd.Series([True, False, True, True]),
         pd.Series([True, np.nan, True, True]),
+        pd.Series([1, 0, 0, 1]),
+        pd.Series(["y", "n", "N", "Y"]),
+        pd.Series(["True", "false", "FALSE", "TRUE"]),
+        pd.Series(["t", "f", "T", "T"]),
+        pd.Series(["yes", "no", "NO", "Yes"]),
+        pd.Series(["1", "1", "1", "0"]),
+        pd.Series(["y", "n", "N", None]),
+        pd.Series(["True", "false", pd.NA, "TRUE"]),
+        pd.Series(["t", "f", "T", np.nan]),
+        pd.Series(["yes", "no", "NO", pd.NA]),
+        pd.Series(["1", "1", np.nan, "0"]),
     ]
 
 
@@ -270,6 +281,9 @@ def pandas_strings():
         pd.Series(
             ["Mr. John Doe", "Doe, Mrs. Jane", "James Brown", "Ms. Paige Turner"],
         ),
+        pd.Series(["y", "no", "N", None]),
+        pd.Series(["T", "false", pd.NA, "TRUE"]),
+        pd.Series(["1", "1", np.nan, "0.0"]),
     ]
 
 
diff --git a/woodwork/tests/type_system/test_ltype_inference.py b/woodwork/tests/type_system/test_ltype_inference.py
index 778d41f..3050d25 100644
--- a/woodwork/tests/type_system/test_ltype_inference.py
+++ b/woodwork/tests/type_system/test_ltype_inference.py
@@ -1,7 +1,7 @@
 from unittest.mock import patch
 
 import woodwork as ww
-from woodwork.accessor_utils import _is_spark_series
+from woodwork.accessor_utils import _is_dask_series, _is_spark_series
 from woodwork.logical_types import (
     Boolean,
     BooleanNullable,
@@ -72,7 +72,11 @@ def test_boolean_inference(bools):
 
     for series in bools:
         for dtype in dtypes:
-            cast_series = series.astype(dtype)
+            if _is_dask_series(series):
+                series = series.compute()
+            cast_series = series
+            if True in series.values:
+                cast_series = series.astype(dtype)
             inferred_type = ww.type_system.infer_logical_type(cast_series)
             if to_pandas(cast_series).isnull().any():
                 assert isinstance(inferred_type, BooleanNullable)
diff --git a/woodwork/tests/utils/test_read_file.py b/woodwork/tests/utils/test_read_file.py
index 0523fec..f628089 100644
--- a/woodwork/tests/utils/test_read_file.py
+++ b/woodwork/tests/utils/test_read_file.py
@@ -82,7 +82,7 @@ def test_read_file_validation_control(
             "sample.csv",
             ("to_csv", {"index": False}),
             {
-                "nrows": 2,
+                "nrows": 3,
                 "dtype": {
                     "age": "Int64",
                     "is_registered": "boolean",
@@ -193,7 +193,6 @@ def test_read_file(
         semantic_tags=kwargs.get("semantic_tags"),
         column_origins=kwargs.get("column_origins"),
     )
-
     if func == "to_csv":
         for c in ["signup_date", "datetime_with_NaT"]:
             df.ww.logical_types[