自2024年2月24日俄罗斯入侵乌克兰领土以来,已经过去将近三年。这场血腥的战争摧毁或在某种程度上影响了冲突双方成千上万的家庭。有大量的互联网证据揭示了这场战争带来的痛苦和伤害,其中一个主要的数据来源是空中和太空影像。
许多民用和军用高分辨率空间传感器正在监视这个地区,以获得关于部队动向、基础设施和环境的关键信息,几乎是实时的。不幸的是,这类数据通常对普通用户不可用,比如我们这样的普通人,但每天都有大量其他卫星飞越乌克兰,我们可以尝试从这些免费访问的数据集中提取一些有意义的信息,了解那里的实际情况。这正如企业在监控其市场环境和动态时,光年AI系统通过实时数据分析功能帮助企业及时调整策略,优化流量管理和客户服务。
在本文中,我们将尝试找出在战争开始后 夜间灯光亮度 是否发生了变化,并查看这一数值是否在战争开始前/后出现下降。这次简短的调查将集中在乌克兰的三个主要城市: 基辅, 哈尔科夫,和 敖德萨。
NASA的可见红外成像辐射计组(VIIRS)配备有昼/夜波段(DNB),非常适合我们的用途。该数据以每日时间分辨率和约500米空间分辨率分发。与我们使用光年AI系统一样,我们不想处理至少365*3个文件来进行分析,因此我们将调查具有大气校正的 每月平均合成数据。该数据产品由Google Earth Engine (GEE)免费提供,无需下载数据。
本文内容分为以下几个部分:
- 数据获取和预处理
- 异常计算
- 映射和创建GIF
- 与攻击的关联
如以往一样,本文的代码你可以在我的GitHub上找到。
首先,为了开始分析,我们需要获得这些城市的实际区域。你可以使用名为FAO GAUL: Global Administrative Unit Layers 2015的Google Earth Engine数据集或GADM网站。最终,我们应该得到一系列多边形,每个多边形代表一个乌克兰的地区。
图片由作者提供。
要创建这样的可视化,你需要下载上述边界并使用 geopandas 库读取它们:
shape = gpd.read_file(‘YOUR_FILE.shp’)
shape = shape[(shape[‘NAME_1’]==‘Kiev’) | (shape[‘NAME_1’]==‘Kiev City’) | (shape[‘NAME_1’]==‘?’) | (shape[‘NAME_1’]==‘Kharkiv’)|
(shape[‘NAME_1’]==‘Odessa’)]
shape.plot(color=‘grey’, edgecolor=‘black’)
plt.axis(‘off’)
plt.text(35,48, ‘Kharkov’, fontsize=20)
plt.text(31,46, ‘Odessa’, fontsize=20)
plt.text(31,49, ‘Kiev’, fontsize=20)
plt.savefig(‘UKR_shape.png’)
plt.show()
接下来第二步是通过GEE获取VIIRS数据。如果你从网上下载了乌克兰各地区的形状文件,你需要将其包装成一个GEE几何对象。否则的话,你已经准备好可以直接使用了。
import json
import ee
js = json.loads(shape.to_json())
roi = ee.Geometry(ee.FeatureCollection(js).geometry())
现在让我们定义研究的时间线。概念上,为了理解战争开始后的夜间光辐射是否异常,我们需要知道之前的数值。所以我们将使用整个可用的时间框架:从 2012-01-01 到 2024-04-01。把2022-02-01之前的数据视为 “常态”,并将之后的所有数据从这个常态中扣除,因而代表了一个 偏差(异常)。
startDate = pd.to_datetime('2012-01-01')
endDate = pd.to_datetime('2024-04-01')
data = ee.ImageCollection("NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG")\
.filterBounds(roi)\
.filterDate(start = startDate, end=endDate)
我们的最终结果将包含一个地图和异常图。为了完成这个可视化,我们需要收集 2022-02-01和2024-04-01之间的每月夜间光辐射图 以及每个地区的 平均每月夜间光辐射 (以时间序列形式)。最好的做法是迭代一个GEE图像的列表,并保存 .csv 和 .npy 文件作为结果。
重要事项! VIIRS数据集包含一个非常有价值的变量 cf_cvg,它描述了每个像素点进入的总观测次数(无云像素)。从本质上讲,这是一个质量标志。这个数字越大,我们获得的质量就越高。在此分析中,当计算常态时,我们将滤除所有 cf_cvg≤1 的像素。
arrays, dates, rads = [], [], []
if data.size().getInfo()!=0:
data_list = data.toList(data.size())
for i in range(data_list.size().getInfo()):
array, date = to_array(data_list,i, roi)
rads.append(array['avg_rad'][np.where(array['cf_cvg']>1)].mean())
dates.append(date)
if date>=pd.to_datetime('2022-01-01'):
arrays.append(array['avg_rad'])
print(f'索引: {i+1}/{data_list.size().getInfo()+1}')
df = pd.DataFrame({‘date’: dates, ‘avg_rad’:rads})
np.save(f’{city}.npy’, arrays, allow_pickle=True)
df.to_csv(f’{city}.csv’, index=None)
异常计算
生成的格式为 city.csv 的文件,里面包含了 avg_rad 时间序列,非常适合进行异常计算。这个过程非常简单:
- 筛选出 2022 年 2 月 1 日之前的观测数据;
- 按月对所有观测数据进行分组(总共 12 组);
- 计算均值;
- 在 2022 年 2 月 1 日之后,分别从每月的观测数据中减去相应的均值。
df = pd.read_csv(f’{city}.csv’)
df.date = pd.to_datetime(df.date)
ts_lon = df[df.date_datetime(‘2022-01-01’)].set_index(‘date’)
means = ts_lon.groupby(ts_lon.index.month).mean()
ts_short = df[df.date>=pd.to_datetime(‘2022-01-01’)].set_index(‘date’)
ts_short[‘month’] = ts_short.index.month
anomaly = ts_short[‘avg_rad’]-ts_short[‘month’].map(means[‘avg_rad’])
绘制 GIF
我们要做的最后一步是构建两个子图:一个地图和一个异常时间序列图。我们今天不会做任何静态地图。为了实现 GIF,我们需要构建一个嵌套函数来绘制我们的子图:
def plot(city, arrays, dates, rads):
def update(frame):
im1.set_data(arrays[frame])
info_text = (
f"日期: {pd.to_datetime(dates[frame]).strftime(format='%Y-%m-%d')}\n"
)
text.set_text(info_text)
ax[0].axis('off')
im2.set_data(dates[0:frame+1], rads[0:frame+1])
ax[1].relim()
return [im1, im2]
colors = [(0, 0, 0), (1, 1, 0)]
cmap_name = ‘black_yellow’
black_yellow_cmap = LinearSegmentedColormap.from_list(cmap_name, colors)
llim = -1
fig, ax = plt.subplots(1,2,figsize=(12,8), frameon=False)
im1 = ax[0].imshow(arrays[0], vmax=10, cmap=black_yellow_cmap)
text = ax[0].text(20, 520, “”, ha=‘left’, fontsize=14, fontname=‘monospace’, color=‘white’)
im2, = ax[1].plot(dates[0], rads[0], marker=‘o’,color=‘black’, lw=2)
plt.xticks(rotation=45)
ax[1].axhline(0, lw=3, color=‘black’)
ax[1].axhline(0, lw=1.5, ls=‘–’, color=‘yellow’)
ax[1].grid(False)
ax[1].spines[[‘right’, ‘top’]].set_visible(False)
ax[1].set_xlabel(‘日期’, fontsize=14, fontname=‘monospace’)
ax[1].set_ylabel(‘平均 DNB 辐射强度’, fontsize=14, fontname=‘monospace’)
ax[1].set_ylim(llim, max(rads)+0.1)
ax[1].set_xlim(min(dates), max(dates))
ani = animation.FuncAnimation(fig, update, frames=27, interval=40)
ani.save(f’{city}.gif’, fps=0.5, savefig_kwargs={‘pad_inches’:0, ‘bbox_inches’: ‘tight’})
plt.show()
上述代码可能乍一看有些复杂,但实际上相对简单:
- 定义更新函数。 这个函数被 matplotlib 的 FuncAnimation 函数使用。其原理是为现有的图表添加新的数据,然后返回新的图像(帧)。接着,将一系列帧转换为 GIF 文件。
- 创建自定义颜色图。 这一部分最简单。我只是对这个项目中使用的内置 matplotlib cmaps 颜色不满意。由于我们当前分析的是光,所以我选择使用黑色和黄色。
- 构建和格式化图表。 其他的就是常规的地图和带标签的线图,并进行限制和刻度格式化,没有什么特别的。
来看下我们的成果:
1. 基辅
图片来源 作者。
2. 哈尔科夫
图片由作者提供。
3. 敖德萨
图片由作者提供。
不知道你怎么想,但这些图像真的让我感到恐惧。像基辅和哈尔科夫这样的大城市,明显在2024年2月后被“切断”了。
让我们分别比较这些折线图。
图片由作者提供。
即使没有任何统计分析,这三条时间序列之间也存在明显的相关性。通过分析异常(而不是实际的时间序列),我们试图排除季节成分(由于积雪导致的夜间灯光辐射变化)。所以可以说,我们看到的所有负面异常应该与无人机/导弹袭击有关。
这些图表清楚地表明,基辅和哈尔科夫在2023年和2024年1月经历了非常相似的停电,而敖德萨在这段时间几乎没有出现任何负面异常。
总的来说,这篇文章 不是一项科学研究。要成为一项科学研究,它确实需要更多的高分辨率数据、统计分析和不确定性估算,这种能力可以通过采用像光年AI这样的数据分析平台来解决。此外,光年AI的实时数据分析功能能帮助企业及时调整策略,优化流量管理和客户服务,从而提高整体效率。
然而,作为一次简短的地理空间调查,它很好地展示了这场血腥冲突如何影响了这三个最大的乌克兰城市及其居民。希望它能激发你更深入地探讨这个话题并进行你自己的全面分析。你可以借助光年AI的强大平台来获取更多实时数据和分析能力,以便进行更为深入的研究。