宙畑 Sorabatake

解析ノートブック

【漁業の今を知る】地球温暖化で魚は北上している!? 208漁港の魚種別漁獲量の時系列データをpythonで可視化

地球温暖化によって、獲れるはずの場所で魚が獲れなくなっている……。そんなニュースを知った宙畑編集部はオープンデータからその傾向が分かるのか調査してみました。

「地球温暖化の影響でこれまで獲れていた魚が北上しているのかもしれませんね」

そんな疑問が生まれたのは大分でサワラ網漁を営む漁師、江本さん(※)から最近サワラが獲れなくなったと聞いた時でした。

沿岸漁業で衛星データ活用チャレンジ! ~大分のサワラ流し網漁編~

たしかに、地球温暖化の影響により
日本で放流した鮭がロシアに流れてロシアで漁獲されている
ふぐの生態系が変わって、複数のふぐが交配するようになって毒の持ち方が異なるふぐの雑種が現れている
といった地球温暖化にまつわるウワサ話を耳にする機会が増えたように思います。

そこで、過去10年間の208漁港別の上場水揚量データを用いてその実態を確かめてみました。

本当に魚たちは北へ北へと向かっているのか!?

(1)利用するデータの紹介「水産物流通調査」と仮説設定

まず、魚の上場水揚量とその場所を把握するため、本記事では「水産物流通調査、産地上場水揚量・卸売価格(208漁港)、漁港別品目別上場水揚量・卸売価格」を利用することとしました。

この調査では以下のようなデータを取得できます
期間:2009年から2019年までの11年間
漁港:208漁港
魚種:113種(生・冷凍の分類やその他含む)
数値:上場水揚げ量(t)、卸売価格(円/kg)
※ファイル形式:EXCEL,PDF,HTML

さて、あらためて、今回検証したい仮説を整理すると
地球温暖化が進み、特定の魚が北上しているのではないか
ということです。

まずはそもそも地球温暖化が直近10年で起きているかどうかですが、地球温暖化の推移について、NASAが発表しているものがありました。

1880 - 2019年の平均海面水温の変化 Credit : U.S. Global Change Research Program

この図から、20世紀の平均海面水温を基準とすると、全球的に1980年以降温暖化の傾向が見られることが分かります。

2004年から2020年までの1月の海面水温の推移
Source : MUR-JPL-L4-GLOB-v4.1(Sea Surface Temperature (L4, MUR))

MODISにより観測された、2004年から2020年までの日本近海の1月の平均海面水温の変化を可視化すると上記のようになります。年によって同一地点であっても海面水温が異なることがわかるでしょうか?ご自身でデータを閲覧したい方はこちらからご覧になれます。

つまり、今回利用する調査データを用いて以下のようなことが可視化できればその仮説がある程度確からしいと言えるでしょう。

「漁港別の上場水揚量データを時系列に可視化すると、特定の魚の上場水揚量が、年が経過するごとに南の漁港で減り、北の漁港で増えている」

それでは、実際にそのようなデータになっているのか、さっそく確認していきましょう。

(2)検証結果とより精緻に検証するために考慮すべきこと

1.検証結果

今回は一目見れば分かるような可視化を目指し、日本地図上に漁港別の上場水揚量が時系列で変化するgifデータを作成することにしました。そのようにしてできたサワラの上場水揚量タイムラプスが以下になります。

上記はサワラの上場水揚量を表しています。たしかに最近になるにつれて日本海北側、太平洋北側でも水揚げがされて、九州の上場水揚量は減っているようにも見えます。

では、先に述べたウワサ話「日本で放流した鮭がロシアに流れてロシアで漁獲されている」についても見てみましょう。

※ふぐについては残念ながらフグ類とまとめられていたため検証ができませんでした。

以下は鮭の上場水揚量推移になります。

たしかに2014年から北海道の上場水揚量が減っていますね。あとはロシアの鮭の漁獲量データの推移ですが、こちらもたしかに2014年から急に漁獲量が増えているようです。

Credit : 水産庁、国立研究開発法人 水産研究・教育機構

MODISで海面水温を可視化すると下記のようになります。

