Pythonの内包表記はRにないのか?

RにないPythonの文法・テクニックとして,これまで2つの記事(1つめ2つめ)を書きました。

今回は内包表記について書きます。Pythonユーザーにはお馴染みの,for文を1行で書ける便利なやつです。

結論から言うと,Rに内包表記はありません。処理の目的次第で内包表記よりRの方が簡単に書けたり,複雑に書かざるを得なかったりします。その辺を説明していきます。

Pythonの内包表記とは

簡単に言うと「複数行に渡るfor文を1行に凝縮して,リスト・タプル・辞書・セットを作成する方法」が内包表記です。

リスト内包表記

内包表記でリストを作る方法がこちらです。

[変数を使った式/操作 for 変数 in イテラブルなオブジェクト]

例として「0から5までの数字をstr型として持つリスト」をfor文と内包表記でそれぞれ書くとこうなります。

# for文
>>> num_str = []
>>> for i in range(5):
...    num_str.append(str(i))
>>> print(num_str)
['0', '1', '2', '3', '4']

#内包表記
>>> [str(i) for i in range(5)]
['0', '1', '2', '3', '4']

このように内包表記を使えばfor文なしで簡単にリストを作成できます。

if文を使った内包表記

次の書き方でif文の条件式に合う値だけリストに格納することができます。

[変数を使った式/操作 for 変数 in イテラブルなオブジェクト if 変数を使った条件]

例として「0から49までの整数のうち3で割り切れるもの」は以下のように書けます。

>>> [x for x in range(50) if x % 3== 0]
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48]

ネストした内包表記

2重のfor文も1行の内包表記で表現できます。「colorsとthingsの要素の文字列を総当りしてfstringで結合しリストに格納する」という例が以下です。

>>> colors = ["red", "green"]
>>> things = ["house", "car", "tree"]

# for文
>>> color_thing = []
...for x in colors:
...    for y in things:
...        color_thing.append(f'{x} {y}')
>>> print(color_thing)
['red house', 'red car', 'red tree', 'green house', 'green car', 'green tree']

# 内包表記
>>> [f'{x} {y}' for x in colors for y in things]
['red house', 'red car', 'red tree', 'green house', 'green car', 'green tree']

辞書内包表記

内包表記はリストだけではなく辞書を作るのにも重宝されます。「文字列をキー,その文字数を値とする辞書」を内包表記を作成するなら次のようになります。

>>> words = ['Python', 'R', 'C++', 'Ruby']
>>> {word: len(word) for word in words}
{'Python': 6, 'R': 1, 'C++': 3, 'Ruby': 4}

Rにリスト内包表記はないのか?なければどう表現するか?

私の知る限り,Rに内包表記はありません*1

特定の処理/操作を行う際,Pythonでは内包表記がベストプラクティスだったとしても,Rでは異なる方法を用いる必要があります。処理の目的によってRでのベストプラクティスが異なるので,ここからは思いついた例をいくつか挙げていきます。

1. そもそも内包表記(for文)が必要ない場合が多い

「0から4までの整数を2乗してstr型に変換しリストに格納」という処理を考えます。内包表記で書くとこうなります。

>>> [str(i * i) for i in range(5)]
['0', '1', '4', '9', '16']

Rユーザーは「Pythonではこんな簡単な処理にfor文/内包表記が必要なのか?」と思うことでしょう。Rだったらfor文なしでこう書けます。

> as.character((0:4)^2)
 [1] "0"  "1"  "4"  "9"  "16"

Rではこのように0:4というベクトルに対し累乗の演算子やas.characterという関数を適用できます。Rと同じノリでPythonで書こうと思ってもエラーになります。

str([0,1,2,3,4]**2) # TypeError: unsupported operand

この例のように,Rには「ベクトルを引数として受け取って,要素ごとに処理を行う」という関数/演算子が多くあります。一方でPythonの組み込み関数や演算子はそのような仕様になっていないため,for文や内包表記を用いてコンテナオブジェクト(リストや辞書)の要素ごとに関数/演算子を適用し,処理する必要があります。

別の例として「リストの要素である文字列の文字数をカウントしてリストを作成」という操作を考えます。Pythonでは内包表記が必要になりますが,Rではnchar関数(またはstringr::str_length関数)で簡単に書けます。

>>> words = ['Python', 'R', 'C++', 'Ruby']
>>> [len(word) for word in words]
[6, 1, 3, 4]
> words <- c('Python', 'R', 'C++', 'Ruby')
> nchar(words)
[1] 6 1 3 4

先に挙げたif文を使った内包表記もRではベクトル操作と演算子だけで簡単に表現できます。「0から49までの整数のうち3で割り切れるもの」は以下のように書けます。

>>> [x for x in range(50) if x % 3== 0]
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48]
> num <- 0:49
> num[num %% 3 == 0]
 [1]  0  3  6  9 12 15 18 21 24 27 30 33 36 39 42 45 48

以上のように,Pythonにおいて内包表記が伴う操作の多くはRでfor文の手を借りずに表現することができます。

2. sapplyやpurrr:mapで対応

先に見た「0から5までの数字をstr型に変換してリストに作成」のように「繰り返し変数に関数や演算子を適用し,入力(変数)と出力(操作後の要素)が1対1対応したリストを作成する」という単純な内包表記であれば,Rのbase::sapply関数やpurrr:map関数で表現できます。どちらの関数も「list/ベクトル(第1引数)の各要素に対して関数(第2引数)を適用して,その関数の返り値を出力する」という機能です。

map関数の派生系が特に便利で,map_dblは出力をdouble型に,map_chrは出力を文字列として出力できたりします。

