Numpy

Numpy以外のコンテナ

Python では,値の集合を「コンテナ」と呼び,代表的なものとして

  • リスト(List): [1, 'two', [3], 3.14]
    • タプル(Tuple): (1, 'two', [3], 3.14)
    • 文字列(Str): 'aa83988848'
  • 集合(Set): {1, 'two', [3], 3.14}
    • 辞書(Dict): {'v1':1, 'v2':'two', 'v3':[3], 'v4':3.14}

があります.コンテナは,基本的に各要素が異なる型(クラス)でも構いません.要素がさらにコンテナであっても構いません.

MyList = [1, 'two', [3], 3.14]
MyTuple = (1, 'two', [3], 3.14)
MyStr = 'aa83988848'
MySet = {1, 'two', 3.14} # setはコンテナを要素に持てない
MyDict = {'v1':1, 'v2':'two', 'v3':[3], 'v4':3.14}

print('==MyList==')
print(f'{MyList}\n')

print('==MyTuple==')
print(f'{MyTuple}\n')

print('==MyStr==')
print(f'{MyStr}\n')

print('==MySet==')
print(f'{MySet}\n')

print('==MyDict==')
print(f'{MyDict}\n')
==MyList==
[1, 'two', [3], 3.14]

==MyTuple==
(1, 'two', [3], 3.14)

==MyStr==
aa83988848

==MySet==
{'two', 1, 3.14}

==MyDict==
{'v1': 1, 'v2': 'two', 'v3': [3], 'v4': 3.14}

Numpy以外のコンテナに対する演算

これらのコンテナに対して演算をすると

print("==MyList * 3==")
print(f"{MyList * 3}\n")

print("==MyTuple * 3==")
print(f"{MyTuple * 3}\n")

print("==MyStr * 3==")
print(f"{MyStr * 3}\n")

# print('==MySet * 3==')
# print(f'{MySet * 3}\n') # Setは演算できない

# print('==MyDict * 3==')
# print(f'{MyDict * 3}'\n) # Dictは演算できない

MyList2 = [1, 2, 3]
print("==MyList + MyList2==")
print(f"{MyList + MyList2}\n")
==MyList * 3==
[1, 'two', [3], 3.14, 1, 'two', [3], 3.14, 1, 'two', [3], 3.14]

==MyTuple * 3==
(1, 'two', [3], 3.14, 1, 'two', [3], 3.14, 1, 'two', [3], 3.14)

==MyStr * 3==
aa83988848aa83988848aa83988848

==MyList + MyList2==
[1, 'two', [3], 3.14, 1, 2, 3]

というように,後ろに繰り返し要素を付け加えることになります.

numpy.NDarray はベクトル演算

それぞれの要素に対し演算するには,どうしたらいいでしょうか.そのような演算をベクトル演算といいます.

ベクトル演算をするためのコンテナを Numpy NDArray といいます.

NumPy というパッケージに NumPy NDArray クラスが定義されています.

import numpy as np

MyList = [2, 3, 5,7]
print('==MyList==')
print(f'{MyList}\n')

print('==MyList * 3==')
print(f'{MyList * 3}\n')

MyArray = np.array(MyList) # <-- Numpy NDArray
print('==MyArray==')
print(f'{MyArray}\n')

print('==MyArray * 3==')
print(f'{MyArray * 3}\n')
==MyList==
[2, 3, 5, 7]

==MyList * 3==
[2, 3, 5, 7, 2, 3, 5, 7, 2, 3, 5, 7]

==MyArray==
[2 3 5 7]

==MyArray * 3==
[ 6  9 15 21]

要素数の異なる NumpyArray との加算はできません.

MyArray2 = np.array(MyList2)
print(f"{MyArray + MyArray2}")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 2
      1 MyArray2 = np.array(MyList2)
----> 2 print(f"{MyArray + MyArray2}")

ValueError: operands could not be broadcast together with shapes (4,) (3,) 

NumpyArray は,線形代数における行列やベクトルなので,行列やベクトルに対する演算しか許されないというわけです.

numpy.NDArray は本当はテンソル演算

NumpyArray は,行列やベクトル」と言いましたが,行列やベクトルには形があります.

\[ \pmb{M} = \begin{bmatrix} 1 & 2 & 3\\ 4 & 5 & 6 \end{bmatrix} \]

