Skip to main content

GdeltForge: Global Event Data Pipeline

Global event data, forged.

Forges the raw GDELT archive into clean, reproducibly-sampled, cross-referenced Parquet.

CI Ruff Checked with pyright PyPI Python versions License: Apache 2.0 Open In Colab

Documentation  ·  Getting started  ·  CLI reference  ·  Recipes

gdeltforge sample, crossref and codes running in a terminal, sampling 20 rows out of 542,483,885

pip install gdeltforge
gdeltforge --help

GDELT's Events archive spans hundreds of millions of rows across 50+ years, but the official API caps queries at ~250 rows and BigQuery's free tier can't cover a full historical pull. GdeltForge downloads, converts, filters, and reproducibly samples the whole archive locally: checksum-verified downloads, concurrent I/O, and reservoir sampling built for a single pass over the full history. It also scrapes the Global Knowledge Graph (both current GKG 2.1 and legacy GKG 1.0) and Mentions, and can cross-reference a sampled Events output back onto GKG (themes, tone, people, organizations) with crossref.

  • Full historical archive, not just the last 3 months the API allows
  • Events enriched with GKG via crossref, preserving the real many-to-many structure instead of collapsing it
  • Efficient columnar storage (Parquet) instead of raw CSV/ZIP
  • Reproducible sampling: indexed, calendar, and filtered modes, filtered mode also supports stratified sampling (fixed N per group)
  • Each stage (scrape/convert/filter/sample/crossref) runs independently and inspectably

Contents


Stack

Python uv PyArrow Polars NumPy Requests Selenium

Challenges with Official Access Methods

The GDELT Events Database is extremely rich, but extracting the full archive through official methods is difficult. This pipeline solves the following issues:

GDELT API Limitations

The standard GDELT APIs are optimized for recent and small queries. They impose strict limits on:

  • time windows (often only the last 3 months)
  • maximum rows per query (≈ 250)
  • request rate limits

These constraints make it effectively impossible to retrieve the full historical dataset.

Google BigQuery Mirror Constraints

While BigQuery provides full access, it suffers from practical issues:

  • free-tier quotas (1TB/month query, 10GB storage, 1GB egress) are far too small for the ~hundreds of GB required
  • full-table scans are expensive without billing
  • some researchers prefer local offline workflows

Raw Bulk Downloads Are Available, But Hard to Use

The raw GDELT archives contain thousands of files. Processing them requires:

  • automated downloading
  • streaming or chunked processing
  • efficient columnar storage (Parquet)
  • memory-safe filtering and sampling

This pipeline automates these steps end-to-end.

Comparison to Other GDELT Tools

GdeltForge's genuinely distinguishing feature is treating reproducible sampling as a first-class pipeline stage, not download-and-convert alone: seeded indexed, calendar, and filtered reservoir sampling over the full archive in a single streaming pass, filtered mode also supports stratified sampling. The other is crossref: GKG 2.1 carries no event ID at all, only the source article's URL, so joining it to Events means a two-hop trip through Mentions that's easy to get subtly wrong; crossref does that join with filter pushdown and keeps the real many-to-many structure intact instead of silently collapsing it.

See Comparison to Other Tools for an honest breakdown of when GdeltForge fits and when a DOC API client, BigQuery, or an existing Spark/DuckDB pipeline is the better choice.

Pipeline Design Principles

The pipeline follows a single-responsibility, single-stage execution model. Each command performs exactly one transformation:

scrape produces CSV, convert produces Parquet, filter produces cleaned data, sample produces a sample, crossref produces sample plus GKG

Stage Does
scrape download raw GDELT CSV files (Events, GKG 2.1, GKG 1.0, or Mentions)
convert transform CSV -> Parquet
filter remove rows with missing values
sample reproducibly sample from Parquet files
crossref join a sampled Events output back onto GKG

This design is intentionally transparent and low-magic:

  • every operation is explicit
  • no hidden steps
  • each module is individually testable
  • any stage can be re-run without affecting others

The same stages run independently per dataset (--dataset events, gkg-v2, mentions, ...); crossref is the one stage that reads two datasets at once, joining a sampled Events output against a GKG dataset processed the same way.

Each stage consumes the output of the previous one, enabling incremental execution, streaming-friendly operations, memory-efficient processing, simple debugging, and reusable intermediate data.

Installation

pip install gdeltforge
gdeltforge --help

No config file required to get started: GdeltForge falls back to a conservative built-in default (real ./data/... paths, no row/column filtering) and writes it to config/settings.yaml the first time you run a command with nothing else configured, so it works immediately in a fresh environment like a Colab session. See Configuration to customize it instead.

