微忘録

好奇心に記憶力がついていかない人のブログ

クリスマス(季節性)に負けない統計的因果推論

(※Qiitaからの移行記事です。)

Retty Advent Calendar2018の24日目の記事です。 先日は @shindo-taichi さんのRettyマネタイズを支えるプログラマティック広告運用 という記事でした。

24日という美味しい枠を貰いました、分析チーム19卒内定者の@wtnVengaと申します! 「Rettyの分析チームの内情を知りたい」 という方は、是非コチラの記事をご覧ください:santa_tone1:

はじめに

Rettyは生活の基本となる「衣・食・住」のうち「食」を扱うため、ほぼ常に季節性が加わった観測データを扱う事になります。特に クリスマス忘年会のある12月は飲食店需要の高まりから、目算で施策などの良し悪しを判断することは不可能になります。

しかし「季節性のせいで効果検証は行えません」などの言葉を発するのは、分析者として髀肉之嘆の限り。 「季節性に負けた、すなわちクリスマスに負けた」 と言っても過言ではないでしょう。1 季節性に負けない検証で、意思決定をサポートすること が分析者には求められます。

そこで今回はRettyの分析チームが重宝している、季節性を考慮できて便利な DID分析についてご紹介します。

TL; DR

  • 季節性を考慮した検証において、簡単便利な DID分析 をご紹介。
  • DID分析は 平行トレンド仮定という仮定を満たせば心強い分析手法。
  • マサカリプレゼント をお待ちしております:santa_tone2:

DID分析とは?

"DID(Difference in Difference)分析" とは、その名の通り「差分の差分法分析」になります。 以下の図のように、施策群と比較群それぞれの前後差から施策効果を検証する分析手法です。

f:id:wtnVenga:20200326010458p:plain

また一見すると扱い易く見えますが、 以下のような強い仮定のもとで成立する分析手法です。

  • 平行トレンド仮定…施策実施がないとき、両群ともに観測データは同傾向のまま
  • 共通ショック仮定…期間前後において、両群ともに施策以外に同様の処置のみ受けている
  • スピルオーバー効果がない…両群間に施策影響の波及・漏洩が起きていない

一応は平行トレンド仮定を満たしていれば、季節性などもはや敵ではありませんが、この仮定を満たす証明が困難です。しかし有難い事に非平行トレンド時の検証法も数多く提案されているため2、まずは基礎のDID分析を知ることで、より季節性に強い分析者としての端緒になります。

DID分析の実践

DID分析は基本的には回帰式で表現され、ダミー変数の群種($D_g$)・期間($D_t$)が説明変数になります。3

 y = β_0 + β_1D_g + β_2D_t + β_3D_gD_t + ε

モデリング時には、前提におく仮定と効果差分の種類によって手法が変わります。今回は上2種を実践します。

  • 平行トレンド仮定・数量の効果差分 → OLS回帰
  • 平行トレンド仮定・比率の効果差分 → GLMロジスティック回帰(二項分布)^4
  • 非平行トレンド仮定 → 共変量を説明変数に追加、ロジット関数により推計した処置率の差を加重し推計4、 etc...

(以降の検証は全てコチラのcolaboratory資料 から実践できます。)

数量を目的変数とする場合

「Rettyアプリで新規フォローされる週間UU数を増やしたい。施策として4種のユーザー区分において各50%のユーザーにキャンペーン通知を表示した。結果としてキャンペーン通知は効果的であったか」という例で効果検証します。

#サンプルデータの準備
import pandas as pd
import numpy as np
np.random.seed(1224)

outcome = []
for i in range(4): 
  outcome.extend([ 
      #before
      np.random.normal(5000,100),
      np.random.normal(5000,100),

      #after :両群ともに倍増し対象群の方がさらに10ptの施策効果があった
      np.random.normal(10000,100),
      np.random.normal(10000,100) *1.1
  ])

params = pd.DataFrame({
    'Treated' : np.array([0,1,0,1]*4), # D_t 項: [比較群, 対象群] で [0,1,0,1...] 
    'Period' : np.array([0,0,1,1]*4), # D_p項 : [前期間, 前期間, 後期間, 後期間] で [0,0,1,1,...]
    'DID' : np.array([0,1,0,1]*4) * np.array([0,0,1,1]*4) # D_t*D_g項
})
#施策効果(平均効果差分)の計算
join_df = pd.concat([pd.Series(outcome), params], axis=1)
avg_df = join_df.groupby(['Treated', 'Period'], as_index=False).mean()

