La plataforma de datos de AlpinaShop funciona. Los pedidos entran solos, Dataflow los limpia, Spark calcula la cesta media, Data Fusion trae el fichero del transportista y Workflows lo coordina todo cada noche con su comprobación de calidad. Técnicamente es un éxito.

Y sin embargo, tres cosas siguen sin funcionar.

Nadie sabe qué hay. En alpinashop_analitica hay dieciséis tablas, seis vistas, dos vistas materializadas y una llamada pedidos_evento que nadie recuerda por qué existe. Cuando alguien de marketing pregunta "¿dónde está el dato de conversión?", la respuesta es "pregúntale a Lucía". Y si Lucía se va de vacaciones, la respuesta es "no lo sé".

Nadie sabe qué significa. La columna ventas_eur aparece en cuatro tablas. ¿Incluye IVA? ¿Descuenta devoluciones? ¿Cuenta los pedidos cancelados? Marta cree que sí, Lucía cree que no, y el informe que va a dirección usa una de las dos definiciones sin decir cuál. Nadie lo ha escrito nunca.

Nadie sabe quién puede ver qué. En 04-01 se protegió email_cliente con una etiqueta de política. Pero desde entonces han pasado seis lecciones: Dataflow escribe en pedidos_streaming, la suscripción de Pub/Sub vuelca en pedidos_evento, el archivo de eventos va a Cloud Storage y Data Fusion trajo el CSV del transportista con el nombre del destinatario. ¿Hay emails de clientes en alguno de esos sitios sin protección? Nadie lo ha comprobado.

Y hay una cuarta cosa que falta, la que dirección lleva pidiendo desde el módulo 3: el cuadro de mando. Los datos están impecables y nadie los ve.

En esta lección cerramos el módulo resolviendo las cuatro. Verás qué es el gobierno del dato y por qué una pyme lo necesita tanto como una multinacional, organizarás y documentarás la plataforma con Dataplex, definirás reglas de calidad que se comprueban solas, descubrirás y desidentificarás los datos personales con Sensitive Data Protection, y construirás por fin en Looker Studio el cuadro de mando de AlpinaShop, evitando el error de permisos que convierte un informe compartido en una fuga de datos.

Contenido

  1. Qué es el gobierno del dato y por qué también en una pyme
  2. Dataplex: lagos, zonas y activos
  3. El catálogo universal y la búsqueda de datos
  4. Etiquetas y plantillas: documentar alpinashop_analitica
  5. Perfiles de datos: qué hay realmente en tus tablas
  6. Reglas de calidad y qué hacer cuando fallan
  7. Linaje automático de extremo a extremo
  8. Sensitive Data Protection: descubrir dónde están los datos personales
  9. Desidentificación: enmascarado y tokenización
  10. Looker Studio: conectar y modelar
  11. El cuadro de mando de dirección de AlpinaShop
  12. Coste y rendimiento: extracción, consulta viva y BI Engine
  13. Permisos: el error clásico de las credenciales del propietario
  14. Looker y LookML: cuándo justifica su precio
  15. Checklist de una plataforma de datos sana

  1. Qué es el gobierno del dato y por qué también en una pyme

El gobierno del dato es el conjunto de prácticas que garantizan que los datos de una organización sean encontrables, comprensibles, fiables, seguros y gestionados a lo largo de su vida. Cinco patas:

Pata Pregunta que responde Herramienta en Google Cloud
Catálogo ¿Qué datos tenemos y dónde están? Dataplex Universal Catalog
Linaje ¿De dónde viene este dato y qué depende de él? Dataplex Lineage
Calidad ¿Puedo fiarme de este número? Dataplex Data Quality
Clasificación ¿Qué datos son sensibles y quién los ve? Sensitive Data Protection + etiquetas de política
Ciclo de vida ¿Cuánto tiempo lo guardamos y cuándo se borra? Políticas de retención, TTL, ciclo de vida de Cloud Storage

La objeción habitual es que esto es cosa de bancos y multinacionales. Es exactamente al revés, y merece argumentarse:

El RGPD no distingue por tamaño. Una empresa de 40 personas que trata datos de clientes europeos tiene las mismas obligaciones que una de 40.000: saber qué datos personales tiene, dónde están, con qué base legal, cuánto tiempo los conserva, y poder atender un derecho de supresión. Si no se sabe dónde está el email de un cliente, no se puede borrar cuando lo pida.

El coste del desconocimiento es proporcionalmente mayor. En una empresa grande hay redundancia: si alguien se va, otro sabe. En AlpinaShop, si Lucía se va, el conocimiento de la plataforma se va con ella. La documentación no es burocracia, es continuidad.

Los errores llegan antes a dirección. Con tres personas y un cuadro de mando, un dato mal calculado no pasa por ningún filtro intermedio. Va directo a una decisión.

Y el momento correcto es ahora. Gobernar dieciséis tablas es un trabajo de tarde. Gobernar doscientas, después de tres años de crecimiento desordenado, es un proyecto de meses. La deuda de gobierno se acumula con intereses.

  1. Dataplex: lagos, zonas y activos

Dataplex es la capa de gestión unificada de datos de Google Cloud. Su idea central: los datos de una organización están repartidos entre BigQuery, Cloud Storage y otros sistemas, y hace falta una capa lógica por encima que los organice, los describa y les aplique políticas comunes sin moverlos.

Su jerarquía:

Nivel Qué es En AlpinaShop
Lago (lake) Dominio de datos; agrupa todo lo de un ámbito alpinashop-lago-comercial
Zona (zone) Subdivisión por grado de refinamiento zona-cruda, zona-curada
Activo (asset) Recurso concreto: un bucket o un dataset El bucket del lago, el dataset analítico

Las zonas tienen dos tipos con semántica distinta:

  • Raw: datos en su formato original, sin validar. Cualquier formato.
  • Curated: datos ya limpios y estructurados, con esquema. Solo formatos estructurados (Parquet, Avro, ORC) o tablas de BigQuery. Dataplex valida que lo que se registra aquí cumple.
flowchart TD
    L["Lago: alpinashop-lago-comercial"]
    Z1["Zona RAW: zona-cruda<br/>datos tal como llegan"]
    Z2["Zona CURATED: zona-curada<br/>datos limpios y modelados"]
    A1["Activo: gs://alpinashop-datalake<br/>eventos, exportaciones, cuarentena"]
    A2["Activo: gs://alpinashop-catalogo<br/>imagenes y exportaciones"]
    A3["Activo: BQ alpinashop_analitica<br/>pedidos, lineas, productos, visitas"]
    A4["Activo: gs://alpinashop-datalake/resultados<br/>Parquet de Spark"]

    L --> Z1 --> A1
    Z1 --> A2
    L --> Z2 --> A3
    Z2 --> A4

Creación:

gcloud config set project alpinashop-datos
gcloud services enable dataplex.googleapis.com \
  datacatalog.googleapis.com datalineage.googleapis.com

# 1) El lago: el dominio de datos comercial de AlpinaShop
gcloud dataplex lakes create alpinashop-lago-comercial \
  --location=europe-west1 \
  --display-name="Lago comercial de AlpinaShop" \
  --description="Pedidos, catalogo, navegacion y logistica" \
  --labels=entorno=produccion,equipo=datos,centro-coste=analitica

# 2) Zona cruda: lo que llega tal cual
gcloud dataplex zones create zona-cruda \
  --location=europe-west1 --lake=alpinashop-lago-comercial \
  --type=RAW \
  --resource-location-type=SINGLE_REGION \
  --display-name="Datos en crudo" \
  --discovery-enabled \
  --discovery-schedule="0 3 * * *"

# 3) Zona curada: lo que ya esta limpio y modelado
gcloud dataplex zones create zona-curada \
  --location=europe-west1 --lake=alpinashop-lago-comercial \
  --type=CURATED \
  --resource-location-type=SINGLE_REGION \
  --display-name="Datos curados" \
  --discovery-enabled \
  --discovery-schedule="0 4 * * *"

# 4) Activos
gcloud dataplex assets create activo-datalake \
  --location=europe-west1 --lake=alpinashop-lago-comercial --zone=zona-cruda \
  --resource-type=STORAGE_BUCKET \
  --resource-name=projects/alpinashop-datos/buckets/alpinashop-datalake \
  --discovery-enabled

gcloud dataplex assets create activo-analitica \
  --location=europe-west1 --lake=alpinashop-lago-comercial --zone=zona-curada \
  --resource-type=BIGQUERY_DATASET \
  --resource-name=projects/alpinashop-datos/datasets/alpinashop_analitica \
  --discovery-enabled

La opción --discovery-enabled es la que aporta valor inmediato: Dataplex rastrea automáticamente los activos, infiere esquemas de los ficheros del bucket, detecta particiones por la estructura de carpetas, y crea tablas externas de BigQuery para que esos ficheros sean consultables con SQL sin que nadie las declare.

Para AlpinaShop eso significa que los Parquet que el trabajo de Spark deja en gs://alpinashop-datalake/resultados/cesta/ aparecen como tabla consultable sin trabajo adicional. Es descubrimiento automático de datos, y es la razón práctica de registrar los buckets aunque el gobierno formal no interese.

  1. El catálogo universal y la búsqueda de datos

El Universal Catalog de Dataplex —heredero de Data Catalog— indexa automáticamente los metadatos de BigQuery, Cloud Storage, Pub/Sub, Spanner y Bigtable de toda la organización. Y sobre ese índice hay un buscador.

La sintaxis de búsqueda:

# Todo lo que mencione 'pedido'
pedido

# Solo tablas de BigQuery
type=TABLE system=BIGQUERY pedido

# Por columna: donde hay un campo que se llame email
column:email

# Por etiqueta de negocio
tag:sensibilidad.nivel=alto

