はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

livedoor グルメの DataSet を横目で見ながら東京都のグルメ分布のイメージをひとりごとする

この記事では、下記の過去記事の手順、および本記事の末尾に引用のスクリプトで、livedoorグルメDataSetをElasticsearchに取り込み、significant_termsで、東京都の各エリアごとの特徴的なグルメカテゴリのランキングを取得してみました。

f:id:azotar:20200619195309p:plain

itdepends.hateblo.jp

itdepends.hateblo.jp

分類・ランキング結果(色付けなどは後述の説明用に恣意的であることに注意)

ということで、最初に結果情報です。

取得した結果情報を、エリアごとに得られたカテゴリ順に並べたのがこちらです。↓

f:id:azotar:20200619172408p:plain

私も関東在住歴がそれなりに長くなったので、得られた結果と自分の東京各地域イメージを照らし合わせて、独り言をいいたくなった。

この記事はそんな位置付けです。

着色前の表の元ネタを作るところまででは過去の記事などの手順に合わせていただけると、再現可能だと思いますが、以下の内容は見ての通り、私の主観と私の東京の個人的なイメージの混在・混沌としたテキストなので、ご了承ください。

銀座、新橋、有楽町

オトナがお酒を嗜むエリアって感じがしますね。

お食事の傾向も、和寄りのカテゴリが多めというところが、六本木・麻布などとは少し色味が違う。他のエリアより和装の人が多そうなイメージ。

六本木・麻布・広尾・白金

銀座エリアと比べると、欧風の風を感じます。

私は銀座も六本木も無縁の身ですが、おなじオトナの街でも銀座エリアと六本木エリアのパブリックイメージってあるなあという感じです。

代官山、恵比寿-五反田

六本木・麻布・広尾・白金とおおよそ地続きということもありますので、似た印象。

ただし、特徴ランキングで、「バー」ではなく「ダイニングバー」と色がつくあたり、お酒だけではなく、あるいはお酒を注いでもらうだけではなく、ゴハンを食べてお酒も楽しむという感の色が出てくる感じがします。なんとなくわかりますね。

渋谷・神泉

続いて、渋谷・神泉。グルメ系という意味でいうと、東京四大飲みエリアの最後のひとつと言って良いのかな。

他の三つのエリアに比べると、飲み方・呑み方のバラエティが幅広い感じがしますね。

夜・食事・お酒の楽しみ方は人それぞれというイメージ。

若い街、カジュアルというところもにじみ出ています。エリアの中でも東西南北、また通りごと(道玄坂、公園通り、神宮通り、井の頭通り、センター街といったもの)に見せるグルメ街の表情を渋谷で一括りにするとこうなるのかというところです。 (といいつつ、この可視化の元ネタは、冒頭に述べたとおり、Livedoor レストラン情報(2011年)なので、渋谷・神泉は、最近は「オトナ」ど真ん中の印象になってきているかもしれません。渋谷の再開発も出揃ってきた印象もありますし、おなじデータの2020年版があれば比べてみたいところです。)

原宿・表参道・青山

ラグジュアリー感があるエリアです。

西洋料理が特徴として出るのは表参道・青山からの印象どおり?でしょうか。

ここまで登場していない、「カフェ・喫茶」が特徴として見えてくるのも、表参道・青山あたりのオープンカフェっぽい印象と一致します。私は仕事でしか行かないエリアですが...

東急沿線

渋谷を起点に田園都市線。 中目黒、自由が丘を経由して、横浜につながる、東横線。また、これらと連なって走る目黒線

二子玉川も忘れてはいけないでしょう。東急沿線はオシャレに敏感な鉄道会社が作った街というイメージがありますがどうでしょう。

確かに、この特徴ランキングでは、他のエリアでは上位に出てこない、スイーツ系、カフェが目を引きます。

昔、自由が丘でお使いの菓子箱を複数もって歩いていたら、この辺の美味しいスイーツのお店を尋ねられました。確かに菓子箱はヒントですが、私の見た目のどこに、スイーツ通をみて取れたのだろうか。ナンパではないことは確実ですが、何かの勧誘だったのか。

東京・日本橋・大手町

東急沿線のスイーツ(チョコレート)と、東京・日本橋・大手町の「チョコレート」はちょっと印象が違います。

もちろん、どちらも美味しい、頬が緩む感はありますが、前者はお持ち帰りやカフェ感、後者はややお土産・上納品感を感じませんか。

東京駅の印象に引っ張られているのかな。日本橋と銀座につながるエリアの昼の銀座の印象も含むのかもしれません。 また、東京(駅)と言えばですが、他のエリアでは特徴ランキングに現れない、軽食やサンドイッチというお弁当感があるカテゴリが目をひきますね。

新宿・代々木 / 上野・日暮里・湯島

山手線内側東西の両脇の(あるいは、中央線・総武線、はたまた都営新宿線の両端あたりの)個性的的エリアといえそうな両エリア。

エスニック系のグルメが目立っていますね。それぞれのエリアであのエリアにお店が集中していることが特徴ランキングに出てるのかなと思われる面もありますね。

