Open In Colab

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

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

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

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

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

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

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

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

本章では、ファイルの場所を指定するパスという概念がたびたび登場する。 末尾にパスの説明があるので、以下のコードが実行できたら、パスの説明を確認しよう。

  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に保存したpython_handling_test.csvという名前のファイルがあるかをlsコマンドで確認してみる。

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

*はワイルドカード記号で、対象を任意とする命令に相当し、*.拡張子 などとして使うと、拡張子が特定のもののファイルをすべて対象とすることができる。

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

No such file or directory

などと表示される。

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

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

8.2.1. csvファイルの説明#

こちらのpython_handling_test.csvというファイルはGoogleフォームで作成したアンケートで、国数英社理(中学の5科目)に対する得意/苦手意識の調査を想定した疑似アンケートになっている。このようなアンケート調査は事務作業や卒業研究などで頻繁に見られ、何らかの意思決定に用いられることも多い。こうしたアンケート分析を行っていると、

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

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

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

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

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

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

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

などと新たに設問を増やしてアンケートをやり直すというのは得策ではない。そこで、既に得られた情報からさらなる情報を引き出すことを考えたい。こんなとき、csvファイルに記載された情報を整理してプログラムで扱いやすくすることを考えてみよう。

余談1: このcsvファイルをExcelで開こうとするとお使いの環境によっては文字化けを起こす。これはgoogleフォームで作成されたcsvファイルの文字コードが世界標準のutf-8を使用しているのに対し、ExcelがShift-JISという時代遅れな文字コードでcsvファイルを開こうとするためである。談じてファイルの作成者が悪いわけではない。

余談2: 2000件の回答は、もちろん手作業で入力したわけでも誰かに協力してもらったわけでもなく、一定のルール(傾向)を勝手に設定した上でランダムに回答を作成しフォームから自動回答するPythonスクリプトを書いて生成したものである。こうした疑似データの自動生成も、データ分析の練習をする上で役に立つことがあるので、興味がある人は授業後にでも調べてみよう。

8.2.2. テキストファイル(csv)を開いて内容を読み出す#

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

filename = "/content/drive/My Drive/Pro1AdDS/chapter8_data/python_handling_test.csv" 
inp = open(filename,"r")
lines = inp.readlines()
inp.close()
  • 1行目でファイルパスを指定しfilenameという変数(文字列)とした。

  • 2行目では、指定したパスにあるファイルを開いている。
    今はファイルに書き込むのではなく、既にあるファイルを開いて読み込むので"r"というオプションを指定している。
    他には"w"(書き出し,上書き), "a"(書き出し,追記)などがあり、新しく上書きでファイルを作成したい場合は"w",すでにあるファイルの内容は消さずに追記したい場合は"a"を指定してopenする。

  • 3行目では、inp(ファイルをopenして得たオブジェクト)に対してreadlinesというメソッドを適用している。 これは、ファイルに書かれているテキストを(可能なら)全行に渡って読み込みメモリにストアするメソッドになっている。

  • 4行目は、開いたファイルを閉じるための命令で、closeメソッドを適用している。
    特に重いファイルを扱う場合や、書き込んだファイルを続くプログラムで操作する場合などは、明示的にcloseを行うことが推奨される。

print(lines)

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

ループを回して一行ずつ表示させてみる。少し行数が多いので、最初の数行だけ表示させてみる。

for i, line in enumerate(lines):
    if i == 5:
        break
    print(line)

といった感じ。

行ごとにスペースが生じている理由については、ファイルの各行の末尾に改行が含まれているためである。 print関数はデフォルトで末尾に改行\nを挿入するのでファイルにある改行記号とあわせて2回改行してしまう。
参考リンク

必要な行番号が分かっている場合は、スライスなどを用いて必要な行だけを取り出すこともできる。

nlines = lines[:5]

nlines

8.2.3. 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())

表示が冗長にならないよう、上で作っておいた5行だけとった文字列を使いつつ、strip関数を適用してやると…

for i, line in enumerate(nlines):
    print(line.strip())

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

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

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

もちろんPythonではインデントが文法なので、インデントを一律で消す、といった操作は必要ないが、特定の状況では、lstripも使えると便利な場合がある。 例えばアンケートフォームなどで、誰かが入力した文字列を処理する際などは、その人が入力の前後に空白を入れてしまうこともあるので、stripを使って空白を消してから処理するのが無難である。

8.2.4. split関数#

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

