4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【matplotlib】Pythonで学術論文クオリティの散布図を作ろう

Last updated at Posted at 2023-12-06

0. はじめに

今回は化学の学術論文で使用できるレベルのグラフを Python のモジュールの一つである Matplotlib を用いて作成します.
グラフによって使う設定が異なるので,初回は最も基本的な散布図を対象に説明します.

本記事で学べること

  • Python, Matplotlib を用いた散布図の作成手法
  • 検量線の作成に必要な基礎的な単回帰分析の手法
  • Matplotlib の散布図に関する関数や引数の知識
  • 学術論文に使用するグラフの要件と作り方

対象とする読者

  • 基本的な Linux の操作, Python3 の文法が分かる人
  • Matplotlib で学術論文クオリティの図が書きたい人
  • 有料のソフトウェアを使わないことに快感を憶える人
  • Matplotlib の公式リファレンス1を読むのは大変という人

対象としない読者

  • りぬっくす?ぴちょん?なにそれ美味しいの?という人
  • 色鮮やかで見栄えの良いグラフを描きたい人
  • 商用ソフトウェアで満足できる人
  • Matplotlib の公式リファレンス1を読みこなせる人

Windows 11における記事執筆時点(2023年12月)での環境構築

WSL(Windows Subsystem for Linux)の登場により,Windows でも Linux の環境を整えることが容易になりました.今回は一例として,Ubuntu 22.04 LTS の環境構築のやり方をざっくり示しておきます.

  • Microsoft Store から Windows Subsystem for Linux をインストールする
  • Microsoft Store から Ubuntu 22.04 LTS をインストールする
  • 公式サイトから Visual Studio Code をインストールする
  • Visual Studio Code (VSCode) を起動し,拡張機能の WSL をインストールする
  • Ubuntu 22.04 LTS を起動し,ユーザ名とパスワードを登録する
  • 以下のコードを実行する
初期設定
# 最新の状態に更新する
sudo apt update && sudo apt upgrade -y
# 日本語を使えるようにする(任意)
sudo apt install language-pack-ja -y
sudo update-locale LANG=ja_JP.UTF8
# Windowsのフォントを使えるようにする
sudo apt install fontconfig -y
# モジュールを管理するため,pipをインストールする
sudo apt install python3-pip -y
# 必要なモジュールをインストールする
pip3 install numpy scikit-learn pandas matplotlib ipykernel

実行時にパスワードの入力が求められますので,最初に登録したパスワードを入力してください.

  • VSCode を起動する
VSCodeの起動
# VSCodeをカレントディレクトリで起動する
code .
  • VSCode を開いて以下の拡張機能を追加する
    • Python
    • Jupyter

拡張機能をインストールすると,追加で Pylance などの拡張機能も追加でインストールされます.
私が実際に使用している環境はもっとごちゃごちゃしていますが,最小構成はこちらとなります.
これだけではやり方が分からない方は以下の記事を参考に環境構築をしてみてください.

実行環境

  • Ubuntu 22.04 LTS (WSL2)
  • Visual Studio Code (Windows版 1.84.2)
    • Jupyter
    • Python
    • その他セットで付いてくるもの
  • Python 3.10.12
    • Matplotlib (3.8.2)
    • NumPy (1.26.2)
    • Scikit-learn (1.3.2)
    • Pandas (2.1.3)
    • ipykernel (6.27.1)

グラフ作成の基礎

ファイル形式(ラスタ形式,ベクタ形式)

Python でグラフを作成する前に基本的な知識を共有しておきます.
初めに Figure を作る際には切っても切り離せない画像のファイル形式の話をします.
画像のファイル形式には大きくわけてラスタ形式とベクタ形式の2つが存在します.

ラスタ形式とは各ピクセル(画素)にカラーコード(RGBやCMYK)を割り当てる形式です.
作成した図の解像度は保存した時の画素数に依存するので,画素数が少ないと拡大した時にぼやけた画像になります.
ラスタ形式で保存するファイルの拡張子にはbmp, tif, jpg, png, gifなどがあります.

一方で,ベクタ形式は図における点や線の情報を保存する形式です.
簡単に言えば,「ある点からある点まで直線を引く」や「中心点から半径いくつの円を360度描く」という数式を保存する形式です.
ベクタ形式は拡大してもその度に見た目が更新されるのでぼやけることはありません.
しかしながら,複雑な図式を描くことは苦手です.
ベクタ形式で保存するファイルの拡張子にはsvg, eps, ai, emfなどがあります.

私は,基本的にはラスタ形式なら PNG か TIFF を,ベクタ形式では SVG を使用しています.
PNG は背景を白ではなく,透明にすることができ,Microsoft の PowerPoint で編集する際に他の図形と被らないので便利です.
TIFF は主に画像ファイルを保存するために使用します.
SVG は Adobe の Illustrator を用いて図を作成する際に使用します.
SVG ファイルで読み込むと,Illustrator でも微修正が可能なのでオススメです.

可逆性圧縮と非可逆性圧縮

続いてファイル形式とファイルサイズの圧縮について説明します.
ラスタ形式のファイルにおいて,全ての画素の RGB を保存しておくと,日本の国旗のようなシンプルなイラストであっても非常に大きなファイルとなってしまいます.
そこで情報を圧縮することにより,ファイルのサイズを削減したファイル形式が広く用いられています.
この圧縮形式は大きく分けて可逆性圧縮と非可逆性圧縮の2つがあり,字のごとく可逆的に圧縮するものと一度圧縮したら非可逆になってしまうものに分かれています.
ラスタ形式の画像の有名な圧縮ファイル形式として,前者では PNG (Portable Network Graphics, .png) が,後者では JPEG (Joint Photographic Experts Group, .jpg) が知られています.
また,画像ファイルでよく見かける BMP (Bitmap Image File, .bmp) や TIFF (Tagged Image File Format, .tif)は圧縮されていないファイルですので,ファイルサイズが大きくなります.

