@@ -2264,6 +2264,91 @@ def _is_null_mapping_key(value):
22642264 return bool (pd .isna (value ))
22652265
22662266
2267+ def _apply_value_mapping_to_series (
2268+ series : pd .Series ,
2269+ mapping : Mapping [Any , Any ],
2270+ * ,
2271+ operation : str ,
2272+ ) -> pd .Series :
2273+ """Apply scalar value mappings to a Series, including null-like keys."""
2274+ null_key_present = False
2275+ null_replacement = None
2276+ normalized_mapping = {}
2277+
2278+ for k , v in mapping .items ():
2279+ if _is_null_mapping_key (k ):
2280+ null_key_present = True
2281+ null_replacement = v
2282+ elif is_scalar (k ) and not isinstance (
2283+ k , (tuple , list , np .ndarray , pd .Series , pd .Index )
2284+ ):
2285+ normalized_mapping [k ] = v
2286+ else :
2287+ raise TypeError (
2288+ f"{ operation } () does not support non-scalar mapping keys. "
2289+ f"Got key { k !r} of type '{ type (k ).__name__ } '. "
2290+ f"Only scalar values (str, int, float, bool) and null-like keys "
2291+ f"(None, float('nan'), pd.NA, pd.NaT) are supported."
2292+ )
2293+
2294+ original_null_mask = series .isna () if null_key_present else None
2295+ result = series .replace (normalized_mapping ) if normalized_mapping else series
2296+ if null_key_present :
2297+ result = result .where (~ original_null_mask , null_replacement )
2298+ return result
2299+
2300+
2301+ def map_values (
2302+ frame : ArFrame | pd .DataFrame ,
2303+ mapping : Mapping [Any , Any ],
2304+ subset : Sequence [str ] | None = None ,
2305+ ) -> ArFrame | pd .DataFrame :
2306+ """Map selected column values through a user-provided mapping.
2307+
2308+ Values not present in ``mapping`` are preserved. When ``subset`` is
2309+ provided, only those columns are transformed; otherwise the mapping is
2310+ applied to all columns.
2311+ """
2312+ frame , is_arframe = _validate_frame (frame , allow_pandas = True )
2313+ mapping = _validate_mapping (
2314+ mapping ,
2315+ argument_name = "mapping" ,
2316+ allow_empty = False ,
2317+ non_mapping_message = (
2318+ "mapping must be a dict-like mapping of {old_value: new_value}, "
2319+ f"not { type (mapping ).__name__ } ."
2320+ ),
2321+ )
2322+
2323+ df = to_pandas (frame ) if is_arframe else frame .copy (deep = False )
2324+ if subset is None :
2325+ target_columns = list (df .columns )
2326+ else :
2327+ target_columns = _validate_existing_column_sequence (
2328+ subset ,
2329+ available_columns = df .columns ,
2330+ argument_name = "subset" ,
2331+ reject_duplicates = True ,
2332+ missing_error = ValueError ,
2333+ missing_message = lambda missing , available : (
2334+ f"Unknown columns in subset: { missing } . Available: { available } "
2335+ ),
2336+ )
2337+
2338+ if not target_columns :
2339+ return frame if is_arframe else df
2340+
2341+ result_df = df .copy (deep = False )
2342+ for column in target_columns :
2343+ result_df [column ] = _apply_value_mapping_to_series (
2344+ result_df [column ],
2345+ mapping ,
2346+ operation = "map_values" ,
2347+ )
2348+
2349+ return from_pandas (result_df ) if is_arframe else result_df
2350+
2351+
22672352def replace_values (
22682353 frame : ArFrame | pd .DataFrame ,
22692354 mapping : dict ,
@@ -2328,50 +2413,19 @@ def replace_values(
23282413 f"Column '{ column } ' not found. Available columns: { available } "
23292414 )
23302415
2331- # Normalize mapping and separate null-key handling because NaN != NaN
2332- null_key_present = False
2333- null_replacement = None
2334- normalized_mapping = {}
2335-
2336- for k , v in mapping .items ():
2337- # Handle scalar null-like keys safely without evaluating
2338- # tuple/list/array-like objects in boolean context.
2339- if _is_null_mapping_key (k ):
2340- null_key_present = True
2341- null_replacement = v
2342- # Exclude tuple/list/ndarray/series/index keys which pandas.replace
2343- # does not support and can raise confusing errors (e.g. operand
2344- # length mismatch). Treat strings and true scalars as valid keys.
2345- elif is_scalar (k ) and not isinstance (
2346- k , (tuple , list , np .ndarray , pd .Series , pd .Index )
2347- ):
2348- normalized_mapping [k ] = v
2349- else :
2350- raise TypeError (
2351- f"replace_values() does not support non-scalar mapping keys. "
2352- f"Got key { k !r} of type '{ type (k ).__name__ } '. "
2353- f"Only scalar values (str, int, float, bool) and null-like keys "
2354- f"(None, float('nan'), pd.NA, pd.NaT) are supported."
2355- )
2356-
23572416 if column :
2358- s = df [column ]
2359- original_null_mask = s .isna () if null_key_present else None
2360- if normalized_mapping :
2361- s = s .replace (normalized_mapping )
2362- if null_key_present :
2363- # Replace only values that were already null before replacement so
2364- # null-valued mapping results remain real nulls.
2365- s = s .where (~ original_null_mask , null_replacement )
2366- df [column ] = s
2417+ df [column ] = _apply_value_mapping_to_series (
2418+ df [column ],
2419+ mapping ,
2420+ operation = "replace_values" ,
2421+ )
23672422 else :
2368- original_null_mask = df .isna () if null_key_present else None
2369- if normalized_mapping :
2370- df = df .replace (normalized_mapping )
2371- if null_key_present :
2372- # Replace only values that were already null before replacement so
2373- # null-valued mapping results remain real nulls.
2374- df = df .where (~ original_null_mask , null_replacement )
2423+ for current_column in df .columns :
2424+ df [current_column ] = _apply_value_mapping_to_series (
2425+ df [current_column ],
2426+ mapping ,
2427+ operation = "replace_values" ,
2428+ )
23752429
23762430 return from_pandas (df ) if is_arframe else df
23772431
0 commit comments