Proyectos de datos para tu portfolio: 5 con datos abiertos

Proyectos de datos para tu portfolio: 5 con datos abiertos

Cinco proyectos de datos para estrenar tu portfolio con datos abiertos

Por:

Franco Brutti

·

Fecha de Actualización:

Para estrenar tu portfolio de datos te bastan cinco proyectos con datos abiertos oficiales: la inflación por grupos del INE, la compraventa de vivienda nueva y usada, el paro de España frente a la UE con Eurostat, el consumo eléctrico de Red Eléctrica y la contaminación por horas en Barcelona. Todos se descargan sin registro.

Los ejemplos que circulan por ahí suelen usar ventas de una tienda inventada o el mismo conjunto de Kaggle que han subido miles de personas. Funcionan para aprender, pero en una entrevista no dicen mucho. Un proyecto con datos oficiales, con su fecha y su licencia, demuestra algo más útil: que sabes encontrar datos, leer sus condiciones y sacar una conclusión que se puede comprobar.

Descargamos los cinco conjuntos el 6 de noviembre de 2026 y ejecutamos el primer paso de cada proyecto con Python 3.13 y pandas 3.1. Las salidas que verás son las reales, y las cifras salen solo de esos conjuntos y de esa fecha.

Qué hace que un proyecto de datos sirva en un portfolio

Un proyecto de datos sirve en un portfolio cuando responde una pregunta concreta, usa datos que otra persona puede descargar y comprobar, y termina en una conclusión que entiende alguien sin perfil técnico. Lo de menos es la herramienta. Un dashboard muy vistoso sin pregunta detrás enseña poco de cómo piensas.

Por eso cada proyecto de esta lista arranca con una pregunta, no con un gráfico. Si todavía estás ubicando qué hace un analista en el día a día, empieza por qué es un data analyst y qué skills necesita. Y si quieres el método completo, de la pregunta a la decisión, lo tienes en las cinco fases del análisis de datos.

Mi criterio para elegir estos cinco fue sencillo: datos que se actualizan solos (así el proyecto no caduca), una trampa real en cada conjunto (así tienes algo que contar) y un resultado que se presta a Power BI. Las trampas no las he buscado yo: aparecieron al cargar los datos.

Antes de empezar: comprueba la licencia y la fecha del conjunto

Antes de publicar un proyecto con datos abiertos comprueba tres cosas en el portal oficial: qué licencia o condiciones de reutilización tiene el conjunto, cuándo se actualizó por última vez y cómo pide la fuente que la cites. Abierto no significa sin condiciones, y cada organismo fija las suyas en su aviso legal.

En estos cinco casos las condiciones cambian bastante de una fuente a otra:

  • INE. Su aviso legal aplica la licencia Creative Commons Reconocimiento 4.0 (CC BY 4.0) a su información estadística, también con fines comerciales. Pide citar «Fuente: Sitio web del INE: www.ine.es» o, si tratas los datos, «Elaboración propia con datos extraídos del sitio web del INE: www.ine.es».

  • Eurostat. Su aviso de copyright autoriza reutilizar sus datos, con o sin fines comerciales, siempre que reconozcas la fuente e indiques si los has modificado. Hay excepciones para el uso comercial, como los datos de países que no son de la UE, de la AELC (EFTA) ni candidatos a la adhesión, por ejemplo Estados Unidos o Japón.

  • Red Eléctrica. Su aviso legal permite publicar sus contenidos en tu web o usarlos en materiales académicos con fines informativos y no comerciales. Exige mencionar a la sociedad como fuente, no alterar el sentido de los datos y citar la fecha de su última actualización.

  • Ajuntament de Barcelona. Los conjuntos de calidad del aire de Open Data BCN llevan licencia CC BY 4.0, y así aparecen también en el catálogo nacional datos.gob.es.

Apunta la fecha de descarga en el README del proyecto. Quien lo abra dentro de seis meses verá otros números, y así sabrá por qué.

Tabla de cinco proyectos de datos con datos abiertos: fuente y conjunto, condiciones de reutilización y primer paso en pandas de cada uno

Proyecto 1: qué ha subido más con el IPC del INE

Este proyecto responde a qué grupos de gasto suben más de precio y en qué comunidades. Usa la tabla 76136 del INE, «Índices por comunidades autónomas: general y de grupos ECOICOP ver.2», que el INE actualizó por última vez el 30 de octubre de 2026. Es un primer proyecto ideal porque la pregunta se entiende sin explicaciones.

El conjunto. Tabla 76136 del INE, descargable en CSV sin registro. Trae índice, variación mensual, variación anual y variación en lo que va de año, por grupo y por comunidad. Licencia CC BY 4.0.

El primer paso, probado el 6-11-2026:

import pandas as pd

URL = "https://www.ine.es/jaxiT3/files/t/es/csv_bdsc/76136.csv"
df = pd.read_csv(URL, sep=";", encoding="utf-8-sig", decimal=",", thousands=".")
df.columns = ["ccaa", "grupo", "tipo", "periodo", "valor"]

