自分のキャリアをあれこれ考えながら、Pythonで様々なデータを分析していくブログです

(その1) Heart Disease(Cleveland)のデータ俯瞰

Data Analytics
Data Analytics

このブログも幅広くデータ分析に関する情報をまとめて来ました。

Python環境構築からデータ操作の方法、機械学習でのモデル作成とディープラーニングによる物体検知まで手広くやってみました。

そろそろ原点である「様々なデータを分析する」に戻ろうかと思います 笑 (また寄り道をする可能性はあります)

本当はSIGNATEの「【第36回_Beginner限定コンペ】従業員の離職予測 」がとても面白そうで分析して見たかったのですが、ビギナー限定のようなので昔に登録していた私は参加資格がないので断念。もし全メンバー公開に変更になったら取り組んでみたいと思います。

何か自分が興味ありそうなデータセットということで、今回はUC Irvine Machine Learning RepositoryからHeart Diseaseというデータセットを選択しました。

スポンサーリンク

Heart Diseaseデータセットについて

Heart Diseaseデータセットは血管造影検査を受けている患者の心臓病の有無のデータセットになるようです。詳細はInternational application of a new probability algorithm for the diagnosis of coronary artery disease に記載されています。

具体的には下記4つのデータベースが含まれているデータセットになりますが、Clevelandのデータセットのみが現在も機械学習の研究者に使われているようです。

  • Cleveland → Cleveland Clinic in Cleveland, Ohio
  • Hungary → Hungarian Institute of Cardiology in Budapest, Hungary
  • Switzerland → University Hospitals in Zurich and Basel, Switzerland
  • VA Long Beach → Veterans Administration Medical Center in Long Beach, California

また、ペーパーを確認する限りここでいう心臓病とは具体的には「虚血性心疾患」のことのようです。

It was concluded that coronary disease probabilities derived from discriminant functions are reliable and clinically useful when applied to patients with chest pain syndromes and intermediate disease prevalence.
引用: https://pubmed.ncbi.nlm.nih.gov/2756873/

少し検索して見ましたが、Clevelandのデータセットは2022年に公開されているEnhanced Heart Disease Prediction Based on Machine Learning and χ2 Statistical Optimal Feature Selection ModelというタイトルのArticleでも使われていました。

本記事ではClevelandのデータセットを使って分析していこうと思います。

スポンサーリンク

データマイニングプロセスについて

こんな手順で分析を進めていますという記事になります。

データマイニングのフレームワークであるCRISP-DMとは
ヒノマルク データマイニングを教わったときにCRISP-DMが基本概念にありました。ClementineというIBMのSPSS Modelerの前身のデータイニングツールで分析キャリアを始めました。ClementineでCRISP-DMが導...

本記事はCRISP-DMの下記の部分を掘り下げていこうと思います。

  1. ビジネスの理解
  2. データの理解
スポンサーリンク

ビジネスの理解

心臓病についての分析をするので、心臓病に関して知識があるのが望ましいです。

医療系の情報になるので医療団体やクリニックなど信頼できそうなソースから必要最低限の知識を引用していこうと思います。

心臓の構造について

図を丸々引用させていただきました。名称と位置関係くらいは知っておくべきかなと思い調べました。

引用: https://yotsuba-heart.jp/department/circulatory_diseases/heart_disease.html

心臓病とは

心臓病とは、心臓の構造や機能(働き)の異常により生じる病気の総称で、その中に、心不全、冠動脈疾患(虚血性心疾患ともいう)、心臓弁膜症、心筋症、不整脈、先天性心疾患などがあります。 心不全は、心臓病の中の1つです。
引用: https://www.jacr.jp/faq/q160/

心臓病にも色々種類があることが分かりますね。

心臓病の危険因子

