The field everybody filters on is the field hardest to parse. A freelance budget is a string written by a marketplace in a language you chose, describing a shape that may have no lower bound, no upper bound, or no number at all — and every one of those cases breaks a naive parser differently.
A budget is not a number
"USD 100 - 250" is a range. "Less than USD 50" has no minimum. "Over USD 3,000" has no maximum. "Open" has no figure. Flattening all of those into budget_min and budget_max without recording which case it was throws away the information that makes the numbers comparable.
The five shapes, measured
budget_type | Share | Meaning |
|---|---|---|
range | 70.6% | "USD 100 - 250" — both bounds exist |
max_only | 25.0% | "Less than USD 50" — no lower bound exists |
min_only | 4.4% | "Over USD 3,000" — no upper bound exists |
fixed | rare | A single figure; min and max are the same |
unspecified | rare | "Open" — Workana shows no figure at all |
Measured on 272 budgets. The distinction is decided by structure rather than vocabulary: a budget string with no digits is unspecified in any language, which is what makes the classification robust across locales.
Missing versus non-existent
This is the whole point of budget_type. budget_min is null on 25% of rows, and reporting that as a coverage gap would be wrong — the client posted “less than USD 50”, so there is no minimum to report. Nothing failed.
The consequence for analysis is direct: averaging budget_min across a mixed set silently drops a quarter of the projects, and the quarter it drops is the cheap end. The average comes out high and looks plausible.
-- Wrong: silently excludes 25% of projects, biasing upward
SELECT AVG(budget_min) FROM projects;
-- Right: say which population you are describing
SELECT budget_type, COUNT(*), AVG(budget_min), AVG(budget_max)
FROM projects
WHERE budget_unit = 'project'
GROUP BY budget_type;The separator that moves with the language
Workana renders its text in the language you request, and the thousands separator follows:
| String | Locale | Actual value |
|---|---|---|
"USD 1,000" | en | 1000 |
"USD 1.000" | es | 1000 |
A parser that assumes the English convention reads the Spanish string as 1 — a three-orders-of-magnitude error in the field people filter on most, and one that produces a suspiciously large population of one-dollar projects rather than an exception.
So the Actor parses per locale and records locale on every row. If you ever see a cluster of implausibly small budgets, that field is the first thing to check.
Why every parsed field keeps its original
budget_min: 100 travels with budget_raw: "USD 100 - 250". total_bids: 30 travels with total_bids_raw: "Propuestas: 30".
The parsed number is an interpretation; the raw string is the marketplace's data. Keeping both means a disagreement is discoverable rather than invisible — the same principle as `fuel_type_raw` on coches.net and the Korean category original on Naver.
Per project or per hour
Some budgets are quoted "USD 15 - 45 / hour", so budget_unit is hour rather than project. An average that mixes the two is meaningless in a way that is hard to spot, because both populations produce plausible-looking numbers.
is_hourly is true on 11.7% of projects, so this is not a rounding error — it is one project in nine.
Analysis that survives all of this
- Group by
budget_typeandbudget_unitbefore computing anything. Two group-bys, and most of the traps are gone. - Report distributions, not averages. A median and quartiles within a type say more than a mean across types.
- Use
budget_maxwhen you need one number. It is present on 95.6% of rows against 75.0% for the minimum, because most budgets have a ceiling and a quarter have no floor. - Sanity-check against
budget_rawon a sample. Ten rows read by eye catches a locale bug that no aggregate will reveal.