anual = df[df["tipo"] == "Variación anual"].dropna(subset=["valor"])
# El mes más reciente puede traer solo el índice general.
# Nos quedamos con el último mes en el que están todos los grupos.
grupos_por_mes = anual[anual["ccaa"] == "Nacional"].groupby("periodo")["grupo"].nunique()
completo = grupos_por_mes[grupos_por_mes == grupos_por_mes.max()].index.max()
print("Último mes con todos los grupos:", completo)

nacional = (anual[(anual["ccaa"] == "Nacional") & (anual["periodo"] == completo)]
            .sort_values("valor", ascending=False)[["grupo", "valor"]])
print(nacional.head(4).to_string(index=False))
print(nacional.tail(1).to_string(index=False, header=False))
import pandas as pd

URL = "https://www.ine.es/jaxiT3/files/t/es/csv_bdsc/76136.csv"
df = pd.read_csv(URL, sep=";", encoding="utf-8-sig", decimal=",", thousands=".")
df.columns = ["ccaa", "grupo", "tipo", "periodo", "valor"]

anual = df[df["tipo"] == "Variación anual"].dropna(subset=["valor"])
# El mes más reciente puede traer solo el índice general.
# Nos quedamos con el último mes en el que están todos los grupos.
grupos_por_mes = anual[anual["ccaa"] == "Nacional"].groupby("periodo")["grupo"].nunique()
completo = grupos_por_mes[grupos_por_mes == grupos_por_mes.max()].index.max()
print("Último mes con todos los grupos:", completo)

nacional = (anual[(anual["ccaa"] == "Nacional") & (anual["periodo"] == completo)]
            .sort_values("valor", ascending=False)[["grupo", "valor"]])
print(nacional.head(4).to_string(index=False))
print(nacional.tail(1).to_string(index=False, header=False))
import pandas as pd

URL = "https://www.ine.es/jaxiT3/files/t/es/csv_bdsc/76136.csv"
df = pd.read_csv(URL, sep=";", encoding="utf-8-sig", decimal=",", thousands=".")
df.columns = ["ccaa", "grupo", "tipo", "periodo", "valor"]

anual = df[df["tipo"] == "Variación anual"].dropna(subset=["valor"])
# El mes más reciente puede traer solo el índice general.
# Nos quedamos con el último mes en el que están todos los grupos.
grupos_por_mes = anual[anual["ccaa"] == "Nacional"].groupby("periodo")["grupo"].nunique()
completo = grupos_por_mes[grupos_por_mes == grupos_por_mes.max()].index.max()
print("Último mes con todos los grupos:", completo)

nacional = (anual[(anual["ccaa"] == "Nacional") & (anual["periodo"] == completo)]
            .sort_values("valor", ascending=False)[["grupo", "valor"]])
print(nacional.head(4).to_string(index=False))
print(nacional.tail(1).to_string(index=False, header=False))

Salida:

Último mes con todos los grupos: 2026M09
                                                    grupo  valor
                                            07 Transporte   10.5
04 Vivienda, agua, electricidad, gas y otros combustibles    7.2
               11 Restaurantes y servicios de alojamiento    4.9
                                           Índice general    4.9
05 Muebles, artículos del hogar y artículos para el mantenimiento corriente del hogar 1.5
Último mes con todos los grupos: 2026M09
                                                    grupo  valor
                                            07 Transporte   10.5
04 Vivienda, agua, electricidad, gas y otros combustibles    7.2
               11 Restaurantes y servicios de alojamiento    4.9
                                           Índice general    4.9
05 Muebles, artículos del hogar y artículos para el mantenimiento corriente del hogar 1.5
Último mes con todos los grupos: 2026M09
                                                    grupo  valor
                                            07 Transporte   10.5
04 Vivienda, agua, electricidad, gas y otros combustibles    7.2
               11 Restaurantes y servicios de alojamiento    4.9
                                           Índice general    4.9
05 Muebles, artículos del hogar y artículos para el mantenimiento corriente del hogar 1.5

La trampa. En nuestra descarga, octubre de 2026 ya aparecía, pero solo con la variación anual del índice general (4,9 %). El resto de grupos estaba vacío. Si filtras por «el último mes» sin mirar, comparas un dato completo con uno a medias. Por eso el código busca el último mes en el que están todos los grupos: septiembre de 2026, con el transporte a la cabeza (10,5 % de variación anual) y muebles y artículos del hogar al final (1,5 %).

Qué montar en Power BI. Una matriz de comunidades por grupos con formato condicional, y un gráfico de líneas con la evolución del índice general de la comunidad que elija quien mira el informe. La historia que puedes contar es cuánto se separa cada grupo de la media.

Proyecto 2: vivienda nueva frente a usada con la estadística de compraventas del INE

Este proyecto responde a dónde pesa más la vivienda nueva en las compraventas. Usa la tabla 6150 del INE, «Compraventa de viviendas según régimen y estado», actualizada el 26 de octubre de 2026, con datos mensuales por provincia. Te obliga a lidiar con una tabla jerárquica, que es justo lo que te encontrarás en una empresa.

