Repository: moeyensj/difi

Issue:
Warn user when not all of the unique truths appear in the given truth classes
When the user gives a column name or dictionary of the classes to which every observation belongs to, there should be a warning thrown if not all of the unique truths in the data set appear in the given classes. 

Discussion:

Patch:
diff --git a/difi/utils.py b/difi/utils.py
index 350eea3..f5d2941 100644
--- a/difi/utils.py
+++ b/difi/utils.py
@@ -1,3 +1,4 @@
+import warnings
 import numpy as np
 from pandas.api.types import is_object_dtype
 
@@ -107,12 +108,18 @@ def _classHandler(classes, dataframe, column_mapping):
         A list of class names. 
     truths_list : list
         A list of the truths belonging to each class. 
+
+    Raises
+    ------
+    UserWarning : If not all truths in the dataframe are assigned a class
     """
     class_list = ["All"]
     truths_list = [[]]
+    unique_truths = []
 
     if classes == None:
         truths_list = [dataframe[column_mapping["truth"]].unique()]
+        unique_truths = [truths_list[0]]
     
     elif type(classes) == str:
         if classes not in dataframe.columns:
@@ -122,9 +129,11 @@ def _classHandler(classes, dataframe, column_mapping):
             raise ValueError(err.format(classes))
         else:
             
-            for c in dataframe[classes].unique():
+            for c in dataframe[~dataframe[classes].isna()][classes].unique():
                 class_list.append(c)
-                truths_list.append(dataframe[dataframe[classes].isin([c])][column_mapping["truth"]].unique())
+                class_truths = dataframe[dataframe[classes].isin([c])][column_mapping["truth"]].unique()
+                unique_truths.append(class_truths)
+                truths_list.append(class_truths)
 
         truths_list[0] = dataframe[column_mapping["truth"]].unique()
             
@@ -138,6 +147,7 @@ def _classHandler(classes, dataframe, column_mapping):
             else:
                 class_list.append(c)
                 truths_list[0].append(t)
+                unique_truths.append(t)
                 if type(t) is list:
                     truths_list.append(np.array(t))
                 else:
@@ -156,6 +166,26 @@ def _classHandler(classes, dataframe, column_mapping):
             "  None : If there are no classes of truths."
         )
         raise ValueError(err)
+
+    # Test that the unique truths are in fact unique
+    unique_truths = np.concatenate(unique_truths)
+    if not len(np.unique(unique_truths)) == len(unique_truths):
+        err = (
+            "Some truths are duplicated in multiple classes."
+        )
+        raise ValueError(err)
+
+    if not dataframe[column_mapping["truth"]].isin(unique_truths).all():
+        warning = (
+            "Some truths do not have an assigned class.\n" \
+            "Unclassified truths have been added as 'Unclassified'."
+        )
+
+        unclassified = dataframe[~dataframe[column_mapping["truth"]].isin(unique_truths)][column_mapping["truth"]].unique()
+        class_list.append("Unclassified")
+        truths_list.append(unclassified)
+        truths_list[0] = np.concatenate([truths_list[0], unclassified])
+        warnings.warn(warning, UserWarning)
         
     return class_list, truths_list
 


Test patch:
diff --git a/difi/tests/test_utils.py b/difi/tests/test_utils.py
index 9332bac..a410767 100644
--- a/difi/tests/test_utils.py
+++ b/difi/tests/test_utils.py
@@ -1,3 +1,4 @@
+import copy
 import pytest
 import numpy as np
 import pandas as pd
@@ -199,14 +200,68 @@ def test__classHandler_errors():
     
     # Test for ValueError when a truth appears twice for the same class
     with pytest.raises(ValueError):
-        classes_dict_ = classes_dict.copy()
+        classes_dict_ = copy.deepcopy(classes_dict)
         classes_dict_["green"].append(classes_dict_["green"][-1])
     
         class_list, truths_list = _classHandler(classes_dict_, observations, column_mapping)
         
     # Test for ValueError when an incorrect column name is given
     with pytest.raises(ValueError):
-       
         class_list, truths_list = _classHandler("abc", observations, column_mapping)
+
+    # Test for ValueError when a truth appears in more than two classes
+    classes_dict["blue"].append(classes_dict["green"][-1])
+    with pytest.raises(ValueError):
+        class_list, truths_list = _classHandler(classes_dict, observations, column_mapping)
+        
+    return
+
+def test__classHandler_warnings():
+    ### Test _classHandler for warnings
+    
+    column_mapping = {
+        "truth" : "obj_id"
+    }
+
+    classes_dict = {}
+    all_truths = []
+    
+    # Create list of expected outputs from _classHandler
+    class_list_test = ["All"]
+    truths_list_test = []
+    
+    # Add to the expected outputs and build the expected
+    # input observations dataframe and classes dictionary
+    for c in ["green", "blue", "red", "orange"]:
+        truths = ["{}{:02d}".format(c, i) for i in range(10)]
+        truths_list_test.append(truths)
+        all_truths += truths
+        classes_dict[c] = truths
+        class_list_test.append(c)
+    truths_list_test.insert(0, all_truths)
+
+    observations = pd.DataFrame({
+        "obs_id" : ["obs{:02d}".format(i) for i in range(len(all_truths))],
+        "obj_id" : all_truths})
+    for c in ["green", "blue", "red", "orange"]:
+        observations.loc[observations["obj_id"].isin(classes_dict[c]), "class"] = c
+
+    # Remove the orange class from classes dict
+    classes_dict_ = copy.deepcopy(classes_dict)
+    classes_dict_.pop("orange")
+    observations.loc[observations["class"].isin(["orange"]), "class"] = np.NaN
+    
+    # Test for UserWarning when not all truths have an assigned class
+    with pytest.warns(UserWarning):
+        class_list, truths_list = _classHandler(classes_dict_, observations, column_mapping)
+
+        assert "Unclassified" in class_list
+        assert np.all(truths_list[-1] == classes_dict["orange"])
+
+    with pytest.warns(UserWarning):
+        class_list, truths_list = _classHandler("class", observations, column_mapping)
+
+        assert "Unclassified" in class_list
+        assert np.all(truths_list[-1] == classes_dict["orange"])
     
     return
\ No newline at end of file