この章では大気再解析 (Reanalysis)
を用いた解析を行う。
前章までと大きく異なるのはデータの次元が一気に増えたことである(「時間1次元」->「時間1次元+空間3次元」)。
したがって単純に考えれば、どの軸に沿った図を描くかによって、ラインプロットでは4通り、二次元平面図では$_4\text{C}_2 = 6$通りの
選択肢がある。
再解析データを含むグローバルデータセットの多くはNetCDF形式で提供されている。これは自己記述式
と言われるもので、データのヘッダーにデータに関する様々な情報が記載されている。
ここではNCEP/NCAR再解析を使って、NetCDF形式データの読み込みと情報の縮約、そして二次元データの様々な描画方法について実習を行う。
再解析データでは様々な物理量のデータが提供されているが、本チュートリアルでは月平均東西風データ (uwnd.mon.mean.nc)
を用いることにする(ベクトル場では、これに加えてvwnd.mon.mean.nc
も用いる)。
この章で新たに用いるライブラリは以下の通り:
cartopy: 地図描画ライブラリ (https://scitools.org.uk/cartopy/docs/latest/)
xarray: 多次元配列解析ライブラリ(http://xarray.pydata.org/en/stable/index.html)
xarrayはpandasをベースにした多次元配列解析ライブラリである。pandasでは一次元(Series型)あるいは二次元(DataFrame型)までしか扱えなかった(注)が、これを多次元に拡張したものと考えることができる。
(注)正確にはDataFrameのインデックスに階層構造を持たせることで扱う次元を増やすこともできる(階層型インデックス)が、少し面倒。
XArrayを用いる最大の利点は、データ配列に軸(インデックス)の情報が結合したオブジェクトのまま解析できる点である。ここで軸とは経度・緯度・高度・時間の情報である。軸情報を保持しているので、2章で紹介したようにメソッドを用いて時系列解析や描画を行うことができる。
一方で、XArrayのメソッドで行える解析・描画には限界がある。アドバンストな解析・描画をするには、xarrayで読み込んだデータを数値配列(NumPy配列)に変換して解析を行い、再びXArrayに戻して描画を行えばよい。例えば、XArrayのオブジェクトについては.values
の属性を参照することで数値データ を取り出せる(軸情報は落とされる)。【練習問題 (4)】や、3.5節のベクトル場プロットを参照されたい。
以下、3.1-3.4において
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import xarray as xr
import cartopy.crs as ccrs
import cartopy.util as cutil
import cartopy.feature as cfeature
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
#from pandas.plotting import register_matplotlib_converters
#register_matplotlib_converters()
Warning: ecCodes 2.21.0 or higher is recommended. You are running version 2.17.0
#月平均東西風データを指定
file = "../../DB_2020/data/ncep.reanalysis.derived/pressure/uwnd.mon.mean.nc" # --> 【パスを適宜変更すること!!!!】
#データの読み込み
data = xr.open_dataset(file)
#データ構造の確認
print(data)
#必要な変数("uwnd")の切りだし
uwnd = data["uwnd"]
<xarray.Dataset> Dimensions: (level: 17, lat: 73, lon: 144, time: 855) Coordinates: * level (level) float32 1e+03 925.0 850.0 700.0 ... 50.0 30.0 20.0 10.0 * lat (lat) float32 90.0 87.5 85.0 82.5 80.0 ... -82.5 -85.0 -87.5 -90.0 * lon (lon) float32 0.0 2.5 5.0 7.5 10.0 ... 350.0 352.5 355.0 357.5 * time (time) datetime64[ns] 1948-01-01 1948-02-01 ... 2019-03-01 Data variables: uwnd (time, level, lat, lon) float32 ... Attributes: title: monthly mean u wind from the NCEP Reanalysis description: Data from NCEP initialized reanalysis (4x/day). These ar... platform: Model Conventions: COARDS NCO: 20121012 history: Mon Jul 5 22:36:33 1999: ncrcat uwnd.mon.mean.nc /Datase... References: http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reana... dataset_title: NCEP-NCAR Reanalysis 1
csvファイルと異なり(1節)、データの読み込みが圧倒的に楽であることがわかる。
データ配列だけでなく、軸情報やその他の属性も含むオブジェクト(Dataset型)になっていることが分かる。
これから以下の情報が読み取れる:
次元(Dimensions): (緯度:73, レベル: 17, 経度: 144, 時間: 855)
軸(Coordinates)の値
レベル(level)
: 1000, 925, 850, 700, ... 緯度(lat)
: 90, 87.5, 85.0, ...経度(lon)
: 0, 2.5, 5.0, ...時間(time)
: 1948-01-01, 1948-02-01,...データ変数 (Data variables): uwnd
データ属性(Attributes): ...
この情報を確認した上で、まずは必要な変数を切り出しておく(uwnd = data["uwnd"]
)
.sel(軸1の名前=軸1の値,軸2の名前=軸2の値,method=,..)
(.isel()
の場合は、(値でなく)整数インデックスで指定)
method
: None
(一致するもののみ;デフォルト)、nearest
(指定した点に一番近い点を取ってくる)
範囲で指定する際にはslice(start,stop,step)
を用いる。ただし、データの並び(降順か昇順か)には注意。この例では、緯度と気圧レベルは降順になっているので(上の"Coordinates"を見ればわかる)、例えば30N-50Nの領域を抜き出す際には、lat=slice(50,30)
とする必要がある。
pandasにおける切り取りと同じ。各軸の範囲をラベル(日付、経度など)もしくはインデックス位置を用いて指定する。
軸の並びに注意すること:ここでは(time, level, lat, lon)
の順に並んでいる。
pandasと同様に、条件を指定してデータを切り出すこともできる。
時刻軸のデータは(上の軸情報を参照して)
index = uwnd['time']
などとして取得できるが、ここから月・日などの情報抜き出すにはさらに.dt
をつける:
month = index.dt.month
(月の情報を抜き出す)
(参考)DateimeIndex属性の一覧はこちら
ある断面でデータを切り出したり、軸に沿った統計操作を行ったりした際、大きさ1の次元が残ってしまう場合がある(e.g., A(1,1,36,36))。このままの型の配列ではうまく二次元描画できないので、A_sub = A.squeeze()
として、大きさ1の次元を削除する必要がある。
# (1) 京都上空(135E, 35N)における風速の時間変動
# データの切り出し(2010-2018年, 500 hPaレベル)
uwnd_1D = uwnd.sel(lon = 135, lat = 35, time=slice('2010','2018'), level=500)
# 描画
uwnd_1D.plot()
plt.show()
時系列データの解析では、時間方向の処理を仮定していた。4次元データの解析では、どの軸に沿った処理を行うかに常に注意して解析を行う必要がある(例えば「平均」するにしても、時間平均? or 東西平均?)。
xarrayの型にも、pandasと同様に統計量を求めるメソッドが定義されている。
.mean()
, .std()
などのメソッドがある。
ただし、処理を行う軸をaxis=(次元の番号)
、もしくは、dim=(次元の名前)
としてオプションで指定する(指定しなければ全平均の値が出力される)。例えば、data.mean(dim="lon")
(経度平均)やdata.mean(dim=("lon","lat"))
(経度・緯度平均)など。
pandasと同様に、reshape
やgroupby
などのメソッドを使うことで時系列操作も簡単に行える。
ただし、処理を行う軸を明示する必要がある:
uwnd.resample(time="A-DEC").mean(dim="time")
3.2.2を参考に、key = uwnd["time"].dt.month
としておいて、
uwnd.groupby(key).mean(dim="time")
あるいは、
uwnd.groupby("time.month").mean(dim="time")
と簡単に書くこともできる。
XArrayのメソッドでは事足らない場合には、.values
としてndarray型
に変換して適宜解析を行えばよい。
# (1) 季節変化と年平均を図示する発展版
# 図の作成(1行2列のサブプロット)
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
# 月情報を取得(1〜12月)
month_key = uwnd_1D["time"].dt.month
# 季節平均(各月ごとの平均)を計算してプロット
uwnd_1D.groupby(month_key).mean(dim="time").plot(ax=axes[0])
axes[0].set_title("Monthly Mean")
# 年平均(年ごとに平均)を計算してプロット
uwnd_1D.resample(time="A-DEC").mean(dim="time").plot(ax=axes[1])
axes[1].set_title("Inter-annual variaton (annual-mean)")
# 表示
plt.tight_layout()
plt.show()
最後にデータの二次元描画を演習する。最初に述べたようにどの断面で切り出すかを常に意識しながら描画すること。
.plot.contour
,.plot.contourf(ax=,levels=,cmap=,colors=,...)
:
ax=
: 用いるサブプロット領域levels
: レベル。(1) 整数で指定した場合:レベル数。(2) 配列で指定した場合:レベルの値を陽に指定。 cmap
: カラーマップ(e.g., "gray"(白黒)、"bwr"(青-赤)、"rainbow"など。詳細はこちらcolors
: コンター、もしくは、塗りつぶしの色。リストで渡すと各レベルの色を陽に指定する。xincrease
,yincrease
: x/y軸を反転させる場合は"False"(デフォルトは"True")xscale
,yscale
: 軸のスケーリング。"linear", "symlog", "log", "logit"(デフォルトは"linear")cbar_kwargs={}
: カラーバーの調整オプション。例:'orientation': 'horizontal'
(横向きにする); 'label': 'hogehoge'
(カラーバーのラベルをhogehogeにする)。なお、カラーバーはデフォルトで描いてくれる(素晴らしい!)。.plot.contour() メソッドの返り値
を ax.clabel()
に渡すことで等高線のラベルを作成できる。
ax.clabel(cntr, fmt=, fontsize=)
: コンターラベルを作成cntr
: contour()の返り値fmt=
: フォーマット(e.g., "%f3.1", "%3i")fontsize=
: フォントサイズCartopyというライブラリを使って、地図投影を行う。
最低限、サブプロット領域の作成(ax = fig.add_subplot(...)
、および、図の描画(.contourf(...)
)の
段階で、地図投影オプションを指定することで描画できる(cartopyというライブラリを呼び出している)。
ax(サブプロット領域)のcartopy版を作る: ax = fig.add_subplot(projection=)
projection=
のオプションを入れることで地図描画モードになる(入れなければ通常の絵)。projection
の選択肢: ccrs.PlateCarree(central_longitude=)
: 正距円筒図法(緯度・経度をそれぞれ地図の縦・横に読み替えた円筒図法)ccrs.NearsidePerspective(central_latitude=,central_longitude=)
: 宇宙から見た鳥瞰図図を描く: ax.contourf(...,transform = ccrs.PlateCarree())
etc..
transform=
: データ自身の座標系を明示する。再解析データは正距円筒図法でデータが入っているのでccrs.PlateCarree()
とする。詳しい(英語の)説明はこちら[オプション] 目盛りの設定: ax.set_xticks(xticks,crs=)
, ax.set_yticks(yticks,crs=)
xticks
,yticks
: 目盛りの位置crs=
: 目盛り自身の座標系(緯度経度で指定するときはccrs.PlateCarree()
とすればよい; 4.も参照)ax.xaxis.set_major_formatter(lon_formatter)
: 目盛りのラベルを描いている。lon_formatter
は地図描画用のの書式($^\circ$Eとか)。[オプション] 描画領域の設定: ax.set_extent([x0,x1,y0,y2],crs=)
[x0, x1, y1, y2]
: データを描く部分領域を指定するcrs=
: 上記のリストの値の座標系(緯度経度で指定するときはccrs.PlateCarree()
とすればよい; 4.も参照)[オプション] その他の情報を付加
ax.coastlines(resolution=)
ax.set_gridlines()
ax.add_feature(cartopy.feature.LAND)
ax.add_feature(cartopy.feature.OCEAN)
ax.stock_img()
[全球データを使う場合には] オマジナイ: ax.set_global()
(注意) 3 (目盛りの設定)と4(描画領域の設定)の順序を変えてはいけない!変えると、4(範囲)が3によってにリセットされてしまうようである(私はこれを理解するのに大分苦しみました)。
# (2) データの切りだし・平均
uwnd_mean_t = uwnd.sel(time=slice('2010','2018'), level=500).mean(dim="time")
# カラーマップのレベル(等高線の値の範囲と間隔)
clevels = np.arange(-20, 50, 5)
# 図の作成
fig = plt.figure(figsize=(8, 5))
# 地図投影(PlateCarree:正距円筒図法、中心経度を180度に設定)
ax = fig.add_subplot(111, projection=ccrs.PlateCarree(central_longitude=180))
# 等高線塗りつぶしプロット
uwnd_mean_t.plot.contourf(
ax=ax,
transform=ccrs.PlateCarree(), # データの座標系を指定(通常PlateCarreeでOK)
levels=clevels,
cmap='rainbow',
cbar_kwargs={'label': 'U-velocity (m s$^{-1}$)'}
)
# 緯度・経度の目盛り設定
xticks = np.arange(0, 361, 30)
yticks = np.arange(-90, 91, 30)
ax.set_xticks(xticks, crs=ccrs.PlateCarree())
ax.set_yticks(yticks, crs=ccrs.PlateCarree())
ax.xaxis.set_major_formatter(LongitudeFormatter(zero_direction_label=True))
ax.yaxis.set_major_formatter(LatitudeFormatter())
# 描画範囲の設定(経度90〜180度、緯度20〜70度)
ax.set_extent([90, 180, 20, 70], crs=ccrs.PlateCarree())
# 地図のオプション:海岸線とグリッド線
ax.coastlines()
ax.gridlines(draw_labels=False)
# 表示
plt.show()
lon = uwnd_mean_t["lon"]
lat = uwnd_mean_t["lat"]
# 描画設定
fig = plt.figure(figsize=(5, 5))
# projectionのオプションを変更
ax = fig.add_subplot(111, projection=ccrs.NearsidePerspective(central_latitude=90, central_longitude=140))
#データをサイクリックに(360Eの飛びを埋める)
data_cyclic, lon_cyclic = cutil.add_cyclic_point(uwnd_mean_t.values, coord=lon.values)
#↑add_cyclic_pointはNumPy配列に適用できるので、.valuesとしてデータ配列を渡している
# 塗りつぶし図
contour = ax.contourf(
lon_cyclic,
lat,
data_cyclic,
transform=ccrs.PlateCarree(),
cmap="jet",
levels=np.linspace(-10, 30, 9)
)
ax.coastlines()
ax.gridlines(draw_labels=False)
ax.set_global()
plt.colorbar(
contour,
label = 'U-velocity (m s$^{-1}$)',
)
plt.show()
data_mean_xt = # ここを各自埋めよ
## ここから描画-----
# コンターレベルを設定
clevels = np.arange(-20,80,10) # 塗りつぶし用
cnt_levels = np.arange(-20,80,10) #コンター用
fig,ax = plt.subplots(figsize=(8,6))
# ベタ塗り
data_mean_xt.plot.contourf(ax=ax,cmap='rainbow',levels=clevels,yincrease=False,yscale="log")
# Y軸は上下反転させ,かつlog-scaleに(気圧は高度と共に指数関数的に減少するので)
# コンター
ctr = data_mean_xt.plot.contour(ax=ax,levels=cnt_levels,colors='black',yincrease=False,yscale="log")
# コンターラベル
ctr.clabel(fmt='%3i',fontsize=12,colors='black')
# 軸の書式設定
ax.tick_params(labelsize=15)
ax.set_ylabel('Pressure (hPa)',size=15)
ax.set_xlabel('Latitude ($^\circ$N)',size=15) # $...$はTexでの数式の表現
plt.show()
このような図が出れば正解!(対流圏のジェット気流がきれいに見えますね)
XArrayで読み込んだデータを使って多少凝った解析をする(XArrayのメソッドが使えないような解析)には、データ配列(NumPy配列)に変換し、描画の際にXArrayに戻してやるのが楽である。
# 指定時刻・高度のデータを取得して次元を縮小
data_cut = uwnd.sel(time="2010-01", level=500).squeeze()
### 方法 (1): Xarrayを用いた簡便な偏差計算 ###
anom_xr = data_cut - data_cut.mean(dim="lon")
### 方法 (1) ここまで ###
### 方法 (2): NumPy配列に変換して手動で処理する場合 ###
# NumPy配列に変換
data_v = data_cut.values
# データの形状を確認し、同じサイズの空配列を作成
ny, nx = data_v.shape
anom_v = np.empty((ny, nx))
# 経度方向の平均を計算
mean_v = data_v.mean(axis=1)
# 平均を拡張して"ブロードキャスト"し、偏差を計算
anom_v = data_v - mean_v[:, np.newaxis]
# ↑ 以下のように書くとエラーになる:
# anom_v = data_v - data_v.mean(axis=1)
# 理由:配列の形が (73,144) と (73,) で異なり、計算できないため。
# mean_v[:, np.newaxis] により (73,1) の形に変換し、経度方向に平均値を並べることで対応。
# この処理を「ブロードキャスト」という。
# XarrayのDataArrayとして再構築(座標・次元情報を保持)
anom_xr_new = xr.DataArray(anom_v, coords=data_cut.coords, dims=data_cut.dims)
### 方法 (2) ここまで ###
### 結果をプロット(地図描画は省略) ###
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
anom_xr.plot.contourf(ax=axes[0])
anom_xr_new.plot.contourf(ax=axes[1])
plt.show()
(Tips) 東西偏差(東西平均からの偏差)を描画すると見やすい。
(ヒント) 熱帯は例えば、10S-10Nの緯度平均とする。
def read_csv(START_YEAR,END_YEAR,STATION):
#START_YEAR = 1960
#END_YEAR = 2023
DATA_DIR = "../../DB_2020/data/"+STATION+"/"+STATION+"_" #<---【注】"data/AMeDAS/Kyoto/Kyoto_"に変更のこと
ENCODING = "shift-jis"
# データを格納するリスト
dataframes = []
for year in range(START_YEAR, END_YEAR + 1):
file_path = DATA_DIR + str(year) + ".csv"
df = pd.read_csv(
file_path,
encoding=ENCODING,
header=2,
index_col=0,
parse_dates=[0]
)
# 余計なヘッダー行を除去
df = df.iloc[2:, :]
dataframes.append(df)
# すべての年のデータを結合(時間軸方向に)
dfm = pd.concat(dataframes, axis=0)
return dfm
yst = 2018; yen = 2019
#注:演習では yst = 2021; yen = 2023と変更してください(6時間間隔の再解析データが2021年以降しかないため)
dfm = read_csv(yst,yen,"Maizuru")
snow = dfm["降雪量合計(cm)"]
mask = snow > 0
#print(snow[mask])
filesSLP = []; filesAIR = []
for yy in range(yst,yen+1):
cyr = str(yy) # 文字列化
fileSLP = "../../DB_2020/surface/slp."+ cyr +".nc"#<---【注】パスを変更のこと
fileAIR = "../../DB_2020/pressure/air."+ cyr +".nc"#<---【注】パスを変更のこと
filesSLP.append(fileSLP)
filesAIR.append(fileAIR)
slp = xr.open_mfdataset(filesSLP,combine="by_coords")["slp"].sel(lon=slice(110,160),lat=slice(60,20))
# xr.open_mfdataset([file1,file2,...],combine="by_coords")とすると、複数のファイルを纏めて開くことができます
#(ただし、時刻が連続的に並んでいる場合のみ)
air = xr.open_mfdataset(filesAIR,combine="by_coords")["air"].sel(lon=slice(110,160),lat=slice(60,20),level=850)
#日平均値に
slp_daily = slp.resample(time="D").mean(dim="time")
air_daily = air.resample(time="D").mean(dim="time")
#舞鶴で積雪があった日のデータを抜き出す
slp_snow = slp_daily[mask.values,:,:].mean(dim="time")
air_snow = air_daily[mask.values,:,:].mean(dim="time")
## maskを再解析データの時間軸(次元0)に適用;slp_daily[mask]とやってもうまくいかない
# 描画
fig = plt.figure(figsize=(8,5))
# (1) 領域を用意(地図投影オプションをつけて)
# PlateCaree: 正距円筒図法, central_longitude: 図の中心の経度
ax = fig.add_subplot(111,projection=ccrs.PlateCarree(central_longitude=135))
(air_snow-273.15).plot.contourf(ax=ax,levels=np.linspace(-30,20,11),cmap="rainbow",transform=ccrs.PlateCarree()) #unit: K -> degrees C
ctr = (slp_snow/100).plot.contour(ax=ax,levels=np.linspace(980,1050,15),transform=ccrs.PlateCarree()) #unit: Pa -> hPa
#ctr.clabel(fmt='%3i',fontsize=12,colors='black')
ax.coastlines()
plt.show()
ベクトル場をプロットするメソッドは無いので、matplotlibを使う。
fileu = "../../DB_2020/data/ncep.reanalysis.derived/pressure/uwnd.mon.mean.nc" #要変更
filev = "../../DB_2020/data/ncep.reanalysis.derived/pressure/vwnd.mon.mean.nc" #要変更
# ファイルの読み込み
U = xr.open_dataset(fileu)
V = xr.open_dataset(filev)
p_cut = 500; time_cut = '2017-01'
Usub = U['uwnd'].sel(level=p_cut,time=time_cut).squeeze()
Vsub = V['vwnd'].sel(level=p_cut,time=time_cut).squeeze()
# low-resolution data
# (経度-緯度方向は適当に間引いて読み込む; そうしないと、描いたベクトルがギチギチになるので)
dx = 4; dy = 2
U_lr = Usub[::dy,::dx].values #[::c]は "c毎にデータを取得する"]の意味, ".values"はndarrayへの変換(軸情報を落として単なる数値配列に)
V_lr = Vsub[::dy,::dx].values
# 軸もそれに合わせて間引く
lon_lr = Usub['lon'][::dx].values; lat_lr = Vsub['lat'][::dy].values
# ここから描画
fig = plt.figure(figsize=(10,7))
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180))
#ax = plt.axes(projection=ccrs.NearsidePerspective(central_latitude=30,central_longitude=140))
# ベクトル場を描く
Q = ax.quiver(lon_lr,lat_lr,U_lr,V_lr,color='blue',transform=ccrs.PlateCarree())
# 単位ベクトルを描く
ax.quiverkey(Q,0.9,0.8,20,label='20 m s$^{-1}$',labelpos='E',coordinates="figure")
# 目盛り
xticks = np.arange(13)*30
yticks = -90 + np.arange(7)*30
ax.set_xticks(xticks,crs=ccrs.PlateCarree())
ax.set_yticks(yticks,crs=ccrs.PlateCarree())
lon_formatter = LongitudeFormatter(zero_direction_label=True)
lat_formatter = LatitudeFormatter()
ax.xaxis.set_major_formatter(lon_formatter)
ax.yaxis.set_major_formatter(lat_formatter)
ax.coastlines()
#ax.stock_img() # 地形を描く
ax.add_feature(cfeature.LAND)
ax.add_feature(cfeature.OCEAN)
#plt.savefig('test_vector.png') #画面出力だとユニットベクトルが見えないが、保存データにはちゃんと描かれている
plt.show()
C:\Users\takas\Anaconda3\lib\site-packages\cartopy\mpl\geoaxes.py:1841: UserWarning: Some vectors at source domain corners may not have been transformed correctly u, v = self.projection.transform_vectors(t, x, y, u, v)