Orchestrarea Polyglot, Reproducible Data Science cu Nix și {rixpress}

URMĂREȘTE-NE
16,065FaniÎmi place
1,142CititoriConectați-vă

TL;DR: {rixpress} vă permite să construiți conducte de date în mai multe limbi (R, Python, Julia) în care fiecare pas rulează în propriul mediu reproductibil. Folosește Nix sub capotă. Acum pe CRAN și există chiar și un port Python pe PyPI!

{rixpress} este acum pe CRAN! După cum sa discutat în postările anterioare de pe blog, {rixpress} este un pachet puternic inspirat de {targets} care utilizează Nix ca instrument de bază de automatizare a construcției pentru a construi conducte reproductibile de știință a datelor.

Dar și eu am vrut {rixpress} să fie un instrument de automatizare a construcției agnostic de limbă: conductele sunt definite ca o listă R, dar pot include R, Julia și Python derivatii (gândiți-vă la o derivare ca o etapă de construcție).

{rixpress} vă permite să definiți și să executați conducte complexe, în mai multe limbi, în care fiecare pas se desfășoară într-un mediu propriu, perfect reproductibil, sigilat ermetic.

Deoarece instalarea lucrurilor este atât de ușoară cu Nix, costul utilizării Python sau Julia pentru un proiect este foarte mic. Înainte de Nix, aș încerca din răsputeri să găsesc pachete R echivalente, doar pentru a evita nevoia de a configura un mediu Python, dar acum, dacă chiar trebuie să folosesc Python, nu mă deranjează atât de mult (și pentru că pot delega scrierea Python unui LLM).

Să presupunem că aveți un proiect care folosește Julia, Python și R: fără Nix, {rix} şi {rixpress}configurarea totul și executarea codului va fi destul de enervant. Dar cu instrumentele menționate mai sus? Ușor ca o plăcintă.

Să luăm în considerare un exemplu din economie, în care Julia este folosită pentru a defini un model structural Real Business Cycle (și a simula datele din acesta), Python (cu pachetul său xgboost) este folosit pentru a face predicții din datele simulate, iar R pentru a vizualiza, folosind {ggplot2}. Într-adevăr, s-ar fi putut folosi doar una dintre aceste trei limbi, dar de dragul argumentării, să le folosim pe toate.

Cu {rixpress}întregul flux de lucru poliglot este definit declarativ într-un singur script R. Fiecare pas este un apel de funcție, ceea ce face pipeline ușor de citit și de gestionat.

Începe proiectul cu rixpress::rxp_init()care generează două fișiere, gen-env.R şi gen-pipeline.R. În gen-env.Rvei defini mediul de care ai nevoie:

library(rix)

rix(
  # Pin the environment to a specific date to ensure that all package
  # versions are resolved as they were on this day.
  date = "2025-10-14",

  # 1. R Packages
  # We need packages for plotting, data manipulation, and reading arrow files.
  # We also include reticulate as it can be useful for rixpress internals.
  r_pkgs = c(
    "ggplot2",
    "ggdag",
    "dplyr",
    "arrow",
    "rix",
    "rixpress",
    "quarto"
  ),

  # 2. Julia Configuration
  # We specify the Julia version and the list of packages needed
  # for our manual RBC model simulation.
  jl_conf = list(
    jl_version = "lts",
    jl_pkgs = c(
      "Distributions", # For creating random shocks
      "DataFrames", # For structuring the output
      "Arrow", # For saving the data in a cross-language format
      "Random"
    )
  ),

  # 3. Python Configuration
  # We specify the Python version and the packages needed for the
  # machine learning step.
  py_conf = list(
    py_version = "3.13",
    py_pkgs = c(
      "pandas",
      "scikit-learn",
      "xgboost",
      "pyarrow",
      "ryxpress" # Python port of rixpress
    )
  ),

  # We set the IDE to 'none' for a minimal environment. You could change
  # this to "rstudio" if you prefer to work interactively in RStudio.
  ide = "none",

  # Define the project path and allow overwriting the default.nix file.
  project_path = ".",
  overwrite = TRUE
)

Dacă sunteți pe un sistem în care Nix este disponibil, puteți trece într-un shell temporar cu R și {rix} disponibil pentru a genera necesarul default.nix (care este expresia Nix care, odată construită, oferă mediul):

nix-shell -I \
  nixpkgs=https://github.com/rstats-on-nix/nixpkgs/tarball/2025-10-20 -p \
  R rPackages.rix

