GRADE VALIDATOR, ERROR MINIMIZER, DATA CONSOLIDATOR

Vectores

Algunas funciones toman vectores como entrada y regresan un solo valor. Se dice que estas funciones “agregan” o “resumen” el vector de acuerdo a algún concepto. Ve estos ejemplos:

x <- 10:1
sum(x) # suma
## [1] 55
mean(x) # promedio
## [1] 5.5
indu <- c("Health Care", "Financials", "Info Tech", "Materials", "
Industrials")
length(indu) # conteo
## [1] 5

Otras funciones toman vectores como entrada y regresan un vector del mismo tamaño como salida. En este caso se aplica la función a cada elemento del vector. Aquí está un ejemplo:

sqrt(9)
## [1] 3
sqrt(1:10)
##  [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427
##  [9] 3.000000 3.162278

En el caso de operaciones con vectores, R busca hacer operaciones elemento por elemento.

ingresos <- c(100, 90, 100, 110, 105)
gastos <- c(80, 75, 90, 115, 85)
utilidad <- ingresos - gastos
utilidad
## [1] 20 15 10 -5 20

Cuando hacemos operaciones con vectores de diferentes tamaños, se recicla lo que se necesite del vector de más corto, para que el resultado sea del mismo tamaño que el vector más largo. En la práctica esto suele hacer sentido solo cuando uno de los vectores es de tamaño uno.

ingresos_MXN <- c(100, 90, 100, 110, 105)
tdc <- 20.5
ingresos_USD <- ingresos_MXN * tdc
ingresos_USD
## [1] 2050.0 1845.0 2050.0 2255.0 2152.5

Una desventaja del reciclado de vectores, es que cuando sucede R solo marca una advertencia (“warning”) y no un error. Aun así, mi recomendación es que como quiera se revise la causa de la advertencia. Aunque haya sido posible hacer la operación aritmética, no necesariamente hará sentido de negocio. Veamos el ejemplo siguiente:

ingresos <- c(100, 90, 100, 110, 105)
gastos <- c(80, 75, 90)
utilidad <- ingresos - gastos
## Warning in ingresos - gastos: longitud de objeto mayor no es múltiplo de la
## longitud de uno menor
## Warning in ingresos - gastos: longer object length is not a multiple of shorter
## object length
utilidad
## [1] 20 15 10 30 30

Recibimos una advertencia y no un error, por lo que el cálculo como quiera se pudo hacer. Sin embargo, es importante reconocer que el vector de gastos está incompleto, le faltan datos de dos tiendas y es un error simplemente reutilizar los valores de gastos de otras sin darnos cuenta. Aquí puedes ver como se utilizaron los valores de gastos[1] y gastos[2] para los cálculos de utilidad[1] y utilidad[2], y luego se reutilizaron para los cálculos de utilidad[4] y **utilidad[5].

Por tanto, las operaciones elemento por elemento suelen hacer sentido solo para operaciones entre:

a. Vectores del mismo tamaño ó

b. Un vector y un escalar (vector de tamaño 1), donde se reutiliza el escalar para la operación con cada posición del vector.

Esto no solo aplica para los operadores aritméticos, sino también para los lógicos y los relacionales.

Si utilizamos un vector booleano de la misma longitud que el vector que queremos filtrar, se desplegarán solo las posiciones que tengan el valor TRUE.

# Recuerda que T es lo mismo que TRUE y F lo mismo que FALSE
ingresos[c(T, F, T, F, T)]
## [1] 100 100 105

En la sección anterior hicimos subsetting de vectores “por posición”, pero también podemos hacer subsetting “por criterio”, para filtrar en base a una condición. Esto es mucho más práctico, dado que el vector boleano se genera en automático con la operación relacional que utilicemos.

Veamos este ejemplo donde aprovecharemos para hacer un filtrado de las tiendas:

tiendas <- c("A", "B", "C", "D", "E")
ingresos <- c(100, 90, 100, 110, 105)
altos <- ingresos > 100
altos
## [1] FALSE FALSE FALSE  TRUE  TRUE

En el bloque de código anterior generamos un vector boleano que nos indica las tiendas que tienen ingresos mayores a 100.

tiendas[altos]
## [1] "D" "E"

Filtramos las tiendas en las que altos == TRUE. Al utilizar un vector boleano para filtrar otro, por default se muestran los valores que son TRUE. Observa como las siguientes instrucciones dan el mismo resultado. De hecho, nos podemos saltar la parte de crear un vector boleano a parte y simplemente utilizar directamente la comparación que buscamos, como filtro.

tiendas[altos == TRUE]
## [1] "D" "E"
tiendas[ingresos > 100]
## [1] "D" "E"

También podemos filtrar lo opuesto. Las siguientes tres instrucciones dan el resultado contrario a las anteriores. Observa el uso del operador ! que significa NOT. De igual manera nos podemos evitar el crear un vector boleano independiente.

tiendas[!altos]
## [1] "A" "B" "C"
tiendas[altos == FALSE]
## [1] "A" "B" "C"
tiendas[ingresos <= 100]
## [1] "A" "B" "C"

En los ejemplos anteriores, hicimos todas las comparaciones contra un valor fijo (100) para determinar si los ingresos eran altos o no. En ese caso el valor 100 se recicló y no obtuvimos ningún mensaje de advertencia ya que es un caso de uso normal hacer una operación de vector con escalar (vector de tamaño 1).

El otro caso de uso normal es una operación con vectores del mismo tamaño. Así que ahora filtraremos comparando cada valor contra un valor correspondiente, operación elemento por elemento. Por ejemplo, supongamos que ahora queremos detectar las tiendas rentables (las que tienen ingresos mayores a gastos).

ingresos <- c(100, 90, 100, 110, 105)
gastos <- c(80, 75, 90, 115, 85)
rentables <- ingresos > gastos
rentables
## [1]  TRUE  TRUE  TRUE FALSE  TRUE

Generamos el vector boleano rentables, aunque como puedes ver a continuación, nos podemos saltar ese paso.

tiendas[rentables]
## [1] "A" "B" "C" "E"
tiendas[ingresos > gastos]
## [1] "A" "B" "C" "E"

Veamos ahora otro ejemplo donde combinamos operación de vectores elemento por elemento con operación de vector y escalar, donde se recicla el escalar.Supongamos que tenemos un vector de precios p de una acción, con datos para cinco días de la semana. Entonces el retorno del primer día se puede calcular de la siguiente manera:

p <- c(10, 9, 10, 11, 10)
p[2]/p[1] - 1
## [1] -0.1

Siguiendo una lógica similar, podemos calcular el retorno simple para todos los días en una sola operación, como en el caso del vector r abajo. Observa como realizamos una división entre vectores del mismo tamaño y luego le restamos 1 a todo el vector resultante.

\[ r=\left(p_1-p_0\right) / p_0=p_1 / p_0-1 \]

r <- p[2:5]/p[1:4] - 1

r
## [1] -0.10000000  0.11111111  0.10000000 -0.09090909

Ahora calculemos el retorno logarítmico:

\[ r=\ln \frac{P_1}{P_{0}}=\ln P_1-\ln P_{0} \]

# recuerda que en R, log es la función para calcular el logaritmo natural
log(p[2:5]/p[1:4])
## [1] -0.10536052  0.10536052  0.09531018 -0.09531018

Esta funcionalidad de poder operar con vectores, hace que algunos algoritmos sean más fáciles y rápidos de implementar, incluso es muy útil para analizar varios escenarios al mismo tiempo.

Veamos el ejemplo de un flujo de caja cf que queremos descontar n periodos en base a una tasa de interés i. Supongamos que tenemos los datos de cf y de n pero no tenemos certidumbre de la tasa. Podemos verificar varios escenarios:

cf <- 100
n <- 5
i <- seq(0.025, 0.10, by = 0.025)
pv <- cf/(1+i)^n
pv
## [1] 88.38543 78.35262 69.65586 62.09213

Dado que ya contamos con varios valores de tasa de interés y de valor presente, podemos incluso hacer una gráfica sencilla para ver esta relación inversa:

plot(x = i, y = pv, type = "l")

Data Frames y Matrices

Lo anterior también aplica para matrices y data frames, regresemos a nuestro ejemplo de la NFL.

qbs <- c("Mahomes", "Garoppolo", "Brady", "Rodgers", "Brees")
teams <- c("Chiefs", "49ers", "Patriots", "Packers", "Saints")
ages <- c(24, 28, 42, 36, 41)
nfl <- data.frame(qbs, teams, ages)
nfl$cities <- c("Kansas", "San Francisco", "New England",
"Green Bay", "New Orleans")

nfl
##         qbs    teams ages        cities
## 1   Mahomes   Chiefs   24        Kansas
## 2 Garoppolo    49ers   28 San Francisco
## 3     Brady Patriots   42   New England
## 4   Rodgers  Packers   36     Green Bay
## 5     Brees   Saints   41   New Orleans

Filtrando renglones que cumplen cierta condición:

nfl[nfl$teams == "Saints",]
##     qbs  teams ages      cities
## 5 Brees Saints   41 New Orleans

Hasta ahora hemos usado solo una condición para filtrar renglones, pero en el siguiente ejemplo utilizamos dos condiciones al mismo tiempo. Queremos los renglones que tengan edades mayores a 20 años Y menores a 40 años:

nfl[nfl$ages > 20 & nfl$ages < 40,]
##         qbs   teams ages        cities
## 1   Mahomes  Chiefs   24        Kansas
## 2 Garoppolo   49ers   28 San Francisco
## 4   Rodgers Packers   36     Green Bay

Como podemos ver en los ejemplos anteriores y en los que siguen: El filtrado de renglones por condiciones y la selección de columnas por nombres es más práctico y entendible que filtrar renglones y columnas por posición.

nfl[nfl$ages > 30 & nfl$ages < 40, c("qbs", "ages")]
##       qbs ages
## 4 Rodgers   36

Una ventaja de referenciar las celdas por nombre, es que las podemos utilizar como si fueran variables independientes fácilmente.

mean(nfl$ages)
## [1] 34.2
sum(nfl$ages)
## [1] 171
length(nfl$ages)
## [1] 5

¿Adivina cómo se calcularían el mínimo y el máximo de la variable edades? Inténtalo en RStudio. Hint: minimum and maximum.

Otro ejemplo con una función ifelse( ) similar al IF de Excel.

min(nfl$ages)
## [1] 24
max(nfl$ages)
## [1] 42
ifelse(ages < 25, "novato", "experto")
## [1] "novato"  "experto" "experto" "experto" "experto"

Dado que las columnas de los data frames siempre tienen nombre, todo esto aplica de manera natural para esa estructura de datos. En el caso de matrices, también se puede filtrar por nombre siempre y cuando se les haya puesto nombres específicos a las columnas. Lo que no funciona para matrices es el operador $.

m0 <- matrix(c(1:9), nrow = 3)
m0
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9
colnames(m0) <- c("A", "B", "C")
m0
##      A B C
## [1,] 1 4 7
## [2,] 2 5 8
## [3,] 3 6 9

En el caso de matrices, no funciona seleccionar la columna B utilizando m0$B, pero lo siguiente sí:

m0[, "B"]
## [1] 4 5 6

De igual manera se pueden filtrar renglones y seleccionar columnas al mismo tiempo:

m0[m0[,"B"] > 4, c("A","C")]
##      A C
## [1,] 2 8
## [2,] 3 9

Regresemos al dataset mtcars para ver otras funciones comunes.

data(mtcars)
head(mtcars)
##                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
## Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
## Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
## Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
## Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
## Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1

Supongamos que queremos calcular varias estadísticas de una columna específica. Podemos hacerlo de la siguiente manera:

mean(mtcars$mpg) # media
## [1] 20.09062
median(mtcars$mpg) # mediana
## [1] 19.2
var(mtcars$mpg) # varianza
## [1] 36.3241
sd(mtcars$mpg) # desviación estándar
## [1] 6.026948
min(mtcars$mpg) # valor máximo
## [1] 10.4
max(mtcars$mpg) # valor mínimo
## [1] 33.9

Es importante notar que el dataset mtcars no tiene NAs. Sin embargo, la presencia de un NA en la columna de interés imposibilita todos estos cálculos, a menos que utilicemos el argumento na.rm = TRUE para indicarle a estas funciones que ignore los renglones con NA. Le haremos un cambio manual a la base de datos para ejemplificar:

mtcars[1,1] <- NA

mean(mtcars$mpg)
## [1] NA
median(mtcars$mpg)
## [1] NA
var(mtcars$mpg)
## [1] NA
sd(mtcars$mpg)
## [1] NA
min(mtcars$mpg)
## [1] NA
max(mtcars$mpg)
## [1] NA
mtcars[1,1] <- NA

mean(mtcars$mpg, na.rm = T)
## [1] 20.06129
median(mtcars$mpg, na.rm = T)
## [1] 19.2
var(mtcars$mpg, na.rm = T)
## [1] 37.50645
sd(mtcars$mpg, na.rm = T)
## [1] 6.124251
min(mtcars$mpg, na.rm = T)
## [1] 10.4
max(mtcars$mpg, na.rm = T)
## [1] 33.9

Obviamente los valores cambiaron dado que le hicimos un cambio a los datos.

¿Qué pasa si quisiéramos calcular las mismas estadísticas, pero para toda la tabla mtcars? Desafortunadamente no funciona simplemente intentar mean(mtcars) o mean(mtcars, na.rm = T), dado que estas funciones solo operan con vectores, no con matrices. Lo que podemos hacer es replicar los mismos cálculos para cada columna, o utilizar la función apply( ).

La función apply( ) ocupa como primer argumento el objeto tabular, como segundo argumento el margen para los cálculos (renglones = 1, columnas = 2), como tercer argumento la función a ejecutar (ej. mean) y como cuarto y demás argumentos los argumentos que queramos especificar de la función a ejecutar (ej. na.rm = T).

apply(mtcars, 2, mean)
##        mpg        cyl       disp         hp       drat         wt       qsec 
##         NA   6.187500 230.721875 146.687500   3.596563   3.217250  17.848750 
##         vs         am       gear       carb 
##   0.437500   0.406250   3.687500   2.812500
apply(mtcars, 2, sd)
##         mpg         cyl        disp          hp        drat          wt 
##          NA   1.7859216 123.9386938  68.5628685   0.5346787   0.9784574 
##        qsec          vs          am        gear        carb 
##   1.7869432   0.5040161   0.4989909   0.7378041   1.6152000

Obtuvimos valores para todas las columnas menos mpg dado que le agregamos un NA a mpg. Tomemos eso en cuenta:

apply(mtcars, 2, mean, na.rm = T)
##        mpg        cyl       disp         hp       drat         wt       qsec 
##  20.061290   6.187500 230.721875 146.687500   3.596563   3.217250  17.848750 
##         vs         am       gear       carb 
##   0.437500   0.406250   3.687500   2.812500
apply(mtcars, 2, sd, na.rm = T)
##         mpg         cyl        disp          hp        drat          wt 
##   6.1242511   1.7859216 123.9386938  68.5628685   0.5346787   0.9784574 
##        qsec          vs          am        gear        carb 
##   1.7869432   0.5040161   0.4989909   0.7378041   1.6152000

De igual manera, también podemos calcular matrices de correlaciones y de covarianzas. Puedes ejecutar las siguientes funciones para las matrices completas (ej. cor(mtcars) y cov(mtcars)), pero aquí las mostramos con menos variables (haciendo un subsetting) por cuestión de espacio.

Matriz de correlación:

cor(mtcars[, c("mpg", "hp", "wt", "cyl")])
##     mpg        hp        wt       cyl
## mpg   1        NA        NA        NA
## hp   NA 1.0000000 0.6587479 0.8324475
## wt   NA 0.6587479 1.0000000 0.7824958
## cyl  NA 0.8324475 0.7824958 1.0000000

Matriz de covarianzas:

cov(mtcars[, c("mpg", "hp", "wt", "cyl")])
##     mpg         hp        wt        cyl
## mpg  NA         NA        NA         NA
## hp   NA 4700.86694 44.192661 101.931452
## wt   NA   44.19266  0.957379   1.367371
## cyl  NA  101.93145  1.367371   3.189516

Observa de nuevo como tenemos NAs en el primer renglón y la primera columna (mpg). En el caso de estas funciones, el argumento que nos permite definir cómo podemos manejar los NAs es use. Te recomiendo leer la documentación de ?cor para ver las diferentes opciones. Este autor recomienda utilizar use = complete.obs o use = pairwise.complete.obs para ignorar NAs. complete.obs ignora para todos los cálculos cualquier renglón que contenga NAs mientras que pairwise.complete.obs solo ignora los renglones que contengan NAs si son de las columnas entre las cuales se está haciendo el cálculo.

cor(mtcars[, c("mpg", "hp", "wt", "cyl")], use = "complete.obs")
##            mpg         hp         wt        cyl
## mpg  1.0000000 -0.7774885 -0.8703363 -0.8521139
## hp  -0.7774885  1.0000000  0.6550588  0.8347180
## wt  -0.8703363  0.6550588  1.0000000  0.7853924
## cyl -0.8521139  0.8347180  0.7853924  1.0000000
cov(mtcars[, c("mpg", "hp", "wt", "cyl")], use = "complete.obs")
##             mpg        hp         wt        cyl
## mpg   37.506452 -330.2752 -5.2685527  -9.472258
## hp  -330.275161 4811.2495 44.9118022 105.092473
## wt    -5.268553   44.9118  0.9770178   1.409097
## cyl   -9.472258  105.0925  1.4090968   3.294624

El paquete corrplot es uno muy utilizado para mostrar matrices de correlaciones más visuales:

library(corrplot)
## corrplot 0.92 loaded
corrplot(cor(mtcars[, c("mpg", "hp", "wt", "cyl")], use = "complete.obs"),
 method = "color")

Álgebra matricial

Una ventaja de R sobre algunos otros lenguajes es que R base es capaz de realizar álgebra matricial sin problemas y sin necesidad de agregar paquetes externos.

Recordemos que en el caso de multiplicación de matrices el orden de los factores sí altera el producto y las dimensiones de las matrices involucradas dictaminan tanto las dimensiones del resultado como si es posible la operación en primer lugar.


1. Si multiplicamos A ∗ B = C:

c. Requisito: Cantidad de columnas de A = Cantidad de renglones de B

d. Renglones de C = Cantidad de renglones de A

e. Columnas de C = Cantidad de columnas de B

2. Si multiplicamos B ∗ A = D:

f. Requisito: Cantidad de columnas de B = Cantidad de renglones de A

g. Renglones de D = Cantidad de renglones de B

h. Columnas de D = Cantidad de columnas de A


Dado que el propósito de este curso no es enseñar álgebra matricial, sino solo demostrar que es posible en R, veremos solo algunos ejemplos para diferenciar álgebra matricial de las operaciones elemento por elemento.

r es una matriz de 5 renglones y 3 columnas. Supongamos que las columnas representan acciones y que los renglones representan retornos diarios de 5 días

r <- matrix(runif(15), 5, 3)
r
##           [,1]       [,2]      [,3]
## [1,] 0.8670258 0.69444787 0.1588098
## [2,] 0.2010973 0.63495154 0.3511251
## [3,] 0.1038993 0.83123855 0.8480530
## [4,] 0.2605629 0.31332815 0.5607126
## [5,] 0.2113934 0.01392533 0.2551474

w es una matriz de 3 renglones y 1 columna. Supongamos que es el porcentaje que tenemos invertido en cada una de las acciones de r

w <- matrix(c(0.30, 0.40, 0.30), 3, 1)
w
##      [,1]
## [1,]  0.3
## [2,]  0.4
## [3,]  0.3

El operador de multiplicación matricial (también conocida como producto interno o producto punto) en R es **%*%. Así podemos calcular el retorno diario de un portafolio que tiene acciones con retornos r y pesos w**

r %*% w
##           [,1]
## [1,] 0.5855298
## [2,] 0.4196474
## [3,] 0.6180811
## [4,] 0.3717139
## [5,] 0.1455324

Poder utilizar multiplicación matricial con esta facilidad permite replicar las ecuaciones de diferentes métodos econométricos fácilmente. Tres operaciones matriciales muy utilizadas también son:

m1 <- matrix(c(2,1,-2,3), 2, 2)
m1
##      [,1] [,2]
## [1,]    2   -2
## [2,]    1    3

La transpuesta de una matriz, con t( )

t(m1)
##      [,1] [,2]
## [1,]    2    1
## [2,]   -2    3

Compara m1 con t(m1), observa como “se gira” la matriz 90º, resultando en un intercambio de renglones y columnas. La inversa de una matriz, con solve( )

solve(m1)
##        [,1] [,2]
## [1,]  0.375 0.25
## [2,] -0.125 0.25

Una matriz por su inversa da la matriz identidad I. Una matriz identidad tiene solo 1’s en la diagonal y 0’s fuera de la diagonal.

m1 %*% solve(m1)
##      [,1] [,2]
## [1,]    1    0
## [2,]    0    1

La descomposición de Cholesky:

chol(m1)
##          [,1]      [,2]
## [1,] 1.414214 -1.414214
## [2,] 0.000000  1.000000

No todas las matrices tienen inversa ni descomposición de Cholesky, pero esas reglas son para temas específicos de álgebra matricial que no cubre este curso. Simplemente mostramos las funciones disponibles y realizamos ya algunas comprobaciones.

En resumen: - Multiplicación elemento por elemento se realiza con - Multiplicación matricial se realiza con ”%%”

Otros tipos de datos: Fechas y Factores

Fechas

Hay más tipos de datos además de los básicos que ya vimos (numéricos, caracteres, boleanos), hay todavía un par de tipos de datos adicionales con los que también nos toparemos conforme leemos datos externos o conforme realzamos modelos. Veamos un tipo de dato que parece una fecha:

mi_fecha <- "2020-12-31"
mi_fecha
## [1] "2020-12-31"
class(mi_fecha)
## [1] "character"

Observa cómo se grabó el dato en el ambiente global. Aunque nosotros podemos saber que ese dato es una fecha, para R es un texto simplemente. Si intentas ejecutar el siguiente código, arrojará error:

#mi_fecha + 1

Las fechas tienden a ser identificadas por muchas funciones como texto, sobre todo al leerlas de fuentes de datos externas. Por tanto, suele ser necesario transformarlas usando la función as.Date( ), similar a las funciones de conversión que vimos para los tipos de datos básicos.

R usa por default el estándar ISO de yyyy-mm-dd, es decir: cuatro dígitos para año (“yyyy”), dos para mes (“mm”) y dos para día (“dd”). Por tanto, as.Date( ) considera por default que los datos están en alguno de estos dos formatos “%Y-%m-%d” o “%Y/%m/%d”.

En caso de que los datos de fecha tengan un formato diferente al default, lo cual es muy común (diferentes regiones geográficas siguen diferentes convenciones), es necesario especificarlo en la función as.Date( ) usando el argumento format =. POSIxct es un estándar de fecha y tiempo compatible en varias plataformas y el argumento format = sigue las convenciones de ese estándar. Para mayor información pueden revisar la documentación de ese estándar en ?strptime.

Dado que la fecha en mi_fecha ya tiene el formato default, no es necesario especificar el argumento format = en el código abajo, pero se incluye como ejemplo de uso

mi_fecha <- as.Date(mi_fecha, format = "%Y-%m-%d")
class(mi_fecha)
## [1] "Date"
mi_fecha
## [1] "2020-12-31"
mi_fecha + 1
## [1] "2021-01-01"

Puedes ver que ahora mi_fecha se ve un poco diferente en el Global Environment, ya no como texto y además la función class( ) nos indica que es del tipo “Date”. Adicional a esto, ya puedes hacer sumas y restas entre fechas (por ejemplo, para contar días) o entre fechas y números, donde la unidad que se suma o resta son “días”. Estas operaciones aritméticas son posibles dado que en el fondo las fechas son números en R, pero con etiquetas que vemos como fechas.

as.numeric(mi_fecha)
## [1] 18627

En casi todos los lenguajes hay alguna fecha que se determina como el inicio o el 0 y de ahí hacia adelante las fechas utilizan números positivos mientras que hacia atrás ocupan números negativos. En el caso de R, la fecha del 0 es el 1ero de enero de 1970.

as.numeric(as.Date("1970-01-01"))
## [1] 0
as.Date("2020-12-31") - as.Date("1970-01-01")
## Time difference of 18627 days

Algunas funciones que ya conocemos también pueden tomar fechas como entrada. Por ejemplo, la función seq( ) que usamos anteriormente para hacer secuencias numéricas.

seq(as.Date("2019-01-01"), as.Date("2020-12-31"), by = "quarter")
## [1] "2019-01-01" "2019-04-01" "2019-07-01" "2019-10-01" "2020-01-01"
## [6] "2020-04-01" "2020-07-01" "2020-10-01"

También podemos leer la fecha y el tiempo del sistema operativo.

hoy <- Sys.Date()
class(hoy)
## [1] "Date"

En el caso del tiempo, la unidad de medida son los segundos y de igual manera se cuentan desde el 1er de enero de 1970.

ahora <- Sys.time()
ahora
## [1] "2022-10-23 01:00:45 -05"
class(ahora)
## [1] "POSIXct" "POSIXt"
as.numeric(ahora)
## [1] 1666504845
ahora + 1
## [1] "2022-10-23 01:00:46 -05"

Factores

Para algunas funciones que ejecutan modelos econométricos, es conveniente convertir variables categóricas y ordinales a factores. Similar a los datos de fecha, los factores terminan siendo números con etiquetas representando a cada una de las categorías de esa variable.

Dichas variables pueden tener un orden natural (ordinales) o no (categóricas), por tanto, los factores pueden ser “ordered factor” o “unordered factor”.

Ejemplo de “unordered factor”:

gender <- c("Female", "Male", "Male", "Female", "Female", "Female", "Male")
genderf <- factor(gender)
genderf
## [1] Female Male   Male   Female Female Female Male  
## Levels: Female Male

Observa que al desplegar la variable no aparecen las comillas y además aparecen los niveles (“levels”) listados abajo. Por niveles nos referimos a las diferentes categorías de dicha variable categórica.

as.numeric(genderf)
## [1] 1 2 2 1 1 1 2

Puedes observar que, en el fondo, los factores son números, con el primer nivel teniendo el valor 1. Si no especificamos un orden de los niveles, R los tomará en orden alfabético.

Ejemplo de “ordered factor”:

temps <- c("High", "High", "Med", "High", "Med", "Low", "Med")
tempsf <- factor(temps, ordered = T, levels = c("Low", "Med", "High"))
tempsf
## [1] High High Med  High Med  Low  Med 
## Levels: Low < Med < High

Observa cómo para indicar que los niveles del factor tienen un orden natural es necesario utilizar el argumento ordered = TRUE y además especificar el orden que llevan las categorías con el argumento levels =.

as.numeric(tempsf)
## [1] 3 3 2 3 2 1 2

Conversión de tipo de dato de factor a texto:

tempst <- as.character(tempsf)
tempst
## [1] "High" "High" "Med"  "High" "Med"  "Low"  "Med"

Cuidado: Siempre que se quiera convertir de un tipo de dato factor al tipo de dato original, es necesario convertir siempre primero a texto. Después de ese paso, si el tipo de dato original era numérico podemos convertir el texto a número. Esto dado que si convertimos a número directamente obtendremos el valor numérico del nivel factor (el que utiliza R “tras bambalinas”), no necesariamente el valor numérico que vemos en sus etiquetas de texto.

Observa como en el siguiente ejemplo num.numeric no es igual a num.orig, pero num.text sí.

num.orig <- c(10, 9, 8, 11, 10, 8)
num.factor <- factor(num.orig)
num.factor
## [1] 10 9  8  11 10 8 
## Levels: 8 9 10 11
num.numeric <- as.numeric(num.factor)
num.numeric
## [1] 3 2 1 4 3 1
num.text <- as.numeric(as.character(num.factor))
num.text
## [1] 10  9  8 11 10  8

Lectura de archivos externos

Existen varias funciones para leer archivos externos. Típicamente las funciones varían por tipo de archivo y funciones más nuevas pueden haber surgido por cuestiones de eficiencia.

Funciones recomendadas para tipos de archivos comunes:

i. Para archivos de texto en general: read.table()

j. Para archivos separados por comas (comma-separated values): read.csv()

k. Del paquete openxlsx: read.xlsx()

l. Del paquete readr en tidyverse: read_csv()

m. Del paquete readxl en tidyverse: read_excel()

Las primeras dos funciones son de R base mientras que las demás son de paquetes instalables. Otra forma de especificar funciones que no son de los paquetes base de R, es precediéndolas con el nombre del paquete. Ej.: openxlsx::read.xlsx() y readxl::read_excel(). De hecho, esta notación puede servir para asegurarnos de llamar la función correcta, cuando instalamos paquetes con funciones que se llaman igual (esto casi no debe pasar, pero sí hay algunos casos).

Adicional a seleccionar la función adecuada para el tipo de archivo que queremos leer, es necesario especificar en los argumentos de la función tanto el nombre del archivo como la ruta o ubicación del mismo. Hay varias maneras de hacer esto:

a) Averiguar el directorio de trabajo o “working directory” de RStudio y colocar los archivos que queramos leer ahí. De esta manera, no es necesario especificar la ruta sino solo el nombre del archivo. Para esto sirve la función getwd().

