Yuta NakataのBlog

Python / AWS / ITについて役立つ情報を発信します

【numpyのバグ?】numpy.ma.core.MaskedArrayで意図せずMaskがはずされる場合があるって話

はじめに・前提

numpyにかかるバグ?っぽい挙動を発見したので、その共有記事です。

本内容については、Github issuesとして共有済みです(2024.11.9現在返信待ち)。

github.com

前提となる実行環境は以下のとおりです。

結論

下記のようなプログラムを書くと、numpyでマスクした値が外れるようです。

import numpy as np


a3 = np.array([[-9999, 8, -9999], [1, 100, 2], [-9999, -9999, 2]]) # a3.shapeは、3.3
a3 = np.ma.masked_where(a3 == -9999, a3) # -9999をマスクする
b3 = np.array([[-9999, 8, -9999], [1, 100, 2], [-9999, -9999, 2]]) # b3.shapeは、3.3
b3 = np.ma.masked_where(b3 == -9999, b3) # -9999をマスクする

print(a3) # [[-- 8 --] [1 100 2] [-- -- 2]]
print(b3) # [[-- 8 --] [1 100 2] [-- -- 2]]

x3 = np.zeros(((2, 3, 3))) # a3やb3を含むためのnumpy.ndarrayを定義
x3[0, :, :] = a3
x3[1, :, :] = b3
print(x3)
"""
マスクしたはずの値に対して、マスクが外れる!
[[[-9.999e+03  8.000e+00 -9.999e+03]
  [ 1.000e+00  1.000e+02  2.000e+00]
  [-9.999e+03 -9.999e+03  2.000e+00]]

 [[-9.999e+03  8.000e+00 -9.999e+03]
  [ 1.000e+00  1.000e+02  2.000e+00]
  [-9.999e+03 -9.999e+03  2.000e+00]]]
"""

低次元配列を複数定義し、その低次元配列を高次元配列で覆うように書き換えると、マスクが外れるようです。

ただし、

a = np.array([3, 4, -9999, 8, -9999])
a = np.ma.masked_where(a == -9999, a)

c = np.zeros(len(a))
c = a # [3 4 -- 8 --] マスクが維持される!
print(c)

のように同次元系の配列の場合、マスクは外れないようです。。。

マスクされていることが前提の統計処理等はよく気をつけたほうがいいですね。

numpy.ma.core.MaskedArrayとは

値をマスクするときに使われるnumpyのマスクメソッドです。

欠損・異常値処理において役に立つモジュールです。

a = np.array([3, 4, np.nan, 8, np.nan])
print(a.mean()) # nan
a = np.ma.masked_where(np.isnan(a), a)
print(a) # [3.0 4.0 -- 8.0 --]
print(a.mean()) # 5.0

qiita.com

numpy.ndarrayとは

numpyで多次元配列を定義するために、使われます。

numpyユーザーの中でも最もよく使われるもののひとつではないかと思います。

a3 = np.array([[-9999, 8, -9999], [1, 100, 2], [-9999, -9999, 2]])

高次元の配列も簡単に定義することが可能です。

追記 2024.11.23

上記バグについて、numpy側からフォローがあったので共有します。

結論、

x3 = np.zeros(((2, 3, 3)))

ではなく、

x3 = np.zeros(((2, 3, 3)))
x3 = np.ma.asarray(x3)

とするとよいとのことでした。

私の環境下でも改善が確認できたので、共有します。

GitHub Actionsを用いて、pep8のチェックを自動で行う

やりたいこと

Github Actionsを用いてpep8のチェックを自動で行うようにしたい

また、下記の条件下でGithub Actionsを起動させるようにしたいと思います

  • mainブランチに対してPull Requestがあったとき
  • チェックの結果をGithub上のインラインアノテーションとして表記させたい
  • PRのファイルのみチェック対象とし、それ以外は対象にしない
  • チェック対象外も指定したい(E501は除きたい)

サンプルコード

/.github/workflows/ci.yamlに以下の内容を記載

name: CI