# Por proyecto y dataset
parent:alpinashop-datos.alpinashop_analitica

# Combinado: tablas con columna de importe en el dataset analitico
type=TABLE parent:alpinashop_analitica column:importe

Desde la línea de comandos:

# Buscar todas las columnas llamadas 'email' en la organizacion
gcloud data-catalog search "column:email" \
  --include-project-ids=alpinashop-datos,alpinashop-prod \
  --format="table(relativeResourceName, searchResultSubtype)"

Esa consulta responde en segundos a una pregunta que sin catálogo requiere abrir tabla por tabla: ¿dónde hay emails? Es el primer paso del apartado 8.

La respuesta a la pregunta de marketing —"¿dónde está el dato de conversión?"— pasa de "pregúntale a Lucía" a buscar conversion en el catálogo y encontrar agg_embudo_dia con su descripción. Ese cambio es todo el valor del catálogo.

  1. Etiquetas y plantillas: documentar alpinashop_analitica

El catálogo indexa lo que existe, pero no sabe lo que significa. Para eso están las etiquetas (tags), que añaden metadatos de negocio a tablas y columnas, estructurados según una plantilla.

Primero la plantilla, que define qué campos se documentan y con qué valores permitidos:

cat > plantilla-gobierno.json <<'EOF'
{
  "displayName": "Gobierno del dato AlpinaShop",
  "fields": {
    "propietario": {
      "displayName": "Propietario del dato",
      "type": {"primitiveType": "STRING"},
      "isRequired": true,
      "description": "Persona o equipo responsable de este conjunto de datos"
    },
    "dominio": {
      "displayName": "Dominio de negocio",
      "type": {"enumType": {"allowedValues": [
        {"displayName": "ventas"},
        {"displayName": "catalogo"},
        {"displayName": "logistica"},
        {"displayName": "marketing"},
        {"displayName": "finanzas"}
      ]}},
      "isRequired": true
    },
    "sensibilidad": {
      "displayName": "Nivel de sensibilidad",
      "type": {"enumType": {"allowedValues": [
        {"displayName": "publico"},
        {"displayName": "interno"},
        {"displayName": "confidencial"},
        {"displayName": "datos-personales"}
      ]}},
      "isRequired": true
    },
    "frescura_esperada": {
      "displayName": "Frescura esperada",
      "type": {"enumType": {"allowedValues": [
        {"displayName": "tiempo-real"},
        {"displayName": "diaria"},
        {"displayName": "semanal"},
        {"displayName": "mensual"}
      ]}},
      "isRequired": true
    },
    "retencion_meses": {
      "displayName": "Retencion en meses",
      "type": {"primitiveType": "DOUBLE"},
      "isRequired": true
    },
    "apto_para_informes": {
      "displayName": "Apto para informes de direccion",
      "type": {"primitiveType": "BOOL"},
      "isRequired": true,
      "description": "Falso en tablas de staging, temporales o experimentales"
    },
    "definicion_negocio": {
      "displayName": "Definicion de negocio",
      "type": {"primitiveType": "STRING"},
      "isRequired": false,
      "description": "Que significa exactamente este dato, en lenguaje de negocio"
    }
  }
}
EOF

gcloud data-catalog tag-templates create gobierno_alpinashop \
  --location=europe-west1 \
  --display-name="Gobierno del dato AlpinaShop" \
  --field-file=plantilla-gobierno.json

Y ahora se etiqueta cada tabla:

# Tabla de pedidos: dato personal, diaria, apta para informes
gcloud data-catalog tags create \
  --entry-group=@bigquery \
  --entry="//bigquery.googleapis.com/projects/alpinashop-datos/datasets/alpinashop_analitica/tables/pedidos" \
  --tag-template=gobierno_alpinashop \
  --tag-template-location=europe-west1 \
  --tag-file=- <<'EOF'
{
  "propietario": "[email protected]",
  "dominio": "ventas",
  "sensibilidad": "datos-personales",
  "frescura_esperada": "diaria",
  "retencion_meses": 84,
  "apto_para_informes": true,
  "definicion_negocio": "Cabecera de pedido confirmado. total_pedido INCLUYE IVA y gastos de envio, y NO descuenta devoluciones posteriores; para ventas netas usar agg_ventas_categoria_dia."
}
EOF

Ese campo definicion_negocio es la solución al segundo problema del inicio de la lección. Una frase escrita una vez elimina para siempre la discusión sobre si total_pedido lleva IVA. Y queda donde se busca, no en un documento que nadie abre.

La disciplina de etiquetado que AlpinaShop adopta:

Tabla Sensibilidad Apto para informes Nota
pedidos datos-personales Contiene email_cliente
lineas_pedido interno Sin datos personales
productos confidencial coste_compra es margen comercial
visitas datos-personales cliente_id es seudónimo
pedidos_staging interno No Tabla de trabajo del DAG
pedidos_evento datos-personales No Volcado crudo de Pub/Sub
agg_* interno Agregados, sin identificación

La columna apto para informes es la más útil en el día a día: distingue lo que puede alimentar un cuadro de mando de lo que es fontanería. Sin ella, alguien acabará construyendo un informe de dirección sobre pedidos_staging, que se vacía cada noche.

  1. Perfiles de datos: qué hay realmente en tus tablas

Antes de definir reglas de calidad hay que saber cómo son los datos. El perfilado de Dataplex analiza una tabla y produce estadísticas por columna: valores nulos, distintos, mínimos, máximos, medias, percentiles y los valores más frecuentes.

gcloud dataplex datascans create data-profile perfil-pedidos \
  --location=europe-west1 \
  --data-source-resource="//bigquery.googleapis.com/projects/alpinashop-datos/datasets/alpinashop_analitica/tables/pedidos" \
  --display-name="Perfil de la tabla de pedidos" \
  --on-demand \
  --data-profile-spec-sampling-percent=100

gcloud dataplex datascans run perfil-pedidos --location=europe-west1
gcloud dataplex datascans describe perfil-pedidos --location=europe-west1 --view=FULL

Un perfil típico revela cosas que nadie sospechaba:

Columna Nulos Distintos Hallazgo
pedido_id 0 % 100 % Correcto: identificador único
cliente_id 12 % 8.400 Compras como invitado. ¿Se sabía?
email_cliente 0 % 8.900 Dato personal en todas las filas
canal 0 % 5 Hay 5 valores, no 3: aparecen WEB y Web
estado 0 % 6 Correcto
total_pedido 0 % mín. -45,00; máx. 4.120,00
envio.pais 3 % 14 Hay pedidos sin país

Tres hallazgos accionables en una sola pasada: el 12 % de compras sin cliente identificado (que puede ser normal o puede ser un fallo del registro), la inconsistencia de mayúsculas en canal que rompe cualquier GROUP BY, y un total_pedido negativo, que probablemente sea un abono mal modelado y que está distorsionando todas las medias.

Ese último es el ejemplo perfecto de por qué el perfilado va antes que las reglas: la regla "total nunca negativo" no se inventa, se descubre.

  1. Reglas de calidad y qué hacer cuando fallan

Con el perfil en la mano se definen las reglas. Un escaneo de calidad ejecuta un conjunto de comprobaciones y da un resultado de aprobado o fallido con el detalle por regla.

# calidad-pedidos.yaml
rules:
  # --- Completitud ---
  - column: pedido_id
    nonNullExpectation: {}
    dimension: COMPLETENESS
    threshold: 1.0                  # el 100 % de las filas debe cumplir
    name: pedido-id-obligatorio
    description: "Todo pedido debe tener identificador"

  - column: fecha_pedido
    nonNullExpectation: {}
    dimension: COMPLETENESS
    threshold: 1.0
    name: fecha-obligatoria

  # --- Unicidad ---
  - column: pedido_id
    uniquenessExpectation: {}
    dimension: UNIQUENESS
    threshold: 1.0
    name: pedido-id-unico
    description: "Detecta duplicados por reprocesos mal hechos"

  # --- Validez de rango ---
  - column: total_pedido
    rangeExpectation:
      minValue: "0"
      maxValue: "10000"
      strictMinEnabled: false
    dimension: VALIDITY
    threshold: 0.999                # se tolera un 0,1 % de excepciones
    name: total-en-rango
    description: "Importes no negativos y por debajo de 10.000 EUR"

  # --- Conjunto de valores permitidos ---
  - column: estado
    setExpectation:
      values: ["confirmado", "enviado", "entregado", "devuelto", "cancelado"]
    dimension: VALIDITY
    threshold: 1.0
    name: estado-valido

  - column: canal
    setExpectation:
      values: ["web", "movil", "telefono"]
    dimension: VALIDITY
    threshold: 1.0
    name: canal-normalizado
    description: "Detecta 'WEB' y 'Web' sin normalizar"

  # --- Formato con expresion regular ---
  - column: email_cliente
    regexExpectation:
      regex: "^[^@\\s]+@[^@\\s]+\\.[a-zA-Z]{2,}$"
    dimension: VALIDITY
    threshold: 0.99
    name: email-con-formato-valido
    ignoreNull: true

  # --- Regla SQL a nivel de fila ---
  - sqlAssertion:
      sqlStatement: |
        SELECT pedido_id, subtotal, descuento, iva, total_pedido
        FROM ${data()}
        WHERE ABS(total_pedido - (subtotal - IFNULL(descuento,0)
                                  + IFNULL(iva,0))) > 0.01
    dimension: ACCURACY
    name: cuadre-de-importes
    description: "El total debe cuadrar con sus componentes (tolerancia 1 centimo)"

  # --- Regla SQL agregada: frescura ---
  - sqlAssertion:
      sqlStatement: |
        SELECT 1 FROM (
          SELECT MAX(fecha_pedido) AS ultima FROM ${data()}
        ) WHERE DATE_DIFF(CURRENT_DATE(), ultima, DAY) > 2
    dimension: FRESHNESS
    name: datos-frescos
    description: "Debe haber pedidos de los ultimos 2 dias"
