GEE进行Mann Kendall检验(附python API)

Mann-Kendall原理

Mann-Kendall检验是一种用于检测时间序列中趋势变化的非参数统计方法,它不需要对数据分布做出任何假设,因此适用于各种类型的数据。

Mann-Kendall检验的假设是原假设:时间序列中不存在趋势变化,备择假设:时间序列中存在趋势变化。其基本思想是通过比较每对数据点之间的大小关系来检查序列中的趋势,然后根据秩和的正负性来确定趋势的方向。

Mann-Kendall检验的计算过程包括以下步骤:

1.对于长度为n的时间序列X,计算n(n-1)/2个变量关系差(d):

2.将所有d[i,j]的符号相乘,得到Sgn。即:

3.计算所有d[i,j]的绝对值的秩和,即:

4.计算标准正态分布的z统计量,即:

5.根据显著性水平选择拒绝原假设的临界值,如果z值小于临界值,则接受原假设,否则拒绝原假设。

在Mann-Kendall检验确定存在趋势后,可以使用Sen's slope来计算趋势的斜率。Sen's slope是一种计算趋势斜率的方法,它利用中位数差来估计趋势的斜率,与线性回归方法不同,Sen's slope不受异常值的影响,因此更适用于含有异常值的数据。

Sen's slope的计算方法如下:

1.对于长度为n的时间序列X,计算n(n-1)/2个变量关系差(d):

2.计算所有d[i,j]的绝对值的中位数,即:

3.计算Sen's slope,即:

其中,k = (n-1)/2

Sen's slope表示时间序列中单位时间的变化量。当Sen's slope为正数时,表示时间序列呈现上升趋势,当Sen's slope为负数时,表示时间序列呈现下降趋势。

简而言之,Sen's slope是所有点连线的中位数。

GEE JS

时间序列

要求多年ImageCollection必须有时间属性:

以PML_v2的ET为例(也可也相应换成其它,如EVI、NDVI、LAI等等)

image-20230322113316942
image-20230322113316942

获取一个时间序列查看:

function get_chart(imgcol, name){
  

  var chart = ui.Chart.image.series({
      imageCollection: imgcol, 
      region         : point,
      reducer        : ee.Reducer.mean(),
      scale          : 500
  });
  print(chart, name);
}

get_chart(img_ET.select('ET'), 'ET_series');
image-20230322113527649
image-20230322113527649

由于MK sensSlope的原理是所有时间连线的中位数

因此需要组成一个Join,构建了所有前后点的连线。施加一个Filter,使用lessThan过滤器将集合与其自身结合起来。过滤器将通过所有按时间顺序排列较晚的图像。在连接的集合中,每个图像都将在一个属性中存储它之后的所有图像after

var afterFilter = ee.Filter.lessThan({
  leftField: 'system:time_start',
  rightField: 'system:time_start'
});
var coll = img_ET.select('ET')
var joined = ee.ImageCollection(ee.Join.saveAll('after').apply({
  primary: coll,
  secondary: coll,
  condition: afterFilter
}));
print(coll)

趋势检验

Mann-Kendall 趋势定义为所有正的符号之和。定义了sign函数,用来判断ET值大小(后减去前),正值为1,负值为 -1,相等为零。

var kendall = ee.ImageCollection(joined.map(function(current) {
  var afterCollection = ee.ImageCollection.fromImages(current.get('after'));
  return afterCollection.map(function(image) {
    // The unmask is to prevent accumulation of masked pixels that
    // result from the undefined case of when either current or image
    // is masked.  It won't affect the sum, since it's unmasked to zero.
    return ee.Image(sign(current, image)).unmask(0);
  });
  // Set parallelScale to avoid User memory limit exceeded.
}).flatten()).reduce('sum', 2);
var palette = ['red''white''green'];
// Stretch this as necessary.
Map.addLayer(kendall, {palette: palette}, 'kendall');

SensSlope

SensSlope的计算方式与 Mann-Kendall 统计量类似。但是,不是将所有正的符号相加,而是计算所有正sign的斜率。SensSlope是上述斜率中位数

尤其注意的是,我这里是年均值,ee.Date.difference是Year,如果是日序列改为days

var vis_et  = { min: -10, max: 10, palette: pkg_vis.colors.RdYlBu[11]};
// Stretch this as necessary.

var slope = function(i, j) { // i and j are images
  return ee.Image(j).subtract(i)
      .divide(ee.Image(j).date().difference(ee.Image(i).date(), 'year'))
      .rename('slope')
      .float();
};