G1 = avg_df.query('Treated == 1 and Period == 1').iloc[:, 2].values - avg_df.query('Treated == 1 and Period == 0').iloc[:, 2].values
G0 = avg_df.query('Treated == 0 and Period == 1').iloc[:, 2].values - avg_df.query('Treated == 0 and Period == 0').iloc[:, 2].values

print(G1 - G0)

> [974.36110163]
#OLS回帰による検証
import statsmodels.api as sm
lm = sm.OLS(outcome,  sm.add_constant(params)).fit()
print(lm.summary())

> OLS Regression Results
...
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const       4957.9688     68.284     72.608      0.000    4809.190    5106.748
DID          974.3611    136.569      7.135      0.000     676.803    1271.919
Period      5050.1552     96.569     52.296      0.000    4839.750    5260.560
Treated       26.0136     96.569      0.269      0.792    -184.392     236.419
==============================================================================

以上から、結果として施策効果(平均効果差分)は約974UUの差がつき、P>|t|列の値からDID変数が(もし有意水準0.05と設定するならば)統計的に有意であり、この施策は結果に関係がある可能性が見えてきました。 一方で群種を意味するTreated変数が有意でないため、ここでは予定通りPeriodとDIDの2つの変数が着眼点になります。5

比率を目的変数とする場合

「『〇〇が美味しいお店20選』というページの直帰率を下げたい。施策として4種のページ上部に50%の確率でランダムに出し分けるキャンペーンバナーを追加した。結果としてキャンペーンは効果的であったか」という例で効果検証します。

import pandas as pd
import numpy as np
np.random.seed(1224)

#二項分布を仮定した一般化線形モデルを用いるため、[success, fail]の目的変数を作成。
outcome_n = [10000,10000,15000,15000]*4

outcome_s = []
for i in range(4): 
  outcome_s.extend([ 
      #before
      np.random.binomial(10000, 0.4,1)[0],
      np.random.binomial(10000, 0.4,1)[0],      #施策前、直帰率40%で開始

      #after :両群ともに1.5倍増し、対象群は2pt直帰率が減少した
      np.random.binomial(15000, 0.4,1)[0],
      np.random.binomial(15000, 0.38,1)[0],
  ])

outcome =[]
for n, s in zip(outcome_n, outcome_s):
    outcome.append([s, n-s]) #[success, all - success]

params = pd.DataFrame({
    'Treated' : np.array([0,1,0,1]*4), 
    'Period' : np.array([0,0,1,1]*4), 
    'DID' : np.array([0,1,0,1]*4) * np.array([0,0,1,1]*4)
})
# 施策効果(平均効果差分)の計算
join_df = pd.concat([pd.Series(outcome), params], axis=1)
rate_li = []
for i in join_df.iloc[:,0]: rate_li.append(i[0] / (i[0] + i[1]))
join_df_cal = pd.concat([pd.Series(rate_li), join_df], axis=1)

avg_df = join_df_cal.groupby(['Treated', 'Period'], as_index=False).mean()
G1 = avg_df.query('Treated == 1 and Period == 1').iloc[:, 2].values - avg_df.query('Treated == 1 and Period == 0').iloc[:, 2].values
G0 = avg_df.query('Treated == 0 and Period == 1').iloc[:, 2].values - avg_df.query('Treated == 0 and Period == 0').iloc[:, 2].values

print(G1- G0)
> [-0.012475]
#GLMロジスティック回帰による検証
import statsmodels.api as sm

#statmodelsのGLMには切片が必要なので、 sm.ad_constant()で追加。
glm = sm.GLM(outcome, sm.add_constant(params), family=sm.families.Binomial()).fit()
print(glm.summary())

> Generalized Linear Model Regression Results 
...
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const         -0.3929      0.010    -38.542      0.000      -0.413      -0.373
DID           -0.0526      0.019     -2.817      0.005      -0.089      -0.016
Period        -0.0236      0.013     -1.795      0.073      -0.049       0.002
Treated       -0.0105      0.014     -0.728      0.466      -0.039       0.018
==============================================================================