配色について

私はお絵描きが下手なので配色に関して言及することはあまりありませんが,これだけは意識しています.
それは,「白黒印刷でも違いが分かるようにすること」です.
近年では,PDF ファイルで論文を読むことが多くなったので,印刷して読む人はだいぶ少なくなっているかと思いますが,私は重要な論文は印刷して読みたい派なので,白黒印刷でも違いが分かるということはかなり重要です.
また,読者の中には一定の割合で色盲の方がいらっしゃるので,そのような方でも違いが明確に分かるような配色にすることが研究者としての最低限の配慮だと思っています.
以下のサイトが参考になります.

Matplotlib では予め初期設定されている色の順番のままでもコントラストが明瞭なので,あまり気にしなくても問題はありませんが,色を多く使うことは学術論文では避けて,線や記号を使い分けることを推奨します.

1. 今回作成する図について

基本的な散布図である検量線のグラフを描いてみます.
紫外可視吸光光度法(UV-Vis)により得られた物質の吸光度のデータから検量線を作成することを想定しています.
凡例は普段なら必要ありませんが,練習のためにわざと載せています.

完成形

まずは今回のゴールである図を示します.

test.jpg

仕様を確認する

図を作成する前に学術論文で求められる図の要件について知っておきましょう.
学術論文には出版社,雑誌ごとに図の体裁に厳格なルールが設定されています.
このルールを守ることで,出版された時に拡大するとぼやけて文字が潰れてしまうという悲劇を防げます。
また,近年の学術論文は一つのグラフだけで一枚の Figure を構成することは少ないので,Matplotlib で作成したグラフを Microsoft の PowerPoint や Adobe の Illustrator,無料のソフトウェアだと Inkscape などのソフト上で並べ替えて最終的な Figure を出力することが多いと思います.
私はラボの事情で Illustrator を使って整形しています.

例えば私が論文を出したことのある ACS(アメリカ化学会)のサイト2には Figure size の要件が以下のように示されています.

Size
Graphics must fit a one- or two-column format. Single-column graphics can be sized up to 240 points wide (3.33 in.) and double-column graphics must be sized between 300 and 504 points (4.167 in. and 7 in.). The maximum depth for all graphics is 660 points (9.167 in.) including the caption (allow 12 pts. For each line of caption text). Lettering should be no smaller than 4.5 points in the final published format. The text should be legible when the graphic is viewed full-size. Helvetica or Arial fonts work well for lettering. Lines should be no thinner than 0.5 point.

私によるてきとーな和訳 サイズ

図表は 1段か 2段のフォーマットに揃える必要があります. 1段の図表は 240ポイント(3.33インチ)まで,2段の図表は 300ポイントから 504ポイント(4.167インチから 7インチ)の間に揃えなければなりません.また,全ての図において最大縦幅はキャプション(12ポイント)を含めて,660ポイント(9.167インチ)となります.文字は最終版の図において 4.5ポイントを下回ってはいけません.テキストはフルサイズで見た際に見えるようにしておくべきです.Helvetica(Mac)か Arial(Windows)のフォントが良いでしょう.線は 0.5ポイントを下回らないようにしてください.

アメリカはヤード・ポンド法の世界なのでインチ単位で指定することが多いので注意しましょう.

さて,グラフを作る上で重要なことはグラフの大きさと解像度、そしてフォントサイズです.
グラフの大きさは基本的には 1 colunn(3.33 in.)か 2 column(7 in.)の大きさで作成します.
ACS では白黒画像,グレースケール画像,カラー画像でそれぞれ 1200 dpi、600 dpi、300 dpi と定められています.
dpi とは dot per inch の略で 1インチあたりの画素数という意味になっています.
私は凡例でグレーを使うことが多いので基本的には 300 dpi か 600 dpi に設定することが多いです.
フォントサイズは図表をそのままのサイズで使用する場合は 10 から 12 ポイントになるように設定しましょう.
もし図表を縮小する場合は,縮小したあとの大きさが 10 ポイントくらいになるように設定します.

2. 初期設定とモジュールのインポート

フォントの更新(初回だけ)

いよいよ,本格的に Python のコードを書いていきます...と言いたいところですが,その前に WSL の Ubuntu 側から Windows のフォントを読み込む設定をしましょう.
以下の記事を参考にします.

はじめに Ubuntu 側で Windows のフォントを読むためのファイル(local.conf)を作成します.

フォントの参照先の追加
cd /etc/fonts/
sudo vi local.conf

ここでは Vim を使っていますが,エディタは Emacs でも Nano でも構いません(私は寛大なので).
このディレクトリでは一般ユーザは編集できないので,必ずsudoで実行してください(環境構築の際に設定したパスワードを入力します).
次に,local.confに以下の内容をコピペ記入して,保存してください.

/etc/fonts/local.conf
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir>/mnt/c/Windows/Fonts</dir>
</fontconfig>

local.confは XML 形式のファイルになっています.
これを追加することで Ubuntu の内部で Windows のフォントが保存されているディレクトリを参照するようになります.
続いて,フォントの情報を更新します.

フォントの更新
fc-cache -fv

最後に Matplotlib に保存されているフォント情報を削除し,再度読み込んでもらいます.
もし,ファイルが存在しませんというエラーが出たら気にしなくて結構です.

Matplotlibのフォント設定の消去
rm ~/.cache/matplotlib/fontlist-v330.json

ディレクトリの構造について

今回使用するディレクトリはこのような構成になっています.

