Preparación¶

In [5]:
# Constants
HISTOGRAM_DATA = './data/houses_madrid.csv'
CHLOROPETH_DATA_MAP = './data/ne_110m_admin_0_countries/ne_110m_admin_0_countries.shp'
CHLOROPETH_DATA_DENSITY = './data/densityPopulation.csv'
CHLOROPETH_DATA_SAVED_MAP = './assets/chloropeth_map.html'
MARIMEKKO_DATA = './data/01 Presupuestos Generales del Estado Consolidados.xlsx'

1. Histograma¶

Historia y Origen¶

El histograma es una herramienta gráfica fundamental en la estadística, creada para representar la distribución de un conjunto de datos continuos o discretos agrupados en intervalos. Fue desarrollado por el estadístico británico Karl Pearson a fines del siglo XIX, en el contexto de la estadística descriptiva y el estudio de las distribuciones de frecuencia. Pearson introdujo el concepto de histograma como parte de sus investigaciones para visualizar datos en la estadística moderna, permitiendo observar la forma de la distribución de los datos en una representación clara.

A lo largo de los años, el histograma se ha convertido en una técnica estándar en múltiples campos, como las ciencias sociales, la economía y la ingeniería, gracias a su capacidad para mostrar la frecuencia con la que ocurren ciertos valores o intervalos de valores en un conjunto de datos. Esta representación permite a los analistas identificar patrones, tendencias y distribuciones de una manera visualmente accesible, como la simetría, la dispersión o la existencia de valores atípicos.

Pros y Contras¶

  • Pros:

    • Claridad en la distribución de datos: Permite visualizar cómo se distribuyen los datos a través de distintos intervalos, mostrando frecuencias y facilitando la interpretación de patrones.
    • Identificación de patrones de datos: Ideal para detectar tendencias generales y desviaciones de la normalidad en un conjunto de datos.
    • Adecuado para grandes volúmenes de datos: Puede manejar grandes cantidades de datos al agruparlos en intervalos, reduciendo la complejidad.
  • Contras:

    • Dependencia de la elección de intervalos: La forma del histograma puede variar según el número y el tamaño de los intervalos, lo que puede llevar a interpretaciones erróneas.
    • No muestra valores exactos: Representa intervalos de datos en lugar de valores específicos, lo cual puede ocultar detalles finos.
    • Dificultad con datos de diferentes escalas: Cuando los datos están en escalas muy distintas, la comparación en un histograma puede ser complicada sin normalización.

Tipos de Datos que Admite¶

  • Variables cuantitativas continuas: El histograma es especialmente útil para visualizar datos cuantitativos continuos agrupados en intervalos, mostrando la frecuencia de los valores en cada rango.

No obstante, admite otro tipo de datos modificando ligeramente su configuración, aunque para variables discretas o categóricas hay otro tipo de visualizaciones que pueden ser más recomendables como los diagramas de frecuencias.

Aplicaciones Típicas¶

  • Análisis de Distribución de una variable dada: Observar el valor de una variable continua, como el ejemplo utilizado de los precios de la vivienda en la comunidad de Madrid.
  • Control de Procesos en Ingeniería: Utilizado en el control estadístico de procesos para evaluar la variabilidad y el rendimiento de sistemas productivos.
  • Estudios Demográficos: Para analizar la distribución de poblaciones en función de alguna variable como la edad.
  • Finanzas y Economía: Aplicaciones similares al punto primero.
In [6]:
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np


# Set interval for visualization
INTERVAL = 50000

# Load data
df_histogram = pd.read_csv(HISTOGRAM_DATA, sep=',')

# Convert 'price' to numeric and remove NA values
df_histogram['price'] = pd.to_numeric(df_histogram['price'], errors='coerce')
df_cleaned = df_histogram.dropna(subset=['price'])

# Calculate IQR
Q1 = df_cleaned['price'].quantile(0.25)
Q3 = df_cleaned['price'].quantile(0.75)
IQR = Q3 - Q1

# Remove outliers
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df_histogram_no_outliers = df_cleaned[(df_cleaned['price'] >= lower_bound) & (df_cleaned['price'] <= upper_bound)]

# Define bin edges from 0 to the maximum value in intervals of 25,000
max_price = df_histogram_no_outliers['price'].max()
bins = np.arange(0, max_price + INTERVAL, INTERVAL)

# Show histogram with specified bins
plt.figure(figsize=(10, 6))
df_histogram_no_outliers['price'].plot(kind='hist', bins=bins, color='skyblue', edgecolor='black')
plt.title('Price Distribution without Outliers')
plt.xlabel('Price (€)')
plt.ylabel('Frequency')
plt.grid(axis='x', alpha=0.7)
plt.show()
No description has been provided for this image

