BigQueryへのデータ読み込み_2.Cloud Storageにデータ追加されたら自動で読み込む

前の記事では、手動でBigQueryにデータを読み込む方法を試した。この記事では、データが新しく生成されたら自動で読み込みを行いたいケースを想定し、「Cloud StorageにCSVファイルが追加されたら、自動でBigQueryにデータを読み込む」ことを目標に進める。

概要

今回はCloud Functionsを使った。Cloud FunctionsはGCP上で関数を実行できるサービスで、関数を実行するトリガーの一つにCloud Storageでのイベントの発生がある。そのため、今回の「Cloud StorageにCSVファイルが追加されたら」もトリガーとして設定できる。
今回のイメージ図
今回、やりたいことを分解すると以下の2つが必要になる。
A. Cloud Storageにオブジェクトが追加されたことをトリガーとしてCloud Fuctionsの関数を実行する
B. Cloud Fuctionsの関数内で、BigQueryのデータにアクセスして操作を行う
今回、まずはA, Bそれぞれ単体で試して作り方を確認後、AとBを合わせて元々作りたかった関数を作成する手順で進めた。不慣れでA, B単体でもエラーが出たり色々とまどう部分があったので、それも含めて記載していく。
A. Cloud Storageにオブジェクトが追加されたことをトリガーとしてCloud Fuctionsの関数を実行する
B. Cloud Fuctionsの関数において、BigQueryのデータにアクセスして操作を行う
C. Cloud Storageにオブジェクトが追加されたらBigQueryにデータを読み込む

手順

A. Cloud Storageにオブジェクトが追加されたことをトリガーとしてCloud Fuctionsの関数を実行する

Cloud Storage のチュートリアル | Cloud Functions ドキュメントに、Cloud Storageでは下記4点のイベントに対応していると記載がある。
・ ファイナライズ
・ 削除
・ アーカイブ
・ メタデータの更新
このうち、ファイナライズについて
オブジェクト ファイナライズ イベントは、Cloud Storage オブジェクトの「書き込み」が正常にファイナライズされた時点でトリガーされます。つまり、新しいオブジェクトの作成または既存のオブジェクトの上書きによって、このイベントがトリガーされます。
とあるので、今回はファイナライズをトリガーとすればよいことがわかった。
同ドキュメント内でファイナライズのサンプル関数が掲載されているので、それを以下で実行してみた(ただし、ドキュメントはコマンドラインを用いた操作手順だったが、今回はCloud Consoleから操作した)。
① 関数を作成する(トリガーの指定)
1) 関数の作成をクリック
2) 関数名、リージョン、トリガーの内容等を入力
トリガーのEvent typeは先ほど記載したファイナライズ、バケットはCloud Fucntionsのオブジェクトのアップロード先バケットを指定する。
② 関数を作成する(関数の内容の記載)
チュートリアルに記載の内容のままmain.pyに記載する。
③ 関数が動作することを確認する
先ほど指定したCloud Storageのバケットに適当にテキストファイルをアップロードし、これをトリガーとして関数が実行されることを確認する。Cloud Functionsのログに、print関数の結果が吐き出されていることを確認した。

B. Cloud Fuctionsの関数において、BigQueryのデータにアクセスして操作を行う

クイックスタート: クライアント ライブラリの使用 | BigQuery ドキュメントにBigQueryのテーブルにクエリを投げてデータを取得する関数の事例が掲載されている。今回最終的に行いたいのはSelect クエリではなくデータのインサートだが、まずはこの事例を試してみる。
① クエリを実行する関数を作成する
A.と同様の手順で、記載の関数を少し変形してCloud Functionsにデプロイした。(※ このB.では、トリガーはHTTPリクエストとなっている)
main.py

from google.cloud import bigquery


def query_stackoverflow(request):
    client = bigquery.Client()
    query_job = client.query(
        """
        SELECT
          CONCAT(
            'https://stackoverflow.com/questions/',
            CAST(id as STRING)) as url,
          view_count
        FROM `bigquery-public-data.stackoverflow.posts_questions`
        WHERE tags like '%google-bigquery%'
        ORDER BY view_count DESC
        LIMIT 10"""
    )

    results = query_job.result()  # Waits for job to complete.

    for row in results:
        print("{} : {} views".format(row.url, row.view_count))
    return "finish!"
requirements.txt
google-cloud-bigquery>=1.28.0
② 関数をテストする
200が返ってきてprint関数が出力されていることが確認できる。
つまずいた点
上記手順で実行できるまでにつまずいた点を記載する。
1) requirements.txtの不足
requirements.txtを記載していなかったところ、以下のようなエラーが出た。
"/workspace/main.py", line 1, in  from google.cloud import bigquery ImportError: 
cannot import name 'bigquery' from 'google.cloud' (unknown location)
今回ローカルに開発環境を作らずCloud Consoleで行っていたので、予めインストールされるようなイメージを勝手に持ってしまっていたけど、requirements.txtへの記載が必要だった。
requirements.txtに google-cloud-bigquery>=1.28.0 を記載することで解決した。
2) 引数の不足
上記手順で def query_stackoverflow(request): としているところを当初 def query_stackoverflow(): として引数を入れていなかったらエラーとなった。ログには以下の記載があった。
TypeError: query_stackoverflow() takes 0 positional arguments but 1 was given
関数の内容上、引数は必要なかったため記載していなかったが、Cloud Functionsでトリガーにより実行する場合は暗黙的に引数が渡されるため関数で引数が必要なようだった(参考)(ドキュメントでの明確な言及は見つけられなかったが、トリガーにより実行する以上、引数が必要なのは当然ということなのかもしれない)。
関数の引数を追加することで解決した。
3) returnの不足
特に何も返さない関数にしていたところ、以下のエラーが返ってきた。
TypeError( TypeError: The view function did not return a valid response. 
The function either returned None or ended without a return statement.
関数の内容上、特に必要なかったが関数にreturnを追加することで解決した。

C. Cloud Storageにオブジェクトが追加されたらBigQueryにデータを読み込む

A, Bを試す中で今回やりたいことに必要な概要がつかめたので、最終的にやりたかった内容の関数を作る。
B.ではSelectクエリの実行だったけど、この部分をCSVデータの読み込みに置き換える必要がある。この部分はCloud Storage からの CSV データの読み込み | BigQuery ドキュメントを参照した。
① 関数をデプロイする
関数のデプロイ手順自体はA.と同様。
任意の名称のCSVファイルが追加されたら、BigQueryにデータの追加を行うようにした。
main.py

from google.cloud import bigquery


def append_data_into_bigquery(table_id, uri):
    client = bigquery.Client()

    job_config = bigquery.LoadJobConfig(
      autodetect=True,  # スキーマの自動検出
      write_disposition=bigquery.WriteDisposition.WRITE_APPEND,  # データの追加
      skip_leading_rows=1  # 冒頭1行は今回ヘッダ行なので読み飛ばし
    )

    load_job = client.load_table_from_uri(
      uri,
      table_id,
      job_config=job_config
    )
    load_job.result()

    table = client.get_table(table_id)
    print("Loaded {} rows to table {}".format(table.num_rows, table_id))


def append_weather_data_into_bigquery(event, context):
    if event['name'].endswith('weather_tokyo.csv') == True:
        project_id = 'learn-bigquery-327203'
        bq_dataset = 'level2_from_gcs'
        bq_table = 'weather_tokyo'
        table_id = project_id + '.' + bq_dataset + '.' + bq_table  # project.dataset.table_name
        uri = 'gs://' + event['bucket'] + '/' + event['name']  # gs://bucket_name/object_name_or_glob
        append_data_into_bigquery(table_id, uri)
requirements.txt
google-cloud-bigquery>=1.28.0
② 関数が実行されることを確認する
Cloud StorageにCSVファイルをアップロードしたことをトリガーとして、BigQueryにデータがインサートされるかを確認する。 Cloud Functionsのログでは関数の実行が完了した旨が出ている。
BigQueryでも、データが追加されていることが確認できた。

次:

今回、Cloud StorageにCSVデータが追加されたら自動でBigQueryにデータを読み込むことをやってみた。しかしながら、CSVは予め手動でBigQueryに受け入れられる形式に変換していた。この部分も自動化できそうなので、次回はその辺りもGCP上で行う方法を調べてみたい。

BigQueryへのデータ読み込み_1.まずは手動で直接入れてみる

漠然とGCP怖いな(気づかずに課金されそう、サービスが広大で理解しきれなさそうなど)と思っていたけど、ちょっとでも慣れようと色々触ってみることにした。
データを格納された状態からしかBigQueryに触ったことがなかったので、データを格納する各種手法を手を動かしてやってみる。

概要

この記事では、まず単純な以下の方法を試してみる。
A. BigQueryにローカルのCSVを読み込む
B. BigQueryにCloud StorageのCSVを読み込む

手順

※ Google Cloudのアカウントを作成して、プロジェクトを作成した状態からスタート
※ 操作はCloud Consoleを使用した

A. BigQueryにローカルのCSVを読み込む

① データセットを作成する
データセットの概要 | BigQueryドキュメントに、以下の記載がある。
データセットは、特定のプロジェクト内に含まれています。データセットは、テーブルとビューへのアクセスを整理して制御するために使用される最上位のコンテナです。テーブルまたはビューはデータセットに属していなければなりません。したがって、データを BigQuery に読み込む前に、1つ以上のデータセットを作成する必要があります。
プロジェクト/データセット/テーブルの関係
そのため、まずはデータセットを作る。
データセットの作成 | BigQueryドキュメントに作成時の注意点や作成手順が記載されているので、これを参照しながら作成した。
1) プロジェクトを選択した状態で「データセットを作成」を選択
2) データセット名とロケーションを入力
ロケーションはデータを保存するサーバの地理的な場所のことで、データセットのロケーション | BigQueryドキュメントに色々注意点が記載されている(データセット作成後にロケーションは変更することはできず、データの読み込み先と同じロケーションである必要がある, 料金もロケーションによって異なるなど)。今回はのちに利用するCloud StorageをUSにしていたため、デフォルトのUSマルチリージョンとした。 ② 読み込み用データの準備
今回は、気象庁HPからダウンロードした東京都の2021年1月の気象データを読み込み用データとした。
スキーマの指定 | BigQueryドキュメントCloud Storage からの CSV データの読み込み 制限事項 | BigQueryドキュメントに以下のような制限が記載されているので、それに合わせて整形する。
・ 列名には、英字(a-z、A-Z)、数字(0-9)、アンダースコア(_)のみを使用できる  
・ CSV データは UTF-8 でエンコードされている必要がある  
・ DATE 列の値に区切りとしてダッシュ(-)を使用し、YYYY-MM-DDの形式にする
今回は下記のようなCSVとなった。
date,ave_temp,max_temp,min_temp,max_wind_speed,wind_direction,weather
2021-01-01,4.4,10.5,-1.3,3.1,北北東,快晴
2021-01-02,4.8,10.8,0.1,4.7,北北東,快晴
 …
③ データを読み込む
テーブルを作成して、CSVデータを読み込んでいく。
1)データセットを選択した状態で「テーブルを作成」をクリック
2)ローカルからCSVをアップロードし、テーブル名等を入力 
スキーマは自動検出でやってみた。
3) 無事、テーブルにデータが読み込まれた。
自動検出としていたが、データの型も問題なく判定されていそう。

B. BigQueryにCloud StorageのCSVを読み込む

A.の方法から半歩進んで、GCPのストレージサービスであるCloud Storageに格納したCSVデータからBigQueryに読み込んでみる。
① Cloud StorageにCSVデータを入れる
まずは読み込み用CSVデータをCloud Storageにアップロードする。
1) Cloud Storageにバケットを作成する
バケットはオブジェクトの格納先(オブジェクトは格納する個々のデータのこと。今回だとCSVデータ)。Cloud Storageに保存するデータはすべてバケットに格納する必要があるので、まずはバケットを作成する必要がある。
ストレージ バケットの作成 | Cloud Storageドキュメントを参照しながら作成した。
データの保存場所については、データセットのロケーション | BigQuery ドキュメントに注意点の記載がある。BigQueryデータセットと読み込み元のCloud Storageバケットは同じロケーションとする必要がある。今回、BigQueryデータセットをUSマルチリージョンとしていたので、バケットをus-east1としてみた。
2) Cloud StorageにCSVデータをアップロードする
1)で作ったバケット内に、今回はフォルダを作成し、その中にローカルからCSVファイルをアップロードした。
② Cloud StorageからBigQueryに読み込む
基本的にA. のローカルからアップロードした場合と同じ手順でアップロードできる。ソース部分のみGCSに変えている。
無事、テーブルにデータが読み込まれた。

次:

今回、ローカルあるいはCloud Storageから手動でBigQueryにデータを読み込む方法を試した。単発の読み込みなら今回の方法でも良いのかもしれないが、データが新しく生成され、都度読み込みを行いたいケースも多そうだ。次回はその場合の読み込みを試してみる。
BigQueryへのデータ読み込み_2.Cloud Storageにデータ追加されたら自動で読み込む

Notionで学習管理をしてみた半年のふりかえり

Notionを利用して、自分が学びたいことの進捗管理を半年間やってみた。
具体的にどうやったか、そこから気づいた改善点についてまとめる。

課題認識

今まで、なんとなく「これ勉強しないと」「こんなの作ってみよう」と脳内で考えて漠然と進めてきた。その時々でtodoを書き出してはいたけど、長期的な視点では整理できていなかったため、場当たり的な進捗になりがちだった。 この脳内管理には以下のようなデメリットもある。
・優先順位の整理が曖昧になった結果、結局どれも手をつけない
・常に「あれもしなきゃ」が脳の一部分を占領することになる
・進捗が可視化されていないので進んでいる実感が持ちづらい デメリットが多いことは承知しつつも、改善が面倒で「まあ仕事じゃないしね」という言い訳を盾に放置していた。重い腰を上げ、2020上期はNotionで管理する運用を試してみたので、ふりかえってみる。

具体的な管理方法

上記の課題(デメリット)を解決するには、以下の情報が整理されれば良いと考えた。
① 学びたい分野として何があって
② その分野ごとに取り組みたいことに何があり、いつ着手する予定で
③ 各取り組み内容に対して今日やるタスクは何か これを整理するために、3つのテーブルを親 – 子 – 孫の関係でリンクさせて管理するようにした。
重複レコードが許されたり正規化されていなかったりするので、イメージ図

① 親:Fieldテーブル

自分が学びを深めていきたい分野のテーブル
・例えば、統計 / マーケティング / 機械学習 / …などのような大きな粒度
 ※ 専門分野を深めていく場合は、その分野の中でさらに分化した分野という粒度になる

② 子:Projectテーブル

Fieldテーブルの各分野に対して、具体的に取り組む内容(以下「プロジェクト」と呼ぶ)の各種情報をまとめたテーブル
具体的なやりたいこと、優先度、取り組む時期などを管理する。
・○○という本を読む / ○○を写経する / ○○を作ってみる …という粒度

