Python を使ったデータ分析

Python の基礎を学んだので、Python でデータ整理、分析をする際に役立つパッケージを紹介します。

Numpy

numpy は行列計算用のパッケージです。 内部は C で実装されており、高速に計算をすることができます。 また、anaconda を使って python をインストールした場合には、 計算に Intel の Math Kernel Library が使われており、さらに高速に計算をすることができます。 numpy は np として 略して import するのが慣習です。

[1]:
import numpy as np

ndarray

numpy では ndarray オブジェクトが行列です。 ndarray を作るには、array 関数を用いて、次のようにシークエンス型(リストやタプルなど)を渡します。

[2]:
np.array([1, 2, 3])
[2]:
array([1, 2, 3])

リストを 1 つ渡すとベクトル、行列にするにはリストの中にリストを渡します。 各リストが行に相当されます。 リストをさらに入れ子にすると、より多次元にすることができます。

[3]:
np.array([[1, 2, 3], [4, 5, 6]])
[3]:
array([[1, 2, 3],
       [4, 5, 6]])

リストの長さを同じにしないと、リストのベクトルが作成されてしまうので注意してください。

[4]:
np.array([[1, 2, 3], [4, 5]])
[4]:
array([list([1, 2, 3]), list([4, 5])], dtype=object)

各要素を参照するのは、リストと同様の方法ですが、行列の場合、参照できるのは行と列です。 [1] とすると、1 行目が参照されます。[:,1] とすると、1 列目が参照されます。 [1,1] とすれば、1 行目の 1 列目の要素が参照されます。

[5]:
A = np.array([[1, 2, 3], [4, 5, 6]])
A
[5]:
array([[1, 2, 3],
       [4, 5, 6]])
[6]:
A[1]
[6]:
array([4, 5, 6])
[7]:
A[:, 1]
[7]:
array([2, 5])
[8]:
A[1, 1]
[8]:
5

n x m の ndarray に対し、同じ行列数で要素が bool 型の ndarray を渡すと、True の要素のみが返ってきます。

[9]:
mask = np.array([[True, False, True], [False, False, True]])
A[mask]
[9]:
array([1, 3, 6])

各要素が条件式を満たすかどうかを確認することが可能です。 この結果を用いて、条件を満たす要素のみを参照することも可能です。

[10]:
A > 4
[10]:
array([[False, False, False],
       [False,  True,  True]])
[11]:
A[A > 4]
[11]:
array([5, 6])

複数の条件も設定できます。各条件は () で囲み、& ならば積集合、| ならば和集合になります。

[12]:
(A > 4) & (A < 8)
[12]:
array([[False, False, False],
       [False,  True,  True]])
[13]:
(A < 3) | (A > 8)
[13]:
array([[ True,  True, False],
       [False, False, False]])

array 以外にも ndarray を生成する方法があります。 例えば、arangerange 関数と同じような挙動で ndarray を生成します。

[14]:
np.arange(1, 10)
[14]:
array([1, 2, 3, 4, 5, 6, 7, 8, 9])

ones は 1 のみの ndarray、zeros は 0 のみの ndarray を生成します。

[15]:
np.ones((3, 3))
[15]:
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
[16]:
np.zeros((3, 3))

[16]:
array([[0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.]])

reshape を使えば、n x m 行列を l x k 行列に変更できます。

[17]:
C = np.arange(1, 10)
C
[17]:
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
[18]:
np.reshape(C,(3, 3))
[18]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

同名のメソッドもあります。

[19]:
C.reshape((3, 3))
[19]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

属性

ndarray の属性には、例えば、配列の行列数を返す shape や 要素の型を返す dtype などがあります。

[20]:
A = np.array([[1, 2, 3], [4, 5, 6]])
A.shape
[20]:
(2, 3)
[21]:
A.dtype
[21]:
dtype('int64')

T で転置行列を返します。

[22]:
A.T
[22]:
array([[1, 4],
       [2, 5],
       [3, 6]])

行列計算

先述したように、numpy では様々な行列計算をすることができます。

[23]:
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
B = np.array([[9, 8, 7], [6, 5, 4], [3, 2, 1]])

まずは行列の加算です。

[24]:
A + B
[24]:
array([[10, 10, 10],
       [10, 10, 10],
       [10, 10, 10]])

