📅 日期时间处理

← 知识体系

你遇到什么场景?

从EDC收集来的日期经常不完整——有时只有年月、有时只有年、有时甚至缺失。而这些日期直接决定AE的开始结束时间、治疗 emergent 标识、暴露量计算。处理不好就是数据错误

核心策略

遇到不完整日期,不是随便填一个值——有一套从保守到激进的插补阶梯

不完整程度插补策略示例
完整 YYYY-MM-DD直接使用2025-03-15 → 2025-03-15
仅 YYYY-MM开始日期取月初(01日),结束日期取月末2025-03 → 开始:03-01 / 结束:03-31
仅 YYYY开始取0101,结束取1231;
但优先对齐到首次给药日期
2025 → 开始:01-01 / 结束:12-31
缺失开始日期用首次给药日;
结束日期留缺失或用末次接触日截断
→ TRTSDT / LSTALVDT
原则:插补后的开始日期不能晚于结束日期,结束日期不能超过末次接触日(LSTALVDT)。插补后必须做这个校验。

代码模板

模板一:AE开始日期的三级插补

最常用的模式。按完整度逐级降级,打上插补标识(D=日级/M=月级/Y=年级)。

SAS
/* AE开始日期三级插补 */
data adae2;
    set adae1;
    format ASTDT date9.;  length ASTDTF $1;

    /* Level 1: 完整日期 YYYY-MM-DD */
    if count(AESTDTC, '-') = 2 then
        ASTDT = input(AESTDTC, yymmdd10.);

    /* Level 2: 只有年月 YYYY-MM → 开始日期取月初 */
    else if count(AESTDTC, '-') = 1 then do;
        if AESTDTC = put(TRTSDT, yymmd7.) then ASTDT = TRTSDT;
        else ASTDT = input(cats(AESTDTC, '-01'), yymmdd10.);
        ASTDTF = 'D';  /* 日级插补 */
    end;

    /* Level 3: 只有年份 YYYY → 优先对齐首次给药年 */
    else if length(AESTDTC) = 4 then do;
        if AESTDTC = put(year(TRTSDT), 4.) then ASTDT = TRTSDT;
        else ASTDT = input(cats(AESTDTC, '-01-01'), yymmdd10.);
        ASTDTF = 'M';  /* 月级插补 */
    end;

    /* Level 4: 完全缺失 → 用首次给药日 */
    else if AESTDTC = '' then do;
        ASTDT = TRTSDT;
        ASTDTF = 'Y';  /* 年级插补 */
    end;
run;

模板二:AE结束日期 → 月末/年末处理

结束日期与开始不同——不完整时应该取月末/年末(保守:AE结束于该时间段的最后一天)。

SAS
/* AE结束日期插补 → 取月末/年末 */
data adae2;
    set adae1;
    format AENDT date9.;  length AENDTF $1;

    if count(AEENDTC, '-') = 2 then
        AENDT = input(AEENDTC, yymmdd10.);

    else if count(AEENDTC, '-') = 1 then do;
        /* 只有年月 → 取该月最后一天 */
        AENDT = intnx('month',
                      input(cats(AEENDTC, '-01'), yymmdd10.),
                      0, 'end');
        AENDTF = 'D';
    end;

    else if length(AEENDTC) = 4 then do;
        /* 只有年份 → 取12月31日 */
        AENDT = input(cats(AEENDTC, '-12-31'), yymmdd10.);
        AENDTF = 'M';
    end;

    /* 插补后校验:结束日不能超过末次接触日 */
    if AENDTF ^= '' and LSTALVDT ^= . then do;
        if AENDT > LSTALVDT then AENDT = LSTALVDT;
    end;

    /* 插补后校验:开始日不能晚于结束日 */
    if ASTDTF ^= '' and AENDT ^= . then do;
        if ASTDT > AENDT then ASTDT = AENDT;
    end;
run;

模板三:AE完整日期时间插补(I期高精度)

健康人I期需要精确到分钟。日期+时间分离处理,参考首次给药日期时间(TR01SDT/TR01STM)。

SAS
/* AE日期+时间完整插补(I期) */
data adae2;
    set adae1;
    format ASTDT date9. ASTTM time8. ASTDTM datetime20.;
    length ASTDTF ASTTMF $1;

    /* --- 日期部分 --- */
    ASTDC_DATE = strip(scan(AESTDTC, 1, 'T'));
    ASTDC_TIME = strip(scan(AESTDTC, 2, 'T'));

    if count(ASTDC_DATE, '-') = 2 then
        ASTDT = input(ASTDC_DATE, yymmdd10.);
    else if count(ASTDC_DATE, '-') = 1 then do;
        if ASTDC_DATE = put(TRTSDT, yymmd7.) then ASTDT = TRTSDT;
        else ASTDT = input(cats(ASTDC_DATE, '-01'), yymmdd10.);
        ASTDTF = 'D';
    end;

    /* --- 时间部分 --- */
    if count(ASTDC_TIME, ':') = 1 then
        ASTTM = input(ASTDC_TIME, time8.);
    else if length(ASTDC_TIME) = 2 then do;
        /* 只有小时HH → 对齐首次给药的小时或填:00 */
        if ASTDT = TR01SDT and input(ASTDC_TIME, best.) = hour(TR01STM) then
            ASTTM = TR01STM;
        else ASTTM = input(cats(ASTDC_TIME, ':00'), time8.);
        ASTTMF = 'M';
    end;
    else if ASTDC_TIME = '' then do;
        if ASTDT = TR01SDT then ASTTM = TR01STM;
        else ASTTM = input('00:00', time8.);
        ASTTMF = 'H';
    end;

    /* 合并为datetime(秒级) */
    ASTDTM = ASTDT * 86400 + ASTTM;
    drop ASTDC_:;
run;

关键函数速查

函数用途示例
count(dtc,'-')判断日期完整度(2=完整/1=月/0=年)
intnx('month',dt,0,'end')取月末最后一天intnx('month','01MAR2025'd,0,'end') → 31MAR2025
cats(year,'-12-31')拼年末日期cats('2025','-12-31') → '2025-12-31'
put(TRTSDT,yymmd7.)日期转YYYY-MM格式,用于比较put('15MAR2025'd,yymmd7.) → '2025-03'
scan(dtc,1,'T')从ISO格式分离日期部分scan('2025-03-15T08:30',1,'T') → '2025-03-15'
hour(TR01STM)提取datetime中的小时数hour('08:30:00't) → 8

常见坑

⚠️ 插补后不校验顺序
插补完开始/结束日期后必须检查:ASTDT ≤ AENDT,AENDT ≤ LSTALVDT。否则Pinnacle 21会报SDTM inconsistency。
⚠️ count(dtc,'-')的陷阱
count('2025','-') = 0(只有年份),count('2025-03','-') = 1(年月),count('2025-03-15','-') = 2(完整)。但如果ISO格式后面带T:count('2025-03-15T08:30','-')也是2,先用scan分离。
⚠️ intnx('month',...,'end')不会自动截断
INTNX只生成月末日期,不会自动比较LSTALVDT。必须单独加校验逻辑。
⚠️ ASTDTF标记与P21校验
只要做了插补就应打上ASTDTF标记(D/M/Y)。Pinnacle 21会检查这些标记的一致性。