Извлечение значения из набора столбцов на основе минимального значения в другом наборе столбцов в R

В моем фрейме данных у меня 15 столбцов:

  1. Идентификатор субъекта
  2. Набор из 7 столбцов с возрастом субъекта в определенные моменты времени (возраст1, возраст2 и т. д.).
  3. Набор из 7 столбцов с оценками субъектов в определенные моменты времени (соответствующими возрасту, указанному выше; балл 1, балл 2 и т. д.).

У большинства участников есть только возраст1 и балл1 (т. е. они получили балл только в один момент времени), но у некоторых их будет больше, если они прошли тестирование в несколько моментов времени.

Я хотел бы создать два новых столбца:

  1. minScore: минимальное значение за пределами столбцов Оценка1:оценка7, игнорируя NA.
  2. ScoreAge: возраст субъекта, соответствующий моменту времени, когда он получил минимальный балл. Например, если самый низкий балл субъекта равен значению баллов3, я хочу, чтобы в этом столбце было значение возраста3 и т. д. Это может быть значение NA, если возраст субъекта для данного момента времени отсутствует.
data <- structure(list(subject_id = c("191-11173897", "191-11561329", 
"191-11700002", "191-11857141", "191-11933910"), age1 = c(39, 
7, NA, NA, 16), age2 = c(36, NA, NA, NA, 37), age3 = c(9, NA, 
NA, NA, NA), age4 = c(NA_real_, NA_real_, NA_real_, NA_real_, 
NA_real_), age5 = c(NA_real_, NA_real_, NA_real_, NA_real_, NA_real_
), age6 = c(NA_real_, NA_real_, NA_real_, NA_real_, NA_real_), 
    age7 = c(NA_real_, NA_real_, NA_real_, NA_real_, NA_real_
    ), score1 = c(10.6, 12.1, 9.8, NA, 10.6), score2 = c(9.8, 
    NA, NA, NA, 11), score3 = c(11.3, NA, NA, NA, NA), score4 = c(NA_real_, 
    NA_real_, NA_real_, NA_real_, NA_real_), score5 = c(NA_real_, 
    NA_real_, NA_real_, NA_real_, NA_real_), score6 = c(NA_real_, 
    NA_real_, NA_real_, NA_real_, NA_real_), score7 = c(NA_real_, 
    NA_real_, NA_real_, NA_real_, NA_real_)), row.names = c(NA, 
-5L), class = c("tbl_df", "tbl", "data.frame"))
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
0
125
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Вы можете использовать несколько функций, чтобы сделать это эффективно.

Для столбца minScore я сначала проверяю, все ли значения NA находятся в столбцах, начинающихся со слова «оценка», затем помещаю NA в качестве вывода; в противном случае получите минимум по столбцам с именем «оценка», также удалив NA. Аналогичные шаги предприняты для столбца scoreAge.

data %>%
  rowwise() %>%
  mutate(
    minScore = if (all(is.na(c_across(starts_with("score"))))) {
      NA
    } else {
      min(c_across(starts_with("score")), na.rm = TRUE)
    },
    scoreAge = {
      score_cols <- c_across(starts_with("score"))
      age_cols <- c_across(starts_with("age"))
      if (all(is.na(score_cols))) {
        NA
      } else {
        min_score_index <- which.min(score_cols)
        age_cols[min_score_index]
      }
    }
  ) %>%
  ungroup()

Вы можете использовать rowwise, c_across и функцию аккуратного выбора starts_with. Учебник по rowwise, который также охватывает c_across. Документация по start_with.

library(dplyr)

# function to calculate minScore
calculate_min_score <- function(scores) {
  #if all scores are NA, return NA
  if (all(is.na(scores))) {
    return(NA_real_)
  } else { # otherwise return the min score
    return(min(scores, na.rm = TRUE))
  }
}

# function to calculate scoreAge
calculate_score_age <- function(scores, ages) {
  #If all scores are NA, return NA
  if (all(is.na(scores))) {
    return(NA_real_)
  } else { #Otherwise, find which score column number contains the min score
    min_score_idx <- which.min(scores)
    return(ages[min_score_idx]) #return the age column value associated with that score
  }
  
  
  
}


data %>%
  rowwise() %>%
  mutate(
    minScore = calculate_min_score(c_across(starts_with("score"))),
    scoreAge = calculate_score_age(
    scores = c_across(starts_with("score")), 
    ages = c_across(starts_with("age")))

  ) |> 
  ungroup() 

# A tibble: 5 × 17
  subject_id    age1  age2  age3  age4  age5  age6  age7 score1 score2 score3 score4 score5 score6 score7 minScore scoreAge
  <chr>        <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>    <dbl>    <dbl>
