蛇ノ目の記

技術のことも。そうでないことも。

Notebookを見やすくするCSSを書いた

sora tob sakana東名阪ツアー大阪公演に遠征した@大阪

どういうわけかWiFiが入らない梅田のスタバで書いている。なにが悲しくてスタバでテザリングしないといけないのか…。

というのは置いておいて、近頃Colaboratoryが便利だなぁと思ってるんですが、どうにもフォントサイズが小さかったり行間が狭かったりしてコードが書きづらい、読みづらい。 そういうことを嘆いていたら、kashewさんからStylusを使うのがよさそうと助言を受けたのでChromeに入れてColaboratoryの見た目をいじることにした。kashew++。

chrome.google.com

というわけでCSS

gist.github.com

これを入れるとこうなるというのが以下のツイート。

ちなみにJupyter Notebookにも対応しているので、Colaboratory使ってないけどNotebookのコードが読みづらいなと感じてる人もこのCSSを試してみてくれよな。

iframeになっているアウトプット部分も調整しようと試行錯誤中。なにかいい方法あったら教えてください。

ダウンロードリンクからCSVをスクレイピングしてDataFrameにする - Spotify Charts編

少し前にこのブログでも取り上げたSpotify Charts。各国のヒットチャート・バイラルチャートが公開されている見ていて飽きないWebサービス

Spotify ChartsではチャートをCSV形式でダウンロードできる。そのCSVスクレイピングしてDataFrameにしてみる。ついでに順位の推移ごとにグルーピングして可視化めいたこともやってみた。

Notebook

今回のコードはColaboratoryで閲覧できるようにしている。Colaboratory便利かよ……。

colab.research.google.com

DataFrame作成

