備忘録とか日常とか

学んだこととかを書きます。

キーバインドをいじって全能感を手に入れる【AutoHotKey, ChangeKey】

「こいつらExcel好きすきだろ・・・」と思いながら日々仕事してます。

業務時間の大部分をExcelに費やしているわけが、

  • カーソル移動が煩わしい
  • よく使うショートカットが押しにくい

という問題があったため、改善してみた。
多くの操作をキーボードで完結させることができると、クソつまんない仕事のモチベーションも多少は上がる、と思いたい。

ここでは二つのツールを使用する。

レジストリレベルで設定する「Change Key」

forest.watch.impress.co.jp

レジストリを編集してキーバインドを変えられる。非常駐なのでリソース圧迫しない。
windows7対応となっているが、2019/4/21現在でwindows10でも使用できているので問題はない。
ただし念の為レジストリのバックアップはとっておいたほうがよいかも。

www.ask-mswin.com

ChangeKeyでは特定のキーを他のキーに割り当てられる。
管理者権限でChgKey.exeを実行すると、以下のような画面が表示される。

f:id:may46onez:20190421001453p:plain

ここから変更したいキーを選択し、次の画面でどのキーに変更するかを選択する。

f:id:may46onez:20190421001603p:plain

自分の場合は以下のように設定している。

  • CapsLock -> 左Ctrl
  • 半角・全角 -> Esc
  • Insert -> 右windows

CapsLockはほぼ必須、というか誤爆しやすい人は使わなくても変えておくと捗ると思う。
Esc多用するときは若干遠いので、半角全角キーを変更している。IMEオンオフはCtrl+Spaceで。
Insertはまあなくてもいいけど、右手でwindowsキー押せたらうれしい場面がたまにあるので。(キーボードに右windowsがない)

AutoHotKey」で理想のキーマップを手に入れる

autohotkey.com

ご存知キーボードリマップツール。実はマウスコマンドも設定できるらしい。可能性は無限大。
こちらは常駐型となるが、それでも余りあるメリットを享受できる。
インストール後に.ahkという拡張子で書かれたスクリプトを実行することで、
設定したキーマップを使用できるようになる。

自分の要望としては、
1. ホームポジションから動かずにカーソル移動したい
2. ファンクションキー遠いのでなんとかしたい
3. ホームポジションを崩す必要があるショートカットを変更したい
という要求をまず満たしたい。

方法としては、変換・無変換のキーと同時押しを割り当てることで各種機能を実現する。
そのため変換・無変換キーを他の用途に使うことはできなくなるが、
まあ普通の人ならほぼ使わないと思う。
自分は以前の記事でこれら二つのキーにIMEオン・オフを割り当てていたので、そこを根性で矯正した。

変換・無変換は本来の機能で使わなくなるので、IMEの設定で切っておくことをお勧めする。
f:id:may46onez:20190421003954p:plain

以下に自分のスクリプトを貼っておく

; ウィンドウを閉じる
!q::Send, !{F4}

; Chrome等、進む・戻る用設定
vk1D & ,::Send, !{Left}
vk1D & .::Send, !{Right}

; ウィンドウ移動
vk1C & Tab::Send, #+{Right}
; ウィンドウ操作
vk1C & i::Send, #{Up}           ; 最大化
vk1C & j::Send, #{Left}         ; 左寄せ
vk1C & l::Send, #{Right}        ; 右寄せ
vk1C & k::Send, #{Down}         ; 最小化

; 仮想デスクトップ切り替え
vk1C & f::Send, #^{Right}
vk1C & d::Send, #^{Left}

;無変換+jkil = 上下左右
;無変換+shift+上下左右 = shift+上下左右

;無変換+j→左
vk1D & j::
    if GetKeyState("shift", "P") && GetKeyState("ctrl", "P"){
        Send, +^{Left}
    }else if GetKeyState("shift", "P"){
        Send, +{Left}
    }else if GetKeyState("ctrl", "P"){
        Send, ^{Left}
    }else{
        Send, {Left}
    }
    return
;無変換+k→下
vk1D & i::
    if GetKeyState("shift", "P") && GetKeyState("ctrl", "P"){
        Send, +^{Up}
    }else if GetKeyState("shift", "P"){
        Send, +{Up}
    }else if GetKeyState("ctrl", "P"){
        Send, ^{Up}
    }else{
        Send, {Up}
    }
    return
;無変換+i→上
vk1D & k::
    if GetKeyState("shift", "P") && GetKeyState("ctrl", "P"){
        Send, +^{Down}
    }else if GetKeyState("shift", "P"){
        Send, +{Down}
    }else if GetKeyState("ctrl", "P"){
        Send, ^{Down}
    }else{
        Send, {Down}
    }
    return
;無変換+l→右
vk1D & l::
    if GetKeyState("shift", "P") && GetKeyState("ctrl", "P"){
        Send, +^{Right}
    }else if GetKeyState("shift", "P"){
        Send, +{Right}
    }else if GetKeyState("ctrl", "P"){
        Send, ^{Right}
    }else{
        Send, {Right}
    }
    return

; PageUp
vk1D & u::
    if GetKeyState("ctrl", "P"){
        Send,^{PgUp}
    }else{
        Send,{PgUp}
    }
    return

; PageDown
vk1D & o::
    if GetKeyState("ctrl", "P"){
        Send,^{PgDn}
    }else{
        Send,{PgDn}
    }
    return

; ファンクションキー置き換え
vk1C & 1::Send, {F1}
vk1C & 2::Send, {F2}
vk1C & 3::Send, {F3}
vk1C & 4::Send, {F4}
vk1C & 5::Send, {F5}
vk1C & 6::Send, {F6}
vk1C & 7::Send, {F7}
vk1C & 8::Send, {F8}
vk1C & 9::Send, {F9}
vk1C & 0::Send, {F10}
vk1C & -::Send, {F11}
vk1C & ^::Send, {F12}

