为什么pandas读Excel日期列全是浮点数字?

上个月接了个活,要把客户发来的几十个Excel报表汇总到一个数据库里。数据量不大,逻辑也简单,我心想pandas几行代码就搞定。

结果read_excel一跑出来,日期列全是44562.0、44601.5这种数字。Excel里明明显示的是"2025-03-15",怎么到pandas嘴里就变味了?

这个问题我前前后后折腾了两天,试了好几种方法,最后才理清楚到底怎么回事。写下来给同样被这坑绊过的朋友参考。

为什么日期变成了浮点数

这不是pandas的bug,是Excel自己存日期的方式。

Excel内部用浮点数表示日期和时间。整数部分是从1900年1月1日起的天数(有个著名的1900年闰年bug,2月29日其实是虚拟的),小数部分是当天的时间比例。所以44562就代表从1900年1月1日往后数44562天,对应2025年3月15日。

你在Excel里看到的"2025-03-15"只是单元格格式化后的显示效果,底层存的还是那个浮点数。

pandas默认读Excel的时候,如果单元格格式信息没有被正确传递,它就只拿到了底层的浮点数。尤其是用xlrd引擎读xls文件,或者Excel里日期列的格式被手动改过,这种情况特别容易出现。

方法一:read_excel的parse_dates参数

最直接的办法是在read_excel的时候告诉pandas哪些列是日期:

python 复制代码
import pandas as pd

df = pd.read_excel(
    'sales_report.xlsx',
    parse_dates=['order_date', 'delivery_date']
)

如果列名不确定,也可以用列索引:

python 复制代码
df = pd.read_excel(
    'sales_report.xlsx',
    parse_dates=[2, 5]  # 第3列和第6列是日期
)

这个方法大部分时候管用。但有个前提:Excel文件里这些列的格式必须是"日期"类型。如果有人把格式改成了"常规"或者"文本",parse_dates就不起作用了,读出来照样是浮点数。

我碰到的第一个坑就是这个,客户的报表里,有人在日期列上点了"清除格式",结果日期格式信息全丢了。

方法二:浮点数手动转日期

当parse_dates不生效,或者数据已经读出来了,可以手动把浮点数转成日期。

Excel的浮点数日期有个基准:从1899年12月30日开始算(那个1900闰年bug导致偏移了一天)。转换公式:

python 复制代码
from datetime import datetime, timedelta

def excel_float_to_date(f):
    # Excel基准日期是1899-12-30(因为1900闰年bug)
    base = datetime(1899, 12, 30)
    return base + timedelta(days=f)

# 测试
print(excel_float_to_date(44562))  # 2025-03-15 00:00:00
print(excel_float_to_date(44601.5))  # 2025-03-15 12:00:00 + 39.5天

批量转换整列:

python 复制代码
df['order_date'] = df['order_date'].apply(
    lambda x: datetime(1899, 12, 30) + timedelta(days=x) if pd.notna(x) and isinstance(x, (int, float)) else x
)

这里有两个容易忽略的点:

第一,列里可能混着浮点数和正常字符串。有的人在Excel里既敲了"2025-03-15"这样的文本,又敲了格式化的日期,读出来就一半是float一半是str。apply里要加类型判断,别把字符串也当浮点数处理了。

第二,空单元格读出来是NaN,NaN是float类型,isinstance(x, float)会匹配到它。所以必须先判断pd.notna(x)。

方法三:换引擎读Excel

pandas读Excel支持两种引擎:xlrd(只支持xls)和openpyxl(支持xlsx)。不同引擎对日期格式的识别能力不一样。

openpyxl引擎在读取xlsx时,会尝试根据单元格的格式信息来判断类型。如果你的文件是xlsx格式,指定openpyxl引擎往往能更好地保留日期类型:

python 复制代码
df = pd.read_excel('report.xlsx', engine='openpyxl')

但openpyxl也有问题,它读大文件比较慢,而且如果单元格格式是"自定义"(比如"yyyy年mm月dd日"这种中文格式),有时候也识别不出来。

xlrd引擎在老版本里读xlsx是默认行为,但新版xlrd(2.0以后)只支持xls了。所以如果你用xlrd读xlsx会直接报错,得用openpyxl。

