Contents/ Part XXVIII · Capstone Projects: Sampling & Data Collection/ Chapter 179

Job Postings as a Sampling Frame

Capstone 19. You built this dataset yourself, so there is no sampling error at all. Every listing that matched was captured. The salary estimate is still wrong by eleven percent, and no confidence interval will ever tell you.

⏱️ ~19 min read
🎯 Scraped frames
📊 Chapter 179

The previous two capstones drew samples, and in both of them the analysis turned on sampling error. This one does not sample at all. It takes everything the job board would show, which removes the only error most analyses know how to measure and leaves every other one intact.

The brief
Setting
Every open data-analyst vacancy on one public job board for one region and one quarter, collected by scraping within robots.txt and a set rate limit.
The question
What does a data analyst in this region earn?
Why it matters
The figure would be used to set pay bands. It is exactly the kind of number that gets scraped, published and repeated without anyone asking what population it describes.
What we do
Treat the scrape as a census of the frame rather than a sample, resolve listings back to vacancies, show that salary disclosure is missing not at random, benchmark against an outside source, and measure how much of the gap reweighting can repair.
🖹
A scraped frame is whatever was reachable and not blocked. Nobody designed it for research, nobody documents it, and membership is decided by business practice rather than by a sampling rule. It is a frame all the same, and it carries coverage error like any other.
The finding, up front

Counting rows overstates the market by 71 percent. The scraped mean salary is $64,153 against an official benchmark of $71,800, low by 10.6 percent. And the standard error is $704, so the naive interval is confidently, precisely wrong.

1

A Census of the Frame Has No Sampling Error

The target population is every open data-analyst vacancy in the region this quarter. There is no register of vacancies, so the frame became whatever one public job board would show: vacancies visible on that board, not excluded by robots.txt, and reachable inside the rate limit we set.

Every listing matching the search was captured. That makes this a census of the frame, not a sample of it, and that changes which errors are in play.

Why scraped data is dangerous rather than merely imperfect

Sampling error here is zero. Any confidence interval computed from these data is answering a question that was never at issue. The one error a statistician is trained to look for is the one error that is definitely absent, and the three that remain, coverage, duplication and non-disclosure, have no standard formula and produce no warning in any output.

Scraping ten times as many pages would narrow the interval around exactly the same wrong number. More data does not fix a frame problem.

A CENSUS REMOVES EXACTLY ONE OF THE FOUR ERRORS sampling error = ZERO coverage error 30% never advertised duplication counts inflated 71% non-disclosure salary MNAR the one with a formula the three without one, and the three that are actually present A confidence interval will be computed happily, look reassuringly narrow, and describe the only thing that was never wrong.
The opener's four-error chain, with the census applied. Removing sampling error does not improve the estimate. It removes the one error with a formula attached and leaves the other three exactly where they were.
2

Permissions Come Before Code

The collection plan records what was and was not done, and it belongs in the method section of any scraped study exactly as an ethics approval number would.

CheckWhat was doneWhy it matters
robots.txtChecked and honoredGoverns whether you may collect at all
Terms of serviceRead; collection within permitted useLegal basis, independent of robots.txt
Rate limitOne request per two secondsProtects the host from your convenience
User agentIdentifying string with a contact addressLets the host reach you instead of blocking you
Login wallsNot bypassedCircumvention changes the legal picture entirely
Personal dataNamed recruiters and direct contacts discardedVacancy text is corporate; the contact block is not

The distinction that matters is that "technically possible" is not "permitted". None of the six rows above is enforced by your code failing. A scraper that ignores all of them runs perfectly well, which is precisely why the decisions have to be made deliberately and written down.

3

A Row Is a Listing, Not a Job

This is the first substantive finding, and it comes before any statistics. The crawler ran twice, which is trivial to fix. The real problem is that the same vacancy is advertised repeatedly, sometimes by the employer and sometimes by an agency acting for them under a different name.

Left: a three-bar funnel showing 1,385 scraped rows falling to 1,382 after removing crawler duplicates and then to 808 resolved vacancies. Right: a stacked horizontal bar showing 70 percent of regional vacancies appear on the board and 30 percent are never advertised publicly.
Left: 1,382 listings resolve to 808 vacancies. Counting rows overstates the market by 71 percent, and that is the number a naive row count produces. Right: the error the file cannot see from the inside.
Listings for one vacancyVacancies
1458
2182
3112
456

A headline of "1,382 data-analyst vacancies advertised this quarter" would be wrong by 71 percent. Entity resolution is not a tidying step here; it is the difference between 1,382 jobs and 808.

4

The Salary Is Missing, and Not at Random

Thirty-seven percent of vacancies publish no number, printing "Competitive", "DOE" or "Negotiable" instead. If that were a coin flip it would cost precision and nothing else. It is not a coin flip.

SeniorityVacanciesPublished a salaryMean of those disclosed
Junior21186.3%$50,171
Mid36864.4%$66,020
Senior16546.1%$84,388
Lead6423.4%$101,787
WHAT DROPPING THE BLANKS ACTUALLY REMOVES lower pay higher pay published a number "Competitive" / "DOE" mean of what you keep true mean −10.6%
The blanks are not scattered across the distribution, they are concentrated at one end. Solid bars published a number; faded ones printed "Competitive". Because the faded ones sit to the right, the mean of what survives sits to the left of the truth. A complete-case analysis is not a smaller sample of the same thing; it is a full sample of a different, poorer thing.
Missing not at random, in its purest form

The probability that a value is missing depends on the value itself. Junior roles publish a number 86 percent of the time; lead roles do so 23 percent of the time, and lead roles pay roughly twice as much.

Dropping the blanks, which is what every default does, therefore does not merely lose data. It removes the top of the distribution, and the mean of what remains is pulled down.