; 右クリック
vk1D & 0::Send, +{F10}


ホットキー設定するだけなら単純で、::の左に登録したいキー組み合わせ、右にSend, と送信したいキーを書く。shift, ctrl, alt, windowsキーはそれぞれ修飾キーと呼ばれ、+^!#で表現する。二行目はalt + qalt + F4に置き換えている。vk1Cvk1Dはそれぞれ変換と無変換のキーを表す。


少々面倒だが、登録したホットキーと別のキーの同時押しは対応できないため、すべて登録しておく必要がある。上の例でいえば無変換+jキーで矢印左が入力できるが、shiftを押しながらの範囲選択はできない。なので::の後に場合分けを入れ、矢印キーと同時に修飾キーが押された場合をすべてホットキーとして登録している。


詳しい使用方法はwikiに書かれている。正直機能が多すぎて扱いきれる気がしない。
AutoHotkey Wiki
wikiのキーリストに各キーをスクリプト上でどう表現するかが書かれているが、少し内容が古いのでそこだけは注意。vkXXscYYみたいなキーはscYYを除いた部分を表記すれば動く。(Invalid HotKey みたいなエラーが出る)
詳細は下の記事を参照
qiita.com


自分好みのスクリプトが作れたらスタートアップに登録しておくとよい。
windowsキー+r で出る窓にshell:startupと入力し、出てくる場所に.ahkを置くだけで起動時にホットキーが有効になる。

後日談

これはこれで満足しているが、変換・無変換を多用するので使うキーボードが若干限られるという問題に直面した。スペースキーが長いものだと親指がちょっとしんどい。
realforceの新しいやつが欲しかったのにスペースキーが伸びてて悲しい・・・

「プログラムはなぜ動くのか」を読んだ

朝出社したら自分のデスクにおもむろに置いてあったので読んだ。

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

この「なぜ」シリーズは2001年から刊行されており、他にも「コンピュータはなぜ動くのか」「ネットワークはなぜつながるのか」等の書籍がある。
昔から入門とか情報工学の基礎知識を身に着けるために読まれている本らしい。

ネットワークはなぜつながるのか 第2版 知っておきたいTCP/IP、LAN、光ファイバの基礎知識

ネットワークはなぜつながるのか 第2版 知っておきたいTCP/IP、LAN、光ファイバの基礎知識



ちなみに「プログラムはなぜ動くのか」第二版の1刷は2007年なので、内容としてはかなり古い。メモリ512MBとかの時代。江戸時代かっつの。。
それでもアセンブリの基礎に触れる機会など、かなり限られた業種やニッチな趣味でもしてない限りないと思われるので勉強にはなった。ハード寄りな部分の挙動は昔から劇的に変化することもなさそうなので、多少古くても問題ないのかもしれない。

以下各章の内容とひとこと要約・感想。

1. プログラマにとってCPUとは何か

CPU内の大まかな構造とレジスタについての説明。
種々のレジスタがそれぞれどのような役割を果たし、条件分岐、繰り返し、関数呼び出し等がどのように実現されているかを簡単に解説している。突き詰めればCPUは非常に単純な処理しか行っていない、らしい。

2. データを2進数でイメージしよう

コンピュータは2進数ですべてを扱っており、そのほうが都合がいい理由などのお話。
情報系の分野に疎い人のための易しめな解説。

3. コンピュータが小数点数の計算を間違える理由

引き続き2進数の話、浮動小数点がどう表現するかなど。
情報系の分野に(略)

4. 四角いメモリを丸く使う

メモリを使ってどのようにデータを表現するか、というお話。
char, int, longのメモリ確保みたいなところから、配列の実現方法(スタック、キュー、リスト、2分木探索)等の説明。メモリーICの物理的な仕組みみたいなところも。pushとかpopとかqueueとか待ち行列とか、情報工学の基本用語なんかも解説してくれて情報量は多い。

C言語初学者はこの章を読むとポインタやメモリに関する理解が深まると思う。

5. メモリーとディスクの親密な関係

モリーに対して、ディスクはどのような役割を担うかみたいな話。
ディスク上のデータはメモリに一度格納されてからしか使えないという原則から、キャッシュ、仮想記憶(ページング方式・セグメント方式)、DLL、スタティックリンク、ディスクの物理構造(セクタ単位で扱われる)など。

中でも_stdcall呼び出しというのは初めて知った。

スタックはさまざまな場面で再利用されるメモリー領域なので、使い終わったら元の状態に戻す処理が必要になります。

このスタックのクリーンアップ処理を、何度も呼び出される関数の側で行うようにすれば、呼び出す側で行う場合よりもプログラム全体のサイズを小さくできます。

通常mainで呼び出した関数のクリーンアップ処理は呼び出し側で行われるため、何回も同じ関数を呼び出すとその分だけクリーンアップ処理部分のコードが冗長になる。じゃあ関数側に処理を書いたら節約になるじゃん、ということらしい。

計算資源の制約が厳しかった時代の涙ぐましい努力が伺えて面白かった。今でも組み込みとかでは使うのか…?
どちらかというとコンパイラとか作る人のための知識な気がする。

6. 自分でデータを圧縮してみよう

急に毛色が変わって圧縮の話。
ランレングス法、ハフマン法のアイディアから基本アルゴリズムの説明と、可逆・非可逆圧縮等の説明。
普段使っているzipのLHA圧縮がハフマン法の応用だと初めて知った。こういうの思いつける人は天才だと思う。

7. プログラムはどんな環境で動くのか

OSの偉大さを説く話。
APIを提供してくれることでハードに依存しないプログラムを作成できるということを、歴史的経緯と一緒に説明してくれる。MS-DOS時代って大変だったんだね。