El conjunto. Tabla 6150 del INE, en CSV. Trae total, vivienda nueva, usada, libre y protegida, a nivel nacional, de comunidad y de provincia. Licencia CC BY 4.0.

El primer paso, probado el 6-11-2026:

import pandas as pd

URL = "https://www.ine.es/jaxiT3/files/t/es/csv_bdsc/6150.csv"
df = pd.read_csv(URL, sep=";", encoding="utf-8-sig", dtype={"Total": "string"})
df.columns = ["nacional", "ccaa", "provincia", "tipo", "periodo", "valor"]
df["valor"] = pd.to_numeric(df["valor"].str.replace(".", "", regex=False), errors="coerce")

prov = df[df["provincia"].notna() & df["tipo"].isin(["Vivienda nueva", "Vivienda usada"])]
ult12 = sorted(prov["periodo"].unique())[-12:]
print("Periodo:", ult12[0], "a", ult12[-1])

tabla = (prov[prov["periodo"].isin(ult12)]
         .pivot_table(index="provincia", columns="tipo", values="valor", aggfunc="sum"))
tabla["% nueva"] = (100 * tabla["Vivienda nueva"] / tabla.sum(axis=1)).round(1)
print(tabla.sort_values("% nueva", ascending=False).head(3).to_string())
import pandas as pd

URL = "https://www.ine.es/jaxiT3/files/t/es/csv_bdsc/6150.csv"
df = pd.read_csv(URL, sep=";", encoding="utf-8-sig", dtype={"Total": "string"})
df.columns = ["nacional", "ccaa", "provincia", "tipo", "periodo", "valor"]
df["valor"] = pd.to_numeric(df["valor"].str.replace(".", "", regex=False), errors="coerce")

prov = df[df["provincia"].notna() & df["tipo"].isin(["Vivienda nueva", "Vivienda usada"])]
ult12 = sorted(prov["periodo"].unique())[-12:]
print("Periodo:", ult12[0], "a", ult12[-1])

tabla = (prov[prov["periodo"].isin(ult12)]
         .pivot_table(index="provincia", columns="tipo", values="valor", aggfunc="sum"))
tabla["% nueva"] = (100 * tabla["Vivienda nueva"] / tabla.sum(axis=1)).round(1)
print(tabla.sort_values("% nueva", ascending=False).head(3).to_string())
import pandas as pd

URL = "https://www.ine.es/jaxiT3/files/t/es/csv_bdsc/6150.csv"
df = pd.read_csv(URL, sep=";", encoding="utf-8-sig", dtype={"Total": "string"})
df.columns = ["nacional", "ccaa", "provincia", "tipo", "periodo", "valor"]
df["valor"] = pd.to_numeric(df["valor"].str.replace(".", "", regex=False), errors="coerce")

prov = df[df["provincia"].notna() & df["tipo"].isin(["Vivienda nueva", "Vivienda usada"])]
ult12 = sorted(prov["periodo"].unique())[-12:]
print("Periodo:", ult12[0], "a", ult12[-1])

tabla = (prov[prov["periodo"].isin(ult12)]
         .pivot_table(index="provincia", columns="tipo", values="valor", aggfunc="sum"))
tabla["% nueva"] = (100 * tabla["Vivienda nueva"] / tabla.sum(axis=1)).round(1)
print(tabla.sort_values("% nueva", ascending=False).head(3).to_string())

Salida:

Periodo: 2025M09 a 2026M08
tipo            Vivienda nueva  Vivienda usada  % nueva
provincia
01 Araba/Álava            1672            2492     40.2
29 Málaga                11695           23501     33.2
31 Navarra                2469            5230     32.1
Periodo: 2025M09 a 2026M08
tipo            Vivienda nueva  Vivienda usada  % nueva
provincia
01 Araba/Álava            1672            2492     40.2
29 Málaga                11695           23501     33.2
31 Navarra                2469            5230     32.1
Periodo: 2025M09 a 2026M08
tipo            Vivienda nueva  Vivienda usada  % nueva
provincia
01 Araba/Álava            1672            2492     40.2
29 Málaga                11695           23501     33.2
31 Navarra                2469            5230     32.1

La trampa. Hay dos. La primera, el punto como separador de miles: «49.319», el total nacional de agosto de 2026, son más de cuarenta y nueve mil compraventas, no cuarenta y nueve. Si pandas lo lee como decimal, todo sale mal sin dar error. La segunda, la jerarquía: la tabla mezcla filas de España, de comunidad y de provincia, y si sumas sin filtrar cuentas cada operación tres veces. Por eso nos quedamos con las filas que tienen provincia. En los doce meses de septiembre de 2025 a agosto de 2026, Araba/Álava encabeza el peso de la vivienda nueva, con un 40,2 %.

Qué montar en Power BI. Un mapa de provincias coloreado por el porcentaje de vivienda nueva, con una segmentación por comunidad y un gráfico de columnas apiladas por mes. Si ya has trabajado el paso a paso con pandas, este proyecto es la continuación natural de nuestro tutorial de primeros pasos con pandas sobre un dataset real del INE.