dot または @ で行列の積を計算します。

[25]:
A @ B
[25]:
array([[ 30,  24,  18],
       [ 84,  69,  54],
       [138, 114,  90]])
[26]:
np.dot(A, B)
[26]:
array([[ 30,  24,  18],
       [ 84,  69,  54],
       [138, 114,  90]])

* を使うと、各要素をかけ合わせます。

[27]:
A * B
[27]:
array([[ 9, 16, 21],
       [24, 25, 24],
       [21, 16,  9]])

linalg は様々な線形代数の計算をする関数が含まれています。 例えば、inv は逆行列を計算します。

[28]:
C = np.array([[1, 3, 5], [2, 9, 7], [2, 8, 4]])
np.linalg.inv(C)
[28]:
array([[ 1.66666667, -2.33333333,  2.        ],
       [-0.5       ,  0.5       , -0.25      ],
       [ 0.16666667,  0.16666667, -0.25      ]])

ここで実際に、numpy の計算が高速だという例をお見せします。 10000 個の要素を足し合わせていく作業をループ文と numpy の sum を使って試してみます。 セルの最初に %%timeit を使うことによって、そのセルの計算速度を測定できます。

[29]:
%%timeit
sum_Z = 0
for i in range(10000):
    sum_Z += 1

1.26 ms ± 174 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
[30]:
Z = np.ones(10000)
[31]:
%%timeit
np.sum(Z)
20.8 µs ± 7.98 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

大規模な計算をするときは、できるだけ numpy を使うのを勧めます。

ブロードキャスト

ndarray には、ブロードキャストという機能がついています。 これによって、配列の行列数が違う場合にも計算を可能にします。 実際に見てみましょう。行列の加算は配列の数が同じもの同士に定義されますが、X と Y は配列数が違います。 この 2 つを足してみます。

[32]:
X = np.array([[1, 2, 3],[4, 5, 6],[7, 8, 9]])
Y = np.array([[1, 2, 3]])
X + Y
[32]:
array([[ 2,  4,  6],
       [ 5,  7,  9],
       [ 8, 10, 12]])

この結果は次の計算によるものであることは明らかですね。

[33]:
Y_wide = np.array([[1, 2, 3],[1 ,2, 3],[1, 2, 3]])
X + Y_wide
[33]:
array([[ 2,  4,  6],
       [ 5,  7,  9],
       [ 8, 10, 12]])

ブロードキャストというのは、つまり、行列同士を計算できるように、配列数が足りない行列を引き伸ばしてくれるということです。 例えば、ある数字を全ての要素に足したい場合、行列数を同じにしないでもすむというメリットがあります。

乱数生成

Python にも random という疑似乱数を生成するモジュールがありますが、numpy の random はより高機能です。 計量経済学をするなら気になるところですね。 さっそく見ていきましょう。 rand は一様分布から疑似乱数を生成します。引数に配列数を渡します。

[34]:
np.random.rand(3, 3)
[34]:
array([[0.81604635, 0.9374695 , 0.61670905],
       [0.15385651, 0.35901032, 0.84625432],
       [0.88591126, 0.40596778, 0.14039651]])

randn は標準正規分布からの疑似乱数を発生させます。

[35]:
np.random.randn(3, 3)
[35]:
array([[ 1.28406631, -0.53822411,  0.80013257],
       [ 0.81690022, -0.58300972, -2.70036416],
       [-0.59812875, -1.26783858,  0.7256989 ]])

choice は引数に渡した 1 次元の ndarray もしくはリストからランダムに要素を抽出します。

[36]:
countries = ['Japan', 'China', 'Korea']
np.random.choice(countries)
[36]:
'Japan'

引数には要素を抽出する回数、復元抽出か非復元抽出か、各要素を抽出する確率を渡せます。

[37]:
np.random.choice(countries, size = 2)
[37]:
array(['Japan', 'China'], dtype='<U5')
[38]:
np.random.choice(countries, size = 2, replace = False)
[38]:
array(['China', 'Korea'], dtype='<U5')
[39]:
np.random.choice(countries, size = 2, p = [0.9, 0.05, 0.05])
[39]:
array(['Japan', 'Japan'], dtype='<U5')