③ 孫:Todoテーブル

実際に取り組める粒度まで分解したやるべきこと(以下「タスク」と呼ぶ)を管理するテーブル
・1章を読む / ○○について調べる …という粒度

運用の仕方

以下1 ~ 3の整理を随時実施して時々見直しつつ、4の整理は日々行う形で運用した。

1. 学ぶ内容(プロジェクト)を追加する

各分野に対して、今取り組んでいる/今後取り組む予定のプロジェクトとして何があったかを眺めて、この分野はこれもやりたいというものを追加する。
この時、そのプロジェクトはどこまで取り組めたら完了とみなすかゴールを決めておくのが重要だった(「○○をやる」だけだと、完了の判定がしづらいため)。ICEスコアをつけて優先度を決め、取り組む時期を期程度の大きな単位で決めておく。(※ 途中からICEスコアはやめた 後述)

2. 学ぶ内容(プロジェクト)に取り組む時期を決める

進行中のプロジェクトの実施時期と進捗を見つつ、取り組む時期を決める。
Timelineビューだと、複数のプロジェクトの重なりが見えるので時期を決めやすい

3. プロジェクトをタスクに落とす

各プロジェクトのゴールにたどり着くまでにやるべきことは何か、タスクとして書き出す。

4. タスクに取り組む日を決める

Timelineビューで動かして、各タスクの実施日を調整する。実施日は大体で入れておいて、日々状況に合わせ気軽に動かしていた。
Timelineビューだと、複数のタスクの重なりが見えるので調整しやすい

ふりかえり

よかったこと

「いつかあれやらなきゃな(いつかは来ない)」が多少軽減された
取り組みたい内容を登録しておくことで、次に取り組む内容を決める時に選択肢の一つとして検討することになる。
後から見た時、何をやったのか、どのくらいかかったのかがわかる
後からふりかえると「何もできなかった」という気持ちになることがあるけど、記録が残っていることで、ちゃんと進捗はしているのだ(歩みは遅くても)とわかる。
「何もできなかった」が本当に何にも取り組まなかったから正しい認識なのか、あるいは何か具体的に取り組んだけど思ったより進まなかったからそう感じているのかが判断できる。
1回仕組みを作れば、回していくのに手間はさほどかからない
よくこんな面倒なことやってるなと見えるだろうが、一度仕組みができてしまえば見た目ほど面倒ではなかった。

よくなかったこと/反省点

ICEスコアによる優先度決定はあまりフィットしなかった
当初はICEスコア () をつけてスコアが高いものから取り組んでいた。これだとやるべきことばかりが優先度上位に上がり、徐々に学ぶことの苦痛が大きくなっていった。
そこで、途中からはICEスコアによる判断をやめ、やりたいことも並行して取り組むようにした。結果、同時並行でいくつものプロジェクトが進むことになり、脳の切り替えが大変になった。
ICEスコアでなくても、取り組む優先度を決めるための何らかのルールは必要だった。やりたいこととやるべきこと、それぞれ1〜2個ずつしか同時には取り組まないというルールにしてみようと思っている。
タスクの割り方をもう少し工夫したい
例えば技術書を読むというプロジェクトの場合、単純に1章ごとにタスクとして登録していた。実際のところ、分量や難易度によって要する時間は異なる。そのため、複数週にわたって取り組んでも完了できないタスクが出てきた。
進捗が見えづらくなるしやる気も低下するので、もう少し実際の中身を考慮してタスクを割るように変える。
焦燥感はさほど減らなかった
脳内でタスクを管理していた時は、常に「あれもしなきゃ」という焦燥感があった。今回Notionで管理することで見える化され、それが減るのではと思っていたが、さほど減らなかった。これは、やるべきことを整理できていないことが要因の焦燥でなかった、あるいは見える化しても自分のタスクを進める速度は変わらないことあたりが要因かもしれない。

具体的な設定の仕方

似たことをやってみようという方がもしもいた時のため、直感的にわかりづらい箇所の作成手順を簡単に記載しておく。

テーブル同士の関連づけの仕方

紐づく親テーブルのTagsを子テーブルで表示したい場合を例とする。 1. 親テーブルと紐づける
子テーブルで、①プロパティがRelationのカラムを追加し、②親のテーブルを選択する。
③フィールド部分をクリックすると親テーブルのカラム一覧が出るので、紐付けたい親のレコードを選択する。 2. 紐付けた親の任意のカラムを子側で表示する
①子側のテーブルで、プロパティがRollupのカラムを追加する。②フィールド部分をクリックすると各種設定が出てくるので、RELATIONを親テーブル、PROPERTYを表示したい親のカラムにする。
※ 書いておいてなんだけど、公式のhelpページがわかりやすいし詳しい

進捗率のバーの出し方

今回の例だと、そのプロジェクトに紐づくタスクのうち完了した割合を進捗バーとして表示するようにしている。
具体的な設定の仕方については、35D BLOG | Notion でプロジェクトの進捗を可視化する(Formula 機能の使い方)を参照させていただいた。

脚注 (※)
ICEスコア:複数の着手すべき事案がある時に、優先すべきものを順序づける方法
ICEは影響力(Impact) / 信頼度(Confidence) / 容易性(Ease)の頭文字で、この3指標の掛け合わせが大きいものから着手する

OpenCVを利用した矩形検出の試行錯誤_エッジ検出・適応的閾値処理

前記事で示したOpenCVを用いた矩形検出の改善案のうち、ここでは下記2案の内容について記載する。
改善案3 エッジ検出を使う
改善案4 適応的閾値処理を使う

環境

Python 3.7.8
OpenCV-Python 4.5.1.48

改善案3 エッジ検出を使う

案の概要

画像の輝度が急激に変化している箇所をエッジ(今回だと輪郭)として捉える処理を行い、エッジのみになった画像に対して輪郭検知を行うという案。
エッジとみなすのは周辺と比べ値が急激に変化している箇所なので、微分した値が大きい箇所をエッジとみなす形で処理が行われる。調べると、エッジ検出としてはSobelフィルタ、Laplacianフィルタ、Cannyフィルタなどがよく使われているようだったが、ここではCannyフィルタを利用した場合の結果を示す。

案の実践

エッジ検出した結果に対して輪郭の検知を行った。 一見うまくいくように見えるが、実際にはその後矩形のみに絞る処理がうまくいかず、1枚もレシートを検出できなかった。これは、レシートの外形を輪郭が囲っているように見えるがそれらはひとつながりの線になっておらず、別々の矩形と捉えられているからと思われる。
エッジ検出の拡大

そこで輪郭の検知前にノイズ処理(ここではモルフォロジー変換)を追加し、検出したエッジを単純化(膨張処理)することにした。 概ねレシートの輪郭を捉えられているように見えるが、一部途切れているなどしていて、その後レシートの矩形のみに絞る処理をすると1枚のレシートしか検出できなかった。
そこで、エッジの膨張させる程度をもう少し強くしてみる。 これだと3枚ともレシートを検出できた。
検出結果を見ると、レシートを囲む2重線のうち、内側のみ矩形と判断されていた。これは、外側のレシートを囲む線は背景のノイズを拾った線とつながるなどしていて、矩形と判定されなかったことによる。
一方、内側のレシートを囲む線はレシートの印字の輪郭線とつながっていないため、矩形と判断できた。今回の膨張の程度だと問題ないが、もう少し大きくすると今度はレシートの印字の検知の範囲とレシートを囲む内側の線がくっついてしまい、うまく矩形検出されなくなることが予想される。
輪郭検知の拡大

※ モルフォロジー変換を膨張でなくクロージング(エッジをいったん膨張させて他の細かいエッジと結合させた後、膨張を戻す)にすれば上記の問題が解決できるのではと考え試してみた。しかし、以下の結果となり、最終的にレシートの矩形はうまくいかなかった。

案の評価

レシートの矩形検出自体は行えるが、上記のようにモルフォロジー変換の強さを各画像に合わせ調整する必要がありそうなので、任意の画像に対応するのは難しいだろう。

コード

案3の一連の処理を行う際に書いたコードを以下に示す。

※ 案3に特有の部分
エッジ処理(Canny法)は cv2.Canny で行っている。
Pythonbinary_img = cv2.Canny(gray_img, 100, 200) # 100はminVal、200はmaxVal
# 画素値の微分値が maxVal 以上であればエッジとみなす  
# 画素値の微分値が minVal 以下であればエッジではないとみなし除外する
# 画素値の微分値が二つの閾値の間の場合、エッジと区別された画素(maxVal以上)につながっていればエッジとみなし,そうでなければエッジではないとみなし除外する
モルフォロジー変換(膨張)は cv2.dilate で行っている。
kernel = np.ones((30,30), np.uint8) # 処理の際参照する領域のサイズ
dilation = cv2.dilate(img, kernel, iterations=1) # iterationsは処理回数

一連の処理を行い、検出結果を画像で返すところまでのコード
import cv2
import numpy as np
from matplotlib import pyplot as plt


def binarize(img):
    """画像を2値化する
    """
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    binary_img = cv2.Canny(gray_img, 100, 200)
    plot_img(binary_img, 'binary_img')
    binary_img = binary_img.astype('uint8')
    return binary_img


def noise_reduction(img):
    """ノイズ処理(膨張)を行う
    """
    kernel = np.ones((30,30), np.uint8)
    dilation = cv2.dilate(img, kernel, iterations=1)
    plot_img(dilation, 'dilation')
    return dilation


def find_contours(img):
    """輪郭の一覧を得る
    """
    contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    return contours


def approximate_contours(img, contours):
    """輪郭を条件で絞り込んで矩形のみにする
    """
    height, width, _ = img.shape
    img_size = height * width
    approx_contours = []
    for i, cnt in enumerate(contours):
        arclen = cv2.arcLength(cnt, True)
        area = cv2.contourArea(cnt)
        if arclen != 0 and img_size*0.02 < area < img_size*0.9:
            approx_contour = cv2.approxPolyDP(cnt, epsilon=0.01*arclen, closed=True)
            if len(approx_contour) == 4:
                approx_contours.append(approx_contour)
    return approx_contours


def draw_contours(img, contours, file_name):
    """輪郭を画像に書き込む
    """
    draw_contours_file = cv2.drawContours(img.copy(), contours, -1, (0, 0, 255, 255), 10)
    plot_img(draw_contours_file, file_name)


def plot_img(img, file_name):
    """画像の書き出し
    """
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.title(file_name)
    plt.show()
    cv2.imwrite('./{}.png'.format(file_name), img)


def get_receipt_contours(img):
    """矩形検出までの一連の処理を行う
    """
    binary_img = binarize(img)
    noise_reduction_binary_img = noise_reduction(binary_img)
    contours = find_contours(noise_reduction_binary_img)
    approx_contours = approximate_contours(img, contours)
    draw_contours(img, contours, 'draw_all_contours')
    draw_contours(img, approx_contours, 'draw_rectangle_contours')


input_file = cv2.imread('/path/to/example.jpg')
get_receipt_contours(input_file)

改善案4 適応的閾値処理を使う

案の概要

前記事で示した現在の検出手順では、画像全体の画素値を対象として計算し2値化の閾値を決めていた。適応的閾値処理は画像全体ではなく、画像中の小領域ごとに閾値を計算する方法になる。そのため、領域ごとに光源環境が変わるような画像であっても限られた領域内の画素を対象とすることで、画像全体を対象とした場合よりも良い結果が得られる。

案の実践

OpenCVで用意されている適応的閾値処理の関数として cv2.adaptiveThreshold がある。以下の2引数を動かして変化を確認した。
・Block Size: 閾値計算時に対象にする小領域の大きさ(奇数とする)
・C: 計算された閾値から引く定数

Block Sizeを動かす(Cは2に固定)
Cを動かす(Block Sizeは101に固定)
上記結果をみて、Bloce Size=255、C=2でやってみた。 レシート3枚拾えているが、余計な背景も矩形として拾ってしまっている。
輪郭検知の段階でかなりノイズが多いことが要因と考え、ノイズ処理(中央値フィルタ)を追加してみた。 今度はレシートのみ過不足なく拾えている。

案の評価

上記のように結果を見ながら閾値を調整したので、画像によってはレシートの矩形検出がうまく行えない恐れがある。

コード

案4の一連の処理を行う際に書いたコードを以下に示す。

※ 案4に特有の部分
適応的閾値処理の関数として cv2.adaptiveThreshold を利用している。
binary_img = cv2.adaptiveThreshold(gray_img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 255, 2)
# 第2引数:輝度値の最大値(今回255)
# 第3引数:閾値計算の方法(今回のADAPTIVE_THRESHOLD_GAUSSIAN_Cだと小領域で閾値を計算する方法にガウス分布による重み付けをした平均値を使うことになる)
# 第4引数:閾値処理の種類(今回のTHRESH_BINARYだと閾値より小さい範囲は黒大きい範囲は白に変換する)
# 第5引数:閾値計算時に対象にする小領域の大きさ(今回255としている)
# 第6引数:計算された閾値から引く定数
ノイズ処理(中央値フィルタ)として cv2.medianBlur を利用している。
median = cv2.medianBlur(img, 9) # 9はカーネルサイズ(中央値を計算する対象とする範囲)
一連の処理を行い、検出結果を画像で返すところまでのコード
import cv2
import numpy as np
from matplotlib import pyplot as plt


def binarize(img):
    """画像を2値化する
    """
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    binary_img = cv2.adaptiveThreshold(gray_img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 255, 2)
    plot_img(binary_img, 'binary_img')
    return binary_img


def noise_reduction(img):
    """ノイズ処理(中央値フィルタ)を行う
    """
    median = cv2.medianBlur(img, 9)
    plot_img(median, 'median')
    return median


def find_contours(img):
    """輪郭の一覧を得る
    """
    contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    return contours


def approximate_contours(img, contours):
    """輪郭を条件で絞り込んで矩形のみにする
    """
    height, width, _ = img.shape
    img_size = height * width
    approx_contours = []
    for i, cnt in enumerate(contours):
        arclen = cv2.arcLength(cnt, True)
        area = cv2.contourArea(cnt)
        if arclen != 0 and img_size*0.02 < area < img_size*0.9:
            approx_contour = cv2.approxPolyDP(cnt, epsilon=0.05*arclen, closed=True)
            if len(approx_contour) == 4:
                approx_contours.append(approx_contour)
    return approx_contours


def draw_contours(img, contours, file_name):
    """輪郭を画像に書き込む
    """
    draw_contours_file = cv2.drawContours(img.copy(), contours, -1, (0, 0, 255, 255), 10)
    plot_img(draw_contours_file, file_name)


def plot_img(img, file_name):
    """画像の書き出し
    """
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.title(file_name)
    plt.show()
    cv2.imwrite('./{}.png'.format(file_name), img)


