How to make datas our friends

「エンジニアは発信していくことが責務である」という言葉に感化されて始めた勉強したことを書き留めていく備忘録的なやつ。

Python/Shellでtxtファイルの行数をカウントする

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

Pythonで以下URLのhightemp.txtを読み込み行数をカウントする。
同様にUnixコマンドでも行数の確認を行い、その際wcコマンドを使用する。

http://www.cl.ecei.tohoku.ac.jp/nlp100/data/hightemp.txt

結果

Pythonで行数カウント

> import pandas as pd
> data = pd.read_table('http://www.cl.ecei.tohoku.ac.jp/nlp100/data/hightemp.txt', header=None)

> len(data)
> 24

Unixコマンドで行数カウント

> wc -l /Path/To/hightemp.txt
> 24

解説/考察

今回はPythonでtxtファイルを読み込むためにpandasを使用しました。
CSVファイルを読み込む場合はread_csvを、txtの場合はread_tableを使用します。

import pandas as pd

# CSVを読み込み
pd.read_csv('/Path/To/Csv.csv')

# Textを読み込み
pd.read_table('/Path/To/Text.txt')

オプションで区切り文字の指定が可能ですが、デフォルトでread_csv=","/read_table="Tab"になっているので指定しなくても問題ないです。

ちなみにheaderオプションを指定しないと勝手に先頭行をヘッダーとしてデータフレームが作成されます。
今回はhightemp.txt内にヘッダーにあたる行が存在しないので、headerオプションを使用して先頭行がヘッダーにならないようにしています。

> data = pd.read_table('/Path/To/Text.txt', header=None)

こんな感じにデータが読み込まれました。
ヘッダーは数字になってますね。

f:id:minion024:20170520192455p:plain

ちなみにjupyter notebookを使用しています。

あとはlen()を使用して行数をカウントできます。

> len(data)
> 24

.indexを使用して行数を確認することも可能です。

> data.index
> RangeIndex(start=0, stop=24, step=1)

今回はUnixコマンドのwcを使用して行数を確認するよう指示されているのでUnixコマンドでも同様のことをやりました。
wc -l /Path/To/Text.txtで行数確認ができるので一瞬で終わりましたw

> wc -l /Path/To/hightemp.txt
> 24

Pythonで文章をTypoglycemia化する

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

スペースで区切られた単語列に対して、各単語の先頭と末尾の文字は残しそれ以外の文字の順序をランダムに並び替えるプログラムを作成する。
ただし、長さが4以下の単語は並び替えないこと。

適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え、その実行結果を確認する。

結果

単語の文字列をランダムに並び替える関数と、それを使用して文章をTypoglycemia化する関数の2つを用意しました。
なにかしらの関数で同じことができるかなと思ったのですが、すぐに見つからなかった&面白そうなので一から関数を用意して挑戦してみました。

▼ 単語の文字列をランダムに並び替える関数

import random
import re

def radomizeWord(word):
    # 変数を用意
    charList = []
    newList = []
    result = ''

    # 単語を分割してリスト化
    for i in range(0, len(word)):
        charList.insert(i, word[i])

    cnt = len(charList)-1
    
    # 末尾の文字を新リストの末端に挿入し旧リストから削除
    newList.insert(cnt, charList[cnt])
    charList.pop()
    # 先頭の文字を新リストの先頭に挿入し旧リストから削除
    newList.insert(0, charList[0])
    charList.pop(0)

    # 先頭と末尾の単語以外をランダムに抽出して新リストのii番目に挿入
    for ii in range (0, len(charList)):
        num = random.randrange(0, len(charList))
        newList.insert(ii+1, charList[num])
        # 挿入した文字は旧リストから削除
        charList.pop(num)

    for iii in range(0, len(newList)):
        result += newList[iii]

    return result

▼ 上の関数を使用して文章をTypoglycemia化する関数

