Julia para la ciencia de datos.

Josué Acevedo Maldonado
12 min readJul 1, 2023

¿Por qué debería dejar de usar python para la ciencia de datos?

La ciencia de datos es un campo interdisciplinario que combina técnicas, herramientas y teorías de diversas disciplinas como la estadística, las matemáticas y la informática. Su objetivo principal es extraer conocimiento y comprensión a partir de conjuntos de datos, ya sean estructurados o no estructurados.

La ciencia de datos involucra el proceso de recopilar, limpiar, analizar y visualizar datos con el fin de descubrir patrones, tendencias y relaciones. Esto se logra a través de una combinación de técnicas como el aprendizaje automático (machine learning), la minería de datos, la estadística, la visualización de datos y la programación.

“La ciencia de datos involucra el proceso de recopilar, limpiar, analizar y visualizar datos con el fin de descubrir patrones, tendencias y relaciones.”

Ciencia de datos tomado de https://datascientest.com/es/data-science-definicion-problematica-y-casos-de-uso

Los científicos de datos utilizan diversas herramientas y lenguajes de programación para construir modelos predictivos, los cuales pueden utilizarse para tomar decisiones informadas, realizar pronósticos, identificar oportunidades comerciales, optimizar procesos y resolver problemas complejos en una amplia gama de campos, como la medicina, las finanzas, la industria, el marketing y muchas otras áreas.

La ciencia de datos ha ganado una gran importancia en los últimos años debido al crecimiento exponencial en la cantidad de datos generados y disponibles de diversas fuentes, como redes sociales, sensores y dispositivos conectados a Internet. Su aplicación puede ayudar a las organizaciones a obtener información valiosa, tomar decisiones basadas en datos y obtener una ventaja competitiva en el mundo actual impulsado por la información.

Casos de estudio

Proyecto celeste.

El equipo de investigación del proyecto Celeste procesó 178 terabytes de datos de imágenes astronómicas en solo 14,6 minutos, lo que resultó en el catálogo más preciso conformado por 188 millones de objetos astronómicos provenientes del Sloan Digital Sky Survey.

Celeste se une a la lista de aplicaciones que superan el rendimiento de 1 petaflop por segundo, y es la primera en hacerlo en un lenguaje dinámico de alto nivel.

Proyecto RecSys.jl

Es un motor de recomendaciones de peliculas de aproximadamente 500 millones de parametros. Se puede ejecutar en Julia y Spark.

El modelo distribuido de Julia es cerca de 2 veces mas rapido que el de Spark.

AOCS

El Subsistema de Control de Altitud y Órbita (AOCS) del satélite brasileño Amazonia-1 está escrito 100% en Julia por Ronan Arraes Jardim Chagas

CliMa

La Climate Modeling Alliance (CliMa) utiliza principalmente Julia para ejecutar modelos del clima tanto en GPU como en CPU . CliMA está utilizando avances recientes en ciencia computacional para desarrollar un modelo del sistema terrestre que puede predecir sequías, olas de calor y precipitaciones con una precisión y velocidad sin precedentes.

Pfizer

Se han acelerado un 175x la ejecucion de los modelos de farmacología de Pfizer usando Julia en combinacion de una GPU.

Por que Julia?

Para nuevos usuarios en ciencia de datos

  • Julia es un nuevo enfoque tanto para la programación como para la ciencia de datos. Todo lo que se hace en Python o en R, puede hacerse en Julia con la ventaja de poder escribir código legible, rápido y potente.

Para usuarios experimentados

  • Julia esta pensado para ser rapido, genera codigo optimizado para la maquina de forma automatica, evita el tener que emplear cosas como python + fortran.

Por ejemplo:

julia> inner(x) = x + 3
inner (generic function with 1 method)

julia> outer(x) = inner(2 * x)
outer (generic function with 1 method)

julia> outer(3)

dadas este par de funciones, lo normal seria pensar que el programa necesita hacer muchas cosas:

  1. calcular2 * 3
  2. pasar el resultado de 2 * 3a la funcion inner
  3. calcular3 + el valor pasado a la funcion