b) Cambiar el working directory a donde hayamos guardado los archivos que queremos leer. Para esto sirve la función setwd(). Pero también se puede hacer desde la interface, ya sea en el panel multipropósito (panel inferior derecho) en la pestaña de “Files” o dando clic en el menú superior de RStudio “Session -> Set Working Directory -> Choose Directory”.

c) Especificar la ruta completa del archivo junto con su nombre. Es importante resaltar que el símbolo “” ya tiene otro uso en programación (“escape character”), por lo que típicamente las rutas se especifican con “/” aunque algunos sistemas operativos como Windows las muestren con “”. Ejemplo: read.csv(”c:/Mis_Documentos/R/archivo.csv”)

Para los ejemplos a continuación sugiero ya sea las alternativas “a” o “b” descritas arriba, en resumen:

a. Grabar los archivos que descargues en el working directory ó

b. Especificar el folder donde ubiques los archivos con “Session -> Set Working Directory -> Choose Directory”.

Si deseas utilizar la alternativa “c”, tendrías que actualizar los códigos mostrados para incluir la ubicación de los archivos.

Descarga el archivo US_Stocks.zip y extrae los 42 archivos en el folder de tu preferencia, ya sea el working directory que identifiques con getwd() o el que selecciones tú como ya explicamos. Solo si sigues estos pasos funcionarán los siguientes códigos.

