Positive-Influence-Data

[SmartFarm] 스마트팜코리아 데이터로 생육 유사도 측정 (유사도 측정편) 본문

Analysis

[SmartFarm] 스마트팜코리아 데이터로 생육 유사도 측정 (유사도 측정편)

DS쟁이 2025. 5. 2. 12:54
우리 농가의 생육 패턴은 다른 농가와 얼마나 비슷할까?
이 질문에 답을 주기 위해 필자는 생육 유사도라는 지표를 계산한다.

 

👉 이번 글에서는 앞서 전처리한 스마트팜 생육 데이터를 활용해, DTW(Dynamic Time Warping) 알고리즘을 이용한   
     생육 유사도 측정 방법을 소개한다.

✔ DTW(Dynamic Time Warping)

생육 유사도를 측정하기에 앞서 필자가 사용할 DTW(Dynamic Time Warping)에 대해 간단하게 설명하고 넘어가겠다.

📌 DTW(Dynamic Time Warping)는 시계열 데이터 간의 유사도를 측정하는 데 널리 사용되는 알고리즘이다.
      예를 들어, 두 농가에서 딸기를 재배했을 때 생육 시점은 다르더라도 비슷한 성장 패턴을 보였다면,
      DTW는 이런 "시점 차이를 보정" 해주면서 유사한 패턴임을 인식할 수 있다.

📌 왜 DTW를 사용하는가?
      일반적인 거리 계산(예: 유클리드 거리)은 두 시계열의 동일한 시간축을 전제로 한다.
      하지만 실제 생육 데이터에서는 다음과 같은 차이가 존재한다.
      A 농가는 2월 초에 정식,
      B 농가는 2월 말에 정식
     특정 생육단계가 빠르거나 느리게 진행 또는 조사 간격이 동일하지 않음.

👉 이처럼 "타이밍이 달라도 흐름은 비슷할 수 있는" 경우가 많기 때문에,
      DTW는 시점이 달라도 유사한 패턴을 정렬해서 비교할 수 있는 강력한 도구이다.

👉 알고리즘 자체에 대한 자세한 내용은 링크를 들어가 보자.

 

✔ 생육 데이터 유사도 측정 준비

📌 필자는 스마트팜코리아의 시설원예 데이터셋 중에서 2023년 딸기 데이터를 필자의 농가라고 생각하고 DTW를 적용해서 2023년 딸기 농가데이터 중 일부와 농촌진흥청 생육 데이터의 생육 유사도를 측정해 보겠다.

 

필자는 PF_0024357_01 농가의 생육 데이터를 다운로드하였다. 데이터가 어떻게 생겼는지 엑셀로 열어서 확인해 보니

PF_0024357_01 생육데이터

조사일, 단체표준명, 단체표준영문영 이렇게 세줄이 하나의 컬럼으로 작용하는 거 같다.

필자가 판단하기에 가장 윗부분 조사일, 주차, 초장(mm)... 이게 필요할 거 같다. 
엑셀에서 삭제할 수도 있지만 필자는 Python을 통해서 들어오는 생육 데이터마다 일괄적으로 적용하는 것을 목표로 하기 때문에 Python으로 프로그래밍하여 처리하겠다.

import re

list_sample_data = os.listdir("./sample_data")
sample_data = pd.read_excel(f'./sample_data/{list_sample_data[0]}', header=[0, 1, 2])
# 데이터를 불러오는데 위의 세줄(아까 언급했던 3개의 칼럼)에 대해서 모두 헤더 처리를 한다.
# 그럼 컬럼은 (초장(mm), 초장, PlantHeight) 이렇게 한 묶음이 된다.

def clean_column(col):
    return re.sub(r"\(.*?\)", "", col[0]).strip()
# 컬럼을 정리하는 함수인데 위에서 불러온 (초장(mm), 초장, PlantHeight)에서 첫번째만 불러와서
# "초장(mm)" 여기에서 "초장" 이렇게 바꿔주는 함수이다.

