Estructuras de datos

Hoja de ruta

Leccion: 40 min
Prácticas: 15 min
Preguntas
  • ¿Cómo puedo leer datos en R?

  • ¿Cuáles son los tipos de datos básicos en R?

  • ¿Cómo represento la información categórica en R?

Objetivos
  • Conocer los distintos tipos de datos.

  • Comenzar a explorar los data frames y entender cómo se relacionan con vectors, factors y lists.

  • Ser capaz de preguntar sobre el tipo, clase y estructura de un objeto en R.

Una de las características más poderosas de R es su habilidad de manejar datos tabulares - como los que puedes tener en una planilla de cálculo o un archivo CSV. Comencemos creando un dataset de ejemplo en tu directorio datos/, en el archivo llamado feline-data.csv:

coat,weight,likes_string
calico,2.1,1
black,5.0,0
tabby,3.2,1

Consejo: Edición de archivos de texto en R

Alternativamente, puedes crear el archivo datos/datos-felinos.csv usando un editor de texto (Nano), o en RStudio usando el ítem del Menú File -> New File -> Text File.

Podemos leer el archivo en R con el siguiente comando:

cats <- read.csv(file = "data/feline-data.csv")
cats
    coat weight likes_string
1 calico    2.1            1
2  black    5.0            0
3  tabby    3.2            1

La función read.table se usa para leer datos tabulares que están guardados en un archivo de texto, donde las columnas de datos están separadas por un signo de puntuación como en los archivos CSV (donde csv es comma-separated values en inglés, es decir, valores separados por comas).

Los signos de puntuación más comunmente usados para separar o delimitar datos en archivos de texto son tabuladores y comas. Por conveniencia, R provee dos versiones de la función read.table. Estas versiones son: read.csv para archivos donde los datos están separados por comas y read.delim para archivos donde los datos están separados por tabuladores. De las tres variantes, read.csv es la usada más comúnmente. De ser necesario, es posible sobrescribir el signo de puntuación usado por defecto para ambas funciones: read.csv y read.delim.

Podemos empezar a explorar el dataset inmediatamente, proyectando las columnas usando el operador $:

cats$weight
[1] 2.1 5.0 3.2
cats$coat
[1] calico black  tabby 
Levels: black calico tabby

Podemos efectuar otras operaciones a las columnas:

## Say we discovered that the scale weighs two Kg light:
cats$weight + 2
[1] 4.1 7.0 5.2
paste("My cat is", cats$coat)
[1] "My cat is calico" "My cat is black"  "My cat is tabby" 

Pero qué pasa con:

cats$weight + cats$coat
Warning in Ops.factor(cats$weight, cats$coat): '+' not meaningful for
factors
[1] NA NA NA

Entender qué es lo que pasa en este case es clave para analizar datos en R exitosamente.

Tipos de datos

Si adivinaste que el último comando iba a resultar en un error porque 2.1 más "black" no tiene sentido, estás en lo cierto - y ya tienes alguna intuición sobre un concepto importante en programación que se llama tipos de datos. Podemos preguntar cuál es el tipo de datos de algo:

typeof(cats$weight)
[1] "double"

Hay 5 tipos de datos principales: double, integer, complex, logical and character.

typeof(3.14)
[1] "double"
typeof(1L) # The L suffix forces the number to be an integer, since by default R uses float numbers
[1] "integer"
typeof(1+1i)
[1] "complex"
typeof(TRUE)
[1] "logical"
typeof('banana')
[1] "character"

No importa cuan complicado sea nuestro análisis, todos los datos en R se interpretan con uno de estos tipos de datos básicos. Este rigor tiene algunas consecuencias importantes.

Un usuario ha agregado detalles de otro gato. Esta información está en el archivo datos/datos-felinos_v2.csv.

file.show("data/feline-data_v2.csv")
coat,weight,likes_string
calico,2.1,1
black,5.0,0
tabby,3.2,1
tabby,2.3 or 2.4,1