のとき,\(\pmb{M}\) の形は「2 行 3 列」です.

\[ \pmb{v} = \begin{bmatrix} 7\\ 8\\ 9 \end{bmatrix} \]

のとき,\(\pmb{v}\) の形は「3 行 1 列」ですね.

\[ \pmb{u} = \begin{bmatrix} 10 & 11\end{bmatrix} \]

のとき,\(\pmb{u}\) の形は「1 行 2 列」ですね.

NumpyArray で 2 行 3 列の行列を作ってみましょう. すべての要素がゼロな 2 行 3 列の行列は,NumPy の zeros 関数で作ることができます.

M = np.zeros((2,3))
print(f'{M = }')
M = array([[0., 0., 0.],
       [0., 0., 0.]])

行列の形は 属性.shapeに保存されています.

print(f'{M.shape = }')
M.shape = (2, 3)

(2,3)Mprintしたときの表示を見てみると,「行ごとの 3 つの要素がリストになっており,2 行のリストをさらにリストに」しています.

実は,このような リストから NumpyArray を作る こともできます.

u1 = [1,2,3]
u2 = [4,5,6]
L = [u1, u2]
M = np.array(L)
print(f'{M = }')
print(f'{M.shape = }')
M = array([[1, 2, 3],
       [4, 5, 6]])
M.shape = (2, 3)

では,7 と 8 と 9 を要素にもつ 1 行 3 列の NumpyArray を作ってみましょう.行ごとにリストに,全体をさらにリストにします.つまり 2 重リストになります.

Lx = [[7,8,9]]
x = np.array(Lx)
print(f'{x = }')
print(f'{x.shape = }')
x = array([[7, 8, 9]])
x.shape = (1, 3)

この x を転置した \(\pmb{x^{\prime}}\) ならば,\(\pmb{M}\pmb{x^{\prime}}\) が計算できます.転置は Numpy の transpose 関数を使います.また,行列積は Numpy の matmul 関数です.(ちなみに 演算子 * は,要素積(要素同士の積) です.)

x_prime = np.transpose(x)
# `x_prime = x.T` でもよい.
R = np.matmul(M, x_prime)
print(f"{R = }")
print(f"{R.shape = }")
R = array([[ 50],
       [122]])
R.shape = (2, 1)

皆が知っているように,行列積は,「左の行列の列数」と「右の行列の行数」が同じでなければいけません.上の例の場合,shape(2,3)(3,1) なので行列積の shape(2,1) となるわけですね.

ところで,

print(f'{x = }')
x = array([[7, 8, 9]])

この x は行列でしょうか?ベクトルでしょうか?

行ベクトルとか列ベクトルと言われているものですが,Numpy では 「行数」とか「列数」とか言っているので,これは「行列」なのです.shapeが 2元タプルになっています.

先の x が行列ならば,ベクトルはどんなもの?

ベクトルとは,1 重のリストから作ったもの をいいます. 「行」や「列」という概念がありません.

Ly = [7,8,9]
y = np.array(Ly)
print(f'{y = }')
print(f'{y.shape = }')
y = array([7, 8, 9])
y.shape = (3,)

さきほどの行ベクトル(\(\in\) 行列) xshape(1,3) で,転置した np.transpose(x)(3,1) で 列ベクトルでした. 今度のベクトル yshape(3,) です.2元目がヌル(空)で,1元タプルです.

ちなみに,行や列の概念が無いので転置は無意味で,形を変えません.

print(f"{y.T = }")
print(f"{y.T.shape = }")
y.T = array([7, 8, 9])
y.T.shape = (3,)

さきほどの行列積ができます.ベクトルは行列に右から掛けます.

R2 = np.matmul(M, y)
print(f'{R2 = }')
print(f'{R2.shape = }')
R2 = array([ 50, 122])
R2.shape = (2,)

R2 はさきほどの R と要素は同じ数値ですが,shape(2,1) ではなく (2,) となります.(行列積の作られ方どおりです.)

ちなみに,内積というのもありました.\(a\)\(b\) も行列ならば \(a^\top b\) で計算されるので,np.matmul(a.T, b) です.

ところで Numpy にも内積は用意されていて, np.inner(a,b) です.しかし,ab が行列のとき,\(a^{\top}b\) にはなりません. 本来の内積の定義は,「内側の積和」であり,\(\sum_{i=1}^{N} \pmb{a_i} \pmb{b_i}\) という演算です(\(N\)\(R-1\) 階の次数.\(R\) は階数)

