Salta el contingut

Visualització de Dades

Introducció

La visualització de dades és l'acte de representar informació de forma gràfica per facilitar la comprensió i la presa de decisions. Edward Tufte, el "guru" de la visualització, va establir que el propòsit d'una visualització és revelar les dades, no decorar-les. En el context del Big Data, la visualització és l'última milla: de res serveix tenir petabytes d'informació analitzada si no es pot comunicar de forma clara i accionable.

El 2025, el panorama de la visualització de dades ha evolucionat en tres direccions: (1) la democratització gràcies a eines com Power BI i Tableau, (2) la visualització en temps real gràcies a Grafana i Streamlit, i (3) la integració de la IA generativa per a la generació automàtica de dashboards i la interpretació en llenguatge natural.


Principis fonamentals de visualització

Les lleis de Tufte

Edward Tufte, en el seu llibre "The Visual Display of Quantitative Information" (1983), va establir principis que segueixen vigents:

Maximitzar el data-ink ratio: la proporció de "tinta" dedicada a les dades ha de ser màxima. Cada element visual ha de tenir justificació informativa. Elements decoratius (fons de colors, gradients innecessaris, bordures, efectes 3D en gràfics 2D) distreuen i oculten la informació.

Evitar chartjunk: Tufte va encunyar el terme "chartjunk" per a tots els elements visuals superflus que contaminen un gràfic: graelles innecessàries, efectes d'ombra, imatges de fons, emojis a les etiquetes (sic).

Mostrar la variació de les dades, no la variació del disseny: el lector ha de poder comparar valors directament, no lluitar amb escales inconsistents o eixos truncats.

Les "mentides estadístiques"

Darrell Huff va escriure el 1954 "How to Lie with Statistics", un clàssic que segueix absolutament rellevant:

  1. Eix Y truncat: iniciar l'eix Y en un valor diferent de zero exagera visualment les diferències
  2. Gràfics de pastís amb massa segments: l'ull humà és molt dolent comparant àrees i angles
  3. Escales no lineals sense advertir: usar escala logarítmica sense indicar-ho distorsiona la percepció
  4. Correlació vs causalitat: mostrar dues línies que evolucionen juntes no implica relació causal
  5. Selecció de l'interval temporal: triar el perióde que "confirma" la tesi prèvia

Gràfics 3D

Els gràfics de barres, pastís i línies en 3D mai milloren la comprensió de les dades. La perspectiva distorsiona les proporcions i dificulta la comparació de valors. Evita sempre els gràfics 3D tret que representin dades veritablement tridimensionals (mapes topogràfics, núvols de punts 3D).

Seleccionar el gràfic adequat

Comparació entre categories → Barres verticals o horitzontals
Evolució temporal            → Línies
Distribució d'una variable  → Histograma, violin plot, box plot
Relació entre dues variables → Scatter plot
Part del tot                 → Barres apilades (NO pastís per >5 categories)
Distribució geogràfica      → Mapa (choropleth, mapa de punts)
Jerarquia                   → Treemap, sunburst
Fluxos i connexions         → Sankey diagram, chord diagram

Biblioteques Python de visualització

Matplotlib: la base de tot

Matplotlib és la biblioteca de visualització de Python per excel·lència, present des de 2003. Totes les altres biblioteques (Seaborn, Pandas plot) es construeixen sobre ella.

import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np

# Estil professional
plt.style.use('seaborn-v0_8-whitegrid')

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('Anàlisi de Vendes 2024 — Institut Sa Palomera Store',
             fontsize=16, fontweight='bold', y=1.02)

# Gràfic 1: Vendes mensuals (línia)
mesos = ['Gen', 'Feb', 'Mar', 'Abr', 'Mai', 'Jun',
         'Jul', 'Ago', 'Set', 'Oct', 'Nov', 'Des']
vendes_2024 = [12500, 11800, 15200, 14100, 16800, 18300,
               15600, 12900, 17200, 19800, 22100, 28500]
vendes_2023 = [11000, 10500, 13800, 12500, 15200, 16900,
               14100, 11800, 15600, 17900, 20100, 25000]