Carga los datos de los nuevos gatos de la misma forma anterior, y comprueba qué tipos de datos encuentras en la columna weight:

cats <- read.csv(file="data/feline-data_v2.csv")
typeof(cats$weight)
[1] "integer"

Oh no, nuestros pesos ya no son de tipo double! Si intentamos hacer los mismos cálculos anteriores, tenemos problemas:

cats$weight + 2
Warning in Ops.factor(cats$weight, 2): '+' not meaningful for factors
[1] NA NA NA NA

¿Qué ocurrió? Cuando R lee un archivo CSV en una de estas tablas, insiste que todas las columnas sean del mismo tipo de datos básico; si no puede entender todo en la columna como un double, entonces ningún elemento de la columna se interpreta como double. La tabla que R cargó con los datos de los gatos se denomina un data.frame, y es nuestro primer ejemplo de algo que se llama una estructura de datos - es decir, una estructura que R sabe cómo construir basada en tipos de datos básicos.

Podemos ver que es un data.frame si usamos la función class:

class(cats)
[1] "data.frame"

Para usar nuestros datos en R exitosamente, necesitamos entender cuáles son las estructuras de datos básicas, y cómo se comportan. Por ahora, eliminemos la línea extra de los datos sobre gatos y volvamos a leer el archivo para investigar el comportamiento más en detalle:

feline-data.csv:

coat,weight,likes_string
calico,2.1,1
black,5.0,0
tabby,3.2,1

Y en RStudio:

cats <- read.csv(file="data/feline-data.csv")

Vectores y Coerción de Tipos

Para entender mejor este comportamiento, veamos otra de las estructuras de datos en R: el vector.

my_vector <- vector(length = 3)
my_vector
[1] FALSE FALSE FALSE

Un vector en R es esencialmente una lista ordenada de cosas, con la condición especial de que todos los elementos en un vector tienen que ser del mismo tipo de datos básico*. Si no eliges un tipo de datos, por defecto R elige el tipo de datos logical. También puedes declarar un vector vacío de cualquier tipo que quieras.

another_vector <- vector(mode='character', length=3)
another_vector
[1] "" "" ""

Puedes checar si algo es un vector:

str(another_vector)
 chr [1:3] "" "" ""

La salida algo críptica de este comando indica el tipo de datos básico encontrado en este vector -en este caso chr o character; una indicación del número de elementos en el vector - específicamente los índices del vector, en este caso [1:3] y unos pocos ejemplos de los elementos del vector - en este caso strings vacíos.

Si, en forma similar, hacemos:

str(cats$weight)
 num [1:3] 2.1 5 3.2

podemos ver que cats$weight también es un vector - las columnas de datos que cargamos en data.frames de R son todas vectores y este es el motivo por el cuál R requiere que todas las columnas sean del mismo tipo de datos básico.

Discusión 1

¿Por qué R es tan obstinado acerca de lo que ponemos en nuestras columnas de datos? ¿Cómo nos ayuda esto?

Discusión 1

Al mantener todos los elementos de una columna del mismo tipo, podemos hacer suposiciones simples sobre nuestros datos; si puedes interpretar un elemento en una columna como un número, entonces puedes interpretar todos los elementos como números, y por tanto no hace falta comprobarlo cada vez. Esta consistencia es lo que se suele mencionar como datos limpios; a la larga, la consistencia estricta hace nuestras vidas más fáciles cuando usamos R.

También puedes crear vectores con contenido explícito con la función combine:

combine_vector <- c(2,6,3)
combine_vector
[1] 2 6 3

Dado lo que aprendimos hasta ahora, ¿qué piensas que va a producir el siguiente código?

quiz_vector <- c(2,6,'3')

Esto se denomina coerción de tipos de datos y es motivo de muchas sorpresas y la razón por la cual es necesario conocer los tipos de datos básicos y cómo R los interpreta. Cuando R encuentra una mezcla de tipos de datos (en este caso númerico y caracteres) para combinarlos en un vector, va a forzarlos a ser del mismo tipo.

Considera:

coercion_vector <- c('a', TRUE)
coercion_vector
[1] "a"    "TRUE"
another_coercion_vector <- c(0, TRUE)
another_coercion_vector
[1] 0 1

Las reglas de coerción son: logical -> integer -> numeric -> complex -> character, donde -> se puede leer como se transforma en. Puedes intentar forzar la coerción de acuerdo a esta cadena usando las funciones as.:

character_vector_example <- c('0','2','4')
character_vector_example
[1] "0" "2" "4"
character_coerced_to_numeric <- as.numeric(character_vector_example)
character_coerced_to_numeric
[1] 0 2 4
numeric_coerced_to_logical <- as.logical(character_coerced_to_numeric)
numeric_coerced_to_logical
[1] FALSE  TRUE  TRUE

Como puedes ver, algunas cosas sorprendentes ocurren cuando R forza un tipo de datos en otro tipo! Dejando de lado los detalles de la coerción, la cuestión es: si tus datos no lucen como pensabas que deberían lucir, puede ser culpa de la coerción de tipos; asegúrate que todos los elementos de tus vectores y las columnas de tus data.frames son del mismo tipo o te encontrarás con sorpresas desagradables!

Pero la coerción de tipos también puede ser muy útil. Por ejemplo, en los datos de cats, likes_string es numérica, pero sabemos que los 1s y 0s en realidad representan TRUE y FALSE (una forma habitual de representarlos). Deberíamos usar el tipo de datos logical en este caso, que tiene dos estados: TRUE o FALSE, que es exactamente lo que nuestros datos representan. Podemos convertir esta columna al tipo de datos logical usando la función as.logical:

cats$likes_string
[1] 1 0 1
cats$likes_string <- as.logical(cats$likes_string)
cats$likes_string
[1]  TRUE FALSE  TRUE

La función combine, c(), también agregará elementos al final de un vector existente:

ab_vector <- c('a', 'b')
ab_vector
[1] "a" "b"
combine_example <- c(ab_vector, 'SWC')
combine_example
[1] "a"   "b"   "SWC"

También puedes hacer una serie de números:

mySeries <- 1:10
mySeries
 [1]  1  2  3  4  5  6  7  8  9 10
seq(10)
 [1]  1  2  3  4  5  6  7  8  9 10
seq(1,10, by=0.1)
 [1]  1.0  1.1  1.2  1.3  1.4  1.5  1.6  1.7  1.8  1.9  2.0  2.1  2.2  2.3
[15]  2.4  2.5  2.6  2.7  2.8  2.9  3.0  3.1  3.2  3.3  3.4  3.5  3.6  3.7
[29]  3.8  3.9  4.0  4.1  4.2  4.3  4.4  4.5  4.6  4.7  4.8  4.9  5.0  5.1
[43]  5.2  5.3  5.4  5.5  5.6  5.7  5.8  5.9  6.0  6.1  6.2  6.3  6.4  6.5
[57]  6.6  6.7  6.8  6.9  7.0  7.1  7.2  7.3  7.4  7.5  7.6  7.7  7.8  7.9
[71]  8.0  8.1  8.2  8.3  8.4  8.5  8.6  8.7  8.8  8.9  9.0  9.1  9.2  9.3
[85]  9.4  9.5  9.6  9.7  9.8  9.9 10.0

Podemos preguntar algunas cosas sobre los vectores:

sequence_example <- seq(10)
head(sequence_example, n=2)
[1] 1 2
tail(sequence_example, n=4)
[1]  7  8  9 10
length(sequence_example)
[1] 10
class(sequence_example)
[1] "integer"
typeof(sequence_example)
[1] "integer"

Finalmente, puedes darle nombres a los elementos de tu vector:

my_example <- 5:8
names(my_example) <- c("a", "b", "c", "d")
my_example
a b c d 
5 6 7 8 
names(my_example)
[1] "a" "b" "c" "d"

Desafío 1

