Edit – Updated Answer
The cloupy
package is not supported anymore due to outdated functionalities & unmaintainable code. See the README in the package repository for more information.
However, there is a new tool that perfectly addresses this problem – the GeoKrige
package. This section of the documentation is especially helpful when it comes to creating interpolation maps in Python.
Creating interpolation map with GeoKrige
The GeoKrige package enables interpolation of data using Kriging Methods. To gain a better understanding of the GeoKrige package and Kriging Methods themselves, please refer to the GeoKrige documentation. A good starting point is this section.
1. Transform the data
Transform the data into numpy.ndarray
objects, so that the GeoKrige
package can operate on it:
import numpy as np
data = '''
1 -25.53 -48.51 4.50
2 -25.30 -48.49 59.00
3 -25.16 -48.32 40.00
4 -25.43 -49.26 923.50
5 -24.49 -49.15 360.00
6 -25.47 -49.46 910.00
7 -23.30 -49.57 512.00
8 -25.13 -50.01 880.00
9 -23.00 -50.02 450.00
10 -23.06 -50.21 440.00
11 -24.78 -50.00 1008.80
12 -25.27 -50.35 893.00
13 -24.20 -50.37 768.00
14 -23.16 -51.01 484.00
15 -22.57 -51.12 600.00
16 -23.54 -51.13 1020.00
17 -25.21 -51.30 1058.00
18 -23.30 -51.32 746.00
19 -26.29 -51.59 1100.00
20 -26.25 -52.21 930.00
21 -25.25 -52.25 880.00
22 -23.31 -51.13 566.00
23 -23.40 -51.91 542.00
24 -23.05 -52.26 480.00
25 -24.40 -52.34 540.00
26 -24.05 -52.36 616.40
27 -23.40 -52.35 530.00
28 -26.07 -52.41 700.00
29 -25.31 -53.01 513.00
30 -26.05 -53.04 650.00
31 -23.44 -53.17 480.00
32 -24.53 -53.33 660.00
33 -25.42 -53.47 400.00
34 -24.18 -53.55 310.00
'''
lines = data.strip().splitlines()
values = [[float(x) for x in line.split()[1:]] for line in lines]
data = np.array(values)
X = np.column_stack([data[:, 1], data[:, 0]])
y = data[:, 2]
2. Load the data & prepare the Kriging Model for interpolation
from geokrige.methods import OrdinaryKriging
kgn = OrdinaryKriging()
kgn.load(X, y)
kgn.variogram(plot=False)
kgn.fit(model='exp', plot=False)
Now, the Kriging Model is ready to predict values for the points with unknown values. The Kriging Model can be adjusted in many ways to meet the user's expectations, but the default settings in GeoKrige also handle many cases well. For more information about different components of the Kriging Model, please refer to the documentation.
3. Create a mesh grid on which predictions will be performed
import geopandas as gpd
from geokrige.tools import TransformerGDF
shp_file = 'shapefile/qj614bt0216.shp'
prediction_gdf = gpd.read_file(shp_file).to_crs(crs='EPSG:4326')
transformer = TransformerGDF()
transformer.load(prediction_gdf)
meshgrid = transformer.meshgrid(density=2)
mask = transformer.mask()
The shapefile has been downloaded from here. It has been read with the GeoPandas
package and stored in a GeoDataFrame object.
The GeoKrige package facilitates an easy transformation of shapefiles into mesh grids on which interpolation is usually performed. Additionally, it is also possible to create effortlessly a masking array, which can be later handy in the visualization process.
Now the meshgrid
object can be passed to the predict
method of the Kriging Model.
4. Predict values & mask them to the shapefile boundaries
X, Y = meshgrid
Z = kgn.predict(meshgrid)
Z[~mask] = None
Here, the variables that will be used in the visualization are created. The mesh grid is divided into separate X
and Y
matrices, and another matrix with interpolated values is created – Z
.
Note that the mask
object is a boolean matrix – a True
value signifies that a given point resides within the boundaries of the loaded shapefile. Thus, the masking matrix is inverted and the None
values are assigned to the points that are located outside the boundaries. In this manner, the matplotlib
package will make appropriate pixels empty.
5. Visualize the interpolation results
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
prediction_gdf.plot(facecolor='none', edgecolor='black', linewidth=1.5, zorder=5, ax=ax)
cbar = ax.contourf(X, Y, Z, cmap='terrain', levels=np.arange(0, 1300, 50), extend='min')
# Cbar aligned with the plot on the right side
cax = fig.add_axes([0.93, 0.134, 0.02, 0.72])
colorbar = plt.colorbar(cbar, cax=cax, orientation='vertical')
clabels = ax.contour(X, Y, Z, levels=np.arange(250, 1501, 200), colors='k', linewidths=0.5)
ax.clabel(clabels, fontsize=8)
ax.grid(lw=0.2)
ax.set_title('Elevation of Parana State (Brazil)', fontweight='bold', pad=15)
ax.set_xlim(-55, -47.7)
ax.set_ylim(-27, -22.2)
plt.show()

