Yours Ever, Data Chronicles

파이썬 생산계획 최적화 (python pulp, ortoolpy) / 파이썬 데이터 분석 실무 테크닉 100 본문

Data Science/Analysis Study

파이썬 생산계획 최적화 (python pulp, ortoolpy) / 파이썬 데이터 분석 실무 테크닉 100

Everly. 2022. 4. 29. 17:06

저번 포스팅까지는 '운송 비용 최적화'를 진행했습니다.

"운송비용을 가장 최소화시키기 위해 창고-공장 간 이동하는 물품 수를 어떻게 조정할 것인가?" 에 대한 문제를 풀어보았죠. 

이를 통해 최적해, 최적값을 구할 수 있었습니다. 또한 결과를 바탕으로, 집중할 경로는 더 집중하고, 아닌 경로는 덜 집중하는 방법이 최적임을 파악했습니다.

 

이번 포스팅부턴 '생산 계획 최적화'를 진행합니다. 

이는 "어떤 제품을, 얼마나 만들 것인가?" 에 대한 최적화입니다. 어렵지만 재밌는 최적화의 세계로 빠져봅시다! ><

 

사진 출처: 정책주간지 공감(gonggam.korea.kr)

 

✔Table of Contents

     

    Tech 64. 데이터 불러오기

    여기서 사용하는 데이터는 다음과 같습니다.

     

    •  product_plan_material.csv : 제품 제조에 필요한 원료 비율
    • product_plan_profit.csv : 제품 이익
    • product_plan_stock.csv : 원료의 재고량
    • product_plan.csv : 제품의 생산량

     

    import pandas as pd
    
    df_m = pd.read_csv('7장/product_plan_material.csv', index_col = '제품')
    df_p = pd.read_csv('7장/product_plan_profit.csv', index_col = '제품')
    df_s = pd.read_csv('7장/product_plan_stock.csv', index_col = '항목')
    df_plan = pd.read_csv('7장/product_plan.csv', index_col = '제품')
    
    display(df_m, df_p, df_s, df_plan)

     

    각각 제품내 원료비율, 제품당 이익(=판매가-원가), 원료 재고량, 제품 생산량을 의미합니다.

     

    데이터를 보면, 이 공장은 2개의 제품(제품1, 제품2)를 생산합니다. 그리고 이것들을 제조하는 데엔 3개의 원료(원료1, 원료2, 원료3)가 사용되고 있죠.

    현재 제품 생산은 이익이 큰 제품1만 생산되고, 제품2는 아예 생산이 안 되고 있습니다. → 이익을 늘리기 위해선 제품2의 생산량을 늘리는 게 필요하겠군요! (얼마나 늘리는 게 좋을까요?)

     

    앞의 운송최적화 문제에서 했던 대로 목적함수와 제약조건을 정의하고, 제약조건 하 목표를 최대화하는 최적값을 찾아봅시다.


    Tech 65. 목적 함수 만들기

    여기서 사용할 목적함수는 '이익을 계산하는 함수'가 되겠죠. 우리는 이익을 최대화하는 것이 목표니까요!

    그래서 이 목적함수를 최대화하는 최적값을 찾는 게 우리의 과제입니다.

     

    어떻게 총이익을 계산하면 될까요? → 직감적으로 생각해보면 알 수 있듯이, 총이익은 각 제품별 이익*생산량의 합을 계산하면 됩니다.

     

    def product_plan(df_profit, df_plan):
        profit = 0
        for i in range(len(df_profit.index)):
            profit += df_profit.iloc[i][0] * df_plan.iloc[i][0] #이익 * 생산량 
        return profit
        
    print('총이익: ', product_plan(df_p, df_plan))

     

    책에나온 코드와 살짝 다르게 짰습니다.

    이 사례에선 생산되는 제품이 단 2개밖에 없죠. 그러니 제품이익인 df_p(df_profit)와 제품생산량인 df_plan 데이터를 활용해 곱 연산을 해줍니다.

    어차피 데이터프레임이 작아 한개의 열만 있기 때문에, for문을 하나로만 구성했습니다. 

     

    계산해보면 5*16 + 4*0 = 80이 되겠죠!

     


    Tech 66. 생산 최적화 문제 풀기 (pulp, ortoolpy 라이브러리 활용)

    앞서 만든 목적함수 product_plan을 최대화하는 최적값과 최적해를 찾아봅시다. 역시 문제를 푸는 흐름은 앞서 운송비용 최적화 문제와 비슷합니다.

     

    단, 여기서 주의해야 할 점! ★최적해는 무엇일까요?

     

    위의 그림을 보면 알 수 있듯, 제품별 이익은 이미 정해져 있어서 우리가 어찌할 수 없는 영역이죠.

    우리가 바꿀 수 있는 것은 '생산량' 뿐입니다. 

    최대 이익을 얻을 수 있는 '제품별 생산량' 값을 우리는 '최적해' 로 두겠습니다.

    이미 생산되고 있는 제품별 생산량 값은 'df_plan' 데이터에 있죠. 기존엔 제품1만 생산중이었습니다. 이를 최적화하면 몇 개를 생산하는 것이 최적인지를 알아봅시다. 그리고 그 때의 총이익 값이 최적값이 됩니다.

     


     

    그리고 ★목적 함수에 대응하는 제약조건은 무엇일까요?

    바로, 원료의 양이겠죠. 솔직히 생산량을 무한대로 늘리면 총이익은 무한대로 늘어날 것입니다. 

    하지만 우리는 제품을 만드는 데 드는 원료의 양이 제한되어 있죠(df_s 데이터). 이를 제약 조건으로 사용합니다.

     

    편리하게 최적화를 하기 위해서 이번에도 pulp, ortoolpy 라이브러리의 힘을 빌려봅시다. 

     

    앞에서 한 것처럼

    1) 목적 함수 m2를 만들고, 제약 조건 추가하기 

    2) 최적해 v2 만들기

    3) 최적값(최대화된 총이익) 구하기

     

    순서대로 진행해봅시다.

     

    import pandas as pd
    from pulp import LpVariable, lpSum, value
    from ortoolpy import model_max, addvars, addvals
    
    m2 = model_max() #목적함수 - 이익 최대화가 목적
    v2 = {(i) : LpVariable('v%d'%(i), lowBound = 0) for i in range(len(df_p))}
    print(v2)

     

    먼저 목적함수 m2를 정의했습니다. 이번엔 '최대화'가 목적이니 model_max를 사용했습니다.

    그리고 최적해 v2를 정의합니다. 이 값은 제품별 생산량이므로 v0, v1 이렇게 2개 제품의 생산량입니다.

     

    #이제 m2에 제약조건을 하나씩 추가해보자.
    
    ##각 제품별 이익*생산량 
    m2 += lpSum(df_p.iloc[i] * v2[i] for i in range(len(df_p))) 
    
    ##제약조건
    for j in range(len(df_m.columns)): #j: 원료 0,1,2 / i: 제품 0, 1
        m2 += lpSum(df_m.iloc[i, j] * v2[i] for i in range(len(df_p))) <= df_s.iloc[:, j] #제약조건: 각 제품에 필요한 생산량*원료합 <= 최대 원료 재고량
    
    m2.solve() #최적해 구해줘!

     

    다음은 목적함수를 구하는 식을 추가합니다. 목적함수인 m2는 각 제품별 이익*생산량의 합입니다. 이 값을 첫번째 코드에 추가해줍니다. 

     

    다음으로는 제약조건을 추가합니다. 각 제품의 생산량과 거기에 사용되는 원료의 양을 합한 것이, 원료의 재고보다 더 많으면 안됩니다. (물건을 못만드니까요.) 그래서 원료 재고 이하로 설정합니다.

     

    이제 pulp 라이브러리가 알아서 구해준 최적해 값이 무엇인지 볼까요?

    df_plan_sol = df_plan.copy()
    display(df_plan_sol) #원래 생산계획
    
    for k, x in v2.items():
        df_plan_sol.iloc[k] = value(x) #v2 최적해 
    display(df_plan_sol) #최적으로 찾아낸 생산계획 (생산량)

     

    위는 원래 생산계획(df_plan), 아래는 최적의 생산계획(df_plan_sol) 입니다. 즉, 최적해입니다.

    원래는 제품1만 16개 생산했던 것을, 제품1을 15개, 제품2를 5개 생산하는 것이 가장 이익을 극대화한다고 계산해주었군요!

     

    그럼 이 때의 최적값(총이익)은 얼마일까요?

    print(value(m2.objective))

     

    원래 80이었던 총이익이 95로 늘어났습니다! 최적화가 잘 되었네요~!

     


    Tech 67. 최적 생산계획이 제약조건을 만족하는가?

    책에서는 말합니다. 

    "최적화 문제를 풀 때 가장 주의할 점은, 최적화 계산을 한 결과를 이해하지 않고 그냥 받아들이면 안 된다는 점이다."

     

    그냥 라이브러리가 다 해결해 준다고, 이걸 그냥 받아들이면 안됩니다. 항상 꼼꼼한 검산! 데싸라면 항상 기억해야 합니다.

     

    display(df_m, df_s, df_plan)
    # df_m은 제품에 사용되는 원료비율, df_s는 원료 재고량, df_plan은 현재 제품 생산량(최적해 아님)

     

    그럼 제약조건을 만족하는지 확인해보죠. 우리가 만든 제약조건으로 규정한 "'각 원료의 사용량'이 재고를 효율적으로 이용하고 있는가?" 를 알아봅시다.

    그래서 각 원료 1,2,3에 대해, 최적해(최적의 생산량)보다 원료의 재고량이 더 많아야겠죠! 즉, 최적해가 재고량 이하인지를 알아보고, 조건을 만족하면 1, 아니면 0을 출력합니다.

     

    # 제약조건으로 규정한 '각 원료의 사용량'이 '재고를 효율적으로 이용하고 있는가'를 알아보자.
    
    def cond_stock(df_plan, df_material, df_stock):
        flag = np.zeros(len(df_material.columns)) #초기값
        
        for i in range(len(df_material.columns)): #i: 원료(0,1,2)
            temp_sum = 0
            for j in range(len(df_material.index)): #j: 제품(0,1)
                #각 원료에 대해 제품의 생산량*사용량 => temp_sum: 각 원료 총사용량
                temp_sum = temp_sum + df_material.iloc[j][i] * float(df_plan.iloc[j]) 
            if (temp_sum <= float(df_stock.iloc[0][i])): 
                flag[i] = 1 
            print(df_material.columns[i] + ' 사용량: ' +str(temp_sum) + ', 재고: ' + str(float(df_stock.iloc[0][i])))
            
        return flag

     

    코드를 보면 아시겠지만, temp_sum은 각 원료별 사용량을 구하게 됩니다. 그리고 이를 df_stock(원료 재고량)과 비교를 해서, temp_sum이 더 작거나 같다면 1(제약조건 만족)을 출력하죠.

     

    실제 생산량인 df_plan과, 우리가 구한 최적해 df_plan_sol을 각각 넣어 결과를 비교해봅시다.

     

    #출력: 원래의 df_plan 생산계획 하에서는..
    cond_stock(df_plan, df_m, df_s)

     

    실제 생산량 df_plan을 넣으면, 결과가 1로 나옵니다. 즉, 제약조건은 만족했다는 뜻이죠.

    하지만 원료의 사용량과 재고를 비교해봅시다. 너무 많이 남고 있어요! 원료 3만 재고에 가깝게 쓰고 있고, 원료 1,2는 재고가 너무 많이 남는군요. 이걸 얼른 써야 돈을 벌 텐데 !

     

    #최적화 이후 df_plan_sol 생산계획 하에서는..
    cond_stock(df_plan_sol, df_m, df_s)

     

    하지만 최적화를 적용한다면? 역시 제약조건은 모두 만족합니다. 그리고 원료 2,3의 재고는 완전히 다 쓰고 있습니다. 원료1이 아직 많이 남는 게 아쉽긴 하지만요. 

    그래도 이전에 비해서는 훨씬 효율적으로 원료를 쓰는 것을 알 수 있습니다.

     


    이렇게 하여 7장의 '운송비용 최적화', '생산계획 최적화' 두 가지 최적화 문제를 풀어보았습니다.

    하지만 실제 현장에선 이렇게 따로 최적화 문제를 푸는 게 아니라, 두 가지 문제를 동시에 고려해야 합니다.

    그래서 다음 포스팅에선 이 2개를 모두 최적화한, "물류 네트워크를 설계하는 문제"를 ortoolpy 라이브러리를 사용해 진행하겠습니다. 

     

    읽어주셔서 감사합니다! ♥

    반응형