vc1 = np.array([[1], [2]])
v1 = np.array([1, 2])
vc2 = np.array([[3], [4]])
v2 = np.array([3, 4])
inner1 = np.inner(vc1, vc2)
print(f"{inner1 = }")
inner2 = np.matmul(vc1.T, vc2)
print(f"{inner2 = }")
inner3 = np.inner(v1, v2)
print(f"{inner3 = }")
inner1 = array([[3, 4],
       [6, 8]])
inner2 = array([[11]])
inner3 = 11

まとめると

  • 行列: 2 重のリストから作られる.形は (S1, S2),行数 S1, 列数 S2

  • ベクトル: 1 重のリストから作られる.形は (S1,), 要素数 S1

  • 行列積は,np.matmul 関数で,

    • 左の Array の形が(S1,S2)で,右の Array の形が(S3,S4)のとき,S2==S3 なら演算できて,答えの Array の形は(S1,S4)
  • 転置は np.transpose 関数で,

    • 形が(S1,S2)の行列の転置は,形が(S2,S1)となる
    • 形が(S1,)のベクトルの転置は,形が変わらず(S1,)となる
print(y.shape)
yt = np.transpose(y)
print(yt.shape)
(3,)
(3,)

テンソル

行列やベクトルをまとめて,一般的に「テンソル」と言います.

さきほどの「n 重のリスト」の n を階数といいます.

  • 行列: 2 階テンソル
  • ベクトル: 1 階テンソル

ここで,1 重リストを並べたものをさらにリストにすれば,2 重リストになるので,すなわち(S1,)ベクトル(1 階テンソル)を S2 個並べると(S2,S1)行列(2 階テンソル)になるということです. 要素が(S1,)ベクトルである(S2,)ベクトルは,(S2,S1)という行列になります.

属性.ndimに保存されています.属性.shapeに保存されている「形」を表すタプルの要素数でもあります.

A1 = np.array([1,2,3,4])
print(A1)
print(f'shape(形):{A1.shape}')
print(f'ndim(階数):{A1.ndim}')
print(f'size(要素数):{A1.size}')
print('\n')

A2 = np.array([[1,2,3,4]])
print(A2)
print(f'shape(形):{A2.shape}')
print(f'ndim(階数):{A2.ndim}')
print(f'size(要素数):{A2.size}')
print('\n')

A3 = np.array([[[1,2,3,4]]])
print(A2)
print(f'shape(形):{A3.shape}')
print(f'ndim(階数):{A3.ndim}')
print(f'size(要素数):{A3.size}')
print('\n')
[1 2 3 4]
shape(形):(4,)
ndim(階数):1
size(要素数):4


[[1 2 3 4]]
shape(形):(1, 4)
ndim(階数):2
size(要素数):4


[[1 2 3 4]]
shape(形):(1, 1, 4)
ndim(階数):3
size(要素数):4

3 階以上になっても行列積(テンソル積)は同じです.

T3 = np.array([[[1,2,3,4]]])
T1 = np.array([5,6,7,8])
Result = np.matmul(T3,T1)
print(f'{T3.ndim}階テンソル(形{T3.shape})と{T1.ndim}階テンソル(形{T1.shape})の行列積は{Result.ndim}階テンソル(形{Result.shape})')
print(Result)
3階テンソル(形(1, 1, 4))と1階テンソル(形(4,))の行列積は2階テンソル(形(1, 1))
[[70]]

抽出

M = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]])
print(M,type(M),M.shape,M.ndim,M.size,'\n')

M_row = M[1] # 行
print(M_row,type(M_row),M_row.shape,M_row.ndim,M_row.size,'\n')

M_item = M[1][2] # (1,2)要素 M[1,2]とは書けない
print(M_item,type(M_item),M_item.shape,M_item.ndim,M_item.size,'\n')

M_slice = M[1:3, 0:2] #スライス
print(M_slice,type(M_slice),M_slice.shape,M_slice.ndim,M_slice.size,'\n')

M_column = M[:, 1] # 列抽出はスライスで
print(M_column,type(M_column),M_column.shape,M_column.ndim,M_column.size,'\n')