Del dato limpio al informe que se entiende

Comunicar con datos: visualización y storytelling para convertir números en narrativas que persuaden. Es lo que trabajas en el máster en AI Data Analytics, con módulos de visualización, storytelling y reporting y de Power BI. Descarga el dossier y conoce el programa completo.

Descargar dossier →

Proyecto 3: el paro en España frente a la UE con Eurostat

Este proyecto responde a cómo evoluciona el paro en España comparado con la media de la Unión Europea. Usa el conjunto une_rt_m de Eurostat, la tasa de paro mensual, que Eurostat actualizó el 5 de noviembre de 2026. Es útil porque te obliga a trabajar con códigos de país y a decidir qué comparas con qué.

El conjunto. une_rt_m de Eurostat, descargable por su API en CSV sin clave. Pedimos la serie desestacionalizada, total por sexo y edad, en porcentaje de la población activa. Reutilización permitida citando la fuente; el uso comercial excluye los datos de países ajenos a la UE, la AELC y los candidatos.

El primer paso, probado el 6-11-2026:

import pandas as pd

URL = ("https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1/data/une_rt_m/"
       "M.SA.TOTAL.PC_ACT.T.?format=SDMX-CSV&startPeriod=2024-01")
df = pd.read_csv(URL)
print("Actualizado por Eurostat:", df["LAST UPDATE"].iloc[0])

ue = ["AT","BE","BG","CY","CZ","DE","DK","EE","EL","ES","FI","FR","HR","HU","IE",
      "IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK","EU27_2020"]
df = df[df["geo"].isin(ue)]

serie = df.pivot_table(index="TIME_PERIOD", columns="geo", values="OBS_VALUE")
ultimo = serie[["ES", "EU27_2020"]].dropna().index.max()
hace12 = serie.index[serie.index.get_loc(ultimo) - 12]
print(serie.loc[[hace12, ultimo], ["ES", "EU27_2020"]])
import pandas as pd

URL = ("https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1/data/une_rt_m/"
       "M.SA.TOTAL.PC_ACT.T.?format=SDMX-CSV&startPeriod=2024-01")
df = pd.read_csv(URL)
print("Actualizado por Eurostat:", df["LAST UPDATE"].iloc[0])

ue = ["AT","BE","BG","CY","CZ","DE","DK","EE","EL","ES","FI","FR","HR","HU","IE",
      "IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK","EU27_2020"]
df = df[df["geo"].isin(ue)]

serie = df.pivot_table(index="TIME_PERIOD", columns="geo", values="OBS_VALUE")
ultimo = serie[["ES", "EU27_2020"]].dropna().index.max()
hace12 = serie.index[serie.index.get_loc(ultimo) - 12]
print(serie.loc[[hace12, ultimo], ["ES", "EU27_2020"]])
import pandas as pd

URL = ("https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1/data/une_rt_m/"
       "M.SA.TOTAL.PC_ACT.T.?format=SDMX-CSV&startPeriod=2024-01")
df = pd.read_csv(URL)
print("Actualizado por Eurostat:", df["LAST UPDATE"].iloc[0])

ue = ["AT","BE","BG","CY","CZ","DE","DK","EE","EL","ES","FI","FR","HR","HU","IE",
      "IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK","EU27_2020"]
df = df[df["geo"].isin(ue)]

serie = df.pivot_table(index="TIME_PERIOD", columns="geo", values="OBS_VALUE")
ultimo = serie[["ES", "EU27_2020"]].dropna().index.max()
hace12 = serie.index[serie.index.get_loc(ultimo) - 12]
print(serie.loc[[hace12, ultimo], ["ES", "EU27_2020"]])

Salida:

Actualizado por Eurostat: 05/11/26 23:00:00
geo            ES  EU27_2020
TIME_PERIOD
2025-09      10.4        6.0
2026-09      10.3        6.2
Actualizado por Eurostat: 05/11/26 23:00:00
geo            ES  EU27_2020
TIME_PERIOD
2025-09      10.4        6.0
2026-09      10.3        6.2
Actualizado por Eurostat: 05/11/26 23:00:00
geo            ES  EU27_2020
TIME_PERIOD
2025-09      10.4        6.0
2026-09      10.3        6.2

La trampa. La descarga trae también Estados Unidos, Japón, Turquía y otros países que no son de la UE. Si haces un ranking sin filtrar, comparas con quien no toca, y los datos de Estados Unidos o Japón no se pueden reutilizar con fines comerciales. Por eso filtramos los 27 estados miembros y el agregado EU27_2020. Otro detalle: el catálogo de Eurostat ya llega a octubre de 2026, pero el último mes con dato para España y la UE a la vez es septiembre. Con esa comparación, España pasa del 10,4 % al 10,3 % en un año y la media de la UE, del 6,0 % al 6,2 %.

Qué montar en Power BI. Un gráfico de líneas España frente a la UE y un ranking de los 27 países en el último mes común. Mide la brecha en puntos, no en porcentaje, y explica por qué en una nota del informe.

