[{"data":1,"prerenderedAt":2100},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas":2069,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas":2078,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas":2083,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas":2084},{"id":4,"title":5,"body":6,"dateModified":2034,"datePublished":2034,"description":2035,"extension":2036,"faq":2037,"keyword":2046,"kind":2047,"meta":2048,"navigation":363,"path":2057,"seo":2058,"slug":2061,"stem":2062,"topics":2063,"type":2067,"__hash__":2068},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas\u002Findex.md","Convert Percentage Strings Like \"12.5%\" to Numbers with pandas",{"type":7,"value":8,"toc":2019},"minimark",[9,32,280,285,313,323,327,419,447,451,460,464,474,892,911,1056,1060,1074,1197,1205,1209,1212,1335,1338,1469,1473,1476,1677,1699,1703,1714,1853,1871,1875,1924,1928,1941,1945,1954,1958,1964,1970,1976,1982,1986,2015],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"A column of margins, discount rates or growth figures should be the easiest thing in the world to average. In practice it arrives mixed: some cells were typed as text with a percent sign, some were real percentage cells that pandas reads as fractions, one uses a decimal comma, one is in basis points, and one is a bare ",[14,15,16],"code",{},"12.5"," where somebody forgot the ",[14,19,20],{},"%",". ",[14,23,24],{},"pd.to_numeric"," makes a mess of all of it, and an average taken naively is wrong by a factor of a hundred in some rows. This page, part of ",[27,28,30],"a",{"href":29},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002F","Cleaning Excel Data With Pandas",", converts every form to one consistent fraction and flags the values a rule cannot safely decide.",[33,34,42,43,42,47,42,51,42,59,42,66,42,75,42,79,42,85,42,89,42,93,42,97,42,101,42,105,42,110,42,112,42,116,42,118,42,121,42,123,42,126,42,129,42,133,42,135,42,138,42,140,42,143,42,145,42,148,42,151,42,155,42,157,42,159,42,164,42,168,42,170,42,173,42,176,42,180,42,182,42,184,42,186,42,189,42,191,42,193,42,196,42,200,42,202,42,205,42,207,42,210,42,212,42,214,42,217,42,221,42,223,42,226,42,228,42,231,42,233,42,235,42,238,42,242,42,244,42,247,42,251,42,254,42,256,42,259,42,262,42,266,42,268,42,271,42,273,42,276,42,278],"svg",{"viewBox":35,"role":36,"ariaLabelledBy":37,"xmlns":40,"style":41},"193 0 373 312","img",[38,39],"pct-mixed-t","pct-mixed-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:373px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[44,45,46],"title",{"id":38},"One margin column, six different forms",[48,49,50],"desc",{"id":39},"A margins sheet whose margin column holds the text 12.5%, a percent-formatted cell that stores 0.125, the text 12,5 % with a decimal comma, the text -3%, the text 25 bp, a typed number 12.5 with no percent sign, the text 0.4 and a blank. They must all become fractions.",[52,53],"rect",{"x":54,"y":55,"width":56,"height":57,"fill":58},"193","0","373","312","#ffffff",[60,61,65],"text",{"x":62,"y":63,"style":64},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","margins.xlsx — what the cells hold",[52,67],{"x":68,"y":69,"width":70,"height":71,"rx":55,"fill":72,"stroke":73,"style":74},"209.5","48.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[52,76],{"x":77,"y":69,"width":78,"height":71,"rx":55,"fill":72,"stroke":73,"style":74},"241.5","72.6",[60,80,84],{"x":81,"y":82,"style":83},"277.8","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[52,86],{"x":87,"y":69,"width":88,"height":71,"rx":55,"fill":72,"stroke":73,"style":74},"314.1","126.0",[60,90,92],{"x":91,"y":82,"style":83},"377.1","B",[52,94],{"x":95,"y":69,"width":96,"height":71,"rx":55,"fill":72,"stroke":73,"style":74},"440.1","110.3",[60,98,100],{"x":99,"y":82,"style":83},"495.3","C",[52,102],{"x":68,"y":103,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"72.0","28.0",[60,106,109],{"x":107,"y":108,"style":83},"225.5","91.0","1",[52,111],{"x":77,"y":103,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,113,115],{"x":81,"y":108,"style":114},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","product",[52,117],{"x":87,"y":103,"width":88,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,119,120],{"x":91,"y":108,"style":114},"margin",[52,122],{"x":95,"y":103,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,124,125],{"x":99,"y":108,"style":114},"source",[52,127],{"x":68,"y":128,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"100.0",[60,130,132],{"x":107,"y":131,"style":83},"119.0","2",[52,134],{"x":77,"y":128,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,136,84],{"x":81,"y":131,"style":137},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[52,139],{"x":87,"y":128,"width":88,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,141,142],{"x":91,"y":131,"style":137},"\"12.5%\"",[52,144],{"x":95,"y":128,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,146,147],{"x":99,"y":131,"style":137},"typed",[52,149],{"x":68,"y":150,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"128.0",[60,152,154],{"x":107,"y":153,"style":83},"147.0","3",[52,156],{"x":77,"y":150,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,158,92],{"x":81,"y":153,"style":137},[52,160],{"x":87,"y":150,"width":88,"height":104,"rx":55,"fill":161,"stroke":162,"style":163},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[60,165,167],{"x":91,"y":153,"style":166},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","0.125 (as 12.5%)",[52,169],{"x":95,"y":150,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,171,172],{"x":99,"y":153,"style":137},"formatted",[52,174],{"x":68,"y":175,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"156.0",[60,177,179],{"x":107,"y":178,"style":83},"175.0","4",[52,181],{"x":77,"y":175,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,183,100],{"x":81,"y":178,"style":137},[52,185],{"x":87,"y":175,"width":88,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,187,188],{"x":91,"y":178,"style":137},"\"12,5 %\"",[52,190],{"x":95,"y":175,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,192,147],{"x":99,"y":178,"style":137},[52,194],{"x":68,"y":195,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"184.0",[60,197,199],{"x":107,"y":198,"style":83},"203.0","5",[52,201],{"x":77,"y":195,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,203,204],{"x":81,"y":198,"style":137},"D",[52,206],{"x":87,"y":195,"width":88,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,208,209],{"x":91,"y":198,"style":137},"\"-3%\"",[52,211],{"x":95,"y":195,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,213,147],{"x":99,"y":198,"style":137},[52,215],{"x":68,"y":216,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"212.0",[60,218,220],{"x":107,"y":219,"style":83},"231.0","6",[52,222],{"x":77,"y":216,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,224,225],{"x":81,"y":219,"style":137},"E",[52,227],{"x":87,"y":216,"width":88,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,229,230],{"x":91,"y":219,"style":137},"\"25 bp\"",[52,232],{"x":95,"y":216,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,234,147],{"x":99,"y":219,"style":137},[52,236],{"x":68,"y":237,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"240.0",[60,239,241],{"x":107,"y":240,"style":83},"259.0","7",[52,243],{"x":77,"y":237,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,245,246],{"x":81,"y":240,"style":137},"F",[52,248],{"x":87,"y":237,"width":88,"height":104,"rx":55,"fill":249,"stroke":250,"style":163},"#fee8f2","var(--accent,#f43f8f)",[60,252,16],{"x":91,"y":240,"style":253},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[52,255],{"x":95,"y":237,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,257,258],{"x":99,"y":240,"style":137},"typed number",[52,260],{"x":68,"y":261,"width":70,"height":104,"rx":55,"fill":72,"stroke":73,"style":74},"268.0",[60,263,265],{"x":107,"y":264,"style":83},"287.0","8",[52,267],{"x":77,"y":261,"width":78,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,269,270],{"x":81,"y":264,"style":137},"G",[52,272],{"x":87,"y":261,"width":88,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,274,275],{"x":91,"y":264,"style":137},"\"0.4\"",[52,277],{"x":95,"y":261,"width":96,"height":104,"rx":55,"fill":58,"stroke":73,"style":74},[60,279,147],{"x":99,"y":264,"style":137},[281,282,284],"h2",{"id":283},"prerequisites","Prerequisites",[286,287,292],"pre",{"className":288,"code":289,"language":290,"meta":291,"style":291},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,293,294],{"__ignoreMap":291},[295,296,299,303,307,310],"span",{"class":297,"line":298},"line",1,[295,300,302],{"class":301},"sMTad","pip",[295,304,306],{"class":305},"srMev"," install",[295,308,309],{"class":305}," pandas",[295,311,312],{"class":305}," openpyxl\n",[10,314,315,318,319,322],{},[14,316,317],{},"margins.xlsx"," has eight products. Product B is a real percentage cell (0.125 formatted as ",[14,320,321],{},"0.0%","); the others are typed text, a typed number, or blank.",[281,324,326],{"id":325},"step-1-see-what-pandas-reads","Step 1 — See what pandas reads",[286,328,332],{"className":329,"code":330,"language":331,"meta":291,"style":291},"language-python shiki shiki-themes github-light github-dark-high-contrast","import re\nimport pandas as pd\n\nmargins = pd.read_excel(\"margins.xlsx\")\nprint(margins)\nprint(pd.to_numeric(margins[\"margin\"], errors=\"coerce\").tolist())\n","python",[14,333,334,344,358,365,383,393],{"__ignoreMap":291},[295,335,336,340],{"class":297,"line":298},[295,337,339],{"class":338},"s-kum","import",[295,341,343],{"class":342},"skGVy"," re\n",[295,345,347,349,352,355],{"class":297,"line":346},2,[295,348,339],{"class":338},[295,350,351],{"class":342}," pandas ",[295,353,354],{"class":338},"as",[295,356,357],{"class":342}," pd\n",[295,359,361],{"class":297,"line":360},3,[295,362,364],{"emptyLinePlaceholder":363},true,"\n",[295,366,368,371,374,377,380],{"class":297,"line":367},4,[295,369,370],{"class":342},"margins ",[295,372,373],{"class":338},"=",[295,375,376],{"class":342}," pd.read_excel(",[295,378,379],{"class":305},"\"margins.xlsx\"",[295,381,382],{"class":342},")\n",[295,384,386,390],{"class":297,"line":385},5,[295,387,389],{"class":388},"sP0c6","print",[295,391,392],{"class":342},"(margins)\n",[295,394,396,398,401,404,407,411,413,416],{"class":297,"line":395},6,[295,397,389],{"class":388},[295,399,400],{"class":342},"(pd.to_numeric(margins[",[295,402,403],{"class":305},"\"margin\"",[295,405,406],{"class":342},"], ",[295,408,410],{"class":409},"sa561","errors",[295,412,373],{"class":338},[295,414,415],{"class":305},"\"coerce\"",[295,417,418],{"class":342},").tolist())\n",[10,420,421,422,425,426,428,429,432,433,435,436,438,439,442,443,446],{},"The formatted cell arrives as ",[14,423,424],{},"0.125"," — pandas reads the stored value, not the display — while product A's text ",[14,427,142],{}," arrives as a string. ",[14,430,431],{},"to_numeric"," keeps ",[14,434,424],{},", ",[14,437,16],{}," and ",[14,440,441],{},"0.4",", and turns the four text values with symbols into ",[14,444,445],{},"NaN",". Averaging that result would mix a fraction (0.125), a percentage number (12.5) and a fraction again (0.4), and ignore half the rows.",[281,448,450],{"id":449},"step-2-decide-on-the-target-fractions","Step 2 — Decide on the target: fractions",[10,452,453,454,456,457,459],{},"Before converting anything, choose one representation. Use fractions — 12.5% is ",[14,455,424],{},". That is what Excel stores in a percentage cell, what formulas expect when they multiply by a rate, and what a ",[14,458,321],{}," number format displays correctly. Everything the parser produces will be a fraction, and the display will be handled by formatting at the end.",[281,461,463],{"id":462},"step-3-parse-every-form-into-a-fraction","Step 3 — Parse every form into a fraction",[10,465,466,467,469,470,473],{},"Handle each marker explicitly: a ",[14,468,20],{}," sign means divide by 100, ",[14,471,472],{},"bp"," means divide by 10,000, a decimal comma becomes a point, and a number that is already numeric came from a cell and is left as it is. Anything else is reported as a question rather than guessed:",[286,475,477],{"className":329,"code":476,"language":331,"meta":291,"style":291},"def parse_rate(value):\n    if pd.isna(value):\n        return None, \"blank\"\n    if isinstance(value, (int, float)):\n        return float(value), \"numeric cell\"\n    s = str(value).strip().lower().replace(\" \", \"\")\n    m = re.fullmatch(r\"([+-]?\\d+(?:[.,]\\d+)?)(%|bps?)?\", s)\n    if not m:\n        return None, \"unparseable\"\n    number = float(m.group(1).replace(\",\", \".\"))\n    unit = m.group(2)\n    if unit == \"%\":\n        return number \u002F 100, \"percent text\"\n    if unit in (\"bp\", \"bps\"):\n        return number \u002F 10_000, \"basis points\"\n    return number, \"bare text number\"\n\nparsed = margins[\"margin\"].map(parse_rate)\nmargins[\"rate\"] = parsed.str[0].astype(\"Float64\")\nmargins[\"how\"] = parsed.str[1]\nprint(margins[[\"product\", \"margin\", \"rate\", \"how\"]])\n",[14,478,479,491,499,512,533,546,569,643,654,666,695,710,727,746,770,787,799,804,820,847,866],{"__ignoreMap":291},[295,480,481,484,488],{"class":297,"line":298},[295,482,483],{"class":338},"def",[295,485,487],{"class":486},"s_Opv"," parse_rate",[295,489,490],{"class":342},"(value):\n",[295,492,493,496],{"class":297,"line":346},[295,494,495],{"class":338},"    if",[295,497,498],{"class":342}," pd.isna(value):\n",[295,500,501,504,507,509],{"class":297,"line":360},[295,502,503],{"class":338},"        return",[295,505,506],{"class":388}," None",[295,508,435],{"class":342},[295,510,511],{"class":305},"\"blank\"\n",[295,513,514,516,519,522,525,527,530],{"class":297,"line":367},[295,515,495],{"class":338},[295,517,518],{"class":388}," isinstance",[295,520,521],{"class":342},"(value, (",[295,523,524],{"class":388},"int",[295,526,435],{"class":342},[295,528,529],{"class":388},"float",[295,531,532],{"class":342},")):\n",[295,534,535,537,540,543],{"class":297,"line":385},[295,536,503],{"class":338},[295,538,539],{"class":388}," float",[295,541,542],{"class":342},"(value), ",[295,544,545],{"class":305},"\"numeric cell\"\n",[295,547,548,551,553,556,559,562,564,567],{"class":297,"line":395},[295,549,550],{"class":342},"    s ",[295,552,373],{"class":338},[295,554,555],{"class":388}," str",[295,557,558],{"class":342},"(value).strip().lower().replace(",[295,560,561],{"class":305},"\" \"",[295,563,435],{"class":342},[295,565,566],{"class":305},"\"\"",[295,568,382],{"class":342},[295,570,572,575,577,580,583,586,589,593,596,599,602,605,608,611,614,616,619,621,624,626,629,632,634,636,638,640],{"class":297,"line":571},7,[295,573,574],{"class":342},"    m ",[295,576,373],{"class":338},[295,578,579],{"class":342}," re.fullmatch(",[295,581,582],{"class":338},"r",[295,584,585],{"class":305},"\"",[295,587,588],{"class":388},"([",[295,590,592],{"class":591},"sSjpA","+-",[295,594,595],{"class":388},"]",[295,597,598],{"class":338},"?",[295,600,601],{"class":388},"\\d",[295,603,604],{"class":338},"+",[295,606,607],{"class":388},"(?:[",[295,609,610],{"class":591},".,",[295,612,613],{"class":388},"]\\d",[295,615,604],{"class":338},[295,617,618],{"class":388},")",[295,620,598],{"class":338},[295,622,623],{"class":388},")(",[295,625,20],{"class":305},[295,627,628],{"class":338},"|",[295,630,631],{"class":305},"bps",[295,633,598],{"class":338},[295,635,618],{"class":388},[295,637,598],{"class":338},[295,639,585],{"class":305},[295,641,642],{"class":342},", s)\n",[295,644,646,648,651],{"class":297,"line":645},8,[295,647,495],{"class":338},[295,649,650],{"class":338}," not",[295,652,653],{"class":342}," m:\n",[295,655,657,659,661,663],{"class":297,"line":656},9,[295,658,503],{"class":338},[295,660,506],{"class":388},[295,662,435],{"class":342},[295,664,665],{"class":305},"\"unparseable\"\n",[295,667,669,672,674,676,679,681,684,687,689,692],{"class":297,"line":668},10,[295,670,671],{"class":342},"    number ",[295,673,373],{"class":338},[295,675,539],{"class":388},[295,677,678],{"class":342},"(m.group(",[295,680,109],{"class":388},[295,682,683],{"class":342},").replace(",[295,685,686],{"class":305},"\",\"",[295,688,435],{"class":342},[295,690,691],{"class":305},"\".\"",[295,693,694],{"class":342},"))\n",[295,696,698,701,703,706,708],{"class":297,"line":697},11,[295,699,700],{"class":342},"    unit ",[295,702,373],{"class":338},[295,704,705],{"class":342}," m.group(",[295,707,132],{"class":388},[295,709,382],{"class":342},[295,711,713,715,718,721,724],{"class":297,"line":712},12,[295,714,495],{"class":338},[295,716,717],{"class":342}," unit ",[295,719,720],{"class":338},"==",[295,722,723],{"class":305}," \"%\"",[295,725,726],{"class":342},":\n",[295,728,730,732,735,738,741,743],{"class":297,"line":729},13,[295,731,503],{"class":338},[295,733,734],{"class":342}," number ",[295,736,737],{"class":338},"\u002F",[295,739,740],{"class":388}," 100",[295,742,435],{"class":342},[295,744,745],{"class":305},"\"percent text\"\n",[295,747,749,751,753,756,759,762,764,767],{"class":297,"line":748},14,[295,750,495],{"class":338},[295,752,717],{"class":342},[295,754,755],{"class":338},"in",[295,757,758],{"class":342}," (",[295,760,761],{"class":305},"\"bp\"",[295,763,435],{"class":342},[295,765,766],{"class":305},"\"bps\"",[295,768,769],{"class":342},"):\n",[295,771,773,775,777,779,782,784],{"class":297,"line":772},15,[295,774,503],{"class":338},[295,776,734],{"class":342},[295,778,737],{"class":338},[295,780,781],{"class":388}," 10_000",[295,783,435],{"class":342},[295,785,786],{"class":305},"\"basis points\"\n",[295,788,790,793,796],{"class":297,"line":789},16,[295,791,792],{"class":338},"    return",[295,794,795],{"class":342}," number, ",[295,797,798],{"class":305},"\"bare text number\"\n",[295,800,802],{"class":297,"line":801},17,[295,803,364],{"emptyLinePlaceholder":363},[295,805,807,810,812,815,817],{"class":297,"line":806},18,[295,808,809],{"class":342},"parsed ",[295,811,373],{"class":338},[295,813,814],{"class":342}," margins[",[295,816,403],{"class":305},[295,818,819],{"class":342},"].map(parse_rate)\n",[295,821,823,826,829,832,834,837,839,842,845],{"class":297,"line":822},19,[295,824,825],{"class":342},"margins[",[295,827,828],{"class":305},"\"rate\"",[295,830,831],{"class":342},"] ",[295,833,373],{"class":338},[295,835,836],{"class":342}," parsed.str[",[295,838,55],{"class":388},[295,840,841],{"class":342},"].astype(",[295,843,844],{"class":305},"\"Float64\"",[295,846,382],{"class":342},[295,848,850,852,855,857,859,861,863],{"class":297,"line":849},20,[295,851,825],{"class":342},[295,853,854],{"class":305},"\"how\"",[295,856,831],{"class":342},[295,858,373],{"class":338},[295,860,836],{"class":342},[295,862,109],{"class":388},[295,864,865],{"class":342},"]\n",[295,867,869,871,874,877,879,881,883,885,887,889],{"class":297,"line":868},21,[295,870,389],{"class":388},[295,872,873],{"class":342},"(margins[[",[295,875,876],{"class":305},"\"product\"",[295,878,435],{"class":342},[295,880,403],{"class":305},[295,882,435],{"class":342},[295,884,828],{"class":305},[295,886,435],{"class":342},[295,888,854],{"class":305},[295,890,891],{"class":342},"]])\n",[10,893,894,895,897,898,901,902,905,906,910],{},"Most rows now agree: A, B and C are all ",[14,896,424],{},", D is ",[14,899,900],{},"-0.03"," and E is ",[14,903,904],{},"0.0025",". Recording ",[907,908,909],"em",{},"how"," each value was read in its own column is cheap and makes the next step possible — deciding what to do about the rows a rule cannot settle.",[33,912,42,918,42,921,42,924,42,928,42,934,42,941,42,947,42,950,42,954,42,958,42,962,42,964,42,968,42,970,42,973,42,976,42,980,42,982,42,985,42,987,42,990,42,993,42,996,42,998,42,1001,42,1003,42,1006,42,1009,42,1012,42,1014,42,1017,42,1019,42,1022,42,1025,42,1028,42,1030,42,1033,42,1035,42,1038,42,1041,42,1045,42,1047,42,1051,42,1053],{"viewBox":913,"role":36,"ariaLabelledBy":914,"xmlns":40,"style":917},"4 0 752 276",[915,916],"pct-rules-t","pct-rules-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[44,919,920],{"id":915},"How each form becomes a fraction",[48,922,923],{"id":916},"A reference table. \"12.5%\" becomes 0.125 by dividing by 100. A percent-formatted cell is already 0.125 and is kept. \"12,5 %\" becomes 0.125 after replacing the decimal comma. \"-3%\" becomes -0.03. \"25 bp\" becomes 0.0025 by dividing by 10,000. A bare 12.5 or \"0.4\" cannot be decided by rule and is flagged for review.",[52,925],{"x":179,"y":55,"width":926,"height":927,"fill":58},"752","276",[52,929],{"x":930,"y":931,"width":932,"height":933,"rx":55,"fill":58,"stroke":73,"style":74},"20.0","16.0","168.9","40.0",[52,935],{"x":936,"y":931,"width":937,"height":933,"rx":55,"fill":938,"stroke":939,"style":940},"188.9","275.6","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[60,942,946],{"x":943,"y":944,"style":945},"326.7","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Becomes",[52,948],{"x":949,"y":931,"width":937,"height":933,"rx":55,"fill":938,"stroke":939,"style":940},"464.4",[60,951,953],{"x":952,"y":944,"style":945},"602.2","Rule",[52,955],{"x":930,"y":956,"width":932,"height":957,"rx":55,"fill":72,"stroke":73,"style":74},"56.0","34.0",[60,959,142],{"x":70,"y":960,"style":961},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[52,963],{"x":936,"y":956,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,965,424],{"x":943,"y":966,"style":967},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[52,969],{"x":949,"y":956,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,971,972],{"x":952,"y":966,"style":967},"divide by 100",[52,974],{"x":930,"y":975,"width":932,"height":957,"rx":55,"fill":72,"stroke":73,"style":74},"90.0",[60,977,979],{"x":70,"y":978,"style":961},"112.0","0.125 formatted 0.0%",[52,981],{"x":936,"y":975,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,983,424],{"x":943,"y":984,"style":967},"111.0",[52,986],{"x":949,"y":975,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,988,989],{"x":952,"y":984,"style":967},"already a fraction",[52,991],{"x":930,"y":992,"width":932,"height":957,"rx":55,"fill":72,"stroke":73,"style":74},"124.0",[60,994,188],{"x":70,"y":995,"style":961},"146.0",[52,997],{"x":936,"y":992,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,999,424],{"x":943,"y":1000,"style":967},"145.0",[52,1002],{"x":949,"y":992,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,1004,1005],{"x":952,"y":1000,"style":967},"comma to point, \u002F 100",[52,1007],{"x":930,"y":1008,"width":932,"height":957,"rx":55,"fill":72,"stroke":73,"style":74},"158.0",[60,1010,209],{"x":70,"y":1011,"style":961},"180.0",[52,1013],{"x":936,"y":1008,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,1015,900],{"x":943,"y":1016,"style":967},"179.0",[52,1018],{"x":949,"y":1008,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,1020,1021],{"x":952,"y":1016,"style":967},"sign kept, \u002F 100",[52,1023],{"x":930,"y":1024,"width":932,"height":957,"rx":55,"fill":72,"stroke":73,"style":74},"192.0",[60,1026,230],{"x":70,"y":1027,"style":961},"214.0",[52,1029],{"x":936,"y":1024,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,1031,904],{"x":943,"y":1032,"style":967},"213.0",[52,1034],{"x":949,"y":1024,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,1036,1037],{"x":952,"y":1032,"style":967},"divide by 10,000",[52,1039],{"x":930,"y":1040,"width":932,"height":957,"rx":55,"fill":72,"stroke":73,"style":74},"226.0",[60,1042,1044],{"x":70,"y":1043,"style":961},"248.0","12.5 or \"0.4\" (no unit)",[52,1046],{"x":936,"y":1040,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,1048,1050],{"x":943,"y":1049,"style":967},"247.0","flag for review",[52,1052],{"x":949,"y":1040,"width":937,"height":957,"rx":55,"fill":58,"stroke":73,"style":74},[60,1054,1055],{"x":952,"y":1049,"style":967},"ambiguous",[281,1057,1059],{"id":1058},"step-4-flag-the-ambiguous-values-instead-of-guessing","Step 4 — Flag the ambiguous values instead of guessing",[10,1061,1062,1063,1065,1066,21,1068,1070,1071,1073],{},"Two rows carry no unit: the typed number ",[14,1064,16],{}," and the text ",[14,1067,275],{},[14,1069,441],{}," is plausibly a fraction — a 40% margin — and ",[14,1072,16],{}," is plausibly a percentage with the sign left off. But \"plausibly\" is doing a lot of work: a 1250% markup exists, and so does a 0.4% fee. List these rows for a person to confirm, with a suggested value, rather than dividing silently:",[286,1075,1077],{"className":329,"code":1076,"language":331,"meta":291,"style":291},"unitless = margins[\"how\"].isin([\"numeric cell\", \"bare text number\"]) & (margins[\"source\"] != \"formatted\")\nsuspect = unitless & (margins[\"rate\"].abs() > 1)\nmargins.loc[suspect, \"suggested\"] = margins.loc[suspect, \"rate\"] \u002F 100\nprint(margins.loc[unitless, [\"product\", \"margin\", \"rate\", \"suggested\"]])\n",[14,1078,1079,1123,1150,1174],{"__ignoreMap":291},[295,1080,1081,1084,1086,1088,1090,1093,1096,1098,1101,1104,1107,1110,1113,1115,1118,1121],{"class":297,"line":298},[295,1082,1083],{"class":342},"unitless ",[295,1085,373],{"class":338},[295,1087,814],{"class":342},[295,1089,854],{"class":305},[295,1091,1092],{"class":342},"].isin([",[295,1094,1095],{"class":305},"\"numeric cell\"",[295,1097,435],{"class":342},[295,1099,1100],{"class":305},"\"bare text number\"",[295,1102,1103],{"class":342},"]) ",[295,1105,1106],{"class":338},"&",[295,1108,1109],{"class":342}," (margins[",[295,1111,1112],{"class":305},"\"source\"",[295,1114,831],{"class":342},[295,1116,1117],{"class":338},"!=",[295,1119,1120],{"class":305}," \"formatted\"",[295,1122,382],{"class":342},[295,1124,1125,1128,1130,1133,1135,1137,1139,1142,1145,1148],{"class":297,"line":346},[295,1126,1127],{"class":342},"suspect ",[295,1129,373],{"class":338},[295,1131,1132],{"class":342}," unitless ",[295,1134,1106],{"class":338},[295,1136,1109],{"class":342},[295,1138,828],{"class":305},[295,1140,1141],{"class":342},"].abs() ",[295,1143,1144],{"class":338},">",[295,1146,1147],{"class":388}," 1",[295,1149,382],{"class":342},[295,1151,1152,1155,1158,1160,1162,1165,1167,1169,1171],{"class":297,"line":360},[295,1153,1154],{"class":342},"margins.loc[suspect, ",[295,1156,1157],{"class":305},"\"suggested\"",[295,1159,831],{"class":342},[295,1161,373],{"class":338},[295,1163,1164],{"class":342}," margins.loc[suspect, ",[295,1166,828],{"class":305},[295,1168,831],{"class":342},[295,1170,737],{"class":338},[295,1172,1173],{"class":388}," 100\n",[295,1175,1176,1178,1181,1183,1185,1187,1189,1191,1193,1195],{"class":297,"line":367},[295,1177,389],{"class":388},[295,1179,1180],{"class":342},"(margins.loc[unitless, [",[295,1182,876],{"class":305},[295,1184,435],{"class":342},[295,1186,403],{"class":305},[295,1188,435],{"class":342},[295,1190,828],{"class":305},[295,1192,435],{"class":342},[295,1194,1157],{"class":305},[295,1196,891],{"class":342},[10,1198,1199,1200,1204],{},"The rule here is deliberately narrow: a unitless value above 1 in a margin column is suggested as a percentage, and a unitless value at or below 1 is left as a fraction. Both are shown, so the reviewer sees the decision. When a source is known — \"the Paris office always types percentages without the sign\" — write that rule for that source, and stop flagging its rows. ",[27,1201,1203],{"href":1202},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-data-with-pandera-schemas\u002F","Validate Excel Data with pandera Schemas"," shows how to encode such a range rule so a whole column fails loudly when it is broken.",[281,1206,1208],{"id":1207},"step-5-apply-the-decision-and-check-the-range","Step 5 — Apply the decision and check the range",[10,1210,1211],{},"Once the ambiguous rows are settled — here, accepting the suggestion for product F — take the final value and check that every rate lies in a sensible range for the column:",[286,1213,1215],{"className":329,"code":1214,"language":331,"meta":291,"style":291},"margins[\"rate_final\"] = margins[\"suggested\"].fillna(margins[\"rate\"])\nout_of_range = margins[\"rate_final\"].notna() & ~margins[\"rate_final\"].between(-1, 1)\nprint(margins[[\"product\", \"rate_final\"]])\nprint(\"out of range:\", margins.loc[out_of_range, \"product\"].tolist())\nprint(\"average margin:\", round(margins[\"rate_final\"].mean(), 4))\n",[14,1216,1217,1240,1277,1291,1309],{"__ignoreMap":291},[295,1218,1219,1221,1224,1226,1228,1230,1232,1235,1237],{"class":297,"line":298},[295,1220,825],{"class":342},[295,1222,1223],{"class":305},"\"rate_final\"",[295,1225,831],{"class":342},[295,1227,373],{"class":338},[295,1229,814],{"class":342},[295,1231,1157],{"class":305},[295,1233,1234],{"class":342},"].fillna(margins[",[295,1236,828],{"class":305},[295,1238,1239],{"class":342},"])\n",[295,1241,1242,1245,1247,1249,1251,1254,1256,1259,1261,1263,1266,1269,1271,1273,1275],{"class":297,"line":346},[295,1243,1244],{"class":342},"out_of_range ",[295,1246,373],{"class":338},[295,1248,814],{"class":342},[295,1250,1223],{"class":305},[295,1252,1253],{"class":342},"].notna() ",[295,1255,1106],{"class":338},[295,1257,1258],{"class":338}," ~",[295,1260,825],{"class":342},[295,1262,1223],{"class":305},[295,1264,1265],{"class":342},"].between(",[295,1267,1268],{"class":338},"-",[295,1270,109],{"class":388},[295,1272,435],{"class":342},[295,1274,109],{"class":388},[295,1276,382],{"class":342},[295,1278,1279,1281,1283,1285,1287,1289],{"class":297,"line":360},[295,1280,389],{"class":388},[295,1282,873],{"class":342},[295,1284,876],{"class":305},[295,1286,435],{"class":342},[295,1288,1223],{"class":305},[295,1290,891],{"class":342},[295,1292,1293,1295,1298,1301,1304,1306],{"class":297,"line":367},[295,1294,389],{"class":388},[295,1296,1297],{"class":342},"(",[295,1299,1300],{"class":305},"\"out of range:\"",[295,1302,1303],{"class":342},", margins.loc[out_of_range, ",[295,1305,876],{"class":305},[295,1307,1308],{"class":342},"].tolist())\n",[295,1310,1311,1313,1315,1318,1320,1323,1326,1328,1331,1333],{"class":297,"line":385},[295,1312,389],{"class":388},[295,1314,1297],{"class":342},[295,1316,1317],{"class":305},"\"average margin:\"",[295,1319,435],{"class":342},[295,1321,1322],{"class":388},"round",[295,1324,1325],{"class":342},"(margins[",[295,1327,1223],{"class":305},[295,1329,1330],{"class":342},"].mean(), ",[295,1332,179],{"class":388},[295,1334,694],{"class":342},[10,1336,1337],{},"A margin between -100% and +100% is the range check for this column; a growth rate would allow more. The average now uses seven comparable values and ignores the blank, instead of mixing fractions with percentage numbers.",[33,1339,42,1344,42,1347,42,1350,42,1353,42,1359,42,1370,42,1376,42,1380,42,1385,42,1390,42,1393,42,1400,42,1406,42,1410,42,1414,42,1418,42,1421,42,1426,42,1433,42,1438,42,1441,42,1445,42,1448,42,1451,42,1458,42,1463,42,1466],{"viewBox":1340,"role":36,"ariaLabelledBy":1341,"xmlns":40,"style":917},"4 0 752 142",[1342,1343],"pct-flow-t","pct-flow-d",[44,1345,1346],{"id":1342},"From a mixed column to trustworthy rates",[48,1348,1349],{"id":1343},"Four steps. Parse each value by its marker: percent sign, basis points, or a numeric cell. Record how each was read. Flag unitless values for review with a suggested reading. Apply the reviewed decision, check the range and write fractions with a percent format.",[52,1351],{"x":179,"y":55,"width":926,"height":1352,"fill":58},"142",[52,1354],{"x":930,"y":931,"width":1355,"height":1356,"rx":1357,"fill":938,"stroke":939,"style":1358},"153.0","110.5","12","stroke-width:2px",[1360,1361,1363],"g",{"transform":1362},"translate(84.5,37.8) scale(1.000)",[1364,1365],"path",{"d":1366,"fill":1367,"stroke":1368,"style":1369},"M3 5h18l-7 8v6l-4 2v-8z","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[60,1371,1375],{"x":1372,"y":1373,"style":1374},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Parse by marker",[60,1377,1379],{"x":1372,"y":1378,"style":967},"103.8","%, bp, numeric cell",[297,1381],{"x1":1382,"y1":1383,"x2":1384,"y2":1383,"stroke":939,"style":1358},"178.0","71.2","200.0",[1386,1387],"polygon",{"points":1388,"fill":1389},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[52,1391],{"x":1392,"y":931,"width":1355,"height":1356,"rx":1357,"fill":72,"stroke":73,"style":1358},"209.0",[1360,1394,1396],{"transform":1395},"translate(273.5,29.0) scale(1.000)",[1364,1397],{"d":1398,"fill":1367,"stroke":1399,"style":1369},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--text,#172033)",[60,1401,1405],{"x":1402,"y":1403,"style":1404},"285.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Record how",[60,1407,1409],{"x":1402,"y":1408,"style":967},"95.0","one column per",[60,1411,1413],{"x":1402,"y":1412,"style":967},"112.5","decision",[297,1415],{"x1":1416,"y1":1383,"x2":1417,"y2":1383,"stroke":939,"style":1358},"367.0","389.0",[1386,1419],{"points":1420,"fill":1389},"393.0,71.2 384.0,76.2 384.0,66.2",[52,1422],{"x":1423,"y":931,"width":1355,"height":1356,"rx":1357,"fill":1424,"stroke":1425,"style":1358},"398.0","#fdefd8","var(--gold,#b4740a)",[1360,1427,1429],{"transform":1428},"translate(462.5,37.8) scale(1.000)",[1364,1430],{"d":1431,"fill":1367,"stroke":1432,"style":1369},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[60,1434,1437],{"x":1435,"y":1373,"style":1436},"474.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Flag unitless",[60,1439,1440],{"x":1435,"y":1378,"style":967},"suggest, do not guess",[297,1442],{"x1":1443,"y1":1383,"x2":1444,"y2":1383,"stroke":939,"style":1358},"556.0","578.0",[1386,1446],{"points":1447,"fill":1389},"582.0,71.2 573.0,76.2 573.0,66.2",[52,1449],{"x":1450,"y":931,"width":1355,"height":1356,"rx":1357,"fill":161,"stroke":162,"style":1358},"587.0",[1360,1452,1454],{"transform":1453},"translate(651.5,29.0) scale(1.000)",[1364,1455],{"d":1456,"fill":1367,"stroke":1457,"style":1369},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[60,1459,1462],{"x":1460,"y":1403,"style":1461},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Check and write",[60,1464,1465],{"x":1460,"y":1408,"style":967},"range check, 0.0%",[60,1467,1468],{"x":1460,"y":1412,"style":967},"format",[281,1470,1472],{"id":1471},"step-6-write-fractions-with-a-percent-format","Step 6 — Write fractions with a percent format",[10,1474,1475],{},"Write the fractions as numbers and let the cell format show them as percentages, so the workbook can calculate with them and readers see familiar figures:",[286,1477,1479],{"className":329,"code":1478,"language":331,"meta":291,"style":291},"from openpyxl import load_workbook\n\nout = margins[[\"product\", \"rate_final\", \"margin\", \"how\"]].rename(columns={\"rate_final\": \"margin_pct\", \"margin\": \"original\"})\nout.to_excel(\"margins_clean.xlsx\", index=False)\nwb = load_workbook(\"margins_clean.xlsx\")\nws = wb.active\nfor (cell,) in ws.iter_rows(min_row=2, min_col=2, max_col=2):\n    cell.number_format = \"0.0%\"\nwb.save(\"margins_clean.xlsx\")\nprint(pd.read_excel(\"margins_clean.xlsx\")[\"margin_pct\"].round(4).tolist())\n",[14,1480,1481,1494,1498,1553,1573,1587,1597,1637,1647,1656],{"__ignoreMap":291},[295,1482,1483,1486,1489,1491],{"class":297,"line":298},[295,1484,1485],{"class":338},"from",[295,1487,1488],{"class":342}," openpyxl ",[295,1490,339],{"class":338},[295,1492,1493],{"class":342}," load_workbook\n",[295,1495,1496],{"class":297,"line":346},[295,1497,364],{"emptyLinePlaceholder":363},[295,1499,1500,1503,1505,1508,1510,1512,1514,1516,1518,1520,1522,1525,1528,1530,1533,1535,1538,1541,1543,1545,1547,1550],{"class":297,"line":360},[295,1501,1502],{"class":342},"out ",[295,1504,373],{"class":338},[295,1506,1507],{"class":342}," margins[[",[295,1509,876],{"class":305},[295,1511,435],{"class":342},[295,1513,1223],{"class":305},[295,1515,435],{"class":342},[295,1517,403],{"class":305},[295,1519,435],{"class":342},[295,1521,854],{"class":305},[295,1523,1524],{"class":342},"]].rename(",[295,1526,1527],{"class":409},"columns",[295,1529,373],{"class":338},[295,1531,1532],{"class":342},"{",[295,1534,1223],{"class":305},[295,1536,1537],{"class":342},": ",[295,1539,1540],{"class":305},"\"margin_pct\"",[295,1542,435],{"class":342},[295,1544,403],{"class":305},[295,1546,1537],{"class":342},[295,1548,1549],{"class":305},"\"original\"",[295,1551,1552],{"class":342},"})\n",[295,1554,1555,1558,1561,1563,1566,1568,1571],{"class":297,"line":367},[295,1556,1557],{"class":342},"out.to_excel(",[295,1559,1560],{"class":305},"\"margins_clean.xlsx\"",[295,1562,435],{"class":342},[295,1564,1565],{"class":409},"index",[295,1567,373],{"class":338},[295,1569,1570],{"class":388},"False",[295,1572,382],{"class":342},[295,1574,1575,1578,1580,1583,1585],{"class":297,"line":385},[295,1576,1577],{"class":342},"wb ",[295,1579,373],{"class":338},[295,1581,1582],{"class":342}," load_workbook(",[295,1584,1560],{"class":305},[295,1586,382],{"class":342},[295,1588,1589,1592,1594],{"class":297,"line":395},[295,1590,1591],{"class":342},"ws ",[295,1593,373],{"class":338},[295,1595,1596],{"class":342}," wb.active\n",[295,1598,1599,1602,1605,1607,1610,1613,1615,1617,1619,1622,1624,1626,1628,1631,1633,1635],{"class":297,"line":571},[295,1600,1601],{"class":338},"for",[295,1603,1604],{"class":342}," (cell,) ",[295,1606,755],{"class":338},[295,1608,1609],{"class":342}," ws.iter_rows(",[295,1611,1612],{"class":409},"min_row",[295,1614,373],{"class":338},[295,1616,132],{"class":388},[295,1618,435],{"class":342},[295,1620,1621],{"class":409},"min_col",[295,1623,373],{"class":338},[295,1625,132],{"class":388},[295,1627,435],{"class":342},[295,1629,1630],{"class":409},"max_col",[295,1632,373],{"class":338},[295,1634,132],{"class":388},[295,1636,769],{"class":342},[295,1638,1639,1642,1644],{"class":297,"line":645},[295,1640,1641],{"class":342},"    cell.number_format ",[295,1643,373],{"class":338},[295,1645,1646],{"class":305}," \"0.0%\"\n",[295,1648,1649,1652,1654],{"class":297,"line":656},[295,1650,1651],{"class":342},"wb.save(",[295,1653,1560],{"class":305},[295,1655,382],{"class":342},[295,1657,1658,1660,1663,1665,1668,1670,1673,1675],{"class":297,"line":668},[295,1659,389],{"class":388},[295,1661,1662],{"class":342},"(pd.read_excel(",[295,1664,1560],{"class":305},[295,1666,1667],{"class":342},")[",[295,1669,1540],{"class":305},[295,1671,1672],{"class":342},"].round(",[295,1674,179],{"class":388},[295,1676,418],{"class":342},[10,1678,1679,1680,435,1683,438,1686,1689,1690,1693,1694,1698],{},"Reading the file back returns the same fractions, and in Excel the column shows ",[14,1681,1682],{},"12.5%",[14,1684,1685],{},"-3.0%",[14,1687,1688],{},"0.3%"," (for 25 basis points, rounded to one decimal place; use ",[14,1691,1692],{},"0.00%"," if basis-point precision matters). ",[27,1695,1697],{"href":1696},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002Fformat-numbers-as-percentages-in-excel-with-python\u002F","Format Numbers as Percentages in Excel with Python"," covers the format codes.",[281,1700,1702],{"id":1701},"report-changes-in-rates-carefully","Report changes in rates carefully",[10,1704,1705,1706,1709,1710,1713],{},"Once rates are clean fractions, the next report usually compares them — this quarter's margin against last quarter's — and there are two different \"changes\" that are easy to confuse. A margin moving from 10% to 12% has risen by 2 ",[907,1707,1708],{},"percentage points",", and by 20% ",[907,1711,1712],{},"relative to where it was",". Label the columns so readers know which one they are looking at:",[286,1715,1717],{"className":329,"code":1716,"language":331,"meta":291,"style":291},"q = pd.DataFrame({\"product\": [\"A\", \"B\"], \"last_q\": [0.10, 0.125], \"this_q\": [0.12, 0.10]})\nq[\"change_pts\"] = (q[\"this_q\"] - q[\"last_q\"]) * 100          # percentage points\nq[\"change_rel\"] = q[\"this_q\"] \u002F q[\"last_q\"] - 1              # relative change, a fraction\nprint(q.round(3))\n",[14,1718,1719,1773,1810,1842],{"__ignoreMap":291},[295,1720,1721,1724,1726,1729,1731,1734,1737,1739,1742,1744,1747,1749,1752,1754,1756,1758,1761,1763,1766,1768,1770],{"class":297,"line":298},[295,1722,1723],{"class":342},"q ",[295,1725,373],{"class":338},[295,1727,1728],{"class":342}," pd.DataFrame({",[295,1730,876],{"class":305},[295,1732,1733],{"class":342},": [",[295,1735,1736],{"class":305},"\"A\"",[295,1738,435],{"class":342},[295,1740,1741],{"class":305},"\"B\"",[295,1743,406],{"class":342},[295,1745,1746],{"class":305},"\"last_q\"",[295,1748,1733],{"class":342},[295,1750,1751],{"class":388},"0.10",[295,1753,435],{"class":342},[295,1755,424],{"class":388},[295,1757,406],{"class":342},[295,1759,1760],{"class":305},"\"this_q\"",[295,1762,1733],{"class":342},[295,1764,1765],{"class":388},"0.12",[295,1767,435],{"class":342},[295,1769,1751],{"class":388},[295,1771,1772],{"class":342},"]})\n",[295,1774,1775,1778,1781,1783,1785,1788,1790,1792,1794,1797,1799,1801,1804,1806],{"class":297,"line":346},[295,1776,1777],{"class":342},"q[",[295,1779,1780],{"class":305},"\"change_pts\"",[295,1782,831],{"class":342},[295,1784,373],{"class":338},[295,1786,1787],{"class":342}," (q[",[295,1789,1760],{"class":305},[295,1791,831],{"class":342},[295,1793,1268],{"class":338},[295,1795,1796],{"class":342}," q[",[295,1798,1746],{"class":305},[295,1800,1103],{"class":342},[295,1802,1803],{"class":338},"*",[295,1805,740],{"class":388},[295,1807,1809],{"class":1808},"s-wDw","          # percentage points\n",[295,1811,1812,1814,1817,1819,1821,1823,1825,1827,1829,1831,1833,1835,1837,1839],{"class":297,"line":360},[295,1813,1777],{"class":342},[295,1815,1816],{"class":305},"\"change_rel\"",[295,1818,831],{"class":342},[295,1820,373],{"class":338},[295,1822,1796],{"class":342},[295,1824,1760],{"class":305},[295,1826,831],{"class":342},[295,1828,737],{"class":338},[295,1830,1796],{"class":342},[295,1832,1746],{"class":305},[295,1834,831],{"class":342},[295,1836,1268],{"class":338},[295,1838,1147],{"class":388},[295,1840,1841],{"class":1808},"              # relative change, a fraction\n",[295,1843,1844,1846,1849,1851],{"class":297,"line":367},[295,1845,389],{"class":388},[295,1847,1848],{"class":342},"(q.round(",[295,1850,154],{"class":388},[295,1852,694],{"class":342},[10,1854,1855,1856,1859,1860,438,1863,1866,1867,1870],{},"Product A's margin is up 2 points, or 20%; product B's is down 2.5 points, or 20% the other way. Write ",[14,1857,1858],{},"change_pts"," with a number format like ",[14,1861,1862],{},"+0.0\" pts\";-0.0\" pts\"",[14,1864,1865],{},"change_rel"," with ",[14,1868,1869],{},"+0.0%;-0.0%",", so the two never look alike on the page. Mixing them — calling a two-point rise \"up 2%\" — is one of the most common errors in management reports, and one of the hardest to spot once it has been typed into a slide.",[281,1872,1874],{"id":1873},"common-pitfalls","Common pitfalls",[1876,1877,1878,1886,1900,1906,1912],"ul",{},[1879,1880,1881,1885],"li",{},[1882,1883,1884],"strong",{},"Dividing everything by 100."," Percent-formatted cells are already fractions; dividing them again gives 0.00125.",[1879,1887,1888,1894,1895,1897,1898,1893],{},[1882,1889,1890,1891,1893],{},"Coercing text with ",[14,1892,431],{},"."," Every value with a ",[14,1896,20],{}," becomes ",[14,1899,445],{},[1879,1901,1902,1905],{},[1882,1903,1904],{},"Guessing unitless values."," Flag them; a silent guess is right until the day it is not.",[1879,1907,1908,1911],{},[1882,1909,1910],{},"Averaging rates without weights."," The mean of seven margins treats a tiny product like a large one; for a company-wide margin, weight by revenue.",[1879,1913,1914,1917,1918,1920,1921,1923],{},[1882,1915,1916],{},"Formatting instead of converting."," A ",[14,1919,321],{}," format on the text ",[14,1922,142],{}," does nothing — it is still text.",[281,1925,1927],{"id":1926},"performance-notes","Performance notes",[10,1929,1930,1931,1933,1934,1937,1938,1893],{},"The parser runs once per value through Python, which is fine for the size of typical rate tables. For large columns dominated by one form — say, text with a ",[14,1932,20],{}," sign — convert that majority with vectorised operations (",[14,1935,1936],{},"s.str.rstrip(\"%\").astype(float) \u002F 100",") and send only the remaining rows through ",[14,1939,1940],{},"parse_rate",[281,1942,1944],{"id":1943},"conclusion","Conclusion",[10,1946,1947,1948,1950,1951,1953],{},"Store rates as fractions. Parse each value by its marker — divide ",[14,1949,20],{}," text by 100 and basis points by 10,000, replace decimal commas, keep numeric cells as they are because Excel already stores percentages as fractions — and record how each was read. Flag unitless values with a suggested reading instead of guessing, apply a range check, and write the fractions back with a ",[14,1952,321],{}," number format.",[281,1955,1957],{"id":1956},"frequently-asked-questions","Frequently asked questions",[10,1959,1960,1963],{},[1882,1961,1962],{},"Why does read_excel give 0.125 for a cell that shows 12.5%?","\nExcel stores a percentage as a fraction and only displays it multiplied by 100. The cell holds 0.125 with a percent number format, and pandas reads the stored value. That is the right number to calculate with.",[10,1965,1966,1969],{},[1882,1967,1968],{},"Should percentages be stored as 12.5 or 0.125?","\nAs fractions, 0.125. That is what Excel stores, what formulas expect (a price times 0.125), and what a percent number format displays correctly as 12.5%.",[10,1971,1972,1975],{},[1882,1973,1974],{},"What about a bare number like 12.5 in a percentage column?","\nIt is ambiguous: someone may have meant 12.5% or 1250%. A value above 1 in a margin column almost always means the percent sign was left off, but the rule belongs to the data, so flag such values and decide per source rather than silently dividing.",[10,1977,1978,1981],{},[1882,1979,1980],{},"What is a basis point?","\nOne hundredth of a percentage point, so 25 bp is 0.25% or 0.0025 as a fraction. Interest rates and fees are often quoted that way.",[281,1983,1985],{"id":1984},"related","Related",[1876,1987,1988,1994,2001,2008],{},[1879,1989,1990,1991,1993],{},"Up one level: ",[27,1992,30],{"href":29}," — the full cleaning workflow.",[1879,1995,1996,2000],{},[27,1997,1999],{"href":1998},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-currency-strings-to-numbers-with-pandas\u002F","Convert Currency Strings Like \"$1,234.56\" and \"(500)\" to Numbers with pandas"," — the same approach for amounts.",[1879,2002,2003,2007],{},[27,2004,2006],{"href":2005},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002Fread-cell-values-as-displayed-in-excel\u002F","Read Cell Values as Displayed in Excel with Python"," — when you need \"12.5%\" as text instead.",[1879,2009,2010,2014],{},[27,2011,2013],{"href":2012},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas\u002F","Check Excel Data Types with pandas"," — spotting mixed-type columns early.",[2016,2017,2018],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":291,"searchDepth":346,"depth":346,"links":2020},[2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033],{"id":283,"depth":346,"text":284},{"id":325,"depth":346,"text":326},{"id":449,"depth":346,"text":450},{"id":462,"depth":346,"text":463},{"id":1058,"depth":346,"text":1059},{"id":1207,"depth":346,"text":1208},{"id":1471,"depth":346,"text":1472},{"id":1701,"depth":346,"text":1702},{"id":1873,"depth":346,"text":1874},{"id":1926,"depth":346,"text":1927},{"id":1943,"depth":346,"text":1944},{"id":1956,"depth":346,"text":1957},{"id":1984,"depth":346,"text":1985},"2026-09-25","A rate column that mixes typed text like \"12.5%\" with real percentage cells that pandas reads as 0.125 cannot be averaged. Convert every form — \"12.5%\", \"12,5 %\", \"-3%\", \"25 bp\", fractions and bare numbers — to one consistent fraction, flag the ambiguous values, and write them back with a percent format.","md",[2038,2040,2042,2044],{"q":1962,"a":2039},"Excel stores a percentage as a fraction and only displays it multiplied by 100. The cell holds 0.125 with a percent number format, and pandas reads the stored value. That is the right number to calculate with.",{"q":1968,"a":2041},"As fractions, 0.125. That is what Excel stores, what formulas expect (a price times 0.125), and what a percent number format displays correctly as 12.5%.",{"q":1974,"a":2043},"It is ambiguous: someone may have meant 12.5% or 1250%. A value above 1 in a margin column almost always means the percent sign was left off, but the rule belongs to the data, so flag such values and decide per source rather than silently dividing.",{"q":1980,"a":2045},"One hundredth of a percentage point, so 25 bp is 0.25% or 0.0025 as a fraction. Interest rates and fees are often quoted that way.","convert percentage strings to numbers with pandas",null,{"breadcrumb":2049},[2050,2052,2055],{"name":2051,"item":737},"Home",{"name":2053,"item":2054},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2056,"item":29},"Cleaning Excel Data With Pandas: A Production Workflow","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas",{"title":2059,"description":2060},"Convert \"12.5%\" Text to Numbers in pandas (Mixed Percent Columns)","Parse percentage strings in pandas: strip the percent sign and divide by 100, handle decimal commas and basis points, reconcile text with percent-formatted cells read as fractions, and flag ambiguous bare numbers.","convert-percentage-strings-to-numbers-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas\u002Findex",[2064,2065,2066],"data-cleaning","pandas","number-formats","how-to","PSiPkSHkGmX8KYE3Ur32e4ZPTTOy81e7ECnJBrLHq04",[2070,2074],{"title":2071,"path":2072,"stem":2073,"children":-1},"Convert Excel Text Columns to Numbers with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-excel-text-columns-to-numbers-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-excel-text-columns-to-numbers-with-pandas\u002Findex",{"title":2075,"path":2076,"stem":2077,"children":-1},"Convert Trailing Minus and CR\u002FDR Amounts with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-trailing-minus-and-cr-dr-amounts-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-trailing-minus-and-cr-dr-amounts-with-pandas\u002Findex",[2079,2081],{"path":2080,"title":2056},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas",{"path":2082,"title":2053},"\u002Fadvanced-data-transformation-and-cleaning",[],[2085,2088,2091,2094,2097,2099],{"path":2086,"title":2087,"kind":2067},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python","Find Duplicate Rows in Excel with Python",{"path":2089,"title":2090,"kind":2067},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-duplicate-values-in-excel-with-openpyxl","Highlight Duplicate Values in Excel with openpyxl",{"path":2092,"title":2093,"kind":2067},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fclean-mixed-currency-amounts-into-amount-and-currency-columns","Clean Mixed-Currency Amounts into Amount and Currency Columns",{"path":2095,"title":2096,"kind":2067},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas","Coalesce Several Excel Columns into One with pandas",{"path":2098,"title":1999,"kind":2067},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-currency-strings-to-numbers-with-pandas",{"path":2072,"title":2071,"kind":2067},1790972080598]