Comienza construyendo un vector con los números del 1 al 26. Multiplica el vector por 2 y asigna al vector resultante los nombres A hasta Z (Pista: hay un vector pre-definido llamado LETTERS)

Solución del desafío 1

x <- 1:26
x <- x * 2
names(x) <- LETTERS

Data Frames

Ya mencionamos que las columnas en los data.frames son vectores:

str(cats$weight)
 num [1:3] 2.1 5 3.2
str(cats$likes_string)
 logi [1:3] TRUE FALSE TRUE

Esto tiene sentido, pero qué pasa con:

str(cats$coat)
 Factor w/ 3 levels "black","calico",..: 2 1 3

Factores

Otra estructura de datos importante se llama factor. Factores usualmente parecen caracteres, pero se usan para representar información categórica. Por ejemplo, construyamos un vector de strings con etiquetas para las coloraciones para todos los gatos en nuestro estudio:

coats <- c('tabby', 'tortoiseshell', 'tortoiseshell', 'black', 'tabby')
coats
[1] "tabby"         "tortoiseshell" "tortoiseshell" "black"        
[5] "tabby"        
str(coats)
 chr [1:5] "tabby" "tortoiseshell" "tortoiseshell" "black" ...

Podemos convertir un vector en un factor de la siguiente manera:

CATegories <- factor(coats)
class(CATegories)
[1] "factor"
str(CATegories)
 Factor w/ 3 levels "black","tabby",..: 2 3 3 1 2

Ahora R puede interpretar que hay tres posibles categorías en nuestros datos - pero también hizo algo sorprendente; en lugar de imprimir los strings como se las dimos, imprimió una serie de números. R ha reemplazado las categorías con índices numéricos, lo cuál es necesario porque muchos cálculos estadísticos usan esa representación para datos categóricos:

typeof(coats)
[1] "character"
typeof(CATegories)
[1] "integer"

Desafío 2

¿Hay algún un factor en nuestro data.frame cats? ¿Cuál es el nombre? Intenta usar ?read.csv para darte cuenta cómo mantener las columnas de texto como vectores de caracteres en lugar de factores; luego escribe uno o más comandos para mostrar que el factor en cats es en realidad un vector de caracteres cuando se carga de esta manera.

Solución al desafío 2

Una solución es usar el argumento stringAsFactors:

cats <- read.csv(file="data/feline-data.csv", stringsAsFactors=FALSE)
str(cats$coat)

Otra solución es usar el argumento colClasses que permiten un control más fino.

cats <- read.csv(file="data/feline-data.csv", colClasses=c(NA, NA, "character"))
str(cats$coat)

Nota: Los nuevos estudiantes encuentran los archivos de ayuda difíciles de entender; asegúrese de hacerles saber que esto es normal, y anímelos a que tomen su mejor opción en función del significado semantico, incluso si no están seguros.

En las funciones de modelado, es importante saber cuáles son los niveles de referencia. Se asume que es el primer factor, pero por defecto los factores están etiquetados en orden alfabetico. Puedes cambiar esto especificando los niveles:

mydata <- c("case", "control", "control", "case")
factor_ordering_example <- factor(mydata, levels = c("control", "case"))
str(factor_ordering_example)
 Factor w/ 2 levels "control","case": 2 1 1 2

En este caso, le hemos dicho explícitamente a R que “control” debería estar representado por 1, y “case” por 2. Esta designación puede ser muy importante para interpretar los resultados de modelos estadísticos!

Listas

Otra estructura de datos que quedrás en tu bolsa de trucos es list. Una lista es más simple, en algunos aspectos que los otros tipos, porque puedes poner cualquier cosa que tú quieras en ella:

list_example <- list(1, "a", TRUE, 1+4i)
list_example
[[1]]
[1] 1

[[2]]
[1] "a"

[[3]]
[1] TRUE

[[4]]
[1] 1+4i
another_list <- list(title = "Numbers", numbers = 1:10, data = TRUE )
another_list
$title
[1] "Numbers"

$numbers
 [1]  1  2  3  4  5  6  7  8  9 10