Proyecto 4: cuándo consume más electricidad España con la API de Red Eléctrica

Este proyecto responde a cómo cambia la demanda eléctrica entre días laborables y fines de semana, y qué meses consumen más. Usa la API REData de Red Eléctrica, que devuelve la demanda diaria del sistema peninsular en JSON sin registro. Es el proyecto de la lista que te enseña a trabajar con una API en lugar de con un CSV.

El conjunto. El endpoint de evolución de la demanda de la API REData. Pedimos los días del 1 de noviembre de 2025 al 31 de octubre de 2026 para el sistema peninsular. Las condiciones de uso permiten fines informativos y académicos, no comerciales, citando a Red Eléctrica y la fecha de actualización de los datos.

El primer paso, probado el 6-11-2026:

import pandas as pd
import requests

URL = "https://apidatos.ree.es/es/datos/demanda/evolucion"
params = {"start_date": "2025-11-01T00:00", "end_date": "2026-10-31T23:59", "time_trunc": "day",
          "geo_trunc": "electric_system", "geo_limit": "peninsular", "geo_ids": 8741}
js = requests.get(URL, params=params, timeout=60).json()
valores = js["included"][0]["attributes"]["values"]

df = pd.DataFrame(valores)
df["fecha"] = pd.to_datetime(df["datetime"].str[:10])
print("Unidad que declara la API:", js["included"][0]["attributes"]["magnitude"])
df["GWh"] = df["value"] / 1000
print("Días:", len(df), "de", df["fecha"].min().date(), "a", df["fecha"].max().date())

df["tipo_dia"] = df["fecha"].dt.day_of_week.map(lambda d: "fin de semana" if d >= 5 else "laborable")
print(df.groupby("tipo_dia")["GWh"].mean().round(1))
import pandas as pd
import requests

URL = "https://apidatos.ree.es/es/datos/demanda/evolucion"
params = {"start_date": "2025-11-01T00:00", "end_date": "2026-10-31T23:59", "time_trunc": "day",
          "geo_trunc": "electric_system", "geo_limit": "peninsular", "geo_ids": 8741}
js = requests.get(URL, params=params, timeout=60).json()
valores = js["included"][0]["attributes"]["values"]

df = pd.DataFrame(valores)
df["fecha"] = pd.to_datetime(df["datetime"].str[:10])
print("Unidad que declara la API:", js["included"][0]["attributes"]["magnitude"])
df["GWh"] = df["value"] / 1000
print("Días:", len(df), "de", df["fecha"].min().date(), "a", df["fecha"].max().date())

df["tipo_dia"] = df["fecha"].dt.day_of_week.map(lambda d: "fin de semana" if d >= 5 else "laborable")
print(df.groupby("tipo_dia")["GWh"].mean().round(1))
import pandas as pd
import requests

URL = "https://apidatos.ree.es/es/datos/demanda/evolucion"
params = {"start_date": "2025-11-01T00:00", "end_date": "2026-10-31T23:59", "time_trunc": "day",
          "geo_trunc": "electric_system", "geo_limit": "peninsular", "geo_ids": 8741}
js = requests.get(URL, params=params, timeout=60).json()
valores = js["included"][0]["attributes"]["values"]

df = pd.DataFrame(valores)
df["fecha"] = pd.to_datetime(df["datetime"].str[:10])
print("Unidad que declara la API:", js["included"][0]["attributes"]["magnitude"])
df["GWh"] = df["value"] / 1000
print("Días:", len(df), "de", df["fecha"].min().date(), "a", df["fecha"].max().date())

df["tipo_dia"] = df["fecha"].dt.day_of_week.map(lambda d: "fin de semana" if d >= 5 else "laborable")
print(df.groupby("tipo_dia")["GWh"].mean().round(1))

Salida:

Unidad que declara la API: None
Días: 365 de 2025-11-01 a 2026-10-31
tipo_dia
fin de semana    606.2
laborable        703.1
Name: GWh, dtype: float64
Unidad que declara la API: None
Días: 365 de 2025-11-01 a 2026-10-31
tipo_dia
fin de semana    606.2
laborable        703.1
Name: GWh, dtype: float64
Unidad que declara la API: None
Días: 365 de 2025-11-01 a 2026-10-31
tipo_dia
fin de semana    606.2
laborable        703.1
Name: GWh, dtype: float64

La trampa. La respuesta no dice en qué unidad viene la demanda: el campo de magnitud llega vacío. Por el orden de magnitud de los valores los tratamos como MWh y dividimos entre mil para hablar de GWh, pero antes de rotular un gráfico compruébalo en la web de REData. Ese tipo de verificación es lo que distingue un análisis serio. El otro detalle son las fechas, que llegan como texto con zona horaria: recortarlas a día evita líneas duplicadas con el cambio de hora. En la descarga, la demanda media de un laborable es de 703,1 GWh frente a 606,2 GWh en fin de semana, y julio de 2026 es el mes de mayor demanda del periodo.

