[{"data":1,"prerenderedAt":2026},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas":1994,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas":2002,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas":2007,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas":2008},{"id":4,"title":5,"body":6,"dateModified":1961,"datePublished":1961,"description":1962,"extension":1963,"faq":1964,"keyword":1973,"kind":1974,"meta":1975,"navigation":246,"path":1983,"seo":1984,"slug":1987,"stem":1988,"topics":1989,"type":1992,"__hash__":1993},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas\u002Findex.md","Calculate Age and Tenure from Dates with pandas (DATEDIF Equivalent)",{"type":7,"value":8,"toc":1946},"minimark",[9,28,177,182,212,218,284,290,294,371,374,378,385,537,548,552,563,779,786,900,904,911,1023,1026,1030,1037,1273,1282,1286,1293,1461,1472,1556,1560,1567,1799,1810,1814,1854,1858,1868,1872,1881,1885,1891,1897,1903,1909,1913,1942],[10,11,12,13,17,18,21,22,27],"p",{},"\"How old is each customer?\" and \"how long has each employee been with us?\" sound like subtraction. In Excel they are ",[14,15,16],"code",{},"DATEDIF(start, end, \"Y\")",", a function so undocumented that people copy it from forum posts. In pandas the usual first attempt is ",[14,19,20],{},"(end - start).days \u002F 365.25",", which is right most of the time and wrong on exactly the days that matter — birthdays and work anniversaries, when the count should tick over. This page, part of ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","Working with Dates and Times in Excel Data",", computes completed years, months and days for whole columns, matching DATEDIF, and covers the 29 February case, fractional years and tenure bands.",[29,30,38,39,38,43,38,47,38,55,38,63,38,73,38,80,38,85,38,89,38,93,38,97,38,102,38,106,38,108,38,110,38,115,38,121,38,125,38,129,38,133,38,135,38,137,38,142,38,148,38,152,38,156,38,158,38,160,38,165,38,170,38,173,38,175],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"4 0 752 278","img",[34,35],"age-line-t","age-line-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  ",[40,41,42],"title",{"id":34},"Why completed years are counted by calendar, not by days",[44,45,46],"desc",{"id":35},"A timeline from a birth date of 25 September 2001 to 25 September 2026. The 25th birthday falls on the as-of date. Dividing the 9,131 days by 365.25 gives 24.9993, which truncates to 24. Comparing month and day shows the birthday has been reached, so the completed age is 25.",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"4","0","752","278","#ffffff",[56,57],"line",{"x1":58,"y1":59,"x2":60,"y2":59,"stroke":61,"style":62},"76.0","139.0","684.0","var(--muted,#5b6780)","stroke-width:2.5px",[48,64],{"x":65,"y":66,"width":67,"height":68,"rx":69,"fill":70,"stroke":71,"style":72},"20.0","16.0","136.0","91.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[74,75,79],"text",{"x":76,"y":77,"style":78},"88.0","57.2","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Born 2001-09-25",[74,81,84],{"x":76,"y":82,"style":83},"75.8","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","start date",[56,86],{"x1":76,"y1":87,"x2":76,"y2":59,"stroke":61,"style":88},"107.0","stroke-width:1.5px",[90,91],"circle",{"cx":76,"cy":59,"r":92,"fill":61,"stroke":54,"style":72},"7.0",[48,94],{"x":95,"y":96,"width":67,"height":68,"rx":69,"fill":70,"stroke":71,"style":72},"166.0","171.0",[74,98,101],{"x":99,"y":100,"style":78},"234.0","212.2","9,131 days later",[74,103,105],{"x":99,"y":104,"style":83},"230.8","includes 6 leap days",[56,107],{"x1":99,"y1":59,"x2":99,"y2":96,"stroke":61,"style":88},[90,109],{"cx":99,"cy":59,"r":92,"fill":61,"stroke":54,"style":72},[48,111],{"x":112,"y":66,"width":67,"height":68,"rx":69,"fill":113,"stroke":114,"style":72},"312.0","#fee8f2","var(--accent,#f43f8f)",[74,116,120],{"x":117,"y":118,"style":119},"380.0","40.5","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","÷ 365.25 =",[74,122,124],{"x":117,"y":123,"style":119},"57.0","24.9993",[74,126,128],{"x":117,"y":127,"style":83},"75.5","truncates to 24 —",[74,130,132],{"x":117,"y":131,"style":83},"92.5","wrong",[56,134],{"x1":117,"y1":87,"x2":117,"y2":59,"stroke":61,"style":88},[90,136],{"cx":117,"cy":59,"r":92,"fill":114,"stroke":54,"style":72},[48,138],{"x":139,"y":96,"width":67,"height":68,"rx":69,"fill":140,"stroke":141,"style":72},"458.0","#f0f4ff","var(--brand,#5b5cf0)",[74,143,147],{"x":144,"y":145,"style":146},"526.0","204.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Month\u002Fday",[74,149,151],{"x":144,"y":150,"style":146},"220.5","reached?",[74,153,155],{"x":144,"y":154,"style":83},"239.0","25 Sep ≥ 25 Sep",[56,157],{"x1":144,"y1":59,"x2":144,"y2":96,"stroke":61,"style":88},[90,159],{"cx":144,"cy":59,"r":92,"fill":141,"stroke":54,"style":72},[48,161],{"x":162,"y":66,"width":67,"height":68,"rx":69,"fill":163,"stroke":164,"style":72},"604.0","#d9f4f1","var(--teal,#0f9488)",[74,166,169],{"x":167,"y":77,"style":168},"672.0","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Age 25",[74,171,172],{"x":167,"y":82,"style":83},"matches DATEDIF",[56,174],{"x1":167,"y1":87,"x2":167,"y2":59,"stroke":61,"style":88},[90,176],{"cx":167,"cy":59,"r":92,"fill":164,"stroke":54,"style":72},[178,179,181],"h2",{"id":180},"prerequisites","Prerequisites",[183,184,189],"pre",{"className":185,"code":186,"language":187,"meta":188,"style":188},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl python-dateutil\n","bash","",[14,190,191],{"__ignoreMap":188},[192,193,195,199,203,206,209],"span",{"class":56,"line":194},1,[192,196,198],{"class":197},"sMTad","pip",[192,200,202],{"class":201},"srMev"," install",[192,204,205],{"class":201}," pandas",[192,207,208],{"class":201}," openpyxl",[192,210,211],{"class":201}," python-dateutil\n",[10,213,214,217],{},[14,215,216],{},"staff.xlsx"," has six employees with birth and hire dates, including two born on 29 February, one whose birthday is the report date and one whose birthday is the day after it.",[183,219,223],{"className":220,"code":221,"language":222,"meta":188,"style":188},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nstaff = pd.read_excel(\"staff.xlsx\")\nAS_OF = pd.Timestamp(\"2026-09-25\")\n","python",[14,224,225,241,248,266],{"__ignoreMap":188},[192,226,227,231,235,238],{"class":56,"line":194},[192,228,230],{"class":229},"s-kum","import",[192,232,234],{"class":233},"skGVy"," pandas ",[192,236,237],{"class":229},"as",[192,239,240],{"class":233}," pd\n",[192,242,244],{"class":56,"line":243},2,[192,245,247],{"emptyLinePlaceholder":246},true,"\n",[192,249,251,254,257,260,263],{"class":56,"line":250},3,[192,252,253],{"class":233},"staff ",[192,255,256],{"class":229},"=",[192,258,259],{"class":233}," pd.read_excel(",[192,261,262],{"class":201},"\"staff.xlsx\"",[192,264,265],{"class":233},")\n",[192,267,269,273,276,279,282],{"class":56,"line":268},4,[192,270,272],{"class":271},"sP0c6","AS_OF",[192,274,275],{"class":229}," =",[192,277,278],{"class":233}," pd.Timestamp(",[192,280,281],{"class":201},"\"2026-09-25\"",[192,283,265],{"class":233},[10,285,286,287,289],{},"Fixing the report date in ",[14,288,272],{},", rather than using today, makes every run of the report give the same answer.",[178,291,293],{"id":292},"step-1-see-the-days-divided-by-36525-problem","Step 1 — See the days-divided-by-365.25 problem",[183,295,297],{"className":220,"code":296,"language":222,"meta":188,"style":188},"naive = ((AS_OF - staff[\"birth_date\"]).dt.days \u002F 365.25).astype(int)\nprint(pd.DataFrame({\"employee\": staff[\"employee\"], \"birth_date\": staff[\"birth_date\"].dt.date, \"naive_age\": naive}))\n",[14,298,299,337],{"__ignoreMap":188},[192,300,301,304,306,309,311,314,317,320,323,326,329,332,335],{"class":56,"line":194},[192,302,303],{"class":233},"naive ",[192,305,256],{"class":229},[192,307,308],{"class":233}," ((",[192,310,272],{"class":271},[192,312,313],{"class":229}," -",[192,315,316],{"class":233}," staff[",[192,318,319],{"class":201},"\"birth_date\"",[192,321,322],{"class":233},"]).dt.days ",[192,324,325],{"class":229},"\u002F",[192,327,328],{"class":271}," 365.25",[192,330,331],{"class":233},").astype(",[192,333,334],{"class":271},"int",[192,336,265],{"class":233},[192,338,339,342,345,348,351,353,356,358,360,362,365,368],{"class":56,"line":243},[192,340,341],{"class":271},"print",[192,343,344],{"class":233},"(pd.DataFrame({",[192,346,347],{"class":201},"\"employee\"",[192,349,350],{"class":233},": staff[",[192,352,347],{"class":201},[192,354,355],{"class":233},"], ",[192,357,319],{"class":201},[192,359,350],{"class":233},[192,361,319],{"class":201},[192,363,364],{"class":233},"].dt.date, ",[192,366,367],{"class":201},"\"naive_age\"",[192,369,370],{"class":233},": naive}))\n",[10,372,373],{},"Dev was born on 25 September 2001 and turns 25 on the report date, but the naive formula says 24: the 9,131 days include six leap days, and 9,131 \u002F 365.25 is 24.9993. Dividing by 365 fails the other way, making people a year older a few days early. No constant divisor is right, because leap days are not spread evenly across a lifetime.",[178,375,377],{"id":376},"step-2-count-completed-years-by-comparing-month-and-day","Step 2 — Count completed years by comparing month and day",[10,379,380,381,384],{},"A person has completed another year once the end date's month and day have reached the start date's. That is DATEDIF's ",[14,382,383],{},"\"Y\""," rule, and it vectorises neatly:",[183,386,388],{"className":220,"code":387,"language":222,"meta":188,"style":188},"def completed_years(start, end):\n    years = end.year - start.dt.year\n    not_yet = (end.month \u003C start.dt.month) | ((end.month == start.dt.month) & (end.day \u003C start.dt.day))\n    return years - not_yet.astype(int)\n\nstaff[\"age\"] = completed_years(staff[\"birth_date\"], AS_OF)\nprint(staff[[\"employee\", \"birth_date\", \"age\"]].to_string(index=False))\n",[14,389,390,402,418,456,473,478,503],{"__ignoreMap":188},[192,391,392,395,399],{"class":56,"line":194},[192,393,394],{"class":229},"def",[192,396,398],{"class":397},"s_Opv"," completed_years",[192,400,401],{"class":233},"(start, end):\n",[192,403,404,407,409,412,415],{"class":56,"line":243},[192,405,406],{"class":233},"    years ",[192,408,256],{"class":229},[192,410,411],{"class":233}," end.year ",[192,413,414],{"class":229},"-",[192,416,417],{"class":233}," start.dt.year\n",[192,419,420,423,425,428,431,434,437,440,443,445,448,451,453],{"class":56,"line":250},[192,421,422],{"class":233},"    not_yet ",[192,424,256],{"class":229},[192,426,427],{"class":233}," (end.month ",[192,429,430],{"class":229},"\u003C",[192,432,433],{"class":233}," start.dt.month) ",[192,435,436],{"class":229},"|",[192,438,439],{"class":233}," ((end.month ",[192,441,442],{"class":229},"==",[192,444,433],{"class":233},[192,446,447],{"class":229},"&",[192,449,450],{"class":233}," (end.day ",[192,452,430],{"class":229},[192,454,455],{"class":233}," start.dt.day))\n",[192,457,458,461,464,466,469,471],{"class":56,"line":268},[192,459,460],{"class":229},"    return",[192,462,463],{"class":233}," years ",[192,465,414],{"class":229},[192,467,468],{"class":233}," not_yet.astype(",[192,470,334],{"class":271},[192,472,265],{"class":233},[192,474,476],{"class":56,"line":475},5,[192,477,247],{"emptyLinePlaceholder":246},[192,479,481,484,487,490,492,495,497,499,501],{"class":56,"line":480},6,[192,482,483],{"class":233},"staff[",[192,485,486],{"class":201},"\"age\"",[192,488,489],{"class":233},"] ",[192,491,256],{"class":229},[192,493,494],{"class":233}," completed_years(staff[",[192,496,319],{"class":201},[192,498,355],{"class":233},[192,500,272],{"class":271},[192,502,265],{"class":233},[192,504,506,508,511,513,516,518,520,522,525,529,531,534],{"class":56,"line":505},7,[192,507,341],{"class":271},[192,509,510],{"class":233},"(staff[[",[192,512,347],{"class":201},[192,514,515],{"class":233},", ",[192,517,319],{"class":201},[192,519,515],{"class":233},[192,521,486],{"class":201},[192,523,524],{"class":233},"]].to_string(",[192,526,528],{"class":527},"sa561","index",[192,530,256],{"class":229},[192,532,533],{"class":271},"False",[192,535,536],{"class":233},"))\n",[10,538,539,540,543,544,547],{},"Dev is 25 and Ben, whose birthday is tomorrow, is still 35. These results were checked against ",[14,541,542],{},"DATEDIF(A1, B1, \"Y\")"," recalculated in LibreOffice for every row, and they agree. When the end date varies per row — tenure until each employee's leaving date, say — pass a Series and use ",[14,545,546],{},".dt"," on both sides.",[178,549,551],{"id":550},"step-3-months-and-the-remainder-like-datedif-m-and-ym","Step 3 — Months and the remainder, like DATEDIF \"M\" and \"YM\"",[10,553,554,555,558,559,562],{},"DATEDIF's ",[14,556,557],{},"\"M\""," gives completed months and ",[14,560,561],{},"\"YM\""," the months left over after whole years — together they produce \"36 years, 0 months\". The same comparison, applied at month level, gives both:",[183,564,566],{"className":220,"code":565,"language":222,"meta":188,"style":188},"def completed_months(start, end):\n    months = (end.year - start.dt.year) * 12 + (end.month - start.dt.month)\n    return months - (end.day \u003C start.dt.day).astype(int)\n\nstaff[\"tenure_months\"] = completed_months(staff[\"hire_date\"], AS_OF)\nstaff[\"tenure_y\"] = staff[\"tenure_months\"] \u002F\u002F 12\nstaff[\"tenure_m\"] = staff[\"tenure_months\"] % 12\nstaff[\"tenure\"] = staff[\"tenure_y\"].astype(str) + \" y \" + staff[\"tenure_m\"].astype(str) + \" m\"\nprint(staff[[\"employee\", \"hire_date\", \"tenure_months\", \"tenure\"]].to_string(index=False))\n",[14,567,568,577,608,628,632,655,678,700,748],{"__ignoreMap":188},[192,569,570,572,575],{"class":56,"line":194},[192,571,394],{"class":229},[192,573,574],{"class":397}," completed_months",[192,576,401],{"class":233},[192,578,579,582,584,587,589,592,595,598,601,603,605],{"class":56,"line":243},[192,580,581],{"class":233},"    months ",[192,583,256],{"class":229},[192,585,586],{"class":233}," (end.year ",[192,588,414],{"class":229},[192,590,591],{"class":233}," start.dt.year) ",[192,593,594],{"class":229},"*",[192,596,597],{"class":271}," 12",[192,599,600],{"class":229}," +",[192,602,427],{"class":233},[192,604,414],{"class":229},[192,606,607],{"class":233}," start.dt.month)\n",[192,609,610,612,615,617,619,621,624,626],{"class":56,"line":250},[192,611,460],{"class":229},[192,613,614],{"class":233}," months ",[192,616,414],{"class":229},[192,618,450],{"class":233},[192,620,430],{"class":229},[192,622,623],{"class":233}," start.dt.day).astype(",[192,625,334],{"class":271},[192,627,265],{"class":233},[192,629,630],{"class":56,"line":268},[192,631,247],{"emptyLinePlaceholder":246},[192,633,634,636,639,641,643,646,649,651,653],{"class":56,"line":475},[192,635,483],{"class":233},[192,637,638],{"class":201},"\"tenure_months\"",[192,640,489],{"class":233},[192,642,256],{"class":229},[192,644,645],{"class":233}," completed_months(staff[",[192,647,648],{"class":201},"\"hire_date\"",[192,650,355],{"class":233},[192,652,272],{"class":271},[192,654,265],{"class":233},[192,656,657,659,662,664,666,668,670,672,675],{"class":56,"line":480},[192,658,483],{"class":233},[192,660,661],{"class":201},"\"tenure_y\"",[192,663,489],{"class":233},[192,665,256],{"class":229},[192,667,316],{"class":233},[192,669,638],{"class":201},[192,671,489],{"class":233},[192,673,674],{"class":229},"\u002F\u002F",[192,676,677],{"class":271}," 12\n",[192,679,680,682,685,687,689,691,693,695,698],{"class":56,"line":505},[192,681,483],{"class":233},[192,683,684],{"class":201},"\"tenure_m\"",[192,686,489],{"class":233},[192,688,256],{"class":229},[192,690,316],{"class":233},[192,692,638],{"class":201},[192,694,489],{"class":233},[192,696,697],{"class":229},"%",[192,699,677],{"class":271},[192,701,703,705,708,710,712,714,716,719,722,725,728,731,733,735,737,739,741,743,745],{"class":56,"line":702},8,[192,704,483],{"class":233},[192,706,707],{"class":201},"\"tenure\"",[192,709,489],{"class":233},[192,711,256],{"class":229},[192,713,316],{"class":233},[192,715,661],{"class":201},[192,717,718],{"class":233},"].astype(",[192,720,721],{"class":271},"str",[192,723,724],{"class":233},") ",[192,726,727],{"class":229},"+",[192,729,730],{"class":201}," \" y \"",[192,732,600],{"class":229},[192,734,316],{"class":233},[192,736,684],{"class":201},[192,738,718],{"class":233},[192,740,721],{"class":271},[192,742,724],{"class":233},[192,744,727],{"class":229},[192,746,747],{"class":201}," \" m\"\n",[192,749,751,753,755,757,759,761,763,765,767,769,771,773,775,777],{"class":56,"line":750},9,[192,752,341],{"class":271},[192,754,510],{"class":233},[192,756,347],{"class":201},[192,758,515],{"class":233},[192,760,648],{"class":201},[192,762,515],{"class":233},[192,764,638],{"class":201},[192,766,515],{"class":233},[192,768,707],{"class":201},[192,770,524],{"class":233},[192,772,528],{"class":527},[192,774,256],{"class":229},[192,776,533],{"class":271},[192,778,536],{"class":233},[10,780,781,782,785],{},"Finn, hired in January 2026, shows ",[14,783,784],{},"0 y 8 m",". A hire date in the future — a signed contract that has not started — would give a negative count; filter or flag such rows rather than reporting negative tenure.",[29,787,38,792,38,795,38,798,38,801,38,806,38,811,38,817,38,820,38,824,38,828,38,834,38,836,38,841,38,843,38,846,38,849,38,853,38,855,38,859,38,861,38,864,38,867,38,871,38,873,38,877,38,879,38,882,38,885,38,889,38,891,38,895,38,897],{"viewBox":788,"role":32,"ariaLabelledBy":789,"xmlns":36,"style":37},"4 0 752 208",[790,791],"age-datedif-t","age-datedif-d",[40,793,794],{"id":790},"DATEDIF units and their pandas equivalents",[44,796,797],{"id":791},"A reference table. DATEDIF with Y counts completed years, implemented by the year difference minus one if the month and day are not yet reached. M counts completed months, implemented at month level with the same day check. YM is the months left after whole years, the completed months modulo 12. D is the number of days, the difference of the two dates in days.",[48,799],{"x":50,"y":51,"width":52,"height":800,"fill":54},"208",[48,802],{"x":65,"y":66,"width":803,"height":804,"rx":51,"fill":54,"stroke":71,"style":805},"162.0","40.0","stroke-width:1px",[48,807],{"x":808,"y":66,"width":809,"height":804,"rx":51,"fill":140,"stroke":141,"style":810},"182.0","279.0","stroke-width:1.2px",[74,812,816],{"x":813,"y":814,"style":815},"321.5","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Meaning",[48,818],{"x":819,"y":66,"width":809,"height":804,"rx":51,"fill":140,"stroke":141,"style":810},"461.0",[74,821,823],{"x":822,"y":814,"style":815},"600.5","pandas",[48,825],{"x":65,"y":826,"width":803,"height":827,"rx":51,"fill":70,"stroke":71,"style":805},"56.0","34.0",[74,829,833],{"x":830,"y":831,"style":832},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","DATEDIF(a, b, \"Y\")",[48,835],{"x":808,"y":826,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,837,840],{"x":813,"y":838,"style":839},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","completed years",[48,842],{"x":819,"y":826,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,844,845],{"x":822,"y":838,"style":839},"completed_years(a, b)",[48,847],{"x":65,"y":848,"width":803,"height":827,"rx":51,"fill":70,"stroke":71,"style":805},"90.0",[74,850,852],{"x":830,"y":851,"style":832},"112.0","DATEDIF(a, b, \"M\")",[48,854],{"x":808,"y":848,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,856,858],{"x":813,"y":857,"style":839},"111.0","completed months",[48,860],{"x":819,"y":848,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,862,863],{"x":822,"y":857,"style":839},"completed_months(a, b)",[48,865],{"x":65,"y":866,"width":803,"height":827,"rx":51,"fill":70,"stroke":71,"style":805},"124.0",[74,868,870],{"x":830,"y":869,"style":832},"146.0","DATEDIF(a, b, \"YM\")",[48,872],{"x":808,"y":866,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,874,876],{"x":813,"y":875,"style":839},"145.0","months after whole years",[48,878],{"x":819,"y":866,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,880,881],{"x":822,"y":875,"style":839},"completed_months(a, b) % 12",[48,883],{"x":65,"y":884,"width":803,"height":827,"rx":51,"fill":70,"stroke":71,"style":805},"158.0",[74,886,888],{"x":830,"y":887,"style":832},"180.0","DATEDIF(a, b, \"D\")",[48,890],{"x":808,"y":884,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,892,894],{"x":813,"y":893,"style":839},"179.0","days",[48,896],{"x":819,"y":884,"width":809,"height":827,"rx":51,"fill":54,"stroke":71,"style":805},[74,898,899],{"x":822,"y":893,"style":839},"(b - a).dt.days",[178,901,903],{"id":902},"step-4-decide-the-29-february-rule","Step 4 — Decide the 29 February rule",[10,905,906,907,910],{},"Cleo and Finn were born on 29 February. In a non-leap year, is their birthday 28 February or 1 March? The rule above, like DATEDIF, says 1 March: on 28 February they have not yet reached \"month 2, day 29\". ",[14,908,909],{},"dateutil.relativedelta",", a common Python alternative, says 28 February:",[183,912,914],{"className":220,"code":913,"language":222,"meta":188,"style":188},"from dateutil.relativedelta import relativedelta\n\nleap_born = pd.Series(pd.to_datetime([\"2004-02-29\"]))\nfor day in [\"2027-02-28\", \"2027-03-01\"]:\n    end = pd.Timestamp(day)\n    rd = relativedelta(end.to_pydatetime(), leap_born[0].to_pydatetime()).years\n    print(day, \"month\u002Fday rule:\", completed_years(leap_born, end)[0], \"| relativedelta:\", rd)\n",[14,915,916,929,933,949,974,984,999],{"__ignoreMap":188},[192,917,918,921,924,926],{"class":56,"line":194},[192,919,920],{"class":229},"from",[192,922,923],{"class":233}," dateutil.relativedelta ",[192,925,230],{"class":229},[192,927,928],{"class":233}," relativedelta\n",[192,930,931],{"class":56,"line":243},[192,932,247],{"emptyLinePlaceholder":246},[192,934,935,938,940,943,946],{"class":56,"line":250},[192,936,937],{"class":233},"leap_born ",[192,939,256],{"class":229},[192,941,942],{"class":233}," pd.Series(pd.to_datetime([",[192,944,945],{"class":201},"\"2004-02-29\"",[192,947,948],{"class":233},"]))\n",[192,950,951,954,957,960,963,966,968,971],{"class":56,"line":268},[192,952,953],{"class":229},"for",[192,955,956],{"class":233}," day ",[192,958,959],{"class":229},"in",[192,961,962],{"class":233}," [",[192,964,965],{"class":201},"\"2027-02-28\"",[192,967,515],{"class":233},[192,969,970],{"class":201},"\"2027-03-01\"",[192,972,973],{"class":233},"]:\n",[192,975,976,979,981],{"class":56,"line":475},[192,977,978],{"class":233},"    end ",[192,980,256],{"class":229},[192,982,983],{"class":233}," pd.Timestamp(day)\n",[192,985,986,989,991,994,996],{"class":56,"line":480},[192,987,988],{"class":233},"    rd ",[192,990,256],{"class":229},[192,992,993],{"class":233}," relativedelta(end.to_pydatetime(), leap_born[",[192,995,51],{"class":271},[192,997,998],{"class":233},"].to_pydatetime()).years\n",[192,1000,1001,1004,1007,1010,1013,1015,1017,1020],{"class":56,"line":505},[192,1002,1003],{"class":271},"    print",[192,1005,1006],{"class":233},"(day, ",[192,1008,1009],{"class":201},"\"month\u002Fday rule:\"",[192,1011,1012],{"class":233},", completed_years(leap_born, end)[",[192,1014,51],{"class":271},[192,1016,355],{"class":233},[192,1018,1019],{"class":201},"\"| relativedelta:\"",[192,1021,1022],{"class":233},", rd)\n",[10,1024,1025],{},"On 28 February 2027 the month\u002Fday rule gives 22 and relativedelta 23; from 1 March both give 23. Both conventions are used in law and in payroll systems, so this is a question for whoever owns the policy, not a bug to fix. What matters is that one convention is applied everywhere — mixing DATEDIF in a workbook with relativedelta in a script produces reports that disagree for a day each year.",[178,1027,1029],{"id":1028},"step-5-fractional-years-for-pro-rating","Step 5 — Fractional years for pro-rating",[10,1031,1032,1033,1036],{},"Pro-rated benefits and service awards sometimes need tenure as a decimal, like Excel's ",[14,1034,1035],{},"YEARFRAC",". A simple and transparent version adds completed years to the fraction of the current year elapsed:",[183,1038,1040],{"className":220,"code":1039,"language":222,"meta":188,"style":188},"def years_fraction(start, end):\n    whole = completed_years(start, end)\n    last = pd.to_datetime({\"year\": start.dt.year + whole, \"month\": start.dt.month,\n                           \"day\": start.dt.day.where(~((start.dt.month == 2) & (start.dt.day == 29)), 28)})\n    nxt = pd.to_datetime({\"year\": start.dt.year + whole + 1, \"month\": start.dt.month,\n                          \"day\": start.dt.day.where(~((start.dt.month == 2) & (start.dt.day == 29)), 28)})\n    return whole + (end - last).dt.days \u002F (nxt - last).dt.days\n\nstaff[\"tenure_years\"] = years_fraction(staff[\"hire_date\"], AS_OF).round(3)\nprint(staff[[\"employee\", \"hire_date\", \"tenure_years\"]].to_string(index=False))\n",[14,1041,1042,1051,1061,1088,1128,1157,1188,1214,1218,1246],{"__ignoreMap":188},[192,1043,1044,1046,1049],{"class":56,"line":194},[192,1045,394],{"class":229},[192,1047,1048],{"class":397}," years_fraction",[192,1050,401],{"class":233},[192,1052,1053,1056,1058],{"class":56,"line":243},[192,1054,1055],{"class":233},"    whole ",[192,1057,256],{"class":229},[192,1059,1060],{"class":233}," completed_years(start, end)\n",[192,1062,1063,1066,1068,1071,1074,1077,1079,1082,1085],{"class":56,"line":250},[192,1064,1065],{"class":233},"    last ",[192,1067,256],{"class":229},[192,1069,1070],{"class":233}," pd.to_datetime({",[192,1072,1073],{"class":201},"\"year\"",[192,1075,1076],{"class":233},": start.dt.year ",[192,1078,727],{"class":229},[192,1080,1081],{"class":233}," whole, ",[192,1083,1084],{"class":201},"\"month\"",[192,1086,1087],{"class":233},": start.dt.month,\n",[192,1089,1090,1093,1096,1099,1102,1104,1107,1109,1111,1114,1116,1119,1122,1125],{"class":56,"line":268},[192,1091,1092],{"class":201},"                           \"day\"",[192,1094,1095],{"class":233},": start.dt.day.where(",[192,1097,1098],{"class":229},"~",[192,1100,1101],{"class":233},"((start.dt.month ",[192,1103,442],{"class":229},[192,1105,1106],{"class":271}," 2",[192,1108,724],{"class":233},[192,1110,447],{"class":229},[192,1112,1113],{"class":233}," (start.dt.day ",[192,1115,442],{"class":229},[192,1117,1118],{"class":271}," 29",[192,1120,1121],{"class":233},")), ",[192,1123,1124],{"class":271},"28",[192,1126,1127],{"class":233},")})\n",[192,1129,1130,1133,1135,1137,1139,1141,1143,1146,1148,1151,1153,1155],{"class":56,"line":475},[192,1131,1132],{"class":233},"    nxt ",[192,1134,256],{"class":229},[192,1136,1070],{"class":233},[192,1138,1073],{"class":201},[192,1140,1076],{"class":233},[192,1142,727],{"class":229},[192,1144,1145],{"class":233}," whole ",[192,1147,727],{"class":229},[192,1149,1150],{"class":271}," 1",[192,1152,515],{"class":233},[192,1154,1084],{"class":201},[192,1156,1087],{"class":233},[192,1158,1159,1162,1164,1166,1168,1170,1172,1174,1176,1178,1180,1182,1184,1186],{"class":56,"line":480},[192,1160,1161],{"class":201},"                          \"day\"",[192,1163,1095],{"class":233},[192,1165,1098],{"class":229},[192,1167,1101],{"class":233},[192,1169,442],{"class":229},[192,1171,1106],{"class":271},[192,1173,724],{"class":233},[192,1175,447],{"class":229},[192,1177,1113],{"class":233},[192,1179,442],{"class":229},[192,1181,1118],{"class":271},[192,1183,1121],{"class":233},[192,1185,1124],{"class":271},[192,1187,1127],{"class":233},[192,1189,1190,1192,1194,1196,1199,1201,1204,1206,1209,1211],{"class":56,"line":505},[192,1191,460],{"class":229},[192,1193,1145],{"class":233},[192,1195,727],{"class":229},[192,1197,1198],{"class":233}," (end ",[192,1200,414],{"class":229},[192,1202,1203],{"class":233}," last).dt.days ",[192,1205,325],{"class":229},[192,1207,1208],{"class":233}," (nxt ",[192,1210,414],{"class":229},[192,1212,1213],{"class":233}," last).dt.days\n",[192,1215,1216],{"class":56,"line":702},[192,1217,247],{"emptyLinePlaceholder":246},[192,1219,1220,1222,1225,1227,1229,1232,1234,1236,1238,1241,1244],{"class":56,"line":750},[192,1221,483],{"class":233},[192,1223,1224],{"class":201},"\"tenure_years\"",[192,1226,489],{"class":233},[192,1228,256],{"class":229},[192,1230,1231],{"class":233}," years_fraction(staff[",[192,1233,648],{"class":201},[192,1235,355],{"class":233},[192,1237,272],{"class":271},[192,1239,1240],{"class":233},").round(",[192,1242,1243],{"class":271},"3",[192,1245,265],{"class":233},[192,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267,1269,1271],{"class":56,"line":1248},10,[192,1250,341],{"class":271},[192,1252,510],{"class":233},[192,1254,347],{"class":201},[192,1256,515],{"class":233},[192,1258,648],{"class":201},[192,1260,515],{"class":233},[192,1262,1224],{"class":201},[192,1264,524],{"class":233},[192,1266,528],{"class":527},[192,1268,256],{"class":229},[192,1270,533],{"class":271},[192,1272,536],{"class":233},[10,1274,1275,1276,1278,1279,1281],{},"Each year's fraction is measured against that year's actual length, so the result increases smoothly and hits a whole number exactly on each anniversary. Excel's ",[14,1277,1035],{}," has five \"basis\" conventions (actual\u002Factual, 30\u002F360 and others) that give slightly different decimals; if the output must match a workbook's ",[14,1280,1035],{}," to the last digit, recalculate the workbook and compare, or state the basis in the report.",[178,1283,1285],{"id":1284},"step-6-group-tenure-into-bands","Step 6 — Group tenure into bands",[10,1287,1288,1289,1292],{},"Reports usually want bands rather than exact values — under one year, one to three, three to five, five to ten, ten and over. ",[14,1290,1291],{},"pd.cut"," on completed years makes the bands explicit and keeps their order:",[183,1294,1296],{"className":220,"code":1295,"language":222,"meta":188,"style":188},"bins = [0, 1, 3, 5, 10, 100]\nlabels = [\"\u003C1 year\", \"1-3 years\", \"3-5 years\", \"5-10 years\", \"10+ years\"]\nstaff[\"band\"] = pd.cut(staff[\"tenure_y\"], bins=bins, labels=labels, right=False)\nprint(staff[[\"employee\", \"tenure\", \"band\"]].to_string(index=False))\nprint(staff[\"band\"].value_counts(sort=False).to_dict())\n",[14,1297,1298,1336,1370,1413,1439],{"__ignoreMap":188},[192,1299,1300,1303,1305,1307,1309,1311,1314,1316,1318,1320,1323,1325,1328,1330,1333],{"class":56,"line":194},[192,1301,1302],{"class":233},"bins ",[192,1304,256],{"class":229},[192,1306,962],{"class":233},[192,1308,51],{"class":271},[192,1310,515],{"class":233},[192,1312,1313],{"class":271},"1",[192,1315,515],{"class":233},[192,1317,1243],{"class":271},[192,1319,515],{"class":233},[192,1321,1322],{"class":271},"5",[192,1324,515],{"class":233},[192,1326,1327],{"class":271},"10",[192,1329,515],{"class":233},[192,1331,1332],{"class":271},"100",[192,1334,1335],{"class":233},"]\n",[192,1337,1338,1341,1343,1345,1348,1350,1353,1355,1358,1360,1363,1365,1368],{"class":56,"line":243},[192,1339,1340],{"class":233},"labels ",[192,1342,256],{"class":229},[192,1344,962],{"class":233},[192,1346,1347],{"class":201},"\"\u003C1 year\"",[192,1349,515],{"class":233},[192,1351,1352],{"class":201},"\"1-3 years\"",[192,1354,515],{"class":233},[192,1356,1357],{"class":201},"\"3-5 years\"",[192,1359,515],{"class":233},[192,1361,1362],{"class":201},"\"5-10 years\"",[192,1364,515],{"class":233},[192,1366,1367],{"class":201},"\"10+ years\"",[192,1369,1335],{"class":233},[192,1371,1372,1374,1377,1379,1381,1384,1386,1388,1391,1393,1396,1399,1401,1404,1407,1409,1411],{"class":56,"line":250},[192,1373,483],{"class":233},[192,1375,1376],{"class":201},"\"band\"",[192,1378,489],{"class":233},[192,1380,256],{"class":229},[192,1382,1383],{"class":233}," pd.cut(staff[",[192,1385,661],{"class":201},[192,1387,355],{"class":233},[192,1389,1390],{"class":527},"bins",[192,1392,256],{"class":229},[192,1394,1395],{"class":233},"bins, ",[192,1397,1398],{"class":527},"labels",[192,1400,256],{"class":229},[192,1402,1403],{"class":233},"labels, ",[192,1405,1406],{"class":527},"right",[192,1408,256],{"class":229},[192,1410,533],{"class":271},[192,1412,265],{"class":233},[192,1414,1415,1417,1419,1421,1423,1425,1427,1429,1431,1433,1435,1437],{"class":56,"line":268},[192,1416,341],{"class":271},[192,1418,510],{"class":233},[192,1420,347],{"class":201},[192,1422,515],{"class":233},[192,1424,707],{"class":201},[192,1426,515],{"class":233},[192,1428,1376],{"class":201},[192,1430,524],{"class":233},[192,1432,528],{"class":527},[192,1434,256],{"class":229},[192,1436,533],{"class":271},[192,1438,536],{"class":233},[192,1440,1441,1443,1446,1448,1451,1454,1456,1458],{"class":56,"line":475},[192,1442,341],{"class":271},[192,1444,1445],{"class":233},"(staff[",[192,1447,1376],{"class":201},[192,1449,1450],{"class":233},"].value_counts(",[192,1452,1453],{"class":527},"sort",[192,1455,256],{"class":229},[192,1457,533],{"class":271},[192,1459,1460],{"class":233},").to_dict())\n",[10,1462,1463,1466,1467,1471],{},[14,1464,1465],{},"right=False"," makes each band include its lower edge, so exactly three years falls in \"3-5 years\" rather than \"1-3 years\". ",[23,1468,1470],{"href":1469},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut\u002F","Bin Values into Ranges with pandas cut"," covers band edges in detail.",[29,1473,38,1479,38,1482,38,1485,38,1489,38,1494,38,1499,38,1505,38,1509,38,1511,38,1515,38,1517,38,1520,38,1522,38,1526,38,1529,38,1532,38,1534,38,1538,38,1540,38,1542,38,1545,38,1549,38,1552],{"viewBox":1474,"role":32,"ariaLabelledBy":1475,"xmlns":36,"style":1478},"4 0 744 220",[1476,1477],"age-bands-t","age-bands-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,1480,1481],{"id":1476},"Staff by tenure band on the report date",[44,1483,1484],{"id":1477},"Bars counting employees in each tenure band as of 25 September 2026: one with less than a year, one with one to three years, none with three to five, one with five to ten, and three with ten years or more.",[48,1486],{"x":50,"y":51,"width":1487,"height":1488,"fill":54},"744","220",[48,1490],{"x":65,"y":66,"width":1491,"height":1492,"rx":1493,"fill":70,"stroke":71,"style":805},"96.0","28.0","7",[74,1495,1498],{"x":1496,"y":1497,"style":78},"68.0","34.5","\u003C1 year",[48,1500],{"x":1501,"y":66,"width":1502,"height":1492,"rx":1493,"fill":1503,"stroke":1504,"style":88},"126.0","196.5","#fdefd8","var(--gold,#b4740a)",[74,1506,1313],{"x":1507,"y":1497,"style":1508},"330.5","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start",[48,1510],{"x":65,"y":826,"width":1491,"height":1492,"rx":1493,"fill":70,"stroke":71,"style":805},[74,1512,1514],{"x":1496,"y":1513,"style":78},"74.5","1-3 years",[48,1516],{"x":1501,"y":826,"width":1502,"height":1492,"rx":1493,"fill":140,"stroke":141,"style":88},[74,1518,1313],{"x":1507,"y":1513,"style":1519},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start",[48,1521],{"x":65,"y":1491,"width":1491,"height":1492,"rx":1493,"fill":70,"stroke":71,"style":805},[74,1523,1525],{"x":1496,"y":1524,"style":78},"114.5","3-5 years",[48,1527],{"x":1501,"y":1491,"width":1528,"height":1492,"rx":1493,"fill":140,"stroke":141,"style":88},"6.0",[74,1530,51],{"x":1531,"y":1524,"style":1519},"140.0",[48,1533],{"x":65,"y":67,"width":1491,"height":1492,"rx":1493,"fill":70,"stroke":71,"style":805},[74,1535,1537],{"x":1496,"y":1536,"style":78},"154.5","5-10 years",[48,1539],{"x":1501,"y":67,"width":1502,"height":1492,"rx":1493,"fill":140,"stroke":141,"style":88},[74,1541,1313],{"x":1507,"y":1536,"style":1519},[48,1543],{"x":65,"y":1544,"width":1491,"height":1492,"rx":1493,"fill":70,"stroke":71,"style":805},"176.0",[74,1546,1548],{"x":1496,"y":1547,"style":78},"194.5","10+ years",[48,1550],{"x":1501,"y":1544,"width":1551,"height":1492,"rx":1493,"fill":163,"stroke":164,"style":88},"589.5",[74,1553,1243],{"x":1554,"y":1547,"style":1555},"723.5","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start",[178,1557,1559],{"id":1558},"put-datedif-next-to-the-results-for-checking","Put DATEDIF next to the results for checking",[10,1561,1562,1563,1566],{},"When a workbook will be reviewed by people who trust Excel more than Python, write the pandas results and a ",[14,1564,1565],{},"DATEDIF"," formula side by side. Anyone can see the two agree, and the formula keeps working if they change a date:",[183,1568,1570],{"className":220,"code":1569,"language":222,"meta":188,"style":188},"from openpyxl import load_workbook\n\nout = staff[[\"employee\", \"birth_date\", \"hire_date\", \"age\", \"tenure_months\"]]\nout.to_excel(\"staff_ages.xlsx\", index=False)\nwb = load_workbook(\"staff_ages.xlsx\")\nws = wb.active\nws[\"G1\"], ws[\"F1\"] = \"as_of\", \"age_check\"\nws[\"G2\"] = AS_OF.to_pydatetime()\nfor row in range(2, ws.max_row + 1):\n    ws[f\"F{row}\"] = f'=DATEDIF(B{row},$G$2,\"Y\")'\nwb.save(\"staff_ages.xlsx\")\nprint(ws[\"F2\"].value)\n",[14,1571,1572,1584,1588,1619,1637,1651,1661,1687,1704,1732,1775,1785],{"__ignoreMap":188},[192,1573,1574,1576,1579,1581],{"class":56,"line":194},[192,1575,920],{"class":229},[192,1577,1578],{"class":233}," openpyxl ",[192,1580,230],{"class":229},[192,1582,1583],{"class":233}," load_workbook\n",[192,1585,1586],{"class":56,"line":243},[192,1587,247],{"emptyLinePlaceholder":246},[192,1589,1590,1593,1595,1598,1600,1602,1604,1606,1608,1610,1612,1614,1616],{"class":56,"line":250},[192,1591,1592],{"class":233},"out ",[192,1594,256],{"class":229},[192,1596,1597],{"class":233}," staff[[",[192,1599,347],{"class":201},[192,1601,515],{"class":233},[192,1603,319],{"class":201},[192,1605,515],{"class":233},[192,1607,648],{"class":201},[192,1609,515],{"class":233},[192,1611,486],{"class":201},[192,1613,515],{"class":233},[192,1615,638],{"class":201},[192,1617,1618],{"class":233},"]]\n",[192,1620,1621,1624,1627,1629,1631,1633,1635],{"class":56,"line":268},[192,1622,1623],{"class":233},"out.to_excel(",[192,1625,1626],{"class":201},"\"staff_ages.xlsx\"",[192,1628,515],{"class":233},[192,1630,528],{"class":527},[192,1632,256],{"class":229},[192,1634,533],{"class":271},[192,1636,265],{"class":233},[192,1638,1639,1642,1644,1647,1649],{"class":56,"line":475},[192,1640,1641],{"class":233},"wb ",[192,1643,256],{"class":229},[192,1645,1646],{"class":233}," load_workbook(",[192,1648,1626],{"class":201},[192,1650,265],{"class":233},[192,1652,1653,1656,1658],{"class":56,"line":480},[192,1654,1655],{"class":233},"ws ",[192,1657,256],{"class":229},[192,1659,1660],{"class":233}," wb.active\n",[192,1662,1663,1666,1669,1672,1675,1677,1679,1682,1684],{"class":56,"line":505},[192,1664,1665],{"class":233},"ws[",[192,1667,1668],{"class":201},"\"G1\"",[192,1670,1671],{"class":233},"], ws[",[192,1673,1674],{"class":201},"\"F1\"",[192,1676,489],{"class":233},[192,1678,256],{"class":229},[192,1680,1681],{"class":201}," \"as_of\"",[192,1683,515],{"class":233},[192,1685,1686],{"class":201},"\"age_check\"\n",[192,1688,1689,1691,1694,1696,1698,1701],{"class":56,"line":702},[192,1690,1665],{"class":233},[192,1692,1693],{"class":201},"\"G2\"",[192,1695,489],{"class":233},[192,1697,256],{"class":229},[192,1699,1700],{"class":271}," AS_OF",[192,1702,1703],{"class":233},".to_pydatetime()\n",[192,1705,1706,1708,1711,1713,1716,1719,1722,1725,1727,1729],{"class":56,"line":750},[192,1707,953],{"class":229},[192,1709,1710],{"class":233}," row ",[192,1712,959],{"class":229},[192,1714,1715],{"class":271}," range",[192,1717,1718],{"class":233},"(",[192,1720,1721],{"class":271},"2",[192,1723,1724],{"class":233},", ws.max_row ",[192,1726,727],{"class":229},[192,1728,1150],{"class":271},[192,1730,1731],{"class":233},"):\n",[192,1733,1734,1737,1740,1743,1747,1750,1753,1756,1758,1760,1763,1766,1768,1770,1772],{"class":56,"line":1248},[192,1735,1736],{"class":233},"    ws[",[192,1738,1739],{"class":229},"f",[192,1741,1742],{"class":201},"\"F",[192,1744,1746],{"class":1745},"sSjpA","{",[192,1748,1749],{"class":233},"row",[192,1751,1752],{"class":1745},"}",[192,1754,1755],{"class":201},"\"",[192,1757,489],{"class":233},[192,1759,256],{"class":229},[192,1761,1762],{"class":229}," f",[192,1764,1765],{"class":201},"'=DATEDIF(B",[192,1767,1746],{"class":1745},[192,1769,1749],{"class":233},[192,1771,1752],{"class":1745},[192,1773,1774],{"class":201},",$G$2,\"Y\")'\n",[192,1776,1778,1781,1783],{"class":56,"line":1777},11,[192,1779,1780],{"class":233},"wb.save(",[192,1782,1626],{"class":201},[192,1784,265],{"class":233},[192,1786,1788,1790,1793,1796],{"class":56,"line":1787},12,[192,1789,341],{"class":271},[192,1791,1792],{"class":233},"(ws[",[192,1794,1795],{"class":201},"\"F2\"",[192,1797,1798],{"class":233},"].value)\n",[10,1800,1801,1802,1805,1806,1809],{},"Recalculated, the ",[14,1803,1804],{},"age_check"," column matches ",[14,1807,1808],{},"age"," row for row — which is how the rule in Step 2 was verified. Keep the check column in review copies and drop it from the final report, or keep it permanently if readers like to see the working.",[178,1811,1813],{"id":1812},"common-pitfalls","Common pitfalls",[1815,1816,1817,1825,1831,1837,1843],"ul",{},[1818,1819,1820,1824],"li",{},[1821,1822,1823],"strong",{},"Dividing days by 365.25."," It is off by one on birthdays and anniversaries.",[1818,1826,1827,1830],{},[1821,1828,1829],{},"Using today's date."," Reports change when rerun; pass the report date explicitly, and print it in the report header.",[1818,1832,1833,1836],{},[1821,1834,1835],{},"Two leap-day conventions in one system."," Choose one and document it.",[1818,1838,1839,1842],{},[1821,1840,1841],{},"Negative values."," Future start dates produce negative ages or tenure; flag them.",[1818,1844,1845,1848,1849,1853],{},[1821,1846,1847],{},"Dates stored as text."," Parse them first; ",[23,1850,1852],{"href":1851},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fparse-excel-dates-into-python-datetimes-with-pandas\u002F","Parse Excel Dates into Python datetimes with pandas"," shows how.",[178,1855,1857],{"id":1856},"performance-notes","Performance notes",[10,1859,1860,1861,1863,1864,1867],{},"All the functions here are vectorised: year, month and day components come from ",[14,1862,546],{}," accessors and the comparisons run on whole columns, so they handle millions of rows in well under a second. ",[14,1865,1866],{},"relativedelta"," is a per-row Python call and is best kept for single values or small tables.",[178,1869,1871],{"id":1870},"conclusion","Conclusion",[10,1873,1874,1875,1877,1878,1880],{},"Count completed years and months by comparing month and day, not by dividing days by 365.25 — the rule that ",[14,1876,1565],{}," uses and that the functions here implement for whole columns. Pass a fixed report date, decide once whether 29 February birthdays fall on 28 February or 1 March, use a year-length-aware fraction when tenure must be pro-rated, and group the results into ordered bands with ",[14,1879,1291],{},".",[178,1882,1884],{"id":1883},"frequently-asked-questions","Frequently asked questions",[10,1886,1887,1890],{},[1821,1888,1889],{},"Why not just divide the number of days by 365.25?","\nBecause leap years are not spread evenly, the result drifts across birthdays. On someone's 25th birthday, 9,131 days divided by 365.25 is 24.9993, which truncates to 24. Count completed years by comparing month and day instead.",[10,1892,1893,1896],{},[1821,1894,1895],{},"How does this match Excel's DATEDIF?","\nDATEDIF(start, end, \"Y\") counts completed years: the year difference, minus one if the end date's month and day come before the start date's. The pandas code on this page implements the same rule and was checked against DATEDIF results.",[10,1898,1899,1902],{},[1821,1900,1901],{},"When does someone born on 29 February turn a year older?","\nIt depends on the convention. DATEDIF, and the rule on this page, count the birthday as reached on 1 March in non-leap years. dateutil's relativedelta counts it on 28 February. Pick the convention your organisation uses and apply it consistently.",[10,1904,1905,1908],{},[1821,1906,1907],{},"How do I calculate tenure as of a report date rather than today?","\nPass the report date as the end date for every row instead of using today's date. Reports then give the same answer whenever they are rerun.",[178,1910,1912],{"id":1911},"related","Related",[1815,1914,1915,1921,1928,1935],{},[1818,1916,1917,1918,1920],{},"Up one level: ",[23,1919,26],{"href":25}," — dates from import to report.",[1818,1922,1923,1927],{},[23,1924,1926],{"href":1925},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas\u002F","Calculate Business Days Between Excel Dates with pandas"," — working days rather than calendar ones.",[1818,1929,1930,1934],{},[23,1931,1933],{"href":1932},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas\u002F","EOMONTH and EDATE Equivalents in pandas"," — moving dates by months.",[1818,1936,1937,1941],{},[23,1938,1940],{"href":1939},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002F","Group Excel Rows by Month and Quarter with pandas"," — calendar grouping.",[1943,1944,1945],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":188,"searchDepth":243,"depth":243,"links":1947},[1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960],{"id":180,"depth":243,"text":181},{"id":292,"depth":243,"text":293},{"id":376,"depth":243,"text":377},{"id":550,"depth":243,"text":551},{"id":902,"depth":243,"text":903},{"id":1028,"depth":243,"text":1029},{"id":1284,"depth":243,"text":1285},{"id":1558,"depth":243,"text":1559},{"id":1812,"depth":243,"text":1813},{"id":1856,"depth":243,"text":1857},{"id":1870,"depth":243,"text":1871},{"id":1883,"depth":243,"text":1884},{"id":1911,"depth":243,"text":1912},"2026-09-25","Compute completed years, months and days between two dates for a whole column — ages from birth dates, tenure from hire dates, contract length — matching Excel's DATEDIF. Covers why days divided by 365.25 is wrong on birthdays, 29 February, an as-of date, fractional years and tenure bands.","md",[1965,1967,1969,1971],{"q":1889,"a":1966},"Because leap years are not spread evenly, the result drifts across birthdays. On someone's 25th birthday, 9,131 days divided by 365.25 is 24.9993, which truncates to 24. Count completed years by comparing month and day instead.",{"q":1895,"a":1968},"DATEDIF(start, end, \"Y\") counts completed years: the year difference, minus one if the end date's month and day come before the start date's. The pandas code on this page implements the same rule and was checked against DATEDIF results.",{"q":1901,"a":1970},"It depends on the convention. DATEDIF, and the rule on this page, count the birthday as reached on 1 March in non-leap years. dateutil's relativedelta counts it on 28 February. Pick the convention your organisation uses and apply it consistently.",{"q":1907,"a":1972},"Pass the report date as the end date for every row instead of using today's date. Reports then give the same answer whenever they are rerun.","calculate age or tenure from dates with pandas datedif",null,{"breadcrumb":1976},[1977,1979,1982],{"name":1978,"item":325},"Home",{"name":1980,"item":1981},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":26,"item":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas",{"title":1985,"description":1986},"DATEDIF in pandas — Age and Tenure in Years, Months and Days","Calculate age and tenure from Excel dates in pandas the way DATEDIF does: completed years and months, the days\u002F365.25 bug, 29 February birthdays, dateutil relativedelta, YEARFRAC-style fractions and tenure bands.","calculate-age-and-tenure-from-dates-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-age-and-tenure-from-dates-with-pandas\u002Findex",[1990,1991,823],"dates-and-times","formula-equivalents","how-to","gwlnkwAdpmcC3lkl_-kMUFwCdIvPgYyzRWLE_6sCpck",[1995,1999],{"title":1996,"path":1997,"stem":1998,"children":-1},"Build a Date Table for Excel Reports with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fbuild-a-date-table-for-excel-reports-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fbuild-a-date-table-for-excel-reports-with-pandas\u002Findex",{"title":1926,"path":2000,"stem":2001,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-business-days-between-excel-dates-with-pandas\u002Findex",[2003,2005],{"path":2004,"title":1980},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2006,"title":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data",[],[2009,2012,2015,2018,2021,2023],{"path":2010,"title":2011,"kind":1992},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-overdue-dates-with-openpyxl","Highlight Overdue Dates in Excel with openpyxl",{"path":2013,"title":2014,"kind":1992},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-weekends-and-holidays-in-a-date-column","Highlight Weekends and Holidays in a Date Column",{"path":2016,"title":2017,"kind":1992},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fkeep-the-latest-record-per-key-with-pandas","Keep the Latest Record per Key When Deduplicating with pandas",{"path":2019,"title":2020,"kind":1992},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-moving-averages-with-pandas-rolling","Calculate Moving Averages with pandas rolling",{"path":2022,"title":1933,"kind":1992},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Feomonth-and-edate-equivalents-in-pandas",{"path":2024,"title":2025,"kind":1992},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fyearfrac-and-days360-equivalents-in-python","YEARFRAC and DAYS360 Equivalents in Python",1790972101073]