Material de Estudio Intermedio - Python UCEMA - Versión de Preliminar
Material de Estudio Intermedio - Python UCEMA - Versión de Preliminar
NOTA: cuando importamos una librería lo que hacemos es lo sugerido por la palabra importar, sin
embrago, esto es una posibilidad porque la librería se encuentra en el paquete anaconda. Cuando la
librería deseado no está incluida, es necesario escribir el siguiente código antes de importarla:
𝑝𝑖𝑝 𝑖𝑛𝑠𝑡𝑎𝑙𝑙 𝑛𝑜𝑚𝑏𝑟𝑒
Donde “nombre” es el nombre de la librería.
INDICE
Finalmente, entre las funciones que permiten introducir el azar en listas con strings, veremos dos
funciones más. La primera es choice(), y permite elegir al azar un string o elemento de la lista que
se incorpora como argumento. La segunda es shuffle(), y modifica el orden de la lista de forma
aleatoria. Veamos cómo se escribe el código de cada una:
Choice(). 𝑟𝑎𝑛𝑑𝑜𝑚. 𝑐ℎ𝑜𝑖𝑐𝑒(𝑎)
Shuffle().𝑟𝑎𝑛𝑑𝑜𝑚. 𝑠ℎ𝑢𝑓𝑓𝑙𝑒(𝑎)
Como puede apreciarse, este último par de funciones no devuelven un resultado, pero sí modifican
la lista en cuestión, que en este caso llamamos “a”.
Colecciones de datos
Son un conjunto de datos que comparten características ¿Para qué sirven? Muchas veces
tendremos series de datos, tal vez precios, tal vez volumen, tal vez fechas, etcétera, y en lugar
asignar una variable a cada dato, será mejor utilizar colecciones, donde guardaremos todos estos
datos en una única variable. En este sentido, se puede decir, que todas las colecciones son vectores.
Un detalle no menor, es que un string es una colección de caracteres y, del mismo modo que
recortamos los strings para acceder a fracciones de ellos, podremos recordar las colecciones para
acceder a sus contenidos (el procedimiento es el mismo).
¿Cuándo necesito una colección en lugar de muchas variables?
Estructuras de colección en Python
Estructuras Nativas. Estas ya vienen incorporadas en el lenguaje de Python.
o Generadores (funciones). Estos permiten completar una colección sin necesidad de
crear un objeto a la vez.
o Listas (ordenados, no únicos, modificable). Son colecciones de elementos
ordenados que no necesariamente son únicos. Además, las listas son objetos
“mutables”, es decir, las listas pueden modificarse al cambiar al menos uno de los
objetos que las integran. La gracia de las listas es que sus objetos sean homogéneos
o, de un mismo tipo de variable.
o Sets (no ordenados, únicos, modificable). Los elementos que lo conforman no
están ordenados y son únicos, pero pueden modificarse como las listas. Los sets
suelen utilizarse como conjuntos en lógica.
o Tuplas (ordenados, no únicos, no modificable). Son similares a las listas, su única
particularidad, y que las diferencia de ellas, es que sus objetos no pueden
modificarse.
o Diccionarios (Estructuras clave valor, cada elemento tiene un etiqueta). Este tipo
de colección de objetos permite almacenar datos en forma de llave y valor. En los
diccionarios, cada objeto tiene una llave (key) y un valor (value).
Estructuras no nativas. Estas no vienen incorporadas en el lenguaje de Python y deben
llamarse a través de librerías, no obstante, estas toman las colecciones nativas y les otorgan
funciones. A continuación se mencionan tres:
o Numpy arrays. Utiliza la librería numpy, y son objetos multidimensionales y sirven
para trabajar con matrices numéricas.
o DataFrames. Utiliza la librería numpy, y son objetos o matrices bidimensionales
que pueden llevar etiquetas (nombres) en sus filas y columnas. Esto permite
trabajar con series de tiempo, por ejemplo, con un cuadro con fechas, precios de
cierre, precios de cierre ajustados, etcétera.
o Dask Arrays/DataFRames.
Listas
Una lista se crea utilizando corchetes [ ] y colocando dentro los objetos de interés, además, estos
objetos deben separarse por una coma. Estos elementos pueden ser de diferentes tipos, string,
integer, float, booleano… Veamos varios ejemplos:
Las listas también pueden crearse usando el método list(), poniendo dentro del paréntesis los
objetos de interés. Por ejemplo:
Como puede verse, las listas son diferentes a una cadena de caracteres, pues en la lista cada objeto
se separa con una coma, mientras en una lista de caracteres no necesariamente, pues depende de lo
que se desea escribir.
En este sentido, las listas pueden clasificarse del siguiente modo:
Lista vacía. Una lista vacía no es más que escribir:
𝑙𝑖𝑠𝑡𝑎 = [ ]
Sintaxis y slicing de listas. Como se mencionó antes, los string que se trabajaron son
colecciones de caracteres, por ende, su fraccionamiento también sirve para tomar porciones
de listas. Recordemos que, para tomar una porción trabajamos con el nombre de la variable
e inmediatamente escribimos [desde: hasta: paso], por ejemplo:
Asignar valores a una lista. Supongamos que la lista trabajada es la siguiente:
𝑙𝑖𝑠𝑡𝑎 = [0,1,2,3,4,5,6,7,8,9,10]
Si escribimos lista[4], estaremos llamando al objeto en la posición 4 de la variable lista
llamada lista. Lo que se puede hacer, es modificar este objeto sobre escribiéndolo (lo
mismo se podría hacer con cualquier otro objeto en cualquier otra posición). Para hacer
esto debemos escribir:
𝑙𝑖𝑠𝑡𝑎[4] = "𝑗𝑢𝑎𝑛"
Por ende, la lista quedará así:
𝑙𝑖𝑠𝑡𝑎 = [0,1,2,3, ′𝑗𝑢𝑎𝑛′, 5,6,7,8,9,10]
Sumar listas. Esto lo hacemos simplemente sumándolas, por ejemplo, si continuamos
trabajando con la lista creada hace un momento, podemos sumar dos porciones de la
misma del siguiente modo:
𝑙𝑖𝑠𝑡[: 3] + 𝑙𝑖𝑠𝑡𝑎[: 2]
El resultado es:
𝑙𝑖𝑠𝑡𝑎𝑛𝑢𝑒𝑣𝑎 = [0,1,2,0,1]
Esta posibilidad es importante, pues permite expandir la cantidad de objetos dentro de una
lista. En el ejemplo de hace un momento se sumaron dos elementos nuevos, pero bien se
podría sumar sólo uno y, no sería necesario que éste único elemento sea de una lista ya
conocida. En este sentido, sólo basta escribir lo siguiente:
𝑙𝑖𝑠𝑡𝑎 + ["𝑎"]
Haciendo esto sumamos una posición más hacia el final de la lista, cuyo objeto es un
string. El resultado es:
𝑙𝑖𝑠𝑡𝑎 = [0,1,2,3,4,5,6,7,8,9,10, ′𝑎′]
La conclusión de esto es que para sumar nuevas posiciones a una lista, lo que se adhiera
tiene que ser una lista, por ello o es una variable tipo lista o, es una colección escrita entre
corchetes.
Sumar listas es simple, sólo debemos escribir el código como si estuviésemos sumando
números o variables tipo int o float. La particularidad de las listas es que el resultado es la
suma de ubicaciones, es decir, si las listas sumadas tienen n y m ubicaciones, entonces la
nueva lista o el resultado, tendrá n+m ubicaciones. Asimismo, la suma de las lista implica
un ensamble, o sea, las m ubicaciones serán colocadas luego de las n ubicaciones. Por
ende, la suma de listas es en realidad una concatenación de listas.
Supongamos que tenemos la siguiente lista:
𝑙𝑖𝑠𝑡𝑎 = [0,1,2,5,6,7]
𝑙𝑖𝑠𝑡𝑎2 = [′ 𝑎′ , ′𝑏′]
𝑝𝑟𝑖𝑛𝑡(𝑙𝑖𝑠𝑡𝑎 + 𝑙𝑖𝑠𝑡𝑎2)
[1,2,3,5,6,′ 𝑎′ , ′𝑏′]
Imaginemos ahora lo siguiente:
𝑙𝑖𝑠𝑡𝑎 = [0,1,2,5,6,7]
𝑙𝑖𝑠𝑡𝑎2 = [′ 𝑎′ ,′ 𝑏 ′ , [1,2,3]]
𝑙𝑖𝑠𝑡𝑎3 = 𝑙𝑖𝑠𝑡𝑎 + 𝑙𝑖𝑠𝑡𝑎2
𝑝𝑟𝑖𝑛𝑡(𝑙𝑖𝑠𝑡𝑎3)
[1,2,3,5,6,′ 𝑎′ ,′ 𝑏 ′ , [1,2,3]]
¿Cómo hacemos para llamar algún objeto de la última ubicación? En otras palabras, el
objeto de la última ubicación es 7, y es una lista cuyo valor es [1,2,3], entonces ¿Cómo
hacemos para llamar alguno de sus valores? Primero debemos conocer su ubicación dentro
de la lista3, sabemos que es la ubicación 7, no obstante, si no conocemos la cantidad de
objetos que hay, pero sí sabemos que está en la última posición, podemos escribir
𝑙𝑖𝑠𝑡𝑎3[−1]. Luego, lo que acabo de escribir es otra lista, por ende, debemos abrir
nuevamente corchetes, por lo tanto, la solución es, si buscamos el valor 2, la siguiente:
𝑙𝑖𝑠𝑡𝑎3[−1][1]
El método count() sirve para conocer la cantidad de veces que un objeto en particular está
contenido en la colección. Por ejemplo:
𝑙𝑖𝑠𝑡𝑎 = [1,1,2,3,4,4,4,5,6,4,4]
𝑙𝑖𝑠𝑡𝑎. 𝑐𝑜𝑢𝑛𝑡(4)
El resultado devuelto es cinco, pues el número 4 aparece cinco veces. La función sum()
suma los elementos dentro de la colección, devolviendo como resultado dicha suma, para
ello, debemos colocar la lista como argumento.
Finalmente, dentro las funciones que no devuelven un resultado (función sin return) se encuentran
aquellas que alteran al objeto (una función que no devuelve resultado, y tampoco modifica al
objeto, no hace nada). Ejemplos son sort(), append(), e insert(). El método sort() permite ordenar
una lista de forma ascendente o descendente. Es así como aplicado a una lista, la cambia de forma
permanente. Si la lista ya está ordenada, entonces la consola devuelve el valor “None”. Veamos un
ejemplo sin argumentos:
Este método cuenta con dos argumentos opcionales, reverse y key. El segundo es una función que
establece el criterio para ordenar la lista, por ejemplo, si se escribe key=len, entonces se ordena la
lista por longitud, del más pequeño al más largo. Veamos un ejemplo:
Reverse, por otro lado, asume un valor booleano (True o False). Por ende, si escribimos
reverse=True, la lista se ordenará en orden alfabético inverso. Veamos un ejemplo:
El método append(), por otro lado, permite agregar un elemento al final de la lista. Este nuevo dato
puede ser de cualquier tipo, por ende, también puede ser otra lista (no obstante, esta se agregará
como un objetivo, no como un conjunto de objetos). Cuando este método sea aplicado, recuerde
que no devuelve un resultado, se ejecuta y opera, pero no devuelve ningún resultado. Su sintaxis es
la siguiente:
Por ejemplo:
Un detalle alrededor de append() es que, cada vez que sea ejecutado agregará el objeto que está
entre paréntesis sin eliminar el que ya agregó en las ejecuciones anteriores (a menos que se vuelva
a definir la lista original previamente).
Finalmente, el método insert() permite agregar un objeto en la lista, sin importar la posición. Su
sintaxis es la siguiente:
Como ocurre con append(), si el método es ejecutado una y otra vez sin volver a partir de la lista
original, se estará agregando constantemente lo indicado con insert(). En otras palabras, si por
ejemplo la lista es la siguiente:
𝑙𝑖𝑠𝑡𝑎 = [1,2,3,5,6]
Y escribimos
𝑙𝑖𝑠𝑡𝑎. 𝑖𝑛𝑠𝑒𝑟𝑡(2,′ 𝑝𝑒𝑝𝑒 ′ )
𝑝𝑟𝑖𝑛𝑡(𝑙𝑖𝑠𝑡𝑎)
Tendremos lo siguiente:
[1,2,′ 𝑝𝑒𝑝𝑒 ′ , 3,5,6]
Si volvemos a ejecutar el método, pero no la lista, al imprimir el resultado obtendremos:
[1,2,′ 𝑝𝑒𝑝𝑒 ′ ,′ 𝑝𝑒𝑝𝑒 ′ , 3,5,6]
Finalmente, también se pueden agregar otras listas, sólo que no serán concatenadas, sino que se
ubicarán en el orden indicado y estarán identificadas como listas, por ejemplo:
𝑙𝑖𝑠𝑡𝑎2 = [′𝑎′, ′𝑏′]
𝑙𝑖𝑠𝑡𝑎. 𝑖𝑛𝑠𝑒𝑟𝑡(2, 𝑙𝑖𝑠𝑡𝑎2)
𝑝𝑟𝑖𝑛𝑡(𝑙𝑖𝑠𝑡𝑎)
[1,2, [′𝑎′, ′𝑏′],3,5,6]
Generadores
Estos son métodos que permiten escribir una colección sin la necesidad de escribir cada uno de sus
elementos. En otras palabras, son métodos que permiten llamar aquello que se quiere generar, pero
sin generarlo. Imaginemos, por poner un sencillo ejemplo, que queremos una lista con un millón de
objetos, cuyos valores pueden ser cualquier cosa. En lugar de escribirlos, podemos utilizar un
método que genere el millón de valores y los ubique en la lista. A esto se le llama generador.
El método que veremos es range(). Siguiendo el ejemplo, podemos escribir:
𝑟𝑎𝑛𝑔𝑒(1,1_000_000)
NOTA: El guión bajo se puede usar como separador de miles.
A priori, esto no es más que un rango entre 1 y un millón, de hecho, su tipo es range. Para
convertirlo en lista debemos escribir lo siguiente:
𝑙𝑖𝑠𝑡(𝑟𝑎𝑛𝑔𝑒(1,1_000_000))
Con esto ya creamos la lista con el millón de objetos, cuyos valores van desde el 1 hasta el millón.
Los argumentos de este método son:
𝑟𝑎𝑛𝑔𝑒(𝑑𝑒𝑠𝑑𝑒, ℎ𝑎𝑠𝑡𝑎, 𝑠𝑎𝑙𝑡𝑜)
El “desde” es inclusive, y el “hasta” es exclusive. El “salto” es un argumento donde se coloca el
tamaño del salto deseado, por ejemplo, si ponemos 1, se creará un rango que vaya de uno en uno,
en cambio, si colocamos 2, el rango irá de dos en dos.
Clear() y del son funciones que sirven para vaciar de contenido al set.
También está el método update(), que inserta todos los elementos de un set en otro.
.issuperset(). Este trabaja de forma similar al anterior, pues devuelve un valor booleano de acuerdo
a si un set es contenedor de otro.
.len(), podemos conocer la longitud del Set o, la cantidad de objetos que tiene (objetos únicos).
¿Para qué sirven los operadores? El | permite realizar la unión de dos sets, es decir, es equivalente
al uso del método union():
Para saber si un objeto está o no en el Set, usamos el operador in. Si el mismo está, entonces el
valor será verdadero o True:
SET - Intersección de colecciones
Aunque con las listas se puede obtener la intersección, siempre será necesario utilizar al menos las
listas por comprensión. En cambio, si utilizamos sets, sólo deberemos utilizar operadores lógicos.
Veamos un ejemplo, supongamos los siguientes sets:
𝑠1 = {1,2,3,4,5,6}
𝑠2 = {5,6,7,8,9,10}
La intersección serían los elementos cuyo valor es 5 y 6. El código para obtener esto es el
siguiente:
Intersección: 𝑠1 & 𝑠2
{5,6}
Unión: 𝑠1 | 𝑠2
{1,2,3,4,5,6,7,8,9,10}
XOR: 𝑠1 ^ 𝑠2
{1,2,3,4,7,8,9,10}
Asimismo, si queremos que los elementos estén en un set y no en otro, por ejemplo, si deseamos
conocer los elementos del set 1 que no están en el set 2, entonces escribimos:
𝑠1 − 𝑠2
El resultado es:
{1,2,3,4}
Ejercicio de intersección
El ejercicio propuesto es escribir el código que cree el diagrama de “ven” para tres sets, y que en su
interior aparezcan los elementos que a cada subconjunto corresponden. Los tres sets fueron
definidos por el profesor, resta escribir el código para lograr lo mencionado. La solución se obtuvo
“googleando”, se encontró una web que explica cómo funciona el código de esta librería. A partir
de la lectura de esta página, se escribió el siguiente código, logrando el objetivo propuesto:
Tuplas
Las tuplas se escriben utilizando paréntesis, dentro de los cuales están los objetos separados por
comas.
Las tuplas pueden contener un objeto vacío y, si se les aplica la función len(), se obtiene la cantidad
de objetos dentro de ellas. En el caso de una tupla vacía, el resultado será cero:
En este sentido, si una Tupla tiene sólo un objeto, será necesario que vaya acompañado de una
coma, de lo contrario, Python no la reconocerá como Tupla.
Como ocurre con la cadena de caracteres, podemos referirnos a los elementos de las Tuplas
utilizando corchetes []. Por ejemplo:
𝑡1 = (0,1,2,3,4,5)
𝑡1[2]
2
Como se mencionó antes, las tuplas se caracterizan por tener objetos que no pueden modificarse,
así, si deseamos reemplazar el objeto de la posición 2 por ‘juan’, obtendremos un error. En este
sentido, las tuplas tampoco admiten la incorporación de nuevos objetos (usando por ejemplo la
función append()). ¿Por qué usar una tupla entonces? Para asegurarnos que su contenido no pueda
modificarse por un error al escribir el código. En paralelo, las tuplas pueden concatenarse dando
nacimiento a una nueva Tupla, es decir, la concatenación no modifica a las tuplas concatenadas.
𝑙𝑖𝑠𝑡𝑎 = [ ]
𝑓𝑜𝑟 𝑥 𝑖𝑛 𝑟𝑎𝑛𝑔𝑒(10):
𝑣𝑎𝑙𝑜𝑟 = 𝑥 ∗ 2 + 1
𝑙𝑖𝑠𝑡𝑎. 𝑎𝑝𝑝𝑒𝑛𝑑(𝑣𝑎𝑙𝑜𝑟)
El resultado es el siguiente:
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
Observe que el generador rang() sirve en este caso para establecer la cantidad de iteraciones que
deseamos que sucedan o, la cantidad de objetos que deseamos se cree. Por otro lado, a diferencia de
la lista por comprensión, el bucle for permite modificar los objetos de la lista de diversas formas,
pues es posible escribir varias líneas de código para ello, mientras que la lista por comprensión
sólo permite un tipo de modificación.
NOTA: “CAGR medio del activo es del 5,4%”. El CAGR son las siglas en inglés que significan
“Compound annual Growth rate” o, en español, “Tasa anual de crecimiento compuesta”. En otras
palabras, es una tasa efectiva anual (TEA). En este caso, la frase se obtuvo de un ejercicio que
asume implícitamente que esta tasa tiene una frecuencia de capitalización de diaria, en otras
palabras, a partir de una serie de precios diaria, se calculó el rendimiento diario promedio y luego
se calculó su TEA equivalente. En estos casos, siempre debemos recordar que se asume que
existen, en promedio 252 ruedas al año, por ende, el año bursátil tiene esta cantidad total de días.
Bucle for – Productorias y sumatorias
Además de crear objetos para completar una lista, el bucle for también es útil para calcular
sumatorias y productorias. A continuación lo aplicaremos para calcular el desvío estándar, para
esto, supondremos el siguiente conjunto de diez precios diarios:
𝑙𝑖𝑠𝑡𝑎𝑝𝑟𝑒𝑐𝑖𝑜𝑠 = [100.29, 99.95, 100.19, 99.21, 99.94, 99.74, 99.23, 98.78, 97.89, 99.66]
La fórmula del desvío estándar es:
𝑗
1
𝜎=√ ∑(𝜇 − 𝑥𝑖 )2
𝑛−1
𝑖=1
Los diccionarios son dinámicos, es decir, pueden crecer o decrecer (añadiendo o eliminando
objetos); son indexados, pues sus objetos son accesibles a través del key; y son anidados, pues
pueden contener otra colección en su campo value. Una importante consideración respecto a estas
características, es que cada valor debe estar asociado con una única clave, pero, una clave puede
asociarse a muchos valores. En este sentido, para que una clave pueda asociarse a muchos valores,
estos deben pertenecer a una misma lista, de lo contrario, el diccionario sobre escribirá la clave
asignándole como valor el último que se ha introducido.
Para acceder a los valores contenidos en los diccionarios, debemos aplicar la misma lógica que la
utilizada con las listas y tuplas, aunque en este caso, en lugar de llamar una ubicación, llamamos la
clave. En este sentido, se puede obtener un valor particular llamándolo con su etiqueta, para esto se
llama al diccionario y se utilizan corchetes. Veamos un ejemplo:
𝑑𝑖𝑐["𝐴𝐿𝑈𝐴"]
Obteniendo 29.35. Este modo de acceder a los valores implica que el diccionario no cuente con
varios valores para una misma etiqueta o clave, pues de lo contrario, esta línea de código nos traerá
de vuelta la lista que contiene todos los elementos vinculados con esta etiqueta. Por ejemplo, si
llamamos a:
𝑑𝑖𝑐["𝐵𝑌𝑀𝐴"]
Obtendremos la lista [290,299]. Para poder acceder a uno de estos datos, la lógica a aplicar es la
misma, a continuación se escribe la línea de código correspondiente:
𝑑𝑖𝑐["𝐵𝑌𝑀𝐴"][1]
De este modo estaremos llamando el valor ubicado en la posición uno de la lista vinculada a la
clave “BYMA”, es decir, obtendremos como devolución el valor 299.
En línea con la búsqueda de valores en el diccionario, si introducimos una clave que en el mismo
no existe, el resultado que tendremos será un error que indica la inexistencia de dicha clave. El
problema con los errores es que cortan la ejecución del código, por ende, debemos evitar estas
situaciones. En este caso, para evitarlo utilizaremos la función get(), quien cumple la misma
función que los código de hace un momento, nos permite acceder al valor que corresponde a
determinada clave. Al utilizar esta función introduciendo una clave que no existe, la respuesta es
“none”. Esto se explica en la siguiente sección.
En penúltimo lugar, para agregar nueva información en un diccionario que ya existe, debemos
utilizar corchetes [ ], dentro de los cuales escribiremos la clave, luego usaremos el “=”, y seguido a
esto introduciremos su clave. Veamos un ejemplo:
Finalmente, si deseamos modificar la información del diccionario, esto es, si deseamos cambiar el
valor asociado a una clave en particular, sólo debemos escribir su clave y colocar el nuevo valor.
Por ejemplo, si el diccionario es “jota” y la clave es “aire” y el valor es 16000, y a éste queremos
modificarlo por 20000, debemos escribir lo siguiente:
𝑗𝑜𝑡𝑎[′ 𝑎𝑖𝑟𝑒 ′ ] = 20_000
Métodos y funciones para los diccionarios
Como se explicó antes, para acceder a los valores de los diccionarios utilizamos corchetes [ ] o la
función get(). En cualquier caso, lo que se busca es el value a partir del key. Veamos un ejemplo:
Utilizando get(), para acceder a un valor particular de una lista, utilizamos la misma lógica,
escribimos lo siguiente:
𝑑𝑖𝑐. 𝑔𝑒𝑡(′𝐵𝑌𝑀𝐴′)[0]
Vimos al final de la sección anterior que, si utilizamos la función get() para conseguir el valor de
una clave que no existe, obtendremos como resultado “none”. Este resultado podemos cambiarlo,
para ello, al llamar al valor utilizando la clave, el siguiente argumento debe ser la frase que
deseamos aparezca en lugar de “none”. Veamos esto:
𝑑𝑖𝑐. 𝑔𝑒𝑡(′𝐴𝑃𝑃𝐿′ ,′ 𝐿𝑎 𝑟𝑢𝑏𝑖𝑎 𝑑𝑒 𝑎𝑙 𝑙𝑎𝑑𝑜 𝑒𝑠𝑡á 𝑚𝑢𝑦 𝑙𝑖𝑛𝑑𝑎′ )
Al ejecutar este código, y como APPL no está en la colección dic, aparecerá el siguiente resultado
en lugar de “none”:
𝐿𝑎 𝑟𝑢𝑏𝑖𝑎 𝑑𝑒 𝑎𝑙 𝑙𝑎𝑑𝑜 𝑒𝑠𝑡á 𝑚𝑢𝑦 𝑙𝑖𝑛𝑑𝑎
Veamos los métodos keys(), values(), e ítems(). Estas funciones permiten recorrer al diccionario.
Veamos cada una:
o Keys(). Este método permite conocer todas las claves presentes en el diccionario. Este
método devuelve un vector del tipo especial de diccionario (dict_keys). Veamos un
ejemplo utilizando el diccionario de la sección anterior:
𝑑𝑖𝑐. 𝑘𝑒𝑦𝑠()
([′ 𝐴𝐿𝑈𝐴′ ,′ 𝐵𝐵𝐴𝑅 ′ ,′ 𝐵𝑀𝐴′ , ′𝐵𝑌𝑀𝐴′ ])
o Values(). Del mismo modo que el método anterior, este permite conocer todos los valores
del diccionario en cuestión. Este método devuelve un vector del tipo especial de
diccionario (dict_values). Veamos un ejemplo utilizando el diccionario de la sección
anterior:
𝑑𝑖𝑐. 𝑣𝑎𝑙𝑢𝑒𝑠()
([29.35, 120.85, 265.2, [290,299]])
o Items(). Como los métodos anteriores, este también devuelve un tipo especial de
diccionario (dict_items), pero en este caso será uno compuesto de elementos que son
tuplas conformadas por dos objetos: el string (clave) y el valor. Veamos un ejemplo:
𝑑𝑖𝑐. 𝑖𝑡𝑒𝑚𝑠()
([('ALUA', 29.35), ('BBAR',120.85), ('BMA', 265.2), (′BYMA', [290,299])])
Recorriendo un diccionario – Bucle for
Recordemos el diccionario ejemplo de las dos secciones anteriores:
𝑑𝑖𝑐 = {"𝐴𝐿𝑈𝐴": 29.35, "𝐵𝐵𝐴𝑅": 120.85, "𝐵𝑀𝐴": 265.2, "𝐵𝑌𝑀𝐴": [290,299]}
Para recorrer las claves, podemos escribir el siguiente código:
𝑓𝑜𝑟 𝑥 𝑖𝑛 𝑑𝑖𝑐 ∶
𝑝𝑟𝑖𝑛𝑡(𝑑𝑖𝑐)
Quien arroja el siguiente resultado:
𝐴𝐿𝑈𝐴
𝐵𝐵𝐴𝑅
𝐵𝑀𝐴
𝐵𝑌𝑀𝐴
Pero si deseamos recorrer los valores, sólo debemos aplicar lo visto hasta ahora, veamos:
𝑓𝑜𝑟 𝑥 𝑖𝑛 𝑑𝑖𝑐 ∶
𝑝𝑟𝑖𝑛𝑡(𝑑𝑖𝑐[𝑥])
Devolviéndonos lo siguiente:
29.35
120.85
265.2
[290, 299]
Este par de modos logra el cometido, pero es ineficiente. El siguiente modo es mejor en estos
términos:
𝑓𝑜𝑟 𝑡𝑖𝑐𝑘𝑒𝑟, 𝑝𝑟𝑒𝑐𝑖𝑜𝑠 𝑖𝑛 𝑑𝑖𝑐. 𝑖𝑡𝑒𝑚𝑠() ∶
𝑝𝑟𝑖𝑛𝑡(𝑡𝑖𝑐𝑘𝑒𝑟, 𝑝𝑟𝑒𝑐𝑖𝑜𝑠)
El resultado es el siguiente:
¿Cómo funciona este código? Las variables internas que nombramos, ticker y precios, se asocian a
la clave y al valor de la lista con tuplas que llamamos al escribir dic.items(). Siguiendo esta lógica,
bien podríamos haber declarado sólo una variable interna, por ejemplo ‘caca’, en este caso, el
intérprete la tomaría como una tupla y, por ende, al imprimirla nos devolvería justamente una tupla
con dos objetos en cada iteración. Esto es así porque recuerdo que al escribir dic.items() estamos
creando una lista con objetos que son tuplas. Veamos el ejemplo:
𝑓𝑜𝑟 𝑐𝑎𝑐𝑎 𝑖𝑛 𝑑𝑖𝑐. 𝑖𝑡𝑒𝑚𝑠():
𝑝𝑟𝑖𝑛𝑡(𝑐𝑎𝑐𝑎)
El resultado es:
(′𝐴𝐿𝑈𝐴′ , 29.35)
(′𝐵𝐵𝐴𝑅 ′, 120.85)
(′𝐵𝑀𝐴′, 265.2)
(′𝐵𝑌𝑀𝐴′ , [290, 299])
Ahora, para acceder a cada objeto de cada tupla, sólo debemos indicar su ubicación y, como son
sólo dos objetos, la ubicación será 0 y 1. Veamos el ejemplo:
𝑓𝑜𝑟 𝑐𝑎𝑐𝑎 𝑖𝑛 𝑑𝑖𝑐. 𝑖𝑡𝑒𝑚𝑠():
𝑝𝑟𝑖𝑛𝑡(𝑐𝑎𝑐𝑎[0], 𝑐𝑎𝑐𝑎[1])
Con esto obtenemos el resultado siguiente:
Función zip
Esta función permite empaquetar diferentes listas y diferentes tipos de colecciones. Por ejemplo,
pensemos que tenemos tres tipos de listas, una con los nombres de acciones, otra con sus precios de
cierre ajustados de la última rueda, y otra con los volúmenes operados durante la última rueda.
Concretamente, pensemos que tenemos lo siguiente:
𝑡𝑖𝑐𝑘𝑒𝑡𝑠 = [′ 𝐴𝐿𝑈𝐴′ , ′𝐵𝐵𝐴𝑅 ′ , ′𝐵𝑀𝐴′ , ′𝐵𝑌𝑀𝐴′]
𝑝𝑟𝑒𝑐𝑖𝑜𝑠 = [1,2,3,4]
𝑣𝑜𝑙𝑢𝑚𝑒𝑛 = [100,120,130,140]
A estas listas podemos empaquetarlas con la función zip(), veamos:
𝑝𝑎𝑞𝑢𝑒𝑡𝑒 = 𝑧𝑖𝑝(𝑡𝑖𝑐𝑘𝑒𝑡𝑠, 𝑝𝑟𝑒𝑐𝑖𝑜𝑠, 𝑣𝑜𝑙𝑢𝑚𝑒𝑛)
Esta variable tipo zip no se puede imprimir con la función print(), pues es un objeto que aun no
está desplegado, por ello recurrimos al bucle for, tanto para recorrerlo como para imprimirlo:
𝑓𝑜𝑟 𝑡, 𝑝, 𝑣 𝑖𝑛 𝑝𝑎𝑞𝑢𝑒𝑡𝑒:
𝑝𝑟𝑖𝑛𝑡(𝑡, 𝑝, 𝑣)
Obteniendo:
El detalle con los paquetes, es que cada lista o colección que lo constituye, siempre se debe
corresponder con otro elemento de otra colección incorporada, es decir, si hay dos listas en el
paquete, estas deben tener la misma cantidad de objetos, lo mismo si hay tres listas, cuatro,
etcétera. Si una de estas listas tiene más elementos que las demás, el intérprete no considerará
dichos elementos, los eliminará. Por ejemplo, si tenemos tres listas, una con 3 elementos, otra con
8, y otra con 24 elementos, al empaquetarlas, tendremos un paquete con 3 elementos compuestos
de 3 objetos.
¿Cómo podemos armar un diccionario a partir de un paquete zip? Imaginemos que partimos de un
conjunto de listas, a estas las empaquetaremos, y finalmente convertiremos el paquete en un
diccionario. Veamos este proceso con el siguiente ejemplo:
NOTA: El guión bajo es un separador de mil, y “e1” es lo mismo que escribir 7*10**1, pero
escrito de forma más abreviada (por ende, e1=10**1).
Con las siguientes preguntas se pretende mostrar cómo resolver algunas cuestiones básicas, pero
principalmente, dejar en evidencia el trabajo a realizar al usar diccionarios. Este objetivo principal
permite valorar más el uso de DataFrame que se aborda en otra sección. Las preguntas son las
siguientes:
1. ¿Cómo podemos sumar la totalidad de los precios? Aquí veremos dos modos, iterando, y
aplicando los métodos de los diccionarios. Comencemos iterando, en este caso deberemos
crear una variable cuyo valor inicial sea cero, y a quien le asignaremos con cada iteración
el valor que corresponde a cada etiqueta. Por ende, en la iteración estaremos llamando a los
valores usando las claves. Veamos:
𝑠𝑢𝑚𝑎 = 0
𝑓𝑜𝑟 𝑥 𝑖𝑛 𝑑𝑖𝑐:
𝑎 = 𝑑𝑖𝑐[𝑥]
𝑠𝑢𝑚𝑎+= 𝑎
𝑝𝑟𝑖𝑛𝑡(𝑠𝑢𝑚𝑎)
El resultado es: 48.180.
La alternativa más eficiente es utilizar un método del diccionario, el value(). La idea detrás
de la siguiente línea de código es extraer los valores del diccionario, convertirlos en listas,
y sumarlos con la función sum(). Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑠𝑢𝑚(𝑙𝑖𝑠𝑡(𝑑𝑖𝑐. 𝑣𝑎𝑙𝑢𝑒𝑠())))
El resultado es el mismo: 48.180.
2. ¿Cómo podemos calcular el porcentaje de tenencia de cada activo? Veamos dos formas,
ambas son la continuación de las soluciones anteriores. En el caso del bucle for, la
solución planteada es otro bucle for. Primero creamos una lista vacía donde se contendrán
los porcentajes, luego creamos un bucle for para que se itere el recorrido de la lista, en este
bucle llamaremos a los valores utilizando las claves, y a cada valor lo dividiremos por el
total (suma) que calculamos antes. Además, en cada llamado, a cada cociente lo
multiplicaremos por 100 y lo redondearemos a 2 decimales, para posteriormente
adicionarlo como objeto en la lista vacía. Finalmente, imprimimos el resultado. Las líneas
de código son las siguientes:
𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒 = []
𝑓𝑜𝑟 𝑥 𝑖𝑛 𝑑𝑖𝑐:
𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒. 𝑎𝑝𝑝𝑒𝑛𝑑(𝑟𝑜𝑢𝑛𝑑(𝑑𝑖𝑐[𝑥]/𝑠𝑢𝑚𝑎 ∗ 100,2))
𝑝𝑟𝑖𝑛𝑡(𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒)
Respecto al segundo modo de resolver esto, podemos utilizar una lista por comprensión. La
idea es recorrer la lista obtenida a partir del método value(), y en ese recorrido, a cada valor
lo dividiremos por la suma obtenida antes (deberemos crear una variable que contenga esta
suma), y lo multiplicaremos por 100 y redondearemos a 2 decimales. Veamos:
𝑛 = [𝑟𝑜𝑢𝑛𝑑(𝑥 ∗ 100/𝑠𝑢𝑚𝑎, 2) 𝑓𝑜𝑟 𝑥 𝑖𝑛 𝑙𝑖𝑠𝑡(𝑑𝑖𝑐. 𝑣𝑎𝑙𝑢𝑒𝑠())]
𝑝𝑟𝑖𝑛𝑡(𝑛)
3. ¿Cómo podemos obtener el monto correspondiente a TLT? Sólo debemos escribir lo
siguiente:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑖𝑐[′ 𝑇𝐿𝑇′])
O, utilizamos la función get(). En este sentido, es recomendable utilizarlo como buena
práctica, pues recuerde que si el valor buscado no existe en el diccionario, la ejecución se
detendrá por un error, situación que se evita al utilizar get():
𝑝𝑟𝑖𝑛𝑡(𝑑𝑖𝑐. 𝑔𝑒𝑡(′𝑇𝐿𝐸′ ))
4. ¿Cómo obtenemos los tres activos cuya tenencia es la mayor? Este objetivo se puede
lograr ordenado de mayor a menor o, de menor a mayor el diccionario. También puede
obtenerse iterando, tomando el primer valor y preguntando si es mayor al siguiente, y en
caso negativo se toma el nuevo valor en reemplazo del anterior. Las líneas
correspondientes a estas soluciones no se escribirán aquí, pues como se comentó, la idea
principal es mostrar el esfuerzo que requiere trabajar con diccionarios, trabajo que puede
realizarse de forma más eficiente con DataFrame.
5. Imaginemos que tenemos 100 tenencias diferentes ¿Cómo conseguimos los tickets del
primer decil con mayor tenencia? ¿Y lo del percentil 6? La solución en este caso es
parecida a la ofrecida anteriormente, en otras palabras, es mejor trabajar con DataFrame.
o Min. Permite conocer el valor más pequeño dentro del vector o matriz. Veamos el caso del
vector:
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. min()
o Max. Permite conocer el valor más grande dentro del vector o matriz. Veamos el caso del
vector:
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. max()
o Ptp. Permite conocer el rango entre el valor máximo y mínimo del conjunto de elementos
que conforman al vector o matriz. Veamos el caso del vector:
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. ptp()
o Mean. Calcula la media o promedio de los valores que están en el vector o matriz. Veamos
el caso del vector:
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. mean()
o Std. Permite conocer el desvío estándar (poblacional) del conjunto de elementos que
componen al vector o matriz. Veamos el caso del vector:
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. std()
o Suma. Suma todos los objetos del array:
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. sum()
o Len. Esta es una función que devuelve la cantidad de objetos que constituyen al array:
len(𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝)
Las funciones siguientes no pueden utilizarse como métodos. Los ejemplos que se ilustran también
utilizan el vector de precios del ejemplo de la sección anterior:
o Median(). Sirve para obtener la mediana del conjunto de elementos que conforman al
vector o matriz. Veamos un ejemplo:
𝑛𝑝. 𝑚𝑒𝑑𝑖𝑎𝑛(𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝)
o Quantile(). Sirve para obtener un cuantil del conjunto de elementos que conforman al
vector o matriz. Veamos un ejemplo donde pedimos el cuartil inferior (podemos pedir el
que querramos):
𝑛𝑝. 𝑞𝑢𝑎𝑛𝑡𝑖𝑙𝑒(𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝 , 0,25)
o Cumsum(). Esta función genera un vector a partir de otro, cuyos elementos son la suma
acumulativa y secuencial de los elementos del vector original. Veamos un ejemplo:
𝑎 = [(1,2,3,4,5)]
𝑏 = 𝑛𝑝. 𝑐𝑢𝑚𝑠𝑢𝑚(𝑎)
𝑝𝑟𝑖𝑛𝑡(𝑏)
[(1,3,6,10,15)]
Los atributos siguientes se utilizan como métodos, pero a diferencia de los métodos, no se
escriben los paréntesis. Para su ejemplificación se continúa utilizando el vector de precios de la
sección anterior:
o Ndim. Permite conocer la dimensión del objeto, es decir si es vector será 1 (tiene sólo
columnas o sólo filas), y si es matriz será igual a 2 (tiene filas y columnas). Una matriz con
más de dos dimensiones es aquella compuesta por varias matrices, es decir, una matriz
dentro de otra (ejemplo de esto se ve en la siguiente sección). El código para conocer la
dimensión es el siguiente:
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. ndim
o Shape. Permite conocer la cantidad de filas y columna del objeto. Su resultado es del tipo
tupla.
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. shape
o Size. Permite conocer el número de elementos que conforman al vector o matriz.
𝑏𝑎𝑛𝑑𝑎𝑠𝑢𝑝. size
𝑖𝑚𝑝𝑜𝑟𝑡 𝑛𝑢𝑚𝑝𝑦 𝑎𝑠 𝑛𝑝
𝑖𝑚𝑝𝑜𝑟𝑡 𝑟𝑎𝑛𝑑𝑜𝑚
La librería random se aplica sobre la librería numpy. Debido a esto, sobre las funciones de
random se incorpora otro argumento que pide especificar la cantidad de elementos (size) de
acuerdo a la a la forma (shape). Con este nuevo argumento podremos crear matrices
multidimensionales ¿Para qué sirve hacerlo? En el contexto financiero, en una misma matriz
podemos trabajar con varios clientes por separado, donde cada dimensión es un cliente. Antes de
ilustrar esto con un ejemplo, veamos lo más simple, creemos un vector con diez elementos
generados aleatoriamente, pero que sigan una distribución normal estándar:
𝑛𝑝. 𝑟𝑎𝑛𝑑𝑜𝑚. 𝑛𝑜𝑟𝑚𝑎𝑙(0,1,10)
Con esto se realiza lo siguiente:
En este ejemplo tenemos un vector de tamaño diez (diez elementos), de una dimensión de 1. Si
deseamos ampliar la dimensión, por ejemplo a 3, e incorporar filas y columnas debemos escribir el
siguiente código:
𝑛𝑝. 𝑟𝑎𝑛𝑑𝑜𝑚. 𝑛𝑜𝑟𝑚𝑎𝑙(0,1, 𝑠𝑖𝑧𝑒 = (3,5,2))
Con este código se obtienen 3 matrices de orden 5x2 cada una, por ende, los argumentos de size
son: cantidad de matrices, cantidad de filas que cada una tiene, y cantidad de columnas que cada
una tiene. Este tipo de matrices se conoce como matrices multidimensionales. Ejecutando el
código, el resultado es el siguiente:
¿De qué sirve utilizar matrices multidimensionales? Como se mencionó antes, en el contexto de las
finanzas nos permite trabajar con varios clientes en simultáneo, donde cada uno tiene la misma
cantidad carteras (filas, en este caso, cinco), y cada cartera tiene la misma cantidad de activos
(columnas, en este caso, dos)
Del mismo modo se puede utilizar la distribución uniforme. En este caso, además de especificar
la cantidad de dimensiones, filas, y columnas, también será necesario definir los valores máximos y
mínimos. El código es el siguiente:
𝑛𝑝. 𝑟𝑎𝑛𝑑𝑜𝑚. 𝑢𝑛𝑖𝑓𝑜𝑟𝑚(𝑙𝑜𝑤 = −10, ℎ𝑖𝑔ℎ = 10, 𝑠𝑖𝑧𝑒 = (2,5)). 𝑟𝑜𝑢𝑛𝑑(3)
Se obtiene una matriz de orden 2x5, cuyos valores siguen una distribución uniforme y no pueden
ser mayores a 10 ni menos a -10:
Finalmente, si a todas las ejecuciones de números aleatorios se les precede con una semilla,
entonces cada vez que las ejecutemos, los resultados serán siempre iguales, de lo contrario, en cada
oportunidad tendremos diferentes valores para cada uno de los elementos. El código de la semilla
que debe preceder al código de número aleatorio es el siguiente:
𝑛𝑝. 𝑟𝑎𝑛𝑑𝑜𝑚. 𝑠𝑒𝑒𝑑(0)
Generadores numpy
Arange.
𝑛𝑝. 𝑎𝑟𝑎𝑛𝑔𝑒(0,20)
Linspace.
𝑛𝑝. 𝑙𝑖𝑛𝑠𝑝𝑎𝑐𝑒(0,2,10)
𝑗
1
𝜎=√ ∑(𝜇 − 𝑥𝑖 )2
𝑛−1
𝑖=1
NOTA. Concepto: Instanciar un objeto. Esta frase significa que se va a crear un objeto de
determinada clase, con determinadas características.
Por otro lado, ambos argumentos admiten diferentes tipos de datos, desde string, float, e
int, hasta colecciones, como listas. Por ende, si deseamos tener varias columnas con datos
float, en data deberemos crearlas luego de crear el objeto DataFrame, esto se explica a
continuación, en el punto 2.
Finalmente, un DataFrame también puede pensarse como una lista donde cada objeto es un
diccionario, y donde cada uno de estos diccionarios es una línea del DataFrame. Veamos un
ejemplo, si escribimos lo siguiente, tendremos una lista con una estructura de DataFrame, de
hecho, al convertirla en uno, el resultado no cambia más allá de su caracterización como objeto.
Veamos:
𝑎𝑐𝑐𝑖𝑜𝑛𝑒𝑠_𝑙𝑖𝑠𝑡
= [{′𝑇𝑖𝑐𝑘𝑒𝑟′: ′𝐺𝐺𝐴𝐿′, ′𝑁𝑜𝑚𝑏𝑟𝑒′: ′𝐵𝑎𝑛𝑐𝑜 𝐺𝑎𝑙𝑖𝑐𝑖𝑎′, ′𝑃𝑎𝑖𝑠′: ′𝐴𝑟𝑔𝑒𝑛𝑡𝑖𝑛𝑎′, ′𝑅𝑢𝑏𝑟𝑜′: ′𝐵𝑎𝑛𝑐𝑜𝑠′},
{′𝑇𝑖𝑐𝑘𝑒𝑟′: ′𝑃𝐴𝑀𝑃′, ′𝑁𝑜𝑚𝑏𝑟𝑒′: ′𝑃𝑎𝑚𝑝𝑎 𝐸𝑛𝑒𝑟𝑔í𝑎′, ′𝑃𝑎𝑖𝑠′: ′𝐴𝑟𝑔𝑒𝑛𝑡𝑖𝑛𝑎′, ′𝑅𝑢𝑏𝑟𝑜′: ′𝐸𝑛𝑒𝑟𝑔é𝑡𝑖𝑐𝑎𝑠′},
{′𝑇𝑖𝑐𝑘𝑒𝑟′: ′𝑀𝑆𝐹𝑇′, ′𝑁𝑜𝑚𝑏𝑟𝑒′: ′𝑀𝑖𝑐𝑟𝑜𝑠𝑜𝑓𝑡′, ′𝑃𝑎𝑖𝑠′: ′𝑈𝑆𝐴′, ′𝑅𝑢𝑏𝑟𝑜′: ′𝑇𝑒𝑐𝑛𝑜𝑙ó𝑔𝑖𝑐𝑎𝑠′},
{′𝑇𝑖𝑐𝑘𝑒𝑟′: ′𝐵𝐴𝐵𝐴′, ′𝑁𝑜𝑚𝑏𝑟𝑒′: ′𝐴𝑙𝑖𝑏𝑎𝑏𝑎′, ′𝑃𝑎𝑖𝑠′: ′𝐶ℎ𝑖𝑛𝑎′, ′𝑅𝑢𝑏𝑟𝑜′: ′𝐶𝑜𝑛𝑠𝑢𝑚𝑜′}]
𝑝𝑟𝑖𝑛𝑡(𝑎𝑐𝑐𝑖𝑜𝑛𝑒𝑠_𝑙𝑖𝑠𝑡)
Vemos aquí que, en lugar de utilizar el argumento index de la función pd.DataFrame, usamos la
función set_index. Esto lo hacemos porque el argumento de la primera función es útil para cuando
el índice a colocar es una colección de algún tipo, como una lista. En cambio, si el índice a elegir es
un valor o clave, como en este caso, será necesario utilizar la segunda función.
Del mismo modo, un DataFrame puede obtenerse como un diccionario de listas, esto es,
definiendo las claves y, definiendo los valores como elementos de listas. Veamos un ejemplo:
𝑑𝑎𝑡𝑎 = {′𝑇𝑖𝑐𝑘𝑒𝑟′: [′𝐴𝐿𝑈𝐴′, ′𝐵𝐵𝐴𝑅′, ′𝐵𝑀𝐴′, ′𝐵𝑌𝑀𝐴′], ′𝑃𝑟𝑒𝑐𝑖𝑜𝑠′: [19.15,73.7,234,144.4]}
𝑡𝑎𝑏𝑙𝑎 = 𝑝𝑑. 𝐷𝑎𝑡𝑎𝐹𝑟𝑎𝑚𝑒(𝑑𝑎𝑡𝑎)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Existe una gran diferencia entre este modo de construir el DataFrame y el de hace un momento, y
es que en este último, las listas necesariamente deben tener la misma extensión o cantidad de
elementos (por ende, un cero o vacío también debe escribirse), de lo contrario, el DataFrame no se
armará, de hecho el código tendrá un error. En cambio, con el modo anterior no hay problema con
omitir un elemento, de hecho, al hacerlo, se interpreta que dicho valor está vacío o es igual a cero.
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑐𝑜𝑙𝑢𝑚𝑛𝑠)
Ambos códigos generan el mismo resultado a la luz de lo que se pretende saber, sin
embargo, el segundo brinda más información al señalar el tipo de objeto que son las
etiquetas de las columnas.
Dimensiones del DataFrame: Con esto podemos conocer las dimensiones del archivo
importado, es decir, cuántas filas y columnas tiene. La información se presenta con el
formato habitual de las matrices. Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑠ℎ𝑎𝑝𝑒)
Toda la información del DataFrame: Con el siguiente código podemos conocer no sólo
los datos conseguidos a través de los dos códigos anteriores, sino también que sabremos el
tipo de datos que forma parte de cada columna (string, int, float, datetime, etc). Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑖𝑛𝑓𝑜)
Vemos que trabajar de este modo modifica automáticamente el objeto DataFrame. Como
alternativa, si no deseamos modificar el objeto DataFrame, escribimos directamente lo
siguiente:
𝑟𝑜𝑢𝑛𝑑(𝑎. 𝑚𝑜𝑛𝑡𝑜𝑠/𝑎. 𝑚𝑜𝑛𝑡𝑜𝑠. 𝑠𝑢𝑚() ∗ 100,2)
Si imprimimos esto, veremos como resultado lo siguiente:
En definitiva, para agregar más columnas debemos escribir el nombre que le asignaremos e
inmediatamente asignarle una lista o valor. Por ende, la lógica es como cuando usamos la
función append() de las listas, o sea, incorporamos columnas una a la vez, y las nuevas se
colocan como la última columna.
3. ¿Cómo podemos transponer un DataFrame? Para hacerlo usamos el método
“.transpose()”. Supongamos que contamos con la siguiente información de partida, y
deseamos transponer el DataFrame correspondiente:
𝑡𝑖𝑐𝑘𝑒𝑡 = [′𝑇𝑆𝐿𝐴′ , ′𝐴𝑀𝑍𝑁′ , ′𝐺𝑂𝑂𝐺𝐿′ , ′𝑇𝐿𝑇′ , ′𝑆𝐻𝑌′, ′𝐵𝑇𝐶′]
𝑝𝑟𝑒𝑐𝑖𝑜𝑠 = [12500,5000,10500,20_000,7 + 7 ∗ 10 ∗∗ 1,7𝑒1]
𝑚𝑎𝑡𝑟𝑖𝑧 = 𝑝𝑑. 𝐷𝑎𝑡𝑎𝐹𝑟𝑎𝑚𝑒(𝑑𝑎𝑡𝑎 = 𝑝𝑟𝑒𝑐𝑖𝑜𝑠, 𝑖𝑛𝑑𝑒𝑥 = 𝑡𝑖𝑐𝑘𝑒𝑡)
𝑚𝑎𝑡𝑟𝑖𝑧. 𝑐𝑜𝑙𝑢𝑚𝑛𝑠 = [′𝑀𝑜𝑛𝑡𝑜𝑠′]
𝑝𝑟𝑖𝑛𝑡(𝑚𝑎𝑡𝑟𝑖𝑧. 𝑡𝑟𝑎𝑛𝑠𝑝𝑜𝑛𝑠𝑒)
Finalmente, el objeto DataFrame creado hasta ahora es un objeto del tipo data, mientras que cada
una de sus columnas numéricas son del tipo series. Para ver esto sólo basta con escribir lo siguiente
(que vimos cómo hacer):
𝑝𝑟𝑖𝑛𝑡(𝑡𝑦𝑝𝑒(𝑎. 𝑀𝑜𝑛𝑡𝑜𝑠))
Finalmente, para extraer las tres mayores participaciones debemos utilizar la función
head(), donde su argumento es un número tipo int que debemos elegir en función de la
cantidad de objetos que queremos como retorno. Por defecto, este método trae siempre los
primeros cinco elementos del objeto. En otras palabras, si escribimos tres, obtendremos los
tres primeros elementos del objeto. Asumiendo que los objetos están ordenados de mayor a
menor, y que estamos trabajando con el objeto tipo Data, escribimos:
𝑝𝑟𝑖𝑛𝑡(𝑎. ℎ𝑒𝑎𝑑(3))
Ahora, si en lugar de ticket y montos, sólo queremos los tickets, debemos modificar estos
código adicionándoles index. Veamos (ambos casos son lo mismo):
𝑝𝑟𝑖𝑛𝑡(𝑎. ℎ𝑒𝑎𝑑(3). 𝑖𝑛𝑑𝑒𝑥)
𝑝𝑟𝑖𝑛𝑡(𝑏. ℎ𝑒𝑎𝑑(3). 𝑖𝑛𝑑𝑒𝑥)
En el mismo sentido, si deseamos acceder sólo a los montos más grandes, adicionamos a lo
anterior tolist(). Veamos, aquí es sólo aplicable al caso del objeto tipo serie, no al objeto
tipo data:
𝑝𝑟𝑖𝑛𝑡(𝑏. ℎ𝑒𝑎𝑑(3). 𝑡𝑜𝑙𝑖𝑠𝑡())
En el caso del objeto tipo data, debemos acceder a la columna de interés (Montos), luego
seleccionar los tres primeros objetos, y luego aplicar este método:
𝑝𝑟𝑖𝑛𝑡(𝑎. 𝑀𝑜𝑛𝑡𝑜𝑠. ℎ𝑒𝑎𝑑(3). 𝑡𝑜𝑙𝑖𝑠𝑡())
Siguiendo con el objeto tipo data, también podemos aplicarle la función iloc[], cuyo
argumento es del tipo slicing, es decir, tiene un argumento desde, otro hasta, y otro
argumento de salto. Por ende, aplicándolo sobre la columna de interés (Montos), podemos
seleccionar la cantidad de objetos que queremos como resultado, indicando la ubicación de
ellos:
𝑝𝑟𝑖𝑛𝑡(𝑎. 𝑀𝑜𝑛𝑡𝑜𝑠. 𝑖𝑙𝑜𝑐[: 3])
Como detalle, si deseamos obtener los papeles con menor participación, en lugar de
reordenar de menor a mayor, se puede utilizar la función tail(). La lógica de la línea del
código es la misma, por ello se prescinde del ejemplo.
o ¿Cómo podemos acumular los valores de una columna? Para esto utilizamos dos métodos,
el cumsum() para crear la columna de sumas acumuladas, y el método loc[] para pedir los
tickets hasta cierto monto dentro de la columna de interés.
Antes de aplicar ambos métodos debemos ordenar de mayor a menor o, de menor a mayor.
Siempre se trabaja con un objeto tipo data. Asumiendo que ya hemos ordenado de mayor a
menor, ahora aplicamos el método mencionado y lo asignamos a una nueva columna, asi:
𝑎[′𝑀𝑜𝑛𝑡𝑜𝑠_𝑎𝑐𝑢𝑚′] = 𝑎[′𝑀𝑜𝑛𝑡𝑜𝑠′]. 𝑐𝑢𝑚𝑠𝑢𝑚()
𝑝𝑟𝑖𝑛𝑡(𝑎)
Si deseamos obtener tickets cuyo valor acumulado sea menor a 40 mil, entonces
escribimos:
𝑝𝑟𝑖𝑛𝑡(𝑎. 𝑙𝑜𝑐[𝑎. 𝑀𝑜𝑛𝑡𝑜𝑠_𝑎𝑐𝑢𝑚 < 40_000])
Para obtener las cotizaciones y volúmenes de cualquier papel, utilizaremos la función download().
Veamos un ejemplo para el papel de AAPL:
𝑝𝑟𝑒𝑐𝑖𝑜𝑠_𝑎𝑎𝑝𝑙 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(′𝐴𝐴𝑃𝐿′ , 𝑠𝑡𝑎𝑟𝑡 =′ 2010 − 01 − 01′, 𝑎𝑢𝑡𝑜_𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑇𝑟𝑢𝑒)
Vemos tres argumentos, el primero es el precio del papel de interés, el segundo es la fecha inicial
de serie, y el tercero permite definir si deseamos traer o no la columna con precios de cierre
ajustados. Si escribimos True, no obtenemos los precios de cierre ajustados, sólo los precios de
cierre, y si escribimos False obtendremos ambos En este caso, si imprimimos el objeto data
tendremos lo siguiente:
NOTA. Para conocer los parámetros de una función debemos utilizar la función help(). Por
ejemplo:
ℎ𝑒𝑙𝑝(𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑)
Esta información puede exportarse a Excel, para ello debemos escribir el nombre del objeto tipo
DataFrame a exportar, y utilizar la función to_excel() cuyo argumento es el nombre del archivo
Excel (tipo string). El siguiente sería el código para este caso:
𝑝𝑟𝑒𝑐𝑖𝑜𝑠_𝑎𝑎𝑝𝑙. 𝑡𝑜_𝑒𝑥𝑐𝑒𝑙("𝐶𝑎𝑐𝑎𝑡𝑢𝑎. 𝑥𝑙𝑠𝑥")
En línea con esto, para importar archivos de Excel es necesario utilizar la librería de pandas.
Imaginemos que lo hemos hecho y la llamamos “pd”. Ahora debemos escribir, utilizando la
función read_excel(), cuyo argumento es el mismo que la función anterior, el código es el
siguiente código:
𝑏 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑒𝑥𝑐𝑒𝑙("𝐶𝑎𝑐𝑎𝑡𝑢𝑎. 𝑥𝑙𝑠𝑥")
Vea que hemos asignado a la variable “b” el archivo Excel importado. En este sentido, si
imprimimos el resultado obtendremos lo siguiente:
Este resultado tiene una particularidad, aunque es un objeto tipo DataFrame, su columna index no
tiene fecha, en su lugar se tiene el número de filas y las fechas están como segunda columna. Para
solucionar esto, en el mismo momento donde importamos el archivo Excel debemos utilizar la
función set_index(), y en su argumento debemos colocar el nombre de la columna que queremos
que funcione como index. Entonces, en lugar de lo anterior, escribimos lo siguiente:
𝑏 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑒𝑥𝑐𝑒𝑙("𝐶𝑎𝑐𝑎𝑡𝑢𝑎. 𝑥𝑙𝑠𝑥"). 𝑠𝑒𝑡_𝑖𝑛𝑑𝑒𝑥(′𝐷𝑎𝑡𝑒 ′ )
Al imprimir obtenemos:
Para descargar más de un ticket a la vez, aplicamos la misma línea de código mencionada antes,
pero en lugar de escribir el nombre del ticket, colocamos una lista que contiene el nombre los
papeles de interés. En paralelo, si estamos interesados sólo en un tipo de dato para cada papel,
debemos realizar un slice, en este sentido, podemos obtener sólo los precios de cierre. Veamos un
ejemplo:
𝑡𝑖𝑐𝑘𝑒𝑡 = [′ 𝐴𝐴𝑃𝐿′ ,′ 𝐹𝐵′ ,′ 𝐴𝑀𝑍𝑁 ′,′ 𝐺𝑂𝑂𝐺𝐿′ ,′ 𝑁𝐹𝐿𝑋 ′ ,′ 𝑄𝑄𝑄 ′ , ′𝑆𝑄𝑄𝑄′]
𝑝𝑟𝑒𝑐𝑖𝑜𝑠 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(𝑡𝑖𝑐𝑘𝑒𝑡𝑠, 𝑠𝑡𝑎𝑟𝑡 =′ 2010 − 01 − 01′ , 𝑎𝑢𝑡𝑜𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑡𝑟𝑢𝑒)[′ 𝐶𝑙𝑜𝑠𝑒 ′ ]
El resultado es el siguiente:
La alternativa es descargar toda la información y aplicar el slice luego sobre la variable a quien se
le asigna todo esto.
𝑖𝑚𝑝𝑜𝑟𝑡 𝑦𝑓𝑖𝑛𝑎𝑛𝑐𝑒 𝑎𝑠 𝑦𝑓
𝑖𝑚𝑝𝑜𝑟𝑡 𝑝𝑎𝑛𝑑𝑎𝑠 𝑎𝑠 𝑝𝑑
𝑡𝑖𝑐𝑘𝑒𝑡 = [′ 𝐴𝐴𝑃𝐿′ ,′ 𝐹𝐵′ ,′ 𝐴𝑀𝑍𝑁 ′ ,′ 𝐺𝑂𝑂𝐺𝐿′ ,′ 𝑁𝐹𝐿𝑋 ′ ,′ 𝑄𝑄𝑄 ′ , ′𝑆𝑄𝑄𝑄′]
𝑝𝑟𝑒𝑐𝑖𝑜𝑠 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(𝑡𝑖𝑐𝑘𝑒𝑡𝑠, 𝑠𝑡𝑎𝑟𝑡 =′ 2019 − 12 − 31′ , 𝑎𝑢𝑡𝑜𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑡𝑟𝑢𝑒)[′ 𝐶𝑙𝑜𝑠𝑒 ′ ]
2. ¿Cómo podemos calcular medias? Para lograr utilizamos los métodos rolling() y mean().
El primero método mencionado agrupa los elementos de cada columna en grupos de la
cantidad mencionada en su argumento, por ejemplo, si escribimos rolling(3), agrupará los
elementos en grupo de 3. El segundo método calcula el promedio sobre lo señalado. Por
ende, si combinamos ambos métodos podemos obtener las medias móviles. Veamos cómo
obtener las medias móviles de 3 ruedas:
𝑚𝑒𝑑𝑖𝑎_𝑚𝑜𝑣𝑖𝑙_3 = 𝑝𝑟𝑒𝑐𝑖𝑜𝑠. 𝑟𝑜𝑙𝑙𝑖𝑛𝑔(3). 𝑚𝑒𝑎𝑛()
𝑝𝑟𝑖𝑛𝑡(𝑚𝑒𝑑𝑖𝑎_𝑚𝑜𝑣𝑖𝑙_3)
4. ¿Cómo calculamos la matriz de varianzas y covarianza entre cada uno de los papeles?
Una vez que contamos con las series de variaciones entre rueda y rueda, aplicamos el
método cov(). En nuestro ejemplo, el objeto en cuestión se llama “porcentajes”, veamos
cómo es el código:
𝑣𝑎𝑟_𝑐𝑜𝑣 = 𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠. 𝑐𝑜𝑣()
𝑝𝑟𝑖𝑛𝑡(𝑣𝑎𝑟_𝑐𝑜𝑣)
NOTA. Como los objetos DataFrame son matrices, sobre estos aplican las reglas operativas de
ellas. Del mismo modo, también podemos aplicarles la función round().
𝑖𝑚𝑝𝑜𝑟𝑡 𝑦𝑓𝑖𝑛𝑎𝑛𝑐𝑒 𝑎𝑠 𝑦𝑓
𝑖𝑚𝑝𝑜𝑟𝑡 𝑝𝑎𝑛𝑑𝑎𝑠 𝑎𝑠 𝑝𝑑
𝑡𝑖𝑐𝑘𝑒𝑡 = [′ 𝐴𝐴𝑃𝐿′ ,′ 𝐴𝑀𝑍𝑁 ′ ,′ 𝐺𝑂𝑂𝐺𝐿′ ,′ 𝑁𝐹𝐿𝑋 ′ ,′ 𝑄𝑄𝑄 ′ , ′𝑆𝑄𝑄𝑄′]
𝑝𝑟𝑒𝑐𝑖𝑜𝑠 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(𝑡𝑖𝑐𝑘𝑒𝑡𝑠, 𝑠𝑡𝑎𝑟𝑡 =′ 2019 − 12 − 31′ , 𝑎𝑢𝑡𝑜𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑡𝑟𝑢𝑒)[′ 𝐶𝑙𝑜𝑠𝑒 ′ ]
𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠 = 𝑝𝑟𝑒𝑐𝑖𝑜𝑠. 𝑝𝑐𝑡_𝑐ℎ𝑎𝑛𝑔𝑒()
Los métodos se ven a continuación:
1. ¿Cómo podemos aplicar modificaciones sobre los valores del objeto DataFrame? Aquí
asumimos que estos valores son numéricos, entonces, si deseamos realizarles algún cambio
podemos utilizar, entre otras cosas, la función mul() que multiplica estos valores por lo que
deseemos. El argumento de esta función es el número que aplicaremos sobre los valores
del DataFrame. Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠. 𝑚𝑢𝑙(100))
No obstante esta función, también es posible alcanzar este mismo resultado utilizando las
propiedades matriciales, es decir, escribiendo lo siguiente:
𝑝𝑟𝑖𝑛𝑡(𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠 ∗ 100)
Otro método aplicable es divide(), con características similares al mul(), pero que en lugar
de multiplicar los valores del DataFrame, lo divide. Veamos:
𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠2 = 𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠 ∗ 100
𝑝𝑟𝑖𝑛𝑡(𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠. 𝑑𝑖𝑣𝑖𝑑𝑒(100))
2. El método describe() permite obtener varias estadísticas descriptivas. Por ejemplo, si
deseamos caracterizar las series de precios de cierre podemos aplicar. Veamos esto
aplicado a “porcentajes”:
𝑝𝑟𝑖𝑛𝑡(𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠. 𝑑𝑒𝑠𝑐𝑟𝑖𝑏𝑒())
5. El método std() es idéntico al visto en una sección pasada, permite obtener el desvío
estándar. Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠. 𝑠𝑡𝑑())
CURTOSIS
Observando el numerador, al estar elevado a la cuarta, los números grandes (valores extremos)
tienen mayor influencia que en el caso donde se eleva al cuadrado. Esto es lo que permite darle
más peso a las colas de la distribución de probabilidad. En paralelo, su denominador permite
capturar la concentración alrededor de la media, pues la varianza (sigma a la cuarta es igual a
multiplicar por sí misma la varianza) será pequeña mientras más acampanada sea la distribución y,
en consecuencia, la curtosis crecerá.
En la distribución normal se verifica que 𝜇4 = 3𝜎 4, donde 𝜇4 es el momento de orden 4 respecto a
la media, y 𝜎 es la desviación típica. Por eso, está más extendida la siguiente definición del
coeficiente de curtosis, también denominada exceso de curtosis:
𝜇4
𝑔2 = 4 − 3
𝜎
Donde se ha sustraído 3 (que es la curtosis de la distribución normal o gaussiana) con objeto de
generar un coeficiente que valga 0 para la Normal y tome a ésta como referencia de curtosis.
Tomando, pues, la distribución normal como referencia, una distribución puede ser:
o Leptocúrtica: Ocurre cuando 𝛽2 > 3 y 𝑔2 > 0. En estos casos, la distribución de
frecuencias está más apuntada y con colas más gruesas de lo normal.
o Platicúrtica. Ocurre cuando 𝛽2 < 3 y 𝑔2 < 0. En estos casos, la distribución de
frecuencias está menos apuntada y con colas menos gruesas de lo normal.
o Mesocúrtica. Ocurre cuando 𝛽2 = 3 y 𝑔2 = 0. En estos casos, la distribución de
frecuencias tiene una distribución normal.
La librería Pandas calcula el exceso de curtosis.
COEFICIENTE DE ASIMETRIA
Esta es una medida que permite conocer el sesgo y grado de una distribución de probabilidad. Las
medidas de asimetría son indicadores que permiten establecer el grado de simetría (o asimetría)
que presenta una distribución de probabilidad de una variable aleatoria sin tener que hacer su
representación gráfica. Como eje de simetría consideramos una recta paralela al eje de ordenadas
que pasa por la media de la distribución. Si una distribución es simétrica, existe el mismo número
de valores a la derecha que a la izquierda de la media, por tanto, el mismo número de desviaciones
con signo positivo que con signo negativo. Decimos que hay asimetría positiva (o a la derecha) si
la "cola" a la derecha de la media es más larga que la de la izquierda, es decir, si hay valores más
separados de la media a la derecha. Diremos que hay asimetría negativa (o a la izquierda) si la
"cola" a la izquierda de la media es más larga que la de la derecha, es decir, si hay valores más
separados de la media a la izquierda.
En teoría de la probabilidad y estadística, la medida de asimetría más utilizada parte del uso del
tercer momento estándar. La razón de esto es que nos interesa mantener el signo de las
desviaciones con respecto a la media, para obtener si son mayores las que ocurren a la derecha de
la media que las de la izquierda.
El coeficiente de asimetría de Fisher, representado por 𝛾1 , se define como
𝜇
𝛾1 = 3
𝜎3
Donde 𝜇3 es el tercer momento en torno a la media y, 𝜎3 es la desviación estándar. Su forma
algebraica es la siguiente:
Al estar elevado al cubo, su signo toma relevancia o significado, indicado el sentido del sesgo,
mientras que su resultado numérico refiere a la importancia del mismo.
NOTA. Transponer una tabla o matriz. Para transponer sólo debemos utilizar el método T, por
ejemplo, imaginemos que tenemos el siguiente DataFrame:
𝑝𝑟𝑖𝑛𝑡(𝑝𝑜𝑟𝑐𝑒𝑛𝑡𝑎𝑗𝑒𝑠)
11. ¿Cómo podemos graficar a partir del DataFrame? Para presentar esta respuesta, primero
veamos cómo armar un índice. Partiendo del objeto original “precios”, nuestro objetivo es
lograr una serie de índices, donde el precio base es el primer precio de la serie. Esto lo
podemos lograr utilizando la función divide() e iloc[], pero también es posible haciendo
uso de las propiedades de las matrices y la función iloc[]. Veamos ambos casos,
comenzado por la función divide():
𝑝𝑟𝑖𝑛𝑡(𝑝𝑟𝑒𝑐𝑖𝑜𝑠. 𝑑𝑖𝑣𝑖𝑑𝑒(𝑝𝑟𝑒𝑐𝑖𝑜𝑠. 𝑖𝑙𝑜𝑐[0]))
Este mismo resultado se obtiene con este otro código:
Entre los argumentos de este método plot(), tenemos a figsize, quien permite definir el
tamaño de la gráfica y su leyenda, veamos cómo queda la gráfica cuando lo usamos:
𝑖𝑛𝑑𝑖𝑐𝑒. 𝑝𝑙𝑜𝑡(𝑓𝑖𝑔𝑠𝑖𝑧𝑒 = (16,8))
Asimismo, también tenemos el argumento grid (que asume valores booleanos) y permite
incorporar las líneas de coordenadas, veamos:
𝑖𝑛𝑑𝑖𝑐𝑒. 𝑝𝑙𝑜𝑡(𝑓𝑖𝑔𝑠𝑖𝑧𝑒 = (16,6), 𝑔𝑟𝑖𝑑 = 𝑇𝑟𝑢𝑒)
Finalmente, imaginemos que deseamos concentrar la atención sólo sobre los papeles que
suben, esto es, descartamos SQQQ. Para esto, usamos la función iloc[] del siguiente modo:
𝑖𝑛𝑑𝑖𝑐𝑒. 𝑖𝑙𝑜𝑐[: , : 5]. 𝑝𝑙𝑜𝑡(𝑓𝑖𝑔𝑠𝑖𝑧𝑒 = (16,6), 𝑔𝑟𝑖𝑑 = 𝑇𝑟𝑢𝑒)
Lo que importa en este caso es cómo utilizamos iloc[], observe que hemos utilizado las
propiedades “desde”, “hasta”, y “salto”, para las filas y las columnas. En este sentido, para
las filas definimos utilizarlas todas, pero para las columnas nos limitamos a todos los
papeles excepto el SQQQ.
Excepciones y errores
Cuando tenemos un error en la ejecución del código, el mismo se interrumpe, para solucionar esto
podemos utilizar sentencias condicionales o el bloque “try, except”. Aquí veremos este segundo, a
partir del siguiente ejemplo:
𝑎 = 20
𝑏=0
𝑐 = 𝑎/𝑏
𝑝𝑟𝑖𝑛𝑡(′𝑁𝑒𝑐𝑒𝑠𝑖𝑡𝑜 𝑢𝑛𝑎𝑠 𝑏𝑢𝑒𝑛𝑎𝑠 𝑣𝑎𝑐𝑖𝑜𝑛𝑒𝑠 ′ )
Al ejecutar esto tendremos un error, pues no existe resultado alguno de una división por cero.
Aplicando el bloque mencionado podemos escribir:
𝑎 = 20
𝑏=0
𝑡𝑟𝑦 :
𝑐 = 𝑎/𝑏
𝑒𝑥𝑐𝑒𝑝𝑡 :
𝑝𝑟𝑖𝑛𝑡(′𝑁𝑒𝑐𝑒𝑠𝑖𝑡𝑜 𝑢𝑛𝑎𝑠 𝑏𝑢𝑒𝑛𝑎𝑠 𝑣𝑎𝑐𝑖𝑜𝑛𝑒𝑠 ′ )
Con esto decimos algo así: “intentá hacer la división, y si no se puede (except) haz esto otro…”. En
línea con esto, una vez que el código ejecuta el “except” continúa con el resto de las líneas sin
interrumpir la ejecución.
En paralelo, si el bloque que corresponde a “except” no se define aun, el código puede correrse
igual, pero en este lugar deberá escribirse “pass”. En el ejemplo anterior sería:
𝑎 = 20
𝑏=0
𝑡𝑟𝑦 :
𝑐 = 𝑎/𝑏
𝑒𝑥𝑐𝑒𝑝𝑡 :
𝑝𝑎𝑠𝑠
En este caso, si “b” es igual a cero el código no arrojará error, pero claramente tampoco arrojará
alguna leyenda como antes. Si no se coloca “pass” u otra línea de código, lo escrito producirá un
error al ejecutarse.
El uso de condicionales trae consigo al bloque if else. En este caso, el código para este ejemplo es:
𝑎 = 20
𝑏=0
𝑖𝑓 𝑏! = 0 ∶
𝑐 = 𝑎/𝑏
𝑒𝑙𝑠𝑒 ∶ :
𝑝𝑟𝑖𝑛𝑡(′𝑁𝑒𝑐𝑒𝑠𝑖𝑡𝑜 𝑢𝑛𝑎𝑠 𝑏𝑢𝑒𝑛𝑎𝑠 𝑣𝑎𝑐𝑖𝑜𝑛𝑒𝑠 ′ )
Con esto estamos diciendo que si b es diferente de cero, entonces se ejecute la división, y si es
igual a cero, que se ejecute la leyenda.
La diferencia entre utilizar el primer bloque y el segundo es, que el primero es más general, para
todo tipo de errores que no permiten el cálculo, en cambio, en el caso del segundo tipo de bloque,
sólo se salva el error producido por una situación donde b es igual a cero, pero aun es un número.
En otras palabras, si el error fuese producido porque b es un string, el primer bloque sí salva el
código, pero el segundo no. En conclusión, se recomienda usar el primer tipo de bloques para
resolver errores, no el segundo, pues no siempre podremos imaginar todos los tipos de problemas.
Operadores lógicos
Los más comunes son los siguientes:
Vamos a concentrarnos en los últimos dos, pues asumimos conocidos al resto. Respecto a in
imaginemos que tenemos la siguiente lista y deseamos saber si determinado ticket está en ella,
escribimos lo siguiente:
𝑙𝑖𝑠𝑡𝑎 = [′ 𝐺𝐺𝐴𝐿′ ,′ 𝑃𝐴𝑀𝑃′ ,′ 𝑌𝑃𝐹𝐷′ ,′ 𝐶𝐸𝑃𝑈 ′ ,′ 𝐸𝐷𝑁 ′ ,′ 𝐿𝑂𝑀𝐴′ , ′𝐶𝑅𝐸𝑆′]
𝑡𝑖𝑐𝑘𝑒𝑡 = ′𝐺𝐺𝐴𝐿′
𝑡𝑖𝑐𝑘𝑒𝑡 𝑖𝑛 𝑙𝑖𝑠𝑡𝑎
El resultado será True. En línea con esto, el not in es su negación.
Sentencia if & comando break
Este es un condicional y como tal define ramas del flujo de acción del código. Con esta sentencia
se “pregunta” si se cumple determinada condición, y de ser así aplica otra línea de código, pero en
caso de no cumplirse la condición, entonces aplica otra línea de código. Como en Excel, este
sentencia puede anidarse o concatenarse con otras sentencias if. Su estructura básica es la siguiente:
𝑖𝑓 (𝑐𝑜𝑛𝑑𝑖𝑐𝑖𝑜𝑛):
𝑙𝑖𝑛𝑒𝑎𝑠 𝑑𝑒 𝑐ó𝑑𝑖𝑔𝑜𝑠
𝑒𝑙𝑠𝑒 ∶
𝑙𝑖𝑛𝑒𝑎𝑠 𝑑𝑒 𝑐ó𝑑𝑖𝑔𝑜𝑠
Una particularidad de esta sentencia, es que el else puede no estar.
Por otro lado, esta sentencia puede utilizarse de dos modos:
o Concatenar al no cumplir condición. En este caso, la concatenación de sentencias if se
produce sólo cuando las condiciones no se cumplen. En estos casos, la concatenación
siempre es incorporando nuevos elif. La siguiente imagen ilustra esto:
Por otro lado, el comando break permite interrumpir la ejecución del bucle for cuando se cumple
determinada condición. Veamos un ejemplo, donde generamos un bucle for que iterará 10 veces y,
en cada oportunidad imprimirá el valor de la iteración, aunque, la condición del break será que
dicha ejecución se detenga luego de la quinta iteración. Veamos:
𝑓𝑜𝑟 𝑖 𝑖𝑛 𝑟𝑎𝑛𝑔𝑒(10):
𝑝𝑟𝑖𝑛𝑡(𝑖)
𝑖𝑓 𝑖 == 5:
𝑏𝑟𝑒𝑎𝑘
Veamos a continuación un ejemplo de aplicación del condicional if else, el bucle for, y el
comando break. En este ejemplo, la intención es crear un código que imprima una leyenda según
el contraste entre el precio actual de cotización y las resistencias. A continuación se muestra una
posible solución:
Este código puede mejorarse combinando bucle for, condicional if else, y comando break. Es
necesario mejorarlo, pues de lo contrario, en un escenario donde se cuente con 10 o más
resistencias sería demasiado engorroso utilizar este encadenamiento. A continuación se comparte el
código correspondiente:
𝑝𝑟𝑒𝑐𝑖𝑜𝑠 = []
𝑓𝑜𝑟 𝑖 𝑖𝑛 𝑟𝑎𝑛𝑔𝑒(0,170,10):
𝑝𝑟𝑒𝑐𝑖𝑜𝑠. 𝑎𝑝𝑝𝑒𝑛𝑑(𝑖)
𝑝𝑟𝑒𝑐𝑖𝑜 = 𝑓𝑙𝑜𝑎𝑡(𝑖𝑛𝑝𝑢𝑡("Introduzca el precio actual:")
𝑓𝑜𝑟 𝑖 𝑖𝑛 𝑝𝑟𝑒𝑐𝑖𝑜𝑠:
𝑖𝑓 𝑝𝑟𝑒𝑐𝑖𝑜 < 𝑖:
𝑝𝑟𝑖𝑛𝑡("La resistencia a vencer es: ", 𝑖)
𝑏𝑟𝑒𝑎𝑘
𝑒𝑙𝑖𝑓 𝑝𝑟𝑒𝑐𝑖𝑜 ≥ 𝑝𝑟𝑒𝑐𝑖𝑜𝑠[−1]:
𝑝𝑟𝑖𝑛𝑡("Estamos en máximos históricos")
𝑏𝑟𝑒𝑎𝑘
SEGUNDA PARTE: BASES – PROFUNDIZANDO
DATAFRAME
DataFrame: ¿Qué es? – 2/2
Existen dos modos de “levantar” un DataFrame utilizando la librería pandas:
1. Levantarlo online. Esto implica navegar por internet para encontrar el archivo de interés y
leerlo. Por ejemplo, cuando usamos un CSV, como será el caso a continuación. El CSV es
parecido a Excel, sólo que su formato está cifrado en texto (como un txt), así cada
valor/columna está separado por una coma, y los saltos de línea por saltos de línea. Un
ejemplo de esto es la siguiente imagen:
Segundo: Habiendo constatado esto, escribimos el código para importar el archivo. En este
caso tenemos varios archivos CSV guardados, de los cuales elegiremos el llamado
AAPL.CSV (como se indica en el video de la clase). Para importarlo escribimos:
𝑟𝑢𝑡𝑎 = ′𝐴𝐴𝑃𝐿. 𝑐𝑠𝑣′
𝑑𝑎𝑡𝑎 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑐𝑠𝑣(𝑟𝑢𝑡𝑎)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Entonces, en ruta escribimos el nombre del archivo, de este modo, el código buscará el
archivo en la máquina (esto se conoce como ruta relativa). En este sentido, si conocemos
específicamente la dirección donde está ubicado el archivo, la escribimos en lugar de poner
sólo su nombre (a esto se le conoce como ruta absoluta). El resultado es el siguiente:
Renombrar columnas
Supongamos que tenemos un DataFrame llamado “a”, y tiene una cantidad “n” de columnas. Para
renombrar sus columnas, podemos utilizar uno de dos métodos. Veamos cada método a través de
un ejemplo, lo siguiente es la imagen del vector fila, y a continuación se explica cada método:
Cambio de todas las columnas. Este método requiere conocer la cantidad “n” de columnas
y su orden o contenido, pues el mismo cambia el nombre de todas las variables, y el modo
de hacerlo es similar a la forma utilizada cuando creamos una nueva columna. Lo que se
hace es asignar al vector fila correspondiente una lista con los nuevos nombres. Veamos un
ejemplo:
Cambio de algunas columnas. Con esta alternativa utilizamos el método rename(), cuyo
argumento principal es ‘columns’. Lo siguiente es colocar un diccionario, donde la clave es
el nombre original que deseamos modificar, y el valor es el nuevo nombre a asignar.
Veamos cómo modificar lo señalado:
Esto lo escribimos asignándolo a un nuevo objeto por si no deseamos reescribir el objeto
original, sin embargo, si no importa, esto lo asignamos al objeto original.
Parte de renombrar columnas consiste en pasar de mayúsculas a minúsculas. Para hacer esto
utilizamos el código str.lower(). Esto debemos aplicarlo sobre las columnas, valga la redundancia,
veamos:
𝑑𝑓. 𝑐𝑜𝑙𝑢𝑚𝑛𝑠 = 𝑑𝑓. 𝑐𝑜𝑙𝑢𝑚𝑛𝑠. 𝑠𝑡𝑟. 𝑙𝑜𝑤𝑒𝑟()
Renombrar filas
Para lograr esto utilizamos el método rename(), cuyo argumento principal será index. Modificar el
nombre de las filas implica modificar el valor de cada celda del índice. Una alternativa es modificar
el nombre de las columnas y luego transponer, sin embargo, si no hacemos esto, debemos utilizar el
método mencionado. Los valores del argumento index se deben escribir como un diccionario,
señalando en lugar de la clave la ubicación o nombre original de la fila, y en su valor el nuevo
nombre. Veamos un ejemplo, supongamos que el vector fila con los nombres originales del
DataFrame son los siguientes:
Al importar el archivo indicando la hoja dos como señala el código siguiente, tenemos como
resultado un DataFrame muy desordenado, veamos:
𝑒𝑥𝑐𝑒𝑙 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑒𝑥𝑐𝑒𝑙(′𝑘𝑜_𝑒𝑥𝑐𝑒𝑙. 𝑥𝑙𝑠𝑥′, 𝑠ℎ𝑒𝑒𝑡_𝑛𝑎𝑚𝑒 = ′𝑘𝑜′)
𝑝𝑟𝑖𝑛𝑡(𝑒𝑥𝑐𝑒𝑙)
Vemos que se importó la tabla, pero junto a ella también las dos primeras columnas vacías, y las
dos filas vacías. Además, la primera de estas filas vacías fue interpretada como índice de columna.
Para solucionar esto utilizaremos tres argumentos de la función read_excel(). Veamos cada
argumento:
Para indicar desde la fila que se comienza a leer. Utilizamos el argumento skiprows,
igualando el argumento al número de fila hasta la que incluso se borra o, desde la cual
comienza la tabla sin incluir dicha fila. Recuerde que como toda ubicación, las filas
comienzan desde 0.
Para indicar desde la columna que se comienza a leer. Utilizamos el argumento usecols, y
lo igualemos al rango de columnas que abarcan al DataFrame. Para señalar este rango
escribimos range(), cuyos extremos son inclusivos.
Para indicar la fila que será índice de columna. Utilizamos el argumento head,
asignándole la ubicación de la fila que será utilizada como índice de columnas.
Atendiendo a estos argumentos escribimos el siguiente código en reemplazo del anterior:
𝑒𝑥𝑐𝑒𝑙 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑒𝑥𝑐𝑒𝑙(′𝑘𝑜_𝑒𝑥𝑐𝑒𝑙. 𝑥𝑙𝑠𝑥′, 𝑠ℎ𝑒𝑒𝑡_𝑛𝑎𝑚𝑒 = ′𝑘𝑜′, 𝑠𝑘𝑖𝑝𝑟𝑜𝑤𝑠 = 1, ℎ𝑒𝑎𝑑𝑒𝑟 = 1, 𝑢𝑠𝑒𝑐𝑜𝑙𝑠 = 𝑟𝑎𝑛𝑔𝑒(2,8))
Para resolver el índice de filas se recomienda la lectura se la sección sobre importar archivos de
Excel.
Otro tema a evaluar es la presencia de valores nulos. Para corroborar la existencia o no de ellos
utilizamos la función/método insull(). Si la utilizamos como función, podremos imprimir el
resultado para observar visualmente si existe o no un valor nulo. Como función, reemplaza
temporariamente los valores de cada celda por un ‘false’ si en dicha celda hay un valor, y por un
‘true’ si en dicha celda hay un valor nulo. Como método, podemos utilizarla junto a sum() para
calcular la cantidad de valores nulos en cada variable/columna. Veamos ambos casos a
continuación:
Como función. El problema con este uso ocurre cuando tenemos grandes bases de datos,
pues no podremos ver una celda a la vez, es impráctico. Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑝𝑑. 𝑖𝑛𝑠𝑢𝑙𝑙(𝑒𝑥𝑐𝑒𝑙))
Como método. Aunque no podamos conocer exactamente la ubicación del valor nulo, saber
que existe y que está ubicado en determinada columna permite construir un criterio para
solucionar su presencia, decidiendo borrar la fila o reemplazando dicho valor por otra cosa.
Veamos cómo utilizar este método:
𝑝𝑟𝑖𝑛𝑡(𝑝𝑑. 𝑖𝑛𝑠𝑢𝑙𝑙(𝑒𝑥𝑐𝑒𝑙). 𝑠𝑢𝑚())
Ordenando el DataFrame
Imaginemos que queremos reordenar la información de un DataFrame de acuerdo a los valores de
una de sus columnas/variables. Por ejemplo, supongamos que una de estas variables es la fecha, o
ésta, es el índice del DataFrame ¿Cómo reordenamos todo? Veamos dos modos según la columna
sea o no el índice:
Columna cualquiera. En este caso utilizamos la función sort_values(), y como argumento
se coloca el nombre de la columna de interés. Otro argumento de la función es
“ascending”, si lo igualamos a True, entonces el DataFrame se ordenará de menor a
mayor, y si es False, se ordenará de mayor a menor (por defecto es True). El código sería,
para un DataFrame llamado df, el siguiente:
𝑑𝑓 = 𝑑𝑓. 𝑠𝑜𝑟𝑡_𝑣𝑎𝑙𝑢𝑒𝑠(′𝑜𝑝𝑒𝑛 ′ , 𝑎𝑠𝑐𝑒𝑛𝑑𝑖𝑛𝑔 = 𝑇𝑟𝑢𝑒)
También puede escribirse del siguiente modo:
𝑑𝑓 = 𝑑𝑓. 𝑜𝑝𝑒𝑛. 𝑠𝑜𝑟𝑡_𝑣𝑎𝑙𝑢𝑒𝑠(𝑎𝑠𝑐𝑒𝑛𝑑𝑖𝑛𝑔 = 𝑇𝑟𝑢𝑒)
Columna índice. En este caso usamos sort_index(), cuyo uso y argumento es similar al
explicado anteriormente. Por ejemplo:
𝑑𝑓 = 𝑑𝑓. 𝑠𝑜𝑟𝑡_𝑖𝑛𝑑𝑒𝑥(𝑎𝑠𝑐𝑒𝑛𝑑𝑖𝑛𝑔 = 𝐹𝑎𝑙𝑠𝑒)
Filtro de columnas
Veamos ahora cómo filtrar una columna, es decir, veamos cómo trabajar con cada columna por
separado. Para esto usaremos como ejemplo el archivo levantado desde internet. Veamos una pre-
visualización del mismo:
Imaginemos que deseamos trabajar primero la columna de precios de cierre. La forma más común
de trabajarla es la siguiente:
𝑑𝑎𝑡𝑎[′ 𝐶𝑙𝑜𝑠𝑒 ′ ]
Otro modo es escribiendo el llamado como si la columna fuese un atributo del DataFrame, de
hecho, todas las columnas de los DataFrame son atributos de la misma. Veamos:
𝑑𝑎𝑡𝑎. 𝐶𝑙𝑜𝑠𝑒
Si deseamos filtrar más de una columna a la vez utilizamos corchetes dobles, es decir, escribimos el
siguiente código:
𝑑𝑎𝑡𝑎[[′ 𝑂𝑝𝑒𝑛 ′ , ′𝐶𝑙𝑜𝑠𝑒′]]
𝑑𝑎𝑡𝑎[: 10: 2]
Estaremos pidiendo los 8 primeros valores de ubicación par (salto de dos en dos) del DataFrame
para cada una de las columnas, en otras palabras, obtendremos lo siguiente:
Esta lógica también puede aplicarse con las columnas de forma individual, veamos:
𝑑𝑎𝑡𝑎[′ 𝐶𝑙𝑜𝑠𝑒 ′ ][: 10: 2]
Del mismo modo, todo esto también puede lograrse utilizando la función iloc[] (locación por
índice, pues son las siglas de “index location”) quien cumple la misma función que el slicing. Esta
ya se describió en una de las primeras secciones donde se presentó por primera vez el DataFrame.
No obstante, aquí lo volvemos a presentar para refrescarlo y, para mostrar que a diferencia del
Slicing, esta función también permite trabajar las columnas, incluso en simultáneo a las filas.
Veamos cómo hacer esto con el siguiente código:
𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[: 10: 2, ∶ 2]
Con esto pedimos en filas lo mismo de antes, pero ahora lo pedimos sólo para las dos primeras
columnas, indicándolo con un “: 2” separado con una coma (la lógica es como la utilizada en
notación matricial, primero filas, luego columnas). En este sentido, la lógica del slicing de
columnas es la misma que para las filas. El resultado es el siguiente:
En línea con esto, si traemos sólo una línea del DataFrame, en lugar de obtener otro DataFrame
como mencionamos al inicio, lo que obtendremos será un objeto tipo “Data Series”. Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[0])
𝑝𝑟𝑖𝑛𝑡(𝑡𝑦𝑝𝑒(𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[0]))
Siguiendo en esta línea, si deseamos un dato en particular, podemos obtenerlo de dos modos: 1)
indicando la fila y luego la columna; o 2) al revés, indicando la columna y luego la fila. Veamos:
𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[0]. 𝑉𝑜𝑙𝑢𝑚𝑒
𝑑𝑎𝑡𝑎[′ 𝑉𝑜𝑙𝑢𝑚𝑒 ′ ]. 𝑖𝑙𝑜𝑐[0]
Con cualquiera de las líneas obtenemos 1260000.0.
Función between()
Esta función evalúa los valores del DataFrame como falsos o verdaderos al compararlos con los
argumentos que tiene en ella. De este modo, la función puede ser utilizada como ubicación al ser
combinada con otras funciones, por ejemplo, con el slicing, y de este modo, se puede obtener un
DataFrame que sea un recorte de otro.
Veamos cómo aplicar esto asumiendo que contamos con un DataFrame llamado ‘excel’ que puede
trabajarse sin problemas. Lo que queremos es recortarlo, para obtener otro que sólo contenga datos
de los años 2007 a 2009 (ambos inclusive). Sabemos, además, que existe una variable llamada
‘year’, cuyos valores son los años que se corresponden con el índice. Veamos cómo utilizar esta
función:
𝑝𝑟𝑖𝑛𝑡(𝑒𝑥𝑐𝑒𝑙[𝑒𝑥𝑐𝑒𝑙[year]. 𝑏𝑒𝑡𝑤𝑒𝑒𝑛(2007, 2009)])
Vea que, para lograr lo indicado se realizó el slice, luego, dentro del mismo se colocó la ubicación
seleccionando la columna y, al final, utilizando la función between().
Entonces, podemos crear una nueva columna a partir de la columna “timestamp”, cuyos valores
sean efectivamente tipo fecha. El cómo crear columnas nuevas sigue la misma lógica que la
explicada para diccionarios. Veamos el siguiente código para lograr el cometido:
𝑑𝑎𝑡𝑎[′ 𝐹𝑒𝑐ℎ𝑎_𝑝𝑜𝑠𝑡𝑎′] = 𝑝𝑑. 𝑡𝑜_𝑑𝑎𝑡𝑒𝑡𝑖𝑚𝑒(𝑑𝑎𝑡𝑎. 𝑡𝑖𝑚𝑒𝑠𝑡𝑎𝑚𝑝, 𝑓𝑜𝑟𝑚𝑎𝑡 =′ %𝑑/%𝑚/%𝑌′)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Vea que el primer argumento de la función to_datetime() es la columna a modificar, y el segundo
es el formato del string a transformar en fecha (formato Timestamp). Como se indicó en el párrafo
anterior, con esta función estamos creando los valores de la nueva columna, y asignándosela a su
nombre data[‘Fecha_posta’]. Veamos, el resultado en cuanto el tipo de dato:
𝑝𝑟𝑖𝑛𝑡(𝑡𝑦𝑝𝑒(𝑑𝑎𝑡𝑎[′𝐹𝑒𝑐ℎ𝑎_𝑝𝑜𝑠𝑡𝑎′]. 𝑖𝑙𝑜𝑐[0]))
¿Cómo fue posible realizar esto? Sucede que al crear la variable “año”, la misma se vincula
directamente con los índices del DataFrame “data”, por ende, al introducirla como una nueva
columna, sus valores se ordenan para que los índices de cada una se correspondan. En este sentido,
la variable “año” tiene un índice “oculto” o implícito, por ello puede posteriormente ordenarse en el
DataFrame. Finalmente, la referencia de orden es el mismo DataFrame llamado “data”, en otras
palabras, “año” se ordena para adaptarse a él. Esto es evidente hacer lo siguiente:
𝑝𝑟𝑖𝑛𝑡(𝑎ñ𝑜)
La primera columna no es una columna en sí misma, son los índices ocultos. Entonces, estos
índices también están presentes en “data”, y por ello es posible ordenar “año” adaptándolo a “data”.
Obsérvese que los argumentos de ambas funciones trabajan con la misma lógica, “desde : hasta”,
sin embargo, como la primera maneja ubicaciones, también permite saltos. Por otro lado, se
observa claramente cómo mientras iloc[] busca ubicación (índice), loc[] busca el nombre en el
índice. Finalmente, en el caso de loc[] el “desde: hasta” es inclusivo en ambos casos (como también
se aprecia al contrastar el resultado con su línea de código).
Una particularidad de la función loc[] es que permite obtener específicamente una u otra columna
sin necesidad de establecer el rango “desde:hasta”. Para esto, el código es:
𝑎𝑐𝑐𝑖𝑜𝑛𝑒𝑠. 𝑙𝑜𝑐[[′𝑃𝐴𝑀𝑃′ , ′𝐵𝐴𝐵𝐴′]]
Estas funciones trabajan de este modo en el caso de filas y columnas. El ejemplo de columnas se
omite por considerarse redundante.
Todas estas posibilidades de cortar el DataFrame permiten, en combinación con lo visto en las
secciones anteriores, construir nuevos DataFrame a partir de sólo uno o varios, como cuando se
trabaja con Excel y se crean nuevas variables a partir de otras.
A pesar de todo esto, la diferencia más importante entre estas funciones son sus argumentos, iloc[]
lee ubicación, mientras loc[] label o etiqueta. En este sentido, la columna de índice se lee de ambos
modos, por ende, si modificamos el orden del DataFrame, para hacer un slicing usando loc[]
deberemos prestar atención al índice que acompaña a cada valor, mientras que si usamos iloc[] no
deberemos cuidarnos de esto.
Para comprender mejor lo mencionado en el párrafo anterior basta con ver que la siguiente
DataFrame está “desordenada”. Veamos esto comenzando por el DataFrame original:
Vemos que las fechas están desordenadas, pues se comienza desde el 6 de marzo del 2020 y se
termina el 6 de marzo del 2000, y debería ser al revés. Para resolver esto aplicamos la siguiente
línea de código:
𝑑𝑎𝑡𝑎[∷ −1]
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Vemos que hemos resuelto este “desorden”, pero las etiquetas de la columna de índice no
cambiaron redefiniéndose, sino que se reordenaron junto al resto de la tabla. Ahora usemos las
funciones en cuestión:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[: 4])
Vemos que cada dato está separado del siguiente por puntos y comas. Este tipo de separación
implica un problema al momento de querer pasar los datos a un DataFrame, veamos:
𝑑𝑎𝑡𝑎 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑐𝑠𝑣(′𝑆𝑃𝑌. 𝑐𝑠𝑣 ′ )
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Vemos que los datos no están ordenados según columnas, sólo por filas. Este es el problema que
acarrean los puntos y comas. Para resolverlo utilizamos el argumento de la función que utilizamos
recién, indicando cuál es el separador:
𝑑𝑎𝑡𝑎 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑐𝑠𝑣(′𝑆𝑃𝑌. 𝑐𝑠𝑣 ′ , 𝑠𝑒𝑝 =′ ; ′)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Leyendo un HTML
Recordemos que siempre que importamos o leemos un archivo, debemos hacerlo utilizando la
librería pandas, en otras palabras, debemos importarla. Asimismo, en este caso usaremos la función
read_html() cuyo argumento es la página web desde donde descargamos el DataFrame.
Comencemos con el siguiente código:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑝𝑎𝑛𝑑𝑎𝑠 𝑎𝑠 𝑝𝑑
𝑑𝑎𝑡𝑎 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_ℎ𝑡𝑚𝑙(′https://en. wikipedia. org/wiki/List_of_S%26P_500_companies′)[0]
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Ahora, para conseguir lo que queremos, los tickers, usaremos el código del profe, que no explica
cómo se escribe, así que aquí directamente lo transcribimos:
Como comentario: En general, al importar archivos se suelen importar formatos CSV, pues estos
son texto compatible con cualquier sistema operativo, incluso MAC, mientras que el formato XLS
lo es sólo con Windows. Asimismo, XLS sólo cuenta con un máximo de 1.048.575 de filas,
mientras que un formato CSV no tiene límite.
Aquí vemos que, por ejemplo, si el DataFrame tiene más de 60 filas, entonces automáticamente lo
corta en la quinta para mostrar los tres puntos y luego mostrar las últimas cinco filas. Así ocurre
con el resto de los valores por defecto.
Para cambiar estos valores por defecto usamos todo lo anterior como atributos de display. Veamos
un ejemplo donde el máximo de 60 filas lo reducimos hasta 8, y el de columnas lo reducimos de 20
a 10. Veamos cómo escribirlo:
𝑝𝑑. 𝑜𝑝𝑡𝑖𝑜𝑛𝑠. 𝑑𝑖𝑠𝑝𝑙𝑎𝑦. max _𝑟𝑜𝑤𝑠 = 8
𝑝𝑑. 𝑜𝑝𝑡𝑖𝑜𝑛𝑠. 𝑑𝑖𝑠𝑝𝑙𝑎𝑦. max _𝑐𝑜𝑙𝑢𝑚𝑛𝑠 = 10
Luego de esto escribimos el código de importación nuevamente. NOTA: por alguna razón, no
podemos hacer esto en nuestro spider, sin embargo, el profe sí tuvo éxito al aplicarlo, en este
sentido se muestra una foto de su código a continuación.
Para eliminar la fila con el dato nulo (Nan) usamos el método mencionado, quien por defecto ya
elimina columnas con este tipo de datos:
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝𝑛𝑎()
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Si deseamos que en lugar de filas se eliminen las columnas que tengan estos datos, entonces
debemos modificar el argumento del método, veamos:
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝𝑛𝑎(𝑎𝑥𝑖𝑠 = 1)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Esto puede ser útil para cuando tomamos una sub matriz del DataFrame y deseamos que el índice
comience de 0 nuevamente.
Operaciones matriciales
Cuando deseamos modificar alguna columna o fila, si bien es posible utilizar un bucle for, lo mejor
es utilizar las propiedades matriciales o una combinación. Veremos tres modos de hacerlo (aunque
hay más), el primero, pero no el más eficiente es multiplicar la matriz o vector por un escalar y
luego sumar. El segundo consiste en combinar el bucle for con las propiedades matriciales. Y el
tercero es aplicar las propiedades matriciales relacionadas al producto entre matrices.
Veamos el primero, ya mencionado en alguna sección anterior, supongamos que estamos
trabajando con el archivo AAPL.xlsx y deseamos obtener el precio promedio ponderado entre
“open” y “close”, otorgando un 30% y un 70% de peso respectivamente. Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑜𝑝𝑒𝑛 ∗ 0.3 + 𝑑𝑎𝑡𝑎. 𝑐𝑙𝑜𝑠𝑒 ∗ 0.7)
Ahora apliquemos lo mismo, pero como una combinación de bucle for y lo anterior (se pasa foto de
lo que hizo el profe porque ahora estoy cansado como para pasarlo a mano). Como se verá, “p” es
un vector lista que tiene las ponderaciones.
Un detalle sobre este código, px comienza siendo un int, pero termine siendo una serie. Esto ocurre
en el bucle for, donde le asignamos el resultado de multiplicar el vector columna por el escalar. Es
gracias a esto que hacia el final del código podemos asignar px al DataFrame.
Finalmente, la multiplicación entre matrices puede hacerse utilizando la librería pandas o, la
librería numpy. Como veremos ambos modos, descargaremos la librería que falta:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑛𝑢𝑚𝑝𝑦 𝑎𝑠 𝑛𝑝
Lo siguiente es utilizar el método dot() que sirve para productos matriciales. En el caso de la
librería numpy, los argumentos de este método son las matrices o vectores que se multiplicarán, y
claro, ambos deben respetar las propiedades del producto matricial, esto es, que las columnas de la
matriz de la izquierda del producto coincidan en cantidad con las filas de la matriz de la derecha.
En este sentido, si uno de los argumentos es una lista, el programa la “acomodará” como
corresponda para poder calcular el producto, esto es, una lista puede pensarse como un vector fila o
columna, lo que hace anaconda es interpretarla de uno u otro modo de acuerdo a lo que permita
realizar el cálculo sin errores.
Lo que vemos a continuación es la aplicación de este método de dos modos diferentes, pero con el
mismo resultado. La aplicación se hace sobre el archivo AAPL.xlsx. Veamos:
𝑝 = [0.2,0.15,0.15,0.5]
𝑑𝑎𝑡𝑎[′𝑃𝑅𝐸𝐶𝐼𝑂_𝑂𝐻𝐿𝐶′] = 𝑛𝑝. 𝑑𝑜𝑡(𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[: , : 4], 𝑝)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
El segundo modo de resolver esto es con la librería pandas, quien también utiliza el método dot(),
pero en este caso, tiene un único argumento, que será la matriz o vector que está a la derecha del
producto. El resultado obtenido es el mismo, veamos el código:
𝑝 = [0.2,0.15,0.15,0.5]
𝑑𝑎𝑡𝑎[′𝑃𝑅𝐸𝐶𝐼𝑂_𝑂𝐻𝐿𝐶′] = 𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[: , : 4]. 𝑑𝑜𝑡(𝑝)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Ahora veamos cómo utilizar la función where() de numpy. Para esto debemos trabajar con las
librerías pandas (por el uso del DataFrame), y con la librería numpy (por la función). Los
argumentos de esta función son similares a un condicional if, desde que primero se escribe una
condición, le sigue el resultado a aplicar si ella se cumple, y luego el resultado si no se cumple.
Asimismo, y como el condicional if, también puede concatenarse, volviendo a llamar a la función
where() en lugar del resultado aplicado cuando la condición no se cumple. Lo que haremos será
crear el valor o resultado deseado utilizando esta función, y lo asignaremos a la nueva columna.
Veamos cómo utilizamos esta función para resolver lo propuesto:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑛𝑢𝑚𝑝𝑦 𝑎𝑠 𝑛𝑝
𝑑𝑎𝑡𝑎[′𝐶𝑜𝑙𝑜𝑟_𝑉𝑒𝑙𝑎′] = 𝑛𝑝. 𝑤ℎ𝑒𝑟𝑒(𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′]
> 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′], ′𝑅𝑜𝑗𝑜′, 𝑛𝑝. 𝑤ℎ𝑒𝑟𝑒(𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′]
< 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′], ′𝑉𝑒𝑟𝑑𝑒′, ′𝐷𝑜𝑗𝑖′))
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Esto también lo podemos conseguir usando máscaras (filtros condicionales). Estas son útiles como
método de trabajo cuando queremos crear una nueva columna o fila en un DataFrame, y estamos
utilizando variables categóricas. Una máscara es una fracción del DataFrame obtenido a partir de
un operador lógico, y al ser una fracción, es posible utilizarlo para crear nuevas variables y valores.
Entonces, para trabajar con una máscara, primero debemos crearla a partir de un DataFrame, la
función .loc[], y operadores lógicos, y luego la podremos utilizar. Veamos cómo la creamos en este
caso:
𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′] > 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′]
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′] > 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′])
Lo que haremos ahora será crear la máscara, quien tendrá la fracción de valores booleanos
verdaderos dentro de esta fracción del DataFrame. Para esto sólo debemos pedir esta fracción
recortada para los valores verdaderos:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑙𝑜𝑐[𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′] > 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′]])
Vemos que la cantidad de filas disminuyó, lógicamente, quedando sólo aquellas donde el precio de
apertura es mayor al de cierre. Esto es una máscara.
Finalmente, para colocar “Rojo” en una nueva columna llamada “Color_Vela”, cuando el precio de
apertura es mayor al de cierre, sólo asignamos el string a la columna. En otras palabras, utilizando
la máscara e iloc[] creamos la columna y le asignamos el valor a cada celda de acuerdo al operador
lógico. Veamos:
𝑑𝑎𝑡𝑎. 𝑙𝑜𝑐[𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′] > 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′], ′𝐶𝑜𝑙𝑜𝑟_𝑉𝑒𝑙𝑎′] = ′𝑅𝑜𝑗𝑜′
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Observe que, 𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′] > 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′] como argumento de la función iloc[] significa que se
busca el número de la ubicación siempre que se cumpla la condición lógica. Por ejemplo, la
primera fila, 0, cumple la condición, por ende, dicha expresión es igual a 0.
En definitiva, para completar el objetivo planteado al inicio, se escribe el siguiente código:
𝑑𝑎𝑡𝑎. 𝑙𝑜𝑐[𝑑𝑎𝑡𝑎[′𝑜𝑝𝑒𝑛′] > 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′], ′𝐶𝑜𝑙𝑜𝑟_𝑉𝑒𝑙𝑎′] = ′𝑅𝑜𝑗𝑜′
𝑑𝑎𝑡𝑎. 𝑙𝑜𝑐[𝑑𝑎𝑡𝑎[′ 𝑜𝑝𝑒𝑛 ′ ] < 𝑑𝑎𝑡𝑎[′ 𝑐𝑙𝑜𝑠𝑒 ′ ,′ 𝐶𝑜𝑙𝑜𝑟_𝑉𝑒𝑙𝑎′] =′ 𝑉𝑒𝑟𝑑𝑒′
𝑑𝑎𝑡𝑎. 𝑙𝑜𝑐[𝑑𝑎𝑡𝑎[′ 𝑜𝑝𝑒𝑛 ′ ] == 𝑑𝑎𝑡𝑎[′𝑐𝑙𝑜𝑠𝑒′], ′𝐶𝑜𝑙𝑜𝑟_𝑉𝑒𝑙𝑎′] = ′𝐷𝑜𝑗𝑖′
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Where() de DataFrame
Esta función es similar al where() de numpy, pero difiere en la relación condición – resultado. En
este caso, sólo hay dos argumentos (no tres), y el primero es la condición a cumplirse, y el segundo
es el resultado que deberá aplicarse en caso donde la condición no se cumpla. Por ende, dicha
condición sirve para mantener el valor que actualmente existe. Veamos el ejemplo dado en clase
por el profesor:
Aquí estamos diciendo que por defecto, el valor es ‘Verde’, y éste se mantendrá siempre que se
cumpla que el precio de cierre sea menor al de apertura, en caso contrario, el valor será ‘Roja’.
Resampleo
Mediante el método resample podemos reagrupar rápidamente una serie dada en función de
diferentes timeframes. Es importante aclarar que para que funcione el resampleo, el índice de la
tabla debe ser el timestamp. Las Frecuencias posibles son:
B = business day frequency
D = calendar day frequency
W = weekly frequency
M = month end frequency
BM = business month end frequency
MS = month start frequency
BMS = business month start frequency
Q = quarter end frequency
BQ = business quarter endfrequency
QS = quarter start frequency
BQS = business quarter start frequency
A = year end frequency
BA = business year end frequency
AS = year start frequency
BAS = business year start frequency
BH = business hour frequency
H = hourly frequency
T = minutely frequency
S = secondly frequency
L = milliseonds
Veamos el ejemplo que usó el profesor:
Cómo calcular estadísticas agrupando categorías
Partiendo de la imagen siguiente, la idea es lograr la frecuencia de las velas de acuerdo a cada
categoría, es decir, queremos saber cuántas velas verdes hay, cuántas rojas, y cuántas doji. Para
esto utilizamos la función groupby() cuyo argumento es el nombre de la columna que agrupa las
categorías con quienes agruparemos. Veamos cómo utilizarla:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑔𝑟𝑜𝑢𝑝𝑏𝑦(′𝐶𝑜𝑙𝑜𝑟_𝑉𝑒𝑙𝑎′). 𝑠𝑖𝑧𝑒())
Del mismo modo, si deseamos conocer cuál es el cambio promedio del precio de cierre para cada
grupo de velas, primero creamos las columnas de cambio diario, y luego utilizamos esta función
para obtener lo buscado. Veamos:
𝑑𝑎𝑡𝑎[′𝑣𝑎𝑟𝑖𝑎𝑐𝑖ó𝑛′] = 𝑑𝑎𝑡𝑎. 𝑐𝑙𝑜𝑠𝑒. 𝑝𝑐𝑡_𝑐ℎ𝑎𝑛𝑔𝑒()
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑔𝑟𝑜𝑢𝑝𝑏𝑦(′𝐶𝑜𝑙𝑜𝑟_𝑉𝑒𝑙𝑎′). 𝑣𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛. 𝑚𝑒𝑎𝑛())
Veamos que primero agrupamos y después llamamos la columna, para finalmente aplicar la media.
Quantiles y Ranks
En esta sección veremos estadística con DataFrame, por ende, es importante tener en claro las
líneas de código anteriormente descriptas. En particular, aquí nos concentraremos en Rank(), quien
asigna un número a cada índice de acuerdo al orden en el ranking, el cual puede armarse de menor
a mayor o, de mayor a menor. También nos concentraremos en la función Quantile(), quien
permite conocer el valor de todas las variables asociadas a determinado ranking, sólo se debe
colocar el número de orden como argumento.
Trabajaremos con la serie de precios y volumen correspondiente a GGAL, que descargaremos de
Yahoo Finance, por ende, primero prepararemos esta información:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑦𝑓𝑖𝑛𝑎𝑛𝑐𝑒 𝑎𝑠 𝑦𝑓
𝑑𝑎𝑡𝑎 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(′𝐺𝐺𝐴𝐿′)
𝑑𝑎𝑡𝑎[′𝑉𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛′] = 𝑑𝑎𝑡𝑎[′𝐴𝑑𝑗 𝐶𝑙𝑜𝑠𝑒′]. 𝑝𝑐𝑡_𝑐ℎ𝑎𝑛𝑔𝑒() ∗ 100
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝𝑛𝑎()
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Rank
Como se mencionó anteriormente, este método rank() asigna un número de orden a cada índice, el
cual señala la ubicación de dicho índice en un ranking construido con referencia a una variable en
particular. Si no escribimos nada en su argumento, entonces se ordena de menor a mayor, siendo el
número 1 el menor de todos. Veamos un ejemplo utilizando los datos de GGAL preparados en la
sección anterior, la intención es crear un ranking a partir de los valores de la variable “Variacion”.
Veamos cómo lo hacemos:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑉𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛. 𝑟𝑎𝑛𝑘())
Aquí se puede ver que para cada índice (fecha) hay un número. Este número indica el orden de la
variación asociada al mismo índice. Veamos que esta información puede asignarse a una columna
nueva del DataFrame:
𝑑𝑎𝑡𝑎[′ 𝑟𝑎𝑛𝑘𝑖𝑛𝑔 ′ ] = 𝑑𝑎𝑡𝑎. 𝑉𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛. 𝑟𝑎𝑛𝑘()
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Si queremos conocer cuáles son las diez variaciones diaria de cierre más bajas, primero re
ordenamos la tabla de acuerdo a la columna “ranking” y luego seleccionamos las 10 primeras filas.
Para esto utilizaremos la función sort_values(), y el método head(). La función tiene como
argumento la variable con quien se ordena, y el método tiene como argumento la cantidad de filas
que queremos ver, contando desde la primera. Veamos:
𝑑𝑎𝑡𝑎2 = 𝑑𝑎𝑡𝑎. 𝑠𝑜𝑟𝑡_𝑣𝑎𝑙𝑢𝑒𝑠(′𝑟𝑎𝑛𝑘𝑖𝑛𝑔′, 𝑎𝑠𝑐𝑒𝑛𝑑𝑖𝑛𝑔 = 𝑇𝑟𝑢𝑒). ℎ𝑒𝑎𝑑(10)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎2)
Esta información es en términos absolutos y por ello no es útil para comparar, pues necesitamos
relativizarlo respecto al tamaño del ranking, por ello utilizamos quantiles, percentiles, etc. En este
caso podemos utilizar percentiles, lo cual requiere dividir el número de orden por el total de filas, o,
utilizar el argumento pct=True de Rank(). Veamos esto reescribiendo el código:
𝑑𝑎𝑡𝑎[′ 𝑟𝑎𝑛𝑘𝑖𝑛𝑔′ ] = 𝑑𝑎𝑡𝑎. 𝑉𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛. 𝑟𝑎𝑛𝑘(𝑝𝑐𝑡 = 𝑇𝑟𝑢𝑒)
𝑑𝑎𝑡𝑎2 = 𝑑𝑎𝑡𝑎. 𝑠𝑜𝑟𝑡_𝑣𝑎𝑙𝑢𝑒𝑠(′𝑟𝑎𝑛𝑘𝑖𝑛𝑔′, 𝑎𝑠𝑐𝑒𝑛𝑑𝑖𝑛𝑔 = 𝑇𝑟𝑢𝑒). ℎ𝑒𝑎𝑑(10)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎2)
Quantiles
La función Quantile() permite conocer el valor de todas las variables asociadas al índice rankeado.
Para utilizarla debemos colocar el ranking de la variable como argumento. Entonces, partiendo de
la misma base de datos, escribimos nuevamente las mismas líneas de código que antes, quienes
asignaron una posición relativa a cada índice de acuerdo al ranking menor a mayor de la variable
“Variación”. Estas líneas pueden buscar en la sección anterior (Rank), aquí sólo mostraremos el
resultado, quien es nuestro punto de partida:
Si aplicamos la función Quantile() para el ranking 0,5, tendremos el valor de todas las variables:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑞𝑢𝑎𝑛𝑡𝑖𝑙𝑒(0.5))
Ahora apliquemos la función para el mismo ranking, pero sólo para la variable “Variacion”:
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑉𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛. 𝑞𝑢𝑎𝑛𝑡𝑖𝑙𝑒(0.5))
Lo siguiente es calcular el retorno diario logarítmico de estos precios de cierre. La función que
utilizaremos es log() de numpy, por ende, en su argumento colocaremos los vectores filas que
usaremos. En este sentido, el método shift() será útil para indicar que queremos una cotización
anterior a la actual, y para esto, en su argumento pondremos 1.
Luego de calcular estos retornos, deberemos obtener el retorno promedio para cada día de la
semana, por ello, primero los agruparemos según el día, y después obtendremos su promedio.
Finalmente, a este promedio lo anualizaremos.
Lo siguiente será crear un nuevo índice, donde cada fila será un día de la semana, para luego
transponerlo así el mismo queda como columna. Finalmente aplicaremos la función describe(), que
es útil para media, meadiana, máx, mín, std y conteos para una columna en particular. La
particularidad de esta función es que calcula estas medidas tomando todos los datos, es decir, no la
calcula para cada papel, sino para todos. Veamos el código:
𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠 = 𝑛𝑝. 𝑙𝑜𝑔((𝑐𝑖𝑒𝑟𝑟𝑒𝑠/𝑐𝑖𝑒𝑟𝑟𝑒𝑠. 𝑠ℎ𝑖𝑓𝑡(1)))
𝑚𝑎𝑡_𝑟𝑒𝑡 = 𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠. 𝑔𝑟𝑜𝑢𝑝𝑏𝑦(𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠. 𝑖𝑛𝑑𝑒𝑥. 𝑑𝑎𝑦𝑜𝑓𝑤𝑒𝑒𝑘). 𝑚𝑒𝑎𝑛() ∗ 250
𝑚𝑎𝑡_𝑟𝑒𝑡 = 𝑚𝑎𝑡_𝑟𝑒𝑡. 𝑐𝑙𝑖𝑝(𝑙𝑜𝑤𝑒𝑟 = −1, 𝑢𝑝𝑝𝑒𝑟 = 1)
𝑚𝑎𝑡_𝑟𝑒𝑡. 𝑖𝑛𝑑𝑒𝑥 = [′𝐿𝑢𝑛𝑒𝑠′, ′𝑀𝑎𝑟𝑡𝑒𝑠′, ′𝑀𝑖𝑒𝑟𝑐𝑜𝑙𝑒𝑠′, ′𝐽𝑢𝑒𝑣𝑒𝑠′, ′𝑉𝑖𝑒𝑟𝑛𝑒𝑠′]
𝑟𝑒𝑠𝑢𝑚𝑒𝑛 = 𝑚𝑎𝑡_𝑟𝑒𝑡. 𝑡𝑟𝑎𝑛𝑠𝑝𝑜𝑠𝑒()
𝑟𝑒𝑠𝑢𝑚𝑒𝑛. 𝑑𝑒𝑠𝑐𝑟𝑖𝑏𝑒()
𝑝𝑟𝑖𝑛𝑡(𝑟𝑒𝑠𝑢𝑚𝑒𝑛)
Como surge de este ejercicio, el archivo manejado es muy grande para la capacidad de la máquina
actual (i3), por ende, cuando esto ocurre se puede guardar el archivo en formato CSV o Excel, para
luego importarlo desde la misma máquina. Este proceso de importación también tardará, pero
mucho menos. Recuerdo que en estos casos se deberá indicar que el índice corresponde a la
columna cuyo nombre tiene a las fechas como valores.
Persistir objeto serializado
Lo que veremos a continuación es útil para guardar objetos muy grandes tipo DataFrame, quienes
al tener un tamaño inmenso vuelven lento los cálculos. Con la librería pickle podremos serializar y
guardar objetos serializados. Para serializar importamos la librería y serializamos del siguiente
modo:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑝𝑖𝑐𝑘𝑙𝑒
𝑤𝑖𝑡ℎ 𝑜𝑝𝑒𝑛(′𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠. 𝑑𝑎𝑡′, ′𝑤𝑏′) 𝑎𝑠 𝑓𝑖𝑙𝑒:
𝑝𝑖𝑐𝑘𝑙𝑒. 𝑑𝑢𝑚𝑝(𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠, 𝑓𝑖𝑙𝑒)
El archivo serializado se levanta del siguiente modo:
𝑤𝑖𝑡ℎ 𝑜𝑝𝑒𝑛(′𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠. 𝑑𝑎𝑡′, ′𝑟𝑏′) 𝑎𝑠 𝑓𝑖𝑙𝑒:
𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠_𝑙𝑜𝑎𝑑 = 𝑝𝑖𝑐𝑘𝑙𝑒. 𝑙𝑜𝑎𝑑(𝑓𝑖𝑙𝑒)
𝑝𝑟𝑖𝑛𝑡(𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠_𝑙𝑜𝑎𝑑)
Histogramas de un DataFrame
Continuamos utilizando los archivos de las últimas dos secciones, por si acaso, esto implica que
importadores el archivo serializados siguiente:
𝑤𝑖𝑡ℎ 𝑜𝑝𝑒𝑛(′𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠. 𝑑𝑎𝑡′, ′𝑟𝑏′) 𝑎𝑠 𝑓𝑖𝑙𝑒:
𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠_𝑙𝑜𝑎𝑑 = 𝑝𝑖𝑐𝑘𝑙𝑒. 𝑙𝑜𝑎𝑑(𝑓𝑖𝑙𝑒)
𝑚𝑎𝑡_𝑟𝑒𝑡 = 𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠_𝑙𝑜𝑎𝑑. 𝑔𝑟𝑜𝑢𝑝𝑏𝑦(𝑟𝑒𝑡𝑜𝑟𝑛𝑜𝑠_𝑙𝑜𝑎𝑑. 𝑖𝑛𝑑𝑒𝑥. 𝑑𝑎𝑦𝑜𝑓𝑤𝑒𝑒𝑘). 𝑚𝑒𝑎𝑛() ∗ 250
Los histogramas visto antes tienen un problema, no permiten comparar visualmente y con facilidad
las distribuciones de frecuencias para los diferentes días. La herramienta que sí permite realizar tal
cosa es el diagrama de caja o BoxPlot. Para conseguirlo escribimos el siguiente código:
𝑝𝑑. 𝑝𝑙𝑜𝑡𝑡𝑖𝑛𝑔. 𝑏𝑜𝑥𝑝𝑙𝑜𝑡_𝑓𝑟𝑎𝑚𝑒(𝑟𝑒𝑠𝑢𝑚𝑒𝑛, 𝑤ℎ𝑖𝑠 = 1.5, 𝑠ℎ𝑜𝑤𝑚𝑒𝑎𝑛𝑠 = 𝑇𝑟𝑢𝑒, 𝑓𝑖𝑔𝑠𝑖𝑧𝑒 = (12,7))
Este código pertenece a la librería pandas y nos permite conseguir lo siguiente:
Funciones acumulativas
Estas funciones son útiles para acumular desde un valor que se considera inicial, hasta el valor que
se considera final. Estas acumulaciones pueden ser por suma o por producto. En términos de un
DataFrame, una función acumulativa permite trabajar en la columna "n" de la tabla con todos los
datos de las filas "0 a n". Dentro de este tipo de funciones encontramos:
.cummax() Permite conseguir el valor máximo acumulado para el rango de fila
seleccionado, por ejemplo, es ideal para máximo histórico por fecha. Aplicado a una serie
de precios, calculará el precio máximo para la primera fila, luego para el conjunto
compuesto por la primera y segunda fila, luego este conjunto se extenderá hasta la tercera,
y así sucesivamente.
.cummin() Permite conseguir el valor mínimo acumulado para el rango de fila
seleccionado, por ejemplo, es ideal para mínimo histórico por fecha. Aplicado a una serie
de precios, calculará el precio mínimo para la primera fila, luego para el conjunto
compuesto por la primera y segunda fila, luego este conjunto se extenderá hasta la tercera,
y así sucesivamente.
.cumsum() Es una sumatoria de las filas indicadas, por ejemplo, es ideal para armado de
subtotales por fecha.
.cumprod() Es una productoria de las filas indicadas, por ejemplo, es ideal para
rendimiento compuesto.
Cummax()
Para ejemplificar su uso utilicemos la serie de precios de GGAL descargada con la librería
yfinance. Como veremos, el argumento “auto_adjust” de la función “yf.dowload” cambia la serie
de precios de cierre por la de cierre ajustados, pero siempre que coloquemos “True”. De todas las
series de precios descargadas, trabajemos exclusivamente con la de precios de cierre ajustados.
Crearemos una variable/columna nueva, cuyos valores serán el precio de cierre ajustado máximo
registrado hasta ese momento (índice). Veamos el código:
𝑑𝑎𝑡𝑎 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(′𝐺𝐺𝐴𝐿′, 𝑎𝑢𝑡𝑜_𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑇𝑟𝑢𝑒)
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝(["𝐻𝑖𝑔ℎ", "𝐿𝑜𝑤", "𝑉𝑜𝑙𝑢𝑚𝑒"], 𝑎𝑥𝑖𝑠 = 1)
𝑑𝑎𝑡𝑎[′𝑚𝑎𝑥𝐻𝑖𝑠𝑡′] = 𝑑𝑎𝑡𝑎. 𝐶𝑙𝑜𝑠𝑒. 𝑐𝑢𝑚𝑚𝑎𝑥()
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. ℎ𝑒𝑎𝑑(6))
Cummin()
Para ejemplificar su uso utilicemos la serie de precios de GGAL descargada con la librería
yfinance. Como veremos, el argumento “auto_adjust” de la función “yf.dowload” cambia la serie
de precios de cierre por la de cierre ajustados, pero siempre que coloquemos “True”. De todas las
series de precios descargadas, trabajemos exclusivamente con la de precios de cierre ajustados.
Crearemos una variable/columna nueva, cuyos valores serán el precio de cierre ajustado mínimo
registrado hasta ese momento (índice). Veamos el código:
𝑑𝑎𝑡𝑎 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(′𝐺𝐺𝐴𝐿′, 𝑎𝑢𝑡𝑜_𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑇𝑟𝑢𝑒)
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝(["𝐻𝑖𝑔ℎ", "𝐿𝑜𝑤", "𝑉𝑜𝑙𝑢𝑚𝑒"], 𝑎𝑥𝑖𝑠 = 1)
𝑑𝑎𝑡𝑎[′𝑚𝑖𝑛𝐻𝑖𝑠𝑡′] = 𝑑𝑎𝑡𝑎. 𝐶𝑙𝑜𝑠𝑒. 𝑐𝑢𝑚𝑚𝑖𝑛()
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
𝑑𝑎𝑡𝑎. ℎ𝑒𝑎𝑑(6)
.
Habiendo calculado los máximos y mínimos históricos alcanzados en cada fecha, los mismos
pueden ordenarse gráficamente junto con la serie de precios de cierre ajustados. Esto permite
apreciar si la cotización actual se encuentra cerca de su mínimo o máximo histórico. Veamos:
𝑑𝑎𝑡𝑎. 𝑖𝑙𝑜𝑐[: , −3: ]. 𝑝𝑙𝑜𝑡(𝑙𝑜𝑔𝑦 = 𝑇𝑟𝑢𝑒)
𝑐𝑢𝑚𝑠𝑢𝑚(𝑋𝑛) = ∑ 𝑥𝑖
𝑖=0
Para ejemplificar su uso continuamos utilizando la serie de precios de cierre ajustados de GGAL.
Lo que haremos será descargar esta serie, y crear la columna que contenga la suma acumulada del
volumen por millón de nominales (por ello, la suma se hará sobre el volumen operado dividido un
millón). Veamos:
𝑑𝑎𝑡𝑎 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(′𝐺𝐺𝐴𝐿′, 𝑎𝑢𝑡𝑜_𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑇𝑟𝑢𝑒)
𝑑𝑎𝑡𝑎[′𝑣𝑜𝑙𝑢𝑚𝑒𝑛𝐴𝑐𝑢𝑚′] = 𝑑𝑎𝑡𝑎. 𝑉𝑜𝑙𝑢𝑚𝑒. 𝑐𝑢𝑚𝑠𝑢𝑚()/1000000
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝([′𝑂𝑝𝑒𝑛′, ′𝐻𝑖𝑔ℎ′, ′𝐿𝑜𝑤′, ′𝐶𝑙𝑜𝑠𝑒′], 𝑎𝑥𝑖𝑠 = 1). 𝑑𝑟𝑜𝑝𝑛𝑎(). 𝑟𝑜𝑢𝑛𝑑(2)
𝑑𝑎𝑡𝑎. ℎ𝑒𝑎𝑑(6)
Cumprod()
Cumprod() es una función de productoria, es decir el producto acumulado de 0 a n, para la fila n.
Algebraicamente:
𝑛
𝐶𝑢𝑚𝑝𝑟𝑜𝑑(𝑋𝑛) = ∏ 𝑥𝑖
𝑖=0
Para ejemplificar su uso calculemos el rendimiento compuesto. Para esto, deberemos cumplir tres
pasos:
1. Creamos una columna "variación" con el valor "r", rendimiento porcentual diario.
2. Creamos una columna "factor" con el valor (1+r).
3. Luego vamos a aplicar el productorio para cada fila de esa columna "factor" y le restamos
1 al resultado.
Veamos las líneas de código:
𝑑𝑎𝑡𝑎 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(′𝐺𝐺𝐴𝐿′, 𝑎𝑢𝑡𝑜_𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑇𝑟𝑢𝑒)
𝑑𝑎𝑡𝑎[′𝑣𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛′] = 𝑑𝑎𝑡𝑎[′𝐶𝑙𝑜𝑠𝑒′]. 𝑝𝑐𝑡_𝑐ℎ𝑎𝑛𝑔𝑒()
𝑑𝑎𝑡𝑎[′𝑓𝑎𝑐𝑡𝑜𝑟′] = 1 + 𝑑𝑎𝑡𝑎[′𝑣𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛′]
𝑑𝑎𝑡𝑎[′𝑟𝑒𝑛𝑑𝑖𝑚𝑖𝑒𝑛𝑡𝑜𝐴𝑐𝑢𝑚′] = (𝑑𝑎𝑡𝑎. 𝑓𝑎𝑐𝑡𝑜𝑟. 𝑐𝑢𝑚𝑝𝑟𝑜𝑑() − 1) ∗ 100
𝑑𝑎𝑡𝑎[′𝑏𝑎𝑠𝑒100′] = (𝑑𝑎𝑡𝑎. 𝑓𝑎𝑐𝑡𝑜𝑟. 𝑐𝑢𝑚𝑝𝑟𝑜𝑑()) ∗ 100
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝([′𝑂𝑝𝑒𝑛′, ′𝐻𝑖𝑔ℎ′, ′𝐿𝑜𝑤′, ′𝐶𝑙𝑜𝑠𝑒′, ′𝑉𝑜𝑙𝑢𝑚𝑒′], 𝑎𝑥𝑖𝑠 = 1). 𝑑𝑟𝑜𝑝𝑛𝑎(). 𝑟𝑜𝑢𝑛𝑑(4)
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎)
Librería MatPlotLib
Partiendo de los datos trabajados en la sección anterior, quienes se trabajaron con la función
cumprod(), veremos cómo graficarlos de un modo mucho mejor utilizando la librería MatPlotLib.
La intención es graficar la ganancia acumulada que se tiene en cada momento del tiempo, siendo
que la compra de la acción fue en el año 2000. En este sentido, la intención es mostrar esto junto
con los eventos políticos de la última elección.
Para lograr esto utilizaremos dos sub-paquetes de esta librería: “pyplot”, y “dates”, este último
permite trabajar fechas (reformatearlas). También utilizaremos la librería “datetime” que ya fue
presentada en una de las tres primeras clases (está en el resumen hecho en papel). En la siguiente
web se pueden encontrar explicaciones sobre cómo usar el módulo de esta librería:
https://aprendeconalf.es/docencia/python/manual/matplotlib/
Un detalle sobre el uso de esta nueva librería, cuando grafiquemos es necesario instanciar la figura
y su eje de forma conjunta, esto es crearlas. Esto se realiza en una misma línea de código, que en
este caso la escribimos con color rojo para identificarla a los fines educativos.
Los comentarios sobre el código se marcarán en verde oscuro:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑚𝑎𝑡𝑝𝑙𝑜𝑡𝑙𝑖𝑏. 𝑝𝑦𝑝𝑙𝑜𝑡 𝑎𝑠 𝑝𝑙𝑡
𝑖𝑚𝑝𝑜𝑟𝑡 𝑚𝑎𝑡𝑝𝑙𝑜𝑡𝑙𝑖𝑏. 𝑑𝑎𝑡𝑒𝑠 𝑎𝑠 𝑚𝑑𝑎𝑡𝑒𝑠
𝑓𝑟𝑜𝑚 𝑑𝑎𝑡𝑒𝑡𝑖𝑚𝑒 𝑖𝑚𝑝𝑜𝑟𝑡 𝑑𝑎𝑡𝑒𝑡𝑖𝑚𝑒 𝑎𝑠 𝑑𝑡𝑑𝑡
# 𝐶𝑜𝑛 𝑒𝑙 𝑠𝑖𝑔𝑢𝑖𝑒𝑛𝑡𝑒 𝑐ó𝑑𝑖𝑔𝑜 𝑠𝑒 𝑐𝑟𝑒𝑎 𝑙𝑎 𝑓𝑖𝑔𝑢𝑟𝑎 𝑜 𝑙𝑜𝑠 𝑒𝑗𝑒.
𝑓𝑖𝑔, 𝑎𝑥 = 𝑝𝑙𝑡. 𝑠𝑢𝑏𝑝𝑙𝑜𝑡𝑠(𝑓𝑖𝑔𝑠𝑖𝑧𝑒 = (10,5))
𝑐𝑢𝑟𝑣𝑎 = 𝑑𝑎𝑡𝑎. 𝑟𝑒𝑛𝑑𝑖𝑚𝑖𝑒𝑛𝑡𝑜𝐴𝑐𝑢𝑚. 𝑟𝑜𝑙𝑙𝑖𝑛𝑔(30). 𝑚𝑒𝑎𝑛()
# 𝐿𝑜𝑠 𝑎𝑟𝑔𝑢𝑚𝑒𝑛𝑡𝑜𝑠 𝑑𝑒 𝑙𝑎 𝑓𝑢𝑛𝑐𝑖ó𝑛 . 𝑝𝑙𝑜𝑡 𝑞𝑢𝑒 𝑠𝑒 𝑚𝑢𝑒𝑠𝑡𝑟𝑎 𝑎 𝑐𝑜𝑛𝑡𝑖𝑛𝑢𝑎𝑐𝑖ó𝑛 𝑠𝑜𝑛:
# 𝑐 𝑒𝑠 𝑐𝑜𝑙𝑜𝑟
# 𝑙𝑠 𝑒𝑠 𝑙𝑖𝑛𝑒𝑠𝑡𝑦𝑙𝑒
# 𝑙𝑤 𝑒𝑠 𝑙𝑖𝑛𝑒𝑤𝑖𝑑𝑡ℎ
𝑎𝑥. 𝑝𝑙𝑜𝑡(𝑐𝑢𝑟𝑣𝑎, 𝑐 = ′𝑘′, 𝑙𝑠 = ′ − ′, 𝑙𝑤 = .5, 𝑙𝑎𝑏𝑒𝑙 = ′𝐺𝐺𝐴𝐿 𝐵𝑢𝑦&𝐻𝑜𝑙𝑑 𝑑𝑒𝑠𝑑𝑒 𝑎ñ𝑜 2000′)
𝑎𝑥. 𝑓𝑖𝑙𝑙_𝑏𝑒𝑡𝑤𝑒𝑒𝑛(𝑑𝑎𝑡𝑎. 𝑖𝑛𝑑𝑒𝑥, 𝑐𝑢𝑟𝑣𝑎, 0, 𝑤ℎ𝑒𝑟𝑒 = 𝑐𝑢𝑟𝑣𝑎 < 0 , 𝑐𝑜𝑙𝑜𝑟 = ′𝑝𝑖𝑛𝑘′)
𝑎𝑥. 𝑓𝑖𝑙𝑙_𝑏𝑒𝑡𝑤𝑒𝑒𝑛(𝑑𝑎𝑡𝑎. 𝑖𝑛𝑑𝑒𝑥, 𝑐𝑢𝑟𝑣𝑎, 0, 𝑤ℎ𝑒𝑟𝑒 = 𝑐𝑢𝑟𝑣𝑎 > 0 , 𝑐𝑜𝑙𝑜𝑟 = ′𝑙𝑖𝑔ℎ𝑡𝑔𝑟𝑒𝑒𝑛′)
𝑎𝑥. 𝑙𝑒𝑔𝑒𝑛𝑑(𝑙𝑜𝑐 = ′𝑢𝑝𝑝𝑒𝑟 𝑙𝑒𝑓𝑡′, 𝑓𝑜𝑛𝑡𝑠𝑖𝑧𝑒 = 15)
# 𝑡𝑖𝑐𝑘𝑠 𝑦 𝑙𝑎𝑏𝑒𝑙𝑠
𝑎𝑥. 𝑥𝑎𝑥𝑖𝑠. 𝑠𝑒𝑡_𝑚𝑎𝑗𝑜𝑟_𝑙𝑜𝑐𝑎𝑡𝑜𝑟(𝑚𝑑𝑎𝑡𝑒𝑠. 𝑀𝑜𝑛𝑡ℎ𝐿𝑜𝑐𝑎𝑡𝑜𝑟(𝑖𝑛𝑡𝑒𝑟𝑣𝑎𝑙 = 18))
𝑎𝑥. 𝑥𝑎𝑥𝑖𝑠. 𝑠𝑒𝑡_𝑚𝑎𝑗𝑜𝑟_𝑓𝑜𝑟𝑚𝑎𝑡𝑡𝑒𝑟(𝑚𝑑𝑎𝑡𝑒𝑠. 𝐷𝑎𝑡𝑒𝐹𝑜𝑟𝑚𝑎𝑡𝑡𝑒𝑟(′%𝑌 − %𝑚′))
𝑎𝑥. 𝑡𝑖𝑐𝑘_𝑝𝑎𝑟𝑎𝑚𝑠(𝑎𝑥𝑖𝑠 = ′𝑥′, 𝑟𝑜𝑡𝑎𝑡𝑖𝑜𝑛 = 45, 𝑙𝑎𝑏𝑒𝑙𝑠𝑖𝑧𝑒 = 10, 𝑤𝑖𝑑𝑡ℎ = 5)
𝑎𝑥. 𝑔𝑟𝑖𝑑()
# 𝐴𝑛𝑜𝑡𝑎𝑐𝑖𝑜𝑛𝑒𝑠
𝑒𝑣𝑒𝑛𝑡𝑜𝑠 = { "𝐸𝑙𝑒𝑐𝑐 2015": 𝑑𝑡𝑑𝑡(2015,10,23), " 28 − 𝐷" ∶ 𝑑𝑡𝑑𝑡(2017,12,28), ′ 𝑃𝑅𝐸 − 𝑃𝐴𝑆𝑂′ ∶
𝑑𝑡𝑑𝑡(2019,8,9) }
Veamos otro ejemplo de uso de esta librería. Para esto usaremos directamente el ejemplo
construido por el profesor en clase. Comencemos con el armado del DataFrame de interés, la idea
es continuar trabajando con volatilidad:
Ahora graficamos el precio de cierre ajustado junto con la volatilidad. En este caso, el problema es
que las escalas son diferentes, como se ve a continuación:
Para resolver situaciones donde tenemos escalas diferentes, escribimos un código que permita
colocar en el eje vertical derecho una escala, y otra en el izquierdo. Para esto utilizamos el método
twinx(). Veamos:
Con este código definimos la cantidad de filas y columnas, es decir, el cuadriculado de sub
gráficas. Por defecto, es una fila y una columna. Al hacer esto, el eje u objeto “axs” pasa a ser un
array, por ello es que luego llamamos a su ubicación.
El subpltos_adjust(), es útil para justar cuestiones estéticas entre los sub gráficas. En este sentido,
“hspace” es la distancia de vacío/separado entre las filas de una sub gráfica y la otra. El “wspace”
es lo mismo, pero para las columnas.
Estilos
Los diferentes estilos son los siguientes:
El DataFrame se extiende entre los años 2000 y 2020, aquí tenemos la variación diaria promedio
para cada uno de los doce meses de cada uno de estos años.
CURIOSIDADES DE INDEX
.index también tiene ‘dayofweek’ que extrae el día de la semana, no su fecha, si no el día en sí
mismo.
Asimismo, también podemos usar to_period() e indicar en el argumento ‘Q’ que indica un
cuatrimestre (“quarter”). Si colocamos ‘Y’ será año (“year”).
Del mismo modo, se puede utilizar esta función para agrupar con más de un criterio, la
concatenación de criterios ocurre dentro de la función, por supuesto, separando cada condición por
una coma, y encerrando a ambos criterios entre corchetes (pues se pasan como una lista). Veamos
el ejemplo del profesor:
En paralelo, el argumento de groupby() no tiene porqué ser una variable (junto a sus valores),
también puede ser un valor en sí mismo. Saber esto es importante, pues así podremos agrupar sólo
por esa categoría, para posteriormente ver cómo se distribuye alguna otra variable. Veamos el
ejemplo dado el profesor en clase:
Del mismo modo que se señaló antes, podemos agrupar por más de un criterio:
Supongamos que queremos acortar las variaciones diarias a menos de 4%, luego:
Método apply()
Este permite concatenar métodos, es decir, hay métodos que no permiten concatenación con
algunos otros, por ejemplo, rolling() no se puede concatenar con quantile() o con kurtosis() o kurt(),
es por ello que utilizamos apply(). Veamos un ejemplo con el archivo ‘AAPL.xlsx’:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑛𝑢𝑚𝑝𝑦 𝑎𝑠 𝑛𝑝
𝑖𝑚𝑝𝑜𝑟𝑡 𝑝𝑎𝑛𝑑𝑎𝑠 𝑎𝑠 𝑝𝑑
𝑑𝑎𝑡𝑎 = 𝑝𝑑. 𝑟𝑒𝑎𝑑_𝑒𝑥𝑐𝑒𝑙(′𝐴𝐴𝑃𝐿. 𝑥𝑙𝑠𝑥′, 𝑖𝑛𝑑𝑒𝑥_𝑐𝑜𝑙 = ′𝑡𝑖𝑚𝑒𝑠𝑡𝑎𝑚𝑝′)
𝑑𝑎𝑡𝑎[′𝑣𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛′] = 𝑑𝑎𝑡𝑎[′𝑎𝑑𝑗𝑢𝑠𝑡𝑒𝑑_𝑐𝑙𝑜𝑠𝑒′]. 𝑝𝑐𝑡_𝑐ℎ𝑎𝑛𝑔𝑒()
𝑑𝑎𝑡𝑎 = 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝𝑛𝑎()
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑎. 𝑣𝑎𝑟𝑖𝑎𝑐𝑖𝑜𝑛. 𝑔𝑟𝑜𝑢𝑝𝑏𝑦(𝑑𝑎𝑡𝑎. 𝑖𝑛𝑑𝑒𝑥. 𝑦𝑒𝑎𝑟). 𝑎𝑝𝑝𝑙𝑦(𝑝𝑑. 𝐷𝑎𝑡𝑎𝐹𝑟𝑎𝑚𝑒. 𝑘𝑢𝑟𝑡𝑜𝑠𝑖𝑠))
TERCERA PARTE: BASES – CREACION DE
FUNCIONES
Creando funciones propias
Una función permite ahorrar líneas de código sobre una acción que generalmente la repetimos.
Entonces, al crear la función que resume esta acción, para ejecutarla sólo deberemos llamarla. En
otras palabras, crear una función es “empaquetar” un conjunto de acciones como lo hacen las
funciones nativas o de las librerías que importamos.
Antes de abordar la explicación sobre cómo crear funciones propias, veamos la clasificación de las
funciones o, las posibles funciones que podemos construir:
Sin Argumentos
Sin return.
Con return.
Con argumentos nativos
Sin return.
Con return.
Funciones con argumentos obligatorios y/o opcionales
Con argumentos específicos
Funciones que modifican los argumentos
En las siguientes secciones veremos cada una y cómo declararlas. Una característica de todas las
funciones, nativas y no nativas, es que las variables utilizadas en sus líneas de código internas
“mueren” dentro de la función, por ende, nunca habrá solapamiento entre estas variables y una que
nosotros creemos por fuera de la función.
Vea que el resultado devuelto es una tupla, por defecto. No obstante, esto puede modificarse al
indicar qué debe devolverse.
Si no explicitamos el nombre de los argumentos, entonces el orden sí importa, y el que se respeta es
el orden con que fueron creados los argumentos cuando se armó la función, es decir, primero
tendremos que colocar el valor del precio y luego el del porcentaje.
Podríamos calcular las raíces de esta función, para ello utilizamos la función solve(). Veamos:
𝑝𝑟𝑖𝑛𝑡(𝑠𝑜𝑙𝑣𝑒(𝑦))
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑜_𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙)
En este caso, utilizar como valor del argumento una colección implica usar una referencia. En
cambio, si utilizamos un string, por ejemplo, usaremos un valor y la función no lo modificará:
𝑑𝑎𝑡𝑜_𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙 = "𝐸𝑙 𝑡𝑎𝑐𝑜 𝑛𝑜"
𝑝𝑟𝑖𝑛𝑡(𝑙𝑜𝑛𝑔𝑖𝑡𝑢𝑑(𝑑𝑎𝑡𝑜_𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙))
𝑝𝑟𝑖𝑛𝑡(𝑑𝑎𝑡𝑜_𝑜𝑟𝑖𝑔𝑖𝑛𝑎𝑙)
𝑝𝑟𝑖𝑛𝑡(𝑐𝑎𝑙𝑐𝑢𝑙𝑎𝑑𝑜𝑟𝑎(𝑟𝑎𝑖𝑧, 8,3))
2.0 # 𝑅𝑒𝑠𝑢𝑙𝑡𝑎𝑑𝑜
Observe que, el orden de creación de las funciones es irrelevante, en otras palabras, aunque su
creación sigue el orden lógico de arriba hacia abajo, la ejecución al momento de llamar la función
“calculadora” es como otro bloque de código que no depende del orden mencionado.
Una vez creado el archivo .py con la función propia, podemos importarla al entorno de trabajo
como hacemos cuando importamos cualquier librería, es decir, escribimos “import” seguido del
nombre del archivo.
Funciones para análisis técnico
A continuación veremos aplicaciones prácticas de la generación de funciones propias, en estos
casos veremos cómo crear los indicadores técnicos usuales: Bandas de bollinger, RSI, MACD,
Cruces de medias, Ichimoku, y gráficos de correlaciones. Veamos cada uno en una sección
individual. En la siguiente página web se describe la matemática detrás de cada indicador:
https://docs.anychart.com/Stock_Charts/Technical_Indicators/Mathematical_Description
De esta web se obtienen las descripciones que se realizan en las siguientes secciones antes de
introducir el código correspondiente.
Otras dos páginas para conocer más sobre indicadores técnicos son las siguientes:
https://www.fmlabs.com/reference/default.htm
https://docs.anychart.com/Stock_Charts/Technical_Indicators/Mathematical_Description#o
verview
Bandas de bollinger
Las bandas de bollinger permiten tener un rango de movimiento probable del precio.
Concretamente, estas se definen del siguiente modo:
El “BBands” son las bandas de bollinger, y “Bollinger Bands %B” es el indicador utilizado para
tener señales de compra y de venta. Este indicador incorpora el precio de cotización y lo compara
con las bandas. Si el indicador es positivo es porque la cotización es mayor a la banda inferior, y si
a su vez es mayor a 1, es porque el precio de cotización se encuentra en un valor históricamente
elevado (en términos de los promedios para la ventana móvil analizada). Si el indicador es
negativo, el precio de cotización es bajo en relación al promedio móvil. Con este indicador se
pueden generar señales de compra y venta, pues si está por arriba de 1 conviene vender, y si es
negativo conviene comprar.
Este indicador tiene un sentido estadístico, pues suponiendo que la distribución de los retornos
periódicos es normal, la cantidad de observaciones dentro del más menos dos desvíos estándar es
de 95,45%. Por ende, cuando el precio de cotización supera las bandas, es muy probable que el
mismo regrese dentro de las mismas.
Veamos cómo lo hizo el profesor en clase sin definir la función:
Ahora construyamos la función incorporando los argumentos, y sumando algo más: si el volumen
es superior a dos veces su promedio, se sumarán más desvío, y si es inferior, se le restará desvío.
Veamos cómo queda el código:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑦𝑓𝑖𝑛𝑎𝑛𝑐𝑒 𝑎𝑠 𝑦𝑓
𝑖𝑚𝑝𝑜𝑟𝑡 𝑛𝑢𝑚𝑝𝑦 𝑎𝑠 𝑛𝑝
𝑑𝑒𝑓 𝑏𝑜𝑙𝑙𝑖𝑛𝑔𝑒𝑟(𝑡𝑖𝑐𝑘𝑒𝑟, 𝑣𝑒𝑛𝑡𝑎𝑛𝑎 = 20, 𝑐𝑎𝑛𝑡_𝑑𝑒𝑠𝑣𝑖𝑜𝑠 = 2, 𝑖𝑛𝑖𝑐𝑖𝑜 = ′2010 − 01 − 01′, 𝑎𝑗𝑢𝑠𝑡𝑎𝑟_𝑣𝑜𝑙𝑢𝑚𝑒𝑛 = 𝐹𝑎𝑙𝑠𝑒):
"""
𝐴𝑗𝑢𝑠𝑡𝑒 𝑥 𝑉𝑜𝑙𝑢𝑚𝑛𝑒𝑛:
𝐶𝑢𝑎𝑛𝑑𝑜 𝑒𝑙 𝑉𝑜𝑙𝑢𝑚𝑒𝑛 𝐷𝑈𝑃𝐿𝐼𝐶𝐴 𝑎𝑙 𝑉𝑜𝑙𝑢𝑚𝑒𝑛 𝑀𝑒𝑑𝑖𝑜 => + 0.5 𝐷𝑒𝑠𝑣𝑖𝑜𝑠
𝐶𝑢𝑎𝑛𝑑𝑜 𝑒𝑙 𝑉𝑜𝑙𝑢𝑚𝑒𝑛 𝐸𝑆 𝑀𝐸𝑁𝑂𝑅 𝑉𝑜𝑙𝑢𝑚𝑒𝑛 𝑀𝑒𝑑𝑖𝑜 => − 0.5 𝐷𝑒𝑠𝑣𝑖𝑜𝑠
"""
𝑛𝑜𝑚𝑏𝑟𝑒_𝑐𝑜𝑙𝑢𝑚𝑛𝑎_𝑠𝑚𝑎 = ′𝑠𝑚𝑎__′ + 𝑠𝑡𝑟(𝑣𝑒𝑛𝑡𝑎𝑛𝑎)
𝑑𝑎𝑡𝑎 = 𝑦𝑓. 𝑑𝑜𝑤𝑛𝑙𝑜𝑎𝑑(𝑡𝑖𝑐𝑘𝑒𝑟, 𝑎𝑢𝑡𝑜_𝑎𝑑𝑗𝑢𝑠𝑡 = 𝑇𝑟𝑢𝑒, 𝑠𝑡𝑎𝑟𝑡 = 𝑖𝑛𝑖𝑐𝑖𝑜)
𝑑𝑒𝑠𝑣𝑖𝑜 = 𝑑𝑎𝑡𝑎. 𝐶𝑙𝑜𝑠𝑒. 𝑟𝑜𝑙𝑙𝑖𝑛𝑔(𝑣𝑒𝑛𝑡𝑎𝑛𝑎). 𝑠𝑡𝑑()
𝑣𝑜𝑙𝑢𝑚𝑒𝑛_𝑚𝑒𝑑𝑖𝑜 = 𝑑𝑎𝑡𝑎. 𝑉𝑜𝑙𝑢𝑚𝑒. 𝑚𝑒𝑎𝑛()
𝑑𝑎𝑡𝑎[′𝑘_𝑣𝑜𝑙′] = 𝑑𝑎𝑡𝑎. 𝑉𝑜𝑙𝑢𝑚𝑒 / 𝑣𝑜𝑙𝑢𝑚𝑒𝑛_𝑚𝑒𝑑𝑖𝑜
𝑑𝑎𝑡𝑎[′𝑝𝑙𝑢𝑠_𝑑𝑒𝑠𝑣𝑖𝑜𝑠′] = 𝑛𝑝. 𝑤ℎ𝑒𝑟𝑒(𝑑𝑎𝑡𝑎. 𝑘_𝑣𝑜𝑙 > 2, 0.5, 𝑛𝑝. 𝑤ℎ𝑒𝑟𝑒(𝑑𝑎𝑡𝑎. 𝑘_𝑣𝑜𝑙 > 1, 0, −0.5))
𝑑𝑎𝑡𝑎[𝑛𝑜𝑚𝑏𝑟𝑒_𝑐𝑜𝑙𝑢𝑚𝑛𝑎_𝑠𝑚𝑎] = 𝑑𝑎𝑡𝑎[′𝐶𝑙𝑜𝑠𝑒′]. 𝑟𝑜𝑙𝑙𝑖𝑛𝑔(𝑣𝑒𝑛𝑡𝑎𝑛𝑎). 𝑚𝑒𝑎𝑛()
𝑖𝑓 𝑎𝑗𝑢𝑠𝑡𝑎𝑟_𝑣𝑜𝑙𝑢𝑚𝑒𝑛:
𝑑𝑎𝑡𝑎[′𝑏𝑜𝑙𝑙_𝑠𝑢𝑝′] = 𝑑𝑎𝑡𝑎[𝑛𝑜𝑚𝑏𝑟𝑒_𝑐𝑜𝑙𝑢𝑚𝑛𝑎_𝑠𝑚𝑎] + (𝑐𝑎𝑛𝑡_𝑑𝑒𝑠𝑣𝑖𝑜𝑠 + 𝑑𝑎𝑡𝑎. 𝑝𝑙𝑢𝑠_𝑑𝑒𝑠𝑣𝑖𝑜𝑠) ∗ 𝑑𝑒𝑠𝑣𝑖𝑜
𝑑𝑎𝑡𝑎[′𝑏𝑜𝑙𝑙_𝑖𝑛𝑓′] = 𝑑𝑎𝑡𝑎[𝑛𝑜𝑚𝑏𝑟𝑒_𝑐𝑜𝑙𝑢𝑚𝑛𝑎_𝑠𝑚𝑎] − (𝑐𝑎𝑛𝑡_𝑑𝑒𝑠𝑣𝑖𝑜𝑠 + 𝑑𝑎𝑡𝑎. 𝑝𝑙𝑢𝑠_𝑑𝑒𝑠𝑣𝑖𝑜𝑠) ∗ 𝑑𝑒𝑠𝑣𝑖𝑜
𝑒𝑙𝑠𝑒:
𝑑𝑎𝑡𝑎[′𝑏𝑜𝑙𝑙_𝑠𝑢𝑝′] = 𝑑𝑎𝑡𝑎[𝑛𝑜𝑚𝑏𝑟𝑒_𝑐𝑜𝑙𝑢𝑚𝑛𝑎_𝑠𝑚𝑎] + 𝑐𝑎𝑛𝑡_𝑑𝑒𝑠𝑣𝑖𝑜𝑠 ∗ 𝑑𝑒𝑠𝑣𝑖𝑜
𝑑𝑎𝑡𝑎[′𝑏𝑜𝑙𝑙_𝑖𝑛𝑓′] = 𝑑𝑎𝑡𝑎[𝑛𝑜𝑚𝑏𝑟𝑒_𝑐𝑜𝑙𝑢𝑚𝑛𝑎_𝑠𝑚𝑎] − 𝑐𝑎𝑛𝑡_𝑑𝑒𝑠𝑣𝑖𝑜𝑠 ∗ 𝑑𝑒𝑠𝑣𝑖𝑜
𝑟𝑒𝑡𝑢𝑟𝑛 𝑑𝑎𝑡𝑎. 𝑑𝑟𝑜𝑝𝑛𝑎(). 𝑟𝑜𝑢𝑛𝑑 (2)
Ahora crearemos el oscilador o indicador llamado “BANDS BOLLINGER %B” descripto al inicio
de la sección:
Relative Strenght Index (RSI)
Este indicador es por sí sólo un indicador que brinda señales de compra y venta. El indicador es un
oscilador de medias móviles de velas que suben contra velas que bajan, por ende, compara la
“potencia” de las velas verdes contra la “potencia” de las velas rojas. El indicador oscila entre 0 y
100, y brinda señales de venta al estar cercano a 100 y señales de compra al estar cercano a 0.
Concretamente, este indicador se define como se describe a continuación:
“MA” es la media móvil. En este caso se utiliza el precio actual versus el precio inmediatamente
anterior, es por esto que deberemos utilizar el método shift(), indicando en su argumento el número
1 para indicar que se desea el valor inmediatamente anterior. Como alternativa, se puede utilizar el
método diff(), que permite calcular la diferencia entre el valor actual y otro anterior
correspondiente al período que se indica en el argumento, es decir, si colocamos 1, hará la
diferencia entre el precio de hoy y de ayer. Finalmente, la media móvil exponencial tiene como
función ewm(), donde el argumento es la ventana móvil (entre sus argumentos se puede especificar
el tipo de ponderación que se desea, sólo se debe mirar cuáles tiene con la función help()).
Veamos cómo lo hizo el profesor sin definir función:
Ahora veamos cómo construir el indicador sin usar ewm():
Para obtener una señal de compra y venta deberemos fijar límites, uno superior y otro inferior.
Estos límites podemos testearlos de tal modo de elegir aquél que maximice la ganancia del período
bajo estudio.
Ahora creemos la función:
CONSEJO
Como en la sección donde mostramos que podemos utilizar una función como argumento de otra,
aquí podemos hacer lo mismo, preparando individualmente las funciones correspondientes a cada
indicador técnico, y luego construyendo otra función donde sólo indiquemos el ticket y el tipo de
indicador a calcular. Veamos lo que hizo el profesor:
Restaría crear el indicador que permita contar con señales de compra y venta.
Ahora construyamos la función:
Cruces de medias
Estos son los cruces de medias móviles, generando la señal de compra cuando la media rápida
cruza de arriba hacia abajo a la lenta, y la señal de venta cuando la cruza de arriba hacia abajo.
Veamos la función:
Graficándolo:
Aquí vemos que podemos tomar señales de compra y venta en el punto cero, o también, tomando
límites superiores e inferiores de acuerdo a esta serie que está en la gráfica, definiendo dichos
límites como aquellos que maximizan la ganancia de la operatoria.
Gráficos de correlaciones
Estudiar las correlaciones en análisis técnico permite evaluar las señales de compra y venta, por
ejemplo, con las tendencias de precios. Para esto, comparamos la tendencia del precio con la
tendencia del indicador utilizando la correlación. La tendencia del precio la calculamos como un
cociente entre el precio de cierre ajustado de “tantas ruedas en el futuro” y el precio de cierre
ajustado actual menos 1.
asdasd
CUARTA PARTE: BACK-TESTING
Backtesting
¿En qué consiste el backtesting? Con este nombre se señala la evaluación de la estrategia de
trabajo/trading aplicándola a la serie de precios. En otras palabras, se elige un período de tiempo
sobre el cual se aplicará la estrategia. Luego se evalúa el desempeño de la estrategia.
Por ejemplo, si la estrategia de trading consiste en comprar y vender con cruces de medias móviles,
su backtesting será aplicar dicho cruce sobre una base de datos/serie de precios pasados, valga la
redundancia. Por lo tanto, el backtesting es una evaluación necesaria pero no suficiente para
evaluar el desempeño de una estrategia de trading.
En resumidas cuentas, el backtesting sirve para:
Descartar ideas que no van.
Comprender la sensibilidad de una idea ante cambios en sus parámetros. Retomando el
ejemplo del cruce de medias, la sensibilidad refiere a cómo cambia la ganancia ante
cambios en la venta móvil de cada media, y también, a cómo cambia la ganancia ante el
reemplazo de la media móvil simple por una exponencial.
Para entender qué tan “portable” es la idea. Una idea/estrategia de trading es “portable”
cuando puede aplicarse sobre dos o más activos financieros, en otras palabras, si la
estrategia sólo sirve para AAPL y no para otros activos, no será “portable”.
Evaluar las razones por las cuales sí funciona y no funciona. Como se indica, el
backtesting permite inspirar hipótesis sobre las razones que explican el por qué no funciona
la estrategia, o por qué sí lo hace.
En general, una estrategia con muchos parámetros no es “portable”. En paralelo, el backtesting NO
sirve para:
Aprobar una idea o asumir que es rentable. Como se mencionó, es una evaluación
necesaria pero no suficiente, pues el pasado no asegura el futuro.
Optimizar la parametrización (overfiting). Se sostiene esto por la misma razón esgrimida
en el punto anterior, que algo haya funcionado antes no es garantía para el futuro.
Para buscar activos donde la idea sí funciona. No tiene sentido plantear una estrategia y
buscar los activos para los cuales funciona. El trabajo debe enfocarse y adaptarse a la
realidad, es decir, se plantea una estrategia que deberá adoptar valores para sus parámetros
de acuerdo al patrón oculto en la serie de precios, la estrategia se adapta a los datos.
Entonces, el backtesting se utiliza para realizar un análisis de sensibilidad, que permitirá conocer el
rango de variación de los parámetros a partir del cual o dentro del cual la ganancia de la estrategia
de trading mejora. En otras palabras, en lugar de buscar un valor óptimo para los parámetros, se
busca un rango de valores, o sea, un rango óptimo.
NOTA: Riskmanagement.
El manejo de riesgos consiste en analizar cuál es la pérdida máxima para determinado período.
Esto dependerá de la exposición al tipo de riesgos y de la distribución de frecuencias de la serie
del activo analizado. En este sentido, al identificar los riesgos es posible parametrizar el análisis,
pero también, sólo se puede realizar un análisis de probabilidad a través de un Montecarlo, con el
cual se cuantificará la máxima pérdida posible.
Con un análisis de Riskmanagement se inspiran estrategias de cobertura utilizando derivados.
Tipos de Backtests
A grandes rasgos se identifican tres grandes tipos de backtesting:
Enfoque matricial. Este es un análisis discreto, donde el análisis se realiza a partir de
diferentes momentos del tiempo. Por ejemplo, habiendo tomado posición larga, se vende
luego de determinada cantidad de ruedas o en la rueda donde se cumple determinada
condición sobre los precios de cierre. En estos casos, el momento es el precio de cierre
(por ejemplo), y el análisis se realiza sobre estos, sin importar la vela o el resto de precios
que han ocurrido durante el día.
Enfoque even-driven. Este enfoque se concentra y opera sobre cada orden de compra/venta
que se realiza en el mercado.
Los elementos en común entre los enfoques son las “condiciones de realismo”, que en general, no
son tenidos en cuenta en los primeros backtests. En este sentido, aunque no se tengan en cuenta, es
importante saber que son elementos presentes en la realidad, y por ello afectan las ganancias de la
estrategia. Veamos algunos ejemplos:
Tipos activos/takers ¿Que condicion impongo para volúmenes/ventanas de tiempo? Por
ejemplo, al establecer como condición de compra/venta el cruce de medias, deberemos
especificar el precio de compra venta, y para esto deberemos tener presente el libro de
órdenes. En concreto, los precios de compra y venta que se operan en el mercado junto a
los volúmenes ofertados serán otro conjunto de datos a analizar, pues será en función de
dicho rango que deberemos establecer el precio para operar.
Tipos posivos/makers ¿Que % de ejecución bajo X Circunstancias asumo?
En gral ¿hubiera afectado mi orden al mercado? ¿como lo se? ¿como lo valido? ¿que pue
do asumir?
Costos tranasaccionales, comision, derechos de mercado, spread, impuestos, costos infrae
str, mkt data etc
Etapas de un backtest
En esta cuarta etapa veremos el backtest básico y de sensibilidad, no obstante, es importante
conocer el resto. Veamos.
PreBackTest (Etapa de Research). El objetivo es plantear todo tipo de idea posible,
verificar antes que nada la correlación entre los inputs planteados y la reacción del
mercado, etc. La idea es plantear la mayor cantidad de hipótesis posibles, ser amplio en la
visión y riguroso en al momento de señalar para qué sirve y para qué no sirve, ante la duda
dejar para validar más adelante.
El siguiente conjunto no necesariamente debe desarrollarse completo, en otras palabras,
hay etapas que pueden omitirse, por ejemplo, el análisis de correlación. Veamos cada
etapa:
o Armado de tablas de indicadores potenciales. Se seleccionan los indicadores que
permitirían cumplir con la hipótesis de trabajo.
o Planteo de "Racional" o Hipótesis de trabajo. Aquí se responde cuándo vamos a
comprar y cuándo a vender. En esta etapa se estructura la estrategia de trading, los
pasos lógicos para establecer las señales de compra y de venta, por ende, se
establecen las razones/criterios. Por ejemplo, la hipótesis de trabajo es comprar y
vender anticipando la tendencia, de tal modo que nos subiremos a ella cuando
comience a desarrollarse, y saldremos de ella cuando pierda fuerza.
o Análisis de correlación
Regresiones inputs/forwards. Por forward se entiende a precios futuros,
por ende, se estudia la correlación entre el indicador de trading y los
precios forward.
Algos de clasificacion o probabilidades de suba o baja en funcion de
inputs.
o Tabla de posibles trades pasados el racional de trading.
o Tabla de resultados o reporting básico.
% de trades positivos y negativos
Esperanza matemática del método
Tiempo comprado / libre
Backtest básico. El objetivo principal es empezar a validar en forma rápida la
conveniencia de aplicar un método algorítmico, tanto por el riesgo asumido como contra el
benchmark de su mercado y la variabilidad en diferentes contextos, épocas etc.
Este es similar al realizado en el trabajo final de la especialización en mercado de capitales.
La esencia de toda estrategia es ganarle al benchmark, y ganarle a la estrategia de “buy and
hold”. Entonces, este backtest básico es un informe compuesto de los siguientes elementos:
o Trades en un grupo de activos, en un rango de parámetros.
o Tabla de resultados intermedia.
Resultados año a año
Comparación con el buy&Hold
Comparación con el banchmark
Ratios de riesgo (Sharpe, Sortino, etc)
Backtest más profundo: Análisis de sensibilidad. El objetivo principal de esta etapa
es el análisis de cada parámetro del sistema, ver como varían los resultados variando todo
tipo de parámetro, donde aumentan o disminuyen la cantidad de señales, de eventos
positivos, negativos, extremos, medios, largos, cortos, etc. Claramente es la etapa más
abierta que puede dar pie a volver a la etapa de research y empezar a replantear
indicadores.
Habiendo superado la evaluación básica, se procede a realizar el análisis de sensibilidad
con el objetivo de establecer el rango de valores para los parámetros, y para conocer las
razones detrás del éxito y fracaso de la estrategia. Veamos:
o Parametrización de variables.
o Cambio de indicadores.
o Uso de grupos de control.
Análisis de portabilidad. Es el anteúltimo filtro del método, es la prueba de fuego al
overfiting, aquí se pone a prueba el método frente a diferentes mercados, a diferentes
timeframes, a diferentes activos o grupos de activos etc., obviamente no necesariamente
tiene que ser portable a todo, pero tampoco puede ser un método aplicable a un solo activo,
en un solo timeframe, en una sola época.
Si bien siempre hay variabilidad, un método bien robusto se mueve en rangos acotados, es
decir no puede dar lo contrario en un activo que en otro, puede tener un desempeño un
poco mejor, un poco peor, etc., de hecho si construyen una distribución de rendimientos a
varios activos, mientras menor desvió del rendimiento más robusto el método o mas
portable entre activos.
En este punto resultan de mucha utilidad los algos de clusterizacion, pues permiten ver
comportamientos en diferentes clusters (que pueden ser regímenes de volatilidad, volumen,
tendencias, etc..).
Habiendo determinado los elementos fundamentales detrás del activo analizado, se procede
a seleccionar otros activos similares para evaluar la “portabilidad” de la estrategia. En este
sentido, Juan Pablo (profesor) señala que, si de 500 activos la estrategia sirve para 100
activos o más, será “portable”, en cambio, si sirve para 20 de 500, no será “portable”. En
este sentido, en el caso de los 100 de cada 500 habrá que evaluar qué tienen en común
dichos activos, por ejemplo, podríamos ver como resultado de este análisis, que la
estrategia sirve para los activos con determinado nivel de volatilidad o de volumen. Ahora,
si de esos 100 de 500 activos no se encuentra al menos un elemento común, la estrategia
tampoco será “portable”. Veamos:
o Riesgo de overfiting
o Cruce y armado de matrices de resultados
o Matrices de correlación cross mkt
o Matrices de correlación cross time-frame
o Cauterización por regímenes (volatilidad, épocas, ciclos etc.)
Backtest Avanzado. Esta es la etapa final, aquí se valida la viabilidad en cuanto a
liquidez, costo transaccional, spreads, posibles fallas del mercado, apis, tiempos,
volúmenes, reglamentaciones, etc. También se valida la exposición real y el manejo de
posición buscando un tamaño de posición óptima al riesgo a asumir, esto puede estimarse
vía montecarlo o modelarse con criterios como Kelly.
o Manejo de posición/riesgo, exposición óptima (Kelly, Montecarlo etc.). Refiere a
testear dos estrategias en una, por ejemplo, una cartera compuesta por otras dos,
una que representa el 95% del capital y que tiene un bajo riesgo, y el 5% restante
(la otra sub cartera), que es la más riesgosa.
o Factibilidad técnica (volúmenes, liquidez, spreads, fallas, tiempos etc.). Esto
implica incorporar los costos de transacción, la posibilidad de operar los
volúmenes deseados dada la profundidad del mercado, etcétera.
Se construye, a continuación, una estrategia de trading sobre cruce de medias. Preste atención al
uso de la función shif(). Recuerde que esta función señala el valor anterior al actual, y en su
argumento se señala con un número qué tan atrás está ese valor pasado respecto del actual. Si no
usáramos el shift() en la línea señalada con un comentario, entonces estaríamos cometiendo el error
de establecer la estrategia de trading con datos que aun no han ocurrido. En otras palabras, al día de
hoy, nosotros tomamos una u otra posición con los datos de ayer, no los de hoy, pues estos aun no
existen. Esto es lo que nos permite representar el uso de esta función shift().
En paralelo, la línea de código que crea la variable “cruce”, establece un valor superior a 0 para
general la señal de entrada, no obstante, esta puede cambiarse para que sea mayor a 1% para tener
mayor seguridad, o menor a 1% para adelantarnos al cruce, o utilizar otros porcentajes. Es decir,
colocar 0 es tan arbitrario como colocar cualquier otro porcentaje, queda todo al criterio del
analista.
El siguiente código tiene por objetivo utilizar como estrategia de tranding el cruce de medias, por
ende, a partir de la serie de precios del papel de interés, se crean las variables requeridas, para
posteriormente generar las señales de compra y venta pertinentes. Es a partir del momento donde se
tienen estas señales que estamos en condiciones de realizar el pre backtesting, el backtesting
básico, y el análisis de sensibilidad.
Comencemos con el código base que permite crear las señales de compra y de venta dada la serie
de precios del activo de interés:
Ahora graficaremos esto para poder realizar una inspección visual de lo que creamos, para ello
deberemos escribir el siguiente código:
Vea que las primeras tres líneas permiten acotar la serie de precios, y crean las series de precios
que se corresponden a los que se pagaron al entrar en el papel, y los precios que se cobraron al salir
del papel. Las siguientes líneas corresponden al código de la gráfica, quien se ilustra a
continuación:
Ahora evaluemos esta estrategia, comencemos indagando la tasa nominal correspondiente a las
posiciones compradas versus las posiciones vendidas. Para esto evaluaremos la variable “estado”
(comprado = “in”, vendido = “out”), sumando las variaciones diarias del precio de cierre ajustado.
Veamos:
Dos son los errores de proceder de este modo son: 1) estamos sumando posiciones largas sin tener
en cuenta la duración de cada una, y 2) sumamos variaciones diarias cuando en realidad deberías
multiplicar factores de capitalización. No obstante, Juan Pablo (Profesor), afirma que este análisis
interno es válido como primera evaluación superficial de la estrategia de trading, por ende, la
misma tendrá un desempeño aceptable si la suma de las variaciones diarias en la posición
comprada es superior a la posición vendida. Estos errores se solucionan tomando variaciones
logarítimicas y con ellas construyendo factores de capitalización, que luego de utilizan para
calcular la multiplicatoria (tasa efectiva, de este modo no importará la extensión de la posición
comprada).
Lo siguiente es evaluar la cantidad de ruedas en la que mantuvimos una posición de compra versus
la cantidad de ruedas en las que se mantuvo la posición vendida. Veamos:
A esta estrategia le falta incorporar los días que se estuvo fuera del papel, pues se asume que en
esas ocasiones hicimos tasas con cauciones, o se realizó otra inversión. Luego de esto debemos
comparar esta estrategia con la estrategia “buy and hold”, para lo cual deberemos crear la variable
de factor de capitalización utilizando la función cumproud() sobre la variación diaria de los precios
de cierre ajustados más uno.
Para evaluar el costo de transacción debemos conocer la cantidad de compras y ventas, quienes
también se deben relativizar respecto a la cantidad total de ruedas. Para esto se escribe el siguiente
código:
Viendo que la estrategia tiene un desempeño aceptable para las posiciones compradas (rendimiento
superior al rendimiento de las posiciones donde no se operó), se procede a realizar la matriz de
trades. Esta matriz permite que veamos directamente los precios de cierre que pagamos al comprar
y que recibimos al vender
Con esta matriz de trades podremos realizar los cálculos que permiten realizar los análisis de
sensibilidad posteriores.
Ideas de parametrización
En esta sección veremos varias ideas que serán presentadas una a continuación de otra:
Buscamos el mejor momento para anticipar o retrasar la entrada. Sobre el código anterior,
en lugar de generar la señal cuando el cruce es mayor a cero, podríamos pedir que sea
mayor a un porcentaje positivo o uno negativo. Esto lo hacemos y a la par evaluamos la
rentabilidad compuesta anual. La función add(1) está sumando uno a las variaciones
diarias, prod() está multiplicando los factores de capitalización diarios de toda la serie, y
luego se calcula la raíz de 42 (arbitrariamente, pues la serie original tiene aproximadamente
esa cantidad de años, pues comienza en 1980), para finalmente restarle uno y obtener la
TEA. Esta tasa la imprimimos junto al valor del parámetro que la generó, y así
comparamos visualmente para determinar el rango de tasas donde se obtiene el mejor
rendimiento.
Esto puede graficarse para poder apreciar mucho mejor el rango de conveniencia,
colocando la TEA en el eje vertical, y el valor del parámetro en el horizontal. Veamos:
Además de poder establecer el rango de valores para el parámetro, queda claro que lo
mejor es adelantarse al cruce antes que sobrepasarlo (lo cual es lógico).
Ahora calcularemos la TEA de la estrategia “Buy and hold”:
Entonces, comparamos esta TEA de “Buy and hold” contra la TEA de la gráfica según el
valor del parámetro. De este modo elegimos el rango de valores del parámetro que le gana
a esta estrategia. Recuerde que a la estrategia de trading resta incorporarle la tasa de
caución, mínimamente.
Busquemos el mejor cruce de medias. Este análisis tiene por objetivo lograr identificar el
rango de valores para las medias que se cruzan y generan la señal de compra y de venta.
Veamos el código: El primer ciclo for es para la media móvil rápida, el segundo para la
lenta, y el condicional if es para ejecutar la evaluación. Esta evaluación debe ejecutarse
sólo se la venta móvil de la media móvil rápida es menor a la lenta. La mecánica es la
siguiente: se comienza con media móvil rápida de 10 y lenta de 25, luego se sigue con
rápida de 11 y lenta de 25, luego rápida de 12 y lenta de 25, y así sucesivamente hasta que
la rápida es de 24 y la lenta de 25. Cuando la rápida es de 25, la lenta salta a 27, cuando la
rápida crece a 27, la lenta salta a 29, y así sucesivamente hasta la última evaluación donde
la rápida será de 39 y la lenta de 69.
Con cada combinación de medias rápidas y lentas, las acciones ejecutadas son:
construcción de las medias móviles, construcción de la señal de cruce, construcción de la
variable “estado”, cálculo de la TEA de todas las posiciones compradas, y construcción de
tabla que relaciona esta TEA con los valores de las ventas móviles que las generaron.
Finalmente, se grafica la tabla para poder realizar un análisis visual rápido.
Veamos el código:
Este código sirve para graficar la relación entre la TEA de la estrategia de tranding y el par de
valores para el cruce de medias. Veamos:
Construyendo una cartera
Supongamos que el capital es partido en tres porciones, colocando 1/3 en cada una. Supongamos,
además, que cada estrategia es un cruce de medias, que difieren entre sí por el valor de sus ventanas
móviles. ¿Cómo podemos analizar esto? Con el siguiente código:
Partiendo de esta base, ahora construiremos nuestra cartera, compuesta por estas tres estrategias de
trading. Vale aclarar que, “frisk” es la tasa libre de riesgo, claramente colocada de forma arbitraria.
Se la coloca allí porque en caso de no estar comprados, la rentabilidad diaria obtenida
corresponderá a esta tasa.
Ahora vamos a graficar. Podemos apreciar cómo la rentabilidad de cada estrategia, incluida la
cartera, se ubica en el eje vertical, y las fechas en el eje horizontal. En la siguiente gráfica se
compara la rentabilidad acumulada de la estrategia “buy and hold”, de cada estrategia de media por
separado, y de la cartera. Veamos:
Ahora comparemos exclusivamente la cartera contra la estrategia “buy and hold”:
Análisis de métricas
Con ciertos indicadores podremos cuantificar lo que se observa en una gráfica. Por ejemplo,
siguiendo la línea de análisis de la sección anterior, y en particular la última gráfica, podemos
evaluar la volatilidad de cada estrategia. Entonces, lo primero a hacer es calcular el cambio diario
de cada estrategia, en este sentido, calculemos el que corresponde a la cartera compuesta por las
tres estrategias, vista en la sección anterior, y por la estrategia de “buy and hold”, quien actuará
como el benchmark. Veamos:
Aquí creamos los cambios porcentuales para cada rueda. Ahora calcularemos el retorno acumulado,
el retorno por rueda, y la volatilidad. Esto lo haremos en las siguientes secciones.
Retorno acumulado
Lo calculamos para el período completo de análisis, tanto para la cartera como para el benchmark:
CAGR
Ahora calculamos lo mismo que antes pero lo anualizamos. El CAGR es la TEA:
Veamos el código:
Volatilidad
Ahora calculamos la volatilidad anualizada para cada estrategia:
Indice de Sharpe
Ahora calculamos lo indicado en el título, y lo haremos para una venta móvil (suavizado) y para el
caso acumulado para todo el período. Recuerde que continuamos con el ejemplo brindado en la
sección anterior.
Para una ventana móvil. Veamos el código para el caso de la cartera:
Es por ello que entra en escena la ratio Sortino, incorporando la idea de “volatilidad buena”,
volatilidad en tendencia alcista, y “volatilidad mala”, volatilidad en tendencia lateral y bajista.
Entonces, la ratio Sortino toma sólo la volatilidad de las ruedas donde el retorno por rueda es
negativo (tendencia negativa). Calculemos esta ratio para la cartera y el benchmark.
Observe que se han mostrado dos modos de calcular la ratio de Sortino, la diferencia está en el
denominador (cantidad de observaciones). En el primer caso se toman sólo las ruedas donde el
retorno fue negativo, esto es lo que sugiere la bibliografía, mientras que en la segunda, se toman
todas las ruedas de la serie.
Max DrawDown
Este indicador mide para cada rueda qué tal abajo se está respecto al último techo de precio.
Entonces se calcula como el valor de cada día dividido el máximo histórico hasta dicho momento.
Veamos:
Correlación: r^2
Esta métrica busca conocer la correlación lineal entre la cartera y el benchmark, en otras palabras,
podremos conocer qué tan parecido son los movimientos de la cartera a los movimientos del
Benchmark. Veamos:
Calmar CAGR
Este indicador relativiza el CAGR respecto a la situación actual del precio en relación al último
techo. La idea es contar con una ratio que sea mayor mientras más grande sea la TEA, y también,
que sea más grande mientras más bajo esté el precio en relación al último techo histórico. Del
mismo modo, será bajo mientras más baja sea la TEA y más cerca esté el precio de su techo. Este
cociente se obtiene del siguiente modo:
Asimetría y Kurtosis
Estos indicadores son explicados en un recuadro en la parte uno de este documento, concretamente,
en la sección llamada “DataFrame: Estadística descriptiva y algo más (3/3)”. Son útiles para
derivados,1 es decir, para estrategias donde las colas importan, o donde hay mucha concentración
en el centro de la distribución de frecuencias (para comprender la razón de esto se recomienda la
lectura del material de derivados del posgrado “especialización en mercado de capitales”). En este
sentido, para estrategias de seguimiento de tendencia no son relevantes. Veamos el código:
1 Una kurtosis alta implica colas pesadas y/o mucha concentración en el centro de la distribución de frecuencias (en
estrategias de salto de volatilidad de usa mucho la kurtosis, dice Juan Pablo). El sesgo es un buen indicador para las
estrategias de neutralización de riesgos (letras griegas).
Rendimientos diario, mensual, anual, máximo, y mínimo
Este conjunto de indicadores permite caracterizar la estrategia de cartera y el “buy and hold”, por
ejemplo, permitiendo conocer el último rendimiento período del mes (mensualizado), y los
rendimientos mensuales más bajos y más altos, para después compararlos con las mismas
características del benchmark.
El código siguiente responde o tiene la estructura que se verá porque la serie utilizada es de
factores de capitalización, lo cual queda en evidencia al revisar todas las secciones pasadas, desde
que se inicia el ejemplo. No obstante la aclaración, lo buscado en el rendimiento diario, la TEM, y
la TEA. Veamos:
Estos son útiles para los gestores de cartera, principalmente los valores máximos y mínimos.
Kelly
Este criterio mide la eficiencia del capital, maximizando el valor esperado del logaritmo de la
riqueza, y es útil para el largo plazo. Es muy útil para opciones, pero más allá de esto, lo que se
busca en términos generales es penalizar las bajas potenciales en el precio del papel, y premiar las
subas potenciales en el precio del papel. Las penalizaciones y premios consisten en colocar menos
y más capital respectivamente, o sea, apostar menos capital cuando el potencial de baja es alto, y
más cuando el potencial de suba es grande.
El siguiente link contiene una variedad de definiciones:
https://en.wikipedia.org/wiki/Kelly_criterion
El código es el siguiente:
Armemos la función:
Riesgo de ruina
El riesgo de ruina es la probabilidad de que un individuo pierda significativas cantidades de
dinero, de tal modo que ya no sea probable su recuperación y que tampoco pueda continuar
apostando/invirtiendo. Por convención, esta probabilidad se la calcula como una probabilidad de
pérdida. Este concepto es general, pues puede calcularse utilizando value at risk (VaR) u otras
medidas.
Esto permite cuantificar la probabilidad de quebrar a partir de los estadísticos media y desvío, y
una distribución de probabilidad muestral o, en su defecto de la distribución de frecuencias. Esta la
medimos siempre a través de una distribución de probabilidad supuesta o una distribución de
frecuencias (serie). Lo que veremos es cómo obtener esta medida suponiendo una distribución de
probabilidad normal, y también, utilizando la distribución de frecuencias que tenemos a mano
gracias a la serie de precios.
Las fórmulas a programar son las siguientes, donde “C” es el capital inicial, quien siempre se
asume igual a uno por convención. Veamos el álgebra:
Para la cartera, el riesgo de ruina nos dice que 1 de cada 20 veces la estrategia nos arruina,
mientras que la estrategia benchmark o “buy and hold”, casi 1 de cada 2 veces nos lleva a
la ruina.
Utilizando distribución de frecuencias. En estos casos debemos trabajar con la fracción de
la distribución donde los retornos son negativos. En este caso nosotros debemos definir el
tope o situación donde se considera que hemos caído en bancarrota. A partir de esta
definición, calculamos el riesgo de superar dicha pérdida máxima dada la distribución de
frecuencias. Entonces, calculemos la cantidad de rachas o ruedas malas que debemos
atravesar para llegar a esa pérdida máxima a partir de la pérdida media, calculemos U:
Entonces:
.
El riesgo de ruina en este caso se calcula del siguiente modo, es una probabilidad
binomial, donde W y L son probabilidades de ganar y perder respectivamente, U es la
cantidad de días con malas rachas. El cociente termina siendo una probabilidad que, al
estar elevado a U se revela como una variable aleatoria binomial. Veamos:
Esta fórmula tiene en cuenta situaciones donde superamos el tope máximo de pérdida
soportado e indicado al comienzo.
Volvamos a calcular todo de nuevo, pero esta vez también con el riesgo de ruina y para la
cartera y el benchmark, veamos:
Value at risk (VaR)
Esta métrica de riesgo es la más utilizada, y permite conocer qué porcentaje del capital se tiene en
riesgo en determinado porcentaje de las peores ruedas. Este porcentaje determinado de las peores
ruedas es un parámetro que elegimos nosotros. Por ejemplo, en general se fija el cuantil 5%, quien
el retorno que corresponde como límite máximo al 5% de las peores ruedas. Si la distribución de
frecuencias de los retornos es tal que este valor es un -3% entonces, el cuantil éste indica que 5 de
cada 100 veces perderemos un 3% o más del capital.
Veamos esto siguiendo el ejemplo:
También podemos calcular el VaR simulándolo con una distribución que elijamos, por ejemplo,
supongamos una distribución normal, y la más utilizada en finanzas que es la distribución de
probabilidad de jhonsonsu. Veamos estas simulaciones:
El método rvs() genera de forma aleatoria la cantidad de valores que se indiquen en su argumento.
Luego, el método hist() es el histograma, donde le pedimos 100 barras.
Ahora hacemos lo mismo con la distribución de probabilidad Jhonsonsu:
Para el caso de la cartera, vemos que el resultado indicaría que cuando se gana, en promedio se
obtiene dos tercios de lo que se pierde los días malos. En este sentido, el benchmark es mejor, sin
embargo, es necesario realizar una importante aclaración alrededor de esta ratio. Esta ratio no tiene
en cuenta la cantidad de días ganadores, ni tampoco la cantidad de días perdedores. Por ende, esta
ratio permite caracterizar comparativamente una estrategia en función de su benchmark, por
ejemplo, en este caso y sabiendo que la estrategia le gana al benchmark, vemos que la cantidad de
días ganadores es muy importante, ya que la rentabilidad promedio de los días ganadores es menor
a la rentabilidad promedio de los días perdedores.
Profit Factor
Esta métrica sí tiene en cuenta las ganancias totales y las pérdidas totales, de hecho, se calcula
como el cociente entre las ganancias totales y las pérdidas totales. Veamos:
Esta ratio tiene que ser mayor a 1, de lo contrario la estrategia genera pérdidas de capital. Lo malo
de esta ratio es que está expuesta a valores extremos, por ende, podría ser mayor a 1 sólo gracias a
unas pocas ruedas en las que nos fue bien, ruedas donde el resultado fue extraordinario y súper
raro. Por ende, esta ratio está afectada por las colas de la distribución de frecuencias.
En este caso vemos que la distribución de frecuencias de la cartera está estirada en favor de los
retornos positivos.
Gráfico comparativo de la TEA
Aquí comparamos el rendimiento anual de la cartera año a año versus el del benchmark. Veamos:
El mejor modo de consumir datos al navegar es utilizando APIs o, el “consumo de APIs”. Las APIs
son interfaces creadas por los servidores, y existen para humanos y para bots. La diferencia es
importante, porque para el bots, la estética de la página no importa, sólo importan los datos que se
quieren descargar. En este sentido, los humanos utilizan la interface web, mientras que los Bots
utilizarán un Jason, que son como un diccionario de Python.
Ahora, no todas las interfaces tienen APIs, por ejemplo, cuando se pueden ver en la web, pero no
podemos descargarlos, es porque no tienen APIs. Cuando esto ocurre, debemos buscar la forma de
descargarlos con un Bot utilizando la interface web.
En resumen, hay tres maneras de capturar datos en internet:
Interface web. Un robot que leerá la página web.
Consumo de API. Este es el camino serie. Están preparadas para enviar un bot y consumir
los datos. Las otras dos formas pueden generar un error cuando cambia la interface web,
por ende, en ellas es necesario brindar un “mantenimiento” al robot.
Captura de datos. Aquí se espera que la API interna del servidor envíe los datos al
interface web. Cuando los datos “están llegando” a la interface web, los capturamos.
Estas son las tres que veremos. Juan Pablo (profesor) sugiere usar siempre APIs, si es que existen y
el servidor la ofrece, si no, deberemos analizar alguna de las otras dos opciones. Si no es posible
con APIs, entonces intentaremos capturar datos, y sólo si esto no es posible utilizaremos la
interface web.
Veamos rápidamente las ventas y desventajas de las API y el Web Scraping:
Las siguientes APIs, que no sabemos si continúan en línea, son gratuitas:
Lo indicado es un Endpoint.
Llamados/Requests/Calls: Es la acción de entrar/comunicarse con el Endpoint, es decir,
con la URL.
Response: Es la respuesta de la API a ese llamado.
StatusCode: Es un código estandarizado que nos devuelve junto con el Response (abajo
los explico mejor).
Servidor: Es la compu de la API.
Cliente: Es la compu nuestra o nuestro programa que hace el request.
Credenciales: Son las claves o tokens de autenticación.
Body: Es el cuerpo de los mensajes (contenido) entre cliente y servidor.
Headers: Son encabezados de los mensajes entre cliente y servidor (A veces aquí viajan
las credenciales).
Parámetros: Son justamente los parámetros (variables) que necesita el endpoint que le
mandemos en el mensaje, hay opcionales y obligatorios.
SandBox: Son ambientes de prueba, para testear las funciones sin efecto real (como un
simulador).
POST, GET, PATCH, DELETE, UPDATE: Son los principales métodos de comunicación
cliente/servidor, a grandes rasgos GET es el más común, POST es más seguro y se usa
para autenticación o para envío de info. sensible ya que cuando viaja por GET podría ser
interceptada más fácilmente.
RATE LIMITS. Todas las APIs tienen esto, refiere al número de llamadas simultáneas o para
un período determinado de tiempo que se tiene. Estos límites están, algunas veces, porque los
servidores no dan abasto con todas las llamadas a la vez.
Por día, mes, semana
Por minuto, segundo
Ponderados según "peso" de cada endpoint
Por token o por IP.
STATUS CODE. Esto es una comunicación, como una llamada telefónica, en otras palabras,
es un intercambio entre nosotros, el cliente, que queremos el dato, y el servidor.
En general, los códigos de respuesta Http que nos devuelven todas las APIs en cada llamada,
pueden significar que la llamada salió bien o mal. La siguiente tabla muestra el código de
respuesta del servidor y su significado (si salió bien o mal). El código también funciona como
un mínimo estándar que orienta sobre dónde está la falla, en caso de haberla. Estos códigos
siempre son de tres dígitos, y siempre son numéricos. Veamos:
Aclaración: Un error del cliente es un error de nuestro, pues nosotros somos el cliente.
Interface Web. Web Scraping – Creando página de búsqueda
Para hacerlo debemos inspeccionar el código HTML del objeto de interés, por ejemplo, la tabla con
los datos que queremos, los nombres de los resultados que obtenemos al buscar en google, etc. Para
realizar esta inspección, y si tenemos el explorador Google Chrome, sólo debemos hacer clic
derecho sobre el objeto de interés, y luego clic izquierdo sobre “inspeccionar”. Al hacer esto se
abrirá una sub ventana con el código HTML, donde deberemos buscar la “class” del objeto de
interés. Con el código de dicha clase habremos identificado su “nombre”, y con ello podremos
indicarle al robot que enviaremos qué queremos que haga.
Veamos las siguientes imágenes para ejemplificar la inspección descripta:
Aquí podemos ver que hasta google.com/search? Es la URL de búsqueda, mientras que q=rofex
indica que se buscará dicha palabra. Por otro lado, y siguiendo lo descripto antes:
Ahora hacemos clic derecho sobre el primer resultado, pues es el que “nos interesa”. Vemos que se
abre una venta con la opción “inspeccionar”. Hacemos clic izquierdo y obtenemos lo mostrado en
la siguiente imagen.
Aquí podemos ver a la derecha de la imagen el código HTML y también el código de la clase que
nos interesa: “LC20lb MBeuO DKV0Md”. La siguiente fue la búsqueda que hizo Juan Pablo
(profesor) en julio 2022, la intención es que se vea que lo indicado es un nombre de objeto, pues
tanto ahora (06/12/2022) como en julio del mismo año, tiene el mismo nombre. Veamos la
búsqueda mencionada:
Todo lo que aparezca como resultado de búsqueda “Rofex” será de la misma clase, y por tanto,
tendrá asignado el mismo nombre de clase.
Ahora veamos cómo armar el robot. Importaremos la librería “requests” y escribiremos el siguiente
código, donde la intención es buscar los primeros 100 resultados de búsqueda que aparecen al
buscar “rofex”. Veamos:
Escribiendo “start=0” estamos diciendo que la búsqueda comience desde el 0, luego, al escribir
“&num=” estaremos indicando dónde queremos que termine, por ello es que luego le sumamos las
100 búsquedas. A esto le sumamos lo que queremos buscar. Con este código automatizamos la
búsqueda de lo que queremos encontrar.
Ahora debemos resolver el cómo obtener de dicha búsqueda lo que buscamos. Antes, veamos qué
forma tiene este HTML que obtuvimos:
Interface Web. Web Scraping – Obteniendo lo buscado
Aunque la búsqueda de páginas dentro del HTML creado se puede hacer de muchas formas, la
manera más “cómoda” es con librerías. Veamos el código anterior y sumemos más:
ACLARACION.
Esta parte del código “sirve” para engañar al servidor, pues nos “colocamos” el disfraz de un
navegador, haciéndonos pasar por uno para pedir los datos. La variable “h” contiene los diferentes
clientes/agents, que luego se coloca en el método get() de esta librería:
Por otro lado, imaginemos que deseamos descargar ratios fundamentales de acciones. Para
ejemplificar el Web Scraping de esto usaremos la web de MacroTrends. Veamos:
El código mostrado es posible de lograr lo querido porque la información está contenida en una
tabla en la interface web.
Finalmente, es importante saber que existen técnicas de Web Scraping más complejas como
solución a situación más difíciles. A continuación se copia la descripción hecha por Juan Pablo
(profesor):
Donde “function”, “symbol”, y “outputsize” son los parámetros que elegimos y debemos señalar el
valor requerido. El valor requerido es algo que también se busca en este documento de API.
En python, para llamar esta API debemos escribir lo siguiente según el ejemplo ubicado en el
mismo documento:
“json()” es una estructura de datos estándar en las APIs similar a un diccionario. Difieren por
ejemplo en que las comillas deben ser doble, no simples.
El ejemplo de Juan Pablo (Profesor) con esta API es el siguiente. Facilitando la siguiente página
señalada a continuación:
https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=AAPL&inte
rval=15min&apikey=2RG2NEF3IPXMIPX3
Como se puede leer en la documentación de esta API, la serie de precios intradiarios requiere la
definición de un conjunto de parámetros obligatorios y otros opcionales:
function: El nombre de la función (en esta API es obligatorio siempre este parámetro)
symbol: El ticker por ejemplo "AAPL"
interval: El intervalo entre cada vela (1min, 5min, 15min, 30min 60min)
apikey: Es nuestra clave, o token como definimos antes
Y una serie de parámetros optativos:
outputsize: Compacto o Full (aclara que por default compacto devuelve solo 100 datos)
datatype: Nos da la posibilidad de descargarlo en un CSV (tipo excel) o devolver un JSON
(texto que leerá python), por default es JSON y es lo que siempre vamos a usar, a menos
que sen la API para descargarse excels, pero no tendría mucho sentido
Al hacer Web Scraping, el procedimiento es el mismo, la diferencia es que la interface web
requiere buscar las claves, y uno está a la merced del cambio de la web. En cambio, con una API
todo está preparado para que el bot baje los datos sin mayores inconvenientes.
Las URL siempre tienen la misma estructura:
La URL base es: https://www.alphavantage.co/query
Luego le debemos poner los parámetros, para ello:
Al primer parámetro lo antecedemos por el signo "?"
Y para concatenar los otros usamos el signo "&"
Quedaría del siguiente modo:
https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=AAPL&inte
rval=15min&apikey=2RG2NEF3IPXMIPX3
Para escribir el código que nos permita tomar los datos debemos importar dos librerías:
requests: Para hacer el llamado HTTP a la API.
pandas: Para guardar los datos en un DataFrame.
Además, en el caso de esta API, la autenticación implica la solicitud de una API KEY (es un
token), que se ofrece gratuitamente (debemos colocar un correo que ni siquiera hace falta validar)
Teniendo la API KEY debemos identificar el EndPoint de interés a través del cual podremos
obtener los datos que queremos. Este EndPoint lo encontraremos en la documentación de la API.
Dentro de los parámetros de la API está el de API KEY, donde claramente deberemos colocar la
que hemos conseguido al aprobar el proceso de autenticación.
El código es el siguiente:
Vemos que el “r” es un objeto “response”. Para conocer sus atributos, donde veremos que contiene
un “json()”, escribimos dir(r). Veamos:
Esto continúa, el objeto tipo “json()” está más abajo en esta lista.
El “r” que obtuvimos es un objeto de la librería requests, el cual tiene varios elementos y funciones,
veamos algunas:
text o content: Es el texto plano como lo vemos en la web.
headers: Los encabezados.
status_code: Como vimos el código que ya vimos que es 200, es decir que la comunicación
esta ok.
url: la url.
cookies: Las cookies.
json(): El método para obtener el objeto json, o diccionario del contenido.
En este caso, y para si el código funcionó debemos conocer el status_code, y para ello escribimos
lo siguiente:
Como es 200, está todo bien, recuerde que los código que indican algún problema comienzan con 4
o 5. Más adelante utilizaremos los headers, veamos cómo llamarlos para conocerlos:
Esto es útil, porque en requests más avanzados, además de colocar la URL también indicaremos
los headers que queremos, tenga en cuenta que estos son diccionarios, como se aprecia en la
imagen.
Para ver la información que pedimos escribimos lo siguiente, de tal modo de verlo como variable
string:
El formato texto se utiliza en casos extremos cuando lo demás falla. En general, lo utilizado es la
función json(), que al introducirla en python transforma automáticamente el objeto tipo “json” a un
objeto diccionario. En otras palabras, utilizamos un diccionario en lugar de texto o un json.
Veamos:
Lo siguiente es guardar este diccionario en una variable. En este ejemplo se guarda en la variable
nombrada “mi_dicc”. Hecho esto, ahora pasamos esto a una DataFrame, quien deberá ser
transpuesto porque el diccionario tiene como claves las fechas en lugar del tipo de precio. Además,
las claves y las fechas son string, pues son números entre comillas simples, por ende, debemos
trabajar esto también. Finalmente, el orden de los datos está alterado, el primero es el más actual y
el último es el más viejo, y nosotros necesitamos que sea al revés por razones obvias.
Como ejercicio, veremos el paso a paso de este trabajo. Comencemos por la transformación y
transposición, veamos:
2Detalle: Si este diccionario es plano, entonces sí podemos usar “params”, pero si el diccionario tiene saltos de línea,
deberemos utilizar sí o sí “json” o “data”.
Meter todo el código en una función. Ahora vamos a crear la función que realice el mismo
procedimiento que el código que acabamos de hacer más prolijo:
Lo probamos:
Mecanismo para evitar errores (API/Usuario). Por ejemplo, hay errores evitables, como el
colocar mal un ticket, pues en estos casos, el código puede señalarnos que lo hemos escrito
mal. Para ejemplificar este trabajo, provoquemos un error en la función anterior, colocando
mal el ticket:
Para resolver esto tenemos que agrandar el código introduciendo, por ejemplo, un
condicional que revise el status_code y el ticket introducido, para posteriormente tomar
una acción en consecuencia (imprimir una frase que explique qué ocurre, por ejemplo).
Que el DataFrame tenga valores numéricos. Para cambiar los valores de string a float
aplicamos el siguiente código que sirve sólo aplicado para todo el DataFrame:
Veamos:
Dentro de la documentación está todo explicado acerca de cuáles son los parámetros
correspondientes para descargar los datos, para colocar órdenes, y para hacer otras operaciones. En
este sentido, en la sección anterior siempre utilizamos la función get(), pues sólo descargamos los
datos, pero ahora utilizaremos otras funciones, pues debemos realizar otras operaciones
adicionales. Por ejemplo, patch() se utiliza para modificar órdenes, delete() para eliminar, etc. Lo
conveniente es crear una función para cada acción, de tal modo que al querer operar se utilice
directamente la función.
Piense que estos códigos pueden combinarse con las estrategias de trading, brindando órdenes de
compra ante señales de compra, y órdenes de venta ante señales de venta. Los códigos de las
funciones que generan automáticamente las órdenes están en el google drive, “clase 11
complemento”. Aquí no se transcribieron por comodidad. En paralelo, Alpaca como otros brokers,
pueden conectarse con TradingView, para ello sólo tenemos que ingresar en la página de
TradingView, elegir “Trading panel” y buscar Alpaca para elegir “connect”.
Además de órdenes, también podemos generar WatchList, es decir, listas que contienen a los
papeles que cumplen determinadas condiciones, fundamentales y/o técnicas. Para armar esto
usamos CRUD, es decir, cuatro comandos: create, read, update, y delete. Estas también tienen
APIs con sus parámetros y demás (primero creamos las condiciones y después creamos la
WatchList).
Para aplicar esta estrategia usaremos nuevamente las librerías pandas y requests.
Lo que haremos será inspeccionar como antes cuando hicimos Web Scraping, es decir, haremos
clic derecho e “inspeccionar” sobre lo que querramos. En la sub ventana que se abra, iremos a la
pestaña de “Network” (el código HTML está en la pestaña “Element”), como se muestra en la
siguiente figura:
En esta pestaña se pueden ver cuáles son los procesos que se ejecutan en la web, incluso los
asincrónicos (aquellos que cargan luego de la aparición de otros), y para evaluar esto basta con
realizar un “refresh” de la página, lo que se verá es cómo se reinician todos los procesos.
En esta pestaña ordenaremos los procesos según su tipo, “Type”, haciendo clic izquierdo, y
buscamos y buscamos los tipos “xhr”, pues son los datos que provienen de algún sitio.
Luego buscaremos los procesos que en su nombre tengan la palabra “ajax”, pues estos tienen los
datos que buscamos en forma de json. En estos procesos haremos doble clic izquierdo para que se
abra una página o, sólo un clic y copiaremos en el navegador la URL que aparecerá.
Abriremos cada “ajax” para buscar los datos que necesitamos, por ejemplo, el que mostramos en la
imagen anterior. Entonces, en términos de la jerga, todos estos “xhr” + “ajax” tienen datos, son
todos EndPoints que potencialmente nos sirven dependiendo lo que queramos. En otras palabras,
todas estas URL son APIs, pero no son públicas ni privadas, por ende, no tienen documentación.
¡¡¡OJO!!!
Juan Pablo (profesor) menciona que, aun cuando hay APIs privadas, siempre podremos realizar
esta estrategia, sin embargo, para no tener problemas legales nunca deberemos utilizar esos datos
para venderlos u hacer nuestro propio negocio. Si lo hacemos, nos pondríamos en riesgo de tener
problemas legales, siempre que el demandante pueda demostrar que utilizamos sin pagar los datos
suyos en nuestro negocio.
Además de la búsqueda de los datos como se explicó, también tendremos que “adivinar” cómo
funcionan, es decir, cuál es su URL y cuáles son sus parámetros y posibles valores. No obstante,
los símbolos separadores son los mismos siempre, primero “?” y luego & como separador de cada
parámetro.
Por ejemplo, el profesor hizo este trabajo para BYMA y resumió lo siguientes EndPoints:
Antes de escribir el código es importante notar lo siguiente, es posible que con algunos
navegadores aparezca el siguiente mensaje:
# SSLError: HTTPSConnectionPool(host='www.byma.com.ar', port=443) =>
verify=False
Es por esto que la segunda línea del código se escribe utilizando la librería warnings. Esto ocurre
por lo siguiente: Al conectarnos a internet vía HTTP, el servidor siempre busca identificar si el
cliente consulta con navegador o un software diferente (como Python), entonces, para evitar un
error porque estamos con Python, le decimos que estamos navegando con cualquiera de los
navegadores que allí se muestran. No obstante, aun es posible que surja el error mostrado en verde,
pues no contamos con un encriptado que utilizan ellos. Recordar siempre que, la comunicación
HTTP es entre nosotros, cliente, y el servidor, y si la conversación no está encriptada, entonces
cualquiera puede espiar. Para que no surja el error señalado colocamos “verify=False”.
Cuando hagamos esto, aparecerá una advertencia recomendando colocar True. Para que ésta no
aparezca utilizamos las dos primeras líneas del código que se muestran a continuación. El código
sería el siguiente:
SEXTA PARTE: BACK OFFICE
Flujo de tarea
Esta sección la desarrollaremos teniendo como eje el siguiente conjunto de tareas, quienes son un
ejemplo de acciones habituales de backoffice. En este sentido, deberemos imaginar que tenemos un
listado de clientes, y que habitualmente realizamos las tareas listadas a continuación, por ende, el
objetivo de esta sección es desarrollar un código que las automatice. Concretamente, debemos
imaginar que:
Contamos con un listado de clientes.
Cada cliente tiene sus papeles en cartera.
Las tareas son:
1. Leer ese listado.
2. Ver para cada cliente si alguno de sus activos está entrando en bear-mkt, y en caso
afirmativo:
a. Preparar reporte de opciones (el derivado) en un rango dado de vencimientos, para
armar estrategia de cobertura (comprar un put del papel que se tiene y está en bear-
mkt).
b. Mandarle un mail avisando potencial bearMkt y adjuntarle listado de cobertura.
3. Mandar un reporte semanal con información sobre la cartera, por ejemplo: variaciones
semanales y mensuales por activo.
4. Adjuntar un gráfico en el mail con variacion de sus activos en base100 para los últimos 30
días.
5. Guardar en carpeta de cada cliente todos los reportes separados (el texto, la imagen, y el
Excel de coberturas).
6. Enviar un mail con el texto, la imagen y los excels de coberturas al cliente.
7. Guardar un ZIP en la carpeta del cliente con todos lo enviado por email, por fecha.
8. Borrar los excels sueltos que habiamos guardado para enviar el email.
Adicionalmente, calcularemos lo siguiente:
Volatilidad histórica (usaremos 40 ruedas)
Variación % de los últimos 7 y 30 días.
El bear-mkt será cuando (condición puesta para el ejemplo):
o Variación semanal es menor a -2%
o Variación mensual menor a -5%
Puts sugeridos:
Vencimientos de 20 a 90 días
Volatilidad implícita no más del 20% más alta que la histórica en 40 ruedas
El diagrama del “flujo de tareas” es el siguiente:
Por convención, en los diagramas de flujo siempre se coloca un rombo en la tarea que implica
tomar una decisión, quien por ende, implica codear un condicional (if else). Del mismo modo, los
cuadrados y rectángulos son acciones que no implican el tener que tomar una decisión. El tercer
tipo de figura, Juan Pablo (profesor) la colocó porque sí. Finalmente, todo lo que está dentro de la
línea punteada es un ciclo for o un while, pues el proceso se debe repetir para cada cliente.
Este diagrama de flujo es un ejemplo, las tareas no tienen porqué encadenarse de este modo, de
hecho, si tenemos muchos clientes, es muy probable que muchos tengan las mismas posiciones
sobre los mismos activos, por ende, este diagrama de flujo repetiría tareas de análisis de datos que
serían redundantes luego del primer análisis. Esto tiene solución, pero como el objetivo de esta
sección es mostrar cómo son los códigos para realizar tareas BackOffice, no viene al caso 3.
3 Una posible solución consiste en leer todos los activos que tienen los clientes, para a partir de esto descargar los datos,
realizar el análisis de bear-mkt y, luego de esto, seguir con el resto del código.
Conocer el directorio donde estamos
Vamos a utilizar la librería “os”, que es del sistema operativo. Con esta librería podremos
movernos por los directorios, crear y eliminar carpetas, y también moverlas de lugar. La función
“getcwd()” de esta librería indica el directorio (dirección) donde el programa está ubicado al
exportar e importar archivos (actualmente). En la notebook ASUS al 08/12/2022, tenemos lo
siguiente:
𝑖𝑚𝑝𝑜𝑟𝑡 𝑜𝑠
𝑝𝑟𝑖𝑛𝑡(𝑜𝑠. 𝑔𝑒𝑡𝑐𝑤𝑑())
En paralelo, y dependiendo del sistema operativo, para escribir la dirección de alguna carpeta hay
que utilizar barras simples en algunos casos, en otros barras dobles, y también, barras inclinadas
hacia la derecha, y en otros casos, inclinadas hacia la izquierda. Veamos el ejemplo que muestra
Juan Pablo (profesor):
Crearemos uno, pero lo haremos con rutas relativas y no absolutas (una absoluta es aquella donde
se indica paso a pasa el recorrido entre las carpetas, mientras que una relativa indica sólo el nombre
de la carpeta, y el software la busca sólo en el directorio donde estamos ubicados). Utilizar rutas
absolutas y relativas es útil, con cualquiera de ambas. Si sólo utilizamos rutas relativas, siempre
deberemos conocer dónde estamos ubicados, en cambio, si utilizamos absolutas, siempre
deberemos conocer la ruta previamente.
Usando la librería os, tomaremos su atributo “path” (ruta donde estamos), y preguntaremos,
utilizando el método “exists()” determinada carpeta, cuyo nombre indicaremos como argumento
del método. Hacer esto devuelve un resultado booleano, si existe la carpeta tenemos True, si no,
tenemos False.
Para crear un directorio utilizamos el método “mkdir()” indicando en su argumento el nombre de
la carpeta que deseamos generar. Si no es necesario crear la carpeta, entonces pedimos su ruta con
el método indicado antes “getcwd()”.
El código que construiremos tiene que preguntar si existe la carpeta, si no existe la creamos, y si
existe, entonces debe devolvernos su ruta. El código es el siguiente:
Continuando el hilo de la sección anterior, si el directorio existe, entonces nos moveremos dentro
del mismo utilizando el método “chdir()” cuyo argumento es el nombre de la carpeta. Estando
dentro, pediremos nuevamente la ruta hacia ella con “getcwd()”. Si, en cambio, el directorio no
existe, indicaremos que no está en la ruta designada con la función “getcwd()”. El código quedaría
del siguiente modo:
Este archivo se guarda donde está ubicado el intérprete, y no donde indicamos que se dirija
utilizando el código.
Cómo retroceder en el directorio
Para retroceder utilizamos el mismo método que para avanzar, o sea, “chdir()”, lo que cambia es el
argumento. Para ejemplificarlo, veamos dónde estamos parados, apliquemos el retroceso, y veamos
dónde quedamos:
Sólo debemos indicar la ruta, posicionar allí al intérprete, e importar el archivo, que este caso es un
Excel. Veamos:
No es buena idea iterar DataFrame, pues ello requeriría recorrer una matriz anidando bucles for, es
más sencillo interar un diccionario. Es por esto que crearemos un diccionario para cada tipo de
variable, o sea, para correos por un lado y para activos por el otro. Esto lo hacemos para cada
cliente, y en este sentido, el DataFrame que hemos hecho los tiene cómo índice, por ende, debemos
iterar el índice y pedir que cada fila (cliente) del índice sea asociada con cada valor de cada
columna. Además, con la variable “stocks” deberemos separar los papeles. Veamos el código:
Vemos que los activos son una lista en forma de clave en el diccionario. La idea es imprimir un
mensaje que señale los activos que tiene cada cliente. Hagamos el siguiente código:
Para obtener los datos utilizaremos la API de AmeriTrade. Para el caso de las acciones:
Enviando correos
Antes de pasar al código, primero debemos dar permiso a Python. Para esto, y partiendo de un
correo Gmail (de google), entramos en la página de inicio de google y hacemos clic izquierdo sobre
el logo de nuestra cuenta. Abriéndose una ventana, elegiremos “Gestionar tu cuenta de Google”,
como indica la imagen:
Debemos crear una contraseña y señalar el tipo de app (que es el conjunto de código que creamos
para enviar los correos). Con esto generaremos una clave de forma automática que sólo sirve para
que Python pueda enviar correos.
El código para enviar correos es el siguiente:
Primero traemos la contraseña y nos posicionamos en el directorio donde están las carpetas de
nuestros clientes:
Ahora comenzamos con el código para enviar los correos. Necesitamos crear las variables
remitente (nosotros que enviamos la información) y destinatario (cada uno de nuestros clientes –
recuerde que en este ejemplo todos los clientes tienen el mismo mail, al no ser así debemos trabajar
con un diccionario e iterarlo). También importamos la librería “smtplib”, que es útil para enviar
correos con Gmail.
Ahora pasamos este código a un ciclo for, para loguarse sólo una vez, y enviar el correo tantas
veces como cliente, cambiando el mensaje según corresponda:
A este código le hace falta adjuntar los archivos, pero antes de hacerlo es necesario aprender a leer
los directorios. Para esto usaremos el método “scandir()” de la librería “os”, que recorre todo el
directorio. También usaremos is_dir() que nos permite saber si cada elementos del directorio es
otro directorio o sólo un archivo (a nosotros sólo nos interesan los archivos). También utilizaremos
el método Split() indicando como argumento el símbolo a partir del cual se separa el string,
creando así un objeto lista con elementos que se identifican por estar, en su versión original,
separados por ese símbolo. Veamos un ejemplo de Split:
Todo esto nos permitirá identificar los archivos que no son zip. Veamos el código, quien nos dará
como resultado el nombre de los archivos que no son zip en la carpeta del cliente 2:
Ahora ya podemos enviar los correos con los archivos adjuntos. Por la complejidad evidente del
código, y el poco tiempo que queda para ver las clases restantes, sólo se copia el código, pero no se
coloca ninguna explicación. Lo único que se dirá es que la librería “email” es nativa de Python.
Comprimiendo y eliminando los archivos
Para hacerlo debemos importar la librería os y zipfile. Código:
SEPTIMA PARTE: INTELIGENCIA ARTIFICIAL
(IA)
Campos de estudio de la IA
Podemos ordenarlos entre cuatro grandes niveles:
Campo de estudio de la IA.
Paradigmas de IA.
o Tipo de Problema a resolver.
Algoritmos de IA que resuelven estos problemas.
Campos de estudio de la IA
Dentro de los campos de estudio de la IA se identifican tres grandes áreas, no obstante, en el fondo,
todas las IA son pura fuerza bruta, pues requieren de un “aprendizaje” indicado paso a paso,
repitiendo la receta a base de una fuente de datos, hasta que luego de repetir miles de veces los
mismos pasos, aprende. Más allá de esto, las áreas son:
Inteligencia Artificial. Es un intento por emular la inteligencia humana. Abarca campos de
especialización, como los que se menciona a continuación, incluyendo todo aquello que
aprende del entorno.
Machine Learning. Son algoritmos de aprendizaje automático. Su aprendizaje se nutre de
la actualización de la base de datos, con la cual se mejora el modelo. Permite resolver
muchos problemas, la industria está madura (hay muchas librerías)
Redes Neuronales: Deep Learning. Es más específico y técnico que el Machine learning.
Permite acceder a mejor soluciones. La industria está inmadura.
A continuación se ilustra la relación entre éstos campos.
Paradigmas de la IA
Cada paradigma reconoce diferentes tipos de problemas. Se reconocen tres, los primeros dos que
veremos, comparten una característica, tienen una entrada y salida de datos (input y output),
mientras que en el tercero sólo importa el cómo. Los tres siguientes:
Aprendizaje Supervisado. Es “fuerza bruta”, es decir, si deseamos que el código diferencia
a un perro de un gato damos al algoritmo una base de datos, y con ésta, aprenderá
buscando patrones a partir de los criterios que le indicamos. 4 Técnicas comprendidas aquí
son las denominadas de “clasificación” para variable categóricas, y regresión para variables
numéricas. Sus características/requerimientos del algoritmo son:
o Necesita datos "etiquetados" (diario del lunes).
o Se entrena un modelo para poder predecir.
Aprendizaje No-Supervisado. En este caso se pide lo mismo, diferenciar un perro de un
gato, y se otorga la base de datos, pero no se indica ningún criterio. Entonces, en base a una
variedad de criterios, elige cuál es el más relevante para clasificar. En este tipo
encontramos la IA llamadas “clustering”.
o No se "entrena", hace un análisis "indirecto".
o No predice, sino que más bien: separa, organiza etc.
Aprendizaje Reforzado. Este escapa al área de las finanzas, es muy aplicado en la industria
de los videojuegos.
o Se basa en recompensas.
o El aprendizaje es adaptativo.
o Ambientes dinámicos y cambiantes.
o El Output son las "acciones" que llevan a la meta.
Gráficamente:
4 Por ejemplo, el largo de la oreja, la distancia entre oreja y hocico, etcétera. Como complemento, una red neuronal
compararía pixel por pixel.
Tipos de problemas a resolver
Por ejemplo, en el aprendizaje supervisado encontramos diferentes tipos de problemas y por ello
diferentes tipos de soluciones. Con la regresión queremos conocer, por ejemplo, la temperatura que
hará mañana, mientras que con la clasificación querremos saber si una cosa es o no, o también,
obtener respuestas categóricas. En general, los problemas a resolver se clasifican entre los
siguientes:
Regresión.
Clasificación.
Agrupamiento/Clustering. Esto es lo más útil en finanzas.
Reducción de Dimensionalidad.
La mezcla entre clasificación y agrupamiento también es muy útil en finanzas. Las regresiones
prácticamente no se utilizan en el campo de las finanzas, al menos eso dice Juan Pablo (profesor),
pero todo lo que ellas implican, junto con sus supuestos, forman la base de lo demás.
Entrenamiento supervisado – Regresiones
Antes de aplicar una regresión debemos plantear para qué la queremos utilizar, y luego debemos
construir la base de datos, y luego de construir la base y testearla, haremos un ensayo de pre
sensibilidad. Es por esto que esta sección se divide en tres, una primera donde se construye la base
de datos, una segunda donde se realiza la regresión, y la tercera donde se realiza el ensayo de pre
sensibilidad.
Los algoritmos lo que se hacen es utilizar “fuerza bruta”, es decir, no aplican las fórmulas
vinculadas a la regresión, si no que van probando diferentes valores para los estimadores (betas),
con quienes obtienen un valor predicho y comparan con el valor real. Es así como los algoritmos
calculan el error. Habiendo obtenido un conjunto de pruebas y errores de estimación, eligen el
modelo o valores de estimadores que minimizan el error de estimación.
Research previo
Esta etapa implica construir la base de datos que usaremos para realizar la regresión, en otras
palabras, es necesario definir cuáles son los regresores y cuál es el regresando. Además, también
debemos asegurarnos que los supuestos de la regresión se cumplan, por esto también veremos qué
análisis hacer sobre los datos para después decidir si eliminar el regresor o parametrizarlo, si es que
es un indicador AT (Juan Pablo – profesor – dice que pocas veces se cumplen, pero que no obstante
los modelos se usan igual). Aplicado a finanzas, lo que haremos es regresar la rentabilidad
logarítmica de dos meses (60 días) respecto a seis indicadores, tres tipos de cruces de medias
móviles, un RSI, y dos volatilidades. En otras palabras, nuestra intención es predecir el movimiento
de un papel utilizando estos indicadores. Lo que buscamos con estos modelos es predecir con el
menor grado de error posible, y en este sentido, desecharemos los modelos cuando el porcentaje no
explicado de la variación sea mayor al sí explicado. Veamos la preparación:
La columna “fw” es el “diario del lunes”, mientras el resto de las columnas son el “feature”. Esto es
entrenamiento supervisado, pues como se puede ver en la tabla, brindamos las variables predictoras
y también indicamos qué es lo que ocurrió cuando esas predictoras asumían determinados valores.
Luego de construir la base de datos pasamos a evaluar las condiciones que debe cumplir la
regresión, veamos cada condición en las siguientes sub-secciones.
Multicolinealidad
Existe multicolinealidad cuando un regresor se explica con otro (correlación alta/significativa). Por
ende, debemos evaluar la matriz de correlaciones sobre los regresores o features. Veamos:
Los indicadores han sido elegidos a propósito, para mostrar justamente esto, la alta correlación
entre el RSI y las medias móviles, y el RSI y el otro indicador de volatilidad. La sugerencia de Juan
Pablo (profesor) es eliminar alguna de las dos variables que tienen una correlación igual o mayor al
50% (en sí, no existe un número estándar).
Este análisis también se puede realizar mirando una gráfica de dispersión entre cada una de los
regresores, veamos:
Eficiencia
También podemos mirar la distribución de frecuencias simples de los regresores, siendo que lo
ideal que todos tengan distribuciones diferentes, pues así, habrá mayor dispersión (menos de lo
mismo para explicar algo). Veamos:
En base a esto podemos eliminar uno de dos regresores con distribuciones similares.
Tenemos que hacer también diagramas de caja para visualizar valores extremos, su dispersión
alrededor de sus medias, y su distribución intercuartículica. Para esto, primero normalizamos todas
las variables (para trabajar con las mismas escalas):
En función de esto, podemos eliminar regresores en base a la cantidad de valores extremos que
tiene.
Autocorrelación
Esto es muy importante en serie de tiempo. La autocorrelación mide un regresor consigo mismo,
pero en el pasado. Lo siguiente es la autocorrelación de un cruce de medias para un lag de una
rueda. Es lógico que el resultado sea el que se observa:
Es lógico esperar autocorrelación alta en los primeros lags, no así en lags más largos. Lo que
buscamos con este análisis es que no exista una especie de “estacionalidad” en las
autocorrelaciones, es decir, que a partir de determinado tiempo o con cierto patrón de
comportamiento, la autocorrelación suba y baje.
Para evaluar lo mencionado utilizamos el gráfico de autocorrelación, veamos:
Donde en el eje horizontal tenemos los “lags” y en el eje vertical tenemos el r-cuadrado. Vemos,
que en este caso, el cruce de medias número 3 debe descartarse.
Realizando la regresión
Como otro repaso, recordemos los supuestos sobre la regresión múltiple, junto con sus tests.
Recordemos que se supone que los residuos se distribuyen como una normal (la distancia entre el
valor predicho y el real), por ende, para que la regresión minimice el error/residuo, debe cumplir
con las siguientes condiciones:
Linealidad de las variables (regplot). Esto implica que todos los regresores deben tener
una dispersión, versus los residuos, que sea lineal, de lo contrario, habrá algún patrón de
cambio que no se estará capturando con dichos regresores.
Independencia/Auto correlación de residuos (Durbin/Watson). Esto puede solucionarse
evitando la autocorrelación entre regresores.
Normalidad en los residuos (test Omnibus c/Kurtosis/skew, Test Shapiro-Wilk etc, qqplot).
Homocedasticidad (Varianza constante).
No existe multicolinealidad (heatmap de correlación entre predictores).
No hay exogeneidad o es débil. Esto significa que no existen otras variables predictoras
además de los regresores elegidos para explicar la variable endógena. Esto es un supuesto
más teórico que práctico.
Para realizar la regresión utilizaremos la librería “statsmodels.formula.api”, y particularmente, la
sub librería “ols”. Para ver su uso y cómo se escriben sus parámetros, veamos la aplicación a la
base de datos que trabajamos al principio:
NOTA. R cuadro ajustado.
Como se mencionó antes, las regresiones prácticamente no se utilizan, no obstante, Juan Pablo
(profesor), sugiere que no nos preocupemos por valores bajos de este, siempre que esté cerca a 0,3
o sea mayor, pues esto significa que podemos explicar al menos la mitad de las variaciones de los
precios, dejando al azar el resto. Esto en finanzas es bastante (dice).
Hagamos unos chequeos sobre de los supuestos sobre el modelo en cada una de las siguientes sub
secciones.
Para evaluar esto haremos una regresión lineal entre las predicciones del modelo y los valores
verdaderos. La relación debe ser lineal para respetar el supuesto.
El eje horizontal son las predicciones, mientras el vertical los valores verdaderos. Vemos que hay
tramos de linealidad, lo vemos en la siguiente figura:
Vemos entonces que en dicho tramo, el modelo sí cumple con la hipótesis de linealidad.
Veamos:
Normalidad de los residuos
Veamos
También graficamos los valores predichos estandarizados versus los verdaderos, para después
graficarlos según su ubicación en los cuantiles.
Multicolinealidad
Veamos:
Vemos que por multicolinealidad, como vimos antes, convendría borrar la variable “ema_vol” o
“roll_vol”, y por inflación de la varianza (que lo normal sería que sea menor a tres en la tabla de
arriba), vemos que conviene borrar “ema_vol”.
Homocedasticidad
Veamos la gráfica entre las predicciones y sus residuos (eje x predicción, y residuos):
Vemos nuevamente que la zona de la derecha tiene un buen desempeño, no así la zona de la
izquierda. En paralelo, Juan Pablo (profesor) dice que modelos con más – menos un sigma de
varianza es ideal, y por arriba de 2 estamos mal. Aunque lo principal es que se mantenga constante.
Ensayo de pre sensibilidad
En esta etapa buscamos mejorar el modelo, por ende, podemos probar diferentes valores de
parámetros para los regresores (cruce de medias, por ejemplo). Para esto haremos una simulación
de Montecarlo. Luego de esto se usa el modelo para evaluar portabilidad, es decir, se lo aplica a
otros papeles.
NOTA. Overfitting.
Es la situación donde el ajuste del modelo lo hacemos de tal modo que queda casi perfecto para
determinado papel/activo, pero al momento de realizar la evaluación de portabilidad (su
aplicabilidad para otros papales), vemos que no sirve. Entonces, lo que se sugiere al realizar el
análisis de sensibilidad, es que de la distribución de frecuencias de los valores de los parámetros,
se tome un rango que corresponde al más concentrado (pensando en una distribución, sería el
rango cercano a la campana de la distribución). De este modo se aumentan las chances de tener un
modelo portable.
Para contrastar y comprender mejor, un ejemplo de programación clásica sería lo que hecho con el
cruce de medias, donde brindamos la base de datos y establecemos la regla del cruce para decidir
comprar y vender, para que posteriormente el algoritmo aplique esta regla a la base de datos y nos
señale los momentos de compra y venta. En cambio, con machine learning introducimos la base de
datos y los momentos donde compramos y vendemos, para que el algoritmo cree reglas que estén
acorde.
Los algoritmos que resuelven una clasificación son:
Arboles de decisión.
Regresión Logística.
Máquinas de vectores de soporte.
Naive Bayes.
Bosques Aleatorios.
Estos son los algoritmos que veremos en las siguientes secciones. Pero antes de pasar a cada tipo de
algoritmo, es importante que en la base de datos con que se entrene el algoritmo, estén las
respuestas o valores de la variable a explicar, y para esto es importante haber construido predictores
o features. En finanzas, estos predictores pueden clasificarse entre algunas de las siguientes
categorías, y la elección y uso es un arte y parte del marco teórico que se debe realizar en el
research previo:
Indicadores sobre serie de precios:
o Indicadores tardíos (trend-following).
o Indicadores de saturación (contrarians).
o Indicadores absolutos/referencia: Osciladores acotados (sobre precio o sobre
indicadores).
Indicadores de flujos (volumen y derivados).
Indicadores combinados. Por ejemplo, cruce de medias ponderados por volumen.
Conteos discretos. Por ejemplo, de todos los papeles que se está analizando ¿cuántos están
por encima de su media y cuántos por debajo?
Estacionalidad.
Indicadores Estadísticos. Por ejemplo, sesgos, curtosis, varianza, etcétera.
Referenciales (Benchmarks).
Ratios y series de Análisis fundamental.
Sentiment.
Exógenos al mercado.
Juan Pablo – profesor – señala que el modelo debe trabajarse con al menos 9 de los 10 indicadores,
explicando por qué no está el que falta. En este sentido, al observar los indicadores, y teniendo en
cuenta el trabajo que se hizo para el posgrado (especialización en mercado de capitales), queda
claro que deben estar todos los indicadores (tal vez no el de “sentiment”).
Finalmente, también es vital el tipo de datos, identificando los momentos o épocas de cada uno.
Algunos consejos son, tomar:
Distintas épocas (por ejemplo, que tengan bear markets, derrapes fuertes, grandes
voladuras, y mercados laterales).
Variabilidad de activos similares.
Variabilidad de industrias.
Variabilidad de países.
Cantidades de datos acordes al fiting que se pretenda.
En definitiva, tenemos que tener en claro qué queremos hacer, pues esto será la referencia.
Arboles de decisión
Esta técnica busca como resultado “la estrategia” que genera las chances más altas de obtener los
resultados a partir de la base de datos que brindamos. En otras palabras, con esta técnica buscamos
la obtención de una regla/patrón que genere los mismos resultados a partir de una idéntica base de
datos. Dicho de otra manera, dada la base de datos ¿qué regla permite obtener los resultados? La
lógica detrás de esta técnica es la entropía de la información, que más adelante se definirá,
mientras que ahora la ejemplificaremos con un juego de mesa.
En el siguiente juego de mesa, que damos por entendido que se conoce, debemos adivinar al
personaje que ha seleccionado nuestro rival. Para esto realizamos una pregunta por turno, la cual
nos permitirá eliminar a los personajes que no cumplan con las características mencionadas en la
pregunta, por ejemplo ¿es hombre o mujer? Si la respuesta es “mujer” entonces se eliminan a todos
los personajes hombre. En un juego así, la estrategia que más chances de victoria genera es aquella
donde en pocas preguntas podemos eliminar a la mayor cantidad de personajes. Por ejemplo,
preguntar por el sexo permite eliminar una gran cantidad de personajes, por ello, lo que debemos
hacer siempre es realizar preguntas que permitan eliminar muchos personas en un mismo turno.
En el área de finanzas, buscaremos determinar chance de subas y bajas en la cotización, por ende,
el algoritmo preguntaría algo como ¿El RSI es mayor a 20? ¿Es mayor a 40? Y así sucesivamente.
Así procederá con cada feature, teniendo como resultado el siguiente árbol (ejemplo):
Mirando el inicio del árbol, vemos que si el indicador “ema_vol” es mayor a 1,33, entonces
tendremos que esperar una suba en la cotización del precio, pero si es menor o igual a este valor,
entonces tendremos que mirar el “cruce_2”, que si es mayor a 0,798 entonces esperaremos una
suba como lo más probable, pero si es menor o igual, tendremos que mirar nuevamente el
“ema_vol”… y así sucesivamente. Esto tiene un final como se puede apreciar en la segunda figura.
Obsérvese que existe un indicador llamado “entropy”. Cuando éste tiene el valor cero, indica que la
base de datos está completamente ordenada. Cuando es distinto de cero (positivo), indica que
existen chances o, que no hay certeza sobre las subas y bajas. Esta es la clave de la técnica, el
modelo busca aquellas variables y aquellos rango de valor de las variables que permiten bajar más
rápidamente la entropía (tener más certeza). El “riesgo” de trabajo con esta técnica es el de
overfitting (que haya muchas preguntas o ramas), por ende, perder capacidad de portabilidad.
NOTA PERSONAL. Podemos utilizar esta técnica para análisis fundamental, incorporando
indicadores macro.
El valor a predecir es la variación, a partir de hoy y dentro de cien ruedas, de GGAL, que
llamaremos “fw” (cotización de 100 ruedas en el futuro dividido por la cotización actual menos 1).
Los features que calcularemos son los mismos de antes: dos volatilidades, tres cruces de medias, y
un RSI. Veamos la línea de código:
Para que se comprenda, a riesgo de ser redundante: vamos a intentar predecir los valores de la
columna “fw” a partir de las columnas de indicadores:
¿Cómo escribir este código si queremos que se analice más de un papel? Veamos lo que hizo Juan
Pablo (profesor):
Esto se hace antes de tener la base de datos, así que en esta sección, más que definir qué queremos
predecir, vamos a ver cómo podemos traducir ese objetivo en código. No obstante, y como
ejemplos, lo que podemos buscar predecir son resultados categóricos para clasificar, por ejemplo:
Un resultado binario, con probabilidades.
Un suceso de cola direccional, por ejemplo, solamente me interesa predecir cuándo se
vuele más del 50% en un mes.
Un evento de cola no direccional, por ejemplo, que se mueva más o menos un 20% en una
semana. Juan Pablo (profesor) señala que esto se usa mucho para opciones.
También se puede contrastar verosimilitud entre predicciones.
También podemos combinar modelos con diferentes features, lo cual serviría para reafirmar las
predicciones entre ellos, y cuando éstas se contradigan, servirá para ponerlos en duda y pensar en
cómo solucionar sus errores. En el modelo que veremos, buscaremos:
1. Modelo 1: predecir si sube o si baja con cruces de medias.
2. Modelo 2: predecir si se vuela con volatilidades.
El código correspondiente es el siguiente:
Aquí creamos la variable “target” y le damos valor cero. Adicionalmente, la segunda línea de
código revaloriza los valores de “target”, colocándoles valor 1 para aquellos que se corresponden
con un “fw” mayor o igual a cero.
Asimismo, guardaremos una copia de la base de datos sin redondear y sin eliminar las filas con
valores “NaN”. Esto nos servirá más adelante, pero para adelantar, esto nos servirá para cuando
tengamos nuevos datos.
Finalmente, creamos dos series, una de respuestas (y o respuestas/labels) y otra de features (x, o
características). Veamos cómo quedó la base de datos “data”:
Caractericemos ahora la base de datos ¿Cuántas ruedas sube y cuántas baja? Veamos:
Mientras más cerca de 50% - 50% esté, mejor será el modelo, pues menos sesgo habrá. Que el
sesgo casi cero es lo mejor, pues en caso contrario, los features estarán muy relacionados con alzas,
en otras palabras, habrá pocos datos para relacionarlos con bajas. En otras palabras, en un modelo
muy sesgado, el vínculo entre features y label probablemente sea espúreo, y sea necesario
incrementar el período de análisis. Esta reflexión tiene detrás la idea de representatividad, que en
este caso debe ser reflejada por la base de datos. O sea, para poder predecir bajas y subas, debemos
tener muchos casos de ambos tipos, para poder compararlos con las features.
Como se mencionó, esto se puede solucionar reemplazando la base de datos, pero también puede
ser modificada a través de un “Oversampling” o de un “Undersampling”. El primero implica
replicar los casos menos frecuentes, o sea, copiar esos datos hasta que el sesgo desaparezca. El
segundo significa eliminar casos del grupo más frecuente, hasta que el sesgo desaparezca.
Primero corroboramos que la matriz de features tenga la misma cantidad de filas que el vector de
resultados:
Ahora separaremos la base de datos, pues con una parte construiremos el modelo, y con la otra
evaluaremos su capacidad de predicción, es decir, con la otra parte haremos el test de validación.
En general, se entrena con el 70% u 80% de la base de datos (no hay regla, aunque se dice que para
validar lo mejor es tener dos mil datos o más – por cuestiones estadísticas –). Aquí lo haremos con
el 60%. ¿Cómo realizamos esta separación? Lo haremos de forma aleatoria con el siguiente código
y librería:
Esta caso soluciona la autocorrelación, pues entrenamos con los primeros 7 años (aprox), y
validamos con los 7 años (aprox) siguientes. Y como los cruces de medias se autocorrelacionan en
meses, no tendremos el problema mencionado.
Importamos el modelo
Veamos:
Validando el modelo
Habiendo creado el modelo, ahora debemos validarlo, para ello haremos predicciones utilizando los
features que dejamos fuera al crear el modelo. Luego compararemos los resultados. En este sentido,
recuerde que las predicciones son valores de 0 (cero) y 1 (uno), o sea, rendimiento para dentro de
100 días menores a cero, o iguales o mayores a cero, respectivamente. Este vector de valores lo
guardaremos en un objeto:
Este modelo no sólo puede predecir si el precio sube o baja, también las chances de que ello
suceda. Para obtener las probabilidades escribimos lo siguiente (lo aplicamos para los primeros
diez datos para mostrar un ejemplo):
Ahora resta comparar la predicción con el dato verdadero. En este sentido, señalaremos de un
modo y de otro cuándo la predicción acierte y cuándo no lo haga. Si se predice que sube y sube,
entonces tenemos un acierto positivo, si se predice que baja y baja, tenemos un acierto negativo.
Del mismo modo, si se predice que sube y baja, tenemos un falso positivo, y si se predice que baja
y sube, tenemos un falso negativo. Esto lo podemos resumir en la matriz de confusión siguiente:
En función de la matriz de confusión podemos medir qué tan bueno es nuestro modelo, y
compararlo con alternativas. En este sentido, aunque tengamos falsos positivos y negativos, mirar
su magnitud relativa importa, pues si nos interesan sólo las predicciones bajistas y los falsos
negativos son bajos, entonces el modelo será bueno para estos fines.
Esta matriz la podemos construir con las siguientes líneas de código:
En paralelo, podemos obtener la matriz de confusión normalizada directamente (con porcentajes).
Para esto dividimos los valores por el total de observaciones – las utilizadas para el modelo y las
utilizadas para la validación-, obteniendo un 100% con la suma de los 4 casos. Como alternativa
podemos utilizar sólo las observaciones utilizadas para el modelo, en cuyo caso, en lugar de dar el
valor “all” al argumento, deberemos escribir “pred”, de este modo la suma de las columnas será
100%. Otra alternativa es dividir por los valores verdaderos, por ende, obtendremos el 100% como
la suma de las filas, y el valor del argumento a colocar es “True”. Veamos cada caso:
Ahora grafiquemos:
(OJO que estos resultados son espurios, pues hemos separado la base de datos, entre
entrenamiento y validación, de un modo random, incrementando las chances de validar
espuriamente por autocorrelación. De hecho, si utilizamos la separación alternativa que se
mencionó, el modelo dará malos resultados – dice Juan Pablo (profesor)-).
A continuación mostramos otro método para graficar la matriz de confusión, pues las chances de
que el anterior quede obsoleto son altas (lo sacarán). Veamos:
Con esto ya tenemos el resultado, no obstante, también podemos resumirlo del siguiente modo,
mostrando el sesgo alcista. Esto es, recuerde que en los datos reales, el modelo tenía un sesgo
alcista, es decir, el 56% de los fw erán iguales a cero o mayores. También dijimos que la técnica de
árboles de decisión tiende a exagerar los sesgos, esto es, cuando reproducimos los valores
predichos en la validación, se generará el mismo sesgo un poco más grande. Es lo que ocurre en
este caso, como se ve a continuación:
Prediciendo
El ejercicio que haremos ahora requiere del uso de tuplas, pues el valor predicho se arroja como
una tupla. En este sentido, recuerde que una tupla con un único valor distinto a cero se escribe
como se muestra a continuación:
𝑥 = (1, )
Asimismo, una tupla con lista única se escribe:
𝑥 = ([1,2,3,4], )
Retomemos la base de datos que guardamos al comienzo de este ejercicio:
La idea es tomar los datos que habíamos eliminado por tener corresponderse con algún “NaN”, y
los utilizaremos para predecir. Por ejemplo, utilicemos la última fila de valores de cada feature:
Vemos que el valor predicho para dentro de 100 días es una caída en la cotización, y que la
probabilidad de que esto ocurra es del 57%.
Crítica a la técnica
El problema que tienen los modelos de árboles de decisión son los “clusters”, veamos primero
gráficamente este problema:
Las situaciones como la indicada con un rectángulo verde, son situaciones a las que se asigna la
misma probabilidad a pesar de tener valores diferentes para los features. Esto sucede por la
autocorrelación. Esto también sucedería si se parte de la base de datos con la técnica alternativa (lo
dice Juan Pablo – profesor-). En definitiva, el problema es que está “muy” clusterizado. En
palabras simples, entrenamos con datos similares a los datos con que validamos (esto es lo que
genera la autocorrelación).
Para resolver la autocorrelación debemos estudiar la multicolinealidad entre los features, y la
autorregresión de estos consigo mismos. Habiéndolo analizado, debemos reemplazas/eliminar los
features con multicolinealidad o autorregresión.
Graficando el modelo
Veamos:
Guardando el modelo - Serialización
Veamos:
Con este código guardamos el modelo entrenado bajo el nombre de “modelo_arboles.dat”. Lo que
hacemos luego si queremos usarlo es llamar el archivo, es decir, ya no es necesario hacerlo correr o
crearlo nuevamente. Veamos cómo llamarlo, descarguemos la información y utilicemos los últimos
valores de las series, para con ellos utilizar el modelo y predecir. Veamos:
Oversampling y Undersampling
Supongamos que queremos predecir un evento de cola, por ejemplo, predecir la probabilidad de
que GGAL crezca un 20% o más el próximo mes. En este caso, el label o variable verdadera que
construiremos, tendrá muy pocas observaciones con éxito y muchas observaciones con fracasos,
por la naturaleza del evento (muy pocas ruedas con crecimiento por arriba del 20% y muchas por
debajo). Estaríamos del siguiente modo, con las naranjas como éxito y las azules como fracasos:
En estos casos, el sesgo será muy grande, por ende, el modelo predecirá siempre que el evento no
ocurrirá (recuerde que la técnica de árboles de decisión sobre exagera los sesgos).
Para resolver esto se puede hacer es eliminar datos azules, es decir, eliminar datos donde el
crecimiento fue menor al 20%. Esto se llama “Undersampling”. Como alternativa, se pueden
duplicar o más, las ruedas donde la cotización supera el 20%, a esto se llama “Oversampling”.
Gráficamente:
Para poder aplicar alguno de estos métodos debemos instalar las librerías:
Estas líneas de código son sólo para establecer cuestiones estéticas sobre las figuras que se
construirán más adelante. Lo que haremos ahora es descargar las series de precios y preparar la
base de datos para trabajarla con esta técnica. Lo que intentaremos hacer es predecir si una vela
diaria será verde (alcista) o roja (bajista) en base al GAP entre el precio de de cierre del día anterior
y el precio de apertura del día. Además, clasificaremos estos resultados de acuerdo a la tendencia,
que se define como la posición del precio de cierre respecto al precio de cierre promedio de las
últimas 100 ruedas. Del mismo modo, incorporaremos los días de la semana para clasificar estos
resultados. Veamos:
La lógica del razonamiento, al aplicar esta técnica, consiste en agrupar al conjunto de pares
ordenados entre diferentes categorías, por ejemplo, según el día de la semana, o si la tendencia es
“Bear” o “Bull”. Para que se comprenda mejor la hipótesis de trabajo, comencemos a buscar
correlación con un diagrama de dispersión sobre el conjunto total de pares ordenados entre
regresando y regresores. Es muy probable que al observar el gráfico de dispersión no se observe
correlación alguna, sin embargo, y como hipótesis de trabajo, tal vez sí exista correlación entre los
diferentes sub conjuntos/grupos. Por ende, la clave se encuentra en el mismo sitio que al aplicar la
técnica de árbol de decisión, la creación de la variable label, pero en este caso, la creación de los
diferentes grupos que sirven para clasificar al regresando.
Lo mencionado puede apreciarse en la siguiente gráfica, donde se presenta el diagrama de
dispersión de todo el conjunto de pares ordenados, y donde a simple vista no se observa correlación
alguna entre las variables (GAP y cierre de vela). En esta gráfica, se presentan diferentes tonos de
colores, quienes permiten revisar visualmente cada subconjunto definido según su frecuencia.
Veamos la figura:
Aquí podemos ver cómo para el sub conjunto de pares ordenados en el grupo de tendencia alcista,
la correlación es leve y negativa, mientras que, para el subconjunto de pares ordenados del grupo
de tendencia bajista, la correlación es nula.
Del mismo modo se grafican estos diagramas de dispersión para los grupos formados según el día
de la semana:
Antes de aplicar la técnica de regresión logística, veamos algo de su álgebra y las hipótesis de
trabajo. El modelo es el siguiente:
Otras funciones de reactivación (del tipo de la segunda ecuación) son las siguientes:
Lo que se puede apreciar, como se marca en la siguiente imagen, es que el modelo no es muy
bueno al clasificar, no obstante, se puede ver alguna diferente dentro de cada grupo. Por ejemplo,
para el caso de tendencia bajista (Bull), se puede ver cómo las predicciones que se ubican en el
techo son menos frecuentes que las predicciones que se ubican en el piso, no obstante la gran
frecuencia en alrededor del centro tanto para techo como para piso.
Preparando la base de datos
Descarguemos la serie de precios de GGAL, es decir, la misma base de datos que se trabajó con
árbol de decisión. Asimismo, buscaremos predecir la posibilidad de un incremento del precio
dentro de 100 ruedas (crece o no crece), utilizando como regresores tres tipos de cruces de media
móvil, el RSI, y dos medidas de volatilidad. Por ende, descargaremos la base de datos y la
prepararemos para aplicar la regresión logística:
Preparamos la matriz de regresores por un lado (X) y el vector de respuesta o con valores del
regresando (Y). Veamos la matriz de regresores:
Como antes, separaremos base de datos entre aquella que se utilizará para “entrenar” el modelo, y
aquella otra que utilizaremos para la “validación”:
Separación Random:
En este caso aplicamos la separación Random. Recuerde que para el caso donde existe
autocorrelación en los regresores, lo que se debe es separar del segundo modo, no del primero, pues
en este tipo de casos corremos el riesgo de validar datos con valores fuertemente
autocorrelacionados. En otras palabras, con el segundo tipo de separación de datos, no importa si
existe autocorrelación, no habrá problemas en la validación.
Finalmente, debemos escalar los datos, esto es, como en general los regresores no comparten una
misma escala, se producen problemas que se traducirían en estimadores ineficientes (betas), pues
estos darán mayor ponderación a los regresores con escalas más grandes sobre los que tienen
escalas más pequeñas. Es por esto que se normalizan con una u otra técnica, en este caso lo
haremos será: a cada valor se le resta su media histórica para luego dividirlo por su desvío estándar
histórico. De este modo logramos el cometido de tener un mismo rango para todos los regresores:
Las últimas dos líneas de código escalan lo datos de la matriz copia que se generó con el código
inicial (al descargar y preparar la base de datos). Este escalamiento se realiza con todos los datos, y
servirá para utilizar los valores más actuales para predecir qué ocurrirá con el regresando. En este
sentido, recuerde que al estar prediciendo a 100 ruedas con medias móviles, hay valores de los
regresores que no utilizamos.
Por último, sepa que hay otros escalamientos que se utilizan cuando los regresores tienen una
distribución de frecuencias muy alejada de una normal, pero en la mayoría de los casos, el utilizado
aquí es útil.
ACLARACION
El escalamiento de los datos debe hacerse después de la partición de la base de datos, pues al
entrenar el modelo utilizamos una parte de la base de datos, y los estadísticos (media y desvío)
utilizados para escalar los datos deben ser construidos con la base de entrenamiento, no con la
base de datos completo, en caso contrario, estaríamos entrenando el modelo con una base partida
que utiliza medias y desvíos de la base completa. Lo mismo aplica a la base de datos de
validación.
Entrenando el modelo
Este vector de probabilidades refleja la decisión automática y por defecto que toma el modelo de
regresión logística: El valor predicho se asigna a las probabilidades que están por encima del 50%,
por ejemplo, si tenemos un 56,74% de chance de que el precio dentro de 100 ruedas esté por debajo
del actual, entonces el valor predicho será cero, y si fuese al revés, será uno. Este valor por defecto
puede modificarse, sólo se debe conocer el argumento correspondiente.
Veamos:
Recuerde que el aumento de sesgo visto con el árbol de decisión, aquí no surge. En este sentido, en
estos modelos también se debe realizar el OverSampling o el UnderSampling.
En este sentido, observemos que las probabilidades de ocurrencia de los últimos 5 datos no son las
mismas como sí ocurría con el modelo de árbol de decisión (aunque el valor predicho sí es el
mismo):
Regresión logística versus árbol de decisión
El árbol de decisión es muy sensible a cambios en el valor del regresor, en tanto que pequeños
cambios tienen altas chances de modificar el valor predicho, ceteris paribus el resto de los
predictores. En cambio, en regresión logística, esta sensibilidad es sustancialmente menor.
En paralelo, la regresión logística no puede caer en overfitting, como si sucede con el árbol de
decisión.
Otro modo de clasificar sería con círculos, por ejemplo, procediendo como se ve en la siguiente
imagen. Así lograríamos dos grupos, uno de azul oscuro y otro de azul claro. Incluso podríamos
incorporar un segundo círculo y tener tres grupos con tres tonalidades diferentes, el primer círculo
oscuro, el segundo con un tono más claro, y el tercer grupo sería el de colores claros. La diferencia
entre el primer círculo y el segundo se llama “tolerancia/soporte”.
El siguiente es otro ejemplo, más directo:
El problema de los círculos es que no cumplen con la definición de función, pues tienen para un
mismo valor del dominio, más de un valor en la imagen. Esta situación es quien nos acerca a la
transformación no lineal de la función, llevando la función de dos dimensiones a otra de al menos
tres dimensiones.
A partir de esta idea, el orden de trabajo con esta técnica se divide en dos pasos, aunque son
similares a los casos anteriores: I) primero identificamos los grupos, azules claros, azules oscuros,
etcétera, es decir, en la base de datos deberemos crear una variable que permita identificar cada
rueda/fila de acuerdo a los grupos que creemos; y II) segundo, aplicaremos la fórmula con
diferentes parámetros, según el caso, para crear estos grupos de forma abstracta y de acuerdo a lo
que señalamos en el primer paso. Entonces, con esta fórmula y los nuevos valores de features
predeciremos si una nueva observación corresponde a un grupo u otro.
Este modo de clasificar requiere que se defina el modo en que se tratará aquellas observaciones que
“caen” en zonas grises. Es aquí donde se utiliza el concepto de “vectores de soporte”, quienes son
otra transformación algebraica que permite terminar definiendo dicha observación como parte de
un grupo u otro. Gráficamente:
Los cuadrados rojos estaban en la zona del vector de soporte, quien al aplicarse permite ubicarlos
en uno u otro grupo.
Aplicación
Lo que debemos hacer ahora es descargar la serie de precios, definir qué queremos predecir,
trabajar la base de datos para crear los indicadores o features, partir la base de datos para crear (de
forma aletoria o no – recordar la advertencias sobre autocorrelación), escalar los features, entrenar
el modelo, validarlo, y predecir.
Como antes, lo que haremos será predecir si la cotización de GGAL dentro de 100 ruedas es mayor
a la actual o no. Para esto utilizaremos tres tipos de cruces de medias móviles, un RSI, y dos
medidas de volatilidad. La separación de la base de datos se hará de forma aleatoria. Veamos el
código para descargar la serie de precios y preparar la base de datos:
Ahora partimos la base de datos:
El parámetro “kernel” y particularmente su valor ‘rbf’ es quien transforma la función lineal en una
radial. En la bolsa, este es el argumento principal por no decir el único, en palabras de Juan Pablo
(profesor), los demás argumentos no son muy útiles. En parámetro “C” es el vector de soporte, en
este sentido, este es el parámetro a través del cual el modelo que creemos puede terminar estando
overfitting (por el valor que coloquemos en este argumento).
Ahora realizamos la predicción:
Los valores de acierto son tan buenos que contrastan con la experiencia de Juan Pablo (profesor), y
es a partir de esto que toma forma la sospecha sobre la existencia de algún error. Si bien es
importante este contraste entre resultado y experiencias, lo que siempre debemos hacer es evaluar
el cumplimiento de los supuestos, es decir, que no exista multicolinealidad ni autocorrelación. Por
el tipo de features que utilizamos, y la técnica de separación de base de datos utilizada, lo que está
claro es la presencia de autocorrelación. En este sentido, si utilizamos el método de separación
alternativo, los resultados obtenidos son los siguientes:
Lo que vemos aquí es la solución al error provocado por la autocorrelación entre la matriz de
entrenamiento y de validación. También vemos otro problema, el sesgo positivo, que nos está
reflejando lo indicado al ver árbol de decisión, en la muestra (base de datos de entrenamiento) se
tienen muy pocos datos que reflejen situaciones a la baja. Por ende, este tipo de problemas se
soluciona aumentando el tamaño de la muestra, y/o incorporando períodos de tiempo donde las
cotizaciones han caído. Como alternativa, también se pueden modificar los valores asignados a los
parámetros del modelo, concretamente, los relacionados con “gamma” y el “C”.5
Una última observación sobre este modelo: siempre utiliza una semilla inicial para introducir
aleatoriedad, para fijarla, al modelo debemos explicitarle el argumento opcional “random_state”, y
asignarle el valor cero.
A diferencia del árbol de decisión, el SVM genera probabilidades extremas con más frecuencia
(más cercanas a 1 y 0). Y a diferencia de la regresión logística, el SVM es más sensible al
momento de clasificar, es decir, ante un leve cambio en el valor de los features es más probable que
se produzca un cambio de clasificación bajo este método que bajo la regresión logística (en este
sentido, es más parecido al árbol de decisión).
5Apreciación personal: Antes de modificar los valores por defecto, sería conveniente profundizar el entendimiento sobre
cómo funciona esta técnica.
Bosques aleatorios
Este método toma la base de datos y las fracciones en sub muestras, aplicando en cada una un árbol
de decisión. Por ejemplo, toma sólo un feature con la mitad de las filas, luego toma tres features y
toma las dos mil filas que se ubican en la mitad de la base de datos, y así sucesivamente.
Entonces, para cada sub muestra se genera un árbol, hasta general un bosque con K árboles:
A partir del bosque, se aplica nuevamente un criterio de consenso, entropía o gini, por ejemplo,
sobre las predicciones de cada uno de los árboles, y aquella combinación que genere la entropía
más baja es la utilizada para tomar la decisión.
Recordemos que el problema del árbol de decisión es su sensibilidad ante pequeños cambios en los
valores de los features. Con esta combinación de árboles de decisión, esta sensibilidad se reduce
sustancialmente.
Aplicación
Lo que debemos hacer ahora es descargar la serie de precios, definir qué queremos predecir,
trabajar la base de datos para crear los indicadores o features, partir la base de datos para crear (de
forma aletoria o no – recordar la advertencias sobre autocorrelación), escalar los features, entrenar
el modelo, validarlo, y predecir.
Como antes, lo que haremos será predecir si la cotización de GGAL dentro de 100 ruedas es mayor
a la actual o no. Para esto utilizaremos tres tipos de cruces de medias móviles, un RSI, y dos
medidas de volatilidad. La separación de la base de datos se hará de forma aleatoria. Veamos el
código para descargar la serie de precios y preparar la base de datos:
Separamos la base de datos de forma aleatoria (recuerde las aclaraciones hechas sobre la
autocorrelación en la validación):
Escalamos el modelo (aunque no hace falta dado que la técnica es un árbol de decisión):
Entrenamos el modelo:
Predecimos:
Validamos:
Comparación entre Bosques aleatorios y los otros métodos
A diferencia del árbol de decisión, el bosque aleatorio es menos sensible ante cambios en los
valores de sus features, en este sentido se encuentra más cercano a la regresión logística. Del
mismo modo que los árboles de decisión, el bosque aleatorio puede caer en un overfitting debido a
las capas de profundidad indicadas en relación al tamaño de la base de datos. La gran particularidad
de este tipo de modelos es que tiende a generar probabilidades muy cercanas al 50%.
Métricas para modelos predictivos
Las siguientes métricas permiten comparar modelos entre sí de un modo más completo (además del
uso de la matriz de confusión). Juan Pablo (profesor) menciona que estas métricas no son muy
útiles en la bolsa, no obstante, es importante conocerlas. Son las siguientes:
Positivos Totales = Positivos detectados + Falsos negativos.
Negativos Totales = Negativos detectados + Falsos positivos.
Sensibilidad = Positivos detectados / Positivos Totales.
o Es la probabilidad de que un positivo efectivamente sea pronosticado positivo.
Especificidad = Negativos detectados / Negativos Totales.
o Es la probabilidad de que un negativo efectivamente sea pronosticado negativo.
Exactitud (Accuracy) = Mediciones Correctas / Totales.
Precisión = Positivos detectados / (Positivos detectados + Falsos Positivos).
F1 score = 2 * Precisión * Sensibilidad / (Precisión + Sensibilidad).
La librería y los códigos son los siguientes:
Entrenamiento no supervisado - Clustering
Una primera diferencia con los algoritmos anteriores, es que estos son útiles sólo para clasificar, no
para predecir. Además, otra diferencia respecto a los modelos anteriores, es que aquellos se ubican
dentro del entrenamiento supervisado, mientras estos no requiere supervisión. La presencia de
supervisión significa que existe un vector de labels, es decir, al descargar la base de datos, y antes
de aplicar el método de clasificación, nosotros debemos etiquetar las observaciones (ruedas) de
acuerdo a algún criterio, luego, el algoritmo que utilicemos establecerá el vínculo entre los features
que elegimos y los lables que establecimos. Cuando la supervisión no existe, para que el algoritmo
pueda clasificar se utiliza un criterio que es especificado por nosotros mismos, y esta es justamente
la clave al trabajar con estos algoritmos, el criterio que definimos. Este algoritmo permitirá crear
las clases o el vector de labels. Veamos la siguiente imagen para ver un ejemplo de esta forma de
razonar:
Ahora incorporamos código para identificar la forma de las velas anteriores y posteriores a la
actual:
Clusterización – Conceptos
Siempre tenga presente que por Cluster nos referimos a grupos, y que la clusterización es útil
segmentar:
Clientes. Por ejemplo, se quiere agrupar los clientes para identificar fidelidad y así
establecer promociones, los features podrían ser I) cantidad de compras, II) gasto realizado,
III) variedad de productos comprados, IV) medios de pago utilizados, V) cantidad de
referidos que tiene, VI) cantidad de tiempo que hace que no compra, etcétera. Como no se
sabría qué elemento es más importante para clasificar, se terminaría utilizando esta técnica.
Nichos de mercado.
Detectar fraudes.
Estos métodos son muy eficientes siempre que se apliquen a bases de datos de hasta 10 mil
conglomerados iniciales (cantidad de datos inicial), más de esa cantidad empieza a ser ineficiente y
conviene migrar a otro tipo de algoritmo. Esto es así porque a partir de dicha cantidad
(aproximada), aplicar el método tiene un alto costo computacional.
Al momento de crear una clase que permita diferenciar sus integrantes de los miembros de otras
clases, el criterio de diferenciación debe definirse para los tres puntos siguientes:
Grupos que me diferencien bien los distintos tipos de datos.
COHESION (distancia intra cluster).
SEPARACION (Distancia entre clusteres).
Antes de aplicar algún método fijando el criterio de clasificación, introduciremos una base de datos
creada por nosotros mismos. El código de la base de datos es el siguiente:
En este diagrama de dispersión se pueden apreciar cinco “nubes”, pero también se podría decir que
sólo hay dos grupos, el conformado por las tres nubes en la zona baja izquierda, y el conformado
por dos nubes en la zona alta derecha. ¿Hay cinco grupos o dos? ¿O hay más grupos? Todo
depende del criterio que utilicemos. Las variables de los ejes bien pueden ser el RSI en el eje de
abscisas y la cotización forward en el eje de ordenadas. No se introducen más variables porque,
valga la redundancia, es un eje cartesiano, y a los efectos didácticos es mejor presentar el tema de
este modo, sin embargo, presentarlo así no perjudica el método, pues
simbólicamente/algebraicamente, es lo mismo.
Como se mencionó antes, el criterio debe cumplir definir con precisión qué se entiende por
cohesión y separación, y en este sentido, el concepto que sintetiza a ambos es la distancia, por
ende, la elección del criterio de agrupamiento se reduce a una elección que implica definir qué es
distancia.
Antes de describir algunos conceptos de distancia, mejoremos la gráfica facilitada arriba, para esto
colocaremos etiquetas (números) a cada punto del diagrama, así facilitaremos la identificación de
cada punto. Veamos el código que nos permite hacer esto:
Las coordenadas de cada punto permiten identificarlos simbólicamente. Por ejemplo:
Entonces, buscaremos agrupar los puntos según la distancia que cada uno tenga con los demás.
¿Qué entendemos por distancia? Un concepto puede ser la distancia geométrica o hipotenusa, y si
el movimiento diagonal está prohibido, puede ser la suma de los catetos ¿Y la escala importa? Si
importa, entonces deberíamos tener escalas logarítmicas. El punto a tener presente aquí es la
variedad de definiciones que existen al momento de definir distancia. Veamos algunas
definiciones:
Distancia geométrica (euclidea): Es el teorema de Pitágoras pero generalizado:
Aquí, p y q son ejes x e y, mientras que el sub índice son las dimensiones. Por ende,
podemos hablar del x e y de la dimensión 1, 2, 3, etcétera. Por ejemplo, para el caso de una
dimensión:
Cityblock geomtry (Manhattan): Esta distancia es la suma de las distancias absolutas de los
ejes en cada dimensión (como antes, p y q son los ejes x e y, mientras que el subíndice es
la dimensión):
Por ejemplo:
Chebyshev: Aquí, la distancia se define como la máxima diferencia absoluta entre las los
ejes x e y (p y q) entre todas las dimensiones.
Por ejemplo:
La librería que se utiliza para medir distancia con una u otra geometría es la que se muestra a
continuación:
Aquí también podemos conocer el nombre de otros tipos de geometrías que no se describieron aquí.
Para continuar con el ejemplo, y antes de aplicar el método, calcularemos la distancia entre los
puntos utilizando el criterio euclideo (donde además, todas las dimensiones tienen el mismo
peso/ponderación). Veamos:
Clusterización – Sección gráfica
En las siguientes sub-secciones se describen los diferentes criterios que terminan por definir
diferentes modelos de clasificación. Según Juan Pablo (profesor), los más utilizados son los
primeros dos que se mencionan a continuación. Por otro lado, en cada sub-sección veremos que
importamos una librería y alguna sub-librería, estas son:
La “dendrogram” nos sirve para graficar, “linkage” sirve para agrupar, y con “fcluster” obtenemos
las etiquetas de cada grupo una vez definido el corte, es decir, con esta última sub librería logramos
establecer un corte para definir la cantidad de grupos con quienes trabajaremos.
Un Dendograma jerárquico es un árbol que ubica cada punto de acuerdo a su cercanía, y lo ubica
de acuerdo a una jerarquía. Entonces, se establece una cantidad determinada de distancias que
permite crear grupos grandes, y dentro de estos se aplican otras distancias para crear otros grupos, y
dentro de estos se repite el procedimiento. El resultado final es algo parecido a lo siguiente, donde
se agruparon los puntos generados en la sección anterior.
Este criterio sirve cuando queremos hacer hincapié en la cohesión antes que en la separación. En
el código debemos asignar el valor “single” al argumento “method”. Siempre se comienza uniendo
los puntos más cercanos entre sí, pero luego de unir dos puntos, es decir, de haber creado el primer
grupo, el tercer punto se elije evaluando la distancia de los puntos al grupo, y en particular, al punto
más cercano que ya pertenece al grupo. Se miden las distancias y se toma el punto más cercano:
El argumento “color_threshold” nos permite establecer un corte para establecer los grupos. Si le
asignamos el valor 3, estaremos haciendo un corte gráfico en el eje vertical, y obtendríamos los dos
grupos que se han formado por debajo de tres, o sea:
En el mismo sentido, si el corte lo realizamos en 1,5, obtendríamos una mayor cantidad de grupos
(5 grupos), pues los cinco grupos anteriores se ramifican en varios más. En definitiva, el corte y la
construcción de clases dependen del criterio de distancia que usemos y del corte que realicemos (en
este último sentido es similar al modelo logit). En definitiva, este parámetro representa la distancia
mínima al más cercano a partir de la cual ya se considera que un punto es parte del grupo.
Un Dendograma jerárquico es un árbol que ubica cada punto de acuerdo a su cercanía, y lo ubica
de acuerdo a una jerarquía. Entonces, se establece una cantidad determinada de distancias que
permite crear grupos grandes, y dentro de estos se aplican otras distancias para crear otros grupos, y
dentro de estos se repite el procedimiento. El resultado final es algo parecido a lo siguiente, donde
se agruparon los puntos generados en la sección anterior.
Este criterio sirve cuando queremos hacer hincapié en la separación antes que en la cohesión. En
el código debemos asignar el valor “complete” al argumento “method”. Siempre se comienza
uniendo los puntos más cercanos entre sí, pero luego de unir dos puntos, es decir, de haber creado
el primer grupo, el tercer punto se elije evaluando la distancia de los puntos al grupo, y en
particular, al punto más alejado que ya pertenece al grupo. Se miden las distancias y se toma el
punto más cercano:
El argumento “color_threshold” nos permite establecer un corte para establecer los grupos. Si le
asignamos el valor 3, estaremos haciendo un corte gráfico en el eje vertical, y obtendríamos los
cinco grupos que se han formado por debajo de tres, o sea:
En el mismo sentido, si el corte lo realizamos en 1,5, obtendríamos una mayor cantidad de grupos,
pues los cinco grupos anteriores se ramifican en varios más. En definitiva, este parámetro
representa la distancia mínima al más lejano a partir de la cual ya se considera que un punto es
parte del grupo
En definitiva, el corte y la construcción de clases dependen del criterio de distancia que usemos y
del corte que realicemos (en este último sentido es similar al modelo logit).
Un Dendograma jerárquico es un árbol que ubica cada punto de acuerdo a su cercanía, y lo ubica
de acuerdo a una jerarquía. Entonces, se establece una cantidad determinada de distancias que
permite crear grupos grandes, y dentro de estos se aplican otras distancias para crear otros grupos, y
dentro de estos se repite el procedimiento. El resultado final es algo parecido a lo siguiente, donde
se agruparon los puntos generados en la sección anterior.
Este criterio utiliza como medida de distancia la varianza de las distancias euclideas, es decir,
habiendo calculado las distancias, se las normaliza (se toma la diferencia respecto de la media y al
resultado se lo divide por la varianza). En el código debemos asignar el valor “ward” al argumento
“method”. Luego de obtener las distancias normalizadas, se unen los puntos de acuerdo a la
cercanía entre sí, aplicando cualquiera de los criterios (máximo o mínimo), pues da igual. Esta
indiferencia entre elegir entre el punto más cerca y lejano se aprecia en la siguiente imagen:
Aquí se puede ver cómo el punto 3 está más cerca del grupo de la izquierda según distancia
absoluta, tanto para el punto más cerca como para el más lejano. Pero, según el criterio de la
varianza, será más parecido al grupo de la derecha, donde los puntos están más dispersos entre sí.
En este caso, el valor asignado al argumento “color_threshold” tiene que ver con los puntos de
desvío, y no a la distancia absoluta como en los dos métodos anteriores. Por ende, este parámetro
representa la cantidad de varianzas mínimas necesarias para incorporar una observación a un grupo.
La “dendrogram” nos sirve para graficar, “linkage” sirve para agrupar, y con “fcluster” obtenemos
las etiquetas de cada grupo una vez definido el corte, es decir, con esta última sub librería logramos
establecer un corte para definir la cantidad de grupos con quienes trabajaremos (se define la
distancia mínima hacia el punto más cercano a partir de la cual se considera que una observación es
parte de un grupo). Veamos las líneas de código utilizando sólo las últimas dos sub librerías (la
primera línea de código es quien crea los puntos/base de datos):
La variable que llamamos “clusters” tiene asignado un array donde cada elemento es la etiqueta
que se corresponde con cada punto generado en la primera línea de código.
Ahora graficaremos este resultado en un diagrama de dispersión, distinguiendo cada grupo a partir
de los colores de los puntos:
Clusterización – Aplicación real
Armaremos grupos de acciones norteamericanas utilizando cuatro medidas. Para obtener los datos
utilizaremos la siguiente FMP API (Juan Pablo –profesor- sostiene que es muy buena, pero al
utilizarse de manera gratuita sólo podremos realizar 200 requests como máximo):
https://fmpcloud.io/
La información que se puede encontrar es la siguiente:
ETFs (424).
Commodities (28).
Equity Europa: Euronext (1248).
Equity Nyse (4380).
Equity Amex (274).
Equity Nasdaq (3825).
Equity Canadá: TSX (1413).
Indices (56).
Fondos Mutuales (1504).
Nosotros buscaremos renta variable con capitalización superior a los 10 mil millones de USD:
Esta API nos permite conseguir datos de papeles de exchanges de diferentes países:
Es por esto que filtraremos la base de datos para quedarnos sólo con los 75 primeros papeles del
NASDAQ, y nos quedaremos sólo con las columnas que se pueden leer en el código:
Las columnas que no son el index y el ticket son los features, las características con quienes
construiremos los grupos, son las dimensiones. Por lo tanto, cada fila es un punto, cuyo nombre es
el ticket, y se corresponde con cuatro dimensiones. Entonces, al aplicar la técnica de clusterización
estaremos respondiendo para cada empresa a ¿qué tal lejos está la empresa X de la empresa Y de
acuerdo a los valores que las cuatro dimensiones toman? Con la respuesta lograremos formar
grupos, pues se miden la distancia entre los puntos para cada una de las dimensiones.
Un asunto importante con las características elegidas es su escala. Como se puede apreciar en la
tabla, los valores/escalas difieren significativamente, por ende, de utilizar esta base de datos se
estaría dando mucha más importancia para clasificar a la capitalización antes que a la beta. Para
resolver esto sin cambiar el concepto de distancia (utilizamos la euclidea), estandarizaremos los
valores de las variables (a los valores le restamos la media y al resultado lo dividimos por la
varianza). Veamos:
Vemos que hay 19 grupos. Para analizar las características del agrupamiento, primero creemos los
labels y asignemoslos a la base de datos:
Veamos ahora un gráfico de barra que nos permita apreciar visualmente la frecuencia o cantidad de
empresas en cada grupo:
Para realizar el análisis prestemos atención principalmente a los grupos más grandes.
A estos grupos los graficaremos (recordar que los features están normalizados, por ende, son
puntos de desvío respecto de la media):
Ahora hagamos el análisis, pero cuantitativamente y sin escalar:
Entrenamiento no supervisado - Kmeans
Este método tiene la misma base que la clusterización, es decir, debemos parametrizar el criterio,
pero a diferencia del método anterior, en lugar de seleccionar la distancia mínima, aquí elegiremos
la cantidad de grupos que deseamos tener. En términos de cohesión y separación (los conceptos que
deben definirse con el criterio), al señalar la cantidad de grupos estamos definiendo una posición
sobre estos aspectos de forma indirecta.
Este método utiliza como criterio de creación de grupos a la técnica de centros (centroides),
alrededor de los cuales los datos se agrupan uniformemente (esféricamente si son puntos en 3D,
radialmente si son datos en 2D y linealmente si son datos unidimensionales, etc.). Para ejemplificar
el uso de la técnica, imaginemos que nuestra base de datos se compone de datos que, en un gráfico
de dispersión de dos dimensiones es igual al siguiente:
Lo que tenemos que hacer ahora es definir la cantidad de grupos que deseamos se construyan, a
partir de lo cual, el algoritmo ubica dos centroides (un punto) de forma aleatoria6 en el mapa de
dispersión. Habiendo hecho esto, lo siguiente es medir la distancia entre cada centroide y cada
observación, y con estas medidas realizará las agrupaciones, clasificando los puntos en los
centroides más cercanos. Las distancias pueden medirse de cualquiera de las formas que hemos
descripto antes (sección de clusterización), aquí como antes utilizaremos la distancia euclidea.
Habiendo terminado con esto, lo siguiente es etiquetar a cada observación según el grupo al que
pertenece:
6 En el método “Kmeans ++”, esta ubicación no es tan al azar, dice Juan Pablo (profesor).
Luego de esto, lo siguiente es reiniciar el proceso y volver a ubicar dos centroides en el mapa de
dispersión, tomar medidas y etiquetar las observaciones. Este proceso se repite N veces (el
algoritmo repite esto una cantidad determinada de veces por defecto, no obstante, podemos
indicarle cuántas repeticiones deseamos que realice). En esta segunda oportunidad, y en las
siguientes, para ubicar los centroides se tomará cada grupo por separado y se calculará el promedio
de los valores de cada dimensión (promedio del eje x y promedio del eje y para cada grupo),
obteniendo de este modo la coordenada de ubicación del centroide para la segunda ronda.
Por lo tanto, la clave de la técnica es la definición de la cantidad de grupos que deseamos se armen.
Es por esto que siempre deberemos definir diferentes cantidades de grupos y comparar los
diferentes resultados obtenidos.
Ejemplo de aplicación
Para ejemplificar el uso de la librería sklearn, donde se encuentra este método, primero crearemos
nuestra base de datos y la graficaremos:
Ahora aplicaremos el método Kmeans++. En la siguientes líneas “entrenaremos” al modelo
(antepenúltima línea) y también, haremos el etiquetado (penúltima línea). La última línea de código
es una variable que tiene guardadas las coordenadas de los centroides finales (característica de cada
grupo). Veamos:
Las labels o etiquetas están guardados en “y_means”, veamos los primeros 50:
Las coordenadas de los centroides son:
Este ejemplo permite ver claramente lo mencionado antes, la clave se encuentra en cómo definir la
cantidad de grupos (centroides). En este sentido, piense que al contar con dos dimensiones, o
incluso tres, un espacio de dispersión puede ser de gran ayuda, sin embargo, esta herramienta ya no
será accesible cuando tengamos cuatro o más dimensiones (características). Por lo tanto, definir la
cantidad de grupos (centroides) no es un asunto de sencilla resolución.
Este método mide distancias intra - grupos, es decir, define diferentes cantidades de grupos, y para
cada modelo (cada cantidad de grupo) calcula la distancia que cada miembro tiene con los demás
(hace esto para cada grupo), luego calcula la distancia promedio de cada grupo, y finalmente
calcula la distancia promedio del modelo. Por ende, a mayor cantidad de grupos, menor será la
distancia intra cluster
Es con esta gráfica que se decide la cantidad óptima de grupos. Para hacerlo se toma el punto a
partir del cual la pendiente de la recta trazada de izquierda a derecha cambia más bruscamente. En
la siguiente imagen se ejemplifica esto, vemos que 6 grupos es el punto a partir del cual la
pendiente tiene el mayor cambio:
Analíticamente debemos calcular las pendientes, sabemos que el cambio en la cantidad de grupos
siempre es uno, mientras que el cambio en las distancias se obtiene tomando la diferencia entre los
valores correspondientes de cada punto. Una vez calculadas las derivadas para cada punto,
calculamos el cociente entre ellas o tasa de cambio de la pendiente, quedándonos con la más
pequeña, pues este indicará el mayor cambio de pendiente y así el grupo óptimo.
Método de la silueta
Este método contempla tanto la cohesión como la separación, es decir, distancia entre grupos y
distancia intra grupo. En este método se elige la cantidad de grupos cuya ratio es la más alta.
Donde i es el centroide, y:
a: distancia promedio de i a todos los demás puntos en el mismo cluster.
b: distancia promedio de i a todos los demás puntos en el cluster más cercano.
Donde el valor de s(i) puede variar entre -1 y 1,
-1 si es un mal agrupamiento.
0 si es indiferente.
1 si es un buen agrupamiento.
Por lo tanto, el coeficiente de la silueta para cada modelo es:
𝑁
1
𝑆𝐶 = ∑ 𝑠(𝑖)
𝑁
𝑖=1
Este método tiene en cuenta los conceptos de cohesión y separación, pero también penaliza la
mayor cantidad de grupos. Para lograr esto realiza una combinación de métodos, su fórmula es casi
artesanal, en tanto que la penalización sobre la cantidad de grupos puede modificarse a criterio
propio. Su fórmula es un cociente, donde:
Silueta en el numerador (a mayor valor mejor).
Método del codo o distancia media intra clúster en el denominador (a menor distancia
mejor).
Numero de clústeres en el denominador (penalizando el sobre-ajuste). Podríamos elevar
esta cantidad a alguna potencia para modificar la penalización.
La cantidad óptima de grupos estará indicada por el valor más alto del índice, que este caso será
para seis agrupaciones.
Críticas al modelo Kmeans
El caso extremo que permite ilustrar la situación donde falla con seguridad, es el siguiente, veamos
la gráfica de dispersión:
Ahora, estos ejemplos son sencillos pues tenemos dos dimensiones ¿cómo podríamos estudiar los
casos con cuatro o más dimensiones? Juan Pablo (profesor) sugiere tomar cada dimensión y
estudiar su distribución de frecuencias. Cuando las distribuciones no son uniformes, este método no
es recomendable.
Entrenamiento no supervisado – Mezcla Gaussiana
Este método resuelve situaciones que el método Kmeans no, concretamente, es útil para resolver
casos donde la dispersión se alarga (última figura mostrada en la sección de crítica al Kmeans),
pero no resuelve para dispersiones como la medialuna. Como eje central también tiene la necesidad
de definir la cantidad de grupos, por ende, para encontrar su óptimo podemos utilizar cualquiera de
los métodos mencionados antes. Por otro lado, este método no utiliza el concepto de centroide, en
su lugar asume lo siguiente:
Los puntos no se distribuyen uniformemente en las dimensiones que tenga el set de datos.
Se distribuyen según la distribución normal.
Los clústeres pueden tomar formas elípticas siguiendo distribución normal para dimensión
X e Y (dist focales).
IMPORTANTE: La mayoría de los puntos están aglomerados en el centro del clúster, pero
permite outliers como en una distribución normal.
Cada clúster puede tener una matriz de covarianzas independiente (covariance_type=Full)
o bien compartir todos la misma matriz (covariance_type=tied) incluso puede generalizarse
a un kmeans usando covariance_type=spherical tratando a cada clúster con una varianza
única}. O sea, las varianzas de cada dimensión clúster no tienen porqué ser iguales.
Este método simula diferentes distribuciones de probabilidad para cada dimensión (característica),
pero siempre son distribuciones normales. Luego calcula matrices de varianza y covarianza
(correlaciones entre las diferentes dimensiones) y se selecciona aquella matriz con la mayor
correlación.
Veamos un ejemplo básico para entender cómo trabaja este método. La base de datos con dos
dimensiones es la siguiente, gráficamente:
Ahora importamos el modelo y lo aplicamos, indicando que deseamos darla la misma importancia
a la relación de todas las dimensiones, por ello, en el argumento “covariance_type” asignamos el
valor “full”.
Para evaluar estos modelos, y poder encontrar la cantidad óptima de grupos podemos utilizar el
método de la silueta, y también, utilizar los criterios de Akaike (CA) y el criterio Bayesiano (CB).
Con estos criterios, la cantidad óptima se define a partir del valor más pequeño del índice. En este
sentido, si el índice continua reduciendo su valor a medida que la cantidad de grupos aumenta,
entonces el modelo mezcla Gaussiana no funciona para agrupar. En resumidas cuentas, ambos
criterios permiten identificar la cantidad óptima de grupos y, si el método es adecuado para la base
de datos.
Veamos su aplicación en estos casos:
.
Las ventajas de este modelo se resumen en los siguientes puntos:
No requiere un número K de clústeres como input, lo determina solo (pero requiere una
parametrizacion de distancia mínima y cantidad de observaciones).
Permite clasificar o etiquetar como "ruido" a los puntos visitados que no entran en la
distancia mínima de ningún clúster.
Mientras que las desventajas son:
Cuando hay clústeres de densidad variable anda muy mal, porque la épsilon es un
parámetro único. En este sentido si este parámetro es:
o Grande genera muchos clúster en las zonas menos densas.
o Chico deja las zonas menos densas como "ruido".
Veamos todo esto con el gráfico de dispersión en forma de medialuna:
Vemos cómo a medida que modificamos el tamaño del entorno, la cantidad de grupos crece, al
igual que los puntos aislados (que no forman grupos por no contener la cantidad mínima de puntos
predefinida).
Entrenamiento no supervisado – MeanShift
Este es similar al Kmeans, no es igual porque la diferencia es que automatiza la selección de la
cantidad de grupos, sin embargo, en lugar de ello lo que elegimos es el “ancho de banda” o
distancia entre puntos intra clúster. Por ende, sus ventajas son:
No necesitas pasarle el número de clúster, lo calcula solo.
No hay riesgo de overfiting.
Y sus desventajas son:
Es muy lento.
Igual que Kmeans no sirve para nubes de puntos no uniformes y radiales.
No se coloca el ejemplo porque es un método similar al Kmeans, no obstante, su aplicación
(ejemplo sencillo) se encuentra en el archivo de clase número 16.
Ejemplo de aplicación
Lo que mostraremos a continuación es el problema a resolver y su resultado, es decir, no se copian
las líneas de código (que se pueden buscar en el archivo de clase número 16). El problema a
resolver es el siguiente: ¿Existen zonas de precios para las cuales el precio de la acción es más o
menos sensible? En otras palabras, se entiende que existen zonas de precios del petróleo para las
cuales la cotización de las diferentes petroleras son más y menos sensibles. Por lo tanto, el objetivo
es encontrar estas zonas y calcular dicha correlación. Para resolverlo se aplica algún método de
clasificación sobre las dos dimensiones y para cada compañía (precio materia prima y cotización de
la firma en la bolsa), luego, teniendo cada grupo, se calcula la correlación con una regresión
simple. El resultado para el caso de la firma XOM es el siguiente:
A esto se aplicaron los métodos de cantidad óptima de grupos y también, otros métodos de
clasificación como el de mezcla Gaussinaa.
Aunque no está escrito, el ejercicio quiere que calculemos un retorno logarítmico (neperiano claro)
ACLARACION: El valor absoluto del quantil 95 vs 5 significa conocer qué valores de variación
corresponden a esos cuantiles para después calcular su cociente. En este sentido, siempre es
conveniente que la ratio sea mayor a 1, pues esto significa que en general, los mejores días
presentan subas más altas que las bajas de los peores días. Entonces, si vamos largo en un activo,
esta es una característica deseada. En este sentido, si la distribución fuese normal, el ratio entre
estos cuantiles sería 1, pero en general, dicha distribución no es una normal.
El aspecto que escapa a esta ratio es que se desconoce hasta dónde llegan los máximos y los
mínimos. En otras palabras, no tenemos el área de la distribución, sólo un punto a partir del cual
está cada quantil. Esto es importante, porque para recuperarse de una pérdida diaria de 10% vamos
a necesitar una suba de más del 10%, pero, si usamos retornos logarítmicos, esto no será así, pues
las bajas se recuperarán con aumentos similares. CONCLUSION: para usar ratios de percentiles,
los retornos a utilizar deben ser logarítimicos.
En la carpeta de la UCEMA hay un archivo metodológico sobre estos ratios.
Se toma la volatilidad de las ruedas del último año (en promedio, un año tiene 250 ruedas)
Podemos hacerlo con el where()