3.2 Vectores y operaciones

Quizá ya hayas escuchado lo siguiente en algún lado: R es un lenguaje vectorial.

El comportamiento al que hace referencia esta frase será de mucha ayuda en la construcción de rutinas avanzadas de programación que veremos más adelante, por ahora la implicación más relevante reside en el hecho de que la estructura básica en R serán justamente vectores.

3.2.1 Vectores atómicos

En todos los lenguajes de programación se tienen diferentes tipos de datos primitivos; por ejemplo en Java se tiene int, long, char, byte, etc. Para Python: String, Float, Boolean, entre otros. En R se tienen 6 tipos de datos primitivos, también conocidos como “atómicos”. En R, en general todo estará compuesto de vectores o de estructuras hechas con vectores y los datos primitivos no son la excepción, ya que estos también son vectores.

  • Lógicos (boolean): TRUE, FALSE
  • Enteros (integer): 5
  • Double (doble): 5.54
    • Es interesante preguntarse el porqué, en general, un valor en coma flotante se le llama (traducido) doble.
    • En el siguiente enlace se hace este interrogante en la página Quora.
    • Los enteros y double son categorizados como vectores numéricos(numeric).
  • Caracteres (character): "word"
  • Complejos (complex): 5 + 7i
  • Crudos o sin procesamiento (raw): raw(5)
NOTA: A partir de aquí, véase el uso de distintas funciones.

Existen algunas funciones de R para saber el tipo de dato que se está usando: typeof(), class() y mode(). Las tres funciones tienen diferentes criterios para determinar que tipo de vector atómico es con el que se está tratando; typeof() identifica el tipo de dato visto desde el punto de vista de R y es la función más usada,mode() es compatible con S y class() identifica el tipo de datos visto desde una POO en R (específicamente el sistema S3).

cat(paste(class(TRUE), 
class(5),  
typeof(5.54),
typeof(5L),
mode("word"), 
class(charToRaw("5")), 
typeof(5 + 7i) ,sep = "\n"))
logical
numeric
double
integer
character
raw
complex

En la siguiente tabla se muestra las posibles salidas con todas las variables de prueba.

typeof() class() mode()
TRUE logical logical logical
5 double numeric numeric
5.4 double numeric numeric
5L integer integer numeric
word character character character
charToRaw(“5”) raw raw raw
5 + 7i complex complex complex

Con lo anterior surge la siguiente pregunta: ¿Hay alguna diferencia entre un vector de tipo integer y un tipo double?

En general, cuando se están trabajando con datos de punto flotante, se esta considerando una aproximación del verdadero número, tanta aproximación como lo permita la memoria, por lo que pueden existir errores de precisión. De hecho, una manera más adecuada de comparar valores con punto decimal sería utilizando la función dplyr::near() en lugar de ==. Después se tratará el tema de las funciones.

Otro punto interesante es que, dependiendo del tipo de vector, se tiene un distinto representante para los valores faltantes (Missing values); para un entero se tiene NA pero con un vector tipo double se tienen NA, NaN, Inf y –Inf. Un ejemplo de esto se puede obtener al dividir distintos números entre 0.


3.2.2 Otros tipos de datos

Además de los vectores atómicos, se tienen otro tipo de vector llamado factor. Un factor es un tipo especial de dato en el cual se puede establecer una jerarquía en variables categóricas.

Supongase que se tiene una variable correspondiente al tamaño de un objeto donde los posibles valores son “Big”, “Little”, “Medium”, “Jumbo”. Intuitivamente se sabe que esta variable es categórica ordinal por lo que se desearía dejar establecido esta jerarquía en el vector que contenga esta información.

factor(c("Big", "Little", "Medium","Jumbo"))
[1] Big    Little Medium Jumbo 
Levels: Big Jumbo Little Medium

Como se puede observar, se tiene un atributo (más adelante se verán los atributos de un vector) indicando los niveles del factor. Los niveles se establecieron en orden alfabético pero sin ningún orden realmente. Para establecer los niveles se puede dar este atributo directamente desde la creación o modificarlo después de la creación del factor.

factor(c("Big", "Little", "Medium","Jumbo"), levels = c("Little", "Medium", "Big", "Jumbo"))
[1] Big    Little Medium Jumbo 
Levels: Little Medium Big Jumbo

De manera análoga, se puede dar el orden a los niveles.

factor(c("Big", "Little", "Medium","Jumbo"), levels = c("Little", "Medium", "Big", "Jumbo"), ordered = TRUE)
[1] Big    Little Medium Jumbo 
Levels: Little < Medium < Big < Jumbo
  • ¿Qué sucede si se agregan más datos pero que pertenecen a los niveles?
  • ¿Qué sucede cuando no se agrega un nivel?
  • ¿Qué pasará con los niveles en datos numéricos?
  • ¿Qué pasará con los niveles en datos numéricos pero dados como caracteres?
  • ¿Qué obtenemos al usar class() y typeof() con un factor?
  • ¿Cómo puede ayudar la función rev() en un factor?

