1: Gathering & Cleaning · 2: Cleaning & the Model · 3: Transformation · 4: Cube Operations · 5: Dashboard
Python and SQL. You need the file Practice 1 produced.
Practice 1 cleaned each of the seven sources separately, then stacked them into one file:
build/consolidated/consolidated_long.csv
It has one row per source per observation, with these columns:
| Column | Example | Meaning |
|---|---|---|
| source_id | S1_worldbank_api | which source this row came from |
| iso3 | USA | country code |
| indicator_cd | GDP_CUR_USD | which measure |
| year | 2024 | year |
| value | 29298000000000 | the number |
| unit | USD | unit of the number |
| flag | FORECAST | empty, or why the value is special |
Today you turn that one long table into a database you can query: a few small description tables and one big table of numbers.
Your file has rows from several sources. Pick one source per indicator and keep only those rows — one line of code. Which source is a judgement call: the World Bank for development series, the IMF for fiscal ones. Write down what you chose.
KEEP = {
"GDP_CUR_USD": "S1_worldbank_api",
"GDP_GROWTH": "S2_imf_datamapper",
"INFLATION_CPI": "S2_imf_datamapper",
"UNEMPLOYMENT": "S1_worldbank_api",
"POPULATION": "S1_worldbank_api",
"EXPORTS_GDP": "S1_worldbank_api",
"GOVT_DEBT_GDP": "S2_imf_datamapper",
"GDP_PCAP_PPP": "S3_worldbank_csv",
"GDP_PCAP_EUR": "S4_eurostat_tsv",
"POLICY_RATE": "S5_wiki_scrape",
}
rows = [r for r in rows
if r["source_id"] == KEEP[r["indicator_cd"]]]
print(len(rows), "rows")One long table is fine for cleaning, but slow and repetitive to query. Split it into small description tables (dimensions) and one big number table (facts).
| Table | One row per | Holds |
|---|---|---|
| dim_entity | country or aggregate | name, ISO3, region, income group, type |
| dim_indicator | indicator | code, label, unit |
| dim_year | year | year, decade, is it a projection |
| dim_source | source | name, format, endpoint |
| fact_observation | country x indicator x year | the value, plus keys to the four dimensions |
Every arrow is a join. The fact table stores four numbers that point at the four dimensions; the dimensions store everything you would want to filter or group by.
Note what dim_entity does: countries and aggregates live in the same table, told apart by one column.
CREATE TABLE dim_entity (
entity_key INTEGER PRIMARY KEY,
iso3 TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
entity_type TEXT NOT NULL, -- 'country' or 'aggregate'
region TEXT,
income_level TEXT
);
CREATE TABLE dim_indicator (
indicator_key INTEGER PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
unit TEXT NOT NULL
);
CREATE TABLE dim_year (
year_key INTEGER PRIMARY KEY,
year INTEGER UNIQUE NOT NULL,
is_projection INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE dim_source (
source_key INTEGER PRIMARY KEY,
source_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
format TEXT NOT NULL
);
CREATE TABLE fact_observation (
entity_key INTEGER NOT NULL REFERENCES dim_entity,
indicator_key INTEGER NOT NULL REFERENCES dim_indicator,
year_key INTEGER NOT NULL REFERENCES dim_year,
source_key INTEGER NOT NULL REFERENCES dim_source,
value REAL NOT NULL,
PRIMARY KEY (entity_key, indicator_key, year_key)
);The primary key on the fact table is the grain: one row per country, indicator and year. If you try to insert a second row for the same three, the database stops you. That is the point.
Facts point at dimensions, so dimensions must exist first. Build a lookup from the natural key to the surrogate key as you go:
entity_keys = {}
for i, iso3 in enumerate(sorted(all_countries), start=1):
entity_keys[iso3] = i
db.execute("INSERT INTO dim_entity VALUES (?,?,?,?,?,?)",
(i, iso3, name[iso3], kind[iso3], region[iso3], income[iso3]))
# then facts, translating each natural key into its number
db.execute("INSERT INTO fact_observation VALUES (?,?,?,?,?)",
(entity_keys[iso3], indicator_keys[code],
year, source_keys[src], value))Now the payoff. Top ten economies in 2024 — note the entity_type filter:
SELECT e.name, f.value / 1e12 AS gdp_trillions FROM fact_observation f JOIN dim_entity e ON e.entity_key = f.entity_key JOIN dim_indicator i ON i.indicator_key = f.indicator_key JOIN dim_year y ON y.year_key = f.year_key WHERE i.code = 'GDP_CUR_USD' AND y.year = 2024 AND e.entity_type = 'country' ORDER BY f.value DESC LIMIT 10;
Delete AND e.entity_type = 'country' and run it again. What comes top now? How many of the ten are real countries?
Open dwhdemo.vedara.net, pick a country, and compare a few numbers against your own database.
Where you differ, it is almost always because you kept a different source. That is a choice, not a bug — but you should be able to say which one you kept and why.
Load. Running the build twice must not create duplicates or half-written tables. We will look at building into a fresh copy, checking it, and only then replacing the live one.