虚血性心疾患には、狭心症や心筋梗塞があります。・・・虚血性心疾患の3大危険因子は、喫煙・LDLコレステロールの高値・高血圧です。またメタボリックシンドロームも危険因子の一つです。生活習慣では、喫煙のほか、動物性の油に多く含まれる飽和脂肪酸のとりすぎ、お酒の飲み過ぎ、食塩のとりすぎ、運動不足、ストレスが虚血性心疾患のリスクを高くします。一方、魚や野菜、大豆製品には、虚血性心疾患を予防する働きがあります。
引用: https://www.e-healthnet.mhlw.go.jp/information/metabolic/m-05-005.html

酒・タバコはNG、魚や野菜中心の生活をし適度な運動をすると長生きをするとはまさにこのことですね 笑

食塩は摂取しすぎている自覚ありますね、、気をつけよう。

心臓病の自覚症状

坂道で息切れしたり、動悸がする
急いで歩いた時、しばらく胸が痛む
夜明け方トイレに起きたときや洗面の時に胸が痛む
胸の痛みと共に不安感、動悸、息切れ、冷や汗、めまい、脱力感を感じる
引用: https://www.okabefujiko-clinic.com/heartdisease/

日常的にあり得そうな症状なので不安になりますね。

虚血性心疾患(coronary artery disease)とは

横浜市立大学附属病院のウェブサイトより引用しました。症状の重さによって狭心症または心筋梗塞という診断になるようですね。

虚血性心疾患は、いわゆる心臓発作と言われる病気です。心臓を栄養している血管である冠動脈に狭窄や閉塞をきたす病気で、大きく分けると、狭心症と心筋梗塞があります。狭心症は、冠動脈が狭くなり、先端の血管の血流が少なくなり、一時的に「息苦しい」・「胸が締め付けられる」といった症状が出ます。心筋梗塞は、さらに症状が悪化し冠動脈が詰まり、心臓の筋肉細胞が死んでしまい機能が低下してしまう病気です。
特に、急性心筋梗塞は、最初の数時間以内の適切な初期治療が生死をわける疾患です。
引用: https://www.yokohama-cu.ac.jp/fukuhp/section/center_disease/kyoketuseisinnsikkann.html

血管造影検査とは

血管造影検査を受けている患者さんのデータなので、血管造影とは何かも調べました。

血管造影検査とは、細い管(カテーテル)を使って造影剤というお薬を血管に流し込み、血管の疾患を調べるための検査です。... 血管が狭くなったり詰まったりしていないか、腫瘍に栄養を送っている血管はどれかなど、血管が関係している疾患を詳しく調べることができます。また、狭くなった血管を広げたり、腫瘍に栄養を送っている血管を詰めたりなど、治療を行うことも可能です。検査・治療時間は30分から数時間となります。
引用: https://sk-kumamoto.jp/departments/examination/5115/

心電図とは

データセットの変数に心電図の結果の変数があったの調べました。T波やSTなど専門用語が出て来ます。

引用:https://www.dock-tokyo.jp/results/heart/electrocardiogram.html

心臓が全身に血液を循環させるために拡張と収縮を繰り返すとき、微弱な活動電流が発生します。その変化を波形として記録することで、例えば心筋梗塞や不整脈などの疾患を診断することができます。
引用: https://y-heart.adic.or.jp/kensa/cardiogram.html

スポンサーリンク

データの理解

次にデータの理解です。

Heart DiseaseからDownloadボタンを押下してデータをダウンロードします。

heart+disease.zipを解答すると下記の様にたくさんファイルが存在します。

今回利用するのはprocessed.cleveland.dataという名前のファイルになります。

clevelandのデータセットの変数を確認