> sapply(0:5, function(x) x*2)
[1]  0  2  4  6  8 10

> map_dbl(0:5, function(x) x*2)
[1]  0  2  4  6  8 10

> map_dbl(0:5, ~ .x*2) # map系はfunctionを省いてチルダと.xで表現可能
[1]  0  2  4  6  8 10

> map_chr(0:5, ~ .x*2) # map系はfunctionを省いてチルダと.xで表現可能
[1] "0.000000"  "2.000000"  "4.000000"  "6.000000"  "8.000000"  "10.000000"

Pythonの内包表記と比較するため「円周率を小数点i桁で丸めて,文字列にする。iを1から6まで処理してリストに格納」という操作を考えます。PythonとRでそれぞれ以下のように書けます*2

>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']
> sapply(1:5, function(i) as.character(round(pi, i)))
[1] "3.1"     "3.14"    "3.142"   "3.1416"  "3.14159"

> map_chr(1:5, ~as.character(round(pi, .x)))
[1] "3.1"     "3.14"    "3.142"   "3.1416"  "3.14159"

3. 妥協してfor文で書く または tidyverse系でゴリ押し

内包表記の処理によっては,Rで上手いこと表現できないものもあります。例えば,上で挙げたネストした内包表記の例はRのsapplyやmapで表現しきれません*3「colorsとthingsの要素の文字列をfstringで総当りして結合しリストに格納する」という処理でしたが,妥協してfor文で書くとアホみたいに冗長になります。

> colors = c("red", "green")
> things = c("house", "car", "tree")
> 
> color_thing <- c()
> for(color in colors){
+   for(thing in things){
+     color_thing <- c(color_thing, paste(color, thing))
+   }
+ }

> print(color_thing)
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

総当りなので,tidyr::expand_gridを使ってゴリ押しすることもできます。

> colors = c("red", "green")
> things = c("house", "car", "tree")

> library(tidyr)
> expand_grid(colors, things) %>%
+   mutate(color_thing = paste(colors, things)) %>%
+   pull(color_thing)
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

この例くらい複雑な処理になると内包表記の便利さを感じることができますね。

終わりに

今回はPythonの内包表記をRでどう表現するかについて書いてみました。 Pythonでは内包表記が超便利なので,RユーザーでPythonをマスターしたい人は早く身につけましょう。

追記 (2020年6月11日)

@dd_am11さんから「0から4までの整数を2乗してstr型に変換しリストに格納」の処理に関して「numpyだったらRっぽく書けるよね」というコメントを頂きましたので,追記しておきます。

>>> import numpy as np
>>> list((np.arange(5)**2).astype(str))
['0', '1', '4', '9', '16']

またR界隈で有名な@Atsushi776さんから,最後のcolorsとthingsの処理に関してRでシンプルに書く方法を2つ教えて頂きました。 1つめはlapplyしてunlist,2つのexpand.gridしてdo.callです。

> unlist(lapply(colors, paste, things))
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

> do.call(paste, rev(expand.grid(things, colors)))
[1] "red house"   "red car"     "red tree"    "green house" "green car"   "green tree" 

何ともオシャレですね。 ベクトルの処理はRの最も得意とする所なので,書き方が色々あって面白いですね。

*1:厳密にはcomnprehrというPythonの内包表記を模したRのライブラリはありますが,おそらく誰も使っていません。

*2:実際こんな例だとsapplyやmapは使わず,as.character(round(pi, 0:5) ) と書いてしまいます。

*3:簡単に書く方法を教えて頂いたので追記を御覧ください!

dplyr 1.00のacross関数が便利だった

「dplyr 1.00がリリースされCRANからインストールできるようになった」と教祖様(William Hadley氏)がツイートしていました。

主な変更点はTidyverseの公式ウェブサイトにまとめられています。

私も早速dplyr 1.00にアップデートしまして,変更点の中で目玉の1つであるacross()関数を使ってみたので,今回はその使い方をまとめていきます。

across関数とは

dplyr 1.00をインストールしてhelp(across)を見ると以下の記述があります。

Description: across() makes it easy to apply the same transformation to multiple columns, allowing you to use select() semantics inside in summarise() and mutate(). across() supersedes the family of "scoped variants" like summarise_at(), summarise_if(), and summarise_all().

簡単に言うと「across()はsummarise()やmutate()の中でselect()の機能を果たす。従来の_atや_ifに代替されるもの。」ということです。具体例を見ていきましょう。

従来の書き方とacross関数

従来の書き方とacross関数を比較するため,「irisでSpeciesごとに4変数(Sepal.Lengthなど)の平均値を計算する」という処理を考えます。従来の書き方として3つ紹介します。

従来パターン1: summariseの中身をベタ書き

最悪の書き方です。

library(dplyr)
library(tidyr)

> iris %>%
+   group_by(Species) %>%
+   summarise(Sepal.Length = mean(Sepal.Length),
+             Sepal.Width  = mean(Sepal.Width),
+             Petal.Length = mean(Petal.Length),
+             Petal.Width  = mean(Petal.Width))
`summarise()` ungrouping output (override with `.groups` argument)
# A tibble: 3 x 5
  Species    Sepal.Length Sepal.Width Petal.Length Petal.Width
  <fct>             <dbl>       <dbl>        <dbl>       <dbl>
1 setosa             5.01        3.43         1.46       0.246
2 versicolor         5.94        2.77         4.26       1.33 
3 virginica          6.59        2.97         5.55       2.03 
従来パターン2: tidyrでgatherしてspread

処理次第でこの書き方が楽な場合もありますが,今回の処理では悪手です。