以上から、結果として施策効果(平均効果差分)は約1.2%の直帰率の差が見え、この場合でもP>|z|列の値から(もし有意水準0.05と設定するならば)DID変数が統計的に有意であり、施策が結果に影響があると考えられます。 またPeriodとTreated変数は有意でない点から、施策効果の可能性がより見えてきます。

おわりに

ざっくり説明ですが、簡単便利かつ奥深いDID分析での検証方法について実践しました。 前述の通りこの検証方法には強い仮定があり、またそれ以前にデータが適切に設計・取得されている大前提があります。

「アナリスト・エンジニア・プランナーの3者で密なコミュニケーションが取れる結果、ビジネスの場でより簡潔かつ正確にデータ分析ができる」というのがRetty分析チームの文化の1つなので、ご興味のある方は是非以下もお目通しください

それでは、皆さまからのマサカリプレゼントをお待ちしております。:santa_tone2:


主な参考元

Mora, R., & Reggio, I., "Treatment Effect Identification Using Alternative Parallel Assumptions", https://e-archivo.uc3m.es/bitstream/handle/10016/16065/we1233.pdf?sequence=1, 2012 Mora, R., & Reggio, I., "Alternative diff-in-diffs estimators with several pretreatment periods", https://www.tandfonline.com/doi/abs/10.1080/07474938.2017.1348683, 2017


  1. 過言です。

  2. 例えば以下。

  3. 期間・施策の項はともに連続値を取ることも可能です。

  4. Alberto Abadie, “Semiparametric Difference-in-Differences Estimators”, https://economics.mit.edu/files/11869, 2005

  5. 勿論その他の変数も統計的に有意であることも解釈時に考慮すべきです。期間差はそもそもどの程度なのか、群間差を連続値で表した変数の追加、ダミー変数の多重共線性の問題などなど…

2019年の仕事と趣味を振り返る

はじめに

今年もお疲れ様でした。データアナリスト新卒採用者が、どのような1年間を過ごしたのかの振り返り記事です。

TL;DR

  • 仕事で、思考のピボット力がついて業務遂行力があがったこと。
  • 仕事で、対外的なコミュニティの運営を始めたこと。
  • 趣味で、マンガをよく読み、Vtuberにどハマりしたこと。
  • 趣味で、ロードバイクに乗らず心体のバランスを崩したこと。

仕事について

仕事については、新卒就職からつい先日のお仕事までを振り返ります。まずは新卒就職から。

院進を断念。長期インターンから新卒就職

f:id:wtnVenga:20191231155414p:plain

今年は2018年の4月から長期インターンを続けていた企業に、そのままのチーム・役職に配属で就職しました。
そこまでの経緯を2018年から簡単に流れを説明しますと以下です。

  • 2018年1月…18卒での内定を辞退。院進準備開始
  • 2018年4月…研究計画書の書き始め、研究室見学も開始。(このときスキマ時間で長期インターン開始)
  • 2018年6月…志望研究室が無いと分かり院進断念。就活開始
  • 2018年8月…エントリー6社、最終3社、内定1社で就活終了。

当初の予定であれば今頃は修士1年生ですが、2018年6月頃に研究計画書を書いているときに志望研究が国内では不可能と分かり断念しました。
(院進のために希望留年で卒業を1年伸ばしたのですが、この点にはやく気づかない短期的な思考は去年の反省材料です。)

そんなこんなで、データアナリスト/サイエンティスト職で就活を始め、エントリー自体は6社、選考ルートに乗り最終に至ったのは3社です。残り3社はWebテストを受けた段階で興味がなくなり辞退しました。 その後選考ルートに乗った3社では、人事の方と面接官の方はだいぶ親身でお世話になりました。具体的な社名は伏せますが、最終で落ちた2社は以下理由で落ちた認識です。

  • 名刺管理の会社…提供プロダクト自体への、マーケットインなアナリスト業務の専門職がなさそう。
  • 不動産の会社…ビジョンへの共感ではなく、ビジョンに共感する組織の強さに自分が惹かれていた。