on:
  pull_request:
    branches: [ "main" ]
    types: [ opened, synchronize ]

jobs:
  run-ruff:
    name: Check Ruff
    runs-on: ubuntu-latest

    steps:
      - name: Check out source repository
        uses: actions/checkout@v4

      - name: Set up Python environment
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: python -m pip install --upgrade pip

      - name: Lint with Ruff
        run: |
          pip install ruff

          git fetch
          DIFF_FILES=$(git diff remotes/origin/${{ github.base_ref }}..HEAD --diff-filter=ACDMR --name-only "*.py")

          for f in $DIFF_FILES; do
            ruff check $f --output-format=github
          done

pyproject.tomlに以下の内容を記載

[tool.ruff]
ignore = [
    "E501"
]

対象のリポジトリを下記に示します

github.com

実装上のPoint

上記のコードについて解説していきます。

1. name

name: CI

何でもOKです。

Actions上で表記されるので、わかりやすい名前が推奨されます。

2. on

on:
  pull_request:
    branches:
        - "main"
    types: [ opened, synchronize ]

どのブランチを対象としたpull request時に、に実行するかを記載します。

複数のブランチを対象にする場合は、下記のように書きます。

on:
  pull_request:
    branches: [ "main", "branch-A", "branch-B" ]
    types: [ opened, synchronize ]

3. jobs

実際の実行内容を記載していきます。

jobs:
  run-ruff:
    name: Check Ruff
    runs-on: ubuntu-latest

    steps:
      - name: Check out source repository
        uses: actions/checkout@v4

      - name: Set up Python environment
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: python -m pip install --upgrade pip

      - name: Lint with Ruff
        run: |
          pip install ruff

          git fetch
          DIFF_FILES=$(git diff remotes/origin/${{ github.base_ref }}..HEAD --diff-filter=ACDMR --name-only "*.py")

          for f in $DIFF_FILES; do
            ruff check $f --output-format=github
          done
- name: Set up Python environment
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

実行するpythonのversionを記載します。

      - name: Lint with Ruff
        run: |
          pip install ruff

          git fetch
          DIFF_FILES=$(git diff remotes/origin/${{ github.base_ref }}..HEAD --diff-filter=ACDMR --name-only "*.py")

          for f in $DIFF_FILES; do
            ruff check $f --output-format=github
          done

ここが一番のPointです。

大事なPointが2箇所あります。

1つ目は、

--output-format=github

とすることで、インラインアノテーションが実施されます。

2つ目は、

git fetch
DIFF_FILES=$(git diff remotes/origin/${{ github.base_ref }}..HEAD --diff-filter=ACDMR --name-only "*.py")

for f in $DIFF_FILES; do
    ruff check $f --output-format=github
done

とすることで、ベースブランチと比較して、今回の変更箇所のみを対象にしています。

これを入れない場合、"Unchanged files with check annotations"として表記してくれます(表示してくれるのはいいんですが、CIが落ちる)。

なお、今回チェックツールとして、ruffを入れています。

pyproject.toml