一様分布や標準正規分布といったおなじみのもの以外にも、ベータ関数やラプラス分布、ロジスティック分布などから疑似乱数を生成することもできます。 次の例は、自由度 5 の\(\chi^2\)分布から疑似乱数を生成しています。

[40]:
np.random.chisquare(5, 3)
[40]:
array([4.15855897, 6.61777927, 5.16556098])

Scipy

scipy を語るにはスペースが足りないほど、scipy は統計、機械学習、画像処理など様々な科学計算用の関数を備えています。 ネーミングセンスからお分かりのように、scipynumpy と併用して用います。 せっかくなので、いくつか見ていきましょう。

optimize

scipy はそ機能の大きさのため、各機能毎に import することが勧められています。 optimize は最適化に関する関数を備えています。 brentq は方程式の解を見つけ出します。

[41]:
from scipy import optimize as opt
def func1(x):
    fx = 3 * x - 5
    return fx

opt.brentq(func1, -10, 10)
[41]:
1.666666666666666

minimize は関数の最小値を探す関数です。オプションによって、様々な探索アルゴリズムを選択できます。

[42]:
def func2(x):
    fx = x ** 2 + 3 * x - 5
    return fx

opt.minimize(func2, 0)
[42]:
      fun: -7.25
 hess_inv: array([[0.5]])
      jac: array([0.])
  message: 'Optimization terminated successfully.'
     nfev: 9
      nit: 2
     njev: 3
   status: 0
  success: True
        x: array([-1.50000001])

stats

stats は統計分析に関する関数を備えています。 例えば、numpy のように疑似乱数を生成することも可能です。 norms.rvs とすると、正規分布の疑似乱数を生成します。

[43]:
from scipy import stats
stats.norm.rvs(loc = 0, scale = 1, size = 10)
[43]:
array([ 1.00585753, -1.03111348,  0.16008137, -0.2425014 ,  0.02450575,
        0.88017105, -1.32771746, -0.59314082, -0.15458929, -1.02338975])

rvs は疑似乱数の生成ですが、他にも豊富なメソッドがあります。 例えば、

  1. pdf : 確率密度関数の値を返す

  2. cdf : 累積密度関数の値を返す

  3. ppf : パーセント点の値を返す

[44]:
stats.norm.pdf(x = 0, loc = 0, scale = 1)
[44]:
0.3989422804014327
[45]:
stats.norm.cdf(x = 0, loc = 0, scale = 1)
[45]:
0.5
[46]:
stats.norm.ppf(q = 0.05, loc = 0, scale = 1)
[46]:
-1.6448536269514729

検定もすることも可能です。

例えば、ttest_ind は 2 つのデータ (ndarray) の差を t 検定します。

[47]:
N_A = stats.norm.rvs(loc = -5, scale = 1, size = 10)
N_B = stats.norm.rvs(loc = 3, scale = 1, size = 10)
stats.ttest_ind(N_A,N_B)
[47]:
Ttest_indResult(statistic=-20.51082893599917, pvalue=6.229373675859823e-14)

Pandas

pandas は 様々な I/O (入出力) 処理、高機能なデータ処理をすることができるパッケージです。 pandas のデータ型は、SeriesDataFrame です。 Series はラベル付きのベクトル、DataFrame はラベル付きの行列です。 R を使ったことがある人はピンとくるかもしれませんが、DataFrame は R の data.frame 型と同じようなものです。 実際、pandas は R のデータ整理用のパッケージ、dplyr でできることの多くを実装しています。 pandaspd と略して import するのが慣習です。 それでは、Series から見ていきましょう。

[48]:
import pandas as pd

Series

Series の作り方は、numpy の1次元の ndarray と同じようなものです。

[49]:
pd.Series([1, 2, 3])
[49]:
0    1
1    2
2    3
dtype: int64

ndarray とは似ていますが、異なるところもあります。 まず、左側に index が表示されていますね。 この index は好きなようにラベルを付けることも可能です。

[50]:
pd.Series([1, 2, 3], index = ['one', 'two', 'three'])
[50]:
one      1
two      2
three    3
dtype: int64

Series に名前を付けることも可能です。