最終面接の相手は2社ともCTOや部門長だったのですが、共通理解をしながら質問を続けてくれたことは圧倒的感謝です。未だに要点が思い出せるのはその証左だと思います。そんなこんなで、2018年の8月には就職活動が終わり、あとはひたすら長期インターンを続けて2019年4月に至ります。

視野の広い分析者への成長

というわけで上述の通り、長期インターンをし始めてから1年間を経て新卒就職しました。 個人的には嬉しい話ですが、1年目でも2,3年目の扱いからスタートです。既にWeb/NativeAppの両方の分析経験があったため、プロダクト横断的なデータ分析とアルゴリズム開発業務で残り1年間を過ごしました。

この1年間ではひたすら「思考のピボット」に注力して業務に当たりました。要は下記の図に従い、都度振り返りました。

  • 「デジタル」はBooleanのような状態、「アナログ」はグラデーションのある状態
  • 「マクロ」はマーケット全体への着目、「ミクロ」は自社プロダクトへの着目

f:id:wtnVenga:20191231134744p:plain
自分の思考が何処にいるのかの再確認

思考のピボットを習慣化するには、UXに強いデザイナーとアナリスト(マネージャー)と一緒の業務に当たれたことが最大の要因だったと思います。仮説から結果と考察まで、各人の領域からツッコミがあること。自分の職務領域としての意見がきちんと議論に乗ること。の条件が揃った環境で1年過ごしたからだと思います。

また問題・課題・分析・検証・評価など場面は色々あれど、目的から逸れないより良い手段をとる考えも、大分安定してきたなという実感があります。 確率統計と実験計画の領域に強みがあるので、ひたすら論文読んだり海外ブログ漁ったりして知識収集して、業務に活かすことを目論んでいます。 具体的な話はデータ分析職あるあるで割愛します。会社のブログに記事があるので興味のある方はどうぞ。

engineer.retty.me

対外的なコミュニティの運営開始

会社の所属チームマネージャーと一緒に、「Data Platform Meetup」というコミュニティを立ち上げました。
組織のデータ活用の活性化に伴う、組織や個人のTipsを共有できるコミュニティがあると良い。という思いから発足です。

data-platform-meetup.connpass.com

大学生時代に色々なイベント企画運営もしていたのが役立ちましたが、今のところ関わってくれる方々の恩恵もあり成功しています。 初回から120名規模でしたが無事成功して、既に2回開催済みです。次回の登壇者も会場提供も募集中です。


趣味について

趣味についてはサックリ振り返りでいきます。

漫画を改めて沢山読むようになった。

もともと漫画は読む方です。日本橋ヨヲコ作品をはじめとして、ヒューマン系や戦略スポーツ系の作品が好きで読んでました。

少女ファイト(1) (イブニングコミックス)

少女ファイト(1) (イブニングコミックス)

アオアシ(1) (ビッグコミックス)

アオアシ(1) (ビッグコミックス)

ちひろさん 1 (A.L.C. DX)

ちひろさん 1 (A.L.C. DX)

さらに今年は漫画好きの社内メンバーからオススメされたものを読み倒しました。以下は一部ですが、Kindleだけで合計100冊は買ったらしいです。

鈴木先生 : 1 (アクションコミックス)

鈴木先生 : 1 (アクションコミックス)

ブルーピリオド(1) (アフタヌーンコミックス)

ブルーピリオド(1) (アフタヌーンコミックス)

五等分の花嫁(1) (週刊少年マガジンコミックス)

五等分の花嫁(1) (週刊少年マガジンコミックス)

Vtuverにどハマりした

テレビも映画も飽きてきたので、スキマ時間にYoutubeを観てたらどハマりしました。最初に出会ったVtuberさんとは別なのですが、今はこの3人の放送を楽しみにしています。話が面白く作業BGMにもなります。

www.youtube.com www.youtube.com www.youtube.com

ロードバイクに乗れず、心身のバランスを崩した。

仕事を頑張る一方で、リフレッシュまで考えが及ばない一年でした。 いままでは年間5,000kmくらいは毎年ロードバイクに乗るようにしていたのですが、そのために使う余力も余暇もありませんでした。ただ日常生活のその他にはあまり影響がでていないので、ぼちぼち運動の習慣を取り戻そうと思います。