あえて言うなら、新宿・代々木はおなじアジア系でも、東・東南アジア色が出ていて、上野・日暮里・湯島は、南アジア側のようにも見えるがどうだろうか。

秋葉原-水道橋・神田

カレーの聖地神保町を含むエリアであることを強く感じさせるランキング。

担々麺は表記揺れの考慮ができていないことをどう考えるのかはここではさておいて、私はこのエリアに対して担々麺の印象は強くはないのですが、東京に限らず全国どこでも多い中華料理屋がこの地域は特に多いんじゃないか(※確かめていません)と言われると、そんな気がしないでもないですね。

板橋・赤羽・東武沿線 / 池袋-高田馬場巣鴨

池袋を中心とした、新宿などとはまた別のアジア系エスニックエリアのように見えますが、みなさんの印象はどうだろうか。

おなじ、アジア系エスニックでも、新宿などに比べると少し乾燥した雰囲気とややカジュアルというか屋台(実際に屋台かは別として)の本格的だけど生活感グルメと勝手に論じておく(JR池袋駅北口あたりの印象だけで語っている)。

西武沿線 /京王、小田急沿線 / 大井・大森・蒲田 /千住、綾瀬 /中野・高円寺-三鷹 /多摩(府中・立川・八王子)

京王、小田急沿線に「カフェ」が現れることを除けば、東京の23区の中心あたりから放射状に伸びる各路線(特に西の地域)は、ラーメン王国だなという印象。

まあ、東京自体がラーメン王国・帝国というところではありますが、ラーメンの麺のように各沿線が東京をラーメン文化で覆っている印象。

浅草・両国-小岩・錦糸町

他とは一線を画すのちゃんこ鍋、もんじゃ、屋形船の3大巨頭。観光客が思う東京!という印象どおりかもしれない。

湾岸・築地・お台場

築地(というかおそらく月島)のもんじゃがキャラ付け一位。

また、築地(場外市場)の寿司のイメージどおりの寿司も特徴的。数を数えたわけではないですが、築地ど真ん中エリア以外も、市場が近い分、お寿司屋さんや魚介をいただけるお店は多い印象です。

また、浅草出発の屋形船に対して、湾岸エリア出発の屋形船という印象どおり。

三田、浜松町-品川

品川あたりは、屋形船の印象がありますね。実際、北品川に屋形船が多く停泊しており、運河を通って東京湾に船出しています。

また、三田、浜松町、品川というと、各企業の本社が比較的集まっている印象で、(新橋エリアとはまたちょっと違った)サラリーマンが業務終わりにかるくいっぱいやるためのザ・居酒屋的なお店が他エリア以上に特徴的なのかもしれません。

四ツ谷-飯田橋・神楽坂

神楽坂周辺にには秘蔵の/全国の地酒を扱っている隠れ家酒造(酒の密造みたいな言い方になっていますがボキャ貧なだけです)とかありそうです。

データから勝手に妄想すると、(ワインではなく)フレンチ、上海料理、イタリアンと日本のお酒を合わせて出しているイメージも浮かびましたがどうだろう*1

赤坂・溜池山王

他エリアと比較して、特筆すべき数の和系のお店が多いということかと思います。

私は全くもって、このエリア自体はともかく、そのようなお店には無縁ですが、このエリアから漆の香りがしそうだというところは、町歩きとして感じるところがあります。

本件の表作り用のスクリプト(Python)

livedoorグルメDataSetインデックスにsignificant_termsして、表形式データにする

import elasticsearch
import pandas as pd

es = elasticsearch.Elasticsearch("localhost:9200")
_r = es.search(
    index='ldgourmet',
    body={
  "query": {"term": {
    "pref.raw": {
      "value": "13__東京都"
    }
  }}, 
  "aggregations": {
    "a1": {
      "terms": {
        "field": "area_name.raw",
        "size": 47
      },
      "aggregations": {
        "a2": {
          "significant_terms": {
            "field": "cates.raw",
            "size": 50,
            "min_doc_count": 5, 
            "chi_square": {}
          }
        }
      }
    }
  }
}
)

a = []
b = {}
for x in r:
    k = x['key']
    _ = [i['key'].strip() for i in x['a2']['buckets']]
    b[k] = _
    for i in _:
        a.append([k,i,1])

allkeysSet = list(set(sum(b.values(), [])))
allkeys = ' '.join(sum(b.values(),[]))

for k, v in b.items():
    print(k,',',','.join(v),file=sys.stderr)



*1:だんだんいい加減になってきた。

Elasticsearchで検索〜pandasのDataFrameに格納(私的なスニペット)

Elasticsearchとpandasという組み合わせでいうと、Elasticsearchの公式サイトでも紹介があるeland | Elasticなのかなと思います。

しかしながら、ElandでElasticsearchの検索クエリをかませて、それをDataFrameに入れる方法が(おそらくそのような機能を具備しているように見えたものの掘り下げ不足なのか)パッと見わかりませんでした。

てことで、Pythonクライアントで自分が欲しいのはこの程度だからいいやということで代用したサンプルコードのメモです。

確認した、 Elasticsearchのバージョンは6.8です。 pandasは、1.01 です。