def get_receipt_contours(img):
    """矩形検出までの一連の処理を行う
    """
    binary_img = binarize(img)
    binary_img = noise_reduction(binary_img)
    contours = find_contours(binary_img)
    approx_contours = approximate_contours(img, contours)
    draw_contours(img, contours, 'draw_all_contours')
    draw_contours(img, approx_contours, 'draw_rectangle_contours')


input_file = cv2.imread('/path/to/example.jpg')
get_receipt_contours(input_file)

OpenCVを利用した矩形検出の試行錯誤_減色・色空間の変更

前記事で示したOpenCVを用いた矩形検出の改善案のうち、ここでは下記2案の内容について記載する。レシートを白・背景を黒に2値化できるような変換を目指す。
改善案1 減色する
改善案2 HSV色空間にする

環境

Python 3.7.8
OpenCV-Python 4.5.1.48

改善案1 減色する


案の概要

前記事で示した現在の検出手順では、2値化に大津の2値化を利用している。大津の2値化は画素値の分散を用いて閾値が決められるため、画素値をヒストグラムで表した時に双峰性を持つような分布になる画像だとうまく閾値を決めることができる(と理解している)。
今回の画像のヒストグラムを見てみると、山がいくつもある。また、大津の2値化による閾値は107だったが、背景で白飛びしている箇所(画像右下)が閾値より右(白色側)に入っているためうまくレシートと背景を2値化で分離できていない状態になっている。
今回の画像の画素値のヒストグラム

単純に考えると、2値化する際に背景の机が黒と判定されれば(ヒストグラムで背景が白色側の山に入らず、黒色側の山に入るように調整できれば)うまくいくはず。
単純化すれば扱う画像は以下の3色で構成されている。
・レシート:白
・背景:任意の単一色(グレースケールだと灰)
・レシートの印字:黒
この3色に減色できている画像をグレースケールに変換すれば、ヒストグラムの山が3つになる。その状態で2値化し、レシート(白)と背景(灰)の間で閾値が引かれればうまくいくという案となる(仮に期待通りに減色できたとしても、背景(灰)と印字(黒)の間で閾値が引かれてしまうという問題は残っているが、背景が1色になれば少なくとも背景が白黒両方に分布することはなくなるはず)。

案の実践

減色はk-meansを用いて行うことができる。(考えてみれば当然だけど、色は3つの数字の組で表されているので、3次元空間でクラスタリングするのと同じことだった)
概要に記載した3色に減色を試してみると以下のようになった。 そう思い通りにはいかず、背景が複数色に分かれ、かつ背景の一部はレシートと同色になる結果だった。
減色後のヒストグラムを確認すると、背景は1色にならず3色いずれにも分布している状態だった。
3色に減色した場合のヒストグラム
レシートと背景が同化しないよう、色数(クラスタリング数)を少し増やして5色にすると以下の結果だった。 5色に増やすと、3色の時にはレシートと同色に分類された画像右下部分に関してもレシートと別の色に分けられている。しかし、閾値が期待した位置で引けていないため、結局2値化した際にレシートと背景の一部が同化している。 そこで、2値化の閾値を人間が与える形に変えてみる。何色に減色したかによるが、x色に減色したうちレシートは白側上位1色か2色に属することが多いだろうから、2値化の閾値を白側から2色と3色の間の位置とするルールにしてみた。
※ 以下は7色に減色し、閾値は白側から2色と3色の間に引いた場合 これだと、一応レシートが全て無事検出できた。

案の評価

「レシートは減色したx色のうち白側上位1色か2色になる」という仮定の元、閾値を任意で定めている。そのため、レシートが白側上位3色になっている場合、あるいは白側上位1、2位に背景も含まれてしまっている場合、この案は役に立たなくなる。特に背景が白色系だと背景とレシートをうまく分離できないだろう。

コード

案1の一連の処理を行う際に書いたコードを以下に示す。

※ 案1に特有の部分
 減色は cv2.kmeans を利用している。
pixels = img.reshape(-1, 3).astype(np.float32) # 画像の変換(np.float32型で渡す必要がある)
criteria = cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 10, 1.0 # 繰り返しの終了条件 これだと精度が1に達するor繰り返し10回いずれかに達したら終了する
attempts = 10 # k-meansの初期値の試行回数
flags = cv2.KMEANS_RANDOM_CENTERS # k-meansの重心の初期値の決め方
_, labels, centers = cv2.kmeans(pixels, K, None, criteria, attempts, flags)
一連の処理を行い、検出結果を画像で返すところまでのコード
import cv2
import numpy as np
from matplotlib import pyplot as plt


def sub_color(img, K):
    """色数を指定して減色する
    """
    pixels = img.reshape(-1, 3).astype(np.float32)
    criteria = cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 10, 1.0
    attempts = 10
    flags = cv2.KMEANS_RANDOM_CENTERS
    _, labels, centers = cv2.kmeans(pixels, K, None, criteria, attempts, flags)
    sub_color_img = centers[labels].reshape(img.shape).astype(np.uint8)
    plot_img(sub_color_img, 'sub_color_img')
    return sub_color_img


def plot_histgram(img):
    """画像の画素値の分布をヒストグラムにする
    """
    hist = cv2.calcHist([img], [0], None, [256], [0,256])
    plt.bar([i for i in range(0,256)], hist.ravel())
    plt.show()


def binarize(img):
    """画像を2値化する
    """
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    plot_img(gray_img, 'gray_img')
    threshold = np.unique(np.array(gray_img).ravel())[-2] -1  # 白側から2色と3色の間の位置を閾値とする
    _, binary_img = cv2.threshold(gray_img, threshold, 255, cv2.THRESH_BINARY)
    plot_img(binary_img, 'binary_img')
    return gray_img, binary_img


def find_contours(img):
    """輪郭の一覧を得る
    """
    contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    return contours


def approximate_contours(img, contours):
    """輪郭を条件で絞り込んで矩形のみにする
    """
    height, width, _ = img.shape
    img_size = height * width
    approx_contours = []
    for i, cnt in enumerate(contours):
        arclen = cv2.arcLength(cnt, True)
        area = cv2.contourArea(cnt)
        if arclen != 0 and img_size*0.02 < area < img_size*0.9:
            approx_contour = cv2.approxPolyDP(cnt, epsilon=0.01*arclen, closed=True)
            if len(approx_contour) == 4:
                approx_contours.append(approx_contour)
    return approx_contours


def draw_contours(img, contours, file_name):
    """輪郭を画像に書き込む
    """
    draw_contours_file = cv2.drawContours(img.copy(), contours, -1, (0, 0, 255, 255), 10)
    plot_img(draw_contours_file, file_name)


def plot_img(img, file_name):
    """画像の書き出し
    """
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.title(file_name)
    plt.show()
    cv2.imwrite('./{}.png'.format(file_name), img)


def get_receipt_contours(img, K):
    """矩形検出までの一連の処理を引数の色数で行う
    """
    sub_color_img = sub_color(img, K)
    gray_img, binary_img = binarize(sub_color_img)
    contours = find_contours(binary_img)
    approx_contours = approximate_contours(img, contours)
    draw_contours(img, contours, 'draw_all_contours')
    draw_contours(img, approx_contours, 'draw_rectangle_contours')
    plot_histgram(gray_img)


# 7色に減色して矩形検出を試す
input_file = cv2.imread('/path/to/example.jpg')
get_receipt_contours(input_file, 7)

改善案2 HSV色空間にする

案の概要

ほとんどのレシートは白色なので、色を条件として背景と分離できないかと考えた(背景も白色系だと使えなくなるが)。

色を条件とした検出の場合、RGB色空間よりHSV色空間を使ったほうが検出しやすいらしい。
・RGBは、赤(R)・緑(G)・青(B)の各要素がどれだけ含まれているか(3つの色の混色の割合)で表される。同一の色でも明度や彩度の違いによってRGB3つのパラメータが変動し、範囲を指定しづらい。
・HSVは、色相(H)・彩度(S)・明度(V)のパラメータを使って表される。色相(色合い)を単独で指定できる(= 特定の色を指定しやすい)。

白色はHSV色空間だと色相は関係なくなる(0°~360°全て)ので、その長所が生かせないような気もするが、色相:制限なし / 彩度:小さめ / 明度:大きめという範囲を条件として処理してみる。

案の実践

以下の手順で処理を行う。
1. 画像をHSV色空間に変換する
2. レシートの白色とみなす範囲をHSVで指定して、それ以外はマスク(黒に変換)する
3. 2値化できた状態になるので、輪郭の検出を行う
2値化が期待した形でできているので、3枚とも検出できている。

案の評価

任意の画像だと、背景色やレシートの白色度合いによってはレシートの色(白色)とみなす指定範囲を調整する必要がある。今回の画像では偶然うまくいったが、レシートの色(白色)とみなす範囲からレシートの一部が外れた場合、その箇所は黒に分類されるため矩形検出がうまくいかなくなる。

コード

案2の一連の処理を行う際に書いたコードを以下に示す。

※ 案2に特有の部分
HSV色空間への変換後、白色部分のみにするマスク処理は cv2.inRange で行っている。
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # HSV色空間への変換
lower_white = np.array([0,0,100]) # 白色とみなすHSVの各値の下限
upper_white = np.array([180,25,255]) # 白色とみなすHSVの各値の上限
binary_img = cv2.inRange(hsv_img, lower_white, upper_white) # 上限と下限を指定してマスク処理する
一連の処理を行い、検出結果を画像で返すところまでのコード
import cv2
import numpy as np
from matplotlib import pyplot as plt


def binarize(img):
    """画像を2値化する
    """
    hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    lower_white = np.array([0,0,100])  # 白色とみなすHSVの各値の下限
    upper_white = np.array([180,25,255])  # 白色とみなすHSVの各値の上限
    binary_img = cv2.inRange(hsv_img, lower_white, upper_white)
    plot_img(binary_img, 'binary_img')
    return binary_img


def find_contours(img):
    """輪郭の一覧を得る
    """
    contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    return contours


def approximate_contours(img, contours):
    """輪郭を条件で絞り込んで矩形のみにする
    """
    height, width, _ = img.shape
    img_size = height * width
    approx_contours = []
    for i, cnt in enumerate(contours):
        arclen = cv2.arcLength(cnt, True)
        area = cv2.contourArea(cnt)
        if arclen != 0 and img_size*0.02 < area < img_size*0.9:
            approx_contour = cv2.approxPolyDP(cnt, epsilon=0.01*arclen, closed=True)
            if len(approx_contour) == 4:
                approx_contours.append(approx_contour)
    return approx_contours


def draw_contours(img, contours, file_name):
    """輪郭を画像に書き込む
    """
    draw_contours_file = cv2.drawContours(img.copy(), contours, -1, (0, 0, 255, 255), 10)
    plot_img(draw_contours_file, file_name)


def plot_img(img, file_name):
    """画像の書き出し
    """
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.title(file_name)
    plt.show()
    cv2.imwrite('./{}.png'.format(file_name), img)


def get_receipt_contours(img):
    """矩形検出までの一連の処理を行う
    """
    binary_img = binarize(img)
    contours = find_contours(binary_img)
    approx_contours = approximate_contours(img, contours)
    draw_contours(img, contours, 'draw_all_contours')
    draw_contours(img, approx_contours, 'draw_rectangle_contours')


input_file = cv2.imread('/path/to/example.jpg')
get_receipt_contours(input_file)

OpenCVを利用した矩形検出の試行錯誤

以前作ったレシートのOCRアプリを改善するため、OpenCVを利用した矩形検出について試行錯誤を行った。

環境

Python 3.7.8
OpenCV-Python 4.5.1.48

課題設定

レシートOCRアプリにおいて、一部のレシートが正しく検出されないことがそれなりの頻度で発生している(人間の目で見る分には背景との差異がある程度あるように思える場合にも)。レシートの矩形を過不足なく検出できるようにしたい。
レシートの検出結果(赤枠が検出できたレシート)

現在の検出手順

どこに改善点があるか調べるため、現在の処理手順を順に追ってみる。
[ 手順 ]
1. 読み込んだ画像をグレースケールに変換
2. 白と黒の2値化(大津の2値化を利用)
3. 輪郭の検知
4. 検知した輪郭を条件で取捨(内の面積が一定以上ある輪郭に絞った上で、輪郭の形状を近似し、頂点が4点の輪郭のみ選択) 手順2.の2値化の段階でレシートの地が白、背景とレシート印字が黒になるイメージだったが、背景の一部も白になってしまっている。そのため、レシートと背景の境界が一部消え、手順3.で輪郭が検知されない結果となっている。

改善ポイント

2値化の段階で、レシートと背景をうまく分離できれば、その後の輪郭の検知等はうまくいくと思われる。そのため、レシートを白・背景を黒に変換する精度を上げることを目指し、2値化の前処理について下記のような複数の手法を試してみた。
改善案1 減色する
改善案2 HSV色空間にする
改善案3 エッジ検出を使う
改善案4 適応的閾値処理を使う
改善案5 ハフ変換を使う ※ レシート外形を検知できなかったので省略

先に結果

上記改善案1~4を試した結果を先に示しておく。
詳細は別記事に記載しているが、この画像を元にパラメータを調整したのでどの手法でも全てのレシートが検出できている。
どの程度汎用性があるか見るため、異なる条件を付加した2パターンの画像で試してみた。
まず、背景の明るさの変化が大きい画像で試した。
背景の明るさの変化というより、各手法の中で指定しているパラメータの範囲外になることで検出できない部分が生じている。
次に部分的に影がかかっている画像で試した。
影の影響を受けやすい案1、2において、検出できないレシートが生じている。

各改善案の詳細

各改善案の詳細・コードは下記に分割して記載した。
OpenCVを利用した矩形検出の試行錯誤_減色・色空間の変更
改善案1 減色する
改善案2 HSV色空間にする
OpenCVを利用した矩形検出の試行錯誤_エッジ検出・適応的閾値処理
改善案3 エッジ検出を使う
改善案4 適応的閾値処理を使う

まとめ

今回試した3つの画像に限ると、適応的閾値処理が一番検出率が高い結果となった。しかし、この方法も万能ではなく、設定したパラメータから外れる画像に対しては検出できない結果となる。
より良い手法や調整の仕方がありそうだが、今の知識でわかるのはここまでなので、いったんレシートOCRアプリを適応的閾値処理を利用する形で修正を行った。

大人こそ自由研究をしよう

これはGMOペパボ ディレクター Advent Calendar 2020の18日目の記事です。


自由研究が好きだ。

 

自由研究は子どもの特権ではない。むしろ、大人になってからの方が基礎知識や考える力、経済力がついている分、研究できる範囲は広がっている。

 

これは、大人になってから行う自由研究は楽しいよ、みんなやろう、そして研究結果を教えてほしい、あわよくば自分の結果も見てほしい、という趣旨のエントリーである。

