1414
1515from __future__ import annotations
1616
17+ import numbers
1718import typing
1819import warnings
19- from typing import Tuple , Union
20+ from typing import Any , Sequence , Tuple , Union , cast
2021
2122import bigframes_vendored .constants as constants
2223import bigframes_vendored .ibis .common .exceptions as ibis_exceptions
24+ import numpy as np
2325import pandas as pd
26+ import pyarrow as pa
27+ import pyarrow .types # type: ignore
2428
2529import bigframes .core .blocks
2630import bigframes .core .col
2731import bigframes .core .expression as ex
2832import bigframes .core .guid as guid
2933import bigframes .core .indexes as indexes
3034import bigframes .core .scalar
35+ import bigframes .core .validations as validations
3136import bigframes .core .window_spec as windows
3237import bigframes .dataframe
3338import bigframes .dtypes
4550 ]
4651
4752
53+ _DATAFRAME_ILOC_ERROR = "Only DataFrame.iloc[:, col_indexer] = value is supported."
54+
55+
4856class LocSeriesIndexer :
4957 def __init__ (self , series : bigframes .series .Series ):
5058 self ._series = series
@@ -102,6 +110,9 @@ def __getitem__(
102110
103111 Other key types are not yet supported.
104112 """
113+ if not _is_noop_slice (key ):
114+ validations .enforce_ordered (self ._series , "iloc" )
115+
105116 return _iloc_getitem_series_or_dataframe (self ._series , key )
106117
107118
@@ -110,9 +121,9 @@ def __init__(self, series: bigframes.series.Series):
110121 self ._series = series
111122
112123 def __getitem__ (self , key : int ) -> bigframes .core .scalar .Scalar :
113- if not isinstance (key , int ):
124+ if not _is_integer_scalar (key ):
114125 raise ValueError ("Series iAt based indexing can only have integer indexers" )
115- return self ._series .iloc [key ]
126+ return self ._series .iloc [_to_python_int ( key ) ]
116127
117128
118129class AtSeriesIndexer :
@@ -186,14 +197,7 @@ def __setitem__(
186197 key : Tuple [slice , str ],
187198 value : bigframes .dataframe .SingleItemValue ,
188199 ):
189- if (
190- isinstance (key , tuple )
191- and len (key ) == 2
192- and isinstance (key [0 ], slice )
193- and (key [0 ].start is None or key [0 ].start == 0 )
194- and (key [0 ].step is None or key [0 ].step == 1 )
195- and key [0 ].stop is None
196- ):
200+ if isinstance (key , tuple ) and len (key ) == 2 and _is_noop_slice (key [0 ]):
197201 # TODO(swast): Support setting multiple columns with key[1] as a list
198202 # of labels and value as a DataFrame.
199203 df = self ._dataframe .assign (** {key [1 ]: value })
@@ -244,8 +248,41 @@ def __getitem__(self, key) -> Union[bigframes.dataframe.DataFrame, pd.Series]:
244248
245249 Other key types are not yet supported.
246250 """
251+ requires_ordering = True
252+ if isinstance (key , tuple ):
253+ if len (key ) > 0 :
254+ row_indexer = key [0 ]
255+ if _is_noop_slice (row_indexer ):
256+ requires_ordering = False
257+ elif _is_noop_slice (key ):
258+ requires_ordering = False
259+
260+ if requires_ordering :
261+ validations .enforce_ordered (self ._dataframe , "iloc" )
262+
247263 return _iloc_getitem_series_or_dataframe (self ._dataframe , key )
248264
265+ def __setitem__ (
266+ self ,
267+ key : Tuple [
268+ slice , Union [int , typing .Sequence [int ], slice , typing .Sequence [bool ]]
269+ ],
270+ value : Union [
271+ bigframes .dataframe .SingleItemValue , bigframes .dataframe .DataFrame
272+ ],
273+ ):
274+ if not (isinstance (key , tuple ) and len (key ) == 2 ):
275+ raise NotImplementedError (_DATAFRAME_ILOC_ERROR )
276+
277+ row_indexer , col_indexer = key
278+
279+ if not _is_noop_slice (row_indexer ):
280+ raise NotImplementedError (_DATAFRAME_ILOC_ERROR )
281+
282+ col_offsets = _iloc_col_indexer_to_offsets (self ._dataframe , col_indexer )
283+ df = self ._dataframe ._assign_multi_items_by_offsets (col_offsets , value )
284+ self ._dataframe ._set_block (df ._get_block ())
285+
249286
250287class IatDataFrameIndexer :
251288 def __init__ (self , dataframe : bigframes .dataframe .DataFrame ):
@@ -254,19 +291,21 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
254291 def __getitem__ (self , key : tuple ) -> bigframes .core .scalar .Scalar :
255292 error_message = "DataFrame.iat should be indexed by a tuple of exactly 2 ints"
256293 # we raise TypeError or ValueError under the same conditions that pandas does
257- if isinstance (key , int ):
294+ if _is_integer_scalar (key ):
258295 raise TypeError (error_message )
259296 if not isinstance (key , tuple ):
260297 raise ValueError (error_message )
261- key_values_are_ints = [isinstance (key_value , int ) for key_value in key ]
298+ key_values_are_ints = [_is_integer_scalar (key_value ) for key_value in key ]
262299 if not all (key_values_are_ints ):
263300 raise ValueError (error_message )
264301 if len (key ) != 2 :
265302 raise TypeError (error_message )
303+ row_idx = _to_python_int (key [0 ])
304+ col_idx = _to_python_int (key [1 ])
266305 block : bigframes .core .blocks .Block = self ._dataframe ._block
267- column_block = block .select_columns ([block .value_columns [key [ 1 ] ]])
306+ column_block = block .select_columns ([block .value_columns [col_idx ]])
268307 column = bigframes .series .Series (column_block )
269- return column .iloc [key [ 0 ] ]
308+ return column .iloc [row_idx ]
270309
271310
272311class AtDataFrameIndexer :
@@ -283,6 +322,16 @@ def __getitem__(
283322 return self ._dataframe .loc [key ]
284323
285324
325+ def _is_noop_slice (key : Any ) -> bool :
326+ """Return True if key is a slice selecting all elements in the original order."""
327+ return (
328+ isinstance (key , slice )
329+ and (key .start is None or key .start == 0 )
330+ and (key .step is None or key .step == 1 )
331+ and key .stop is None
332+ )
333+
334+
286335@typing .overload
287336def _loc_getitem_series_or_dataframe (
288337 series_or_dataframe : bigframes .series .Series , key
@@ -304,12 +353,14 @@ def _loc_getitem_series_or_dataframe(
304353 pd .Series ,
305354 bigframes .core .scalar .Scalar ,
306355]:
356+ if _is_noop_slice (key ):
357+ return series_or_dataframe .copy ()
358+
307359 if isinstance (key , slice ):
308- if (key .start is None ) and (key .stop is None ) and (key .step is None ):
309- return series_or_dataframe .copy ()
310360 raise NotImplementedError (
311361 f"loc does not yet support indexing with a slice. { constants .FEEDBACK_LINK } "
312362 )
363+
313364 if isinstance (key , bigframes .core .col .Expression ):
314365 label_to_col_ref = {
315366 label : ex .deref (id )
@@ -426,6 +477,119 @@ def _struct_accessor_check_and_warn(
426477 warnings .warn (msg , stacklevel = 7 , category = bfe .BadIndexerKeyWarning )
427478
428479
480+ def _to_python_int (value : Any ) -> int :
481+ if isinstance (value , pa .Scalar ):
482+ return int (value .as_py ())
483+ return int (value )
484+
485+
486+ def _iloc_clip_to_offset (index : Any , length : int , name : str ) -> int :
487+ """Support negative values for offsets."""
488+ if not _is_integer_scalar (index ):
489+ raise TypeError (f"got unexpected { type (index )} for { name } " )
490+ offset = _to_python_int (index )
491+ if offset < 0 :
492+ offset += length
493+
494+ if offset < 0 or offset >= length :
495+ raise IndexError (f"{ name } { index } is out-of-bounds" )
496+
497+ return offset
498+
499+
500+ def _is_integer_scalar (value : Any ) -> bool :
501+ return not (
502+ isinstance (value , bool )
503+ or isinstance (value , np .bool_ )
504+ or (isinstance (value , pa .Scalar ) and pyarrow .types .is_boolean (value .type ))
505+ ) and (
506+ isinstance (value , numbers .Integral )
507+ or (isinstance (value , pa .Scalar ) and pyarrow .types .is_integer (value .type ))
508+ )
509+
510+
511+ def _is_boolean_scalar (value : Any ) -> bool :
512+ return (
513+ isinstance (value , bool )
514+ or isinstance (value , np .bool_ )
515+ or (isinstance (value , pa .Scalar ) and pyarrow .types .is_boolean (value .type ))
516+ )
517+
518+
519+ def _truth_val (value : Any ) -> bool :
520+ if value is None or value is pd .NA or pd .isna (value ):
521+ return False
522+ if isinstance (value , pa .Scalar ):
523+ return bool (value .as_py ()) if value .is_valid else False
524+ return bool (value )
525+
526+
527+ def _is_boolean_indexer (indexer : Any ) -> bool :
528+ if hasattr (indexer , "dtype" ) and pd .api .types .is_bool_dtype (indexer .dtype ):
529+ return True
530+ if (
531+ hasattr (indexer , "type" )
532+ and isinstance (indexer .type , pa .DataType )
533+ and pyarrow .types .is_boolean (indexer .type )
534+ ):
535+ return True
536+ if pd .api .types .is_list_like (indexer ):
537+ lst = (
538+ list (indexer )
539+ if not isinstance (indexer , (bigframes .series .Series , indexes .Index ))
540+ else list (indexer .to_pandas ())
541+ )
542+ if len (lst ) > 0 and all (
543+ _is_boolean_scalar (x ) or (x is None ) or (x is pd .NA ) or pd .isna (x )
544+ for x in lst
545+ ):
546+ return any (_is_boolean_scalar (x ) for x in lst )
547+ return False
548+
549+
550+ def _iloc_col_indexer_to_offsets (
551+ df : bigframes .dataframe .DataFrame , col_indexer : Any
552+ ) -> Sequence [int ]:
553+ """Convert col_indexer from one of the many pandas-compatible formats to a list of offsets."""
554+ n_cols = len (df .columns )
555+
556+ if _is_integer_scalar (col_indexer ):
557+ col_offset = _to_python_int (col_indexer )
558+ return [
559+ _iloc_clip_to_offset (
560+ col_offset , n_cols , "single positional iloc column indexer"
561+ )
562+ ]
563+
564+ elif isinstance (col_indexer , slice ):
565+ return list (range (* col_indexer .indices (n_cols )))
566+
567+ elif _is_boolean_indexer (col_indexer ):
568+ col_indexer_list = list (col_indexer )
569+ if len (col_indexer_list ) != n_cols :
570+ raise ValueError (
571+ f"Boolean iloc column indexer has wrong length: { len (col_indexer_list )} instead of { n_cols } "
572+ )
573+ return [i for i , val in enumerate (col_indexer_list ) if _truth_val (val )]
574+
575+ elif pd .api .types .is_list_like (col_indexer ):
576+ col_indexer_list = list (col_indexer )
577+ return [
578+ _iloc_clip_to_offset (idx , n_cols , "iloc column indexer" )
579+ for idx in col_indexer_list
580+ ]
581+
582+ raise TypeError (f"got unexpected { type (col_indexer )} for iloc column indexer" )
583+
584+
585+ def _iloc_df_from_column_offsets (
586+ df : bigframes .dataframe .DataFrame , key : Sequence [int ]
587+ ) -> bigframes .dataframe .DataFrame :
588+ block = df ._block
589+ selected_ids = tuple (block .value_columns [offset ] for offset in key )
590+ return bigframes .dataframe .DataFrame (block .select_columns (selected_ids ))
591+
592+
429593@typing .overload
430594def _iloc_getitem_series_or_dataframe (
431595 series_or_dataframe : bigframes .series .Series , key
@@ -447,9 +611,10 @@ def _iloc_getitem_series_or_dataframe(
447611 bigframes .core .scalar .Scalar ,
448612 pd .Series ,
449613]:
450- if isinstance (key , int ):
451- stop_key = key + 1 if key != - 1 else None
452- internal_slice_result = series_or_dataframe ._slice (key , stop_key , 1 )
614+ if _is_integer_scalar (key ):
615+ key_int = _to_python_int (key )
616+ stop_key = key_int + 1 if key_int != - 1 else None
617+ internal_slice_result = series_or_dataframe ._slice (key_int , stop_key , 1 )
453618 result_pd_df = internal_slice_result .to_pandas ()
454619 if result_pd_df .empty :
455620 raise IndexError ("single positional indexer is out-of-bounds" )
@@ -470,21 +635,49 @@ def _iloc_getitem_series_or_dataframe(
470635
471636 # len(key) == 2
472637 df = typing .cast (bigframes .dataframe .DataFrame , series_or_dataframe )
473- if isinstance (key [1 ], int ):
638+ if _is_integer_scalar (key [0 ]) and _is_integer_scalar ( key [ 1 ] ):
474639 return df .iat [key ]
475- elif isinstance (key [1 ], list ):
476- columns = df .columns [key [1 ]]
477- return _iloc_getitem_series_or_dataframe (df [columns ], key [0 ])
478- raise NotImplementedError (
479- f"iloc does not yet support indexing with { key } . { constants .FEEDBACK_LINK } "
480- )
640+
641+ row_indexer , column_indexer = key
642+ column_offsets = _iloc_col_indexer_to_offsets (df , column_indexer )
643+ df_subset = _iloc_df_from_column_offsets (df , column_offsets )
644+
645+ if _is_integer_scalar (column_indexer ):
646+ selected_columns = cast (
647+ Union [bigframes .dataframe .DataFrame , bigframes .series .Series ],
648+ df_subset [df_subset .columns [0 ]],
649+ )
650+ else :
651+ selected_columns = df_subset
652+
653+ return _iloc_getitem_series_or_dataframe (selected_columns , row_indexer )
481654 elif pd .api .types .is_list_like (key ):
482655 if len (key ) == 0 :
483656 return typing .cast (
484657 Union [bigframes .dataframe .DataFrame , bigframes .series .Series ],
485658 series_or_dataframe .iloc [0 :0 ],
486659 )
487660
661+ if _is_boolean_indexer (key ):
662+ key_list = (
663+ list (key )
664+ if not isinstance (key , (bigframes .series .Series , indexes .Index ))
665+ else list (key .to_pandas ())
666+ )
667+ n_rows = len (series_or_dataframe )
668+ if len (key_list ) != n_rows :
669+ raise IndexError (
670+ f"Boolean index has wrong length: { len (key_list )} instead of { n_rows } "
671+ )
672+ key = [i for i , val in enumerate (key_list ) if _truth_val (val )]
673+ if len (key ) == 0 :
674+ return typing .cast (
675+ Union [bigframes .dataframe .DataFrame , bigframes .series .Series ],
676+ series_or_dataframe .iloc [0 :0 ],
677+ )
678+ else :
679+ key = [_to_python_int (k ) for k in list (key )]
680+
488681 # Check if both positive index and negative index are necessary
489682 if isinstance (key , (bigframes .series .Series , indexes .Index )):
490683 # Avoid data download
0 commit comments