tsla <- read.csv("TSLA.csv")
head(tsla)
##         Date   Open   High    Low  Close Adj.Close  Volume
## 1 2017-12-29 316.18 316.41 310.00 311.35    311.35 3777200
## 2 2018-01-02 312.00 322.11 311.00 320.53    320.53 4352200
## 3 2018-01-03 321.00 325.25 315.55 317.25    317.25 4521500
## 4 2018-01-04 312.87 318.55 305.68 314.62    314.62 9946300
## 5 2018-01-05 316.62 317.24 312.00 316.58    316.58 4591200
## 6 2018-01-08 316.00 337.02 315.50 336.41    336.41 9859400

En este caso, la función read.csv( ) funcionó sin que tuviéramos que especificar argumentos adicionales. Sin embargo, pudiera ser necesario ajustar algunos parámetros si el archivo no sigue las convenciones usuales. Puedes ver en la documentación de ?read.csv que esta función tiene muchos argumentos opcionales. Dos de los más usados son:

a. skip = cuando la información del archivo no empieza en el primer renglón, podemos especificar cuántos renglones nos queremos saltar.

b. header = sirve para especificar si las columnas tienen nombre o no.

Utilizando los valores default de esos parámetros, la siguiente instrucción hace lo mismo que la anterior:

tsla <- read.csv("TSLA.csv", skip = 0, header = T)
head(tsla)
##         Date   Open   High    Low  Close Adj.Close  Volume
## 1 2017-12-29 316.18 316.41 310.00 311.35    311.35 3777200
## 2 2018-01-02 312.00 322.11 311.00 320.53    320.53 4352200
## 3 2018-01-03 321.00 325.25 315.55 317.25    317.25 4521500
## 4 2018-01-04 312.87 318.55 305.68 314.62    314.62 9946300
## 5 2018-01-05 316.62 317.24 312.00 316.58    316.58 4591200
## 6 2018-01-08 316.00 337.02 315.50 336.41    336.41 9859400

