ラベル WSL の投稿を表示しています。 すべての投稿を表示
ラベル WSL の投稿を表示しています。 すべての投稿を表示

2023年1月16日月曜日

InstagramグラフAPIとPythonで指定ハッシュタグ付きの最新投稿を取得する

InstagramグラフAPIを使うとInstagramの投稿情報などを取得できる。ただ、APIの使用に必要なアクセストークンの取得にやや手間がかかる。Pythonで指定したハッシュタグ付きの最新投稿を取得してみたので、アクセストークンの取得も含めて手順をまとめておく。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


APIアクセストークンの取得

InstagramグラフAPIを使うため、FacebookグラフAPIのアクセストークンを取得する。FacebookとInstagramのアカウントはあるとする。Instagramアカウントはプロアカウントに切り替えておく。


FacebookページとInstagramプロアカウントの紐づけ

アクセストークン取得前に、FacebookページとInstagramプロアカウントを紐づけておく。Facebookページがなければ、Facebookのメニューの[ページ]を開いて、[新しいページを作成]から作成できる。

InstagramプロアカウントのFacebookページへの紐づけは、Facebookページの画面の[Instagram投稿を宣伝]>[アカウントをリンク]で行う。


Facebook for Developersでアプリの作成

次に、Facebook for Developersアカウントを作成する。Meta  for Developersにアクセスして、アカウントを作成する。アカウントを作成したら、マイアプリの画面でアプリを作成する。今回は以下の設定で作成。

アプリタイプ:ビジネス
アプリ名:テストアプリ(任意名)
アプリの連絡先メールアドレス:登録メールアドレス(デフォルト)
ビジネスマネージャーアカウントをお持ちですか?:ビジネスマネージャアカウントが設定されていません(デフォルト)

アプリを作成したら、[アプリに製品を追加]からInstagramグラフAPIの[設定]ボタンを押す。これで、メニューの商品欄にInstagramグラフAPIが追加される。ここまでやったら、画面右下の[変更を保存]ボタンを押す。


permissonの追加

Facebook for Developersのトップメニューの[ツール]>[グラフAPIエクスプローラー]を開く。[Metaアプリ]で作成したアプリを選択し、[ユーザーまたはページ]で[ページアクセストークン]からFacebookページを選択してから、permissionを追加する。▼をクリックし、さらにOtherを開くとInstagramのpermissionが表示される。今回は以下5つを追加。

  • instagram_basic
  • instagram_content_publish
  • instagram_manage_comments
  • instagram_manage_insights
  • instagram_manage_messages



permissionを追加し、[Generate Access Token]をクリックすると、Facebookでログインのポップアップ画面が表示される。この画面ではFacebookユーザーとして続行をクリック。

次に表示される画面では、今回は[現在のページのみにオプトイン]を選択。Facebookページを選択してから[続行]ボタンを押す。

続く画面では、今回は[現在のInstagramアカウントのみにオプトイン]を選択。Instagramプロアカウントを選択してから[続行]ボタンを押す。

アプリが要求するアクセス許可の確認画面が表示されるので、[続行]で先に進む。次の画面で[OK]を押すと、アクセストークンが表示される。


無期限アクセストークンの取得

ここまでで取得できたアクセストークンには期限があるので、無期限のアクセストークンを取得する。[ツール]>[グラフAPIエクスプローラー]でグラフAPIエクスプローラを開いてて、[Metaアプリ]で対称のアプリを選択し、アクセストークン左のiアイコンをクリックするとアクセストークン情報が表示される。


画面右下の[アクセストークンツールで開く]をクリックして、アクセストークンツールを開き、[アクセストークンを延長]をクリックすると、期限のないアクセストークンが表示される。


[デバッグ]ボタンを押すと無期限のアクセストークン情報が表示される。有効期限が「受け取らない」になっていればOK。



InstagramビジネスアカウントID の取得

アクセストークンのほかにAPI使用に必要なInstagramビジネスアカウントIDを取得する。Facebook for Developersのトップメニューの[ツール]>[グラフAPIエクスプローラー]を開く。クエリに「me?fields=accounts{instagram_business_account}」を指定して、[送信]ボタンを押して表示されるレスポンスのinstagram_business_accountがInstagramビジネスアカウントID。



指定ハッシュタグ付きの最新投稿を取得

無期限アクセストークンとInstagram ビジネスアカウントIDが取得できたので、指定したハッシュタグが付いた最新の投稿をAPIで取得してみる。

はじめに、APIのエンドポイントアクセスに使うRequestsをインストールしておく。

$ pip3 install requests

指定ハッシュタグ付きの最新投稿を取得する手順としては、IG Hashtag SearchでハッシュタグIDを取得して、そのIDをもとにIGハッシュタグ付きの最近のメディアで最新投稿を検索する。以下コードでは、ハッシュタグ「スイーツ」が付いた最新の投稿を取得する。

from pprint import pprint

import requests

API_VER = 'v15.0'
INSTA_ACCOUNT_ID = 'xxxxxxxxxx' # Instagram ビジネスアカウントID
INSTA_ACCESS_TOKEN = 'xxxxxxxxxxx' # 無期限アクセストークン


# ハッシュタグ「スイーツ」のIDを取得
query = 'スイーツ'
ig_hashtag_search_url = f'https://graph.facebook.com/{API_VER}/ig_hashtag_search?user_id={INSTA_ACCOUNT_ID}&q={query}&access_token={INSTA_ACCESS_TOKEN}'
response = requests.get(ig_hashtag_search_url)
res = response.json()['data']
hashtag_id = res[0]['id']
print('hashtag_id=', hashtag_id)

# ハッシュタグ「スイーツ」がついた最新投稿を取得
recent_media_url = f'https://graph.facebook.com/{hashtag_id}/recent_media?user_id={INSTA_ACCOUNT_ID}&fields=id,media_type,media_url,comments_count,like_count,caption,permalink,timestamp&access_token={INSTA_ACCESS_TOKEN}'
response = requests.get(recent_media_url)
res = response.json()['data']
pprint(res)

上記コードを実行すると、以下のような結果が得られる。