さらにJava仮想マシンBIOSについても少し触れてる。Javaコンパイルバイトコード生成→OS毎のJava仮想マシン→ネイティブコードという流れでOSの違いを吸収する、とか。
ブートローダの語源がまじのブーツだったとは思わなかった。

8. ソース・ファイルから実行可能ファイルができるまで

コンパイラによって実行可能ファイルができるまでの流れの話。
ソースをコンパイルして.objファイルができ、スタートアップやライブラリからも.objを引っ張ってきてリンクして出来上がり。
実行時にDLL呼び出して使うとか。

再配置情報、スタック、ヒープなんかもこの章で解説がある。

9. OSとアプリケーションの関係

ハードを意識せずプログラムを作成できるのはOSのおかげだよ、という話。
また、Windows OSの基本機能(マルチタスクとかデバイスドライバの自動設定とか)についてちょっとだけ説明。

10. アセンブリ言語からプログラムの本当の姿を知る

Cのソースとアセンブリを見比べて、CPUが実際はどのような挙動をしているのかを見る話。この本で最も内容が濃い章かもしれない。
1章でさらっと触れた分岐や関数呼び出しの仕組みを、アセンブリ言語を追いかけながら実際に理解を深めることを目的としており、勉強になる。

研修で実際のアセンブリ(といっても超単純なプログラムだが)を見る機会はあったが、意味不明すぎて結局ちゃんと見ずにCのソースしかいじってなかった。
もう少し早く読んでおけば学ぶこともあったかもしれない。

11. ハードウェアを制御する方法

CPUやメモリと違い、外部接続のハードウェアがどのように制御されているかという話。
IN、OUT命令とポートを使ってデータのI/Oを実現している。加えて、割り込み処理やDMA(Direct Memory Access)などを解説。
組み込みの基本みたいなとこか。

12. コンピュータに「考え」させるためには

プログラムに人間的な考えや勘を盛り込んでみよう、というお話。(この章は若干の蛇足感がある…)
じゃんけんプログラムをつくるのだが、括弧つきで書いてるように、実際に考えてるわけでない。あくまで「それっぽく」動くようにするために経験に基づいてif-thenルールをひたすら盛り込んでいる。

今のAI時代に下手に「コンピュータが考える」とか「知能を実装する」とか言っちゃうと、「知能とは何か」みたいな話まで行ってしまうので、まあ昔の本ということでご愛敬である。

終わりに

教養として知っておいて損はない内容が多かった。
アセンブリ言語に興味を持つ入り口にはなるかもしれない。自分はちょっとしんどいので無理ですごめんなさい。
入門書的な扱いだが、全くIT知識のない人が読むには少々つらいかもしれない。
大学一年の電気・情報系学部の教養の授業とかで扱うといいんじゃないかな。と思いました。

応用情報技術者試験に合格したので振り返り

タイトルの通り、合格できました。午前90点、午後73点という結果に。
「わーい」と手放しに喜ぶこともできるけど、実際本番でかなり焦った部分などあるので今後のために振り返り。
しょうもない新人研修でPDCAがどうとか教わったので、それになぞらえてみる。

あくまで自分はこうだったという振り返りでしかないので、合格法を指南するものではないです。参考程度に。

結論

  • 参考書はリファレンス程度に使用し、過去問をメインで解く(なんなら参考書買わなくてもいい)
  • 午後問題はすべての分野を一問は解いてみる(そのあと絞る)
  • 午後試験過去問を、問題選択する時間を入れて本番の制限時間で解く
  • 外乱に動じない鋼の心を持って試験に臨む

Background

  • 高校からずっと理系
  • 学部で電気電子、大学院で情報工学を専攻
  • 暗記はあまり得意ではない
  • IPA試験の受験経験なし、いきなり応用情報受けてみる

自分のバックグラウンドはこんな感じ。
「基本情報はだいたい受かるし応用受けたほうがいいで」という先輩の挑発(?)にまんまと乗せられ、いきなり応用を受けてみた。人によっては応用のほうが受かりやすいという話も聞くが、午後試験が記述になるので自分には難しく感じた。


ネット上には「〇〇時間で合格!」や「午後対策はしなくていい」等の記事が散見されますが、ぶっちゃけ人によります。
がっつり業務でシステム構築等に携わっている人や情報科学出身の人は簡単に感じるかもしれないし、文系未経験SEの人には難しく感じるかもしれない。自分は普通に午後対策しました。

他の資格試験と比べても受験する層が幅広いと思うので、余計難易度は測りにくいと思う。難易度に関する評判はあまりあてにしないほうがいい。

Plan

  • 試験の約三か月前に応募、書籍購入
  • 午前対策二か月、午後対策一か月 を目安に
  • 午前・午後いずれも過去問から取り組む(重要)
  • 一日0.5 - 1時間くらい、週5時間以上を目標に


実は学生の時に一回受けようとして参考書を買ったのだが、5ページくらい読んで本棚の肥やしになっていた。
「お前それ計画破綻しとるやんけ」と言われればその通りなのだが、
ここから得られた知見が一つ

参考書を最初から読んで勉強するのは無理

ということ。
当時買ったのは公式の参考書(ただし平成28年度版)↓

平成31年【春期】/01年【秋期】応用情報技術者 合格教本

平成31年【春期】/01年【秋期】応用情報技術者 合格教本

出題範囲の各分野を網羅的に紹介・解説しており、これまでの出題傾向から最新のトピックなども取り上げられているらしい。これを読めば試験対策は万全…と思ったが、よほど忍耐力がないと半分も読み切れない。それくらい応用情報は出題範囲が広い。

なので何もわからない状態でも過去問から取り組み、間違えた問題を覚えていくという手順のほうが知識が身についた実感があるので捗る。
参考書はわからない部分を調べる辞書的な使い方が望ましいと考え、そのように運用した。というか最後らへんはググって調べてたのでもはや買う必要もないかもしれない。。

