引き続いて、numpy, pandasの基本的な使い方をざっくりと載せておきます。
本記事では行列系の演算は扱ってません。
(とは言っても、機械学習、ディープラーニングでデータを扱っていると避けては通れないので、適宜追加していきます。)
前回までのおさらいは、以下の記事をご参照ください。
[ざっくりPythonのおさらい その1。基本文法など]
https://qiita.com/48hands/items/31b7ac2b49addbb8658c
[ざっくりPythonのおさらい その2。おもにデータ保存]
https://qiita.com/48hands/items/ab86b7463268e669d216
Numpy
import numpy as np
で利用する。
np.array
一次元の配列
import numpy as np
a1 = np.array([1, 2, 3, 4, 5])
print(a1)
# 配列サイズの確認
print(a1.shape)
# 次元数の確認
print(a1.ndim)
# 要素数の確認
print(a1.size)
[1 2 3 4 5]
(5,)
1
5
多次元の配列
import numpy as np
# 二次元の配列生成
a2 = np.array([[1, 2, 3], [4, 5, 6]])
print(a2[0])
print(a2[1])
print(a2[0][1])
# 配列サイズの確認
print(a2.shape)
# 次元数の確認
print(a2.ndim)
# 要素数の確認
print(a2.size)
[1 2 3]
[4 5 6]
2
(2, 3)
2
6
np.arange
# 0から9の一次元配列
a = np.arange(10)
print(a)
# 0から29まで5ずつスキップの一次元配列
a = np.arange(0, 30, 5)
print(a)
# 0から2まで0.3ずつスキップの一次元配列
a = np.arange(0, 2, 0.3)
print(a)
[0 1 2 3 4 5 6 7 8 9]
[ 0 5 10 15 20 25]
[ 0. 0.3 0.6 0.9 1.2 1.5 1.8]
np.zeros / np.ones
業務処理では滅多に使うことがないかもしれませんが、行列計算でよく使います。
np.zeros
# 3行4列の二次元配列
a = np.zeros((3, 4), dtype=np.int32)
print(a)
[[0 0 0 0]
[0 0 0 0]
[0 0 0 0]]
np.ones
# 3行4列の二次元配列
a = np.ones((3, 4), dtype=np.int32)
print(a)
[[1 1 1 1]
[1 1 1 1]
[1 1 1 1]]
np.reshape
arange
関数と組み合わせて使うことが多いです。
# 2 * 3の配列を作成
a = np.arange(6).reshape(2, 3)
print(a)
[[0 1 2]
[3 4 5]]
# 2 * 3 * 4の配列を作成
a = np.arange(24).reshape(2, 3, 4)
print(a)
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
np.vstack / np.hstack
以下のようなデータを扱うものとして記載しています。
x = np.arange(0, 10, 2)
y = np.arange(5)
z = np.arange(0, 100, 20)
print(x)
print(y)
print(z)
[0 2 4 6 8]
[0 1 2 3 4]
[ 0 20 40 60 80]
np.vstack
垂直にスタックします。
print(np.vstack([x, y, z]))
[[ 0 2 4 6 8]
[ 0 1 2 3 4]
[ 0 20 40 60 80]]
np.hstack
水平方向にスタックします。
print(np.hstack([x, y, z]))
[ 0 2 4 6 8 0 1 2 3 4 0 20 40 60 80]
np.random
# 2行3列の値がランダムな数の配列を生成
a = np.random.randn(2,3)
print(a)
[[-0.00500654 -0.252814 -0.57280965]
[-1.44175418 -0.22190351 -0.48507611]]
演算
配列同士の演算
配列の要素同士の計算。(行列計算ではない。)
a = np.array([[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]])
b = np.array([[2, 4, 6, 7, 9], [1, 2, 4, 2, 8]])
# 要素同士の足し算
print(a + b)
# 要素同士の掛け算
print(a * b)
[[ 3 6 9 11 14]
[ 4 6 9 8 15]]
[[ 2 8 18 28 45]
[ 3 8 20 12 56]]
統計的な計算
a = np.array([[1, 2, 3, 4, 5], [3, 4, 5, 6, 7]])
print(a)
# すべての要素を使って計算
print(a.sum())
print(a.max())
print(a.min())
print(a.mean())
print(a.std())
# 列ごとに集計
print(a.sum(axis=0))
# 行ごとに集計
print(a.sum(axis=1))
[[1 2 3 4 5]
[3 4 5 6 7]]
40
7
1
4.0
1.73205080757
[ 4 6 8 10 12]
[15 25]
Pandas
業務処理としては、Pandasの方がNumpyよりも使う印象。(PandasもNumpyを中で使っているのでこの表現は正確ではないですが..)
Pandasによってデータをデータフレームの形式(表のようなもの)で扱えます。
import pandas as pd
で利用する。
データフレームの生成
一次元データ
import pandas as pd
import numpy as np
# 一次元
s = pd.Series([1, 2, 3, 4, np.nan, 5])
print(s)
0 1.0
1 2.0
2 3.0
3 4.0
4 NaN
5 5.0
左の0,1,2,3,4,5
はインデックスを示している。
二次元データ
二次元の場合はよくdf
を変数名としてつかう。
カラムとデータをセットで与える場合
pd.DataFrame
の引数にディクショナリ形式でデータを与える。
df = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
print(df)
A B
0 1 3
1 2 4
左の0,1
はインデックスを示している。
カラムとデータを別で与えたい場合
df = pd.DataFrame(np.random.randn(5, 4),
columns=['A', 'B', 'C', 'D'])
print(df)
A B C D
0 -0.305246 -0.244308 0.960713 -1.948262
1 1.827907 2.123698 1.880136 -0.663541
2 -0.194168 1.093406 1.690319 0.609997
3 -0.530745 -0.740414 0.669239 -1.154399
4 -0.344853 -1.834740 -0.075733 -0.057627
さらに、インデックスを0,1,2,...
のように定義せずに任意のものを与えたい場合は、以下のように与えることもできる。
df = pd.DataFrame(np.random.randn(5, 4),
index=["a" + str(i) for i in range(5)],
columns=['A', 'B', 'C', 'D'])
print(df)
A B C D
a0 0.530294 0.322370 0.478835 -0.126045
a1 0.432960 -1.546127 0.492869 2.012067
a2 -1.603422 -0.610451 0.463467 0.199561
a3 -0.368643 1.409762 -0.185814 -0.573302
a4 0.090929 0.036672 -0.027835 0.558301
もしくは、pandasのxx_range
関数が用意されている。
# date_range関数を使って、20180101-20180105をインデックスとして定義
df = pd.DataFrame(np.random.randn(5, 4),
index=pd.date_range('20180101', periods=5),
columns=['A', 'B', 'C', 'D'])
print(df)
A B C D
2018-01-01 -0.586486 -0.291565 0.473952 -0.685849
2018-01-02 -1.393820 -0.487958 -1.589376 0.105860
2018-01-03 -0.753569 -0.864522 0.662988 0.645619
2018-01-04 0.606714 0.260588 0.128833 0.432453
2018-01-05 -0.189775 -0.656559 0.161292 -0.429529
データの参照
head / tail / columns / values
df = pd.DataFrame(np.random.randn(5, 4),
columns=['A', 'B', 'C', 'D'])
# 上から2行を取得
print(df.head(2))
# 下から2行を取得
print(df.tail(2))
# カラムを取得
print(df.columns)
# 値を取得
print(df.values)
A B C D
0 -0.999091 -0.122181 -0.894873 0.039718
1 0.718243 -0.133020 1.106097 0.786122
A B C D
3 1.535138 -1.018447 1.178315 -0.277484
4 -0.741541 -1.409563 -0.349262 1.506808
Index(['A', 'B', 'C', 'D'], dtype='object')
[[-0.99909119 -0.12218051 -0.89487256 0.03971803]
[ 0.71824258 -0.13302039 1.10609694 0.78612167]
[ 0.74734781 1.28302495 -0.25022485 -0.92677622]
[ 1.53513828 -1.01844676 1.17831484 -0.27748411]
[-0.74154141 -1.4095629 -0.34926238 1.50680833]]
なお、head
,tail
,columns
,values
の型は以下のようになっている。
print(type(df.head(2)))
print(type(df.tail(2)))
print(type(df.columns))
print(type(df.values))
<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.indexes.base.Index'>
<class 'numpy.ndarray'>
columns
、values
をリスト形式にしたい場合は、以下のようにすればリストに変換できると思われる。(このやり方がいいのかはわかりませんが、どうしてもpandasのデータをリストで扱いたい場合の手段として残しておきます。)
df = pd.DataFrame(np.random.randn(5, 4),
index=pd.date_range('20180101', periods=5),
columns=['A', 'B', 'C', 'D'])
print(list(df.columns))
print(list([list(a) for a in df.values]))
['A', 'B', 'C', 'D']
[[-0.002235757935339935, 1.6529973427058939, 0.64203999192301642, -1.3210444284424014], [2.3671442978642845, -1.0722592191428966, 1.2885945049511809, 0.026156012112749916], [0.77976030738001834, 0.33266376052480762, -2.5191588600551702, -0.36360286000823311], [-1.9147929973641573, 0.71150943274316547, 0.17461225929264279, -0.38409302462051303], [2.4247639005597312, 1.3167549003901313, 1.0682682650746929, 0.50789521949882876]]
行のみを指定したアクセス
行のアクセスはインデックスを使う。
# 5行4列のデータフレーム
df = pd.DataFrame(np.random.randn(5, 4),
index=["a" + str(i) for i in range(5)],
columns=['A', 'B', 'C', 'D'])
# インデックス番号を指定して参照する
print(df[0:3])
# インデックス名を指定して参照する
# df[0:3]と同じ結果になる
print(df['a0':'a2'])
A B C D
a0 0.805062 -1.033655 3.012355 -0.283463
a1 0.944916 -0.506144 -1.118409 -1.420088
a2 0.045544 -0.466361 -2.671331 -1.080808
A B C D
a0 0.805062 -1.033655 3.012355 -0.283463
a1 0.944916 -0.506144 -1.118409 -1.420088
a2 0.045544 -0.466361 -2.671331 -1.080808
行とカラムを指定したアクセス
loc
関数にインデックスのレンジと取得したいカラムの配列を渡す。
# 5行4列のデータフレーム
df = pd.DataFrame(np.random.randn(5, 4),
index=["a" + str(i) for i in range(5)],
columns=['A', 'B', 'C', 'D'])
# a0〜a2までの行かつ、カラムA,Cのみを取得
print(df.loc['a0':'a2', ['A', 'C']])
# すべての行かつ、カラムA,Cのみを取得
print(df.loc[:, ['A', 'C']])
A C
a0 0.144033 -1.296219
a1 1.005402 1.443974
a2 -1.426863 -0.036709
A C
a0 0.144033 -1.296219
a1 1.005402 1.443974
a2 -1.426863 -0.036709
a3 -1.915530 -1.260232
a4 -0.385383 -0.243092
iloc
関数で行と列の要素番号を指定したアクセスはこちら。
df = pd.DataFrame(np.random.randn(5, 4),
index=["a" + str(i) for i in range(5)],
columns=['A', 'B', 'C', 'D'])
print(df.iloc[0, 0])
print(df.iloc[0:3, 0:3])
-0.196552994109
A B C
a0 -0.196553 0.819213 -0.185168
a1 -0.783100 -1.275004 -0.215038
a2 -0.430817 -0.497900 0.128841
カラムの追加
カラムの追加はディクショナリ形式でリストを追加するようにできる。
# もとのデータフレーム(3行4列)
df = pd.DataFrame(np.random.randn(3, 4),
index=pd.date_range('20180101', periods=3),
columns=['A', 'B', 'C', 'D'])
print(df)
# カラムEを追加する
df['E'] = ['Makio', 'Shintaro', 'Hanako']
print(df)
A B C D
2018-01-01 -0.899274 -2.721258 0.397787 0.318388
2018-01-02 0.773072 -0.091913 -0.086014 1.379136
2018-01-03 -0.203123 0.191961 1.127242 0.989349
A B C D E
2018-01-01 -0.899274 -2.721258 0.397787 0.318388 Makio
2018-01-02 0.773072 -0.091913 -0.086014 1.379136 Shintaro
2018-01-03 -0.203123 0.191961 1.127242 0.989349 Hanako
関数の適用
map
もしくはapply
に関数を渡す。
df = pd.DataFrame(
{'A': [1, 2, 3, 4], 'B': ['Hanako', 'Makio', 'Taro', 'Kanako']})
print(df)
df['A2'] = df['A'].map(lambda x: x * 2)
df['B2'] = df['B'].map(lambda x: x.startswith('H'))
df['AB'] = df.apply(lambda x: x['B'] + str(x['A']), axis=1) # axis=1で各行に関数を適用している
print(df)
A B
0 1 Hanako
1 2 Makio
2 3 Taro
3 4 Kanako
A B A2 B2 AB
0 1 Hanako 2 True Hanako1
1 2 Makio 4 False Makio2
2 3 Taro 6 False Taro3
3 4 Kanako 8 False Kanako4
上の例ではlambda
で渡しているが、以下のようにも記述できる。
lambda
で定義する処理が長くなりそうな場合は、こちらを使うのがよさそう。
df = pd.DataFrame(
{'A': [1, 2, 3, 4], 'B': ['Hanako', 'Makio', 'Taro', 'Kanako']})
print(df)
def func1(x):
return x * 2
def func2(x):
return x.startswith('H')
def func3(x):
return x['B'] + str(x['A'])
df['A2'] = df['A'].map(func1)
df['B2'] = df['B'].map(func2)
df['AB'] = df.apply(func3, axis=1)
print(df)
また、assign
メソッドを使って以下のようにも記述できる。
df = pd.DataFrame(
{'A': [1, 2, 3, 4], 'B': ['Hanako', 'Makio', 'Taro', 'Kanako']})
df2 = df.assign(
A2=lambda df: df['A'].apply(lambda x: x * 2),
B2=lambda df: df['B'].apply(lambda x: x.startswith('H')),
AB=lambda df: df.apply(lambda x: x['B'] + str(x['A']), axis=1))
print(df2)
データの絞り込み
where句相当
query
メソッドを使うのが簡単。
df = pd.DataFrame(
{'A': [-1, 0, 1, 0, -4], 'B': ['one', 'two', 'two', 'three', 'five']})
# Aが0かつBが'two'を条件として抽出
print(df.query('A == 0 and B == "two"'))
A B
1 0 two
where in相当
df = pd.DataFrame(
{'A': [-1, 0, 1, 0, -4], 'B': ['one', 'two', 'two', 'three', 'five']})
# B列がtwo,fourのものを抽出
df2 = df[df['B'].isin(['two', 'four'])]
print(df2)
A B
1 0 two
2 1 two
データのソート
sort_values
メソッドを使う。
df = pd.DataFrame(
np.array([[1, 4], [2, 7], [5, 2], [3, 3], [-1, 6], [2, 2], [3, 4]]),
index=pd.date_range('20180101', periods=7), columns=['A', 'B'])
print(df)
# カラムA降順でソート
print(df.sort_values(by='A', ascending=False))
# カラムA降順、カラムB降順でソート
print(df.sort_values(by=['A', 'B'], ascending=[False, False]))
A B
2018-01-01 1 4
2018-01-02 2 7
2018-01-03 5 2
2018-01-04 3 3
2018-01-05 -1 6
2018-01-06 2 2
2018-01-07 3 4
A B
2018-01-03 5 2
2018-01-04 3 3
2018-01-07 3 4
2018-01-02 2 7
2018-01-06 2 2
2018-01-01 1 4
2018-01-05 -1 6
A B
2018-01-03 5 2
2018-01-07 3 4
2018-01-04 3 3
2018-01-02 2 7
2018-01-06 2 2
2018-01-01 1 4
2018-01-05 -1 6
データの集計
groupby
メソッドで集計できる。
df = pd.DataFrame({'A': ['foo', 'bar', 'foo', 'bar'],
'B': np.random.randn(4)}
)
print(df)
# カラムAをグループ集計して合計を求める
result_df = df.groupby('A').sum()
print(result_df)
A B
0 foo 0.125125
1 bar 0.886186
2 foo -2.036310
3 bar -1.993052
B
A
bar -1.106866
foo -1.911185
簡単に統計値を確認する
describe
メソッドを使うと簡単に基本統計量が確認できる。
df = pd.DataFrame(np.random.randn(5, 4),
index=pd.date_range('20180101', periods=5),
columns=['A', 'B', 'C', 'D'])
print(df)
print(df.describe())
A B C D
a0 -0.101431 -0.540538 0.413183 -1.112668
a1 1.139180 0.236635 1.234778 0.005053
a2 1.546940 -0.474595 0.860131 -0.058529
a3 0.339556 0.032907 0.069717 -0.638064
a4 -1.490855 1.516060 -1.045449 -0.065478
A B C D
count 5.000000 5.000000 5.000000 5.000000
mean 0.286678 0.154094 0.306472 -0.373937
std 1.186178 0.829791 0.875132 0.488301
min -1.490855 -0.540538 -1.045449 -1.112668
25% -0.101431 -0.474595 0.069717 -0.638064
50% 0.339556 0.032907 0.413183 -0.065478
75% 1.139180 0.236635 0.860131 -0.058529
max 1.546940 1.516060 1.234778 0.005053
データフレーム同士の結合
# 3行2列のデータフレームを作成
df1 = pd.DataFrame(np.random.randn(3, 2), columns=['A', 'C'])
df2 = pd.DataFrame(np.random.randn(3, 2), columns=['A', 'B'])
# 縦方向に結合
# ignore_indexでdf1,df2で振られているインデックスを無視して振り直す。
merge_df = pd.concat([df1, df2], ignore_index=True)
print(merge_df)
A B C
0 -0.195391 NaN 0.597881
1 -1.412883 NaN -1.028382
2 0.010281 NaN 0.837267
3 0.351978 -0.251637 NaN
4 -0.479735 -1.370914 NaN
5 0.907136 1.774478 NaN
欠損値NaNを扱う
置換する場合
前節のmerge_df
でカラムB,CのNaNを0で置換する。
merge_df['B'].fillna(0, inplace=True)
merge_df['C'].fillna(0, inplace=True)
print(merge_df)
A B C
0 0.496169 0.000000 2.323138
1 -0.094766 0.000000 0.250895
2 -1.200690 0.000000 0.891685
3 -0.528584 -0.358417 0.000000
4 0.706201 0.381958 0.000000
5 0.736269 -0.130781 0.000000
削除する場合
dropna
メソッドを利用してレコードを削除する。
オプションの違いもあるため、以下のようなデータフレームで例示しています。
df = pd.DataFrame(
{'A': [np.nan, 1, 2],
'B': [np.nan, np.nan, 3]
})
print(df)
A B
0 NaN NaN
1 1.0 NaN
2 2.0 3.0
すべてのカラムがNaNのレコード削除
how
にall
を指定する。
print(df.dropna(how='all'))
A B
1 1.0 NaN
2 2.0 3.0
いずれかのカラムがNaNのレコード削除
how
にany
を指定する。
(dropna
メソッドのデフォルトなので明示的に指定しなくてもよい。)
print(df.dropna(how='all'))
A B
2 2.0 3.0
データフレームのコピー
データフレームのコピーはcopy
メソッドを使う。
df1 = pd.DataFrame(
{'A': [1, 2, 3],
'B': [2, 2, 3]
})
df2 = df1.copy()
df2['C'] = [5,6,7]
print(df1)
print(df2)
A B
0 1 2
1 2 2
2 3 3
A B C
0 1 2 5
1 2 2 6
2 3 3 7
当たり前だが、データフレームも参照渡しなので、以下のdf1
とdf2
は同じ結果になる。
df1 = pd.DataFrame(
{'A': [1, 2, 3],
'B': [2, 2, 3]
})
df2 = df1
df2['C'] = [5,6,7]
print(df1)
print(df2)
A B C
0 1 2 5
1 2 2 6
2 3 3 7
A B C
0 1 2 5
1 2 2 6
2 3 3 7
転置
とても簡単に転置できる。
df = pd.DataFrame(np.random.randn(5, 4),
index=pd.date_range('20180101', periods=5),
columns=['A', 'B', 'C', 'D'])
print(df)
# 転置
print(df.T)
A B C D
2018-01-01 1.623736 -1.138568 0.963784 1.143847
2018-01-02 0.473417 0.937296 -0.274963 0.803029
2018-01-03 -0.628118 1.058111 0.848354 -0.563482
2018-01-04 -0.129076 -0.135657 1.400161 0.094259
2018-01-05 0.985847 1.522559 -0.094080 0.899734
2018-01-01 2018-01-02 2018-01-03 2018-01-04 2018-01-05
A 1.623736 0.473417 -0.628118 -0.129076 0.985847
B -1.138568 0.937296 1.058111 -0.135657 1.522559
C 0.963784 -0.274963 0.848354 1.400161 -0.094080
D 1.143847 0.803029 -0.563482 0.094259 0.899734
外部データの読み書き
テキストデータの読み書き
読み込み
pd.read_csv
で読み込む。
name,age
hanako,22
taro,25
df = pd.read_csv('hoge.csv', sep=',', encoding='utf-8')
# ヘッダーがない場合には、header=Noneを指定する。
# df = pd.read_csv('hoge.csv', sep=',', header=None,encoding='utf-8')
print(df)
name age
0 hanako 22
1 taro 25
書き込み
import csv
# データフレームのインデックスはファイル出力させたくなかったので
# index=Falseを指定している。
# quoting=csv.QUOTE_ALLの部分は、quoting=1としても結果は同じ
df.to_csv('target.csv',
sep=',',
index=False,
encoding='utf-8',
header=True,
quoting=csv.QUOTE_ALL)
"name","age"
"hanako","22"
"taro","25"
RDBに対する読み書き
読み込み
pd.read_sql
を使ってデータを読み込む。
SQLAlchemyを使ってengineを引数に渡すVer
engine
を渡すだけなので、コネクション管理しなくてもよいのかと思う。
import sqlalchemy
# mysql+pymysqlを指定する
url = 'mysql+pymysql://username:password@127.0.0.1/my_db'
engine = sqlalchemy.create_engine(url, echo=True)
df = pd.read_sql('select * from persons', engine)
print(df)
id name
0 1 Robot-1
1 2 Robot-2
2 3 Robot-3
SQLAlchemyを使わずにコネクションを渡すVer
import mysql.connector
conn = mysql.connector.connect(host='127.0.0.1',
port=3306,
user='root',
database='my_db')
df = pd.read_sql('select * from persons', conn)
print(df)
id name
0 1 Robot-1
1 2 Robot-2
2 3 Robot-3
書き込み
df.to_sql
で実行すればよいが、引数con
にはSQLAlchemyのengine
を指定しないと実行できなかった。
url = 'mysql+pymysql://username:password@127.0.0.1/my_db'
engine = sqlalchemy.create_engine(url, echo=True)
df = pd.read_csv('robot.csv')
# 第一引数にテーブル名を指定。
# conにはengineを指定
# index=FalseにしてDataFrameのインデックスは無効化した。
# if_existsをappendとしてテーブルにデータを新規に追加する
df.to_sql('persons',
con=engine,
index=False,
if_exists='append')
[注意事項]
ここで、if_exists
は、'append'
のほか、'fail'
,'replace'
を指定できるが、'fail'
はテーブルが存在していたらValueError
になる。
'replae'
を指定すると、drop table
してからデータをインサートするので注意が必要。
また、用意したデータ(robot.csv)はこちら。
id,name
100,Robot-101
101,Robot-102
102,Robot-103
echo=True
でengine
を生成しているので、SQL実行ログも出力してくれる。
2018-09-02 19:11:02,923 INFO sqlalchemy.engine.base.Engine SHOW VARIABLES LIKE 'sql_mode'
2018-09-02 19:11:02,923 INFO sqlalchemy.engine.base.Engine {}
2018-09-02 19:11:02,926 INFO sqlalchemy.engine.base.Engine SELECT DATABASE()
2018-09-02 19:11:02,926 INFO sqlalchemy.engine.base.Engine {}
2018-09-02 19:11:02,927 INFO sqlalchemy.engine.base.Engine show collation where `Charset` = 'utf8mb4' and `Collation` = 'utf8mb4_bin'
2018-09-02 19:11:02,927 INFO sqlalchemy.engine.base.Engine {}
2018-09-02 19:11:02,928 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS CHAR(60)) AS anon_1
2018-09-02 19:11:02,928 INFO sqlalchemy.engine.base.Engine {}
2018-09-02 19:11:02,929 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS CHAR(60)) AS anon_1
2018-09-02 19:11:02,929 INFO sqlalchemy.engine.base.Engine {}
2018-09-02 19:11:02,929 INFO sqlalchemy.engine.base.Engine SELECT CAST('test collated returns' AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_bin AS anon_1
2018-09-02 19:11:02,929 INFO sqlalchemy.engine.base.Engine {}
2018-09-02 19:11:02,930 INFO sqlalchemy.engine.base.Engine DESCRIBE `persons`
2018-09-02 19:11:02,930 INFO sqlalchemy.engine.base.Engine {}
2018-09-02 19:11:02,932 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-09-02 19:11:02,932 INFO sqlalchemy.engine.base.Engine INSERT INTO persons (id, name) VALUES (%(id)s, %(name)s)
2018-09-02 19:11:02,932 INFO sqlalchemy.engine.base.Engine ({'id': 100, 'name': 'Robot-101'}, {'id': 101, 'name': 'Robot-102'}, {'id': 102, 'name': 'Robot-103'})
2018-09-02 19:11:02,936 INFO sqlalchemy.engine.base.Engine COMMIT