No Attribute Name Description 和訳 説明
1 age 年齢
2 sex 性別 1 = male; 0 = female
3 cp chest pain type 胸痛のタイプ 1:典型的な狭心症、2:非典型的な狭心症、3:非狭心症痛、4:無症状
4 trestbps resting blood pressure (on admission to the hospital) 安静時血圧(入院時)
5 chol serum cholestoral 血清コレステロール
6 fbs fasting blood sugar > 120 mg/dl 空腹時血糖 > 120 mg/dl 1:true、0:false
7 restecg resting electrocardiographic results 安静時心電図の結果 0: 正常、1:ST-T波異常を示す2:左室肥大の可能性または明確な証拠がある
8 thalach maximum heart rate achieved 最大心拍数
9 exang exercise induced angina 運動時に狭心症を誘発 1:true、0:false
10 oldpeak ST depression induced by exercise relative to rest [心電図]運動によって誘発されたST低下(安静時と比較)
11 slope the slope of the peak exercise ST segment [心電図]ピーク運動時のST部分の傾斜 1:上向き、2:平坦、3:下降
12 ca number of major vessels (0-3) colored by fluoroscopy 心臓の主要な血管がフルオロスコピーによって色付けされた数
13 thal thalassemia サラセミア 3:正常、6:固定欠損、7:可逆性欠損
14 num diagnosis of heart disease 心臓病の診断 0:血管狭窄が直径の50%未満、1以上:血管狭窄が直径の50%以上
  • 「サラセミア」が何のことだが分かりませんでしたが、血液に関連する遺伝子の異常によって引き起こされる血液障害のようです。サラセミアが原因で心臓疾患などを起こすこともあるようです。

サラセミアは溶血性貧血のみならず,脾腫・心臓疾患など多岐にわたって臓器障害を起こす
引用: https://www.jstage.jst.go.jp/article/shinzo/45/7/45_806/_pdf/-char/en

  • フルオロスコピー(透視検査)は体内組織の動態画像を生成することが出来る技術のようです。胃の検査だとバリウム検査、心臓病の検査でいうと血管造影検査を指していると思われます。

フルオロスコピーでは、1秒以下程度の周期で撮像と画像再構成とを繰り返すことにより、あたかもX線の透視撮影のように体内組織の動態画像を生成、表示する。
引用: https://patents.google.com/patent/JP2004008398A/ja

透視検査とは、Ⅹ線を連続又はパルス出力することによって体内や骨などを観察、処置するための検査です。 バリウム製剤を使用した胃の検査がよく知られていますが、それ以外にも造影剤を使用したさまざまな処置や検査、関節や靭帯の損傷具合をみるための撮影など多岐にわたっています。
引用: https://www.twmu.ac.jp/hospital/rad/ext/imaging/fluoroscopy/

Angiography, or angiogram, uses fluoroscopy to identify and diagnose narrowing or blockages in the arteries in your body.
引用: https://my.clevelandclinic.org/health/diagnostics/21992-fluoroscopy

clevelandのデータセットの中身を確認してみる

心臓病に関する知識と出現する医療用語の理解が進んできたので、データを見ていこうと思います。

とりあえずpandasに読み込んでみる
import pandas as pd

column_names = [
    'age', 'sex', 'cp', 'trestbps', 'chol', 'fbs', 'restecg',
    'thalach', 'exang', 'oldpeak', 'slope', 'ca', 'thal', 'num'
]

df = pd.read_csv("/Users/hinomaruc/Desktop/blog/dataset/heart+disease/processed.cleveland.data", header=None, names=column_names)

df
Out[0]
    age     sex     cp  trestbps    chol    fbs     restecg     thalach     exang   oldpeak     slope   ca  thal    num
0   63.0    1.0     1.0     145.0   233.0   1.0     2.0     150.0   0.0     2.3     3.0     0.0     6.0     0
1   67.0    1.0     4.0     160.0   286.0   0.0     2.0     108.0   1.0     1.5     2.0     3.0     3.0     2
2   67.0    1.0     4.0     120.0   229.0   0.0     2.0     129.0   1.0     2.6     2.0     2.0     7.0     1
3   37.0    1.0     3.0     130.0   250.0   0.0     0.0     187.0   0.0     3.5     3.0     0.0     3.0     0
4   41.0    0.0     2.0     130.0   204.0   0.0     2.0     172.0   0.0     1.4     1.0     0.0     3.0     0
...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...
298     45.0    1.0     1.0     110.0   264.0   0.0     0.0     132.0   0.0     1.2     2.0     0.0     7.0     1
299     68.0    1.0     4.0     144.0   193.0   1.0     0.0     141.0   0.0     3.4     2.0     2.0     7.0     2
300     57.0    1.0     4.0     130.0   131.0   0.0     0.0     115.0   1.0     1.2     2.0     1.0     7.0     3
301     57.0    0.0     2.0     130.0   236.0   0.0     2.0     174.0   0.0     0.0     2.0     1.0     3.0     1
302     38.0    1.0     3.0     138.0   175.0   0.0     0.0     173.0   0.0     0.0     1.0     ?   3.0     0

303 rows × 14 columns

最後の行のcaの値が「?」になっていますね。元データを見ても「?」になっていました。これは後ほどNaNに変えるなど処理をしたいと思います。

infoメソッドでデータフレームの概要を確認
df.info()
Out[0]
RangeIndex: 303 entries, 0 to 302
Data columns (total 14 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   age       303 non-null    float64
 1   sex       303 non-null    float64
 2   cp        303 non-null    float64
 3   trestbps  303 non-null    float64
 4   chol      303 non-null    float64
 5   fbs       303 non-null    float64
 6   restecg   303 non-null    float64
 7   thalach   303 non-null    float64
 8   exang     303 non-null    float64
 9   oldpeak   303 non-null    float64
 10  slope     303 non-null    float64
 11  ca        303 non-null    object 
 12  thal      303 non-null    object 
 13  num       303 non-null    int64  
dtypes: float64(11), int64(1), object(2)
memory usage: 33.3+ KB

caとthalがobjectになっていますね。「?」みたいにきっと数値以外が入っている可能性が高いので確認して見ます。

数値以外の情報があるか確認
print(df[df['ca'].apply(pd.to_numeric, errors='coerce').isnull()])
print("---")
print(df[df['thal'].apply(pd.to_numeric, errors='coerce').isnull()])
Out[0]
      age  sex   cp  trestbps   chol  fbs  restecg  thalach  exang  oldpeak  \
166  52.0  1.0  3.0     138.0  223.0  0.0      0.0    169.0    0.0      0.0   
192  43.0  1.0  4.0     132.0  247.0  1.0      2.0    143.0    1.0      0.1   
287  58.0  1.0  2.0     125.0  220.0  0.0      0.0    144.0    0.0      0.4   
302  38.0  1.0  3.0     138.0  175.0  0.0      0.0    173.0    0.0      0.0   

     slope ca thal  num  
166    1.0  ?  3.0    0  
192    2.0  ?  7.0    1  
287    2.0  ?  7.0    0  
302    1.0  ?  3.0    0  
---
      age  sex   cp  trestbps   chol  fbs  restecg  thalach  exang  oldpeak  \
87   53.0  0.0  3.0     128.0  216.0  0.0      2.0    115.0    0.0      0.0   
266  52.0  1.0  4.0     128.0  204.0  1.0      0.0    156.0    1.0      1.0   

     slope   ca thal  num  
87     1.0  0.0    ?    0  
266    2.0  0.0    ?    2  

合計で6レコードが「?」になっていました。

clevelandのデータセットの欠損値処理

「?」になっているのをNaNにして、欠損値処理をします。

数値以外をNaNに変更
df['ca'] = pd.to_numeric(df['ca'], errors='coerce')
df['thal'] = pd.to_numeric(df['thal'], errors='coerce')

print(df[df['ca'].apply(pd.to_numeric, errors='coerce').isnull()])
print("---")
print(df[df['thal'].apply(pd.to_numeric, errors='coerce').isnull()])
Out[0]
      age  sex   cp  trestbps   chol  fbs  restecg  thalach  exang  oldpeak  \
166  52.0  1.0  3.0     138.0  223.0  0.0      0.0    169.0    0.0      0.0   
192  43.0  1.0  4.0     132.0  247.0  1.0      2.0    143.0    1.0      0.1   
287  58.0  1.0  2.0     125.0  220.0  0.0      0.0    144.0    0.0      0.4   
302  38.0  1.0  3.0     138.0  175.0  0.0      0.0    173.0    0.0      0.0   

     slope  ca  thal  num  
166    1.0 NaN   3.0    0  
192    2.0 NaN   7.0    1  
287    2.0 NaN   7.0    0  
302    1.0 NaN   3.0    0  
---
      age  sex   cp  trestbps   chol  fbs  restecg  thalach  exang  oldpeak  \
87   53.0  0.0  3.0     128.0  216.0  0.0      2.0    115.0    0.0      0.0   
266  52.0  1.0  4.0     128.0  204.0  1.0      0.0    156.0    1.0      1.0   

     slope   ca  thal  num  
87     1.0  0.0   NaN    0  
266    2.0  0.0   NaN    2

NaNになりました。これだけだと欠損値になってしまうので、何かしらの値で置換しようと思います。

置換用に予測モデルを作ってもいいのですが、今回は簡単にnumごとにcaとthalの中央値を計算し埋めてあげようと思います。

numごとにcaとthal_medianの中央値を計算
ca_median = df.dropna().groupby("num")["ca"].median()
thal_median = df.dropna().groupby("num")["thal"].median()

print("ca_median",ca_median)
print("thal_median",thal_median)
Out[0]
ca_median num
0    0.0
1    1.0
2    1.0
3    2.0
4    2.0
Name: ca, dtype: float64
thal_median num
0    3.0
1    7.0
2    7.0
3    7.0
4    7.0
Name: thal, dtype: float64
中央値で欠損値を埋める
df['ca'] = df['ca'].fillna(df['num'].map(ca_median))
df['thal'] = df['thal'].fillna(df['num'].map(thal_median))

# 欠損があったレコードを抽出し置換出来ているかを確認
print(df[["ca","thal","num"]].iloc[[166, 192, 287, 302,87, 266]])
Out[0]
      ca  thal  num
166  0.0   3.0    0
192  1.0   7.0    1
287  0.0   7.0    0
302  0.0   3.0    0
87   0.0   3.0    0
266  0.0   7.0    2

きちんと置換できているようです。

clevelandのデータセットの各変数の統計量を確認

変数の統計量を確認
df.describe(include='all',percentiles=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.95,0.99]).transpose()
Out[0]
col count mean std min 10% 20% 30% 40% 50% 60% 70% 80% 90% 95% 99% max
age 303.0 54.438944 9.038662 29.0 42.0 45.0 50.0 53.00 56.0 58.00 59.4 62.0 66.0 68.0 71.00 77.0
sex 303.0 0.679868 0.467299 0.0 0.0 0.0 0.0 1.00 1.0 1.00 1.0 1.0 1.0 1.0 1.00 1.0
cp 303.0 3.158416 0.960126 1.0 2.0 2.0 3.0 3.00 3.0 4.00 4.0 4.0 4.0 4.0 4.00 4.0
trestbps 303.0 131.689769 17.599748 94.0 110.0 120.0 120.0 126.00 130.0 134.00 140.0 144.6 152.0 160.0 180.00 200.0
chol 303.0 246.693069 51.776918 126.0 188.8 204.0 218.0 230.00 241.0 254.00 268.4 286.0 308.8 326.9 406.74 564.0
fbs 303.0 0.148515 0.356198 0.0 0.0 0.0 0.0 0.00 0.0 0.00 0.0 0.0 1.0 1.0 1.00 1.0
restecg 303.0 0.990099 0.994971 0.0 0.0 0.0 0.0 0.00 1.0 2.00 2.0 2.0 2.0 2.0 2.00 2.0
thalach 303.0 149.607261 22.875003 71.0 116.0 130.0 140.6 146.00 153.0 159.00 163.0 170.0 176.6 181.9 191.96 202.0
exang 303.0 0.326733 0.469794 0.0 0.0 0.0 0.0 0.00 0.0 0.00 1.0 1.0 1.0 1.0 1.00 1.0
oldpeak 303.0 1.039604 1.161075 0.0 0.0 0.0 0.0 0.38 0.8 1.12 1.4 1.9 2.8 3.4 4.20 6.2
slope 303.0 1.600660 0.616226 1.0 1.0 1.0 1.0 1.00 2.0 2.00 2.0 2.0 2.0 3.0 3.00 3.0
ca 303.0 0.666667 0.933790 0.0 0.0 0.0 0.0 0.00 0.0 1.00 1.0 1.0 2.0 3.0 3.00 3.0
thal 303.0 4.735974 1.940231 3.0 3.0 3.0 3.0 3.00 3.0 6.00 7.0 7.0 7.0 7.0 7.00 7.0
num 303.0 0.937294 1.228536 0.0 0.0 0.0 0.0 0.00 0.0 1.00 1.0 2.0 3.0 3.0 4.00 4.0

・年齢は29歳~77歳までと幅広い年齢層のようです。
・性別は1:男性が7割と多いデータになっています。
・胸痛のタイプは4割が無症状

といった情報が分かりました。

clevelandのデータセットの目的変数の作成

心臓病有無フラグの作成
#numは0~4までの値が存在するようです。0と1が心臓病の疑いがあるためフラグに変更しておきます。
df['target'] = df['num'].apply(lambda x: 1 if x >= 1 else 0)

print(df["target"].quantile([0,0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99,1.0]))
Out[0]
0.00    0.0
0.10    0.0
0.20    0.0
0.30    0.0
0.40    0.0
0.50    0.0
0.60    1.0
0.70    1.0
0.80    1.0
0.90    1.0
0.95    1.0
0.99    1.0
1.00    1.0
Name: target, dtype: float64

だいたい半数が心臓病ありのようです。モデル作成のためにバランスデータになっているのでしょうか。

スポンサーリンク

グラフにしてデータの視覚化

そんなにデータ量も変数の数を多くないデータセットなのでさくっと確認して見ます。

各変数の分布の確認

# 各変数の分布確認
fig = plt.figure(figsize=(18,50))

for index in range(len(numerical_df.columns)):
    plt.subplot(10,4,index+1)
    sns.histplot(data=numerical_df.iloc[:,index].dropna(),bins=20)