自由研究の楽しいところ

やりたいことが無限に存在する中で、あえて自由研究に時間を注ぎ込むのは、一言でいえば(苦しくも)楽しいからである。
何が楽しいかというと、「自分が知りたかったこと」かつ「まだ誰も知らないこと」を自分が動いたことで最初に知ることができるのである。興奮しますね。

テーマの選び方

自由研究をするには、まずテーマを決める必要がある。
テーマは大きなものや人類全般に役立つものでなく、自分が本当に感じたことから発想したものの方がよい。

ある一定期間そのテーマを考え続けることになるので、人の役に立つことよりも自分が本当に知りたいことの方が、モチベーションが維持されやすいからである。

私の場合は、日常の中で感じたことや課題をメモしておいて、しばらく経ってもまだやりたいと思えるものの中から選定している。

テーマのアイデア出し
Trello にどんどん追加していく(後から見るとなんのことかわからないものも多い)

いかにくだらない(しかし自分には重要な)内容をテーマに据えてきたかをイメージしてもらうため、過去に取り組んだテーマとそのきっかけを記載してみる。

    • 精神や体調は、何によって決まるのか(2018)
      前職で毎日ぎりぎりの闘いをしていた中で、切実に「風邪をひきたくない」「少しでも心健やかにいたい」と感じたことがきっかけ。活動量や食事内容、睡眠時間等の様々な値を記録し、それと体調等の関係性を検証した。
    •  なるべく日陰を通るには、どのルートを選ぶべきか(2019)
      2018夏、初めて福岡で迎える夏があまりに暑く「もしやこれから毎年この暑さを乗り切らねばならぬのか」と恐れおののいたことがきっかけ。出発点と到着点を指定した時、どのルートを通るとどの程度日陰になるか計算を行った。(なお、2018夏は特異な年で、翌年以降はそれほど暑くなかった)
    • レシートの家計簿入力を少しでも楽したい(2020)
      スプレッドシートに手入力していたが、「外税か内税か軽減税率適用商品か目で見て判断するのほんとめんどい」「同じ品目名何回も入力するの人生の無駄だな」と思ったのがきっかけ。レシートをOCRした後、結果を調整してCSVに吐き出す仕組みを作った。

自由研究の進め方

取り組むテーマが決まったら、いざ研究に手をつけていく。
ここでは、上記で事例に挙げた「精神や体調は、何によって決まるのか」を例に、私なりの自由研究の進め方を示していく。

 

1.   すでに調査・実現している先達がいないか確認する
「体調に最も影響をもたらすファクターは○○!」みたいな結論を誰かが出しているなら、そのテーマに取り組む意義は薄くなる(「誰かが類似研究をしていても、〜の点を解決したいから or どうしても自分でやってみたいから このテーマを押し進める」という判断をすることもある)。
このテーマの時は、仮に先行研究があったとしても「自分という個体にカスタマイズして知りたい」状態だったため、ざっとCiNiiで検索する程度でよしとした。

 

2.   何がわかればそれを調査・実現できるか検討する
取り組む前から手順が想像できていれば容易いが、どうやって取り組めばよいかわからない状態で始めることも多い。
このテーマの時は「たぶん各種指標と体調の相関を見ればよいのでは」程度の認識だったので、「相関ってどうやって計算するのか」「本当に相関を計算することで検証できるのか」をまず調べることが必要だと判断した。

 

3.   2.で検討した内容を学ぶ

読んだだけだと即忘却し、元のわからなかった状態に戻るので、内容をメモしながら進める

このテーマの時は、統計の勉強をしないと調査を進められないと感じていたので、相関を含め統計の基本を勉強した。具体的には、統計の入門から書かれている本を10冊程度並行して読んでいった(1冊を集中して読む方法もあると思うが、複数読むことで何度も出てくるところは重要だとわかるし、1冊で理解できなかった箇所を他の本の説明で理解できるメリットがある)。

 

4.   テーマの調査・実現を試みる

たとえ実質進捗がなくても、やったことを記載して自分を励ますことが肝要である

3.で理解した内容をもとに、テーマの解決を実際に試みてみる。これで結論までいけそうならそのまま進めるが、実際は「やってみたらここが不明だからこれ以上進められない」とどこかでつまづくことが多い。そうしたら、またその不明な点がわかるように勉強、新たに得た知識を元にまた進める、を繰り返していく。
このテーマの時は、「時系列データの時は見せかけの回帰というのが発生するらしい」と知ったので、そのあたりを学べる本を探して読んだ(難しくて理解できず、かなり苦しかった記憶がある)

 

5.   ある程度区切りがよいところまで進んだらまとめる
自分の中でいったんここまで、というところまで来れたら、アウトプットする。仕事でなく遊びなので、「飽きた」「明確な結論が得られなかった」状態でもやめてよいことにしている(ただ、それであっても何らかの形でアウトプットを行う)。
子どもの自由研究は見てくれる人(教師なりクラスメイトなり)が労せず得られるが、大人になると「見てもらう」こと自体も難しい。まずは「理解してもらえる形で目に触れる場所にある」ことが最低条件となる。
一人で進めていると内容が誤っている可能性もあるのが怖いところだが、機会があれば勉強会等で発表すると助言が得られることもある。

自由研究のメリットデメリット

「楽しい」というだけでやるに値するが、一応メリットデメリットを整理する。

メリット:心の安定に寄与する
自分が動くことで、今までなかったものが生み出される。たとえ、仕事で価値をあまり生み出せていない時期も、自由研究で成果が出れば、自分の中で多少心の支えになる(業務面の課題は何も解決していないのだが、別方面で成果を得ると脳が錯覚するのである)。
また、業務後に仕事とは異なる内容に頭を使わないといけなくなり、業務のあれこれを考える余裕がなくなるので、結果的に気持ちの切り替えもしやすい。
※ なお、自由研究でも進捗が出なくて二重に苦しくなることもままある

デメリット:可処分時間の減少
自由研究に取り組むと、思った以上に時間を食うことに気づく。しかし、可処分時間の大半を注がないとなかなか進捗が出ないので、楽しくならないというジレンマがある。遊ぶ時間や他の勉強をする時間に影響するので地味につらい。

結論

大人になってから行う自由研究は(苦しくも)楽しいぞ、みんなやろう

 

よい自由研究ライフを!

レシートを読み取ってCSVに変換するデスクトップアプリを作った

こんな感じで、レシートを検知して切取り → 1枚ずつOCR → 誤って読み取ったところの修正・付加情報の追加を手作業で受け付け → CSVで保存 …という流れをGUI上で行えるようにした。

 

きっかけ

元々手作業でレシートの入力を行っていたが、少しでも手間をかけずに行いたいと思ったため。

要件

全て電子決済にしてその電子的な履歴を参照するようにすれば、こんな面倒なことをする必要はないと思う。今回、以下の要件を実現したかったため、このようなちょっと操作が面倒なものになった。

    • レシートの合計額でなく、品目単位で見れるようにしたい
      1回の購入の中でも品目の分類が異なる(スーパーで食品と雑貨を同時に買うとか)ことがあり、分けてデータを集計できるようにしたいため、品目ごとの取得が必要だった。
    • 軽減税率や外税/内税等を反映させて品目ごとの価格を自動で計算したい
      レシートを観察すると、表記の仕方として外税の場合と内税の場合があり、さらに品目により軽減税率の場合もある。なるべくそれらの条件は自動で読み取って、計算を自動で行うようにしたかった。
    • レシートを複数枚同時に扱えるようにしたい
      OCRするには当然レシートの画像が必要だけど、1枚ごとに撮影するのは面倒なので、複数枚の画像でも扱えるようにしたかった。
    • そのものの品目名で印字されていないが繰り返し出てくる品目は、2回目からは自動で入力してほしい
      例)「スッキリCAテツ1L」というのは「牛乳」という名称で登録したい(「すっきりCa鉄 1000ml」という名称で販売されている、カルシウムや鉄分が付加されている牛乳である)。
    • 分類もなるべく自動で判定してほしい
      「牛乳」が分類としては「食費」というのは(自分の分類方法では)自明であり、こちらも2度目からは自動入力されるようにしたかった。
    • ローカルで操作を完結させたい
      レシート画像を通信するのは何となく不安だったので、全ての操作がローカル上で完結するようにしたかった。

処理の流れ

大まかには以下の流れで処理するようにした。

 

1 OpenCVでレシートの矩形を検知して切取り
   ↓
2 TesseractでOCRを行う
   ↓
3 正規表現で頑張って品目や価格、購入日等を抽出
   ↓
4 品目名等を過去の履歴を参照して修正したり、品目ごとの価格(税込・軽減税率適用)を計算したりする
   ↓
5 Tkinter(GUI作成用のPythonの標準ライブラリ)でデータ(品目や価格等)を表示、手作業での修正を受け付ける
   ↓
6 最終的なデータを取得してCSVに吐き出し

今後

とりあえず最低限動くというレベルなので、今後自分で実際に使っていってみて、不便なところを徐々に直していくようにしたい。
特に以下の点についてはすでに気になっている。

 

    • 品目ごとの価格の調整
      外税表記で、合計額に対し消費税を最後に計算されているレシートだと、品目ごとに消費税を計算すると合計額が実際の価格から数円ずれてしまうことがある。今は単純に小数点以下四捨五入にしているけど、何らかの調整を入れて合計額がぴったり合うようにしたい。
    • レシートの矩形の検知性能向上
      レシートと背景のコントラストが低い画像の場合など、うまくレシートを検知できない場合がある。OpenCVや画像認識を勉強して、もう少し性能を上げたい。
    • 文字の認識精度向上
      レシートは半角カタカナもけっこうな頻度で使われていて、特にその場合の認識精度は相当低い。ただ、OCRはtesseractを使わせてもらっているだけなので、今のところ具体的な改善点を思いついていない。
正直、すでに「レシートをまとめて撮影するのさえ面倒だな…」という気持ちが芽生えており、このファーストステップの簡略化の方法も検討が必要かもしれない。

Couseraの機械学習講座を受講した

オンライン教育サービスであるCouseraで、機械学習の基礎的な学習講座として有名なMachine Learning講座を受講した。

前々から気になりつつ、「自分がついていけるレベルだろうか」「英語わかんないしな」(講義には日本語訳がついているけどテストや課題は英語)、「Pythonじゃないし」(プログラミング課題はOctaveで提出する)となかなか踏み出せずにいた。
在宅勤務になり家にいる時間が増えたことも後押しになって、受講することができたので、感想等記録しておく。

扱われる内容

下記のキーワードに関連する内容について、11週にわたり講義が行われる。

Week 1 機械学習の概要
線形単回帰、最小二乗法、最急降下法

Week 2 線形重回帰
特徴量のスケーリング、正則化

Week 3 分類 ロジスティック回帰
過学習、正則化、One-vs-All

Week 4・5 ニューラルネットワーク
隠れ層、論理ゲート、backpropagation、gradient checking、ランダム初期化

Week 6 機械学習の評価
交差検証、high bias、high variance、学習曲線、適合率、再現率、F値

Week 7 サポートベクタマシン(SVM)
マージン、決定境界、カーネル法

Week 8 クラスタリング・主成分分析(PCA)
K平均法、局所最適、エルボー法
次元削減、射影誤差、共分散行列

Week 9 異常検知・レコメンドシステム
正規分布、多変量正規分布
協調フィルタリング、類似度

Week 10 大規模データ
確率的勾配降下法、ミニバッチ勾配降下法、逐次学習、並列化

Week 11 Photo OCR
パイプライン、スライディングウインドウ、データ合成、ceiling analysis

学習の進め方

ノート
こんな感じでノートにメモしていった

各週、動画による講義 + テスト + プログラミング課題から構成されている。
私は講義を平日にざっと見て(わからないところがあってもあまり気にせず最後までいったん見る)、休日にもう1回見直しながら話の流れをノートにメモしていった。平日見たときは「何言ってるか全然わからん」と思った箇所も、2回目に休日に見た時は「あれ、なんで理解できなかったんだろう」となることも多かった(何回見直してもやっぱりわからん、となる箇所ももちろんあった)。
講師のAndrew先生の説明がわかりやすかったのはもちろんだけど、ノートにまとめようとする過程で理解が曖昧な箇所をつぶし、流れをしっかり捉えられるのが自分にはよかったのかなと思う。

かかった時間

ネット上の体験談だと1ヶ月未満で完了したという方も割と見かけたけど(みんな天才なのかな?と思った)、私は設定されているのと同じペースで進めていったので、約2ヶ月かかった。週により内容の重さの差が大きくて、3時間で終わった週もあれば15時間以上かかった週もあった。

感想

講座全体を通じて感じたことを記録しておく。

  • 勉強を続けて納得感を高めていきたいと思った

とても勉強になったので受講してよかったなと思う。
説明が非常にわかりやすいので、そのアルゴリズムの意図や、数式の意味するところの大枠について理解することができた。全体的に納得しながら進めることができたのだけど、「この数式がなぜこうなるかは、ここでは説明しない」という説明で次に進む部分もしばしばあったので、数式の導出などは少しもやもやが残った。また、課題は一から自分でコードを書くのではなく、重要な数式部分のみ自分で書くという形式だった。そのため、ふんわりとした理解になっている自覚がある。自分でPythonで書き直してみるとか、数式の導出をしてみるとかするべきなのだろう。勉強を続けていって、理解を深めていきたいと思う。

  • 英語をもうちょっと読めるようになると色々楽だなと思った

講義には有志の方がつけてくれた訳文がついているけど、テストや課題は英語で書かれている。最初の数週は頑張って単語を調べながら読んでいたけど、途中から面倒になってDeepLに突っ込んで訳文を読むようになった。俄然課題の進みが速くなって、自分の英語力の低さを改めて実感した。DeepLは訳が自然で、訳文だと意味が不明で結局原文を読むみたいなことはほとんど発生しなかった(訳文を読んで「解くのに必要な前提条件が足りない…」となって、原文見たら訳されていない箇所があった、ということは数度あった)。

  • ペース配分を自分でしなくて済むのって楽だなと思った

課題を提出すると、何割終わったか示してくれる

オンライン講座では当たり前なのかもしれないけど、見終わった動画にはチェックマークがついたり、プログラミング課題も途中提出の度にどこまで終わったか明確に表してくれる。「今週中にあと3個動画見て課題を解くから、x時間くらいで終わる」とわかると、残りの時間は別の学習に充てようなど見通しが立てやすい。ペース配分を自分でしなくて済むこと、どこまで進んでいるか示してくれることがこんなに楽なのかというのは意外な発見だった。

今後機械学習の勉強をしていく上で、非常に役に立つ講座だった。継続して学習していきたい。

データ分析者がCS経験から得たこと

※ ここでのCSはカスタマーサービス(コンピュータサイエンスではない)

