Explorando data frames

Hoja de ruta

Leccion: 20 min
Prácticas: 10 min
Preguntas
  • ¿Cómo puedo manipular un dataframe?

Objetivos
  • Poder agregar y quitar filas y columnas.

  • Poder quitar filas con valores NA.

  • Poder anexar dos dataframe.

  • Poder articular qué es un factor y cómo convertir entre factor y character.

  • Poder entender las propiedades básicas de un dataframe, incluyendo tamaño, clase o tipo de columnas, nombres y primeras filas.

A esta altura, ya viste todo - en la última lección, donde recorrimos las estructuras básicas de R. Todo lo que hagas va a ser una manipulación de esas herramientas. Pero la mayoría del tiempo, la estrella del show va a ser el dataframe - la tabla que creamos al cargar información de un archivo csv. En ésta lección, vamos a aprender un par de cosas sobre cómo trabajar con la clase dataframe.

Agregando columnas y filas a un dataframe

Aprendimos que las columnas en un dataframe son vectores. Por lo tanto, sabemos que nuestros datos son consistentes con el tipo de dato dentro de esa columna. Si queremos agregar una nueva columna, podemos empezar por crear un nuevo vector:

age <- c(2,3,5,12)
cats
    coat weight likes_string
1 calico    2.1            1
2  black    5.0            0
3  tabby    3.2            1

Podemos entonces agregarlo como una columna via:

cats <- cbind(cats, age)
Error in data.frame(..., check.names = FALSE): arguments imply differing number of rows: 3, 4

¿Por qué no funcionó? Claro, R quiere ver un elemento en nuestra nueva columna para cada fila de la tabla:

cats
    coat weight likes_string
1 calico    2.1            1
2  black    5.0            0
3  tabby    3.2            1
age <- c(4,5,8)
cats <- cbind(cats, age)
cats
    coat weight likes_string age
1 calico    2.1            1   4
2  black    5.0            0   5
3  tabby    3.2            1   8

Ahora, qué tal si agregamos filas, en este caso, la última vez vimos que las filas de un dataframe están compuestas por listas:

newRow <- list("tortoiseshell", 3.3, TRUE, 9)
cats <- rbind(cats, newRow)
Warning in `[<-.factor`(`*tmp*`, ri, value = "tortoiseshell"): invalid
factor level, NA generated

Factors

Los objetos de la clase factor son otro tipo de datos que debemos usar con cuidado. Cuando R crea un factor, únicamente permite los valores que originalmente estaba allí cuando cargamos los datos. Por ejemplo, en nuestro caso ‘black’, ‘calico’ y ‘tabby’. Cualquier categoría nueva que no entre en esas categorías será rechazada (y se conviertirá en NA).

La advertencia (Warning) nos está diciendo que agregamos ‘tortoiseshell’ a nuestro factor coat. Pero los otros valores, 3.3 (de tipo numeric), TRUE (de tipo logical), y 9 (de tipo numeric) se añadieron exitosamente a weight, likes_string, y age, respectivamente, dado que esos valores no son de tipo factor. Para añadir una nueva categoría ‘tortoiseshell’ al dataframe cats en la columna coat, debemos agregar explícitamente a ‘tortoiseshell’ como un nuevo nivel (level) en el factor:

levels(cats$coat)
[1] "black"  "calico" "tabby" 
levels(cats$coat) <- c(levels(cats$coat), 'tortoiseshell')
cats <- rbind(cats, list("tortoiseshell", 3.3, TRUE, 9))

De manera alternativa, podemos cambiar la columna a tipo character. En este caso, perdemos las categorías, pero a partir de ahora podemos incorporar cualquier palabra a la columna, sin problemas con los niveles del factor.

str(cats)
'data.frame':	5 obs. of  4 variables:
 $ coat        : Factor w/ 4 levels "black","calico",..: 2 1 3 NA 4
 $ weight      : num  2.1 5 3.2 3.3 3.3
 $ likes_string: int  1 0 1 1 1
 $ age         : num  4 5 8 9 9
cats$coat <- as.character(cats$coat)
str(cats)
'data.frame':	5 obs. of  4 variables:
 $ coat        : chr  "calico" "black" "tabby" NA ...
 $ weight      : num  2.1 5 3.2 3.3 3.3
 $ likes_string: int  1 0 1 1 1
 $ age         : num  4 5 8 9 9

Desafío 1

Imaginemos que, como los perros, 1 año humano es equivalente a 7 años en los gatos (La compañía Purina usa un algoritmo más sofisticado).

  1. Crea un vector llamado human.age multiplicando cats$age por 7.
  2. Convierte human.age a factor.
  3. Convierte human.age de nuevo a un vector numérico usando la función as.numeric(). Ahora, divide por 7 para regresar a las edades originales. Explica lo sucedido.

