Regional Junta de Andalucía

Junta de Andalucía — Contratos menores

Contratos menores publicados en la Plataforma de Contratación de la Junta de Andalucía. Un dataset CKAN por año (contratacion-menor-plataforma-de-contratacion-andalucia-AAAA), de 2018 a 2026, con un CSV y un JSON en cada uno.

Ficha técnica

Formato
CSV (separador «|») y JSON, un dataset CKAN por año
Registros
~769.000
Cobertura
2018 a junio de 2026
Actualización
Trimestral
Licencia
CC BY 4.0
Entidad
Junta de Andalucía

Descripción

La Junta de Andalucía publica en su portal de datos abiertos los contratos menores (artículo 118 de la Ley 9/2017) adjudicados y publicados en la Plataforma de Contratación de la Junta. No hay un conjunto único: cada año es un dataset CKAN distinto, de contratacion-menor-plataforma-de-contratacion-andalucia-2018 a contratacion-menor-plataforma-de-contratacion-andalucia-2026, y cada uno trae el mismo contenido en CSV y en JSON. Los publica el Servicio de Coordinación y Contratación Electrónica de la Consejería de Hacienda, con licencia CC BY 4.0 y actualización trimestral según la ficha.

En octubre de 2026 los nueve ficheros sumaban 768.648 filas: unas 50.500 en 2018 y cerca de 125.000 en 2025. El de 2026 llegaba hasta el 30 de junio; se actualizó por última vez el 24 de julio. Cada fila es un expediente, aunque en 2021 y 2022 hay identificadores repetidos.

Las 23 columnas son las mismas todos los años, pero el formato cambia de un fichero a otro y conviene tratarlos uno a uno:

- De 2018 a 2023 el CSV va comprimido en ZIP; desde 2024, sin comprimir (más de 200 MB en 2024 y 2025). - La codificación es Latin-1 hasta 2025 y UTF-8 en 2026. - Las fechas son ISO con zona horaria («2024-02-20T00:00:00+0100») hasta 2024 y DD/MM/AAAA desde 2025. Hay algunas imposibles (años 201, 1492 o 5384) y los ficheros de 2018 a 2023 incluyen adjudicaciones de años anteriores. - Los importes usan punto decimal, salvo en 2025, que usa coma. El importe sin IVA está en todas las filas; el importe con IVA falta en cerca del 90 % de las de 2022 y 2023, y el valor estimado casi no aparece hasta 2021. - Desde 2023 casi todos los campos vienen rellenos de espacios hasta un ancho fijo, y en 2022 la cabecera lleva un espacio delante de las dos fechas. Hay que recortar nombres y valores. - De 2022 a 2024 casi todos los NIF y nombres de adjudicatario terminan en «;», que es el separador cuando hay varios (en 2021 lo lleva una fila de cada ocho). Desde 2025 los varios adjudicatarios se separan con «; » y sin «;» final.

Las URL de descarga que devuelve la API apuntan a un servidor interno (gdc-pdpopendata-ckan.paas.junta-andalucia.es) que no resuelve desde fuera. La misma ruta funciona con www.juntadeandalucia.es, que es lo que hacen los ejemplos.

Estructura de datos

  • ID_EXPEDIENTE — Identificador interno del expediente
  • ORGANO_CONTRATACION — Órgano de contratación
  • NUM_EXPEDIENTE — Número de expediente
  • TITULO / DESCRIPCION — Título y objeto del contrato
  • TIPO_CONTRATO — Tipo (Servicios, Suministros, Obras…)
  • PROCEDIMIENTO_ADJUDICACION — Siempre MENOR
  • DURACION_CONTRATO / DURACION_MEDIDA — Duración y unidad (D, M…)
  • LUGAR_EJECUCION_CODIGO / LUGAR_EJECUCION_DENOMINACION — Código NUTS y nombre
  • VALOR_ESTIMADO — Valor estimado (EUR)
  • IMPORTE_ADJUDICACION_SIN_IVA / IMPORTE_ADJUDICACION_CON_IVA — Importe adjudicado (coma decimal en 2025; con IVA, casi vacío en 2022 y 2023)
  • FINANCIACION_EUROPEA / FINANCIADO_POR / TASA_COFINANCIACION — Fondos europeos
  • NUM_LICITADORES_PRESENTADOS — Número de ofertas
  • NIF_ADJUDICATARIO / ADJUDICATARIO_DENOMINACION — Adjudicatario
  • FECHA_ADJUDICACION / FECHA_FORMALIZACION — Fechas (ISO hasta 2024, DD/MM/AAAA desde 2025)
  • ESTADO — Estado del expediente (Resuelto, Adjudicado…)