Installing from source (contributing, or an editable install)

This project uses uv for dependency management; uv sync builds and installs GdeltForge itself (editable), so the gdeltforge command becomes available inside the virtual environment.

# Install uv (if not already installed)
pip install uv

git clone https://github.com/Vinicius-Teixeirac/GdeltForge.git
cd GdeltForge

# Create virtual environment, install dependencies, and install GdeltForge itself
uv sync

# Activate the virtual environment
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate

# Verify the CLI is installed
gdeltforge --help

Optional, but worth doing for a real project rather than accepting the built-in default: copy the example config and adjust paths:

cp config/settings.example.yaml config/settings.yaml
Running the tests

The test suite is pure unit tests (no network access, no browser, no real GDELT data required) covering the scraping and conversion logic.

uv sync --group dev
uv run pytest

Project Structure

GdeltForge follows a standard src/ package layout, installable via uv sync / pip install.

Full source tree
project_root/
├── config/
│ └── settings.example.yaml # Annotated starting point; copy to settings.yaml and edit
│
├── src/gdeltforge/
│ ├── py.typed # PEP 561 marker: this package ships inline type hints
│ ├── cli.py # Argument parsing + subcommand dispatch (the gdeltforge entry point)
│ ├── _version.py # Generated at build time from the git tag (hatch-vcs); not hand-edited
│ │
│ ├── config/
│ │ └── default_settings.yaml # Bundled fallback config, used when no settings.yaml/--config is found at all
│ │
│ ├── data/
│ │ └── cameo_codes.json # Bundled CAMEO/FIPS code -> name reference data, backs the `codes` command
│ │
│ ├── conversion/
│ │ └── converter.py # CSV -> Parquet conversion logic
│ │
│ ├── crossref/
│ │ └── crossref.py # Events<->GKG join (direct for GKG 1.0, two-hop via Mentions for GKG 2.1)
│ │
│ ├── filtering/
│ │ └── filter.py # Filtering logic (drop invalid rows)
│ │
│ ├── sampling/
│ │ ├── cameo_codes.py # Loads data/cameo_codes.json, groups columns by code family, backs the `codes` command
│ │ ├── indexer.py # File indexing for reproducible sampling
│ │ ├── rng.py # Random number generation helpers
│ │ └── samplers.py # Indexed, daily, and filtered sampling
│ │
│ ├── scraping/
│ │ └── scraper.py # Downloader for Events, GKG 2.1, GKG 1.0, and Mentions
│ │
│ └── utils/
│   ├── branding.py # Terminal ANSI colors and the --version banner (brand system's terminal voice)
│   ├── config.py # Config resolution (--config / env var / CWD / bundled default) and YAML loading
│   ├── io.py # File and chunked-IO helpers
│   └── logging.py # Central logging system
│
├── docs/assets/brand/ # Emblem, favicon, wordmark lockups, icon set, GitHub social preview
├── tests/ # pytest suite (unit tests, no network/browser required)
│
├── main.py # Backward-compatible shim: `python main.py <command>` still works
├── pyproject.toml # Package metadata, build backend, gdeltforge console-script entry point
└── README.md

Command Line Interface

Run pipeline stages using the installed console script:

gdeltforge <command> [options]

python main.py <command> [options] is kept as a backward-compatible equivalent, so existing scripts keep working unchanged. This alias only exists in a source checkout, not in the PyPI package installed above; gdeltforge itself always works either way.

Command Description
scrape Download raw GDELT data (ZIP -> CSV)
convert Convert downloaded CSV files to Parquet
filter Apply row-column filtering to Parquet files
sample Efficient, reproducible sampling
crossref Join a sampled Events output back onto GKG
codes Look up valid CAMEO/FIPS codes across seven column families

The CLI intentionally does not chain stages automatically. You run each stage explicitly to maintain full control.

By default the config is read from ./config/settings.yaml (relative to wherever you run the command from). To point at a config file anywhere else on disk, use --config /path/to/settings.yaml or set the GDELTFORGE_CONFIG environment variable, useful once GdeltForge is installed and invoked from outside the repo checkout. If none of those resolve to a real file, GdeltForge falls back to a built-in default instead of failing; see Configuration.

Usage Examples

Below are practical, beginner-friendly examples covering all common workflows.

Scrape Raw Data

Download the entire archive (--dataset is required on every command; see CLI Reference for the other six choices):

gdeltforge scrape --dataset events