[51]:
pd.Series([1, 2, 3], index = ['one', 'two', 'three'], name = 'numbers')
[51]:
one      1
two      2
three    3
Name: numbers, dtype: int64

index、要素、名前などが属性として参照できます。

[52]:
A_s = pd.Series([1, 2, 3], index = ['one', 'two', 'three'], name = 'numbers')
[53]:
A_s.index
[53]:
Index(['one', 'two', 'three'], dtype='object')
[54]:
A_s.values
[54]:
array([1, 2, 3])
[55]:
A_s.name
[55]:
'numbers'

要素を参照すると、ndarray が返ってきました。 pandasnumpy との間は簡単に行き来することができます。 今度は ndarraySeries にしてみます。

[56]:
A_np = np.array([1, 2, 3])
A_index = np.array(['one', 'two', 'three'])
pd.Series(A_np, index = A_index)
[56]:
one      1
two      2
three    3
dtype: int64

後で見ますが、DataFrame でも同じことができます。 実際にデータ分析をする際には、まず pandas を使ってデータを整理し、numpyscipy を使って分析を行うという流れになると思います。

要素の参照

Series は様々な方法で要素を参照することができます。 まず、要素の番号を入れて参照してみましょう。

[57]:
A_s
[57]:
one      1
two      2
three    3
Name: numbers, dtype: int64
[58]:
A_s[1]
[58]:
2

index 名で要素を参照することも可能です。

[59]:
A_s['three']
[59]:
3

参照した要素に違う値を代入してみます。

[60]:
A_s['three'] = 5
A_s
[60]:
one      1
two      2
three    5
Name: numbers, dtype: int64

numpy のところで説明したように、各要素が条件式を満たしているかを判別できます。 また、この結果を用いて条件式を満たす要素を参照できます。

[61]:
A_s > 2
[61]:
one      False
two      False
three     True
Name: numbers, dtype: bool
[62]:
A_s[A_s > 2]
[62]:
three    5
Name: numbers, dtype: int64

計算

Series の計算は numpy の時と違います。 Series では四則演算は index が同じもの同士で行います。

[63]:
A_s = pd.Series([1, 2, 3], index = ['one', 'two', 'three'])
B_s = pd.Series([4, 5, 6], index = ['one', 'two', 'three'])
B_s
[63]:
one      4
two      5
three    6
dtype: int64
[64]:
A_s + B_s
[64]:
one      5
two      7
three    9
dtype: int64
[65]:
A_s - B_s
[65]:
one     -3
two     -3
three   -3
dtype: int64
[66]:
A_s * B_s
[66]:
one       4
two      10
three    18
dtype: int64
[67]:
A_s / B_s
[67]:
one      0.25
two      0.40
three    0.50
dtype: float64

index が同じもの同士で計算するので、商も定義されます。 index が合致しないものは NaN (Not a number) が返ってきます。

[68]:
C_s = pd.Series([1, 2, 3], index = ['one', 'two', 'four'])
C_s
[68]:
one     1
two     2
four    3
dtype: int64
[69]:
A_s + C_s
[69]:
four     NaN
one      2.0
three    NaN
two      4.0
dtype: float64

文字列操作

Series には str という強力な文字列操作のメソッドが備わっています。 これを用いることによって、データのクリーニングが非常に楽になります。 まずは、基本的な操作からしていきましょう。 lower は文字列を小文字に、upper は文字列を大文字に、len は文字列の文字数をカウントします。

[70]:
small_text = pd.Series(['A', 'B', 'C'])
small_text.str.lower()
[70]:
0    a
1    b
2    c
dtype: object
[71]:
large_text = pd.Series(['a', 'b', 'c'])
large_text.str.upper()
[71]:
0    A
1    B
2    C
dtype: object
[72]:
number_text = pd.Series(['one', 'two', 'three'])
number_text.str.len()
[72]:
0    3
1    3
2    5
dtype: int64

extract は指定した文字を抜き出します。

[73]:
number_with_text = pd.Series(['one1', 'two2', 'three3'])
number_with_text.str.extract(r'(\d)', expand = False)
[73]:
0    1
1    2
2    3
dtype: object