def generateTypoglycemia(sentense):
    # 文章を単語に分割
    wordsList = re.split('\s', sentense)
    # 変数を用意
    resultList = []
    result = ''
    
    for i in range (0, len(wordsList)) :
        if len(wordsList[i]) <= 4:
            resultList.insert(i, wordsList[i])
        elif len(wordsList[i]) > 4:
            resultList.insert(i, radomizeWord(wordsList[i]))
            
    for ii in range (0, len(resultList)) :
        if ii == 0 :
            result += resultList[ii]
        elif ii != 0 :
            result += (' ' + resultList[ii])
    
    return result


▼ 実行結果

> generateTypoglycemia("I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind.")
> "I c'dnlout blveeie that I cuold alalutcy unrndetsad what I was rainedg : the pheonnamel pwoer of the huamn mdin."

解説/考察

単語の頭と末尾をそのままに間の文字を入れ替えても読めるぞおおお!というのがタイポグリセミアらしいです。
Typoglycemiaとは (タイポグリセミアとは) [単語記事] - ニコニコ大百科

ちょっと長いだけで特別難しいことはやっていないです。

単語を頭とケツの二文字以外ランダムに入れ替える方法ですが、今回は文字をばらして最初の文字をリストの頭に最後の文字をリストのケツに入れてから間の文字をランダムに抽出してリストに順番に入れていく方法で対応しました。

▼単語を文字単位で分割して順番にリストに格納

for i in range(0, 文字列の文字数):
 文字リスト.insert(i, 文字列[i])

▼文字列の頭とケツの文字を新しいリストの頭とケツにINSERTして削除。

「文字列の文字数-1」をやっているのはリストが0から始まるので、len()関数で取得した数字から1引いてあげる必要があるため。

新文字リスト.insert(文字列の文字数-1, 文字リスト[文字列の文字数-1])
文字リスト.pop()
新文字リスト.insert(0, 文字リスト[0])
文字リスト.pop(0)

文字リスト内の残りの文字を、文字リストに収められている文字数分for文を回してランダムに抽出して新文字リストに挿入しています。
挿入が終わったら最後にリストから除外。

for ii in range (0, len(文字リスト)):
 num = random.randrange(0, len(文字リスト))
 新文字リスト.insert(ii+1,文字リスト[num])
 文字リスト.pop(num)

あとは文章を単語に分割して、単語の文字数が4以上の場合この関数を使用して文字を入れ替える、というのをやっています。

心残りは、文章の最後にコンマが来た場合文字として認識してしまう件ですw
たとえば今回の例だと最後の単語が「mind.」であるため四文字以上と判定されランダムに文字を入れ替えてしまうので時間があるときに直したいです。

Pythonで文字列を文字コードに置換し暗号文化する

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

与えられた文字列の各文字を以下の仕様で変換する関数cipherを実装する。

  • 英小文字ならば(219 - 文字コード)の文字に置換
  • その他の文字はそのまま出力

この関数を用い、英語のメッセージを暗号化・復号化する。

結果

▼ 関数cipher

def cipher (sentense):
    # 変数を用意
    sentenseList = []
    result = ''

    for i in range (0, len(sentense)):
        # i番目の文字が小文字の場合
        if sentense[i].islower():
            # リストのi番目にアスキーコード(219-アスキーコード)に対応する文字を挿入
            sentenseList.insert(i, chr(219-ord(sentense[i])))
        # i番目の文字が小文字ではない場合そのままリストに挿入
        else:
            sentenseList.insert(i, sentense[i])
                
    # リストを文字列に戻す
    for ii in range (0, len(sentense)):
        result += sentenseList[ii]
        
    #文字列を返す
    return result

▼ 実行結果

> sentense = "I Am An Engineer."
> txt = cipher(sentense)
> txt
> 'I An Am Emtrmvvi.'
> cipher(txt)
> 'I Am An Engineer.'

解説/考察

リストを作成して文字数(i)回for文を回してリストに文字列のi番目の文字をリストのi番目に挿入しています。

