Bienvenido al curso introductorio de Julia para Data Science¶

Objetivos¶

  • Comprender los conceptos y funcionalidades básicas del lenguaje Julia
  • Análisis y desarrollo de código para su posterior uso.

Índice¶

Inicio ▲

  1. Introducción
  2. Variables básicas
  3. Ciclos
  4. Funciones
  5. Diccionarios
  6. Paquetes

Introducción 🎯¶

Inicio ▲

Julia es un lenguaje de propósito general de código abierto,dinámico y de alto rendimiento basado en un compilador avanzado (JIT),que permite que el código se ejecute rápido como si fuera compilado.

Sin embargo,algunas de las caracteristicas mas interesantes de Julia para Ciencias e Ingeniería,son las siguientes:

  • Sintáxis Matemáticas: Lleva el lenguaje matemático al código de programación similar a como lo realizan software como Mathematica.

  • Buen desempeño: Tiene un rendimiento similar a lenguajes compilados como C.

  • Diseño paralelo: Esta desarrollado para la computación paralela y distribuida.

  • Entre otros...

La última versión descargable de Julia es la versión 1.8.1 y la puedes descargar desde el siguiente enlance Julia.Además puedes instalar algunos Entornos de desarrollo para utilizar Julia como VSCode o incluso puedes utilizar la Suite JupyterLab.Sin embargo en este curso utilizaremos la versión 1.7.3.

Variables básicas 🎹¶

Inicio ▲

Uno de los primeros puntos a considerar en lenguajes como MatLab,R,Python,entre otros,es la definición de variables.En el caso de Julia,podemos considerar que las variables básicas que podemos definir son variables numéricas y de cadena de texto.

In [9]:
x=10
typeof(x)
Out[9]:
Int64
In [10]:
y="Proyecto exitoso"
typeof(y)
Out[10]:
String

En este caso definimos la variable *x* que almacena el valor 10 y la variable y que almacena el texto "Proyecto exitoso".Luego verificamos el tipo de variable que es cada una,mediante la funcion typeof().

Sin embargo,tal vez una de las características mas importantes de Julia es la existencia de jerarquía en las variables numéricas.De esta forma, es posible transformar un valor entero en formatos de Float64,Float32 o Float16 correspondientes a números de doble,única o la mitad de la precisión.

In [21]:
Float64(2)
Out[21]:
2.0
In [22]:
Float32(2)
Out[22]:
2.0f0
In [23]:
Float16(2)
Out[23]:
Float16(2.0)

Si queremos realizar la operación inversa,transformar una variable float en integer,no siempre sera válida debido a la jerarquía en las variables.Es por esto que es preferible utilizar funciones como ceil(),floor() o round().

Si consideramos el siguiente caso

In [ ]:
Int64(2.4)

Nos lanzara un error,ya que no se puede transformar a valor entero el decimal 2.4.Para esto preferimos utilizar la siguientes funciones.

In [26]:
floor(Int64,2.4)
Out[26]:
2
In [27]:
ceil(Int64,2.4)
Out[27]:
3
In [28]:
round(Int64,2.4)
Out[28]:
2

En los 3 casos estamos transformando el mismo valor decimal,sin embargo la función determina si se redondea al valor mas cercano hacia abajo o hacia arriba.

Ciclos 🎢¶

Inicio ▲

Uno de los aspectos mas importantes en la mayoría de los lenguajes, son los ciclos o loops para el desarrollo de iteraciones en distintos contextos,ya sea recorrer los valores en una lista o matriz,iterar una función, imprimir mensajes por pantalla con alguna variable que cambia de valor,entre otros aspectos.

Uno de los primeros puntos que podemos ver es la sintáxis,la cual es muy similar a lenguajes como Matlab o R,en donde usamos la funcion println(),ya que nos permite imprimir cada valor de i en una nueva línea.

In [3]:
for i in 1:10
    println(i)
end
1
2
3
4
5
6
7
8
9
10

En caso contrario si usamos en Julia la funcion print() obtenemos la misma secuencia pero se imprimen todos los valores en la misma línea.

In [4]:
for i in 1:10
    print(i)
end
12345678910

