NumPyのconcatenate(), vstack(), hstack(), dstack(), stack()の違い

NumPyのconcatenate(), vstack(), hstack(), dstack(), stack()の違い について毎回混乱するのでまとめました。

concatenate()

複数のアレイを、既存の軸に沿って結合する関数です。

基本的に、後述するvstack(), hstack(), dstack()の上位互換がconcatenate()だと思っておけば良さそう。

2つの同shapeなアレイを結合する例を示します。axisの指定によりどの軸に沿って結合するかを指定します。

a = np.random.random((5, 6, 7, 8))
b = np.random.random((5, 6, 7, 8))

c = np.concatenate([a, b], axis=0)  # shape: (10, 6, 7, 8)
c = np.concatenate([a, b], axis=1)  # shape: (5, 12, 7, 8)
c = np.concatenate([a, b], axis=2)  # shape: (5, 6, 14, 8)
c = np.concatenate([a, b], axis=3)  # shape: (5, 6, 7, 16)

# 軸を省略したときはaxis=0を指定したのと同じ
c = np.concatenate([a, b])  # shape: (10, 6, 7, 8)

axisで指定した軸に関しては長さがずれていても結合できます。

a = np.random.random((5, 6, 7, 8))
b = np.random.random((5, 6, 70, 8))

c = np.concatenate([a, b], axis=2)  # shape: (5, 6, 77, 8)

vstack(), hstack(), dstack()

基本的に以下のように思っておけばよいです。

  • vstack(): concatenate(axis=0)と同じ
  • hstack(): concatenate(axis=1)と同じ
  • dstack(): concatenate(axis=2)と同じ

hstack()の例だけ以下に示します。

a = np.random.random((5, 6, 7, 8))
b = np.random.random((5, 6, 7, 8))

c = np.hstack([a, b])  # (5, 12, 7, 8)

しかし、以下に示すように、細部が微妙に異なります。個人的には、これらを覚えるのはかなり困難なので、 事前に結合対象となるアレイのdimを合わせたあとにconcatenate()(または場合に応じてvstack(), hstack(), dstack())を使うほうが 読みやすいコードになるのではないかと思います。

vstack()

vstack()に渡された1-Dアレイ(e.g. (N,))は、結合前に2-Dアレイ(e.g. (1, N,))に拡張されます。よって1-Dアレイと2-Dアレイの結合が可能です。

a = np.random.random((7, ))  # (1, 7)に拡張される
b = np.random.random((100, 7))

c = np.vstack([a, b])  # shape: (101, 7)

hstack()

hstack()に1-Dアレイと1-Dアレイが渡された場合は、例外的に、連結された1-Dアレイが返ります。

a = np.random.random((5,))
b = np.random.random((5,))

c = np.hstack([a, b])  # shape: (10,)

1-Dアレイと2-Dアレイが渡された場合はエラーになります。

dstack()

dstack()に渡された1-Dアレイ(e.g. (N,))は、結合前に3-Dアレイ(e.g. (1,N,1))に拡張されます。

dstack()に渡された2-Dアレイ(e.g. (M,N))は、結合前に3-Dアレイ(e.g. (M,N,1))に拡張されます。

よって1-Dアレイ、2-Dアレイ、3-Dアレイの結合が可能となることがあります。

a = np.random.random((5,))  # (1, 5, 1)に拡張される
b = np.random.random((1,5))  # (1, 5, 1)に拡張される
c = np.random.random((1,5,100)) 

d = np.dstack([a, b, c])  # shape: (1, 5, 1+1+100)

stack()

stack()はこれまで紹介したコードと異なり、新規に軸を生成します。

a = np.random.random((5, 6, 7))
b = np.random.random((5, 6, 7))

c = np.stack([a, b], axis=0)  # shape: (2, 5, 6, 7)
c = np.stack([a, b], axis=1)  # shape: (5, 2, 6, 7)
c = np.stack([a, b], axis=2)  # shape: (5, 6, 2, 7)

