Loading and Displaying Well Log Data from LAS Files with Python

Photo by Chris Ried on Unsplash

Anyone who has worked or is currently working within the oil and gas industry will understand that there are a large variety of formats that well log data can be stored in. Some of the common formats that we as petrophysicists work with include LAS, ASCII and CSV files. Many of these formats can easily be loaded into a Python script ot Jupyter Notebook.

Many months back, I put together a series of Jupyter Notebooks illustrating different ways of working with well log data. These can be accessed at the link below, along with a Jupyter Notebook version of this article and associated data. The data used for this article comes from the Volve dataset that was released by Equinor in 2018.

This article will cover:

  1. Loading and Checking Data
  2. Creating a Pandas Dataframe
  3. Visualising Data Extent
  4. Plotting Log Data

Loading and Checking Data

The first step is to import the required libraries: pandas, matplotlib and LASIO.

lasio is a library that has been developed to handle and work with LAS files. More info on the library can be found at: https://lasio.readthedocs.io/en/latest/

import pandas as pd
import matplotlib.pyplot as plt
import lasio

To load our file, we can use the .read() method from LASIO and pass in the LAS file name like so:

las = lasio.read("15-9-19_SR_COMP.LAS")

Now that our file has been loaded, we can start investigating it’s contents.

To find information about where the file originated from, such as the well name, location and what the depth range of the file covers, we can create a simple for loop to go over each header item. Using Python’s f-string we can join the items together.

for item in las.well:
print(f"{item.descr} ({item.mnemonic}): {item.value}")

This returns the header item description, it’s mnemonic and its value as a formatted string:

Top Depth (STRT): 102.1568
Bottom Depth (STOP): 4636.514
Depth Increment (STEP): 0.1524
Null Value (NULL): -999.25
Field Name (FLD): Q15
NAME (WELL): 15/9-19
WELLBORE (WBN): 15/9-19 SR
COUNTRY (NATI): NOR
COUNTRY (CTRY): NOR
OPERATOR (COMP): STATOIL
PERM DATUM (PDAT): MSL
RIG NAME (COUN): NORTH SEA
STATE (STAT): NORWAY
PB WELL ID (PBWE): 15/9-19
PB WELLBORE ID (APIN): 15/9-19 SR
PB WELL NAME SET (PBWS): ALL

If we just want to extract the Well Name, we can simply call it by:

print(las.well.WELL.value)

Which returns:

'15/9-19'

To see what curves are present within the las file, we can repeat the process with the CurveItem object and call upon the `unit` and `descr` functions to get info on the units and the curve’s description.

The enumerate function allows us to keep a count of the number of curves that are present within the file. As enumerate returns a 0 on the first loop, we need to 1 to it if we want to include the depth curve.

for count, curve in enumerate(las.curves):
print(f"Curve: {curve.mnemonic}, Units: {curve.unit}, Description: {curve.descr}")
print(f"There are a total of: {count+1} curves present within this file")

Which gives us:

Curve: DEPT, Units: M, Description: 1  DEPTH
Curve: AC, Units: US/F, Description: 2 Sonic Transit Time (Slowness)
Curve: CALI, Units: IN, Description: 3 Caliper
Curve: DEN, Units: G/CC, Description: 4 Bulk Density
Curve: GR, Units: GAPI, Description: 5 Gamma Ray
Curve: NEU, Units: %, Description: 6 Neutron Porosity
Curve: RDEP, Units: OHMM, Description: 7 Deep Resistivity
Curve: RMED, Units: OHMM, Description: 8 Medium Resistivity
There are a total of: 8 curves present within this file

Creating a Pandas Dataframe

Data loaded in using LASIO can be converted to a pandas dataframe using the .df() function. This allows us to easily work with the data, plot the data or pass it into one of the many machine learning algorithms that are used for prediction.

well = las.df()
well.head()

We can view the contents of our new dataframe by using the .head() function. This generates a table view of the header and the first 5 rows within the dataframe.

To find out more information about data, we can call upon the .info() and .describe() functions.

  • The info() function provides information about the data types and how many non-null values are present within each curve.
  • The .describe() function, provides statistical information about each curve and can be a useful QC for each curve.