Download only files within a date range (any combination of bounds is valid):

gdeltforge scrape --dataset events --start-date 2020-01-01 --end-date 2023-12-31
gdeltforge scrape --dataset events --start-date 2022-01-01          # from date onward
gdeltforge scrape --dataset events --end-date   2015-12-31          # up to date

The filter applies to all three file types the GDELT archive provides:

File type Example filename Included when
Daily 20200315.export.CSV.zip day falls within range
Monthly 202003.zip month overlaps range
Yearly 2020.zip year overlaps range

Files already present in the download directory are skipped regardless of the date filter.

Downloads run concurrently (scraping.max_workers in config/settings.yaml, default 8) and are checksum-verified: the GDELT index publishes an MD5 per file, which the scraper captures and checks against each download after it completes. A mismatch is treated the same as a network failure: the file is discarded and retried up to scraping.retries times before being reported as failed, so a corrupted or truncated download never silently ends up in the dataset.

Link collection method: requests vs selenium

The scraper needs to read the file index at data.gdeltproject.org/events/ before it can download anything. That index turns out to be a plain, server-rendered HTML directory listing (not JS-rendered), so a headless browser is unnecessary overhead. scraping.method in config/settings.yaml controls which collector is used:

requests (default) selenium
How it works Plain HTTP GET + regex over the HTML Launches headless Chrome, waits for the DOM, reads <a> tags
Dependencies Just requests (already required) Chrome install + a version-matched ChromeDriver + selenium/webdriver-manager (uv pip install '.[selenium]')
Measured speed ~0.4s ~16s (~40x slower)
Failure modes None specific to this site Breaks whenever Chrome auto-updates past the pinned ChromeDriver version (the original reason this option was added)
When to use Always, unless the page below stops being static Fallback only, in case GDELT ever switches this index page to a JS-rendered listing

Both methods were verified to return an identical set of URLs (4,953 files). Timing was a single wall-clock run of each method's link-collection call only (_collect_gdelt_links_requests / _collect_gdelt_links_selenium, timed with time.perf_counter()), back-to-back against the live site on the same machine/network: it does not include the subsequent file downloads, which are identical for both methods. It's a one-off measurement, not an average over multiple runs, so treat the ~40x figure as indicative of the order of magnitude rather than a precise benchmark; the gap is structural (Chrome process startup + page-render wait vs. a single HTTP GET), so it should hold up under repeated measurement.