Solución al Desafío 1

  1. human.age <- cats$age * 7
  2. human.age <- factor(human.age) o as.factor(human.age) las dos opciones funcionan igual de bien.
  3. as.numeric(human.age) produce 1 2 3 4 4 porque los factores se guardan como objetos de tipo entero integer (1:4), cada uno de los cuales tiene asociado una etiqueta label (28, 35, 56, y 63). Convertir un objeto de un tipo de datos a otro, por ejemplo de factor a numeric nos dá los enteros, no las etiquetas labels. Si queremos los números originales, necesitamos un paso intermedio, debemos convertir human.age al tipo character y luego a numeric (¿cómo funciona esto?). Esto aparece en la vida real cuando accidentalmente incluimos un character en alguna columna de nuestro archivo .csv, que se suponía que únicamente contendría números. Tendremos este problema, si al leer el archivo olvidamos incluir stringsAsFactors=FALSE.

Quitando filas

Ahora sabemos cómo agregar filas y columnas a nuestro dataframe en R, pero en nuestro primer intento para agregar un gato llamado ‘tortoiseshell’ agregamos una fila que no sirve.

cats
           coat weight likes_string age
1        calico    2.1            1   4
2         black    5.0            0   5
3         tabby    3.2            1   8
4          <NA>    3.3            1   9
5 tortoiseshell    3.3            1   9

Podemos pedir el dataframe sin la fila errónea:

cats[-4,]
           coat weight likes_string age
1        calico    2.1            1   4
2         black    5.0            0   5
3         tabby    3.2            1   8
5 tortoiseshell    3.3            1   9

Notar que -4 significa que queremos remover la cuarta fila, la coma sin nada detrás indica que se aplica a todas las columnas. Podríamos remover ambas filas en un llamado usando ambos números dentro de un vector: cats[c(-4,-5),]

Alternativamente, podemos eliminar filas que contengan valores NA:

na.omit(cats)
           coat weight likes_string age
1        calico    2.1            1   4
2         black    5.0            0   5
3         tabby    3.2            1   8
5 tortoiseshell    3.3            1   9

Volvamos a asignar el nuevo resultado output al dataframe cats, así nuestros cambios son permanentes:

cats <- na.omit(cats)

Añadiendo a un dataframe

La clave que hay que recordar al añadir datos a un dataframe es que las columnas son vectores o factores, mientras que las filas son listas. Podemos pegar dos dataframes usando rbind que significa unir las filas (verticalmente):

cats <- rbind(cats, cats)
cats
            coat weight likes_string age
1         calico    2.1            1   4
2          black    5.0            0   5
3          tabby    3.2            1   8
5  tortoiseshell    3.3            1   9
11        calico    2.1            1   4
21         black    5.0            0   5
31         tabby    3.2            1   8
51 tortoiseshell    3.3            1   9

Pero ahora los nombres de las filas rownames son complicados. Podemos removerlos y R los nombrará nuevamente, de manera secuencial:

rownames(cats) <- NULL
cats
           coat weight likes_string age
1        calico    2.1            1   4
2         black    5.0            0   5
3         tabby    3.2            1   8
4 tortoiseshell    3.3            1   9
5        calico    2.1            1   4
6         black    5.0            0   5
7         tabby    3.2            1   8
8 tortoiseshell    3.3            1   9

Desafío 2

Puedes crear un nuevo dataframe desde R con la siguiente sintaxis:

df <- data.frame(id = c('a', 'b', 'c'),
                 x = 1:3,
                 y = c(TRUE, TRUE, FALSE),
                 stringsAsFactors = FALSE)

Crear un dataframe que contenga la siguiente información personal:

  • Nombre
  • Apellido
  • Número favorito

Luego usa rbind para agregar una entrada para la gente sentada alrededor tuyo. Finalmente, usa cbind para agregar una columna con espacio para que cada persona conteste a la siguiente pregunta: “¿Es hora de una pausa?”

Solución al Desafío 2

df <- data.frame(first = c('Grace'),
                 last = c('Hopper'),
                 lucky_number = c(0),
                 stringsAsFactors = FALSE)
df <- rbind(df, list('Marie', 'Curie', 238) )
df <- cbind(df, coffeetime = c(TRUE,TRUE))

Ejemplo realista

Hasta ahora, hemos visto las manipulaciones básicas que pueden hacerse en un dataframe. Ahora, vamos a extender esas habilidades con un ejemplo más real. Vamos a importar el gapminder dataset que descargamos previamente:

gapminder <- read.csv("data/gapminder-FiveYearData.csv")

