Session 11 — Data Acquisition: Files, APIs, Web Data¶
Week 6 · Session 11 · Module A · 90 min · CLO-1
1. Learning objectives¶
By the end of this session, students can:
- Choose the right acquisition method: local files, public APIs, or (carefully) web pages.
- Fetch JSON from an API with requests and turn it into a DataFrame.
- Explain what an API key is, and why some APIs need one.
- Handle common HTTP errors and slow/absent network connections gracefully.
- Describe the ethics and legality of web scraping, and when scraping is not appropriate.
2. Key concepts¶
- Acquisition is a pipeline stage with its own traps: formats, encodings, schemas, permissions.
- APIs — programmatic interfaces: send a request (URL + params), receive structured data (usually JSON).
requests— the standard HTTP library:get(),.status_code,.json().- JSON — nested key-value text; flatten with
pd.json_normalize. - Scraping ≠ API: scraping parses HTML meant for humans; it needs consent, rate limiting, and respect for
robots.txt. - Robustness: check status codes, wrap in
try/except, cache results.
3. Detailed lecture notes¶
Why acquisition? So far data came from built-ins (sns.load_dataset) and
CSVs. In the real world you must obtain data: download exports, query APIs,
read web pages. Acquisition choices shape everything downstream — a dataset with
the wrong granularity or a schema you don't understand will cost hours later.
The lesson: understand where data comes from before you analyze it.
Files first. Recap the I/O from Session 8 (read_csv, read_excel,
read_json) and add the sanity checks: print shape and head(), confirm
dtypes. Mention pd.read_csv(url) — Pandas can read directly from a URL,
which is convenient but requires care with rate limits and reliability; for
course work, prefer downloading once and caching locally.
APIs — the modern data source. An API is a contract: you send a request
(URL, optional parameters, sometimes a key/headers), the server answers with a
status code and a payload (JSON). The requests library does this in three
lines: r = requests.get(url, params={...}), check r.status_code == 200,
then r.json(). Show a live demo with a keyless API (Open-Meteo weather:
https://api.open-meteo.com/v1/forecast?latitude=31.5&longitude=74.3¤t_weather=true).
Parse the JSON: it's nested dicts/lists — pd.json_normalize(r.json()) flattens
nested records into columns. Emphasize: JSON in Python is just dicts and lists —
the skills from Session 2 apply directly.
API keys and ethics. Some APIs require an API key (registration, rate
limits, usage tracking). Rules: never commit keys to Git (.env file +
python-dotenv, or environment variables; .gitignore already ignores .env).
Respect rate limits (time.sleep between calls); read the terms of service.
Introduce this as professional etiquette, not bureaucracy — APIs are shared
infrastructure.
Scraping — when no API exists. Sometimes the only data is HTML. Scraping
parses that HTML (pd.read_html(url) or BeautifulSoup). Warn clearly: scraping
raises legal/ethical issues (terms of service, copyright, server load). Rules of
thumb: check robots.txt; scrape slowly and politely; only public data; better
yet, prefer official APIs or datasets. This course only demonstrates scraping
concepts — we don't run a scraping assignment. This is the first explicit
"responsible data use" moment; it recurs in Session 30.
Robustness. Networks fail: timeouts, 404s, rate limiting. Pattern:
r = requests.get(url, timeout=10)
r.raise_for_status() # raise if 4xx/5xx
data = r.json()
try/except around the call so one failed request doesn't kill your notebook.
Cache the response (data.to_csv(...)) so you don't re-hit the API on every run
— polite to the server and makes your notebook reproducible offline.
4. Important terminology¶
- API — Application Programming Interface: a server endpoint for programmatic data access.
- HTTP status code — 200 OK, 404 Not Found, 429 Too Many Requests, 500 Server Error.
- JSON — JavaScript Object Notation: nested dicts/lists; the lingua franca of APIs.
- Endpoint — a specific URL an API exposes (e.g.,
/v1/forecast). - Query parameter —
?key=valuepairs in the URL. - API key — a secret token identifying you; keep it out of Git.
requests— Python's HTTP client library.pd.json_normalize— flatten nested JSON into a DataFrame.- Scraping — parsing HTML pages for data; needs consent and care.
robots.txt— a site's rules for crawlers/scrapers.- Rate limit — how many requests a server allows per time window.
5. Python examples¶
import requests
import pandas as pd
# --- A keyless public API: Open-Meteo weather forecast ---
url = "https://api.open-meteo.com/v1/forecast"
params = {"latitude": 31.5, "longitude": 74.3, "current_weather": True}
r = requests.get(url, params=params, timeout=10)
print(r.status_code) # 200 means OK
if r.status_code == 200:
data = r.json()
print(data["current_weather"]) # dict with temperature, windspeed, ...
# --- Generic JSON practice: fake todos from JSONPlaceholder ---
todos = requests.get("https://jsonplaceholder.typicode.com/todos", timeout=10)
df = pd.DataFrame(todos.json())
print(df.head())
# --- Robust pattern ---
def fetch_todos():
r = requests.get("https://jsonplaceholder.typicode.com/todos", timeout=10)
r.raise_for_status() # fail loudly on 4xx/5xx
return pd.DataFrame(r.json())
try:
df = fetch_todos()
df.to_csv("datasets/todos.csv", index=False) # cache for offline reproducibility
except requests.exceptions.RequestException as e:
print("Request failed:", e)
6. Beginner example¶
import requests
r = requests.get("https://jsonplaceholder.typicode.com/todos/1", timeout=10)
print(r.json())
# {'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False}
One call, one dict. Everything else is variations: more data, parameters, error handling.
7. Practical Data Science example¶
import requests
import pandas as pd
import time
# Fetch one week of hourly weather for a city, politely and cached
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": 24.86, "longitude": 67.01, # Karachi
"hourly": "temperature_2m,relative_humidity_2m",
"forecast_days": 7,
}
r = requests.get(url, params=params, timeout=15)
r.raise_for_status()
data = r.json()
# Flatten nested hourly arrays into a tidy DataFrame
df = pd.DataFrame({
"time": data["hourly"]["time"],
"temp_c": data["hourly"]["temperature_2m"],
"humidity": data["hourly"]["relative_humidity_2m"],
})
df["time"] = pd.to_datetime(df["time"])
print(df.head())
print("Max temp this week:", df["temp_c"].max(), "°C")
print("Mean humidity:", round(df["humidity"].mean(), 1), "%")
# Cache locally so reruns don't hammer the API
df.to_csv("datasets/karachi-weather.csv", index=False)
time.sleep(1) # be polite to the API
8. In-class activity (50 min)¶
In notebooks/week-06/session-11-data-acquisition.ipynb:
- HTTP basics (10 min): request
https://httpbin.org/status/404and inspectr.status_code; try200. Write a markdown table of the codes you saw. - API → DataFrame (20 min): fetch the weather example for your own city;
flatten into a DataFrame; compute daily mean temperature via
groupbyontime.dt.date. - Robustness (10 min): wrap your fetch in
try/except, addtimeout=10, and cache the result withto_csv; delete the cache and re-run to see it work. - Ethics discussion (10 min): three scenarios — scrape a review site vs. use its official API vs. buy the dataset. Which is appropriate and why? (This feeds the ethics session in Week 15.)
9. Lab exercise¶
Lab 11 (labs/lab-11-data-acquisition.md) — due before Session 12: data
acquisition — fetch a small public API dataset, flatten it, cache it, and load
a local CSV, then answer two checkpoint questions.
10. Common mistakes¶
- Forgetting
.json()and working with the raw response text (or worse, the response object). - Not checking
status_code→ silent empty DataFrames on failure. - No
timeout→ notebooks that hang forever when the network is down. - Committing an API key to the repo (
.envis ignored — use it). - Hammering an API in a loop without
time.sleep→ 429s and possibly a ban. - Re-fetching on every rerun instead of caching — slow, rude, and unreproducible.
- Scraping sites with
robots.txtdisallowing it or with terms of service against it.
11. Short assessment questions¶
- What does
r.status_codetell you? Name one good and one bad code. - What is the difference between
r.textandr.json()? - Why shouldn't you commit an API key to Git?
- What is
pd.json_normalizefor? - Your loop of API calls starts failing with 429. What happened and what do you do?
- True/False: if data is available on a website, scraping it is always legal and ethical. (False — check terms, robots.txt, and copyright.)
12. CLO mapping¶
CLO-1: acquisition is the literal "acquire datasets" verb of CLO-1. The robustness and consent habits introduced here are also the first explicit link to the ethics dimension of CLO-3 (deepened in Sessions 24 and 30).
13. Suggested homework¶
- Finish Lab 11 (
labs/lab-11-data-acquisition.md, due before Session 12) and push. - Practice: fetch another keyless API (e.g.,
https://api.open-meteo.comvariants, or a country/flag/quote API) and flatten it yourself. - Read: the
requestsquickstart (requests.readthedocs.io) — sections "Quickstart" and "Errors and Exceptions". - Preview: build two small DataFrames with a shared key column (
id); Session 12 shows howmerge/concatcombine them and why this is the basis of relational data.