ちなみに和歌山県の白浜エリアでリハビリのライド旅をしました。夜までに宿に戻るために35kmを引き続けるプチハードな場面もありましたが、総じて楽しかったです。社会人のお財布力があるのでもっと色々な所に愛車のEmondaSL6と一緒に行こうかなと思います。同行者も募集中です。

f:id:wtnVenga:20191231151840j:plain:w2000f:id:wtnVenga:20191231151327j:plain:w400
白浜の夕焼けと熊野本宮大社


2020年の豊富

正直まだ纏まらないですが、質実剛健な強み作り」を目標に、以下方針になると思います。

  • 仕事:インプットとアウトプットの精緻化
  • 生活:社会人としての整いのある日常と、お財布力を活かした活動範囲の拡大

まだまだインプット量も伸び代があると思うので、学生時代のような時間も忘れて耽けるスタイルから転換したいです。 まあ、引き続き目標からブレずに、好奇心に従うマイペースであり続けることは変わらないと思いますが…。

来年もどうぞよろしくお願いします。

BigQueryだけで統計的因果推論を行いたかった

この記事はRettyアドベントカレンダー12日目の記事です。
昨日は櫻井さんの「KubernetesでVolumeトリックを行う方法」でした。

はじめに

Retty株式会社でデータアナリストとしてお仕事をしています。 6日目の「あなたのエリアは何処から? ~地理空間クラスタリングとの差分検証~」の記事と同様に、箸休めの記事になります。 それでは表題通り、BigQueryだけで統計的因果推論を行いたかった話です。

ことの発端

BigQueryは高速処理や豊富な関数など、それ自体でも強力なツールです。最近ではBigQueryMLによる機械学習の実施も可能になり、利用できるモデルの数も増え続けています。

cloud.google.com

Rettyではデータの民主化を進めており、アナリストだけではなくプランナーもBigQueryを活用しています。そのためBigQueryの可能範囲が分かることは重要ですし、時には背伸びをした使い方をアナリストが試す必要があります。そのため今回はBigQueryMLの可能範囲について、統計的因果推論の手法が用いれるか実験したいと思います。


いざ挑戦

今回は岩波DSvol3よりサンプルデータを拝借して、CM視聴とアプリ利用の因果効果を推定します。カラム構成は主に以下です。詳細については書籍をご一読ください。

  • gamedummy…アプリ利用
  • gamesecond…アプリ利用秒数
  • gamecount…アプリ利用回数
  • cm_dummy…CM接触有無
  • etc...

①データの準備

BigQueryでは外部データソースを参照したテーブルを用意することが可能です。
今回はGoogleDriveにデータを設置し、外部データソースとして認識させます。

cloud.google.com

②BigQueryMLによる2項ロジスティック回帰と傾向スコア算出

傾向スコアを算出したい「CM接触有無(cm_dummy)」をlabel(目的変数)に、ゲーム利用関連の列を除き、その他の変数を説明変数にBigQueryMLを実行します。

create model `project_name.sandbox.ml_adv_logreg` 
options(model_type='logistic_reg', auto_class_weights=true, early_stop = true) as
select
 * except(cm_dummy, gamedummy, gamesecond, gamecount, id)
 , cm_dummy as label
from `project_name.sandbox.adventcalendar`
;

次にモデルの結果画面を確認します。モデル評価に必要な情報はおおよそこの画面で確認できます。陽性クラスの閾値も動的に確認でき、その際には下記のプロットが動的に変動します。

f:id:wtnVenga:20191212173900p:plain
モデル結果画面①

f:id:wtnVenga:20191212173957p:plain
モデル結果画面②

モデルが傾向スコアの算出に有用なのか否かについての検討を行います。 まずはAUCが0.78程度あるためモデルのfittingはある程度は良しと考え、次に実際に傾向スコアの推論を行い、分布の対称性があるか確認します。

with pred as ( 
  select *
  from ml.predict(
    model `project_name.sandbox.ml_adv_logreg`,(
    select * except(cm_dummy, gamedummy, gamesecond, gamecount), cm_dummy as label
    from `project_name.sandbox.adventcalendar`),
    struct(0.5192 as threshold)
  )
)

select pred.* except(predicted_label_probs) , _p.prob
from pred, unnest(pred.predicted_label_probs) as _p
where pred.predicted_label = _p.label

ML.PREDICT 関数  |  BigQuery ML  |  Google Cloud