gcloud dataplex datascans create data-quality calidad-pedidos \
  --location=europe-west1 \
  --data-source-resource="//bigquery.googleapis.com/projects/alpinashop-datos/datasets/alpinashop_analitica/tables/pedidos" \
  --display-name="Calidad de la tabla de pedidos" \
  --data-quality-spec-file=calidad-pedidos.yaml \
  --schedule="0 6 * * *" \
  --export-results-table="//bigquery.googleapis.com/projects/alpinashop-datos/datasets/alpinashop_analitica/tables/resultados_calidad"

Las seis dimensiones de calidad estándar, con su interpretación en AlpinaShop:

Dimensión Pregunta Ejemplo
Completitud ¿Falta algo? Pedidos sin sku
Unicidad ¿Hay duplicados? El mismo pedido_id dos veces tras un reproceso
Validez ¿Los valores son posibles? estado fuera de la lista, importe negativo
Exactitud ¿Cuadran entre sí? total ≠ subtotal - descuento + iva
Consistencia ¿Coinciden entre sistemas? Pedidos en BigQuery frente a Cloud SQL
Frescura ¿Están actualizados? Sin pedidos nuevos desde hace 3 días

Qué hacer cuando una regla falla. Esta es la parte que más se descuida, y sin ella el escaneo es decorativo:

Gravedad Ejemplo Acción
Crítica Duplicados, frescura rota Bloquear: el cuadro de mando no debe actualizarse con esos datos
Alta Importes que no cuadran Alerta inmediata al propietario, investigación en el día
Media canal sin normalizar Ticket de corrección en el origen; el pipeline lo normaliza mientras tanto
Baja Un 0,3 % de emails con formato raro Informe semanal; puede ser aceptable

La integración con el orquestador de 04-06 cierra el círculo:

from airflow.providers.google.cloud.operators.dataplex import (
    DataplexRunDataQualityScanOperator,
)

calidad = DataplexRunDataQualityScanOperator(
    task_id="escaneo_calidad_pedidos",
    project_id="alpinashop-datos",
    region="europe-west1",
    data_scan_id="calidad-pedidos",
    asynchronous=False,
    fail_on_dq_failure=True,   # si falla la calidad, FALLA EL DAG
)

consolidar_pedidos >> calidad >> lanzar_dataflow

Con fail_on_dq_failure=True, un fallo de calidad detiene el proceso nocturno antes de propagar datos malos al cuadro de mando. Es la versión industrializada de la comprobación de calidad que escribimos a mano en 04-06.

  1. Linaje automático de extremo a extremo

En 04-05 vimos el linaje de Data Fusion, limitado a sus pipelines. Dataplex Lineage lo hace de forma transversal: captura automáticamente el linaje de BigQuery (cada consulta que escribe en una tabla), Dataflow, Data Fusion, Composer y Dataproc.

gcloud services enable datalineage.googleapis.com

Y ya está: a partir de ahí, cada trabajo registra su linaje solo.

El grafo completo de AlpinaShop, que Dataplex construye sin que nadie lo dibuje:

flowchart LR
    CS["Cloud SQL<br/>alpinashop-pedidos"]
    GCS["gs://alpinashop-datalake<br/>exportaciones"]
    PS["Pub/Sub<br/>pedidos-nuevos"]
    ERP["MySQL ERP<br/>Sabadell"]
    ST["BQ pedidos_staging"]
    PE["BQ pedidos"]
    LP["BQ lineas_pedido"]
    CE["BQ costes_producto_erp"]
    AG["BQ agg_ventas_categoria_dia"]
    MV["MV mv_ventas_diarias_sku"]
    LS["Looker Studio<br/>Cuadro de mando"]

    CS -->|export| GCS -->|GCSToBigQuery| ST -->|MERGE| PE
    PS -->|Dataflow| PE
    PS -->|suscripcion BQ| PE
    ERP -->|Datastream| CE
    PE --> LP
    LP --> AG
    CE --> AG
    AG --> MV --> LS

Las preguntas que este grafo responde en un clic:

  • "El margen del informe sale raro. ¿De dónde viene?"agg_ventas_categoria_dialineas_pedido + costes_producto_erp ← Datastream ← MySQL del ERP. En cuatro saltos se llega al origen.
  • "Vamos a cambiar el esquema del ERP. ¿Qué se rompe?" → Todo lo que cuelga de costes_producto_erp, incluido el cuadro de mando de dirección.
  • "Un cliente ejerce su derecho de supresión. ¿Dónde está su dato?" → Se rastrea pedidos hacia adelante y aparecen todas las tablas derivadas.
  • "¿Podemos apagar la exportación nocturna de Cloud SQL?" → El grafo muestra que pedidos_staging depende de ella. No.

Esa última pregunta es la que más dinero ahorra en la práctica: saber qué se puede apagar. Toda plataforma de datos con dos años acumula procesos que ya no alimentan nada, y sin linaje nadie se atreve a tocarlos.

  1. Sensitive Data Protection: descubrir dónde están los datos personales

Aviso de RGPD. Este apartado y el siguiente tratan datos personales. Las herramientas que se describen son controles técnicos, no un dictamen jurídico. Determinar la base legal de un tratamiento, los plazos de conservación, la necesidad de una evaluación de impacto o si una seudonimización es suficiente corresponde a un profesional de compliance o al delegado de protección de datos, y debe hacerse antes de poner nada en producción. Todos los datos de este curso son ficticios.

Sensitive Data Protection —el servicio antes conocido como Cloud DLP— hace dos cosas: inspeccionar para descubrir dónde hay datos sensibles, y desidentificar para que dejen de serlo.

Empecemos por la inspección, que responde al tercer problema del inicio de la lección: ¿hay emails de clientes en sitios donde no debería haberlos?

gcloud services enable dlp.googleapis.com

Un trabajo de inspección sobre todo el dataset:

{
  "inspectJob": {
    "storageConfig": {
      "bigQueryOptions": {
        "tableReference": {
          "projectId": "alpinashop-datos",
          "datasetId": "alpinashop_analitica",
          "tableId": "pedidos_evento"
        },
        "sampleMethod": "RANDOM_START",
        "rowsLimitPercent": 10
      }
    },
    "inspectConfig": {
      "infoTypes": [
        {"name": "EMAIL_ADDRESS"},
        {"name": "PHONE_NUMBER"},
        {"name": "STREET_ADDRESS"},
        {"name": "PERSON_NAME"},
        {"name": "IBAN_CODE"},
        {"name": "CREDIT_CARD_NUMBER"},
        {"name": "ES_NIF_NUMBER"},
        {"name": "IP_ADDRESS"}
      ],
      "minLikelihood": "LIKELY",
      "includeQuote": false,
      "limits": {"maxFindingsPerRequest": 1000}
    },
    "actions": [
      {
        "saveFindings": {
          "outputConfig": {
            "table": {
              "projectId": "alpinashop-datos",
              "datasetId": "alpinashop_analitica",
              "tableId": "hallazgos_dlp"
            }
          }
        }
      },
      {"publishSummaryToCscc": {}}
    ]
  }
}
curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  "https://dlp.googleapis.com/v2/projects/alpinashop-datos/locations/europe-west1/dlpJobs" \
  -d @inspeccion-pedidos-evento.json

Dos decisiones deliberadas en esa configuración:

  • includeQuote: false: los hallazgos no incluyen el valor encontrado. Si lo pusieras en true, la tabla de hallazgos contendría los emails reales, es decir, habrías creado una copia sin proteger de los datos que intentas proteger. Es un error clásico y grave.
  • rowsLimitPercent: 10: para descubrir si hay datos sensibles no hace falta escanear el 100 %. Un muestreo del 10 % encuentra el problema y cuesta la décima parte.

Y una consulta sobre los hallazgos:

-- Donde hay datos personales y de que tipo
SELECT
  info_type.name                        AS tipo_dato,
  location.container_name               AS tabla,
  location.content_locations[SAFE_OFFSET(0)]
    .record_location.field_id.name      AS columna,
  likelihood,
  COUNT(*)                              AS hallazgos
FROM `alpinashop-datos.alpinashop_analitica.hallazgos_dlp`
GROUP BY tipo_dato, tabla, columna, likelihood
ORDER BY hallazgos DESC;

Resultado típico en AlpinaShop, y por qué duele:

Tipo Tabla Columna Comentario
EMAIL_ADDRESS pedidos email_cliente Esperado: ya protegido con etiqueta de política en 04-01
EMAIL_ADDRESS pedidos_evento payload No esperado: la suscripción de Pub/Sub vuelca el JSON crudo
PERSON_NAME envios incidencia No esperado: el transportista escribe nombres en texto libre
PHONE_NUMBER opiniones texto No esperado: clientes que dejan su teléfono en la opinión
IP_ADDRESS visitas dispositivo No esperado: una IP es dato personal según el RGPD

Cuatro de los cinco hallazgos son sorpresas. Ninguno es fruto de mala fe: son consecuencias naturales de mover datos. La suscripción de BigQuery de 04-04 vuelca el mensaje entero. El transportista escribe lo que quiere en el campo de incidencia. Los clientes ponen su teléfono en una opinión. Y una IP, que parece técnica, es dato personal.

Esto es exactamente lo que ninguna organización sabe sin inspeccionar. Y es la razón por la que este apartado existe.

Para hacerlo continuo, Dataplex ofrece perfilado de datos sensibles a nivel de organización, que escanea automáticamente todo BigQuery y mantiene un inventario actualizado de dónde hay datos sensibles y con qué nivel de riesgo, sin lanzar trabajos a mano.

  1. Desidentificación: enmascarado y tokenización