gist5b5a14c739067d7c43c03d0e24695126

参考

1) この例は、次の記事で作成したインデックスを対象にしています。

itdepends.hateblo.jp

2) 発行しているクエリは、検索クエリ(term)によるシンプルなクエリと、Aggregationsのsignificant_termsクエリです。

3) この記事を書いた隠れたモチベーションがあります。

私が見逃していただけかもしれませんが、長らく公式サイトでは、Elasticsearchの検索結果のJSONフォーマットに関してはっきりした記述が見られませんでした。

... というと言い過ぎなのかもしれませんが、とりあえず、7系の公式ドキュメントでは、次のセクションで、hitsの構造と、各配下のプロパティについて明記されているようなので、それを見つけてちょっと嬉しくなったというところでした。

www.elastic.co

検索結果のJSONフォーマットについては、実際に現物をさわっていれば、経験的に分かります。

また、断片的に公式ドキュメントの中で何度も事例が出てくるので、これといった不自由はありません。

しかし、公式に明記してあると、誰かに紹介しやすかったりしていろいろありがたいですね。

Webサイトのアクセスログのよくあるかもしれない集計パターンの自分用覚書(Python/Pandas関連)

Webサイトのアクセスログの集計のワンライナーのようなものについては、かつてはいろいろ思うところがありました。 思うというか苦しめられておりました。

しかし、いまや 自分が語るには、この世界が発展・複雑化しすぎており、世界の片隅でのひとりごととしても、それほど特別なことは言えないなあ、特に出番はないなあというのが最近の状況でした。

一方、さらに直近の出来事で、かつて自分が苦労したようなところの地味テクが、いまだにしばしば発生するような本気調査・分析前のゼロ次の切り分けなどの微妙なハザマの用途にピッタリはまったという経験に立て続けに遭遇しました。

ということで、ちょっと気を良くして、かつての地味テクのいくつかを、ちょいとデフォルメして、2020年の覚書としてまとめてみました。つまるところ毎度の個人的な覚書です。

なお、Pandas/Pythonを活用しています。Pandasは、便利メソッドのちょっとした実行例になっているので、たまたまこのページにたどり着いた方のために、ちょい解説と公式リファレンスにポツポツとリンクしてあります。

[前提] この例で共通的にimportしているモジュール

※全部の例で全部を使っているわけではありません。

import pandas as pd
import sys
import urllib.parse
import collections
import re
import numpy as np

API reference — pandas 1.0.4 documentation

(1) よくある(かもしれない)アクセスログの分類(ラベル付け)

ログのイメージ と pandas.read_csvでの読み込み

pandas.read_csv — pandas 1.0.4 documentation

"""
# acc.log のイメージ(タブ区切り)
url cnt
https:// example.com/search/b/?q=アクセスログ&source=lnms&tbm=nws&sa=X&ved=2ahUKEwj-3YKHTHtBjcQ_AUoAnoECA0QBA&biw=1810&bih=1487&dpr=2   7
https:// example.com/search/a/?q=アクセスログ&tbm=vid&source=lnms&sa=X&ved=0ahUKEwia9tCY3fTpAhUBv0Q_AUIECgD&biw=1810&bih=1487&dpr=2 10
https:// example.com/search/b/?q=アクセスログ&tbm=vid&source=lnms&sa=X&ved=CY3fTpAhUByIsBHcDKDv0Q_AUIECgD&biw=1810&bih=1487&dpr=2   24
https:// example.com/search/c/d/e?q=アクセスログ&tbm=vid&source=lnms&sa=X&ved=0fTpAhUByIsBHcDKDv0Q_AUIECgD&biw=1810&bih=1487&dpr=2  5
https:// example.com/search/a/f/?q=アクセスログ&tbm=vid&source=lnms&sa=X&ved=0ahUKEhUByIsBHcDKDv0Q_AUIECgD&biw=1810&bih=1487&dpr=2  8
https:// example.com/search/g/h?q=アクセスログ&tbm=vid&source=lnms&sa=X&ved=0ahUKEwHcDKDv0Q_AECgD&biw=1810&bih=1487&dpr=2   7
...
...
"""

d = pd.read_csv('acc.log', sep='\t')