M_sub = M[[0,2],[1,3]] # (0,1)要素と(2,3)要素 (スライス)
print(M_sub,type(M_sub),M_sub.shape,M_sub.ndim,M_sub.size)
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]] <class 'numpy.ndarray'> (3, 4) 2 12 

[5 6 7 8] <class 'numpy.ndarray'> (4,) 1 4 

7 <class 'numpy.int64'> () 0 1 

[[ 5  6]
 [ 9 10]] <class 'numpy.ndarray'> (2, 2) 2 4 

[ 2  6 10] <class 'numpy.ndarray'> (3,) 1 3 

[ 2 12] <class 'numpy.ndarray'> (2,) 1 2

追加

u1 = np.array([[1,2],[3,4]])
u2 = np.array([[11,12],[13,14]])

print('==np.concat: 階数を変えずに,連結する.==')
M1 = np.concatenate([u1,u2])
print(M1)
print(f'形:{M1.shape},階:{M1.ndim},要素数:{M1.size}\n')
M2 = np.concatenate([u1,u2],axis=1)
print(M2)
print(f'形:{M2.shape},階:{M2.ndim},要素数:{M2.size}\n')

print('==np.stack: 指定した階に,積む==')
M3 = np.stack([u1,u2],axis=0)
print(M3)
print(f'形:{M3.shape},階:{M3.ndim},要素数:{M3.size}\n')

M4 = np.stack([u1,u2],axis=1)
print(M4)
print(f'形:{M4.shape},階:{M4.ndim},要素数:{M4.size}\n')

M5 = np.stack([u1,u2],axis=2)
print(M5)
print(f'形:{M5.shape},階:{M5.ndim},要素数:{M5.size}\n')
==np.concat: 階数を変えずに,連結する.==
[[ 1  2]
 [ 3  4]
 [11 12]
 [13 14]]
形:(4, 2),階:2,要素数:8

[[ 1  2 11 12]
 [ 3  4 13 14]]
形:(2, 4),階:2,要素数:8

==np.stack: 指定した階に,積む==
[[[ 1  2]
  [ 3  4]]

 [[11 12]
  [13 14]]]
形:(2, 2, 2),階:3,要素数:8

[[[ 1  2]
  [11 12]]

 [[ 3  4]
  [13 14]]]
形:(2, 2, 2),階:3,要素数:8

[[[ 1 11]
  [ 2 12]]

 [[ 3 13]
  [ 4 14]]]
形:(2, 2, 2),階:3,要素数:8

形を変形

T = np.array([[1,2,3,4],[5,6,7,8]])
print(T)
print(f'形:{T.shape}, 階:{T.ndim}, 要素数:{T.size}\n')

T2 = np.reshape(T,(1,8))
print(T2)
print('前から順に8要素のリストを1個')
print(f'形:{T2.shape}, 階:{T2.ndim}, 要素数:{T2.size}\n')

T3 = np.reshape(T,(8,1))
print(T3)
print('前から順に1要素のリストを8個')
print(f'形:{T3.shape}, 階:{T3.ndim}, 要素数:{T3.size}\n')


T4 = np.reshape(T,(4,2))
print(T4)
print('前から順に2要素のリストを4個')
print(f'形:{T4.shape}, 階:{T4.ndim}, 要素数:{T4.size}\n')

T5 = np.reshape(T,(8,))
print(T5)
print('前から順に8要素')
print(f'形:{T5.shape}, 階:{T5.ndim}, 要素数:{T5.size}\n')

T6 = np.reshape(T,(1,2,4))
print(T6)
print('前から順に4要素2個の要素を1個')
print(f'形:{T6.shape}, 階:{T6.ndim}, 要素数:{T6.size}\n')
[[1 2 3 4]
 [5 6 7 8]]
形:(2, 4), 階:2, 要素数:8

[[1 2 3 4 5 6 7 8]]
前から順に8要素のリストを1個
形:(1, 8), 階:2, 要素数:8

[[1]
 [2]
 [3]
 [4]
 [5]
 [6]
 [7]
 [8]]
前から順に1要素のリストを8個
形:(8, 1), 階:2, 要素数:8

[[1 2]
 [3 4]
 [5 6]
 [7 8]]
前から順に2要素のリストを4個
形:(4, 2), 階:2, 要素数:8

[1 2 3 4 5 6 7 8]
前から順に8要素
形:(8,), 階:1, 要素数:8