Sin embargo, el ciclo for tambien puede recorrer otro tipo de estructuras de datos o variables.En algunos casos podriamos querer recorrer los valores en un vector previamente definido

In [6]:
for i in [10,20,30,40,50]
    println(i)
end
10
20
30
40
50

O incluso podriamos recorrer uno a uno los caracteres en una cadena de texto... cualquier parecido con la realidad es mera coincidencia. 👀

In [8]:
for i in "Lunes"
    println(i)
end
L
u
n
e
s

Afortunadamente no solo existe el ciclo for para realizar iteraciones.Sino que también podemos hacer uso del ciclo *while*,el cual tiene una sintáxis un poco mas breve,pero que sin embargo debemos tener algunos cuidados.

En este caso el ciclo while se ejecuta siempre que sea cierta la condición lógica,pero en el caso que la condición logica deje de ser verdadera se ejecuta el código que viene despues de la palabra reservada end

Una sintáxis clásica es el homólogo del ciclo for.

In [10]:
i = 1
while i <= 10
    println(i)
    i=i+1
end
1
2
3
4
5
6
7
8
9
10

En este caso a diferencia del ciclo for,primero tenemos que inicializar la variable i,para luego definir la condición lógica, en este caso mientras i sea menor o igual que 10 se debe imprimir las instrucciones que se encuentran en el bloque.Luego definimos 2 instrucciones,primero que se imprima el valor de i para luego redefinir el valor de i como i=i+1,asi de esta forma en cada iteración el valor de i va aumentando en 1.🪃

Uno de los principales cuidados que debemos tener al utilizar el ciclo While,es que mientras la condición lógica sea verdadera las instrucciones siempre se ejecutan,por tanto si la condición nunca deja de ser verdadera el ciclo se ejecuta de forma indefinida,ocasionando que en el peor de los casos el software se cierre de forma abrupta.Un caso común es el que se muestra en la sintáxis... 🔥

In [ ]:
i = 1
while i > 0
    println(i)
    i=i+1
end

Pero bueno,si necesitamos que la condición si sea la que ya definimos,podriamos hacer uso de dos comandos muy útiles,en este caso break y continue.

Si consideramos el último ciclo while,podriamos hacer que la variable *i* alcance un determinado valor el ciclo termine.

In [3]:
i = 1
while i > 0
    if i ==15
        break;
    end
    println(i)
    i=i+1
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Afortunadamente resulto bien la sintáxis jajaj... Bueno como pueden ver acá agregamos una condición,en este caso se verifiqua si el valor de *i* llegó a 15,si es el caso entonces el while termina,pero mientras no llegue a ese valor se puede seguir ejecutando.

Y tal vez uno de los favoritos para muchos son los ciclos anidados 🙄.En general cuando hablamos de ciclos anidados hacemos referencia a incluir un ciclo dentro de otro,algo típico que podriamos ver seria lo siguiente.

In [14]:
for i in 1:3
    for j=1:3
        println("i tiene el valor ",i," y j tiene el valor ",j)
    end
end
i tiene el valor 1 y j tiene el valor 1
i tiene el valor 1 y j tiene el valor 2
i tiene el valor 1 y j tiene el valor 3
i tiene el valor 2 y j tiene el valor 1
i tiene el valor 2 y j tiene el valor 2
i tiene el valor 2 y j tiene el valor 3
i tiene el valor 3 y j tiene el valor 1
i tiene el valor 3 y j tiene el valor 2
i tiene el valor 3 y j tiene el valor 3

Aca estamos usando 2 ciclos for.El primero de ellos recorre los valores que puede tomar la variable *i* (1,2,3) y luego el ciclo de adentro recorre los valores que puede tomar la variable *j.De esta forma cuando i* tiene el valor de 1,se recorren todos los valores de j y asi de forma sucesiva con los siguientes valores.

Haciendo uso de ciclos pero esta vez de forma separada,podriamos hacer algo un poco mas matemático.🤓

In [39]:
x=[0 0 0 0 0; 0 0 0 0 0; 0 0 0 0 0; 0 0 0 0 0;  0 0 0 0 0]

for i in 1:5
    x[i,i]=1
end
for i in 1:5
    x[i,-i+6]=1
end

