Scijens
Scijens

Reputation: 561

New variable that indicates the first occurrence of a specific value

I want to create a new variable that indicates the first specific observation of a value for a variable.

In the following example dataset I want to have a new variable "firstna" that is "1" for the first observation of "NA" for this player.

game_data <- data.frame(player = c(1,1,1,1,2,2,2,2), level = c(1,2,3,4,1,2,3,4), points = c(20,NA,NA,NA,20,40,NA,NA))

game_data
  player level points
1      1     1     20
2      1     2     NA
3      1     3     NA
4      1     4     NA
5      2     1     20
6      2     2     40
7      2     3     NA
8      2     4     NA

The resulting dataframe should look like this:

game_data_new <- data.frame(player = c(1,1,1,1,2,2,2,2), level = c(1,2,3,4,1,2,3,4), points = c(20,NA,NA,NA,20,40,NA,NA), firstna = c(0,1,0,0,0,0,1,0))

game_data_new
  player level points firstna
1      1     1     20       0
2      1     2     NA       1
3      1     3     NA       0
4      1     4     NA       0
5      2     1     20       0
6      2     2     40       0
7      2     3     NA       1
8      2     4     NA       0

To be honest i don't know how to do this. It would be perfect if there is a dplyr option to do so.

Upvotes: 5

Views: 713

Answers (8)

moodymudskipper
moodymudskipper

Reputation: 47340

An option using diff

transform(game_data, firstna = ave(is.na(points), player, FUN = function(x) c(0,diff(x))))
#   player level points firstna
# 1      1     1     20       0
# 2      1     2     NA       1
# 3      1     3     NA       0
# 4      1     4     NA       0
# 5      2     1     20       0
# 6      2     2     40       0
# 7      2     3     NA       1
# 8      2     4     NA       0

And its dplyr equivalent:

library(dplyr)
game_data %>% group_by(player) %>% mutate(firstna = c(0,diff(is.na(points))))
# # A tibble: 8 x 4
# # Groups:   player [2]
#   player level points firstna
#    <dbl> <dbl>  <dbl>   <dbl>
# 1      1     1     20       0
# 2      1     2     NA       1
# 3      1     3     NA       0
# 4      1     4     NA       0
# 5      2     1     20       0
# 6      2     2     40       0
# 7      2     3     NA       1
# 8      2     4     NA       0

Upvotes: 0

iod
iod

Reputation: 7592

game_data %>% 
  group_by(player) %>% 
  mutate(firstna=as.numeric(is.na(points) & !duplicated(points)))

Group by player, then create a boolean vector for cases that are both NA and not duplicates for previous rows.

# A tibble: 8 x 4
# Groups:   player [2]
  player level points firstna
   <dbl> <dbl>  <dbl>   <dbl>
1      1     1     20       0
2      1     2     NA       1
3      1     3     NA       0
4      1     4     NA       0
5      2     1     20       0
6      2     2     40       0
7      2     3     NA       1
8      2     4     NA       0

If you want the 1s on the last non-NA line before an NA, replace the mutate line with this:

mutate(lastnonNA=as.numeric(!is.na(points) & is.na(lead(points))))

First row of a block of NAs that runs all the way to the end of the player's group:

game_data %>% 
  group_by(player) %>% 
  mutate(firstna=as.numeric(is.na(points) & !duplicated(cbind(points,cumsum(!is.na(points))))))

Upvotes: 1

hrbrmstr
hrbrmstr

Reputation: 78832

library(tidyverse)
library(data.table)

data.frame(
  player = c(1,1,1,1,2,2,2,2),
  level = c(1,2,3,4,1,2,3,4),
  points = c(20,NA,NA,NA,20,40,NA,NA)
) -> game_data

game_data_base1 <- game_data
game_data_dt <- data.table(game_data)