对于xls文件,xlrd是唯一选择,而且它确实更容易出现日期变浮点数的问题。一个解决办法是用xlrd单独打开文件,先检测哪些列是日期类型:

python 复制代码
import xlrd

book = xlrd.open_workbook('old_report.xls')
sheet = book.sheet_by_index(0)

# 找出日期列
date_cols = []
for col_idx in range(sheet.ncols):
    # 检查前几行的单元格类型
    for row_idx in range(min(5, sheet.nrows)):
        cell = sheet.cell(row_idx, col_idx)
        if cell.ctype == xlrd.XL_CELL_DATE:
            date_cols.append(col_idx)
            break

print(f"日期列索引: {date_cols}")

# 然后用pandas读取,指定这些列为日期
df = pd.read_excel('old_report.xls', parse_dates=date_cols)

xlrd.XL_CELL_DATE的值是3,代表单元格类型是日期。这个检测方法虽然多写几行,但比瞎猜parse_dates靠谱得多。

几个我踩过的边角坑

同一列里日期格式不一致。比如前100行是日期格式,后面有人手敲了"3月15日"这种文本。pandas读出来列类型就变成mixed,parse_dates直接失效。解决办法是先在Excel里统一格式,或者读出来后分两段处理。

时区问题。Excel日期没有时区概念,转成pandas的datetime后也是naive的。如果你要做跨时区对比,记得手动加时区:

python 复制代码
df['order_date'] = pd.to_datetime(df['order_date']).dt.tz_localize('Asia/Shanghai')

时间精度丢失。44601.5这种小数代表某个时刻,但如果你只取日期部分(用.dt.date),时间信息就丢了。如果业务需要精确到小时,转的时候注意保留小数部分。

pandas的to_datetime也能处理浮点数。如果你不想写手动转换函数,其实pd.to_datetime对某些浮点数也能处理,但它默认把浮点数当Unix时间戳来解析(从1970年开始算),跟Excel的基准(1899年)完全不同。所以千万别直接对Excel浮点数用to_datetime,出来的日期会差一百多年。

python 复制代码
# 错误示范,这会把44562解释成1970年以后的44562秒
pd.to_datetime(44562, unit='s')  # 1970-01-01 + 44562秒 ≈ 1970-01-02

# 正确做法:先转成Excel基准的datetime,再让pandas识别
df['date'] = df['date'].apply(excel_float_to_date)

我的最终方案

折腾一圈后,我最后稳定用的流程:

  1. 先用openpyxl引擎读取xlsx文件,配合parse_dates参数
  2. 读完后检查日期列的类型,如果还是float,就用excel_float_to_date手动转换
  3. 对于xls文件,先用xlrd检测日期列索引,再传给parse_dates
  4. 最后统一用pd.to_datetime做一轮格式校验,确保没有遗漏的str类型日期
python 复制代码
import pandas as pd
from datetime import datetime, timedelta

def excel_float_to_date(f):
    base = datetime(1899, 12, 30)
    return base + timedelta(days=float(f))

def clean_date_column(col):
    """统一处理日期列中的混合类型"""
    result = []
    for val in col:
        if pd.isna(val):
            result.append(pd.NaT)
        elif isinstance(val, (int, float)):
            result.append(excel_float_to_date(val))
        else:
            result.append(pd.to_datetime(val, errors='coerce'))
    return pd.Series(result)

# 主流程
df = pd.read_excel('sales_report.xlsx', engine='openpyxl', parse_dates=['order_date'])

# 检查:如果日期列没被正确解析
if df['order_date'].dtype != 'datetime64[ns]':
    df['order_date'] = clean_date_column(df['order_date'])

这套流程跑下来,几十个报表没再出日期问题。虽然多写了些代码,但省了后续反复修数据的时间。

碰到日期变浮点数怎么办

说到底,Excel把格式和值分开存,内部没问题,跨工具读就麻烦了。pandas只能尽力去识别格式,识别不了就给你原始值。

下次碰到日期列全是浮点数,大概率不是代码写错了,是Excel格式信息丢了。parse_dates不行就手动转,手动转注意NaN和混合类型,基本都能搞定。

如果你自己生成Excel,一开始就把日期列设成明确的日期格式,别用"常规"或"文本"。这样后面不管谁读,都不会再踩这个坑。