var slopes = ee.ImageCollection(joined.map(function(current) {
  var afterCollection = ee.ImageCollection.fromImages(current.get('after'));
  return afterCollection.map(function(image) {
      return ee.Image(slope(current, image));
  });
}).flatten());

var sensSlope = slopes.reduce(ee.Reducer.median(), 2); // Set parallelScale.
Map.addLayer(sensSlope, vis_et, 'sensSlope');
image-20230322114712853
image-20230322114712853

计算方差

还记得原理中公式4的方差吗?

具体的推导请看Mann和Kendall两位学者于1945发表在Econometrica的《Nonparametric tests against trend》

(反正比较麻烦)

image-20230322115255637
image-20230322115255637
image-20230322115304860
image-20230322115304860

这里直接套公式:

// Values that are in a group (ties).  Set all else to zero.
var groups = coll.map(function(i) {
  var matches = coll.map(function(j) {
    return i.eq(j); // i and j are images.
  }).sum();
  return i.multiply(matches.gt(1));
});

// Compute tie group sizes in a sequence.  The first group is discarded.
var group = function(array) {
  var length = array.arrayLength(0);
  // Array of indices.  These are 1-indexed.
  var indices = ee.Image([1])
      .arrayRepeat(0, length)
      .arrayAccum(0, ee.Reducer.sum())
      .toArray(1);
  var sorted = array.arraySort();
  var left = sorted.arraySlice(0, 1);
  var right = sorted.arraySlice(0, 0, -1);
  // Indices of the end of runs.
  var mask = left.neq(right)
  // Always keep the last index, the end of the sequence.
      .arrayCat(ee.Image(ee.Array([[1]])), 0);
  var runIndices = indices.arrayMask(mask);
  // Subtract the indices to get run lengths.
  var groupSizes = runIndices.arraySlice(0, 1)
      .subtract(runIndices.arraySlice(0, 0, -1));
  return groupSizes;
};

// See equation 2.6 in Sen (1968).
var factors = function(image) {
  return image.expression('b() * (b() - 1) * (b() * 2 + 5)');
};

var groupSizes = group(groups.toArray());
var groupFactors = factors(groupSizes);
var groupFactorSum = groupFactors.arrayReduce('sum', [0])
      .arrayGet([0, 0]);

var count = joined.count();

var kendallVariance = factors(count)
    .subtract(groupFactorSum)
    .divide(18)
    .float();
//Map.addLayer(kendallVariance, {}, 'kendallVariance');

显著性检验

对于适当大的样本,Mann-Kendall 统计量渐近正态。

假设我们的样本足够大且不相关。在这些假设下,Mann-Kendall 统计量的真实均值为零,方差在上一步已经计算好了。

要计算标准正态统计量 ( z),请将统计量除以其标准差。z 统计量的 P 值为

对于在 95% 置信水平(双边检验)下检验存在正面或负面趋势的置信度。将 P 值与 0.95进行比较。

// Compute Z-statistics.
var zero = kendall.multiply(kendall.eq(0));
var pos = kendall.multiply(kendall.gt(0)).subtract(1);
var neg = kendall.multiply(kendall.lt(0)).add(1);

var z = zero
    .add(pos.divide(kendallVariance.sqrt()))
    .add(neg.divide(kendallVariance.sqrt()));
Map.addLayer(z, {min: -2, max: 2}, 'z');

// https://en.wikipedia.org/wiki/Error_function#Cumulative_distribution_function
function eeCdf(z) {
  return ee.Image(0.5)
      .multiply(ee.Image(1).add(ee.Image(z).divide(ee.Image(2).sqrt()).erf()));
}

function invCdf(p) {
  return ee.Image(2).sqrt()
      .multiply(ee.Image(p).multiply(2).subtract(1).erfInv());
}

// Compute P-values.
var p = ee.Image(1).subtract(eeCdf(z.abs()));
Map.addLayer(p, {min: 0, max: 1}, 'p');

// Pixels that can have the null hypothesis (there is no trend) rejected.
// Specifically, if the true trend is zero, there would be less than 5%
// chance of randomly obtaining the observed result (that there is a trend).
Map.addLayer(p.lte(0.05), {min: 0, max: 1}, 'significant trends');

计算的P值如下:

image-20230322115709407
image-20230322115709407

Python API