チェック対象外も指定したい(E501は除きたい

これを実現するために、入れます。

方法としては、

[tool.ruff]
ignore = [
    "E501"
]

と記載します。

【Python】print()を卒業してloggerを始める

この記事の対象者

  • Pythonでlogを書き始めたい人
  • loggerについて学びたい人

なぜlogを書くのか?

理由は色々あると思いますが、個人的にprintではなくlogが必要な理由は下記があると考えます。

  • print()では、いつ起きたかがわからない
  • print()では、表示したい場合と表示したくない場合、全てのコメントをコメントアウトする必要がある
  • logを使えば、どこでエラーがおきたかすぐわかる

逆にprint()でもいいケースとは何でしょうか?

私の所感だと、

  • 10 ~ 20行程度のプログラム等での軽いデバック用
  • 1時的に中身を確認したいとき

だと考えています。

loggerの始め方

はじめに、logレベルについて学びましょう。

logには5つの段階があります。

DEBUG 開発中に、原因を突き止めやすくするために、詳細な情報を出力したいときに利用
INFO 運用中に、イベントの発生情報 (または、想定通りの動作情報) を出すときに利用
WARNING 今は問題ではないけど、今後の運用で注意が必要な情報出力として利用
ERROR 機能実行でエラーが発生した際に利用
CRITICAL 大問題が発生し、アプリ動作がおかしいときに利用

開発中には、Debug、運用中であれば局面局面でのお知らせにInfo等がよく使われるかと思います。

続いて、loggerの設定の仕方について学びましょう。

以下のコードをコピペすれば設定は完了です。

from logging import getLogger, basicConfig, INFO


logger = getLogger(__name__)
basicConfig(
    filename='log.log',
    filemode='w',
    level=INFO,
    format='{asctime} {name}:{lineno} [{levelname}]: {message}',
    style='{'
)

各、設定について解説します。

from logging import getLogger, basicConfig, INFO


logger = getLogger(__name__)
basicConfig(
    filename='log.log', # log.logというファイル名で書き出します
    filemode='w', # defaultは、'a'です。'w'にすると、実行の度に新たにファイルが作成されます。デフォルトでは上書きになります。
    level=INFO, # どのレベルからlogに表示させるかを設定します。デフォルトでは、WARNINGです。
    format='{asctime} {name}:{lineno} [{levelname}]: {message}', # 書き出すformatの指定です。詳細は下記にまとめます。
    style='{' # フォーマットのstyleを指定します。デフォルトは、"%"です。
)

書き出すフォーマットは下記のような候補があります。

{asctime}  # 生成時間。YYYY-MM-DD HH:MM:SS,UUU 形式。datefmtでフォーマット変更可能
{created}       # 生成時間。time.time(}が返却する形式
{msecs}     # 生成時間のミリ秒部
{relativeCreated}   # logginモジュールが読み込まれてからの経過時間(ミリ秒}
{levelname}     # レベル名(DEBUG, INFO, WARNING, ERROR, CRITICAL}
{levelno}       # レベル番号。DEBUGは10, INFOは20など
{module}        # モジュール名
{pathname}      # パス名
{filename}      # ファイル名
{funcName}      # 関数名
{lineno}        # 行番号
{message}       # ログメッセージ
{name}      # ロガー名
{process}       # プロセスID
{processName}       # プロセス名
{thread}        # スレッドID
{threadName}        # スレッド名

サンプルコード

上記の設定を行うと、サンプルコードは以下のようになります。

from logging import getLogger, basicConfig, INFO


logger = getLogger(__name__)
basicConfig(
    filename='log.log',
    filemode='w',
    level=INFO,
    format='{asctime} {name}:{lineno} [{levelname}]: {message}',
    style='{'
)

logger.debug('debug log')
logger.info('info log')
logger.warning('warning log')
logger.error('error log')
logger.critical('critical log')

log.logは下記のものが書き出されます。

2024-09-20 22:25:08,256 __main__:14 [INFO]: info log
2024-09-20 22:25:08,256 __main__:15 [WARNING]: warning log
2024-09-20 22:25:08,256 __main__:16 [ERROR]: error log
2024-09-20 22:25:08,256 __main__:17 [CRITICAL]: critical log

これで、いつ・何行目で・どのようなLogが出ているか把握することができるようになりましたね!

netcdfファイルを軽量化する方法

背景

netCDFは、多次元データを格納するのに便利な拡張子です。

気象、海洋、気候の世界では広く使われています。

一方で、このデータの課題として、多次元配列が故にファイルサイズが大きくなることが挙げられます。

これにより、データの読み込み時にメモリの使用量が増え、かつ待機時間が伸びることが課題になりがちです。

そこで、今回はnetcdfのファイルサイズを圧縮する方法をお伝えします。

結論

xarrayライブラリを使います。

公式Documentは以下の通りです。

docs.xarray.dev

print(ds)
"""
<xarray.Dataset> Size: 416B
Dimensions:        (loc: 2, instrument: 3, time: 4)
Coordinates:
    lon            (loc) float64 16B -99.83 -99.32
    lat            (loc) float64 16B 42.25 42.21
Dimensions without coordinates: loc, instrument, time
Data variables:
    temperature    (loc, instrument, time) float64 192B 9.584 18.36 ... 16.99
    precipitation  (loc, instrument, time) float64 192B 4.323 8.643 ... 1.573
Attributes:
    description:  Weather related data.
"""
ds.to_netcdf("output.nc", encoding={'temperature':{'dtype': 'int16', "zlib": True, , "complevel": 9}}})

