DPCデータの分析とかやるブログ

DPCデータの分析なんかをテキトーにやってます。

EF/Dファイルのテーブル定義について

今更ですが、EFファイル/DファイルをDBに読み込む際のテーブル定義について、少し見直しをしていました。

今まで、「主キーとすべき項目が見当たらない」という理由で、id列を連番で振っていましたが、冷静になってみると複合主キー使えばいいんですね…

Postgresのドキュメントを見てみましょう。

5.3. 制約

主キーも複数の列に渡ることがあり、その構文は一意性制約に似ています。
CREATE TABLE example ( a integer, b integer, c integer, PRIMARY KEY (a, c) );

と、はっきり例が載ってます。
ということで、自動付番のid列を作るのを止めたのが以下の例です。

  • EFファイル
CREATE TABLE ETABLE
(施設コード VARCHAR(255),
データ識別番号 INTEGER,
退院年月日 DATE,
入院年月日 DATE,
データ区分 DOUBLE PRECISION, 
順序番号 DOUBLE PRECISION,
行為明細番号 DOUBLE PRECISION,
病院点数マスタコード DOUBLE PRECISION,
レセプト電算コード DOUBLE PRECISION,
解釈番号 VARCHAR(255),
診療明細名称 VARCHAR(255),
使用量 DOUBLE PRECISION,
基準単位 DOUBLE PRECISION,
明細点数・金額 DOUBLE PRECISION,
円点区分 DOUBLE PRECISION,
出来高実績点数 DOUBLE PRECISION,
行為明細区分情報 CHAR(12),
行為点数 DOUBLE PRECISION,
行為薬剤料 DOUBLE PRECISION,
行為材料料 DOUBLE PRECISION,
行為回数 DOUBLE PRECISION,
保険者番号 DOUBLE PRECISION,
レセプト種別コード DOUBLE PRECISION,
実施年月日 DATE,
レセプト科区分 DOUBLE PRECISION,
診療科区分 DOUBLE PRECISION,
医師コード VARCHAR(255),
病棟コード VARCHAR(255),
病棟区分 DOUBLE PRECISION,
入外区分 VARCHAR(8),
施設タイプ VARCHAR(8),
PRIMARY KEY(データ識別番号,データ区分,順序番号,行為明細番号,実施年月日));
  • Dファイル
CREATE TABLE DTABLE
(施設番号 VARCHAR(255),
データ識別番号 INTEGER,
退院年月日 DATE,
入院年月日 DATE,
データ区分 DOUBLE PRECISION, 
順序番号 DOUBLE PRECISION,
点数マスタコード DOUBLE PRECISION,
レセ電算処理コード DOUBLE PRECISION,
解釈番号 VARCHAR(255),
診療行為名称 VARCHAR(255),
行為点数 DOUBLE PRECISION,
行為薬剤料 DOUBLE PRECISION,
行為材料料 DOUBLE PRECISION,
円点区分 DOUBLE PRECISION,
行為回数 DOUBLE PRECISION,
保険者番号 DOUBLE PRECISION,
レセプト種別コード DOUBLE PRECISION,
実施年月日 DATE,
レセプト科区分 DOUBLE PRECISION,
診療科区分 DOUBLE PRECISION,
医師コード VARCHAR(255),
病棟コード VARCHAR(255),
病棟区分 DOUBLE PRECISION,
入外区分 DOUBLE PRECISION,
施設タイプ DOUBLE PRECISION,
算定開始日 VARCHAR(8),
算定終了日 VARCHAR(8),
算定起算日 VARCHAR(8),
分類番号 VARCHAR(255),
医療機関係数 VARCHAR(255),
PRIMARY KEY(データ識別番号,データ区分,順序番号,実施年月日));

ご覧のとおり、

  • EFファイルはデータ識別番号,データ区分,順序番号,行為明細番号,実施年月日

  • Dファイルはータ識別番号,データ区分,順序番号,実施年月日

の組み合わせを主キーとしています。仕様書を読む限り、この組み合わせでデータが一意に定まる…はず…
もし実際に使って問題が起きたら、ご連絡頂けると助かります。
2,3ヶ月分これで登録出来たので、多分大丈夫だとは思うんですが…

「病床の機能別分類」を境界点に従って区分する

いきなり何のこと?

病床の機能別分類は、国が(かなりいい加減に使っていた)高度急性期・急性期・回復期・慢性期の定義付けです。
ちゃんとした数字で指標を作らないと、統計も取れませんからね。
紹介記事はこちらを参考にしてください。

該当記事によれば、日別の出来高点数を見て、

  • C1 3000点以上 高度急性期
  • C2 600点以上 急性期
  • C3 225点以上 回復期
  • それ以下 慢性期

と分けるようです。 「リハビリテーション料の一部を除く」とあるのが気になりますが、ひとまず大雑把にリハビリを除外してしまいましょう。
さくっとSQLを書くとこんな感じです。

一日あたり点数を出すSQL

SELECT データ識別番号,入院年月日,実施年月日,SUM(出来高実績点数*行為回数) AS 出来高総点数
FROM efile
WHERE データ区分 < 90
AND データ区分 <> 70
group by データ識別番号,入院年月日,実施年月日

以上終了です、ありがとうございました…
というのはあんまりですね。これからエクセルで集計しろ、というのでしょうか。
もう少しSQL側で何とか出来ないか考えてみましょう。

CASE式で区分を組み込む

SELECT データ識別番号,入院年月日,実施年月日,SUM(出来高実績点数*行為回数) AS 出来高総点数,
CASE WHEN SUM(出来高実績点数*行為回数) > 2999 THEN '高度急性期'
WHEN SUM(出来高実績点数*行為回数) > 599 AND SUM(出来高実績点数*行為回数) < 3000 THEN '急性期'
WHEN SUM(出来高実績点数*行為回数) > 224 AND SUM(出来高実績点数*行為回数) < 600 THEN '回復期'
ELSE '慢性期' END AS class
FROM efile
WHERE データ区分 < 90
AND データ区分 <> 70
group by データ識別番号,入院年月日,実施年月日

いきなり面倒になりました。
とはいえ、やっていることはそんなに複雑ではありません。
CASE式は、この場合ですとclassというカラムを定義します。
(END AS classのところで名前を決めています)
あとは、わりと見ての通りです。出来高点数の合計と、最初に上げたC1~C3の区分を比較して、分類の振り分けを行っているだけです。
さて、これで区分がつきましたので、あとはピボットテーブルで…

クエリ全体をSELECTする

それも面倒くさいわ!そもそもエクセル触りたくない!
という方のためのクエリです。 クエリ全体をAS fooという感じで名付けてやり、それに大してSELECTしてみましょう。
こんな感じになります。

SELECT count(sum.データ識別番号),sum.class
FROM (
SELECT データ識別番号,入院年月日,実施年月日,SUM(出来高実績点数*行為回数) AS 出来高総点数,
CASE WHEN SUM(出来高実績点数*行為回数) > 2999 THEN '高度急性期'
WHEN SUM(出来高実績点数*行為回数) > 599 AND SUM(出来高実績点数*行為回数) < 3000 THEN '急性期'
WHEN SUM(出来高実績点数*行為回数) > 224 AND SUM(出来高実績点数*行為回数) < 600 THEN '回復期'
ELSE '慢性期' END AS class
FROM efile
WHERE データ区分 < 90
AND データ区分 <> 70
group by データ識別番号,入院年月日,実施年月日) AS sum
group by sum.class

ここまで来ると、正直おぞましい気がしますが、追加したのはSELECT count(sum.データ識別番号),sum.classFROM ( 〜 group by sum.classの部分だけです。
結果は、区分別の患者延件数になります。
お好みで、診療科や病棟を追加してやれば、診療科別・病棟別のデータもすぐに作れて便利です。
このようにCASE式は便利ですので、ちょっとした分析にどうぞ。
(ある程度複雑になったら、さっさとPandasに移行するのがオススメですが…)

DPCチェッカーの32bit版を公開…してみるが…

例によってこちらからどうぞ。
が、残念ながら既知の問題があります。
ある大きさ以上のファイルを読み込もうとすると、メモリ不足で落ちるようです。
(実際、検証したらエラーを吐きました)
32bit版WIndowsで、メモリ4GB(うちOSが3GB使用)で落ちたので、う、うーん…
EF/Dファイルのサイズによっては動きますが、あんまりアテになりません。

今のところ考えている対策

現在、

  1. EF/Dファイルをファイルアップロードの方法でローカルにコピーする
  2. tmpフォルダに移動する
  3. SQLiteコマンドを走らせてDBに登録する

というステップを踏んでいますが、この辺りの設計を見直そうかなーと思ってます。
今回の問題は2のステップでコケてるので、2のステップを無くすのも手かな…
解決できたらまた記事にします。

scikit-learn 勉強ノート(1)

まえがきと目標

scikit-learnは最も有名なPython機械学習ライブラリです。
私も去年、「そろそろ機械学習をやるぞ!」と思い立って公式サイトを覗き、早々に諦めた覚えがあります。
だって恐ろしい数式が乱舞してますし、そもそも何を説明しているのかも分からないんですからね!

しかし、最近になって素晴らしい解説書が続いています。

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

特に「Pythonではじめる機械学習」は優れた入門書です。まだ読んでいる途中ですが、最初の数章を読むだけでライブラリを使い始めることが出来ます。
多くのPythonライブラリと違い、scikit-learnはそもそも使い始めるのにハードルが高い印象だったので、とても助かりました。

ちなみに、この勉強ノートでは

  • ライブラリを(お試し程度に)使えるようになること

を目標とし、数学的原理には踏み込みません。

基本準備

前提条件としてnumpyが必須です。import numpy as npでインポートしておきます。

データを用意し、分割する

なにはともあれデータを用意しましょう。
都合よく、頻用されるirisデータセットを読み込むための関数があります。

from sklearn.datasets import load_iris
iris = load_iris()

さて、load_iris()の返り値となるデータセットには、'target', ‘data’, ‘feature_names’, ‘target_names’, ‘DESCR’ の要素が含まれています。

  • target 各々のデータが何の品種だったかが数字で表されている。回答ラベル
  • data 花の萼片と花弁のサイズが記録されている。これを元にtargetを推測するのが練習問題
  • feature_names dataの各カラムに何の情報が入っているかの説明
  • target_names targetは数字で記録されているので、それに対応する品種名
  • DESCR このデータセットの解説

さて実際に学習に使うデータはdataで、回答ラベルはtargetです。まずはこの2つを、学習用とテスト用に分割してやらないといけません。
そんなことを手作業ではやってられないので、ちゃんと方法が用意されています。

from sklearn.model_selection import train_test_split
(旧バージョンのscikit-learnではfrom sklearn.cross_validation import train_test_split)

train_test_splitはデータと回答ラベルを引数に取り、それを学習用とテスト用に自動分割します。
慣例でデータはX_train, X_testに、回答ラベルはy_train,y_testに分割することとなっているようです。

X_train,X_test,y_train,y_test = train_test_split(
iris['data'],iris['target'],random_state=0)

これで4つの変数に学習用とテスト用データが保存されました。

モデルのインスタンスを作成する

from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier()

scikit-learnには様々なモデルが含まれています。モデルは特定のやり方でデータを読み込み、予測または分類を行います。
Classifierとなっていれば分類用のモデルですし、Regressorとなっていれば回帰用のモデルです。
今回例にしたランダムフォレストというモデルは、ClassifierとRegressorを両方持っています。
(つまりRandomForestRegressorもあります)

最初にすることはモデルのインスタンスを作成することで、まあ、forest = RandomForestClassifier()とあるように適当な変数に格納しておきます。
この時点ではデータを読み込みません。

モデルにデータを読み込ませて、予測や正答率を確かめる

モデルにデータを読み込ませるのは簡単です。.fitメソッドを使います。

forest.fit(X_train,y_train) 

これでforest上書きされて、学習データと回答ラベルから予測モデルを作ります。
X_trainはnumpyの多次元配列、y_trainはnumpyの一次元配列である必要があります。

最後に、予測と正答率を見てみましょう。

テストデータに対する予測をするには、

forest.predict(X_test)

とするだけで、予測したラベルを出してくれます。これを元に手作業で正解ラベルと比べてもいいのですが、もちろんもっと良い方法が用意されています。

forest.score(X_test,y_test)

で、モデルをテストデータに適用した場合の正答率を出してくれます。
今回は、まずIrisデータセットを使った分類問題をやってみました。いざやってみると、scikit-learnは本当に使いやすいライブラリですね。

エクセルは事務職の生産性を本当に上げたのか?

事務職の生産性を上げるために

えー、歴史を鑑みると、近代というのは機械化の時代です。
人間は逆立ちしてもトラクターには勝てませんし、馬ではトラックと競争出来ません。
事務職にとってコンピュータは、運送業におけるトラックと同じくらい大きな意味があります。
トラックは運送業の生産性を飛躍させました。 コンピュータも、同じように事務職の生産性を…うーん、あれ、あんまり上がってないような…

神エクセル問題

エクセルを使うと、まずいことに紙の書類をそのまんま再現出来てしまいます。
これはエクセルというツールの功罪です。紙の書類をディスプレイで再現する時、我々はコンピュータを「消しゴムの要らない紙」として使っているのです。

消しゴムの要らない紙は、確かに便利ですが、生産性を上げることはありません。

そして本来なら、それはワードの仕事でした。ワードが「紙の代替品」として余りにも非力なため、何故かエクセルで方眼紙を作る羽目になっていますが、本来エクセルは表計算ツールです!

エクセルの功罪

マイクロソフトは「機能を削る」ことをしない会社です。
そのため、エクセルはユーザーの要求のまま肥大化を重ね、ほとんど何でも出来る万能ツールになってしまいました。
一見いいことに見えますよね?
ですが、平凡なドライバーにはマニュアル車よりオートマ車の方が安全なように、機能を削ったほうが「正しく」使える道具というのは存外多いものです。

例えば、セルの結合を使うと、見た目は綺麗な表が出来上がりますが、データの再利用は困難になります。ちょっと考えてみて下さい。
エクセルは表計算ツールです。データの再利用を困難にしたら、これは一体何のためのツールなんでしょうか?
エクセルの落とし穴は、表計算ツールなのに、体裁を整える機能がやたらと多い点に尽きるでしょう。

データと見た目は分割すべき

理想的なツールは恐らく、データの保存部分と表の整形部分が完全に分割されたものです。
これなら上記の問題は無くなります。
ウェブサイトは正にそういう構造をしていて、データはDBに、デザインはHTML/CSSに、と完全に分割されていますね。
サイトデザインに致命的な変更を行って、大失敗に終わったとしても、DBのデータが消し飛んだりはしません。
ですが、エクセルではそんなことが日常茶飯事です。これはとんでもない欠陥です。

これからは、事務職も生産性を上げることが求められるでしょう。
その手段は、ほぼ間違いなくスクリプトによる自動化です。
しかし、紙の書類を再現したエクセルは、自動化を大いに妨げてしまいます。
「元データを保存するシート」「見た目をいじるシート」のように完全に分離して作ると、大分マシにはなるんですが…

ヒートマップで相関係数を可視化する

以前、相関係数について、ざっくりした記事を書きました。

stagira.hatenablog.com

おさらいですが、相関係数は-1から+1の間を取る指標で、±0.7以上なら強い相関があると認められます。
マイナスなら負の相関、プラスなら正の相関ですね。
さて。以前の記事では「df.corr()メソッド一発で計算終了!楽ちん!」で終わってますが、実際のデータというのは15〜20項目あったりして、相関係数の表を見ているだけで目が痛くなります。
(重複はありますが、15✕15マスの表が出来上がります…)

ヒートマップを使えば、色の濃いところが相関が強い!くらいに一望出来るので、目に優しいですね。
実際どんなグラフになるのか、早速描画してみましょう。
今回は適当な例題データを思いつかなかったので、機械学習でよく使うボストン住宅データを流用します。

HeatMap_example

いかがでしょうか。
ボストン住宅データは、わりと実務で目にするデータに近い形です。
手元のデータで実際にヒートマップを描いてみると、案外面白い発見があるかも知れません。

EFファイルを自力で統合する(2017年版)

どうも最近、EFファイル統合でこのブログに来られる方が多いようです。

stagira.hatenablog.com

こんな記事も書きましたが、中身に関しては「適当にコード読んどいてね!」と投げっぱなし感溢れてましたので、ちゃんとした解説を書くことにしました。 ちょっと長いですが、EFファイル統合の流れを追っていきます。

方針

  • 実装はPythonのライブラリ、Pandasに全面的に頼る。
  • ライブラリの便利メソッドにおんぶにだっこ、極力難しいことはしない。

ということでPython環境のない方はAnaconndaを導入して下さい。
EFファイル統合スクリプトこちらにあります。並行してご覧ください。

おさらい

EファイルとFファイルに共通する項目で、重要なのはデータ識別番号 入院年月日 データ区分 順序番号の4項目です。
実際には、この4項目をキーにして結合を行い、同一の順序番号の中で更に行為明細番号を割り振ります。 図にするとこんな感じ。

データ識別番号  入院年月日  データ区分 順序番号 行為明細番号
000001 (Eファイル由来) 2017-04-01 60 1    0
000001 (Fファイル由来) 2017-04-01 60 1 1
000001 (Fファイル由来) 2017-04-01 60 1 2

重要なことは行為明細番号はFファイルにしかなく、Eファイル由来の行の行為明細番号は0になるということです。 これは、この後再び言及します。統合のミソです。

下準備

まず、カラム名のリストを用意します。

e_names = ['施設コード','データ識別番号','退院年月日','入院年月日','データ区分','順序番号',
'病院点数マスタコード','レセプト電算コード','解釈番号','診療明細名称','行為点数','行為薬剤料',
'行為材料料','円点区分','行為回数','保険者番号','レセプト種別コード','実施年月日',
'レセプト科区分  ','診療科区分','医師コード','病棟コード','病棟区分','入外区分','施設タイプ']
#正しくはEファイルには「診療明細名称」ではなく「診療行為名称」だが、統合のため変えている

f_names = ['施設コード','データ識別番号','退院年月日','入院年月日','データ区分',
'順序番号','行為明細番号','病院点数マスタコード','レセプト電算コード','解釈番号','診療明細名称',
'使用量','基準単位','行為明細点数','行為明細薬剤料','行為明細材料料','円点区分',
'出来高実績点数','行為明細区分情報']

続いて、EファイルとFファイルを読み込みます。医師コードを文字列(str)で読み込んでいるのは、病院によって数字のみだったり英数字で管理していたりとバラバラだからです。
shift_jisx0213でエンコード指定していますが、単にshift-jisでも行けるかも知れません。

edata = pd.read_csv('DRGE.TXT',delimiter='\t',parse_dates=['入院年月日','実施年月日'],dtype={'医師コード':str},
    encoding = 'shift_jisx0213',names=e_names)

fdata = pd.read_csv('DRGF.TXT',delimiter='\t',parse_dates=['入院年月日'],dtype={'行為明細区分情報':str},
    encoding = 'shift_jisx0213',names=f_names)

EFファイルの「明細点数・金額」は、Fファイルの'行為明細点数','行為明細薬剤料','行為明細材料料を一緒くたにしたものです。
ここでは、まず3つを単純に足し合わせた新たなカラムを作り、元の3つをdropしています。

fdata['明細点数・金額'] = fdata['行為明細点数'] + fdata['行為明細薬剤料'] + fdata['行為明細材料料']
fdata.drop(['行為明細点数','行為明細薬剤料','行為明細材料料'],axis=1,inplace = True)

下準備が終わりました。ここからは結合処理に入ります。

結合処理と行為明細番号

efdata = pd.concat([edata,fdata])

まずはpd.concatで縦方向に連結します。この時点では、本当に

Eファイル ↓ Fファイル

の順で縦に繋げただけです。
実際には [‘データ識別番号’,‘入院年月日’,‘データ区分’,‘順序番号’,‘行為明細番号’]の順番に並べる必要があります。
さっそく並べましょう!…と言いたいのですが、Eファイルには行為明細番号がありません。
そう、初っ端に言及した通り、Eファイル由来の行は行為明細番号0になります。並べる前に埋めておきましょう。

efdata['行為明細番号'].fillna(0,inplace=True)

fillnaメソッドは単純な穴埋めから高度な補完まで使えます。ここでは単純に0での穴埋めをしていますが、この先もうちょっと高度なことをやってもらいます。
続いては単純な並び替えです。

efdata.sort_values(['データ識別番号','入院年月日','データ区分','順序番号','行為明細番号'],inplace = True)
efdata.reset_index(inplace=True)

項目ごとの穴埋め

EFファイルにしかない項目を、穴埋めリストにしてみます。

fill_list='行為回数','実施年月日','診療科区分','医師コード','病棟コード','病棟区分'

さて、各項目がEファイル由来ということは、必ずレコードの先頭にある(=行為明細番号が0)ということです。 その後の空白行については、直近の一番上のデータから穴埋めしていけばよいのです。
fillna(method='pad')正にそのような挙動をしてくれます。

for val in fill_list:
    efdata[val].fillna(method='pad',inplace=True)

さて、これで殆ど出来上がりですが、私の環境では

データ区分、データ識別番号、レセプト電算コード…

のようにカタカナ順になり、続いて漢字…という並び方になってしまいました。
これでは困るので、続いてカラムの順番を変えます。

sort_list = '施設コード','データ識別番号','退院年月日','入院年月日','データ区分',\
'順序番号','行為明細番号','病院点数マスタコード','レセプト電算コード','解釈番号','診療明細名称',\
'使用量','基準単位','明細点数・金額','円点区分','出来高実績点数','行為明細区分情報','行為点数','行為薬剤料',\
'行為材料料','行為回数','保険者番号','レセプト種別コード','実施年月日','レセプト科区分',\
'診療科区分','医師コード','病棟コード','病棟区分','入外区分','施設タイプ'


efdata_sorted = efdata.reindex(columns=sort_list)

df.reindexはcolumnsに指定してやることで、カラムの指定が可能です。

おまけ

DB取り込み用に、0をinfinityに置換してやります。
また、入院年月日に0が入っていた場合は、こちらも'epoch'に置換してやります(多くの場合、この手順はスキップして 構いません。通常は起きない筈なので…)

efdata_sorted['退院年月日'] = efdata_sorted['退院年月日'].replace(0,'infinity')
efdata_sorted['入院年月日'] = efdata_sorted['入院年月日'].replace('0','epoch')

最後に空白のままになっている項目を、便宜上0で埋めておきます。

Fillzero = ['行為点数','行為薬剤料','行為材料料','使用量','基準単位','明細点数・金額','出来高実績点数']
for fillzero in Fillzero:
    efdata_sorted[fillzero].fillna(0,inplace=True)

あとはCSVに吐き出せばおしまいです。
GitHubのコードでは医師コードをいじってますが、環境によって先頭に空白が交じることがあるからで、特に意味はありません。
(悪さをするようなコードでもないです)
実際には、思わぬエラーが出てくることもあり得ますので、適宜修正するのがオススメです。

注記

この統合はDB登録を目的にしたもので、データ提出を視野に入れていません!
データ提出に使うと文字コード絡みで確実に弾かれますので、注意して下さい。