对于python API只要将上述函数重写为python就可以了:

以NDVI为例,使用geemap:

计算NDVI

加载中国边界

countries = geemap.shp_to_ee("../data/countries.shp")
roi = countries.filterMetadata('NAME','equals''China')

计算NDVI

这里是定义了两个函数maskcloudndvi

然后使用List循环计算每年的NDVI,再后续进行MK检验

这里尤其注意的是通过setMulti添加Year属性,便于后续计算sensSlope

# 去云操作

def maskcloud(image):
    qa = image.select('BQA')
    mask = qa.bitwiseAnd(1 << 4).Or(qa.bitwiseAnd(1 << 8))
    return image.updateMask(mask.Not())
# 计算NDVI
def ndvi(image):
    ndvi = image.normalizedDifference(['B5''B4']).rename('NDVI')
    return image.addBands(ndvi)


time = ee.List.sequence(2013, 2020)
def funyear(year):
    year_origin = year
    year = ee.Number(year).format("%04d").cat('-01-01')               
    image = ee.ImageCollection("LANDSAT/LC08/C01/T1_RT_TOA")\
    .filterBounds(roi)\
    .filterDate(year, ee.Date(year).advance(1, 'year'))\
    .map(maskcloud)\
    .map(ndvi)\
    .median()\
    .select(['NDVI'])\
    .clip(roi.geometry())\
    .setMulti({'system:index':year, 'system:time_start':ee.Date(year), 'year':ee.Number(year_origin)})
    return image

imgYear = time.map(funyear)       
imgYear = ee.ImageCollection(imgYear)

加载可视化:

vis_params = {
  'min': -0.2,
  'max': 1,
  'palette': [
    '#000080''#0000D9''#4000FF''#8000FF''#0080FF''#00FFFF''#00FF80',
    '#80FF00''#DAFF00''#FFFF00''#FFF500''#FFDA00''#FFB000''#FFA400',
    '#FF4F00''#FF2500''#FF0A00''#FF00FF'
  ],
}
Map.addLayer(imgYear.first(), vis_params, 'ndvi')
Map
image-20230322153210227
image-20230322153210227

MK检验

进行MK检验,首先需要组成一个Join,构建了所有前后点的连线。

注意这里的Field是之前手动添加的'year'属性

afterFilter = ee.Filter.lessThan(
    leftField = 'year',
    rightField = 'year'
)

joined = ee.ImageCollection(ee.Join.saveAll('after').apply(
    primary = imgYear,
    secondary = imgYear,
    condition = afterFilter
))

之后定义两个函数,与之前JS相同,只是定义函数的方式不一样,python是def

def sign(i, j):  # i and j are images:
    return ee.Image(j).neq(i).multiply(ee.Image(j).subtract(i).clamp(-1, 1)).int()

def func(current):
    afterCollection = ee.ImageCollection.fromImages(current.get('after'))
    return afterCollection.map(lambda image: ee.Image(sign(current, image)).unmask(0))

kendall = ee.ImageCollection(joined.map(func).flatten()).sum()

#palette = ['red', 'yellow', 'green'];
#Map.addLayer(kendall, {'min':-100,'max':100, 'palette': palette}, 'kendall')

计算SensSlope

计算sensSlope并可视化

相对于传统的JS定义函数.map(function(img){}),python则要改写为匿名函数lambda img: img...

def slope(i, j):
#    return ee.Image(j).subtract(i).divide(ee.Image(j).date().difference(ee.Image(i).date(), 'days')).rename('slope').float()
    return ee.Image(j).subtract(i).divide(ee.Number(ee.Image(j).get('year')).subtract(ee.Number(ee.Image(i).get('year')))).rename('slope').float()


slopes = ee.ImageCollection(joined.map(lambda current: ee.ImageCollection.fromImages(current.get('after'))\
                                       .map(lambda image: ee.Image(slope(current, image)))).flatten())

sensSlope = slopes.reduce(ee.Reducer.median(), 2)

vis_params = {
  'min': -0.01,
  'max': 0.01,
  'palette': [
    '#000080''#0000D9''#4000FF''#8000FF''#0080FF''#00FFFF''#00FF80',
    '#80FF00''#DAFF00''#FFFF00''#FFF500''#FFDA00''#FFB000''#FFA400',
    '#FF4F00''#FF2500''#FF0A00''#FF00FF'
  ],
}

Map.addLayer(sensSlope, vis_params, 'sensSlope')
#Map
alt