apoi pur și simplu porniți R și apoi source("gen-env.R"). Aceasta va genera default.nix. Apoi lăsați R, părăsiți shell-ul temporar (prin tastând exit sau folosind CTRL-D) și construiți mediul cu nix-build. Așteptați să se termine. Apoi putem aborda conducta. Vă arăt mai jos scenariul complet, dar nu veți scrie asta dintr-o singură mișcare. În schimb, ai adăuga o derivație, ai construi conducta, ai încărca artefactul în memorie utilizând rxp_load("artefact_name")uită-te la el, joacă-te cu el și apoi continuă. Dacă ești familiarizat cu {targets} ar trebui să te simți în largul tău.

Iată scenariul complet:

# This script defines and orchestrates the entire reproducible analytical
# pipeline using the {rixpress} package.

library(rixpress)

list(
  # STEP 0: Define RBC Model Parameters as Derivations
  # This makes the parameters an explicit part of the pipeline.
  # Changing a parameter will cause downstream steps to rebuild.
  rxp_jl(alpha, 0.3), # Capital's share of income
  rxp_jl(beta, 1 / 1.01), # Discount factor
  rxp_jl(delta, 0.025), # Depreciation rate
  rxp_jl(rho, 0.95), # Technology shock persistence
  rxp_jl(sigma, 1.0), # Risk aversion (log-utility)
  rxp_jl(sigma_z, 0.01), # Technology shock standard deviation

  # STEP 1: Julia - Simulate a Real Business Cycle (RBC) model.
  # This derivation runs our Julia script to generate the source data.
  rxp_jl(
    name = simulated_rbc_data,
    expr = "simulate_rbc_model(alpha, beta, delta, rho, sigma, sigma_z)",
    user_functions = "functions/functions.jl", # The file containing the function
    encoder = "arrow_write" # The function to use for saving the output
  ),

  # STEP 2.1: Python - Prepare features (lagging data)
  rxp_py(
    name = processed_data,
    expr = "prepare_features(simulated_rbc_data)",
    user_functions = "functions/functions.py",
    # Decode the Arrow file from Julia into a pandas DataFrame
    decoder = "feather.read_feather"
    # Note: No encoder needed here. {rixpress} will use pickle by default
    # to pass the DataFrame between Python steps.
  ),

  # STEP 2.2: Python - Split data into training and testing sets
  rxp_py(
    name = X_train,
    expr = "get_X_train(processed_data)",
    user_functions = "functions/functions.py"
  ),

  rxp_py(
    name = y_train,
    expr = "get_y_train(processed_data)",
    user_functions = "functions/functions.py"
  ),

  rxp_py(
    name = X_test,
    expr = "get_X_test(processed_data)",
    user_functions = "functions/functions.py"
  ),

  rxp_py(
    name = y_test,
    expr = "get_y_test(processed_data)",
    user_functions = "functions/functions.py"
  ),

  # STEP 2.3: Python - Train the model
  rxp_py(
    name = trained_model,
    expr = "train_model(X_train, y_train)",
    user_functions = "functions/functions.py"
  ),

  # STEP 2.4: Python - Make predictions
  rxp_py(
    name = model_predictions,
    expr = "make_predictions(trained_model, X_test)",
    user_functions = "functions/functions.py"
  ),

  # STEP 2.5: Python - Format final results for R
  rxp_py(
    name = predictions,
    expr = "format_results(y_test, model_predictions)",
    user_functions = "functions/functions.py",
    # We need an encoder here to save the final DataFrame as an Arrow file
    # so the R step can read it.
    encoder = "save_arrow"
  ),

  # STEP 3: R - Visualize the predictions from the Python model.
  # This final derivation depends on the output of the Python step.
  rxp_r(
    name = output_plot,
    expr = plot_predictions(predictions), # The function to call from functions.R
    user_functions = "functions/functions.R",
    # Specify how to load the upstream data (from Python) into R.
    decoder = arrow::read_feather
  ),

  # STEP 4: Quarto - Compile the final report.
  rxp_qmd(
    name = final_report,
    additional_files = "_rixpress",
    qmd_file = "readme.qmd"
  )
) |>
  rxp_populate(
    py_imports = c(
      pandas = "import pandas as pd",
      pyarrow = "import pyarrow.feather as feather",
      sklearn = "from sklearn.model_selection import train_test_split",
      xgboost = "import xgboost as xgb"
    ),
    project_path = ".", # The root of our project
    build = TRUE, # Set to TRUE to execute the pipeline immediately
    verbose = 1
  )

(funcțiile de ajutor sunt definite în scripturi separate, în interiorul functions/ dosar pe care nu îl arăt aici)