[[[1 2 3 4]
  [5 6 7 8]]]
前から順に4要素2個の要素を1個
形:(1, 2, 4), 階:3, 要素数:8

pandas のデータフレーム

行列といえば,データフレームと見た目が似ています.

  • どちらも,行の要素数が各列で同じ,列の要素数が各行で同じでなければならない.(普通の List,Tuple,Set,Dict では違う)

ということで,

  • 2 階テンソル(行列)の NumpyArray は pandas データフレームに変換できます.: pandas のDataFrame関数
  • pandas データフレームは,2 階の NumpyArray(行列)に変換できます.: pandas の DataFrame クラスのto_numpyメソッド

NumpyArray の行が,pandas の 1 行のサンプルに対応しています.

import pandas as pd

M = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
display(M)


columnsM = ["v0", "v1", "v2", "v3"]
indexM = ["#1", "#2", "#3"]
df = pd.DataFrame(M, columns=columnsM, index=indexM)
display(df)

# dataframe to numpy ndarray
M2 = df.to_numpy()
display(M2)
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])
v0 v1 v2 v3
#1 1 2 3 4
#2 5 6 7 8
#3 9 10 11 12
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])

pandas.read_html によるスクレイピング

ホームページや画像などからデータフレームを読み込む操作を「スクレイピング」と呼びます.

例えば,長崎の気温変化をデータフレームに読み込んでみましょう.

長崎の気温変化は,Linkにあります.

このページには,文章の中に,表がありますね.

pandas のread_html関数は URL を指定すると,その URL のページに含まれる表の数だけデータフレームを作って,そのリストを返します. (html/xml を解釈する外部パッケージ lxml のインストールが必要です.)

python コードによるスクレイピングは,機械的な自動操作なのでホームページ作成者から敬遠される傾向があります. もし python コードから https 経由でファイルを読み込めない場合は,普段使っているウェブブラウザで手動で読み込み,ページをstats_nagasaki.htmlなどと名前をつけて保存し, それを読み込むように変更してください.

import lxml
from datetime import datetime

URL = "https://www.data.jma.go.jp/obd/stats/etrn/view/monthly_s3.php?prec_no=84&block_no=47817&year=&month=&day=&view=a1"
# URL = "stats_nagasaki.html"
df_List = pd.read_html(URL, header=0)
df = df_List[0]
df
1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月 年の値
0 1878 NaN NaN NaN NaN NaN NaN 26.0 26.6 24.9 19.2 12.1 6.9 19.3 ]
1 1879 5.7 7.7 8.8 14.2 18.6 21.9 26.6 27.6 23.3 17.2 12.1 8.4 16.0
2 1880 4.8 8.3 9.9 14.1 19.4 21.2 25.4 25.8 23.7 18.8 11.7 5.3 15.7
3 1881 4.0 5.9 7.5 14.1 18.8 22.3 26.3 27.8 24.6 18.2 13.6 7.9 15.9
4 1882 7.3 6.7 8.8 14.7 17.9 20.6 25.0 26.4 23.0 19.4 12.3 6.8 15.7
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
143 2021 7.4 10.3 13.9 16.7 19.8 23.7 27.9 27.3 25.7 21.1 14.1 9.2 18.1
144 2022 7.1 6.5 12.9 16.4 20.0 23.9 27.8 28.6 25.9 19.9 16.5 7.8 17.8
145 2023 7.4 9.2 13.6 16.9 19.7 23.7 28.0 29.2 26.7 19.8 15.1 10.2 18.3
146 2024 8.4 10.6 12.0 17.7 20.0 23.2 28.4 29.8 28.6 22.6 16.5 9.0 18.9
147 2025 6.9 5.8 12.1 16.1 19.4 21.1 ] NaN NaN NaN NaN NaN NaN 12.1 ]

148 rows × 14 columns

「年の値」の列は,その年の平均気温です.今回は使わないので消します.

各月の値は,すべて同じ尺度・分布になっていて,いわゆるwide形式 なので,メルトでlong形式に直します. 列「年」を固定して,その他の列について,その列名を変数に,値を変数の値に整形します. - id_vars = ["年"] - var_name = '月' # 元の列名を 列「月」の値にする. -value_name = ‘気温’ # 元の列の値を 列「気温」の値にする.