The site's TLS certificate doesn't match its hostname (it's served off a GCS bucket cert), so both methods intentionally skip certificate verification for this one connection; the actual file downloads happen over plain http://, which sidesteps the issue entirely.

Convert CSV to Parquet

gdeltforge convert --dataset events

Extracts all CSV files from the downloaded ZIP archives and converts them to Parquet files. Each ZIP is processed independently, so conversion runs across a pool of worker processes (converter.max_workers in config/settings.yaml; null, the default, uses all available CPU cores).

Optional: Hive partitioning for historical data

The GDELT archive distributes pre-2013 data in yearly and monthly ZIPs (e.g. 1979.zip, 200601.zip) rather than daily files. When you are working with the full archive (1979-present), keeping those as flat Parquet files means every query scans thousands of files. Enabling Hive partitioning routes those files into a structured directory tree, so filters on Year or MonthYear skip irrelevant files entirely.

This feature is off by default. To enable it, add the following to settings.yaml:

paths:
  # existing paths ...
  parquet_historical_directory: "./data/events/historical"
  filtered_historical_directory: "./data/events/filtered_historical"

converter:
  partitioning:
    enabled: true
    rules:
      - file_type: yearly    # e.g. 1979.zip
        by: ["Year"]
      - file_type: monthly   # e.g. 200601.zip
        by: ["Year", "MonthYear"]

With partitioning enabled, running gdeltforge convert produces two separate output areas:

data/events/
├── parquet/                  # daily files (2013-present), unchanged
│   ├── 20130401.export.parquet
│   └── ...
└── historical/               # yearly/monthly files, Hive-organized
    ├── Year=1979/
    │   └── 1979.parquet
    ├── Year=2006/
    │   └── MonthYear=200601/
    │       └── 200601.parquet
    └── ...

Daily ZIPs (2013-present) always go to parquet_data_directory as flat files. Yearly and monthly ZIPs go to parquet_historical_directory under the directory structure defined by their rule.

Historical ZIPs that have already been converted are tracked with .done marker files, so re-running convert skips them safely.

All downstream stages (filter, sample) detect the historical directory automatically from the config and include its data without any extra flags.

convert also accepts --start-date/--end-date, same as scrape, narrowing which already-downloaded ZIPs get converted:

gdeltforge convert --dataset events --start-date 2020-01-01 --end-date 2020-12-31

Filter the Parquet Dataset

gdeltforge filter --dataset events

Drops rows with missing values in the columns defined in settings.yaml. Also accepts --start-date/--end-date, narrowing which already-converted files get read (which files get filtered, not which rows survive within them).

Sampling

The three sampling modes, indexed, calendar and filtered, plus stratified as filtered's own optional sub-mode

All sampling modes read from the filtered directory by default. Pass --source converted to sample from raw converted Parquet instead, skipping the filter stage entirely.

Note: sampling is already as memory-friendly as the underlying data volume allows, but --source converted will still use noticeably more RAM than the default, since it isn't working from data that's already had its missing-value rows dropped.

Indexed Sampling (Uniform Random)

gdeltforge sample --dataset events --mode indexed -n 10000 --seed 123 --out sample.parquet

This samples 10000 instances considering the entire data.

Calendar Sampling (N Rows Per Period)

gdeltforge sample --dataset events --mode calendar --per-period 20 --out daily.parquet

This samples 20 instances per day from the entire period (1979-present).

Filtered Sampling (Using JSON Filters)

5000 events whose QuadClass is in {1,2}:

gdeltforge sample \
    --dataset events \
    --mode filtered \
    --filter '{"QuadClass": [1, 2]}' \
    -n 5000 \
    --out qc12.parquet

2000 "Verbal Cooperation" events that happened in the USA (ActionGeo_CountryCode is a FIPS 2-letter code, US, not the 3-letter CAMEO code USA that Actor1CountryCode/Actor2CountryCode use):

gdeltforge sample \
    --dataset events \
    --mode filtered \
    --filter '{"ActionGeo_CountryCode": ["US"], "QuadClass": [1]}' \
    -n 2000

You can select specific columns of interest, which is a memory friendly practice:

gdeltforge sample \
    --dataset events \
    --mode filtered \
    --filter '{"ActionGeo_CountryCode": ["US"], "QuadClass": [1]}' \
    --columns GlobalEventID Year Actor1Code \
    -n 1000

It outputs 1000 instances following the same rule as before, but this time with only three columns.

Stratified Sampling (Fixed N Per Group)

Combines a filter with stratified reservoir sampling: draws exactly --n-per-group rows for each distinct value of a chosen column.

gdeltforge sample \
    --dataset events \
    --mode filtered \
    --filter '{"ActionGeo_CountryCode": ["US"]}' \
    --stratify QuadClass \
    --n-per-group 500 \
    --out stratified.parquet

This produces a balanced dataset with 500 USA events per QuadClass value, regardless of the natural class distribution.

--stratify requires --n-per-group. The -n flag is ignored when --stratify is set.

Cross-Referencing Events with GKG

GKG 2.1 joins to Events in two hops through Mentions on the article URL; GKG 1.0 joins directly on EventIds. Both preserve the many-to-many structure.

crossref enriches a sampled Events output with GKG (themes, tone, people, organizations). --gkg-version picks the join strategy: GKG 1.0 carries EventIds directly (a direct join), while GKG 2.1 carries no event ID at all and needs a two-hop join through Mentions on the source article's URL. --gkg-version auto attempts every eligible event against both generations instead of requiring one for the whole sample, useful for a sample spanning both eras (GKG 2.1/Mentions don't exist before 2015-02-18; GKG 1.0 has covered everything since 2013-04-01, and remains live today). All preserve the real many-to-many structure (one event can produce several output rows, one article covering several events contributes one row per event) instead of collapsing it.

gdeltforge scrape --dataset gkg-v2
gdeltforge scrape --dataset mentions
gdeltforge convert --dataset gkg-v2
gdeltforge convert --dataset mentions
gdeltforge filter --dataset gkg-v2
gdeltforge filter --dataset mentions

gdeltforge crossref \
    --events sample.parquet \
    --gkg-version v2 \
    --out sample_with_gkg.parquet

See Recipes for the GKG 1.0 direct-join variant and more detail.

Full Pipeline Examples

Six ready-to-run pipelines (full run, reproducible, USA-only, per-day, shell loops, date-restricted)

Full pipeline: sample 10,000 rows

gdeltforge scrape --dataset events
gdeltforge convert --dataset events
gdeltforge filter --dataset events
gdeltforge sample --dataset events --mode indexed -n 10000