Recordando otras funciones para explorar el data frame:

summary(tsla)
##      Date                Open            High            Low       
##  Length:314         Min.   :252.8   Min.   :260.3   Min.   :244.6  
##  Class :character   1st Qu.:293.1   1st Qu.:298.6   1st Qu.:288.0  
##  Mode  :character   Median :312.9   Median :318.5   Median :306.0  
##                     Mean   :313.9   Mean   :320.1   Mean   :307.5  
##                     3rd Qu.:337.9   3rd Qu.:345.8   3rd Qu.:331.9  
##                     Max.   :375.0   Max.   :387.5   Max.   :367.1  
##      Close         Adj.Close         Volume        
##  Min.   :250.6   Min.   :250.6   Min.   : 3080700  
##  1st Qu.:292.3   1st Qu.:292.3   1st Qu.: 5632475  
##  Median :312.5   Median :312.5   Median : 7203800  
##  Mean   :314.1   Mean   :314.1   Mean   : 8571240  
##  3rd Qu.:337.9   3rd Qu.:337.9   3rd Qu.: 9507275  
##  Max.   :379.6   Max.   :379.6   Max.   :33649700
str(tsla)
## 'data.frame':    314 obs. of  7 variables:
##  $ Date     : chr  "2017-12-29" "2018-01-02" "2018-01-03" "2018-01-04" ...
##  $ Open     : num  316 312 321 313 317 ...
##  $ High     : num  316 322 325 319 317 ...
##  $ Low      : num  310 311 316 306 312 ...
##  $ Close    : num  311 321 317 315 317 ...
##  $ Adj.Close: num  311 321 317 315 317 ...
##  $ Volume   : int  3777200 4352200 4521500 9946300 4591200 9859400 7146600 4309900 6645500 4825100 ...

Como puedes observar en base a las funciones summary() y str(), la columna “Date” tiene formato de texto en vez de fecha. Si no hacemos esta corrección, otras funciones no harán lo esperado, por ejemplo, la función plot no le dará el formato idóneo al eje que use la fecha.

tsla$Date <- as.Date(tsla$Date, format = "%Y-%m-%d")
str(tsla)
## 'data.frame':    314 obs. of  7 variables:
##  $ Date     : Date, format: "2017-12-29" "2018-01-02" ...
##  $ Open     : num  316 312 321 313 317 ...
##  $ High     : num  316 322 325 319 317 ...
##  $ Low      : num  310 311 316 306 312 ...
##  $ Close    : num  311 321 317 315 317 ...
##  $ Adj.Close: num  311 321 317 315 317 ...
##  $ Volume   : int  3777200 4352200 4521500 9946300 4591200 9859400 7146600 4309900 6645500 4825100 ...
summary(tsla)
##       Date                 Open            High            Low       
##  Min.   :2017-12-29   Min.   :252.8   Min.   :260.3   Min.   :244.6  
##  1st Qu.:2018-04-24   1st Qu.:293.1   1st Qu.:298.6   1st Qu.:288.0  
##  Median :2018-08-14   Median :312.9   Median :318.5   Median :306.0  
##  Mean   :2018-08-15   Mean   :313.9   Mean   :320.1   Mean   :307.5  
##  3rd Qu.:2018-12-05   3rd Qu.:337.9   3rd Qu.:345.8   3rd Qu.:331.9  
##  Max.   :2019-04-01   Max.   :375.0   Max.   :387.5   Max.   :367.1  
##      Close         Adj.Close         Volume        
##  Min.   :250.6   Min.   :250.6   Min.   : 3080700  
##  1st Qu.:292.3   1st Qu.:292.3   1st Qu.: 5632475  
##  Median :312.5   Median :312.5   Median : 7203800  
##  Mean   :314.1   Mean   :314.1   Mean   : 8571240  
##  3rd Qu.:337.9   3rd Qu.:337.9   3rd Qu.: 9507275  
##  Max.   :379.6   Max.   :379.6   Max.   :33649700

Haremos a continuación algunas gráficas de R base para seguir explorando los datos:

plot(x = tsla$Date, y = tsla$Adj.Close, type = "l", col = "red")

