Open In Colab

8. ファイル・文字列操作

[この章の目的] テキストファイルやcsv・xlsx形式のデータをプログラムで扱えるようになる。

この章では、テキストファイルやcsvファイル(excelファイルはおまけ\(\clubsuit\))をPythonで操作する簡単な方法を学習する。

これまでの章では、データはリストとして既に与えられた状態から解析を行ったが、実際にデータを扱う際は
既に誰かが作成した何らかのファイルをプログラムで読み込んで操作する場合も多い。
この章の内容は、データ解析というよりは、Pythonでデータ解析をするための下準備に相当する。

愚直にコードを書いている事もあり少々泥臭い部分が多いが、この章のような操作のエッセンスを抑えておけば
普通にやると膨大な時間がかかる様々な処理を高速化・自動化することができるので、頑張って学習しよう。

これまでの章で学んだfor文や自作関数、if文などを駆使する格好の練習問題でもある。

8.1. 授業で使うファイルの準備

予め共有しておいた以下のファイルを用いる。

それぞれのファイルをダウンロードし、ご自身のGoogle Driveに指定されたディレクトリを作成し、ファイルをアップロードしてもよいが、少し時間がかかるので、Google Driveのマウントと、Linuxコマンドを駆使してファイルをダウンロードする方法を以下に示す。

本章では、ファイルの場所を指定するパスという概念がたびたび登場する。 以下のコードをそのまま使いたいという方は、マイドライブ直下にPro1AdDSというフォルダを作り、さらにその中にchapter8_dataというフォルダを作成して作業する必要がある。

  1. まずはGoogle Driveのマウントを行う:
    以降の多くのコードは、google driveの中にあるファイルを読み書きしたりする作業を伴うため、 Google Driveをマウントした状態でなければ、実行しても多くがエラーとなることに注意しよう。

from google.colab import drive
drive.mount('/content/drive')
  1. 以下のコードを一度だけ実行する:

!git clone https://github.com/SotaYoshida/Lecture_DataScience
!mkdir /content/drive/MyDrive/Pro1AdDS/
!mv Lecture_DataScience/Chapter8_data /content/drive/MyDrive/Pro1AdDS/chapter8_data
!ls /content/drive/MyDrive/Pro1AdDS/chapter8_data

\(\clubsuit\) 解説(気になる人むけ):

1つめの行ではまず授業資料のGitHubレポジトリをColab環境で間借りしているgoogleのサーバー上にクローン(≒コピー)する。2行目(mkdirコマンド)でマイドライブの下にPro1AdDSというフォルダの作成を試み、3行目(mvコマンド)でダウンロードしてきたレポジトリにあるChapter8_dataをさっき作ったPro1AdDSというフォルダの中に別名(先頭が小文字になっている)で移動する。 最後に、どんなファイルがあるかをlsコマンドで確認している。

8.2. テキストファイルの操作

膨大な行・数のテキストファイルに対して、人間が手で操作をするというのは非効率だし、時として非現実的となる。

誤変換を置換するくらいなら、どのテキスト/メモ帳アプリやwordでもできるが、全行(数千とか数万)に対して、決まった操作が必要な場合や複数ファイルについて同じ操作が必要になるときは、プログラムを書くと系統的な操作が可能となる。

まずはgoogle driveに保存したtest.txtという名前のファイルがあるかをlsコマンドで確認してみる。