Outdated Answer
Please, see the first two paragraphs of this answer – the cloupy
package is no longer supported and there are better tools to resolve this problem.
To create an interpolation map, I recommend using cloupy
Firstly, the data should be cleaned up and properly structured to meet cloupy's data structure requirements for creating an interpolation map:
data = '''
1 -25.53 -48.51 4.50
2 -25.30 -48.49 59.00
3 -25.16 -48.32 40.00
4 -25.43 -49.26 923.50
5 -24.49 -49.15 360.00
6 -25.47 -49.46 910.00
7 -23.30 -49.57 512.00
8 -25.13 -50.01 880.00
9 -23.00 -50.02 450.00
10 -23.06 -50.21 440.00
11 -24.78 -50.00 1008.80
12 -25.27 -50.35 893.00
13 -24.20 -50.37 768.00
14 -23.16 -51.01 484.00
15 -22.57 -51.12 600.00
16 -23.54 -51.13 1020.00
17 -25.21 -51.30 1058.00
18 -23.30 -51.32 746.00
19 -26.29 -51.59 1100.00
20 -26.25 -52.21 930.00
21 -25.25 -52.25 880.00
22 -23.31 -51.13 566.00
23 -23.40 -51.91 542.00
24 -23.05 -52.26 480.00
25 -24.40 -52.34 540.00
26 -24.05 -52.36 616.40
27 -23.40 -52.35 530.00
28 -26.07 -52.41 700.00
29 -25.31 -53.01 513.00
30 -26.05 -53.04 650.00
31 -23.44 -53.17 480.00
32 -24.53 -53.33 660.00
33 -25.42 -53.47 400.00
34 -24.18 -53.55 310.00
'''
clean_data = []
for row in data.split('\n'):
row = row.split(' ')
row = [value for value in row if value != '']
clean_data.append(row)
clean_data = clean_data[1:-2] # delete empty values
import pandas as pd
df = pd.DataFrame(clean_data)
df = df.drop(0, axis=1)
df = df.iloc[:, [2, 1, 0]] # meet cloupy's data structure requirements
df = df.astype('float64')
The required data structure to create an interpolation map is the pandas.DataFrame object with the following column order: value, longitude, latitude. When the data meets the required data structure, we can create an interpolation map object:
import cloupy as cl
import numpy as np
imap = cl.m_MapInterpolation(
shapefile_path='shapefiles/41UFE250GC_SIR.shp',
crs='epsg:4326',
dataframe=df
)
The shapefile_path
argument shows the path to the shapefile from which the state boundaries will be drawn. In this case, the shapefile from this website was used, but it could be any other state's shapefile (the state of Paraná in Brazil). The crs
argument specifies the shapefile's coordinate system (usually epsg:4326
, but it can be different). When the interpolation map object is properly prepared, we can specify drawing arguments and draw the map:
imap.draw(
levels=np.arange(0, 1200, 100),
cmap='RdBu',
interpolation_method='linear',
interpolation_within_levels=True, # do not return values below 0, because the elevation must be equal to or greater than 0
boundaries_lw=0.2,
show_points=True,
show_grid=True,
xticks=[-50],
yticks=[-23, -27],
figsize=(5, 4),
)

This map is like the map in the question. The interpolation result is not satisfactory and the map style is poor. However, the imap.draw()
method has many arguments which can be specified and change significantly the interpolation result or/and the map style. Exemplary map based on the same data but with different arguments set in the imap.draw()
method:
imap.draw(
levels=np.arange(0, 1550, 50),
cmap='terrain',
interpolation_method='cubic', # default value
interpolation_within_levels=True,
boundaries_lw=0.2,
show_points=False, # default value
show_grid=False, # default value
xticks=np.arange(-55, -47, 1),
yticks=np.arange(-27, -21, 1),
figsize=(5, 4),
show_contours=True,
contours_levels=np.arange(250, 1501, 250),
clabels_add=[
(-53.6, -23.7), # specify manually the position of contour labels
(-53.5, -25),
(-52.5, -25.5),
(-51, -25.5),
(-51, -24.3),
],
cbar_ticks=np.arange(0, 1550, 250),
cbar_title='Elevation [m]',
title='Topography in the state of Paraná (Brazil)',
title_x_position=0.5,
title_ha='center'
)

For more information on creating interpolation maps see cloupy's README or check cloupy's docstrings.
Note that
when cloupy extrapolates values, it assumes that the values outside the mesh grid do not decrease/increase - it tries to keep the values outside the mesh grid as the extreme values of the mesh grid, which should be a correct approach in case of altitude
the cloupy's map object (cloupy.m_MapInterpolation
) does not always require passing a shapefile manually. The cloupy's map object allows you to draw boundaries from the default shapefile using the country
argument. The default shapefile contains administrative boundaries from the entire world
check the add_shape
argument in the draw()
method. It allows you to combine the main shape (boundary shape) with additional shapes
the map content will always be cropped to the main shape (the boundary shape specified in the cloupy.m_MapInterpolation(...)
object)