6  Cargando datos

Autor/a

Iraitz Montalbán

Ya conocemos las bases de Julia y ahora toca empezar a ver cómo hacer uso de los recursos más comunes en ciencia de datos. Dado que en el ejemplo anterior ya hemos creado un fichero con datos del Titanic, veamos si podemos cargarlo.

6.1 CSV

Quizás uno de los formatos más comunes en el mundo empresarial es el fichero de texto con extensión CSV (Comma Separated Value). Extraído habitualmente de ficheros Excel o bien de exportaciones de sistemas gestores de base de datos, veremos que presenta un formato tabular donde cada celda viene separado por un carácter (siendo , o ; los más habituales); y el salto de línea marca el inicio de una nueva fila.

Dado que leer y trabajar con estos ficheros puede ser tedioso, recurriremos a uno de las primeras librerías que nos serán de ayuda en nuestra tarea.Dos en realidad, CSV que sabe como tratar estos ficheros y DataFrames que nos ofrece todas las funcionalidades de las estructuras tabulares.

using Pkg;
Pkg.add("CSV")
Pkg.add("DataFrames")
using CSV, DataFrames

df = CSV.read(joinpath(data_path,"titanic.csv"), DataFrame)
first(df)
DataFrameRow (12 columns)
Row PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
Int64 Int64 Int64 String String7 Float64? Int64 Int64 String31 Float64 String15? String1?
1 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.25 missing S

Podemos extender este uso sobre la carga de datos tabulares a otros formatos comunes como Excel gracias a librerías como XLSX.jl

6.2 JSON

Otro formato habitual, particularmente cuando trabajamos contra APIs es JSON. Necesitaremos entender las estructuras de estos documentos de cara a darles el formato tabular adecuado.

readlines(joinpath(data_path,"titanic.json"))
20946-element Vector{String}:
 "["
 "  {"
 "    \"pclass\": 1,"
 "    \"survived\": 1,"
 "    \"name\": \"Allen, Miss. Elisabeth Walton\","
 "    \"sex\": \"female\","
 "    \"age\": 29,"
 "    \"sibsp\": 0,"
 "    \"parch\": 0,"
 "    \"ticket\": 24160,"
 ⋮
 "    \"ticket\": 315082,"
 "    \"fare\": 7.875,"
 "    \"cabin\": \"\","
 "    \"embarked\": \"S\","
 "    \"boat\": \"\","
 "    \"body\": \"\","
 "    \"home.dest\": \"\""
 "  }"
 "]"

Podemos ver que en este caso disponemos de una lista de objetos con un único nivel de datos que podemos trasladar directamente a un DataFrame.

using Pkg;
Pkg.add("JSON")
using JSON, DataFrames

# Leemos las lineas y las juntamos en una cadena única
data = join(readlines(joinpath(data_path,"titanic.json")))

df = DataFrame(JSON.parse(data))
first(df)
DataFrameRow (14 columns)
Row sex age parch boat name home.dest cabin fare embarked body sibsp pclass survived ticket
String Any Int64 Any String String String Any String Any Int64 Int64 Int64 Any
1 female 29.0 0 2 Allen, Miss. Elisabeth Walton St Louis, MO B5 211.338 S 0 1 1 24160

Es importante prestar atención a la estructura del JSON ya que si dispone de múltiples anidamientos y objetos complejos será difícil disponer de una estructura tabular plana directamente sin necesitar trabajo de preprocesado y aplanado previo.

6.3 Parquet

Si hay un formato omnipresente desde que fue creado allá por 2013 es Parquet. Formato columnar y comprimido por defecto, permite trabajar con estructuras de datos tabulares de forma eficiente para los procesos analíticos, con una huella de almacenamiento mínima y compatible con multitud de las herramientas de stack de ciencia de datos. De hecho, es la base para estructuras de almacenamiento más complejas que ofrecen capacidades ACID como Iceberg o Delta Lake.

using Pkg;
Pkg.add("Parquet")

Podemos cargar el contenido directamente como DataFrame gracias a la compatibilidad entre estas dos librerías.

using Parquet, DataFrames

df = DataFrame(read_parquet(joinpath(data_path,"titanic.parquet")))
first(df)
DataFrameRow (7 columns)
Row Survived Pclass Sex Age SibSp Parch Fare
Bool? String? String? Float64? Int64? Int64? Float64?
1 false 3 male 22.0 1 0 7.25

