Multi-Source Data Aggregator: Combining Two Public APIs Into One Clean Dataset
Data Science & ML Projects (Beginner)
Chapter 6 · Multi-Source Data Aggregator: Combining Two Public APIs Into One Clean Dataset
ds1-5's own merge material, applied to a real join key that has to be derived rather than one that's simply handed to you.
What We're Building
A dataset combining country information from the REST Countries API (population, capital, currency — free, no signup) with live currency exchange rates from an open exchange-rate API (also free, no signup) — producing one table showing each country's population alongside its own currency's current value against the US dollar. Neither API alone can answer "which populous countries currently have the weakest currency" — only the combination can.
Step 1: Fetching From the First API
import requests import pandas as pd response = requests.get( "https://restcountries.com/v3.1/region/europe", params={"fields": "name,capital,population,currencies"}, ) countries_data = response.json() print(len(countries_data), "countries returned")
Same requests.get(url, params={...}) pattern Chapter 3 introduced — just a different service, restricted to one region to keep this example a manageable size.
Step 2: Extracting a Currency Code From Nested JSON
records = [] for c in countries_data: currencies = c.get("currencies", {}) currency_code = list(currencies.keys())[0] if currencies else None records.append({ "country": c["name"]["common"], "capital": c.get("capital", [None])[0], "population": c["population"], "currency_code": currency_code, }) countries_df = pd.DataFrame(records) print(countries_df.head())
Each country's currency arrives as a nested dict keyed by its own three-letter code (e.g. {"EUR": {...}}) — currency_code here isn't a field this API hands over directly, it's derived by pulling the first key out of that nested structure. This derived value is exactly what makes the merge in Step 4 possible at all.
Step 3: Fetching From the Second, Unrelated API
rates_response = requests.get("https://open.er-api.com/v6/latest/USD") rates_data = rates_response.json()["rates"] rates_df = pd.DataFrame(list(rates_data.items()), columns=["currency_code", "usd_rate"]) print(rates_df.head())
This API has never heard of REST Countries and doesn't organize its data by country at all — it's a flat dict of currency codes to exchange rates. rates_data.items() turns that dict into a list of (code, rate) pairs, which becomes a two-column DataFrame — the same currency-code column countries_df already has, arrived at completely independently.
Step 4: The Merge — ds1-5's Own Material, on a Derived Key
combined = countries_df.merge(rates_df, on="currency_code", how="left") print(combined.sort_values("population", ascending=False).head(10))
how="left" keeps every country from countries_df even if its currency code has no match in rates_df — some currency codes are obscure enough that a live exchange-rate feed simply doesn't track them. Those rows end up with NaN in usd_rate, exactly the missing-value situation ds1-4's own cleaning material covers — a real, honest consequence of combining two independently-maintained data sources rather than a mistake in this chapter's own code.
A Quick Combined Insight
top_by_population = combined.sort_values("population", ascending=False).head(10) print(top_by_population[["country", "population", "currency_code", "usd_rate"]])
A table like this — population from one API, live currency strength from a completely different one — genuinely doesn't exist as a single downloadable file anywhere. It only exists because this chapter built it, on the spot, from two independent sources.
The Complete Aggregator
import requests, pandas as pd countries_data = requests.get( "https://restcountries.com/v3.1/region/europe", params={"fields": "name,capital,population,currencies"}, ).json() records = [] for c in countries_data: currencies = c.get("currencies", {}) records.append({ "country": c["name"]["common"], "capital": c.get("capital", [None])[0], "population": c["population"], "currency_code": list(currencies.keys())[0] if currencies else None, }) countries_df = pd.DataFrame(records) rates_data = requests.get("https://open.er-api.com/v6/latest/USD").json()["rates"] rates_df = pd.DataFrame(list(rates_data.items()), columns=["currency_code", "usd_rate"]) combined = countries_df.merge(rates_df, on="currency_code", how="left") combined.to_csv("country_currency_combined.csv", index=False) print(f"Combined {len(combined)} countries across two independent APIs.")
Independently-shaped sources
Two APIs, two different JSON structures, no shared convention between them.
Deriving a join key
currency_code isn't handed over directly — it's pulled from nested JSON first.
how="left" and its honest cost
Keeps every row from the primary source; unmatched keys become real, meaningful NaNs.
Request-count-aware design
One call per API, not one call per row — considerate by design, not just by delay.
Try these on your own:
- Change the region to
"asia","africa", or"americas"and compare how many currency codes fail to find a match in each region. - Use ds1-4's own cleaning toolkit to decide, explicitly, what to do with the countries that ended up with a missing
usd_rate— drop them, or leave them flagged. - Add a third source: a real, free country-flag or ISO-code API, merged in on a different derived key.
- Build a scatter plot of population vs.
usd_rateand look, cautiously (per Chapter 5's own selection-bias warning), for any pattern worth investigating further rather than concluding one exists.
What's Next
Chapter 7: A First Real Classifier — this course's own single, deliberately light touch of ml1, a small, approachable classification project on a well-known dataset.
Chapter 6 Quick Reference
- Extends Chapter 3's own single-API pattern to two independent APIs, combined via ds1-5's own merge material
- A join key can be derived (pulled out of nested JSON) rather than handed over directly by either source
how="left"preserves the primary source's own rows; unmatched keys become real, honest missing values (ds1-4)- Choosing an API that returns all needed data in one call avoids one-request-per-row volume entirely
- The resulting combined table doesn't exist anywhere as a single downloadable file — it's only produced by the combination itself