Partimos de nuestros datos ya cargados, y deberemos ver la estructura base de nuestro dataframe. La función describe nos ofrece una primera revisión de los datos indicando nombre de las columnas, estadísticas de la distribución de datos, valores faltantes y typo de dato.
A esto podemos unirle conocer el tamaño de nuestro conjunto mediante el método size.
size(df)
(891, 12)
Deberemos entender la naturaleza de los datos que nos muestran y conocer, entre otras cuestiones:
Si son valores numéricos, que fenómeno representan y cómo se distribuyen
Si son valores categóricos, si las categorías vienen bien informadas y si muestran algún orden.
Esto nos obliga a explorar si son variables categóricas codificadas con números, si existen campos con una granularidad alta o con valor constante. Podemos pedir estadísticas concretas al método describe.
describe(df, :nunique)
12×2 DataFrame
Row
variable
nunique
Symbol
Union…
1
PassengerId
2
Survived
3
Pclass
4
Name
891
5
Sex
2
6
Age
7
SibSp
8
Parch
9
Ticket
681
10
Fare
11
Cabin
147
12
Embarked
3
7.1 Selección de campos
Podemos por tanto descartar aquellos casos donde el grano de detalle es prácticamente individual.
campos_a_descartar = [:Ticket, :Name]
2-element Vector{Symbol}:
:Ticket
:Name
Vemos que únicamente a contemplado los casos donde nos encontramos con tipos String. Pero sabemos, por ejemplo, que la columna Survived es una condición booleana de si sobrevivieron o no. Emplearemos la propia función de conversión de cada tipo, añadiéndole un . al final que denote por fila. En Julia, el punto (.) al final de una función o nombre de tipo (como string.) indica broadcasting, lo que aplicar una función elemento por elemento a lo largo de un array o colección, sin necesidad de escribir un bucle explícito.
Al igual que antes, empleamos el indicador ! ya que vamos a modificar la columna en su sitio. En Julia:
df[:, :Columna] devuelve una copia de la columna. Si modificas el resultado, el DataFrame original no cambia.
df[!, :Columna] devuelve una referencia a la columna original. Si modificas el resultado, el DataFrame original sí cambia.
El carácter final ; s simplemente para que no imprima el resultado por pantalla cuando se emplean Notebooks o scripts the Quarto .qmd.
Gracias a la librería Query.jl podemos emplear una sintaxis más compacta en nuestras consultas, añadiendo etapas de transformación como en los pipelines habituales en R o Python.
usingPkg;Pkg.add("Query")
usingDataFrames, Querycampos = [:Survived, :PassengerId, :Pclass]filas =nrow(df)for col in campos únicos = df |>@select({Symbol(col) = _.col}) |> collect |> x ->unique(df[!, col])println("Columna $col tiene $(length(únicos)) valores únicos en $(filas) filas.")println()end
Columna Survived tiene 2 valores únicos en 891 filas.
Columna PassengerId tiene 891 valores únicos en 891 filas.
Columna Pclass tiene 3 valores únicos en 891 filas.
Tenemos varios puntos en el código anterior:
@select es una macro para seleccionar columnas de un DataFrame. {Symbol(col) = _.col} crea un diccionario o mapeo donde la clave es el símbolo de la variable col y el valor es la columna correspondiente en el DataFrame. Symbol(col)en sí convierte el nombre de la columna (probablemente un string) en un símbolo, que es un tipo de dato común en Julia para referirse a nombres de columnas.
|> es el operador “pipe” en Julia, que pasa el resultado de la expresión anterior como argumento a la función siguiente.
collect toma un iterable (como el resultado de (select?)) y lo convierte en una colección concreta, como un array.
x -> unique(df[!, col]) es una función anónima (sin nombre designado como en el caso de las lambda) que toma x (aunque aquí no se usa directamente) y devuelve los valores únicos de la columna col del DataFrame df.
Hemos dado un salto en este bloque así que tomaros tiempo para entender cada parte. De aquí concluimos que la columna PassengerId no será muy relevante en el análisis estadístico ya que muestra un grano muy fino de valores que únicamente permiten identificar a cada individuo de nuestro conjunto. Pasará algo muy similar con valores de texto que indiquen nombre o ticket. Este dato ya lo obtuvimos anteriormente con lo que podemos determinar que las columnas de interés en adelante serán
Una de las mejores formes de conocer las distribuciones de datos es precisamente visualizando su histograma o gráfico de densidad. Deberéis instalar los paquetes de visualización pertinentes.
Pkg.add("StatsPlots")
usingStatsPlotshistogram(df.Age, normalize=true)
Vemos que si lo comparamos con una distribución normal, las gráficas difieren bastante. Esto suele requerir de evaluar la normalidad de la
El primer reto al que nos enfrentaremos será el de poder mostrar el diagrama de densidad, ya que no permite la existencia de valores faltantes. Y como podemos ver existen varios para la columna age. Deberemos aprender a filtrar en base a una condición.
7.2.1 Valores faltantes
En el caso de Julia missing es un tipo de datos especial que no indica el valor faltante en una celda.
typeof(missing)
Missing
Existen funciones específicas para eliminar o simplemente filtrar (filter) en base a la condición de dato faltante (ismissing). En este caso podremos ver los primeros tres elementos en nuestro dataframe donde se cumple la condición de dato faltante en el campo edad (:Age).
first(filter(:Age => ismissing, df ),3# mostramos los primeros tres elementos)
3×9 DataFrame
Row
Survived
Pclass
Sex
Age
SibSp
Parch
Fare
Cabin
Embarked
Bool
String
String7
Float64?
Int64
Int64
Float64
String15?
String1?
1
false
3
male
missing
0
0
8.4583
missing
Q
2
true
2
male
missing
0
0
13.0
missing
S
3
true
3
female
missing
0
0
7.225
missing
C
Y así podemos ver la densidad indicando nuestro dataframe modificado para que no cumpla esta condición. StatsPlots nos da la opción de explorar otras macros (ya vimos la macro de @time) que en este caso será el turno de @df. Esta macro, empleada con la sintaxis
@df<tabla><función de visualización>
nos permite definir una estructura tabular que luego será empleada en la función de visualización a la que solo deberemos indicarle en qué columnas centrarse.
@dffilter(:Age => !ismissing, df) density(:Age)
Como vemos, la función de densidad nos presenta un perímetro continuo sobre el histograma anterior. Podemos evaluar la normalidad de nuestra columna empleando tests estadísticos. La librería HypothesisTests.jl nos dará buenas referencias de cuales podemos usar.
Por simplificar de momento, viendo que la media y la mediana muestra valores parejos para el intervalo de datos, asumiremos dicha normalidad y veremos como imputar la media como valor estimado a los datos faltantes de nuestro dataframe.
usingStatisticsmean(skipmissing(df.Age))
29.69911764705882
Este valor lo reemplazaremos indicando que queremos transformar (alterando el objeto df)
Tras esto, con las dos columnas restantes podemos optar por eliminar estos datos. dropmissing eliminará las filas que contengan valores faltantes en sus celdas.
df_sinfilas =dropmissing(df)size(df_sinfilas)
(202, 9)
O mediante la función select podemos indicarle que no queremos las columnas con datos faltantes. Para esta macro deberemos usar otro paquete muy habitual para este tipo de labores.
WARNING: using DataFramesMeta.@select in module Notebook conflicts with an existing identifier.
7×7 DataFrame
Row
variable
mean
min
median
max
nmissing
eltype
Symbol
Union…
Any
Union…
Any
Int64
DataType
1
Survived
0.383838
false
0.0
true
0
Bool
2
Pclass
1
3
0
String
3
Sex
female
male
0
String7
4
Age
29.6991
0.42
29.6991
80.0
0
Float64
5
SibSp
0.523008
0
0.0
8
0
Int64
6
Parch
0.381594
0
0.0
6
0
Int64
7
Fare
32.2042
0.0
14.4542
512.329
0
Float64
7.3 Variables categóricas
Las variables categóricas son un tipo especial de variable que se utiliza para representar datos que pueden tomar un número limitado de valores distintos, llamados categorías o niveles.
En Julia, las variables categóricas se manejan principalmente usando el paquete CategoricalArrays.jl. Este paquete proporciona el tipo CategoricalArray, que permite almacenar datos categóricos de manera eficiente y realizar operaciones como ordenamiento, agrupamiento y comparación entre categorías.
En nuestro caso, la columna Pclass muestra categorías de prioridad de los pasajeros que encajarían dentro de la descripción de niveles.
Pkg.add("CategoricalArrays")
usingCategoricalArrays# Creamos una copia del dataframedf_cat =copy(df)# Primero, reemplazamos los valores numéricos por los nombres de nivel deseadosdf_cat[!, :Pclass] =replace(df_cat[!, :Pclass], "1"=>"Primera", "2"=>"Segunda", "3"=>"Tercera")# Luego, convertimos la columna a categórica con los niveles y orden deseadolevels = ["Tercera", "Segunda", "Primera"]df_cat[!, :Pclass] =categorical(df_cat[!, :Pclass]; levels, ordered=true)# Y comprobamos el resultadodescribe(df_cat)
7×7 DataFrame
Row
variable
mean
min
median
max
nmissing
eltype
Symbol
Union…
Any
Union…
Any
Int64
DataType
1
Survived
0.383838
false
0.0
true
0
Bool
2
Pclass
Tercera
Primera
0
CategoricalValue{String, UInt32}
3
Sex
female
male
0
String7
4
Age
29.6991
0.42
29.6991
80.0
0
Float64
5
SibSp
0.523008
0
0.0
8
0
Int64
6
Parch
0.381594
0
0.0
6
0
Int64
7
Fare
32.2042
0.0
14.4542
512.329
0
Float64
Gracias a ser categóricas ordenadas, podemos realizar comparativas como si de números se tratara pero empleando una sintaxis que nos aporta cierta claridad semántica. Si ordenamos de forma ascendente vemos que los viajeros en Tercera están en una categoría inferior a los de Primera.
first(sort(df_cat, :Pclass), 3)
3×7 DataFrame
Row
Survived
Pclass
Sex
Age
SibSp
Parch
Fare
Bool
Cat…
String7
Float64
Int64
Int64
Float64
1
false
Tercera
male
22.0
1
0
7.25
2
true
Tercera
female
26.0
0
0
7.925
3
false
Tercera
male
35.0
0
0
8.05
last(sort(df_cat, :Pclass), 3)
3×7 DataFrame
Row
Survived
Pclass
Sex
Age
SibSp
Parch
Fare
Bool
Cat…
String7
Float64
Int64
Int64
Float64
1
true
Primera
female
56.0
0
1
83.1583
2
true
Primera
female
19.0
0
0
30.0
3
true
Primera
male
26.0
0
0
30.0
Cosa que no podríamos hacer con el uso de cadenas de texto.
"Primera"<"Tercera"
true
Ya tenemos un conjunto de datos algo más limpio. Ahora toca estudiarlo en profundidad. Si queremos guardarlo para más adelante podemos volcar el DataFrame a fichero aunque si lo volcamos a un CSV o fichero de texto podemos perder la información categórica. De hecho, muchos formatos y entornos no serán compatibles con esta tipología de dato concreta.