# Plotly and Python: Creating Interactive Heatmaps for Petrophysical & Geological Data | by Andy McDonald | Jun, 2023

## Visualising Geospatial Variations in Well Log Measurements Within the Subsurface

Interpreting the subsurface requires understanding how geological and petrophysical data varies across a region. This often involves dealing with well log measurements and interpreted properties scattered across the area, which leads to the challenge of estimating the values between these measurements.

One way that we can estimate the values (or fill in the gaps) is by using a geostatistical method called kriging. This method estimates and extrapolates data between observed measurements and predicts values at unmeasured locations.

In my previous article, we focused on using pykrige and matplotlib to map and visualise geological variation across the Norwegian continental shelf. This article will take that visualisation further and make those plots interactive.

Before we use Plotly, we will quickly recap the code used in the previous article so that you are up to speed with the process.

The first step is to import the libraries that we require. In this case, we need pandas for loading our csv data, pykrige to carry out the interpolation between the data points, and numpy to carry out a few mathematical operations.

`import pandas as pdfrom pykrige import OrdinaryKrigingimport numpy as npdf = pd.read_csv('Data/Xeek Force 2020/Xeek_2020_Balder_DTC_AVG.csv')`

Once the data has been loaded, we can carry out the kriging process by calling upon pykrige’s `OrdinaryKriging` method.

Within this call, we pass in our x and y data, representing our data’s latitude and longitude. We also need to pass in the variable we want to extrapolate. In this case, we are using the average Acoustic Compressional Slowness (DTC) value for the Balder Formation.

Once the model has been generated, we can apply it to custom latitude and longitude ranges that cover the locations of the wells.

`OK = OrdinaryKriging(x=df['LON'], y=df['LAT'], z=df['DTC_MEAN'],variogram_model='exponential',verbose=True, enable_plotting=True,coordinates_type='geographic')grid_lat = np.arange(57.5, 62, 0.01, dtype='float64')grid_long = np.arange(1.5, 4.5, 0.01,dtype='float64')zstar, ss = OK.execute('grid', grid_long, grid_lat)zstar`

We then take our two positional arrays, `grid_lat` and `grid_long` and our gridded data and pass them into a matplotlib `imshow` plot. This will generate a plot similar to the one below.

Even though the figure we returned tells a story about trends in our data, it is difficult to identify specific wells and any of the values between the measurement points.

One way to instantly change this would be to use the Plotly library. Plotly is a great library for creating highly interactive charts that are easy to put together.

Plotly comes with two main ways in which to construct plots: Plotly Express and Plotly Graph Objects.

Plotly Express provides a high-level interface for Plotly, and utilises simple syntax for creating powerful interactive charts. However, customising certain aspects of the plot can take a lot of work and can be difficult to do. This is where the Graph Objects part of the library comes into play. It provides a low-level interface which provides complete control over your figures; however, it does mean that putting a figure together is slightly more complex.

For this example, we will be using Graph Objects, which can be imported as follows:

`import plotly.graph_objects as go`

Next, we can define our `x` and `y` arrays using numpy’s `linspace` function.

This will create two arrays the same size as the data grid we created earlier.

We will also create two lists for longitude and latitude. These values extend beyond the data’s longitude and latitude values and allow us to have padding around the edge of our data points.

`longitude = [1.5, 4.5]latitude = [57.5, 62]x = np.linspace(longitude[0], longitude[1], zstar.shape[1])y = np.linspace(latitude[0], latitude[1], zstar.shape[0])`

When using matplotlib, we could display this type of data using `imshow`.

Even though Plotly also has an `imshow` plot, we are not (as far as I am aware at the time of writing) able to control the extent of the graph. This means we can’t specify the values for the starting points of the axes.

Therefore, to display our data grid, we can switch to using Plotly’s heatmap.

The heatmap colours each data cell within our grid based on its value. You can find out more about heatmaps in my article on Seaborn.

We can use the following code to create our heatmap with Plotly Graph Objects.