傾向スコアとしてのヒストグラムの対称性を、一旦spreadsheetに出力して確認します。ある程度の分布としての重なりが確認できるため、最後の段階へと移ります。 f:id:wtnVenga:20191212181920p:plain f:id:wtnVenga:20191212181927p:plain

ちなみに単に実行結果を出力するだけだと、以下のような結果が得られます。

f:id:wtnVenga:20191212175118p:plain
ml.predictの実行結果

③統計的因果推論に用いる推定量への変換と検証

ここで問題が発生します。

あれ・・・IDを変数に入れていないからJOINできないぞ・・・?

上記のクエリを拡張し、推定量への変換とATTの算出を行いたかったのですが、説明変数に「ID」を含める御法度をしなければ、predict結果を元テーブルとJOINすることができません。

真の原因変数を用いれないので、層別解析、傾向スコアマッチングさえ用いることは困難になりました。

最後に

どこかにJOINできるINDEXが隠されているのでしょうか。 今回以外にもRMSEやMAEなど様々な誤差指標を算出したい場合はどうすればよいのでしょうか。 本記事はここまでですが、個人的に解決策を探し続けます。情報提供もお待ちしております。

pandasでtimezoneを含むstringレコードをdatetimeに型変換する方法

pandasのto_datetime()関数はtimezoneフォーマットの%zに対応していなかったので、解決策を備忘録。

t.co

github.com

解決策

今回はnginxログで23/Apr/2018:14:21:43 +0000 形式の文字列を例として用います。 解決方法は要は、datetime.strptime()関数で推論できる状態に整えてから、to_datetime()関数に与えてあげます。

import pandas as pd
from datetime import datetime

df = pd.DataFrame({'datetime_like_column' : pd.Series(['23/Apr/2018:14:21:43 +0000', '29/Dec/2018:03:36:27 +0000'])})

f = lambda x: datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')

#datetimeもしくはdate型かつタイムゾーンはutc指定
df['datetime_like_column_as_datetime'] = pd.to_datetime(df['datetime_like_column'].apply(f), utc=True)
df['datetime_like_column_as_date'] = pd.to_datetime(df['datetime_like_column'].apply(f), utc=True).dt.date
df
datetime_like_column datetime_like_column_as_datetime datetime_like_column_as_date
0 23/Apr/2018:14:21:43 +0000 2018-04-23 14:21:43+00:00 2018-04-23
1 29/Dec/2018:03:36:27 +0000 2018-12-29 03:36:27+00:00 2018-12-29

終わりに

今回扱った問題はpandas の major release 0.24.0 で修正されるようです。 f:id:wtnVenga:20190107005004p:plain

また、同一カラムに複数のtimezoneがある場合の変換方法については、以下をご参照ください。 (indexカラムとして扱うことで攻略できるらしいです)

note.nkmk.me

numpyオブジェクトをjson.dumpできるようにエンコーダーを拡張する方法

最近、友人の運営する大学講義の検索サイトを、機械学習で良い感じにしてます。

地味に悩んだのが「JSON形式でのデータ入出力」の部分。具体的には、「JSON形式で渡されたデータに、推定結果の一部numpyオブジェクトを加えたJSONデータを返す」作業。

Pythonなど動的言語はデータ型をよしなに判断してくれますが、少し例外に触れた時には指定してあげる必要があります。
今回はその例外のためにクラスを自作して指定する方法を備忘録。

問題

何も引数に指定せず、ただ以下の様にjson.dumps()メソッドにnumpy.int64などを渡すと、以下の様なエラーに遭遇する。

json.dumps(numpy.int64(111))
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-12-c21d581d4bed> in <module>()
----> 1 json.dumps(np.int64(111))


/usr/local/var/pyenv/versions/anaconda3-5.0.0/lib/python3.6/json/__init__.py in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    229         cls is None and indent is None and separators is None and
    230         default is None and not sort_keys and not kw):
--> 231         return _default_encoder.encode(obj)
    232     if cls is None:
    233         cls = JSONEncoder


/usr/local/var/pyenv/versions/anaconda3-5.0.0/lib/python3.6/json/encoder.py in encode(self, o)
    197         # exceptions aren't as detailed.  The list call should be roughly
    198         # equivalent to the PySequence_Fast that ''.join() would do.
