Examples by Prof. Jacob Escobar, EGADE Business School, Tec de Monterrey
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, sobretodo 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()
hoy
## [1] "2021-10-11"
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] "2021-10-11 12:41:23 CDT"
class(ahora)
## [1] "POSIXct" "POSIXt"
as.numeric(ahora)
## [1] 1633974083
ahora + 1
## [1] "2021-10-11 12:41:24 CDT"
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 refierimos 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
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:
read.table( )
read.csv( )
openxlsx
: read.xlsx( )
readr
en tidyverse
: read_csv( )
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:
getwd( )
.setwd( )
. Pero también se puede hacer desde la interfase, ya sea en el panel multipropósito (panel inferior derecho) en la pestaña de “Files” o dando click en el menú superior de RStudio “Session -> Set Working Directory -> Choose Directory”.read.csv("c:/Mis_Documentos/R/archivo.csv")
En el caso de Google Colab, podemos subir los archivos que queramos leer seleccionando en la barra de la izquierda el icono de la carpeta, para abrir el panel de Files. Ese es el Working Directory de R al usar Google Colab, así que los archivos que tenamos ahí los podemos leer sin especificar su ubicación.
Adicional a este enfoque, también podemos leer archivos que estén en la red, por ejemplo en un repositorio de GitHub o de Google Drive.
Descarga el archivo US_Stocks.zip y extrae los 42 archivos en el folder de tu preferencia en tu dispositivo. Sin embargo, para que los puedas leer con este código es además requerido que los subas a la sección de Files antes descrita. Solo si sigues estos pasos funcionarán los siguientes códigos.
tsla <- read.csv("TSLA.csv", skip = 0, header = T)
class(tsla)
## [1] "data.frame"
head(tsla, 6)
## 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
La función read.csv( )
tiene muchos argumentos opcionales que podemos especificar, puedes verlos en su documentación ?read.csv
. En la instrucción anterior especifiqué dos de los más usados:
skip =
cuando la información del archivo no empieza en el primer renglón, podemos especificar cuántos renglones nos queremos saltar.header =
sirve para especificar si las columnas tienen nombre o no.Puedes ver en la documentación que los valores que usé son precisamente los default, por tanto no era necesario especificarlos para este caso, pero los incluyo dado que hay muchos otros ejemplos comunes donde sí es necesario.
Funciones para explorar el data frame:
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 ...
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
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 correció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(color = "darkgrey")
ggplot(data = tsla, aes(x = Adj.Close)) + geom_histogram(color = "red", fill = "orange", bins = 30)
ggplot(data = tsla, aes(y = Adj.Close)) + geom_boxplot(color = "blue", fill = "lightblue")
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(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 al utilizarla con data frames 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:
tsla$LaggedAdj.Close <- dplyr::lag(tsla$Adj.Close, 1)
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, calculamos el retorno logarítmico en una sola instrucción:
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
getwd()
## [1] "D:/Documents/OneDrive - Instituto Tecnologico y de Estudios Superiores de Monterrey/MyDocs/ITESM/1d.FinancialEngineering"
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, sobretodo al combinarlas con el operador “pipe” (%>%) que vimos anteriormente. Veamos algunos ejemplos:
filter( )
permite filtrar renglones en base a una o más condiciones.select( )
permite seleccionar columnas.group_by( )
permite agrupar la tabla en base a los valores de una columnasummarize( )
permite hacer cálculos a nivel agrupadoMostrando 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:
tslaAnio1 <- tsla %>% filter(Date >= "2018-12-31" & Date <= "2019-01-10") %>%
select(Date, Adj.Close, Volume)
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 x 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 sobreescribir el objeto. Las instrucciones abajo hacen exactamente lo mismo:
tsla <- tsla[, c("Date", "Adj.Close")]
tsla <- tsla %>% select("Date", "Adj.Close")
head(tsla)
## 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
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")]
head(tsla)
## 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
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")
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( )
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 quisé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 informaicón de tsla. Sin embargo, las columnas de ambas tablas se llaman igual, lo cual es problemático por dos razones:
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
x
y y
son las tablas que se fusionaránby.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.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 NA
s. 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 NA
s, 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 varible 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 utilizando ciclos.
# 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
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 universe 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.
full_join( )
es como merge(x = ..., y = ..., all.x = TRUE, all.y = TRUE)
left_join( )
es como merge(x = ..., y = ..., all.x = TRUE, all.y = FALSE)
right_join( )
es como merge(x = ..., y = ..., all.x = FALSE, all.y = TRUE)
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( )
.
semi_join( )
regresa todos los renglones de x
que concuerdan con y
anti_join( )
regresa todos los renglones de x
que NO concuerdan con y
Una función que nos servirá para ese propósito y tener todos los archivos y tickers que queremos leer en vectores corresondientes es list.files( )
.
# Esta función toma por default el "Working Directory", que en el caso de Google
# Colab es la sección de "Files"
archivos <- list.files(pattern = "(*).csv", recursive = FALSE)
archivos
## [1] "^GSPC.csv" "FB.csv" "MCD.csv" "SPY.csv" "TSLA.csv" "WMT.csv"
Los argumentos pattern
y recursive
son opcionales, dependiendo del caso. Los incluyo aquí dado que pudieran tener otro archivos en la misma carpeta que no quisieran leer.
pattern = "(*).csv"
limita a que los archivos sean solo aquellos con extensión “.csv”.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] "^GSPC" "FB" "MCD" "SPY" "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] 6
Ya que tenemos la lista de archivos, su cantidad y los tickers que corresponden a cada uno, podemos ejecutar el siguiente ciclo para leer y consolidar toda la información.
Observa como creo el objeto p
(precios) antes de entrar al ciclo, para de esa manera ya tener algo donde pegar el resto de la información que vayamos leyendo. Dado que para “inicializar p
” ocupo el primer archivo de la lista, el ciclo for
comienza leyendo a partir del segundo archivo.
p <- read.csv(archivos[1])[,c("Date","Adj.Close")]
names(p) <- c("Date", tickers[1])
for(i in 2:n) {
temp <- read.csv(archivos[i])[ , c("Date","Adj.Close")]
names(temp) <- c("Date", tickers[i])
p <- merge(p, temp)
}
p$Date <- as.Date(p$Date)
head(p)
## Date ^GSPC FB MCD SPY TSLA WMT
## 1 2017-12-29 2673.61 176.46 166.8067 260.7371 311.35 95.94424
## 2 2018-01-02 2695.81 181.42 167.8727 262.6032 320.53 95.78879
## 3 2018-01-03 2713.06 184.67 167.1653 264.2642 317.25 96.62435
## 4 2018-01-04 2723.99 184.33 168.3379 265.3781 314.62 96.71180
## 5 2018-01-05 2743.15 186.85 168.6771 267.1465 316.58 97.28503
## 6 2018-01-08 2747.71 188.28 168.5608 267.6351 336.41 98.72298
Visualicemos algunos datos:
# Serie de tiempo con dos activos
plot(x = p$Date, y = p$FB, ylim = c(min(p$FB, p$TSLA), max(p$FB, p$TSLA)),
type = "l", lty = 1, col = "blue",
xlab = "Date", ylab = "Prices", main = "FB & TSLA Prices")
lines(x = p$Date, y = p$TSLA, col="red")
legend(x = "bottomleft", legend = c("FB", "TSLA"),
lty = 1, col = c("blue","red"))
También es posible escribir archivos, por ejemplo:
# OJO: Al guardar este archivo junto con los otros, lo leeremos junto con los
# archivos de acciones individuales si ejecutamos nuevamente el código anterior
# write.csv(p, "all_stocks.csv", row.names = F)
# Gráfica de dispersión: ¿qué tal la Beta?
plot(x = p$SPY, y = p$WMT, type = "p", pch = 1, col = "blue",
xlab = "SPY", ylab = "WMT", main = "Prices vs S&P500")
Una API (Application Programming Interface) es una interfase 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 un 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
# en el caso de Google Colab, es necesario instalarlo cada sesión, R Studio solo
# la primera vez que se utilice
# install.packages("quantmod")
# tanto en Colab como en R Studio, se requiere activar cada paquete en cada
# sesión que se quiera utilizar
library(quantmod)
## 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
Se puede utilizar la función getSymbols( )
, del paquete quantmod
para bajar datos de varias fuentes, por ejemplo de la reserva federal de los Estados Unidos (Federal Reserve Economic Data, FRED), de OANDA (plataforma de trading de Forex), de AlphaVantage, entre otras. La fuente default es Yahoo Finance y es la que usamos a continuación:
mcd <- getSymbols(Symbols = "mcd", src = "yahoo",
from = "2020-06-30", to = "2021-06-30",
periodicity = "daily", 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(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 179.2208
## 2020-07-01 184.95 186.44 183.72 184.66 2193800 179.4054
## 2020-07-02 187.00 187.00 182.86 183.52 2690200 178.2978
## 2020-07-06 186.00 188.72 184.14 188.50 3171500 183.1361
## 2020-07-07 187.37 187.85 185.25 185.82 2399600 180.5323
## 2020-07-08 185.50 187.23 184.75 185.85 2776400 180.5615
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.
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: "2021-10-11 12:41:29"
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 179.2208
## 2020-07-01 184.95 186.44 183.72 184.66 2193800 179.4054
## 2020-07-02 187.00 187.00 182.86 183.52 2690200 178.2978
## 2020-07-06 186.00 188.72 184.14 188.50 3171500 183.1361
## 2020-07-07 187.37 187.85 185.25 185.82 2399600 180.5323
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 231.1527
## 2021-06-28 232.67 232.75 230.01 231.09 2901700 229.8300
## 2021-06-29 231.53 231.71 230.05 230.37 1932100 229.1139
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 179.2208
## 2020-07-01 184.95 186.44 183.72 184.66 2193800 179.4054
## 2020-07-02 187.00 187.00 182.86 183.52 2690200 178.2978
## 2020-07-06 186.00 188.72 184.14 188.50 3171500 183.1361
## 2020-07-07 187.37 187.85 185.25 185.82 2399600 180.5323
## 2020-07-08 185.50 187.23 184.75 185.85 2776400 180.5615
## 2020-07-09 186.22 186.59 182.62 184.33 2333500 179.0847
## 2020-07-10 183.78 185.44 183.01 184.88 2714100 179.6191
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 231.9683
## 2021-06-02 233.97 234.33 232.81 233.78 3172000 232.5053
## 2021-06-03 232.57 232.76 230.15 232.45 3249400 231.1826
## 2021-06-04 233.44 233.80 232.07 233.38 1615200 232.1075
## 2021-06-07 234.00 234.07 231.16 231.69 1877000 230.4267
## 2021-06-08 231.50 233.98 231.34 232.64 2106200 231.3715
## 2021-06-09 232.98 234.32 231.45 231.47 1982200 230.2079
## 2021-06-10 232.05 234.90 231.93 234.59 2534000 233.3109
## 2021-06-11 235.00 237.50 234.71 236.93 2654300 235.6381
## 2021-06-14 237.18 237.77 234.81 236.98 1836800 235.6879
## 2021-06-15 237.53 237.81 235.66 236.35 1948300 235.0613
## 2021-06-16 237.21 237.28 233.78 235.58 2951700 234.2955
## 2021-06-17 235.08 236.27 233.28 233.88 1895600 232.6048
## 2021-06-18 231.47 232.89 228.82 229.62 4408200 228.3680
## 2021-06-21 230.63 233.23 229.47 232.90 2193000 231.6301
## 2021-06-22 233.49 234.86 232.43 233.88 1758300 232.6048
## 2021-06-23 233.30 234.45 232.70 233.24 1701500 231.9683
## 2021-06-24 234.10 235.16 232.74 233.33 1839800 232.0578
## 2021-06-25 232.74 233.41 232.34 232.42 3000100 231.1527
## 2021-06-28 232.67 232.75 230.01 231.09 2901700 229.8300
## 2021-06-29 231.53 231.71 230.05 230.37 1932100 229.1139
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()))
Generalmente estructuras de datos tabulares de paquetes instalables 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. Veamos un ejemplo:
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 179.2208
## 2 2020-07-01 184.95 186.44 183.72 184.66 2193800 179.4054
## 3 2020-07-02 187.00 187.00 182.86 183.52 2690200 178.2978
## 4 2020-07-06 186.00 188.72 184.14 188.50 3171500 183.1361
## 5 2020-07-07 187.37 187.85 185.25 185.82 2399600 180.5323
## 6 2020-07-08 185.50 187.23 184.75 185.85 2776400 180.5615
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 179 179 178 183 181 ...
Aunque ahora leemos los datos de cada acción directamente de Yahoo Finance, la parte de la consolidación será muy parecida a lo que hicimos en el caso de los archivos externos.
tickers <- c("AAPL", "AMZN", "LMT", "NFLX", "TSLA", "WMT", "SBUX", "MCD",
"DIS", "GOOG", "GM", "JPM", "FB", "GS", "LVS", "UPS",
"MSFT", "NVDA", "HD", "SPY", "^SP500TR")
tickers
## [1] "AAPL" "AMZN" "LMT" "NFLX" "TSLA" "WMT"
## [7] "SBUX" "MCD" "DIS" "GOOG" "GM" "JPM"
## [13] "FB" "GS" "LVS" "UPS" "MSFT" "NVDA"
## [19] "HD" "SPY" "^SP500TR"
n <- length(tickers)
n
## [1] 21
Ahora especificamos solo la columna 6 por las siguientes dos razones: * En el caso de los xts
, las fechas están en los “rownames”, por tanto ya viene integrada y no se cuenta como columna. * getSymbols( )
le pone el ticker como prefijo a cada nombre de columna, lo cual es bueno para seguir diferenciando cada columna al fusionar la información de diferentes acciones. Sin embargo el nombre de columna Adj.Close
que solíamos usar de los archivos, ahora va cambiando en cada acción y es más fácil referirnos a esa columna por su posición que por su nombre.
p <- getSymbols(Symbols = tickers[1], src = "yahoo",
from = "2016-12-31", to = "2020-12-31",
periodicity = "monthly", auto.assign = F)[, 6]
head(p)
## AAPL.Adjusted
## 2017-01-01 28.59782
## 2017-02-01 32.28360
## 2017-03-01 34.00226
## 2017-04-01 33.99989
## 2017-05-01 36.15609
## 2017-06-01 34.22816
for (i in 2:n) {
p <- merge(p, getSymbols(Symbols = tickers[i], src = "yahoo",
from = "2016-12-31", to = "2020-12-31",
periodicity = "monthly", auto.assign = F)[, 6])
}
names(p) <- sub(".Adjusted", "", names(p))
head(p)
## AAPL AMZN LMT NFLX TSLA WMT SBUX MCD
## 2017-01-01 28.59782 823.48 222.1799 140.71 50.386 60.54938 50.34516 109.1400
## 2017-02-01 32.28360 845.04 235.6612 142.13 49.998 64.35074 51.84948 113.6634
## 2017-03-01 34.00226 886.54 238.1926 147.81 55.660 65.39405 53.47520 116.2580
## 2017-04-01 33.99989 924.99 239.8393 152.20 62.814 68.70803 55.00464 125.5149
## 2017-05-01 36.15609 994.62 250.2357 163.07 68.202 71.83362 58.25582 135.3458
## 2017-06-01 34.22816 968.00 248.6983 149.41 72.322 69.62784 53.62191 137.3820
## DIS GOOG GM JPM FB GS LVS UPS
## 2017-01-01 105.9607 796.79 32.02243 73.83686 130.32 211.1513 44.74626 94.30125
## 2017-02-01 105.4244 823.21 32.22361 79.50041 135.54 228.4065 45.06114 91.38919
## 2017-03-01 108.5846 829.56 30.92908 77.06153 142.05 212.0725 48.56731 93.43126
## 2017-04-01 110.7009 905.96 30.60930 76.32458 150.25 206.6072 50.84839 93.57055
## 2017-05-01 103.3655 964.86 29.98192 72.48382 151.46 195.0306 50.96907 92.27314
## 2017-06-01 101.7471 908.73 30.86556 80.64542 150.98 205.5428 55.07211 97.06252
## MSFT NVDA HD SPY SP500TR
## 2017-01-01 60.22013 26.94322 123.0192 208.9750 4359.81
## 2017-02-01 59.59604 25.04303 129.5734 217.1860 4532.93
## 2017-03-01 61.71915 26.91544 131.2902 216.5155 4538.21
## 2017-04-01 64.15568 25.77143 140.4286 219.6160 4584.82
## 2017-05-01 65.44890 35.66737 138.0986 222.7154 4649.34
## 2017-06-01 64.96638 35.75686 138.7971 223.0475 4678.36
plot(p[, c("AAPL","NFLX")], col = c("blue", "red"), main = "Time-series")
La función lag( )
se encuentra en los paquetes stats
y dplyr
y cuál usar depende de la estructura de datos con la que estemos trabajando: * Para dataframes usardplyr::lag( )
* Para xts usar stats::lag( )
r <- na.omit(p/stats::lag(p) - 1)
head(r)
## AAPL AMZN LMT NFLX TSLA
## 2017-02-01 0.12888332949 0.02618157 0.060677303 0.01009166 -0.007700571
## 2017-03-01 0.05323625618 0.04911010 0.010741832 0.03996336 0.113244508
## 2017-04-01 -0.00006967184 0.04337087 0.006913304 0.02970028 0.128530345
## 2017-05-01 0.06341800116 0.07527650 0.043347387 0.07141925 0.085777121
## 2017-06-01 -0.05332249109 -0.02676399 -0.006143803 -0.08376772 0.060408724
## 2017-07-01 0.03270383121 0.02043391 0.052303700 0.21584900 -0.105472735
## WMT SBUX MCD DIS GOOG
## 2017-02-01 0.06278119 0.02988017 0.04144579 -0.005061104 0.033158103
## 2017-03-01 0.01621290 0.03135449 0.02282776 0.029975497 0.007713677
## 2017-04-01 0.05067709 0.02860089 0.07962339 0.019490184 0.092097044
## 2017-05-01 0.04549085 0.05910749 0.07832488 -0.066262936 0.065013865
## 2017-06-01 -0.03070679 -0.07954410 0.01504416 -0.015656835 -0.058174249
## 2017-07-01 0.05695028 -0.07425845 0.01927758 0.034635340 0.023956533
## GM JPM FB GS LVS
## 2017-02-01 0.006282471 0.076703572 0.040055139 0.08171992 0.007036923
## 2017-03-01 -0.040173551 -0.030677589 0.048030178 -0.07151304 0.077809309
## 2017-04-01 -0.010338944 -0.009563085 0.057726130 -0.02577077 0.046967349
## 2017-05-01 -0.020496612 -0.050321492 0.008053291 -0.05603207 0.002373231
## 2017-06-01 0.029472399 0.112599008 -0.003169226 0.05390053 0.080500551
## 2017-07-01 0.041555643 0.004376194 0.121009435 0.01545731 -0.024920983
## UPS MSFT NVDA HD SPY
## 2017-02-01 -0.030880355 -0.010363445 -0.070525657 0.053278040 0.039291552
## 2017-03-01 0.022344743 0.035625103 0.074767995 0.013249492 -0.003087082
## 2017-04-01 0.001490829 0.039477599 -0.042504072 0.069604559 0.014319755
## 2017-05-01 -0.013865570 0.020157530 0.383988680 -0.016591962 0.014112937
## 2017-06-01 0.051904324 -0.007372485 0.002509212 0.005058211 0.001491244
## 2017-07-01 -0.002712664 0.054693138 0.124169897 -0.024771971 0.025530862
## SP500TR
## 2017-02-01 0.039708179
## 2017-03-01 0.001164762
## 2017-04-01 0.010270539
## 2017-05-01 0.014072531
## 2017-06-01 0.006241750
## 2017-07-01 0.020562804
Ahora calcularemos el retorno de un portafolio con todas estas acciones “equally-weighted”. Solo omitiremos el índice SP500TR
dado que no es un activo que podamos adquirir directamente (en cambio, el SPY
sí lo podemos adquirir).
# w es el vector de pesos
w <- rep(1/(length(tickers) - 1), length(tickers) - 1)
w
## [1] 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
## [16] 0.05 0.05 0.05 0.05 0.05
# EQPORT será una columna en nuestra tabla de retornos r
# nota como esta multiplicación matricial es equivalente a hacer un
# "SUMPRODUCT" de cada renglón de retornos por el vector de pesos (w) y eso nos
# da el retorno de cada día del portafolio
r$EQPORT <- coredata(r[, 1:(length(tickers)-1)]) %*% w
head(r$EQPORT)
## EQPORT
## 2017-02-01 0.028646780
## 2017-03-01 0.025037797
## 2017-04-01 0.031017154
## 2017-05-01 0.039614519
## 2017-06-01 0.002413334
## 2017-07-01 0.030538293
head(r)
## AAPL AMZN LMT NFLX TSLA
## 2017-02-01 0.12888332949 0.02618157 0.060677303 0.01009166 -0.007700571
## 2017-03-01 0.05323625618 0.04911010 0.010741832 0.03996336 0.113244508
## 2017-04-01 -0.00006967184 0.04337087 0.006913304 0.02970028 0.128530345
## 2017-05-01 0.06341800116 0.07527650 0.043347387 0.07141925 0.085777121
## 2017-06-01 -0.05332249109 -0.02676399 -0.006143803 -0.08376772 0.060408724
## 2017-07-01 0.03270383121 0.02043391 0.052303700 0.21584900 -0.105472735
## WMT SBUX MCD DIS GOOG
## 2017-02-01 0.06278119 0.02988017 0.04144579 -0.005061104 0.033158103
## 2017-03-01 0.01621290 0.03135449 0.02282776 0.029975497 0.007713677
## 2017-04-01 0.05067709 0.02860089 0.07962339 0.019490184 0.092097044
## 2017-05-01 0.04549085 0.05910749 0.07832488 -0.066262936 0.065013865
## 2017-06-01 -0.03070679 -0.07954410 0.01504416 -0.015656835 -0.058174249
## 2017-07-01 0.05695028 -0.07425845 0.01927758 0.034635340 0.023956533
## GM JPM FB GS LVS
## 2017-02-01 0.006282471 0.076703572 0.040055139 0.08171992 0.007036923
## 2017-03-01 -0.040173551 -0.030677589 0.048030178 -0.07151304 0.077809309
## 2017-04-01 -0.010338944 -0.009563085 0.057726130 -0.02577077 0.046967349
## 2017-05-01 -0.020496612 -0.050321492 0.008053291 -0.05603207 0.002373231
## 2017-06-01 0.029472399 0.112599008 -0.003169226 0.05390053 0.080500551
## 2017-07-01 0.041555643 0.004376194 0.121009435 0.01545731 -0.024920983
## UPS MSFT NVDA HD SPY
## 2017-02-01 -0.030880355 -0.010363445 -0.070525657 0.053278040 0.039291552
## 2017-03-01 0.022344743 0.035625103 0.074767995 0.013249492 -0.003087082
## 2017-04-01 0.001490829 0.039477599 -0.042504072 0.069604559 0.014319755
## 2017-05-01 -0.013865570 0.020157530 0.383988680 -0.016591962 0.014112937
## 2017-06-01 0.051904324 -0.007372485 0.002509212 0.005058211 0.001491244
## 2017-07-01 -0.002712664 0.054693138 0.124169897 -0.024771971 0.025530862
## SP500TR EQPORT
## 2017-02-01 0.039708179 0.028646780
## 2017-03-01 0.001164762 0.025037797
## 2017-04-01 0.010270539 0.031017154
## 2017-05-01 0.014072531 0.039614519
## 2017-06-01 0.006241750 0.002413334
## 2017-07-01 0.020562804 0.030538293
Dos formas de calcular la volatilidad del portafolio, ya sea directamente de la serie de sus retornos o indirectamente mediante la matriz de covarianzas de los activos y sus pesos en el portafolio:
print(sd(r$EQPORT))
## [1] 0.05623999
print(sqrt(w %*% cov(r[, 1:(length(tickers)-1)]) %*% w))
## [,1]
## [1,] 0.05623999
Ahora calcularé los retornos promedio anualizados y las volatilidades anualizadas, dado que así se identifican más en la industria:
ann_r <- apply(r, 2, mean)*12
print(round(ann_r, 4))
## AAPL AMZN LMT NFLX TSLA WMT SBUX MCD DIS GOOG
## 0.4444 0.3971 0.1398 0.4069 0.9100 0.2367 0.2175 0.1852 0.1772 0.2264
## GM JPM FB GS LVS UPS MSFT NVDA HD SPY
## 0.1297 0.1646 0.2317 0.0978 0.1234 0.1873 0.3515 0.5042 0.2160 0.1598
## SP500TR EQPORT
## 0.1615 0.2754
ann_sd <- apply(r, 2, sd)*sqrt(12)
print(round(ann_sd, 4))
## AAPL AMZN LMT NFLX TSLA WMT SBUX MCD DIS GOOG
## 0.3128 0.2938 0.2255 0.3570 0.7226 0.1870 0.2334 0.1785 0.2902 0.2199
## GM JPM FB GS LVS UPS MSFT NVDA HD SPY
## 0.3543 0.2520 0.2974 0.3039 0.3134 0.2977 0.1768 0.4425 0.2192 0.1687
## SP500TR EQPORT
## 0.1652 0.1948
Veamos ahora algunas gráficas:
plot(ann_r ~ ann_sd, ylim = c(min(ann_r), max(ann_r)*1.05))
text(ann_r ~ ann_sd, labels = names(ann_r), pos = 3, cex = 0.6, offset = 0.3)
rsd <- data.frame(ann_sd, ann_r)
ggplot(rsd, aes(x = ann_sd, y = ann_r)) +
geom_point() +
geom_text(label = rownames(rsd), size = 3, nudge_y = -0.01)
La función lm( )
ejecuta una regresión lineal. Los comandos que muestro después son varias formas de obtener información de la regresión.
reg <- lm(formula = EQPORT ~ SP500TR, data = r)
reg
##
## Call:
## lm(formula = EQPORT ~ SP500TR, data = r)
##
## Coefficients:
## (Intercept) SP500TR
## 0.00784 1.12224
summary(reg)
##
## Call:
## lm(formula = EQPORT ~ SP500TR, data = r)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.033593 -0.012054 -0.000379 0.010709 0.056714
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.007840 0.002656 2.952 0.005 **
## SP500TR 1.122242 0.054144 20.727 <0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.01751 on 45 degrees of freedom
## Multiple R-squared: 0.9052, Adjusted R-squared: 0.9031
## F-statistic: 429.6 on 1 and 45 DF, p-value: < 0.00000000000000022
confint(reg)
## 2.5 % 97.5 %
## (Intercept) 0.002491114 0.01318965
## SP500TR 1.013189227 1.23129433
anova(reg)
## Analysis of Variance Table
##
## Response: EQPORT
## Df Sum Sq Mean Sq F value Pr(>F)
## SP500TR 1 0.131700 0.131700 429.6 < 0.00000000000000022 ***
## Residuals 45 0.013795 0.000307
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
print(reg$coefficients)
## (Intercept) SP500TR
## 0.007840381 1.122241778
print(head(reg$fitted.values))
## 2017-02-01 2017-03-01 2017-04-01 2017-05-01 2017-06-01 2017-07-01
## 0.052402558 0.009147526 0.019366409 0.023633164 0.014845134 0.030916819
print(head(reg$residuals))
## 2017-02-01 2017-03-01 2017-04-01 2017-05-01 2017-06-01
## -0.0237557780 0.0158902709 0.0116507443 0.0159813554 -0.0124318007
## 2017-07-01
## -0.0003785258
summary(reg)$coefficients
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.007840381 0.002655905 2.952056 0.005000310637824433024345438525
## SP500TR 1.122241778 0.054144466 20.726805 0.000000000000000000000001184373
print(summary(reg)$adj.r.squared)
## [1] 0.9030764
print(summary(reg)$fstatistic)
## value numdf dendf
## 429.6004 1.0000 45.0000
Si graficamos el objeto que obtuvimos de la regresión, obtenemos 4 gráficas: * 1. Residuals vs Fitted: Los puntos deberían estar balanceados * 2. QQ Plot: La línea debería ser derecha y a 45º indicando normalidad * 3. Scale-Location: Si no se ve constante, hay heterosedasticidad * 4. Leverage: Para detectar “outliers” influyentes (afuera de la linea punteada)
plot(reg)
Ahora una gráfica de dispersión (“scatterplot”) con la línea de regresión:
ggplot(data = r, aes(x = SP500TR, y = EQPORT)) +
geom_point(color = "blue") +
labs(title = "EQPORT vs SP500TR", y = "EQPORT", x = "SP500TR") +
geom_abline(aes(intercept = reg$coefficients[1], slope = reg$coefficients[2]),
color = "green", size = 1) +
geom_smooth(method = "lm", se = F, color = "magenta", size = 1)
## `geom_smooth()` using formula 'y ~ x'
Podemos replicar cualquier cálculo de los anteriores utilizando álgebra de matrices. Algunos ejemplos:
x <- cbind(1, r$SP500TR)
y <- r$EQPORT
# betas == coefficients
betas <- solve(t(x) %*% x) %*% t(x) %*% y
betas
## EQPORT
## X1 0.007840381
## SP500TR 1.122241778
# yhat == fitted values
yhat <- x %*% betas
head(yhat)
## EQPORT
## [1,] 0.052402558
## [2,] 0.009147526
## [3,] 0.019366409
## [4,] 0.023633164
## [5,] 0.014845134
## [6,] 0.030916819
# e == residuals
e <- y - yhat
head(e)
## EQPORT
## 2017-02-01 -0.0237557780
## 2017-03-01 0.0158902709
## 2017-04-01 0.0116507443
## 2017-05-01 0.0159813554
## 2017-06-01 -0.0124318007
## 2017-07-01 -0.0003785258
# Beta of the first stock in the tickers array
reg <- lm(r[ , 1] ~ SP500TR, r)
betas <- data.frame(x = c(reg$coefficients[2],
summary(reg)$coefficients[2, 4],
summary(reg)$r.squared))
rownames(betas) <- c("Beta", "pval", "r2")
print(round(betas, 4))
## x
## Beta 1.2479
## pval 0.0000
## r2 0.4343
# Beta of the rest of the stocks in the tickers array
n <- ncol(r)
for (i in 2:n) {
reg <- lm(r[ , i] ~ SP500TR, r)
betas <- data.frame(betas, x = c(reg$coefficients[2],
summary(reg)$coefficients[2, 4],
summary(reg)$r.squared))
}
## Warning in summary.lm(reg): essentially perfect fit: summary may be unreliable
## Warning in summary.lm(reg): essentially perfect fit: summary may be unreliable
names(betas) <- names(r)
print(round(betas, 4))
## AAPL AMZN LMT NFLX TSLA WMT SBUX MCD DIS GOOG
## Beta 1.2479 1.1630 0.9484 0.8972 2.0832 0.4934 0.8323 0.5847 1.2193 0.9811
## pval 0.0000 0.0000 0.0000 0.0037 0.0007 0.0022 0.0000 0.0001 0.0000 0.0000
## r2 0.4343 0.4274 0.4825 0.1723 0.2267 0.1899 0.3469 0.2927 0.4816 0.5428
## GM JPM FB GS LVS UPS MSFT NVDA HD SPY
## Beta 1.4184 1.2176 1.2889 1.4843 1.4740 0.9737 0.7950 1.3365 0.9885 1.0173
## pval 0.0000 0.0000 0.0000 0.0000 0.0000 0.0001 0.0000 0.0004 0.0000 0.0000
## r2 0.4372 0.6370 0.5123 0.6506 0.6033 0.2919 0.5519 0.2488 0.5546 0.9924
## SP500TR EQPORT
## Beta 1 1.1222
## pval 0 0.0000
## r2 1 0.9052
Tenemos varias formas de calcular la Beta del portafolio. Algo muy sencillo es una regresión lineal en base a los retornos históricos del portafolio (método 1 a continuación), pero esto solo es válido si la composición actual del portafolio es la misma a la histórica (los pesos de los activos). Si el portafolio ha sufrido cambios, entonces la Beta corresponderá a ese portafolio histórico pero no necesariamente al actual hacia adelante (“going forward”).
\[ r_p = \beta_p*r_m + \epsilon \]
breg <- betas$EQPORT[1]
print(breg)
## [1] 1.122242
Los métodos 2 y 3 son apropiados independientemente de la historia del portafolio, dado que toman en cuenta la composición actual del portafolio. De estos dos, el método 2 es el más sencillo.
bwtd <- sum(w * betas[1, 1:(length(tickers)-1)])
print(bwtd)
## [1] 1.122242
bcov <- cov(r$EQPORT, r$SP500TR) / var(r$SP500TR)
print(bcov)
## SP500TR
## EQPORT 1.122242