sample_data.columns = [clean_column(col) for col in sample_data.columns]
# 각 컬럼별로 적용한다.

 

 

sample_data

실행했을 때 이렇게 정리된 모습으로 나온다.

📌 필자는 딸기농가 대상으로 DTW를 측정할 예정이기 때문에 일단 딸기로 픽스, 설정하겠다.

CFG = {'crop_name':'딸기'}

if CFG['crop_name'] == '딸기':
    nogjinchoen_df = crop_dfs['딸기']
    hangmoks = ['초장','엽장','엽폭','엽수','엽병장','관부직경']
elif CFG['crop_name'] == '토마토':
    nogjinchoen_df = crop_dfs['토마토']
    hangmoks = ['초장','생장길이','줄기굵기','엽장','엽폭','엽수','화방높이']
elif CFG['crop_name'] == '방울토마토':
    nogjinchoen_df = crop_dfs['방울토마토']
    hangmoks = ['초장','생장길이','줄기굵기','엽장','엽폭','엽수','화방높이']
elif CFG['crop_name'] == '오이':
    nogjinchoen_df = crop_dfs['오이']
    hangmoks = ['초장','마디수','줄기굵기','엽장','엽폭','엽수']
elif CFG['crop_name'] == '파프리카':
    nogjinchoen_df = crop_dfs['파프리카']
    hangmoks = ['초장','생장길이','줄기굵기','엽장','엽폭','엽수']

👉 딸기는 초장, 엽장, 엽폭, 엽수, 엽병장, 관부직경 총 6개의 항목이 공통적이다. 이 6개의 항목을 토대로 DTW를 적용하겠다.

✔ 생육 데이터 유사도 측정 

📌 DTW 전략
1.  농촌진흥청 데이터가 2018년 ~ 2021년까지 총 4개년의 데이터이므로 4년의 데이터를 먼저 쪼갠다.
2.  농촌진흥청 데이터에는 여러 개의 농가가 있으므로 연도별로 농촌진흥청 데이터의 각 항목 평균, 최대, 최소 데이터를 산출한다. (예시: 2018년 딸기 초장의 최소 150mm, 평균 200mm, 최대 350mm)
3. 비교하려는 농가(필자는 PF_0024357_01)와 주차를 일치시킨다. 
(예시: PF_0024357_01 농가는 4주 차 ~ 31주 차까지 생육 데이터가 있음, 농촌진흥청 데이터도 4주 차 ~ 31주 차까지 데이터를 맞춰준다.)
4. 각 조사항목별로 비교하려는 농가와 연도별 농촌진흥청 평균 데이터와 DTW 수행(비교농가와 농촌진흥청 간의 DTW 거리산출)
5. 연도별 농촌진흥청 최대 데이터와 연도별 농촌진흥청 최소 데이터와 DTW 수행(최악의 경우, DTW 최대거리 산출)
6. 연도별로 각 조사항목의 DTW거리, 최대거리를 하나로 그룹핑하여 평균화
7. (1 - (DTW 거리 / DTW 최대거리))*100 공식으로 농촌진흥청 대비 해당 농가의 각 조사항목이 얼마나 유사한지 %로 표시

👉 DTW를 총 7단계에 거쳐서 실행해 보겠다.

 

❓왜 생육유사도는 (1 - (DTW 거리 / DTW 최대거리))*100 공식일까?
👉 DTW 거리 = 비교 농가와 평균 농가의 차이 정도
👉 DTW 최대거리 = 해당 항목의 최악의 차이 (비슷하지 않은 정도의 최대치)
👉 (1 - 거리비율) = 유사도로 변환
👉 ×100 = 직관적인 퍼센트(%)로 표현

👉 즉, DTW 거리 대비 최대 거리를 이용해 100에서 차이 비율을 빼면,
두 조사항목 간의 유사성을 직관적으로 퍼센트로 표현할 수 있기 때문

 

from tslearn.metrics import dtw, dtw_path