Reproducible sampling

gdeltforge scrape --dataset events
gdeltforge convert --dataset events
gdeltforge filter --dataset events
gdeltforge sample --dataset events --mode indexed -n 5000 --seed 42

USA-only events

gdeltforge scrape --dataset events
gdeltforge convert --dataset events
gdeltforge filter --dataset events
gdeltforge sample \
    --dataset events \
    --mode filtered \
    --filter '{"ActionGeo_CountryCode": ["US"]}' \
    -n 3000

30 events per day

gdeltforge scrape --dataset events
gdeltforge convert --dataset events
gdeltforge filter --dataset events
gdeltforge sample --dataset events --mode calendar --per-period 30

Bash one-liner

gdeltforge scrape --dataset events && \
gdeltforge convert --dataset events && \
gdeltforge filter --dataset events && \
gdeltforge sample --dataset events --mode indexed -n 10000

PowerShell loop

foreach ($c in "scrape", "convert", "filter") {
    gdeltforge $c --dataset events
}
gdeltforge sample --dataset events --mode indexed -n 10000

Date-restricted pipeline

gdeltforge scrape --dataset events --start-date 2020-01-01 --end-date 2023-12-31
gdeltforge convert --dataset events --start-date 2020-01-01 --end-date 2023-12-31
gdeltforge filter --dataset events --start-date 2020-01-01 --end-date 2023-12-31
gdeltforge sample --dataset events --mode indexed -n 10000 --start-date 2020-01-01 --end-date 2023-12-31

scrape, convert, filter, and sample each accept --start-date/--end-date independently. scrape narrows the remote listing it discovers files from; the later stages narrow which already-on-disk files they read, so repeating the same range keeps the whole pipeline focused on that window instead of processing every file already on disk once scrape is done.

Further Examples

The complete filtered-sampling syntax reference and a full set of runnable recipes are in the documentation: see Filtered Sampling and Recipes. For which classical sampling algorithm each mode actually runs, see Sampling Theory.

Logging System

Logging is enabled through a shared logger helper:

from gdeltforge.utils.logging import get_logger
logger = get_logger(__name__)

Logging to a file:

logger = get_logger(__name__, log_to_file=True)

Limitations and Roadmap

GdeltForge is intentionally simple: one pipeline stage per command (no automatic chaining or dependency resolution), CSV -> Parquet only, and scrape/convert/filter now exit non-zero if any individual file fails.

The full, current list of limitations and the roadmap live in one place, the docs site, rather than duplicated here where they'd inevitably drift: see Limitations & Roadmap.

Contributing

Contributions are welcome. See CONTRIBUTING.md for dev setup, the branch/commit conventions this repo follows, and how to propose a change. Please also read the Code of Conduct.

See CHANGELOG.md for a history of notable changes.

Found a security issue? See SECURITY.md rather than opening a public issue.

License

This project is licensed under the Apache License, Version 2.0. See the LICENSE file for details.

Data Attribution

GdeltForge processes data published by the GDELT Project, which makes it available for unlimited and unrestricted use, provided any use or redistribution includes a citation and a link to their site. GdeltForge itself is an independent, unofficial tool, not affiliated with, endorsed by, or sponsored by the GDELT Project.

Metadata

Release files for gdeltforge 0.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gdeltforge 0.10.0
File Size Uploaded
gdeltforge-0.10.0.tar.gz 1.5 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for gdeltforge 0.10.0
File Interpreter ABI Platform
gdeltforge-0.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.6 MB

Release files / gdeltforge-0.10.0.tar.gz

Download URL gdeltforge-0.10.0.tar.gz
Size 1.5 MB
Tags Source
SHA-256 checksum
How to use checksums
7136308b416364b737bda1464787e37f630928f5076df0c7c63d2e4a8e55b095
BLAKE2b-256 checksum
How to use checksums
468261a2d9c31d91cf57cd3c16721f0bbdb6409877a0ee4f6fde18ebd772d70a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release files / gdeltforge-0.10.0-py3-none-any.whl

Download URL gdeltforge-0.10.0-py3-none-any.whl
Size 172.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c007a8a518709144eaf6bfbd463eb2cd5e0107891d5811af3544a1c637a005fb
BLAKE2b-256 checksum
How to use checksums
e10c4e107d3908f4ca0bfe6d0cb8ecd886f01c206f27b632d15a30b09225b4b9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release history Release notifications | RSS feed

0.14.0

2 release files

0.13.0

2 release files

0.11.0

2 release files

This release

0.10.0 This release

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page