Qué montar en Power BI. Un calendario de calor con un cuadro por día, coloreado por demanda, y una tarjeta con la diferencia entre laborables y fines de semana. Si dudas entre herramientas para este tipo de visual, tienes la comparativa de Power BI o Tableau.

Proyecto 5: la contaminación por horas en Barcelona con Open Data BCN

Este proyecto responde a qué horas del día y qué zonas de Barcelona registran más dióxido de nitrógeno (NO2). Usa los datos horarios de calidad del aire que publica el Ajuntament de Barcelona en Open Data BCN, con licencia CC BY 4.0. Es el más laborioso: el fichero viene en formato ancho y con banderas de validación.

El conjunto. Calidad del aire, detalle por estación, un CSV por mes. El último publicado el 6-11-2026 es el de mayo de 2026. Para poner nombre a las estaciones usamos el conjunto de estaciones de 2026 y, para saber qué contaminante es cada código, el de contaminantes, ambos del mismo portal y con la misma licencia.

El primer paso, probado el 6-11-2026:

import pandas as pd

BASE = "https://opendata-ajuntament.barcelona.cat/data/dataset/"
DATOS = BASE + "0582a266-ea06-4cc5-a219-913b22484e40/resource/77cae9fe-0af8-4668-a22a-1d9fed0e7dcc/download"
ESTACIONES = BASE + "4dff88b1-151b-48db-91c2-45007cd5d07a/resource/7de4c08f-5962-4d4d-9dca-e10badffbffd/download/2026_qualitat_aire_estacions.csv"

df = pd.read_csv(DATOS)
no2 = df[df["CODI_CONTAMINANT"] == 8]  # 8 = NO2 (µg/m³), según el CSV de contaminantes

# De formato ancho (H01..H24 + V01..V24) a largo: una fila por estación, día y hora
largo = pd.concat([
    no2[["ESTACIO", "DIA"]].assign(hora=h, valor=no2[f"H{h:02d}"], validado=no2[f"V{h:02d}"])
    for h in range(1, 25)
])
largo = largo[largo["validado"] == "V"]
print("Filas validadas:", len(largo))

nombres = pd.read_csv(ESTACIONES).drop_duplicates("Estacio").set_index("Estacio")["nom_cabina"]
print(largo.groupby("hora")["valor"].mean().round(1).sort_values().tail(3))
print(largo.groupby("ESTACIO")["valor"].mean().round(1).rename(index=nombres)
      .sort_values(ascending=False).to_string())
import pandas as pd

BASE = "https://opendata-ajuntament.barcelona.cat/data/dataset/"
DATOS = BASE + "0582a266-ea06-4cc5-a219-913b22484e40/resource/77cae9fe-0af8-4668-a22a-1d9fed0e7dcc/download"
ESTACIONES = BASE + "4dff88b1-151b-48db-91c2-45007cd5d07a/resource/7de4c08f-5962-4d4d-9dca-e10badffbffd/download/2026_qualitat_aire_estacions.csv"

df = pd.read_csv(DATOS)
no2 = df[df["CODI_CONTAMINANT"] == 8]  # 8 = NO2 (µg/m³), según el CSV de contaminantes

# De formato ancho (H01..H24 + V01..V24) a largo: una fila por estación, día y hora
largo = pd.concat([
    no2[["ESTACIO", "DIA"]].assign(hora=h, valor=no2[f"H{h:02d}"], validado=no2[f"V{h:02d}"])
    for h in range(1, 25)
])
largo = largo[largo["validado"] == "V"]
print("Filas validadas:", len(largo))

nombres = pd.read_csv(ESTACIONES).drop_duplicates("Estacio").set_index("Estacio")["nom_cabina"]
print(largo.groupby("hora")["valor"].mean().round(1).sort_values().tail(3))
print(largo.groupby("ESTACIO")["valor"].mean().round(1).rename(index=nombres)
      .sort_values(ascending=False).to_string())
import pandas as pd

BASE = "https://opendata-ajuntament.barcelona.cat/data/dataset/"
DATOS = BASE + "0582a266-ea06-4cc5-a219-913b22484e40/resource/77cae9fe-0af8-4668-a22a-1d9fed0e7dcc/download"
ESTACIONES = BASE + "4dff88b1-151b-48db-91c2-45007cd5d07a/resource/7de4c08f-5962-4d4d-9dca-e10badffbffd/download/2026_qualitat_aire_estacions.csv"

df = pd.read_csv(DATOS)
no2 = df[df["CODI_CONTAMINANT"] == 8]  # 8 = NO2 (µg/m³), según el CSV de contaminantes

# De formato ancho (H01..H24 + V01..V24) a largo: una fila por estación, día y hora
largo = pd.concat([
    no2[["ESTACIO", "DIA"]].assign(hora=h, valor=no2[f"H{h:02d}"], validado=no2[f"V{h:02d}"])
    for h in range(1, 25)
])
largo = largo[largo["validado"] == "V"]
print("Filas validadas:", len(largo))

