Repository: autogluon/autogluon
Pull request: Fix string dtype
*Issue #, if available:*
https://github.com/awslabs/autogluon/issues/957

*Description of changes:*
The issue was caused by numpy not able to recognize pandas new string dtype while autogluon inferring problem type. String dtype should be categorized as MULTICLASS

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.


Problem statement:
minor issue with StringDtype dtype columns
There is minor issue with pandas dataframe with columns of pd.StringDtype dtype.  If the label column is StringDtype, and the problem type is multiclass classification, AG fails. If one of the feature columns is of StringDtype, AG issues a warning, but manages to finish training.

Here is a toy example to reproduce the issue:
```
df = pd.DataFrame({
    'label': ['1', '0', '1', '0', '2'],
    's': ['a', 'b', 'a', 'b', 'c'],
}, dtype = pd.StringDtype()
)
df.dtypes
```
which gives
```
label    string
s        string
dtype: object
```

Now we train an AG model
```
label = 'label'

predictor = TabularPredictor(
    label=label,
    verbosity=2,
    path = 'autogluon-models/test/',
).fit(
    train_data=df,
#    hyperparameters='multimodal',
)
```
The training step fails with the following error:
```
TypeError                                 Traceback (most recent call last)
<ipython-input-27-c7252802bdb4> in <module>
      6     path = 'autogluon-models/test/',
      7 ).fit(
----> 8     train_data=df,
      9 #    hyperparameters='multimodal',
     10 )

~/efs/home/qiangson/workspace/autogluon/core/src/autogluon/core/utils/decorators.py in _call(*args, **kwargs)
     27         def _call(*args, **kwargs):
     28             gargs, gkwargs = g(*other_args, *args, **kwargs)
---> 29             return f(*gargs, **gkwargs)
     30         return _call
     31     return _unpack_inner

~/efs/home/qiangson/workspace/autogluon/tabular/src/autogluon/tabular/predictor/predictor.py in fit(self, train_data, tuning_data, time_limit, presets, hyperparameters, feature_metadata, **kwargs)
    631                           holdout_frac=holdout_frac, num_bag_folds=num_bag_folds, num_bag_sets=num_bag_sets, num_stack_levels=num_stack_levels,
    632                           hyperparameters=hyperparameters, core_kwargs=core_kwargs,
--> 633                           time_limit=time_limit, verbosity=verbosity)
    634         self._set_post_fit_vars()
    635 

~/efs/home/qiangson/workspace/autogluon/tabular/src/autogluon/tabular/learner/abstract_learner.py in fit(self, X, X_val, **kwargs)
     97             raise AssertionError('Learner is already fit.')
     98         self._validate_fit_input(X=X, X_val=X_val, **kwargs)
---> 99         return self._fit(X=X, X_val=X_val, **kwargs)
    100 
    101     def _fit(self, X: DataFrame, X_val: DataFrame = None, scheduler_options=None, hyperparameter_tune=False,

~/efs/home/qiangson/workspace/autogluon/tabular/src/autogluon/tabular/learner/default_learner.py in _fit(self, X, X_val, X_unlabeled, holdout_frac, num_bag_folds, num_bag_sets, time_limit, verbosity, **trainer_fit_kwargs)
     61         time_preprocessing_start = time.time()
     62         logger.log(20, 'Preprocessing data ...')
---> 63         X, y, X_val, y_val, X_unlabeled, holdout_frac, num_bag_folds = self.general_data_processing(X, X_val, X_unlabeled, holdout_frac, num_bag_folds)
     64         time_preprocessing_end = time.time()
     65         self._time_fit_preprocessing = time_preprocessing_end - time_preprocessing_start

~/efs/home/qiangson/workspace/autogluon/tabular/src/autogluon/tabular/learner/default_learner.py in general_data_processing(self, X, X_val, X_unlabeled, holdout_frac, num_bag_folds)
    112 
    113         if self.problem_type is None:
--> 114             self.problem_type = self.infer_problem_type(X[self.label])
    115 
    116         if X_val is not None and self.label in X_val.columns:

~/efs/home/qiangson/workspace/autogluon/tabular/src/autogluon/tabular/learner/abstract_learner.py in infer_problem_type(y)
    466     @staticmethod
    467     def infer_problem_type(y: Series):
--> 468         return infer_problem_type(y=y)
    469 
    470     def save(self):

~/efs/home/qiangson/workspace/autogluon/core/src/autogluon/core/utils/utils.py in infer_problem_type(y, silent)
    315         problem_type = MULTICLASS
    316         reason = f"dtype of label-column == {y.dtype.name}"
--> 317     elif np.issubdtype(y.dtype, np.floating):
    318         unique_ratio = unique_count / float(num_rows)
    319         if (unique_ratio <= REGRESS_THRESHOLD) and (unique_count <= MULTICLASS_LIMIT):

~/efs/home/qiangson/workspace/autogluon/conda-env-ag/lib/python3.7/site-packages/numpy/core/numerictypes.py in issubdtype(arg1, arg2)
    386     """
    387     if not issubclass_(arg1, generic):
--> 388         arg1 = dtype(arg1).type
    389     if not issubclass_(arg2, generic):
    390         arg2 = dtype(arg2).type

TypeError: Cannot interpret 'StringDtype' as a data type
```

