ni_haruのブログ

ほぼ日記

Pythonで言語処理100本ノックを解く 06. 集合

Python言語処理100本ノックを解いてみます。
7本目です。

環境

Ubuntu 16.04.3 LTS
Python 3.5.2

06. 集合

"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

解答

from nlp05 import char_n_gram

if __name__ == "__main__":
    setX = set(char_n_gram("paraparaparadise",2))
    setY = set(char_n_gram("paragraph",2))
    print("X", setX)
    print("Y", setY)
    print("和集合", setX | setY)
    print("積集合", setX & setY)
    print("差集合(X-Y)", setX - setY)
    print("差集合(Y-X)", setY - setX)
    print("集合Xにseが含まれるか?", "se" in setX)
    print("集合Yにseが含まれるか?", "se" in setY)

前回の問題で作成した、n-gramを取得する関数を使っています。
集合演算はかなり簡潔に、分かりやすく書くことが出来ますね。

Pythonで言語処理100本ノックを解く 05. n-gram

Python言語処理100本ノックを解いてみます。
6本目です。

環境

Ubuntu 16.04.3 LTS
Python 3.5.2

05. n-gram

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

解答

def _n_gram(target, n):
    return [target[i:i+n] for i in range(0, len(target) - (n -1))]

def word_n_gram(target, n):
    return [" ".join(s) for s in _n_gram(target.split(), n)]

def char_n_gram(target, n):
    return ["".join(s) for s in _n_gram(list(target), n)]

if __name__ == "__main__":
    s = "I am an NLPer"
    print("単語bi-gram",word_n_gram(s, 2))
    print("文字bi-gram",char_n_gram(s, 2))

出力結果

単語bi-gram ['I am', 'am an', 'an NLPer']
文字bi-gram ['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']

冗長かも知れませんが、単語n-gram と文字n-gram で関数を分けました。
両方から呼ばれる関数 n_gram は外部から参照されない関数としたいのですが、pythonにはアクセス制限する修飾子がありません。 (Javaでいうprivateみたいな)
その代わり、先頭に"_"(アンダースコア)を付けることで、「内部でだけ使う」ということを示します。
命名規則なので実際はアクセスできますが、from M import * と書いてもインポートされなくなるので、書くべきでしょう。 (Pythonの慣習等をよく知らないまま書いてます。間違いあれば指摘下さると嬉しいです)

以上です。

Pythonで言語処理100本ノックを解く 04. 元素記号

Python言語処理100本ノックを解いてみます。
5本目です。

環境

Ubuntu 16.04.3 LTS
Python 3.5.2

04. 元素記号

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

解答

# -*- coding: utf-8 -*-

if __name__ == "__main__":
    ele_str = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
    single_list = [1, 5, 6, 7, 8, 9, 15, 16, 19]
    dict = {i+1:s[0] if i+1 in single_list else s[:2] for i,s in enumerate(ele_str.split())}
    print(dict)

出力内容

{1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 7: 'N', 8: 'O', 9: 'F', 10: 'Ne', 11: 'Na', 12: 'Mi', 13: 'Al', 14: 'Si', 15: 'P', 16: 'S', 17: 'Cl', 18: 'Ar', 19: 'K', 20: 'Ca'}

内包表記ってリストだけかと思っていましたが、辞書やセットにも使えるんですね。
「:」の前後に key と value をそれぞれ記載しています。

余談

この問題だと、12番目のマグネシウムが "Mi" となってしまいます。(正しくは "Mg" )
"Mg" で始まる単語が無かったのでしょうか…。

Pythonで言語処理100本ノックを解く 03. 円周率

Python言語処理100本ノックを解いてみます。
4本目です。

環境

Ubuntu 16.04.3 LTS
Python 3.5.2

03. 円周率

"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

解答

# -*- coding: utf-8 -*-

if __name__ == "__main__":
    pi_str = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
    pi_list = [ len(p.replace(",", "").replace(".", "")) for p in pi_str.split()]
    print(pi_list)

解いた後に他の方の解答を調べて気付いたのですが、この解答だと文章に ? や " があると修正が必要になってしまいますね。
問題文に「アルファベットの文字数」とあるので、アルファベットのみカウントした方が良さそうです。

pi_list = [ len(re.findall('[a-zA-Z]', p)) for p in pi_str.split()]

正規表現でアルファベットのみ抜き出して、その文字数をカウントしてみました。

Pythonで言語処理100本ノックを解く 02. 「パトカー」+「タクシー」=「パタトクカシーー」

