Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit 3d428a2

Browse files
authored
Add option to choose dtypes by column in to_dataframe. (#7126)
* Add option to choose dtypes by column in to_dataframe. This allows pandas users to select different sized floats for performance at the expense of accuracy. With pandas 0.24, it will also allow pandas users to use the new pandas.Int64Dtype() for nullable integer columns. * Adjust deps for testing. Blacken.
1 parent 2b3068b commit 3d428a2

5 files changed

Lines changed: 76 additions & 10 deletions

File tree

google/cloud/bigquery_storage_v1beta1/reader.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import absolute_import
1616

17+
import collections
1718
import itertools
1819
import json
1920

@@ -155,11 +156,11 @@ def rows(self, read_session):
155156
if fastavro is None:
156157
raise ImportError(_FASTAVRO_REQUIRED)
157158

158-
avro_schema = _avro_schema(read_session)
159+
avro_schema, _ = _avro_schema(read_session)
159160
blocks = (_avro_rows(block, avro_schema) for block in self)
160161
return itertools.chain.from_iterable(blocks)
161162

162-
def to_dataframe(self, read_session):
163+
def to_dataframe(self, read_session, dtypes=None):
163164
"""Create a :class:`pandas.DataFrame` of all rows in the stream.
164165
165166
This method requires the pandas libary to create a data frame and the
@@ -176,6 +177,13 @@ def to_dataframe(self, read_session):
176177
The read session associated with this read rows stream. This
177178
contains the schema, which is required to parse the data
178179
blocks.
180+
dtypes ( \
181+
Map[str, Union[str, pandas.Series.dtype]] \
182+
):
183+
Optional. A dictionary of column names pandas ``dtype``s. The
184+
provided ``dtype`` is used when constructing the series for
185+
the column specified. Otherwise, the default pandas behavior
186+
is used.
179187
180188
Returns:
181189
pandas.DataFrame:
@@ -186,14 +194,29 @@ def to_dataframe(self, read_session):
186194
if pandas is None:
187195
raise ImportError("pandas is required to create a DataFrame")
188196

189-
avro_schema = _avro_schema(read_session)
197+
if dtypes is None:
198+
dtypes = {}
199+
200+
avro_schema, column_names = _avro_schema(read_session)
190201
frames = []
191202
for block in self:
192-
dataframe = pandas.DataFrame(list(_avro_rows(block, avro_schema)))
203+
dataframe = _to_dataframe_with_dtypes(
204+
_avro_rows(block, avro_schema), column_names, dtypes
205+
)
193206
frames.append(dataframe)
194207
return pandas.concat(frames)
195208

196209

210+
def _to_dataframe_with_dtypes(rows, column_names, dtypes):
211+
columns = collections.defaultdict(list)
212+
for row in rows:
213+
for column in row:
214+
columns[column].append(row[column])
215+
for column in dtypes:
216+
columns[column] = pandas.Series(columns[column], dtype=dtypes[column])
217+
return pandas.DataFrame(columns, columns=column_names)
218+
219+
197220
def _avro_schema(read_session):
198221
"""Extract and parse Avro schema from a read session.
199222
@@ -206,10 +229,13 @@ def _avro_schema(read_session):
206229
blocks.
207230
208231
Returns:
209-
A parsed Avro schema, using :func:`fastavro.schema.parse_schema`.
232+
Tuple[fastavro.schema, Tuple[str]]:
233+
A parsed Avro schema, using :func:`fastavro.schema.parse_schema`
234+
and the column names for a read session.
210235
"""
211236
json_schema = json.loads(read_session.avro_schema.schema)
212-
return fastavro.parse_schema(json_schema)
237+
column_names = tuple((field["name"] for field in json_schema["fields"]))
238+
return fastavro.parse_schema(json_schema), column_names
213239

214240

215241
def _avro_rows(block, avro_schema):

noxfile.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def default(session):
4343
session.run(
4444
'py.test',
4545
'--quiet',
46-
'--cov=google.cloud.bigquery_storage',
4746
'--cov=google.cloud.bigquery_storage_v1beta1',
4847
'--cov=tests.unit',
4948
'--cov-append',

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
name = 'google-cloud-bigquery-storage'
2323
description = 'BigQuery Storage API API client library'
24-
version = '0.1.1'
24+
version = '0.2.0dev1'
2525
release_status = 'Development Status :: 3 - Alpha'
2626
dependencies = [
2727
'google-api-core[grpc] >= 1.6.0, < 2.0.0dev',

tests/system/test_system.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import os
1919

20+
import numpy
2021
import pytest
2122

2223
from google.cloud import bigquery_storage_v1beta1
@@ -78,11 +79,15 @@ def test_read_rows_to_dataframe(client, project_id):
7879
stream=session.streams[0]
7980
)
8081

81-
frame = client.read_rows(stream_pos).to_dataframe(session)
82+
frame = client.read_rows(stream_pos).to_dataframe(
83+
session, dtypes={"latitude": numpy.float16}
84+
)
8285

8386
# Station ID is a required field (no nulls), so the datatype should always
8487
# be integer.
8588
assert frame.station_id.dtype.name == "int64"
89+
assert frame.latitude.dtype.name == "float16"
90+
assert frame.longitude.dtype.name == "float64"
8691
assert frame["name"].str.startswith("Central Park").any()
8792

8893

tests/unit/test_reader.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
{"name": "time_col", "type": "time"},
5656
{"name": "ts_col", "type": "timestamp"},
5757
]
58+
SCALAR_COLUMN_NAMES = [field["name"] for field in SCALAR_COLUMNS]
5859
SCALAR_BLOCKS = [
5960
[
6061
{
@@ -281,7 +282,9 @@ def test_to_dataframe_w_scalars(class_under_test):
281282
)
282283
got = reader.to_dataframe(read_session)
283284

284-
expected = pandas.DataFrame(list(itertools.chain.from_iterable(SCALAR_BLOCKS)))
285+
expected = pandas.DataFrame(
286+
list(itertools.chain.from_iterable(SCALAR_BLOCKS)), columns=SCALAR_COLUMN_NAMES
287+
)
285288
# fastavro provides its own UTC definition, so
286289
# compare the timestamp columns separately.
287290
got_ts = got["ts_col"]
@@ -301,6 +304,39 @@ def test_to_dataframe_w_scalars(class_under_test):
301304
)
302305

303306

307+
def test_to_dataframe_w_dtypes(class_under_test):
308+
# TODOTODOTODOTODO
309+
avro_schema = _bq_to_avro_schema(
310+
[
311+
{"name": "bigfloat", "type": "float64"},
312+
{"name": "lilfloat", "type": "float64"},
313+
]
314+
)
315+
read_session = _generate_read_session(avro_schema)
316+
blocks = [
317+
[{"bigfloat": 1.25, "lilfloat": 30.5}, {"bigfloat": 2.5, "lilfloat": 21.125}],
318+
[{"bigfloat": 3.75, "lilfloat": 11.0}],
319+
]
320+
avro_blocks = _bq_to_avro_blocks(blocks, avro_schema)
321+
322+
reader = class_under_test(
323+
avro_blocks, mock_client, bigquery_storage_v1beta1.types.StreamPosition(), {}
324+
)
325+
got = reader.to_dataframe(read_session, dtypes={"lilfloat": "float16"})
326+
327+
expected = pandas.DataFrame(
328+
{
329+
"bigfloat": [1.25, 2.5, 3.75],
330+
"lilfloat": pandas.Series([30.5, 21.125, 11.0], dtype="float16"),
331+
},
332+
columns=["bigfloat", "lilfloat"],
333+
)
334+
pandas.testing.assert_frame_equal(
335+
got.reset_index(drop=True), # reset_index to ignore row labels
336+
expected.reset_index(drop=True),
337+
)
338+
339+
304340
def test_copy_stream_position(mut):
305341
read_position = bigquery_storage_v1beta1.types.StreamPosition(
306342
stream={"name": "test"}, offset=41

0 commit comments

Comments
 (0)