barplot(tsla$Adj.Close)

hist(tsla$Adj.Close)

boxplot(tsla$Adj.Close)

Otra función muy utilizada, que también forma parte del universo tidyverse es ggplot2::ggplot(). Esta función puede generar una mayor variedad de gráficas que R base y con mayor personalización. Sin embargo, puede hacerse un curso completo al respecto, por lo que solo pondré el siguiente ejemplo.

library(ggplot2)
ggplot(data = tsla, aes(x = Date, y = Adj.Close)) + geom_line(color = "red")

ggplot(data = tsla, aes(x = Date, y = Adj.Close)) + geom_col()

ggplot(data = tsla, aes(x = Adj.Close)) + geom_histogram(color = "orange", fill = "gold")
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

ggplot(data = tsla, aes(y = Adj.Close)) + geom_boxplot(color = "blue", fill = "lightblue")

Hagamos el cálculo de unas columnas adicionales, para propósitos de ejemplificar otras funciones.

Cálculo de retornos logarítmicos

En el caso de acciones, es necesario tomar en cuenta los dividendos que estas pagan como parte de su retorno, y no solo los cambios de precio. Por esta razón, calcularemos los retornos con la columna Adj.Close dado que esta muestra los precios de cierre ajustados ya por dividendos pagados.

\[ logreturn{_i}=\ln (\frac{adj.close_i}{adj.close_{i-1}}) \]

Para esto ocuparemos dos funciones anidadas: log() que nos calcula logaritmos naturales y lag() que nos calcula rezagos de una variable.

Por cierto, lag() es una de las funciones que suele tener conflictos con funciones similares de otros paquetes, por lo que recomiendo que su uso sea: dplyr::lag()para especificar la que realmente queremos usar (la del paquete dplyr).

Veamos primero el cálculo del rezago de los precios, creando una columna nueva dentro de tsla que nos permita verlos:

tsla$LaggedAdj.Close <- dplyr::lag(tsla$Adj.Close)
head(tsla)
##         Date   Open   High    Low  Close Adj.Close  Volume LaggedAdj.Close
## 1 2017-12-29 316.18 316.41 310.00 311.35    311.35 3777200              NA
## 2 2018-01-02 312.00 322.11 311.00 320.53    320.53 4352200          311.35
## 3 2018-01-03 321.00 325.25 315.55 317.25    317.25 4521500          320.53
## 4 2018-01-04 312.87 318.55 305.68 314.62    314.62 9946300          317.25
## 5 2018-01-05 316.62 317.24 312.00 316.58    316.58 4591200          314.62
## 6 2018-01-08 316.00 337.02 315.50 336.41    336.41 9859400          316.58

Ahora haremos ese cálculo dentro de la función log() y nos ahorramos crear esa columna de rezagos:

tsla$LogReturns <- log(tsla$Adj.Close/dplyr::lag(tsla$Adj.Close))
head(tsla)
##         Date   Open   High    Low  Close Adj.Close  Volume LaggedAdj.Close
## 1 2017-12-29 316.18 316.41 310.00 311.35    311.35 3777200              NA
## 2 2018-01-02 312.00 322.11 311.00 320.53    320.53 4352200          311.35
## 3 2018-01-03 321.00 325.25 315.55 317.25    317.25 4521500          320.53
## 4 2018-01-04 312.87 318.55 305.68 314.62    314.62 9946300          317.25
## 5 2018-01-05 316.62 317.24 312.00 316.58    316.58 4591200          314.62
## 6 2018-01-08 316.00 337.02 315.50 336.41    336.41 9859400          316.58
##     LogReturns
## 1           NA
## 2  0.029058172
## 3 -0.010285766
## 4 -0.008324561
## 5  0.006210388
## 6  0.060754733

Ahora también crearemos una columna con el “Año-Mes” por si queremos hacer cálculos mensuales en vez de diarios.

tsla$yrMonth <- substr(tsla$Date, 1, 7)
head(tsla)
##         Date   Open   High    Low  Close Adj.Close  Volume LaggedAdj.Close
## 1 2017-12-29 316.18 316.41 310.00 311.35    311.35 3777200              NA
## 2 2018-01-02 312.00 322.11 311.00 320.53    320.53 4352200          311.35
## 3 2018-01-03 321.00 325.25 315.55 317.25    317.25 4521500          320.53
## 4 2018-01-04 312.87 318.55 305.68 314.62    314.62 9946300          317.25
## 5 2018-01-05 316.62 317.24 312.00 316.58    316.58 4591200          314.62
## 6 2018-01-08 316.00 337.02 315.50 336.41    336.41 9859400          316.58
##     LogReturns yrMonth
## 1           NA 2017-12
## 2  0.029058172 2018-01
## 3 -0.010285766 2018-01
## 4 -0.008324561 2018-01
## 5  0.006210388 2018-01
## 6  0.060754733 2018-01

En el tema anterior vimos como filtrar renglones y seleccionar columnas utilizando la sintaxis de R base. Sin embargo, el paquete dplyr dentro de tidyverse nos provee con otras funciones que pudieran ser más amigables, sobre todo al combinarlas con el operador “pipe” (%>%) que vimos anteriormente. Veamos algunos ejemplos:

a. filter() permite filtrar renglones en base a una o más condiciones.

b. select() permite seleccionar columnas.

c. group_by() permite agrupar la tabla en base a los valores de una columna

d. summarize() permite hacer cálculos a nivel agrupado

Mostrando los renglones con volumen extremo:

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
tsla %>% filter(Volume <= 4000000 | Volume >= 30000000) %>%
 select(Date, Adj.Close, Volume)
##          Date Adj.Close   Volume
## 1  2017-12-29    311.35  3777200
## 2  2018-02-02    343.75  3704800
## 3  2018-02-14    322.31  3950700
## 4  2018-02-21    333.30  3219600
## 5  2018-03-05    333.35  3823800
## 6  2018-03-08    329.10  3566200
## 7  2018-05-25    278.85  3875100
## 8  2018-08-07    379.57 30875800
## 9  2018-08-24    322.82  3602600
## 10 2018-09-28    264.77 33649700
## 11 2018-11-29    341.17  3080700
## 12 2019-01-17    347.31  3676700
## 13 2019-02-15    307.88  3904900

Mostrando un rango de fechas:

tsla %>% filter(Date >= "2018-12-31" & Date <= "2019-01-10") %>%
 select(Date, Adj.Close, Volume)
##         Date Adj.Close   Volume
## 1 2018-12-31    332.80  6302300
## 2 2019-01-02    310.12 11658600
## 3 2019-01-03    300.36  6965200
## 4 2019-01-04    317.69  7394100
## 5 2019-01-07    334.96  7551200
## 6 2019-01-08    335.35  7008500
## 7 2019-01-09    338.53  5432900
## 8 2019-01-10    344.97  6056400

Agrupando y resumiendo por grupos:

tsla %>% group_by(yrMonth) %>%
 select(Date, Adj.Close, Volume) %>%
 summarize(obs = n(), meanAdjClose = mean(Adj.Close), meanVolume = mean(Volume))
## Adding missing grouping variables: `yrMonth`
## # A tibble: 17 × 4
##    yrMonth   obs meanAdjClose meanVolume
##    <chr>   <int>        <dbl>      <dbl>
##  1 2017-12     1         311.   3777200 
##  2 2018-01    21         339.   5916748.
##  3 2018-02    19         336.   5746842.
##  4 2018-03    21         316.   7488976.
##  5 2018-04    21         290.   9062419.
##  6 2018-05    22         290.   7070277.
##  7 2018-06    21         336.  10163100 
##  8 2018-07    21         312.   8206105.
##  9 2018-08    23         331.  12055061.
## 10 2018-09    19         290.  10319758.
## 11 2018-10    23         285.  12450465.
## 12 2018-11    21         344.   6334243.
## 13 2018-12    19         344.   7707958.
## 14 2019-01    21         318.   8364386.
## 15 2019-02    19         308.   6765795.
## 16 2019-03    21         278.  10180643.
## 17 2019-04     1         289.   8103300

Al agrupar, la función n() nos arroja el tamaño de cada grupo, en este caso la cantidad de observaciones que tenemos para cada mes-año.

Este tipo de archivos con las columnas de precios diarios “Open” (apertura), “High” (máximo), “Low” (mínimo), “Close” (cierre) son conocidos como archivos “OHLC” o “OHLCV” si se incluye la columna de “Volume” (volumen de transacciones).

Si queremos actualizar los nombres de la tabla:

names(tsla)
##  [1] "Date"            "Open"            "High"            "Low"            
##  [5] "Close"           "Adj.Close"       "Volume"          "LaggedAdj.Close"
##  [9] "LogReturns"      "yrMonth"
names(tsla) <- c("Date", "O", "H", "L", "C", "Adj.Close", "V")

head(tsla)
##         Date      O      H      L      C Adj.Close       V     NA           NA
## 1 2017-12-29 316.18 316.41 310.00 311.35    311.35 3777200     NA           NA
## 2 2018-01-02 312.00 322.11 311.00 320.53    320.53 4352200 311.35  0.029058172
## 3 2018-01-03 321.00 325.25 315.55 317.25    317.25 4521500 320.53 -0.010285766
## 4 2018-01-04 312.87 318.55 305.68 314.62    314.62 9946300 317.25 -0.008324561
## 5 2018-01-05 316.62 317.24 312.00 316.58    316.58 4591200 314.62  0.006210388
## 6 2018-01-08 316.00 337.02 315.50 336.41    336.41 9859400 316.58  0.060754733
##        NA
## 1 2017-12
## 2 2018-01
## 3 2018-01
## 4 2018-01
## 5 2018-01
## 6 2018-01

