Repository: unionai-oss/pandera

Issue:
Parse Hypothesis strageies for schema inference, validation, and synthesis
**Is your feature request related to a problem? Please describe.**
Pandera's new synthesis feature is very attractive, but is broken if the constraints are non-trivial (e.g. `pa.Check.str_matches(complicated_regex)`).  [Related discussion here](https://github.com/HypothesisWorks/hypothesis/issues/2641#issuecomment-772187225). 

**Describe the solution you'd like**
Ideally, we could use complex schema for both validation and synthesis. In order for that to be possible, Pandera needs to generate data more efficiently, which means not using rejection sampling. Luckily, Hypothesis has solved the efficient synthesis problem, but Hypothesis strategies can't be used for validation.

It would be great if Pandera could parse a Hypothesis `data_frames` strategy into Schemas for validation:

`my_schema = pa.infer_schema(hypothesis.extra.pandas.data_frames(...))` 

The Schema, then, could use hypothesis for efficient data generation.

**Describe alternatives you've considered**
I've [tried disabling Hypothesis's health checks](https://github.com/HypothesisWorks/hypothesis/issues/2641#issuecomment-771907944) but `Hypothesis literally ran out of random bytes to parse into your dataframe, and there's not really anything that [Hypothesis] can do about that`

**Additional context**
We were attracted to Pandera over Great Expectations due to the ability to create `hypothesis` strategies directly from schemas. This saves us the labor of maintaining separate validation schemas and Hypothesis strategies.


Discussion:
hey @crypdick, yes getting data synthesis right is pretty challenging and I'd like to smooth out the rough edges. Just so I get a better sense of the issue, can you provide an example schema that's causing the health check issues?
@cosmicBboy sure! I had to obfuscate sensitive details but hopefully this gives you a rough idea.

```python
import re

import pandera as pa

# simplified for security
IMG_URL_REGEX = re.compile(
    r"""
    ^s3://   # prefix
    [-a-z0-9]+/
    [A-Za-z0-9-]+/
    20[1-2][0-9]+/  # years 2010-2029
    [A-Za-z0-9-_,.]+/  # including _, commas, period
    v[0-9]/  # versions v0-v9
    2[4-5]/ 
    [0-9]+/
    [0-9]+  # file name
    [.]png$  # extension, escape dot
    """,
    re.X,
)


valid_ids = {str(i) for i in range(1, 1000)}   # placeholder for more complicated logic

is_valid_img_url = pa.Check.str_matches(IMG_URL_REGEX)
is_not_empty_series = pa.Check(lambda series_: len(series_) > 0, name="Series not empty")
is_valid_id = pa.Check.isin(valid_ids)

# class IDs joined by |
# this later gets filtered to ensure each element is a valid ID (invalid IDs throw Exceptions downstream)
numeric_str_delim_by_pipes = re.compile(
    r"""[0-9]{1,4}  # in reality, this is more complicated
        (\|[0-9]{1,4})*  # 0 or more IDs
        """,
    re.X,
)

grouped_by_s3uri_schema = pa.DataFrameSchema(
    columns={
        "img_url": pa.Column(
            pa.String,
            allow_duplicates=False,
            nullable=False,
            checks=[is_valid_img_url, is_not_empty_series],  # most restrictive check first
        ),
        "labels": pa.Column(
            pa.String,
            checks=[
                pa.Check.str_matches(numeric_str_delim_by_pipes),
                pa.Check(
                    lambda str_delim_pipes: all([id_ in valid_ids for id_ in str_delim_pipes.split("|")]),
                    element_wise=True,
                ),
            ],
        ),
    }
)
```
> It would be great if Pandera could parse a Hypothesis data_frames strategy into Schemas for validation:
>
> my_schema = pa.infer_schema(hypothesis.extra.pandas.data_frames(...))
>
> The Schema, then, could use hypothesis for efficient data generation.

This sounds complicated 😅 (I did sort of look into this when building out the data synthesis functionality but found it way over my head, I'm not sure if there's a straightforward way of parsing complex multi-part strategies)

Note that rejection sampling only occurs after the first check in the `checks` list, so inefficiencies in the first check would not be due to rejection sampling. Testing the code out locally it looks like `img_url` checks are leading to `data_too_large` and `large_base_example`, and `labels` checks are  leading to `filter_too_much` issues.

I think it's up to `pandera` to make data generation as easy as possible and providing users with an interface to customize data synthesis using the `hypothesis` API when built-in checks/strategies aren't up to snuff.

### Another potential solution: custom checks and strategies

The [extensions](https://pandera.readthedocs.io/en/stable/extensions.html) API was designed to give users access to the `hypothesis` API to make custom data synthesis strategies. This lets you register custom checks into the `Check` namespace and associate it with a data-generation strategy. You can effectively combine multiple checks into one, or implement any hypothesis strategy you want with `hypothesis` (with a little bit of ceremony imposed by `pandera`).

You can do something like:

```python
import re
from typing import Optional

import hypothesis
import hypothesis.strategies as st
import pandas as pd
import pandera as pa
import pandera.extensions as extensions


# simplified for security
IMG_URL_REGEX = re.compile(
    r"""
    ^s3://   # prefix
    [-a-z0-9]+/
    [A-Za-z0-9-]+/
    20[1-2][0-9]+/  # years 2010-2029
    [A-Za-z0-9-_,.]+/  # including _, commas, period
    v[0-9]/  # versions v0-v9
    2[4-5]/ 
    [0-9]+/
    [0-9]+  # file name
    [.]png$  # extension, escape dot
    """,
    re.X,
)

NUMERIC_STR_DELIM_REGEX = re.compile(
    r"""[0-9]{1,4}  # in reality, this is more complicated
        (\|[0-9]{1,4})*  # 0 or more IDs
        """,
    re.X,
)

VALID_IDS = [str(i) for i in range(1, 1000)]


# Define custom url strategy and check
def url_strategy(
    pandas_dtype: pa.PandasDtype,
    strategy: Optional[st.SearchStrategy] = None,
    *,
    url_regex,
):
    if strategy is None:
        # replace this with more efficient Hypothesis strategy if desired
        return st.from_regex(url_regex, fullmatch=True)
    raise pa.errors.BaseStrategyOnlyError(
        "'url_strategy' must be a base strategy"
    )


@extensions.register_check_method(
    statistics=["url_regex"],
    strategy=url_strategy,
    supported_types=pd.Series,
)
def valid_url(pandas_obj, *, url_regex):
    """Url regex check."""
    return pandas_obj.str.match(url_regex, na=False)


# Define custom label strategy and check
def labels_strategy(
    pandas_dtype: pa.PandasDtype,
    strategy: Optional[st.SearchStrategy] = None,
    *,
    valid_ids,
):
    if strategy is None:
        # replace this with more efficient Hypothesis strategy if desired
        return st.lists(
            st.sampled_from(VALID_IDS), unique=True, min_size=1
        ).map("|".join)
    raise pa.errors.BaseStrategyOnlyError(
        "'labels_strategy' must be a base strategy"
    )


@extensions.register_check_method(
    statistics=["valid_ids"],
    strategy=labels_strategy,
    supported_types=pd.Series,
)
def valid_labels(pandas_obj, *, valid_ids):
    # combines the regex match check and the valid_ids check
    valid_ids = set(valid_ids)
    return pandas_obj.str.match(NUMERIC_STR_DELIM_REGEX) & (
        pandas_obj.map(lambda x: all(id_ in valid_ids for id_ in x.split("|")))
    )


schema = pa.DataFrameSchema(
    columns={
        "img_url": pa.Column(
            pa.String,
            allow_duplicates=False,
            nullable=False,
            checks=[
                pa.Check.valid_url(url_regex=IMG_URL_REGEX),
                pa.Check(
                    lambda series_: len(series_) > 0, name="Series not empty"
                ),
            ],
        ),
        "labels": pa.Column(
            pa.String,
            checks=pa.Check.valid_labels(valid_ids=VALID_IDS),
        ),
    }
)


@hypothesis.given(schema.strategy(size=5))
def test_schema(df):
    print(df)
    # test something
```

Phew! thanks for bearing with me. All that said, I did find some inefficiencies in the way the dataframe strategy was being constructed: #400 << this PR should actually make the `img_url` health checks go away and the code as you had it in your sample code snippet should work. Feel free to pull the `master` branch and try it out locally!

For the `label` generation code I'd recommend using the `labels_strategy` + `valid_labels` approach that I implemented above, since it generates data by sampling the set of valid ids instead of from the regex, which guarantees that `|` delimited strings contain valid ids
Other types of solutions that would be fairly heavy lifts i.e. going into the guts of the `pandera.strategies` module:
- supporting [composite strategies](https://hypothesis.readthedocs.io/en/latest/data.html#composite-strategies) as a way of chaining multiple checks together.
- providing an interface for "multi-check resolution", which basically aggregates the statistics of multiple checks and constructs a single strategy from them.
@cosmicBboy Tyvm for the detailed answer! When I copy-paste your code, I get a `TypeError: url_strategy() missing 1 required keyword-only argument: 'check_regex'`.
woops! just edited the code snippet , should work now
@cosmicBboy you beat me to it :) ty for pointing me to the extensions docs, I hadn't noticed this feature before
> > It would be great if Pandera could parse a Hypothesis data_frames strategy into Schemas for validation:
> > `my_schema = pa.infer_schema(hypothesis.extra.pandas.data_frames(...))`
> > The Schema, then, could use hypothesis for efficient data generation.
> 
> This sounds complicated 😅 (I did sort of look into this when building out the data synthesis functionality but found it way over my head, I'm not sure if there's a straightforward way of parsing complex multi-part strategies)

Ah, yeah.  As a Hypothesis core dev *I* wouldn't try this; complex strategies like `data_frames()` often have all the relevant structure inside `@composite` strategy functions and pulling it back out of the closures is totally infeasible.

--------------

A better approach, at least for element-wise checks, would be to get our "efficient filter rewriting" https://github.com/HypothesisWorks/hypothesis/issues/2701 done - I think it would be reasonably simple to support numeric bounds and string regex patterns for an initial pass, and that would probably cover a lot of your use-cases.
Thanks for the pointers @Zac-HD! Also, just wanted to say I'm a big fan of `hypothesis`, it's made a huge difference in the way I test DS/ML code (indeed, pandera strategies simply wraps hypothesis for convenience)

The current implementation of check strategy chaining in pandera does heavily use `filter`, really as a first pass to offer this functionality. I'll look into using `composite` and `map` more to make strategy chaining more efficient
@cosmicBboy the solution you posted seems to break down when used with indexes. In particular, if you edit the  `"img_url": pa.Column(...)` part to 

```python
    index=pa.Index(
        pa.String,
        allow_duplicates=False,
        nullable=False,
        name="img_url",
        checks=[
            is_valid_img_url,
            is_not_empty_series,
        ]
    ),
```

Then run `my_schema.strategy().example()` we get a `ValueError: Length mismatch: Expected axis has 7 elements, new values have 0 elements`. However, when we use it in `@given` it works fine.
@crypdick do you have a stacktrace of the error? this is definitely a bug
@cosmicBboy sure, here you go:

```python
  File "/home/richard/.config/JetBrains/PyCharm2021.1/scratches/scratch_2.py", line 85, in <module>
    print(multihot_dataset_schema.strategy().example())
  File "/home/richard/src/DENDRA/venv/lib/python3.8/site-packages/hypothesis/strategies/_internal/strategies.py", line 319, in example
    example_generating_inner_function()
  File "/home/richard/src/DENDRA/venv/lib/python3.8/site-packages/hypothesis/strategies/_internal/strategies.py", line 307, in example_generating_inner_function
    @settings(
  File "/home/richard/src/DENDRA/venv/lib/python3.8/site-packages/hypothesis/core.py", line 1163, in wrapped_test
    raise the_error_hypothesis_found
  File "/home/richard/src/DENDRA/venv/lib/python3.8/site-packages/pandera/strategies.py", line 117, in set_pandas_index
    df_or_series.index = index
  File "/home/richard/src/DENDRA/venv/lib/python3.8/site-packages/pandas/core/generic.py", line 5154, in __setattr__
    return object.__setattr__(self, name, value)
  File "pandas/_libs/properties.pyx", line 66, in pandas._libs.properties.AxisProperty.__set__
  File "/home/richard/src/DENDRA/venv/lib/python3.8/site-packages/pandas/core/generic.py", line 564, in _set_axis
    self._mgr.set_axis(axis, labels)
  File "/home/richard/src/DENDRA/venv/lib/python3.8/site-packages/pandas/core/internals/managers.py", line 226, in set_axis
    raise ValueError(
ValueError: Length mismatch: Expected axis has 6 elements, new values have 0 elements
```

Resolving patch:
diff --git a/noxfile.py b/noxfile.py
--- a/noxfile.py
+++ b/noxfile.py
@@ -12,6 +12,7 @@
 import nox
 from nox import Session
 from pkg_resources import Requirement, parse_requirements
+from packaging import version
 
 
 nox.options.sessions = (
@@ -33,6 +34,10 @@
 REQUIREMENT_PATH = "requirements-dev.txt"
 
 CI_RUN = os.environ.get("CI") == "true"
+if CI_RUN:
+    print("Running on CI")
+else:
+    print("Running locally")
 
 LINE_LENGTH = 79
 
@@ -103,17 +108,48 @@ def _build_requires() -> Dict[str, Dict[str, str]]:
 
 REQUIRES: Dict[str, Dict[str, str]] = _build_requires()
 
+CONDA_ARGS = [
+    "--channel=conda-forge",
+    "--update-specs",
+]
 
-def install(session: Session, *args: str) -> str:
+
+def conda_install(session: Session, *args):
+    """Use mamba to install conda dependencies."""
+    run_args = [
+        "install",
+        "--yes",
+        *CONDA_ARGS,
+        "--prefix",
+        session.virtualenv.location,  # type: ignore
+        *args,
+    ]
+
+    # By default, all dependencies are re-installed from scratch with each
+    # session. Specifying external=True allows access to cached packages, which
+    # decreases runtime of the test sessions.
+    try:
+        session.run(
+            *["mamba", *run_args],
+            external=True,
+        )
+    # pylint: disable=broad-except
+    except Exception:
+        session.run(
+            *["conda", *run_args],
+            external=True,
+        )
+
+
+def install(session: Session, *args: str):
     """Install dependencies in the appropriate virtual environment
     (conda or virtualenv) and return the type of the environmment."""
-    try:
-        session.conda_install("--channel=conda-forge", *args)
-        return "conda"
-    except ValueError:
-        # Not a conda venv
+    if isinstance(session.virtualenv, nox.virtualenv.CondaEnv):
+        print("using conda installer")
+        conda_install(session, *args)
+    else:
+        print("using pip installer")
         session.install(*args)
-        return "virtualenv"
 
 
 def install_from_requirements(session: Session, *packages: str) -> None:
@@ -128,15 +164,22 @@ def install_from_requirements(session: Session, *packages: str) -> None:
         install(session, specs)
 
 
-def install_extras(session: Session, extra: str = "core") -> None:
+def install_extras(
+    session: Session, pandas: str = "latest", extra: str = "core"
+) -> None:
     """Install dependencies."""
-    specs = REQUIRES[extra].values()
-    try:
-        session.conda_install("--channel=conda-forge", *specs)
-    except ValueError:
-        # Not a conda venv
+    pandas_version = "" if pandas == "latest" else f"=={pandas}"
+    specs = [
+        spec if spec != "pandas" else f"pandas{pandas_version}"
+        for spec in REQUIRES[extra].values()
+    ]
+    if isinstance(session.virtualenv, nox.virtualenv.CondaEnv):
+        print("using conda installer")
+        conda_install(session, *specs)
+    else:
+        print("using pip installer")
         session.install(*specs)
-    session.install(".")  # install pandera
+    session.install("-e", ".", "--no-deps")  # install pandera
 
 
 def _generate_pip_deps_from_conda(
@@ -227,18 +270,19 @@ def mypy(session: Session) -> None:
     session.run("mypy", "--follow-imports=silent", *args, silent=True)
 
 
-def _install_pandas(session: Session, pandas: str) -> bool:
-    python_version = cast(str, session.python)
-    if pandas == "0.25.3" and python_version >= "3.9":
+def _invalid_python_pandas_versions(session: Session, pandas: str) -> bool:
+    python_version = version.parse(cast(str, session.python))
+    if pandas == "0.25.3" and python_version >= version.parse("3.9"):
         print("Python 3.9 does not support pandas 0.25.3")
-        return False
+        return True
+    return False
 
-    pandas_version = "" if pandas == "latest" else f"=={pandas}"
-    install(session, f"pandas{pandas_version}")
-    return True
 
-
-EXTRA_NAMES = [extra for extra in REQUIRES if extra != "all"]
+EXTRA_NAMES = [
+    extra
+    for extra in REQUIRES
+    if extra != "all" and not extra.startswith(":python_version")
+]
 
 
 @nox.session(python=PYTHON_VERSIONS)
@@ -246,9 +290,9 @@ def _install_pandas(session: Session, pandas: str) -> bool:
 @nox.parametrize("extra", EXTRA_NAMES)
 def tests(session: Session, pandas: str, extra: str) -> None:
     """Run the test suite."""
-    if not _install_pandas(session, pandas):
+    if _invalid_python_pandas_versions(session, pandas):
         return
-    install_extras(session, extra)
+    install_extras(session, pandas, extra)
 
     if session.posargs:
         args = session.posargs
@@ -257,7 +301,11 @@ def tests(session: Session, pandas: str, extra: str) -> None:
         args = []
         if extra == "strategies":
             # enable threading via pytest-xdist
-            args = ["-n=auto", "-q", "--hypothesis-profile=ci"]
+            args = [
+                "-n=auto",
+                "-q",
+                f"--hypothesis-profile={'ci' if CI_RUN else 'dev'}",
+            ]
         args += [
             f"--cov={PACKAGE}",
             "--cov-report=term-missing",
@@ -275,9 +323,9 @@ def tests(session: Session, pandas: str, extra: str) -> None:
 @nox.parametrize("pandas", ["0.25.3", "latest"])
 def docs(session: Session, pandas: str) -> None:
     """Build the documentation."""
-    if not _install_pandas(session, pandas):
+    if _invalid_python_pandas_versions(session, pandas):
         return
-    install_extras(session, extra="all")
+    install_extras(session, pandas, extra="all")
     session.chdir("docs")
 
     args = session.posargs or ["-W", "-E", "-b=doctest", "source", "_build"]
diff --git a/pandera/strategies.py b/pandera/strategies.py
--- a/pandera/strategies.py
+++ b/pandera/strategies.py
@@ -120,12 +120,11 @@ def null_dataframe_masks(
 def set_pandas_index(
     draw,
     df_or_series_strat: SearchStrategy,
-    index_strat: SearchStrategy,
+    index: IndexComponent,
 ):
     """Sets Index or MultiIndex object to pandas Series or DataFrame."""
     df_or_series = draw(df_or_series_strat)
-    index = draw(index_strat)
-    df_or_series.index = index
+    df_or_series.index = draw(index.strategy(size=df_or_series.shape[0]))
     return df_or_series
 
 
@@ -1014,7 +1013,7 @@ def _dataframe_strategy(draw):
             strategy = null_dataframe_masks(strategy, nullable_columns)
 
         if index is not None:
-            strategy = set_pandas_index(strategy, index.strategy(size=size))
+            strategy = set_pandas_index(strategy, index)
 
         for check in undefined_strat_df_checks:
             strategy = undefined_check_strategy(strategy, check)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -16,6 +16,6 @@
 if not HAS_HYPOTHESIS:
     collect_ignore.append("test_strategies.py")
 else:
-    settings.register_profile("ci", max_examples=100)
-    settings.register_profile("dev", max_examples=10)
+    settings.register_profile("ci", max_examples=100, deadline=5000)
+    settings.register_profile("dev", max_examples=10, deadline=2000)
     settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))
diff --git a/tests/strategies/test_strategies.py b/tests/strategies/test_strategies.py
--- a/tests/strategies/test_strategies.py
+++ b/tests/strategies/test_strategies.py
@@ -137,7 +137,6 @@ def value_ranges(pdtype: pa.PandasDtype):
 )
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_check_strategy_chained_continuous(
@@ -184,7 +183,6 @@ def test_check_strategy_chained_continuous(
 @pytest.mark.parametrize("chained", [True, False])
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_in_range_strategy(pdtype, chained, data):
@@ -226,7 +224,6 @@ def test_in_range_strategy(pdtype, chained, data):
 @pytest.mark.parametrize("chained", [True, False])
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_isin_notin_strategies(pdtype, chained, data):
@@ -403,7 +400,6 @@ def _custom_check(series: pd.Series) -> pd.Series:
 
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_series_strategy(data):
@@ -437,17 +433,28 @@ def test_column_example():
     "pdtype",
     SUPPORTED_DTYPES,
 )
+@pytest.mark.parametrize(
+    "size",
+    [None, 0, 1, 3, 5],
+)
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
-def test_dataframe_strategy(pdtype, data):
+def test_dataframe_strategy(pdtype, size, data):
     """Test DataFrameSchema strategy."""
     dataframe_schema = pa.DataFrameSchema(
         {f"{pdtype.value}_col": pa.Column(pdtype)}
     )
-    dataframe_schema(data.draw(dataframe_schema.strategy(size=5)))
+    df_sample = data.draw(dataframe_schema.strategy(size=size))
+    if size == 0:
+        assert df_sample.empty
+    elif size is None:
+        assert df_sample.empty or isinstance(
+            dataframe_schema(df_sample), pd.DataFrame
+        )
+    else:
+        assert isinstance(dataframe_schema(df_sample), pd.DataFrame)
     with pytest.raises(pa.errors.BaseStrategyOnlyError):
         strategies.dataframe_strategy(
             pdtype, strategies.pandas_dtype_strategy(pdtype)
@@ -469,9 +476,8 @@ def test_dataframe_example():
         "[a-z]+_[0-9]+_[a-z]+",
     ],
 )
-@hypothesis.given(st.data(), st.integers(min_value=-10, max_value=30))
+@hypothesis.given(st.data(), st.integers(min_value=-5, max_value=5))
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_dataframe_with_regex(regex, data, n_regex_columns):
@@ -493,7 +499,6 @@ def test_dataframe_with_regex(regex, data, n_regex_columns):
 
 @pytest.mark.parametrize("pdtype", NUMERIC_DTYPES)
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 @hypothesis.given(st.data())
@@ -517,7 +522,6 @@ def test_dataframe_checks(pdtype, data):
 @pytest.mark.parametrize("pdtype", [pa.Int, pa.Float, pa.String, pa.DateTime])
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_dataframe_strategy_with_indexes(pdtype, data):
@@ -536,6 +540,9 @@ def test_dataframe_strategy_with_indexes(pdtype, data):
 
 
 @hypothesis.given(st.data())
+@hypothesis.settings(
+    suppress_health_check=[hypothesis.HealthCheck.too_slow],
+)
 def test_index_strategy(data):
     """Test Index schema component strategy."""
     pdtype = pa.PandasDtype.Int
@@ -558,6 +565,9 @@ def test_index_example():
 
 
 @hypothesis.given(st.data())
+@hypothesis.settings(
+    suppress_health_check=[hypothesis.HealthCheck.too_slow],
+)
 def test_multiindex_strategy(data):
     """Test MultiIndex schema component strategy."""
     pdtype = pa.PandasDtype.Float
@@ -619,7 +629,6 @@ def test_field_element_strategy(pdtype, data):
 @pytest.mark.parametrize("nullable", [True, False])
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_check_nullable_field_strategy(pdtype, field_strategy, nullable, data):
@@ -648,7 +657,6 @@ def test_check_nullable_field_strategy(pdtype, field_strategy, nullable, data):
 @pytest.mark.parametrize("nullable", [True, False])
 @hypothesis.given(st.data())
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[hypothesis.HealthCheck.too_slow],
 )
 def test_check_nullable_dataframe_strategy(pdtype, nullable, data):
@@ -696,7 +704,6 @@ def test_check_nullable_dataframe_strategy(pdtype, nullable, data):
     ],
 )
 @hypothesis.settings(
-    deadline=2000,
     suppress_health_check=[
         hypothesis.HealthCheck.filter_too_much,
         hypothesis.HealthCheck.too_slow,