Skip to content

Commit c3c830c

Browse files
authored
feat: add DatetimeIndex class (#1719)
* [WIP] added DatetimeIndex. Docs and tests to come next * add tests and docs * fix mypy * fix lint * handle empty index scenario
1 parent 0479763 commit c3c830c

File tree

8 files changed

+254
-4
lines changed

8 files changed

+254
-4
lines changed

bigframes/core/indexes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
# limitations under the License.
1414

1515
from bigframes.core.indexes.base import Index
16+
from bigframes.core.indexes.datetimes import DatetimeIndex
1617
from bigframes.core.indexes.multi import MultiIndex
1718

1819
__all__ = [
1920
"Index",
2021
"MultiIndex",
22+
"DatetimeIndex",
2123
]

bigframes/core/indexes/base.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import numpy as np
2626
import pandas
2727

28+
from bigframes import dtypes
2829
import bigframes.core.block_transforms as block_ops
2930
import bigframes.core.blocks as blocks
3031
import bigframes.core.expression as ex
@@ -90,12 +91,17 @@ def __new__(
9091
block = df.DataFrame(pd_df, session=session)._block
9192

9293
# TODO: Support more index subtypes
93-
from bigframes.core.indexes.multi import MultiIndex
9494

95-
if len(block._index_columns) <= 1:
96-
klass = cls
95+
if len(block._index_columns) > 1:
96+
from bigframes.core.indexes.multi import MultiIndex
97+
98+
klass: type[Index] = MultiIndex # type hint to make mypy happy
99+
elif _should_create_datetime_index(block):
100+
from bigframes.core.indexes.datetimes import DatetimeIndex
101+
102+
klass = DatetimeIndex
97103
else:
98-
klass = MultiIndex
104+
klass = cls
99105

100106
result = typing.cast(Index, object.__new__(klass))
101107
result._query_job = None
@@ -555,3 +561,10 @@ def to_numpy(self, dtype=None, *, allow_large_results=None, **kwargs) -> np.ndar
555561

556562
def __len__(self):
557563
return self.shape[0]
564+
565+
566+
def _should_create_datetime_index(block: blocks.Block) -> bool:
567+
if len(block.index.dtypes) != 1:
568+
return False
569+
570+
return dtypes.is_datetime_like(block.index.dtypes[0])

bigframes/core/indexes/datetimes.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""An index based on a single column with a datetime-like data type."""
16+
17+
from __future__ import annotations
18+
19+
from bigframes_vendored.pandas.core.indexes import (
20+
datetimes as vendored_pandas_datetime_index,
21+
)
22+
23+
from bigframes.core import expression as ex
24+
from bigframes.core.indexes.base import Index
25+
from bigframes.operations import date_ops
26+
27+
28+
class DatetimeIndex(Index, vendored_pandas_datetime_index.DatetimeIndex):
29+
__doc__ = vendored_pandas_datetime_index.DatetimeIndex.__doc__
30+
31+
# Must be above 5000 for pandas to delegate to bigframes for binops
32+
__pandas_priority__ = 12000
33+
34+
@property
35+
def year(self) -> Index:
36+
return self._apply_unary_expr(date_ops.year_op.as_expr(ex.free_var("arg")))
37+
38+
@property
39+
def month(self) -> Index:
40+
return self._apply_unary_expr(date_ops.month_op.as_expr(ex.free_var("arg")))
41+
42+
@property
43+
def day(self) -> Index:
44+
return self._apply_unary_expr(date_ops.day_op.as_expr(ex.free_var("arg")))
45+
46+
@property
47+
def dayofweek(self) -> Index:
48+
return self._apply_unary_expr(date_ops.dayofweek_op.as_expr(ex.free_var("arg")))
49+
50+
@property
51+
def day_of_week(self) -> Index:
52+
return self.dayofweek
53+
54+
@property
55+
def weekday(self) -> Index:
56+
return self.dayofweek

bigframes/pandas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ def clean_up_by_session_id(
271271
DataFrame = bigframes.dataframe.DataFrame
272272
Index = bigframes.core.indexes.Index
273273
MultiIndex = bigframes.core.indexes.MultiIndex
274+
DatetimeIndex = bigframes.core.indexes.DatetimeIndex
274275
Series = bigframes.series.Series
275276
__version__ = bigframes.version.__version__
276277

@@ -357,6 +358,7 @@ def reset_session():
357358
"DataFrame",
358359
"Index",
359360
"MultiIndex",
361+
"DatetimeIndex",
360362
"Series",
361363
"__version__",
362364
# Other public pandas attributes

docs/reference/bigframes.pandas/indexing.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ Index objects
77
:members:
88
:inherited-members:
99
:undoc-members:
10+
11+
12+
.. autoclass:: bigframes.core.indexes.multi.MultiIndex
13+
:members:
14+
:inherited-members:
15+
:undoc-members:
16+
17+
18+
.. autoclass:: bigframes.core.indexes.datetimes.DatetimeIndex
19+
:members:
20+
:inherited-members:
21+
:undoc-members:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import pandas
17+
import pandas.testing
18+
import pytest
19+
20+
21+
@pytest.fixture(scope="module")
22+
def datetime_indexes(session):
23+
pd_index = pandas.date_range("2024-12-25", periods=10, freq="d")
24+
bf_index = session.read_pandas(pd_index)
25+
26+
return bf_index, pd_index
27+
28+
29+
@pytest.mark.parametrize(
30+
"access",
31+
[
32+
pytest.param(lambda x: x.year, id="year"),
33+
pytest.param(lambda x: x.month, id="month"),
34+
pytest.param(lambda x: x.day, id="day"),
35+
pytest.param(lambda x: x.dayofweek, id="dayofweek"),
36+
pytest.param(lambda x: x.day_of_week, id="day_of_week"),
37+
pytest.param(lambda x: x.weekday, id="weekday"),
38+
],
39+
)
40+
def test_datetime_index_properties(datetime_indexes, access):
41+
bf_index, pd_index = datetime_indexes
42+
43+
actual_result = access(bf_index).to_pandas()
44+
45+
expected_result = access(pd_index).astype(pandas.Int64Dtype())
46+
pandas.testing.assert_index_equal(actual_result, expected_result)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/indexes/datetimes.py
2+
3+
from __future__ import annotations
4+
5+
from bigframes_vendored import constants
6+
from bigframes_vendored.pandas.core.indexes import base
7+
8+
9+
class DatetimeIndex(base.Index):
10+
"""Immutable sequence used for indexing and alignment with datetime-like values"""
11+
12+
@property
13+
def year(self) -> base.Index:
14+
"""The year of the datetime
15+
16+
**Examples:**
17+
18+
>>> import bigframes.pandas as bpd
19+
>>> import pandas as pd
20+
>>> bpd.options.display.progress_bar = None
21+
22+
>>> idx = bpd.Index([pd.Timestamp("20250215")])
23+
>>> idx.year
24+
Index([2025], dtype='Int64')
25+
"""
26+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
27+
28+
@property
29+
def month(self) -> base.Index:
30+
"""The month as January=1, December=12.
31+
32+
**Examples:**
33+
34+
>>> import bigframes.pandas as bpd
35+
>>> import pandas as pd
36+
>>> bpd.options.display.progress_bar = None
37+
38+
>>> idx = bpd.Index([pd.Timestamp("20250215")])
39+
>>> idx.month
40+
Index([2], dtype='Int64')
41+
"""
42+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
43+
44+
@property
45+
def day(self) -> base.Index:
46+
"""The day of the datetime.
47+
48+
**Examples:**
49+
50+
>>> import bigframes.pandas as bpd
51+
>>> import pandas as pd
52+
>>> bpd.options.display.progress_bar = None
53+
54+
>>> idx = bpd.Index([pd.Timestamp("20250215")])
55+
>>> idx.day
56+
Index([15], dtype='Int64')
57+
"""
58+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
59+
60+
@property
61+
def day_of_week(self) -> base.Index:
62+
"""The day of the week with Monday=0, Sunday=6.
63+
64+
**Examples:**
65+
66+
>>> import bigframes.pandas as bpd
67+
>>> import pandas as pd
68+
>>> bpd.options.display.progress_bar = None
69+
70+
>>> idx = bpd.Index([pd.Timestamp("20250215")])
71+
>>> idx.day_of_week
72+
Index([5], dtype='Int64')
73+
"""
74+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
75+
76+
@property
77+
def dayofweek(self) -> base.Index:
78+
"""The day of the week with Monday=0, Sunday=6.
79+
80+
**Examples:**
81+
82+
>>> import bigframes.pandas as bpd
83+
>>> import pandas as pd
84+
>>> bpd.options.display.progress_bar = None
85+
86+
>>> idx = bpd.Index([pd.Timestamp("20250215")])
87+
>>> idx.dayofweek
88+
Index([5], dtype='Int64')
89+
"""
90+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
91+
92+
@property
93+
def weekday(self) -> base.Index:
94+
"""The day of the week with Monday=0, Sunday=6.
95+
96+
**Examples:**
97+
98+
>>> import bigframes.pandas as bpd
99+
>>> import pandas as pd
100+
>>> bpd.options.display.progress_bar = None
101+
102+
>>> idx = bpd.Index([pd.Timestamp("20250215")])
103+
>>> idx.weekday
104+
Index([5], dtype='Int64')
105+
"""
106+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)

0 commit comments

Comments
 (0)