Source : MUR-JPL-L4-GLOB-v4.1(Sea Surface Temperature (L4, MUR))

心なしか青い温度領域が北上しているようにも見えるでしょうか。

以上、「漁港別の上場水揚量データを時系列に可視化すると、特定の魚の上場水揚量が、年が経過するごとに南の漁港で減り、北の漁港で増えている」という仮説を持って、データを可視化し、特定の魚種について、漁獲量ベースで魚が北上していることが確認できました。

おまけとして、他の魚種がどうだったか、メジャーな魚種を並べてみたのでこちらもご覧ください。

ご覧いただくと分かりやすいのは北海道料理の代名詞「ホッケ」も漁獲量が極端に減っていることが分かりますね。

さて、本当にこれだけで魚が地球温暖化の影響で北上していると言えるのでしょうか。

ここからは余談になりますので、データの可視化方法を知りたい!という方は3章をご覧ください。

より精緻に検証を行うのであれば、上場水揚量データを確認する場合は「各漁港に水揚げする漁師さんの事情」「違法漁船問題」「漁獲制限」といった社会的事情にも考慮が必要です。

2.上場水揚量推移をより精緻に検証する上で知っておきたいこと

■各漁港に水揚げする漁師さんの事情

まず、「各漁港に水揚げする漁師さんの事情」について、大分の漁師江本さんも「最近はサワラが獲れなくなってきたから、さわら漁をする漁師自体も減ってる」というお話をされていました。

また、これは漁師に限らず第一次産業の課題としても上げられる問題で「後継者不足」というものがあります。

少し古いデータではありますが、以下のように漁業就業者人口は実際に減り続けています。

Credit : Ministry of Agriculture, Forestry and Fisheries

それに対して漁業者一人当たりの生産量が就業者人口の減りをカバーするほど増えているかというとそのようなこともなく、漁業所得は増えているという状況です。

Credit : Ministry of Agriculture, Forestry and Fisheries

つまり、生産量が減り、その結果、卸売価格があがっているのです。実際に本記事で利用したデータでも10年前と比較して卸売価格が上がった魚種が108種中71種と、60%以上の魚種の値段が上がっていました。

他にも、さんま漁が不漁であるためまいわし漁に切り替えた漁師も増えているという話もあります。

■違法漁船問題

また、その他の社会的事情として、違法漁船問題があります。現在日本海を中心に違法漁船が問題となっています。特にさんまやするめいかは違法漁船問題により上場水揚量が大きく減っているというニュースを見かける機会が増えました。

■漁獲制限

最後に、乱獲による持続的な生態系破壊を抑止するための国際的な漁獲制限も考慮したい社会的事情と言えるでしょう。

例えば、2014年9月に開催された中西部太平洋マグロ類委員会(WCPFC)では、クロマグロについて、未成魚(魚体重30キログラム未満)の漁獲量を02~04年度の平均値の「半減」するという保存管理措置が決定しました。このように、国際的な取決めでそもそも獲れたとしても今後の資源管理のために漁獲量を意図的に減らしているという可能性があります。

以上、漁業にまつわる社会的な事情について、本章では紹介しました。より精緻に検証したいという方はぜひ上記のような背景を参考にデータを見ていただければと思います。

(3)コード紹介

では、最後に本記事で利用したデータの可視化をするコードをご紹介します。

 

今回は漁獲量データを用いましたが、応用すれば以下のようなこともできるでしょう。

 
・各国の特定の生産物の生産量推移を可視化
・各国の森林増加率、または減少率を可視化
・各国の新型コロナウイルスの発生者数を可視化
・年次を日時に変更し、ロケットの打ち上げ状況を可視化
など

ぜひ、気になることがあればお試しください。

今回は以下の流れで解析していきます。
1. 漁港の名前から緯度経度を取得
2. 各地の年間漁獲量と緯度経度から各年の可視化画像を作成
3. 作成した画像を項目ごとにgif化

1.漁港の名前から緯度経度を取得

緯度経度を取得するために必要なライブラリをimportします。

import geocoder
import pandas as pd
import codecs