nombres = pd.read_csv(ESTACIONES).drop_duplicates("Estacio").set_index("Estacio")["nom_cabina"]
print(largo.groupby("hora")["valor"].mean().round(1).sort_values().tail(3))
print(largo.groupby("ESTACIO")["valor"].mean().round(1).rename(index=nombres)
      .sort_values(ascending=False).to_string())

Salida:

Filas validadas: 6628
hora
8     26.5
10    27.6
9     28.7
Name: valor, dtype: float64
ESTACIO
Barcelona - Eixample             27.2
Barcelona - Navas                25.1
Barcelona - Gràcia               24.8
Barcelona - Vall Hebron          21.0
Barcelona - Poblenou             20.6
Barcelona - Ciutadella           18.6
Barcelona - Sants                18.6
Barcelona - Palau Reial          14.5
Barcelona - Observatori Fabra     8.4
Filas validadas: 6628
hora
8     26.5
10    27.6
9     28.7
Name: valor, dtype: float64
ESTACIO
Barcelona - Eixample             27.2
Barcelona - Navas                25.1
Barcelona - Gràcia               24.8
Barcelona - Vall Hebron          21.0
Barcelona - Poblenou             20.6
Barcelona - Ciutadella           18.6
Barcelona - Sants                18.6
Barcelona - Palau Reial          14.5
Barcelona - Observatori Fabra     8.4
Filas validadas: 6628
hora
8     26.5
10    27.6
9     28.7
Name: valor, dtype: float64
ESTACIO
Barcelona - Eixample             27.2
Barcelona - Navas                25.1
Barcelona - Gràcia               24.8
Barcelona - Vall Hebron          21.0
Barcelona - Poblenou             20.6
Barcelona - Ciutadella           18.6
Barcelona - Sants                18.6
Barcelona - Palau Reial          14.5
Barcelona - Observatori Fabra     8.4

La trampa. Cada fila es un día de una estación, con 24 columnas de valores (H01 a H24) y otras 24 que indican si cada valor está validado (V01 a V24). Si promedias las columnas H sin mirar las V, mezclas datos validados con datos que no lo están. El código pasa la tabla a formato largo, una fila por hora, y se queda solo con lo validado: 6.628 valores horarios de NO2 en mayo de 2026. Las columnas H08 a H10 concentran las medias más altas y la estación de Eixample, la media mensual más alta (27,2 µg/m³).

Qué montar en Power BI. Un mapa con las estaciones (el conjunto de estaciones trae latitud y longitud) y un gráfico de líneas del perfil horario con una segmentación por estación. Si te pierdes con algún término del modelo de datos, el glosario de Power BI te lo resuelve.

Cómo presentar estos proyectos en tu portfolio

Cada proyecto de datos se presenta igual: la pregunta en una línea, el conjunto con su enlace, fecha de descarga y licencia, el método en pocos pasos, la conclusión en una frase y el informe o el notebook enlazados. Lo importante va arriba, para que se entienda de un vistazo.

Una estructura de README que funciona para cualquiera de los cinco:

  1. Pregunta. «¿Qué grupos de gasto han subido más en mi comunidad?»

  2. Datos. Fuente, enlace, fecha de descarga, licencia y la cita que pide el organismo.

  3. Método. Limpieza (qué trampa encontraste y cómo la resolviste), agregación y visualización.

  4. Resultado. Una conclusión con su cifra y su fecha, y una captura del informe.

  5. Límites. Qué no responde el análisis. En el proyecto del paro, por ejemplo, no explica por qué se mueve la tasa.

Dos consejos que me parecen los más útiles. El primero: la trampa de cada conjunto es tu mejor material para una entrevista, porque demuestra que miras los datos antes de graficarlos. El segundo: no publiques los cinco a medias. Dos proyectos terminados, con README y un informe revisado, dicen más que cinco notebooks sin cerrar. Antes de compartir un informe, pásale nuestro checklist de 15 comprobaciones de Power BI.

Para la parte de presentación, GitHub y cómo contar un proyecto en una entrevista, el artículo sobre qué proyectos incluir en un portfolio de programador tiene ideas que valen igual para datos. Y si aún no sabes qué herramienta aprender primero, la ruta paso a paso para empezar en análisis de datos ordena Excel, SQL, Power BI y Python.

Siguiente paso: de un proyecto suelto a contar una historia con datos

Cargar un conjunto y agregarlo es el primer paso; el trabajo que se valora es convertir ese resultado en un informe que alguien use para decidir. Eso pide visualización con criterio, una narrativa clara y dominio de Power BI, que es la herramienta con la que vas a montar casi todos los informes de esta lista.

Estos proyectos usan justo lo que se trabaja en el máster en AI Data Analytics de ThePower Tech School. Según su ficha, consultada el 6-11-2026, el plan de estudios incluye el «Módulo 6: Visualización, storytelling y reporting» y el «Módulo 11: Power BI», y entre lo que serás capaz de hacer está «Comunicar con datos: visualización y storytelling para convertir números en narrativas que persuaden». El programa también incluye «Plantillas y descargables: cheatsheets, glosarios y KPI library para aplicar desde el primer día».

