Using Line Plots from Matplotlib to Create Simple Log Plots of Well Log Data | by Andy McDonald | Towards Data Science

Well log plots are a common visualization tool within geoscience and petrophysics. They allow easy visualization of data (for example, Gamma Ray, Neutron Porosity, Bulk Density, etc) that have been acquired along the length (depth) of a wellbore. On these plots, we display our logging measurements on the x-axis and measured depth or true vertical depth on the y-axis.
In this short article, we will see how to create a simple log plot visualisation from one of the Volve Wells that was released as part of a larger dataset by Equinor in 2018.
I have previously covered different aspects of making these plots in the following articles:
Displaying Lithology Data on a Well Log Plot Using Python
Displaying Logging While Drilling (LWD) Image Logs in Python
Enhancing Visualization of Well Logs With Plot Fills
For this tutorial, the notebook can be found here and the following video also accompanies it.
The first stage of any python project or notebook is to import the required libraries. In this case, we are going to be using lasio to load our las file, pandas for storing our well log data, and matplotlib for visualising our data.
import pandas as pdimport lasioimport matplotlib.pyplot as plt
To read the data we will use the lasio library which we explored in the previous notebook and video.
las = lasio.read("Data/15-9-19_SR_COMP.LAS")
Once the file has been loaded, we can check the contents of the file by using df.head() . This will return back the first five rows of our dataframe.
df = las.df()df.head()
We can see from the returned results that we have several columns of data, each column represents measurements that have been taken whilst the logging tools have been moved along the well.
The columns represent the following:
AC for acoustic compressional slowness
CALI for borehole caliper
DEN for bulk density
GR for gamma ray
NEU for neutron porosity
RDEP for deep resisitivity
RMED for medium resistivity
To make it easier to work with our dataframe, we can convert the dataframe index, which is set to depth, to a column within the dataframe. We can achieve this by resetting the index like so.
df.reset_index(inplace=True)
Note that inplace=True allows us to make the changes to the original dataframe object.
We can call upon df.head() again to ensure our new column has been created.
df.head()
We also need to do a slight rename on the DEPT column and change it to DEPTH
df.rename(columns={'DEPT':'DEPTH'}, inplace=True)df.head()
Now that our data is in the right format, and the columns are correctly labeled, we can move onto generating our log plot.
We can easily create a simple plot by calling upon df.plot() and passing in two of our columns
df.plot('GR', 'DEPTH')
When we run this cell, we get a very simple plot that is hard to read and is also upside down.
If we want to view all of the columns within the dataframe, we can generate a subplot grid.
This is done by taking the same line as before (df.plot()), and instead of passing in curve names, we pass in subplots=True. We can also specify a figure size (figsize(), which controls how large the plot will appear.
df.plot(subplots=True, figsize=(15, 15))
And we now see a grid of plots, one for each of the columns within the dataframe. This is a useful way to check where we have data and where we may have gaps.
However, we do not have much control over this plot. In the following sections we will see how we can start building up a well log plot with multiple measurements.
There are many ways to generate subplots in using matplotlib. For this particular tutorial, we will work with subplot2grid.
fig = plt.subplots(figsize=(10,10))#Set up the plot axisax1 = plt.subplot2grid((1,1), (0,0), rowspan=1, colspan = 1) ax1.plot("GR", "DEPTH", data = df, color = "green") # Call the data from the df dataframeax1.set_xlabel("Gamma") # Assign a track titleax1.set_xlim(0, 200) # Change the limits for the curve being plottedax1.set_ylim(4700, 3500) # Set the depth rangeax1.grid() # Display the grid
In the above code, we first have to define a figure (fig) and assign it to plt.subplots. Within the subplots arguments we can pass in the figure size, which we will set to 10 by 10.
Next, we need to create an axis on our figure object. We can do this by assigning ax1 is equal to plt.subplot2grid()..We first pass in the shape of the subplot grid, which for this example we will set 1 by 1.
Next we specify the location of our subplot using index positions. As we only have 1 subplot, we will set the location to zero, zero (0,0). Rowspan and colspan are both set to 1, which means they will only be 1 column wide by 1 row high.
Then, we need to tell matplotlib what we want to plot. In this case we are going use ax1.plot, and pass in our Gamma Ray and Depth columns.
Using the ax notation, we can further customise the plot by setting the x and y limits, and displaying the grid.
When we run this code we generate the following plot.
Great! There we have a much better looking plot, and we can now start adding new tracks / subplots to our overall log plot.
To add a new track / subplot to our figure, we can repeat what we did above and add a new axis, ax2.
We then increment the second digit in the layout argument of plt.subplot2grid() so it is now 1 row, by 2 columns.
For ax2, we need to place it in the second subplot and this is done by changing the location argument from (0,0) to (0,1).
We then end up with:
ax1 = plt.subplot2grid((1,2), (0,0), rowspan=1, colspan = 1) ax2 = plt.subplot2grid((1,2), (0,1), rowspan=1, colspan = 1)
Also, as resistivity is normally scaled logarithmically, we need to add in the line: ax2.semilogx().
fig = plt.subplots(figsize=(10,10))#Set up the plot axesax1 = plt.subplot2grid((1,2), (0,0), rowspan=1, colspan = 1) ax2 = plt.subplot2grid((1,2), (0,1), rowspan=1, colspan = 1)ax1.plot("GR", "DEPTH", data = df, color = "green") # Call the data from the well dataframeax1.set_xlabel("Gamma") # Assign a track titleax1.set_xlim(0, 200) # Change the limits for the curve being plottedax1.set_ylim(4700, 3500) # Set the depth rangeax1.grid() # Display the gridax2.plot("RDEP", "DEPTH", data = df, color = "red")ax2.set_xlabel("Deep Res.")ax2.set_xlim(0.2, 2000)ax2.semilogx()ax2.set_ylim(4700, 3500)ax2.grid()
To add a third track / subplot, we can repeat the above by adding in the density as a new subplot.
fig = plt.subplots(figsize=(10,10))#Set up the plot axesax1 = plt.subplot2grid((1,3), (0,0), rowspan=1, colspan = 1) ax2 = plt.subplot2grid((1,3), (0,1), rowspan=1, colspan = 1)ax3 = plt.subplot2grid((1,3), (0,2), rowspan=1, colspan = 1)ax1.plot("GR", "DEPTH", data = df, color = "green") # Call the data from the well dataframeax1.set_xlabel("Gamma") # Assign a track titleax1.set_xlim(0, 200) # Change the limits for the curve being plottedax1.set_ylim(4700, 3500) # Set the depth rangeax1.grid() # Display the gridax2.plot("RDEP", "DEPTH", data = df, color = "red")ax2.set_xlabel("Deep Res.")ax2.set_xlim(0.2, 2000)ax2.semilogx()ax2.set_ylim(4700, 3500)ax2.grid()ax3.plot("DEN", "DEPTH", data = df, color = "red")ax3.set_xlabel("Density")ax3.set_xlim(1.95, 2.95)ax3.set_ylim(4700, 3500)ax3.grid()
We can see from the previous code snippet that we have a number of elements that are repeated in each of the axis calls such as ax.set_ylim(4700, 3500). We can separate these out so that we only need to call these functions once. This saves on the amount of lines we need to write and makes the code more readable.
To achieve this, we add in a new for loop which will loop over the axes within fig.axes.
for i, ax in enumerate(fig.axes):    ax.set_ylim(4700, 3500) # Set the depth range    ax.grid()    ax.set_xlabel(curve_names[i])
Our final code, after separating out the common elements looks like this:
fig, axes = plt.subplots(figsize=(10,10))curve_names = ['Gamma', 'Deep Res', 'Density']#Set up the plot axesax1 = plt.subplot2grid((1,3), (0,0), rowspan=1, colspan = 1) ax2 = plt.subplot2grid((1,3), (0,1), rowspan=1, colspan = 1)ax3 = plt.subplot2grid((1,3), (0,2), rowspan=1, colspan = 1)ax1.plot("GR", "DEPTH", data = df, color = "green")ax1.set_xlim(0, 200) ax2.plot("RDEP", "DEPTH", data = df, color = "red")ax2.set_xlim(0.2, 2000)ax2.semilogx()ax3.plot("DEN", "DEPTH", data = df, color = "red")ax3.set_xlim(1.95, 2.95)for i, ax in enumerate(fig.axes):    ax.set_ylim(4700, 3500) # Set the depth range    ax.grid()    ax.set_xlabel(curve_names[i])
To tidy the plot up a little more, we can remove the depth labels in between each subplot / track and reduce the space between them. This is achieved by the looping over only ax2 and ax3, and by adjusting the width of the padding between plots.
#Hide tick labels on the y-axis for ax in [ax2, ax3]:    plt.setp(ax.get_yticklabels(), visible = False)#Reduce the space between each subplotfig.subplots_adjust(wspace = 0.05)
We then end up with the following:
fig, axes = plt.subplots(figsize=(10,10))curve_names = ['Gamma', 'Deep Res', 'Density']#Set up the plot axesax1 = plt.subplot2grid((1,3), (0,0), rowspan=1, colspan = 1) ax2 = plt.subplot2grid((1,3), (0,1), rowspan=1, colspan = 1)ax3 = plt.subplot2grid((1,3), (0,2), rowspan=1, colspan = 1)#Set up the individual log tracks / subplotsax1.plot("GR", "DEPTH", data = df, color = "green")ax1.set_xlim(0, 200) ax2.plot("RDEP", "DEPTH", data = df, color = "red")ax2.set_xlim(0.2, 2000)ax2.semilogx()ax3.plot("DEN", "DEPTH", data = df, color = "red")ax3.set_xlim(1.95, 2.95)#Set up the common elements between the subplotsfor i, ax in enumerate(fig.axes):    ax.set_ylim(4700, 3500) # Set the depth range    ax.grid()    ax.set_xlabel(curve_names[i])#Hide tick labels on the y-axis for ax in [ax2, ax3]:    plt.setp(ax.get_yticklabels(), visible = False)#Reduce the space between each subplotfig.subplots_adjust(wspace = 0.05)
It is standard to plot bulk density and neutron porosity on the same subplot / track. The interaction of these two curves allows us to identify lithology variations and hydrocarbon presence.
As the two measurements are on different units and scales (1.95 to 2.95 g/cc for Bulk Density and -15 to 60 for Neutron Porosity) we need to add another subplot on top using the twiny function. This allows us to use the same y-axis between the plots, but the x-axis can vary.
All labels have been moved to the top of the plot which is done by using ax.xaxis.set_ticks_position('top') and ax.xaxis.set_label_position('top').
We then need to modify the main for loop to check when we reach ax4 (which will be when i = 3, as Python indexes from 0) and then adjust the spine so that it sits above the Density label.
fig, axes = plt.subplots(figsize=(10,10))curve_names = ['Gamma', 'Deep Res', 'Density', 'Neutron']#Set up the plot axesax1 = plt.subplot2grid((1,3), (0,0), rowspan=1, colspan = 1) ax2 = plt.subplot2grid((1,3), (0,1), rowspan=1, colspan = 1)ax3 = plt.subplot2grid((1,3), (0,2), rowspan=1, colspan = 1)ax4 = ax3.twiny()#Set up the individual log tracks / subplotsax1.plot("GR", "DEPTH", data = df, color = "green", lw = 0.5)ax1.set_xlim(0, 200) ax2.plot("RDEP", "DEPTH", data = df, color = "red", lw = 0.5)ax2.set_xlim(0.2, 2000)ax2.semilogx()ax3.plot("DEN", "DEPTH", data = df, color = "red", lw = 0.5)ax3.set_xlim(1.95, 2.95)ax4.plot("NEU", "DEPTH", data = df, color = "blue", lw = 0.5)ax4.set_xlim(45, -15)#Set up the common elements between the subplotsfor i, ax in enumerate(fig.axes):    ax.set_ylim(4700, 3500) # Set the depth range        ax.xaxis.set_ticks_position("top")    ax.xaxis.set_label_position("top")    ax.set_xlabel(curve_names[i])        if i == 3:        ax.spines["top"].set_position(("axes", 1.08))    else:        ax.grid()        #Hide tick labels on the y-axis for ax in [ax2, ax3]:    plt.setp(ax.get_yticklabels(), visible = False)#Reduce the space between each subplotfig.subplots_adjust(wspace = 0.05)
In this short tutorial, we have covered the basics of how to display a well log plot using matplotlib, how to add multiple tracks/subplots, and plot two curves on top of each other. Matplotlib provides a great way to build simple log plots from scratch and is a great library to learn.
Thanks for reading!
If you have found this article useful, please feel free to check out my other articles looking at various aspects of Python and well log data. You can also find my code used in this article and others at GitHub.
If you want to get in touch you can find me on LinkedIn or at my website.
Interested in learning more about python and well log data or petrophysics? Follow me on Medium.