5

Compare Against Something Outside the Frame

The damage is invisible from inside the dataset. The only way to see it is a measurement that did not come from the job board, and the regional occupational earnings survey covers all employers, advertised or not.

Scraped mean
$64,153
disclosed salaries only
Official benchmark
$71,800
covers all employers
Gap
−10.6%
−$7,647
Standard error
$704
and utterly beside the point

The naive 95 percent interval runs from $62,773 to $65,534. It does not contain the benchmark, and it never would however many pages were scraped, because the error is not sampling error. That is the sentence to carry out of this chapter.

6

How Much Can Be Repaired

Some of the damage is structure we can see. Disclosure varies by seniority, and seniority is known for every vacancy whether or not it published a salary. That makes it usable as a weighting variable, exactly as age was in Capstone 17.

EstimateValueGapFixes
Unadjusted, disclosed only$64,153−10.6%nothing
Reweighted to the board's seniority mix$68,465−4.6%the disclosure filter
Reweighted to the region's seniority mix$69,754−2.9%disclosure and coverage skew
Official benchmark$71,800
Left: bars of salary disclosure rate by seniority falling from 86 percent at junior to 23 percent at lead, with the mean disclosed salary printed inside each bar rising from 31k to 64k. Right: four bars comparing the scraped mean, two reweighted estimates and the official benchmark against a dashed benchmark line.
Left: disclosure and pay move in opposite directions, which is what makes the missingness so damaging. Right: the two reweightings recover 56 and 73 percent of the gap respectively, and neither closes it.

What remains is unreachable. Within every seniority band, the employers who published a number still pay less than those who did not, so each band mean is itself biased downward. No weighting scheme built from the board can see inside a band it never observed.

7

Who Never Appeared

Thirty percent of the region's vacancies never reached any public board. Absent vacancies leave no trace in the file, so this is the error the analysis cannot see from the inside at all; the employer census supplies the number.

GroupReaches a public boardPays
Small employers79.2%around average
Medium employers74.3%around average
Large employers50.5%above average
Junior roles75.4%below average
Lead roles58.2%well above average
The two errors point the same way, which is the worst case

Under-coverage removes senior roles at large employers. Non-disclosure removes high salaries within every band. Neither cancels the other and both drag the estimate down.

Had they pointed in opposite directions the scraped mean might have looked correct while resting on two large offsetting errors, which is a more dangerous situation than a visibly wrong number, because nothing would have prompted anyone to look.

8

The Variable That Survived

Remote share, board
36.5%
from the scrape
Remote share, benchmark
37.2%
official
Gap
−1.9%
against −10.6% for salary

Whether a role is remote is printed on essentially every listing, so there is no disclosure filter. Coverage still applies, but remote working is not strongly related to whether a vacancy reaches a public board, so the two-stage filter that wrecked the salary estimate leaves this one nearly intact.

The transferable lesson

Salary is filtered twice, by coverage and by disclosure. Remote status is filtered once, and roughly neutrally. A dataset is not trustworthy or untrustworthy as a whole. Each variable has to be argued for separately, and the argument is always about the mechanism that decided whether the value would exist at all.

9

What to Watch

10

Scraped Frames in Data Science & AI

Most large modern datasets are scraped, and almost none ship with a statement of what population they represent.

Where it appearsThe frame, stated honestly
Web-scale training corporaText that was reachable, not paywalled and not blocked, in the languages that dominate the open web
Product review datasetsReviews from customers motivated enough to write one, on platforms that permit collection
Public code repositoriesCode its authors chose to publish, under licenses permitting reuse
Social media panelsAccounts that are public, active, and not rate-limited away during collection
Price or listing monitorsWhatever the site rendered to an unauthenticated visitor on the day
Practice note

The duplication problem in section 3 has a direct analog at scale. Web corpora contain the same document many times over, mirrored, syndicated and quoted, and near-duplicate removal is standard practice precisely because raw counts overstate coverage and repeated text is over-weighted in training. The salary problem has an analog too: a field is present only when someone chose to publish it, and that choice is rarely independent of the value. Any statistic computed over rows where a field happens to be populated inherits whatever decided the populating.

🐍

The full project, step by step

The companion notebook prints the collection plan and its permissions, resolves 1,382 listings to 808 vacancies and shows one vacancy advertised four times under three titles, voids the scraper's parse failures, tabulates salary disclosure against seniority, compares the result to an external benchmark, runs two levels of reweighting and reports what each recovers, quantifies the coverage shortfall it cannot reach, and closes on the one variable that came through intact.

📓 View Notebook (code & outputs) ▶ Open in Colab ⬇ View / Download on GitHub
Read the reports & get the data

The dataset (capstone-web-scraping-job-postings.xlsx) holds the raw scrape with every flaw intact: crawler duplicates, one vacancy under several employer names, parse failures and undisclosed salaries. It also carries the written collection plan with its permissions record, and an external benchmark sheet. Two written reports accompany it: a plain-language brief for a research lead, and a technical report covering entity resolution, the missingness mechanism and the reweighting.

🎓 Key Takeaways

  • A census of the frame has zero sampling error and can still be badly wrong. It removes the warning light, not the fault.
  • A row is a listing, not a job. 1,382 listings were 808 vacancies; counting rows overstates the market by 71%.
  • Salary is missing not at random. Disclosure falls from 86% at junior level to 23% at lead level, so dropping blanks removes the top of the distribution.
  • Reweighting recovered 73% of the gap and no more. What is left is inside bands the board never observed.
  • Judge each variable separately. The same frame was 10.6% wrong on salary and 1.9% wrong on remote share.
11

Quiz: Test Yourself

Eight questions on scraped frames, entity resolution and missingness. Answer them, hit Check Answers, and keep refining until you score 100%. Your progress is saved.