Pointは、

  • encodingで、zlib=Trueにすること。
  • 型を与えて不必要以上の型を与えないこと。
  • floatは、スケールファクターを与えてint型で保存すると効果はバツグン

です。

軽量化したファイルの書き込み

上記のとおりです。

公式Documentを見ながら、試してみてください。

docs.xarray.dev

軽量化したファイルの読み込み

特に変化はありません。

import xarray as xr

file = "NC_H08_20161117_0230_B01_JP02_R10.nc"

ds = xr.open_dataset(file)

サンプルデータ

www.data.jma.go.jp

xarrayでデータを作る

書き込みが想定されるので、データの作り方も合わせて例示します。

import numpy as np
import pandas as pd
import xarray as xr


temperature = 15 + 8 * np.random.randn(2, 3, 4)
precipitation = 10 * np.random.rand(2, 3, 4)
lon = [-99.83, -99.32]
lat = [42.25, 42.21]
instruments = ["manufac1", "manufac2", "manufac3"]
time = pd.date_range("2014-09-06", periods=4)
reference_time = pd.Timestamp("2014-09-05")

ds = xr.Dataset(
    data_vars=dict(
        temperature=(["loc", "instrument", "time"], temperature),
        precipitation=(["loc", "instrument", "time"], precipitation),
    ),
    coords=dict(
        lon=("loc", lon),
        lat=("loc", lat),
    ),
    attrs=dict(description="Weather related data.")
)
print(ds)

ナウでヤングな方法は、netcdf4からxarrayかもしれない(?)

【コピペでOK】EC2にPython3.12を入れる

背景

EC2はデフォルトでは、PythonのVersionが非常に古いです。

そこで、EC2のVersionUpを試みるも環境構築に苦戦するケースが散見されます。

巷にあふれる同様のHow To記事も見かけますが、

  • 対象OSの記載がない
  • versionが古い

とうまくいかないことが多いです。

そこで、本記事ではEC2(Amazon Linux2)を使ってPython3.12を導入する方法を記載します。

特に、今後同様のユースケースにおいてコピペで使えるように、丁寧な導入込みで解説します。

前提

OSは、Amazon Linux2を対象とします。

また、EC2へのログインはできたが、なにもinstallしていないことを前提条件とします。

既に何らかのライブラリをinstallしている場合、挙動の保証はいたしません。

手順

以下のステップに分けて説明していきます。

  1. 管理者パスワードを設定する
  2. gitをinstallする
  3. brewをinstallする
  4. 関連パッケージをinstallする
  5. pyenvをinstallする
  6. python3.12をinstallする

具体的なコマンドは下記がすべてです。

# 1. 管理者パスワードを設定する
sudo passwd ec2-user

sudo yum update

# 2. gitをinstallする
sudo yum install git

# gitのversionが表記されていることを確認してください
git version

# 3. brewをinstallする
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

(echo; echo 'eval "$(/opt/homebrew/bin/brew shellenv)"') >> /home/ec2-user/.bashrc

eval "$(/opt/homebrew/bin/brew shellenv)"

# brewのversionが表記されていることを確認してください
brew --version

# 4. 関連パッケージをinstallする
sudo yum install -y openssl11 openssl11-devel

sudo yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel xz-devel

# 5. pyenvをinstallする
curl https://pyenv.run | bash

~/.bashrcに下記を記載してください。

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "($pyenv init -)"
source ~/.bashrc

# pyenvのversionが表記されていることを確認してください
pyenv --version

# 次のコマンドで出てきたものはすべて、Install可能です。Python3.11でも12でもOKです
pyenv install --list