Directory
test/
  ├─ test.ipynb
  └─ data/
       └─ test.csv

test/というディレクトリに,test.ipynbという IPythonNotebook ファイルとdata/というディレクトリがあり,その中にtest.csvという今回使うデータが入った CSV ファイルがあります.
Linux のコマンドの細かい説明は割愛しますが,以下の通りに実行すると,同じ内容のディレクトリ構造が作成できます.

ディレクトリとファイルの作成
# ホームディレクトリに移動する
cd $HOME
# ホームディレクトリにtestディレクトリを作成して,移動する
mkdir test
cd test
# 空のtest.ipynbを作成する
touch test.ipynb
# testディレクトリにdataディレクトリを作成して,移動する
mkdir data
cd data
# 空のtest.csvを作成する
touch test.csv
# testディレクトリに移動する
cd ..

モジュールのインポート

それでは VSCode を立ち上げてファイルを編集していきましょう.
VSCode はコマンドライン上でcode .と入力すれば立ち上がります.
左側のパネルに現在のディレクトリ以下のファイルが見えると思うので,そこからtest.ipynbを開いてください.
空のファイルが開かれているかと思います.

1セル目に以下のコードを入力してShift + Enterを押しましょう.
Shift + Enterはコードを実行して,次のセルに移る,次のセルがない場合は作成して移るというショートカットです.
次のセルを作成したり,移動する必要が無い場合はCtrl + Enterを押すと実行できます.

モジュールのインポート
%matplotlib inline
import os
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from matplotlib import pyplot as plt
from matplotlib import ticker

各モジュールの説明をします.
%matplotlib inlineは Matplotlib で作成した Figure を VSCode 上で表示するための文です.
osはカレントディレクトリを入手するために使用します.
numpyは配列を保存して計算をするために使用します.
pandasは CSV ファイルを読み込むために使用します.
scikit-learn.linear_modelLinearRegressionは線型回帰をするために使用します.
matplotlibライブラリにあるpyplotはグラフを作成するためには欠かせないモジュールです.
tickerpyplotにはない,目盛り設定を細かく行うためのモジュールです.
... as ~~~...モジュールの名前の名前を~~~として扱いますという意味です.

【重要】rcParamsの設定

Matplotlib には,グラフの体裁を整えるための様々な変数が存在します.
その変数を設定する辞書がrcParamsになります.
設定項目の詳細に関しては以下の記事や公式サイト1を参考にしています.

設定項目があまりに多いので全ての設定項目は説明しきれませんが,一度コピペして使用してみて不満があるところはご自身で修正してみてください.

rcParamsの設定
# matplotlib setting parameters
FONTSIZE = 12
FIGSIZE = [(3.14, 3.14),
           (5, 3.14),
           (5, 5),]

# Figure
plt.rcParams['figure.figsize']        = FIGSIZE[0]  # Figure size
plt.rcParams['figure.dpi']            = 150         # Dots per inch
plt.rcParams["figure.autolayout"]     = False       # レイアウトの自動調整を利用するかどうか
plt.rcParams["figure.subplot.left"]   = 0.10        # 余白
plt.rcParams["figure.subplot.bottom"] = 0.10        # 余白
plt.rcParams["figure.subplot.right"]  = 0.90        # 余白
plt.rcParams["figure.subplot.top"]    = 0.90        # 余白
plt.rcParams["figure.subplot.wspace"] = 0.20        # 図が複数枚ある時の左右との余白
plt.rcParams["figure.subplot.hspace"] = 0.20        # 図が複数枚ある時の上下との余白

# Fonts
plt.rcParams["font.family"]      = "sans-serif"       # フォントの種類
plt.rcParams["font.serif"]       = "Times New Roman"  # セリフ体の種類
plt.rcParams["font.sans-serif"]  = "Arial"            # サンセリフ体の種類
plt.rcParams["font.size"]        = FONTSIZE           # フォントサイズ
plt.rcParams["mathtext.cal"]     = "serif"            # 数式フォントの設定(カリグラフィ)
plt.rcParams["mathtext.rm"]      = "serif"            # 数式フォントの設定(ロマン体)
plt.rcParams["mathtext.it"]      = "serif:italic"     # 数式フォントの設定(斜体)
plt.rcParams["mathtext.bf"]      = "serif:bold"       # 数式フォントの設定(太字)
plt.rcParams["mathtext.fontset"] = "cm"               # 数式フォント(cmはComputer Modern)

# Ticks
plt.rcParams["xtick.direction"]     = "in"      # 目盛り線の向き、内側"in"か外側"out"かその両方"inout"か
plt.rcParams["ytick.direction"]     = "in"      # 目盛り線の向き、内側"in"か外側"out"かその両方"inout"か
plt.rcParams["xtick.top"]           = True      # 上部に目盛り線を描くかどうか
plt.rcParams["xtick.bottom"]        = True      # 下部に目盛り線を描くかどうか
plt.rcParams["ytick.left"]          = True      # 左部に目盛り線を描くかどうか
plt.rcParams["ytick.right"]         = True      # 右部に目盛り線を描くかどうか
plt.rcParams["xtick.major.size"]    = 4.0       # x軸主目盛り線の長さ
plt.rcParams["ytick.major.size"]    = 4.0       # y軸主目盛り線の長さ
plt.rcParams["xtick.major.width"]   = 1.0       # x軸主目盛り線の線幅
plt.rcParams["ytick.major.width"]   = 1.0       # y軸主目盛り線の線幅
plt.rcParams["xtick.minor.visible"] = False     # x軸副目盛り線を描くかどうか
plt.rcParams["ytick.minor.visible"] = False     # y軸副目盛り線を描くかどうか
plt.rcParams["xtick.minor.size"]    = 2.0       # x軸副目盛り線の長さ
plt.rcParams["ytick.minor.size"]    = 2.0       # y軸副目盛り線の長さ
plt.rcParams["xtick.minor.width"]   = 0.6       # x軸副目盛り線の線幅
plt.rcParams["ytick.minor.width"]   = 0.6       # y軸副目盛り線の線幅
plt.rcParams["xtick.labelsize"]     = FONTSIZE  # 目盛りのフォントサイズ
plt.rcParams["ytick.labelsize"]     = FONTSIZE  # 目盛りのフォントサイズ