En la siguiente liga se puede ver un poco más de información sobre este tipo de datos.

En la sección 15 del libro R para ciencia de Datos (traducción al español de R for Data Science de Wickham y Grolemund) se mencionan algunas fuentes para aprender más acerca de los factores, el cual es un tema bastante útil de dominar ya que existe una librería llamada forcats con la que se puede trabajar de manera adecuada estos tipos de datos que son de gran ayuda al momento de hacer algún tipo de análisis descriptivo, graficación y creación de modelos. Aquí se enlistan los artículos correspondientes.

Otro tipo de dato con el que se cuenta es aquel que no está definido, es decir, como en Java, un objeto nulo: NULL. En las siguientes líneas de código se muestra como hacer comparaciones con este tipo de dato.

is.null(NULL)
[1] TRUE
0 == NULL
logical(0)
NA == NULL
logical(0)

Otro punto muy común al analizar datos es encontrarse con información faltante. Aquí, como en otros lenguajes, los valores perdidos son distintos a un objeto no definido; ya que un valor perdido bien puede indicar un error humano al momento del registro de la información pero con la característica de que se puede inferir un posible valor para ese registro faltante. Un NA es una constante que puede ser asignada a cualquier tipo de vector a excepción de un vector de tipo raw.

Al tener esta ausencia de datos se desea identificarlos de manera rápida y esto puede ser complicado en una base de datos con cientos o miles de registros; al igual que si se tiene el conocimiento de la existencia de estos, se desea que no afecten en operaciones o aplicaciones de funciones en el resto de los datos. Véase lo siguiente

sum(c(1,NA,3), c(1,2,3))
[1] NA

Es decir, que cuando se tiene algún valor perdido, la operación suma (más adelante se verá a detalle las operaciones en este lenguaje) queda determinada con NA. ¿Sucede lo mismo con otro tipo de operaciones?

Como se había mencionado, se desea que si se tiene conocimiento de la existencia de al menos un NA, este no afecte a las operaciones

sum(c(1,NA,3), c(1,2,3), na.rm = TRUE)
[1] 10

Técnicamente, existen cinco missing values para cada uno de los vectores atómicos (a excepción de raw claro): NA, NA_interger_, NA_real_, NA_character_ y NA_complex_, pero eso solo es de manera interna.


3.2.3 Asignación de nombres y atributos

Como en cualquier lenguaje de programación, existen identificadores para guardar o asignar el valor de los tipos de datos primitivos o resultados y así utilizarlos posteriormente. En el caso de R, la asociación de un nombre con un cierto valor se puede realizar mediante la función <-() y gracias a que R es un lenguaje interpretado, no es necesario declarar el tipo de dato antes de darle una instanciación. Por ejemplo, véase que en las siguientes líneas de código se asignan a algunos vectores atómicos un identificador.

a <- 5+7i
b <- 5L
c <- 10
d <- TRUE
e <- NA

Y en este caso, basta con llamar a la variable por su nombre para poder usarla.

a
[1] 5+7i
c
[1] 10
e
[1] NA

También se puede asignar un valor a una variable con el operador = pero esto no es nada recomendable ya que, por buenas prácticas, en este lenguaje, se acostumbra a usar el igual para dar valores a parámetros. ¿Qué pasará cuando se ejecute x <- NULL? ¿Qué pasará si antes x tenía otro tipo de dato?

Aquí se dejan otros ejemplos útiles, más adelante se verá la función assign().

roma <- c(0.1, 0.2, 0.3)

assign("mora", c(0.4, 0.5, 0.6))

c(0.7, 0.8, 0.9) -> z

mora <- c(roma, 0, 0, 0, roma)

N.B. Los vectores en R no solo pueden ser numéricos también los hay aquellos con cadenas de texto.

mora[3]
[1] 0.3
mora[-3]
[1] 0.1 0.2 0.0 0.0 0.0 0.1 0.2 0.3
mora[c(1,5,7)]
[1] 0.1 0.0 0.1

Algo que hay que entender sobre R es que este lenguaje esta basado en el paradigma de programación orientada a objetos, aunque también tiene ciertas características de un lenguaje funcional. Existen muchos sistemas para hacer todo lo que se tiene conocimiento de POO en R, como lo es el encapsulamiento, herencia y polimorfismo. Aquí se evitará en la mayor medida posible el tema pero se harán menciones en la marcha de esto.