Domina Power BI Service en 3 meses

100% Online | Práctico

Aprende a utilizar la herramienta Power BI desde cero

Descubre Power BI →

Domina Power BI Service en 3 meses

100% Online | Práctico

Aprende a utilizar la herramienta Power BI desde cero

Descubre Power BI →

Preguntas frecuentes sobre proyectos de datos para el portfolio

¿Cuántos proyectos de datos necesito en mi portfolio?

Con dos o tres proyectos terminados tienes un buen punto de partida. Importa más que cada uno tenga una pregunta clara, datos que se puedan comprobar y una conclusión explicada que la cantidad. Un proyecto acabado, con su README y su informe, enseña más de tu forma de trabajar que cinco ejercicios a medio hacer.

¿Dónde encuentro datos abiertos para practicar análisis de datos?

En España, los puntos de partida más directos son el INE, el catálogo nacional datos.gob.es y los portales de las comunidades y de los ayuntamientos, como Open Data BCN. Para comparar con Europa, Eurostat. Muchos organismos, como Red Eléctrica, publican además su propia API. Antes de usar un conjunto, revisa su licencia y su fecha.

¿Puedo usar datos abiertos en un portfolio público?

Sí, siempre que respetes las condiciones de cada fuente. El INE y Open Data BCN usan CC BY 4.0, que pide citar la fuente. Eurostat permite reutilizar sus datos reconociendo la fuente, con algunas excepciones para el uso comercial. Red Eléctrica permite fines informativos no comerciales, citando la fecha de actualización.

¿Es mejor hacer los proyectos en Python o en Power BI?

Lo más completo es combinar los dos: Python con pandas para descargar, limpiar y agregar, y Power BI para el informe que vería alguien de negocio. Si empiezas desde cero, puedes hacer todo el proyecto en Power BI con Power Query y añadir Python más adelante. Lo importante es que el resultado responda la pregunta.

¿Qué pasa si los datos se actualizan después de publicar mi proyecto?

Es lo normal con datos oficiales y juega a tu favor. Anota en el README la fecha de descarga y las cifras de ese momento. Si el código descarga los datos desde la URL oficial, cualquiera puede ejecutarlo y obtener la versión actual, y tú puedes actualizar el informe en minutos. Eso también es algo que contar en una entrevista.

Comparte este artículo

THE POWER TECH SCHOOL

¿Qué máster de Tech encaja contigo?

Cuéntanos tu punto de partida y te orientamos hacia el programa adecuado.

Pedir información

RECOMENDADOS PARA TI

Full stack con IA desde cero, sin conocimientos previos

Conviértete en Data Analyst con IA generativa

IA en producción para developers: RAG, LangGraph y MLOps

También puede interesarte

Categorías

¿Buscas algo en concreto?

Artículo siguiente

Usar IA para programar vs programar con IA: la diferencia real

Usar IA para programar vs programar con IA: la diferencia real

Hay una diferencia real entre usar IA para programar y programar con IA. Uno acelera tu flujo. El otro cambia completamente qué puedes construir.

Ver artículo

Mapas de Karnaugh: cómo simplificar funciones lógicas paso a paso

Mapas de Karnaugh: cómo simplificar funciones lógicas paso a paso

Qué es un mapa de Karnaugh, por qué el orden es 00, 01, 11, 10 y cómo agrupar bien, con un ejemplo resuelto de 4 variables y condiciones indiferentes.

Ver artículo

https://thepower.education/blog/tech/de-developer-backend-a-agentic-developer

De developer backend a Agentic Developer: hoja de ruta paso a paso

Si ya desarrollas en backend o fullstack, tienes más de la mitad del camino hecho para convertirte en Agentic Developer. Esta es la ruta concreta paso a paso.

Ver artículo

Programar con IA: el nuevo Full Stack Developer y cómo formarte

Programar con IA: el nuevo Full Stack Developer y cómo formarte

Programar con IA es más que autocompletar código. Descubre qué hace el Full Stack Developer con IA, cuánto gana en España y cómo aprender desde cero.

Ver artículo

Herramientas ciberseguridad: guía técnica

Herramientas de ciberseguridad: lo que usa un profesional para proteger su sistema

Las herramientas de ciberseguridad que usa un profesional para proteger su sistema, cómo configurarlas y cuándo usar cada una. Guía técnica para 2026.

Ver artículo

Perfil Desarrollador Agéntico: habilidades y salidas en 2026

Perfil del Desarrollador Agéntico: habilidades y salidas profesionales en 2026

Descubre qué habilidades define el perfil de un desarrollador agéntico, qué roles existen en el mercado y cuánto gana este perfil en España en 2026.

Ver artículo

En tech, quien no se forma cada año, se queda atrás
En tech, quien no se forma cada año, se queda atrás

VER MÁSTERS TECH

VER MÁSTERS TECH

Business & IA

Tech & Data

Pharma

FP Oficial

Oposiciones

Oficios

In Company