As a quick fix, I need to do something like
```
str_cols = list(df.dtypes[df.dtypes == 'string'].index)
for col in str_cols:
    df[col] = df[col].astype('object')
```
It will be nice to have this quick fix built into AutoGluon.

Hints from issue discussion:

Commit message: change return type

Patch to review:
diff --git a/common/src/autogluon/common/features/infer_types.py b/common/src/autogluon/common/features/infer_types.py
index b0d3d568d2dd..4a1da03fc106 100644
--- a/common/src/autogluon/common/features/infer_types.py
+++ b/common/src/autogluon/common/features/infer_types.py
@@ -18,6 +18,8 @@ def get_type_family_raw(dtype) -> str:
             return 'category'
         if 'datetime' in dtype.name:
             return 'datetime'
+        if 'string' in dtype.name:
+            return 'object'
         elif np.issubdtype(dtype, np.integer):
             return 'int'
         elif np.issubdtype(dtype, np.floating):
diff --git a/tabular/src/autogluon/tabular/learner/default_learner.py b/tabular/src/autogluon/tabular/learner/default_learner.py
index b2d88c6d2f5c..91484c5132b2 100644
--- a/tabular/src/autogluon/tabular/learner/default_learner.py
+++ b/tabular/src/autogluon/tabular/learner/default_learner.py
@@ -110,7 +110,10 @@ def _fit(self, X: DataFrame, X_val: DataFrame = None, X_unlabeled: DataFrame = N
     def general_data_processing(self, X: DataFrame, X_val: DataFrame, X_unlabeled: DataFrame, holdout_frac: float, num_bag_folds: int):
         """ General data processing steps used for all models. """
         X = copy.deepcopy(X)
-
+        str_cols = list(X.select_dtypes('string').columns)
+        for col in str_cols:
+            logger.warning(f'Detected string dtype for column {col}, converting to object dtype')
+            X[col] = X[col].astype('object')
         # TODO: We should probably uncomment the below lines, NaN label should be treated as just another value in multiclass classification -> We will have to remove missing, compute problem type, and add back missing if multiclass
         # if self.problem_type == MULTICLASS:
         #     X[self.label] = X[self.label].fillna('')


Code review comments:
diff_hunk: @@ -110,7 +110,10 @@ def _fit(self, X: DataFrame, X_val: DataFrame = None, X_unlabeled: DataFrame = N
     def general_data_processing(self, X: DataFrame, X_val: DataFrame, X_unlabeled: DataFrame, holdout_frac: float, num_bag_folds: int):
         """ General data processing steps used for all models. """
         X = copy.deepcopy(X)
-
+        str_cols = list(X.select_dtypes('string').columns)
+        for col in str_cols:
+            logger.warning(f'Detected string dtype for column {col}, converting to object dtype')
+            X[col] = X[col].astype('object')
path: tabular/src/autogluon/tabular/learner/default_learner.py
text: @user1:
All logic that alters feature types must be located in the feature generator. Otherwise, someone could fit a feature generator outside of Predictor and pass it in to predictor.fit, which would force the type back to string.