`fig = go.Figure()fig.add_trace(go.Heatmap(z=zstar, x=x, y=y))fig.update_xaxes(range=(longitude[0], longitude[1]))fig.update_yaxes(range=(latitude[0], latitude[1]))fig.update_layout(autosize=False, width=800, height=800)fig.show()`

First, we create a figure object and then add a trace to it. This trace contains our `x` and `y` location data, as well as our grid (`zstar`)created by kriging.

We will also set the size of the figure to 800 x 800, which will give us a large enough plot to work with inside Jupyter notebooks.

After running the above code, we get the heatmap with all our data values and the axes displayed within the correct range.

The great thing about this plot is that we can hover over it and view the values at any point. Additionally, Plotly allows us to zoom in on sections for a closer look.

Even though the above plot is great, we are lacking additional information which would help the reader, such as our well locations and also labels for our axes.

To add our well locations, we need to add a second trace. This time using `go.scatter()` and passing in the latitude and longitude values from our dataframe. We can also control how these points appear by adding a dictionary for our markers. In this example, we will set them to black.

`fig = go.Figure()fig.add_trace(go.Heatmap(z=zstar, x=x, y=y))# Add Well Locationsfig.add_trace(go.Scatter(x=df['LON'], y=df['LAT'],mode='markers', marker=dict(color='black')))fig.update_xaxes(range=(longitude[0], longitude[1]))fig.update_yaxes(range=(latitude[0], latitude[1]))fig.update_layout(autosize=False, width=800, height=800)fig.show()`

Now, we can see where our wells are located; however, if we hover over the markers, all we get back is the latitude and longitude values. This is useful to a certain extent; however, it would be nice to know what well the marker represents and what value of DTC was measured for that well.

To solve that, we can create our hover text directly within the dataframe as a new column. This is useful if we want to use it later for other plots.

`fig = go.Figure()fig.add_trace(go.Heatmap(z=zstar, x=x, y=y, colorbar=dict(title='DTC (us/ft)',title_font=dict(size=18))))df['hover_text'] = df.apply(lambda row: f"""<b>{row['WELL']}</b><br>Latitude: {row['LAT']}<br>Longitude: {row['LON']}<br>Log Value: {round(row['DTC_MEAN'], 2)}""",axis=1)fig.add_trace(go.Scatter(x=df['LON'], y=df['LAT'],mode='markers', marker=dict(color='black'),name='Wells', text=df['hover_text'], hoverinfo='text', showlegend=True))fig.update_xaxes(range=(longitude[0], longitude[1]), title='Longitude')fig.update_yaxes(range=(latitude[0], latitude[1]), title='Latitude')fig.update_layout(autosize=False, width=800, height=800, legend=dict(x=1, y=0, xanchor='auto', yanchor='auto', bgcolor='rgba(255, 255, 255, 0.5)'))fig.show()`

When we run the above code, we get back the following plot.

Now, when we hover over any of the wells, we will get the well name, followed by the latitude and longitude, and the log value. In this case, we are displaying the acoustic compressional slowness.

In this short tutorial, we have seen how we can go beyond a simple and static matplotlib figure of our measurement variation. The extra functionality and interactivity from Plotly makes it a great choice to visualise geospatial variations in well log measurements.

The extra interactivity allows us to identify what well each of the dots represents, what the measured value was at that location, and interpret the values of the grid that do not have a direct measurement.

The dataset used in this article is a subset of a training dataset used as part of a Machine Learning competition run by Xeek and FORCE 2020 (Bormann et al., 2020). It is released under a NOLD 2.0 licence from the Norwegian Government, details of which can be found here: Norwegian Licence for Open Government Data (NLOD) 2.0. The full dataset can be accessed here.

The full reference for the dataset is:

Bormann, Peter, Aursand, Peder, Dilib, Fahad, Manral, Surrender, & Dischington, Peter. (2020). FORCE 2020 Well well log and lithofacies dataset for machine learning competition [Data set]. Zenodo. http://doi.org/10.5281/zenodo.4351156