# axis=0を指定したのと同じ
c = np.stack([a, b])  # shape: (2, 5, 6, 7)

参考URL

CygwinからWSLに移行するときのメモ

多分15年以上Cygwinを使い続けていますが、いい加減WSLに移行していこうと考えています。以下、CygwinでできていたことをWSLでやるためにはどうすればいいかというメモです。

ExplorerのファイルをDrag & Dropしたときに/mntで始まるパスに自動変換

Cygwinでは、例えば C:\temp\file.txt というファイルを Cygwin のターミナルにD&Dすると、自動的に/cygdrive/c/temp/file.txtというCygwinのためのパスに変換してくれて便利でした。

WSLでは、 Drag & Drop from File Explorer produces incorrect file paths with WSL · Issue #331 · microsoft/terminal · GitHub を見る限り、現時点でこのような仕組みはありません。面倒ですが、毎回

$ wslpath "C:\temp\file1.txt"

というコマンドを使ってパスを変換するしかなさそうです。

小さなことですが、個人的にはWSLに移行する心理的障壁のひとつです。

(追記)WSLのコンソールを便利で高機能な「wsltty」に置き換える:Tech TIPS - @IT を参考に wsltty を入れることで、Drag & Dropしたときに/mntで始まるパスに自動変換できるようになりました!

ファイルの内容をクリップボードにコピー

Cygwinのときは以下のコマンドを使ってfile.txtの内容をクリップボードにコピーしていました。

$ lv file.txt > /dev/clipboard

lvはlessを多元化対応したページャで、EUC-JPなどの文字コードが現役だったころから癖で使い続けています。catでもいいと思います。

WSLでは以下でOKです。.exe をつけるとWindowsのコマンドを呼べるのですね。 (参考: How can one copy text from nano in Ubuntu WSL2 and paste it into a Windows application? - Ask Ubuntu )

$ cat file.txt > clip.exe

文字列のペースト

CygwinではShift + Insertで文字列ペーストできたのですが、WSLでは

2~

という文字列が表示されてしまいます。

Copy and Paste arrives for Linux/WSL Consoles - Windows Command Line によると、タイトルバーで右クリックしてダイアログを開き、「Ctrl+Shift+C/V をコピー/貼り付けとして使用する」にチェックすれば、Ctrl+Shift+V でペーストできるようになります。

(追記) wsltty を入れると慣れ親しんだShift + Insertで文字列ペーストできます。 (参考: terminal - Use shift-Insert to paste in WSL - Unix & Linux Stack Exchange )

Ubuntu 20.04にemacsをインストール

WSL2上のUbuntu 20.04にemacsをインストールしようとして

$ sudo apt install emacs

としたところインストールできませんでした(エラーメッセージは記録し忘れました)。

お決まりの

$ sudo apt update
$ sudo apt upgrade

したあともう一度最初のコマンドを打てばインストールできました。ただし、バージョンは26.3でやや古いです(現時点で最新は27.2)。

その際のログを載せておきます。

$ sudo apt install emacs
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  adwaita-icon-theme at-spi2-core emacs-bin-common emacs-common emacs-el emacs-gtk emacsen-common fontconfig
  fonts-droid-fallback fonts-noto-mono fonts-urw-base35 ghostscript gsfonts gtk-update-icon-cache hicolor-icon-theme
  humanity-icon-theme imagemagick-6-common libatk-bridge2.0-0 libatk1.0-0 libatk1.0-data libatspi2.0-0
  libavahi-client3 libavahi-common-data libavahi-common3 libcairo-gobject2 libcairo2 libcolord2 libcups2 libdatrie1
  libepoxy0 libfftw3-double3 libgd3 libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-bin libgdk-pixbuf2.0-common libgif7 libgomp1
  libgraphite2-3 libgs9 libgs9-common libgtk-3-0 libgtk-3-bin libgtk-3-common libharfbuzz0b libidn11 libijs-0.35
  libjbig0 libjbig2dec0 libjpeg-turbo8 libjpeg8 liblcms2-2 liblqr-1-0 libm17n-0 libmagickcore-6.q16-6
  libmagickwand-6.q16-6 libopenjp2-7 libotf0 libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 libpaper-utils
  libpaper1 libpixman-1-0 librest-0.7-0 librsvg2-2 librsvg2-common libsoup-gnome2.4-1 libthai-data libthai0 libtiff5
  libwayland-cursor0 libwayland-egl1 libwebp6 libwebpmux3 libxcb-render0 libxcursor1 libxkbcommon0 m17n-db
  poppler-data ubuntu-mono
