Notes · SQL and Python

SQL or Python? Yes.

Same question, two languages, one answer. Plus a county where one tile out-tests everybody.

At the end of Percentiles I promised an answer to "should I use SQL or Python?" Here it is: yes. The answer is 100% yes. I use both every week, and they're good at different jobs. So instead of picking a side, I gave them the same question and made them race.

The question

What's the median fixed download speed in every Texas county, every quarter of 2025?

The data is Ookla's open performance tiles, shared publicly through Ookla for Good. Each tile is about 600x600 meters and carries an average download speed plus a count of how many tests were taken in each tile. For Texas in 2025 that's 535,217 tile-quarters and about 6.7 million tests across all 254 counties.

How the work actually split

  1. Get the data: SQL. This step is the E and the L in ELT: extract, load, transform. You pull the raw data out of wherever it lives (extract), park it somewhere you can work with it (load), and do the real shaping afterward (transform). Ookla publishes one Parquet file per quarter for the whole planet, which is a lot more internet than I need. So DuckDB reads those files right off S3 and only grabs the Texas tiles and the nine columns I care about. Four files covering the planet go in, and one 10 MB Texas file lands on my laptop. Steps 2 and 3 are the T.
  2. Put each tile in a county: Python. Each tile comes with a point (its center) but no county. Figuring out which county a point falls inside is a spatial join, sometimes called point-in-polygon. SQL can do this too! DuckDB has a spatial extension, and so do Postgres and most warehouses. I still reach for Python here for a few reasons. geopandas reads the Census county map file directly, warns me if the two layers use different coordinate systems, and builds a spatial index on its own, so 535,000 points don't each get checked against all 254 counties. And when something looks off, I can drop the tiles on a map in a couple of lines and see it. The join itself is one line, and it ran in about 15 seconds.
  3. Compute the medians: both. This is the race.
  4. Prove they match: Python. A small script runs both versions and fails loudly if they disagree. They agree on all 1,016 county-quarter rows.

Notice neither language won every step. Moving and shrinking data is SQL's bread and butter. Geometry, gluing steps together and checking the work is where Python shines.

Two medians, not one

Here's the twist that made this fun. "Median speed by county" sounds like one number, and it's really two.

The middle tile: line up every tile from slowest to fastest and take the one in the middle. Each tile counts once, whether it had 1 test or 1,000.

The middle test: same line, but keep a running total of tests as you walk from the slow end. The tile where you pass the halfway mark is your median. Busy tiles count more.

Here's the middle-test version in SQL (trimmed a little for the page):

SELECT county, quarter,
       MEDIAN(mbps) AS median_tile,
       -- first tile where the running total of tests passes half
       MIN(mbps) FILTER (WHERE running >= total / 2.0) AS median_test
FROM (
    SELECT *,
           SUM(tests) OVER (PARTITION BY county, quarter
                            ORDER BY mbps ROWS UNBOUNDED PRECEDING) AS running,
           SUM(tests) OVER (PARTITION BY county, quarter) AS total
    FROM tiles
)
GROUP BY ALL;

And the same thing in pandas:

keys = ["county", "quarter"]
out = tiles.groupby(keys).agg(median_tile=("mbps", "median"))

# sort slowest first, keep a running total of tests
tiles = tiles.sort_values(keys + ["mbps"])
running = tiles.groupby(keys)["tests"].cumsum()
total = tiles.groupby(keys)["tests"].transform("sum")
out["median_test"] = tiles[running >= total / 2].groupby(keys)["mbps"].min()

Which one reads better? SQL reads like a description of the answer, while pandas reads more like a recipe.

What the two medians said

All of TexasMiddle tileMiddle test
Q1 2025288 Mbps360 Mbps
Q2 2025283 Mbps361 Mbps
Q3 2025293 Mbps372 Mbps
Q4 2025309 Mbps401 Mbps

