Calculate data averages in blocks

6. Calculate data averages in blocks#

Point or line data can often be oversampled, for example along flight lines or ship tracks, or unevenly sampled across the data region. This can be undesired for plotting or lead to biases in interpolation and other analyses. A safe way to reduce (downsample) the data without causing aliasing is to divide the data into blocks and then take the mean or other statistic of the points that fall inside each block. We’ll demonstrate how to do this with bordado.block_split and pandas.

import ensaio
import pyproj
import pygmt
import pandas as pd
import bordado as bd

First, let’s get some topography data that we can use for this example through ensaio.fetch_british_columbia_lidar:

fname = ensaio.fetch_british_columbia_lidar(version=1)
data = pd.read_csv(fname)
data
longitude latitude elevation_m
0 -123.813753 49.460263 -0.67
1 -123.813725 49.460274 -0.96
2 -123.813764 49.460254 -0.78
3 -123.813744 49.460262 -0.61
4 -123.813737 49.460265 -0.62
... ... ... ...
829728 -123.807407 49.455007 -1.24
829729 -123.807410 49.454995 -1.25
829730 -123.807416 49.454980 -1.19
829731 -123.807428 49.454966 -1.21
829732 -123.807432 49.454963 -1.20

829733 rows × 3 columns

This is a LIDAR dataset from a few islands in British Columbia, Canada. Let’s use bordado.get_region and bordado.pad_region to extract the data bounding box and pad it a little. This will be useful to make plots of the data without having the plot margins directly touching the data.

region = bd.pad_region(
    bd.get_region((data.longitude, data.latitude)),
    pad=(5 / 3600, 3 / 3600),
)
print(region)
(-123.82038848888888, -123.80540841111112, 49.45199186666667, 49.46111413333333)

Let’s plot the data with pygmt to see what we’ve got:

fig = pygmt.Figure()
pygmt.makecpt(
    cmap="viridis",
    series=[data.elevation_m.min(), data.elevation_m.max()],
)
fig.plot(
    x=data.longitude,
    y=data.latitude,
    fill=data.elevation_m,
    cmap=True,
    style="c0.01c",
    projection="M15c",
    region=region,
    frame="afg",
)
fig.colorbar(frame=["af+lOriginal topography", "y+lm"])
fig.show()
../_images/block-average_3_0.png

Notice that the sampling is not uniform, with areas of denser sampling and areas with no points at all.

The dataset is in geodetic longitude and latitude coordinates so we should first project it so we can use the regular Cartesian distance calculations in block_split:

projection = pyproj.Proj(proj="merc", lat_ts=data.latitude.mean())
easting, northing = projection(data.longitude, data.latitude)

Now we can divide the data into 2 meter blocks. The labels variable contains the index of the block to which each point belongs.

block_coordinates, labels = bd.block_split(
    coordinates=(easting, northing),
    block_size = 2,
)
data["block_id"] = labels
data
longitude latitude elevation_m block_id
0 -123.813753 49.460263 -0.67 183178
1 -123.813725 49.460274 -0.96 183179
2 -123.813764 49.460254 -0.78 182735
3 -123.813744 49.460262 -0.61 182736
4 -123.813737 49.460265 -0.62 183178
... ... ... ... ...
829728 -123.807407 49.455007 -1.24 53901
829729 -123.807410 49.454995 -1.25 53459
829730 -123.807416 49.454980 -1.19 53017
829731 -123.807428 49.454966 -1.21 53017
829732 -123.807432 49.454963 -1.20 52575

829733 rows × 4 columns

Our dataset now contains the block indices (IDs) for each point. To do a reduction operation (like mean, median, standard deviation, sum, etc), we can use the pandas.DataFrame.groupby method:

block_data = data.groupby("block_id").mean()
block_data
longitude latitude elevation_m
block_id
155 -123.814695 49.452839 -1.025000
156 -123.814677 49.452836 -0.687692
157 -123.814654 49.452837 -0.262667
158 -123.814625 49.452836 -0.527143
159 -123.814594 49.452836 -0.517500
... ... ... ...
183188 -123.813461 49.460268 -1.058182
183189 -123.813437 49.460271 -0.916400
183190 -123.813410 49.460271 -0.952000
183191 -123.813386 49.460269 -0.964545
183192 -123.813363 49.460266 -1.190000

62928 rows × 3 columns

This new dataset has the mean longitude, latitude, and elevation per 2 meter block. The reduced dataset looks like this:

fig = pygmt.Figure()
pygmt.makecpt(
    cmap="viridis",
    series=[data.elevation_m.min(), data.elevation_m.max()],
)
fig.plot(
    x=block_data.longitude,
    y=block_data.latitude,
    fill=block_data.elevation_m,
    cmap=True,
    style="c0.01c",
    projection="M15c",
    region=region,
    frame="afg",
)
fig.colorbar(frame=["af+lBlock average topography", "y+lm"])
fig.show()
../_images/block-average_7_0.png

Notice that now the points are more uniformly spaced and there are no areas with much higher point density.

See also

This is how the verde.BlockReduce class works!