Descubierto el problema, hay que actuar. Las técnicas, de menor a mayor utilidad conservada:

Técnica Qué hace ¿Reversible? Cuándo usarla
Supresión Elimina el valor No El dato no se necesita para nada
Enmascarado [email protected]***@correo.com No Se necesita el dominio, no la persona
Sustitución Reemplaza por una constante No Solo hace falta saber que había algo
Hash cripto HMAC-SHA256 con clave en KMS No Contar clientes distintos sin saber quiénes
Tokenización determinista (FPE) Token con el formato original , con la clave Cruzar entre sistemas y poder revertir
Generalización 34 años → 30-39; 0801308 No Análisis demográfico o geográfico
Desplazamiento de fechas Desplaza todas las fechas de un sujeto Series temporales sin fechas reales

La distinción crítica es entre hash y tokenización determinista:

  • El hash con clave produce el mismo resultado para el mismo valor, así que permite contar y agrupar clientes distintos, pero no se puede volver atrás. Es lo correcto para analítica pura.
  • La tokenización determinista con preservación de formato produce un token que parece un email y se puede revertir con la clave. Es lo correcto cuando un sistema autorizado necesita recuperar el valor original.

Configuración de desidentificación para AlpinaShop:

{
  "deidentifyTemplate": {
    "displayName": "Desidentificacion analitica AlpinaShop",
    "description": "Aplica a las tablas expuestas a analisis y a Looker Studio",
    "deidentifyConfig": {
      "recordTransformations": {
        "fieldTransformations": [
          {
            "fields": [{"name": "email_cliente"}],
            "primitiveTransformation": {
              "cryptoHashConfig": {
                "cryptoKey": {
                  "kmsWrapped": {
                    "wrappedKey": "CiQA...",
                    "cryptoKeyName": "projects/alpinashop-prod/locations/europe-west1/keyRings/alpinashop-keyring/cryptoKeys/clave-pedidos"
                  }
                }
              }
            }
          },
          {
            "fields": [{"name": "codigo_postal"}],
            "primitiveTransformation": {
              "characterMaskConfig": {
                "maskingCharacter": "X",
                "numberToMask": 3,
                "reverseOrder": true
              }
            }
          },
          {
            "fields": [{"name": "texto_opinion"}],
            "infoTypeTransformations": {
              "transformations": [
                {
                  "infoTypes": [
                    {"name": "EMAIL_ADDRESS"},
                    {"name": "PHONE_NUMBER"},
                    {"name": "PERSON_NAME"}
                  ],
                  "primitiveTransformation": {
                    "replaceWithInfoTypeConfig": {}
                  }
                }
              ]
            }
          }
        ]
      }
    }
  }
}

Qué hace cada transformación:

  • email_cliente → hash con clave envuelta en KMS. Reutiliza clave-pedidos del keyring alpinashop-keyring creado en 03-06. La clave nunca sale de KMS, así que ni siquiera quien tenga acceso a la tabla puede revertir el hash. Lucía sigue pudiendo contar clientes únicos con COUNT(DISTINCT email_hash).
  • codigo_postal → enmascarado de los 3 últimos. 08013 se convierte en 08XXX. Se conserva la provincia, que es lo que sirve para el análisis geográfico, y se pierde la calle, que es lo que identifica.
  • texto_opinion → sustitución por tipo dentro del texto libre. "Llamadme al 611223344, soy Ana" se convierte en "Llamadme al [PHONE_NUMBER], soy [PERSON_NAME]". La opinión sigue siendo analizable —incluido el análisis de sentimiento del módulo 5— sin datos personales dentro.

La última es la más valiosa y la que ninguna otra herramienta hace bien: desidentificar dentro de texto libre, donde el dato personal no está en una columna sino incrustado en una frase.

Y la vista que expone Lucía y el cuadro de mando:

-- Vista desidentificada: es la UNICA que se conecta a Looker Studio
CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.v_pedidos_analitica` AS
SELECT
  pedido_id,
  fecha_pedido,
  momento_pedido,
  cliente_id,
  canal,
  estado,
  -- Provincia sí, direccion no
  envio.pais                                   AS pais,
  SUBSTR(envio.codigo_postal, 1, 2)            AS provincia_cp,
  metodo_pago,
  subtotal, descuento, iva, total_pedido
FROM `alpinashop-datos.alpinashop_analitica.pedidos`;
-- Sin email_cliente, sin ciudad, sin codigo postal completo.
-- Es una VISTA AUTORIZADA (04-01): quien la consulta NO necesita
-- permiso sobre la tabla base, y por tanto no puede saltarsela.

El principio operativo: los datos personales existen en la tabla base, protegida y con acceso restringido a gcp-seguridad@; todo lo demás —analítica, cuadros de mando, exploración— consume vistas desidentificadas. La minimización no es una limpieza puntual, es una arquitectura.

  1. Looker Studio: conectar y modelar

Looker Studio (antes Data Studio) es la herramienta gratuita de visualización de Google. Se conecta a BigQuery y a decenas de orígenes, y permite construir informes interactivos sin código.

Hay tres formas de conectarlo a BigQuery, y elegir bien determina el coste y el rendimiento:

Modo Qué hace Coste Cuándo
Tabla/vista Consulta viva contra la tabla Cada interacción cuesta Datos que cambian y se filtran mucho
Consulta personalizada SQL propio como origen Cada interacción ejecuta ese SQL Modelado complejo; cuidado con el coste
Extracción Copia los datos en la caché de Looker Studio Casi cero Volúmenes pequeños, refresco diario

La regla de oro: nunca conectes Looker Studio directamente a una tabla de detalle grande. Conéctalo a una tabla agregada o a una vista materializada. Un cuadro de mando con seis gráficos que consulta lineas_pedido ejecuta seis consultas cada vez que alguien cambia un filtro, y con diez usuarios refrescando, la factura de BigQuery se dispara —exactamente el escenario del ejercicio 3 de 04-01.

Para AlpinaShop, el origen del cuadro de mando será una tabla preparada por el proceso nocturno:

-- Tabla base del cuadro de mando: pequena, agregada, lista para consumir
CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.bi_resumen_diario`
PARTITION BY dia
CLUSTER BY canal, pais AS
SELECT
  p.fecha_pedido                                    AS dia,
  p.canal,
  p.envio.pais                                      AS pais,
  pr.categoria,
  COUNT(DISTINCT p.pedido_id)                       AS pedidos,
  COUNT(DISTINCT p.cliente_id)                      AS clientes,
  SUM(l.cantidad)                                   AS unidades,
  ROUND(SUM(l.importe_linea), 2)                    AS ventas_eur,
  ROUND(SUM(l.cantidad * c.coste_medio_eur), 2)     AS coste_eur,
  COUNTIF(p.estado = 'devuelto')                    AS pedidos_devueltos
FROM `alpinashop-datos.alpinashop_analitica.pedidos`               AS p
JOIN `alpinashop-datos.alpinashop_analitica.lineas_pedido`         AS l
  ON l.pedido_id = p.pedido_id AND l.fecha_pedido = p.fecha_pedido
JOIN `alpinashop-datos.alpinashop_analitica.productos`             AS pr USING (sku)
LEFT JOIN `alpinashop-datos.alpinashop_analitica.costes_producto_erp` AS c USING (sku)
WHERE p.fecha_pedido >= DATE_SUB(CURRENT_DATE(), INTERVAL 800 DAY)
  AND p.estado NOT IN ('cancelado')
GROUP BY dia, p.canal, pais, pr.categoria;

Esa tabla tiene unos pocos miles de filas —dos años × 3 canales × 14 países × 6 categorías— frente a los millones de lineas_pedido. Consultarla es prácticamente gratis.

Los campos calculados de Looker Studio se definen sobre el origen y se comportan como columnas:

# Ticket medio
SUM(ventas_eur) / NULLIF(SUM(pedidos), 0)

# Margen bruto en euros
SUM(ventas_eur) - SUM(coste_eur)

# Margen porcentual
100 * (SUM(ventas_eur) - SUM(coste_eur)) / NULLIF(SUM(ventas_eur), 0)

# Tasa de devolucion
100 * SUM(pedidos_devueltos) / NULLIF(SUM(pedidos), 0)

# Agrupacion de canales para una vista simplificada
CASE
  WHEN canal IN ('web','movil') THEN 'Digital'
  ELSE 'Otros'
END

Consejo de modelado: define los campos calculados en la vista de BigQuery, no en Looker Studio, siempre que puedas. Un campo definido en SQL está versionado en Git, es reutilizable por otros informes y por Spark, y no se pierde si alguien duplica el informe. Un campo definido en Looker Studio vive dentro de ese informe y nadie más lo ve.

  1. El cuadro de mando de dirección de AlpinaShop

Diseñemos el informe que dirección lleva pidiendo desde el módulo 3.

Estructura en tres páginas:

Pagina 1 — RESUMEN EJECUTIVO
+----------------------------------------------------------+
|  [Selector de fechas]  [Canal]  [Pais]  [Categoria]       |
+----------------------------------------------------------+
| Ventas mes  | Pedidos   | Ticket medio | Margen %         |
|  42.180 EUR |    387    |   108,99 EUR |   38,2 %         |
|  ^ +12,4 %  | ^ +8,1 %  |  ^ +4,0 %    |  v -1,2 pp       |
+----------------------------------------------------------+
| Evolucion de ventas (linea, 13 meses, con ano anterior)   |
+----------------------------------------------------------+
| Top 10 productos (barras)   | Ventas por categoria (aro)  |
+----------------------------------------------------------+

Pagina 2 — EMBUDO Y CONVERSION
+----------------------------------------------------------+
| Embudo: sesiones > vieron > carrito > pago > compra       |
+----------------------------------------------------------+
| Tasa de conversion (linea)  | Coste de adquisicion (linea)|
+----------------------------------------------------------+
| Conversion por canal y dispositivo (tabla con barras)     |
+----------------------------------------------------------+