Same data, same quarter, and a 92 Mbps gap in Q4 just from asking a slightly different question. Busy tiles tend to be in urban or suburban strata, and those strata tend to have faster service. Across Texas in Q4, tiles with a single test had a median of 145 Mbps. Tiles with 20 or more tests came in at 409 Mbps.

Where Texas tiles actually land

Fixed tiles by average download speed, Q4 2025, 50 Mbps bins

Histogram of 136,555 Texas fixed tiles in Q4 2025 by average download speed, in 50 Mbps bins. The 10th percentile is 49 Mbps, the middle tile 309 Mbps and the middle test 401 Mbps. 0 2k 4k 6k 8k 10k 12k 14k 0-50 Mbps: 13,857 tiles 50-100 Mbps: 13,553 tiles 100-150 Mbps: 10,675 tiles 150-200 Mbps: 8,956 tiles 200-250 Mbps: 9,045 tiles 250-300 Mbps: 10,011 tiles 300-350 Mbps: 11,430 tiles 350-400 Mbps: 11,583 tiles 400-450 Mbps: 11,307 tiles 450-500 Mbps: 10,013 tiles 500-550 Mbps: 7,616 tiles 550-600 Mbps: 5,491 tiles 600-650 Mbps: 3,836 tiles 650-700 Mbps: 2,653 tiles 700-750 Mbps: 1,770 tiles 750-800 Mbps: 1,310 tiles 800-850 Mbps: 867 tiles 850-900 Mbps: 657 tiles 900-950 Mbps: 654 tiles 950-1000 Mbps: 333 tiles 1,000+ Mbps: 938 tiles 0 200 400 600 800 1,000+ average download per tile, Mbps tiles 10th percentile 49 Mbps Middle tile 309 Mbps Middle test 401 Mbps Histogram of 136,555 Texas fixed tiles in Q4 2025 by average download speed, in 50 Mbps bins. The 10th percentile is 49 Mbps, the middle tile 309 Mbps and the middle test 401 Mbps. 0 4k 8k 12k tiles 0-50 Mbps: 13,857 tiles 50-100 Mbps: 13,553 tiles 100-150 Mbps: 10,675 tiles 150-200 Mbps: 8,956 tiles 200-250 Mbps: 9,045 tiles 250-300 Mbps: 10,011 tiles 300-350 Mbps: 11,430 tiles 350-400 Mbps: 11,583 tiles 400-450 Mbps: 11,307 tiles 450-500 Mbps: 10,013 tiles 500-550 Mbps: 7,616 tiles 550-600 Mbps: 5,491 tiles 600-650 Mbps: 3,836 tiles 650-700 Mbps: 2,653 tiles 700-750 Mbps: 1,770 tiles 750-800 Mbps: 1,310 tiles 800-850 Mbps: 867 tiles 850-900 Mbps: 657 tiles 900-950 Mbps: 654 tiles 950-1000 Mbps: 333 tiles 1,000+ Mbps: 938 tiles 0 200 400 600 800 1,000+ average download per tile, Mbps 10th percentile 49 Mbps Middle tile 309 Mbps Middle test 401 Mbps
One tile in ten averages under 50 Mbps (the teal bar). The middle tile sits at 309 Mbps, and the middle test, which leans toward busy tiles, sits at 401. It replays every few seconds; hover a bar for its count. Source: Speedtest® by Ookla® open data (fixed tiles) via Ookla for Good.

The middle tile and the middle test can tell very different stories

Median fixed download speed in Mbps, Q4 2025