iris %>%
  pivot_longer(names_to = "feature", values_to = "value", cols = -Species) %>%
  group_by(Species, feature) %>%
  summarise(value = mean(value)) %>%
  pivot_wider(names_from = feature, values_from = value)
従来パターン3: summarise_ifかsummarise_at

目的の処理に対して従来の最適な書き方がこちらです。

# summarise_if
iris %>%
  group_by(Species) %>%
  summarise_if(is.numeric, mean)

# summarise_at
iris %>%
  group_by(Species) %>%
  summarise_at(vars(Petal.Length:Sepal.Width), mean)

acrossを使った書き方

across()の使い方はこちらです。

across(.cols = everything(), .fns = NULL, ..., .names = NULL)

第1引数には変数,第2引数には関数を渡します。ポイントはselect()と同じように第1引数を渡せるという点です。starts_with()やwhere()などのヘルパー関数を用いることができます。

上で行ったirisの処理を(1)コロン,(2)ヘルパー関数where,(3)ヘルパー関数everythingを使ってやってみるとこんな感じです。

# (1) colon
iris %>%
  group_by(Species) %>%
  summarise(across(Sepal.Length:Petal.Width, mean))

# (2) where()
iris %>%
  group_by(Species) %>%
  summarise(across(where(is.numeric), mean))

# (3) everything()
iris %>%
  group_by(Species) %>%
  summarise(across(everything(), mean))

「従来のsummarise_ifやsummarise_atと一緒じゃん」と思う方もいるかもしれません。しかし,across()関数の方が圧倒的に便利な点があります。

across関数の,ここが凄い!!!

across()を使えば,従来の_ifや_atには不可能だった表現が可能になります。例えばirisを使って

  1. 4変数(SepalとPetal)の平均値を計算
  2. Speciesのレベル数を出力
  3. 行数を出力

を一気にできます。

> iris %>%
+   summarise(across(where(is.numeric), mean),
+             across(where(is.factor), nlevels),
+             count = n())

  Sepal.Length Sepal.Width Petal.Length Petal.Width Species count
1     5.843333    3.057333        3.758    1.199333       3   150

まとめ

across関数を使ってみました。利点のまとめがこちらです。

  • 引数の渡し方に応じて_atや_ifで処理を分ける必要がなく,summariseやmutateの引数として柔軟に表現できる。
  • vars()すら要らない。
  • 従来の_ifや_atには不可能だった表現が可能になる。

皆さんも_ifや_atを卒業して,across()に入門してみましょう。 終わり。

ランダウ記号と漸近展開

微分積分の教科書やプログラミングの計算量の文脈でたまに出てくるランダウの記号o()と,それを用いた漸近展開について勉強したのでメモ。

ランダウ記号って何

2つの関数f(x)g(x)がある点 x=aに収束するスピードはどっちが速いか,を表現できる方法がランダウ記号である。厳密な定義は以下。

ランダウ記号  o() 関数f(x)g(x) x=aの近傍で定義されており \displaystyle\lim_{x \rightarrow a}\frac{f(x)}{g(x)}=0の時, f(x)=o{(g(x))} \ \ (x \rightarrow a)と表す。

記号の解釈として以下のような表現がされる。

  •  x=aの近傍において,f(x)g(x)より遥かに小さい
  •  x=aの近傍において,f(x)g(x)に対して無視できる無限小である
  •  x=aの近傍において,f(x)g(x)より高位(higher order)の無限小である

  •   \displaystyle\lim_{x \rightarrow 0}\frac{x ^ 3}{x}=0なので, x ^3 = o(x) \ \ (x \rightarrow 0)
  •  \alpha>0のとき  \displaystyle\lim_{x \rightarrow \infty}\frac{\log{x}}{x ^\alpha}=0なので, \log{x} = o(x ^\alpha) \ \ (x \rightarrow \infty)

漸近展開

このランダウ記号を用いれば,テイラー展開を書き換えて漸近展開を表現できる。

漸近展開  f(x) aを含む開区間で定義されていて C ^n級の関数である時,次のように表せる。

 f(x) = \displaystyle \sum_{k=0}^{n}\frac{1}{k!}f^{(k)}(a)(x-a) ^k+o( (x-a) ^n) \ \ \ (x \rightarrow a)

(証明)

 \theta \ (0\lt\theta\lt 1) xに依存する実数とし, x=aにおける有限テイラー展開を表すと,


f(x) = \displaystyle \sum_{k=0}^{n-1}\frac{1}{k!}f^{(k)}(a)(x-a)^k+\frac{1}{n!}f^{(n)}(a+\theta(x-a))(x-a)^n \\
ここで \ \ g(x) = \frac{1}{n!}f^{(n)}(a+\theta(x-a))(x-a)^n-\frac{1}{n!}f^{(n)}(a))(x-a)^n \ と置き,f(x)に代入すると\\
f(x) = \displaystyle \sum_{k=0}^{n-1}\frac{1}{k!}f^{(k)}(a)(x-a)^k+\frac{1}{n!}f^{(n)}(a))(x-a)^n+ g(x) \\
f(x) = \displaystyle \sum_{k=0}^{n}\frac{1}{k!}f^{(k)}(a)(x-a)^k+ g(x) \\

ここで  \ \ g(x) = o( (x-a) ^n) \ \ \ (x \rightarrow a)を示せれば目的の漸近展開を表現できる。

 \displaystyle\lim_{x \rightarrow a}\frac{g(x)}{(x-a) ^n} =  \displaystyle\lim_{x \rightarrow a}\frac{ \frac{1}{n!}f^{(n)}(a+\theta(x-a))(x-a)^n-\frac{1}{n!}f^{(n)}(a)(x-a)^n}{(x-a) ^n}

