<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Open Brain</title>
    <link>https://gguzunhagae.tistory.com/</link>
    <description>안녕하세요. 데이터 사이언스를 공부하는 대학원생입니다.
제가 공부한 지식을 기록하고 공유하기 위해 만들어진 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 26 May 2026 07:40:22 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>월요일zoa</managingEditor>
    <item>
      <title>yaml 파일이란?</title>
      <link>https://gguzunhagae.tistory.com/84</link>
      <description>&lt;p data-path-to-node=&quot;12,0&quot; data-ke-size=&quot;size16&quot;&gt;Docker Compose를 통해 Airflow 환경을 구축하다 보면 반드시 보게 되는 파일이 있습니다. 바로 .yaml 파일입니다. 프로젝트의 핵심 설정이 담기는 yaml 파일은 현대 개발 생태계에서 빼놓을 수 없는 요소입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;12,0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12,0&quot; data-ke-size=&quot;size16&quot;&gt;저도 부트캠프 최종 프로젝트에서 Airflow를 구축하며 처음으로 docker-compose.yaml 파일을 마주했습니다. 가이드에 따라 파일을 설치하고 실행하니 마법처럼 Airflow가 구동되었습니다. 하지만 문득 궁금해졌습니다. &lt;b data-index-in-node=&quot;126&quot; data-path-to-node=&quot;10,0&quot;&gt;이 파일 안에 가득한 지시어들은 정확히 어떤 의미일까? 왜 하필 .yaml 파일일까?&lt;/b&gt; 무심코 지나칠 수 있었던 YAML에 대해 제가 공부한 내용을 정리해 보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12,1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12,1&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 프로젝트 경험을 바탕으로 &lt;b data-index-in-node=&quot;24&quot; data-path-to-node=&quot;12,1&quot;&gt;YAML의 정의부터 주요 특징, 그리고 왜 많은 도구들이 YAML을 설정 파일 형식으로 채택하는지&lt;/b&gt;를 심도 있게 다뤄보고자 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1551&quot; data-origin-height=&quot;871&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biYUqd/dJMcabXehRR/pdcHurkXhD5P4RdED6FN1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biYUqd/dJMcabXehRR/pdcHurkXhD5P4RdED6FN1k/img.png&quot; data-alt=&quot;airflow의 docker-compose.yaml 파일 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biYUqd/dJMcabXehRR/pdcHurkXhD5P4RdED6FN1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiYUqd%2FdJMcabXehRR%2FpdcHurkXhD5P4RdED6FN1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1551&quot; height=&quot;871&quot; data-origin-width=&quot;1551&quot; data-origin-height=&quot;871&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;airflow의 docker-compose.yaml 파일 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;airflow docker install&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;yaml이란&lt;/h2&gt;
&lt;p data-path-to-node=&quot;4,0&quot; data-ke-size=&quot;size16&quot;&gt;YAML은 원래 'Yet Another Markup Language(또 다른 마크업 언어)'의 약자였지만, 나중에는 'YAML Ain't Markup Language(YAML은 마크업 언어가 아니다)'라는 뜻으로 이름이 바뀌었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4,1&quot; data-ke-size=&quot;size16&quot;&gt;이걸 꼭 알아야 하냐고요? 아뇨. 하지만 전 꼭 말해드리고 싶었습니다. 어쨌든, YAML은 Docker Compose나 Kubernetes 같은 많은 유명한 도구들의 설정을 구성하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size16&quot;&gt;그리고 괄호나 태그(acorn)를 사용해 형식을 맞추는 JSON, XML과는 달리, YAML은 &lt;b&gt;'공백 들여쓰기'&lt;/b&gt;를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768268252775&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# yaml(공백 중심)
user:
  name: &quot;dong-ho&quot;
  role: &quot;ML engineer&quot;
  skills:
    - Python
    - Docker
    
# json(괄호 중심)
{
  &quot;user&quot;: {
    &quot;name&quot;: &quot;dong-ho&quot;,
    &quot;role&quot;: &quot;ML engineer&quot;,
    &quot;skills&quot;: [&quot;Python&quot;, &quot;Docker&quot;]
  }
}

# XML(태그 중심)
&amp;lt;user&amp;gt;
  &amp;lt;name&amp;gt;dong-ho&amp;lt;/name&amp;gt;
  &amp;lt;role&amp;gt;ML engineer&amp;lt;/role&amp;gt;
  &amp;lt;skills&amp;gt;
    &amp;lt;skill&amp;gt;Python&amp;lt;/skill&amp;gt;
    &amp;lt;skill&amp;gt;skill&amp;gt;Docker&amp;lt;/skill&amp;gt;
  &amp;lt;/skills&amp;gt;
&amp;lt;/user&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Scalars(data type)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yaml에서 scalars는 단일 값을 나타내는 기본 단위이며, Yaml은 데이터 타입을 명시하지 않아도 알아서 판단하는 똑똑한 능력이 있습니다. Sclars의 종류는 총 5가지가 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;문자열(string): 텍스트 데이터. 따옴표 없이 표현 가능하며, 특수문자나 공백이 포함되면 따옴표 사용&lt;/li&gt;
&lt;li&gt;숫자 (Number): 정수와 부동소수점. 8진수(0o), 16진수(0x), 지수 표기법 지원&lt;/li&gt;
&lt;li&gt;불린 (Boolean): true/false, yes/no, on/off 등 여러 표현 가능&lt;/li&gt;
&lt;li&gt;Null: 값이 없음을 나타냄. null, ~, 빈 값으로 표현&lt;/li&gt;
&lt;li&gt;날짜/시간: ISO 8601 형식 지원&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1768269324530&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Integer
positive: 34
zero: 0
negative: -12
hex: 0xDEADBEEF

# Boolean
# recommended boolean usage
is_gold: true
is_released: false
# legacy boolean usage(avoid)
is_gold: on
is_released: off

# Date
# YYYY-MM-DD
created: 2027-02-11
# YYYY-MM-DD HH:mm:ss
created: 2027-02-11 11:02:56

# Float
positive: 3.14
negative: -8.6
infinity: .inf
not-a-number: .nan&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문자열(strings) 표현법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YAML 스칼라 중 가장 강력하고 복잡한 것이 문자열입니다. 따옴표를 쓰느냐 안 쓰느냐에 따라 의미가 달라집니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 68px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;방식&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;예시&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;Plain (따옴표 없음)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;name: Gemini&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;가장 간결함. 특수문자가 포함되면 오류 가능성 있음.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;Single Quotes ('' )&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;'Hello \n World' &amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,2,0&quot;&gt;있는 그대로&lt;/b&gt; 인식. \n을 줄바꿈으로 해석하지 않음.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;Double Quotes (&quot; &quot;)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&quot;Hello \n World&quot;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,3,2,0&quot;&gt;이스케이프 시퀀스&lt;/b&gt; 해석. \n을 실제 줄바꿈으로 인식함.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 줄 스칼라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose나 Airflow 설정을 하다 보면 긴 스크립트를 써야 할 때가 있습니다. 이때 사용하는 두 가지 기호가 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 리터럴 스타일 (|): &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;줄바꿈을 포함하여 작성한 모양 그대로를 저장합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768269032286&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;description: |
  첫 번째 줄입니다.
  두 번째 줄입니다.
  (줄바꿈이 유지됩니다.)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 폴디드 스타일 (&amp;gt;): 작성할 때는 여러 줄이지만, 실제 데이터로 읽을 때는 중간의 줄바꿈을 공백 하나로 바꿉니다. (문단 끝의 줄바꿈만 유지)&lt;/p&gt;
&lt;pre id=&quot;code_1768269060867&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;summary: &amp;gt;
  이 내용은 여러 줄로
  작성되었지만 실제로는
  한 줄의 문장으로 읽힙니다.&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Mapping(object)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;YAML에서 데이터를 구조화하는 가장 기본적인 단위는 &lt;b&gt;매핑(Mapping)&lt;/b&gt;입니다. 우리가 흔히 프로그래밍에서 말하는 '객체'와 같은 개념입니다. 재미있는 점은 YAML이 '공백'에 매우 민감하다는 것입니다. 단순히 글자를 적는 게 아니라, 콜론 뒤에 한 칸을 띄우고 아래 줄에서 들여쓰기를 하는 그 '빈 공간'이 데이터 간의 부모-자식 관계를 결정합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;Airflow 설정 파일에서 services: 아래에 airflow-webserver: 같은 항목이 들어가는 것도 바로 매핑 원리를 이용한 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768269707403&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Yaml
info:
    lang: python
    version: 3.14.1
    type: open-source

