{
  "id": "P168",
  "slug": "data-anonymizer",
  "key": "P168-data-anonymizer",
  "title": "Data anonymizer",
  "summary": "200 made-up patient records released with names, emails and phones removed: postcode, birth date and sex still single out every one, and a log mask of one asterisk per letter publishes each name's length.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P168 - Data anonymizer\n\nA study release of 200 made-up patient records with every direct\nidentifier removed, and what the fields that were kept still identify.\n\n- `main.eml` - the menu, the release, how many records are alone on what\n  was kept, the masked order log, and one record looked up\n- `anon.eml` - the records, drawn from a fixed seed; what each release\n  keeps of a record; what a masked log line shows; and counting groups\n\nHow each part works:\n\n- Name, email and phone are removed and a check of every column proves it.\n  Postcode, birth date and sex are kept, because the study needs them.\n- As in the corpus case `the-numbers-were-anonymised-and-the-combination-was-unique`,\n  all 200 records are the only one with their postcode, birth date and\n  sex: a voter roll that lists the same three against a name puts a name\n  on each. Kept as postcode and birth year, 196 are alone; as district and\n  year, 165; as district and decade, still 23. No level here makes the\n  smallest group bigger than 1.\n- As in `the-mask-removed-the-value-and-kept-the-length`, the order log\n  masks a name and an email name one asterisk per letter, and keeps the\n  city: 38 of the 200 customers are alone on the two lengths and the city.\n  Eight asterisks for every name leave the city alone to tell lines apart,\n  and no customer alone on it. The build's check that no line holds a\n  name passes on both logs.\n\nWhat is checked: menu choices 1 to 5; a record from 1 to 200. An empty\nanswer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the release, the counts of records alone and the masked log;\n- records 17, 1 and 200.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- records: a cancel, 0, 201, x and 1.5;\n- record 99.\n\nBuilt on the verified corpus cases `the-numbers-were-anonymised-and-the-combination-was-unique`\n(a release with direct identifiers removed and postcode, birth date and\nsex kept) and `the-mask-removed-the-value-and-kept-the-length` (a log\nmask of one asterisk per character).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P168 data anonymizer: a study release with the direct identifiers\n# removed - the release, how many records are alone on what was kept, the\n# masked order log, and one record looked up.\nimport anon\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef stars(n):\n    \"\" => out\n    while len(out) < n:\n        out + \"*\" => out\n    return out\n\ndef levels():\n    return [\"postcode, birth date, sex\", \"postcode, birth year, sex\", \"district, birth year, sex\", \"district, birth decade, sex\"]\n\ndef keys(rs, level):\n    [] => out\n    for r in rs:\n        out + [anon.key(r, level)] => out\n    return out\n\ndef log_keys(rs, mask):\n    [] => out\n    for r in rs:\n        out + [anon.log_key(r, mask)] => out\n    return out\n\ndef row(r):\n    return pad(r[2] + \" \" + str(r[3]), 8) + str(r[4]) + \"-\" + anon.two(r[5]) + \"-\" + anon.two(r[6]) + \"  \" + r[7]\n\ndef show_release():\n    anon.records() => rs\n    (\"A study gets \" + str(len(rs)) + \" patient records. Name, email and phone were removed, and a check of every column proved them gone.\") ^0\n    \"Kept for the study: postcode, birth date and sex, with the measurements. The first six records as released:\" ^0\n    0 => i\n    while i < 6:\n        (\"  \" + lpad(str(i + 1), 3) + \"  \" + row(rs[i])) ^0\n        i + 1 => i\n\ndef show_alone():\n    anon.records() => rs\n    \"What each release keeps, and how many records are alone on it:\" ^0\n    (\"  \" + pad(\"kept\", 30) + lpad(\"alone\", 6) + lpad(\"smallest group\", 16) + lpad(\"groups\", 8)) ^0\n    [] => st\n    0 => level\n    while level < 4:\n        anon.stats(keys(rs, level)) => s\n        st + [s] => st\n        (\"  \" + pad(levels()[level], 30) + lpad(str(s[0]), 6) + lpad(str(s[1]), 16) + lpad(str(s[2]), 8)) ^0\n        level + 1 => level\n    (str(st[0][0]) + \" of \" + str(len(rs)) + \" records are the only one with their postcode, birth date and sex. A voter roll lists the same three against a name, and the join puts a name on each of them.\") ^0\n    (\"Kept as the district and the decade of birth, \" + str(st[3][0]) + \" are still alone. A release hides a person only where the smallest group is more than 1, and no level here gets there.\") ^0\n\ndef show_log():\n    anon.records() => rs\n    \"The order log masks a customer's name and email name one asterisk per letter; the city is not personal data:\" ^0\n    0 => i\n    while i < 3:\n        (\"  order \" + str(1001 + i) + \": \" + stars(len(rs[i][0])) + \" (email \" + stars(rs[i][1]) + \") in \" + rs[i][8]) ^0\n        i + 1 => i\n    anon.stats(log_keys(rs, \"per letter\")) => a\n    anon.stats(log_keys(rs, \"fixed\")) => b\n    (\"One asterisk per letter: \" + str(a[0]) + \" of \" + str(len(rs)) + \" customers are alone on their two lengths and city.\") ^0\n    (\"Eight asterisks for every name: \" + str(b[0]) + \" alone; the city is all a line tells apart.\") ^0\n    \"The build's check that no line holds a customer's name passes on both logs. It asks about the letters, not about the length.\" ^0\n\ndef show_record():\n    anon.records() => rs\n    plain(input(\"record (1 to 200)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    if len(t) > 3 or not digits(t) or int(t) < 1 or int(t) > 200:\n        \"Type a whole number from 1 to 200.\" ^0\n        return\n    rs[int(t) - 1] => r\n    (\"Record \" + t + \": \" + r[2] + \" \" + str(r[3]) + \", born \" + str(r[4]) + \"-\" + anon.two(r[5]) + \"-\" + anon.two(r[6]) + \", \" + r[7] + \". Other records that share what each release keeps:\") ^0\n    0 => level\n    while level < 4:\n        anon.key(r, level) => k\n        anon.groups(keys(rs, level))[k] - 1 => n\n        (\"  \" + pad(levels()[level], 30) + lpad(str(n), 4)) ^0\n        level + 1 => level\n    anon.groups(log_keys(rs, \"per letter\"))[anon.log_key(r, \"per letter\")] - 1 => m\n    \" others\" => word\n    if m == 1:\n        \" other\" => word\n    (\"In the masked log it shares its two lengths and city with \" + str(m) + word + \".\") ^0\n\n\"== Data anonymizer ==\" ^0\n\"A study release with every direct identifier removed, and what the rest still identifies.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_release()\n    elif choice == \"2\":\n        show_alone()\n    elif choice == \"3\":\n        show_log()\n    elif choice == \"4\":\n        show_record()\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import anon\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef stars(n):\n    out = \"\"\n    while len(out) < n:\n        out = out + \"*\"\n    return out\n\ndef levels():\n    return [\"postcode, birth date, sex\", \"postcode, birth year, sex\", \"district, birth year, sex\", \"district, birth decade, sex\"]\n\ndef keys(rs, level):\n    out = []\n    for r in rs:\n        out = out + [anon.key(r, level)]\n    return out\n\ndef log_keys(rs, mask):\n    out = []\n    for r in rs:\n        out = out + [anon.log_key(r, mask)]\n    return out\n\ndef row(r):\n    return pad(r[2] + \" \" + str(r[3]), 8) + str(r[4]) + \"-\" + anon.two(r[5]) + \"-\" + anon.two(r[6]) + \"  \" + r[7]\n\ndef show_release():\n    rs = anon.records()\n    print(\"A study gets \" + str(len(rs)) + \" patient records. Name, email and phone were removed, and a check of every column proved them gone.\")\n    print(\"Kept for the study: postcode, birth date and sex, with the measurements. The first six records as released:\")\n    i = 0\n    while i < 6:\n        print(\"  \" + lpad(str(i + 1), 3) + \"  \" + row(rs[i]))\n        i = i + 1\n\ndef show_alone():\n    rs = anon.records()\n    print(\"What each release keeps, and how many records are alone on it:\")\n    print(\"  \" + pad(\"kept\", 30) + lpad(\"alone\", 6) + lpad(\"smallest group\", 16) + lpad(\"groups\", 8))\n    st = []\n    level = 0\n    while level < 4:\n        s = anon.stats(keys(rs, level))\n        st = st + [s]\n        print(\"  \" + pad(levels()[level], 30) + lpad(str(s[0]), 6) + lpad(str(s[1]), 16) + lpad(str(s[2]), 8))\n        level = level + 1\n    print(str(st[0][0]) + \" of \" + str(len(rs)) + \" records are the only one with their postcode, birth date and sex. A voter roll lists the same three against a name, and the join puts a name on each of them.\")\n    print(\"Kept as the district and the decade of birth, \" + str(st[3][0]) + \" are still alone. A release hides a person only where the smallest group is more than 1, and no level here gets there.\")\n\ndef show_log():\n    rs = anon.records()\n    print(\"The order log masks a customer's name and email name one asterisk per letter; the city is not personal data:\")\n    i = 0\n    while i < 3:\n        print(\"  order \" + str(1001 + i) + \": \" + stars(len(rs[i][0])) + \" (email \" + stars(rs[i][1]) + \") in \" + rs[i][8])\n        i = i + 1\n    a = anon.stats(log_keys(rs, \"per letter\"))\n    b = anon.stats(log_keys(rs, \"fixed\"))\n    print(\"One asterisk per letter: \" + str(a[0]) + \" of \" + str(len(rs)) + \" customers are alone on their two lengths and city.\")\n    print(\"Eight asterisks for every name: \" + str(b[0]) + \" alone; the city is all a line tells apart.\")\n    print(\"The build's check that no line holds a customer's name passes on both logs. It asks about the letters, not about the length.\")\n\ndef show_record():\n    rs = anon.records()\n    t = plain(input(\"record (1 to 200)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    if len(t) > 3 or not digits(t) or int(t) < 1 or int(t) > 200:\n        print(\"Type a whole number from 1 to 200.\")\n        return\n    r = rs[int(t) - 1]\n    print(\"Record \" + t + \": \" + r[2] + \" \" + str(r[3]) + \", born \" + str(r[4]) + \"-\" + anon.two(r[5]) + \"-\" + anon.two(r[6]) + \", \" + r[7] + \". Other records that share what each release keeps:\")\n    level = 0\n    while level < 4:\n        k = anon.key(r, level)\n        n = anon.groups(keys(rs, level))[k] - 1\n        print(\"  \" + pad(levels()[level], 30) + lpad(str(n), 4))\n        level = level + 1\n    m = anon.groups(log_keys(rs, \"per letter\"))[anon.log_key(r, \"per letter\")] - 1\n    word = \" others\"\n    if m == 1:\n        word = \" other\"\n    print(\"In the masked log it shares its two lengths and city with \" + str(m) + word + \".\")\n\nprint(\"== Data anonymizer ==\")\nprint(\"A study release with every direct identifier removed, and what the rest still identifies.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_release()\n    elif choice == \"2\":\n        show_alone()\n    elif choice == \"3\":\n        show_log()\n    elif choice == \"4\":\n        show_record()\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "anon.eml",
      "eml": "# P168 data anonymizer - 200 made-up patient records released for a study\n# with the names, emails and phone numbers removed. As in the corpus case\n# the-numbers-were-anonymised-and-the-combination-was-unique, what was kept,\n# postcode, birth date and sex, is worthless alone and together names one\n# record nearly every time. And as in\n# the-mask-removed-the-value-and-kept-the-length, a log that masks a name\n# with one asterisk per letter publishes its length.\n\ndef surnames():\n    return [\"Ng\", \"Li\", \"Wu\", \"Shah\", \"Khan\", \"Ross\", \"Hall\", \"Wood\", \"Patel\", \"Evans\",\n            \"Ahmed\", \"Moore\", \"Clark\", \"Lewis\", \"Hughes\", \"Turner\", \"Begum\", \"Foster\", \"Morgan\", \"Kowalski\",\n            \"Fernandez\", \"Oyelaran\", \"Bennett\", \"Robinson\", \"Thompson\", \"Lindqvist\", \"Fitzgerald\", \"Okonkwo\", \"Hargreaves\", \"Abernathy\"]\n\ndef districts():\n    return [\"LS1\", \"LS2\", \"LS6\", \"LS7\", \"LS8\", \"LS11\"]\n\ndef cities():\n    return [\"Leeds\", \"York\", \"Hull\", \"Bradford\", \"Wakefield\"]\n\ndef step(x):\n    # the next number from a seed, 1 to 2147483646\n    return (x * 48271) % 2147483647\n\ndef records():\n    # [surname, email name length, district, sector, year, month, day, sex, city]\n    [0] * 200 => out\n    1019202711 => x\n    0 => k\n    while k < 200:\n        step(x) => x\n        surnames()[x % 30] => name\n        step(x) => x\n        len(name) + 2 + x % 3 => email\n        step(x) => x\n        districts()[x % 6] => d\n        step(x) => x\n        1 + x % 9 => sector\n        step(x) => x\n        1940 + x % 66 => year\n        step(x) => x\n        1 + x % 12 => month\n        step(x) => x\n        1 + x % 28 => day\n        step(x) => x\n        \"F\" => sex\n        if x % 2 == 1:\n            \"M\" => sex\n        step(x) => x\n        cities()[x % 5] => city\n        [name, email, d, sector, year, month, day, sex, city] => out[k]\n        k + 1 => k\n    return out\n\ndef two(n):\n    if n < 10:\n        return \"0\" + str(n)\n    return str(n)\n\ndef key(r, level):\n    # what a release keeps of a record, at a level of coarsening:\n    # 0 postcode, birth date, sex; 1 postcode, birth year, sex;\n    # 2 district, birth year, sex; 3 district, birth decade, sex\n    if level == 0:\n        return r[2] + \" \" + str(r[3]) + \"|\" + str(r[4]) + \"-\" + two(r[5]) + \"-\" + two(r[6]) + \"|\" + r[7]\n    if level == 1:\n        return r[2] + \" \" + str(r[3]) + \"|\" + str(r[4]) + \"|\" + r[7]\n    if level == 2:\n        return r[2] + \"|\" + str(r[4]) + \"|\" + r[7]\n    return r[2] + \"|\" + str(r[4] - r[4] % 10) + \"s|\" + r[7]\n\ndef log_key(r, mask):\n    # what a log line shows of a customer: per letter, each name's length\n    # and the city; fixed, eight asterisks and the city\n    if mask == \"per letter\":\n        return str(len(r[0])) + \"|\" + str(r[1]) + \"|\" + r[8]\n    return r[8]\n\ndef groups(keys):\n    # how many records share each key\n    {} => counts\n    for k in keys:\n        if k in counts:\n            counts[k] + 1 => counts[k]\n        else:\n            1 => counts[k]\n    return counts\n\ndef stats(keys):\n    # [records alone on their key, the smallest group, the number of groups]\n    groups(keys) => counts\n    0 => alone\n    0 => smallest\n    0 => n\n    for k in counts:\n        n + 1 => n\n        if counts[k] == 1:\n            alone + 1 => alone\n        if smallest == 0 or counts[k] < smallest:\n            counts[k] => smallest\n    return [alone, smallest, n]\n",
      "python": "def surnames():\n    return [\"Ng\", \"Li\", \"Wu\", \"Shah\", \"Khan\", \"Ross\", \"Hall\", \"Wood\", \"Patel\", \"Evans\", \"Ahmed\", \"Moore\", \"Clark\", \"Lewis\", \"Hughes\", \"Turner\", \"Begum\", \"Foster\", \"Morgan\", \"Kowalski\", \"Fernandez\", \"Oyelaran\", \"Bennett\", \"Robinson\", \"Thompson\", \"Lindqvist\", \"Fitzgerald\", \"Okonkwo\", \"Hargreaves\", \"Abernathy\"]\n\ndef districts():\n    return [\"LS1\", \"LS2\", \"LS6\", \"LS7\", \"LS8\", \"LS11\"]\n\ndef cities():\n    return [\"Leeds\", \"York\", \"Hull\", \"Bradford\", \"Wakefield\"]\n\ndef step(x):\n    return x * 48271 % 2147483647\n\ndef records():\n    out = [0] * 200\n    x = 1019202711\n    k = 0\n    while k < 200:\n        x = step(x)\n        name = surnames()[x % 30]\n        x = step(x)\n        email = len(name) + 2 + x % 3\n        x = step(x)\n        d = districts()[x % 6]\n        x = step(x)\n        sector = 1 + x % 9\n        x = step(x)\n        year = 1940 + x % 66\n        x = step(x)\n        month = 1 + x % 12\n        x = step(x)\n        day = 1 + x % 28\n        x = step(x)\n        sex = \"F\"\n        if x % 2 == 1:\n            sex = \"M\"\n        x = step(x)\n        city = cities()[x % 5]\n        out[k] = [name, email, d, sector, year, month, day, sex, city]\n        k = k + 1\n    return out\n\ndef two(n):\n    if n < 10:\n        return \"0\" + str(n)\n    return str(n)\n\ndef key(r, level):\n    if level == 0:\n        return r[2] + \" \" + str(r[3]) + \"|\" + str(r[4]) + \"-\" + two(r[5]) + \"-\" + two(r[6]) + \"|\" + r[7]\n    if level == 1:\n        return r[2] + \" \" + str(r[3]) + \"|\" + str(r[4]) + \"|\" + r[7]\n    if level == 2:\n        return r[2] + \"|\" + str(r[4]) + \"|\" + r[7]\n    return r[2] + \"|\" + str(r[4] - r[4] % 10) + \"s|\" + r[7]\n\ndef log_key(r, mask):\n    if mask == \"per letter\":\n        return str(len(r[0])) + \"|\" + str(r[1]) + \"|\" + r[8]\n    return r[8]\n\ndef groups(keys):\n    counts = {}\n    for k in keys:\n        if k in counts:\n            counts[k] = counts[k] + 1\n        else:\n            counts[k] = 1\n    return counts\n\ndef stats(keys):\n    counts = groups(keys)\n    alone = 0\n    smallest = 0\n    n = 0\n    for k in counts:\n        n = n + 1\n        if counts[k] == 1:\n            alone = alone + 1\n        if smallest == 0 or counts[k] < smallest:\n            smallest = counts[k]\n    return [alone, smallest, n]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\n0\n4\n201\n4\nx\n4\n1.5\n4\n99\n5\n",
      "screen": "== Data anonymizer ==\nA study release with every direct identifier removed, and what the rest still identifies.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> \nCancelled.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> 0\nType a whole number from 1 to 200.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> 201\nType a whole number from 1 to 200.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> x\nType a whole number from 1 to 200.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> 1.5\nType a whole number from 1 to 200.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> 99\nRecord 99: LS7 7, born 1965-02-10, F. Other records that share what each release keeps:\n  postcode, birth date, sex        0\n  postcode, birth year, sex        0\n  district, birth year, sex        0\n  district, birth decade, sex      2\nIn the masked log it shares its two lengths and city with 6 others.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n17\n4\n1\n4\n200\n5\n",
      "screen": "== Data anonymizer ==\nA study release with every direct identifier removed, and what the rest still identifies.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 1\nA study gets 200 patient records. Name, email and phone were removed, and a check of every column proved them gone.\nKept for the study: postcode, birth date and sex, with the measurements. The first six records as released:\n    1  LS11 5  1974-11-12  M\n    2  LS2 5   1940-07-03  F\n    3  LS2 8   1996-10-12  M\n    4  LS11 2  1996-03-11  F\n    5  LS7 8   1966-07-04  F\n    6  LS6 9   1957-07-02  F\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 2\nWhat each release keeps, and how many records are alone on it:\n  kept                           alone  smallest group  groups\n  postcode, birth date, sex        200               1     200\n  postcode, birth year, sex        196               1     198\n  district, birth year, sex        165               1     182\n  district, birth decade, sex       23               1      76\n200 of 200 records are the only one with their postcode, birth date and sex. A voter roll lists the same three against a name, and the join puts a name on each of them.\nKept as the district and the decade of birth, 23 are still alone. A release hides a person only where the smallest group is more than 1, and no level here gets there.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 3\nThe order log masks a customer's name and email name one asterisk per letter; the city is not personal data:\n  order 1001: ***** (email *******) in Wakefield\n  order 1002: ********* (email ***********) in Bradford\n  order 1003: ****** (email *********) in York\nOne asterisk per letter: 38 of 200 customers are alone on their two lengths and city.\nEight asterisks for every name: 0 alone; the city is all a line tells apart.\nThe build's check that no line holds a customer's name passes on both logs. It asks about the letters, not about the length.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> 17\nRecord 17: LS11 6, born 1997-12-24, F. Other records that share what each release keeps:\n  postcode, birth date, sex        0\n  postcode, birth year, sex        1\n  district, birth year, sex        1\n  district, birth decade, sex      4\nIn the masked log it shares its two lengths and city with 1 other.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> 1\nRecord 1: LS11 5, born 1974-11-12, M. Other records that share what each release keeps:\n  postcode, birth date, sex        0\n  postcode, birth year, sex        0\n  district, birth year, sex        0\n  district, birth decade, sex      3\nIn the masked log it shares its two lengths and city with 5 others.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 4\nrecord (1 to 200)> 200\nRecord 200: LS2 4, born 1949-10-20, F. Other records that share what each release keeps:\n  postcode, birth date, sex        0\n  postcode, birth year, sex        0\n  district, birth year, sex        0\n  district, birth decade, sex      4\nIn the masked log it shares its two lengths and city with 1 other.\n\n1) the release  2) alone on what was kept  3) the masked log  4) one record  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "the-numbers-were-anonymised-and-the-combination-was-unique",
      "caseId": "698-the-numbers-were-anonymised-and-the-combination-was-unique",
      "title": "The numbers were anonymised and the combination was unique"
    },
    {
      "slug": "the-mask-removed-the-value-and-kept-the-length",
      "caseId": "549-the-mask-removed-the-value-and-kept-the-length",
      "title": "The mask removed the value and kept the length"
    }
  ],
  "updated": "2026-10-19"
}