# Axes
plt.rcParams["axes.labelsize"] = FONTSIZE  # 軸ラベルのフォントサイズ
plt.rcParams["axes.linewidth"] = 1.0       # グラフ囲う線の太さ
plt.rcParams["axes.grid"]      = False     # グリッドを表示するかどうか

# Grids
plt.rcParams["grid.color"]     = "black"   # グリッドの色
plt.rcParams["grid.linewidth"] = 1.0       # グリッドの線幅

# Legend
plt.rcParams["legend.loc"]        = "best"   # 凡例の位置、"best"でいい感じのところ
plt.rcParams["legend.frameon"]    = True     # 凡例を囲うかどうか、Trueで囲う、Falseで囲わない
plt.rcParams["legend.framealpha"] = 0.5      # 透過度、0.0から1.0の値を入れる
plt.rcParams["legend.facecolor"]  = "white"  # 背景色
plt.rcParams["legend.edgecolor"]  = "black"  # 囲いの色
plt.rcParams["legend.fancybox"]   = False    # Trueにすると囲いの四隅が丸くなる

# Check rc_Params (necessary)
# plt.rcParams

冒頭の数行は私が普段使う定数を示しています.
3.14 in.が約 8 cmで,5 in.が約 12.7 cmとなっています.
FIGSIZEの括弧の中は(横,縦)をインチ表記で示しています.
何故このような比率にしているかというと,単に黄金比に近づけたほうが見栄えが良さそうという安直な考えからです.
dpiを150 ptsにしている理由は,プレビュー画面で表示される図が大きくなってしまうからです.
解像度は後で修正することができます.
目盛りの向き((x/y)tick.direction)は内向き派(in)と外向き派(out)がありますので,どちらの派閥に属するかで適宜修正してください.
フォントの種類は仕様の説明にもあった通り,Arial を使用します.
また,数式フォントはcm,Computer Modern を使用します.
$\LaTeX$を扱ったことがある人はおなじみかもしれません.

最後のplt.rcParamsはコメントアウトしていますが,# を削除すると,パラメータ一覧が表示されます.
パラメータの数は350種類以上あるので,これでも全てのパラメータを網羅してはいませんが,必要な設定項目は抑えてあると思います.

3. データの読み込み

準備ができたところでデータを読み込みます.
今回使用するデータは以下のファイルになります.
先程作成したtest.csvに以下の内容をコピペしましょう.

test.csv
Concentration[mmol/L],Absorbance[-]
1.0,0.0102
2.0,0.0190
4.0,0.0401
8.0,0.0829
16.0,0.1534
32.0,0.3309
64.0,0.6684
128.0,1.2646

これは濃度既知の状態で色素の吸光度を測定した時のデータを想定した仮想のデータです.
一般的に Lambert-Beer の法則が成り立つ場合3は,吸光度は以下の式で表されます.

\mathrm{Abs}=-\log{\frac{T}{T_0}}=\varepsilon cl

ここで,$T,T_0$はそれぞれ透過光と入射光の強度,$\varepsilon$はモル吸光係数,$c$はモル濃度,$l$はセルの光路長を示しています.
つまり,適切な条件が整っていれば,吸光度はモル濃度に比例します.
それでは Pandas の機能を使用して CSV ファイルを読み込みます.

データの読み込み
# read data
dirName  = os.getcwd() + '/'
dataDir  = 'data/'
fileName = 'test.csv'
df = pd.read_csv(dirName + dataDir + fileName, header=0, encoding='utf-8')
df.head()

ここでは,はじめにos.getcwd()により,カレントディレクトリの取得を行っています.
ファイル名の指定は相対パスでも良いのですが,私はクラウドストレージにデータを保存しているので,クラウドストレージのディレクトリを正しく指定できるように絶対パスを取得しています.
dataDirdataディレクトリを指定するための変数です.
fileNameはファイルの名前を入力するための変数です.
ちなみに複数ファイルを読み込みたい場合はglobというモジュールを使用すると,簡単にファイル名を一覧で取得できます(後述).

pandasのread_csvという関数を使うと,DataFrameクラスのインスタンスを受け取ります.
この関数の引数は以下のものが挙げられます.
これらは引数の中の一部ですので,詳しくは公式リファレンス4を参照してください.

引数 デフォルト値 説明
filepath なし ファイルのパスを指定する
sep ',' CSVファイルの区切り文字を指定する
header 'infer' 見出しの行数(整数)を指定する
encoding 'utf-8' エンコードする文字コードを指定する
names optional 各列の名前を配列で指定する
index_col optional インデックスを含む列を列番号(整数)で指定する
usecols optional 読み込む列を配列で指定する
dtype optional データの型を辞書(キー:列名,要素:データ型)で指定する
skiprows optional スキップする行数を整数で指定する

