@@ -200,7 +200,7 @@ def read_dictionary_page(file_obj, schema_helper, page_header, column_metadata,
200200
201201def read_data_page_v2 (infile , schema_helper , se , data_header2 , cmd ,
202202 dic , assign , num , use_cat , file_offset , ph , idx = None ,
203- selfmade = False , row_filter = None ):
203+ selfmade = False , row_filter = None , remap_array = None ):
204204 """
205205 :param infile: open file
206206 :param schema_helper:
@@ -211,6 +211,7 @@ def read_data_page_v2(infile, schema_helper, se, data_header2, cmd,
211211 :param assign: output array (all of it)
212212 :param num: offset, rows so far
213213 :param use_cat: output is categorical?
214+ :param remap_array: array for remapping categorical indices
214215 :return: None
215216
216217 test data "/Users/mdurant/Downloads/datapage_v2.snappy.parquet"
@@ -338,6 +339,9 @@ def read_data_page_v2(infile, schema_helper, se, data_header2, cmd,
338339 if bit_width in [8 , 16 , 32 ] and selfmade :
339340 # special fastpath for cats
340341 outbytes = raw_bytes [pagefile .tell ():]
342+ if remap_array is not None :
343+ # Apply remapping to outbytes.
344+ outbytes = remap_array [outbytes ]
341345 if len (outbytes ) == assign [num :num + data_header2 .num_values ].nbytes :
342346 assign [num :num + data_header2 .num_values ].view ('uint8' )[row_filter ] = outbytes [row_filter ]
343347 else :
@@ -358,6 +362,9 @@ def read_data_page_v2(infile, schema_helper, se, data_header2, cmd,
358362 encoding .NumpyIO (assign [num :num + data_header2 .num_values ].view ('uint8' )),
359363 itemsize = bit_width
360364 )
365+ if remap_array is not None :
366+ # Apply remapping after reading
367+ assign [num :num + data_header2 .num_values ] = remap_array [assign [num :num + data_header2 .num_values ]]
361368 else :
362369 temp = np .empty (data_header2 .num_values , assign .dtype )
363370 encoding .read_rle_bit_packed_hybrid (
@@ -367,6 +374,8 @@ def read_data_page_v2(infile, schema_helper, se, data_header2, cmd,
367374 encoding .NumpyIO (temp .view ('uint8' )),
368375 itemsize = bit_width
369376 )
377+ if remap_array is not None :
378+ temp = remap_array [temp ]
370379 if not nullable :
371380 assign [num :num + data_header2 .num_values ][nulls [row_filter ]] = None
372381 assign [num :num + data_header2 .num_values ][~ nulls [row_filter ]] = temp [row_filter ]
@@ -429,7 +438,7 @@ def read_data_page_v2(infile, schema_helper, se, data_header2, cmd,
429438
430439def read_col (column , schema_helper , infile , use_cat = False ,
431440 selfmade = False , assign = None , catdef = None ,
432- row_filter = None ):
441+ row_filter = None , global_cats = None ):
433442 """Using the given metadata, read one column in one row-group.
434443
435444 Parameters
@@ -443,10 +452,20 @@ def read_col(column, schema_helper, infile, use_cat=False,
443452 use_cat: bool (False)
444453 If this column is encoded throughout with dict encoding, give back
445454 a pandas categorical column; otherwise, decode to values
455+ selfmade: bool (False)
456+ If data created by fastparquet
457+ assign: numpy array
458+ Where to store the result
459+ catdef: pandas.Categorical or CategoricalDtype
460+ If reading a categorical column, the categorical definition (categories and
461+ ordering).
446462 row_filter: bool array or None
447463 if given, selects which of the values read are to be written
448464 into the output. Effectively implies NULLs, even for a required
449465 column.
466+ global_cats: dict or None
467+ Optional dictionary for storing global categorical values across row groups.
468+ Format: {col_path: array}
450469 """
451470 cmd = column .meta_data
452471 try :
@@ -480,6 +499,15 @@ def read_col(column, schema_helper, infile, use_cat=False,
480499 row_idx = [0 ] # map/list objects
481500 dic = None
482501 index_off = 0 # how far through row_filter we are
502+
503+ # Initialize tracking variables for categorical dictionaries
504+ # Only set up global dictionary tracking if using categorical and global_cats is provided
505+ remap_dict = {} # Dictionary for collecting mappings
506+ if use_cat and global_cats is not None :
507+ path_str = "." .join (cmd .path_in_schema )
508+ # Register this column in global_cats if not already present
509+ if path_str not in global_cats :
510+ global_cats [path_str ] = None
483511
484512 while num < rows :
485513 off = infile .tell ()
@@ -497,7 +525,44 @@ def read_col(column, schema_helper, infile, use_cat=False,
497525 ddt = [kv .value .decode () for kv in (cmd .key_value_metadata or [])
498526 if kv .key == b"label_dtype" ]
499527 ddt = ddt [0 ] if ddt else None
500- catdef ._set_categories (pd .Index (dic , dtype = ddt ), fastpath = True )
528+
529+ if global_cats is not None :
530+ # Check if categorical values are consistent with global dictionary.
531+ if global_cats [path_str ] is None :
532+ # This is the first dictionary for this column, save it as global
533+ global_cats [path_str ] = dic
534+ else :
535+ # Dictionary already defined for this column, check for inconsistency.
536+ global_dict = global_cats [path_str ]
537+ new_values = []
538+ # Build remap_dict in a single comprehension,
539+ # appending new values to new_values at the same time:
540+ # - Use walrus operator (:=) to store found_idx from global_dict lookup.
541+ # - When found_idx is -1, append val to new_values and use its new position.
542+ # - Only include indices that need remapping (found_idx != i).
543+ remap_dict = {i : (len (global_dict ) + len (new_values ) - 1 )
544+ if found_idx == - 1 and not new_values .append (val ) else found_idx
545+ for (i ,), val in np .ndenumerate (dic )
546+ if (found_idx := next ((j
547+ for (j ,), gval in np .ndenumerate (global_dict )
548+ if val == gval ), - 1 )) != i
549+ }
550+ if remap_dict :
551+ # If any remapping is needed, create a complete remap array.
552+ # Initialize with identity mapping (no change)
553+ remap_array = np .arange (len (dic ), dtype = np .int32 )
554+ # Update indices that need remapping
555+ remap_array [list (remap_dict )] = list (remap_dict .values ())
556+ if new_values :
557+ # Add new values to global dictionary
558+ global_cats [path_str ] = np .append (global_dict , new_values )
559+ # Update categories
560+ catdef ._set_categories (pd .Index (global_cats [path_str ], dtype = ddt ), fastpath = True )
561+
562+ # Normal case - always set categories for this dictionary
563+ if global_cats is None or not remap_dict :
564+ catdef ._set_categories (pd .Index (dic , dtype = ddt ), fastpath = True )
565+
501566 if np .iinfo (assign .dtype ).max < len (dic ):
502567 raise RuntimeError ('Assigned array dtype (%s) cannot accommodate '
503568 'number of category labels (%i)' %
@@ -509,7 +574,7 @@ def read_col(column, schema_helper, infile, use_cat=False,
509574 if ph .type == parquet_thrift .PageType .DATA_PAGE_V2 :
510575 num += read_data_page_v2 (infile , schema_helper , se , ph .data_page_header_v2 , cmd ,
511576 dic , assign , num , use_cat , off , ph , row_idx , selfmade = selfmade ,
512- row_filter = row_filter )
577+ row_filter = row_filter , remap_array = remap_array if remap_dict else None )
513578 continue
514579 if (selfmade and hasattr (cmd , 'statistics' ) and
515580 getattr (cmd .statistics , 'null_count' , 1 ) == 0 ):
@@ -563,6 +628,9 @@ def read_col(column, schema_helper, infile, use_cat=False,
563628 part [defi == max_defi ] = dic [val ]
564629 elif not use_cat :
565630 part [defi == max_defi ] = convert (val , se , dtype = assign .dtype )
631+ elif remap_dict :
632+ # Apply remapping of categorical codes
633+ part [defi == max_defi ] = remap_array [val ]
566634 else :
567635 part [defi == max_defi ] = val
568636 else :
@@ -582,14 +650,18 @@ def read_col(column, schema_helper, infile, use_cat=False,
582650 piece [:] = dic [val ]
583651 elif not use_cat :
584652 piece [:] = convert (val , se , dtype = assign .dtype )
653+ elif remap_dict :
654+ # Apply remapping of categorical codes
655+ piece [:] = remap_array [val ]
585656 else :
586657 piece [:] = val
587658
588659 num += len (defi ) if defi is not None else len (val )
589660
590661
591662def read_row_group_arrays (file , rg , columns , categories , schema_helper , cats ,
592- selfmade = False , assign = None , row_filter = False ):
663+ selfmade = False , assign = None , row_filter = False ,
664+ global_cats = None ):
593665 """
594666 Read a row group and return as a dict of arrays
595667
@@ -615,7 +687,7 @@ def read_row_group_arrays(file, rg, columns, categories, schema_helper, cats,
615687 read_col (column , schema_helper , file , use_cat = name + '-catdef' in out ,
616688 selfmade = selfmade , assign = out [name ],
617689 catdef = out .get (name + '-catdef' , None ),
618- row_filter = row_filter )
690+ row_filter = row_filter , global_cats = global_cats )
619691
620692 if _is_map_like (schema_helper , column ):
621693 # TODO: could be done in fast loop in _assemble_objects?
@@ -634,15 +706,17 @@ def read_row_group_arrays(file, rg, columns, categories, schema_helper, cats,
634706
635707def read_row_group (file , rg , columns , categories , schema_helper , cats ,
636708 selfmade = False , index = None , assign = None ,
637- scheme = 'hive' , partition_meta = None , row_filter = False ):
709+ scheme = 'hive' , partition_meta = None , row_filter = False ,
710+ global_cats = None ):
638711 """
639712 Access row-group in a file and read some columns into a data-frame.
640713 """
641714 partition_meta = partition_meta or {}
642715 if assign is None :
643716 raise RuntimeError ('Going with pre-allocation!' )
644717 read_row_group_arrays (file , rg , columns , categories , schema_helper ,
645- cats , selfmade , assign = assign , row_filter = row_filter )
718+ cats , selfmade , assign = assign , row_filter = row_filter ,
719+ global_cats = global_cats )
646720
647721 for cat in cats :
648722 if cat not in assign :
0 commit comments