well.describe() provides statistical info about the curves within the dataframe.
well.info() provides information about nulls and data types.

Visualising Data Extent

Instead of the summary provided by the pandas describe() function, we can create a visualisation using matplotlib which will be more useful at understanding where data exists within this particular LAS file.

Firstly, we need to work out where we have nulls (nan values). We can do this by creating a second dataframe and calling .notnull() on our well dataframe.

well_nan = well.notnull() * 1

As this returns a boolean (True or False) for each depth, we can multiply by 1 to convert the values from True and False to 1 and 0 respectively.

Now that we have created a dataframe, we can now create the plot.

fig = plt.subplots(figsize=(7,10))#Set up the plot axes
ax1 = plt.subplot2grid((1,7), (0,0), rowspan=1, colspan = 1)
ax2 = plt.subplot2grid((1,7), (0,1), rowspan=1, colspan = 1)
ax3 = plt.subplot2grid((1,7), (0,2), rowspan=1, colspan = 1)
ax4 = plt.subplot2grid((1,7), (0,3), rowspan=1, colspan = 1)
ax5 = plt.subplot2grid((1,7), (0,4), rowspan=1, colspan = 1)
ax6 = plt.subplot2grid((1,7), (0,5), rowspan=1, colspan = 1)
ax7 = plt.subplot2grid((1,7), (0,6), rowspan=1, colspan = 1)
columns = well_nan.columns
axes = [ax1, ax2, ax3, ax4, ax5, ax6, ax7]
for i, ax in enumerate(axes):
ax.plot(well_nan.iloc[:,i], well_nan.index, lw=0)
ax.set_ylim(5000, 0)
ax.set_xlim(0, 1)
ax.set_title(columns[i])
ax.set_facecolor('whitesmoke')
ax.fill_betweenx(well_nan.index, 0, well_nan.iloc[:,i], facecolor='red')
# Remove tick labels from each subplot
if i > 0:
plt.setp(ax.get_yticklabels(), visible = False)
plt.setp(ax.get_xticklabels(), visible = False)
ax1.set_ylabel('Depth', fontsize=14)plt.subplots_adjust(wspace=0)
plt.show()

This gives us the following plot. Red shaded areas indicate where we have data, and the grey areas indicate where we don’t.

Plotting Log Data

Finally, we can plot our data using the code below. Essentially, the code is building up a series of subplots and plotting the data on the relevant tracks.