x
Out[39]:
5×5 Matrix{Int64}:
 1  0  0  0  1
 0  1  0  1  0
 0  0  1  0  0
 0  1  0  1  0
 1  0  0  0  1

Un último importante y no menor,es algo que vimos de forma indirecta en uno de los ciclos anteriormente y corresponde a los condicionales y operadores lógicos.

Los *condicionales* sirven para ejecutar una instrucción siempre que se cumpla una condición.Sin embargo,podemos verificar n condiciones si fuera necesario las cuales las podemos realizar mediante operadores lógicos.

Operador Descripción
< Menor a
> Mayor a
< = Menor o igual a
>= Mayor o igual a
== Igual a
!= No es igual a
| Si al menos una es verdadera (o).
|| Si al menos la primera s verdadera
& Es Verdadero si todas lo son.
! Negación
any Es Verdadero si algun elemento de un arreglo booleano es verdadero.
all Es verdadero si se cumple que todos los elementos de un arreglo booleano son Verdaderos.
|| Menor a

A pesar de ser una tabla un poco extensa... estos operadores si que son muy útiles.🧭

Una caso bastante común es querer validar si un valor es positivo,negativo o igual a 0.Para esto utilizaremos una función que nos genere automáticamente números aleatorios.

In [60]:
A=randn()

println(A)

if A>0
    println("A es positivo")
elseif A==0
    println("A es cero")
else
    println("A es negativo")
end
1.0388530297170369
A es positivo

Así cada vez que se ejecute el bloque de código anterior,se genera un número nuevo y se valida cualquiera de las 3 condiciones,devolviendonos el mensaje correspondiente.

Funciones ⛩¶

Inicio ▲

Los bloques de código que hemos visto hasta el momento,representan definiciones de variables,iteración de una o mas instrucciones,entre otros aspectos.Sin embargo cuando hablamos de funciones,nos referimos a un conjunto de instrucciones pero que encapsulamos bajo un nombre,a la cual se le puede asignar una o mas variables de entrada y asi mismo podemos tener algunas salidas de esta misma.

Una de las funciones que ya hemos utilizado y que viene precargada en Julia es la función *println()*,de la cual incluso hemos visto algunos de los argumentos de esta.

In [3]:
mensaje=println("Hola mundo")
mensaje
Hola mundo

Otra función importante en Julia es *parse()*,la cual toma una cadena y si es posible la transforma en algún tipo de número,sin embargo si no es posible arroja un error.

In [8]:
conversion_1=parse(Int64,"44")
conversion_1
Out[8]:
3.1415

Tambien podriamos aplicar una función a una ya utilizada.En este caso convertimos la cadena de texto "3.1415" a numero,pero luego utilizamos la función *trunc()* para truncar el valor de $\pi.$

In [9]:
conversion_2=parse(Float64,"3.1415")
trunc(conversion_2)
Out[9]:
3.0

Pero vamos a lo interesante... ya que por mas funciones que vengan diseñadas,en muchos casos vamos a requerir de la creación de nuestras propias funciones.

De esta forma para crear una función,debemos especificar tanto su nombre como las instrucciones que queremos ejecutar.Asi podriamos definir una función que solo imprima un mensaje por pantalla cada vez que la llamemos.

In [11]:
function saludo()
    println("Hola a todos")
end

saludo()
Hola a todos

De esta forma debemos siempre respetar la estructura para el diseño de funciones.Esto implica que cada función siempre debe comenzar con la palabra reservada function seguida de el nombre de la función con una apertura y cierre de paréntesis.Luego debemos desarrollar el conjunto de instrucciones que buscamos que ejecute la función cada vez que la llamemos y finalmente debemos terminar con la palabra reservada end.

Sin embargo,una función tambien puede utilizar parámetros de entrada los cuales pueden modificar parcialmente o totalmente el conjunto de instrucciones que se definen dentro de ella.

Si tomamos la función saludo que definimos anteriormente,podriamos hacer que esta no solo salude de forma genérica sino que salude a una persona en particular,lo cual lo hara cuando le pasemos el nombre de la persona.

In [29]:
function saludo(nombre)
    println(string("Buen día"," ",nombre))
end

saludo("juan")
Buen día juan