Magia aici este dublă. Primul, {rixpress} gestionează fără probleme transmiterea datelor între medii de limbă, folosind formate eficiente precum Apache Arrow prin encoder şi decoder funcții. În al doilea rând, deoarece fiecare pas este o derivație Nix, rulează în propriul său mediu izolat. Simularea Julia poate avea propriile sale dependențe, complet separate de pașii Python și R, eliminând „infernul dependenței” pentru totdeauna. De asemenea, artefactele construite de conductă sunt de fapt copii ai mediului. Adică, dacă schimbați mediul (de exemplu, prin adăugarea unui pachet), acest lucru invalidează totul și întreaga conductă este reconstruită. Acest lucru este destul de util, deoarece, uneori, schimbarea mediului ar putea sparge artefactele din aval în moduri subtile, dar cu instrumentele clasice de automatizare a construcției, artefactele și mediul nu sunt legate, astfel încât o reconstrucție nu ar fi declanșată.

Odată construit, puteți explora interactiv artefacte:

# From R
rxp_load("simulated_rbc_data")
rxp_load("output_plot")
# Or from Python (using ryxpress)
from ryxpress import rxp_make, rxp_load
rxp_load("predictions")

Conducta memorează în cache rezultatele, așa că schimbarea unui pas nu face decât să reconstruiască ceea ce este afectat. {rixpress} (şi ryxpress) va face tot posibilul să vă arate să convertiți fără probleme obiectele din R în Python și invers. Dacă încercați să încărcați un obiect construit într-un mediu Python, {rixpress} va folosi {reticulate} (dacă l-ați adăugat la lista de pachete R) pentru a-l converti într-un obiect R echivalent. Dintr-o sesiune Python, dacă ați adăugat rds2py Pachetul Python, același lucru se va întâmpla, dar conversia unui obiect R în obiectul Python echivalent (deoarece Python nu are o implementare nativă a cadrului de date, utilizați biocframe pentru a converti din cadrele de date R în Python bioccadre, care vin cu o metodă de conversie la pandas sau polars cadre de date).

Puteți găsi codul pentru acest exemplu aici.

Dacă sunteți principal un utilizator Python, cred că încă ați putea găsi {rixpress} util. Definirea conductei ca o listă R nu ar trebui să fie o problemă prea mare și puteți explora conducta și artefactele cu portul Python, ryxpress. Acest port Python facilitează construirea conductei și încărcarea și explorarea artefactelor dintr-o sesiune Python.

Un alt avertisment legat de Python este că, în timp ce depozitul de pachete al lui Nix, nixpkgseste vast, ecosistemul Python (PyPI) este celebru eterogen. Nu toate pachetele Python sau versiunile specifice de care ați putea avea nevoie sunt disponibile direct în nixpkgs.

Pentru a rezolva acest lucru, este posibil să se instaleze uvun manager de pachete Python modern și rapid, cu Nix, și let uv se ocupă de pachetele Python și interpretul Python, dar lasă Nix să se ocupe de orice altceva:

rix(date = "2025-10-20",
  r_pkgs = c("rix", "dplyr", "chronicler"),
  system_pkgs = c("uv"),
  project_path = ".",
  overwrite = TRUE)

Această abordare vă oferă ce este mai bun din ambele lumi: folosiți {rix} pentru a defini mediul central, reproductibil. Aceasta include biblioteci de sistem critice (cum ar fi GDAL sau HDF5) și toate dependențele dvs. R și Julia. Această parte a mediului dumneavoastră este reproductibilă bit cu bit. Apoi, în acest mediu gestionat de Nix, utilizați standard uv comenzi (de exemplu, uv pip install pandas) pentru a vă gestiona pachetele Python. uv creează o uv.lock fișier care fixează versiunile și hash-urile exacte ale dependențelor dvs. Python, asigurând un set de pachete Python reproductibil.

În timp ce acest model hibrid schimbă întregul determinism din timpul de construcție al unei abordări pure-Nix pentru pachetele Python, oferă o flexibilitate imensă și rezolvă problema nixpkgs nu oglindește PyPI.

Cred că este cel mai mare obstacol pentru {rix} şi {rixpress} Adopția pentru oamenii de știință de date Python este dragostea lor pentru notebook-urile Jupyter.

Apropo, este posibil să utilizați un IDE alături de Nix și {rix} şi {rixpress}. Cred că voi face un videoclip pentru asta, totuși, dar pentru cei dintre voi care preferă să citească, citiți asta.

Dominic Botezariu
Dominic Botezariuhttps://www.noobz.ro/
Creator de site și redactor-șef.

Cele mai noi știri

Pe același subiect

LĂSAȚI UN MESAJ

Vă rugăm să introduceți comentariul dvs.!
Introduceți aici numele dvs.