Occupation Generator 3.0.1
Employment status and occupation by country, age, sex and education for C++23
Loading...
Searching...
No Matches
Usage Guide

This guide covers every feature of the occupation-generator library. For a quick overview, see the README. For the data and the model, see Data and Method. For the full API reference, run doxygen Doxyfile from the repository root and open doc/api/html/index.html.

Quick Start

#include <iostream>
int main()
{
auto& gen = dasmig::ocg::instance();
std::cout << gen.get_occupation({.country = "BR", .age = 35}) << '\n'; // for example "Shop assistant"
}
static ocg & instance()
Access the global singleton instance.
Occupation Generator library — employment status, education and occupation by country,...

Installation

Copy dasmig/occupationgen.hpp, dasmig/core.hpp and the dasmig/core/ directory into your include path, keeping them together. Supply resources/ next to the program, or define DASMIG_OCCUPATION_EMBED to compile the lite tier in.

Loading Resources

instance() loads the lite tier on first use (or full if there is no lite), from the compiled-in copy or from the first of resources/, ../resources, occupation-generator/resources and ../occupation-generator/resources that has it.

dasmig::ocg gen; // an empty, independent instance
bool ok = gen.load(dasmig::dataset::lite); // false when the tier is not found
gen.load(std::filesystem::path{"path/to/resources/lite"}); // a directory
gen.has_data();
gen.country_count(); // countries that can be drawn from
gen.job_count(); // jobs in the catalogue
Occupation Generator: draws a person's employment status, education and job.
bool has_data() const
Check whether status data has been loaded.
std::size_t job_count() const
Return the number of jobs in the catalogue.
std::size_t country_count() const
Return the number of countries that can be drawn from.
void load(const std::filesystem::path &dir)
Load a resource directory, replacing all data and layers loaded before.

Each load replaces the data and every layer loaded before. The tiers have the same measured data and differ in the catalogue: lite has 95 jobs, full 320 (see Data and Method).

Generating

Every call takes an occupation_request; every field may be omitted. These are the only generation overloads, and choices are never stored as instance defaults:

.country = "JP", // ISO 3166-1 alpha-2; empty draws one
.age = 28, // default 40
.person_sex = dasmig::sex::female, // unknown draws one (even odds)
.education = dasmig::education_level::advanced, // unknown draws one
.status = dasmig::employment_status::employed, // unknown draws one
};
auto v = gen.get_occupation(req); // draws a seed from the instance engine
auto w = gen.get_occupation(req, 1234); // deterministic; does not touch the engine
auto a = gen.get_occupation({.country = "US"}); // age 40, everything else drawn
auto b = gen.get_occupation({}); // a random country as well
occupation get_occupation(const occupation_request &request)
Generate a random occupation for a request.
Per-call generation choices. Every field may be omitted.
std::string_view country
ISO 3166-1 alpha-2 code; empty draws a country (uniform over those loaded).

The old no-argument, country-only and seed-only overloads have been removed. Use a request (including {} for the defaults) first, followed by a seed when deterministic output is needed. Data loading and layers remain instance-level configuration.

  • Status. From the country's employment and unemployment rates for the person's sex and five-year age band; the inactive are split into students, retired and others by age. From 6 to 14 a person is always a student; under 6, inactive with no job.
  • Education. From the country's shares for the sex and age band, unless given. A person too young for a level has the one below.
  • Job. An ISCO-08 group is drawn by the country's employment shares for the sex, shifted by how common the group is at the person's age and education, then a job of the catalogue within the group. A job with an age limit is not given to someone outside it. A student has no job; an unemployed, retired or inactive person has the job the data would give a worker like them (their usual or last occupation).
// The worker and their working conditions, with a forced status:
auto nurse_or_so = gen.get_occupation({.country = "DE", .age = 45,
.status = dasmig::employment_status::employed});

A country chosen at random is uniform over the countries loaded; the data has no population column. Pass a country chosen by country-generator for a population-weighted world.