Pagina 3 — PRODUCTO Y LOGISTICA
+----------------------------------------------------------+
| Margen por categoria (barras) | Productos sin ventas      |
+----------------------------------------------------------+
| Tasa de devolucion por categoria | Plazo medio de entrega |
+----------------------------------------------------------+

Los indicadores clave, con su definición explícita —que es lo que evita la discusión de "este número no es el que yo tenía":

Indicador Fórmula Definición que hay que escribir en el informe
Ventas del mes SUM(ventas_eur) Importe de líneas de pedido, IVA incluido, sin pedidos cancelados
Evolución Comparación con el periodo anterior Mismo número de días, no mes natural completo
Ticket medio ventas / pedidos Por pedido, no por línea
Top de productos SUM(unidades) por SKU Por unidades vendidas, no por importe
Tasa de conversión compras / sesiones Sesiones con actividad, no visitas totales
Coste de adquisición inversión / clientes nuevos Requiere el dato de inversión en marketing
Margen (ventas - coste) / ventas Coste del ERP; sin gastos generales ni logística

El coste de adquisición merece una nota honesta: requiere un dato que AlpinaShop no tiene todavía en la plataforma —la inversión mensual en publicidad—. Hay dos opciones: cargarlo como una tabla manual mantenida por marketing, o integrarlo desde la API de la plataforma publicitaria. La primera es la sensata para empezar. Y hay que mostrarlo en el informe indicando de dónde sale, porque un indicador cuyo origen nadie conoce acaba desacreditando al resto.

Buenas prácticas de diseño, que son las que separan un informe que se usa de uno que se abre una vez:

  • Lo más importante arriba a la izquierda. Es donde va la vista.
  • Cuatro tarjetas de indicador, no doce. Un cuadro de mando con veinte cifras no se lee: se ignora.
  • Comparación siempre. Un número sin contexto no informa. 42.180 € no dice nada; +12,4 % frente al mes anterior sí.
  • Un tipo de gráfico por pregunta: línea para evolución, barras para comparar categorías, tabla para el detalle. Nada de gráficos de tarta con doce porciones.
  • Filtros arriba y comunes a la página, no repartidos por los gráficos.
  • Colores con significado y accesibles: verde y rojo tienen sentido para bueno y malo, pero hay que asegurarse de que se distinguen también para daltónicos, añadiendo flechas o signos además del color.
  • Fecha de última actualización visible. Un informe sin ella genera desconfianza y llamadas.
  • Una nota metodológica al pie. Dónde se define cada indicador. Es la diferencia entre un informe y una discusión.

  1. Coste y rendimiento: extracción, consulta viva y BI Engine

Los tres mecanismos, en orden de coste creciente:

1. Extracción de datos. Looker Studio copia hasta un límite de datos en su propia caché y los sirve desde ahí. Refresco programable de hasta una vez al día. Coste de BigQuery prácticamente nulo, rendimiento excelente. Es la opción por defecto para un informe con datos agregados que no necesita ser de hoy mismo.

2. Consulta viva con caché. Looker Studio cachea los resultados durante un tiempo configurable (hasta 12 horas). Con la caché activada, diez personas mirando el mismo informe generan una consulta, no diez. Es lo que debe usarse en un cuadro de mando compartido.

3. BI Engine. Un servicio de aceleración en memoria de BigQuery. Se reserva una capacidad y las consultas que caben en ella se responden en milisegundos y sin coste de bytes escaneados.

# Reserva de BI Engine de 2 GB para el cuadro de mando
bq update --reservation --project_id=alpinashop-datos \
  --location=europe-west1 \
  --bi_reservation_size=2147483648

Con la tabla bi_resumen_diario, que ocupa unos pocos megabytes, 2 GB de BI Engine caben de sobra: el cuadro de mando entero se sirve desde memoria, con latencia de milisegundos y sin facturar escaneo. El coste de la reserva es del orden de unos pocos euros al mes, y suele salir más barato que pagar las consultas que evita.

Comprobar que BI Engine se está usando:

SELECT
  job_id,
  bi_engine_statistics.bi_engine_mode          AS modo,
  ROUND(total_bytes_billed / POW(1024,2), 2)   AS mb_facturados,
  TIMESTAMP_DIFF(end_time, start_time, MILLISECOND) AS ms
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
  AND job_type = 'QUERY'
  AND bi_engine_statistics IS NOT NULL
ORDER BY creation_time DESC
LIMIT 20;

Si modo es FULL, la consulta se resolvió íntegramente en memoria. Si es DISABLED o PARTIAL, el campo bi_engine_statistics.acceleration_mode explica por qué —normalmente porque la consulta usa una función no soportada o la tabla no cabe.

La receta completa de AlpinaShop, resumida: tabla agregada pequeña + caché activada + BI Engine + informe conectado a la vista desidentificada. Coste mensual del cuadro de mando: unos pocos euros. Sin esa disciplina, los mismos gráficos costarían cientos.

  1. Permisos: el error clásico de las credenciales del propietario

Aquí está el error más peligroso de toda la lección, y ocurre constantemente porque la interfaz lo pone fácil.

Cuando se crea un origen de datos en Looker Studio hay que elegir con qué credenciales se ejecutan las consultas:

Modo Cómo funciona Consecuencia
Credenciales del propietario Todas las consultas usan los permisos de quien creó el origen Quien ve el informe accede a los datos con los permisos del propietario, aunque él no tenga ninguno
Credenciales del espectador Cada consulta usa los permisos de quien mira Cada persona ve lo que sus permisos de BigQuery le permiten

El escenario del desastre, paso a paso:

  1. Lucía crea el cuadro de mando conectado a BigQuery con credenciales del propietario. Es lo cómodo, y es lo que la interfaz sugiere.
  2. Lucía tiene, además de acceso a los agregados, permiso de lectura sobre pedidos completo.
  3. Lucía comparte el informe con "cualquier persona con el enlace" para que un proveedor externo vea un gráfico.
  4. Ese proveedor —o cualquiera a quien reenvíen el enlace— puede explorar los datos con los permisos de Lucía. Puede añadir campos, cambiar dimensiones y, si el origen es una tabla con datos personales, verlos.
  5. Nadie en BigQuery le ha dado ningún permiso a esa persona. Los permisos de BigQuery no lo protegen, porque las consultas no se hacen en su nombre.

Eso es una brecha de datos personales con obligación de notificación bajo el RGPD.

Las reglas que AlpinaShop adopta, sin excepción:

  1. El origen apunta siempre a una vista desidentificada y autorizada, nunca a una tabla con datos personales. Aunque el modo de credenciales sea el equivocado, no hay nada sensible detrás. Esta es la defensa que no depende de que nadie se acuerde.
  2. Credenciales del espectador en cualquier informe que salga del equipo de datos.
  3. Nunca "cualquier persona con el enlace" para informes con datos de negocio. Compartir siempre con personas o grupos concretos (gcp-datos@, direccion@).
  4. Revisión trimestral de con quién están compartidos los informes. Los permisos se acumulan solos.
  5. Si hace falta credenciales del propietario —porque los espectadores no tienen ni deben tener permisos de BigQuery—, entonces el origen tiene que ser una vista agregada sin ningún dato personal ni confidencial. Sin negociación.

Verificación desde BigQuery de quién está consultando desde Looker Studio:

SELECT
  user_email,
  COUNT(*)                                      AS consultas,
  ROUND(SUM(total_bytes_billed)/POW(1024,3), 2) AS gb,
  MIN(creation_time)                            AS primera,
  MAX(creation_time)                            AS ultima
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  AND job_type = 'QUERY'
  AND (labels.requestor = 'looker_studio'
       OR STRPOS(IFNULL(query, ''), 'Looker Studio') > 0)
GROUP BY user_email
ORDER BY consultas DESC;

Si en esa lista aparece una sola dirección de correo —la de Lucía— cuando el informe lo miran quince personas, estás en modo credenciales del propietario. Es la comprobación de treinta segundos que conviene hacer hoy.

  1. Looker y LookML: cuándo justifica su precio

Looker (sin "Studio") es un producto distinto y mucho más caro. Su diferencia esencial es LookML, un lenguaje de modelado donde se define una sola vez qué significa cada métrica y cada dimensión, y todos los informes de la empresa consumen esas definiciones.

# modelo.lkml — la definicion vive en UN sitio, versionada en Git
view: pedidos {
  sql_table_name: `alpinashop-datos.alpinashop_analitica.bi_resumen_diario` ;;

  dimension_group: dia {
    type: time
    timeframes: [date, week, month, quarter, year]
    sql: ${TABLE}.dia ;;
  }

  dimension: canal { type: string  sql: ${TABLE}.canal ;; }

  measure: ventas_eur {
    type: sum
    sql: ${TABLE}.ventas_eur ;;
    value_format_name: eur
    description: "Importe de lineas de pedido, IVA incluido, sin cancelados"
  }

  measure: pedidos { type: sum  sql: ${TABLE}.pedidos ;; }

  measure: ticket_medio {
    type: number
    sql: ${ventas_eur} / NULLIF(${pedidos}, 0) ;;
    value_format_name: eur
    description: "Ventas dividido entre numero de pedidos"
  }
}
Criterio Looker Studio Looker
Coste Gratuito (Pro tiene coste) Miles de € al mes
Modelado centralizado No: cada informe define lo suyo Sí, LookML
Versionado en Git No Sí, nativo
Coherencia de métricas Depende de la disciplina Garantizada por diseño
Permisos por fila Vía BigQuery Nativo en el modelo
API y contenido embebido Limitado Completo
Curva de aprendizaje Horas Semanas