When we add curves to the tracks, we need to set the curve’s properties, including the limits, colour and labels. We can also specify the shading between curves. An example has been added to the caliper curve to show shading between a bitsize value (8.5") and the CALI curve.

If there are a number of features that are common between the plots, we can iterate over them using a for loop. For example, the grid setup or any limits that are commonly shared between the axes.

fig, ax = plt.subplots(figsize=(15,10))#Set up the plot axes
ax1 = plt.subplot2grid((1,6), (0,0), rowspan=1, colspan = 1)
ax2 = plt.subplot2grid((1,6), (0,1), rowspan=1, colspan = 1, sharey = ax1)
ax3 = plt.subplot2grid((1,6), (0,2), rowspan=1, colspan = 1, sharey = ax1)
ax4 = plt.subplot2grid((1,6), (0,3), rowspan=1, colspan = 1, sharey = ax1)
ax5 = ax3.twiny() #Twins the y-axis for the density track with the neutron track
ax6 = plt.subplot2grid((1,6), (0,4), rowspan=1, colspan = 1, sharey = ax1)
ax7 = ax2.twiny()
# As our curve scales will be detached from the top of the track,
# this code adds the top border back in without dealing with splines
ax10 = ax1.twiny()
ax10.xaxis.set_visible(False)
ax11 = ax2.twiny()
ax11.xaxis.set_visible(False)
ax12 = ax3.twiny()
ax12.xaxis.set_visible(False)
ax13 = ax4.twiny()
ax13.xaxis.set_visible(False)
ax14 = ax6.twiny()
ax14.xaxis.set_visible(False)
# Gamma Ray track
ax1.plot(well["GR"], well.index, color = "green", linewidth = 0.5)
ax1.set_xlabel("Gamma")
ax1.xaxis.label.set_color("green")
ax1.set_xlim(0, 200)
ax1.set_ylabel("Depth (m)")
ax1.tick_params(axis='x', colors="green")
ax1.spines["top"].set_edgecolor("green")
ax1.title.set_color('green')
ax1.set_xticks([0, 50, 100, 150, 200])
# Resistivity track
ax2.plot(well["RDEP"], well.index, color = "red", linewidth = 0.5)
ax2.set_xlabel("Resistivity - Deep")
ax2.set_xlim(0.2, 2000)
ax2.xaxis.label.set_color("red")
ax2.tick_params(axis='x', colors="red")
ax2.spines["top"].set_edgecolor("red")
ax2.set_xticks([0.1, 1, 10, 100, 1000])
ax2.semilogx()
# Density track
ax3.plot(well["DEN"], well.index, color = "red", linewidth = 0.5)
ax3.set_xlabel("Density")
ax3.set_xlim(1.95, 2.95)
ax3.xaxis.label.set_color("red")
ax3.tick_params(axis='x', colors="red")
ax3.spines["top"].set_edgecolor("red")
ax3.set_xticks([1.95, 2.45, 2.95])
# Sonic track
ax4.plot(well["AC"], well.index, color = "purple", linewidth = 0.5)
ax4.set_xlabel("Sonic")
ax4.set_xlim(140, 40)
ax4.xaxis.label.set_color("purple")
ax4.tick_params(axis='x', colors="purple")
ax4.spines["top"].set_edgecolor("purple")
# Neutron track placed ontop of density track
ax5.plot(well["NEU"], well.index, color = "blue", linewidth = 0.5)
ax5.set_xlabel('Neutron')
ax5.xaxis.label.set_color("blue")
ax5.set_xlim(45, -15)
ax5.set_ylim(4150, 3500)
ax5.tick_params(axis='x', colors="blue")
ax5.spines["top"].set_position(("axes", 1.08))
ax5.spines["top"].set_visible(True)
ax5.spines["top"].set_edgecolor("blue")
ax5.set_xticks([45, 15, -15])
# Caliper track
ax6.plot(well["CALI"], well.index, color = "black", linewidth = 0.5)
ax6.set_xlabel("Caliper")
ax6.set_xlim(6, 16)
ax6.xaxis.label.set_color("black")
ax6.tick_params(axis='x', colors="black")
ax6.spines["top"].set_edgecolor("black")
ax6.fill_betweenx(well_nan.index, 8.5, well["CALI"], facecolor='yellow')
ax6.set_xticks([6, 11, 16])
# Resistivity track - Curve 2
ax7.plot(well["RMED"], well.index, color = "green", linewidth = 0.5)
ax7.set_xlabel("Resistivity - Med")
ax7.set_xlim(0.2, 2000)
ax7.xaxis.label.set_color("green")
ax7.spines["top"].set_position(("axes", 1.08))
ax7.spines["top"].set_visible(True)
ax7.tick_params(axis='x', colors="green")
ax7.spines["top"].set_edgecolor("green")
ax7.set_xticks([0.1, 1, 10, 100, 1000])
ax7.semilogx()
# Common functions for setting up the plot can be extracted into
# a for loop. This saves repeating code.
for ax in [ax1, ax2, ax3, ax4, ax6]:
ax.set_ylim(4500, 3500)
ax.grid(which='major', color='lightgrey', linestyle='-')
ax.xaxis.set_ticks_position("top")
ax.xaxis.set_label_position("top")
ax.spines["top"].set_position(("axes", 1.02))


for ax in [ax2, ax3, ax4, ax6]:
plt.setp(ax.get_yticklabels(), visible = False)

plt.tight_layout()
fig.subplots_adjust(wspace = 0.15)
plot.show()

When we run the code, we generate the following log plot that we are all familiar with.

That concludes this article and I hope it has given you some insight on how to load data from a LAS file, check for nulls and display the data on a log plot.

Petrophysicist | Geoscientist | Data Scientist with a strong interest in data analytics, machine learning and artificial intelligence.