Para el caso de los vectores, se tienen ciertos atributos fijos desde su creación: la longitud y el tipo de dato. Más adelante, en las estructuras de datos, se detallará sobre este tema; por el momento, véase lo siguiente

a <- c(1,2,3,4,5)
paste0("longitud: ", length(a), ".Tipo de dato: ", typeof(a))
[1] "longitud: 5.Tipo de dato: double"

Aclaremos ciertas cosas:

  • La manera más rápida de crear un vector es con el constructor c() y agregando datos del mismo tipo dentro de él separados con una coma.
    • Si v_1 es un vector de un cierto tipo y se desea hacer otro vector v_2 del mismo tipo que contenga el contenido de v_1 más otros elementos ¿Se puede hacer v_2 <- c(v_1,...)?
  • Otra forma de crear cualquier otro vector es con la función vector("type", length).
    • En cierto sentido, usar la última función es una mejor manera de trabajar ya que así se reserva un espacio de memoria para un vector fijo, mientras que con c() este puede ir variando.
    • Para crear un vector vacío de una cierta longitud, la función vector() es la ideal.
  • Se pueden crear vectores atómicos de cierta longitud con sus valores por defecto al usar los constructores double(length), interger(length), character(length), etc.
  • Arriba se creo una secuencia de números de manera rudimentaria, ya que en R se pueden crear secuencias de números con la función :, por ejemplo: 1:5.
  • Con seq_along() se puede crear también una secuencia de números; esta la podemos usar en lugar de 1:length(v) con v un vector.
    • ¿Qué pasa en las siguientes instrucciones? y <- vector("double", 0); seq_along(y); 1:length(y).
  • Finalmente, se pueden hacer secuencias cada \(n\) números con la función seq(): seq(1,10,by=2).

Ya que R es un lenguaje de código libre y orientado a objetos, podemos ver el contenido de sus funciones. Para las funciones que están escritas en R podemos ejecutar directamente el nombre de la función omitiendo los paréntesis.

integer
function (length = 0L) 
.Internal(vector("integer", length))
<bytecode: 0x7fda80bff4e0>
<environment: namespace:base>

¿Cómo están hechos los constructores para double, character, boolean y raw? ¿Cómo están hechos los constructores para un complejo y un factor?

Muchas veces es útil comparar distintos tipos de variables o verificar si estas son de algún tipo en específico. Para ello existen ciertas funciones como is.character(), is.na(), is.double(), etc. En general se tiene este tipo de funciones para cualquier objeto, incluso se tiene is.function(). En el caso de los posibles valores perdidos de un double, hay que evita usar == para verificar los posibles valores como Inf y –Inf. En su lugar usar las funciones is.finite(), is.infinite(), e is.nan().


3.2.4 Operaciones básicas en R

Es importante que existan operaciones en los lenguajes de programación, y en este caso se tienen las más comunes y algunas especiales.

  • Adición: 5 + 7; sum(5,7).
  • Sustracción: 5 - 7.
  • Multiplicación: 5*7; prod(5,7).
  • División: 5 / 7.
  • Modulo: 5 %% 7
  • División entera (piso): 5 %/% 7.
  • Potencia: 5 ^ 7; 5 ** 7.
  • Multiplicación Matricial: %*%

Al igual que los clásicos operadores booleanos o relacionales

  • TRUE & TRUE.
  • TRUE & FALSE.
  • TRUE | TRUE.
  • TRUE | FALSE.
  • !TRUE.
  • TRUE == FALSE.
  • TRUE != FALSE.

En resumen, tenemos la siguiente tabla

Aritméticos Comparación Lógicos
Suma (+) Menor que (<) NOT (!)
Resta (-) Mayor que (>) AND (&)
Multiplicación (*) Menor o igual que (<=) OR (|)
División (/) Mayor o igual que (>=) Cierto (TRUE)
Potencia (^) Igual (==) Falso (FALSE)
Modulo (%%) Diferente (!=)
División entera (%/%)

Ahora, lo poderoso de R es poder trabajar de manera vectorial, por lo que las operaciones actúan también de manera vectorial. Los siguientes ejemplos muestran el comportamiento asociado a un lenguaje vectorial que mencionamos:

v <- 2*roma+mora+1
v
[1] 1.3 1.6 1.9 1.2 1.4 1.6 1.3 1.6 1.9
1:5 + 2:6
[1]  3  5  7  9 11
1:5 * 2:6
[1]  2  6 12 20 30
  • ¿Qué pasará con las siguientes operaciones?
    • 1:5 ^ 1:5
    • (1:6) ^ c(2,3)
    • s <- seq(1,10, by = 2); s + 7
    • sqrt(s)
    • 1:10 + 1:5
    • 1:10 * 1:5
    • 1:10 * 10