headerinfernameを指定していない場合は先頭の行をヘッダとして読み込み,指定した場合はデータとして読み込む処理になります.
この中で私がよく使う引数はencodingskiprowsになります.
encodingはファイルを読み込む際の文字コードを指定することができます.
使用するメーカによってエクスポートされる CSV ファイルの文字コードが異なるため,utf-8の他にshift-jisで読み込むこともあります.
また,メーカのエクスポートファイルには最初の数行は装置の設定項目などを記述しているので,それらの情報は読み込まないように指定した行をスキップするskiprowsを指定します.
ちなみに Pandas には Excel ファイルを読み込むread_excel()という関数もありますが,read_csv()と少し勝手が異なるので注意が必要です.

(おまけ)複数データの読み込み

今回は単一のデータを読み込みましたが,複数のファイルからデータを読み込むこともあります.
その場合は,以下のコードを実行して読み込みます.
上記のコードとの違いはglobを使用して正規表現により複数のファイルのパスを有する配列を得るところと,その配列から複数の DataFrame インスタンスを作成することです.

複数ファイルの読み込み
import glob
fileNames = glob.glob(dirName + dataDir + "*.csv", recursive=False)
# ファイル名の確認
print(fileNames)
# ファイル名の順番が不適当な場合ソートする
# fileNames.sort()
# print(fileNames)
# データを読み込む
dfs = []
for fileName in fileNames:
    df = pd.read_csv(dirName + dataDir + fileName, header=0, encoding='utf-8')
    dfs.append(df)
dfs[0].head()

globの引数のrecursiveは再帰的にファイルを検索するかどうかを変更します.
recursive=Falseの状態では,/test/data/にある任意の CSV ファイルの絶対パスを返します.
df.head()は読み込んだ CSV ファイルの最初の 5行を表示します.
df.head(10)のように括弧内に自然数を入れることで指定した行数を表示することができます.
同様にdf.tail()は読み込んだ CSV ファイルの最後の 5行を表示します.

4. データの統計解析

変数の代入

この章では得られた DataFrame から必要な値を取り出して,統計処理を行います.
今回はモル濃度を$x$に,吸光度を$y$という変数に格納します.
データ解析では Python のリストは極力使わずに NumPy の Array を使用します.

変数の代入
x = df.iloc[:,0].values
y = df.iloc[:,1].values
columns = df.columns.values
# こちらでも可能
# x = df[columns[0]].values
# y = df[columns[1]].values
print(columns)

Pandas の DataFrame から NumPy の Array を取り出す方法はいくつかありますが,私は上記の2種類を使います.
前者は DataFrame を行列のように扱うdf.iloc[row,column]を用いて指定した行や列の情報を取り出します.
df.iloc[:,0]は 0列目の全ての要素を取得するという意味になります.
後者はdf.columns.valuesで取得した列のヘッダの名前から各列を取り出す処理になります.
わざわざdf.columns.valuesを使用しなくても構いませんが,その場合はdf['Absorbance[-]']のように正しい列名を入力しなければならないので面倒です.
列名は正しくデータが取得できているのか確かめる上でも重要ですので,取得したら print 文で確認してみましょう.
また.valuesは使用しないと,NumPy のndarrayというオブジェクトにならないので必ず付けてください.

1次関数を用いた単回帰分析

理論的な背景

今回は得られた吸光度のデータを単回帰分析により検量線を作成します.
単回帰分析の詳細な説明は専門書を読んでもらうとして,本記事では最小二乗法を用いた計算だけ示します.
まず,$n$個のデータ $(x_i, y_i)\ (1\leq i \leq n)$ があります.
このデータを $(1)\ y=ax$ か $(2)\ y=bx+c$ という2本の直線を引いたときに,各データとの残差が最小になるように係数 $a,b,c$ を決めます.

その時,各データとの残差を二乗和で示すと以下の通りになります.

\begin{align}
&(1)\quad f(a)=\sum^n_{i=1}(y_i-ax_i)^2,\\
&(2)\quad g(b,c)=\sum^n_{i=1}(y_i-bx_i-c)^2
\end{align}

まず,(1)について$a$で偏微分すると,

\begin{align}
\frac{\partial f(a)}{\partial a}&=\sum^n_{i=1}2(y_i-ax_i)\cdot(-x_i) \\
&=-2\sum^n_{i=1}x_iy_i + 2a\sum^n_{i=1}x_i^2 \\
&=0
\end{align}

となるので,

a=\frac{\displaystyle \sum^n_{i=1}x_iy_i}{\displaystyle \sum^n_{i=1}x_i^2}

が得られます.
Python の NumPy 配列では,a = np.dot(x, y)/np.dot(x, x)で得られます.
ここで,np.dot(x, y)はベクトルの内積を計算する関数です.
同様に(2)において,

\begin{align}
\frac{\partial g(b,c)}{\partial b}&=\sum^n_{i=1}2(y_i-bx_i-c)\cdot(-x_i) \\
&=-2\sum^n_{i=1}x_iy_i+2b\sum^n_{i=1}x_i^2+2c\sum^n_{i=1}x_i \\
&=0 \\
\frac{\partial g(b,c)}{\partial c}&=\sum^n_{i=1}2(y_i-bx_i-c)\cdot(-1) \\
&=-2\sum^n_{i=1}y_i + 2b\sum^n_{i=1}x_i + 2cn \\
&=0
\end{align}

となるので,

\begin{align}
c&=\frac{1}{n}\sum^n_{i=1}y_i -\frac{b}{n}\sum^n_{i=1}x_i \\
&=\overline{y}-b\overline{x}
\end{align}

となります.
ここで,$\overline{x}$は平均値を意味します.
これを$b$の式に代入すると,