df = df.drop(columns='年の値')
df2 = df.melt(id_vars=["年"], var_name='月', value_name='気温')  # pd.melt() : 先週やった pivotの逆操作
df2
気温
0 1878 1月 NaN
1 1879 1月 5.7
2 1880 1月 4.8
3 1881 1月 4.0
4 1882 1月 7.3
... ... ... ...
1771 2021 12月 9.2
1772 2022 12月 7.8
1773 2023 12月 10.2
1774 2024 12月 9.0
1775 2025 12月 NaN

1776 rows × 3 columns

列「年」の値(例えば「1920」)と 列「月」を値(例えば「4月」)を合成すると,1つの列「年月」の値としてまとめることができます.

df2["年月"] = df2["年"].astype(str) + "年" + df2["月"].astype(str)
df2
気温 年月
0 1878 1月 NaN 1878年1月
1 1879 1月 5.7 1879年1月
2 1880 1月 4.8 1880年1月
3 1881 1月 4.0 1881年1月
4 1882 1月 7.3 1882年1月
... ... ... ... ...
1771 2021 12月 9.2 2021年12月
1772 2022 12月 7.8 2022年12月
1773 2023 12月 10.2 2023年12月
1774 2024 12月 9.0 2024年12月
1775 2025 12月 NaN 2025年12月

1776 rows × 4 columns

列「年月」は,時刻データとして扱うようにします.

df2['Date'] = [datetime.strptime(s,'%Y年%m月') for s in df2['年月']]
df2
気温 年月 Date
0 1878 1月 NaN 1878年1月 1878-01-01
1 1879 1月 5.7 1879年1月 1879-01-01
2 1880 1月 4.8 1880年1月 1880-01-01
3 1881 1月 4.0 1881年1月 1881-01-01
4 1882 1月 7.3 1882年1月 1882-01-01
... ... ... ... ... ...
1771 2021 12月 9.2 2021年12月 2021-12-01
1772 2022 12月 7.8 2022年12月 2022-12-01
1773 2023 12月 10.2 2023年12月 2023-12-01
1774 2024 12月 9.0 2024年12月 2024-12-01
1775 2025 12月 NaN 2025年12月 2025-12-01

1776 rows × 5 columns

整形前の列は,もう不要なので消します.

df3 = df2.drop(columns=['年','月','年月'])
df3
気温 Date
0 NaN 1878-01-01
1 5.7 1879-01-01
2 4.8 1880-01-01
3 4.0 1881-01-01
4 7.3 1882-01-01
... ... ...
1771 9.2 2021-12-01
1772 7.8 2022-12-01
1773 10.2 2023-12-01
1774 9.0 2024-12-01
1775 NaN 2025-12-01

1776 rows × 2 columns

列「気温」の値は間隔尺度なので,floatにしたいのですが, 数字以外の記号が含まれているので,まずその記号部分を消します.

df3['気温'] = df3['気温'].astype(str)
df3['気温'] = df3['気温'].str.extract('([ 0-9.]+)') # 「0」~「9」,「.」で構成される文字列(すなわち気温の数値)を取り出す.
df3["気温"] = df3["気温"].astype(float)
df3
気温 Date
0 NaN 1878-01-01
1 5.7 1879-01-01
2 4.8 1880-01-01
3 4.0 1881-01-01
4 7.3 1882-01-01
... ... ...
1771 9.2 2021-12-01
1772 7.8 2022-12-01
1773 10.2 2023-12-01
1774 9.0 2024-12-01
1775 NaN 2025-12-01

1776 rows × 2 columns

NaN は,Not-A-Number(非数)を表します.欠損値ですので,気温がNaNの行は測定されていないことを表します.このような行を消します.

df4 = df3.dropna()
df4
気温 Date
1 5.7 1879-01-01
2 4.8 1880-01-01
3 4.0 1881-01-01
4 7.3 1882-01-01
5 5.8 1883-01-01
... ... ...
1770 8.5 2020-12-01
1771 9.2 2021-12-01
1772 7.8 2022-12-01
1773 10.2 2023-12-01
1774 9.0 2024-12-01

1763 rows × 2 columns

Date列をインデックスにして昇順に並べ直します.

