From a072ca0066a2928e4f70c678855d633a5fab68c0 Mon Sep 17 00:00:00 2001 From: amanlai Date: Mon, 26 Feb 2024 22:08:46 -0800 Subject: [PATCH 1/7] concat along axis=1 --- python/cudf/cudf/core/reshape.py | 144 ++++++++++++++++++++------ python/cudf/cudf/tests/test_concat.py | 36 +++++++ 2 files changed, 148 insertions(+), 32 deletions(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 2ef39e9357d..61d9e244fcf 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -120,9 +120,10 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): Parameters ---------- - objs : list of DataFrame, Series, or Index + objs : list or dictionary of DataFrame, Series, or Index axis : {0/'index', 1/'columns'}, default 0 The axis to concatenate along. + `axis=1` must be passed if a dictionary is passed. join : {'inner', 'outer'}, default 'outer' How to handle indexes on other axis (or axes). ignore_index : bool, default False @@ -229,13 +230,28 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): letter number animal name 0 a 1 bird polly 1 b 2 monkey george + + Combine a dictionary of DataFrame objects horizontally: + + >>> d = {'first': df1, 'second': df2} + >>> cudf.concat(d, axis=1) + first second + letter number letter number + 0 a 1 c 3 + 1 b 2 d 4 """ # TODO: Do we really need to have different error messages for an empty # list and a list of None? if not objs: raise ValueError("No objects to concatenate") - objs = [obj for obj in objs if obj is not None] + if isinstance(objs, dict): + objs = {k: obj for k, obj in objs.items() if obj is not None} + keys = list(objs) + objs = list(objs.values()) + else: + objs = [obj for obj in objs if obj is not None] + keys = None if not objs: raise ValueError("All objects passed were None") @@ -249,7 +265,6 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): # Return for single object if len(objs) == 1: obj = objs[0] - if ignore_index: if axis == 1: result = cudf.DataFrame._from_data( @@ -280,6 +295,16 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): else: if axis == 0: result = obj.copy() + if keys is not None: + raise NotImplementedError( + "Concatenation along axis = 0 " + "when passing a dictionary is not supported yet." + ) + + result.index = cudf.MultiIndex.from_product([ + keys, + result.index + ]) else: data = obj._data.copy(deep=True) if isinstance(obj, cudf.Series) and obj.name is None: @@ -288,6 +313,17 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): result = cudf.DataFrame._from_data( data, index=obj.index.copy(deep=True) ) + if keys is not None: + if isinstance(result, cudf.DataFrame): + k = keys[0] + result.columns = cudf.MultiIndex.from_tuples( + [(k, *c) if isinstance(c, tuple) else (k, c) for c in result.columns] + ) + + result.columns = cudf.MultiIndex.from_product([ + keys, + result.columns + ]) if isinstance(result, cudf.Series) and axis == 0: # sort has no effect for series concatted along axis 0 @@ -351,35 +387,56 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): objs = _align_objs(objs, how=join, sort=sort) df.index = objs[0].index - for o in objs: - for name, col in o._data.items(): - if name in df._data: - raise NotImplementedError( - f"A Column with duplicate name found: {name}, cuDF " - f"doesn't support having multiple columns with " - f"same names yet." - ) - if empty_inner: - # if join is inner and it contains an empty df - # we return an empty df, hence creating an empty - # column with dtype metadata retained. - df[name] = cudf.core.column.column_empty_like( - col, newsize=0 - ) - else: - df[name] = col + if keys is None: + + for o in objs: + for name, col in o._data.items(): + if name in df._data: + raise NotImplementedError( + f"A Column with duplicate name found: {name}, cuDF " + f"doesn't support having multiple columns with " + f"same names yet." + ) + if empty_inner: + # if join is inner and it contains an empty df + # we return an empty df, hence creating an empty + # column with dtype metadata retained. + df[name] = cudf.core.column.column_empty_like( + col, newsize=0 + ) + else: + df[name] = col + + result_columns = ( + objs[0] + ._data.to_pandas_index() + .append([obj._data.to_pandas_index() for obj in objs[1:]]) + .unique() + ) + + # need to create a MultiIndex column + else: + for k, o in zip(keys, objs): + for name, col in o._data.items(): + # the existing column might be multiindex + if not isinstance(name, tuple): + name = (name,) + if empty_inner: + df[(k, *name)] = cudf.core.column.column_empty_like( + col, newsize=0 + ) + else: + df[(k, *name)] = col + + # MultiIndex construction here + result_columns = cudf.MultiIndex.from_tuples(df.columns) - result_columns = ( - objs[0] - ._data.to_pandas_index() - .append([obj._data.to_pandas_index() for obj in objs[1:]]) - ) if ignore_index: # with ignore_index the column names change to numbers - df.columns = pd.RangeIndex(len(result_columns.unique())) + df.columns = pd.RangeIndex(len(result_columns)) else: - df.columns = result_columns.unique() + df.columns = result_columns if empty_inner: # if join is inner and it contains an empty df @@ -388,7 +445,14 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): return df + # If we get here, we are always concatenating along axis 0 (the rows). + if keys is not None: + raise NotImplementedError( + "Concatenation along axis = 0 " + "when passing a dictionary is not supported yet." + ) + typ = list(typs)[0] if len(typs) > 1: if allowed_typs == typs: @@ -411,11 +475,20 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): return cudf.DataFrame() elif len(objs) == 1: obj = objs[0] + # check if we need to construct a MultiIndex + if ignore_index: + index = cudf.RangeIndex(len(obj)) + else: + if keys is None: + index = obj.index.copy(deep=True) + else: + index = cudf.MultiIndex.from_product([ + keys, + obj.index + ]) result = cudf.DataFrame._from_data( data=None if join == "inner" else obj._data.copy(deep=True), - index=cudf.RangeIndex(len(obj)) - if ignore_index - else obj.index.copy(deep=True), + index=index, ) return result else: @@ -429,16 +502,23 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): ignore_index=ignore_index, # Explicitly cast rather than relying on None being falsy. sort=bool(sort), + keys=keys ) return result elif typ is cudf.Series: new_objs = [obj for obj in objs if len(obj)] if len(new_objs) == 1 and not ignore_index: - return new_objs[0] + new_srs = new_objs[0] + if keys is not None: + new_srs.index = cudf.MultiIndex.from_product([ + keys, + new_srs.index + ]) + return new_srs else: return cudf.Series._concat( - objs, axis=axis, index=None if ignore_index else True + objs, axis=axis, index=None if ignore_index else True, keys=keys ) elif typ is cudf.MultiIndex: return cudf.MultiIndex._concat(objs) diff --git a/python/cudf/cudf/tests/test_concat.py b/python/cudf/cudf/tests/test_concat.py index cdb47ea79d8..090e4b943f5 100644 --- a/python/cudf/cudf/tests/test_concat.py +++ b/python/cudf/cudf/tests/test_concat.py @@ -1889,3 +1889,39 @@ def test_concat_mixed_list_types_error(s1, s2): with pytest.raises(NotImplementedError): cudf.concat([s1, s2], ignore_index=True) + + +@pytest.mark.parametrize( + "d", + [ + { + 'first': cudf.DataFrame({'A': [1, 2], 'B': [3, 4]}), + 'second': cudf.DataFrame({'A': [5, 6], 'B': [7, 8]}) + }, + { + 'first': cudf.DataFrame({'A': [1, 2], 'B': [3, 4]}) + }, + { + 'first': cudf.DataFrame({'A': [1, 2], 'B': [3, 4]}), + 'second': cudf.DataFrame({'A': [5, 6], 'B': [7, 8]}), + 'third': cudf.DataFrame({'C': [1, 2, 3]}) + }, + { + 'first': cudf.Series([1, 2, 3]), + 'second': cudf.Series([4, 5, 6]) + } + ] +) +def test_concat_dictionary(d): + result1 = cudf.concat(d, axis=1) + expected1 = cudf.from_pandas(pd.concat({k: df.to_pandas() for k,df in d.items()}, axis=1)) + assert_eq(expected1, result1) + + + +def test_concat_dict_incorrect_type(): + d = { + 'first': cudf.Index([1, 2, 3]), + } + with pytest.raises(TypeError, match=f"cannot concatenate object of type {type(d['first'])}"): + cudf.concat(d, axis=1) \ No newline at end of file From 5dfb0b9c875e24d36e0d57c1412f4a2fd3e6299a Mon Sep 17 00:00:00 2001 From: amanlai Date: Mon, 26 Feb 2024 22:20:14 -0800 Subject: [PATCH 2/7] axis=0 is not implemented --- python/cudf/cudf/core/reshape.py | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 61d9e244fcf..3211f7ac2fe 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -300,11 +300,6 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): "Concatenation along axis = 0 " "when passing a dictionary is not supported yet." ) - - result.index = cudf.MultiIndex.from_product([ - keys, - result.index - ]) else: data = obj._data.copy(deep=True) if isinstance(obj, cudf.Series) and obj.name is None: @@ -388,7 +383,6 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): df.index = objs[0].index if keys is None: - for o in objs: for name, col in o._data.items(): if name in df._data: @@ -475,20 +469,11 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): return cudf.DataFrame() elif len(objs) == 1: obj = objs[0] - # check if we need to construct a MultiIndex - if ignore_index: - index = cudf.RangeIndex(len(obj)) - else: - if keys is None: - index = obj.index.copy(deep=True) - else: - index = cudf.MultiIndex.from_product([ - keys, - obj.index - ]) result = cudf.DataFrame._from_data( data=None if join == "inner" else obj._data.copy(deep=True), - index=index, + index=cudf.RangeIndex(len(obj)) + if ignore_index + else obj.index.copy(deep=True), ) return result else: @@ -502,23 +487,16 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): ignore_index=ignore_index, # Explicitly cast rather than relying on None being falsy. sort=bool(sort), - keys=keys ) return result elif typ is cudf.Series: new_objs = [obj for obj in objs if len(obj)] if len(new_objs) == 1 and not ignore_index: - new_srs = new_objs[0] - if keys is not None: - new_srs.index = cudf.MultiIndex.from_product([ - keys, - new_srs.index - ]) - return new_srs + return new_objs[0] else: return cudf.Series._concat( - objs, axis=axis, index=None if ignore_index else True, keys=keys + objs, axis=axis, index=None if ignore_index else True ) elif typ is cudf.MultiIndex: return cudf.MultiIndex._concat(objs) From 6d773609cca0bc24dbf96be6ebc39f93661b0884 Mon Sep 17 00:00:00 2001 From: Ashwin Srinath Date: Wed, 28 Feb 2024 10:27:08 -0500 Subject: [PATCH 3/7] Style --- python/cudf/cudf/core/reshape.py | 18 ++++++------- python/cudf/cudf/tests/test_concat.py | 37 +++++++++++++-------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 3211f7ac2fe..30daf625fd8 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -236,7 +236,7 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): >>> d = {'first': df1, 'second': df2} >>> cudf.concat(d, axis=1) first second - letter number letter number + letter number letter number 0 a 1 c 3 1 b 2 d 4 """ @@ -312,13 +312,15 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): if isinstance(result, cudf.DataFrame): k = keys[0] result.columns = cudf.MultiIndex.from_tuples( - [(k, *c) if isinstance(c, tuple) else (k, c) for c in result.columns] + [ + (k, *c) if isinstance(c, tuple) else (k, c) + for c in result.columns + ] ) - result.columns = cudf.MultiIndex.from_product([ - keys, - result.columns - ]) + result.columns = cudf.MultiIndex.from_product( + [keys, result.columns] + ) if isinstance(result, cudf.Series) and axis == 0: # sort has no effect for series concatted along axis 0 @@ -407,7 +409,7 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): .append([obj._data.to_pandas_index() for obj in objs[1:]]) .unique() ) - + # need to create a MultiIndex column else: for k, o in zip(keys, objs): @@ -425,7 +427,6 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): # MultiIndex construction here result_columns = cudf.MultiIndex.from_tuples(df.columns) - if ignore_index: # with ignore_index the column names change to numbers df.columns = pd.RangeIndex(len(result_columns)) @@ -439,7 +440,6 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): return df - # If we get here, we are always concatenating along axis 0 (the rows). if keys is not None: raise NotImplementedError( diff --git a/python/cudf/cudf/tests/test_concat.py b/python/cudf/cudf/tests/test_concat.py index 090e4b943f5..86d1855a3a5 100644 --- a/python/cudf/cudf/tests/test_concat.py +++ b/python/cudf/cudf/tests/test_concat.py @@ -1895,33 +1895,32 @@ def test_concat_mixed_list_types_error(s1, s2): "d", [ { - 'first': cudf.DataFrame({'A': [1, 2], 'B': [3, 4]}), - 'second': cudf.DataFrame({'A': [5, 6], 'B': [7, 8]}) + "first": cudf.DataFrame({"A": [1, 2], "B": [3, 4]}), + "second": cudf.DataFrame({"A": [5, 6], "B": [7, 8]}), }, + {"first": cudf.DataFrame({"A": [1, 2], "B": [3, 4]})}, { - 'first': cudf.DataFrame({'A': [1, 2], 'B': [3, 4]}) + "first": cudf.DataFrame({"A": [1, 2], "B": [3, 4]}), + "second": cudf.DataFrame({"A": [5, 6], "B": [7, 8]}), + "third": cudf.DataFrame({"C": [1, 2, 3]}), }, - { - 'first': cudf.DataFrame({'A': [1, 2], 'B': [3, 4]}), - 'second': cudf.DataFrame({'A': [5, 6], 'B': [7, 8]}), - 'third': cudf.DataFrame({'C': [1, 2, 3]}) - }, - { - 'first': cudf.Series([1, 2, 3]), - 'second': cudf.Series([4, 5, 6]) - } - ] + {"first": cudf.Series([1, 2, 3]), "second": cudf.Series([4, 5, 6])}, + ], ) def test_concat_dictionary(d): result1 = cudf.concat(d, axis=1) - expected1 = cudf.from_pandas(pd.concat({k: df.to_pandas() for k,df in d.items()}, axis=1)) + expected1 = cudf.from_pandas( + pd.concat({k: df.to_pandas() for k, df in d.items()}, axis=1) + ) assert_eq(expected1, result1) - def test_concat_dict_incorrect_type(): d = { - 'first': cudf.Index([1, 2, 3]), - } - with pytest.raises(TypeError, match=f"cannot concatenate object of type {type(d['first'])}"): - cudf.concat(d, axis=1) \ No newline at end of file + "first": cudf.Index([1, 2, 3]), + } + with pytest.raises( + TypeError, + match=f"cannot concatenate object of type {type(d['first'])}", + ): + cudf.concat(d, axis=1) From f950f358d06b564c2a7e043205113a3e80d9d44d Mon Sep 17 00:00:00 2001 From: Ashwin Srinath <3190405+shwina@users.noreply.github.com> Date: Wed, 28 Feb 2024 13:40:33 -0500 Subject: [PATCH 4/7] Update python/cudf/cudf/core/reshape.py --- python/cudf/cudf/core/reshape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 30daf625fd8..f6b312b3130 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -425,7 +425,7 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): df[(k, *name)] = col # MultiIndex construction here - result_columns = cudf.MultiIndex.from_tuples(df.columns) + result_columns = cudf.MultiIndex.from_tuples(df._column_names) if ignore_index: # with ignore_index the column names change to numbers From 7925ed62af348c1b15b054181d5a26a151baf63a Mon Sep 17 00:00:00 2001 From: Ashwin Srinath <3190405+shwina@users.noreply.github.com> Date: Wed, 28 Feb 2024 14:58:39 -0500 Subject: [PATCH 5/7] Update python/cudf/cudf/core/reshape.py --- python/cudf/cudf/core/reshape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index f6b312b3130..9e553600275 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -314,7 +314,7 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): result.columns = cudf.MultiIndex.from_tuples( [ (k, *c) if isinstance(c, tuple) else (k, c) - for c in result.columns + for c in result._column_names ] ) From a3138cde7dce5b56baa032717af52cc5350af8e8 Mon Sep 17 00:00:00 2001 From: Ashwin Srinath <3190405+shwina@users.noreply.github.com> Date: Thu, 29 Feb 2024 06:44:50 -0500 Subject: [PATCH 6/7] Update python/cudf/cudf/core/reshape.py --- python/cudf/cudf/core/reshape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 9e553600275..cae3dde97be 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -319,7 +319,7 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): ) result.columns = cudf.MultiIndex.from_product( - [keys, result.columns] + [keys, result._column_names] ) if isinstance(result, cudf.Series) and axis == 0: From eaea83fb2e65e2e12bdd14ad2b36b44f0457ae66 Mon Sep 17 00:00:00 2001 From: amanlai Date: Thu, 29 Feb 2024 11:09:24 -0800 Subject: [PATCH 7/7] fix column mismatch when there are only series or if there is a single object (pulled unnecessary changes in previous commit) --- python/cudf/cudf/core/reshape.py | 73 +++++++++++++++------------ python/cudf/cudf/tests/test_concat.py | 4 ++ 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index cae3dde97be..aeeee6bcba8 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -262,6 +262,24 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): f'`axis` must be 0 / "index" or 1 / "columns", got: {axis}' ) + # Retrieve the base types of `objs`. In order to support sub-types + # and object wrappers, we use `isinstance()` instead of comparing + # types directly + typs = set() + for o in objs: + if isinstance(o, cudf.MultiIndex): + typs.add(cudf.MultiIndex) + elif isinstance(o, cudf.BaseIndex): + typs.add(type(o)) + elif isinstance(o, cudf.DataFrame): + typs.add(cudf.DataFrame) + elif isinstance(o, cudf.Series): + typs.add(cudf.Series) + else: + raise TypeError(f"cannot concatenate object of type {type(o)}") + + allowed_typs = {cudf.Series, cudf.DataFrame} + # Return for single object if len(objs) == 1: obj = objs[0] @@ -301,6 +319,11 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): "when passing a dictionary is not supported yet." ) else: + o_typ = typs.pop() + if o_typ not in allowed_typs: + raise TypeError( + f"cannot concatenate object of type {o_typ}" + ) data = obj._data.copy(deep=True) if isinstance(obj, cudf.Series) and obj.name is None: # If the Series has no name, pandas renames it to 0. @@ -318,34 +341,12 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): ] ) - result.columns = cudf.MultiIndex.from_product( - [keys, result._column_names] - ) - if isinstance(result, cudf.Series) and axis == 0: # sort has no effect for series concatted along axis 0 return result else: return result.sort_index(axis=(1 - axis)) if sort else result - # Retrieve the base types of `objs`. In order to support sub-types - # and object wrappers, we use `isinstance()` instead of comparing - # types directly - typs = set() - for o in objs: - if isinstance(o, cudf.MultiIndex): - typs.add(cudf.MultiIndex) - elif isinstance(o, cudf.BaseIndex): - typs.add(type(o)) - elif isinstance(o, cudf.DataFrame): - typs.add(cudf.DataFrame) - elif isinstance(o, cudf.Series): - typs.add(cudf.Series) - else: - raise TypeError(f"cannot concatenate object of type {type(o)}") - - allowed_typs = {cudf.Series, cudf.DataFrame} - # when axis is 1 (column) we can concat with Series and Dataframes if axis == 1: if not typs.issubset(allowed_typs): @@ -384,6 +385,10 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): objs = _align_objs(objs, how=join, sort=sort) df.index = objs[0].index + # if the dictionary consists of only dictionaries + # it must be handled differently + only_series = len(typs) == 1 and cudf.Series in typs + if keys is None: for o in objs: for name, col in o._data.items(): @@ -414,24 +419,30 @@ def concat(objs, axis=0, join="outer", ignore_index=False, sort=None): else: for k, o in zip(keys, objs): for name, col in o._data.items(): - # the existing column might be multiindex - if not isinstance(name, tuple): - name = (name,) + # if only series, then only keep keys as column labels + # if the existing column is multiindex, prepend it + # to handle cases where dfs and srs are concatenated, + # explicitly cast int column labels into str + if only_series: + col_label = k + elif isinstance(name, tuple): + col_label = (k, *name) + else: + col_label = (k, str(name)) if empty_inner: - df[(k, *name)] = cudf.core.column.column_empty_like( + df[col_label] = cudf.core.column.column_empty_like( col, newsize=0 ) else: - df[(k, *name)] = col - - # MultiIndex construction here - result_columns = cudf.MultiIndex.from_tuples(df._column_names) + df[col_label] = col if ignore_index: # with ignore_index the column names change to numbers df.columns = pd.RangeIndex(len(result_columns)) + elif not only_series: + df.columns = cudf.MultiIndex.from_tuples(df._column_names) else: - df.columns = result_columns + pass if empty_inner: # if join is inner and it contains an empty df diff --git a/python/cudf/cudf/tests/test_concat.py b/python/cudf/cudf/tests/test_concat.py index 86d1855a3a5..72c1efc4d02 100644 --- a/python/cudf/cudf/tests/test_concat.py +++ b/python/cudf/cudf/tests/test_concat.py @@ -1905,6 +1905,10 @@ def test_concat_mixed_list_types_error(s1, s2): "third": cudf.DataFrame({"C": [1, 2, 3]}), }, {"first": cudf.Series([1, 2, 3]), "second": cudf.Series([4, 5, 6])}, + { + "first": cudf.DataFrame({"A": [1, 2], "B": [3, 4]}), + "second": cudf.Series([5, 6], name="C"), + }, ], ) def test_concat_dictionary(d):