分母分子を (x-a) ^nで割って, 1/n!で括って

 \displaystyle\lim_{x \rightarrow a}\frac{g(x)}{(x-a) ^n} =   \displaystyle\lim_{x \rightarrow a} \frac{1}{n!}(f^{(n)}(a+\theta(x-a))-f^{(n)}(a)) = 0

よって,ランダウ記号の定義式から  \ \ g(x) = o( (x-a) ^n) \ \ \ (x \rightarrow a)を示せた。したがって

 f(x) = \displaystyle \sum_{k=0}^{n}\frac{1}{k!}f^{(k)}(a)(x-a) ^k+o( (x-a) ^n) \ \ \ (x \rightarrow a)

と表せる。

ランダウ記号を使えばこんな綺麗に表現できる!!!! 美しい!!!!

漸近展開の利用

 f(x)=(1 + x ^2)e ^x x=0における4次の漸近展開を求めたい。愚直に1項ずつ計算してもよいが,ランダウ記号の特性を活かして計算する。 f(x)=e ^x + x ^2e ^xとして, e ^x x ^2e ^x x \rightarrow 0の4次の漸近展開をそれぞれ用意し足し合わせる。

 n=4で,e^x=1+x+\frac{1}{2}x ^2+\frac{1}{6}x ^3 + \frac{1}{24}x ^4 + o(x ^4)  \ \ \ \ \ \ \cdots  (i)   \\
n=2で,e^x=1+x+\frac{1}{2}x ^2 + o(x ^2)  \ \ \ \ \ \  \cdots (ii) \\

 (ii)の両辺に x ^2をかけて

 x ^2e^x=x ^2+x ^3+\frac{1}{2}x ^4 + x ^2o(x ^2) \\
補題より \ \  x ^2 o(x ^2) = o(x ^4)を利用して \\
x ^2e^x=x ^2+x ^3+\frac{1}{2}x ^4 + o(x ^4) \ \ \ \ \ \ \cdots (iii)

 (i)+(iii)より

 
f(x) = e ^x + x ^2e ^x = 1 + x + \frac{3}{2}x ^2 + \frac{7}{6}x ^3 + \frac{13}{24}x ^4 + o(x ^4) \ \ \ (x \rightarrow 0)

ランダウ記号の補題を証明

補題1:  x \rightarrow 0のとき  x^ m \cdot o(x^ n)=o(x^ {m+n})

(証明)

 f(x) =x^ m \cdot o(x^ n) g(x) = x^ {m+n}とおき, \displaystyle\lim_{x \rightarrow 0}\frac{f(x)}{g(x)}を考える。

 \displaystyle\lim _ {x \rightarrow 0}\frac{f(x)}{g(x)} = \lim _ {x \rightarrow 0}\frac{x^ m \cdot o(x^ n)}{x^ {m+n}} =\lim _ {x \rightarrow 0}\frac{x^ m \cdot o(x^ n)}{x^ mx^ n} =\lim _ {x \rightarrow 0}\frac{o(x^ n)}{x^ n} =0

よって,ランダウの記号の定義式が適用できて, f(x)=o{(g(x))}と表わせ,  x^ m \cdot o(x^ n)=o(x^ {m+n}) となる。

補題2:  x \rightarrow 0のとき  o(x ^m) \cdot o(x^ n)=o(x^ {m+n})

(証明)

 f(x) =o(x ^m) \cdot o(x^ n) g(x) = x^ {m+n}とおき, \displaystyle\lim_{x \rightarrow 0}\frac{f(x)}{g(x)}を考える。

 \displaystyle\lim _ {x \rightarrow 0}\frac{f(x)}{g(x)} = \lim _ {x \rightarrow 0}\frac{o(x ^m) \cdot o(x^ n)}{x^ {m+n}} =\lim _ {x \rightarrow 0}\frac{o(x^ m)}{x^ m}\cdot \frac{o(x^ n)}{x^ n} =\lim _ {x \rightarrow 0}\frac{o(x^ m)}{x^ m}\cdot\lim _ {x \rightarrow 0} \frac{o(x^ n)}{x^ n} =0

よって,ランダウの記号の定義式が適用できて, f(x)=o{(g(x))}と表わせ,  o(x ^m) \cdot o(x^ n)=o(x^ {m+n}) となる。

補題3:  m \leq nのとき,x \rightarrow 0 o(x ^m) + o(x^ n)=o(x ^m)

(証明)

 f(x) =o(x ^m) + o(x^ n) g(x) = x ^m とおき, \displaystyle\lim_{x \rightarrow 0}\frac{f(x)}{g(x)}を考える。

 \displaystyle\lim _ {x \rightarrow 0}\frac{f(x)}{g(x)} = \lim _ {x \rightarrow 0}\frac{o(x ^m) + o(x^ n)}{x ^m}  =\lim _ {x \rightarrow 0}\frac{o(x^ m)}{x^ m} + x^{n-m}\frac{o(x^ n)}{x^ n} =0

よって,ランダウの記号の定義式が適用できて, f(x)=o{(g(x))}と表わせ,  o(x ^m) + o(x^ n)=o(x ^m) となる。

終わりに

はてなブログのmarkdown記法で数式書いてみたんですが,Overleafと同じように書けなくて,癖が凄いですね。慣れるのに時間かかりそう。

RにはないPythonの文法・テクニック (データ型 編)

前回の記事で「RにはないPythonの文法・テクニック」として演算子,for文,条件分岐について書きました。記事を公開してからRガチ勢の方々に「これはRでもあるよ!」とツッコミを頂きすぐに訂正しました。

datascience-blog.com

