Municipalities lab — machine learning on Italy's data
Machine learning on public data, **right in your browser**: pick a region, group its municipalities by income, age and foreign residents, find the ones that stand out, discover which indicators move together. The models (scikit-learn) run on your device: the first run downloads the library (~60 MB, one-time), then everything is instant and reactive.
data space «laboratorio-comuni» — the suite’s glue: on your device, multi-user with one invite (link or QR), end-to-end encrypted.
What it records
- Region code (2 digits)
Composed of 4 micro apps
One job each, over the same data: every micro app reads and writes the shared data space “laboratorio-comuni” (with your consent, asked on first open). Everything stays on your device — and with a single invite the whole suite goes multi-user, end-to-end encrypted.
How it works
This is a Reactive app: a plain text document that your browser turns into a working app — forms, live data, tables, statistics and even charts, with nothing to install or configure.
- Use it right away. Hit “Use this app”: it opens right in your browser, ready to go, with data saved only on your device.
- Your data stays yours. Everything is stored locally on your device, kept separate for each app: no account, no server, nothing to set up.
- Make it your own. The text below is the entire app: copy it, tweak fields, views and words, and your changes become interactive instantly.
- Share it as a file. An app is a simple file: save it, export it or share it with a link; and with encrypted collaboration several people edit the same data in real time.
The app’s source
# 🧪 Municipalities lab
Machine learning on public data, **right in your browser**: pick a region,
group its municipalities by income, age and foreign residents, find the ones
that stand out, discover which indicators move together. The models
(scikit-learn) run on your device: the first run downloads the library
(~60 MB, one-time), then everything is instant and reactive.
:::::page{title="Groups" icon="🗺️"}
## The region's groups of municipalities
Two-digit region code (e.g. `01` Piedmont, `03` Lombardy, `05` Veneto,
`08` Emilia-Romagna, `09` Tuscany, `12` Lazio, `15` Campania, `19` Sicily).
Then move the slider: the clustering recomputes and the map recolors.
::input[reg]{value="08" placeholder="Region code (2 digits)"}
::od-query{into="comuni" sql="SELECT c.comune AS comune, c.geojson AS geojson, r.reddito_medio AS reddito, i.eta_media AS eta, i.perc_stranieri AS stranieri FROM istat_confini_comuni c JOIN mef_redditi r ON r.codice_istat = c.codice_istat JOIN istat_indicatori i ON i.codice_istat = c.codice_istat WHERE c.cod_reg = '{#reg}'" limit="2000"}
::range[k]{min="2" max="8" value="4" legend="Number of groups"}
::ml-cluster{data="comuni" features="reddito,eta,stranieri" k="#k" into="gruppi"}
:::map{path="gruppi" geojson="geojson" fill="cluster" height="26rem"}
**{comune}** — group {cluster} · income {reddito} € · average age {eta}
:::
Similar municipalities end up in the same group: K-means works on average
income, average age and percentage of foreign residents (standardized). The
map colors are the groups.
:::::
:::::page{title="Outliers" icon="🔍"}
## The municipalities that stand out
The Isolation Forest assigns each municipality an anomaly score (0-1):
high = the municipality doesn't resemble the others in the region. Click the
table headers to sort.
::ml-anomaly{data="comuni" features="reddito,eta,stranieri" contamination="0.05" into="anomali"}
:::table{path="anomali" headers="Municipality,Income €,Age,Foreigners %,Anomaly,Outlier" pagesize="10"}
{comune} | {reddito} | {eta} | {stranieri} | {anomalia} | {flag}
:::
:::::
:::::page{title="Correlations" icon="🔗"}
## What moves together
The Pearson correlation between the indicators of the region's municipalities:
`r` close to **1** = they grow together, close to **-1** = opposite, close to
**0** = independent.
::ml-correlate{data="comuni" features="reddito,eta,stranieri" into="correlazioni"}
:::table{path="correlazioni" headers="Indicator A,Indicator B,Correlation r" pagesize="10" search="false"}
{a} | {b} | {r}
:::
::chart-bar{data="correlazioni" x="a" y="r" height="14rem"}
:::::
:::::page{title="Predictions" icon="🎯"}
## How much income do age and foreigners "explain"?
A regression learns the relationship between the demographic indicators and
the average income of the region's municipalities, then **estimates** it for
each one: where the estimate misses the most, something the demographic data
alone can't explain is going on. The **R²** in the status tells you how much
of the income the model explains.
::ml-predict{data="comuni" features="eta,stranieri" target="reddito" model="forest" into="stime"}
Each point is a municipality: the horizontal axis is the **real** income, the
vertical one the model's **estimate**. The closer the points sit to the
diagonal, the better the model explains.
::chart-scatter{data="stime" x="reddito" y="previsione" height="20rem"}
:::table{path="stime" headers="Municipality,Real income €,Model estimate €" pagesize="8"}
{comune} | {reddito} | {previsione}
:::
:::::
---
The models run **on your device** (scikit-learn via Pyodide): the data
downloaded from the open service stays in the browser and none of your data
leaves it. The sources are official: MEF **IRPEF incomes** (CC BY 3.0), ISTAT
**socio-demographic indicators** and **boundaries** (CC BY 4.0), joined on
their own via the municipality's ISTAT code.