Sin embargo, si le pedimos a Julia el código optimizado a través de la macro@code_typed, vemos qué instrucciones procesa realmente la computadora:


julia> @code_llvm debuginfo=:none outer(3)

; Function Attrs: uwtable
define i64 @julia_outer_98(i64 signext %0) #0 {
top:
%1 = shl i64 %0, 1
%2 = add i64 %1, 3
ret i64 %2
}

Este es el código LLVM de bajo nivel generado que muestra que el programa solo hace lo siguiente:

  1. desplaza la entrada (en este ejemplo es un 3) un bit a la izquierda, lo que tiene el mismo efecto que multiplicar por 2; y
  2. suma 3 al resultado anterior.
  • Julia permite el uso de distintos paradigmas de programacion (funcional, estructurado, orientado a objetos “simulado”) sin casi pérdida de rendimiento.
  • Julia pretende terminar con el problema de los dos lenguajes.
El problema de los dos lenguajes tomado de https://www.intel.com/content/dam/www/public/us/en/documents/presentation/julia-in-parallel-and-high-performance-computing.pdf

Características de Julia

Julia ( Bezanson et al., 2017 ) es un lenguaje relativamente nuevo, lanzado por primera vez en 2012, y pretende ser fácil y rápido . “Funciona como C pero se lee como Python”. Fue hecho para computación científica, capaz de manejar grandes cantidades de datos y computación sin dejar de ser bastante fácil de manipular y crear prototipos de código .

Stefan Karpinski estudiaba en la Universidad de California cuando tuvo que hacer frente a un problema: el desarrollo de una herramienta de simulación de redes que requería usar varios lenguajes de programación porque ninguno servía para realizar el 100% de la tarea encomendada. En ese momento dedidió unir esfuerzos con su compañero de universidad Viral Shah, y con Jeff Bezanson y Alan Edelman del MIT para diseñar un nuevo lenguaje que fuera compatible con casi cualquier tarea. Así, tras dos años de desarrollo interno, la primera versión del lenguaje de programación Julia se lanzó el 14 de febrero de 2012.

“Funciona como C pero se lee como Python”

  • Compilación JIT: Escribe código que parece interpretado pero se ejecuta tan eficientemente como código compilado, eliminando la necesidad de vectorizar para obtener mejor rendimiento.
Comparativa de Julia con otros lenguajes
  • Tipado opcional: Prototipa rápidamente con máxima flexibilidad y luego optimiza para obtener mejor rendimiento.
1::Int # Entero de 64 bits en Julia
1.0::Float64 # Flotante de 64 bits (NaN, -Inf, Inf)
true::Bool # Booleano ("true", "false")
'λ'::Char # Carácter, permite Unicode
"LᴬTₑX"::String # Cadenas de texto, permite Unicode
  • Sintaxis matemática: Amplía y supera a lenguajes matemáticos tradicionales como Fortran, Matlab y Mathematica.
#using Pkg
#Pkg.add("SymPy")
using SymPy
x,y,z = symbols("x,y,z", real=true)g = 3*y+z
f = 2*x^2
ff = f(g)

g = 3y + z

f = 2x²

ff = 2(3y + z)²

  • Propósito general: Accede a una amplia gama de funcionalidades desde el gestor de paquetes, lo que permite realizar tareas como leer diversas bases de datos, visualizar datos y ejecutar servidores HTTP.
  • Permite la programación en paralelo y distribuida.
  • Es capaz de ejecutar código escrito en otros lenguajes como C, Fortran, Python, R, Matlab, entre otros.

Introduccion a Julia

Uso

Julia incluye una terminal interactiva, llamada REPL en donde se puede visualizar automáticamente los resultados de la ejecución del programa o segmento de código.

Neomatrix@DESKTOP-5FE1RU6 C:\Users\Neomatrix$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.9.0 (2023-05-07)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |

julia>