今回はデータ型についてピックアップしてみます。名著である『Python 実践入門』『Python言語によるプログラミングイントロダクション』を参考にしながら,Python documentationを参照したいと思います。

Rに無いものだけピックアップする予定だったのに,書いてる途中にいくつかの機能はRにもあることが判明しました(私の知識不足)。でもメジャーな機能ではないということで載せたままにします爆

文字列への変数の埋め込み + 書式化

フォーマット済み文字列リテラル (f-string)

Python documentationの 2.4.3. フォーマット済み文字列リテラルのページに以下の記述があります。

フォーマット済み文字列リテラル(formatted string literal)または f-string は、接頭辞 'f' または 'F' の付いた文字列リテラルです。これらの文字列には、波括弧 {} で区切られた式である置換フィールドを含めることができます。

まず文字列リテラルとは単純にクオーテーション'で囲んでできる文字列/str型を指します。"apple"みたいなものです。フォーマット済み文字列リテラルは,文字列リテラルの一部を,別で用意している変数(文字列や数値)と置き換えられるという機能です*1。変数の埋め込みを行う際のポイントは2点です。

  • クオーテーションの前にfを付け
  • クオーテーションの中で{変数名}を用いる
>>> my_name = "パイソン太郎"
>>> my_age = 65
>>> f"Hi, my name is {my_name}. I'm {my_age} years old"
"Hi, my name is パイソン太郎. I'm 65 years old"

また以下のように数値や日付の書式化も可能です。

>>> width = 10
>>> precision = 4
>>> value = 12.34567
>>> f"result: {value:{width}.{precision}}" 
'result:      12.35'


- Rにおける変数埋め込みと書式化 -

「Rで変数埋め込みと書式化をするなら可読性の酷いpaste()とsprintf()の組み合わせだろうなぁ」と思っていましたが念の為調べてみると,なんとRにもf-stringと同じ用途のライブラリが3つもありました

どのライブラリもPythonのf-stringを模して作成されたものだと思います。str_interpとglueを使ってみました。超使いやすいです。

> library(stringr)
> my_name <- "パイソン太郎"
> my_age <- 65
> str_interp("Hi, my name is ${my_name}. I'm ${my_age} years old")
[1] "Hi, my name is パイソン太郎. I'm 65 years old"

> library(glue)
> glue("Hi, my name is {my_name}. I'm {my_age} years old")
Hi, my name is パイソン太郎. I'm 65 years old

ただ,Pythonではf-stringがデフォルトで搭載されている機能であるのに対し,Rのstr_interpやglueは比較的新しい関数なので,こういった記法がRユーザーの中ではまだ定着していないのではないかなと感じます。便利なのでどんどん使っていきましょう。

str.format

文字列メソッドの1つであるstr.formatはf-stringと同じように変数の埋め込みに利用できます用語集に以下の記述があります。

文字列の書式化操作を行います。このメソッドを呼び出す文字列は通常の文字、または、 {} で区切られた置換フィールドを含みます。それぞれの置換フィールドは位置引数のインデックスナンバー、または、キーワード引数の名前を含みます。返り値は、それぞれの置換フィールドが対応する引数の文字列値で置換された文字列のコピーです。

少し分かりづらいのでまとめると以下です。

  • 文字列の中に波括弧{}を含める
  • 文字列の後に.formatメソッドを続け,波括弧{}に入れたいもの(文字列や数値)を引数として渡す
>>> "Hi, my name is {}. I'm {} years old".format("パイソン太郎", 65)
"Hi, my name is パイソン太郎. I'm 65 years old"

他にも辞書型とformatを組み合わせた書き方など色々ありますが割愛します。


- Rにおけるstr.format -

str.formatはRのsprintfに似ていますが,sprintfは実数のみ扱えるのに対し,str.formatは文字列でも数値でも式でも扱えるので,これらは適応範囲が異なります。しかしながら上に挙げたstr_interpやglueで代用できるのでこのstr.formatの記法がRになくても特に問題なさそうです。

immutable(不変)なオブジェクト

用語集のimmutableを見てみます。

immutable(イミュータブル): 固定の値を持ったオブジェクトです。イミュータブルなオブジェクトには、数値、文字列、およびタプルなどがあります。これらのオブジェクトは値を変えられません。別の値を記憶させる際には、新たなオブジェクトを作成しなければなりません。

抑えておくべき点は数値・文字列・タプルは不変で,オブジェクトの中身を変えられないという点です。つまり数値1や文字列"a"に何か代入することができない,ということです。

>>> 1 = 2
  File "<input>", line 1
SyntaxError: cant assign to literal

>>> "a" = 2
  File "<input>", line 1
SyntaxError: cant assign to literal

Rでも数値や文字列に何か代入するはできませんが,そもそもそんな馬鹿げたことは誰もやらないと思います。なのでRユーザーで普段から"immutableなオブジェクト"について意識している人は少ないかと思います。

では何故Pythonにおいてimmutableなオブジェクトが敢えて取り上げられるのか。理由がこちらです。

イミュータブルなオブジェクトは、固定のハッシュ値が必要となる状況で重要な役割を果たします。辞書のキーがその例です。

つまり,辞書型のキーにはimmutableなオブジェクトしか与えられないということです。

# immutableオブジェクトの例
>>> {'one': 1} # 文字列
{'one': 1}
>>> {('John', '26'): 1} # タプル
{('John', '26'): 1}
>>> {1: 'one'} # 数値
{1: 'one'}

# mutable object
>>> {[1]: 0} # リストを辞書のキーとして与える
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: unhashable type: 'list'

>>> {{'one': 1}: 'first case'} # 辞書を辞書のキーとして与える
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: unhashable type: 'dict'