午前対策

午前の過去問は以下のアプリと過去問道場で、毎日通勤時間(40分くらい)問題を解く。
アプリは有料だが解説が見やすい&UIがシンプルで使いやすくておすすめ。
収録問題数が若干少ないので、ある程度覚えたら過去問道場でさらに固めるとなおよい。

応用情報技術者 午前 一問一答問題集 - Appliv
応用情報技術者過去問道場|応用情報技術者試験.com

午後対策

ネットで評判の緑の本で勉強。解説が充実している&問題数が多いのでおすすめ。

2019応用情報技術者午後問題の重点対策 (重点対策シリーズ)

2019応用情報技術者午後問題の重点対策 (重点対策シリーズ)

Do

  • ひたすら隙間時間で午前問題を勉強
  • 選択問題はすべての選択肢について理解することに努める
  • 午後試験は最初から問題を絞って勉強(ダメなパターン)


隙間時間って大事で、朝家出る前に10分、通勤片道で40分、昼休みに10分、とするだけで1時間使える。午前問題なら解説読む時間含めても一問3,4分程度なので、毎日15問は解ける。継続は力なり。

午前の4択問題も、全ての選択肢に対して説明できるくらいまでやりこむのがベスト。言わずもがなだが答えだけ覚えるのは良くない。
正直午前だけなら丸暗記でも解けなくはないが、午後試験に使える知識が身につかないのでやめたほうがいい。午前午後は出題形式が違うだけで範囲や深度は同じようなものです。

午前試験に関しては上記やり方でひたすら実行したが、午後試験対策の三つめはあまり良くなかった。後述。

Check

  • 午後の問題選択にてこずった
  • 一問に長く時間を使いすぎた(アルゴリズム)
  • その結果、解答欄を間違える凡ミスを犯した(アルゴリズム)
  • 外乱に集中を乱された

午前試験

午前に関しては特に問題なく解けた。
パッと見わからなくても、頑張って考えて2択ぐらいまで絞り込んだ結果正解した問題も多かったので、
「諦めたらそこで試合終了ですよ」の精神を胸に秘めておくとよい。

午後試験

午後は選択問題であり、10問ある中から最も点を取れそうな分野の問題を4問選ぶ。(+必答問題一問で計5問解く)
自分はハナから5分野(アルゴリズム、システムアーキテクチャ、ネットワーク、組み込み、情報システム開発)に絞って勉強し、本番で4つ選ぼうとしていたのだが、その考えは甘かった。本番で解けないとめちゃくちゃ焦ってしまう。

個人的な感想でいうと、ネットワークは前提とする知識が多いため高得点が難しいように感じる(業務で扱っている人は余裕だと思う)。本番で「あ、これ無理」ってなったので、必然的に勉強していた残り四つの分野に取り組むことになるのだが、システムアーキテクチャも問題文が全然頭に入ってこない。
苦し紛れにプロジェクトマネジメントを解いて結果事なきを得たが、午後は得点率が公表されないので真相は闇の中である。。

得意としていたアルゴリズムも焦りのせいですんなり解けず、他問題を手早く済ませて時間ギリギリまで粘ってやっと解けたが、小問の解答欄を間違えるという凡ミスを犯した。焦りは本番最大の敵である。


こういう不測の事態のためにも、全ての分野の問題を一度は解いておくことが大事だと感じた。
あと各分野の勉強はしても、自分が得点できそうな分野の問題を時間内に見極める訓練もあったほうがよく、そのためには一度時間を図って午後問題を解いてみることをおすすめします。


余談だが、本番になるといろんなものが集中力を削いでくる。具体的には開始30分でいびきかいて寝るやつとか、尋常じゃないスピードで退出していくやつとか(たぶんどっちも問題解けてない)。
そういった外乱に惑わされずに問題を解く心構えをしておくと良いかもしれない。

Action

  • 高度区分試験受ける…?

高度試験まで受けるかどうか未定だが、応用情報取得後2年くらいは高度区分の午前試験が免除になるので、受けてみてもいいかもしれない。でもあんまり旨みを感じない…


以上、駄文を読んでいただき感謝します。これから受験予定の方は頑張ってください!

中古車購入に向けてのデータ分析をしてみたかった【python, scraping】

中古車を対象としたなんちゃってデータ分析をしてみた。以下の本を読んでなんか作ってみたくなったので。n番煎じなのは気にしない。

本のレビューもそのうち書くかもしれない。

動機

若者の車離れが叫ばれて(?)久しいが、自分としては昔からマイカーは憧れの象徴なので車が欲しい。 当然お金はないので中古車を買うことにはなるが、中古車は様々な要素から価格が決まってくる。

  • 年式、走行距離

  • 修復歴あり or なし

  • カーナビなどオプションの有無

値段に強く影響しそうなのはこの辺な気がする。車の素人が購入を判断するための材料が欲しいので、色々データをいじってみる次第である。 まずはwebサイトから各種情報をとってくるところから始める。

スクレイピング

ざっくり流れ

requestsでHTTP接続し、 lxml.htmlでパースする。今回はログインなどはせず、ステートレスなクロールを行うので特に複雑な処理は発生しない。取得したデータはpymongoでMongoDBに保存する。 MongoDB初めて使ったけど便利だなこれ。

カーセンサーのページを対象とする。カーセンサーは特定の条件で検索すると、その条件に合致した中古車の一覧ページが生成され、その一覧から気になった車をクリックすると その車の詳細ページを閲覧できる。なので「一覧ページを取得 -> 詳細ページのhtml取得 -> スクレイピング」という手順となる。

ソースコード

コードは以下。

github.com

動作環境は

  • MongoDB v4.0.1
  • pymongo 3.7.1
  • scikit-learn 0.20.0
  • statsmodel 0.9.0

ぐらいあれば動くと思う。