$data
[1] TRUE

Ahora podemos entender algo un poco sorprendente en nuestro data.frame; ¿Qué pasa si corremos?

typeof(cats)
[1] "list"

Vemos que los data.frames parecen listas ‘en su cara oculta’ - ​​esto es porque un data.frame es realmente una lista de vectores y factores, como debe ser - para mantener esas columnas que son una combinación de vectores y factores, el data.frame necesita algo más flexible que un vector para poner todas las columnas juntas en una tabla. En otras palabras, un data.frame es una lista especial en la que todos los vectores deben tener la misma longitud.

En nuestro ejemplo de cats, tenemos un número entero, un doble y una variable lógica. Como ya hemos visto, cada columna del data.frame es un vector.

cats$coat
[1] calico black  tabby 
Levels: black calico tabby
cats[,1]
[1] calico black  tabby 
Levels: black calico tabby
typeof(cats[,1])
[1] "integer"
str(cats[,1])
 Factor w/ 3 levels "black","calico",..: 2 1 3

Cada fila es una observación de diferentes variables del mismo data.frame, y por lo tanto puede estar compuesto de elementos de diferentes tipos.

cats[1,]
    coat weight likes_string
1 calico    2.1         TRUE
typeof(cats[1,])
[1] "list"
str(cats[1,])
'data.frame':	1 obs. of  3 variables:
 $ coat        : Factor w/ 3 levels "black","calico",..: 2
 $ weight      : num 2.1
 $ likes_string: logi TRUE

Desafío 3

Hay varias maneras sutílmente diferentes de indicar variables, observaciones y elementos de data.frames:

  • cats[1]
  • cats[[1]]
  • cats$coat
  • cats["coat"]
  • cats[1, 1]
  • cats[, 1]
  • cats[1, ]

Investiga cada uno de los ejemplos anteriores y explica el resultado de cada uno.

Sugerencia: Usa la función typeof() para examinar el resultado en cada caso.

Solución al desafío 3

cats[1]
    coat
1 calico
2  black
3  tabby

Podemos interpretar un data frame como una lista de vectores. Un único par de corchetes [1] resulta en la primer proyección de la lista, como otra lista. En este caso es la primer columna del data frame.

cats[[1]]
[1] calico black  tabby 
Levels: black calico tabby

El doble corchete [[1]] devuelve el contenido del elemento de la lista. En este caso, es el contenido de la primera columna, un vector de tipo factor.

cats$coat
[1] calico black  tabby 
Levels: black calico tabby

Este ejemplo usa el caracter $ para direccionar elementos por nombre. coat es la primer columna del marco de datos, de nuevo un vector de tipo factor.

cats["coat"]
    coat
1 calico
2  black
3  tabby

Aquí estamos usando un solo corchete ["coat"] reemplazando el número del índice con el nombre de la columna. Como el ejemplo 1, el objeto devuelto es un list.

cats[1, 1]
[1] calico
Levels: black calico tabby

Este ejemplo usa un sólo corchete, pero esta vez proporcionamos coordenadas de fila y columna. El objeto devuelto es el valor en la fila 1, columna 1. El objeto es un integer pero como es parte de un vector de tipo factor, R muestra la etiqueta “calico” asociada con el valor entero.

cats[, 1]
[1] calico black  tabby 
Levels: black calico tabby

Al igual que en el ejemplo anterior, utilizamos corchetes simples y proporcionamos las coordenadas de fila y columna. La coordenada de la fila no se especifica, R interpreta este valor faltante como todos los elementos en este column vector.

cats[1, ]
    coat weight likes_string
1 calico    2.1         TRUE

De nuevo, utilizamos el corchete simple con las coordenadas de fila y columna. La coordenada de la columna no está especificada. El valor de retorno es una list que contiene todos los valores en la primera fila.

Matrices

Por último, pero no menos importante están las matrices. Podemos declarar una matriz llena de ceros:

matrix_example <- matrix(0, ncol=6, nrow=3)
matrix_example
     [,1] [,2] [,3] [,4] [,5] [,6]