Python言語処理100本ノックを解いてみます。
3本目です。

環境

Ubuntu 16.04.3 LTS
Python 3.5.2

02. 「パトカー」+「タクシー」=「パタトクカシーー」

「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

解答

# -*- coding: utf-8 -*-

if __name__ == "__main__":
    print("".join([s1+s2 for (s1,s2) in zip("パトカー","タクシー")]))

zip とリスト内包表記を使ってワンライナーで書けます。

zipで交互に取得

zip について、公式ドキュメントには以下のように書いてあります。

この関数はタプルのイテレータを返し、その i 番目のタプルは引数シーケンスまたはイテラブルそれぞれの i 番目の要素を含みます。

つまり、複数のリストから同じインデックスの値を取得し、それぞれのタプルを返してくれるわけですね。

print(*zip("パトカー","タクシー")) # --> ('パ', 'タ') ('ト', 'ク') ('カ', 'シ') ('ー', 'ー')

あとはタプルのイテレータから全ての要素を取得して連結すればよいです。
普通に for 文で書くと以下のようになります。

result = ""
for (s1, s2) in zip("パトカー","タクシー"):
    result += s1 + s2

print(result)

リスト内包表記で簡潔に

内包表記を使うと、ループ処理をワンライナーで書くことができます。
リスト内包表記はその名の通りリストを返すので、先ほど zip で取得したタプルの要素をリスト化できます。

[s1+s2 for (s1,s2) in zip("パトカー","タクシー")]  # --> ['パタ', 'トク', 'カシ', 'ーー']

あとは、リスト内の要素を連結すればよいです。 "区切り文字".join(リスト) で区切り文字を空にすれば単純に連結されます。

"".join([s1+s2 for (s1,s2) in zip("パトカー","タクシー")])  # --> "パタトクカシーー"

以上です。

Pythonで言語処理100本ノックを解く 01.「パタトクカシーー」

Python言語処理100本ノックを解いてみます。

環境

Ubuntu 16.04.3 LTS
Python 3.5.2

01.「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

解答

# -*- coding: utf-8 -*-

if __name__ == "__main__":
    s = u"パタトクカシーー"
    print(s[::2])

スライスを使えば簡単ですね。

Pythonで言語処理100本ノックを解く 00. 文字列の逆順

Python言語処理100本ノックを解いてみます。

環境

Ubuntu 16.04.3 LTS
Python 3.5.2

00.文字列の逆順

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

解答

# -*- coding: utf-8 -*-

def reverse(s):
    return s[::-1]

if __name__ == "__main__":
    print(reverse("stressed"))

自分で最初解いたときは、"".join(reverse(s)) と書いていましたが、スライスを使うと簡潔に書ける上に高速なので、そちらを採用しました。

スライスって?

シーケンス型(文字列はテキストシーケンス型です)に対して、s[i:j:k] と書くことで、s の「 i から j まででステップが k」の要素を取得できます。
また、k に負の数を指定すると、逆順に要素を取得できます。

s = "abcde"
s[0:4:1] # abcd
s[0:len(s):2] # ace
s[len(s):0:-1] # edcb

さらに、i, j, k は全て省略可能です。i または j を省略すると、それぞれ"端"の値を使用し、k を省略すると 1 が使用されます。 よって、以下のコードは同じ意味になります。

s[0:len(s):1] # abcde
s[::] # abcde 省略しているけど↑と同じ

逆順にする

さて、この場合 "端"の値とは 0 と len(s) のことですが、わざわざ"端"と書いたのには理由があります。
実は、どちらの端になるかは k の符号によって変わるのです。
k が負の数の場合、i が len(s) となり、j が 0 となります…と言いたいのですが、それだと上手くいきません。

s[len(s):0:-1] # edcb

終端に0を指定すると、0番目の要素の手前までを取得するので、先頭の要素が欠けますね。
と言って終端に -1 を指定すると、空になってしまいます。負の数を指定した場合、右から数えるため、要素が取得できなくなるためです。

s[len(s):-1:-1] # 空になる

これは、負の数をさらに大きくしていくと解決します。シーケンスの長さより、さらに1引いた数を終端にすれば上手くいきます。 i と j を省略しても、ちゃんと逆順の"端"(おそらく len(s) と -len(s)-1 )が使用されるので、逆順の文字列が取得できます。

s[lens(s):-len(s)-1:-1] # edcba
s[::-1] # edcba 省略しているけど↑と同じ

結論

s[::-1]と書けばよい。