Si queremos quedarnos solo con algunas columnas, podemos seleccionarlas y sobrescribir el objeto. Las instrucciones abajo hacen exactamente lo mismo:

tsla <- tsla[, c("Date", "Adj.Close")]

tsla <- tsla %>% select("Date", "Adj.Close")

En el caso de archivos con muchas columnas, puede ser útil seleccionar solo las columnas que queremos desde el momento de leerlos. Aquí un ejemplo, observa como solo leemos del archivo externo las columnas de interés:

tsla <- read.csv("TSLA.csv")[, c("Date", "Adj.Close")]

Ahora visualicemos los datos en una gráfica de línea, como serie de tiempo. Nota que para que el eje de tiempo sea correcto, es necesario asegurarnos que la columna Date es de fecha y no de texto. Aunque ya habíamos hecho la conversión más arriba, volvimos a leer los datos después de eso.

tsla$Date <- as.Date(tsla$Date)
plot(x = tsla$Date, y = tsla$Adj.Close, 
 xlab = "Fecha", ylab = "Precio ajustado de cierre",
 type = "l", col = "red")

Consolidación de tablas

La consolidación de información de diferentes fuentes en alguna tabla, suele ser un buen reto. Por un lado, se ocupa algún índice único (“key”) que tenga la misma información en ambas tablas que se unirán, para poder identificar la información que corresponde al mismo renglón.

La función cbind() que nos sirve para pegar una columna a una tabla e incluso nos puede servir para pegar dos tablas, sin embargo esta función no es “inteligente”, es decir, no hace ninguna verificación por nosotros. Simplemente asume que la información de ambas tablas ya está en el mismo orden, que cada renglón corresponde uno a uno, y si las tablas tienen la misma cantidad de renglones las pegará. Esto sirve para un número muy limitado de casos, por tanto, la mejor práctica es utilizar funciones diferentes para esto.

Veremos la función base merge() dado que no requiere paquetes adicionales y luego mencionaremos algunas alternativas del paquete tidyverse dado que también es muy utilizado.

Supongamos que queremos tener una tabla con puros precios de diarios, donde la primera columna sea la fecha y todas las demás columnas correspondan precios de cierre de diferentes acciones.

Leamos primero algunos archivos con precios de cierre que quisiéramos consolidar en una tabla. Una vez hecho esto, podemos continuar leyendo archivos de otras acciones e ir agregando sus precios de cierre a esta misma tabla.

En este ejemplo tendremos ya los nombres de los archivos en un vector, y los tomaremos de ahí para leerlos.


OJO: El cambio de Working Directory** en R Studio solo es válido para la sesión en la que estás trabajando. Si sales de R o reinicias sesión, es necesario volver a definir el Working Directory. De igual manera, en el caso de Google Colab es necesario volver a subir los archivos al reiniciar la sesión.**


Leeremos el primer archivo como base y a ese le “pegaremos” la información de los que leamos después, utilizando la función merge().

rm(list = ls()) # para limpiar los datos del ambiente global

archivos <- c("MCD.csv", "TSLA.csv", "WMT.csv")

consolidado <- read.csv(archivos[1])[, c("Date", "Adj.Close")]

head(consolidado)
##         Date Adj.Close
## 1 2017-12-29  166.8067
## 2 2018-01-02  167.8727
## 3 2018-01-03  167.1653
## 4 2018-01-04  168.3379
## 5 2018-01-05  168.6771
## 6 2018-01-08  168.5608

Hasta ahora, la información en la tabla consolidado, que es la que tendrá todos los precios de cierre, es solo la de mcd, dado que es el archivo que estaba en la primera posición de nuestro vector. Ahora leeremos la información del segundo archivo y la guardaremos en una variable temporal.

temporal <- read.csv(archivos[2])[, c("Date", "Adj.Close")]
head(temporal)
##         Date Adj.Close
## 1 2017-12-29    311.35
## 2 2018-01-02    320.53
## 3 2018-01-03    317.25
## 4 2018-01-04    314.62
## 5 2018-01-05    316.58
## 6 2018-01-08    336.41

Como puedes ver, consolidado tiene la información de mcd y temporal tiene la información de tsla. Sin embargo, las columnas de ambas tablas se llaman igual, lo cual es problemático por dos razones:

1. Conforme vayamos juntando la información de varias acciones en una misma tabla, no podremos identificar fácilmente a cuál acción pertenece cada columna.

2. La función merge() ocupa utilizar alguna(s) columna(s) clave para identificar qué información de una tabla corresponde a los renglones de la otra. Por default, la función considera como columnas clave todas aquellas que se llaman igual en ambas tablas. Para nuestro caso, solo la columna de fecha, Date, es columna clave, dado que los precios de cada acción que corresponden a la misma fecha en cada tabla, van en el mismo renglón de consolidado.

Por estas dos razones, nos conviene renombrar las columnas de Adj.Close por algo que nos permita diferenciar cada acción. Para esto utilizaremos los tickers de cada acción. Dado que los nombres de los archivos ya contienen el ticker correspondiente, utilizaremos la función de texto sub() para extraerlos.

tickers <- sub(".csv", "", archivos)
tickers
## [1] "MCD"  "TSLA" "WMT"

Ahora renombramos cada columna de Adj.Close por su tickers correspondiente.

# el [2] en names(consolidado)[2] se debe a que solo queremos renombrar la
# segunda columna, no la primera. Le pondremos como nombre el valor en la 1era
# posición de tickers (tickers[1]).

names(consolidado)[2] <- tickers[1]
head(consolidado)
##         Date      MCD
## 1 2017-12-29 166.8067
## 2 2018-01-02 167.8727
## 3 2018-01-03 167.1653
## 4 2018-01-04 168.3379
## 5 2018-01-05 168.6771
## 6 2018-01-08 168.5608
# el [2] en names(temporal)[2] se debe a que solo queremos renombrar la
# segunda columna, no la primera. Le pondremos como nombre el valor en la 2da
# posición de tickers (tickers[2]).

names(temporal)[2] <- tickers[2]
head(temporal)
##         Date   TSLA
## 1 2017-12-29 311.35
## 2 2018-01-02 320.53
## 3 2018-01-03 317.25
## 4 2018-01-04 314.62
## 5 2018-01-05 316.58
## 6 2018-01-08 336.41

Ahora sí ya estamos listos para utilizar la función merge(). Aquí especificamos los argumentos más relevantes de la función merge(), para explicarlos, aunque en este caso el default de los últimos 4 argumentos nos sirve y no era necesario especificarlos.

consolidado <- merge(x = consolidado, y = temporal, 
 by.x = "Date", by.y = "Date", 
 all.x = T, all.y = T)
head(consolidado)
##         Date      MCD   TSLA
## 1 2017-12-29 166.8067 311.35
## 2 2018-01-02 167.8727 320.53
## 3 2018-01-03 167.1653 317.25
## 4 2018-01-04 168.3379 314.62
## 5 2018-01-05 168.6771 316.58
## 6 2018-01-08 168.5608 336.41

a. x y y son las tablas que se fusionarán

b. by.x y by.y indican las columnas clave de cada tabla, por default toma las que se llamen igual, pero si una se llamara fecha y otra date se pueden especificar como en este ejemplo.

c. all.x y all.y indican qué hacer en el caso de renglones de una tabla que no correspondan a la otra. all.x = TRUE indica que se incluyan todos los renglones de la tabla x, por lo tanto en donde no haya correspondencia de la tabla y aparecerán NAs. Lo mismo para el caso de all.y = TRUE. Por tanto, la unión con all.x = T y all.y = T es la que genera la mayor cantidad de renglones y la que pudiera tener más NAs, mientras que la unión con ambos argumentos como FALSE es la que genera la menor cantidad de renglones.

Ahora podemos repetir los pasos para agregar la información del tercer archivo. Dado que ya no ocupamos la variable temporal, la reutilizaremos. Este enfoque de utilizar la información de un vector para ir leyendo diferentes archivos y de poder usar los mismos nombres de variables u objetos, es lo que nos permitirá automatizar la lectura y consolidación de varios archivos ya que veamos el tema de Estructuras de Control Iterativas en el siguiente módulo.

# Aquí leemos la información de "WMT.csv". Nota como el único cambio respecto al
# bloque donde leímos la información de "TSLA.csv", es el subíndice del vector
# "archivos": antes era 2 y ahora es 3
# Aprovechamos para renombrar la columna "Adj.Close" de una vez, de nuevo
# observa como ahora tomamos la posición 3 del vector "tickers".

temporal <- read.csv(archivos[3])[, c("Date", "Adj.Close")]

names(temporal)[2] <- tickers[3]

head(temporal) 
##         Date      WMT
## 1 2017-12-29 95.94424
## 2 2018-01-02 95.78879
## 3 2018-01-03 96.62435
## 4 2018-01-04 96.71180
## 5 2018-01-05 97.28503
## 6 2018-01-08 98.72298

