8  Análisis exploratorio

Autor/a

Iraitz Montalbán

Tras haber trabajado con nuestros datos originales, ya disponemos de unos datos limpios, sin datos faltantes y con cierta carga informativa.

using Parquet
using DataFrames
using CategoricalArrays

df = read_parquet(joinpath(data_path,"titanic.parquet")) |> DataFrames.DataFrame

# Primero, reemplazamos los valores numéricos por los nombres de nivel deseados
df[!, :Pclass] = replace(df[!, :Pclass], "1" => "Primera", "2" => "Segunda", "3" => "Tercera")

# Luego, convertimos la columna a categórica con los niveles y orden deseado
levels = ["Tercera", "Segunda", "Primera"]
df[!, :Pclass] = categorical(df[!, :Pclass]; levels, ordered=true)

levels = ["male", "female"]
df[!, :Sex] = categorical(df[!, :Sex]; levels, ordered=false)

describe(df)
7×7 DataFrame
Row variable mean min median max nmissing eltype
Symbol Union… Any Union… Any Int64 Union
1 Survived 0.383838 false 0.0 true 0 Union{Missing, Bool}
2 Pclass Tercera Primera 0 Union{Missing, CategoricalValue{String, UInt32}}
3 Sex male female 0 Union{Missing, CategoricalValue{String, UInt32}}
4 Age 29.6991 0.42 29.6991 80.0 0 Union{Missing, Float64}
5 SibSp 0.523008 0 0.0 8 0 Union{Missing, Int64}
6 Parch 0.381594 0 0.0 6 0 Union{Missing, Int64}
7 Fare 32.2042 0.0 14.4542 512.329 0 Union{Missing, Float64}

Ahora procederemos a explorar las distribuciones de datos y estadísticas de las categorías pertinentes.

8.1 Balance de muestras

Necesitamos evaluar si existen categoría mayoritarias en nuestro conjunto de datos. StatsBase nos ofrece la función countmap que directamente nos dará las ocurrencias de cada opción en las columnas como clave y valor de un diccionario.

import Pkg

Pkg.add("StatsBase")
using StatsBase

countmap(df[:,:Survived])
Dict{Union{Missing, Bool}, Int64} with 2 entries:
  0 => 549
  1 => 342

Y con StatsPlots podremos visualizar un gráfico de barras de cara a comparar los valores.

using StatsPlots

# Conteo de ocurrencias
conteo = countmap(df[:,:Survived])

# Convertir a porcentajes
total = sum(values(conteo))
porcentajes = Dict(k => v/total*100 for (k,v) in conteo)

# Etiquetas según los valores de Survived
etiquetas = ["No sobrevivió", "Sobrevivió"]
# Ordenar los porcentajes según etiquetas (asumiendo false = No sobrevivió, true = Sobrevivió)
valores = [get(porcentajes, false, 0.0), get(porcentajes, true, 0.0)]

# Graficar
bar(
    etiquetas,
    valores,
    legend=false,
    ylabel="Porcentaje (%)",
    xlabel="Survived", 
    title="Porcentaje de supervivientes"
)

Vemos que un porcentaje superior de pasajeros (61%) no sobrevivió al Titanic.

8.2 Distribución de datos

Podemos querer analizar si estas dos poblaciones muestran características significativas entre sí. Algo que permita distinguir a los dos conjuntos de datos de forma significativa.

8.2.1 Numérica (edad)

@df df violin(
    :Survived,
    :Age,
    legend=false,
    ylabel="Edad",
    xlabel="Supervivencia",
    title="Distribución de edad según supervivencia",
    xticks=([false, true], ["No sobrevivió", "Sobrevivió"])
)

Se observa que en el espectro más bajo de edad el conjunto de viajeros que sobrevivió es algo superior.

8.2.2 Categórica (sexo)

using StatsPlots

# Filtrar por supervivientes y no supervivientes
df_no = filter(:Survived => x -> x == false, df)
df_si = filter(:Survived => x -> x == true, df)

# Conteo por sexo en cada grupo
conteo_no = countmap(df_no[:, :Sex])
conteo_si = countmap(df_si[:, :Sex])

# Etiquetas y valores
etiquetas_sexo = ["Hombre", "Mujer"]
valores_no = [get(conteo_no, "male", 0), get(conteo_no, "female", 0)]
valores_si = [get(conteo_si, "male", 0), get(conteo_si, "female", 0)]

# Dibujar dos gráficas en paralelo
plot(
    bar(etiquetas_sexo, valores_no, title="No sobrevivió", legend=false, ylabel="Cantidad", xlabel="Sexo"),
    bar(etiquetas_sexo, valores_si, title="Sobrevivió", legend=false, ylabel="Cantidad", xlabel="Sexo"),
    layout=(1,2)
)