axes[0, 0].plot(mesos, vendes_2024, 'b-o', linewidth=2, markersize=6, label='2024')
axes[0, 0].plot(mesos, vendes_2023, 'r--s', linewidth=1.5, markersize=5,
                alpha=0.7, label='2023')
axes[0, 0].fill_between(range(len(mesos)), vendes_2024, vendes_2023,
                         alpha=0.1, color='blue')
axes[0, 0].set_title('Evolució de Vendes Mensuals (€)')
axes[0, 0].legend()
axes[0, 0].yaxis.set_major_formatter(mticker.FuncFormatter(
    lambda x, p: f'{x:,.0f}€'))

# Gràfic 2: Vendes per categoria (barres horitzontals)
categories = ['Electrònica', 'Roba', 'Llar', 'Esports', 'Alimentació']
totals = [85200, 62300, 48900, 37600, 29100]
colors = ['#2196F3', '#4CAF50', '#FF9800', '#9C27B0', '#F44336']

bars = axes[0, 1].barh(categories, totals, color=colors, edgecolor='white')
axes[0, 1].set_title('Vendes per Categoria (€)')
axes[0, 1].set_xlabel('Import (€)')
# Afegir etiquetes de valor
for bar in bars:
    width = bar.get_width()
    axes[0, 1].text(width + 500, bar.get_y() + bar.get_height()/2,
                    f'{width:,.0f}€', va='center', fontsize=9)

# Gràfic 3: Distribució del valor de comanda (histograma)
np.random.seed(42)
valors_comandes = np.concatenate([
    np.random.lognormal(4.0, 0.8, 800),
    np.random.lognormal(5.5, 0.5, 200)
])
axes[1, 0].hist(valors_comandes, bins=50, color='steelblue',
                edgecolor='white', alpha=0.8)
axes[1, 0].axvline(np.median(valors_comandes), color='red',
                    linestyle='--', label=f'Mediana: {np.median(valors_comandes):.0f}€')
axes[1, 0].set_title('Distribució del Valor de Comanda')
axes[1, 0].set_xlabel('Valor (€)')
axes[1, 0].set_ylabel('Freqüència')
axes[1, 0].legend()

# Gràfic 4: Relació preu-quantitat (scatter plot)
preus = np.random.uniform(10, 500, 200)
quantitats = 1000 / preus + np.random.normal(0, 0.5, 200)
quantitats = np.clip(quantitats, 0.5, 10)
scatter = axes[1, 1].scatter(preus, quantitats,
                              c=preus * quantitats,  # color = valor total
                              cmap='viridis', alpha=0.6, s=50)
axes[1, 1].set_title('Relació Preu vs Quantitat Venuda')
axes[1, 1].set_xlabel('Preu unitari (€)')
axes[1, 1].set_ylabel('Quantitat venuda (unitats)')
plt.colorbar(scatter, ax=axes[1, 1], label='Valor total (€)')

plt.tight_layout()
plt.savefig('analisi_vendes_2024.png', dpi=150, bbox_inches='tight')
plt.show()

Plotly: visualitzacions interactives

Plotly és la biblioteca de visualització interactiva de referència el 2025. Genera gràfics HTML interactius (zoom, hover, filtres) perfectes per a dashboards web i notebooks.

import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np

# Dataset de vendes simulat
np.random.seed(42)
dates = pd.date_range('2024-01-01', '2024-12-31', freq='D')
df = pd.DataFrame({
    'data': dates,
    'vendes': np.random.lognormal(8, 0.5, len(dates)) + \
              np.sin(np.arange(len(dates)) * 2 * np.pi / 365) * 5000,
    'categoria': np.random.choice(['Electrònica', 'Roba', 'Llar'], len(dates)),
    'pais': np.random.choice(['ES', 'FR', 'DE', 'IT'], len(dates)),
})

# Gràfic de línies temporal interactiu
fig = px.line(
    df.groupby('data')['vendes'].sum().reset_index(),
    x='data', y='vendes',
    title='Evolució de Vendes Diàries 2024',
    labels={'data': 'Data', 'vendes': 'Import (€)'},
    template='plotly_white'
)
fig.update_traces(line=dict(width=1.5, color='royalblue'))
fig.add_vline(x='2024-11-29', line_dash='dash', line_color='red',
              annotation_text='Black Friday')
