[{"data":1,"prerenderedAt":2530},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy":2496,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy":2505,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy":2510,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy":2511},{"id":4,"title":5,"body":6,"dateModified":2463,"datePublished":2463,"description":2464,"extension":2465,"faq":2466,"keyword":2475,"kind":2476,"meta":2477,"navigation":98,"path":2486,"seo":2487,"slug":2490,"stem":2491,"topics":2492,"type":2494,"__hash__":2495},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy\u002Findex.md","Validate Parent-Child Codes in a Hierarchy",{"type":7,"value":8,"toc":2451},"minimark",[9,19,24,53,63,67,70,304,323,327,330,704,707,858,862,865,1293,1305,1309,1312,1819,1822,1905,2028,2032,2035,2322,2334,2338,2379,2383,2386,2390,2396,2402,2408,2414,2418,2447],[10,11,12,13,18],"p",{},"Chart-of-accounts trees, cost-centre structures and product category lists are usually maintained in Excel as two columns: a code and its parent's code. People add, rename and move rows by hand, and the table slowly accumulates errors that break everything built on it: a code that appears twice, a parent that was renamed so its children point at nothing, two rows that point at each other, a second \"top\" node created by accident. A roll-up through a broken hierarchy produces totals that look plausible and are wrong — postings under an orphaned branch simply vanish from the top. This page checks a hierarchy for each of these problems with pandas, adds the business rules a real structure usually has — a maximum depth, child codes that start with the parent's code, postings only to bottom-level nodes — and writes a report that tells the maintainer exactly which Excel row to fix. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",".",[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],{},[32,56,57],{},"accounts.xlsx"," holds a small chart of accounts with nineteen rows: code, name and parent code, read as text so leading zeros would survive. It contains the usual accumulated problems — deliberately all at once. ",[32,60,61],{},"postings.xlsx"," holds a few balances by account, including one posted to an intermediate account and one to a code that does not exist.",[20,64,66],{"id":65},"step-1-load-the-table-and-keep-the-excel-row-numbers","Step 1 — Load the table and keep the Excel row numbers",[10,68,69],{},"Every finding should point at a row the maintainer can find, so keep the Excel row number with each record:",[25,71,75],{"className":72,"code":73,"language":74,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ntree = pd.read_excel(\"accounts.xlsx\", sheet_name=\"Tree\", dtype=str)\ntree[\"Code\"] = tree[\"Code\"].str.strip()\ntree[\"Parent\"] = tree[\"Parent\"].str.strip()\ntree[\"Excel row\"] = tree.index + 2                     # header is row 1\nfindings = []\n\ndef flag(rows, check, detail):\n    for r in rows:\n        findings.append({\"Excel row\": int(r), \"Check\": check, \"Detail\": detail})\n\nprint(len(tree), \"rows read\")\n","python",[32,76,77,93,100,141,163,181,206,217,222,235,250,279,284],{"__ignoreMap":30},[35,78,79,83,87,90],{"class":37,"line":38},[35,80,82],{"class":81},"s-kum","import",[35,84,86],{"class":85},"skGVy"," pandas ",[35,88,89],{"class":81},"as",[35,91,92],{"class":85}," pd\n",[35,94,96],{"class":37,"line":95},2,[35,97,99],{"emptyLinePlaceholder":98},true,"\n",[35,101,103,106,109,112,115,118,122,124,127,129,132,134,138],{"class":37,"line":102},3,[35,104,105],{"class":85},"tree ",[35,107,108],{"class":81},"=",[35,110,111],{"class":85}," pd.read_excel(",[35,113,114],{"class":45},"\"accounts.xlsx\"",[35,116,117],{"class":85},", ",[35,119,121],{"class":120},"sa561","sheet_name",[35,123,108],{"class":81},[35,125,126],{"class":45},"\"Tree\"",[35,128,117],{"class":85},[35,130,131],{"class":120},"dtype",[35,133,108],{"class":81},[35,135,137],{"class":136},"sP0c6","str",[35,139,140],{"class":85},")\n",[35,142,144,147,150,153,155,158,160],{"class":37,"line":143},4,[35,145,146],{"class":85},"tree[",[35,148,149],{"class":45},"\"Code\"",[35,151,152],{"class":85},"] ",[35,154,108],{"class":81},[35,156,157],{"class":85}," tree[",[35,159,149],{"class":45},[35,161,162],{"class":85},"].str.strip()\n",[35,164,166,168,171,173,175,177,179],{"class":37,"line":165},5,[35,167,146],{"class":85},[35,169,170],{"class":45},"\"Parent\"",[35,172,152],{"class":85},[35,174,108],{"class":81},[35,176,157],{"class":85},[35,178,170],{"class":45},[35,180,162],{"class":85},[35,182,184,186,189,191,193,196,199,202],{"class":37,"line":183},6,[35,185,146],{"class":85},[35,187,188],{"class":45},"\"Excel row\"",[35,190,152],{"class":85},[35,192,108],{"class":81},[35,194,195],{"class":85}," tree.index ",[35,197,198],{"class":81},"+",[35,200,201],{"class":136}," 2",[35,203,205],{"class":204},"s-wDw","                     # header is row 1\n",[35,207,209,212,214],{"class":37,"line":208},7,[35,210,211],{"class":85},"findings ",[35,213,108],{"class":81},[35,215,216],{"class":85}," []\n",[35,218,220],{"class":37,"line":219},8,[35,221,99],{"emptyLinePlaceholder":98},[35,223,225,228,232],{"class":37,"line":224},9,[35,226,227],{"class":81},"def",[35,229,231],{"class":230},"s_Opv"," flag",[35,233,234],{"class":85},"(rows, check, detail):\n",[35,236,238,241,244,247],{"class":37,"line":237},10,[35,239,240],{"class":81},"    for",[35,242,243],{"class":85}," r ",[35,245,246],{"class":81},"in",[35,248,249],{"class":85}," rows:\n",[35,251,253,256,258,261,264,267,270,273,276],{"class":37,"line":252},11,[35,254,255],{"class":85},"        findings.append({",[35,257,188],{"class":45},[35,259,260],{"class":85},": ",[35,262,263],{"class":136},"int",[35,265,266],{"class":85},"(r), ",[35,268,269],{"class":45},"\"Check\"",[35,271,272],{"class":85},": check, ",[35,274,275],{"class":45},"\"Detail\"",[35,277,278],{"class":85},": detail})\n",[35,280,282],{"class":37,"line":281},12,[35,283,99],{"emptyLinePlaceholder":98},[35,285,287,290,293,296,299,302],{"class":37,"line":286},13,[35,288,289],{"class":136},"print",[35,291,292],{"class":85},"(",[35,294,295],{"class":136},"len",[35,297,298],{"class":85},"(tree), ",[35,300,301],{"class":45},"\"rows read\"",[35,303,140],{"class":85},[10,305,306,307,310,311,314,315,318,319,322],{},"Reading codes as text and stripping spaces avoids two classic false alarms: ",[32,308,309],{},"1000"," and ",[32,312,313],{},"\"1000\""," not matching, and ",[32,316,317],{},"\"1100 \""," with a trailing space looking like a different code. The ",[32,320,321],{},"flag"," helper collects every problem as a row number, a check name and a readable detail, so all checks feed one report.",[20,324,326],{"id":325},"step-2-duplicates-orphans-and-roots","Step 2 — Duplicates, orphans and roots",[10,328,329],{},"The structural basics come first: each code once, every parent present, exactly one top node — or the expected number, for a chart of accounts with separate trees for assets, liabilities and so on:",[25,331,333],{"className":72,"code":332,"language":74,"meta":30,"style":30},"dup = tree[tree[\"Code\"].duplicated(keep=False)]\nfor code, grp in dup.groupby(\"Code\"):\n    flag(grp[\"Excel row\"], \"duplicate code\", f\"{code} appears on rows {', '.join(map(str, grp['Excel row']))}\")\n\ncodes = set(tree[\"Code\"])\norphans = tree[tree[\"Parent\"].notna() & ~tree[\"Parent\"].isin(codes)]\nfor _, r in orphans.iterrows():\n    flag([r[\"Excel row\"]], \"missing parent\", f\"{r['Code']} points to {r['Parent']}, which does not exist\")\n\nself_ref = tree[tree[\"Code\"] == tree[\"Parent\"]]\nflag(self_ref[\"Excel row\"], \"self-reference\", \"a code cannot be its own parent\")\n\nroots = tree[tree[\"Parent\"].isna()]\nEXPECTED_ROOTS = {\"1000\", \"2000\"}\nunexpected = roots[~roots[\"Code\"].isin(EXPECTED_ROOTS)]\nflag(unexpected[\"Excel row\"], \"unexpected top node\", \"has no parent but is not an expected root\")\nprint(pd.DataFrame(findings).to_string(index=False))\n",[32,334,335,361,379,442,446,464,491,503,554,558,581,600,604,618,640,666,686],{"__ignoreMap":30},[35,336,337,340,342,345,347,350,353,355,358],{"class":37,"line":38},[35,338,339],{"class":85},"dup ",[35,341,108],{"class":81},[35,343,344],{"class":85}," tree[tree[",[35,346,149],{"class":45},[35,348,349],{"class":85},"].duplicated(",[35,351,352],{"class":120},"keep",[35,354,108],{"class":81},[35,356,357],{"class":136},"False",[35,359,360],{"class":85},")]\n",[35,362,363,366,369,371,374,376],{"class":37,"line":95},[35,364,365],{"class":81},"for",[35,367,368],{"class":85}," code, grp ",[35,370,246],{"class":81},[35,372,373],{"class":85}," dup.groupby(",[35,375,149],{"class":45},[35,377,378],{"class":85},"):\n",[35,380,381,384,386,389,392,394,397,400,404,406,409,412,414,417,420,423,425,427,430,433,436,438,440],{"class":37,"line":102},[35,382,383],{"class":85},"    flag(grp[",[35,385,188],{"class":45},[35,387,388],{"class":85},"], ",[35,390,391],{"class":45},"\"duplicate code\"",[35,393,117],{"class":85},[35,395,396],{"class":81},"f",[35,398,399],{"class":45},"\"",[35,401,403],{"class":402},"sSjpA","{",[35,405,32],{"class":85},[35,407,408],{"class":402},"}",[35,410,411],{"class":45}," appears on rows ",[35,413,403],{"class":402},[35,415,416],{"class":45},"', '",[35,418,419],{"class":85},".join(",[35,421,422],{"class":136},"map",[35,424,292],{"class":85},[35,426,137],{"class":136},[35,428,429],{"class":85},", grp[",[35,431,432],{"class":45},"'Excel row'",[35,434,435],{"class":85},"]))",[35,437,408],{"class":402},[35,439,399],{"class":45},[35,441,140],{"class":85},[35,443,444],{"class":37,"line":143},[35,445,99],{"emptyLinePlaceholder":98},[35,447,448,451,453,456,459,461],{"class":37,"line":165},[35,449,450],{"class":85},"codes ",[35,452,108],{"class":81},[35,454,455],{"class":136}," set",[35,457,458],{"class":85},"(tree[",[35,460,149],{"class":45},[35,462,463],{"class":85},"])\n",[35,465,466,469,471,473,475,478,481,484,486,488],{"class":37,"line":183},[35,467,468],{"class":85},"orphans ",[35,470,108],{"class":81},[35,472,344],{"class":85},[35,474,170],{"class":45},[35,476,477],{"class":85},"].notna() ",[35,479,480],{"class":81},"&",[35,482,483],{"class":81}," ~",[35,485,146],{"class":85},[35,487,170],{"class":45},[35,489,490],{"class":85},"].isin(codes)]\n",[35,492,493,495,498,500],{"class":37,"line":208},[35,494,365],{"class":81},[35,496,497],{"class":85}," _, r ",[35,499,246],{"class":81},[35,501,502],{"class":85}," orphans.iterrows():\n",[35,504,505,508,510,513,516,518,520,522,524,527,530,533,535,538,540,542,545,547,549,552],{"class":37,"line":219},[35,506,507],{"class":85},"    flag([r[",[35,509,188],{"class":45},[35,511,512],{"class":85},"]], ",[35,514,515],{"class":45},"\"missing parent\"",[35,517,117],{"class":85},[35,519,396],{"class":81},[35,521,399],{"class":45},[35,523,403],{"class":402},[35,525,526],{"class":85},"r[",[35,528,529],{"class":45},"'Code'",[35,531,532],{"class":85},"]",[35,534,408],{"class":402},[35,536,537],{"class":45}," points to ",[35,539,403],{"class":402},[35,541,526],{"class":85},[35,543,544],{"class":45},"'Parent'",[35,546,532],{"class":85},[35,548,408],{"class":402},[35,550,551],{"class":45},", which does not exist\"",[35,553,140],{"class":85},[35,555,556],{"class":37,"line":224},[35,557,99],{"emptyLinePlaceholder":98},[35,559,560,563,565,567,569,571,574,576,578],{"class":37,"line":237},[35,561,562],{"class":85},"self_ref ",[35,564,108],{"class":81},[35,566,344],{"class":85},[35,568,149],{"class":45},[35,570,152],{"class":85},[35,572,573],{"class":81},"==",[35,575,157],{"class":85},[35,577,170],{"class":45},[35,579,580],{"class":85},"]]\n",[35,582,583,586,588,590,593,595,598],{"class":37,"line":252},[35,584,585],{"class":85},"flag(self_ref[",[35,587,188],{"class":45},[35,589,388],{"class":85},[35,591,592],{"class":45},"\"self-reference\"",[35,594,117],{"class":85},[35,596,597],{"class":45},"\"a code cannot be its own parent\"",[35,599,140],{"class":85},[35,601,602],{"class":37,"line":281},[35,603,99],{"emptyLinePlaceholder":98},[35,605,606,609,611,613,615],{"class":37,"line":286},[35,607,608],{"class":85},"roots ",[35,610,108],{"class":81},[35,612,344],{"class":85},[35,614,170],{"class":45},[35,616,617],{"class":85},"].isna()]\n",[35,619,621,624,627,630,632,634,637],{"class":37,"line":620},14,[35,622,623],{"class":136},"EXPECTED_ROOTS",[35,625,626],{"class":81}," =",[35,628,629],{"class":85}," {",[35,631,313],{"class":45},[35,633,117],{"class":85},[35,635,636],{"class":45},"\"2000\"",[35,638,639],{"class":85},"}\n",[35,641,643,646,648,651,654,657,659,662,664],{"class":37,"line":642},15,[35,644,645],{"class":85},"unexpected ",[35,647,108],{"class":81},[35,649,650],{"class":85}," roots[",[35,652,653],{"class":81},"~",[35,655,656],{"class":85},"roots[",[35,658,149],{"class":45},[35,660,661],{"class":85},"].isin(",[35,663,623],{"class":136},[35,665,360],{"class":85},[35,667,669,672,674,676,679,681,684],{"class":37,"line":668},16,[35,670,671],{"class":85},"flag(unexpected[",[35,673,188],{"class":45},[35,675,388],{"class":85},[35,677,678],{"class":45},"\"unexpected top node\"",[35,680,117],{"class":85},[35,682,683],{"class":45},"\"has no parent but is not an expected root\"",[35,685,140],{"class":85},[35,687,689,691,694,697,699,701],{"class":37,"line":688},17,[35,690,289],{"class":136},[35,692,693],{"class":85},"(pd.DataFrame(findings).to_string(",[35,695,696],{"class":120},"index",[35,698,108],{"class":81},[35,700,357],{"class":136},[35,702,703],{"class":85},"))\n",[10,705,706],{},"Account 1120 appears twice, on rows 5 and 13. Retained earnings (3100) points to a parent 3000 that does not exist — probably a top node someone forgot to add — and 5100 is its own parent. The list of expected roots is part of the hierarchy's definition: an account tree may legitimately have several top nodes, but they should be the ones you intend. Missing-parent errors are the most damaging: every posting under that branch would drop out of the rolled-up totals.",[708,709,717,718,717,722,717,726,717,734,717,742,717,749,717,756,717,759,717,763,717,768,717,774,717,776,717,781,717,783,717,786,717,789,717,793,717,795,717,799,717,801,717,804,717,807,717,811,717,813,717,817,717,819,717,822,717,825,717,829,717,831,717,835,717,837,717,840,717,843,717,847,717,849,717,853,717,855],"svg",{"viewBox":710,"role":711,"ariaLabelledBy":712,"xmlns":715,"style":716},"4 0 752 242","img",[713,714],"hv-checks-t","hv-checks-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[719,720,721],"title",{"id":713},"The structural checks and what they catch",[723,724,725],"desc",{"id":714},"Duplicate codes catch a code entered twice. Missing parents catch children pointing at a code that does not exist. Self-references catch a code that is its own parent. Cycles catch two or more codes pointing at each other. Unexpected roots catch an accidental extra top node.",[727,728],"rect",{"x":729,"y":730,"width":731,"height":732,"fill":733},"4","0","752","242","#ffffff",[727,735],{"x":736,"y":737,"width":738,"height":739,"rx":730,"fill":733,"stroke":740,"style":741},"20.0","16.0","133.8","40.0","var(--line,#cdd5e6)","stroke-width:1px",[727,743],{"x":744,"y":737,"width":745,"height":739,"rx":730,"fill":746,"stroke":747,"style":748},"153.8","293.1","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[750,751,755],"text",{"x":752,"y":753,"style":754},"300.4","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","rule",[727,757],{"x":758,"y":737,"width":745,"height":739,"rx":730,"fill":746,"stroke":747,"style":748},"446.9",[750,760,762],{"x":761,"y":753,"style":754},"593.5","example in this tree",[727,764],{"x":736,"y":765,"width":738,"height":766,"rx":730,"fill":767,"stroke":740,"style":741},"56.0","34.0","#e7ebef",[750,769,773],{"x":770,"y":771,"style":772},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","duplicate code",[727,775],{"x":744,"y":765,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,777,780],{"x":752,"y":778,"style":779},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","each code once",[727,782],{"x":758,"y":765,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,784,785],{"x":761,"y":778,"style":779},"1120 on rows 5 and 13",[727,787],{"x":736,"y":788,"width":738,"height":766,"rx":730,"fill":767,"stroke":740,"style":741},"90.0",[750,790,792],{"x":770,"y":791,"style":772},"112.0","missing parent",[727,794],{"x":744,"y":788,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,796,798],{"x":752,"y":797,"style":779},"111.0","parent must exist",[727,800],{"x":758,"y":788,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,802,803],{"x":761,"y":797,"style":779},"3100 → 3000 (absent)",[727,805],{"x":736,"y":806,"width":738,"height":766,"rx":730,"fill":767,"stroke":740,"style":741},"124.0",[750,808,810],{"x":770,"y":809,"style":772},"146.0","self-reference",[727,812],{"x":744,"y":806,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,814,816],{"x":752,"y":815,"style":779},"145.0","code ≠ parent",[727,818],{"x":758,"y":806,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,820,821],{"x":761,"y":815,"style":779},"5100 → 5100",[727,823],{"x":736,"y":824,"width":738,"height":766,"rx":730,"fill":767,"stroke":740,"style":741},"158.0",[750,826,828],{"x":770,"y":827,"style":772},"180.0","cycle",[727,830],{"x":744,"y":824,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,832,834],{"x":752,"y":833,"style":779},"179.0","no loops",[727,836],{"x":758,"y":824,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,838,839],{"x":761,"y":833,"style":779},"4100 → 4200 → 4100",[727,841],{"x":736,"y":842,"width":738,"height":766,"rx":730,"fill":767,"stroke":740,"style":741},"192.0",[750,844,846],{"x":770,"y":845,"style":772},"214.0","unexpected root",[727,848],{"x":744,"y":842,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,850,852],{"x":752,"y":851,"style":779},"213.0","only listed tops",[727,854],{"x":758,"y":842,"width":745,"height":766,"rx":730,"fill":733,"stroke":740,"style":741},[750,856,857],{"x":761,"y":851,"style":779},"none here",[20,859,861],{"id":860},"step-3-cycles-and-unreachable-branches","Step 3 — Cycles and unreachable branches",[10,863,864],{},"A cycle — 4100's parent is 4200 and 4200's parent is 4100 — never reaches a top node, so a roll-up would loop forever or skip the branch. Walk each code's chain of parents and record where it ends:",[25,866,868],{"className":72,"code":867,"language":74,"meta":30,"style":30},"parent = {r.Code: r.Parent for r in tree.drop_duplicates(\"Code\").itertuples()}\nrow_of = dict(zip(tree.drop_duplicates(\"Code\")[\"Code\"], tree.drop_duplicates(\"Code\")[\"Excel row\"]))\n\ndef chain(code, limit=50):\n    seen = [code]\n    while isinstance(parent.get(seen[-1]), str):\n        nxt = parent[seen[-1]]\n        if nxt in seen:\n            return seen + [nxt], \"cycle\"\n        if nxt not in parent:\n            return seen + [nxt], \"broken\"\n        seen.append(nxt)\n        if len(seen) > limit:\n            return seen, \"too deep\"\n    return seen, \"ok\"\n\nreported = set()\nfor code in parent:\n    path, status = chain(code)\n    if status == \"cycle\" and len(set(path)) > 1:          # a code pointing at itself is reported above\n        loop = frozenset(path[path.index(path[-1]):-1])\n        if loop not in reported:\n            reported.add(loop)\n            flag([row_of[c] for c in loop], \"cycle\", \" → \".join(path[path.index(path[-1]):]))\nprint([f for f in findings if f[\"Check\"] == \"cycle\"])\n",[32,869,870,894,931,935,952,962,986,1002,1015,1031,1046,1059,1064,1080,1090,1100,1104,1116,1128,1139,1177,1204,1219,1225,1259],{"__ignoreMap":30},[35,871,872,875,877,880,882,884,886,889,891],{"class":37,"line":38},[35,873,874],{"class":85},"parent ",[35,876,108],{"class":81},[35,878,879],{"class":85}," {r.Code: r.Parent ",[35,881,365],{"class":81},[35,883,243],{"class":85},[35,885,246],{"class":81},[35,887,888],{"class":85}," tree.drop_duplicates(",[35,890,149],{"class":45},[35,892,893],{"class":85},").itertuples()}\n",[35,895,896,899,901,904,906,909,912,914,917,919,922,924,926,928],{"class":37,"line":95},[35,897,898],{"class":85},"row_of ",[35,900,108],{"class":81},[35,902,903],{"class":136}," dict",[35,905,292],{"class":85},[35,907,908],{"class":136},"zip",[35,910,911],{"class":85},"(tree.drop_duplicates(",[35,913,149],{"class":45},[35,915,916],{"class":85},")[",[35,918,149],{"class":45},[35,920,921],{"class":85},"], tree.drop_duplicates(",[35,923,149],{"class":45},[35,925,916],{"class":85},[35,927,188],{"class":45},[35,929,930],{"class":85},"]))\n",[35,932,933],{"class":37,"line":102},[35,934,99],{"emptyLinePlaceholder":98},[35,936,937,939,942,945,947,950],{"class":37,"line":143},[35,938,227],{"class":81},[35,940,941],{"class":230}," chain",[35,943,944],{"class":85},"(code, limit",[35,946,108],{"class":81},[35,948,949],{"class":136},"50",[35,951,378],{"class":85},[35,953,954,957,959],{"class":37,"line":165},[35,955,956],{"class":85},"    seen ",[35,958,108],{"class":81},[35,960,961],{"class":85}," [code]\n",[35,963,964,967,970,973,976,979,982,984],{"class":37,"line":183},[35,965,966],{"class":81},"    while",[35,968,969],{"class":136}," isinstance",[35,971,972],{"class":85},"(parent.get(seen[",[35,974,975],{"class":81},"-",[35,977,978],{"class":136},"1",[35,980,981],{"class":85},"]), ",[35,983,137],{"class":136},[35,985,378],{"class":85},[35,987,988,991,993,996,998,1000],{"class":37,"line":208},[35,989,990],{"class":85},"        nxt ",[35,992,108],{"class":81},[35,994,995],{"class":85}," parent[seen[",[35,997,975],{"class":81},[35,999,978],{"class":136},[35,1001,580],{"class":85},[35,1003,1004,1007,1010,1012],{"class":37,"line":219},[35,1005,1006],{"class":81},"        if",[35,1008,1009],{"class":85}," nxt ",[35,1011,246],{"class":81},[35,1013,1014],{"class":85}," seen:\n",[35,1016,1017,1020,1023,1025,1028],{"class":37,"line":224},[35,1018,1019],{"class":81},"            return",[35,1021,1022],{"class":85}," seen ",[35,1024,198],{"class":81},[35,1026,1027],{"class":85}," [nxt], ",[35,1029,1030],{"class":45},"\"cycle\"\n",[35,1032,1033,1035,1037,1040,1043],{"class":37,"line":237},[35,1034,1006],{"class":81},[35,1036,1009],{"class":85},[35,1038,1039],{"class":81},"not",[35,1041,1042],{"class":81}," in",[35,1044,1045],{"class":85}," parent:\n",[35,1047,1048,1050,1052,1054,1056],{"class":37,"line":252},[35,1049,1019],{"class":81},[35,1051,1022],{"class":85},[35,1053,198],{"class":81},[35,1055,1027],{"class":85},[35,1057,1058],{"class":45},"\"broken\"\n",[35,1060,1061],{"class":37,"line":281},[35,1062,1063],{"class":85},"        seen.append(nxt)\n",[35,1065,1066,1068,1071,1074,1077],{"class":37,"line":286},[35,1067,1006],{"class":81},[35,1069,1070],{"class":136}," len",[35,1072,1073],{"class":85},"(seen) ",[35,1075,1076],{"class":81},">",[35,1078,1079],{"class":85}," limit:\n",[35,1081,1082,1084,1087],{"class":37,"line":620},[35,1083,1019],{"class":81},[35,1085,1086],{"class":85}," seen, ",[35,1088,1089],{"class":45},"\"too deep\"\n",[35,1091,1092,1095,1097],{"class":37,"line":642},[35,1093,1094],{"class":81},"    return",[35,1096,1086],{"class":85},[35,1098,1099],{"class":45},"\"ok\"\n",[35,1101,1102],{"class":37,"line":668},[35,1103,99],{"emptyLinePlaceholder":98},[35,1105,1106,1109,1111,1113],{"class":37,"line":688},[35,1107,1108],{"class":85},"reported ",[35,1110,108],{"class":81},[35,1112,455],{"class":136},[35,1114,1115],{"class":85},"()\n",[35,1117,1119,1121,1124,1126],{"class":37,"line":1118},18,[35,1120,365],{"class":81},[35,1122,1123],{"class":85}," code ",[35,1125,246],{"class":81},[35,1127,1045],{"class":85},[35,1129,1131,1134,1136],{"class":37,"line":1130},19,[35,1132,1133],{"class":85},"    path, status ",[35,1135,108],{"class":81},[35,1137,1138],{"class":85}," chain(code)\n",[35,1140,1142,1145,1148,1150,1153,1156,1158,1160,1163,1166,1168,1171,1174],{"class":37,"line":1141},20,[35,1143,1144],{"class":81},"    if",[35,1146,1147],{"class":85}," status ",[35,1149,573],{"class":81},[35,1151,1152],{"class":45}," \"cycle\"",[35,1154,1155],{"class":81}," and",[35,1157,1070],{"class":136},[35,1159,292],{"class":85},[35,1161,1162],{"class":136},"set",[35,1164,1165],{"class":85},"(path)) ",[35,1167,1076],{"class":81},[35,1169,1170],{"class":136}," 1",[35,1172,1173],{"class":85},":          ",[35,1175,1176],{"class":204},"# a code pointing at itself is reported above\n",[35,1178,1180,1183,1185,1188,1191,1193,1195,1198,1200,1202],{"class":37,"line":1179},21,[35,1181,1182],{"class":85},"        loop ",[35,1184,108],{"class":81},[35,1186,1187],{"class":136}," frozenset",[35,1189,1190],{"class":85},"(path[path.index(path[",[35,1192,975],{"class":81},[35,1194,978],{"class":136},[35,1196,1197],{"class":85},"]):",[35,1199,975],{"class":81},[35,1201,978],{"class":136},[35,1203,463],{"class":85},[35,1205,1207,1209,1212,1214,1216],{"class":37,"line":1206},22,[35,1208,1006],{"class":81},[35,1210,1211],{"class":85}," loop ",[35,1213,1039],{"class":81},[35,1215,1042],{"class":81},[35,1217,1218],{"class":85}," reported:\n",[35,1220,1222],{"class":37,"line":1221},23,[35,1223,1224],{"class":85},"            reported.add(loop)\n",[35,1226,1228,1231,1233,1236,1238,1241,1244,1246,1249,1252,1254,1256],{"class":37,"line":1227},24,[35,1229,1230],{"class":85},"            flag([row_of[c] ",[35,1232,365],{"class":81},[35,1234,1235],{"class":85}," c ",[35,1237,246],{"class":81},[35,1239,1240],{"class":85}," loop], ",[35,1242,1243],{"class":45},"\"cycle\"",[35,1245,117],{"class":85},[35,1247,1248],{"class":45},"\" → \"",[35,1250,1251],{"class":85},".join(path[path.index(path[",[35,1253,975],{"class":81},[35,1255,978],{"class":136},[35,1257,1258],{"class":85},"]):]))\n",[35,1260,1262,1264,1267,1269,1272,1274,1277,1280,1283,1285,1287,1289,1291],{"class":37,"line":1261},25,[35,1263,289],{"class":136},[35,1265,1266],{"class":85},"([f ",[35,1268,365],{"class":81},[35,1270,1271],{"class":85}," f ",[35,1273,246],{"class":81},[35,1275,1276],{"class":85}," findings ",[35,1278,1279],{"class":81},"if",[35,1281,1282],{"class":85}," f[",[35,1284,269],{"class":45},[35,1286,152],{"class":85},[35,1288,573],{"class":81},[35,1290,1152],{"class":45},[35,1292,463],{"class":85},[10,1294,1295,1296,1300,1301,1304],{},"Following parents from 4100 leads to 4200 and back to 4100: a cycle, reported once with both rows and the loop spelled out. A cycle can be longer than two codes, and the walk catches any length. Codes whose chain ends at a missing parent are already reported as orphans; their ",[1297,1298,1299],"em",{},"descendants"," are affected too, and the same walk shows which codes cannot reach a top node — useful for explaining why a whole branch is missing from a report. The ",[32,1302,1303],{},"limit"," guards against pathological chains.",[20,1306,1308],{"id":1307},"step-4-business-rules-depth-code-format-and-leaf-postings","Step 4 — Business rules: depth, code format and leaf postings",[10,1310,1311],{},"Real hierarchies have conventions that a generic check cannot know. Encode yours explicitly; three common ones:",[25,1313,1315],{"className":72,"code":1314,"language":74,"meta":30,"style":30},"MAX_DEPTH = 4\ndepths = {}\nfor code in parent:\n    path, status = chain(code)\n    if status == \"ok\":\n        depths[code] = len(path) - 1\n        if depths[code] > MAX_DEPTH - 1:\n            flag([row_of[code]], \"too deep\", f\"{code} is at level {depths[code] + 1}, maximum is {MAX_DEPTH}\")\n\ndef prefix_ok(code, par):\n    return code[:len(par.rstrip('0'))] == par.rstrip('0')\n\nfor r in tree.drop_duplicates(\"Code\").itertuples():\n    if isinstance(r.Parent, str) and r.Parent in parent and not prefix_ok(r.Code, r.Parent):\n        flag([r._4], \"code format\", f\"{r.Code} does not start with its parent's prefix {r.Parent.rstrip('0')}\")\n\npostings = pd.read_excel(\"postings.xlsx\", dtype={\"Account\": str})\nhas_children = set(tree[\"Parent\"].dropna())\nfor i, p in postings.iterrows():\n    if p[\"Account\"] not in parent:\n        findings.append({\"Excel row\": i + 2, \"Check\": \"posting to unknown code\", \"Detail\": f\"postings.xlsx: {p['Account']}\"})\n    elif p[\"Account\"] in has_children:\n        findings.append({\"Excel row\": i + 2, \"Check\": \"posting to parent\", \"Detail\": f\"postings.xlsx: {p['Account']} has child accounts\"})\nprint([f for f in findings if f[\"Check\"] in (\"too deep\", \"code format\", \"posting to parent\", \"posting to unknown code\")])\n",[32,1316,1317,1327,1337,1347,1355,1369,1386,1405,1452,1456,1466,1493,1497,1512,1545,1585,1589,1619,1635,1647,1664,1713,1729,1775],{"__ignoreMap":30},[35,1318,1319,1322,1324],{"class":37,"line":38},[35,1320,1321],{"class":136},"MAX_DEPTH",[35,1323,626],{"class":81},[35,1325,1326],{"class":136}," 4\n",[35,1328,1329,1332,1334],{"class":37,"line":95},[35,1330,1331],{"class":85},"depths ",[35,1333,108],{"class":81},[35,1335,1336],{"class":85}," {}\n",[35,1338,1339,1341,1343,1345],{"class":37,"line":102},[35,1340,365],{"class":81},[35,1342,1123],{"class":85},[35,1344,246],{"class":81},[35,1346,1045],{"class":85},[35,1348,1349,1351,1353],{"class":37,"line":143},[35,1350,1133],{"class":85},[35,1352,108],{"class":81},[35,1354,1138],{"class":85},[35,1356,1357,1359,1361,1363,1366],{"class":37,"line":165},[35,1358,1144],{"class":81},[35,1360,1147],{"class":85},[35,1362,573],{"class":81},[35,1364,1365],{"class":45}," \"ok\"",[35,1367,1368],{"class":85},":\n",[35,1370,1371,1374,1376,1378,1381,1383],{"class":37,"line":183},[35,1372,1373],{"class":85},"        depths[code] ",[35,1375,108],{"class":81},[35,1377,1070],{"class":136},[35,1379,1380],{"class":85},"(path) ",[35,1382,975],{"class":81},[35,1384,1385],{"class":136}," 1\n",[35,1387,1388,1390,1393,1395,1398,1401,1403],{"class":37,"line":208},[35,1389,1006],{"class":81},[35,1391,1392],{"class":85}," depths[code] ",[35,1394,1076],{"class":81},[35,1396,1397],{"class":136}," MAX_DEPTH",[35,1399,1400],{"class":81}," -",[35,1402,1170],{"class":136},[35,1404,1368],{"class":85},[35,1406,1407,1410,1413,1415,1417,1419,1421,1423,1425,1428,1430,1433,1435,1437,1439,1442,1444,1446,1448,1450],{"class":37,"line":219},[35,1408,1409],{"class":85},"            flag([row_of[code]], ",[35,1411,1412],{"class":45},"\"too deep\"",[35,1414,117],{"class":85},[35,1416,396],{"class":81},[35,1418,399],{"class":45},[35,1420,403],{"class":402},[35,1422,32],{"class":85},[35,1424,408],{"class":402},[35,1426,1427],{"class":45}," is at level ",[35,1429,403],{"class":402},[35,1431,1432],{"class":85},"depths[code] ",[35,1434,198],{"class":81},[35,1436,1170],{"class":136},[35,1438,408],{"class":402},[35,1440,1441],{"class":45},", maximum is ",[35,1443,403],{"class":402},[35,1445,1321],{"class":136},[35,1447,408],{"class":402},[35,1449,399],{"class":45},[35,1451,140],{"class":85},[35,1453,1454],{"class":37,"line":224},[35,1455,99],{"emptyLinePlaceholder":98},[35,1457,1458,1460,1463],{"class":37,"line":237},[35,1459,227],{"class":81},[35,1461,1462],{"class":230}," prefix_ok",[35,1464,1465],{"class":85},"(code, par):\n",[35,1467,1468,1470,1473,1475,1478,1481,1484,1486,1489,1491],{"class":37,"line":252},[35,1469,1094],{"class":81},[35,1471,1472],{"class":85}," code[:",[35,1474,295],{"class":136},[35,1476,1477],{"class":85},"(par.rstrip(",[35,1479,1480],{"class":45},"'0'",[35,1482,1483],{"class":85},"))] ",[35,1485,573],{"class":81},[35,1487,1488],{"class":85}," par.rstrip(",[35,1490,1480],{"class":45},[35,1492,140],{"class":85},[35,1494,1495],{"class":37,"line":281},[35,1496,99],{"emptyLinePlaceholder":98},[35,1498,1499,1501,1503,1505,1507,1509],{"class":37,"line":286},[35,1500,365],{"class":81},[35,1502,243],{"class":85},[35,1504,246],{"class":81},[35,1506,888],{"class":85},[35,1508,149],{"class":45},[35,1510,1511],{"class":85},").itertuples():\n",[35,1513,1514,1516,1518,1521,1523,1526,1529,1532,1534,1537,1539,1542],{"class":37,"line":620},[35,1515,1144],{"class":81},[35,1517,969],{"class":136},[35,1519,1520],{"class":85},"(r.Parent, ",[35,1522,137],{"class":136},[35,1524,1525],{"class":85},") ",[35,1527,1528],{"class":81},"and",[35,1530,1531],{"class":85}," r.Parent ",[35,1533,246],{"class":81},[35,1535,1536],{"class":85}," parent ",[35,1538,1528],{"class":81},[35,1540,1541],{"class":81}," not",[35,1543,1544],{"class":85}," prefix_ok(r.Code, r.Parent):\n",[35,1546,1547,1550,1553,1555,1557,1559,1561,1564,1566,1569,1571,1574,1576,1579,1581,1583],{"class":37,"line":642},[35,1548,1549],{"class":85},"        flag([r._4], ",[35,1551,1552],{"class":45},"\"code format\"",[35,1554,117],{"class":85},[35,1556,396],{"class":81},[35,1558,399],{"class":45},[35,1560,403],{"class":402},[35,1562,1563],{"class":85},"r.Code",[35,1565,408],{"class":402},[35,1567,1568],{"class":45}," does not start with its parent's prefix ",[35,1570,403],{"class":402},[35,1572,1573],{"class":85},"r.Parent.rstrip(",[35,1575,1480],{"class":45},[35,1577,1578],{"class":85},")",[35,1580,408],{"class":402},[35,1582,399],{"class":45},[35,1584,140],{"class":85},[35,1586,1587],{"class":37,"line":668},[35,1588,99],{"emptyLinePlaceholder":98},[35,1590,1591,1594,1596,1598,1601,1603,1605,1607,1609,1612,1614,1616],{"class":37,"line":688},[35,1592,1593],{"class":85},"postings ",[35,1595,108],{"class":81},[35,1597,111],{"class":85},[35,1599,1600],{"class":45},"\"postings.xlsx\"",[35,1602,117],{"class":85},[35,1604,131],{"class":120},[35,1606,108],{"class":81},[35,1608,403],{"class":85},[35,1610,1611],{"class":45},"\"Account\"",[35,1613,260],{"class":85},[35,1615,137],{"class":136},[35,1617,1618],{"class":85},"})\n",[35,1620,1621,1624,1626,1628,1630,1632],{"class":37,"line":1118},[35,1622,1623],{"class":85},"has_children ",[35,1625,108],{"class":81},[35,1627,455],{"class":136},[35,1629,458],{"class":85},[35,1631,170],{"class":45},[35,1633,1634],{"class":85},"].dropna())\n",[35,1636,1637,1639,1642,1644],{"class":37,"line":1130},[35,1638,365],{"class":81},[35,1640,1641],{"class":85}," i, p ",[35,1643,246],{"class":81},[35,1645,1646],{"class":85}," postings.iterrows():\n",[35,1648,1649,1651,1654,1656,1658,1660,1662],{"class":37,"line":1141},[35,1650,1144],{"class":81},[35,1652,1653],{"class":85}," p[",[35,1655,1611],{"class":45},[35,1657,152],{"class":85},[35,1659,1039],{"class":81},[35,1661,1042],{"class":81},[35,1663,1045],{"class":85},[35,1665,1666,1668,1670,1673,1675,1677,1679,1681,1683,1686,1688,1690,1692,1694,1697,1699,1702,1705,1707,1709,1711],{"class":37,"line":1179},[35,1667,255],{"class":85},[35,1669,188],{"class":45},[35,1671,1672],{"class":85},": i ",[35,1674,198],{"class":81},[35,1676,201],{"class":136},[35,1678,117],{"class":85},[35,1680,269],{"class":45},[35,1682,260],{"class":85},[35,1684,1685],{"class":45},"\"posting to unknown code\"",[35,1687,117],{"class":85},[35,1689,275],{"class":45},[35,1691,260],{"class":85},[35,1693,396],{"class":81},[35,1695,1696],{"class":45},"\"postings.xlsx: ",[35,1698,403],{"class":402},[35,1700,1701],{"class":85},"p[",[35,1703,1704],{"class":45},"'Account'",[35,1706,532],{"class":85},[35,1708,408],{"class":402},[35,1710,399],{"class":45},[35,1712,1618],{"class":85},[35,1714,1715,1718,1720,1722,1724,1726],{"class":37,"line":1206},[35,1716,1717],{"class":81},"    elif",[35,1719,1653],{"class":85},[35,1721,1611],{"class":45},[35,1723,152],{"class":85},[35,1725,246],{"class":81},[35,1727,1728],{"class":85}," has_children:\n",[35,1730,1731,1733,1735,1737,1739,1741,1743,1745,1747,1750,1752,1754,1756,1758,1760,1762,1764,1766,1768,1770,1773],{"class":37,"line":1221},[35,1732,255],{"class":85},[35,1734,188],{"class":45},[35,1736,1672],{"class":85},[35,1738,198],{"class":81},[35,1740,201],{"class":136},[35,1742,117],{"class":85},[35,1744,269],{"class":45},[35,1746,260],{"class":85},[35,1748,1749],{"class":45},"\"posting to parent\"",[35,1751,117],{"class":85},[35,1753,275],{"class":45},[35,1755,260],{"class":85},[35,1757,396],{"class":81},[35,1759,1696],{"class":45},[35,1761,403],{"class":402},[35,1763,1701],{"class":85},[35,1765,1704],{"class":45},[35,1767,532],{"class":85},[35,1769,408],{"class":402},[35,1771,1772],{"class":45}," has child accounts\"",[35,1774,1618],{"class":85},[35,1776,1777,1779,1781,1783,1785,1787,1789,1791,1793,1795,1797,1799,1802,1804,1806,1808,1810,1812,1814,1816],{"class":37,"line":1227},[35,1778,289],{"class":136},[35,1780,1266],{"class":85},[35,1782,365],{"class":81},[35,1784,1271],{"class":85},[35,1786,246],{"class":81},[35,1788,1276],{"class":85},[35,1790,1279],{"class":81},[35,1792,1282],{"class":85},[35,1794,269],{"class":45},[35,1796,152],{"class":85},[35,1798,246],{"class":81},[35,1800,1801],{"class":85}," (",[35,1803,1412],{"class":45},[35,1805,117],{"class":85},[35,1807,1552],{"class":45},[35,1809,117],{"class":85},[35,1811,1749],{"class":45},[35,1813,117],{"class":85},[35,1815,1685],{"class":45},[35,1817,1818],{"class":85},")])\n",[10,1820,1821],{},"The depth rule flags 1213 and 1214, at levels five and six of a structure meant to have at most four — vehicles were subdivided twice beyond the agreed depth. The prefix rule — a child's code starts with its parent's code minus trailing zeros, so 1210's children are 121x — catches 2120 Accruals filed under 1100 Current assets, almost certainly a mistake. It also flags 1213 and 1214 again, because four-digit codes leave no room for a fifth level — a sign that the depth and the code scheme have to agree — and the two codes in the 4100\u002F4200 cycle. One underlying error often trips several rules; the report groups findings by row so that is easy to see. The posting checks catch balances on 1100 and 1210, which both have child accounts (in a strict chart only leaf accounts take postings), and a balance on 9999, which is not in the tree at all. Each rule is a few lines because the structure has already been reduced to a parent dictionary.",[708,1823,717,1828,717,1831,717,1834,717,1837,717,1843,717,1854,717,1859,717,1864,717,1867,717,1871,717,1875,717,1880,717,1887,717,1892,717,1896,717,1899,717,1902],{"viewBox":1824,"role":711,"ariaLabelledBy":1825,"xmlns":715,"style":716},"4 0 752 207",[1826,1827],"hv-rules-t","hv-rules-d",[719,1829,1830],{"id":1826},"Generic structure checks and business rules",[723,1832,1833],{"id":1827},"Generic checks apply to any hierarchy, such as duplicates, missing parents, cycles and roots. Business rules depend on the organisation, such as a maximum depth, child codes starting with the parent's prefix, and postings allowed only on leaf nodes.",[727,1835],{"x":729,"y":730,"width":731,"height":1836,"fill":733},"207",[727,1838],{"x":736,"y":737,"width":1839,"height":1840,"rx":1841,"fill":746,"stroke":747,"style":1842},"351.0","175.0","14","stroke-width:2px",[1844,1845,1847],"g",{"transform":1846},"translate(182.5,29.0) scale(1.083)",[1848,1849],"path",{"d":1850,"fill":1851,"stroke":1852,"style":1853},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","none","var(--brand-strong,#4338ca)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[750,1855,1858],{"x":1856,"y":771,"style":1857},"195.5","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","generic structure",[37,1860],{"x1":1861,"y1":1862,"x2":1863,"y2":1862,"stroke":747,"style":741},"36.0","91.0","355.0",[750,1865,780],{"x":1856,"y":797,"style":1866},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[750,1868,1870],{"x":1856,"y":1869,"style":1866},"137.0","parents exist, no cycles",[750,1872,1874],{"x":1856,"y":1873,"style":1866},"163.0","expected top nodes only",[727,1876],{"x":1877,"y":737,"width":1839,"height":1840,"rx":1841,"fill":1878,"stroke":1879,"style":1842},"389.0","#d9f4f1","var(--teal,#0f9488)",[1844,1881,1883],{"transform":1882},"translate(551.5,29.0) scale(1.083)",[1848,1884],{"d":1885,"fill":1851,"stroke":1886,"style":1853},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--teal-ink,#0b6157)",[750,1888,1891],{"x":1889,"y":771,"style":1890},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","business rules",[37,1893],{"x1":1894,"y1":1862,"x2":1895,"y2":1862,"stroke":1879,"style":741},"405.0","724.0",[750,1897,1898],{"x":1889,"y":797,"style":1866},"max depth 4 levels",[750,1900,1901],{"x":1889,"y":1869,"style":1866},"child starts with parent prefix",[750,1903,1904],{"x":1889,"y":1873,"style":1866},"postings on leaf accounts only",[708,1906,717,1911,717,1914,717,1917,717,1920,717,1925,717,1933,717,1939,717,1943,717,1947,717,1952,717,1957,717,1960,717,1965,717,1970,717,1973,717,1976,717,1979,717,1982,717,1985,717,1991,717,1997,717,2001,717,2005,717,2008,717,2013,717,2020,717,2025],{"viewBox":1907,"role":711,"ariaLabelledBy":1908,"xmlns":715,"style":716},"4 0 752 142",[1909,1910],"hv-flow-t","hv-flow-d",[719,1912,1913],{"id":1909},"The order of the checks",[723,1915,1916],{"id":1910},"Read codes as text and keep Excel row numbers, run the structural checks for duplicates, missing parents, self-references and roots, walk parent chains to find cycles and unreachable codes, apply business rules for depth, code format and postings, then write a findings list and a highlighted copy of the sheet.",[727,1918],{"x":729,"y":730,"width":731,"height":1919,"fill":733},"142",[727,1921],{"x":736,"y":737,"width":1922,"height":1923,"rx":1924,"fill":767,"stroke":740,"style":1842},"153.0","110.5","12",[1844,1926,1928],{"transform":1927},"translate(84.5,29.0) scale(1.000)",[1848,1929],{"d":1930,"fill":1851,"stroke":1931,"style":1932},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[750,1934,1938],{"x":1935,"y":1936,"style":1937},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Read as text",[750,1940,1942],{"x":1935,"y":1941,"style":779},"95.0","keep Excel row",[750,1944,1946],{"x":1935,"y":1945,"style":779},"112.5","numbers",[37,1948],{"x1":1949,"y1":1950,"x2":1951,"y2":1950,"stroke":747,"style":1842},"178.0","71.2","200.0",[1953,1954],"polygon",{"points":1955,"fill":1956},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[727,1958],{"x":1959,"y":737,"width":1922,"height":1923,"rx":1924,"fill":746,"stroke":747,"style":1842},"209.0",[1844,1961,1963],{"transform":1962},"translate(273.5,29.0) scale(1.000)",[1848,1964],{"d":1850,"fill":1851,"stroke":1852,"style":1932},[750,1966,1969],{"x":1967,"y":1936,"style":1968},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Structure",[750,1971,1972],{"x":1967,"y":1941,"style":779},"duplicates, orphans,",[750,1974,1975],{"x":1967,"y":1945,"style":779},"roots",[37,1977],{"x1":1978,"y1":1950,"x2":1877,"y2":1950,"stroke":747,"style":1842},"367.0",[1953,1980],{"points":1981,"fill":1956},"393.0,71.2 384.0,76.2 384.0,66.2",[727,1983],{"x":1984,"y":737,"width":1922,"height":1923,"rx":1924,"fill":1878,"stroke":1879,"style":1842},"398.0",[1844,1986,1988],{"transform":1987},"translate(462.5,37.8) scale(1.000)",[1848,1989],{"d":1990,"fill":1851,"stroke":1886,"style":1932},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[750,1992,1996],{"x":1993,"y":1994,"style":1995},"474.5","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Chains",[750,1998,2000],{"x":1993,"y":1999,"style":779},"103.8","cycles, unreachable",[37,2002],{"x1":2003,"y1":1950,"x2":2004,"y2":1950,"stroke":747,"style":1842},"556.0","578.0",[1953,2006],{"points":2007,"fill":1956},"582.0,71.2 573.0,76.2 573.0,66.2",[727,2009],{"x":2010,"y":737,"width":1922,"height":1923,"rx":1924,"fill":2011,"stroke":2012,"style":1842},"587.0","#fdefd8","var(--gold,#b4740a)",[1844,2014,2016],{"transform":2015},"translate(651.5,37.8) scale(1.000)",[1848,2017],{"d":2018,"fill":1851,"stroke":2019,"style":1932},"M5 12.5l4.5 4.5L19 7","var(--gold-ink,#7a4e06)",[750,2021,2024],{"x":2022,"y":1994,"style":2023},"663.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Rules + report",[750,2026,2027],{"x":2022,"y":1999,"style":779},"depth, prefix, postings",[20,2029,2031],{"id":2030},"step-5-a-report-the-maintainer-can-act-on","Step 5 — A report the maintainer can act on",[10,2033,2034],{},"Write all findings to Excel, sorted by row, and highlight the rows in a copy of the original sheet:",[25,2036,2038],{"className":72,"code":2037,"language":74,"meta":30,"style":30},"from openpyxl import load_workbook\nfrom openpyxl.comments import Comment\nfrom openpyxl.styles import PatternFill\n\nreport = pd.DataFrame(findings).sort_values([\"Check\", \"Excel row\"])\nreport.to_excel(\"hierarchy_findings.xlsx\", index=False)\n\nwb = load_workbook(\"accounts.xlsx\")\nws = wb[\"Tree\"]\nfor row, grp in report[~report[\"Check\"].str.startswith(\"posting\")].groupby(\"Excel row\"):\n    for c in ws[row]:\n        c.fill = PatternFill(\"solid\", fgColor=\"FFC7CE\")\n    ws.cell(row, 1).comment = Comment(\"; \".join(f\"{a}: {b}\" for a, b in zip(grp[\"Check\"], grp[\"Detail\"])), \"validator\")\nwb.save(\"accounts_checked.xlsx\")\nprint(report.groupby(\"Check\").size().to_dict())\nprint(\"hierarchy valid:\", report.empty)\n",[32,2039,2040,2053,2065,2077,2081,2099,2117,2121,2135,2150,2182,2193,2218,2288,2298,2310],{"__ignoreMap":30},[35,2041,2042,2045,2048,2050],{"class":37,"line":38},[35,2043,2044],{"class":81},"from",[35,2046,2047],{"class":85}," openpyxl ",[35,2049,82],{"class":81},[35,2051,2052],{"class":85}," load_workbook\n",[35,2054,2055,2057,2060,2062],{"class":37,"line":95},[35,2056,2044],{"class":81},[35,2058,2059],{"class":85}," openpyxl.comments ",[35,2061,82],{"class":81},[35,2063,2064],{"class":85}," Comment\n",[35,2066,2067,2069,2072,2074],{"class":37,"line":102},[35,2068,2044],{"class":81},[35,2070,2071],{"class":85}," openpyxl.styles ",[35,2073,82],{"class":81},[35,2075,2076],{"class":85}," PatternFill\n",[35,2078,2079],{"class":37,"line":143},[35,2080,99],{"emptyLinePlaceholder":98},[35,2082,2083,2086,2088,2091,2093,2095,2097],{"class":37,"line":165},[35,2084,2085],{"class":85},"report ",[35,2087,108],{"class":81},[35,2089,2090],{"class":85}," pd.DataFrame(findings).sort_values([",[35,2092,269],{"class":45},[35,2094,117],{"class":85},[35,2096,188],{"class":45},[35,2098,463],{"class":85},[35,2100,2101,2104,2107,2109,2111,2113,2115],{"class":37,"line":183},[35,2102,2103],{"class":85},"report.to_excel(",[35,2105,2106],{"class":45},"\"hierarchy_findings.xlsx\"",[35,2108,117],{"class":85},[35,2110,696],{"class":120},[35,2112,108],{"class":81},[35,2114,357],{"class":136},[35,2116,140],{"class":85},[35,2118,2119],{"class":37,"line":208},[35,2120,99],{"emptyLinePlaceholder":98},[35,2122,2123,2126,2128,2131,2133],{"class":37,"line":219},[35,2124,2125],{"class":85},"wb ",[35,2127,108],{"class":81},[35,2129,2130],{"class":85}," load_workbook(",[35,2132,114],{"class":45},[35,2134,140],{"class":85},[35,2136,2137,2140,2142,2145,2147],{"class":37,"line":224},[35,2138,2139],{"class":85},"ws ",[35,2141,108],{"class":81},[35,2143,2144],{"class":85}," wb[",[35,2146,126],{"class":45},[35,2148,2149],{"class":85},"]\n",[35,2151,2152,2154,2157,2159,2162,2164,2167,2169,2172,2175,2178,2180],{"class":37,"line":237},[35,2153,365],{"class":81},[35,2155,2156],{"class":85}," row, grp ",[35,2158,246],{"class":81},[35,2160,2161],{"class":85}," report[",[35,2163,653],{"class":81},[35,2165,2166],{"class":85},"report[",[35,2168,269],{"class":45},[35,2170,2171],{"class":85},"].str.startswith(",[35,2173,2174],{"class":45},"\"posting\"",[35,2176,2177],{"class":85},")].groupby(",[35,2179,188],{"class":45},[35,2181,378],{"class":85},[35,2183,2184,2186,2188,2190],{"class":37,"line":252},[35,2185,240],{"class":81},[35,2187,1235],{"class":85},[35,2189,246],{"class":81},[35,2191,2192],{"class":85}," ws[row]:\n",[35,2194,2195,2198,2200,2203,2206,2208,2211,2213,2216],{"class":37,"line":281},[35,2196,2197],{"class":85},"        c.fill ",[35,2199,108],{"class":81},[35,2201,2202],{"class":85}," PatternFill(",[35,2204,2205],{"class":45},"\"solid\"",[35,2207,117],{"class":85},[35,2209,2210],{"class":120},"fgColor",[35,2212,108],{"class":81},[35,2214,2215],{"class":45},"\"FFC7CE\"",[35,2217,140],{"class":85},[35,2219,2220,2223,2225,2228,2230,2233,2236,2238,2240,2242,2244,2246,2248,2250,2252,2255,2257,2259,2262,2265,2267,2270,2273,2275,2278,2280,2283,2286],{"class":37,"line":286},[35,2221,2222],{"class":85},"    ws.cell(row, ",[35,2224,978],{"class":136},[35,2226,2227],{"class":85},").comment ",[35,2229,108],{"class":81},[35,2231,2232],{"class":85}," Comment(",[35,2234,2235],{"class":45},"\"; \"",[35,2237,419],{"class":85},[35,2239,396],{"class":81},[35,2241,399],{"class":45},[35,2243,403],{"class":402},[35,2245,14],{"class":85},[35,2247,408],{"class":402},[35,2249,260],{"class":45},[35,2251,403],{"class":402},[35,2253,2254],{"class":85},"b",[35,2256,408],{"class":402},[35,2258,399],{"class":45},[35,2260,2261],{"class":81}," for",[35,2263,2264],{"class":85}," a, b ",[35,2266,246],{"class":81},[35,2268,2269],{"class":136}," zip",[35,2271,2272],{"class":85},"(grp[",[35,2274,269],{"class":45},[35,2276,2277],{"class":85},"], grp[",[35,2279,275],{"class":45},[35,2281,2282],{"class":85},"])), ",[35,2284,2285],{"class":45},"\"validator\"",[35,2287,140],{"class":85},[35,2289,2290,2293,2296],{"class":37,"line":620},[35,2291,2292],{"class":85},"wb.save(",[35,2294,2295],{"class":45},"\"accounts_checked.xlsx\"",[35,2297,140],{"class":85},[35,2299,2300,2302,2305,2307],{"class":37,"line":642},[35,2301,289],{"class":136},[35,2303,2304],{"class":85},"(report.groupby(",[35,2306,269],{"class":45},[35,2308,2309],{"class":85},").size().to_dict())\n",[35,2311,2312,2314,2316,2319],{"class":37,"line":668},[35,2313,289],{"class":136},[35,2315,292],{"class":85},[35,2317,2318],{"class":45},"\"hierarchy valid:\"",[35,2320,2321],{"class":85},", report.empty)\n",[10,2323,2324,2325,2329,2330,18],{},"The findings workbook lists every problem with its check and detail; the checked copy of the tree highlights each faulty row in red with a comment explaining what is wrong, so the maintainer fixes the problems in place without cross-referencing two files. The final line is the gate for anything downstream: a roll-up or a database load should run only when the report is empty. Once the hierarchy is clean, rolling amounts up through it is covered in ",[14,2326,2328],{"href":2327},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Froll-up-totals-through-a-parent-child-hierarchy\u002F","Roll Up Totals Through a Parent-Child Hierarchy",", and highlighting invalid cells in general in ",[14,2331,2333],{"href":2332},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002F","Highlight Invalid Cells in Excel with Python",[20,2335,2337],{"id":2336},"common-pitfalls","Common pitfalls",[2339,2340,2341,2355,2361,2367,2373],"ul",{},[2342,2343,2344,2348,2349,2351,2352,2354],"li",{},[2345,2346,2347],"strong",{},"Codes read as numbers."," Leading zeros vanish and ",[32,2350,313],{}," stops matching ",[32,2353,309],{},"; read as text.",[2342,2356,2357,2360],{},[2345,2358,2359],{},"Rolling up before validating."," Orphaned branches silently drop out of the totals.",[2342,2362,2363,2366],{},[2345,2364,2365],{},"Assuming one top node."," List the expected roots explicitly.",[2342,2368,2369,2372],{},[2345,2370,2371],{},"Checking only two-node cycles."," Walk the full parent chain to catch loops of any length.",[2342,2374,2375,2378],{},[2345,2376,2377],{},"Reports without row numbers."," Point at the Excel row so fixes are quick.",[20,2380,2382],{"id":2381},"conclusion","Conclusion",[10,2384,2385],{},"A parent-child hierarchy maintained by hand needs checking before anything is rolled up through it. Read codes as text, keep Excel row numbers, and test for duplicate codes, parents that do not exist, self-references, unexpected top nodes and cycles of any length by walking each code's chain of parents. Add the organisation's own rules — maximum depth, child codes matching their parent's prefix, postings only on leaf nodes — and write the findings both as a list and as highlighted rows with comments in a copy of the sheet, so the maintainer can fix them where they are.",[20,2387,2389],{"id":2388},"frequently-asked-questions","Frequently asked questions",[10,2391,2392,2395],{},[2345,2393,2394],{},"How do I find orphan codes in a hierarchy with pandas?","\nRows whose parent is not blank and does not appear in the Code column are orphans; test the Parent column with notna and isin against the set of codes.",[10,2397,2398,2401],{},[2345,2399,2400],{},"How do I detect cycles in a parent-child table?","\nFollow each code's parent links and keep a list of codes seen; if a parent is already in the list, the chain contains a cycle.",[10,2403,2404,2407],{},[2345,2405,2406],{},"How do I check that postings only go to bottom-level accounts?","\nCollect the set of codes that appear as someone's parent; a posting to any code in that set is a posting to a non-leaf node.",[10,2409,2410,2413],{},[2345,2411,2412],{},"What should a hierarchy validation report contain?","\nThe Excel row, the check that failed and a readable detail for each problem, ideally with the faulty rows highlighted in a copy of the sheet.",[20,2415,2417],{"id":2416},"related","Related",[2339,2419,2420,2426,2433,2440],{},[2342,2421,2422,2423,2425],{},"Up one level: ",[14,2424,17],{"href":16}," — checking data before it is used.",[2342,2427,2428,2432],{},[14,2429,2431],{"href":2430},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-references-between-excel-sheets\u002F","Check References Between Excel Sheets"," — foreign-key checks between tables.",[2342,2434,2435,2439],{},[14,2436,2438],{"href":2437},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python\u002F","Find Duplicate Rows in Excel with Python"," — duplicates in general.",[2342,2441,2442,2446],{},[14,2443,2445],{"href":2444},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-trial-balance-from-a-general-ledger\u002F","Build a Trial Balance from a General Ledger"," — what the account tree feeds.",[2448,2449,2450],"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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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}",{"title":30,"searchDepth":95,"depth":95,"links":2452},[2453,2454,2455,2456,2457,2458,2459,2460,2461,2462],{"id":22,"depth":95,"text":23},{"id":65,"depth":95,"text":66},{"id":325,"depth":95,"text":326},{"id":860,"depth":95,"text":861},{"id":1307,"depth":95,"text":1308},{"id":2030,"depth":95,"text":2031},{"id":2336,"depth":95,"text":2337},{"id":2381,"depth":95,"text":2382},{"id":2388,"depth":95,"text":2389},{"id":2416,"depth":95,"text":2417},"2026-10-02","Check a parent-child table maintained in Excel — an account tree, cost-centre structure or product category list — before anything is rolled up through it: duplicate codes, parents that do not exist, more than one top node, cycles and self-references, unreachable branches, depth limits, code-format rules that tie children to parents, and postings to non-leaf nodes, all reported with the Excel row to fix.","md",[2467,2469,2471,2473],{"q":2394,"a":2468},"Rows whose parent is not blank and does not appear in the Code column are orphans; test the Parent column with notna and isin against the set of codes.",{"q":2400,"a":2470},"Follow each code's parent links and keep a list of codes seen; if a parent is already in the list, the chain contains a cycle.",{"q":2406,"a":2472},"Collect the set of codes that appear as someone's parent; a posting to any code in that set is a posting to a non-leaf node.",{"q":2412,"a":2474},"The Excel row, the check that failed and a readable detail for each problem, ideally with the faulty rows highlighted in a copy of the sheet.","validate parent-child codes in a hierarchy",null,{"breadcrumb":2478},[2479,2482,2485],{"name":2480,"item":2481},"Home","\u002F",{"name":2483,"item":2484},"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\u002Fvalidate-parent-child-codes-in-a-hierarchy",{"title":2488,"description":2489},"Validate Parent-Child Codes in an Excel Hierarchy with Python","Validate an Excel hierarchy with pandas: duplicate codes, orphans with missing parents, multiple roots, cycles, unreachable nodes, maximum depth, child-code prefix rules and postings to parent nodes, with a row-numbered report.","validate-parent-child-codes-in-a-hierarchy","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy\u002Findex",[2493],"data-validation","how-to","rOSpm30Eps5TmXP3GnVhThpxeHScJZbnFuAUvcvrPz4",[2497,2501],{"title":2498,"path":2499,"stem":2500,"children":-1},"Validate Excel Rows with pydantic Models","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic\u002Findex",{"title":2502,"path":2503,"stem":2504,"children":-1},"Working with Dates and Times in Excel Data","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Findex",[2506,2508],{"path":2507,"title":2483},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2509,"title":17},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python",[],[2512,2515,2518,2521,2524,2527],{"path":2513,"title":2514,"kind":2494},"\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":2516,"title":2517,"kind":2494},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl","Highlight Cells Containing Specific Text with openpyxl",{"path":2519,"title":2520,"kind":2494},"\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":2522,"title":2523,"kind":2494},"\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":2525,"title":2526,"kind":2494},"\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":2528,"title":2529,"kind":2494},"\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",1790972100994]