\d正規表現 というもので、パターンマッチングをする際に強力な助けとなります。 正規表現はマッチングしたい文字列の条件式を非常に簡単に書くことができます。 いくつかの文字はメタキャラクタというもので、例えば \d は任意の数とマッチングするという意味です。 さきほどの文は、文頭から文字を参照していき、任意の数が当たったらその文字を抜き出す ということをしています。 マッチングの文に正規表現を使う場合は最初に r を付けておきましょう。 先程とは逆に、任意の数字以外の文字列を抜き出す場合次のようにします。 \D は数以外の任意の文字とマッチングします。 + は直前のマッチング式が一致していたら、それを繰り返します。 例えば、two+twotwoooooooo とも一致しますが、tw とは一致しません。

[74]:
number_with_text.str.extract(r'(\D+)', expand = False)
[74]:
0      one
1      two
2    three
dtype: object

replace はマッチした文字列を指定した文字列に置き換えます。

[75]:
number_with_text.str.replace(r'(\d)', '')
[75]:
0      one
1      two
2    three
dtype: object

match はマッチング文が先頭からマッチしているか、contains はマッチング文を含んでいるかを返します。

[76]:
match_text = pd.Series(['1', 'one2', 'one'])
match_text
[76]:
0       1
1    one2
2     one
dtype: object
[77]:
match_text.str.match(r'\d')
[77]:
0     True
1    False
2    False
dtype: bool
[78]:
match_text.str.contains(r'\d')
[78]:
0     True
1     True
2    False
dtype: bool

get_dummies はダミー変数を作成できます。 DataFrame を返します。

[79]:
dummies_text = pd.Series(['one', 'one', 'two'])
dummies_text.str.get_dummies()
[79]:
one two
0 1 0
1 1 0
2 0 1

sep で文字列を分けられます。

[80]:
dummies_text = pd.Series(['one|two', 'one', 'two'])
dummies_text.str.get_dummies(sep = '|')
[80]:
one two
0 1 1
1 1 0
2 0 1

DataFrame

DataFrame は 多次元の ndarray を作るときと似ています。

[81]:
pd.DataFrame([[1, 2, 3], [4, 5, 6]])
[81]:
0 1 2
0 1 2 3
1 4 5 6

Series と同じように左に index がありますが、さらに上に columns があります。 これらはどちらもラベル付けすることができます。

[82]:
index_list = ['one', 'two']
columns_list = ['a', 'b', 'c']
pd.DataFrame(
    [[1, 2, 3], [4, 5, 6]],
    index = index_list,
    columns = columns_list)
[82]:
a b c
one 1 2 3
two 4 5 6

Series のように index、columns、要素が属性として用意されています。

[83]:
A_dt = pd.DataFrame(
    [[1, 2, 3], [4, 5, 6]],
    index = index_list,
    columns = columns_list)
[84]:
A_dt.index
[84]:
Index(['one', 'two'], dtype='object')
[85]:
A_dt.columns
[85]:
Index(['a', 'b', 'c'], dtype='object')
[86]:
A_dt.values
[86]:
array([[1, 2, 3],
       [4, 5, 6]])

Series と同じように、要素を参照すると ndarray が返ってきます。 また、Series と同じように ndarrayDataFrame にすることもできます。

要素の参照

DataFrame にも様々な要素の参照の方法があります。

[87]:
A_dt
[87]:
a b c
one 1 2 3
two 4 5 6

columns の参照は [] か、.columns名 で行います。

[88]:
A_dt['a']
[88]:
one    1
two    4
Name: a, dtype: int64
[89]:
A_dt.b
[89]:
one    2
two    5
Name: b, dtype: int64

複数の columns の参照はリストを使います。

[90]:
A_dt[['a', 'c']]
[90]:
a c
one 1 3
two 4 6

index の参照は loc を使います。 loc[index名, columns名] とします。

[91]:
A_dt.loc['one']
[91]:
a    1
b    2
c    3
Name: one, dtype: int64
[92]:
A_dt.loc[:, 'c']
[92]:
one    3
two    6
Name: c, dtype: int64
[93]:
A_dt.loc['one', 'b']
[93]:
2

iloc はラベル名ではなく要素の番号によって参照します。

[94]:
A_dt.iloc[1]
[94]:
a    4
b    5
c    6
Name: two, dtype: int64
[95]:
A_dt.iloc[:, 2]
[95]:
one    3
two    6
Name: c, dtype: int64
[96]:
A_dt.iloc[1, 1]
[96]:
5

