[{"data":1,"prerenderedAt":4003},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data":3995},{"id":4,"title":5,"body":6,"dateModified":610,"datePublished":610,"description":3970,"extension":3971,"faq":3972,"meta":3986,"navigation":223,"path":3987,"seo":3988,"slug":3991,"stem":3992,"type":3993,"__hash__":3994},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Findex.md","Working with Dates and Times in Excel Data",{"type":7,"value":8,"toc":3957},"minimark",[9,28,150,155,179,182,190,321,324,489,512,560,564,574,724,734,904,915,918,991,1000,1004,1007,1432,1439,1616,1627,1631,1634,1741,1744,2005,2012,2016,2027,2366,2373,2377,2380,2475,2763,2779,2786,2822,2826,2829,2851,2947,2976,2986,3110,3120,3196,3200,3207,3378,3388,3391,3638,3646,3653,3735,3739,3823,3827,3839,3845,3854,3880,3886,3902,3906,3953],[10,11,12,13,17,18,21,22,27],"p",{},"Dates are where Excel and Python disagree most. Excel has no date type at all — it stores a floating-point count of days since an epoch and decides at display time whether that number looks like a date, a time, or a plain number. Python has ",[14,15,16],"code",{},"datetime",", which is a real type with real semantics. Every read and every write crosses that boundary, and most date bugs in reporting scripts live exactly there: a column that arrives as ",[14,19,20],{},"45292.0",", a European date silently parsed as American, a timezone-aware timestamp that openpyxl refuses to write. This page covers the model, the parsing, the writing, and the period grouping that reports actually need. It sits within ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002F","Advanced Data Transformation and Cleaning",".",[29,30,39,40,39,44,39,48,39,55,39,65,39,72,39,77,39,83,39,87,39,99,39,109,39,118,39,124,39,129,39,134,39,139,39,144,39,147],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"ariaLabelledBy":34,"xmlns":37,"style":38},"0 0 820 250","img","The Excel date model: a stored serial number combined with a number-format string produces the displayed date, while Python sees the serial and needs the 1899-12-30 origin to convert it to a datetime.",[35,36],"datemodel-t","datemodel-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:820px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[41,42,43],"title",{"id":35},"How Excel stores a date and how Python sees it",[45,46,47],"desc",{"id":36},"One stored value, 45292.75, feeds two consumers. Excel combines it with the cell's number format string to display the fifteenth of August 2026 at six in the evening. Python receives the raw number and must apply the 1899-12-30 origin to reconstruct the same datetime. The integer part is the day count and the fractional part is the time of day.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","820","250","#ffffff",[49,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"270","20","280","66","14","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[66,67,71],"text",{"x":68,"y":69,"style":70},"410","44","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","what the cell actually holds",[66,73,76],{"x":68,"y":74,"style":75},"72","font-size:20px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","45292.75",[66,78,82],{"x":79,"y":80,"style":81},"196","112","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","45292 = whole days",[66,84,86],{"x":85,"y":80,"style":81},"628",".75 = 18:00, three quarters of a day",[88,89,91,92,91,96,39],"g",{"stroke":63,"style":64,"fill":90},"none","\n    ",[93,94],"path",{"d":95},"M340 86 V 122 H 200 V 150",[93,97],{"d":98},"M480 86 V 122 H 620 V 150",[88,100,91,102,91,106,39],{"fill":101},"#5b5cf0",[103,104],"polygon",{"points":105},"200,158 193,146 207,146",[103,107],{"points":108},"620,158 613,146 627,146",[49,110],{"x":111,"y":112,"width":113,"height":114,"rx":115,"fill":116,"stroke":117,"style":64},"40","158","320","76","13","#d9f4f1","var(--teal,#0f9488)",[66,119,123],{"x":120,"y":121,"style":122},"200","182","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Excel: value + number format",[66,125,128],{"x":120,"y":126,"style":127},"204","font-size:11px;fill:var(--text,#172033);text-anchor:middle","\"yyyy-mm-dd hh:mm\" applied at display",[66,130,133],{"x":120,"y":131,"style":132},"224","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","2026-08-15 18:00",[49,135],{"x":136,"y":112,"width":113,"height":114,"rx":115,"fill":137,"stroke":138,"style":64},"460","#fdefd8","var(--gold,#b4740a)",[66,140,143],{"x":141,"y":121,"style":142},"620","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Python: needs the origin",[66,145,146],{"x":141,"y":126,"style":127},"to_datetime(v, unit=\"D\", origin=\"1899-12-30\")",[66,148,149],{"x":141,"y":131,"style":132},"datetime(2026, 8, 15, 18, 0)",[151,152,154],"h2",{"id":153},"the-serial-number-model-and-the-epoch-that-is-off-by-two-days","The serial-number model, and the epoch that is off by two days",[10,156,157,158,161,162,165,166,169,170,173,174,178],{},"An Excel date is a number. ",[14,159,160],{},"1"," is 1 January 1900, ",[14,163,164],{},"45292"," is 1 January 2024, and the fractional part is the time of day: ",[14,167,168],{},".5"," is noon, ",[14,171,172],{},".75"," is six in the evening. Whether a cell ",[175,176,177],"em",{},"looks"," like a date is purely a matter of its number format string — which is why the single most common Excel-from-Python complaint, \"my date shows as 45292\", is a formatting problem, not a data problem.",[10,180,181],{},"The epoch is where it gets strange. Excel treats 1900 as a leap year. It was not: century years are leap years only when divisible by 400. The bug was inherited from Lotus 1-2-3 in the 1980s and deliberately never fixed, because fixing it would shift every date in every existing spreadsheet. So Excel's calendar contains a 29 February 1900 that never existed, and every serial after it is one higher than a correct day count would be.",[10,183,184,185,189],{},"The practical consequence is a single constant. Anchor conversions at ",[186,187,188],"strong",{},"1899-12-30",", not 1900-01-01, and the phantom day cancels out:",[191,192,197],"pre",{"className":193,"code":194,"language":195,"meta":196,"style":196},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nserials = pd.Series([45292.0, 45658.5, 46023.75])\n\ndates = pd.to_datetime(serials, unit=\"D\", origin=\"1899-12-30\")\nprint(dates)\n# 0   2024-01-01 00:00:00\n# 1   2025-01-01 12:00:00\n# 2   2025-12-31 18:00:00\n","python","",[14,198,199,218,225,254,259,293,302,309,315],{"__ignoreMap":196},[200,201,204,208,212,215],"span",{"class":202,"line":203},"line",1,[200,205,207],{"class":206},"s-kum","import",[200,209,211],{"class":210},"skGVy"," pandas ",[200,213,214],{"class":206},"as",[200,216,217],{"class":210}," pd\n",[200,219,221],{"class":202,"line":220},2,[200,222,224],{"emptyLinePlaceholder":223},true,"\n",[200,226,228,231,234,237,240,243,246,248,251],{"class":202,"line":227},3,[200,229,230],{"class":210},"serials ",[200,232,233],{"class":206},"=",[200,235,236],{"class":210}," pd.Series([",[200,238,20],{"class":239},"sP0c6",[200,241,242],{"class":210},", ",[200,244,245],{"class":239},"45658.5",[200,247,242],{"class":210},[200,249,250],{"class":239},"46023.75",[200,252,253],{"class":210},"])\n",[200,255,257],{"class":202,"line":256},4,[200,258,224],{"emptyLinePlaceholder":223},[200,260,262,265,267,270,274,276,280,282,285,287,290],{"class":202,"line":261},5,[200,263,264],{"class":210},"dates ",[200,266,233],{"class":206},[200,268,269],{"class":210}," pd.to_datetime(serials, ",[200,271,273],{"class":272},"sa561","unit",[200,275,233],{"class":206},[200,277,279],{"class":278},"srMev","\"D\"",[200,281,242],{"class":210},[200,283,284],{"class":272},"origin",[200,286,233],{"class":206},[200,288,289],{"class":278},"\"1899-12-30\"",[200,291,292],{"class":210},")\n",[200,294,296,299],{"class":202,"line":295},6,[200,297,298],{"class":239},"print",[200,300,301],{"class":210},"(dates)\n",[200,303,305],{"class":202,"line":304},7,[200,306,308],{"class":307},"s-wDw","# 0   2024-01-01 00:00:00\n",[200,310,312],{"class":202,"line":311},8,[200,313,314],{"class":307},"# 1   2025-01-01 12:00:00\n",[200,316,318],{"class":202,"line":317},9,[200,319,320],{"class":307},"# 2   2025-12-31 18:00:00\n",[10,322,323],{},"Going the other way — turning a Python datetime into the serial Excel expects — is the same arithmetic reversed:",[191,325,327],{"className":193,"code":326,"language":195,"meta":196,"style":196},"from datetime import datetime\n\nEXCEL_EPOCH = datetime(1899, 12, 30)\n\ndef to_excel_serial(dt):\n    \"\"\"Convert a naive datetime to Excel's day-count representation.\"\"\"\n    delta = dt - EXCEL_EPOCH\n    return delta.days + delta.seconds \u002F 86_400\n\nprint(to_excel_serial(datetime(2024, 1, 1)))        # 45292.0\nprint(to_excel_serial(datetime(2026, 8, 15, 18)))   # 46249.75\n",[14,328,329,342,346,372,376,388,393,409,429,433,458],{"__ignoreMap":196},[200,330,331,334,337,339],{"class":202,"line":203},[200,332,333],{"class":206},"from",[200,335,336],{"class":210}," datetime ",[200,338,207],{"class":206},[200,340,341],{"class":210}," datetime\n",[200,343,344],{"class":202,"line":220},[200,345,224],{"emptyLinePlaceholder":223},[200,347,348,351,354,357,360,362,365,367,370],{"class":202,"line":227},[200,349,350],{"class":239},"EXCEL_EPOCH",[200,352,353],{"class":206}," =",[200,355,356],{"class":210}," datetime(",[200,358,359],{"class":239},"1899",[200,361,242],{"class":210},[200,363,364],{"class":239},"12",[200,366,242],{"class":210},[200,368,369],{"class":239},"30",[200,371,292],{"class":210},[200,373,374],{"class":202,"line":256},[200,375,224],{"emptyLinePlaceholder":223},[200,377,378,381,385],{"class":202,"line":261},[200,379,380],{"class":206},"def",[200,382,384],{"class":383},"s_Opv"," to_excel_serial",[200,386,387],{"class":210},"(dt):\n",[200,389,390],{"class":202,"line":295},[200,391,392],{"class":278},"    \"\"\"Convert a naive datetime to Excel's day-count representation.\"\"\"\n",[200,394,395,398,400,403,406],{"class":202,"line":304},[200,396,397],{"class":210},"    delta ",[200,399,233],{"class":206},[200,401,402],{"class":210}," dt ",[200,404,405],{"class":206},"-",[200,407,408],{"class":239}," EXCEL_EPOCH\n",[200,410,411,414,417,420,423,426],{"class":202,"line":311},[200,412,413],{"class":206},"    return",[200,415,416],{"class":210}," delta.days ",[200,418,419],{"class":206},"+",[200,421,422],{"class":210}," delta.seconds ",[200,424,425],{"class":206},"\u002F",[200,427,428],{"class":239}," 86_400\n",[200,430,431],{"class":202,"line":317},[200,432,224],{"emptyLinePlaceholder":223},[200,434,436,438,441,444,446,448,450,452,455],{"class":202,"line":435},10,[200,437,298],{"class":239},[200,439,440],{"class":210},"(to_excel_serial(datetime(",[200,442,443],{"class":239},"2024",[200,445,242],{"class":210},[200,447,160],{"class":239},[200,449,242],{"class":210},[200,451,160],{"class":239},[200,453,454],{"class":210},")))        ",[200,456,457],{"class":307},"# 45292.0\n",[200,459,461,463,465,468,470,473,475,478,480,483,486],{"class":202,"line":460},11,[200,462,298],{"class":239},[200,464,440],{"class":210},[200,466,467],{"class":239},"2026",[200,469,242],{"class":210},[200,471,472],{"class":239},"8",[200,474,242],{"class":210},[200,476,477],{"class":239},"15",[200,479,242],{"class":210},[200,481,482],{"class":239},"18",[200,484,485],{"class":210},")))   ",[200,487,488],{"class":307},"# 46249.75\n",[10,490,491,492,495,496,242,499,502,503,506,507,511],{},"There is a second, rarer epoch: workbooks created on very old Macs use a ",[186,493,494],{},"1904"," date system, anchored at 1904-01-01. Files in the wild carry a flag telling you which. When reading a legacy ",[14,497,498],{},".xls",[14,500,501],{},"xlrd"," exposes it as ",[14,504,505],{},"book.datemode",", as shown in ",[23,508,510],{"href":509},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas\u002F","reading .xls files with xlrd and pandas",". Dates off by roughly four years and a day are the signature of the wrong epoch.",[513,514,515,531],"table",{},[516,517,518],"thead",{},[519,520,521,525,528],"tr",{},[522,523,524],"th",{},"System",[522,526,527],{},"Origin to use",[522,529,530],{},"Signature of getting it wrong",[532,533,534,547],"tbody",{},[519,535,536,540,544],{},[537,538,539],"td",{},"1900 (default)",[537,541,542],{},[14,543,188],{},[537,545,546],{},"Dates one day late if you anchor at 1900-01-01",[519,548,549,552,557],{},[537,550,551],{},"1904 (old Mac)",[537,553,554],{},[14,555,556],{},"1904-01-01",[537,558,559],{},"Dates roughly 4 years and 1 day early",[151,561,563],{"id":562},"reading-dates-that-arrive-as-text","Reading dates that arrive as text",[10,565,566,567,569,570,573],{},"When a cell carries a date number format, openpyxl and pandas hand you a real ",[14,568,16],{}," and there is nothing to do. The work starts when dates arrive as ",[175,571,572],{},"strings"," — because somebody typed them, because an upstream CSV lost the typing, or because the export wrote text.",[29,575,39,582,39,585,39,588,39,592,39,598,39,606,39,611,39,613,39,617,39,620,39,624,39,627,39,631,39,634,39,637,39,655,39,660,39,666,39,670,39,674,39,682,39,690,39,696,39,701,39,705,39,709,39,714,39,718,39,721],{"viewBox":576,"role":32,"ariaLabel":577,"ariaLabelledBy":578,"xmlns":37,"style":581},"0 0 800 268","A parsing funnel for a messy date column: mixed formats and blanks pass through to_datetime with coerce, producing valid timestamps plus a bucket of NaT rows that must be inspected rather than silently dropped.",[579,580],"parse-t","parse-d","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[41,583,584],{"id":579},"Parsing a mixed-format date column safely",[45,586,587],{"id":580},"Five raw values — an ISO date, a slash-separated day-first date, a long written date, an empty string and the word pending — enter pd.to_datetime with errors set to coerce. Three parse to real timestamps. Two become NaT and land in a rejects bucket that the script inspects and reports rather than discarding silently.",[49,589],{"x":51,"y":51,"width":590,"height":591,"fill":54},"800","268",[66,593,597],{"x":594,"y":595,"style":596},"98","26","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","raw column",[49,599],{"x":600,"y":601,"width":602,"height":369,"rx":603,"fill":604,"stroke":605},"16","36","164","7","#f0f2f5","var(--line,#cdd5e6)",[66,607,610],{"x":594,"y":608,"style":609},"56","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","2026-08-15",[49,612],{"x":600,"y":74,"width":602,"height":369,"rx":603,"fill":604,"stroke":605},[66,614,616],{"x":594,"y":615,"style":609},"92","15\u002F08\u002F2026",[49,618],{"x":600,"y":619,"width":602,"height":369,"rx":603,"fill":604,"stroke":605},"108",[66,621,623],{"x":594,"y":622,"style":609},"128","Aug 15, 2026",[49,625],{"x":600,"y":626,"width":602,"height":369,"rx":603,"fill":604,"stroke":605},"144",[66,628,630],{"x":594,"y":602,"style":629},"font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","(empty)",[49,632],{"x":600,"y":633,"width":602,"height":369,"rx":603,"fill":604,"stroke":605},"180",[66,635,636],{"x":594,"y":120,"style":609},"pending",[88,638,91,640,91,643,91,646,91,649,91,652,39],{"stroke":605,"style":639,"fill":90},"stroke-width:1.5px",[93,641],{"d":642},"M180 51 H 236 V 118",[93,644],{"d":645},"M180 87 H 236 V 118",[93,647],{"d":648},"M180 123 H 236",[93,650],{"d":651},"M180 159 H 236 V 123",[93,653],{"d":654},"M180 195 H 236 V 128",[49,656],{"x":657,"y":658,"width":659,"height":74,"rx":115,"fill":62,"stroke":63,"style":64},"244","88","216",[66,661,665],{"x":662,"y":663,"style":664},"352","114","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pd.to_datetime",[66,667,669],{"x":662,"y":668,"style":81},"134","format=\"mixed\"",[66,671,673],{"x":662,"y":672,"style":81},"150","errors=\"coerce\"",[88,675,91,676,91,679,39],{"stroke":63,"style":64,"fill":90},[93,677],{"d":678},"M460 110 H 512 V 74 H 546",[93,680],{"d":681},"M460 138 H 512 V 196 H 546",[88,683,91,684,91,687,39],{"fill":101},[103,685],{"points":686},"554,74 542,68 542,80",[103,688],{"points":689},"554,196 542,190 542,202",[49,691],{"x":692,"y":693,"width":694,"height":695,"rx":115,"fill":116,"stroke":117,"style":64},"562","34","222","80",[66,697,700],{"x":698,"y":699,"style":122},"673","60","3 parsed",[66,702,704],{"x":698,"y":703,"style":127},"82","datetime64[ns]",[66,706,708],{"x":698,"y":707,"style":81},"100","ready to group and format",[49,710],{"x":692,"y":711,"width":694,"height":695,"rx":115,"fill":712,"stroke":713,"style":64},"156","#fee8f2","var(--accent,#f43f8f)",[66,715,717],{"x":698,"y":121,"style":716},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","2 became NaT",[66,719,720],{"x":698,"y":126,"style":127},"report, do not discard",[66,722,723],{"x":698,"y":694,"style":81},"blank vs unparseable differ",[10,725,726,727,729,730,733],{},"The safe parse names the failure mode explicitly. ",[14,728,673],{}," turns anything unparseable into ",[14,731,732],{},"NaT"," rather than raising, which keeps the pipeline running — but only if you then look at what failed:",[191,735,737],{"className":193,"code":736,"language":195,"meta":196,"style":196},"import pandas as pd\n\ndf = pd.read_excel(\"orders.xlsx\")\n\nraw = df[\"invoice_date\"].copy()\ndf[\"invoice_date\"] = pd.to_datetime(raw, errors=\"coerce\", format=\"mixed\")\n\nfailed = df.loc[df[\"invoice_date\"].isna() & raw.notna(), :]\nif len(failed):\n    print(f\"{len(failed)} unparseable dates:\")\n    print(raw[failed.index].value_counts().head(10))\n",[14,738,739,749,753,768,772,788,823,827,848,859,891],{"__ignoreMap":196},[200,740,741,743,745,747],{"class":202,"line":203},[200,742,207],{"class":206},[200,744,211],{"class":210},[200,746,214],{"class":206},[200,748,217],{"class":210},[200,750,751],{"class":202,"line":220},[200,752,224],{"emptyLinePlaceholder":223},[200,754,755,758,760,763,766],{"class":202,"line":227},[200,756,757],{"class":210},"df ",[200,759,233],{"class":206},[200,761,762],{"class":210}," pd.read_excel(",[200,764,765],{"class":278},"\"orders.xlsx\"",[200,767,292],{"class":210},[200,769,770],{"class":202,"line":256},[200,771,224],{"emptyLinePlaceholder":223},[200,773,774,777,779,782,785],{"class":202,"line":261},[200,775,776],{"class":210},"raw ",[200,778,233],{"class":206},[200,780,781],{"class":210}," df[",[200,783,784],{"class":278},"\"invoice_date\"",[200,786,787],{"class":210},"].copy()\n",[200,789,790,793,795,798,800,803,806,808,811,813,816,818,821],{"class":202,"line":295},[200,791,792],{"class":210},"df[",[200,794,784],{"class":278},[200,796,797],{"class":210},"] ",[200,799,233],{"class":206},[200,801,802],{"class":210}," pd.to_datetime(raw, ",[200,804,805],{"class":272},"errors",[200,807,233],{"class":206},[200,809,810],{"class":278},"\"coerce\"",[200,812,242],{"class":210},[200,814,815],{"class":272},"format",[200,817,233],{"class":206},[200,819,820],{"class":278},"\"mixed\"",[200,822,292],{"class":210},[200,824,825],{"class":202,"line":304},[200,826,224],{"emptyLinePlaceholder":223},[200,828,829,832,834,837,839,842,845],{"class":202,"line":311},[200,830,831],{"class":210},"failed ",[200,833,233],{"class":206},[200,835,836],{"class":210}," df.loc[df[",[200,838,784],{"class":278},[200,840,841],{"class":210},"].isna() ",[200,843,844],{"class":206},"&",[200,846,847],{"class":210}," raw.notna(), :]\n",[200,849,850,853,856],{"class":202,"line":317},[200,851,852],{"class":206},"if",[200,854,855],{"class":239}," len",[200,857,858],{"class":210},"(failed):\n",[200,860,861,864,867,870,873,877,880,883,886,889],{"class":202,"line":435},[200,862,863],{"class":239},"    print",[200,865,866],{"class":210},"(",[200,868,869],{"class":206},"f",[200,871,872],{"class":278},"\"",[200,874,876],{"class":875},"sSjpA","{",[200,878,879],{"class":239},"len",[200,881,882],{"class":210},"(failed)",[200,884,885],{"class":875},"}",[200,887,888],{"class":278}," unparseable dates:\"",[200,890,292],{"class":210},[200,892,893,895,898,901],{"class":202,"line":460},[200,894,863],{"class":239},[200,896,897],{"class":210},"(raw[failed.index].value_counts().head(",[200,899,900],{"class":239},"10",[200,902,903],{"class":210},"))\n",[10,905,906,907,910,911,27],{},"Distinguishing a genuinely blank cell from an unparseable one — the ",[14,908,909],{},"raw.notna()"," clause — is what turns this from a silent data loss into a report. The same principle drives ",[23,912,914],{"href":913},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffind-and-report-missing-values-in-an-excel-file\u002F","finding and reporting missing values in an Excel file",[10,916,917],{},"Two flags decide the ambiguous cases:",[191,919,921],{"className":193,"code":920,"language":195,"meta":196,"style":196},"# 03\u002F04\u002F2026: European data means 3 April, not 4 March.\npd.to_datetime(col, dayfirst=True, errors=\"coerce\")\n\n# A single known layout parses far faster than inference,\n# and rejects anything that does not match exactly.\npd.to_datetime(col, format=\"%d\u002F%m\u002F%Y\", errors=\"coerce\")\n",[14,922,923,928,951,955,960,965],{"__ignoreMap":196},[200,924,925],{"class":202,"line":203},[200,926,927],{"class":307},"# 03\u002F04\u002F2026: European data means 3 April, not 4 March.\n",[200,929,930,933,936,938,941,943,945,947,949],{"class":202,"line":220},[200,931,932],{"class":210},"pd.to_datetime(col, ",[200,934,935],{"class":272},"dayfirst",[200,937,233],{"class":206},[200,939,940],{"class":239},"True",[200,942,242],{"class":210},[200,944,805],{"class":272},[200,946,233],{"class":206},[200,948,810],{"class":278},[200,950,292],{"class":210},[200,952,953],{"class":202,"line":227},[200,954,224],{"emptyLinePlaceholder":223},[200,956,957],{"class":202,"line":256},[200,958,959],{"class":307},"# A single known layout parses far faster than inference,\n",[200,961,962],{"class":202,"line":261},[200,963,964],{"class":307},"# and rejects anything that does not match exactly.\n",[200,966,967,969,971,973,975,978,981,983,985,987,989],{"class":202,"line":295},[200,968,932],{"class":210},[200,970,815],{"class":272},[200,972,233],{"class":206},[200,974,872],{"class":278},[200,976,977],{"class":875},"%d",[200,979,980],{"class":278},"\u002F%m\u002F%Y\"",[200,982,242],{"class":210},[200,984,805],{"class":272},[200,986,233],{"class":206},[200,988,810],{"class":278},[200,990,292],{"class":210},[10,992,993,994,996,997,999],{},"Passing an explicit ",[14,995,815],{}," is both a performance win and a correctness win: on a column of a million values it is roughly an order of magnitude faster than inference, and it refuses to guess. Reach for ",[14,998,669],{}," only when the column really does contain more than one layout.",[151,1001,1003],{"id":1002},"writing-dates-back-so-excel-displays-them","Writing dates back so Excel displays them",[10,1005,1006],{},"Writing is where the \"shows as 45292\" problem appears. openpyxl writes the value correctly and applies a default date format; the moment you set your own number format, or write through a path that does not, you own the display.",[191,1008,1010],{"className":193,"code":1009,"language":195,"meta":196,"style":196},"from datetime import datetime, date\nfrom openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\nws.title = \"Orders\"\n\nws.append([\"order\", \"placed\", \"due\", \"processed_at\"])\nws.append([1001, date(2026, 8, 15), date(2026, 8, 29),\n           datetime(2026, 8, 15, 18, 4, 32)])\nws.append([1002, date(2026, 8, 16), date(2026, 8, 30),\n           datetime(2026, 8, 16, 9, 12, 7)])\n\n# Without these, the cells display as raw serial numbers.\nfor row in ws.iter_rows(min_row=2, min_col=2, max_col=3):\n    for cell in row:\n        cell.number_format = \"yyyy-mm-dd\"\n\nfor row in ws.iter_rows(min_row=2, min_col=4, max_col=4):\n    for cell in row:\n        cell.number_format = \"yyyy-mm-dd hh:mm:ss\"\n\nws.column_dimensions[\"B\"].width = 14\nws.column_dimensions[\"C\"].width = 14\nws.column_dimensions[\"D\"].width = 22\nwb.save(\"orders.xlsx\")\n",[14,1011,1012,1023,1035,1039,1049,1059,1069,1073,1098,1135,1167,1200,1230,1235,1241,1286,1300,1311,1316,1351,1362,1372,1377,1394,1408,1422],{"__ignoreMap":196},[200,1013,1014,1016,1018,1020],{"class":202,"line":203},[200,1015,333],{"class":206},[200,1017,336],{"class":210},[200,1019,207],{"class":206},[200,1021,1022],{"class":210}," datetime, date\n",[200,1024,1025,1027,1030,1032],{"class":202,"line":220},[200,1026,333],{"class":206},[200,1028,1029],{"class":210}," openpyxl ",[200,1031,207],{"class":206},[200,1033,1034],{"class":210}," Workbook\n",[200,1036,1037],{"class":202,"line":227},[200,1038,224],{"emptyLinePlaceholder":223},[200,1040,1041,1044,1046],{"class":202,"line":256},[200,1042,1043],{"class":210},"wb ",[200,1045,233],{"class":206},[200,1047,1048],{"class":210}," Workbook()\n",[200,1050,1051,1054,1056],{"class":202,"line":261},[200,1052,1053],{"class":210},"ws ",[200,1055,233],{"class":206},[200,1057,1058],{"class":210}," wb.active\n",[200,1060,1061,1064,1066],{"class":202,"line":295},[200,1062,1063],{"class":210},"ws.title ",[200,1065,233],{"class":206},[200,1067,1068],{"class":278}," \"Orders\"\n",[200,1070,1071],{"class":202,"line":304},[200,1072,224],{"emptyLinePlaceholder":223},[200,1074,1075,1078,1081,1083,1086,1088,1091,1093,1096],{"class":202,"line":311},[200,1076,1077],{"class":210},"ws.append([",[200,1079,1080],{"class":278},"\"order\"",[200,1082,242],{"class":210},[200,1084,1085],{"class":278},"\"placed\"",[200,1087,242],{"class":210},[200,1089,1090],{"class":278},"\"due\"",[200,1092,242],{"class":210},[200,1094,1095],{"class":278},"\"processed_at\"",[200,1097,253],{"class":210},[200,1099,1100,1102,1105,1108,1110,1112,1114,1116,1118,1121,1123,1125,1127,1129,1132],{"class":202,"line":317},[200,1101,1077],{"class":210},[200,1103,1104],{"class":239},"1001",[200,1106,1107],{"class":210},", date(",[200,1109,467],{"class":239},[200,1111,242],{"class":210},[200,1113,472],{"class":239},[200,1115,242],{"class":210},[200,1117,477],{"class":239},[200,1119,1120],{"class":210},"), date(",[200,1122,467],{"class":239},[200,1124,242],{"class":210},[200,1126,472],{"class":239},[200,1128,242],{"class":210},[200,1130,1131],{"class":239},"29",[200,1133,1134],{"class":210},"),\n",[200,1136,1137,1140,1142,1144,1146,1148,1150,1152,1154,1156,1159,1161,1164],{"class":202,"line":435},[200,1138,1139],{"class":210},"           datetime(",[200,1141,467],{"class":239},[200,1143,242],{"class":210},[200,1145,472],{"class":239},[200,1147,242],{"class":210},[200,1149,477],{"class":239},[200,1151,242],{"class":210},[200,1153,482],{"class":239},[200,1155,242],{"class":210},[200,1157,1158],{"class":239},"4",[200,1160,242],{"class":210},[200,1162,1163],{"class":239},"32",[200,1165,1166],{"class":210},")])\n",[200,1168,1169,1171,1174,1176,1178,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198],{"class":202,"line":460},[200,1170,1077],{"class":210},[200,1172,1173],{"class":239},"1002",[200,1175,1107],{"class":210},[200,1177,467],{"class":239},[200,1179,242],{"class":210},[200,1181,472],{"class":239},[200,1183,242],{"class":210},[200,1185,600],{"class":239},[200,1187,1120],{"class":210},[200,1189,467],{"class":239},[200,1191,242],{"class":210},[200,1193,472],{"class":239},[200,1195,242],{"class":210},[200,1197,369],{"class":239},[200,1199,1134],{"class":210},[200,1201,1203,1205,1207,1209,1211,1213,1215,1217,1220,1222,1224,1226,1228],{"class":202,"line":1202},12,[200,1204,1139],{"class":210},[200,1206,467],{"class":239},[200,1208,242],{"class":210},[200,1210,472],{"class":239},[200,1212,242],{"class":210},[200,1214,600],{"class":239},[200,1216,242],{"class":210},[200,1218,1219],{"class":239},"9",[200,1221,242],{"class":210},[200,1223,364],{"class":239},[200,1225,242],{"class":210},[200,1227,603],{"class":239},[200,1229,1166],{"class":210},[200,1231,1233],{"class":202,"line":1232},13,[200,1234,224],{"emptyLinePlaceholder":223},[200,1236,1238],{"class":202,"line":1237},14,[200,1239,1240],{"class":307},"# Without these, the cells display as raw serial numbers.\n",[200,1242,1244,1247,1250,1253,1256,1259,1261,1264,1266,1269,1271,1273,1275,1278,1280,1283],{"class":202,"line":1243},15,[200,1245,1246],{"class":206},"for",[200,1248,1249],{"class":210}," row ",[200,1251,1252],{"class":206},"in",[200,1254,1255],{"class":210}," ws.iter_rows(",[200,1257,1258],{"class":272},"min_row",[200,1260,233],{"class":206},[200,1262,1263],{"class":239},"2",[200,1265,242],{"class":210},[200,1267,1268],{"class":272},"min_col",[200,1270,233],{"class":206},[200,1272,1263],{"class":239},[200,1274,242],{"class":210},[200,1276,1277],{"class":272},"max_col",[200,1279,233],{"class":206},[200,1281,1282],{"class":239},"3",[200,1284,1285],{"class":210},"):\n",[200,1287,1289,1292,1295,1297],{"class":202,"line":1288},16,[200,1290,1291],{"class":206},"    for",[200,1293,1294],{"class":210}," cell ",[200,1296,1252],{"class":206},[200,1298,1299],{"class":210}," row:\n",[200,1301,1303,1306,1308],{"class":202,"line":1302},17,[200,1304,1305],{"class":210},"        cell.number_format ",[200,1307,233],{"class":206},[200,1309,1310],{"class":278}," \"yyyy-mm-dd\"\n",[200,1312,1314],{"class":202,"line":1313},18,[200,1315,224],{"emptyLinePlaceholder":223},[200,1317,1319,1321,1323,1325,1327,1329,1331,1333,1335,1337,1339,1341,1343,1345,1347,1349],{"class":202,"line":1318},19,[200,1320,1246],{"class":206},[200,1322,1249],{"class":210},[200,1324,1252],{"class":206},[200,1326,1255],{"class":210},[200,1328,1258],{"class":272},[200,1330,233],{"class":206},[200,1332,1263],{"class":239},[200,1334,242],{"class":210},[200,1336,1268],{"class":272},[200,1338,233],{"class":206},[200,1340,1158],{"class":239},[200,1342,242],{"class":210},[200,1344,1277],{"class":272},[200,1346,233],{"class":206},[200,1348,1158],{"class":239},[200,1350,1285],{"class":210},[200,1352,1354,1356,1358,1360],{"class":202,"line":1353},20,[200,1355,1291],{"class":206},[200,1357,1294],{"class":210},[200,1359,1252],{"class":206},[200,1361,1299],{"class":210},[200,1363,1365,1367,1369],{"class":202,"line":1364},21,[200,1366,1305],{"class":210},[200,1368,233],{"class":206},[200,1370,1371],{"class":278}," \"yyyy-mm-dd hh:mm:ss\"\n",[200,1373,1375],{"class":202,"line":1374},22,[200,1376,224],{"emptyLinePlaceholder":223},[200,1378,1380,1383,1386,1389,1391],{"class":202,"line":1379},23,[200,1381,1382],{"class":210},"ws.column_dimensions[",[200,1384,1385],{"class":278},"\"B\"",[200,1387,1388],{"class":210},"].width ",[200,1390,233],{"class":206},[200,1392,1393],{"class":239}," 14\n",[200,1395,1397,1399,1402,1404,1406],{"class":202,"line":1396},24,[200,1398,1382],{"class":210},[200,1400,1401],{"class":278},"\"C\"",[200,1403,1388],{"class":210},[200,1405,233],{"class":206},[200,1407,1393],{"class":239},[200,1409,1411,1413,1415,1417,1419],{"class":202,"line":1410},25,[200,1412,1382],{"class":210},[200,1414,279],{"class":278},[200,1416,1388],{"class":210},[200,1418,233],{"class":206},[200,1420,1421],{"class":239}," 22\n",[200,1423,1425,1428,1430],{"class":202,"line":1424},26,[200,1426,1427],{"class":210},"wb.save(",[200,1429,765],{"class":278},[200,1431,292],{"class":210},[10,1433,1434,1435,1438],{},"Through pandas the equivalent knobs live on ",[14,1436,1437],{},"ExcelWriter",", which sets a workbook-wide default so you do not touch cells one at a time:",[191,1440,1442],{"className":193,"code":1441,"language":195,"meta":196,"style":196},"import pandas as pd\n\ndf = pd.DataFrame({\n    \"order\": [1001, 1002],\n    \"placed\": pd.to_datetime([\"2026-08-15\", \"2026-08-16\"]),\n    \"processed_at\": pd.to_datetime([\"2026-08-15 18:04\", \"2026-08-16 09:12\"]),\n})\n\nwith pd.ExcelWriter(\n    \"orders.xlsx\",\n    engine=\"xlsxwriter\",\n    date_format=\"yyyy-mm-dd\",\n    datetime_format=\"yyyy-mm-dd hh:mm\",\n) as writer:\n    df.to_excel(writer, sheet_name=\"Orders\", index=False)\n",[14,1443,1444,1454,1458,1467,1484,1503,1520,1525,1529,1537,1545,1557,1569,1581,1591],{"__ignoreMap":196},[200,1445,1446,1448,1450,1452],{"class":202,"line":203},[200,1447,207],{"class":206},[200,1449,211],{"class":210},[200,1451,214],{"class":206},[200,1453,217],{"class":210},[200,1455,1456],{"class":202,"line":220},[200,1457,224],{"emptyLinePlaceholder":223},[200,1459,1460,1462,1464],{"class":202,"line":227},[200,1461,757],{"class":210},[200,1463,233],{"class":206},[200,1465,1466],{"class":210}," pd.DataFrame({\n",[200,1468,1469,1472,1475,1477,1479,1481],{"class":202,"line":256},[200,1470,1471],{"class":278},"    \"order\"",[200,1473,1474],{"class":210},": [",[200,1476,1104],{"class":239},[200,1478,242],{"class":210},[200,1480,1173],{"class":239},[200,1482,1483],{"class":210},"],\n",[200,1485,1486,1489,1492,1495,1497,1500],{"class":202,"line":261},[200,1487,1488],{"class":278},"    \"placed\"",[200,1490,1491],{"class":210},": pd.to_datetime([",[200,1493,1494],{"class":278},"\"2026-08-15\"",[200,1496,242],{"class":210},[200,1498,1499],{"class":278},"\"2026-08-16\"",[200,1501,1502],{"class":210},"]),\n",[200,1504,1505,1508,1510,1513,1515,1518],{"class":202,"line":295},[200,1506,1507],{"class":278},"    \"processed_at\"",[200,1509,1491],{"class":210},[200,1511,1512],{"class":278},"\"2026-08-15 18:04\"",[200,1514,242],{"class":210},[200,1516,1517],{"class":278},"\"2026-08-16 09:12\"",[200,1519,1502],{"class":210},[200,1521,1522],{"class":202,"line":304},[200,1523,1524],{"class":210},"})\n",[200,1526,1527],{"class":202,"line":311},[200,1528,224],{"emptyLinePlaceholder":223},[200,1530,1531,1534],{"class":202,"line":317},[200,1532,1533],{"class":206},"with",[200,1535,1536],{"class":210}," pd.ExcelWriter(\n",[200,1538,1539,1542],{"class":202,"line":435},[200,1540,1541],{"class":278},"    \"orders.xlsx\"",[200,1543,1544],{"class":210},",\n",[200,1546,1547,1550,1552,1555],{"class":202,"line":460},[200,1548,1549],{"class":272},"    engine",[200,1551,233],{"class":206},[200,1553,1554],{"class":278},"\"xlsxwriter\"",[200,1556,1544],{"class":210},[200,1558,1559,1562,1564,1567],{"class":202,"line":1202},[200,1560,1561],{"class":272},"    date_format",[200,1563,233],{"class":206},[200,1565,1566],{"class":278},"\"yyyy-mm-dd\"",[200,1568,1544],{"class":210},[200,1570,1571,1574,1576,1579],{"class":202,"line":1232},[200,1572,1573],{"class":272},"    datetime_format",[200,1575,233],{"class":206},[200,1577,1578],{"class":278},"\"yyyy-mm-dd hh:mm\"",[200,1580,1544],{"class":210},[200,1582,1583,1586,1588],{"class":202,"line":1237},[200,1584,1585],{"class":210},") ",[200,1587,214],{"class":206},[200,1589,1590],{"class":210}," writer:\n",[200,1592,1593,1596,1599,1601,1604,1606,1609,1611,1614],{"class":202,"line":1243},[200,1594,1595],{"class":210},"    df.to_excel(writer, ",[200,1597,1598],{"class":272},"sheet_name",[200,1600,233],{"class":206},[200,1602,1603],{"class":278},"\"Orders\"",[200,1605,242],{"class":210},[200,1607,1608],{"class":272},"index",[200,1610,233],{"class":206},[200,1612,1613],{"class":239},"False",[200,1615,292],{"class":210},[10,1617,1618,1619,1622,1623,27],{},"The full vocabulary of Excel format strings — and the difference between ",[14,1620,1621],{},"mm"," meaning months and minutes depending on context — is covered in ",[23,1624,1626],{"href":1625},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002Fformat-dates-in-excel-cells-with-python\u002F","formatting dates in Excel cells with Python",[151,1628,1630],{"id":1629},"timezones-convert-then-strip","Timezones: convert, then strip",[10,1632,1633],{},"Excel has no concept of a timezone. A cell holds a day count, and that is all. openpyxl enforces this by raising if you hand it an aware datetime, which is better than the alternative of silently discarding the offset.",[191,1635,1637],{"className":193,"code":1636,"language":195,"meta":196,"style":196},"from datetime import datetime, timezone\nfrom openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\naware = datetime(2026, 8, 15, 18, 0, tzinfo=timezone.utc)\n\nws[\"A1\"] = aware\n# ValueError: Excel does not support timezones in datetimes.\n",[14,1638,1639,1650,1660,1664,1672,1680,1717,1721,1736],{"__ignoreMap":196},[200,1640,1641,1643,1645,1647],{"class":202,"line":203},[200,1642,333],{"class":206},[200,1644,336],{"class":210},[200,1646,207],{"class":206},[200,1648,1649],{"class":210}," datetime, timezone\n",[200,1651,1652,1654,1656,1658],{"class":202,"line":220},[200,1653,333],{"class":206},[200,1655,1029],{"class":210},[200,1657,207],{"class":206},[200,1659,1034],{"class":210},[200,1661,1662],{"class":202,"line":227},[200,1663,224],{"emptyLinePlaceholder":223},[200,1665,1666,1668,1670],{"class":202,"line":256},[200,1667,1043],{"class":210},[200,1669,233],{"class":206},[200,1671,1048],{"class":210},[200,1673,1674,1676,1678],{"class":202,"line":261},[200,1675,1053],{"class":210},[200,1677,233],{"class":206},[200,1679,1058],{"class":210},[200,1681,1682,1685,1687,1689,1691,1693,1695,1697,1699,1701,1703,1705,1707,1709,1712,1714],{"class":202,"line":295},[200,1683,1684],{"class":210},"aware ",[200,1686,233],{"class":206},[200,1688,356],{"class":210},[200,1690,467],{"class":239},[200,1692,242],{"class":210},[200,1694,472],{"class":239},[200,1696,242],{"class":210},[200,1698,477],{"class":239},[200,1700,242],{"class":210},[200,1702,482],{"class":239},[200,1704,242],{"class":210},[200,1706,51],{"class":239},[200,1708,242],{"class":210},[200,1710,1711],{"class":272},"tzinfo",[200,1713,233],{"class":206},[200,1715,1716],{"class":210},"timezone.utc)\n",[200,1718,1719],{"class":202,"line":304},[200,1720,224],{"emptyLinePlaceholder":223},[200,1722,1723,1726,1729,1731,1733],{"class":202,"line":311},[200,1724,1725],{"class":210},"ws[",[200,1727,1728],{"class":278},"\"A1\"",[200,1730,797],{"class":210},[200,1732,233],{"class":206},[200,1734,1735],{"class":210}," aware\n",[200,1737,1738],{"class":202,"line":317},[200,1739,1740],{"class":307},"# ValueError: Excel does not support timezones in datetimes.\n",[10,1742,1743],{},"The working pattern is three steps: convert everything to one agreed zone, drop the tzinfo, and record the zone somewhere a reader will see.",[191,1745,1747],{"className":193,"code":1746,"language":195,"meta":196,"style":196},"import pandas as pd\n\nevents = pd.DataFrame({\n    \"event\": [\"login\", \"export\", \"logout\"],\n    \"at\": pd.to_datetime([\n        \"2026-08-15T18:04:00+00:00\",\n        \"2026-08-15T19:30:00+00:00\",\n        \"2026-08-15T21:15:00+00:00\",\n    ], utc=True),\n})\n\n# 1. Convert to the zone the readers think in.\nevents[\"at\"] = events[\"at\"].dt.tz_convert(\"Europe\u002FBerlin\")\n\n# 2. Drop the offset — Excel cannot hold it.\nevents[\"at\"] = events[\"at\"].dt.tz_localize(None)\n\n# 3. Say so, in the sheet.\nwith pd.ExcelWriter(\"events.xlsx\", engine=\"xlsxwriter\",\n                    datetime_format=\"yyyy-mm-dd hh:mm\") as writer:\n    events.to_excel(writer, sheet_name=\"Events\", index=False, startrow=1)\n    writer.sheets[\"Events\"].write(0, 0, \"All times Europe\u002FBerlin (CEST)\")\n",[14,1748,1749,1759,1763,1772,1794,1802,1809,1816,1823,1837,1841,1845,1850,1875,1879,1884,1906,1910,1915,1936,1951,1982],{"__ignoreMap":196},[200,1750,1751,1753,1755,1757],{"class":202,"line":203},[200,1752,207],{"class":206},[200,1754,211],{"class":210},[200,1756,214],{"class":206},[200,1758,217],{"class":210},[200,1760,1761],{"class":202,"line":220},[200,1762,224],{"emptyLinePlaceholder":223},[200,1764,1765,1768,1770],{"class":202,"line":227},[200,1766,1767],{"class":210},"events ",[200,1769,233],{"class":206},[200,1771,1466],{"class":210},[200,1773,1774,1777,1779,1782,1784,1787,1789,1792],{"class":202,"line":256},[200,1775,1776],{"class":278},"    \"event\"",[200,1778,1474],{"class":210},[200,1780,1781],{"class":278},"\"login\"",[200,1783,242],{"class":210},[200,1785,1786],{"class":278},"\"export\"",[200,1788,242],{"class":210},[200,1790,1791],{"class":278},"\"logout\"",[200,1793,1483],{"class":210},[200,1795,1796,1799],{"class":202,"line":261},[200,1797,1798],{"class":278},"    \"at\"",[200,1800,1801],{"class":210},": pd.to_datetime([\n",[200,1803,1804,1807],{"class":202,"line":295},[200,1805,1806],{"class":278},"        \"2026-08-15T18:04:00+00:00\"",[200,1808,1544],{"class":210},[200,1810,1811,1814],{"class":202,"line":304},[200,1812,1813],{"class":278},"        \"2026-08-15T19:30:00+00:00\"",[200,1815,1544],{"class":210},[200,1817,1818,1821],{"class":202,"line":311},[200,1819,1820],{"class":278},"        \"2026-08-15T21:15:00+00:00\"",[200,1822,1544],{"class":210},[200,1824,1825,1828,1831,1833,1835],{"class":202,"line":317},[200,1826,1827],{"class":210},"    ], ",[200,1829,1830],{"class":272},"utc",[200,1832,233],{"class":206},[200,1834,940],{"class":239},[200,1836,1134],{"class":210},[200,1838,1839],{"class":202,"line":435},[200,1840,1524],{"class":210},[200,1842,1843],{"class":202,"line":460},[200,1844,224],{"emptyLinePlaceholder":223},[200,1846,1847],{"class":202,"line":1202},[200,1848,1849],{"class":307},"# 1. Convert to the zone the readers think in.\n",[200,1851,1852,1855,1858,1860,1862,1865,1867,1870,1873],{"class":202,"line":1232},[200,1853,1854],{"class":210},"events[",[200,1856,1857],{"class":278},"\"at\"",[200,1859,797],{"class":210},[200,1861,233],{"class":206},[200,1863,1864],{"class":210}," events[",[200,1866,1857],{"class":278},[200,1868,1869],{"class":210},"].dt.tz_convert(",[200,1871,1872],{"class":278},"\"Europe\u002FBerlin\"",[200,1874,292],{"class":210},[200,1876,1877],{"class":202,"line":1237},[200,1878,224],{"emptyLinePlaceholder":223},[200,1880,1881],{"class":202,"line":1243},[200,1882,1883],{"class":307},"# 2. Drop the offset — Excel cannot hold it.\n",[200,1885,1886,1888,1890,1892,1894,1896,1898,1901,1904],{"class":202,"line":1288},[200,1887,1854],{"class":210},[200,1889,1857],{"class":278},[200,1891,797],{"class":210},[200,1893,233],{"class":206},[200,1895,1864],{"class":210},[200,1897,1857],{"class":278},[200,1899,1900],{"class":210},"].dt.tz_localize(",[200,1902,1903],{"class":239},"None",[200,1905,292],{"class":210},[200,1907,1908],{"class":202,"line":1302},[200,1909,224],{"emptyLinePlaceholder":223},[200,1911,1912],{"class":202,"line":1313},[200,1913,1914],{"class":307},"# 3. Say so, in the sheet.\n",[200,1916,1917,1919,1922,1925,1927,1930,1932,1934],{"class":202,"line":1318},[200,1918,1533],{"class":206},[200,1920,1921],{"class":210}," pd.ExcelWriter(",[200,1923,1924],{"class":278},"\"events.xlsx\"",[200,1926,242],{"class":210},[200,1928,1929],{"class":272},"engine",[200,1931,233],{"class":206},[200,1933,1554],{"class":278},[200,1935,1544],{"class":210},[200,1937,1938,1941,1943,1945,1947,1949],{"class":202,"line":1353},[200,1939,1940],{"class":272},"                    datetime_format",[200,1942,233],{"class":206},[200,1944,1578],{"class":278},[200,1946,1585],{"class":210},[200,1948,214],{"class":206},[200,1950,1590],{"class":210},[200,1952,1953,1956,1958,1960,1963,1965,1967,1969,1971,1973,1976,1978,1980],{"class":202,"line":1364},[200,1954,1955],{"class":210},"    events.to_excel(writer, ",[200,1957,1598],{"class":272},[200,1959,233],{"class":206},[200,1961,1962],{"class":278},"\"Events\"",[200,1964,242],{"class":210},[200,1966,1608],{"class":272},[200,1968,233],{"class":206},[200,1970,1613],{"class":239},[200,1972,242],{"class":210},[200,1974,1975],{"class":272},"startrow",[200,1977,233],{"class":206},[200,1979,160],{"class":239},[200,1981,292],{"class":210},[200,1983,1984,1987,1989,1992,1994,1996,1998,2000,2003],{"class":202,"line":1374},[200,1985,1986],{"class":210},"    writer.sheets[",[200,1988,1962],{"class":278},[200,1990,1991],{"class":210},"].write(",[200,1993,51],{"class":239},[200,1995,242],{"class":210},[200,1997,51],{"class":239},[200,1999,242],{"class":210},[200,2001,2002],{"class":278},"\"All times Europe\u002FBerlin (CEST)\"",[200,2004,292],{"class":210},[10,2006,2007,2008,27],{},"Skipping step three is how a report becomes unreproducible six months later, when nobody remembers whether the timestamps were UTC or local. The dedicated walkthrough is ",[23,2009,2011],{"href":2010},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python\u002F","handling timezones in Excel timestamps with Python",[151,2013,2015],{"id":2014},"durations-and-why-they-are-not-dates","Durations, and why they are not dates",[10,2017,2018,2019,2022,2023,2026],{},"A duration in Excel is also a day count — ",[14,2020,2021],{},"0.5"," is twelve hours — displayed with a bracketed format like ",[14,2024,2025],{},"[h]:mm",". The brackets matter: without them, a 30-hour duration displays as 6 hours, because the format wraps at 24.",[191,2028,2030],{"className":193,"code":2029,"language":195,"meta":196,"style":196},"import pandas as pd\n\nsessions = pd.DataFrame({\n    \"user\": [\"alice\", \"bob\", \"carol\"],\n    \"start\": pd.to_datetime([\"2026-08-15 08:00\", \"2026-08-15 09:30\",\n                             \"2026-08-14 22:00\"]),\n    \"end\": pd.to_datetime([\"2026-08-15 17:15\", \"2026-08-15 12:00\",\n                           \"2026-08-16 06:30\"]),\n})\n\nsessions[\"duration\"] = sessions[\"end\"] - sessions[\"start\"]\n\n# Excel wants a fraction of a day, not a Timedelta.\nsessions[\"duration_days\"] = sessions[\"duration\"] \u002F pd.Timedelta(days=1)\n\nwith pd.ExcelWriter(\"sessions.xlsx\", engine=\"xlsxwriter\") as writer:\n    out = sessions[[\"user\", \"start\", \"end\", \"duration_days\"]]\n    out.to_excel(writer, sheet_name=\"Sessions\", index=False)\n\n    book, sheet = writer.book, writer.sheets[\"Sessions\"]\n    # [h] does not wrap at 24 — essential for the 32-hour row.\n    elapsed = book.add_format({\"num_format\": \"[h]:mm\"})\n    sheet.set_column(\"D:D\", 12, elapsed)\n    sheet.set_column(\"B:C\", 18,\n                     book.add_format({\"num_format\": \"yyyy-mm-dd hh:mm\"}))\n",[14,2031,2032,2042,2046,2055,2077,2094,2101,2118,2125,2129,2133,2163,2167,2172,2203,2207,2230,2258,2280,2284,2298,2303,2324,2339,2352],{"__ignoreMap":196},[200,2033,2034,2036,2038,2040],{"class":202,"line":203},[200,2035,207],{"class":206},[200,2037,211],{"class":210},[200,2039,214],{"class":206},[200,2041,217],{"class":210},[200,2043,2044],{"class":202,"line":220},[200,2045,224],{"emptyLinePlaceholder":223},[200,2047,2048,2051,2053],{"class":202,"line":227},[200,2049,2050],{"class":210},"sessions ",[200,2052,233],{"class":206},[200,2054,1466],{"class":210},[200,2056,2057,2060,2062,2065,2067,2070,2072,2075],{"class":202,"line":256},[200,2058,2059],{"class":278},"    \"user\"",[200,2061,1474],{"class":210},[200,2063,2064],{"class":278},"\"alice\"",[200,2066,242],{"class":210},[200,2068,2069],{"class":278},"\"bob\"",[200,2071,242],{"class":210},[200,2073,2074],{"class":278},"\"carol\"",[200,2076,1483],{"class":210},[200,2078,2079,2082,2084,2087,2089,2092],{"class":202,"line":261},[200,2080,2081],{"class":278},"    \"start\"",[200,2083,1491],{"class":210},[200,2085,2086],{"class":278},"\"2026-08-15 08:00\"",[200,2088,242],{"class":210},[200,2090,2091],{"class":278},"\"2026-08-15 09:30\"",[200,2093,1544],{"class":210},[200,2095,2096,2099],{"class":202,"line":295},[200,2097,2098],{"class":278},"                             \"2026-08-14 22:00\"",[200,2100,1502],{"class":210},[200,2102,2103,2106,2108,2111,2113,2116],{"class":202,"line":304},[200,2104,2105],{"class":278},"    \"end\"",[200,2107,1491],{"class":210},[200,2109,2110],{"class":278},"\"2026-08-15 17:15\"",[200,2112,242],{"class":210},[200,2114,2115],{"class":278},"\"2026-08-15 12:00\"",[200,2117,1544],{"class":210},[200,2119,2120,2123],{"class":202,"line":311},[200,2121,2122],{"class":278},"                           \"2026-08-16 06:30\"",[200,2124,1502],{"class":210},[200,2126,2127],{"class":202,"line":317},[200,2128,1524],{"class":210},[200,2130,2131],{"class":202,"line":435},[200,2132,224],{"emptyLinePlaceholder":223},[200,2134,2135,2138,2141,2143,2145,2148,2151,2153,2155,2157,2160],{"class":202,"line":460},[200,2136,2137],{"class":210},"sessions[",[200,2139,2140],{"class":278},"\"duration\"",[200,2142,797],{"class":210},[200,2144,233],{"class":206},[200,2146,2147],{"class":210}," sessions[",[200,2149,2150],{"class":278},"\"end\"",[200,2152,797],{"class":210},[200,2154,405],{"class":206},[200,2156,2147],{"class":210},[200,2158,2159],{"class":278},"\"start\"",[200,2161,2162],{"class":210},"]\n",[200,2164,2165],{"class":202,"line":1202},[200,2166,224],{"emptyLinePlaceholder":223},[200,2168,2169],{"class":202,"line":1232},[200,2170,2171],{"class":307},"# Excel wants a fraction of a day, not a Timedelta.\n",[200,2173,2174,2176,2179,2181,2183,2185,2187,2189,2191,2194,2197,2199,2201],{"class":202,"line":1237},[200,2175,2137],{"class":210},[200,2177,2178],{"class":278},"\"duration_days\"",[200,2180,797],{"class":210},[200,2182,233],{"class":206},[200,2184,2147],{"class":210},[200,2186,2140],{"class":278},[200,2188,797],{"class":210},[200,2190,425],{"class":206},[200,2192,2193],{"class":210}," pd.Timedelta(",[200,2195,2196],{"class":272},"days",[200,2198,233],{"class":206},[200,2200,160],{"class":239},[200,2202,292],{"class":210},[200,2204,2205],{"class":202,"line":1243},[200,2206,224],{"emptyLinePlaceholder":223},[200,2208,2209,2211,2213,2216,2218,2220,2222,2224,2226,2228],{"class":202,"line":1288},[200,2210,1533],{"class":206},[200,2212,1921],{"class":210},[200,2214,2215],{"class":278},"\"sessions.xlsx\"",[200,2217,242],{"class":210},[200,2219,1929],{"class":272},[200,2221,233],{"class":206},[200,2223,1554],{"class":278},[200,2225,1585],{"class":210},[200,2227,214],{"class":206},[200,2229,1590],{"class":210},[200,2231,2232,2235,2237,2240,2243,2245,2247,2249,2251,2253,2255],{"class":202,"line":1302},[200,2233,2234],{"class":210},"    out ",[200,2236,233],{"class":206},[200,2238,2239],{"class":210}," sessions[[",[200,2241,2242],{"class":278},"\"user\"",[200,2244,242],{"class":210},[200,2246,2159],{"class":278},[200,2248,242],{"class":210},[200,2250,2150],{"class":278},[200,2252,242],{"class":210},[200,2254,2178],{"class":278},[200,2256,2257],{"class":210},"]]\n",[200,2259,2260,2263,2265,2267,2270,2272,2274,2276,2278],{"class":202,"line":1313},[200,2261,2262],{"class":210},"    out.to_excel(writer, ",[200,2264,1598],{"class":272},[200,2266,233],{"class":206},[200,2268,2269],{"class":278},"\"Sessions\"",[200,2271,242],{"class":210},[200,2273,1608],{"class":272},[200,2275,233],{"class":206},[200,2277,1613],{"class":239},[200,2279,292],{"class":210},[200,2281,2282],{"class":202,"line":1318},[200,2283,224],{"emptyLinePlaceholder":223},[200,2285,2286,2289,2291,2294,2296],{"class":202,"line":1353},[200,2287,2288],{"class":210},"    book, sheet ",[200,2290,233],{"class":206},[200,2292,2293],{"class":210}," writer.book, writer.sheets[",[200,2295,2269],{"class":278},[200,2297,2162],{"class":210},[200,2299,2300],{"class":202,"line":1364},[200,2301,2302],{"class":307},"    # [h] does not wrap at 24 — essential for the 32-hour row.\n",[200,2304,2305,2308,2310,2313,2316,2319,2322],{"class":202,"line":1374},[200,2306,2307],{"class":210},"    elapsed ",[200,2309,233],{"class":206},[200,2311,2312],{"class":210}," book.add_format({",[200,2314,2315],{"class":278},"\"num_format\"",[200,2317,2318],{"class":210},": ",[200,2320,2321],{"class":278},"\"[h]:mm\"",[200,2323,1524],{"class":210},[200,2325,2326,2329,2332,2334,2336],{"class":202,"line":1379},[200,2327,2328],{"class":210},"    sheet.set_column(",[200,2330,2331],{"class":278},"\"D:D\"",[200,2333,242],{"class":210},[200,2335,364],{"class":239},[200,2337,2338],{"class":210},", elapsed)\n",[200,2340,2341,2343,2346,2348,2350],{"class":202,"line":1396},[200,2342,2328],{"class":210},[200,2344,2345],{"class":278},"\"B:C\"",[200,2347,242],{"class":210},[200,2349,482],{"class":239},[200,2351,1544],{"class":210},[200,2353,2354,2357,2359,2361,2363],{"class":202,"line":1410},[200,2355,2356],{"class":210},"                     book.add_format({",[200,2358,2315],{"class":278},[200,2360,2318],{"class":210},[200,2362,1578],{"class":278},[200,2364,2365],{"class":210},"}))\n",[10,2367,2368,2369,2372],{},"Writing a ",[14,2370,2371],{},"Timedelta"," straight to a cell is the common mistake — it lands as text, and no format string will make Excel sum it.",[151,2374,2376],{"id":2375},"grouping-by-month-quarter-and-week","Grouping by month, quarter and week",[10,2378,2379],{},"Almost every recurring report aggregates by period, and pandas has vectorised tools that make row loops unnecessary.",[29,2381,39,2387,39,2390,39,2393,39,2396,39,2399,39,2405,39,2409,39,2414,39,2418,39,2422,39,2427,39,2430,39,2435,39,2438,39,2441,39,2444,39,2447,39,2451,39,2454,39,2459,39,2462,39,2465,39,2468,39,2471],{"viewBox":2382,"role":32,"ariaLabel":2383,"ariaLabelledBy":2384,"xmlns":37,"style":581},"0 0 800 218","Three period grouping approaches: to_period for a compact label, Grouper with a frequency for a real timestamp index, and resample for filling gaps in a continuous series.",[2385,2386],"period-t","period-d",[41,2388,2389],{"id":2385},"Three ways to group a date column into periods",[45,2391,2392],{"id":2386},"Three side-by-side options for a daily date column. dt.to_period M yields a compact label like 2026-08 and is best for a display column. Grouper with frequency month-start yields a real timestamp index and is best for sorting and joining. resample fills gaps so months with no rows still appear with a zero, which matters for charts and trend lines.",[49,2394],{"x":51,"y":51,"width":590,"height":2395,"fill":54},"218",[49,2397],{"x":61,"y":58,"width":2398,"height":121,"rx":61,"fill":62,"stroke":63,"style":64},"248",[66,2400,2404],{"x":2401,"y":2402,"style":2403},"138","48","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","dt.to_period(\"M\")",[66,2406,2408],{"x":2401,"y":2407,"style":127},"78","gives a label: 2026-08",[66,2410,2413],{"x":2401,"y":2411,"style":2412},"102","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","compact and readable",[66,2415,2417],{"x":2401,"y":2416,"style":2412},"126","not a timestamp",[49,2419],{"x":601,"y":2420,"width":126,"height":2421,"rx":1219,"fill":54,"stroke":63},"146","38",[66,2423,2426],{"x":2401,"y":2424,"style":2425},"170","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","use for a display column",[49,2428],{"x":2429,"y":58,"width":2398,"height":121,"rx":61,"fill":116,"stroke":117,"style":64},"276",[66,2431,2434],{"x":2432,"y":2402,"style":2433},"400","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Grouper(freq=\"MS\")",[66,2436,2437],{"x":2432,"y":2407,"style":127},"gives 2026-08-01",[66,2439,2440],{"x":2432,"y":2411,"style":2412},"a real timestamp",[66,2442,2443],{"x":2432,"y":2416,"style":2412},"sorts and joins correctly",[49,2445],{"x":2446,"y":2420,"width":126,"height":2421,"rx":1219,"fill":54,"stroke":117},"298",[66,2448,2450],{"x":2432,"y":2424,"style":2449},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","use for aggregation keys",[49,2452],{"x":2453,"y":58,"width":2398,"height":121,"rx":61,"fill":137,"stroke":138,"style":64},"538",[66,2455,2458],{"x":2456,"y":2402,"style":2457},"662","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","resample(\"MS\")",[66,2460,2461],{"x":2456,"y":2407,"style":127},"fills empty months",[66,2463,2464],{"x":2456,"y":2411,"style":2412},"zero rows still appear",[66,2466,2467],{"x":2456,"y":2416,"style":2412},"needs a datetime index",[49,2469],{"x":2470,"y":2420,"width":126,"height":2421,"rx":1219,"fill":54,"stroke":138},"560",[66,2472,2474],{"x":2456,"y":2424,"style":2473},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","use for charts and trends",[191,2476,2478],{"className":193,"code":2477,"language":195,"meta":196,"style":196},"import pandas as pd\n\ndf = pd.read_excel(\"sales.xlsx\")\ndf[\"date\"] = pd.to_datetime(df[\"date\"], errors=\"coerce\")\ndf = df.dropna(subset=[\"date\"])\n\n# A readable label for a report column.\ndf[\"month\"] = df[\"date\"].dt.to_period(\"M\").astype(str)      # \"2026-08\"\ndf[\"quarter\"] = df[\"date\"].dt.to_period(\"Q\").astype(str)    # \"2026Q3\"\n\n# A real timestamp key for aggregation — sorts and joins properly.\nmonthly = (\n    df.groupby(pd.Grouper(key=\"date\", freq=\"MS\"))[\"amount\"]\n      .agg([\"sum\", \"count\"])\n      .rename(columns={\"sum\": \"revenue\", \"count\": \"orders\"})\n)\n\n# Fill months with no sales so a chart shows the gap rather than skipping it.\nmonthly = monthly.asfreq(\"MS\", fill_value=0)\nprint(monthly)\n",[14,2479,2480,2490,2494,2507,2534,2555,2559,2564,2597,2627,2631,2636,2646,2676,2691,2721,2725,2729,2734,2756],{"__ignoreMap":196},[200,2481,2482,2484,2486,2488],{"class":202,"line":203},[200,2483,207],{"class":206},[200,2485,211],{"class":210},[200,2487,214],{"class":206},[200,2489,217],{"class":210},[200,2491,2492],{"class":202,"line":220},[200,2493,224],{"emptyLinePlaceholder":223},[200,2495,2496,2498,2500,2502,2505],{"class":202,"line":227},[200,2497,757],{"class":210},[200,2499,233],{"class":206},[200,2501,762],{"class":210},[200,2503,2504],{"class":278},"\"sales.xlsx\"",[200,2506,292],{"class":210},[200,2508,2509,2511,2514,2516,2518,2521,2523,2526,2528,2530,2532],{"class":202,"line":256},[200,2510,792],{"class":210},[200,2512,2513],{"class":278},"\"date\"",[200,2515,797],{"class":210},[200,2517,233],{"class":206},[200,2519,2520],{"class":210}," pd.to_datetime(df[",[200,2522,2513],{"class":278},[200,2524,2525],{"class":210},"], ",[200,2527,805],{"class":272},[200,2529,233],{"class":206},[200,2531,810],{"class":278},[200,2533,292],{"class":210},[200,2535,2536,2538,2540,2543,2546,2548,2551,2553],{"class":202,"line":261},[200,2537,757],{"class":210},[200,2539,233],{"class":206},[200,2541,2542],{"class":210}," df.dropna(",[200,2544,2545],{"class":272},"subset",[200,2547,233],{"class":206},[200,2549,2550],{"class":210},"[",[200,2552,2513],{"class":278},[200,2554,253],{"class":210},[200,2556,2557],{"class":202,"line":295},[200,2558,224],{"emptyLinePlaceholder":223},[200,2560,2561],{"class":202,"line":304},[200,2562,2563],{"class":307},"# A readable label for a report column.\n",[200,2565,2566,2568,2571,2573,2575,2577,2579,2582,2585,2588,2591,2594],{"class":202,"line":311},[200,2567,792],{"class":210},[200,2569,2570],{"class":278},"\"month\"",[200,2572,797],{"class":210},[200,2574,233],{"class":206},[200,2576,781],{"class":210},[200,2578,2513],{"class":278},[200,2580,2581],{"class":210},"].dt.to_period(",[200,2583,2584],{"class":278},"\"M\"",[200,2586,2587],{"class":210},").astype(",[200,2589,2590],{"class":239},"str",[200,2592,2593],{"class":210},")      ",[200,2595,2596],{"class":307},"# \"2026-08\"\n",[200,2598,2599,2601,2604,2606,2608,2610,2612,2614,2617,2619,2621,2624],{"class":202,"line":317},[200,2600,792],{"class":210},[200,2602,2603],{"class":278},"\"quarter\"",[200,2605,797],{"class":210},[200,2607,233],{"class":206},[200,2609,781],{"class":210},[200,2611,2513],{"class":278},[200,2613,2581],{"class":210},[200,2615,2616],{"class":278},"\"Q\"",[200,2618,2587],{"class":210},[200,2620,2590],{"class":239},[200,2622,2623],{"class":210},")    ",[200,2625,2626],{"class":307},"# \"2026Q3\"\n",[200,2628,2629],{"class":202,"line":435},[200,2630,224],{"emptyLinePlaceholder":223},[200,2632,2633],{"class":202,"line":460},[200,2634,2635],{"class":307},"# A real timestamp key for aggregation — sorts and joins properly.\n",[200,2637,2638,2641,2643],{"class":202,"line":1202},[200,2639,2640],{"class":210},"monthly ",[200,2642,233],{"class":206},[200,2644,2645],{"class":210}," (\n",[200,2647,2648,2651,2654,2656,2658,2660,2663,2665,2668,2671,2674],{"class":202,"line":1232},[200,2649,2650],{"class":210},"    df.groupby(pd.Grouper(",[200,2652,2653],{"class":272},"key",[200,2655,233],{"class":206},[200,2657,2513],{"class":278},[200,2659,242],{"class":210},[200,2661,2662],{"class":272},"freq",[200,2664,233],{"class":206},[200,2666,2667],{"class":278},"\"MS\"",[200,2669,2670],{"class":210},"))[",[200,2672,2673],{"class":278},"\"amount\"",[200,2675,2162],{"class":210},[200,2677,2678,2681,2684,2686,2689],{"class":202,"line":1237},[200,2679,2680],{"class":210},"      .agg([",[200,2682,2683],{"class":278},"\"sum\"",[200,2685,242],{"class":210},[200,2687,2688],{"class":278},"\"count\"",[200,2690,253],{"class":210},[200,2692,2693,2696,2699,2701,2703,2705,2707,2710,2712,2714,2716,2719],{"class":202,"line":1243},[200,2694,2695],{"class":210},"      .rename(",[200,2697,2698],{"class":272},"columns",[200,2700,233],{"class":206},[200,2702,876],{"class":210},[200,2704,2683],{"class":278},[200,2706,2318],{"class":210},[200,2708,2709],{"class":278},"\"revenue\"",[200,2711,242],{"class":210},[200,2713,2688],{"class":278},[200,2715,2318],{"class":210},[200,2717,2718],{"class":278},"\"orders\"",[200,2720,1524],{"class":210},[200,2722,2723],{"class":202,"line":1288},[200,2724,292],{"class":210},[200,2726,2727],{"class":202,"line":1302},[200,2728,224],{"emptyLinePlaceholder":223},[200,2730,2731],{"class":202,"line":1313},[200,2732,2733],{"class":307},"# Fill months with no sales so a chart shows the gap rather than skipping it.\n",[200,2735,2736,2738,2740,2743,2745,2747,2750,2752,2754],{"class":202,"line":1318},[200,2737,2640],{"class":210},[200,2739,233],{"class":206},[200,2741,2742],{"class":210}," monthly.asfreq(",[200,2744,2667],{"class":278},[200,2746,242],{"class":210},[200,2748,2749],{"class":272},"fill_value",[200,2751,233],{"class":206},[200,2753,51],{"class":239},[200,2755,292],{"class":210},[200,2757,2758,2760],{"class":202,"line":1353},[200,2759,298],{"class":239},[200,2761,2762],{"class":210},"(monthly)\n",[10,2764,2765,2766,2769,2770,2774,2775,27],{},"The ",[14,2767,2768],{},"asfreq"," line is the one people forget. Without it, a month with no rows simply does not appear, and a line chart drawn from that series connects across the gap as though nothing happened. The grouping recipes are expanded in ",[23,2771,2773],{"href":2772},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002F","grouping Excel rows by month and quarter with pandas",", and the aggregate output feeds naturally into ",[23,2776,2778],{"href":2777},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002F","creating pivot tables from Excel data",[10,2780,2781,2782,2785],{},"Fiscal years need one extra argument. A year ending in March is ",[14,2783,2784],{},"Q-MAR",":",[191,2787,2789],{"className":193,"code":2788,"language":195,"meta":196,"style":196},"# Fiscal year ending 31 March: April 2026 falls in FY2027 Q1.\ndf[\"fiscal_quarter\"] = df[\"date\"].dt.to_period(\"Q-MAR\").astype(str)\n",[14,2790,2791,2796],{"__ignoreMap":196},[200,2792,2793],{"class":202,"line":203},[200,2794,2795],{"class":307},"# Fiscal year ending 31 March: April 2026 falls in FY2027 Q1.\n",[200,2797,2798,2800,2803,2805,2807,2809,2811,2813,2816,2818,2820],{"class":202,"line":220},[200,2799,792],{"class":210},[200,2801,2802],{"class":278},"\"fiscal_quarter\"",[200,2804,797],{"class":210},[200,2806,233],{"class":206},[200,2808,781],{"class":210},[200,2810,2513],{"class":278},[200,2812,2581],{"class":210},[200,2814,2815],{"class":278},"\"Q-MAR\"",[200,2817,2587],{"class":210},[200,2819,2590],{"class":239},[200,2821,292],{"class":210},[151,2823,2825],{"id":2824},"date-only-time-only-and-the-cells-in-between","Date-only, time-only and the cells in between",[10,2827,2828],{},"Excel makes no type distinction between a date, a time, and a timestamp — all three are the same number with a different display format. Python does distinguish them, and the mismatch produces two recurring annoyances.",[10,2830,2831,2832,2835,2836,2839,2840,2842,2843,2846,2847,2850],{},"The first is the ",[186,2833,2834],{},"phantom midnight",". Write a ",[14,2837,2838],{},"datetime.date"," and read it back, and you get a ",[14,2841,16],{}," at 00:00:00. Nothing was lost; the time component was never there. But an equality test against a ",[14,2844,2845],{},"date"," object now fails, and a ",[14,2848,2849],{},"groupby"," on the column produces one group per timestamp rather than per day:",[191,2852,2854],{"className":193,"code":2853,"language":195,"meta":196,"style":196},"import pandas as pd\n\ndf = pd.read_excel(\"orders.xlsx\", parse_dates=[\"invoice_date\"])\n\n# Normalise to midnight so date comparisons and grouping behave.\ndf[\"invoice_day\"] = df[\"invoice_date\"].dt.normalize()\n\n# Or drop to a plain date object when you never need the time again.\ndf[\"invoice_date_only\"] = df[\"invoice_date\"].dt.date\n",[14,2855,2856,2866,2870,2893,2897,2902,2920,2924,2929],{"__ignoreMap":196},[200,2857,2858,2860,2862,2864],{"class":202,"line":203},[200,2859,207],{"class":206},[200,2861,211],{"class":210},[200,2863,214],{"class":206},[200,2865,217],{"class":210},[200,2867,2868],{"class":202,"line":220},[200,2869,224],{"emptyLinePlaceholder":223},[200,2871,2872,2874,2876,2878,2880,2882,2885,2887,2889,2891],{"class":202,"line":227},[200,2873,757],{"class":210},[200,2875,233],{"class":206},[200,2877,762],{"class":210},[200,2879,765],{"class":278},[200,2881,242],{"class":210},[200,2883,2884],{"class":272},"parse_dates",[200,2886,233],{"class":206},[200,2888,2550],{"class":210},[200,2890,784],{"class":278},[200,2892,253],{"class":210},[200,2894,2895],{"class":202,"line":256},[200,2896,224],{"emptyLinePlaceholder":223},[200,2898,2899],{"class":202,"line":261},[200,2900,2901],{"class":307},"# Normalise to midnight so date comparisons and grouping behave.\n",[200,2903,2904,2906,2909,2911,2913,2915,2917],{"class":202,"line":295},[200,2905,792],{"class":210},[200,2907,2908],{"class":278},"\"invoice_day\"",[200,2910,797],{"class":210},[200,2912,233],{"class":206},[200,2914,781],{"class":210},[200,2916,784],{"class":278},[200,2918,2919],{"class":210},"].dt.normalize()\n",[200,2921,2922],{"class":202,"line":304},[200,2923,224],{"emptyLinePlaceholder":223},[200,2925,2926],{"class":202,"line":311},[200,2927,2928],{"class":307},"# Or drop to a plain date object when you never need the time again.\n",[200,2930,2931,2933,2936,2938,2940,2942,2944],{"class":202,"line":317},[200,2932,792],{"class":210},[200,2934,2935],{"class":278},"\"invoice_date_only\"",[200,2937,797],{"class":210},[200,2939,233],{"class":206},[200,2941,781],{"class":210},[200,2943,784],{"class":278},[200,2945,2946],{"class":210},"].dt.date\n",[10,2948,2949,2952,2953,2956,2957,2960,2961,2964,2965,2967,2968,2971,2972,2975],{},[14,2950,2951],{},"dt.normalize()"," keeps the column as ",[14,2954,2955],{},"datetime64",", which is usually what you want — ",[14,2958,2959],{},"dt.date"," produces an ",[14,2962,2963],{},"object"," column of Python ",[14,2966,2845],{}," objects that is slower and loses the ",[14,2969,2970],{},".dt"," accessor. Reach for ",[14,2973,2974],{},".dt.date"," only at the very end, when writing a display column.",[10,2977,2978,2979,2982,2983,2985],{},"The second is the ",[186,2980,2981],{},"1899 time",". A cell holding only a time of day stores a value below 1 — ",[14,2984,2021],{}," for noon. Read that into pandas and you get a timestamp on 30 December 1899, because the day part is zero:",[191,2987,2989],{"className":193,"code":2988,"language":195,"meta":196,"style":196},"import pandas as pd\n\n# A \"shift start\" column that holds times only.\ntimes = pd.Series([0.25, 0.5, 0.75])\nas_ts = pd.to_datetime(times, unit=\"D\", origin=\"1899-12-30\")\nprint(as_ts.dt.time.tolist())\n# [datetime.time(6, 0), datetime.time(12, 0), datetime.time(18, 0)]\n\n# Usually more useful: keep them as durations from midnight.\nas_td = pd.to_timedelta(times, unit=\"D\")\nprint(as_td.tolist())\n# [Timedelta('0 days 06:00:00'), ..., Timedelta('0 days 18:00:00')]\n",[14,2990,2991,3001,3005,3010,3033,3059,3066,3071,3075,3080,3098,3105],{"__ignoreMap":196},[200,2992,2993,2995,2997,2999],{"class":202,"line":203},[200,2994,207],{"class":206},[200,2996,211],{"class":210},[200,2998,214],{"class":206},[200,3000,217],{"class":210},[200,3002,3003],{"class":202,"line":220},[200,3004,224],{"emptyLinePlaceholder":223},[200,3006,3007],{"class":202,"line":227},[200,3008,3009],{"class":307},"# A \"shift start\" column that holds times only.\n",[200,3011,3012,3015,3017,3019,3022,3024,3026,3028,3031],{"class":202,"line":256},[200,3013,3014],{"class":210},"times ",[200,3016,233],{"class":206},[200,3018,236],{"class":210},[200,3020,3021],{"class":239},"0.25",[200,3023,242],{"class":210},[200,3025,2021],{"class":239},[200,3027,242],{"class":210},[200,3029,3030],{"class":239},"0.75",[200,3032,253],{"class":210},[200,3034,3035,3038,3040,3043,3045,3047,3049,3051,3053,3055,3057],{"class":202,"line":261},[200,3036,3037],{"class":210},"as_ts ",[200,3039,233],{"class":206},[200,3041,3042],{"class":210}," pd.to_datetime(times, ",[200,3044,273],{"class":272},[200,3046,233],{"class":206},[200,3048,279],{"class":278},[200,3050,242],{"class":210},[200,3052,284],{"class":272},[200,3054,233],{"class":206},[200,3056,289],{"class":278},[200,3058,292],{"class":210},[200,3060,3061,3063],{"class":202,"line":295},[200,3062,298],{"class":239},[200,3064,3065],{"class":210},"(as_ts.dt.time.tolist())\n",[200,3067,3068],{"class":202,"line":304},[200,3069,3070],{"class":307},"# [datetime.time(6, 0), datetime.time(12, 0), datetime.time(18, 0)]\n",[200,3072,3073],{"class":202,"line":311},[200,3074,224],{"emptyLinePlaceholder":223},[200,3076,3077],{"class":202,"line":317},[200,3078,3079],{"class":307},"# Usually more useful: keep them as durations from midnight.\n",[200,3081,3082,3085,3087,3090,3092,3094,3096],{"class":202,"line":435},[200,3083,3084],{"class":210},"as_td ",[200,3086,233],{"class":206},[200,3088,3089],{"class":210}," pd.to_timedelta(times, ",[200,3091,273],{"class":272},[200,3093,233],{"class":206},[200,3095,279],{"class":278},[200,3097,292],{"class":210},[200,3099,3100,3102],{"class":202,"line":460},[200,3101,298],{"class":239},[200,3103,3104],{"class":210},"(as_td.tolist())\n",[200,3106,3107],{"class":202,"line":1202},[200,3108,3109],{"class":307},"# [Timedelta('0 days 06:00:00'), ..., Timedelta('0 days 18:00:00')]\n",[10,3111,3112,3113,3115,3116,3119],{},"Treating a time-of-day column as a ",[14,3114,2371],{}," rather than a ",[14,3117,3118],{},"Timestamp"," is the cleaner model: you can add it to a date, compare two of them, and sum them, none of which makes sense for a timestamp anchored in 1899.",[29,3121,39,3127,39,3130,39,3133,39,3136,39,3140,39,3142,39,3146,39,3149,39,3152,39,3156,39,3160,39,3162,39,3165,39,3168,39,3171,39,3174,39,3177,39,3180,39,3184,39,3187,39,3190,39,3193],{"viewBox":3122,"role":32,"ariaLabel":3123,"ariaLabelledBy":3124,"xmlns":37,"style":581},"0 0 800 226","One stored number interpreted three ways: a value below one is a time of day, a whole number is a date at midnight, and a number with a fraction is a full timestamp.",[3125,3126],"dtt-t","dtt-d",[41,3128,3129],{"id":3125},"Date, time and timestamp are the same number in different ranges",[45,3131,3132],{"id":3126},"Three bands on one number line. Values below one carry no day component and represent a time of day, which pandas will place on 30 December 1899 unless you convert to a Timedelta instead. Whole numbers are dates at midnight. Numbers with both an integer and a fractional part are full timestamps. Excel tells them apart only by the cell's number format.",[49,3134],{"x":51,"y":51,"width":590,"height":3135,"fill":54},"226",[66,3137,3139],{"x":2432,"y":595,"style":3138},"font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Excel has one storage type; the range tells you what it means",[49,3141],{"x":600,"y":69,"width":2398,"height":672,"rx":61,"fill":137,"stroke":138,"style":64},[66,3143,3145],{"x":3144,"y":74,"style":2457},"140","value \u003C 1",[66,3147,2021],{"x":3144,"y":707,"style":3148},"font-size:16px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[66,3150,3151],{"x":3144,"y":2416,"style":127},"a time of day: noon",[66,3153,3155],{"x":3144,"y":3154,"style":2412},"148","no day component at all",[66,3157,3159],{"x":3144,"y":3158,"style":2473},"176","use to_timedelta",[49,3161],{"x":2429,"y":69,"width":2398,"height":672,"rx":61,"fill":116,"stroke":117,"style":64},[66,3163,3164],{"x":2432,"y":74,"style":2433},"whole number",[66,3166,3167],{"x":2432,"y":707,"style":3148},"46249",[66,3169,3170],{"x":2432,"y":2416,"style":127},"a date at midnight",[66,3172,3173],{"x":2432,"y":3154,"style":2412},"2026-08-15 00:00:00",[66,3175,3176],{"x":2432,"y":3158,"style":2449},"use dt.normalize",[49,3178],{"x":3179,"y":69,"width":2398,"height":672,"rx":61,"fill":62,"stroke":63,"style":64},"536",[66,3181,3183],{"x":3182,"y":74,"style":2403},"660","both parts",[66,3185,3186],{"x":3182,"y":707,"style":3148},"46249.75",[66,3188,3189],{"x":3182,"y":2416,"style":127},"a full timestamp",[66,3191,3192],{"x":3182,"y":3154,"style":2412},"2026-08-15 18:00:00",[66,3194,3195],{"x":3182,"y":3158,"style":2425},"read as-is",[151,3197,3199],{"id":3198},"date-arithmetic-that-respects-the-business-calendar","Date arithmetic that respects the business calendar",[10,3201,3202,3203,3206],{},"Reports rarely want raw calendar arithmetic. \"Due in 30 days\" usually means 30 ",[175,3204,3205],{},"business"," days, and \"last month\" means the month that ended, not the last 30 days. pandas has offset objects for exactly this, and using them beats hand-rolled loops on both correctness and speed.",[191,3208,3210],{"className":193,"code":3209,"language":195,"meta":196,"style":196},"import pandas as pd\nfrom pandas.tseries.offsets import BDay, MonthEnd, MonthBegin\n\ndf = pd.DataFrame({\n    \"invoice_date\": pd.to_datetime([\"2026-08-14\", \"2026-08-28\", \"2026-07-31\"]),\n})\n\n# Business days skip weekends automatically.\ndf[\"due\"] = df[\"invoice_date\"] + BDay(10)\n\n# Month boundaries, without any day arithmetic.\ndf[\"period_start\"] = df[\"invoice_date\"] - MonthBegin(1)\ndf[\"period_end\"] = df[\"invoice_date\"] + MonthEnd(0)\n\nprint(df)\n",[14,3211,3212,3222,3234,3238,3246,3268,3272,3276,3281,3306,3310,3315,3341,3367,3371],{"__ignoreMap":196},[200,3213,3214,3216,3218,3220],{"class":202,"line":203},[200,3215,207],{"class":206},[200,3217,211],{"class":210},[200,3219,214],{"class":206},[200,3221,217],{"class":210},[200,3223,3224,3226,3229,3231],{"class":202,"line":220},[200,3225,333],{"class":206},[200,3227,3228],{"class":210}," pandas.tseries.offsets ",[200,3230,207],{"class":206},[200,3232,3233],{"class":210}," BDay, MonthEnd, MonthBegin\n",[200,3235,3236],{"class":202,"line":227},[200,3237,224],{"emptyLinePlaceholder":223},[200,3239,3240,3242,3244],{"class":202,"line":256},[200,3241,757],{"class":210},[200,3243,233],{"class":206},[200,3245,1466],{"class":210},[200,3247,3248,3251,3253,3256,3258,3261,3263,3266],{"class":202,"line":261},[200,3249,3250],{"class":278},"    \"invoice_date\"",[200,3252,1491],{"class":210},[200,3254,3255],{"class":278},"\"2026-08-14\"",[200,3257,242],{"class":210},[200,3259,3260],{"class":278},"\"2026-08-28\"",[200,3262,242],{"class":210},[200,3264,3265],{"class":278},"\"2026-07-31\"",[200,3267,1502],{"class":210},[200,3269,3270],{"class":202,"line":295},[200,3271,1524],{"class":210},[200,3273,3274],{"class":202,"line":304},[200,3275,224],{"emptyLinePlaceholder":223},[200,3277,3278],{"class":202,"line":311},[200,3279,3280],{"class":307},"# Business days skip weekends automatically.\n",[200,3282,3283,3285,3287,3289,3291,3293,3295,3297,3299,3302,3304],{"class":202,"line":317},[200,3284,792],{"class":210},[200,3286,1090],{"class":278},[200,3288,797],{"class":210},[200,3290,233],{"class":206},[200,3292,781],{"class":210},[200,3294,784],{"class":278},[200,3296,797],{"class":210},[200,3298,419],{"class":206},[200,3300,3301],{"class":210}," BDay(",[200,3303,900],{"class":239},[200,3305,292],{"class":210},[200,3307,3308],{"class":202,"line":435},[200,3309,224],{"emptyLinePlaceholder":223},[200,3311,3312],{"class":202,"line":460},[200,3313,3314],{"class":307},"# Month boundaries, without any day arithmetic.\n",[200,3316,3317,3319,3322,3324,3326,3328,3330,3332,3334,3337,3339],{"class":202,"line":1202},[200,3318,792],{"class":210},[200,3320,3321],{"class":278},"\"period_start\"",[200,3323,797],{"class":210},[200,3325,233],{"class":206},[200,3327,781],{"class":210},[200,3329,784],{"class":278},[200,3331,797],{"class":210},[200,3333,405],{"class":206},[200,3335,3336],{"class":210}," MonthBegin(",[200,3338,160],{"class":239},[200,3340,292],{"class":210},[200,3342,3343,3345,3348,3350,3352,3354,3356,3358,3360,3363,3365],{"class":202,"line":1232},[200,3344,792],{"class":210},[200,3346,3347],{"class":278},"\"period_end\"",[200,3349,797],{"class":210},[200,3351,233],{"class":206},[200,3353,781],{"class":210},[200,3355,784],{"class":278},[200,3357,797],{"class":210},[200,3359,419],{"class":206},[200,3361,3362],{"class":210}," MonthEnd(",[200,3364,51],{"class":239},[200,3366,292],{"class":210},[200,3368,3369],{"class":202,"line":1237},[200,3370,224],{"emptyLinePlaceholder":223},[200,3372,3373,3375],{"class":202,"line":1243},[200,3374,298],{"class":239},[200,3376,3377],{"class":210},"(df)\n",[10,3379,3380,3383,3384,3387],{},[14,3381,3382],{},"MonthEnd(0)"," is the subtle one: with an offset of zero it means \"roll forward to the end of the current month, or stay put if already there\", which is what a period-end column wants. ",[14,3385,3386],{},"MonthEnd(1)"," would push a date that is already month-end into the following month.",[10,3389,3390],{},"Public holidays need a calendar. pandas ships US federal holidays and lets you define your own, which is what most non-US teams end up doing:",[191,3392,3394],{"className":193,"code":3393,"language":195,"meta":196,"style":196},"import pandas as pd\nfrom pandas.tseries.holiday import AbstractHolidayCalendar, Holiday\nfrom pandas.tseries.offsets import CustomBusinessDay\n\nclass CompanyCalendar(AbstractHolidayCalendar):\n    rules = [\n        Holiday(\"New Year\", month=1, day=1),\n        Holiday(\"Company Day\", month=6, day=12),\n        Holiday(\"Christmas\", month=12, day=25),\n        Holiday(\"Boxing Day\", month=12, day=26),\n    ]\n\nworkday = CustomBusinessDay(calendar=CompanyCalendar())\n\nsla = pd.to_datetime([\"2026-12-23\", \"2026-12-24\"]) + 2 * workday\nprint(sla)     # skips both the weekend and the two December holidays\n",[14,3395,3396,3406,3418,3429,3433,3450,3460,3488,3514,3540,3565,3570,3574,3592,3596,3628],{"__ignoreMap":196},[200,3397,3398,3400,3402,3404],{"class":202,"line":203},[200,3399,207],{"class":206},[200,3401,211],{"class":210},[200,3403,214],{"class":206},[200,3405,217],{"class":210},[200,3407,3408,3410,3413,3415],{"class":202,"line":220},[200,3409,333],{"class":206},[200,3411,3412],{"class":210}," pandas.tseries.holiday ",[200,3414,207],{"class":206},[200,3416,3417],{"class":210}," AbstractHolidayCalendar, Holiday\n",[200,3419,3420,3422,3424,3426],{"class":202,"line":227},[200,3421,333],{"class":206},[200,3423,3228],{"class":210},[200,3425,207],{"class":206},[200,3427,3428],{"class":210}," CustomBusinessDay\n",[200,3430,3431],{"class":202,"line":256},[200,3432,224],{"emptyLinePlaceholder":223},[200,3434,3435,3438,3442,3444,3448],{"class":202,"line":261},[200,3436,3437],{"class":206},"class",[200,3439,3441],{"class":3440},"sMTad"," CompanyCalendar",[200,3443,866],{"class":210},[200,3445,3447],{"class":3446},"sHg5J","AbstractHolidayCalendar",[200,3449,1285],{"class":210},[200,3451,3452,3455,3457],{"class":202,"line":295},[200,3453,3454],{"class":210},"    rules ",[200,3456,233],{"class":206},[200,3458,3459],{"class":210}," [\n",[200,3461,3462,3465,3468,3470,3473,3475,3477,3479,3482,3484,3486],{"class":202,"line":304},[200,3463,3464],{"class":210},"        Holiday(",[200,3466,3467],{"class":278},"\"New Year\"",[200,3469,242],{"class":210},[200,3471,3472],{"class":272},"month",[200,3474,233],{"class":206},[200,3476,160],{"class":239},[200,3478,242],{"class":210},[200,3480,3481],{"class":272},"day",[200,3483,233],{"class":206},[200,3485,160],{"class":239},[200,3487,1134],{"class":210},[200,3489,3490,3492,3495,3497,3499,3501,3504,3506,3508,3510,3512],{"class":202,"line":311},[200,3491,3464],{"class":210},[200,3493,3494],{"class":278},"\"Company Day\"",[200,3496,242],{"class":210},[200,3498,3472],{"class":272},[200,3500,233],{"class":206},[200,3502,3503],{"class":239},"6",[200,3505,242],{"class":210},[200,3507,3481],{"class":272},[200,3509,233],{"class":206},[200,3511,364],{"class":239},[200,3513,1134],{"class":210},[200,3515,3516,3518,3521,3523,3525,3527,3529,3531,3533,3535,3538],{"class":202,"line":317},[200,3517,3464],{"class":210},[200,3519,3520],{"class":278},"\"Christmas\"",[200,3522,242],{"class":210},[200,3524,3472],{"class":272},[200,3526,233],{"class":206},[200,3528,364],{"class":239},[200,3530,242],{"class":210},[200,3532,3481],{"class":272},[200,3534,233],{"class":206},[200,3536,3537],{"class":239},"25",[200,3539,1134],{"class":210},[200,3541,3542,3544,3547,3549,3551,3553,3555,3557,3559,3561,3563],{"class":202,"line":435},[200,3543,3464],{"class":210},[200,3545,3546],{"class":278},"\"Boxing Day\"",[200,3548,242],{"class":210},[200,3550,3472],{"class":272},[200,3552,233],{"class":206},[200,3554,364],{"class":239},[200,3556,242],{"class":210},[200,3558,3481],{"class":272},[200,3560,233],{"class":206},[200,3562,595],{"class":239},[200,3564,1134],{"class":210},[200,3566,3567],{"class":202,"line":460},[200,3568,3569],{"class":210},"    ]\n",[200,3571,3572],{"class":202,"line":1202},[200,3573,224],{"emptyLinePlaceholder":223},[200,3575,3576,3579,3581,3584,3587,3589],{"class":202,"line":1232},[200,3577,3578],{"class":210},"workday ",[200,3580,233],{"class":206},[200,3582,3583],{"class":210}," CustomBusinessDay(",[200,3585,3586],{"class":272},"calendar",[200,3588,233],{"class":206},[200,3590,3591],{"class":210},"CompanyCalendar())\n",[200,3593,3594],{"class":202,"line":1237},[200,3595,224],{"emptyLinePlaceholder":223},[200,3597,3598,3601,3603,3606,3609,3611,3614,3617,3619,3622,3625],{"class":202,"line":1243},[200,3599,3600],{"class":210},"sla ",[200,3602,233],{"class":206},[200,3604,3605],{"class":210}," pd.to_datetime([",[200,3607,3608],{"class":278},"\"2026-12-23\"",[200,3610,242],{"class":210},[200,3612,3613],{"class":278},"\"2026-12-24\"",[200,3615,3616],{"class":210},"]) ",[200,3618,419],{"class":206},[200,3620,3621],{"class":239}," 2",[200,3623,3624],{"class":206}," *",[200,3626,3627],{"class":210}," workday\n",[200,3629,3630,3632,3635],{"class":202,"line":1288},[200,3631,298],{"class":239},[200,3633,3634],{"class":210},"(sla)     ",[200,3636,3637],{"class":307},"# skips both the weekend and the two December holidays\n",[10,3639,3640,3641,3645],{},"Getting this wrong is a quiet class of reporting bug: a service-level report that counts calendar days will show breaches over every holiday period, and nobody notices until somebody argues about a number. Where the calculation feeds an aggregate that people act on, it belongs in the validated part of your pipeline — see ",[23,3642,3644],{"href":3643},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","validating Excel data with Python"," for how to assert on that kind of derived column before a report ships.",[10,3647,3648,3649,3652],{},"One more habit worth building: never compute date arithmetic row by row. A ",[14,3650,3651],{},".apply"," over a date column is typically two orders of magnitude slower than the vectorised offset, and on a workbook with a few hundred thousand rows the difference is the whole runtime of the job:",[191,3654,3656],{"className":193,"code":3655,"language":195,"meta":196,"style":196},"# Slow — a Python-level call per row.\ndf[\"due\"] = df[\"invoice_date\"].apply(lambda d: d + pd.Timedelta(days=30))\n\n# Fast — one vectorised operation over the whole column.\ndf[\"due\"] = df[\"invoice_date\"] + pd.Timedelta(days=30)\n",[14,3657,3658,3663,3698,3702,3707],{"__ignoreMap":196},[200,3659,3660],{"class":202,"line":203},[200,3661,3662],{"class":307},"# Slow — a Python-level call per row.\n",[200,3664,3665,3667,3669,3671,3673,3675,3677,3680,3683,3686,3688,3690,3692,3694,3696],{"class":202,"line":220},[200,3666,792],{"class":210},[200,3668,1090],{"class":278},[200,3670,797],{"class":210},[200,3672,233],{"class":206},[200,3674,781],{"class":210},[200,3676,784],{"class":278},[200,3678,3679],{"class":210},"].apply(",[200,3681,3682],{"class":206},"lambda",[200,3684,3685],{"class":210}," d: d ",[200,3687,419],{"class":206},[200,3689,2193],{"class":210},[200,3691,2196],{"class":272},[200,3693,233],{"class":206},[200,3695,369],{"class":239},[200,3697,903],{"class":210},[200,3699,3700],{"class":202,"line":227},[200,3701,224],{"emptyLinePlaceholder":223},[200,3703,3704],{"class":202,"line":256},[200,3705,3706],{"class":307},"# Fast — one vectorised operation over the whole column.\n",[200,3708,3709,3711,3713,3715,3717,3719,3721,3723,3725,3727,3729,3731,3733],{"class":202,"line":261},[200,3710,792],{"class":210},[200,3712,1090],{"class":278},[200,3714,797],{"class":210},[200,3716,233],{"class":206},[200,3718,781],{"class":210},[200,3720,784],{"class":278},[200,3722,797],{"class":210},[200,3724,419],{"class":206},[200,3726,2193],{"class":210},[200,3728,2196],{"class":272},[200,3730,233],{"class":206},[200,3732,369],{"class":239},[200,3734,292],{"class":210},[151,3736,3738],{"id":3737},"key-takeaways","Key takeaways",[3740,3741,3742,3755,3763,3775,3788,3798,3807],"ul",{},[3743,3744,3745,3748,3749,3751,3752,27],"li",{},[186,3746,3747],{},"An Excel date is a number plus a format."," A cell showing ",[14,3750,164],{}," holds the right value and the wrong format; set ",[14,3753,3754],{},"number_format",[3743,3756,3757,3762],{},[186,3758,3759,3760,27],{},"Anchor at ",[14,3761,188],{}," The 1900-leap-year bug means a naive 1900-01-01 origin is one day off on every date.",[3743,3764,3765,3774],{},[186,3766,3767,3768,3770,3771,3773],{},"Parse with ",[14,3769,673],{},", then inspect the ",[14,3772,732],{}," rows."," Separate genuinely blank cells from unparseable ones and report the difference.",[3743,3776,3777,3783,3784,3787],{},[186,3778,3779,3780,3782],{},"Pass an explicit ",[14,3781,815],{}," when you know it"," — faster, and it refuses to guess ",[14,3785,3786],{},"03\u002F04"," for you.",[3743,3789,3790,3793,3794,3797],{},[186,3791,3792],{},"Excel has no timezones."," Convert to one zone, ",[14,3795,3796],{},"tz_localize(None)",", and label the sheet.",[3743,3799,3800,3806],{},[186,3801,3802,3803,3805],{},"Durations are day fractions with a ",[14,3804,2025],{}," format."," The brackets stop the display wrapping at 24 hours.",[3743,3808,3809,3822],{},[186,3810,3811,3812,425,3815,3818,3819,3821],{},"Group with ",[14,3813,3814],{},"Grouper",[14,3816,3817],{},"to_period",", and ",[14,3820,2768],{}," to fill empty periods"," so gaps in a series stay visible.",[151,3824,3826],{"id":3825},"frequently-asked-questions","Frequently asked questions",[10,3828,3829,3832,3833,3835,3836,3838],{},[186,3830,3831],{},"Why does Excel show a number where I wrote a date?","\nThe value is correct but the cell has no date number format. Excel stores dates as a day count and decides how to display them from the format string, so set ",[14,3834,3754],{}," on the cell — for example ",[14,3837,1566],{}," — and the same value renders as a date.",[10,3840,3841,3844],{},[186,3842,3843],{},"What is the 1899-12-30 epoch and why not 1900-01-01?","\nExcel reproduces a 1980s Lotus bug that treats 1900 as a leap year, adding a day that never existed. Anchoring conversions at 1899-12-30 cancels that offset, which is why pandas uses it as the origin for Excel serials.",[10,3846,3847,3850,3851,3853],{},[186,3848,3849],{},"Can I write a timezone-aware datetime to Excel?","\nNot directly — Excel has no timezone concept and openpyxl raises on aware datetimes. Convert to a single agreed zone, drop the tzinfo with ",[14,3852,3796],{},", and record the zone in a header or a separate column.",[10,3855,3856,3865,3866,3869,3870,3872,3873,3876,3877,3879],{},[186,3857,3858,3859,3861,3862,3864],{},"How do I read a column that mixes ",[14,3860,610],{}," and ",[14,3863,616],{},"?","\nParse with ",[14,3867,3868],{},"pd.to_datetime(col, errors=\"coerce\", format=\"mixed\")"," and then inspect the ",[14,3871,732],{}," rows. For genuinely ambiguous day-first data, pass ",[14,3874,3875],{},"dayfirst=True"," so ",[14,3878,3786],{}," is read as the fourth of March rather than the third of April.",[10,3881,3882,3885],{},[186,3883,3884],{},"Why did my times drift by a second after a round trip?","\nExcel stores the time of day as a binary fraction of a day, so values that are not exact binary fractions pick up a tiny representation error. Round to the second after reading if exact equality matters.",[10,3887,3888,3891,3892,3894,3895,3898,3899,3901],{},[186,3889,3890],{},"What is the fastest way to group Excel rows into months?","\nConvert the column to datetime once, then use ",[14,3893,2404],{}," for a label or ",[14,3896,3897],{},"Grouper(key=\"date\", freq=\"MS\")"," inside a ",[14,3900,2849],{},". Both are vectorised; never loop over rows to build month strings.",[151,3903,3905],{"id":3904},"related","Related",[3740,3907,3908,3914,3921,3928,3934,3940,3947],{},[3743,3909,3910,3911,3913],{},"Up to the parent: ",[23,3912,26],{"href":25}," — the section this topic belongs to.",[3743,3915,3916,3920],{},[23,3917,3919],{"href":3918},"\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"," — the parsing walkthrough in full.",[3743,3922,3923,3927],{},[23,3924,3926],{"href":3925},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates\u002F","Fix Excel Serial Numbers Showing Instead of Dates"," — the single most common date complaint, solved.",[3743,3929,3930,3933],{},[23,3931,3932],{"href":2010},"Handle Timezones in Excel Timestamps with Python"," — the convert-then-strip pattern in depth.",[3743,3935,3936,3939],{},[23,3937,3938],{"href":2772},"Group Excel Rows by Month and Quarter with pandas"," — period aggregation for recurring reports.",[3743,3941,3942,3946],{},[23,3943,3945],{"href":3944},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002F","Cleaning Excel Data with pandas"," — the sibling topic for the non-date columns.",[3743,3948,3949,3952],{},[23,3950,3951],{"href":1625},"Format Dates in Excel Cells with Python"," — the display side of the same coin.",[3954,3955,3956],"style",{},"html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .sHg5J, html code.shiki .sHg5J{--shiki-default:#6F42C1;--shiki-dark:#91CBFF}",{"title":196,"searchDepth":220,"depth":220,"links":3958},[3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969],{"id":153,"depth":220,"text":154},{"id":562,"depth":220,"text":563},{"id":1002,"depth":220,"text":1003},{"id":1629,"depth":220,"text":1630},{"id":2014,"depth":220,"text":2015},{"id":2375,"depth":220,"text":2376},{"id":2824,"depth":220,"text":2825},{"id":3198,"depth":220,"text":3199},{"id":3737,"depth":220,"text":3738},{"id":3825,"depth":220,"text":3826},{"id":3904,"depth":220,"text":3905},"Handle Excel dates correctly in Python: the serial-number model, parsing mixed date text with pandas, timezone-aware timestamps, durations, and period grouping for reports.","md",[3973,3975,3977,3979,3982,3984],{"q":3831,"a":3974},"The value is correct but the cell has no date number format. Excel stores dates as a day count and decides how to display them from the format string, so set number_format on the cell — for example \"yyyy-mm-dd\" — and the same value renders as a date.",{"q":3843,"a":3976},"Excel reproduces a 1980s Lotus bug that treats 1900 as a leap year, adding a day that never existed. Anchoring conversions at 1899-12-30 cancels that offset, which is why pandas uses it as the origin for Excel serials.",{"q":3849,"a":3978},"Not directly — Excel has no timezone concept and openpyxl raises on aware datetimes. Convert to a single agreed zone, drop the tzinfo with tz_localize(None), and record the zone in a header or a separate column.",{"q":3980,"a":3981},"How do I read a column that mixes 2026-08-15 and 15\u002F08\u002F2026?","Parse with pd.to_datetime(col, errors=\"coerce\", format=\"mixed\") and then inspect the NaT rows. For genuinely ambiguous day-first data, pass dayfirst=True so 03\u002F04 is read as the fourth of March rather than the third of April.",{"q":3884,"a":3983},"Excel stores the time of day as a binary fraction of a day, so values that are not exact binary fractions pick up a tiny representation error. Round to the second after reading if exact equality matters.",{"q":3890,"a":3985},"Convert the column to datetime once, then use dt.to_period(\"M\") for a label or Grouper(key=\"date\", freq=\"MS\") inside a groupby. Both are vectorised; never loop over rows to build month strings.",{},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data",{"title":3989,"description":3990},"Excel Dates in Python: Parsing, Serials and Timezones","A complete guide to date handling between Excel and Python — the 1900 serial system, pd.to_datetime on messy columns, timezone-aware writes, durations, and month grouping.","working-with-dates-and-times-in-excel-data","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Findex","guide","ZBkKX0xtETB6EvzM9rqtbvOn4O_I6XKl4jlQpKWvL40",[3996,4000],{"title":3997,"path":3998,"stem":3999,"children":-1},"Validate Excel Columns Before Import with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002Findex",{"title":3926,"path":4001,"stem":4002,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates\u002Findex",1786800026511]