The Result

v.country_code; // "JP"
v.person_sex; // dasmig::sex::female
v.age; // 28
v.status; // employed, unemployed, student, retired or inactive
v.education; // std::optional<education_level>; empty under 15 unless given
v.job; // std::optional<job_info>; empty for students
if (v.job)
{
v.job->key; // "software_developer"
v.job->label; // "Software developer"
v.job->isco; // "25", the ISCO-08 group
v.job->isco_label; // "Information and communications technology professionals"
v.job->unit; // "2512", the ISCO-08 unit group
v.job->skill; // "3-4", the ISCO-08 skill level
v.job->skills; // {"programming", "debugging", "algorithms"}
v.job->gear; // {"laptop", "headphones"}
v.job->tags; // {"technical", "desk"}: for other generators
}
dasmig::to_string(v.status); // "employed"
dasmig::to_string(*v.education); // "advanced"

A result converts to std::string (the job label, or the status when there is no job) and streams with <<.

Seeding and Deterministic Generation

  • get_occupation(request, seed) gives the same result for the same seed, request and data, on every platform.
  • get_occupation(request) draws a seed from the instance's entropy-seeded engine. Instance seed() / unseed() controls have been removed; deterministic calls always take an explicit seed.
  • v.seed() is the seed a result was generated from: get_occupation(request, v.seed()) replays it.
  • Each field has a stream of its own (country, sex, status, education, ISCO group, job), so forcing one in the request never changes the others. The same seed with .education forced gives the same sex and status.

For a deterministic sequence, keep the seed source with the caller:

dasmig::core::rng seeds{7};
const dasmig::occupation_request request{.country = "BR", .age = 35};
for (int i = 0; i < 5; ++i)
{
auto v = gen.get_occupation(request, seeds());
}

Extending Data

The shipped data is a base layer. Files with the same columns as the shipped tables can be loaded on top of it to add rows, change them or remove them, without editing the shipped files. The tables and their keys:

Table (ocg::table) File Key
status status.tsv country_code, sex, band
inactivity inactivity.tsv band
education education.tsv country_code, sex, band
isco isco.tsv code
shares shares.tsv country_code, sex, code
factors factors.tsv country_code, major, dimension, value
occupations occupations.tsv key
using table = dasmig::ocg::table;
// Add a job to a group (here 54, protective services), from text; for
// example supplied by a WebAssembly host. In override mode (the default) a
// row replaces the loaded row with its key and new keys are added.
gen.extend_from_string(table::occupations,
"key\tlabel\tisco\tweight\ttags\tskills\tgear\n"
"bounty_hunter\tBounty hunter\t54\t5\trisk;solo\ttracking\tvest;handcuffs\n", "scenario");
// Remove jobs: a layer needs only the key and a disabled column.
gen.extend_from_string(table::occupations, "key\tdisabled\nbounty_hunter\t1\n", "cleanup");
// A directory with any of the files, applied together or not at all.
gen.extend("mods/post-collapse");
// One file; add mode refuses to touch keys that are already loaded.
gen.extend("mods/jobs.tsv", dasmig::core::layer_mode::add);
table
The data tables.
const core::layer_info & extend_from_string(table which, std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table from TSV text (for example from a WebAssembly host).
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add data layers from a directory or a file.
  • A group's jobs share the group's weight by their weight, so a job added to a group takes a share of what the group already has. Give a new group (isco.tsv) rows in shares.tsv, or it is never drawn.
  • A band is 15-19 or 65+ (no upper limit). The bands of status.tsv and education.tsv need not match the shipped ones; a person is looked up by age in the rows of their country and sex.
  • A row that names an ISCO group which is not loaded is ignored and listed in unresolved(), so removing a group needs no other change.
  • Every value in a layer is checked before anything changes. A bad layer throws dasmig::core::tsv_error with the file, line and column, and the data stays as it was.
  • layers(table) lists what is loaded: source, mode, rows added, replaced and removed, and warnings such as unknown columns.
  • load(...) replaces the base and drops every layer.

Replay and JSON

Every result records the seed and the data it came from:

auto v = gen.get_occupation({});
v.signature().seed; // the call seed
v.signature().data; // gen.data_hash() when it was generated
const core::seed_signature & signature() const
The seed and the data this occupation was generated from.

to_json() gives the result as JSON, the form the WebAssembly layer and the game consume. Seeds are strings because JavaScript numbers cannot hold every 64-bit value. to_json(writer) writes into an existing dasmig::core::json_writer, for composing larger documents.

{"country_code":"DE","sex":"female","age":40,"status":"unemployed","education":"advanced","job":{"key":"accountant","label":"Accountant","isco":"24","isco_label":"Business and administration professionals","unit":"2411","skill":"3-4","skills":["accounting","auditing","spreadsheets"],"gear":["suit","calculator"],"tags":["desk","academic"]},"signature":{"seed":"000000000000002a","data":"1810ef13288a98e8"}}

education and job are null when there are none.

Compiled-in Data

Define DASMIG_OCCUPATION_EMBED before including the header to compile the lite tier into the program. load(dataset::lite) and instance() then use it and need no files. The compiled-in data has the same data_hash() as the file, so seeds replay the same either way. The header is about 2 MB.

dasmig/occupationgen_data.hpp is generated from resources/lite by make embed; CI checks it is up to date.

Thread Safety

Each ocg instance is independent. Concurrent calls to get_occupation(request) on the same instance require external synchronisation (e.g. a std::mutex).

Batch Generation

Every seeded getter has batch forms: get_occupation_batch return a std::vector, and for_each_occupation streams the items to a sink in index order without holding the batch.

auto& gen = dasmig::ocg::instance();
const auto request = dasmig::occupation_request{};
// 1,000 results from one batch seed, on four threads.
auto items = gen.get_occupation_batch(request, {.count = 1000, .threads = 4}, 42);
// Item 1 replays on its own: the batch seed and the index give its seed.
auto again = gen.get_occupation(request, dasmig::core::item_seed(42, 1));
// A million results to a file as JSON Lines, never all in memory.
std::ofstream out{"items.jsonl"};
dasmig::core::jsonl_writer lines{out};
gen.for_each_occupation(request, {.count = 1'000'000, .threads = 0}, 42, lines);
void for_each_occupation(const occupation_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of occupation results to a sink without holding the whole batch.
std::vector< occupation > get_occupation_batch(const occupation_request &request, const core::batch_options &options)
Generate a batch of occupation results from a seed drawn like get_occupation() draws its own.
  • Item seeds. Item i is exactly get_occupation(request, core::item_seed(batch_seed, first + i)). A range can be generated directly (first = 1000 starts at item 1000), two batches that follow each other equal one, and the output is the same for any threads (0 means the hardware count). Without a batch_seed one is drawn like the single getter draws its own.
  • Options. core::batch_options has count, first, threads and unique. The request describes one item; the options describe the batch.
  • Uniqueness. .unique = core::unique_rule::by<occupation>(key) regenerates an item whose key already appeared in the batch, with a deterministic retry seed, and throws std::runtime_error naming the item when the attempts run out.
  • Sinks. core::jsonl_writer writes one to_json() per line, core::csv_writer flattens each result's JSON into dotted columns. Any callable (std::uint64_t index, occupation&) works.
  • Batch calls use the same const getters from several threads, which the thread-safety rules above allow; the CI runs the batch tests under ThreadSanitizer.

Error Reference

Exception When
std::runtime_error Any generation call before data is loaded
std::invalid_argument An unknown country code; a country with no status row for the person's sex and age; no job open to the person (a country with no shares for the sex, or a job age limit that excludes everyone); with no country, no country has status rows for both sexes
std::invalid_argument extend() of a file whose name is not a table's
dasmig::core::tsv_error A malformed table file or layer (message gives file, line and column); the data is unchanged