Repository: alteryx/evalml (python)

Issue:
Automl: infer problem type from target data
Hello!

I wrote this tiny helper function that tests if a target variable is multiclass or binary for classification problems. Max thought it might be helpful if we added it to EvalML.

```python
def get_problem_type(input_problem_type, target_data):
    """helper function to determine if classification problem is binary or multiclass dependent on target variable values."""
    if input_problem_type == "classification":
        values = target_data.value_counts()
        if values.size == 2:
            return ProblemTypes.BINARY
        elif values.size > 2:
            return ProblemTypes.MULTICLASS
        else:
            raise ValueError(
                "The target field contains less than two unique values. It cannot be used for modeling."
            )
    return ProblemTypes.REGRESSION
```

Pull request:
Infer Problem type 
fix #826 

Create a method to infer problem type given the target data. Design doc for this is [here](https://alteryx.quip.com/Fr2IAMv2utid/AutoML-Infer-problem-type-from-target-data)

Updated api docs [here](https://evalml.alteryx.com/en/bc_826_targets/api_reference.html#problem-types)

Patch:
diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst
index 209857f1..4dbeccf0 100644
--- a/docs/source/api_reference.rst
+++ b/docs/source/api_reference.rst
@@ -352,6 +352,7 @@ Problem Types
     :nosignatures:
 
     handle_problem_types
+    detect_problem_type
 
 
 .. currentmodule:: evalml.model_family
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 5623a999..673a327c 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -5,6 +5,7 @@ Release Notes
     * Enhancements
         * Updated partial dependence methods to support calculating numeric columns in a dataset with non-numeric columns :pr:`1150`
         * Added `get_feature_names` on `OneHotEncoder` :pr:`1193`
+        * Added `detect_problem_type` to `problem_type/utils.py` to automatically detect the problem type given targets :pr:`1194`
         * Added LightGBM to AutoMLSearch :pr:`1199`
         * Updates scikit-learn and scikit-optimize to use latest versions - 0.23.2 and 0.8.1 respectively :pr:`1141`
         * Add `ProblemTypes.all_problem_types` helper to get list of supported problem types :pr:`1219`
diff --git a/docs/source/user_guide/automl.ipynb b/docs/source/user_guide/automl.ipynb
index 936ac2d2..c1fc1852 100644
--- a/docs/source/user_guide/automl.ipynb
+++ b/docs/source/user_guide/automl.ipynb
@@ -71,6 +71,30 @@
     "The AutoML interface supports a variety of other parameters. For a comprehensive list, please [refer to the API reference.](../generated/evalml.automl.AutoMLSearch.ipynb)"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Detecting Problem Type\n",
+    "\n",
+    "EvalML includes a simple method, `detect_problem_type`, to help determine the problem type given the target data. \n",
+    "\n",
+    "This function can return the predicted problem type as a ProblemType enum, choosing from ProblemType.BINARY, ProblemType.MULTICLASS, and ProblemType.REGRESSION. If the target data is invalid (for instance when there is only 1 unique label), the function will throw an error instead.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from evalml.problem_types import detect_problem_type\n",
+    "\n",
+    "y = pd.Series([0, 1, 1, 0, 1, 1])\n",
+    "detect_problem_type(y)"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
diff --git a/evalml/problem_types/__init__.py b/evalml/problem_types/__init__.py
index 4a1f7eec..fcff2000 100644
--- a/evalml/problem_types/__init__.py
+++ b/evalml/problem_types/__init__.py
@@ -1,3 +1,3 @@
 # flake8:noqa
 from .problem_types import ProblemTypes
-from .utils import handle_problem_types
+from .utils import handle_problem_types, detect_problem_type
diff --git a/evalml/problem_types/utils.py b/evalml/problem_types/utils.py
index 6bcf2057..0a4e76cf 100644
--- a/evalml/problem_types/utils.py
+++ b/evalml/problem_types/utils.py
@@ -1,5 +1,9 @@
+import pandas as pd
+
 from .problem_types import ProblemTypes
 
+from evalml.utils.gen_utils import numeric_dtypes
+
 
 def handle_problem_types(problem_type):
     """Handles problem_type by either returning the ProblemTypes or converting from a str.
@@ -19,3 +23,30 @@ def handle_problem_types(problem_type):
     if isinstance(problem_type, ProblemTypes):
         return problem_type
     raise ValueError('`handle_problem_types` was not passed a str or ProblemTypes object')
+
+
+def detect_problem_type(y):
+    """Determine the type of problem is being solved based on the targets (binary vs multiclass classification, regression)
+        Ignores missing and null data
+
+    Arguments:
+        y (pd.Series): the target labels to predict
+
+    Returns:
+        ProblemType: ProblemType Enum
+
+    Example:
+        >>> y = pd.Series([0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1])
+        >>> problem_type = detect_problem_type(y)
+        >>> assert problem_type == ProblemTypes.BINARY
+    """
+    y = pd.Series(y).dropna()
+    num_classes = y.nunique()
+    if num_classes < 2:
+        raise ValueError("Less than 2 classes detected! Target unusable for modeling")
+    if num_classes == 2:
+        return ProblemTypes.BINARY
+    if y.dtype in numeric_dtypes:
+        if (num_classes > 10):
+            return ProblemTypes.REGRESSION
+    return ProblemTypes.MULTICLASS
diff --git a/evalml/utils/gen_utils.py b/evalml/utils/gen_utils.py
index c1513b89..537ada3c 100644
--- a/evalml/utils/gen_utils.py
+++ b/evalml/utils/gen_utils.py
@@ -212,7 +212,6 @@ def _rename_column_names_to_numeric(X):
 
     Returns:
         Transformed X where column names are renamed to numerical values
-
     """
     name_to_col_num = dict((col, col_num) for col_num, col in enumerate(X.columns.values))
     return X.rename(columns=name_to_col_num, inplace=False)


Test patch:
diff --git a/evalml/tests/problem_type_tests/test_problem_types.py b/evalml/tests/problem_type_tests/test_problem_types.py
index dc7fcc6b..5567da36 100644
--- a/evalml/tests/problem_type_tests/test_problem_types.py
+++ b/evalml/tests/problem_type_tests/test_problem_types.py
@@ -1,6 +1,12 @@
+import numpy as np
+import pandas as pd
 import pytest
 
-from evalml.problem_types import ProblemTypes, handle_problem_types
+from evalml.problem_types import (
+    ProblemTypes,
+    detect_problem_type,
+    handle_problem_types
+)
 
 
 @pytest.fixture
@@ -10,7 +16,7 @@ def correct_problem_types():
 
 
 def test_handle_string(correct_problem_types):
-    problem_types = ['regression', 'multiclass', 'binary']
+    problem_types = ['regression', ProblemTypes.MULTICLASS, 'binary']
     for problem_type in zip(problem_types, correct_problem_types):
         assert handle_problem_types(problem_type[0]) == problem_type[1]
 
@@ -31,6 +37,78 @@ def test_handle_incorrect_type():
         handle_problem_types(5)
 
 
+def test_detect_problem_type_error():
+    y_empty = pd.Series([])
+    y_one_value = pd.Series([1, 1, 1, 1, 1, 1])
+    y_nan = pd.Series([np.nan, np.nan, 0])
+    y_all_nan = pd.Series([np.nan, np.nan])
+
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_empty)
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_one_value)
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_nan)
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_all_nan)
+
+
+def test_detect_problem_type_binary():
+    y_binary = pd.Series([1, 0, 1, 0, 0, 1])
+    y_bool = pd.Series([True, False, True, True, True])
+    y_float = pd.Series([1.0, 0.0, 1.0, 1.0, 0.0, 0.0])
+    y_object = pd.Series(['yes', 'no', 'no', 'yes'])
+    y_categorical = pd.Series(['yes', 'no', 'no', 'yes'], dtype='category')
+    y_null = pd.Series([None, 0, 1, 1, 1])
+
+    assert detect_problem_type(y_binary) == ProblemTypes.BINARY
+    assert detect_problem_type(y_bool) == ProblemTypes.BINARY
+    assert detect_problem_type(y_float) == ProblemTypes.BINARY
+    assert detect_problem_type(y_object) == ProblemTypes.BINARY
+    assert detect_problem_type(y_categorical) == ProblemTypes.BINARY
+    assert detect_problem_type(y_null) == ProblemTypes.BINARY
+
+
+def test_detect_problem_type_multiclass():
+    y_multi = pd.Series([1, 2, 0, 2, 0, 0, 1])
+    y_categorical = pd.Series(['yes', 'no', 'maybe', 'no'], dtype='category')
+    y_classes = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, np.nan] * 5)
+    y_float = pd.Series([1.0, 2, 3])
+    y_obj = pd.Series(["y", "n", "m"])
+    y_neg = pd.Series([1, 2, -3])
+
+    assert detect_problem_type(y_multi) == ProblemTypes.MULTICLASS
+    assert detect_problem_type(y_categorical) == ProblemTypes.MULTICLASS
+    assert detect_problem_type(y_classes) == ProblemTypes.MULTICLASS
+    assert detect_problem_type(y_float) == ProblemTypes.MULTICLASS
+    assert detect_problem_type(y_obj) == ProblemTypes.MULTICLASS
+    assert detect_problem_type(y_neg) == ProblemTypes.MULTICLASS
+
+
+def test_detect_problem_type_regression():
+    y_values = pd.Series([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, np.nan])
+    y_float = pd.Series([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11.0])
+
+    assert detect_problem_type(y_values) == ProblemTypes.REGRESSION
+    assert detect_problem_type(y_float) == ProblemTypes.REGRESSION
+
+
+def test_nan_none_na():
+    y_none = pd.Series([None])
+    y_pdna = pd.Series([pd.NA])
+    y_nan = pd.Series([np.nan])
+    y_all_null = pd.Series([None, np.nan, np.nan])
+
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_none)
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_pdna)
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_nan)
+    with pytest.raises(ValueError, match="Less than 2"):
+        detect_problem_type(y_all_null)
+
+
 def test_string_repr():
     assert ProblemTypes.BINARY.value == ProblemTypes.BINARY.__str__()
     assert ProblemTypes.MULTICLASS.value == ProblemTypes.MULTICLASS.__str__()