python进行Theil-Sen Median斜率估计和Mann-Kendall趋势分析

Theil-Sen Median斜率估计和Mann-Kendall趋势分析

Theil-Sen Median斜率估计和Mann-Kendall趋势分析都是非参数统计方法,用于分析时间序列数据中的趋势。

应用案例

Liu等(2024)利用Theil-Sen Median斜率估计和Mann-Kendall趋势分析对沉积物连续性指标(IC)1990-2020年间30年的变化趋势进行了分析

一般来说,可以根据Theil-Sen Median计算出的β值和Mann-Kendall计算出的Z值进行趋势分类
在Theil-Sen Median斜率估计和Mann-Kendall趋势分析:以多年NPP数据为例这篇博客中提到了具体的分类表如下:

代码思路

我这里有2000-2022年的NDVI的tif文件,可以参考我这篇博客获取NDVI数据GEE平台获取指定区域DEM。我将这些文件命名为2000_NDVI.tif、2021_NDVI.tif等等。

以下是需要导入的包:

import glob
import re
import numpy as np
import rasterio
from pymannkendall import original_test
from scipy.stats import theilslopes
from rasterio.transform import from_bounds

1、我将代码文件与tif文件放在同一文件夹中,首先就是要匹配出所有的tif文件

# 查找当前目录下所有的.tif文件,这些tif文件都只有一个波段
tif_files = glob.glob('*.tif')

虽然匹配到了所有的tif文件,但不是按照年份顺序排列的,所有接下来需要对文件列表重新排序

# 定义一个辅助函数,用于从文件名中提取年份
def extract_year(filename):
    # 使用正则表达式查找文件名中的第一个四位数作为年份
    match = re.search(r'\d{4}', filename)
    if match:
        return int(match.group())  # 返回找到的年份
    else:
        return 0  # 如果没有找到年份,则返回0或任何其他默认值


# 根据年份对文件列表进行排序
sorted_tif_files = sorted(tif_files, key=extract_year)

这样就把所有的tif文件按照年份从小到大排列了

2、获取tif文件所有像元的值,并将这些tif文件像元的值组合起来形成像元矩阵

# 创建像元矩阵
pixels_matrix = []
for i in sorted_tif_files:
    pixels_list = []
    with rasterio.open(i) as src:
        data = src.read(1)  # 读取第一个波段
        for row in data:
            for value in row:
                pixels_list.append(value)
    pixels_matrix.append(pixels_list)

这样就得到了像元矩阵,矩阵中的行代表某一年的所有像元值。固定列,从第一行到最后一行,即是某一个像元的时间序列。这样获取某一列数据就代表某个像元的时间序列数据,或者可以对矩阵进行转置,这样每一行就代表某个像元的时间序列,这里直接采用列数据。

# 获取像元矩阵中每一列的元素,即NDVI时间序列
# 使用zip函数解压缩得到列
columns = list(zip(*pixels_matrix))
# 将每列转换为列表
columns = [list(column) for column in columns]

3、根据β和Z值的分类表创建趋势分类函数

# 分类规则
def classify_trends(beta, z):
    if np.isnan(beta) or np.isnan(z):
        return np.nan

    if beta == 0:
        return 0  # 无变化

    if beta > 0:
        if z > 2.58:
            return 4  # 极显著增加
        elif 1.96 < z <= 2.58:
            return 3  # 显著增加
        elif 1.65 < z <= 1.96:
            return 2  # 微显著增加
        else:
            return 1  # 不显著增加

    elif beta < 0:
        if z > 2.58:
            return -4  # 极显著减少
        elif 1.96 < z <= 2.58:
            return -3  # 显著减少
        elif 1.65 < z <= 1.96:
            return -2  # 微显著减少
        else:
            return -1  # 不显著减少

4、利用Theil-Sen Median斜率估计和Mann-Kendall趋势分析计算每个像元序列的β和Z值,并根据分类函数进行分类

classify_result = []
# 获取每个序列的β(slope)和Z值,并根据分类函数赋予指定的分类标量
for j in columns:
    j_array = np.array(j)
    has_nan = np.isnan(j_array).any()
    if has_nan:
        classify_result.append(np.nan)
    else:
        # 1. Theil-Sen Median 斜率估计
        slope, intercept, lower, upper = theilslopes(j, range(len(j)))

        # 2. Mann-Kendall 检验
        mk_result = original_test(j)
        z_value = mk_result.z
        classify_result.append(classify_trends(slope, z_value))

这样就获取到了每个像元的分类结果

5、将分类结果写入到新的tif文件中

# 将分类好的结果写入到新的tif文件中
# 打开原始TIFF文件
with rasterio.open(sorted_tif_files[0]) as src:
    # 获取源文件的高度和宽度
    height, width = src.shape
    # 确保新值列表长度与原图大小一致
    if len(classify_result) != height * width:
        raise ValueError("The length of the new values list does not match the size of the TIFF file.")

    # 将一维列表转换为二维数组
    new_data = np.array(classify_result).reshape(height, width)

    # 读取源文件的元数据
    meta = src.meta
    # 获取图像的边界
    bounds = src.bounds
    # 如果需要,可以调整元数据
    # 比如改变数据类型
    # meta.update(dtype=new_data.dtype)  # 根据你的new_data类型调整