Suggested packages:
  mailutils emacs-common-non-dfsg fonts-noto fonts-freefont-otf | fonts-freefont-ttf fonts-texgyre ghostscript-x
  colord cups-common libfftw3-bin libfftw3-dev libgd-tools gvfs liblcms2-utils m17n-docs libmagickcore-6.q16-6-extra
  librsvg2-bin poppler-utils fonts-japanese-mincho | fonts-ipafont-mincho fonts-japanese-gothic | fonts-ipafont-gothic
  fonts-arphic-ukai fonts-arphic-uming fonts-nanum
The following NEW packages will be installed:
  adwaita-icon-theme at-spi2-core emacs emacs-bin-common emacs-common emacs-el emacs-gtk emacsen-common fontconfig
  fonts-droid-fallback fonts-noto-mono fonts-urw-base35 ghostscript gsfonts gtk-update-icon-cache hicolor-icon-theme
  humanity-icon-theme imagemagick-6-common libatk-bridge2.0-0 libatk1.0-0 libatk1.0-data libatspi2.0-0
  libavahi-client3 libavahi-common-data libavahi-common3 libcairo-gobject2 libcairo2 libcolord2 libcups2 libdatrie1
  libepoxy0 libfftw3-double3 libgd3 libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-bin libgdk-pixbuf2.0-common libgif7 libgomp1
  libgraphite2-3 libgs9 libgs9-common libgtk-3-0 libgtk-3-bin libgtk-3-common libharfbuzz0b libidn11 libijs-0.35
  libjbig0 libjbig2dec0 libjpeg-turbo8 libjpeg8 liblcms2-2 liblqr-1-0 libm17n-0 libmagickcore-6.q16-6
  libmagickwand-6.q16-6 libopenjp2-7 libotf0 libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 libpaper-utils
  libpaper1 libpixman-1-0 librest-0.7-0 librsvg2-2 librsvg2-common libsoup-gnome2.4-1 libthai-data libthai0 libtiff5
  libwayland-cursor0 libwayland-egl1 libwebp6 libwebpmux3 libxcb-render0 libxcursor1 libxkbcommon0 m17n-db
  poppler-data ubuntu-mono
0 upgraded, 81 newly installed, 0 to remove and 0 not upgraded.
Need to get 67.2 MB of archives.
After this operation, 273 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
(以下略)

ちなみに、snapを使ってemacsをインストールする方法も本来は存在するのですが、WSL2上のUbuntu 20.04では以下のようにエラーが出ました。

$ sudo snap install emacs --classic
Interacting with snapd is not yet supported on Windows Subsystem for Linux.
This command has been left available for documentation purposes only.

emacsの検索・置換に関するメモ

emacsを使うとき、検索はC-s、置換はM-%を使う以上のことをこれまでしてきませんでした。より高度な検索・置換の仕方をまとめました。

検索

単語単位で検索

いくつか方法があります。ユースケースに応じて使い分けてください。

1つ目の方法は、C-s M-s w としてから、検索したい単語を入力する方法です。( 参考 )

2つ目の方法は、C-sでのインクリメンタルサーチ中に、M-s w とする方法です。これで単語単位の検索をするか否かのフラグがトグルします。( 参考 )