Vaya, aquí si que vemos dos extremos importantes. De los que sobrevivieron existe un desbalance positivo con respecto a las mujeres frente al caso de no superviviencia, donde la población de hombres fue mucho más significativa.

Posiblemente si añadieramos un eje adicional con respecto a la clase, veríamos cómo esta última característica afecta significativamente a la probabilidad de supervivencia.

8.2.3 De dispersión (sexo y clase)

using DataFrames, StatsPlots

# Agrupar por clase y sexo, calcular tasa de supervivencia
df_group = combine(groupby(df, [:Pclass, :Sex]), :Survived => mean => :SurvivalRate)

# Convertir Sex a string para etiquetas
df_group.Sex = string.(df_group.Sex)

# Crear etiquetas combinadas para el eje x
df_group.label = string.(df_group.Pclass, " - ", df_group.Sex)

# Graficar barras agrupadas
@df df_group bar(
    :label,
    :SurvivalRate,
    group = :Sex,
    ylabel = "Tasa de supervivencia",
    legend = :topright,
    title = "Tasa de supervivencia por clase y sexo",
    bar_position = :dodge,
    ylim = (0,1),
    xrotation = 45
)
Warning: Keyword argument bar_position not supported with Plots.GRBackend().  Choose from: annotationcolor, annotationfontfamily, annotationfontsize, annotationhalign, annotationrotation, annotations, annotationvalign, arrow, aspect_ratio, axis, background_color, background_color_inside, background_color_outside, background_color_subplot, bar_width, bins, bottom_margin, camera, clims, color_palette, colorbar, colorbar_entry, colorbar_scale, colorbar_title, colorbar_titlefont, colorbar_titlefontcolor, colorbar_titlefontrotation, colorbar_titlefontsize, connections, contour_labels, discrete_values, fill, fill_z, fillalpha, fillcolor, fillrange, fillstyle, flip, fontfamily, fontfamily_subplot, foreground_color, foreground_color_axis, foreground_color_border, foreground_color_grid, foreground_color_subplot, foreground_color_text, formatter, framestyle, grid, gridalpha, gridlinewidth, gridstyle, group, guide, guidefont, guidefontcolor, guidefontfamily, guidefonthalign, guidefontrotation, guidefontsize, guidefontvalign, html_output_format, inset_subplots, label, layout, left_margin, legend_background_color, legend_column, legend_font, legend_font_color, legend_font_family, legend_font_halign, legend_font_pointsize, legend_font_rotation, legend_font_valign, legend_foreground_color, legend_position, legend_title, legend_title_font_color, legend_title_font_family, legend_title_font_pointsize, legend_title_font_rotation, legend_title_font_valigm, levels, lims, line, line_z, linealpha, linecolor, linestyle, linewidth, link, margin, marker_z, markeralpha, markercolor, markershape, markersize, markerstrokealpha, markerstrokecolor, markerstrokewidth, minorgrid, minorgridalpha, minorgridlinewidth, minorgridstyle, minorticks, mirror, normalize, orientation, overwrite_figure, permute, plot_title, plot_titlefontcolor, plot_titlefontfamily, plot_titlefontrotation, plot_titlefontsize, plot_titlelocation, plot_titlevspan, polar, primary, projection, quiver, ribbon, right_margin, rotation, scale, series_annotations, seriesalpha, seriescolor, seriestype, show, show_empty_bins, showaxis, size, smooth, subplot, subplot_index, thickness_scaling, tick_direction, tickfontcolor, tickfontfamily, tickfonthalign, tickfontrotation, tickfontsize, tickfontvalign, ticks, title, titlefontcolor, titlefontfamily, titlefonthalign, titlefontrotation, titlefontsize, titlefontvalign, top_margin, unitformat, weights, widen, window_title, x, xdiscrete_values, xerror, xflip, xforeground_color_axis, xforeground_color_border, xforeground_color_grid, xforeground_color_text, xformatter, xgrid, xgridalpha, xgridlinewidth, xgridstyle, xguide, xguidefontcolor, xguidefontfamily, xguidefonthalign, xguidefontrotation, xguidefontsize, xguidefontvalign, xlims, xlink, xminorgrid, xminorgridalpha, xminorgridlinewidth, xminorgridstyle, xminorticks, xmirror, xrotation, xscale, xshowaxis, xtick_direction, xtickfontcolor, xtickfontfamily, xtickfonthalign, xtickfontrotation, xtickfontsize, xtickfontvalign, xticks, xunitformat, xwiden, y, ydiscrete_values, yerror, yflip, yforeground_color_axis, yforeground_color_border, yforeground_color_grid, yforeground_color_text, yformatter, ygrid, ygridalpha, ygridlinewidth, ygridstyle, yguide, yguidefontcolor, yguidefontfamily, yguidefonthalign, yguidefontrotation, yguidefontsize, yguidefontvalign, ylims, ylink, yminorgrid, yminorgridalpha, yminorgridlinewidth, yminorgridstyle, yminorticks, ymirror, yrotation, yscale, yshowaxis, ytick_direction, ytickfontcolor, ytickfontfamily, ytickfonthalign, ytickfontrotation, ytickfontsize, ytickfontvalign, yticks, yunitformat, ywiden, z, z_order, zdiscrete_values, zerror, zflip, zforeground_color_axis, zforeground_color_border, zforeground_color_grid, zforeground_color_text, zformatter, zgrid, zgridalpha, zgridlinewidth, zgridstyle, zguide, zguidefontcolor, zguidefontfamily, zguidefonthalign, zguidefontrotation, zguidefontsize, zguidefontvalign, zlims, zlink, zminorgrid, zminorgridalpha, zminorgridlinewidth, zminorgridstyle, zminorticks, zmirror, zrotation, zscale, zshowaxis, ztick_direction, ztickfontcolor, ztickfontfamily, ztickfonthalign, ztickfontrotation, ztickfontsize, ztickfontvalign, zticks, zunitformat, zwiden
@ Plots ~/.julia/packages/Plots/zgMHY/src/args.jl:1563