Se ha realizado una limpieza de valores extremos, ya que la visualización quedaba distorsionada y no se podía apreciar la distribución. La limpieza de outliers es algo muy común cuando se trabaja con conjuntos de datos, ya que en muchas ocasiones, estos outliers suelen provocar problemas tanto a la hora de visualizar los datos como a la hora de trabajar con ellos para entrenar algoritmos o hacer algún modelo.

Se ha seguido el método del rango intercuartílico, que es un método muy común utilizado para este propósito y que consiste en establecer unos rangos inferior y superior, 1.5 veces el IQR, de manera que todos los valores que queden fuera de dichos márgenes se eliminan de la muestra.

Como el propósito de esta práctica es de visualización se han eliminado los datos directamente. Existen alternativas como sustituir los outliers por valores medios, inferir los valores en base al resto de datos del conjunto, por ejemplo, si un piso es muy caro para la zona en la que está se puede utilizar un modelo no supervisado para inferir el valor aproximado para ese registro del conjunto de datos.

Se ha ajustado a un intervalo que aumenta 50.000 euros por cada bloque. De esta manera podemos visualizar en líneas generales la distribución de precios, que como vemos se centra entre los 150.000 y los 250.000 euros para la mayoría de las viviendas. Esta información, no obstante, puede llevar a engaño sin un contexto mayor, pero nos permite entender la distribución de precios de una manera general.

2. Mapa Coroplético¶

Origen y Autoría¶

El mapa coroplético o de coropletas se originó a principios del siglo XIX, y se atribuye su primera conceptualización al geógrafo francés Charles Dupin en 1826. Este tipo de mapa temático se diseñó para representar variaciones de datos cuantitativos en regiones geográficas, utilizando distintos tonos o colores para ilustrar rangos de valores y facilitar la interpretación visual de patrones geoespaciales.

Pros y Contras¶

  • Pros:

    • Visualización efectiva de datos espaciales: Permite observar rápidamente patrones y tendencias geográficas, facilitando la interpretación de datos relacionados con la ubicación, como densidad de población o índices económicos.
    • Comparación visual clara: Los gradientes de color permiten comparar áreas geográficas y ver diferencias o similitudes de manera intuitiva.
    • Flexibilidad en representación de datos: Puede adaptarse para mostrar diferentes rangos de datos, desde porcentajes hasta datos categóricos.
  • Contras:

    • Percepción influida por el tamaño de las áreas: Áreas geográficas más grandes pueden crear una impresión exagerada de importancia visual, independientemente del valor representado.
    • Interpretación dependiente de la escala de colores: La elección de colores y rangos puede afectar la percepción, llevando a interpretaciones erróneas si no se eligen cuidadosamente.
    • Simplificación excesiva: Al promediar o agrupar datos en regiones, se pierde precisión en los datos locales y variaciones internas.

Tipos de Datos que Admite¶

  • Datos Continuos: Valores que varían dentro de un rango específico, como la densidad de población o el ingreso per cápita.
  • Datos Categóricos: También puede usarse para clasificar áreas en categorías específicas, como zonas de riesgo bajo, medio y alto, aunque esta aplicación es menos común que los datos continuos. Un ejemplo podría ser en las elecciones, representando el partido que ha ganado con un color sobre el mapa de la región.

Aplicaciones Típicas¶

  • Demografía: Representación de densidad de población, tasas de natalidad, niveles de educación, y otros indicadores demográficos a nivel regional o nacional.
  • Economía: Visualización de indicadores económicos como el PIB per cápita, tasa de desempleo, y distribución de ingresos en diferentes áreas geográficas.
  • Salud pública: Mapeo de la incidencia de enfermedades, tasas de vacunación, acceso a servicios de salud en diferentes regiones.
  • Ciencias ambientales: Representación de la calidad del aire, niveles de contaminación, disponibilidad de agua, y distribución de recursos naturales en distintas áreas.
In [7]:
import geopandas as gpd
import pandas as pd
import folium

# Load world booundaries data and density population data
shapefile_path = CHLOROPETH_DATA_MAP
world_boundaries = gpd.read_file(shapefile_path)
csv_path = CHLOROPETH_DATA_DENSITY
df_density = pd.read_csv(csv_path)

# This has been used to check column names and see what columns could be useful for this purpose.
# The same procedure has been made for density.
# for c in world.columns.to_list():
#     print(c)

# for c in df_density.columns.to_list():
#     print(c)

Vamos a utilizar las columnas NAME y CONTINENT del mapa con las fronteras de los distintos países para comparar con el mapa de densidad de población y, posteriormente, filtrar por países de Europa.