# json
{
    &quot;info&quot;: {
    	&quot;lang&quot;: &quot;python&quot;
        &quot;version&quot;: &quot;3.14.1&quot;
        &quot;type&quot;: &quot;open-source&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sequence(List)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yaml에서 배열을 생성하기 위해서는 dash나 공백을 사용합니다. 배열은 파이썬에서 사용하는 그것과 같은 개념으로 순서가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 블록 스타일: 가장 일반적입니다. &lt;b&gt;dash 뒤에 반드시 공백&lt;/b&gt;이 있어야 합니다. 공백을 사용하지 않으면 문자열로 인식됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768269912432&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# block style
language:
    - python
    - java
    - go&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 괄호 스타일: json과 유사하게 괄호를 사용하는 방식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768270018036&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# flow style
language: [python, java, go]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sequence와 Mapping의 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 docker compose나 airflow 설정에서는 '객체들을 담은 리스트' 형태를 가장 많이 마주합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768270345056&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;employee
    - name: jnb
      role: designer
    - name: kxn
      role: pm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;employee라는 객체 안에 두 개의 sequence가 들어가 있는 형태입니다. 각 dash는 새로운 시작점입니다. &lt;b&gt;이때도 마찬가지로 dash 후에 꼭 공백을 작성해야 합니다!&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;docker-compose.yaml 파일 분석&lt;/h2&gt;
&lt;pre id=&quot;code_1768270521793&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.8'  # [스칼라] 키-값 쌍의 버전 정보

services:        # [매핑] 서비스들을 담는 최상위 객체
  airflow-webserver:  # [매핑] 웹서버 서비스 정의
    image: apache/airflow:2.7.1  # [스칼라] 이미지 이름
    restart: always              # [스칼라] 재시작 정책
    
    volumes:      # [리스트] 여러 경로를 연결하기 위한 목록
      - ./dags:/opt/airflow/dags      # [리스트 항목 1]
      - ./logs:/opt/airflow/logs      # [리스트 항목 2]
      - ./plugins:/opt/airflow/plugins # [리스트 항목 3]

    environment:  # [매핑] 환경 변수 설정 객체
      _PIP_ADDITIONAL_DEPENDENCIES: &quot;pandas numpy&quot; # [스칼라]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;구조적 계층&lt;/b&gt;: services라는 큰 바구니(매핑) 안에 airflow-webserver라는 작은 바구니가 있고, 그 안에 다시 이미지 이름(스칼라)과 볼륨 목록(리스트)이 담겨 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;들여쓰기의 마법:&lt;/b&gt; 별도의 괄호가 없어도 들여쓰기만으로 volumes가 airflow-webserver에 속한 설정이라는 것을 명확히 알 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Airflow가 yaml 파일을 사용하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow에서 docker compose를 위해서 yaml을 사용하는 이유는 명확한 기술적 이점이 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;① 개발자를 배려하는 '가독성'&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;Docker Compose나 Airflow의 설정은 인프라 구조를 정의합니다. JSON처럼 { }와 ,가 가득하면 눈이 피로하고 구조를 한눈에 파악하기 어렵지만, &lt;b data-index-in-node=&quot;91&quot; data-path-to-node=&quot;10&quot;&gt;YAML은 문서처럼 읽히기 때문에&lt;/b&gt; 복잡한 컨테이너 간의 관계를 파악하는 데 최적입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;② 주석(#)의 존재 (가장 큰 이유 중 하나)&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;JSON은 공식적으로 주석을 지원하지 않습니다. 하지만 인프라 설정 파일에는 &quot;이 포트를 왜 열었는지&quot;, &quot;이 볼륨은 왜 연결했는지&quot; 기록하는 것이 필수적입니다. YAML은 #을 통해 상세한 설명을 남길 수 있어 &lt;b data-index-in-node=&quot;119&quot; data-path-to-node=&quot;12&quot;&gt;협업과 유지보수에 유리&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;③ 유연한 데이터 표현&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;Airflow는 DAG(작업 흐름)를 정의할 때 환경 변수나 의존성을 복잡하게 설정해야 합니다. YAML은 문자열을 여러 줄로 쓰거나(|), 복잡한 리스트 안에 객체를 넣는 작업을 기호의 방해 없이 깔끔하게 처리할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;④ 인프라 업계의 표준 (Infrastructure as Code)&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;Kubernetes, Docker 등 현대 인프라 도구들은 모두 YAML을 기본으로 채택하고 있습니다. 따라서 Airflow 환경을 구축할 때 YAML을 사용하는 것은 &lt;b data-index-in-node=&quot;103&quot; data-path-to-node=&quot;16&quot;&gt;다른 도구들과의 호환성 및 생태계 통합&lt;/b&gt; 면에서 매우 유리한 선택입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;결국 YAML이 Docker와 Airflow에서 사랑받는 이유는 컴퓨터도 잘 알아듣지만, 사람에게 가장 친절한 언어이기 때문입니다. 부트캠프 프로젝트를 진행하며 무심코 복사했던 그 코드들 속에 들여쓰기와 콜론 하나하나가 인프라의 뼈대를 구성하는 정교한 설계도였다는 것을 이해하니 설정 파일이 더 이상 어렵게 느껴지지 않았습니다.&lt;/p&gt;</description>
      <category>Data Engineering</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/84</guid>
      <comments>https://gguzunhagae.tistory.com/84#entry84comment</comments>
      <pubDate>Tue, 13 Jan 2026 11:20:39 +0900</pubDate>
    </item>
    <item>
      <title> [Blog series] Airflow로 구축하는 NASA 배터리 파이프라인-3.2</title>
      <link>https://gguzunhagae.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 3.1에서 이어지는 ML/DL 후속 Airflow dag 설명 글입니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DAG 코드 주요 로직 및 엔지니어링 포인트 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dag 3: LOF build and experiment&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bapkqO/dJMcacBOh52/UofHHXarQOr1MsT2kisaKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bapkqO/dJMcacBOh52/UofHHXarQOr1MsT2kisaKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bapkqO/dJMcacBOh52/UofHHXarQOr1MsT2kisaKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbapkqO%2FdJMcacBOh52%2FUofHHXarQOr1MsT2kisaKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;299&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번째 DAG는 가공된 데이터를 바탕으로 &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;4&quot;&gt;LOF(Local Outlier Factor)&lt;/b&gt; 모델을 학습시키고, 최적의 임계치(Threshold)를 설정하여 결과를 저장하는 &lt;b data-index-in-node=&quot;94&quot; data-path-to-node=&quot;4&quot;&gt;MLOps의 핵심 워크플로우&lt;/b&gt;를 수행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1767853914525&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from airflow.decorators import dag, task
from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
from airflow.providers.amazon.aws.hooks.s3 import S3Hook
from datetime import datetime
import os
import json
import pandas as pd
import numpy as np

from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import LocalOutlierFactor
import joblib


# ----------------------------
# Config (battery_dag_02_load.py 패턴 준수)
# ----------------------------
AWS_CONN_ID = &quot;aws_conn&quot;
SNOWFLAKE_CONN_ID = &quot;snowflake_conn&quot;

S3_BUCKET = &quot;bucket&quot;
S3_PREFIX = &quot;battery/ml_lof/&quot;  # ML 결과 저장 prefix

BATTERY_ID = &quot;B0005&quot;

SNOWFLAKE_DB = &quot;BATTERY_DATABASE&quot;
SNOWFLAKE_SCHEMA = &quot;RAW_DATA&quot;

# battery_dag_02_load.py가 생성/적재하는 LOWESS 결과 테이블
LOWESS_TABLE = f&quot;BATTERY_{BATTERY_ID}_LOWESS&quot;  # BATTERY_B0005_LOWESS

# ML 결과 적재 테이블
RESULT_TABLE = f&quot;BATTERY_{BATTERY_ID}_LOF_RESULTS&quot;  # BATTERY_B0005_LOF_RESULTS

SNOWFLAKE_INTERNAL_STAGE_PATH = &quot;@~/battery_upload&quot;

# b0005.ipynb에서 사용한 피처 구성(원본 + lowess 파생)
FEATURE_COLS = [
    &quot;Voltage_measured&quot;, &quot;Current_measured&quot;, &quot;Temperature_measured&quot;,
    &quot;Current_load&quot;, &quot;Voltage_load&quot;,
    &quot;Voltage_measured_smooth&quot;, &quot;Voltage_measured_residual&quot;, &quot;Voltage_measured_trend&quot;,
    &quot;Current_measured_smooth&quot;, &quot;Current_measured_residual&quot;, &quot;Current_measured_trend&quot;,
    &quot;Temperature_measured_smooth&quot;, &quot;Temperature_measured_residual&quot;, &quot;Temperature_measured_trend&quot;,
    &quot;Current_load_smooth&quot;, &quot;Current_load_residual&quot;, &quot;Current_load_trend&quot;,
    &quot;Voltage_load_smooth&quot;, &quot;Voltage_load_residual&quot;, &quot;Voltage_load_trend&quot;,
]

# quantile 후보(Validation 기준)
THRESHOLD_QUANTILES = [0.99, 0.995, 0.999]
DEFAULT_THRESHOLD_Q = 0.995

# LOF 하이퍼파라미터(노트북 기본 흐름 반영)
LOF_N_NEIGHBORS = 30


def _safe_float_df(df: pd.DataFrame, cols: list[str]) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;문자/NULL 혼입 방어: 숫자 변환 불가값은 NaN 처리 후 drop.&quot;&quot;&quot;
    out = df.copy()
    for c in cols:
        out[c] = pd.to_numeric(out[c], errors=&quot;coerce&quot;)
    return out

# =========================================================
# 공통 유틸 (Snowflake 대문자 문제 완전 차단)
# =========================================================
def snowflake_select_expr(cols: list[str]) -&amp;gt; str:
    &quot;&quot;&quot;
    Snowflake 실제 컬럼은 대문자,
    pandas 컬럼은 원래 이름 유지
    &quot;&quot;&quot;
    return &quot;,\n    &quot;.join([f&quot;{c.upper()} AS {c}&quot; for c in cols])


@dag(
    dag_id=&quot;battery_dag_03_ml_lof&quot;,
    start_date=datetime(2024, 12, 1),
    schedule=None,
    catchup=False,
    tags=[&quot;battery&quot;, &quot;ml&quot;, &quot;lof&quot;, &quot;anomaly&quot;, &quot;lowess&quot;],
)
def battery_ml_lof_pipeline():
    @task
    def extract_lowess_from_snowflake() -&amp;gt; str:
        hook = SnowflakeHook(snowflake_conn_id=SNOWFLAKE_CONN_ID)

        sql = f&quot;&quot;&quot;
            SELECT
                CYCLE_IDX,
                VOLTAGE_MEASURED,
                CURRENT_MEASURED,
                TEMPERATURE_MEASURED,
                CURRENT_LOAD,
                VOLTAGE_LOAD,
                VOLTAGE_MEASURED_SMOOTH,
                VOLTAGE_MEASURED_RESIDUAL,
                VOLTAGE_MEASURED_TREND,
                CURRENT_MEASURED_SMOOTH,
                CURRENT_MEASURED_RESIDUAL,
                CURRENT_MEASURED_TREND,
                TEMPERATURE_MEASURED_SMOOTH,
                TEMPERATURE_MEASURED_RESIDUAL,
                TEMPERATURE_MEASURED_TREND,
                CURRENT_LOAD_SMOOTH,
                CURRENT_LOAD_RESIDUAL,
                CURRENT_LOAD_TREND,
                VOLTAGE_LOAD_SMOOTH,
                VOLTAGE_LOAD_RESIDUAL,
                VOLTAGE_LOAD_TREND
            FROM {SNOWFLAKE_DB}.{SNOWFLAKE_SCHEMA}.{LOWESS_TABLE}
            ORDER BY CYCLE_IDX
        &quot;&quot;&quot;

        df = hook.get_pandas_df(sql)

        # 디버깅
        print(&quot;[DEBUG raw Snowflake columns]:&quot;, df.columns.tolist())

        rename_map = {
            &quot;CYCLE_IDX&quot;: &quot;cycle_idx&quot;,
            &quot;VOLTAGE_MEASURED&quot;: &quot;Voltage_measured&quot;,
            &quot;CURRENT_MEASURED&quot;: &quot;Current_measured&quot;,
            &quot;TEMPERATURE_MEASURED&quot;: &quot;Temperature_measured&quot;,
            &quot;CURRENT_LOAD&quot;: &quot;Current_load&quot;,
            &quot;VOLTAGE_LOAD&quot;: &quot;Voltage_load&quot;,
            &quot;VOLTAGE_MEASURED_SMOOTH&quot;: &quot;Voltage_measured_smooth&quot;,
            &quot;VOLTAGE_MEASURED_RESIDUAL&quot;: &quot;Voltage_measured_residual&quot;,
            &quot;VOLTAGE_MEASURED_TREND&quot;: &quot;Voltage_measured_trend&quot;,
            &quot;CURRENT_MEASURED_SMOOTH&quot;: &quot;Current_measured_smooth&quot;,
            &quot;CURRENT_MEASURED_RESIDUAL&quot;: &quot;Current_measured_residual&quot;,
            &quot;CURRENT_MEASURED_TREND&quot;: &quot;Current_measured_trend&quot;,
            &quot;TEMPERATURE_MEASURED_SMOOTH&quot;: &quot;Temperature_measured_smooth&quot;,
            &quot;TEMPERATURE_MEASURED_RESIDUAL&quot;: &quot;Temperature_measured_residual&quot;,
            &quot;TEMPERATURE_MEASURED_TREND&quot;: &quot;Temperature_measured_trend&quot;,
            &quot;CURRENT_LOAD_SMOOTH&quot;: &quot;Current_load_smooth&quot;,
            &quot;CURRENT_LOAD_RESIDUAL&quot;: &quot;Current_load_residual&quot;,
            &quot;CURRENT_LOAD_TREND&quot;: &quot;Current_load_trend&quot;,
            &quot;VOLTAGE_LOAD_SMOOTH&quot;: &quot;Voltage_load_smooth&quot;,
            &quot;VOLTAGE_LOAD_RESIDUAL&quot;: &quot;Voltage_load_residual&quot;,
            &quot;VOLTAGE_LOAD_TREND&quot;: &quot;Voltage_load_trend&quot;,
        }

        df = df.rename(columns=rename_map)

        # 방어 로직
        if &quot;cycle_idx&quot; not in df.columns:
            raise ValueError(f&quot;cycle_idx missing after rename. columns={df.columns.tolist()}&quot;)

        out_path = f&quot;/tmp/{BATTERY_ID}_lowess_ml_input.csv&quot;
        df.to_csv(out_path, index=False)

        print(f&quot;[OK] Extracted LOWESS for ML: rows={len(df)}&quot;)
        return out_path
    @task
    def validate_ml_data(file_path: str) -&amp;gt; str:
        &quot;&quot;&quot;
        ML 입력 검증:
        - cycle_idx 존재
        - feature 컬럼 존재
        - 결측/비수치 방어(필요시 drop)
        &quot;&quot;&quot;
        df = pd.read_csv(file_path)

        assert len(df) &amp;gt; 0, &quot;Empty dataframe&quot;
        assert &quot;cycle_idx&quot; in df.columns, &quot;cycle_idx missing&quot;

        missing = [c for c in FEATURE_COLS if c not in df.columns]
        assert len(missing) == 0, f&quot;Missing feature cols: {missing}&quot;

        # 숫자 변환(에러는 NaN) 후 결측 drop
        df = _safe_float_df(df, FEATURE_COLS)
        before = len(df)
        df = df.dropna(subset=[&quot;cycle_idx&quot;] + FEATURE_COLS).copy()
        after = len(df)

        assert after &amp;gt; 0, &quot;All rows dropped after numeric coercion / NaN removal&quot;

        # cycle_idx는 int로 캐스팅
        df[&quot;cycle_idx&quot;] = pd.to_numeric(df[&quot;cycle_idx&quot;], errors=&quot;coerce&quot;).astype(int)

        clean_path = f&quot;/tmp/{BATTERY_ID}_lowess_for_ml_clean.csv&quot;
        df.to_csv(clean_path, index=False)

        print(f&quot;✓ Validation passed: before={before}, after={after}, saved={clean_path}&quot;)
        return clean_path

    @task
    def train_score_lof(clean_path: str) -&amp;gt; str:
        &quot;&quot;&quot;
        cycle 기반 6:2:2 split
        - scaler fit(train)
        - LOF fit(train, novelty=True)
        - train/val/test score 산출
        결과(행 단위) 저장 후 path 반환
        &quot;&quot;&quot;
        df = pd.read_csv(clean_path)
        df[&quot;cycle_idx&quot;] = df[&quot;cycle_idx&quot;].astype(int)

        cycle_list = sorted(df[&quot;cycle_idx&quot;].unique().tolist())
        total_cycles = len(cycle_list)
        assert total_cycles &amp;gt;= 10, f&quot;Too few cycles for split: total_cycles={total_cycles}&quot;

        train_cycles = int(total_cycles * 0.6)
        val_cycles = int(total_cycles * 0.8)  # train+val

        train_threshold_cycle = cycle_list[train_cycles - 1]
        val_threshold_cycle = cycle_list[val_cycles - 1]

        train_df = df[df[&quot;cycle_idx&quot;] &amp;lt;= train_threshold_cycle].copy()
        val_df = df[(df[&quot;cycle_idx&quot;] &amp;gt; train_threshold_cycle) &amp;amp; (df[&quot;cycle_idx&quot;] &amp;lt;= val_threshold_cycle)].copy()
        test_df = df[df[&quot;cycle_idx&quot;] &amp;gt; val_threshold_cycle].copy()

        print(f&quot;총 Cycle 수: {total_cycles}&quot;)
        print(f&quot;Train: &amp;lt;= {train_threshold_cycle} (cycles={train_cycles}) rows={len(train_df)}&quot;)
        print(f&quot;Val:   ({train_threshold_cycle}, {val_threshold_cycle}] rows={len(val_df)}&quot;)
        print(f&quot;Test:  &amp;gt;  {val_threshold_cycle} rows={len(test_df)}&quot;)

        # Feature matrix
        X_train = train_df[FEATURE_COLS].values
        X_val = val_df[FEATURE_COLS].values
        X_test = test_df[FEATURE_COLS].values

        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)
        X_val_scaled = scaler.transform(X_val)
        X_test_scaled = scaler.transform(X_test)

        lof = LocalOutlierFactor(
            n_neighbors=LOF_N_NEIGHBORS,
            contamination=&quot;auto&quot;,
            novelty=True,
        )
        lof.fit(X_train_scaled)

        # 점수: 클수록 이상(outlier)으로 해석하기 위해 음수부호 처리 흐름을 유지
        train_scores = -lof.negative_outlier_factor_
        val_scores = -lof.score_samples(X_val_scaled)
        test_scores = -lof.score_samples(X_test_scaled)

        # 결과 DF (행 단위)
        train_out = train_df[[&quot;cycle_idx&quot;]].copy()
        train_out[&quot;split&quot;] = &quot;train&quot;
        train_out[&quot;score&quot;] = train_scores

        val_out = val_df[[&quot;cycle_idx&quot;]].copy()
        val_out[&quot;split&quot;] = &quot;val&quot;
        val_out[&quot;score&quot;] = val_scores

        test_out = test_df[[&quot;cycle_idx&quot;]].copy()
        test_out[&quot;split&quot;] = &quot;test&quot;
        test_out[&quot;score&quot;] = test_scores

        scored = pd.concat([train_out, val_out, test_out], axis=0, ignore_index=True)

        # 아티팩트 저장
        model_dir = f&quot;/tmp/{BATTERY_ID}_lof_artifacts&quot;
        os.makedirs(model_dir, exist_ok=True)

        joblib.dump(scaler, os.path.join(model_dir, &quot;scaler.joblib&quot;))
        joblib.dump(lof, os.path.join(model_dir, &quot;lof.joblib&quot;))

        scored_path = os.path.join(model_dir, f&quot;{BATTERY_ID}_scored_rows.csv&quot;)
        scored.to_csv(scored_path, index=False)

        meta = {
            &quot;battery_id&quot;: BATTERY_ID,
            &quot;total_cycles&quot;: total_cycles,
            &quot;train_threshold_cycle&quot;: int(train_threshold_cycle),
            &quot;val_threshold_cycle&quot;: int(val_threshold_cycle),
            &quot;feature_cols&quot;: FEATURE_COLS,
            &quot;lof_n_neighbors&quot;: LOF_N_NEIGHBORS,
        }
        with open(os.path.join(model_dir, &quot;run_meta.json&quot;), &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)

        print(f&quot;✓ LOF trained &amp;amp; scored. artifacts_dir={model_dir}&quot;)
        print(f&quot;✓ scored_rows={scored_path}, rows={len(scored)}&quot;)
        return model_dir

    @task
    def select_threshold(artifacts_dir: str) -&amp;gt; str:
        &quot;&quot;&quot;
        Validation split의 score 분포를 기준으로 quantile threshold 비교 후 선택.
        선택된 threshold/quantile을 meta에 기록.
        &quot;&quot;&quot;
        scored_path = os.path.join(artifacts_dir, f&quot;{BATTERY_ID}_scored_rows.csv&quot;)
        scored = pd.read_csv(scored_path)

        val_scores = scored.loc[scored[&quot;split&quot;] == &quot;val&quot;, &quot;score&quot;].dropna().astype(float).values
        assert len(val_scores) &amp;gt; 0, &quot;No validation scores found&quot;

        results = []
        for q in THRESHOLD_QUANTILES:
            thr = float(np.quantile(val_scores, q))
            # val에서 q-quantile이면 대략 (1-q) 비율이 이상으로 잡힘
            val_anom_rate = float((val_scores &amp;gt;= thr).mean())
            results.append({&quot;quantile&quot;: q, &quot;threshold&quot;: thr, &quot;val_anom_rate&quot;: val_anom_rate})

        # 기본: DEFAULT_THRESHOLD_Q, 없으면 중앙값에 가까운 후보 선택
        chosen = next((r for r in results if abs(r[&quot;quantile&quot;] - DEFAULT_THRESHOLD_Q) &amp;lt; 1e-12), results[0])

        print(&quot;=== Threshold candidates (validation) ===&quot;)
        for r in results:
            print(f&quot;q={r['quantile']:.3f} thr={r['threshold']:.6f} val_anom_rate={r['val_anom_rate']:.4f}&quot;)

        print(f&quot;✓ Chosen threshold: q={chosen['quantile']:.3f}, thr={chosen['threshold']:.6f}&quot;)

        # meta 업데이트
        meta_path = os.path.join(artifacts_dir, &quot;run_meta.json&quot;)
        with open(meta_path, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
            meta = json.load(f)

        meta[&quot;threshold_quantile&quot;] = chosen[&quot;quantile&quot;]
        meta[&quot;threshold_value&quot;] = chosen[&quot;threshold&quot;]
        meta[&quot;threshold_candidates&quot;] = results

        with open(meta_path, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)

        return artifacts_dir

    @task
    def persist_outputs(artifacts_dir: str):
        &quot;&quot;&quot;
        1) S3 업로드: scaler/lof/meta/scored_rows
        2) Snowflake 적재: BATTERY_B0005_LOF_RESULTS (행 단위 결과)
        &quot;&quot;&quot;
        run_ts = datetime.utcnow().strftime(&quot;%Y%m%d_%H%M%S&quot;)
        s3_hook = S3Hook(aws_conn_id=AWS_CONN_ID)
        sf_hook = SnowflakeHook(snowflake_conn_id=SNOWFLAKE_CONN_ID)

        # --- S3 upload ---
        files_to_upload = [
            os.path.join(artifacts_dir, &quot;scaler.joblib&quot;),
            os.path.join(artifacts_dir, &quot;lof.joblib&quot;),
            os.path.join(artifacts_dir, &quot;run_meta.json&quot;),
            os.path.join(artifacts_dir, f&quot;{BATTERY_ID}_scored_rows.csv&quot;),
        ]

        for fp in files_to_upload:
            key = f&quot;{S3_PREFIX}{BATTERY_ID}/{run_ts}/{os.path.basename(fp)}&quot;
            s3_hook.load_file(filename=fp, key=key, bucket_name=S3_BUCKET, replace=True)
            print(f&quot;✓ Uploaded: s3://{S3_BUCKET}/{key}&quot;)

        # --- Snowflake load (COPY via PUT) ---
        scored_path = os.path.join(artifacts_dir, f&quot;{BATTERY_ID}_scored_rows.csv&quot;)

        conn = sf_hook.get_conn()
        cur = conn.cursor()
        try:
            cur.execute(f&quot;USE DATABASE {SNOWFLAKE_DB};&quot;)
            cur.execute(f&quot;USE SCHEMA {SNOWFLAKE_SCHEMA};&quot;)

            # 결과 테이블 생성
            cur.execute(f&quot;&quot;&quot;
                CREATE TABLE IF NOT EXISTS {RESULT_TABLE} (
                    cycle_idx INT,
                    split STRING,
                    score FLOAT,
                    run_ts STRING
                );
            &quot;&quot;&quot;)

            # 로컬 CSV에 run_ts 컬럼을 추가한 임시 파일 생성
            df = pd.read_csv(scored_path)
            df[&quot;run_ts&quot;] = run_ts
            tmp_path = f&quot;/tmp/{BATTERY_ID}_scored_rows_with_runts.csv&quot;
            df.to_csv(tmp_path, index=False)

            # temp table
            cur.execute(f&quot;CREATE TEMP TABLE temp_lof_results LIKE {RESULT_TABLE};&quot;)

            abs_path = os.path.abspath(tmp_path)
            filename = os.path.basename(abs_path)

            cur.execute(
                f&quot;PUT 'file://{abs_path}' {SNOWFLAKE_INTERNAL_STAGE_PATH} auto_compress=false overwrite=true;&quot;
            )

            cur.execute(f&quot;&quot;&quot;
                COPY INTO temp_lof_results
                FROM {SNOWFLAKE_INTERNAL_STAGE_PATH}/{filename}
                FILE_FORMAT = (TYPE = 'CSV' SKIP_HEADER = 1 FIELD_OPTIONALLY_ENCLOSED_BY='&quot;')
                ON_ERROR = 'ABORT_STATEMENT';
            &quot;&quot;&quot;)

            # (정책 선택) append 적재: run_ts로 구분하여 누적
            cur.execute(f&quot;INSERT INTO {RESULT_TABLE} SELECT * FROM temp_lof_results;&quot;)

            cnt = cur.execute(f&quot;SELECT COUNT(*) FROM {RESULT_TABLE} WHERE run_ts='{run_ts}';&quot;).fetchone()[0]
            print(f&quot;✓ Inserted into {RESULT_TABLE}: run_ts={run_ts}, rows={cnt}&quot;)

        finally:
            cur.close()
            conn.close()

    # Dependency
    extracted = extract_lowess_from_snowflake()
    cleaned = validate_ml_data(extracted)
    artifacts = train_score_lof(cleaned)
    artifacts2 = select_threshold(artifacts)
    persist_outputs(artifacts2)


battery_ml_lof_pipeline()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;1. 머신러닝을 위한 데이터 무결성 가드레일 (validate_ml_data)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;로직&lt;/b&gt;: _safe_float_df 유틸리티를 통해 수치형 변환이 불가능한 데이터를 강제로 NaN 처리하고 dropna로 제거합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: ML 모델은 데이터에 문자열이나 결측치가 섞여 있을 때 치명적인 오류를 냅니다. 파이프라인 중간에 &lt;b data-index-in-node=&quot;65&quot; data-path-to-node=&quot;6,1,0&quot;&gt;'ML-Ready' 상태를 보장하는 검증 단계&lt;/b&gt;를 두어 모델 학습의 안정성을 확보했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7&quot;&gt;2. 시계열 특성을 고려한 시간순 데이터 분할 (Chronological Split)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;로직&lt;/b&gt;: 일반적인 랜덤 샘플링(train_test_split) 대신, cycle_idx를 기준으로 데이터를 정렬한 뒤 &lt;b data-index-in-node=&quot;65&quot; data-path-to-node=&quot;8,0,0&quot;&gt;6:2:2 비율로 순차 분할&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 배터리 데이터는 시간(Cycle)에 따라 상태가 변하는 시계열 데이터입니다. 미래의 데이터가 과거의 학습에 포함되는 데이터 누수(Data Leakage)를 방지하기 위해 철저히 시간 흐름에 따른 검증 전략을 채택했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9&quot;&gt;3. 통계 기반의 동적 임계치(Threshold) 선정 기법&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;로직&lt;/b&gt;: select_threshold 태스크에서 Validation 세트의 이상 점수(Anomaly Score) 분포를 분석하고, 상위 0.995 분위수(Quantile)를 기준으로 임계치를 동적으로 결정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: &quot;어디서부터 이상치인가?&quot;라는 질문에 하드코딩된 숫자로 답하지 않고, &lt;b data-index-in-node=&quot;49&quot; data-path-to-node=&quot;10,1,0&quot;&gt;데이터의 통계적 분포에 근거한 유연한 의사결정&lt;/b&gt; 로직을 파이프라인에 통합했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;4. 모델 아티팩트 및 메타데이터 관리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;로직&lt;/b&gt;: 학습된 scaler와 lof 모델 객체를 joblib으로 저장함과 동시에, 학습에 사용된 피처 목록과 파라미터를 run_meta.json이라는 &lt;b data-index-in-node=&quot;84&quot; data-path-to-node=&quot;12,0,0&quot;&gt;메타데이터 파일&lt;/b&gt;로 기록합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 나중에 모델 성능이 변했을 때 &quot;어떤 피처로, 어떤 설정으로 학습했는가?&quot;를 즉시 추적할 수 있도록 실험 관리(Experiment Tracking)의 기초를 설계했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13&quot;&gt;5. 버전 관리를 포함한 하이브리드 저장 전략&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;로직&lt;/b&gt;: 모델 파일과 메타데이터는 &lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;14,0,0&quot;&gt;AWS S3&lt;/b&gt;에 보존하고, 행 단위의 이상 탐지 결과는 run_ts(실행 타임스탬프)와 함께 &lt;b data-index-in-node=&quot;69&quot; data-path-to-node=&quot;14,0,0&quot;&gt;Snowflake&lt;/b&gt;에 누적 적재합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 대용량 바이너리 파일(모델)과 구조화된 쿼리가 필요한 데이터(결과값)를 각각 최적의 저장소에 배치했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Dag 3: LOF build and experiment&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 DAG는 최신 딥러닝 모델인 &lt;b data-index-in-node=&quot;20&quot; data-path-to-node=&quot;4&quot;&gt;Anomaly Transformer&lt;/b&gt;를 활용하여 배터리의 미세한 열화 징후를 탐지합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1767854297673&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from airflow.decorators import dag, task
from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
from airflow.providers.amazon.aws.hooks.s3 import S3Hook
from datetime import datetime
import os
import json
import pandas as pd
import numpy as np
import mlflow
import sys

# Anomaly Transformer 모델 import
anomaly_transformer_path = '/opt/airflow/plugins/Anomaly-Transformer'
sys.path.insert(0, anomaly_transformer_path)
sys.path.insert(0, os.path.join(anomaly_transformer_path, 'model'))
sys.path.insert(0, os.path.join(anomaly_transformer_path, 'data_factory'))
sys.path.insert(0, os.path.join(anomaly_transformer_path, 'utils'))

from solver import Solver

# ----------------------------
# Config
# ----------------------------
AWS_CONN_ID = &quot;aws_conn&quot;
SNOWFLAKE_CONN_ID = &quot;snowflake_conn&quot;

S3_BUCKET = &quot;bucket&quot;
S3_PREFIX = &quot;battery/anomaly_transformer/&quot;

BATTERY_ID = &quot;B0005&quot;

SNOWFLAKE_DB = &quot;BATTERY_DATABASE&quot;
SNOWFLAKE_SCHEMA_RAW = &quot;RAW_DATA&quot;
SNOWFLAKE_SCHEMA_ANALYTICS = &quot;ANALYTICS&quot;

# LOWESS 결과 테이블 (RAW_DATA 스키마)
LOWESS_TABLE = f&quot;BATTERY_{BATTERY_ID}_LOWESS&quot;

# Anomaly Transformer 결과 테이블 (ANALYTICS 스키마)
RESULT_TABLE = f&quot;BATTERY_{BATTERY_ID}_AT_RESULTS&quot;

SNOWFLAKE_INTERNAL_STAGE_PATH = &quot;@~/battery_upload&quot;

# Feature columns
FEATURE_COLS = [
    &quot;Voltage_measured&quot;, &quot;Current_measured&quot;, &quot;Temperature_measured&quot;,
    &quot;Current_load&quot;, &quot;Voltage_load&quot;,
    &quot;Voltage_measured_smooth&quot;, &quot;Voltage_measured_residual&quot;, &quot;Voltage_measured_trend&quot;,
    &quot;Current_measured_smooth&quot;, &quot;Current_measured_residual&quot;, &quot;Current_measured_trend&quot;,
    &quot;Temperature_measured_smooth&quot;, &quot;Temperature_measured_residual&quot;, &quot;Temperature_measured_trend&quot;,
    &quot;Current_load_smooth&quot;, &quot;Current_load_residual&quot;, &quot;Current_load_trend&quot;,
    &quot;Voltage_load_smooth&quot;, &quot;Voltage_load_residual&quot;, &quot;Voltage_load_trend&quot;,
]

# Anomaly Transformer hyperparameters
# main.py 파라미터 기준
AT_PARAMS = {
       &quot;lr&quot;: 1e-4,
       &quot;num_epochs&quot;: 10,  # epochs &amp;rarr; num_epochs
       &quot;k&quot;: 3,
       &quot;win_size&quot;: 100,
       &quot;input_c&quot;: len(FEATURE_COLS),  # 20
       &quot;output_c&quot;: len(FEATURE_COLS),  # 20
       &quot;batch_size&quot;: 32,
       &quot;stride&quot;: 1,
       &quot;dataset&quot;: &quot;nasa_battery&quot;,
       &quot;anormly_ratio&quot;: 1.00,
       &quot;split_mode&quot;: &quot;two&quot;,  # train/test 8:2
   }

# MLflow
MLFLOW_TRACKING_URI = &quot;http://mlflow:5000&quot;
MLFLOW_EXPERIMENT_NAME = &quot;battery_anomaly_transformer&quot;

@dag(
    dag_id=&quot;battery_dag_04_ml_transformer&quot;,
    start_date=datetime(2024, 12, 1),
    schedule=None,
    catchup=False,
    tags=[&quot;battery&quot;, &quot;ml&quot;, &quot;anomaly_transformer&quot;, &quot;deep_learning&quot;, &quot;lowess&quot;],
)

def battery_ml_transformer_pipeline():

    @task
    def extract_lowess_from_snowflake() -&amp;gt; str:
        &quot;&quot;&quot;Snowflake에서 LOWESS 전처리된 데이터 추출&quot;&quot;&quot;
        hook = SnowflakeHook(snowflake_conn_id=SNOWFLAKE_CONN_ID)

        sql = f&quot;&quot;&quot;
            SELECT
                CYCLE_IDX,
                VOLTAGE_MEASURED,
                CURRENT_MEASURED,
                TEMPERATURE_MEASURED,
                CURRENT_LOAD,
                VOLTAGE_LOAD,
                VOLTAGE_MEASURED_SMOOTH,
                VOLTAGE_MEASURED_RESIDUAL,
                VOLTAGE_MEASURED_TREND,
                CURRENT_MEASURED_SMOOTH,
                CURRENT_MEASURED_RESIDUAL,
                CURRENT_MEASURED_TREND,
                TEMPERATURE_MEASURED_SMOOTH,
                TEMPERATURE_MEASURED_RESIDUAL,
                TEMPERATURE_MEASURED_TREND,
                CURRENT_LOAD_SMOOTH,
                CURRENT_LOAD_RESIDUAL,
                CURRENT_LOAD_TREND,
                VOLTAGE_LOAD_SMOOTH,
                VOLTAGE_LOAD_RESIDUAL,
                VOLTAGE_LOAD_TREND
            FROM {SNOWFLAKE_DB}.{SNOWFLAKE_SCHEMA_RAW}.{LOWESS_TABLE}
            ORDER BY CYCLE_IDX
        &quot;&quot;&quot;

        df = hook.get_pandas_df(sql)
        print(&quot;[DEBUG raw Snowflake columns]:&quot;, df.columns.tolist())

        rename_map = {
            &quot;CYCLE_IDX&quot;: &quot;cycle_idx&quot;,
            &quot;VOLTAGE_MEASURED&quot;: &quot;Voltage_measured&quot;,
            &quot;CURRENT_MEASURED&quot;: &quot;Current_measured&quot;,
            &quot;TEMPERATURE_MEASURED&quot;: &quot;Temperature_measured&quot;,
            &quot;CURRENT_LOAD&quot;: &quot;Current_load&quot;,
            &quot;VOLTAGE_LOAD&quot;: &quot;Voltage_load&quot;,
            &quot;VOLTAGE_MEASURED_SMOOTH&quot;: &quot;Voltage_measured_smooth&quot;,
            &quot;VOLTAGE_MEASURED_RESIDUAL&quot;: &quot;Voltage_measured_residual&quot;,
            &quot;VOLTAGE_MEASURED_TREND&quot;: &quot;Voltage_measured_trend&quot;,
            &quot;CURRENT_MEASURED_SMOOTH&quot;: &quot;Current_measured_smooth&quot;,
            &quot;CURRENT_MEASURED_RESIDUAL&quot;: &quot;Current_measured_residual&quot;,
            &quot;CURRENT_MEASURED_TREND&quot;: &quot;Current_measured_trend&quot;,
            &quot;TEMPERATURE_MEASURED_SMOOTH&quot;: &quot;Temperature_measured_smooth&quot;,
            &quot;TEMPERATURE_MEASURED_RESIDUAL&quot;: &quot;Temperature_measured_residual&quot;,
            &quot;TEMPERATURE_MEASURED_TREND&quot;: &quot;Temperature_measured_trend&quot;,
            &quot;CURRENT_LOAD_SMOOTH&quot;: &quot;Current_load_smooth&quot;,
            &quot;CURRENT_LOAD_RESIDUAL&quot;: &quot;Current_load_residual&quot;,
            &quot;CURRENT_LOAD_TREND&quot;: &quot;Current_load_trend&quot;,
            &quot;VOLTAGE_LOAD_SMOOTH&quot;: &quot;Voltage_load_smooth&quot;,
            &quot;VOLTAGE_LOAD_RESIDUAL&quot;: &quot;Voltage_load_residual&quot;,
            &quot;VOLTAGE_LOAD_TREND&quot;: &quot;Voltage_load_trend&quot;,
        }

        df = df.rename(columns=rename_map)

        if &quot;cycle_idx&quot; not in df.columns:
            raise ValueError(f&quot;cycle_idx missing after rename. columns={df.columns.tolist()}&quot;)

        out_path = f&quot;/tmp/{BATTERY_ID}_lowess_transformer_input.csv&quot;
        df.to_csv(out_path, index=False)

        print(f&quot;[OK] Extracted LOWESS for Anomaly Transformer: rows={len(df)}&quot;)
        return out_path

    @task
    def validate_ml_data(file_path: str) -&amp;gt; str:
        &quot;&quot;&quot;AT 입력 검증&quot;&quot;&quot;
        df = pd.read_csv(file_path)

        assert len(df) &amp;gt; 0, &quot;Empty dataframe&quot;
        assert &quot;cycle_idx&quot; in df.columns, &quot;cycle_idx missing&quot;

        missing = [c for c in FEATURE_COLS if c not in df.columns]
        assert len(missing) == 0, f&quot;Missing feature cols: {missing}&quot;

        # 숫자 변환 (NaN 처리)
        def _safe_float_df(df: pd.DataFrame, cols: list[str]) -&amp;gt; pd.DataFrame:
            out = df.copy()
            for c in cols:
                out[c] = pd.to_numeric(out[c], errors=&quot;coerce&quot;)
            return out

        df = _safe_float_df(df, FEATURE_COLS)
        before = len(df)
        df = df.dropna(subset=[&quot;cycle_idx&quot;] + FEATURE_COLS).copy()
        after = len(df)

        assert after &amp;gt; 0, &quot;All rows dropped after numeric coercion / NaN removal&quot;

        # cycle_idx int 변환
        df[&quot;cycle_idx&quot;] = pd.to_numeric(df[&quot;cycle_idx&quot;], errors=&quot;coerce&quot;).astype(int)

        clean_path = f&quot;/tmp/{BATTERY_ID}_lowess_for_transformer_clean.csv&quot;
        df.to_csv(clean_path, index=False)

        print(f&quot;✓ Validation passed: before={before}, after={after}, saved={clean_path}&quot;)
        return clean_path

    @task
    def train_model(clean_path: str) -&amp;gt; str:
        &quot;&quot;&quot;
        Anomaly Transformer 학습
        - Solver 활용
        - MLflow tracking
        &quot;&quot;&quot;
        # MLflow 설정
        mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
        mlflow.set_experiment(MLFLOW_EXPERIMENT_NAME)
        
        # Artifacts 저장 경로
        model_dir = f&quot;/tmp/{BATTERY_ID}_transformer_artifacts&quot;
        os.makedirs(model_dir, exist_ok=True)
        
        # Config 생성 (Solver에 전달할 딕셔너리)
        config = {
            &quot;lr&quot;: AT_PARAMS[&quot;lr&quot;],
            &quot;num_epochs&quot;: AT_PARAMS[&quot;num_epochs&quot;],
            &quot;k&quot;: AT_PARAMS[&quot;k&quot;],
            &quot;win_size&quot;: AT_PARAMS[&quot;win_size&quot;],
            &quot;input_c&quot;: AT_PARAMS[&quot;input_c&quot;],
            &quot;output_c&quot;: AT_PARAMS[&quot;output_c&quot;],
            &quot;batch_size&quot;: AT_PARAMS[&quot;batch_size&quot;],
            &quot;stride&quot;: AT_PARAMS[&quot;stride&quot;],
            &quot;dataset&quot;: AT_PARAMS[&quot;dataset&quot;],
            &quot;data_path&quot;: clean_path,  # Task 2에서 받은 경로
            &quot;model_save_path&quot;: model_dir,
            &quot;anormly_ratio&quot;: AT_PARAMS[&quot;anormly_ratio&quot;],
            &quot;split_mode&quot;: AT_PARAMS[&quot;split_mode&quot;],
        }
        
        # MLflow Run 시작
        with mlflow.start_run() as run:
            run_id = run.info.run_id
            print(f&quot;MLflow Run ID: {run_id}&quot;)
            
            # Hyperparameters logging
            mlflow.log_params(config)
            mlflow.log_param(&quot;battery_id&quot;, BATTERY_ID)
            
            # Solver 초기화 및 학습
            solver = Solver(config)
            solver.train()
            
            # Training history 로깅
            history_path = os.path.join(model_dir, 'training_history.pkl')
            if os.path.exists(history_path):
                import pickle
                with open(history_path, 'rb') as f:
                    history = pickle.load(f)
                
                # Epoch별 metrics 로깅
                for epoch, (train_loss, vali_loss1, vali_loss2) in enumerate(
                    zip(history['train_loss'], history['vali_loss1'], history['vali_loss2'])
                ):
                    mlflow.log_metric(&quot;train_loss&quot;, train_loss, step=epoch)
                    mlflow.log_metric(&quot;vali_loss1&quot;, vali_loss1, step=epoch)
                    mlflow.log_metric(&quot;vali_loss2&quot;, vali_loss2, step=epoch)
                
                mlflow.log_artifact(history_path)
            
            # Checkpoint 로깅
            checkpoint_path = os.path.join(model_dir, f&quot;{config['dataset']}_checkpoint.pth&quot;)
            if os.path.exists(checkpoint_path):
                mlflow.log_artifact(checkpoint_path)
            
            # Meta 정보 저장
            meta = {
                &quot;battery_id&quot;: BATTERY_ID,
                &quot;mlflow_run_id&quot;: run_id,
                &quot;config&quot;: config,
            }
            meta_path = os.path.join(model_dir, &quot;train_meta.json&quot;)
            with open(meta_path, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
                json.dump(meta, f, ensure_ascii=False, indent=2)
            mlflow.log_artifact(meta_path)
            
            print(f&quot;✓ Training completed. artifacts_dir={model_dir}&quot;)
            print(f&quot;✓ MLflow Run ID: {run_id}&quot;)
        
        return model_dir

    @task
    def test_model(artifacts_dir: str) -&amp;gt; str:
        &quot;&quot;&quot;
        Anomaly Transformer 테스트
        - Train set으로 threshold 계산
        - Test set anomaly score 계산
        - Cycle별 anomaly score 계산
        &quot;&quot;&quot;
        # MLflow 설정 (train과 동일한 experiment)
        mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
        mlflow.set_experiment(MLFLOW_EXPERIMENT_NAME)
        
        # Train meta 읽기
        meta_path = os.path.join(artifacts_dir, &quot;train_meta.json&quot;)
        with open(meta_path, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
            meta = json.load(f)
        
        config = meta[&quot;config&quot;]
        
        # MLflow Run 시작 (train과 연결하려면 같은 run_id 사용 가능)
        with mlflow.start_run(run_id=meta[&quot;mlflow_run_id&quot;]):
            print(f&quot;MLflow Run ID: {meta['mlflow_run_id']}&quot;)
            
            # Solver 초기화 및 테스트
            solver = Solver(config)
            accuracy, precision, recall, f_score = solver.test()
            
            # Test metrics 로깅
            mlflow.log_metric(&quot;test_accuracy&quot;, accuracy)
            mlflow.log_metric(&quot;test_precision&quot;, precision)
            mlflow.log_metric(&quot;test_recall&quot;, recall)
            mlflow.log_metric(&quot;test_f_score&quot;, f_score)
            
            # Test results 로깅
            test_results_path = os.path.join(artifacts_dir, 'test_results.pkl')
            if os.path.exists(test_results_path):
                import pickle
                with open(test_results_path, 'rb') as f:
                    results = pickle.load(f)
                
                # Threshold 로깅
                mlflow.log_param(&quot;threshold&quot;, results['threshold'])
                
                # Cycle별 anomaly scores를 CSV로 저장
                if 'cycle_scores' in results:
                    cycle_scores_df = pd.DataFrame([
                        {&quot;cycle_idx&quot;: cycle, &quot;anomaly_score&quot;: score}
                        for cycle, score in results['cycle_scores'].items()
                    ])
                    cycle_scores_path = os.path.join(artifacts_dir, f&quot;{BATTERY_ID}_cycle_scores.csv&quot;)
                    cycle_scores_df.to_csv(cycle_scores_path, index=False)
                    
                    print(f&quot;✓ Cycle scores saved: {len(cycle_scores_df)} cycles&quot;)
                    mlflow.log_artifact(cycle_scores_path)
                
                # Test results artifact 로깅
                mlflow.log_artifact(test_results_path)
            
            print(f&quot;✓ Testing completed.&quot;)
            print(f&quot;  Accuracy: {accuracy:.4f}, Precision: {precision:.4f}&quot;)
            print(f&quot;  Recall: {recall:.4f}, F-score: {f_score:.4f}&quot;)
        
        return artifacts_dir

    @task
    def persist_outputs(artifacts_dir: str):
        &quot;&quot;&quot;S3 및 Snowflake에 결과 저장&quot;&quot;&quot;
        run_ts = datetime.utcnow().strftime(&quot;%Y%m%d_%H%M%S&quot;)
        s3_hook = S3Hook(aws_conn_id=AWS_CONN_ID)
        sf_hook = SnowflakeHook(snowflake_conn_id=SNOWFLAKE_CONN_ID)

        # --- S3 upload ---
        files_to_upload = [
            os.path.join(artifacts_dir, f&quot;{AT_PARAMS['dataset']}_checkpoint.pth&quot;),
            os.path.join(artifacts_dir, &quot;train_meta.json&quot;),
            os.path.join(artifacts_dir, &quot;training_history.pkl&quot;),
            os.path.join(artifacts_dir, &quot;test_results.pkl&quot;),
            os.path.join(artifacts_dir, f&quot;{BATTERY_ID}_cycle_scores.csv&quot;),
        ]

        for fp in files_to_upload:
            if os.path.exists(fp):
                key = f&quot;{S3_PREFIX}{BATTERY_ID}/{run_ts}/{os.path.basename(fp)}&quot;
                s3_hook.load_file(filename=fp, key=key, bucket_name=S3_BUCKET, replace=True)
                print(f&quot;✓ Uploaded: s3://{S3_BUCKET}/{key}&quot;)
            else:
                print(f&quot;⚠ File not found, skipping: {fp}&quot;)

        # --- Snowflake load ---
        cycle_scores_path = os.path.join(artifacts_dir, f&quot;{BATTERY_ID}_cycle_scores.csv&quot;)
        
        if not os.path.exists(cycle_scores_path):
            print(&quot;⚠ cycle_scores.csv not found, skipping Snowflake load&quot;)
            return

        conn = sf_hook.get_conn()
        cur = conn.cursor()
        try:
            cur.execute(f&quot;USE DATABASE {SNOWFLAKE_DB};&quot;)
            
            # ANALYTICS 스키마 생성 (없으면)
            cur.execute(f&quot;CREATE SCHEMA IF NOT EXISTS {SNOWFLAKE_SCHEMA_ANALYTICS};&quot;)
            cur.execute(f&quot;USE SCHEMA {SNOWFLAKE_SCHEMA_ANALYTICS};&quot;)

            # 결과 테이블 생성
            cur.execute(f&quot;&quot;&quot;
                CREATE TABLE IF NOT EXISTS {RESULT_TABLE} (
                    cycle_idx INT,
                    anomaly_score FLOAT,
                    run_ts STRING
                );
            &quot;&quot;&quot;)

            # run_ts 컬럼 추가
            df = pd.read_csv(cycle_scores_path)
            df[&quot;run_ts&quot;] = run_ts
            tmp_path = f&quot;/tmp/{BATTERY_ID}_cycle_scores_with_runts.csv&quot;
            df.to_csv(tmp_path, index=False)

            # Temp table
            cur.execute(f&quot;CREATE TEMP TABLE temp_at_results LIKE {RESULT_TABLE};&quot;)

            abs_path = os.path.abspath(tmp_path)
            filename = os.path.basename(abs_path)

            cur.execute(
                f&quot;PUT 'file://{abs_path}' {SNOWFLAKE_INTERNAL_STAGE_PATH} auto_compress=false overwrite=true;&quot;
            )

            cur.execute(f&quot;&quot;&quot;
                COPY INTO temp_at_results
                FROM {SNOWFLAKE_INTERNAL_STAGE_PATH}/{filename}
                FILE_FORMAT = (TYPE = 'CSV' SKIP_HEADER = 1 FIELD_OPTIONALLY_ENCLOSED_BY='&quot;')
                ON_ERROR = 'ABORT_STATEMENT';
            &quot;&quot;&quot;)

            # Append 적재
            cur.execute(f&quot;INSERT INTO {RESULT_TABLE} SELECT * FROM temp_at_results;&quot;)

            cnt = cur.execute(f&quot;SELECT COUNT(*) FROM {RESULT_TABLE} WHERE run_ts='{run_ts}';&quot;).fetchone()[0]
            print(f&quot;✓ Inserted into {RESULT_TABLE}: run_ts={run_ts}, rows={cnt}&quot;)

        finally:
            cur.close()
            conn.close()
    
    # Dependency 정의
    extracted = extract_lowess_from_snowflake()
    cleaned = validate_ml_data(extracted)
    artifacts_train = train_model(cleaned)
    artifacts_test = test_model(artifacts_train)
    persist_outputs(artifacts_test)
    
# DAG 실행
battery_ml_transformer_pipeline()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;1. MLflow를 활용한 실험 관리 및 추적 (Experiment Tracking)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;로직&lt;/b&gt;: mlflow.start_run()을 통해 학습 과정을 세션화하고, 하이퍼파라미터(log_params)와 Epoch 별 손실 함수(log_metric), 그리고 최종 모델 파일(log_artifact)을 중앙 서버에서 관리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 딥러닝은 파라미터 변화에 따른 성능 차이가 큽니다. 단순히 결과를 저장하는 것이 아니라, &lt;b data-index-in-node=&quot;60&quot; data-path-to-node=&quot;6,1,0&quot;&gt;MLflow&lt;/b&gt;를 통해 수많은 실험 중 '최적의 모델'이 무엇인지 시각적으로 비교하고 관리할 수 있는 &lt;b data-index-in-node=&quot;115&quot; data-path-to-node=&quot;6,1,0&quot;&gt;MLOps 환경을 구축&lt;/b&gt;했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7&quot;&gt;2. 외부 딥러닝 모듈의 동적 통합 (Plugin System)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;로직&lt;/b&gt;: sys.path.insert를 사용해 Airflow 플러그인 경로에 위치한 외부 Anomaly-Transformer 소스 코드를 동적으로 불러와 Solver 객체를 초기화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 연구용 코드를 실제 운영 환경(Airflow)에 통합할 때 발생하는 경로 문제를 해결했습니다. 이를 통해 모델 아키텍처 코드를 수정하지 않고도 &lt;b data-index-in-node=&quot;90&quot; data-path-to-node=&quot;8,1,0&quot;&gt;파이프라인 내에 딥러닝 엔진을 이식&lt;/b&gt;하는 유연성을 확보했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9&quot;&gt;3. 데이터 분석을 위한 스키마 분리 (Schema Isolation)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;로직&lt;/b&gt;: 결과 데이터를 기존 RAW_DATA 스키마가 아닌 별도의 ANALYTICS 스키마에 적재합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 원천 데이터(Raw)와 가공 데이터(Preprocessed), 그리고 모델이 생성한 통계 결과(Analytics)를 물리적으로 분리했습니다. 이는 &lt;b data-index-in-node=&quot;92&quot; data-path-to-node=&quot;10,1,0&quot;&gt;데이터 거버넌스&lt;/b&gt; 측면에서 분석가들이 신뢰할 수 있는 데이터만 조회할 수 있게 하는 실무적인 설계입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;4. 평가 지표의 자동화된 로깅 (Evaluation Automation)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;로직&lt;/b&gt;: test_model 태스크에서 Accuracy, Precision, Recall, F-score를 계산하고 이를 다시 MLflow에 기록합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 모델의 '학습'과 '검증'을 분리된 태스크로 정의하여, 학습이 완료된 후 즉시 객관적인 성능 지표를 산출합니다. 이는 배포 여부를 결정하는 &lt;b data-index-in-node=&quot;88&quot; data-path-to-node=&quot;12,1,0&quot;&gt;CI/CD 파이프라인의 판단 근거&lt;/b&gt;가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13&quot;&gt;5. 체크포인트 기반의 아티팩트 보존 전략&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;로직&lt;/b&gt;: 학습 중 생성된 .pth 체크포인트와 training_history.pkl을 S3와 Snowflake 내부 스테이지에 이중으로 백업합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 딥러닝 모델은 학습 시간이 길고 자원이 많이 소모됩니다. 장애가 발생하거나 특정 시점의 모델로 롤백해야 할 경우를 대비해 &lt;b data-index-in-node=&quot;78&quot; data-path-to-node=&quot;14,1,0&quot;&gt;학습의 결과물(Artifacts)을 체계적으로 버전 관리&lt;/b&gt;하도록 설계했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Trouble shooting&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sys.path.insert를 사용 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0,0&quot;&gt;문제 상황&lt;/b&gt;: Anomaly Transformer는 논문 구현을 위한 커스텀 코드로 구성되어 있어, 내부적으로 from solver import Solver와 같은 상대 경로 참조가 가득했습니다. 하지만 Airflow Worker 환경이나 MLflow가 이 코드를 실행할 때, 실행 위치(Working Directory)가 달라지면서 모듈을 찾지 못하는 ModuleNotFoundError가 발생했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,0&quot;&gt;원인&lt;/b&gt;: Airflow의 Python 인터프리터는 프로젝트 루트를 기준으로 모듈을 찾지만, 커스텀 모델 패키지는 독립된 폴더(plugins/Anomaly-Transformer) 아래에 있어 인식이 되지 않았습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,2,0&quot;&gt;해결&lt;/b&gt;: sys.path.insert(0, path)를 통해 파이썬이 모듈을 검색하는 우선순위 리스트에 직접 커스텀 코드 경로를 주입했습니다. 특히 MLflow 전송 시에도 모델 소스 코드가 유실되지 않도록 경로를 명시적으로 제어하여 코드 성공률을 100%로 만들었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ML/DL 파이프라인을 구축하며 가장 중요하게 생각한 것은 &lt;b&gt;'모델의 재현성'&lt;/b&gt;이었습니다. 단순히 한 번의 학습으로 끝나는 것이 아니라, 새로운 데이터가 들어올 때마다 동일한 검증 과정을 거치고, 그 결과가 메타데이터와 함께 기록되어 언제든 복기할 수 있는 구조인 MLOps의 본질을 구현하고자 했습니다.&lt;/p&gt;</description>
      <category>Data Engineering</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/83</guid>
      <comments>https://gguzunhagae.tistory.com/83#entry83comment</comments>
      <pubDate>Thu, 8 Jan 2026 15:44:39 +0900</pubDate>
    </item>
    <item>
      <title> [Blog series] Airflow로 구축하는 NASA 배터리 파이프라인-3.1</title>
      <link>https://gguzunhagae.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 24시간 멈추지 않는 스마트 팩토리: Airflow와 Snowflake 기반 자동화 시스템&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;앞선 포스팅을 통해 데이터 파이프라인의 기초와 장애에 대비하는 설계 원칙(멱등성, 트랜잭션)에 대해 심도 있게 다루었습니다. 하지만 훌륭한 설계 원칙도 실제 구현 환경에서 제대로 작동하지 않는다면 무용지물입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;시리즈의 마지막인 이번 글에서는 NASA 배터리 시계열 데이터를 처리하기 위해 제가 구축한 End-to-End 파이프라인의 실체를 공개합니다. 복잡한 센서 노이즈를 제거하는 LOWESS 전처리부터, 데이터 웨어하우스인 Snowflake로의 안정적인 적재까지의 전 과정을 코드를 통해 상세히 분석해보려 합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;단순히 '돌아가는 코드'를 작성하는 것에 그치지 않고, 왜 이 기술 스택을 선택했는지, 그리고 개발 과정에서 마주친 예기치 못한 에러들을 어떻게 엔지니어링적으로 해결했는지에 대한 저의 치열한 고민 과정을 담았습니다. 이 기록이 안정적인 MLOps 환경을 구축하려는 분들에게 실질적인 가이드가 되기를 바랍니다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Snowflake와 Airflow를 사용한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 프로젝트에서 Airflow와 Snowflake를 선택한 이유는 배터리 이상 탐지 시스템의 자동화와 확장성을 확보하기 위함이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow는 배터리 데이터 전처리(LOWESS smoothing)부터 모델 학습, 평가까지의 전체 워크플로우를 오케스트레이션하는 역할을 수행합니다. 이를 통해 주기적인 재학습과 배치 처리를 자동화할 수 있으며, 각 태스크 간 의존성 관리와 실패 처리를 체계적으로 구현할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake는 50,000개 이상의 timestep을 가진 멀티배터리 시계열 데이터를 중앙 집중식으로 저장하고 쿼리하는 데이터 웨어하우스로 활용되었습니다. 여러 배터리(B0005, B0006, B0007) 간 cross-battery 분석을 지원하며, 확장 가능한 스토리지와 빠른 쿼리 성능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스마트 팩토리 환경에서 이러한 툴들은 더욱 중요한 의미를 갖습니다. 실제 제조 현장에서는 여러 배터리 팩과 ESS 장비에서 멀티소스 센서 데이터가 지속적으로 발생하며, 이를 통합 관리하고 실시간 모니터링과 이상 탐지를 수행해야 합니다. Airflow와 Snowflake의 조합은 프로덕션 환경에서 지속적인 모델 업데이트와 배포를 자동화하는 확장 가능한 MLOps 파이프라인의 핵심 인프라입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DAG 코드 주요 로직 및 엔지니어링 포인트 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dag 1: Data Ingestion&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1209&quot; data-origin-height=&quot;927&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jn4IN/dJMcacBOclB/IP25HlN0cSSFkTK6e6yHsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jn4IN/dJMcacBOclB/IP25HlN0cSSFkTK6e6yHsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jn4IN/dJMcacBOclB/IP25HlN0cSSFkTK6e6yHsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJn4IN%2FdJMcacBOclB%2FIP25HlN0cSSFkTK6e6yHsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;429&quot; height=&quot;329&quot; data-origin-width=&quot;1209&quot; data-origin-height=&quot;927&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1767852712180&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from airflow.decorators import dag, task
from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
from airflow.providers.amazon.aws.hooks.s3 import S3Hook
from datetime import datetime
import pandas as pd
import os

SNOWFLAKE_CONN_ID = 'snowflake'
S3_BUCKET = 'bucket'
S3_PREFIX = 'battery/raw/'

@dag(
    dag_id='battery_dag_01_load',
    start_date=datetime(2024, 12, 1),
    schedule=None,
    catchup=False,
    tags=['battery', 'load']
)
def battery_load_pipeline():
    
    @task
    def extract_battery_data():
        &quot;&quot;&quot;CSV 배터리 데이터 추출 및 기본 전처리&quot;&quot;&quot;
        csv_path = '/opt/airflow/data/B0007_discharge.csv'
        df = pd.read_csv(csv_path)
        
        # 정렬
        df = df.sort_values(['cycle_idx']).reset_index(drop=True)
        
        # 불필요한 컬럼 제거
        drop_cols = ['start_time_raw', 'Capacity', 'type', 'ambient_temperature', 'Time']
        df = df.drop([col for col in drop_cols if col in df.columns], axis=1)
        
        # snowflake 스키마와 동일하게 컬럼 순서 재배치
        df = df[['cycle_idx', 'Voltage_measured', 'Current_measured', 
             'Temperature_measured', 'Current_load', 'Voltage_load']]
        
        # 임시 저장
        file_path = '/tmp/battery_b0007_raw.csv'
        df.to_csv(file_path, index=False)
        
        print(f&quot;✓ Extracted {len(df)} rows, cycles {df['cycle_idx'].min()}-{df['cycle_idx'].max()}&quot;)
        return file_path
    
    @task
    def validate_data(file_path: str):
        &quot;&quot;&quot;데이터 검증&quot;&quot;&quot;
        df = pd.read_csv(file_path)
        
        # 검증
        assert df.isnull().sum().sum() == 0, &quot;Missing values detected&quot;
        assert len(df) &amp;gt; 0, &quot;Empty dataframe&quot;
        assert (df['Voltage_measured'] &amp;gt; 0).all(), &quot;Invalid voltage values&quot;
        
        print(f&quot;✓ Validation passed: {len(df)} rows&quot;)
        return file_path
    
    @task
    def upload_to_s3(file_path: str):
        &quot;&quot;&quot;S3 업로드&quot;&quot;&quot;
        s3_hook = S3Hook(aws_conn_id='aws_conn')
        s3_key = f&quot;{S3_PREFIX}{os.path.basename(file_path)}&quot;
        
        s3_hook.load_file(
            filename=file_path,
            key=s3_key,
            bucket_name=S3_BUCKET,
            replace=True
        )
        
        print(f&quot;✓ Uploaded to s3://{S3_BUCKET}/{s3_key}&quot;)
        return s3_key
    
    @task
    def load_to_snowflake(s3_key: str):
        &quot;&quot;&quot;Snowflake 적재 (S3 Stage 경유)&quot;&quot;&quot;
        hook = SnowflakeHook(snowflake_conn_id=SNOWFLAKE_CONN_ID)
        conn = hook.get_conn()
        cur = conn.cursor()
        
        try:
            cur.execute(&quot;USE DATABASE BATTERY_DATABASE;&quot;)
            cur.execute(&quot;USE SCHEMA RAW_DATA;&quot;)
            
            # 테이블 생성 (PRIMARY KEY 제거)
            cur.execute(&quot;&quot;&quot;
                CREATE TABLE IF NOT EXISTS BATTERY_B0007_RAW (
                    cycle_idx INT,
                    Voltage_measured FLOAT,
                    Current_measured FLOAT,
                    Temperature_measured FLOAT,
                    Current_load FLOAT,
                    Voltage_load FLOAT
                );
            &quot;&quot;&quot;)
            
            # Staging 테이블
            cur.execute(&quot;CREATE TEMP TABLE temp_battery LIKE BATTERY_B0007_RAW;&quot;)
            
            # S3에서 COPY
            cur.execute(f&quot;&quot;&quot;
                COPY INTO temp_battery
                FROM @battery_s3_stage/{os.path.basename(s3_key)}
                FILE_FORMAT = (TYPE = 'CSV' SKIP_HEADER = 1)
                ON_ERROR = 'ABORT_STATEMENT';
            &quot;&quot;&quot;)
            
            # TRUNCATE + INSERT 방식으로 적재
            cur.execute(&quot;TRUNCATE TABLE BATTERY_B0007_RAW;&quot;)
            cur.execute(&quot;INSERT INTO BATTERY_B0007_RAW SELECT * FROM temp_battery;&quot;)
            
            # 결과 확인
            result = cur.execute(&quot;SELECT COUNT(*) FROM BATTERY_B0007_RAW;&quot;).fetchone()
            print(f&quot;✓ Total rows in BATTERY_B0007_RAW: {result[0]}&quot;)
            
        finally:
            cur.close()
            conn.close()

    # Task 의존성
    file_path = extract_battery_data()
    validated_path = validate_data(file_path)
    s3_key = upload_to_s3(validated_path)
    load_to_snowflake(s3_key)

battery_load_pipeline()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6&quot;&gt;1. Upstream에서의 데이터 품질 검증 (Data Quality Check)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;데이터가 데이터 웨어하우스에 들어가기 전, validate_data 태스크를 통해 엄격한 검증 과정을 거칩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;로직&lt;/b&gt;: assert 문을 사용하여 결측치 여부, 데이터 유무, 그리고 도메인 지식을 반영한 전압값(Voltage)의 유효성을 체크합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;의도&lt;/b&gt;: 잘못된 데이터가 하류(Downstream)로 흘러가 분석 결과나 모델 성능을 오염시키는 것을 원천 차단했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9&quot;&gt;2. S3 Staging을 통한 클라우드 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;로컬 데이터를 직접 Snowflake로 넣지 않고 중간에 &lt;b data-index-in-node=&quot;32&quot; data-path-to-node=&quot;10&quot;&gt;AWS S3&lt;/b&gt;를 거치도록 설계했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;이유&lt;/b&gt;: Snowflake의 COPY INTO 명령은 클라우드 스토리지를 활용할 때 가장 높은 성능을 발휘합니다. S3를 &lt;b data-index-in-node=&quot;67&quot; data-path-to-node=&quot;11,0,0&quot;&gt;Staging 영역&lt;/b&gt;으로 활용함으로써 대용량 시계열 데이터를 효율적이고 안정적으로 적재할 수 있는 기반을 마련했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12&quot;&gt;3. SQL 트랜잭션을 활용한 멱등성(Idempotency) 확보&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;가장 공을 들인 부분은 load_to_snowflake 태스크의 적재 전략입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;전략&lt;/b&gt;: TEMP TABLE 생성 &amp;rarr; COPY INTO로 데이터 로드 &amp;rarr; 최종 테이블 TRUNCATE &amp;rarr; INSERT&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;의도&lt;/b&gt;: 2편에서 강조했던 &lt;b data-index-in-node=&quot;14&quot; data-path-to-node=&quot;14,1,0&quot;&gt;멱등성&lt;/b&gt;을 실무적으로 구현한 부분입니다. 네트워크 오류 등으로 DAG가 재실행되더라도 데이터가 중복으로 쌓이지 않고, 항상 최신의 단일 상태를 유지하게 하여 데이터 정합성을 보장했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15&quot;&gt;4. Python Decorators를 통한 가독성 높은 DAG 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;Airflow의 최신 방식인 Taskflow API(@dag, @task)를 사용하여 파이프라인을 구축했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,0,0&quot;&gt;장점&lt;/b&gt;: 기존의 Operator 방식보다 태스크 간 데이터 흐름(XCom)이 직관적으로 보이며, 코드의 가독성이 높아 유지보수가 용이합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Dag 2: lowess feature engineering&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;933&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOsHW/dJMcadgmX76/4vhLa3kRkkdIvuKVFVASLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOsHW/dJMcadgmX76/4vhLa3kRkkdIvuKVFVASLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOsHW/dJMcadgmX76/4vhLa3kRkkdIvuKVFVASLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOsHW%2FdJMcadgmX76%2F4vhLa3kRkkdIvuKVFVASLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;515&quot; height=&quot;304&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;933&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1767853564350&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from airflow.decorators import dag, task
from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
from airflow.providers.amazon.aws.hooks.s3 import S3Hook
from datetime import datetime
import pandas as pd
import numpy as np
import os
import warnings
from statsmodels.nonparametric.smoothers_lowess import lowess

# battery_dag_01_load.py의 S3 업로드 패턴과 동일한 형태로 구성 
S3_BUCKET = &quot;bucket&quot;
S3_PREFIX = &quot;battery/preprocess/&quot;
AWS_CONN_ID = &quot;aws_conn&quot;
SNOWFLAKE_CONN_ID = 'snowflake_conn'

# 입력 CSV (discharge only)
# CSV_PATH = &quot;/opt/airflow/data/B0005_discharge.csv&quot;
BATTERY_ID = &quot;B0007&quot;

SNOWFLAKE_DB = &quot;BATTERY_DATABASE&quot;
SNOWFLAKE_SCHEMA = &quot;RAW_DATA&quot;
SOURCE_TABLE = f&quot;BATTERY_{BATTERY_ID}_RAW&quot;      # BATTERY_B0005_RAW
TARGET_TABLE = f&quot;BATTERY_{BATTERY_ID}_LOWESS&quot;   # BATTERY_B0005_LOWESS

SNOWFLAKE_INTERNAL_STAGE_PATH = &quot;@~/battery_upload&quot;

# build_dataset.py 기본값 
LOWESS_FRAC = 0.05

# build_dataset.py에서 LOWESS 대상 컬럼 
TARGET_COLS = [
    &quot;Voltage_measured&quot;,
    &quot;Current_measured&quot;,
    &quot;Temperature_measured&quot;,
    &quot;Current_load&quot;,
    &quot;Voltage_load&quot;,
]

# build_dataset.py에서 drop 대상 컬럼 
DROP_COLS = [&quot;start_time_raw&quot;, &quot;Capacity&quot;, &quot;type&quot;, &quot;ambient_temperature&quot;, &quot;Time&quot;]


def apply_lowess_by_cycle(df: pd.DataFrame, col: str, frac: float) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;
    build_dataset.py의 apply_lowess 로직(사이클별 LOWESS -&amp;gt; smooth/residual/trend) 동일 구현
    &quot;&quot;&quot;
    smooth_data, residual_data, trend_data, indices = [], [], [], []

    for cycle in sorted(df[&quot;cycle_idx&quot;].unique()):
        mask = df[&quot;cycle_idx&quot;] == cycle
        cycle_idx = df[mask].index
        values = df.loc[mask, col].values

        n = len(values)
        if n == 0:
            continue

        time_idx = np.arange(n)
        with warnings.catch_warnings():
            warnings.simplefilter(&quot;ignore&quot;)
            smoothed = lowess(values, time_idx, frac=frac, return_sorted=False)

        residual = values - smoothed
        trend = np.gradient(smoothed)

        smooth_data.extend(smoothed)
        residual_data.extend(residual)
        trend_data.extend(trend)
        indices.extend(cycle_idx)

    df[f&quot;{col}_smooth&quot;] = pd.Series(smooth_data, index=indices).reindex(df.index)
    df[f&quot;{col}_residual&quot;] = pd.Series(residual_data, index=indices).reindex(df.index)
    df[f&quot;{col}_trend&quot;] = pd.Series(trend_data, index=indices).reindex(df.index)

    return df


@dag(
    dag_id=&quot;battery_dag_02_load&quot;,
    start_date=datetime(2024, 12, 1),
    schedule=None,
    catchup=False,
    tags=[&quot;battery&quot;, &quot;discharge&quot;, &quot;lowess&quot;, &quot;dataset&quot;],
)
def battery_build_dataset_discharge_lowess_pipeline():
    @task
    def extract_and_preprocess_discharge() -&amp;gt; str:
        &quot;&quot;&quot;
        Snowflake의 RAW 테이블(BATTERY_B0005_RAW)에서 데이터를 읽어서
        정렬/전처리 후 로컬 tmp CSV로 저장
        &quot;&quot;&quot;
        hook = SnowflakeHook(snowflake_conn_id=SNOWFLAKE_CONN_ID)

        sql = f&quot;&quot;&quot;
            SELECT
                CYCLE_IDX,
                VOLTAGE_MEASURED,
                CURRENT_MEASURED,
                TEMPERATURE_MEASURED,
                CURRENT_LOAD,
                VOLTAGE_LOAD
            FROM {SNOWFLAKE_DB}.{SNOWFLAKE_SCHEMA}.{SOURCE_TABLE}
            ORDER BY CYCLE_IDX
        &quot;&quot;&quot;

        # SnowflakeHook는 pandas df를 바로 받을 수 있습니다.
        df = hook.get_pandas_df(sql)

        # 1. 실제 Snowflake에서 넘어온 컬럼 확인 (디버깅 핵심)
        print(&quot;DEBUG [raw Snowflake columns]:&quot;, df.columns.tolist())

         # 2. 컬럼명 표준화 (대문자 &amp;rarr; DAG 전체 기준 컬럼명)
        rename_map = {
            &quot;CYCLE_IDX&quot;: &quot;cycle_idx&quot;,
            &quot;VOLTAGE_MEASURED&quot;: &quot;Voltage_measured&quot;,
            &quot;CURRENT_MEASURED&quot;: &quot;Current_measured&quot;,
            &quot;TEMPERATURE_MEASURED&quot;: &quot;Temperature_measured&quot;,
            &quot;CURRENT_LOAD&quot;: &quot;Current_load&quot;,
            &quot;VOLTAGE_LOAD&quot;: &quot;Voltage_load&quot;,
        }

        df = df.rename(columns=rename_map)

        # (방어) drop (존재하는 컬럼만)
        df = df.drop([c for c in DROP_COLS if c in df.columns], axis=1)

        # 정렬/인덱스 정리
        df = df.sort_values([&quot;cycle_idx&quot;]).reset_index(drop=True)

        file_path = f&quot;/tmp/{BATTERY_ID}_discharge_preprocessed.csv&quot;
        df.to_csv(file_path, index=False)

        print(f&quot;✓ Loaded from Snowflake {SOURCE_TABLE}: {len(df)} rows, cycles {df['cycle_idx'].min()}-{df['cycle_idx'].max()}&quot;)
        return file_path

    @task
    def validate_data(file_path: str) -&amp;gt; str:
        &quot;&quot;&quot;
        데이터 검증
        &quot;&quot;&quot;
        df = pd.read_csv(file_path)

        assert len(df) &amp;gt; 0, &quot;Empty dataframe&quot;
        assert &quot;cycle_idx&quot; in df.columns, &quot;cycle_idx missing&quot;
        assert df.isnull().sum().sum() == 0, &quot;Missing values detected&quot;

        # LOWESS 대상 컬럼 중 실제 존재하는 컬럼만 검증
        existing_targets = [c for c in TARGET_COLS if c in df.columns]
        assert len(existing_targets) &amp;gt; 0, f&quot;No target cols exist among {TARGET_COLS}&quot;

        # load 파이프라인에서도 voltage &amp;gt; 0 검증을 했으므로 유지
        if &quot;Voltage_measured&quot; in df.columns:
            assert (df[&quot;Voltage_measured&quot;] &amp;gt; 0).all(), &quot;Invalid Voltage_measured (&amp;lt;=0) detected&quot;

        print(f&quot;✓ Validation passed: {len(df)} rows, target_cols={existing_targets}&quot;)
        return file_path

    @task
    def build_lowess_features(file_path: str) -&amp;gt; str:
        &quot;&quot;&quot;
        build_dataset.py의 apply_lowess(discharge-only) 구현 
        &quot;&quot;&quot;
        df = pd.read_csv(file_path)

        # 존재하는 컬럼만 LOWESS 처리 (방어)
        existing_targets = [c for c in TARGET_COLS if c in df.columns]
        for col in existing_targets:
            print(f&quot;LOWESS 처리 중: {col}&quot;)
            df = apply_lowess_by_cycle(df, col=col, frac=LOWESS_FRAC)

        out_path = f&quot;/tmp/{BATTERY_ID}_discharge_with_lowess_features.csv&quot;
        df.to_csv(out_path, index=False)

        print(f&quot;✓ LOWESS done: {out_path}, shape={df.shape}&quot;)
        return out_path

    @task
    def upload_to_s3(file_path: str) -&amp;gt; str:
        &quot;&quot;&quot;
        s3 업로드 
        &quot;&quot;&quot;
        s3_hook = S3Hook(aws_conn_id=AWS_CONN_ID)
        s3_key = f&quot;{S3_PREFIX}{os.path.basename(file_path)}&quot;

        s3_hook.load_file(
            filename=file_path,
            key=s3_key,
            bucket_name=S3_BUCKET,
            replace=True,
        )

        print(f&quot;✓ Uploaded to s3://{S3_BUCKET}/{s3_key}&quot;)
        return s3_key
    
    @task
    def load_to_snowflake(file_path: str):
        &quot;&quot;&quot;LOWESS 결과를 Snowflake(BATTERY_B0005_LOWESS)에 적재 (S3 경유 X)&quot;&quot;&quot;
        hook = SnowflakeHook(snowflake_conn_id=SNOWFLAKE_CONN_ID)
        conn = hook.get_conn()
        cur = conn.cursor()

        try:
            cur.execute(f&quot;USE DATABASE {SNOWFLAKE_DB};&quot;)
            cur.execute(f&quot;USE SCHEMA {SNOWFLAKE_SCHEMA};&quot;)

            # 결과 CSV를 읽어서 컬럼 목록 기반으로 테이블 스키마 생성(간단 매핑)
            df = pd.read_csv(file_path)
            cols = df.columns.tolist()

            # cycle_idx는 INT, 나머지는 FLOAT로 가정 (LOWESS 파생은 모두 수치)
            col_defs = []
            for c in cols:
                if c == &quot;cycle_idx&quot;:
                    col_defs.append(f&quot;{c} INT&quot;)
                else:
                    col_defs.append(f&quot;{c} FLOAT&quot;)

            create_sql = f&quot;&quot;&quot;
                CREATE TABLE IF NOT EXISTS {TARGET_TABLE} (
                    {&quot;, &quot;.join(col_defs)}
                );
            &quot;&quot;&quot;
            cur.execute(create_sql)

            # 임시 테이블 (동일 스키마)
            cur.execute(f&quot;CREATE TEMP TABLE temp_lowess LIKE {TARGET_TABLE};&quot;)

            # PUT: 로컬 파일 -&amp;gt; Snowflake 내부 stage
            abs_path = os.path.abspath(file_path)
            filename = os.path.basename(abs_path)
            cur.execute(f&quot;PUT 'file://{abs_path}' {SNOWFLAKE_INTERNAL_STAGE_PATH} auto_compress=false overwrite=true;&quot;)

            # COPY: 내부 stage -&amp;gt; temp
            cur.execute(f&quot;&quot;&quot;
                COPY INTO temp_lowess
                FROM {SNOWFLAKE_INTERNAL_STAGE_PATH}/{filename}
                FILE_FORMAT = (TYPE = 'CSV' SKIP_HEADER = 1 FIELD_OPTIONALLY_ENCLOSED_BY='&quot;')
                ON_ERROR = 'ABORT_STATEMENT';
            &quot;&quot;&quot;)

            # 풀 리프레시(기존 패턴 유지)
            cur.execute(f&quot;TRUNCATE TABLE {TARGET_TABLE};&quot;)
            cur.execute(f&quot;INSERT INTO {TARGET_TABLE} SELECT * FROM temp_lowess;&quot;)

            result = cur.execute(f&quot;SELECT COUNT(*) FROM {TARGET_TABLE};&quot;).fetchone()
            print(f&quot;✓ Total rows in {TARGET_TABLE}: {result[0]}&quot;)

        finally:
            cur.close()
            conn.close()
    # Task 의존성 
    preprocessed = extract_and_preprocess_discharge()
    validated = validate_data(preprocessed)
    lowess_csv = build_lowess_features(validated)
    upload_to_s3(lowess_csv)
    load_to_snowflake(lowess_csv)


battery_build_dataset_discharge_lowess_pipeline()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;1. 도메인 지식을 반영한 통계적 피처 엔지니어링 (LOWESS)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;로직&lt;/b&gt;: apply_lowess_by_cycle 함수를 통해 배터리 사이클별로 데이터를 분할하고, 센서 노이즈가 제거된 smooth, 원본과의 차이인 residual, 변화율인 trend라는 3가지 새로운 피처를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 배터리 센서 데이터는 미세한 전압 변화가 중요하지만 노이즈에 취약합니다. 이를 단순히 머신러닝 모델에 넣기보다, 통계적 평활화(Smoothing)를 선행하여 모델이 데이터의 본질적인 패턴(열화 경향)을 더 잘 학습할 수 있도록 설계했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7&quot;&gt;2. 'Fail-Fast'를 위한 방어적 데이터 검증&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;로직&lt;/b&gt;: validate_data 태스크에서 cycle_idx 존재 여부, 결측치(Null) 검사, 전압값 유효성 등을 다시 한번 체크합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: 전처리는 연산 비용이 높습니다. 잘못된 데이터가 전처리 단계로 진입하여 리소스를 낭비하지 않도록, 중간 관문을 두어 파이프라인의 효율성을 높였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9&quot;&gt;3. Snowflake 내부 스테이지(Internal Stage) 활용 능력&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;로직&lt;/b&gt;: load_to_snowflake에서 PUT 명령어를 사용해 로컬 파일을 Snowflake의 내부 스테이지(@~/battery_upload)로 직접 업로드한 뒤 COPY INTO를 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: dag 1에서는 S3(외부 스테이지)를 썼다면, dag 2에서는 &lt;b data-index-in-node=&quot;40&quot; data-path-to-node=&quot;10,1,0&quot;&gt;Snowflake 고유의 내부 스테이지&lt;/b&gt;를 활용했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;4. 유연한 스키마 설계 (Dynamic Schema Generation)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;로직&lt;/b&gt;: Pandas DataFrame의 컬럼 리스트를 기반으로 Snowflake 테이블의 CREATE TABLE 문을 동적으로 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;엔지니어링 의도&lt;/b&gt;: LOWESS 처리를 거치면 기존 피처 수의 3배가 넘는 컬럼이 생성됩니다. 이를 하드코딩하지 않고 &lt;b data-index-in-node=&quot;65&quot; data-path-to-node=&quot;12,1,0&quot;&gt;코드 기반으로 스키마를 동적 생성&lt;/b&gt;하게 함으로써, 추후 가공 피처가 추가되거나 변경되어도 파이프라인 수정 없이 대응할 수 있는 확장성을 확보했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Data Engineering</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/82</guid>
      <comments>https://gguzunhagae.tistory.com/82#entry82comment</comments>
      <pubDate>Thu, 8 Jan 2026 11:42:30 +0900</pubDate>
    </item>
    <item>
      <title> [Blog series] Airflow로 구축하는 NASA 배터리 파이프라인-2</title>
      <link>https://gguzunhagae.tistory.com/81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 성공하는 파이프라인보다 중요한 것은, 실패해도 안전한 파이프라인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 엔지니어링의 세계에서 &quot;완벽한 시스템&quot;이란 존재하지 않습니다. 네트워크 장애, 데이터 소스의 급격한 변화, 혹은 예상치 못한 서버 다운까지, 우리가 구축한 파이프라인은 언제나 실패할 가능성에 노출되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진정한 엔지니어링 역량은 멱등성과 트랜잭션으로 완성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;저 역시 NASA 배터리 데이터 파이프라인을 구축하며 Raw data load와 연산량이 많은 LOWESS 전처리 과정에서 여러 차례의 태스크 중단을 경험했습니다. 이때 제가 고민한 것은 단순히 에러를 고치는 것이 아니라, 수십 번을 재실행해도 데이터 정합성이 깨지지 않는 구조를 만드는 것이었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 안정적인 MLOps 시스템의 근간이 되는 설계 원칙들을 살펴보고, 제가 프로젝트에서 장애를 대비해 어떤 '안전장치'들을 설계했는지 공유하고자 합니다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;멱등성이란?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성은 어떤 연산이나 작업을 여러 번 반복해도 결과가 한 번 했을 때와 달라지지 않는 성질을 말합니다. 특히, API와 HTTP 설계에서 중요한 개념입니다. &lt;span&gt;f(f(x))=f(x)&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성을 보장하면 네트워크 장애로 요청이 중복 전송되더라도 데이터가 중복으로 생성되거나 깨지는 위험을 줄일 수 있습니다. 그래서 ​결제 API를 설계할 때는 같은 작업을 여러 번 전송해도 결과가 한 번만 반영되도록 &quot;멱등 키&quot; 같은 것을 만들어 멱등성을 구현하는 경우가 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹, API 에서 멱등성은 &quot;동일한 요청을 여러 번 보내도 서버 상태와 결과가 첫 요청과 같게 유지되는 성질&quot;을 뜻합니다. 예를 들어 HTTP GET, PUT, DELETE 같은 메서드는 올바르게 설계하면 여러 번 호출하더라도 서버의 최종 상태가 한 번 호출했을 때와 동일하므로 멱등 메서드로 간주됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1767838915064&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 멱등적 연산 예시
user.update(name=&quot;John&quot;)  # 여러 번 호출해도 같은 결과
user.update(name=&quot;John&quot;)  

# 비멱등적 연산 예시
user.increment_count() -&amp;gt; 3  # 호출할 때마다 값이 변함
user.increment_count() -&amp;gt; 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성이 깨지면 아래와 같은 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1767838949161&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 나쁜 예: 멱등성 없음
def load_data(date):
    df = extract_data(date)
    df = transform(df)
    # 매번 INSERT만 함 &amp;rarr; 중복 데이터 발생!
    db.execute(f&quot;INSERT INTO sales VALUES {df}&quot;)

# 실행 결과
# 1차 실행: sales 테이블에 100건
# 2차 실행: sales 테이블에 200건 (중복!)
# 3차 실행: sales 테이블에 300건 (중복!)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성 확보 방법&lt;/p&gt;
&lt;pre id=&quot;code_1767839045935&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 좋은 예 1: DELETE + INSERT
def load_data(date):
    df = extract_data(date)
    df = transform(df)
    
    # 해당 날짜 데이터 먼저 삭제
    db.execute(f&quot;DELETE FROM sales WHERE date = '{date}'&quot;)
    # 새로 삽입
    db.execute(f&quot;INSERT INTO sales VALUES {df}&quot;)

# 좋은 예 2: MERGE (UPSERT)
def load_data(date):
    df = extract_data(date)
    df = transform(df)
    
    # 있으면 UPDATE, 없으면 INSERT
    db.execute(f&quot;&quot;&quot;
        MERGE INTO sales USING staged_data
        ON sales.id = staged_data.id
        WHEN MATCHED THEN UPDATE SET ...
        WHEN NOT MATCHED THEN INSERT ...
    &quot;&quot;&quot;)

# 좋은 예 3: TRUNCATE + INSERT (전체 교체)
def load_data():
    df = extract_all_data()
    df = transform(df)
    
    db.execute(&quot;TRUNCATE TABLE sales&quot;)
    db.execute(f&quot;INSERT INTO sales VALUES {df}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SQL transaction 이란?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 트랜잭션(Transaction)은 &lt;b&gt;데이터베이스에서 하나의 논리적 작업 단위로 처리되는 일련의 SQL 연산들&lt;/b&gt;이다. 트랜잭션은 데이터베이스의 상태를 변경하는 여러 SQL 문(INSERT, UPDATE, DELETE 등)을 하나의 논리적인 작업으로 묶은 것이다. &lt;b&gt;묶음 안에 있는 연산들은 전부 성공해서 반영되거나, 하나라도 실패하면 전체를 원래 상태로 되돌려야 한다는 특징&lt;/b&gt;을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 계좌이체에서 &amp;ldquo;A 계좌에서 1만원 출금&amp;rdquo;과 &amp;ldquo;B 계좌에 1만원 입금&amp;rdquo; 두 UPDATE가 하나의 트랜잭션이 되고, 둘 다 성공해야만 실제로 돈이 옮겨진 것으로 인정된다.&lt;/p&gt;
&lt;pre id=&quot;code_1767839555104&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Transaction 없이 (위험)
def load_without_transaction(date):
    db.execute(f&quot;DELETE FROM sales WHERE date = '{date}'&quot;)
    # ⚠️ 여기서 에러 발생하면?
    # &amp;rarr; DELETE는 완료, INSERT는 안됨 &amp;rarr; 데이터 유실!
    db.execute(f&quot;INSERT INTO sales VALUES {df}&quot;)

# Transaction 사용 (안전)
def load_with_transaction(date):
    try:
        db.begin_transaction()
        
        db.execute(f&quot;DELETE FROM sales WHERE date = '{date}'&quot;)
        db.execute(f&quot;INSERT INTO sales VALUES {df}&quot;)
        
        db.commit()  # 모두 성공 시 저장
    except Exception as e:
        db.rollback()  # 하나라도 실패 시 전부 취소
        raise e&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Atomicity (원자성)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 연산이 완전히 수행되거나, 전혀 수행되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Consistency (일관성)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 완료 후 데이터베이스가 일관된 상태 유지&lt;/li&gt;
&lt;li&gt;제약조건, 규칙 위반 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Isolation (격리성)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시 실행되는 트랜잭션들이 서로 간섭하지 않음&lt;/li&gt;
&lt;li&gt;각 트랜잭션은 독립적으로 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Durability (지속성)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;완료된 트랜잭션 결과는 영구적으로 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1767839285674&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 주요 명령어

BEGIN;  -- 트랜잭션 시작
-- SQL 연산들
COMMIT;  -- 변경사항 확정
-- 또는
ROLLBACK;  -- 변경사항 취소
-- 또는
SAVEPOINT; -- 중간 롤백을 위한 저장 지점&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;※ 멱등성과 SQL 트랜잭션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 번 실행해도 결과가 동일한 &lt;b&gt;멱등성&lt;/b&gt; + 여러 작업을 하나의 논리적 작업 단위로 묶어 안전하게 실행하는 &lt;b&gt;SQL 트랜잭션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; ETL 및 Airflow 재실행 시 안정성 확보 측면에서 유리함&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;멱등성과 transaction을 고려한 핵심 설계 원칙&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 반복 실행 전제 (Rerunnable Design)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETL = 스케줄러 기반 반복 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;darr; 과거 데이터도 재실행 가능해야 함 (Backfill)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;darr; 설계 단계부터 &quot;여러 번 실행&quot; 가정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Backfill이 중요한 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 누락 발견 시 과거 기간 재처리&lt;/li&gt;
&lt;li&gt;로직 변경 시 전체 데이터 재계산&lt;/li&gt;
&lt;li&gt;Airflow가 backfill 지원 우수&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Load 실행 전 안전 장치 고려&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Extract &amp;rarr; Transform &amp;rarr; Load&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;darr;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재실행 O&amp;nbsp; &amp;nbsp; 재실행 O&amp;nbsp; &amp;nbsp; 되돌리기 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span data-token-index=&quot;0&quot;&gt;Load의 특수성&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에 한 번 적재되면 복구 어려움&lt;/li&gt;
&lt;li&gt;잘못된 데이터가 downstream으로 전파&lt;/li&gt;
&lt;li&gt;비즈니스 의사결정에 직접 영향&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;2&quot;&gt;Load 설계 시 고려사항&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;멱등성 확보 (DELETE+INSERT / MERGE)&lt;/li&gt;
&lt;li&gt;Transaction 사용&lt;/li&gt;
&lt;li&gt;적재 전 validation (row count, null check)&lt;/li&gt;
&lt;li&gt;적재 후 검증 로직&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 파이프라인에서 멱등성이 무너지면 중복 데이터가 발생하고, 이는 결국 잘못된 분석 결과로 이어져 비즈니스 의사결정을 망칩니다. 이번 설계를 통해 &lt;b data-index-in-node=&quot;85&quot; data-path-to-node=&quot;6,0&quot;&gt;엔지니어링의 사소한 빈틈이 데이터의 신뢰성을 어떻게 파괴할 수 있는지&lt;/b&gt;를 체감했습니다.&lt;/p&gt;</description>
      <category>Data Engineering</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/81</guid>
      <comments>https://gguzunhagae.tistory.com/81#entry81comment</comments>
      <pubDate>Thu, 8 Jan 2026 11:36:11 +0900</pubDate>
    </item>
    <item>
      <title> [Blog series] Airflow로 구축하는 NASA 배터리 파이프라인-1</title>
      <link>https://gguzunhagae.tistory.com/80</link>
      <description>&lt;h2 data-path-to-node=&quot;4,0&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0&quot;&gt; 제1편: 데이터 엔지니어링의 기초와 Airflow 도입 배경 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;4,0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0&quot;&gt;NASA 배터리 데이터를 활용&lt;/b&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0&quot;&gt;한 Airflow 파이프라인 구축기&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4,1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4,1&quot; data-ke-size=&quot;size16&quot;&gt;데이터 분석가나 ML 엔지니어가 가장 많은 시간을 쏟는 곳은 역설적이게도 '모델링'이 아닌 '데이터 준비' 단계입니다. 저 또한 NASA의 배터리 충방전 데이터를 분석하며, 복잡한 시계열 데이터를 수동으로 전처리하는 과정에서 휴먼 에러와 비효율이라는 벽에 부딪혔습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4,2&quot; data-ke-size=&quot;size16&quot;&gt;단순히 '한 번 돌아가는 코드'를 짜는 것은 어렵지 않습니다. 하지만 실제 운영 환경에서는 시스템이 멈추더라도 언제든 재실행 가능해야 하며, 데이터의 정합성이 깨지지 않아야 합니다. 이를 위해 저는 &lt;b data-index-in-node=&quot;111&quot; data-path-to-node=&quot;4,2&quot;&gt;Airflow&lt;/b&gt;를 도입하여 전처리 과정을 자동화하고, 엔지니어링의 핵심 원칙인 '멱등성(Idempotency)'과 '트랜잭션(Transaction)'을 설계에 녹여냈습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size16&quot;&gt;본 시리즈에서는 노션에 기록해 온 저의 기술 문서를 바탕으로, 안정적인 데이터 파이프라인을 구축하기 위한 저의 고민과 구현 과정을 상세히 공유하고자 합니다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size26&quot;&gt;시작하며: NASA 배터리 데이터를 선택한 이유와 프로젝트의 목적&lt;/h2&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;배터리 데이터셋을 선택한 이유는 다변량 시계열 이상 탐지에 적합하고, 국내 배터리 산업의 중요성 때문입니다. 최근 전기차 수요 둔화에도 불구하고 ESS(에너지 저장장치) 등 에너지 저장 시장이 가파르게 성장하고 있어 산업적 의의가 매우 큽니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;실제 ESS나 전기차 관리 시스템에서는 수천 개의 배터리 셀 데이터가 매일 밤 혹은 주기적인 배치(Batch) 단위로 중앙 서버에 전송됩니다. 저는 이 방대한 시계열 데이터를 수동 작업 없이 안정적으로 처리하기 위해 &lt;b data-index-in-node=&quot;121&quot; data-path-to-node=&quot;4&quot;&gt;워크플로우 오케스트레이션 도구인 Airflow를 도입&lt;/b&gt;했습니다. Airflow를 통해 복잡하게 얽힌 전처리 및 적재 과정을 하나의 유기적인 파이프라인으로 연결하고, 정해진 스케줄에 따라 작업을 자동화하여 운영 효율성을 극대화하고자 했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;특히, 본 프로젝트에서 사용한 LOWESS 스무딩은 노이즈 제거에 탁월하지만 연산 복잡도가 높습니다. 대규모 데이터를 한 번에 처리할 경우 시스템 부하로 인한 실패 가능성이 존재하므로, 이를 &lt;b data-index-in-node=&quot;107&quot; data-path-to-node=&quot;5&quot;&gt;Airflow 내에서 독립적인 Task로 분리하여 설계&lt;/b&gt;했습니다. 이를 통해 특정 단계에서 오류가 발생하더라도 전체 파이프라인을 멈추지 않고 &lt;b data-index-in-node=&quot;185&quot; data-path-to-node=&quot;5&quot;&gt;실패한 부분만 자동으로 재시도하거나 해당 지점부터 복구&lt;/b&gt;할 수 있는 관리의 편의성과 파이프라인의 탄력성을 확보했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;또한, 배터리 사이클별로 데이터 길이가 가변적이라는 특징은 데이터베이스 저장 시 스키마 설계의 유연성을 요구합니다. 고정된 테이블 구조에 이를 억지로 맞추려 하면 불필요한 NULL 값이 발생하고 저장 효율이 떨어지기 때문입니다. 이러한 가변적 데이터를 효율적으로 관리하기 위해 고성능 클라우드 데이터 웨어하우스인 &lt;b data-index-in-node=&quot;175&quot; data-path-to-node=&quot;6&quot;&gt;Snowflake&lt;/b&gt;를 선택하여, 대용량 시계열 데이터를 압축 저장하고 전처리 전후의 데이터를 신속하게 쿼리할 수 있는 최적의 환경을 구축했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IV2D8/dJMcajgzcMa/We92AyQFoX8RVeoNwqnQvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IV2D8/dJMcajgzcMa/We92AyQFoX8RVeoNwqnQvK/img.png&quot; data-alt=&quot;Airflow를 활용한 데이터 파이프라인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IV2D8/dJMcajgzcMa/We92AyQFoX8RVeoNwqnQvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIV2D8%2FdJMcajgzcMa%2FWe92AyQFoX8RVeoNwqnQvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;382&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Airflow를 활용한 데이터 파이프라인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size26&quot;&gt;데이터 파이프라인이란?:&amp;nbsp;분석가와&amp;nbsp;엔지니어&amp;nbsp;사이의&amp;nbsp;가교&amp;nbsp;역할&lt;/h2&gt;
&lt;h3 data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size23&quot;&gt;데이터 분석가와 데이터 엔지니어&lt;/h3&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;데이터 파이프라인 안에서 두 직무는 '데이터'라는 같은 재료를 다루지만, 그 목적과 과정에서 뚜렷한 차이가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;데이터 엔지니어 (Data Engineer): 데이터의 길을 닦는 사람&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,1,0,0&quot;&gt;핵심 역할:&lt;/b&gt; 산재한 Raw 데이터를 수집하여 분석 가능한 형태로 가공하고, 이를 안정적으로 저장소에 전달하는 인프라를 구축합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,1,1,0&quot;&gt;주요 과업:&lt;/b&gt; ETL/ELT 파이프라인 구축, 데이터 품질 관리, 워크플로우 자동화.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,1,2,0&quot;&gt;사용 도구:&lt;/b&gt; Airflow, Snowflake, Spark, Kafka 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;데이터 분석가 (Data Analyst): 데이터에서 답을 찾는 사람&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,1,0,0&quot;&gt;핵심 역할:&lt;/b&gt; 엔지니어가 닦아놓은 길(인프라)을 통해 들어온 데이터를 분석하여 비즈니스 의사결정에 필요한 인사이트를 도출합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,1,1,0&quot;&gt;주요 과업:&lt;/b&gt; 지표 정의, 통계 및 머신러닝 분석, 대시보드 시각화.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,1,2,0&quot;&gt;사용 도구:&lt;/b&gt; Python, SQL, Tableau, PowerBI 등&lt;i data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,1,3,0&quot;&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size16&quot;&gt;현업에서는 분석가가 신뢰할 수 있는 데이터를 바탕으로 모델을 만들기 위해, 엔지니어의 안정적인 파이프라인 구축이 선행되어야 합니다. 저는 두 영역의 접점인 '데이터 파이프라인 자동화'를 이해하기 위해 Airflow를 학습하며, 엔지니어링적 안정성과 분석적 가치를 동시에 확보하는 것을 목표로 삼았습니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size23&quot;&gt;데이터 파이프라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 생성 &amp;rarr; 수집 &amp;rarr; 저장 &amp;rarr; 가공 &amp;rarr; 분석의 전체 과정을 하나의 흐름으로 정의하고 자동화하는 시스템입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적으로 모델 개발과 운영을 연결하는 핵심 인프라입니다. 수동 작업 없이 새로운 데이터가 지속적으로 모델에 반영되어 예측 성능을 유지하고, 실시간 배포 환경에서 안정적인 서비스를 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 데이터 생성 (Data Generation)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;센서, 로그, 트랜잭션 등에서 원시 데이터 발생&lt;/li&gt;
&lt;li&gt;본 프로젝트: NASA 배터리 충방전 사이클 데이터 (전압, 전류, 온도, 용량 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 데이터 수집 (Data Ingestion)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산된 소스에서 데이터를 중앙화&lt;/li&gt;
&lt;li&gt;본 프로젝트: Local CSV &amp;rarr; S3 &amp;rarr; Snowflake 적재 (Airflow DAG 1)&lt;/li&gt;
&lt;li&gt;실무 시나리오: ESS/전기차의 수천 개 셀 데이터를 배치 단위로 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 데이터 저장 (Data Storage)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Raw 데이터와 가공 데이터를 구조화하여 보관&lt;/li&gt;
&lt;li&gt;본 프로젝트: Snowflake 3-layer 구조 (Raw &amp;rarr; Processed &amp;rarr; Predictions)&lt;/li&gt;
&lt;li&gt;가변 길이 시계열 데이터 효율적 압축 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 데이터 가공 (Data Transformation)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노이즈 제거, 특징 추출, 정규화 등 전처리&lt;/li&gt;
&lt;li&gt;본 프로젝트: LOWESS 스무딩으로 노이즈 제거 + 통계적 특징 추출 (Airflow DAG 2)&lt;/li&gt;
&lt;li&gt;Task 단위 분리로 고연산 작업의 독립적 실행 및 재시도 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 데이터 분석 (Data Analysis)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;머신러닝 모델 학습 및 예측 수행&lt;/li&gt;
&lt;li&gt;본 프로젝트: LOF + Anomaly Transformer 학습 &amp;rarr; 이상 점수 산출 (Airflow DAG 3)&lt;/li&gt;
&lt;li&gt;MLflow 실험 추적, Streamlit 대시보드 시각화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자동화의 핵심 가치&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스케줄 기반 실행&lt;/b&gt;: 정해진 시간에 파이프라인 자동 트리거 (cron expression)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 관리&lt;/b&gt;: 이전 단계 성공 시에만 다음 단계 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오류 복구&lt;/b&gt;: Task 실패 시 자동 재시도, 특정 지점부터 복구 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링&lt;/b&gt;: 각 단계별 실행 상태 및 로그 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 파이프라인 문서화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 파이프라인 문서화는 &lt;b&gt;데이터의 출처, 변환 과정, 저장 위치, 품질 규칙 등을 체계적으로 기록하는 작업&lt;/b&gt;입니다. 목적은 장애 대응, 변경 영향 분석, 신규 인력 온보딩, 재현 가능한 분석(데이터/실험)을 가능하게 하는 것입니다. 실무에서는 설계 문서 + 다이어그램(DAG, 데이터 흐름도) + 각 테이블/컬럼 메타데이터 + 운영/장애 기록(포스트모템) 정도가 한 세트가 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 카탈로그&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정의:&lt;/b&gt; 조직 내 모든 데이터 자산의 메타데이터를 중앙에서 관리하는 시스템(무슨 데이터인지 설명하는 데이터 자산의 사전 역할)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포함 정보:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터셋 이름, 위치, 스키마&lt;/li&gt;
&lt;li&gt;소유자, 생성일, 업데이트 주기&lt;/li&gt;
&lt;li&gt;데이터 품질 지표&lt;/li&gt;
&lt;li&gt;비즈니스 용어 설명&lt;/li&gt;
&lt;li&gt;접근 권한 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1767685540214&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 데이터 카탈로그 메타데이터 예시
{
    &quot;table_name&quot;: &quot;customer_transactions&quot;,
    &quot;location&quot;: &quot;s3://bucket/data/transactions/&quot;,
    &quot;schema&quot;: {
        &quot;user_id&quot;: &quot;INTEGER&quot;,
        &quot;amount&quot;: &quot;DECIMAL(10,2)&quot;,
        &quot;timestamp&quot;: &quot;TIMESTAMP&quot;
    },
    &quot;owner&quot;: &quot;data_team@company.com&quot;,
    &quot;update_frequency&quot;: &quot;daily&quot;,
    &quot;last_updated&quot;: &quot;2024-12-15&quot;,
    &quot;description&quot;: &quot;고객 거래 내역 데이터&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;4,3&quot; data-ke-size=&quot;size20&quot;&gt;데이터 리니지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 리니지(lineage)는 데이터가 &amp;ldquo;원천 &amp;rarr; 중간 산출물 &amp;rarr; 최종 테이블/리포트&amp;rdquo;로 이동 및 변환되는 전체 경로와 의존성을 기록 및 시각화한 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FnosQ/dJMcag5g0m4/9GPrKD9iHpsuMqQ13uUwp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FnosQ/dJMcag5g0m4/9GPrKD9iHpsuMqQ13uUwp1/img.png&quot; data-alt=&quot;데이터 리니지 개념도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FnosQ/dJMcag5g0m4/9GPrKD9iHpsuMqQ13uUwp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFnosQ%2FdJMcag5g0m4%2F9GPrKD9iHpsuMqQ13uUwp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;475&quot; height=&quot;316&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 리니지 개념도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0J4u9/dJMcagc71E9/SAnLtIQHSCwHEckcIe8Ed1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0J4u9/dJMcagc71E9/SAnLtIQHSCwHEckcIe8Ed1/img.png&quot; data-alt=&quot;데이터 리니지 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0J4u9/dJMcagc71E9/SAnLtIQHSCwHEckcIe8Ed1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0J4u9%2FdJMcagc71E9%2FSAnLtIQHSCwHEckcIe8Ed1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;484&quot; height=&quot;249&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 리니지 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리니지가 있으면 다음이 쉬워집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어느 소스가 장애 나면 어떤 다운스트림 테이블/리포트가 깨지는지 영향 분석&lt;/li&gt;
&lt;li&gt;컬럼 하나 삭제/정의 변경 시 어디까지 영향을 주는지 확인&lt;/li&gt;
&lt;li&gt;특정 지표가 &amp;ldquo;정확히 어떤 변환을 거쳤는지&amp;rdquo; 감사/설명(Explainability, Audit) 대응&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 문서화&amp;ndash;카탈로그&amp;ndash;리니지 관계&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 파이프라인 문서화: 전체 프로세스(작업 단위, 스케줄, 장애 대응 포함)에 대한 서술 중심&lt;/li&gt;
&lt;li&gt;데이터 카탈로그: &amp;ldquo;정지된 상태의 자산 목록&amp;rdquo;에 대한 정의와 설명 중심&lt;/li&gt;
&lt;li&gt;데이터 리니지: 자산들 사이의 &amp;ldquo;그래프(흐름/의존성)&amp;rdquo; 중심&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지를 같이 사용하면 어떤 테이블이 무슨 의미인지(카탈로그), 어디서 어떻게 만들어졌는지(리니지), 파이프라인 입‧출력, 스케줄, 운영 방식이 무엇인지(문서화)를 한 번에 이어서 볼 수 있어서, 데이터 규모가 커질수록 필수에 가깝게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ETL과 ELT의 차이&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-18 115624.png&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WtgZ9/dJMcah4apap/Kcr2mDpf4eAwiJV7pKQHKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WtgZ9/dJMcah4apap/Kcr2mDpf4eAwiJV7pKQHKk/img.png&quot; data-alt=&quot;ETL &amp;amp;amp; ELT&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WtgZ9/dJMcah4apap/Kcr2mDpf4eAwiJV7pKQHKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWtgZ9%2FdJMcah4apap%2FKcr2mDpf4eAwiJV7pKQHKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1001&quot; height=&quot;357&quot; data-filename=&quot;스크린샷 2025-12-18 115624.png&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ETL &amp;amp; ELT&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ETL (Extract &amp;rarr; Transform &amp;rarr; Load) - 전통적 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 추출 &amp;rarr; 외부에서 변환 &amp;rarr; DB 저장&lt;/li&gt;
&lt;li&gt;장점: 변환 로직 집중 관리, 복잡한 Python/Spark 라이브러리 활용 가능, 민감 데이터 전처리 후 저장으로 보안 강화, DB 부하 감소&lt;/li&gt;
&lt;li&gt;단점: 변환 서버의 처리 능력이 병목, 확장성 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ELT (Extract &amp;rarr; Load &amp;rarr; Transform) - 현대적 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 추출 &amp;rarr; DB에 먼저 저장 &amp;rarr; DB 내에서 변환&lt;/li&gt;
&lt;li&gt;장점: 클라우드 DW의 강력한 컴퓨팅 파워 활용, SQL 기반 병렬 처리로 대용량 데이터 변환 빠름, 유지보수 용이, Raw 데이터 보존으로 재처리 유연&lt;/li&gt;
&lt;li&gt;단점: DB 컴퓨팅 비용 발생, SQL로 구현 어려운 복잡한 변환 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택 기준&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 변환 로직 + 외부 라이브러리 필요 (Python/Spark) &amp;rarr; ETL&lt;/li&gt;
&lt;li&gt;레거시 시스템 + 제한된 DB 성능 &amp;rarr; ETL&lt;/li&gt;
&lt;li&gt;단순 집계/조인 중심 + 클라우드 DW 활용 &amp;rarr; ELT&lt;/li&gt;
&lt;li&gt;데이터 크기 &amp;gt; 수십 GB + SQL 변환 가능 &amp;rarr; ELT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 프로젝트 적용: ETL 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSV 추출 &amp;rarr; S3 &amp;rarr; Snowflake Raw 적재 (DAG 1)&lt;/li&gt;
&lt;li&gt;Snowflake에서 추출 &amp;rarr; Airflow/Python에서 LOWESS 전처리 &amp;rarr; Snowflake Processed 적재 (DAG 2)&lt;/li&gt;
&lt;li&gt;Snowflake에서 추출 &amp;rarr; Python에서 모델 학습 &amp;rarr; Snowflake Predictions 적재 (DAG 3)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ETL 선택 이유:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;LOWESS 전처리&lt;/b&gt;: Python statsmodels 라이브러리 필수, Snowflake SQL로 구현 불가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 특징 추출&lt;/b&gt;: 통계 기반 Feature Engineering을 Python으로 모듈화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Task 독립성&lt;/b&gt;: Airflow에서 전처리를 별도 Task로 분리하여 실패 시 해당 단계만 재시도&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배치 처리와 실시간 처리의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치 처리 (Batch Processing)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;정의&lt;/b&gt;: 일정 주기(시간, 일, 주)에 따라 누적된 데이터를 한 번에 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조 간단, 디버깅 용이, 유지보수 비용 낮음&lt;/li&gt;
&lt;li&gt;높은 처리량(Throughput) - 대용량 데이터 효율적 처리&lt;/li&gt;
&lt;li&gt;지연 발생 (Latency) - 분~시간 단위&lt;/li&gt;
&lt;li&gt;실패 시 재처리 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기술 스택&lt;/b&gt;: Apache Airflow, Apache Spark, Cron&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 케이스&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일별 매출 집계, 월간 리포트 생성&lt;/li&gt;
&lt;li&gt;머신러닝 모델 학습 (historical data)&lt;/li&gt;
&lt;li&gt;데이터 웨어하우스 ETL&lt;/li&gt;
&lt;li&gt;예측 유지보수 (Predictive Maintenance)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실시간 처리 (Real-time/Stream Processing)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;정의&lt;/b&gt;: 데이터 발생 즉시 처리하여 밀리초~초 단위 응답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 의사결정 가능&lt;/li&gt;
&lt;li&gt;낮은 지연 (Low Latency)&lt;/li&gt;
&lt;li&gt;구조 복잡, 장애 대응 어려움, 운영 비용 높음&lt;/li&gt;
&lt;li&gt;데이터 순서 보장, 중복 처리 등 고려사항 많음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기술 스택&lt;/b&gt;: Apache Kafka, Apache Flink, AWS Kinesis, Spark Streaming&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 케이스&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이상 거래 탐지 (fraud detection)&lt;/li&gt;
&lt;li&gt;실시간 추천 시스템&lt;/li&gt;
&lt;li&gt;IoT 센서 모니터링 알람&lt;/li&gt;
&lt;li&gt;주식 트레이딩&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본 프로젝트 적용: 배치 처리 가정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;처리 방식&lt;/b&gt;: Airflow 스케줄 기반 배치 파이프라인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주기&lt;/b&gt;: 새 데이터 추가 시 또는 일정 주기로 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리 흐름&lt;/b&gt;: CSV &amp;rarr; S3 &amp;rarr; Snowflake &amp;rarr; LOWESS 전처리 &amp;rarr; 모델 학습 &amp;rarr; 예측 결과 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치 처리 선택 이유:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;도메인 특성&lt;/b&gt;: 배터리 열화는 수백~수천 사이클에 걸쳐 진행되는 점진적 현상, 즉각 대응 불필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 수집 패턴&lt;/b&gt;: ESS/전기차는 매일 밤 또는 주기적으로 배치 전송 (실시간 스트리밍 아님)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예측 유지보수&lt;/b&gt;: 사전 경고가 목적이므로 시간~일 단위 지연 허용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LOWESS 연산 복잡도&lt;/b&gt;: 고연산 전처리를 배치로 효율적 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 효율성&lt;/b&gt;: 단순한 파이프라인 구조로 소규모 팀 운영 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 시나리오&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ESS 관리 시스템에서 수천 개 배터리 셀의 충방전 데이터를 매일 밤 12시에 수집 &amp;rarr; Airflow가 자동으로 전처리 및 이상 탐지 수행 &amp;rarr; 다음날 아침 관리자에게 열화 위험 배터리 리스트 제공. 실시간 모니터링 대비 인프라 비용 1/3, 유지보수 인력 50% 절감 가능.&lt;/p&gt;</description>
      <category>Data Engineering</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/80</guid>
      <comments>https://gguzunhagae.tistory.com/80#entry80comment</comments>
      <pubDate>Tue, 6 Jan 2026 16:36:25 +0900</pubDate>
    </item>
    <item>
      <title>배터리 데이터를 활용한 다변량 시계열 이상탐지 모델 및 MLOps 파이프라인 개발 프로젝트</title>
      <link>https://gguzunhagae.tistory.com/79</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;주제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 분석 부트캠프 최종 주제로 '배터리셋을 활용한 다변량 시계열 이상탐지 모델 및 MLOps 파이프라인 개발 프로젝트'를 수행했다. 본 프로젝트는 배터리셋을 분석하는 다변량 시계열 이상탐지 모델을 개발하고, 실무 현장에 적용 가능한 데이터 파이프라인을 설계하는 것을 목표로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 연구 목표는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리튬 이온 배터리의 열화 진행 과정 정밀 관찰&lt;/li&gt;
&lt;li&gt;비지도 이상탐지 모델 개발&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열화는 단발성으로 일어나는 것이 아닌, 사이클에 따라 점진적으로 누적되는 시간적 추세(trend)를 가지는 현상이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CXuQb/dJMcaiIIxJU/Co8TbVKgvZldBxusmffyAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CXuQb/dJMcaiIIxJU/Co8TbVKgvZldBxusmffyAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CXuQb/dJMcaiIIxJU/Co8TbVKgvZldBxusmffyAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCXuQb%2FdJMcaiIIxJU%2FCo8TbVKgvZldBxusmffyAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;284&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리튬 이온 배터리에서 나타나는 열화는 크게 3가지가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLfB2D/dJMcadADftB/L1qXqGf5JxJF8j7rqskqS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLfB2D/dJMcadADftB/L1qXqGf5JxJF8j7rqskqS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLfB2D/dJMcadADftB/L1qXqGf5JxJF8j7rqskqS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLfB2D%2FdJMcadADftB%2FL1qXqGf5JxJF8j7rqskqS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;573&quot; height=&quot;238&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터셋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;배터리 데이터셋을 선택한 이유는 다변량 시계열 이상탐지에 적합하고, 국내 배터리 산업의 중요성 때문이다. 전기차 수요 둔화에도 불구하고 ESS 등 에너지 저장장치 시장이 성장하고 있어 산업적 의의가 크다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;데이터셋은 NASA PCoE Battery Dataset으로, RUL 및 이상탐지 연구의 벤치마크로 활용된다. 총 4개 배터리 셀로 구성되며, 실온에서 정격 용량 30%까지 실험했다.&lt;/span&gt;&lt;span&gt;각 셀은 종료 전압과 조건이 상이하다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;데이터는 충전, 방전, 임피던스로 구분되며, 하나의 방전을 1 사이클로 정의한다. 사이클별 길이는 가변적이고, 임피던스는 40 사이클 이후 측정되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;본 프로젝트는 방전 데이터만 사용했다. 선행 연구에서 주로 방전 데이터로 RUL과 이상탐지를 수행했으며, 열화 현상이 방전 데이터에서만 관찰되기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 Raw 데이터에 LOWESS smoothing 방법을 적용한 파생 feature 데이터셋을 실험에 사용했다. LOWESS(Locally Weighted Scatterplot Smoothing)는 국소 가중 회귀 기반 스무딩 기법으로, 센서 노이즈를 제거하면서 열화 추세를 보존한다. 다양한 도메인의 시계열 이상탐지 연구에서 LOWESS 적용 시 모델 성능이 향상된 바 있으며, 본 프로젝트에서도 노이즈 감소를 통한 이상 패턴 검출 성능 개선을 목적으로 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EDA&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터 전압, 전류, 온도와 열화와의 EDA 결과를 설명하겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전압&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/53763/dJMcahQCqBd/QlPQAIXfrTm9XsMKFMz5uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/53763/dJMcahQCqBd/QlPQAIXfrTm9XsMKFMz5uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/53763/dJMcahQCqBd/QlPQAIXfrTm9XsMKFMz5uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F53763%2FdJMcahQCqBd%2FQlPQAIXfrTm9XsMKFMz5uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;288&quot; height=&quot;227&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전압은 초기 방전 사이클에서는 특별한 패턴을 보이지 않다가 400 사이클 이후부터는 특별한 패턴을 보이기 시작했다. EDA 결과 전압은 열화에 가장 민감한 신호임을 알 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전류&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w6jfo/dJMcaiWfZAI/KSoWzWnPZ2oyZ7Bz89kSc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w6jfo/dJMcaiWfZAI/KSoWzWnPZ2oyZ7Bz89kSc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w6jfo/dJMcaiWfZAI/KSoWzWnPZ2oyZ7Bz89kSc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw6jfo%2FdJMcaiWfZAI%2FKSoWzWnPZ2oyZ7Bz89kSc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;281&quot; height=&quot;204&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전류는 방전 사이클 초기에는 큰 변화를 보였지만 후기부터는 안정적인 패턴을 확인할 수 있었다. 전압과 달리 열화가 지속될 수록 변동성이 급감한다는 점을 발견했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;온도&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IO1TA/dJMcahb0mXI/3yD4zM7iWGwGLGKGuK92i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IO1TA/dJMcahb0mXI/3yD4zM7iWGwGLGKGuK92i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IO1TA/dJMcahb0mXI/3yD4zM7iWGwGLGKGuK92i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIO1TA%2FdJMcahb0mXI%2F3yD4zM7iWGwGLGKGuK92i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;287&quot; height=&quot;218&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도는 열화의 누적을 반영하지만 초기 이상이나 특정 사이클에서의 이상을 직접적으로 나타내지 않는다. 열화가 뚜렷하지 않은 초기에도 변동이 클 수 있고, 열화가 나타나는 후기에도 변동이 클 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0CabA/dJMcaiBWiN8/OKdVU3UcJjKZyatiBydfpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0CabA/dJMcaiBWiN8/OKdVU3UcJjKZyatiBydfpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0CabA/dJMcaiBWiN8/OKdVU3UcJjKZyatiBydfpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0CabA%2FdJMcaiBWiN8%2FOKdVU3UcJjKZyatiBydfpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;523&quot; height=&quot;288&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;결론을 종합하자면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;배터리 열화는 모든 변수에서 동시에 나타나지 않으며 사이클마다 상이하다.&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;따라서 평균 용량 감소보다 순간적인 전압 불안정성(spike, noise)으로 먼저 감지된다. 또한, 가장 먼저 배터리 상태를 보여주는 것은 전압의 trend 속에서 나타나는 spike 혹은 noise이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Capacity와 사이클 평균 전압의 trend&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBp8LS/dJMcagEb3Ui/ufJ34AdNhs2Zwq2hpXwsAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBp8LS/dJMcagEb3Ui/ufJ34AdNhs2Zwq2hpXwsAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBp8LS/dJMcagEb3Ui/ufJ34AdNhs2Zwq2hpXwsAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBp8LS%2FdJMcagEb3Ui%2FufJ34AdNhs2Zwq2hpXwsAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;231&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열화는 선형적이지 않으며, 초기 안정 구간 &amp;rarr; 중기 급변 구간 &amp;rarr; 후기 저용량 구간으로 비선형적으로 진행된다. 또한, 열화 초기에는 다양한 변수가 열화에 영향을 미치지만 후기로 갈 수록 영향을 미치는 변수들이 줄어들고, 전압이 열화에 가장 큰 영향을 미치는 것을 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;머신러닝&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXk9Sh/dJMcabiziLA/OSVXhhxAegAlpQhKt5CSY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXk9Sh/dJMcabiziLA/OSVXhhxAegAlpQhKt5CSY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXk9Sh/dJMcabiziLA/OSVXhhxAegAlpQhKt5CSY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXk9Sh%2FdJMcabiziLA%2FOSVXhhxAegAlpQhKt5CSY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;206&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 연구에서는 대표적인 이상탐지 머신러닝 모델인 Isolation Forest와 LOF 모델을 비교하여 LOF 모델을 선택했다. LOF 모델은 Isolation Forest에 비해 더 local한 데이터 포인터 간 관계를 파악하는 데 적합하기 때문에 열화 국면에서 발생하는 국소적인 이상 반응을 명확히 포착하는 데 더 적합하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GHqgq/dJMcagYsMXM/Aq8mG8wnzRpi1dDruk16i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GHqgq/dJMcagYsMXM/Aq8mG8wnzRpi1dDruk16i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GHqgq/dJMcagYsMXM/Aq8mG8wnzRpi1dDruk16i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGHqgq%2FdJMcagYsMXM%2FAq8mG8wnzRpi1dDruk16i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;123&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 연구에서는 raw 데이터셋과 lowess 기반 feature 추출 방법을 사용한 데이터셋을 비교하여 실험했다. 분석 결과 사이클 별 최대 이상 점수보다 평균과 중앙 이상 점수의 R_ohmic 간 상관관계가 파생피처 사용 모델에서 더 높았다. 이는 LOWESS 파생 피처가 극단값의 영향을 줄이고 평균적 열화 경향을 포착하여 더 안정적인 이상 탐지를 가능하게 했음을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ R_ohmic이란&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배터리 내부 오믹 저항을 직접 반영하는 물리적 지표&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방전(load) 시작 시 발생하는 순간 전압 강하(IR drop)를 기반으로 계산&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Anomaly score와의 높은 상관성은 실제 물리적 열화 반영을 의미&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 기반 지표의 물리적 타당성 검증 근거&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;딥러닝&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuEVCx/dJMcai20ihd/11WaWVof6JSMQnhIwjHPBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuEVCx/dJMcai20ihd/11WaWVof6JSMQnhIwjHPBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuEVCx/dJMcai20ihd/11WaWVof6JSMQnhIwjHPBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuEVCx%2FdJMcai20ihd%2F11WaWVof6JSMQnhIwjHPBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;267&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 연구에서는 다양한 다변량 시계열 이상탐지 딥러닝 모델 중 Anomaly Transformer(AT)를 선택했다. AT를 선정한 이유도 머신러닝 모델 선정 이유와 동일하다. AT는 prior association과 series assocation 간의 차이를 minmax 전략을 사용해서 극대화하여 열화의 국소적인 이상 반응을 시간적인 문맥을 고려해서 명확히 포착할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CIPc7/dJMcabv6f4F/2mRhRt0tpZEMJAQKtFrhI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CIPc7/dJMcabv6f4F/2mRhRt0tpZEMJAQKtFrhI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CIPc7/dJMcabv6f4F/2mRhRt0tpZEMJAQKtFrhI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCIPc7%2FdJMcabv6f4F%2F2mRhRt0tpZEMJAQKtFrhI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;606&quot; height=&quot;211&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AT로&amp;nbsp;사이클별&amp;nbsp;이상&amp;nbsp;점수를&amp;nbsp;분석한&amp;nbsp;결과,&amp;nbsp;Raw&amp;nbsp;데이터에서는&amp;nbsp;capacity와의&amp;nbsp;상관관계가&amp;nbsp;-0.015로&amp;nbsp;거의&amp;nbsp;없었으나,&amp;nbsp;LOWESS&amp;nbsp;적용&amp;nbsp;후&amp;nbsp;-0.747의&amp;nbsp;강한&amp;nbsp;음의&amp;nbsp;상관관계가&amp;nbsp;나타났다.&amp;nbsp;이는&amp;nbsp;LOWESS&amp;nbsp;파생&amp;nbsp;피처를&amp;nbsp;사용할&amp;nbsp;경우&amp;nbsp;이상&amp;nbsp;점수가&amp;nbsp;물리적&amp;nbsp;열화&amp;nbsp;지표(capacity&amp;nbsp;fade)와&amp;nbsp;일치하는&amp;nbsp;패턴을&amp;nbsp;보이며,&amp;nbsp;열화&amp;nbsp;진행에&amp;nbsp;따른&amp;nbsp;이상&amp;nbsp;변화를&amp;nbsp;효과적으로&amp;nbsp;포착했음을&amp;nbsp;의미한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모델 활용 전략&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEvGRb/dJMcafFgKtX/tKTMckf3aQTiPwRYXckNPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEvGRb/dJMcafFgKtX/tKTMckf3aQTiPwRYXckNPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEvGRb/dJMcafFgKtX/tKTMckf3aQTiPwRYXckNPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEvGRb%2FdJMcafFgKtX%2FtKTMckf3aQTiPwRYXckNPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;577&quot; height=&quot;249&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;249&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 연구는 LOF와 Anomaly Transformer를 상호보완적으로 활용하여 이상 탐지의 신뢰성을 높였다. LOF는 window 단위 feature 기반 분석으로 &quot;이 구간에서 무슨 일이 일어나는가?&quot;라는 국소적 이상을 탐지하고, Anomaly Transformer는 시계열 전체 맥락에서 재구성 오류를 분석하여 &quot;전체적으로 의미가 있는 패턴인가?&quot;를 판단한다. 두 모델의 교차 검증을 통해 검증된 열화 신호만을 추출하여 false positive를 감소시켰다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MLOps 시스템&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 173500.png&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTYmqM/dJMcagjRPcA/eRYdAfLpb91CtkretMGYCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTYmqM/dJMcagjRPcA/eRYdAfLpb91CtkretMGYCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTYmqM/dJMcagjRPcA/eRYdAfLpb91CtkretMGYCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTYmqM%2FdJMcagjRPcA%2FeRYdAfLpb91CtkretMGYCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;534&quot; height=&quot;285&quot; data-filename=&quot;스크린샷 2026-01-03 173500.png&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;본 프로젝트는 Airflow 기반 MLOps 파이프라인으로 구성된다. &lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;데이터 처리 단계에서는 3개의 DAG를 통해 NASA 배터리 데이터를 수집하고(DAG 1), LOWESS 기반 피처 엔지니어링을 수행하며(DAG 2), LOF와 Anomaly Transformer 모델을 학습한다(DAG 3&amp;amp;4). &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;저장 계층은 Snowflake에서 원본 데이터, 전처리된 피처, 예측 결과를 관리하고, AWS S3에 모델 학습 이력을 저장하며, MLflow로 실험 추적 및 하이퍼파라미터를 기록한다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;최종적으로 Streamlit 대시보드를 통해 Overview, Anomaly Scores, Health Indicators, Correlation Analysis, Model Comparison 5개 탭으로 시각화하여 배터리 관리자와 ML 엔지니어 모두에게 인사이트를 제공한다. Streamlit 대시보드는 s3에서 데이터를 실시간으로 다운로드 받고, github과 코드가 연동되어 있어 수정 사항을 즉각적으로 반영할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Dag 1 &amp;amp; 2&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 173941.png&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wjlad/dJMcac9yX1v/8Im3mski6WO88LVqKxY13K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wjlad/dJMcac9yX1v/8Im3mski6WO88LVqKxY13K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wjlad/dJMcac9yX1v/8Im3mski6WO88LVqKxY13K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwjlad%2FdJMcac9yX1v%2F8Im3mski6WO88LVqKxY13K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;245&quot; data-filename=&quot;스크린샷 2026-01-03 173941.png&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;DAG 1은 데이터 수집 파이프라인으로, NASA 원본 CSV 파일을 추출(Extract)하고 데이터 검증 및 변환(Transform)을 거쳐 Snowflake Layer 1에 적재(Load)한다. &lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DAG 2는 피처 엔지니어링 파이프라인으로, Snowflake Layer 1에서 원본 데이터를 추출한 후 LOWESS 기반 파생 변수 생성 및 전처리(Transform)를 수행하고, 최종 결과를 Snowflake Layer 2에 저장한다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;두 DAG는 순차적으로 실행되며 Snowflake를 중간 저장소로 활용하여 데이터 계층을 분리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;※ Dag: &lt;span style=&quot;color: #000000;&quot;&gt;실행해야 할 작업들의 흐름과 의존성을 표현한 워크플로우 정의로 무엇을 어떤 순서로 실행할지 선언하는 &amp;lsquo;설계도&amp;rsquo;&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dag 3 &amp;amp; 4&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 174127.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CbtUv/dJMcai9McW7/YvVv6KYwT59D1ziFR1WTh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CbtUv/dJMcai9McW7/YvVv6KYwT59D1ziFR1WTh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CbtUv/dJMcai9McW7/YvVv6KYwT59D1ziFR1WTh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCbtUv%2FdJMcai9McW7%2FYvVv6KYwT59D1ziFR1WTh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;258&quot; data-filename=&quot;스크린샷 2026-01-03 174127.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; DAG 3&amp;amp;4는 모델&amp;nbsp; 학습 및 실험 관리 파이프라인으로, Snowflake Layer 2에서 전처리된 데이터를 추출(Extract)하고 Train/Test로 분할한다. &lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;학습 단계(Train Task)에서는 LOF와 Anomaly Transformer 모델을 학습하며, MLflow를 통해 하이퍼파라미터, 메트릭, 아티팩트를 실시간으로 추적한다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;학습 완료 후 예측 결과와 성능 지표를 Snowflake Layer 3에 저장(Load Task)하고, 학습된 모델은 AWS S3 object storage에 버전 관리하여 저장한다(Model Save). &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MLflow UI와 Optuna를 통해 실험 비교, 하이퍼파라미터 튜닝 이력, 성능 시각화를 제공하여 모델 개선 및 재현성을 보장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ MLFlow: &lt;span style=&quot;color: #000000;&quot;&gt;머신러닝 / 딥러닝 실험 진행을 기록하기 위한 실험 관리 플랫폼&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ Optuna: &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;파이썬 기반의 하이퍼파라미터 최적화 (hyperparameter optimization) 프레임워크&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대시보드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드는 ML 엔지니어와 현장 배터리 실험 엔지니어의 데이터 기반 의사결정 니즈를 충족시키는 것을 목표로 설계했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;859&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkih4J/dJMcah39cUy/YpkIh6imwWHkbMBr7Ms17K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkih4J/dJMcah39cUy/YpkIh6imwWHkbMBr7Ms17K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkih4J/dJMcah39cUy/YpkIh6imwWHkbMBr7Ms17K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkih4J%2FdJMcah39cUy%2FYpkIh6imwWHkbMBr7Ms17K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1917&quot; height=&quot;859&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;859&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 화면의 가장 상단에서는 모델과 데이터셋 별 주요 실험 결과를 확인할 수 있다. 또한 배터리 셀 별 실험 결과도 드롭박스로 변경해가며 실험 결과를 비교할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Overview Tab&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w54Zs/dJMcabbNkyj/G1cd6q2TywHBZZ3u0XuFk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w54Zs/dJMcabbNkyj/G1cd6q2TywHBZZ3u0XuFk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w54Zs/dJMcabbNkyj/G1cd6q2TywHBZZ3u0XuFk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw54Zs%2FdJMcabbNkyj%2FG1cd6q2TywHBZZ3u0XuFk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1599&quot; height=&quot;788&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오버뷰 탭에서는 사이클 변화에 따라 주요 데이터들의 용량, 온도, 전압, 전류 변화를 한 눈에 확인할 수 있다. 이를 통해 사용자는 배터리의 전반적인 건강 상태와 열화 진행 추이를 직관적으로 파악하고, 주요 변수 간 상호작용을 신속하게 모니터링할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Anomaly Scores&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 175215.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekZmt0/dJMcabv6gte/Mi39C9COkXKckmCXTTy8ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekZmt0/dJMcabv6gte/Mi39C9COkXKckmCXTTy8ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekZmt0/dJMcabv6gte/Mi39C9COkXKckmCXTTy8ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekZmt0%2FdJMcabv6gte%2FMi39C9COkXKckmCXTTy8ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1514&quot; height=&quot;534&quot; data-filename=&quot;스크린샷 2026-01-03 175215.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 175228.png&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R7qW0/dJMcabv6gtk/ckLHGDnqbzQsS4WlI2ZL6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R7qW0/dJMcabv6gtk/ckLHGDnqbzQsS4WlI2ZL6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R7qW0/dJMcabv6gtk/ckLHGDnqbzQsS4WlI2ZL6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR7qW0%2FdJMcabv6gtk%2FckLHGDnqbzQsS4WlI2ZL6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1495&quot; height=&quot;625&quot; data-filename=&quot;스크린샷 2026-01-03 175228.png&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anomaly Score Analysis 탭에서는 사이클별 이상 점수 추이를 시계열 그래프로 확인할 수 있으며, threshold(빨간 점선)를 초과하는 Top 5 이상 사이클이 빨간 점으로 강조 표시된다.&amp;nbsp;&lt;br /&gt;하단의 Top 5 Anomalous Cycles 섹션에서는 이상 점수가 높은 사이클을 랭킹 순으로 나열하고, 각 사이클의 Warning 수준과 정확한 이상 점수를 수평 바 차트로 시각화한다.&amp;nbsp;&lt;br /&gt;사용자는&amp;nbsp;각&amp;nbsp;이상&amp;nbsp;사이클&amp;nbsp;옆&amp;nbsp;체크박스를&amp;nbsp;통해&amp;nbsp;조치&amp;nbsp;완료&amp;nbsp;여부를&amp;nbsp;기록할&amp;nbsp;수&amp;nbsp;있어,&amp;nbsp;배터리&amp;nbsp;관리자가&amp;nbsp;실시간으로&amp;nbsp;이상&amp;nbsp;사이클에&amp;nbsp;대한&amp;nbsp;대응&amp;nbsp;현황을&amp;nbsp;추적하고&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Feature Importance&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 175533.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cusxEx/dJMb99LN3zm/0M5IiCYFrS2DDyVasbzKj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cusxEx/dJMb99LN3zm/0M5IiCYFrS2DDyVasbzKj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cusxEx/dJMb99LN3zm/0M5IiCYFrS2DDyVasbzKj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcusxEx%2FdJMb99LN3zm%2F0M5IiCYFrS2DDyVasbzKj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1596&quot; height=&quot;819&quot; data-filename=&quot;스크린샷 2026-01-03 175533.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;819&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 175551.png&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F7zxK/dJMcahC4vqz/L4BAVCRk7Ghr6sPsoFxnpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F7zxK/dJMcahC4vqz/L4BAVCRk7Ghr6sPsoFxnpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F7zxK/dJMcahC4vqz/L4BAVCRk7Ghr6sPsoFxnpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF7zxK%2FdJMcahC4vqz%2FL4BAVCRk7Ghr6sPsoFxnpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1599&quot; height=&quot;570&quot; data-filename=&quot;스크린샷 2026-01-03 175551.png&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 175601.png&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;683&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eSL8cj/dJMcahC4vqB/kl5fiQaFczIrZHFFumnwMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eSL8cj/dJMcahC4vqB/kl5fiQaFczIrZHFFumnwMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eSL8cj/dJMcahC4vqB/kl5fiQaFczIrZHFFumnwMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeSL8cj%2FdJMcahC4vqB%2Fkl5fiQaFczIrZHFFumnwMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1594&quot; height=&quot;683&quot; data-filename=&quot;스크린샷 2026-01-03 175601.png&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;683&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;Feature Importance 탭에서는 SHAP(SHapley Additive exPlanations)을 활용하여 LOF 모델의 예측에 각 feature가 미치는 영향을 정량적으로 분석한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;상단에는 가장 중요한 feature(Current_load_residual)와 그 중요도 점수(4.4045)가 표시되며, Feature Contribution 섹션에서는 전체 feature의 중요도를 수평 막대 차트로 시각화하여 상대적 기여도를 비교할 수 있다. 사용자는 Top N Features 드롭다운으로 표시할 feature 개수를 조정 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;SHAP Value Analysis에서는 각 feature의 값(Feature Value)에 따른 SHAP value를 산점도로 표현하여, feature 값의 변화가 이상 탐지에 미치는 영향의 방향성과 크기를 직관적으로 파악할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;주요 Feature 설명 섹션에서는 각 변수의 의미(_trend: LOWESS 추세, _smooth: 이동평균, _residual: 잔차)를 제공하여, 도메인 지식이 없는 사용자도 feature의 물리적 의미를 이해하고 모델 판단 근거를 해석할 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Health Indicator&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 175810.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjT8ko/dJMcaaKIUSW/0jhW5BhpN2Bf29dyWeiD6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjT8ko/dJMcaaKIUSW/0jhW5BhpN2Bf29dyWeiD6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjT8ko/dJMcaaKIUSW/0jhW5BhpN2Bf29dyWeiD6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjT8ko%2FdJMcaaKIUSW%2F0jhW5BhpN2Bf29dyWeiD6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1504&quot; height=&quot;190&quot; data-filename=&quot;스크린샷 2026-01-03 175810.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 175824.png&quot; data-origin-width=&quot;1591&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg8qJI/dJMcacaHDsK/ESEr4iurWav95d0MwQNuDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg8qJI/dJMcacaHDsK/ESEr4iurWav95d0MwQNuDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg8qJI/dJMcacaHDsK/ESEr4iurWav95d0MwQNuDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg8qJI%2FdJMcacaHDsK%2FESEr4iurWav95d0MwQNuDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1591&quot; height=&quot;774&quot; data-filename=&quot;스크린샷 2026-01-03 175824.png&quot; data-origin-width=&quot;1591&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;Health Indicator 탭에서는 배터리 건강 지표의 변동성을 통해 열화 진행 상태를 분석한다. &lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Statistical Summary에서는 안정 임계값(Stable Threshold), 초기 구간 변동성(Early Phase HI Volatility), 후기 구간 변동성(Late Phase HI Volatility)을 정량적으로 제시한다. 초기 대비 후기의 변동성이 감소하는 것은 열화가 진행되면서 배터리 거동이 상대적으로 안정화됨을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Variability Analysis에서는 세 가지 지표를 시각화한다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HI_ema(파란선): 지수이동평균 기반 건강 지표의 전체 추세&lt;/li&gt;
&lt;li&gt;HI Absolute Change(보라색): 사이클 간 절대 변화량으로 급격한 변동 구간 파악&lt;/li&gt;
&lt;li&gt;HI Slope Volatility(오렌지): 기울기 변동성으로 추세 변화의 불안정성 측정 Threshold(점선)를 초과하는 구간에서 이상 징후를 조기 경고하며, Test/Event 구분선을 통해 특정 시점의 배터리 상태 변화를 추적할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Correlation Tab&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-03 180116.png&quot; data-origin-width=&quot;1623&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RY631/dJMcad1HbIB/mi3RFkMj92WU5VlLMm3WB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RY631/dJMcad1HbIB/mi3RFkMj92WU5VlLMm3WB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RY631/dJMcad1HbIB/mi3RFkMj92WU5VlLMm3WB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRY631%2FdJMcad1HbIB%2Fmi3RFkMj92WU5VlLMm3WB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1623&quot; height=&quot;648&quot; data-filename=&quot;스크린샷 2026-01-03 180116.png&quot; data-origin-width=&quot;1623&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Correlation Analysis 탭에서는 이상 점수와 물리적 열화 지표 간의 상관관계를 통해 모델 검증을 수행한다.&lt;br /&gt;상단 산점도는 Anomaly Score와 Capacity(Pearson r=-0.947), R_ohmic(Pearson r=0.937)의 강한 상관관계를 보여준다. Capacity와의 강한 음의 상관관계는 용량 감소 시 이상 점수가 증가함을 의미하고, R_ohmic과의 강한 양의 상관관계는 저항 증가 시 이상 점수가 동반 상승함을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단&amp;nbsp;시계열&amp;nbsp;그래프는&amp;nbsp;사이클&amp;nbsp;진행에&amp;nbsp;따른&amp;nbsp;Anomaly&amp;nbsp;Score와&amp;nbsp;물리적&amp;nbsp;지표의&amp;nbsp;동시&amp;nbsp;변화를&amp;nbsp;시각화하여,&amp;nbsp;이상&amp;nbsp;탐지&amp;nbsp;결과가&amp;nbsp;실제&amp;nbsp;배터리&amp;nbsp;열화&amp;nbsp;패턴과&amp;nbsp;일치하는지&amp;nbsp;검증한다.&amp;nbsp;이는&amp;nbsp;Ground&amp;nbsp;Truth&amp;nbsp;Label이&amp;nbsp;없는&amp;nbsp;비지도&amp;nbsp;학습&amp;nbsp;환경에서&amp;nbsp;물리적&amp;nbsp;degradation&amp;nbsp;signal을&amp;nbsp;통한&amp;nbsp;교차&amp;nbsp;검증(Cross-modal&amp;nbsp;Validation)을&amp;nbsp;수행하는&amp;nbsp;것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀장으로서 1달 반의 기간 동안 프로젝트를 리드하며 상당히 많은 것을 배웠다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;팀장은 실무로 바쁘면 안된다.&lt;/b&gt; &lt;b&gt;일을 많이 하기 보다 프로젝트가 제대로 된 방향으로 흘러가는지, 팀원 간 의사소통에 오해는 없는지를 수시로 확인하는 것이 더 중요하다.&lt;/b&gt; 이번 프로젝트의 경우 내가 짊어진 부분이 많아 프로젝트 관리에 생각보다 많은 공수를 투자하기 현실적으로 어려웠다. 하지만 다음 프로젝트에서는 이런 점을 반영하여 좀 더 원활한 프로젝트 관리를 할 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;새로운 기술을 가장 빨리 배우는 법은 에러를 마주하는 것이다.&lt;/b&gt; 사전에 디버깅을 꼼꼼히 해서 에러가 발생하지 않는다면 더할 나위 없이 좋겠지만, 우선 실행하고 그 속에서 새빨간 에러를 해결하는 것이 프로젝트 일정도 준수하면서 기술 공부도 가능하게 한다. 완벽보다는 완성을 추구하자.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드는 리팩토링과 모듈화가 생명이다.&lt;/b&gt; 처음에는 귀찮고, 코드를 짜지도 않았는데 어떤 걸 리팩토링 할지 감도 안온다. 하지만 어느 정도 구상은 해야 한다. 일례로 streamlit 대시보드를 만들 때, 전체 코드를 하나의 파이썬 파일에 적었다. 이런 방식으로 하니 디버깅할 때 굉장히 머리가 아팠다. Streamlit의 경우 앞쪽의 tab이 뒤쪽의 tab에 영향을 미치기 때문에 디버깅하기 까다로웠다. 다시 처음으로 돌아가 tab들과 dataloader를 전부 쪼개기 시작했다. 그 이후로는 에러도 거의 발생하지 않을 뿐더러 에러 메세지를 통해 어느 tab에서 에러가 발생하는지 한 눈에 확인할 수 있었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하드코딩 하지 말자.&lt;/b&gt; 하드 코딩을 하면 처음에만 편하고 추후에 디버깅을 하거나 실험 조건들을 변경할 때 많이 귀찮아진다. 파일 경로, threshold 값, 하이퍼파라미터 등을 코드에 직접 작성하면 배터리 셀을 변경하거나 모델을 재학습할 때마다 여러 파일을 수정해야 한다. 대신 config 파일이나 환경변수로 분리하면 실험 조건 변경이 간편하고 재사용성과 유지보수성이 높아진다. 특히 MLOps 파이프라인에서는 DAG 파라미터화를 통해 동일한 코드로 다양한 실험을 자동화할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt; 갈등 관리 시 가장 중요한 점은 경청이다.&lt;/b&gt; 가장 어려운 건 소프트스킬이다. 이번 프로젝트에서는 기술적 구현 못지않게 팀원 간 갈등 관리에 집중했다. AI 도구와 튜터의 도움으로 기술적 허들을 넘는 법을 배웠으나, 동시에 가장 어려운 과제는 팀원 간의 의견 조율임을 체감했다. 소프트스킬은 온전히 인간의 몫이기에 팀원들 간 분쟁이나 언쟁이 생겼을 때 어떻게 조율해야 할지 많이 고민했다. 갈등 관리 시 가장 중요한 점은 경청이다. 상대의 불만을 정확히 파악하는 것이 프로젝트의 지연을 막고 최종 목표를 향해 원팀으로 나아가는 가장 빠른 길임을 배울 수 있었다. ML 엔지니어와 MLOps 직무는 협업이 필수적이기에 다양한 이해관계자와 협업이 필수적이다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>데이터 분석 부트캠프</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/79</guid>
      <comments>https://gguzunhagae.tistory.com/79#entry79comment</comments>
      <pubDate>Sat, 3 Jan 2026 16:35:31 +0900</pubDate>
    </item>
    <item>
      <title>Battery prognostics and health management from a machine learning perspective</title>
      <link>https://gguzunhagae.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;다변량&lt;span&gt; &lt;/span&gt;&lt;/span&gt;배터리 충방전 시계열 데이터 분석을 진행하기 앞서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;머신러닝 관점에서 배터리 PHM을 진행한 연구를 리뷰한 survey 논문&lt;/b&gt;&lt;/span&gt;을 정리해 보았습니다. survey 논문에서 프로젝트와 관련된 부분만 정리하였으니 이외의 내용은 논문을 참고하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHM이란 운용 유지 단계에서 센서를 이용하여 장비나 기계시스템의 상태를 모니터링하고, 고장 징후를 진단(diagnostic)과 잔여유효수명(RUL)을 예지(prognostic)하는 효과적인 건정성 기술을 의미합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Abstract&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전기 배터리는 여러 이점을 바탕으로 다양한 분야에서 활용되고 있다. 다양한 이점에도 불구하고, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;배터리의 수명을 단축시키는 열화 문제&lt;/b&gt;&lt;/span&gt;는 여전히 도전 과제로 남아 있다. 열화 문제를 해결하기 위해 전통적인 물리, 분자적 접근법을 활용하여 연구하고 있지만 이러한 모델들은 높은 컴퓨팅 비용과 불확실성으로 인해 내재된 문제점들을 충분히 포착하고 있지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 들어, 머신러닝 모델 기반의 접근이 주목 받고 있다. 이러한 접근법은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;복잡한 데이터 구조로 부터 패턴과 시공간적 특징 포착&lt;/b&gt;&lt;/span&gt;한다. 특히, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;물리적 과정과 딥러닝의 유연성을 결합한 하이브리드 모델링 전략이 큰 연구 성과를 거두었다.&lt;/b&gt;&lt;/span&gt; 따라서, 본 논문에서는 배터리 PHM을 DNN과 커널 기반 회귀 네트워크를 중점으로 설명한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Introduction&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배터리 열화로 인한 성능 저하는 운전 거리 감소, 잔여 용량 예측의 부정확성 등과 같이 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;배터리 시스템 전반에 걸쳐 부정적인 영향&lt;/b&gt;&lt;/span&gt;을 미친다. 열와 원리는 두 가지 주요 카테고리로 나눌 수 있다. 첫째, 물리적 원리(열 및 기계적 스트레스). 둘째, 화학적 원리(전기화학적 부작용).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열화 문제를 해결하기 위해 data-driven 방법을 사용할 수 있다. data-driven 방법은 여러 장점을 제공한다. 화학적 특정에 구애 받지 않고, 모델링할 수 있는 능력, 시스템 고유의 복잡성을 해결하는 능력, 그리고 관측된 데이터를 설명할 뿐만 아니라 복잡한 조건에서도 주석이 달리지 않은 샘플에 대해 예측을 수행할 수 있는 능력을 포함한다. 최근 AI의 한 분야인 머신러닝은 배터리 분석 분야에서 지속적인 혁신을 위한 새로운 지평을 열고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 연구에서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;LSTM, RNN과 같은 시계열 모델들이 State of Charge 리튬 이온 배터리 라이프 사이틀 예측에서 유의미한 성과&lt;/b&gt;&lt;/span&gt;를 얻고 있다. 강화된 feedforward-lstm이나 anti-noise adpative lstm과 같은 방법론들이 배터리 안정성과 RUL 예측 분야에서 후속 연구에 활용되고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLnuh4/dJMcabP8clY/iZ1I97aHQs4Zol8qDbc0tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLnuh4/dJMcabP8clY/iZ1I97aHQs4Zol8qDbc0tk/img.png&quot; data-alt=&quot;배터리 PHM&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLnuh4/dJMcabP8clY/iZ1I97aHQs4Zol8qDbc0tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLnuh4%2FdJMcabP8clY%2FiZ1I97aHQs4Zol8qDbc0tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;931&quot; height=&quot;487&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;배터리 PHM&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;머신러닝 기반의 배터리 PHM 시스템 연구&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;data-driven 연구에서 머신러닝 기반의 PHM 연구는 다중 스케일과 다중 물리 배터리 시스템의 본질적인 복잡성을 해결하고, 학계와 산업계 간의 기술 이전을 가속화하는 대안적인 방법으로 부상하고 있다. 이러한 머신러닝 기반 배터리 PHM은 광범위한 기술과 역량에 걸친 지속적인 협력을 필요로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기술들은 물리 시스템이 확장된 시공간적 규모 속에서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;탐지 또는 분류에 필요한 데이터 표현을 포착&lt;/b&gt;&lt;/span&gt;한다. 머신러닝 방법론들은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;불확실성을 표현하고 관리하는 강력한 프레임워크를 제공&lt;/b&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PCA와 클러스터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PCA의 목표는 중복 정보를 제공하고 계산 비용을 절감하여 배터리 시스템의 수명을 예측하는 것이다. 이러한 관점에서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;grey relational analysis는 건강 특성과 용량의 연관성을 분석하는 도구를 제공&lt;/b&gt;&lt;/span&gt;한다. 실제 응용 분야에서는 수백 또는 수천 개의 셀이 직렬/병렬 구조로 연결된다. 이 때, PCA와 같은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;비지도 학습이 일관성 평가 및 이상탐지를 위한 강력한 방법론&lt;/b&gt;&lt;/span&gt;으로 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;일반화 성능 증가&lt;/b&gt;를 위한 물리 정보 기반의 머신러닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 데이터 기반 모델은 관측치에 잘 부합할 수 있지만, 낮은 일반화 성능을 초래할 수 있다. 따라서 사전 정보나 강력한 이론적 제약을 제공할 수 잇는 물리 규칙을 머신러닝 모델에 부여하여 근본적인 물리 법칙과 도메인 지식을 결합하는 것이 중요하다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;물리 정보 기반 머신러닝은 결측치 또는 노이즈가 있는 데이터 환경에서도 강건&lt;/b&gt;&lt;/span&gt;하며, 일반화 작업에서도 정확하고 물리적으로 일관된 예측을 제공할 수 있는 보다 해석 가능한 머신러닝 방법론을 만드는데 기여한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 NASA&amp;nbsp; ARC 연구원들은 무인 항공기 배터리 모델링 및 예지(prognosis)를 위해 신경망 내부에 네른스트 및 버틀러-볼머 방정식을 구현하여 하이브리드 모델링 접근 방식[1]을 확립했다. 또한 NASA PRoE에서 제공하는 공개 실험 데이터를 사용해서 배터리 SOH 예지를 위한 다중 충실도 모델을 갖춘 물리 정보 기반 머신러닝[2]이 개발되었다. 또한, 나사 공개 실험 데이터로 RUL을 예측하기 위해 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;물리 정보 기반 LSTM과 물리 기반 캘린더 및 사이클 노화 모델을 결합하는 것을 제안&lt;/b&gt;&lt;/span&gt;했다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;물리 정보 기반 머신러닝의 최근 발전이 RUL 예측에 사용되는 초기 예측 모델의 성능을 크게 향상&lt;/b&gt;&lt;/span&gt;시키고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장기 시계열 예측을 위한 attention 기반 transformer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜스포머 모델의 핵심 구성 요소는 셀프 어텐션 매커니즘, positional encoding, 그리고 한 개 이상의 encoder/decoder 구조가 있다. 최근 여러 연구들은 자기지도학습 프레임워크를 사용하는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜스포머 모델이 동적인 충방전 주기 하에서도 배터리 상태를 정확하게 예측&lt;/b&gt;&lt;/span&gt;할 수 있음을 보여줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 survey 논문의 저자도 다중 물리 배터리 시스템에 필수적인 symplectic 구조를 보전하도록 설계된 듀얼 인코더 기반 아키텍처를 설계했고, 후속 연구에서는 적응형 슬라이딩 윈도우를 갖춘 특수 트랜스포머 모델을 통합했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;트랜스포머 모델은 데이터에 상당한 노이즈가 존재하더라도 배터리 RUL을 예측하도록 구축될 수 있다.&lt;/b&gt;&lt;/span&gt; denoising auto-encoder는 손실 함수 재구성을 통해 노이즈가 있는 입력으로부터 강건한 표현을 학습할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더욱 흥미로운 점은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;CNN과 트랜스포머를 결합함으로써, 정확도를 향상시켜 다중물리 배터리 애플리케이션에서 실시간으로 정확한 예측을 달성하는 것이 가능&lt;/b&gt;&lt;/span&gt;하다는 것이다. 특히, CNN은 local information을 추출하는 도구를 제공하고, self-attetion transformer는 global representation을 포착할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추후 연구 방향&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;강건함과 일반화 성능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배터리 건강 예측 작업에서 순수 데이터 기반의 머신러닝 모델은 사전 지식과 물리 규칙 부재로 인해 낮은 일반화 성능을 보일 수 있다. 모델에서 관측되지 않은 데이터에 대한 예측을 할 때, 불확실성이 근본적인 문제로 작용할 수 있다. 반면에 수학, 물리, 공학 과학에서 얻은 사전 지식으로 예측 정확도를 높일 수 있다. 따라서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;물리와 머신러닝 모델을 결합하면 강건성과 일반화 성능이 향상될 뿐만 아니라, 모델의 신뢰성과 일관성까지 고려&lt;/b&gt;&lt;/span&gt;할 수 있다. 더 나아가, 비지도 사전학습은 작은 데이터에 대한 과적합을 줄이는데 도움이 되고, 전이 상황에서 일반화 성능을 높이는데 도움이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해석 가능한 머신러닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재료 과학 응용 분야에서 배터리 연구를 위한 표준 성능 지표, 정형화된 예측 모델 및 설명 가능한 프레임워크의 부족은 딥러닝과 같은 머신러닝 기술을 셀 동작의 근간이 되는 재료, 특성 및 메커니즘과 연결하는 잠재력을 제한한다. 물리 지식을 머신러닝에 통합하는 것이 불확실성(노이즈 데이터)을 보다 효과적으로 관리할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화학습을 통한 지능형 배터리 건정성 관리, 물리 정보 기반 머신러닝, 어텐션 기반 트랜스포머, 전이 학습, 공개 데이터가 배터리 PHM 분야 발전에 핵심적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;물리 정보 기반 머신러닝 모델은 더욱 신뢰할 수 있는 모델을 만드는데 도움&lt;/b&gt;&lt;/span&gt;이 된다. 또한, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;어텐션 기반 트랜스포머 모델은 시계열 데이터에서 장기 의존성을 포착하는 데 탁월한 능력을 보여주기 때문에, 배터리 열화 과정을 정확하게 모델링하는데 매우 중요&lt;/b&gt;&lt;/span&gt;하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나의 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. RUL 예측에서는 feature extraction이 핵심이다. 용량 열화 분석과 높은 상관관계를 가진 변수를 추출한 뒤에 고차원 입력 값으로 사용해야 좋은 결과를 얻을 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 그렇다면 배터리 이상탐지에서도 도메인 지식으로 feature를 추출하는게 중요하지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- grey relational analysis같은 통계적 feature extraction으로 AutoEncoder 혹은 Transformer 같은 이상탐지 모델을 고도화할 수 있지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 물리 정보 기반 머신러닝이 강건함에서 강점을 보이니 통계적 방법으로 새로운 feature를 추가하는 것도 강건함 증가에 많은 도움이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 강건함을 높일 수 있는 도메인 지식 + 성능과 해석 가능성을 높일 수 있는 Transformer&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. GNN을 사용하는 것도 해석 가능성을 높일 수 있는 차별점이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[1] R.G. Nascimento, M. Corbetta, C.S. Kulkarni, F.A. Viana, Hybrid physics-informed neural networks for lithium-ion battery modeling and prognosis, J. Power Sources 513 (2021), 230526&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[2] S. Kohtz, Y. Xu, Z. Zheng, P. Wang, Physics-informed machine learning model for battery state of health prognostics using partial charging segments, Mech. Syst. Signal Process. 172 (2022), 109002.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>AI</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/78</guid>
      <comments>https://gguzunhagae.tistory.com/78#entry78comment</comments>
      <pubDate>Thu, 20 Nov 2025 12:56:48 +0900</pubDate>
    </item>
    <item>
      <title>데이터 사이언스 기술 면접 스터디 12회차</title>
      <link>https://gguzunhagae.tistory.com/77</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;50개의 작은 의사결정 나무는 큰 의사결정 나무보다 괜찮을까요? 왜 그렇게 생각하나요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100개의 의사결정 나무가 더 좋은 결과를 만들 확률이 높다. 개별 의사결정 나무는 분산이 높기 때문에 과적합 가능성이 높고, 고차원 학습 데이터를 깊이 있게 학습하지 못한다. 100개의 의사결정 나무가 모인다면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;분산을 줄이고, 과적합 가능성을 크게 낮춰 더 안정적인 예측이 가능&lt;/b&gt;&lt;/span&gt;하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앙상블 방법에는 어떤 것들이 있나요?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bagging (Bootstrap Aggregating)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p7AsH/dJMcajtKw8J/ag0uQaSN17xKpBcvMsPqNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p7AsH/dJMcajtKw8J/ag0uQaSN17xKpBcvMsPqNk/img.png&quot; data-alt=&quot;Bagging&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p7AsH/dJMcajtKw8J/ag0uQaSN17xKpBcvMsPqNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp7AsH%2FdJMcajtKw8J%2Fag0uQaSN17xKpBcvMsPqNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;624&quot; height=&quot;368&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Bagging&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 학습 데이터에서 여러 데이터셋을 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;복원(Bootstrap) 추출&lt;/b&gt;&lt;/span&gt;로 만든 후 각각의 데이터셋을 동일한 모델에게 학습시켜, 모델의 결과를 평균 내거나 다수의 결과로 최종 결과를 선정하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 데이터셋을 다양한 측면에서 학습할 수 있기 때문에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;분산을 줄여 과적합을 낮추는데 효과적&lt;/b&gt;&lt;/span&gt;인 방법이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대표 모델&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RandomForest&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Boosting&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcm7Fl/dJMcaiInntC/eeJBJkUki60rqAKNGnv1Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcm7Fl/dJMcaiInntC/eeJBJkUki60rqAKNGnv1Kk/img.png&quot; data-alt=&quot;Boosting&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcm7Fl/dJMcaiInntC/eeJBJkUki60rqAKNGnv1Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcm7Fl%2FdJMcaiInntC%2FeeJBJkUki60rqAKNGnv1Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;624&quot; height=&quot;338&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Boosting&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델을 순차적으로 학습시켜 최종적으로 강력한 예측 모델을 만드는 방법이다. 처음에는 모든 데이터에 동일한 가중치로 모델을 학습시키지만, 이전 모델이 틀린 데이터에 더 높은 가중치를 주는 방식으로 오차를 줄여나간다. 주로 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;편향을 감소시키고 예측 성능을 극대화&lt;/b&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대표 모델
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XGBoost&lt;/li&gt;
&lt;li&gt;GBM&lt;/li&gt;
&lt;li&gt;LGBM&lt;/li&gt;
&lt;li&gt;AdaBoost&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Stacking&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9N3j/dJMcaboWRX9/uLYiRTv986eUXTkc4qdKo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9N3j/dJMcaboWRX9/uLYiRTv986eUXTkc4qdKo1/img.png&quot; data-alt=&quot;Stacking&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9N3j/dJMcaboWRX9/uLYiRTv986eUXTkc4qdKo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9N3j%2FdJMcaboWRX9%2FuLYiRTv986eUXTkc4qdKo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;624&quot; height=&quot;293&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Stacking&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 개별 모델이 출력한 결과를 입력 값으로 새롭게 사용하여 모델의 성능을 높이는 방법이다. 개별 모델이 학습한 결과를 모은 모델을 Meta 모델이라 하고, 여기에 시험 데이터로 모델의 성능을 평가한다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이전 학습의 결과를 다시 입력값으로 사용하기 때문에 과적합에 유의&lt;/b&gt;&lt;/span&gt;해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Voting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linear Regression, Decision Tree 와 같이 여러 모델들의 결과 값을 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;평균이나 다수결로 최종 값을 예측&lt;/b&gt;&lt;/span&gt;하는 방법이다. 다른 방법들에 비해 비교적 단순하기 때문에 성능이 상대적으로 낮게 나올 확률이 높다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앙상블 모델의 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;높은 정확도&lt;/b&gt;: 여러 모델들을 결합해서 사용하기 때문에 하나의 모델을 사용하는 것보다 데이터의 패턴을 더 다양한 관점에서 학습할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;강건함&lt;/b&gt;: 여러 모델의 결과 값을 종합해서 최종 결과 값을 도출하기 때문에 하나의 모델을 사용하는 것보다 특정 데이터에 크게 의존하는 가능성을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과적합 방지&lt;/b&gt;: 높은 분산을 가진 의사결정 나무와 같은 모델의 분산을 평균 내거나 투표하는 방식으로 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 가능성&lt;/b&gt;: 앙상블 모델은 분류나 회귀처럼 여러 task에 활용되기 적합하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datasciencedojo.com/blog/ensemble-methods-in-machine-learning/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://datasciencedojo.com/blog/ensemble-methods-in-machine-learning/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;XGBoost 모델을 아시나요? 왜 캐글에서 유명할까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;XGBoost(eXtreme Gradient Boosting)는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;경사하강법을 활용하는 지도 학습 부스팅 알고리즘&lt;/b&gt;&lt;/span&gt;인 그레이디언트 부스트 Decision Trees를 사용하는 분산형 오픈 소스 머신 러닝 라이브러리이다. &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;속도, 효율성, 대규모 데이터 세트에 대한 확장성이 뛰어난&lt;/b&gt;&lt;/span&gt; 것으로 잘 알려져 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N6pkD/dJMb995KFD8/KR1QCz8FuL0C27VI3wvKjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N6pkD/dJMb995KFD8/KR1QCz8FuL0C27VI3wvKjK/img.png&quot; data-alt=&quot;XGBoost&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N6pkD/dJMb995KFD8/KR1QCz8FuL0C27VI3wvKjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN6pkD%2FdJMb995KFD8%2FKR1QCz8FuL0C27VI3wvKjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;326&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;XGBoost&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Gradient Boosting은 기본 의사결정 나무를 생성한다. 이후에 기존 모델의 실수를 바탕으로 residual을 줄이는 방향으로 새로운 트리를 생성해가며 모델의 성능을 점점 개선한다. 이때, 잔차를 집계하여 손실함수(경사하강법)를 사용하여 모델에 점수를 매긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;XGBoost의 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;병렬 및 분산 컴퓨팅
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;라이브러리는 블록이라는 인메모리 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;단위로 데이터를 저장&lt;/b&gt;&lt;/span&gt;한다. 개별 블록을 여러 컴퓨터에 분산하거나 아웃오브코어 컴퓨팅을 사용하여 외부 메모리에 저장할 수 있다. 또한 XGBoost는 계산 속도를 높이기 위해 여러 컴퓨터 클러스터에 분산된 학습과 같은 고급 사용 사례도 지원한다. Apache Spark, Dask 또는 Kubernetes와 같은 툴을 사용하여 분산 모드로 구현할 수도 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;캐시 인식 사전 페칭 알고리즘
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;XGBoost는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;대규모 데이터 세트의 런타임을 줄이는 데 도움&lt;/b&gt;&lt;/span&gt;이 되는 캐시 인식 사전 페칭 알고리즘을 사용한다. 라이브러리는 단일 시스템에서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;다른 기존 프레임워크보다 10배 이상 빠르게 실행&lt;/b&gt;&lt;/span&gt;할 수 있다. 인상적인 속도 덕분에 XGBoost는 더 적은 리소스를 사용하여 수십억 개의 예제를 처리할 수 있으므로 확장 가능한 트리 부스팅 시스템이 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정규화 기능 내장
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;일반 그레이디언트 부스팅과 달리 XGBoost는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;학습 목표의 일부로 정규화&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;를 포함&lt;/b&gt;&lt;/span&gt;한다. 하이퍼 파라미터 튜닝을 통해 데이터를 정규화할 수도 있다. XGBoost에 내장된 정규화를 사용하면 라이브러리가 일반 사이킷런(scikit-learn) 그레이디언트 부스팅 패키지보다 더 나은 결과를 제공할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #161616;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;또한, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;트리 기반 모델이기 때문에 정규화를 필수적으로 요구하지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동 결측치 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;XGBoost는 희소 데이터에 희소성 인식 알고리즘을 사용한다. 데이터 세트에 값이 누락된 경우 데이터 포인트는 기본 방향으로 분류되고 알고리즘은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;누락된 값을 처리하기 위한 최적의 방향을 학습&lt;/b&gt;&lt;/span&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐글에서 유명한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐글에서 유명한 이유는 위 XGBoost의 기능 때문이다. 대규모 데이터에 대한 빠른 학습, 분산형 컴퓨팅을 통한 안정적인 학습, 자동 전처리 등 다양한 기능을 편리하게 제공하기 때문에 프로그래밍에 미숙한 데이터 과학자도 쉽게 대규모 데이터를 분석할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/machine-learning/implementation-of-xgboost-extreme-gradient-boosting/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.geeksforgeeks.org/machine-learning/implementation-of-xgboost-extreme-gradient-boosting/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.nvidia.com/ko-kr/glossary/xgboost/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.nvidia.com/ko-kr/glossary/xgboost/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.ibm.com/kr-ko/think/topics/xgboost&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ibm.com/kr-ko/think/topics/xgboost&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>기술 면접 스터디</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/77</guid>
      <comments>https://gguzunhagae.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 31 Oct 2025 18:47:51 +0900</pubDate>
    </item>
    <item>
      <title>데이터 사이언스 기술 면접 스터디 11회차</title>
      <link>https://gguzunhagae.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;회귀/분류 시 알맞은 metric은 무엇일까요?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회귀&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAE(Mean Absolute Error)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예측값과 실제값의 차이를 절대값으로 변환하여 평균한 값이다. 오차의 크기를 직관적으로 파악하기 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M5cIF/dJMcaiVT1BP/auFGU8w4aN8yZwChR8Uy70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M5cIF/dJMcaiVT1BP/auFGU8w4aN8yZwChR8Uy70/img.png&quot; data-alt=&quot;MAE&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M5cIF/dJMcaiVT1BP/auFGU8w4aN8yZwChR8Uy70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM5cIF%2FdJMcaiVT1BP%2FauFGU8w4aN8yZwChR8Uy70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;311&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MAE&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MSE(Mean Squared Error)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예측값과 실제값 차이를 제곱하여 평균한 값이다. 오차에 제곱을 취하기 때문에, 큰 오차에 더 큰 패널티를 부여한다. 따라서, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이상치에 민감&lt;/b&gt;&lt;/span&gt;한 모습을 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLabmU/dJMcahJsxfD/xyZhGIKKeRTtzgn7B3MeGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLabmU/dJMcahJsxfD/xyZhGIKKeRTtzgn7B3MeGK/img.png&quot; data-alt=&quot;MSE&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLabmU/dJMcahJsxfD/xyZhGIKKeRTtzgn7B3MeGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLabmU%2FdJMcahJsxfD%2FxyZhGIKKeRTtzgn7B3MeGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;311&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MSE&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RMSE(Root Mean Squared Error)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MSE에 루트를 씌운 것으로 MSE와 마찬가지로 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이상치에 민감&lt;/b&gt;&lt;/span&gt;하지만 루트로 인해 값이 작아져 해석에 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;R-sqaure
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회귀 모델의 설명력을 나타내는 것으로 0 ~ 1 사이의 값을 가진다. 1에 가까울 수록 설명력이 높다고 해석한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분류 모델 평가 지표를 설명하기 위해서는 혼동행렬에 대한 개념이 필요하다. 분류 모델에서 가장 대표적인 지표는 정확도이지만 데이터 불균형이 심할 경우 모델의 성능을 충분히 설명하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start; color: #000000;&quot;&gt; 예를 들어, 1000개의 샘플 중 990개가 음성 클래스이고 10개가 양성 클래스일 때, 모델이 모든 샘플을 음성으로만 예측해도 99%의 정확도를 보이게 된다. 이 경우 정확도는 높지만, 실제 중요한 양성 클래스는 전혀 탐지하지 못한다. 혼동행렬은 이런 상황에서 모델이 어떤 클래스를 맞추고 어떤 클래스를 틀렸는지 명확하게 보여준다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eutco/dJMcajmYkWQ/qzarPRGKOq65WI3GYX8Wy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eutco/dJMcajmYkWQ/qzarPRGKOq65WI3GYX8Wy0/img.png&quot; data-alt=&quot;혼동행렬 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eutco/dJMcajmYkWQ/qzarPRGKOq65WI3GYX8Wy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEutco%2FdJMcajmYkWQ%2FqzarPRGKOq65WI3GYX8Wy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;566&quot; height=&quot;206&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;혼동행렬 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 개념: 혼동 행렬 (Confusion Matrix) &lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;True Positive (TP): 실제 Positive를 Positive로 예측 (정답)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;True Negative (TN): 실제 Negative를 Negative로 예측 (정답)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;False Positive (FP): 실제 Negative를 Positive로 잘못 예측 (오답) - 1종 오류&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;False Negative (FN): 실제 Positive를 Negative로 잘못 예측 (오답) - 2종 오류&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주요 분류 평가 지표&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;Accuracy (정확도)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;전체 예측 중 올바르게 예측한 비율: (TP + TN) / (TP + TN + FP + FN)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;클래스 불균형이 심할 때는 Precision만 확인하는 것이 치명적인 문제가 될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;Precision (정밀도)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Positive로 예측한 것들 중에서 실제로 Positive인 비율: TP / (TP + FP)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;FP를 줄이는게 중요할 때 사용. (예: 스팸 메일 분류)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;Recall (재현율)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;실제로 Positive 중에서 모델이 Positive로 예측한 비율: TP / (TP + FN)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;FN을 줄이는게 중요할 때 사용: (예: 암 진단)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;F1-Score
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;정밀도(Precision)와 재현율(Recall)의 조화 평균&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;정밀도와 재현율이 균형을 이룰 때 높은 값. 두 지표 모두 중요할 때 사용. (예: 클래스 불균형 모델 평가)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;ROC Curve &amp;amp; AUC
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;ROC Curve: 분류 모델의 임계값(Threshold)을 바꿔가며 TP 비율과 FP 비율의 변화를 시각화한 그래프&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;AUC: ROC 곡선 아래의 면적. 1에 가까울 수록 모델 성능이 높다고 평가. X 축인 FPR이 0에 가깝고, Y 축인 TPR이 1에 가까워야 성능이 1.0에 가까워짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPhj9/dJMcabWMfQH/Zvn5uX23Ed1gCO7xcE1t8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPhj9/dJMcabWMfQH/Zvn5uX23Ed1gCO7xcE1t8k/img.png&quot; data-alt=&quot;ROC AUC&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPhj9/dJMcabWMfQH/Zvn5uX23Ed1gCO7xcE1t8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPhj9%2FdJMcabWMfQH%2FZvn5uX23Ed1gCO7xcE1t8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;450&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ROC AUC&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/machine-learning/metrics-for-machine-learning-model/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.geeksforgeeks.org/machine-learning/metrics-for-machine-learning-model/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@zxxzx1515/AUROC-AUC-ROC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@zxxzx1515/AUROC-AUC-ROC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://neptune.ai/blog/performance-metrics-in-machine-learning-complete-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://neptune.ai/blog/performance-metrics-in-machine-learning-complete-guide&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>기술 면접 스터디</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/76</guid>
      <comments>https://gguzunhagae.tistory.com/76#entry76comment</comments>
      <pubDate>Wed, 29 Oct 2025 19:56:20 +0900</pubDate>
    </item>
    <item>
      <title>데이터 사이언스 기술 면접 스터디 10회차</title>
      <link>https://gguzunhagae.tistory.com/75</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;고유값(eigenvalue)과 고유벡터(eigenvector)에 대해 설명해주세요. 그리고 왜 중요할까요?&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정방행렬 A에 대해 Ax = $\lambda$x (상수 $\lambda$가 성립하는 0이 아닌 벡터 x가 존재할 때, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;상수 $\lambda$를 행렬 A의 고유값(eigenvalue), x를 이에 대응하는 고유벡터 (eigenvector)&lt;/b&gt;라고 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해서, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;고유벡터는 방향이 변하지 않는 선, 고유 값은 선이 줄어들고 늘어나는 비율이다.&lt;/b&gt;&lt;/span&gt; 보통의 벡터는 값에 변화를 주면 길이가 달라지게 되지만 고유벡터는 방향이 절대 변하지 않는다. 이처럼, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;선의 형태는 그대로 유지하면서 길이가 늘어나고 줄어드는 것을 선형 변환&lt;/b&gt;&lt;/span&gt;이라고 한다. 참고로 Eigen은 독일어로 '전형적인', '고유의' 라는 뜻을 가진 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고유값과 고유벡터는 선형대수의 근본적인 개념으로 PCA와 같은 차원축소 기법에서 활용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RhX4v/btsQXLLMeJp/Yt44uC4Q935G8yY1lQVDrK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RhX4v/btsQXLLMeJp/Yt44uC4Q935G8yY1lQVDrK/img.webp&quot; data-alt=&quot;Eigen value와 Eigen Vector&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RhX4v/btsQXLLMeJp/Yt44uC4Q935G8yY1lQVDrK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRhX4v%2FbtsQXLLMeJp%2FYt44uC4Q935G8yY1lQVDrK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;326&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Eigen value와 Eigen Vector&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$A\vec{x}$와 $\lambda\vec{x}$이 평행하기 때문에 $A\vec{x}$ &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;의 &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;실수 배로 표현&lt;/b&gt;&lt;/span&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTOaj5/btsQXzEP03v/52gryBgKqSa3S57HkIsIDk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTOaj5/btsQXzEP03v/52gryBgKqSa3S57HkIsIDk/img.jpg&quot; data-alt=&quot;정의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTOaj5/btsQXzEP03v/52gryBgKqSa3S57HkIsIDk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTOaj5%2FbtsQXzEP03v%2F52gryBgKqSa3S57HkIsIDk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;549&quot; height=&quot;405&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0 벡터는 고유벡터로 보지 않는다.&lt;/li&gt;
&lt;li&gt;무수히 많은 고유 벡터가 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duEkv2/btsQZqNGr74/uxOgZ1NaS18Nz4YvLbN5C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duEkv2/btsQZqNGr74/uxOgZ1NaS18Nz4YvLbN5C0/img.png&quot; data-alt=&quot;선형 변환&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duEkv2/btsQZqNGr74/uxOgZ1NaS18Nz4YvLbN5C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduEkv2%2FbtsQZqNGr74%2FuxOgZ1NaS18Nz4YvLbN5C0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1922&quot; height=&quot;376&quot; data-origin-width=&quot;1922&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;선형 변환&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계산 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고유값 ($\lambda$)를 찾는 방법: $det(A - \lambda I) = 0$&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A: 어떤 물체에 가해지는 '변화'를 나타내는 행렬&lt;/li&gt;
&lt;li&gt;I: 항등행렬. 숫자 1과 같은 행렬로 어떤 행렬에 곱해도 행렬 그 자체가 변하지 않는 행렬이다.&lt;/li&gt;
&lt;li&gt;det(): 행렬식으로 행렬이 변화시키는 만큼을 숫자로 나타낸 것. det()가 0이라면 원래의 변화 A에서 어떤 특별한 비율 $\lambda$의 변화를 뺐더니, 특정 방향으로의 모든 변화가 0이 되어 사라져 버리는 특별한 비율을 찾는 것이다.&lt;/li&gt;
&lt;li&gt;쉽게 말해서 A와 같은 $\lambda$를 찾는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고유벡터(v)를 찾는 방법: $(A - \lambda I)v$ = 0&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 고유값을 찾았다면 위 식에 고유값을 넣어준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;v: 우리가 찾고 싶은 고유 벡터이다.&lt;/li&gt;
&lt;li&gt;$(A - \lambda I)$ = 0: &quot;위에서 찾은 특별한 $\lambda$ 값을 이용하여$(A - \lambda I)$라는 변화를 주었을 때, 사라져서 0이 되어버리는 벡터 v가 무엇인가?&quot;를 묻는 식이다.&lt;/li&gt;
&lt;li&gt;즉, 고유값이 정해졌으니, 그 $\lambda$에 해당하는 '특별한 방향(v)'이 어떤 것인지 식을 풀어서 찾아내는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;고유값과 고유벡터는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;데이터의 주요 정보가 어느 방향으로 가장 크게 퍼져 있는지&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 알려주기 때문에 매우 중요하다. 이는 데이터를 효율적으로 간략화하는 &lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;차원 축소 과정에서 핵심적인 역할&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;을 하며, 특히 대표적인 차원 축소 알고리즘인&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;주성분 분석(PCA)의 기본 원리&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터의 주요 방향 찾기: 고유벡터는 데이터가 가장 크게 흩어져 있는 방향, 즉 데이터의 분산이 큰 방향을 알려준다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;중요한 정보만 남기기: 고유값이 클수록 그 방향(고유벡터)로 데이터가 많이 흩어져 있다는 뜻이고, 그 만큼 중요한 정보가 많다는 것을 의미한다. 따라서 우리는 고유값이 큰 일부 고유벡터만 선별해서 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;결과적으로, 여러 feature들을 결합해서 핵심 성분만 남기게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차원을 줄이면 좋은 점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모델이 더 빠르게 연산하고, 메모리 사용량을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;데이터의 핵심 표현을 효과적으로 학습할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=PFDu9oVAE-g&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=PFDu9oVAE-g&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://rfriend.tistory.com/181&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://rfriend.tistory.com/181&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/engineering-mathematics/eigen-values/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.geeksforgeeks.org/engineering-mathematics/eigen-values/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=xDARfmKauuA&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=xDARfmKauuA&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.samsungsds.com/kr/insights/mathematics_for_ml.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.samsungsds.com/kr/insights/mathematics_for_ml.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>기술 면접 스터디</category>
      <author>월요일zoa</author>
      <guid isPermaLink="true">https://gguzunhagae.tistory.com/75</guid>
      <comments>https://gguzunhagae.tistory.com/75#entry75comment</comments>
      <pubDate>Wed, 1 Oct 2025 16:19:14 +0900</pubDate>
    </item>
  </channel>
</rss>