Ranked Movers & Outlier Interpretation
What Ranked Movers Shows
The Ranked Movers page answers "which occupations changed the most?" for a given metric and year. Top gainers are occupations with the highest year-over-year percent change; top losers have the lowest (most negative). The user selects a metric (employment count, mean wage, median wage) and a year — the results change for each combination.
This makes Ranked Movers a discovery tool rather than a fixed report. A user exploring wage trends will see a completely different set of occupations than one exploring employment shifts, even for the same year. The rankings surface occupations that might not appear on anyone's radar through conventional browsing.
The Dual-Direction Sort
Gainers and losers come from the same query — only the sort direction
changes. The key join is through base_metric_key, which connects
each YoY percent change back to the specific base metric the user selected.
-- Same query, only ORDER BY direction changes
movers_sql = """
SELECT o.soc_code, o.occupation_title,
d.derived_value AS pct_change,
abs.derived_value AS abs_change
FROM fact_derived_series d
JOIN dim_metric m ON d.metric_key = m.metric_key
JOIN dim_metric bm ON d.base_metric_key = bm.metric_key
JOIN dim_occupation o ON d.occupation_key = o.occupation_key
...
WHERE m.metric_name = 'yoy_percent_change'
AND bm.metric_name = ? -- user-selected base metric
AND tp.year = ?
ORDER BY d.derived_value {direction}
LIMIT ?
"""
gainers = execute(movers_sql.format(direction="DESC"), params)
losers = execute(movers_sql.format(direction="ASC"), params)
The key is bm.metric_name = ? — this joins through
base_metric_key to filter YoY percent changes for only the selected
base metric. The dim_metric table is joined twice: once for the
derived metric itself (m) and once for the base metric it was derived
from (bm). Different base metrics produce completely different
rankings because the underlying values and their volatility profiles differ.
Small-Denominator Volatility
Percentage changes on small bases are statistically volatile. An occupation with 100 workers gaining 50 shows +50% YoY change, outranking an occupation with 100,000 workers gaining 5,000 (+5%). The percentage is mathematically correct in both cases, but the economic significance is vastly different.
# Both are "real" changes, but one is much more meaningful:
Dancers: 3,850 → 8,930 (+131.9%) # Volatile
Software Developers: 1,795,300 → 1,847,800 (+2.9%) # Stable
The API returns both pct_change and abs_change so the
UI shows absolute context alongside percentages. A +131.9% change in 5,080 jobs
is less economically significant than a +2.9% change in 52,500 jobs. Users see
both numbers side by side and can judge for themselves which changes represent
meaningful labor market shifts versus statistical noise in small populations.
Suppressed Data Creates Apparent Outliers
When BLS suppresses a value in year N but not year N+1 (or vice versa), the YoY calculation is NULL — since the pipeline never imputes, there is simply no derived row. But when suppression is lifted, the "first visible" year may show extreme apparent changes because it is being compared to a period with very different conditions.
For example, if an occupation's wage was suppressed in 2021 and published in 2022, there is no 2021→2022 change. But the 2022→2023 change is computed normally — and if the 2022 value was unusually low (the reason it was near the suppression threshold), the 2023 change may appear artificially large.
Important: Always check absolute changes alongside percentages. A +200% wage increase for Broadcast Announcers reflects small-sample volatility in a niche occupation, not a genuine doubling of the profession's pay.
Year and Metric as Filters
The API returns available_years and defaults to the latest year.
Users can select any combination of metric and year to explore different facets
of the labor market. The static site pre-generates separate JSON files for each
combination so the client-side shim can serve the correct data without a live
server.
movers.json # default (employment_count, latest year)
movers_mean_annual_wage.json # mean wage, latest year
movers-2022.json # employment count, 2022
movers-2022_median_annual_wage.json # median wage, 2022
This URL-to-file mapping is how the static site shim serves different data per
metric and year — each combination is pre-generated at build time by
build_static.py. The naming convention encodes both the year and the
metric in the filename. When the JavaScript requests
/api/trends/movers?year=2022&metric=median_annual_wage, the shim
translates it to movers-2022_median_annual_wage.json and returns the
pre-built response.