Rで普段意識しない"不変なオブジェクト"について,Pythonでは意識する必要があります。

カッコなしでタプルを作成

入門書には必ず説明が載っている配列の1つがタプルです。リストは要素が可変(mutable)な配列ですが,タプルは要素が不変(immutable)な配列です。

タプルの作り方についてPython documentationの5.3. タプルとシーケンスを見てみましょう。

タプルの表示には常に丸括弧がついていて、タプルのネストが正しく解釈されるようになっています。タプルを書くときは必ずしも丸括弧で囲まなくてもいいですが、(タプルが大きな式の一部だった場合は) 丸括弧が必要な場合もあります。

要するに,タプルは丸括弧かカッコなしで作成できます。また要素1つのタプルは,1つめの要素の直後にカンマを打てば作れます。

# カッコあり
>>> x = (1, 2, 3)
>>> print(x)
(1, 2, 3)

# カッコなし
>>> y = 1, 2, 3
>>> print(y)
(1, 2, 3)

# 要素一つカッコなし
>>> z = 1,
>>> print(z)
(1,)

このカッコなしで配列を作れたり,カンマで終了してOKという記法がRユーザーにとって見慣れない部分かなと思います。Rでベクトル/行列/配列を作る際は,それぞれc(),matrix(), array()関数で作りますが,関数なので当然カッコが必要あり,カンマで終わったりできません。

終わりに

間違いがあればすぐに訂正するので是非ご指摘ください。

書きながら思ったんですが,「RにないPythonのテクニック」なんて挙げ始めたら切りがないですね。 それでもメジャー所をピックアップしているつもりなので目を瞑ることにします。

次回は内包表記を取り上げます。

*1:Python3.6以前は%演算子を使った方法が主流でしたが,可読性が低いためPython3.6以降はf-stringが推奨されています。参照: PEP 498 Literal String Interpolation

RにはないPythonの文法・テクニック (演算子/for文/条件分岐 編)

学生時代からデータ解析にはRをメインで使ってきました。Pythonについてはデータ構造クラス/インスタンスなど最低限知っておくべきPythonの知識は学んだものの,細かい文法・テクニックなどについて詳しくなく,困ったときにはRのノリ + ググった付け焼き刃で乗り越えてきました。最近仕事でPythonを使う機会が段々多くなり体系的に学ぶ必要があると感じ,『Python 実践入門』と『Python言語によるプログラミングイントロダクション*1など読み進めています。

本を読み進める中で「こんなのRにはないぞ」「これは知らん」というポイントがいくつもありました。 そこで今回はPython documentationを参照しながらまとめていきます。今回は条件分岐,for文,演算子についてです。 私のようにRをメインで使っていてたまにPythonを使う,という人には役に立つかもしれません。(Pythonのヘビーユーザーの方はそっと閉じてもらって大丈夫です)

*1:こちらの本はPython3でなくPython2に対応しているので,読む際には注意が必要

続きを読む

WordPressからはてなブログに引っ越ししてみた。

WordPressからはてなブログに引っ越して来ました,しんめーです。

一年前にWordPressでブログを開設し,月に一度のノロノロペースで記事を書いていたのですが,色々悩んだ挙げ句に引っ越しました。

何故WordPressをやめたのか

以下の3つが大きな理由です。

  1. 広告管理が面倒くさそう & PV稼げるような万人受けする記事を書いていないから
  2. 気軽に記事を書ける雰囲気がWordPressにないから
  3. プラグインの管理が面倒くさいから

1つめについて,当初は「ブログで稼げたらいいな〜☆」と意気込んで,アフィリエイトに強いWordPressを選びました。しかし,大変面倒くさがりな性分であるため,広告をつける設定すら怠ったまま1年が経ちました。また,PVを大量に稼げるような記事を書きたいわけではないことに次第に気が付き,あくまでもブログは自分の備忘録なんだということがわかりました。広告も付けないただの備忘録を書くのであれば,わざわざWordPressを使う必要はなさそうだなと思ったわけです。

2つめについて,WordPressを使っている多くの方はデザインにこだわると思います。私の場合,デザインにこだわるほど,「ちゃんとした記事を書かなければ」と思いこむようになってしまいました。ヘッダーのデザインやロゴなどAdobeイラストレーターで作成したり,使用する色にもこだわったりした結果,毎回記事を書くのにとてつもない気合が必要になり,気軽に書きこめなくなっていました。本末転倒とんかつ弁当です。

3つめについて,WordPressはカスタマイズの自由度が高いのですが,自分で設定できないものに関しては他人が開発したプラグインをインストールして,定期的にアップデート/メンテナンスする必要があります。これが結構大変です。例えば,ソースコードを綺麗に表示するためのシンタックスハイライトのプラグインを使っていたところ,WordPressのバージョンアップデートを機にこれが使えなくなり,他のプラグインを探す必要がありました。こんなことが頻繁にあると,記事を書くどころではありません。

続きを読む

衛星画像データの解析を行うために学んだこと

先日の記事で紹介した通り,東大のディープラーニング講座DL4USを受講し,最終課題として「衛星画像とCNNを用いたスラムの特定」というテーマで解析を行いました。

この解析を行うにあたって,衛星画像の取得やQGISやPythonを用いた前処理が必要でした。今回の記事では,それについて私がどのような勉強をしたか順を追って紹介します。

学習のステップ

データ解析に関する私の事前知識ですが,普段の業務でテーブルデータは扱うものの,生の衛星画像を扱った経験はなく,観測衛星や地理データに関する知識もほとんどありませんでした。

