Fazendo um join aproximado

Prezadxs, boa noite!

Preciso de uma ajuda em uma atividade que estou desenvolvendo, mais especificamente ajuda em um join/loop aproximado.

Vou explicar o contexto e qual o objetivo:

Tenho uma base de talhões florestais, nos quais preciso alocar um volume estimado. Para fazer isso uso uma informação chave (estrato) + idade de colheita desse talhão pra gerar um index único, com esse index trago a informação de volume estimado de uma tabela de produção.

Base de talhões


talhao idade_de_colheita estrato index
ACD001 14 EST01 EST01-14
ACD002 15 EST01 EST01-15
ACD003 16 EST01 EST01-16
ACD004 16 EST01 EST01-16
AFF001 14 EST02 EST02-14
AFF002 15 EST02 EST02-15
AFF003 18 EST02 EST02-18
AFF004 18 EST02 EST02-18
AFF005 19 EST02 EST02-19
AYY001 13 EST03 EST03-13
AYY002 13 EST03 EST03-13
AYY003 9 EST03 EST03-9

A tabela de produção seria minha tabela dimensão, onde tenho vários estratos para várias idades, como pode ser observado em um exemplo hipotético na tabela a seguir:

Tabela de produção

estrato idade index volume_t
EST01 15 EST01-15 388.000
EST01 16 EST01-16 400.000
EST01 17 EST01-17 424.000
EST01 18 EST01-18 462.000
EST02 16 EST02-16 1.203
EST02 17 EST02-17 1.239
EST02 18 EST02-18 1.313
EST02 19 EST02-19 1.432
EST03 7 EST03-7 921.000
EST03 8 EST03-8 976.000
EST03 9 EST03-9 1064.000
EST03 10 EST03-10 1192.000
EST03 11 EST03-11 1371.000

O problema que estou tendo se encontra nos talhões que serão colhidos com uma idade que não foi previamente calculada na tabela de produção (TP) , como o talhão ACD001, que será colhido com 14 anos porém a TP só possui informações a partir dos 15 anos, ou o talhão AYY001, que será colhido com 13 anos mas a TP só possui informações desse estrato até 11 anos.

Para esses casos eu preciso pegar a informação mais próxima, no caso do talhão ACD001, pego a informação de 15 anos. No caso do talhão AYY001, pego a informação de 11 anos, sempre a info mais próxima que existe dentro das estimativas.

Estruturei um script simples com dplyr::left_join quando comecei e ai me dei de cara com esses problemas. Tentei alguns testes com o pacote fuzzyjoin, porém não consegui encaixar nenhuma função que resolvesse meu problema. Outra tentativa que realizei foi fazer o cálculo das distâncias de uma string para outra usando a coluna de “index” na função RecordLinkage::levenshteinSim , como exemplificado em um trabalho do Julio com a Bruna, disponível no seguinte link: Análise de dados musicais no R (brunaw.com), porém o resultado também não era o que eu precisava.

Por gentileza, alguém tem alguma ideia de como resolver esse problema?

Na prática minha chave de procura é o ESTRATO + IDADE, uso o index mais por facilidade quando está tudo estruturado no excel. O que estou tentando agora é fazer um loop onde encontro a idade mais próxima para cada um dos estratos que não deram match, com isso busco o respectivo estrato dessa idade e refaço o join com uma base auxiliar, entretanto, estou sofrendo pra encaixar o código, então achei que valia a pena compartilhar a situação aqui.

Agradeço muitíssimo qualquer ajuda ou ideia!

Bases de exemplo para facilitar qualquer exploração:

talhoes <- tibble::tribble(
    ~talhao, ~idade_de_colheita, ~estrato,     ~index,
    "ACD001",                14L,  "EST01", "EST01-14",
    "ACD002",                15L,  "EST01", "EST01-15",
    "ACD003",                16L,  "EST01", "EST01-16",
    "ACD004",                16L,  "EST01", "EST01-16",
    "AFF001",                14L,  "EST02", "EST02-14",
    "AFF002",                15L,  "EST02", "EST02-15",
    "AFF003",                18L,  "EST02", "EST02-18",
    "AFF004",                18L,  "EST02", "EST02-18",
    "AFF005",                19L,  "EST02", "EST02-19",
    "AYY001",                13L,  "EST03", "EST03-13",
    "AYY002",                13L,  "EST03", "EST03-13",
    "AYY003",                 9L,  "EST03",  "EST03-9"
)

