1414# '
1515# ' @details The default in-memory path converts with reticulate and then patches
1616# ' individual columns:
17- # ' * `int64`/`uint64` columns are recovered as `bit64::integer64` so large ids
18- # ' do not overflow to `NA`.
17+ # ' * Integer columns (`int64`, `uint64`, and `object` columns whose cells are
18+ # ' all Python ints) are classified by magnitude so each maps to the
19+ # ' narrowest faithful R type. This mirrors reticulate's own `py_to_r()`
20+ # ' where reticulate is faithful, and reserves `bit64::integer64` for values
21+ # ' base R cannot hold exactly -- see `bigint`.
1922# ' * `object` columns whose cells are all scalar (or `NA`) are flattened from a
2023# ' list of length-1 vectors to an atomic vector, with integer-valued cells
2124# ' read as strings so arbitrary-precision Python ints round-trip. Genuine
2225# ' list-valued columns (e.g. multi-select values) are left intact.
2326# ' * datetime columns are normalised to `POSIXct` in UTC.
2427# '
2528# ' The optional `use_arrow` path round-trips through a Feather file and needs
26- # ' the Suggested `arrow` package.
29+ # ' the Suggested `arrow` package; `bigint` does not apply to it .
2730# '
2831# ' @param x A pandas `DataFrame` (a reticulate `pandas.core.frame.DataFrame`).
2932# ' @param use_arrow If `TRUE`, convert via a temporary Feather file using the
3033# ' `arrow` package rather than in memory. Implies a tibble result.
3134# ' @param keep_index Whether to keep the pandas index as a column.
3235# ' @param tibble Whether to return a tibble rather than a base data frame.
3336# ' Defaults to `use_arrow`.
37+ # ' @param bigint How to represent integer columns. `"auto"` (the default) maps
38+ # ' each column to the narrowest faithful base R type: values fitting a 32-bit
39+ # ' R `integer` become `integer`, larger values below 2^53 become `double`
40+ # ' (exact), and values from 2^53 up to the signed 64-bit maximum become
41+ # ' `bit64::integer64` -- reserving `integer64` for what base R cannot hold
42+ # ' exactly. `"integer64"` returns every integer column as `bit64::integer64`
43+ # ' for a stable schema regardless of magnitude. `"character"` behaves like
44+ # ' `"auto"` but returns the large (>= 2^53) tier as character, as
45+ # ' `data.table::fread()` offers. `uint64` values beyond the signed 64-bit
46+ # ' range cannot be held by `integer64`; they are always returned as character,
47+ # ' with a warning unless `bigint = "character"`.
3448# '
3549# ' @return An R `data.frame`, or a tibble when `tibble = TRUE`.
3650# ' @export
4155# ' pandas2df(df)
4256# ' }
4357pandas2df <- function (x , use_arrow = FALSE , keep_index = FALSE ,
44- tibble = use_arrow ) {
58+ tibble = use_arrow ,
59+ bigint = c(" auto" , " integer64" , " character" )) {
60+ bigint <- match.arg(bigint )
4561 use_arrow <- isTRUE(use_arrow )
4662 keep_index <- isTRUE(keep_index )
4763 tibble <- isTRUE(tibble )
@@ -52,7 +68,7 @@ pandas2df <- function(x, use_arrow = FALSE, keep_index = FALSE,
5268 x $ reset_index(drop = ! keep_index , inplace = TRUE )
5369 nr <- nrow(x )
5470 if (! use_arrow )
55- return (pandas2df_inmem(x , tibble = tibble ))
71+ return (pandas2df_inmem(x , tibble = tibble , bigint = bigint ))
5672 check_suggested(" arrow" , " for the use_arrow = TRUE conversion path" )
5773 if (nr == 0L ) {
5874 check_suggested(" tibble" , " for the use_arrow = TRUE conversion path" )
@@ -83,7 +99,7 @@ is_pandas_dataframe <- function(x) {
8399 identical(nm , " DataFrame" )
84100}
85101
86- pandas2df_inmem <- function (df , tibble = FALSE ) {
102+ pandas2df_inmem <- function (df , tibble = FALSE , bigint = " auto " ) {
87103 if (! is_pandas_dataframe(df ))
88104 stop(" `df` must be a pandas DataFrame." , call. = FALSE )
89105 res <- pandas_py_to_r_frame(df )
@@ -98,9 +114,10 @@ pandas2df_inmem <- function(df, tibble = FALSE) {
98114 int_cols <- names(dtypes )[tolower(dtypes ) %in% c(" int64" , " uint64" )]
99115 for (col in intersect(int_cols , names(res ))) {
100116 series <- reticulate :: py_get_item(df , col )
101- i64 <- pandas_series_integer64(series , dtypes [[col ]])
102- if (! is.null(i64 ))
103- res [[col ]] <- i64
117+ conv <- classify_integer_strings(pandas_series_character_values(series ),
118+ bigint = bigint , col = col )
119+ if (! is.null(conv ))
120+ res [[col ]] <- conv
104121 }
105122
106123 # Object dtype: each cell can be an arbitrary Python object. reticulate's
@@ -130,7 +147,7 @@ pandas2df_inmem <- function(df, tibble = FALSE) {
130147 # would instead capture Python's list repr (e.g. "['AB']") as a literal
131148 # string, so leave genuine list-valued columns alone.
132149 if (pandas_series_has_list_cells(series )) next
133- flat <- pandas_object_series_to_vector(series )
150+ flat <- pandas_object_series_to_vector(series , bigint = bigint , col = col )
134151 if (! is.null(flat ))
135152 res [[col ]] <- flat
136153 }
@@ -165,36 +182,69 @@ pandas_dataframe_dtypes <- function(df) {
165182 unlist(dtypes , use.names = TRUE )
166183}
167184
168- pandas_series_integer64 <- function (series , dtype ) {
169- vals <- pandas_series_character_values(series )
185+ # Classify a vector of exact integer strings (NA for missing) into the natural
186+ # R representation, honouring `bigint`. This is the single decision point shared
187+ # by the typed int64/uint64 path and the object-column path, so both dtypes and
188+ # both code paths behave identically.
189+ #
190+ # "auto" small -> integer, mid (< 2^53) -> double, large -> integer64
191+ # "integer64" every column -> integer64 (stable schema)
192+ # "character" as "auto" but the large (>= 2^53) tier -> character
193+ #
194+ # The tiering mirrors reticulate's py_to_r() where it is faithful (small ->
195+ # integer, mid -> double) and only reaches for bit64::integer64 once a value
196+ # exceeds 2^53, the largest integer a double represents exactly.
197+ #
198+ # uint64 values beyond the signed 64-bit maximum cannot be held by integer64
199+ # (which is signed); they are returned as character -- the only faithful option
200+ # -- and, outside "character" mode, warned about.
201+ #
202+ # Returns NULL when the caller should leave the column untouched: input is NULL,
203+ # all cells are missing, or the cells are not all integer-shaped.
204+ classify_integer_strings <- function (vals ,
205+ bigint = c(" auto" , " integer64" ,
206+ " character" ),
207+ col = NULL ) {
208+ bigint <- match.arg(bigint )
170209 if (is.null(vals ))
171210 return (NULL )
172211 present <- ! is.na(vals )
173- if (! any(present )) {
174- if (dtype == " uint64" )
175- return (bit64 :: as.integer64(vals ))
176- return (NULL )
177- }
178- if (dtype != " uint64" &&
179- all(abs(as.numeric(vals [present ])) < = .Machine $ integer.max ))
212+ if (! any(present ))
180213 return (NULL )
181- if (dtype == " uint64" &&
182- all(as.numeric(vals [present ]) < = .Machine $ integer.max ))
183- return (bit64 :: as.integer64(vals ))
184214 if (! all(grepl(" ^-?[0-9]+$" , vals [present ])))
185215 return (NULL )
186- if (any(int64_overflows(vals ), na.rm = TRUE ))
187- stop(" int64 overflow! id cannot be represented as int64" )
188- bit64 :: as.integer64(vals )
216+
217+ if (any(int64_overflows(vals ), na.rm = TRUE )) {
218+ if (! identical(bigint , " character" ))
219+ warning(sprintf(
220+ paste0(" %s holds integers beyond the signed 64-bit range; " ,
221+ " returning character. Pass bigint = \" character\" to silence." ),
222+ if (is.null(col )) " A column" else sprintf(" Column '%s'" , col )),
223+ call. = FALSE )
224+ return (vals )
225+ }
226+
227+ i64 <- bit64 :: as.integer64(vals )
228+ if (identical(bigint , " integer64" ))
229+ return (i64 )
230+
231+ amax <- max(abs(i64 ), na.rm = TRUE )
232+ if (amax > = bit64 :: as.integer64(" 9007199254740992" )) # 2^53
233+ return (if (identical(bigint , " character" )) vals else i64 )
234+ if (amax > .Machine $ integer.max ) # mid -> double
235+ return (as.double(i64 ))
236+ as.integer(i64 ) # small -> integer
189237}
190238
191239# Flatten a pandas Series of object dtype whose cells are all the same scalar
192240# Python type (or NA) into an atomic R vector. Reads cells as strings via
193241# series.map(str).tolist() so arbitrary-precision Python ints round-trip
194242# without int32 truncation (the bug that corrupts uint32 columns >= 2^31
195243# returned by the L2 cache).
196- pandas_object_series_to_vector <- function (series ) {
197- classify_object_values(pandas_series_character_values(series ))
244+ pandas_object_series_to_vector <- function (series , bigint = " auto" ,
245+ col = NULL ) {
246+ classify_object_values(pandas_series_character_values(series ),
247+ bigint = bigint , col = col )
198248}
199249
200250# TRUE if any non-missing cell of this pandas object Series is itself a
@@ -216,20 +266,19 @@ pandas_series_has_list_cells <- local({
216266# already mapped via str()), classify and convert to the natural atomic R
217267# type. Returns NULL when the column can't be classified (mixed types) and
218268# the caller should leave the list-column intact.
219- classify_object_values <- function (vals ) {
269+ classify_object_values <- function (vals , bigint = c(" auto" , " integer64" ,
270+ " character" ),
271+ col = NULL ) {
272+ bigint <- match.arg(bigint )
220273 if (is.null(vals )) return (NULL )
221274 present <- ! is.na(vals )
222275 if (! any(present )) return (rep(NA , length(vals ))) # all-NA: caller's choice
223276
224- # Integer-shaped strings -> numeric, or integer64 when beyond 2^53.
225- if (all(grepl(" ^-?[0-9]+$" , vals [present ]))) {
226- if (any(int64_overflows(vals ), na.rm = TRUE )) return (NULL )
227- nums <- suppressWarnings(as.numeric(vals ))
228- # 2^53 is the largest integer exactly representable as double; values >=
229- # 2^53 + 1 silently round when converted via as.numeric.
230- if (all(is.na(nums ) | abs(nums ) < 2 ^ 53 )) return (nums )
231- return (bit64 :: as.integer64(vals ))
232- }
277+ # Integer-shaped strings share the magnitude-based classifier used by the
278+ # typed int64/uint64 path, so an id column comes back the same type whether
279+ # pandas declared it int64 or handed it to us as an object of Python ints.
280+ if (all(grepl(" ^-?[0-9]+$" , vals [present ])))
281+ return (classify_integer_strings(vals , bigint = bigint , col = col ))
233282
234283 # Float-shaped strings (incl. "nan", "inf", "-inf", scientific notation).
235284 # as.numeric returns NA for unparseable strings; require the parse to
0 commit comments