1515from fastparquet .util import (default_open , default_remove , ParquetException , val_to_num ,
1616 ops , ensure_bytes , ensure_str , check_column_names , metadata_from_many ,
1717 ex_from_sep , _strip_path_tail , get_fs , PANDAS_VERSION , join_path )
18+ from fastparquet .encoding import _HAVE_ARROW , _USE_ARROW_STRINGS
19+
20+ if _HAVE_ARROW :
21+ import pyarrow as _pa
1822
1923
2024# Find in names of partition files the integer matching "**part.*.parquet",
@@ -102,11 +106,16 @@ class ParquetFile(object):
102106 _pdm = None
103107 _kvm = None
104108 _categories = None
109+ _user_dtypes = False # whether _base_dtype was user-supplied
105110
106111 def __init__ (self , fn , verify = False , open_with = default_open , root = False ,
107112 sep = None , fs = None , pandas_nulls = True , dtypes = None ):
108113 self .pandas_nulls = pandas_nulls
109114 self ._base_dtype = dtypes
115+ # Track whether _base_dtype was user-supplied (True) or auto-inferred (False).
116+ # This is needed so that an auto-inferred dtype('O') for a UTF-8 column
117+ # does not prevent the arrow string path from activating.
118+ self ._user_dtypes = dtypes is not None
110119 self .tz = None
111120 self ._columns_dtype = None
112121 if open_with is default_open and fs is None :
@@ -346,7 +355,7 @@ def row_group_filename(self, rg):
346355
347356 def read_row_group_file (self , rg , columns , categories , index = None ,
348357 assign = None , partition_meta = None , row_filter = False ,
349- infile = None ):
358+ infile = None , arrow_string_columns = None ):
350359 """ Open file for reading, and process it as a row-group
351360
352361 assign is None if this method is called directly (not from to_pandas),
@@ -389,7 +398,7 @@ def read_row_group_file(self, rg, columns, categories, index=None,
389398 f , rg , columns , categories , self .schema , self .cats ,
390399 selfmade = self .selfmade , index = index ,
391400 assign = assign , scheme = self .file_scheme , partition_meta = partition_meta ,
392- row_filter = row_filter
401+ row_filter = row_filter , arrow_string_columns = arrow_string_columns ,
393402 )
394403 if ret :
395404 return df
@@ -665,26 +674,26 @@ def _column_filter(self, df, filters):
665674 if name in self .cats :
666675 continue
667676 if op == 'in' :
668- out |= df [name ].isin (val ). values
677+ out |= np . asarray ( df [name ].isin (val ), dtype = bool )
669678 elif op == "not in" :
670- out |= ~ df [name ].isin (val ). values
679+ out |= ~ np . asarray ( df [name ].isin (val ), dtype = bool )
671680 elif op in ops :
672- out |= ops [op ](df [name ], val ). values
681+ out |= np . asarray ( ops [op ](df [name ], val ), dtype = bool )
673682 elif op == "~" :
674- out |= ~ df [name ]. values
683+ out |= ~ np . asarray ( df [name ], dtype = bool )
675684 else :
676685 and_part = np .ones (len (df ), dtype = bool )
677686 for name , op , val in or_part :
678687 if name in self .cats :
679688 continue
680689 if op == 'in' :
681- and_part &= df [name ].isin (val ). values
690+ and_part &= np . asarray ( df [name ].isin (val ), dtype = bool )
682691 elif op == "not in" :
683- and_part &= ~ df [name ].isin (val ). values
692+ and_part &= ~ np . asarray ( df [name ].isin (val ), dtype = bool )
684693 elif op in ops :
685- and_part &= ops [op ](df [name ]. values , val )
694+ and_part &= np . asarray ( ops [op ](df [name ], val ), dtype = bool )
686695 elif op == "~" :
687- and_part &= ~ df [name ]. values
696+ and_part &= ~ np . asarray ( df [name ], dtype = bool )
688697 out |= and_part
689698 return out
690699
@@ -771,6 +780,44 @@ def to_pandas(self, columns=None, categories=None, filters=[],
771780 import json
772781 df .attrs = json .loads (self .key_value_metadata ["PANDAS_ATTRS" ])
773782
783+ # Build per-column accumulators for arrow string columns.
784+ # Object-dtype columns whose parquet schema marks them as UTF-8 will
785+ # use the fast Cython path that builds ArrowStringArray directly.
786+ # Skip columns whose dtype was explicitly requested as object (via dtypes=)
787+ # to honour the caller's preference and to handle schema-evolution cases.
788+ arrow_string_columns = {}
789+ if _HAVE_ARROW and _USE_ARROW_STRINGS :
790+ _cats = categories or {}
791+ # Resolve user-supplied dtype overrides only (not auto-inferred ones).
792+ # Auto-inferred dtypes always show dtype('O') for UTF-8 columns, so
793+ # including them would block the arrow path for all string columns.
794+ _user_dtypes = {}
795+ if self ._user_dtypes and self ._base_dtype :
796+ _user_dtypes .update (self ._base_dtype )
797+ if dtypes is not None :
798+ _user_dtypes .update (dtypes )
799+ # Detect schema-evolution columns: absent from some row groups.
800+ # For those, the existing object-array fallback is more reliable.
801+ _schema_evo_cols = set ()
802+ if len (rgs ) > 1 :
803+ for rg in rgs :
804+ rg_cols = {'.' .join (c .meta_data .path_in_schema )
805+ for c in rg .columns }
806+ for col in df .columns :
807+ if col not in rg_cols :
808+ _schema_evo_cols .add (col )
809+ for col in df .columns :
810+ if (df [col ].dtype == np .dtype ('O' )
811+ and col not in _cats
812+ and col not in _schema_evo_cols
813+ and _user_dtypes .get (col ) != np .dtype ('O' )):
814+ arrow_string_columns [col ] = []
815+ # Also check index columns that are object dtype
816+ if hasattr (df .index , 'dtype' ) and df .index .dtype == np .dtype ('O' ):
817+ idx_name = df .index .name
818+ if idx_name and idx_name in columns and idx_name not in _schema_evo_cols :
819+ arrow_string_columns [idx_name ] = []
820+
774821 start = 0
775822 if self .file_scheme == 'simple' :
776823 infile = self .open (self .fn , 'rb' )
@@ -789,8 +836,27 @@ def to_pandas(self, columns=None, categories=None, filters=[],
789836 for (name , v ) in views .items ()}
790837 self .read_row_group_file (rg , columns , categories , index ,
791838 assign = parts , partition_meta = self .partition_meta ,
792- row_filter = sel , infile = infile )
839+ row_filter = sel , infile = infile ,
840+ arrow_string_columns = arrow_string_columns )
793841 start += thislen
842+
843+ # Finalize arrow string columns: concatenate per-page arrays and assign.
844+ if _HAVE_ARROW and _USE_ARROW_STRINGS :
845+ for col , parts in arrow_string_columns .items ():
846+ if not parts :
847+ continue
848+ final_arr = _pa .concat_arrays (parts )
849+ if len (final_arr ) != len (df ):
850+ # Schema evolution: column absent in some row groups. The
851+ # pre-allocated object array already has None for the missing
852+ # rows; skip the arrow conversion so callers see partial data.
853+ continue
854+ arrow_str = pd .arrays .ArrowStringArray (_pa .chunked_array ([final_arr ]))
855+ if col in df .columns :
856+ df [col ] = pd .Series (arrow_str , index = df .index , name = col )
857+ elif col == df .index .name :
858+ df .index = pd .Index (arrow_str , name = col )
859+
794860 return df
795861
796862 def pre_allocate (self , size , columns , categories , index , dtypes = None ):
0 commit comments