[{"data":1,"prerenderedAt":2101},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data":2067,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data":2076,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data":2081,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data":2082},{"id":4,"title":5,"body":6,"dateModified":2034,"datePublished":2034,"description":2035,"extension":2036,"faq":2037,"keyword":2046,"kind":2047,"meta":2048,"navigation":123,"path":2056,"seo":2057,"slug":2060,"stem":2061,"topics":2062,"type":2065,"__hash__":2066},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data\u002Findex.md","Use Polars Window Functions on Excel Data",{"type":7,"value":8,"toc":2021},"minimark",[9,45,50,78,84,88,95,272,290,524,528,535,643,666,670,685,858,872,879,965,974,1061,1065,1072,1185,1192,1196,1222,1319,1322,1326,1341,1700,1727,1861,1865,1924,1928,1956,1960,1966,1972,1978,1984,1988,2017],[10,11,12,13,17,18,22,23,26,27,30,31,34,35,38,39,44],"p",{},"Many spreadsheet calculations need a figure about a row's ",[14,15,16],"em",{},"group"," on the row itself: each sale's share of its region's total, a product's rank within its category, a running total that restarts for each customer, the change from the same product's previous month. In Excel these are ",[19,20,21],"code",{},"SUMIFS"," divided by a total, ",[19,24,25],{},"RANK.EQ"," with helper columns, or carefully anchored ranges copied down. A ",[19,28,29],{},"group_by"," in Polars collapses the rows, which is not what you want here. ",[14,32,33],{},"Window functions"," — expressions followed by ",[19,36,37],{},".over(...)"," — compute per group and broadcast the result back to every row, keeping the table intact. This page applies them to monthly sales from Excel: shares, ranks, running totals, previous-period changes and top-N filters, with the options that control ordering. It belongs to ",[40,41,43],"a",{"href":42},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002F","Reading Excel with Polars and Arrow",".",[46,47,49],"h2",{"id":48},"prerequisites","Prerequisites",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install polars fastexcel\n","bash","",[19,58,59],{"__ignoreMap":56},[60,61,64,68,72,75],"span",{"class":62,"line":63},"line",1,[60,65,67],{"class":66},"sMTad","pip",[60,69,71],{"class":70},"srMev"," install",[60,73,74],{"class":70}," polars",[60,76,77],{"class":70}," fastexcel\n",[10,79,80,83],{},[19,81,82],{},"monthly_units.xlsx"," holds units sold per month, region and product for June to September 2026 — 24 rows in shuffled order, as exports often are. The examples were run with Polars 1.44.",[46,85,87],{"id":86},"step-1-share-of-the-group-total","Step 1 — Share of the group total",[10,89,90,91,94],{},"The classic window calculation: each row's units as a share of its region-month total. ",[19,92,93],{},"pl.col(\"Units\").sum().over(\"Region\", \"Month\")"," computes the total per region and month and repeats it on every row of that group:",[51,96,100],{"className":97,"code":98,"language":99,"meta":56,"style":56},"language-python shiki shiki-themes github-light github-dark-high-contrast","import polars as pl\n\ndf = pl.read_excel(\"monthly_units.xlsx\").sort(\"Month\", \"Region\", \"Product\")\nshares = df.with_columns(\n    pl.col(\"Units\").sum().over(\"Region\", \"Month\").alias(\"Region total\"),\n).with_columns(\n    (pl.col(\"Units\") \u002F pl.col(\"Region total\") * 100).round(1).alias(\"Share %\"),\n)\nprint(shares.filter(pl.col(\"Month\") == \"2026-09\"))\n","python",[19,101,102,118,125,160,171,198,204,245,250],{"__ignoreMap":56},[60,103,104,108,112,115],{"class":62,"line":63},[60,105,107],{"class":106},"s-kum","import",[60,109,111],{"class":110},"skGVy"," polars ",[60,113,114],{"class":106},"as",[60,116,117],{"class":110}," pl\n",[60,119,121],{"class":62,"line":120},2,[60,122,124],{"emptyLinePlaceholder":123},true,"\n",[60,126,128,131,134,137,140,143,146,149,152,154,157],{"class":62,"line":127},3,[60,129,130],{"class":110},"df ",[60,132,133],{"class":106},"=",[60,135,136],{"class":110}," pl.read_excel(",[60,138,139],{"class":70},"\"monthly_units.xlsx\"",[60,141,142],{"class":110},").sort(",[60,144,145],{"class":70},"\"Month\"",[60,147,148],{"class":110},", ",[60,150,151],{"class":70},"\"Region\"",[60,153,148],{"class":110},[60,155,156],{"class":70},"\"Product\"",[60,158,159],{"class":110},")\n",[60,161,163,166,168],{"class":62,"line":162},4,[60,164,165],{"class":110},"shares ",[60,167,133],{"class":106},[60,169,170],{"class":110}," df.with_columns(\n",[60,172,174,177,180,183,185,187,189,192,195],{"class":62,"line":173},5,[60,175,176],{"class":110},"    pl.col(",[60,178,179],{"class":70},"\"Units\"",[60,181,182],{"class":110},").sum().over(",[60,184,151],{"class":70},[60,186,148],{"class":110},[60,188,145],{"class":70},[60,190,191],{"class":110},").alias(",[60,193,194],{"class":70},"\"Region total\"",[60,196,197],{"class":110},"),\n",[60,199,201],{"class":62,"line":200},6,[60,202,203],{"class":110},").with_columns(\n",[60,205,207,210,212,215,218,221,223,225,228,232,235,238,240,243],{"class":62,"line":206},7,[60,208,209],{"class":110},"    (pl.col(",[60,211,179],{"class":70},[60,213,214],{"class":110},") ",[60,216,217],{"class":106},"\u002F",[60,219,220],{"class":110}," pl.col(",[60,222,194],{"class":70},[60,224,214],{"class":110},[60,226,227],{"class":106},"*",[60,229,231],{"class":230},"sP0c6"," 100",[60,233,234],{"class":110},").round(",[60,236,237],{"class":230},"1",[60,239,191],{"class":110},[60,241,242],{"class":70},"\"Share %\"",[60,244,197],{"class":110},[60,246,248],{"class":62,"line":247},8,[60,249,159],{"class":110},[60,251,253,256,259,261,263,266,269],{"class":62,"line":252},9,[60,254,255],{"class":230},"print",[60,257,258],{"class":110},"(shares.filter(pl.col(",[60,260,145],{"class":70},[60,262,214],{"class":110},[60,264,265],{"class":106},"==",[60,267,268],{"class":70}," \"2026-09\"",[60,270,271],{"class":110},"))\n",[10,273,274,275,278,279,282,283,285,286,289],{},"Every row keeps its own units and gains the region total and its share; the table still has 24 rows. The ",[19,276,277],{},"over"," partition can be several columns, here region ",[14,280,281],{},"and"," month, so each month's shares add up to 100 within each region. The Excel equivalent is a ",[19,284,21],{}," over the same keys in every row, which recalculates the whole column on every change — fine for hundreds of rows, slow for hundreds of thousands. A pandas ",[19,287,288],{},"groupby().transform(\"sum\")"," does the same job; the comparison is at the end of this page.",[291,292,300,301,300,305,300,309,300,317,300,326,300,330,300,337,300,341,300,345,300,349,300,353,300,355,300,359,300,361,300,364,300,367,300,371,300,373,300,377,300,379,300,382,300,385,300,389,300,391,300,394,300,396,300,399,300,402,300,406,300,408,300,411,300,413,300,416,300,419,300,423,300,428,300,432,300,436,300,440,300,443,300,446,300,450,300,454,300,456,300,459,300,462,300,465,300,467,300,469,300,471,300,474,300,477,300,480,300,482,300,484,300,486,300,489,300,495,300,499,300,505,300,510,300,518],"svg",{"viewBox":293,"role":294,"ariaLabelledBy":295,"xmlns":298,"style":299},"19 0 722 192","img",[296,297],"win-share-t","win-share-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:722px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[302,303,304],"title",{"id":296},"over() repeats a group figure on every row",[306,307,308],"desc",{"id":297},"Three North rows for September with 150, 88 and 70 units. The sum over Region and Month, 308, is repeated on each row, and each row's share is its units divided by that total.",[310,311],"rect",{"x":312,"y":313,"width":314,"height":315,"fill":316},"19","0","722","192","#ffffff",[310,318],{"x":319,"y":320,"width":321,"height":322,"rx":313,"fill":323,"stroke":324,"style":325},"35.5","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[310,327],{"x":328,"y":320,"width":329,"height":322,"rx":313,"fill":323,"stroke":324,"style":325},"67.5","66.7",[331,332,336],"text",{"x":333,"y":334,"style":335},"100.8","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[310,338],{"x":339,"y":320,"width":340,"height":322,"rx":313,"fill":323,"stroke":324,"style":325},"134.2","52.1",[331,342,344],{"x":343,"y":334,"style":335},"160.2","B",[310,346],{"x":319,"y":347,"width":321,"height":348,"rx":313,"fill":323,"stroke":324,"style":325},"64.0","28.0",[331,350,237],{"x":351,"y":352,"style":335},"51.5","83.0",[310,354],{"x":328,"y":347,"width":329,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,356,358],{"x":333,"y":352,"style":357},"font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Product",[310,360],{"x":339,"y":347,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,362,363],{"x":343,"y":352,"style":357},"Units",[310,365],{"x":319,"y":366,"width":321,"height":348,"rx":313,"fill":323,"stroke":324,"style":325},"92.0",[331,368,370],{"x":351,"y":369,"style":335},"111.0","2",[310,372],{"x":328,"y":366,"width":329,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,374,376],{"x":333,"y":369,"style":375},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Drill",[310,378],{"x":339,"y":366,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,380,381],{"x":343,"y":369,"style":375},"150",[310,383],{"x":319,"y":384,"width":321,"height":348,"rx":313,"fill":323,"stroke":324,"style":325},"120.0",[331,386,388],{"x":351,"y":387,"style":335},"139.0","3",[310,390],{"x":328,"y":384,"width":329,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,392,393],{"x":333,"y":387,"style":375},"Saw",[310,395],{"x":339,"y":384,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,397,398],{"x":343,"y":387,"style":375},"88",[310,400],{"x":319,"y":401,"width":321,"height":348,"rx":313,"fill":323,"stroke":324,"style":325},"148.0",[331,403,405],{"x":351,"y":404,"style":335},"167.0","4",[310,407],{"x":328,"y":401,"width":329,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,409,410],{"x":333,"y":404,"style":375},"Hose",[310,412],{"x":339,"y":401,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,414,415],{"x":343,"y":404,"style":375},"70",[310,417],{"x":418,"y":320,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},"514.8",[331,420,363],{"x":421,"y":422,"style":357},"540.9","59.0",[310,424],{"x":425,"y":320,"width":426,"height":348,"rx":313,"fill":323,"stroke":324,"style":427},"567.0","88.2","stroke-width:1.5px",[331,429,431],{"x":430,"y":422,"style":357},"611.1","Region total",[310,433],{"x":434,"y":320,"width":435,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},"655.2","69.4",[331,437,439],{"x":438,"y":422,"style":357},"689.9","Share %",[310,441],{"x":418,"y":442,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},"68.0",[331,444,381],{"x":421,"y":445,"style":375},"87.0",[310,447],{"x":425,"y":442,"width":426,"height":348,"rx":313,"fill":448,"stroke":449,"style":427},"#d9f4f1","var(--teal,#0f9488)",[331,451,453],{"x":430,"y":445,"style":452},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","308",[310,455],{"x":434,"y":442,"width":435,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,457,458],{"x":438,"y":445,"style":375},"48.7",[310,460],{"x":418,"y":461,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},"96.0",[331,463,398],{"x":421,"y":464,"style":375},"115.0",[310,466],{"x":425,"y":461,"width":426,"height":348,"rx":313,"fill":448,"stroke":449,"style":427},[331,468,453],{"x":430,"y":464,"style":452},[310,470],{"x":434,"y":461,"width":435,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,472,473],{"x":438,"y":464,"style":375},"28.6",[310,475],{"x":418,"y":476,"width":340,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},"124.0",[331,478,415],{"x":421,"y":479,"style":375},"143.0",[310,481],{"x":425,"y":476,"width":426,"height":348,"rx":313,"fill":448,"stroke":449,"style":427},[331,483,453],{"x":430,"y":479,"style":452},[310,485],{"x":434,"y":476,"width":435,"height":348,"rx":313,"fill":316,"stroke":324,"style":325},[331,487,488],{"x":438,"y":479,"style":375},"22.7",[331,490,494],{"x":491,"y":492,"style":493},"110.9","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","rows",[331,496,498],{"x":497,"y":492,"style":493},"619.7","with window columns",[62,500],{"x1":501,"y1":461,"x2":502,"y2":461,"stroke":503,"style":504},"196.3","500.8","var(--brand,#5b5cf0)","stroke-width:2px",[506,507],"polygon",{"points":508,"fill":509},"504.8,96.0 495.8,101.0 495.8,91.0","#5b5cf0",[310,511],{"x":512,"y":513,"width":514,"height":515,"rx":516,"fill":316,"stroke":324,"style":517},"235.2","63.0","230.8","22.0","11","stroke-width:1.2px",[331,519,523],{"x":520,"y":521,"style":522},"350.6","78.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","sum().over(\"Region\", \"Month\")",[46,525,527],{"id":526},"step-2-rank-within-a-group","Step 2 — Rank within a group",[10,529,530,531,534],{},"Ranks work the same way: ",[19,532,533],{},"rank"," evaluated over a partition ranks within each group:",[51,536,538],{"className":97,"code":537,"language":99,"meta":56,"style":56},"ranked = df.with_columns(\n    pl.col(\"Units\").rank(\"ordinal\", descending=True).over(\"Region\", \"Month\").alias(\"Rank in region\"),\n    pl.col(\"Units\").rank(\"dense\", descending=True).over(\"Month\").alias(\"Rank overall\"),\n)\nprint(ranked.filter(pl.col(\"Month\") == \"2026-09\").sort(\"Rank overall\"))\n",[19,539,540,549,588,618,622],{"__ignoreMap":56},[60,541,542,545,547],{"class":62,"line":63},[60,543,544],{"class":110},"ranked ",[60,546,133],{"class":106},[60,548,170],{"class":110},[60,550,551,553,555,558,561,563,567,569,572,575,577,579,581,583,586],{"class":62,"line":120},[60,552,176],{"class":110},[60,554,179],{"class":70},[60,556,557],{"class":110},").rank(",[60,559,560],{"class":70},"\"ordinal\"",[60,562,148],{"class":110},[60,564,566],{"class":565},"sa561","descending",[60,568,133],{"class":106},[60,570,571],{"class":230},"True",[60,573,574],{"class":110},").over(",[60,576,151],{"class":70},[60,578,148],{"class":110},[60,580,145],{"class":70},[60,582,191],{"class":110},[60,584,585],{"class":70},"\"Rank in region\"",[60,587,197],{"class":110},[60,589,590,592,594,596,599,601,603,605,607,609,611,613,616],{"class":62,"line":127},[60,591,176],{"class":110},[60,593,179],{"class":70},[60,595,557],{"class":110},[60,597,598],{"class":70},"\"dense\"",[60,600,148],{"class":110},[60,602,566],{"class":565},[60,604,133],{"class":106},[60,606,571],{"class":230},[60,608,574],{"class":110},[60,610,145],{"class":70},[60,612,191],{"class":110},[60,614,615],{"class":70},"\"Rank overall\"",[60,617,197],{"class":110},[60,619,620],{"class":62,"line":162},[60,621,159],{"class":110},[60,623,624,626,629,631,633,635,637,639,641],{"class":62,"line":173},[60,625,255],{"class":230},[60,627,628],{"class":110},"(ranked.filter(pl.col(",[60,630,145],{"class":70},[60,632,214],{"class":110},[60,634,265],{"class":106},[60,636,268],{"class":70},[60,638,142],{"class":110},[60,640,615],{"class":70},[60,642,271],{"class":110},[10,644,645,647,648,650,651,654,655,657,658,661,662,44],{},[19,646,560],{}," gives every row a distinct rank (ties broken by position), ",[19,649,598],{}," gives equal values the same rank with no gaps, and ",[19,652,653],{},"\"min\""," matches Excel's ",[19,656,25],{},", where ties share the lowest rank and the next rank is skipped. Ranking over ",[19,659,660],{},"Month"," alone ranks across both regions; in September North's hoses and South's saws both sold 70, so the dense overall rank gives both a 5, while the ordinal rank within each region still separates every row. Ranks are integers starting at 1, as in Excel. The pandas and Excel ranking options are compared in ",[40,663,665],{"href":664},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frank-and-percentile-formulas-in-pandas\u002F","RANK and PERCENTILE Formulas in pandas",[46,667,669],{"id":668},"step-3-running-totals-and-change-from-the-previous-month","Step 3 — Running totals and change from the previous month",[10,671,672,673,676,677,680,681,684],{},"Running totals and period-on-period changes need the rows ",[14,674,675],{},"in order"," within each group. Sort first, then ",[19,678,679],{},"cum_sum"," and ",[19,682,683],{},"shift"," over the group:",[51,686,688],{"className":97,"code":687,"language":99,"meta":56,"style":56},"trend = df.sort(\"Region\", \"Product\", \"Month\").with_columns(\n    pl.col(\"Units\").cum_sum().over(\"Region\", \"Product\").alias(\"Units YTD\"),\n    (pl.col(\"Units\") - pl.col(\"Units\").shift(1)).over(\"Region\", \"Product\").alias(\"vs prev month\"),\n    (pl.col(\"Units\") \u002F pl.col(\"Units\").shift(1) * 100 - 100).round(1).over(\"Region\", \"Product\").alias(\"Growth %\"),\n)\nprint(trend.filter((pl.col(\"Region\") == \"North\") & (pl.col(\"Product\") == \"Hose\")))\n",[19,689,690,712,734,770,818,822],{"__ignoreMap":56},[60,691,692,695,697,700,702,704,706,708,710],{"class":62,"line":63},[60,693,694],{"class":110},"trend ",[60,696,133],{"class":106},[60,698,699],{"class":110}," df.sort(",[60,701,151],{"class":70},[60,703,148],{"class":110},[60,705,156],{"class":70},[60,707,148],{"class":110},[60,709,145],{"class":70},[60,711,203],{"class":110},[60,713,714,716,718,721,723,725,727,729,732],{"class":62,"line":120},[60,715,176],{"class":110},[60,717,179],{"class":70},[60,719,720],{"class":110},").cum_sum().over(",[60,722,151],{"class":70},[60,724,148],{"class":110},[60,726,156],{"class":70},[60,728,191],{"class":110},[60,730,731],{"class":70},"\"Units YTD\"",[60,733,197],{"class":110},[60,735,736,738,740,742,745,747,749,752,754,757,759,761,763,765,768],{"class":62,"line":127},[60,737,209],{"class":110},[60,739,179],{"class":70},[60,741,214],{"class":110},[60,743,744],{"class":106},"-",[60,746,220],{"class":110},[60,748,179],{"class":70},[60,750,751],{"class":110},").shift(",[60,753,237],{"class":230},[60,755,756],{"class":110},")).over(",[60,758,151],{"class":70},[60,760,148],{"class":110},[60,762,156],{"class":70},[60,764,191],{"class":110},[60,766,767],{"class":70},"\"vs prev month\"",[60,769,197],{"class":110},[60,771,772,774,776,778,780,782,784,786,788,790,792,794,797,799,801,803,805,807,809,811,813,816],{"class":62,"line":162},[60,773,209],{"class":110},[60,775,179],{"class":70},[60,777,214],{"class":110},[60,779,217],{"class":106},[60,781,220],{"class":110},[60,783,179],{"class":70},[60,785,751],{"class":110},[60,787,237],{"class":230},[60,789,214],{"class":110},[60,791,227],{"class":106},[60,793,231],{"class":230},[60,795,796],{"class":106}," -",[60,798,231],{"class":230},[60,800,234],{"class":110},[60,802,237],{"class":230},[60,804,574],{"class":110},[60,806,151],{"class":70},[60,808,148],{"class":110},[60,810,156],{"class":70},[60,812,191],{"class":110},[60,814,815],{"class":70},"\"Growth %\"",[60,817,197],{"class":110},[60,819,820],{"class":62,"line":173},[60,821,159],{"class":110},[60,823,824,826,829,831,833,835,838,840,843,846,848,850,852,855],{"class":62,"line":200},[60,825,255],{"class":230},[60,827,828],{"class":110},"(trend.filter((pl.col(",[60,830,151],{"class":70},[60,832,214],{"class":110},[60,834,265],{"class":106},[60,836,837],{"class":70}," \"North\"",[60,839,214],{"class":110},[60,841,842],{"class":106},"&",[60,844,845],{"class":110}," (pl.col(",[60,847,156],{"class":70},[60,849,214],{"class":110},[60,851,265],{"class":106},[60,853,854],{"class":70}," \"Hose\"",[60,856,857],{"class":110},")))\n",[10,859,860,863,864,867,868,871],{},[19,861,862],{},"shift(1)"," inside the window looks at the previous row ",[14,865,866],{},"of the same group",", so the first month of each product has no previous value (",[19,869,870],{},"null",") rather than borrowing the last month of another product — the most common bug when this is done with a plain shift or a copied-down Excel formula. The cumulative sum restarts for each region and product. Because the result depends on row order, the explicit sort matters; a shuffled export without it produces running totals in a random order.",[10,873,874,875,878],{},"Rather than sorting the whole frame, you can give the window its own ordering with ",[19,876,877],{},"order_by",":",[51,880,882],{"className":97,"code":881,"language":99,"meta":56,"style":56},"same = df.with_columns(\n    pl.col(\"Units\").cum_sum().over(\"Region\", \"Product\", order_by=\"Month\").alias(\"Units YTD\"),\n).sort(\"Region\", \"Product\", \"Month\")\nprint(\"same running totals:\", same[\"Units YTD\"].to_list() == trend[\"Units YTD\"].to_list())\n",[19,883,884,893,921,937],{"__ignoreMap":56},[60,885,886,889,891],{"class":62,"line":63},[60,887,888],{"class":110},"same ",[60,890,133],{"class":106},[60,892,170],{"class":110},[60,894,895,897,899,901,903,905,907,909,911,913,915,917,919],{"class":62,"line":120},[60,896,176],{"class":110},[60,898,179],{"class":70},[60,900,720],{"class":110},[60,902,151],{"class":70},[60,904,148],{"class":110},[60,906,156],{"class":70},[60,908,148],{"class":110},[60,910,877],{"class":565},[60,912,133],{"class":106},[60,914,145],{"class":70},[60,916,191],{"class":110},[60,918,731],{"class":70},[60,920,197],{"class":110},[60,922,923,925,927,929,931,933,935],{"class":62,"line":127},[60,924,142],{"class":110},[60,926,151],{"class":70},[60,928,148],{"class":110},[60,930,156],{"class":70},[60,932,148],{"class":110},[60,934,145],{"class":70},[60,936,159],{"class":110},[60,938,939,941,944,947,950,952,955,957,960,962],{"class":62,"line":162},[60,940,255],{"class":230},[60,942,943],{"class":110},"(",[60,945,946],{"class":70},"\"same running totals:\"",[60,948,949],{"class":110},", same[",[60,951,731],{"class":70},[60,953,954],{"class":110},"].to_list() ",[60,956,265],{"class":106},[60,958,959],{"class":110}," trend[",[60,961,731],{"class":70},[60,963,964],{"class":110},"].to_list())\n",[10,966,967,969,970,973],{},[19,968,877],{}," sorts ",[14,971,972],{},"within"," each window for the calculation and puts the results back in the frame's original row order. The figures are identical to the sorted version. This is handy when the table must keep its existing order — the order of an export someone will compare against — while the calculation needs time order.",[291,975,300,981,300,984,300,987,300,991,300,1000,300,1011,300,1016,300,1021,300,1025,300,1029,300,1033,300,1036,300,1043,300,1048,300,1052,300,1055,300,1058],{"viewBox":976,"role":294,"ariaLabelledBy":977,"xmlns":298,"style":980},"4 0 752 207",[978,979],"win-shift-t","win-shift-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[302,982,983],{"id":978},"A plain shift versus a shift over the group",[306,985,986],{"id":979},"A plain shift on a table sorted by product and month takes the previous row, so the first month of Saw takes the last month of Hose. A shift over Region and Product looks only within the group, so each product's first month has no previous value.",[310,988],{"x":405,"y":313,"width":989,"height":990,"fill":316},"752","207",[310,992],{"x":993,"y":994,"width":995,"height":996,"rx":997,"fill":998,"stroke":999,"style":504},"20.0","16.0","351.0","175.0","14","#fce9e9","var(--danger,#dc2626)",[1001,1002,1004],"g",{"transform":1003},"translate(182.5,29.0) scale(1.083)",[1005,1006],"path",{"d":1007,"fill":1008,"stroke":1009,"style":1010},"M6 6l12 12 M18 6L6 18","none","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[331,1012,1015],{"x":1013,"y":521,"style":1014},"195.5","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","pl.col('Units').shift(1)",[62,1017],{"x1":1018,"y1":1019,"x2":1020,"y2":1019,"stroke":999,"style":325},"36.0","91.0","355.0",[331,1022,1024],{"x":1013,"y":369,"style":1023},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","previous row of the table",[331,1026,1028],{"x":1013,"y":1027,"style":1023},"137.0","Saw June 'vs prev' uses Hose September",[331,1030,1032],{"x":1013,"y":1031,"style":1023},"163.0","silent nonsense at group edges",[310,1034],{"x":1035,"y":994,"width":995,"height":996,"rx":997,"fill":448,"stroke":449,"style":504},"389.0",[1001,1037,1039],{"transform":1038},"translate(551.5,29.0) scale(1.083)",[1005,1040],{"d":1041,"fill":1008,"stroke":1042,"style":1010},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[331,1044,1047],{"x":1045,"y":521,"style":1046},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","….shift(1).over('Region', 'Product')",[62,1049],{"x1":1050,"y1":1019,"x2":1051,"y2":1019,"stroke":449,"style":325},"405.0","724.0",[331,1053,1054],{"x":1045,"y":369,"style":1023},"previous row in the same group",[331,1056,1057],{"x":1045,"y":1027,"style":1023},"first month of each product is null",[331,1059,1060],{"x":1045,"y":1031,"style":1023},"order with sort or order_by",[46,1062,1064],{"id":1063},"step-4-top-n-per-group-with-a-window-filter","Step 4 — Top N per group with a window filter",[10,1066,1067,1068,1071],{},"Windows also work inside ",[19,1069,1070],{},"filter",", which turns \"the best-selling product per region each month\" into one line:",[51,1073,1075],{"className":97,"code":1074,"language":99,"meta":56,"style":56},"top = df.filter(pl.col(\"Units\") == pl.col(\"Units\").max().over(\"Region\", \"Month\"))\nprint(top.sort(\"Month\", \"Region\"))\n\ntop2 = df.filter(pl.col(\"Units\").rank(\"ordinal\", descending=True).over(\"Region\", \"Month\") \u003C= 2)\nprint(\"top two per region and month:\", top2.height, \"rows\")\n",[19,1076,1077,1108,1123,1127,1168],{"__ignoreMap":56},[60,1078,1079,1082,1084,1087,1089,1091,1093,1095,1097,1100,1102,1104,1106],{"class":62,"line":63},[60,1080,1081],{"class":110},"top ",[60,1083,133],{"class":106},[60,1085,1086],{"class":110}," df.filter(pl.col(",[60,1088,179],{"class":70},[60,1090,214],{"class":110},[60,1092,265],{"class":106},[60,1094,220],{"class":110},[60,1096,179],{"class":70},[60,1098,1099],{"class":110},").max().over(",[60,1101,151],{"class":70},[60,1103,148],{"class":110},[60,1105,145],{"class":70},[60,1107,271],{"class":110},[60,1109,1110,1112,1115,1117,1119,1121],{"class":62,"line":120},[60,1111,255],{"class":230},[60,1113,1114],{"class":110},"(top.sort(",[60,1116,145],{"class":70},[60,1118,148],{"class":110},[60,1120,151],{"class":70},[60,1122,271],{"class":110},[60,1124,1125],{"class":62,"line":127},[60,1126,124],{"emptyLinePlaceholder":123},[60,1128,1129,1132,1134,1136,1138,1140,1142,1144,1146,1148,1150,1152,1154,1156,1158,1160,1163,1166],{"class":62,"line":162},[60,1130,1131],{"class":110},"top2 ",[60,1133,133],{"class":106},[60,1135,1086],{"class":110},[60,1137,179],{"class":70},[60,1139,557],{"class":110},[60,1141,560],{"class":70},[60,1143,148],{"class":110},[60,1145,566],{"class":565},[60,1147,133],{"class":106},[60,1149,571],{"class":230},[60,1151,574],{"class":110},[60,1153,151],{"class":70},[60,1155,148],{"class":110},[60,1157,145],{"class":70},[60,1159,214],{"class":110},[60,1161,1162],{"class":106},"\u003C=",[60,1164,1165],{"class":230}," 2",[60,1167,159],{"class":110},[60,1169,1170,1172,1174,1177,1180,1183],{"class":62,"line":173},[60,1171,255],{"class":230},[60,1173,943],{"class":110},[60,1175,1176],{"class":70},"\"top two per region and month:\"",[60,1178,1179],{"class":110},", top2.height, ",[60,1181,1182],{"class":70},"\"rows\"",[60,1184,159],{"class":110},[10,1186,1187,1188,44],{},"Comparing each row with its group's maximum keeps the winners (ties would keep all tied rows); filtering on a rank keeps the top N. There is no intermediate table and no join back — the window does the grouping inside the filter. The pandas version of this is in ",[40,1189,1191],{"href":1190},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fget-the-top-n-rows-per-group-in-pandas\u002F","Get the Top N Rows per Group in pandas",[46,1193,1195],{"id":1194},"windows-that-return-more-than-one-value-per-group","Windows that return more than one value per group",[10,1197,1198,1199,1202,1203,1206,1207,1210,1211,1214,1215,1218,1219,1221],{},"So far each window expression returned either one value per group (a sum, a maximum) or one value per row (a rank, a running total). Polars handles both automatically: a single group value is repeated on every row, a per-row result is put back in place. A third kind — an expression that returns a ",[14,1200,1201],{},"list"," or a different number of values per group — needs a decision, made with ",[19,1204,1205],{},"mapping_strategy",". The default, ",[19,1208,1209],{},"\"group_to_rows\"",", requires one result per row; ",[19,1212,1213],{},"\"join\""," stores the group's whole result as a list on every row; ",[19,1216,1217],{},"\"explode\""," returns one row per result and changes the frame's length. The common use of ",[19,1220,1213],{}," on spreadsheet data is attaching a group's full set of values, such as a product's monthly series, to each of its rows for later comparison:",[51,1223,1225],{"className":97,"code":1224,"language":99,"meta":56,"style":56},"series = df.sort(\"Month\").with_columns(\n    pl.col(\"Units\").over(\"Region\", \"Product\", mapping_strategy=\"join\").alias(\"All months\"),\n)\nprint(series.filter((pl.col(\"Region\") == \"South\") & (pl.col(\"Product\") == \"Saw\")).select(\"Month\", \"Units\", \"All months\"))\n",[19,1226,1227,1240,1269,1273],{"__ignoreMap":56},[60,1228,1229,1232,1234,1236,1238],{"class":62,"line":63},[60,1230,1231],{"class":110},"series ",[60,1233,133],{"class":106},[60,1235,699],{"class":110},[60,1237,145],{"class":70},[60,1239,203],{"class":110},[60,1241,1242,1244,1246,1248,1250,1252,1254,1256,1258,1260,1262,1264,1267],{"class":62,"line":120},[60,1243,176],{"class":110},[60,1245,179],{"class":70},[60,1247,574],{"class":110},[60,1249,151],{"class":70},[60,1251,148],{"class":110},[60,1253,156],{"class":70},[60,1255,148],{"class":110},[60,1257,1205],{"class":565},[60,1259,133],{"class":106},[60,1261,1213],{"class":70},[60,1263,191],{"class":110},[60,1265,1266],{"class":70},"\"All months\"",[60,1268,197],{"class":110},[60,1270,1271],{"class":62,"line":127},[60,1272,159],{"class":110},[60,1274,1275,1277,1280,1282,1284,1286,1289,1291,1293,1295,1297,1299,1301,1304,1307,1309,1311,1313,1315,1317],{"class":62,"line":162},[60,1276,255],{"class":230},[60,1278,1279],{"class":110},"(series.filter((pl.col(",[60,1281,151],{"class":70},[60,1283,214],{"class":110},[60,1285,265],{"class":106},[60,1287,1288],{"class":70}," \"South\"",[60,1290,214],{"class":110},[60,1292,842],{"class":106},[60,1294,845],{"class":110},[60,1296,156],{"class":70},[60,1298,214],{"class":110},[60,1300,265],{"class":106},[60,1302,1303],{"class":70}," \"Saw\"",[60,1305,1306],{"class":110},")).select(",[60,1308,145],{"class":70},[60,1310,148],{"class":110},[60,1312,179],{"class":70},[60,1314,148],{"class":110},[60,1316,1266],{"class":70},[60,1318,271],{"class":110},[10,1320,1321],{},"Each South saw row now carries the list of all four months' units. That is rarely what a report shows directly, but it makes calculations such as \"this month versus the product's best month\" a list operation instead of a self-join. For most Excel-style work, though, the default strategy is the right one, and Polars raises an error rather than guessing when the shapes do not fit.",[46,1323,1325],{"id":1324},"step-5-the-pandas-equivalents","Step 5 — The pandas equivalents",[10,1327,1328,1329,1332,1333,217,1336,217,1338,1340],{},"Every calculation above has a pandas counterpart with ",[19,1330,1331],{},"groupby().transform()"," or grouped ",[19,1334,1335],{},"cumsum",[19,1337,683],{},[19,1339,533],{},". Putting them side by side confirms the results match:",[51,1342,1344],{"className":97,"code":1343,"language":99,"meta":56,"style":56},"import pandas as pd\n\np = pd.read_excel(\"monthly_units.xlsx\").sort_values([\"Region\", \"Product\", \"Month\"])\np[\"Region total\"] = p.groupby([\"Region\", \"Month\"])[\"Units\"].transform(\"sum\")\np[\"Units YTD\"] = p.groupby([\"Region\", \"Product\"])[\"Units\"].cumsum()\np[\"vs prev month\"] = p.groupby([\"Region\", \"Product\"])[\"Units\"].diff()\n\npl_check = trend.join(shares.select(\"Month\", \"Region\", \"Product\", \"Region total\"), on=[\"Month\", \"Region\", \"Product\"])\nmerged = p.merge(pl_check.to_pandas(), on=[\"Month\", \"Region\", \"Product\"], suffixes=(\"_pd\", \"_pl\"))\nfor col in [\"Region total\", \"Units YTD\", \"vs prev month\"]:\n    same = (merged[f\"{col}_pd\"].fillna(-1) == merged[f\"{col}_pl\"].fillna(-1)).all()\n    print(f\"{col:14} pandas == polars: {same}\")\n",[19,1345,1346,1358,1362,1390,1424,1449,1474,1478,1525,1571,1599,1665],{"__ignoreMap":56},[60,1347,1348,1350,1353,1355],{"class":62,"line":63},[60,1349,107],{"class":106},[60,1351,1352],{"class":110}," pandas ",[60,1354,114],{"class":106},[60,1356,1357],{"class":110}," pd\n",[60,1359,1360],{"class":62,"line":120},[60,1361,124],{"emptyLinePlaceholder":123},[60,1363,1364,1367,1369,1372,1374,1377,1379,1381,1383,1385,1387],{"class":62,"line":127},[60,1365,1366],{"class":110},"p ",[60,1368,133],{"class":106},[60,1370,1371],{"class":110}," pd.read_excel(",[60,1373,139],{"class":70},[60,1375,1376],{"class":110},").sort_values([",[60,1378,151],{"class":70},[60,1380,148],{"class":110},[60,1382,156],{"class":70},[60,1384,148],{"class":110},[60,1386,145],{"class":70},[60,1388,1389],{"class":110},"])\n",[60,1391,1392,1395,1397,1400,1402,1405,1407,1409,1411,1414,1416,1419,1422],{"class":62,"line":162},[60,1393,1394],{"class":110},"p[",[60,1396,194],{"class":70},[60,1398,1399],{"class":110},"] ",[60,1401,133],{"class":106},[60,1403,1404],{"class":110}," p.groupby([",[60,1406,151],{"class":70},[60,1408,148],{"class":110},[60,1410,145],{"class":70},[60,1412,1413],{"class":110},"])[",[60,1415,179],{"class":70},[60,1417,1418],{"class":110},"].transform(",[60,1420,1421],{"class":70},"\"sum\"",[60,1423,159],{"class":110},[60,1425,1426,1428,1430,1432,1434,1436,1438,1440,1442,1444,1446],{"class":62,"line":173},[60,1427,1394],{"class":110},[60,1429,731],{"class":70},[60,1431,1399],{"class":110},[60,1433,133],{"class":106},[60,1435,1404],{"class":110},[60,1437,151],{"class":70},[60,1439,148],{"class":110},[60,1441,156],{"class":70},[60,1443,1413],{"class":110},[60,1445,179],{"class":70},[60,1447,1448],{"class":110},"].cumsum()\n",[60,1450,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469,1471],{"class":62,"line":200},[60,1452,1394],{"class":110},[60,1454,767],{"class":70},[60,1456,1399],{"class":110},[60,1458,133],{"class":106},[60,1460,1404],{"class":110},[60,1462,151],{"class":70},[60,1464,148],{"class":110},[60,1466,156],{"class":70},[60,1468,1413],{"class":110},[60,1470,179],{"class":70},[60,1472,1473],{"class":110},"].diff()\n",[60,1475,1476],{"class":62,"line":206},[60,1477,124],{"emptyLinePlaceholder":123},[60,1479,1480,1483,1485,1488,1490,1492,1494,1496,1498,1500,1502,1505,1508,1510,1513,1515,1517,1519,1521,1523],{"class":62,"line":247},[60,1481,1482],{"class":110},"pl_check ",[60,1484,133],{"class":106},[60,1486,1487],{"class":110}," trend.join(shares.select(",[60,1489,145],{"class":70},[60,1491,148],{"class":110},[60,1493,151],{"class":70},[60,1495,148],{"class":110},[60,1497,156],{"class":70},[60,1499,148],{"class":110},[60,1501,194],{"class":70},[60,1503,1504],{"class":110},"), ",[60,1506,1507],{"class":565},"on",[60,1509,133],{"class":106},[60,1511,1512],{"class":110},"[",[60,1514,145],{"class":70},[60,1516,148],{"class":110},[60,1518,151],{"class":70},[60,1520,148],{"class":110},[60,1522,156],{"class":70},[60,1524,1389],{"class":110},[60,1526,1527,1530,1532,1535,1537,1539,1541,1543,1545,1547,1549,1551,1554,1557,1559,1561,1564,1566,1569],{"class":62,"line":252},[60,1528,1529],{"class":110},"merged ",[60,1531,133],{"class":106},[60,1533,1534],{"class":110}," p.merge(pl_check.to_pandas(), ",[60,1536,1507],{"class":565},[60,1538,133],{"class":106},[60,1540,1512],{"class":110},[60,1542,145],{"class":70},[60,1544,148],{"class":110},[60,1546,151],{"class":70},[60,1548,148],{"class":110},[60,1550,156],{"class":70},[60,1552,1553],{"class":110},"], ",[60,1555,1556],{"class":565},"suffixes",[60,1558,133],{"class":106},[60,1560,943],{"class":110},[60,1562,1563],{"class":70},"\"_pd\"",[60,1565,148],{"class":110},[60,1567,1568],{"class":70},"\"_pl\"",[60,1570,271],{"class":110},[60,1572,1574,1577,1580,1583,1586,1588,1590,1592,1594,1596],{"class":62,"line":1573},10,[60,1575,1576],{"class":106},"for",[60,1578,1579],{"class":110}," col ",[60,1581,1582],{"class":106},"in",[60,1584,1585],{"class":110}," [",[60,1587,194],{"class":70},[60,1589,148],{"class":110},[60,1591,731],{"class":70},[60,1593,148],{"class":110},[60,1595,767],{"class":70},[60,1597,1598],{"class":110},"]:\n",[60,1600,1602,1605,1607,1610,1613,1616,1620,1623,1626,1629,1632,1634,1636,1638,1640,1643,1645,1647,1649,1651,1653,1656,1658,1660,1662],{"class":62,"line":1601},11,[60,1603,1604],{"class":110},"    same ",[60,1606,133],{"class":106},[60,1608,1609],{"class":110}," (merged[",[60,1611,1612],{"class":106},"f",[60,1614,1615],{"class":70},"\"",[60,1617,1619],{"class":1618},"sSjpA","{",[60,1621,1622],{"class":110},"col",[60,1624,1625],{"class":1618},"}",[60,1627,1628],{"class":70},"_pd\"",[60,1630,1631],{"class":110},"].fillna(",[60,1633,744],{"class":106},[60,1635,237],{"class":230},[60,1637,214],{"class":110},[60,1639,265],{"class":106},[60,1641,1642],{"class":110}," merged[",[60,1644,1612],{"class":106},[60,1646,1615],{"class":70},[60,1648,1619],{"class":1618},[60,1650,1622],{"class":110},[60,1652,1625],{"class":1618},[60,1654,1655],{"class":70},"_pl\"",[60,1657,1631],{"class":110},[60,1659,744],{"class":106},[60,1661,237],{"class":230},[60,1663,1664],{"class":110},")).all()\n",[60,1666,1668,1671,1673,1675,1677,1679,1681,1684,1686,1689,1691,1694,1696,1698],{"class":62,"line":1667},12,[60,1669,1670],{"class":230},"    print",[60,1672,943],{"class":110},[60,1674,1612],{"class":106},[60,1676,1615],{"class":70},[60,1678,1619],{"class":1618},[60,1680,1622],{"class":110},[60,1682,1683],{"class":106},":14",[60,1685,1625],{"class":1618},[60,1687,1688],{"class":70}," pandas == polars: ",[60,1690,1619],{"class":1618},[60,1692,1693],{"class":110},"same",[60,1695,1625],{"class":1618},[60,1697,1615],{"class":70},[60,1699,159],{"class":110},[10,1701,1702,1705,1706,1708,1709,680,1711,1714,1715,1718,1719,1722,1723,44],{},[19,1703,1704],{},"transform"," broadcasts a group result like ",[19,1707,277],{},"; grouped ",[19,1710,1335],{},[19,1712,1713],{},"diff"," correspond to ",[19,1716,1717],{},"cum_sum().over()"," and the shifted difference. The results agree column for column. The Polars versions read as one expression per column inside a single ",[19,1720,1721],{},"with_columns",", which keeps longer pipelines tidy, and run in parallel across columns. For a full comparison of the two libraries on Excel work, see ",[40,1724,1726],{"href":1725},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpandas-vs-polars-for-excel-workflows\u002F","pandas vs Polars for Excel Workflows",[291,1728,300,1733,300,1736,300,1739,300,1742,300,1745,300,1750,300,1756,300,1759,300,1763,300,1766,300,1770,300,1774,300,1778,300,1780,300,1785,300,1787,300,1790,300,1792,300,1795,300,1798,300,1802,300,1804,300,1807,300,1809,300,1812,300,1814,300,1817,300,1819,300,1823,300,1825,300,1829,300,1831,300,1834,300,1836,300,1838,300,1841,300,1845,300,1847,300,1851,300,1853,300,1856,300,1858],{"viewBox":1729,"role":294,"ariaLabelledBy":1730,"xmlns":298,"style":980},"4 0 752 208",[1731,1732],"win-map-t","win-map-d",[302,1734,1735],{"id":1731},"Window calculations in Excel, pandas and Polars",[306,1737,1738],{"id":1732},"Share of group total is SUMIFS in Excel, transform sum in pandas and sum over in Polars. Rank within group is RANK with helper ranges, groupby rank and rank over. Running total is an anchored SUM, groupby cumsum and cum_sum over. Previous period change is an offset formula, groupby diff and shift over.",[310,1740],{"x":405,"y":313,"width":989,"height":1741,"fill":316},"208",[310,1743],{"x":993,"y":994,"width":1744,"height":320,"rx":313,"fill":316,"stroke":324,"style":325},"121.9",[310,1746],{"x":1747,"y":994,"width":1748,"height":320,"rx":313,"fill":1749,"stroke":503,"style":517},"141.9","199.4","#f0f4ff",[331,1751,1755],{"x":1752,"y":1753,"style":1754},"241.6","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Excel",[310,1757],{"x":1758,"y":994,"width":1748,"height":320,"rx":313,"fill":1749,"stroke":503,"style":517},"341.3",[331,1760,1762],{"x":1761,"y":1753,"style":1754},"441.0","pandas",[310,1764],{"x":1765,"y":994,"width":1748,"height":320,"rx":313,"fill":1749,"stroke":503,"style":517},"540.6",[331,1767,1769],{"x":1768,"y":1753,"style":1754},"640.3","Polars",[310,1771],{"x":993,"y":1772,"width":1744,"height":1773,"rx":313,"fill":323,"stroke":324,"style":325},"56.0","34.0",[331,1775,1777],{"x":321,"y":521,"style":1776},"font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","share of group",[310,1779],{"x":1747,"y":1772,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1781,1784],{"x":1752,"y":1782,"style":1783},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","x \u002F SUMIFS(…)",[310,1786],{"x":1758,"y":1772,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1788,1789],{"x":1761,"y":1782,"style":1783},"transform('sum')",[310,1791],{"x":1765,"y":1772,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1793,1794],{"x":1768,"y":1782,"style":1783},"sum().over()",[310,1796],{"x":993,"y":1797,"width":1744,"height":1773,"rx":313,"fill":323,"stroke":324,"style":325},"90.0",[331,1799,1801],{"x":321,"y":1800,"style":1776},"112.0","rank in group",[310,1803],{"x":1747,"y":1797,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1805,1806],{"x":1752,"y":369,"style":1783},"RANK + helpers",[310,1808],{"x":1758,"y":1797,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1810,1811],{"x":1761,"y":369,"style":1783},"groupby().rank()",[310,1813],{"x":1765,"y":1797,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1815,1816],{"x":1768,"y":369,"style":1783},"rank().over()",[310,1818],{"x":993,"y":476,"width":1744,"height":1773,"rx":313,"fill":323,"stroke":324,"style":325},[331,1820,1822],{"x":321,"y":1821,"style":1776},"146.0","running total",[310,1824],{"x":1747,"y":476,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1826,1828],{"x":1752,"y":1827,"style":1783},"145.0","SUM($B$2:B2)",[310,1830],{"x":1758,"y":476,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1832,1833],{"x":1761,"y":1827,"style":1783},"groupby().cumsum()",[310,1835],{"x":1765,"y":476,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1837,1717],{"x":1768,"y":1827,"style":1783},[310,1839],{"x":993,"y":1840,"width":1744,"height":1773,"rx":313,"fill":323,"stroke":324,"style":325},"158.0",[331,1842,1844],{"x":321,"y":1843,"style":1776},"180.0","vs previous",[310,1846],{"x":1747,"y":1840,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1848,1850],{"x":1752,"y":1849,"style":1783},"179.0","B3 - B2 per block",[310,1852],{"x":1758,"y":1840,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1854,1855],{"x":1761,"y":1849,"style":1783},"groupby().diff()",[310,1857],{"x":1765,"y":1840,"width":1748,"height":1773,"rx":313,"fill":316,"stroke":324,"style":325},[331,1859,1860],{"x":1768,"y":1849,"style":1783},"shift(1).over()",[46,1862,1864],{"id":1863},"common-pitfalls","Common pitfalls",[1866,1867,1868,1881,1890,1905,1911],"ul",{},[1869,1870,1871,1875,1876,1878,1879,44],"li",{},[1872,1873,1874],"strong",{},"Unsorted data for running totals and shifts."," Sort, or pass ",[19,1877,877],{}," to ",[19,1880,277],{},[1869,1882,1883,1889],{},[1872,1884,1885,1886,1888],{},"A plain ",[19,1887,683],{}," across groups."," Put it inside the window so groups do not leak into each other.",[1869,1891,1892,1898,1899,1901,1902,1904],{},[1872,1893,1894,1895,1897],{},"Using ",[19,1896,29],{}," when rows must stay."," ",[19,1900,29],{}," collapses; ",[19,1903,277],{}," broadcasts.",[1869,1906,1907,1910],{},[1872,1908,1909],{},"Averaging shares."," Recompute shares from totals rather than averaging percentages.",[1869,1912,1913,1916,1917,1920,1921,1923],{},[1872,1914,1915],{},"Expecting Excel's rank ties."," Choose the rank method (",[19,1918,1919],{},"min"," matches ",[19,1922,25],{},").",[46,1925,1927],{"id":1926},"conclusion","Conclusion",[10,1929,1930,1931,1934,1935,1937,1938,1940,1941,1943,1944,1946,1947,1949,1950,1952,1953,1955],{},"Polars window functions — any expression followed by ",[19,1932,1933],{},".over(keys)"," — compute per group and write the result onto every row: shares of a group total with ",[19,1936,1794],{},", ranks with ",[19,1939,1816],{},", running totals with ",[19,1942,1717],{},", changes from the previous period with ",[19,1945,1860],{},", and top-N filters by using a window inside ",[19,1948,1070],{},". Sort the data or pass ",[19,1951,877],{}," whenever the result depends on order, and remember that pandas' ",[19,1954,1331],{}," family gives the same results when you need them there.",[46,1957,1959],{"id":1958},"frequently-asked-questions","Frequently asked questions",[10,1961,1962,1965],{},[1872,1963,1964],{},"What does over() do in Polars?","\nIt evaluates the expression separately for each group defined by the given columns and maps the result back to every row of that group, so the DataFrame keeps all its rows.",[10,1967,1968,1971],{},[1872,1969,1970],{},"How do I calculate percent of group total in Polars?","\nDivide the column by its sum over the group: take the column, call sum, then over with the group columns, and divide the original column by that.",[10,1973,1974,1977],{},[1872,1975,1976],{},"How do I compute a running total per group in Polars?","\nUse cum_sum().over(group columns) on data sorted by date, or pass order_by=\"Month\" to over so the window is ordered internally.",[10,1979,1980,1983],{},[1872,1981,1982],{},"How do I keep the top row per group in Polars?","\nFilter where the value equals its maximum over the group, or where its rank over the group is at most N.",[46,1985,1987],{"id":1986},"related","Related",[1866,1989,1990,1996,2003,2010],{},[1869,1991,1992,1993,1995],{},"Up one level: ",[40,1994,43],{"href":42}," — fast Excel reading with Polars.",[1869,1997,1998,2002],{},[40,1999,2001],{"href":2000},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas\u002F","Calculate Percent of Total per Group in pandas"," — the pandas version of Step 1.",[1869,2004,2005,2009],{},[40,2006,2008],{"href":2007},"\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"," — the pandas version of Step 3.",[1869,2011,2012,2016],{},[40,2013,2015],{"href":2014},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fclean-wide-excel-sheets-with-polars-column-selectors\u002F","Clean Wide Excel Sheets with Polars Column Selectors"," — more Polars expressions for spreadsheets.",[2018,2019,2020],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":56,"searchDepth":120,"depth":120,"links":2022},[2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033],{"id":48,"depth":120,"text":49},{"id":86,"depth":120,"text":87},{"id":526,"depth":120,"text":527},{"id":668,"depth":120,"text":669},{"id":1063,"depth":120,"text":1064},{"id":1194,"depth":120,"text":1195},{"id":1324,"depth":120,"text":1325},{"id":1863,"depth":120,"text":1864},{"id":1926,"depth":120,"text":1927},{"id":1958,"depth":120,"text":1959},{"id":1986,"depth":120,"text":1987},"2026-10-02","Add group-level figures to every row of an Excel table without collapsing it: Polars over() for share of group total, rank within group, running totals, month-on-month change and top N per group, the order_by and mapping_strategy options that control how windows behave, and the same calculations side by side with pandas transform.","md",[2038,2040,2042,2044],{"q":1964,"a":2039},"It evaluates the expression separately for each group defined by the given columns and maps the result back to every row of that group, so the DataFrame keeps all its rows.",{"q":1970,"a":2041},"Divide the column by its sum over the group: take the column, call sum, then over with the group columns, and divide the original column by that.",{"q":1976,"a":2043},"Use cum_sum().over(group columns) on data sorted by date, or pass order_by=\"Month\" to over so the window is ordered internally.",{"q":1982,"a":2045},"Filter where the value equals its maximum over the group, or where its rank over the group is at most N.","use polars window functions on excel data",null,{"breadcrumb":2049},[2050,2052,2055],{"name":2051,"item":217},"Home",{"name":2053,"item":2054},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":43,"item":42},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data",{"title":2058,"description":2059},"Polars Window Functions with over() on Excel Data","Use Polars over() on Excel data: percent of group total, rank within group, cumulative sums, previous-period change with shift, top N per group, order_by inside windows, and pandas transform equivalents.","use-polars-window-functions-on-excel-data","advanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fuse-polars-window-functions-on-excel-data\u002Findex",[2063,2064],"polars","pivots-and-summaries","how-to","r5N_N6_NuQM7lXxNz2yBF-ZODg7Objlgq7Fm6daO1rY",[2068,2072],{"title":2069,"path":2070,"stem":2071,"children":-1},"Speed Up pandas Excel Reads with the calamine Engine","\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fspeed-up-pandas-excel-reads-with-the-calamine-engine","advanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fspeed-up-pandas-excel-reads-with-the-calamine-engine\u002Findex",{"title":2073,"path":2074,"stem":2075,"children":-1},"Write a Polars DataFrame to Excel with Formatting","\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fwrite-a-polars-dataframe-to-excel-with-formatting","advanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fwrite-a-polars-dataframe-to-excel-with-formatting\u002Findex",[2077,2079],{"path":2078,"title":2053},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2080,"title":43},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow",[],[2083,2086,2088,2091,2095,2098],{"path":2084,"title":2085,"kind":2065},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fclean-and-aggregate-excel-data-with-polars","Clean and Aggregate Excel Data with Polars Expressions",{"path":2087,"title":2015,"kind":2065},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fclean-wide-excel-sheets-with-polars-column-selectors",{"path":2089,"title":2090,"kind":2065},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-pandas-excel-code-to-polars","Convert pandas Excel Code to Polars",{"path":2092,"title":2093,"kind":2094},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Ffix-schema-and-type-errors-when-reading-excel-with-polars","Fix Schema and Type Errors When Reading Excel with Polars","fix",{"path":2096,"title":2097,"kind":2065},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fjoin-and-pivot-excel-data-with-polars","Join and Pivot Excel Data with Polars",{"path":2099,"title":2100,"kind":2065},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-an-excel-file-with-polars-read-excel","Read an Excel File with polars.read_excel",1790972098372]