sample_text = "This is a\nsample\ttext."
sample_text.split()

その他にもカンマで分割する、とか、特定の記号で分割するような操作もできる。

sample_text = "1, 2, 3, 4, 5"
sample_text.split(",")

さきほどのcsvファイルで試してみよう。何も指定しないと、区切りが悪いので

for line in nlines:
    print(line.split())

カンマで区切ってやると… 入力項目ごとにリストに分割されていることがわかる。

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

8.2.5. replace関数#

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

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

8.2.6. \(\clubsuit\) map関数#

ファイルから読み込んだデータは文字列として読み込まれるため、数値を扱う場合は、int関数などを使って整数型に変換する必要が生じる。

Pythonには、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)) )

もちろん、リスト内包表記を使っても同様のことができる。既に用意された機能を使うのはもちろんいいことだが、たまにはこうして愚直な操作をしてみるのも、Pythonの基本的な操作を理解する上で役に立つこともある。

tmp = ['1', ' 2', ' 3', ' 4', ' 5', ' 6']
print("リスト内包表記=>", [int(x) for x in tmp])    

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

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

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

次に、テキストファイルを書き込んで保存してみよう。

csvファイルの先頭5行を、上で紹介したreplaceなどを適用してみて、書き出すとする。

filename = "/content/drive/My Drive/Pro1AdDS/chapter8_data/write_practice.csv"
oup = open(filename,"w")  
for line in nlines:
    line = line.strip()  #stripで改行を除去
    line = line.replace("どちらか", "どっちか")   #置換
    print(line, file=oup) # lineをファイルに書き込む.
oup.close() #ファイルはきちんと閉じる.

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

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

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

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

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

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

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

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

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

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

import pandas as pd 
df = pd.read_csv("https://raw.githubusercontent.com/SotaYoshida/Lecture_DataScience/refs/heads/main/Chapter8_data/python_handling_test.csv" )
df

ただし、csvファイルのフリをしたいい加減なファイルの場合はエラーになることもあるので、独自にコードを書いて例外的な処理をすることも必要になる。 世の中には、空白やタブで区切られたテキストファイルに、.csvという拡張子を付与した極悪なファイルも存在する。

さて、csvから読み出したデータをもう少し扱いやすいように整形していこう。

ファイルをみると、最初の0行目はどういうデータが入っているか(データの項目)を表している。
1-2000行目には2000人分の回答が詰まっている。

これによると、

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

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

  • あえて、pandasは使わずにcsvファイルはカンマ区切りのテキストファイルと思って開き

  • 処理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

# 再度、csvファイルを読み出して、csv_linesという名前でリストに格納しておく
filename = "/content/drive/My Drive/Pro1AdDS/chapter8_data/python_handling_test.csv" 
inp = open(filename,"r")
csv_lines = inp.readlines()
inp.close()

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.95以上のもの(32通り)をプロットして画像として保存します。

pthreの値を小さくすると、生成される画像の数がとんでもなく増えるのでやらないでください。
(0.9 → 291通り, 0.8 → 1234通り, 0.7 → 2871通り, 0.6 → 5233通り, 0.5 → 8375通り, 0.0 → 32876通り)

# 画像がいっぱい生成されると面倒なので画像を保存するフォルダを作成しておく
!mkdir -p /content/drive/MyDrive/Pro1AdDS/chapter8_data/kakei_cor_pic
!pip install japanize_matplotlib 
import os
import time

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import japanize_matplotlib


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

        self.s_kakei = [i for i, sheetname in enumerate(self.sname) if "Sheet" in sheetname]
        self.s_kikou = [i for i, sheetname in enumerate(self.sname) if sheetname.startswith("S") and "Sheet" not in sheetname]

    def indices(self):
        return self.s_kakei, self.s_kikou

    def readkakei(self, ikakei):
        ws = self.input_file.parse(sheet_name=self.sname[ikakei])
        city_indices = []
        city_names = []
        values_kakei = []
        items = []
        header_found = False

        for row_index in range(ws.shape[0]):
            row = list(ws.iloc[row_index])

            if not header_found:
                hits = [col_index for col_index, value in enumerate(row) if isinstance(value, str) and "市" in value]
                if len(hits) > 5:
                    header_found = True
                    city_indices = hits
                    city_names = [row[col_index] for col_index in city_indices]
                continue

            if row_index < 22:
                continue

            if isinstance(row[8], str) and row[8] != "":
                values_kakei.append([row[col_index + 1] for col_index in city_indices])
                items.append(row[8])

        if not header_found:
            raise ValueError("家計データの都市ヘッダーが見つかりませんでした。")

        return city_indices, city_names, values_kakei, items

    def readkikou(self, ikikou):
        ws = self.input_file.parse(sheet_name=self.sname[ikikou], header=None)
        quantities = []
        values_kikou = []
        places = None
        header_found = False

        for row_index in range(ws.shape[0]):
            row = list(ws.iloc[row_index])

            if not header_found:
                if any(isinstance(value, str) and "市" in value for value in row):
                    places = row[1:]
                    header_found = True
                continue

            quantities.append(row[0])
            values_kikou.append(row[1:])

        if not header_found:
            raise ValueError("気候データの都市ヘッダーが見つかりませんでした。")

        return places, values_kikou, quantities


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


