will while used the switch only multiple length has funcion first else doble contador conditions condicional anidados and r loops if-statement nested-loops

while - if r==



Alternativas a declaraciones ifelse anidadas en R (7)

Supongamos que tenemos los siguientes datos. Las filas representan un país y las columnas (en in05:in09 ) indican si ese país estuvo presente en una base de datos de interés en el año determinado ( 2005:2009 ).

id <- c("a", "b", "c", "d") in05 <- c(1, 0, 0, 1) in06 <- c(0, 0, 0, 1) in07 <- c(1, 1, 0, 1) in08 <- c(0, 1, 1, 1) in09 <- c(0, 0, 0, 1) df <- data.frame(id, in05, in06, in07, in08, in09)

Quiero crear una variable de primer año que indique el primer año en el que el país estuvo presente en la base de datos. En este momento hago lo siguiente:

df$firstyear <- ifelse(df$in05==1,2005, ifelse(df$in06==1,2006, ifelse(df$in07==1, 2007, ifelse(df$in08==1, 2008, ifelse(df$in09==1, 2009, 0)))))

El código anterior ya no es muy bueno y mi conjunto de datos contiene muchos más años. ¿Hay alguna alternativa, usar *apply funciones de *apply , bucles u otra cosa, para crear esta variable de primer año?


Aquí hay otra opción:

years <- as.integer(substr(names(df[-1]), 3, 4)) + 2000L cbind(df, yr=do.call(pmin.int, Map(`/`, years, df[-1])))

Produce:

id in05 in06 in07 in08 in09 yr 1 a 1 0 1 0 0 2005 2 b 0 0 1 1 0 2007 3 c 0 0 0 1 0 2008 4 d 1 1 1 1 1 2005

Y es rápido. Aquí solo se mide el tiempo para encontrar el año mínimo utilizando los datos de Alexis:

Unit: milliseconds expr min lq median uq max neval do.call(pmin.int, Map(`/`, 11:20, DF[-1])) 178.46993 194.3760 219.8898 229.1597 307.1120 10 ff(DF[-1]) 416.07297 434.0792 439.1970 452.8345 496.2048 10 max.col(DF[-1], "first") 99.71936 138.2285 175.2334 207.6365 239.6519 10

Curiosamente, esto no reproduce los tiempos de Alexis, mostrando a David como el más rápido. Esto está en R 3.1.2.

EDITAR : basado en convo con Frank, actualicé la función Alexis para ser más compatible con R 3.1.2:

ff2 = function(x) { ans = as.integer(x[[1]]) for(i in 2:length(x)) { inds = which(ans == 0L) if(!length(inds)) return(ans) ans[inds] = i * (x[[i]][inds] == 1) } return(ans) }

Y esto se acerca a los resultados originales:

Unit: milliseconds expr min lq median uq max neval ff(DF[-1]) 407.92699 415.11716 421.18274 428.02092 462.2474 10 ff2(DF[-1]) 64.20484 72.74729 79.85748 81.29153 148.6439 10


Otra respuesta con algunas notas de eficiencia (aunque esta QA no se trata de velocidad).

En primer lugar, podría ser mejor evitar la conversión de una estructura de "lista" -y a una "matriz"; a veces vale la pena convertirlo en una "matriz" y usar una función que maneja eficientemente un ''vector con un'' atributo ''atenuado'' (es decir, una ''matriz'' / ''matriz'') - otras veces no lo es. Tanto max.col como apply convierten en una "matriz".

En segundo lugar, en situaciones como estas, donde no es necesario que verifiquemos todos los datos mientras llegamos a una solución, podríamos beneficiarnos de una solución con un ciclo que controla lo que pasa en la siguiente iteración. Aquí sabemos que podemos detenernos cuando encontremos el primer "1". Ambos max.col (y which.max ) tienen que which.max una vez para, en realidad, encontrar el valor máximo; el hecho de que sabemos que "max == 1" no se aprovecha.

En tercer lugar, la match es potencialmente más lenta cuando buscamos solo un valor en otro vector de valores porque la configuración de los match es bastante complicada y costosa:

x = 5; set.seed(199); tab = sample(1e6) identical(match(x, tab), which.max(x == tab)) #[1] TRUE microbenchmark::microbenchmark(match(x, tab), which.max(x == tab), times = 25) #Unit: milliseconds # expr min lq median uq max neval # match(x, tab) 142.22327 142.50103 142.79737 143.19547 145.37669 25 # which.max(x == tab) 18.91427 18.93728 18.96225 19.58932 38.34253 25

En resumen, una forma de trabajar en la estructura de "lista" de un "data.frame" y detener los cálculos cuando encontramos un "1" podría ser un ciclo como el siguiente:

ff = function(x) { x = as.list(x) ans = as.integer(x[[1]]) for(i in 2:length(x)) { inds = ans == 0L if(!any(inds)) return(ans) ans[inds] = i * (x[[i]][inds] == 1) } return(ans) }

Y las soluciones en las otras respuestas (ignorando los pasos adicionales para la salida):

david = function(x) max.col(x, "first") plafort = function(x) apply(x, 1, match, x = 1) ff(df[-1]) #[1] 1 3 4 1 david(df[-1]) #[1] 1 3 4 1 plafort(df[-1]) #[1] 1 3 4 1

Y algunos puntos de referencia:

set.seed(007) DF = data.frame(id = seq_len(1e6), "colnames<-"(matrix(sample(0:1, 1e7, T, c(0.25, 0.75)), 1e6), paste("in", 11:20, sep = ""))) identical(ff(DF[-1]), david(DF[-1])) #[1] TRUE identical(ff(DF[-1]), plafort(DF[-1])) #[1] TRUE microbenchmark::microbenchmark(ff(DF[-1]), david(DF[-1]), as.matrix(DF[-1]), times = 30) #Unit: milliseconds # expr min lq median uq max neval # ff(DF[-1]) 64.83577 65.45432 67.87486 70.32073 86.72838 30 # david(DF[-1]) 112.74108 115.12361 120.16118 132.04803 145.45819 30 # as.matrix(DF[-1]) 20.87947 22.01819 27.52460 32.60509 45.84561 30 system.time(plafort(DF[-1])) # user system elapsed # 4.117 0.000 4.125

No es realmente un apocalipsis, pero vale la pena ver que los enfoques algorítmicos sencillos y directos pueden, de hecho, probar ser igualmente buenos o incluso mejores dependiendo del problema. Obviamente, (la mayoría) otras veces bucle en R puede ser laborioso.


Otras alternativas desordenadas:

library(tidyr) library(sqldf) newdf <- gather(df, year, code, -id) df$firstyear <- sqldf(''SELECT min(rowid) rowid, id, year as firstyear FROM newdf WHERE code = 1 GROUP BY id'')[3]

library(tidyr) df2 <- gather(df, year, code, -id) df2 <- df2[df2$code == 1, 1:2] df2 <- df2[!duplicated(df2$id), ] merge(df, df2)

library(tidyr) library(dplyr) newdf <- gather(df, year, code, -id) df$firstyear <- (newdf %>% filter(code==1) %>% select(id, year) %>% group_by(id) %>% summarise(first = first(year)))[2]

Salida:

id in05 in06 in07 in08 in09 year 1 a 1 0 1 0 0 in05 2 b 0 0 1 1 0 in07 3 c 0 0 0 1 0 in08 4 d 1 1 1 1 1 in05

Una solución más limpia que combina la solución de plaforts con alexises_laz es:

names(df) <- c("id", 2005, 2006, 2007, 2008, 2009) df$firstyear <- names(df[-1])[apply(df[-1], 1, which.max)] id 2005 2006 2007 2008 2009 firstyear 1 a 1 0 1 0 0 2005 2 b 0 0 1 1 0 2007 3 c 0 0 0 1 0 2008 4 d 1 1 1 1 1 2005

Si quisiéramos conservar los nombres de las columnas originales, podríamos usar el cambio de nombre provisto por @David Arenburg.

df$firstYear <- gsub(''in'', ''20'', names(df[-1]))[apply(df[-1], 1, which.max)] id in05 in06 in07 in08 in09 firstYear 1 a 1 0 1 0 0 2005 2 b 0 0 1 1 0 2007 3 c 0 0 0 1 0 2008 4 d 1 1 1 1 1 2005


Puede usar dplyr::case_when dentro de dplyr::mutate() siguiendo las líneas del método presentado en este tweet .

# Using version 0.5.0. # Dev version may work without `with()`. df %>% mutate(., firstyear = with(., case_when( in05 == 1 ~ 2005, in06 == 1 ~ 2006, in07 == 1 ~ 2007, in08 == 1 ~ 2008, in09 == 1 ~ 2009, TRUE ~ 0 )))


Puedes vectorizar usando max.col

indx <- names(df)[max.col(df[-1], ties.method = "first") + 1L] df$firstyear <- as.numeric(sub("in", "20", indx)) df # id in05 in06 in07 in08 in09 firstyear # 1 a 1 0 1 0 0 2005 # 2 b 0 0 1 1 0 2007 # 3 c 0 0 0 1 0 2008 # 4 d 1 1 1 1 1 2005


Siempre prefiero trabajar con datos arreglados. Primer método filtra en cumsums

# Tidy df <- df %>% gather(year, present.or.not, -id) # Create df of first instances first.df <- df %>% group_by(id, present.or.not) %>% mutate(ranky = rank(cumsum(present.or.not)), first.year = year) %>% filter(ranky == 1) # Prepare for join first.df <- first.df[,c(''id'', ''first.year'')] # Join with original df <- left_join(df,first.df) # Spread spread(df, year, present.or.not)

O esta alternativa que, después de ordenar, corta la primera fila de grupos arreglados.

df %>% gather(year, present_or_not, -id) %>% filter(present_or_not==1) %>% group_by(id) %>% arrange(id, year) %>% slice(1) %>% mutate(year = str_replace(year, "in", "20")) %>% select(1:2) %>% right_join(df)`


df$FirstYear <- gsub(''in'', ''20'', names(df))[apply(df, 1, match, x=1)] df id in05 in06 in07 in08 in09 FirstYear 1 a 1 0 1 0 0 2005 2 b 0 0 1 1 0 2007 3 c 0 0 0 1 0 2008 4 d 1 1 1 1 1 2005

Hay muchas formas de hacerlo. Usé la match porque encontrará la primera instancia de un valor especificado. Las otras partes del código son para presentación. Primero ir línea por línea con apply y nombrar los años por los nombres de columna con names . La asignación <- y df$FirstYear es una forma de agregar el resultado al marco de datos.

crédito adicional @David Arenburg tiene una buena idea acerca de sustituir la entrada por 20 en la columna FirstYear .