昨年、分析を中心としたディレクター職から、CS職に異動して半年ほど過ごした。
この半年間の経験から何を得て、意識や行動がどう変わったのか、再びディレクターに戻り数ヶ月経った現在までに感じた差分をふりかえってみる。

  • ユーザーの姿が以前より立体的になった
    CS業務の中心である問合せ対応を行うなかで、自分の中のユーザー像が少しずつ具体的になっていった。以前はデータから「こういうお客様がいるのかな」と思ったり、周りから「こういう傾向のユーザーが多いよ」と聞いて、漠然とユーザーの姿を想像していた。それが、お客様からの問合せを千数百人分読み、1件ずつ自分の手で回答文を作成することで、「〜を目的として操作する人には〜でつまずく人が多く、それはこの部分が理解しづらいことが原因かもしれない」などの仮説が自分の中に蓄積されていった。
    データを見るとき全体的な視点になりがちだったけど、1件のデータの裏にいる1人のユーザーを実感を伴った形で想像できるようになったことで、データから仮説を考えやすくなったと感じている。1

  • 「分析が役に立てる」と認識する範囲が広がった
    CS業務を毎日していると、「こういう傾向の問合せが多いから改善したい」という部分(明らかな不具合であれば即エンジニア等に対応してもらうけど、そうではなく、より使いやすくなるような改善点)が見えてくる。そう感じる部分は同じサービスに携わるCS内では大抵共通認識となっていたけど、他職種の人も同様に認識しているかというと必ずしもそうではない場合もあった。
    その内の1つについて、事象の起こっている件数、それによる損失額、具体な原因と改善案を問合せ対応の合間にまとめ、CS外に提案したところ、実際に改善に至ることができた。
    以前は、当たり前なことを可視化しても「そうだね」「知ってる」となるだけで、さほど意味がないのではと思っていた。でも、当たり前の範囲は人により差があり、各自が認識しているサービスの姿は思っている以上に異なっていることがわかってきた。また、当たり前であってもその課題の大きさや姿は具体的に認識されていない場合もあることもわかった。
    こういった点について、分析者が媒介になる(課題を適切に可視化して共有することで、解決につなげる)ことも分析が役に立てる範囲なんだなと認識が広がった。
    媒介となるためには、分析者が課題の存在を認識している必要がある。CSにいればサービスへの反応を自然と知ることができるけど、現在は離れているし半年で認識できた範囲はわずかだと思うので、これからもCSのメンバーに積極的に話を聞かせてもらいに行こうと思っている。

  • 分析を通じて貢献するぞ、という意識が強くなった
    CS在籍中、エンジニアやデザイナーなど色々な人が折にふれ、気にかけて声をかけてくれた。また、CSの同僚も私が分析をやりたい(CSとして今後やっていきたいわけではない)ことを知った上で、色々質問しても、どの部分を見てどう考えた結果その判断に至ったのか、サービスがそうなっている経緯など納得するまで教えてくれた。
    ディレクターに戻り、それらに報いたいという気持ちとともに、分析面でしっかり貢献できないと(分析専門でない部署で分析中心にやっている分余計に)自分がここに存在する意味がなくなってしまうなと思うようになった。
    存在意義を示すためには、分析により改善につながる部分を見定めること、分析結果を示すだけでなくそれを施策に落として提案することが必要だと思っている(そもそもの分析能力も当然まだまだ精進が必要だけど)。色々スマートにできなくて転げながらやっている毎日だけど、分析を業務としてやれることが嬉しいから、かっこわるくてもやっていく。


  1. 利用中に一度も問合せをされないお客様も多いので、問合せだけからユーザーの姿を思い込みすぎるのは危険である、という点も留意する必要はあると思っている 

暖かいルートを検索するWebアプリを作ろうとした話_うまくいかなかった部分

こちらでふりかえりを書いたWebアプリについて、デプロイでうまくいかなかった部分を記録しておく。

概要

ローカルでは一応意図した形で動いてくれていたDjangoをHerokuにデプロイしたところ、エラーが出てうまく動かなかった。そして、そのエラーを解決することができなかった。
自分の理解では、利用しているライブラリが参照しているファイルをうまく参照させることができなかったのでエラーとなったと認識している。
コードはここにいったん置いた。

詳細

1- ログの確認

Herokuでデプロイして、該当サイトにアクセスしたところ「Application error」という表示になっていた。

その下に表示されていた文に従い、ログを確認した。

$ heroku logs --tail

(前略)
2019-12-30T05:46:04.064263+00:00 app[web.1]: import osmnx as ox
2019-12-30T05:46:04.064272+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/osmnx/__init__.py", line 9, in
2019-12-30T05:46:04.064274+00:00 app[web.1]: from .core import *
2019-12-30T05:46:04.064276+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/osmnx/core.py", line 10, in
2019-12-30T05:46:04.064278+00:00 app[web.1]: import geopandas as gpd
2019-12-30T05:46:04.064288+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/geopandas/__init__.py", line 1, in
2019-12-30T05:46:04.064290+00:00 app[web.1]: from geopandas.geoseries import GeoSeries # noqa
2019-12-30T05:46:04.064292+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/geopandas/geoseries.py", line 15, in
2019-12-30T05:46:04.064294+00:00 app[web.1]: from geopandas.base import GeoPandasBase, _delegate_property
2019-12-30T05:46:04.064296+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/geopandas/base.py", line 16, in
2019-12-30T05:46:04.064298+00:00 app[web.1]: from rtree.core import RTreeError
2019-12-30T05:46:04.064300+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/rtree/__init__.py", line 1, in
2019-12-30T05:46:04.064302+00:00 app[web.1]: from .index import Rtree
2019-12-30T05:46:04.064310+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/rtree/index.py", line 5, in
2019-12-30T05:46:04.064312+00:00 app[web.1]: from . import core
2019-12-30T05:46:04.064314+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/rtree/core.py", line 125, in
2019-12-30T05:46:04.064316+00:00 app[web.1]: raise OSError("Could not find libspatialindex_c library file")

計算して求めたルートを地図にプロットするのに、osmnxというライブラリを利用している。そして、osmnxではRtreeというライブラリが使用されている。Rtreeのコードの中で、libspatialindex_c library fileが見つからなかったことによりエラーになっていると理解した。

2- ローカルではエラーが起きなかったのは何が違うのか確認する

上記のエラー文にFile "/app/.heroku/python/lib/python3.7/site-packages/rtree/core.py", line 125 とあり、.venv/lib/python3.7/site-packages/rtree/core.pyの125行目付近を確認した。
/app/.heroku/~というのがどのように確認できるかわからなかったのでvenvでローカルでインストールしたものと同じだろうと考えて上記を確認した)

elif os.name == 'posix':
    if 'SPATIALINDEX_C_LIBRARY' in os.environ:
        lib_name = os.environ['SPATIALINDEX_C_LIBRARY']
    else:
        lib_name = find_library('spatialindex_c')

    if lib_name is None:
        raise OSError("Could not find libspatialindex_c library file")

    rt = ctypes.CDLL(lib_name)
else:
    raise RTreeError('Unsupported OS "%s"' % os.name)

lib_nameが定義できていなかった(環境変数の中にSPATIALINDEX_C_LIBRARYがなく、またspatialindex_cというライブラリも見つけられなかった)ためにエラーとなっていたことがわかった。

ターミナルでPythonを立ち上げて、ローカル環境だとどうなっているか確認してみた。

