파이썬 데이터 분석 실무 테크닉 100 -최적화(3)
테크닉 051~060을 통해 최적의 물류 계획을 세우기 위한 흐름을 배워보았다.
만약 "당신이 가지고 있는 데이터 분석 기술을 이용해서 우리 회사의 경영 현황을 계산해주세요"라는 의뢰를 받으면 그 업무를 어떻게 수행할까?
이 장에서는 몇 개의 라이브러리를 이용해 최적화 계산을 진행할 것이다.
고객 요청 | 회사의 제조에서 물류까지의 전체 흐름 중 어디에 비용 개선 가능성이 있는지 분석해 주셨으면 합니다. |
전제조건 : 대리점(P, Q)가 있고 판매되는 상품군(제품 A, B)에는 일정 수요가 예측되어 있어, 이 수요량을 근거로 공장(공장 X, Y)에서의 생산량을 결정한다. 각 공장에서 대리점으로까지의 운송비, 재고 비용들을 고려해서 결정
테크닉 061 : 운송 최적화 문제를 풀어보자
운송 최적화 문제를 풀어볼 것이다. 이용할 라이브러리는 pulp, ortoolpy이다. pulp는 최적화 모델을 작성하고, ortoolpy는 목적함수를 생성하여 최적화 문제를 푸는 역할을 한다.
import numpy as np
import pandas as pd
from itertools import product
from pulp import LpVariable, lpSum, value
from ortoolpy import model_min, addvars, addvals
# 데이터 불러오기
df_tc = pd.read_csv('data/6장/trans_cost.csv', index_col = '공장')
df_demand = pd.read_csv('data/6장/demand.csv')
df_supply = pd.read_csv('data/6장/supply.csv')
# 초기 설정
np.random.seed(1)
nw = len(df_tc.index)
nf = len(df_tc.columns)
pr = list(product(range(nw), range(nf)))
# 수리 모델 작성
m1 = model_min()
v1 = {(i, j):LpVariable('v%d_%d'%(i, j), lowBound = 0) for i, j in pr}
m1 += lpSum(df_tc.iloc[i][j] * v1[i, j] for i, j in pr)
for i in range(nw):
m1 += lpSum(v1[i, j] for j in range(nf)) <= df_supply.iloc[0][i]
for j in range(nf):
m1 += lpSum(v1[i, j] for i in range(nw)) >= df_demand.iloc[0][j]
m1.solve()
# 총 운송 비용 계산
df_tr_sol = df_tc.copy()
total_cost = 0
for k, x in v1.items():
i, j = k[0], k[1]
df_tr_sol.iloc[i][j] = value(x)
total_cost += df_tc.iloc[i][j] * value(x)
print(df_tr_sol)
print('총 운송 비용 : '+str(total_cost))
위의 코드에서 제일 중요한 부분은 '수리 모델 작성' 부분이다. m1의 경우 '최소화를 실행하는 모델'이다. 이제부터 정의하는 목적함수를 제약 조건 하에서 '최소화'할 수 있다.
그리고 m1에 목적함수와 제약 조건을 추가한다. 목적함수 m1을 lpSum을 이용해 정의하고, 각 운송 경로의 비용을 저장한 df_tc와 주요 변수 v1과의 각 요소의 곱의 합으로 목적함수를 정의한다. v1은 LpVariable을 사용해 딕셔너리 형식으로 정의한다.
제약 조건 또한 lpSum을 이용해서 정의하였다. 여기서는 공장이 제조할 제품 수요량을 만족시키고 창고가 제공할 부품이 제공 한계를 넘지 않게 제약 조건을 주었다.
이 조건으로 최적화 문제를 solve로 해결한다.
solve를 실행하면 변수 v1이 최적화되고 최적의 총 운송 비용이 구해진다. 최적화 계산 결과, 총 운송 비용은 1296(만 원)이며, 저번 시간에 계산한 총 운송 비용 1428(만 원)과 비교할 때 크게 비용이 절감된 것을 알 수 있다.
테크닉 062 : 최적 운송 경로를 네트워크로 확인하자
import matplotlib.pyplot as plt
import networkx as nx
# 데이터 불러오기
df_tr = df_tr_sol.copy()
df_pos = pd.read_csv('data/6장/trans_route_pos.csv')
# 객체 생성
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.show()
최적화 계산 후 가시화한 결과를 보면 테크닉 057에서 작성한 '거의 완전한 결합'의 네트워크와 차이가 확실히 보인다. 창고 W1에서 공장 F1으로, 창고 W2에서 공장 F3으로, 창고 W3에서 공장 F2와 F4로의 공급이 대부분이다.
그 외의 공급은 최소한으로 제한되있다. 테크닉 057에서 세운 '운송 경로는 어느 정도 집중돼야 한다.'라는 가설이 최적화 계산에 의해 밝혀졌다고 할 수 있다.
테크닉 063 : 최적 운송 경로가 제약 조건을 만족하는지 확인하자
테크닉 060에서 작성한 제약 조건을 계산하는 함수를 이용해서 계산된 운송 경로가 제약 조건을 만족하는지 확인하자.
# 데이터 불러오기
df_demand = pd.read_csv('data/6장/demand.csv')
df_supply = pd.read_csv('data/6장/supply.csv')
# 제약 조건 계산 함수
# 수요축
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_sol, df_demand)))
print('공급 조건 계산 결과 : '+str(condition_supply(df_tr_sol, df_supply)))
수요 쪽도 공급 쪽도 모든 제약 조건이 1로 충족되고 있음을 알 수 있다. 최적화 문제에는 다양한 종류가 있고 모든 최적화 문제가 반드시 풀린다고 말할 수는 없지만, 운송 최적화 문제처럼 선형 최적화로 정식화할 수 있는 것은 비교적 짧은 시간에 정답을 구할 수 있다.
테크닉 064 : 생산 계획 데이터를 불러오자
지금까지 운송 비용 최적화를 계산했다. 이번엔 생산 계획을 세워보자.
No. | 파일 이름 | 개요 |
1 | product_plan_material | 제품 제조에 필요한 원료 비율 |
2 | product_plan_profit | 제품 이익 |
3 | product_plan_stock | 원료 재고 |
4 | product_plan | 제품 생산량 |
df_material = pd.read_csv('data/7장/product_plan_material.csv', index_col = '제품')
print(df_material)
df_profit = pd.read_csv('data/7장/product_plan_profit.csv', index_col = '제품')
print(df_profit)
df_stock = pd.read_csv('data/7장/product_plan_stock.csv', index_col = '항목')
print(df_stock)
df_plan = pd.read_csv('data/7장/product_plan.csv', index_col = '제품')
print(df_plan)
material을 보면 두 종류의 제품(제품1, 제품2)와 그것들을 제조하는데 필요한 원료(원료1, 원료2, 원료3)의 비율이 저장되어 있다. profit의 경우 각 제품의 이익이 저장돼있다. stock은 각 원료의 재고가 저장돼있고, plan에는 제품의 생산량이 저장되어있다. 살펴보면 현재는 이익이 큰 제품1만 생산하고 있으며, 제품2는 생산하지 않고 있다. 원료가 효과적으로 사용되지 않고 있기 때문에 제품2의 생산량을 늘린다면 이익을 높일 수 있을 것이다.
테크닉 065 : 이익을 계산하는 함수를 만들자
생산계획 최적화를 푸는 방법은 최적화 문제의 일반적인 흐름과 동일하다. 먼저 목저함수와 제약 조건을 정의하고, 제약 조건 아래서 목적함수를 최소화하는 변수의 조합을 찾는다.
def product_plan(df_profit, df_plan):
profit = 0
for i in range(len(df_profit.index)):
for j in range(len(df_plan.columns)):
profit += df_profit.iloc[i][j] * df_plan.iloc[i][j]
return profit
print('총 이익 : '+str(product_plan(df_profit, df_plan)))
생산 계획의 총 이익은 각 제품의 이익과 제조량과의 곱의 합으로 계산할 수 있다. 총이익은 80(만 원)이다. 이것은 제품1만 제조한 결과이므로 제품2를 늘리면 어느 정도 이익이 증가하는지 계산해보자.
테크닉 066 : 생산 최적화 문제를 풀어보자
from ortoolpy import model_max
df = df_material.copy()
inv = df_stock
m = model_max()
v1 = {(i) : LpVariable('v%d'%(i), lowBound=0) for i in range(len(df_profit))}
m += lpSum(df_profit.iloc[i]*v1[i] for i in range(len(df_profit)))
for i in range(len(df_material.columns)):
m+= lpSum(df_material.iloc[j, i]* v1[j] for j in range(len(df_profit))) <= df_stock.iloc[:, i]
m.solve()
df_plan_sol = df_plan.copy()
for k, x in v1.items():
df_plan_sol.iloc[k] = value(x)
print(df_plan_sol)
print('총 이익 : '+str(value(m.objective)))
코드를 살펴보면 model_max를 선언해 '최대화'를 선언하고, v1을 제품 수와 같은 차원으로 정의하고, v1과 제품별 이익의 곱의 합으로 목적함수를 정의하고, 제약 조건을 정의한다. 각 원료의 사용량이 재고를 넘지 않게 제약 조건을 정의한다.
결과를 보면 제품1의 생산량을 16에서 15로 1을 줄이고 제품2의 생산량을 5로 늘렸으며, 이익을 95만 원까지 늘릴 수 있다는 것을 알 수 있었다.