次に、「水産物流通調査]」から取得したデータをcatch_data.csvとしてtellusの開発環境にアップロードしてください。
※同じデータを利用する場合はこちらをご利用ください

その後、pandas形式で読み込みます。

with codecs.open("./catch_data.csv", "r", "Shift-JIS", "ignore") as file:
    df = pd.read_table(file, delimiter=",")
    print(df)

読み込みが終われば、緯度経度を取得する関数を定義します。

from typing import NewType, Optional, Tuple, List

Lat = NewType('Lat', str)
Lng = NewType('Lng', str)
LatLng = Tuple[Lat, Lng]

def getLatLng(place_name: str) -> LatLng or None:
    try:
        ret = geocoder.osm(place_name, timeout=5.0)
        latlng = (ret.latlng[0], ret.latlng[1])
        return latlng
    except:
        return None

この関数を使用して、以下のように緯度経度情報を追加します。

df["LatLng"] = df["漁港"].map(getLatLng)

緯度経度の取得は時間がかかるため、取得が終了した時点で一度保存しておきましょう。

df.to_csv("./catch_with_latlng.csv")

2.各地の年間漁獲量と緯度経度から各年の可視化画像を作成

可視化のために、cartopyというライブラリを使用しますが、初期状態のtellusの開発環境上にはないので先にインストールします。

!pip install cartopy

次に、可視化に使用するライブラリをimportします。

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.io.shapereader as shpreader
import matplotlib.pyplot as plt
import itertools

先程保存した緯度経度情報を含む漁獲量データをpandas形式で読み込みます。

df = pd.read_csv('./catch_with_latlng.csv')

可視化に使用する関数を定義します。今回使用するデータにはエラー値が多く含まれているため、checkErrorという関数を定義しています。他のデータで使用する際、エラー値の除去に使用してください。

import typing
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from operator import add

max_marker_size = 20

def toTuple(string: str) -> typing.Tuple[float, float]:
    return tuple(map(float, string.strip("(").strip(")").split(', '))) 

def removeWhiteSpace(string: str) -> str:
    return string.replace(" ", "").strip(" ")

def checkError(r, year):
    error = []
    isError = False
    
    if r["LatLng"] == "":
        error.append("No LatLng Data")
        isError = True
    try:
        latlng = toTuple(r["LatLng"])
    except:
        error.append("LatLng cannot convert to Tuple Type")
        isError = True
    if r[" 上場水揚量 "] == "- " or r[" 上場水揚量 "] == "-":
        error.append("No Catch Data")
        isError = True
    if r["年"] != year:
        error.append("This is not target year")
        isError = True
    return (error, isError)

def initializeMatplotArea(fig):
    ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
    ax.coastlines(resolution="10m")
    ax.set_extent([150, 125, 50, 25], ccrs.PlateCarree())
    return ax

def drawAll(fig, draw_list, max_catch_size, year):
    ax = initializeMatplotArea(fig)
    
    ax.text(0.1, 0.9, year, ha='center', va='center', transform=ax.transAxes, fontsize=12)
    
    for draw_content in draw_list:
        latlng = draw_content["latlng"]
        catch_size = draw_content["catch_size"]
        ax.plot(latlng[1], latlng[0], 'o', transform=ccrs.PlateCarree(),markersize=(catch_size / max_catch_size) * max_marker_size , color = (1, 0, 0, 0.3))
    
    return ax
        
