【Python】実践データ分析100本ノック 第6章

2024/03/17に公開

この記事は、現場で即戦力として活躍することを目指して作られた現場のデータ分析の実践書である「Python 実践データ分析 100本ノック(秀和システム社)」で学んだことをまとめています。

ただし、本を参考にして自分なりに構成などを変更している部分が多々あるため、ご注意ください。細かい解説などは是非本をお手に取って読まれてください。

目的

物流データからネットワーク構造を可視化する方法を学び、最適な物流計画を立案する流れを学ぶ

Import

# 必要に応じてインストールを行う
#!pip install networkx > /dev/null
%%time

import numpy as np
import pandas as pd
from gc import collect                                # ガーベッジコレクション
from colorama import Fore, Style, init                # Pythonの文字色指定ライブラリ
from IPython.display import display_html, clear_output
import matplotlib.pyplot as plt
%matplotlib inline

import networkx as nx                                 # ネットワーク可視化
%%time

# テキスト出力の設定
def PrintColor(text:str, color = Fore.GREEN, style = Style.BRIGHT):
    print(style + color + text + Style.RESET_ALL);

# displayの表示設定
pd.set_option('display.max_columns', 50);
pd.set_option('display.max_rows', 50); 

print()
collect()

Knock51:データを読み込んで利用データを読み込む

%%time

"""データ読み込み"""
factories  = pd.read_csv('tbl_factory.csv',     index_col = 0)
warehouses = pd.read_csv('tbl_warehouse.csv',   index_col = 0)
cost       = pd.read_csv('rel_cost.csv',        index_col = 0)
trans      = pd.read_csv('tbl_transaction.csv', index_col = 0)

print()
collect()
%%time

"""読み込んだデータを確認"""
PrintColor(f'\n factories')
display(factories.head())

PrintColor(f'\n warehouses')
display(warehouses.head())

PrintColor(f'\n cost')
display(cost.head())

PrintColor(f'\n trans')
display(trans.head())

print()
collect()

transデータとcostデータを結合していきます。ただし、今回は二つのデータにおいて、共通する列名がありません。しかし、データを見るとFCID=ToFC、FromWH=FromWHであることは明らかであり、データの意味を考えても、これらをキーとして結合すればよいことがわかります。よって、left_on

%%time

join_data = pd.merge(trans,
                     cost,
                     left_on  = ['ToFC', 'FromWH'], # left-dataのtransのToFC列とFromWH列をキーとする
                     right_on = ['FCID', 'WHID'],   # right-dataのcostのFCID列とWHID列をキーとする
                     how      = 'left'              # 左外部結合を行う
                    )

# 確認用
PrintColor(f'\n join_data')
display(join_data)

print()
collect()

さらに、join_dataに工場のデータを付与します。

%%time

join_data = pd.merge(join_data,
                     factories,
                     left_on  = 'ToFC',
                     right_on = 'FCID',
                     how      = 'left'
                    )
"""
既に先ほどの結合でFCIDを持っているものの、今回はtransデータを基準として結合している。
外部結合の性質上、join_dataのFCID列が欠損値を持っている可能性もあるため(本来はあり得ないが)、
ここは基準データのtransデータのToFC列をキーとして結合する方が適切である。
"""

"""読み込んだデータを確認"""
PrintColor(f'\n join_data')
display(join_data)

print()
collect()

さらに倉庫情報も付与し、直観的に見やすいように列データをの並び替えも行います。

%%time

join_data = pd.merge(join_data,
                     warehouses,
                     left_on  = 'FromWH',
                     right_on = 'WHID',
                     how      = 'left'
                    )

# 列データを並び替える
join_data = join_data[['TransactionDate',
                       'Quantity',
                       'Cost',
                       'ToFC',
                       'FCName',
                       'FCDemand',
                       'FromWH',
                       'WHName',
                       'WHSupply',
                       'WHRegion']]

"""読み込んだデータを確認"""
PrintColor(f'\n join_data')
display(join_data)

print()
collect()