microbenchmark::microbenchmark(

  better_base = game_data$first_na <- ave(
    game_data$points,
    game_data$player,
    FUN=function(x) seq_along(x)==match(NA,x,nomatch=0)
  ),

  brute_base = do.call(
    rbind.data.frame,
    lapply(
      split(game_data, game_data$player),
      function(x) {
        x$firstna <- 0
        na_loc <- which(is.na(x$points))
        if (length(na_loc) > 0) x$firstna[na_loc[1]] <- 1
        x
      }
    )
  ),

  tidy = game_data %>%
    group_by(player) %>%
    mutate(firstna=as.numeric(is.na(points) & !duplicated(points))) %>%
    ungroup(),

  dt = game_data_dt[, firstna:=as.integer(is.na(points) & !is.na(shift(points))), player]

)
## Unit: microseconds
##         expr     min       lq      mean    median        uq        max neval
##  better_base 125.188  156.861  362.9829  191.6385  355.6675   3095.958   100
##   brute_base 366.642  450.002 2782.6621  658.0380 1072.6475 174373.974   100
##         tidy 998.924 1119.022 2528.3687 1509.0705 2516.9350  42406.778   100
##           dt 330.428  421.211 1031.9978  535.8415 1042.1240   9671.991   100

Upvotes: 2

Ronak Shah
Ronak Shah

Reputation: 389175

Another ave option to find out first NA by group (player).

game_data$firstna <- ave(game_data$points, game_data$player, 
                         FUN = function(x) cumsum(is.na(x)) == 1)

game_data
#  player level points firstna
#1      1     1     20       0
#2      1     2     NA       1
#3      1     3     NA       0
#4      1     4     NA       0
#5      2     1     20       0
#6      2     2     40       0
#7      2     3     NA       1
#8      2     4     NA       0

Upvotes: 5

Andre Elrico
Andre Elrico

Reputation: 11500

Another way using base:

game_data$firstna <-
unlist(
tapply(game_data$points, game_data$player, function(x) {i<-which(is.na(x))[1];x[]<-0;x[i]<-1;x})
)

or as another ?ave clone:

ave(game_data$points, game_data$player, FUN = function(x) {
    i<-which(is.na(x))[1];x[]<-0;x[i]<-1;x
})

Upvotes: 0

nicola
nicola

Reputation: 24500

A base R solution:

ave(game_data$points, game_data$player,
    FUN = function(x) seq_along(x) == match(NA, x, nomatch = 0))

Upvotes: 8

Randall Helms
Randall Helms

Reputation: 859

You can do this by grouping by player and then mutating to check if a row has an NA value and the previous row doesn't

game_data %>%
  group_by(player) %>%
  mutate(firstna = ifelse(is.na(points) & lag(!is.na(points)),1,0)) %>%
  ungroup()

Result:

# A tibble: 8 x 4
# Groups:   player [2]
  player level points firstna
   <dbl> <dbl>  <dbl>   <dbl>
1      1     1     20       0
2      1     2     NA       1
3      1     3     NA       0
4      1     4     NA       0
5      2     1     20       0
6      2     2     40       0
7      2     3     NA       1
8      2     4     NA       0

Upvotes: 2

jogo
jogo

Reputation: 12569

Here is a solution with data.table:

library("data.table")

game_data <- data.table(player = c(1,1,1,1,2,2,2,2), level = c(1,2,3,4,1,2,3,4), points = c(20,NA,NA,NA,20,40,NA,NA))

game_data[, firstna:=is.na(points) & !is.na(shift(points)), player][]
# > game_data[, firstna:=is.na(points) & !is.na(shift(points)), player][]
#    player level points firstna
# 1:      1     1     20   FALSE
# 2:      1     2     NA    TRUE
# 3:      1     3     NA   FALSE
# 4:      1     4     NA   FALSE
# 5:      2     1     20   FALSE
# 6:      2     2     40   FALSE
# 7:      2     3     NA    TRUE
# 8:      2     4     NA   FALSE

Upvotes: 3

Related Questions