1 191-11173897    39    36     9    NA    NA    NA    NA   10.6    9.8   11.3     NA     NA     NA     NA      9.8       36
2 191-11561329     7    NA    NA    NA    NA    NA    NA   12.1   NA     NA       NA     NA     NA     NA     12.1        7
3 191-11700002    NA    NA    NA    NA    NA    NA    NA    9.8   NA     NA       NA     NA     NA     NA      9.8       NA
4 191-11857141    NA    NA    NA    NA    NA    NA    NA   NA     NA     NA       NA     NA     NA     NA     NA         NA
5 191-11933910    16    37    NA    NA    NA    NA    NA   10.6   11     NA       NA     NA     NA     NA     10.6       16

Вы можете изменить форму data, а затем применить filter, чтобы получить строку с наименьшим score для каждого subject_id.

library(dplyr)
library(tidyr)

data |>
  pivot_longer(-subject_id,
               names_to = c('measure', 'obs'),
               names_pattern = '([A-Za-z]+)(\\d+)',
               values_to = 'score') |>
  pivot_wider(names_from = measure, values_from = score) |> 
  filter(score == min(score, na.rm = TRUE), .by = subject_id)

#> # A tibble: 4 × 4
#>   subject_id   obs     age score
#>   <chr>        <chr> <dbl> <dbl>
#> 1 191-11173897 2        36   9.8
#> 2 191-11561329 1         7  12.1
#> 3 191-11700002 1        NA   9.8
#> 4 191-11933910 1        16  10.6

Created on 2024-05-20 with reprex v2.1.0

Я использовал pivot_longer(), чтобы получить фрейм данных с одной строкой для каждого теста, содержащей возраст и балл. Рассчитывается минимальный балл для каждого subject_id и фильтруется, где score == minScore. (Я добавил distinct(), чтобы избежать ничьей при наименьшем количестве баллов.)

Затем соединитесь с исходными данными.

data %>% 
  pivot_longer(!matches("subject_id"), names_pattern = "(\\w+)(\\d+)", names_to = c(".value", "testid")) %>%
  mutate(minScore = min(score, na.rm = TRUE), .by = subject_id) %>%
  filter(minScore == score) %>%
  distinct(subject_id, score, .keep_all = TRUE) %>%
  select(subject_id, minScore, scoreAge=age) %>%
  right_join(data, by = "subject_id")
Ответ принят как подходящий

С rowwise и sort/order, чтобы избежать предупреждений, и Inf в случае всех NAs

library(dplyr)

data %>% 
  rowwise() %>% 
  mutate(minScore = sort(c_across(score1:score7))[1], 
         scoreAge = c_across(age1:age7)[order(c_across(score1:score7))[1]]) %>% 
  ungroup()

выход

# A tibble: 5 × 17
  subject_id    age1  age2  age3  age4  age5  age6  age7 score1 score2 score3
  <chr>        <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl>  <dbl>
1 191-11173897    39    36     9    NA    NA    NA    NA   10.6    9.8   11.3
2 191-11561329     7    NA    NA    NA    NA    NA    NA   12.1   NA     NA  
3 191-11700002    NA    NA    NA    NA    NA    NA    NA    9.8   NA     NA  
4 191-11857141    NA    NA    NA    NA    NA    NA    NA   NA     NA     NA  
5 191-11933910    16    37    NA    NA    NA    NA    NA   10.6   11     NA  
  score4 score5 score6 score7 minScore scoreAge
   <dbl>  <dbl>  <dbl>  <dbl>    <dbl>    <dbl>
1     NA     NA     NA     NA      9.8       36
2     NA     NA     NA     NA     12.1        7
3     NA     NA     NA     NA      9.8       NA
4     NA     NA     NA     NA     NA         NA
5     NA     NA     NA     NA     10.6       16

Использование apply() один раз:

data[c("minScore", "scoreAge")] = 
  apply(data, 1L, \(x) { n = names(x)
  y = x[grepl("score", n)]; z = x[grepl("age", n)]
  i = if (all(is.na(y))) NA else which(y == min(y, na.rm = TRUE)) 
  l1 = if (is.na(i)) NA else y[i]; l2 = if (is.na(i)) NA else z[i]
  cbind(l1,l2) }) |> t() 

> data[c("minScore", "scoreAge")] 
  minScore scoreAge
1      9.8       36
2     12.1        7
3      9.8     <NA>
4     <NA>     <NA>
5     10.6       16

или (потенциально быстрее?)

f = \(d) {
  stopifnot(is.data.frame(d)); nd = names(d)
  a = as.matrix(d[grepl("score",  nd)]); b = as.matrix(d[grepl("age", nd)])
  l1 = matrix(a[order(row(a), a)], ncol = ncol(a), byrow = TRUE)[, 1L]
  l2 = mapply(\(i, j) b[i, j], seq(nrow(a)), Rfast::rowMins(a)) |> unname()
  cbind("minScore" = l1, "scoreAge" = l2)
}

> f(data)
     minScore scoreAge
[1,]      9.8       36
[2,]     12.1        7
[3,]      9.8       NA
[4,]       NA       NA
[5,]     10.6       16

Другие вопросы по теме