次に、関東支社と東北支社のデータを比較するためにそれぞれのデータを抽出します。

%%time

# 関東支社のみのデータ
kanto  = join_data.loc[join_data['WHRegion'] == '関東']
# 東北支社のみのデータ
tohoku = join_data.loc[join_data['WHRegion'] == '東北']

"""読み込んだデータを確認"""
PrintColor(f'\n 関東支社のみのデータ')
display(kanto)

PrintColor(f'\n 東北支社のみのデータ')
display(tohoku)

print()
collect()

Knock52:現状の輸送量、コストを確認してみる

実際に1年間に輸送した部品数やそれに掛かったコストを集計します。

%%time

# 輸送実績の総コスト集計結果
PrintColor(f'\n 輸送実績の総コスト集積結果')
print('関東支社の総コスト:' + str(kanto['Cost'].sum()) + '万円')
print('東北支社の総コスト:' + str(tohoku['Cost'].sum()) + '万円')

# 輸送実績の総輸送部品個数集計結果
PrintColor(f'\n 輸送実績の総輸送部品個数集計結果')
print('関東支社の総部品輸送個数:' + str(kanto['Quantity'].sum()) + '個')
print('東北支社の総部品輸送個数:' + str(tohoku['Quantity'].sum()) + '個')

# 輸送部品の1つ当たりの輸送コスト
PrintColor(f'\n 輸送部品の1つ当たりの輸送コスト')
tmp_kanto  = (kanto['Cost'].sum() / kanto['Quantity'].sum()) * 10000
tmp_tohoku = (tohoku['Cost'].sum() / tohoku['Quantity'].sum()) * 10000
print('関東支社の部品1つ当たりの輸送コスト:' + str(int(tmp_kanto)) + '円')
print('東北支社の部品1つ当たりの輸送コスト:' + str(int(tmp_tohoku)) + '円')

# コストデータから支社ごとの平均輸送コストの算出
PrintColor(f'\n コストデータから支社ごとの平均輸送コストの算出')
cost_chk = pd.merge(cost,
                    factories,
                    on  = 'FCID',
                    how = 'left'
                   )
print('関東支社の平均輸送コスト:' + str(cost_chk['Cost'].loc[cost_chk['FCRegion'] == '関東'].mean()) + '万円')
print('東北支社の平均輸送コスト:' + str(cost_chk['Cost'].loc[cost_chk['FCRegion'] == '東北'].mean()) + '万円')

print()
collect()

Knock53:ネットワークを可視化してみよう

%%time

"""グラフオブジェクトの作成"""
G = nx.Graph()

# 頂点の設定
G.add_node('nodeA')
G.add_node('nodeB')
G.add_node('nodeC')

# 辺の設定
G.add_edge('nodeA', 'nodeB')
G.add_edge('nodeA', 'nodeC')
G.add_edge('nodeB', 'nodeC')

# 座標の設定
pos = {}
pos['nodeA'] = (0, 0)
pos['nodeB'] = (1, 1)
pos['nodeC'] = (0, 1)

# 描画
nx.draw(G, pos)

# 保存
plt.savefig('network-fig0.png')

# 表示
plt.show()

print()
collect()

Knock54:ネットワークにノード(頂点)を追加する

%%time

# ノードの追加
G.add_node('nodeD')
G.add_edge('nodeA', 'nodeD')
pos['nodeD'] = (1, 0)

# 描画
nx.draw(G, pos, with_labels = True)

# 保存
plt.savefig('network-fig1.png')

# 表示
plt.show()

print()
collect()

Knock55:ルートの重みづけを実施する

ノード(頂点)間のリンクの太さを変える(重みづけ)方法を学びます。

%%time

"""データ読み込み"""
df_w = pd.read_csv('network_weight.csv')
df_p = pd.read_csv('network_pos.csv')

"""読み込んだデータを確認"""
PrintColor(f'\n network_weight(df_w)')
display(df_w.head())

PrintColor(f'\n network_pos(df_p)')
display(df_p.head())

print()
collect()
%%time

"""グラフオブジェクトの作成"""
G = nx.Graph()