car_scrape.pyスクレイピング実行してMongoDBにデータ保存するスクリプト。実行時にコマンドライン引数で車種を指定する。とりあえずPRIUS, VEZEL, FITに対応しているが、URLを追加すれば他の車種も取得可能。 車一台につき固有のIDを割り当てて車両情報と一緒にDBに保存し、次にスクリプトを実行するときはIDが未登録の車両のみをDBに追加することができる。新たに追加された中古車を登録するのに便利な設計(ただし 車両価格更新には対応できない...)。オプション引数に--reset_dbを指定すると、MongoDBのcollectionを削除して一からスクレイピングを行う。

中身はえらいごちゃごちゃしてしまったが、CSS selectorとスクレイプのとこが冗長になってしまっただけなので、処理自体は単純。scrape_list_page()で一覧ページを読み込み、その中でscrape_detail_page()を順次呼び出して 詳細ページのhtmlを取得する。また今回は車種だけ指定し、その他の条件(グレード等)は特に指定せずにスクレイピングする形をとった。

visualize.ipynbでは取得したデータの可視化を行い、analyze.ipynbで簡単な分析ごっこをしてみた。後述。

可視化してみる

カーセンサーに掲載されている中で数が最も多い普通車がプリウスっぽかったので、プリウスについて調べた。

年式 - 本体価格

まずは年式と本体価格の関係を散布図で見てみる。 f:id:may46onez:20181106080957p:plain

横軸のmodel_yearは年式、縦軸のbase_priceは車両本体価格(単位:万円)となっている。赤い線とプロットは各年代の中央値を示している。まあ当然っちゃ当然だが、年式が新しいほうが価格が高い。そしてパッと見、分布の重心は価格の低い方に位置しており、高価な車両はまばらに分布していることがわかる。これはカスタムすればするだけ車は高くなるので、正規分布より若干ロングテール気味になるからであると思われる。

上の散布図を見ると、2015年から2016年で価格が大きく上がっているように見える。確認のために本体価格のヒストグラムをプロットしてみる。 f:id:may46onez:20181107080511p:plain

これを見ると明らかに山が二つあり、年式の違いで価格に大きな隔たりがあることが確認できる。もうちょい詳しく見たいのでプロットする年式を絞ってみる。 f:id:may46onez:20181107081302p:plain

やはり2015年と2016年の分布に隔たりがある。中央値だと50~60万円ほど変わってくるか。実はプリウスは2015年12月にフルモデルチェンジされており、その影響で旧モデルとの価格差が顕著になっている。 ちなみにその前のフルモデルチェンジは2009年5月であり、2008 ~ 2009年でも価格が見受けられる(ただし価格差は直近のモデルチェンジよりは控えめ)。要は旧モデルは露骨に安くなることがわかったので、特にこだわりがなければモデルチェンジ後の車両なら比較的安く手に入れられる。逆にモデルチェンジが発表された車種はちょっと購入を遅らせてみてもよいのかもしれない。

走行距離 - 本体価格

次に走行距離を見てみる。ネットで中古車購入の手引き的なページを見ると、

走行距離〇万km(3万, 5万とか)を超えると値段がかくっと下がる

といった記述が散見される。4.9万kmと5.1万kmでは心理的なハードルが違ってくるかららしい。まあ何となくそんな気もしなくはない。

確かめるために、横軸に走行距離distance(単位:万km)、縦軸に本体価格base_priceをとり、年式ごとにプロットする。

f:id:may46onez:20181111214842p:plain

色が赤に近づくほど年式が新しくなる。年式は新しいほど価格が上がり、走行距離が伸びると価格が下がるといったことが読み取れる。まあ当たり前だけど。

色が重なりすぎて見づらいので2015年以降でプロット f:id:may46onez:20181111215728p:plain

最新の年式は試走等の目的で使われた後中古に流れたいわゆる新古車が多く、ほぼ0kmのものが多い。カスタム内容を選ばなければ意外と安く最新モデルが手に入りそうだ。 そして前述の通り16年と15年には価格の隔たりがある。16年は6万kmを超えたあたりから価格の下落が大きくなっているような気がする。

ついでに2011年 ~ 2015年もプロットする。

f:id:may46onez:20181111221915p:plain

この辺の年式は結構値段が似通ってるので、分布ががっつり重なってくる。11年は若干安いくらいか。こうしてみると、どうせ値段同じくらいなんだし14年の車買ったほうがお得なんじゃないかと思えてくる (他の条件を無視しているので実際はわからん)。 そして肝心の、「〇万km超えると安くなる説」はこの図からはほぼ読み取れない。実際全く同じ色、グレード、カスタムの車種を比べると気持ち〇万km超えのほうが安くなるのかもしれないが、 中古市場において全く同条件の車両が出回ることはほぼあり得ないので、走行距離の〇万kmはほぼ意識しなくてよいと結論付けることにした。そこにこだわるくらいなら別のとここだわったほうが良い。

分析あれこれ

なんかデータ分析っぽいことがしてみたかったので、やってみた。

特徴選択

車両の価格に影響しそうな情報として、オプションがどの程度ついているかという情報を取得した。

f:id:may46onez:20181127224312p:plain
サンプルのオプション例

走行距離、年式に加えて上記のオプション情報を使用して、車両価格を推定する。これらは全て二値のダミー変数として扱う。 カーナビは種類が複数あり(CD, DVD, HDD, メモリナビ)、面倒なのでHDD、メモリナビは1, それ以外は0とする。同じ理由でヘッドランプもディスチャージドランプとLEDランプはまとめて1とする。オーディオはカーナビと 一体となっている車が多いので使用せず、他にも3列シートとかウォークスルーとか車種に依存しそうな情報は抜きにした。あとは禁煙車などを加味して、結果的に全部で22項目を使用することになった。 走行距離、年式は平均0, 分散1になるように正規化する。

