splor-mg / atividades

Gestão de tarefas
0 stars 0 forks source link

Estudar a viabilidade de executar códigos R em Python #147

Open gabrielbdornas opened 1 month ago

gabrielbdornas commented 1 month ago

See #148.

labanca commented 1 month ago

Existe um pacote chamado rpy2 que se propõe a executar funções de pacotes R dentro de um programa python.

rpy2 is an interface to R running embedded in a Python process

No windows, somente consegui instalá-lo em um ambiente virtual no Python 3.10 e a versão do rpy2 sendo 3.5.12 (em versões mais recentes ocorreu erro de instalação):

py -3.10 -m venv venv
source venv/Scripts/activate
pip install rpy2==3.5.12

e o seguinte snipet foi executado utilizando o rpy2, definindo uma função R e recebendo o resultado dela no Python:

import rpy2.robjects as ro
from rpy2.robjects.packages import importr

# Load the R base package
base = importr('base')

# Load a specific R package (e.g., ggplot2)
ggplot2 = importr('ggplot2')

# Define an R function and use it
ro.r('''
my_function <- function(x) {
    return(x + 1)
}
''')

# Call the R function from Python
r_my_function = ro.globalenv['my_function']
result = r_my_function(5)
print(result[0])  # Output will be 6
output: 6.0

Também foi possível acessar pelo Python um dataframe criado no R (vetores e dataframes são objetos geralmente retornados pelo pacote relatórios):

import rpy2.robjects as ro
import pandas as pd
from rpy2.robjects import pandas2ri

# Activate the pandas2ri conversion
pandas2ri.activate()

# R code to create a data.frame
ro.r('''
    my_dataframe <- data.frame(
        column1 = c(1, 2, 3, 4),
        column2 = c('A', 'B', 'C', 'D'),
        stringsAsFactors = FALSE
    )
''')

# Access the R data.frame
r_dataframe = ro.globalenv['my_dataframe']

# Display the Python DataFrame
print(r_dataframe)
  column1 column2
1       1       A
2       2       B
3       3       C
4       4       D

Por fim, foi possível criar um dataframe no Python, passa-lo para o R e recuperá-lo. Em seguida é adicionada uma coluna a esse dataframe dentro do R e repassado esse dataframe modificado para o Python (convertendo-o também em um dataframe do pandas):

import pandas as pd
import rpy2.robjects as ro
from rpy2.robjects import pandas2ri

# Activate the conversion between pandas and R data frames
pandas2ri.activate()

# Step 1: Create a pandas DataFrame in Python
df_python = pd.DataFrame({
    'column1': [1, 2, 3, 4],
    'column2': ['A', 'B', 'C', 'D']
})

# Convert the pandas DataFrame to an R data.frame
r_dataframe = pandas2ri.py2rpy(df_python)

#step 2: Pass the dataframe to rpy2
ro.globalenv['r_dataframe'] = r_dataframe

# Step 3: Define and apply an R function
# (for example, here we're just returning the summary of the dataframe)
ro.r('''
    # R code that manipulates the dataframe
    result <- summary(r_dataframe)
''')

# Retrieve the result from R (could be a modified dataframe, list, or other object)
r_summary = ro.r('result')

# Prints the summary received from rpy2
print("-- Returned R dataframe summary --")
print(r_summary)

# Recover to Python the dataframe sent to R (note the type in python is <class 'rpy2.robjects.vectors.DataFrame'>)
r_dataframe = ro.r('r_dataframe')
print("\n-- R dataframe --")
print(r_dataframe)

# Adds a new line to the dataframe inside R
ro.r('''
    new_row <- data.frame(column1 = 5, column2 = "E")
    df_r <- rbind(r_dataframe, new_row)
''')

# receives dataframe with a new line and convert it back to pandas
pandas_dataframe = pandas2ri.rpy2py_dataframe(ro.globalenv['df_r'])
print("\n-- Pandas dataframe --")
print(pandas_dataframe)
-- Returned R dataframe summary --
['Min.   :1.00  ' '1st Qu.:1.75  ' 'Median :2.50  ' 'Mean   :2.50  '
 '3rd Qu.:3.25  ' 'Max.   :4.00  ' 'Length:4          '
 'Class :character  ' 'Mode  :character  ' None None None]

-- R dataframe --
  column1 column2
0       1       A
1       2       B
2       3       C
3       4       D

-- Pandas dataframe --
    column1 column2
0       1.0       A
1       2.0       B
2       3.0       C
3       4.0       D
11      5.0       E
gabrielbdornas commented 1 month ago

@labanca, bastante interessante. Acredito que utilizar a versão 3.10 Python não seria um problema. Dou exemplo rápido da Frictionless, que exige a versão Python 3.8 ou superior.

Você chegou a montar/pensar em algum reprex para a chamada do pacote relatórios? Por falar nisso, ele fica em nossa organização (não achei nada em uma pesquisa rápida)? Poderia incluir o link aqui para mim?

labanca commented 1 month ago

Você chegou a montar/pensar em algum reprex para a chamada do pacote relatórios?

Não sei se entendi a pergunta, seria montar um reprex com esses exemplos? Acho que se você estiver perguntando sobre criar um exemplo do pacote relatórios sendo usado pelo rpy2 , é o próximo passo.

Por falar nisso, ele fica em nossa organização (não achei nada em uma pesquisa rápida)? Poderia incluir o link aqui para mim?

Os pacotes ficam no bitbucket na conta da DCGF: https://bitbucket.org/dcgf/relatorios/src/master/

gabrielbdornas commented 1 month ago

@labanca, achei este issue no repo dpm. Será que Francisco já estava pensando algo neste sentido?