tp <- tibble::tribble(
          ~estrato, ~idade,     ~index, ~volume_t,
           "EST01",    15L, "EST01-15",        388,
           "EST01",    16L, "EST01-16",        400,
           "EST01",    17L, "EST01-17",        424,
           "EST01",    18L, "EST01-18",        462,
           "EST02",    16L, "EST02-16",      1.203,
           "EST02",    17L, "EST02-17",      1.239,
           "EST02",    18L, "EST02-18",      1.313,
           "EST02",    19L, "EST02-19",      1.432,
           "EST03",     7L,  "EST03-7",        921,
           "EST03",     8L,  "EST03-8",        976,
           "EST03",     9L,  "EST03-9",       1064,
           "EST03",    10L, "EST03-10",       1192,
           "EST03",    11L, "EST03-11",       1371
          )

Created on 2022-03-14 by the reprex package (v2.0.1)

Maykon,

Se eu entendi corretamente os requisitos, acho que você está complicando o problema mais do que o necessário. Pacotes como o fuzzyjoin e o RecordLinkage te ajudam com métricas de distância entre os “textos” dos valores e não é isso que você quer… Dependendo da métrica, o fuzzyjoin, por exemplo, vai classificar “19” como sendo mais próximo de “11” do que de “20” (porque você só precisa mudar 1 caractere para ir de “19” para “11” e 2 caracteres para ir de “19” para “20”). Enfim, espero que não esteja falando besteira, mas você tem uma regra muito clara e que não envolve índices ou distâncias entre strings: para cada talhão de talhoes, você quer pegar de tp a idade mais próxima dentre as disponíveis para o estrato ao qual aquele talhão pertence.

Minha solução foi fazer um join só pelo estrato (removi a coluna com o índice) e calcular, para cada linha, a diferença entre talhoes$idade_de_colheita e tp$idade. Eu então agrupei por talhao (a sua unidade amostral) e mantive, para cada grupo, apenas a linha cuja diferença de idades era a menor. Quando havia correspondência perfeita entre as idades de talhoes e tp, a diferença era 0 (sempre a menor); caso contrário, ficaríamos com a idade mais próxima disponível para aquele estrato.

Note que eu usei o valor absoluto da diferença entre as idades. Neste caso, eu não dou preferência para um lado nem para o outro, mas pode ser que você não queira isso!

# Tabela exemplo de talhões
talhoes <- tibble::tribble(
   ~talhao, ~idade_de_colheita, ~estrato,
  "ACD001",                14L,  "EST01",
  "ACD002",                15L,  "EST01",
  "ACD003",                16L,  "EST01",
  "ACD004",                16L,  "EST01",
  "AFF001",                14L,  "EST02",
  "AFF002",                15L,  "EST02",
  "AFF003",                18L,  "EST02",
  "AFF004",                18L,  "EST02",
  "AFF005",                19L,  "EST02",
  "AYY001",                13L,  "EST03",
  "AYY002",                13L,  "EST03",
  "AYY003",                 9L,  "EST03"
)

# Tabela exemplo de produção
tp <- tibble::tribble(
  ~estrato, ~idade, ~volume_t,
   "EST01",    15L,        388,
   "EST01",    16L,        400,
   "EST01",    17L,        424,
   "EST01",    18L,        462,
   "EST02",    16L,      1.203,
   "EST02",    17L,      1.239,
   "EST02",    18L,      1.313,
   "EST02",    19L,      1.432,
   "EST03",     7L,        921,
   "EST03",     8L,        976,
   "EST03",     9L,       1064,
   "EST03",    10L,       1192,
   "EST03",    11L,       1371
)