[1,]    0    0    0    0    0    0
[2,]    0    0    0    0    0    0
[3,]    0    0    0    0    0    0

Y de manera similar a otras estructuras de datos, podemos preguntar cosas sobre la matriz:

class(matrix_example)
[1] "matrix"
typeof(matrix_example)
[1] "double"
str(matrix_example)
 num [1:3, 1:6] 0 0 0 0 0 0 0 0 0 0 ...
dim(matrix_example)
[1] 3 6
nrow(matrix_example)
[1] 3
ncol(matrix_example)
[1] 6

Desafío 4

¿Cuál crees que es el resultado del comando length(matrix_example)? Inténtalo. ¿Estabas en lo correcto? ¿Por qué / por qué no?

Solución al desafío 4

¿Cuál crees que es el resultado del comando length(matrix_example)?

matrix_example <- matrix(0, ncol=6, nrow=3)
length(matrix_example)
[1] 18

Debido a que una matriz es un vector con atributos de dimensión añadidos, length proporciona la cantidad total de elementos en la matriz.

Desafío 5

Construye otra matriz, esta vez conteniendo los números 1:50, con 5 columnas y 10 renglones. ¿Cómo llenó la función matrix de manera predeterminada la matriz, por columna o por renglón? Investiga como cambiar este comportamento. (Sugerencia: lee la documentación de la función matrix.)

Solución al desafío 5

Construye otra matriz, esta vez conteniendo los números 1:50, con 5 columnas y 10 renglones. ¿Cómo llenó la función matrix de manera predeterminada la matriz, por columna o por renglón? Investiga como cambiar este comportamento. (Sugerencia: lee la documentación de la función matrix.)

x <- matrix(1:50, ncol=5, nrow=10)
x <- matrix(1:50, ncol=5, nrow=10, byrow = TRUE) # to fill by row

Desafío 6

Crea una lista de longitud dos que contenga un vector de caracteres para cada una de las secciones en esta parte del curso:

  • tipos de datos
  • estructura de datos

Inicializa cada vector de caracteres con los nombres de los tipos de datos y estructuras de datos que hemos visto hasta ahora.

Solución al desafío 6

dataTypes <- c('double', 'complex', 'integer', 'character', 'logical')
dataStructures <- c('data.frame', 'vector', 'factor', 'list', 'matrix')
answer <- list(dataTypes, dataStructures)

Nota: es útil hacer una lista en el pizarrón o en papel colgado en la pared listando todos los tipos y estructuras de datos y mantener la lista durante el resto del curso para recordar la importancia de estos elementos básicos.

Desafío 7

Considera la salida de R para la siguiente matriz:

     [,1] [,2]
[1,]    4    1
[2,]    9    5
[3,]   10    7

¿Cuál fué el comando correcto para escribir esta matriz? Examina cada comando e intenta determinar el correcto antes de escribirlos. Piensa en qué matrices producirán los otros comandos.

  1. matrix(c(4, 1, 9, 5, 10, 7), nrow = 3)
  2. matrix(c(4, 9, 10, 1, 5, 7), ncol = 2, byrow = TRUE)
  3. matrix(c(4, 9, 10, 1, 5, 7), nrow = 2)
  4. matrix(c(4, 1, 9, 5, 10, 7), ncol = 2, byrow = TRUE)

Solución al desafío 7

Considera la salida de R para la siguiente matriz:

     [,1] [,2]
[1,]    4    1
[2,]    9    5
[3,]   10    7

¿Cuál era el comando correcto para escribir esta matriz? Examina cada comando e intenta determinar el correcto antes de escribirlos. Piensa en qué matrices producirán los otros comandos.

matrix(c(4, 1, 9, 5, 10, 7), ncol = 2, byrow = TRUE)

Puntos Clave

  • Usar read.csv para leer los datos tabulares en R.

  • Los tipos de datos básicos en R son double, integer, complex, logical, y character.

  • Usa factors para representar categorías en R.