>>> import os
>>> os.name
'posix'
>>> os.environ
environ({'TERM_PROGRAM': 'Apple_Terminal',(後略)

>>> from ctypes.util import find_library
>>> find_library('spatialindex_c')
'/usr/local/lib/libspatialindex_c.dylib'

os.environでSPATIALINDEX_C_LIBRARYがなかったことから、ローカル環境ではlib_name = find_library('spatialindex_c')の方が実行されたのだろうこと、またspatialindex_cはローカル環境だと/usr/local/lib/libspatialindex_c.dylibにあるのだということがわかった。

検索してみると、ローカル環境で同じような問題にぶつかっている人がいた。Homebrewでspatialindexをインストールしたら解決したとされており、$ brew listで確認してみるとインストール済みだった。そのため、ローカルでは問題が起きなかったのだろうと思った。

3- どうしたら本番環境でも参照できるようになるか考える

ローカル環境では該当ファイルが存在していたため参照できたが、本番環境には該当のファイルがないためエラーになる。
pipでインストールできるファイルならrequirements.txtに書いておけるけど、そうではなさそうだ。それなら本番環境にもそのファイルを置いて、参照できればとりあえずは解決できるのではと考えた。

/usr/local/lib/libspatialindex_c.dylibをコピーして、本番環境のプロジェクト直下にlibディレクトリを作り、置いてみた。

これが正しい行為なのかわからないけど、環境変数にパスを設定してみた。
$ heroku configで、登録できていることが確認できた。

(前略)
SPATIALINDEX_C_LIBRARY: lib/libspatialindex_c.dylib

この修正をしても同様のエラー画面だったため、再度、$ heroku logs --tailでログを確認した。

(前略)
2019-12-30T06:38:08.950338+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/site-packages/rtree/core.py", line 127, in
2019-12-30T06:38:08.950339+00:00 app[web.1]: rt = ctypes.CDLL(lib_name)
2019-12-30T06:38:08.950340+00:00 app[web.1]: File "/app/.heroku/python/lib/python3.7/ctypes/__init__.py", line 364, in __init__
2019-12-30T06:38:08.950342+00:00 app[web.1]: self._handle = _dlopen(self._name, mode)
2019-12-30T06:38:08.950344+00:00 app[web.1]: OSError: lib/libspatialindex_c.dylib: invalid ELF header

表示が前回と変わっている。
最初のエラーログで出ていたraise OSError("Could not find libspatialindex_c library file")はなく、次の行のrt = ctypes.CDLL(lib_name)が表示されているので、libspatialindex_cを見つけること自体はできたと考えてよいのだろうか。

エラーの最後の行で、invalid ELF headerと表示されている。検索すると、違うOSでのビルドだから読み込めない場合に表示されるという記載があった。

「2- ローカルではエラーが起きなかったのは何が違うのか確認する」に記載した.venv/lib/python3.7/site-packages/rtree/core.pyで、もう少し上の方に以下のような記載箇所がある。

if 'SPATIALINDEX_C_LIBRARY' in os.environ:
  lib_path, lib_name = os.path.split(os.environ['SPATIALINDEX_C_LIBRARY'])
  rt = _load_library(lib_name, ctypes.cdll.LoadLibrary, (lib_path,))
else:
  rt = _load_library('spatialindex_c.dll', ctypes.cdll.LoadLibrary)
if not rt:
  raise OSError("could not find or load spatialindex_c.dll")

このspatialindex_c.dllの方が必要なのかなと思い(多分違う)、探したが見つけられなかった。

4- いったん諦める

いったん諦める。もう少し知識がついたらまた考えてみる。

暖かいルートを検索するWebアプリを作ろうとした話_作業経過

初めてWebアプリを作ろうとして、方法の検索からデプロイ(失敗した)まで色々試行錯誤したので、経過を記録しておく。

目的

そもそも作ろうと思ったのは以下のようなことが理由だった。

  • 途中まで作った日陰の計算を一般的に使えるものにしたい
    以前、道路にかかる影の割合を計算して、任意のルートの涼しさを調べようとしたことがあったので(「夏には日陰を歩きたい」という欲望を満たすために計算する)、これをローカルで実行できるだけでなく一般的に使えるようにしたいと考えた。
    考えた時点ですでに夏が過ぎていたので、涼しい道でなく暖かい道を調べられるよう、直射日光の量を計算するように変えた。

  • Webサイトの仕組みについて、雰囲気だけでもつかみたい
    「ブラウザからリクエストが来たら、なんか処理して、レスポンスが返されるんでしょ…?」という非常にふわっとした理解だったので、「なんか処理」の部分がどのように作られているのか少しでもイメージできるようになりたいという気持ちがあった。
    普段仕事をしていても、エンジニアやデザイナーが何をして、サイトの表示やアカウントの登録等ができているのか、それを実現するコードがどのようなものなのか想像ができなかった。一緒に仕事するにあたり、詳しくはわからなくても、概略をイメージできるとよいなあという気持ちがあったので、自分で作ってみることにした。

できたこと

緯度経度と日時を入れたら、暖かいルートを表示する仕組みを作った(ローカルでは動く)

できなかったこと

ライブラリがうまく扱えず、デプロイができなかった
※うまくできなかった部分の詳細については、こちらに記載した。

作業経過

1- 調べる : 1h
「Pythonでweb上で動く仕組みを作るにはどうしたら良いのかな〜」と思い、そのまま「Python Webアプリ」と検索した。
DjangoとかFlaskというのが有名で、Djangoの方が色々やれることが多いらしいとわかった。自分のやりたいことに対してFlaskでもできるかの判断がつかず(多分できた)、Djangoでやってみることにした。

2- Djangoの学習をする : 24h
最初にDjangoのチュートリアルをやった。
丁寧に手順を追ってくれていてその通りに操作をすることはできたけど、各操作が何を目的としたものなのかや、全体的なファイル構成とかがわからなかったので、下記のような感じで各ファイルのつながりをメモしながら再度なぞってみたら少し理解ができた。

この時点で実際に自分の作りたいものに着手して、よくわからなくなったら本や動画を参照する形で進めた。以下を参考にさせていただいた。
本:現場で使える-Django-の教科書《基礎編》
動画:【3日でできる】Django 入門

3- 暖かいルートを計算するにはどうしたら良いか考える : 18h
以前計算した道路にかかる影の割合(「夏には日陰を歩きたい」という欲望を満たすために計算する)をベースに、暖かいルートをどのように計算したら良いか考えた。
結果、厳密には正確ではないだろうが「距離当たりの体の受ける直射日射の量」で代替することにした。太陽から降り注ぐ日射のうち、角度のついた鉛直面の日射量を計算する方法を本やネット上の論文から調べた。また、計算に用いる道路の緯度経度をOpenStreetMapから得て、各道路の方位角と長さを求めた。
大まかには、以下のような計算を行っている。コードはこちら

4- コードを書く : 28h
日射の計算のコードを書いていたのが10hくらいで、残りはDjango関係のコードを書いていた。地図の表示や、Djangoでの画像の表示がうまくいかず、時間がかかった。
コードはとりあえずこちらに置いてある。

5- デプロイする(失敗) : 22h
最初は別サービスを使ってデプロイしようとしたがうまくいかず(色々いじっているうちになぜかpipがうまく働かなくなり、最終的にOSの再インストールに至った)、Herokuを利用して進めていった。
Herokuのアカウントを作って、チュートリアルをやった。丁寧に説明されていて、例となるコードをクローンして進める形だったので静的ファイルの取り扱い等も参考にでき、初めてでもすごくわかりやすかった。
結局、ライブラリの扱いの部分でうまくいかず、年内の完成を目指していたのでいったん諦めることにした。
※うまくできなかった部分の詳細については、こちらに記載した。

やってみてどうだったか

当初の目的に対し、どうだったかをふりかえる。

  • 途中まで作った日陰の計算を一般的に使えるものにしたい
    → 結局デプロイできなかったので、一般に使えるものにならなかった。悔しいので、またもう少し理解が深まったら、つまづいた部分を解決してデプロイまで至るようにしたい。

  • Webサイトの仕組みについて、雰囲気だけでもつかみたい
    → (Djangoの場合の)各ファイルがどのように関係して、サイトが表示されるのか(MTVモデル)がうっすらわかった。Djangoの内部でどのような処理が行われているかはわかっていないので、「フレームワークすごいな〜」という気持ちになった。Webサイトの作りについて、さわりだけ知れたかな…という感じ。

何に時間を使ったか_2019上期

統計検定を受けた時のふりかえりで「何に時間をかけているか意識をしよう」と考えてから、ちまちまと記録をしていたので、半年間の時間の使い方をふりかえってみる。
以下、自分で「学び」系の区分に入れていた時間の内訳を記載する(なので、一般に「これは学びというより趣味なのでは…」という部分も含まれる)。

⑴ データ分析関係

  • 分析手法を学ぶ 18h 7冊
    データ分析の手法や、業務として分析を進めていく方法などについて書かれている本を読んだ。読んだ中では、以下の本が一番実際の業務に当てはめながら読めて、学びが多かった。
    ビジネス活用事例で学ぶ データサイエンス入門

  • 機械学習について学ぶ 15h 2冊
    scikit-learnのライブラリを利用して実行する方法と、機械学習に使われている数学の基礎について書かれた本を途中まで読んだ。半年で15hと、お前やる気あるのかという状態なので、来期はもう少しまじめに勉強する。
    機械学習を理解するための数学のきほん
    Pythonではじめる機械学習 scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

  • 実際のデータで試す 62h
    上記で学んだ手法を実際のデータで試して遊んでいた時間。けっこう時間かけている割には、ちゃんとまとめておらず、ちょっと試してみたJupyter Notebookが散逸しているような状態で、自分の中に残っているものが少ないのが反省点。

⑵ エンジニアリング関係の学び

⑶ 業務理解

  • ビジネス系の本を読む 34h 16冊
    周りの人がごく普通に感覚として持っていることでも、自分は知らないなと思い、なるべく業務に関係しそうな本を読んだ。

この辺は、知識として読んでよかったなと思う。
カスタマーサクセス――サブスクリプション時代に求められる「顧客の成功」10の原則
サブスクリプション――「顧客の成功」が収益を生む新時代のビジネスモデル

逆に、直接的に業務とつながるわけではないけど、「面白い」と思ったのはこの辺り。
TRUST 世界最先端の企業はいかに〈信頼〉を攻略したか
Airbnbが登場当初、「見知らぬ人の家に宿泊する」というアイデアへの理解・信頼を得るため、どのようなサイト構成にしたのかという部分が興味深かった。

すいません、ほぼ日の経営。
“いまは「あなたはなにもしなくていい」という商品ばかりが売れる時代です。でも手帳は、あなたがなにかをしなければいけない商品です。使いながら完成させていくものです。ぼくらは、いわば未完成品を売っているわけで、それを完成品にするのは使う人です。”

色々条件は異なるけど、作ってもらうという部分は今の仕事で扱うサービスと共通で、なんかヒントがありそうだなと思った。

  • インタビューについての本を読む 17h 3冊
    社内で分析課題について探る時、「その業務に精通している人から的確に話を聞いて、分析課題に落とす」ことが上手くできていないなという気持ちから、インタビューにヒントがないだろうか、と考え読んだ。メモを取ってまとめたは良いものの、そのままになってしまっていたので、見返して日々に活かすにはどうしたらよいかを考える必要がある。

マーケティング・インタビュー 問題解決のヒントを「聞き出す」技術
ユーザーインタビューをはじめよう ―UXリサーチのための、「聞くこと」入門
インタビュー 木村俊介

⑷ アウトプット

仕事でPythonに触れる割合が減ったので、忘れないようにという意味合いもあって行った。作成のふりかえりはすでに書いた。
はじめてのLTで緊張したけど、一人で作っていたものについて人が話を聞いてくれて、感想まで教えてくれるってすばらしいなと思った。

ブログへのまとめに時間がかかっている。本編より長い。まとめ出すとよくわかっていない部分に気づいて調べたり、どう書いたらわかりやすいのか考えたりとかしているうちに時間がかかってしまっている状態。もう少し短縮したい。

⑸ その他

  • ふりかえり 14h
    主に「今週のふりかえり」(何をしたか、何を考えたか)をしていた時間。平均0.5h/週。今のところ「やっててよかった」と思う機会は訪れていない。

  • 色々考える時間 23h
    仕事/仕事外両者について、「こういう分析やれないかな」「やるには具体的にどうしたらいいか」等考えていた時間。のはずなんだけど、しっかりまとめてメモしていなかったせいで、かかっている時間の割には実りが少ない。平均1h/週。

  • その他 28h
    Pythonのライブラリ入れてうまくいかなくて調べたり、疑問に思ったことを調べたりしていた時間。

  • 不明 26h
    「何やったか書くのめんどいな…」と思ってその場で書かないとこうなる。⑴〜⑶のいずれかに入ると思われる。

ふりかえっての所感

記録を取ることで時間に対する感覚が転換した

以前から、時間の使い方は毎日記録していた。ただ、それは睡眠x時間、仕事y時間…という項目別のレベルの記録だった。それだと、学びに対して「今週はx時間かー」というふりかえりになり、「いかに学びにかける時間を増やせるか」という見方になっていた。
それが学びの中身を具体的に記録するようになったら、「この本読むのにもうx時間かかってるのか…急ごう」となり、「いかに短い時間で学べるか」という見方に変わった。
どちらも「記録する」という点では同じなのに、自分の見方が逆に変わったのは面白いなと思う。

漫然と学んでしまっている

その週、仕事で触れていた分野だったり、図書館で偶然借りた本だったりを学んでいることが多かった。計画性に欠けていた。
半年単位で集計すると数十時間かけていたことでも、ふりかえると「こんなにかけてた…?ていうか何も得てなくない…?」と思う分野もあった。特に「本を読んだ」分野だと、メモを取っていないと内容がほぼ記憶に残っていない(メモを取っていても、鮮明には残っていない)。

勉強すること自体には意味がない

そう、「勉強すること」自体には意味がないのである(※ 娯楽として勉強する場合は別として)。今の自分の学びは、基本的に「データ分析の観点から役に立てるようになる」ためにやっているので、学んで「なるほど」で終わったら意味がないのである。この点の意識が不十分だったなと思う。

半年あったら、もっとできるんじゃねーの、と思った

総量的な意味でも、時間あたりの質的な意味でも、ふりかえるともっとできるよね?という感じ。

図書館を利用することの弊害が出ている

8割方の本は図書館で借りさせていただいて読んだ。図書館で借りることのメリットは、返却期限があることで、強制的に勉強する期限が設定できることだ。
一方、図書館で借りて「腰を据えて勉強しよう」と思った本は買って読むんだけど、そうすると「いつでも読める」と思ってしまう。結果、自分にとって重要度の高い本を読まずに、返却期限のある本を優先することになってしまっていた。

バックアップはちゃんと取ろう

6/29、PCが壊れ、ここ半年で学んでローカルに保存していたファイルやデータベースが消えた。バックアップの重要性が身にしみた。この記事もスマホで書いているけど、画面が小さくてつらい。

で、これからどうするの?

  • もう少しこまめにふりかえって、軌道修正するようにする
  • 学んだ内容をちゃんと文に落とす
  • 図書館で本を借りるのはもう少し抑える
  • 理論面、技術面を真剣に学ぶ

「『夏には日陰を歩きたい』という欲望を満たすために計算する」のふりかえり

やろうと思った目的

  • GW10日間チャレンジ
    今回、10日間という例年にない長期休みだったため、普段の勉強とは違い、何か具体的な課題に取り組みたいと考えた。

  • 課題への時間の見積もりができるようになりたい
    業務で分析課題を行う際、「どのくらいでできるか」と問われてもなかなか答えられなかった。これは、経験値が少ないことも当然あるのだけど、自分が「何の作業にどのくらいかけているか」をあまり意識できていないこともあると思った。
    今回、解決までが見通せていない課題に取り組む中で、どのような過程で何を考え、何に何時間かけたかを記録して、時間感覚を得たいと考えた。

  • 見えていないものを見える化してみたい
    普段漠然と感じていることを数字に落とし、それを視覚化することで容易に理解できる形にする。すでにある数字から何かを作るのではなく、数字化する部分からやってみたいと思っていた。

当初目標としていた内容

当初は、地図の画像を読み込んで建物を検出し、それぞれに高さを与えて任意の時間における影を地図に重ねて描きたいと考えていた。
結果的に、以下の理由により断念した。

  • 建物の輪郭だけを得るのが大変
    無料で使用できる白地図が見つからなかったので、OpenStreetMapを利用させてもらうことを考えた。だが、自分の技量不足により、建物の輪郭を検出する際に、地図上の建物名や通り名等も検出されてしまい、うまくできなかった。

  • 建物高さを得るのが大変
    とりあえず自分の行動範囲だけでよかったので、Googleマップ等で目視で調べようかと思っていた。だが、GW中に完成させるという目標に対し、それをしている時間がなかった。また、地図上の建物と高さを機械的に紐づける方法も思いつけなかった。

8日目の時点で諦め、目標を「単純化した建物モデルに対し、道路の方位角ごとに影のでき方を調べる」に変更した。

作業の変遷

開始時点では、以下のような認識だった。

  • 各建物のコーナー部の座標が得られれば、そこから影は求められそう
  • 建物の色と地の色に差があれば、画像処理でうまく建物だけ検出できないかな? 画像処理ってどうやるのかわからないけど
  • 著作権の問題で、Googleマップとかは使えないかもなあ… 無料で使える地図ってないのだろうか

そのような認識から始めたので、作業は以下の3分野を並行して進めていくことになった。

  • 地図から建物外形を得る:地図に描かれた建物の枠線を検出しようとしていた。建物名や通りの名前が入った状態の地図から枠線をうまく検出できず、最終的に断念した。

  • 建物の座標を得る:テスト用の図を自分で用意し、その図の中の建物の枠線を検出し、コーナー部のみになるよう間引き、その座標を得ることができた。

  • 影を計算して描く:建物のコーナー部の座標に対し影の位置を計算し、影の枠線を描き、道路のうち影の重なる面積を求めた。また、ある経路の方位角を求め、日陰になる割合を求めた。

3分野ごとの作業内容を時系列で示すと以下の通り。

円の面積はかかった時間に比例している。また、灰色の円は結果的に不要だった作業を示す。
かかった時間は全体で60時間、そのうち結果的に不要だった作業は9時間くらいだった。また、色々調べたり検討したりしていたのが12時間、コードを書いていたのが48時間くらい(後者はコードを書くために調べていた時間も多いとは思う)だった。60時間と聞くと「かかりすぎだな」と思うけど、上図のように一つずつの作業に分解すると、「今の自分の力だとそのくらいはかかるか…」という気もする。

次回の改善点

  • 作業に取りかかる前にゴールを明確に設定する
    今回、8日目の段階で当初の目標を断念して目標を変えたため、結果的に無駄になる作業が出たり、回りくどいコードになったりしてしまった。開始当初の時点で「現時点の知識からして、10日間でこの目標を達成するのは難しそう」と感じていたので、変更した目標を当初から目標としていれば、もう少し短時間で目標を達成できたと思う。

  • もう少し系統立てて進められるようにする
    やり方を調べるところから始める部分が多く、最後の数日になるまではずっと混沌の中で進めていた。
    「今どこまでできていて、何が課題なのか」を脳内で考えるだけでなく書き出して進めていけば、自分の脳内を整理する意味でもよかったかなと思う。

感想

  • 普段の土日を何週か使って進めていたら、たぶん途中でやめていた。「遊びのコード書いてないで、普段の業務に直結することやろう…勉強したいこともたくさんあるし」となっていたと思う。「この10日間で完成まで持っていく、何らかの形にする」と決めてやったことで、集中してやることができた。
  • 段階ごとに自分で小さな問いを立てて、それをクリアするとゴールに近づいていく嬉しさがあった。今回の課題は、さらにそれが現実世界とつながっている面白さみたいなものもあったと思う。
  • 楽しかったけど、やっている最中は「どこが間違っているかわからない」「落ち着け」「あと3日なのに、全然ゴールが見えない」「ここ直したいけど数時間かかるだろうし」とほぼ焦燥感の中にいた。他者が作ってくれたライブラリを利用させてもらっているだけで、しかもうまく使えてないし、色々わかってないし、と道のりの長さを感じるけど、足元に目を落として一歩ずつ淡々と進んでいくしかない。

「夏には日陰を歩きたい」という欲望を満たすために計算する

目的

昨年の夏、外出のたびに「暑い…少しでも日陰を歩きたい」と思っていた。出かける前に、道路の方位と太陽の位置から影の出来かたを脳内でなんとなくシミュレートするんだけど、だいたい実際の影とずれていた。
夏を迎える前に、時間帯ごとの影の位置を求めておきたい。何時に出かけるのがベストなのかを知った上で、覚悟を持って日差しの元に出て行きたい。
そう思ったので計算した。

作ったもの

まず、与えた建物と道路のモデルに対し、道路の何割が日陰になるかを求めるコードを書いた。
– 入力:求める地点の緯度経度、日付、建物と道路の平面図、建物の高さ
– 出力:建物と道路の平面図に影を重ねた図、道路の方位角ごとの影の割合を求めたグラフ

次に、この結果を使って経路の何割が日陰になるかを求めるコードを書いた。
– 入力:出発地点と到着地点の緯度経度
– 出力:時間帯ごとの影の割合を求めたグラフ

結果

先に結果を示す。

影の計算は、下図のような単純化した建物と道路のモデルを対象とした。4.0mの道路から1.0m後退した位置に、幅・奥行・高さが外法で6,000mmの建物が、1.0mの離れで立ち並んでいる状態とした。
モデル

道路の方位を22.5°ずつ変え、日の出〜日の入りまで、15分ごとに道路のうち影のかかる割合を計算した。(道路の方位は北を0°とした。南北に通る道路なら0°、北西-南東に通る道路なら45°となる) 地点は福岡市(ここでは福岡県庁の緯度経度を代表として利用)とした。

まずは、夏至(2019/6/22)の結果を示す。

  • 南中時刻には、どの方位でもほとんど影ができていない(太陽高度が約80°とかなり天頂に近い位置に太陽があるので、どの方位であっても影ができにくい)。
  • 方位によってかなり影の割合が異なることがわかる。例えば90°(東西の道路)だと、8時過ぎには影がなくなり、16時をすぎるまでそれが続く。一方、0°(南北の道路)だと、10時でもまだ半分以上影ができている。

  • 特徴的なのは67.5°と112.5°の時。前者を取り出して見てみる。
    67.5°

太陽方位と道路の方位が一致、あるいは180°となる(つまり影のできる方位と道路が平行になる)のが日が出ている間に2回あり、その前後は道路に全く影ができない状態になる。
南中後、他の方位だとどんどん影の割合が増えるのに比べ、16時頃を頂点として影の割合が増えた後は太陽方位が道路の方位に近づくのに比例してまた影が減っていっている。

次に、真夏(2019/8/1)の場合の結果を見てみる。

1ヶ月強で、けっこう変化があることがわかる。
方位角と時間帯によって影の割合が違うので、すごく頑張って毎日方違えとかすれば、日差しを避けて生きていけそう。

方位角と時間帯ごとの影の割合がわかったところで、目的地まで至る経路上の影の割合を時間ごとに求めてみる。
今回は緯度経度の代表として利用した福岡県庁から、天神駅までの経路を対象とした。最短経路の方位と距離を求めて、方位については22.5°ずつの方位で近似した。

route

この方位ごとの延長距離から、経路上の影の割合を時間帯ごとに求めてみた。

方位角に偏りのある経路を選んだので、一番長い方位角(135°)の形状を概ね踏襲した結果となった。

計算方法

Pythonで画像処理ができるOpenCVと地理情報を得られるosmnxを利用して求めた。
コードはJupyter Notebookの形式でGitHubに置いた

大まかな計算の流れは以下のとおり
1. モデルとなる画像を読み込み、建物の高さを画像のピクセル数に変換しておく
2. 道路の方位ごとの計算を行うため画像を回転させる
3. 建物の輪郭を検出、間引いてから座標化
4. 緯度経度・日付・時刻から太陽高度・方位角を求める
5. 建物の輪郭の座標ごとに影の位置を求め、それを図形化する
6. (建物と影を重ねた図を書き出し)
7. 影と道路の重なる割合を求め、グラフ化
8. ある経路上における道路の方位とその距離を調べる
9. 方位の割合に応じた時間ごとの影の割合を算出する

反省点/改善点

  • モデルは適当なの?
    正直十分ではないと思う。道路からの後退距離や隣の建物との離れなど、もっと色々なパターンを用意したモデルの平均値を取るなどした方がよかったかもしれない。例えば、今回のモデルでは隣の建物との離れの位置を道路の両側で同じにしているけど、これを互い違いにしたモデルで計算したら影の割合が10%程度違う時間帯もあった。

  • 建物の高さが均一というのはどうなの?
    道路の幅と建物の高さは、ある程度一定(幅の広い道路沿いほど建物の高さは高い)となる傾向にあるのでは、と思う。そうすると、高層地域と低層地域で時間帯間で比べた影の割合は似た傾向になるのでは…という仮説になった。(経路上に高層地域と低層地域両方がある場合は上記の仮説は成り立たなくなるので、現実的ではない)
    今回、GWの10連休中に作り切る、と決めていたので、かなり乱暴な近似をしているけど、影のでき方の傾向性を見る程度には使えるのでは、と考えている。

  • 道路全面に対する影の割合を計算しているけど、真ん中を歩くことは実際にはないよね?
    ある程度幅の広い道路なら、中心でなく端の方を歩くので、道路の端1.0m程度での影の割合を計算する方が実際的だったと思う。ただ、そうすると左右の端ごとの計算が必要になり、結果が複雑になるので今回はやらなかった。

  • ある程度幅の広い道路だと、街路樹があるよね?
    街路樹は今回考慮に入れることができなかった。建物は垂直方向の遮蔽物だけど、樹木は水平方向の遮蔽物で、影のできる面積がかなり大きいので、本当は考慮に入れたかった。幅の広い道路に対しては、x[m]おきにy[m]の高さの円錐状の木のモデルが立っている、というようにすれば良いかもしれない。

  • 隣の建物にかかった影の形が再現できていない
    隣の建物の壁に影がかかると影の形が変形するけど、それは再現できていない。ただ、影を遮った隣の建物による影もあることを考えると、道路にかかる影の量は結果的に変わらないはずと考えている。

  • わざわざ画像として読み込まなくても、もっと簡単にモデルを作れたんじゃない?
    たぶんそうだと思う。当初は実際の地図から建物を読み込んでそれに対する影を計算しようとしていたので、そういう回りくどい形式になってしまった。1回計算するのに20分くらいかかる。

  • コードが整理できていない
    そう思う。後日整理する。

その他全般的な反省は別記事に書いた。
→ 「『夏には日陰を歩きたい』という欲望を満たすために計算する」のふりかえり

MySQLでCSVファイルからデータベースを作る

MySQLをインストールして、CSVファイルからテーブルを作るところまでやってみた過程を記録する。
きっかけは、趣味で計測している時間の使い方記録が11,000行を超え、Excelによる取り回しが重くなってきていたこと。CSVのままPythonで操作するのでもよかったけど、せっかくなのでデータベース化をやってみた。

バージョン

  • macOS 10.13.6
  • MySQL 8.0.15

具体的な操作

以下、リンク先は公式ドキュメントの参照したページ

1 – MySQLのインストール

まず、homebrewでMySQLをインストールした。

$ brew install mysql

(前略)
==> Caveats
We've installed your MySQL database without a root password. To secure it run:
mysql_secure_installation

MySQL is configured to only allow connections from localhost by default

To connect run:
mysql -uroot

To have launchd start mysql now and restart at login:
brew services start mysql
Or, if you don't want/need a background service you can just run:
mysql.server start
(後略)

一番目に書かれている$ mysql_secure_installationをやろうとする。

$ mysql_secure_installation

Securing the MySQL server deployment.

Enter password for user root:
Error: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

パスワードの設定をしていなかったので、Enter password for user root:でそのままEnterを押したが、エラーになった。

can-not-connect-to-server

A MySQL client on Unix can connect to the mysqld server in two different ways: By using a Unix socket file to connect through a file in the file system (default /tmp/mysql.sock), or by using TCP/IP, which connects through a port number.

とあり、接続方法の一つであるソケット接続に失敗したようだ。

結局、MySQLの起動($ mysql.server start)が必要だったようで、その後なら$ mysql_secure_installationを行えた。

$ mysql.server start

Starting MySQL
........ SUCCESS!
$ mysql_secure_installation

Securing the MySQL server deployment.
(長いので後略)

これにより、どのようにセキュリティが向上するかがmysql-secure-installation に書かれている。

  • anonymousユーザー(匿名ユーザー)の削除
  • リモートホストからroot ユーザー(MySQLの操作に対し全権限を持つユーザー)でのログイン禁止
  • testデータベースの削除

などを行った。

2 – MySQLに接続する

$ mysql -u user -pという形で接続する。-uでユーザー名を指定し、-pとすると Enter password: というプロンプトが表示されるのでパスワードを入力する。

rootユーザーでつないでみる。

$ mysql -u root -p

Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 8.0.15 Homebrew

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

つながった!

どんなデータベースがあるか見てみる(testデータベースが消えていることが確認できる)。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.06 sec)

3 – データベースを作る

lifeというデータベースを作り、その中にtime_bookというテーブルを作成する。

データベースを作る

mysql> CREATE DATABASE life;
Query OK, 1 row affected (0.08 sec)

テーブルを作る

mysql> CREATE TABLE life.time_book(
-> id INTEGER PRIMARY KEY AUTO_INCREMENT,
-> start_datetime DATETIME NOT NULL,
-> end_datetime DATETIME NOT NULL,
-> duration TIME NOT NULL,
-> content VARCHAR(24) NOT NULL,
-> memo VARCHAR(60)
-> );

Query OK, 0 rows affected (0.69 sec)

CREATE TABLE db_name.table_name(col_name data_type, …);という形で作れる。

  • data_typeはデータ型 今回使用しているのはDATETIME(日付と時刻),TIME(時刻),VARCHAR(可変長文字列)
  • NOT NULL:そのカラムにはNULLを格納することができなくなる
  • PRIMARY KEY:主キー制約 行の一意性を確保する
  • AUTO_INCREMENT:連番を自動的に振る

テーブル一覧を確認する

mysql> SHOW TABLES FROM life;
+----------------+
| Tables_in_life |
+----------------+
| time_book      |
+----------------+
1 row in set (0.09 sec)

テーブルのカラム一覧を確認する

mysql> SHOW COLUMNS FROM time_book FROM life;
+----------------+-------------+------+-----+---------+----------------+
| Field          | Type        | Null | Key | Default | Extra          |
+----------------+-------------+------+-----+---------+----------------+
| id             | int(11)     | NO   | PRI | NULL    | auto_increment |
| start_datetime | datetime    | NO   |     | NULL    |                |
| end_datetime   | datetime    | NO   |     | NULL    |                |
| duration       | time        | NO   |     | NULL    |                |
| content        | varchar(24) | NO   |     | NULL    |                |
| memo           | varchar(60) | YES  |     | NULL    |                |
+----------------+-------------+------+-----+---------+----------------+
6 rows in set (0.06 sec)

4 – CSVファイルからデータを挿入する

まずデータベースを選択する。

mysql> use life;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

CSVを準備する
~/time_book.csv

id,start_datetime,end_datetime,duration,content,memo
1,2017-01-02 09:45,2017-01-02 10:34,00:49,'移動',
2,2017-01-02 10:34,2017-01-02 11:34,01:00,'家事','料理'
…

データをローカルから読み込もうとする。

mysql> LOAD DATA LOCAL INFILE '~/time_book.csv'
-> INTO TABLE time_book
-> FIELDS TERMINATED BY ','
-> IGNORE 1 LINES;

ERROR 1148 (42000): The used command is not allowed with this MySQL version
  • FIELDS TERMINATED BY ','はカンマ区切りを指定している
  • IGNORE 1 LINESはCSVの1行目がカラム名なので、読み込まないようにするため

エラーになった。
load-data-localを参照すると

  • セキュリティの問題から、デフォルトではLOAD DATA LOCALを使えないようにしてある
  • サーバー側、クライアント側それぞれで許可の設定が必要

なことがわかった。

サーバーサイド

The local_infile system variable controls server-side LOCAL capability. Depending on the local_infile setting, the server refuses or permits local data loading by clients that have LOCAL enabled on the client side. By default, local_infile is disabled.

option-modifiers

The “enabled” form of the option may be specified in any of these ways:
(中略)
–column-names=1

とあるので、1にすれば有効になる。

mysql> SET PERSIST local_infile = 1;
Query OK, 0 rows affected (0.08 sec)

SET文でシステム変数を変更できる(再起動すると設定した内容が失われる)。 SET PERSISTでパラメーターの値を設定すると、再起動後も値が保持される。

mysql> SELECT @@local_infile;
+----------------+
| @@local_infile |
+----------------+
| 1              |
+----------------+
1 row in set (0.00 sec)

クライアントサイド

For the mysql client, local data loading is disabled by default. To disable or enable it explicitly, use the –local-infile=0 or –local-infile[=1] option.

接続時に–local-infile=1と指定する
$ mysql -u root --local-infile=1 -p

サーバー側、クライアント側それぞれで設定できたので、改めてLOAD DATA LOCAL INFILEしたところ、先ほどのエラーは出なくなったが、以下の結果になった。

Query OK, 0 rows affected (0.10 sec)
Records: 0 Deleted: 0 Skipped: 0 Warnings: 0

OKって出てるけど、Records: 0ということはつまり…中身を確認する。

mysql> select * from time_book;
Empty set (0.04 sec)

やっぱり読み込まれていない。

試行錯誤しながら問題がある部分を修正していったので、以下1つずつ示す。

  • datetimeとtimeの型の部分を””で囲っていなかった

最初はLOAD DATA LOCAL INFILEでやっているのが原因かと思い、試しにinsert文で読み込めるかやっていた中で気づいた。

mysql> insert into time_book (start_datetime,end_datetime,duration,content,memo) values(1990-01-01 00:00, 1990-01-01 00:01, 00:01:00, "a","b");

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '00:00, 1990-01-01 00:01, 00:01:00, "a","b")' at line 1
mysql> insert into time_book (start_datetime,end_datetime,duration,content,memo) values("1990-01-01 00:00", "1990-01-01 00:01", "00:01:00", "a","b");
Query OK, 1 row affected (0.03 sec)

“”で日時を囲ったら読み込めた。ごく基本的な話なんだけど、わかっていなかった。
CSVファイルを修正し、LOAD DATA LOCAL INFILEにFIELDS ENCLOSED BY "'"を追加した(これは要素を囲むのに’ ‘を使っていることを指定している)。

  • 文字コードを変えた

まず、MySQL側での文字コードを確認する。

mysql> SHOW VARIABLES LIKE 'char%';
+--------------------------+------------------------------------------------------+
| Variable_name            | Value                                                |
+--------------------------+------------------------------------------------------+
| character_set_client     | utf8mb4                                              |
| character_set_connection | utf8mb4                                              |
| character_set_database   | utf8mb4                                              |
| character_set_filesystem | binary                                               |
| character_set_results    | utf8mb4                                              |
| character_set_server     | utf8mb4                                              |
| character_set_system     | utf8                                                 |
| character_sets_dir       | /usr/local/Cellar/mysql/8.0.15/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+
8 rows in set (0.00 sec)

utf8mb4って何…?と思い内容を見てみる。

mysql> SHOW CHARACTER SET LIKE "utf8%";
+---------+---------------+--------------------+--------+
| Charset | Description   | Default collation  | Maxlen |
+---------+---------------+--------------------+--------+
| utf8    | UTF-8 Unicode | utf8_general_ci    | 3      |
| utf8mb4 | UTF-8 Unicode | utf8mb4_0900_ai_ci | 4      |
+---------+---------------+--------------------+--------+
2 rows in set (0.00 sec)

utf8mb4もUTF-8の一種っぽい。

csvファイルの文字コードを確認する

$ file --mime time_book.csv

time_book.csv: text/plain; charset=unknown-8bit

UTF-8でなかったので、$ iconvで文字コードを変えようとする。

$ iconv -f unknown-8bit -t utf8 time_book.csv > time_book.csv

iconv: conversion from unknown-8bit unsupported
iconv: try 'iconv -l' to get the list of supported encodings

unknown-8bitという文字コードは対応していないと言われる。$ iconv -lで見てみると、確かにunknown-8bitというのはない。$ iconvでは出来ないのか…と思ったが、unknown-8bitはshift_JISのことのようだとわかった。

$ iconv -f shift_JIS -t utf8 time_book.csv > time_book.csv
$ file --mime time_book.csv

time_book.csv: text/plain; charset=utf-8

CSVファイルをUTF-8に変換できた。

  • 改行コードを指定していなかった

上記を修正しても、LOAD DATA LOCAL INFILEで1行目しか読み込まれなかったことから気づいた。
LOAD DATA LOCAL INFILEにLINES TERMINATED BY '\r'を追加した。

  • (これは、読み込みができなかったのと直接関係しないが)id列について、auto_incrementなのに数字を入れていた

mysql> SHOW COLUMNS FROM time_book FROM life;で見ると、id列のNullがnoになっていたので、auto_incrementだけど数値を入れていた。
example-auto-increment

No value was specified for the AUTO_INCREMENT column, so MySQL assigned sequence numbers automatically. You can also explicitly assign 0 to the column to generate sequence numbers, unless the NO_AUTO_VALUE_ON_ZERO SQL mode is enabled.

とあったので、id列は0に変更した。

結果、CSVはこのような形になった。

id,start_datetime,end_datetime,duration,content,memo
0,'2017-01-02 09:45','2017-01-02 10:34','00:49','移動',
0,'2017-01-02 10:34','2017-01-02 11:34','01:00','家事','料理'

再度csvを読み込み

mysql> LOAD DATA LOCAL INFILE '~/time_book.csv'
-> INTO TABLE time_book
-> FIELDS TERMINATED BY ',' ENCLOSED BY "'"
-> LINES TERMINATED BY '\r'
-> IGNORE 1 LINES;

Query OK, 11348 rows affected, 1 warning (0.59 sec)
Records: 11348 Deleted: 0 Skipped: 0 Warnings: 1

読み込みができたっぽいぞ!

mysql> SELECT * FROM life.time_book LIMIT 2;
+----+---------------------+---------------------+----------+---------+--------------+
| id | start_datetime      | end_datetime        | duration | content | memo         |
+----+---------------------+---------------------+----------+---------+--------------+
|  1 | 2017-01-02 09:45:00 | 2017-01-02 10:34:00 | 00:49:00 | 移動    |              |
|  2 | 2017-01-02 10:34:00 | 2017-01-02 11:34:00 | 01:00:00 | 家事    | 料理          |
+----+---------------------+---------------------+----------+---------+--------------+
2 rows in set (0.00 sec)

できた!

感想

なるべく公式ドキュメント(英語)のみで理解しようとして進めたけど、けっこうつらかった。今回よりも古いバージョンなら日本語のドキュメントもあって、わからない場合はそちらも参照したけど、英語で読んでわからない場合は日本語でもだいたいわからず、英語力でなく知識不足が原因か、と思いつつ不明な単語等を調べて進めていった。

単に1つのテーブルを作るだけなのに各所で色々引っかかったけど、作業自体は全般的に楽しかった。やっとデータが読み込めた時はうれしくて、ひとり小躍りした。

楽しんで働くへの現時点の返答

前職にいた時、必要以上に真面目に取り組むことで許されようとする悪癖があった(今もちょっと残っている)。
期限に間に合わせるため、土日や年末年始にも非公式に職場にやってきて働くとか、そういう行動のことです。

この行動は、前職においておおむね批判されなかった。仕事量も人員数も調整がきかない場だったので、便利な存在という側面もあったと思う(そうやって働く人も多かった)1
一方で、その姿勢を諌めてくれる先輩もいた。「お前のまじめさは美徳なんかじゃねーからな」と。
だって、やるしかないじゃん、と思っていた。さらには、この日々は何らかの形で報われると勝手に思っていた。

3年目のある日、気づいた。
部長や課長、あるいは組織がこの頑張りに報いてくれるわけでないし、私の人生に責任を持ってくれるわけではない(当然、持つ必要もない)。
頑張りによる成果は彼らを喜ばせ、それは単純に嬉しいけど、それを自分の最終目的にしても、(頑張っている内容からして)自分に何かが残るわけではない。

この人生を生きているのは誰?
私だ。
まじか、そうかと思った。

目前の仕事だけでいっぱいな日々は、その間思考停止できて、実は楽だ。「でも今はこれをしなきゃ」が汎用的な言い訳になる。
でも、それを何年も続けていると、自分の価値は目前の仕事だけだから、その出来に精神状態が100%左右されるようになる。そして、頑張る見返りとして、自分の人生の責任を取るのは自分であるということを放棄したい心持ちになっていった。

(しつこいですが、ここまで前職での話です)

転職して数ヶ月、楽しんで働くってなんだ、とぼんやり考え続けていた2

まだ考えがまとまっていないけど、一つ考えたことは、真面目であることは、真剣なのと似ているけど違うということだ。真面目さは義務感から発している一方、真剣さはその対象を自分ごととして捉えることから発している。真面目さは深刻さへつながっていく一方、真剣さはユーモアと同居できる。
「どうしたらそれができるって思う?」「今の自分・状況で取りうる、最善の行動ってなんだろうね?」と自分に問いかけてやって、困難3にも、にいっと笑って、楽しんでやっていきたい。今、自分は事業にしっかり貢献できるだけの実力が持てていないけれど、その中でも、考えることはできるはずなのだ。

真面目さで評価されようとするよりも、真剣に取り組んで成果を出す方が厳しい世界なのは気づいている。
無意識に慣れた手法を取ってしまうけど、頑張り方も変えていきたい。1歩進んで、1歩下がって、の繰り返しだろうけど、それでも振り返れば進んでいる。


  1. 数年前の話なので、今は改善されている部分もある。また、強制されたものではない 
  2. 入った当初言われた「楽しんでやりましょう」の衝撃が大きくて、ずっと心の中にあった 
  3. ここでいう困難とは、あくまで現段階の自分から見ての話 他の人から見たらそんなの困難と呼ばないよ、というものも含む 

混乱した統計検定2級の用語をまとめる

統計検定2級の過去問を解いていた時、似た単語がよく出てきて「何でしたっけ…」となっていた。忘れないうちに、それらの単語を書き出してみる。

標本の抽出方法

母集団の要素全てに対して調べることは現実的にできない場合、その中からいくつか取り出して調べることになる。どのように取り出すのがなるべく偏りが出ないかというのと、調べるにあたってどれだけ手間をかけられるか、両者をどこでバランスさせるかによって、色々な手法がある。

単純無作為抽出法

母集団の要素から、どの要素も同じ確率となるよう無作為に抽出する方法。
– 母集団N個から標本n個を抽出する時に、各個体が標本として選択される確率n/N
– どのn個の個体の組も選択される確率が1/NCn
→ 10人から4人選ぶ時に、男2女2という条件がつくと、単純無作為抽出ではなくなる(層化無作為抽出法になる)

系統抽出法

要素全てに番号をふり、1つ目の要素は無作為に抽出し、2つ目以降は等間隔の番号を抽出する方法

層化無作為抽出法

性別、年代、…などで母集団が複数の層にわかれる場合に、いずれかの層に偏りが出ないよう、層ごとにランダム抽出する方法

多段抽出法

例)全国から複数の県を抽出、各県から複数の学校を抽出、各学校からクラスを抽出…というように絞り込んでいく方法
段数が多くなるほど、平均などの推定精度は悪くなる