dtw_total = pd.DataFrame()
# DTW 정보를 저장 할 데이터 프레임

for year in nogjinchoen_df['crps_year'].unique():
    temp = nogjinchoen_df[nogjinchoen_df['crps_year']==year]
    # 1번 항목으로 연도별로 농촌진흥청 데이터를 쪼갠다
    
    dtw_njc = temp.groupby(['주차'])[hangmoks].mean().reset_index() # # 농촌진흥청 데이터의 주차, 항목별 조사항목 퍙균값
    dtw_njc_min = temp.groupby(['주차'])[hangmoks].min().reset_index() # 농촌진흥청 데이터의 주차, 항목별 조사항목 최소값
    dtw_njc_max = temp.groupby(['주차'])[hangmoks].max().reset_index() # 농촌진흥청 데이터의 주차, 항목별 조사항목 최대값
    dtw_njc.fillna(method='bfill',inplace=True)
    dtw_njc_min.fillna(method='bfill',inplace=True)
    dtw_njc_max.fillna(method='bfill',inplace=True)
    # 2번 항목으로 농촌진흥청 데이터의 조사항목별 최소, 평균, 최대값 산출
    
    dtw_distance = []
    dtw_max_distance = []
    dtw_hangmoks = []
    # DTW의 거리, 최대거리, 조사항목을 넣을 리스트
    
    for hangmok in hangmoks:
    	# 3번과 4번 5번 항목으로 비교하려는 농가와 농촌진흥청의 주차를 맞춰준다.
        A = dtw_njc[(dtw_njc['주차']<=sample_data['주차'].max()) & (dtw_njc['주차']>=sample_data['주차'].min())][hangmok]
        ## 비교하려는 농가 주차의 최대값과 최소값을 찾아서 그 범위에 있는 농촌진흥청 데이터를 추출하여 A라는 변수에 넣는다.(농촌진흥청 주차, 조사항목별 평균데이터)
        
        A_max = dtw_njc_max[(dtw_njc_max['주차']<=sample_data['주차'].max()) & (dtw_njc_max['주차']>=sample_data['주차'].min())][hangmok]
        A_min = dtw_njc_min[(dtw_njc_min['주차']<=sample_data['주차'].max()) & (dtw_njc_min['주차']>=sample_data['주차'].min())][hangmok]
        ## 비교하려는 농가 주차의 최대값과 최소값을 찾아서 그 범위에 있는 농촌진흥청 데이터를 추출하여 A라는 변수에 넣는다.(농촌진흥청 주차, 조사항목별 최소, 최대 데이터)

        
        B = sample_data.groupby(['주차'])[hangmok].mean()
        ## 비교하는 농가의 주차 조사항목별 평균 데이터
        
        path ,distance = dtw_path(A, B)
        max_path, max_distance = dtw_path(A_max, A_min)
        ## path = 조사하려는 농가와 농촌진흥청 데이터의 DTW 
        ## max_path = 농촌진흥청 최소 데이터와 농촌진흥청 최대 데이터의 DTW(최악의 DTW결과)
        
        dtw_distance.append(distance)
        dtw_max_distance.append(max_distance)
        dtw_hangmoks.append(hangmok)
    
    dtw_df = pd.DataFrame({'항목':hangmoks,
                           '거리':dtw_distance,
                           '최대거리':dtw_max_distance})
    # dtw_df를 만들고 데이터프레임으로 만들어줌
    
    dtw_df['years'] = year
    dtw_total = pd.concat([dtw_total, dtw_df])
    # dtw_total에 dtw_df를 계속 넣어줌
    
dtw_calc = dtw_total.groupby(['항목'])['거리','최대거리'].mean()
# 6번 항목으로 연도별로 dtw_total에 있는 것을 하나의 평균으로 통합

dtw_calc['생육유사도'] = round((1-(dtw_calc['거리']/dtw_calc['최대거리']))*100,2)
dtw_calc.reset_index()
# 7번 항목으로 생육유사도 측정
## 생육유사도 = (1 - (각 조사항목별 거리 / 각 조사항목별 최대거리))*100 이고 소수점 2자리까지 반올림

 