# 頂点の設定
for i in range(len(df_w.columns)):
    G.add_node(df_w.columns[i])

# 辺の設定とエッジの重みのリスト化
size         = 10
edge_weights = []
num_pre      = 0    # テキストでは欠落している一行

for i in range(len(df_w.columns)):
    for j in range(len(df_w.columns)):
        if not (i == j):
            # 辺の追加
            G.add_edge(df_w.columns[i], df_w.columns[j])
            
            # 以下のif文のコードは、テキストでは欠落している部分なので要注意です
            if num_pre < len(G.edges):
                num_pre = len(G.edges)
                # エッジの重みの追加
                edge_weights.append(df_w.iloc[i][j] * size)

"""読み込んだデータを確認"""
#display(edge_weights)
#print(len(edge_weights))    # len(G.edges)と一致するはず
#print(G.edges)
#print(len(G.edges))         # len(edges_weights)と一致するはず

# 座標の設定
pos = {}
for i in range(len(df_w.columns)):
    node      = df_w.columns[i]
    pos[node] = (df_p[node][0], df_p[node][1])

# 描画
nx.draw(G,
        pos,
        with_labels = True,
        font_size   = 16,
        node_size   = 1000,
        node_color  = 'k',
        font_color  = 'w',
        width       = edge_weights
       )

# 保存
plt.savefig('network-fig2.png')

# 表示
plt.show()

print()
collect()

Knock56:輸送ルート情報を読み込む

まずは現状でどの倉庫からどの工場へどれだけの量の輸送が行われているのか記録したデータを読み込みます。

%%time

df_tr = pd.read_csv('trans_route.csv', index_col = '工場')

"""読み込んだデータを確認"""
PrintColor(f'\n ルート情報の読み込み(df_tr)')
display(df_tr.head())

print()
collect()

Knock57:輸送ルート情報からネットワークを可視化する

%%time

"""データの読み込み"""
df_tr  = pd.read_csv('trans_route.csv', index_col = '工場')
df_pos = pd.read_csv('trans_route_pos.csv')

"""読み込んだデータを確認"""
PrintColor(f'\n df_tr')
display(df_tr.head())

PrintColor(f'\n df_pos')
display(df_pos.head())

print()
collect()
%%time

# グラフオブジェクトの作成
G = nx.Graph()

# 頂点の設定
for i in range(len(df_pos.columns)):
    G.add_node(df_pos.columns[i])

# 辺の設定とエッジの重みのリスト化
num_pre      = 0
edge_weights = []
size         = 0.1

for i in range(len(df_pos.columns)):
    for j in range(len(df_pos.columns)):
        
        if not (i == j):
            # 辺の追加
            G.add_edge(df_pos.columns[i], df_pos.columns[j])
            # エッジの重みの追加
            
            if num_pre < len(G.edges):
                num_pre = len(G.edges)
                weight  = 0
                
                if (df_pos.columns[i] in df_tr.columns) and (df_pos.columns[j] in df_tr.index):
                    
                    if df_tr[df_pos.columns[i]][df_pos.columns[j]]:
                        weight = df_tr[df_pos.columns[i]][df_pos.columns[j]] * size
                
                elif(df_pos.columns[j] in df_tr.columns) and (df_pos.columns[i] in df_tr.index):
                    
                    if df_tr[df_pos.columns[j]][df_pos.columns[i]]:
                        weight = df_tr[df_pos.columns[j]][df_pos.columns[i]] + size
                
                edge_weights.append(weight)

# 座標の設定
pos = {}
for i in range(len(df_pos.columns)):
    node      = df_pos.columns[i]
    pos[node] = (df_pos[node][0], df_pos[node][1])

# 描画
nx.draw(G,
        pos,
        with_labels = True,
        font_size   = 16,
        node_size   = 1000,
        node_color  = 'k',
        font_color  = 'w',
        width       = edge_weights
       )

# 保存
plt.savefig('network-fig3.png')

# 表示
plt.show()

print()
collect()

Knock58:輸送コスト関数を作成する