# 写入新的像元值到TIFF文件
with rasterio.open('final.tif', 'w', **meta) as dst:
    # 将new_data写入到dst
    dst.write(new_data, 1)  # 假设只有一个波段,所以是1

    # 如果需要设置地理参考信息
    if not dst.crs:
        dst.crs = src.crs  # 使用源文件的坐标系统
    if not dst.transform:
        dst.transform, _ = from_bounds(*bounds, width, height)

这样就获取到了最终分类结果的final.tif文件

完整代码如下:

import glob
import re

import numpy as np
import rasterio
from pymannkendall import original_test
from scipy.stats import theilslopes
from rasterio.transform import from_bounds

# 查找当前目录下所有的.tif文件,这些tif文件都只有一个波段
tif_files = glob.glob('*.tif')


# 定义一个辅助函数,用于从文件名中提取年份
def extract_year(filename):
    # 使用正则表达式查找文件名中的第一个四位数作为年份
    match = re.search(r'\d{4}', filename)
    if match:
        return int(match.group())  # 返回找到的年份
    else:
        return 0  # 如果没有找到年份,则返回0或任何其他默认值


# 根据年份对文件列表进行排序
sorted_tif_files = sorted(tif_files, key=extract_year)

# 创建像元矩阵
pixels_matrix = []
for i in sorted_tif_files:
    pixels_list = []
    with rasterio.open(i) as src:
        data = src.read(1)  # 读取第一个波段
        for row in data:
            for value in row:
                pixels_list.append(value)
    pixels_matrix.append(pixels_list)

# 获取像元矩阵中每一列的元素,即NDVI时间序列
# 使用zip函数解压缩得到列
columns = list(zip(*pixels_matrix))
# 将每列转换为列表
columns = [list(column) for column in columns]


# 分类规则
def classify_trends(beta, z):
    if np.isnan(beta) or np.isnan(z):
        return np.nan

    if beta == 0:
        return 0  # 无变化

    if beta > 0:
        if z > 2.58:
            return 4  # 极显著增加
        elif 1.96 < z <= 2.58:
            return 3  # 显著增加
        elif 1.65 < z <= 1.96:
            return 2  # 微显著增加
        else:
            return 1  # 不显著增加

    elif beta < 0:
        if z > 2.58:
            return -4  # 极显著减少
        elif 1.96 < z <= 2.58:
            return -3  # 显著减少
        elif 1.65 < z <= 1.96:
            return -2  # 微显著减少
        else:
            return -1  # 不显著减少


classify_result = []
# 获取每个序列的β(slope)和Z值,并根据分类函数赋予指定的分类标量
for j in columns:
    j_array = np.array(j)
    has_nan = np.isnan(j_array).any()
    if has_nan:
        classify_result.append(np.nan)
    else:
        # 1. Theil-Sen Median 斜率估计
        slope, intercept, lower, upper = theilslopes(j, range(len(j)))

        # 2. Mann-Kendall 检验
        mk_result = original_test(j)
        z_value = mk_result.z
        classify_result.append(classify_trends(slope, z_value))

# 将分类好的结果写入到新的tif文件中
# 打开原始TIFF文件
with rasterio.open(sorted_tif_files[0]) as src:
    # 获取源文件的高度和宽度
    height, width = src.shape
    # 确保新值列表长度与原图大小一致
    if len(classify_result) != height * width:
        raise ValueError("The length of the new values list does not match the size of the TIFF file.")

    # 将一维列表转换为二维数组
    new_data = np.array(classify_result).reshape(height, width)

    # 读取源文件的元数据
    meta = src.meta
    # 获取图像的边界
    bounds = src.bounds
    # 如果需要,可以调整元数据
    # 比如改变数据类型
    # meta.update(dtype=new_data.dtype)  # 根据你的new_data类型调整
# 写入新的像元值到TIFF文件
with rasterio.open('final.tif', 'w', **meta) as dst:
    # 将new_data写入到dst
    dst.write(new_data, 1)  # 假设只有一个波段,所以是1

    # 如果需要设置地理参考信息
    if not dst.crs:
        dst.crs = src.crs  # 使用源文件的坐标系统
    if not dst.transform:
        dst.transform, _ = from_bounds(*bounds, width, height)

结果展示

由于我每个tif文件都包含四百多万个像元,所以运行上述代码比较耗时。这里我以2000-2004年的变化趋势为例,大概运行了几分钟。下面是QGIS展示的结果,可以看到大部分区域都是不显著减少或者增加,有少量地区出现了微显著增加。

参考文献

Liu, Chuanxiu, et al. “Impact of climatic and geomorphologic drivers on sediment connectivity in the Tarim River Basin, China.” Journal of Hydrology 643 (2024): 132027.

作者:彭博锐

物联沃分享整理
物联沃-IOTWORD物联网 » python进行Theil-Sen Median斜率估计和Mann-Kendall趋势分析

发表回复