如何在 ggplot2 中正确绘制投影网格数据?
Posted
技术标签:
【中文标题】如何在 ggplot2 中正确绘制投影网格数据?【英文标题】:How to properly plot projected gridded data in ggplot2? 【发布时间】:2017-09-22 14:23:33 【问题描述】:多年来,我一直在使用ggplot2
绘制气候网格数据。这些通常是投影的 NetCDF 文件。单元格在模型坐标中是方形的,但根据模型使用的投影,在现实世界中可能并非如此。
我通常的做法是先将数据重新映射到合适的规则网格上,然后再进行绘图。这会对数据进行小幅修改,通常这是可以接受的。
但是,我认为这已经不够好了:我想直接绘制投影数据,而不需要重新映射,因为如果我没记错的话,其他程序(例如 ncl
)可以这样做,而无需触摸模型输出值。
但是,我遇到了一些问题。我将在下面逐步详细说明可能的解决方案,从最简单到最复杂,以及它们的问题。我们能克服它们吗?
编辑:感谢@lbusett 的回答,我得到了包含解决方案的this nice function。如果喜欢请点赞@lbusett's answer!
初始设置
#Load packages
library(raster)
library(ggplot2)
#This gives you the starting data, 's'
load(url('https://files.fm/down.php?i=kew5pxw7&n=loadme.Rdata'))
#If you cannot download the data, maybe you can try to manually download it from http://s000.tinyupload.com/index.php?file_id=04134338934836605121
#Check the data projection, it's Lambert Conformal Conic
projection(s)
#The data (precipitation) has a 'model' grid (125x125, units are integers from 1 to 125)
#for each point a lat-lon value is also assigned
pr <- s[[1]]
lon <- s[[2]]
lat <- s[[3]]
#Lets get the data into data.frames
#Gridded in model units:
pr_df_basic <- as.data.frame(pr, xy=TRUE)
colnames(pr_df_basic) <- c('lon', 'lat', 'pr')
#Projected points:
pr_df <- data.frame(lat=lat[], lon=lon[], pr=pr[])
我们为每个模型单元创建了两个数据框,一个带有模型坐标,一个带有真实的经纬交叉点(中心)。
可选:使用较小的域
如果您想更清楚地看到单元格的形状,您可以对数据进行子集化并仅提取少量模型单元格。请注意,您可能需要调整点大小、绘图限制和其他便利设施。您可以像这样子集,然后重做上面的代码部分(减去load()
):
s <- crop(s, extent(c(100,120,30,50)))
如果您想完全理解问题,也许您想同时尝试大域和小域。代码是相同的,只是点大小和地图限制发生了变化。下面的值适用于大的完整域。 好的,现在让我们开始吧!
从瓷砖开始
最明显的解决方案是使用瓷砖。让我们试试吧。
my_theme <- theme_bw() + theme(panel.ontop=TRUE, panel.background=element_blank())
my_cols <- scale_color_distiller(palette='Spectral')
my_fill <- scale_fill_distiller(palette='Spectral')
#Really unprojected square plot:
ggplot(pr_df_basic, aes(y=lat, x=lon, fill=pr)) + geom_tile() + my_theme + my_fill
结果如下:
好的,现在更高级:我们使用真正的 LAT-LON,使用方形瓷砖
ggplot(pr_df, aes(y=lat, x=lon, fill=pr)) + geom_tile(width=1.2, height=1.2) +
borders('world', xlim=range(pr_df$lon), ylim=range(pr_df$lat), colour='black') + my_theme + my_fill +
coord_quickmap(xlim=range(pr_df$lon), ylim=range(pr_df$lat)) #the result is weird boxes...
好的,但那些不是真正的模型方块,这是一个 hack。此外,模型框在域的顶部发散,并且都以相同的方式定向。不是很好。让我们自己投影正方形,即使我们已经知道这不是正确的做法……也许看起来不错。
#This takes a while, maybe you can trust me with the result
ggplot(pr_df, aes(y=lat, x=lon, fill=pr)) + geom_tile(width=1.5, height=1.5) +
borders('world', xlim=range(pr_df$lon), ylim=range(pr_df$lat), colour='black') + my_theme + my_fill +
coord_map('lambert', lat0=30, lat1=65, xlim=c(-20, 39), ylim=c(19, 75))
首先,这需要很多时间。不能接受的。另外,再次重申:这些不是正确的模型单元。
尝试使用点,而不是图块
也许我们可以用圆点或方点代替瓦片,也可以投影它们!
#Basic 'unprojected' point plot
ggplot(pr_df, aes(y=lat, x=lon, color=pr)) + geom_point(size=2) +
borders('world', xlim=range(pr_df$lon), ylim=range(pr_df$lat), colour='black') + my_cols + my_theme +
coord_quickmap(xlim=range(pr_df$lon), ylim=range(pr_df$lat))
我们可以使用方点...和投影!我们越来越近了,尽管我们知道它仍然不正确。
#In the following plot pointsize, xlim and ylim were manually set. Setting the wrong values leads to bad results.
#Also the lambert projection values were tired and guessed from the model CRS
ggplot(pr_df, aes(y=lat, x=lon, color=pr)) +
geom_point(size=2, shape=15) +
borders('world', xlim=range(pr_df$lon), ylim=range(pr_df$lat), colour='black') + my_theme + my_cols +
coord_map('lambert', lat0=30, lat1=65, xlim=c(-20, 39), ylim=c(19, 75))
不错的结果,但不是完全自动的,而且绘图点不够好。我想要真实的模型细胞,它们的形状被投影改变了!
多边形,也许吧?
如您所见,我正在寻求一种正确绘制模型框的方法,以正确的形状和位置投影。当然,模型中为正方形的模型框,一旦投影出来,就不再是规则的形状了。所以也许我可以使用多边形并投影它们?
我尝试使用rasterToPolygons
和fortify
并关注this 帖子,但没有这样做。我试过这个:
pr2poly <- rasterToPolygons(pr)
#http://mazamascience.com/WorkingWithData/?p=1494
pr2poly@data$id <- rownames(pr2poly@data)
tmp <- fortify(pr2poly, region = "id")
tmp2 <- merge(tmp, pr2poly@data, by = "id")
ggplot(tmp2, aes(x=long, y=lat, group = group, fill=Total.precipitation.flux)) + geom_polygon() + my_fill
好的,让我们尝试用经纬度替换...
tmp2$long <- lon[]
tmp2$lat <- lat[]
#Mh, does not work! See below:
ggplot(tmp2, aes(x=long, y=lat, group = group, fill=Total.precipitation.flux)) + geom_polygon() + my_fill
(对不起,我改变了图中的色标)
嗯,甚至不值得尝试投影。也许我应该尝试计算模型单元角的纬度,并为此创建多边形,然后重新投影?
结论
-
我想在其原生网格上绘制投影模型数据,但我无法这样做。使用瓦片是不正确的,使用点是不正确的,并且使用多边形似乎由于未知原因而不起作用。
通过
coord_map()
投影时,网格线和轴标签错误。这使得投影的 ggplots 无法用于出版物。
【问题讨论】:
你试过ggmap
吗?
@ulfelder 我记得过去使用过它,但最近没有。我忘记了它的存在。我会调查的。有什么特别要指出的吗?
我无法加载您的数据;你确定网址正确吗?
@SymbolixAU 我更改了下载提供程序,请重试。这应该更可靠地工作。
您的数据是从磁盘调用的:“/home/adriano/EURO-CORDEX_59_tas_pr_prc_monmean_1998-2000.nc” 所以您将一个没有真实数据但只有一个 url 的对象保存到您的机器中。您可以将 RData 保存在栅格图层中吗?
【参考方案1】:
进一步挖掘后,您的模型似乎基于“朗伯圆锥”投影中的 50 公里规则网格。但是,您在 netcdf 中的坐标是“单元格”中心的 lat-lon WGS84 坐标。
鉴于此,一种更简单的方法是重建原始投影中的单元格,然后在转换为sf
对象后绘制多边形,最终在重新投影后绘制。这样的东西应该可以工作(请注意,您需要从 github 安装 devel
版本的 ggplot2
才能工作):
load(url('https://files.fm/down.php?i=kew5pxw7&n=loadme.Rdata'))
library(raster)
library(sf)
library(tidyverse)
library(maps)
devtools::install_github("hadley/ggplot2")
# ____________________________________________________________________________
# Transform original data to a SpatialPointsDataFrame in 4326 proj ####
coords = data.frame(lat = values(s[[2]]), lon = values(s[[3]]))
spPoints <- SpatialPointsDataFrame(coords,
data = data.frame(data = values(s[[1]])),
proj4string = CRS("+init=epsg:4326"))
# ____________________________________________________________________________
# Convert back the lat-lon coordinates of the points to the original ###
# projection of the model (lcc), then convert the points to polygons in lcc
# projection and convert to an `sf` object to facilitate plotting
orig_grid = spTransform(spPoints, projection(s))
polys = as(SpatialPixelsDataFrame(orig_grid, orig_grid@data, tolerance = 0.149842),"SpatialPolygonsDataFrame")
polys_sf = as(polys, "sf")
points_sf = as(orig_grid, "sf")
# ____________________________________________________________________________
# Plot using ggplot - note that now you can reproject on the fly to any ###
# projection using `coord_sf`
# Plot in original projection (note that in this case the cells are squared):
my_theme <- theme_bw() + theme(panel.ontop=TRUE, panel.background=element_blank())
ggplot(polys_sf) +
geom_sf(aes(fill = data)) +
scale_fill_distiller(palette='Spectral') +
ggtitle("Precipitations") +
coord_sf() +
my_theme
# Now Plot in WGS84 latlon projection and add borders:
ggplot(polys_sf) +
geom_sf(aes(fill = data)) +
scale_fill_distiller(palette='Spectral') +
ggtitle("Precipitations") +
borders('world', colour='black')+
coord_sf(crs = st_crs(4326), xlim = c(-60, 80), ylim = c(15, 75))+
my_theme
要添加边界,也可以在原始投影中绘制,但是,您必须将 loygon 边界提供为 sf
对象。从这里借:
Converting a "map" object to a "SpatialPolygon" object
这样的事情会起作用:
library(maptools)
borders <- map("world", fill = T, plot = F)
IDs <- seq(1,1627,1)
borders <- map2SpatialPolygons(borders, IDs=borders$names,
proj4string=CRS("+proj=longlat +datum=WGS84")) %>%
as("sf")
ggplot(polys_sf) +
geom_sf(aes(fill = data), color = "transparent") +
geom_sf(data = borders, fill = "transparent", color = "black") +
scale_fill_distiller(palette='Spectral') +
ggtitle("Precipitations") +
coord_sf(crs = st_crs(projection(s)),
xlim = st_bbox(polys_sf)[c(1,3)],
ylim = st_bbox(polys_sf)[c(2,4)]) +
my_theme
作为旁注,现在我们“恢复”了正确的空间参考,也可以构建正确的raster
数据集。例如:
r <- s[[1]]
extent(r) <- extent(orig_grid) + 50000
会给你一个正确的raster
in r
:
r
class : RasterLayer
band : 1 (of 36 bands)
dimensions : 125, 125, 15625 (nrow, ncol, ncell)
resolution : 50000, 50000 (x, y)
extent : -3150000, 3100000, -3150000, 3100000 (xmin, xmax, ymin, ymax)
coord. ref. : +proj=lcc +lat_1=30. +lat_2=65. +lat_0=48. +lon_0=9.75 +x_0=-25000. +y_0=-25000. +ellps=sphere +a=6371229. +b=6371229. +units=m +no_defs
data source : in memory
names : Total.precipitation.flux
values : 0, 0.0002373317 (min, max)
z-value : 1998-01-16 10:30:00
zvar : pr
看到现在分辨率是 50Km,范围是公制坐标。因此,您可以使用raster
数据的函数来绘制/处理r
,例如:
library(rasterVis)
gplot(r) + geom_tile(aes(fill = value)) +
scale_fill_distiller(palette="Spectral", na.value = "transparent") +
my_theme
library(mapview)
mapview(r, legend = TRUE)
【讨论】:
object 'transf' not found
。另外,您是如何获得“宽容”的?为什么需要重新映射到epsg:4326
?这会引入(尽管很小)错误吗?
操作,抱歉。我编辑了帖子。关于公差,需要创建 SpatialPixelsDataFrame
您需要一个规则网格,并且单元格的“重新投影”可能会导致一些舍入误差。如果您在没有 tolerance
参数的情况下调用 SpatialPixelsDataFrame(orig_grid, orig_grid@data)
,则会给出“建议的”容差。关于投影:不,不需要转换为 4326。这只是为了向您展示您可以轻松地重新投影到任何 CRS。
那是因为borders("world",.....
不是sf
对象,所以显然没有应用“重投影”。有关解决方法,请参阅更新的答案。
您需要将范围扩展到点的半个像素,因为raster
对象的范围是在单元格的角上计算的。我使用 50000 是因为当对 extent
对象“求和”时,范围在所有方向上都扩大了一半(我通过反复试验发现了这一点)。
好主意。我会将其纳入答案中。另外:在您添加到问题的功能中,考虑您可以删除行points_sf = as(orig_grid, "sf")
。这是测试单元格的多边形是否与点正确对齐的剩余部分。【参考方案2】:
“放大”以查看作为单元中心的点。你可以看到它们在矩形网格中。
我计算了多边形的顶点如下。
将 125x125 纬度和经度转换为矩阵
为单元顶点(角)初始化 126x126 矩阵。
将单元顶点计算为每个 2x2 组的平均位置 分。
为边和角添加单元格顶点(假设单元格宽度和高度为 等于相邻单元格的宽度和高度)。
生成 data.frame,每个单元格有四个顶点,所以我们最终 4x125x125 行。
代码变成
pr <- s[[1]]
lon <- s[[2]]
lat <- s[[3]]
#Lets get the data into data.frames
#Gridded in model units:
#Projected points:
lat_m <- as.matrix(lat)
lon_m <- as.matrix(lon)
pr_m <- as.matrix(pr)
#Initialize emptry matrix for vertices
lat_mv <- matrix(,nrow = 126,ncol = 126)
lon_mv <- matrix(,nrow = 126,ncol = 126)
#Calculate centre of each set of (2x2) points to use as vertices
lat_mv[2:125,2:125] <- (lat_m[1:124,1:124] + lat_m[2:125,1:124] + lat_m[2:125,2:125] + lat_m[1:124,2:125])/4
lon_mv[2:125,2:125] <- (lon_m[1:124,1:124] + lon_m[2:125,1:124] + lon_m[2:125,2:125] + lon_m[1:124,2:125])/4
#Top edge
lat_mv[1,2:125] <- lat_mv[2,2:125] - (lat_mv[3,2:125] - lat_mv[2,2:125])
lon_mv[1,2:125] <- lon_mv[2,2:125] - (lon_mv[3,2:125] - lon_mv[2,2:125])
#Bottom Edge
lat_mv[126,2:125] <- lat_mv[125,2:125] + (lat_mv[125,2:125] - lat_mv[124,2:125])
lon_mv[126,2:125] <- lon_mv[125,2:125] + (lon_mv[125,2:125] - lon_mv[124,2:125])
#Left Edge
lat_mv[2:125,1] <- lat_mv[2:125,2] + (lat_mv[2:125,2] - lat_mv[2:125,3])
lon_mv[2:125,1] <- lon_mv[2:125,2] + (lon_mv[2:125,2] - lon_mv[2:125,3])
#Right Edge
lat_mv[2:125,126] <- lat_mv[2:125,125] + (lat_mv[2:125,125] - lat_mv[2:125,124])
lon_mv[2:125,126] <- lon_mv[2:125,125] + (lon_mv[2:125,125] - lon_mv[2:125,124])
#Corners
lat_mv[c(1,126),1] <- lat_mv[c(1,126),2] + (lat_mv[c(1,126),2] - lat_mv[c(1,126),3])
lon_mv[c(1,126),1] <- lon_mv[c(1,126),2] + (lon_mv[c(1,126),2] - lon_mv[c(1,126),3])
lat_mv[c(1,126),126] <- lat_mv[c(1,126),125] + (lat_mv[c(1,126),125] - lat_mv[c(1,126),124])
lon_mv[c(1,126),126] <- lon_mv[c(1,126),125] + (lon_mv[c(1,126),125] - lon_mv[c(1,126),124])
pr_df_orig <- data.frame(lat=lat[], lon=lon[], pr=pr[])
pr_df <- data.frame(lat=as.vector(lat_mv[1:125,1:125]), lon=as.vector(lon_mv[1:125,1:125]), pr=as.vector(pr_m))
pr_df$id <- row.names(pr_df)
pr_df <- rbind(pr_df,
data.frame(lat=as.vector(lat_mv[1:125,2:126]), lon=as.vector(lon_mv[1:125,2:126]), pr = pr_df$pr, id = pr_df$id),
data.frame(lat=as.vector(lat_mv[2:126,2:126]), lon=as.vector(lon_mv[2:126,2:126]), pr = pr_df$pr, id = pr_df$id),
data.frame(lat=as.vector(lat_mv[2:126,1:125]), lon=as.vector(lon_mv[2:126,1:125]), pr = pr_df$pr, id= pr_df$id))
具有多边形单元格的相同缩放图像
Labels Fix
ewbrks <- seq(-180,180,20)
nsbrks <- seq(-90,90,10)
ewlbls <- unlist(lapply(ewbrks, function(x) ifelse(x < 0, paste(abs(x), "°W"), ifelse(x > 0, paste(abs(x), "°E"),x))))
nslbls <- unlist(lapply(nsbrks, function(x) ifelse(x < 0, paste(abs(x), "°S"), ifelse(x > 0, paste(abs(x), "°N"),x))))
用 geom_polygon 替换 geom_tile 和 geom_point
ggplot(pr_df, aes(y=lat, x=lon, fill=pr, group = id)) + geom_polygon() +
borders('world', xlim=range(pr_df$lon), ylim=range(pr_df$lat), colour='black') + my_theme + my_fill +
coord_quickmap(xlim=range(pr_df$lon), ylim=range(pr_df$lat)) +
scale_x_continuous(breaks = ewbrks, labels = ewlbls, expand = c(0, 0)) +
scale_y_continuous(breaks = nsbrks, labels = nslbls, expand = c(0, 0)) +
labs(x = "Longitude", y = "Latitude")
ggplot(pr_df, aes(y=lat, x=lon, fill=pr, group = id)) + geom_polygon() +
borders('world', xlim=range(pr_df$lon), ylim=range(pr_df$lat), colour='black') + my_theme + my_fill +
coord_map('lambert', lat0=30, lat1=65, xlim=c(-20, 39), ylim=c(19, 75)) +
scale_x_continuous(breaks = ewbrks, labels = ewlbls, expand = c(0, 0)) +
scale_y_continuous(breaks = nsbrks, labels = nslbls, expand = c(0, 0)) +
labs(x = "Longitude", y = "Latitude")
编辑 - 解决轴刻度
我一直无法为纬度的网格线和标签找到任何快速解决方案。可能有一个 R 包可以用更少的代码解决你的问题!
需要手动设置 nsbreaks 并创建 data.frame
ewbrks <- seq(-180,180,20)
nsbrks <- c(20,30,40,50,60,70)
nsbrks_posn <- c(-16,-17,-16,-15,-14.5,-13)
ewlbls <- unlist(lapply(ewbrks, function(x) ifelse(x < 0, paste0(abs(x), "° W"), ifelse(x > 0, paste0(abs(x), "° E"),x))))
nslbls <- unlist(lapply(nsbrks, function(x) ifelse(x < 0, paste0(abs(x), "° S"), ifelse(x > 0, paste0(abs(x), "° N"),x))))
latsdf <- data.frame(lon = rep(c(-100,100),length(nsbrks)), lat = rep(nsbrks, each =2), label = rep(nslbls, each =2), posn = rep(nsbrks_posn, each =2))
删除 y 轴刻度标签和相应的网格线,然后使用 geom_line
和 geom_text
“手动”添加回来
ggplot(pr_df, aes(y=lat, x=lon, fill=pr, group = id)) + geom_polygon() +
borders('world', xlim=range(pr_df$lon), ylim=range(pr_df$lat), colour='black') + my_theme + my_fill +
coord_map('lambert', lat0=30, lat1=65, xlim=c(-20, 40), ylim=c(19, 75)) +
scale_x_continuous(breaks = ewbrks, labels = ewlbls, expand = c(0, 0)) +
scale_y_continuous(expand = c(0, 0), breaks = NULL) +
geom_line(data = latsdf, aes(x=lon, y=lat, group = lat), colour = "white", size = 0.5, inherit.aes = FALSE) +
geom_text(data = latsdf, aes(x = posn, y = (lat-1), label = label), angle = -13, size = 4, inherit.aes = FALSE) +
labs(x = "Longitude", y = "Latitude") +
theme( axis.text.y=element_blank(),axis.ticks.y=element_blank())
【讨论】:
让我们continue this discussion in chat。 我已经修复了这个错误。 我明白你的意思。标签实际上与 -20E 经度线的截距水平对齐。这在某种程度上是有道理的,因为这是该轴上的指定限制。但显然不适用于兰伯特投影。 好吧,如果在 Lambert 中绘图,“手动”删除第一个和最后一个中断标签就足够了。顺便说一句:重新分配标签时出错:Green 以东的标签应该是 10 E、20 E 等。 @LoBu 混淆了 E 和 W,这很尴尬。我从 *** 得到代码,我认为它是可靠的,我的错。以上是关于如何在 ggplot2 中正确绘制投影网格数据?的主要内容,如果未能解决你的问题,请参考以下文章