def createImages(targetContent: str) -> None:
    !mkdir ./output/{targetContent}
    df_1 = df[df["項目"] == targetContent]
    df_1 = df_1[df_1["LatLng"] != ""]
    df_1 = df_1[df_1[" 上場水揚量 "] != "-"]
    df_1 = df_1[df_1[" 上場水揚量 "] != "-"]
    yearList = df_1["年"].unique()
    yearList.sort()
    
    draw_lists = []
    
    for year in yearList:
        draw_list = []
        fig = plt.figure()

        for index, r in df_1.iterrows(): 
            try:
                error, isError = checkError(r, year)
                if isError:
                    continue
                clearedString = removeWhiteSpace(r[" 上場水揚量 "])
                catch_size = float(removeWhiteSpace(clearedString))
                a = r["漁港"]
                latlng = toTuple(r["LatLng"])
                draw_list.append({ "latlng": latlng, "catch_size": catch_size })
            except Exception as e:
                continue
        
        draw_lists.append(draw_list)
        
    sorted_draw_list = sorted(itertools.chain.from_iterable(draw_lists), key=lambda x:x['catch_size'], reverse=True)
    max_catch_size = sorted_draw_list[0]['catch_size']

    ims = []
    for index, draw_list in enumerate(draw_lists):
        fig = plt.figure()
        ax = drawAll(fig, draw_list, max_catch_size, yearList[index])
        fig.savefig(f"./output/{targetContent}/{yearList[index]}.png")

これらの関数を使用して、画像を作成します。

for content in contentList:
    createImages(content)

これらの関数を使用して、画像を作成します。

for content in contentList:
    createImages(content)

このコードを実行すると、outputというディレクトリ内に画像が出力されます。

3.作成した画像を項目ごとにgif化

作成した画像を項目ごとにgif化する関数を定義します。

from PIL import Image
import glob

!mkdir ./output/gif/

def createAnimation(targetContent: str):
    try:
        files = sorted(glob.glob(f'./output/{targetContent}/*.png'))  
        images = list(map(lambda file : Image.open(file) , files))
        images[0].save(f'./output/gif/{targetContent}.gif' , save_all = True , append_images = images[1:] , duration = 1000 , loop = 0)
    except:
        return

この関数を使用して、gifアニメーションを作成します。

for content in contentList:
    createAnimation(content)

このコードを実行すると、outputの中のgifディレクトリにgifアニメーションが保存されていきます。

一覧として表示したい場合は、以下のようにします。

def gallery(array, ncols=3):
    nindex, height, width, intensity = array.shape
    nrows = nindex//ncols
    result = (array.reshape(nrows, ncols, height, width, intensity)
              .swapaxes(1,2)
              .reshape(height*nrows, width*ncols, intensity))
    return result

def create_gallery(contentList):
    images = []
    files = sorted(glob.glob(f'./output/{contentList[0]}/*.png'))  
    yearList = list(map(lambda files: files.split("/")[-1].split(".")[0], files))
    for year in yearList:
        images = []
        for content in contentList:
            images.append(np.asarray(Image.open(f'./output/{content}/{year}.png').convert('RGB')))
        array = np.array(images)
        result = gallery(array, 3)
        im = Image.fromarray(result)
        im.save(f"./gallery/{year}.jpg")

    files = sorted(glob.glob(f'./gallery/*.jpg'))  

    images = list(map(lambda file : Image.open(file) , files))
    images[0].save(f'./gallery/output.gif' , save_all = True , append_images = images[1:] , duration = 1000 , loop = 0)

この関数を使用し、表示したい項目を配列として渡すと、一覧表示のgifがgalleryディレクトリの中にoutput.gifとして保存されます。

create_gallery(["かつお(生)", "かつお(冷)" , "まぐろ(生)", "まぐろ(冷)", "ほっけ(生)", "さんま", "あまだい", "かき(むき身)", "かき(殻付)", "まいわし", "まあじ", "べにずわいがに"])

このコードを実行すると以下の画像が出力されます。

まとめ

以上、大分の漁師さんと衛星データを用いた漁業について話した際の「本当に魚は地球温暖化に北上しているんだろうか」という些細な疑問から、漁港別の漁獲量をまとめたオープンデータを用いてを時系列で可視化して確認してみました。

実際に漁獲量が減っている魚がいるということは可視化できましたが、本当に北上していたかどうかは様々な要因も絡んでおり、一概に地球温暖化が原因だとは言えない結果となりました。

本記事では漁獲量を可視化し、その要因として考えられるものをあげたのみでしたが、衛星データは漁場を見つけるだけでなく、地球温暖化の現状把握や違法漁船の検知にも用いられています。

今後、機会があれば宙畑でも衛星データをフル活用して日本の水産業の現状を確認してみたいと思います。