1. 压窜诊断的数据准备与曲线对齐

为什么“对齐”是压窜诊断的第一道门槛

邻井压裂干扰诊断的本质,是在同一时间轴上比较两口井的压力响应。现场拿到的数据往往来自不同采集系统:生产井压力来自SCADA/RTU,采样间隔可能是1 min或5 min;邻井压裂施工数据来自压裂车组记录仪,采样间隔常为1 s,且时间戳可能是本地时间、UTC或设备内部时钟。若直接叠加,压窜起跳点会被人为错位几分钟到几十分钟,导致“谁先响应、响应滞后多久”这类关键判据完全失真。

本案例针对一口受邻井压裂干扰的生产井(记为A井),完成三件事:压力数据清洗(剔除异常点、统一单位与量纲)、时间戳统一(全部换算到同一时区与同一时间基准)、多井曲线对齐(重采样到统一时间栅格并做时钟偏差校正),最终输出可直接用于压窜识别的标准化压力序列。

数据对象与字段约定

假设手上有三份原始数据(示例数据,非实测):

  • A井井口压力:字段 ts_local(本地时间,格式 YYYY-MM-DD HH:MM:SS)、p_MPa(单位MPa),采样间隔5 min,存在少量重复时间戳与跳变毛刺。
  • B井压裂施工压力/排量:字段 ts_utc(UTC时间)、p_frac_MPa、rate_m3min,采样间隔1 s。
  • 井时钟偏差记录:现场校时记录显示A井RTU时钟比标准时间慢 92 s(示例值),需在统一时间基准时补偿。

约定:所有时间最终统一为UTC,所有压力统一为MPa(表压),统一时间栅格为1 min。参照约定一旦确定,后续正文、代码与图中保持一致。

处理链路与对齐逻辑

下图给出从原始数据到标准化序列的完整处理链,重点标出时钟偏差补偿与重采样两个容易出错的环节:

原始数据源 A井 井口压力 ts_local(本地时间) p_MPa · 5 min 间隔 含重复戳 / 毛刺 B井 压裂施工 ts_utc(UTC时间) p_frac_MPa · 1 s 间隔 rate_m3min 校时记录 A井RTU 慢 92 s (示例偏差) ① 清洗 去重 · 去毛刺 单位统一为 MPa 缺失标记不插值 ① 清洗 剔除停泵空档 排量为0段标记 保留施工有效段 ② 时间戳统一 本地时间 → UTC A井 +92 s 补偿 统一为 UTC 基准 ③ 重采样对齐 统一 1 min 栅格 A井线性插值 B井取窗口均值 标准化序列 ts_utc(1 min) p_A_MPa p_frac_MPa rate_m3min 对齐校验判据 · 起跳时刻差应 ≤ 1 个采样栅格(1 min) · 补偿前后起跳点位移应等于 92 s(示例) · 对齐后两井压力序列长度与时间范围一致

关键实现:清洗、时间统一与重采样

下面给出可直接运行的 Python 实现(依赖 pandas、numpy)。示例数据在代码内构造,便于独立验证;实际使用时替换为读取 CSV 即可。

import pandas as pd
import numpy as np

# ---------- 1. 构造示例原始数据(非实测) ----------
# A井:本地时间,5 min 间隔,含重复戳与毛刺
tA = pd.date_range("2024-05-01 08:00:00", periods=60, freq="5min")
pA = 28.0 + np.linspace(0, -1.2, 60)          # 正常递减趋势
pA[20] = 45.0                                  # 人为毛刺
dfA = pd.DataFrame({"ts_local": tA, "p_MPa": pA})
dfA = pd.concat([dfA, dfA.iloc[[10]]], ignore_index=True)  # 重复时间戳

# B井:UTC时间,1 s 间隔,施工段
tB = pd.date_range("2024-05-01 00:00:00", periods=3600, freq="1s")
pB = np.where((tB >= "2024-05-01 00:20:00") & (tB < "2024-05-01 00:40:00"),
              60 + 8*np.sin(np.linspace(0, 6.28, 3600)), np.nan)
rB = np.where(~np.isnan(pB), 12.0, 0.0)
dfB = pd.DataFrame({"ts_utc": tB, "p_frac_MPa": pB, "rate_m3min": rB})

# ---------- 2. 清洗 ----------
# A井:去重(保留最后一条)、去毛刺(3σ 或绝对阈值)
dfA = dfA.drop_duplicates(subset="ts_local", keep="last").sort_values("ts_local")
med = dfA["p_MPa"].rolling(5, center=True, min_periods=1).median()
dfA["p_MPa"] = np.where((dfA["p_MPa"] - med).abs() > 5.0, med, dfA["p_MPa"])

# B井:仅保留有效施工段(排量>0 且压力非空)
dfB = dfB[(dfB["rate_m3min"] > 0) & dfB["p_frac_MPa"].notna()].copy()

# ---------- 3. 时间戳统一到 UTC ----------
# 假设本地时区为 UTC+8,A井RTU慢92s → 真实UTC = 本地 - 8h + 92s
TZ_OFFSET = pd.Timedelta(hours=8)
CLOCK_SKEW = pd.Timedelta(seconds=92)
dfA["ts_utc"] = dfA["ts_local"] - TZ_OFFSET + CLOCK_SKEW
dfA = dfA[["ts_utc", "p_MPa"]].set_index("ts_utc").sort_index()
dfB = dfB.set_index("ts_utc").sort_index()

# ---------- 4. 重采样到 1 min 统一栅格 ----------
start = min(dfA.index.min(), dfB.index.min()).floor("min")
end   = max(dfA.index.max(), dfB.index.max()).ceil("min")
grid  = pd.date_range(start, end, freq="1min")

A_1min = dfA["p_MPa"].reindex(dfA.index.union(grid)).interpolate("time").reindex(grid)
B_1min = dfB["p_frac_MPa"].resample("1min").mean().reindex(grid)
R_1min = dfB["rate_m3min"].resample("1min").mean().reindex(grid)

aligned = pd.DataFrame({"p_A_MPa": A_1min, "p_frac_MPa": B_1min, "rate_m3min": R_1min})
print(aligned.head())
print("对齐后长度:", len(aligned), "时间范围:", aligned.index.min(), "→", aligned.index.max())

结果判读与对齐校验

运行后应检查三点,任何一点不满足都说明对齐环节有问题:

  • 时间范围一致:aligned 的索引从两井最早时刻到最晚时刻,步长严格1 min,无缺口。
  • 补偿量可验证:把 CLOCK_SKEW 改为0再跑一次,A井压力序列整体应平移92 s;若平移方向相反,说明补偿符号写反了。
  • 起跳点对齐:在B井排量开始大于0的时刻附近,A井压力若出现同步抬升,则两井起跳时刻差应≤1 min;若差出数分钟,优先怀疑时区偏移或时钟偏差未补偿。
不要对缺失段做前向填充后再参与压窜判读:前向填充会把停泵期的恒定值伪装成“压力平台”,掩盖真实响应。缺失就保留为 NaN,在后续诊断中显式跳过。
若现场无法获得校时记录,可用两井共同事件(如B井开泵瞬间的A井压力微扰动)做互相关,估计时钟偏差;但该方法需要足够信噪比,且估计结果应记录为“估计值”而非实测值。