\begin{align}
b\left[\sum^n_{i=1}x_i^2-\frac{1}{n}\left(\sum^n_{i=1}x_i\right)^2\right]&=\sum^n_{i=1}x_iy_i-\frac{1}{n}\left(\sum^n_{i=1}x_i\right)\left(\sum^n_{i=1}y_i\right) \\
\Leftrightarrow b&=\frac{\displaystyle \frac{1}{n}\sum^n_{i=1}x_iy_i-\frac{1}{n^2}\left(\sum^n_{i=1}x_i\right)\left(\sum^n_{i=1}y_i\right)}{\displaystyle \frac{1}{n}\sum^n_{i=1}x_i^2-\frac{1}{n^2}\left(\sum^n_{i=1}x_i\right)^2} \\
&=\frac{\mathrm{Cov}(x,y)}{\mathrm{Var}(x)}
\end{align}

となります.
ここで $\mathrm{Cov}(x,y)$ は$x$と$y$の(不偏)共分散,$\mathrm{Var}(x)$ は$x$の(不偏)分散を意味します.
Python の NumPy 配列では,b = np.cov(x,y)[0][1]/np.cov(x,y)[0][0], c = y.mean() - b*x.mean()で得られます.
ここで,np.cov(x,y)は2×2の共分散行列を返すため,$x$と$y$の不偏共分散を得る場合は(1,0)成分か(0,1)成分を,$x$の不偏分散を得る場合は(0,0)成分を指定します.

さて,単回帰直線の精度は決定係数という値から評価できます.
決定係数は

R^2=1-\frac{\mathrm{Var}(y-\hat{y})}{\mathrm{Var}(y)}

で表されます.
近似直線とサンプルが完全に一致している場合は残差平方和が0となるため,決定係数は1になります.

簡単な単回帰分析

さて,理論的なところのおさらいが済んだところで実装していきます.

単回帰分析(NumPy)
# y = ax
a = np.dot(x, y)/np.dot(x, x)
r2_a = 1 - (y - a*x).var()/y.var()
print(a, r2_a)
# y = bx + c
b = np.cov(x, y)[0][1]/np.cov(x, y)[0][0]
c = y.mean() - b*x.mean()
r2_bc = 1 - (y - b*x - c).var()/y.var()
print(b, c, r2_bc)

ここの$b$の計算で$x$の分散をnp.cov(x,y)[0][0]で計算している理由は母分散と不偏分散の違いに由来します.
x.var()では母分散を計算しますが,np.cov(x,y)[0][0]では不偏分散を計算しています.
x.var()で計算する場合は,x.var(ddof=1)というように引数で不偏分散であることを指定します.
NumPy の ndarray は要素の形が同じ場合,通常の変数と同様に和や差,スカラ倍の計算ができます.

さて,ここまで細々と NumPy を用いて説明しましたが,機械学習用のライブラリである Scikit-learn を用いると,何も考えずとも単回帰分析ができてしまいます.
以下にそのコードを示します.

単回帰分析(sklearn)
lr = LinearRegression()
# ベクトルを整形する
n = len(x)
X = x.reshape(n,1)
Y = y.reshape(n,1)
# 線形モデルにフィッティングする
lr.fit(X,Y)
# 結果の表示
print(lr.coef_[0][0], lr.intercept_[0], lr.score(X, Y))

おそらく NumPy の結果と同様の結果が得られたかと思います.
いちいち,数式を覚えるのは面倒だという人はオススメの方法です.

5. データの図示

各パーツの説明

Matplotlib は非常に大きいライブラリですので,私も全てを覚えているわけではありません.
そこで,Matplotlib の公式が公開しているチートシート(カンペ)を見ながら,作成しています.

その中で最優先で覚えなければならないことは Matplotlib で扱っている各パーツの名前を覚えることです.
以下に,公式サイト5の図を転載します.

Anatomy of a figure

丸で囲った部分が代表的なパーツ,太字がパーツの名前,その下の黒い字が表示や設定する関数を示しています.
この中で分かりにくいものが Figure と Axes です.
簡単に書くと,Figure は図全体を示しており,Axes は図に含まれる一つ一つのグラフを示します.
今回は簡単な図しか描かないので不要ですが,一つの Figure に複数のグラフを作成することが可能です.

コード全体について

それでは実際にグラフを描いてみます.
はじめにセル全体のコードをお見せします.

Figure全体
# Figure, Axesオブジェクトの取得
fig, ax = plt.subplots(figsize=FIGSIZE[0], nrows=1, ncols=1, layout='constrained')

# 散布図の設定
ax.scatter(x, y, s=20, marker='o', c='w', ec='k', label='raw data')

# 検量線の設定
# y = ax
# ax.plot([0, 130],[0, a*130], lw=1, c='k', label='calibration line')
# y = bx + c
ax.plot([0, 130],[c, b*130 + c], lw=1, c='k', label='calibration line')

# 軸スケールの設定('linear', 'log')
ax.set_xscale('linear')
ax.set_yscale('linear')

# 軸ラベルの設定
ax.set_xlabel('Concentration [mmol/L]')
ax.set_ylabel('Absorbance [-]')

# 表示範囲の設定
ax.set_xlim([0,130])
ax.set_ylim([0,1.3])

# 軸目盛りの設定
ax.xaxis.set_major_locator(ticker.MultipleLocator(25))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(5))
ax.yaxis.set_major_locator(ticker.MultipleLocator(0.2))
ax.yaxis.set_minor_locator(ticker.MultipleLocator(0.1))

# 凡例の追加
ax.legend(loc='lower right')

# テキストの追加
# y = ax
# s_a = fr"$y={b:.5f}x$"
# s_r2a = fr"$R^2={rc_bc:.4f}$"
# ax.text(x=20, y=1.1, s=s_a)
# ax.text(x=13, y=0.98, s=s_r2a)
# y = bx + c
s_bc = fr"$y={b:.5f}x+{c:.5f}$"
s_r2bc = fr"$R^2={r2_bc:.4f}$"
ax.text(x=20, y=1.1, s=s_bc)
ax.text(x=13, y=0.98, s=s_r2bc)

