[{"data":1,"prerenderedAt":2051},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas":2017,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas":2026,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas":2031,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas":2032},{"id":4,"title":5,"body":6,"dateModified":1983,"datePublished":1983,"description":1984,"extension":1985,"faq":1986,"keyword":1995,"kind":1996,"meta":1997,"navigation":266,"path":2005,"seo":2006,"slug":2009,"stem":2010,"topics":2011,"type":2015,"__hash__":2016},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas\u002Findex.md","EOMONTH and EDATE Equivalents in pandas",{"type":7,"value":8,"toc":1969},"minimark",[9,36,187,192,222,225,445,449,460,524,545,549,555,708,726,842,846,852,913,923,1058,1070,1181,1185,1192,1287,1295,1299,1305,1515,1525,1529,1543,1699,1713,1717,1724,1827,1834,1838,1883,1887,1906,1910,1916,1922,1928,1934,1938,1965],[10,11,12,13,17,18,21,22,25,26,29,30,35],"p",{},"Contract end dates, invoice due dates, reporting periods and subscription renewals all rest on two Excel functions. ",[14,15,16],"code",{},"EDATE(start, months)"," moves a date by whole months, and ",[14,19,20],{},"EOMONTH(start, months)"," returns the last day of the month that many months away. pandas has the pieces to do the same for a whole column at once, but its most obvious tool, ",[14,23,24],{},"MonthEnd",", behaves differently from ",[14,27,28],{},"EOMONTH"," in a way that quietly shifts some dates by a month. This guide, part of ",[31,32,34],"a",{"href":33},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",", gives exact equivalents, checked against a spreadsheet, and covers the edge cases that matter in schedules.",[37,38,46,47,46,51,46,55,46,63,46,71,46,81,46,92,46,99,46,104,46,108,46,112,46,116,46,121,46,126,46,129,46,131,46,133,46,138,46,145,46,150,46,153,46,155,46,157,46,162,46,169,46,175,46,179,46,183,46,185],"svg",{"viewBox":39,"role":40,"ariaLabelledBy":41,"xmlns":44,"style":45},"4 0 752 313","img",[42,43],"em-timeline-t","em-timeline-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[48,49,50],"title",{"id":42},"EDATE and EOMONTH from 15 January and 31 January",[52,53,54],"desc",{"id":43},"From 15 January, EDATE one month gives 15 February and EOMONTH one month gives 28 February. From 31 January, EDATE one month also gives 28 February because the day is clamped to the shorter month, and EOMONTH one month again gives 28 February.",[56,57],"rect",{"x":58,"y":59,"width":60,"height":61,"fill":62},"4","0","752","313","#ffffff",[64,65],"line",{"x1":66,"y1":67,"x2":68,"y2":67,"stroke":69,"style":70},"94.0","156.5","666.0","var(--muted,#5b6780)","stroke-width:2.5px",[56,72],{"x":73,"y":74,"width":75,"height":76,"rx":77,"fill":78,"stroke":79,"style":80},"20.0","16.0","172.0","108.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[82,83,85],"g",{"transform":84},"translate(94.0,37.5) scale(1.000)",[86,87],"path",{"d":88,"fill":89,"stroke":90,"style":91},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[93,94,98],"text",{"x":95,"y":96,"style":97},"106.0","83.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","15 Jan",[93,100,103],{"x":95,"y":101,"style":102},"101.5","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","start date",[64,105],{"x1":95,"y1":106,"x2":95,"y2":67,"stroke":69,"style":107},"124.5","stroke-width:1.5px",[109,110],"circle",{"cx":95,"cy":67,"r":111,"fill":69,"stroke":62,"style":80},"7.0",[56,113],{"x":114,"y":115,"width":75,"height":76,"rx":77,"fill":78,"stroke":79,"style":80},"202.7","188.5",[82,117,119],{"transform":118},"translate(276.7,210.0) scale(1.000)",[86,120],{"d":88,"fill":89,"stroke":90,"style":91},[93,122,125],{"x":123,"y":124,"style":97},"288.7","255.5","31 Jan",[93,127,103],{"x":123,"y":128,"style":102},"274.0",[64,130],{"x1":123,"y1":67,"x2":123,"y2":115,"stroke":69,"style":107},[109,132],{"cx":123,"cy":67,"r":111,"fill":69,"stroke":62,"style":80},[56,134],{"x":135,"y":74,"width":75,"height":76,"rx":77,"fill":136,"stroke":137,"style":80},"385.3","#f0f4ff","var(--brand,#5b5cf0)",[82,139,141],{"transform":140},"translate(459.3,37.5) scale(1.000)",[86,142],{"d":143,"fill":89,"stroke":144,"style":91},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--brand-strong,#4338ca)",[93,146,149],{"x":147,"y":96,"style":148},"471.3","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","15 Feb",[93,151,152],{"x":147,"y":101,"style":102},"EDATE(15 Jan, 1)",[64,154],{"x1":147,"y1":106,"x2":147,"y2":67,"stroke":69,"style":107},[109,156],{"cx":147,"cy":67,"r":111,"fill":137,"stroke":62,"style":80},[56,158],{"x":159,"y":115,"width":75,"height":76,"rx":77,"fill":160,"stroke":161,"style":80},"568.0","#d9f4f1","var(--teal,#0f9488)",[82,163,165],{"transform":164},"translate(642.0,201.5) scale(1.000)",[86,166],{"d":167,"fill":89,"stroke":168,"style":91},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[93,170,174],{"x":171,"y":172,"style":173},"654.0","247.0","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","28 Feb",[93,176,178],{"x":171,"y":177,"style":102},"265.5","EDATE(31 Jan, 1),",[93,180,182],{"x":171,"y":181,"style":102},"282.5","EOMONTH(either, 1)",[64,184],{"x1":171,"y1":67,"x2":171,"y2":115,"stroke":69,"style":107},[109,186],{"cx":171,"cy":67,"r":111,"fill":161,"stroke":62,"style":80},[188,189,191],"h2",{"id":190},"prerequisites","Prerequisites",[193,194,199],"pre",{"className":195,"code":196,"language":197,"meta":198,"style":198},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas numpy openpyxl\n","bash","",[14,200,201],{"__ignoreMap":198},[202,203,205,209,213,216,219],"span",{"class":64,"line":204},1,[202,206,208],{"class":207},"sMTad","pip",[202,210,212],{"class":211},"srMev"," install",[202,214,215],{"class":211}," pandas",[202,217,218],{"class":211}," numpy",[202,220,221],{"class":211}," openpyxl\n",[10,223,224],{},"Dates that exercise the edge cases: a 31st, a mid-month date, a leap day, a date with a time and a year end:",[193,226,230],{"className":227,"code":228,"language":229,"meta":198,"style":198},"language-python shiki shiki-themes github-light github-dark-high-contrast","import numpy as np\nimport pandas as pd\n\ncontracts = pd.DataFrame({\n    \"contract\": [\"C1\", \"C2\", \"C3\", \"C4\", \"C5\"],\n    \"start\": pd.to_datetime([\"2026-01-31\", \"2026-01-15\", \"2024-02-29\",\n                             \"2026-03-31 14:30\", \"2026-12-31\"], format=\"mixed\"),\n    \"term_months\": [1, 12, -12, 2, 3],\n})\ncontracts.to_excel(\"contracts.xlsx\", index=False)\ndf = pd.read_excel(\"contracts.xlsx\")\n","python",[14,231,232,248,261,268,280,316,341,367,402,408,430],{"__ignoreMap":198},[202,233,234,238,242,245],{"class":64,"line":204},[202,235,237],{"class":236},"s-kum","import",[202,239,241],{"class":240},"skGVy"," numpy ",[202,243,244],{"class":236},"as",[202,246,247],{"class":240}," np\n",[202,249,251,253,256,258],{"class":64,"line":250},2,[202,252,237],{"class":236},[202,254,255],{"class":240}," pandas ",[202,257,244],{"class":236},[202,259,260],{"class":240}," pd\n",[202,262,264],{"class":64,"line":263},3,[202,265,267],{"emptyLinePlaceholder":266},true,"\n",[202,269,271,274,277],{"class":64,"line":270},4,[202,272,273],{"class":240},"contracts ",[202,275,276],{"class":236},"=",[202,278,279],{"class":240}," pd.DataFrame({\n",[202,281,283,286,289,292,295,298,300,303,305,308,310,313],{"class":64,"line":282},5,[202,284,285],{"class":211},"    \"contract\"",[202,287,288],{"class":240},": [",[202,290,291],{"class":211},"\"C1\"",[202,293,294],{"class":240},", ",[202,296,297],{"class":211},"\"C2\"",[202,299,294],{"class":240},[202,301,302],{"class":211},"\"C3\"",[202,304,294],{"class":240},[202,306,307],{"class":211},"\"C4\"",[202,309,294],{"class":240},[202,311,312],{"class":211},"\"C5\"",[202,314,315],{"class":240},"],\n",[202,317,319,322,325,328,330,333,335,338],{"class":64,"line":318},6,[202,320,321],{"class":211},"    \"start\"",[202,323,324],{"class":240},": pd.to_datetime([",[202,326,327],{"class":211},"\"2026-01-31\"",[202,329,294],{"class":240},[202,331,332],{"class":211},"\"2026-01-15\"",[202,334,294],{"class":240},[202,336,337],{"class":211},"\"2024-02-29\"",[202,339,340],{"class":240},",\n",[202,342,344,347,349,352,355,359,361,364],{"class":64,"line":343},7,[202,345,346],{"class":211},"                             \"2026-03-31 14:30\"",[202,348,294],{"class":240},[202,350,351],{"class":211},"\"2026-12-31\"",[202,353,354],{"class":240},"], ",[202,356,358],{"class":357},"sa561","format",[202,360,276],{"class":236},[202,362,363],{"class":211},"\"mixed\"",[202,365,366],{"class":240},"),\n",[202,368,370,373,375,379,381,383,385,388,390,392,395,397,400],{"class":64,"line":369},8,[202,371,372],{"class":211},"    \"term_months\"",[202,374,288],{"class":240},[202,376,378],{"class":377},"sP0c6","1",[202,380,294],{"class":240},[202,382,77],{"class":377},[202,384,294],{"class":240},[202,386,387],{"class":236},"-",[202,389,77],{"class":377},[202,391,294],{"class":240},[202,393,394],{"class":377},"2",[202,396,294],{"class":240},[202,398,399],{"class":377},"3",[202,401,315],{"class":240},[202,403,405],{"class":64,"line":404},9,[202,406,407],{"class":240},"})\n",[202,409,411,414,417,419,422,424,427],{"class":64,"line":410},10,[202,412,413],{"class":240},"contracts.to_excel(",[202,415,416],{"class":211},"\"contracts.xlsx\"",[202,418,294],{"class":240},[202,420,421],{"class":357},"index",[202,423,276],{"class":236},[202,425,426],{"class":377},"False",[202,428,429],{"class":240},")\n",[202,431,433,436,438,441,443],{"class":64,"line":432},11,[202,434,435],{"class":240},"df ",[202,437,276],{"class":236},[202,439,440],{"class":240}," pd.read_excel(",[202,442,416],{"class":211},[202,444,429],{"class":240},[188,446,448],{"id":447},"step-1-edate-with-dateoffset","Step 1 — EDATE with DateOffset",[10,450,451,452,455,456,459],{},"For a fixed number of months, add a ",[14,453,454],{},"DateOffset",". It moves the month and clamps the day to the end of shorter months, exactly as ",[14,457,458],{},"EDATE"," does:",[193,461,463],{"className":227,"code":462,"language":229,"meta":198,"style":198},"plus_one = (df[\"start\"] + pd.DateOffset(months=1)).dt.normalize()\nprint(plus_one.dt.strftime(\"%Y-%m-%d\").tolist())\n# ['2026-02-28', '2026-02-15', '2024-03-29', '2026-04-30', '2027-01-31']\n",[14,464,465,497,518],{"__ignoreMap":198},[202,466,467,470,472,475,478,481,484,487,490,492,494],{"class":64,"line":204},[202,468,469],{"class":240},"plus_one ",[202,471,276],{"class":236},[202,473,474],{"class":240}," (df[",[202,476,477],{"class":211},"\"start\"",[202,479,480],{"class":240},"] ",[202,482,483],{"class":236},"+",[202,485,486],{"class":240}," pd.DateOffset(",[202,488,489],{"class":357},"months",[202,491,276],{"class":236},[202,493,378],{"class":377},[202,495,496],{"class":240},")).dt.normalize()\n",[202,498,499,502,505,508,512,515],{"class":64,"line":250},[202,500,501],{"class":377},"print",[202,503,504],{"class":240},"(plus_one.dt.strftime(",[202,506,507],{"class":211},"\"%Y-%m-",[202,509,511],{"class":510},"sSjpA","%d",[202,513,514],{"class":211},"\"",[202,516,517],{"class":240},").tolist())\n",[202,519,520],{"class":64,"line":263},[202,521,523],{"class":522},"s-wDw","# ['2026-02-28', '2026-02-15', '2024-03-29', '2026-04-30', '2027-01-31']\n",[10,525,526,527,530,531,534,535,538,539,541,542,544],{},"Recalculating ",[14,528,529],{},"=EDATE(DATE(2026,1,31),1)"," in a spreadsheet gives 28 February 2026, and ",[14,532,533],{},"=EDATE(DATE(2024,2,29),-12)"," gives 28 February 2023 — the same clamping. The ",[14,536,537],{},"normalize()"," matters for C4: ",[14,540,454],{}," keeps the 14:30 time, but ",[14,543,458],{}," returns a whole date and drops it. Leave the time in and a later comparison such as \"due date is before today\" can flip on the due day itself.",[188,546,548],{"id":547},"step-2-edate-with-a-different-number-of-months-per-row","Step 2 — EDATE with a different number of months per row",[10,550,551,552,554],{},"In the workbook, the months argument usually points at a cell — each contract has its own term. ",[14,553,454],{}," takes a single number, so for a column of month counts, use monthly periods: shift the month, then put back the original day, clamped to the length of the target month:",[193,556,558],{"className":227,"code":557,"language":229,"meta":198,"style":198},"def edate(dates, months):\n    dates = pd.to_datetime(dates)\n    target = dates.dt.to_period(\"M\") + np.asarray(months, dtype=int)\n    day = np.minimum(dates.dt.day, target.dt.days_in_month)\n    return target.dt.to_timestamp() + pd.to_timedelta(day - 1, unit=\"D\")\n\ndf[\"end\"] = edate(df[\"start\"], df[\"term_months\"])\nprint(df[\"end\"].dt.strftime(\"%Y-%m-%d\").tolist())\n# ['2026-02-28', '2027-01-15', '2023-02-28', '2026-05-31', '2027-03-31']\n",[14,559,560,572,582,613,623,653,657,683,703],{"__ignoreMap":198},[202,561,562,565,569],{"class":64,"line":204},[202,563,564],{"class":236},"def",[202,566,568],{"class":567},"s_Opv"," edate",[202,570,571],{"class":240},"(dates, months):\n",[202,573,574,577,579],{"class":64,"line":250},[202,575,576],{"class":240},"    dates ",[202,578,276],{"class":236},[202,580,581],{"class":240}," pd.to_datetime(dates)\n",[202,583,584,587,589,592,595,598,600,603,606,608,611],{"class":64,"line":263},[202,585,586],{"class":240},"    target ",[202,588,276],{"class":236},[202,590,591],{"class":240}," dates.dt.to_period(",[202,593,594],{"class":211},"\"M\"",[202,596,597],{"class":240},") ",[202,599,483],{"class":236},[202,601,602],{"class":240}," np.asarray(months, ",[202,604,605],{"class":357},"dtype",[202,607,276],{"class":236},[202,609,610],{"class":377},"int",[202,612,429],{"class":240},[202,614,615,618,620],{"class":64,"line":270},[202,616,617],{"class":240},"    day ",[202,619,276],{"class":236},[202,621,622],{"class":240}," np.minimum(dates.dt.day, target.dt.days_in_month)\n",[202,624,625,628,631,633,636,638,641,643,646,648,651],{"class":64,"line":282},[202,626,627],{"class":236},"    return",[202,629,630],{"class":240}," target.dt.to_timestamp() ",[202,632,483],{"class":236},[202,634,635],{"class":240}," pd.to_timedelta(day ",[202,637,387],{"class":236},[202,639,640],{"class":377}," 1",[202,642,294],{"class":240},[202,644,645],{"class":357},"unit",[202,647,276],{"class":236},[202,649,650],{"class":211},"\"D\"",[202,652,429],{"class":240},[202,654,655],{"class":64,"line":318},[202,656,267],{"emptyLinePlaceholder":266},[202,658,659,662,665,667,669,672,674,677,680],{"class":64,"line":343},[202,660,661],{"class":240},"df[",[202,663,664],{"class":211},"\"end\"",[202,666,480],{"class":240},[202,668,276],{"class":236},[202,670,671],{"class":240}," edate(df[",[202,673,477],{"class":211},[202,675,676],{"class":240},"], df[",[202,678,679],{"class":211},"\"term_months\"",[202,681,682],{"class":240},"])\n",[202,684,685,687,690,692,695,697,699,701],{"class":64,"line":369},[202,686,501],{"class":377},[202,688,689],{"class":240},"(df[",[202,691,664],{"class":211},[202,693,694],{"class":240},"].dt.strftime(",[202,696,507],{"class":211},[202,698,511],{"class":510},[202,700,514],{"class":211},[202,702,517],{"class":240},[202,704,705],{"class":64,"line":404},[202,706,707],{"class":522},"# ['2026-02-28', '2027-01-15', '2023-02-28', '2026-05-31', '2027-03-31']\n",[10,709,710,711,714,715,717,718,721,722,725],{},"Everything happens on whole columns, so this is fast for hundreds of thousands of rows, and ",[14,712,713],{},"to_timestamp()"," produces midnight, dropping the time like ",[14,716,458],{},". Excel truncates fractional months — ",[14,719,720],{},"EDATE(date, 2.9)"," moves two months — and converting the months to integers with ",[14,723,724],{},"np.asarray(months, dtype=int)"," does the same. Negative fractions truncate towards zero in both.",[37,727,46,732,46,735,46,738,46,741,46,745,46,750,46,756,46,761,46,766,46,771,46,774,46,779,46,784,46,787,46,791,46,794,46,799,46,806,46,812,46,816,46,820,46,824,46,827,46,830,46,835,46,839],{"viewBox":728,"role":40,"ariaLabelledBy":729,"xmlns":44,"style":45},"4 0 752 142",[730,731],"em-edate-t","em-edate-d",[48,733,734],{"id":730},"How the per-row edate function works",[52,736,737],{"id":731},"Each date becomes a monthly period, the row's number of months is added, the original day number is clamped to the number of days in the target month, and the result is converted back to a date at midnight.",[56,739],{"x":58,"y":59,"width":60,"height":740,"fill":62},"142",[56,742],{"x":73,"y":74,"width":743,"height":744,"rx":77,"fill":136,"stroke":137,"style":80},"153.0","110.5",[82,746,748],{"transform":747},"translate(84.5,37.8) scale(1.000)",[86,749],{"d":88,"fill":89,"stroke":144,"style":91},[93,751,755],{"x":752,"y":753,"style":754},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","To month period",[93,757,760],{"x":752,"y":758,"style":759},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","2026-01",[64,762],{"x1":763,"y1":764,"x2":765,"y2":764,"stroke":137,"style":80},"178.0","71.2","200.0",[767,768],"polygon",{"points":769,"fill":770},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[56,772],{"x":773,"y":74,"width":743,"height":744,"rx":77,"fill":160,"stroke":161,"style":80},"209.0",[82,775,777],{"transform":776},"translate(273.5,37.8) scale(1.000)",[86,778],{"d":143,"fill":89,"stroke":168,"style":91},[93,780,783],{"x":781,"y":753,"style":782},"285.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Add months",[93,785,786],{"x":781,"y":758,"style":759},"per row",[64,788],{"x1":789,"y1":764,"x2":790,"y2":764,"stroke":137,"style":80},"367.0","389.0",[767,792],{"points":793,"fill":770},"393.0,71.2 384.0,76.2 384.0,66.2",[56,795],{"x":796,"y":74,"width":743,"height":744,"rx":77,"fill":797,"stroke":798,"style":80},"398.0","#fdefd8","var(--gold,#b4740a)",[82,800,802],{"transform":801},"translate(462.5,29.0) scale(1.000)",[86,803],{"d":804,"fill":89,"stroke":805,"style":91},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[93,807,811],{"x":808,"y":809,"style":810},"474.5","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Clamp the day",[93,813,815],{"x":808,"y":814,"style":759},"95.0","min(day,",[93,817,819],{"x":808,"y":818,"style":759},"112.5","days_in_month)",[64,821],{"x1":822,"y1":764,"x2":823,"y2":764,"stroke":137,"style":80},"556.0","578.0",[767,825],{"points":826,"fill":770},"582.0,71.2 573.0,76.2 573.0,66.2",[56,828],{"x":829,"y":74,"width":743,"height":744,"rx":77,"fill":136,"stroke":137,"style":80},"587.0",[82,831,833],{"transform":832},"translate(651.5,37.8) scale(1.000)",[86,834],{"d":167,"fill":89,"stroke":144,"style":91},[93,836,838],{"x":837,"y":753,"style":754},"663.5","Back to a date",[93,840,841],{"x":837,"y":758,"style":759},"midnight",[188,843,845],{"id":844},"step-3-eomonth-with-periods-not-monthend","Step 3 — EOMONTH with periods, not MonthEnd",[10,847,848,849,851],{},"The obvious pandas tool for month ends is the ",[14,850,24],{}," offset. It is also the trap:",[193,853,855],{"className":227,"code":854,"language":229,"meta":198,"style":198},"from pandas.tseries.offsets import MonthEnd\n\nprint((df[\"start\"] + MonthEnd(1)).dt.strftime(\"%Y-%m-%d\").tolist()[:2])\n# ['2026-02-28', '2026-01-31']\n",[14,856,857,870,874,908],{"__ignoreMap":198},[202,858,859,862,865,867],{"class":64,"line":204},[202,860,861],{"class":236},"from",[202,863,864],{"class":240}," pandas.tseries.offsets ",[202,866,237],{"class":236},[202,868,869],{"class":240}," MonthEnd\n",[202,871,872],{"class":64,"line":250},[202,873,267],{"emptyLinePlaceholder":266},[202,875,876,878,881,883,885,887,890,892,895,897,899,901,904,906],{"class":64,"line":263},[202,877,501],{"class":377},[202,879,880],{"class":240},"((df[",[202,882,477],{"class":211},[202,884,480],{"class":240},[202,886,483],{"class":236},[202,888,889],{"class":240}," MonthEnd(",[202,891,378],{"class":377},[202,893,894],{"class":240},")).dt.strftime(",[202,896,507],{"class":211},[202,898,511],{"class":510},[202,900,514],{"class":211},[202,902,903],{"class":240},").tolist()[:",[202,905,394],{"class":377},[202,907,682],{"class":240},[202,909,910],{"class":64,"line":270},[202,911,912],{"class":522},"# ['2026-02-28', '2026-01-31']\n",[10,914,915,918,919,922],{},[14,916,917],{},"MonthEnd(1)"," means \"roll forward to the next month end\". For 15 January that is 31 January — the current month — while for 31 January, already a month end, it is 28 February. ",[14,920,921],{},"EOMONTH(date, 1)"," returns the end of the following month for both. The period approach has no such dependency:",[193,924,926],{"className":227,"code":925,"language":229,"meta":198,"style":198},"def eomonth(dates, months=0):\n    dates = pd.to_datetime(dates)\n    target = dates.dt.to_period(\"M\") + np.asarray(months, dtype=int)\n    return target.dt.to_timestamp(how=\"end\").dt.normalize()\n\nprint(eomonth(df[\"start\"], 1).dt.strftime(\"%Y-%m-%d\").tolist())\n# ['2026-02-28', '2026-02-28', '2024-03-31', '2026-04-30', '2027-01-31']\nprint(eomonth(df[\"start\"], 0).dt.strftime(\"%Y-%m-%d\").tolist()[:2])\n# ['2026-01-31', '2026-01-31']\n",[14,927,928,945,953,977,994,998,1022,1027,1053],{"__ignoreMap":198},[202,929,930,932,935,938,940,942],{"class":64,"line":204},[202,931,564],{"class":236},[202,933,934],{"class":567}," eomonth",[202,936,937],{"class":240},"(dates, months",[202,939,276],{"class":236},[202,941,59],{"class":377},[202,943,944],{"class":240},"):\n",[202,946,947,949,951],{"class":64,"line":250},[202,948,576],{"class":240},[202,950,276],{"class":236},[202,952,581],{"class":240},[202,954,955,957,959,961,963,965,967,969,971,973,975],{"class":64,"line":263},[202,956,586],{"class":240},[202,958,276],{"class":236},[202,960,591],{"class":240},[202,962,594],{"class":211},[202,964,597],{"class":240},[202,966,483],{"class":236},[202,968,602],{"class":240},[202,970,605],{"class":357},[202,972,276],{"class":236},[202,974,610],{"class":377},[202,976,429],{"class":240},[202,978,979,981,984,987,989,991],{"class":64,"line":270},[202,980,627],{"class":236},[202,982,983],{"class":240}," target.dt.to_timestamp(",[202,985,986],{"class":357},"how",[202,988,276],{"class":236},[202,990,664],{"class":211},[202,992,993],{"class":240},").dt.normalize()\n",[202,995,996],{"class":64,"line":282},[202,997,267],{"emptyLinePlaceholder":266},[202,999,1000,1002,1005,1007,1009,1011,1014,1016,1018,1020],{"class":64,"line":318},[202,1001,501],{"class":377},[202,1003,1004],{"class":240},"(eomonth(df[",[202,1006,477],{"class":211},[202,1008,354],{"class":240},[202,1010,378],{"class":377},[202,1012,1013],{"class":240},").dt.strftime(",[202,1015,507],{"class":211},[202,1017,511],{"class":510},[202,1019,514],{"class":211},[202,1021,517],{"class":240},[202,1023,1024],{"class":64,"line":343},[202,1025,1026],{"class":522},"# ['2026-02-28', '2026-02-28', '2024-03-31', '2026-04-30', '2027-01-31']\n",[202,1028,1029,1031,1033,1035,1037,1039,1041,1043,1045,1047,1049,1051],{"class":64,"line":369},[202,1030,501],{"class":377},[202,1032,1004],{"class":240},[202,1034,477],{"class":211},[202,1036,354],{"class":240},[202,1038,59],{"class":377},[202,1040,1013],{"class":240},[202,1042,507],{"class":211},[202,1044,511],{"class":510},[202,1046,514],{"class":211},[202,1048,903],{"class":240},[202,1050,394],{"class":377},[202,1052,682],{"class":240},[202,1054,1055],{"class":64,"line":404},[202,1056,1057],{"class":522},"# ['2026-01-31', '2026-01-31']\n",[10,1059,1060,1063,1064,1066,1067,1069],{},[14,1061,1062],{},"to_timestamp(how=\"end\")"," returns the last nanosecond of the month; ",[14,1065,537],{}," brings it back to midnight so it compares equal to a plain date. The ",[14,1068,489],{}," argument accepts one number or a column, just like the Excel function referencing a cell.",[37,1071,46,1076,46,1079,46,1082,46,1085,46,1090,46,1097,46,1103,46,1109,46,1115,46,1123,46,1127,46,1132,46,1136,46,1141,46,1143,46,1148,46,1153,46,1157,46,1161,46,1166,46,1168,46,1173,46,1176],{"viewBox":1072,"role":40,"ariaLabelledBy":1073,"xmlns":44,"style":45},"4 0 752 207",[1074,1075],"em-trap-t","em-trap-d",[48,1077,1078],{"id":1074},"MonthEnd(1) versus EOMONTH(date, 1)",[52,1080,1081],{"id":1075},"MonthEnd(1) rolls forward to the next month end, so 15 January becomes 31 January while 31 January becomes 28 February. EOMONTH with one month and the period-based function return 28 February for both dates.",[56,1083],{"x":58,"y":59,"width":60,"height":1084,"fill":62},"207",[56,1086],{"x":73,"y":74,"width":1087,"height":1088,"rx":1089,"fill":797,"stroke":798,"style":80},"351.0","175.0","14",[82,1091,1093],{"transform":1092},"translate(182.5,29.0) scale(1.083)",[86,1094],{"d":1095,"fill":89,"stroke":805,"style":1096},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[93,1098,1102],{"x":1099,"y":1100,"style":1101},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","date + MonthEnd(1)",[64,1104],{"x1":1105,"y1":1106,"x2":1107,"y2":1106,"stroke":798,"style":1108},"36.0","91.0","355.0","stroke-width:1px",[93,1110,1114],{"x":1111,"y":1112,"style":1113},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","15 Jan → 31 Jan",[82,1116,1118],{"transform":1117},"translate(38.5,99.5) scale(0.625)",[86,1119],{"d":1120,"fill":89,"stroke":1121,"style":1122},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[93,1124,1126],{"x":1111,"y":1125,"style":1113},"137.0","31 Jan → 28 Feb",[82,1128,1130],{"transform":1129},"translate(38.5,125.5) scale(0.625)",[86,1131],{"d":167,"fill":89,"stroke":161,"style":1122},[93,1133,1135],{"x":1111,"y":1134,"style":1113},"163.0","result depends on the start day",[82,1137,1139],{"transform":1138},"translate(38.5,151.5) scale(0.625)",[86,1140],{"d":1120,"fill":89,"stroke":1121,"style":1122},[56,1142],{"x":790,"y":74,"width":1087,"height":1088,"rx":1089,"fill":160,"stroke":161,"style":80},[82,1144,1146],{"transform":1145},"translate(551.5,29.0) scale(1.083)",[86,1147],{"d":167,"fill":89,"stroke":168,"style":1096},[93,1149,1152],{"x":1150,"y":1100,"style":1151},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","eomonth(date, 1)",[64,1154],{"x1":1155,"y1":1106,"x2":1156,"y2":1106,"stroke":161,"style":1108},"405.0","724.0",[93,1158,1160],{"x":1159,"y":1112,"style":1113},"429.0","15 Jan → 28 Feb",[82,1162,1164],{"transform":1163},"translate(407.5,99.5) scale(0.625)",[86,1165],{"d":167,"fill":89,"stroke":161,"style":1122},[93,1167,1126],{"x":1159,"y":1125,"style":1113},[82,1169,1171],{"transform":1170},"translate(407.5,125.5) scale(0.625)",[86,1172],{"d":167,"fill":89,"stroke":161,"style":1122},[93,1174,1175],{"x":1159,"y":1134,"style":1113},"same as Excel for every date",[82,1177,1179],{"transform":1178},"translate(407.5,151.5) scale(0.625)",[86,1180],{"d":167,"fill":89,"stroke":161,"style":1122},[188,1182,1184],{"id":1183},"step-4-first-day-of-the-month","Step 4 — First day of the month",[10,1186,1187,1188,1191],{},"The Excel idiom ",[14,1189,1190],{},"=EOMONTH(date, -1) + 1"," gives the first day of the date's month. With periods it is one call:",[193,1193,1195],{"className":227,"code":1194,"language":229,"meta":198,"style":198},"first = df[\"start\"].dt.to_period(\"M\").dt.to_timestamp()\nprint(first.dt.strftime(\"%Y-%m-%d\").tolist())\n# ['2026-01-01', '2026-01-01', '2024-02-01', '2026-03-01', '2026-12-01']\nsame_as_excel = eomonth(df[\"start\"], -1) + pd.Timedelta(days=1)\nprint((first == same_as_excel).all())       # True\n",[14,1196,1197,1217,1232,1237,1271],{"__ignoreMap":198},[202,1198,1199,1202,1204,1207,1209,1212,1214],{"class":64,"line":204},[202,1200,1201],{"class":240},"first ",[202,1203,276],{"class":236},[202,1205,1206],{"class":240}," df[",[202,1208,477],{"class":211},[202,1210,1211],{"class":240},"].dt.to_period(",[202,1213,594],{"class":211},[202,1215,1216],{"class":240},").dt.to_timestamp()\n",[202,1218,1219,1221,1224,1226,1228,1230],{"class":64,"line":250},[202,1220,501],{"class":377},[202,1222,1223],{"class":240},"(first.dt.strftime(",[202,1225,507],{"class":211},[202,1227,511],{"class":510},[202,1229,514],{"class":211},[202,1231,517],{"class":240},[202,1233,1234],{"class":64,"line":263},[202,1235,1236],{"class":522},"# ['2026-01-01', '2026-01-01', '2024-02-01', '2026-03-01', '2026-12-01']\n",[202,1238,1239,1242,1244,1247,1249,1251,1253,1255,1257,1259,1262,1265,1267,1269],{"class":64,"line":270},[202,1240,1241],{"class":240},"same_as_excel ",[202,1243,276],{"class":236},[202,1245,1246],{"class":240}," eomonth(df[",[202,1248,477],{"class":211},[202,1250,354],{"class":240},[202,1252,387],{"class":236},[202,1254,378],{"class":377},[202,1256,597],{"class":240},[202,1258,483],{"class":236},[202,1260,1261],{"class":240}," pd.Timedelta(",[202,1263,1264],{"class":357},"days",[202,1266,276],{"class":236},[202,1268,378],{"class":377},[202,1270,429],{"class":240},[202,1272,1273,1275,1278,1281,1284],{"class":64,"line":282},[202,1274,501],{"class":377},[202,1276,1277],{"class":240},"((first ",[202,1279,1280],{"class":236},"==",[202,1282,1283],{"class":240}," same_as_excel).all())       ",[202,1285,1286],{"class":522},"# True\n",[10,1288,1289,1290,1294],{},"First-of-month dates are the usual key for monthly aggregation; grouping by them sorts naturally and writes to Excel as real dates. Monthly and fiscal grouping more broadly is covered in ",[31,1291,1293],{"href":1292},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-data-by-fiscal-year-and-iso-week\u002F","Group Excel Data by Fiscal Year and ISO Week",".",[188,1296,1298],{"id":1297},"step-5-build-month-end-schedules-without-drift","Step 5 — Build month-end schedules without drift",[10,1300,1301,1302,1304],{},"A payment schedule built by applying ",[14,1303,458],{}," repeatedly — each date one month after the previous — drifts. Start on 31 January and the second date is 28 February; one month after that is 28 March, not 31 March, and every later date sticks to the 28th:",[193,1306,1308],{"className":227,"code":1307,"language":229,"meta":198,"style":198},"chained = [pd.Timestamp(\"2026-01-31\")]\nfor _ in range(3):\n    chained.append(chained[-1] + pd.DateOffset(months=1))\nfrom_start = [pd.Timestamp(\"2026-01-31\") + pd.DateOffset(months=k) for k in range(4)]\nmonth_ends = eomonth(pd.Series([pd.Timestamp(\"2026-01-31\")] * 4), range(4))\nprint([d.strftime(\"%m-%d\") for d in chained])       # ['01-31', '02-28', '03-28', '04-28']\nprint([d.strftime(\"%m-%d\") for d in from_start])    # ['01-31', '02-28', '03-31', '04-30']\nprint(month_ends.dt.strftime(\"%m-%d\").tolist())     # ['01-31', '02-28', '03-31', '04-30']\n",[14,1309,1310,1325,1346,1370,1409,1442,1471,1497],{"__ignoreMap":198},[202,1311,1312,1315,1317,1320,1322],{"class":64,"line":204},[202,1313,1314],{"class":240},"chained ",[202,1316,276],{"class":236},[202,1318,1319],{"class":240}," [pd.Timestamp(",[202,1321,327],{"class":211},[202,1323,1324],{"class":240},")]\n",[202,1326,1327,1330,1333,1336,1339,1342,1344],{"class":64,"line":250},[202,1328,1329],{"class":236},"for",[202,1331,1332],{"class":240}," _ ",[202,1334,1335],{"class":236},"in",[202,1337,1338],{"class":377}," range",[202,1340,1341],{"class":240},"(",[202,1343,399],{"class":377},[202,1345,944],{"class":240},[202,1347,1348,1351,1353,1355,1357,1359,1361,1363,1365,1367],{"class":64,"line":263},[202,1349,1350],{"class":240},"    chained.append(chained[",[202,1352,387],{"class":236},[202,1354,378],{"class":377},[202,1356,480],{"class":240},[202,1358,483],{"class":236},[202,1360,486],{"class":240},[202,1362,489],{"class":357},[202,1364,276],{"class":236},[202,1366,378],{"class":377},[202,1368,1369],{"class":240},"))\n",[202,1371,1372,1375,1377,1379,1381,1383,1385,1387,1389,1391,1394,1396,1399,1401,1403,1405,1407],{"class":64,"line":270},[202,1373,1374],{"class":240},"from_start ",[202,1376,276],{"class":236},[202,1378,1319],{"class":240},[202,1380,327],{"class":211},[202,1382,597],{"class":240},[202,1384,483],{"class":236},[202,1386,486],{"class":240},[202,1388,489],{"class":357},[202,1390,276],{"class":236},[202,1392,1393],{"class":240},"k) ",[202,1395,1329],{"class":236},[202,1397,1398],{"class":240}," k ",[202,1400,1335],{"class":236},[202,1402,1338],{"class":377},[202,1404,1341],{"class":240},[202,1406,58],{"class":377},[202,1408,1324],{"class":240},[202,1410,1411,1414,1416,1419,1421,1424,1427,1430,1433,1436,1438,1440],{"class":64,"line":282},[202,1412,1413],{"class":240},"month_ends ",[202,1415,276],{"class":236},[202,1417,1418],{"class":240}," eomonth(pd.Series([pd.Timestamp(",[202,1420,327],{"class":211},[202,1422,1423],{"class":240},")] ",[202,1425,1426],{"class":236},"*",[202,1428,1429],{"class":377}," 4",[202,1431,1432],{"class":240},"), ",[202,1434,1435],{"class":377},"range",[202,1437,1341],{"class":240},[202,1439,58],{"class":377},[202,1441,1369],{"class":240},[202,1443,1444,1446,1449,1452,1454,1456,1458,1460,1463,1465,1468],{"class":64,"line":318},[202,1445,501],{"class":377},[202,1447,1448],{"class":240},"([d.strftime(",[202,1450,1451],{"class":211},"\"%m-",[202,1453,511],{"class":510},[202,1455,514],{"class":211},[202,1457,597],{"class":240},[202,1459,1329],{"class":236},[202,1461,1462],{"class":240}," d ",[202,1464,1335],{"class":236},[202,1466,1467],{"class":240}," chained])       ",[202,1469,1470],{"class":522},"# ['01-31', '02-28', '03-28', '04-28']\n",[202,1472,1473,1475,1477,1479,1481,1483,1485,1487,1489,1491,1494],{"class":64,"line":343},[202,1474,501],{"class":377},[202,1476,1448],{"class":240},[202,1478,1451],{"class":211},[202,1480,511],{"class":510},[202,1482,514],{"class":211},[202,1484,597],{"class":240},[202,1486,1329],{"class":236},[202,1488,1462],{"class":240},[202,1490,1335],{"class":236},[202,1492,1493],{"class":240}," from_start])    ",[202,1495,1496],{"class":522},"# ['01-31', '02-28', '03-31', '04-30']\n",[202,1498,1499,1501,1504,1506,1508,1510,1513],{"class":64,"line":369},[202,1500,501],{"class":377},[202,1502,1503],{"class":240},"(month_ends.dt.strftime(",[202,1505,1451],{"class":211},[202,1507,511],{"class":510},[202,1509,514],{"class":211},[202,1511,1512],{"class":240},").tolist())     ",[202,1514,1496],{"class":522},[10,1516,1517,1518,1521,1522,1524],{},"Always offset from the original start date — ",[14,1519,1520],{},"EDATE(start, k)"," for the k-th payment — rather than from the previous payment. When the schedule is meant to fall on month ends, use ",[14,1523,28],{}," semantics directly; it states the intent and cannot drift.",[188,1526,1528],{"id":1527},"step-6-prorate-by-days-in-the-month","Step 6 — Prorate by days in the month",[10,1530,1531,1532,1534,1535,1538,1539,1542],{},"A common reason for ",[14,1533,28],{}," in a workbook is proration: a subscription that starts on the 20th is charged for the remaining days of that month. Excel writes ",[14,1536,1537],{},"DAY(EOMONTH(start, 0))"," for the number of days in the month and ",[14,1540,1541],{},"EOMONTH(start, 0) - start + 1"," for the days remaining. Both translate directly:",[193,1544,1546],{"className":227,"code":1545,"language":229,"meta":198,"style":198},"month_end = eomonth(df[\"start\"], 0)\ndays_in_month = df[\"start\"].dt.days_in_month\ndays_left = (month_end - df[\"start\"].dt.normalize()).dt.days + 1\nmonthly_fee = 90.0\ndf[\"first_charge\"] = (monthly_fee * days_left \u002F days_in_month).round(2)\nprint(pd.DataFrame({\"start\": df[\"start\"].dt.strftime(\"%m-%d\"), \"left\": days_left,\n                    \"of\": days_in_month, \"charge\": df[\"first_charge\"]}).values.tolist()[:3])\n# [['01-31', 1, 31, 2.9], ['01-15', 17, 31, 49.35], ['02-29', 1, 29, 3.1]]\n",[14,1547,1548,1565,1579,1603,1613,1642,1672,1694],{"__ignoreMap":198},[202,1549,1550,1553,1555,1557,1559,1561,1563],{"class":64,"line":204},[202,1551,1552],{"class":240},"month_end ",[202,1554,276],{"class":236},[202,1556,1246],{"class":240},[202,1558,477],{"class":211},[202,1560,354],{"class":240},[202,1562,59],{"class":377},[202,1564,429],{"class":240},[202,1566,1567,1570,1572,1574,1576],{"class":64,"line":250},[202,1568,1569],{"class":240},"days_in_month ",[202,1571,276],{"class":236},[202,1573,1206],{"class":240},[202,1575,477],{"class":211},[202,1577,1578],{"class":240},"].dt.days_in_month\n",[202,1580,1581,1584,1586,1589,1591,1593,1595,1598,1600],{"class":64,"line":263},[202,1582,1583],{"class":240},"days_left ",[202,1585,276],{"class":236},[202,1587,1588],{"class":240}," (month_end ",[202,1590,387],{"class":236},[202,1592,1206],{"class":240},[202,1594,477],{"class":211},[202,1596,1597],{"class":240},"].dt.normalize()).dt.days ",[202,1599,483],{"class":236},[202,1601,1602],{"class":377}," 1\n",[202,1604,1605,1608,1610],{"class":64,"line":270},[202,1606,1607],{"class":240},"monthly_fee ",[202,1609,276],{"class":236},[202,1611,1612],{"class":377}," 90.0\n",[202,1614,1615,1617,1620,1622,1624,1627,1629,1632,1635,1638,1640],{"class":64,"line":282},[202,1616,661],{"class":240},[202,1618,1619],{"class":211},"\"first_charge\"",[202,1621,480],{"class":240},[202,1623,276],{"class":236},[202,1625,1626],{"class":240}," (monthly_fee ",[202,1628,1426],{"class":236},[202,1630,1631],{"class":240}," days_left ",[202,1633,1634],{"class":236},"\u002F",[202,1636,1637],{"class":240}," days_in_month).round(",[202,1639,394],{"class":377},[202,1641,429],{"class":240},[202,1643,1644,1646,1649,1651,1654,1656,1658,1660,1662,1664,1666,1669],{"class":64,"line":318},[202,1645,501],{"class":377},[202,1647,1648],{"class":240},"(pd.DataFrame({",[202,1650,477],{"class":211},[202,1652,1653],{"class":240},": df[",[202,1655,477],{"class":211},[202,1657,694],{"class":240},[202,1659,1451],{"class":211},[202,1661,511],{"class":510},[202,1663,514],{"class":211},[202,1665,1432],{"class":240},[202,1667,1668],{"class":211},"\"left\"",[202,1670,1671],{"class":240},": days_left,\n",[202,1673,1674,1677,1680,1683,1685,1687,1690,1692],{"class":64,"line":343},[202,1675,1676],{"class":211},"                    \"of\"",[202,1678,1679],{"class":240},": days_in_month, ",[202,1681,1682],{"class":211},"\"charge\"",[202,1684,1653],{"class":240},[202,1686,1619],{"class":211},[202,1688,1689],{"class":240},"]}).values.tolist()[:",[202,1691,399],{"class":377},[202,1693,682],{"class":240},[202,1695,1696],{"class":64,"line":369},[202,1697,1698],{"class":522},"# [['01-31', 1, 31, 2.9], ['01-15', 17, 31, 49.35], ['02-29', 1, 29, 3.1]]\n",[10,1700,1701,1704,1705,1709,1710,1294],{},[14,1702,1703],{},"dt.days_in_month"," handles leap years, so 29 February 2024 is correctly the last of 29 days. Normalising the start date before subtracting keeps a start time such as 14:30 from shortening the count by a day. If the charge must match the workbook to the cent, round with the Excel-compatible function from ",[31,1706,1708],{"href":1707},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fmatch-excel-rounding-in-pandas\u002F","Match Excel Rounding in pandas"," rather than ",[14,1711,1712],{},"round",[188,1714,1716],{"id":1715},"step-7-write-the-dates-back-as-excel-dates","Step 7 — Write the dates back as Excel dates",[10,1718,1719,1720,1723],{},"The results are ordinary datetime columns, so ",[14,1721,1722],{},"to_excel"," writes them as real Excel dates. Give them a date format so they do not show a time of 00:00:00:",[193,1725,1727],{"className":227,"code":1726,"language":229,"meta":198,"style":198},"with pd.ExcelWriter(\"contracts_out.xlsx\", engine=\"openpyxl\", datetime_format=\"yyyy-mm-dd\") as writer:\n    df.assign(month_end=eomonth(df[\"end\"])).to_excel(writer, index=False)\nprint(pd.read_excel(\"contracts_out.xlsx\")[\"month_end\"].dt.strftime(\"%Y-%m-%d\").tolist()[:2])\n# ['2026-02-28', '2027-01-31']\n",[14,1728,1729,1767,1793,1822],{"__ignoreMap":198},[202,1730,1731,1734,1737,1740,1742,1745,1747,1750,1752,1755,1757,1760,1762,1764],{"class":64,"line":204},[202,1732,1733],{"class":236},"with",[202,1735,1736],{"class":240}," pd.ExcelWriter(",[202,1738,1739],{"class":211},"\"contracts_out.xlsx\"",[202,1741,294],{"class":240},[202,1743,1744],{"class":357},"engine",[202,1746,276],{"class":236},[202,1748,1749],{"class":211},"\"openpyxl\"",[202,1751,294],{"class":240},[202,1753,1754],{"class":357},"datetime_format",[202,1756,276],{"class":236},[202,1758,1759],{"class":211},"\"yyyy-mm-dd\"",[202,1761,597],{"class":240},[202,1763,244],{"class":236},[202,1765,1766],{"class":240}," writer:\n",[202,1768,1769,1772,1775,1777,1780,1782,1785,1787,1789,1791],{"class":64,"line":250},[202,1770,1771],{"class":240},"    df.assign(",[202,1773,1774],{"class":357},"month_end",[202,1776,276],{"class":236},[202,1778,1779],{"class":240},"eomonth(df[",[202,1781,664],{"class":211},[202,1783,1784],{"class":240},"])).to_excel(writer, ",[202,1786,421],{"class":357},[202,1788,276],{"class":236},[202,1790,426],{"class":377},[202,1792,429],{"class":240},[202,1794,1795,1797,1800,1802,1805,1808,1810,1812,1814,1816,1818,1820],{"class":64,"line":263},[202,1796,501],{"class":377},[202,1798,1799],{"class":240},"(pd.read_excel(",[202,1801,1739],{"class":211},[202,1803,1804],{"class":240},")[",[202,1806,1807],{"class":211},"\"month_end\"",[202,1809,694],{"class":240},[202,1811,507],{"class":211},[202,1813,511],{"class":510},[202,1815,514],{"class":211},[202,1817,903],{"class":240},[202,1819,394],{"class":377},[202,1821,682],{"class":240},[202,1823,1824],{"class":64,"line":270},[202,1825,1826],{"class":522},"# ['2026-02-28', '2027-01-31']\n",[10,1828,1829,1830,1294],{},"If the workbook will keep calculating after you hand it over — users changing start dates, for example — write the formulas instead of the values, so Excel stays the source of the calculation; see ",[31,1831,1833],{"href":1832},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fwrite-formula-to-excel-cell-with-openpyxl\u002F","Write a Formula to an Excel Cell with openpyxl",[188,1835,1837],{"id":1836},"common-pitfalls","Common pitfalls",[1839,1840,1841,1849,1862,1877],"ul",{},[1842,1843,1844,1848],"li",{},[1845,1846,1847],"strong",{},"Adding 30 days for \"one month\"."," It lands on the wrong day in every month that is not 30 days long, and the error grows with every month added. Use month arithmetic.",[1842,1850,1851,1854,1855,1858,1859,1861],{},[1845,1852,1853],{},"Dates read as text."," Month arithmetic needs datetime columns. Convert with ",[14,1856,1857],{},"pd.to_datetime"," and an explicit ",[14,1860,358],{}," first.",[1842,1863,1864,1867,1868,1870,1871,1873,1874,1294],{},[1845,1865,1866],{},"Business days."," ",[14,1869,458],{}," and ",[14,1872,28],{}," ignore weekends; a month end can be a Sunday. If a due date must fall on a working day, roll it with a business-day offset afterwards, the equivalent of Excel's ",[14,1875,1876],{},"WORKDAY",[1842,1878,1879,1882],{},[1845,1880,1881],{},"Timezone-aware timestamps."," Periods drop the timezone. Convert to local dates first when month boundaries depend on the local calendar.",[188,1884,1886],{"id":1885},"conclusion","Conclusion",[10,1888,1889,1891,1892,1895,1896,1898,1899,1902,1903,1905],{},[14,1890,458],{}," is ",[14,1893,1894],{},"DateOffset(months=n)"," for a fixed count, or a period shift with a clamped day for a count per row; normalise either result to drop the time as Excel does. ",[14,1897,28],{}," is a period shift converted back with ",[14,1900,1901],{},"how=\"end\""," and normalised — not ",[14,1904,24],{},", which depends on whether the start is already a month end. Offset every schedule date from the original start to avoid drift, and write the results as formatted Excel dates.",[188,1907,1909],{"id":1908},"frequently-asked-questions","Frequently asked questions",[10,1911,1912,1915],{},[1845,1913,1914],{},"What is the pandas equivalent of EDATE?","\nAdd pd.DateOffset(months=n) to a datetime column. Like EDATE, it clamps to the last valid day, so 31 January plus one month is 28 February. Normalise the result to drop the time, which EDATE also discards.",[10,1917,1918,1921],{},[1845,1919,1920],{},"What is the pandas equivalent of EOMONTH?","\nConvert to a monthly Period, add the number of months, and convert back with to_timestamp(how=\"end\") followed by normalize(). That gives the last day of the target month for every date, whether or not it is already a month end.",[10,1923,1924,1927],{},[1845,1925,1926],{},"Why does MonthEnd(1) give different results for different dates?","\nMonthEnd(1) rolls forward to the next month end, so 15 January goes to 31 January but 31 January goes to 28 February. EOMONTH(date, 1) always returns the end of the following month. Use Period arithmetic to avoid the difference.",[10,1929,1930,1933],{},[1845,1931,1932],{},"How do I get the first day of the month?","\nConvert to a monthly Period and back with to_timestamp(), which returns the first day of the month at midnight. It gives the same result as the Excel idiom EOMONTH(date, -1) + 1.",[188,1935,1937],{"id":1936},"related","Related",[1839,1939,1940,1946,1953,1958],{},[1842,1941,1942,1943,1945],{},"Up one level: ",[31,1944,34],{"href":33}," — Excel functions translated to pandas.",[1842,1947,1948,1952],{},[31,1949,1951],{"href":1950},"\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"," — month-over-month comparisons.",[1842,1954,1955,1957],{},[31,1956,1708],{"href":1707}," — another place where pandas and Excel differ quietly.",[1842,1959,1960,1964],{},[31,1961,1963],{"href":1962},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","Working with Dates and Times in Excel Data"," — reading and converting Excel dates.",[1966,1967,1968],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .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}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":198,"searchDepth":250,"depth":250,"links":1970},[1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982],{"id":190,"depth":250,"text":191},{"id":447,"depth":250,"text":448},{"id":547,"depth":250,"text":548},{"id":844,"depth":250,"text":845},{"id":1183,"depth":250,"text":1184},{"id":1297,"depth":250,"text":1298},{"id":1527,"depth":250,"text":1528},{"id":1715,"depth":250,"text":1716},{"id":1836,"depth":250,"text":1837},{"id":1885,"depth":250,"text":1886},{"id":1908,"depth":250,"text":1909},{"id":1936,"depth":250,"text":1937},"2026-09-23","Reproduce Excel's EDATE and EOMONTH in pandas for whole columns — month arithmetic that clamps 31 January to 28 February, month ends that do not depend on whether a date is already a month end, a different number of months per row, first-of-month dates and the time-of-day that Excel drops.","md",[1987,1989,1991,1993],{"q":1914,"a":1988},"Add pd.DateOffset(months=n) to a datetime column. Like EDATE, it clamps to the last valid day, so 31 January plus one month is 28 February. Normalise the result to drop the time, which EDATE also discards.",{"q":1920,"a":1990},"Convert to a monthly Period, add the number of months, and convert back with to_timestamp(how=\"end\") followed by normalize(). That gives the last day of the target month for every date, whether or not it is already a month end.",{"q":1926,"a":1992},"MonthEnd(1) rolls forward to the next month end, so 15 January goes to 31 January but 31 January goes to 28 February. EOMONTH(date, 1) always returns the end of the following month. Use Period arithmetic to avoid the difference.",{"q":1932,"a":1994},"Convert to a monthly Period and back with to_timestamp(), which returns the first day of the month at midnight. It gives the same result as the Excel idiom EOMONTH(date, -1) + 1.","eomonth and edate equivalents in pandas",null,{"breadcrumb":1998},[1999,2001,2004],{"name":2000,"item":1634},"Home",{"name":2002,"item":2003},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":34,"item":33},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas",{"title":2007,"description":2008},"EOMONTH and EDATE Equivalents in pandas (month arithmetic)","Translate Excel EDATE and EOMONTH to pandas — DateOffset and Period arithmetic, the MonthEnd rollover trap, per-row month counts, first and last day of month, dropping the time like Excel, and month-end schedules.","eomonth-and-edate-equivalents-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas\u002Findex",[2012,2013,2014],"formula-equivalents","dates-and-times","pandas","how-to","d4czIAmXDcEl0ONLBSrSlQne8oyW4_qV-1XxT1xo4iY",[2018,2022],{"title":2019,"path":2020,"stem":2021,"children":-1},"COUNTIF and COUNTIFS Equivalent in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fcountif-and-countifs-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fcountif-and-countifs-equivalent-in-pandas\u002Findex",{"title":2023,"path":2024,"stem":2025,"children":-1},"EXACT and Case-Sensitive Comparisons in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexact-and-case-sensitive-comparisons-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexact-and-case-sensitive-comparisons-in-pandas\u002Findex",[2027,2029],{"path":2028,"title":34},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas",{"path":2030,"title":2002},"\u002Fadvanced-data-transformation-and-cleaning",[],[2033,2036,2039,2042,2045,2048],{"path":2034,"title":2035,"kind":2015},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files","The VLOOKUP Equivalent in pandas for Excel Files",{"path":2037,"title":2038,"kind":2015},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas","Extract Numbers and Codes from Text with pandas",{"path":2040,"title":2041,"kind":2015},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fsplit-full-names-into-first-and-last-names-with-pandas","Split Full Names into First and Last Names with pandas",{"path":2043,"title":2044,"kind":2015},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut","Bin Values into Ranges Like Excel FREQUENCY with pd.cut",{"path":2046,"title":2047,"kind":2015},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas","Calculate Percent of Total per Group in pandas",{"path":2049,"title":2050,"kind":2015},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas","Count Distinct Values per Group in pandas",1790972086871]