Integración R + Python
Puentes en proceso, ficheros columnares y documentos polyglot
Sobre interoperabilidad R ↔︎ Python
Casi ningún proyecto serio de ciencia de datos vive en un solo lenguaje. R domina la modelización estadística clásica y el ecosistema Bioconductor. Python domina el aprendizaje profundo, el tooling productivo y gran parte de la ingeniería de datos. La pregunta práctica no es “cuál uso”, sino cómo los compongo sin pagar un coste alto en mantenimiento ni en reproducibilidad.
Hay tres patrones que cubren la inmensa mayoría de los casos. Conviene escoger conscientemente cuál aplicar antes de empezar:
- En proceso: una sesión de un lenguaje carga al intérprete del otro y le pasa objetos por referencia.
reticulatelo hace desde R.rpy2lo hace desde Python. Latencia mínima, pero acoplas dos runtimes en el mismo proceso y heredas la fragilidad del más débil (típicamente la coexistencia con entornosconda). - Basado en ficheros: cada lenguaje vive en su propio proceso y se intercambian datos a través de un formato columnar eficiente. Apache Arrow (en memoria), Parquet (en disco, comprimido) y Feather (Arrow IPC en disco, sin compresión) son las opciones razonables. Cero acoplamiento de runtimes, ideal para pipelines reproducibles y para CI.
- Polyglot a nivel de documento: Quarto ejecuta chunks de R y de Python en el mismo
.qmdy comparte objetos víareticulatepor debajo. Útil para informes y notebooks. No es el patrón adecuado para código de producción.
Tres reglas que evitan los problemas más comunes:
- No mezcles patrones sin necesidad. Si ya tienes Arrow funcionando, no añadas
reticulate“para una llamada rápida”: acabas debiendo dos cadenas de dependencias. - Aísla siempre el entorno de Python. Tanto
reticulatecomorpy2se rompen cuando R o el sistema toman un Python distinto del esperado. Fija el intérprete explícitamente. - Coerciones de tipos en la frontera. Factor ↔︎ Categorical, integer R (32 bits) ↔︎ int Python (arbitrary precision),
NA↔︎None/NaN: revisa los tipos en cada cruce, no asumas que el puente “hace lo razonable”.
Esta página cataloga las cinco piezas que cubren los tres patrones, en orden de menor a mayor desacoplamiento: primero los puentes en proceso (reticulate, rpy2), luego el intercambio columnar (pyarrow + Parquet/Feather) y finalmente el patrón polyglot a nivel de documento (Quarto).
reticulate
reticulate es el paquete de R que embebe un intérprete de Python dentro de la sesión de R. Permite llamar funciones Python, importar módulos, ejecutar .py completos y compartir objetos casi sin fricción. La conversión entre data.frame ↔︎ pandas.DataFrame, list ↔︎ list, vectores atómicos ↔︎ numpy.array se hace automáticamente.
Mantenido por Posit. Es la pieza de interoperabilidad más extendida en R y la que Quarto utiliza por debajo para chunks de Python.
Cuándo usarlo
- Necesitas llamar a una librería Python concreta (scikit-learn, PyTorch, HuggingFace, scvi-tools) desde un análisis principalmente escrito en R.
- Quieres un único proceso, un único log, un único entorno de error. La sesión R es la “dueña” del flujo y Python es una librería invitada.
- Estás trabajando dentro de un
.qmdpolyglot, Quarto delega enreticulatepara los chunks de Python.
Cuándo NO usarlo
- Si el flujo principal es Python. Forzar
reticulatedesde R cuando la lógica natural vive en Python suele degenerar. Mejor invertir la dirección (script Python que llama a R conrpy2) o desacoplar vía ficheros. - Si los intercambios son grandes y repetidos. La conversión
data.frame↔︎pandasno es gratis. Por encima de unos millones de filas con muchas idas y vueltas, Arrow/Parquet es más eficiente. - Si el proyecto debe correr en CI ligero. Mantener un Python concreto disponible en cada job es una carga. Un pipeline desacoplado por ficheros es más simple.
Conceptos clave
- Selección del intérprete.
use_python(),use_virtualenv(),use_condaenv()o la variable de entornoRETICULATE_PYTHON. Una vez Python se ha inicializado en la sesión no se puede cambiar, fíjalo antes de la primera llamada. - Conversión automática vs manual. Por defecto los objetos cruzan convertidos. Si quieres trabajar con el objeto Python “tal cual” (p. ej. un
torch.Tensor), usaimport("torch", convert = FALSE)y luegopy_to_r()puntualmente. r_to_py()/py_to_r(). Conversión explícita. Útil para inspeccionar tipos en la frontera.py$objyr.obj. Desde R,py$xaccede al objetoxdel namespace Python. Desde un chunk de Python en Quarto,r.xaccede al objeto R.- Entornos.
py_install("paquete", envname = "r-tessera")instala dentro del entorno gestionado por reticulate. Conviene fijarlo en.Rprofiledel proyecto o en_quarto.yml.
Patrón mínimo
library(reticulate)
# Fijar el intérprete ANTES de la primera llamada
use_virtualenv("r-tessera", required = TRUE)
# Importar módulos como si fueran namespaces R
np <- import("numpy")
sk <- import("sklearn.ensemble")
# Datos R -> Python (conversión automática)
x <- as.matrix(iris[, 1:4])
y <- as.integer(iris$Species) - 1L # sklearn quiere 0-indexed
clf <- sk$RandomForestClassifier(n_estimators = 200L, random_state = 42L)
clf$fit(x, y)
preds <- clf$predict(x) # numpy.array -> integer vector R
table(preds, y)Y desde un chunk Python equivalente en Quarto, accediendo a objetos R:
import numpy as np
from sklearn.ensemble import RandomForestClassifier
x = np.asarray(r.iris.iloc[:, :4])
y = r.iris["Species"].cat.codes.values
clf = RandomForestClassifier(n_estimators=200, random_state=42).fit(x, y)Trampas habituales
- Integers. R interpreta
200comodouble. Muchas APIs Python (sklearn, numpy) requierenint. Usa el sufijoL(200L) oas.integer()al pasar argumentos. - Factores.
factorR no esCategoricalpandas. Si lo pasas tal cual recibes un vector de strings, perdiendo el orden de niveles. Convierte a entero (as.integer(factor) - 1L) o explícitamente apd.Categoricalconr_to_py()y un paso intermedio. NA. Cruzan comoNaNen columnas numéricas y comoNoneen columnas tipo objeto. En enteros R sin extensión,NA_integer_puede llegar como-2147483648(el centinela interno), comprueba siempre el dtype resultante en pandas.- Múltiples Python en el sistema. Reticulate elige uno con su propia heurística (
reticulate::py_discover_config()te dice cuál). Si tu CI tiene varios, fíjalo conRETICULATE_PYTHONouse_virtualenv(..., required = TRUE). - Conda + reticulate en Windows corporativo. Conflictos clásicos con SSL/PATH cuando el usuario tiene Anaconda y reticulate intenta crear su propio entorno. Usa
virtualenvo fija un únicocondaenvexplícito.
Enlaces
Relacionados en esta página
rpy2, el puente simétrico, desde Python.Quarto polyglot, apoyado en reticulate por debajo.pyarrow, alternativa desacoplada cuando los intercambios pesan.
rpy2
rpy2 es el puente equivalente desde el lado contrario: embebe un intérprete de R dentro de una sesión Python. La sesión Python es la dueña del flujo y R aparece como una librería invitada. Mantiene la mayor parte del API de R accesible desde Python (modelos, gráficos, paquetes Bioconductor, etc.) y se integra bien con pandas vía rpy2.robjects.pandas2ri.
Es la opción natural cuando el proyecto principal está en Python (notebook, servicio, pipeline de ML) y necesitas puntualmente la maquinaria estadística de R: survival, lme4, lavaan, DESeq2, etc.
Cuándo usarlo
- El flujo principal vive en Python y necesitas un test estadístico o modelo que solo está bien implementado en R.
- Quieres reutilizar un paquete Bioconductor (
DESeq2,edgeR,limma,phyloseq) desde un worker de Python. - Integración en un servicio Python existente, no quieres cambiar el lenguaje principal del servicio por un único paso.
Cuándo NO usarlo
- Si el flujo principal es R. Mejor
reticulate. Mantener una sesión R como invitada dentro de Python es más frágil (instalación de R,R_HOME, paquetes Bioconductor) que la dirección opuesta. - Si los intercambios pesan o se repiten. Igual que con reticulate: por encima de cierto volumen, intercambio columnar por ficheros.
- Si despliegas en contenedores ligeros. Necesitas R completo instalado en la imagen, con sus paquetes compilados. La imagen crece notablemente.
Conceptos clave
- Inicialización.
rpy2.robjectscarga R al importarse. La variableR_HOMEdebe apuntar al R correcto antes del import. En entornos virtuales conviene fijarla explícitamente. robjects.rcomo puerta de entrada.robjects.r["nombre"]accede a cualquier objeto/función del entorno global de R.robjects.r('expr')evalúa una expresión.- Activación de conversores. Por defecto los objetos R llegan a Python como
ListVector,FloatVector, etc.pandas2ri.activate()o el contextolocalconverter()añaden la conversión automática apandas.DataFrame/numpy.array. importr(). Importa un paquete R como módulo Python.base = importr("base"),stats = importr("stats"),deseq2 = importr("DESeq2").- Contextos de conversión. El patrón moderno recomendado es usar
localconverteren lugar deactivate()global. Evita efectos colaterales en otras partes del código.
Patrón mínimo
import pandas as pd
from rpy2 import robjects
from rpy2.robjects import pandas2ri
from rpy2.robjects.packages import importr
from rpy2.robjects.conversion import localconverter
base = importr("base")
stats = importr("stats")
df = pd.DataFrame({
"y": [2.3, 3.1, 4.0, 5.2, 6.1, 7.4],
"x": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
"g": ["a", "a", "a", "b", "b", "b"],
})
# Conversión pandas <-> R data.frame solo dentro del contexto
with localconverter(robjects.default_converter + pandas2ri.converter):
r_df = robjects.conversion.py2rpy(df)
# Ajustar un modelo lineal en R y traer el resultado a Python
robjects.globalenv["df"] <- r_df # asignar a R
fit = robjects.r('lm(y ~ x + g, data = df)')
print(base.summary(fit))
# Coeficientes como numpy
with localconverter(robjects.default_converter + pandas2ri.converter):
coefs = robjects.conversion.rpy2py(stats.coef(fit))Trampas habituales
- GIL. R no es thread-safe desde rpy2: no llames a R desde múltiples hilos del mismo proceso Python. Para paralelizar, lanza procesos separados.
R_HOMEambiguo en CI. Si el sistema tiene varios R, rpy2 puede no elegir el correcto. FijaR_HOMEyPATHexplícitamente antes de importar.- Conversores globales.
pandas2ri.activate()modifica el conversor global y puede sorprender a librerías terceras que importan rpy2 después. Prefierelocalconvertercasi siempre. - Factores. Cruzan como
FactorVector(noCategoricalpandas) salvo dentro del conversor. Comprueba el tipo si vas a usarlos en agrupaciones. - Diferencia de versiones. rpy2 está fuertemente acoplado a la versión de R con la que se compila. Actualizar R sin reinstalar rpy2 rompe el binding con errores opacos.
Enlaces
Relacionados en esta página
reticulate, el puente simétrico, desde R.pyarrow, alternativa desacoplada cuando los intercambios pesan.
pyarrow
pyarrow es la implementación Python de Apache Arrow, el estándar columnar en memoria que sustenta la mayor parte de la analítica de datos moderna. Para interoperar con R, su papel es doble:
- Formato en memoria compartido entre R (
arrowpackage) y Python (pyarrow), zero-copy dentro del mismo proceso si se monta víareticulate, o vía IPC entre procesos. - Lectura/escritura de Parquet y Feather, los dos formatos columnares en disco que ambos lenguajes leen sin pérdida de tipo.
Es la forma más sólida y reproducible de mover datos entre R y Python cuando los flujos viven en procesos distintos. Especialmente cuando se trata de tablas anchas, particionadas o que conviene leer por columnas.
Cuándo usarlo
- Pipeline en dos pasos: un script R produce datos, un script Python los consume (o viceversa). Cada script gestiona sus propias dependencias.
- Tablas grandes (millones de filas, muchas columnas) que cargas parcialmente, Parquet permite leer columnas y filtros sin descomprimir todo.
- Necesitas reproducibilidad estricta: el formato Arrow/Parquet preserva tipos (decimal, timestamp con zona, listas anidadas) mejor que CSV.
Cuándo NO usarlo
- Tablas pequeñas y un solo intercambio puntual. Para un
data.framede unas miles de filas,reticulateorpy2son más directos y no añaden una dependencia binaria. - Si no controlas el entorno de despliegue. Arrow es una dependencia compilada relativamente pesada. En entornos restringidos puede ser un problema.
Conceptos clave
- Parquet vs Feather. Parquet está comprimido, optimizado para almacenamiento prolongado e interoperabilidad con motores OLAP (DuckDB, Spark, BigQuery). Feather (Arrow IPC) es esencialmente un volcado del layout en memoria, ideal para handoffs rápidos entre R y Python sin compresión.
pyarrow.Table↔︎pandas.DataFrame.table.to_pandas()ypa.Table.from_pandas(df). La conversión preserva tipos cuando es posible. Revisa el schema si tienes tipos complejos.- Datasets particionados.
pyarrow.datasetpermite leer un directorio Parquet con particiones (year=2025/month=04/) aplicando predicate pushdown. - Tipo
timestampcon zona. Arrow lo preserva. CSV no. Esta es una razón frecuente para migrar a Parquet. nanoarrow/arrow::write_feather()en R producen ficheros quepyarrowlee sin tocar el schema.
Patrón mínimo
Lado R, escribir Parquet:
library(arrow)
# Cualquier data.frame, tibble, o dataset arrow
write_parquet(iris, "iris.parquet")
# Variante Feather (más rápido, sin comprimir)
write_feather(iris, "iris.feather")Lado Python, leer y operar:
import pyarrow.parquet as pq
import pyarrow.feather as feather
# Lectura completa
tbl = pq.read_table("iris.parquet")
df = tbl.to_pandas()
# Lectura selectiva: solo dos columnas, con filtro
tbl2 = pq.read_table(
"iris.parquet",
columns=["Sepal.Length", "Species"],
filters=[("Species", "=", "setosa")],
)
# Feather equivalente
df_fea = feather.read_feather("iris.feather")Trampas habituales
- Nombres con punto. R usa
Sepal.Length. Python lo acepta pero requieredf["Sepal.Length"](nodf.Sepal.Length). Considera renombrar asnake_caseen la frontera si el destino es Python. - Tipos enteros. R no distingue
int32deint64endata.framebase por defecto. Arrow sí. Si el destino Python esperaint64, fija el tipo explícitamente al escribir. factor↔︎dictionary. Arrow representafactorcomodictionary<string>. En pandas llega comoCategoricalDtype. Si lo quieres como string puro, convierte antes de escribir.NAvsNaN. En columnas numéricas el comportamiento es consistente. En columnas string,NAR se convierte enNone(que pandas representa comoNaNen object o comopd.NAcon dtypestring[pyarrow]). Decide explícitamente qué dtype usas en el lado pandas.- Versiones de Arrow.
arrowR ypyarrowdeben ser razonablemente compatibles. Los breaking changes son raros pero ocurren. Fija versiones enrenv/pyproject.toml.
Enlaces
Relacionados en esta página
reticulateyrpy2, alternativas en proceso.Quarto polyglot, Arrow es ideal para cachear datos entre chunks costosos.
Quarto polyglot
Quarto permite un único documento .qmd con chunks de R y de Python en la misma renderización. Internamente, los chunks de Python se ejecutan vía reticulate cuando el motor es knitr, o vía jupyter (kernel propio) cuando se declara explícitamente. La elección del motor cambia las reglas de compartición de objetos.
Es el patrón natural para informes, notebooks y documentación de análisis que combinan, por ejemplo, ingesta y limpieza en R con un modelo de deep learning entrenado/inferido en Python, todo dentro del mismo render.
Cuándo usarlo
- Informes y vignettes donde una parte del análisis vive cómoda en R (estadística clásica, gráficos
ggplot2, paquetes Bioconductor) y otra en Python (modelo entrenado en PyTorch, llamada a una API). - Material docente y guías donde queremos mostrar el mismo análisis en ambos lenguajes.
- Notebooks de proyecto que un equipo bilingüe edita.
Cuándo NO usarlo
- Código de producción. Un
.qmdes un artefacto de informe, no un módulo. Para producción, separa los pasos en scripts/paquetes y deja el.qmdsolo para reporting. - Si el equipo no domina los dos lenguajes. Un documento polyglot multiplica la carga cognitiva. Si solo una persona escribe R y otra Python, mejor dos cuadernos separados conectados por Parquet/Arrow.
- Si el render entra en un CI restrictivo. Necesitas R + Python disponibles y bien configurados en la imagen. Cualquier desalineación rompe el render.
Conceptos clave
- Motor
knitr(por defecto). Cada chunk Python se ejecuta conreticulatedentro de la misma sesión R.r.objetoaccede desde Python a objetos R.py$objetoaccede desde R a objetos Python. - Motor
jupyter. Se declara conengine: jupyteren el front matter. Todos los chunks corren en un kernel Jupyter. Los chunks de R requieren el kernelIRkernel. No comparten estado con un proceso R independiente. _quarto.ymlcentraliza la configuración: motor, intérprete Python, kernel Jupyter, freeze de resultados, etc.freeze: auto. Almacena los resultados ejecutados en_freeze/. Esencial para evitar recomputar análisis pesados en cada render y para que el CI no necesite todas las dependencias.- Caché de chunks.
cache: truepor chunk. Complementario afreeze. La política habitual esfreezepara todo el documento +cachepara chunks muy lentos durante la edición.
Patrón mínimo
Front matter del .qmd:
---
title: "Análisis polyglot R + Python"
format: html
execute:
freeze: auto
---Chunk R que prepara los datos:
# Quarto chunk: {r prep}
library(dplyr)
library(arrow)
datos <- iris |>
filter(Species != "setosa") |>
mutate(target = as.integer(Species == "virginica"))
write_feather(datos, "_cache/iris_clean.feather")
datos |> head()Chunk Python que entrena un modelo y devuelve resultados:
# Quarto chunk: {python model}
import pyarrow.feather as feather
from sklearn.linear_model import LogisticRegression
# Lectura desde el feather producido por R (más robusto que cruzar el data.frame
# por reticulate cuando el dataset crece)
df = feather.read_feather("_cache/iris_clean.feather")
X = df[["Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"]]
y = df["target"]
clf = LogisticRegression(max_iter=1000).fit(X, y)
clf.coef_Chunk R que recoge el objeto Python:
# Quarto chunk: {r read-back}
coefs <- py$clf$coef_ # numpy array -> matriz R
colnames(coefs) <- colnames(py$X)
coefsTrampas habituales
- Motor cambiado a mitad del proyecto. Pasar de
knitrajupytercambia las reglas de compartición de objetos (py$/r.solo funcionan conknitr+reticulate). Decide el motor al principio. reticulateno inicializado antes del primer chunk Python. Si el primer chunk Python no encuentra el intérprete esperado, Quarto inicializa con cualquiera del sistema. FijaRETICULATE_PYTHONen_quarto.ymlo en.Rprofile.freezecorrompido tras cambios de dependencias. Si actualizas un paquete pero_freeze/mantiene resultados antiguos, los gráficos del render no reflejan el código actual. Borra_freeze/ante cambios estructurales.- Conflictos de nombres entre
r.dfypy$df. Mantén convenciones claras (p. ej.df_rydf_py). Reasignar el mismo nombre cruza estados de forma sorprendente. - Outputs no determinísticos. Modelos con
random_stateno fijado producen diffs ruidosos en renders sucesivos. Fija semillas en ambos lenguajes.
Enlaces
Relacionados en esta página
reticulate, motor por debajo de los chunks Python enknitr.pyarrow, vía recomendada para cachear datos entre chunks costosos.
pickle / feather / parquet como intercambio
Cuando solo necesitas pasar un objeto entre dos procesos independientes, sin compartir runtime ni renderizar nada conjunto, la opción correcta es serializar a un fichero. Las tres alternativas razonables, y por qué elegir cada una:
- Parquet (recomendado por defecto). Columnar, comprimido, schema explícito, leído nativamente por R (
arrow), Python (pyarrow,pandas), DuckDB, Spark, BigQuery, Polars. Es el formato más portable. - Feather / Arrow IPC. Igual de portable que Parquet pero sin compresión y con un layout casi idéntico al de memoria. Más rápido de leer y escribir. Ocupa más en disco. Indicado para caches intermedios de un pipeline corto.
- Pickle. Específico de Python,
pickle.dump()serializa cualquier objeto Python, incluidos modelos sklearn, redes neuronales, etc. Solo legible desde Python. Útil cuando lo que pasas no es una tabla (un modelo entrenado, un grafo de configuración) y no necesitas que R lo lea.
Cuándo usarlo
- Parquet/Feather: cuando el objeto es tabular (
data.frame/DataFrame). Especialmente si los dos lados son procesos distintos en máquinas distintas o si el dataset es grande. - Pickle: cuando el objeto es un modelo Python ya entrenado, una estructura compleja Python-específica, o un checkpoint intermedio dentro de un pipeline puramente Python. No lo uses para intercambiar con R.
Cuándo NO usarlo
- Pickle a través de red o entre versiones distintas de Python/librerías. Es un formato frágil: cambia la versión de sklearn y el pickle deja de cargar. Para persistencia a largo plazo usa
joblib(mejor que pickle para arrays grandes) o formatos específicos como ONNX (modelos) o Parquet (datos). - Pickle desde fuentes no confiables. Ejecuta código arbitrario en deserialización. Nunca cargues un pickle que no controles.
Conceptos clave
joblib.dump/joblib.loades el preferido en el ecosistema scikit-learn para modelos grandes: maneja eficientementenumpy.arrayde gran tamaño.- Versionado del modelo. Si vas a guardar un modelo Python, persiste también el
requirements.txto elpyproject.tomlque lo produjo. Sin eso, la carga futura puede fallar silenciosamente. ONNXes el formato neutro de intercambio de modelos entre frameworks (PyTorch ↔︎ TF ↔︎ ONNX Runtime ↔︎ scikit-learn víaskl2onnx). Útil cuando R necesita servir el modelo (onnxruntimetiene binding R víareticulate).
Patrón mínimo
Serializar un modelo Python:
import joblib
from sklearn.ensemble import RandomForestClassifier
clf = RandomForestClassifier(n_estimators=200).fit(X, y)
joblib.dump(clf, "models/rf.joblib")Cargarlo en otro proceso Python:
import joblib
clf = joblib.load("models/rf.joblib")
clf.predict(X_new)Para datos tabulares, usa Parquet/Feather como se muestra en la sección de pyarrow. Para servir un modelo Python desde R sin instalar Python en producción, exporta a ONNX y consume con onnxruntime.
Trampas habituales
- Pickle entre versiones. Un pickle producido con sklearn 1.3 puede no cargar en 1.5. Pin versionado en el entorno de producción.
- Tamaño. Pickles de modelos grandes (>1 GB) son lentos de cargar.
joblibconcompress=3reduce significativamente sin sacrificar velocidad de carga. - Pickle de objetos con referencias a clases propias. Si la clase no está disponible en el
PYTHONPATHal cargar,picklefalla conAttributeErroropaco. Empaqueta tus clases en un paquete instalable, no las dejes en un script suelto. - Mezclar Parquet con pickle. Si tu artefacto es un par “datos + modelo”, guarda datos como Parquet y modelo como joblib. No metas todo en un pickle: pierdes interoperabilidad con R y con cualquier herramienta no-Python.
Enlaces
Relacionados en esta página
pyarrow, formato de elección para datos tabulares entre R y Python.reticulateyrpy2, alternativas en proceso cuando no quieres pasar por disco.