まずはどの特徴が価格に大きく寄与しているのかを調べる。 特徴選択の方法は種々あって、重回帰分析でよく利用されるのは変数増減法とからしい。scikit-learnにもいくつか用意されている(指定した評価指標で上位k個を選択するSelectKBest, 変数減少法のRFE, モデルから算出される 重要度や係数を使用するSelectFromModel)。

今回はscikit-learnのRandomForestRegressorを使用し、feature_importance_を 算出して重要度が大きい順に特徴を選択してみる(それぞれの特徴が相対的に見てどれくらい重要度が高いかをみたかったので、ここではSelectFromModelは使用しなかった)。 RandomForestはブートストラップサンプルに対して決定木を学習させるアンサンブル手法で、out-of-bag誤り率を算出することで特徴の重要度を図ることができる 手法である。このサイトとかはじパタとか読めばわかりやすいと思う。

以下analyze.ipynbのコードの一部を抜粋
念の為データをtest と trainに分けて、trainデータのみで特徴選択をしている。

rf = RandomForestRegressor(n_estimators=200, random_state=50)
rf.fit(x, y)

fi = rf.feature_importances_
result = []
for i, label in zip(fi, x_data_df.columns):
    result.append((i, label))
    
result.sort(reverse=True)
v = []
for i, label in result:
    print("{0:15}  {1:0.6}".format(label, i))
    v.append(label)

出力は以下

model_year       0.836925
distance         0.0789052
lowdown          0.0168161
aero             0.00976041
repare           0.00806706
cruise           0.00589656
auto_brake       0.00579045
record_book      0.0051595
sheat_heater     0.00469167
camera           0.00400534
ETC              0.00370045
anti_theft       0.00357377
navi             0.00314469
AS_sensor        0.00273345
ESC              0.00260764
parking_assist   0.00197162
cold_area        0.00177578
smartkey         0.00158297
alumi_wheel      0.00135012
keyless          0.000664804
ABS              0.000528251
sheat_air        0.000165102
around_camera    0.000112198
liftup           7.20092e-05

重要度降順結果。年式、走行距離に続いてローダウン、フルエアロ、修復歴、クルーズコントロール、衝突被害軽減ブレーキ・・・と続いていくことがわかる。 ローダウンとフルエアロあたりが装備されている車は前オーナーが車好きである可能性が高く、そういう車はほかにもいっぱいオプションがついてたりするので価格が高くなる傾向にあるのかもしれない(未確認)。 その次が修復歴なので、やっぱり事故車は価格にかなり影響するのだなあと実感。

次に、適当にモデルを構築してみる。重要度上位10個の特徴を使用した場合と全て使用した場合について、三つのモデル(線形回帰、RandomForest, SVR)でcross validationしてみた。

以下コード抜粋

# cross validationで各モデルを学習・評価
# 評価指標には adjusted r2
# linear regression, random forest, svrを使用

x2 = x_data_df.loc[:, v[:10]].values
linear_reg = linear_model.LinearRegression()
rf = RandomForestRegressor(n_estimators=200, random_state=50)
svr = SVR(gamma='scale')
num_y = y.shape[0] // 10

lr_scores_sub = cross_val_score(linear_reg, x2, y, cv=10, scoring='r2')
rf_scores_sub = cross_val_score(rf, x2, y, cv=10, scoring='r2')
svr_scores_sub = cross_val_score(svr, x2, y, cv=10, scoring='r2')
lr_scores_all = cross_val_score(linear_reg, x, y, cv=10, scoring='r2')
rf_scores_all = cross_val_score(rf, x, y, cv=10, scoring='r2')
svr_scores_all = cross_val_score(svr, x, y, cv=10, scoring='r2')

lr_adr2_sub = adjusted_r2(lr_scores_sub, num_y, x2.shape[1])
rf_adr2_sub = adjusted_r2(rf_scores_sub, num_y, x2.shape[1])
svr_adr2_sub = adjusted_r2(svr_scores_sub, num_y, x2.shape[1])
lr_adr2_all = adjusted_r2(lr_scores_all, num_y, x.shape[1])
rf_adr2_all = adjusted_r2(rf_scores_all, num_y, x.shape[1])
svr_adr2_all = adjusted_r2(svr_scores_all, num_y, x.shape[1])

for a in [lr_adr2_sub, rf_adr2_sub, svr_adr2_sub, lr_adr2_all, rf_adr2_all, svr_adr2_all]:
    print('mean:{0:.5}    std:{1:.5}'.format(np.mean(a), np.std(a)))

相変わらず見づらいクソコードである。結果は以下

mean:0.87579    std:0.0078976
mean:0.90187    std:0.0049071
mean:0.89518    std:0.0054207
mean:0.87769    std:0.0073689
mean:0.90777    std:0.0063641
mean:0.89033    std:0.0057512

10 foldのcross validationで、平均と標準偏差を算出した。上三つが特徴量10個、下三つが全特徴使用。上から順に線形回帰、RandomForest、SVR、・・・の結果となる。

ここでは特徴の数が多いほうが精度が少し上がっているように見える。線形回帰においては多重共線性のある特徴が含まれていると精度が下がるはずなので、

  1. 評価指標に問題があった
  2. 特徴同士の相関が低かった

が原因かと思った。変数が増えるとモデルの近似がうまくいってなくてもR2は勝手に増加するそうで、それを調整したのがadjusted R2なのだが、なにか使い方に問題があったのかも知れない… 。 まあお遊びなのでそこまで厳密にする必要はないのだけれども。

特徴同士の相関が低い、とは思えない。なぜならスマートキーがついてるのにキーレスじゃないとかありえないし、衝突被害軽減ブレーキがついてたら障害物センサーもついてるんじゃないの?と思うから。 というか大半がダミー変数の場合の回帰ってうまくいくのだろうか。本職の人はどう判断するのか気になります。