# Juntar por estrato e calcular a diferença entre as idades
join_talhoes_tp <- talhoes |>
  dplyr::left_join(tp, "estrato") |>
  dplyr::mutate(dif_idade = abs(idade_de_colheita - idade))

# Demonstração de como fica a tabela para ACD002 (há uma
# linha com dif_idade == 0, então é essa que vai ficar)
join_talhoes_tp |>
  dplyr::filter(talhao == "ACD002")
#> # A tibble: 4 × 6
#>   talhao idade_de_colheita estrato idade volume_t dif_idade
#>   <chr>              <int> <chr>   <int>    <dbl>     <int>
#> 1 ACD002                15 EST01      15      388         0
#> 2 ACD002                15 EST01      16      400         1
#> 3 ACD002                15 EST01      17      424         2
#> 4 ACD002                15 EST01      18      462         3

# Demonstração de como fica a tabela para ACD001 (não há
# nenhuma dif_idade == 0, então a linha com dif_idade == 1
# é que vai ficar)
join_talhoes_tp |>
  dplyr::filter(talhao == "ACD001")
#> # A tibble: 4 × 6
#>   talhao idade_de_colheita estrato idade volume_t dif_idade
#>   <chr>              <int> <chr>   <int>    <dbl>     <int>
#> 1 ACD001                14 EST01      15      388         1
#> 2 ACD001                14 EST01      16      400         2
#> 3 ACD001                14 EST01      17      424         3
#> 4 ACD001                14 EST01      18      462         4


# Demonstração de como fica a tabela para AYY001 (não há
# nenhuma dif_idade == 0, então a linha com dif_idade == 2
# e que vai ficar)
join_talhoes_tp |>
  dplyr::filter(talhao == "AYY001")
#> # A tibble: 5 × 6
#>   talhao idade_de_colheita estrato idade volume_t dif_idade
#>   <chr>              <int> <chr>   <int>    <dbl>     <int>
#> 1 AYY001                13 EST03       7      921         6
#> 2 AYY001                13 EST03       8      976         5
#> 3 AYY001                13 EST03       9     1064         4
#> 4 AYY001                13 EST03      10     1192         3
#> 5 AYY001                13 EST03      11     1371         2

# Agrupar por talhão e manter apenas a linha com a menor
# diferença de idade (eliminando empates, caso ocorram)
join_talhoes_tp |>
  dplyr::group_by(talhao) |>
  dplyr::slice_min(dif_idade, with_ties = FALSE) |>
  dplyr::ungroup()
#> # A tibble: 12 × 6
#>    talhao idade_de_colheita estrato idade volume_t dif_idade
#>    <chr>              <int> <chr>   <int>    <dbl>     <int>
#>  1 ACD001                14 EST01      15   388            1
#>  2 ACD002                15 EST01      15   388            0
#>  3 ACD003                16 EST01      16   400            0
#>  4 ACD004                16 EST01      16   400            0
#>  5 AFF001                14 EST02      16     1.20         2
#>  6 AFF002                15 EST02      16     1.20         1
#>  7 AFF003                18 EST02      18     1.31         0
#>  8 AFF004                18 EST02      18     1.31         0
#>  9 AFF005                19 EST02      19     1.43         0
#> 10 AYY001                13 EST03      11  1371            2
#> 11 AYY002                13 EST03      11  1371            2
#> 12 AYY003                 9 EST03       9  1064            0

Created on 2022-03-15 by the reprex package (v2.0.1)

P.S.: Ótima pergunta! E obrigado por mandar dados reprodutíveis de exemplo.

3 curtidas

Caio, simplesmente sensacional!

Minha resolução estava indo por um caminho bem mais cru e difícil de compreender, a sua ficou perfeita para o que preciso e com uma quantidade mínima de fórmulas.

Era exatamente o que eu precisava.

Muitíssimo obrigado pela ajuda!

Fico feliz que a pergunta tenha gerado um engajamento para uma boa resposta, e espero que sirva pra outros que enfrentarem problemas parecidos.

Abraço!

1 curtida