for i in range (0, len(文字列)):
 リスト.insert(i, 文字列[i])

挿入の際にi番目の文字が小文字かどうかをif文で条件分岐させています。

if 文字列[i].islower():
 # i番目が小文字の場合の処理
else:
 # i番目が小文字ではない場合の処理

小文字の場合は、文字をアスキーコードに変換し ”219 - アスキーコード” の計算をおこない得た数字を再度文字に戻してリストに挿入します。
暗号化した文字をもとに戻す場合も同様の処理をおこなえば問題なしです。

リスト.insert(i, chr(219-ord(文字列[i])))

文字を暗号化する一連の流れを確認するとこんな感じ。

> ord('a')
> 97
> 219-97
> 122
> chr(122)
> 'z'
> ord('z')
> 122
> 219-122
> 97
> chr(97)
> 'a'

最後に変換したリスト内の各文字を1つの文字列に戻す作業です。
最初に分解したときと逆のことをやっているだけ。

for ii in range (0, len(文字列)):
       新文字列 += リスト[ii]

ちなみに最初に作成したときは、暗号化と復号化の関数を別々に用意して、関数内で以下のように計算をしていました。

# 暗号化
リスト.insert(i, chr(219-ord(文字列[i])))
# 復号化
リスト.insert(i, chr(219+ord(文字列[i])))

結果、わけわからない文字列が返ってきて失敗しました。
安直な考えで引いたのだから足せばいいやろ!と思ったのですがアホですね。

原因は上でやったように一連の流れを追うとわかります。

> ord('a')
> 97
> 219-97
> 122
> chr(122)
> 'z'
> ord('z')
> 122
> 219+122
> 341
> chr(341)
> 'ŕ'

まあ、わざわざやるまでもないですが。笑

Pythonでテンプレートによる文生成をおこなう関数を作成する

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装する。
さらに、x=12, y="気温", z=22.4として、実行結果を確認。

結果

Python3.6から新しく導入されたf-string関数を使用して作成してみました。

def getTemplate(x, y, z) :
    result = f'{x}時の{y}は{z}'
    return result
    
str = getTemplate(12, '気温', 22.4)

# 実行結果
> str
> '12時の気温は22.4'

解説/考察

今まで文字列への変数組み込みは%演算子でおこなっていたのですが、今回はf-string関数なるものが登場したと教えてもらったのでそれを利用しました。
初めて使いましたが、これは楽ちんでした!そしてスマート!

www.python.org

こんな感じで、文章内に直接変数名を入れることができるので可読性も申し分なし。

> age = 28
> f'I am {age} yo'
> 'I am 28 yo'

{}内で演算も可能です。

> age = 28
> f'I was {age-8} yo 8 years ago'
> 'I was 20 yo 8 years ago'

> import datetime
> birthday = datetime.date(1989, 2, 10)
> f'I was born in {birthday:%Y}'
> 'I was born in 1989'

Pythonには他にも文字列フォーマットに%演算子やstr.format()関数等があるのでそちらバージョンも一応作成しました。

# %演算子
def getTemplate(x, y, z) :
    result = '%s時の%sは%s' % (x, y, z)
    return result
str = getTemplate(12, '気温', 22.4)

#実行結果
> '12時の気温は22.4'
# str.format()
def getTemplate(x, y, z) :
    result = '{x}時の{y}は{z}'.format(x=x, y=y, z=z)
    return result
str = getTemplate(12, '気温', 22.4)

#実行結果
> '12時の気温は22.4'

やっぱり、f-staringが一番スマート

Pythonで2つのbi-gramの集合を求める

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

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

結果

set型を使用して意外と簡単にできました。

> x = set(ngram('paraparaparadise', 2, 'char'))
> y = set(ngram('paragraph', 2, 'char'))

# x y の和集合
> x.union(y)
> {'ad', 'ag', 'ap', 'ar', 'di', 'gr', 'is', 'pa', 'ph', 'ra', 'se'}