Tips misceláneos

  • Otro tipo de archivo que puedes encontrar es el separado por tabuladores (.tsv). Para especificar este separador, usa "\t" o read.delim().

  • Los archivos pueden descargarse de Internet a una carpeta local usando download.file. La función read.csv puede ser ejecutada para leer el archivo descargado, por ejemplo:

download.file("https://raw.githubusercontent.com/swcarpentry/r-novice-gapminder/gh-pages/_episodes_rmd/data/gapminder-FiveYearData.csv", destfile = "data/gapminder-FiveYearData.csv")
gapminder <- read.csv("data/gapminder-FiveYearData.csv")
  • De manera alternativa, puedes leer los archivos directamente en R, usando una dirección web y read.csv. Es importante notar que, si se hace esto último, no habrá una copia local del archivo csv en tu computadora. Por ejemplo,
gapminder <- read.csv("https://raw.githubusercontent.com/swcarpentry/r-novice-gapminder/gh-pages/_episodes_rmd/data/gapminder-FiveYearData.csv")
  • Puedes leer directamente planillas de Excel sin necesidad de convertirlas a texto plano usando el paquete readxl.

Vamos a investigar gapminder un poco; lo primero que hay que hacer siempre es ver cómo se ve el dataset usando str:

str(gapminder)
'data.frame':	1704 obs. of  6 variables:
 $ country  : Factor w/ 142 levels "Afghanistan",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ year     : int  1952 1957 1962 1967 1972 1977 1982 1987 1992 1997 ...
 $ pop      : num  8425333 9240934 10267083 11537966 13079460 ...
 $ continent: Factor w/ 5 levels "Africa","Americas",..: 3 3 3 3 3 3 3 3 3 3 ...
 $ lifeExp  : num  28.8 30.3 32 34 36.1 ...
 $ gdpPercap: num  779 821 853 836 740 ...

También podemos examinar columnas individuales del dataframe con la función typeof:

typeof(gapminder$year)
[1] "integer"
typeof(gapminder$country)
[1] "integer"
str(gapminder$country)
 Factor w/ 142 levels "Afghanistan",..: 1 1 1 1 1 1 1 1 1 1 ...

También podemos interrogar al dataframe por la información sobre sus dimensiones; recordando que str(gapminder) dijo que había 1704 observaciones de 6 variables en gapminder, ¿Qué piensas que el siguiente código producirá y por qué?

length(gapminder)
[1] 6

Un intento certero hubiera sido decir que el largo (length) de un dataframe es el número de filas (1704), pero no es el caso; recuerda, un dataframe es una lista de vectores y factors.

typeof(gapminder)
[1] "list"

Cuando length devuelve 6, es porque gapminder está construida por una lista de 6 columnas. Para conseguir el número de filas, intenta:

nrow(gapminder)
[1] 1704
ncol(gapminder)
[1] 6

O, para obtener ambos de una vez:

dim(gapminder)
[1] 1704    6

Probablemente queremos saber los nombres de las columnas. Para hacerlo, podemos pedir:

colnames(gapminder)
[1] "country"   "year"      "pop"       "continent" "lifeExp"   "gdpPercap"

A esta altura, es importante preguntarnos si la estructura de R está en sintonía con nuestra intuición y nuestras expectativas, ¿tienen sentido los tipos de datos reportados para cada columna? Si no lo tienen, necesitamos resolver cualquier problema antes de que se conviertan en sorpresas ingratas luego. Podemos hacerlo usando lo que aprendimos sobre cómo R interpreta los datos y la importancia de la estricta consistencia con la que registramos los datos.

Una vez que estamos contentos con el tipo de datos y que la estructura parece razonable, es tiempo de empezar a investigar nuestros datos. Mira las siguientes líneas:

head(gapminder)
      country year      pop continent lifeExp gdpPercap
1 Afghanistan 1952  8425333      Asia  28.801  779.4453
2 Afghanistan 1957  9240934      Asia  30.332  820.8530
3 Afghanistan 1962 10267083      Asia  31.997  853.1007
4 Afghanistan 1967 11537966      Asia  34.020  836.1971
5 Afghanistan 1972 13079460      Asia  36.088  739.9811
6 Afghanistan 1977 14880372      Asia  38.438  786.1134

Desafío 3

También es útil revisar algunas líneas en el medio y el final del dataframe ¿Cómo harías eso?

Buscar líneas exactamente en el medio no es tan difícil, pero simplemente revisar algunas lineas al azar es suficiente. ¿cómo harías eso?

Solución al desafío 3

Para revisar las últimas líneas del dataframe R tiene una función para esto:

tail(gapminder)
      country year      pop continent lifeExp gdpPercap