df5 = df4.set_index("Date")
df5 = df5.sort_index()
df5 = df5.reset_index('Date')
df5
Date 気温
0 1878-07-01 26.0
1 1878-08-01 26.6
2 1878-09-01 24.9
3 1878-10-01 19.2
4 1878-11-01 12.1
... ... ...
1758 2025-02-01 5.8
1759 2025-03-01 12.1
1760 2025-04-01 16.1
1761 2025-05-01 19.4
1762 2025-06-01 21.1

1763 rows × 2 columns

matplotlib

Matplotlib は,入力した系列に対応してグラフを描くライブラリです.

下のようなグラフを描きます.このグラフに書いてある用語(英単語)をそのまま覚えてください. 「図」の解剖

  • Figure:フィギュア:所謂,キャンバスのことです.Figure に描かれるのは,

    • 複数のグラフ(Axes)
    • キャンバスのタイトル,凡例
  • Axes:アクセス:所謂,グラフのことです.Axes に描かれるのは,

    • 軸線(Axis)
    • 軸ラベル(xlabel, ylabel)
    • プロット(Plot, データを使って書かれた線や点)
  • Axis:アクシス:所謂,軸線のことです.Axis 上に描かれるのは,

    • 目盛印(Tick,ティック):目盛として軸に刻みで描かれる細い線
    • 目盛文字列(Tick Label):目盛の刻みに振られる文字列(数直線ならば数字.数字でなくてもいい)

グラフを描くには,

  1. まず,キャンバスを用意する
from matplotlib import pyplot as plt

fig = plt.figure(figsize=(10, 5), tight_layout=True)
fig
<Figure size 960x480 with 0 Axes>
<Figure size 960x480 with 0 Axes>
  1. グラフ領域を用意する: fig.add_subplot(3,2,4)

    例) キャンバスを 3 行 2 列の行列状に領域を分けたときの左上から 4 番目の領域

# グラフ領域を用意
# 例えばキャンバスを 2x3 に分割し,左上から右下に横方向に数えて,1番目と5番目に作成
ax1 = fig.add_subplot(2,3,1)
ax5 = fig.add_subplot(2,3,5)
fig

  1. グラフを描く

    2.1. プロットを描く

    ```
    ax1.plot(x座標の系列, y座標の系列)
    ```
# 2024年の全ての月を抽出 `df[]` の中は,target_yearの1月1日以上,かつ,
# target_yearの翌年1月1日より小さい を満たすような日付を
# df5の列["Date"]から探したときの,その行番号のリストとなります.
target_year = 2024
df6 = df5[
    (datetime(target_year, 1, 1) <= df5["Date"])
    & (df5["Date"] < datetime(target_year + 1, 1, 1))
]
# 内包表記.
# df6の列["Date"]の値の全てをリストとして,その要素をfor文でいちいち抽出するという操作でリストを作る.
month = [dt.month for dt in df6["Date"]]
ax1.plot(month, df6["気温"])
fig

2.2. 軸ラベル,タイトル,凡例を補正する

    ```
    ax1.set_xlabel('横軸ラベル')
    ```
ax1.set_xlabel("Month")
ax1.set_ylabel("temperature")
ax1.set_title(f"Nagasaki {target_year}")
fig

2.3. グリッド線をつけたり,表示領域を決めたり
    ```
    ax1.set_xlim()
    ax1.grid()
    ```
ax1.set_ylim([0,35])
ax1.set_xlim([1,12])
ax1.grid()
fig

キャンバスの別の領域に別のグラフを書く

target_year2 = 1879
df7 = df5[
    (datetime(target_year2, 1, 1) <= df5["Date"])
    & (df5["Date"] < datetime(target_year2 + 1, 1, 1))
]
month2 = [dt.month for dt in df7["Date"]]
ax5.plot(month2, df7["気温"])
ax5.set_xlabel('Month')
ax5.set_ylabel('temperature')
ax5.set_title(f'Nagasaki {target_year2}')
ax5.set_ylim([0,35])
ax5.set_xlim([1,12])
ax5.grid()
fig

凝ったグラフを 3番目 (キャンバスの (2,1)要素)に追加してみましょう.