Lo que sucedió arriba se conoce como reciclaje, lo cual tiene el efecto de repetir de manera ordenada los vectores más pequeños para que ambos vectores que se están operando tengan la misma longitud, esto resulta muy útil cuando se quiere repetir ciertos parámetros en una operación cada \(n\) cantidad de pasos. Por ejemplo, si se desea obtener el cuadrado de todos los pares de números del \(1\) al \(100\), no es necesario hacer algún proceso usando condicionales y algún bucle, sino sólo operar de manera vectorial y usar reciclaje.

seq(1,100)^c(1,2)
  [1]     1     4     3    16     5    36     7    64     9   100    11   144
 [13]    13   196    15   256    17   324    19   400    21   484    23   576
 [25]    25   676    27   784    29   900    31  1024    33  1156    35  1296
 [37]    37  1444    39  1600    41  1764    43  1936    45  2116    47  2304
 [49]    49  2500    51  2704    53  2916    55  3136    57  3364    59  3600
 [61]    61  3844    63  4096    65  4356    67  4624    69  4900    71  5184
 [73]    73  5476    75  5776    77  6084    79  6400    81  6724    83  7056
 [85]    85  7396    87  7744    89  8100    91  8464    93  8836    95  9216
 [97]    97  9604    99 10000

Hay que tener cuidado en algunos casos, por ejemplo 1:4^c(2,4).


Ejercicios

  1. El siguiente vector contiene una muestra referente al genero en un grupo de personas: c('H','M','M','H','H','M','H','M','H','H','M','M','M','M','M','M','M','M','H','H','H','M','H','M','M','M', 'M','H','H','H','H','M','M','M','H','M','H','H','M','H','M','H','M','H','M','M','H','H','M','H'). Mediante la función table() determina la frecuencia de cada categoría usando un factor.

  2. Convierte el siguiente vector de caracteres en un vector numérico y obtén la suma de todos sus elementos. c('7', '50', '7', '18', '42', '37', '20', '11', '33', '38', '28', '3', '16', '40', '3', '40', '36', '37', '1', '6', '8', '23', '25', '48', '5', '22', '21', '21', '1', '5', '8', '48', '34', '16', '4'). Guárdalo con el nombre v.

  3. La función table() también funciona con elementos numéricos, así que crea una tabla de frecuencias de v.

  4. Ordena el vector v de forma descendente.

  5. Se desea categorizar a v en 3 grupos (los menores a 4, los mayores o igual a 4 pero menores a 31 y los restantes), por lo que hay que hacer lo siguiente

  1. Crea distintos vectores que contengan sólo los elementos que caen dentro de cada categoría.
  2. Agregar nombres a los vectores de acuerdo al grupo que pertenecen. Pueden usar la función rep(). Los nombres serán el número de grupo (Uno, Dos y Tres).
  3. Une los anteriores vectores en un nuevo vector (puede ser un factor o no).
  4. Obtén una tabla de frecuencias para mostrar la cantidad de elementos por cada categoría.
  1. Obtén los nombres del vector que se obtuvo al final del punto 5 y guárdalo como una nueva variable. Crea un vector numérico con los datos 1, 2, 3 y con nombres “Uno”, “Dos” y “Tres”. Utiliza character subsetting para obtener un vector de 1, 2 y 3 con el vector de nombres que se menciono al inicio de este ejercicio.

  2. Obtén la raíz cuadrada de los primeros 100 números naturales.

  3. Crea un vector numérico y eleva cada elemento a la potencia determinada por la longitud de ese vector.

  4. Crea un vector numérico y eleva cada uno de sus elementos al cubo sin usar el operador ^ ni **.

  5. Con los datos del inciso 1 y con la función seq_along(), crea un vector con los datos antes mencionados pero que sus nombres sean una indexación de estos.

  6. Con los siguientes datos c('14', '1', '12', '27', '5', '38', '9', '29', '21', '19', '34', '18', '22', '1','26', '18','36', '28', '24','39','36', '21', '36', '28', '8', '13'), crea un vector booleano determinando que elementos son menores a 26 y con ese vector booleano concluye cuantos datos son menores a 26 y cuantos no lo son.

  7. Crea una lista con cinco elementos donde, al menos dos, deben ser listas que contengan listas a su vez.

  8. Se desea crear una lista de 10 elementos con las siguientes secuencias de números: c(1, 2, 3, ..., 10), c(11, 12, ..., 20), ..., c(91, 92, ..., 100). Además cada elemento debe tener nombre.

  9. En la lista anterior, cambia el vector c(40, ..., 50) por c(50, 49, ..., 40) sin escribir explícitamente ese vector.

  10. Agrega tu nombre como un atributo a cualquier vector anterior y a la lista del punto 13 y convierte la lista a un vector.