计算方差和显著性检验

与JS相同,直接改写形式即可:

## image
groups = imgYear.map(lambda i: i.multiply(imgYear.map(lambda j: i.eq(j)).sum().gt(1)))
##
def group(array):
    length = array.arrayLength(0)
    indices = ee.Image([1]).arrayRepeat(0, length).arrayAccum(0, ee.Reducer.sum()).toArray(1)
    sorted = array.arraySort()
    left = sorted.arraySlice(0, 1)
    right = sorted.arraySlice(0, 0, -1)
    mask = left.neq(right).arrayCat(ee.Image(ee.Array([[1]])), 0)
    runIndices = indices.arrayMask(mask)
    groupSizes = runIndices.arraySlice(0, 1).subtract(runIndices.arraySlice(0, 0, -1))
    return groupSizes
    
def factors(image):
    return image.expression('b() * (b() - 1) * (b() * 2 + 5)')


groupSizes = group(groups.toArray())
groupFactors = factors(groupSizes)
groupFactorSum = groupFactors.arrayReduce('sum', [0]).arrayGet([0, 0])

count = joined.count()

kendallVariance = factors(count).subtract(groupFactorSum).divide(18).float()
#Map.addLayer(kendallVariance, {}, 'kendallVariance')
zero = kendall.multiply(kendall.eq(0));
pos = kendall.multiply(kendall.gt(0)).subtract(1);
neg = kendall.multiply(kendall.lt(0)).add(1);

z = zero.add(pos.divide(kendallVariance.sqrt())).add(neg.divide(kendallVariance.sqrt()))
#Map.addLayer(z, {'min': -2, 'max': 2}, 'z');

def eeCdf(z):
    return ee.Image(0.5).multiply(ee.Image(1).add(ee.Image(z).divide(ee.Image(2).sqrt()).erf()))

def invCdf(p):
    ee.Image(2).sqrt().multiply(ee.Image(p).multiply(2).subtract(1).erfInv())

# Compute P-values.
p = ee.Image(1).subtract(eeCdf(z.abs()))
Map.addLayer(p, {'min': 0, 'max': 1}, 'p')

#Map.addLayer(p.lte(0.025), {min: 0, max: 1}, 'significant trends')
#Map

Highlights

  • 本文介绍了Mann-Kendall的原理
  • 分Javascript和python两种接口下进行了实现
  • 介绍了python API不同于Javascript的一些操作和改写方法

参考

[1] Mann, H. B. (1945). NONPARAMETRIC TESTS AGAINST TREND. Econometrica, 13(3), 245-259. https://doi.org/10.2307/1907187

[2] Non-Parametric Trend Analysis, Google Earth Engine Community. https://developers.google.cn/earth-engine/tutorials/community/nonparametric-trends

本文由 mdnice 多平台发布

  • 5
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
gee(Google Earth Engine)是由谷歌开发的一款云端平台,用于存储、处理、分析和可视化地球数据。它提供了丰富的 Python API,使得用户可以使用 Python 编程语言来访问和操作地球数据。 使用 gee Python API,可以方便地进行地理数据的获取和处理。首先,我们可以使用 gee Python API 连接到 Google Earth Engine 服务器,获取世界范围内的各种地球数据集。这些数据集包括遥感图像(如卫星图像、气候数据等)、地形数据、地表覆盖数据等等。通过 Python API,我们可以使用简洁的代码来获取这些数据,并进行进一步的处理。 在数据获取后,gee Python API 提供了丰富的数据处理和分析功能。例如,我们可以使用 Python API 对遥感图像进行影像处理,如镶嵌、裁剪、融合等。此外,Python API 还支持各种地理统计分析、空间分析和机器学习算法。这些功能使得用户能够从大规模的地球数据中提取有用的信息,并进行复杂的分析和建模工作。 除了数据处理和分析,gee Python API 还支持数据的可视化。它提供了丰富的绘图函数和库,可以生成各种静态和交互式地图,将地球数据以清晰、直观的方式展示出来。这使得使用者可以更好地理解和传达数据的结果,并进行更深入的探索和研究。 总之,gee Python API 提供了一个强大而灵活的平台,使得用户能够轻松地访问、处理和分析地球数据。它的丰富功能和易于使用的编程接口,使得科学家、学生和开发者们能够更好地利用地球数据,进行各种地理信息系统、生态环境、气候变化等方面的研究和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

地学万事屋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值