Analyze events of a Gancio instance with DuckDB
Find a file
2025-03-28 10:00:42 +01:00
map map 2025-03-28 10:00:42 +01:00
go.mod map 2025-03-28 10:00:42 +01:00
go.sum map 2025-03-28 10:00:42 +01:00
main.go map 2025-03-28 10:00:42 +01:00
places.sql map 2025-03-28 10:00:42 +01:00
README.md map 2025-03-28 10:00:42 +01:00
schema.sql first commit 2025-03-27 15:35:23 +01:00

Gancio events as data

Analyze events of a Gancio instance with DuckDB.

Run in DuckDB Web Shell

Steps

create database:

$ duckdb gancio.db

create table events:

CREATE TABLE events (
    id BIGINT,
    title VARCHAR,
    slug VARCHAR,
    multidate BOOLEAN,
    start_datetime TIMESTAMP,
    end_datetime TIMESTAMP,
    media STRUCT(
        url VARCHAR,
        height BIGINT,
        width BIGINT,
        "name" VARCHAR,
        size BIGINT,
        focalpoint DOUBLE[]
    )[],
    online_locations JSON,
    updatedAt DATETIME,
    apUserApId JSON,
    tags VARCHAR[],
    place STRUCT(
        id BIGINT,
        "name" VARCHAR,
        address VARCHAR,
        latitude DOUBLE,
        longitude DOUBLE
    ),
    ap_user JSON
);

load data from the API (the following examples uses https://balotta.org):

INSERT INTO events
SELECT 
    id,
    title,
    slug,
    multidate,
    to_timestamp(CAST(start_datetime AS BIGINT)) AS start_datetime,
    to_timestamp(CAST(end_datetime AS BIGINT)) AS end_datetime,
    media,
    online_locations,
    updatedAt,
    apUserApId,
    tags,
    place,
    ap_user
FROM read_json_auto('https://balotta.org/api/events?start=2010');

Example queries

top tags

WITH tag_counts AS (
      SELECT
          unnest AS tag,
          COUNT(*) as tag_count
      FROM events,
           UNNEST(tags) AS unnest
      GROUP BY unnest
  ),
  total_events AS (
      SELECT COUNT(DISTINCT id) as total FROM events
  )
  SELECT
      tag,
      tag_count,
      ROUND(tag_count * 100.0 / (SELECT total FROM total_events), 2) AS percentage
  FROM tag_counts
  ORDER BY tag_count DESC
  LIMIT 30;
┌───────────────────┬───────────┬────────────┐
│        tag        │ tag_count │ percentage │
│      varchar      │   int64   │   double   │
├───────────────────┼───────────┼────────────┤
│ musica            │       529 │      20.98 │
│ concerti          │       267 │      10.59 │
│ libri             │       179 │        7.1 │
│ presentazione     │       173 │       6.86 │
│ diy               │       161 │       6.39 │
│ laboratorio       │       129 │       5.12 │
│ cena              │       125 │       4.96 │
│ punk              │       118 │       4.68 │
│ aperitivo         │       104 │       4.13 │
│ femminismo        │        99 │       3.93 │
│ incontro          │        80 │       3.17 │
│ cinema            │        75 │       2.98 │
│ benefit           │        71 │       2.82 │
│ cena benefit      │        69 │       2.74 │
│ Camere d'Aria     │        67 │       2.66 │
│ dibattito         │        67 │       2.66 │
│ Vag61             │        66 │       2.62 │
│ proiezione        │        65 │       2.58 │
│ Ofpcina           │        63 │        2.5 │
│ autogestione      │        63 │        2.5 │
│ assemblea         │        63 │        2.5 │
│ mercatino         │        62 │       2.46 │
│ transfemminismo   │        62 │       2.46 │
│ palestina         │        59 │       2.34 │
│ autofinanziamento │        59 │       2.34 │
│ lotte ambientali  │        55 │       2.18 │
│ solidarietà       │        54 │       2.14 │
│ bolognina         │        53 │        2.1 │
│ autoproduzioni    │        52 │       2.06 │
│ concerto          │        51 │       2.02 │
├───────────────────┴───────────┴────────────┤
│ 30 rows                          3 columns │
└────────────────────────────────────────────┘

top places

SELECT 
    place.name AS place_name,
    COUNT(*) AS event_count,
    ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events WHERE place IS NOT NULL), 2) AS percentage