輸送ルートを最適なものにするために、輸送コストを下げられる効率的な輸送ルートがあることを仮定します。この仮定を立証するために輸送コストを計算する関数を作成します。

%%time

"""データの読み込み"""
df_tr = pd.read_csv('trans_route.csv', index_col = '工場')
df_tc = pd.read_csv('trans_cost.csv',  index_col = '工場')

"""読み込んだデータを確認"""
PrintColor(f'\n df_tr')
display(df_tr.head())

PrintColor(f'\n df_tc')
display(df_tc.head())

print()
collect()
%%time

"""輸送コスト関数"""
def trans_cost(df_tr, df_tc):
    cost = 0

    for i in range(len(df_tc.index)):
        for j in range(len(df_tr.columns)):
            cost += df_tr.iloc[i][j] * df_tc.iloc[i][j]
    return cost

print('総輸送コスト:' + str(trans_cost(df_tr, df_tc)))

print()
collect()

Knock59:制約条件を作る

次に、輸送コスト関数を最低化していくうえでの制約条件(各倉庫の供給可能な部品数の上限や、各工場ごとの最低限の製品製造量)について考えます。

%%time

"""データ読み込み"""
df_tr     = pd.read_csv('trans_route.csv', index_col = '工場')
df_demand = pd.read_csv('demand.csv')
df_supply = pd.read_csv('supply.csv')

"""読み込んだデータを確認"""
PrintColor(f'\n df_tr')
display(df_tr.head())

PrintColor(f'\n df_demand')
display(df_demand.head())

PrintColor(f'\n df_supply')
display(df_supply.head())

print()
collect()
%%time

# 需要側の制約条件
for i in range(len(df_demand.columns)):
    temp_sum = sum(df_tr[df_demand.columns[i]])
    print(str(df_demand.columns[i]) + 'への輸送量:' + str(temp_sum) + '(需要量:' + str(df_demand.iloc[0][i]) + ')')

    if temp_sum >= df_demand.iloc[0][i]:
        print('需要量を満たしています。')
    else:
        print('需要量を満たしていません。輸送ルートを再計算してください。')

# 供給側の制約条件
for i in range(len(df_supply.columns)):
    temp_sum = sum(df_tr.loc[df_supply.columns[i]])
    print(str(df_supply.columns[i]) + 'からの輸送量:' + str(temp_sum) + '(供給限界:' + str(df_supply.iloc[0][i]) + ')')

    if temp_sum <= df_supply.iloc[0][i]:
        print('供給限界の範囲内です。')
    else:
        print('供給限界を超過しています。輸送ルートを再計算してください。')

print()
collect()

Knock60:輸送ルートを変更して、輸送コスト関数の変化を確認する。

%%time

"""データ読み込み"""
df_tr_new = pd.read_csv('trans_route_new.csv', index_col = '工場')

"""読み込んだデータを確認"""
PrintColor(f'\n df_tr_new')
display(df_tr_new)

print()
collect()
%%time

# 総輸送コスト再計算
print('総輸送コスト(変更後):' + str(trans_cost(df_tr_new, df_tc)))

"""制約条件計算関数"""
# 需要側
def condition_demand(df_tr, df_demand):
    flag = np.zeros(len(df_demand.columns))
    
    for i in range(len(df_demand.columns)):
        temp_sum = sum(df_tr[df_demand.columns[i]])

        if (temp_sum >= df_demand.iloc[0][i]):
            flag[i] = 1
    
    return flag

# 供給側
def condition_supply(df_tr, df_supply):
    flag = np.zeros(len(df_supply.columns))
    
    for i in range(len(df_supply.columns)):
        temp_sum = sum(df_tr.loc[df_supply.columns[i]])

        if temp_sum <= df_supply.iloc[0][i]:
            flag[i] = 1

    return flag

print('需要条件計算結果:' + str(condition_demand(df_tr_new, df_demand)))
print('供給条件計算結果:' + str(condition_supply(df_tr_new, df_supply)))

print()
collect()

実践データ分析記事一覧

Discussion