--> 199         chunks = self.iterencode(o, _one_shot=True)
    200         if not isinstance(chunks, (list, tuple)):
    201             chunks = list(chunks)


~~~

TypeError: Object of type 'int64' is not JSON serializable

この一文は「JSONモジュールの既存エンコーダーに対応している型じゃないから変換無理です」という話

TypeError: Object of type 'int64' is not JSON serializable

なので既存エンコーダーに、自前のエンコード用クラスを与えて拡張する必要があります。

解決策

以下のような、型の種類ごとに変換を行うクラスを作成して、JSONモジュールに対応する型にします。

  • isinstance(object, class)で、一致する場合はreturn以下で変換して返す。
  • numpy.integerint型へ
  • numpy.floatingfloat型へ
  • numpy.ndarraylist型へ

(今回はint型の変換だけで良いのですが、オマケでその他も加えておきます。)

import numpy
import json

class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, numpy.integer):
            return int(obj)
        elif isinstance(obj, numpy.floating):
            return float(obj)
        elif isinstance(obj, numpy.ndarray):
            return obj.tolist()
        else:
            return super(MyEncoder, self).default(obj)

そしてjson.dump()cls引数にクラスを渡してあげます。

json.dumps(numpy.int64(111),cls = MyEncoder)
'111'

解決!

最後に

JSON形式で数値を扱うこと自体ナンセンスかもしれませんが、遭遇しやすい問題かなと思うので載せておきました。 良き分析ライフを御過ごしください。

参考記事

以下記事を参考にしております。先人の知恵に感謝です。

GitHubのセキュリティ設定でやるべきこと

GitHub周りのセキュリティ設定、何回忘れたのかさえ忘れました。 公開鍵でSSH接続して、EmailをPrivateするだけの備忘録です。

※厳密なセキュリティ設定は記述しないので、他の猛者の方々の記事を参考にして下さい。


アカウントと公開鍵の設定

鍵セットの作成

rsa指定で鍵セットを作成。既存がある際は重複削除しないよう注意。

$ cd ~/.ssh #移動して
$ ssh-keygen -t rsa -C mymail@example.com #鍵作成。-C以下任意で自分のメールアドレス

SSH連携

ssh/configに記入し、GitHubとのssh接続を楽にする。

$ vim ~/.ssh/config 

#以下をインサート
Host github.com
  HostName github.com
  IdentityFile ~/.ssh/id_rsa
  User git

公開鍵をクリップボードにコピーして、GitHubアカウントのSSH keysでコピペして登録。

$ pbcopy < ~/.ssh/id_rsa.pub

接続成功したか確認。成功したら以下のような文が出てくる

$ ssh -T git@github.com
Hi wtnVenga! You've successfully authenticated, but GitHub does not provide shell access.

ローカルリポジトリのconfig設定

ローカルリポジトリに移動。config設定を開いて、https://~git@~に変更。

$ cd [local repository]
$ vim .git/config

#'url = https://github.com/[user_name]/[repository_name].git' の部分を以下形式に変更。
url = git@github.com:[user_name]/[repository_name].git

PrivateEmailの利用

GithubのEmail設定をPrivate化するとEmail情報を守れる。
(※リモートリポジトリをcloneされた時にcommitログから漏れない。)

GitHubアカウントの設定

まずはEmail設定の Keep my email address privateBlock command line pushes that expose my email にチェック。 表示される[userid]+[username]@users.noreply.github.com をコピペ。

git configの設定

git configのemail設定を書き換える(--global--local かはお好みで)

$ git config --global user.email [userid]+[username]@users.noreply.github.com

(commitログ書き換え)

最新コミットログの変更。(開いたら:ZZで保存するだけ)

git commit --amend --reset-author

過去コミットログの変更。自分のSHA(コミットハッシュ値から)を指定

git rebase -i [my_SHA] -x "git commit --amend --reset-author -CHEAD"

参考元

git - Meaning of the GitHub message: push declined due to email privacy restrictions - Stack Overflow

How to amend several commits in Git to change author - Stack Overflow

gensimのmodels.TfidfModel()で、引数にSMART notationが使えるようになっていた話

つい先日こんなツイートをしたところ、爆速で公式からリプライが来ました。