1699 Zimbabwe 1982  7636524    Africa  60.363  788.8550
1700 Zimbabwe 1987  9216418    Africa  62.351  706.1573
1701 Zimbabwe 1992 10704340    Africa  60.377  693.4208
1702 Zimbabwe 1997 11404948    Africa  46.809  792.4500
1703 Zimbabwe 2002 11926563    Africa  39.989  672.0386
1704 Zimbabwe 2007 12311143    Africa  43.487  469.7093
tail(gapminder, n = 15)
      country year      pop continent lifeExp gdpPercap
1690   Zambia 1997  9417789    Africa  40.238 1071.3538
1691   Zambia 2002 10595811    Africa  39.193 1071.6139
1692   Zambia 2007 11746035    Africa  42.384 1271.2116
1693 Zimbabwe 1952  3080907    Africa  48.451  406.8841
1694 Zimbabwe 1957  3646340    Africa  50.469  518.7643
1695 Zimbabwe 1962  4277736    Africa  52.358  527.2722
1696 Zimbabwe 1967  4995432    Africa  53.995  569.7951
1697 Zimbabwe 1972  5861135    Africa  55.635  799.3622
1698 Zimbabwe 1977  6642107    Africa  57.674  685.5877
1699 Zimbabwe 1982  7636524    Africa  60.363  788.8550
1700 Zimbabwe 1987  9216418    Africa  62.351  706.1573
1701 Zimbabwe 1992 10704340    Africa  60.377  693.4208
1702 Zimbabwe 1997 11404948    Africa  46.809  792.4500
1703 Zimbabwe 2002 11926563    Africa  39.989  672.0386
1704 Zimbabwe 2007 12311143    Africa  43.487  469.7093

Para revisar algunas lineas al azar?

sugerencia: Hay muchas maneras de hacer esto

La solución que presentamos aquí utiliza funciones anidadas, por ejemplo una función es el argumento de otra función. Esto te puede parecer nuevo, pero ya lo haz usado. Recuerda my_dataframe[rows, cols] imprime el dataframe con la sección de filas y columnas definidas (incluso puedes seleccionar un rando de filas y columnas usando : por ejemplo). Para obtener un número al azar o varios números al azar R tiene una función llamada sample.

gapminder[sample(nrow(gapminder), 5), ]
        country year      pop continent lifeExp gdpPercap
1039 Mozambique 1982 12587223    Africa  42.795  462.2114
73      Austria 1952  6927772    Europe  66.800 6137.0765
25      Algeria 1952  9279525    Africa  43.077 2449.0082
1028    Morocco 1987 22987397    Africa  62.677 2755.0470
510    Ethiopia 1977 34617799    Africa  44.510  556.8084

Para que nuestro analisis sea reproducible debemos poner el código en un script entonces podemos volver y editar en el futuro.

Desafío 4

Ve a Archivo -> nuevo -> R script, y crea un script de R llamado load-gapminder.R para cargar el dataset gapminder. Ponlo en el directorio scripts/ y agrégalo al control de versiones.

Ejecuta el script usando la función source, usando el path como su argumento o apretando el botón de “source” en RStudio.

Solución al desafío 4

Los contenidos de scripts/load-gapminder.R:

download.file("https://raw.githubusercontent.com/swcarpentry/r-novice-gapminder/gh-pages/_episodes_rmd/data/gapminder-FiveYearData.csv", destfile = "data/gapminder-FiveYearData.csv")
gapminder <- read.csv(file = "data/gapminder-FiveYearData.csv")

Para ejecutar el script y cargar los archivos en la variable gapminder:

To run the script and load the data into the gapminder variable:

source(file = "scripts/load-gapminder.R")

Desafío 5

Leer el output de str(gapminder) de nuevo; esta vez, usar lo que has aprendido de factores, listas y vectores, las funciones como colnames y dim para explicar qué significa el output de str. Si hay partes que no puedes entender, discútelo con tus compañeros.

Solución desafío 5

El objeto gapminder es un dataframe con columnas

  • country y continent como factors.
  • year como integer vector.
  • pop, lifeExp, and gdpPercap como numeric vectors.

Puntos Clave

  • Usar cbind() para agregar una nueva columna a un dataframe.

  • Usar rbind() para agregar una nueva fila a un dataframe.

  • Quitar filas de un dataframe.

  • Usar na.omit() para remover filas de un dataframe con valores NA.

  • Usar levels() y as.character() para explorar y manipular columnas de clase factor

  • Usar str(), nrow(), ncol(), dim(), colnames(), rownames(), head() y typeof() para entender la estructura de un dataframe

  • Leer un archivo csv usando read.csv()

  • Entender el uso de length() en un dataframe