labanca commented 1 month ago

@labanca, achei este issue no repo dpm. Será que Francisco já estava pensando algo neste sentido?

Discutimos isso na época, mas depois não seguimos com a ideia. Não recordo os motivos ao certo, mas creio que havia demandas mais urgentes e essa abordagem era complexa.

labanca commented 1 month ago

Outro update sobre o estudo.

É possível atribuir a um objeto Python um pacote, função ou código R e utilizá-lo no ambiente Python.

Com isso eu consegui instanciar o pacote relatórios no Python e aplicar uma de suas funções na base acoes_planejamento e receber o resultado dessa função do pacote relatórios no Python:

import rpy2.robjects as ro
from rpy2.robjects import pandas2ri
from rpy2.robjects.packages import importr
import unidecode

def clean_column_names(column_name):
    """
    Reproduce the LOA project data cleansing on the dataframe to be able to use relatorios functions
    """

    # Replace white spaces with dots
    column_name = column_name.replace(" ", ".")

    # Convert to unaccented equivalents
    column_name = unidecode.unidecode(column_name)

    # Convert to lower case
    column_name = column_name.lower()

    return column_name

# Activate the automatic conversion between R data frames and pandas DataFrames
pandas2ri.activate()
base = importr('base')
utils = importr('utils')
readr = importr('readr')

# You can convert a R package to a python object and use it in the python env
relatorios = importr('relatorios')

# Read the pipe-separated text file
acoes_planejamento = readr.read_delim("data/acoes_planejamento.txt", delim="|")

# Convert to pandas DataFrame
acoes_planejamento_df = pandas2ri.rpy2py(acoes_planejamento)

cleaned_column_names = [clean_column_names(name) for name in acoes_planejamento_df.columns]

acoes_planejamento_df.columns = cleaned_column_names

# LOA project names conventions (clunky)
nomes_esperados = [
    "codigo.do.programa", "nome.do.programa", "codigo.da.unidade.orcamentaria.responsavel.pela.acao",
    "codigo.da.funcao", "funcao", "codigo.da.subfuncao", "subfuncao",
    "codigo.do.tipo.de.acao", "tipo.de.acao", "codigo.da.acao", "titulo.da.acao",
    "codigo.do.identificador.de.acao.governamental..iag.", "exclusao.logica.da.acao",
    "finalidade.da.acao", "codigo.do.produto", "produto", "unidade.de.medida.do.produto"
]

novos_nomes = [
    "cod_prog", "nome_prog", "UO_COD",
    "cod_funcao", "nome_funcao", "cod_subfuncao", "nome_subfuncao",
    "cod_tipo_acao", "tipo_acao", "cod_acao", "titulo_acao",
    "cod_iag", "exc_acao", "final_acao", "prod_acao",
    "Produto", "unid_med_prod"
]

# Create a mapping from expected names to new names
name_mapping = dict(zip(nomes_esperados, novos_nomes))

# Rename the columns using the mapping, only if they exist in the DataFrame
acoes_planejamento_df.rename(columns=name_mapping, inplace=True)

# relatorios package need a column named ANO
acoes_planejamento_df['ANO'] = 2024

# Convert back to R DataFrame to apply the relatios function
acoes_planejamento_r = pandas2ri.py2rpy(acoes_planejamento_df)

# Assign the dataframe to R's global environment
ro.globalenv['acoes_planejamento'] = acoes_planejamento_r

# Call the function from the R package relatorios
result = relatorios.is_outros_poderes(ro.globalenv['acoes_planejamento'])

# Print the result
print(result)

O resultado é um vetor booleano, o que é o retorno das funções do pacote relatorios utilizadas para filtrar as bases:

[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
  [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
  [25] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
  [37] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
  [49] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
  [61] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
  [73] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

Há uma série de procedimentos que foram feitos para o exemplo que não necessariamente precisariam ser feitos. Como a base de dados ser lida utilizando o pacote readr do R e os nomes das colunas serem transformados para ficarem equivalentes aos nomes da LOA e PPAG, onde a função is_outros_poderes é geralmente utilizada.

gabrielbdornas commented 1 month ago

@labanca, estou entendendo que a resposta para este Issue é SIM, conseguimos executar códigos R em Python. O que acha de fecharmos este Issue e já abrirmos um no dpm com a chamada para ação de criar esta funcionalidade lá?

Como já estão definidas várias atividades para o mês de novembro, isso poderá entrar como prioridade para dezembro.

O que acha?

labanca commented 1 month ago

@gabrielbdornas estou bem empolgado com os resultados! Porém, acho que ainda temos de estudar um pouco mais. Tudo indica que é possível, mas um teste prático em alguma aplicação nossa seria mais seguro.

E no caso, quais seriam as funcionalidades em R que viriam para o DPM? Hoje o DPM que é cuido somente possui código Python.

gabrielbdornas commented 1 month ago

@gabrielbdornas estou bem empolgado com os resultados! Porém, acho que ainda temos de estudar um pouco mais. Tudo indica que é possível, mas um teste prático em alguma aplicação nossa seria mais seguro.

E no caso, quais seriam as funcionalidades em R que viriam para o DPM? Hoje o DPM que é cuido somente possui código Python.

@labanca, concordo em fazer o teste em alguma aplicação. Alguma sugestão de qual?

Quanto às funcionalidades R que viriam para o dpm, estou entendendo que você não está se referindo somente ao pacote relatórios, mas sim tudo relacionado ao #148, correto? Se sim, acho que temos que pensar nesta conversa aqui para definir o que vai fazer sentido migrar para o dpm.