Ya que tenemos información nueva en la variable temporal, podemos utilizar exactamente el mismo código de antes, para fusionarla con consolidado:

consolidado <- merge(x = consolidado, y = temporal, 
 by.x = "Date", by.y = "Date", 
 all.x = T, all.y = T)
head(consolidado)
##         Date      MCD   TSLA      WMT
## 1 2017-12-29 166.8067 311.35 95.94424
## 2 2018-01-02 167.8727 320.53 95.78879
## 3 2018-01-03 167.1653 317.25 96.62435
## 4 2018-01-04 168.3379 314.62 96.71180
## 5 2018-01-05 168.6771 316.58 97.28503
## 6 2018-01-08 168.5608 336.41 98.72298

Como puedes ver, así podemos seguir leyendo y agregando archivos. Ya nos estamos ahorrando algo de tiempo, pero cuando realmente desquitaremos la programación será cuando automaticemos esto con un ciclo for.

El paquete dplyr dentro del universo de paquetes tidyverse contiene alternativas que pueden hacer lo mismo que merge( ) y pueden ser más familiares para quienes ya conocen SQL o Access.

a. full_join() es como merge(x = …, y = …, all.x = TRUE, all.y = TRUE)

b. left_join() es como merge(x = …, y = …, all.x = TRUE, all.y = FALSE)

c. right_join() es como merge(x = …, y = …, all.x = FALSE, all.y = TRUE)

d. inner_join() es como merge(x = …, y = …, all.x = FALSE, all.y = FALSE)

“Full join” también se puede conocer como “outer join”.

Adicional a las funciones arriba, dplyr tiene un par de funciones o “joins” que sirven como filtros y no tienen equivalente con merge().

e. semi_join() regresa todos los renglones de x que concuerdan con y

f. anti_join() regresa todos los renglones de x que NO concuerdan con y

Automatización de lectura y consolidación

Una función que nos servirá para ese propósito y tener todos los archivos y tickers que queremos leer en vectores correspondientes es list.files().

Observa estos ejemplos:

# Esta función toma por default el "Working Directory", así que si ya definiste
# la carpeta con los archivos que quieres leer como el directorio de trabajo,
# puedes utilizar la función sin especificar el argumento "path". De lo
# contrario, habría que especificar la ruta de la carpeta ahí.

archivos <- list.files(pattern = "(*).csv", recursive = FALSE)
archivos 
## [1] "MCD.csv"  "TSLA.csv" "WMT.csv"

Los argumentos pattern y recursive son opcionales, dependiendo del caso. Los incluyo aquí dado que pudieran tener otros archivos en la misma carpeta que no quisieran leer.

a. **pattern = “(*).csv” limita a que los archivos sean solo aquellos con extensión “.csv”**.

b. recursive = TRUE serviría en caso que quisiéramos que la función también buscara en subcarpetas, pero ese no es nuestro caso.

# quitar la extensión .csv para tener solo los nombres

tickers <- sub(".csv", "", archivos)
tickers
## [1] "MCD"  "TSLA" "WMT"
# para tener un conteo de los archivos en la lista y saber cuántas iteraciones
# tendremos que hacer en su momento

n <- length(archivos)
n
## [1] 3

Archivos en la red

En este último ejemplo veremos cómo leer archivos en formato de Excel y adicionalmente que éstos no tienen que estar localmente en nuestro equipo. Conforme especifiquemos bien la ruta del archivo, es posible leer archivos en directorios compartidos o incluso en la web, siempre y cuando tengamos acceso a ellos.

# R base no tiene funciones para leer archivos de Excel, pero hay muchos
# paquetes que pueden agregar esa funcionalidad. openxlsx es solo uno de varias
# opciones buenas

library(openxlsx)

netflix <- read.xlsx("https://public.tableau.com/s/sites/default/files/media/netflix_titles.xlsx")

head(netflix)
##   duration_minutes duration_seasons    type
## 1               90               NA   Movie
## 2               94               NA   Movie
## 3             <NA>                1 TV Show
## 4             <NA>                1 TV Show
## 5               99               NA   Movie
## 6             <NA>                1 TV Show
##                                     title date_added release_year   rating
## 1 Norm of the North: King Sized Adventure      43717         2019    TV-PG
## 2              Jandino: Whatever it Takes      42622         2016    TV-MA
## 3                      Transformers Prime      43351         2013 TV-Y7-FV
## 4        Transformers: Robots in Disguise      43351         2016    TV-Y7
## 5                            #realityhigh      42986         2017    TV-14
## 6                                 Apaches      42986         2016    TV-MA
##                                                                                                                                            description
## 1         Before planning an awesome wedding for his grandfather, a polar bear king must take back a stolen artifact from an evil archaeologist first.
## 2    Jandino Asporaat riffs on the challenges of raising kids and serenades the audience with a rousing rendition of "Sex on Fire" in his comedy show.
## 3         With the help of three human allies, the Autobots once again protect Earth from the onslaught of the Decepticons and their leader, Megatron.
## 4                       When a prison ship crash unleashes hundreds of Decepticons on Earth, Bumblebee leads a new Autobot force to protect humankind.
## 5 When nerdy high schooler Dani finally attracts the interest of her longtime crush, she lands in the cross hairs of his ex, a social media celebrity.
## 6             A young journalist is forced into a life of crime to save his father and family in this series based on the novel by Miguel Sáez Carral.
##    show_id
## 1 81145628
## 2 80117401
## 3 70234439
## 4 80058654
## 5 80125979
## 6 80163890

Lectura de API’s

Una API (Application Programming Interface) es una interface que sirve de conexión entre equipos, plataformas, programas o cualquier combinación de estos. En nuestro caso, una API es lo que utilizaríamos para conectarnos a fuentes de información como Bloomberg, Refinitiv, Capital IQ, Yahoo Finance, Facebook Twitter, MLB, NFL, etc.

Dado que la API tiene que ser desarrollada específicamente para permitir que alguien se conecte a una base de datos y que pueda ejecutar consultas, no todos ofrecen ese servicio y en muchos casos es un servicio con costo. En algunos casos el costo es incluso mayor que el de una suscripción normal, dado que programáticamente se puede descargar un mayor volumen de información y a mayor velocidad, requiriendo un nivel de servicio más alto.

Lo servicios que requieren suscripción, sean de paga o no, proporcionan un token como parte de la cuenta y éste se tiene que proporcionar como un argumento adicional de las funciones que consultan su información. Afortunadamente existen algunos sitios gratuitos con los que podemos practicar y descargar su información de manera anónima, sin necesidad de identificarnos con un token.

Veamos unos ejemplos utilizando el paquete quantmod(). Recuerda instalarlo con install.packages(“quantmod”) la primera vez que lo vayas a utilizar.

rm(list = ls()) # para limpiar los datos del ambiente global

library(quantmod) # se requiere activar quantmod en cada sesión que se utilice
## Loading required package: xts
## Loading required package: zoo
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
## 
## Attaching package: 'xts'
## The following objects are masked from 'package:dplyr':
## 
##     first, last
## Loading required package: TTR
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo

Descargando datos de la reserva federal de los Estados Unidos, “Federal Reserve Economic Data” (FRED):

gdp <- getSymbols(Symbols = "GDP", src = "FRED", auto.assign = F)

## 'getSymbols' currently uses auto.assign=TRUE by default, but will
## use auto.assign=FALSE in 0.5-0. You will still be able to use
## 'loadSymbols' to automatically load data. getOption("getSymbols.env")
## and getOption("getSymbols.auto.assign") will still be checked for
## alternate defaults.
## 
## This message is shown once per session and may be disabled by setting 
## options("getSymbols.warning4.0"=FALSE). See ?getSymbols for details.

head(gdp)
##                GDP
## 1947-01-01 243.164
## 1947-04-01 245.968
## 1947-07-01 249.585
## 1947-10-01 259.745
## 1948-01-01 265.742
## 1948-04-01 272.567

Descargando datos de OANDA, plataforma de trading de Forex:

usdmxn <- getSymbols("USD/MXN", src = "oanda", auto.assign = F)

head(usdmxn)
##             USD.MXN
## 2022-04-27 20.39800
## 2022-04-28 20.45723
## 2022-04-29 20.38567
## 2022-04-30 20.42520
## 2022-05-01 20.42515
## 2022-05-02 20.43713

Descargando datos de Yahoo Finance (el default de quantmod):

mcd <- getSymbols(Symbols = "mcd", src = "yahoo",
 from = "2020-06-30", to = "2021-06-30",
 periodicity = "daily", auto.assign = F)

head(mcd)
##            MCD.Open MCD.High MCD.Low MCD.Close MCD.Volume MCD.Adjusted
## 2020-06-30   182.92   185.20  181.89    184.47    3163100     175.3101
## 2020-07-01   184.95   186.44  183.72    184.66    2193800     175.4907
## 2020-07-02   187.00   187.00  182.86    183.52    2690200     174.4073
## 2020-07-06   186.00   188.72  184.14    188.50    3171500     179.1400
## 2020-07-07   187.37   187.85  185.25    185.82    2399600     176.5931
## 2020-07-08   185.50   187.23  184.75    185.85    2776400     176.6216