!ls /content/drive/MyDrive/Pro1AdDS/chapter8_data/*

*はワイルドカード記号で、対象を任意とする命令に相当し、*.拡張子 などとして使うのも便利.

ファイルが見つからない場合やディレクトリ構造の指定に間違いがある場合は

No such file or directory

などと表示される。

!ls /content/drive/MyDrive/Pro1AdDS/chapter8_data/*txt 

とするとマイドライブ以下のPro1AdDS/chapter8_data内にある.txt形式のファイル一覧を表示させることができる。

では以降で、このtest.txtファイルに書かれているテキストを取得してみよう。
方法は幾つかあるが、最も標準的なものとして、ファイルを開いてテキストを取得する方法を試してみよう。

8.2.1. テキストファイルを開いて内容を読み出す

filename = "/content/drive/My Drive/Pro1AdDS/chapter8_data/test.txt" 
inp = open(filename,"r")
lines = inp.readlines()
inp.close()

1行目でファイルパスを指定しfilenameという変数(文字列)とした。
2行目では、指定したパスにあるファイルを開いている。
今はファイルに書き込むのではなく、既にあるファイルを開いて読み込むので"r"というオプションを指定している。
他には"w"(書き出し,上書き), "a"(書き出し,追記)などがあり、新しく上書きでファイルを作成したい場合は"w",すでにあるファイルの内容は消さずに追記したい場合は"a"を指定してopenする。
3行目では、inp(ファイルをopenして得たオブジェクト)に対してreadlinesというメソッドを適用している。 これは、ファイルに書かれているテキストを(可能なら)全行に渡って読み込みメモリにストアするメソッドになっている。

print(lines)

とすると、全ての行が読み込まれ、変数linesに格納されていることがわかる。ここで\nは改行記号を意味する。

ループを回して一行ずつ表示させると

for line in lines:
    print(line)

といった感じ(行ごとにスペースが生じている理由については後で説明します)。

必要な行番号が分かっている場合は、nlines = lines[2:10]などとして、要らないところは捨てても良い。 ※リストのスライスについては2章を参照

次に、もう少し具体的なテキスト操作をしてみよう。
まず、上の1行ずつ表示するコードでは、改行コードを明示的に含む文字列を一行ずつ表示したため、改めてprintすると余分なスペースが空いてしまう。

\(\clubsuit\) なぜならprint関数はデフォルトで末尾に改行\nを挿入するのでファイルにある改行記号とあわせて2回改行してしまう→参考リンク

8.2.2. strip関数

たとえばstrip()関数を使うと、文字列に含まれる空白、タブや改行コードを消去することができる。

a = "test character\t"
b = "test2 \n"
print("a", a, "←タブが隠れている")
print("b", b, "←改行される")
### strip関数をもちいて...
print("a.strip()", a.strip(),"b.strip()",b.strip())

先程のforループでstrip関数を適用してやると…

for line in lines:
    print(line.strip())

文字列の右側に空白や改行コードが入っていることが明確な場合は
stripの代わりにrstripを使ってもOK(rstripのrはrightの意味)。

for line in lines:
    print(line.rstrip())

ファイルによってはインデントをするために左側にタブ\tが含まれる場合もあります(PythonのコードをテキストとしてPythonから読むときなどがこれに該当)。そのような場合に左側にある空白やタブのみを取り除きたければlstrip()を使って取り除くことができる。

もちろんPythonではインデントが文法なので、インデントを一律で消す、といった操作は必要ないが、特定の状況では、lstripも使えると便利な場合がある。

Q. 上のファイルの文字列で#記号を含む行以降だけが必要な場合はどうすればいいか考えてみよう。

最も単純(?)な実装は

hit = 0 
for line in lines:
    if "###" in line:
        hit += 1 
        continue
    #hitが0の状態では何もしない
    if hit == 0 :
        continue 
    print(line.rstrip())

といった愚直な例が考えられる。 つまり、#を含む行に到達するまでの行は無視して、それ以降の行だけをリストに格納するというもの。 もちろん#を含む行が複数あるようなケースでは、自分が実現したい操作にあわせてコードを書き換える必要がある。

以下では、###dataまでの行が必要ないので、必要なところまでを別のリストに格納してしまおう。

hit = 0 #
nlines = []
for line in lines:
    if "###" in line:
        hit += 1 
        continue
    if hit == 0 :
        continue #hitが0の状態では何もしない
    nlines += [line]
print(nlines)

ひとくちにテキストファイルといっても様々な形式があり、表記方法もバラバラである。 プログラムで扱う際には、そのファイルの特性に合わせて適切な処理を行う必要があることに注意しよう。

8.2.3. split関数

また、1,2,3,4,5,6といったコンマやスペースで区切られたものをリストに格納したい場合には、split関数が便利。split関数は引数に何も指定しなければ、スペースや改行もしくはタブごとに文字列を区切ったリストを返す。

sample_text = "This is a\nsample\ttext."
sample_text.split()
for line in nlines:
    print(line.split())

カンマがあるときはカンマで分割する、という約束を表現したければ

for line in nlines:
    if "," in line :
        print(line.rstrip().split(","))
    else :
        print(line.rstrip().split())

などとすれば良い。これを利用すれば、空のリストにファイルから読んだ要素を詰めていくといった操作も実現できる。

# 数字とプロフィールの空リストを作り、そこに読み込んだものを詰めていく
# その際に、数字のリストとプロフィールのリストを分けたいとする
nums = [] 
profs = [] 

for line in nlines:
    if "," in line :
        nums += [ line.rstrip().split(",") ]
    else :
        profs += [ line.rstrip().split()]
print("nums", nums)
print("profs", profs)

上のnumsの様に、予め全ての要素が整数だと分かっていて整数に対する演算(四則演算など)を後でするのなら、str(文字列)型ではなくint型にしておくほうが良いこともあるだろう。

##リスト内包表記を使った実装
nums = []
for line in nlines:
    if "," in line : 
        tl =  line.rstrip().split(",")
        nums += [ [ int(tmp) for tmp in tl] ]
print("方法1:", nums)

## map関数(後述)を使った実装
nums = []
for line in nlines:
    if "," in line : 
        nums += [ list(map(int, line.rstrip().split(",") )) ]
print("方法2:", nums)

8.2.4. replace関数

replace関数で文字の置換が可能:

text = "abcdあいうえお"
text = text.replace("abcd", "1234")
print("置換や→",text)
print("除去にも→", text.replace("4", ""))

8.2.5. \(\clubsuit\) map関数

map関数はmap(操作,対象)という風に使って、対象の各要素に対して一括で操作を適用することができる。
今の場合、['1', ' 2', ' 3', ' 4', ' 5', ' 6']などの文字列のリストに対して、
整数型に変換するint関数を作用させるという操作を一度に行う。

注: map関数の返り値はmap objectというものであり、単純にprintしても中身が見れない。 元のようなリストの形で使いたい場合はlist()を使ってリストに変換するステップが必要になる。

tmp = ['1', ' 2', ' 3', ' 4', ' 5', ' 6']
print("map=>", map(int, tmp), "さらにlist化=>", list(map(int, tmp)) )
[1, 2, 3, 4, 5, 6]

世の中には、アンケート結果や産業データなどがcsv(カンマ区切りのテキスト)ファイルで公開されている場合が多いが、
その場合は上で説明したような手順でリストなどに格納すれば今まで行ったような解析やグラフ描画が実行できる

もちろんcsvを読むのに便利なライブラリもあり、上のような文字列操作が必ずしも必要でない場合もあるが、 いろんな形式のファイルをプログラムで読み込む場合には、上のような基本的な操作を組み合わせることで、必要なデータを取り出すことができることは覚えておこう。

8.2.6. テキストファイルの書き出し

次に、テキストファイルを書き込んで保存してみよう。
上の文字列で、敬称を”さん”から”様”に置換したテキストを作成して、それを別ファイルとして保存してみる:

filename = "/content/drive/My Drive/Pro1AdDS/chapter8_data/test_replace.txt" 
oup = open(filename,"w")  
for line in lines:
    print(line.rstrip().replace("さん","様"), file=oup) # file=["w"ないし"a"でopenしたファイル]にすることで、printする先をファイルに指定できます。
oup.close() #ファイルはきちんと閉じる.

Google Driveで、作成されたファイルをチェックしてみよう。

なお、filenameに元ファイルと同じものを指定するとopen(filename,"w")を実行した時点でファイルが上書きされて空ファイルになるので注意。

今の例ではもちろん、手で置き換えたりするほうが遥かに速いが、こうしたPythonによるファイル操作を覚えておくと

  • ファイル自体が大量にあり、同じ操作を繰り返す場合

  • 単一のテキストファイルに大量の行に渡って情報がある場合

など、手作業が非現実的な様々な状況でも、楽ちんに作業を終わらせることができる(かもしれない)。

一度、プログラミングを用いたファイル操作をする発想を持つと、もうそれなしでは戻れない…かもしれない。

8.2.7. 文字コードに関連した余談

Windows環境で作成されたテキストファイルを扱う際は読み込みで、文字コードによるエラーが出るかもしれない。 文字コードとは、英数字のみならず、日本語や中国語などの文字をコンピュータで扱うための規則のことで、こうした文字・記号などとコンピュータ内部での数値(バイト列)との対応を定めたものである。 最も一般的なのはUTF-8と呼ばれるものであるが、Windows環境ではShift-JISという文字コードが使われることもある。 これが原因で、テキストファイルを読み込む際に文字化けが生じることがある。

最近ではメモ帳でもUTF-8(世界標準)を採用しているよう(→MicrosoftのWindows blogの記事)だが、古いテキストファイルだとShift-JISになっているかも。そういうときは、open(file, "r", encoding = "shift_jis")など、ファイルを開くときにencodingを明示的に指定する必要がある。明示的にUTF-8で保存したいときはopen(file, "w", encoding = "utf-8")などとする。
参考: 公式ドキュメント
ここまで勉強してきた皆さんには「そんなの、パソコンに存在するShift-JISで書かれたテキストファイルを全てUTF-8に変換するPythonスクリプト書けばいいんじゃね?」という発想があることを期待しています。

8.3. csv

csvファイルは、カンマ区切りのテキストファイルであり、表形式のデータを保存するのに適している。

8.3.1. アンケート分析

冒頭の二番目のファイルpython_handling_test.csv(https://raw.githubusercontent.com/SotaYoshida/Lecture_DataScience/main/Chapter8_data/python_handling_test.csv)はあるアンケート結果をまとめたファイルになっている。
これは、Google フォームで作成したアンケートで、国数英社理(中学の5科目)に対する得意/苦手意識の調査を想定した疑似アンケートになっている。

このようなアンケート調査は事務作業や卒業研究などで頻繁に見られ、会社や大学など所属コミュニティで何らかの意思決定に用いられることも多い。 こうしたアンケート分析を行っていると、

  • 各回答項目同士の関係が知りたい

  • 明示的な項目以外の情報も抽出したい

といった要望が出てくる。今の場合でいうと、

  • 各科目ごとの得意・苦手意識の相関を調べたい

  • 夜中(あるいは日中)にアンケートを回答した夜型(昼型)の人に見られる特徴がなにかないか?

といったイメージ。そんなとき、

国語が得意(どちらかというと得意)と回答した方に質問です。
英語についてはどうでしょうか?

などと新たに設問を増やしてアンケートをやり直すというのは得策ではないので、 すでに得られた情報からさらなる情報を引き出すことを考えたい。

こんなとき、csvファイルに記載された情報を整理してプログラムで扱いやすくすることを考えてみよう。

余談: このcsvファイルをExcelで開こうとするとお使いの環境によっては文字化けを起こす。これはgoogleフォームで作成されたcsvファイルの文字コードが世界標準のutf-8を使用しているのに対し、ExcelがShift-JISという時代遅れな文字コードでcsvファイルを開こうとするためで、Googleのスプレッドシートや、Mac標準のNumbersで開くと文字化けはしない。

2000件の回答は、もちろん手作業で入力したわけでも誰かに協力してもらったわけでもなく、一定のルール(傾向)を勝手に設定した上でランダムに回答を作成しフォームから自動回答するPythonスクリプトを書いている。 時間に余裕があれば、こうしたWeb操作を自動化する方法も授業で扱います。 c.f. ブラウザ操作, Webスクレイピング

#読み込むファイルのパスの指定
filename = "/content/drive/My Drive/Pro1AdDS/chapter8_data/python_handling_test.csv" 

とりあえずファイルの中身を数行表示してみる。csvファイル(コンマ区切りのテキスト)なので、テキストファイルと同じ方法をとる(他の方法ももちろんある)

inp=open(filename,"r")
csv_lines=inp.readlines() 
inp.close()
print("行数は",len(csv_lines))
for i in range(5):
    print(csv_lines[i].rstrip())

ちなみに…pandasライブラリを使うとcsvをサクッと読み込むことができる

import pandas as pd 
df = pd.read_csv(filename)
df

ただし、csvファイルのフリをしたいい加減なファイルの場合はエラーになることもあるので、 独自にコードを書いて例外的な処理をすることも必要になる。

さて、csv_linesに格納したデータをもう少し扱いやすいように変更しよう。
最初の0行目はどういうデータが入っているか(データの項目)を表している。
1-2000行目には2000人分の回答が詰まっている。

これによると、

0列目: 回答した時刻
1列目: 性別
2列目: 国語
3列目: 数学
4列目: 英語
5列目: 社会
6列目: 理科

らしい。いろいろなデータの整理方法があると思うがここでは、

  • 処理A 0列目の時刻を24時間表記にして表示する

  • 処理B 2-6列目の各科目の得意・苦手意識を、文字列を除去して数値[-2,-1,0,1,2]として扱う

をまずやってみよう。

#処理Aのための関数
#input_strが、"年月日 時刻(h:m:s) 午前/午後 GMT+9" という文字列である、というデータの"構造"を知った上での実装になっていることに注意
def make_time_24h(input_str):        
    time  = input_str.split()[1]
    AMPM = input_str.split()[2]
    hms = time.split(":")
    h = int(hms[0])
    if AMPM == "午前":
        output_str = time 
    else :
        if h != 12:
            output_str = str(h +12)+":"+hms[1]+":"+hms[2]
        else:
            output_str = str(h)+":"+hms[1]+":"+hms[2] # 12時xx分だけは別の取り扱いが必要
    return output_str

nlines=[] #整理したものをリストとしてまとめるための空のリスト
for nth,line in enumerate(csv_lines[1:]): 
    nline = line.rstrip().replace('"','').split(",") # 改行文字の除去、ダブルクォーテーションの除去, カンマで分割    
    # 処理A)
    time = make_time_24h(nline[0])
    M_or_F = nline[1] #性別

    # 処理B)
    points = [ int(nline[k].split()[0]) for k in range(2,7)] #各科目の値だけのリスト(points)を作成, map関数にするのもあり

    nline = [time, M_or_F]+points  #リストを連結(時刻,性別と各科目の値を同じ階層で結合)して、nlineという名前で上書き
    nlines += [ nline ]

    # うまく編集できたか400行おきほどでprintしてチェックしてみる
    if nth % 400 == 0 :
        print("編集前", line.rstrip())
        print("編集後", nline)
        print("")

最後に、各項目の得点を適当なリスト(あるいはnp.array)に整形しておけば、種々の分析を行うことができます。

import numpy as np
points = [ [] for i in range(5)]
for tmp in nlines:
    for i in range(5):
        points[i]+=[tmp[2+i]]
print("points", np.array(points))
print("各科目の平均スコア:", [np.mean(points[i]) for i in range(5)])

相関分析は別の章で扱ったので、具体例は省略する。

もちろん、このようなデータを扱う際には、pandasライブラリを使うともっと簡単にデータを扱うことができるが、 このような泥臭い?基本的な操作を覚えておくことは、データ解析の基礎を固める上で非常に重要である。

8.4. \(\clubsuit\) 複雑なエクセルファイルの操作

家計調査のデータが入ったエクセルファイルkakei.xlsxを使用する。
以下では、上と同じディレクトリにkakei.xlsxを置いたと仮定して処理を行うので、適宜ご自身の環境にパスを置き換えて実行しよう。

#読み込むファイルのパスの指定
filename = "/content/drive/My Drive/Pro1AdDS/chapter8_data/kakei.xlsx" 

まずはxlsxファイルをPythonで読み込んで、どんな”シート”があるのかを確認してみよう。

import pandas as pd
input_file = pd.ExcelFile(filename)
sheet_names = input_file.sheet_names
print("pandas: シート名",sheet_names)

たくさんシートがあることが分かった。

次に、Sheet1の中身をのぞいてみよう。まずは行と列の数を取得してみる:

Sheet1 = pd.read_excel(filename, sheet_name="Sheet1")
print("行,列の数", Sheet1.shape)

0-5番目の行にはどんな値がセルに入っているのかな…と思ったら

for i in range(5):
    print( list(Sheet1.iloc[i]) )

などとする。このように、扱いたいファイルの”構造”を知ることがやりたい操作を系統的に実行するための第一歩になる。

このエクセルを実際に開くとSheet1からSheet12までが複数都市の家計調査のデータでS1からS12までが気候データになっていて、 1-12までの数字が2017年の1月から12月までに対応していることが分かる。

実際のデータを触っていると「2006年までとそれ以降とでデータファイル(.xlsx)の”構造”が違う」といったことも出てくるので、 最初は特定のものに合わせたコードを作り、徐々に汎用性の高い(例外に対応できる)コードにしていくのが良い。

このエクセルを使って実際に作業をするには、pandas等のライブラリの細かい使い方を説明することになるため、授業ではやらず、以下の”おまけ”にいれておく。

8.4.1. \(\clubsuit\clubsuit\) おまけ

以下のコードは、プログラミングの”ありがたみ”を感じてもらうためのお試し用。 (昔書いたかなり読みにくいコードなのであまり真剣に読まないで…) 例外処理も完璧でないのだが、許してほしい。

大量の画像ファイルをドライブに生成するので、以下を読んだ上で実行してください

以下のコードたちを何もいじらずに実行すると、
全都市の月別平均気温と全品目の世帯平均支出のうち、
相関係数の絶対値が0.9以上のもの(291通り)をプロットして画像として保存します。
pthreの値を小さくすると、生成される画像の数がとんでもなく増えるのでやらないでください。

(0.9 → 291通り, 0.8 → 1234通り, 0.7 → 2871通り,
0.6 → 5233通り, 0.5 → 8375通り, 0.0 → 32876通り)

Google Colab上で実行して291枚の画像が生成されるまでに80~150秒程度かかるようです。

この時間未満でエクセルで操作をして同様の処理を完了出来るという方は…おそらく地球上にいないでしょう(要出典)

# 画像がいっぱい生成されると面倒なので画像を保存するフォルダを作成しておく
!mkdir /content/drive/MyDrive/Pro1AdDS/chapter8_data/kakei_cor_pic 
!pip install japanize_matplotlib 
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import japanize_matplotlib
import time

class ebook:
    def __init__(self,inpf):
        self.input_file = pd.ExcelFile(inpf)
        sheet_names = input_file.sheet_names
        self.sname = sheet_names
        self.ns = len(sheet_names)
        print("pandas: シート名",sheet_names)
        print("self.ns", self.ns)

        s_kikou=[]; s_kakei=[]
        for i, sheetname in enumerate(self.sname):
            if "Sheet" in sheetname :
                s_kakei += [ i ]
            elif "S" in sheetname :
                s_kikou += [ i ]
        self.s_kakei,self.s_kikou = s_kakei,s_kikou
    def indices(self):
        return self.s_kakei, self.s_kikou
    def readkakei(self,ikakei) :
        ws = self.input_file.parse(sheet_name=self.sname[ikakei])
        nr = ws.shape[0]
        premode = True
        items = []
        for ii in range(nr): 
            trow = list(ws.iloc[ii])
            hit = 0
            if premode == True:
                for jj,tmp in enumerate(trow):
                    if type(tmp) is str:
                        if "市" in tmp:
                            hit += 1
                if hit > 5:
                    premode=False
                    i_kakei=[];p_kakei=[]
                    for jj,tmp in enumerate(trow):
                        if type(tmp) is str:
                            if "市" in tmp:
                                i_kakei += [jj]
                                p_kakei +=[ tmp ] 
                    v_kakei = [ ]
            else:                    
                if ii >= 22:
                    if type(trow[8]) is str and trow[8] != "":
                        v_kakei += [ [trow[jj+1] for jj in i_kakei] ]
                        items += [trow[8]]                         
        return i_kakei, p_kakei, v_kakei,items
    def readkikou(self,ikikou):
        ws = self.input_file.parse(sheet_name=self.sname[ikikou], header=None)
        nr = ws.shape[0]
        quantities = [];v_kikou=[]
        premode=True
        for ii in range(nr): 
            trow = list(ws.iloc[ii])
            if premode :
                if any(["市" in str(tmp) for tmp in trow]):
                    Tplaces = trow[1:]
                    premode=False
            else:
                quantities += [ trow[0] ]
                v_kikou += [ trow[1:] ]
        return Tplaces, v_kikou,quantities

# 月ごとの色を決める関数
def seasoncolor(month):
    if month <= 2 or month ==12:
        return "blue"
    elif 3 <= month <=5:
        return "green"
    elif 6 <= month <=8:
        return "red"
    elif 9<= month <=11:
        return "orange"
    return tcol

# 指定した品目・地点の散布図を描く関数
def plot_cor(x,y,item,quantity,place,corrcoef):    
    fig = plt.figure(figsize=(4,4))
    ax = fig.add_subplot(1,1,1)
    ax.set_facecolor("#e0e0e0")
    ax.set_title(place+"   r="+str("%8.2f" % corrcoef).strip())
    ax.set_xlabel(item);ax.set_ylabel(quantity)
    ax.grid(True,axis="both",color="w", linestyle="dotted", linewidth=0.8)
    for i in range(len(x)):
        tcol=seasoncolor(i+1)
        ax.scatter(x[i],y[i],marker="o",s=5,color=tcol,zorder=20000,alpha=0.7)
        ax.text(x[i],y[i],str(i+1)+"月",color="k",fontsize=8)
    plt.savefig(oupdir + "corr_"+item+"vs"+quantity+"_at_"+place+".png",dpi=300) 
    plt.close()

# 指定した品目・地点の散布図を描く関数
def calcor(places,items, Vs_kakei,Tplaces,quantities,Vs_kikou):
    hit = 0; num_pic=0
    Vs = [] 
    for j_K,place in enumerate(places):
        for j_T, Tplace in enumerate(Tplaces):
            if place != Tplace :
                continue
            for ik,item in enumerate(items):
                kvalue = np.array([ Vs_kakei[i][ik][j_K] for i in range(len(Vs_kakei))])
                quantity=quantities[iT]
                Tvalue = np.array([ Vs_kikou[i][iT][j_T] for i in range(len(Vs_kikou))])
                if all(Tvalue) == 0.0: ## missing value in climate data
                    continue
                if printlog:
                    print("@", place," ",item,kvalue," VS ",quantity, ",",Tvalue)
                corrcoef=np.corrcoef(kvalue,Tvalue)[0][1]
                Vs += [ [ corrcoef, item, quantity, place] ]
                if abs(corrcoef) > pthre:
                    hit += 1
                    if pltmode==True:
                        plot_cor(kvalue,Tvalue,item,quantity,place,corrcoef)                       
                        num_pic += 1
    print("hit:",hit, " number of picture", num_pic)

if __name__ == "__main__":
    ti=time.time()
    T=True
    F=False
    inpf = "/content/drive/My Drive/Pro1AdDS/chapter8_data/kakei.xlsx"
    oupdir = "/content/drive/My Drive/Pro1AdDS/chapter8_data/kakei_cor_pic/" #適宜置き換える
    iT = 6  # iT=6: 日平均気温
    printlog= F #条件にhitした都市の品目と気候データを逐次printするかどうか. (Fを推奨)
    pthre= 0.90 ## corrplotを描く相関係数の絶対値のthreshold(下限)
    pltmode = T ## T:plotする F:計算のみ (画像をいちいちplotして保存する必要がない場合Fを推奨)
    year="2017" 

    wb=ebook(inpf)
    s_kakei,s_kikou=wb.indices()   
    Vs_kakei=[]; Vs_kikou=[]
    for i,ind_kakei in enumerate(s_kakei):
        i_places,places, v_kakei,items = wb.readkakei(ind_kakei)
        Tplaces, v_kikou, quantities  = wb.readkikou(s_kikou[i])
        Vs_kakei += [ v_kakei ]
        Vs_kikou += [ v_kikou ]
    calcor(places,items,Vs_kakei,Tplaces,quantities,Vs_kikou)    

    tf=time.time()
    print("Elapced time[sec]:", tf-ti)

8.5. 電子ファイルのフォーマット

プログラムでデータを機械的に読み出して活用することで、人間が到底出来ないような作業効率を実現することができる場合も多い。 そんな光の側面ばかりなら良いが、実際にはそう上手くは行かないことも多い。

業務のデジタル化・デジタルトランスフォーメーションなどといった標語とは裏腹に、世の中にあふれるcsv,スプレッドシートなどは、 csvと謳っておいて、実際にはカンマ区切りではなくタブ区切りであったり、機械判読を全く想定していないデータの書き方・並べ方となっているものが多く、 プログラムを書ける人にとっては苦痛な状況も多い。

総務省統計局は令和2年2月に、政府統計(e-Stat)に関して統計表における機械判読可能なデータの表記方法の統一ルールの策定というものを出している。 これが最適な提案かはさておき、データの記述に法則性と機械判読性をもたせる意識を全員が持つことが重要なように思う。

お掃除ロボットが床を綺麗にするためには、まずお掃除ロボットが走れるよう掃除する(床に物が散乱していない)という条件が求められる、という話だ(そうなの?)。

8.6. ファイルパスの指定

ファイルがコンピュータ上でどこにあるかを指し示す文字列はファイルパス(パス, path)と呼ばれる。
"/content/drive/My Drive/XXX.png"もファイルパスの一例になっている。

たとえば…

[Sota]というユーザの[ドキュメント] (あるいは[書類])フォルダに
[csv_file]というフォルダがあり[test.csv]というcsvファイルが入っている

とするとそのファイルを指し示すパスは
Windowsの場合→ C:\Users\Sota\Douments\csv_file\test.csv
macOSの場合→ /Users/Sota/Documents/csv_file/test.csv となる。

注:

  • Windowsの場合→”C”の部分は皆さんのディスク環境に依存

  • Google Colab.環境では、Unix(Mac)やLinuxと同様の方式(スラッシュを使用)

  • バックスラッシュ\はWindowsの日本語環境では¥円記号で表示される
    (プログラムなどを書く人には厄介な仕様だったりする)

コンピュータには、ホームディレクトリというものが指定されておりWindowsなら C:\Users\ユーザー名,Macなら /Users/ユーザー名に通常設定されていて、ユーザーがよく使うデスクトップや写真・ドキュメントなどのフォルダはホームディレクトリ直下に配置されている。また、ホームディレクトリは~/で簡略化して指定することもできる。 OSにもよるが…ライトユーザーはホームディレクトリより上の階層を触らないことが推奨されている。理由は、システムファイルなどが入っているため。

パスの指定の仕方にはその他にも方法があり、ピリオドやスラッシュを駆使して現在のディレクトリからの[相対パス]で指定する事もできる。たとえば…

Home
├ Documents
│└─ AdDS2021
││  └─ Report1
│└─ AdDS2020
││  └─ Report1
││  │  └─ StudentA
││  │  └─ StudentB
││  └─ Report2
│└─ AdDS2019
├ Picures

こういう階層構造になっていて、現在Home/Documents/AdDS2020/Report1という ディレクトリにいるとすると、そこから

  • StudentAへの相対パスは ./StudentAStudentA

  • Report2への相対パスは ../Report2

  • AdDS2019への相対パスは ../../AdDS2019

  • Pictures内のhonyarara.jpgへの相対パスは../../../Pictures/honyarara.jpg

といった感じ。前述のように愚直にReport1フォルダを指定するときは/Users/Sota/Documents/AdDS2020/Report1といった感じで、これを相対パスと対比させて絶対パスと呼んだりする。

8.6.1. ファイル名に使用すべきでない文字

授業で公開しているノートブックの名前は基本的に半角英数字とアンダースコアのみで構成されている。 これは別に作成者(吉田)がイキってる訳ではない。

  • 半角スペース(以下␣という記号で表現する)

  • 各種括弧 (),{},[]

  • カンマ ,

  • ピリオド .

  • ハイフン -

  • スラッシュ /

  • エクスクラメーションマーク !

  • 円記号(バックスラッシュ) ¥

  • その他、機種依存文字などはもちろん、全角記号等

などは、(プログラムで扱う予定がある)ファイルの名前には使用しないことが推奨される。その理由は色々あるが

  1. 機械の解釈にambiguity(あいまいさ)が生じる

  2. (1.により人間側の操作が増えて)面倒

というところに尽きる。例を示そう。
Google Colab.上では冒頭に!を付けることで、以下に例を示すようなLinuxコマンドを実行できる。

!ls hogehoge.pdf #← lsコマンド リスト(該当ファイル等)を表示
!mkdir hogehoge #← make directoryコマンド
!rm hogehoge #←remove(削除)コマンド

たとえば半角スペースが入ったtest␣.pdfというファイルがあったとする。
これをlsコマンドで表示させようとして

!ls test .pdf

という命令を行うと、test␣.pdfという指定ではなくtest.pdfという2つのファイルが存在するかどうかを尋ねる命令になってしまう。
この場合、test␣.pdfの有無を調べたければ、別途バックスラッシュを入れて「記号としての空白です」と機械に教えなくてはならない。このことを、エスケープとかエスケープシーケンスなどと呼ぶ。

!ls test\ .pdf

といった具合に、人間側の手間が必要になってしまう。

人間が目で見るフォルダ名と機械に与えるべきパスが異なるというのは、やはり色んな場面で不便が生じる。 上記の記号や2バイト以上の文字はファイル(フォルダ)名に使わないのがコンピューターにとっては無難である。

こういうことは小中高や大学でも理由付きで教えてくれなかったりするので、プログラミングをやって初めて気がつく(気にするようになった)という人も多いかもしれない。

機械判読可能なデータの表記方法やファイル名の命名規則などは、プログラムを書く上での基本的な知識として覚えておくと良い。