Estructuras de datos
Hoja de ruta
Leccion: 40 min
Prácticas: 15 minPreguntas
¿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 encats
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ónmatrix
.)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ónmatrix
.)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.
matrix(c(4, 1, 9, 5, 10, 7), nrow = 3)
matrix(c(4, 9, 10, 1, 5, 7), ncol = 2, byrow = TRUE)
matrix(c(4, 9, 10, 1, 5, 7), nrow = 2)
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.