Pero si profundizamos un poco en la función saludo,incluso podriamos agregar otros datos o información releevante de a quien debe saludar la función.Además del nombre,podriamos agregar la edad de la persona y el saldo que tiene en su tarjeta de transporte

In [41]:
function saludo(nombre,edad,saldo)
    println(string("Buen día"," ",nombre," su edad es de ",edad," y usted tiene un saldo de ",saldo," en su tarjeta"))
end

saludo("juan",25,100)
Buen día juan su edad es de 25 y usted tiene un saldo de 100 en su tarjeta

Como se ve pudimos editar la función,ahora la función saludo() acepta 3 parámetros de entrada los cuales corresponden al nombre,la edad y el saldo.Así tambien se edito el texto que imprime la función,ya que ahora hace uso de los 2 nuevos parámetros

Diccionarios 📊¶

Inicio ▲

Un tipo de estructura importante en Julia asi como en otros lenguajes como Python son los diccionarios.Estos tienen algunos elementos en común con las matrices,pero son de un uso mas general 📚.

Un diccionario contiene un conjunto de índices correspondientes a las *claves* y otro conjunto correspondiente a los *valores.De esta forma cada clave se asocia a un único valor,llamando a esta asociación par clave-valor*.

Tal vez de forma similar a lo que podemos hacer en Python con algunas librerias, en Julia podemos hacer uso de la función *Dict()* para crear un nuevo diccionario que inicialmente no guarde ningún par *clave-valor*.

In [2]:
diccionario=Dict()

diccionario
Out[2]:
Dict{Any, Any}()

Sin embargo si quisieramos agregar un par clave-valor,podríamos utilizar la siguiente sintáxis.

In [5]:
diccionario["primero"]=2

diccionario
Out[5]:
Dict{Any, Any} with 1 entry:
  "primero" => 2

Como se observa en la salida del diccionario,aca solo contamos con un único par clave-valor.En donde la llave es la cadena "primero" y el valor es 2

Pero si buscamos tener un diccionario con una mayor cantidad de pares,podemos hacer lo siguiente.

In [8]:
diccionario=Dict("primero"=>1,"segundo"=>2,"tercero"=>3,"cuarto"=>4,"quinto"=>5)

diccionario
Out[8]:
Dict{String, Int64} with 5 entries:
  "segundo" => 2
  "primero" => 1
  "cuarto"  => 4
  "quinto"  => 5
  "tercero" => 3

Como podemos ver,en este caso ya contamos con un diccionario que cuenta con 5 pares,en donde llaves son del tipo *"String"* y los valores son del tipo *"Int64"*.Sin embargo los pares no se muestran en el mismo orden en que realizamos su definición,lo cual es irrelevante en los diccionarios ya que las pares no se crean de forma correlativa y generalmente llamaremos a la llave para obtener el valor que deseamos.

En este contexto si contamos con diccionarios con una mayor longitud,en muchas ocasiones habran algunas caracteristicas que no son tan evidentes🧭 como en el diccionario que ya hemos definido 📕.

Para esto podemos utilizar algunas de las funciones que vienen precargadas en Julia... las cuales utilizaremos a continuación.

In [ ]:
Una de las primeras cosas que podemos hacer es buscar un valor en el diccionario por su llave
In [9]:
diccionario=Dict("primero"=>1,"segundo"=>2,"tercero"=>3,"cuarto"=>4,"quinto"=>5)

diccionario["quinto"]
Out[9]:
5

Como podemos ver, si buscamos el valor que esta asignado a la llave "quinto",obtenemos el valor que ya habiamos definido

In [ ]:
Sin embargo,en algunas ocasiones no sabremos exactamente que pares tenemos o los nombres de ellos.Para esto primero podemos verificar el largo de nuestro diccionario.
In [14]:
largo=length(diccionario)
println(largo)

llaves=keys(diccionario)
println(llaves)
5
["segundo", "primero", "cuarto", "quinto", "tercero"]

En este utilizamos 2 funciones para hacer un barrido a nuestro diccionario.Primero verificamos el largo de nuestro diccionario con la función length() y luego usamos la función keys() para verificar cuales son todas las llaves que podemos llamar... lo que podemos verificar rapidamente porque no es muy extenso este diccionario 📋.

Paquetes 📦¶

Inicio ▲

Uno de los aspectos principales en lenguajes como Python,R,Julia y otros.Es la posibilidad de extender las capacidades del software.Es asi que mediante la carga de paquetes externos en Julia,podemos realizar calculos con algebra simbolica, realizar graficos,resolver problemas de optimización,entre otras tareas.

Sin embargo en este primer curso introductorio de Julia abordaremos el desarrollo de cálculo con algebra simbolica y el desarrollo de gráficos con el paquete Plots

Paquete SymPy 🧪¶

Sin embargo,la primera librería que veremos es SymPy la cual nos permite declarar variables y funciones simbólicas,asi posteriormente podemos manipular estas expresiones y realizar la evaluación numérica.

In [5]:
using SymPy;
Out[5]:
(x, y, z)

Luego tenemos 3 formas de definir las variables simbólicas,con las cuales queremos trabajar.La primera seria utilizar la función Sym(),como se muestra a continuación.

In [ ]:
x,y,z=Sym("x,y,z")

Además conservando la misma sintáxis,podriamos utilizar la función symbols().

In [10]:
a,b,c=symbols("a,b,c")
Out[10]:
(a, b, c)

Y una tercera opción seria utilizar la definición mediante los comandos @vars o @syms,obteniendo en ambos casos el mismo resultado.

In [12]:
@vars a b c
@syms a b c
Out[12]:
(a, b, c)

De esta forma,podemos utilizar cualquiera de los métodos anteriores para definir nuestras variables simbolicas.

Sin embargo, la parte mas interesante para trabajar con algebra simbolica viene a continuación...🥁. Ya teniendo 2 set de variables,podemos de igual forma definir algunas ecuaciones para luego trabajar con ellas.

In [16]:
f=10*x+1*y+3*z
Out[16]:
$10 x + y + 3 z$
In [17]:
g=1*a-2*b+10*c
Out[17]:
$a - 2 b + 10 c$

Como podemos ver hemos definido 2 expresiones en donde por el momento,ninguna de nuestras variables tienen un valor asignado.Sin embargo,podriamos evaluar ambas expresiones para un conjunto de valores que nosotros definamos.

Si tomamos la primera expresión y le damos los valores x=1,y=2 y z=2.Podemos utilizar la función subs() para evaluar la expresión en los valores recien mencionados

In [18]:
f_num=subs(f,(x,1),(y,2),(z,2))
f_num
Out[18]:
$18$

De esta forma,al evaluar la expresión obtenemos el valor arrojado por pantalla.Pero si de todas formas queremos examinar otra respuesta de la expresión,podemos volver a evaluar.

In [19]:
f_num=subs(f,(x,10),(y,4),(z,8))
f_num
Out[19]:
$128$

En este caso,evaluamos la expresión f en los valores x=10,y=4 y z=8.

Sin embargo,tambien es posible realizar alguna operaciones mas complejas como el calculo de derivadas,integrales y límites.

De esta forma si definimos la siguiente funcion.

In [24]:
x=Sym("x")
fx=10*x^3+5*x^2+4*x
Out[24]:
$10 x^{3} + 5 x^{2} + 4 x$

La derivada de la función será:

$$\frac{\partial}{\partial x}{10x^3+5x^2+4x}$$

Luego el resultado seria el siguiente:

In [25]:
diff(fx)
Out[25]:
$30 x^{2} + 10 x + 4$
In [ ]:
o tambien podriamos determinal la integral indefinida
In [26]:
integrate(fx,x)
Out[26]:
$\frac{5 x^{4}}{2} + \frac{5 x^{3}}{3} + 2 x^{2}$

En cambio si quisieramos determinar la integral definida de la función anterior entre 0 y 1,tendriamos lo siguiente:

$$\int_{0}^1 {10x^3+5x^2+4x}$$

y al resolverlo el resultado seria el siguiente

In [27]:
integrate(fx,(x,0,1))
Out[27]:
$\frac{37}{6}$

Paquete Plots 📊¶

Dejando un poco de lado la matemática un poco mas dura de la sección anterior...📎 Sigamos con un tema un poco distinto,incluso tal vez un poco mas práctico.Si bien el desarrollo de visualizaciones efectivas con los datos correctos requiere de horas de trabajo,en esta sección veremos algunos de los gráficos mas utilizados en Data Science.