En cuanto al mapa de densidad vamos a utilizar las columnas name y Density.

Se hace un rename para juntar ambos dataframe en uno solo, de manera similar a como lo haríamos con una tabla SQL. La idea es usar un dataset unificado donde la referencia para la unión de ambos conjuntos sea el nombre del país.

Se ha modificado en el dataset original de densityPopulation.csv el nombre de República Checa (Czech Republic) por Czechia. También se ha modifica Bosnia y Herzegovina por Bosnia and Herz. Por otra parte, el país que aparece en negro es Kosovo. Se ha decidido no modificar nada relativo a Kosovo al tratarse de cuestiones políticas.

In [8]:
# Rename for merge both dataframes
world_boundaries = world_boundaries.rename(columns={'NAME': 'country'})
df_density = df_density.rename(columns={'name': 'country'})

# Filter by CONTINENT == Europe
europe = world_boundaries[world_boundaries['CONTINENT'] == 'Europe']

# Merge Europe with density using country
merged_europe = europe.merge(df_density, on='country', how='left')

# Initialize a Folium map centered on Europe
map = folium.Map(location=[54, 15], zoom_start=4)  # Centered on Europe

# Add the choropleth layer for European countries with red-blue gradient
folium.Choropleth(
    geo_data=merged_europe,      # GeoDataFrame for Europe with density data
    data=merged_europe,          # Data source for density values
    columns=['country', 'Density'],  # Columns to match: country and density
    key_on='feature.properties.country',  # Key to match GeoJSON feature name
    fill_color='YlOrRd',        # Color gradient from red (high) to blue (low)
    fill_opacity=1,
    line_opacity=1,
    legend_name='Density Population'
).add_to(map)

# Save and display
map.save(CHLOROPETH_DATA_SAVED_MAP)
map  # Display map if running in Jupyter Notebook or similar environment
Out[8]:
Make this Notebook Trusted to load map: File -> Trust Notebook

3. Gráfico Marimekko¶

Historia y Origen¶

El gráfico Marimekko, también conocido como gráfico de mosaico o Mekko, tiene sus raíces en la década de 1960 fue desarrollado por el economista y estadístico finlandés Arvo A. Aho al analizar la segmentación y cuotas de mercado. Se trata de una herramienta que permite mostrar la distribución de datos categóricos en dos dimensiones mediante el uso de barras rectangulares en ambas dimensiones, generando prismas, donde el ancho y alto de cada prisma corresponde a la proporción de datos que representa.

Fuente: https://www.jaspersoft.com/articles/what-is-a-marimekko-chart

Pros y Contras¶

  • Pros:

    • Proporciones comparativas claras: Muestra visualmente las proporciones dentro de cada categoría y las compara con otras categorías en un solo gráfico.
    • Datos con múltiples categorías: Útil para mostrar datos categóricos y proporcionales en una estructura de bloques, permitiendo captar de un vistazo las relaciones complejas.
    • Comprensión intuitiva: El área de los prismas permite hacernos una idea clara de qué estamos viendo y qué peso tiene cada categoría en el conjunto de datos.
  • Contras:

    • Dificultad con muchas categorías pequeñas: Si el gráfico incluye numerosas categorías con proporciones pequeñas, puede volverse difícil de leer e interpretar.
    • Limitaciones para leer valores exactos: Proporciona una buena visión de las proporciones generales, pero es menos eficaz para extraer valores numéricos exactos sin herramientas adicionales.
    • Espacio visual reducido para categorías menores: Las categorías pequeñas pueden no mostrarse claramente cuando la proporción es muy baja, dificultando la comparación precisa.

Tipos de Datos que Admite¶

  • Datos Categóricos basados en proporciones: datos en los que, a partir de las categorías, podemos extraer proporciones del total, permitiendo así la generación del gráfico acorde a lo que se busca representar.

Aplicaciones Típicas¶

Tiene un claro enfoque en el márketing y las ventas:

  • Análisis de Mercado: cuota de mercado relativa de diferentes empresas o productos en una industria específica.
  • Segmentación de Clientes: Comparar segmentos de clientes, como los grupos demográficos, para ver sus proporciones relativas dentro de un mercado.
  • Análisis de Ventas y Productos: Visualizar las ventas de productos en diferentes categorías o mercados y comparar la participación de cada producto o grupo.
  • Estrategia Empresarial: Permite evaluar la importancia relativa de cada línea de negocio o categoría en la planificación estratégica.