# 6. python3.12をinstallする
pyenv install 3.12.5

pyenv global 3.12.5

pyenv local 3.12.5

# 指定したversion(ここでは3.12.5が表記されていることを確認)
python3 --version

上記でエラーが出る、うまくいかないなどあればお気軽にコメントください!

また、この記事が

役に立った!応援してます!

などがあれば、ぜひスター★シェアをお願いします!

cartopy逆引き大全

0. 背景

cartopyを使いたいユーザーに向けて、逆引き大全を作成しています。

すべて、サンプルコード付きで紹介しています。

現在、対応しているサンプルコードは以下のとおりです。

  • 基本(basic)
    • cartopyをinstallする
  • 描画(plot)
    • 海岸線を描く
    • 緯度・軽度線を描く
    • 国境線を描く
    • 表示されるエリアを日本域だけにする
    • 複数のグラフを作成する
    • 海や陸地に色をつける
    • パラパラ漫画(.gif)を作成する
    • 図法を変更する
  • データのplot(data)
    • 点をplotする
    • 線をplotする
    • 面データをplotする
    • 文字をplotする
    • カラーバーをplotする
  • 図の体裁(graph-technic)
    • グラフにタイトルを入れる

その他のテーマや質問は、本ブログ・もしくはGithubにてお知らせください。

github.com

1. cartopyをinstallする

pip install cartopy

2-1. 海岸線を描く

import cartopy.crs as ccrs
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

2-2. 緯度・軽度線を描く

import cartopy.crs as ccrs
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=0))
ax.coastlines()

lat_array = np.arange(-90, 90.1, 30)
lon_array = np.arange(-180, 180.1, 30)

gl = ax.gridlines(crs=ccrs.PlateCarree(), linewidth=0.5, linestyle='--')
gl.xlocator = mticker.FixedLocator(lon_array)
gl.ylocator = mticker.FixedLocator(lat_array)
ax.set_xticks(lon_array, crs=ccrs.PlateCarree())
ax.set_yticks(lat_array, crs=ccrs.PlateCarree())
plt.show()

2-3. 国境線を描く

import warnings

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker


warnings.filterwarnings('ignore')
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

ax.add_feature(cfeature.BORDERS) 

2-4. 表示されるエリアを日本域だけにする

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

ax.add_feature(cfeature.BORDERS)

ax.set_extent([120, 150, 20, 50], ccrs.PlateCarree())

2-5. 複数のグラフを作成する

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(12, 6))

i = 1
for irow in range(1, 4, 1):
    for icol in range(1, 4, 1):
        ax = fig.add_subplot(3, 3, i, projection=ccrs.PlateCarree())
        ax.coastlines()
        i += 1

2-6. 海や陸地に色をつける

import warnings

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker


warnings.filterwarnings('ignore')
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

ax.add_feature(cfeature.BORDERS)
ax.add_feature(cfeature.LAND)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.LAKES)

ax.set_extent([90, 150, 20, 50], ccrs.PlateCarree())

2-7. パラパラ漫画(.gif)を作成する

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
from PIL import Image
for i in range(1, 32, 1):
    title = f"2024/01/{str(i).zfill(2)}"

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
    ax.coastlines()

    ax.set_title(title)
    plt.savefig(f"./img/{i}.png")
    plt.close()
pictures=[]


for i in range(1, 32, 1):
    pic_name = f"./img/{str(i)}.png"
    img = Image.open(pic_name)
    pictures.append(img)

pictures[0].save('./gif/anime.gif',save_all=True, append_images=pictures[1:], optimize=True, duration=500, loop=0)

2-8. 図法を変更する

import cartopy.crs as ccrs
import matplotlib.pyplot as plt

# 正距円筒図法
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

#メルカトル図法
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=0))
ax.coastlines()

# ポーラーステレオ図法(極投影図法)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.NorthPolarStereo(central_longitude=0))
ax.coastlines()

# ランベルト図法(ランベルト正角円錐図法)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.LambertConformal(central_longitude=0))
ax.coastlines()