📌 DTW 결과

항목 거리 최대거리 생육유사도(%)
관부직경 850.858914 1674.362759 49.18
엽병장 144.919577 1327.416629 89.08
엽수 27.482090 226.485856 87.87
엽장 58.633136 617.418810 90.50
엽폭 38.818240 509.025882 92.37
초장 217.124423 1895.705919 88.55

각 항목마다 거리, 최대거리, 생육유사도가 측정되어 저장되었다.

결과를 살펴보면 거리가 작을수록 생육유사도가 크다.

즉, 농촌진흥청 생육 데이터와 비교 농가와의 DTW 거리가 차이가 안 난다는 뜻이다.

관부 직경의 경우 49.18%로 생육 유사도가 작지만 다른 항목들이 농촌진흥청 데이터와 유사하므로 크게 걱정할 필요는 없어 보인다.

✔ 생육 유사도 시각화

표로만 봐서는 사실 한눈에 보기 어렵고 와닿기 어려울 수 있다.
간단하게 시각화해서 각 항목별로 수치가 얼마나 되는지 보겠다.

hangmoks = dtw_calc['항목'].tolist()
similarities = dtw_calc['생육유사도'].tolist()

angles = np.linspace(0, 2 * np.pi, len(hangmoks), endpoint=False).tolist()
similarities += similarities[:1]
angles += angles[:1]

fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))
ax.plot(angles, similarities, color='tab:blue', linewidth=2)
ax.fill(angles, similarities, color='tab:blue', alpha=0.25)

for angle, sim in zip(angles, similarities):
    ax.text(angle, sim + 3, f'{sim:.1f}%', ha='center', va='center', fontsize=10)

ax.set_yticks([20, 40, 60, 80, 100])
ax.set_yticklabels(['20%', '40%', '60%', '80%', '100%'])
ax.set_xticks(angles[:-1])
ax.set_xticklabels(hangmoks, fontproperties="Malgun Gothic")

plt.tight_layout()
plt.show()

 

👉 위의 코드를 실행하면 차트가 나오게 된다. 필자가 원하는 시각화가 나왔다.

👉 이 시각화는 각 항목별로 얼마나 생육 패턴이 유사한지를 한눈에 보여준다.

👉 유사도가 높을수록 농촌진흥청 기준 데이터와 비슷하게 관리되었다는 뜻이 된다.

 

지금까지 필자는 스마트팜 생육 데이터를 기반으로 DTW(Dynamic Time Warping) 알고리즘을 활용해 농가 생육 데이터와 농촌진흥청 데이터를 비교하고, 생육 유사도를 수치화해 보았다.

 

이 과정은 다음과 같은 상황에서 유용하게 활용될 수 있다:

  • 우리 농가의 생육 데이터가 "표준 생육 패턴"과 얼마나 유사한지 평가하고 싶을 때
  • 새로운 재배 전략을 도입했을 때, 생육 패턴이 이전보다 좋아졌는지를 정량적으로 판단하고 싶을 때
  • 여러 농가 간 유사도를 분석해, 유사한 생육 전략을 그룹핑하거나 벤치마킹하고 싶을 때

👉  추후에 개발까지 하여 파일을 업로드하면 자동으로 생육유사도가 나올 수 있게 개발을 할 예정이다.

 

📌 스마트팜코리아 데이터로 생육 유사도 측정 시리즈

👉이전편

2025.04.20 - [Analysis] - [SmartFarm] 스마트팜코리아 데이터로 생육 유사도 측정 (기획편)

2025.04.25 - [Analysis] - [SmartFarm] 스마트팜코리아 데이터로 생육 유사도 측정 (수집&처리편)

 

👉다음편

2025.06.12 - [Analysis] - [SmartFarm] 스마트팜코리아 데이터로 생육 유사도 측정(배포편)

Comments