Examina los tres objetos creados en el Global Environment. También puedes dar clic en el icono de tabla a la derecha de cada objeto en el panel superior derecho o usar la función View() (con “V” mayúscula) para visualizar las tablas en una pestaña independiente dentro de R Studio.

Observa cómo en este caso no leímos la información de un archivo, sino más bien ejecutamos consultas a las fuentes respectivas, especificando la información que ocupamos, por ejemplo, el identificador del activo financiero y los rangos de fechas (en el caso de Yahoo Finance).

Otros tipos de estructuras

Como podrás ver del ejemplo anterior, las tablas que obtenemos de la función getSymbols(), mediante el paquete quantmod() no son data frames sino xts(eXtensible Time Series).

Observa cómo la función class() nos indica que mcd es un xts, que a su vez se basa en otra estructura de datos llamada zoo y por eso nos muestra ambas. La función str() nos indica lo mismo.

class(mcd)
## [1] "xts" "zoo"
str(mcd)
## An 'xts' object on 2020-06-30/2021-06-29 containing:
##   Data: num [1:252, 1:6] 183 185 187 186 187 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : NULL
##   ..$ : chr [1:6] "MCD.Open" "MCD.High" "MCD.Low" "MCD.Close" ...
##   Indexed by objects of class: [Date] TZ: UTC
##   xts Attributes:  
## List of 2
##  $ src    : chr "yahoo"
##  $ updated: POSIXct[1:1], format: "2022-10-23 01:00:57"

De la misma manera, algunas funciones de tidyverse generan tibble’s en vez de data frames.

En ambos casos, la razón de ser de las estructuras nuevas es eficiencia respecto a las funciones que incluyen sus paquetes respectivos. Por ejemplo, una vez que se manejan bien los xts, es más fácil manejar y transformar información financiera dado que ésta casi siempre la utilizamos como series de tiempo.

Un ejemplo es que es más fácil filtrar xts en base a fechas. Por ejemplo, para los xts están disponibles las funciones first() y tail() adicionales a las de head() y tail() que se basan en renglones.

head(mcd, 5)
##            MCD.Open MCD.High MCD.Low MCD.Close MCD.Volume MCD.Adjusted
## 2020-06-30   182.92   185.20  181.89    184.47    3163100     175.3101
## 2020-07-01   184.95   186.44  183.72    184.66    2193800     175.4907
## 2020-07-02   187.00   187.00  182.86    183.52    2690200     174.4073
## 2020-07-06   186.00   188.72  184.14    188.50    3171500     179.1400
## 2020-07-07   187.37   187.85  185.25    185.82    2399600     176.5931
tail(mcd, 3)
##            MCD.Open MCD.High MCD.Low MCD.Close MCD.Volume MCD.Adjusted
## 2021-06-25   232.74   233.41  232.34    232.42    3000100     226.1089
## 2021-06-28   232.67   232.75  230.01    231.09    2901700     224.8150
## 2021-06-29   231.53   231.71  230.05    230.37    1932100     224.1146
first(mcd, "2 weeks")
##            MCD.Open MCD.High MCD.Low MCD.Close MCD.Volume MCD.Adjusted
## 2020-06-30   182.92   185.20  181.89    184.47    3163100     175.3101
## 2020-07-01   184.95   186.44  183.72    184.66    2193800     175.4907
## 2020-07-02   187.00   187.00  182.86    183.52    2690200     174.4073
## 2020-07-06   186.00   188.72  184.14    188.50    3171500     179.1400
## 2020-07-07   187.37   187.85  185.25    185.82    2399600     176.5931
## 2020-07-08   185.50   187.23  184.75    185.85    2776400     176.6216
## 2020-07-09   186.22   186.59  182.62    184.33    2333500     175.1770
## 2020-07-10   183.78   185.44  183.01    184.88    2714100     175.6998
last(mcd, "1 month")
##            MCD.Open MCD.High MCD.Low MCD.Close MCD.Volume MCD.Adjusted
## 2021-06-01   235.98   235.99  232.74    233.24    2574300     226.9067
## 2021-06-02   233.97   234.33  232.81    233.78    3172000     227.4320
## 2021-06-03   232.57   232.76  230.15    232.45    3249400     226.1381
## 2021-06-04   233.44   233.80  232.07    233.38    1615200     227.0428
## 2021-06-07   234.00   234.07  231.16    231.69    1877000     225.3987
## 2021-06-08   231.50   233.98  231.34    232.64    2106200     226.3230
## 2021-06-09   232.98   234.32  231.45    231.47    1982200     225.1847
## 2021-06-10   232.05   234.90  231.93    234.59    2534000     228.2200
## 2021-06-11   235.00   237.50  234.71    236.93    2654300     230.4965
## 2021-06-14   237.18   237.77  234.81    236.98    1836800     230.5451
## 2021-06-15   237.53   237.81  235.66    236.35    1948300     229.9322
## 2021-06-16   237.21   237.28  233.78    235.58    2951700     229.1831
## 2021-06-17   235.08   236.27  233.28    233.88    1895600     227.5293
## 2021-06-18   231.47   232.89  228.82    229.62    4408200     223.3849
## 2021-06-21   230.63   233.23  229.47    232.90    2193000     226.5759
## 2021-06-22   233.49   234.86  232.43    233.88    1758300     227.5293
## 2021-06-23   233.30   234.45  232.70    233.24    1701500     226.9067
## 2021-06-24   234.10   235.16  232.74    233.33    1839800     226.9942
## 2021-06-25   232.74   233.41  232.34    232.42    3000100     226.1089
## 2021-06-28   232.67   232.75  230.01    231.09    2901700     224.8150
## 2021-06-29   231.53   231.71  230.05    230.37    1932100     224.1146

Adicional a esto, quantmod incluye la función chartSeries() para hacer análisis técnico. Esta función puede trabajar con data frames pero está especializada para xts que además tengan formato “OHLC”. Aquí un ejemplo:

chartSeries(mcd, 
 subset = "last 8 weeks", 
 type = "candle", 
 TA = c(addVo(), addSMA()))

Sin embargo, tanto los xts’s como tibble’s se pueden transformar a data frames sin mucho problema. Por tanto, si un usuario sigue estando más cómodo con data frames, puede seguir trabajando con ellos.

En el caso de los xts, se requiere especificar la columna de fecha para el data frame, que es el índice del xts, mientras que el resto de la información del xts serán las demás columnas. La instrucción sería de la siguiente manera:

dataframe <- data.frame(fecha = index(xts), coredata(xts))

Veamos un ejemplo con los datos que ya tenemos en el ambiente:

mcd.df <- data.frame(fecha = index(mcd), coredata(mcd))

head(mcd.df)
##        fecha MCD.Open MCD.High MCD.Low MCD.Close MCD.Volume MCD.Adjusted
## 1 2020-06-30   182.92   185.20  181.89    184.47    3163100     175.3101
## 2 2020-07-01   184.95   186.44  183.72    184.66    2193800     175.4907
## 3 2020-07-02   187.00   187.00  182.86    183.52    2690200     174.4073
## 4 2020-07-06   186.00   188.72  184.14    188.50    3171500     179.1400
## 5 2020-07-07   187.37   187.85  185.25    185.82    2399600     176.5931
## 6 2020-07-08   185.50   187.23  184.75    185.85    2776400     176.6216

Observa cómo ahora tanto class() y str() nos indican que mcd.df es un data frame. También puedes notar en el Global Environment cómo se ve diferente la descripción de mcd.df y mcd.

class(mcd.df)
## [1] "data.frame"
str(mcd.df)
## 'data.frame':    252 obs. of  7 variables:
##  $ fecha       : Date, format: "2020-06-30" "2020-07-01" ...
##  $ MCD.Open    : num  183 185 187 186 187 ...
##  $ MCD.High    : num  185 186 187 189 188 ...
##  $ MCD.Low     : num  182 184 183 184 185 ...
##  $ MCD.Close   : num  184 185 184 188 186 ...
##  $ MCD.Volume  : num  3163100 2193800 2690200 3171500 2399600 ...
##  $ MCD.Adjusted: num  175 175 174 179 177 ...

En el caso de los tibbles, simplemente se utiliza una función de la siguiente manera: dataframe <- as.data.frame(tibble).

Nota sobre web scraping

Adicional a la lectura de datos mediante archivos y a la lectura de datos mediante APIs, la tercera forma es conocida como “web scraping”. Las tablas en sitios web html y xml pueden ser leídas sin mucho problema con diferentes paquetes. Uno muy usado es rvest. Sin embargo, web scraping tiene dos limitantes importantes:

1. El sitio del que se está leyendo no necesariamente está diseñado para que se consulten datos de sus tablas de manera programática. En cualquier momento lo pueden actualizar y el código ya diseñado dejaría de funcionar. Es más probable que código que hace web scraping deje de funcionar y se tenga que actualizar que códigos que leen información de APIs. Aunque códigos que leen archivos o que se conectan a APIs tampoco son inmunes a esto, web scraping lo es mucho más.

2. Muchos sitios tienen prohíbido accederlos con herramientas de web scraping, por lo que también es importante asegurarnos de no estar quebrantando alguna ley. En el mejor de los casos simplemente le harán cambios al sitio con cierta frecuencia para evitar esto.

Por estas razones no cubriremos este tema aquí, pero lo mencionamos para completar la información de las diferentes maneras para leer datos externos.