[{"data":1,"prerenderedAt":2989},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas":2981},{"id":4,"title":5,"body":6,"dateModified":2950,"datePublished":2950,"description":2951,"extension":2952,"faq":2953,"meta":2966,"navigation":193,"path":2974,"seo":2975,"slug":2977,"stem":2978,"type":2979,"__hash__":2980},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Findex.md","Excel Formula Equivalents in pandas",{"type":7,"value":8,"toc":2933},"minimark",[9,19,149,152,157,160,488,502,506,631,634,846,993,997,1000,1083,1092,1096,1102,1175,1316,1331,1399,1406,1410,1413,1426,1433,1443,1532,1539,1543,1642,1645,1815,1823,1827,1830,1838,1842,1845,2009,2015,2018,2022,2025,2275,2283,2287,2290,2531,2538,2546,2550,2553,2556,2560,2563,2761,2771,2786,2790,2822,2826,2833,2839,2845,2851,2857,2863,2867,2929],[10,11,12,13,18],"p",{},"Most Python Excel work starts with a workbook that already computes the answer. Somebody has spent\nyears building SUMIFs, an INDEX\u002FMATCH pair and a column of nested IFs, and the automation project\nhas to produce the same numbers without them. That translation is mechanical once you see the\npattern behind it, and this section of\n",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002F","Advanced Data Transformation and Cleaning"," walks\nthrough it function by function.",[20,21,29,30,29,34,29,38,29,45,29,55,29,62,29,69,29,74,29,78,29,82,29,86,29,91,29,96,29,100,29,103,29,106,29,109,29,112,29,121,29,127,29,132,29,137,29,141,29,144],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 224","img",[25,26],"fx-model-t","fx-model-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"One cell at a time, or one column at once",[35,36,37],"desc",{"id":26},"An Excel formula is copied down and evaluated per row, re-scanning the data each time. The pandas equivalent computes the whole column in a single vectorised pass.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","224","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53,"style":54},"20","28","270.0","162","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[56,57,61],"text",{"x":58,"y":59,"style":60},"155.0","54","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Excel",[63,64],"line",{"x1":65,"y1":66,"x2":67,"y2":66,"stroke":53,"style":68},"36","64","274.0","stroke-width:1px",[56,70,73],{"x":58,"y":71,"style":72},"86","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","one formula per cell",[56,75,77],{"x":58,"y":76,"style":72},"109","re-scans per row",[56,79,81],{"x":58,"y":80,"style":72},"132","recalculates on change",[56,83,85],{"x":58,"y":84,"style":72},"155","logic lives in the sheet",[39,87],{"x":88,"y":48,"width":49,"height":50,"rx":51,"fill":89,"stroke":90,"style":54},"470.0","#d9f4f1","var(--teal,#0f9488)",[56,92,95],{"x":93,"y":59,"style":94},"605.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","pandas",[63,97],{"x1":98,"y1":66,"x2":99,"y2":66,"stroke":90,"style":68},"486.0","724.0",[56,101,102],{"x":93,"y":71,"style":72},"one pass per column",[56,104,105],{"x":93,"y":76,"style":72},"scans once",[56,107,108],{"x":93,"y":80,"style":72},"runs when you say",[56,110,111],{"x":93,"y":84,"style":72},"logic lives in code",[39,113],{"x":114,"y":115,"width":116,"height":117,"rx":118,"fill":119,"stroke":120},"316.0","90.0","128","38","19","#ebebfd","var(--brand,#5b5cf0)",[56,122,126],{"x":123,"y":124,"style":125},"380.0","114.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","translate",[63,128],{"x1":129,"y1":130,"x2":131,"y2":130,"stroke":120,"style":54},"295.0","109.0","309.0",[133,134],"polygon",{"points":135,"fill":136},"309.0,109.0 300.0,104.0 300.0,114.0","#5b5cf0",[63,138],{"x1":139,"y1":130,"x2":140,"y2":130,"stroke":120,"style":54},"449.0","463.0",[133,142],{"points":143,"fill":136},"463.0,109.0 454.0,104.0 454.0,114.0",[56,145,148],{"x":123,"y":146,"style":147},"210","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the answers match; the amount of work does not",[10,150,151],{},"The single idea that makes the translation easy: an Excel formula computes one cell at a time and\npandas computes a whole column at once. A SUMIF asks \"for this row's criteria, total the matching\nrows\"; the pandas version computes every group's total once and looks the answer up. The result is\nthe same and the amount of work is very different.",[153,154,156],"h2",{"id":155},"the-shape-of-every-translation","The shape of every translation",[10,158,159],{},"Excel's conditional functions bundle two operations that pandas keeps apart: choosing rows, and\ndoing something with them. Separating them is what makes the pandas version more capable rather than\nmerely different.",[161,162,167],"pre",{"className":163,"code":164,"language":165,"meta":166,"style":166},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nsales = pd.DataFrame({\n    \"Region\": [\"North\", \"South\", \"North\", \"West\", \"South\"],\n    \"Rep\": [\"Ana\", \"Ben\", \"Cara\", \"Dev\", \"Eve\"],\n    \"Product\": [\"Widget\", \"Gadget\", \"Widget\", \"Widget\", \"Gadget\"],\n    \"Revenue\": [12400.0, 9800.5, 15320.25, 7010.0, 4300.75],\n    \"Ordered\": pd.to_datetime([\"2026-01-04\", \"2026-01-19\", \"2026-02-02\", \"2026-02-27\", \"2026-03-08\"]),\n})\n\n# =SUMIF(A:A, \"North\", D:D)\nnorth_total = sales.loc[sales[\"Region\"] == \"North\", \"Revenue\"].sum()\n\n# =SUMIFS(D:D, A:A, \"North\", C:C, \"Widget\")\nboth = sales.loc[(sales[\"Region\"] == \"North\") & (sales[\"Product\"] == \"Widget\"), \"Revenue\"].sum()\nprint(north_total, both)\n","python","",[168,169,170,188,195,207,242,275,305,339,374,380,385,392,423,428,434,479],"code",{"__ignoreMap":166},[171,172,174,178,182,185],"span",{"class":63,"line":173},1,[171,175,177],{"class":176},"s-kum","import",[171,179,181],{"class":180},"skGVy"," pandas ",[171,183,184],{"class":176},"as",[171,186,187],{"class":180}," pd\n",[171,189,191],{"class":63,"line":190},2,[171,192,194],{"emptyLinePlaceholder":193},true,"\n",[171,196,198,201,204],{"class":63,"line":197},3,[171,199,200],{"class":180},"sales ",[171,202,203],{"class":176},"=",[171,205,206],{"class":180}," pd.DataFrame({\n",[171,208,210,214,217,220,223,226,228,230,232,235,237,239],{"class":63,"line":209},4,[171,211,213],{"class":212},"srMev","    \"Region\"",[171,215,216],{"class":180},": [",[171,218,219],{"class":212},"\"North\"",[171,221,222],{"class":180},", ",[171,224,225],{"class":212},"\"South\"",[171,227,222],{"class":180},[171,229,219],{"class":212},[171,231,222],{"class":180},[171,233,234],{"class":212},"\"West\"",[171,236,222],{"class":180},[171,238,225],{"class":212},[171,240,241],{"class":180},"],\n",[171,243,245,248,250,253,255,258,260,263,265,268,270,273],{"class":63,"line":244},5,[171,246,247],{"class":212},"    \"Rep\"",[171,249,216],{"class":180},[171,251,252],{"class":212},"\"Ana\"",[171,254,222],{"class":180},[171,256,257],{"class":212},"\"Ben\"",[171,259,222],{"class":180},[171,261,262],{"class":212},"\"Cara\"",[171,264,222],{"class":180},[171,266,267],{"class":212},"\"Dev\"",[171,269,222],{"class":180},[171,271,272],{"class":212},"\"Eve\"",[171,274,241],{"class":180},[171,276,278,281,283,286,288,291,293,295,297,299,301,303],{"class":63,"line":277},6,[171,279,280],{"class":212},"    \"Product\"",[171,282,216],{"class":180},[171,284,285],{"class":212},"\"Widget\"",[171,287,222],{"class":180},[171,289,290],{"class":212},"\"Gadget\"",[171,292,222],{"class":180},[171,294,285],{"class":212},[171,296,222],{"class":180},[171,298,285],{"class":212},[171,300,222],{"class":180},[171,302,290],{"class":212},[171,304,241],{"class":180},[171,306,308,311,313,317,319,322,324,327,329,332,334,337],{"class":63,"line":307},7,[171,309,310],{"class":212},"    \"Revenue\"",[171,312,216],{"class":180},[171,314,316],{"class":315},"sP0c6","12400.0",[171,318,222],{"class":180},[171,320,321],{"class":315},"9800.5",[171,323,222],{"class":180},[171,325,326],{"class":315},"15320.25",[171,328,222],{"class":180},[171,330,331],{"class":315},"7010.0",[171,333,222],{"class":180},[171,335,336],{"class":315},"4300.75",[171,338,241],{"class":180},[171,340,342,345,348,351,353,356,358,361,363,366,368,371],{"class":63,"line":341},8,[171,343,344],{"class":212},"    \"Ordered\"",[171,346,347],{"class":180},": pd.to_datetime([",[171,349,350],{"class":212},"\"2026-01-04\"",[171,352,222],{"class":180},[171,354,355],{"class":212},"\"2026-01-19\"",[171,357,222],{"class":180},[171,359,360],{"class":212},"\"2026-02-02\"",[171,362,222],{"class":180},[171,364,365],{"class":212},"\"2026-02-27\"",[171,367,222],{"class":180},[171,369,370],{"class":212},"\"2026-03-08\"",[171,372,373],{"class":180},"]),\n",[171,375,377],{"class":63,"line":376},9,[171,378,379],{"class":180},"})\n",[171,381,383],{"class":63,"line":382},10,[171,384,194],{"emptyLinePlaceholder":193},[171,386,388],{"class":63,"line":387},11,[171,389,391],{"class":390},"s-wDw","# =SUMIF(A:A, \"North\", D:D)\n",[171,393,395,398,400,403,406,409,412,415,417,420],{"class":63,"line":394},12,[171,396,397],{"class":180},"north_total ",[171,399,203],{"class":176},[171,401,402],{"class":180}," sales.loc[sales[",[171,404,405],{"class":212},"\"Region\"",[171,407,408],{"class":180},"] ",[171,410,411],{"class":176},"==",[171,413,414],{"class":212}," \"North\"",[171,416,222],{"class":180},[171,418,419],{"class":212},"\"Revenue\"",[171,421,422],{"class":180},"].sum()\n",[171,424,426],{"class":63,"line":425},13,[171,427,194],{"emptyLinePlaceholder":193},[171,429,431],{"class":63,"line":430},14,[171,432,433],{"class":390},"# =SUMIFS(D:D, A:A, \"North\", C:C, \"Widget\")\n",[171,435,437,440,442,445,447,449,451,453,456,459,462,465,467,469,472,475,477],{"class":63,"line":436},15,[171,438,439],{"class":180},"both ",[171,441,203],{"class":176},[171,443,444],{"class":180}," sales.loc[(sales[",[171,446,405],{"class":212},[171,448,408],{"class":180},[171,450,411],{"class":176},[171,452,414],{"class":212},[171,454,455],{"class":180},") ",[171,457,458],{"class":176},"&",[171,460,461],{"class":180}," (sales[",[171,463,464],{"class":212},"\"Product\"",[171,466,408],{"class":180},[171,468,411],{"class":176},[171,470,471],{"class":212}," \"Widget\"",[171,473,474],{"class":180},"), ",[171,476,419],{"class":212},[171,478,422],{"class":180},[171,480,482,485],{"class":63,"line":481},16,[171,483,484],{"class":315},"print",[171,486,487],{"class":180},"(north_total, both)\n",[10,489,490,493,494,497,498,501],{},[168,491,492],{},"sales[\"Region\"] == \"North\""," is a boolean Series — a column of True and False the same length as the\nframe. ",[168,495,496],{},".loc"," uses it to select rows, and ",[168,499,500],{},".sum()"," aggregates what is left. Every conditional\nfunction in Excel decomposes into those three steps, and once you see it the rest of this section is\ndetail.",[153,503,505],{"id":504},"the-function-by-function-map","The function-by-function map",[20,507,29,512,29,515,29,518,29,521,29,530,29,535,29,538,29,542,29,545,29,550,29,553,29,556,29,559,29,563,29,566,29,569,29,573,29,578,29,581,29,584,29,587,29,591,29,594,29,597,29,600,29,605,29,608,29,611,29,616,29,621,29,624,29,627],{"viewBox":508,"role":23,"ariaLabelledBy":509,"xmlns":27,"style":28},"0 0 760 336",[510,511],"fx-map-t","fx-map-d",[31,513,514],{"id":510},"The Excel functions a working spreadsheet is built from, and their counterparts",[35,516,517],{"id":511},"SUMIF maps to a masked sum or a grouped transform, COUNTIF to summing a boolean mask, VLOOKUP and INDEX MATCH to merge, nested IFs to np.select, and text functions to string slicing.",[39,519],{"x":41,"y":41,"width":42,"height":520,"fill":44},"336",[39,522],{"x":523,"y":524,"width":525,"height":526,"rx":527,"fill":528,"stroke":529},"18","30","724","34","8","#e7ebef","var(--line,#cdd5e6)",[56,531,61],{"x":532,"y":533,"style":534},"126.6","52","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle",[56,536,95],{"x":537,"y":533,"style":534},"361.9",[56,539,541],{"x":540,"y":533,"style":534},"615.3000000000001","The idea",[39,543],{"x":523,"y":544,"width":525,"height":526,"rx":527,"fill":89,"stroke":90},"67",[56,546,549],{"x":532,"y":547,"style":548},"88.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","SUMIF \u002F SUMIFS",[56,551,552],{"x":537,"y":547,"style":72},"loc[mask].sum()",[56,554,555],{"x":540,"y":547,"style":72},"select, then total",[39,557],{"x":523,"y":558,"width":525,"height":526,"rx":527,"fill":89,"stroke":90},"107",[56,560,562],{"x":532,"y":561,"style":548},"128.0","COUNTIF",[56,564,565],{"x":537,"y":561,"style":72},"mask.sum()",[56,567,568],{"x":540,"y":561,"style":72},"booleans are ones",[39,570],{"x":523,"y":571,"width":525,"height":526,"rx":527,"fill":572,"stroke":120},"147","#f0f4ff",[56,574,577],{"x":532,"y":575,"style":576},"168.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","VLOOKUP \u002F XLOOKUP",[56,579,580],{"x":537,"y":575,"style":72},"merge(how='left')",[56,582,583],{"x":540,"y":575,"style":72},"join on a name",[39,585],{"x":523,"y":586,"width":525,"height":526,"rx":527,"fill":572,"stroke":120},"187",[56,588,590],{"x":532,"y":589,"style":576},"208.0","INDEX + MATCH",[56,592,593],{"x":537,"y":589,"style":72},"merge or .map()",[56,595,596],{"x":540,"y":589,"style":72},"direction is irrelevant",[39,598],{"x":523,"y":599,"width":525,"height":526,"rx":527,"fill":52,"stroke":53},"227",[56,601,604],{"x":532,"y":602,"style":603},"248.0","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","nested IF \u002F IFS",[56,606,607],{"x":537,"y":602,"style":72},"np.select()",[56,609,610],{"x":540,"y":602,"style":72},"first match wins",[39,612],{"x":523,"y":613,"width":525,"height":526,"rx":527,"fill":614,"stroke":615},"267","#fee8f2","var(--accent,#d81b73)",[56,617,620],{"x":532,"y":618,"style":619},"288.0","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","LEFT \u002F MID \u002F RIGHT",[56,622,623],{"x":537,"y":618,"style":72},".str[a:b]",[56,625,626],{"x":540,"y":618,"style":72},"slicing, not calls",[56,628,630],{"x":123,"y":629,"style":147},"324","nine functions cover most of a real workbook",[10,632,633],{},"Nine Excel functions cover most of what a working spreadsheet contains, and each has a direct\ncounterpart. The map below is the summary; the guides under this topic take them one at a time with\nrunnable examples and the edge cases that catch people.",[635,636,637,651],"table",{},[638,639,640],"thead",{},[641,642,643,646,648],"tr",{},[644,645,61],"th",{},[644,647,95],{},[644,649,650],{},"Notes",[652,653,654,678,697,718,736,763,781,799,825],"tbody",{},[641,655,656,666,675],{},[657,658,659,662,663],"td",{},[168,660,661],{},"SUMIF"," \u002F ",[168,664,665],{},"SUMIFS",[657,667,668,671,672],{},[168,669,670],{},"df.loc[mask, col].sum()"," or ",[168,673,674],{},"groupby().sum()",[657,676,677],{},"Group once instead of per row",[641,679,680,687,694],{},[657,681,682,662,684],{},[168,683,562],{},[168,685,686],{},"COUNTIFS",[657,688,689,671,691],{},[168,690,565],{},[168,692,693],{},"value_counts()",[657,695,696],{},"Booleans sum as ones and zeros",[641,698,699,704,709],{},[657,700,701],{},[168,702,703],{},"AVERAGEIF",[657,705,706],{},[168,707,708],{},"df.loc[mask, col].mean()",[657,710,711,714,715,717],{},[168,712,713],{},"mean"," skips NaN; ",[168,716,703],{}," skips blanks",[641,719,720,728,733],{},[657,721,722,662,725],{},[168,723,724],{},"VLOOKUP",[168,726,727],{},"XLOOKUP",[657,729,730],{},[168,731,732],{},"df.merge(other, on=key, how=\"left\")",[657,734,735],{},"Direction of lookup stops mattering",[641,737,738,747,757],{},[657,739,740,743,744],{},[168,741,742],{},"INDEX"," + ",[168,745,746],{},"MATCH",[657,748,749,752,753,756],{},[168,750,751],{},"merge",", or ",[168,754,755],{},".map()"," from a Series",[657,758,759,762],{},[168,760,761],{},"map"," is the closest one-column form",[641,764,765,770,775],{},[657,766,767],{},[168,768,769],{},"IF",[657,771,772],{},[168,773,774],{},"np.where(cond, a, b)",[657,776,777,778],{},"Nested IFs become ",[168,779,780],{},"np.select",[641,782,783,791,796],{},[657,784,785,788,789],{},[168,786,787],{},"IFS"," \u002F nested ",[168,790,769],{},[657,792,793],{},[168,794,795],{},"np.select(conditions, choices)",[657,797,798],{},"Order matters, first match wins",[641,800,801,812,822],{},[657,802,803,662,806,662,809],{},[168,804,805],{},"LEFT",[168,807,808],{},"RIGHT",[168,810,811],{},"MID",[657,813,814,222,817,222,820],{},[168,815,816],{},".str[:n]",[168,818,819],{},".str[-n:]",[168,821,623],{},[657,823,824],{},"Slicing, not functions",[641,826,827,835,843],{},[657,828,829,662,832],{},[168,830,831],{},"RANK",[168,833,834],{},"PERCENTILE",[657,836,837,222,840],{},[168,838,839],{},".rank()",[168,841,842],{},".quantile()",[657,844,845],{},"Tie handling is explicit",[161,847,849],{"className":163,"code":848,"language":165,"meta":166,"style":166},"import numpy as np\n\n# =IF(D2>10000, \"Large\", \"Standard\")\nsales[\"Tier\"] = np.where(sales[\"Revenue\"] > 10000, \"Large\", \"Standard\")\n\n# =IFS(D2>15000,\"A\", D2>10000,\"B\", TRUE,\"C\")\nsales[\"Grade\"] = np.select(\n    [sales[\"Revenue\"] > 15000, sales[\"Revenue\"] > 10000],\n    [\"A\", \"B\"],\n    default=\"C\",\n)\n",[168,850,851,863,867,872,910,914,919,933,960,975,989],{"__ignoreMap":166},[171,852,853,855,858,860],{"class":63,"line":173},[171,854,177],{"class":176},[171,856,857],{"class":180}," numpy ",[171,859,184],{"class":176},[171,861,862],{"class":180}," np\n",[171,864,865],{"class":63,"line":190},[171,866,194],{"emptyLinePlaceholder":193},[171,868,869],{"class":63,"line":197},[171,870,871],{"class":390},"# =IF(D2>10000, \"Large\", \"Standard\")\n",[171,873,874,877,880,882,884,887,889,891,894,897,899,902,904,907],{"class":63,"line":209},[171,875,876],{"class":180},"sales[",[171,878,879],{"class":212},"\"Tier\"",[171,881,408],{"class":180},[171,883,203],{"class":176},[171,885,886],{"class":180}," np.where(sales[",[171,888,419],{"class":212},[171,890,408],{"class":180},[171,892,893],{"class":176},">",[171,895,896],{"class":315}," 10000",[171,898,222],{"class":180},[171,900,901],{"class":212},"\"Large\"",[171,903,222],{"class":180},[171,905,906],{"class":212},"\"Standard\"",[171,908,909],{"class":180},")\n",[171,911,912],{"class":63,"line":244},[171,913,194],{"emptyLinePlaceholder":193},[171,915,916],{"class":63,"line":277},[171,917,918],{"class":390},"# =IFS(D2>15000,\"A\", D2>10000,\"B\", TRUE,\"C\")\n",[171,920,921,923,926,928,930],{"class":63,"line":307},[171,922,876],{"class":180},[171,924,925],{"class":212},"\"Grade\"",[171,927,408],{"class":180},[171,929,203],{"class":176},[171,931,932],{"class":180}," np.select(\n",[171,934,935,938,940,942,944,947,950,952,954,956,958],{"class":63,"line":341},[171,936,937],{"class":180},"    [sales[",[171,939,419],{"class":212},[171,941,408],{"class":180},[171,943,893],{"class":176},[171,945,946],{"class":315}," 15000",[171,948,949],{"class":180},", sales[",[171,951,419],{"class":212},[171,953,408],{"class":180},[171,955,893],{"class":176},[171,957,896],{"class":315},[171,959,241],{"class":180},[171,961,962,965,968,970,973],{"class":63,"line":376},[171,963,964],{"class":180},"    [",[171,966,967],{"class":212},"\"A\"",[171,969,222],{"class":180},[171,971,972],{"class":212},"\"B\"",[171,974,241],{"class":180},[171,976,977,981,983,986],{"class":63,"line":382},[171,978,980],{"class":979},"sa561","    default",[171,982,203],{"class":176},[171,984,985],{"class":212},"\"C\"",[171,987,988],{"class":180},",\n",[171,990,991],{"class":63,"line":387},[171,992,909],{"class":180},[153,994,996],{"id":995},"group-once-look-up-many-times","Group once, look up many times",[10,998,999],{},"The most common translation mistake is to keep Excel's shape — computing a total per row — instead\nof adopting the pandas one. A SUMIF written into 50,000 rows scans the column 50,000 times. The\npandas equivalent scans it once.",[161,1001,1003],{"className":163,"code":1002,"language":165,"meta":166,"style":166},"# Excel: =SUMIF(A:A, A2, D:D) copied down 50,000 rows\nregion_totals = sales.groupby(\"Region\")[\"Revenue\"].sum()\nsales[\"Region total\"] = sales[\"Region\"].map(region_totals)\n\n# Or in one step, which is the idiom worth learning\nsales[\"Region total\"] = sales.groupby(\"Region\")[\"Revenue\"].transform(\"sum\")\n",[168,1004,1005,1010,1029,1048,1052,1057],{"__ignoreMap":166},[171,1006,1007],{"class":63,"line":173},[171,1008,1009],{"class":390},"# Excel: =SUMIF(A:A, A2, D:D) copied down 50,000 rows\n",[171,1011,1012,1015,1017,1020,1022,1025,1027],{"class":63,"line":190},[171,1013,1014],{"class":180},"region_totals ",[171,1016,203],{"class":176},[171,1018,1019],{"class":180}," sales.groupby(",[171,1021,405],{"class":212},[171,1023,1024],{"class":180},")[",[171,1026,419],{"class":212},[171,1028,422],{"class":180},[171,1030,1031,1033,1036,1038,1040,1043,1045],{"class":63,"line":197},[171,1032,876],{"class":180},[171,1034,1035],{"class":212},"\"Region total\"",[171,1037,408],{"class":180},[171,1039,203],{"class":176},[171,1041,1042],{"class":180}," sales[",[171,1044,405],{"class":212},[171,1046,1047],{"class":180},"].map(region_totals)\n",[171,1049,1050],{"class":63,"line":209},[171,1051,194],{"emptyLinePlaceholder":193},[171,1053,1054],{"class":63,"line":244},[171,1055,1056],{"class":390},"# Or in one step, which is the idiom worth learning\n",[171,1058,1059,1061,1063,1065,1067,1069,1071,1073,1075,1078,1081],{"class":63,"line":277},[171,1060,876],{"class":180},[171,1062,1035],{"class":212},[171,1064,408],{"class":180},[171,1066,203],{"class":176},[171,1068,1019],{"class":180},[171,1070,405],{"class":212},[171,1072,1024],{"class":180},[171,1074,419],{"class":212},[171,1076,1077],{"class":180},"].transform(",[171,1079,1080],{"class":212},"\"sum\"",[171,1082,909],{"class":180},[10,1084,1085,1088,1089,1091],{},[168,1086,1087],{},"transform"," is the direct answer to \"give every row its group's aggregate\" — the exact thing a\ncopied-down SUMIF does — and it is the function that makes most spreadsheet translations collapse to\na single line. ",[168,1090,761],{}," from a grouped Series does the same job when you already have the totals for\nanother purpose.",[153,1093,1095],{"id":1094},"lookups-and-why-direction-stops-mattering","Lookups, and why direction stops mattering",[10,1097,1098,1099,1101],{},"VLOOKUP's most-cursed limitation is that the key must sit to the left of the value. ",[168,1100,751],{}," joins on\na column name, so the physical order of columns is irrelevant, and a lookup that Excel needs\nINDEX\u002FMATCH for is the same call as one VLOOKUP could handle.",[20,1103,29,1108,29,1111,29,1114,29,1117,29,1122,29,1128,29,1133,29,1138,29,1143,29,1146,29,1149,29,1151,29,1154,29,1158,29,1161,29,1164,29,1169,29,1172],{"viewBox":1104,"role":23,"ariaLabelledBy":1105,"xmlns":27,"style":28},"0 0 760 232",[1106,1107],"fx-lookup-t","fx-lookup-d",[31,1109,1110],{"id":1106},"Why merge replaces three different lookup formulas",[35,1112,1113],{"id":1107},"VLOOKUP requires the key to sit left of the value and INDEX MATCH exists to work around that, while merge joins on a column name and never cared about physical order.",[39,1115],{"x":41,"y":41,"width":42,"height":1116,"fill":44},"232",[56,1118,1121],{"x":123,"y":1119,"style":1120},"32","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","one call, any direction",[39,1123],{"x":1124,"y":1125,"width":589,"height":1126,"rx":1127,"fill":572,"stroke":120,"style":54},"24.0","74","96","12",[56,1129,1132],{"x":561,"y":1130,"style":1131},"114","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","key column",[56,1134,1137],{"x":561,"y":1135,"style":1136},"136","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","matched by name",[63,1139],{"x1":1140,"y1":1141,"x2":1142,"y2":1141,"stroke":120,"style":54},"237.0","122.0","269.0",[133,1144],{"points":1145,"fill":136},"269.0,122.0 260.0,117.0 260.0,127.0",[39,1147],{"x":1148,"y":1125,"width":589,"height":1126,"rx":1127,"fill":572,"stroke":120,"style":54},"276.0",[56,1150,580],{"x":123,"y":1130,"style":1131},[56,1152,1153],{"x":123,"y":1135,"style":1136},"every row survives",[63,1155],{"x1":1156,"y1":1141,"x2":1157,"y2":1141,"stroke":120,"style":54},"489.0","521.0",[133,1159],{"points":1160,"fill":136},"521.0,122.0 512.0,117.0 512.0,127.0",[39,1162],{"x":1163,"y":1125,"width":589,"height":1126,"rx":1127,"fill":89,"stroke":90,"style":54},"528.0",[56,1165,1168],{"x":1166,"y":1130,"style":1167},"632.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","all columns joined",[56,1170,1171],{"x":1166,"y":1135,"style":1136},"not just one",[56,1173,1174],{"x":123,"y":146,"style":147},"column order stops being a constraint, so the workaround disappears",[161,1176,1178],{"className":163,"code":1177,"language":165,"meta":166,"style":166},"targets = pd.DataFrame({\n    \"Region\": [\"North\", \"South\", \"West\"],\n    \"Target\": [30000.0, 20000.0, 12000.0],\n    \"Owner\": [\"Ana\", \"Ben\", \"Dev\"],\n})\n\n# =VLOOKUP(A2, targets, 2, FALSE) — and the third column, in the same call\nenriched = sales.merge(targets, on=\"Region\", how=\"left\")\nprint(enriched[[\"Region\", \"Revenue\", \"Target\", \"Owner\"]].head())\n",[168,1179,1180,1189,1207,1229,1248,1252,1256,1261,1290],{"__ignoreMap":166},[171,1181,1182,1185,1187],{"class":63,"line":173},[171,1183,1184],{"class":180},"targets ",[171,1186,203],{"class":176},[171,1188,206],{"class":180},[171,1190,1191,1193,1195,1197,1199,1201,1203,1205],{"class":63,"line":190},[171,1192,213],{"class":212},[171,1194,216],{"class":180},[171,1196,219],{"class":212},[171,1198,222],{"class":180},[171,1200,225],{"class":212},[171,1202,222],{"class":180},[171,1204,234],{"class":212},[171,1206,241],{"class":180},[171,1208,1209,1212,1214,1217,1219,1222,1224,1227],{"class":63,"line":197},[171,1210,1211],{"class":212},"    \"Target\"",[171,1213,216],{"class":180},[171,1215,1216],{"class":315},"30000.0",[171,1218,222],{"class":180},[171,1220,1221],{"class":315},"20000.0",[171,1223,222],{"class":180},[171,1225,1226],{"class":315},"12000.0",[171,1228,241],{"class":180},[171,1230,1231,1234,1236,1238,1240,1242,1244,1246],{"class":63,"line":209},[171,1232,1233],{"class":212},"    \"Owner\"",[171,1235,216],{"class":180},[171,1237,252],{"class":212},[171,1239,222],{"class":180},[171,1241,257],{"class":212},[171,1243,222],{"class":180},[171,1245,267],{"class":212},[171,1247,241],{"class":180},[171,1249,1250],{"class":63,"line":244},[171,1251,379],{"class":180},[171,1253,1254],{"class":63,"line":277},[171,1255,194],{"emptyLinePlaceholder":193},[171,1257,1258],{"class":63,"line":307},[171,1259,1260],{"class":390},"# =VLOOKUP(A2, targets, 2, FALSE) — and the third column, in the same call\n",[171,1262,1263,1266,1268,1271,1274,1276,1278,1280,1283,1285,1288],{"class":63,"line":341},[171,1264,1265],{"class":180},"enriched ",[171,1267,203],{"class":176},[171,1269,1270],{"class":180}," sales.merge(targets, ",[171,1272,1273],{"class":979},"on",[171,1275,203],{"class":176},[171,1277,405],{"class":212},[171,1279,222],{"class":180},[171,1281,1282],{"class":979},"how",[171,1284,203],{"class":176},[171,1286,1287],{"class":212},"\"left\"",[171,1289,909],{"class":180},[171,1291,1292,1294,1297,1299,1301,1303,1305,1308,1310,1313],{"class":63,"line":376},[171,1293,484],{"class":315},[171,1295,1296],{"class":180},"(enriched[[",[171,1298,405],{"class":212},[171,1300,222],{"class":180},[171,1302,419],{"class":212},[171,1304,222],{"class":180},[171,1306,1307],{"class":212},"\"Target\"",[171,1309,222],{"class":180},[171,1311,1312],{"class":212},"\"Owner\"",[171,1314,1315],{"class":180},"]].head())\n",[10,1317,1318,1321,1322,1325,1326,1330],{},[168,1319,1320],{},"how=\"left\""," is the part that maps onto VLOOKUP's semantics: every original row survives, and a key\nwith no match gets NaN where Excel would show ",[168,1323,1324],{},"#N\u002FA",". Checking for those unmatched rows straight\nafter the merge is the equivalent of wrapping the formula in IFERROR, except that it tells you ",[1327,1328,1329],"em",{},"how\nmany"," failed rather than hiding them one at a time.",[161,1332,1334],{"className":163,"code":1333,"language":165,"meta":166,"style":166},"missing = enriched.loc[enriched[\"Target\"].isna(), \"Region\"].unique()\nif len(missing):\n    print(f\"no target defined for: {list(missing)}\")\n",[168,1335,1336,1356,1367],{"__ignoreMap":166},[171,1337,1338,1341,1343,1346,1348,1351,1353],{"class":63,"line":173},[171,1339,1340],{"class":180},"missing ",[171,1342,203],{"class":176},[171,1344,1345],{"class":180}," enriched.loc[enriched[",[171,1347,1307],{"class":212},[171,1349,1350],{"class":180},"].isna(), ",[171,1352,405],{"class":212},[171,1354,1355],{"class":180},"].unique()\n",[171,1357,1358,1361,1364],{"class":63,"line":190},[171,1359,1360],{"class":176},"if",[171,1362,1363],{"class":315}," len",[171,1365,1366],{"class":180},"(missing):\n",[171,1368,1369,1372,1375,1378,1381,1385,1388,1391,1394,1397],{"class":63,"line":197},[171,1370,1371],{"class":315},"    print",[171,1373,1374],{"class":180},"(",[171,1376,1377],{"class":176},"f",[171,1379,1380],{"class":212},"\"no target defined for: ",[171,1382,1384],{"class":1383},"sSjpA","{",[171,1386,1387],{"class":315},"list",[171,1389,1390],{"class":180},"(missing)",[171,1392,1393],{"class":1383},"}",[171,1395,1396],{"class":212},"\"",[171,1398,909],{"class":180},[10,1400,1401,1405],{},[14,1402,1404],{"href":1403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files\u002F","VLOOKUP Equivalent in pandas for Excel Files","\ncovers the join semantics in full, including the many-to-one duplication that silently inflates row\ncounts.",[153,1407,1409],{"id":1408},"where-the-two-engines-genuinely-disagree","Where the two engines genuinely disagree",[10,1411,1412],{},"Three differences produce most of the \"the numbers do not match\" reports, and all three are worth\nchecking before a formula is retired.",[10,1414,1415,1416,1418,1419,1421,1422,1425],{},"Blanks and zeros. Excel's ",[168,1417,703],{}," ignores blank cells but includes zeros; pandas' ",[168,1420,713],{}," ignores\nNaN and includes zeros too — but a blank cell read from a sheet may arrive as an empty string rather\nthan NaN, in which case the column is text and the mean is not computed at all. The check is one\nline: ",[168,1423,1424],{},"frame.dtypes",".",[10,1427,1428,1429,1432],{},"Text that looks numeric. A column of numbers stored as text totals to zero in pandas and works fine\nin Excel, because Excel coerces silently. ",[168,1430,1431],{},"pd.to_numeric(col, errors=\"coerce\")"," makes the coercion\nexplicit and leaves NaN where a value was not a number — which then shows up in a null count rather\nthan in a wrong total.",[10,1434,1435,1436,1439,1440,1442],{},"Rounding and display. Excel shows a rounded value and stores the full precision, so a total that\nlooks like it should be 1,000.00 can differ in the cents. Comparing with ",[168,1437,1438],{},"numpy.isclose"," rather than\n",[168,1441,411],{}," avoids chasing a difference that only exists at the fifteenth decimal place.",[161,1444,1446],{"className":163,"code":1445,"language":165,"meta":166,"style":166},"import numpy as np\n\nsales[\"Revenue\"] = pd.to_numeric(sales[\"Revenue\"], errors=\"coerce\")\nmatches = np.isclose(sales[\"Revenue\"].sum(), 48831.5, atol=0.005)\nprint(\"totals agree:\", matches)\n",[168,1447,1448,1458,1462,1490,1520],{"__ignoreMap":166},[171,1449,1450,1452,1454,1456],{"class":63,"line":173},[171,1451,177],{"class":176},[171,1453,857],{"class":180},[171,1455,184],{"class":176},[171,1457,862],{"class":180},[171,1459,1460],{"class":63,"line":190},[171,1461,194],{"emptyLinePlaceholder":193},[171,1463,1464,1466,1468,1470,1472,1475,1477,1480,1483,1485,1488],{"class":63,"line":197},[171,1465,876],{"class":180},[171,1467,419],{"class":212},[171,1469,408],{"class":180},[171,1471,203],{"class":176},[171,1473,1474],{"class":180}," pd.to_numeric(sales[",[171,1476,419],{"class":212},[171,1478,1479],{"class":180},"], ",[171,1481,1482],{"class":979},"errors",[171,1484,203],{"class":176},[171,1486,1487],{"class":212},"\"coerce\"",[171,1489,909],{"class":180},[171,1491,1492,1495,1497,1500,1502,1505,1508,1510,1513,1515,1518],{"class":63,"line":209},[171,1493,1494],{"class":180},"matches ",[171,1496,203],{"class":176},[171,1498,1499],{"class":180}," np.isclose(sales[",[171,1501,419],{"class":212},[171,1503,1504],{"class":180},"].sum(), ",[171,1506,1507],{"class":315},"48831.5",[171,1509,222],{"class":180},[171,1511,1512],{"class":979},"atol",[171,1514,203],{"class":176},[171,1516,1517],{"class":315},"0.005",[171,1519,909],{"class":180},[171,1521,1522,1524,1526,1529],{"class":63,"line":244},[171,1523,484],{"class":315},[171,1525,1374],{"class":180},[171,1527,1528],{"class":212},"\"totals agree:\"",[171,1530,1531],{"class":180},", matches)\n",[10,1533,1534,1535,1425],{},"The cleaning steps that precede any of this — stripping whitespace, fixing types, dropping blank\nrows — are in ",[14,1536,1538],{"href":1537},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002F","Cleaning Excel Data with Pandas",[153,1540,1542],{"id":1541},"validating-a-translation-against-the-original","Validating a translation against the original",[20,1544,29,1549,29,1552,29,1555,29,1558,29,1563,29,1568,29,1573,29,1579,29,1584,29,1587,29,1590,29,1594,29,1598,29,1602,29,1605,29,1608,29,1612,29,1616,29,1620,29,1623,29,1626,29,1630,29,1634,29,1638],{"viewBox":1545,"role":23,"ariaLabelledBy":1546,"xmlns":27,"style":28},"0 0 760 308",[1547,1548],"fx-verify-t","fx-verify-d",[31,1550,1551],{"id":1547},"Retiring a formula safely",[35,1553,1554],{"id":1548},"Compute the pandas version beside the workbook's existing column for one period, compare with a tolerance rather than equality, investigate the rows that differ, and only then remove the formula.",[39,1556],{"x":41,"y":41,"width":42,"height":1557,"fill":44},"308",[39,1559],{"x":1560,"y":48,"width":1561,"height":533,"rx":1562,"fill":89,"stroke":90},"22","716","10",[1564,1565],"circle",{"cx":533,"cy":1566,"r":1567,"fill":136},"54.0","15",[56,1569,1572],{"x":533,"y":1570,"style":1571},"59.0","font-size:13px;font-weight:700;fill:#ffffff;text-anchor:middle","1",[56,1574,1578],{"x":1575,"y":1576,"style":1577},"82","50","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","Compute both, side by side",[56,1580,1583],{"x":1575,"y":1581,"style":1582},"68","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:start","the sheet's column and yours, same file",[39,1585],{"x":1560,"y":1586,"width":1561,"height":533,"rx":1562,"fill":89,"stroke":90},"90",[1564,1588],{"cx":533,"cy":1589,"r":1567,"fill":136},"116.0",[56,1591,1593],{"x":533,"y":1592,"style":1571},"121.0","2",[56,1595,1597],{"x":1575,"y":1596,"style":1577},"112","Compare with a tolerance",[56,1599,1601],{"x":1575,"y":1600,"style":1582},"130","np.isclose, not ==, so rounding is not a bug",[39,1603],{"x":1560,"y":1604,"width":1561,"height":533,"rx":1562,"fill":89,"stroke":90},"152",[1564,1606],{"cx":533,"cy":1607,"r":1567,"fill":136},"178.0",[56,1609,1611],{"x":533,"y":1610,"style":1571},"183.0","3",[56,1613,1615],{"x":1575,"y":1614,"style":1577},"174","Investigate the differences",[56,1617,1619],{"x":1575,"y":1618,"style":1582},"192","usually a stale range or a spelling variant",[39,1621],{"x":1560,"y":1622,"width":1561,"height":533,"rx":1562,"fill":89,"stroke":90},"214",[1564,1624],{"cx":533,"cy":1625,"r":1567,"fill":136},"240.0",[56,1627,1629],{"x":533,"y":1628,"style":1571},"245.0","4",[56,1631,1633],{"x":1575,"y":1632,"style":1577},"236","Then retire the formula",[56,1635,1637],{"x":1575,"y":1636,"style":1582},"254","after the numbers agree, not before",[56,1639,1641],{"x":123,"y":1640,"style":147},"296","the differences you find are usually bugs in the spreadsheet",[10,1643,1644],{},"The reliable way to retire a formula is to run both for one period and compare, rather than to reason\nabout whether they agree. Read the workbook, compute your version alongside the sheet's existing\ncolumn, and print the rows where they differ.",[161,1646,1648],{"className":163,"code":1647,"language":165,"meta":166,"style":166},"import pandas as pd\n\nsheet = pd.read_excel(\"current-report.xlsx\", sheet_name=\"Detail\")\nsheet[\"Python total\"] = sheet.groupby(\"Region\")[\"Revenue\"].transform(\"sum\")\n\ndrift = sheet.loc[\n    ~np.isclose(sheet[\"Excel total\"], sheet[\"Python total\"], atol=0.005),\n    [\"Region\", \"Excel total\", \"Python total\"],\n]\nprint(f\"{len(drift)} row(s) differ\")\nprint(drift.head(20))\n",[168,1649,1650,1660,1664,1689,1716,1720,1730,1757,1773,1778,1803],{"__ignoreMap":166},[171,1651,1652,1654,1656,1658],{"class":63,"line":173},[171,1653,177],{"class":176},[171,1655,181],{"class":180},[171,1657,184],{"class":176},[171,1659,187],{"class":180},[171,1661,1662],{"class":63,"line":190},[171,1663,194],{"emptyLinePlaceholder":193},[171,1665,1666,1669,1671,1674,1677,1679,1682,1684,1687],{"class":63,"line":197},[171,1667,1668],{"class":180},"sheet ",[171,1670,203],{"class":176},[171,1672,1673],{"class":180}," pd.read_excel(",[171,1675,1676],{"class":212},"\"current-report.xlsx\"",[171,1678,222],{"class":180},[171,1680,1681],{"class":979},"sheet_name",[171,1683,203],{"class":176},[171,1685,1686],{"class":212},"\"Detail\"",[171,1688,909],{"class":180},[171,1690,1691,1694,1697,1699,1701,1704,1706,1708,1710,1712,1714],{"class":63,"line":209},[171,1692,1693],{"class":180},"sheet[",[171,1695,1696],{"class":212},"\"Python total\"",[171,1698,408],{"class":180},[171,1700,203],{"class":176},[171,1702,1703],{"class":180}," sheet.groupby(",[171,1705,405],{"class":212},[171,1707,1024],{"class":180},[171,1709,419],{"class":212},[171,1711,1077],{"class":180},[171,1713,1080],{"class":212},[171,1715,909],{"class":180},[171,1717,1718],{"class":63,"line":244},[171,1719,194],{"emptyLinePlaceholder":193},[171,1721,1722,1725,1727],{"class":63,"line":277},[171,1723,1724],{"class":180},"drift ",[171,1726,203],{"class":176},[171,1728,1729],{"class":180}," sheet.loc[\n",[171,1731,1732,1735,1738,1741,1744,1746,1748,1750,1752,1754],{"class":63,"line":307},[171,1733,1734],{"class":176},"    ~",[171,1736,1737],{"class":180},"np.isclose(sheet[",[171,1739,1740],{"class":212},"\"Excel total\"",[171,1742,1743],{"class":180},"], sheet[",[171,1745,1696],{"class":212},[171,1747,1479],{"class":180},[171,1749,1512],{"class":979},[171,1751,203],{"class":176},[171,1753,1517],{"class":315},[171,1755,1756],{"class":180},"),\n",[171,1758,1759,1761,1763,1765,1767,1769,1771],{"class":63,"line":341},[171,1760,964],{"class":180},[171,1762,405],{"class":212},[171,1764,222],{"class":180},[171,1766,1740],{"class":212},[171,1768,222],{"class":180},[171,1770,1696],{"class":212},[171,1772,241],{"class":180},[171,1774,1775],{"class":63,"line":376},[171,1776,1777],{"class":180},"]\n",[171,1779,1780,1782,1784,1786,1788,1790,1793,1796,1798,1801],{"class":63,"line":382},[171,1781,484],{"class":315},[171,1783,1374],{"class":180},[171,1785,1377],{"class":176},[171,1787,1396],{"class":212},[171,1789,1384],{"class":1383},[171,1791,1792],{"class":315},"len",[171,1794,1795],{"class":180},"(drift)",[171,1797,1393],{"class":1383},[171,1799,1800],{"class":212}," row(s) differ\"",[171,1802,909],{"class":180},[171,1804,1805,1807,1810,1812],{"class":63,"line":387},[171,1806,484],{"class":315},[171,1808,1809],{"class":180},"(drift.head(",[171,1811,47],{"class":315},[171,1813,1814],{"class":180},"))\n",[10,1816,1817,1818,1822],{},"Nine times out of ten the differences are informative rather than embarrassing: a stale range in the\nSUMIF that stopped at row 5,000, a region spelled two ways, a filter left applied when the workbook\nwas saved. Finding those is usually worth more than the automation itself.\n",[14,1819,1821],{"href":1820},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python\u002F","Compare Two Excel Files for Differences with Python","\ngeneralises the comparison to whole workbooks.",[153,1824,1826],{"id":1825},"what-should-stay-in-the-workbook","What should stay in the workbook",[10,1828,1829],{},"Not every formula is worth translating. Anything whose meaning depends on the sheet — a cell\nreference, a conditional format, a subtotal that follows a filter — belongs where it is. So does a\ndisplay formula the recipients edit themselves: replacing it with a static value written by a script\nremoves a capability they were using.",[10,1831,1832,1833,1837],{},"The rule that holds up: translate the formulas that produce the report's numbers, and leave the ones\nthat produce its behaviour. A workbook where Python writes the values and Excel keeps a handful of\npresentation formulas is not a compromise — it is usually the right design, and\n",[14,1834,1836],{"href":1835},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002F","Working with Excel Formulas in Python","\ncovers writing those remaining formulas from code.",[153,1839,1841],{"id":1840},"array-formulas-and-the-modern-dynamic-functions","Array formulas and the modern dynamic functions",[10,1843,1844],{},"Excel's newer functions — FILTER, UNIQUE, SORT, SEQUENCE, LET — describe operations on whole ranges\nrather than single cells, which makes them much closer to pandas than the classics they replace. The\ntranslations are correspondingly direct.",[161,1846,1848],{"className":163,"code":1847,"language":165,"meta":166,"style":166},"import pandas as pd\n\n# =FILTER(A2:D200, (A2:A200=\"North\")*(D2:D200>10000))\nfiltered = sales[(sales[\"Region\"] == \"North\") & (sales[\"Revenue\"] > 10000)]\n\n# =UNIQUE(A2:A200)\nregions = sales[\"Region\"].drop_duplicates()\n\n# =SORT(A2:D200, 4, -1)\nranked = sales.sort_values(\"Revenue\", ascending=False)\n\n# =SUMPRODUCT((A2:A200=\"North\")*(C2:C200=\"Widget\")*D2:D200)\nweighted = ((sales[\"Region\"] == \"North\") & (sales[\"Product\"] == \"Widget\")).mul(sales[\"Revenue\"]).sum()\n",[168,1849,1850,1860,1864,1869,1904,1908,1913,1927,1931,1936,1960,1964,1969],{"__ignoreMap":166},[171,1851,1852,1854,1856,1858],{"class":63,"line":173},[171,1853,177],{"class":176},[171,1855,181],{"class":180},[171,1857,184],{"class":176},[171,1859,187],{"class":180},[171,1861,1862],{"class":63,"line":190},[171,1863,194],{"emptyLinePlaceholder":193},[171,1865,1866],{"class":63,"line":197},[171,1867,1868],{"class":390},"# =FILTER(A2:D200, (A2:A200=\"North\")*(D2:D200>10000))\n",[171,1870,1871,1874,1876,1879,1881,1883,1885,1887,1889,1891,1893,1895,1897,1899,1901],{"class":63,"line":209},[171,1872,1873],{"class":180},"filtered ",[171,1875,203],{"class":176},[171,1877,1878],{"class":180}," sales[(sales[",[171,1880,405],{"class":212},[171,1882,408],{"class":180},[171,1884,411],{"class":176},[171,1886,414],{"class":212},[171,1888,455],{"class":180},[171,1890,458],{"class":176},[171,1892,461],{"class":180},[171,1894,419],{"class":212},[171,1896,408],{"class":180},[171,1898,893],{"class":176},[171,1900,896],{"class":315},[171,1902,1903],{"class":180},")]\n",[171,1905,1906],{"class":63,"line":244},[171,1907,194],{"emptyLinePlaceholder":193},[171,1909,1910],{"class":63,"line":277},[171,1911,1912],{"class":390},"# =UNIQUE(A2:A200)\n",[171,1914,1915,1918,1920,1922,1924],{"class":63,"line":307},[171,1916,1917],{"class":180},"regions ",[171,1919,203],{"class":176},[171,1921,1042],{"class":180},[171,1923,405],{"class":212},[171,1925,1926],{"class":180},"].drop_duplicates()\n",[171,1928,1929],{"class":63,"line":341},[171,1930,194],{"emptyLinePlaceholder":193},[171,1932,1933],{"class":63,"line":376},[171,1934,1935],{"class":390},"# =SORT(A2:D200, 4, -1)\n",[171,1937,1938,1941,1943,1946,1948,1950,1953,1955,1958],{"class":63,"line":382},[171,1939,1940],{"class":180},"ranked ",[171,1942,203],{"class":176},[171,1944,1945],{"class":180}," sales.sort_values(",[171,1947,419],{"class":212},[171,1949,222],{"class":180},[171,1951,1952],{"class":979},"ascending",[171,1954,203],{"class":176},[171,1956,1957],{"class":315},"False",[171,1959,909],{"class":180},[171,1961,1962],{"class":63,"line":387},[171,1963,194],{"emptyLinePlaceholder":193},[171,1965,1966],{"class":63,"line":394},[171,1967,1968],{"class":390},"# =SUMPRODUCT((A2:A200=\"North\")*(C2:C200=\"Widget\")*D2:D200)\n",[171,1970,1971,1974,1976,1979,1981,1983,1985,1987,1989,1991,1993,1995,1997,1999,2001,2004,2006],{"class":63,"line":425},[171,1972,1973],{"class":180},"weighted ",[171,1975,203],{"class":176},[171,1977,1978],{"class":180}," ((sales[",[171,1980,405],{"class":212},[171,1982,408],{"class":180},[171,1984,411],{"class":176},[171,1986,414],{"class":212},[171,1988,455],{"class":180},[171,1990,458],{"class":176},[171,1992,461],{"class":180},[171,1994,464],{"class":212},[171,1996,408],{"class":180},[171,1998,411],{"class":176},[171,2000,471],{"class":212},[171,2002,2003],{"class":180},")).mul(sales[",[171,2005,419],{"class":212},[171,2007,2008],{"class":180},"]).sum()\n",[10,2010,2011,2012,2014],{},"SUMPRODUCT is worth singling out because it is the function experienced spreadsheet authors reach\nfor when SUMIFS runs out of expressiveness, and its translation is the most literal of all: the\nmultiplication of boolean arrays in Excel is exactly the ",[168,2013,458],{}," of boolean Series in pandas, and the\nfinal sum is the same sum. Anyone who has written a lot of SUMPRODUCT formulas already thinks in\ncolumns, and will find pandas familiar rather than foreign.",[10,2016,2017],{},"The legacy array formulas entered with Ctrl+Shift+Enter translate the same way, and are often the\nbest candidates to move: they are the slowest thing a workbook can contain, they break silently when\na range is resized, and nobody remembers how they work.",[153,2019,2021],{"id":2020},"reading-the-formulas-out-of-the-workbook-first","Reading the formulas out of the workbook first",[10,2023,2024],{},"Before translating anything it helps to know what is actually there. openpyxl reads formulas as\ntext, so a short script can inventory every distinct formula pattern in a workbook and count how\noften each appears — which turns \"translate the spreadsheet\" into a list ordered by importance.",[161,2026,2028],{"className":163,"code":2027,"language":165,"meta":166,"style":166},"import re\nfrom collections import Counter\nfrom openpyxl import load_workbook\n\nbook = load_workbook(\"current-report.xlsx\")           # formulas, not cached values\npatterns = Counter()\nfor sheet in book.worksheets:\n    for row in sheet.iter_rows():\n        for cell in row:\n            if isinstance(cell.value, str) and cell.value.startswith(\"=\"):\n                shape = re.sub(r\"\\b[A-Z]{1,3}\\d+\\b\", \"REF\", cell.value)\n                patterns[shape] += 1\n\nfor shape, count in patterns.most_common(15):\n    print(f\"{count:6d}  {shape[:90]}\")\n",[168,2029,2030,2037,2050,2062,2066,2084,2094,2108,2121,2134,2162,2208,2219,2223,2239],{"__ignoreMap":166},[171,2031,2032,2034],{"class":63,"line":173},[171,2033,177],{"class":176},[171,2035,2036],{"class":180}," re\n",[171,2038,2039,2042,2045,2047],{"class":63,"line":190},[171,2040,2041],{"class":176},"from",[171,2043,2044],{"class":180}," collections ",[171,2046,177],{"class":176},[171,2048,2049],{"class":180}," Counter\n",[171,2051,2052,2054,2057,2059],{"class":63,"line":197},[171,2053,2041],{"class":176},[171,2055,2056],{"class":180}," openpyxl ",[171,2058,177],{"class":176},[171,2060,2061],{"class":180}," load_workbook\n",[171,2063,2064],{"class":63,"line":209},[171,2065,194],{"emptyLinePlaceholder":193},[171,2067,2068,2071,2073,2076,2078,2081],{"class":63,"line":244},[171,2069,2070],{"class":180},"book ",[171,2072,203],{"class":176},[171,2074,2075],{"class":180}," load_workbook(",[171,2077,1676],{"class":212},[171,2079,2080],{"class":180},")           ",[171,2082,2083],{"class":390},"# formulas, not cached values\n",[171,2085,2086,2089,2091],{"class":63,"line":277},[171,2087,2088],{"class":180},"patterns ",[171,2090,203],{"class":176},[171,2092,2093],{"class":180}," Counter()\n",[171,2095,2096,2099,2102,2105],{"class":63,"line":307},[171,2097,2098],{"class":176},"for",[171,2100,2101],{"class":180}," sheet ",[171,2103,2104],{"class":176},"in",[171,2106,2107],{"class":180}," book.worksheets:\n",[171,2109,2110,2113,2116,2118],{"class":63,"line":341},[171,2111,2112],{"class":176},"    for",[171,2114,2115],{"class":180}," row ",[171,2117,2104],{"class":176},[171,2119,2120],{"class":180}," sheet.iter_rows():\n",[171,2122,2123,2126,2129,2131],{"class":63,"line":376},[171,2124,2125],{"class":176},"        for",[171,2127,2128],{"class":180}," cell ",[171,2130,2104],{"class":176},[171,2132,2133],{"class":180}," row:\n",[171,2135,2136,2139,2142,2145,2148,2150,2153,2156,2159],{"class":63,"line":382},[171,2137,2138],{"class":176},"            if",[171,2140,2141],{"class":315}," isinstance",[171,2143,2144],{"class":180},"(cell.value, ",[171,2146,2147],{"class":315},"str",[171,2149,455],{"class":180},[171,2151,2152],{"class":176},"and",[171,2154,2155],{"class":180}," cell.value.startswith(",[171,2157,2158],{"class":212},"\"=\"",[171,2160,2161],{"class":180},"):\n",[171,2163,2164,2167,2169,2172,2175,2177,2180,2183,2186,2189,2192,2195,2198,2200,2202,2205],{"class":63,"line":387},[171,2165,2166],{"class":180},"                shape ",[171,2168,203],{"class":176},[171,2170,2171],{"class":180}," re.sub(",[171,2173,2174],{"class":176},"r",[171,2176,1396],{"class":212},[171,2178,2179],{"class":315},"\\b[",[171,2181,2182],{"class":1383},"A-Z",[171,2184,2185],{"class":315},"]",[171,2187,2188],{"class":176},"{1,3}",[171,2190,2191],{"class":315},"\\d",[171,2193,2194],{"class":176},"+",[171,2196,2197],{"class":315},"\\b",[171,2199,1396],{"class":212},[171,2201,222],{"class":180},[171,2203,2204],{"class":212},"\"REF\"",[171,2206,2207],{"class":180},", cell.value)\n",[171,2209,2210,2213,2216],{"class":63,"line":394},[171,2211,2212],{"class":180},"                patterns[shape] ",[171,2214,2215],{"class":176},"+=",[171,2217,2218],{"class":315}," 1\n",[171,2220,2221],{"class":63,"line":425},[171,2222,194],{"emptyLinePlaceholder":193},[171,2224,2225,2227,2230,2232,2235,2237],{"class":63,"line":430},[171,2226,2098],{"class":176},[171,2228,2229],{"class":180}," shape, count ",[171,2231,2104],{"class":176},[171,2233,2234],{"class":180}," patterns.most_common(",[171,2236,1567],{"class":315},[171,2238,2161],{"class":180},[171,2240,2241,2243,2245,2247,2249,2251,2254,2257,2259,2262,2265,2267,2269,2271,2273],{"class":63,"line":436},[171,2242,1371],{"class":315},[171,2244,1374],{"class":180},[171,2246,1377],{"class":176},[171,2248,1396],{"class":212},[171,2250,1384],{"class":1383},[171,2252,2253],{"class":180},"count",[171,2255,2256],{"class":176},":6d",[171,2258,1393],{"class":1383},[171,2260,2261],{"class":1383},"  {",[171,2263,2264],{"class":180},"shape[:",[171,2266,1586],{"class":315},[171,2268,2185],{"class":180},[171,2270,1393],{"class":1383},[171,2272,1396],{"class":212},[171,2274,909],{"class":180},[10,2276,2277,2278,2282],{},"Replacing cell references with a placeholder collapses ten thousand copied-down formulas into a\nsingle pattern with a count beside it, which is exactly the view needed to decide what to translate\nfirst. It also surfaces the surprises — a formula that appears once in the middle of a column of\nidentical ones, which is nearly always where a manual override is hiding.\n",[14,2279,2281],{"href":2280},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fread-formula-results-with-openpyxl-data-only\u002F","Read Formula Results with openpyxl data_only","\ncovers the other half of that read: getting the values those formulas produced.",[153,2284,2286],{"id":2285},"writing-the-answers-back-where-the-formulas-were","Writing the answers back where the formulas were",[10,2288,2289],{},"A translation project has an awkward middle stage: the numbers are computed in Python but the\nrecipients still expect the workbook they know. The workable pattern is to keep the layout and\nreplace only the values, which openpyxl does without disturbing the styling, the header block or the\ncharts pointing at the range.",[161,2291,2293],{"className":163,"code":2292,"language":165,"meta":166,"style":166},"import pandas as pd\nfrom openpyxl import load_workbook\n\nsummary = (\n    pd.read_excel(\"source-data.xlsx\", sheet_name=\"Detail\")\n      .groupby(\"Region\", as_index=False)[\"Revenue\"].sum()\n      .sort_values(\"Revenue\", ascending=False)\n)\n\nbook = load_workbook(\"monthly-report.xlsx\")\nsheet = book[\"Summary\"]\nfor offset, (region, revenue) in enumerate(summary.itertuples(index=False), start=0):\n    sheet.cell(row=4 + offset, column=2, value=region)\n    sheet.cell(row=4 + offset, column=3, value=float(revenue))\nbook.save(\"monthly-report-2026-09.xlsx\")\n",[168,2294,2295,2305,2315,2319,2329,2347,2369,2386,2390,2394,2407,2421,2454,2489,2521],{"__ignoreMap":166},[171,2296,2297,2299,2301,2303],{"class":63,"line":173},[171,2298,177],{"class":176},[171,2300,181],{"class":180},[171,2302,184],{"class":176},[171,2304,187],{"class":180},[171,2306,2307,2309,2311,2313],{"class":63,"line":190},[171,2308,2041],{"class":176},[171,2310,2056],{"class":180},[171,2312,177],{"class":176},[171,2314,2061],{"class":180},[171,2316,2317],{"class":63,"line":197},[171,2318,194],{"emptyLinePlaceholder":193},[171,2320,2321,2324,2326],{"class":63,"line":209},[171,2322,2323],{"class":180},"summary ",[171,2325,203],{"class":176},[171,2327,2328],{"class":180}," (\n",[171,2330,2331,2334,2337,2339,2341,2343,2345],{"class":63,"line":244},[171,2332,2333],{"class":180},"    pd.read_excel(",[171,2335,2336],{"class":212},"\"source-data.xlsx\"",[171,2338,222],{"class":180},[171,2340,1681],{"class":979},[171,2342,203],{"class":176},[171,2344,1686],{"class":212},[171,2346,909],{"class":180},[171,2348,2349,2352,2354,2356,2359,2361,2363,2365,2367],{"class":63,"line":277},[171,2350,2351],{"class":180},"      .groupby(",[171,2353,405],{"class":212},[171,2355,222],{"class":180},[171,2357,2358],{"class":979},"as_index",[171,2360,203],{"class":176},[171,2362,1957],{"class":315},[171,2364,1024],{"class":180},[171,2366,419],{"class":212},[171,2368,422],{"class":180},[171,2370,2371,2374,2376,2378,2380,2382,2384],{"class":63,"line":307},[171,2372,2373],{"class":180},"      .sort_values(",[171,2375,419],{"class":212},[171,2377,222],{"class":180},[171,2379,1952],{"class":979},[171,2381,203],{"class":176},[171,2383,1957],{"class":315},[171,2385,909],{"class":180},[171,2387,2388],{"class":63,"line":341},[171,2389,909],{"class":180},[171,2391,2392],{"class":63,"line":376},[171,2393,194],{"emptyLinePlaceholder":193},[171,2395,2396,2398,2400,2402,2405],{"class":63,"line":382},[171,2397,2070],{"class":180},[171,2399,203],{"class":176},[171,2401,2075],{"class":180},[171,2403,2404],{"class":212},"\"monthly-report.xlsx\"",[171,2406,909],{"class":180},[171,2408,2409,2411,2413,2416,2419],{"class":63,"line":387},[171,2410,1668],{"class":180},[171,2412,203],{"class":176},[171,2414,2415],{"class":180}," book[",[171,2417,2418],{"class":212},"\"Summary\"",[171,2420,1777],{"class":180},[171,2422,2423,2425,2428,2430,2433,2436,2439,2441,2443,2445,2448,2450,2452],{"class":63,"line":394},[171,2424,2098],{"class":176},[171,2426,2427],{"class":180}," offset, (region, revenue) ",[171,2429,2104],{"class":176},[171,2431,2432],{"class":315}," enumerate",[171,2434,2435],{"class":180},"(summary.itertuples(",[171,2437,2438],{"class":979},"index",[171,2440,203],{"class":176},[171,2442,1957],{"class":315},[171,2444,474],{"class":180},[171,2446,2447],{"class":979},"start",[171,2449,203],{"class":176},[171,2451,41],{"class":315},[171,2453,2161],{"class":180},[171,2455,2456,2459,2462,2464,2466,2469,2472,2475,2477,2479,2481,2484,2486],{"class":63,"line":425},[171,2457,2458],{"class":180},"    sheet.cell(",[171,2460,2461],{"class":979},"row",[171,2463,203],{"class":176},[171,2465,1629],{"class":315},[171,2467,2468],{"class":176}," +",[171,2470,2471],{"class":180}," offset, ",[171,2473,2474],{"class":979},"column",[171,2476,203],{"class":176},[171,2478,1593],{"class":315},[171,2480,222],{"class":180},[171,2482,2483],{"class":979},"value",[171,2485,203],{"class":176},[171,2487,2488],{"class":180},"region)\n",[171,2490,2491,2493,2495,2497,2499,2501,2503,2505,2507,2509,2511,2513,2515,2518],{"class":63,"line":430},[171,2492,2458],{"class":180},[171,2494,2461],{"class":979},[171,2496,203],{"class":176},[171,2498,1629],{"class":315},[171,2500,2468],{"class":176},[171,2502,2471],{"class":180},[171,2504,2474],{"class":979},[171,2506,203],{"class":176},[171,2508,1611],{"class":315},[171,2510,222],{"class":180},[171,2512,2483],{"class":979},[171,2514,203],{"class":176},[171,2516,2517],{"class":315},"float",[171,2519,2520],{"class":180},"(revenue))\n",[171,2522,2523,2526,2529],{"class":63,"line":436},[171,2524,2525],{"class":180},"book.save(",[171,2527,2528],{"class":212},"\"monthly-report-2026-09.xlsx\"",[171,2530,909],{"class":180},[10,2532,2533,2534,2537],{},"Two details make this safe over time. Writing ",[168,2535,2536],{},"float(revenue)"," rather than the NumPy scalar avoids\nthe type that openpyxl cannot serialise — a failure that appears as an unhelpful error about an\nunsupported value. And saving under a new name keeps the template intact, so a bad run costs a\nminute rather than the original file.",[10,2539,2540,2541,2545],{},"The alternative — deleting the formula column and appending a new one — moves every cell to the\nright of it and breaks anything that referenced those positions, which is the specific hazard\ndescribed in ",[14,2542,2544],{"href":2543},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel Template Without Losing Formatting",".\nWriting values into the cells that already exist avoids the whole category.",[153,2547,2549],{"id":2548},"deciding-how-far-to-take-it","Deciding how far to take it",[10,2551,2552],{},"There is a point of diminishing returns, and it is worth naming. Translating the formulas that\nproduce a report's headline numbers usually pays for itself immediately: those are the ones that\nbreak silently when a range stops at row 5,000, and the ones that make a workbook take a minute to\nopen. Translating the last few — a percentage in a footer, a conditional label in a status column —\nbuys very little and costs the recipients the ability to adjust them.",[10,2554,2555],{},"A practical stopping rule is to translate everything that feeds a number somebody makes a decision\non, and leave everything that only affects how the sheet reads. That keeps the workbook editable\nwhere editing is legitimate, keeps the automation responsible for the parts that must be right, and\nleaves a clean line between the two that the next person can see without being told.",[153,2557,2559],{"id":2558},"dates-the-functions-with-the-most-hidden-differences","Dates: the functions with the most hidden differences",[10,2561,2562],{},"Date formulas translate cleanly in form and badly in detail, because the two systems disagree about\nwhat a date is. Excel stores a serial number counting days from 1899-12-30 and applies a display\nformat; pandas stores a timestamp with nanosecond precision. Most of the resulting confusion comes\nfrom that gap rather than from the functions themselves.",[161,2564,2566],{"className":163,"code":2565,"language":165,"meta":166,"style":166},"import pandas as pd\n\n# =YEAR(E2), =MONTH(E2), =TEXT(E2,\"YYYY-MM\")\nsales[\"Year\"] = sales[\"Ordered\"].dt.year\nsales[\"Month\"] = sales[\"Ordered\"].dt.month\nsales[\"Period\"] = sales[\"Ordered\"].dt.strftime(\"%Y-%m\")\n\n# =EOMONTH(E2, 0) and =EDATE(E2, 3)\nsales[\"Month end\"] = sales[\"Ordered\"] + pd.offsets.MonthEnd(0)\nsales[\"In three months\"] = sales[\"Ordered\"] + pd.DateOffset(months=3)\n\n# =NETWORKDAYS(E2, TODAY())\nsales[\"Working days\"] = [\n    len(pd.bdate_range(start, pd.Timestamp.today().normalize())) for start in sales[\"Ordered\"]\n]\n",[168,2567,2568,2578,2582,2587,2606,2624,2647,2651,2656,2682,2713,2717,2722,2736,2757],{"__ignoreMap":166},[171,2569,2570,2572,2574,2576],{"class":63,"line":173},[171,2571,177],{"class":176},[171,2573,181],{"class":180},[171,2575,184],{"class":176},[171,2577,187],{"class":180},[171,2579,2580],{"class":63,"line":190},[171,2581,194],{"emptyLinePlaceholder":193},[171,2583,2584],{"class":63,"line":197},[171,2585,2586],{"class":390},"# =YEAR(E2), =MONTH(E2), =TEXT(E2,\"YYYY-MM\")\n",[171,2588,2589,2591,2594,2596,2598,2600,2603],{"class":63,"line":209},[171,2590,876],{"class":180},[171,2592,2593],{"class":212},"\"Year\"",[171,2595,408],{"class":180},[171,2597,203],{"class":176},[171,2599,1042],{"class":180},[171,2601,2602],{"class":212},"\"Ordered\"",[171,2604,2605],{"class":180},"].dt.year\n",[171,2607,2608,2610,2613,2615,2617,2619,2621],{"class":63,"line":244},[171,2609,876],{"class":180},[171,2611,2612],{"class":212},"\"Month\"",[171,2614,408],{"class":180},[171,2616,203],{"class":176},[171,2618,1042],{"class":180},[171,2620,2602],{"class":212},[171,2622,2623],{"class":180},"].dt.month\n",[171,2625,2626,2628,2631,2633,2635,2637,2639,2642,2645],{"class":63,"line":277},[171,2627,876],{"class":180},[171,2629,2630],{"class":212},"\"Period\"",[171,2632,408],{"class":180},[171,2634,203],{"class":176},[171,2636,1042],{"class":180},[171,2638,2602],{"class":212},[171,2640,2641],{"class":180},"].dt.strftime(",[171,2643,2644],{"class":212},"\"%Y-%m\"",[171,2646,909],{"class":180},[171,2648,2649],{"class":63,"line":307},[171,2650,194],{"emptyLinePlaceholder":193},[171,2652,2653],{"class":63,"line":341},[171,2654,2655],{"class":390},"# =EOMONTH(E2, 0) and =EDATE(E2, 3)\n",[171,2657,2658,2660,2663,2665,2667,2669,2671,2673,2675,2678,2680],{"class":63,"line":376},[171,2659,876],{"class":180},[171,2661,2662],{"class":212},"\"Month end\"",[171,2664,408],{"class":180},[171,2666,203],{"class":176},[171,2668,1042],{"class":180},[171,2670,2602],{"class":212},[171,2672,408],{"class":180},[171,2674,2194],{"class":176},[171,2676,2677],{"class":180}," pd.offsets.MonthEnd(",[171,2679,41],{"class":315},[171,2681,909],{"class":180},[171,2683,2684,2686,2689,2691,2693,2695,2697,2699,2701,2704,2707,2709,2711],{"class":63,"line":382},[171,2685,876],{"class":180},[171,2687,2688],{"class":212},"\"In three months\"",[171,2690,408],{"class":180},[171,2692,203],{"class":176},[171,2694,1042],{"class":180},[171,2696,2602],{"class":212},[171,2698,408],{"class":180},[171,2700,2194],{"class":176},[171,2702,2703],{"class":180}," pd.DateOffset(",[171,2705,2706],{"class":979},"months",[171,2708,203],{"class":176},[171,2710,1611],{"class":315},[171,2712,909],{"class":180},[171,2714,2715],{"class":63,"line":387},[171,2716,194],{"emptyLinePlaceholder":193},[171,2718,2719],{"class":63,"line":394},[171,2720,2721],{"class":390},"# =NETWORKDAYS(E2, TODAY())\n",[171,2723,2724,2726,2729,2731,2733],{"class":63,"line":425},[171,2725,876],{"class":180},[171,2727,2728],{"class":212},"\"Working days\"",[171,2730,408],{"class":180},[171,2732,203],{"class":176},[171,2734,2735],{"class":180}," [\n",[171,2737,2738,2741,2744,2746,2749,2751,2753,2755],{"class":63,"line":430},[171,2739,2740],{"class":315},"    len",[171,2742,2743],{"class":180},"(pd.bdate_range(start, pd.Timestamp.today().normalize())) ",[171,2745,2098],{"class":176},[171,2747,2748],{"class":180}," start ",[171,2750,2104],{"class":176},[171,2752,1042],{"class":180},[171,2754,2602],{"class":212},[171,2756,1777],{"class":180},[171,2758,2759],{"class":63,"line":436},[171,2760,1777],{"class":180},[10,2762,2763,2766,2767,2770],{},[168,2764,2765],{},"MonthEnd(0)"," is the one worth memorising: with an offset of zero it snaps a date already at\nmonth-end to itself, where ",[168,2768,2769],{},"MonthEnd(1)"," would push it forward a month — the same off-by-one that\nEOMONTH's second argument produces in a spreadsheet.",[10,2772,2773,2774,2777,2778,2781,2782,1425],{},"NETWORKDAYS has no vectorised equivalent because holiday calendars vary; ",[168,2775,2776],{},"bdate_range"," handles the\nweekend part and a custom ",[168,2779,2780],{},"holidays"," argument covers the rest. The wider set of date problems that\nan Excel export produces — serials arriving as integers, timezones, quarter grouping — is covered in\n",[14,2783,2785],{"href":2784},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","Working with Dates and Times in Excel Data",[153,2787,2789],{"id":2788},"key-takeaways","Key takeaways",[2791,2792,2793,2797,2802,2807,2816,2819],"ul",{},[2794,2795,2796],"li",{},"Every conditional Excel function decomposes into a boolean mask, a selection and an aggregation;\nonce you see that, the translations are mechanical.",[2794,2798,2799,2801],{},[168,2800,1087],{}," is the direct equivalent of a copied-down SUMIF or COUNTIF — one pass over the column\ninstead of one per row.",[2794,2803,2804,2806],{},[168,2805,751],{}," replaces VLOOKUP, INDEX\u002FMATCH and XLOOKUP alike, and the lookup direction stops being a\nconstraint.",[2794,2808,2809,2812,2813,2815],{},[168,2810,2811],{},"np.where"," handles a single IF and ",[168,2814,780],{}," handles nested ones, with first-match-wins ordering.",[2794,2817,2818],{},"Mismatched totals almost always come from text-typed numbers, blanks that are empty strings, or\nrounding — check dtypes before doubting the logic.",[2794,2820,2821],{},"Validate by running both versions side by side for one period; the differences usually reveal a\nbug in the spreadsheet.",[153,2823,2825],{"id":2824},"frequently-asked-questions","Frequently asked questions",[10,2827,2828,2832],{},[2829,2830,2831],"strong",{},"Why is there no single pandas function called SUMIF?","\nBecause SUMIF collapses two ideas that pandas keeps separate: selecting rows and aggregating them. Once they are separate you can filter on anything, aggregate several columns at once, and group by more than one key — none of which SUMIF can express.",[10,2834,2835,2838],{},[2829,2836,2837],{},"Do I have to rewrite every formula in the workbook?","\nNo, and usually you should not. Translate the formulas that feed the numbers a report depends on, and leave presentational ones in the sheet. A hybrid workbook where Python writes values and Excel keeps a few display formulas is a perfectly stable arrangement.",[10,2840,2841,2844],{},[2829,2842,2843],{},"How do I keep the same answers as the spreadsheet?","\nRebuild the formula's result in pandas, write it beside the original for one period, and compare. Differences almost always come from rounding, from blank cells counted differently, or from a filter that silently excluded rows — all of which are worth finding before the formula is retired.",[10,2846,2847,2850],{},[2829,2848,2849],{},"Which Excel functions have no pandas equivalent?","\nThe ones that describe the sheet rather than the data: CELL, INDIRECT, OFFSET with a moving anchor, and anything that depends on cell addresses. If a formula's meaning depends on where it sits, it belongs in the workbook.",[10,2852,2853,2856],{},[2829,2854,2855],{},"Is a pandas version always faster?","\nFor a few hundred rows the difference is invisible. Past tens of thousands it is dramatic, because Excel recalculates a formula per cell while pandas performs one vectorised operation per column — and array formulas across large ranges are the slowest thing a workbook can contain.",[10,2858,2859,2862],{},[2829,2860,2861],{},"What about XLOOKUP?","\nXLOOKUP maps onto merge in exactly the way VLOOKUP does, with the added conveniences — searching right to left, an if-not-found value — coming free because merge never cared about column order in the first place.",[153,2864,2866],{"id":2865},"related","Related",[2791,2868,2869,2875,2882,2889,2896,2903,2910,2917,2924],{},[2794,2870,2871,2872,2874],{},"Up one level: ",[14,2873,17],{"href":16}," — the wider cleaning and reshaping toolkit.",[2794,2876,2877,2881],{},[14,2878,2880],{"href":2879},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas\u002F","SUMIF and SUMIFS Equivalent in pandas"," — conditional totals, one condition or several.",[2794,2883,2884,2888],{},[14,2885,2887],{"href":2886},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fcountif-and-countifs-equivalent-in-pandas\u002F","COUNTIF and COUNTIFS Equivalent in pandas"," — counting rows that match, and counting distinct values.",[2794,2890,2891,2895],{},[14,2892,2894],{"href":2893},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Findex-match-equivalent-in-pandas\u002F","INDEX MATCH Equivalent in pandas"," — lookups in any direction, with map and merge.",[2794,2897,2898,2902],{},[14,2899,2901],{"href":2900},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-if-formulas-as-pandas-conditional-columns\u002F","Excel IF Formulas as pandas Conditional Columns"," — np.where, np.select and the nested-IF ladder.",[2794,2904,2905,2909],{},[14,2906,2908],{"href":2907},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-text-functions-left-right-mid-and-concat-in-pandas\u002F","Excel Text Functions LEFT, RIGHT, MID and CONCAT in pandas"," — string slicing and joining on whole columns.",[2794,2911,2912,2916],{},[14,2913,2915],{"href":2914},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frank-and-percentile-formulas-in-pandas\u002F","RANK and PERCENTILE Formulas in pandas"," — ranking with explicit tie handling, and quantiles.",[2794,2918,2919,2923],{},[14,2920,2922],{"href":2921},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas\u002F","Running Totals and Year-Over-Year Growth in pandas"," — cumulative sums and period comparisons without dragging a formula down.",[2794,2925,2926,2928],{},[14,2927,1404],{"href":1403}," — the join semantics behind every lookup translation.",[2930,2931,2932],"style",{},"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 .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":166,"searchDepth":190,"depth":190,"links":2934},[2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949],{"id":155,"depth":190,"text":156},{"id":504,"depth":190,"text":505},{"id":995,"depth":190,"text":996},{"id":1094,"depth":190,"text":1095},{"id":1408,"depth":190,"text":1409},{"id":1541,"depth":190,"text":1542},{"id":1825,"depth":190,"text":1826},{"id":1840,"depth":190,"text":1841},{"id":2020,"depth":190,"text":2021},{"id":2285,"depth":190,"text":2286},{"id":2548,"depth":190,"text":2549},{"id":2558,"depth":190,"text":2559},{"id":2788,"depth":190,"text":2789},{"id":2824,"depth":190,"text":2825},{"id":2865,"depth":190,"text":2866},"2026-09-04","Translate SUMIF, COUNTIF, VLOOKUP, INDEX MATCH, nested IFs and text functions into pandas — with the mask-select-aggregate pattern behind all of them.","md",[2954,2956,2958,2960,2962,2964],{"q":2831,"a":2955},"Because SUMIF collapses two ideas that pandas keeps separate: selecting rows and aggregating them. Once they are separate you can filter on anything, aggregate several columns at once, and group by more than one key — none of which SUMIF can express.",{"q":2837,"a":2957},"No, and usually you should not. Translate the formulas that feed the numbers a report depends on, and leave presentational ones in the sheet. A hybrid workbook where Python writes values and Excel keeps a few display formulas is a perfectly stable arrangement.",{"q":2843,"a":2959},"Rebuild the formula's result in pandas, write it beside the original for one period, and compare. Differences almost always come from rounding, from blank cells counted differently, or from a filter that silently excluded rows — all of which are worth finding before the formula is retired.",{"q":2849,"a":2961},"The ones that describe the sheet rather than the data: CELL, INDIRECT, OFFSET with a moving anchor, and anything that depends on cell addresses. If a formula's meaning depends on where it sits, it belongs in the workbook.",{"q":2855,"a":2963},"For a few hundred rows the difference is invisible. Past tens of thousands it is dramatic, because Excel recalculates a formula per cell while pandas performs one vectorised operation per column — and array formulas across large ranges are the slowest thing a workbook can contain.",{"q":2861,"a":2965},"XLOOKUP maps onto merge in exactly the way VLOOKUP does, with the added conveniences — searching right to left, an if-not-found value — coming free because merge never cared about column order in the first place.",{"breadcrumb":2967},[2968,2971,2972],{"name":2969,"item":2970},"Home","\u002F",{"name":17,"item":16},{"name":5,"item":2973},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas",{"title":5,"description":2976},"A function-by-function map from Excel formulas to pandas: SUMIFS, COUNTIFS, VLOOKUP, INDEX MATCH, IFS, LEFT\u002FMID\u002FRIGHT and RANK, plus how to verify the numbers match.","excel-formula-equivalents-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Findex","guide","Xx93qyoF68NEmSFj9ShO6LvhQgqIMLuOG1gHxA-vWaY",[2982,2986],{"title":2983,"path":2984,"stem":2985,"children":-1},"Unpivot a Wide Excel Sheet with pandas melt","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Funpivot-a-wide-excel-sheet-with-pandas-melt","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Funpivot-a-wide-excel-sheet-with-pandas-melt\u002Findex",{"title":2887,"path":2987,"stem":2988,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fcountif-and-countifs-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fcountif-and-countifs-equivalent-in-pandas\u002Findex",1788710151645]