Cuándo justifica su precio: cuando el coste de la incoherencia supera el coste de la licencia. Es decir, cuando hay decenas de analistas creando informes, cuando "ventas" significa cosas distintas en tres departamentos y las reuniones se dedican a discutir de quién es el número bueno, o cuando se necesita gobierno del modelo semántico con revisión por pull request.

Para AlpinaShop, Looker Studio es la respuesta obvia: tres personas, un cuadro de mando, presupuesto de pyme. La coherencia se garantiza con la disciplina del apartado 10 —definir las métricas en vistas de BigQuery versionadas en Git— que es el 80 % del valor de LookML por el 0 % del precio.

  1. Checklist de una plataforma de datos sana

Organización y descubrimiento

  • [ ] Todos los datasets y buckets registrados en un lago de Dataplex con sus zonas.
  • [ ] Descubrimiento automático activado en los activos de Cloud Storage.
  • [ ] Toda tabla con description y con etiquetas de gobierno.
  • [ ] Distinción explícita entre tablas aptas para informes y tablas de fontanería.
  • [ ] Cada tabla con un propietario nombrado.

Calidad

  • [ ] Perfilado ejecutado sobre las tablas principales.
  • [ ] Reglas de calidad definidas para completitud, unicidad, validez, exactitud y frescura.
  • [ ] Escaneos programados diariamente.
  • [ ] El orquestador bloquea el proceso si falla una regla crítica.
  • [ ] Resultados históricos exportados a BigQuery para ver la tendencia.

Linaje

  • [ ] API de linaje activada.
  • [ ] Grafo revisado tras cada cambio de arquitectura.
  • [ ] Procesos sin consumidores identificados y apagados.

Seguridad y privacidad

  • [ ] Inspección de datos sensibles ejecutada sobre todo el dataset, no solo donde se sospecha.
  • [ ] Columnas con datos personales etiquetadas con política de acceso.
  • [ ] Vistas desidentificadas como única superficie de exposición analítica.
  • [ ] Ninguna herramienta de BI conectada a una tabla con datos personales.
  • [ ] Claves de hash y tokenización en KMS, nunca en el código.
  • [ ] Revisión por un profesional de compliance o el DPO antes de producción.

Coste

  • [ ] Tablas particionadas y clusterizadas; require_partition_filter en las grandes.
  • [ ] Cuadros de mando conectados a agregados, nunca a tablas de detalle.
  • [ ] Caché de informes activada; BI Engine si compensa.
  • [ ] Presupuestos y alertas por etiqueta de centro de coste.
  • [ ] Nada permanentemente encendido que no lo necesite: sin clústeres de Dataproc olvidados, sin instancias de Data Fusion encendidas, sin pipelines de streaming huérfanos.
  • [ ] Auditoría periódica de consultas caras con INFORMATION_SCHEMA.

Operación

  • [ ] Todos los procesos orquestados, ninguno en un cron de una VM.
  • [ ] Tareas idempotentes; reprocesos seguros.
  • [ ] Alertas que llegan a una persona concreta, no a un correo genérico.
  • [ ] Código —DDL, DAG, pipelines, flujos— versionado en Git.
  • [ ] Documentación de qué hace cada proceso y a quién avisar.

Errores Comunes y Consejos

Documentar solo al principio. Un catálogo con etiquetas de hace un año es peor que ninguno, porque genera confianza injustificada. Documentar debe ser parte de crear la tabla, no un proyecto aparte.

Poner reglas de calidad sin perfilar antes. Se acaban definiendo umbrales inventados que fallan a diario, todo el mundo ignora las alertas, y el sistema deja de servir. Perfila, mira los datos reales, y luego define.

Reglas de calidad que no bloquean nada. Si un fallo de calidad no detiene el proceso ni avisa a nadie, es decoración.

Inspeccionar con includeQuote: true. Se crea una tabla de hallazgos que contiene los datos sensibles que se querían proteger. Es empeorar el problema.

Confundir hash con anonimización. Un hash sin sal ni clave de un email es reversible por fuerza bruta —el espacio de emails plausibles no es tan grande—. Sigue siendo dato personal a efectos del RGPD. La clave debe estar en KMS.

Conectar Looker Studio a una tabla de detalle. Cada filtro dispara consultas sobre millones de filas. Es el camino directo a una factura sorpresa.

Credenciales del propietario más enlace público. El error más grave de la lección. Cualquiera con el enlace consulta con los permisos del propietario.

No poner la fecha de actualización en el informe. Genera desconfianza, llamadas y decisiones basadas en datos de la semana pasada creyendo que son de hoy.

Definir las métricas en Looker Studio en vez de en BigQuery. Se quedan encerradas en ese informe, no se versionan, y al duplicarlo divergen.

Consejo: escribe la definición de negocio de cada métrica y publícala. La mitad de las discusiones sobre datos son discusiones sobre definiciones, no sobre números.

Consejo: pon un enlace al catálogo en el pie del cuadro de mando. Quien quiera saber qué significa un número, que pueda llegar solo.

Consejo: haz la inspección de datos sensibles hoy, no cuando haya un incidente. Cuesta una tarde y casi siempre encuentra algo que nadie sabía.

Ejercicios

Ejercicio 1: gobernar la tabla de opiniones

Para la tabla alpinashop_analitica.opiniones: (a) escribe el comando que la registra como activo en la zona curada del lago; (b) crea la etiqueta de gobierno indicando propietario, dominio, sensibilidad, frescura, retención, aptitud para informes y una definición de negocio precisa; (c) define un escaneo de calidad con al menos cinco reglas que cubran completitud, unicidad, validez de rango, conjunto de valores y una regla SQL propia; (d) explica qué acción tomarías ante el fallo de cada regla.

Ejercicio 2: inspección y desidentificación de opiniones

El campo texto de opiniones es texto libre escrito por clientes. Diseña: (a) el trabajo de inspección que descubra qué tipos de datos personales contiene, con las opciones correctas para no crear una copia de los datos sensibles; (b) la plantilla de desidentificación que permita conservar el texto analizable —para el análisis de sentimiento del módulo 5— eliminando los datos personales; (c) la vista que se expondría a Looker Studio; (d) la advertencia de RGPD que acompañaría la propuesta antes de llevarla a producción.

Ejercicio 3: rediseñar un cuadro de mando problemático

El cuadro de mando de dirección lleva dos meses funcionando y hay cuatro problemas. Uno: tarda 40 segundos en cargar y el coste de BigQuery del proyecto ha pasado de 12 € a 310 € al mes. Dos: INFORMATION_SCHEMA muestra que todas las consultas del informe las hace [email protected], aunque lo miran doce personas. Tres: el informe está compartido como "cualquier persona con el enlace" porque hubo que enseñárselo a un consultor externo. Cuatro: dirección dice que el número de ventas del informe no coincide con el de contabilidad.

Diagnostica cada problema, indica su gravedad relativa y propón la solución concreta con el orden de actuación.

Soluciones

Solución 1

(a) Registrar el activo. La tabla ya está dentro del dataset alpinashop_analitica, que se registró completo como activo activo-analitica en zona-curada. Por tanto no hay que registrar la tabla individualmente: Dataplex la descubre al rastrear el dataset. Si se quisiera un activo separado —por ejemplo, para un dataset distinto— sería:

gcloud dataplex assets create activo-opiniones \
  --location=europe-west1 \
  --lake=alpinashop-lago-comercial --zone=zona-curada \
  --resource-type=BIGQUERY_DATASET \
  --resource-name=projects/alpinashop-datos/datasets/alpinashop_opiniones \
  --discovery-enabled

gcloud dataplex assets describe activo-opiniones \
  --location=europe-west1 --lake=alpinashop-lago-comercial --zone=zona-curada

(b) Etiqueta de gobierno.

gcloud data-catalog tags create \
  --entry-group=@bigquery \
  --entry="//bigquery.googleapis.com/projects/alpinashop-datos/datasets/alpinashop_analitica/tables/opiniones" \
  --tag-template=gobierno_alpinashop \
  --tag-template-location=europe-west1 \
  --tag-file=- <<'EOF'
{
  "propietario": "[email protected]",
  "dominio": "catalogo",
  "sensibilidad": "datos-personales",
  "frescura_esperada": "diaria",
  "retencion_meses": 36,
  "apto_para_informes": false,
  "definicion_negocio": "Opiniones de clientes sobre productos. puntuacion es de 1 a 5, entera. El campo texto es LIBRE y puede contener datos personales incrustados (nombres, telefonos): NO exponer directamente; usar la vista v_opiniones_analitica, ya desidentificada. Una opinion corresponde a un cliente y un SKU; un cliente puede opinar varias veces del mismo producto."
}
EOF

sensibilidad: datos-personales y apto_para_informes: false son las dos decisiones clave: aunque el campo texto parezca inocuo, es texto libre y por tanto imprevisible.

(c) Escaneo de calidad.

# calidad-opiniones.yaml
rules:
  - column: opinion_id
    nonNullExpectation: {}
    dimension: COMPLETENESS
    threshold: 1.0
    name: opinion-id-obligatorio

  - column: opinion_id
    uniquenessExpectation: {}
    dimension: UNIQUENESS
    threshold: 1.0
    name: opinion-id-unico

  - column: puntuacion
    rangeExpectation:
      minValue: "1"
      maxValue: "5"
    dimension: VALIDITY
    threshold: 1.0
    name: puntuacion-de-1-a-5

  - column: pais
    setExpectation:
      values: ["ES","FR","PT","IT","DE","AD","BE","NL","AT","CH","GB","IE","LU","PL"]
    dimension: VALIDITY
    threshold: 0.99
    name: pais-en-lista

  - column: sku
    nonNullExpectation: {}
    dimension: COMPLETENESS
    threshold: 1.0
    name: sku-obligatorio

  # Regla SQL: integridad referencial contra el catalogo
  - sqlAssertion:
      sqlStatement: |
        SELECT o.opinion_id, o.sku
        FROM ${data()} AS o
        LEFT JOIN `alpinashop-datos.alpinashop_analitica.productos` AS p
          ON p.sku = o.sku
        WHERE p.sku IS NULL
    dimension: CONSISTENCY
    name: sku-existe-en-catalogo

  # Regla SQL: frescura
  - sqlAssertion:
      sqlStatement: |
        SELECT 1 FROM (SELECT MAX(fecha) AS ultima FROM ${data()})
        WHERE DATE_DIFF(CURRENT_DATE(), ultima, DAY) > 7
    dimension: FRESHNESS
    name: opiniones-recientes