3つ目の方法は、まず検索したい語にカーソルがある状態でC-s C-wとしてその語を選択状態にした上で、M-s wとする方法です。( 参考

コマンドが覚えられない場合は、メニューバーの"Options" → "Default Search Options" → "Whole Words"にチェックをつける方法も可です。なお、no windowモードでemacsを起動している場合は、F10でメニューを開けます。

大文字・小文字を区別して検索

これもいくつか方法があります。

1つ目の方法は、C-s M-cとしてから、検索したい単語を入力する方法です。

2つ目の方法は、C-sでのインクリメンタルサーチ中に、M-c とする方法です。これでcase-sensitiveで検索をするか否かのフラグがトグルします。( 参考 )

これも、メニューバーの"Options" → "Default Search Options" → "Ignore Case"をアンチェックする方法も可です。

正規表現で検索

C-u C-sとしてから、検索したい正規表現を入力します。

正規表現は、Pythonに組み込まれているような高度なバージョンではないようです。例えば数字一文字を表す\dは使えず、[0-9]とする必要があります。詳細は ここ を参照。

過去の検索キーワードを呼び出す

C-sでのインクリメンタルサーチ中にM-pまたはM-nです。

参考

文字列検索に関するフラグに関するコマンドは C-s C-h bで調べられます

置換

単語単位で置換

C-u M-%ののち、置換前の単語を入力してEnter、置換後の単語を入力してEnterです。 ( 参考 )

大文字・小文字を区別して置換

デフォルトだと置換前の単語をすべて小文字で入力すると、大文字・小文字をいい感じに変換してくれます (参考) が、これがおせっかいだという場合は、大文字・小文字を区別して置換したくなります。

https://emacs.stackexchange.com/questions/12780/how-to-perform-case-sensitive-query-replace を見る限り、ある単語の検索の途中に、さっき紹介した M-c のようなコマンドでフラグを切り替える方法は用意されてないようでした。

メニューバーの"Options" → "Default Search Options" → "Ignore Case"をアンチェックする方法以上に簡単な方法は今のところ見つけられていません。

正規表現で置換 (query-replace-regexp)

C-M-% もしくは M-x replace-regexp としてから、検索したい正規表現を入力してEnter、置換後の正規表現を入力してEnterです。

C-M-%の入力方法は以下のどちらかです。

  • Escを先押ししたあと、Ctrl + Shiftを押しながら5を押す
  • Ctrl + Alt + Shiftを押しながら5を押す

過去の置換キーワードを呼び出す

置換中にM-pまたはM-nです。

置換前・置換後の文字列を変更

eで置換前、Eで置換後の文字列を変更可能です(参考)。上の「過去の置換キーワードを呼び出す」と組み合わせると便利です。

scikit-imageを使ってサンプル画像を簡単に取得

scikit-imageというPythonのライブラリを使うと、サンプル画像をさっと取ってくることができます。

コード例は以下です。

import skimage


print("scikit-image version: {}".format(skimage.__version__))
coffee_image = skimage.data.coffee()
print(coffee_image.shape)

Google Colabでの実行例を以下に示します。縦400px、横600px、カラー画像がNumPy形式で取得されました。dtypeはnp.uint8です。色順はRGBです。

0.16.2
(400, 600, 3)

どんな画像が取得できたかを見てみます。

import matplotlib.pyplot as plt


plt.imshow(coffee_image)
plt.title('coffee_image')
plt.show()

結果は以下です。 f:id:minus9d:20210731232017p:plain

他にどんな画像を取得できるかは 公式API を見てください。バージョンにより微妙にAPIが異なる(例えば0.19.xで提供されているcat()はscikit-iamge 0.16.2では存在しない)ため要注意です。

取得可能な画像を無理矢理全列挙するコードを以下に示します。

import skimage
import matplotlib.pyplot as plt


for i in dir(skimage.data):
  attr = getattr(skimage.data, i)
  if callable(attr) and not i.startswith('_'):
    try:
        img = attr()
        plt.imshow(img)
        plt.title("{} (shape {})".format(i, img.shape))
        plt.show()
    except Exception:
      pass

結果のうち冒頭部分を以下に示します。

f:id:minus9d:20210802213331p:plain

travis-ci.orgでCIを動かしていたリポジトリをtravis-ci.comに移行

久々にtravis-ci.orgにアクセスしたら "Since June 15th, 2021, the building on travis-ci.org is ceased. Please use travis-ci.com from now on." とメッセージが出たので、慌ててtravis-ci.comに移行したときのメモ。あとから思い出しながら書いたので抜けがあるかもしれません。

  1. Migrating repositories to travis-ci.com - Travis CI の "GitHub Apps initial setup" に従ってアカウントの準備
  2. https://travis-ci.com/account/repositories にいって、"Manage repositories on GitHub"ボタンを押して、"All repositories"を選択してSaveボタンを押すも、なぜかtravis-ci.orgにて動かしていた3つのリポジトリだけがリポジトリリストに現れない。3つのリポジトリだけを選択しても "We couldn’t find any active repositories you have access to." などとメッセージが出て失敗。大いに困る。
  3. https://travis-ci.com/account/migrate にいくと、travis-ci.orgにて動かしていた3つのリポジトリが表示されている。しかしグレーアウトされていて選択できない。
  4. 下の方にあったボタン( beta という文字列があったような…メモを取らなかった ) を押して、出てきたダイアログ (これも詳細を失念 ) を押すと、リポジトリを移行できそうな画面に遷移(下図)。ここで"Migrate selected repositories to travis-ci.com"ボタンを押すことでリポジトリを移行できた。

f:id:minus9d:20210615225841p:plain

Python 3のsubprocess.run()の使い方

Python 3で外部コマンドを呼ぶにはsubprocessモジュールで提供されているrun()を使うことが推奨されるのですが、毎回使い方を調べてしまっているので自分用にまとめます。順を追っていかないと引数の意味を理解しづらいところがあるので、冗長ですが簡単な例から書いていきます。なお、run()が導入されたのはPython 3.5なので、それより前のバージョンでは使えません。CygwinPython 3.8.3で動作確認しています。

基本

呼び出される側のスクリプトとして、以下のようなhello.pyを用意します。これは標準出力に「こんにちは」、標準エラー出力に「こんばんは」と出力するだけのトイプログラムです。

import sys

print("こんにちは", file=sys.stdout)
print("こんばんは", file=sys.stderr)

さっそくhello.pyをsubprocess.run()`を使って呼び出してみます。もっともシンプルな呼び出し方は

res = subprocess.run(['python3', 'hello.py'])

ですが、これだとresの中には

CompletedProcess(args=['python3', 'hello.py'], returncode=0)

という情報しか入っていません。これでは肝心の標準出力結果、標準エラー出力結果は得られません。

標準出力結果、標準エラー出力結果を得るためには、以下のようにcapture_output=Trueをつけます(Python 3.7以降限定;Python 3.6以前では代わりにstdout=subprocess.PIPEstderr=subprocess.PIPE の2つを指定)。

res = subprocess.run(['python3', 'hello.py'], capture_output=True)
print("return code: {}".format(res.returncode))
print("captured stdout: {}".format(res.stdout.decode()))
print("captured stderr: {}".format(res.stderr.decode()))

出力結果は以下です。res.stdoutで標準出力結果を、res.stderr標準エラー出力結果を得られていることがわかります。

return code: 0
captured stdout: こんにちは

captured stderr: こんばんは

ここで注意なのは、res.stdoutres.stderrは、ともに文字列ではなくバイトデータであることです。なのでdecode()を呼んで文字列に変換してやる必要があります。

これは面倒なので、以下のようにtext=Trueをつけると、文字列が返ってくるようになり便利です(Python 3.7以降;Python 3.3 ~ 3.6では代わりにuniversal_newlines=Trueをつける)。

res = subprocess.run(['python3', 'hello.py'], capture_output=True, text=True)
print("return code: {}".format(res.returncode))
print("captured stdout: {}".format(res.stdout))  # こんにちは
print("captured stderr: {}".format(res.stderr))  # こんばんは

明に文字列のエンコード方法を指定したいときは、text=Trueの代わりにencoding='utf8'などとつけても良いようです。

標準出力と標準エラー出力とをまとめてキャプチャ

標準出力と標準エラー出力とをまとめてキャプチャするには、stdout=subprocess.PIPE, stderr=subprocess.STDOUT と指定します(参考:subprocess — Subprocess management — Python 3.9.5 documentationIf you wish to capture and combine both streams into one, use stdout=PIPE and stderr=STDOUT instead of capture_output.

res = subprocess.run(['python3', 'hello.py'],
                     stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                     text=True)
print("captured stdout & stderr: {}".format(res.stdout))

ただ、これには大きな欠点があります。標準出力、標準エラー出力それぞれの出力の順番が保存されずマージされてしまいます。上記の例では、

captured stdout & stderr: こんばんは
こんにちは

と、順序が逆に表示されてしまいました。

上記の例だけに限っていうと、python - How can I make subprocess get the stdout and stderr in order? - Stack Overflow にある通り、['python3', '-u', 'hello.py']-uオプションを入れてunbufferedを強制することで期待通りの順序になりました。しかし、一般論でいうと、順序を常に保存するかんたんな方法はなさそうでした。

中身の理解はできていませんが参考になりそうなリンクを張っておきます。

例外処理

呼び出したコマンドが成功したかどうかは、run()の戻り値のreturncodeを確認します。0なら成功、非0なら失敗です。

例として、実行すると異常終了するraise_error.pyを以下のように作成します。

import sys
sys.exit(1)

これの戻り値を確認する例が以下です。

res = subprocess.run(['python3', 'raise_error.py'])
print("return code: {}".format(res.returncode))  # 1が表示される

run()check=Trueを追加することで、呼び出したコマンドが失敗したときに例外を返すようにすることができます。

$ python3 subprocess_test.py
return code: 1
------------------------------------------------------------
Traceback (most recent call last):
  File "subprocess_test.py", line 46, in <module>
    res = subprocess.run(['python3', 'raise_error.py'], check=True)
  File "/usr/lib/python3.8/subprocess.py", line 512, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['python3', 'raise_error.py']' returned non-zero exit status 1.

check=Trueをつけるべきかどうかは趣味と場合によりそうです。

コマンドに引数を与える

呼び出される側のスクリプトとして、1からNまでの整数の和を返すsum_1_to_n.pyを用意します。nは標準入力で受け取る仕様です。

n = int(input())
print(n * (n + 1) // 2)

コンソールで

$ echo 10 | python3 sum_1_to_n.py

とするのと同じノリでsubprocess.run()を使う場合、コマンドをリストで表現する方法ではパイプ(|)を使えないので、以下のようにshell = Trueを使ってコマンドを文字列として表現する必要があります。

res = subprocess.run('echo 10 | python3 sum_1_to_n.py', shell=True, capture_output=True, text=True)
print("captured stdout: {}".format(res.stdout))  # 55と表示される

しかし、一般にshell=Trueと指定すると脆弱性が発生するので推奨されません。個人のスクリプトでは便利なのでよくやってしまいますが…

上記の例だと、input=10というのを使うと、標準入力に10を指定したのと同じになってパイプを回避できます。

res = subprocess.run(['python3', 'sum_1_to_n.py'], input='10', text=True, capture_output=True)
print("captured stdout: {}".format(res.stdout))

パイプを回避するより一般的な例が、 この処理Pythonでどう書く? - エムスリーテックブログ の「パイプを使う」に記載されています。subprocess.run()より低レベルな関数subprocess.Popen()を使うのですが、私は理解できていません。

その他の便利オプション

  • cwdオプションによりコマンド実行時のワーキングディレクトリを変更できます
  • envオプションによりコマンド実行時の環境変数を変更できます

参考URL