ディレクトリの第一階層(https://a.b.c.jp/d/e/?f=xxx&g=yyy ... でいうところの「d」)ごとにグループ化してカウント

ディレクトリの階層を軸にカウントする。あるあるですね。

Pandasであれば、strアクセサ のいくつかのメソッドを活用すると、場合によってはループで小難しい処理をしなければならないものも、宣言的に取り扱えます。

また、通常、URLでは、ディレクトリの階層数や、クエリパラメータの数や並び順が悩ましいのですが、str.splitの「expand」オプションをよろしく利用することとしています。

pandas.Series.str.split — pandas 1.0.4 documentation

URLCOL = 'url'
VALCOL = 'cnt'
PATH_DEPTH = 1
PATH_COL = PATH_DEPTH + 2  # https://a.b.c.jp/d/e/ の形式のカラムを「/」でsplitして、「d」より深いディレクトリ部分を取得する都合。

## ディレクトリの第一階層(https://a.b.c.jp/d/e/?f=xxx&g=yyy ... でいうところの「d」)ごとにグループ化してカウント
d2 = pd.concat([d, d[URLCOL].str.split('?').str[0].str.split('/', expand=True).fillna('')], axis=1)
wrk = d2[[VALCOL, PATH_COL]].groupby(PATH_COL)[VALCOL].apply(sum)
wrk.to_csv(sys.stdout,sep='\t')

グループ化をもう少し複雑な条件で行う。ここでは、クエリパラメータのつき具合で分類し、「label」に格納。

URLのパラメータやパスの特徴から分類してそれごとに集計したい場合がありますが、このような場合、まずは、URLを解析する関数でパターンごとにラベルづけした列を追加してやるという方法があると思います。

以後は、もともといろいろな集計関数があるので、ラベルの中身でグループ化して集計してやることで、少し深いことが可能です。

## グループ化をもう少し複雑な条件で行う。ここでは、クエリパラメータのつき具合で分類し、「label」に格納。
def label(url):
    REGX = re.compile("(act=|oq=|uact=)")
    if m := REGX.search(url):
        lb = url[m.span()[0]:m.span()[1]]
        return lb
    return 'others'
    
d['label'] = d[URLCOL].apply(label)  #ひとまず例はラベルづけするところまで

グループ化のラベルづけその2

集計というレベルでは、前項のほぼ再掲ですが、Python正規表現ではこんなこともできるのねという今更の気づきがあったのでその例です。

具体的には、reモジュールによる正規表現グループ化において後方参照ができますが、この際、正規表現のマッチしたその部分そのものではなくて、パターン名を名付けられるのでした。

へー、面白いですね。以下例では、わざわざこれを名付けして分類するのかという程度の例ですが、同じパターン名で抽象化しておくことで、多少実際のパターン内訳がかわっても、出力形式を変えなくて済みますし、パターン名そのものでもともと何に注目して分類しているのか分かりにくいこの類のアクセスログ分類の観点に名付けを行うことができますね。

re --- 正規表現操作 — Python 3.8.3 ドキュメント

(?P<name>...) 構文 と呼ばれるようです。(本当は、「<>」は半角の「<>」)

## 前項のグループ化について、この度、パターン名を定義できることを知ったのでそれを使う。
def label2(url):
    REGX = re.compile(
        r"("
        r"(?P<パターンA>act=)|"
        r"(?P<パターンB>oq=)|"
        r"(?P<パターンC>uact=)"    
        r")"
    )
    if m := REGX.search(url):
        return list(m.groupdict().keys())[0]
    return 'others'        

d['label'] = d[URLCOL].apply(label2)  #ひとまず例はラベルづけするところまで

(2) クエリパラメータに注目

ディレクトリのパスの次に多いであろう集計グループですが、クエリパラメータ部分によるものかと思います。

最近のPythonでは標準の urllibモジュールが便利です。

urllibモジュールのメソッドで、クエリパラメータをよろしく分解してやって、それをもとに前項のようなラベルづけ〜集計を行うということが比較的容易に可能になると思います。 (例では、urllibでクエリパラメータを分解してやるきっかけ部分まででとどめています。)

urllib.parse --- URL を解析して構成要素にする — Python 3.8.3 ドキュメント

# クエリパラメータに注目

d['q'] = d.url.apply(lambda u: list(urllib.parse.parse_qs(urllib.parse.urlparse(u).query).keys()))

## クエリパラメータのパラメータ名自体のバリエーションを把握する
collections.Counter(sum(d['q'].values, [])).most_common()

## クエリパラメータの組み合わせのバリエーションを把握する
set([','.join(i) for i in list(d['q'].values)])

(3) ネストしたグループ化

次に多いかなと思われるのが、複数のラベル(条件)でグループ化して集計するというものです。

3つ以上のグループ化となるとそもそももっとがっつり分析できるしかけを整える必要があるかもしれませんが、2つぐらいまではボチボチ遭遇するかなと思います。

また、本気ツールなどを使ってがっつり分析する方向にシフトチェンジするかどうかの判断のために、2次ネストぐらいまでのグループ化での集計は、手元でさらっとやってしまいたいというのもまたよくある例でしょう。

ちなみに、この時、外のグループか内側のグループかはともかく、それぞれの合計、小計などをおりまぜてソートしたデータが欲しいという場合がしばしばあるように思います。

例えば、iOSAndroidか、PC系のブラウザのUserAgentのうち、アクセスログが一番多いものから、接続元IPアドレスの述べ件数順に並べて集計を俯瞰したいという場合があります。

f1 f2 f3
A x 10
A x 9
A y 3
B x 15
B y 1
B y 1
C x 7
C y 8
C z 9

 ↓ こうしたい

  f1 f2  f1f2g_sum
  C  z          9
  C  y          8
  C  x          7
  A  x         19
  A  y          3
  B  x         15
  B  y          2

このような例でやっかいなの(≒EXCELではちょっとやりにくいかなと筆者が思うところとして)は、集計数は、UserAgent A と 接続元IPアドレス B のグループごとですが、一番大外の並び順は、UserAgent単位の合計が多いもの順というところです。

Pandasでは、この類の用途には、transformと呼ばれるメソッドが便利です。

また、drop_duplicatesというメソッドも、合わせて利用することが多いのではと思われます。

実際には例を見てもらえば良いのですが、transformで、同じグループ内のすべてのレコードに、そのグループの合計値となる同じ値をラベルづけします。

その後、drop_duplicatesで、グループの先頭のレコードだけ残して、同じグループの他のレコードを除外します。

すると、不思議なことに、グループ名とそのグループの集計値が得られるという演算方法です。

pandas.DataFrame.transform — pandas 1.0.4 documentation

ログの読み込み

# ちょっとしたネストありのグループ化。サブグループごとのグループ化した合計値を得たい。
# サブグループ内はそのサブグループ内の合計順。値を得たいが、ならび順序は上位グループごとの合計値としたい
import io
dummy = io.StringIO("""
f1 f2 f3
A x 10
A x 9
A y 3
B x 15
B y 1
B y 1
C x 7
C y 8
C z 9
""" )

ネストされたグループの集計(ちょっとひねったソートあり)

lg = pd.read_csv(dummy, sep=' ')

lg['f1g_sum'] = lg.groupby(['f1'])[['f3']].transform(np.sum)
lg['f1f2g_sum'] = lg.groupby(['f1','f2'])[['f3']].transform(np.sum)

lg.sort_values(['f1g_sum','f1','f1f2g_sum','f2'],ascending=[False,False,False,False]).drop_duplicates(keep='first', subset=['f1', 'f2'])[['f1', 'f2', 'f1f2g_sum']]   

"""
In [139]: lg.sort_values(['f1g_sum','f1','f1f2g_sum','f2'],ascending=[False,False,False,False])                                                                                                                                                                                           
Out[139]: 
  f1 f2  f3  f1g_sum  f1f2g_sum
8  C  z   8       24          8
7  C  y   8       24          8
6  C  x   8       24          8
0  A  x  10       22         19
1  A  x   9       22         19
2  A  y   3       22          3
3  B  x  15       17         15
4  B  y   1       17          2
5  B  y   1       17          2

"""

(4) 時系列・時間単位数

本来これは、このテーマの中では一番あとに回してはダメなぐらい頻出パターンです。 時系列での集計です。

Pandasをつかっていいならいろいろなやり方があると思いますが、時間が入っているカラムをread_csvなどで読み込み時に明示的に指名して、DataFrameのインデックスに指定してやる(DatetimeIndex型とする)と今回のような軽くいろいろ試してみるには良いでしょう。

pandas.DatetimeIndex — pandas 1.0.4 documentation

時間軸ログの例〜read_csvでparse_datesなどを指定して読み込み

## 擬似ログデータ
_tlg = """time,cnt
2017/03/15 10:03:24,18
2017/03/16 10:03:24,24
2017/03/17 10:03:24,431
2017/03/18 10:03:24,31
2017/03/19 10:03:24,41
2017/03/20 10:03:24,13
2017/03/21 10:03:24,41
2017/03/22 10:03:24,3
2017/03/22 12:03:24,431
2017/03/22 12:03:39,31
2017/03/22 12:13:24,32
2017/03/22 13:03:24,86
2017/03/22 15:23:24,238
2017/03/23 10:03:24,41
2017/03/24 10:03:24,31
2017/03/25 10:03:24,432
2017/03/26 10:03:24,41
2017/03/27 10:03:24,13
2017/03/28 10:03:24,141
2017/03/29 10:03:24,3
2017/03/30 10:03:24,1
2017/03/31 10:03:24,3
2017/04/01 10:03:24,4
2017/04/02 10:03:24,14
"""

tlg = pd.read_csv(io.StringIO(_tlg), sep=',',index_col='time',parse_dates=True)

単位時刻単位にグループ化して集計1

日毎、年ごと、曜日ごとというグループ化が宣言的な方法で実現できます。

## 単位時刻単位にグループ化して集計

### DatetimeIndexにしてしまえば、dtアクセサのように、日付のデータのかゆいところが取り扱える。 またグループ化にも使える。
### 日付でグループ化
tlg.groupby(lambda x: x.date)['cnt'].apply(sum)
### 曜日でグループ化
tlg.groupby(lambda x: x.weekday)['cnt'].apply(sum)

単位時刻単位にグループ化して集計2

resampleを使えば、1週間単位、月単位というような「単位」を指定したグループ化も可能

pandas.DataFrame.resample — pandas 1.0.4 documentation

### resampleを使えば、1週間単位、月単位というようなグループ化も可能
tlg.resample('W')['cnt'].sum()
tlg.resample('W').sum()  # このデータ例の場合は上記と同じ

合計件数が(おおよそ)一定数になる間の期間を取得

前の例は、グループ化する時間の単位を指定するタイプのものでした。

一方、たまにですが、ある集計値を時系列順のログの前の方から数えていって、累計がある値を超えたところで、時刻(前の区切りからの経過時間)をプロットしたいというような場合もあります。

閑散期は、カウンターリセットされる周期よりもトラフィック量の総計が少ないが、繁忙期はカウンターリセットされるよりも早く器が逼迫する傾向があるね...みたいなものを見てとりたい場合という例もあるでしょう。

このような場合に、qcutとcumsumを使うと良さそうです。(qcutとcumsumはそれはそれでもっとシンプルな用途、逆により複雑な用途にも対応できると思いますが、私の身近な例では、以下に挙げた例が一番よく使う気がします。)

pandas.qcut — pandas 1.0.4 documentation

## 合計件数が(おおよそ)一定数になる間の期間を取得
### qcutを利用。 
tlg['cumsum'] = tlg.cumsum().cnt # 他にも一発でできる方法があるかもしれないが、cumsumで累計値をいったんストックするところがポイント
c = tlg.cumsum().cnt
step = int ( (c.max() - c.min())/ 500 ) # なぜか知っているおおよそ500刻み
bins = pd.qcut(tlg['cumsum'], step) # stepで指定のbin数に分割
tlg_gb = tlg.groupby(bins) # binsはgroupbyに使うことができる
pd.concat([
    tlg_gb.sum(),
    tlg_gb.apply(lambda x: [x.index.min(), x.index.max()])],
    axis=1)


## 個人的には、移動平均はあまり使わないが、もちろん、window、rollingを用いたテクニックもある。
###   今回は略

Python: wordcoludでタグクラウド画像生成

Pythonタグクラウド生成ライブラリで有名なword cloudを使って、まさにこんな感じの画像ファイルを出力します。

f:id:azotar:20200614010936p:plain

amueller.github.io

github.com

日本語の場合、本来は、タグクラウドの元になるテキストについて統計的な処理や分かち書きをするところをふくめて、タグクラウドかと思いますが、ここでは、wordcloudのビジュアル処理そのものに絞った最小例を示します。

wordcloudを使った日本語 タグクラウド画像出力 Python プログラム

import wordcloud
    
def WCFunc(splitted, filename):
    #from matplotlib import font_manager
    #font_manager.findSystemFonts() # お手元の環境で、これで得られるもののうち、日本語フォントを指定してください
    font_path = 'ヒラギノ丸ゴ ProN W4.ttc'
    wc = wordcloud.WordCloud(
        font_path=font_path,
        background_color='white',
        collocations=False,
        contour_width=2)
    wc.generate(splitted)
    wc.to_file(filename)

WCFunc("""
みかん みかん みかん みかん りんご りんご
バナナ キウイ ぶどう ぶどう
レモン レモン レモン りんご りんご りんご
すいか すいか パイナップル いちご
""" ,
"wordcloudでタグクラウド.png")

(英語のセンテンスにみせかけるように)改行か半角スペースで区切られた単語からなるテキストを渡してやれば、単語出現数に応じて表示制御されるようですね。

参考にさせていただいたサイト

qiita.com

Python: Pillowで画像にテキスト埋め込み(元画像なし版)

Pythonのプログラムを実行して、まさにこんな感じの画像ファイルを出力します。

f:id:azotar:20200613200303p:plain

2020年の現在においては、Pillowというライブラリがデファクトのようです。

hoge$ pip3 list
... 
Pillow                   7.1.2
...

pillow.readthedocs.io

お作法のハンズオンとしては、次が最小例となっていて分かりやすいと思います。

https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#example-draw-partial-opacity-text

上記例は下地になる画像を用意する方式なので、ここでは、さらにシンプルに、白紙の画像オブジェクトを新たにnewするところからの例としました。

Python Pillow テキスト埋め込み画像」というピンクの文字を600 x 200 画像中心あたりにフォントサイズ42 で出力するPython プログラム例

from PIL import Image,ImageDraw,ImageFont

def textImage(text, w, h, outputFile, font_size):
    #from matplotlib import font_manager
    #font_manager.findSystemFonts() # これで得られるもののうち、日本語フォントを指定する
    font_path = "ヒラギノ丸ゴ ProN W4.ttc"
    font_color = (255, 100, 100)
    bg_color = (255, 255, 255)
    font = ImageFont.truetype(font_path, font_size)
    img  = Image.new(mode="RGB", size=(w,h), color=bg_color)
    d = ImageDraw.Draw(img)
    tw, th = d.textsize(text=text,font=font)
    d.text(
        xy=((w-tw)/2, (h-th)/2),    # 文字サイズなどを考慮しておおよそ、文字が画像の真ん中になるように計算
        text=text,
        font=font, fill=font_color)
    img.save(outputFile)

text = "Python Pillow\nテキスト埋め込み画像"
w, h = 600, 200
outputFile = '画像.png'
font_size = 42

textImage(text, w, h, outputFile, font_size)

参考にさせていただいたサイト

note.nkmk.me

openpyxl のさわりのサンプルコード(PythonでEXCELファイルを扱う一例)

はじめに

感染症の例を見ていると、可視化やそもそも現場でのデータ収集というのはなかなか難しいなと感じる次第です。

大半の混乱は一元化や入力の標準化などの情報化の定石で解決する部分もあるでしょうが、今回のような走りながら速記するというような状況では、他者が後出しで簡単に言うようにはいかないところもあるでしょう。

という壮大なテーマとどこまで関連するかは別として、感染症の件から少し連想して(また言葉足らずなのでかなり飛躍しますが)、罫線入りの既存EXCELファイル(様式のテンプレートファイルに該当)に、なんらかのデータを反映して出力するPythonのミニマムな例2020はどんなもんだろうと、確認してみたので、そのサンプルコードです。

openpyxl

ググった範囲では、openpyxlというものがメジャーなようです。

openpyxl.readthedocs.io

さっそく試してみる

これ(ss2.xlsx) を

f:id:azotar:20200608003527p:plain

これ(ss2.output.xlsx)

f:id:azotar:20200608005207p:plain

にして出力します。

A3セル(ただし、結合セルがどうなるか見てみたかったので、A3-B4までの結合セルにしてあります。)を「えーさん」から「えーさん に追記してみる」という文字に置き換えします。

Pythonサンプルコード(ちょっと本来は不要なトリッキーなことまぜこみあり(後述))

import openpyxl
import sys
import io

wb = openpyxl.load_workbook('ss2.xlsx')
st = wb['Sheet1']
st['A3'] = st['A3'].value + ' に追記してみる'
#wb.save('ss22.xlsx')
b = io.BytesIO()
wb.save(b)
b.seek(0)
with open("ss2.output.xlsx","wb") as f:
    f.write(b.read())

これを、foo.py などとして保存して、カレントディレクトリにss2.xlsxが配置されているディレクトリで、実行してください。

この類のプログラムを見慣れた方には解説不要かもしれません。

ワークブックのオブジェクトをロードして、シートを選択して、セルを指定して... というのが、dictライクな操作でできるようですね。

見てのとおり、例外処理などは行っていませんし、本格的に取り回すには、openpyxlの他メソッドはもちろん、Pythonプログラムとして他にもやるべきことがありそうですが、比較的とっつきやすいアーキテクチャシンタックスのような気がします。

なお、ここではコメントアウトしてありますが、ファイルを素直に別ファイル含めて保存する場合は、「save」メソッドを使えば良いようです。

本来やりたいことだけをシンプルに対応するのであれば、コメントアウトを外して、「b = io.BytesIO()」以降は不要です。

なんで、ややトリッキーな例を入れたかはここでは割愛しますが、何をやっているかだけ解説しておくと、saveの代わりに、一時的にバイナリのバイトストリームにデータ格納して、それを明示的にファイルオブジェクトに書き出ししています。

最近のEXCELファイル自体は、XMLファイル等が入ったフォルダをzip化したものだそうですが、ひとまず今回の例の範囲であればこのような方法でも行けました。

補足

ググった範囲だと、oepnpyxlでは罫線が消える場合が頻出するということを記載しているブログなどがいくつかみられました。

今回のシンプルな例だと特に罫線が消えるということは発生しませんでしたが、注意が必要かもしれません。 また、画像の貼り付けがされている場合に(画像の中身や配置に特に変更はしないとしても)、openpyxlでうまく取り扱えるのかは確認していません。

sumy で要約(spaCy、GiNZA) を試してみた: Pythonで自然言語処理にフリーライド

要約・キーフレーズ抽出について

sumy は、Pythonで実装された、抽出型のドキュメント要約ライブラリです。

3行でまとめて! ってやつですね。

ドキュメント中の最重要と思われるセンテンスを抜き出すことで、元の内容のエッセンスを抽出することをめざします。

sumyについて

sumyの公式ページにも書いてありますが、著名な要約アルゴリズムをいくつか実装しているようです。

今回は、それぞれのアルゴリズムでサンプルデータに対してどのような要約が得られるか確認してみます。

... といいつつ、アカデミックな評価などは行っておりません。 このブログのいつもの、使ってみた・多分うまく動いている系のおためしサンプルコードをペタっと貼り付けですので、ご了承ください。

pypi.org

github.com

sumy/summarizators.md at master · miso-belica/sumy · GitHub

sumyは、「japanese」対応にはなっていますが、表記揺れや語形変化を考慮した、出現数カウントやそもそもの単語の分かち書きには対応していないので、spaCyとGiNZAでサポートすることにしています。

spaCy/ GiNZA

spaCyは、Pythonの今時の自然言語処理ライブラリのようです。実際にプロダクトに組み込んで使えるような機能群がそろっているという触れ込みのようです。

また、GiNZAはspaCy上で日本語ドキュメントを処理するにあたって、日本語関係のいろいろモダンな叡智がつまっているライブラリのようです。

megagonlabs.github.io

spacy.io

それぞれ、今回利用したような範囲以上のパワフルな使い方ができると思いますが、今回は、sumyを日本語対応させるにあたり、日本語での分かち書き・レンマ化のツールとして使いました。

sumyの(ほぼ)最小限の使い方*1

  1. 対象ドキュメントを読み込む(sumyには、WebサイトのhtmlをダウンロードするHTTPクライアント機能などもあるようですが、今回はテキストファイルを文字列として読み込みした例を使います)
  2. 当該ドキュメントに関して、その言語での、文の区切れ、単語の区切れを取得するルールを定義する(Token化)
  3. 上記2のToken化クラス*2を、今回使いたい要約アルゴリズムの関数に引数として与える 〜 解析
  4. 戻り値として、要約の文がイテレータで得られる

1は、青空文庫の「海野十三 ある宇宙塵の秘密」のテキストファイルを使いました。

2では、spaCy/GiNZAで分かち書きなどを行いました。なお、sumy公式の次の How to add new natural language support into Sumy というページを参考にしました。

sumy/how-to-add-new-language.md at master · miso-belica/sumy · GitHub

上記に従い、約束事に従ったTokenizerクラスを作成し、to_sentences、to_wordsを規定のシグネチャに沿って実装すれば良さげでしたので、

spaCyの「sents https://spacy.io/api/doc#sents」「lemma_ https://spacy.io/api/token#attributes

というプロパティを使いました。

LexRankやLSAなどは、文や単語の関係から重みを計算するアルゴリズム(だとヤワな理解をしておりますが...)なので、spaCy/GiNZAで分かち書きするとともに、レンマ化して語形変化などを考慮して本体は同じ単語の出現回数をよろしく数えられるようにするのかなと思って進めてみています。

sumy 日本語利用のサンプルコード

実行時のカレントディレクトリに、インプットファイルの 'unno.txt'が配置されている前提です!

※以下、初出時gistを貼り付けしていたのですが、はてなのブログ貼り付けがNGになってしまうようになったので、ベタ張りに変更。

"""
sumyでドキュメント要約を行うサンプルプログラム
"""
# spaCy
import spacy
# sumy
from sumy.parsers.plaintext import PlaintextParser
# 以下、要約アルゴリズム
from sumy.summarizers.lex_rank import LexRankSummarizer
from sumy.summarizers.lsa import LsaSummarizer
from sumy.summarizers.reduction import ReductionSummarizer
from sumy.summarizers.luhn import LuhnSummarizer
from sumy.summarizers.sum_basic import SumBasicSummarizer
from sumy.summarizers.kl import KLSummarizer
from sumy.summarizers.edmundson import EdmundsonSummarizer


# 前処理と言えば前処理  -----------------------------

# GiNZA/spaCyの初期化
nlp = spacy.load('ja_ginza')

# sumy の sumy.nlp.tokenizers.Tokenizerに似せた、オリジナルのTokenizerを定義
#   https://github.com/miso-belica/sumy/blob/master/docs/how-to-add-new-language
class myTokenizer:
    @staticmethod
    def to_sentences(text) :
        return [str(s) for s in nlp(text).sents] # spaCyは、「sents」で文のジェネレータを戻す

    @staticmethod
    def to_words(sentence) :
        l = next(nlp(sentence).sents).lemma_  # spaCyは、「lemma_」で文のレンマ化した文字列を戻す
        return l.split(' ')  # spacy/GiNZAの仕様により、半角スペース区切りでトークン化されるようなのでそれを前提にリストにする

# ドキュメントの読み込み
doc_str = open('unno.txt').read().replace(' ', '').replace(' ', '')  # 今回は、スペースは最初の時点でストップワードとして除外しておく。

# 何行に要約するかの値を算出
# (※これはsumy利用のポイントではなく、筆者がお試しするのにこうしておくのが便利だと思った味付け。
# この味付けは不要、単に3行に要約したければ、sentences_count=3 とすれば良い)
num = len(doc_str.split('。'))  # 句点の数を文の数とみなす。
N = 3
sentences_count = N if num < 100 else int(num/10) # 長めの文章なら10分の1に、そうでなければN行に要約

# パーサーの設定(入力ドキュメントを読み込ませて、Tokenizerでコーパスを生成する...など)
parser = PlaintextParser.from_string(doc_str, myTokenizer())


# 以下、アルゴリズムを指定して要約する -----------------------------

def summarize(summarizer): # 出力関数(手抜き)
    result = summarizer(document=parser.document, sentences_count=sentences_count)
    print('\n',summarizer)
    for s in result: print(s)


##
summarize(LexRankSummarizer())

##
summarize(LsaSummarizer())

## 
summarize(ReductionSummarizer())

##
summarize(LuhnSummarizer())

##
summarize(SumBasicSummarizer())

##
summarize(KLSummarizer())

## 
# summarize(EdmundsonSummarizer())
##  bonus_wordsが必要と怒られるので、この呼び出し方ではダメなので省略

出力例

上記の実行結果です。

今回は、評価は行わないので雰囲気のみ。

f:id:azotar:20200607205324p:plain

以上です。

◆参考リンク

「要約」について

qiita.com

spaCy と GiNZA

www.ogis-ri.co.jp

sumyに関して日本語参考にさせていただいたサイト

ohke.hateblo.jp

sumyを使って青空文庫を要約してみる - Qiita

Pythonの要約(抽出型 Extractiveのもので)いつか試してみたいもの

boudinfl.github.io

はじめての自然言語処理 pke によるキーフレーズ抽出 | オブジェクトの広場

nakagami.blog.ss-blog.jp

*1:筆者が見よう見まねや公式ホームページを見てこうかなと思うやり方です。

*2:便宜上クラスと言いましたが、ファクトリーメソッド的なもの。