10 Steps in Pandas to Process LAS File and Plot(part1) | by Ryan A. Mardani | Towards Data Science

Log ASCII Standard (LAS) is a popular format to store well log information in the oil and gas industry. Logs are physical properties of subsurface rocks measured through depth intervals. There are several commercial software that can read this format. LAS viewers are sometimes free but with limited capability. If you want to implement data analysis or advanced mathematical calculation you will have to purchase a license.
The following list of steps I use most:1) Reading File2) Data Inspection3) Column Selection4) Missing Data5) Dropping Rows6) Statistics7) Filtering8) Adding Column9) Indexing10) Plotting
If you want to use my example data, you may access the free dataset of The University of Kansas. Download the zipped file of AUG, 2019 and extract in your favorite hard drive. Then copy file number 1050383876 into working Jupyter notebook directory to read. You may also access to full Jupyter notebook source file of this work and data directly from my Github account.
LAS format files cannot be read with a common library in python like pandas. First, we need to install lasio lib. lasio is written to be compatible with python and has NumPy and ordereddict dependency. Run the following code to install:
pip install lasiopip install -r optional-packages.txtpip install --upgrade lasio
After importing pandas, numpy and lasio, read the LAS file as below :
import pandas as pdimport numpy as npimport lasiolas = lasio.read(r'1050383876.las')
lasio has great features such as curves that will show log information in the header part of the LAS file. Run:
las.curves
Using this command, we can see the available curve logs with fundamental information such as units and sample data points quantity in the file. lasio has other functions but I personally prefer to convert LAS file into a pandas data frame to get advantages of its popular functions:
df = las.df()    #store las file in df variable as pandas dataframe
Using head() function in pandas, we can see the 5 rows and full columns if fits screen.
df.head()
df.shape
data frame has 17 log data in columns with 7056 rows as sample points in depth interval. We may consider that shallower part of well is logged just by GR and the rest has null values. The LAS standard for the null value is -999.2500.
Based on your project purpose, you may select different variables in the dataset. First, let’s look at the available logs in this file:
df.columns
From these logs, I would rather select columns of neutron porosity, Gamma Ray, Bulk Density, Sonic, Caliper and calculated Sonic porosity and store in a new variable. Remember to use double brackets as follows :
df_selected = df[['CNPOR', 'GR', 'RHOB', 'DT', 'MELCAL', 'SPOR']]
Missing values are very common in LAS files because depth of interest may vary for specific measurements. Printed values of -999.2500 in LAS files means null value. Missing values are not common in the middle part of the dataset. Usually, it happens in the head or tail of a file. isna function returns True if the location has a null value, otherwise, it will be False. We may add up these boolean values using sum() function as below:
df_selected.isna().sum()
Let’s suppose that we want to preserve GR, DT, and SPOR as important logs in the data set. So, to get rid of missing values we can drop rows that one of those logs is missed in that row.
df_dropped = df_selected.dropna(subset=['GR', 'DT', 'SPOR'],axis=0, how='any')
This line of code will drop all rows that one (or more) of the subset logs (‘GR’, ‘DT’, ‘SPOR’) has a null value.
Run describe command to see data statistics. This can be helpful to see the data range and outliers.
df_dropped.describe()
Petrophysical properties of rocks have a normal range. The readings out of that range should not be credited for validity. These ranges are:
Comparing this table with our file’s statistics shows we have some values out of range and we can filter the dataset. For example, the maximum value of ‘SPOR’ is 162 percent while it is an impossible amount in nature.
There are several ways to filter data such as zscore. In this method, we can get rid of values that out of a specific range of standard deviation such as 2 or more. I personally prefer high/low cut filter because I can control the minimum and maximum values as:
df_filt1 = df_dropped.loc[(df_dropped.CNPOR > -15) & (df_dropped.CNPOR <= 50)]df_filt2 = df_filt1.loc[(df_dropped.GR > 0) & (df_dropped.GR  <= 250)]df_filt3 = df_filt2.loc[(df_dropped.RHOB> 1) & (df_dropped.RHOB<= 3)]df_filt = df_filt3.loc[(df_dropped.DT > 30) & (df_dropped.DT <= 140)]
We do not need to filter SPOR as it is a function of DT.
Sometimes we need to calculate a parameter using existing logs and store in a separate column. Let’s calculate the shale volume using the simplest equation as:
df = df_filt.copy()      #make a copy of the latest datasetdf['Vsh'] = (df.GR - df.GR.min()) / (df.GR.max() - df.GR.min())
Shale volume is added as a column named Vsh.
lasio converts the depth column as an index when we read the LAS file. We can call depth as a new column for plotting aim and reindex dataset.
df_idx = df.rename_axis('Depth').reset_index()df_idx.head()
Plotting in python is a comprehensive discussion but I will use the simplest method to visualize my final data set as processed.
import matplotlib.pyplot as plt
To make code readable and re-usable let’s define a function:
def log_plot(logs):    logs = logs.sort_values(by='Depth')    top = logs.Depth.min()    bot = logs.Depth.max()        f, ax = plt.subplots(nrows=1, ncols=6, figsize=(12,8))    ax[0].plot(logs.GR, logs.Depth, color='green')    ax[1].plot(logs.CNPOR, logs.Depth, color='red')    ax[2].plot(logs.DT, logs.Depth, color='black')    ax[3].plot(logs.MELCAL, logs.Depth, color='blue')    ax[4].plot(logs.RHOB, logs.Depth, color='c')    ax[5].plot(logs.Vsh, logs.Depth, color='m')        for i in range(len(ax)):        ax[i].set_ylim(top,bot)        ax[i].invert_yaxis()        ax[i].grid()            ax[0].set_xlabel("GR")    ax[0].set_xlim(logs.GR.min(),logs.GR.max())    ax[0].set_ylabel("Depth(ft)")    ax[1].set_xlabel("CNPOR")    ax[1].set_xlim(logs.CNPOR.min(),logs.CNPOR.max())    ax[2].set_xlabel("DT")    ax[2].set_xlim(logs.DT.min(),logs.DT.max())    ax[3].set_xlabel("MELCAL")    ax[3].set_xlim(logs.MELCAL.min(),logs.MELCAL.max())    ax[4].set_xlabel("RHOB")    ax[4].set_xlim(logs.RHOB.min(),logs.RHOB.max())    ax[5].set_xlabel("Vsh")    ax[5].set_xlim(logs.Vsh.min(),logs.Vsh.max())        ax[1].set_yticklabels([]); ax[2].set_yticklabels([]);    ax[3].set_yticklabels([])    ax[4].set_yticklabels([]); ax[5].set_yticklabels([])         f.suptitle('Well:KOOCHEL MOUNTAIN #1', fontsize=14,y=0.94)
Call the function like:
log_plot(df_idx)
In this work, I read the LAS file of a single well which included several petrophysical logs. Converting to a pandas data frame makes it easy to work with and process fluently. These steps can be implemented in different orders.
If you prefer other ways, suggest improvements, or grabbling with some difficulties with similar data sets, please leave a comment!