Pythonで美しいグラフを描こう！（plotnine編）

**2022/10/20：plotnineのテーマも記事にしました。**

https://qiita.com/hima2b4/items/e458de32cf96d651c719

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/6f2f3870-f6d9-bd25-08af-b51f2a92e540.png)
##  はじめに

Rにggplot2という視覚化パッケージがあり、このPython版が [**plotnine**](https://pypi.org/project/plotnine/) です。
Python の視覚化といえば、matplotlib、[seaborn](https://pypi.org/project/seaborn/) が代表格。この他に[Bokeh](https://pypi.org/project/bokeh/) や[Plotly](https://pypi.org/project/plotly/) 等 さまざまありますが、ggplot2（＝[**plotnine**](https://pypi.org/project/plotnine/)）でしか描けないグラフもあり、これがまたいいんです。
見た目が美しく、美しいからこそ可読性も高い、ありがたいライブラリです。

この記事では、sklearn のワインのデータセットで [**plotnine**](https://pypi.org/project/plotnine/) ならではの ヒストグラム、箱ひげ図、散布図を描きます。

　
##  実行条件など

>-Google colabで実行
-任意のデータセットとsklearn等のデータセットを読み出せるようにしています。


##  実行
ワインデータセットは、イタリアの同じ地域における3種類の異なるワインの化学分析の結果です。

>- alcohol： アルコール度数
>- malic_acid： リンゴ酸
>- ash： 灰分
>- alcalinity_of_ash： 灰分のアルカリ度
>- magnesium： マグネシウム
>- total_phenols： 全フェノール含量
>- flavanoids： フラボノイド
>- nonflavanoid_phenols： 非フラボノイドフェノール
>- proanthocyanins： プロアントシアニン
>- color_intensity： 色の濃さ
>- hue： 色相
>- od280/od315_of_diluted_wines： 希釈ワインの280nmと315nmの吸光度の比
>- proline： プロリン
>- target：class_0、class_1、class_2（ワインの種類）
　
※説明変数はalchol〜prolineまで全13個あり、目的変数はtargetでワインの種類（3種類）となっています。


　
まず気になるのは、ワインの種類の違いが表現できそうな説明変数はあるだろうか？ということです。
このような時、pairplotがとても役に立ちます。
datasetを読み込んだ後、[seaborn-analyzer](https://pypi.org/project/seaborn-analyzer/) でpairplotを描いてみました。
![7F0A4A76-B1D3-4213-8978-74F6EA47F1F5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/1f499976-3b53-4332-4df2-bf886fcbbc64.png)
seaborn-analyzer のpairplotは可読性が高くて、とてもいいです。
ワインの種類の違いを1つの変数だけでとらえるのは難しそうですが、まずalcoholのヒストグラムをplotnineで見てみてみます。
![6A2CD29C-1FB3-4CFE-8076-3B5D6E77F2AE.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/b3af2544-7825-87e7-aafb-32ef2def3f5d.jpeg)
このヒストグラムでは何の特徴も掴めませんので、ワインの種類毎に層別したヒストグラムをplotnineで描きました。
![0CC8CB67-2A52-40FA-BDDD-C04406342F4B.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/3e9459c2-a652-e4c9-e010-dbb824b6eee1.jpeg)
重なりあっているところも多いですが、ワインの種類ごとにアルコール度数が違ってそうだなということがよくわかります。
次にワインの種類ごとのヒストグラムを横に並べて描いてみます。
plotnineはこのような表現が簡単にできるのがひとつの特徴です。
![349620FF-14AC-4C36-9731-1CB70D930298.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/c9f8ccfb-be4d-8bb9-4743-2a96adc79412.jpeg)
箱ひげ図でも同じように描いてみます。
![56038AF3-75BD-491D-A4C3-0B6EDDDE4DAE.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/406a3fc0-1202-b07a-3a05-c6b4806f336a.jpeg)
ワインの種類によってアルコール度数の中央値に違いがあることがより視覚的にわかります。
次に、先のヒストグラムと同じ要領で、ワインの種類ごとの箱ひげ図を横に並べて描いてみます。
![2A7B3D3D-67D7-4865-90B2-C9B3D2E9CE28.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/f515cf29-e2f7-efa0-cebb-b6db838d6be2.jpeg)
これは、わざわざ独立して描く必要はなかったですが、このような描き方がplotnineで簡単にできます。

次に散布図です。
最初に描いたpairplotで、 alcoholとod280/od315_of_dliluted_winesの関係がもっともワインの種類の違いを区分しているように見えましたので、これで描きました。
![4EF6C289-18E1-4B7E-8C72-86EDB134F2C9.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/23f64789-ecd0-a3b6-3f50-d7e5dcfafe7e.jpeg)
層別表示できているのでわかりやすいですね。

次は、ワインの種類ごとの散布図を横に並べて描いてみます。
![217E773C-54E0-46EA-B5E5-E74D7316C2F5.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/1590b34c-611a-9f94-781a-6eb77e84d474.jpeg)
わかりやすいですね。

あと、plotnineでは、線形回帰を表示することもできます。
![B972D44F-8DA7-4D9A-9B6E-871F2BE7B11E.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/e7f72406-5519-94b0-bf49-4249cd247f24.jpeg)

以下は、参考ですが[seaborn-analyzer](https://pypi.org/project/seaborn-analyzer/) のclass_separator_plot で描いたものです。
SVCによるクラス分けを可視化してくれます。
![C0098FCF-ED2A-48A8-AD7E-29F80B4AFC5D.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/f099b38a-5d6e-46d0-98c4-83b7c21d2104.jpeg)

最後に、[**plotnine**](https://pypi.org/project/plotnine/) のfacet設定を紹介します。
以下は、Titanicデータセットをfacet設定を利用し、可視化したものです。
X軸：age（乗客年齢）、Y軸：fare（運賃）の散布図＋alive（生死）色分け（層別）＋sex（性別）とclass（客室クラス）毎のマトリクスとなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/a24dad98-fef8-84fe-fc9e-15055bec1ff1.png)
男性・女性で見ると、女性の生存が男性を圧倒しています。
男性は、セカンドクラス・サードクラスはほぼ全滅ですが、ファーストクラスのみ生存率が高くなっています。
また、すべてのクラスで幼児は救われたように見えます。
女性は、生存率は高かったようですが、サードクラスのみ亡くなったの多さが顕著です。
「女性・子供は守られた」、「金持ちは優先された」のでしょう。

このようにfacet設定を利用すると、可読性の高いグラフ配置が可能です。


## 実行コード
以下は、[seaborn-analyzer](https://pypi.org/project/seaborn-analyzer/) のインストールです。
```Python:seaborn-analyzerのインストール
!pip install seaborn-analyzer

```
以下で、データセットがドロップダウンで選択できるよう、GoogleColabのフォーム機能でセット。
```Python:データセット選択
#@title Select_Dataset { run: "auto" }
#@markdown  **<font color= "Crimson">注意</font>：かならず 実行する前に 設定してください。**</font>

dataset = 'wine : classification' #@param ['Boston_housing :regression', 'Diabetes :regression', 'Breast_cancer :binary','Titanic :binary', 'Iris :classification', 'Loan_prediction :binary',"wine : classification", 'Upload']
```
以下は、先のドロップダウンで設定したデータセットを読込むコードです。読込み後はデータフレームに格納します。
```Python:データセット読込み⇒データフレーム格納
#@title Load dataset

#ライブラリインポート
import numpy as np
import pandas as pd
import seaborn as sns
import warnings
warnings.simplefilter('ignore')


if dataset =='Upload':
  from google.colab import files
  uploaded = files.upload()#Upload
  target = list(uploaded.keys())[0]
  df = pd.read_csv(target)

elif dataset == "Diabetes :regression":
  from sklearn.datasets import load_diabetes
  dataset = load_diabetes()
  df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
  df["target"] = dataset.target

elif dataset == "Breast_cancer :binary":
  from sklearn.datasets import load_breast_cancer
  dataset = load_breast_cancer()
  df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
  df["target"] = dataset.target

elif dataset == "Titanic :binary":
  data_url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
  df = pd.read_csv(data_url)
  X = df.drop(['Survived'], axis=1) # 目的変数を除いたデータ
  y = df['Survived'] # 目的変数
  df = pd.concat([X, y], axis=1)

elif dataset == "Titanic(seaborn) :binary":
  df = sns.load_dataset('titanic')
  X = df.drop(['survived','pclass','embarked','who','adult_male','alive'], axis=1) # 重複しているカラムを除いたデータ
  y = df['alive'] # 目的変数
  df = pd.concat([X, y], axis=1)

elif dataset == "Iris :classification":
  from sklearn.datasets import load_iris
  iris = load_iris()
  df = pd.DataFrame(iris.data, columns = iris.feature_names)
  df['target'] = iris.target_names[iris.target]

elif dataset == "wine : classification":
  from sklearn.datasets import load_wine
  wine = load_wine()
  df = pd.DataFrame(wine.data, columns = wine.feature_names)
  df['target'] = wine.target_names[wine.target]

elif dataset == "Loan_prediction :binary":
  data_url = "https://github.com/shrikant-temburwar/Loan-Prediction-Dataset/raw/master/train.csv"
  df = pd.read_csv(data_url)

else:
  from sklearn.datasets import load_boston
  dataset = load_boston()
  df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
  df["target"] = dataset.target

#X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
#y = pd.Series(dataset.target, name='target')

source = df.copy()

FEATURES = df.columns[:-1]
TARGET = df.columns[-1]
X = df.loc[:, FEATURES]
y = df.loc[:, TARGET]

df.info(verbose=True)
df.head()

```
以下で、カラムをデータ型区分して表示させています。
※**注意**：categoryやboolもNumerical_columns:に区分されます。

```Python:カラムをデータ型で区分
#@title Datasetの数字・文字列区分

numerical_col = []
Object_col = []

for col_name, item in df.iteritems():
    if item.dtype == object:
        Object_col.append(col_name)
    else:
        numerical_col.append(col_name)
        
print('-----------------------------------------------------------------------------------------')
print('Numerical_columns:', numerical_col)
print('-----------------------------------------------------------------------------------------')
print('Object_columns:', Object_col)
print('-----------------------------------------------------------------------------------------')
```
以下は、[seaborn-analyzer](https://pypi.org/project/seaborn-analyzer/) によるpairplotの実行です。
```Python:ペアプロット表示
#@title Pairplot

from seaborn_analyzer import CustomPairPlot

cp = CustomPairPlot()
cp.pairanalyzer(df, hue=TARGET)
```
### ヒストグラム
```Python:ヒストグラム
#@title　Histgram
from plotnine import *

Column_name =  'alcohol'#@param {type:"raw"}
bins_number_slider = 13 #@param {type:"slider", min:5, max:20, step:1}

(ggplot(df, aes(Column_name))+ geom_histogram(bins = bins_number_slider))
```

```Python:重ね合わせヒストグラム
#@title Stratified histogram by target

Column_name =  'alcohol'#@param {type:"raw"}
bins_number_slider = 13 #@param {type:"slider", min:5, max:20, step:1}

from plotnine import *
(ggplot(df, aes(Column_name, fill = TARGET)) 
 + geom_histogram(bins = bins_number_slider, position = "identity", alpha = 0.7)
) 
```
```Python:クラス毎にヒストグラム
#@title Histogram for each target variable
Column_name =  'alcohol'#@param {type:"raw"}

from plotnine import *
(ggplot(df, aes(x = Column_name, fill = TARGET))
 + geom_histogram()
 + facet_wrap(TARGET))

```
### 箱ひげ図
```Python:箱ひげ図
#@title Stratified box-plot by target

Column_name =  'alcohol'#@param {type:"raw"}

from plotnine import *
(ggplot(df, aes(TARGET, Column_name, fill = TARGET))
 + geom_boxplot()) 
```

```Python:クラス毎に箱ひげ図
#@title box-plot for each target variable
Column_name = 'alcohol'#@param {type:"raw"}

from plotnine import *
(ggplot(df, aes(TARGET, Column_name, fill = TARGET))
+ geom_boxplot()
+ facet_wrap(TARGET))
```
### 散布図
```Python:散布図
#@title Stratified scatter-plot by target

X_column_name =  'alcohol'#@param {type:"raw"}
y_column_name =  'od280/od315_of_diluted_wines'#@param {type:"raw"}

from plotnine import *
(ggplot(df, aes(x=X_column_name, y=y_column_name, color= TARGET))
 +geom_point())
```

```Python:クラス毎に散布図
#@title Scatter-plot for each target variable

X_column_name =  'alcohol'#@param {type:"raw"}
y_column_name =  'od280/od315_of_diluted_wines'#@param {type:"raw"}

from plotnine import *
(ggplot(df,aes(x=X_column_name, y=y_column_name, color=TARGET))
 + geom_point()
 + facet_wrap(TARGET))
```

```Python:クラス毎に直線回帰付き散布図
#@title Scatter-plot for each target variable with linear regression

X_column_name =  'alcohol'#@param {type:"raw"}
y_column_name =  'od280/od315_of_diluted_wines'#@param {type:"raw"}

from plotnine import *
(ggplot(df, aes(x=X_column_name, y=y_column_name, color = TARGET))
 + geom_point()
 + stat_smooth(method='lm')
 + facet_wrap(TARGET))
```
### class_separator_plot
```Python:seaborn-analyzerによるclass_separator_plot
#@title class_separator_plot

X_column_name = 'alcohol'#@param {type:"raw"}
y_column_name = 'od280/od315_of_diluted_wines'#@param {type: "raw"}

import seaborn as sns
from sklearn.svm import SVC
from seaborn_analyzer import classplot

clf = SVC()
classplot.class_separator_plot(clf, [X_column_name, y_column_name], TARGET, df)

```
### facet機能を用いた散布図
```Python:facet機能を用いた散布図
#@title scatter plot with facet

X_column_name = 'age'#@param {type:"raw"}
y_column_name = 'fare'#@param {type: "raw"}
facet = 'sex~class'#@param {type: "raw"}

from plotnine import *
(
    ggplot(df)
    + facet_grid(facets = facet) #facetは 'sex~class'のように記述
    + aes(x= X_column_name, y= y_column_name, color=TARGET)
    + labs(
        x= X_column_name,
        y= y_column_name,
        title="Plot Subsets of Data Into Panels in the Same Plot",
    )
    + geom_point()
)

```

##  最後に
[**plotnine**](https://pypi.org/project/plotnine/) は美しく可読性が高い可視化が簡単コードで実現できます。
私は、[seaborn-analyzer](https://pypi.org/project/seaborn-analyzer/) とのセット利用がお気に入りです。