層化多段抽出法

層化抽出法と多段抽出法を組み合わせたもの

クラスター(集落)抽出法

母集団を分割してクラスターを作った上で複数のクラスターを抽出し、その成員全てを対象とする方法
精度は低め

二相抽出法

調査しやすい項目をまず調査し、それを補助情報として調べたい項目の調査を行う方法(標本を抽出する操作を2度行う)

価格に関係して出てくる用語

ローレンツ曲線、ジニ係数

ローレンツ曲線:
分布を持つようなある事象の偏り(所得の偏りでよく出てくるので、その場合偏りが格差となる)がどの程度あるかを表す
完全に偏りがない場合、下図の点線(均等配分線という)で示す直線になる
ジニ係数:
均等配分線とローレンツ曲線で囲まれた面積の2倍が、縦軸・横軸で囲まれた面積に対して占める割合
0(ローレンツ曲線と均等配分線が一致)だと偏りがないことを示す 最大値は1となる

ラスパイレス指数

物価の変動を表す時によく使われる
下に示すように、基準時とそれと比較する時の価格の変化を基準時の数量をウエイトとして計算する

オッズ比

2つの群を比較した時に、起こりやすさを示す尺度
オッズ比が1だと2つの群の間に起こりやすさの差がないことを意味する

統計検定2級を受けた