La consola interactiva presenta varios modos de uso, por ejemplo al teclear ; la consola de julia pasa a ser una shell de comandos de linux:

shell> echo hola                                                                                                                                      
hola

Se puede ejecutar código de Julia en una sesión interactiva o guardado en un archivo con la extensión .jl y ejecutarlo desde la línea de comandos con la siguiente instrucción:

$ julia <nombre_del_archivo>.jl

Arrays

Los arrays son mutables y se pasan por referencia.

[1, 2] # vector (concatenation)
[1 2 3; 1 2 3] # 2x3 matrix (hvcat function)
[[1 2] 3] # concatenate along rows (hcat)
[[1; 2]; 3] # concatenate along columns (vcat)
[1 2]::Array{Int64, 2} # 2 dimensional array of Int64
[true; false]::Vector{Bool} # vector of Bool
[1 2; 3 4]::Matrix{Int64} # matrix of Int64
[x * y for x in 1:2, y in 1:3] # a comprehension generating 2x3 array
Float64[x^2 for x in 1:4] # casting comprehension result element type to Float64

Diccionarios

Diccionarios clave-valor.

x = Dict{Float64, Int64}() # an empty dictionary mapping floats to integers 
y = Dict("a"=>1, "b"=>2) # a filled dictionary
y["a"] # element retrieval
y["c"] # error
y["c"] = 3 # added element
haskey(y, "b") # check if y contains key "b"
keys(y), values(y) # tuple of collections returning keys and values in y
delete!(y, "b") # delete a key from a collection, see also: pop!
get(y, "c", "default") # return y["c"] or "default" if not haskey(y,"c")

If … else

x = 42; y = 2;
if x<1
print("$x < 1") # $x string interpolation.
elseif x < 3
print("$x < 3")
elseif x < 100
print("$x < 100")
else
print("$x is really big!")
end

if 1 < 3
if 3 < 4
print("eureka!")
end
end

if 1 < 3 & 3 < 4
print("eureka!")
end

x < y ? "es menor" : "es mayor"

Ciclos

La forma más básica de estructurar los cálculos es usando un bucle for.

x=0
for k in 1:100000
x = x + k^2
end

persons = ["Alice", "Bob", "Carla", "Daniel"]

for person in persons
println("Hello $person, welcome to techytok!")
end

for i in 1:3, j in 1:3
print("i=", i, " j=", j, "\n")
end

i=0

while(i<30)
println(i)
i += 1
end

Funciones

Las funciones son los principales bloques de construcción en Julia. Toda operación sobre variables y otros elementos se realiza mediante funciones.

function plus_two(x)
#perform some operations
return x + 2
end

plus_two(x) = x+2 #One-line functions (up to 92 characters)

function add_multiply(x, y)
addition = x + y
multiplication = x * y
return addition, multiplication
end

return_1, return_2 = add_multiply(1, 2)
all_returns = add_multiply(1, 2)


function logarithm(x::Real; base::Real=2.7182818284590)::Real
return log(base, x)
end

logarithm(10)
logarithm(10; base=2)


plus_two = x -> x+2 #Anonymous Functions
function1 = (x, y, z) -> x^y + z


map(x -> x+1, [1,2,3,4,5,7,9])
filter(x->isodd(x), [1,2,3,4,5,7,9])
reduce(*, [2, 3, 4])


(x -> x+2)(3)
(x -> x +1)((x -> x+2)(3))

((x, y, z) -> x^y + z)(2,3,1)
+(1,3,4,6)
(+(1,-(4, 1),*(2,2),/(12, 2)))

√3
∛64

Σ = +
Σ(1,2,3,4)



f(x::Float64, y::Float64) = 2x + y
f(x::Number, y::Number) = 3x + y
println("2(5.7) + 4.3 = ",f(5.7, 4.3))
println("3(3)+2 =",f(3,2))
println("3(4.3)+2 = ",f(4.3,2))


import Base.:+
+(x::String, y::String) = string(x, y)
+(x::String, y::Number) = parse(Int64, x) + y