fig.update_layout(hovermode='x unified')
fig.write_html('vendes_temporal.html')

# Dashboard multi-panel
fig_multi = make_subplots(
    rows=2, cols=2,
    subplot_titles=['Vendes per País', 'Distribució per Categoria',
                    'Evolució Mensual', 'Top Dies de la Setmana'],
    specs=[[{"type": "bar"}, {"type": "pie"}],
           [{"type": "scatter"}, {"type": "bar"}]]
)

# Panel 1: barres per país
vendes_pais = df.groupby('pais')['vendes'].sum().reset_index()
fig_multi.add_trace(
    go.Bar(x=vendes_pais['pais'], y=vendes_pais['vendes'],
           marker_color=['#2196F3', '#4CAF50', '#FF9800', '#9C27B0']),
    row=1, col=1
)

# Panel 2: pastís per categoria
vendes_cat = df.groupby('categoria')['vendes'].sum().reset_index()
fig_multi.add_trace(
    go.Pie(labels=vendes_cat['categoria'], values=vendes_cat['vendes'],
           hole=0.3),
    row=1, col=2
)

fig_multi.update_layout(height=700, showlegend=False,
                         title_text='Dashboard de Vendes 2024')
fig_multi.write_html('dashboard_vendes.html')
fig_multi.show()

Streamlit: aplicacions de dades en minuts

Streamlit permet crear aplicacions web interactives de visualització de dades amb codi Python pur, sense JavaScript:

# app_vendes.py - executar amb: streamlit run app_vendes.py
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from datetime import date

st.set_page_config(
    page_title="Dashboard de Vendes — Institut Sa Palomera",
    page_icon="📊",
    layout="wide"
)

st.title("Dashboard de Vendes en Temps Real")
st.markdown("Anàlisi de vendes actualitzat automàticament cada 5 minuts")

# Sidebar amb filtres
with st.sidebar:
    st.header("Filtres")
    data_inici = st.date_input("Data d'inici", value=date(2024, 1, 1))
    data_fi = st.date_input("Data de fi", value=date(2024, 12, 31))
    categories = st.multiselect(
        "Categories",
        options=["Electrònica", "Roba", "Llar", "Esports"],
        default=["Electrònica", "Roba", "Llar", "Esports"]
    )

# Carregar dades (en producció vindria d'una base de dades)
@st.cache_data(ttl=300)  # caché de 5 minuts
def carregar_dades():
    import numpy as np
    np.random.seed(42)
    dates = pd.date_range('2024-01-01', '2024-12-31', freq='D')
    return pd.DataFrame({
        'data': dates,
        'vendes': np.random.lognormal(8, 0.5, len(dates)),
        'categoria': np.random.choice(["Electrònica", "Roba", "Llar", "Esports"], len(dates)),
        'unitats': np.random.randint(1, 50, len(dates))
    })

df = carregar_dades()
df_filtrat = df[
    (df['data'].dt.date >= data_inici) &
    (df['data'].dt.date <= data_fi) &
    (df['categoria'].isin(categories))
]

# KPIs en columnes
col1, col2, col3, col4 = st.columns(4)
col1.metric("Vendes Totals", f"{df_filtrat['vendes'].sum():,.0f}€",
            delta="+12.3% vs any anterior")
col2.metric("Comandes", f"{len(df_filtrat):,}",
            delta="+8.7%")
col3.metric("Tiquet Mig", f"{df_filtrat['vendes'].mean():.0f}€",
            delta="+3.2%")
col4.metric("Unitats Venudes", f"{df_filtrat['unitats'].sum():,}",
            delta="+15.1%")

# Gràfics
col1, col2 = st.columns([2, 1])

with col1:
    fig_temporal = px.line(
        df_filtrat.groupby('data')['vendes'].sum().reset_index(),
        x='data', y='vendes',
        title='Evolució Temporal de Vendes',
        template='plotly_white'
    )
    st.plotly_chart(fig_temporal, use_container_width=True)