In [9]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Load and prepare data
file_path = MARIMEKKO_DATA
budgets_df = pd.read_excel(file_path, sheet_name='141', skiprows=5, nrows=28)
budgets_df = budgets_df.rename(columns={'Políticas': 'policy'})
budgets_df.columns = budgets_df.columns.astype(str)
budgets_df.head(5)
Out[9]:
policy 2014 2015 2016 2017 2018 2018-P 2019-P 2021 2022 2023
0 Justicia 1500.75404 1508.15445 1604.31179 1726.19093 1780.74441 1779.72937 1779.72937 2048.13086 2283.54940 2291.31848
1 Defensa 5654.45439 5711.68742 5734.29191 7575.59287 8400.56154 8400.56154 8400.56154 9072.01433 9790.81151 12316.82907
2 Seguridad ciudadana e Instituciones penitencia... 7880.95326 7843.12934 7903.61760 7912.33049 8418.13871 8418.08732 8418.08732 9694.41496 10148.79292 10719.20460
3 Política Exterior y de cooperación para el des... 1395.16909 1422.42613 1477.93985 1521.69850 1581.44350 1581.44350 1581.44350 1882.00770 2253.51982 2425.50604
4 Pensiones 127483.83335 131658.53137 135448.92579 139646.72308 144834.30566 144834.30566 144834.30566 163296.58073 171139.65331 190687.24615

Se van a seleccionar 4 políticas que suelen salir a debate y que, a excepción de las pensiones, que son el debate central de los últimos años, tienen un peso más similar en los presupuestos.

Además se han seleccionado los últimos 3 años para no dificultar la visualización y la comprensión de los datos, pues una de las limitaciones de las visualizaciones Marimekko es que si hay muchas categorías resulta difícil de interpretar.

In [10]:
# Selected policies and years
selected_policies = ['Sanidad', 'Defensa', 'Educación', 'Cultura']
selected_years = ['2021', '2022', '2023']

# Filter data
selected_budgets_df = budgets_df[budgets_df['policy'].isin(selected_policies)]
selected_budgets_df = selected_budgets_df[['policy'] + selected_years]

# Transform policy into an index and cast everything to numeric
selected_budgets_df = selected_budgets_df.set_index('policy')
selected_budgets_df.apply(pd.to_numeric)
selected_budgets_df
Out[10]:
2021 2022 2023
policy
Defensa 9072.01433 9790.81151 12316.82907
Sanidad 7329.68378 6606.04994 7049.08276
Educación 4893.45613 5022.78388 5354.97335
Cultura 1148.06383 1589.33219 1803.68982
In [11]:
'''
This code is based on the one I found at:
https://curbal.com/curbal-learning-portal/90-of-100-marimekko-chart-in-matplotlib
'''
# Calculate the total for each year
totals_per_year = selected_budgets_df.sum(axis=0)

# Calculate percentages
percentages = selected_budgets_df / totals_per_year * 100

# Width calculation
widths = totals_per_year / totals_per_year.sum() * 100

# Set colors
policy_colors = {
    'Defensa': 'orange',
    'Sanidad': 'green',
    'Educación': 'yellow',
    'Cultura': 'purple'
}

# Marimekko chart
fig, ax = plt.subplots(figsize=(10, 6))
x_start = np.zeros(len(selected_budgets_df))

# Add each policy's data for each year
for i, year in enumerate(selected_budgets_df.columns):
    for j, policy in enumerate(selected_budgets_df.index):
        ax.bar(
            x_start[j],
            percentages.loc[policy, year],
            width=widths[i],
            bottom=np.sum(percentages.iloc[:j, i]), 
            label=policy if i == 0 else "",
            color=policy_colors[policy],
            align='edge', # separate boxes
            edgecolor='black'
        )

    x_start += widths[i]

# Add labels and title
ax.set_xlabel('Year')
ax.set_ylabel('Percentage')
ax.set_title('Marimekko Chart - Budget Allocation by Policy (2021-2023)')

# Add years and their corresponding percentages to the X-axis
xticks = np.cumsum(widths) - widths / 2
xtick_labels = [
    f'{year} -> {totals_per_year[year] / totals_per_year.sum() * 100:.2f}%'\
        for year in selected_budgets_df.columns
]

ax.set_xticks(xticks)
ax.set_xticklabels(xtick_labels)

# Add legend
ax.legend(selected_budgets_df.index, title='Policy')

# Show the chart
plt.tight_layout()
plt.show()
/var/folders/_1/_rnshwnd0px1m82vsfm_3cg80000gn/T/ipykernel_37746/1885233174.py:32: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
  width=widths[i],
/var/folders/_1/_rnshwnd0px1m82vsfm_3cg80000gn/T/ipykernel_37746/1885233174.py:40: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
  x_start += widths[i]
No description has been provided for this image