FROM 
    events
WHERE 
    place IS NOT NULL
GROUP BY 
    place_name
ORDER BY 
    event_count DESC
LIMIT 30;
┌─────────────────────────────────────────────────┬─────────────┬────────────┐
│                   place_name                    │ event_count │ percentage │
│                     varchar                     │    int64    │   double   │
├─────────────────────────────────────────────────┼─────────────┼────────────┤
│ Circolo Anarchico Berneri                       │         182 │       7.22 │
│ Camere d'Aria                                   │         145 │       5.75 │
│ Vag61                                           │         109 │       4.32 │
│ ExCentrale                                      │         103 │       4.09 │
│ Circolo Hex                                     │          92 │       3.65 │
│ Libreria Modo Infoshop                          │          86 │       3.41 │
│ Parco don Bosco                                 │          56 │       2.22 │
│ Circolo Arci Guernelli                          │          55 │       2.18 │
│ Centro delle donne                              │          53 │        2.1 │
│ Labàs                                           │          52 │       2.06 │
│ Laboratorio Giù dall'Arca                       │          48 │        1.9 │
│ Il muro di via Agucchi                          │          43 │       1.71 │
│ CSA Brigata - Imola                             │          42 │       1.67 │
│ Circolo Arci San Lazzaro                        │          42 │       1.67 │
│ Centro Sociale della Pace                       │          42 │       1.67 │
│ La Casona - Nuova Casa del Popolo di Ponticelli │          32 │       1.27 │
│ Scuola popolare di Musica Ivan Illich           │          32 │       1.27 │
│ Circolo DEV                                     │          29 │       1.15 │
│ Spazio documentazione anarchico Il Tribolo      │          29 │       1.15 │
│ Cassero LGBT Center                             │          27 │       1.07 │
│ Fondo Comini                                    │          27 │       1.07 │
│ Freakout Club                                   │          25 │       0.99 │
│ INFESTAZIONI, Via Stalingrado 31                │          25 │       0.99 │
│ Zamboni 38                                      │          24 │       0.95 │
│ Cinema Galliera                                 │          24 │       0.95 │
│ Piazza Dell'Unità                               │          23 │       0.91 │
│ Piazza Galvani                                  │          22 │       0.87 │
│ Mala Consilia occupata                          │          21 │       0.83 │
│ Piazza Nettuno                                  │          19 │       0.75 │
│ Archivio Via Avesella                           │          19 │       0.75 │
├─────────────────────────────────────────────────┴─────────────┴────────────┤
│ 30 rows                                                          3 columns │
└────────────────────────────────────────────────────────────────────────────┘

year distribution

WITH yearly_counts AS (
    SELECT
      EXTRACT(YEAR FROM start_datetime) AS year,
      COUNT(*) AS event_count
    FROM events
    WHERE start_datetime IS NOT NULL
    GROUP BY EXTRACT(YEAR FROM start_datetime)
    ORDER BY year
  ),
  total AS (
    SELECT SUM(event_count) AS total_events
    FROM yearly_counts
  )
  SELECT
    year,
    event_count,
    ROUND(100.0 * event_count / total_events, 2) AS percentage
  FROM yearly_counts, total
  ORDER BY year;
┌───────┬─────────────┬────────────┐
│ year  │ event_count │ percentage │
│ int64 │    int64    │   double   │
├───────┼─────────────┼────────────┤
│  2022 │         129 │       5.12 │
│  2023 │        1078 │      42.76 │
│  2024 │        1004 │      39.83 │
│  2025 │         310 │       12.3 │
└───────┴─────────────┴────────────┘

map

Display the places of events on a map

Before proceeding, missing coordinates must be geolocalized (requires Go and a Google Maps API Key)

$ MAPS_API_KEY=xxxxxxx go run main.go

Next, execute the query and export the results to JSON format:

duckdb -json gancio.db < places.sql > map/places.json

Deploy the contents of the ./map directory using a web server.

map