先日、統計検定2級の試験を受けた。感触がいまいちだった1ので、結果が出る前にふりかえりを書いてしまおうと思う。(後日、運よく受かっていたことがわかった)

知識のついていく過程

基本的に、過去問2を解くことを中心に学習した。その合間に、公式のテキスト3を読んだり、理解が曖昧で何度も調べる部分をまとめたりした。
過去問の年度別に、正答を導けた割合の変化を時系列で見てみると下図のようになっている。

一応、どの年度も解き直しをする度に少しずつできる割合が上がってはいる。今見ると、解き直しでも8割程度しか取れていないあたり、理解や演習が足りていない様子が表れているなと思う。

大まかに次の3段階で理解が進んでいった。
– 初期:理解していることが自分の中で整理できていない状態。全体の5割は解説を読めば理解できたけど、3割くらいは解説を読んでも理解できなかった。
– 中期:公式テキストをざっと読んで自分の中で整理し、最低限公式的な部分は覚えてしまうことで、過去問の解説を読めばほぼ理解できるようになった。
– 後期:1度理解しただけでは自分で再現できないような、あやふやな部分を解き直した(が、おそらく身についてはいなかった)。

かけた時間

初見の過去問に対し正答を導けた割合と累積学習時間を重ねてみると、割と比例していることがわかる。おそらく、この分野に対する学習がまだ初期段階だから、やるだけ理解が上がっていく楽しい時期にいるんだと思う。

試験終了後に集計してみるまで、これほど時間を投入していたと思わなかった(合計で40~50時間の感覚だったのに、2倍くらい費やしている)ので、投入時間の割に自分の理解のレベルが低く感じ、数日悲しい気分になった。

いったいどの部分にこれほど時間がかかっていたのか?
体感だけど、過去問に下記の時間をかけていたと思う。


これに公式のテキストでの学習やまとめていた時間を足しても、おそらく60時間程度にしかならない。
約30時間が闇の中である4

統計検定の勉強による変化

良いこともあって、社内でやってもらっていた読書会5に使用していた本(統計学入門6)に対する見え方が変わった。
読書会中はだいぶ難しく感じて、「統計とかデータ分析やってる人ってみんな天才なのかな…?」「ていうか自分が向いていないだけか」と思ったし(今もこれはよぎる)、本に対し30回くらいは「いったい何言っているんだ…」と思った。統計検定の勉強をした後に見ると、理解できるようになっていて「かなりわかりやすく丁寧に書いてくれているな」「確かに入門の本だな」と思えるようになった(入門レベルだと判断できることと、その内容を完璧に理解できることはまた別ではあるが)。

学習へのふりかえり

今回、学習時間は記録していたけど、その中で何をしたかは記録していなかった。遊びとして記録していたから、最初にしっかり考えなかったけど、最後に何を見たいのかを考えて記録すべきだった。
今まで学ぶ量(時間)を増やそうという方向に意識が向かっていたと思う。でも時間は有限で、さほど若くないのに新たな分野を物にしようとするなら、何をしたのか、それに時間がどれだけかかったのか、もう少し自覚的になる必要があると思った。試しに1ヶ月、勉強した時間とその内容を記録してみようと思う。


  1. 自己採点だとほぼボーダーライン上だった。1〜2問足りずに落ちそうである(結果発表までどきどきが楽しめてお得だと思うことにする) 
  2. 日本統計学会公式認定 統計検定 2級 公式問題集/日本統計学会編 
  3. 改訂版 日本統計学会公式認定 統計検定2級対応「統計学基礎」/日本統計学会編 
  4. たぶん、理解できない部分についてずっと考えていたり、仕事帰りに勉強のため寄ったカフェで虚無状態になっていた時間が入っていると思われる 
  5. 読書会が相当役に立っていて、これがなかったら統計検定の勉強を途中で諦めていたと思う 
  6. 統計学入門/東京大学教養学部統計学教室 編 

デスクをホワイトボード化した話

ホワイトボード化までの思考回路

きっかけは、思いついたことをふせんにちまちま書いて壁に貼っていたことだった。
ふせんは狭い。ぼんやりと考えたこととかは切り捨てて内容をまとめ、収まるように調整してしまう。とりあえず書き出したい。余計なことを考えず、自由に書きたいのだ(幼児のお絵描きが画用紙を飛び出して壁や床に行われる時と同じ思考回路である)。

Amazonに壁に貼れるホワイトボードのシートがあったので、はじめはそれを机の横の壁に設置することを考えた。
でも、側面だと45度回転しないと書けない。新しいものを導入した時にちょっとした手間があると、結局活用しなくなってしまう。がんばらなくても使える状態にする必要がある。
どこならそのままの状態で目に入り、書けるか。

机の上だ。

作業内容

机の上にはキーボードや本を置いていたけど、物があるとインクがつくことを気にして自由に書けない。
そこで棚とキーボードの収納スペースを作って避難させた上で、机にホワイトボードのシートを設置した。


元の状態(見た目的にはこっちの方がすっきりしていて好き)
→ 改造後(なお、ホワイトボードに書いた計算は間違えている)

コストとしては、ホワイトボードシート・木材・金具類等で、7,000〜8,000円くらい。ホワイトボードシートだけだったら、2,000円しない。
散発的に進めていたけど、累計で、サイズを決めたり計画するのに3〜4時間、材料をネットやホームセンターで集めるのに5〜6時間(キーボードのスライド棚に使ったスライドレールについて調べるのに手間取った)、作業に4〜5時間くらいかかった。

やってみてわかったこと

  • 太いペンで、自由に書けるのは、気持ちいい。いらない紙に書いてその後捨てるのと原理的に変わらないはずなのに、それよりも書くことへのハードルが下がる。アイデアを考えるとか、何となく考えてることを発展させるとかいう用途に特に向いてるなと感じた。
  • キーボードは避難させたけど、ノートや本を広げる時は結局ホワイトボードの上になるので、それが不便。ノートを使うときには一旦消す必要があるな、とか、この書き込みはしばらく残しておきたいから端の方に書いておこう、とか本質と違うことを色々考えてしまう。そういう点では、壁設置に軍配が上がるなと思った。
  • 机が白いと、結構目が疲れる。天板が白い机って結構あるけど、白色度はあまり高くないのは、そういう理由もあるのかもしれない。
  • 線が太いので、文字や図が大きくなる。それが書くことの気持ち良さにつながっている部分もあるんだけど、この広さでも狭いなと感じてしまう。
  • 職場の机(白い)にナチュラルにペンで書こうとして、一人ではっとする。

まだ設置して間もないので、しばらく使ってみたら、新たな発見があるかもしれない。