ax3 = fig.add_subplot(2, 3, 3)
target_year3 = 1945
df8 = df5[
    (datetime(target_year3, 1, 1) <= df5["Date"])
    & (df5["Date"] < datetime(target_year3 + 1, 1, 1))
]
month3 = [dt.month for dt in df8["Date"]]
ax3.plot(month, df6["気温"], "rd-", label=f"{target_year}")
ax3.plot(month2, df7["気温"], "b*:", label=f"{target_year2}")
ax3.plot(month3, df8["気温"], "g^:", label=f"{target_year3}")
ax3.set_xlabel("Month")
ax3.set_ylabel("temperature")
ax3.set_title(f"Nagasaki {target_year}, {target_year2} and {target_year3}")
ax3.set_ylim([0, 35])
ax3.set_xlim([1, 12])
ax3.legend(loc="best") # 判例の位置をベストポジションに
ax3.grid()
fig

箱ひげ図を書くこともできる.: boxplot

  • 箱ひげ図を置く横軸目盛をpositionsで指定する
ax6 = fig.add_subplot(2, 3, 6)
ax6.boxplot(df6["気温"], positions=[1])
ax6.boxplot(df8["気温"], positions=[2])
ax6.boxplot(df7["気温"], positions=[3])
ax6.set_xlabel("Year")
ax6.set_ylabel("temperature")
ax6.set_title("boxcar plot")
ax6.set_ylim([0, 35])
ax6.set_xlim([0, 4])
ax6.set_xticklabels([f"{target_year}", f"{target_year3}", f"{target_year2}"])
ax6.grid()
fig

課題 k03

上記で紹介したリンク のページには,「日平均気温」の表が見えていますが, 「日平均気温」以外にも同様の形式のデータを選ぶことができます.

「降水量」の表をスクレイピングし,適切に表を整形し,指定した年の月別降水量の変化をグラフ表示する main() 関数を記述せよ.引数 years で対象の年を任意個並べた2次元 ndarray を受け取り,その配置通りにグラフ表示すること.また,以下の指示,および表示例に従って体裁も整えること.

  • グラフの縦軸である降水量の値の表示範囲は,全ての Axes 共通で,0から years に含まれる全ての年の最大降水量までとする.
    • 例えば,以下のグラフでは,yearsは,1925年から20年おき6回の月別降水量を表示しているが,1945年の9月が表示対象の年で最高の降水量(約800mm)を記録している.したがって,すべてのグラフの縦軸の表示範囲は,0から約800mmとしている.
  • Figureの figsize は,(12,6) とすること.

いつものように,

> poetry new k03
> cd k03
> poetry add pandas matplotlib numpy lxml ipykernel
> poetry env use py
> code .

でパッケージ開発の準備をする.

k03
┣─ src
│  └─ k03
│     └─ precipi_plot.py
└─ tests
   └─ test_precipi_plot.py
  • k03/src/k03/precipi_plot.py : プログラム本体

  • k03/tests/test_precipi_plot.py : 以下のような動作確認用コード

    import numpy as np
    from k03 import precipi_plot as my
    
    years = [[1925, 1945, 1965], #| 他の行数,列数,表示年でも動作確認すること
             [1985, 2005, 2025]] #|
    my.main(np.array(years))

の2つをコーディングする.

poetry sync を行なってからでないとテストできない.module not found: k03 となるであろう.

python では,あるファイル A から利用できる(参照できる)フォルダやファイルの場所が,

  • ファイル A と同じ階層
  • 利用している python 環境(今回では,.venv にまとめられている)の site_packages フォルダの中

の2箇所に限定されている.

つまり, k03/src/k03/precipi_plot.pytests/test_precipi_plot.py からは参照できない場所にある.

poetry sync を行うと,k03/src/k03フォルダ)を k03というパッケージ名で .venvsite_packages フォルダにリンクすることとなる.すなわち,そのpython環境であれば,どこからでも参照可能となる.

提出物は,いつものように whl ファイルであり,

> poetry version patch
> poetry build

によって,distsの下にできあがる.

課題の提出

  • 以下の2ファイルをLACSで提出
    • poetry build でビルドした dist/k03-x.y.z-py3-none-any.whlファイル
    • Word (.docx) または pdf のレポート (テスト実行画面のスクリーンショット画像と簡単な説明を含めること)
  • 再提出のときは,poetry version patch してから再度ビルド
  • 初回提出〆切: 6/25(水), 最終提出〆切: 7/9(水)
    • 10点になるまで再提出です.最終〆切を過ぎると1日毎に減点.
    • 初回提出〆切は状況把握のためにあります.未完成であってもレポートで現状報告してください.