今回は 2通りの方法で検量線を引いたので,どちらも描けるようにコメントアウトしています.
それでは順に説明していきます.

FigureとAxes

Figure, Axesオブジェクトの取得
fig, ax = plt.subplots(figsize=FIGSIZE[0], nrows=1, ncols=1, layout='constrained')

Matplotlib には図を作成するのにいくつか方法があります.
最も簡単な方法はplt.plot()などの関数を使う方法です.
Python でグラフを描く際に初心者向けのサイトにはよく使われています.
しかしながら,図を何度も表示して微修正するような作業には適していません.
また,2本左右に y軸を描く場合や省略を必要とするようなグラフを描く場合には細かい設定が必要になります.
最初からplt.subplots()に慣れておけば,楽なので最初のうちに苦しんでおきましょう?
ただし,plt.subplots()にはplt.subplot()という複数形のsが無い関数もあるので,間違えないように気をつけてください.
さて,plt.subplots()には,いくつか引数があります.
nrows,ncolsはそれぞれ各行,各列におけるグラフの数を表します(デフォルトは 1です).
figsizeは正確にはplt.figure()という関数の引数です.
初期設定から変えない場合は特に必要ありませんが,グラフによって変えることもあるので再度指定しています.
layoutfigsizeと同様にplt.figure()の引数です.
無駄な余白を無くしたり,要素の重なりを修正したりとグラフの表示を変更するために指定します.
私は基本的にはlayout='constrained'を使用しています.
Python のバージョンが古いとエラーを出すので,その場合はconstraned_layout=Trueか,それでも対応していなかったらtight_layout=Trueを入力すれば指定できると思います.

散布図の追加

散布図の追加
ax.scatter(x, y, s=20, marker='o', c='w', ec='k', label='raw data')

今回はシンプルな図であるので引数の数をかなり減らしています.
今回のコードではマーカの大きさを20,マーカの種類は丸,色(c)は白(w),線の色は黒(k)に設定しています.
色や線幅などの要素はclwのように省略することも可能です.
ラベルは凡例を表示させるために必要なので,ここで記入します.

散布図の基本は点であるマーカのデザインになります.
以下に公式リファレンスからの関数の引数一覧を示します.

Axes.scatter
Axes.scatter(x, y, s=None, c=None, marker=None, \
cmap=None, norm=None, vmin=None, vmax=None, \
alpha=None, linewidths=None, *, edgecolors=None, \
plotnonfinite=False, data=None, **kwargs)

sはマーカのサイズ,cは色,markerはマーカの種類,alphaは透明度(0~1),linewidthは線幅,edgecolorsは線の色になります.
マーカの種類はかなりたくさんあるので,塗りつぶしの状態まで使い分けたら白黒でも十分に表現できます.
公式リファレンスに多く紹介されているので色々試してみてはいかがでしょうか.

Filled Markers

検量線の追加

検量線の追加
# y = ax
# ax.plot([0, 130],[0, a*130], lw=1, c='k', label='calibration line')
# y = bx + c
ax.plot([0, 130],[c, b*130 + c], lw=1, c='k', label='calibration line')

plotは折れ線グラフを描くための関数で,今回は 2点((0,c),(130,b*130+c))を指定して 2点間に直線を引いています.
線を引くだけではなく,マーカを設置することも可能です.
今回は,線幅と色のみを指定しています.
その他にはラベルを設定したり,線の種類を変更したり,色を変えたりできます.
線の種類(linestyle, ls)は簡単なものは4種類で-,--,-.,:で,それぞれ実線,破線,一点鎖線,点線を意味しています.
色は様々な種類のものを選ぶことができますが,基本的には黒を使い,必要に応じて増やします.
以下の公式リファレンスを見たり,チートシートを見ると良いでしょう.
私は'r','g','b','c','m','y','k','w''C0','C1',...'0.5'などを使用します.
3つ目の小数はグレースケールの濃さを表しています.
面倒ですが,お気に入りの色があれば,名前や RGB で指定することも可能です.

基本的には後に描いたものが上書きされるため,現在の設定では散布図のマーカの上に線が乗るように設定されています.
これを変更する場合には,scatter と plot の両方にzorderという引数を追加しましょう.
zorder の値が小さいほうが最初に描かれるため,奥側に設置されます.

軸スケールの設定

軸スケールの設定
ax.set_xscale('linear')
ax.set_yscale('linear')

軸スケールにはlinearlogsymloglogitの 4種類がありますが,基本的にはlinearlogしか使いません.

軸ラベルの設定

軸ラベルの設定
ax.set_xlabel('Concentration [mmol/L]')
ax.set_ylabel('Absorbance [-]')

各軸ラベルの設定をします.
引数は文字列となります.
学術論文では基本的な物理量や論文内で定義された記号を使用することもありますが,昨今では記号の仕様は極力避けて言葉で表すようにするのが一般的です.
もし,物理量を記号で表したい場合はraw文字列を使うと,$\LaTeX$表記で物理量を表示することができます.
raw文字列を使用する場合はr'', r""のように引用符の前にrを付けます.
現在の設定では Computer Modern で表示されるようになっているので,サンセリフ体で表示したい場合は rcParams などに修正が必要になります.
また,単位の表示方法についてはスラッシュ(/)で区切る派,丸括弧(())で括る派,角括弧([])で括る派などがあり,文献や雑誌によっても様々です.
基本的には雑誌によく使用されている形式を使うと良いと思います.

表示範囲の設定

表示範囲の設定
ax.set_xlim([0,130])
ax.set_ylim([0,1.3])