gcloud dataplex datascans create data-quality calidad-opiniones \
  --location=europe-west1 \
  --data-source-resource="//bigquery.googleapis.com/projects/alpinashop-datos/datasets/alpinashop_analitica/tables/opiniones" \
  --data-quality-spec-file=calidad-opiniones.yaml \
  --schedule="0 6 * * *" \
  --export-results-table="//bigquery.googleapis.com/projects/alpinashop-datos/datasets/alpinashop_analitica/tables/resultados_calidad"

(d) Acción ante cada fallo:

Regla Gravedad Acción
opinion-id-obligatorio Crítica Bloquear el proceso. Un identificador nulo rompe la idempotencia del MERGE
opinion-id-unico Crítica Bloquear e investigar: indica reproceso mal hecho, exactamente el fallo de 04-06
puntuacion-de-1-a-5 Alta Alerta y cuarentena de esas filas. Una puntuación de 9 falsea todas las medias
pais-en-lista Media Ticket al equipo web. Puede ser un país nuevo legítimo: revisar y ampliar la lista
sku-obligatorio Alta Alerta: una opinión sin producto no es analizable
sku-existe-en-catalogo Media Investigar: producto descatalogado y borrado del maestro, o error de escritura
opiniones-recientes Alta Alerta al equipo de plataforma: probablemente el pipeline de ingesta está roto y nadie lo sabe

Solución 2

(a) Inspección.

{
  "inspectJob": {
    "storageConfig": {
      "bigQueryOptions": {
        "tableReference": {
          "projectId": "alpinashop-datos",
          "datasetId": "alpinashop_analitica",
          "tableId": "opiniones"
        },
        "identifyingFields": [{"name": "opinion_id"}],
        "sampleMethod": "RANDOM_START",
        "rowsLimitPercent": 20
      }
    },
    "inspectConfig": {
      "infoTypes": [
        {"name": "EMAIL_ADDRESS"},
        {"name": "PHONE_NUMBER"},
        {"name": "PERSON_NAME"},
        {"name": "STREET_ADDRESS"},
        {"name": "ES_NIF_NUMBER"},
        {"name": "IBAN_CODE"},
        {"name": "CREDIT_CARD_NUMBER"},
        {"name": "URL"}
      ],
      "minLikelihood": "POSSIBLE",
      "includeQuote": false,
      "limits": {"maxFindingsPerRequest": 3000}
    },
    "actions": [
      {"saveFindings": {"outputConfig": {"table": {
        "projectId": "alpinashop-datos",
        "datasetId": "alpinashop_analitica",
        "tableId": "hallazgos_dlp_opiniones"
      }}}}
    ]
  }
}

Las tres decisiones que se evalúan:

  • includeQuote: false: sin esto, la tabla hallazgos_dlp_opiniones contendría los teléfonos y nombres reales encontrados. Se habría creado una segunda copia sin proteger de exactamente lo que se quiere proteger.
  • minLikelihood: POSSIBLE en lugar de LIKELY: en texto libre escrito por personas, los datos aparecen en formatos irregulares ("llamadme al seis once..."). Un umbral bajo genera más falsos positivos, que es preferible a no detectar un dato real. En una columna estructurada se usaría LIKELY.
  • identifyingFields: permite saber qué opinión contiene el hallazgo sin guardar el valor, lo que hace el resultado accionable.

(b) Plantilla de desidentificación.

{
  "deidentifyTemplate": {
    "displayName": "Desidentificacion de opiniones",
    "deidentifyConfig": {
      "recordTransformations": {
        "fieldTransformations": [
          {
            "fields": [{"name": "texto"}],
            "infoTypeTransformations": {
              "transformations": [
                {
                  "infoTypes": [
                    {"name": "PERSON_NAME"}, {"name": "PHONE_NUMBER"},
                    {"name": "EMAIL_ADDRESS"}, {"name": "STREET_ADDRESS"},
                    {"name": "ES_NIF_NUMBER"}, {"name": "IBAN_CODE"},
                    {"name": "CREDIT_CARD_NUMBER"}
                  ],
                  "primitiveTransformation": {
                    "replaceWithInfoTypeConfig": {}
                  }
                }
              ]
            }
          }
        ]
      }
    }
  }
}

Por qué replaceWithInfoTypeConfig y no supresión. Sustituye cada hallazgo por su tipo entre corchetes:

  • Original: "Muy comoda. Si teneis dudas escribidme a [email protected] o al 611223344, soy Ana Ruiz"
  • Resultado: "Muy comoda. Si teneis dudas escribidme a [EMAIL_ADDRESS] o al [PHONE_NUMBER], soy [PERSON_NAME]"

El texto sigue siendo analizable. La opinión conserva su estructura, su longitud aproximada y —lo importante para el módulo 5— su sentimiento: "Muy cómoda" sigue ahí. Con supresión pura se perdería la coherencia sintáctica y el análisis de sentimiento se degradaría.

(c) Vista expuesta.

CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.v_opiniones_analitica` AS
SELECT
  opinion_id,
  sku,
  fecha,
  puntuacion,
  texto_desidentificado                              AS texto,
  pais,
  CASE WHEN puntuacion >= 4 THEN 'positiva'
       WHEN puntuacion = 3  THEN 'neutra'
       ELSE 'negativa' END                           AS valoracion,
  LENGTH(texto_desidentificado)                      AS longitud_texto
FROM `alpinashop-datos.alpinashop_analitica.opiniones_desidentificadas`;

El pipeline nocturno escribe en opiniones_desidentificadas aplicando la plantilla, y la tabla original opiniones queda con acceso restringido a gcp-seguridad@. Looker Studio, Lucía y los futuros modelos del módulo 5 consumen exclusivamente la vista.

(d) Advertencia de RGPD.

Las opiniones de clientes contienen datos personales, tanto en campos estructurados (pais combinado con sku y fecha puede ser reidentificable si el volumen es bajo) como incrustados en texto libre. Esta propuesta aplica dos medidas técnicas: desidentificación automática del texto mediante Sensitive Data Protection y restricción de acceso a la tabla original.

Antes de pasar a producción es necesario que un profesional de compliance o el delegado de protección de datos determine: la base legal del tratamiento analítico de las opiniones; si la desidentificación aplicada constituye anonimización efectiva o mera seudonimización —lo que determina si el RGPD sigue aplicando al resultado—; el plazo de conservación adecuado; el riesgo de reidentificación por combinación de campos aparentemente no identificativos; y si el uso previsto en el módulo 5 (análisis de sentimiento) requiere información adicional al interesado. La desidentificación automática no es infalible: un cliente que escriba "soy el que compró la mochila azul el martes en la tienda de Sabadell" no será detectado por ningún tipo de dato predefinido. Todos los datos de este curso son ficticios.

Solución 3

Gravedad relativa, de mayor a menor:

Problema 3 (enlace público) + Problema 2 (credenciales del propietario) = INCIDENTE DE SEGURIDAD. No son dos problemas: son uno solo y grave. Que todas las consultas las haga Lucía significa modo credenciales del propietario. Combinado con "cualquier persona con el enlace", cualquiera que tenga o reciba ese enlace consulta BigQuery con los permisos de Lucía, que incluyen la tabla pedidos con email_cliente. Si el consultor externo reenvió el enlace, o si acabó en un correo o un chat, hay una posible brecha de datos personales con obligación de notificación. Se actúa primero, hoy, en minutos.

Problema 4 (el número no cuadra con contabilidad) = CRISIS DE CONFIANZA. Es el segundo en gravedad porque destruye el valor de todo lo construido. Un cuadro de mando en el que dirección no confía deja de usarse, y el trabajo de siete lecciones se pierde.

Problema 1 (lento y caro) = OPERATIVO. 310 € al mes duelen, pero no comprometen ni la seguridad ni la confianza. Se arregla en una tarde.

Actuación en tres fases:

Fase 1 — Hoy, en minutos: cortar la exposición.

1. Cambiar la compartición a "Personas concretas": gcp-datos@ y direccion@.
   Eliminar "cualquier persona con el enlace".
2. Cambiar el origen de datos a CREDENCIALES DEL ESPECTADOR.
3. Comprobar a qué apunta el origen. Si es una tabla con email_cliente,
   repuntarlo a la vista desidentificada v_pedidos_analitica.
-- Comprobar el alcance real: quien ha consultado y cuanto, en 60 dias
SELECT
  user_email,
  COUNT(*)             AS consultas,
  MIN(creation_time)   AS primera,
  MAX(creation_time)   AS ultima,
  COUNT(DISTINCT DATE(creation_time)) AS dias_distintos
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 60 DAY)
  AND job_type = 'QUERY'
  AND STRPOS(IFNULL(query, ''), 'alpinashop_analitica') > 0
GROUP BY user_email
ORDER BY consultas DESC;

Y en paralelo: avisar al responsable de seguridad y al DPO. La valoración de si esto constituye una brecha notificable bajo el RGPD no la toma el equipo técnico. Hay que documentar qué datos eran accesibles, durante cuánto tiempo y a quién se compartió el enlace.

Fase 2 — Esta semana: recuperar la confianza en el número.

La discrepancia con contabilidad tiene tres causas candidatas, y hay que determinar cuál es antes de tocar nada:

-- Comparar las tres definiciones posibles de "ventas del mes"
SELECT
  'A) Bruto, IVA incluido, sin cancelados'      AS definicion,
  ROUND(SUM(total_pedido), 2)                   AS importe
FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido BETWEEN DATE '2026-03-01' AND DATE '2026-03-31'
  AND estado != 'cancelado'
UNION ALL
SELECT
  'B) Bruto sin IVA, sin cancelados',
  ROUND(SUM(subtotal - IFNULL(descuento,0)), 2)
FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido BETWEEN DATE '2026-03-01' AND DATE '2026-03-31'
  AND estado != 'cancelado'
UNION ALL
SELECT
  'C) Neto sin IVA, sin cancelados NI devueltos',
  ROUND(SUM(subtotal - IFNULL(descuento,0)), 2)
FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido BETWEEN DATE '2026-03-01' AND DATE '2026-03-31'
  AND estado NOT IN ('cancelado','devuelto');

Casi con total seguridad, contabilidad usa la definición C —ventas netas sin IVA— y el informe muestra la A. No es un error de datos: es un error de definición no documentada, exactamente el segundo problema del inicio de esta lección.

La corrección:

  1. Acordar con contabilidad una definición canónica y escribirla.
  2. Implementarla en la tabla bi_resumen_diario, exponiendo ambas magnitudes con nombres inequívocos: ventas_brutas_iva_incl y ventas_netas_sin_iva.
  3. Escribirla en la etiqueta definicion_negocio del catálogo de Dataplex.
  4. Añadir una nota metodológica visible al pie del cuadro de mando.

Fase 3 — Este mes: coste y rendimiento.

El diagnóstico primero:

SELECT
  SUBSTR(REGEXP_REPLACE(query, r'\s+', ' '), 1, 200) AS consulta,
  COUNT(*)                                           AS ejecuciones,
  ROUND(AVG(total_bytes_billed)/POW(1024,3), 2)      AS gb_por_ejecucion,
  ROUND(SUM(total_bytes_billed)/POW(1024,4), 2)      AS tb_totales,
  COUNTIF(cache_hit)                                 AS desde_cache
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH)
  AND job_type = 'QUERY'
GROUP BY consulta
ORDER BY tb_totales DESC
LIMIT 10;

El resultado esperado: el informe está conectado directamente a lineas_pedido o a pedidos, y cada uno de los ocho gráficos ejecuta su propia consulta cada vez que alguien mueve un filtro. Doce personas × varios filtros al día × ocho gráficos = miles de consultas sobre millones de filas.

Las cuatro correcciones, por orden de impacto:

  1. Repuntar el informe a bi_resumen_diario, la tabla agregada de unos pocos miles de filas que produce el proceso nocturno. Sola, esta medida suele dividir el coste por cien y bajar la carga de 40 segundos a menos de 3.
  2. Activar la caché del informe con 12 horas de vigencia. Doce personas mirando generan una consulta, no doce.
  3. Reservar 2 GB de BI Engine. La tabla agregada cabe entera en memoria: respuesta en milisegundos y cero bytes facturados.
  4. Poner una cuota diaria de bytes en el proyecto y una alerta de presupuesto sobre la etiqueta centro-coste:analitica, para que un problema equivalente se detecte a los 20 € y no a los 310 €.

Resultado esperado: de 310 €/mes a menos de 10 €, y de 40 segundos a menos de 3.

La lección transversal del ejercicio: los cuatro problemas eran evitables con lo que ya sabíamos. El coste, con lo de 04-01. Los permisos, con lo de 03-04 y el apartado 13. La discrepancia de definiciones, con el etiquetado del apartado 4. Ninguno era un fallo técnico difícil: los cuatro eran fallos de gobierno.

Conclusión

El módulo 4 se cierra aquí, y se cierra con la plataforma completa.

Has entendido qué es el gobierno del dato —catálogo, linaje, calidad, clasificación y ciclo de vida— y por qué una pyme lo necesita igual o más que una multinacional: el RGPD no distingue por tamaño, el conocimiento no tiene redundancia cuando el equipo son tres personas, los errores llegan a dirección sin filtros intermedios, y la deuda de gobierno se acumula con intereses.

Has organizado la plataforma en Dataplex: el lago alpinashop-lago-comercial con su zona cruda y su zona curada, con los buckets y el dataset registrados como activos y con descubrimiento automático que convierte los Parquet de Spark en tablas consultables sin declarar nada. Has usado el catálogo universal para responder en segundos a "¿dónde está el dato de conversión?" y a "¿dónde hay columnas que se llamen email". Y has documentado con una plantilla de etiquetas que obliga a declarar propietario, dominio, sensibilidad, frescura, retención, aptitud para informes y —lo más valioso— la definición de negocio, esa frase escrita una vez que zanja para siempre si total_pedido incluye IVA.

Has perfilado las tablas antes de juzgarlas, descubriendo lo que nadie sospechaba: un 12 % de pedidos sin cliente, cinco valores en canal donde debía haber tres, y un total_pedido negativo que distorsionaba todas las medias. Y sobre ese conocimiento has definido reglas de calidad en las seis dimensiones, con umbrales, con exportación de resultados y —esto es lo que las hace útiles— con una acción asignada a cada fallo y con integración en el orquestador para que un fallo crítico detenga el proceso antes de propagar datos malos.

Has activado el linaje automático y tienes el grafo completo desde Cloud SQL y el ERP de Sabadell hasta el cuadro de mando, capaz de responder de dónde viene un número, qué se rompe si cambias un esquema, dónde está el dato de un cliente que ejerce su derecho de supresión, y qué proceso se puede apagar sin miedo.

Has inspeccionado con Sensitive Data Protection y has encontrado lo que siempre se encuentra: emails en el volcado crudo de Pub/Sub, nombres en el campo de incidencias del transportista, teléfonos dentro de opiniones de clientes, e IP en los datos de navegación. Cuatro sorpresas de cinco hallazgos, ninguna por mala fe. Y has desidentificado con criterio: hash con clave de KMS para poder contar clientes sin saber quiénes, enmascarado del código postal conservando la provincia, y sustitución por tipo dentro del texto libre para que las opiniones sigan siendo analizables sin datos personales dentro. Con la advertencia expresa, repetida donde tocaba, de que esto son controles técnicos y de que la decisión jurídica corresponde a un profesional de compliance o al DPO.

Y has construido por fin el cuadro de mando que dirección pedía: conectado a una tabla agregada pequeña y no a la tabla de detalle, con caché, con BI Engine, con campos calculados definidos en SQL versionado y no encerrados en el informe, con cuatro indicadores y no doce, con comparación siempre, con fecha de actualización visible y con nota metodológica al pie. Y con la lección de permisos que vale por sí sola toda la lección: credenciales del espectador, compartición con personas concretas, y el origen apuntando siempre a una vista desidentificada, porque es la única defensa que no depende de que nadie se acuerde. Sabes cuándo Looker con LookML justifica su precio —cuando el coste de la incoherencia supera el de la licencia— y por qué AlpinaShop no está en ese caso.

Mira lo que existe ahora y que no existía hace siete lecciones. Los pedidos entran por Pub/Sub sin acoplar la tienda a nada. Dataflow los transforma en lote y en streaming, con el tiempo del evento bien entendido. Dataproc calcula lo algorítmico en clústeres que viven cuatro minutos. Data Fusion trae lo que llega sucio de fuera. BigQuery lo guarda y lo responde en segundos, particionado, clusterizado y sin arruinar a nadie. Workflows lo orquesta cada noche con comprobaciones que bloquean si algo no cuadra. Dataplex lo cataloga, lo mide y lo rastrea. Y Looker Studio lo enseña. AlpinaShop ha pasado de no saber qué mochila se vende más en Cataluña a tener una plataforma de datos gobernada por la que Lucía no le pide nada a nadie.

Pero fíjate en lo que todo esto tiene en común: describe lo que ya pasó. Cuenta las ventas de ayer, el embudo del mes, el margen del trimestre, qué productos se compraron juntos. Es memoria, y la memoria es valiosísima. No es predicción, y no es automatización.

Y AlpinaShop tiene ahora mismo cuatro preguntas que la memoria no puede responder. Cuando un cliente mete unos crampones en el carrito, ¿qué debería sugerirle la web en ese instante, aprovechando la matriz de coocurrencia que calculó Spark? Cuando entran 60 GB de imágenes nuevas al catálogo, ¿tiene que etiquetarlas alguien a mano o puede la máquina decir "esto es una mochila azul de 40 litros"? Con 8.000 opiniones en texto libre ya desidentificadas, ¿alguien las va a leer todas para saber qué falla en los frontales? Y con 2.400 fichas de producto que describir, ¿quién las escribe?

En el módulo 5, Aprendizaje automático e IA, AlpinaShop pasa de describir a predecir y automatizar. Empezaremos por 05-01, Vertex AI, la plataforma que unifica todo el ciclo de vida del modelo —datos, entrenamiento, evaluación, despliegue y monitorización— y que conectará directamente con el dataset alpinashop_analitica que acabas de gobernar. Después llegarán AutoML para entrenar sin escribir código, TensorFlow para cuando haga falta control total, las API de lenguaje natural y de visión que responden a las preguntas de las opiniones y de las imágenes sin entrenar nada, la IA generativa con Gemini para las descripciones, y MLOps con Vertex AI Pipelines para que un modelo no sea un experimento en el portátil de alguien sino un sistema de producción con la misma disciplina de idempotencia, calidad, linaje y coste que has aplicado en todo este módulo.

Los datos ya están limpios, gobernados, documentados y consultables. Eso no era el objetivo: era el requisito. Ahora empieza lo interesante.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados