元医療事務エンジニアの語り場

元医療事務エンジニアの語り場

医療系スタートアップ企業で働く元医療事務エンジニアのアウトプットログ

2022年3月2日〜医療関連のトピックまとめ

診療報酬改定 告示・通知の公開

厚労省の資料はURLがコロコロ変わるので、こちらの一覧から探したほうがいい。

令和4年度診療報酬改定について|厚生労働省

令和4年度診療報酬改定について大量の通知文書があるため、以下に主要なものだけ抜粋↓

YouTube医療機関向けの解説動画がアップされていたので、ご活用ください

令和4年度診療報酬改定説明資料

YouTube改定内容説明動画(医療機関向け集団指導用)

資料のデザインが刷新され、かなり見やすくなったのは良い。

一方で、今回の改定では、変更点がハイライトされる「見え消し版」が公開されないとのこと。。

資料に目を通しながら、変更点を拾っていく作業が必要・・・。(酷い)

第7回第8次医療計画等に関する検討会

第7回第8次医療計画等に関する検討会

医療計画・地域医療構想の検討に関する資料ですが、とても良くまとまっています。、

その辺の本を買うよりも医療業界を俯瞰するのに便利な資料

https://www.mhlw.go.jp/content/10800000/000906889.pdf

データヘルス改革 第8回健康・医療・介護情報利活用検討会資料

第8回健康・医療・介護情報利活用検討会資料

オンライン資格確認に対するインセンティブ「電子的保健医療情報活用加算」が新設されましたが、

次の改定時には、「電子カルテの情報をマイナポータルに提供できるか」まで進むのか?スケジュール的にはかなりキツイ。

f:id:kei_01011:20220310001903p:plainf:id:kei_01011:20220310001910p:plainf:id:kei_01011:20220310001919p:plain

特別養護老人ホームの人材確保に関する調査

  • 55.1%の施設が職員不足(平均3人くらい不足
  • 外国人人材を雇用している施設は44.9%、反対に雇用したことがない施設も47.2%
  • 人材紹介会社の利用
    • 人材紹介会社の手数料は高いと感じている(1施設あたり年間平均393.3万円)

https://www.wam.go.jp/hp/wp-content/uploads/220307_No015.pdf

News

CAREBOOK導入病院数が500を突破

病院向けに展開し、全国シェア10%超え(800病院導入)も見えてきた。

地域医療支援病院や特定機能病院に導入されることで、その連携先の病院も入れざるを得ない。地域連携の仕組みをうまく活用していると思います。

CAREBOOK導入病院数が500を突破しました!|新着情報|退院支援の一歩先へCAREBOOK

 2022年2月21日〜医療関連のトピックまとめ

健康・医療戦略推進本部 第3回 次世代医療基盤法検討ワーキンググループ

第3回 次世代医療基盤法検討ワーキンググループ 議事次第|健康・医療戦略推進本部

医療データの二次利用が進んでいる海外の事例が面白かった

各国とも国主導で大規模なDBを保持していて、一部は商用利用も可能。

財政制度分科会

分配戦略の目玉は看護·介護職の賃上げ

財政制度分科会(令和4年2月16日開催)資料一覧 : 財務省

厚生局による診療報酬改定説明会 (集団指導)

説明動画は、令和4年3月初旬に厚生労働省動画チャンネルにアップされる予定です。アップされ次第、下記「3 令和4年度診療報酬改定内容の説明動画等について」にリンクを掲載します。

また、関係通知等も、令和4年3月初旬に厚生労働省ホームページに掲載される予定です。掲載され次第、下記「2 令和4年度診療報酬改定に係る通知等について」にリンクを掲載します。

令和2年度診療報酬改定関係情報/近畿厚生局

WAM:診療所の経営状況

10月からの介護職員の処遇改善加算「介護職員等ベースアップ等支援加算」として決定

第208回社会保障審議会介護給付費分科会(持ち回り)資料

2月から始まっている介護職員等の賃上げの恒久化に向けて行う今年10月の介護報酬改定について、諮問が行われた。

処遇改善加算、特定処遇改善加算に加えて、第3の加算「介護職員等ベースアップ等支援加算」とされた。

「介護職員等ベースアップ等支援加算」は、2月からの補助金のルールを踏襲したもの。

新たに諮問の中でも触れられていますが、既存加算に加えて新たに加算を創設するとなると、より複雑化することが懸念されます。

理想は、既存の加算に結合されることでしたが、複雑過ぎてこれはキツイ・・。

News

医療・介護・保育分野における適正な有料職業紹介事業者

医療・介護・保育分野における 適正な有料職業紹介事業者として 新たに16社認定しました

  • 第一回認定

↓今回新たに16社を認定

論文執筆や学会準備等で手当支払われず…岐阜市民病院で医師への残業代が未払い 4億6千万円余を追加支給へ

岐阜市民病院は論文執筆などが労働にあたるか基準が曖昧だったとして、今後は上司の命令に基づくものかなど判断基準を明確にするとしています。

2024年度から、医師にも残業時間の上限規制が適用され、

論文執筆や学会発表については上司の命令で行うものは勤務時間にカウントされ、自主的に行うものは自己研鑽として線引きされます。

院内業務フローに特化した医療SaaS働き方改革

地域連携のCAREBOOKのように、医療機関の業務フローの中で、特定のポイントを抑えたSaaSが出てきている印象。

2024年へ向けた業界全体の課題が、「医師の働き方改革」であることを考えれば、医師や看護師の業務を半自動化させるサービスは需要がある。

入院案内を半自動化 ポケさぽ

入院案内を半自動化 | ポケさぽ(株式会社OPERe)

麻酔説明を半自動化 MediOS

もともとICの支援システムを提供していたところが、新たに麻酔説明に特化したサービスをリリース

麻酔説明を半自動化|インフォームド・コンセントを支援するコントレア株式会社、麻酔科向けサービスのリリースを発表

表面的な事実だけで判断せず、背景や理由を推し量るWhy型思考

 

「なぜ」を突き詰めれば本質が見えてくる

 

 

 

 

物事の表面だけ見てない?

What → 目に見える形になったもの。情報や知識、具体的な行動

Why → 形の背景、理由、目的

 

物事の表面だけを見るか、物事の背景も含めてみるかの違いがある。

 

 

たとえば

今ある規則やルールは、作られた背景や目的がある。

作られた当時と違う状況になったとしたら、新しいものを作り直すことが必要になる。

 

What型思考の人は、過去の事例や成功体験をそのまま踏襲してしまう事が多い。

 

  • どんな背景、目的があったのか
  • それは現在の状況でも使えるものなのか

 

Whyの部分にも目を向け、使えない部分は積極的に変えていく。

 

 

ビジネスでの依頼者と解決者の関係

依頼者「○○を出してほしいんだけど」

解決者「わかりましたー!」

 

言われたことをそのままうけとって、即座に次のアクションに移っています。

これだと、使用目的がわかりません。

 

 

依頼者「○○を出してほしいんだけど」

解決者「○○でどんなことをするのでしょうか?」

依頼者「実は✗✗をやりたくて、○○のデータを使えないかと思って」

解決者「それなら△△がありますよ」  

 

 

依頼者から言われた(What)を、「なぜ?」の一言で押し返す。

そこから抽出されたニーズや目的(Why)を解釈して、最適なWhatを提案し直す。

 

 

上司からの指示って、大体はWhatのみで降りてくる事が多く、その背景や目的の説明がされない場合がほとんど。

大抵の人はそのまま受け取って処理してしまいがちですよね。

 

なぜ?から思考がスタートする

なぜ?はその裏側を探りたいという意図で、物事の本質に迫っていく質問。

 

重要なのは、なぜ?を繰り返し問うこと。

 

「なぜですか?」と聞くと、毛嫌いされるケースが多いが、本質を見抜く訓練は必須だと思っている。

 

  • 物事を疑ってかかる
  • 他人と同じことをしない
  • 性格悪くなれ!
  • 考える孤独に耐えよ

 

Why型思考人間って、めっちゃ孤独やん。

 

確かに、人と会話していて「なぜ?」「その背景は?」みたいに聞かれたら、、会話したくなくなります。普段の会話でそこまで考えてないって。

 

ビジネス上で本質を考える場合には、どんどん使うべき思考法だとは思いますが、人とのコミュニケーションでは用法用量に注意ですね。

 

 

 

「Why型思考」が仕事を変える 鋭いアウトプットを出せる人の「頭の使い方」 (PHPビジネス新書)

 

【スクレイピング】ナンバーズの当選番号を直近まで全件取得

今回もスクレイピングしていきます。

pythonを使ってのスクレイビング開発の依頼・外注 | Webシステム開発・プログラミングの仕事・副業 【クラウドソーシング ランサーズ】[ID:3468971]

mizuho銀行の宝くじ"ナンバーズ3"の1回目~直近の回の全データを取得したいです。 取得していただきたいデーターは↓ 「 '回別'(第1回) '当選日'(1994年10月7日) '抽選数字'(191) 」

ライブラリインポート

from bs4 import BeautifulSoup
import requests
import pandas as pd
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

url = 'https://www.mizuhobank.co.jp/retail/takarakuji/check/numbers/numbers3/index.html?year=2021&month=5'

op = Options()
op.add_argument("--headless");
op.add_argument('--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36')
op.add_argument('--lang=ja-JP')

直近の当選番号をデータフレームに格納

f:id:kei_01011:20210619071946p:plain

ナンバーズの当選番号はA表とB表に分かれているので、それぞれから収集していきます。

まずはA表から。

# (A表)先月から過去1年間の当せん番号 のリンクを格納した配列を返す
def backnumber_latest_link_to_lists():
    url = 'https://www.mizuhobank.co.jp/retail/takarakuji/check/numbers/backnumber/index.html'

    driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=op)
    driver.get(url)
    res = requests.get(url)
    soup = BeautifulSoup(res.text,'html.parser')
    
    lists = []
    links = driver.find_elements_by_partial_link_text('ナンバーズ3')
    for link in links:
        lists.append(link.get_attribute('href'))
    
    return lists