# 指定した品目・地点の散布図を描く関数
def plot_cor(x, y, item, quantity, place, corrcoef, output_dir):
    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 month_index in range(len(x)):
        tcol = seasoncolor(month_index + 1)
        ax.scatter(x[month_index], y[month_index], marker="o", s=5, color=tcol, zorder=20000, alpha=0.7)
        ax.text(x[month_index], y[month_index], str(month_index + 1) + "月", color="k", fontsize=8)

    output_path = os.path.join(output_dir, f"corr_{item}vs{quantity}_at_{place}.png")
    plt.savefig(output_path, dpi=300)
    plt.close(fig)


# 指定した品目・地点の相関を調べる関数
def calcor(places, items, vs_kakei, tplaces, quantities, vs_kikou, iT, pthre, printlog=False, pltmode=True, output_dir=None):
    hit = 0
    num_pic = 0
    correlations = []
    quantity = quantities[iT]

    for j_k, place in enumerate(places):
        for j_t, tplace in enumerate(tplaces):
            if place != tplace:
                continue

            for item_index, item in enumerate(items):
                kvalue = np.asarray([vs_kakei[month][item_index][j_k] for month in range(len(vs_kakei))], dtype=float)
                tvalue = np.asarray([vs_kikou[month][iT][j_t] for month in range(len(vs_kikou))], dtype=float)

                if np.allclose(tvalue, 0.0):
                    continue

                corrcoef = np.corrcoef(kvalue, tvalue)[0][1]
                correlations.append([corrcoef, item, quantity, place])

                if printlog:
                    print("@", place, " ", item, kvalue, " VS ", quantity, ",", tvalue)

                if abs(corrcoef) > pthre:
                    hit += 1
                    if pltmode:
                        if output_dir is None:
                            raise ValueError("pltmode=True のときは output_dir を指定してください。")
                        plot_cor(kvalue, tvalue, item, quantity, place, corrcoef, output_dir)
                        num_pic += 1

    print("hit:", hit, " number of picture", num_pic)
    return correlations


def main():
    ti = time.time()
    inpf = "/content/drive/MyDrive/Pro1AdDS/chapter8_data/kakei.xlsx"
    oupdir = "/content/drive/MyDrive/Pro1AdDS/chapter8_data/kakei_cor_pic/"
    iT = 6  # iT=6: 日平均気温
    printlog = False  # 条件にhitした都市の品目と気候データを逐次printするかどうか
    pthre = 0.95  # corrplotを描く相関係数の絶対値のthreshold(下限)
    pltmode = True  # T: plotする F: 計算のみ

    if pltmode:
        os.makedirs(oupdir, exist_ok=True)

    wb = ebook(inpf)
    s_kakei, s_kikou = wb.indices()
    vs_kakei = []
    vs_kikou = []

    for month_index, ind_kakei in enumerate(s_kakei):
        _, places, v_kakei, items = wb.readkakei(ind_kakei)
        tplaces, v_kikou, quantities = wb.readkikou(s_kikou[month_index])
        vs_kakei.append(v_kakei)
        vs_kikou.append(v_kikou)

    correlations = calcor(
        places,
        items,
        vs_kakei,
        tplaces,
        quantities,
        vs_kikou,
        iT=iT,
        pthre=pthre,
        printlog=printlog,
        pltmode=pltmode,
        output_dir=oupdir,
    )

    tf = time.time()
    print("Elapsed time[sec]:", tf - ti)
    return correlations


if __name__ == "__main__":
    correlations = main()

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バイト以上の文字はファイル(フォルダ)名に使わないのがコンピューターにとっては無難である。

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

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