@@ -991,6 +991,9 @@ def map(
991991 TypeError
992992 If `mapping` is not of type `dict` or `Series`.
993993 If `values` is not of type `pdarray`, `Categorical`, or `Strings`.
994+ ValueError
995+ If a mapping with tuple keys has inconsistent lengths, or if a MultiIndex
996+ mapping has a different number of levels than the GroupBy keys.
994997
995998 Examples
996999 --------
@@ -1012,29 +1015,97 @@ def map(
10121015 from arkouda .numpy .pdarraysetops import in1d
10131016 from arkouda .numpy .strings import Strings
10141017 from arkouda .pandas .categorical import Categorical
1018+ from arkouda .pandas .index import MultiIndex
10151019
10161020 keys = values
10171021 gb = GroupBy (keys , dropna = False )
10181022 gb_keys = gb .unique_keys
10191023
1024+ # helper: number of unique keys (works for single key or tuple-of-keys)
1025+ nuniq = gb_keys [0 ].size if isinstance (gb_keys , tuple ) else gb_keys .size
1026+
1027+ # Fast-path: empty mapping => everything is missing
1028+ if (isinstance (mapping , dict ) and len (mapping ) == 0 ) or (
1029+ isinstance (mapping , Series ) and len (mapping .index ) == 0
1030+ ):
1031+ if not isinstance (values , (Strings , Categorical )):
1032+ fillvals = full (nuniq , np .nan , values .dtype )
1033+ else :
1034+ fillvals = full (nuniq , "null" )
1035+ return broadcast (gb .segments , fillvals , permutation = gb .permutation )
1036+
10201037 if isinstance (mapping , dict ):
1021- mapping = Series ([array (list (mapping .keys ())), array (list (mapping .values ()))])
1038+ # Build mapping as a Series with an Index/MultiIndex (avoid rank>1 arrays)
1039+ m_keys = list (mapping .keys ())
1040+ m_vals = list (mapping .values ())
1041+
1042+ k0 = m_keys [0 ]
1043+ if isinstance (k0 , tuple ):
1044+ # validate tuple keys
1045+ if not all (isinstance (k , tuple ) for k in m_keys ):
1046+ raise TypeError ("Mixed key types in mapping dict (tuple and non-tuple)." )
1047+ n = len (k0 )
1048+ if not all (len (k ) == n for k in m_keys ):
1049+ raise ValueError ("All tuple keys in mapping dict must have the same length." )
1050+
1051+ cols = list (zip (* m_keys )) # transpose list[tuple] -> list[level]
1052+ idx = MultiIndex ([array (col ) for col in cols ])
1053+ mapping = Series (array (m_vals ), index = idx )
1054+ else :
1055+ mapping = Series (array (m_vals ), index = array (m_keys ))
10221056
10231057 if isinstance (mapping , Series ):
1024- xtra_keys = gb_keys [in1d (gb_keys , mapping .index .values , invert = True )]
1058+ # Normalize mapping index keys into a "groupable" (single array OR tuple-of-arrays)
1059+ mindex = mapping .index
1060+ if isinstance (mindex , MultiIndex ):
1061+ mkeys = tuple (mindex .index )
1062+ else :
1063+ mkeys = mindex .values
10251064
1026- if xtra_keys .size > 0 :
1027- if not isinstance (mapping .values , (Strings , Categorical )):
1028- nans = full (xtra_keys .size , np .nan , mapping .values .dtype )
1029- else :
1030- nans = full (xtra_keys .size , "null" )
1065+ if isinstance (gb_keys , tuple ) and isinstance (mkeys , tuple ):
1066+ if len (gb_keys ) != len (mkeys ):
1067+ raise ValueError (
1068+ f"Mapping MultiIndex has { len (mkeys )} levels but GroupBy has { len (gb_keys )} keys"
1069+ )
1070+
1071+ mask = in1d (gb_keys , mkeys , invert = True )
1072+
1073+ # Compute extra keys + extra size without mixing tuple/non-tuple assignments
1074+ if isinstance (gb_keys , tuple ):
1075+ xtra_keys_t = tuple (k [mask ] for k in gb_keys )
1076+ xtra_size = xtra_keys_t [0 ].size if len (xtra_keys_t ) > 0 else 0
1077+
1078+ if xtra_size > 0 :
1079+ if not isinstance (mapping .values , (Strings , Categorical )):
1080+ nans = full (xtra_size , np .nan , mapping .values .dtype )
1081+ else :
1082+ nans = full (xtra_size , "null" )
1083+
1084+ # Convert any categorical levels to strings, level-by-level
1085+ xtra_keys_t = tuple (
1086+ k .to_strings () if isinstance (k , Categorical ) else k for k in xtra_keys_t
1087+ )
1088+
1089+ xtra_series = Series (nans , index = MultiIndex (list (xtra_keys_t )))
1090+ mapping = Series .concat ([mapping , xtra_series ])
1091+
1092+ else :
1093+ xtra_keys_s = gb_keys [mask ]
1094+ xtra_size = xtra_keys_s .size
1095+
1096+ if xtra_size > 0 :
1097+ if not isinstance (mapping .values , (Strings , Categorical )):
1098+ nans = full (xtra_size , np .nan , mapping .values .dtype )
1099+ else :
1100+ nans = full (xtra_size , "null" )
10311101
1032- if isinstance (xtra_keys , Categorical ):
1033- xtra_keys = xtra_keys .to_strings ()
1102+ if isinstance (xtra_keys_s , Categorical ):
1103+ xtra_keys_s = xtra_keys_s .to_strings ()
10341104
1035- xtra_series = Series (nans , index = xtra_keys )
1036- mapping = Series .concat ([mapping , xtra_series ])
1105+ xtra_series = Series (nans , index = xtra_keys_s )
1106+ mapping = Series .concat ([mapping , xtra_series ])
10371107
1108+ # Align mapping to gb_keys
10381109 if isinstance (gb_keys , Categorical ):
10391110 mapping = mapping [gb_keys .to_strings ()]
10401111 else :
0 commit comments