Back to Knowledge Base

Exploring Monthly Stats with Python

Edit

A practical guide to loading, filtering, and visualising M-Lab Monthly Stats parquet files in Python using pandas, without needing BigQuery or a Google Cloud account.

intermediate Data AccessResearch & Analysis

Monthly Stats parquet files are designed to be easy to work with in Python. You need only pandas, pyarrow, and requests — all freely available via pip or uv. No Google Cloud account required.

The fastest way to start is to run the community notebooks directly in your browser:

NotebookBinder link
Introduction & data catalogLaunch ↗
Country-level explorerLaunch ↗
ASN / ISP explorerLaunch ↗
Subdivisions (state/province)Launch ↗
CitiesLaunch ↗
Time seriesLaunch ↗
IQB score calculatorLaunch ↗

Running the Notebooks Locally

To run the notebooks on your own machine:

# Clone the repository
git clone https://github.com/m-lab/mlab-notebooks.git
cd mlab-notebooks/monthlystats

# Install dependencies (with uv, recommended)
uv add 'git+https://github.com/m-lab/iqb.git#subdirectory=library' \
       matplotlib seaborn ipywidgets

# Or with pip
pip install -r requirements.txt

# Start Jupyter
jupyter notebook

The first time you load data, the notebook downloads the relevant parquet files from M-Lab’s public storage and caches them locally in ./cache/. Subsequent runs read from the local cache and start instantly.

Loading a Single File Manually

If you want to work outside the notebooks, here is the minimal pattern:

import pandas as pd
import requests
from io import BytesIO

# Step 1: fetch the manifest to find download URLs
manifest = requests.get(
    "https://measurementlab.net/data/iqb/manifest.json",
    timeout=30,
).json()

# Step 2: pick a file — format is cache/v1/{start}/{end}/{slice}/data.parquet
# Timestamps use YYYYMMDDTHHMMSSZ format
path = "cache/v1/20241001T000000Z/20241101T000000Z/downloads_by_country/data.parquet"
url = manifest["files"][path]["url"]

# Step 3: download and read
response = requests.get(url, timeout=60)
df = pd.read_parquet(BytesIO(response.content))

print(df.shape)          # (N_countries, N_columns)
print(df.columns.tolist())

Common Operations

Get the median download speed for every country

# Sort by median download — higher is better
top = df.nlargest(20, "download_p50")[["country_code", "download_p50", "sample_count"]]
print(top)

Filter out low-sample rows

Rows with few tests have unreliable percentiles. Always filter before ranking:

reliable = df[df["sample_count"] >= 100].copy()

Latency: remember lower is better

# Best latency = SMALLEST values — use nsmallest, not nlargest
best_latency = reliable.nsmallest(10, "latency_p50")[["country_code", "latency_p50"]]

See Reading Percentiles in Monthly Stats for a full explanation of the polarity difference between speed and latency/loss columns.

Load multiple months and compare

months = ["2024-01-01", "2024-04-01", "2024-07-01", "2024-10-01"]

frames = []
for start in months:
    # Convert to timestamp format used in manifest paths
    ts = pd.to_datetime(start).strftime("%Y%m%dT000000Z")
    ts_end = (pd.to_datetime(start) + pd.DateOffset(months=1)).strftime("%Y%m%dT000000Z")
    path = f"cache/v1/{ts}/{ts_end}/downloads_by_country/data.parquet"
    url = manifest["files"][path]["url"]
    month_df = pd.read_parquet(BytesIO(requests.get(url, timeout=60).content))
    month_df["month"] = start
    frames.append(month_df)

all_months = pd.concat(frames, ignore_index=True)

# Median US download over time
us = all_months[all_months["country_code"] == "US"][["month", "download_p50"]]
print(us)

Filter to an ASN (ISP) within a country

ASN slices group by both country code and Autonomous System Number:

# Load the country+ASN slice
path = "cache/v1/20241001T000000Z/20241101T000000Z/downloads_by_country_asn/data.parquet"
url = manifest["files"][path]["url"]
asn_df = pd.read_parquet(BytesIO(requests.get(url, timeout=60).content))

# Filter to US providers with at least 500 tests
us_isps = asn_df[
    (asn_df["country_code"] == "US") &
    (asn_df["sample_count"] >= 500)
].nlargest(15, "download_p50")[["asn", "download_p50", "sample_count"]]

print(us_isps)

Available Slices

Slice nameKey columns
downloads_by_countrycountry_code
uploads_by_countrycountry_code
downloads_by_country_asncountry_code, asn
uploads_by_country_asncountry_code, asn
downloads_by_country_subdivision1country_code, subdivision1
uploads_by_country_subdivision1country_code, subdivision1
downloads_by_country_subdivision1_asncountry_code, subdivision1, asn
downloads_by_country_citycountry_code, city
downloads_by_country_city_asncountry_code, city, asn

Download files contain download_p{N}, latency_p{N}, loss_p{N}. Upload files add upload_p{N}.

Computing IQB Scores

If you want to compute Internet Quality Barometer scores from Monthly Stats, use the mlab-iqb library:

from iqb import IQBCalculator

calculator = IQBCalculator()

# Data dict expected by the calculator
data = {
    "m-lab": {
        "download_throughput_mbps": float(row["download_p95"]),
        "upload_throughput_mbps":   float(row["upload_p95"]),
        "latency_ms":               float(row["latency_p5"]),   # note: p5 = near-best latency
        "packet_loss":              float(row["loss_p5"]),       # note: p5 = near-best loss
    }
}

score = calculator.calculate_iqb_score(data=data)
print(f"IQB score: {score:.3f}")

See the IQB scores notebook for a full walkthrough with country comparisons, use-case breakdowns, and time series.

Further Reading