# x y の積集合
> x.intersection(y)
> {'ap', 'ar', 'pa', 'ra'}

# x の差集合
> x.difference(y)
> {'ad', 'di', 'is', 'se'}

# 'se' がxyに含まれるかの判定
> 'se' in x.union(y)
> True

解説/考察

前記事で作成したn-gramを返す関数ngramをそのまま利用して文字列「paraparaparadise」と「paragraph」のbi-gramをそれぞれ変数x/yに代入してます。

> x = set(ngram('paraparaparadise', 2, 'char'))
> x
> {'ad', 'ap', 'ar', 'di', 'is', 'pa', 'ra', 'se'}

> y = set(ngram('paragraph', 2, 'char'))
> y
> {'ag', 'ap', 'ar', 'gr', 'pa', 'ph', 'ra'}

024minion.hatenablog.jp

集合の理解が曖昧だったのでここを参照しました。図があってわかりやすし。
herb.h.kobe-u.ac.jp

また、for文を回してなんとかしようかなと思ったのですが、Pythonにはset型という便利なやーつがあるらしいのでそれで全部まかなえました。

3.7 set(集合)型 -- set, frozenset

今回はunion/intersection/deffirence関数を使用しましたが他の関数でも代用できそう。
今度、別の関数もいじってみようと思います。

Pythonでn-gramを作成する

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

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

結果

なんか色々イケてない感はあるものの関数ngramに引数で文章と何文字で区切るか、文字か単語かを渡してreturnで返すようにしました。
別にエラー文とかいらなかったな、とか思っていたりw

import re

sentense = "I am an NLPer"

def ngram(sentense, scale, type):
    # 関数の第三引数typeがwordの場合
    if type == 'word':
    
        # 変数を用意
        str = ''
        wordlist = []
    
        # 文章からピリオドを除外して空白で分割
        splited = re.split('\s', sentense.replace('.', ''))
    
        # もし第二引数の値が文章内の単語数より少なければエラーを出す
        if scale > len(splited):
            print("ERROR: The argument, scale must be lawer than the actual number of the words in the argument, sentense.")
        else:
            for i in range (0, len(splited)-(scale-1)):
                for ii in range (0, scale):
                    if ii == 0:
                        str += splited[ii+i]
                    else:
                        str += ' ' + splited[ii+i]
                wordlist.insert(i, str)
                # strを初期化
                str = ''
        
            return wordlist

    # 関数の第三引数typeがcharの場合
    elif type == 'char':
        charlist = []
        # 文字nグラムを作成
        trimed = (sentense.replace(' ', '')).replace('.', '')
        
        if scale > len(trimed):
            print("ERROR: The argument, scale must be lawer than the actual number of the characters in the argument, sentense.")
        else:
            for iii in range (0, len(trimed)-(scale-1)):
                charlist.insert(iii, trimed[iii:iii+scale])
    
            return charlist

    # 関数の第三引数typeがword/charでない場合エラーを出す
    else:
        print("ERROR: The 3rd argument, type must be char or word.")

data1 = ngram(sentense, 2, "word")
data2 = ngram(sentense, 2, "char")

実行結果

> data1
> ['I am', 'am an', 'an NLPer']
> data2
> ['Ia', 'am', 'ma', 'an', 'nN', 'NL', 'LP', 'Pe', 'er']

解説/考察

そもそもnグラムってなによってところから始まりました。無知ですみません。
で、色々ググりました。

kotobank.jp

要は、n文字/単語単位で文字列を1文字/単語ずつスライドさせていったやつの集合体という認識です。
今回はbiグラムなので2文字/2単語ですね。

ab cd ef gh を単語bi-gramにすると、ab cd/cd ef/ef gh に、文字bi-gramにすると ab/bc/cd/de/ef/fg/gh になる。
え、この認識で合ってますよね?w

で、今回は関数を作って単語bi-gramと文字bi-gramを作れってお題だったのでこんな感じの関数を作ってみました。

def ngram(sentense, scale, type):

