ryp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ryp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Michael Wainberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ryp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,584 @@
1
+ Metadata-Version: 2.1
2
+ Name: ryp
3
+ Version: 0.1.0
4
+ Summary: R inside Python
5
+ Author-email: Michael Wainberg <m.wainberg@utoronto.ca>
6
+ License: MIT License
7
+ Project-URL: Homepage, https://github.com/Wainberg/ryp
8
+ Project-URL: Bug Tracker, https://github.com/Wainberg/ryp/issues
9
+ Keywords: R,rpy2,reticulate,tidyverse,ggplot,ggplot2,arrow
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: Software Development
24
+ Requires-Python: >=3.7
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: cffi
28
+ Requires-Dist: pyarrow
29
+
30
+ # ryp: R inside Python
31
+
32
+ ryp is a minimalist, powerful Python library for:
33
+ - running R code inside Python
34
+ - quickly transferring huge datasets between Python (NumPy/pandas/polars) and R
35
+ without writing to disk
36
+ - interactively working in both languages at the same time
37
+
38
+ ryp is an alternative to the widely used [rpy2](https://github.com/rpy2/rpy2)
39
+ library. Compared to rpy2, ryp provides:
40
+ - increased stability
41
+ - a much simpler API, with less of a learning curve
42
+ - interactive printouts of R variables that match what you'd see in R
43
+ - a full-featured R terminal inside Python for interactive work
44
+ - inline plotting in Jupyter notebooks (requires the `svglite` R package)
45
+ - much faster data conversion with [Arrow](https://arrow.apache.org) (also
46
+ provided by [rpy2-arrow](https://github.com/rpy2/rpy2-arrow))
47
+ - support for *every* NumPy, pandas and polars data type representable in base
48
+ R, no matter how obscure
49
+ - support for sparse arrays/matrices
50
+ - recursive conversion of containers like R lists, Python tuples/lists/dicts,
51
+ and S3/S4/R6 objects
52
+ - full Windows support
53
+
54
+ ryp does the opposite of the
55
+ [reticulate](https://rstudio.github.io/reticulate) R library, which runs Python
56
+ inside R.
57
+
58
+ ## Installation
59
+
60
+ Install ryp via pip:
61
+
62
+ ```bash
63
+ pip install ryp
64
+ ```
65
+
66
+ conda:
67
+
68
+ ```bash
69
+ conda install ryp
70
+ ```
71
+
72
+ or mamba:
73
+
74
+ ```bash
75
+ mamba install ryp
76
+ ```
77
+
78
+ Or, install the development version via pip:
79
+
80
+ ```bash
81
+ pip install git+https://github.com/Wainberg/ryp
82
+ ```
83
+
84
+ ryp's only mandatory dependencies are:
85
+ - Python 3.7+
86
+ - R
87
+ - the [cffi](https://cffi.readthedocs.io/en/stable) Python package
88
+ - the [pyarrow](https://arrow.apache.org/docs/python) Python package, which
89
+ includes [NumPy](https://numpy.org) as a dependency
90
+ - the [arrow](https://arrow.apache.org/docs/r) R library
91
+
92
+ R and the arrow R library are automatically installed when installing ryp via
93
+ conda or mamba, but not via pip. ryp uses the R installation pointed to by the
94
+ environment variable `R_HOME`, or if `R_HOME` is not defined or not a
95
+ directory, by running `R RHOME` through `subprocess.run()`.
96
+
97
+ ryp also has several optional dependencies, which are not installed
98
+ automatically with pip, conda or mamba. These are:
99
+ - [pandas](https://pandas.pydata.org), for `format='pandas'`
100
+ - [polars](https://pola.rs), for `format='polars'`
101
+ - [SciPy](https://scipy.org) and the
102
+ [Matrix](https://cran.r-project.org/web/packages/Matrix) R library, for sparse
103
+ matrices
104
+ - the [svglite](https://cran.r-project.org/web/packages/svglite) R library, for
105
+ inline plotting in Jupyter notebooks
106
+
107
+ ## Functionality
108
+
109
+ ryp consists of just three core functions:
110
+
111
+ 1. `r(R_code)` runs a string of R code. `r()` with no arguments opens up an R
112
+ terminal inside your Python terminal for interactive work.
113
+ 2. `to_r(python_object, R_variable_name)` converts a Python object into an R
114
+ object named `R_variable_name`.
115
+ 3. `to_py(R_statement)` converts the R object produced by evaluating
116
+ `R_statement` to Python. `R_statement` may be a single variable name, or a
117
+ more complex code snippet that evaluates to the R object you'd like to
118
+ convert.
119
+
120
+ and two more functions, `get_config()` and `set_config()`, for getting and
121
+ setting ryp's global configuration options.
122
+
123
+ ### `r()`
124
+
125
+ ```python
126
+ r(R_code: str = ...) -> None
127
+ ```
128
+
129
+ `r(R_code)` runs a string of R code inside ryp's R interpreter, which is
130
+ embedded inside Python. It can contain multiple statements separated by
131
+ semicolons or newlines (e.g. within a triple-quoted Python string). It returns
132
+ `None`; use `to_py()` instead if you would like to convert the result back to
133
+ Python.
134
+
135
+ `r()` with no arguments opens up an R terminal inside your Python terminal
136
+ for interactive debugging. Press `Ctrl + D` to exit back to the Python
137
+ terminal. R variables defined from Python will be available in the R terminal,
138
+ and variables defined in the R terminal will be available from Python once you
139
+ exit:
140
+
141
+ ```python
142
+ >> > from ryp.ryp.ryp import r
143
+ >> > r('a = 1')
144
+ >> > r()
145
+ > a
146
+ [1]
147
+ 1
148
+ > b < - 2
149
+ >
150
+ >> > r('b')
151
+ [1]
152
+ 2
153
+ ```
154
+
155
+ Note that the default value for `R_code` is the special sentinel value `...`
156
+ (`Ellipsis`) rather than `None`. This stops users from inadvertently opening
157
+ the terminal when passing a variable that is supposed to be a string but is
158
+ unexpectedly `None`.
159
+
160
+ ### `to_r()`
161
+
162
+ ```python
163
+ to_r(python_object: object, R_variable_name: str, *,
164
+ format: Literal['keep', 'matrix', 'data.frame'] | None = None,
165
+ rownames: object = None, colnames: object = None) -> None
166
+ ```
167
+
168
+ `to_r(python_object, R_variable_name)` converts `python_object` to R, adding it
169
+ to R's global namespace (`globalenv`) as a variable named `R_variable_name`.
170
+
171
+ If `python_object` is a container (`list`, `tuple`, or `dict`), `to_r()`
172
+ recursively converts each element and returns an R named list (if
173
+ `python_object` is a `dict`) or unnamed list (if `python_object` is a `list` or
174
+ `tuple`).
175
+
176
+ #### The `format` argument
177
+
178
+ By default (`format='keep'`), ryp converts polars and pandas DataFrames (and
179
+ pandas MultiIndexes) into R data frames, and 2D NumPy arrays into R matrices.
180
+ Specify `format='matrix'` to convert everything (even DataFrames) to R matrices
181
+ (in which case all DataFrame columns must have the same type), and
182
+ `format='data.frame'` to convert everything (even 2D NumPy arrays) to R
183
+ data frames.
184
+
185
+ `format` must be `None` unless `python_object` is a DataFrame, MultiIndex or 2D
186
+ NumPy array – or unless `python_object` is a `list`, `tuple`, or `dict`, in
187
+ which case the `format` will apply recursively to any DataFrames, MultiIndexes
188
+ or 2D NumPy arrays it contains.
189
+
190
+ #### The `rownames` and `colnames` arguments
191
+
192
+ Since NumPy arrays, polars DataFrames and Series, and scipy sparse arrays and
193
+ matrices lack row and column names, you can specify these separately via the
194
+ `rownames` and/or `colnames` arguments, and they will be added to the converted
195
+ R object. `rownames` and `colnames` can be lists, tuples, string Series, or
196
+ categorical Series with string categories, and will be automatically converted
197
+ to R character vectors.
198
+
199
+ `rownames` and `colnames` must match the length or `shape[1]`, respectively, of
200
+ the object being converted. The one exception is that rownames of any length
201
+ may be added to a 0 &times; 0 polars DataFrame, since polars does not have the
202
+ concept of an `N` &times; 0 DataFrame for nonzero `N`. (Dropping all the
203
+ columns of a polars DataFrame always results in a 0 &times; 0 DataFrame, even
204
+ if the original DataFrame had more than 0 rows.)
205
+
206
+ Because Python `bool`, `int`, `float`, and `str` convert to length-1 R vectors
207
+ that support names, you can pass length-1 `rownames` when converting objects of
208
+ these types. You can also pass `rownames` and/or `colnames` when
209
+ `python_object` is a `list`, `tuple`, or `dict`, in which case row and column
210
+ names will only be added to elements that support them. All elements that
211
+ support `rownames` must have the same length as the `rownames`, and similarly
212
+ for the `colnames`.
213
+
214
+ `rownames` cannot be specified if `python_object` is a pandas Series or
215
+ DataFrame (since they already have rownames, i.e. an index), or
216
+ `bytes`/`bytearray` (since these convert to `raw` vectors, which lack
217
+ rownames). `colnames` cannot be specified unless `python_object` is a
218
+ multidimensional NumPy array or scipy sparse array or matrix, or something that
219
+ might contain one (`list`, `tuple`, or `dict`).
220
+
221
+ ### `to_py()`
222
+
223
+ ```python
224
+ to_py(R_statement: str, *,
225
+ format: Literal['polars', 'pandas', 'pandas-pyarrow', 'numpy'] |
226
+ dict[Literal['vector', 'matrix', 'data.frame'],
227
+ Literal['polars', 'pandas', 'pandas-pyarrow',
228
+ 'numpy']] | None = None,
229
+ index: str | Literal[False] | None = None,
230
+ squeeze: bool | None = None) -> Any
231
+ ```
232
+
233
+ `to_py(R_statement)` runs a single statement of R code (which can be as simple
234
+ as a single variable name) and converts the resulting R object to Python.
235
+
236
+ If the object is a list/S3 object, S4 object, or environment/R6 object, it
237
+ recursively converts each attribute/slot/field and returns a Python `dict` (or
238
+ `list`, if the object is an unnamed list). For R6 objects, only public fields
239
+ will be converted.
240
+
241
+ #### The `format` argument
242
+
243
+ By default, or when `format='polars'`, R vectors will be converted to polars
244
+ Series, and R data frames and matrices will be converted to polars DataFrames.
245
+ You can change this by setting the `format` argument to `'pandas'`,
246
+ `'pandas-pyarrow'` (like `'pandas'`, but converting to pyarrow dtypes wherever
247
+ possible) or `'numpy'`. (You can also change the default format, e.g. with
248
+ `set_config(to_py_format='pandas')`.)
249
+
250
+ For finer-grained control, you can set `format` for only certain R variable
251
+ types by specifying a dictionary with `'vector'`, `'matrix'`, and/or
252
+ `'data.frame'` as keys and `'polars'`, `'pandas'`, `'pandas-pyarrow'` and/or
253
+ `'numpy'` as values.
254
+
255
+ `format` must be `None` when `R_statement` evaluates to `NULL`, when it
256
+ evaluates to an array of 3 or more dimensions (these are always converted to
257
+ NumPy arrays), or when the final result would be a Python scalar (see `squeeze`
258
+ below).
259
+
260
+ #### The `index` argument
261
+
262
+ By default, the R object's `names` or `rownames` will become the index (for
263
+ pandas) or the first column (for polars) of the output Python object, named
264
+ `'index'`. Set the `index` argument to a different string to change this name,
265
+ or set `index=False` to not convert the `names`/`rownames`.
266
+
267
+ Note that for polars, the output will be a two-column DataFrame (not a Series!)
268
+ when the input is an R vector, unless `index=False`.
269
+
270
+ When the output is a NumPy array, `names` and `rownames` will always be
271
+ discarded, since numeric NumPy arrays cannot store string indexes except with
272
+ the inefficient `dtype=object`.
273
+
274
+ `index` must be `None` when `format='numpy'`, or when the final result would be
275
+ a Python scalar (see `squeeze` below).
276
+
277
+ #### The `squeeze` argument
278
+
279
+ By default, length-1 R vectors, matrices and arrays will be converted to Python
280
+ scalars instead of Python arrays, Series or DataFrames. Set `squeeze=False` to
281
+ disable this special case. (R data frames are never converted to Python scalars
282
+ even if `squeeze=True`.)
283
+
284
+ `squeeze` must be `None` unless the R object is a vector, matrix or array
285
+ (`raw` vectors don't count, because they always convert to Python scalars).
286
+
287
+ ### `set_config()` and `get_config()`
288
+
289
+ ```python
290
+ set_config(*, to_r_format=None, to_py_format=None, index=None, squeeze=None,
291
+ plot_width: int | float | None = None,
292
+ plot_height: int | float | None = None) -> None
293
+ ```
294
+
295
+ `set_config` sets ryp's configuration settings. Arguments that are `None` are
296
+ left unchanged.
297
+
298
+ For instance, to set pandas as the default format in `to_py()`, run
299
+ `set_config(to_py_format='pandas')`.
300
+
301
+ - `to_r_format`: the default value for the `format` parameter in `to_r()`;
302
+ must be `'keep'` (the default), `'matrix'`, or `'data.frame'`.
303
+ - `to_py_format`: the default value for the `format` parameter in `to_py()`;
304
+ must be `'polars'` (the default), `'pandas'`, `'pandas-pyarrow'`, `'numpy'`,
305
+ or a dictionary with one of those four Python formats and/or `None` as values
306
+ and `'vector'`, `'matrix'` and/or `'data.frame'` as keys. If certain keys are
307
+ missing or have `None` as the format, leave their format unchanged.
308
+ - `index`: the default value for the `index` parameter in to_py(); must be a
309
+ string (default: `'index'`) or `False`.
310
+ - `squeeze`: the default value for the `squeeze` parameter in `to_py()`; must
311
+ be `True` (the default) or `False`.
312
+ - `plot_width`: the width, in inches, of inline plots in Jupyter notebooks;
313
+ must be a positive number. Defaults to 6.4 inches, to match Matplotlib's
314
+ default.
315
+ - `plot_height`: the height, in inches, of inline plots in Jupyter notebooks;
316
+ must be a positive number. Defaults to 4.8 inches, to match Matplotlib's
317
+ default.
318
+
319
+ ```python
320
+ get_config() -> dict[str, dict[str, str] | str | bool | int]
321
+ ```
322
+
323
+ `get_config` returns the current configuration options as a dictionary, with
324
+ keys `to_r_format`, `to_py_format`, `index`, `squeeze`, `plot_width`, and
325
+ `plot_height`.
326
+
327
+ ## Conversion rules
328
+
329
+ ### Python to R (`to_r()`)
330
+
331
+ | Python | R |
332
+ |-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
333
+ | `None` | `NULL` (if scalar) or `NA` (if inside NumPy, pandas or polars) |
334
+ | `nan` | `NaN` (if scalar or inside polars) or `NA` (if inside NumPy or pandas) |
335
+ | `pd.NA` | `NA` |
336
+ | `pd.NaT`, `np.datetime64('NaT')`, `np.timedelta64('NaT')` | `NA` |
337
+ | `bool` | length-1 `logical` vector |
338
+ | `int` | length-1 `integer` (if `abs(x) <= 2_147_483_647`) or `bit64::integer64` vector |
339
+ | `float` | length-1 `numeric` vector |
340
+ | `str` | length-1 `character` vector |
341
+ | `complex` | length-1 `complex` vector |
342
+ | `datetime.date` | length-1 `Date` vector |
343
+ | `datetime.datetime` | length-1 `POSIXct` vector |
344
+ | `datetime.timedelta` | length-1 `difftime(units='secs')` vector |
345
+ | `datetime.time` (`tzinfo` must be `None`) | length-1 `hms::hms` vector |
346
+ | `bytes`, `bytearray` | `raw` vector |
347
+ | `list`, `tuple` | unnamed list |
348
+ | `dict` (all keys must be strings) | named list |
349
+ | polars Series, pandas Series<sup>&ast;</sup>, pandas `Index` | vector |
350
+ | polars DataFrame, pandas DataFrame<sup>&ast;</sup>, pandas `MultiIndex` | matrix<sup>&dagger;</sup> (if `format == 'matrix'`; all columns must have same data type) or `data.frame` |
351
+ | 1D NumPy array | vector |
352
+ | 2D NumPy array | `data.frame` (if `format == 'data.frame'`) or matrix<sup>&dagger;</sup> |
353
+ | &ge; 3D NumPy array | array<sup>&dagger;</sup> |
354
+ | 0D NumPy array (e.g. `np.array(1)`), NumPy generic (e.g. `np.int32(1)`) | length-1 vector |
355
+ | `csr_array`, `csr_matrix` | `dgRMatrix` (if int or float), `lgRMatrix` (if boolean), -- (if complex) |
356
+ | `csc_array`, `csc_matrix` | `dgCMatrix` (if int or float), `lgCMatrix` (if boolean), -- (if complex) |
357
+ | `coo_array`, `coo_matrix` | `dgTMatrix` (if int or float), `lgTMatrix` (if boolean), -- (if complex) |
358
+
359
+ #### NumPy data types
360
+
361
+ | Python | R |
362
+ |---------------------------------------------|----------------------------------------------------------------|
363
+ | `bool` | `logical` |
364
+ | `int8`, `uint8`, `int16`, `uint16`, `int32` | `integer` |
365
+ | `uint32`, `uint64` | `integer` (if `x <= 2_147_483_647`) or `numeric` |
366
+ | `int64` | `integer` (if `abs(x) <= 2_147_483_647`) or `bit64::integer64` |
367
+ | `float16`, `float32`, `float64`, `float128` | `numeric` (note: `float128` loses precision) |
368
+ | `complex64`, `complex128` | `complex` |
369
+ | `bytes` (e.g. `'S1'`) | -- |
370
+ | `str`/`unicode` (e.g. `'U1'`) | `character` |
371
+ | `datetime64` | `POSIXct` |
372
+ | `timedelta64` | `difftime(units='secs')` |
373
+ | `void` (unstructured) | `raw` |
374
+ | `void` (structured) | -- |
375
+ | `object` | depends on the contents<sup>&Dagger;</sup> |
376
+
377
+ #### pandas-specific data types
378
+
379
+ | Python | R |
380
+ |----------------------------------------------------------------------|----------------------------------------------------------------|
381
+ | `BooleanDtype` | `logical` |
382
+ | `Int8Dtype`, `UInt8Dtype`, `Int16Dtype`, `UInt16Dtype`, `Int32Dtype` | `integer` |
383
+ | `UInt32Dtype`, `UInt64Dtype` | `integer` (if `x <= 2_147_483_647`) or `numeric` |
384
+ | `Int64Dtype` | `integer` (if `abs(x) <= 2_147_483_647`) or `bit64::integer64` |
385
+ | `Float32Dtype`, `Float64Dtype` | `numeric` |
386
+ | `StringDtype` | `character` |
387
+ | `CategoricalDtype(ordered=False)` | unordered `factor` |
388
+ | `CategoricalDtype(ordered=True)` | ordered `factor` |
389
+ | `DatetimeTZDtype`, `PeriodDtype` | `POSIXct` |
390
+ | `IntervalDtype`, `SparseDtype` | -- |
391
+
392
+ #### pandas Arrow data types (`pd.ArrowDtype`)
393
+
394
+ | Python | R |
395
+ |------------------------------------------------------------|----------------------------------------------------------------|
396
+ | `pa.bool_` | `logical` |
397
+ | `pa.int8`, `pa.uint8`, `pa.int16`, `pa.uint16`, `pa.int32` | `integer` |
398
+ | `pa.uint32`, `pa.uint64` | `integer` (if `x <= 2_147_483_647`) or `numeric` |
399
+ | `pa.int64` | `integer` (if `abs(x) <= 2_147_483_647`) or `bit64::integer64` |
400
+ | `pa.float32`, `pa.float64` | `numeric` |
401
+ | `pa.string`, `pa.large_string` | `character` |
402
+ | `pa.date32` | `Date` |
403
+ | `pa.date64`, `pa.timestamp` | `POSIXct` |
404
+ | `pa.duration` | `difftime(units='secs')` |
405
+ | `pa.time32`, `pa.time64` | `hms::hms` |
406
+ | `pa.dictionary(any integer type, pa.string(), ordered=0)` | unordered `factor` |
407
+ | `pa.dictionary(any integer type, pa.string(), ordered=1)` | ordered `factor` |
408
+ | `pa.null()` | `vctrs::unspecified` |
409
+
410
+ #### Polars data types
411
+
412
+ | Python | R |
413
+ |---------------------------------------------|----------------------------------------------------------------|
414
+ | `Boolean` | `logical` |
415
+ | `Int8`, `UInt8`, `Int16`, `UInt16`, `Int32` | `integer` |
416
+ | `UInt32`, `UInt64` | `integer` (if `x <= 2_147_483_647`) or `numeric` |
417
+ | `Int64` | `integer` (if `abs(x) <= 2_147_483_647`) or `bit64::integer64` |
418
+ | `Float32`, `Float64` | `numeric` |
419
+ | `Date` | `Date` |
420
+ | `Datetime` | `POSIXct` |
421
+ | `Duration` | `difftime(units='secs')` |
422
+ | `Time` | `hms::hms` |
423
+ | `String` | `character` |
424
+ | `Categorical` | unordered `factor` |
425
+ | `Enum` | ordered `factor` |
426
+ | `Object` | depends on the contents<sup>&Dagger;</sup> |
427
+ | `Null` | `vctrs::unspecified` |
428
+ | `Binary`, `Decimal`, `List`, `Array` | -- |
429
+
430
+ #### Notes
431
+
432
+ <sup>&ast;</sup> For pandas Series and DataFrames, string indexes (and
433
+ categorical indexes where the categories are strings) will be automatically
434
+ converted to `names`/`rownames`. The default index
435
+ (`pd.RangeIndex(len(python_object))`) will be ignored. All other indexes are
436
+ disallowed.
437
+
438
+ <sup>&dagger;</sup> Because R does not support `POSIXct` and `Date` matrices or
439
+ arrays, dates and datetimes cannot be converted to R matrices or arrays.
440
+
441
+ <sup>&Dagger;</sup> For `dtype=object` and `dtype=pl.Object`, the output R type
442
+ depends on the contents, e.g. `'character'` if all elements are strings. Some
443
+ additional notes on ryp's handling of object data types:
444
+ - `None`, `np.nan`, `pd.NA`, `pd.NaT`, `np.datetime64('NaT')`, and
445
+ `np.timedelta64('NaT')` are all treated as missing values &ndash; even for
446
+ polars, where `np.nan` is ordinarily treated as a floating-point number
447
+ rather than a missing value.
448
+ - Length-0 and all-missing data will be converted to the `vctrs::unspecified` R
449
+ type (`vctrs` is part of the tidyverse).
450
+ - If the elements are objects with a mix of types (or datetimes with a mix of
451
+ time zones), Arrow will generally cause the conversion to fail, though mixes
452
+ of related types (e.g. int and float) will be automatically cast to the
453
+ common supertype and succeed.
454
+ - Conversion will also fail if the contents are objects that are not
455
+ representable as R vector elements. This includes `bytes`/`bytearray` (which
456
+ are only representable in R when scalar, as a `raw` vector) and Python
457
+ containers (`list`, `tuple`, and `dict`).
458
+ - pandas `Timedelta` objects will be rounded down to the nearest microsecond,
459
+ following the behavior of Arrow.
460
+
461
+ ### R to Python (`to_py()`)
462
+
463
+ | R | Python |
464
+ |------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
465
+ | `NULL` | `None` |
466
+ | `NA` | `None` (if scalar or `format='polars'`), `None`/`nan`/`pd.NA`/`pd.NaT`/`np.datetime64('NaT', 'us')`/`np.timedelta64('NaT', 'ns')`/etc. (if `format='numpy'` `'pandas'` or `'pandas-pyarrow'`) |
467
+ | `NaN` | `nan` |
468
+ | length-1 vector, matrix or array, `squeeze == False` | scalar |
469
+ | vector or 1D array, `format == 'numpy'` | 1D NumPy array |
470
+ | vector or 1D array, `format == 'pandas'` or `format == 'pandas-pyarrow'` | pandas Series |
471
+ | vector or 1D array, `format == 'polars'` | polars Series (if `index=False`) or two-column DataFrame |
472
+ | matrix or `data.frame`, `format == 'numpy'` | 2D NumPy array |
473
+ | matrix or `data.frame`, `format == 'pandas'` or `format == 'pandas-pyarrow'` | pandas DataFrame |
474
+ | matrix or `data.frame`, `format == 'polars'` | polars DataFrame |
475
+ | &ge; 3D array | NumPy array |
476
+ | unnamed list | `list` |
477
+ | named list, S3 object, S4 object, environment, S6 object | `dict` |
478
+ | `dgRMatrix` | `csr_array(dtype='int32')` |
479
+ | `dgCMatrix` | `csc_array(dtype='int32')` |
480
+ | `dgTMatrix` | `coo_array(dtype='int32')` |
481
+ | `lgRMatrix`, `ngRMatrix` | `csr_array(dtype=bool)` |
482
+ | `lgCMatrix`, `ngCMatrix` | `csc_array(dtype=bool)` |
483
+ | `lgTMatrix`, `ngTMatrix` | `coo_array(dtype=bool)` |
484
+ | formula (`~`) | -- |
485
+
486
+ #### Data types
487
+
488
+ | R | Python scalar | NumPy | pandas | pandas-pyarrow | polars |
489
+ |-----------------------------|--------------------------------------|----------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|--------------------------------------------|
490
+ | `logical` | `bool` | `bool` | `bool` | `ArrowDtype(pa.bool_())` | `Boolean` |
491
+ | `integer` | `int` | `int32` | `int32` | `ArrowDtype(pa.int32())` | `Int32` |
492
+ | `bit64::integer64` | `int` | `int64` | `int64` | `ArrowDtype(pa.int64())` | `Int64` |
493
+ | `numeric` | `float` | `float` | `float` | `ArrowDtype(pa.float64())` | `Float64` |
494
+ | `character` | `str` | `object` (with `str` elements) | `object` (with `str` elements) | `ArrowDtype(pa.string())` | `String` |
495
+ | `complex` | `complex` | `complex128` | `complex128` | `complex128` | -- |
496
+ | `raw` | `bytearray` | -- | -- | -- | -- |
497
+ | unordered `factor` | `str` | `object` (with `str` elements) | `CategoricalDtype(ordered=False)` | `ArrowDtype(pa.dictionary(pa.int8(), pa.string(), ordered=0))` | `Categorical` |
498
+ | ordered `factor` | `str` | `object` (with `str` elements) | `CategoricalDtype(ordered=True)` | `ArrowDtype(pa.dictionary(pa.int8(), pa.string(), ordered=1))` | `Enum` |
499
+ | `POSIXct` without time zone | `datetime.datetime`<sup>&ast;</sup> | `datetime64[us]`<sup>&ast;</sup> | `datetime64[us]`<sup>&ast;</sup> | `ArrowDtype(pa.timestamp('us'))`<sup>&ast;</sup> | `Datetime('us')`<sup>&ast;</sup> |
500
+ | `POSIXct` with time zone | `datetime.datetime`<sup>&ast;</sup> | `datetime64[us]`<sup>&ast;</sup> (time zone discarded) | `DatetimeTZDtype('us', time_zone)`<sup>&ast;</sup> | `ArrowDtype(pa.timestamp('us', time_zone))`<sup>&ast;</sup> | `Datetime('us, time_zone)`<sup>&ast;</sup> |
501
+ | `POSIXlt` | `dict` of scalars | `dict` of NumPy arrays | `dict` of pandas Series | `dict` of pandas Series | `dict` of polars Series |
502
+ | `Date` | `datetime.date` | `datetime64[D]` | `datetime64[ms]` | `ArrowDtype(pa.date32('day'))` | `Date` |
503
+ | `difftime` | `datetime.timedelta`<sup>&ast;</sup> | `timedelta64[ns]` | `timedelta64[ns]` | `ArrowDtype(pa.duration('ns'))` | `Duration(time_unit='ns')` |
504
+ | `hms::hms` | `datetime.time`<sup>&ast;</sup> | `object` (with `datetime.time` elements)<sup>&ast;</sup> | `object` (with `datetime.time` elements)<sup>&ast;</sup> | `ArrowDtype(pa.time64('ns'))`<sup>&ast;</sup> | `Time` |
505
+ | `vctrs::unspecified` | `None` | `object` (with `None` elements) | `object` (with `None` elements) | `ArrowDtype(pa.null())` | `Null` |
506
+
507
+ <sup>&ast;</sup> Due to the limitations of conversion with Arrow, `POSIXct` and
508
+ `hms::hms` values are rounded down to the nearest microsecond when converting
509
+ to Python, except for `hms::hms` when converting to polars. `difftime` values
510
+ are also rounded down to the nearest microsecond, but only when converting to
511
+ scalar `datetime.timedelta` values (which cannot represent nanoseconds).
512
+
513
+ ## Examples
514
+
515
+ 1. Apply R's `scale()` function to a pandas DataFrame:
516
+
517
+ ```python
518
+ >>> import pandas as pd
519
+ >>> from ryp import r, to_py, to_r, set_config
520
+ >>> set_config(to_py_format='pandas')
521
+ >>> data = pd.DataFrame({'a': [1, 2, 3], 'b': [1, 3, 4]})
522
+ >>> to_r(data, 'data')
523
+ >>> r('data')
524
+ a b
525
+ 1 1 1
526
+ 2 2 3
527
+ 3 3 4
528
+ >>> r('data <- scale(data)') # scale the R data.frame
529
+ >>> scaled_data = to_py('data') # convert the R data.frame to Python
530
+ >>> scaled_data
531
+ a b
532
+ 0 -1.0 -1.091089
533
+ 1 0.0 0.218218
534
+ 2 1.0 0.872872
535
+ ```
536
+ Note: we could have just written `to_py('scale(data)')` instead of
537
+ `r('data <- scale(data)')` followed by `to_py('data')`.
538
+
539
+ 2. Run a linear model on a polars DataFrame:
540
+
541
+ ```python
542
+ >>> import polars as pl
543
+ >>> from ryp import r, to_py, to_r
544
+ >>> data = pl.DataFrame({'y': [7, 1, 2, 3, 6], 'x': [5, 2, 3, 2, 5]})
545
+ >>> to_r(data, 'data')
546
+ >>> r('model <- lm(y ~ x, data=data)')
547
+ >>> coef = to_py('summary(model)$coefficients', index='variable')
548
+ >>> p_value = coef.filter(variable='x').select('Pr(>|t|)')[0, 0]
549
+ >>> p_value
550
+ 0.02831035772841884
551
+ ```
552
+
553
+ 3. Recursive conversion, showcasing all the keyword arguments to `to_r()` and
554
+ `to_py()`:
555
+
556
+ ```python
557
+ >>> import numpy as np
558
+ >>> from ryp import r, to_py, to_r
559
+ >>> arrays = {'ints': np.array([[1, 2], [3, 4]]),
560
+ ... 'floats': np.array([[0.5, 1.5], [2.5, 3.5]])}
561
+ >>> to_r(arrays, 'arrays', format='data.frame',
562
+ ... rownames = ['row1', 'row2'], colnames = ['col1', 'col2'])
563
+ >>> r('arrays')
564
+ $ints
565
+ col1 col2
566
+ row1 1 2
567
+ row2 3 4
568
+
569
+ $floats
570
+ col1 col2
571
+ row1 0.5 1.5
572
+ row2 2.5 3.5
573
+ >>> arrays = to_py('arrays', format='pandas', index='foo')
574
+ >>> arrays['ints']
575
+ col1 col2
576
+ foo
577
+ row1 1 2
578
+ row2 3 4
579
+ >>> arrays['floats']
580
+ col1 col2
581
+ foo
582
+ row1 0.5 1.5
583
+ row2 2.5 3.5
584
+ ```