あと特徴選択するなら各fold毎に重要度を求めてやらないと意味ないのだが、ここはめんどくさくてさぼってしまった。これをちゃんとしたら案外うまくいくのかもしれない。

statsmodelによる分析

statsmodelなるライブラリを使うと回帰分析のsummaryがすぐ見れちゃうとのことなので使ってみた。 先ほど選択した特徴量を入力として全データに対して回帰分析してみる。

以下コード抜粋

x_sm_sub = sm.add_constant(x_data_df.loc[:, v[:10]])
model = sm.OLS(y, x_sm_sub) 
results = model.fit()
print(results.summary())

結果

                            OLS Regression Results                            
==============================================================================
Dep. Variable:                      y   R-squared:                       0.878
Model:                            OLS   Adj. R-squared:                  0.878
Method:                 Least Squares   F-statistic:                     5791.
Date:                Mon, 26 Nov 2018   Prob (F-statistic):               0.00
Time:                        23:46:59   Log-Likelihood:                -36688.
No. Observations:                8070   AIC:                         7.340e+04
Df Residuals:                    8059   BIC:                         7.348e+04
Df Model:                          10                                         
Covariance Type:            nonrobust                                         
================================================================================
                   coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------
const          121.1046      0.553    219.193      0.000     120.022     122.188
model_year      40.0663      0.351    114.053      0.000      39.378      40.755
distance       -17.3005      0.326    -53.136      0.000     -17.939     -16.662
lowdown         22.0338      1.130     19.507      0.000      19.820      24.248
aero            16.5364      1.067     15.504      0.000      14.446      18.627
repare         -14.6507      0.832    -17.604      0.000     -16.282     -13.019
cruise          10.7129      0.690     15.536      0.000       9.361      12.065
auto_brake      14.4923      0.990     14.633      0.000      12.551      16.434
record_book     -4.2196      0.534     -7.905      0.000      -5.266      -3.173
sheat_heater    24.9746      1.023     24.423      0.000      22.970      26.979
camera           1.2611      0.593      2.126      0.034       0.098       2.424
==============================================================================
Omnibus:                     1373.771   Durbin-Watson:                   1.990
Prob(Omnibus):                  0.000   Jarque-Bera (JB):             4903.483
Skew:                           0.835   Prob(JB):                         0.00
Kurtosis:                       6.434   Cond. No.                         7.58
==============================================================================

Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

いっぱい数字が出てきて焦る。coefとかはわかるとして、横に続くのはt値、p値、95%信頼区間か。上段はR2, adjusted R2, F値、対数尤度なんかも出してくれている。この辺をすべて理解して使いこなすには もう少し統計の勉強が必要である。statsmodelsの公式ドキュメントはこちら

これを見ると、まあ年式が一番値段に影響してて、次いでヒートシーター、ローダウン、衝突被害軽減ブレーキ、クルーズコントロールか。走行距離と修復歴にはしっかり負の相関が確認できる。 修復歴があるのとないのとでは平均で14.6万円程変わってくるが、これを高いとみるか安いとみるか。修復歴ありの車は骨格やシャーシにダメージがあるほどの事故があった車であり、そういった車はドアの開閉や 直進性など何かと問題が出てくることが多い(某知恵袋から得た知識)。軽微な事故車もあるらしいが、素人には到底見分けはつかないので十数万円ケチって命を乗せる車を買うくらいならおとなしく年式を下げたほうが良い。 まあ調べたらどのサイトでも同じことを言ってるけど、具体的な数字が出せたのはちょっとうれしい。

ただこの結果の中で、定期点検記録簿の有無に負の相関がある理由はわからない。ダミー変数だらけで、車両価格を近似するのに説明変数(というか情報量)が足りなかったとか?迷宮入りだ… 。

まとめ

  • フルモデルチェンジ後の旧モデルは価格が大幅に下落
    プリウスの場合は中央値で約50万円ほど変わる
  • 「〇万km超えたら値段が下がる!」は嘘
  • 修復歴ありとなしでは平均で約15万円の価格差
    修復歴ありはやめといたほうがいい

こうしてみると案外当たり前のことしか発見できてなかったりする。。でも色んな記事を見てると、データ分析をしても新たな知見が得られないことは往々にしてあることらしく、だからこそ本職のデータアナリストの方々は 最初の目的(何を明らかにしたいのか)をしっかり決めるんだろうなあ、と素人ながらに思った。

大変だけど面白そうな仕事で憧れます。おわり。

pathlibとかいう優秀すぎる標準ライブラリ(python)

自分は今までpythonでファイル操作を行うときはos.pathとかglobとかを使っていたが、pathlibという優秀なものがあると聞いて使ってみた。
python3.4以降なら使わない手はない。

以下優れている点

  • os, glob の組み合わせで行ってきた操作が大体pathlib一つでできるようになる
  • メソッドチェーン的に記述でき、可読性が上がる
  • open()などのファイル読み込み系の操作と親和性が高い
  • windowsだとファイルパスにエスケープ二つ\\入れる必要があるが、/区切りで解釈してくれる

以下もうひとつな点

  • 一部ライブラリではPathオブジェクトを受け付けず、str()でキャストする必要がある
  • copyなど一部未対応のメソッドがある

公式ドキュメントはこちら

主な使用方法

import, インスタンス生成

基本はPath オブジェクトを生成して操作することになる。
インスタンス生成時にファイルまたはディレクトリのパスを指定する。(絶対パスでも相対パスでもよい)

>>> from pathlib import Path
>>> p = Path('aaa/bbb/ccc.txt')

pはPathのサブクラスであり、windowsならWindowsPathlinux, maxならPosixPathオブジェクトとなる。
使い方はほぼ同じなので気にする必要はない。

引数を指定しないとカレントディレクトリのリンクが自動的にパスに入る。

>>> Path()
WindowsPath('.')

