[{"data":1,"prerenderedAt":2910},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas":2901},{"id":4,"title":5,"body":6,"dateModified":2876,"datePublished":2876,"description":2877,"extension":2878,"faq":2879,"meta":2892,"navigation":273,"path":2893,"seo":2894,"slug":2897,"stem":2898,"type":2899,"__hash__":2900},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002Findex.md","Group Excel Rows by Month and Quarter with pandas",{"type":7,"value":8,"toc":2862},"minimark",[9,19,206,211,242,245,459,463,470,613,620,624,631,753,764,839,842,941,959,963,966,1053,1063,1216,1224,1228,1231,1318,1329,1426,1561,1564,1628,1638,1642,1645,1762,1769,1773,1862,1865,2160,2167,2171,2332,2336,2343,2346,2360,2396,2405,2499,2505,2681,2684,2688,2707,2711,2749,2765,2782,2804,2818,2822,2858],[10,11,12,13,18],"p",{},"Almost every recurring Excel report is the same shape: a sheet of daily transactions in, a table of monthly or quarterly totals out. pandas does this in one expression, but three details separate a summary that is correct from one that quietly misleads — periods with no rows disappearing, fiscal years that do not start in January, and month labels that sort alphabetically so October comes before September. This guide covers the grouping mechanics and the write-back. It is the aggregation companion to ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","Working with Dates and Times in Excel Data",".",[20,21,30,31,30,35,30,39,30,46,30,53,30,57,30,65,30,70,30,73,30,77,30,80,30,84,30,89,30,93,30,96,30,100,30,105,30,121,30,126,30,134,30,138,30,147,30,153,30,157,30,161,30,166,30,170,30,178,30,184,30,189,30,193,30,195,30,198,30,202],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 250","img","Daily rows collapsing into monthly buckets: many dated transactions are assigned to month-start keys and aggregated into one row per month with a sum and a count.",[26,27],"grp-t","grp-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"Daily transactions collapsing into monthly totals",[36,37,38],"desc",{"id":27},"On the left, a column of individual dated transactions across June, July and August. Each row is assigned a month-start key by the Grouper. On the right, three summary rows — one per month — each carrying a revenue sum and an order count. July has no rows in the source, so it only appears in the summary because asfreq filled it with zero.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","250","#ffffff",[47,48,52],"text",{"x":49,"y":50,"style":51},"120","26","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","daily rows",[47,54,56],{"x":55,"y":50,"style":51},"620","one row per month",[40,58],{"x":59,"y":60,"width":61,"height":50,"rx":62,"fill":63,"stroke":64},"20","38","200","6","#ebebfd","var(--brand,#5b5cf0)",[47,66,69],{"x":49,"y":67,"style":68},"56","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","2026-06-03 · 159.92",[40,71],{"x":59,"y":72,"width":61,"height":50,"rx":62,"fill":63,"stroke":64},"68",[47,74,76],{"x":49,"y":75,"style":68},"86","2026-06-19 · 247.50",[40,78],{"x":59,"y":79,"width":61,"height":50,"rx":62,"fill":63,"stroke":64},"98",[47,81,83],{"x":49,"y":82,"style":68},"116","2026-06-28 · 137.44",[40,85],{"x":59,"y":86,"width":61,"height":50,"rx":62,"fill":87,"stroke":88},"140","#d9f4f1","var(--teal,#0f9488)",[47,90,92],{"x":49,"y":91,"style":68},"158","2026-08-02 · 412.10",[40,94],{"x":59,"y":95,"width":61,"height":50,"rx":62,"fill":87,"stroke":88},"170",[47,97,99],{"x":49,"y":98,"style":68},"188","2026-08-15 · 96.35",[47,101,104],{"x":49,"y":102,"style":103},"220","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","nothing at all in July",[106,107,110,111,110,115,110,118,30],"g",{"stroke":64,"style":108,"fill":109},"stroke-width:2px","none","\n    ",[112,113],"path",{"d":114},"M220 51 H 268 V 76 H 306",[112,116],{"d":117},"M220 81 H 268 V 76",[112,119],{"d":120},"M220 111 H 268 V 76",[122,123],"polygon",{"points":124,"fill":125},"314,76 302,70 302,82","#5b5cf0",[106,127,110,128,110,131,30],{"stroke":88,"style":108,"fill":109},[112,129],{"d":130},"M220 153 H 268 V 186 H 306",[112,132],{"d":133},"M220 183 H 268 V 186",[122,135],{"points":136,"fill":137},"314,186 302,180 302,192","#0f9488",[40,139],{"x":140,"y":141,"width":142,"height":143,"rx":144,"fill":145,"stroke":146,"style":108},"322","60","164","152","12","#fdefd8","var(--gold,#b4740a)",[47,148,152],{"x":149,"y":150,"style":151},"404","112","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Grouper(freq=\"MS\")",[47,154,156],{"x":149,"y":155,"style":68},"136","assigns a month-start",[47,158,160],{"x":149,"y":159,"style":68},"154","key to every row",[162,163],"line",{"x1":164,"y1":155,"x2":165,"y2":155,"stroke":146,"style":108},"486","522",[122,167],{"points":168,"fill":169},"530,136 518,130 518,142","#b4740a",[40,171],{"x":172,"y":173,"width":174,"height":175,"rx":176,"fill":177,"stroke":64},"538","52","246","34","7","#f0f4ff",[47,179,183],{"x":180,"y":181,"style":182},"661","74","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","2026-06-01 · 544.86 · 3",[40,185],{"x":172,"y":186,"width":174,"height":175,"rx":176,"fill":187,"stroke":188},"94","#fee8f2","var(--accent,#f43f8f)",[47,190,192],{"x":180,"y":82,"style":191},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","2026-07-01 · 0.00 · 0",[40,194],{"x":172,"y":155,"width":174,"height":175,"rx":176,"fill":177,"stroke":64},[47,196,197],{"x":180,"y":91,"style":182},"2026-08-01 · 508.45 · 2",[47,199,201],{"x":180,"y":200,"style":103},"196","July exists only because asfreq",[47,203,205],{"x":180,"y":204,"style":103},"212","filled the gap with zero",[207,208,210],"h2",{"id":209},"prerequisites","Prerequisites",[212,213,218],"pre",{"className":214,"code":215,"language":216,"meta":217,"style":217},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[219,220,221],"code",{"__ignoreMap":217},[222,223,225,229,233,236,239],"span",{"class":162,"line":224},1,[222,226,228],{"class":227},"sMTad","pip",[222,230,232],{"class":231},"srMev"," install",[222,234,235],{"class":231}," pandas",[222,237,238],{"class":231}," openpyxl",[222,240,241],{"class":231}," xlsxwriter\n",[10,243,244],{},"A sample workbook to work against, with a deliberate gap in July so the gap-filling section has something to demonstrate:",[212,246,250],{"className":247,"code":248,"language":249,"meta":217,"style":217},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nsales = pd.DataFrame({\n    \"date\": pd.to_datetime([\n        \"2026-06-03\", \"2026-06-19\", \"2026-06-28\",\n        \"2026-08-02\", \"2026-08-15\", \"2026-09-07\", \"2026-09-30\",\n    ]),\n    \"region\": [\"North\", \"South\", \"North\", \"West\", \"North\", \"South\", \"West\"],\n    \"amount\": [159.92, 247.50, 137.44, 412.10, 96.35, 188.00, 301.75],\n})\nsales.to_excel(\"sales.xlsx\", index=False)\n","python",[219,251,252,268,275,287,296,316,339,345,386,430,436],{"__ignoreMap":217},[222,253,254,258,262,265],{"class":162,"line":224},[222,255,257],{"class":256},"s-kum","import",[222,259,261],{"class":260},"skGVy"," pandas ",[222,263,264],{"class":256},"as",[222,266,267],{"class":260}," pd\n",[222,269,271],{"class":162,"line":270},2,[222,272,274],{"emptyLinePlaceholder":273},true,"\n",[222,276,278,281,284],{"class":162,"line":277},3,[222,279,280],{"class":260},"sales ",[222,282,283],{"class":256},"=",[222,285,286],{"class":260}," pd.DataFrame({\n",[222,288,290,293],{"class":162,"line":289},4,[222,291,292],{"class":231},"    \"date\"",[222,294,295],{"class":260},": pd.to_datetime([\n",[222,297,299,302,305,308,310,313],{"class":162,"line":298},5,[222,300,301],{"class":231},"        \"2026-06-03\"",[222,303,304],{"class":260},", ",[222,306,307],{"class":231},"\"2026-06-19\"",[222,309,304],{"class":260},[222,311,312],{"class":231},"\"2026-06-28\"",[222,314,315],{"class":260},",\n",[222,317,319,322,324,327,329,332,334,337],{"class":162,"line":318},6,[222,320,321],{"class":231},"        \"2026-08-02\"",[222,323,304],{"class":260},[222,325,326],{"class":231},"\"2026-08-15\"",[222,328,304],{"class":260},[222,330,331],{"class":231},"\"2026-09-07\"",[222,333,304],{"class":260},[222,335,336],{"class":231},"\"2026-09-30\"",[222,338,315],{"class":260},[222,340,342],{"class":162,"line":341},7,[222,343,344],{"class":260},"    ]),\n",[222,346,348,351,354,357,359,362,364,366,368,371,373,375,377,379,381,383],{"class":162,"line":347},8,[222,349,350],{"class":231},"    \"region\"",[222,352,353],{"class":260},": [",[222,355,356],{"class":231},"\"North\"",[222,358,304],{"class":260},[222,360,361],{"class":231},"\"South\"",[222,363,304],{"class":260},[222,365,356],{"class":231},[222,367,304],{"class":260},[222,369,370],{"class":231},"\"West\"",[222,372,304],{"class":260},[222,374,356],{"class":231},[222,376,304],{"class":260},[222,378,361],{"class":231},[222,380,304],{"class":260},[222,382,370],{"class":231},[222,384,385],{"class":260},"],\n",[222,387,389,392,394,398,400,403,405,408,410,413,415,418,420,423,425,428],{"class":162,"line":388},9,[222,390,391],{"class":231},"    \"amount\"",[222,393,353],{"class":260},[222,395,397],{"class":396},"sP0c6","159.92",[222,399,304],{"class":260},[222,401,402],{"class":396},"247.50",[222,404,304],{"class":260},[222,406,407],{"class":396},"137.44",[222,409,304],{"class":260},[222,411,412],{"class":396},"412.10",[222,414,304],{"class":260},[222,416,417],{"class":396},"96.35",[222,419,304],{"class":260},[222,421,422],{"class":396},"188.00",[222,424,304],{"class":260},[222,426,427],{"class":396},"301.75",[222,429,385],{"class":260},[222,431,433],{"class":162,"line":432},10,[222,434,435],{"class":260},"})\n",[222,437,439,442,445,447,451,453,456],{"class":162,"line":438},11,[222,440,441],{"class":260},"sales.to_excel(",[222,443,444],{"class":231},"\"sales.xlsx\"",[222,446,304],{"class":260},[222,448,450],{"class":449},"sa561","index",[222,452,283],{"class":256},[222,454,455],{"class":396},"False",[222,457,458],{"class":260},")\n",[207,460,462],{"id":461},"step-1-read-and-make-sure-the-column-is-really-a-date","Step 1 — Read and make sure the column is really a date",[10,464,465,466,469],{},"Every grouping technique below fails on an ",[219,467,468],{},"object"," column, usually with a message about the key not being datetime-like. Convert once, at the top:",[212,471,473],{"className":247,"code":472,"language":249,"meta":217,"style":217},"import pandas as pd\n\ndf = pd.read_excel(\"sales.xlsx\")\ndf[\"date\"] = pd.to_datetime(df[\"date\"], errors=\"coerce\")\n\nmissing = df[\"date\"].isna().sum()\nif missing:\n    print(f\"warning: dropping {missing} rows with an unparseable date\")\n    df = df.dropna(subset=[\"date\"])\n",[219,474,475,485,489,503,534,538,553,561,590],{"__ignoreMap":217},[222,476,477,479,481,483],{"class":162,"line":224},[222,478,257],{"class":256},[222,480,261],{"class":260},[222,482,264],{"class":256},[222,484,267],{"class":260},[222,486,487],{"class":162,"line":270},[222,488,274],{"emptyLinePlaceholder":273},[222,490,491,494,496,499,501],{"class":162,"line":277},[222,492,493],{"class":260},"df ",[222,495,283],{"class":256},[222,497,498],{"class":260}," pd.read_excel(",[222,500,444],{"class":231},[222,502,458],{"class":260},[222,504,505,508,511,514,516,519,521,524,527,529,532],{"class":162,"line":289},[222,506,507],{"class":260},"df[",[222,509,510],{"class":231},"\"date\"",[222,512,513],{"class":260},"] ",[222,515,283],{"class":256},[222,517,518],{"class":260}," pd.to_datetime(df[",[222,520,510],{"class":231},[222,522,523],{"class":260},"], ",[222,525,526],{"class":449},"errors",[222,528,283],{"class":256},[222,530,531],{"class":231},"\"coerce\"",[222,533,458],{"class":260},[222,535,536],{"class":162,"line":298},[222,537,274],{"emptyLinePlaceholder":273},[222,539,540,543,545,548,550],{"class":162,"line":318},[222,541,542],{"class":260},"missing ",[222,544,283],{"class":256},[222,546,547],{"class":260}," df[",[222,549,510],{"class":231},[222,551,552],{"class":260},"].isna().sum()\n",[222,554,555,558],{"class":162,"line":341},[222,556,557],{"class":256},"if",[222,559,560],{"class":260}," missing:\n",[222,562,563,566,569,572,575,579,582,585,588],{"class":162,"line":347},[222,564,565],{"class":396},"    print",[222,567,568],{"class":260},"(",[222,570,571],{"class":256},"f",[222,573,574],{"class":231},"\"warning: dropping ",[222,576,578],{"class":577},"sSjpA","{",[222,580,581],{"class":260},"missing",[222,583,584],{"class":577},"}",[222,586,587],{"class":231}," rows with an unparseable date\"",[222,589,458],{"class":260},[222,591,592,595,597,600,603,605,608,610],{"class":162,"line":388},[222,593,594],{"class":260},"    df ",[222,596,283],{"class":256},[222,598,599],{"class":260}," df.dropna(",[222,601,602],{"class":449},"subset",[222,604,283],{"class":256},[222,606,607],{"class":260},"[",[222,609,510],{"class":231},[222,611,612],{"class":260},"])\n",[10,614,615,616,18],{},"Dropping unparseable rows silently is how a monthly total ends up understated. Print the count, or better, route them to a rejects file — the full treatment is in ",[14,617,619],{"href":618},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fparse-excel-dates-into-python-datetimes-with-pandas\u002F","parsing Excel dates with pandas",[207,621,623],{"id":622},"step-2-group-by-month","Step 2 — Group by month",[10,625,626,627,630],{},"Two tools, two purposes. ",[219,628,629],{},"Grouper"," produces a real timestamp key:",[212,632,634],{"className":247,"code":633,"language":249,"meta":217,"style":217},"monthly = (\n    df.groupby(pd.Grouper(key=\"date\", freq=\"MS\"))\n      .agg(revenue=(\"amount\", \"sum\"), orders=(\"amount\", \"size\"))\n)\nprint(monthly)\n#             revenue  orders\n# date\n# 2026-06-01   544.86       3\n# 2026-07-01     0.00       0\n# 2026-08-01   508.45       2\n# 2026-09-01   489.75       2\n",[219,635,636,646,671,710,714,722,728,733,738,743,748],{"__ignoreMap":217},[222,637,638,641,643],{"class":162,"line":224},[222,639,640],{"class":260},"monthly ",[222,642,283],{"class":256},[222,644,645],{"class":260}," (\n",[222,647,648,651,654,656,658,660,663,665,668],{"class":162,"line":270},[222,649,650],{"class":260},"    df.groupby(pd.Grouper(",[222,652,653],{"class":449},"key",[222,655,283],{"class":256},[222,657,510],{"class":231},[222,659,304],{"class":260},[222,661,662],{"class":449},"freq",[222,664,283],{"class":256},[222,666,667],{"class":231},"\"MS\"",[222,669,670],{"class":260},"))\n",[222,672,673,676,679,681,683,686,688,691,694,697,699,701,703,705,708],{"class":162,"line":277},[222,674,675],{"class":260},"      .agg(",[222,677,678],{"class":449},"revenue",[222,680,283],{"class":256},[222,682,568],{"class":260},[222,684,685],{"class":231},"\"amount\"",[222,687,304],{"class":260},[222,689,690],{"class":231},"\"sum\"",[222,692,693],{"class":260},"), ",[222,695,696],{"class":449},"orders",[222,698,283],{"class":256},[222,700,568],{"class":260},[222,702,685],{"class":231},[222,704,304],{"class":260},[222,706,707],{"class":231},"\"size\"",[222,709,670],{"class":260},[222,711,712],{"class":162,"line":289},[222,713,458],{"class":260},[222,715,716,719],{"class":162,"line":298},[222,717,718],{"class":396},"print",[222,720,721],{"class":260},"(monthly)\n",[222,723,724],{"class":162,"line":318},[222,725,727],{"class":726},"s-wDw","#             revenue  orders\n",[222,729,730],{"class":162,"line":341},[222,731,732],{"class":726},"# date\n",[222,734,735],{"class":162,"line":347},[222,736,737],{"class":726},"# 2026-06-01   544.86       3\n",[222,739,740],{"class":162,"line":388},[222,741,742],{"class":726},"# 2026-07-01     0.00       0\n",[222,744,745],{"class":162,"line":432},[222,746,747],{"class":726},"# 2026-08-01   508.45       2\n",[222,749,750],{"class":162,"line":438},[222,751,752],{"class":726},"# 2026-09-01   489.75       2\n",[10,754,755,756,758,759,763],{},"Note that ",[219,757,629],{}," ",[760,761,762],"em",{},"does"," emit the empty July here, because it builds a continuous range between the first and last date. That is a genuine difference from grouping on a derived label, which does not:",[212,765,767],{"className":247,"code":766,"language":249,"meta":217,"style":217},"df[\"month\"] = df[\"date\"].dt.to_period(\"M\")\nby_label = df.groupby(\"month\")[\"amount\"].sum()\nprint(by_label)\n# month\n# 2026-06    544.86\n# 2026-08    508.45      \u003C- July is simply absent\n# 2026-09    489.75\n",[219,768,769,792,812,819,824,829,834],{"__ignoreMap":217},[222,770,771,773,776,778,780,782,784,787,790],{"class":162,"line":224},[222,772,507],{"class":260},[222,774,775],{"class":231},"\"month\"",[222,777,513],{"class":260},[222,779,283],{"class":256},[222,781,547],{"class":260},[222,783,510],{"class":231},[222,785,786],{"class":260},"].dt.to_period(",[222,788,789],{"class":231},"\"M\"",[222,791,458],{"class":260},[222,793,794,797,799,802,804,807,809],{"class":162,"line":270},[222,795,796],{"class":260},"by_label ",[222,798,283],{"class":256},[222,800,801],{"class":260}," df.groupby(",[222,803,775],{"class":231},[222,805,806],{"class":260},")[",[222,808,685],{"class":231},[222,810,811],{"class":260},"].sum()\n",[222,813,814,816],{"class":162,"line":277},[222,815,718],{"class":396},[222,817,818],{"class":260},"(by_label)\n",[222,820,821],{"class":162,"line":289},[222,822,823],{"class":726},"# month\n",[222,825,826],{"class":162,"line":298},[222,827,828],{"class":726},"# 2026-06    544.86\n",[222,830,831],{"class":162,"line":318},[222,832,833],{"class":726},"# 2026-08    508.45      \u003C- July is simply absent\n",[222,835,836],{"class":162,"line":341},[222,837,838],{"class":726},"# 2026-09    489.75\n",[10,840,841],{},"The frequency aliases you will use most:",[843,844,845,861],"table",{},[846,847,848],"thead",{},[849,850,851,855,858],"tr",{},[852,853,854],"th",{},"Alias",[852,856,857],{},"Bucket",[852,859,860],{},"Key lands on",[862,863,864,878,890,903,915,928],"tbody",{},[849,865,866,872,875],{},[867,868,869],"td",{},[219,870,871],{},"MS",[867,873,874],{},"month",[867,876,877],{},"first day of the month",[849,879,880,885,887],{},[867,881,882],{},[219,883,884],{},"ME",[867,886,874],{},[867,888,889],{},"last day of the month",[849,891,892,897,900],{},[867,893,894],{},[219,895,896],{},"QS",[867,898,899],{},"quarter",[867,901,902],{},"first day of the quarter",[849,904,905,910,912],{},[867,906,907],{},[219,908,909],{},"QE",[867,911,899],{},[867,913,914],{},"last day of the quarter",[849,916,917,922,925],{},[867,918,919],{},[219,920,921],{},"W-MON",[867,923,924],{},"week",[867,926,927],{},"the Monday starting the week",[849,929,930,935,938],{},[867,931,932],{},[219,933,934],{},"YS",[867,936,937],{},"year",[867,939,940],{},"1 January",[10,942,943,944,947,948,304,950,304,952,954,955,958],{},"Prefer the ",[760,945,946],{},"start"," aliases (",[219,949,871],{},[219,951,896],{},[219,953,934],{},") for report keys. A month-end key of ",[219,956,957],{},"2026-06-30"," sorts identically but reads worse in a chart axis, and it makes joining against other month-keyed tables fiddly because not every system agrees on which end of the month labels it.",[207,960,962],{"id":961},"step-3-fill-the-periods-that-have-no-rows","Step 3 — Fill the periods that have no rows",[10,964,965],{},"A missing month is the difference between \"we sold nothing in July\" and \"July is not in this report\". Only one of those is visible to a reader.",[212,967,969],{"className":247,"code":968,"language":249,"meta":217,"style":217},"monthly = (\n    df.groupby(pd.Grouper(key=\"date\", freq=\"MS\"))\n      .agg(revenue=(\"amount\", \"sum\"), orders=(\"amount\", \"size\"))\n      .asfreq(\"MS\", fill_value=0)\n)\n",[219,970,971,979,999,1031,1049],{"__ignoreMap":217},[222,972,973,975,977],{"class":162,"line":224},[222,974,640],{"class":260},[222,976,283],{"class":256},[222,978,645],{"class":260},[222,980,981,983,985,987,989,991,993,995,997],{"class":162,"line":270},[222,982,650],{"class":260},[222,984,653],{"class":449},[222,986,283],{"class":256},[222,988,510],{"class":231},[222,990,304],{"class":260},[222,992,662],{"class":449},[222,994,283],{"class":256},[222,996,667],{"class":231},[222,998,670],{"class":260},[222,1000,1001,1003,1005,1007,1009,1011,1013,1015,1017,1019,1021,1023,1025,1027,1029],{"class":162,"line":277},[222,1002,675],{"class":260},[222,1004,678],{"class":449},[222,1006,283],{"class":256},[222,1008,568],{"class":260},[222,1010,685],{"class":231},[222,1012,304],{"class":260},[222,1014,690],{"class":231},[222,1016,693],{"class":260},[222,1018,696],{"class":449},[222,1020,283],{"class":256},[222,1022,568],{"class":260},[222,1024,685],{"class":231},[222,1026,304],{"class":260},[222,1028,707],{"class":231},[222,1030,670],{"class":260},[222,1032,1033,1036,1038,1040,1043,1045,1047],{"class":162,"line":289},[222,1034,1035],{"class":260},"      .asfreq(",[222,1037,667],{"class":231},[222,1039,304],{"class":260},[222,1041,1042],{"class":449},"fill_value",[222,1044,283],{"class":256},[222,1046,42],{"class":396},[222,1048,458],{"class":260},[222,1050,1051],{"class":162,"line":298},[222,1052,458],{"class":260},[10,1054,1055,1058,1059,1062],{},[219,1056,1057],{},"asfreq"," fills gaps ",[760,1060,1061],{},"inside"," the observed range. To cover a fixed reporting window regardless of what the data contains — the usual requirement for a monthly report that must always show twelve rows — reindex against an explicit range instead:",[212,1064,1066],{"className":247,"code":1065,"language":249,"meta":217,"style":217},"import pandas as pd\n\nwindow = pd.date_range(\"2026-01-01\", \"2026-12-01\", freq=\"MS\")\n\nmonthly = (\n    df.groupby(pd.Grouper(key=\"date\", freq=\"MS\"))\n      .agg(revenue=(\"amount\", \"sum\"), orders=(\"amount\", \"size\"))\n      .reindex(window, fill_value=0)\n)\nmonthly.index.name = \"month\"\nprint(len(monthly))     # 12, always\n",[219,1067,1068,1078,1082,1110,1114,1122,1142,1174,1187,1191,1201],{"__ignoreMap":217},[222,1069,1070,1072,1074,1076],{"class":162,"line":224},[222,1071,257],{"class":256},[222,1073,261],{"class":260},[222,1075,264],{"class":256},[222,1077,267],{"class":260},[222,1079,1080],{"class":162,"line":270},[222,1081,274],{"emptyLinePlaceholder":273},[222,1083,1084,1087,1089,1092,1095,1097,1100,1102,1104,1106,1108],{"class":162,"line":277},[222,1085,1086],{"class":260},"window ",[222,1088,283],{"class":256},[222,1090,1091],{"class":260}," pd.date_range(",[222,1093,1094],{"class":231},"\"2026-01-01\"",[222,1096,304],{"class":260},[222,1098,1099],{"class":231},"\"2026-12-01\"",[222,1101,304],{"class":260},[222,1103,662],{"class":449},[222,1105,283],{"class":256},[222,1107,667],{"class":231},[222,1109,458],{"class":260},[222,1111,1112],{"class":162,"line":289},[222,1113,274],{"emptyLinePlaceholder":273},[222,1115,1116,1118,1120],{"class":162,"line":298},[222,1117,640],{"class":260},[222,1119,283],{"class":256},[222,1121,645],{"class":260},[222,1123,1124,1126,1128,1130,1132,1134,1136,1138,1140],{"class":162,"line":318},[222,1125,650],{"class":260},[222,1127,653],{"class":449},[222,1129,283],{"class":256},[222,1131,510],{"class":231},[222,1133,304],{"class":260},[222,1135,662],{"class":449},[222,1137,283],{"class":256},[222,1139,667],{"class":231},[222,1141,670],{"class":260},[222,1143,1144,1146,1148,1150,1152,1154,1156,1158,1160,1162,1164,1166,1168,1170,1172],{"class":162,"line":341},[222,1145,675],{"class":260},[222,1147,678],{"class":449},[222,1149,283],{"class":256},[222,1151,568],{"class":260},[222,1153,685],{"class":231},[222,1155,304],{"class":260},[222,1157,690],{"class":231},[222,1159,693],{"class":260},[222,1161,696],{"class":449},[222,1163,283],{"class":256},[222,1165,568],{"class":260},[222,1167,685],{"class":231},[222,1169,304],{"class":260},[222,1171,707],{"class":231},[222,1173,670],{"class":260},[222,1175,1176,1179,1181,1183,1185],{"class":162,"line":347},[222,1177,1178],{"class":260},"      .reindex(window, ",[222,1180,1042],{"class":449},[222,1182,283],{"class":256},[222,1184,42],{"class":396},[222,1186,458],{"class":260},[222,1188,1189],{"class":162,"line":388},[222,1190,458],{"class":260},[222,1192,1193,1196,1198],{"class":162,"line":432},[222,1194,1195],{"class":260},"monthly.index.name ",[222,1197,283],{"class":256},[222,1199,1200],{"class":231}," \"month\"\n",[222,1202,1203,1205,1207,1210,1213],{"class":162,"line":438},[222,1204,718],{"class":396},[222,1206,568],{"class":260},[222,1208,1209],{"class":396},"len",[222,1211,1212],{"class":260},"(monthly))     ",[222,1214,1215],{"class":726},"# 12, always\n",[10,1217,1218,1219,1223],{},"The distinction matters for charts especially. A line chart drawn from a series with a missing month connects straight across the gap, implying a smooth trend through a period where nothing happened — see ",[14,1220,1222],{"href":1221},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002F","adding a line chart to an Excel report"," for the plotting side.",[207,1225,1227],{"id":1226},"step-4-quarters-weeks-and-fiscal-years","Step 4 — Quarters, weeks and fiscal years",[10,1229,1230],{},"Calendar quarters are a frequency change and nothing more:",[212,1232,1234],{"className":247,"code":1233,"language":249,"meta":217,"style":217},"quarterly = (\n    df.groupby(pd.Grouper(key=\"date\", freq=\"QS\"))\n      .agg(revenue=(\"amount\", \"sum\"), orders=(\"amount\", \"size\"))\n      .asfreq(\"QS\", fill_value=0)\n)\n",[219,1235,1236,1245,1266,1298,1314],{"__ignoreMap":217},[222,1237,1238,1241,1243],{"class":162,"line":224},[222,1239,1240],{"class":260},"quarterly ",[222,1242,283],{"class":256},[222,1244,645],{"class":260},[222,1246,1247,1249,1251,1253,1255,1257,1259,1261,1264],{"class":162,"line":270},[222,1248,650],{"class":260},[222,1250,653],{"class":449},[222,1252,283],{"class":256},[222,1254,510],{"class":231},[222,1256,304],{"class":260},[222,1258,662],{"class":449},[222,1260,283],{"class":256},[222,1262,1263],{"class":231},"\"QS\"",[222,1265,670],{"class":260},[222,1267,1268,1270,1272,1274,1276,1278,1280,1282,1284,1286,1288,1290,1292,1294,1296],{"class":162,"line":277},[222,1269,675],{"class":260},[222,1271,678],{"class":449},[222,1273,283],{"class":256},[222,1275,568],{"class":260},[222,1277,685],{"class":231},[222,1279,304],{"class":260},[222,1281,690],{"class":231},[222,1283,693],{"class":260},[222,1285,696],{"class":449},[222,1287,283],{"class":256},[222,1289,568],{"class":260},[222,1291,685],{"class":231},[222,1293,304],{"class":260},[222,1295,707],{"class":231},[222,1297,670],{"class":260},[222,1299,1300,1302,1304,1306,1308,1310,1312],{"class":162,"line":289},[222,1301,1035],{"class":260},[222,1303,1263],{"class":231},[222,1305,304],{"class":260},[222,1307,1042],{"class":449},[222,1309,283],{"class":256},[222,1311,42],{"class":396},[222,1313,458],{"class":260},[222,1315,1316],{"class":162,"line":298},[222,1317,458],{"class":260},[10,1319,1320,1321,1324,1325,1328],{},"Fiscal years need an anchor. A year ending 31 March is ",[219,1322,1323],{},"Q-MAR",", and the anchor names the month the fiscal year ",[760,1326,1327],{},"ends"," in:",[20,1330,30,1336,30,1339,30,1342,30,1345,30,1350,30,1357,30,1363,30,1366,30,1370,30,1374,30,1378,30,1381,30,1385,30,1388,30,1392,30,1394,30,1397,30,1399,30,1403,30,1405,30,1409,30,1411,30,1414,30,1416,30,1419,30,1422],{"viewBox":1331,"role":23,"ariaLabel":1332,"ariaLabelledBy":1333,"xmlns":28,"style":29},"0 0 800 224","Calendar quarters versus a fiscal year ending in March: the same twelve months split at different boundaries, so April lands in Q2 of the calendar year but Q1 of the following fiscal year.",[1334,1335],"fisc-t","fisc-d",[32,1337,1338],{"id":1334},"Calendar quarters against a fiscal year ending in March",[36,1340,1341],{"id":1335},"Two bands over the same twelve months from January to December. The calendar band splits at January, April, July and October. The fiscal band, anchored to a March year end, splits at April, July, October and January, so a row dated April falls in calendar Q2 but fiscal Q1 of the following fiscal year. Choosing the wrong anchor shifts a quarter of the rows into the wrong bucket.",[40,1343],{"x":42,"y":42,"width":43,"height":1344,"fill":45},"224",[47,1346,1349],{"x":1347,"y":50,"style":1348},"400","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","the same year, two quarter boundaries",[40,1351],{"x":1352,"y":173,"width":1353,"height":60,"rx":1354,"fill":1355,"stroke":1356,"style":108},"14","96","8","#f0f2f5","var(--line,#cdd5e6)",[47,1358,1362],{"x":1359,"y":1360,"style":1361},"62","76","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","calendar",[40,1364],{"x":1365,"y":173,"width":143,"height":60,"rx":1354,"fill":63,"stroke":64,"style":108},"126",[47,1367,1369],{"x":1368,"y":1360,"style":182},"204","Q1 · Jan–Mar",[40,1371],{"x":1372,"y":173,"width":1373,"height":60,"rx":1354,"fill":177,"stroke":64,"style":108},"291","157",[47,1375,1377],{"x":1376,"y":1360,"style":182},"369","Q2 · Apr–Jun",[40,1379],{"x":1380,"y":173,"width":1373,"height":60,"rx":1354,"fill":63,"stroke":64,"style":108},"456",[47,1382,1384],{"x":1383,"y":1360,"style":182},"534","Q3 · Jul–Sep",[40,1386],{"x":1387,"y":173,"width":1373,"height":60,"rx":1354,"fill":177,"stroke":64,"style":108},"621",[47,1389,1391],{"x":1390,"y":1360,"style":182},"699","Q4 · Oct–Dec",[40,1393],{"x":1352,"y":1365,"width":1353,"height":60,"rx":1354,"fill":1355,"stroke":1356,"style":108},[47,1395,1323],{"x":1359,"y":1396,"style":1361},"150",[40,1398],{"x":1365,"y":1365,"width":143,"height":60,"rx":1354,"fill":145,"stroke":146,"style":108},[47,1400,1402],{"x":1368,"y":1396,"style":1401},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Q4 · Jan–Mar",[40,1404],{"x":1372,"y":1365,"width":1373,"height":60,"rx":1354,"fill":87,"stroke":88,"style":108},[47,1406,1408],{"x":1376,"y":1396,"style":1407},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Q1 · Apr–Jun",[40,1410],{"x":1380,"y":1365,"width":1373,"height":60,"rx":1354,"fill":87,"stroke":88,"style":108},[47,1412,1413],{"x":1383,"y":1396,"style":1407},"Q2 · Jul–Sep",[40,1415],{"x":1387,"y":1365,"width":1373,"height":60,"rx":1354,"fill":87,"stroke":88,"style":108},[47,1417,1418],{"x":1390,"y":1396,"style":1407},"Q3 · Oct–Dec",[47,1420,1421],{"x":1347,"y":200,"style":191},"an April row is calendar Q2 but fiscal Q1 — and of the NEXT fiscal year",[47,1423,1425],{"x":1347,"y":1424,"style":103},"214","the anchor names the month the fiscal year ends in",[212,1427,1429],{"className":247,"code":1428,"language":249,"meta":217,"style":217},"import pandas as pd\n\ndf[\"fiscal_quarter\"] = df[\"date\"].dt.to_period(\"Q-MAR\")\n\nprint(df.loc[df[\"date\"] == \"2026-06-03\", \"fiscal_quarter\"].iloc[0])\n# 2027Q1   — June 2026 is Q1 of the fiscal year ending March 2027\n\nfiscal = (\n    df.groupby(\"fiscal_quarter\")[\"amount\"]\n      .agg(revenue=\"sum\", orders=\"size\")\n      .sort_index()\n)\n",[219,1430,1431,1441,1445,1467,1471,1499,1504,1508,1517,1531,1551,1556],{"__ignoreMap":217},[222,1432,1433,1435,1437,1439],{"class":162,"line":224},[222,1434,257],{"class":256},[222,1436,261],{"class":260},[222,1438,264],{"class":256},[222,1440,267],{"class":260},[222,1442,1443],{"class":162,"line":270},[222,1444,274],{"emptyLinePlaceholder":273},[222,1446,1447,1449,1452,1454,1456,1458,1460,1462,1465],{"class":162,"line":277},[222,1448,507],{"class":260},[222,1450,1451],{"class":231},"\"fiscal_quarter\"",[222,1453,513],{"class":260},[222,1455,283],{"class":256},[222,1457,547],{"class":260},[222,1459,510],{"class":231},[222,1461,786],{"class":260},[222,1463,1464],{"class":231},"\"Q-MAR\"",[222,1466,458],{"class":260},[222,1468,1469],{"class":162,"line":289},[222,1470,274],{"emptyLinePlaceholder":273},[222,1472,1473,1475,1478,1480,1482,1485,1488,1490,1492,1495,1497],{"class":162,"line":298},[222,1474,718],{"class":396},[222,1476,1477],{"class":260},"(df.loc[df[",[222,1479,510],{"class":231},[222,1481,513],{"class":260},[222,1483,1484],{"class":256},"==",[222,1486,1487],{"class":231}," \"2026-06-03\"",[222,1489,304],{"class":260},[222,1491,1451],{"class":231},[222,1493,1494],{"class":260},"].iloc[",[222,1496,42],{"class":396},[222,1498,612],{"class":260},[222,1500,1501],{"class":162,"line":318},[222,1502,1503],{"class":726},"# 2027Q1   — June 2026 is Q1 of the fiscal year ending March 2027\n",[222,1505,1506],{"class":162,"line":341},[222,1507,274],{"emptyLinePlaceholder":273},[222,1509,1510,1513,1515],{"class":162,"line":347},[222,1511,1512],{"class":260},"fiscal ",[222,1514,283],{"class":256},[222,1516,645],{"class":260},[222,1518,1519,1522,1524,1526,1528],{"class":162,"line":388},[222,1520,1521],{"class":260},"    df.groupby(",[222,1523,1451],{"class":231},[222,1525,806],{"class":260},[222,1527,685],{"class":231},[222,1529,1530],{"class":260},"]\n",[222,1532,1533,1535,1537,1539,1541,1543,1545,1547,1549],{"class":162,"line":432},[222,1534,675],{"class":260},[222,1536,678],{"class":449},[222,1538,283],{"class":256},[222,1540,690],{"class":231},[222,1542,304],{"class":260},[222,1544,696],{"class":449},[222,1546,283],{"class":256},[222,1548,707],{"class":231},[222,1550,458],{"class":260},[222,1552,1553],{"class":162,"line":438},[222,1554,1555],{"class":260},"      .sort_index()\n",[222,1557,1559],{"class":162,"line":1558},12,[222,1560,458],{"class":260},[10,1562,1563],{},"Weeks carry their own convention question — which day starts the week:",[212,1565,1567],{"className":247,"code":1566,"language":249,"meta":217,"style":217},"# ISO weeks start on Monday; W-SUN if your business week starts Sunday.\nweekly = (\n    df.groupby(pd.Grouper(key=\"date\", freq=\"W-MON\", label=\"left\"))[\"amount\"]\n      .sum()\n)\n",[219,1568,1569,1574,1583,1619,1624],{"__ignoreMap":217},[222,1570,1571],{"class":162,"line":224},[222,1572,1573],{"class":726},"# ISO weeks start on Monday; W-SUN if your business week starts Sunday.\n",[222,1575,1576,1579,1581],{"class":162,"line":270},[222,1577,1578],{"class":260},"weekly ",[222,1580,283],{"class":256},[222,1582,645],{"class":260},[222,1584,1585,1587,1589,1591,1593,1595,1597,1599,1602,1604,1607,1609,1612,1615,1617],{"class":162,"line":277},[222,1586,650],{"class":260},[222,1588,653],{"class":449},[222,1590,283],{"class":256},[222,1592,510],{"class":231},[222,1594,304],{"class":260},[222,1596,662],{"class":449},[222,1598,283],{"class":256},[222,1600,1601],{"class":231},"\"W-MON\"",[222,1603,304],{"class":260},[222,1605,1606],{"class":449},"label",[222,1608,283],{"class":256},[222,1610,1611],{"class":231},"\"left\"",[222,1613,1614],{"class":260},"))[",[222,1616,685],{"class":231},[222,1618,1530],{"class":260},[222,1620,1621],{"class":162,"line":289},[222,1622,1623],{"class":260},"      .sum()\n",[222,1625,1626],{"class":162,"line":298},[222,1627,458],{"class":260},[10,1629,1630,1633,1634,1637],{},[219,1631,1632],{},"label=\"left\""," makes the key the Monday that ",[760,1635,1636],{},"starts"," the week rather than the one that ends it, which is what most people expect when they read a weekly report.",[207,1639,1641],{"id":1640},"step-5-group-by-period-and-another-column","Step 5 — Group by period and another column",[10,1643,1644],{},"Real reports want a region-by-month grid, not a single series. Add the second key and unstack:",[212,1646,1648],{"className":247,"code":1647,"language":249,"meta":217,"style":217},"grid = (\n    df.groupby([pd.Grouper(key=\"date\", freq=\"MS\"), \"region\"])[\"amount\"]\n      .sum()\n      .unstack(\"region\", fill_value=0)\n      .reindex(pd.date_range(\"2026-06-01\", \"2026-09-01\", freq=\"MS\"), fill_value=0)\n)\ngrid.index.name = \"month\"\nprint(grid)\n",[219,1649,1650,1659,1690,1694,1711,1742,1746,1755],{"__ignoreMap":217},[222,1651,1652,1655,1657],{"class":162,"line":224},[222,1653,1654],{"class":260},"grid ",[222,1656,283],{"class":256},[222,1658,645],{"class":260},[222,1660,1661,1664,1666,1668,1670,1672,1674,1676,1678,1680,1683,1686,1688],{"class":162,"line":270},[222,1662,1663],{"class":260},"    df.groupby([pd.Grouper(",[222,1665,653],{"class":449},[222,1667,283],{"class":256},[222,1669,510],{"class":231},[222,1671,304],{"class":260},[222,1673,662],{"class":449},[222,1675,283],{"class":256},[222,1677,667],{"class":231},[222,1679,693],{"class":260},[222,1681,1682],{"class":231},"\"region\"",[222,1684,1685],{"class":260},"])[",[222,1687,685],{"class":231},[222,1689,1530],{"class":260},[222,1691,1692],{"class":162,"line":277},[222,1693,1623],{"class":260},[222,1695,1696,1699,1701,1703,1705,1707,1709],{"class":162,"line":289},[222,1697,1698],{"class":260},"      .unstack(",[222,1700,1682],{"class":231},[222,1702,304],{"class":260},[222,1704,1042],{"class":449},[222,1706,283],{"class":256},[222,1708,42],{"class":396},[222,1710,458],{"class":260},[222,1712,1713,1716,1719,1721,1724,1726,1728,1730,1732,1734,1736,1738,1740],{"class":162,"line":298},[222,1714,1715],{"class":260},"      .reindex(pd.date_range(",[222,1717,1718],{"class":231},"\"2026-06-01\"",[222,1720,304],{"class":260},[222,1722,1723],{"class":231},"\"2026-09-01\"",[222,1725,304],{"class":260},[222,1727,662],{"class":449},[222,1729,283],{"class":256},[222,1731,667],{"class":231},[222,1733,693],{"class":260},[222,1735,1042],{"class":449},[222,1737,283],{"class":256},[222,1739,42],{"class":396},[222,1741,458],{"class":260},[222,1743,1744],{"class":162,"line":318},[222,1745,458],{"class":260},[222,1747,1748,1751,1753],{"class":162,"line":341},[222,1749,1750],{"class":260},"grid.index.name ",[222,1752,283],{"class":256},[222,1754,1200],{"class":231},[222,1756,1757,1759],{"class":162,"line":347},[222,1758,718],{"class":396},[222,1760,1761],{"class":260},"(grid)\n",[10,1763,1764,1765,18],{},"That is a pivot table in all but name, and if the output is destined for a spreadsheet a native pivot is often the better shape — see ",[14,1766,1768],{"href":1767},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002F","creating pivot tables from Excel data",[207,1770,1772],{"id":1771},"step-6-write-the-summary-back-as-a-report","Step 6 — Write the summary back as a report",[20,1774,30,1780,30,1783,30,1786,30,1789,30,1794,30,1799,30,1804,30,1809,30,1811,30,1815,30,1818,30,1822,30,1825,30,1829,30,1833,30,1836,30,1841,30,1844,30,1846,30,1848,30,1850,30,1852,30,1854,30,1856,30,1859],{"viewBox":1775,"role":23,"ariaLabel":1776,"ariaLabelledBy":1777,"xmlns":28,"style":29},"0 0 800 232","Why a text month column sorts wrongly in Excel: alphabetical order puts April first and October before September, while a real timestamp with a display format sorts chronologically.",[1778,1779],"sortm-t","sortm-d",[32,1781,1782],{"id":1778},"Text months sort alphabetically; timestamps sort chronologically",[36,1784,1785],{"id":1779},"Two sorted columns of the same four months. On the left the months were written as text, so Excel's sort produces April, August, July, June — alphabetical order, which is meaningless as a time series. On the right the months were written as real timestamps carrying an mmm yyyy display format, so they sort June, July, August, September while still reading as month names.",[40,1787],{"x":42,"y":42,"width":43,"height":1788,"fill":45},"232",[40,1790],{"x":1352,"y":1791,"width":1792,"height":1793,"rx":1352,"fill":187,"stroke":188,"style":108},"24","368","192",[47,1795,1798],{"x":1796,"y":173,"style":1797},"198","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","month written as text",[40,1800],{"x":141,"y":1801,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":1356},"66","276","28",[47,1805,1808],{"x":1796,"y":1806,"style":1807},"85","font-size:11px;fill:var(--text,#172033);text-anchor:middle","Apr 2026",[40,1810],{"x":141,"y":79,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":1356},[47,1812,1814],{"x":1796,"y":1813,"style":1807},"117","Aug 2026",[40,1816],{"x":141,"y":1817,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":1356},"130",[47,1819,1821],{"x":1796,"y":1820,"style":1807},"149","Jul 2026",[40,1823],{"x":141,"y":1824,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":1356},"162",[47,1826,1828],{"x":1796,"y":1827,"style":1807},"181","Jun 2026",[47,1830,1832],{"x":1796,"y":1831,"style":191},"208","alphabetical — not a time series",[40,1834],{"x":1835,"y":1791,"width":1792,"height":1793,"rx":1352,"fill":87,"stroke":88,"style":108},"418",[47,1837,1840],{"x":1838,"y":173,"style":1839},"602","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","timestamp + \"mmm yyyy\" format",[40,1842],{"x":1843,"y":1801,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":88},"464",[47,1845,1828],{"x":1838,"y":1806,"style":1807},[40,1847],{"x":1843,"y":79,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":88},[47,1849,1821],{"x":1838,"y":1813,"style":1807},[40,1851],{"x":1843,"y":1817,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":88},[47,1853,1814],{"x":1838,"y":1820,"style":1807},[40,1855],{"x":1843,"y":1824,"width":1802,"height":1803,"rx":62,"fill":45,"stroke":88},[47,1857,1858],{"x":1838,"y":1827,"style":1807},"Sep 2026",[47,1860,1861],{"x":1838,"y":1831,"style":1407},"chronological, and still readable",[10,1863,1864],{},"Keep the index as timestamps until the moment you write, so sorting stays correct, then let the number format handle the display:",[212,1866,1868],{"className":247,"code":1867,"language":249,"meta":217,"style":217},"import pandas as pd\n\nout = monthly.reset_index().rename(columns={\"index\": \"month\"})\n\nwith pd.ExcelWriter(\"monthly_report.xlsx\", engine=\"xlsxwriter\") as writer:\n    out.to_excel(writer, sheet_name=\"Monthly\", index=False)\n\n    book, sheet = writer.book, writer.sheets[\"Monthly\"]\n    month_fmt = book.add_format({\"num_format\": \"mmm yyyy\"})\n    money = book.add_format({\"num_format\": \"#,##0.00\"})\n    header = book.add_format({\"bold\": True, \"bg_color\": \"#EEF2FF\", \"border\": 1})\n\n    for col, name in enumerate(out.columns):\n        sheet.write(0, col, name, header)\n\n    sheet.set_column(\"A:A\", 12, month_fmt)\n    sheet.set_column(\"B:B\", 14, money)\n    sheet.set_column(\"C:C\", 10)\n    sheet.freeze_panes(1, 0)\n",[219,1869,1870,1880,1884,1911,1915,1944,1967,1971,1985,2005,2023,2062,2066,2084,2095,2100,2116,2131,2146],{"__ignoreMap":217},[222,1871,1872,1874,1876,1878],{"class":162,"line":224},[222,1873,257],{"class":256},[222,1875,261],{"class":260},[222,1877,264],{"class":256},[222,1879,267],{"class":260},[222,1881,1882],{"class":162,"line":270},[222,1883,274],{"emptyLinePlaceholder":273},[222,1885,1886,1889,1891,1894,1897,1899,1901,1904,1907,1909],{"class":162,"line":277},[222,1887,1888],{"class":260},"out ",[222,1890,283],{"class":256},[222,1892,1893],{"class":260}," monthly.reset_index().rename(",[222,1895,1896],{"class":449},"columns",[222,1898,283],{"class":256},[222,1900,578],{"class":260},[222,1902,1903],{"class":231},"\"index\"",[222,1905,1906],{"class":260},": ",[222,1908,775],{"class":231},[222,1910,435],{"class":260},[222,1912,1913],{"class":162,"line":289},[222,1914,274],{"emptyLinePlaceholder":273},[222,1916,1917,1920,1923,1926,1928,1931,1933,1936,1939,1941],{"class":162,"line":298},[222,1918,1919],{"class":256},"with",[222,1921,1922],{"class":260}," pd.ExcelWriter(",[222,1924,1925],{"class":231},"\"monthly_report.xlsx\"",[222,1927,304],{"class":260},[222,1929,1930],{"class":449},"engine",[222,1932,283],{"class":256},[222,1934,1935],{"class":231},"\"xlsxwriter\"",[222,1937,1938],{"class":260},") ",[222,1940,264],{"class":256},[222,1942,1943],{"class":260}," writer:\n",[222,1945,1946,1949,1952,1954,1957,1959,1961,1963,1965],{"class":162,"line":318},[222,1947,1948],{"class":260},"    out.to_excel(writer, ",[222,1950,1951],{"class":449},"sheet_name",[222,1953,283],{"class":256},[222,1955,1956],{"class":231},"\"Monthly\"",[222,1958,304],{"class":260},[222,1960,450],{"class":449},[222,1962,283],{"class":256},[222,1964,455],{"class":396},[222,1966,458],{"class":260},[222,1968,1969],{"class":162,"line":341},[222,1970,274],{"emptyLinePlaceholder":273},[222,1972,1973,1976,1978,1981,1983],{"class":162,"line":347},[222,1974,1975],{"class":260},"    book, sheet ",[222,1977,283],{"class":256},[222,1979,1980],{"class":260}," writer.book, writer.sheets[",[222,1982,1956],{"class":231},[222,1984,1530],{"class":260},[222,1986,1987,1990,1992,1995,1998,2000,2003],{"class":162,"line":388},[222,1988,1989],{"class":260},"    month_fmt ",[222,1991,283],{"class":256},[222,1993,1994],{"class":260}," book.add_format({",[222,1996,1997],{"class":231},"\"num_format\"",[222,1999,1906],{"class":260},[222,2001,2002],{"class":231},"\"mmm yyyy\"",[222,2004,435],{"class":260},[222,2006,2007,2010,2012,2014,2016,2018,2021],{"class":162,"line":432},[222,2008,2009],{"class":260},"    money ",[222,2011,283],{"class":256},[222,2013,1994],{"class":260},[222,2015,1997],{"class":231},[222,2017,1906],{"class":260},[222,2019,2020],{"class":231},"\"#,##0.00\"",[222,2022,435],{"class":260},[222,2024,2025,2028,2030,2032,2035,2037,2040,2042,2045,2047,2050,2052,2055,2057,2060],{"class":162,"line":438},[222,2026,2027],{"class":260},"    header ",[222,2029,283],{"class":256},[222,2031,1994],{"class":260},[222,2033,2034],{"class":231},"\"bold\"",[222,2036,1906],{"class":260},[222,2038,2039],{"class":396},"True",[222,2041,304],{"class":260},[222,2043,2044],{"class":231},"\"bg_color\"",[222,2046,1906],{"class":260},[222,2048,2049],{"class":231},"\"#EEF2FF\"",[222,2051,304],{"class":260},[222,2053,2054],{"class":231},"\"border\"",[222,2056,1906],{"class":260},[222,2058,2059],{"class":396},"1",[222,2061,435],{"class":260},[222,2063,2064],{"class":162,"line":1558},[222,2065,274],{"emptyLinePlaceholder":273},[222,2067,2069,2072,2075,2078,2081],{"class":162,"line":2068},13,[222,2070,2071],{"class":256},"    for",[222,2073,2074],{"class":260}," col, name ",[222,2076,2077],{"class":256},"in",[222,2079,2080],{"class":396}," enumerate",[222,2082,2083],{"class":260},"(out.columns):\n",[222,2085,2087,2090,2092],{"class":162,"line":2086},14,[222,2088,2089],{"class":260},"        sheet.write(",[222,2091,42],{"class":396},[222,2093,2094],{"class":260},", col, name, header)\n",[222,2096,2098],{"class":162,"line":2097},15,[222,2099,274],{"emptyLinePlaceholder":273},[222,2101,2103,2106,2109,2111,2113],{"class":162,"line":2102},16,[222,2104,2105],{"class":260},"    sheet.set_column(",[222,2107,2108],{"class":231},"\"A:A\"",[222,2110,304],{"class":260},[222,2112,144],{"class":396},[222,2114,2115],{"class":260},", month_fmt)\n",[222,2117,2119,2121,2124,2126,2128],{"class":162,"line":2118},17,[222,2120,2105],{"class":260},[222,2122,2123],{"class":231},"\"B:B\"",[222,2125,304],{"class":260},[222,2127,1352],{"class":396},[222,2129,2130],{"class":260},", money)\n",[222,2132,2134,2136,2139,2141,2144],{"class":162,"line":2133},18,[222,2135,2105],{"class":260},[222,2137,2138],{"class":231},"\"C:C\"",[222,2140,304],{"class":260},[222,2142,2143],{"class":396},"10",[222,2145,458],{"class":260},[222,2147,2149,2152,2154,2156,2158],{"class":162,"line":2148},19,[222,2150,2151],{"class":260},"    sheet.freeze_panes(",[222,2153,2059],{"class":396},[222,2155,304],{"class":260},[222,2157,42],{"class":396},[222,2159,458],{"class":260},[10,2161,2162,2163,2166],{},"Writing the month as a formatted timestamp rather than the string ",[219,2164,2165],{},"\"2026-08\""," is what keeps Excel's own sorting and filtering working. A text month column sorts alphabetically, which puts April first and October before September — the single most common complaint about generated period reports.",[207,2168,2170],{"id":2169},"common-pitfalls-and-fixes","Common pitfalls and fixes",[843,2172,2173,2186],{},[846,2174,2175],{},[849,2176,2177,2180,2183],{},[852,2178,2179],{},"Symptom",[852,2181,2182],{},"Cause",[852,2184,2185],{},"Fix",[862,2187,2188,2204,2225,2240,2258,2277,2292,2303],{},[849,2189,2190,2195,2198],{},[867,2191,2192],{},[219,2193,2194],{},"TypeError: Only valid with DatetimeIndex",[867,2196,2197],{},"Grouping key is not datetime",[867,2199,2200,2203],{},[219,2201,2202],{},"pd.to_datetime"," the column first.",[849,2205,2206,2209,2215],{},[867,2207,2208],{},"Empty months missing",[867,2210,2211,2214],{},[219,2212,2213],{},"groupby"," emits only observed groups",[867,2216,2217,2220,2221,2224],{},[219,2218,2219],{},"asfreq(freq, fill_value=0)"," or ",[219,2222,2223],{},"reindex"," a full range.",[849,2226,2227,2230,2233],{},[867,2228,2229],{},"Months sort alphabetically in Excel",[867,2231,2232],{},"Month written as text",[867,2234,2235,2236,2239],{},"Keep timestamps and use a ",[219,2237,2238],{},"mmm yyyy"," number format.",[849,2241,2242,2245,2248],{},[867,2243,2244],{},"Fiscal quarters one quarter out",[867,2246,2247],{},"Wrong anchor month",[867,2249,2250,2251,2254,2255,2257],{},"The anchor names the ",[760,2252,2253],{},"ending"," month: ",[219,2256,1323],{}," for a March year end.",[849,2259,2260,2263,2266],{},[867,2261,2262],{},"Weekly buckets start on the wrong day",[867,2264,2265],{},"Default week anchor",[867,2267,2268,2269,2220,2271,2274,2275,18],{},"Use ",[219,2270,921],{},[219,2272,2273],{},"W-SUN",", plus ",[219,2276,1632],{},[849,2278,2279,2282,2285],{},[867,2280,2281],{},"Totals too low",[867,2283,2284],{},"Unparseable dates dropped silently",[867,2286,2287,2288,2291],{},"Count and report the ",[219,2289,2290],{},"NaT"," rows before dropping.",[849,2293,2294,2297,2300],{},[867,2295,2296],{},"Late-evening rows in the wrong month",[867,2298,2299],{},"Timezone not normalised",[867,2301,2302],{},"Convert to the report zone first.",[849,2304,2305,2317,2320],{},[867,2306,2307,2310,2311,2220,2314],{},[219,2308,2309],{},"FutureWarning"," about ",[219,2312,2313],{},"M",[219,2315,2316],{},"Q",[867,2318,2319],{},"Older frequency aliases",[867,2321,2268,2322,2324,2325,2327,2328,2324,2330,18],{},[219,2323,884],{},"\u002F",[219,2326,871],{}," and ",[219,2329,909],{},[219,2331,896],{},[207,2333,2335],{"id":2334},"performance-and-scale-notes","Performance and scale notes",[10,2337,2338,2339,2342],{},"Grouping is fast; getting to a groupable column is where time goes. On a workbook of a million rows, parsing dominates — so parse once with an explicit format, as covered in the ",[14,2340,2341],{"href":618},"parsing guide",", and never inside a loop.",[10,2344,2345],{},"Three habits that matter at scale:",[10,2347,2348,2352,2353,2327,2356,2359],{},[2349,2350,2351],"strong",{},"Read only the columns you aggregate."," A summary over ",[219,2354,2355],{},"date",[219,2357,2358],{},"amount"," has no reason to materialise thirty other columns:",[212,2361,2363],{"className":247,"code":2362,"language":249,"meta":217,"style":217},"df = pd.read_excel(\"sales.xlsx\", usecols=[\"date\", \"amount\", \"region\"])\n",[219,2364,2365],{"__ignoreMap":217},[222,2366,2367,2369,2371,2373,2375,2377,2380,2382,2384,2386,2388,2390,2392,2394],{"class":162,"line":224},[222,2368,493],{"class":260},[222,2370,283],{"class":256},[222,2372,498],{"class":260},[222,2374,444],{"class":231},[222,2376,304],{"class":260},[222,2378,2379],{"class":449},"usecols",[222,2381,283],{"class":256},[222,2383,607],{"class":260},[222,2385,510],{"class":231},[222,2387,304],{"class":260},[222,2389,685],{"class":231},[222,2391,304],{"class":260},[222,2393,1682],{"class":231},[222,2395,612],{"class":260},[10,2397,2398,2404],{},[2349,2399,2400,2401,18],{},"Prefer named aggregation to ",[219,2402,2403],{},"apply"," The named form dispatches to vectorised C implementations; a lambda runs Python per group:",[212,2406,2408],{"className":247,"code":2407,"language":249,"meta":217,"style":217},"# Fast — one vectorised pass per statistic.\nsummary = df.groupby(pd.Grouper(key=\"date\", freq=\"MS\")).agg(\n    revenue=(\"amount\", \"sum\"),\n    orders=(\"amount\", \"size\"),\n    largest=(\"amount\", \"max\"),\n)\n",[219,2409,2410,2415,2442,2460,2477,2495],{"__ignoreMap":217},[222,2411,2412],{"class":162,"line":224},[222,2413,2414],{"class":726},"# Fast — one vectorised pass per statistic.\n",[222,2416,2417,2420,2422,2425,2427,2429,2431,2433,2435,2437,2439],{"class":162,"line":270},[222,2418,2419],{"class":260},"summary ",[222,2421,283],{"class":256},[222,2423,2424],{"class":260}," df.groupby(pd.Grouper(",[222,2426,653],{"class":449},[222,2428,283],{"class":256},[222,2430,510],{"class":231},[222,2432,304],{"class":260},[222,2434,662],{"class":449},[222,2436,283],{"class":256},[222,2438,667],{"class":231},[222,2440,2441],{"class":260},")).agg(\n",[222,2443,2444,2447,2449,2451,2453,2455,2457],{"class":162,"line":277},[222,2445,2446],{"class":449},"    revenue",[222,2448,283],{"class":256},[222,2450,568],{"class":260},[222,2452,685],{"class":231},[222,2454,304],{"class":260},[222,2456,690],{"class":231},[222,2458,2459],{"class":260},"),\n",[222,2461,2462,2465,2467,2469,2471,2473,2475],{"class":162,"line":289},[222,2463,2464],{"class":449},"    orders",[222,2466,283],{"class":256},[222,2468,568],{"class":260},[222,2470,685],{"class":231},[222,2472,304],{"class":260},[222,2474,707],{"class":231},[222,2476,2459],{"class":260},[222,2478,2479,2482,2484,2486,2488,2490,2493],{"class":162,"line":298},[222,2480,2481],{"class":449},"    largest",[222,2483,283],{"class":256},[222,2485,568],{"class":260},[222,2487,685],{"class":231},[222,2489,304],{"class":260},[222,2491,2492],{"class":231},"\"max\"",[222,2494,2459],{"class":260},[222,2496,2497],{"class":162,"line":318},[222,2498,458],{"class":260},[10,2500,2501,2504],{},[2349,2502,2503],{},"Aggregate chunk by chunk for files that do not fit in memory."," Monthly sums are additive, so partial results combine cleanly:",[212,2506,2508],{"className":247,"code":2507,"language":249,"meta":217,"style":217},"import pandas as pd\n\ntotals = None\nfor chunk in pd.read_csv(\"huge_export.csv\", parse_dates=[\"date\"],\n                         usecols=[\"date\", \"amount\"], chunksize=200_000):\n    part = chunk.groupby(pd.Grouper(key=\"date\", freq=\"MS\"))[\"amount\"].sum()\n    totals = part if totals is None else totals.add(part, fill_value=0)\n\nmonthly = totals.sort_index().asfreq(\"MS\", fill_value=0)\n",[219,2509,2510,2520,2524,2534,2563,2591,2621,2656,2660],{"__ignoreMap":217},[222,2511,2512,2514,2516,2518],{"class":162,"line":224},[222,2513,257],{"class":256},[222,2515,261],{"class":260},[222,2517,264],{"class":256},[222,2519,267],{"class":260},[222,2521,2522],{"class":162,"line":270},[222,2523,274],{"emptyLinePlaceholder":273},[222,2525,2526,2529,2531],{"class":162,"line":277},[222,2527,2528],{"class":260},"totals ",[222,2530,283],{"class":256},[222,2532,2533],{"class":396}," None\n",[222,2535,2536,2539,2542,2544,2547,2550,2552,2555,2557,2559,2561],{"class":162,"line":289},[222,2537,2538],{"class":256},"for",[222,2540,2541],{"class":260}," chunk ",[222,2543,2077],{"class":256},[222,2545,2546],{"class":260}," pd.read_csv(",[222,2548,2549],{"class":231},"\"huge_export.csv\"",[222,2551,304],{"class":260},[222,2553,2554],{"class":449},"parse_dates",[222,2556,283],{"class":256},[222,2558,607],{"class":260},[222,2560,510],{"class":231},[222,2562,385],{"class":260},[222,2564,2565,2568,2570,2572,2574,2576,2578,2580,2583,2585,2588],{"class":162,"line":298},[222,2566,2567],{"class":449},"                         usecols",[222,2569,283],{"class":256},[222,2571,607],{"class":260},[222,2573,510],{"class":231},[222,2575,304],{"class":260},[222,2577,685],{"class":231},[222,2579,523],{"class":260},[222,2581,2582],{"class":449},"chunksize",[222,2584,283],{"class":256},[222,2586,2587],{"class":396},"200_000",[222,2589,2590],{"class":260},"):\n",[222,2592,2593,2596,2598,2601,2603,2605,2607,2609,2611,2613,2615,2617,2619],{"class":162,"line":318},[222,2594,2595],{"class":260},"    part ",[222,2597,283],{"class":256},[222,2599,2600],{"class":260}," chunk.groupby(pd.Grouper(",[222,2602,653],{"class":449},[222,2604,283],{"class":256},[222,2606,510],{"class":231},[222,2608,304],{"class":260},[222,2610,662],{"class":449},[222,2612,283],{"class":256},[222,2614,667],{"class":231},[222,2616,1614],{"class":260},[222,2618,685],{"class":231},[222,2620,811],{"class":260},[222,2622,2623,2626,2628,2631,2633,2636,2639,2642,2645,2648,2650,2652,2654],{"class":162,"line":341},[222,2624,2625],{"class":260},"    totals ",[222,2627,283],{"class":256},[222,2629,2630],{"class":260}," part ",[222,2632,557],{"class":256},[222,2634,2635],{"class":260}," totals ",[222,2637,2638],{"class":256},"is",[222,2640,2641],{"class":396}," None",[222,2643,2644],{"class":256}," else",[222,2646,2647],{"class":260}," totals.add(part, ",[222,2649,1042],{"class":449},[222,2651,283],{"class":256},[222,2653,42],{"class":396},[222,2655,458],{"class":260},[222,2657,2658],{"class":162,"line":347},[222,2659,274],{"emptyLinePlaceholder":273},[222,2661,2662,2664,2666,2669,2671,2673,2675,2677,2679],{"class":162,"line":388},[222,2663,640],{"class":260},[222,2665,283],{"class":256},[222,2667,2668],{"class":260}," totals.sort_index().asfreq(",[222,2670,667],{"class":231},[222,2672,304],{"class":260},[222,2674,1042],{"class":449},[222,2676,283],{"class":256},[222,2678,42],{"class":396},[222,2680,458],{"class":260},[10,2682,2683],{},"That pattern works for sums, counts, minimums and maximums. Means need care — accumulate the sum and the count separately and divide at the end, rather than averaging the chunk averages, which weights small chunks equally with large ones.",[207,2685,2687],{"id":2686},"conclusion","Conclusion",[10,2689,2690,2691,2693,2694,2697,2698,2700,2701,2703,2704,2706],{},"Grouping Excel rows into periods is a one-liner surrounded by three decisions. Use ",[219,2692,629],{}," with a start-anchored frequency for report keys and ",[219,2695,2696],{},"to_period"," for display labels. Fill the empty periods explicitly, with ",[219,2699,1057],{}," for the observed range or ",[219,2702,2223],{}," for a fixed reporting window, so a quiet month reads as zero rather than disappearing. Anchor fiscal quarters with the month the fiscal year ends in. Then write the period column back as a real timestamp with a ",[219,2705,2238],{}," format, so Excel's own sorting keeps working for whoever opens the file.",[207,2708,2710],{"id":2709},"frequently-asked-questions","Frequently asked questions",[10,2712,2713,2721,2723,2724,2727,2728,2731,2732,2734,2735,2738,2739,2742,2743,2745,2746,2748],{},[2349,2714,2715,2716,2327,2718,2720],{},"What is the difference between ",[219,2717,2696],{},[219,2719,629],{},"?",[219,2722,2696],{}," produces a ",[219,2725,2726],{},"Period"," label such as ",[219,2729,2730],{},"2026-08",", which is compact and reads well in a report column. ",[219,2733,629],{}," with ",[219,2736,2737],{},"freq=\"MS\""," produces a real ",[219,2740,2741],{},"Timestamp"," at the start of each month, which sorts correctly and joins with other date-keyed data. Use ",[219,2744,2696],{}," for display and ",[219,2747,629],{}," for keys.",[10,2750,2751,2754,2756,2757,2760,2761,2764],{},[2349,2752,2753],{},"Why are months with no data missing from my summary?",[219,2755,2213],{}," only emits groups that exist. Call ",[219,2758,2759],{},"asfreq(\"MS\", fill_value=0)"," or reindex against a full ",[219,2762,2763],{},"date_range"," afterwards so quiet months appear as zero instead of vanishing.",[10,2766,2767,2770,2771,2734,2773,2775,2776,2734,2779,2781],{},[2349,2768,2769],{},"How do I group by a fiscal year that ends in March?","\nUse the anchored frequency ",[219,2772,1323],{},[219,2774,2696],{},", or the offset alias ",[219,2777,2778],{},"\"QE-MAR\"",[219,2780,629],{},". April then falls into the first quarter of the following fiscal year, which is what accounting expects.",[10,2783,2784,2793,2795,2796,2798,2799,2734,2801,2803],{},[2349,2785,2786,2787,2790,2791,2720],{},"Should I use ",[219,2788,2789],{},"resample"," instead of ",[219,2792,2213],{},[219,2794,2789],{}," is ",[219,2797,2213],{}," with a datetime index and gap filling built in. It is the cleaner choice for a single continuous series; ",[219,2800,2213],{},[219,2802,629],{}," is better when you are also grouping by another column such as region.",[10,2805,2806,2809,2810,2220,2812,2814,2815,2817],{},[2349,2807,2808],{},"My month column sorts alphabetically — how do I fix it?","\nThe column is text. Sort on the underlying ",[219,2811,2726],{},[219,2813,2741],{}," before converting to a string for display, or keep the ",[219,2816,2726],{}," dtype until the moment you write the file.",[207,2819,2821],{"id":2820},"related","Related",[2823,2824,2825,2832,2838,2845,2851],"ul",{},[2826,2827,2828,2829,2831],"li",{},"Up to the parent: ",[14,2830,17],{"href":16}," — the date model behind the grouping keys.",[2826,2833,2834,2837],{},[14,2835,2836],{"href":618},"Parse Excel Dates into Python datetimes with pandas"," — getting the column groupable in the first place.",[2826,2839,2840,2844],{},[14,2841,2843],{"href":2842},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcreate-pivot-table-from-excel-with-pandas\u002F","Create a Pivot Table from Excel with pandas"," — the two-dimensional version of this summary.",[2826,2846,2847,2850],{},[14,2848,2849],{"href":1221},"Add a Line Chart to an Excel Report with Python"," — plotting the series, gaps included.",[2826,2852,2853,2857],{},[14,2854,2856],{"href":2855},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler\u002F","Schedule Recurring Excel Reports with APScheduler"," — running this summary every month unattended.",[2859,2860,2861],"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}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":217,"searchDepth":270,"depth":270,"links":2863},[2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874,2875],{"id":209,"depth":270,"text":210},{"id":461,"depth":270,"text":462},{"id":622,"depth":270,"text":623},{"id":961,"depth":270,"text":962},{"id":1226,"depth":270,"text":1227},{"id":1640,"depth":270,"text":1641},{"id":1771,"depth":270,"text":1772},{"id":2169,"depth":270,"text":2170},{"id":2334,"depth":270,"text":2335},{"id":2686,"depth":270,"text":2687},{"id":2709,"depth":270,"text":2710},{"id":2820,"depth":270,"text":2821},"2026-08-15","Aggregate Excel data into monthly and quarterly totals with pandas — Grouper vs to_period, filling empty periods, fiscal years, and writing the result back as a formatted report.","md",[2880,2883,2885,2887,2890],{"q":2881,"a":2882},"What is the difference between to_period and Grouper?","to_period produces a Period label such as 2026-08, which is compact and reads well in a report column. Grouper with freq=\"MS\" produces a real Timestamp at the start of each month, which sorts correctly and joins with other date-keyed data. Use to_period for display and Grouper for keys.",{"q":2753,"a":2884},"groupby only emits groups that exist. Call asfreq(\"MS\", fill_value=0) or reindex against a full date_range afterwards so quiet months appear as zero instead of vanishing.",{"q":2769,"a":2886},"Use the anchored frequency Q-MAR with to_period, or the offset alias \"QE-MAR\" with Grouper. April then falls into the first quarter of the following fiscal year, which is what accounting expects.",{"q":2888,"a":2889},"Should I use resample instead of groupby?","resample is groupby with a datetime index and gap filling built in. It is the cleaner choice for a single continuous series; groupby with Grouper is better when you are also grouping by another column such as region.",{"q":2808,"a":2891},"The column is text. Sort on the underlying Period or Timestamp before converting to a string for display, or keep the Period dtype until the moment you write the file.",{},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas",{"title":2895,"description":2896},"Group Excel Data by Month and Quarter in pandas","Turn daily Excel rows into month and quarter summaries: pd.Grouper, dt.to_period, asfreq to fill gaps, fiscal-year offsets, and writing the aggregate back to a workbook.","group-excel-rows-by-month-and-quarter-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002Findex","how-to","nJXTIBReEkGkjqDLu9nRV5bjart6hhIbFpPQs95GMgQ",[2902,2906],{"title":2903,"path":2904,"stem":2905,"children":-1},"Fix Excel Serial Numbers Showing Instead of Dates","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates\u002Findex",{"title":2907,"path":2908,"stem":2909,"children":-1},"Handle Timezones in Excel Timestamps with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python\u002Findex",1786800027129]