[{"data":1,"prerenderedAt":2149},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python":2115,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python":2124,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python":2129,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python":2130},{"id":4,"title":5,"body":6,"dateModified":2081,"datePublished":2081,"description":2082,"extension":2083,"faq":2084,"keyword":2093,"kind":2094,"meta":2095,"navigation":244,"path":2104,"seo":2105,"slug":2108,"stem":2109,"topics":2110,"type":2113,"__hash__":2114},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python\u002Findex.md","Anonymise Personal Data in an Excel File with Python",{"type":7,"value":8,"toc":2067},"minimark",[9,19,24,53,67,209,213,216,340,347,351,364,605,613,617,620,763,770,774,781,1010,1018,1022,1025,1344,1363,1513,1517,1525,1605,1693,1699,1703,1706,1936,1952,1956,1995,1999,2002,2006,2012,2018,2024,2030,2034,2063],[10,11,12,13,18],"p",{},"A support team wants to share ticket data with an analytics agency. HR needs to send salary data to a benchmarking survey. A developer needs a realistic test file. In each case the workbook holds names, emails, dates of birth and addresses that must not leave the organisation, while the rest of the data must stay useful. Deleting the name column is not enough: a free-text note can still say \"called Mrs Okafor on 07700 900123\", and a 67-year-old in a small postcode with an unusual job title is identifiable without any name at all. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",", works through the techniques in the order they are usually applied, and ends with checks that prove the output is clean. It is a technical guide, not legal advice: what counts as anonymous enough depends on your jurisdiction and the recipient.",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl\n",[10,54,55,58,59,62,63,66],{},[32,56,57],{},"customers.xlsx"," has a ",[32,60,61],{},"Customers"," sheet with an ID, name, email, date of birth, postcode, plan, monthly spend and a free-text notes column, plus a ",[32,64,65],{},"Tickets"," sheet keyed by the same customer ID. One cell carries a comment, and the file's author property names a person — both are places personal data hides.",[68,69,77,78,77,82,77,86,77,94,77,104,77,115,77,122,77,127,77,132,77,138,77,142,77,146,77,151,77,158,77,163,77,167,77,172,77,179,77,184,77,188,77,193,77,200,77,205],"svg",{"viewBox":70,"role":71,"ariaLabelledBy":72,"xmlns":75,"style":76},"44 0 672 537","img",[73,74],"an-layers-t","an-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:672px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[79,80,81],"title",{"id":73},"What happens to each kind of column",[83,84,85],"desc",{"id":74},"Layers from most to least identifying. Direct identifiers are dropped or masked, IDs are replaced with keyed pseudonyms, quasi-identifiers such as date of birth and postcode are generalised, free text is scrubbed, and ordinary attributes are kept.",[87,88],"rect",{"x":89,"y":90,"width":91,"height":92,"fill":93},"44","0","672","537","#ffffff",[87,95],{"x":96,"y":97,"width":98,"height":99,"rx":100,"fill":101,"stroke":102,"style":103},"60.0","16.0","640.0","93.0","12","#fce9e9","var(--danger,#dc2626)","stroke-width:2px",[105,106,108],"g",{"transform":107},"translate(368.0,29.0) scale(1.000)",[109,110],"path",{"d":111,"fill":112,"stroke":113,"style":114},"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M4 21c0-4 3.6-7 8-7s8 3 8 7","none","var(--accent-ink,#be185d)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[116,117,121],"text",{"x":118,"y":119,"style":120},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Direct identifiers — name, email",[116,123,126],{"x":118,"y":124,"style":125},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","drop, or mask when the shape matters",[87,128],{"x":96,"y":129,"width":98,"height":99,"rx":100,"fill":130,"stroke":131,"style":103},"119.0","#fee8f2","var(--accent,#f43f8f)",[105,133,135],{"transform":134},"translate(368.0,132.0) scale(1.000)",[109,136],{"d":137,"fill":112,"stroke":113,"style":114},"M8 11a4 4 0 1 0 .01 0z M11 13l9-9 M17 7l2 2 M15 9l2 2",[116,139,141],{"x":118,"y":140,"style":120},"178.5","Keys — CustomerID",[116,143,145],{"x":118,"y":144,"style":125},"198.0","keyed pseudonym, consistent across sheets and files",[87,147],{"x":96,"y":148,"width":98,"height":99,"rx":100,"fill":149,"stroke":150,"style":103},"222.0","#fdefd8","var(--gold,#b4740a)",[105,152,154],{"transform":153},"translate(368.0,235.0) scale(1.000)",[109,155],{"d":156,"fill":112,"stroke":157,"style":114},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[116,159,162],{"x":118,"y":160,"style":161},"281.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Quasi-identifiers — birth date, postcode",[116,164,166],{"x":118,"y":165,"style":125},"301.0","generalise: age band, postcode district",[87,168],{"x":96,"y":169,"width":98,"height":99,"rx":100,"fill":170,"stroke":171,"style":103},"325.0","#f0f4ff","var(--brand,#5b5cf0)",[105,173,175],{"transform":174},"translate(368.0,338.0) scale(1.000)",[109,176],{"d":177,"fill":112,"stroke":178,"style":114},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--brand-strong,#4338ca)",[116,180,183],{"x":118,"y":181,"style":182},"384.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Free text — notes",[116,185,187],{"x":118,"y":186,"style":125},"404.0","scrub emails, phone numbers, names",[87,189],{"x":96,"y":190,"width":98,"height":99,"rx":100,"fill":191,"stroke":192,"style":103},"428.0","#d9f4f1","var(--teal,#0f9488)",[105,194,196],{"transform":195},"translate(368.0,441.0) scale(1.000)",[109,197],{"d":198,"fill":112,"stroke":199,"style":114},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[116,201,204],{"x":118,"y":202,"style":203},"487.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Attributes — plan, spend",[116,206,208],{"x":118,"y":207,"style":125},"507.0","keep",[20,210,212],{"id":211},"step-1-drop-what-the-recipient-does-not-need","Step 1 — Drop what the recipient does not need",[10,214,215],{},"The safest identifier is one that is not in the file. Start from the recipient's actual question and keep only the columns that answer it:",[25,217,221],{"className":218,"code":219,"language":220,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndf = pd.read_excel(\"customers.xlsx\", sheet_name=\"Customers\")\ntickets = pd.read_excel(\"customers.xlsx\", sheet_name=\"Tickets\")\nout = df.drop(columns=[\"Name\"])\nprint(list(out.columns))\n","python",[32,222,223,239,246,276,299,324],{"__ignoreMap":30},[35,224,225,229,233,236],{"class":37,"line":38},[35,226,228],{"class":227},"s-kum","import",[35,230,232],{"class":231},"skGVy"," pandas ",[35,234,235],{"class":227},"as",[35,237,238],{"class":231}," pd\n",[35,240,242],{"class":37,"line":241},2,[35,243,245],{"emptyLinePlaceholder":244},true,"\n",[35,247,249,252,255,258,261,264,268,270,273],{"class":37,"line":248},3,[35,250,251],{"class":231},"df ",[35,253,254],{"class":227},"=",[35,256,257],{"class":231}," pd.read_excel(",[35,259,260],{"class":45},"\"customers.xlsx\"",[35,262,263],{"class":231},", ",[35,265,267],{"class":266},"sa561","sheet_name",[35,269,254],{"class":227},[35,271,272],{"class":45},"\"Customers\"",[35,274,275],{"class":231},")\n",[35,277,279,282,284,286,288,290,292,294,297],{"class":37,"line":278},4,[35,280,281],{"class":231},"tickets ",[35,283,254],{"class":227},[35,285,257],{"class":231},[35,287,260],{"class":45},[35,289,263],{"class":231},[35,291,267],{"class":266},[35,293,254],{"class":227},[35,295,296],{"class":45},"\"Tickets\"",[35,298,275],{"class":231},[35,300,302,305,307,310,313,315,318,321],{"class":37,"line":301},5,[35,303,304],{"class":231},"out ",[35,306,254],{"class":227},[35,308,309],{"class":231}," df.drop(",[35,311,312],{"class":266},"columns",[35,314,254],{"class":227},[35,316,317],{"class":231},"[",[35,319,320],{"class":45},"\"Name\"",[35,322,323],{"class":231},"])\n",[35,325,327,331,334,337],{"class":37,"line":326},6,[35,328,330],{"class":329},"sP0c6","print",[35,332,333],{"class":231},"(",[35,335,336],{"class":329},"list",[35,338,339],{"class":231},"(out.columns))\n",[10,341,342,343,346],{},"An analytics agency studying spend by plan and region does not need names at all, so ",[32,344,345],{},"Name"," goes. Email is kept for one more step only to show masking; in most real shares it would be dropped here too.",[20,348,350],{"id":349},"step-2-replace-ids-with-keyed-pseudonyms","Step 2 — Replace IDs with keyed pseudonyms",[10,352,353,354,356,357,359,360,363],{},"The customer ID links ",[32,355,61],{}," to ",[32,358,65],{},", and the recipient needs that link. Replace the ID with a pseudonym that is the same everywhere the ID appears but cannot be reversed. A plain hash is not enough — anyone can hash every possible ",[32,361,362],{},"C-1xxxx"," and match them up. Use a keyed hash (HMAC) with a secret that stays in-house:",[25,365,367],{"className":218,"code":366,"language":220,"meta":30,"style":30},"import hashlib\nimport hmac\nimport os\n\nSECRET = os.environ.get(\"PSEUDONYM_KEY\", \"use-a-long-random-secret-from-a-vault\").encode()\n\ndef pseudonym(value, prefix=\"P\"):\n    digest = hmac.new(SECRET, str(value).encode(), hashlib.sha256).hexdigest()\n    return f\"{prefix}-{digest[:10]}\"\n\nout[\"CustomerID\"] = out[\"CustomerID\"].map(pseudonym)\ntickets[\"CustomerID\"] = tickets[\"CustomerID\"].map(pseudonym)\nprint(out[\"CustomerID\"].head(3).tolist())\nprint(\"join still works:\", len(out.merge(tickets, on=\"CustomerID\")) == len(out))\n",[32,368,369,376,383,390,394,416,420,441,462,503,508,530,549,568],{"__ignoreMap":30},[35,370,371,373],{"class":37,"line":38},[35,372,228],{"class":227},[35,374,375],{"class":231}," hashlib\n",[35,377,378,380],{"class":37,"line":241},[35,379,228],{"class":227},[35,381,382],{"class":231}," hmac\n",[35,384,385,387],{"class":37,"line":248},[35,386,228],{"class":227},[35,388,389],{"class":231}," os\n",[35,391,392],{"class":37,"line":278},[35,393,245],{"emptyLinePlaceholder":244},[35,395,396,399,402,405,408,410,413],{"class":37,"line":301},[35,397,398],{"class":329},"SECRET",[35,400,401],{"class":227}," =",[35,403,404],{"class":231}," os.environ.get(",[35,406,407],{"class":45},"\"PSEUDONYM_KEY\"",[35,409,263],{"class":231},[35,411,412],{"class":45},"\"use-a-long-random-secret-from-a-vault\"",[35,414,415],{"class":231},").encode()\n",[35,417,418],{"class":37,"line":326},[35,419,245],{"emptyLinePlaceholder":244},[35,421,423,426,430,433,435,438],{"class":37,"line":422},7,[35,424,425],{"class":227},"def",[35,427,429],{"class":428},"s_Opv"," pseudonym",[35,431,432],{"class":231},"(value, prefix",[35,434,254],{"class":227},[35,436,437],{"class":45},"\"P\"",[35,439,440],{"class":231},"):\n",[35,442,444,447,449,452,454,456,459],{"class":37,"line":443},8,[35,445,446],{"class":231},"    digest ",[35,448,254],{"class":227},[35,450,451],{"class":231}," hmac.new(",[35,453,398],{"class":329},[35,455,263],{"class":231},[35,457,458],{"class":329},"str",[35,460,461],{"class":231},"(value).encode(), hashlib.sha256).hexdigest()\n",[35,463,465,468,471,474,478,481,484,487,489,492,495,498,500],{"class":37,"line":464},9,[35,466,467],{"class":227},"    return",[35,469,470],{"class":227}," f",[35,472,473],{"class":45},"\"",[35,475,477],{"class":476},"sSjpA","{",[35,479,480],{"class":231},"prefix",[35,482,483],{"class":476},"}",[35,485,486],{"class":45},"-",[35,488,477],{"class":476},[35,490,491],{"class":231},"digest[:",[35,493,494],{"class":329},"10",[35,496,497],{"class":231},"]",[35,499,483],{"class":476},[35,501,502],{"class":45},"\"\n",[35,504,506],{"class":37,"line":505},10,[35,507,245],{"emptyLinePlaceholder":244},[35,509,511,514,517,520,522,525,527],{"class":37,"line":510},11,[35,512,513],{"class":231},"out[",[35,515,516],{"class":45},"\"CustomerID\"",[35,518,519],{"class":231},"] ",[35,521,254],{"class":227},[35,523,524],{"class":231}," out[",[35,526,516],{"class":45},[35,528,529],{"class":231},"].map(pseudonym)\n",[35,531,533,536,538,540,542,545,547],{"class":37,"line":532},12,[35,534,535],{"class":231},"tickets[",[35,537,516],{"class":45},[35,539,519],{"class":231},[35,541,254],{"class":227},[35,543,544],{"class":231}," tickets[",[35,546,516],{"class":45},[35,548,529],{"class":231},[35,550,552,554,557,559,562,565],{"class":37,"line":551},13,[35,553,330],{"class":329},[35,555,556],{"class":231},"(out[",[35,558,516],{"class":45},[35,560,561],{"class":231},"].head(",[35,563,564],{"class":329},"3",[35,566,567],{"class":231},").tolist())\n",[35,569,571,573,575,578,580,583,586,589,591,593,596,599,602],{"class":37,"line":570},14,[35,572,330],{"class":329},[35,574,333],{"class":231},[35,576,577],{"class":45},"\"join still works:\"",[35,579,263],{"class":231},[35,581,582],{"class":329},"len",[35,584,585],{"class":231},"(out.merge(tickets, ",[35,587,588],{"class":266},"on",[35,590,254],{"class":227},[35,592,516],{"class":45},[35,594,595],{"class":231},")) ",[35,597,598],{"class":227},"==",[35,600,601],{"class":329}," len",[35,603,604],{"class":231},"(out))\n",[10,606,607,608,612],{},"Because the key is the same, next month's extract produces the same pseudonym for the same customer, so the recipient can follow customers over time without knowing who they are. Truncating to ten hex characters keeps the pseudonyms readable; with 16¹⁰ possible values, collisions are not a concern for customer lists. Keep the key in a secret store, not in the script — see ",[14,609,611],{"href":610},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely\u002F","Store Credentials for Excel Report Scripts Safely",". Pseudonymised data is still personal data under laws such as the GDPR, because whoever holds the key can reverse it; only when the key is destroyed does it approach anonymous.",[20,614,616],{"id":615},"step-3-mask-values-whose-shape-matters","Step 3 — Mask values whose shape matters",[10,618,619],{},"Sometimes the recipient needs to see that an email exists and which domain it belongs to — to count business versus consumer customers, say — but not the address itself:",[25,621,623],{"className":218,"code":622,"language":220,"meta":30,"style":30},"def mask_email(e):\n    if not isinstance(e, str) or \"@\" not in e:\n        return e\n    user, domain = e.split(\"@\", 1)\n    return f\"{user[0]}***@{domain}\"\n\nout[\"Email\"] = out[\"Email\"].map(mask_email)\nprint(out[\"Email\"].head(3).tolist())\n",[32,624,625,635,668,676,696,727,731,749],{"__ignoreMap":30},[35,626,627,629,632],{"class":37,"line":38},[35,628,425],{"class":227},[35,630,631],{"class":428}," mask_email",[35,633,634],{"class":231},"(e):\n",[35,636,637,640,643,646,649,651,654,657,660,662,665],{"class":37,"line":241},[35,638,639],{"class":227},"    if",[35,641,642],{"class":227}," not",[35,644,645],{"class":329}," isinstance",[35,647,648],{"class":231},"(e, ",[35,650,458],{"class":329},[35,652,653],{"class":231},") ",[35,655,656],{"class":227},"or",[35,658,659],{"class":45}," \"@\"",[35,661,642],{"class":227},[35,663,664],{"class":227}," in",[35,666,667],{"class":231}," e:\n",[35,669,670,673],{"class":37,"line":248},[35,671,672],{"class":227},"        return",[35,674,675],{"class":231}," e\n",[35,677,678,681,683,686,689,691,694],{"class":37,"line":278},[35,679,680],{"class":231},"    user, domain ",[35,682,254],{"class":227},[35,684,685],{"class":231}," e.split(",[35,687,688],{"class":45},"\"@\"",[35,690,263],{"class":231},[35,692,693],{"class":329},"1",[35,695,275],{"class":231},[35,697,698,700,702,704,706,709,711,713,715,718,720,723,725],{"class":37,"line":301},[35,699,467],{"class":227},[35,701,470],{"class":227},[35,703,473],{"class":45},[35,705,477],{"class":476},[35,707,708],{"class":231},"user[",[35,710,90],{"class":329},[35,712,497],{"class":231},[35,714,483],{"class":476},[35,716,717],{"class":45},"***@",[35,719,477],{"class":476},[35,721,722],{"class":231},"domain",[35,724,483],{"class":476},[35,726,502],{"class":45},[35,728,729],{"class":37,"line":326},[35,730,245],{"emptyLinePlaceholder":244},[35,732,733,735,738,740,742,744,746],{"class":37,"line":422},[35,734,513],{"class":231},[35,736,737],{"class":45},"\"Email\"",[35,739,519],{"class":231},[35,741,254],{"class":227},[35,743,524],{"class":231},[35,745,737],{"class":45},[35,747,748],{"class":231},"].map(mask_email)\n",[35,750,751,753,755,757,759,761],{"class":37,"line":443},[35,752,330],{"class":329},[35,754,556],{"class":231},[35,756,737],{"class":45},[35,758,561],{"class":231},[35,760,564],{"class":329},[35,762,567],{"class":231},[10,764,765,766,769],{},"Masking keeps the domain, which is often all an analysis needs. Be careful with small organisations: ",[32,767,768],{},"j***@tiny-firm.example"," may point to one person. When in doubt, reduce the column to what is actually needed — here, perhaps just the domain's top-level suffix.",[20,771,773],{"id":772},"step-4-generalise-the-quasi-identifiers","Step 4 — Generalise the quasi-identifiers",[10,775,776,777,780],{},"A date of birth plus a postcode identifies most people, even without a name. Replace them with coarser values: an age band instead of a birth date, the postcode district (",[32,778,779],{},"SW1A",") instead of the full postcode:",[25,782,784],{"className":218,"code":783,"language":220,"meta":30,"style":30},"as_of = pd.Timestamp(\"2026-09-29\")\nage = (as_of - out[\"DateOfBirth\"]).dt.days \u002F\u002F 365\nout[\"AgeBand\"] = pd.cut(age, bins=[0, 30, 40, 50, 60, 120], right=False,\n                        labels=[\"under 30\", \"30–39\", \"40–49\", \"50–59\", \"60+\"]).astype(str)\nout[\"District\"] = out[\"Postcode\"].str.split().str[0]\nout = out.drop(columns=[\"DateOfBirth\", \"Postcode\"])\nprint(out[[\"CustomerID\", \"AgeBand\", \"District\", \"Plan\"]].to_string(index=False))\n",[32,785,786,801,827,889,928,952,975],{"__ignoreMap":30},[35,787,788,791,793,796,799],{"class":37,"line":38},[35,789,790],{"class":231},"as_of ",[35,792,254],{"class":227},[35,794,795],{"class":231}," pd.Timestamp(",[35,797,798],{"class":45},"\"2026-09-29\"",[35,800,275],{"class":231},[35,802,803,806,808,811,813,815,818,821,824],{"class":37,"line":241},[35,804,805],{"class":231},"age ",[35,807,254],{"class":227},[35,809,810],{"class":231}," (as_of ",[35,812,486],{"class":227},[35,814,524],{"class":231},[35,816,817],{"class":45},"\"DateOfBirth\"",[35,819,820],{"class":231},"]).dt.days ",[35,822,823],{"class":227},"\u002F\u002F",[35,825,826],{"class":329}," 365\n",[35,828,829,831,834,836,838,841,844,846,848,850,852,855,857,860,862,865,867,870,872,875,878,881,883,886],{"class":37,"line":248},[35,830,513],{"class":231},[35,832,833],{"class":45},"\"AgeBand\"",[35,835,519],{"class":231},[35,837,254],{"class":227},[35,839,840],{"class":231}," pd.cut(age, ",[35,842,843],{"class":266},"bins",[35,845,254],{"class":227},[35,847,317],{"class":231},[35,849,90],{"class":329},[35,851,263],{"class":231},[35,853,854],{"class":329},"30",[35,856,263],{"class":231},[35,858,859],{"class":329},"40",[35,861,263],{"class":231},[35,863,864],{"class":329},"50",[35,866,263],{"class":231},[35,868,869],{"class":329},"60",[35,871,263],{"class":231},[35,873,874],{"class":329},"120",[35,876,877],{"class":231},"], ",[35,879,880],{"class":266},"right",[35,882,254],{"class":227},[35,884,885],{"class":329},"False",[35,887,888],{"class":231},",\n",[35,890,891,894,896,898,901,903,906,908,911,913,916,918,921,924,926],{"class":37,"line":278},[35,892,893],{"class":266},"                        labels",[35,895,254],{"class":227},[35,897,317],{"class":231},[35,899,900],{"class":45},"\"under 30\"",[35,902,263],{"class":231},[35,904,905],{"class":45},"\"30–39\"",[35,907,263],{"class":231},[35,909,910],{"class":45},"\"40–49\"",[35,912,263],{"class":231},[35,914,915],{"class":45},"\"50–59\"",[35,917,263],{"class":231},[35,919,920],{"class":45},"\"60+\"",[35,922,923],{"class":231},"]).astype(",[35,925,458],{"class":329},[35,927,275],{"class":231},[35,929,930,932,935,937,939,941,944,947,949],{"class":37,"line":301},[35,931,513],{"class":231},[35,933,934],{"class":45},"\"District\"",[35,936,519],{"class":231},[35,938,254],{"class":227},[35,940,524],{"class":231},[35,942,943],{"class":45},"\"Postcode\"",[35,945,946],{"class":231},"].str.split().str[",[35,948,90],{"class":329},[35,950,951],{"class":231},"]\n",[35,953,954,956,958,961,963,965,967,969,971,973],{"class":37,"line":326},[35,955,304],{"class":231},[35,957,254],{"class":227},[35,959,960],{"class":231}," out.drop(",[35,962,312],{"class":266},[35,964,254],{"class":227},[35,966,317],{"class":231},[35,968,817],{"class":45},[35,970,263],{"class":231},[35,972,943],{"class":45},[35,974,323],{"class":231},[35,976,977,979,982,984,986,988,990,992,994,997,1000,1003,1005,1007],{"class":37,"line":422},[35,978,330],{"class":329},[35,980,981],{"class":231},"(out[[",[35,983,516],{"class":45},[35,985,263],{"class":231},[35,987,833],{"class":45},[35,989,263],{"class":231},[35,991,934],{"class":45},[35,993,263],{"class":231},[35,995,996],{"class":45},"\"Plan\"",[35,998,999],{"class":231},"]].to_string(",[35,1001,1002],{"class":266},"index",[35,1004,254],{"class":227},[35,1006,885],{"class":329},[35,1008,1009],{"class":231},"))\n",[10,1011,1012,1013,1017],{},"The age calculation divides days by 365, which is off by a day or two near birthdays; for bands ten years wide that does not matter. Choose band widths with the recipient: wider bands protect more and tell less. ",[14,1014,1016],{"href":1015},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut\u002F","Bin Values into Ranges with pandas cut"," covers the binning options.",[20,1019,1021],{"id":1020},"step-5-scrub-free-text","Step 5 — Scrub free text",[10,1023,1024],{},"Notes columns are where identifiers leak. Replace anything that looks like an email address or a phone number, and remove names you know about:",[25,1026,1028],{"className":218,"code":1027,"language":220,"meta":30,"style":30},"import re\n\nEMAIL = re.compile(r\"[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+\")\nPHONE = re.compile(r\"(\\+?\\d[\\d\\s()-]{7,}\\d)\")\nknown_names = set(\" \".join(df[\"Name\"]).replace(\"'\", \" \").split())\n\ndef scrub(text):\n    if not isinstance(text, str):\n        return text\n    text = EMAIL.sub(\"[email]\", text)\n    text = PHONE.sub(\"[phone]\", text)\n    words = re.split(r\"(\\W+)\", text)\n    return \"\".join(\"[name]\" if w in known_names else w for w in words)\n\nout[\"Notes\"] = out[\"Notes\"].map(scrub)\nprint(out[\"Notes\"].tolist())\n",[32,1029,1030,1037,1041,1101,1140,1173,1177,1187,1202,1209,1228,1244,1269,1309,1313,1332],{"__ignoreMap":30},[35,1031,1032,1034],{"class":37,"line":38},[35,1033,228],{"class":227},[35,1035,1036],{"class":231}," re\n",[35,1038,1039],{"class":37,"line":241},[35,1040,245],{"emptyLinePlaceholder":244},[35,1042,1043,1046,1048,1051,1054,1056,1059,1062,1064,1067,1070,1072,1074,1076,1078,1080,1084,1086,1088,1090,1092,1095,1097,1099],{"class":37,"line":248},[35,1044,1045],{"class":329},"EMAIL",[35,1047,401],{"class":227},[35,1049,1050],{"class":231}," re.compile(",[35,1052,1053],{"class":227},"r",[35,1055,473],{"class":45},[35,1057,1058],{"class":329},"[\\w",[35,1060,1061],{"class":476},".+-",[35,1063,497],{"class":329},[35,1065,1066],{"class":227},"+",[35,1068,1069],{"class":45},"@",[35,1071,1058],{"class":329},[35,1073,486],{"class":476},[35,1075,497],{"class":329},[35,1077,1066],{"class":227},[35,1079,333],{"class":329},[35,1081,1083],{"class":1082},"s_b0D","\\.",[35,1085,1058],{"class":329},[35,1087,486],{"class":476},[35,1089,497],{"class":329},[35,1091,1066],{"class":227},[35,1093,1094],{"class":329},")",[35,1096,1066],{"class":227},[35,1098,473],{"class":45},[35,1100,275],{"class":231},[35,1102,1103,1106,1108,1110,1112,1114,1116,1119,1122,1125,1128,1130,1133,1136,1138],{"class":37,"line":278},[35,1104,1105],{"class":329},"PHONE",[35,1107,401],{"class":227},[35,1109,1050],{"class":231},[35,1111,1053],{"class":227},[35,1113,473],{"class":45},[35,1115,333],{"class":329},[35,1117,1118],{"class":1082},"\\+",[35,1120,1121],{"class":227},"?",[35,1123,1124],{"class":329},"\\d[\\d\\s",[35,1126,1127],{"class":476},"()-",[35,1129,497],{"class":329},[35,1131,1132],{"class":227},"{7,}",[35,1134,1135],{"class":329},"\\d)",[35,1137,473],{"class":45},[35,1139,275],{"class":231},[35,1141,1142,1145,1147,1150,1152,1155,1158,1160,1163,1166,1168,1170],{"class":37,"line":301},[35,1143,1144],{"class":231},"known_names ",[35,1146,254],{"class":227},[35,1148,1149],{"class":329}," set",[35,1151,333],{"class":231},[35,1153,1154],{"class":45},"\" \"",[35,1156,1157],{"class":231},".join(df[",[35,1159,320],{"class":45},[35,1161,1162],{"class":231},"]).replace(",[35,1164,1165],{"class":45},"\"'\"",[35,1167,263],{"class":231},[35,1169,1154],{"class":45},[35,1171,1172],{"class":231},").split())\n",[35,1174,1175],{"class":37,"line":326},[35,1176,245],{"emptyLinePlaceholder":244},[35,1178,1179,1181,1184],{"class":37,"line":422},[35,1180,425],{"class":227},[35,1182,1183],{"class":428}," scrub",[35,1185,1186],{"class":231},"(text):\n",[35,1188,1189,1191,1193,1195,1198,1200],{"class":37,"line":443},[35,1190,639],{"class":227},[35,1192,642],{"class":227},[35,1194,645],{"class":329},[35,1196,1197],{"class":231},"(text, ",[35,1199,458],{"class":329},[35,1201,440],{"class":231},[35,1203,1204,1206],{"class":37,"line":464},[35,1205,672],{"class":227},[35,1207,1208],{"class":231}," text\n",[35,1210,1211,1214,1216,1219,1222,1225],{"class":37,"line":505},[35,1212,1213],{"class":231},"    text ",[35,1215,254],{"class":227},[35,1217,1218],{"class":329}," EMAIL",[35,1220,1221],{"class":231},".sub(",[35,1223,1224],{"class":45},"\"[email]\"",[35,1226,1227],{"class":231},", text)\n",[35,1229,1230,1232,1234,1237,1239,1242],{"class":37,"line":510},[35,1231,1213],{"class":231},[35,1233,254],{"class":227},[35,1235,1236],{"class":329}," PHONE",[35,1238,1221],{"class":231},[35,1240,1241],{"class":45},"\"[phone]\"",[35,1243,1227],{"class":231},[35,1245,1246,1249,1251,1254,1256,1258,1261,1263,1265,1267],{"class":37,"line":532},[35,1247,1248],{"class":231},"    words ",[35,1250,254],{"class":227},[35,1252,1253],{"class":231}," re.split(",[35,1255,1053],{"class":227},[35,1257,473],{"class":45},[35,1259,1260],{"class":329},"(\\W",[35,1262,1066],{"class":227},[35,1264,1094],{"class":329},[35,1266,473],{"class":45},[35,1268,1227],{"class":231},[35,1270,1271,1273,1276,1279,1282,1285,1288,1291,1294,1297,1299,1302,1304,1306],{"class":37,"line":551},[35,1272,467],{"class":227},[35,1274,1275],{"class":45}," \"\"",[35,1277,1278],{"class":231},".join(",[35,1280,1281],{"class":45},"\"[name]\"",[35,1283,1284],{"class":227}," if",[35,1286,1287],{"class":231}," w ",[35,1289,1290],{"class":227},"in",[35,1292,1293],{"class":231}," known_names ",[35,1295,1296],{"class":227},"else",[35,1298,1287],{"class":231},[35,1300,1301],{"class":227},"for",[35,1303,1287],{"class":231},[35,1305,1290],{"class":227},[35,1307,1308],{"class":231}," words)\n",[35,1310,1311],{"class":37,"line":570},[35,1312,245],{"emptyLinePlaceholder":244},[35,1314,1316,1318,1321,1323,1325,1327,1329],{"class":37,"line":1315},15,[35,1317,513],{"class":231},[35,1319,1320],{"class":45},"\"Notes\"",[35,1322,519],{"class":231},[35,1324,254],{"class":227},[35,1326,524],{"class":231},[35,1328,1320],{"class":45},[35,1330,1331],{"class":231},"].map(scrub)\n",[35,1333,1335,1337,1339,1341],{"class":37,"line":1334},16,[35,1336,330],{"class":329},[35,1338,556],{"class":231},[35,1340,1320],{"class":45},[35,1342,1343],{"class":231},"].tolist())\n",[10,1345,1346,1347,1350,1351,1354,1355,1358,1359,1362],{},"The phone pattern needs at least nine characters of digits and separators, so a ticket number like ",[32,1348,1349],{},"5521"," survives while ",[32,1352,1353],{},"07700 900123"," and ",[32,1356,1357],{},"+44 161 496 0000"," do not. Name removal uses every word from the known names, which catches \"Adaeze\" and \"Ken's\" (as ",[32,1360,1361],{},"[name]'s","). It is a blunt tool — a first name that is also an ordinary word will be removed from text where it is not a name — and it cannot catch names the file does not otherwise contain. For free text that matters, the honest options are to drop the column or to have a person review what remains.",[68,1364,77,1370,77,1373,77,1376,77,1381,77,1390,77,1395,77,1399,77,1404,77,1407,77,1411,77,1413,77,1417,77,1420,77,1423,77,1425,77,1428,77,1431,77,1435,77,1437,77,1440,77,1443,77,1446,77,1450,77,1453,77,1455,77,1457,77,1460,77,1464,77,1466,77,1468,77,1470,77,1473,77,1475,77,1477,77,1479,77,1481,77,1487,77,1491,77,1495,77,1500,77,1507],{"viewBox":1365,"role":71,"ariaLabelledBy":1366,"xmlns":75,"style":1369},"47 0 666 168",[1367,1368],"an-scrub-t","an-scrub-d","width:100%;max-width:666px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[79,1371,1372],{"id":1367},"A notes cell before and after scrubbing",[83,1374,1375],{"id":1368},"On the left three notes containing a name, a phone number and an email address. On the right the same notes with the name, phone number and email replaced by placeholders, and a ticket number left alone.",[87,1377],{"x":1378,"y":90,"width":1379,"height":1380,"fill":93},"47","666","168",[87,1382],{"x":1383,"y":1384,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},"63.2","40.0","32.0","28.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[116,1391,693],{"x":1392,"y":1393,"style":1394},"79.2","59.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle",[87,1396],{"x":1397,"y":1384,"width":1398,"height":1386,"rx":90,"fill":93,"stroke":1388,"style":1389},"95.2","234.6",[116,1400,1403],{"x":1401,"y":1393,"style":1402},"212.5","font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Notes",[87,1405],{"x":1383,"y":1406,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},"68.0",[116,1408,1410],{"x":1392,"y":1409,"style":1394},"87.0","2",[87,1412],{"x":1397,"y":1406,"width":1398,"height":1386,"rx":90,"fill":93,"stroke":1388,"style":1389},[116,1414,1416],{"x":1401,"y":1409,"style":1415},"font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Called Adaeze on 07700 900123",[87,1418],{"x":1383,"y":1419,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},"96.0",[116,1421,564],{"x":1392,"y":1422,"style":1394},"115.0",[87,1424],{"x":1397,"y":1419,"width":1398,"height":1386,"rx":90,"fill":93,"stroke":1388,"style":1389},[116,1426,1427],{"x":1401,"y":1422,"style":1415},"Prefers email: c.martin@example.net",[87,1429],{"x":1383,"y":1430,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},"124.0",[116,1432,1434],{"x":1392,"y":1433,"style":1394},"143.0","4",[87,1436],{"x":1397,"y":1430,"width":1398,"height":1386,"rx":90,"fill":93,"stroke":1388,"style":1389},[116,1438,1439],{"x":1401,"y":1433,"style":1415},"Complained twice, see ticket 5521",[87,1441],{"x":1442,"y":1384,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},"448.6",[116,1444,693],{"x":1445,"y":1393,"style":1394},"464.6",[87,1447],{"x":1448,"y":1384,"width":1449,"height":1386,"rx":90,"fill":93,"stroke":1388,"style":1389},"480.6","216.2",[116,1451,1403],{"x":1452,"y":1393,"style":1402},"588.7",[87,1454],{"x":1442,"y":1406,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},[116,1456,1410],{"x":1445,"y":1409,"style":1394},[87,1458],{"x":1448,"y":1406,"width":1449,"height":1386,"rx":90,"fill":191,"stroke":192,"style":1459},"stroke-width:1.5px",[116,1461,1463],{"x":1452,"y":1409,"style":1462},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Called [name] on [phone]",[87,1465],{"x":1442,"y":1419,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},[116,1467,564],{"x":1445,"y":1422,"style":1394},[87,1469],{"x":1448,"y":1419,"width":1449,"height":1386,"rx":90,"fill":191,"stroke":192,"style":1459},[116,1471,1472],{"x":1452,"y":1422,"style":1462},"Prefers email: [email]",[87,1474],{"x":1442,"y":1430,"width":1385,"height":1386,"rx":90,"fill":1387,"stroke":1388,"style":1389},[116,1476,1434],{"x":1445,"y":1433,"style":1394},[87,1478],{"x":1448,"y":1430,"width":1449,"height":1386,"rx":90,"fill":93,"stroke":1388,"style":1389},[116,1480,1439],{"x":1452,"y":1433,"style":1415},[116,1482,1486],{"x":1483,"y":1484,"style":1485},"196.5","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[116,1488,1490],{"x":1489,"y":1484,"style":1485},"572.7","after",[37,1492],{"x1":1493,"y1":1419,"x2":1494,"y2":1419,"stroke":171,"style":103},"339.8","434.6",[1496,1497],"polygon",{"points":1498,"fill":1499},"438.6,96.0 429.6,101.0 429.6,91.0","#5b5cf0",[87,1501],{"x":1502,"y":1503,"width":1503,"height":1504,"rx":1505,"fill":93,"stroke":1388,"style":1506},"357.7","63.0","22.0","11","stroke-width:1.2px",[116,1508,1512],{"x":1509,"y":1510,"style":1511},"389.2","78.0","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","scrub()",[20,1514,1516],{"id":1515},"step-6-check-how-many-people-share-each-profile","Step 6 — Check how many people share each profile",[10,1518,1519,1520,1524],{},"Generalising helps only if each combination of remaining details is shared by several people. Count the group sizes for the quasi-identifiers — the smallest group is the file's ",[1521,1522,1523],"em",{},"k"," in k-anonymity terms:",[25,1526,1528],{"className":218,"code":1527,"language":220,"meta":30,"style":30},"quasi = [\"AgeBand\", \"District\", \"Plan\"]\ngroups = out.groupby(quasi).size().rename(\"people\").reset_index()\nprint(groups.sort_values(\"people\").to_string(index=False))\nprint(\"smallest group (k):\", groups[\"people\"].min())\n",[32,1529,1530,1552,1568,1588],{"__ignoreMap":30},[35,1531,1532,1535,1537,1540,1542,1544,1546,1548,1550],{"class":37,"line":38},[35,1533,1534],{"class":231},"quasi ",[35,1536,254],{"class":227},[35,1538,1539],{"class":231}," [",[35,1541,833],{"class":45},[35,1543,263],{"class":231},[35,1545,934],{"class":45},[35,1547,263],{"class":231},[35,1549,996],{"class":45},[35,1551,951],{"class":231},[35,1553,1554,1557,1559,1562,1565],{"class":37,"line":241},[35,1555,1556],{"class":231},"groups ",[35,1558,254],{"class":227},[35,1560,1561],{"class":231}," out.groupby(quasi).size().rename(",[35,1563,1564],{"class":45},"\"people\"",[35,1566,1567],{"class":231},").reset_index()\n",[35,1569,1570,1572,1575,1577,1580,1582,1584,1586],{"class":37,"line":248},[35,1571,330],{"class":329},[35,1573,1574],{"class":231},"(groups.sort_values(",[35,1576,1564],{"class":45},[35,1578,1579],{"class":231},").to_string(",[35,1581,1002],{"class":266},[35,1583,254],{"class":227},[35,1585,885],{"class":329},[35,1587,1009],{"class":231},[35,1589,1590,1592,1594,1597,1600,1602],{"class":37,"line":278},[35,1591,330],{"class":329},[35,1593,333],{"class":231},[35,1595,1596],{"class":45},"\"smallest group (k):\"",[35,1598,1599],{"class":231},", groups[",[35,1601,1564],{"class":45},[35,1603,1604],{"class":231},"].min())\n",[68,1606,77,1612,77,1615,77,1618,77,1622,77,1627,77,1632,77,1636,77,1640,77,1643,77,1647,77,1650,77,1654,77,1656,77,1660,77,1662,77,1666,77,1669,77,1673,77,1675,77,1677,77,1680,77,1684,77,1686,77,1688],{"viewBox":1607,"role":71,"ariaLabelledBy":1608,"xmlns":75,"style":1611},"4 0 744 246",[1609,1610],"an-k-t","an-k-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[79,1613,1614],{"id":1609},"People sharing each combination of age band, district and plan",[83,1616,1617],{"id":1610},"Horizontal bars of group sizes. Three combinations have one person each, one has two people, and 30 to 39 in M1 on the Basic plan has three.",[87,1619],{"x":1434,"y":90,"width":1620,"height":1621,"fill":93},"744","246",[87,1623],{"x":1624,"y":97,"width":1625,"height":1386,"rx":1626,"fill":1387,"stroke":1388,"style":1389},"20.0","150.5","7",[116,1628,1631],{"x":1397,"y":1629,"style":1630},"34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","30–39 · M1 · Basic",[87,1633],{"x":1634,"y":97,"width":1635,"height":1386,"rx":1626,"fill":191,"stroke":192,"style":1459},"180.5","535.4",[116,1637,564],{"x":1638,"y":1629,"style":1639},"723.9","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start",[87,1641],{"x":1624,"y":1642,"width":1625,"height":1386,"rx":1626,"fill":1387,"stroke":1388,"style":1389},"56.0",[116,1644,1646],{"x":1397,"y":1645,"style":1630},"74.5","60+ · SW1A · Pro",[87,1648],{"x":1634,"y":1642,"width":1649,"height":1386,"rx":1626,"fill":149,"stroke":150,"style":1459},"356.9",[116,1651,1410],{"x":1652,"y":1645,"style":1653},"545.4","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start",[87,1655],{"x":1624,"y":1419,"width":1625,"height":1386,"rx":1626,"fill":1387,"stroke":1388,"style":1389},[116,1657,1659],{"x":1397,"y":1658,"style":1630},"114.5","30–39 · LS6 · Pro",[87,1661],{"x":1634,"y":1419,"width":140,"height":1386,"rx":1626,"fill":101,"stroke":102,"style":1459},[116,1663,693],{"x":1664,"y":1658,"style":1665},"366.9","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start",[87,1667],{"x":1624,"y":1668,"width":1625,"height":1386,"rx":1626,"fill":1387,"stroke":1388,"style":1389},"136.0",[116,1670,1672],{"x":1397,"y":1671,"style":1630},"154.5","40–49 · SW1A · Pro",[87,1674],{"x":1634,"y":1668,"width":140,"height":1386,"rx":1626,"fill":101,"stroke":102,"style":1459},[116,1676,693],{"x":1664,"y":1671,"style":1665},[87,1678],{"x":1624,"y":1679,"width":1625,"height":1386,"rx":1626,"fill":1387,"stroke":1388,"style":1389},"176.0",[116,1681,1683],{"x":1397,"y":1682,"style":1630},"194.5","50–59 · LS6 · Team",[87,1685],{"x":1634,"y":1679,"width":140,"height":1386,"rx":1626,"fill":101,"stroke":102,"style":1459},[116,1687,693],{"x":1664,"y":1682,"style":1665},[116,1689,1692],{"x":118,"y":1690,"style":1691},"226.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","A group of one is a person who can be singled out — k = 1 for this file.",[10,1694,1695,1696,1698],{},"Three of the five combinations describe a single customer — with eight rows that is unavoidable, and it shows why small extracts are hard to anonymise. With real volumes, generalise further (wider age bands, region instead of district) or suppress rare combinations until every group has at least, say, five people. The right ",[1521,1697,1523],{}," is a policy decision; the calculation makes it measurable.",[20,1700,1702],{"id":1701},"step-7-write-a-clean-workbook-and-scan-it","Step 7 — Write a clean workbook and scan it",[10,1704,1705],{},"Write only the anonymised data to a new file — not into a copy of the original, which would carry comments, properties and hidden sheets with it — and then scan every cell of the result for patterns that should not be there:",[25,1707,1709],{"className":218,"code":1708,"language":220,"meta":30,"style":30},"from openpyxl import load_workbook\n\nwith pd.ExcelWriter(\"customers_shared.xlsx\", engine=\"openpyxl\") as xw:\n    out.to_excel(xw, sheet_name=\"Customers\", index=False)\n    tickets.to_excel(xw, sheet_name=\"Tickets\", index=False)\n\nhits = []\nwb = load_workbook(\"customers_shared.xlsx\")\nfor ws in wb.worksheets:\n    for row in ws.iter_rows():\n        for c in row:\n            if isinstance(c.value, str) and (EMAIL.search(c.value) or PHONE.search(c.value)):\n                hits.append((ws.title, c.coordinate, c.value))\n            if c.comment:\n                hits.append((ws.title, c.coordinate, \"comment\"))\nprint(\"creator:\", wb.properties.creator, \"| findings:\", hits)\n",[32,1710,1711,1724,1728,1756,1777,1798,1802,1812,1826,1838,1851,1864,1896,1901,1908,1918],{"__ignoreMap":30},[35,1712,1713,1716,1719,1721],{"class":37,"line":38},[35,1714,1715],{"class":227},"from",[35,1717,1718],{"class":231}," openpyxl ",[35,1720,228],{"class":227},[35,1722,1723],{"class":231}," load_workbook\n",[35,1725,1726],{"class":37,"line":241},[35,1727,245],{"emptyLinePlaceholder":244},[35,1729,1730,1733,1736,1739,1741,1744,1746,1749,1751,1753],{"class":37,"line":248},[35,1731,1732],{"class":227},"with",[35,1734,1735],{"class":231}," pd.ExcelWriter(",[35,1737,1738],{"class":45},"\"customers_shared.xlsx\"",[35,1740,263],{"class":231},[35,1742,1743],{"class":266},"engine",[35,1745,254],{"class":227},[35,1747,1748],{"class":45},"\"openpyxl\"",[35,1750,653],{"class":231},[35,1752,235],{"class":227},[35,1754,1755],{"class":231}," xw:\n",[35,1757,1758,1761,1763,1765,1767,1769,1771,1773,1775],{"class":37,"line":278},[35,1759,1760],{"class":231},"    out.to_excel(xw, ",[35,1762,267],{"class":266},[35,1764,254],{"class":227},[35,1766,272],{"class":45},[35,1768,263],{"class":231},[35,1770,1002],{"class":266},[35,1772,254],{"class":227},[35,1774,885],{"class":329},[35,1776,275],{"class":231},[35,1778,1779,1782,1784,1786,1788,1790,1792,1794,1796],{"class":37,"line":301},[35,1780,1781],{"class":231},"    tickets.to_excel(xw, ",[35,1783,267],{"class":266},[35,1785,254],{"class":227},[35,1787,296],{"class":45},[35,1789,263],{"class":231},[35,1791,1002],{"class":266},[35,1793,254],{"class":227},[35,1795,885],{"class":329},[35,1797,275],{"class":231},[35,1799,1800],{"class":37,"line":326},[35,1801,245],{"emptyLinePlaceholder":244},[35,1803,1804,1807,1809],{"class":37,"line":422},[35,1805,1806],{"class":231},"hits ",[35,1808,254],{"class":227},[35,1810,1811],{"class":231}," []\n",[35,1813,1814,1817,1819,1822,1824],{"class":37,"line":443},[35,1815,1816],{"class":231},"wb ",[35,1818,254],{"class":227},[35,1820,1821],{"class":231}," load_workbook(",[35,1823,1738],{"class":45},[35,1825,275],{"class":231},[35,1827,1828,1830,1833,1835],{"class":37,"line":464},[35,1829,1301],{"class":227},[35,1831,1832],{"class":231}," ws ",[35,1834,1290],{"class":227},[35,1836,1837],{"class":231}," wb.worksheets:\n",[35,1839,1840,1843,1846,1848],{"class":37,"line":505},[35,1841,1842],{"class":227},"    for",[35,1844,1845],{"class":231}," row ",[35,1847,1290],{"class":227},[35,1849,1850],{"class":231}," ws.iter_rows():\n",[35,1852,1853,1856,1859,1861],{"class":37,"line":510},[35,1854,1855],{"class":227},"        for",[35,1857,1858],{"class":231}," c ",[35,1860,1290],{"class":227},[35,1862,1863],{"class":231}," row:\n",[35,1865,1866,1869,1871,1874,1876,1878,1881,1884,1886,1889,1891,1893],{"class":37,"line":532},[35,1867,1868],{"class":227},"            if",[35,1870,645],{"class":329},[35,1872,1873],{"class":231},"(c.value, ",[35,1875,458],{"class":329},[35,1877,653],{"class":231},[35,1879,1880],{"class":227},"and",[35,1882,1883],{"class":231}," (",[35,1885,1045],{"class":329},[35,1887,1888],{"class":231},".search(c.value) ",[35,1890,656],{"class":227},[35,1892,1236],{"class":329},[35,1894,1895],{"class":231},".search(c.value)):\n",[35,1897,1898],{"class":37,"line":551},[35,1899,1900],{"class":231},"                hits.append((ws.title, c.coordinate, c.value))\n",[35,1902,1903,1905],{"class":37,"line":570},[35,1904,1868],{"class":227},[35,1906,1907],{"class":231}," c.comment:\n",[35,1909,1910,1913,1916],{"class":37,"line":1315},[35,1911,1912],{"class":231},"                hits.append((ws.title, c.coordinate, ",[35,1914,1915],{"class":45},"\"comment\"",[35,1917,1009],{"class":231},[35,1919,1920,1922,1924,1927,1930,1933],{"class":37,"line":1334},[35,1921,330],{"class":329},[35,1923,333],{"class":231},[35,1925,1926],{"class":45},"\"creator:\"",[35,1928,1929],{"class":231},", wb.properties.creator, ",[35,1931,1932],{"class":45},"\"| findings:\"",[35,1934,1935],{"class":231},", hits)\n",[10,1937,1938,1939,1942,1943,1946,1947,1951],{},"The scan flags one value, on both sheets: the pseudonym ",[32,1940,1941],{},"P-7582122945"," happens to be all digits, and the phone pattern matches it. That is a false positive, and a useful reminder that a pattern scan produces a list for a person to review, not a verdict. The masked emails do not match, because the asterisks break the address pattern. Everything else is clean: the comment about the VIP customer did not travel, and the author property says ",[32,1944,1945],{},"openpyxl"," rather than the original author, because the file was written fresh instead of copied. ",[14,1948,1950],{"href":1949},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fremove-hidden-data-before-sharing-an-excel-file\u002F","Remove Hidden Data Before Sharing an Excel File"," lists the other places a workbook keeps information.",[20,1953,1955],{"id":1954},"common-pitfalls","Common pitfalls",[1957,1958,1959,1971,1977,1983,1989],"ul",{},[1960,1961,1962,1966,1967,1970],"li",{},[1963,1964,1965],"strong",{},"Unkeyed hashes."," ",[32,1968,1969],{},"sha256(\"C-10041\")"," can be reversed by hashing every possible ID. Use HMAC with a secret key.",[1960,1972,1973,1976],{},[1963,1974,1975],{},"Forgetting the other sheets."," An ID pseudonymised on one sheet and left raw on another undoes the work — and breaks the join.",[1960,1978,1979,1982],{},[1963,1980,1981],{},"Copying the original file."," Comments, hidden sheets, defined names and document properties come along. Write fresh.",[1960,1984,1985,1988],{},[1963,1986,1987],{},"Trusting regexes on free text."," They catch formats, not meaning. Drop or review free text that matters.",[1960,1990,1991,1994],{},[1963,1992,1993],{},"Small groups."," A rare combination of age band, district and job title identifies a person. Measure group sizes.",[20,1996,1998],{"id":1997},"conclusion","Conclusion",[10,2000,2001],{},"Drop every column the recipient does not need, replace linking IDs with HMAC pseudonyms from a secret key, mask values whose shape matters, generalise quasi-identifiers into bands and districts, and scrub or drop free text. Then measure: count the smallest group of people sharing the remaining details, write the result to a fresh workbook with no comments or properties, and scan every cell for patterns that should not survive.",[20,2003,2005],{"id":2004},"frequently-asked-questions","Frequently asked questions",[10,2007,2008,2011],{},[1963,2009,2010],{},"How do I anonymise an Excel file with Python?","\nRead it with pandas, drop direct identifiers such as names, replace IDs with keyed pseudonyms, generalise dates of birth and postcodes, scrub free-text columns, and write the result to a new workbook. Finish by scanning the output for emails, phone numbers and comments.",[10,2013,2014,2017],{},[1963,2015,2016],{},"What is the difference between pseudonymisation and anonymisation?","\nPseudonymised data replaces identifiers with codes that the key holder can map back to people, so it is still personal data. Anonymised data cannot be linked back to a person by anyone with reasonable effort.",[10,2019,2020,2023],{},[1963,2021,2022],{},"Why not just hash the customer ID?","\nCustomer IDs follow a predictable format, so an attacker can hash every possible ID and look up the matches. A keyed hash such as HMAC-SHA256 with a secret key prevents that as long as the key stays secret.",[10,2025,2026,2029],{},[1963,2027,2028],{},"How do I keep pseudonyms consistent across monthly files?","\nUse the same secret key every time. HMAC gives the same output for the same input and key, so a customer gets the same pseudonym in every extract.",[20,2031,2033],{"id":2032},"related","Related",[1957,2035,2036,2042,2049,2056],{},[1960,2037,2038,2039,2041],{},"Up one level: ",[14,2040,17],{"href":16}," — checks that data is fit to use and to share.",[1960,2043,2044,2048],{},[14,2045,2047],{"href":2046},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data\u002F","Validate Email and Phone Formats in Excel Data"," — the patterns behind the scrubbing step.",[1960,2050,2051,2055],{},[14,2052,2054],{"href":2053},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fprofile-an-excel-dataset-with-pandas\u002F","Profile an Excel Dataset with pandas"," — understanding columns before deciding what to keep.",[1960,2057,2058,2062],{},[14,2059,2061],{"href":2060},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python\u002F","Password Protect an Excel File with Python"," — protecting the files you cannot anonymise.",[2064,2065,2066],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}",{"title":30,"searchDepth":241,"depth":241,"links":2068},[2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080],{"id":22,"depth":241,"text":23},{"id":211,"depth":241,"text":212},{"id":349,"depth":241,"text":350},{"id":615,"depth":241,"text":616},{"id":772,"depth":241,"text":773},{"id":1020,"depth":241,"text":1021},{"id":1515,"depth":241,"text":1516},{"id":1701,"depth":241,"text":1702},{"id":1954,"depth":241,"text":1955},{"id":1997,"depth":241,"text":1998},{"id":2004,"depth":241,"text":2005},{"id":2032,"depth":241,"text":2033},"2026-09-29","Prepare a workbook of customer or employee records for sharing: drop direct identifiers, replace IDs with keyed pseudonyms that stay consistent across files, generalise ages and postcodes, scrub emails and phone numbers out of free text, check how many people share each combination of details, and scan the output for anything left behind.","md",[2085,2087,2089,2091],{"q":2010,"a":2086},"Read it with pandas, drop direct identifiers such as names, replace IDs with keyed pseudonyms, generalise dates of birth and postcodes, scrub free-text columns, and write the result to a new workbook. Finish by scanning the output for emails, phone numbers and comments.",{"q":2016,"a":2088},"Pseudonymised data replaces identifiers with codes that the key holder can map back to people, so it is still personal data. Anonymised data cannot be linked back to a person by anyone with reasonable effort.",{"q":2022,"a":2090},"Customer IDs follow a predictable format, so an attacker can hash every possible ID and look up the matches. A keyed hash such as HMAC-SHA256 with a secret key prevents that as long as the key stays secret.",{"q":2028,"a":2092},"Use the same secret key every time. HMAC gives the same output for the same input and key, so a customer gets the same pseudonym in every extract.","anonymise personal data in an excel file with python",null,{"breadcrumb":2096},[2097,2100,2103],{"name":2098,"item":2099},"Home","\u002F",{"name":2101,"item":2102},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python",{"title":2106,"description":2107},"Anonymise or Pseudonymise Excel Data with Python and pandas","Anonymise personal data in Excel with pandas: HMAC pseudonyms, masking, age bands and postcode districts, regex scrubbing of notes, a k-anonymity check on quasi-identifiers, a residual PII scan, and removing comments and hidden sheets.","anonymise-personal-data-in-an-excel-file-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python\u002Findex",[2111,2112],"data-validation","protection-and-sharing","how-to","VQ2s-L3HG08MZSHgNGvJuI1hyADE58crCJXA3SnIL1M",[2116,2120],{"title":2117,"path":2118,"stem":2119,"children":-1},"Add Dropdown Data Validation to Excel with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl\u002Findex",{"title":2121,"path":2122,"stem":2123,"children":-1},"Build a Data Quality Scorecard for an Excel File","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fbuild-a-data-quality-scorecard-for-an-excel-file","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fbuild-a-data-quality-scorecard-for-an-excel-file\u002Findex",[2125,2127],{"path":2126,"title":2101},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2128,"title":17},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python",[],[2131,2134,2137,2140,2143,2146],{"path":2132,"title":2133,"kind":2113},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas","Detect Outliers in Excel Data with pandas",{"path":2135,"title":2136,"kind":2113},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl","Highlight Cells Containing Specific Text with openpyxl",{"path":2138,"title":2139,"kind":2113},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffind-near-duplicate-names-within-one-excel-column","Find Near-Duplicate Names Within One Excel Column",{"path":2141,"title":2142,"kind":2113},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook","Read Dropdown Lists and Validation Rules from an Existing Workbook",{"path":2144,"title":2145,"kind":2113},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffind-and-report-missing-values-in-an-excel-file","Find and Report Missing Values in an Excel File",{"path":2147,"title":2148,"kind":2113},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Fflag-rows-with-too-many-missing-values","Flag Rows with Too Many Missing Values",1790972098438]