今回は日本のSpotify最新ヒットチャートのダウンロードリンク(https://spotifycharts.com/regional/jp/daily/latest/download)を扱う。Notebookの実行では2016/06/02のチャートになっている。

import io

import pandas as pd
import requests

from bs4 import BeautifulSoup as bs

res = requests.get('https://spotifycharts.com/regional/jp/daily/latest/download')

content = res.content

df = pd.read_csv(io.BytesIO(content))

バイト列で取得されるCSVの中身をBytesIOを使ってファイルオブジェクトの代わりにしている。この手法は以下のブログを参考にした。

www.haya-programming.com

1行目には Note that these figures are generated using a formula that protects against any artificial inflation of chart positions. というメッセージが入っているので、スキップする。

f:id:Nao_Y:20190604191620p:plain

df = pd.read_csv(io.BytesIO(content), skiprows=[0])

これでチャートのDataFrame化は完了。簡単。

f:id:Nao_Y:20190604191733p:plain

順位の推移をDataFrameに加える

ここからは作成したDataFrameに情報を足してゆく。

Spotify Chartsでは順位の推移を表す記号が表示されているが、この情報はCSVに含まれていないのでスクレイピングしてDataFrameに追加する。

記号はSVGで描画されているので、BeautifulSoupでHTMLをパースして順位の推移を取得する。

res = requests.get('https://spotifycharts.com/regional/jp/daily/latest/')
soup = bs(res.content)

svg_tags = soup.find_all('svg')

get_status関数でSVGタグのブロックから上昇、下降、維持、新規を判定する。 polygon属性の値などはブラウザのDeveloperToolでのぞき見た。

def get_status(svg):
  up = '0 9 12 9 6 3 '
  down = '12 3 0 3 6 9 '
  if svg.find('polygon'):
    polygon = svg.find('polygon')['points']
    if polygon == up:
      return 'up'
    elif polygon == down:
      return 'down'
  elif svg.find('rect'):
    return 'keep'
  elif svg.find('circle'):
    return 'new'

status = []
for svg in svg_tags:
  status.append(get_status(svg))

そしてDataFrameに列を追加。

df['status'] = status

f:id:Nao_Y:20190604192528p:plain

Spotify IDをDataFrameに追加

Simple-Spoitfyを使って曲の特徴情報を取得したくなるかもしれないので楽曲ごとに固有のSpotify IDをDataFrameに追加する。

df['Spotify ID'] = df['URL'].apply(lambda x: x.split('/')[-1])

f:id:Nao_Y:20190604192912p:plain

順位の推移をグルーピングして可視化してみる

順位の推移(status)の値でグルーピングして、推移の仕方による順位のQuantileをまとめる。

グルーピングしてからQuantileを求める手法は以下を参考にした。

stackoverflow.com

status_group = df.groupby(by='status')
positions = status_group['Position'].quantile([0.05, 0.25, 0.5, 0.75, 0.9]).unstack()

最後に推移ごと(上昇, 下降, 維持, 新規)による順位の分布を折れ線グラフにする。

positions.T.plot(figsize=(15, 10), title='Distribution of position change at 2019/06/02');

f:id:Nao_Y:20190604193827p:plain

雑可視化からなんとなくわかること。

  • 順位の維持は100位以下の順位では起きていない

  • ランキング初登場の曲は175~200位の間にしかランクインしていない

  • 初登場で上位に食い込む曲は無い

  • 順位の上昇・下降の約25%は20~70位内で起きている

Spotify Charts面白い

2017/01/01からのチャートが蓄積されているので、その時々の流行りや根強く聴かれている曲を見つけることができ面白い。 ここ半年くらいの日本のチャートを見ると、あいみょん人気がやばいことがわかる。聴いたことないけど。

チャートは2種類あり、TOP 200 はストリーミング数の蓄積によるランキング。一方、VIRAL 50SNSの分析で集計しているランキングらしい。 VIRAL 50の集計方法については以下のフォーラムで議論されている。

community.spotify.com

Spotify Web APIを使えば楽曲の特徴情報が得られるので、人気のある曲が持つ特徴を可視化することもできる。 そこで活きてくるのがSpotify Web APIPythonで使いやすくすることを目的にしているSimple-Spotify(ダイマ)。

PyCon mini Sapporo 2019で登壇した - Simple-Spotify: Pythonでシンプルに楽曲データを扱うライブラリ

5月11日に札幌で開催されたPyCon mini Sapporo 2019で登壇した。

写真撮る人がめっちゃ上手くて影とかいい感じになっててすごい。正装(オサカナTシャツ)もばっちり写っている。

テーマは3ヶ月くらい前から作っていたSpotify Web APIのラッパーライブラリSimple-Spotify。懇親会ではUKロック好きな@shinyorkeとSimple-Spotifyのいい感じな使い方の話で盛り上がった。興味を持ってもらえてとても嬉しかった。

github.com

developer.spotify.com

Spotify Web APIのラッパーライブラリとしてはSpotipyがあるが、requestsに依存しているそれとは異なりSimple-SpotifyはPure Pythonで実装している。 また、SpotipyがAPIのレスポンスを辞書形式で扱っているのに対してSimple-Spotifyではクラスとして扱っている。

Simple-SpotifyとSpotipyの違いを見比べてみる。ここからは発表にはない話になる。

Spotipy

import spotipy
sp = spotipy.Spotify()

results = sp.search(q='weezer', limit=20)
for i, t in enumerate(results['tracks']['items']):
    print ' ', i, t['name']

Simple-Spotify

from simple_spotify.api import Spotify
from simple_spotify.authorization import ClientCredentialsFlow

res = ClientCredentialsFlow.token_request(CLIENT ID, CLIENT_SECRET)
auth = ClientCredentialsFlow(**res)
sp = Spotify(auth)

results = sp.search(q='sora tob sakana', market='JP')
for album in results.albums.items:
    print(album.name)
sora tob sakana
World Fragment Tour
New Stranger
alight ep
cocoon ep
魔法の言葉
夜空を全部
アルファルド

sora tob sakanaはいいぞ。

Spotipyでは認証を行っていないんだけど、Web APIのドキュメントにはすべてのリクエストは認証が必要、と書いてあるのでサンプルコードが古い可能性がある。

Authentication

All requests to Web API require authentication.

https://developer.spotify.com/documentation/web-api/

認証を含めなければ行数はほぼ同じ。 検索結果(results)オブジェクトにアルバム(albums)属性があって、さらにそれぞれのアルバム(items)がある、というのはそこそこ直感的でないかと思っている。

認証について言えば、認証用のエンドポイントからのレスポンスを一度受け取ってから、それをアンパックして認証クラスに渡すというのはちょっとわかりにくい流れになってしまった。 本当のところを言えば、認証用クラスにClient IDとClient Secretを渡せばOKという風にしたかったのだけど、もう一つの認証をWebアプリ上で使えるようにする関係でそれと同じ流れにした。

これがFlaskで作成したSimple-Spotifyを使った簡単なWebアプリ。

@app.route('/')
def top():
    if 'response' in session.keys():
        auth = AuthorizationCodeFlow(**session['response'])
        sp = Spotify(auth)
        user = sp.get_current_user_profile()
        top_artists = sp.get_users_top('artists', time_range='medium_term')
        artists = top_artists.items
        top_tracks = sp.get_users_top('tracks', time_range='medium_term')
        tracks = top_tracks.items

        return render_template('index.html', name=user.display_name, artists=artists, tracks=tracks)
    else:
        seed = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
        url = access_authorize_page(CLIENT_ID, REDIRECT_URI, SCOPES, seed)
        return render_template('index.html', auth_url=url)


@app.route('/callback/')
def callback():
    """
    callback
    """
    session['response'] = AuthorizationCodeFlow.token_request(
        CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, request.args['code'])
    return redirect(url_for('top'))

Spotifyへのログインをしてからアクセストークンを受け取るのだけど、アクセストークンの取得をtop()で行ってしまうとリロードやページ遷移の度にトークンを取りに行こうとしてAPIからエラーが返ってきてしまった。これを避けるためにコールバックでアクセストークンなどを受け取って、セッションに格納。top()でそれを使うという流れにした。 これには認証方法によってSimple-Spotifyの使い方に違いが出てしまうのを避けたかったという思いがある。

と言い訳めいたことを書いてから、実際にSimple-Spotifyの使い方などを書きたいと思うのだけど寒いのでしばらくしてから続きを書く。 なにしろこんなところで書いているので風が冷たい。

みんなのPython勉強会#44を企画・登壇した - 正規表現の話

4/10に開催されたみんなのPython勉強会#44(stapy)を企画・登壇した。

平成が終わる前に書かないと、と思いつつここまで先延ばしにしてしまった。でもまだ平成なのでギリギリセーフということでどうかひとつ。

startpython.connpass.com

平成最後のstapyということで平成生まれである自分が主導し、自分を含めた6人の平成生まれPythonistaによるショートトーク会を企画した。

登壇してくれた皆さんありがとうございました。

今回の内容はPythonにおける正規表現\w (any word character) の振る舞い。

GitPitch Presents: github/NaoY-2501/GitPitch-Slides

Modern Slide Decks for Developers on Linux, OSX, and Windows 10. Preview and present offline. Publish and share online.

以下のリポジトリ\wUnicodeのLetter, Numberカテゴリのうちどれだけの文字にマッチするか、Punctuationカテゴリではアンダースコア以外にマッチしないかを確認した。

GitHub - NaoY-2501/stapy44_re_unicode_checker

Letterカテゴリの中でもマッチしない文字がある。

https://www.fileformat.info/info/unicode/category/ ではLetterカテゴリに分類されているが、unicodedataモジュールで確認するとCn(Other or not assigned)となっている。

>>> import unicodedata
>>> unicodedata.category('\ua7ba')
'Cn'

つまり、公式ドキュメントにある通りであることがわかる。

ユニコードパターンに対しては、 \w は unicodedata モジュールで提供されている Unicode データベースで letters としてマークされている全ての文字とマッチします。

正規表現 HOWTO — Python 3.7.3 ドキュメント

Special Thanks

Thank you for speakers !!

nikkie(@ftnext): P(ython)&I 〜最初の落とし穴を避け、成功体験を積むために〜

山下 卓将(@xxx_boy): Python と挑んだtitanic ~101回目のsubmit~

大島和輝(@shimakaze_soft): FlaskとDjango以外のAPI開発の選択肢

みずき(@mizzsugar0425): DjangoとPyramidで同じアプリ作った話

清原 弘貴(@hirokiky): 無題 (僕の平成について話します)

pyhack冬山合宿に行ってきた - SpotifyチャートにTF-IDFを使ってみた話(概要編)

1/19(金)~1/21(日)にかけてpyhack冬山合宿に行ってきた。

気づいたらもう3月。記事を書くのがかなり遅れてしまった。

pyhack.connpass.com

@takanoryによるTogetterまとめはこちら。Python mini Hack-a-thon 雪山合宿 2019 - Togetter

昨年はsiraphのライブがあったので2日目の昼に帰ってきたんだけど、今年は3日間通しで参加した。

合宿でやったこと

Spotifyのチャートに対して、文書の特徴量を求めるのに使われるTF-IDFを使って国ごとに特徴的な曲を分析してみる、ということをやってみた。

グローバルチャートに対する国ごとにユニークな曲を抽出するなら、グローバルチャートと各国チャートの差集合を取るだけでいいのでは、という向きもあると思うけど、まあ思いつきでやったことなので多めに見てほしい。

Spotify Charts

Spotify Chartsでグローバル・各国の人気曲チャートが公開されている。チャートには以下の2種類がある。

  • Top 200: ストリーミング数による集計。上位200曲が公開されている。

  • Viral 50: SNS上での人気を基にした集計。上位50曲が公開されている。

Viral 50の集計に関しては以下のフォーラムで議論されているが、おそらくソーシャルメディアを分析したものとのこと。

https://community.spotify.com/t5/Content-Questions/Viral-50-Playlists-How-do-they-work/td-p/1627778

今回は2種類のチャートそれぞれを対象とした。

データの取得

前回の記事で紹介したSpotify APIではチャートの取得ができないため、Spotify Chartsを使うことにした。

Spotify Charts Top200, Viral 50のWebページをスクレイピングしてチャートを取得した。

URLはTop 200ならhttps://spotifycharts.com/{{ country code }}/, Viral 50ならhttps://spotifycharts.com/viral/{{ country_code }}/とシンプルな作りになっているのでスクレイピングが容易。

{{ country_code }}ISO 3166-1 alpha-2 - Wikipediaに基づいている。ただし、グローバルチャートの場合はglobalとなる。

国によってはTop 200やViral 50が取得できない場合がある。このとき、レスポンスヘッダに'Content-Encoding'が含まれる。これを考慮しておかないとチャートの取得でコケるので注意が必要。

TF-IDF

TF-IDFは文書の特徴量として使われる指標。文書中に出現する単語の重要度を評価できる。

TFとIDFは以下のように説明できる。

  • TF(Term Frequency): 文書dにおける単語tの出現頻度

  • IDF(Inverse Document Frquency): 逆文書頻度。この値が高ければ単語tの出現頻度が低いということになる。

TF-ICF

TF-IDFが文書の特徴量なら、今回考えるのはトラックの特徴量だろうということで、TF(Track Frequency), ICF(Inverse Chart Frequency)と呼び替えることにする。


実際のコードの話は別の記事で。

SpotifyのAPIを使ってみた

最近Spotifyを契約し始めたのだけど、Spotifyには曲やアルバム、プレイリスト、アーティストの情報を取得したりできるAPIがあるとのことなので試してみた。

Spotipyというラッパーライブラリがあるようだけど、今回はそれを使わずにrequestsを使ってAPIを使ってみる。

アプリの登録

APIを使うにはFreeかPremiumのユーザーである必要がある。登録はhttps://www.spotify.com/jp/から。

ユーザー登録したらダッシュボードページにログインして、アプリ登録をする。

f:id:Nao_Y:20190117234242p:plain

作成したアプリをクリックする。

f:id:Nao_Y:20190117234313p:plain

Client IDSHOW CLIENT SECRETを押して表示されるClient Secretを控えておく。

認証フロー

Spotify APIには3つの認証フローがある。

  • Refreshable user authorization: Authorization Code Flow

長期間稼働するアプリ向けの認証。エンドユーザーは一度だけアプリがリソースにアクセスすることを許可する。アプリにはリフレッシュ可能なアクセストークンが提供される。バックエンドなど安全な場所から実行する。

  • Temporary user authorization: Implicit Grant

JavaScriptを使用するため、リソース所有者のブラウザ上で動作するアプリ向けの認証。

  • Refreshable app authorization: Client Credentials Flow

サーバー間認証で使用される。エンドユーザーのリソースにアクセスしないエンドポイントのみ使用可能。

参考:

アクセストークンの取得

今回はClient Credentials Flowを使う。この認証方法ではClient IDとSecret Keyを使ってAccess Tokenを取得する。

  • Endpoints: POST https://accounts.spotify.com/api/token

  • Request Body Parameter: grant_type(client_credentialsを設定する)

  • Header Parameter: Basic <base64 encoded client_id:client_secret>

import requests

CLIENT_ID = 'YOUR CLIENT ID'

CLIENT_SECRET = 'YOUR CLIENT SECRET'

GRANT_TYPE = 'client_credentials'

TOKEN_URL = 'https://accounts.spotify.com/api/token'

body_params = {'grant_type': GRANT_TYPE}

auth = requests.post(TOKEN_URL, data=body_params, auth=(CLIENT_ID, CLIENT_SECRET)).json()

requestsでHTTP Basic auth(Basic認証)のあるURLにアクセスするときはキーワード引数auth(User, Password)とする。

この後、他のAPIで認証をするときにアクセストークンを使うので辞書形式でレスポンスを取得している。

参考:

アルバムを検索する

  • Endpoints: GET https://api.spotify.com/v1/search

  • Header Fields: Authorization

    • Value: 有効なアクセストークン。Bearer {{access token}}の形式。
  • Query Parameter(必須のものだけ抜粋)

    • q : 検索クエリ

    • type: 検索タイプ。有効な値はalbum, artist, playlist, trackでリストで指定可能

参考:

sora tob sakanaのアルバム・シングルを検索してみる。

header_params = {'Authorization': 'Bearer {}'.format(auth['access_token'])}

query_params = {'q': 'sora tob sakana', 'type': ['album']}

ENDPOINT = 'https://api.spotify.com/v1/search'

res = requests.get(ENDPOINT, headers=header_params, params=query_params)

for each in res.json()['albums']['items']:
    artist = each['artists'][0]
    url = each['external_urls']['spotify']
    print('{}: {} {} ({})'.format(artist['name'], each['name'], each['release_date'], url))
sora tob sakana: sora tob sakana 2016-07-26 (https://open.spotify.com/album/3cJxQS1oi8u2oBu9uhVyEk)
sora tob sakana: cocoon ep 2017-04-11 (https://open.spotify.com/album/467R6azC01pl67nSrLS55x)
sora tob sakana: mahou no kotoba 2016-02-16 (https://open.spotify.com/album/6x0xVz6hdHerGMf2yaMqLj)
sora tob sakana: yozora wo zenbu 2015-10-27 (https://open.spotify.com/album/2FezsFEuQFferFLy4syK4T)

どうしたことか。2017-04-11以降の作品が取得されない。よく見るとタイトルがアルファベット表記になっている。日本国内の設定になっていないっぽい。marketパラメータを指定してみる。このパラメータはISO 3166-1 alpha-2 country codeで表記する。日本はJP

header_params = {'Authorization': 'Bearer {}'.format(auth['access_token'])}

query_params = {'q': 'sora tob sakana', 'type': ['album']}

ENDPOINT = 'https://api.spotify.com/v1/search'

res = requests.get(ENDPOINT, headers=header_params, params=query_params)

for each in res.json()['albums']['items']:
    artist = each['artists'][0]
    url = each['external_urls']['spotify']
    print('{}: {} {} ({})'.format(artist['name'], each['name'], each['release_date'], url))

2017-04-11以降の作品が表示されて、タイトルも日本語表記になった。日本のアーティストを検索するならmarketJPを指定するのがよさそう。海外勢はalight epとかNew Stranger聴けないのかー。

sora tob sakana: sora tob sakana 2016-07-26 (https://open.spotify.com/album/6wy5MYR1ThaC7IuQu3VHHG)
sora tob sakana: New Stranger 2018-07-25 (https://open.spotify.com/album/1ATJgjPZ7Qz0LAUZtMW2Sp)
sora tob sakana: alight ep 2018-05-16 (https://open.spotify.com/album/36w9htd6tgXmNs0iPWte2P)
sora tob sakana: cocoon ep 2017-04-11 (https://open.spotify.com/album/3nN8No2a5ZhzK2xw9RIYQp)
sora tob sakana: アルファルド 2018-11-23 (https://open.spotify.com/album/3nA6Sd5gDIY8xHiVQQ3kvE)
sora tob sakana: 魔法の言葉 2016-02-16 (https://open.spotify.com/album/0UejaOY3nZq4h3fZ2be8lA)
sora tob sakana: 夜空を全部 2015-10-27 (https://open.spotify.com/album/03JuPAwApUfvx61cHvUmOJ)

APIを使ってアルバムを検索することができたので、この辺りで一旦区切りをつけておく。

今後はGET https://api.spotify.com/v1/searchにおけるワイルドカードの使い方(例えば、SawanoHiroyuki[nZk]:~をワイルドカードで検索)やプレイリストの検索を調べてみたい。

2019年ブログ初め

遅ればせながら、あけましておめでとうございます。

早々に「やってみた系」でアイデアが浮かんだんだけど、それより前にブログ初めをしておかねばという謎の義務感に駆られたので簡単に2019年にやりたいことを書いて2019年のブログ初めとする。

以下、目標と願望めいたやつを一緒くたに書きなぐる。

技術

  • 海外ブログの翻訳をやってみる

    • 去年末から着手してる。来月には公開できる、はず。
  • 積読の技術書を読む

    • Web, 機械学習いろいろ積んでる

    • 新しく買う前に読め

  • herokuを使わないWebサービスを作る

    • AWSのサービスをいろいろ使ってやってみたい

    • 2018年は仕事を通してEC2, route53, RDS, S3に触れたので経験を活かしたい

    • nginxもほんの少しわかるようになったのでそれも活かしたい

  • JavaScript覚える

    • そもそもJavaScript全然わからない

    • Vue.jsとか名前聞いたことあるレベルでしかないのでPythonのWebフレームワークと組み合わせる方法を身に付けたい

  • 英語を話す

    • オンライン英会話が気になってるけど、実際どういう感じかわからなくて手が出せてない

    • あと高そう

コミュニティ

  • stapyでイベント企画する

  • PyCon JPスタッフ業(3年目)がんばるぞい

    • コンテンツ盛り上げたい

    • 領収書発行サイトのカイゼン

  • もくもく会の参加率上げる

趣味

  • デジタルに手を出す

    • サブ機としてミラーレスを導入したい
  • 写真で同人誌出してみたい

    • C95で友人が出してたので続きがあるならいっちょ噛みする
  • 百合ポートレートを撮りたい

    • モデルのあてがビタイチない
  • 放置気味のギターを弾く


2019年もよろしくお願いします。