Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions src/data/fts-eu-grants.json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Data loader: FTS EU Grants — finanziamenti UE a beneficiari italiani."""
import json
import sys

sys.path.insert(0, "src/data")

from lab_connectors.duckdb import safe_connect
from lab_connectors.gcs.paths import https_url


SLUG = "fts_eu_grants"
YEARS = [2020, 2021, 2022, 2023, 2024]


parquet_refs = " UNION ALL ".join(
f"SELECT * FROM read_parquet('{https_url('clean', 'clean_parquet', slug=SLUG, year=y)}')"
for y in YEARS
)


PROGRAM_CASE = """
CASE
WHEN nome_programma ILIKE '%Recovery and Resilience%' THEN 'Recovery and resilience'
WHEN nome_programma ILIKE '%Horizon%' THEN 'Ricerca (Horizon)'
WHEN nome_programma ILIKE '%Erasmus%' THEN 'Istruzione (Erasmus+)'
WHEN nome_programma ILIKE '%Digital Europe%' THEN 'Digitale'
WHEN nome_programma ILIKE '%Creative Europe%' OR nome_programma ILIKE '%Culture%' THEN 'Cultura'
WHEN nome_programma ILIKE '%LIFE%' THEN 'Ambiente (LIFE)'
WHEN nome_programma ILIKE '%CERV%' OR nome_programma ILIKE '%Citizens%' THEN 'Cittadinanza'
WHEN nome_programma ILIKE '%Health%' OR nome_programma ILIKE '%EU4H%' THEN 'Salute'
WHEN nome_programma ILIKE '%Humanitarian%' THEN 'Aiuti umanitari'
WHEN nome_programma ILIKE '%Migration%' OR nome_programma ILIKE '%AMIF%' THEN 'Migrazione'
WHEN nome_programma ILIKE '%Connecting Europe Facility%' THEN 'Infrastrutture (CEF)'
WHEN nome_programma ILIKE '%Defence Fund%' THEN 'Difesa'
ELSE 'Altri programmi'
END
"""


ENTITY_CASE = """
CASE
WHEN LOWER(flag_no_profit) IN ('true', 'yes') THEN 'Non-profit'
WHEN LOWER(flag_ong) IN ('true', 'yes') THEN 'ONG'
WHEN tipo_beneficiario ILIKE '%university%'
OR tipo_beneficiario ILIKE '%research%'
OR tipo_beneficiario ILIKE '%higher%' THEN 'Ricerca/Università'
WHEN tipo_beneficiario ILIKE '%SME%'
OR tipo_beneficiario ILIKE '%enterprise%'
OR tipo_beneficiario ILIKE '%company%' THEN 'Impresa'
WHEN tipo_beneficiario ILIKE '%public%'
OR tipo_beneficiario ILIKE '%government%' THEN 'Pubblica amministrazione'
ELSE 'Altro'
END
"""


def _query(con, sql):
rel = con.sql(sql)
cols = [desc[0] for desc in rel.description]
rows = []
for row in rel.fetchall():
rows.append(
{
col: int(val) if isinstance(val, float) and val == int(val) else val
for col, val in zip(cols, row)
}
)
return rows


with safe_connect() as con:
base = f"({parquet_refs})"

totali = _query(
con,
f"""
SELECT
COUNT(*) AS numero_grant,
COUNT(DISTINCT beneficiario_nome) AS beneficiari,
ROUND(SUM(COALESCE(importo_contrattato, 0)), 0) AS importo_totale
FROM {base}
""",
)[0]

per_anno = _query(
con,
f"""
SELECT
anno,
COUNT(*) AS numero_grant,
COUNT(DISTINCT beneficiario_nome) AS beneficiari,
ROUND(SUM(COALESCE(importo_contrattato, 0)), 0) AS importo_totale
FROM {base}
GROUP BY anno
ORDER BY anno
""",
)

per_programma = _query(
con,
f"""
SELECT
anno,
{PROGRAM_CASE} AS categoria_programma,
COUNT(*) AS numero_grant,
COUNT(DISTINCT beneficiario_nome) AS beneficiari,
ROUND(SUM(COALESCE(importo_contrattato, 0)), 0) AS importo_totale
FROM {base}
GROUP BY anno, categoria_programma
ORDER BY anno, importo_totale DESC
""",
)

per_tipo_ente = _query(
con,
f"""
SELECT
anno,
{ENTITY_CASE} AS tipo_ente,
COUNT(*) AS numero_grant,
COUNT(DISTINCT beneficiario_nome) AS beneficiari,
ROUND(SUM(COALESCE(importo_contrattato, 0)), 0) AS importo_totale
FROM {base}
GROUP BY anno, tipo_ente
ORDER BY anno, importo_totale DESC
""",
)

per_citta = _query(
con,
f"""
SELECT
anno,
CASE
WHEN beneficiario_citta IS NULL OR beneficiario_citta IN ('', '-') THEN 'Non indicata'
ELSE beneficiario_citta
END AS citta,
COUNT(*) AS numero_grant,
COUNT(DISTINCT beneficiario_nome) AS beneficiari,
ROUND(SUM(COALESCE(importo_contrattato, 0)), 0) AS importo_totale
FROM {base}
GROUP BY anno, citta
QUALIFY ROW_NUMBER() OVER (PARTITION BY anno ORDER BY importo_totale DESC) <= 30
ORDER BY anno, importo_totale DESC
""",
)

top_beneficiari = _query(
con,
f"""
SELECT
anno,
COALESCE(beneficiario_nome, 'Non indicato') AS beneficiario_nome,
CASE
WHEN beneficiario_citta IS NULL OR beneficiario_citta IN ('', '-') THEN 'Non indicata'
ELSE beneficiario_citta
END AS beneficiario_citta,
{PROGRAM_CASE} AS categoria_programma,
{ENTITY_CASE} AS tipo_ente,
COUNT(*) AS numero_grant,
ROUND(SUM(COALESCE(importo_contrattato, 0)), 0) AS importo_totale
FROM {base}
GROUP BY anno, beneficiario_nome, beneficiario_citta, categoria_programma, tipo_ente
QUALIFY ROW_NUMBER() OVER (PARTITION BY anno ORDER BY importo_totale DESC) <= 100
ORDER BY anno, importo_totale DESC
""",
)


output = {
"anni": YEARS,
"totali": totali,
"per_anno": per_anno,
"per_programma": per_programma,
"per_tipo_ente": per_tipo_ente,
"per_citta": per_citta,
"top_beneficiari": top_beneficiari,
}

json.dump(output, sys.stdout, ensure_ascii=False)
2 changes: 1 addition & 1 deletion src/data/themes.json.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"slug": "finanza-pubblica",
"name": "Finanza pubblica",
"description": "Entrate dello Stato, capacità fiscale, tributi locali, FSC, politiche di coesione, partecipazioni PA e disuguaglianza del reddito",
"datasets": ["irpef-comunale", "entrate-stato", "consip-consumi-convenzione", "istat-gini-regionale", "opencivitas-fsc-2025", "opencoesione-progetti", "mef-partecipazioni", "bdap-spese-stato"],
"datasets": ["irpef-comunale", "entrate-stato", "consip-consumi-convenzione", "istat-gini-regionale", "opencivitas-fsc-2025", "opencoesione-progetti", "fts-eu-grants", "mef-partecipazioni", "bdap-spese-stato"],
},
{
"slug": "sanita",
Expand Down
208 changes: 208 additions & 0 deletions src/dataset/fts-eu-grants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
title: FTS EU Grants — Finanziamenti UE in Italia
description: Finanziamenti UE a beneficiari italiani dal Financial Transparency System, per anno, programma, territorio e beneficiario
source: Commissione europea — Financial Transparency System
source_url: https://commission.europa.eu/funding-tenders/financial-transparency-system_en
period: "2020–2024"
last_modified: 2026-07-01
dataset_slug: fts_eu_grants
---

# FTS EU Grants — Finanziamenti UE in Italia

Il Financial Transparency System della Commissione europea elenca i finanziamenti UE assegnati a beneficiari italiani. Il dataset copre il periodo 2020-2024 e permette di leggere importi, programmi, beneficiari e localizzazione dichiarata.

**Fonte**: [Commissione europea — Financial Transparency System](https://commission.europa.eu/funding-tenders/financial-transparency-system_en) · **Periodo**: 2020–2024

```js
import { num, euroCompact, tableFormat } from "../import/format-utils.js";
```

```js
const data = await FileAttachment("../data/fts-eu-grants.json").json();
```

```js
const anni = [...data.anni].sort((a, b) => b - a);
const annoSel = view(Inputs.select(anni, {label: "Anno", value: anni[0]}));
```

```js
const annoData = data.per_anno.find(d => d.anno === annoSel);
const programmi = data.per_programma
.filter(d => d.anno === annoSel)
.sort((a, b) => d3.descending(a.importo_totale, b.importo_totale));
const tipiEnte = data.per_tipo_ente
.filter(d => d.anno === annoSel)
.sort((a, b) => d3.descending(a.importo_totale, b.importo_totale));
const citta = data.per_citta
.filter(d => d.anno === annoSel)
.sort((a, b) => d3.descending(a.importo_totale, b.importo_totale));
const beneficiari = data.top_beneficiari
.filter(d => d.anno === annoSel)
.sort((a, b) => d3.descending(a.importo_totale, b.importo_totale));
```

<div class="grid grid-cols-3">
<div class="card">
<h3>Importo contrattato</h3>
<span class="big">${euroCompact(annoData?.importo_totale)}</span>
</div>
<div class="card">
<h3>Grant</h3>
<span class="big">${num(annoData?.numero_grant)}</span>
</div>
<div class="card">
<h3>Beneficiari</h3>
<span class="big">${num(annoData?.beneficiari)}</span>
</div>
</div>

---

## Programmi finanziati

La prima lettura mostra come si distribuiscono gli importi per area di programma nell'anno selezionato.

```js
Plot.plot({
title: `Importo contrattato per programma — ${annoSel}`,
width: 800,
height: Math.max(340, programmi.length * 34 + 48),
marginLeft: 190,
x: {grid: true, label: "euro", tickFormat: d => euroCompact(d)},
y: {label: null, tickSize: 0},
color: {scheme: "Tableau10"},
marks: [
Plot.barX(programmi, {
y: "categoria_programma",
x: "importo_totale",
fill: "categoria_programma",
sort: {y: "-x"},
tip: {format: {x: d => euroCompact(d)}}
}),
Plot.ruleX([0])
]
})
```

> **Nota**: la categoria "Recovery and resilience" può concentrare importi molto grandi in poche righe, perché il FTS registra anche flussi collegati al Recovery and Resilience Facility.

---

## Beneficiari e territorio

La vista per tipo di ente aiuta a distinguere imprese, università, amministrazioni e organizzazioni non profit. La vista per città mostra dove sono dichiarati gli importi maggiori, ma alcune righe FTS non indicano una città.

```js
Plot.plot({
title: `Importo per tipo di beneficiario — ${annoSel}`,
width: 800,
height: 320,
marginLeft: 170,
x: {grid: true, label: "euro", tickFormat: d => euroCompact(d)},
y: {label: null, tickSize: 0},
color: {scheme: "Set2"},
marks: [
Plot.barX(tipiEnte, {
y: "tipo_ente",
x: "importo_totale",
fill: "tipo_ente",
sort: {y: "-x"},
tip: {format: {x: d => euroCompact(d)}}
}),
Plot.ruleX([0])
]
})
```

```js
Plot.plot({
title: `Prime città per importo dichiarato — ${annoSel}`,
width: 800,
height: 380,
marginLeft: 130,
x: {grid: true, label: "euro", tickFormat: d => euroCompact(d)},
y: {label: null, tickSize: 0},
marks: [
Plot.barX(citta.slice(0, 12), {
y: "citta",
x: "importo_totale",
fill: "var(--theme-foreground-focus)",
sort: {y: "-x"},
tip: {format: {x: d => euroCompact(d)}}
}),
Plot.ruleX([0])
]
})
```

---

## Trend annuale

Il confronto tra anni serve a vedere la scala complessiva del dataset, non a misurare l'intero budget UE speso in Italia.

```js
Plot.plot({
title: "Importo contrattato per anno",
width: 800,
height: 320,
x: {label: "anno", tickFormat: String},
y: {grid: true, label: "euro", tickFormat: d => euroCompact(d)},
marks: [
Plot.ruleY([0]),
Plot.lineY(data.per_anno, {
x: "anno",
y: "importo_totale",
stroke: "var(--theme-foreground-focus)",
strokeWidth: 2,
marker: true,
tip: {format: {y: d => euroCompact(d)}}
})
]
})
```

---

## Dettaglio beneficiari

```js
const { header, format } = tableFormat({
beneficiario_nome: { label: "Beneficiario", fmt: "string" },
beneficiario_citta: { label: "Città", fmt: "string" },
categoria_programma: { label: "Programma", fmt: "string" },
tipo_ente: { label: "Tipo ente", fmt: "string" },
numero_grant: { label: "Grant", fmt: "num" },
importo_totale: { label: "Importo", fmt: "euroCompact" },
});
```

```js
Inputs.table(beneficiari, {
columns: ["beneficiario_nome", "beneficiario_citta", "categoria_programma", "tipo_ente", "numero_grant", "importo_totale"],
header,
format,
rows: 20,
width: "100%",
sort: "importo_totale",
reverse: true
})
```

---

## Limiti

- **Perimetro**: FTS copre i pagamenti e gli impegni tracciati dalla Commissione europea; non sostituisce OpenCoesione o altre fonti sui fondi strutturali gestiti a livello nazionale.
- **Territorio**: la città del beneficiario può mancare o essere indicata come `-`, quindi le classifiche territoriali non sono una mappa completa della destinazione finale dei fondi.
- **Classificazioni**: le categorie programma e tipo ente sono derivate da campi testuali e servono per orientare la lettura, non come tassonomia ufficiale.

---

## Risorse

- [Financial Transparency System](https://commission.europa.eu/funding-tenders/financial-transparency-system_en)
- [Scarica il parquet pulito 2024](https://storage.googleapis.com/dataciviclab-clean/fts_eu_grants/2024/fts_eu_grants_2024_clean.parquet)
- [Pipeline](https://github.com/dataciviclab/dataset-incubator/tree/main/candidates/fts-eu-grants)