# 正射投影図法(平射図法)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.Orthographic(central_longitude=0))
ax.coastlines()

# ロビンソン図法
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.Robinson(central_longitude=0))
ax.coastlines()

# モルワイデ図法
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.Mollweide(central_longitude=0))
ax.coastlines()

# ランベルト正積円筒図法
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.LambertCylindrical(central_longitude=0))
ax.coastlines()

# ミラー図法
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.Miller(central_longitude=0))
ax.coastlines()

3-1. 点をplotする

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

ax.scatter(135 + 180, 35, 100, color="red")

ax.set_extent([0, 360, -90, 90], ccrs.PlateCarree())

3-2. 線をplotする

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

ax.plot([100 - 180, 135 - 180, 150 - 180], [30, 40, 50], color="red")

ax.set_extent([0, 360, -90, 90], ccrs.PlateCarree())

3-3. 面データをplotする

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np
lat = np.arange(20, 60, 5)
lon = np.arange(110, 150, 5)
dat = np.zeros((len(lat), len(lon)))

k = 0
for i in range(len(lat)):
    for j in range(len(lon)):
        dat[i, j] = k
        k += 1
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

ax.contourf(lon, lat, dat, transform=ccrs.PlateCarree())
ax.set_extent([0, 360, -90, 90], ccrs.PlateCarree())

3-4. 文字をplotする

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

ax.text(150 - 180, 30, "Hello World!", color="green")

ax.set_extent([0, 360, -90, 90], ccrs.PlateCarree())

3-5. カラーバーをplotする

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np
lat = np.arange(20, 60, 5)
lon = np.arange(110, 150, 5)
dat = np.zeros((len(lat), len(lon)))

k = 0
for i in range(len(lat)):
    for j in range(len(lon)):
        dat[i, j] = k
        k += 1
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

cs = ax.contourf(lon, lat, dat, transform=ccrs.PlateCarree())
cbar = fig.colorbar(cs)
cbar.set_label('legend text')

ax.set_extent([0, 360, -90, 90], ccrs.PlateCarree())

4-1. グラフにタイトルを入れる

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()
ax.set_extent([0, 360, -90, 90], ccrs.PlateCarree())

ax.set_title("this is title area.")
Text(0.5, 1.0, 'this is title area.')

【1000倍早くなるPython高速化】for文とベクトル志向演算で計算比較してみた

背景

Python高速化のテクニック、ベクトル志向演算でどれだけ高速ができるか検証してみました。

高速化の題材は、for文ループを用いて、どれだけ差がでるかを検証してみました。

Python高速化については、以下を参照してください。

www.yuta-nakata.net

やってみた

テーマ設定

10万行のデータに対して、①2倍して②10引いて③半分にします

手法1

各行ごとに計算を実行します。

STEP = 100000

def method1():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)

    for i in range(len(df)):
        df.loc[i, 'A'] *= 2
        df.loc[i, 'A'] -= 10
        df.loc[i, 'A'] /= 2

手法2

データフレームに対して、ベクトル志向で計算します。

STEP = 100000

def method2():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)
    df['A'] *= 2
    df['A'] -= 10
    df['A'] /= 2

実行結果比較

計算結果を比較してみます

import time

import numpy as np
import pandas as pd


STEP = 100000


def method1():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)

    for i in range(len(df)):
        df.loc[i, 'A'] *= 2
        df.loc[i, 'A'] -= 10
        df.loc[i, 'A'] /= 2


def method2():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)
    df['A'] *= 2
    df['A'] -= 10
    df['A'] /= 2



if __name__ == '__main__':
    start = time.time()
    method1()
    result_time1 = time.time() - start
    print(f'finish time of method1() is {result_time1} (sec)')

    start = time.time()
    method2()
    result_time2 = time.time() - start
    print(f'finish time of method2() is {result_time2} (sec)')

    print(f'処理時間は、約{int(result_time1 / result_time2)}倍異なります')

この結果、、、

finish time of method1() is 9.090620040893555 (sec)
finish time of method2() is 0.003556966781616211 (sec)
処理時間は、約2555倍異なります