Vemos que la edad es también un factor a tener en cuenta y el precio del billete que de algún modo también indica la clase o ventajas abordo.

En este caso debemos ver si las distribuciones de ambas columnas muestra simetría o están volcadas a uno de los ejes (skewness).

plot(
    density(df.Age),
    density(df.Fare),
    layout=(1,2)
)

Como podemos ver, el precio del billete muestra una asimetría significativa que dificultará un análisis visual de los datos.

using DataFrames, StatsPlots

# Agrupar por clase y sexo, calcular tasa de supervivencia
df_group = combine(groupby(df, [:Fare, :Age]), :Survived => mean => :SurvivalRate)

# Convertir Sex a string para el eje y
df_group.Age = string.(df_group.Age)

# Graficar: eje x = Fare, eje y = Age, tamaño/color = tasa de supervivencia
@df df_group scatter(
    :Fare,
    :Age,
    markersize = 15,
    zcolor = :SurvivalRate,
    xlabel = "Precio",
    ylabel = "Age",
    title = "Tasa de supervivencia por precio del billete y edad",
    legend = :right,
    cbar_title = "Tasa de supervivencia"
)

Podemos compensar este echo mediante una transformación de la distribución empleando un logaritmo que reduzca la distancia de los valores más anómalos contra el centro de la distribución.

plot(
    histogram(df.Fare),
    histogram(log.(df.Fare)),
    layout=(1,2),
    xlabel = ["Precio del billete" "Log(Precio del billete)"]
)

@df df_group scatter(
    log.(:Fare),
    :Age,
    markersize = 15,
    zcolor = :SurvivalRate,
    xlabel = "Precio",
    ylabel = "Age",
    title = "Tasa de supervivencia por precio del billete y edad",
    legend = :right,
    cbar_title = "Tasa de supervivencia"
)

Aunque no del todo claro, ahora podemos observar mejor una mayor concentración de tasas elevadas de superviviencia para los billetes más caros.

8.3 Correlaciones

Aún no encontramos un ecosistema cerrado como es el caso de Scikit-learn en Python o Tidyverse en R. En este caso, para mostrar correlaciones entre apres recurriremos a la librería PairPlots y su dependencia actual con respecto a Makie.

import Pkg

Pkg.add("PairPlots")
Pkg.add("CairoMakie")
using CairoMakie
using PairPlots

# Selecciona solo columnas numéricas relevantes
cols = [:Age, :Fare, :SibSp, :Parch, :Survived]
df_num = dropmissing(df[:, cols])

pairplot(
    df_num => (
        PairPlots.HexBin(colormap=Makie.cgrad([:transparent, :black])),
        PairPlots.Scatter(filtersigma=2, color=:black),
        PairPlots.Contour(color=:black),
        PairPlots.MarginHist(),
        PairPlots.MarginQuantileText(),
        # New:
        PairPlots.MarginQuantileLines(),
    ),
    fullgrid=true
)

Resulta difícil ver las discrepancias así, de modo que separaremos nuestro conjunto en los dos grupos objeto de estudio.

# Por categoría
df_num_survived = filter(:Survived => x -> x == true, df_num)
df_num_didntsurvive = filter(:Survived => x -> x == false, df_num)

pairplot(
    PairPlots.Series(
        df_num_survived, bottomleft=true, topright=false),
    PairPlots.Series(
        df_num_didntsurvive, bottomleft=false, topright=true)
)

continuará