「2018年1月2日のgensim3.3.0のリリースに際して、TfidfModelにSMART notation機能が追加されたから宜しくな!」という話で、つまり「models.TfidfModel()コーパスの重み付け手法の選択が、簡単な記法でできるsmartirs引数を新たに用意したよ」という要旨です。今回はこの機能について宣伝と備忘録。

gensimとTF-IDF法

gensimについて

gensimはLSIやLDAなどのトピック分析を行う際に便利なPythonモジュールです。トピック分析の発端である自然言語処理での活用と主しており、文書ごと単語リスト*1(配列)を用意すれば、gensimモジュールの関数を用いて、辞書とコーパスの作成からトピック推定と評価まで可能です。

TF-IDFについて

そして今回備忘録をするmodels.TfidfModel()は、コーパス作成時に単語にTF-IDF法を用いた重み付けをする関数です。

TFは"Term Frequency"つまり文書内単語頻度、IDFは"Inverse Document Frequency"つまり逆文書頻度、そしてTF-IDFはそれら2種の重み付けの積で、「全文書内での出現頻度は低いが、特定文書にて頻繁に用いられる特定単語に、強い重み付けができる」方法です。

そしてgensim3.3.0以前までのmodels.TfidfModel()ではTF-IDF法のみしか指定できず、「TF法だけ」もしくは「IDF法だけ」は自作する必要がありました。*2 この面倒くささを改善したのが、今回のSMART notation機能の導入です。

SMART notationを試す

SMART notationとは

SMART (System for the Mechanical Analysis and Retrieval of Text) notationとは、「文書検索システムにおける重み付けの簡便表記法」になります。

伝統的なベクトル空間モデルを用いた文書検索では、クエリ内容(文書名)に対して、各文書の単語群(Bag-of-Words)の出現頻度にコサイン類似度を用いた計算により、類似性の高い文書を検索結果として表示します。この時の検索結果を左右するのが、TF-IDF法などによる文書内単語群それぞれ対する重み付けです。*3

そしてこの文書内単語群に行われる重み付けを簡便的に表記法する方法が"SMART notation"であり、以下一覧のような表記になります。*4 f:id:wtnVenga:20180208115703p:plain

ここから"単語""文書""正規化"の順に記号を3つ重ねるだけで、どのような重み付けがされるのかが一目瞭然になります。

gensim3.3.0でサンプルコード

そしてmodels.TfidfModel()に"smartirs"引数が追加されたことでSMART notationが利用可能になりました。以下のように重み付けを指定することができます。

from gensim.corpora import Dictionary
from gensim.models import TfidfModel

documents = ["Human machine interface for lab abc computer applications",
              "A survey of user opinion of computer system response time",
              "The EPS user interface management system",
              "System and human system engineering testing of EPS",
              "Relation of user perceived response time to error measurement",
              "The generation of random binary unordered trees",
              "The intersection graph of paths in trees",
              "Graph minors IV Widths of trees and well quasi ordering",
              "Graph minors A survey"]

# 以下の7語をストップワードとして定義
stop_words = set('for a of the and to in'.split())

# 文を単語に分割し、ストップワードを除去した配列を作成
texts = [[word for word in document.lower().split() if word not in stop_words] for document in documents]

#辞書とコーパスの作成
dictionary = Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]

#"n"(tf法)と、"t"(idf法)で、"c"(コサイン正規化)した重み付け
model = TfidfModel(corpus, id2word=dictionary, smartirs="ntc")
vectorized_corpus = list(model[corpus])

最後に

gensim3.3.0は最新版のAnaconda3-5.0.0では未だ対応していないので、別途"pip install"する必要があります。早く追加されて欲しいところ。

*1:厳密化すると「単語(Term)」よりも、字句解析における「トークン(token)」のことです。形態素解析などを用いても必ずしも意味ある単語にすることは難しく、あくまでも文字のカタマリとして「トークン」と呼ばれます。

*2:L2正規化するか否かの"normalise"引数は既存

*3:https://nlp.stanford.edu/IR-book/html/htmledition/queries-as-vectors-1.html#eqn:cosinescore

*4:https://nlp.stanford.edu/IR-book/html/htmledition/document-and-query-weighting-schemes-1.html