Ejemplos de código

Listar los datasets anuales y sus CSV curl
# Un dataset por año; la API devuelve las URL con un host interno,
# así que se cambia por www.juntadeandalucia.es
curl -s "https://www.juntadeandalucia.es/datosabiertos/portal/api/3/action/package_search?q=name:contratacion-menor-plataforma-de-contratacion-andalucia-*&rows=50" \
  | python3 -c "
import json, sys
for p in sorted(json.load(sys.stdin)['result']['results'], key=lambda p: p['name']):
    for r in p['resources']:
        if r['format'] == 'CSV':
            print(p['name'][-4:], r['url'].replace('gdc-pdpopendata-ckan.paas.junta-andalucia.es', 'www.juntadeandalucia.es'))
"
Juntar todos los años en un DataFrame python
import io
import zipfile

import pandas as pd
import requests

API = 'https://www.juntadeandalucia.es/datosabiertos/portal/api/3/action'
INTERNAL_HOST = 'gdc-pdpopendata-ckan.paas.junta-andalucia.es'

res = requests.get(f'{API}/package_search', params={
    'q': 'name:contratacion-menor-plataforma-de-contratacion-andalucia-*', 'rows': 50,
}).json()

frames = []
for pkg in sorted(res['result']['results'], key=lambda p: p['name']):
    year = int(pkg['name'][-4:])
    csv = next(r for r in pkg['resources'] if r['format'] == 'CSV')
    raw = requests.get(csv['url'].replace(INTERNAL_HOST, 'www.juntadeandalucia.es')).content
    # De 2018 a 2023 el CSV viene dentro de un ZIP
    if raw[:2] == b'PK':
        with zipfile.ZipFile(io.BytesIO(raw)) as z:
            raw = z.read(z.namelist()[0])
    # Latin-1 hasta 2025, UTF-8 en 2026
    try:
        text = raw.decode('utf-8')
    except UnicodeDecodeError:
        text = raw.decode('latin-1')
    df = pd.read_csv(io.StringIO(text), sep='|', dtype=str)
    # Cabeceras y valores con espacios de relleno
    df.columns = df.columns.str.strip()
    df = df.apply(lambda col: col.str.strip())
    # Importe sin IVA: el único completo todos los años (coma decimal en 2025)
    df['importe'] = pd.to_numeric(df['IMPORTE_ADJUDICACION_SIN_IVA'].str.replace(',', '.'), errors='coerce')
    # Fechas ISO hasta 2024 y DD/MM/AAAA después: formato explícito para no cruzar día y mes
    dates = df['FECHA_ADJUDICACION'].str[:10]
    date_format = '%d/%m/%Y' if dates.str.contains('/').any() else '%Y-%m-%d'
    df['fecha'] = pd.to_datetime(dates, format=date_format, errors='coerce')
    df['dataset'] = year
    frames.append(df)
    print(f'{year}: {len(df):,} contratos, {df["importe"].sum():,.0f} EUR sin IVA, '
          f'adjudicados del {df["fecha"].min():%d/%m/%Y} al {df["fecha"].max():%d/%m/%Y}')

menores = pd.concat(frames, ignore_index=True)
print(f'Total: {len(menores):,} contratos menores')