表示範囲の設定をします.
基本的には点や線が十分に見える範囲をリストやタプルで指定します.
コメントアウトして設定しない場合は自動的に表示範囲が設定されます.
後述する目盛りや凡例の大きさ,位置を微調整する際に変更することがあるので,手動で設定することを推奨します.

軸目盛りの設定

軸目盛りの設定
ax.xaxis.set_major_locator(ticker.MultipleLocator(25))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(5))
ax.yaxis.set_major_locator(ticker.MultipleLocator(0.2))
ax.yaxis.set_minor_locator(ticker.MultipleLocator(0.1))

軸目盛りの設定はそこそこ大変です.
まず,軸には太い主目盛り(major tick)と細い補助目盛り(minor tick)を別々に設定します.
また,目盛りは位置を決めるLocatorと目盛りの表示を決めるFormatterに分かれてそれぞれ設定します.
最も簡単な目盛りの設定は上記のコードにあるaxes.(x/y)axis.set_(major/minor)_locator()を使用する方法です.
この関数の引数はticker.MultipleLocator()というオブジェクトとなります.
この関数の引数に値を与えると,最小値からその値の増分に目盛りが追加されるという仕様です.
今回はFormatterの設定はしませんが,例えば,log の目盛りで有効数字を揃える,$n$%で表記するといった設定をする場合は,使用します.
機会があれば,このあたりも詳しく解説したいですね.

凡例の追加

凡例の追加
ax.legend(loc='lower right')

凡例の設定は実は最も大変な作業になります.
なぜなら,限られた領域に線やマーカの情報を適切に示さなければならないからです.
しかしながら,今回はデータは一つしか無いため,大雑把な場所だけ指定しています.
locという引数はbest(ベストな場所,いやどこだよ)の他に左,中央,右,上,下を組み合わせた 9箇所の場所を指定できます.
今回はデータが左下から右上に伸びる構図で,左上に数式を設定する予定なので,凡例は右下に設置しました.
グラフ内部での凡例の設置だけではなく,グラフ外部への設置も可能です.
しかしながら,その場合はbbox_to_anchorという引数を延々と微修正する作業が待っています.
こちらも機会があれば詳しく解説したいです.

テキストの追加

テキストの追加
# y = ax
# s_a = fr"$y={b:.5f}x$"
# s_r2a = fr"$R^2={rc_bc:.4f}$"
# ax.text(x=20, y=1, s=s_a)
# ax.text(x=13, y=0.88, s=s_r2a)
# y = bx + c
s_bc = fr"$y={b:.5f}x+{c:.5f}$"
s_r2bc = fr"$R^2={r2_bc:.4f}$"
ax.text(x=20, y=1.1, s=s_bc)
ax.text(x=13, y=0.98, s=s_r2bc)

グラフに必須ではありませんが,テキストの追加します.
今回は単回帰分析で求めた直線の傾きと切片,そして決定係数を表示します.
私は普段,統計解析を行った後の結果を元に,有意差があるデータにアスタリスクを付けたり,直線を引いたりしています.
さて,コメントアウトしていない部分の最初の 2行はformat文raw文を組み合わせた文字列を作成しています.
format文とは変数を指定した方法で入力する文章になります.
詳しい解説は省きますが,今回の文では{b:.5f}という部分で$b$の値を小数点第5位まで表示することを指定しています.
ちなみに各テキストの$x$座標と$y$座標の値は微調整して値を載せています(結構面倒です).

6. Figureの保存

Figureの保存
fig.savefig(dirName + 'test' + '.tiff', dpi=600)
# fig.savefig(dirName + 'test' + '.png', transparent=True, dpi=600)
# fig.savefig(dirName + 'test' + '.jpg', dpi=600)
# fig.savefig(dirName + 'test' + '.svg')

最後に作成したFigureを保存します.
保存にはfig.savefig()という関数を使用します.
作業ディレクトリにtest.tiffという名前のファイルを保存します.
今回はグレーも使用しているので 600 dpiで保存します(1200 dpiでも大丈夫です).
PNG ファイルは白背景部分を透明にすることができるフォーマットなので,transparentという引数があります.
必要なファイル形式に合わせて適宜変えてください.

7. おわりに

初めてQiitaで技術記事を書いてみました.
私が使い始めた頃と比べて,Matplotlib を詳説するサイト6も増えていて,正直,私の記事なんていらないのではないかと思いましたが,何とか書き上げることができました.
近年の私の分野では著名な雑誌であっても,表計算ソフトで書いたような雑なグラフや華美なグラフを目にすることが増え,グラフの体裁を意識しない研究者が増えていることは嘆かわしいと感じています.
本記事の内容は学術論文とビジネスに使用される見栄え(だけ)が良いグラフは全く異なることを意識してもらえるようにプログラミング以外の要素の解説を多くしています.
なるべく専門的な用語を使わないように配慮はしましたが,Linux と Python の基本的な事柄に関しては断りなく使用しています.
また,本記事で紹介しきれなかった細かい引数の設定が非常に重要であり,記事の内容だけ模倣しても上手く表示できないこともあるかと思われます.
もし,本記事を読んで勉強になったと思った方は感想や評価をQiitaやXでくださると励みになります.

ご愛読いただきありがとうございました.
皆様も良き論文執筆ライフを!

8. 編集履歴

2023年12月6日 公開
2023年12月7日 一部修正
2023年12月8日 数式の誤りを修正

  1. Matplotlib 公式 2 3

  2. ACS Preparing Manuscript Graphics

  3. 釜谷美則, 吸光光度法, ぶんせき 2008, 4, 158–162.

  4. Pandas 公式 read_csv

  5. Matplotlib Quick Start Guide

  6. Matplotlibの機能を丁寧に説明しているサイト

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?