diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 1619ba1a45739..3e8383c66abc2 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -575,7 +575,7 @@ Strings ^^^^^^^ - Bug in the ``__name__`` attribute of several methods of :class:`Series.str`, which were set incorrectly (:issue:`23551`) -- +- Improved error message when passing ``Series`` of wrong dtype to :meth:`Series.str.cat` (:issue:`22722`) - diff --git a/pandas/core/strings.py b/pandas/core/strings.py index bd756491abd2f..218577016fd0e 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -2280,6 +2280,23 @@ def cat(self, others=None, sep=None, na_rep=None, join=None): 'must all be of the same length as the ' 'calling Series/Index.') + # data has already been checked by _validate to be of correct dtype, + # but others could still have Series of dtypes (e.g. integers) which + # will necessarily fail in concatenation. To avoid deep and confusing + # traces, we raise here for anything that's not object or all-NA float. + def _legal_dtype(series): + # unify dtype handling between categorical/non-categorical + dtype = (series.dtype if not is_categorical_dtype(series) + else series.cat.categories.dtype) + legal = dtype == 'O' or (dtype == 'float' and series.isna().all()) + return legal + err_wrong_dtype = ('Can only concatenate list-likes containing only ' + 'strings (or missing values).') + if any(not _legal_dtype(x) for x in others): + raise TypeError(err_wrong_dtype + ' Received list-like of dtype: ' + '{}'.format([x.dtype for x in others + if not _legal_dtype(x)][0])) + if join is None and warn: warnings.warn("A future version of pandas will perform index " "alignment when `others` is a Series/Index/" @@ -2307,23 +2324,28 @@ def cat(self, others=None, sep=None, na_rep=None, join=None): na_masks = np.array([isna(x) for x in all_cols]) union_mask = np.logical_or.reduce(na_masks, axis=0) - if na_rep is None and union_mask.any(): - # no na_rep means NaNs for all rows where any column has a NaN - # only necessary if there are actually any NaNs - result = np.empty(len(data), dtype=object) - np.putmask(result, union_mask, np.nan) - - not_masked = ~union_mask - result[not_masked] = cat_core([x[not_masked] for x in all_cols], - sep) - elif na_rep is not None and union_mask.any(): - # fill NaNs with na_rep in case there are actually any NaNs - all_cols = [np.where(nm, na_rep, col) - for nm, col in zip(na_masks, all_cols)] - result = cat_core(all_cols, sep) - else: - # no NaNs - can just concatenate - result = cat_core(all_cols, sep) + # if there are any non-string, non-null values hidden within an object + # dtype, cat_core will fail; catch error and return with better message + try: + if na_rep is None and union_mask.any(): + # no na_rep means NaNs for all rows where any column has a NaN + # only necessary if there are actually any NaNs + result = np.empty(len(data), dtype=object) + np.putmask(result, union_mask, np.nan) + + not_masked = ~union_mask + result[not_masked] = cat_core([x[not_masked] + for x in all_cols], sep) + elif na_rep is not None and union_mask.any(): + # fill NaNs with na_rep in case there are actually any NaNs + all_cols = [np.where(nm, na_rep, col) + for nm, col in zip(na_masks, all_cols)] + result = cat_core(all_cols, sep) + else: + # no NaNs - can just concatenate + result = cat_core(all_cols, sep) + except TypeError: + raise TypeError(err_wrong_dtype) if isinstance(self._orig, Index): # add dtype for case that result is all-NA diff --git a/pandas/tests/test_strings.py b/pandas/tests/test_strings.py index 1ba0ef3918fb7..5011cead017c1 100644 --- a/pandas/tests/test_strings.py +++ b/pandas/tests/test_strings.py @@ -420,6 +420,22 @@ def test_str_cat_categorical(self, box, dtype_caller, dtype_target, sep): result = s.str.cat(t, sep=sep) assert_series_or_index_equal(result, expected) + # test integer/float dtypes (inferred by constructor) and mixed + @pytest.mark.parametrize('data', [[1, 2, 3], [.1, .2, .3], [1, 2, 'b']], + ids=['integers', 'floats', 'mixed']) + # without dtype=object, np.array would cast [1, 2, 'b'] to ['1', '2', 'b'] + @pytest.mark.parametrize('box', [Series, Index, list, + lambda x: np.array(x, dtype=object)], + ids=['Series', 'Index', 'list', 'np.array']) + def test_str_cat_wrong_dtype_raises(self, box, data): + # GH 22722 + s = Series(['a', 'b', 'c']) + t = box(data) + + msg = 'Can only concatenate list-likes containing only strings.*' + with pytest.raises(TypeError, match=msg): + s.str.cat(t, join='left') + @pytest.mark.parametrize('box', [Series, Index]) def test_str_cat_mixed_inputs(self, box): s = Index(['a', 'b', 'c', 'd'])