要素を参照して値を再代入することもできます。

[97]:
A_dt.loc['one', 'b'] = 5
A_dt
[97]:
a b c
one 1 5 3
two 4 5 6

条件式を満たしているかを判別することもでき、条件式を満足する行を参照することもできます。

[98]:
A_dt > 2
[98]:
a b c
one False True True
two True True True
[99]:
A_dt[A_dt > 2]
[99]:
a b c
one NaN 5 3
two 4.0 5 6

ある columns が条件式を満たしている 行を参照することもできます。

[100]:
A_dt.a > 2
[100]:
one    False
two     True
Name: a, dtype: bool
[101]:
A_dt[A_dt.a > 2]
[101]:
a b c
two 4 5 6

反対に、ある index が条件式を満たしている columns を参照することもできます。

[102]:
A_dt.loc['one'] > 2
[102]:
a    False
b     True
c     True
Name: one, dtype: bool
[103]:
A_dt.loc[:, A_dt.loc['one'] > 2]
[103]:
b c
one 5 3
two 5 6

I/O (入出力)処理

pandas は多様な I/O 処理をすることができ、 csv ファイル、エクセルファイル、Stata ファイル、R ファイルなどの様々なファイルを入出力することが可能です。 例えば、次の例はエクセルのファイルを読み込んでいます。

[104]:
pd.read_excel('data/excel_example.xlsx')
[104]:
one two three
0 1 4 7
1 2 5 8
2 3 6 9

次の例は DataFrame を Stata ファイルに出力しています。

[105]:
A_dt.to_stata('data/stata_output.dta')

グループ化

pandasDataFrame には様々なデータ集計の関数が用意されています。 例えば、groupby は指定した columns の値でグループを作ります。

[106]:
B_dt = pd.DataFrame(
    [['one', 1], ['one', 2], ['two', 3], ['two', 4]],
    columns = ['num', 'val'])
B_dt
[106]:
num val
0 one 1
1 one 2
2 two 3
3 two 4
[107]:
group_dt = B_dt.groupby('num')
group_dt
[107]:
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x1a1943cac8>

グループ化した DataFrame, DataFrameGroupBy に集計関数を与えることで集計値を獲得できます。 例えば、mean メソッドは平均値、 std メソッドは標準偏差が返されます。

[108]:
group_dt.mean()
[108]:
val
num
one 1.5
two 3.5
[109]:
group_dt.std()
[109]:
val
num
one 0.707107
two 0.707107

データの結合

pandas には便利なデータの結合の関数が用意されています。 concat は単純に DataFrame 同士を結合します。

[110]:
A_dt
[110]:
a b c
one 1 5 3
two 4 5 6
[111]:
index_list = ['one', 'two']
columns_list = ['a', 'b', 'c']
C_dt = pd.DataFrame(
    [[4, 5, 6], [7, 8, 9]],
    index = index_list,
    columns = columns_list)
C_dt
[111]:
a b c
one 4 5 6
two 7 8 9
[112]:
pd.concat([A_dt, C_dt])
[112]:
a b c
one 1 5 3
two 4 5 6
one 4 5 6
two 7 8 9

axis = 1 にすることで、横方向の結合をすることができます。

[113]:
pd.concat([A_dt, C_dt], axis = 1)
[113]:
a b c a b c
one 1 5 3 4 5 6
two 4 5 6 7 8 9

append も同様です。

[114]:
A_dt.append(C_dt)
[114]:
a b c
one 1 5 3
two 4 5 6
one 4 5 6
two 7 8 9

merge はキーを指定して、キーが同じもの同士で index を結合します。

[115]:
B_dt
[115]:
num val
0 one 1
1 one 2
2 two 3
3 two 4
[116]:
D_dt = pd.DataFrame(
    [['one', 1], ['two', 2], ['three', 3], ['four', 4]],
    columns = ['num', 'val'])
D_dt
[116]:
num val
0 one 1
1 two 2
2 three 3
3 four 4
[117]:
pd.merge(B_dt, D_dt, on = ['num'])
[117]:
num val_x val_y
0 one 1 1
1 one 2 1
2 two 3 2
3 two 4 2