Q4 2025 median download speed, middle tile versus middle test, for Texas and six counties. Values are listed in the table below the chart. 0 100 200 300 400 500 600 Middle tile Middle test Texas (all tiles) Texas (all tiles): middle tile 309 Mbps Texas (all tiles): middle test 401 Mbps 309 401 Harris Harris: middle tile 388 Mbps Harris: middle test 410 Mbps Dallas Dallas: middle tile 404 Mbps Dallas: middle test 413 Mbps Bexar Bexar: middle tile 397 Mbps Bexar: middle test 426 Mbps Travis Travis: middle tile 377 Mbps Travis: middle test 410 Mbps Falls Falls: middle tile 93 Mbps Falls: middle test 508 Mbps 93 508 Reeves Reeves: middle tile 153 Mbps Reeves: middle test 62 Mbps 62 153 Q4 2025 median download speed, middle tile versus middle test, for Texas and six counties. Values are listed in the table below the chart. 0 200 400 600 Middle tile Middle test Texas (all tiles) Texas (all tiles): middle tile 309 Mbps Texas (all tiles): middle test 401 Mbps 309 401 Harris Harris: middle tile 388 Mbps Harris: middle test 410 Mbps Dallas Dallas: middle tile 404 Mbps Dallas: middle test 413 Mbps Bexar Bexar: middle tile 397 Mbps Bexar: middle test 426 Mbps Travis Travis: middle tile 377 Mbps Travis: middle test 410 Mbps Falls Falls: middle tile 93 Mbps Falls: middle test 508 Mbps 93 508 Reeves Reeves: middle tile 153 Mbps Reeves: middle test 62 Mbps 62 153
Q4 2025Middle tileMiddle test
Texas (all tiles)309 Mbps401 Mbps
Harris388 Mbps410 Mbps
Dallas404 Mbps413 Mbps
Bexar397 Mbps426 Mbps
Travis377 Mbps410 Mbps
Falls93 Mbps508 Mbps
Reeves153 Mbps62 Mbps
Source: Speedtest® by Ookla® Global Fixed and Mobile Network Performance Maps, fixed tiles, Q4 2025. Counties: US Census Bureau. Hover a marker for its value.

In the big metros the two medians land close together. Harris, Dallas, Bexar and Travis are all within about 35 Mbps. The fun is in the small counties.

Reeves County: one tile, two devices

In Q4, Reeves County had 2,124 tests spread across 243 tiles. One of those tiles had 1,044 of them, almost half the county, from just 2 devices. That tile averaged 62 Mbps.

So the middle test for all of Reeves County is 62 Mbps, because that one tile sits right on the halfway mark. The middle tile says 153 Mbps.

I don't know who's behind those 1,044 tests, and the data can't tell me. What I do know is that a test-weighted median listens to whoever talks the most. This is why I check the devices column next to tests before I trust a weighted number in a small area. There are techniques used to ensure these situations don't impact final results of analysis, but for the sake of this conversation, let's leave it here.

Falls County: the opposite problem

Falls County flips it around. The middle tile is 93 Mbps and the middle test is 508 Mbps.

More than half its tiles have 2 tests or fewer, and those tiles have a median around 77 Mbps. The tiles with 10 or more tests have a median of 508 Mbps, and about 60% of their tests come from in and around Marlin, the county seat. The tile median hears from the countryside. The test median hears from town.

Neither number is wrong. They answer different questions: "what does a typical place in this county get?" versus "what does a typical test in this county see?" Pick the one that matches your question, and say which one you picked.

So which do I reach for?

Everything is in the data-stories repo: the download script, the county join, both versions and the check. Run it yourself and tell me if I got something wrong!

Next week: how many speed tests is enough to trust a number at all? Reeves County is going to come up again.

Speedtest® by Ookla® Global Fixed and Mobile Network Performance Maps, from Ookla for Good via the AWS Open Data Registry. Based on analysis by Ookla of Speedtest Intelligence® data for Q1 to Q4 2025. Provided by Ookla and accessed 13 October 2026. Ookla trademarks used under license and reprinted with permission. Data licensed CC BY-NC-SA 4.0. County boundaries: US Census Bureau.

NEXT · DATA QUALITYPercentiles: the 10th tells you who's suffering

The median shows you the typical test. The 10th percentile shows you the people having a rough time.

NEXT · DATA QUALITYAverages lie

Why the median tells you more about your internet speed than the mean ever will.