第一引数に文章を、第二引数に何文字で区切るか(今回だとbi-gramなので2を渡す)、で最後に文字/単語どっちのn-gramがほしいか指定して関数を実行すると戻り値で帰ってくる的なやつです。

単語bi-gramを作る
splited = re.split('\s', sentense.replace('.', ''))
    
for i in range (0, len(splited)-(scale-1)):

 for ii in range (0, scale):
  if ii == 0:
   str += splited[ii+i]
  else:
   str += ' ' + splited[ii+i]
 
 wordlist.insert(i, str)
 str = ''

まず例のごとく文章を空白単位で分割して変数splitedにぶっこんでます。
そのあとfor文を回してリスト型の変数wordlistにデータを流し込んでいます。

for文を回す回数は「単語の数 - (何個単位かの値 - 1)」で計算させてみました。
例えば、bi-gramで3単語の場合 = 3 - (2 -1) で 2 になります。
文章が「AA BB CC」なら「AA BB / BB CC」にしたいので上の計算法を使えばぴったりリスト内に収まる計算です。

for i in range (0, 単語の数 - (何個単位かの値 - 1)):

続いてリスト内に格納する文字列の作成を行っています。

for ii in range (0, scale):
 if ii == 0:
  str += splited[ii+i]
 else:
  str += ' ' + splited[ii+i]

この部分です!単語単位で分割した文字列を、for文を回した回数に応じて横スライドさせ結合させてみました。
[ii+i]なので「ここでのfor文の回数+1つ上のfor文の回数」番目の単語をリストから取り出しています。

で最後にリストに生成した文字列を入れて文字列変数を初期化しています。

文字bi-gramを作る
trimed = (sentense.replace(' ', '')).replace('.', '')
        
for iii in range (0, len(trimed)-(scale-1)):
 charlist.insert(iii, trimed[iii:iii+scale])

こっちはだいぶシンプルです。

まず文章の空白を詰める作業をしています。
最初は以下の関数でやっていたのですがうまくいかず、結局replace関数で置換しています。
途中で気がついたのですが、この関数文章中の空白はトリムできないんですねw

sentense. strip()

あとは単語bi-gramと同じ原理でfor文を回してリストにデータを入れています。
文字列の結合は str[i:j:k] を使用してやってみました。

charlist.insert(iii, trimed[iii:iii+scale])

リストのiii番目に空白詰めした文字列のiii番目から「iii+何番目の文字まで取るかの値」番目までの文字列を切り出して格納しています。

最後に

今回のお題は今までで一番時間かかりました。

きっと僕がもっとPythonをマスターしたら、この記事のコードを書き換えたいと思うんだろうな〜とか思いつつGWが終わってしまったので明日からお仕事がんばります。笑

Pythonで文章を単語ごとに分解してn番目の単語は最初の1文字をそれ以外は2文字を切り出して連層配列を作成する

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

"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文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成する。

結果

ifで条件分岐をおこないfor文で辞書型のdictに値を流し込む方法で対応しました。

import re
sentense = 'Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.'
words = re.split('\s', sentense.replace('.', ''))

for i in range (0, len(words)):
    str = words[i]
    if i+1 in (1, 5, 6, 7, 8, 9, 15, 16, 19):
        dict[i + 1] = str[0]
    else:
        dict[i + 1] = str[:2]        

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'}

解説/考察

基本的な考え方は前記事で書いた内容そのままです。
024minion.hatenablog.jp

何番目の単語かによって切り取る文字数が違うのでシンプルにif文で対象となる数字を直接手打ちしています。
前回はリスト型を使用していますが、指定が連層配列型だったので今回はpythonの辞書型を使用しています。

if i+1 in (1, 5, 6, 7, 8, 9, 15, 16, 19):
 dict[i + 1] = str[0]
else:
 dict[i + 1] = str[:2]

for文のiが0から始まるのでi+1にして実際の数字と合わせるようにしています。

辞書型についてはここを参照しました。

www.pythonweb.jp