カレントディレクトリ、ホームディレクトリ取得

# カレントディレクトリ取得
>>> p.cwd()

# ホームディレクトリ取得
>>> p.home()

上記メソッドは新たなパスが格納されたPathオブジェクトを返す。
これに限らず、Pathオブジェクトのメソッドは新たなPathを返すため、文字列を直に操作する必要がなくなる。

また、上記メソッドの出力はインスタンス生成時の引数に依らず、常に同じである。
Pathオブジェクトならいつでもカレント、ホームディレクトリを呼び出せる。

絶対パス取得

まずは絶対パスを取得する。

# シンボリックリンクを解決して絶対パスを返す
>>> p = Path('.')
>>> p.resolve()
WindowsPath('D:/project/path_test')

# なんかこっちでもいけるっぽい
>>> p.absolute()

公式ドキュメントには書いてなかったが、p.absolute()でも同じことができるっぽい。内部では同じメソッドを呼び出しているのだろうか。
コードの中身を見てないのでわかりません。

ついでに絶対パスかどうかの確認

>>> p.is_absolute()
True

絶対パスならTrueを返す。

ファイル名、拡張子の取得

ファイルまでのパスがPathオブジェクトに入っているなら以下でファイル名を取得できる
拡張子や、拡張子を除いた名前部分(stem)を取得することもできる。戻り値は文字列なので注意

# ファイル名取得 戻り値は文字列
>>> p.name
'aaa.txt'

# 拡張子取得  戻り値は文字列
>>> p.suffix
'.txt'

# .tar.gzみたいなのは.suffixes でリスト取得できる
>>> p.suffixes
['.tar', '.gz']

# 拡張子を除いたファイル名を取得 戻り値は文字列
>>> p.stem
'aaa'

ファイル、ディレクトリ削除、権限変更

パスがファイルかディレクトリかでメソッドが変わる。

# ファイル削除
>>> p.unlink()

# ディレクトリ削除 ただし空ディレクトリに限る
>>> p.rmdir()

# 権限変更 ファイル、ディレクトリ問わず適用可能
>>> p.chmod(755)
>>> p.cwd(755)

ディレクトリが空じゃない場合はおとなしくshutil.rmtreeとかを使う。

ディレクトリ取得

便利。

# 親ディレクトリ取得 戻り値はPATHオブジェクト
>>> p.parent

パス結合

これが一番便利かもしれない。
Pathオブジェクトでは/演算子がパスの結合として再定義されている。

# Pathオブジェクトに文字列で除算するとパス結合されたPathオブジェクトが返る
>>> Path('a/b/c') / 'd'
Path('a/b/c/d')

#  windowsでのエスケープ二つと混ぜても同じ結果が得られる
>>> Path('a\\b\\c') / 'd'
WindowsPath('a/b/c/d')

# 一応メソッドもある
>>> Path('a/b/c').joinpath('d')
WindowsPath('a/b/c/d')

ファイル一覧取得、検索

指定したパス内のファイル、ディレクトリを一覧表示・検索できる。
検索に関しては、標準ライブラリのglobと使い方がほぼ同じである。
ただし、いずれもメソッドの戻り値はジェネレータであることに注意する。

# パス内のファイル、ディレクトリ一覧のジェネレータを返す
>>> p.iterdir()
<generator object Path.iterdir at 0x00000197DA6D3BF8>

# パス内の.py ファイルを検索、ジェネレータを返す
>>> p.glob('*.py')
<generator object Path.glob at 0x000002804AEC6C50>

# リストとして取り出したければキャストする
>>> list(p.glob('*.py'))

# パス以下を再帰的に検索したい場合は以下のように指定する
>>> p.glob('**/*.py')

ファイルを開く

Pathオブジェクトにはファイルを開くメソッドも用意されている。
テキストとして開くならこれで問題ない。当然だがパスはファイルが指定されている必要がある。

# ファイルを開く
>>> f = p.open('r')

# 組み込み関数にPathオブジェクトを渡すこともできる
>>> f = open(p, 'r')

注意すべきは、Pathオブジェクトに対応していない外部ライブラリも存在するということ。
ここによると、どうやらopenCVcv2.imreadには対応していないらしい。

そんな時は文字列にキャストしてからライブラリに渡せば問題ない。

>>> p = Path('aaa/bbb/ccc.txt')
# str()でキャストできる  UNIX環境
>>> str(p)
'aaa/bbb/ccc.txt'

# windows 環境でも使える
>>> str(p)
'aaa\\bbb\\ccc.txt'

# .as_posix()でもできるが、windows環境下でも / で区切られる
>>> p.as_posix()
'aaa/bbb/ccc.txt'

as_posix()は / で区切られた文字列が返されるため、windows環境では使用できない。
じゃあwindows用のメソッドがあるかと思いきや用意されていないっぽいので、環境に依存したくなければstr()を使うようにすべきか。

判定系

以下のメソッドはTrueかFalseで判定を返す。

# 絶対パスかどうかを判定
>>> p.is_absolute()

# 現在のパスが与えられたパターンと一致するかどうかを判定 (パターンはglob形式で指定)
>>> p.match('a/*txt')

# パスのファイル、またはディレクトリが存在しているかどうかを判定
>>> p.exists()

# ディレクトリかどうか判定
>>> p.is_dir()

# ファイルかどうか
>>> p.is_file()

# シンボリックリンクかどうか
>>> p.is_symlink()

# Pathオブジェクトが指定するファイルと引数で指定したファイルが同じファイルを参照しているかどうか
# 引数にはPathオブジェクトか文字列をとる
>>> p.samefile(other_path)

ファイル名変更など

# 指定したファイルまたはディレクトリをtgtにリネームする
>>> p.rename(tgt)

# パスの末尾のファイルまたはディレクトリ名をtgtに置き換える
# 存在する場合はリネームする
>>> p.replace(tgt)