hashtag_id= 17843787853011507
[{'caption': 'xxxxx\n'
             '#xxxxx\n'
             '#xxxxx',
  'comments_count': 0,
  'id': '11111111111111',
  'like_count': 0,
  'media_type': 'IMAGE',
  'media_url': ''https://xxxxx',
  'permalink': 'https://www.instagram.com/p/xxxxx/',
  'timestamp': '2023-01-14T07:30:42+0000'},
 {'caption': 'xxxxxxxxxxxxx\n'
             '\n'
             '#xxxxxxxxxxxxxxxxxx',
  'comments_count': 0,
  ...


2022年12月30日金曜日

PythonとTwitter API v2でリアルタイムにツイートを取得する

Twitter API v1.1のリアルタイムのツイート収集が使えなくなったので、API v2に移行した。PythonライブラリのTweepyとTwitter API v2でリアルタイムにツイート収集する方法をまとめておく。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ pip3 show tweepy
Name: tweepy
Version: 4.12.1
...


Twitter API のBearer Tokenの取得

Twitter API v2でリアルタイムにツイートを収集するために、Twitterの開発者サイトでBearer Tokenを取得しておく。開発者サイトのProjects & Apps > Overviewの画面で、対象アプリの[Keys and tokens](鍵のアイコン)を開く。この画面でBearer Tokenを生成できる。


Tweepyでリアルタイムツイート取得

TweepyでTwitter API v2を使ってリアルタイムにツイートを収集するには、StreamingClientクラスを使う。v1.1のとき(Tweepy4でリアルタイムツイートを取得する)と同様に、このクラスを継承したクラスを作成する。以下のコードでは、Twitter API v2のsampleメソッドを使って、リアルタイムに1%のツイートを出力する。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けページで取得したBearer Tokenを使う
BEARER_TOKEN = 'Bearer Token'

class TwStream(tweepy.StreamingClient):
    def on_tweet(self, tweet):
        print('-------------------------')
        print(tweet.data)

def main():
    twstream = TwStream(BEARER_TOKEN)
    twstream.sample()

if __name__ == '__main__':
    main()

ただ、このコードで取得できるのはツイートのIDやテキストのみで、取得できるデータはv1.1のときと違ってかなり限定されている。v2では、取得する情報を指定する必要がある。


APIで取得対象のデータを指定する

Twitter API v2では、ExpansionsFieldsを指定することで、さまざまなデータを取得できる。以下のコードでは、ExpansionsとTweetフィールド、Userフィールドを指定。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けページで取得したBearer Tokenを使う
BEARER_TOKEN = 'Bearer Token'

class TwStream(tweepy.StreamingClient):
    def on_tweet(self, tweet):
        print('-------------------------')
        print(tweet.data)

def main():
    twstream = TwStream(BEARER_TOKEN)
    # 取得するExpansions
    expansions = ['author_id', 'referenced_tweets.id', 'attachments.media_keys']
    # 取得するTweetフィールド
    tweet_fields = ['author_id', 'created_at', 'attachments', 'lang', 'public_metrics']
    # 取得するUserフィールド
    user_fields = ['created_at', 'description', 'location', 'verified']
    twstream.sample(expansions=expansions, tweet_fields=tweet_fields, user_fields=user_fields)

if __name__ == '__main__':
    main()

上記のように取得データを指定できるが、v1.1のように言語の指定ができない。このままだと、英語やさまざまな言語のツイートが混ざって取得される。filterメソッドを使うと言語の指定ができるが、filterメソッドでは取得できるツイート数に制限があるので、大量のツイートを収集したい場合には使えない。日本語で絞りたい場合は次のようなコードになる。取得したTweetオブジェクトのlangフィールドを参照して、日本語(ja)のみに限定する。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けページで取得したBearer Tokenを使う
BEARER_TOKEN = 'Bearer Token'

class TwStream(tweepy.StreamingClient):
    def on_tweet(self, tweet):
        if tweet.lang == 'ja':
            print('-------------------------')
            print(tweet.data)

def main():
    twstream = TwStream(BEARER_TOKEN)
    # 取得するExpansions
    expansions = ['author_id', 'referenced_tweets.id', 'attachments.media_keys']
    # 取得するTweetフィールド
    tweet_fields = ['author_id', 'created_at', 'attachments', 'lang', 'public_metrics']
    # 取得するUserフィールド
    user_fields = ['created_at', 'description', 'location', 'verified']
    twstream.sample(expansions=expansions, tweet_fields=tweet_fields, user_fields=user_fields)

if __name__ == '__main__':
    main()



2022年12月22日木曜日

作成するDockerイメージを環境により切り替える

 Dockerイメージを作成するときに、環境によってDockerfileで実行する内容を変えたい場合がある。プログラミング言語のif文のように条件によって実行する内容を変えられればいいのだが、Dockerにはif文のようなものはない。そこで、シェルスクリプトのif文を使って、環境によりDockerfileで実行する内容を変えてみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04

Raspberry Pi OS(64bit)。

$ lsb_release -dr
Description:    Debian GNU/Linux 11 (bullseye)
Release:        11

今回はこの2つの環境で、Dockerイメージの内容を切り替えるDockrfileを作成する。


CPUアーキテクチャによりDockerfileの内容を切り替える

Raspberry Piで動作するSelenium+最新PythonのDockerコンテナを作成するでは、Raspberry Pi(ARM64)でDockerイメージを作成するためのDockerfileを作成した。Raspberry PiのCPUはARMで、インストールしているgeckodriverはARM用なので、このDockerfileを使ってWSL2のUbuntu(x86_64)上でDockerイメージを作成しても、インストールされたgeckodriverは動作しない。今回は、このDockerfileをRaspberry PiとWSLのどちらでも動作するようにする。

ただし、Dockerfileではif文のような条件分岐はできないので、CPUアーキテクチャによりインストールするgeckodriverを切り替えられるシェルスクリプトを用意して、それをDockerfileで実行する。


シェルスクリプトの作成

まずはgeckodriverをインストールするシェルスクリプトを作成する。CPUアーキテクチャで条件分岐するために、コマンドでCPUアーキテクチャを取得する。以下のコマンドで、CPUアーキテクチャの文字列を取得できる。


$ lscpu | grep -e Architecture: -e アーキテクチャ: | awk -F : '{print $2}' | xargs
 

上記コマンドの出力結果は、Raspberry Piでは「aarch64」、WSLでは「x86_64」となる。このコマンドを使って、次のシェルスクリプトを作成する。

#!/bin/sh

CPU_ARCH="`lscpu | grep -e Architecture: -e アーキテクチャ: | awk -F : '{print $2}' | xargs`"

if [ "${CPU_ARCH}" = "aarch64" ]; then
    curl -LO https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.32.0-linux-aarch64.tar.gz
    tar xzvf geckodriver-v0.32.0-linux-aarch64.tar.gz
    mv geckodriver /usr/bin/
    rm geckodriver-v0.32.0-linux-aarch64.tar.gz
else
    curl -LO https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.32.0-linux64.tar.gz
    tar xzvf geckodriver-v0.32.0-linux64.tar.gz
    mv geckodriver /usr/bin/
    rm geckodriver-v0.32.0-linux64.tar.gz
fi
 


Dockerfileの作成

シェルスクリプトが用意できたら、Dockerfileを作成する。Raspberry Piで動作するSelenium+最新PythonのDockerコンテナを作成するで作成したDockerfileを変更する。このDockerfileでgeckodriverのインストールをしている箇所を、作成したシェルスクリプトの実行に置き換える。composeファイルなどは前述の記事と同じものを使う。

FROM python:3.11-bullseye
 
WORKDIR /app
 
# Firefoxと日本語フォントインストール
RUN apt update \
    && apt -y install --no-install-recommends \
    # 日本語フォントインストール
    # 日本語フォントをインストールしておかないとFirefoxの表示が文字化けする
    fonts-noto-cjk \
    firefox-esr \
    && apt clean
 
# SeleniumのPythonバインディングインストール
RUN pip install selenium --no-cache-dir

# geckodriverのインストール
COPY ./install_geckodriver.sh ./
RUN chmod u+x install_geckodriver.sh \
    && ./install_geckodriver.sh
 

ファイル構成は以下のようになる。これで、Raspberry PiとWSLのどちらの環境でも、同じDockerfileでコンテナを起動して使える。

|-- Dockerfile
|-- app
| `-- sample.py
|-- install_geckodriver.sh
`-- docker-compose.yml


2022年10月24日月曜日

SQLAlchemyでテーブルレコードを逐次取得してメモリ使用量を減らす

SQLAlchemyでデータベースのテーブルからデータを取得するときに、テーブルレコードの件数が多くなるほど、Pythonプロセスのメモリ使用量が増える。場合によっては、システムのメモリ不足に陥ることもあり得る。SQLAlchemyでは、レコードを逐次取得することでメモリ使用量を減らすことができるので、その方法をまとめておく。


SQLAlchemyのstream_resultsオプションについて

SQLAlchemyはデフォルトでclient side cursorsで動作し、取得したデータをすべてメモリに保持する。一方、server side cursorsで動作した場合は、クライアント側で必要とされる分だけ逐次保持される。つまり、server side cursorsを使うとメモリ使用量を抑えることができる。Working with Engines and ConnectionsのUsing Server Side Cursors (a.k.a. stream results)に説明があり、以下はその抜粋。

A client side cursor here means that the database driver fully fetches all rows from a result set into memory before returning from a statement execution. Drivers such as those of PostgreSQL and MySQL/MariaDB generally use client side cursors by default. A server side cursor, by contrast, indicates that result rows remain pending within the database server’s state as result rows are consumed by the client. 

SQLAlchemyでは、execution_optionsメソッドでstream_results=Trueとすることで、server side cursorsで動作させられる。


環境

WSL2(Ubuntu20.04)とデータベースはMariaDB 10.5。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ mariadbd --version
mariadbd  Ver 10.5.17-MariaDB-1:10.5.17+maria~ubu2004 for debian-linux-gnu on x86_64 (mariadb.org binary distribution)

SQLAlchemyのほかに、DBAPIとしてmysqlclient、メモリ使用量を確認するためにメモリプロファイラーのmemory_profilerをインストールする。

$ pip3 install mysqlclient sqlalchemy memory_profiler

インストールされたバージョン。

$ pip show mysqlclient | grep Version
Version: 2.1.1

$ pip show sqlalchemy | grep Version
Version: 1.4.41

$ pip show memory_profiler | grep Version
Version: 0.60.0


WSL2のDNS設定変更

後述のテストデータベースの準備でgit cloneをするが、そのときに「Could not resolve host: github.com」というメッセージが表示されてcloneできない場合は、UbuntuのDNS設定を変更する(WSL2 DNS stops working #4285)。cloneに問題がない場合は、この項は不要なので、次のテストデータベースの準備へ進む。

WSL2 Ubuntuの/etc/wsl.confに以下記述を追加する。

$ sudo vi /etc/wsl.conf
+ [network]
+ generateResolvConf = false

Ubuntuのターミナルを終了して、一度WSLを停止する。

PS > wsl --shutdown

Ubuntuを起動して、/etc/resolv.confに以下を追記する。

$ sudo vi /etc/resolv.con
+ nameserver 8.8.8.8


テストデータベースの準備

SQLAlchemyでメモリ使用量を確認するためには、それなりのレコード件数のあるテーブルが必要になる。このテスト用データベースとして、Other MySQL Documentationにあるemployee dataを使う。これはGitHubのtest_dbで公開されている。

まずはこのリポジトリをcloneする。

$ git clone https://github.com/datacharmer/test_db.git

このリポジトリにはemployeesというデータベースがあり、以下のようにデータベースをインポートできる。

$ cd test_db
$ mysql -u username -p < employees.sql

インポートが完了したら、テーブルを確認。

$ mysql -u username -p -D employees

MariaDB [employees]> show tables;
+----------------------+
| Tables_in_employees  |
+----------------------+
| current_dept_emp     |
| departments          |
| dept_emp             |
| dept_emp_latest_date |
| dept_manager         |
| employees            |
| salaries             |
| titles               |
+----------------------+


メモリ使用量の比較

stream_results=Trueを設定した場合としない場合でメモリ使用量を比較してみる。以下のようなstream_sample1.pyとdatabase.pyを用意する。以下のコードでは、salariesテーブルから全件を取得して、salaryカラムの平均を求める。stream_results=Trueを設定する場合はfetch_strem関数を、設定しない場合はfetch関数を使用する。

import statistics

from sqlalchemy import MetaData, Table, select
from memory_profiler import profile

from database import db


def _get_table_object(tablename):
    # テーブル名からテーブルオブジェクトを取得
    meta_data = MetaData(bind=db.engine)
    meta_data.reflect(only=[tablename])
    return meta_data.tables[tablename]


@profile
def fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execute(stmt)
    return statistics.fmean([result['salary'] for result in results])


@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True).execute(stmt)
    return statistics.fmean([result['salary'] for result in results])


@profile
def main():
    tablename = 'salaries'
    salary = fetch(tablename)
    # salary = stream_fetch(tablename)
    print(f'salary={salary}')


if __name__ == '__main__':
    main()
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session


class Database:
    def __init__(self):
        db_user = 'username' # DBユーザー名
        db_passwd = 'password' # DB接続パスワード
        db_name = 'employees' # DB名
        self.engine = create_engine(
            f'mysql+mysqldb://{db_user}:{db_passwd}@localhost:3306/{db_name}?charset=utf8mb4'
        )

        self.session = scoped_session(
            sessionmaker(autocommit=False, autoflush=True, bind=self.engine)
        )


db = Database()

以下はfetch関数(stream_results=True設定なし)を使用した場合の結果。SELECT文を実行したところで600MBほどメモリを使用している。

$ time python3 sample_stream1.py
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    88     35.6 MiB     35.6 MiB           1   @profile
    89                                         def fetch(tablename):
    90     36.7 MiB      1.2 MiB           1       con = db.engine.connect()
    91     37.1 MiB      0.3 MiB           1       table_obj = _get_table_object(tablename)
    92     37.1 MiB      0.0 MiB           1       stmt = select(table_obj)
    93    634.7 MiB    597.6 MiB           1       results = con.execute(stmt)
    94    659.2 MiB     24.5 MiB     2844050       return statistics.fmean([result['salary'] for result in results])


salary=63810.744836143705
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
   106     35.6 MiB     35.6 MiB           1   @profile
   107                                         def main():
   108     35.6 MiB      0.0 MiB           1       tablename = 'salaries'
   109     63.3 MiB     27.7 MiB           1       salary = fetch(tablename)
   110                                             # salary = stream_fetch(tablename)
   111     63.3 MiB      0.0 MiB           1       print(f'salary={salary}')



real    3m23.536s
user    2m58.915s
sys     0m23.535s

以下はstream_fetch関数(stream_results=True設定)を使用した場合。SELECT文を実行したところではメモリを消費しておらず、メモリ消費のピークはsalaryカラムの平均を求める箇所で約150MB。stream_results=Trueを設定しない場合に比べて、メモリ使用のピークは4分の1程度となっている。topコマンドで確認した限り、MariaDBサーバーのプロセスが使用するメモリ使用量はどちらでも変わらないので、stream_results=Trueを設定することで、Pythonプロセスで減ったメモリ使用量がそのままシステムのメモリ使用量の減少になる。ちなみに、実行速度とのトレードオフになるが、salaryの平均値を求める際にリストでなくジェネレーター式を使うとさらにメモリ消費を抑えられる。

$ time python3 sample_stream1.py
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    96     35.5 MiB     35.5 MiB           1   @profile
    97                                         def stream_fetch(tablename):
    98     36.7 MiB      1.2 MiB           1       con = db.engine.connect()
    99     37.0 MiB      0.3 MiB           1       table_obj = _get_table_object(tablename)
   100     37.0 MiB      0.0 MiB           1       stmt = select(table_obj)
   101     37.0 MiB      0.0 MiB           1       results = con.execution_options(stream_results=True).execute(stmt)
   102    147.1 MiB      1.6 MiB     2844050       return statistics.fmean([result['salary'] for result in results])


salary=63810.744836143705
Filename: sample_stream1.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
   105     35.5 MiB     35.5 MiB           1   @profile
   106                                         def main():
   107     35.5 MiB      0.0 MiB           1       tablename = 'salaries'
   108                                             # salary = fetch(tablename)
   109     38.2 MiB      2.7 MiB           1       salary = stream_fetch(tablename)
   110     38.2 MiB      0.0 MiB           1       print(f'salary={salary}')



real    3m0.012s
user    2m35.694s
sys     0m23.953s


server side cursorsの注意点

server side cursorsで動作させる場合は注意が必要な点がある。server side cursorsの場合、同じ接続で複数の操作を実行できない。例えば、以下のコードではsalariesテーブルから全件取得したあとにsalariesテーブルからemp_no=10001のレコードを削除しようとしているが、「Commands out of sync; you can't run this command now」というエラーになる。stream_results=Trueを設定していない場合はエラーにならない。

import statistics

from sqlalchemy import MetaData, Table, select, delete
from memory_profiler import profile

from database import db


def _get_table_object(tablename):
    # テーブル名からテーブルオブジェクトを取得
    meta_data = MetaData(bind=db.engine)
    meta_data.reflect(only=[tablename])
    return meta_data.tables[tablename]


@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True).execute(stmt)

    # 以下はエラーになる
    # stream_results=Trueとした同じ接続でテーブル操作を行おうとすると以下エラーになる
    # sqlalchemy.exc.ProgrammingError: (MySQLdb.ProgrammingError) (2014, "Commands out of sync; you can't run this command now")
    stmt = delete(table_obj).where(table_obj.c.emp_no == 10001)
    con.execute(stmt)

    return statistics.fmean([result['salary'] for result in results])


@profile
def main():
    tablename = 'salaries'
    salary = stream_fetch(tablename)
    print(f'salary={salary}')


if __name__ == '__main__':
    main()

複数処理を実行したい場合は、以下のようにデータベース接続を新たに作成する。

import statistics

from sqlalchemy import MetaData, Table, select, delete
from memory_profiler import profile

from database import db


def _get_table_object(tablename):
    # テーブル名からテーブルオブジェクトを取得
    meta_data = MetaData(bind=db.engine)
    meta_data.reflect(only=[tablename])
    return meta_data.tables[tablename]


@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True).execute(stmt)

    # 以下はエラーにならない
    con2 = db.engine.connect()
    stmt2 = delete(table_obj).where(table_obj.c.emp_no == 10001)
    con2.execute(stmt2)

    return statistics.fmean([result['salary'] for result in results])


@profile
def main():
    tablename = 'salaries'
    salary = stream_fetch(tablename)
    print(f'salary={salary}')


if __name__ == '__main__':
    main()

上記コードではstream_results=Trueを設定してSELECTで取得したあとにDELETEしているが、resultsはDELETE前のSELECT時点のデータとなる。


yeild_perオプションについて

stream_results=Trueだけを指定した場合、はじめは少ないバッファーサイズから徐々にmax_row_buffer(デフォルト1000)を上限として増やしていく。それに対して、バッファーサイズを一定にしたい場合は、yeild_perを使う(Fetching Large Result Sets with Yield Per)。yeild_perを使うと、stream_results=Trueの場合と同様にserver side cursorsで動作する。

以下コードは、stream_sample1.pyのstream_fetch関数をyeild_perを使ってバッファサイズを一定(1000)にするように書き換えたもの。

@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(yield_per=1000).execute(stmt)
    return statistics.fmean([result['salary'] for result in results])

stream_results=Trueを指定して上記コードと同じ結果となるコードを書くことができる。ただし、以下のようにオプション指定が増えたりするので、yeild_perを使うほうが簡単。

@profile
def stream_fetch(tablename):
    con = db.engine.connect()
    table_obj = _get_table_object(tablename)
    stmt = select(table_obj)
    results = con.execution_options(stream_results=True, max_row_buffer=1000).execute(stmt)
    return statistics.fmean([result['salary'] for result in results.yield_per(1000)])


2022年10月10日月曜日

WSL2のディストリビューションを別ドライブへ移して空き容量を増やす

 WSL2でUbuntuを使用しているが、Windows側もUbuntuの領域も空き容量が少なくなってきたので、Ubuntuを別ドライブへ移して、Ubuntuの仮想領域を拡張した。その手順をまとめておく。


環境

Windows10 ProとWSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04


現行のUbuntuをエクスポート

別ドライブへ移すWSL2のUbuntuをエクスポートする。

はじめに、WindowsのPower Shelllコンソールを開いて、wslコマンドでUbuntuを停止しておく。

PS > wsl --shutdown

停止したことを確認。

PS > wsl -l -v
  NAME                   STATE           VERSION
* Ubuntu-20.04           Stopped         2

今回はDドライブにexportというフォルダを作成して、そこにエクスポートしたファイル(ubuntu20.tar)を保存する。

PS > mkdir d:\export
PS > wsl --export Ubuntu-20.04 d:\export\ubuntu20.tar 

後でインポートするときに、同じ名前のディストリビューションはインポートできないので、エクスポートが完了したら現行のUbuntuをWSL2から削除しておく。

PS > wsl --unregister Ubuntu-20.04


移行先のドライブへUbuntuをインポート

エクスポートが完了したら、移行先のドライブにインポートする。今回はDドライブにwslというディレクトリを作成して、そこにインポートする。

PS > mkdir d:\wsl
PS > wsl --import Ubuntu-20.04 d:\wsl d:\export\ubuntu20.tar --version 2

インポートが完了したらステータスを確認。

PS > wsl -l -v
  NAME                   STATE           VERSION
* Ubuntu-20.04           Stopped         2


Ubuntuの仮想領域を拡張

続いて別ドライブへ移したUbuntuの仮想領域を拡張する。WSL 2 仮想ハード ディスクのサイズを拡張するを参考に行う。

管理者権限でPower Shellコンソールを開き、diskpartを起動する。

PS > diskpart

diskpartで、インポートしたUbuntuの仮想領域のファイルを選択する。仮想領域のファイルは、インポート先として指定したd:\wsl配下にある拡張子vhdxのファイル。そして仮想領域の情報を確認しておく。WSL2のデフォルトの最大サイズである256MBとなっていることがわかる。

DISKPART> select vdisk file="d:\wsl\ext4.vhdx"
DISKPART> detail vdisk
デバイスの種類 ID: 0 (不明)
ベンダー ID: {00000000-0000-0000-0000-000000000000} (不明)
状態: 追加済み
仮想サイズ:  256 GB
物理サイズ:  234 GB
ファイル名: d:\wsl\ext4.vhdx
子: いいえ
親ファイル名:
関連付けられたディスク番号: 見つかりません。

今回は仮想領域の最大サイズを500GBに拡張する。

DISKPART> expand vdisk maximum=512000
100% 完了しました

DiskPart により、仮想ディスク ファイルは正常に拡張されました。

DISKPART> detail vdisk
デバイスの種類 ID: 0 (不明)
ベンダー ID: {00000000-0000-0000-0000-000000000000} (不明)
状態: 追加済み
仮想サイズ:  500 GB
物理サイズ:  234 GB
ファイル名: d:\wsl\ext4.vhdx
子: いいえ
親ファイル名:
関連付けられたディスク番号: 見つかりません。

DISKPART> exit 

diskpartを終了したら、Ubuntuを起動してログインする。

PS > wsl -d Ubuntu-20.04 -u root

ディスクサイズを確認。256GBからサイズが増えている。

# fdisk -l
...
ディスク /dev/sdb: 500 GiB, 536870912000 バイト, 1048576000 セクタ
Disk model: Virtual Disk
単位: セクタ (1 * 512 = 512 バイト)
セクタサイズ (論理 / 物理): 512 バイト / 4096 バイト
I/O サイズ (最小 / 推奨): 4096 バイト / 4096 バイト 

仮想ディスクサイズが増えたので、次にUbuntuファイルシステムのサイズを拡張する。

# mount -t devtmpfs none /dev
mount: /dev: none は /dev にマウント済みです.
# mount | grep ext4
/dev/sdb on / type ext4 (rw,relatime,discard,errors=remount-ro,data=ordered)

上記コマンド出力結果の/dev/sdbが拡張対象のドライブで、resize2fsコマンドでファイルシステムのサイズを拡張する。

# resize2fs /dev/sdb 512000M
resize2fs 1.45.5 (07-Jan-2020)
Filesystem at /dev/sdb is mounted on /; on-line resizing required
old_desc_blocks = 32, new_desc_blocks = 63
The filesystem on /dev/sdb is now 131072000 (4k) blocks long.

空き容量が増えたか確認してみる。/dev/sdbの容量が約2倍になって、空き容量が増えた。

# df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb        492G  236G  234G  51% /
tmpfs           5.9G     0  5.9G   0% /mnt/wsl
tools           917G  552G  366G  61% /init
...


2022年9月18日日曜日

Twitter Streaming APIで省略されていないツイートテキストを取得する

Twitter Streaming APIでツイートテキストを取得するときに、省略されていないテキスト全文を取得する方法をまとめておく。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


省略されていないテキストを取得する方法

Twitter Streaming APIでツイートテキストを収集する場合、レスポンスデータのtextフィールドに、最大140文字までのテキストが格納されている。

ただし、2017年に半角文字の言語では280文字までに文字数制限が拡張されたために、ツイートテキストが140文字を超える場合がある。ツイートテキストが140文字を超える場合、textフィールドの最後が三点リーダー(…)となり、テキストの一部が省略されてしまう。

拡張された文字数のツイートテキストを取得するには、extended_tweetフィールドを使う。このextended_tweetフィールドはツイートテキストの文字数が140文字を超えた場合に存在し、このフィールドのfull_textフィールドにツイートテキストが格納されている。

ただこれでもまだ省略されてしまう場合がある。それはリツイートの場合で、ツイートテキストにRTとメンションが付与されるので、元のツイートが文字数制限いっぱいだと、ツイートテキストは一部省略されてしまう。

リツイートの場合、レスポンスデータのextended_tweetフィールドに元ツイートの情報があるので、そのツイートテキストを取得することで、省略されていないテキストを取得することができる。


省略されていないテキストを取得するコード

上記をふまえて、PythonでTwitter Streaming APIのレスポンスから省略されていないツイートテキスト全文を取得するコードを作成する。Twitter Streaming APIのデータ取得には、PythonライブラリのTweepyを使う。TweepyでStreaming APIを使う方法はTweepy4でリアルタイムツイートを取得するを参照。以下はStreaming APIでランダムサンプリングを取得して、スクリーンネームとツイートテキストを表示するコード。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けのページで取得したキーとトークンを使う
CONSUMER_KEY = 'Consumer Key'
CONSUMER_SECRET = 'Consumer Secret'
ACCESS_TOKEN = 'Access Token'
ACCESS_TOKEN_SECRET = 'Access Token Secret'

class TwStream(tweepy.Stream):
    def _get_text(self, status):
        # 拡張ツイートの場合
        if hasattr(status, 'extended_tweet'):
            return status.extended_tweet['full_text']
        else:
            return status.text

    def on_status(self, status):
        # status(APIで取得できるデータ)を受け取ったときにon_statusが実行される
        print('-------------------------')
        # リツイートの場合は「RT」+ メンション分だけ文字数オーバーするので省略されることがある
        if hasattr(status, 'retweeted_status'):
            text = self._get_text(status.retweeted_status)
        else:
            text = self._get_text(status)
        # スクリーンネームとツイートを表示
        print(f'@{status.author.screen_name} {text}')

def main():
    twstream = TwStream(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)

    # 日本語ツイートに限定してランダムサンプリングを取得
    twstream.sample(languages=['ja'])

if __name__ == '__main__':
    main()


2022年6月14日火曜日

Pythonで海面水温衛星データを可視化する

G-Portalという地球観測衛星データ提供システムがあり、登録するとさまざまな衛星データを取得できる。今回は気候変動観測衛星「しきさい」(GCOM-C)の海面水温プロダクト(SST) を取得して可視化してみる。


環境

WSL2(Ubuntu20.04)とJupyter。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ jupyter --version
jupyter core     : 4.7.1
jupyter-notebook : 6.2.0
qtconsole        : 5.0.2
ipython          : 7.20.0
ipykernel        : 5.4.3
jupyter client   : 6.1.12
jupyter lab      : 3.0.9
nbconvert        : 6.0.7
ipywidgets       : 7.6.3
nbformat         : 5.1.2
traitlets        : 5.0.5


G-Portalの登録

衛星データをダウンロードするために必要なユーザ登録をしておく。ユーザ登録画面から、アカウント名やメールアドレスなどを登録する。


衛星データのダウンロード

G-Portalトップページのメニューからログインにアクセスして、登録したユーザーアカウントとパスワードでログインする。[衛星からの検索]をクリックして、ダウンロードするデータを選択する画面に遷移する。


1.絞り込み

ダウンロードする衛星データの種類を指定する。

[1.絞り込み]タブの[衛星、センサから選ぶ]を選択する。そして、[GCOM-C/SGLI] > [LEVEL2] > [海洋圏] > [L2-海面水温]のチェックボックスをチェックする。


2.期間指定

取得するデータを検索する期間を指定する。

[2.期間指定]タブで期間を指定する。


3.範囲指定

取得するデータの範囲を指定する。

[3.範囲指定]タブを開いて、右側に表示されている地図で指定するか、緯度・経度を数値で指定する。


検索するデータの指定ができたら[検索する]ボタンを押す。対象データの一覧が表示されるので、使用するデータをダウンロードする。ダウンロードしたファイル名はGC1SG1_L2SG_SSTDQ_3001.h5とする。

ダウンロードしたファイルはHDF形式で、可視化する際にはGeoTIFFが必要になる。データ一覧の[加工]ボタンを押すと、GeoTIFFへの加工要求ができる。加工には少し時間がかかるので、今回は後述するツールでGeoTIFFに変換する。


衛星データをGeoTIFFに変換する

ダウンロードしたHDF形式の衛星データをGeoTIFFに変換する。

まずはG-Portalから変換ツールをダウンロードする。トップページから[ツール・ドキュメント]画面へ遷移し、[GCOM-C]を選択。SGLI 地図投影・GeoTIFF出力ツール(Ver.1.1)をダウンロードする。ここではLinux版を使う。

ファイル名をsgli_geo_map_linuxとしてダウンロードし、先にダウンロードした衛星データを、WSL2上の同じディレクトリに配置する。

変換する前に、ダウンロードした衛星データ内のデータセット名を確認しておく。

! pip3 install h5py

import h5py

def print_dataset_name(name, obj):
    if isinstance(obj, h5py.Dataset):
        print(name)

with h5py.File('./GC1SG1_L2SG_SSTDQ_3001.h5', mode='r') as f:
    f.visititems(print_dataset_name)


上記コードを実行すると、以下のようにHDFファイル内のデータセット一覧が表示される。

Geometry_data/Latitude
Geometry_data/Longitude
Geometry_data/Obs_time
Geometry_data/Sensor_zenith
Geometry_data/Solar_zenith
Image_data/Cloud_probability
Image_data/Line_tai93
Image_data/QA_flag
Image_data/SST

上記データセットのうち、今回使用する海面水温プロダクト(SST)はImage_data/SST。変換時にはこのデータセットを指定する。

以下コマンドでHDF形式からGeoTIFFに変換する。はじめにダウンロードしたツールに実行権限を付与しておく。変換されたGeoTIFFのファイル名はGC1SG1_L2SG_SSTDQ_3001_SST.tif となる。

$ chmod u+x sgli_geo_map_linux
$ ./sgli_geo_map_linux GC1SG1_L2SG_SSTDQ_3001.h5 -d Image_data/SST


SSTデータの値を摂氏(℃)に変換

SSTの値は一般的に使われる温度の単位(摂氏)ではないので、摂氏に変換する必要がある。しきさい(GCOM-C)のページにSSTデータの仕様とその変換方法が記載されている。トップページから[データを使う]>[標準プロダクト&アルゴリズム]を開き、Ver.3の海面水温を選択する。開いた画面のAttribute情報にあるSSTを開く。

この仕様によると、有効な値は0~65531で、65532~65535は陸地や雲、その他のエラーなどで水温が取得できなかった場合。SSTの値を摂氏(℃)に変換する式は以下の通り。DNがSSTの値で、Slopeは0.0012、Offsetは-10となっている。

SST[degree]=DN*Slope+Offset


海面水温データを可視化する

ダウンロードした海面水温衛星データを、Jupyterで可視化してみる。地理空間データ用のPythonライブラリであるrasterioEarthPyを使う。

! pip3 install rasterio # ラスター形式の地理空間データを扱うためのPythonライブラリ
! pip3 install earthpy # ラスターおよびベクター形式空間データを扱うためのPythonライブラリ

import rasterio
from earthpy.mask import mask_pixels
from earthpy.plot  import plot_bands

file_path = './GC1SG1_202206100147B05610_L2SG_SSTDQ_3001_SST.tif'
sst = rasterio.open(file_path)

# バンド番号(1からはじまる)を指定してデータ読み込み
# https://rasterio.readthedocs.io/en/latest/topics/reading.html
sst_array = sst.read(1)
print(sst_array.shape)

# SSTの非有効値をマスクする
# https://earthpy.readthedocs.io/en/latest/api/earthpy.mask.html
mask_values = [65532, 65533, 65534, 65535] # マスクする値
sst_masked = mask_pixels(sst_array, sst_array, vals=mask_values)

# SSTの値を摂氏(℃)に変換
slope = 0.0012
offset = -10.0
sst_deg = sst_masked * slope + offset
sst_deg_min = sst_deg.min() # 最低水温
sst_deg_max = sst_deg.max() # 最高水温

# 海面水温を可視化
plot_bands(sst_deg, vmin=sst_deg_min, vmax=sst_deg_max, cmap='rainbow')

以下のような画像が表示される。陸地や雲などで有効値がない箇所は白色になっている。


2022年5月29日日曜日

Spotify再生回数トップ50の曲を分類して可視化する

Spotify APIで再生回数トップ50の曲データを取得するでは、Spotify APIで再生回数トツプ50の曲一覧を取得した。Spotify APIでは、曲名やアーティスト名などの情報のほかに、曲の特徴を取得できる。曲の特徴とは、ダンスに適した曲か、エネルギッシュさなどといったもので、これらの情報を利用して曲の分類と可視化をしてみる。


環境

WSL2(Ubuntu20.04)とJupyter。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ jupyter --version
jupyter core     : 4.7.1
jupyter-notebook : 6.2.0
qtconsole        : 5.0.2
ipython          : 7.20.0
ipykernel        : 5.4.3
jupyter client   : 6.1.12
jupyter lab      : 3.0.9
nbconvert        : 6.0.7
ipywidgets       : 7.6.3
nbformat         : 5.1.2
traitlets        : 5.0.5

Spotify APIで再生回数トップ50の曲データを取得するの手順でspotipyをインストールしておく。

さらに、曲を特徴でクラスタリングして可視化するためのPythonライブラリをインストールする。

$ pip3 install scikit-learn pandas matplotlib seaborn


spotipyで曲の特徴を取得

APIで取得できる特徴の説明はGet Track's Audio Featuresにある。今回はacousticness(アコースティック度)、danceability(ダンスに適した曲か)、energy(エネルギッシュさ)、instrumentalness(ボーカルがない度合い)、liveness(ライブ感)、loudness(曲全体のボリューム)、speechiness(話し言葉の度合い)、valence(曲のポジティブ度)を使用する。

今回もSpotify APIで再生回数トップ50の曲データを取得すると同様にspotipyを使う。曲の特徴を取得するには、spotifyのaudio_featuresメソッドに引数としてトラックIDのリストを渡す。取得した特徴はPandasのDataFrameにしておく。
import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

def get_tracks(playlist_id):
    results = spotify.playlist_items(
        playlist_id,
        fields=None,
        limit=50,
        offset=0,
        market='JP',
        additional_types=('track', 'episode'),
    )
    items = results['items']

    tracks = []
    for item in items:
        data = {}
        track = item['track']
        data['track_id'] = track['id']
        data['track_name'] = track['name']
        data['artists'] = [ar['name'] for ar in track['artists']]
        tracks.append(data)

    return tracks

def get_track_features(track_ids):
    results = spotify.audio_features(track_ids)
    # idと分類に使用する特徴
    features = ['id', 'danceability', 'energy', 'loudness', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence', 'tempo']
    track_features = []
    for re in results:
        track_features.append(
            {key: value for key, value in re.items() if key in features}
        )
    return track_features

# プレイリストの曲一覧取得
# トップ50 日本
# URLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'
tracks = get_tracks(playlist_id)
track_info = {tr['track_id']: f'{tr["track_name"]} - {"|".join(tr["artists"])}' for tr in tracks}

# 曲の特徴を取得
track_ids = [tr['track_id'] for tr in tracks]
track_features = get_track_features(track_ids)
df_feature = pd.DataFrame([tf for tf in track_features])
df_feature.set_index('id', inplace=True)
df_feature.info()

次のようにDataFrameには50曲分の特徴が格納される。
Index: 50 entries, 4IfrM44LofE9bSs6TDZS49 to 3oQaOjaIYPsnJbGNzXcIID
Data columns (total 9 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   danceability      50 non-null     float64
 1   energy            50 non-null     float64
 2   loudness          50 non-null     float64
 3   speechiness       50 non-null     float64
 4   acousticness      50 non-null     float64
 5   instrumentalness  50 non-null     float64
 6   liveness          50 non-null     float64
 7   valence           50 non-null     float64
 8   tempo             50 non-null     float64


クラスタリングによる分類と可視化

まずはK平均法でクラスタリングする。クラスター数は今回はとりあえず5とする。そして、その結果をもとにクラスターごとの曲を表示する。

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

def get_tracks(playlist_id):
    results = spotify.playlist_items(
        playlist_id,
        fields=None,
        limit=50,
        offset=0,
        market='JP',
        additional_types=('track', 'episode'),
    )
    items = results['items']

    tracks = []
    for item in items:
        data = {}
        track = item['track']
        data['track_id'] = track['id']
        data['track_name'] = track['name']
        data['artists'] = [ar['name'] for ar in track['artists']]
        tracks.append(data)

    return tracks

def get_track_features(track_ids):
    results = spotify.audio_features(track_ids)
    # idと分類に使用する特徴
    features = ['id', 'danceability', 'energy', 'loudness', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence', 'tempo']
    track_features = []
    for re in results:
        track_features.append(
            {key: value for key, value in re.items() if key in features}
        )
    return track_features

def kcluster(df):
    X = StandardScaler().fit_transform(df)
    # とりあえずクラスター数5
    n_clusters = 5
    clusterer = KMeans(init='k-means++', n_clusters=n_clusters, random_state=0)
    kmeans = clusterer.fit(X)
    return kmeans.labels_

# プレイリストの曲一覧取得
# トップ50 日本
# URLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'
tracks = get_tracks(playlist_id)
track_info = {tr['track_id']: f'{tr["track_name"]} - {"|".join(tr["artists"])}' for tr in tracks}

# 曲の特徴を取得
track_ids = [tr['track_id'] for tr in tracks]
track_features = get_track_features(track_ids)
df_feature = pd.DataFrame([tf for tf in track_features])
df_feature.set_index('id', inplace=True)
df_feature.info()

# K平均法でクラスタリング
df_feature['cluster'] = kcluster(df_feature.values)

# クラスターごとの曲名を表示
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
    for cluster, rows in df_feature.groupby('cluster'):
        print(f'\ncluster {cluster}:')
        for id in rows.index:
            print(track_info[id])

結果は以下の通り。

cluster 0:
ミックスナッツ - Official髭男dism
喜劇 - 星野 源
Habit - SEKAI NO OWARI
FEARLESS - LE SSERAFIM
Dynamite - BTS
Butter - BTS
群青 - YOASOBI
一途 - King Gnu
常緑 - 大橋ちっぽけ
怪物 - YOASOBI
WA DA DA - Kep1er
The Feels - TWICE
Bye-Good-Bye - BE:FIRST
ELEVEN - IVE

cluster 1:
ベテルギウス - 優里
なんでもないよ、 - マカロニえんぴつ
水平線 - back number
残響散歌 - Aimer
STAY (with Justin Bieber) - The Kid LAROI|ジャスティン・ビーバー
Cry Baby - Official髭男dism
猫 - DISH//
I LOVE... - Official髭男dism
おもかげ (produced by Vaundy) - milet|Aimer|幾田りら

cluster 2:
M八七 - 米津玄師
ドライフラワー - 優里
カメレオン - King Gnu
シャッター - 優里
勿忘 - Awesome City Club
逆夢 - King Gnu
点描の唄 - Mrs. GREEN APPLE|井上苑子
魔法の絨毯 - 川崎 鷹也
YOKAZE - 変態紳士クラブ
ハート - あいみょん
Stand by me, Stand by you. - 平井 大

cluster 3:
W / X / Y - Tani Yuuki
シンデレラボーイ - Saucy Dog
CITRUS - Da-iCE
恋風邪にのせて - Vaundy
きらり - 藤井 風
LOVE DIVE - IVE
Permission to Dance - BTS
Mela! - 緑黄色社会
夜に駆ける - YOASOBI
三原色 - YOASOBI
115万キロのフィルム - Official髭男dism
ヨワネハキ - MAISONdes|和ぬか|asmi
Pretender - Official髭男dism
踊 - Ado
BOY - King Gnu

cluster 4:
踊り子 - Vaundy

さらに、特徴ごとにクラスター別の箱ひげ図を作成する。上記コードに以下を追記。

# 特徴別の箱ひげ図を作成
fig, ax =plt.subplots(len(df_feature.columns) - 1, 1, figsize=(12, 40), facecolor=(1, 1, 1))
for i, col_name in enumerate(df_feature):
    if col_name != 'cluster':
        sns.boxplot(x=df_feature[col_name], y=df_feature['cluster'], width=0.5, orient='h', ax=ax[i])
        sns.swarmplot(x=df_feature[col_name], y=df_feature['cluster'], color="r", orient='h', dodge=True, ax=ax[i])
plt.tight_layout()
plt.show()

結果は以下のような箱ひげ図が作成される。クラスター4に1曲だけ属する「踊り子」は、ほかの曲に比べてacousticness(アコースティック度)とinstrumentalness(ボーカルがない度合い)が高いことがわかる。



2022年5月8日日曜日

Spotify APIで再生回数トップ50の曲データを取得する

音楽ストリーミングサービスのSpotifyにはAPIが用意されていて、曲に関するさまざまなデータを取得できる。今回は、Spotify APIのPythonラッパーであるspotipyを利用して、プレイリストの曲一覧を取得してみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


Spotify APIのCLIENT IDとCLIENT SECRETの取得

Spotify APIを使用するには、Spotifyの開発者サイトでアプリを作成してCLIENT IDとCLIENT SECRETを取得する必要がある。

アプリを作成するには、Spotify for DevelopersのDASHBOARDにログインして、「CREATE AN APP」をクリックする。アプリ名(App name)とアプリの説明(App description)を記入するだけで作成できる。アプリを作成すると、DASHBOARDでCLIENT IDとCLIENT SECRETが確認できるようになる。


spotipyのインストール

Spotify APIによるデータ取得には、Spotify APIのPythonラッパーであるspotipyを使う。pipでインストールできる。

$ pip3 install spotipy
$ pip3 show spotipy
Name: spotipy
Version: 2.19.0
...

インストールしたら、環境変数SPOTIPY_CLIENT_IDとSPOTIPY_CLIENT_SECRETにそれぞれCLIENT IDとCLIENT SECRETを設定しておく。

export SPOTIPY_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxx
export SPOTIPY_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx


spotipyでプレイリストの曲一覧を取得

spotipyでプレイリスト「トップ50 日本」の曲一覧を取得してみる。spotipyにはプレイリストの曲情報を取得するためのplaylist_itemsメソッドがあり、引数としてプレイリストIDが必要になる。プレイリストIDは、プレイリストURLの最後の22桁。「トップ50 日本」のプレイリストIDは「37i9dQZEVXbKXQ4mDTEBXq」。

playlist_itmesメソッドで取得できるのは曲名、アーティスト名、曲が含まれるアルバム情報などいろいろある。ここでは曲名とアーティスト名を取得して、一覧表示してみる。

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

# プレイリスト「トップ50 日本」のプレイリストID
# SpotifyのプレイリストURLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'

results = spotify.playlist_items(
    playlist_id,
    fields=None,
    limit=50,
    offset=0,
    market='JP',
    additional_types=('track', 'episode'),
)
items = results['items']

tracks = []
for item in items:
    data = {}
    track = item['track']
    data['name'] = track['name']
    data['artists'] = [ar['name'] for ar in track['artists']]
    tracks.append(data)

for i, tr in enumerate(tracks):
    print(i + 1, tr['name'], '-', '|'.join(tr['artists']))

上記コードを実行すると、以下のようにトップ50の曲一覧が取得できる。

1 W / X / Y - Tani Yuuki
2 シンデレラボーイ - Saucy Dog
3 ミックスナッツ - Official髭男dism
4 ベテルギウス - 優里
5 なんでもないよ、 - マカロニえんぴつ
6 カメレオン - King Gnu
7 水平線 - back number
8 喜劇 - 星野 源
9 ドライフラワー - 優里
10 きらり - 藤井 風
11 残響散歌 - Aimer
12 CITRUS - Da-iCE
13 LOVE DIVE - IVE
14 シャッター - 優里
15 Butter - BTS
16 Dynamite - BTS
17 群青 - YOASOBI
18 恋風邪にのせて - Vaundy
19 勿忘 - Awesome City Club
20 逆夢 - King Gnu
21 Permission to Dance - BTS
22 一途 - King Gnu
23 STAY (with Justin Bieber) - The Kid LAROI|ジャスティン・ビーバー
24 夜に駆ける - YOASOBI
25 点描の唄 - Mrs. GREEN APPLE|井上苑子
...
47 まつり - 藤井 風
48 もう少しだけ - YOASOBI
49 虹 - 菅田 将暉
50 FEARLESS - LE SSERAFIM


2022年4月22日金曜日

PythonとCartopyで地図データを可視化する

PythonでGIS用シェープファイルを可視化するでは、日本地図とフェリー航路のシェープファイルをPythonライブラリのGeoPandasで可視化した。日本地図のシェープファイルは国土地理院のホームページからダウンロードしたが、地理空間データ生成用のPythonパッケージであるCartopyを使うと、自分で地図のシェープファイルを用意する必要がない。CartopyにはNatural Earthのシェープファイルをロードするメソッドが用意されている。今回はCartopyを使って地図データを可視化してみる。


環境

WSL2(Ubuntu20.04)とJupyter。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


Pythonライブラリのインストール

必要なPythonライブラリをインストールする。Cartopyのインストールは少し手間がかかるので後述。

$ pip3 install matplotlib geopandas

インストールされたバージョン。

$ pip show matplotlib
Name: matplotlib
Version: 3.5.1
...
$ pip show geopandas
Name: geopandas
Version: 0.10.2
...


Cartopyのインストール

Cartopyはpipでインストールというわけにはいかず、少し手間がかかる。Cartopyをインストールする前に、proj、geos、pyshp、shapelyをインストールしておく必要がある。

まずはprojのインストール。projはaptコマンドでもインストールできるが、バージョンが古いのでソースからインストールする。ビルドの前に必要なパッケージをインストールする。

$ sudo apt install cmake sqlite3 libsqlite3-dev 

続いてソースファイルをダウンロードしてビルドする。今回はバージョン8.2.1を使う。

$ curl -O https://download.osgeo.org/proj/proj-8.2.1.tar.gz
$ tar xzvf proj-8.2.1.tar.gz
$ cd proj-8.2.1
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
$ sudo cmake --build . --target install

次にgoesのインストール。

$ sudo apt install libgeos-dev
$ pip3 install geos

pyshpとshapelyのインストール。

$ pip3 install pyshp shapely

インストールした各ライブラリのバージョン。

$ pip3 show geos
Name: geos
Version: 0.2.3
...
$ pip3 show pyshp
Name: pyshp
Version: 2.2.0
...
$ pip3 show shapely
Name: Shapely
Version: 1.8.1.post1
...

最後にCartopyのインストール。

$ pip3 install cartopy

インストールされたバージョン

$ pip3 show cartopy
Name: Cartopy
Version: 0.20.2
...


CartopyとGeoPandasで地図とフェリー航路を可視化する

地図の他に、PythonでGIS用シェープファイルを可視化すると同様にフェリー航路も可視化する。フェリー航路のシェープファイルは、国土地理院のホームページにある地球地図日本の第2.2版ベクタの全レイヤを使う。ファイルをダウンロードして解凍しておく。フェリー航路のシェープファイルはferryl_jpn.shp。

地図とフェリー航路を可視化するコードは以下の通り。

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import geopandas as gpd

# 解凍したフェリー航路のシェープファイルのパス
path_ferry =  "gm-jpn-all_u_2_2/ferryl_jpn.shp"
df_ferry = gpd.read_file(path_ferry)

plt.figure(figsize=(20, 16))

# 地図投影法を指定
ax = plt.axes(projection=ccrs.PlateCarree())

# Natural Earthの地図シェープファイル(解像度10m)を読み込む
ax.coastlines(resolution='10m')

# フェリー航路をプロット
df_ferry.plot(ax=ax, color='blue', linewidth=1.0, label='Ferry routes')

以下のように地図とフェリー航路が表示される。クリッピングされる地図の位置は、読みこんだシェープファイル(今回はフェリー航路)の位置により自動調整される。


北海道が途中で切れているので北海道全体がクリッピング範囲に入るようにしてみる。Cartopyにはset_extentというメソッドが用意されていて、任意の場所でクリッピングできるらしいのだが、実行しても処理が途中で落ちてしまう。ここでは回避策として、透明なポリゴンの境界を配置して、クリッピング位置が自動調整されたときに北海道が切り取られないようにする。

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import geopandas as gpd
from shapely.geometry import Polygon

# 解凍したフェリー航路のシェープファイルのパス
path_ferry =  "gm-jpn-all_u_2_2/ferryl_jpn.shp"
df_ferry = gpd.read_file(path_ferry)

plt.figure(figsize=(20, 16))

# 地図投影法を指定
ax = plt.axes(projection=ccrs.PlateCarree())

# Natural Earthの地図シェープファイル(解像度10m)を読み込む
ax.coastlines(resolution='10m')

# フェリー航路をプロット
df_ferry.plot(ax=ax, color='blue', linewidth=1.0, label='Ferry routes')

# 指定範囲で地図を切り取る
# set_extentがエラーになるので透明なポリゴンの境界で対処
# ax.set_extent([130, 150, 30, 40], crs=ccrs.PlateCarree())
polygon = Polygon([(130, 40), (150, 40), (150, 30), (130, 30), (130, 40)])
poly_gdf = gpd.GeoDataFrame([1], geometry=[polygon], crs=df_ferry.crs)
poly_gdf.boundary.plot(ax=ax, alpha=0)

以下のように北海道が途中で切れることなくプロットされた。


2022年4月14日木曜日

VS Code上のJupyterノートブックの配色を変える

VS CodeではPipenvとVS Codeの環境でJupyter Notebookを使うのように、Jupyterを使ってグラフなどを表示できる。Jupyter上で動作するpyLDAvisも使えるのだが、VS Codeをダークモードにしていると、以下画像のようにpyLDAvisの結果が見づらい。


そこで、VS Code上のJupyterノートブックの配色を変えてみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


必要なPythonライブラリなどのインストール

VS Code上でJupyterノートブックを作成して、pyLDAvisでLDAの結果を表示するために必要なPythonライブラリなどをインストールする。pyLDAvisとJupyterの他に、LDAモデル作成に使うscikit-learnもインストールする。

$ pip3 install scikit-learn jupyter pyldavis


VS Code上でpyLDAvisの表示

まずはVS Code上でJupyterノートブックを作成して、pyLDAvisでLDAの結果を表示する。PipenvとVS Codeの環境でJupyter Notebookを使うのように、拡張子ipynbのファイルを作成して、以下のコードを記述する。

import re

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
import pyLDAvis
import pyLDAvis.sklearn

pyLDAvis.enable_notebook()

categories = ['rec.sport.baseball', 'rec.sport.hockey', 'comp.sys.mac.hardware', 'comp.windows.x']
twenty_train = fetch_20newsgroups(subset='train', categories=categories, shuffle=True, random_state=42)

def remove_figures(text):
    """数字を除去"""
    text = text.lower()
    text = re.sub(r'\d+', '', text)
    return text

vectorizer = CountVectorizer(preprocessor=remove_figures)

# BOW
X = vectorizer.fit_transform(twenty_train.data)

lda = LatentDirichletAllocation(n_components=5, random_state=0)
lda.fit(X)

pyLDAvis.sklearn.prepare(lda, X, vectorizer)

実行すると以下のようにpyLDAvisでの表示ができているが、VS Codeがダークモードだと背景色と文字色がともに黒系の色なのとても見づらい。



VS Code上のJupyterノートブックの配色を変える

Jupyterの表示結果の配色を変えるには、VS Codeの設定を変更する。VS Codeの設定ファイルを開いて(Windowsの場合は%APPDATA%\Code\User\settings.jsonにある)、以下を追記する。

"workbench.colorCustomizations": {

    "notebook.outputContainerBackgroundColor": "#830af5",

},

設定変更後、ノートブックの背景色が指定した色(#830af5)に変わる。


背景色以外の色も変更可能で、Notebook colorsに設定の一覧がある。


2022年3月21日月曜日

Pythonで新語に対応したmecab-unidic-NEologdを使う

PythonとMeCabでUniDicを使うでは、MeCabとUniDicで形態素解析して、IPA辞書との結果を比較してみた。UniDicにしてもIPA辞書にしても、よく使われる固有名詞には対応しているものの、世の中のさまざまな固有名詞には対応しきれない。ネット資源をもとにIPA辞書をカスタマイズしたmecab-ipadic-NEologdなら、最近登場した新語にも対応できる(mecab-ipadic-NEologdで形態素解析を新語に対応させる)。UniDicにも同様のmecab-unidic-NEologdがあるので、今回はmecab-unidic-NEologdをPythonで使ってみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ git version
git version 2.25.1


MeCabのインストール

IPA辞書やUniDicを使うだけならMeCabのPythonラッパーであるmecab-python3をpipでインストールすればよかったが、NEologdを使うためにはMeCabをインストールする。MeCabのインストールはRaspberry PiのPython3でMecabを使うを参照。


mecab-unidic-NEologdのインストール

gitでリポジトリをクローンして、インストールスクリプトを実行する。

$ git clone --depth 1 https://github.com/neologd/mecab-unidic-neologd.git
$ cd mecab-unidic-neologd
$ ./bin/install-mecab-unidic-neologd -n

インストールできたら、mecab-unidic-NEologdのインストール先ディレクトリを確認しておく。

$ echo `mecab-config --dicdir`"/mecab-unidic-neologd"
/usr/local/lib/mecab/dic/mecab-unidic-neologd


mecab-unidic-NEologdで形態素解析

準備ができたのでmecab-unidic-NEologdで形態素解析してみる。dオプションでmecab-unidic-NEologdのインストールディレクトリを指定する。

import MeCab

tagger = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-unidic-neologd')
text = '青いカザミドリがクルクル回る'
res = tagger.parse(text)
print(res)

上記コードを実行すると次のような結果になる。書字形出現形、発音形出現形、語彙素読み、語彙素、品詞(ハイフン区切り)、活用型、活用形が出力される。この出力項目のフォーマットは、/usr/local/lib/mecab/dic/mecab-unidic-neologd/dicrcで指定されている。

青い    アオイ  アオイ  青い    形容詞-一般     形容詞  連体形-一般

カザミ  カザミ  カザミ  Kazami  名詞-固有名詞-一般

ドリ    ドリ    トリ    鳥      名詞-普通名詞-一般

が      ガ      ガ      が      助詞-格助詞

クルクル        クルクル        クルクル        クルクル        名詞-固有名詞-一般

回る    マワル  マワル  回る    動詞-非自立可能 五段-ラ行       終止形-一般

EOS

以下はUniDicで同じ文字列を形態素解析した結果で、mecab-unidic-NEologdの結果とけっこう違う。またmecab-unidic-NEologdのデフォルトでは、UniDicに比べて取得できる項目が少なくなっている。

青い    形容詞,一般,,,形容詞,連体形-一般,アオイ,青い,青い,アオイ,青い,アオイ,和,"","","","","","",相,アオイ,アオイ,アオイ,アオイ,"2","C1","",73950814151361,269

カザミドリ      名詞,普通名詞,一般,,,

が      助詞,格助詞,,,,,ガ,が,が,ガ,が,ガ,和,"","","","","","",格助,ガ,ガ,ガ,ガ,"","動詞%F2@0,名詞%F1","",2168520431510016,7889

クルクル        副詞,,,,,,クルクル,くるくる,クルクル,クルクル,クルクル,クルクル,和,"","","","","","",相,クルクル,クルクル,クルクル,クルクル,"1","","",2892273994048000,10522

回る    動詞,非自立可能,,,五段-ラ行,連体形-一般,マワル,回る,回る,マワル,回る,マワル,和,"","","","","","",用,マワル,マワル,マワル,マワル,"0","C2","",9928873533907649,36121

EOS

次に、mecab-unidic-NEologdの効果がわかるように「カールおじさんインスタ映え」を形態素解析してみる。以下はmecab-unidic-NEologdを使用した結果。

カールおじさん  カールオジサン  カールオジサン  カールおじさん  名詞-固有名詞-一般

インスタ映え    インスタハエ    インスタハエ    インスタ映え    名詞-固有名詞-一般

EOS

固有名詞と「インスタ映え」とがちゃんと形態素解析できている。対して以下がUniDicの結果。固有名詞などが形態素として認識されていない。

カール  名詞,固有名詞,人名,一般,,,カール,カール-Karl,カール,カール,カール,カール,固,"","","","","","",人名,カール,カール,カール,カール,"1","","",2166596286161408,7882

おじ    名詞,普通名詞,一般,,,,オジ,伯父,おじ,オジ,おじ,オジ,和,"","","","","","",体,オジ,オジ,オジ,オジ,"0","C4","",1362303530443264,4956

さん    接尾辞,名詞的,一般,,,,サン,さん,さん,サン,さん,サン,和,"","","","","","",接尾体,サン,サン,サン,サン,"","C4","",3984363884782080,14495

インスタ        名詞,固有名詞,一般,,,,インスタ,インスタ,インスタ,インスタ,インスタ,インスタ,固,"","","","","","",固有名,インスタ,インスタ,インスタ,インスタ,"","","",92251508095197696,335609

映え    名詞,普通名詞,一般,,,,ハエ,映え,映え,バエ,映え,バエ,和,"ハ濁","濁音形","","","","",体,バエ,バエ,バエ,ハエ,"2","C3","",17388235261100544,63258

EOS


2022年3月9日水曜日

PythonとMeCabでUniDicを使う

PythonとMeCabで形態素解するときに利用できる辞書としては、IPA辞書やネットの資源を利用してIPA辞書をカスタマイズしたmecab-ipadic-NEologdがある。

Raspberry PiのPython3でMecabを使う

mecab-ipadic-NEologdで形態素解析を新語に対応させる

IPA辞書はしばらく更新されていないが、mecab-ipadic-NEologdは定期的に更新されている。ただmecab-ipadic-NEologdは場合によっては副作用がある。

mecab-ipadic-NEologdの辞書を修正する

そこで今回は、今も更新がされているUniDicを使って形態素解析をしてみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


必要なPythonモジュールとUniDicのインストール

まずはPythonでMeCabとUniDicを使用するため、MeCabのPythonラッパーであるmecab-python3とunidic-pyをインストールする。また、あとでUniDicとIPA辞書の比較をするので、IPA辞書もインストールする。

$ pip3 install mecab-python3 unidic ipadic
$ pip3 show mecab-python3
Name: mecab-python3
Version: 1.0.5
...
$ pip3 show unidic
Name: unidic
Version: 1.1.0
...
$ pip3 show ipadic
Name: ipadic
Version: 1.0.0
...

インストールできたらUniDicをダウンロードしておく。

$ python3 -m unidic download


MeCabとUniDicで形態素解析をする

環境が整ったので、UniDicで形態素解析をしてみる。

import MeCab
import unidic

tagger = MeCab.Tagger('-d ' + unidic.DICDIR)
text = '青いカザミドリがクルクル回る'
res = tagger.parse(text)
print(res)

上記コードを実行すると以下の結果が得られる。結果は分割された語ごとに、pos1からlemma_idまでがカンマ区切りになっている。結果の各項目についてはunidic-pyに説明がある。

また、「カザミドリ」は辞書に登録されていないのですべての項目はない。このフォーマットはunidic-pyがインストールされたディレクトリ配下のpython3.8/site-packages/unidic/dicdir/dicrcなどに記述されている。

青い    形容詞,一般,,,形容詞,連体形-一般,アオイ,青い,青い,アオイ,青い,アオイ,和,"","","","","","",相,アオイ,アオイ,アオイ,アオイ,"2","C1","",73950814151361,269

カザミドリ      名詞,普通名詞,一般,,,

が      助詞,格助詞,,,,,ガ,が,が,ガ,が,ガ,和,"","","","","","",格助,ガ,ガ,ガ,ガ,"","動詞%F2@0,名詞%F1","",2168520431510016,7889

クルクル        副詞,,,,,,クルクル,くるくる,クルクル,クルクル,クルクル,クルクル,和,"","","","","","",相,クルクル,クルクル,クルクル,クルクル,"1","","",2892273994048000,10522

回る    動詞,非自立可能,,,五段-ラ行,連体形-一般,マワル,回る,回る,マワル,回る,マワル,和,"","","","","","",用,マワル,マワル,マワル,マワル,"0","C2","",9928873533907649,36121

EOS

次に、書字形基本形だけを取り出してみる。

import MeCab
import unidic

tagger = MeCab.Tagger('-d ' + unidic.DICDIR)
text = '青いカザミドリがクルクル回る'
node = tagger.parseToNode(text)
tokens = []
while node:
    features = node.feature.split(',')
    if features[0] == 'BOS/EOS':
        # BOS/EOSをスキップ
        node = node.next
        continue
    if len(features) >= 11:
        # 書字形基本形
        base_form = features[10]
    else:
        # 書字形基本形がない場合は表層形を使う
        base_form = node.surface

    tokens.append(base_form)
    node = node.next

print(tokens)

結果は以下の通り。

['青い', 'カザミドリ', 'が', 'クルクル', '回る']

さらにUniDicを使うと語彙素も取得できる。語彙素とは異なる形態であるが同じ語であると考えられるものからなる語の集合。「おおきな蒼いふるどけい」を形態素解析すると以下の結果が得られる。品詞からはじまる項目の8番目が語彙素で、語彙素だけを取り出すと['大きな', '青い', 'フル', '時計']となる。「おおきな」が「大きな」、「蒼い」が「青い」などとなる。表記ゆれ対策に使えるかもしれない。

おおきな        連体詞,,,,,,オオキナ,大きな,おおきな,オーキナ,おおきな,オーキナ,和,"","","","","","",相,オオキナ,オオキナ,オオキナ,オオキナ,"1","","",1254551491584512,4564

蒼い    形容詞,一般,,,形容詞,連体形-一般,アオイ,青い,蒼い,アオイ,蒼い,アオイ,和,"","","","","","",相,アオイ,アオイ,アオイ,アオイ,"2","C1","",73950881260225,269

ふる    名詞,固有名詞,地名,一般,,,フル,フル,ふる,フル,ふる,フル,固,"","","","","","",地名,フル,フル,フル,フル,"1","","",56409902964417024,205218

どけい  名詞,普通名詞,一般,,,,トケイ,時計,どけい,ドケー,どけい,ドケー,漢,"ト濁","濁音形","","","","",体,ドケイ,ドケイ,ドケイ,トケイ,"0","C2","",7244141050348032,26354


IPA辞書との比較

UniDicとIPA辞書でどの程度違いがあるのか比較してみる。形態素解析するテキストは、Pythonで青空文庫の作品テキストからルビなどを取り除くの方法でルビなどを除去した青空文庫の『坊ちゃん』。botxhan.txtとして保存しておく。このテキストを形態素解析して、IPA辞書の原型とUnidicの書字形基本形の数をカウントする。以下は名詞のみをカウントして出現数上位20を比較するコード。

from collections import Counter

import MeCab
import unidic
import ipadic

def count(text, dic_arg, base_form_idx):
    tagger = MeCab.Tagger(dic_arg)
    node = tagger.parseToNode(text)
    tokens = []
    while node:
        feature_split = node.feature.split(',')
        pos1 = feature_split[0]

        if pos1 != '名詞':
            node = node.next
            continue

        if len(feature_split) >= base_form_idx+1:
            # 書字形基本形/原形
            base_form = feature_split[base_form_idx]
        else:
            # 書字形基本形/原形がない場合は表層形を使う
            base_form = node.surface

        tokens.append(base_form)
        node = node.next

    return Counter(tokens)

def main():
    # 『坊ちゃん』のテキスト読み込み
    with open('./botchan.txt', encoding='utf8') as f:
        text = f.read()

    unidic_count = count(text, '-d ' + unidic.DICDIR, base_form_idx=10)
    ipadic_count = count(text, ipadic.MECAB_ARGS, base_form_idx=6)

    for rank, (ucnt, icnt) in enumerate(zip(unidic_count.most_common(20), ipadic_count.most_common(20))):
        print(rank+1, ucnt, icnt)

if __name__ == '__main__':
    main()

結果は以下の通り。左側がUnidic、右側がIPA辞書を使った結果。

1 ('事', 292) ('おれ', 472)
2 ('もの', 220) ('の', 373)
3 ('赤', 175) ('事', 291)
4 ('シャツ', 170) ('*', 236)
5 ('山嵐', 154) ('もの', 218)
6 ('一', 153) ('ん', 216)
7 ('時', 115) ('人', 213)
8 ('方', 108) ('君', 184)
9 ('学校', 107) ('赤', 178)
10 ('人', 93) ('一', 173)
11 ('清', 89) ('よう', 173)
12 ('野', 88) ('シャツ', 170)
13 ('顔', 80) ('山嵐', 155)
14 ('気', 79) ('何', 146)
15 ('生徒', 77) ('二', 120)
16 ('今', 73) ('方', 115)
17 ('奴', 72) ('時', 108)
18 ('ところ', 72) ('それ', 100)
19 ('二', 71) ('これ', 99)
20 ('うち', 70) ('三', 92)

形容詞のみにした場合も比較してみる。

1 ('ない', 418) ('ない', 263)
2 ('いい', 121) ('いい', 114)
3 ('よい', 44) ('よい', 42)
4 ('わるい', 36) ('わるい', 36)
5 ('早い', 26) ('早い', 25)
6 ('悪るい', 23) ('長い', 17)
7 ('面白い', 17) ('面白い', 16)
8 ('長い', 17) ('強い', 15)
9 ('強い', 16) ('大きい', 11)
10 ('えらい', 11) ('小さい', 10)
11 ('小さい', 10) ('大人しい', 10)
12 ('大人しい', 10) ('えらい', 10)
13 ('旨い', 10) ('旨い', 10)
14 ('狭い', 10) ('狭い', 10)
15 ('大きい', 10) ('痛い', 9)
16 ('痛い', 9) ('蒼い', 9)
17 ('蒼い', 9) ('まずい', 9)
18 ('難有し', 9) ('嬉しい', 8)
19 ('嬉しい', 8) ('っぽい', 8)
20 ('むずかしい', 8) ('暗い', 8) 

この結果をみただけではUniDicとIPA辞書のどちらがいいのかの判断はできないが、それなりに違いはありそう。


2022年2月25日金曜日

Pythonで漢字やひらがなをローマ字やカタカナに変換する

PykakasiというPythonライブラリを使うと、漢字やひらがなをローマ字やカタカナに変換できる。Pykakasiをインストールして漢字やひらがなを変換するところまでやってみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


Pykakasiのインストール

変換ライブラリのPykakasiはpipでインストールできる。

$ pip3 install pykakasi
$ pip3 show pykakasi
Name: pykakasi
Version: 2.2.1
...


漢字やひらがなをローマ字やカタカナに変換する

Pykakasiがインストールできたら、漢字やひらがなを変換してみる。kakasiクラスのインスタンスを作成して、convertメソッドを使うだけで変換結果が得られる。

from pprint import pprint
import pykakasi

kks = pykakasi.kakasi()
text = "ふんわり名人サイコー"

# 漢字やひらがななどの文字種ごとに変換結果が得られる
result = kks.convert(text)
pprint(result, indent=2)

結果は以下のように、漢字やひらがななどの文字種ごとに変換結果が得られる。origが変換元の文字、hiraがひらがな、kanaがカタカナ、hepburnはヘボン式ローマ字、kunreiは訓令式ローマ字、passportはパスポート表記。

[ { 'hepburn': 'funwari',
    'hira': 'ふんわり',
    'kana': 'フンワリ',
    'kunrei': 'funwari',
    'orig': 'ふんわり',
    'passport': 'funwari'},
  { 'hepburn': 'meijin',
    'hira': 'めいじん',
    'kana': 'メイジン',
    'kunrei': 'meizin',
    'orig': '名人',
    'passport': 'meijin'},
  { 'hepburn': 'saikoo',
    'hira': 'さいこー',
    'kana': 'サイコー',
    'kunrei': 'saikoo',
    'orig': 'サイコー',
    'passport': 'saikoo'}]


2022年2月13日日曜日

PipenvとVS Codeの環境でJupyter Notebookを使う

Jupyter Notebook/Labはデータ分析には便利なのだが、コード編集は使い慣れたIDEなどを使いたい。VS Codeは拡張機能をインストールすることでJupyter Notebookを使えるようになるので、今回はPipenvで構築した環境のVS Code上でJupyter notebookを使えるようにしてみる。Pipenvはパッケージ管理と仮想環境の構築の両方ができるツール。


環境

WSL2(Ubuntu20.04)。VS CodeからRemote DevelopmentでWSL2に接続する。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


VS Codeの設定

今回はVS CodeでWSL2のUbuntu上に接続して環境を作成するので、Remote DevelopmentでWSL2にSSH接続する。方法はRemote DevelopmentでSSH接続先にVS Codeの開発環境を作成するを参照。

VS CodeでWSL2にSSH接続したらVS CodeにPython拡張機能をインストールする。Python拡張機能をインストールするとJupyter拡張機能もインストールされる。Ctrl+Shift+Xで拡張機能のメニューを表示し、検索ボックスで「python」で検索すると見つかる。


Pipenvで環境作成


まずは環境を作成するディレクトリ作成する。VS Code上でCtrl+Shift+@などでターミナルを開いてディレクトリ作成。ここではホームディレクトリ配下にtestenvというディレクトリを作成。
$ mkdir testenv
次にVS Codeから「Open Folder」で作成したディレクトリを開く。この状態でVS Codeのターミナルを開きPipenvをpipでインストール。
$ pip3 install pipenv
$ pip3 show pipenv
Name: pipenv
Version: 2021.11.23
...

続いてPipenvでPython3.8の仮想環境作成。
$ pipenv --python 3.8
Pipenvでjupyterと、Jupyter Notebookの動作確認用にMatplotlibをインストール。
$ pipenv install jupyter matplotlib

jupyterのインストールが完了したら、コマンドパレットを開いて「Python: Select Interpreter」を入力する。Pythonインタープリターの一覧が表示されるので、今回Pipenvで作成したenvtest-xxxxxを選択する。



Jupyterを使う


VS Code上で新規にNotebookを作成するにはコマンドパレットで「Jupyter: Create New Jupyter Notebook」と入力するか、VS CodeのExploreで拡張子ipynbのファイルを作成する。

新規作成したら右上の「Select Kernel」をクリックするとPythonインタープリターの一覧が表示されるので、その中から今回Pipenvで作成したenvtest-xxxxxを選択する。

ここまできたらMatplotlibでグラフを作成してVS Code上で表示してみる。コードを実行してipykernelをインストールするようメッセージが表示されたらインストールする。
%matplotlib inline
import math

import matplotlib.pyplot as plt

f = lambda x: math.exp(pow(x, 2) / 16)
X = [x/10 for x in range(-50, 51)]
y = [f(x) for x in X]
plt.plot(X, y, marker='o')
上記コードを実行すると以下のようなグラフがVS Code上に表示される。


ちなみに、VS Code上のNotebookの行数表示は、コード左側の余白を選択してCtrl-L。

2022年1月10日月曜日

Pythonで絵文字のワードクラウドを作成する

PythonではWordcloudを使うと簡単にワードクラウドを作成できる(Pythonで国会会議録からワードクラウドを作成する)。ワークラウドは文章中のに出現する単語をその出現数などに応じて大きさなどを変えて配置するものだが、今回は絵文字だけを使ったワードクラウド(絵文字クラウド)を作成してみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


データの収集

絵文字クラウドを作成するには絵文字を含むテキストが必要になる。そこでTwitter APIを利用してツイートを収集する。今回はTweepy4でリアルタイムツイートを取得するのようにTweepyを利用する。以下コードでツイートのランダムサンプリングを1000件取得してpickle形式で保存しておく。

import pickle

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けのページで取得したキーとトークンを使う
CONSUMER_KEY = 'Consumer Key'
CONSUMER_SECRET = 'Consumer Secret'
ACCESS_TOKEN = 'Access Token'
ACCESS_TOKEN_SECRET = 'Access Token Secret'

# 取得するツイート数
TWEET_COUNT = 1000

# 取得したツイートを保存するパス
PKL_PATH = './tweet.pkl'


class TwStream(tweepy.Stream):
    texts = []

    def on_status(self, status):
        # status(APIで取得できるデータ)を受け取ったときにon_statusが実行される
        print('-------------------------')
        # スクリーンネームとツイートを表示
        print(f'{len(self.texts)} @{status.author.screen_name} {status.text}')

        self.texts.append(status.text)

        if len(self.texts) >= TWEET_COUNT:
            self.disconnect()


def main():
    twstream = TwStream(
        CONSUMER_KEY,
        CONSUMER_SECRET,
        ACCESS_TOKEN,
        ACCESS_TOKEN_SECRET
    )

    # 日本語ツイートに限定してランダムサンプリングを取得
    twstream.sample(languages=['ja'])

    with open(PKL_PATH, 'wb') as f:
        pickle.dump(twstream.texts, f)


if __name__ == '__main__':
    main()


必要なモジュールなどのインストール

絵文字クラウドの作成に必要なPythonモジュールなどをインストールする。

$ pip3 install wordcloud emoji

$ pip3 list | grep -e wordcloud -e emoji
emoji                    1.2.0
wordcloud                1.8.1


絵文字クラウドの作成

準備ができたので絵文字クラウドを作成する。保存したツイートからから絵文字を抽出して絵文字ごとの出現頻度を集計し、ワードクラウドを作成する。ただし、pngなどの画像に保存するとフォントが表示されない絵文字があるのでSVGで保存する。

import pickle
from collections import Counter

from wordcloud import WordCloud
import emoji

# 取得したツイートを保存したパス
PKL_PATH = './tweet.pkl'

# 最小絵文字頻度
MIN_COUNT = 3


def extract_emojis(text):
    # テキストから絵文字を抽出
    return [c for c in text if c in emoji.UNICODE_EMOJI['en']]


def counter(texts):
    # 絵文字がないテキストを除外
    col = [Counter(t) for t in texts if len(t) > 0]

    # 各テキストの絵文字数を合計
    return sum(col, Counter())


def load_tweets():
    with open(PKL_PATH, 'rb') as f:
        texts = pickle.load(f)

    print(f'len={len(texts)}')
    return texts


def create_word_cloud(features):
    # 絵文字クラウドの作成
    wordcloud = WordCloud(
        background_color='white',
        width=300,
        height=300,
        max_font_size=200
    )
    wordcloud.generate_from_frequencies(frequencies=features)

    # 絵文字クラウドを保存
    # pngなどの画像に直接保存するとフォントが正常に表示されない絵文字があるのでSVGで保存
    # wordcloud.to_file("./emojicloud.png")
    wordcloud_svg = wordcloud.to_svg(embed_font=True)
    with open('./emojicloud.svg', 'w') as f:
        f.write(wordcloud_svg)


def main():
    texts = load_tweets()
    emojis = [extract_emojis(t) for t in texts]
    emoji_freq = counter(emojis)

    # 出現頻度MIN_COUNT以上に制限
    features = {term: cnt for term, cnt in emoji_freq.items() if cnt >= MIN_COUNT}
    print(features)

    create_word_cloud(features)


if __name__ == '__main__':
    main()


作成したSVGをWindowsのChrome、Firefoxで表示するとそれぞれ以下の通り。フォントが違うのか微妙な違いがある。



2021年12月30日木曜日

楽天APIとPythonで楽天市場のランキングを取得する

楽天はいくつかAPIを提供していて(API一覧)、楽天市場のランキングもAPIで取得できる。今回はPythonで楽天市場のランキングを取得してみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ pip3 show requests
Name: requests
Version: 2.25.1
...


ランキングを取得する事前準備

APIの使用にはアプリケーションIDが必要なので事前に取得しておく。アプリケーションIDはRakuten Webserviceでアプリを登録することで取得できる。

今回は楽天市場のランキングを取得したいので、いくつかあるAPIのうち楽天商品ランキングAPIを使用する。このAPIではランキングのジャンルを指定できるので、チョコレートのランキングを取得してみる。

ジャンル指定にはジャンルIDが必要で、ジャンルIDは楽天市場ジャンル検索APIで取得できる。ここではAPIテストフォームを使ってジャンルIDを取得する。ジャンルの階層構造の最上位ジャンルIDが0になっているようで、テストフォームでgenreIdに0を指定してGETボタンを押すとJSON(デフォルト)で最上位のジャンル一覧を取得できる。


その一覧に「スイーツ・お菓子」があるので、そのジャンルIDである551167で再度検索するとチョコレートのジャンルIDが201136であることがわかる。このジャンルIDでランキングを取得する。



楽天商品ランキングを取得する

楽天商品ランキングAPIで楽天市場のチョコレートのランキングを取得する。Pythonコードは以下の通り。

import os
import json

import requests

# APIのエンドポイント
ENDPOINT = 'https://app.rakuten.co.jp/services/api/IchibaItem/Ranking/20170628'

RAKUTEN_APP_ID = 'Rakuten Webserviceで取得したアプリケーションID'


def show_ranking(api_res):
    """ランキング順位とその商品名を表示
    """
    res = json.loads(api_res)
    items = res.get('Items')
    for item in items:
        rank = item.get('rank')
        item_name = item.get('itemName')

        print(f'{rank}位 {item_name}\n')


def get_ranking(genre_id):
    params = {
        'applicationId': RAKUTEN_APP_ID,
        'format': 'json',
        'formatVersion': 2,
        'genreId': genre_id
    }

    r = requests.get(url=ENDPOINT, params=params)
    assert r.status_code == 200, f'status code: {r.status_code}'

    show_ranking(r.text)


def main():
    get_ranking(genre_id='551167')


if __name__ == '__main__':
    main()


以下のようにランキング一覧が取得できる。

1位 ポイント10倍 送料無料 お歳暮 御歳暮 ギフト クリスマス プレゼント お年賀 ヨックモック お菓子 スイーツ 詰め合わせYCD-30 サンク デリス(5種45個入り)洋菓子 クッキー 手土産 個包装 内祝い 焼き菓子 お祝い お返し お取り寄せ 誕生日 チョコ 帰省土産 のし 缶

2位 送料無料 リンツ Lindt チョコレート リンドール 23個入り テイスティングセット| ギフト 洋菓子ギフト かわいい お菓子 スイーツ プレゼント 可愛い 手土産 内祝い 内祝いお返し お礼 リンツチョコ 誕生日 クリスマス X'mas 冬ギフト 退職 職場

3位 お歳暮 ギフト HTA-30AWN 送料無料 タルト・フリュイ・アソート S 秋冬限定お礼 誕生日 プレゼント お菓子 洋菓子 手土産 焼き菓子 出産 内祝 贈り物 お返し 退職 引越し 入学 卒業 定年 退職 ご挨拶 御歳暮
...

取得できる商品の名称は一般的な商品名ではなく、楽天市場の各店舗が設定した名称になっている。APIでは他にも年代や性別も指定できるが、ジャンルIDと同時に指定できない。


2021年12月20日月曜日

Tweepy4でリアルタイムツイートを取得する

Twitter API v1.1でのリアルタイムツイート取得ができなくなっているので、この記事の方法は使えない。リアルタイムにツイートを取得するときは、PythonとTwitter API v2でリアルタイムにツイートを取得するのようにv2を使う。(2022.12.30追記)


 TweepyTwitter APIを利用してツイート検索などを行えるPythonライブラリ。バージョンが3から4になって仕様がけっこう変わっている。今回はTweepyの最新バージョン4でリアルタイムツイートを収集してみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10

Twitter APIの利用に必要なAPIキーやトークンなどが未取得であれば、Twitterの開発者向けサイト(Developer Platform)で取得しておく。


Tweepyのインストール

Tweepyはpipでインストールできる。

$ pip3 install tweepy
$ pip3 show tweepy
Name: tweepy
Version: 4.4.0
Summary: Twitter library for Python
...


Tweepyでリアルタイムにツイートを取得する

Tweepyバージョン3でリアルタイムにツイートを取得するには、StreamListenerを継承するクラスのインスタンスをStreamオブジェクトにわたすようにしていたが、バージョン4ではStreamListenerが廃止されている。TweepyドキュメントのFrequently Asked QuestionsにStreamListener廃止についての記載がある。

Tweepyバージョン4でリアルタイムにツイートを取得するには、Streamを継承するクラスを作成し、そのインスタンスを使う。以下のコードではリアルタイムに日本語ツイートのランダムサンプリングを表示する。

import tweepy

# Twitter APIの認証情報
# Twitterの開発者向けのページで取得したキーとトークンを使う
CONSUMER_KEY = 'Consumer Key'
CONSUMER_SECRET = 'Consumer Secret'
ACCESS_TOKEN = 'Access Token'
ACCESS_TOKEN_SECRET = 'Access Token Secret'

class TwStream(tweepy.Stream):
    def on_status(self, status):
        # status(APIで取得できるデータ)を受け取ったときにon_statusが実行される
        print('-------------------------')
        # スクリーンネームとツイートを表示
        print(f'@{status.author.screen_name} {status.text}')

def main():
    twstream = TwStream(
        CONSUMER_KEY,
        CONSUMER_SECRET,
        ACCESS_TOKEN,
        ACCESS_TOKEN_SECRET
    )

    # 日本語ツイートに限定してランダムサンプリングを取得
    twstream.sample(languages=['ja'])

if __name__ == '__main__':
    main()



2021年11月28日日曜日

WSL(Ubuntu)にRStudio Serverをインストールする

 WSL(Ubuntu)にRStudio Serverをインストールする機会があったので、その手順をまとめておく。


環境


WSL2(Ubuntu20.04)。


Rのインストール


Rの最新版であるR4.1をインストールする。まずは必要なパッケージのインストール。
 

続いて追加するリポジトリ用にGPGキーを追加して、リポジトリの追加。
 

Rと必要なパッケージのインストール。


RStudio Serverのインストール


Rがインストールできたら、続いてRStudio Serverをインストールする。


RStudio Serverの起動


以下のコマンドでRStudio Serverを起動。

起動後にブラウザでhttp://localhost:8787/にアクセスすると、以下のようなログイン画面が表示される。Ubuntuのアカウントでログインできる。


2021年11月15日月曜日

Pythonで世界銀行のオープンデータを使ってバブルチャートを作成する

世界銀行が公開しているオープンデータ(World Bank Open Data)があって、データ取得用のAPIも用意されている。Pythonでこのオープンデータを取得してバブルチャートを作成してみる。


環境


WSL2(Ubuntu20.04)とJupyter Lab。


APIで世界銀行のオープンデータを取得

オープンデータの取得にはWBGAPIを使う。WBGAPIでは世界銀行のAPIを利用してPythonでオープンデータにアクセスできる。インストールはpipでできる。

世界銀行のオープンデータには人口や所得、消費電力などいろいろなデータがある。WBGAPIで「power」という語を含むデータを検索してみる。

import wbgapi as wb

display(wb.series.info(q='power'))

以下のような結果が得られる。データを取得するときにはこの結果にあるidを使う。


次にオープンデータに含まれる国や地域を確認してみる。オープンデータには国別データの他にいくつかの国をまとめた地域ごとのデータもある。

import wbgapi as wb

display(wb.economy.info())

次のような国IDや地域などの情報が確認できる。


先の結果にあるregionの情報は以下のようにして確認できる。

import wbgapi as wb

display(wb.region.info())

データを取得するときにはデータの種類、国または地域、年を指定できる。以下では2019年のアメリカの人口を取得できる。

import wbgapi as wb

display(wb.data.DataFrame(['SP.POP.TOTL'], ['USA'], [2019]))

オープンデータをもとにバブルチャートを作成する

WBGAPIで取得したデータをもとにバブルチャートを作成してみる。まずは必要なPythonライブラリのインストール。

続いて4種類のデータを取得してバブルチャートを作成する。すべての国だと数が多すぎて見づらくなるので人口上位50にしぼってある。使用した4種類のデータは以下の通り。

  • SP.POP.TOTL: 人口
  • NY.GDP.PCAP.CD: ひとりあたりGDP
  • SP.DYN.LE00.IN: 出生時平均寿命
  • BN.GSR.FCTY.CD: 一次純所得

%matplotlib inline

import pandas as pd
import wbgapi as wb
import matplotlib.pyplot as plt
import seaborn as sns

# 地域を除いた国のリストを作成
# regionが空白になっていないidのみにする 
economy_info = wb.economy.info().items
countries = [ei['id'] for ei in economy_info if ei['region'] != '']

# 2019年の国別の以下のデータを取得する
# SP.POP.TOTL: 人口
# NY.GDP.PCAP.CD: ひとりあたりGDP
# SP.DYN.LE00.IN: 出生時平均寿命
# BN.GSR.FCTY.CD: 一次純所得
df=wb.data.DataFrame(
    ['SP.POP.TOTL', 'NY.GDP.PCAP.CD', 'SP.DYN.LE00.IN', 'BN.GSR.FCTY.CD'],
    countries,
    [2019]
)

# データがない国を省いて人口上位50にしぼる
df = df.dropna().sort_values('SP.POP.TOTL', ascending=False).head(50)

plt.figure(figsize=(18, 12))

# バブルチャート作成
# NY.GDP.PCAP.CD: Y軸
# SP.DYN.LE00.IN: X軸
# SP.POP.TOTL: 円の大きさ
# BN.GSR.FCTY.CD: 色
ax = sns.scatterplot(
    data=df, 
    x='NY.GDP.PCAP.CD', 
    y='SP.DYN.LE00.IN', 
    size='SP.POP.TOTL', 
    c=df['BN.GSR.FCTY.CD'], 
    cmap='autumn_r', 
    alpha=0.9, 
    edgecolors='grey', 
    linewidth=1, 
    legend=False, 
    sizes=(20, 5000)
)

# 各円に国IDを表示する
def put_label(x, y, labels, ax):
    a = pd.concat({'x': x, 'y': y, 'label': labels}, axis=1)
    for i, point in a.iterrows():
        ax.text(point['x']+.02, point['y'], str(point['label']))

put_label(df['NY.GDP.PCAP.CD'], df['SP.DYN.LE00.IN'], df.index.to_series(), plt.gca())

plt.show()

次のようなバブルチャートが作成された。