with col2:
    fig_cat = px.pie(
        df_filtrat.groupby('categoria')['vendes'].sum().reset_index(),
        names='categoria', values='vendes',
        title='Per Categoria', hole=0.3
    )
    st.plotly_chart(fig_cat, use_container_width=True)

Power BI: l'estàndard empresarial

Power BI, de Microsoft, és la plataforma de Business Intelligence més usada a les empreses espanyoles el 2025. Disposa de:

  • Power BI Desktop: eina gratuïta per a Windows per crear informes i dashboards
  • Power BI Service: plataforma cloud per publicar, compartir i col·laborar
  • Power BI Mobile: aplicació per a iOS i Android
  • Power BI Embedded: API per integrar dashboards en aplicacions pròpies

Conceptes clau de Power BI

Model de dades: Power BI usa un motor columnar en memòria (VertiPaq) que comprimeix i indexa les dades automàticament. L'eficiència del model depèn del disseny de les relacions.

Relacions: les taules es connecten per camps comuns. Power BI suporta relacions 1:N (les més comunes en Star Schema), N:N (amb taules pont) i 1:1.

DAX (Data Analysis Expressions): el llenguatge de fórmules de Power BI, inspirat en Excel però molt més potent:

-- Mesures DAX bàsiques

-- Suma total de vendes
Total Vendes = SUM(vendes[import_total])

-- Nombre de comandes úniques
Num Comandes = DISTINCTCOUNT(vendes[id_comanda])

-- Tiquet mig
Tiquet Mig = DIVIDE([Total Vendes], [Num Comandes], 0)

-- Vendes Any Anterior (YOY)
Vendes Any Anterior =
CALCULATE(
    [Total Vendes],
    DATEADD(calendar[Data], -1, YEAR)
)

-- Creixement Any Anterior (%)
Creixement % =
DIVIDE(
    [Total Vendes] - [Vendes Any Anterior],
    [Vendes Any Anterior],
    0
)

-- Vendes Acumulades en l'Any (YTD)
Vendes YTD =
TOTALYTD([Total Vendes], calendar[Data])

-- Vendes filtrades per categoria (exemple de context)
Vendes Electronica =
CALCULATE(
    [Total Vendes],
    productes[categoria] = "Electrònica"
)

-- Rang de productes per vendes
Rang Producte =
RANKX(
    ALL(productes[nom_producte]),
    [Total Vendes],
    ,
    DESC,
    DENSE
)

Power Query (M): el motor d'ETL integrat de Power BI per transformar i netejar les dades abans de carregar-les al model:

// Exemple de transformació en Power Query (M)
let
    Origen = Csv.Document(
        File.Contents("C:\dades\vendes_2024.csv"),
        [Delimiter=",", Encoding=65001, QuoteStyle=QuoteStyle.None]
    ),
    Capçaleres = Table.PromoteHeaders(Origen),
    TipusCorrectes = Table.TransformColumnTypes(Capçaleres, {
        {"id_comanda", Int64.Type},
        {"data_comanda", type date},
        {"import", type number}
    }),
    EliminarNuls = Table.SelectRows(TipusCorrectes,
        each [import] <> null and [import] > 0),
    AfegirColumnaAny = Table.AddColumn(EliminarNuls, "any_comanda",
        each Date.Year([data_comanda]), Int64.Type),
    AfegirColMes = Table.AddColumn(AfegirColumnaAny, "nom_mes",
        each Date.ToText([data_comanda], "MMMM yyyy", "ca-ES"), type text)
in
    AfegirColMes

Apache Superset: la alternativa open source

Apache Superset és la plataforma de BI open source de referència, originalment creada per Airbnb. Disponible com a aplicació Docker:

# docker-compose.yml per a Apache Superset
version: '3.8'
services:
  superset:
    image: apache/superset:latest
    container_name: superset-alumne
    ports:
      - "8088:8088"
    environment:
      - SUPERSET_SECRET_KEY=clau-secreta-molt-segura-2025
    volumes:
      - superset_home:/app/superset_home
    command: >
      bash -c "
        superset db upgrade &&
        superset fab create-admin \
          --username admin \
          --firstname Admin \
          --lastname Superset \
          --email admin@superset.com \
          --password admin &&
        superset init &&
        superset run -p 8088 --with-threads --reload --debugger
      "