と、1000倍以上高速化させることができることがわかりました。

改めて、ベクトル志向で計算させることの重要性がわかる内容です。

おまけ

www.yuta-nakata.net

で紹介したiterrows()itertuples()でも実行速度の比較をしてみました。

import time

import numpy as np
import pandas as pd


STEP = 100000


def method1():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)

    for i in range(len(df)):
        df.loc[i, 'A'] *= 2
        df.loc[i, 'A'] -= 10
        df.loc[i, 'A'] /= 2


def method2():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)
    df['A'] *= 2
    df['A'] -= 10
    df['A'] /= 2


def method3():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)
    for _, item in df.iterrows():
        item['A'] = (item['A'] * 2 - 10) / 2


def method4():
    df = pd.DataFrame()
    df['A'] = np.arange(STEP)
    for item in df.itertuples():
        df.A = (df.A * 2 - 10) / 2


if __name__ == '__main__':
    start = time.time()
    method1()
    result_time1 = time.time() - start
    print(f'finish time of method1() is {result_time1} (sec)')

    start = time.time()
    method2()
    result_time2 = time.time() - start
    print(f'finish time of method2() is {result_time2} (sec)')

    start = time.time()
    method3()
    result_time3 = time.time() - start
    print(f'finish time of method3() is {result_time3} (sec)')

    start = time.time()
    method4()
    result_time4 = time.time() - start
    print(f'finish time of method4() is {result_time4} (sec)')

結果は、、、、、

finish time of method1() is 9.206869125366211 (sec)
finish time of method2() is 0.003290891647338867 (sec)
finish time of method3() is 1.6420342922210693 (sec)
finish time of method4() is 16.61213994026184 (sec)

でした。

【脱初心者】Python初心者コードあるある

背景

Python初心者あるあるをまとめます。

また、その改善策も合わせて紹介します。

あるある1:多重forループ

for i in range(10):
    for j in range(10):
        for k in range(10):
            print(i, j, k)

本当に多重ループが必要でしょうか?よく考えましょう。

代替案は、pandas DataFrameやnumpyを使ってベクトル志向な演算を目指しましょう。

ベクトル志向については下記を参照してください。

www.yuta-nakata.net

あるある2:関数志向な実装ができていない

import pandas as pd


df = pd.read_csv("test.csv")

df["A"] = 10

df.to_csv("result.csv")

といったようにベタ書きしていないですか?

適切なまとまりに分けて、関数化しましょう。

import pandas as pd

def read_csv(file: str) -> pd.DataFrame:
    return pd.read_csv(file)

def excute(df: pd.DataFrame) -> pd.DataFrame:
    df["A"] = 10
    return df

def save_csv(file: str) -> None:
    df.to_csv(file, index=False)


if __name__ =='__main__':
    file = "data.csv"

    df = read_csv(file)
    df = execute(df)
    save_csv(df)

こうすることで、今後の使いまわしが行いやすい & 試験しやすくなります。

あるある3:運用サービスは.pyで書け

jupyter notebook等で経験がある人は、.ipynbで実装を試みます。

しかし、Webサービス等で使用する際はこちらの使用はやめましょう。

基本的に、Webエンジニアは、.py形式で仕事をするべきです。

あるある4:pep8違反

pythonにはpep8というコード規約があります。

これを遵守することで、チームとしての可読性が大きく向上します。

また、よくある記法は基本的には踏襲するべきです。

例えば、

import pandas

ではなく、

import pandas as pd

とする等です。

あるある5:安易な例外処理をしない

try-exceptはできる限り避けるべきです。

import requests

res = requests.get("https://www.hogehoge.net")

if res.text != []:
    data = res.text

等とするべきであり、

import requests

try:
    res = requests.get("https://www.hogehoge.net")
except:
    print("ERROR")

等、例外処理で逃げるべきではありません。

また、処理を中断するとき

sys.exit()

で終わらせるのではなく、

raise Exception("ERROR")

といったように、エラーコードとあわせて終了させるべきです。