backnumber_latest = backnumber_latest_link_to_lists()

# A表から回別、抽選日、抽選数字をDataFrameに変換 
backnumbers = []
for link in backnumber_latest:
    url = link

    driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=op)
    driver.get(url)
    no = driver.find_elements_by_class_name('bgf7f7f7')
    date = driver.find_elements_by_class_name('js-lottery-date-pc')
    number = driver.find_elements_by_class_name('js-lottery-number-pc')


    for i in range(0,len(no)):
        backnumber = {}
        backnumber['回別'] = no[i].text
        backnumber['抽せん日'] = date[i].text
        backnumber['ナンバーズ3抽せん数字'] = number[i].text

        backnumbers.append(backnumber)
backnumber_df = pd.DataFrame(backnumbers)

過去の当選番号をデータフレームに格納


#Webドライバーのタイムアウト時間を10秒に設定
driver.implicitly_wait(10)

url = 'https://www.mizuhobank.co.jp/retail/takarakuji/check/numbers/backnumber/index.html'

driver.get(url)

backnumber_links = driver.find_elements_by_css_selector('.typeTK.js-backnumber-b tbody tr td a')

links = []

for item in backnumber_links:
    links.append(item.get_attribute('href'))

all_df = pd.DataFrame()

for i in range(0,len(links)):
    url = links[i]
    driver.get(url)

    # HTMLテーブルを取得
    df = pd.read_html(driver.page_source)[0]
    
    # append dataframe
    all_df = all_df.append(df, ignore_index=True)


# ナンバーズ4抽せん数字の削除 
all_df = all_df.drop(all_df.columns[[3]], axis=1)

全データを結合

df = pd.DataFrame()
df = df.append(all_df, ignore_index=True)
df = df.append(backnumber_df, ignore_index=True)

# 空白行の削除
df = df.dropna(how='all')

CSVデータで出力して完成。

f:id:kei_01011:20210619072600p:plain

今回も、1時間ほどで抽出できました。 案件の単価が1万円〜2万円なので、受注できていたとしたら、いいお小遣いですね!

厚労省の介護情報サイトからPythonでスクレイピング

f:id:kei_01011:20210618130531p:plain

ランサーズに掲載されている「スクレイピング」関連の案件を、実際に受注したと過程してやってみようと思います。

今回はこちら

【1ヶ所のみ抽出】介護の情報データベースのサイトからのスクレイピングの依頼・外注 | Webシステム開発・プログラミングの仕事・副業 【クラウドソーシング ランサーズ】[ID:2593180]

依頼内容

静岡県内に特化した介護・福祉の求人サイトを運営しており、正確なデータベースが必要なので、「介護情報サービス」というサイトから静岡県内のみの「事業開始年月日」という項目をスクレイピングをお願いします。

スクレイピング対象サイトはこちらのようです。

www.kaigokensaku.mhlw.go.jp

見た感じ、そこまで難しくなさそう。 SPAだったので、seleniumを使ってスクレイピングしました。

スクレイピング

ライブラリをインポート

from bs4 import BeautifulSoup
import requests
import pandas as pd
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.select import Select
import math

driverを起動、

url = 'https://www.kaigokensaku.mhlw.go.jp/22/index.php?action_kouhyou_pref_search_list_list=true'

op = Options()
op.add_argument("--disable-gpu");
op.add_argument("--disable-extensions");
op.add_argument("--proxy-server='direct://'");
op.add_argument("--proxy-bypass-list=*");
op.add_argument('--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36')
op.add_argument('--no-sandbox')
op.add_argument('--lang=ja-JP')

driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=op)
driver.get(url)
res = requests.get(url)
soup = BeautifulSoup(res.text,'html.parser')
# 最大ページ数を取得
dataNum = driver.find_element_by_class_name('allDataNum')
post_count = dataNum.text
post_count = post_count.replace('件','')
max_page = math.ceil(int(post_count) / 50)

# selectboxを操作
# 50件表示に変更
select_element = driver.find_element_by_id('displayNumber')
select_object = Select(select_element)
select_object.select_by_value('50')

最大ページ数をループさせてスクレイピング

all_data = []
for page in range(0,(max_page)):
    links = driver.find_elements_by_class_name('detailBtn')
    links.pop()
    link_list = []
    for link in links:
        link_list.append(link.get_attribute('href'))

    for link in link_list:
        add_data = {}
        res = requests.get(link)
        soup = BeautifulSoup(res.text,'html.parser')
        startDateThTag = soup.find('th',string='事業開始年月日')
        startDate = startDateThTag.next_sibling.next_sibling.text
        title = soup.find('span',{'id':'jigyosyoName'}).text

        add_data['title'] = title
        add_data['startDate'] = startDate

        all_data.append(add_data)
    
    sleep(3)
    try:
        next_button = driver.find_elements_by_class_name('page-link')
        next_button[-1].click()
    except:
        print('error')
    sleep(5)

driver.close()
driver.quit()

df = pd.DataFrame(all_data)

結果

f:id:kei_01011:20210618130102p:plain

CSVで出力することに成功しました。

作業時間だけだと、約1時間くらい。 あくまでも、CSVのデータ納品だと過程しての作業でしたが、、

1点うまく行かなかったのが、

Seleniumのwebdriverで、ヘッドレスモードにしたらうまく取得できなかった。 ユーザーエージェントを入れてもだめで、スクリーンサイズを指定してもだめだった。。

ちょっと原因不明なので、この件はまた別で調査することにします。 」

【Tableau】緯度経度で一定位置から範囲内のデータをフィルタさせるパラメータアクション

f:id:kei_01011:20210609061152p:plain

商圏分析をやる機会があったので、Tableauで実装する手段をまとめてみる

地図上のポイントをクリックすると、そのポイントから指定範囲内(画像では1km)のポイントに色付け 範囲内の事業所をフィルタリングして集計することもできる。

仕組みとしては、 カーソルを当てたポイントの緯度を「指定緯度」というパラメータに代入 カーソルを当てたポイントの経度を「指定経度」というパラメータに代入

ダッシュボードの「パラメータアクション」を作成し、 「指定緯度」と「指定経度」とそれぞれのポイントの距離を計算して距離に応じて色を変更する

サンプルVIZ

使えそうな緯度経度の情報がなかったので、ここではOsaka Free Wi-Fiのアクセスポイントを可視化してみた。

www.pref.osaka.lg.jp

パラメータを作成

指定緯度、指定経度は、デフォルトの数値なので、登録するのはなんでもいい。

f:id:kei_01011:20210608185145p:plain

後から距離を指定できるようにしたいので、距離用のパラメータを作成。

f:id:kei_01011:20210608185236p:plain

計算式「距離」を作成

カーソルを当てたポイントの(緯度,経度)とそれぞれの(緯度,経度)の2つをmakepointでラップ、 DISTANCE関数で距離を求める

’m’や’km’の単位をここで指定する。

DISTANCE(
makepoint([指定緯度],[指定経度]),
[#makepoint],
'km'
)

距離判定用の計算式の作成

IF [#距離] = 0
THEN '指定ポイント'
ELSEIF [#距離] <= [距離指定]
THEN '範囲内'
ELSE  '範囲外'
END

それぞれのポイントが、範囲外なのかを判定する計算フィールド

距離のフィルタ

[#距離] <= [距離指定]

地図上のポイントをクリックしたら、他のシートにも指定範囲内でフィルタをかけたい。 上記のサンプルでは、WIFIのアクセスポイントの地図をクリックしたら、その範囲にあるアクセスポイントの詳細が下部分の表にフィルタリングされる。

パラメータアクションを作成

緯度、経度 それぞれのパラメータアクションを作成する。

クリックしたポイントの緯度経度で、パラメータを上書きする。

f:id:kei_01011:20210609060359p:plain

f:id:kei_01011:20210609060426p:plain

これで、マウスを当てたポイントから指定した距離範囲内のポイントについて色を変えることができる 指定した範囲内に存在する顧客数(患者数)を集計したり、商圏分析に利用できる。

BAFFAを使った可視化の方法とかは別途記事にしてみます。

【近傍分析】緯度軽度から指定範囲の場所を抽出する

f:id:kei_01011:20210608180807p:plain

Aテーブルにある緯度経度と、Bテーブルにある緯度経度を総当たりさせて、Bの事業所の半径250m圏内にあるA事業所を抽出したい。

geopyを使用

!pip install geopy

コード

import pandas as pd
A = pd.read_csv('A.csv')
B = pd.read_csv('B.csv')
A_list = hp.values.tolist()
B_list = ph.values.tolist()

from geopy.distance import geodesic
match_lists = []

for B_item in B_list:
    B_id = B_item[0]
    B_lat = B_item[1]
    B_lng = B_item[2]

    for A_item in A_list:
        match_list = {}
        A_id = A_item[0]
        A_lat = A_item[1]
        A_lng = A_item[2]
        bStation = (B_lat,B_lng)
        aStation = (A_lat,a_lng)
        dis = geodesic(bStation,aStation).m
        
        if(dis < 250):
            match_list['A_id'] = A_id
            match_list['B_id'] = B_id
            match_list['distance'] = dis
            match_lists.append(match_list)            
        else:
            pass


df = pd.DataFrame(match_lists)

ロジックは簡単で、単純にループで総当たりさせて、

geodesic(bStation,aStation).mで、2点間の距離をだした。

データフレームには、250m範囲の事業所が縦持ちで入っているので、商圏分析的に使用できる。

【K近傍法】scikit-learnでアヤメの多クラス分類(iris-dataset)

3種類のアヤメについて、それぞれ50サンプルのデータがあります。 それぞれ、Iris setosa、Iris virginica、Iris versicolorという名前で、全部で150のデータ。

4つの特徴量が計測されていて、これが説明変数。

  • 萼片(sepal)の長さ(cm)
  • 萼片(sepal)の幅(cm)
  • 花びら(petal)の長さ(cm)
  • 花びら(petal)の幅(cm)

アヤメの分類は3つのクラス

  • Iris-setosa (n=50)
  • Iris-versicolor (n=50)
  • Iris-virginica (n=50)

特徴量から、それぞれの分類を予測していきます。

データ準備

import numpy as np
import pandas as pd
from pandas import Series,DataFrame

import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('whitegrid')

%matplotlib inline
from sklearn import linear_model
from sklearn.datasets import load_iris

# データの読み込み
iris = load_iris()

# 説明変数をXに
X = iris.data

#目的変数をYに
Y = iris.target
iris_data = DataFrame(X,columns=['Sepal Length','Sepal Width','Petal Length','Petal Width'])

iris_target = DataFrame(Y,columns=['Species'])
def flower(num):
   ''' 数字を受け取って、対応する名前を返す'''
   if num == 0:
       return 'Setosa'
   elif num == 1:
       return 'Veriscolour'
   else:
       return 'Virginica'

iris_target['Species'] = iris_target['Species'].apply(flower)
# 1つのテーブルにまとめる
iris = pd.concat([iris_data,iris_target],axis=1)

iris.head()

特徴量を可視化

#pairPlot で可視化
sns.pairplot(iris,hue='Species',size=2)

f:id:kei_01011:20210521123443p:plain

plt.figure(figsize=(12,4))
sns.countplot('Petal Length',data=iris,hue='Species')

f:id:kei_01011:20210521123520p:plain

plt.figure(figsize=(12,4))
sns.countplot('Petal Width',data=iris,hue='Species')

f:id:kei_01011:20210521123542p:plain

ロジスティック回帰で予測 

ここではロジスティック回帰による分類を簡単に実装してみる。

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

logreg = LogisticRegression()

# テストが全体の40%
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.4,random_state=3)

# データを使って学習
logreg.fit(X_train, Y_train)
LogisticRegression()
# 精度計算
from sklearn import metrics

# テストデータを予測
Y_pred = logreg.predict(X_test)

# 精度を計算
print(metrics.accuracy_score(Y_test,Y_pred))
0.9666666666666667

0.9666666666666667 とかなり精度が高い。

次に、K近傍法で予測してみる

K近傍法で予測

K近傍法は英語で、k-nearest neighbor

学習のプロセスは、単純に学習データを保持するだけ。

与えられたサンプルのk個の隣接する学習データのクラスを使い、このサンプルのクラスを予測します。

f:id:kei_01011:20210521123627p:plain

★が新しいサンプル

これを中心に、 K=3ではAが1つ、Bが2つなので、分類されるクラスは、Bです。 K=6ではAが4つ、Bが2つなので、Aと判別される。

Kの選び方によっては、同数になってしまうことがあるので注意が必要な手法

# K近傍法
from sklearn.neighbors import KNeighborsClassifier

# k=3
knn = KNeighborsClassifier(n_neighbors = 3)

# 学習
knn.fit(X_train,Y_train)

# テストデータを予測
Y_pred = knn.predict(X_test)

# 精度 check
print(metrics.accuracy_score(Y_test,Y_pred))

→ 0.95

# k=1にしてみる
knn = KNeighborsClassifier(n_neighbors = 1)

knn.fit(X_train,Y_train)
Y_pred = knn.predict(X_test)
print(metrics.accuracy_score(Y_test,Y_pred))
0.9666666666666667

もっとも近いサンプルに合わせて分類すると精度が上がった

kの数値による予測精度の推移を見てみる

# 1~90まで繰り返す
k_range = range(1, 90)

accuracy = []

for k in k_range:
   knn = KNeighborsClassifier(n_neighbors=k)
   knn.fit(X_train, Y_train)
   Y_pred = knn.predict(X_test)
   accuracy.append(metrics.accuracy_score(Y_test, Y_pred))

# plot
plt.plot(k_range, accuracy)
plt.xlabel('K for kNN')
plt.ylabel('Testing Accuracy')

f:id:kei_01011:20210521123729p:plain

kは増やしすぎても精度が下がる

こんな感じで、もっとも精度の高いところを選択するのが良さそう

【scikit-learn】ロジスティク回帰でアヤメの分類

前回、パーセプトロンによるアヤメの分類を行ってみました。

kei01011.hatenablog.com

クラスを完全に線形分離できない場合は決して収束しないという問題点もあるため、ロジスティック回帰による分類を試してみた。

データ準備

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
import seaborn as sns

from sklearn import datasets
iris = datasets.load_iris()

X = iris.data[:,[2,3]]

y = iris.target

print('Class',np.unique(y))

from sklearn.model_selection import train_test_split

X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.3,random_state=1,stratify=y)

from sklearn.preprocessing import StandardScaler
sc = StandardScaler()

# トレーニングデータの平均と標準偏差を計算
sc.fit(X_train)

#平均と標準偏差を用いて標準化
X_train_std = sc.transform(X_train)
X_test_std = sc.transform(X_test)

ロジスティック回帰モデルのトレーニン

from sklearn.linear_model import LogisticRegression as LR

lr = LR(C=100.0,random_state=1)
lr.fit(X_train_std,y_train)

# plot
plot_decision_regions(X_combined_std, y_combined, classifier=lr,test_idx=range(105,150))

# plot label
plt.xlabel('petal length std')
plt.ylabel('petal width std')

plt.legend(loc='upper left')

plt.tight_layout()
plt.show()

f:id:kei_01011:20210521123106p:plain

predictを出力してみる。

lr.predict_proba(X_test_std[:3,:])
array([[1.52213484e-12, 3.85303417e-04, 9.99614697e-01], [9.93560717e-01, 6.43928295e-03, 1.14112016e-15], [9.98655228e-01, 1.34477208e-03, 1.76178271e-17]])

1行目は1つ目のサンプルに関するクラスの所属確率 2行目は2つ目のサンプルに関するクラスの所属確率を表している

1行目でもっとも大きいのは0.853で、1つ目のサンプルがクラス3(Virginica)に属している確率を85.3%と予測している。

【scikit-learn】パーセプトロンでアヤメの分類を実装

今回は、パーセプトロンでアヤメの分類を実装していきたいと思います。

パーセプトロンとは

分類問題に使われる手法の一つで、いわゆる「教師あり学習」を行うためのもの。

「あるデータがどのグループに属するか」を学習して、未知のデータからグループごとに分類することができる。

「教師ラベル」といって、学習用データにはどのグループに属しているかという情報があらかじめ与えられていて、この「教師データ」を使って学習していきます。

パーセプトロンは、実はあまり使われていないらしく その理由としては

「与えられたデータが線形分離可能でなければアルゴリズムが収束しない」 という弱点があるかららしい。

線形分離とは、その名前の通りで「一本の直線で二つのグループに分離できる」ことです。 複雑な分類には向いていないのかなーと。

scikit-learnのirisデータセットを使って分類してみました。

アヤメのサンプルデータの準備

# import
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
import seaborn as sns

# import datasets
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data[:,[2,3]]
y = iris.target

# データセット分割 テストデータ30%
from sklearn.model_selection import train_test_split

X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.3,random_state=1,stratify=y)

データの標準化

from sklearn.preprocessing import StandardScaler
sc = StandardScaler()

# トレーニングデータの平均と標準偏差を計算
sc.fit(X_train)

#平均と標準偏差を用いて標準化
X_train_std = sc.transform(X_train)
X_test_std = sc.transform(X_test)

レーニングデータとテストデータの値を相互に比較できるように、標準化をしておく

パーセプトロンのインポート、学習

# パーセプトロンのインポート
from sklearn.linear_model import Perceptron
# エポック数40、学習率0.1でパーセプトロンのインスタンス作成
ppn = Perceptron(eta0=0.1, random_state=1)
# トレーニングデータをモデルに適合
ppn.fit(X_train_std, y_train)

# 予測
y_pred = ppn.predict(X_test_std)

scikit-learn.org

accuracy_scoreで正解率を確認してみます。

# 分類の正解率
from sklearn.metrics import accuracy_score

print('Accuracy: %.2f' % accuracy_score(y_test, y_pred))
# print('Accuracy: %.2f' % ppn.score(X_test_std, y_test)) score でもOK

Accuracy: 0.98 0.98?精度高すぎませんか?

決定領域をプロット

from matplotlib.colors import ListedColormap
def plot_decision_regions(X,y,classifier, test_idx=None,resolution=0.02):
   
   # マーカーとカラーマップ
   markers = ('s','x','o','^','v')
   colors = ('red','blue','lightgreen','gray','cyan')
   cmap = ListedColormap(colors[:len(np.unique(y))])
   
   # 決定領域のプロット
   x1_min, x1_max = X[:,0].min() - 1, X[:,0].max() + 1
   x2_min, x2_max = X[:,1].min() - 1, X[:,1].max() + 1
   
   # グリッドポイントの生成
   xx1,xx2 = np.meshgrid(np.arange(x1_min,x1_max,resolution),
                        np.arange(x2_min,x2_max,resolution))
   
   # 各特徴量を1次元配列に変換して予測を実行
   Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
   
   # 予測結果を元のグリッドポイントのデータサイズに変換
   Z = Z.reshape(xx1.shape)
   
   # グリッドポイントの等高線のプロット
   plt.contourf(xx1,xx2,Z,alpha=0.3,cmap=cmap)
   
   # 軸の範囲の設定
   plt.xlim(xx1.min(), xx1.max())
   plt.ylim(xx2.min(), xx2.max())
   
   # クラスごとにサンプルをプロットする
   for idx, cl in enumerate(np.unique(y)):
       plt.scatter(x=X[y == cl, 0], y=X[y == cl,1],
                  alpha=0.8,
                  c=colors[idx],
                  marker=markers[idx],
                  label=cl,
                  edgecolor='black')
       
   # テストサンプルを目出させる
   if test_idx:
       X_test, y_test = X[test_idx, :], y[test_idx]
       plt.scatter(X_test[:,0], X_test[:,1],
                  c='',
                  edgecolor='black',
                  alpha=1.0,
                  linewidth=1,
                  marker='o',
                  s=100,
                  label='test set')


# トレーニングデータとテストデータの特徴量を行方向に結合
X_combined_std = np.vstack((X_train_std, X_test_std))

# トレーニングデータとテストデータのクラスラベルを結合
y_combined = np.hstack((y_train,y_test))

# 決定領域のプロット
plot_decision_regions(X=X_combined_std, y=y_combined, classifier=ppn,
                    test_idx=range(105,150))

plt.xlabel('petal length [std]')
plt.ylabel('petal width [std]')

plt.legend(loc='upper left')

plt.tight_layout()
plt.show()

f:id:kei_01011:20210521122837p:plain

正解率は高かったですが、、

「3つの品種(多クラス)を線形の決定境界で完全に区切ることはできない」ということがわかりました。

今回はこのくらいにして、次回からはロジスティック回帰や決定木で分類してみたいと思います。

seleniumでマネーフォワードをスクレイピング、LINEへ通知する

f:id:kei_01011:20210519083717p:plain

今月の請求額を各サイトからスクレイピングして、「引き落とし口座にいくら入金すればいいのか」を毎月LINEに通知するシステムを構築中。

楽天カードの請求額をスクレイピングして、LINE Notifyで通知するところまで設定しました。

kei01011.hatenablog.com

今回は、楽天カードのほか、以下の項目を取得して通知するロジックを作っていきます。

  • 自然派コープの請求額を取得
  • マネーフォワードの口座情報を更新
  • マネーフォワードから引落とし口座の残高を取得

スクレイピング自然派コープの請求額を取得

我が家で注文している、コープの請求額を取得します。

コープは、毎月18日ごろに請求額が出てくるので、20日前後に取得させたほうがよさそう。

def get_corp_bill():
    username = 'xxxxx'
    password =  'xxxxxxx'
    target_url = 'https://www.shizenha.ne.jp/shizenha_online/'
    error_flg = False
    options = Options()
    options.add_argument('--headless')
    driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=options)
    driver.get(target_url)
    sleep(3)
    # login
    try:
        username_input = driver.find_element_by_xpath("//input[@name='LoginCode']")
        username_input.send_keys(username)
        sleep(1)
        password_input = driver.find_element_by_xpath("//input[@name='Password']")
        password_input.send_keys(password)

        login_button = driver.find_element_by_xpath("//a[@id='btnLogin']")
        login_button.click()
        sleep(1)

    except Exception:
        error_flg = True
        print('ユーザー名、パスワード入力時にエラーが発生しました')
        driver.close()
        driver.quit()
        pass

    # 請求書ページを開く
    if error_flg is False:
        try:
            driver.get('https://www.shizenha.ne.jp/shizenha_online/Main/Mypage')
            sleep(3)
            notnow_button = driver.find_element_by_xpath("//a[text()='オンライン請求書照会']")
            notnow_button.click()
            sleep(3)
        except Exception:
            print('buttonエラーが発生しました')
            # 例外の場合、処理をしない pass
            driver.close()
            driver.quit()
            pass

    soup = BeautifulSoup(driver.page_source,'html.parser')
    elms = soup.select('body table.bill_list')
    df = pd.read_html(str(elms))[0]
    df = df.drop(df.columns[[0,3]], axis=1)
    df.columns = ['請求月','支払総額']

    latests = {
        "month" : df['請求月'][0],
        "bill" : int(df['支払総額'][0].replace(',','').replace('円',''))
    }

    driver.close()
    driver.quit()

    return latests

マネーフォワードの口座情報を自動で更新

マネーフォワードの無料会員だと、それぞれの口座ごとに「更新」ボタンを押さないと口座情報が更新されません。

ついでなので、登録されている口座を一気に更新しておきます。

f:id:kei_01011:20210521064442p:plain

# FWにログイン 口座情報を更新
def fw_reloaded_bank():
    username = 'email@example.com'
    password =  'xxxxxxxxxxxxxxxxxx'
    assets = {}

    options = Options()
    options.add_argument('--headless')                 # headlessモードを使用する
    options.add_argument('--disable-gpu')              # headlessモードで暫定的に必要なフラグ(そのうち不要になる)
    options.add_argument('--disable-extensions')       # すべての拡張機能を無効にする。ユーザースクリプトも無効にする
    options.add_argument('--proxy-server="direct://"') # Proxy経由ではなく直接接続する
    options.add_argument('--proxy-bypass-list=*')      # すべてのホスト名
    options.add_argument('--start-maximized')          # 起動時にウィンドウを最大化する
    options.add_argument('--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36') # UserAgentを偽装する
    driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=options)

    # login
    driver.get("https://id.moneyforward.com/sign_in/email")
    driver.implicitly_wait(10) # 秒

    driver.find_element_by_xpath("//input[@name='mfid_user[email]']").send_keys(username)
    driver.implicitly_wait(5) # 秒

    driver.find_element_by_class_name("submitBtn").click()
    driver.implicitly_wait(10) # 秒

    driver.find_element_by_xpath("//input[@name='mfid_user[password]']").send_keys(password)
    driver.implicitly_wait(5) # 秒

    driver.find_element_by_class_name("submitBtn").click()
    driver.implicitly_wait(10) # 秒

    driver.get('https://moneyforward.com/accounts/show/zxc8ekTu_VM7A8bTRaI3cg')
    driver.implicitly_wait(20) # 秒
    driver.find_element_by_xpath("//input[@type='submit']").click()
    driver.implicitly_wait(10) # 秒

    driver.get("https://moneyforward.com/accounts")

    elms = driver.find_elements_by_xpath("//input[@data-disable-with='更新']")
    for elm in elms:
        elm.click()
        sleep(1)

seleniumでマネーフォワードから引落とし口座の残高を取得

マネーフォワードにログインできたので、ポートフォリオのページから、引き落とし用の口座の残高、ついでに合計資産を取得してきます。

# カード引き落とし口座の残高、合計資産を取得
def fw_get_assets():
    username = 'email@example.com'
    password =  'xxxxxxxxxxxxxxxxxx'
    assets = {}

    options = Options()
    options.add_argument('--headless')                 # headlessモードを使用する
    options.add_argument('--disable-gpu')              # headlessモードで暫定的に必要なフラグ(そのうち不要になる)
    options.add_argument('--disable-extensions')       # すべての拡張機能を無効にする。ユーザースクリプトも無効にする
    options.add_argument('--proxy-server="direct://"') # Proxy経由ではなく直接接続する
    options.add_argument('--proxy-bypass-list=*')      # すべてのホスト名
    options.add_argument('--start-maximized')          # 起動時にウィンドウを最大化する
    options.add_argument('--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36') # UserAgentを偽装する
    driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=options)

    # login
    driver.get("https://id.moneyforward.com/sign_in/email")
    driver.implicitly_wait(10) # 秒

    driver.find_element_by_xpath("//input[@name='mfid_user[email]']").send_keys(username)
    driver.implicitly_wait(5) # 秒

    driver.find_element_by_class_name("submitBtn").click()
    driver.implicitly_wait(10) # 秒

    driver.find_element_by_xpath("//input[@name='mfid_user[password]']").send_keys(password)
    driver.implicitly_wait(5) # 秒

    driver.find_element_by_class_name("submitBtn").click()
    driver.implicitly_wait(10) # 秒

    driver.get('https://moneyforward.com/accounts/show/zxc8ekTu_VM7A8bTRaI3cg')
    driver.implicitly_wait(20) # 秒
    driver.find_element_by_xpath("//input[@type='submit']").click()
    driver.implicitly_wait(10) # 秒

    # カード引き落とし口座の残高
    soup = BeautifulSoup(driver.page_source,'html.parser')
    assets["card_bank"] = soup.find('h1',{'class':'heading-small'}).text
    assets["card_bank"] = int(assets["card_bank"].replace('資産総額:','').replace(',','').replace('円',''))
    driver.implicitly_wait(20) # 秒

    # 合計資産
    driver.get('https://moneyforward.com/bs/portfolio')

    sleep(5)

    soup = BeautifulSoup(driver.page_source,'html.parser')
    elms = soup.select('section.bs-total-assets table')
    df = pd.read_html(str(elms))[0]
    df = df.T.drop(0,axis=0).drop(2,axis=0)
    df.columns = ['預金合計資産','投資信託']
    df['預金合計資産'][1] = df['預金合計資産'][1].replace(',','').replace('円','')
    df['投資信託'][1] = df['投資信託'][1].replace(',','').replace('円','')
    df = df.astype('int')
    df['預金合計資産'][1] + df['投資信託'][1]

    assets["bank"] = df['預金合計資産'][1]
    assets["stock"] = df['投資信託'][1]
    assets["assets_all"] = df['預金合計資産'][1] + df['投資信託'][1]


    driver.close()
    driver.quit()

    return assets

LINE Notifyへ通知する

それぞれのスクレイピング→通知までできるか、テストしてみます。 残高が足りなかった場合とメッセージをだし分け、いくら残高が不足しているのかをわかりやすくします。

def cal_billing():
    rakuten = get_rakuten_bill()
    corp = get_corp_bill()
    assets = fw_get_assets()
    bill_home = 85000 # 家賃
    # 合計請求額
    bill_all = rakuten + corp['bill'] + bill_home
    # 差額
    cal_billing = assets['card_bank'] - bill_all

    cal_price = ''
    if cal_billing < 0:
        cal_price = f'▷ {cal_billing * -1}円足りません'
    else:
        cal_price = f'▷ 引き落とし後の残高は{cal_billing}円です'
    messages = f"""
    〜〜今月の請求額のお知らせ〜〜
    ■請求金額合計: {bill_all}円
    ---------
     ・カ ー ド: {rakuten}円
     ・自 然 派: {corp['bill']}円
     ・住 居 費: {bill_home}円
    ---------
    ■口座残高: {assets['card_bank']}円\n
    {cal_price}\n
    ■資産状況
    ---------
    預金資産: {assets["bank"]}円
    投資信託: {assets["stock"]}円
    資産合計: {assets["assets_all"]}円
    -----------------
    """

    return messages

if __name__ == "__main__":
    notify_message(cal_billing())

結果

f:id:kei_01011:20210521065854p:plain

無事、スクレイピング結果を通知できました!!

当初の目的であった「引き落とし用口座にいくら入金すればいいか」が簡単にわかる仕組みができたので万々歳です。

一応さらにリファクタリングしてから使ってみようと思います。 定期実行は、、Webサーバーに置くとseleniumの動作が不安なので、ローカルで実行させるつもり。

【Python】楽天カードの請求確定額をLINE Notifyで通知してみた

f:id:kei_01011:20210519083717p:plain カード請求額を毎回チェックするのは面倒なので、LINE Notifyで通知する仕組みを構築中です。

今回まとめるのは以下の部分

最終的に、マネーフォワードから銀行の口座残高や資産状況を取得してきて、毎月の資産を把握できるようにしようかと思っています。

今回はその前段部分です。

楽天カードから請求額をスクレイピング

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import requests
from bs4 import BeautifulSoup
import pandas as pd
from time import sleep
from datetime import datetime

def get_rakuten_bill():
    # password
    USERNAME = 'email@example.com'
    PASSWORD = 'xxxxxxxxxxxxxxxxx'
    error_flg = False

    # open chrome
    options = Options()
    options.add_argument('--headless')
    driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=options)
    target_url = 'https://www.rakuten-card.co.jp/e-navi/index.xhtml'
    driver.get(target_url)
    sleep(3)

    # login
    try:
        username_input = driver.find_element_by_xpath("//input[@name='u']")
        username_input.send_keys(USERNAME)
        sleep(1)
        password_input = driver.find_element_by_xpath("//input[@name='p']")
        password_input.send_keys(PASSWORD)

        login_button = driver.find_element_by_xpath("//input[@type='submit']")
        login_button.submit()
        sleep(1)

    except Exception:
        error_flg = True
        print('ユーザー名、パスワード入力時にエラーが発生しました')

    # 請求書ページを開く
    if error_flg is False:
        try:
            driver.get('https://www.rakuten-card.co.jp/e-navi/members/statement/index.xhtml?tabNo=1&l-id=enavi_top_info-card_statement')
            sleep(3)
        except Exception:
            print('エラーが発生しました')
            # 例外の場合、処理をしない pass
            pass
    # 最新の請求額を取得
    try:
        soup = BeautifulSoup(driver.page_source,'html.parser')
        billing_latest = int(soup.find('span',{'class':'stmt-u-fs-xxl'}).text.replace('\n','').replace(',',''))
        sleep(1)
    except Exception:
        error_flg = True
        print('エラーが発生しました')

    # chrome driver close
    driver.close()
    driver.quit()
    
    return billing_latest

ユーザー名とパスワードはご自身のものに書き換えてください。

楽天カードは、毎月12日に請求額が確定するので、12日以降にスクリプトを実行するようにする予定です。

これで、確定した請求額が取得できます。

LINE Notify で今月の請求額を通知

LINE Notifyはアクセストークンを取得するだけでいいので、とっても簡単に実装できる。

notify-bot.line.me

f:id:kei_01011:20210519082501p:plain

ドキュメントにある、POSTのエンドポイントURLを使用します。

ドキュメント↓ LINE Notify

import requests
def notify_message(message):
    LINE_NOTIFY_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    url = 'https://notify-api.line.me/api/notify'
    headers = {
        'Authorization': f'Bearer {LINE_NOTIFY_TOKEN}'
    }

    data = {
        'message': message
    }
    requests.post(
        url,
        headers=headers,
        data=data
    )

アクセストークンは直書きしてますが、セキュリティを考慮したい人は別ファイルに記述して読み込ませてください。

テスト

f:id:kei_01011:20210519083440p:plain

無事、通知できました。

今月の請求額多いな。。。

また続き書きます。

Pythonで中古住宅情報をスクレイピングしてみた

住宅情報サイトを見ていて、1つずつ情報を見るよりも、「データ形式でざっと全体像を把握したい」って思うことがあり、スクレイピングで解決してみた

今回は、 SUUMOから情報を取得してみました。

スクレイピングのコード

BeautifulSoupを使いました。

import requests
from bs4 import BeautifulSoup
import pandas as pd
from time import sleep

pages = '1'
# 対象URL
url = f"https://suumo.jp/jj/bukken/ichiran/JJ012FC001/?ar=060&bs=021&sa=01&ta=27&po=0&pj=1&pc=100&pn={pages}"

res = requests.get(url)
soup = BeautifulSoup(res.text,'html.parser')
# 検索結果件数からページ数を算出
post_count = soup.select('div.pagination_set-hit')[0].text
post_count = post_count.replace('件', '').replace(',', '').strip()
max_page = math.ceil(int(post_count) / 100)

# ページ数の数だけループ
list_all = []
for page in range(1, (max_page + 1)):
    pages = page
    res = requests.get(url)
    soup = BeautifulSoup(res.text,'html.parser')

    # get htmltag
    title = soup.find_all('h2')
    price = soup.select('span.dottable-value')
    addr = soup.find_all('dt', text='所在地')
    access = soup.find_all('dt', text='沿線・駅')
    land_area = soup.find_all('dt', text='土地面積')
    build_area = soup.find_all('dt', text='建物面積')
    year_built = soup.find_all('dt', text='築年月')
    # 取得したページhtmlからタグ取得
    # 1ページあたりの表示数は100 なので100でループ処理
    for i in range(100):
        csv_list = []

        csv_list = [
          title[i].a.string,
          "https://suumo.jp" + title[i].a['href'],
          price[i].string.replace('万円',''),
          addr[i].find_next_sibling().text,
          access[i].find_next_sibling().text,
          land_area[i].find_next_sibling().text.split('m')[0],
          build_area[i].find_next_sibling().text.split('m')[0],
          year_built[i].find_next_sibling().text,
        ]
        list_all.append(csv_list)
  sleep(10)


# リストをユニークに変換
def get_unique_list(seq):
    seen = []
    return [x for x in seq if x not in seen and not seen.append(x)]

# dataframeへ格納
header = ['タイトル','URL','価格','所在地','沿線・駅','土地面積','建物面積','築年月']
df = pd.DataFrame(get_unique_list(list_all),
                  columns=header)

f:id:kei_01011:20210518093001p:plain

こんな感じで、それぞれの住宅情報がデータ形式にできました。