そんな状態から以下のステップで学んで行きました。

  1. 観測衛星/衛星画像データに関する知識の学習
  2. 地理データ(ラスター/ベクター) に関する知識の学習
  3. QGISを用いた地理データの可視化/加工
  4. Pythonを用いた地理データの加工

観測衛星/衛星画像データの知識

まず基礎知識として衛星データの種類 (センサの種類,観測周波数),観測頻度観測エリア (観測幅),解像度 (分解能),データの入手方法など学びました。こちらは『宙畑 (Sorabatake)』さん(衛星についての記事を毎日アップしてる神ウェブサイト)の記事を読むことで一通り学びました。必読記事がこちらです。

宙畑さんの全記事の8割くらい読んだ覚えがありますが,とりあえずこの6つ読めば必要最低限の知識を学べます。

 

地理データの知識

地理データについては以下の2点,データの種類 (ラスターデータ / ベクターデータ)と座標参照系 (Coordinate Reference System:CRS)を押さえればいいかと思います。

データの種類

データ種類についてはesriジャパンのGIS基礎解説のページの説明がわかりやすいです。データは大きく以下の2つあります。

  1. ラスターデータ: 行と列の格子状に並んだピクセルで構成されるデータ
    • 衛星画像などの画像データなど
    • 拡張子はtiffやbmp
  2. ベクターデータ: ポイント(位置)・ライン(道路)・ポリゴン(建物)で表される情報(フィーチャー)を表現するデータ
    • 拡張子が.shp のいわゆるシェープファイル(shapefile)

シェープファイルでは,ポリゴンなどの1つの形(フィーチャー)が1レコードとなっているため,普段テーブルデータを扱っている方には比較的親しみやすい構造になってます。

f:id:shinmee:20200519203125p:plain
esriジャパンより引用

 

座標参照系 (Coordinate Reference System:CRS) 

CRSとは,地理データが地球上のどこに位置しているかを示す仕組みです(説明ムズい)。wikipediaの説明だと"a coordinate-based local, regional or global system used to locate geographical entities"です。データの種類(ラスターorベクター)にかかわらず,各ファイルがCRSを持っている,つまり座標の仕組みが設定されている必要があります。CRSには以下の2種類あります。

  1. 地理座標系 (Geographic Coordinate System)
    • 緯度と経度と標高によって表される座標
    • WGS 84という1984年にアメリカが開発した世界測地系が最も有名で,最も使われている。
  2. 投影座標系 (Projected Coordinate Reference System)
    • 赤道上のある地点を原点としたときに,対象の場所を(x, y)座標で表す方法

初学者には難解ですが,地理データを扱う際には避けて通れないので,理解が欠かせません。図解付きのわかりやすい説明がQGISの公式ページにあります。

 

QGISを用いた地理データの可視化/加工

QGISに関しては『Learn QGIS - Fourth Edition』という電子書籍を読みました。QGISの最新バージョンQGIS-3に対応している数少ない本の1つです。

私は最終章の手前まで読み,以下のことを一通り学べました。

  • QGISのタスクバーにある大量のボタンの完全なる把握
  • ベクターデータの作成/選択/編集/結合/保存
  • ラスター/ベクターデータの可視化
  • 可視化した地図の出力
  • 空間分析
    • ラスターの切り取り
    • ポリゴンの面積計算
    • ラスターのベクター化 / ベクターのラスター化
    • エリア毎の統計量(zonal statistics)の算出
    • ベクター同士の距離計算

QGISは地図を可視化するには最高のツールですが,正直言うと複雑な加工には適していません。例えば同じような加工を繰り返したい時,クリックで同じ作業をカチカチするのはストレスですし時間もかかります。こんなときPythonの地理データ用のライブラリの出番です。

 

Pythonを用いた地理データの加工

Pythonのライブラリを用いることで地理データの加工を効率的に,かつ再現性を担保しながら行なえます。こちらは特に書籍など用いず,公式のドキュメントだけ読んで勉強しました。便利なモジュールは以下です。

  • fiona
    • シェープファイル(.shp)を読み書きするライブラリ
    • geopandasと相性がいい
  • pyshpのshapefile
    • fiona同様に.shpを読み書きするライブラリ
  • Rasterio
    • 衛星画像(.tifなど)を扱うライブラリ
    • データの読み込み,バンドごとの値抽出,画像の大きさ抽出などできる
  • GeoPandas
    • Pandasの拡張パッケージ
    • shapefileを読み込め,ベクター情報を変数として格納でき,pandas.DataFrameのように扱える
    • CRSはオブジェクトの属性として格納できる
  • Shapely
    • ベクターデータを作成/処理するライブラリ
    • フィーチャー間の関係を真理値で返すメソッドが便利。例えば「点AがエリアBに含まれてるかどうか」簡単に調べられる。

またmatplotlibを使って地図の可視化もPythonで行えるようです(私は使いませんでしたが)。

 

最後に

何か新しいツールを身につける際に最も重要なことは,「ツールに関する最低限の知識を身につけて,そのツールで何ができるのかを把握すること」だと思っています。そこまで辿り着ければ,あとは自分がやりたいことに応じてググって解決方法を見つけられるからです。実際,今回の場合,私は上記の知識を習得した後,DL4USに必要な加工・分析はググって対応できました。是非皆さんもその辺を意識して勉強してみてください。

 

参考

DL4US修了したった

昨年秋頃に「DL4US」の3期生募集をツイッターで偶然知り,勢いで申し込み,2020年3月に無事修了しました。

今回はDL4USの内容と感想について書いていきます。これから応募しようと考えている方は参考にしてみてください。

DL4USってなに