もちろん、このような異常系もpytestの対象です。

pytestで考えるテストケース

背景

テストコードを書いてください

このときにどんなテストコードを書けばいいでしょうか?

具体例をベースにテストケースの考え方について、本記事では考えます。

テストの種類

テストには、

等があります。

ここでは、最も簡単な単体テストについて、取り上げます。

具体例

以下のような関数があります。

このテストケースは何が考えられるでしょうか?

def double_and_get_index(value: int) -> str:
    if not type(value) is int:
        raise Exception("Please Int Value")

    value *= 10
    if value >= 1000:
        return "A"
    elif value >= 100:
        return "B"
    elif value >= 10:
        return "C"

私なら以下のテストコードを書きます。

import pytest 

def test_double_and_get_index():
    # 正常系
    assert double_and_get_index(2) == "C"
    assert double_and_get_index(20) == "B"
    assert double_and_get_index(200) == "A"

    # 異常系
    assert double_and_get_index(-10) is None
    
    with pytest.raises(Exception) as e:
        double_and_get_index("A")
    assert str(e.value) == "Please Int Value"

    with pytest.raises(Exception) as e:
        double_and_get_index(0.1)
    assert str(e.value) == "Please Int Value"

    with pytest.raises(Exception) as e:
        double_and_get_index(None)
    assert str(e.value) == "Please Int Value"

    # 境界値テスト
    assert double_and_get_index(9) == "C"
    assert double_and_get_index(10) == "B"
    assert double_and_get_index(11) == "B"
    assert double_and_get_index(99) == "B"
    assert double_and_get_index(100) == "A"
    assert double_and_get_index(101) == "A"

単体テストのテストケースとしては、以下のことをよく考えます。

1. 正常系

意図通りの入力値が入ってきた場合、意図通りの結果がでるか?

2. 異常系

想定外の入力値が入ってきた場合、Validationができているか?

3.境界値条件系

条件分岐の前後で適切に変更できているか?

4. 条件分岐の網羅性

条件分岐のすべてで動作検証しているか?

私が、レビュワーだったら最低限このぐらいは書こうねと伝えます。

参考になれば。

【爆速Python】今すぐ使えるPythonを高速化する方法

背景

2024年6月24日に「爆速Python」が出版されました。

Pythonユーザーにとって、「高速化」は常に求められる処理であり、本書から今すぐ使えるテクニックを抽出しようと思います。

データ分析を高速化するテクニック

本書を通じて、ひとつだけ取り上げるとしたらPandas・データフレームの高速化です。

一番重要な考え方・思考法はベクトル化です(本書、7章を読むとよくわかります)。

テーマ

データフレームを使って、ループ処理を実装する

これをアマチュアからプロまでのコードの書き方が紹介されています。

レベル1

一行ずつ取り出す方法です。

PandasやNumpyの経験がないPython開発者が一般的に記述する方法

として本書では扱われています。

total = 0
roops = 0

for i in range(len(df)):
    row = df.iloc[i]
    total += row['count']
    roops += 1

print(total / roops)

レベル2

iterrows()メソッドを使った方法です。 レベル1よりは、多少の改善が見られます。

total = 0
roops = 0

for i, row in df.iterrows():
    total += row['count']
    roops += 1

prunt(total / roops) 

レベル3

itertuples()を使う方法です。 レベル2に酷似していますが、大きな改善が見られるようです。

total = 0
roops = 0

for row in df.itertuples():
    total += row.count
    roops += 1

print(total / roops)

レベル4

ベクトル化です。

df['count'].sum() / len(df)

datetime型について

datetimeが含まれているDataFrameについては、型を適切にdatatimeにすると、より早くなります。

その他

色々、目からウロコな内容でしたので、目次だけでも出しておきます

  • 並行性、並列性、非同期処理
  • ハイパフォーマンスなNumPy
  • Cythonを使って重要なコードを再実装する
  • ハイパフォーマンスなpandasとApache Arrow
  • GPUコンピューティングを使ったデータ分析
  • Daskを使ったビッグデータの分析