"2" + 6 # esto lo llama al ponerlo entre los parametros, solo porque + es una funcion del nucleo o sistema
"2" + "6"


run(`python3 -c '
for i in range(1,4):
print("Desde python"),i
'`)

run(`ruby -e '
puts "hola RUBY"

arr = [1,2,5,"hola",9.43]
arr.each{|v| puts v}
'`)


meme de “mijito tu que estudiaste programacion …” version en Julia

Metaprogramación

El legado más fuerte de Lisp en el lenguaje Julia es su soporte de metaprogramación. Al igual que Lisp, Julia representa su propio código como una estructura de datos del propio lenguaje. Dado que el código está representado por objetos que se pueden crear y manipular desde dentro del lenguaje, es posible que un programa transforme y genere su propio código.

Cada programa de Julia comienza su vida como una cadena. El siguiente paso es parsear cada cadena a un objeto llamado expresión, representado por el tipoExpr.

Los objetos Exprcontienen dos partes:

  • SymbolUn símbolo es un identificador de cadena interno (una copia de cada valor de un string, que debe ser inmutable).
  • los argumentos de expresión, que pueden ser símbolos, otras expresiones o valores literales.
prog = "1 + 1"
ex1 = Meta.parse(prog)
typeof(ex1)
dump(ex1)
ex1.head #el simbolo
ex1.args #los argumentos
eval(ex1)


a=1;
b=2;
c=4;

expp = :(a+b+c)
println(expp, " = ", eval(expp))

for op = (:+, :*, :&, :|)
println(:(($op)(($op)($a,$b),$c))," = ",eval(:(($op)(($op)(a,b),c))))
end

Vectorization

Podemos vectorizar operaciones matemáticas como *(multiplicación) o +(suma) usando el operador punto.

[ 1 , 2 , 3 ] .+ 1

También funciona automáticamente con funciones.

plus_two = x -> x+2
plus_two.([ 1 , 2 , 3 ])

DataFrames.jl

DataFrames.jl proporciona un conjunto de herramientas para trabajar con datos tabulares en Julia. Su diseño y funcionalidad son similares a los de pandas (en Python), data.frame, data.table y dplyr( en R), lo que la convierte en una gran herramienta de ciencia de datos de propósito general.

El uso de dataframes permite manipular datos provenientes de archivos en disco o una base de datos, mediante querys similares a SQL los cuales emplean teoria de conjuntos.

Los archivos de valores separados por comas (CSV) son una forma muy popular y efectiva de almacenar tablas.

julia> ]
pkg> add CSV
pkg> add DataFrames

julia> using DataFrames
julia> using CSV

julia> df = DataFrame(Dict("age" => [15, 20, 25],
"name" => ["Rohit", "Rahul", "Akshat"]))

julia> function write_file_csv()
path = "data.csv"
CSV.write(path, df)
end

julia> write_file_csv()

julia> my_data= CSV.read("data.csv", DataFrame)
3×2 DataFrame
Row │ age name
│ Int64 String7
─────┼────────────────
1 │ 15 Rohit
2 │ 20 Rahul
3 │ 25 Akshat

Para recuperar un vector para name, podemos acceder a DataFramecon . , como si se tratara de un atributo de un objeto.

julia> my_data.name

Para cualquier fila , digamos la segunda fila, podemos usar el primer índice como indexación de fila :

my_data[ 2 , :]

También podemos obtener solo namespara las primeras 2 filas usando el slicing

my_data[ 1:2 , :]

tambien es posible seleccionar las columnas mediante nombre o indice

my_data[ 1:2 , 2]
my_data[ 1:2 , ["name"]]

Es posible aplicar un filter sobre un data frame empleando una funcion lambda.

filter(:name => n -> n == "Akshat" , my_data)

Existe una sintaxis aberviada, para realizar el filter

filter(:name => ==("Akshat"), my_data)
filter(:name => !=("Akshat"), my_data)