fig.tight_layout(pad=1.0)
Out[0]

  • 年齢は50~60台が多い
  • 男性が多い
  • 心臓病有無は大体半数ずつでバランスが取れている

num変数で色分けした分布を確認

# 各変数の分布確認 (num変数ごとに色分け)
fig = plt.figure(figsize=(18,50))

for index in range(len(numerical_df.columns)):
    plt.subplot(10,4,index+1)
    sns.histplot(data=numerical_df,x=numerical_df.iloc[:,index].dropna(),hue="num",multiple="stack",bins=20)
fig.tight_layout(pad=1.0)
Out[0]

グラフから読み取れるのは下記でしょうか。

  • 男性の方が心臓病の確率が高い (たまたまかな?)
  • thalach(最大心拍数)は低いほど心臓病の確率が高い
  • 感覚とズレますが、cp(胸痛のタイプ)が無症状ほど心臓病の確率が高い
  • ca(色づけされた血管の数)が大きいほど心臓病の確率が高い
  • slope(ピーク運動時のST部分の傾斜)が平坦か下降だと心臓病の確率が高い

相関係数の確認

# 相関係数確認 (r < 0.3は非表示)
import matplotlib.pyplot as plt
plt.figure(figsize=(18,14))
corr=df.drop(columns=df.columns[[0]]).corr()
sns.heatmap(corr, vmax=1, vmin=-1, center=0, mask = abs(corr) < 0.3,linecolor="black",linewidth=0.5, annot=True,annot_kws={"size":8})
Out[0]

  • num x thalachは負の相関がある
  • num x oldpeak/ca/thalあたりが正の相関あり
  • oldpeak x slopeも正の相関あり
スポンサーリンク

まとめ

医療データの分析は初めてですが、実際にこのようなデータが人々の心臓病へのリスクを発見するのに役立って来たのかと思うとデータ分析を仕事に選んできて良かったなと思います。

今回はビジネスの理解とデータの理解を中心にやりましたので、次回は実際に心臓病かどうかを当てるモデルを作成していきたいと思います。

タイトルとURLをコピーしました