東京大学松尾研究室が開講しているディープラーニング(深層学習)のオンライン講座です。Numpyの入試を受け,抽選で通った人のみ受講できます。特徴は以下の3点です。

  1. Jupyter notebook + GPU + Kerasで深層学習
  2. 毎週テーマ別に課題を出され,コンペ形式で他の受講生と課題の点数を競う
  3. Slackで受講生同士が課題について議論できる

ちなみにGithubに教材とコードがあるので独学もできますが,GPUや受講者同士のコミュニケーションなどのメリットは受講者だけが享受できます。

申し込み〜選考〜抽選

申し込み

Google Formに名前・所属などの基本情報と意気込み(深層学習を学んでどう活かしたいか)を書いた覚えがあります。

選考: Numpyオンラインテスト

申し込みをした後,Numpyのテストに関するメールが届きました。10月11日にメールが届いたので,4日以内にオンラインテストを受けてくださいということでした。

f:id:shinmee:20200520134836p:plain:w400

Numpyだけがテスト範囲でしたので,準備として100 numpy exercisesを解きました(実際は50問くらいしか解いてない)。本番のオンラインテストは問題が20問で,制限時間は2時間でした。内容としては簡単な行列/テンソルの作成・四則演算ぐらいで,解くのに40分もかかりませんでした。100 numpy exercisesを最初の20問やっておけば100点取れるレベルです。点数は公開されませんが,私は100点満点取れたと思います。
 

抽選

Numpyのテストの後に抽選?があったようです。numpyのテストがあまりにも簡単だったので抽選に進めたのは100点近いスコアの人だけだったのではないでしょうか。また最終的な受講者は多様性に富んでいたので,抽選とは言いつつも様々なバックグラウンドの人を選んで取ったのではないかな,と推測してます。
 

倍率

第1, 2期は10-20倍くらいだったと懇親会で公表されたらしいですが,3期の懇親会はまだ開かれていないので倍率についてはよくわかりません。

 

プログラム内容

Githubからのスクショになりますが,プログラムはこのような内容です。毎週木曜日にiLect上に教材とコードが公開され,そこから1週間かけて課題を解くという形でした。
 

f:id:shinmee:20200520135126p:plain

7週に渡ってテーマごとの課題を解きました。課題はKaggleと同じ形式で,データを渡され,1日毎にPublic Leaderboardのランキングが発表され,週の最後にPrivate Leaderboardのスコアが発表される形でした。(ちなみに私は,ガーナ出張と日本帰国が丸かぶりで最後の2週分は提出できませんでした。)
 

最終課題

  • 11月26日: 最終課題発表
  • 1月9日: 締め切り (提出率が悪いので2週間延長)
  • 1月23日: 正式な締め切り

11月26日に最終課題の進め方について運営の方から連絡がありました。

私は「画像認識(CNN)と衛星画像を用いた途上国におけるスラムの判別」というテーマで取り組みました。仕事納めのクリスマス直前からデータを集め始め,年末年始で解析し,最終的に日本語論文 (8ページ・9000字)としてまとめ提出しました。概要は以下です。

  1. 画像の取得: Google Mapからガーナの高画質衛星画像をダウンロード
  2. 前処理: 衛生画像をタイル上に切り取る
  3. アノテーション: 一部のタイル画像にslum or non-slumのラベル
  4. CNNモデル作成: VGG16で転移学習
  5. 推論: 最後にアノテーションしていない残りの画像を予測し,スラムの場所を特定
アノテーション済みのタイル画像
VGG16のアーキテクチャー

結果発表

運営の方が独自性/完成度/アイデアの3つを基準として最終課題を採点し,受講者は平均点と自分の点/順位を教えてもらえました。私の結果は以下のような感じで,総合7位でした。

f:id:shinmee:20200520135325p:plain:w400

受講生の数は講座開始の時点で120人くらい居たそうですが,最終的な修了生は54人ということでした。

修了後の繋がり

修了後はfacebookのグループで1,2,3期生全員がコミュニケーションを図れるようになっています。またツイッターでも個人的に繋がっている人達がいます。

懇親会が3月10日に予定されてましたが,COVID-19のせいで延期になりました。

 

感想

学んだこと

  • 単純なニューラルネット,LSTMあたりは専門書で勉強して仕事で使った経験もあったのですが,生成モデルや強化学習は初めて学ぶ内容でした。まだまだ身についた気はしていないのですが,学ぶ取っ掛かりができてよかったです。
  • 一番大きな学びは,最終課題を通して衛星画像の解析について学べたことでした。生の衛星画像をゴリゴリ前処理しながら解析したのは初めてで,衛星画像の知識やQGISなど勉強するものが多く,非常にやりがいがありました。

良かった点

  • 毎週課題が義務付けられる点: 修了するために取り組まざるを得ない状況はプレッシャーと共にやる気を与えてくれました。
  • Slack上での情報共有: 課題を解くための方法を受講者同士で共有できたので,Githubの教材を独学で学ぶより生産性が高かったと思います。
  • 無料である点: この規模の講座を開くのは結構な労力が必要だと思うのですが,すべて無料です。すごいです。ありがとうございます。

注意点

  • 教材が完全に既習者向け: 機械学習の初学者の人は軒並み脱落していったのではないかと思うほどに,ビギナー向きではありませんでした。教材の中で数式の展開は大幅に端折られていました。というのも,DL4US自体が理論ではなく実践(コーディング)がメインの講座だからです。これからDL4USに応募しようと考えていて機械学習の知識のない方は,機械学習と深層学習の本を1冊ずつ読んでから臨むのがよいと思います。

 

DL4USについてのまとめでした。深層学習について学んでみたい方は是非次の機会に応募してみてください。