volumes:
  superset_home:
# Iniciar Superset
docker compose up -d

# Accedir a http://localhost:8088
# Usuari: admin / Contrasenya: admin

Grafana: visualització en temps real

Grafana és l'estàndard per a la monitorització de sistemes i la visualització de dades en temps real. Es connecta a desenes de fonts de dades (Prometheus, InfluxDB, PostgreSQL, Elasticsearch, Kafka...).

# docker-compose.yml per a Grafana + InfluxDB
version: '3.8'
services:
  influxdb:
    image: influxdb:2.7
    container_name: influxdb-alumne
    ports:
      - "8086:8086"
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=admin
      - DOCKER_INFLUXDB_INIT_PASSWORD=password123
      - DOCKER_INFLUXDB_INIT_ORG=iabd
      - DOCKER_INFLUXDB_INIT_BUCKET=telemetria
      - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=token-admin-secret
    volumes:
      - influxdb_data:/var/lib/influxdb2

  grafana:
    image: grafana/grafana:10.3.0
    container_name: grafana-alumne
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - influxdb

volumes:
  influxdb_data:
  grafana_data:
# Escriure dades de telemetria a InfluxDB
from influxdb_client import InfluxDBClient, Point, WritePrecision
from influxdb_client.client.write_api import SYNCHRONOUS
import random
import time
from datetime import datetime

client = InfluxDBClient(
    url="http://localhost:8086",
    token="token-admin-secret",
    org="iabd"
)
write_api = client.write_api(write_options=SYNCHRONOUS)

# Simular telemetria d'un servidor durant 60 segons
for i in range(60):
    point = Point("metriques_servidor") \
        .tag("host", "servidor-web-01") \
        .tag("entorn", "produccio") \
        .field("cpu_percent", random.uniform(20, 90)) \
        .field("memoria_mb", random.uniform(2048, 7168)) \
        .field("requests_per_second", random.randint(50, 500)) \
        .time(datetime.utcnow(), WritePrecision.MILLISECONDS)

    write_api.write(bucket="telemetria", record=point)
    print(f"Escrit punt {i+1}/60")
    time.sleep(1)

client.close()

BI + IA: el futur ja és present

El 2025, les eines de BI principals han integrat capacitats d'IA generativa:

Power BI Copilot: permet fer preguntes en llenguatge natural ("Mostra'm les vendes d'electrònica el mes passat comparades amb el mateix mes de l'any anterior") i el sistema genera automàticament la visualització i la mesura DAX corresponent.

Tableau Pulse: monitorització proactiva que identifica anomalies en les mètriques i envia alertes amb explicacions en llenguatge natural als responsables del negoci.

ThoughtSpot: plataforma basada completament en cerca en llenguatge natural (NLP) sobre les dades, sense necessitat de crear dashboards manuals.

DuckDB + Ollama (BI local): el 2025, és possible fer anàlisi de dades conversacional completament local (sense núvol) usant DuckDB com a motor analític i un model de llenguatge local (Llama 3, Mistral) per interpretar les preguntes i generar SQL automàticament.

Miniactivitat: Storytelling amb dades

Busca un dataset públic interessant (OpenData Barcelona a opendata-ajuntament.barcelona.cat, o INE a ine.es) sobre un tema que et motivi. Crea una presentació de 3 diapositives que expliqui una història amb les dades: (1) el context i la pregunta, (2) les dades i la visualització principal, (3) les conclusions i recomanacions accionables. Usa Plotly o Power BI per crear les visualitzacions.


Exercici pràctic

Consulta el fitxer practiques/practica_powerbi.md per a la pràctica completa de creació d'un dashboard de BI professional amb Power BI Desktop, que inclou:

  1. Connexió a dades reals (CSV de l'INE, Open Data)
  2. Transformació al Power Query
  3. Model dimensional (Star Schema)
  4. Mesures DAX per a KPIs de negoci
  5. Dashboard amb 6 visualitzacions interactives
  6. Publicació a Power BI Service