Keras
gospelgit commited on
Commit
7526553
·
verified ·
1 Parent(s): 8c41499

Upload build_dataset.py

Browse files
Files changed (1) hide show
  1. build_dataset.py +221 -0
build_dataset.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ build_dataset.py
4
+
5
+ Reproduces the processed Volve well-condition dataset, the grounded event
6
+ labels, and the per-event provenance file directly from Equinor's raw open
7
+ production export.
8
+
9
+ INPUT (obtain from Equinor's open data distribution; not redistributed here):
10
+ Volve_production_data.xlsx -- the raw production export.
11
+ We use only its "Daily Production Data" sheet.
12
+
13
+ OUTPUTS (written to the current directory):
14
+ volve_well_daily.csv / .parquet -- cleaned per-well daily telemetry
15
+ volve_grounded_events.csv -- 236 grounded anomaly events (5 types)
16
+ volve_events_provenance.csv -- per-event audit trail (well, dates,
17
+ channels, event type, PUD rationale)
18
+
19
+ Every processing decision below is a stated choice, documented in the paper's
20
+ Method section and in CODEBOOK.md. Thresholds are declared assumptions, not
21
+ facts: a different threshold would produce a different label set.
22
+
23
+ Usage:
24
+ python build_dataset.py --raw Volve_production_data.xlsx
25
+
26
+ Dependencies: pandas, numpy, openpyxl, pyarrow (no GPU required)
27
+ """
28
+
29
+ import argparse
30
+ import numpy as np
31
+ import pandas as pd
32
+
33
+
34
+
35
+ def load_and_clean(raw_path):
36
+ """Read the Daily Production Data sheet and clean it into a per-well table.
37
+
38
+ All telemetry in this study derives from this single sheet; this is the
39
+ provenance anchor. See paper Section 3.
40
+ """
41
+ df = pd.read_excel(raw_path, sheet_name="Daily Production Data")
42
+ df["DATEPRD"] = pd.to_datetime(df["DATEPRD"])
43
+
44
+ keep = [
45
+ "DATEPRD", "NPD_WELL_BORE_NAME", "WELL_TYPE", "FLOW_KIND",
46
+ "ON_STREAM_HRS", "AVG_DOWNHOLE_PRESSURE", "AVG_DOWNHOLE_TEMPERATURE",
47
+ "AVG_DP_TUBING", "AVG_ANNULUS_PRESS", "AVG_WHP_P", "AVG_WHT_P",
48
+ "AVG_CHOKE_SIZE_P", "BORE_OIL_VOL", "BORE_GAS_VOL", "BORE_WAT_VOL",
49
+ "BORE_WI_VOL",
50
+ ]
51
+ clean = df[keep].rename(columns={
52
+ "DATEPRD": "date", "NPD_WELL_BORE_NAME": "well", "WELL_TYPE": "well_type",
53
+ "FLOW_KIND": "flow_kind", "ON_STREAM_HRS": "on_stream_hrs",
54
+ "AVG_DOWNHOLE_PRESSURE": "dh_pressure",
55
+ "AVG_DOWNHOLE_TEMPERATURE": "dh_temp", "AVG_DP_TUBING": "dp_tubing",
56
+ "AVG_ANNULUS_PRESS": "annulus_press", "AVG_WHP_P": "whp",
57
+ "AVG_WHT_P": "wht", "AVG_CHOKE_SIZE_P": "choke_pct",
58
+ "BORE_OIL_VOL": "oil_vol", "BORE_GAS_VOL": "gas_vol",
59
+ "BORE_WAT_VOL": "water_vol", "BORE_WI_VOL": "wi_vol",
60
+ })
61
+ clean = clean.sort_values(["well", "date"]).reset_index(drop=True)
62
+
63
+ # Derived water cut, used by the water-breakthrough rule below.
64
+ tot = clean["oil_vol"] + clean["water_vol"]
65
+ clean["water_cut"] = np.where(tot > 0, clean["water_vol"] / tot, np.nan)
66
+ return clean
67
+
68
+
69
+
70
+ #2 GROUNDED EVENT LAYER
71
+
72
+ '''
73
+ Each event type is tied to a documented mechanism in the Volve field development plan (PUD). A rule proposes a candidate;
74
+ the candidate is admitted only because the PUD documents that this mechanism occurs on these wells.
75
+ See paper Section 4. Thresholds are declared assumptions.
76
+ '''
77
+ PUD = {
78
+ "shut_in":
79
+ "PUD S5.3.2/S5.3.6: hydrate & wax risk arises specifically during "
80
+ "well shut-in; on_stream_hrs->0 with live well = documented shut-in.",
81
+ "restart_transient":
82
+ "PUD S5.2/S4.5: gas-lift used to restart wells after shut-in; abrupt "
83
+ "pressure/rate recovery after a stop is a documented restart signature.",
84
+ "water_breakthrough":
85
+ "PUD S4.5.2: injection water breaks through ~2 yr into production; "
86
+ "sustained water-cut rise is the documented breakthrough mechanism.",
87
+ "productivity_loss":
88
+ "PUD S5.3.1/S5.3.4: barium/strontium scaling & asphaltene "
89
+ "precipitation reduce productivity; sustained rate decline at stable "
90
+ "choke.",
91
+ "gaslift_instability":
92
+ "PUD S5.2: gas-lift wells show annulus-pressure instability; anomalous "
93
+ "annulus-pressure deviation is a documented lift-system signature.",
94
+ }
95
+
96
+
97
+ def build_events(clean):
98
+ """Rule-derived candidates, admitted with PUD corroboration. See Section 4."""
99
+ op = clean[clean.well_type == "OP"].copy().sort_values(["well", "date"])
100
+ events = []
101
+
102
+ def add(well, t0, t1, etype, chans, detail):
103
+ events.append(dict(
104
+ well=well, onset=t0, offset=t1, event_type=etype,
105
+ channels=";".join(chans), pud_rationale=PUD[etype], detail=detail,
106
+ ))
107
+
108
+ for well, g in op.groupby("well"):
109
+ g = g.sort_values("date").reset_index(drop=True)
110
+ osh = g["on_stream_hrs"].fillna(0).values
111
+ dates = g["date"].values
112
+ oil = g["oil_vol"].replace(0, np.nan).values
113
+ wc = g["water_cut"].values
114
+ ann = g["annulus_press"].replace(0, np.nan).values
115
+ choke = g["choke_pct"].replace(0, np.nan).values
116
+ n = len(g)
117
+
118
+ # SHUT-IN: on_stream_hrs ~0 for >=2 consecutive days (declared: 2 days)
119
+ shut = (osh < 1.0)
120
+ i = 0
121
+ while i < n:
122
+ if shut[i]:
123
+ j = i
124
+ while j < n and shut[j]:
125
+ j += 1
126
+ if j - i >= 2:
127
+ add(well, dates[i], dates[j - 1], "shut_in",
128
+ ["on_stream_hrs"], f"{j-i}-day zero-onstream span")
129
+ # (2) RESTART: recovery day immediately after shut-in ends
130
+ if j < n and not np.isnan(oil[j]):
131
+ add(well, dates[j], dates[min(j + 1, n - 1)],
132
+ "restart_transient",
133
+ ["on_stream_hrs", "whp", "oil_vol"],
134
+ "rate recovery post shut-in")
135
+ i = j
136
+ else:
137
+ i += 1
138
+
139
+ # WATER BREAKTHROUGH: 7-day water cut crosses & holds >=0.5 (declared)
140
+ wcs = pd.Series(wc).rolling(7, min_periods=3).mean().values
141
+ above = wcs >= 0.5
142
+ for k in range(1, n):
143
+ if above[k] and not above[k - 1]:
144
+ end = min(k + 14, n)
145
+ if np.nanmean(wcs[k:end]) >= 0.5:
146
+ add(well, dates[k], dates[end - 1], "water_breakthrough",
147
+ ["water_cut", "water_vol"], "water-cut sustained >=0.5")
148
+ break # first breakthrough only
149
+
150
+ # PRODUCTIVITY LOSS: oil down >30% over 30d at stable choke (declared)
151
+ oil_s = pd.Series(oil).rolling(7, min_periods=3).mean().values
152
+ ch_s = pd.Series(choke).rolling(7, min_periods=3).mean().values
153
+ for k in range(30, n):
154
+ base, cur = oil_s[k - 30], oil_s[k]
155
+ if np.isfinite(base) and np.isfinite(cur) and base > 50:
156
+ if cur < 0.7 * base:
157
+ if (np.isfinite(ch_s[k]) and np.isfinite(ch_s[k - 30])
158
+ and ch_s[k] >= 0.8 * ch_s[k - 30]):
159
+ add(well, dates[k - 30], dates[k], "productivity_loss",
160
+ ["oil_vol", "choke_pct"],
161
+ f"oil {base:.0f}->{cur:.0f} at stable choke")
162
+ break # first sustained decline only
163
+
164
+ # GAS-LIFT INSTABILITY: annulus pressure >3 MAD from well median
165
+ if np.isfinite(np.nanmedian(ann)):
166
+ med = np.nanmedian(ann)
167
+ mad = np.nanmedian(np.abs(ann - med)) or 1
168
+ z = np.abs(ann - med) / (1.4826 * mad)
169
+ spikes = z > 3
170
+ k = 0
171
+ while k < n:
172
+ if spikes[k] and np.isfinite(ann[k]):
173
+ j = k
174
+ while j < n and spikes[j]:
175
+ j += 1
176
+ add(well, dates[k], dates[j - 1], "gaslift_instability",
177
+ ["annulus_press"], f"annulus z>3 ({j-k}d)")
178
+ k = j
179
+ else:
180
+ k += 1
181
+
182
+ ev = pd.DataFrame(events)
183
+ ev["onset"] = pd.to_datetime(ev["onset"])
184
+ ev["offset"] = pd.to_datetime(ev["offset"])
185
+ ev["duration_days"] = (ev["offset"] - ev["onset"]).dt.days + 1
186
+ ev.insert(0, "event_id", [f"VOLVE-EV-{i:04d}" for i in range(len(ev))])
187
+ return ev
188
+
189
+
190
+ # 3. MAIN
191
+ def main():
192
+ parser = argparse.ArgumentParser(
193
+ description="Build the processed Volve dataset, grounded events, and "
194
+ "provenance from Equinor's raw production export.")
195
+ parser.add_argument("--raw", default="Volve_production_data.xlsx",
196
+ help="Path to Equinor's raw Volve_production_data.xlsx")
197
+ args = parser.parse_args()
198
+
199
+ print(f"Reading raw export: {args.raw}")
200
+ clean = load_and_clean(args.raw)
201
+ clean.to_parquet("volve_well_daily.parquet", index=False)
202
+ clean.to_csv("volve_well_daily.csv", index=False)
203
+ print(f" wrote volve_well_daily.csv/.parquet ({len(clean)} daily rows)")
204
+
205
+ ev = build_events(clean)
206
+ ev.to_csv("volve_grounded_events.csv", index=False)
207
+ print(f" wrote volve_grounded_events.csv ({len(ev)} grounded events)")
208
+
209
+ # The provenance file is the same events with the audit columns made explicit.
210
+ prov = ev[["event_id", "well", "onset", "offset", "event_type",
211
+ "channels", "pud_rationale", "detail"]]
212
+ prov.to_csv("volve_events_provenance.csv", index=False)
213
+ print(f" wrote volve_events_provenance.csv ({len(prov)} provenance rows)")
214
+
215
+ print("\nEvent breakdown by type:")
216
+ for etype, count in ev.event_type.value_counts().items():
217
+ print(f" {etype:22s}: {count}")
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()