Static Parquet mirror of the OpenCUP open data archive, the Italian registry of publicly funded investment projects. Each file is sorted on its access key and can be queried in place over HTTP or S3, so you don't have to download 2.7 GB of zipped CSV to ask one question. Also includes a DuckDB catalog of views and, in territori/, service files by onData to read projects by municipality.
Every publicly funded investment project in Italy is assigned a CUP (Codice Unico di Progetto) and recorded in OpenCUP, the national registry. This product republishes the official open data archive as Parquet, so it can be queried over HTTP without downloading anything.
The source is distributed as zipped CSV — over 2 GB for the projects file alone — plus an API that answers one CUP at a time. Neither shape suits a set-wide question like how many projects are there in this municipality. Here that question costs a couple of seconds and a few megabytes.
Data as of: OpenCUP snapshot of 2026-10-05 — the source's own Last-Modified for the four
archives. The source republishes the whole archive periodically (monthly so far) and this mirror
follows, so the figures in this page are rounded on purpose.
Exact row counts, byte sizes and the snapshot date of the files currently online are in
_manifest.json.
CUP is the join key across the first three files, and it is a true primary key in
progetti.parquet: one row per CUP, no duplicates. The first four files cover the same set of
projects — no dataset is a subset of another. revocati-cancellati is the exception: revoked and
cancelled CUPs are not in progetti at all, and comes from a separate archive with its own
snapshot date (files[].snapshot in the manifest).
The manifest is the machine-readable inventory — file names, row counts,
byte sizes, sha256 checksums, sort keys and the snapshot date; the territorial service files are
listed apart, under derived_files. Useful because wildcards do not work
over plain HTTP (see below): there, the manifest is the only way to enumerate this product
programmatically. Over S3 a wildcard lists the files directly.
Every file is sorted on its own access key — progetti, fonti-copertura and
revocati-cancellati by CUP, localizzazione by territory, soggetti by name — so row-group
statistics are selective. Looking
up one project in the projects file reads one row group out of about 120; the same lookup in
fonti-copertura reads one out of about 230.
These files are not an archive to download: they are meant to be queried where they are. Two choices follow from that, and both were measured rather than assumed:
WRITE_BLOOM_FILTER false). DuckDB writes them by default, and on local
disk they help. Over HTTP they don't: each one is a separate block to fetch per row group, which
added 80–120 requests and doubled the time of a scan, to save about 1 MB. Dropping them also made
the file 10 MB smallerThe practical consequence: the cost of a query here is the number of round-trips, not the bytes. Filters on the sort key are nearly free; filters on other columns have to look at every row group's statistics, and cost a few seconds instead of a few hundred milliseconds.
No credentials, no download. There are two ways in, and they read the same files.
The files are stored in the public AWS bucket eu-central-1.opendata.source.coop. Reading them
there is faster and steadier than through https://data.source.coop, and wildcards work. It takes
one line of setup, with empty credentials — the bucket is public:
The same bucket also answers over plain HTTPS, with no setup and no wildcards — this is what the
views in opencup.duckdb use:
Every file is also served at https://data.source.coop/ondata/opencup/. Nothing to configure, and
it is the way to go from a browser. With DuckDB:
~67,000 -- projects located in the municipality of Palermo, in ~1.7 s
Ten municipalities of Sicily with the most projects:
Join the two files — both remote — to ask something neither can answer alone. Here, the third quartile of project cost in Rome, across more than 220,000 projects, in about 4 seconds:
The files also carry their own documentation, readable without downloading them:
One thing to know: over plain HTTP a wildcard is refused (Globs (*) for generic HTTP file are not supported). Name the file you want — there are four, listed above — or use the S3 path.
opencup.duckdb is a small DuckDB database that holds no data, only views pointing at the
Parquet files: the five of the mirror and the four
territorial service files, as territori,
territori_indice, territori_quote and territori_aggregati. Attach it, and each file becomes a
table, with no URL to remember:
Every query still reads the remote Parquet files, straight from the bucket in Frankfurt over plain HTTPS, so there is nothing to configure: the attach takes a couple of seconds, a CUP lookup about one more.
The database documents itself:
info_viste has one row per view: source file, URL, row count, sort order, sha256, snapshot
date, licence — the same figures as _manifest.json — and derivato, true for our elaborationsMetadati.xlsx (all columns but
TIPOLOGIA_CUP, which the source does not define, and CODICE_CATEGORIA_SOGGETTO in
revocati_cancellati, whose official definition would be wrong there — see below). The columns of
the territori views are ours, and so are their commentsIt is rebuilt with every snapshot, together with the Parquet files. It also works in the browser,
with nothing to install: this SQL Workbench link
runs the query above. In a desktop client such as DBeaver, open a DuckDB
connection and run the same ATTACH.
Questions that used to require downloading gigabytes now take seconds. All of the following were run against the published files, over HTTP, with nothing stored locally — timings are real.
Half of Italy's public investment sits in less than 0.1% of its projects (5.3 s)
About ten thousand projects out of twelve million account for half of the more than €4,100 billion recorded since 1950. The other 99.9% of projects, together, are worth as much as those ten thousand.
Half a million projects decided before 2010 are still open (1.3 s)
Over 570,000 still ATTIVO against about 480,000 closed. Either they are genuinely running after fifteen
years, or nobody ever closed them in the registry — and the registry cannot tell you which.
The most expensive projects, and how to spot a wrong one (2.0 s)
Top of the list is the Battipaglia–Reggio Calabria high-speed line (€22 bn, 2003). Second is the internal water network of Fondachelli-Fantina, a village of a few hundred people in Sicily, at €18.5 bn — more than the Strait of Messina bridge. Almost certainly a figure entered in lire and never converted (18.5 bn lire ≈ €9.5 m), but the published value says euro. This is the kind of check that now costs two seconds instead of a week — and a reminder to verify before publishing.
Who holds the most projects (1.0 s)
AGEA, the agricultural payments agency, tops the count with over 1.5 million projects worth
about €5 bn — tiny farm subsidies. Third place goes to a body called soggetto privato — an
anonymised placeholder — with over half a million projects worth more than €200 bn, more than
Lombardy and Campania combined.
A caveat on every figure above:
COSTO_PROGETTOis the declared cost of a project, not money actually spent, and the totals span 1950 to 2026 without adjusting for inflation. For amounts actually disbursed, see BDAP, not OpenCUP.
Five things about this dataset will silently give you wrong numbers if you don't know them.
1. One project can sit in several territories, and there are no shares. About 960,000 CUP (8%) have more than one location, up to ten or more. The source publishes no percentage split, so a project's cost cannot be apportioned between municipalities. If you join this file with project amounts and sum by municipality, the same project is counted once per territory. The territorial service files apply one declared split for you.
2. Not every location reaches the municipality. When a project covers a wider area, the codes are
set to -1 and the labels become generic:
Two traps here. -1 passes a cast to integer without failing, so it slips into joins unnoticed.
And the Italian label is TUTTI for the municipality but TUTTE for province and region — a
filter on PROVINCIA = 'TUTTI' returns nothing while nearly half a million rows match 'TUTTE'. Ranking
municipalities by project count puts TUTTI in fourth place in Sicily, ahead of Syracuse, and it is
not a place.
3. ISTAT codes are strings, and the leading zeros matter. Region 04, province 021,
municipality 021108. They are stored as text on purpose: over 90% of province and municipality codes
begin with a zero, and reading them as numbers turns 021108 into 21108, which matches nothing in
any official table. The municipality code contains the province code as its prefix.
4. It is not only Italy. CODICE_STATO has nearly 200 distinct values (two codes share the same
STATO label); about a quarter of a percent of rows, over 30,000, are abroad — Spain, Germany, the
United Kingdom, France…
Filtering by region without also filtering by country silently drops them.
5. soggetti.parquet is not a registry of bodies. The name suggests one row per public body, but
it holds about 54,000 rows for fewer than 29,000 distinct SOGGETTO_TITOLARE: it is the relation
between the body that owns a project and the one that applied for it. Of the roughly 9,000 bodies
appearing more than once, almost all differ only in SOGGETTO_RICHIEDENTE; in about half the
rows the two coincide. Joining progetti to this file on SOGGETTO_TITOLARE therefore nearly
doubles the rows on average. To attach the address and category of a body, deduplicate first:
The folder territori/ is not a copy of the source. It is an elaboration by onData that joins
OpenCUP with ISTAT's territorial registry (SITUAS), to solve once the
problem described in points 1 and 2 above: a project sits in one or more places, at mixed levels
(municipality, whole province, whole region, whole country, abroad), with territorial codes from
different periods — in Sardinia three generations of province and municipality codes coexist, and
the same municipality can appear under more than one code. Every question of the kind how much is
invested in my area requires the same four steps: translating old codes, splitting a project
between its places, deciding what to do with locations above the municipality, and joining with the
costs. Different choices give different numbers, usually without saying so.
These files make those steps once, with two conventions that are ours, not the source's:
aggregati keeps both values side by side, so you can choose: valore_localizzato is what was
located in the municipality itself; valore_ripartito adds its share of province-, region- and
country-wide projects. Summed over all municipalities, valore_ripartito plus the projects abroad
gives back the national total, to rounding. Values are declared cost, as in COSTO_PROGETTO, not money spent.
How much is invested in the province of Palermo, with and without its share of wider projects:
About €78 bn located in its municipalities, about €97 bn once the share of wider projects is added.
If you prefer another criterion, start from quote and indice instead of aggregati: they carry
the split and the code translation, and leave the rest to you. The livello values are comune,
provincia, regione, italia and estero; codice is the code as published (for italia,
the country code 5).
Known limits:
aggregatifonti) of every file, with the method (metodo)revocati-cancellati.parquet mirrors a separate OpenCUP archive, the list of CUP codes that are no
longer active, published at
https://www.opencup.gov.it/portale/web/opencup/elenco-cup-revocati-e-cancellati. It is the only
place where a revoked code still answers: the portal and the other four files do not show revoked or
cancelled projects. Three states, one per CUP:
When a revoked code was replaced by a new one, CUP_IN_RELAZIONE points to it, with
RUOLO_IN_RELAZIONE = 'SOSTITUITO'. From the revoked code to the project that took its place:
The source's header is broken, and the column names here are reconstructed. The CSV header
declares 71 names, every row has 68 values, and from the 26th column on the names are out of
place (the XML version has the same defect). The 68 names in this file were assigned by matching
each position against progetti, localizzazione and soggetti on the CUPs they share, and
reviewed against a sample by a domain expert. Three names of the source header match no value at all
— COD_COMUNE_SOGGETTO_TITOLARE, OBIETTIVO_FINALE_INT, TIPO_RELAZIONE — and are not in this file.
Before you rely on a column, know these:
CODICE_CATEGORIA_SOGGETTO is not what its name says. It sits where that field sits in the
other files, but its values are a copy of CODICE_CATEGORIA_INTERVENTO, on every row. A defect of
the source's export, kept visible rather than hiddenCUP is not a key. One row per CUP × territory × funding source: about 160 thousand codes
repeat, mostly because they have more than one funding source. Count with count(DISTINCT CUP),
never count(*). A row is unique on CUP plus CODICE_STATO, CODICE_REGIONE,
CODICE_PROVINCIA, CODICE_COMUNE and CODICE_COPERTURA_FINANZIARIACUP_IN_RELAZIONE can hold several codes, separated by ; and always ending with one
(C46G08000220003;C46G08000210003;), up to fifty in a row. It is left as published, as in
progetti: split it before joiningDATA_CHIUSURA_REVOCA almost always separates the states, but not always: nearly every revoked
and closed-by-office row has it, nearly every cancelled one does notprogetti: FLAG_TIPO_GENERICO is S/N instead of
SI/NO, and FINANZA_PROGETTO carries the source's DATO NON DISPONIBILE ("not available"),
left as publishedMirror of the four bulk archives — Progetti, Localizzazione, Fonti di Copertura and
Soggetti — published at
https://www.opencup.gov.it/portale/web/opencup/accesso-agli-open-data, snapshot of 2026-10-05
(the source's own Last-Modified, the same day for all four archives). The date is also in
_manifest.json (source.snapshot) and in the key-value metadata of every file. The list of
revoked and cancelled CUPs comes from its own page (see above) and carries its own date, in
files[].snapshot.
This is a format conversion with minimal, declared cleaning: no column renamed (except the
reconstructed names of revocati-cancellati), no row dropped,
and values are left as published with three exceptions:
NULL. Text inside a
value is untouched. Without this, the same label appeared in two versions (with and without a
trailing space) in millions of rows, silently splitting filters and GROUP BYDATO NON PRESENTE ("not available") and empty strings become NULLANNO_DELIBERA, 9999 and 0000 are placeholders for a missing year and become NULLEvery other oddity of the source — including the ones above — is preserved rather than corrected, so that the data can be compared with the original. One consequence is worth stating plainly: if you are measuring the source's own defects, count them on the original CSVs, not here. Those three cleanings are exactly the kind of thing such an audit looks for, so counting them in this mirror measures the conversion rather than the problem. Every file carries provenance and caveats in its Parquet key-value metadata.
The files in territori/ are the exception to all of the above: they are an elaboration, not a
conversion, described in their own section.
For anything authoritative, and for projects registered after the snapshot date, refer to the source.
CC BY 4.0 — Creative Commons Attribution 4.0 International. You may share and adapt this data, including commercially, as long as you give credit. This mirrors the licence the source declares at opencup.gov.it/portale/web/opencup/licenza-cc-by.
Source: OpenCUP, https://www.opencup.gov.it/. Republished by onData APS.
The files in territori/ also use ISTAT data (SITUAS, https://situas.istat.it/), released under
CC BY 4.0.
Suggested citation:
OpenCUP (https://www.opencup.gov.it/), snapshot 2026-10-05. Parquet mirror published by onData on Source Cooperative, CC BY 4.0.
territori/*.parquet | — | ~45 MB | not part of the source: four service files by onData to read projects by territory, see below |