Alguno de los gráficos que se pueden desarrollar con el paquete Plots y que veremos son los siguientes:

  • Gráficos de dispersión
  • Gráficos de líneas
  • Gráficos de barra
  • Histogramas
  • Diagramas de cajas

Gráficos de dispersión¶

Uno de los gráficos mas utilizados en Data Science es el de dispersión,ya que nos permite realizar un primer análisis exploratorio de las variables de interés.De acuerdo a lo anterior,veremos un primer gráfico utilizando la libreria Plots que debemos cargar antes de utilizar en el entorno de Julia.A continuación veremos la sintáxis para el desarrollo de este tipo de gráfico.

In [ ]:
using Plots

data = rand(10)
f = Plots.scatter(data)

Gráfico de líneas¶

Siguiendo el orden la lista,el segundo de ellos que veremos será el gráfico de líneas.Este tipo de gráfico generalmente lo usamos para visualizar un conjunto de pares ordenados,en donde posteriormente podemos trazar rectas que unan cada uno de estos pares,la sintáxis siguiente muestra como podemos desarrollar un gráfico de este tipo.

In [ ]:
using Plots

data = rand(10)
f = plot(data)

Gráfico de barras¶

Un segundo gráfico que es muy utilizado tanto en Data Science como en otras áreas ,son los gráficos de barra.Este tipo de gráfico nos permite desarrollar una visualización en donde trabajamos con 2 variables.En donde nuestra variable *x* corresponde a las categorías que queremos representar y la variable *y* a la magnitud o cantidades de cada una de estas categorías.

In [ ]:
using Plots

data = rand(10)
f = Plots.bar(data)
f

Histogramas¶

A diferencia de los gráficos anteriores,el histograma es un tipo de visualización que nos permite visualizar las frecuencias acumuladas en una muestra.Si bien hay algunos conceptos y tópicos de mucho intéres que podemos analizar y verificar en un histograma,por ahora solo nos dedicaremos al desarrollo de este tipo de gráfico.Además en este caso,agregamos el parámetro *nbins* el cual permite cambiar la cantidad de columnas que tiene el histograma,generalmente se prefiere el uso de un término impar ya que permite analizar visualmente la asimetría o curtosis,entre otras medidas.

In [ ]:
using Plots

data = rand(100)
f = histogram(data,nbins=9)
f

Diagramas de cajas¶

Y hemos llegado al final de esta sección... 😁.El último gráfico que veremos sera el diagrama de cajas,el cual tiene algunas similitudes con el histograma pero sin duda que nos muestran aspectos distintos de una muestra.Las similitudes que comparten principalmente son las medidas estadísticas que podemos ver en ellos como la media,mediana,entre otros.

Sin embargo el diagrama de cajas es mucho más explícito para mostrar algunos estadísticos relevantes.

A continuación vemos la sintáxis del diagrama de cajas pero con el paquete StatsPlots y además haciendo uso del paquete Random,para la generación de números aleatorios,luego analizamos algunos de los estadísticos que observamos en la imagen.

In [ ]:
using StatsPlots, Random

n = 30
calculo = rand(1:10, n)
estadistica = rand(1:10, n)

b=boxplot([calculo estadistica], label=["Calculo" "Estadística"])
b

En la imagen observamos 2 gráficos de cajas,en donde se muestra la distribución de las notas de los cursos de Cálculo y Estadística.En ellos se observa que la distribución de las notas de Cálculo es mayor que la de estadística,ya que la *"caja"* es mas grande.Además la mediana correspondiente a la línea que se encuentra dentro de ambas cajas,en el caso de Cálculo es mayor.

Y llegamos al final de este curso introductorio con Julia para Data Science.Este curso presenta una guía práctica de como empezar a trabajar con Julia desde un punto de vista funcional,hay algunos conceptos que no se han considerado en este primer curso pero que en cursos posteriores profundizaremos mucho más como Algoritmos,desarrollo de visualizaciones e incluso Modelos de Machine Learning.

Ante cualquier duda o comentario me puedes escribir a juan.venegas@uach.cl o j.venegasgutierrez@gmail.com

Saludos 🌅