Las funciones pueden ser mas complejas, por ejemplo es posible seleccionar a las personas cuyos nombres comiencen con A o R y tengan una edad superior o igual a 20.

function complex_filter(name, age)::Bool
interesting_name = startswith(name, 'A') || startswith(name, 'R')
interesting_age = 20 <= age
interesting_name && interesting_age
end


filter([:name, :age] => complex_filter, my_data)

La subsetfunción se agregó para facilitar el trabajo con valores faltantes. A diferencia de filter, subsetfunciona en columnas completas en lugar de filas o valores individuales. Si queremos usar nuestras funciones definidas anteriormente, debemos envolverlo dentro ByRow.

df = DataFrame(Dict("salary" => [ 1_900, 2_800, 2_800, missing ],
"name" => ["John" , "Hank" , "Karen" , "Zed"]))

filter(:salary => >(2_000), df) #Falla por el valorfaltante (missing)

julia> subset(df, :salary => ByRow(>(2_000)); skipmissing=true)
2×2 DataFrame
Row │ name salary
│ String Int64?
─────┼────────────────
1 │ Hank 2800
2 │ Karen 2800

selectes mucho más versátil para seleccionar columnas que usando el indice o un array de nombres de columnas.

df2 = DataFrame(Dict(
"id" => [ 1, 2 ],
"q1" => [ 28 , 61 ],
"q2" => ["us", "fr"],
"q3" => [ "F" , "B" ],
"q4" => [ "B" , "C" ],
"q5" => [ "A" , "E" ]))

select(df2, "id", "q1", "q2")

Además, es posible usar expresiones regulares, para hacer la seleccdion de columnas.

select(df2, r"^q")

De igual forma que con el filter es posible excluir columnas.

select(df2, :q5, Not(:q5))

Uniones

Con dataframe es posible utilizar los tipos de union mas conocidos.

grades_2020 = DataFrame(Dict(
"name" => [ "Sally", "Bob", "Alice", "Hank" ],
"grade" => [ 1.0 , 5.0, 8.5, 4.0 ]))


grades_2021 = DataFrame(Dict(
"name" => [ "Sally", "Alex", "Hank" ],
"grade" => [ 9.5 , 9.5, 6.0 ]))
  1. innerjoin

Permite seleccionar todos los elementos presentes en ambos conjuntos de datos a unir.

innerjoin(grades_2020, grades_2021; on=:name)

2. outerjoin

El outerjoines mucho menos estricto que el innerjoiny solo toma cualquier fila que pueda encontrar que contenga un nombre en al menos uno de los conjuntos de datos.

outerjoin(grades_2020, grades_2021; on=:name)

3. crossjoin

Retorna el producto cartesiano de las filas , que es básicamente una multiplicación de filas, es decir, para cada fila crea una combinación con cualquier otra fila.

crossjoin(grades_2020, grades_2021; makeunique= true )

4. leftjoin

Devuelve todos los elementos del dataframe de la izquierda.

leftjoin(grades_2020, grades_2021; on=:name)

5. rightjoin

Devuelve todos los elementos del dataframe de la derecha.

rightjoin(grades_2020, grades_2021; on=:name)

6. semijoin

Devuelve solo los elementos del dataframe de la izquierda que están en ambos DataFrames.

semijoin(grades_2020, grades_2021; on=:name)

7. antijoin

Devuelve solo los elementos del dataframe de la izquierda que no están en el dataframe de la derecha.

antijoin(grades_2020, grades_2021; on=:name)

Josue Acevedo Maldonado is a software engineer, currently working as a consultant.

Connect in LinkedIn.

Thank you for being part of the community!
You can find related content on linktr.ee, besides the book Ensamblador X86.

Finally, if you have enjoyed this article and feel that you have learned something valuable, please share so others can learn from it as well.

Thanks for reading!

--

--

Josué Acevedo Maldonado

Amante de la tecnologia y con pasion en resolver problemas interesantes, consultor, y creador del canal de youtube NEOMATRIX. https://linktr.ee/neomatrix