Skip to content

Commit 7b139bd

Browse files
bug(table) - Fix mixed-type table conversion to numpy
1 parent 51a4e0d commit 7b139bd

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

docs/docs/content/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ All notable changes to Rayforce-Py will be documented in this file.
1616
- **Slicing**: `table[1:3]`, `table[:5]`, `table[-2:]` — row slicing backed by the C-level `TAKE` operation.
1717
- **Index list**: `table[[0, 2, 5]]` — select specific rows by position.
1818

19+
### Bug Fixes
20+
21+
- **`Table.to_numpy()` with Timestamp columns**: Fixed `DTypePromotionError` when calling `to_numpy()` on tables containing a mix of incompatible column types (e.g., integers, strings, and timestamps). Mixed-type tables now gracefully fall back to `object` dtype.
22+
1923
2026-02-23 | **[🔗 PyPI](https://pypi.org/project/rayforce-py/0.6.1/)** | **[🔗 GitHub](https://github.com/RayforceDB/rayforce-py/releases/tag/0.6.1)**
2024

2125

rayforce/types/table.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,18 @@ def to_dict(self) -> dict[str, list]:
578578
@DestructiveOperationHandler()
579579
def to_numpy(self) -> t.Any:
580580
vals = self.values()
581-
return np.column_stack([vals[i].to_numpy() for i in range(len(vals))])
581+
arrays = [vals[i].to_numpy() for i in range(len(vals))]
582+
try:
583+
return np.column_stack(arrays)
584+
except (np.exceptions.DTypePromotionError, TypeError):
585+
return np.column_stack(
586+
[
587+
a.astype("datetime64[ms]").astype(object)
588+
if a.dtype.kind == "M"
589+
else a.astype(object)
590+
for a in arrays
591+
]
592+
)
582593

583594

584595
class TableReprMixin:

tests/types/table/test_misc.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,56 @@ def test_mixed_types_coerced(self):
12571257
assert arr[0, 0] == "alice"
12581258
assert arr[0, 1] == "25"
12591259

1260+
def test_with_timestamp_column(self):
1261+
from datetime import datetime
1262+
1263+
table = Table.from_dict(
1264+
{
1265+
"id": Vector([1, 2, 3], ray_type=I64),
1266+
"name": Vector(["Alice", "Bob", "Carol"], ray_type=Symbol),
1267+
}
1268+
)
1269+
birthdays = Vector(
1270+
[datetime(2001, 1, 1), datetime(2002, 2, 2), datetime(2003, 3, 3)],
1271+
ray_type=Timestamp,
1272+
)
1273+
table = table.select("*", birthday=birthdays).execute()
1274+
arr = table.to_numpy()
1275+
assert arr.shape == (3, 3)
1276+
assert arr.dtype == object
1277+
assert arr[0, 2] == datetime(2001, 1, 1)
1278+
assert arr[2, 2] == datetime(2003, 3, 3)
1279+
assert isinstance(arr[0, 2], datetime)
1280+
1281+
def test_with_all_temporal_columns(self):
1282+
from datetime import date, datetime, time, timedelta
1283+
1284+
table = Table.from_dict(
1285+
{
1286+
"id": Vector([1, 2], ray_type=I64),
1287+
"dt": Vector([date(2001, 1, 1), date(2002, 2, 2)], ray_type=Date),
1288+
"tm": Vector([time(9, 0), time(10, 30)], ray_type=Time),
1289+
"ts": Vector(
1290+
[datetime(2001, 1, 1, 9, 0), datetime(2002, 2, 2, 10, 30)],
1291+
ray_type=Timestamp,
1292+
),
1293+
}
1294+
)
1295+
arr = table.to_numpy()
1296+
assert arr.shape == (2, 4)
1297+
assert arr.dtype == object
1298+
1299+
# Date → datetime (datetime64[D].astype(object) produces datetime)
1300+
assert arr[0, 1] == datetime(2001, 1, 1)
1301+
1302+
# Time → timedelta
1303+
assert isinstance(arr[0, 2], timedelta)
1304+
assert arr[0, 2] == timedelta(hours=9)
1305+
1306+
# Timestamp → datetime
1307+
assert isinstance(arr[0, 3], datetime)
1308+
assert arr[0, 3] == datetime(2001, 1, 1, 9, 0)
1309+
12601310
def test_all_same_int_type(self):
12611311
table = Table(
12621312
{

0 commit comments

Comments
 (0)