Existen otras dos grandes fuentes con las que será necesario interactuar para formar nuestro elenco de opciones.

6.4 APIs

Interactuar con APIs requiere realizar consultas siguiendo el protocolo HTTP y obtener los datos de respuesta en un formato compatible. En nuestro caso, al haber visto como el formato JSON es manejable gracias a la librería de Julia, simplemente deberemos añadir librerías de manejo del protocolo en concreto.

using Pkg;
Pkg.add("HTTP")
using HTTP
using JSON

# Dirección para el echo de postman
url="https://postman-echo.com/get?foo1=bar1&foo2=bar2"

respuesta = HTTP.get(url)
texto_respuesta = String(respuesta.body)
JSON.parse(texto_respuesta)["args"]
Dict{String, Any} with 2 entries:
  "foo1" => "bar1"
  "foo2" => "bar2"

Podéis probar con otras APIs donde poder cargar los datos directamente en un DataFrame. Tomemos como ejemplo la PokeAPI

using HTTP
using JSON

# Dirección para el echo de postman
url="https://pokeapi.co/api/v2/pokemon?limit=10"

respuesta = HTTP.get(url)
texto_respuesta = String(respuesta.body)

df = DataFrame(JSON.parse(texto_respuesta)["results"])
first(df, 5)
5×2 DataFrame
Row name url
String String
1 bulbasaur https://pokeapi.co/api/v2/pokemon/1/
2 ivysaur https://pokeapi.co/api/v2/pokemon/2/
3 venusaur https://pokeapi.co/api/v2/pokemon/3/
4 charmander https://pokeapi.co/api/v2/pokemon/4/
5 charmeleon https://pokeapi.co/api/v2/pokemon/5/

6.5 Sistemas gestores de Base de Datos

Los sistemas gestores de base de datos son un recurso habitual donde deberemos de encontrar el modo de poder enviar nuestras consultas (empleando SQL) y obtener una estructura que encaje en nuestros DataFrames.

Para la mayoría de casos deberemos encontrar la librería compatible con nuestra base de datos dado que la especificación de cada una puede variar y requeriremos cierta librería en cada caso.

Y más que podéis encontrar en https://juliapackages.com/c/database.

using Pkg;
Pkg.add("DuckDB")
using DuckDB

con_duckdb = DBInterface.connect(DuckDB.DB, "bd.duckdb")
DuckDB.DB("bd.duckdb")

Una vez conectados podemos leer de los datos del entorno.

# Creamos una serie de datos aleatorios
len = 10_000
datos = (a = collect(1:len), b = rand(1:100, len))

# Create la tabla
create_query = "
CREATE TABLE IF NOT EXISTS data(
  a INT NOT NULL,
  b INT NOT NULL
);"
DBInterface.execute(con_duckdb, create_query);

Insertamos los datos de prueba.

Tip

Con @time podemos ver el tiempo que le toma realizar la tarea en cuestión.

# Escribimos los datos
str = join(repeat('?', length(datos)), ',')
write_query = DBInterface.prepare(con_duckdb, "INSERT INTO data VALUES($str)")
@time DBInterface.executemany(write_query, datos)
  2.228166 seconds (185.07 k allocations: 7.969 MiB, 2.24% compilation time)

Y podemos proceder a leer parte de estos.

@time table_rd = DBInterface.execute(con_duckdb, "SELECT * FROM data")
  0.000574 seconds (18 allocations: 640 bytes)
(a = Int32[1, 2, 3, 4, 5, 6, 7, 8, 9, 10  …  9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999, 10000], b = Int32[29, 92, 34, 59, 61, 23, 15, 74, 27, 88  …  21, 4, 38, 13, 33, 90, 79, 49, 19, 34])

La lectura puede realizarse directamente formando el DataFrame resultante a utilizar ya que las bases de datos relacionales siempre nos retornan estructuras tabulares.

df = DataFrame(DBInterface.execute(con_duckdb, "SELECT * FROM data"))
first(df, 5)
5×2 DataFrame
Row a b
Int32 Int32
1 1 29
2 2 92
3 3 34
4 4 59
5 5 61

Siempre deberemos de acordarnos de cerrar la conexión una vez que hayamos obtenido los datos de la fuente.

DBInterface.close!(con_duckdb)