あとは集計するなり分析するなり、いろいろできそう。

やってること

対象URLの部分には、SUUMOでスクレイピングしたい条件で検索し、検索結果で表示された一覧ページを入力します。

結果ページのページ数を数えて、ページ分だけループ処理して取得してきます。

複数の住宅情報サイトからスクレイピングして比較してみるのも面白そうですね。

【Python】スクレイピングでエンジニア言語別給料幅を集計してみた

f:id:kei_01011:20210516153928p:plain
求人情報

プログラミング言語って、結局どの言語を選択すれば給料が上がるんだろ? 素朴な疑問から、求人サイトをスクレイピングして集計してみました。

集計方法

GREENの求人サイトから集計

バックエンド、データ分析系、フロントエンドの言語で検索し、それぞれの求人情報のタグ、最低給料、最大給料を取得して集計

コード

import requests
from bs4 import BeautifulSoup
import pandas as pd
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep

def get_green_items(keyword):
    
    # driver and beautifulsoup
    options = Options()
    options.add_argument('--headless')
    driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=options)

    pages = '1'
    url = f"https://www.green-japan.com/search_key/01?keyword={keyword}&page={pages}"

    #html取得
    driver.get(url)
    res = requests.get(url)
    soup = BeautifulSoup(res.text,'html.parser')

    # 検索結果件数からページ数を算出
    post_count = soup.select('.pagers')
    max_page = int(post_count[0].find_all('a')[5].text)
    print(max_page)
    # ページ数の数だけループ|
    list_all = []

    for page in range(1, (max_page + 1)):
        try:
            options = Options()
            options.add_argument('--headless')
            driver = webdriver.Chrome('/Users/user/driver/chromedriver',options=options)

            pages = page
            url = f"https://www.green-japan.com/search_key/01?keyword={keyword}&page={pages}"

            driver.get(url)
            res = requests.get(url)
            soup = BeautifulSoup(res.text,'html.parser')

            print(url)
            # get htmltag
            offer = soup.find_all('div',{'class':'job-offer-icon'})
            html_salary = soup.find_all('ul',{'class','job-offer-meta-tags'})
            company = soup.find_all('h3',{'class':'card-info__detail-area__box__title'})
            html_sub_title = soup.find_all('div',{'class':'card-info__detail-area__box__sub-title'})
            html_tag_lists = soup.find_all('ul',{'class':'tag-gray-area'})
            html_href = soup.find_all('a',{'class':'js-search-result-box'})
#             'https://www.green-japan.com/' + 
            # 取得したページhtmlからタグ取得、CSV吐き出し
            print(f"{page}page:start...")
        except:
            print('error')

        # 1ページ分のデータをリストに格納
        for i in range(10):
            data_list = []
            tags = []

            try:
                # HTMLタグがない場合があるので条件分岐させる
                salary = html_salary[i].find('span').next_sibling.replace('\n','').replace(' ','').replace('万円','').split('〜')
                salary_check = html_salary[i].find_all('span',{'class':'icon-salary'})
                if len(salary_check) > 0:
                    min_salary = salary[0]
                    max_salary = salary[1]
                else:
                    min_salary = ''
                    max_salary = ''

                sub_title = html_sub_title[i].find_all('span')
                for title in sub_title:
                    if '設立年月日' in title.text:
                        build = title.text.replace('設立年月日 ','').replace('年','/').replace('月','')
                    elif '従業員数' in title.text:
                        staff = title.text.replace('従業員数 ','').replace('人','')
                    elif '平均年齢' in title.text:
                        age = title.text.replace('平均年齢','').replace('歳','')
                    else:
                        pass

                tag_lists = html_tag_lists[i].find_all('span')
                
                url = 'https://www.green-japan.com/' + html_href[i]['href']
                for tag in tag_lists:
                    tags.append(tag.text)
                tag_list = ','.join(tags)

                data_list = [
                    company[i].text,
                    offer[i].text,
                    min_salary,
                    max_salary,
                    build,
                    staff,
                    age,
                    tag_list,
                    url
                ]
                list_all.append(data_list)
                time.sleep(5)
            except:
                print('page error')
                pass

        print(f"{page}page:done...!")
        time.sleep(8)
        
    columns = ['企業名','オファータイトル','給料最小値','給料最大値','設立年月日','従業員数','平均年齢','タグ','URL']
    df = pd.DataFrame(list_all,columns=columns)
    return df

get_green_items('Python')で実行。

今回、seleniumを使ってみたかっただけなので、わざわざseleniumを使う必要はない。

BeautifulSoupで十分収集可能だと思う。

収集したデータはスプレッドシートなりに吐き出して、Tableauで可視化。

今回は、企業の個別情報も含んでいるのでTableauPublicにパブリッシュはしていません。

【Tableau】何が人を幸せにするのか?世界幸福度調査を可視化してみた

Kaggleのデータセットの中で、World Happiness Report という世界幸福度調査を見つけ、日本の現状を知るために可視化してみました。

TableauPublic↓

public.tableau.com

データ出典 World Happiness Report | Kaggle

2019年日本は58位

f:id:kei_01011:20210519073508p:plain

f:id:kei_01011:20210519073559p:plain

1位のフィンランドと比べるとこんな感じ

項目 日本 フィンランド
ソーシャルサポート 1.419 1.587
1人あたりGDP 1.327 1.340
人生の選択をする自由度 0.445 0.596
健康寿命 1.088 0.986
寛大さ 0.069 0.153
汚職の認識 0.140 0.393

特に、「ソーシャルサポート」、「人生の選択をする自由度」」が低かった。

「自分で選択できる自由度」と「人とのつながり」が重要

1位のフィンランドと日本を比べたときに、大きな差を感じるのは「人とのつながり」と「自由度」

自分自身で選んで進路を決定した人は、達成感や自尊心で主観的幸福度が高まると考えられています。

www.kobe-u.ac.jp

自己決定によって進路を決定した者は、自らの判断で努力することで目的を達成する可能性が高くなり、また、成果に対しても責任と誇りを持ちやすくなることから、達成感や自尊心により幸福感が高まることにつながっていると考えられます。

jibun-jiku.jp

また、対人関係の幸福度への影響についての研究が散見され、こちらも重要な指標みたいです。

http://www.ias.sci.waseda.ac.jp/GraduationThesis/2011_summary/1w080548_s.pdf

ソーシャルサポートに関しては、親世代との世代間ギャップが大きく、現代の考え方とのミスマッチでなかなかうまく行かないケースも出てきそうです。

北欧エリアの幸福度が高いのはなぜなのでしょうか? GDPについては大差がないのですが、国民のための政策が充実しているのか、、このあたりも深堀りしてみたい。