<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>O_u.chan.log</title>
        <link>https://velog.io/</link>
        <description>열심히 하면 재밌다</description>
        <lastBuildDate>Thu, 21 May 2026 13:58:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>O_u.chan.log</title>
            <url>https://velog.velcdn.com/images/o_u--chan/profile/5a6b94d3-e0c7-4a44-a746-e415e9891b42/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. O_u.chan.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/o_u--chan" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[airflow - task 실행 흐름]]></title>
            <link>https://velog.io/@o_u--chan/airflow-task-%EC%8B%A4%ED%96%89-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@o_u--chan/airflow-task-%EC%8B%A4%ED%96%89-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Thu, 21 May 2026 13:58:04 GMT</pubDate>
            <description><![CDATA[<p>&quot;워커는 자리를 비우고 큐에 쌓여있는 다른 중요한 태스크를 가져와서 병렬로 일하기 시작합니다. 그동안 트리거러는 백그라운드에서 아주 가볍게(비동기로) 대기 상태를 모니터링하다가, 마침내 외부 응답이 딱 도착하면 스케줄러에게 알립니다. 스케줄러는 이 태스크를 다시 큐에 넣어 비어있는 워커가 마무리를 지을 수 있도록 배치하죠. 이 효율적인 대기 방식을 가능하게 하는 핵심 컴포넌트가 바로 트리거러입니다.&quot;</p>
<p>강의를 듣다 보니 다음과 같은 구절이 나왔다.
여기서 triggerer가 scheduler한테 알려서 scheduler를 깨우면 scheduler는 task를 어떻게 할당할까? 이미 한 번 executor를 통해 전략이 내려진 task니까 그대로 수행하는 게 더 효율적이지 않을까라는 생각이 들었다.</p>
<p>결론부터 말하면, scheduler는 다시 executor를 통해 task를 queue에 넣거나 worker에 보낸다. </p>
<p><strong>다시 할당하는 이유</strong></p>
<ul>
<li>Airflow의 단위는 Task 코드보다 <strong>TaskInstance(특정 run의 상태)</strong> 라서, Deferred -&gt; 재실행 시점은 사실상 새 실행으로 취급된다.</li>
<li>그 사이에 worker 수, queue state, executor의 parallelism, 우선순위, 다른 task의 대기 상태가 전부 바뀌었을 수 있기 때문에, task가 오면 가장 효율적인 worker를 다시 선택하는 것이 유연한 대처 방법이다.</li>
<li>CeleryExecutor, KubernetesExecutor와 같은 분산/동적 스케일링이 있는 환경에서는 예전에 돌린 work를 고정시키는 게 비효율적이거나 불가한 경우가 많다.</li>
</ul>
<blockquote>
<p>Deferrable Task 
기다리는 동안 잠깐 잠들었다가, 조건이 되면 다시 깨어나는 Task</p>
</blockquote>
<ul>
<li><code>defer()</code>라는 함수를 호출해서 알린다.</li>
<li>기다리는 동안 worker 대신 가벼운 triggerer 프로세스가 외부 조건을 계속 감시한다.</li>
</ul>
<blockquote>
<p>polling : 새로운 일이 생겼는지 계속 물어보는 방식</p>
</blockquote>
<ul>
<li>파일이 생겼는지</li>
<li>API 작업 끝났는지</li>
<li>DB가 특정 상태가 되었는지
polling은 API 호출이 아니다. polling은 주기적으로 반복해서 호출하는 패턴이고, 그 안에서 API를 사용할 뿐이다. </li>
</ul>
<blockquote>
<p>webhook : 서버가 알아서 먼저 연락해주는 API</p>
</blockquote>
<ul>
<li>어떤 시스템에서 특정 이벤트가 발생했을 때, 미리 정해둔 URL로 자동으로 HTTP 요청을 보내는 방식이다.</li>
<li>event 기반 push 알림</li>
<li>알림을 받는 쪽 URL : [Webhook URL / Callback URL / Endpoint] 라고 부른다.</li>
</ul>
<p>polling은 client가 변화가 있는지 계속 물어보는 방식이라 요청이 자주 발생하고 자원을 많이 쓴다. webhook은 서버가 이벤트가 생긴 순간, 1번 알려주는 방식이라 불필요한 요청이 줄어든다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[python map unpacking]]></title>
            <link>https://velog.io/@o_u--chan/python-map-unpacking</link>
            <guid>https://velog.io/@o_u--chan/python-map-unpacking</guid>
            <pubDate>Tue, 19 May 2026 10:49:18 GMT</pubDate>
            <description><![CDATA[<p>알고리즘 문제를 풀다가</p>
<pre><code class="language-python">arr = list(map(int, input().split()))</code></pre>
<p>라는 코드가 있었다. 정말 자연스럽게 주어진 코드였는데 문득,</p>
<pre><code class="language-python">arr = [map(int, input().split())]</code></pre>
<p>로 코드를 작성하는 게 더 간이할 것 같은데? 라는 생각이 들었다.</p>
<pre><code class="language-python">a, b = map(int, input().split())</code></pre>
<p>정말 많이 쓰는 코드니까 당연히 되겠지 싶었는데 <code>[]</code>는 안된다.</p>
<p><code>map()</code>은 주어진 코드에 따라<code>int</code> 형변환을 하려고 대기 중인 map 객체이다. 여기서 포인트는 <strong>대기 중인 map 객체</strong>라는 것이다.</p>
<pre><code class="language-python">a, b = map(int, input().split())</code></pre>
<p>을 보면 대기 중인 map 객체를 a, b로 unpacking을 하면서 값을 할당한 걸 볼 수 있는데
내가 작성한 리스트 리터럴 형식의 문법은 안에서 대기 중인 값들을 unpacking할 방법이 없다. 따라서 arr에 리스트로 씌워진 map 객체 하나만 달랑 있는 것이다.</p>
<p>그럼 여기서 생각해볼만한 부분이 그럼 <code>*</code>로 unpacking 하면 되겠네?</p>
<pre><code class="language-python">arr = [map(int, input().split())]
print(&#39;[map(int,input().split())]&#39;, type(arr))
print(type(arr[0]))

arr_with_unpacking =[*map(int, input().split())]
print(&#39;*map(int, input().split())&#39;, arr_with_unpacking)
print(type(arr_with_unpacking[0]))</code></pre>
<p><img src="https://velog.velcdn.com/images/o_u--chan/post/6de91176-6b1e-47bd-a9bf-51d0666b38e4/image.png" alt=""></p>
<p>잘 되는 걸 볼 수 있다. 문득 생각이 나서 해봤는데, 좋은 접근법이었다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.24(Fri)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.24Fri</link>
            <guid>https://velog.io/@o_u--chan/2026.04.24Fri</guid>
            <pubDate>Fri, 24 Apr 2026 16:20:48 GMT</pubDate>
            <description><![CDATA[<h3 id="mllib-pyspark"><strong>MLlib</strong> #pyspark</h3>
<hr>
<p><strong>Rating Class</strong></p>
<hr>
<p>PySpark(MLlib)에서 추천 시스템 모델(ALS) 만들 때 아주 표준적으로 사용하는 데이터 틀.</p>
<ul>
<li>User</li>
<li>Product</li>
<li>Rating</li>
</ul>
<pre><code class="language-python">from pyspark.mllib.recommendation import Rating

# Rating(user, product, rating)
my_rating = Rating(1, 101, 5.0)</code></pre>
<blockquote>
<p>Rating 객체는 내부적으로 <code>namedtuple</code>과 비슷하게 동작한다. <code>my_rating.user</code>처럼 속성값에 쉽게 접근할 수 있다. </p>
</blockquote>
<blockquote>
<p><strong><code>namedtuple</code>이란?</strong>
Python의 <code>collections</code> 모듈에 있는 가벼운 튜플 기반 자료형
값은 바꿀 필요 없지만, 각 칸에 이름을 붙여 읽기 쉽게 쓰고 싶을 때 사용한다. 불변한 값이고 리스트처럼 수정못 하지만, <code>field name</code>으로 접근할 수 있어 가독성이 좋다.</p>
</blockquote>
<p><strong>Rating class → Transformation</strong></p>
<hr>
<p>Transformation Process</p>
<ol>
<li>Parsing : <code>map</code> → 문자열 줄을 <code>Rating</code> 객체로 변환</li>
<li>Filtering : <code>filter</code> → 특정 조건만 남도록 필터링</li>
<li>Extraction : <code>map</code> → 특정 정보만 추출</li>
</ol>
<pre><code class="language-python">raw_data = sc.parallelize([&quot;1, 101, 5,0&quot;, &quot;1, 102, 3.0&quot;, &quot;2, 101, 4.0&quot;])

ratings_rdd = raw_data.map(lambda line: line.split(&quot;,&quot;)) \
                      .map(lambda x: Rating(int(x[0]), int(x[1]), float(x[2])))

high_ratings = ratings_rdd.filter(lambda r: r.rating &gt;= 4.0)</code></pre>
<blockquote>
<p>RDD를 다룰 때는 <strong>Lazy Evaluation(지연 연산)</strong> 특징을 기억해야 한다. <code>map</code>, <code>filter</code>를 쓴다고 바로 계산이 일어나지 않고 나중에 <code>collect</code>, <code>count</code>와 같은 Action 명령할 때까지 계획만 세워둔다.</p>
</blockquote>
<blockquote>
<p><strong>Lazy Evaluation으로 얻을 수 있는 이점</strong></p>
<ol>
<li><p>Query Optimization
Spark는 명령어를 받으면 바로 실행하지 않고 DAG라는 설계도를 그린다. 
ex : 100만 개의 데이터 중에서 1번 유저만 필터링한 후에 5개만 가져와라. 
  → Spark는 100만 개를 다 뒤지는 개 아니라 5개를 찾는 즉시 작업을 멈추는 최적의 경로를 찾아낸다.</p>
<ol start="2">
<li><p>메모리 효율성
데이터 즉시 변형하지 않기에, 불필요한 중간 데이터 결과를 메모리에 일일이 저장하지 않는다.</p>
</li>
<li><p><strong>장애 복구 (Fault Tolerance)</strong>
계산 도중에 서버 한 대가 고장 나도, spark 그동안 그려온 설계도(Lineage)가 있기 때문에, 고장 난 부분만 다시 계산해서 복구할 수 있다.</p>
</li>
</ol>
</li>
</ol>
</blockquote>
<p><strong>ALS(Alternating Least Squares, 교차 최소 제곱법)</strong></p>
<hr>
<p>추천 시스템에서 가장 유명한 행렬 분해 알고리즘</p>
<p>데이터 : 사용자와 아이템으로 이루어진 거대한 행렬
→ 사용자의 취향 행렬(U)과 아이템의 특성 행렬(I)로 행렬을 분해한다.
이 두 값을 곱하면 예측 평점이 나온다. </p>
<p>Alternating?
두 행렬을 동시에 맞추는 것는 난이도가 높다.</p>
<ol>
<li>사용자 취향을 고정하고, 거기에 맞는 아이템 특성을 계산한다.</li>
<li>아이템 특성을 고정하고, 거기에 맞는 사용자 취향을 다시 계산한다.</li>
<li>만족스러운 과정이 나올 때까지 이 두 과정을 <strong>번갈아가면서(Alternating)</strong> 반복한다. </li>
</ol>
<ul>
<li>확장성(Sacalability) : 데이터가 많아도 사용자/아이템별로 계산을 쪼갤 수 있어 Spark 같은 분산 처리 시스템에 적합하다.</li>
<li>Cold start 완호 : 사용자의 평점이 몇 개 없어도 비슷한 취향의 데이터를 통해 예측이 가능하다.</li>
</ul>
<p><strong>ALS의 핵심 파라미터</strong></p>
<ol>
<li>Rank(계수)<ul>
<li>사용자나 아이템의 특징을 몇 개의 숫자로 표현할 것인가?</li>
<li>10~200 사이에서 결정</li>
</ul>
</li>
<li>Iterations(반복 횟수)<ul>
<li>사용자 행렬 ←→ 아이템 행렬을 몇 번 번갈아가며 업데이트할 것인가?</li>
<li>10~20 </li>
</ul>
</li>
<li>Lambda(정규화 매개변수)<ul>
<li>모델이 너무 복잡해지지 않도록 벌금을 주는 수치</li>
<li>0.01, 0.1, 1.0 같은 값들로 테스트한다. </li>
</ul>
</li>
</ol>
<p>이 수치들을 제각기 조절해가는 것보다는 <strong>Grid Search</strong>라는 방법을 사용한다.
각 파라미터에 리스트를 설정해두면 모든 조합을 컴퓨터가 테스트한다. </p>
<p><strong>Vector &amp; LabeledPoint</strong></p>
<hr>
<p><del>={cyan}<strong>Vector(데이터의 특징을 숫자로)</strong>=</del>
분류 모델이 이해할 수 있도록 데이터를 숫자로 바꾼 것이 <strong>벡터</strong>이다.
Spark에서는 두 가지 형태의 벡터를 지원한다.</p>
<ul>
<li><strong>Dense Vector (밀집 벡터)</strong> : 모든 데이터를 다 적는 방식<ul>
<li><code>[1.0, 0.0, 3.5]</code> (모든 위치의 값을 다 기록)</li>
</ul>
</li>
<li><strong>Sparse Vector (최소 벡터)</strong> : 0이 아주 많을 때, 0이 아닌 값이 어디에 있는지만 적어서 메모리를 아끼는 방식<ul>
<li>100개 데이터 중 5번 위치에 1.0, 10번 위치에 3.5가 있고 나머지는 다 0</li>
</ul>
</li>
</ul>
<p><del>={blue}<strong>LabeledPoint(정답 + data)</strong>=</del>
벡터에 &#39;이건 스팸이야(1), 이건 정상이야(0)&#39;라는 정답(Label)을 붙여아 모델이 학습을 할 수 있다. 그 정답지 역할을 하는 게 <code>LabeledPoint</code>이다.</p>
<ul>
<li>구조 : <code>LabeledPoint(label, features)</code><ul>
<li><strong>label</strong> : 우리가 맞히고 싶은 정답 ( 보통 Double 타입)</li>
<li><strong>features</strong> : Vector (데이터의 특징)</li>
</ul>
</li>
</ul>
<pre><code class="language-python">from pyspark.mllib.regression import LabeledPoint
from pyspark.mllib.linalg import Vectors

lp = LabeledPoint(1.0, Vectors.dense([0.1, 0.5, 0.2]))

print(lp.label)     # 1.0
print(lp.features)  # [0.1, 0.5, 0.2]</code></pre>
<p>MLlib의 많은 알고리즘은 입력값으로 반드시 <code>LabeledPoint</code> 형태의 RDD를 요구한다.</p>
<p><strong>Map vs flatMap</strong></p>
<hr>
<p><code>map</code>을 사용하는 상황</p>
<ul>
<li>데이터의 형식만 바꾸고 싶을 때 사용한다.</li>
<li>전체 데이터의 개수(Line 수)가 변하지 않아야 할 때 적합하다.</li>
</ul>
<p><code>flatMap</code>을 사용하는 상황</p>
<ul>
<li>데이터를 잘게 쪼개거나 펼치고 싶을 때 사용한다. </li>
<li><strong>하나의 행을 여러 개의 데이터로 분리</strong>하고 싶을 때 적합하다.</li>
<li>문장을 단어 단위로 쪼갤 때, 리스트 안에 리스트가 들어있는 구조를 하나로 합치고 싶을 때</li>
</ul>
<blockquote>
<p><code>flatMap</code>은 <code>map</code> + <code>flatten</code>의 합성어다. 
<code>map</code>으로 먼저 데이터를 쪼갠 뒤(<strong>List의 형태로</strong>), 그 리스트의 껍데기를 까서 내용물만 밖으로 꺼내는(<strong>flatten</strong>) 작업을 동시에 해주는 것이다. </p>
</blockquote>
<p><strong>Clustering</strong></p>
<hr>
<p>라벨 없이 유사도가 높은 그룹들을 조직하는 비지도 학습 과제</p>
<ul>
<li>K-means</li>
<li>가우시안 혼합</li>
<li>전력 반복 클러스터링(PIC)</li>
<li>이분 K-means</li>
<li>steraming K-means</li>
</ul>
<p>K-means clustering</p>
<ul>
<li>수치형 특성들로 이루어진 데이터</li>
<li>목표 클러스터링 수 : &#39;K&#39;</li>
</ul>
<p><strong>Project</strong></p>
<hr>
<table>
<thead>
<tr>
<th>column</th>
<th>data type</th>
<th>description</th>
<th>cleaning requirements</th>
</tr>
</thead>
<tbody><tr>
<td><code>order_date</code></td>
<td><code>timestamp</code></td>
<td>Date and time when the order was made</td>
<td><em>Modify: Remove orders placed between 12am and 5am (inclusive); convert from timestamp to date</em></td>
</tr>
<tr>
<td><code>time_of_day</code></td>
<td><code>string</code></td>
<td>Period of the day when the order was made</td>
<td><em>New column containing (lower bound inclusive, upper bound exclusive): &quot;morning&quot; for orders placed 5-12am, &quot;afternoon&quot; for orders placed 12-6pm, and &quot;evening&quot; for 6-12pm</em></td>
</tr>
<tr>
<td><code>product</code></td>
<td><code>string</code></td>
<td>Name of a product ordered</td>
<td><em>Remove rows containing &quot;TV&quot; as the company has stopped selling this product; ensure all values are lowercase</em></td>
</tr>
<tr>
<td><code>category</code></td>
<td><code>string</code></td>
<td>Broader category of a product</td>
<td><em>Ensure all values are lowercase</em></td>
</tr>
<tr>
<td><code>purchase_state</code></td>
<td><code>string</code></td>
<td>US State of the purchase address</td>
<td><em>New column containing: the State that the purchase was ordered from</em></td>
</tr>
<tr>
<td><code>order_date</code> (5)</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 12am ~ 5am 제거 (1)</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- timestamp → date</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p><code>time_of_day</code> (4)</p>
<ul>
<li>새 컬럼 생성<ul>
<li>5-12 am → morning</li>
<li>12-6pm → afternoon</li>
<li>6-12pm → evening</li>
<li>lower bound 포함, upper bound 미포함</li>
</ul>
</li>
</ul>
<p><code>product</code></p>
<ul>
<li>&#39;TV&#39;를 포함하는 행 제거 (1)</li>
<li>모든 values → lowercase (2)</li>
</ul>
<p><code>category</code> </p>
<ul>
<li>values → lowercase (2)</li>
</ul>
<p><code>purchase_state</code> (3)</p>
<ul>
<li>컬럼 추가</li>
<li><code>ordered_from</code>에서 State 뽑아서 만들기 </li>
</ul>
<h4 id="data-processing-in-shell-bash">Data Processing in Shell #bash</h4>
<hr>
<p><strong>curl</strong>(Client for URLs)</p>
<hr>
<p>서버와 데이터를 주고받기 위한 Unix command line 도구
주로 HTTP 사이트나 FTP 서버에서 데이터를 다운로드할 때 사용한다.</p>
<p>기본 구조 : <code>curl [option flag] [URL]</code></p>
<ul>
<li>URL 입력은 필수</li>
</ul>
<p>단일 파일 다운로드</p>
<ul>
<li>원래 파일 이름으로 저장 : <code>-O</code> → <code>curl -O [file URL]</code></li>
<li>다른 이름으로 저장 : <code>-o</code> → <code>curl -o [file URL]</code></li>
</ul>
<p>서버에 비슷한 이름의 파일 다중 다운로드
<code>curl -O https://website.com/datafile*.txt</code></p>
<p>Globbing Parser 활용</p>
<ul>
<li>연속 다운로드 → <code>[ ]</code> 사용<ul>
<li><code>curl -O https://~~~/datafilename[001-100].txt</code></li>
</ul>
</li>
<li>간격 두고 다운로드 : 10번째 파일마다 다운로드 하고 싶다면 콜론<code>:</code> 추가<ul>
<li><code>curl -O https://~~~/datafilename[001-100:10].txt</code></li>
</ul>
</li>
</ul>
<p>선제적 트러블슈팅</p>
<ul>
<li><code>-L</code> : 300번대 에러 코드(리다이렉트) 발생 시 자동으로 해당 URL 따라간다.</li>
<li><code>-C</code> : 다운로드 도중 타임아웃 발생시, 중단된 지점부터 이어서 받는다.</li>
</ul>
<p><strong>Wget</strong>(World Wide Web &amp; Get)</p>
<hr>
<p><code>curl</code>과 마찬가지로 HTTP 및 FTP를 통해 파일을 다운로드할 수 있는 도구</p>
<p><code>curl</code>보다 더 다목적이다.</p>
<ul>
<li>단일 폴더, 폴더 전체, 웹페이지 자체를 다운로드 가능</li>
<li>여러 파일을 <strong>재귀적</strong>으로 다운로드 가능</li>
</ul>
<p>option</p>
<ul>
<li><code>-b</code> : background에서 실행</li>
<li><code>-q</code> : Wget의 실행 로그 출력을 끈다</li>
<li><code>-c</code> : 이전에 중단된 다운로드를 이어서 받는다.(Wget이 아닌 다른 프로그램으로 받던 파일도 가능)</li>
</ul>
<p>preview the log file → <code>cat wget-log</code></p>
<p><strong>Wget으로 여러 파일 다운로드</strong>
다운로드하려는 모든 URL이 <code>url_list.txt</code>에 있을 때, <code>-i</code> → Wget에게 로컬 파일에서 URL 읽어오도록 지시
<code>wget -i url_list.txt</code></p>
<ul>
<li><code>-i url_list.txt</code> 사이에 어떤 옵션도 들어가서는 안된다. <code>-i</code> 앞에 옵션 위치시킬 것</li>
</ul>
<p><strong>대용량 파일을 위한 다운로드 제한 설정</strong>
파일 다운로드가 네트워드 대역폭 전체를 점유하여 다른 작업을 방해하지 않도록 제한을 걸어야 할 때가 있다.
<strong><code>--limit-rate</code> option</strong></p>
<ul>
<li>숫자 입력 시 기본적으로 초당 바이트로 계산된다.</li>
<li><code>wget --limit-rate=200k -i url_list.txt</code><ul>
<li>초당 다운로드 속도가 200KB를 넘지 않도록 제한한다.</li>
</ul>
</li>
</ul>
<p><strong>소용량 파일을 위한 다운로드 제한 설정</strong>
작은 파일들을 여러 개 받을 때는 대역폭 제한보다 서버에 과부하를 주지 않는 것이 중요하다.
이때는 파일 다운로드 사이에 강제적인 대기 시간을 두는 <code>--wait</code> 옵션을 사용</p>
<ul>
<li>시간 단위는 &#39;초&#39;</li>
<li><code>wget --wait=2.5 -i url_list.txt</code> : 파일 다운로드 시마다 2.5초의 휴식시간</li>
</ul>
<p><strong>csvkit</strong></p>
<hr>
<p>Bash 명령어에 부족한 데이터 핸들링 기능을 보완하기 위해 python 라이브러리에 의존한다. csvkit은 이러한 간극을 메워주기 위한 파이썬을 기반으로 개발한, 데이터 변환, 처리, 정제 기능을 모아놓은 suite이다.</p>
<p><strong><code>in2csv</code></strong> : 파일을 CSV로 변환</p>
<ul>
<li><code>in2csv SPotifyData.xlsx &gt; Spotify.csv</code></li>
<li><code>&gt;</code> (리다이렉트 연산자)를 사용하지 않고 xlsx 파일만 입려하면 데이터가 터미널에 출력만 될 뿐, 파일로 저장되지 않는다.</li>
</ul>
<p>엑셀 파일에 여러 시트 있을 때, 특정 시트 변환하기</p>
<ul>
<li><code>in2csv -n Spotify.xlsx</code> (또는 <code>--names</code>) : 모든 시트 목록 출력</li>
<li>특정 시트 변환 <code>in2csv --sheet &quot;work-one_popularity&quot; Spotify.xlsx &gt; Spotify_population.csv</code></li>
<li>시트 이름에 공백이나 특수문자 있다면 따옴표(<code>&quot;&quot;</code>)로 감싸줘야 한다.</li>
</ul>
<p><code>in2csv</code>는 실행 시 별도의 로그를 남기지 않는다. <code>ls</code>를 통해 파일 생성을 확인해라.</p>
<blockquote>
<p><strong><code>csvlook</code></strong> : 데이터 미리보기
<code>cat</code>, <code>less</code>를 쓰면 터미널에서 데이터 형식이 깨져 보이기 쉽다. <strong>csvlook</strong>을 사용하면 마크다운 호환 방식의 고정폭 테이블로 예쁘게 정렬하여 보여준다. 
    <code>csvlook Sptify_population.csv</code></p>
</blockquote>
<blockquote>
<p><strong><code>csvstat</code></strong> : 기술 통계 확인
<code>describe()</code>와 유사한 기능을 한다. 평균, 중앙값, 고유값 개수 등 주요 통계 수치를 요약해서 보여준다.
<code>csvstat Spotify_population.csv</code></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.23(Thu)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.23Thu</link>
            <guid>https://velog.io/@o_u--chan/2026.04.23Thu</guid>
            <pubDate>Fri, 24 Apr 2026 16:20:02 GMT</pubDate>
            <description><![CDATA[<h4 id="pyspark-python"><strong>PySpark</strong> #python</h4>
<hr>
<p><strong>RDD(Resilient Distributed Dataset)</strong> </p>
<hr>
<p>회복 탄력성이 있는 분산 데이터셋</p>
<blockquote>
<p>요즘은 잘 사용하지 않고 DataFrame을 사용하지만, 그 근간이 되는 low-level API이다.</p>
</blockquote>
<p><strong>주요 특징</strong></p>
<ul>
<li>Resilient(회복력) : 데이터 손실이 일어나도, RDD는 데이터 생성 과정을 기록한 <strong>Lineage</strong>를 가지고 있어 자동으로 데이터를 복구한다.</li>
<li>Distributed(분산)</li>
<li>Dataset(데이터셋) </li>
</ul>
<p><strong>RDD의 주요 동작 방식</strong></p>
<hr>
<ol>
<li>Transformation
 기존 RDD에서 새로운 RDD를 만드는 과정
 Lazy Evaluation(지연 연산) → 변환 명령을 내려도 즉시 실행되지 않고 기록만 해둔다.<ul>
<li><code>map()</code>, <code>filter()</code>, <code>flatMap()</code>, <code>distinct()</code></li>
<li><code>flatmap()</code> : 입력 문자열을 단어로 나누는 함수</li>
</ul>
</li>
<li>Action
 실제로 계산을 수행하거나 결과 반환, 저장하는 과정
 Action이 호출되는 순간, 쌓여있던 Transformation들이 최적화되어 한꺼번에 실행된다.<ul>
<li><code>collect()</code>, <code>count()</code>, <code>take()</code>, <code>saveAsTextFile()</code>, <code>first()</code></li>
<li><code>collect()</code> : 모든 요소를 배열로 반환<pre><code>  cluster 곳곳에 흩어져 있는 데이터를 Driver 프로그램의 메모리로 수집하여 파이썬의 List 형태로 반환한다.</code></pre></li>
<li><code>take(N)</code> : 앞의 N개 요소를 배열로 반환</li>
</ul>
</li>
</ol>
<pre><code class="language-python">data = [1, 2, 3, 4, 5]
rdd = sc.parallelize(data)  # list -&gt; RDD

# Transformation
trasformed_rdd = rdd.filter(lambda x: x % 2 == 0).map(lambda x: x * 10)

# Action
result = transformed_rdd.collect()</code></pre>
<p><strong>SparkSession - DataFrame API 진입점</strong></p>
<ul>
<li>SparkContext : RDD 생성을 위한 기본 진입점</li>
<li>SparkSession : Spark DataFrame과 상호작용하는 단일 진입점</li>
<li>SparkSession으로 DataFrame 생성, 등록, SQL 쿼리 실행을 수행</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.22(Wed)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.22Wed-zugji6hu</link>
            <guid>https://velog.io/@o_u--chan/2026.04.22Wed-zugji6hu</guid>
            <pubDate>Fri, 24 Apr 2026 16:12:43 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python">import numpy as np
tmp = sales_df[sales_df[&#39;price_each&#39;].str.strip().replace(&#39;&#39;, np.nan).fillna(&#39;0&#39;).astype(float)]
tmp[tmp[&#39;price_each&#39;] == 0]</code></pre>
<p>→ KeyError 발생</p>
<p>Boolean Indexing을 사용할 때는 대괄호 <code>[]</code> 안에 들어가는 데이터의 형태가 중요하다.</p>
<ol>
<li>대괄호 <code>[]</code> 내부의 데이터 타입<blockquote>
<p><code>sales_df[...]</code> 내부에는 True 또는 False로 이루어진 리스트가 들어가야 한다. </p>
</blockquote>
</li>
</ol>
<p><strong>PySpark</strong> #pyspark</p>
<hr>
<p>Apache Spark : 대규모 데이터를 빠르게 처리하도록 설계된 오픈 소스 분산 컴퓨팅 시스템</p>
<ul>
<li>pyspark → apache spark의 파이썬 인터페이스</li>
</ul>
<p>python workflow에서 병렬 계산으로 대용량 데이터셋을 효율적으로 처리하며, 배치 처리, 실시간 스트리밍, 머신러닝, 데이터 분석, SQL query에 적합하다.</p>
<ul>
<li>대규모 데이터 분석 : spark의 inmemory 연산을 활용한 분산 데이터 처리</li>
<li>대규모 데이터셋 머신러닝 </li>
<li>ETL 및 ELT pipeline : 다양한 소스의 대량 원시 데이터를 구조화된 형식으로 변환</li>
</ul>
<p>pyspark dataframe은 다른 DF와 유사하지만 PySpark에 최적화되어 있다.</p>
<ul>
<li><code>spark.read.csv(file_name, [column_names])</code></li>
<li><code>.printSchema()</code> : DataFrame의 구조 확인</li>
<li><code>.count()</code> : DataFrame 행 개수 세기</li>
<li><code>.groupBy()</code>, <code>agg()</code> → SQL 유사 집계</li>
<li><code>filter()</code> : SQL의 where처럼 동작</li>
<li><code>select()</code> : SQL의 select</li>
</ul>
<p>pandas는 단일 compute instance에서 동작하는 반면, PySpark는 여러 인스턴스에 데이터를 분산해 처리 속도와 확장성을 확보한다. </p>
<p><strong>결측치 처리</strong></p>
<ul>
<li><code>na.drop()</code> : null 값이 있는 행 삭제</li>
<li><code>.where(col(&#39;columnName&#39;).isNotNull())</code> : null 값이 아닌 행만 뽑기</li>
<li><code>.na.fill({&#39;column&#39;: value)</code> : null → 특정 값으로 대체</li>
</ul>
<p><strong>column 작업</strong></p>
<ul>
<li><code>.withColumn()</code> : 계산/기존 컬럼 기반 새 컬럼 추가
  <code>df = df.withColumn(&#39;age_plus_5&#39;, df[&#39;age&#39;] + 5)</code></li>
<li><code>withColumnRenamed()</code> : column명 변경
  <code>df = df.withCOlumnRenamed(&#39;age&#39;, &#39;years&#39;)</code></li>
<li><code>drop()</code> : 불필요한 컬럼 제거</li>
</ul>
<p><strong>UNION 연산</strong> : <strong>구조가 같은</strong> 두 DataFrame을 <strong>위아래로</strong> 쌓아 하나로 만드는 도구이다.</p>
<ul>
<li><code>df_union = df1.union(df2)</code></li>
</ul>
<p><strong>Array와 Map</strong></p>
<ul>
<li>Array : 열 내 리스트 저장에 유용
  <code>ArrayType(StringType(), False)</code></li>
<li>Map: key-value 쌍, 딕셔너리형 데이터에 적합
  <code>MapType(StringType(), StringType()</code></li>
</ul>
<p><strong>StructType &amp; StructField</strong></p>
<hr>
<p>StructType : 제목과 데이터 타입의 묶음 → DataFrame 전체의 구조 : 여러 개의 <code>StructField</code>를 리스트 형태로 담고 있다.</p>
<p>StructField </p>
<ul>
<li>Column name</li>
<li>Data Type : IntegerType, StringType …</li>
<li>Null 허용 여부 : None을 허용할 지 결정(True or False)</li>
</ul>
<p>Pyspark는 데이터의 type를 추론(Inference)할 수 있지만, 데이터의 양이 많으면 type을 알아내기 위해 데이터를 다 흝어봐야 하기 때문에 시간이 오래 걸린다.  → schema를 미리 정해줌으로써 시간을 단축한다.</p>
<p><strong>RDD(Resilient Distributed Dataset)</strong></p>
<hr>
<p>PySpark → 병렬화를 통한 대규모 데이터 처리를 수행하는 능력이 뛰어나다.</p>
<blockquote>
<p>병렬화 : 데이터를 클러스터의 여러 노드로 나누어 데이터와 연산을 분산시킨다. Spark에서 정의된 연산은 자동으로 분산되어, 대규모 데이터셋을 효율적으로 처리할 수 있다.
작업은 워커 노드에 할당되어 병렬로 데이터를 처리하고, 마지막에 결과를 합친다.</p>
</blockquote>
<p>RDD : 클러스터 전반에 분산된 데이터 컬렉션을 표현하는 Spark의 핵심 빌딩 블록</p>
<ul>
<li>불변 객체이므로 한 번 생성되면 변경할 수 없다.</li>
<li>대신 <code>map()</code>, <code>filter()</code>같은 연산으로 새로운 RDD를 만들 수 있다.</li>
<li>RDD 연산의 결과를 가져오는 <code>collect()</code> 같은 액션도 지원한다.</li>
</ul>
<p><strong>DataFrame → SQL 실행</strong></p>
<pre><code class="language-python">df.createOrReplaceTempviwe(&quot;people&quot;)
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.21(Tue)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.21Tue</link>
            <guid>https://velog.io/@o_u--chan/2026.04.21Tue</guid>
            <pubDate>Fri, 24 Apr 2026 16:12:12 GMT</pubDate>
            <description><![CDATA[<h4 id="fixture-python"><strong>Fixture</strong> #python</h4>
<hr>
<p>테스트를 실행하기 위해 필요한 준비물
<code>@pytest.fixture</code>라는 데코레이터를 사용해서 함수를 정의하면 다른 테스트 함수들이 이름을 파라미터처럼 넘겨받아 사용할 수 있다.</p>
<pre><code class="language-python">import pytest

@pytest.fixture
def sample_data():
    return {&quot;name&quot;: &quot;chan&quot;, &quot;age&quot;: &quot;25&quot;}

def test_check_name(sample_data):
    # sample_data return 값이 자동으로 넘겨진다.
    assert sample_data[&#39;name&#39;] == &#39;chan&#39;</code></pre>
<p><strong>Fixture의 범위 (Scope)</strong></p>
<hr>
<p>fixture를 매번 새로 만들지, 아니면 한 번 만들어서 계속 쓸지를 결정하는 것을 scope라고 한다.
<code>@pytest.fixture(scope=&quot;설정&quot;)</code></p>
<ul>
<li>function (default) : 테스트 함수 실행될 때마다 매번 fixture를 새로 만든다.</li>
<li>class : 같은 클래스 안에 있는 테스트 메서드끼리 fixture를 공유한다.</li>
<li>module : 해당 <code>.py</code> 파일(module) 안에 있는 모든 테스트가 fixture를 한 번만 만들어 공유</li>
<li><code>session</code> : 전체 테스트 세션 동안 한 번만 만든다. → DB 연결처럼 무겁고 공통적인 작업에 사용</li>
</ul>
<p><strong>Setup과 Teardown(yield)</strong></p>
<hr>
<p>테스트를 위해 만든 임시 파일들을 정리하는 키워드가 <code>yield</code>이다.</p>
<p>fixture에서 <code>yield</code>를 사용하면</p>
<ol>
<li><code>yield</code> 전까지 코드가 실행된다.(테스트 준비 : <strong>Setup</strong>)</li>
<li>테스트 함수가 실행되는 동안 잠시 멈춰 있는다.</li>
<li>테스트가 끝나면 <code>yield</code> 다음 코드부터 다시 실행된다. (뒷정리 : <strong>Teardown</strong>)</li>
</ol>
<pre><code class="language-python">import pytest
import os

@pytest.fixture
def temp_file():
    # setup 테스트용 파일 만들기
    f = open(&quot;test.txt&quot;, &quot;w&quot;)
    f.wrtie(&quot;hello pytest&quot;)
    f.close()

    yield &quot;test.txt&quot;   # test 함수에 파일 이름 전달하고 대기

    # Teardown : 테스트가 끝나면 파일 삭제
    os.remove(&quot;test.txt&quot;)
    print(&quot;\n tmp file 삭제 완료&quot;)</code></pre>
<p>만약 테스트 도중에 에러가 발생해서 테스트가 실패하더라도 pytest는 <code>yield</code> 뒤의 뒷정리 코드를 실행해서 테스트 환경을 깨끗이 유지할 수 있다.</p>
<p><strong>autouse</strong> </p>
<hr>
<p>자동으로 사용되는 fixture, 
fixture를 사용하려면 테스트 함수 인자에 fixture 이름을 적어줘야 하지만 <code>autouse=True</code> 설정을 하면, 이름을 적지 않아도 <strong>해당 범위 내의 모든 테스트에서 알아서 실행된다.</strong></p>
<pre><code class="language-python">import pytest

@pytest.fixture(autouse=True)
def setup_log():
    print(&quot;[log] test start&quot;)
    yield
    print(&quot;[log] test finish&quot;)

def test_ex_1():
    # 인자에 setup_log 적지 않아도 자동 실행
    assert 1 == 1

def test_ex_2():
    assert &#39;a&#39; == &#39;a&#39;</code></pre>
<p>사용하기 좋은 때</p>
<ul>
<li>성능 모니터링 : 모든 테스트의 시작과 종료 시간을 기록하고 싶을 때</li>
<li>로그 기록 : 테스트마다 어떤 작업이 일어나는지 항상 남기고 싶을 때</li>
<li>환경 초기화 : 특정 폴더 항상 비워두거나, 공통 환경 변수 설정할 때</li>
</ul>
<p><strong>benchmark fixture</strong></p>
<hr>
<p><code>pytest-benchmark</code> 설치 시 제공되는 <code>benchmark</code>라는 fixture → 측정하고 싶은 함수를 인자로 받아서 <strong>여러 번 반복 실행한 뒤, 평균 시간을 계산</strong>해 준다.</p>
<pre><code class="language-python">import pytest

@pytest.fixture
def big_data():
    # test 위한 대용량 데이터 준비
    return list(range(10000, 0, -1))

def test_sort_performance(benchmark, big_data):
    # benchmark fixture가 big_data 정렬하는 시간 측정
    result = benchmark(sorted, big_data)

    assert result == list(range(1, 100001))</code></pre>
<p>fixture를 활용해서 benchmark를 쓰는 이유</p>
<ul>
<li>데이터 준비 시간 제외 : benchmark의 핵심은 그 로직을 실행하는 시간만 재는 것이다. fixture를 쓰면 setup 시간은 측정에 포함되지 않고, <code>benchmark()</code> 안에 들어간 코드의 시간만 잴 수 있다.</li>
<li>다양한 환경 테스트 : fixture의 <code>params</code> 옵션을 사용하면 다양한 조건에서 성능이 어떻게 변하는지(시간 복잡도)를 한 번에 테스트할 수 있다.</li>
</ul>
<h4 id="pytest-python-pytest"><strong>pytest</strong> #python #pytest</h4>
<p>기능테스트, 유닛테스트, 통합테스트, 성능테스트(pytest-benchmark)</p>
<hr>
<p><strong>성능 테스트</strong> </p>
<hr>
<p><strong>종류</strong></p>
<ul>
<li>부하 테스트(Load Testing) : 평상시 부하를 얼마나 잘 견디는지</li>
<li>스트레스 테스트(Stress Testing) : 시스템의 한계치가 어디인지 확인, 시스템 터졌을 때 어떻게 복구되는지</li>
<li>내구성 테스트(Endurance / Soak Testing) : 오랜 시간 부하 주었을 때 메모리 누수나 자원 소모 생기는지</li>
</ul>
<p><strong>성능 테스트 핵심 지표</strong></p>
<ol>
<li>Latency (응답 시간 / 지연 시간) : 사용자가 요청을 보내고 응답을 받을 때까지 걸리는 시간</li>
<li>Throughput (처리량) : 단위 시간당 시스템이 처리하는 요청의 양</li>
<li>Error rate (에러율) : 전체 요청 중 실패한 요청의 비율</li>
</ol>
<p>여기서, 평균 응답 시간에 매몰되어서는 안된다. 90%의 유저는 잘 되어도 10%의 유저가 오래 걸리면 안되기 때문에 P95, P99(상위 95%, 상위 99%의 응답 시간) 같은 지표를 더 중요하게 봐야 한다.</p>
<p><strong>성능 테스트 도구와 프로세스</strong></p>
<hr>
<p>성능 테스트 도구</p>
<ul>
<li>Locust : 파이썬 기반</li>
<li>JMeter : 자바 기반, GUI 환경</li>
<li>nGrinder : 네이버에서 만듦</li>
</ul>
<p><strong>테스트  진행 순서(Process)</strong></p>
<ol>
<li>목표 설정</li>
<li>환경 구축</li>
<li>시나리오 작성</li>
<li>부하 실행</li>
<li>결과 분석 및 튜닝</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.19(Sun)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.19Sun</link>
            <guid>https://velog.io/@o_u--chan/2026.04.19Sun</guid>
            <pubDate>Fri, 24 Apr 2026 15:20:20 GMT</pubDate>
            <description><![CDATA[<h2 id="oop">OOP</h2>
<ul>
<li><p>코드 재사용</p>
</li>
<li><p>DRY : 반복하지 말 것</p>
<p><strong>Python Class Decorator</strong> #python </p>
</li>
</ul>
<hr>
<p>python에서 decorator는 호출 가능한 객체(Callable)를 받아서 다른 객체를 반환하는 함수</p>
<ul>
<li>기존 기능을 수정하지 않고 새로운 기능을 추가</li>
</ul>
<p>class decorator는 클래스 자체를 수정하거나 확장할 때 사용</p>
<ul>
<li><p><code>__init__</code> , <code>__call__</code> </p>
<pre><code class="language-python">class CallCounter:
  def __init__(self, func):
      self.func = func
      self.count = 0  # 호출 횟수를 저장하는 상태

  def __call__(self, *args, **kwargs):
      self.count += 1
      print(f&quot;{self.func.__name__} 함수가 {self.count}번 호출되었습니다.&quot;)
      return self.func(*args, **kwargs)
</code></pre>
</li>
</ul>
<p>@CallCounter
def say_hello():
    print(&quot;안녕하세요!&quot;)</p>
<p>say_hello()
say_hello()</p>
<pre><code>→ `sayl_hello`가 실행될 때마다, `self.count` 1씩 올리며 상태 유지


**class method** #python
___
python에서 일반 메서드는 첫 번째 인자로 `self`를 받는다. → `self`는 이미 만들어진 구체적인 객체(인스턴스)
- 일반 메서드 사용하려면 이미 `객체 = class()` 를 통해 이미 객체가 존재해야 한다.

**IF, 직원 데이터를 파일에서 읽어와서, 그 데이터를 바탕으로 직원을 새로 만들고 싶다면?**
- 직원을 만드려고 하는데
- 아직 직원 객체가 존재하지 않는다.
- 일반 메서드에서는 &#39;존재하는 직원&#39;이 수행하는 동작
- 존재하지 않는 객체에 메서드를 수행시킬 수는 없다!
- → `@classmethod` 등장

class method는 객체가 없어도 클래스(설계도)만 있으면 호출할 수 있다. 
&gt; 자동차 회사에 전화 걸어서 직원 파일을 보낼 테니 자동차(객체)를 뽑아주세요!
&gt; 자동차 객체는 없지만 자동차 회사 클래스는 존재한다!

```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    # 일반 메서드는 불가능
    def from_file_instance(self, filename):
        # 이 메서드를 부르려면 이미 Employee 객체가 있어야 하는데,
        # 우리는 객체를 &#39;만들기 위해&#39; 이 기능을 쓰려는 거라 모순이 생겨요.
        pass

    # 클래스 메서드는 가능!
    @classmethod
    def from_file(cls, filename):
        # 파일 읽기 로직 (생략)
        name, salary = &quot;철수&quot;, 50000 
        # cls는 Employee 클래스 그 자체이므로, 여기서 객체를 생성해서 반환합니다.
        return cls(name, salary) 

# 객체가 하나도 없는 상태에서도 호출 가능!
new_emp = Employee.from_file(&quot;info.txt&quot;)</code></pre><p>일반 메서드는 <strong>만들어진 객체가 하는 행동</strong>이고, 클래스 메서드는 <strong>객체를 만들기 전에도 클래스가 할 수 있는 행동</strong> → 대체 생성자(파일로 만들기, JSON으로 만들기 …)는 클래스 메서드로 만드는 것이 논리적으로 좋다. </p>
<pre><code class="language-python">class Person:
    CURRENT_YEAR = 2024
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # Add a class method decorator
    @classmethod
    # Define the from_birth_year method
    def from_birth_year(cls, name, birth_year):
    # Create age
    age = Person.CURRENT_YEAR - birth_year
    # Return the name and age
    return cls(name, age)

bob = Person.from_birth_year(&quot;Bob&quot;, 1990)</code></pre>
<p><code>self</code>는 일반 메서드에서 <strong>나중에 생성될 실제 객체</strong>를 담기 위한 빈 그릇과 같다. 
일반 메서드 호출 시 파이썬 내부 실행 동작은 다음과 같다.</p>
<ol>
<li><code>emp = Employee()</code> 객체 탄생</li>
<li><code>emp.general_method()</code> 호출</li>
<li>파이썬이 자동으로 <code>Employee.general_method(emp)</code>로 변환해서 실행 → 객체 emp를 <code>self</code> 자리에 넣어준다.</li>
</ol>
<p><strong>그래서, 일반 메서드는 실행되는 순간에 반드시 <code>self</code> 자리에 들어갈 실제 객체가 메모리에 살아있어야만 작동한다.</strong></p>
<p><code>Employee.general_method()</code>처럼 호출하면 <code>self</code> 자리에 객체를 넣어줘야 하는데, 전달된 객체가 아무것도 없기 때문에 <code>self</code>에 뭘 넣어서 실행할 지 모르기에 에러가 발생한다.</p>
<p>→<strong>그렇기 때문에, <del>={cyan}파일을 읽는 로직의 함수가 클래스 내부에 써 있더라도, 그 메서드 형식이 <code>self</code> 인자를 받는 일반 메서드라면 객체가 생겨야만 쓸 수 있는 기능=</del>이다</strong></p>
<p>class method가 가능한 이유는 <code>self</code> 대신 인자로 <code>cls</code>를 받기 때문이다.</p>
<ul>
<li>일반 메서드 : <code>self</code>가 있어야만 기능 수행</li>
<li>클래스 메서드 : <code>self</code>가 아니라 <code>cls</code>(class 설계도)만 있으면 기능 수행 가능</li>
</ul>
<p>그래서 파일로부터 객체를 만드는 <code>from_file</code> 같은 기능은 객체가 생성되기 전이기 때문에, <code>self</code> 인자가 아니라 <strong>class method</strong>나 <strong>정적 메서드(staticmethod)</strong>로 만들어야만 호출이 가능하다!!</p>
<p><strong>static method(정적 메소드)</strong> #python </p>
<hr>
<p>class 안에 있지만, <code>self</code>나 <code>cls</code>에 대한 정보와 관련 없이 사용한다. 
인자를 정의할 때, 첫 인자로 <code>self</code>나 <code>cls</code>를 쓰지 않는다.</p>
<ul>
<li>클래스 이름으로 바로 호출 가능</li>
<li>논리적으로 클래스와 관련 있는 편의 기능 묶어둘 때 사용</li>
</ul>
<pre><code class="language-python">class Calculator:
    @staticmethod
    def add(a, b):
        return a + b

# 객체 만들지 않아도 바로 사용 가능
print(Calculator.add(10, 20))</code></pre>
<p><strong>singleton desing pattern</strong> #python </p>
<hr>
<p>어떤 클래스의 인스턴스가 프로그램 전체에서 오직 하나만 존재하도록 보장하는 디자인 패턴</p>
<p><strong>why?</strong>
여러 곳에서 동시에 접근하면 안되거나, 자원을 공유해야 하는 경우에 필요하다.</p>
<ul>
<li>DB 연결 객체 : 연결을 여러 개 만들면, 서버 부하가 커지기 때문에 하나만 만든다.</li>
<li>Settings 관리자 : 프로그램 설정값은 하나로 통일되어야 한다.</li>
<li>Logging 객체 : 로그를 기록하는 통로를 하나로 통일할 때 쓴다.</li>
</ul>
<pre><code class="language-python">def singleton(cls):
    # 생성될 객체를 담아둘 저장소
    instances = {} 

    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)

        return instances[cls]

    return get_instance

@singleton
class Database:
    def __init__(self):
        print(&#39;connected database&#39;)

db1 = Database()
db2 = Database()

print(db1 is db2) # resutl = True</code></pre>
<p>데코레이터로 구현하는 방법 이외에도, 클래스 자체의 생성 과정을 제어하는 <code>__new__</code> 매직 메서드를 사용할 수 있다.</p>
<pre><code class="language-python">class SingletonClass:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            # 인스턴스가 없을 때만 부모 클래스의 __new__를 호출해 생성
            cls._instance = super().__new__(cls)</code></pre>
<p><strong>팩토리 패턴</strong> #python </p>
<hr>
<p>어떤 객체를 만들지 결정하는 로직을 별도의 클래스나 함수에 둔다.</p>
<ul>
<li>유연성 : 나중에 새로운 종류의 객체가 추가되어도, 메인 코드를 수정하지 않고 팩토리 객체에 추가하면 된다.</li>
<li>복잡함 은폐 : 객체를 만드는 과정이 복잡할 때(여러 설정 거칠 때), 사용자가 이를 모른 상태로도 할 수 있도록 한다.
(식당에 가서 레시피를 모르고 메뉴를 시킬 수 있는 것처럼)</li>
</ul>
<pre><code class="language-python">class Dog:
    def speak(self):  return &quot;wow&quot;
class Cat:
    def speak(self): return &#39;yaong&#39;

class PetFactory:
    @staticmethod
    def get_pet(pet_type):
    pets = {&quot;dog&quot;: Dog, &quot;cat&quot;: Cat}
    return pets.get(pet_type, Dog)()

my_pet = PetFactory.get_pet(&quot;cat&quot;)
print(my_pet.speak())</code></pre>
<p><strong>factory method patter 확장</strong>
<code>if-else</code>, dictionary로 객체를 찍어내는 것을 넘어서 공장 자체를 추상화하는 방식이 있다. 
이를 <strong>factory method pattern</strong>이라고 한다.</p>
<pre><code class="language-python">from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):   pass

class JindoDog(Ainmal):
    def speak(self):   return &#39;진돗개 멍&#39;

class Spasal(Animal):
    def speak(self):   return &#39;삽살개 멍&#39;

class AnimalFactory(ABC):
    @abstractmethod
    def create_method(self):   pass

    def deliver(self):
        animal = self.create_animal()
        print(f&quot; {animal.speak() = }&quot;)

class SeoulFacotry(AnimalFactory):
    def create_animal(self):  return JindoDog()

class BusanFactory(AnimalFactory):
    def create_animal(self):   return Sapsal()


seoul = SeoulFactory()
seoul.deliver()</code></pre>
<p>→ 객체 생성 로직 분리되어 있어, 새로운 종류의 객체가 추가되어도 기존 코드를 수정하기가 편하다.</p>
<p><strong>Abstract class(추상 클래스)</strong> #python </p>
<hr>
<p>상속받는 자식 클래스들이 반드시 구현해야 할 메서드를 지정해 주는 가이드라인</p>
<ul>
<li>자기 자신으로는 객체를 만들 수 없지만, 다른 클래스들이 공통적으로 가져아 할 특정을 정의할 때 사용한다.</li>
</ul>
<p><strong>왜 미완성 클래스를 만들고, <code>abc</code> module이 필요한가?</strong></p>
<ul>
<li>협업할 때, 공통적으로 지켜야 할 규칙을 <strong>강제</strong>하기 위함이다.</li>
</ul>
<pre><code class="language-python">from abc import ABC, abstractmethod

class Animal(ABC): # ABC를 상속받으면 추상 클래스가 된다.
    @abstractmethod
    def move(self):
        &quot;&quot;&quot;animal이라면 반드시 move 기능이 있어야 한다.&quot;&quot;&quot;
        pass

my_animal = Animal() # 추상 클래스는 직접 객체를 만들 수 없으므로 에러 발생

class Human(Animal):
    def move(self):
        print(&#39;두 발로 걸음&quot;)

class Fish(Animal):
    def move(self):
        print(&#39;지느러미로 헤엄침&#39;)

h = Human()
h.move()</code></pre>
<p>만약 <code>move()</code> 메서드를 정의하지 않으면 <code>Can&#39;t instantiate abstract class Fish with abstract method move</code> 라는 에러를 띄운다.</p>
<p>파이썬에는 다른 언어처럼 별도의 <code>interface</code> 키워드는 없지만, 추상 클래스를 이용해 그 역할을 수행한다.</p>
<ul>
<li>추상 클래스 : 자식 클래스는 부모 클래스의 기능을 쓰고, 필요한 건 추가로</li>
<li>인터페이스(형태) : 강제 메서드만 존재(기능은 없고 형태만 갖춤)</li>
</ul>
<pre><code class="language-python">from abc import ABC, abstractmethod

# python style&#39;s interface
class Remocon(ABC):
    @abstractmethod
    def turn_on(self):   pass
    def turn_off(self):  pass

# functions - implement interface
class TV(Remocon):
    def turn_on(self): print(&#39;turn on tv&#39;)
    def turn_off(self): print(&#39;turn off tv&#39;)

class Airconditioner(Remocon):
    def turn_on(self): print(&#39;turn on air&#39;)
    def turn_off(self): print(&#39;turn off air&#39;)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.17(Fri)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.17Fri</link>
            <guid>https://velog.io/@o_u--chan/2026.04.17Fri</guid>
            <pubDate>Fri, 24 Apr 2026 15:19:56 GMT</pubDate>
            <description><![CDATA[<h1 id="shell">Shell</h1>
<p><code>sort</code> puts data in order. By default it does this in ascending alphabetical order, but the flags <code>-n</code> and <code>-r</code> can be used to sort numerically and reverse the order of its output, while <code>-b</code> tells it to ignore leading blanks and <code>-f</code> tells it to <strong>f</strong>old case (i.e., be case-insensitive). Pipelines often use <code>grep</code> to get rid of unwanted records and then <code>sort</code> to put the remaining records in order.</p>
<p><code>uniq</code>, whose job is to remove duplicated lines. More specifically, it removes <em>adjacent</em> duplicated lines</p>
<h1 id="container--가상화">container &amp; 가상화</h1>
<p>컨테이너화</p>
<ul>
<li>운영 체제 수준의 가상화</li>
<li>애플리케이션과 그 의존성을 OS 커널이 관리하는 자체 환경의 컨테이너로 패키징하는 과정</li>
</ul>
<p>컨테이너</p>
<ul>
<li><p>각 애플리케이션마다 전용 OS 필요 없이, 하나의 호스트 OS에서 실행 가능</p>
</li>
<li><p>각 애플리케이션이 고유한 환경을 가지며 다른 application과 의존성 충돌되지 않는다.</p>
</li>
<li><p>격리성 제공 → 다른 ps에 간섭하지 않는다</p>
</li>
<li><p>높은 이식성과 재현성</p>
</li>
</ul>
<p>컨테이너 오케스트레이션</p>
<ul>
<li>선언적 프로그래밍 사용(원하는 결과를 정의하고, 그 결과에 도달하는 절차는 직접 명시하지 않는 방식)</li>
<li>필요할 때 컨테이너 손쉽게 확장 가능</li>
<li>운영 자동화</li>
</ul>
<p>데이터 모델 : 데이터셋의 논리적 구성과 해석을 정의</p>
<ul>
<li>데이터와 그 구성 요소들이 서로 어떻게 관련되는지</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.16(Thu)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.16Thu</link>
            <guid>https://velog.io/@o_u--chan/2026.04.16Thu</guid>
            <pubDate>Fri, 24 Apr 2026 15:19:38 GMT</pubDate>
            <description><![CDATA[<h1 id="현대-데이터-아키텍쳐">현대 데이터 아키텍쳐</h1>
<h2 id="data-mesh--data-febric">Data Mesh &amp; Data Febric</h2>
<table>
<thead>
<tr>
<th align="center">항목</th>
<th>데이터 매쉬</th>
<th>데이터 패브릭</th>
</tr>
</thead>
<tbody><tr>
<td align="center">관점</td>
<td>조직/도메인, 운영 모델 중심</td>
<td>기술/플랫폼, 통합 레이어 중심</td>
</tr>
<tr>
<td align="center">주요 목표</td>
<td>도메인 팀 자율성, 데이터 제품화, 중앙 병목 제거</td>
<td>이질적인 소스의 통합·일관된 거버넌스·단일 접근 계층</td>
</tr>
<tr>
<td align="center">데이터 소유권</td>
<td>도메인 팀이 소유·운영(분산)</td>
<td>중앙 플랫폼/데이터팀이 관리(상대적 중앙집중)</td>
</tr>
<tr>
<td align="center">기술 초점</td>
<td>도메인별 파이프라인, 데이터 제품, 셀프서비스 플랫폼</td>
<td>메타데이터, 카탈로그, 통합·오케스트레이션, 자동화</td>
</tr>
<tr>
<td align="center">지연/워크로드</td>
<td>Batch+Streaming 모두, 도메인별로 선택 (예: Netflix Data Mesh는 실시간 스트리밍 기반)</td>
<td>Batch/Streaming 모두 지원하지만, “한 레이어에서 접근”에 초점</td>
</tr>
<tr>
<td align="center">확장성</td>
<td>조직이 커질수록 도메인 단위로 자연스럽게 확장 가능하나, 거버넌스 복잡도 증가</td>
<td>기술적으로 수평 확장이 용이하지만, 중앙 레이어가 병목/복잡해질 수 있음</td>
</tr>
<tr>
<td align="center">관계</td>
<td>“어떻게 조직을 나눠 데이터 제품을 운영할까?”에 답하는 모델</td>
<td>“어떻게 여러 시스템을 기술적으로 연결·관리할까?”에 답하는 모델</td>
</tr>
<tr>
<td align="center">데이터 처리</td>
<td></td>
<td></td>
</tr>
<tr>
<td align="center">- 탐색</td>
<td></td>
<td></td>
</tr>
<tr>
<td align="center">- 데이터 품질 : 점검 및 변환</td>
<td></td>
<td></td>
</tr>
<tr>
<td align="center">- 분석</td>
<td></td>
<td></td>
</tr>
<tr>
<td align="center">- 집계</td>
<td></td>
<td></td>
</tr>
<tr>
<td align="center">- 변환</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>배치 처리 </p>
<ul>
<li>배치 &amp; 스트리밍</li>
<li>고정된 시간 처리</li>
</ul>
<p>스트리밍 처리 (고정 시간 윈도우 &amp; 슬라이딩 시간 윈도우)</p>
<ul>
<li>윈도우 : 연속적인 데이터 스트림을 시간 또는 크기 기준으로 나눈 파티션으로, 계산과 집계를 수행하기 위해 사용된다.</li>
</ul>
<p>비용 모델</p>
<ul>
<li>pay-as-you-go : 쓴 만큼 내라<ul>
<li><strong>네트워크 트래픽</strong></li>
<li>저장한 바이트 수</li>
<li>보관 기간</li>
<li>데이터 작업<ul>
<li>이동</li>
</ul>
</li>
</ul>
</li>
<li>예약 용량 : 구독료</li>
</ul>
<p>비용 최적화</p>
<ul>
<li>서비스 품질 유지하면서 비용 절감</li>
</ul>
<h2 id="nosql">NoSQL</h2>
<p><strong>표 형식 NoSQL 데이터 저장소</strong></p>
<pre><code class="language-postgresql">SELECT
    title,
    price
FROM books
WHERE pirce &lt;50.00;</code></pre>
<p>열 기반 데이터베이스에서 쿼리 처리 과정</p>
<ol>
<li><code>price</code> 열에 가서 조건절에 맞게 필터링</li>
<li>반환되는 행에서 title 컬럼 값들을 뽑아서 필터링 결과로 반환</li>
<li>title, price 컬럼만 처리하여 반환</li>
</ol>
<p><strong>non-table NoSQL</strong></p>
<p>문서형 데이터베이스 : 키-값, 키-배열, 키-객체 쌍으로 구성된 유연한 반정형 형식에 데이터를 저장</p>
<p>snowflake -micro partition : </p>
<ul>
<li>각 마이크로 파티션에는 50~500 MB 사이의 압축되지 않은 데이터가 포함<ul>
<li>데이터 항상 압축되어 저장되기에 실제로는 더 작다</li>
</ul>
</li>
<li>테이블의 행 그룹은 열 방식으로 구성된 개별 마이크로 파티션에 매핑된다.</li>
<li>→ 초대형 테이블의 매우 세분화된 정리 가능하도록 한다</li>
</ul>
<p><strong>이점</strong></p>
<ul>
<li><p>기존의 정적 파티셔닝과 달리 Snowflake 마이크로 파티션은 자동으로 파생됩니다. 명시적으로 사전에 정의하거나 사용자가 유지 관리할 필요가 없습니다.</p>
</li>
<li><p>이름에서 알 수 있듯이 마이크로 파티션은 크기가 작기 때문에(압축 전 50~500 MB), 매우 효율적인 DML 및 더 빠른 쿼리를 위한 세분화된 정리가 가능합니다.</p>
</li>
<li><p>마이크로 파티션은 값 범위에서 겹칠 수 있으며 균일하게 작은 크기와 결합되어 왜곡을 방지하는 데 도움이 됩니다.</p>
</li>
<li><p>열은 종종 <em>열 저장소</em> 라고 하는 마이크로 파티션 내에 독립적으로 저장됩니다. 이를 통해 개별 열을 효율적으로 스캔할 수 있습니다. 쿼리에서 참조하는 열만 스캔됩니다.</p>
</li>
<li><p>열은 또한 마이크로 파티션 내에서 개별적으로 압축됩니다. Snowflake는 각 마이크로 파티션의 열에 대해 가장 효율적인 압축 알고리즘을 자동으로 결정합니다.</p>
</li>
</ul>
<p>나중에 다시 보자. 아직은 제대로 이해가 안 간다. </p>
<p>테이블에 저장된 데이터는 자연 차원(날짜, 지리)에 따라 정렬된다. → 쿼리 성능에 큰 영향
snowflake에서는 데이터가 테이블에 삽입되면서 클러스터링 메타데이터가 수집되고 프로세스 중에 생성된 각 마이크로 파티션에 대해 기록된다. 그 다음, 이 클러스터링 정보를 활용하여 쿼리 중 마이크로 파티션의 불필요한 스캔을 방지하고 이러한 열을 참조하는 쿼리의 성능을 크게 가속화한다. </p>
<p><strong>데이터 클러스터링</strong></p>
<ul>
<li>유사한 데이터 포인트를 함께 구성/그룹화</li>
<li>데이터 적재 시 자동 수행</li>
</ul>
<p><strong>query pruning</strong>  : 조건에 맞는 데이터가 없을 것 같은 파티션•파일•컬럼•마이크로 파티션은 아예 읽지 않는 최적화 기법</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.15(Wed)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.15Wed</link>
            <guid>https://velog.io/@o_u--chan/2026.04.15Wed</guid>
            <pubDate>Fri, 24 Apr 2026 15:19:11 GMT</pubDate>
            <description><![CDATA[<h1 id="fundamentals-of-data-engineering">Fundamentals of Data Engineering</h1>
<p>데이터는 사실과 수치의 비조직적이고 맥락 없는 집합이다.</p>
<ul>
<li>아날로그와 디지털 형식이 있고</li>
<li>다양한 곳에서 데이터가 수집된다.</li>
<li>→ 원천 시스템 문서를 읽고 그 패턴과 특이점을 이해하자</li>
<li>→ RDBMS를 사용한다면 그 시스템의 작동 방식을 익히고 영향을 줄 수 있는 요소들을 파악하자.</li>
</ul>
<p>파일 : byte의 sequence → disk에 저장된다.</p>
<ul>
<li>로컬 매개변수, 이벤트, 로그, 이미지, 오디오 저장 
주로 보는 파일의 형식은 <strong>엑실, csv, txt, json, xml</strong></li>
<li>정형 : excel, csv</li>
<li>반정형 : json, xml, csv</li>
<li>비정형 : txt, csv
<code>+</code> parquet, ORC 등</li>
</ul>
<h3 id="apiapplication-programming-interface">API(application programming interface)</h3>
<p>시스템 간 데이터를 교환하는 표준 방식
#API</p>
<h3 id="oltp">#OLTP</h3>
<p>짧은 지연시간과 높은 동시성 지원</p>
<p>#ACID : 원자성, 일관성, 독립성, 내구성</p>
<p>일부 분산형 데이터베이스는 최종 일관성과 같은 완화된 일관성 제약 조건을 사용하기도 한다.</p>
<p>원자적 트랜잭션 : 트랜잭션이 진행됨에 있어서, 모든 트랜잭션이 성공하든가 모두 실패해야 한다.
→ 전체 작업이 트랜잭션으로서 발생해야 한다. </p>
<p>#OLAP 
OLAP에서도 OLTP처럼 여러 쿼리문이 실행되면 리소스 경쟁이 일어난다.
하지만 OLTP는 같은 엔진/노드에서 경쟁이 일어나서 UX가 깨진다는 게 문제고, OLAP는 멀티 노드 MPP 구조로 느려지긴 해도 그렇게 큰 문제는 되지 않는다. </p>
<p>OLAP의 Online 부분은 시스템이 들어오는 쿼리를 지속해 수신 대기한다는 뜻으로 OLAP 시스템이 대화형 분석에 적합함을 의미한다. </p>
<p>그런데, 종종 원천 시스템이 아니라 DWH에서 다시 원천 시스템으로 데이터를 보내야 할 때가 있는데, 이때 역뱡향 ETL 워크플로를 OLAP 시스템이 제공할 수 있다. </p>
<p>#log : 최소한 누가, 무엇을, 언제 수행했는지 수집해야 한다.
인코딩 방법</p>
<ul>
<li>바이너리 인코딩 로그</li>
<li>반정형 로그 </li>
<li>일반 텍스트 로그</li>
</ul>
<p><strong>로그 해상도</strong> : log에 캡쳐된 이벤트 데이터의 양
다 저장하면 실용적이지 않으니까 → 특정 유형의 커밋 이벤트가 발생한 사실만 기록할 수 있다.</p>
<p>log level : 로그 엔트리를 기록하는 데 필요한 조건, 특히 에러와 디버깅에 관한 ==조건이다==</p>
<p>reference) Fundamentals of Data Engineering</p>
<hr>
<h1 id="현대-데이터-아키텍쳐">현대 데이터 아키텍쳐</h1>
<p><strong>요구사항</strong></p>
<ul>
<li>유연성과 확정성</li>
<li>클라우드로 확장</li>
<li>증가하는 데이터 처리 가능</li>
<li>핵심 비즈니스 경로<ul>
<li>청구</li>
</ul>
</li>
<li>분산 도메인 통합</li>
<li>데이터 거버넌스와 보안</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.13(Mon)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.13Mon</link>
            <guid>https://velog.io/@o_u--chan/2026.04.13Mon</guid>
            <pubDate>Mon, 13 Apr 2026 16:45:49 GMT</pubDate>
            <description><![CDATA[<h1 id="airflow">Airflow</h1>
<hr>
<h2 id="airflow-연산자">Airflow 연산자</h2>
<p><strong>EmptyOperator</strong> : 문제 해결을 위한 task나 아직 구현되지 않은 task를 표현하는 데 사용</p>
<p><strong>BashOperator</strong> : 지정된 Bash 명령어나 스크립트 실행</p>
<ul>
<li>워크플로우 맥락에서 의미가 있다면, Bash가 할 수 있는 거의 모든 동작을 수행할 수 있다.</li>
</ul>
<p>실제 액션 정의</p>
<h2 id="airflow-task">Airflow Task</h2>
<ul>
<li>operator를 instance화한 실제 실행 단위</li>
<li>Dag 내부에서 정의되고, task 간 연결 정립</li>
<li>한 task는 하나의 책임으로 명확하게 정의 내릴 것! </li>
<li>대용량 데이터를 보내는 것보다는 메타데이터만 보내고 실제 데이터는 s3와 같은 스토리지에서 사용</li>
<li>의존성 확립 중요 (Upstream, Downstream 설정, 방향을 명확하게 설정할 것!)</li>
</ul>
<p><code>t1 &gt;&gt; t2</code> : task1 진행 후, task2</p>
<ul>
<li>t1 : upstream</li>
<li>t2 : downstream</li>
</ul>
<p><code>t1 &gt;&gt; t2 &lt;&lt; t3</code> 
t1, t3가 끝나야 t2 진행</p>
<p>python operator에서 op_kwargs 키워드 인자 딕셔너리의 키와 함수의 이름은 항상 일치해야 한다.</p>
<pre><code class="language-python">def pull_file(URL, savepath):
    r = requests.get(URL)
    with open(savepath, &#39;wb&#39;) as f:
    f.write(r.content)
    # Use the print method for logging
    print(f&quot;File pulled from {URL} and saved to {savepath}&quot;)

from airflow.operators.python import PythonOperator

# Create the task
pull_file_task = PythonOperator(
    task_id=&#39;pull_file&#39;,
    # Add the callable
    python_callable=pull_file,
    # Define the arguments
    op_kwargs={&#39;URL&#39;:&#39;http://dataserver/sales.json&#39;, &#39;savepath&#39;:&#39;latestsales.json&#39;}
)</code></pre>
<p><code>python_callable</code>는 키워드 인자로 <code>pull_file()</code>을 인자로 함수에 괄호를 붙여서 보내면 바로 함수가 실행이 되면서 반환값이 전달되기 때문에 에러가 뜬다. 바로 실행되지 않고 함수만 전달되도록 <code>pull_file</code>만 전달한다.</p>
<h2 id="airflow-cron-기본-문법-5자리">Airflow cron 기본 문법 (5자리)</h2>
<p>Airflow cron은 기본적으로 <strong>유닉스 cron과 같은 5필드</strong>를 씁니다.</p>
<p><code>*    *    *    *    * 분   시   일   월   요일</code>
각 필드 의미:</p>
<ul>
<li>분(minute): <code>0-59</code></li>
<li>시(hour): <code>0-23</code></li>
<li>일(day of month): <code>1-31</code></li>
<li>월(month): <code>1-12</code> 또는 <code>JAN-DEC</code></li>
<li>요일(day of week): <code>0-6</code> 또는 <code>SUN-SAT</code> (0/7 = 일요일)</li>
</ul>
<p>자주 쓰는 패턴:</p>
<ul>
<li><code>*</code> : 가능한 모든 값 (매 분, 매 시 등)</li>
<li><code>,</code> : 여러 값 지정 (예: <code>1,2,5</code>)</li>
<li><code>-</code> : 범위 (예: <code>1-5</code> = 월~금 요일)</li>
<li><code>*/n</code>: n 간격 (예: <code>*/5</code> = 5분마다)</li>
</ul>
<h2 id="자주-쓰는-airflow-cron-예시">자주 쓰는 Airflow cron 예시</h2>
<p>Airflow DAG 정의에서:</p>
<pre><code class="language-python">from airflow import DAG
from datetime import datetime

with DAG(
    dag_id=&quot;example_cron&quot;,
    start_date=datetime(2025, 1, 1),
    schedule_interval=&quot;0 0 * * *&quot;,  # 매일 0시
    catchup=False,
) as dag:
    ...</code></pre>
<p>매일 0시 :  <code>0 0 * * *</code>
매시간 정각 : <code>0 * * * *</code>
매 5분 : <code>*/5 * * * *</code></p>
<h3 id="preset-문자열도-지원">preset 문자열도 지원</h3>
<ul>
<li><code>None</code> : 스케줄 없이 수동 / 외부 트리거로만 실행</li>
<li><code>@once</code>  : 한 번만 실행 </li>
<li><code>@hourly</code> : 매시간 정각</li>
<li>`@daily : 매일 0시</li>
<li>`@weekly : 매주 일요일 0시</li>
<li><code>@monthly</code> : 매달 1일 0시</li>
<li><code>@yearly</code> : 매년 1월 1일 0시 <code>0 0 1 1 *</code></li>
</ul>
<hr>
<h2 id="센서-sensor">센서 (Sensor)</h2>
<p>특정 조건이 참이 될 때까지 계속 체크(polling)만 하는 특수한 operator</p>
<ul>
<li>조건이 만족되면 <code>success</code>로 끝나고 그 뒤의 downstream task들이 실행</li>
<li>EX<ul>
<li>FileSensor : S3/FTP/local에 특정 파일이 생겼는지</li>
<li>SqlSensor : table에 레코드가 생겼는지</li>
<li>ExternalTaskSensor : 다른 DAG/Task가 완려되었는지</li>
</ul>
</li>
</ul>
<h3 id="sensor-동작-방식-poke-vs-reschedule">Sensor 동작 방식 (poke vs reschedule)</h3>
<p>공통 주요 파라미터</p>
<ul>
<li><code>poke_interval</code> : 몇 초마다 조건을 체크할지(default = 60s)</li>
<li><code>timeout</code> : 최대 대기 시간(지나면 실패, 단위 : seconds)</li>
<li><code>mode</code> : <code>&quot;poke&quot;</code> 또는 <code>&quot;reschedule&quot;</code></li>
<li><code>soft_fail</code> : 실패 시 <code>FAILED</code> 대신 <code>SKIPPED</code>로 처리할지 결정</li>
</ul>
<p>mode 차이</p>
<ul>
<li><code>poke</code> 모드(default)<ul>
<li>sensor task가 돌아가는 동안 계속 워커 슬롯 점유</li>
<li>지연 시간(조건 충족 후 반응하는데까지 걸리는 시간)이 짧은 대신, 워커 자원을 많이 사용</li>
</ul>
</li>
<li>reschedule모드<ul>
<li>체크할 때만 잠깐 점유하고 확인하고, 나머지는 자원 반납</li>
<li>워커 자원 효율 좋지만, <code>poke_interval</code> 단위로 약간의 지연 존재</li>
</ul>
</li>
</ul>
<h4 id="example-code">Example Code</h4>
<pre><code class="language-python">from airflow import DAG
from datetime import datetime
from airflow.provides.common.sql.sensors.sql import SqlSensor
from airflow.operators.python import PythonOperator

def process_data():
    pritn(&quot;데이터 처리 시작&quot;)

with DAG(
    dag_id=&#39;example_sql_sensor&#39;,
    start_date=datetime(2025, 1, 1),
    schedule_interval=&quot;@daily&quot;,
    catchup=False,
) as dag:

    wait_for_partition = SqlSensor(
        task_id=&quot;wait_for_partition&quot;,
        conn_id=&quot;postgres_default&quot;,
        sql=&quot;&quot;&quot;
            SELECT 1
            FROM partitions
            WHERE dt = {{ ds }}
        &quot;&quot;&quot;,
        poke_interval=60,       # 60s마다 체크
        timeout=60 * 60 * 3,    # 최대 3시간 기다리기
        mode=&quot;reschedule&quot;,      # 워카 자원 아끼기
    )

    run_processing = PythonOperator(
        task_id=&quot;run_processing&quot;,
        python_callable=process_data,
    )

    wait_for_partition &gt;&gt; run_processing</code></pre>
<p>→ 이 DAG는 매일 1회 실행되면서, 해당 날짜의 partition이 DB에 생길 때까지 seonsor가 기다리고, 이후에 처리 Task를 실행하게 된다. </p>
<h2 id="airflow-debug--troubleshooting">Airflow Debug &amp; TroubleShooting</h2>
<ol>
<li>DAG 레벨<ul>
<li>DAG가 UI에 안 보인다? → DAG parsing/import error</li>
<li><code>airflow dags list-import-errors</code>로 확인 가능</li>
</ul>
</li>
<li>Task 레벨<ul>
<li>task 상태와 log 확인</li>
</ul>
</li>
<li>scheduler/worker 레벨<ul>
<li>스케줄러가 task를 잡아 주는지, 워커가 실제로 실행되는지 확인</li>
</ul>
</li>
<li>외부 시스템(파일, DB, API) 레벨<ul>
<li>sensor, hook, operator가 의존하는 S3, DB, API 쪽 문제</li>
</ul>
</li>
</ol>
<p>이 순서대로 좁히면서 버그 위치를 찾는 게 Debug 핵심!</p>
<p>Debugging Tool &amp; Pattern</p>
<ol>
<li>Airflow UI</li>
<li>CLI로 개별 Task 테스트<ul>
<li><code>airflow tasks test &lt;dag_id&gt; &lt;task_id&gt; &lt;execution_date&gt;</code></li>
<li>메타데이터 DB 상태와 상관 없이 로컬에서 그 task만 실행해 보는 용도</li>
</ul>
</li>
<li><code>dag.test()</code> <ul>
<li>dag  파일 맨 아래 다음 추가 후, IDE/로컬에서 실행<pre><code class="language-python">if __name__ == &quot;__main__&quot;:
    dag_test()</code></pre>
<ul>
<li>전체 dag를 하나의 프로세스에서 순서대로 실행해서, 어디에서 에러가 나는지 확인</li>
</ul>
</li>
</ul>
</li>
<li>DebugExecuter / 로컬 개발 환경</li>
</ol>
<ul>
<li><code>AIRFLOW__CORE__EXECUTOR=DebugExecutor</code>로 두고, SQLite+ 단일 프로세스로 디버깅용 실행</li>
</ul>
<h3 id="slaservice-level-agreement">SLA(Service Level Agreement)</h3>
<ul>
<li>task 또는 DAG가 실행에 걸려야 하는 예상 시간</li>
<li>SLA Miss : task, DAG가 예상 시간 내 완료되지 못한 경우</li>
</ul>
<p><strong>SLA 정의하는 방법</strong></p>
<ol>
<li><p>task에서 <code>sla</code> 인자 사용</p>
<pre><code class="language-python">task1 = BashOperator(
         task_id=&#39;sla_task&#39;,
         bash_command=&#39;runcode.sh&#39;,
         sla=timedelta(seconds=30),
         dag=dag)</code></pre>
</li>
<li><p><code>default_args</code> 딕셔너리에 설정</p>
<pre><code class="language-python">default_args={
 &#39;sla&#39;: timedelta(minutes=20),
 &#39;start_date&#39;: datetime(2023, 2, 20)
}
dag = DAG(&#39;sla_dag&#39;, default_args=default_args)</code></pre>
</li>
</ol>
<h3 id="template이-적용된-bashoperator">Template이 적용된 BashOperator</h3>
<h4 id="jinja-template">Jinja Template</h4>
<blockquote>
<p> Airflow는 내부적으로 Jinja2라는 파이썬 template engine을 사용한다. 이를 통해 Bash 명령어 안에 <code>{{}}</code>형태의 중괄호를 사용하면, Airflow가 이를 실제 값으로 치환해준다.</p>
</blockquote>
<p><strong>자주 사용하는 템플릿 변수</strong></p>
<p>BashOperator의 <code>bash_command</code> 내에서 가장 많이 쓰이는 변수들입니다.</p>
<table>
<thead>
<tr>
<th><strong>변수명</strong></th>
<th><strong>설명</strong></th>
<th><strong>예시 출력</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>{{ ds }}</code></strong></td>
<td>execution_date의 날짜 (YYYY-MM-DD) (datestamp의 약자)</td>
<td><code>2026-04-13</code></td>
</tr>
<tr>
<td><strong><code>{{ ds_nodash }}</code></strong></td>
<td>하이픈이 없는 날짜</td>
<td><code>20260413</code></td>
</tr>
<tr>
<td><strong><code>{{ run_id }}</code></strong></td>
<td>현재 DAG Run의 고유 ID</td>
<td><code>scheduled__2026-04-13...</code></td>
</tr>
<tr>
<td><strong><code>{{ task_instance.task_id }}</code></strong></td>
<td>현재 실행 중인 태스크 이름</td>
<td><code>generate_report</code></td>
</tr>
<tr>
<td><strong><code>{{ params.my_param }}</code></strong></td>
<td>사용자가 직접 정의한 파라미터</td>
<td>(사용자 지정값)</td>
</tr>
<tr>
<td><strong><code>{{ prev_ds }}</code></strong></td>
<td>이전 DAG 실행 날짜</td>
<td></td>
</tr>
<tr>
<td><strong>`Airflow config object: {{conf}}</strong>`</td>
<td>conf 객체를 사용해 코드 안에서 현재 Airflow 설정에 접근 가능</td>
<td></td>
</tr>
<tr>
<td>- <code>.sh</code> 파일 실행 시 끝에 공백 추가하여 템플릿 엔진 오작동 방지</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 파일 경로 찾지 못하면 에러 발생하기 때문에</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- <code>{{ ds }}</code>, <code>{{ ds_nodash }}</code> → 파이썬 datetime 객체가 아니라 <strong>문자열</strong>이다.</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- ds는 datastamp의 약자로, 해당 task가 실행되어야 하는 논리적 시점의 날짜를 의미한다.</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h4 id="macros-변수">macros 변수</h4>
<p>Airflow template에서 유용한 객체나 메서드에 대한 reference를 제공</p>
<ul>
<li><code>macros.datetime</code> ← 파이썬의 <code>datetime.datetime</code> 객체</li>
<li><code>macros.timedelta</code> ← <code>timedelta</code> 객체 참조</li>
<li><code>macros.uuid</code> ←  python의 uuid 객체와 동일</li>
<li><code>macros.ds_add</code> 와 같은 추가 함수도 존재 ← 템플릿 안에서 날짜 계산 간단히 할 수 있도록 도와준다<ul>
<li><code>{{ macros.ds_add(&#39;2020-05-15&#39;, 5) }}</code> : 날짜에 일 수 더하기</li>
</ul>
</li>
</ul>
<blockquote>
<p>python의 <code>uuid</code> 객체란?
Universally Unique Identifier(범용 고유 식별자)의 약자로, 네트워크 상에서 서로 다른 시스템들이 독립적으로 식별자를 생성하더라도 중복될 확률이 거의 없도록 설계된 128비트 길이의 숫자이다. </p>
<p>DB의 기본키, 세션 ID, file name 등 절대 중복되면 안 되는 <strong>고유값</strong>이 필요할 때 사용한다!</p>
</blockquote>
<table>
<thead>
<tr>
<th><strong>버전</strong></th>
<th><strong>생성 방식</strong></th>
<th><strong>특징</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>UUID1</strong></td>
<td>호스트 ID(MAC 주소) + 현재 시간</td>
<td>생성 시간과 위치를 알 수 있지만, 개인정보(MAC) 노출 위험이 있음.</td>
</tr>
<tr>
<td><strong>UUID3</strong></td>
<td>네임스페이스 + 이름 (MD5 해시)</td>
<td>동일한 입력값에 대해 항상 동일한 UUID를 생성함.</td>
</tr>
<tr>
<td><strong>UUID4</strong></td>
<td><strong>완전 무작위(Random)</strong></td>
<td><strong>가장 많이 사용됨.</strong> 중복 가능성이 극히 낮아 일반적인 고유값 생성에 최적.</td>
</tr>
<tr>
<td><strong>UUID5</strong></td>
<td>네임스페이스 + 이름 (SHA-1 해시)</td>
<td>UUID3과 같지만 보안성이 더 높은 해시 알고리즘 사용.</td>
</tr>
<tr>
<td>- <code>my_uuid = uuid.uuid4()</code></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h3 id="고급-template">고급 template</h3>
<pre><code class="language-python">templated_command=&quot;&quot;&quot;
{% for filename in params.filenames %}
    echo &quot;Reading {{ filename }}&quot;
{% endfor %}
&quot;&quot;&quot;</code></pre>
<p><strong>Jinja 구문에서 for 루프의 끝을 나타내려면</strong> → <code>{% endfor %}</code></p>
<h2 id="branch분기">Branch(분기)</h2>
<p>브랜칭 : 조건부 로직 가능토록 한다.
<code>BranchPythonOperator</code> 사용</p>
<ul>
<li><code>from airflow.operators.python import BranchPythonOperator</code>
다음에 실행할 task id, (id 목록)을 반호나하는 <code>python_callable</code>을 받는다.</li>
</ul>
<blockquote>
<p><code>python_callable</code>에서 중괄호를 안 쓰는 이유
    <code>BranchPythonOperator</code>의 <code>python_callable</code> 인자는 문자열이 아니라 <strong>파이썬 함수 객체 그 자체</strong>를 전달받습니다.
    -  <strong>동작 방식</strong>: 이 인자에는 함수의 &quot;이름&quot;을 넘겨줍니다. Airflow는 이 함수를 나중에 직접 호출(Call)합니다.
    -  <strong>중괄호를 쓰지 않는 이유</strong>:
        - <strong>함수는 문자열이 아님</strong>: <code>{{ }}</code>는 문자열 내부의 텍스트를 바꿀 때 쓰는 문법입니다. 함수 객체 자체에는 적용되지 않습니다.
        - <strong>런타임 실행</strong>: 함수 내부에서 날짜 같은 정보가 필요하다면, Airflow는 함수를 호출할 때 <code>context</code>라는 딕셔너리에 모든 정보를 담아 보내줍니다. 함수 안에서 직접 꺼내 쓰면 되기 때문에 굳이 중괄호로 치환할 필요가 없습니다.</p>
</blockquote>
<blockquote>
<p><code>provide_context</code>의 주요 역할
Airflow는 함수를 호출할 때 Context라는 거대한 딕셔너리를 인자로 전달한다. </p>
<ul>
<li>날짜 정보 : <code>ds</code>, <code>logical_date</code>, <code>execution_date</code></li>
<li>객체 정보 : <code>dag</code>, <code>task</code></li>
<li>task 간 통신 : <code>ti</code>, <code>task_instance</code>(Xcom을 사용해 다른 task의 데이터를 가져올 때 필수)</li>
</ul>
</blockquote>
<pre><code class="language-python">def check_weekend(**kwargs):
    dt = datetime.strptime(kwargs[&#39;execution_date&#39;],&quot;%Y-%m-%d&quot;)
    # If dt.weekday() is 0-4, it&#39;s Monday - Friday. If 5 or 6, it&#39;s Sat / Sun.
    if (dt.weekday() &lt; 5):
        return &#39;email_report_task&#39;
    else:
        return &#39;no_email_task&#39;

branch_task = BranchPythonOperator(task_id=&#39;check_if_weekend&#39;,
    python_callable=check_weekend,
    provide_context=True,
    dag=dag)</code></pre>
<h2 id="production-pipeline-구축하기">production pipeline 구축하기</h2>
<p>DAG 및 task 실행</p>
<ul>
<li>command line에서 특정 task 실행 :
<code>airflow tasks test &lt;dag_id&gt; &lt;task_id&gt; &lt;date&gt;</code></li>
<li>전체 DAG 실행
<code>airflow dags trigger -e &lt;date&gt; &lt;dag_id&gt;</code> → 지정한 날짜에 전체 DAG가 실행하는 것처럼 동작한다.</li>
</ul>
<p>Operator 요약</p>
<ul>
<li>Bashoperator → <code>bash_command</code> 필요</li>
<li>PythonOperator → <code>python_callable</code> 필요</li>
<li><code>BranchPythonOperator</code> → <code>python_callable</code>, <code>provide_context=True</code> 필요, 호출 함수는 <code>**kwargs</code>를 받아야 함.</li>
<li>FileSensor → <code>filepath</code> 인자 필요</li>
</ul>
<hr>
<h1 id="building-a-retail-data-pipelineproject">Building a Retail Data Pipeline(Project)</h1>
<h2 id="🛒-식료품-매출-데이터-처리-요구-사항">🛒 식료품 매출 데이터 처리 요구 사항</h2>
<h3 id="1-transform-함수-구현">1. transform() 함수 구현</h3>
<ul>
<li><p><strong>입력:</strong> <code>merged_df</code> (데이터프레임)</p>
</li>
<li><p><strong>수행 작업:</strong></p>
<ul>
<li>수치형 데이터의 결측치(Missing values)를 원하는 방식(예: 0 또는 평균값 등)으로 채웁니다. O</li>
<li><code>Month</code>(월) 컬럼을 새로 추가합니다. O</li>
<li>주간 매출(<code>Weekly_Sales</code>)이 <strong>$10,000를 초과</strong>하는 행만 유지합니다. O</li>
<li>분석에 불필요한 컬럼들을 삭제합니다.<ul>
<li><code>&quot;Store_ID&quot;</code></li>
<li><code>&quot;Month&quot;</code></li>
<li><code>&quot;Dept&quot;</code></li>
<li><code>&quot;IsHoliday&quot;</code></li>
<li><code>&quot;Weekly_Sales&quot;</code></li>
<li><code>&quot;CPI&quot;</code></li>
<li>&quot;<code>&quot;Unemployment&quot;</code>&quot;</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>출력:</strong> 최종 데이터프레임을 반환하며, 결과는 <code>clean_data</code>라는 변수에 저장되어야 합니다.</p>
</li>
</ul>
<h3 id="2-avg_weekly_sales_per_month-함수-구현">2. avg_weekly_sales_per_month() 함수 구현</h3>
<ul>
<li><p><strong>입력:</strong> <code>clean_data</code> (위에서 정제된 데이터프레임)</p>
</li>
<li><p><strong>수행 작업:</strong></p>
<ul>
<li><p>월별 평균 매출을 계산합니다.</p>
</li>
<li><p>분석에 필요한 <code>Month</code>와 <code>Weekly_Sales</code> 컬럼만 선택합니다.</p>
</li>
<li><p><strong>메서드 체이닝(Chain operation)</strong>을 사용하여 다음 함수들을 순서대로 적용해야 합니다:</p>
<ol>
<li><code>groupby()</code>: &quot;Month&quot; 컬럼을 기준으로 그룹화</li>
<li><code>agg()</code>: 평균 매출 계산</li>
<li><code>reset_index()</code>: 인덱스를 새로 재설정</li>
<li><code>round()</code>: 결과를 <strong>소수점 둘째 자리</strong>까지 반올림</li>
</ol>
</li>
</ul>
</li>
</ul>
<h3 id="3-load-함수-구현">3. load() 함수 구현</h3>
<ul>
<li><p><strong>입력:</strong> 정제된 데이터프레임(<code>clean_data</code>), 집계된 데이터프레임(<code>agg_data</code>), 그리고 각각의 저장 경로</p>
</li>
<li><p><strong>수행 작업:</strong></p>
<ul>
<li><p>두 데이터프레임을 각각 <code>clean_data.csv</code>와 <code>agg_data.csv</code> 파일로 저장합니다.</p>
</li>
<li><p>저장 시 <strong>인덱스(index)는 포함하지 않습니다.</strong></p>
</li>
</ul>
</li>
</ul>
<h3 id="4-validation-함수-구현">4. validation() 함수 구현</h3>
<ul>
<li><p><strong>수행 작업:</strong></p>
<ul>
<li><code>load()</code> 함수를 통해 생성된 두 개의 CSV 파일이 현재 작업 디렉토리에 실제로 존재하는지 확인합니다.</li>
</ul>
</li>
</ul>
<hr>
<p><strong>참고 사항:</strong></p>
<ul>
<li><p>데이터베이스 연결을 위한 별도의 엔진 설정은 필요하지 않습니다.</p>
</li>
<li><p>제공된 SQL 코드 셀에 쿼리를 실행하면 결과가 자동으로 <code>grocery_sales</code>라는 이름의 Pandas 데이터프레임으로 저장되며, 이를 바로 Python 코드에서 사용할 수 있습니다.</p>
</li>
</ul>
<pre><code class="language-python">def avg_weekly_sales_per_month(clean_data):
    df = clean_data.groupby(by=&quot;Month&quot;).agg(&#39;mean&#39;).reset_index().round(2)
    return df</code></pre>
<p>error → agg(mean)이라고 작성했는데 <code>agg(&#39;mean&#39;)</code>이라고 작성해야 한다</p>
<blockquote>
<p><code>agg(mean)</code> → 파이썬은 <code>mean</code>이라는 이름을 가진 변수나 객체를 찾으려고 하는데, 이전에 정의해두지 않았으면 이를 찾지 못하고 <code>NameError</code>가 발생시킨다. </p>
</blockquote>
<p> <code>os.path.exists</code> → csv 파일 validation 확인</p>
<p>정규표현식 <code>r&#39;(\d+\.?\d*)&#39;</code></p>
<ul>
<li><code>\d</code> : 숫자</li>
<li><code>+</code> : 하나 이상 반복됨</li>
<li><code>[0-9]</code> : <code>\d</code>와 같은 의미</li>
<li><code>\.?</code> : 마침표 + 0개 또는 1개 → 소수점이 있을수도 있고 없을 수도 있고(<code>?</code>)</li>
<li><code>\d*</code> : 숫자 + 0개 이상 → 소수점 뒤에 숫자가 붙을 수도 있고 없을 수도 있다</li>
</ul>
<p>정규표현식에서 특정 문자열 뒤의 숫자 찾으려면?
→ <strong>캡쳐 그룹</strong> 사용 : <code>특정문자열(\d+\.?\d*)</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.12(Sun)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.12Sun</link>
            <guid>https://velog.io/@o_u--chan/2026.04.12Sun</guid>
            <pubDate>Mon, 13 Apr 2026 16:45:14 GMT</pubDate>
            <description><![CDATA[<h2 id="apache-airflow-입문">Apache Airflow 입문</h2>
<p>DAG(Directed Acyclic Graph) : 방향 비순환 그래피</p>
<ul>
<li>Airflow에서 워크플로를 구성하는 작업 집합</li>
<li>작업과 작업 간 의존성으로 구성</li>
<li>메타데이터와 함께 생성<pre><code class="language-python">etl_dag = DAG(
  dag_id=&#39;etl_pipeline&#39;,
  default_args={&quot;start_date&quot;:&quot;2023-11-15&quot;}
)</code></pre>
</li>
</ul>
<p><code>airflow tasks test &lt;dag_id&gt; &lt;task_id&gt; [execution_date]</code></p>
<p>DAG 는 순환하지 않는다. 한 번의 실행에서 각 1회만 실행된다.
Airflow DAG는 오퍼레이터, 센서 등 실행할 구성 요소들로 이루어져 있고, 보통 이를 task라고 한다.</p>
<ol>
<li><code>from airflow import DAG</code></li>
<li>DAG 구성 요소에 적용할 속성들을 담은 기본 인자 딕셔너리 생성 - Airflow의 런타임 동작을 세미할게 제어</li>
<li>Python 컨텍스트 매니저를 사용해 DAG 객체를 정의한다.</li>
</ol>
<p><strong>command line VS python</strong></p>
<p>command line</p>
<ul>
<li>Airflow process start</li>
<li>DAG/Task 수동 실행</li>
<li>Airflow 로그 정보 확인</li>
</ul>
<p>Python</p>
<ul>
<li>DAG 생성</li>
<li>DAG 속성 개별 편집</li>
</ul>
<p>datetime 함수 인자로 연도, 월, 일을 각각 정수형 인자로 받아야 한다.</p>
<p>dag list 확인 : <code>airflow dags list</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.4.10(Fri)]]></title>
            <link>https://velog.io/@o_u--chan/2026.4.10Fri</link>
            <guid>https://velog.io/@o_u--chan/2026.4.10Fri</guid>
            <pubDate>Fri, 10 Apr 2026 16:02:34 GMT</pubDate>
            <description><![CDATA[<h2 id="streamlined-data-ingestion-with-pandas">Streamlined Data Ingestion with pandas</h2>
<p><code>usecols</code> 키워드 인자 : import할 모든 열의 이름 리스트나 열 번호 리스트를 전달</p>
<ul>
<li>함수를 전달해 열 선택 가능</li>
</ul>
<p><code>nrows</code> 인자 : 임포트되는 행의 수 선택</p>
<ul>
<li>파일을 chunks 단위로 처리하기 위해 <code>skiprows</code> 인자와 결합할 때 유용하다</li>
</ul>
<p><code>skiprows</code> : 건너뛸 행 번호 list, 행 건너뛸지 결정하는 함수, 건너뛸 행의 개수를 인자로 받는다</p>
<p><strong>import된 첫 번째 행을 자동으로 header(column_name)으로 만든다.</strong>
→ 컬럼 이름이 포함된 행을 건너뛴다면 <code>header=None</code>으로 지정해야만 한다.</p>
<p>열 이름이 없을 때, 이름을 할당하려면 <code>read_csv</code>의 다른 인자인 <code>names</code>를 사용한다.
<code>names</code> 인자 : 사용할 <strong>열 이름 리스트</strong>를 인자로 받는다. </p>
<ul>
<li>데이터의 모든 열에 대한 이름이 포함되어야 한다. </li>
<li>일부 이름을 바꾸고 싶으면 import 작업이 끝난 후 진행</li>
</ul>
<p>열 개수가 적은 데이터셋의 경우, Data Dictionary를 참고해 직접 리스트를 만들 수도 있다.</p>
<h3 id="오류-및-결측치-처리">오류 및 결측치 처리</h3>
<ul>
<li><p>데이터 타입 지정 실수 (zipcode를 문자열이 아니라 정수로 pandas가 판단하는 문제)
<code>read_csv</code>의 키워드 인자인 <code>dtype</code>으로 열의 데이터 타입을 지정할 수 있다. - dictionary 형태</p>
</li>
<li><p>결측치 + 결측치라 판단해야 하는 값 (zipcode = 0인 경우)
<code>na_values</code> 키워드 인자 → 단일 값, 값들의 리스트, 컬럼-결측치 값으로 구성된 딕셔너리를 전달할 수 있다. </p>
</li>
</ul>
<p>오류가 있는 행</p>
<ul>
<li><code>on_bad_lines</code> </li>
<li><code>error_bad_lines</code> : 실행 X + 에러문</li>
<li><code>warn_bad_lines</code> : 실행 O + 경고</li>
</ul>
<p><code>pd.concat</code> 함수는 여러 개의 pandas 객체(DataFrame이나 Series 등)를 하나로 합칠 때 사용하는 함수입니다. 이 함수의 첫 번째 인자는 반드시 DataFrame들의 리스트(또는 다른 iterable)여야 합니다.</p>
<p>boolean 값 설정
<code>true_values=[&#39;Yes&#39;], false_values=[&#39;No&#39;]</code> 인자 
→ NA가 True로 변환되는 문제는 여전하다. </p>
<p>+) <code>dtype=bool</code>보다는 <code>dtype=boolean</code>을 사용하면 True, False, NA를 모두 유지할 수 있어 데이터 품질 관리에 유리하다. </p>
<p>JSON</p>
<ul>
<li>record orientation : 레코드 방식의 JSON은 딕셔너리들의 리스트로 구성된다. 각 dictionary는 테이블의 한 행이 된다.</li>
<li>column orientation : key-column name, value - 해당 컬럼의 데이터 리스트나 행 인덱스가 포함된 딕셔너리 </li>
</ul>
<p>실무에서는 Record 방식으로 데이터가 들어오지만 복잡하게 중첩된 경우에는 <code>json_normalize</code>라는 tool을 사용한다.</p>
<p><code>requests.get()</code>의 결과값은 데이터와 메타데이터를 포함하는 response 객체이다.
여기서 실제 데이터만 뽑아내려면 <code>.json()</code> 메서드를 사용해야 하는데, 중요한 점은 <code>.json()</code> 메서드가 dictionary를 반환한다는 것이다. <code>pd.read_json()</code>은 문자열을 기대하기 때문에 dictionary를 직접 분석할 수 없다. → <code>pd.DataFrmae()</code>을 사용한다. </p>
<p><code>pd.json_normalize</code> : dictinoary나 dictionary의 list를 받아 데이터프레임으로 반환한다.
중첩된 속성의 열 이름은 <strong>속성.하위속성</strong>을 따르지만 <code>.</code> 구분자는 판다스의 열 선택 문법과 충돌할 수도 있기 때문에, <code>sep</code>인자를 사용해 다른 구분자로 지정하는 것이 좋다.</p>
<p><code>df = json_normalize(data[&#39;job&#39;], sep=&#39;_&#39;)</code></p>
<pre><code class="language-json">{
    &quot;store_name&quot;: &quot;Cafe Gemini&quot;,
    &quot;location&quot;: &quot;Seoul&quot;,
    &quot;reviews&quot;: [
        {&quot;user&quot;: &quot;Alice&quot;, &quot;score&quot;: 5},
        {&quot;user&quot;: &quot;Bob&quot;, &quot;score&quot;: 4}
    ]
}</code></pre>
<p><code>record_path</code> : reviews 컬럼을 2개의 행으로 쪼개준다.
없으면 reviews 컬럼 하나에 리스트가 통째로 들어간다.</p>
<p><code>record_path</code>는 보통 <code>meta</code> 인자와 함께 사용한다. 
reviews의 리스트를 펼치게 되면 <code>{&quot;user&quot;: &quot;Alice&quot;, &quot;score&quot;: 5}</code>에 해당하는 상위 수준의 정보(가게이름, 위치)가 사라지기 때문에 <code>meta</code>를 이용해 그 정보를 다시 붙여줘야 한다. </p>
<p>다른 상위 정보의 하위 정보를 붙이려고 할 때는 리스트를 활용한다.</p>
<pre><code class="language-python">flat_cafes = json_normalize(data[&quot;businesses&quot;],
    sep=&quot;_&quot;,
    record_path=&quot;categories&quot;,
        meta=[&#39;name&#39;,
        &#39;alias&#39;,
        &#39;rating&#39;,
        [&#39;coordinates&#39;, &#39;latitude&#39;],
        [&#39;coordinates&#39;, &#39;longitude&#39;]],
        meta_prefix=&quot;biz_&quot;)</code></pre>
<p><code>[&#39;coordinates&#39;, &#39;latitude&#39;]</code> : coordinates 안에 있는 latitude의 정보 </p>
<p><code>cafes = pd.concat([top_50_cafes, next_50_cafes], ignore_index=True)</code></p>
<ul>
<li><code>pd.concat</code>은 DataFrame의 리스트를 인자로 전달해야 한다.</li>
</ul>
<hr>
<h2 id="git">#Git</h2>
<p><code>git branch new_name</code> : 브랜치 생성
<code>git switch -c new_branch</code> : branch 생성 후 해당 branch로 이동![[스크린샷 2026-04-10 오후 4.48.40.png]]</p>
<p><code>git branch -m old_name new_name</code> : branch 이름 변경
<code>git branch -d delete_branch_name</code> : branch 삭제
병합되지 않은 채면 오류가 뜨는데, <code>-D</code> 플래그 ㅅ용하면 삭제 가능하다. </p>
<h4 id="병합">병합</h4>
<p><code>source</code> : 병합의 원본 브랜치
<code>destination</code> : 병합의 대상 브랜치</p>
<p><code>piano</code>를 <code>main</code>에 병합할 때:</p>
<ul>
<li>piano - source</li>
<li>main -destination</li>
</ul>
<p>destination 브랜치에 이동한 후에 <code>git merge 병합시킬_브랜치</code>
자기가 사용 중인 브랜치를 다른 브랜치에 병합시키려면 <code>git merge using_branch destination</code></p>
<p>게시판 기능을 만들기 위해 borad 브랜치를 만들고 병합했는데 에러가 생겼어
그러면 board 브랜치에서 고치는 게 아니라 board는 삭제하고 board_fix 브랜치를 만들고 작업 후에 다시 병합한다.</p>
<h4 id="병합-충돌">병합 충돌</h4>
<p>서로 다른 두 파일을 병합하면 git이 어느 걸 병합해야 할 지 알 수 없다. → 충돌!</p>
<p>원격의 파일을 로컬 저장소와 동기화하려면 원격에서 브랜치를 가져와야 한다.
이를 위해서 원격 이름을 지정해 <code>git fetch</code>를 사용한다.<br><code>git fetch origin</code> </p>
<ul>
<li>원격의 모든 브랜치가 로컬 저장소에 내려온다.</li>
<li>브랜치가 원격에만 존재했다면, 로컬에도 생성된다.</li>
<li>원격의 내용을 로컬에 병합하지는 않음에 주의해라.</li>
</ul>
<p><code>git fetch origin main</code> 
origin 원격 저장소 중 특정 브랜치만 가지고 오고 싶을 때는 브랜치도 같이 선언해라.</p>
<p>브랜치를 가지고 왔으면 저장소 간 내용을 동기화해야 한다. 
<code>git merge origin</code> </p>
<ul>
<li>local branch를 명시하지 않으면 현재 위치한 브랜치에 병합된다.
<code>git pull origin</code> </li>
</ul>
<p><code>git pull origin dev</code> : origin의 dev 브랜치에 pull</p>
<p>원격에서 pull 하기 전에 로컬 작업을 저장해둬야 한다.</p>
<h4 id="git-push">git push</h4>
<p>원격에 push 하려면 </p>
<ul>
<li>먼저 로컬에 변경 사항 저장</li>
<li><code>git push remote local_branch</code> : local_branch에서 remote로 push</li>
<li><code>git push origin main</code> : 로컬 main에서 origin으로 push</li>
</ul>
<p>원격에 없는 로컬 브랜치가 있다면?
<code>git push origin local_branch_name</code></p>
<hr>
<h2 id="python으로-배우는-software-engineering-principle">Python으로 배우는 software engineering principle</h2>
<p>모듈성, 문서화, 테스트</p>
<p><strong>PyPi</strong> : python package index </p>
<p>package 이름은 소문자로 설정</p>
<p><code>__init__.py</code> 가 있어야 python이 우리가 만든 패키지를 인식한다.</p>
<p>utils.py - 서브 모듈로 불리며
<code>pakcage_name.sub_module_name.function_name</code> 형식으로 호출할 수 있다.</p>
<p>또 다른 방법으로는,</p>
<pre><code class="language-python"># my_package/__init__.py
form .utils import we_need_to_talk</code></pre>
<p>을 설정해두면, 다른 work directory에서 작업할 때</p>
<pre><code class="language-python">import my_package</code></pre>
<p>만 해도 import 할 수 있다. init 파일에서 import를 대신 해주기 때문.
핵심 기능을 <code>__init__.py</code>에서 임포트하여 바로 접근할 수 있도록 해주고 비주류 함수는 서브모듈 점 표기법으로 접근토록 한다. </p>
<p>package 내부 package도 설정할 수 있는데, 방법은 똑같다. </p>
<p><strong>python 패키지 공유</strong></p>
<ul>
<li>setup.py : package를 어떻게 설치할 지</li>
<li>requirements.txt : 필요한 환경 재현하는 방법
<code>pip install -r requirements.txt</code> </li>
</ul>
<p><code>setup.py</code> 설정되면 터미널에서 같은 디렉토리 내 터미널에서 <code>pip install .</code> → 패키지 설치 된다.</p>
<p>DRY 원치
Don&#39;t Repeart Yourself : 반복할만한 코드 재작성하지 마라.
→ <strong>상속</strong></p>
<pre><code class="language-python">from .parent_class import ParentClass

class ChildClass(ParentClass):
    def __init__(self):
        ParentClass.__init__(self)
        # Add child&#39;s unique thiing</code></pre>
<blockquote>
<p>다중 상속 개념도 알아둘 것!</p>
</blockquote>
<p>주석은 코드가 무엇을 하는지가 아니래 왜 코드를 그렇게 작성헀는지 써라</p>
<p><strong>test</strong>
<code>doctest</code> &amp; <code>pytest</code> 
docstring → doctest</p>
<p>객체 2개를 비교할 때는 <code>==</code>로 하기보다는 속성을 비교하는 방식으로 검증한다. </p>
<p>test.py를 작성할 때는 소스코드 제목은 시작이나 끝이 test 이어야 하고,
함수 정의할 때는 test가 앞에 붙어야 하며, assert로 검증해줘야 한다. </p>
<p>Sphinx로 docstring을 문서화할 수 있다. </p>
<p><code>ivar</code> 키워드 : 인스턴스 변수</p>
<ul>
<li>변수라고 하지 않고 <code>ivar</code>라고 하는 이유 : 선언되는 위치에 따라 이름이 달라지기 때문에<pre><code class="language-python">def __init__(self, table_name):
  # 여기서 self.table_name이 ivar 변수다.
  self.table_name = table.name</code></pre>
</li>
</ul>
<p><strong>Travix CI</strong> 
새 코드를 추가할 때, Travis가 자동으로 테스트 실행하고, 변경으로 문제가 생기면 알려준다.
수정한 걸 push 하면 다시 테스트를 실행해 수정이 제대로 됐는지 확인해준다. 
빌드 예약도 가능 </p>
<p><strong>Codecov</strong> : 자동 테스트가 코드의 어떤 부분을 검증하고 있는지 살펴볼 수 있게 해주는 도구</p>
<ul>
<li>테스트 커버리지라고 한다. </li>
</ul>
<p><strong>Code Climate</strong> : 가독성 개선을 위한 코드 분석</p>
<hr>
<h2 id="performing-a-code-review">Performing a Code Review</h2>
<ul>
<li><p><strong>리팩토링 (DRY 원칙 적용):</strong> 시각화 함수 내에서 중복되는 라벨 생성 로직을 <code>column_to_label()</code> 호출로 대체합니다.</p>
</li>
<li><p><strong>유닛 테스트 수정:</strong> <code>prepare_smartphone_data()</code> 함수가 실제로 <code>NaN</code>을 어떻게 처리하는지(예: <code>dropna()</code>를 사용하는지 등) 확인하고, 테스트 코드의 <code>assert</code> 문이 그 로직을 정확히 검증하도록 수정합니다.</p>
</li>
<li><p><strong>최종 검증:</strong> <code>pytest</code>를 실행하여 결과가 <code>ExitCode.OK</code>(보통 0)를 반환하는지 확인합니다.</p>
</li>
</ul>
<blockquote>
<p>docstring에 example 추가할 지?</p>
</blockquote>
<hr>
<h2 id="python으로-etl과-elt">Python으로 ETL과 ELT</h2>
<p>변환 검증</p>
<ul>
<li><code>.nsmallest(10, [&#39;timestamps&#39;]</code> : 지정한 열 목록에서 가장 작은 값 10개</li>
<li><code>.nlargest(10, [&#39;timestamps&#39;]</code></li>
</ul>
<p><code>to_csv</code> 했을 때 저장 제대로 되었는지 확인하려면</p>
<pre><code class="language-python">import os
print(os.path.exists(&#39;example.csv&#39;))</code></pre>
<p>log : 파이프라인이 실행되는 동안 생성되어 기록되는 메세지</p>
<ul>
<li>실패 시 성능 기록</li>
<li>실패 시 원인 파악 출발점 제공</li>
</ul>
<p>logging - 6가지
<code>debug</code>, <code>info</code>, <code>warning</code>, <code>error</code></p>
<ul>
<li><code>debug</code> : 파이프라인 만드는 동안 사용 → 차원, 타입, 변수 값 제공</li>
<li><code>info</code> : 파이프라인 실행 전반에 걸쳐 기본 정보와 체크포인트 제공</li>
<li><code>warning</code> : 예기치 않은 일 - 이전에 보지 못한 데이터 타입) ex</li>
<li><code>erro</code> : 데이터 타입 변경, 데이터 제공 X</li>
</ul>
<p>json → dictionary → DataFrame</p>
<p><code>isinstance(pipeline, data_type)</code> : 객체와 해당 타입 일치하면 True</p>
<p><code>@pytest.fixture()</code> : test data와 객체를 여러 테스트에서 공유할 수 있게 해주는 함수</p>
<p><code>logging.basicConfig</code>는 파이썬의 표준 라이브러리인 <code>logging</code> 모듈에서 <strong>로그 시스템의 가장 기본적인 설정을 한 번에 마치는 함수</strong></p>
<ul>
<li><strong><code>level</code></strong>: 어떤 수준의 로그부터 출력할지 결정합니다. (<code>DEBUG &lt; INFO &lt; WARNING &lt; ERROR &lt; CRITICAL</code>)</li>
<li><strong><code>format</code></strong>: 로그가 출력될 모양을 지정합니다. (시간, 로그 레벨, 메시지 등)</li>
<li><strong><code>filename</code></strong>: 로그를 콘솔이 아닌 파일에 저장하고 싶을 때 파일 경로를 지정합니다.</li>
<li><strong><code>filemode</code></strong>: 파일을 열 때의 모드입니다. (<code>&#39;a&#39;</code>는 이어쓰기, <code>&#39;w&#39;</code>는 새로 쓰기)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.09(Thu)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.09Thu</link>
            <guid>https://velog.io/@o_u--chan/2026.04.09Thu</guid>
            <pubDate>Fri, 10 Apr 2026 16:01:51 GMT</pubDate>
            <description><![CDATA[<h1 id="python에서-날짜와-시간-다루기">Python에서 날짜와 시간 다루기</h1>
<p>date() →연, 월, 일 
<code>date(2000, 10, 16)</code>, <code>date(2000, 8, 24)</code></p>
<pre><code class="language-python">from datetime import timedelta
# timedelta : 사건 사이에 경과한 시간
td = timedelta(days=29)
print(d1 + td)</code></pre>
<p><code>sort_values()</code> : Dataframe이나 series에서 사용하는 메서드</p>
<p>list 타입을 정렬하려면 내장 함수 <code>sorted()</code>나 <code>sort()</code>를 사용해야 한다. </p>
<ul>
<li><code>sorted()</code> : 정렬된 새로운 리스트 반환</li>
<li><code>sort()</code> : 리스트 자체 정렬하지만 반환값은 없다</li>
</ul>
<h3 id="dtreplacetzinfotimezoneutc랑-dtastimezonetimezoneutc의-차이">dt.replace(tzinfo=timezone.utc))랑 dt.astimezone(timezone.utc))의 차이</h3>
<p><code>dt.replace(tzinfo=timezone.utc))</code> : 시각은 그대로 둔 채 &#39;이 시각은 이제부터 이 시간대야&#39;라고 <strong>이름표만 갈아끼우는</strong> 것이다.</p>
<ul>
<li><code>datetime</code> 객채 값은 변하지 않고, <code>tzinfo</code> 속성만 덮어쓴다.</li>
<li>시간대 정보가 없는 Naive 데이터에 이건 이러한 UTC 시간이야-라고 정의할 때 사용</li>
<li>이미 시간대 정보가 있는 객체에 사용하면, 실제 시점이 변해버린다. → 서울 오후 3시를 <code>replace(tzinfo=UTC)</code>라고 하면 영국 시간(UTC) 오후 3시가 되어서 시간이 어긋난다. 잘못 설정했을 때는 이게 맞지.</li>
</ul>
<p><code>dt.astimezone(timezone.utc))</code> : 실제 시점을 유지하면서 다른 시간대의 시각으로 계산하는 것</p>
<ul>
<li>UTC 기준으로 시각을 더하거나 빼서 실제 같은 순간을 가르키는 <strong>다른 지역의 시간을 계산</strong></li>
<li>서울 오후 3시 → 영국 시간으로 변환할 때</li>
<li>Naive 객체에 사용하면, 파이썬은 이 객체를 시스템의 로컬 시간대로 간주하고 변환을 시도한다. </li>
</ul>
<p><code>astimezone</code> - datetime 객체에서 사용하는 메서드</p>
<p><code>tz_convert</code> : Pandas의 Series나 DatetimeIndex에서 사용한다. </p>
<hr>
<p>timezone 객체를 생성할 때는 <code>timezone(timedelta(hours=-8))</code> 이라면,
datetime 객체를 생성할 때는 <code>dt = datetime(2017, 10, 1, 15, 26, 26, tzinfo=pst)</code></p>
<p>tzinfo ← datetime 객체에서</p>
<table>
<thead>
<tr>
<th>column</th>
<th>data type</th>
<th>description</th>
<th>cleaning requirements</th>
</tr>
</thead>
<tbody><tr>
<td><code>client_id</code></td>
<td><code>integer</code></td>
<td>Client ID</td>
<td>N/A</td>
</tr>
<tr>
<td><code>age</code></td>
<td><code>integer</code></td>
<td>Client&#39;s age in years</td>
<td>N/A</td>
</tr>
<tr>
<td><code>job</code></td>
<td><code>object</code></td>
<td>Client&#39;s type of job</td>
<td>Change <code>&quot;.&quot;</code> to <code>&quot;_&quot;</code></td>
</tr>
<tr>
<td><code>marital</code></td>
<td><code>object</code></td>
<td>Client&#39;s marital status</td>
<td>N/A</td>
</tr>
<tr>
<td><code>education</code></td>
<td><code>object</code></td>
<td>Client&#39;s level of education</td>
<td>Change <code>&quot;.&quot;</code> to <code>&quot;_&quot;</code> and <code>&quot;unknown&quot;</code> to <code>np.NaN</code></td>
</tr>
<tr>
<td><code>credit_default</code></td>
<td><code>bool</code></td>
<td>Whether the client&#39;s credit is in default</td>
<td>Convert to <code>boolean</code> data type:  <br><code>1</code> if <code>&quot;yes&quot;</code>, otherwise <code>0</code></td>
</tr>
<tr>
<td><code>mortgage</code></td>
<td><code>bool</code></td>
<td>Whether the client has an existing mortgage (housing loan)</td>
<td>Convert to boolean data type:  <br><code>1</code> if <code>&quot;yes&quot;</code>, otherwise <code>0</code></td>
</tr>
</tbody></table>
<p><strong>campaign.csv</strong></p>
<table>
<thead>
<tr>
<th>column</th>
<th>data type</th>
<th>description</th>
<th>cleaning requirements</th>
</tr>
</thead>
<tbody><tr>
<td><code>client_id</code></td>
<td><code>integer</code></td>
<td>Client ID</td>
<td>N/A</td>
</tr>
<tr>
<td><code>number_contacts</code></td>
<td><code>integer</code></td>
<td>Number of contact attempts to the client in the current campaign</td>
<td>N/A</td>
</tr>
<tr>
<td><code>contact_duration</code></td>
<td><code>integer</code></td>
<td>Last contact duration in seconds</td>
<td>N/A</td>
</tr>
<tr>
<td><code>previous_campaign_contacts</code></td>
<td><code>integer</code></td>
<td>Number of contact attempts to the client in the previous campaign</td>
<td>N/A</td>
</tr>
<tr>
<td><code>previous_outcome</code></td>
<td><code>bool</code></td>
<td>Outcome of the previous campaign</td>
<td>Convert to boolean data type:  <br><code>1</code> if <code>&quot;success&quot;</code>, otherwise <code>0</code>.</td>
</tr>
<tr>
<td><code>campaign_outcome</code></td>
<td><code>bool</code></td>
<td>Outcome of the current campaign</td>
<td>Convert to boolean data type:  <br><code>1</code> if <code>&quot;yes&quot;</code>, otherwise <code>0</code>.</td>
</tr>
<tr>
<td><code>last_contact_date</code></td>
<td><code>datetime</code></td>
<td>Last date the client was contacted</td>
<td>Create from a combination of <code>day</code>, <code>month</code>, and a newly created <code>year</code> column (which should have a value of <code>2022</code>);  <br><strong>Format =</strong> <code>&quot;YYYY-MM-DD&quot;</code></td>
</tr>
<tr>
<td>month, day 컬럼 불러온 뒤에 2022로 설정하기</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p><strong>economics.csv</strong></p>
<table>
<thead>
<tr>
<th>column</th>
<th>data type</th>
<th>description</th>
<th>cleaning requirements</th>
</tr>
</thead>
<tbody><tr>
<td><code>client_id</code></td>
<td><code>integer</code></td>
<td>Client ID</td>
<td>N/A</td>
</tr>
<tr>
<td><code>cons_price_idx</code></td>
<td><code>float</code></td>
<td>Consumer price index (monthly indicator)</td>
<td>N/A</td>
</tr>
<tr>
<td><code>euribor_three_months</code></td>
<td><code>float</code></td>
<td>Euro Interbank Offered Rate (euribor) three-month rate (daily indicator)</td>
<td>N/A</td>
</tr>
</tbody></table>
<pre><code class="language-python">import pandas as pd
import numpy as np
from datetime import datetime

df = pd.read_csv(&#39;bank_marketing.csv&#39;)
client = df[[&#39;client_id&#39;, &#39;age&#39;, &#39;job&#39;, &#39;marital&#39;,&#39;education&#39;, &#39;credit_default&#39;, &#39;mortgage&#39;]]

client[&#39;job&#39;] = client[&#39;job&#39;].str.replace(&#39;.&#39;,&#39;_&#39;)
client[&#39;education&#39;] = client[&#39;education&#39;].replace({&#39;.&#39;:&#39;_&#39;, &#39;unknown&#39;:np.NaN})
client[&#39;credit_default&#39;] = (client[&#39;credit_default&#39;] == &#39;yes&#39;).astype(int)
client[&#39;mortgage&#39;] = np.where(client[&#39;mortgage&#39;] == &#39;yes&#39;, 1, 0)

client.to_csv(&#39;client.csv&#39;)

campaign = df[[&#39;client_id&#39;, &#39;number_contacts&#39;, &#39;contact_duration&#39;, &#39;previous_campaign_contacts&#39;,&#39;previous_outcome&#39;, &#39;campaign_outcome&#39;]]

campaign[&#39;previous_outcome&#39;] = (campaign[&#39;previous_outcome&#39;] == &#39;success&#39;).astype(int)
campaign[&#39;campaign_outcome&#39;] = (campaign[&#39;campaign_outcome&#39;] == &#39;yes&#39;).astype(int)
campaign[&#39;last_contact_date&#39;] = pd.to_datetime(campaign[&#39;last_contact_date&#39;], format=&#39;YYYY-MM-DD&#39;)

campaign.to_csv(&#39;campaign.csv&#39;)


economics = df[[&#39;client_id&#39;, &#39;cons_price_idx&#39;, &#39;euribor_three_months&#39;]]
economics.to_csv(&#39;economics.csv&#39;)</code></pre>
<p>error1 : Expected the credit_default column in the client.csv file to be bool data type.</p>
<pre><code class="language-python">client[&#39;credit_default&#39;] = (client[&#39;credit_default&#39;] == &#39;yes&#39;).astype(int)

# astype(bool)</code></pre>
<p>wrong : </p>
<pre><code class="language-python">client[&#39;education&#39;] = client[&#39;education&#39;].str.lower().replace({&#39;.&#39;:&#39;_&#39;, &#39;unknown&#39;:np.NaN})</code></pre>
<p>![[스크린샷 2026-04-09 오후 9.35.15.png]]
실제로 체크해보니까 안 바뀌었다.</p>
<p><code>str.lower().replace</code> → str.replace()가 아니라 <code>.replace()</code>로 받아들여진다. 이는 값 전체가 일치할 때, 처리에 적합하다. 따라서, unknown → NaN 처리에는 적합하나, <strong>문자열의 일부인</strong> &quot;.&quot;를 &quot;_ &quot; 로 바꾸는 데는 적합하지 않다. </p>
<p>문자열의 일부를 바꿀 때는 <strong><code>str.replace()</code></strong> 메서드를 활용한다. </p>
<pre><code class="language-python">client[&#39;education&#39;] = client[&#39;education&#39;].str.lower().replace(&#39;unknown&#39;, np.NaN)
client[&#39;education&#39;] = client[&#39;education&#39;].str.replace(&#39;.&#39;, &#39;_&#39;)</code></pre>
<h2 id="answer">answer</h2>
<pre><code class="language-python">import pandas as pd
import numpy as np
from datetime import datetime

df = pd.read_csv(&#39;bank_marketing.csv&#39;)
client = df[[&#39;client_id&#39;, &#39;age&#39;, &#39;job&#39;, &#39;marital&#39;,&#39;education&#39;, &#39;credit_default&#39;, &#39;mortgage&#39;]]

client[&#39;job&#39;] = client[&#39;job&#39;].str.replace(&#39;.&#39;,&#39;_&#39;)
client[&#39;education&#39;] = client[&#39;education&#39;].str.lower().replace(&#39;unknown&#39;, np.NaN)
client[&#39;education&#39;] = client[&#39;education&#39;].str.replace(&#39;.&#39;, &#39;_&#39;)
client[&#39;credit_default&#39;] = (client[&#39;credit_default&#39;] == &#39;yes&#39;).astype(bool)
client[&#39;mortgage&#39;] = np.where(client[&#39;mortgage&#39;] == &#39;yes&#39;, 1, 0).astype(bool)

client.to_csv(&#39;client.csv&#39;, index = False)

campaign = df[[&#39;client_id&#39;, &#39;number_contacts&#39;, &#39;contact_duration&#39;, &#39;previous_campaign_contacts&#39;,&#39;previous_outcome&#39;, &#39;campaign_outcome&#39;]]

campaign[&#39;previous_outcome&#39;] = (campaign[&#39;previous_outcome&#39;] == &#39;success&#39;).astype(bool)
campaign[&#39;campaign_outcome&#39;] = (campaign[&#39;campaign_outcome&#39;] == &#39;yes&#39;).astype(bool)
# campaign[&#39;last_contact_date&#39;] = pd.to_datetime(campaign[&#39;last_contact_date&#39;], format=&#39;YYYY-MM-DD&#39;)
campaign[&#39;last_contact_date&#39;] = pd.to_datetime(
    &quot;2022-&quot; + df[&#39;month&#39;].str.lower() + &quot;-&quot; + df[&#39;day&#39;].astype(str)
).dt.strftime(&#39;%Y-%m-%d&#39;)
campaign.to_csv(&#39;campaign.csv&#39;, index = False)


economics = df[[&#39;client_id&#39;, &#39;cons_price_idx&#39;, &#39;euribor_three_months&#39;]]
economics.to_csv(&#39;economics.csv&#39;, index = False)</code></pre>
<h2 id="효율적인-python-코드-작성">효율적인 python 코드 작성</h2>
<p>Goal : 지연 시간과 오버헤드를 줄인다</p>
<p>unpack 연산자 (&quot; * &quot;) </p>
<pre><code class="language-python">nums = range(1,11,2)
n_list = list(nums)


# unpack 연산자
num_list = [*range(1,11,2)]
</code></pre>
<p><code>enumerate(iterable, start=0)</code> </p>
<ul>
<li>iterable (필수) : 반복 가능한 객체(list, tuple, string, dictionary …)</li>
<li>start(선택) : 인덱스 시작 번호 </li>
</ul>
<p><code>[enumerate(names,1)]</code> → 함수 실행하면 리스트가 아니라 enumerate object라는 특수한 객체 반환한다.
→ 주소 반환
→ 이거는 iterator로 호출하면 결과값을 내보일 준비가 됐다.
→ 결과값을 보기 위해서는 그 안의 내용물을 꺼내야 한다. 
<strong>unpack 연산자는 안의 결과값을 꺼내서 나열하는 연산자다.</strong> </p>
<p>일종의 <strong>Lazy Evaulation</strong>이다. 필요할 때까지 계산을 미루는 것!
이게 나중에 spark나 snowflake 등 모든 곳에 쓰이는 핵심 원리!</p>
<p><code>str.upper()</code>처럼 괄호를 붙이면, <code>str.upper</code> 메서드를 즉시 호출하려고 시도합니다. 하지만 <code>str.upper()</code>는 문자열 인스턴스가 필요합니다(예: <code>&#39;abc&#39;.upper()</code>). <code>map()</code> 함수에는 함수 자체(즉, 호출하지 않은 상태)를 전달해야 합니다. 즉, <code>str.upper</code>처럼 괄호 없이 함수 객체를 전달해야 각 요소에 대해 나중에 호출할 수 있습니다.</p>
<p><strong>함수 객체와 함수 호출의 차이</strong> 
괄호를 붙이면 함수가 바로 실행되고, 괄호 없이 전달하면 함수 자체를 전달하는 것이다.</p>
<h3 id="numpy">numpy</h3>
<p>numpy 배열은 동종으로, 모든 원소가 같은 타입이어야 한다.<br>맞추지 않으면, nupy가 알아서 변환한다. 
python 내장 list는 브로드캐스팅을 지원하지 않는다. </p>
<h2 id="실행-시간">실행 시간</h2>
<p><code>%timeit</code> : 분석하고 싶은 줄 앞에 매직 커맨드를 붙이면 된다.
시간 통계를 평균으로 제공한다.(평균 + 표준편차 제공)</p>
<ul>
<li><code>-r</code> (runs) : 실행 횟수 설정</li>
<li><code>-n</code> (loops) : 루프 수 설정
여러 줄 → <code>%%timeit</code></li>
<li><code>-o</code> : %timeit의 출력을 변수에 저장할 수 있다.</li>
</ul>
<p>코드 프로파일링</p>
<ul>
<li>함수 호출 빈도와 소요 시간의 상세 통계</li>
<li>한 줄 단위 분석</li>
<li><code>line_profiler</code> 설치 필요</li>
</ul>
<p>메모리 사용 코드 프로파일링
memory_profiler</p>
<ul>
<li>메모리 사용량에 대한 상세 통계</li>
<li>분석하고자 하는 함수는 반드시 <code>import</code> 해야 한다.</li>
</ul>
<h2 id="효율적-결합-세고-반복">효율적 결합, 세고, 반복</h2>
<p><code>zip</code> : 객체들을 하나로 맞물려 결합시킨다. → zip 객체 반환하므로, 내용을 보려면 리스트로 풀어(unpack)서 출력해야 한다. 각 항목은 원래 리스트들에서 같은 위치의 원소들을 모은 tuple이다.</p>
<pre><code class="language-python">differing_lengths = [*zip(names[:5], primary_types[:3])]</code></pre>
<p><code>collections</code> 모듈 </p>
<ul>
<li><code>napedtuple</code> : 필드명이 있는 tuple 서브클라스</li>
<li><code>deque</code> : 빠른 append/pop이 가능한 리스트형 컨테이너</li>
<li><code>Counter</code> : 해시 가능한 객체를 세는 dict</li>
<li><code>OrderDict</code> : 삽입 순서 유지하는 dict</li>
<li><code>defaultdict</code> : 누락 값에 공장 함수 호출하는 dict</li>
</ul>
<h2 id="집합-이론">집합 이론</h2>
<p><code>symmetric_difference()</code> : 대칭 차집합
<code>.union()</code> : 중복 없이 두 집합 원소들 결합</p>
<p>iterrows(), itertuples()</p>
<p>values: np.array 타입으로 가져올 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.08(Wed)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.08Wed</link>
            <guid>https://velog.io/@o_u--chan/2026.04.08Wed</guid>
            <pubDate>Fri, 10 Apr 2026 16:01:22 GMT</pubDate>
            <description><![CDATA[<p>dbt workflow </p>
<ol>
<li><code>dbt init</code></li>
<li>profiles.yml 파일에서 설정 정의, 업데이트</li>
<li>데이터 모델 정의 &amp; 사용 (<code>dbt run</code>)<ul>
<li>데이터 모델 : 데이터 웨어하우스에 저장된 원천 데이털르 변환한 결과</li>
<li><code>dbt run</code> : 원본 SQL 코드르 프로필(배포 대상)에 맞게 변환한 뒤, 변환 과정을 수행</li>
</ul>
</li>
<li>데이터 검증 &amp; 테스트</li>
<li></li>
</ol>
<p><strong>데이터를 어떤 형태로 저장하고 보여줄 것인가?</strong> 에 대한 전략</p>
<ol>
<li>View <ul>
<li>데이터 저장 X, 호출될 때마다 SQL 쿼리 실행</li>
<li>저장 공간 차지 X</li>
</ul>
</li>
<li>Table</li>
<li>Incremental<ul>
<li>추가된 데이터만 이어 붙임</li>
</ul>
</li>
<li>Ephemeral(임시)<ul>
<li>DB에 어떤 것도 남기지 않는다.</li>
</ul>
</li>
</ol>
<p>Materialized View(구체화된 뷰)</p>
<ul>
<li>쿼리 결과 저장 → 속도는 Table처럼 빠름</li>
<li>원본 데이터 변하면 백그라운드에서 자동으로 업데이트 시도 </li>
</ul>
<p>완전 무작위 누락(MCAR) : 결측 데이터와 다른 값 사이에 체계적인 관계가 없음</p>
<ul>
<li>데이터 입력 시 입력 오류
무작위 결측(누락)(MAR) : 결측 데이터와 다른 관측된 값 사이에 체계적인 관계가 있음</li>
<li>높은 기온에 대한 오존 데이터 결측
비무작위 결측(MNAR) : 결측 데이터와 관측되지 않은 값 사이에 체계적인 관계가 있음</li>
<li>높은 기온에 대한 온도 값 결측</li>
</ul>
<p><code>process.extract(비교의 기준이 되는 문자열, 비교할 대상들이 담긴 리스트나 딕셔너리, limit=유사도가 높은 순서대로 몇 개의 결과 가져올지 결정)</code></p>
<p>페어 생성하기, 블로킹
recordlinkage,compare, exact</p>
<pre><code class="language-python"># import recordlinkage
import recordlinkage

# create indexing object
indexer = recordlinkage.Index()

# generate pairs blocked on state
indexer.block(&#39;state&#39;)
pairs = indexer.index(column_A, column_B)

# create a compare object
compare_cl = recordlinkage.Compare()

# find exact matches for pairs of column
compare_cl.exact(&#39;column&#39;, &#39;column&#39;, label=&#39;label_name&#39;)

# find similar matches for pairs of surname and address_1 using string similarity
compare_cl.string(&#39;surname&#39;, &#39;surname&#39;, threshold=0.85, label=&#39;label_name&#39;)

# find matches
potential_matches = compare_cl.compute(pairs, column_A, column_B)
</code></pre>
<h2 id="recordlinkage">recordlinkage</h2>
<p>서로 다른 두 데이터셋에서 같은 대상을 가르키는 행들을 찾아내는 과정</p>
<p>ex) A 데이터셋의 A 식당과 B데이터셋의 B 식당이 같은 식당인가?
city, cuisine_type을 먼저 비교해.</p>
<p>Indexing(Block) : 비교할 범위를 제한
Compare(exact) : 완벽히 일치해야 하는 항목 → 같으면 True, 다르면 False
Compare(string): 오타가 있어도 비슷한 항목 → Threshold 이상 = 1
Compute: 최종 점수 계산</p>
<p>페어 생성 → 컬럼 간 비교 → 잠재적 매치 찾기 → 중복 탐색 → 두 데이터프레임 붙이기</p>
<p><strong>Python으로 데이터 정제하기</strong> Again → Record Linkage</p>
<ul>
<li><input disabled="" type="checkbox"> Python에서 날짜와 시간 다루기</li>
<li><input disabled="" type="checkbox"> Python 정규표현식</li>
<li><input disabled="" type="checkbox"> Python에서 결측치 처리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.07(Tue)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.07Tue</link>
            <guid>https://velog.io/@o_u--chan/2026.04.07Tue</guid>
            <pubDate>Fri, 10 Apr 2026 16:00:59 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python">xls = pd.read_excel(url, sheet_name=None)</code></pre>
<p><code>sheet_name=None</code>으로 인수를 전달하면 모든 시트가 dictionary 형태로 반환됨.
<strong>sheet_name</strong>을 인수로 전달하지 않으면 첫 번째 시트만 반환된다.
각 스프레드 시트의 이름을 알고 싶다면 <code>xls.keys()</code>로 키값을 구해야 된다.</p>
<p>웹 개발에서 tag soup는 웹페이지의 html 코드가 구조나 문법 면에서 얽힌 상태를 뜻한다.
BeautifulSoup는 이 tag soup를 보기 좋게 만들고 필요한 정보를 추출하는 것이다.</p>
<pre><code class="language-python"># error
authentication = {&#39;name&#39;:&#39;john@doe.com&#39;, &#39;password&#39;:&#39;Warp_ExtrapolationsForfeited2&#39;}


response = requests.get(&#39;http://localhost:3000/albums&#39;, auth=authentication)

if(response.status_code == 200):
    print(&quot;Success!&quot;)
elif(response.status_code == 401):
    print(&#39;Authentication failed&#39;)
else:
    print(&#39;Another error occurred&#39;)</code></pre>
<p><code>auth</code> 인자는 기본 인증을 위해 사용자 이름과 비밀번호가 들어있는 <strong>Tuple</strong>을 기대한다.
따라서 dictionary 형태로 값을 전달하면 <code>&#39;dict&#39; object is not callable</code>이라는 에러가 뜬다.</p>
<p>reqeusts.get() 에서 URL 매개변수로 값을 전달하는 인자는 <strong><code>params</code></strong> 다.
headers에서 API 키에 대한 값을 전달할 때는 </p>
<pre><code class="language-python">headers = {&quot;Authorization&quot; : &#39;Bearer 8apDFHaNJMxy8Kt818aa6b4a0ed0514b5d3&#39;}</code></pre>
<p>Bearer는 소유자라는 의미다. → 토큰의 소유자로서 토큰 전달</p>
<p><code>content-type</code>  header : API가 응답을 어떤 형식으로 보냈는지를 나타낼 때 사용</p>
<ul>
<li>내가 보내는 것</li>
</ul>
<p><code>accept</code> header : Request 헤더에서만 주로 사용한다</p>
<ul>
<li>받고 싶은 데이터의 형식</li>
</ul>
<p>GET 요청에서 URL 쿼리 파라미터 → <code>params</code> 인자</p>
<p>json 데이터를 보낼 때(POST 요청) → <code>json</code> 인자</p>
<p><code>raise_for_status()</code> : enable raising exceptions for returned error statuscodes</p>
<p><code>except HTTPError as http_err</code> : catch error responses from the API server
<code>except ConnectionError as conn_err</code> : catch any connections errors </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026.04.06(Mon)]]></title>
            <link>https://velog.io/@o_u--chan/2026.04.06Mon</link>
            <guid>https://velog.io/@o_u--chan/2026.04.06Mon</guid>
            <pubDate>Fri, 10 Apr 2026 16:00:32 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python"># Define a function called concat
def concat(**kwargs):

&quot;&quot;&quot;Concatenates keyword arguments into a single string with spaces.&quot;&quot;&quot;

result = &quot;&quot;

# Iterate over the Python kwargs
for kwarg in kwargs.items():
result += &quot; &quot; + kwarg

return result

# Call the function
print(concat(start=&quot;Python&quot;, middle=&quot;is&quot;, end=&quot;great!&quot;))</code></pre>
<p><code>kwargs.iter()</code> → dict has no attribute &#39;iteration&#39; </p>
<blockquote>
<p>파이썬3에는 <code>iter()</code> 메서드 존재하지 않는다. <code>items()</code>, <code>keys()</code>, <code>values()</code>가 iterator와 유사한 view 객체를 반환한다.</p>
</blockquote>
<p><code>kwargs.items()</code> → can only concatenate str (not tuple) to str</p>
<blockquote>
<p><code>kwargs.items()</code>를 반복문으로 돌리면 각 요소는 (key, value) 형태의 튜플로 반환된다. result는 문자열인데, 문자열에 튜플을 더하려고 해서 위와 같은 에러가 발생한 것이다. </p>
</blockquote>
<p><code>kwargs.values()</code>를 사용해서 value만 뽑아 문자열로 사용하면 해결되는 문제였다.</p>
<p>valueerror : 제공된 값이 허용 가능한 범위에 있지 않을 때</p>
<ul>
<li>타입은 맞았는데, 값이 잘못되서 불가함.</li>
<li><code>float(&#39;hello&#39;)</code> : float는 문자열을 받기도 하는데, hello를 바꿔줄 숫자 값이 없다.</li>
</ul>
<p>git revert</p>
<ul>
<li>이전 버전을 되살리고 커밋 만든다</li>
<li>해당 커밋에서 변경된 모든 파일 복원</li>
<li>commit 없이 되돌리고 싶다.(staging에 가져오기) : <code>git revert -n HEAD</code><ul>
<li><code>-n</code> : no commit</li>
</ul>
</li>
<li>편집기 열리는 것을 피하고 싶다. : <code>--no-edit</code></li>
</ul>
<p>만약 단일 파일만 되돌리고 싶다면 → <code>git checkout</code>
<code>git checkout HEAD~1 -- report.md</code></p>
<ul>
<li>staging 파일에 올라가 있다.</li>
</ul>
<p>staging 영역에 있는 파일을 되돌리고 싶다면? → <code>unstaging</code> </p>
<ul>
<li>단일 파일 <code>git restore --staged summary_statistics.csv</code></li>
<li>모든 파일 <code>git restore --staged</code></li>
</ul>
<p>flat file : 구조화된 관계가 없는 레코드</p>
<ul>
<li>테이블 데이터를 담은 기본 텍스트 파일</li>
<li>필드나 속성으로 이루어진 행, 레코드들의 모음</li>
<li>각 필드에는 최대 한 가지 정보만 들어있다. </li>
<li>헤더가 있을 수 있다. (데이터의 열의 내용, 해당하는 속성이나 특징이 무엇인지 설명)다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Data Engineering For Beginners : MAC - (1)]]></title>
            <link>https://velog.io/@o_u--chan/Data-Engineering-%EC%8B%9C%EC%9E%91-2026.03.26</link>
            <guid>https://velog.io/@o_u--chan/Data-Engineering-%EC%8B%9C%EC%9E%91-2026.03.26</guid>
            <pubDate>Thu, 26 Mar 2026 15:54:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공부를 하다 보니 뭘 내 직무로 삼아야 될 지 감이 안 온다. DA? 개발자? 보안?
하나를 진득하니 했어야 했는데, 최소한 누군가한테 기본기로 하루 정도는 설명할 수 있을 지식은 가지고 있어야 했다. 따라서 오늘부터 해보기로 했다. 뭘? 그냥 다!
알고리즘, 판다스, SQL, DE, DA. IT 소식과 CS도 끊임없이 채워넣겠다. 늦었다고는 생각하지 않지만 열심히 해보자. </p>
</blockquote>
<p>모르는 개념, 용어가 나왔을 때 정리하고 그 이외는 넘어갈 예정입니다.
궁금한 점이 생기시면 댓글 남겨주세요. 보는 사람이 있을지는 모르겠지만서도?</p>
<p>일단 시작은 Datacamp로 빠르게 입문하려고 합니다. 그럼 바로 시작할께요.</p>
<p>Data Engineering For Beginners : <a href="https://de101.startdataengineering.com/">https://de101.startdataengineering.com/</a></p>
<h1 id="환경-설정">환경 설정</h1>
<ol>
<li><p>git version &gt;= 2.37.1</p>
</li>
<li><p>Docker version &gt;= 20.10.17 &amp;&amp; Docker compose v2 version &gt;= v2.10.2</p>
</li>
<li><p><a href="https://github.com/josephmachado/data_engineering_for_beginners_code">https://github.com/josephmachado/data_engineering_for_beginners_code</a>
fork 한 후에, 자기 레포지토리에서 클론</p>
</li>
</ol>
<pre><code class="language-bash">git clone clone_address.git
cd data_engineering_for_begineers_code
docker compose up -d --build
sleep 30</code></pre>
<h2 id="mac을-사용하는-경우">MAC을 사용하는 경우</h2>
<p><img src="https://velog.velcdn.com/images/o_u--chan/post/4f861a7e-3540-4610-932a-53ee62478098/image.png" alt="">
이런 에러를 확인할 수 있다. <img src="https://velog.velcdn.com/images/o_u--chan/post/4399979b-a84f-492a-95e1-7d2f925b7cf6/image.png" alt="">
위 사진을 보면 amd64로 spark가 설치되었다..</p>
<p><img src="https://velog.velcdn.com/images/o_u--chan/post/b6a71248-3188-4f9b-999d-878b317dec86/image.png" alt="">
amd64를 arm64로 변경해준다. </p>
<pre><code class="language-bash">docker compose down

docker compose up -d --build</code></pre>
<p>컨테이너 종료하고 다시 build </p>
<p><a href="http://localhost:8888">http://localhost:8888</a> 들어가서 다시 data 만들어보면 ... <strong>성공!!</strong></p>
<p><img src="https://velog.velcdn.com/images/o_u--chan/post/4e35633d-9503-4b9e-b651-1755789e38e5/image.png" alt=""></p>
<p>Data Schema는 아래와 같다.
<img src="https://velog.velcdn.com/images/o_u--chan/post/eb9d6af8-b9f8-4f91-9de8-8f9770983aef/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[100 padas puzzles - No. 26~27(3)]]></title>
            <link>https://velog.io/@o_u--chan/100-padas-puzzles-No.-2627</link>
            <guid>https://velog.io/@o_u--chan/100-padas-puzzles-No.-2627</guid>
            <pubDate>Wed, 25 Mar 2026 15:03:28 GMT</pubDate>
            <description><![CDATA[<h3 id="26-you-have-a-dataframe-that-consists-of-10-columns-of-floating-point-numbers-exactly-5-entries-in-each-row-are-nan-values-for-each-row-of-the-dataframe-find-the-column-which-contains-the-third-nan-value">26. you have a DataFrame that consists of 10 columns of floating-point numbers. Exactly 5 entries in each row are NaN values. For each row of the DataFrame, find the column which contains the third NaN value.</h3>
<p>각 행에서 3번째 NaN 값이 있는 컬럼을 찾아라</p>
<pre><code class="language-python">import numpy as np
nan = np.nan

data = [[0.04,  nan,  nan, 0.25,  nan, 0.43, 0.71, 0.51,  nan,  nan],
        [ nan,  nan,  nan, 0.04, 0.76,  nan,  nan, 0.67, 0.76, 0.16],
        [ nan,  nan, 0.5 ,  nan, 0.31, 0.4 ,  nan,  nan, 0.24, 0.01],
        [0.49,  nan,  nan, 0.62, 0.73, 0.26, 0.85,  nan,  nan,  nan],
        [ nan,  nan, 0.41,  nan, 0.05,  nan, 0.61,  nan, 0.48, 0.68]]

columns = list(&#39;abcdefghij&#39;)

df = pd.DataFrame(data, columns=columns)</code></pre>
<p>isnull()은 isna()와 같다. isnull은 null 값에 익숙한 사람들을 위해 만든 alias 함수이다.
<code>df.isna().sum()</code> - NaN의 총 개수는 구할 수 있는데 어떻게 3번째일 때를 조건절로 세울까?</p>
<p><code>cumsum()</code>이라고 누적합을 구해주는 함수가 있다.
<code>df.loc[:, df.isna().cumsum(axis=1) == 3].idxmax()</code>를 실행하면 
<strong>Cannot index with multidimensional key</strong>라는 에러가 뜨는 것을 볼 수 있다. 
loc 함수는 행이나 열 자리에 조건을 넣을 때, 1줄짜리(1차원) 조건만을 수용하는데, 실제로 넣은 것은 각 행에 boolean 값이 들어가있는 df와 같은 크기의 2차원 표이기 때문에 에러가 발생한 것이다.</p>
<p>그러면 어떻게 할까??
df.loc을 그냥 안 쓰면 된다.
<code>(df.isna().cumsum(axis=1) == 3).idxmax(axis=1)</code> 
여기서 idxmax()??라고 할 수도 있는데, idxmax()는 최댓값이 여러 개가 있으면 가장 먼저 나온 값을 반환한다. 그렇기에 조건절로 max 값을 설정해놓고 가장 맨 첫 번째 값을 출력할 수 있다. 꿀팁이다.</p>
<h3 id="27-a-dataframe-has-a-column-of-groups-grps-and-column-of-integer-values-vals-for-each-group-find-the-sum-of-the-three-greatest-values-you-should-end-up-with-the-answer-as-follows">27. A DataFrame has a column of groups &#39;grps&#39; and column of integer values &#39;vals&#39;. For each group, find the sum of the three greatest values. You should end up with the answer as follows:</h3>
<p>a.b.c 컬럼에서 각각 가장 큰 3개의 값 합 구하시오.</p>
<pre><code>grps
a    409
b    156
c    345</code></pre><pre><code class="language-python">
# 내 풀이
df[&#39;rank&#39;] = df.groupby(by=&#39;grps&#39;)[&#39;vals&#39;].rank(ascending=False)
df.loc[df[&#39;rank&#39;]&lt;=3, [&#39;grps&#39;,&#39;vals&#39;]].sort_values(by=[&#39;grps&#39;]).groupby(&#39;grps&#39;).sum()

# 해설
df.groupby(&#39;grps&#39;)[&#39;vals&#39;].nlargest(3).sum(level=0)</code></pre>
<p>일단 sort_values를 썼으면 굳이 rank가 없었어도 됐다. 
<img src="https://velog.velcdn.com/images/o_u--chan/post/677d5936-8cef-4ed2-a380-97da272866d3/image.png" alt="">
sql을 생각해보면 group by절 사용 후에 특정값을 뽑아내려고 하면 실패하는 경험들이 있을 것이다. 이처럼 pandas에서도 <code>groupby(&#39;grps&#39;)</code>를 실행하는 순간, 그룹화된 보따리 안에 있다고 생각해야 된다. 
<code>.sort_values()</code> 함수는 DataFrame 상태일 때만 사용할 수 있는 전용 함수이기에 그룹화한 후에 정렬을 하려고 하면 정렬 기능 실행 못한다고 에러가 발생되는 것이다!</p>
<p>df[&#39;rank&#39;]를 선언하고 sort_values를 사용했던 것은 의도가 아니었는데 앞으로는 지식을 가지고 사용하자. 다시 돌아가서!</p>
<p>그룹화한 후에 정렬 못하면 어떻게 하냐? GroupBy 객체 상태에서 상위 N개 추출 전용 함수가 있다.
<code>nlargest</code>라는 함수다. </p>
<p><img src="https://velog.velcdn.com/images/o_u--chan/post/076918df-11e3-4663-868c-d9be92594ba9/image.png" alt=""></p>
<p>nlargest(3)을 하고 sum을 하면 그냥 grps가 a,b,c일 때 3번째까지 큰 수를 그냥 다 더한 것 밖에 되지 않는다.</p>
<p>그룹화를 한 것에 주의해야 하는데, 그룹화를 하고 그 안에서 3번째로 큰 수까지 구했다는 건 grps와 vals를 가르는 경계가 있는 것을 생각해볼 수 있다. 이때 밖을 level=0, 안을 level=1이라고 하는데 sum(level=0)이라고 설정하면 레벨 0 기준으로 묶어서 합을 구할 수 있다.</p>
<p>그런데, 에러가 발생할 수도 있어서 요즘은 이렇게 사용하지 않고 
<code>df.groupby(by=&#39;grps&#39;)[&#39;vals&#39;].nlargest(3).groupby(level=0).sum()</code>
과 같이 사용한다.</p>
<h3 id="갑자기-공식-문서-보다가-궁금해진-함수들">갑자기 공식 문서 보다가 궁금해진 함수들</h3>
<p><strong>set_flags()</strong>
pandas는 컬럼 이름이나 인덱스(행 이름)이 중복되는 것을 허용한다. 다른 사람들이 만든 데이터들을 합칠 때, 다른 의미의 데이터가 이름은 같다면 병합되어 데이터의 가치가 없어질 것이다. </p>
<p>그럴 때, <strong>set_flags()</strong> 로 중복된 이름표가 들어올 수 없도록 할 수 있다.
<code>df_strict = df.set_flags(allows_duplicate_labels = False)</code>
-&gt; 원래는 덮어씌우거나 추가되지만, 중복 라벨 허용 불가 옵션을 달면 에러가 뜬다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[100 pandas puzzles No.21 ~ 25 (2)]]></title>
            <link>https://velog.io/@o_u--chan/100-pandas-puzzles-No.21-25-2</link>
            <guid>https://velog.io/@o_u--chan/100-pandas-puzzles-No.21-25-2</guid>
            <pubDate>Mon, 23 Mar 2026 16:04:21 GMT</pubDate>
            <description><![CDATA[<h3 id="21for-each-animal-type-and-each-number-of-visits-find-the-mean-age-in-other-words-each-row-is-an-animal-each-column-is-a-number-of-visits-and-the-values-are-the-mean-ages-hint-use-a-pivot-table">21.For each animal type and each number of visits, find the mean age. In other words, each row is an animal, each column is a number of visits and the values are the mean ages (hint: use a pivot table).</h3>
<p>row : animal, column : 방문횟수, 평균나이</p>
<h4 id="pivot-table">pivot table</h4>
<pre><code class="language-python">pandas.pivot_tabe(data 
                , index = None # 각 행은 무엇으로 정의할지
                , columns = None # 각 열 정의
                , values = None # 각 Cell 어떤 숫자로 계산할지
                , aggfunc = &#39;mean&#39;, &#39;sum&#39;, &#39;nunique&#39;, &#39;std&#39; # 계산 방법
                # 추가 옵션
                , fill_value,  margins, drop_na, margins_name, observed
)</code></pre>
<p>pivot_table로 table 확인하는데 원하지 않는 데이터가 많을 때, query 함수로 원하는 데이터만 조회할 수 있다.
<code>query(&#39;컬럼명 == 원하는 조건&#39;)</code>
활용 예시</p>
<pre><code class="language-python">r1.query(&#39;age == [ 2, 3]&#39;).pivot_table(index = [&#39;animal&#39;], columns = [&#39;visits&#39;], values = [&#39;age&#39;], aggfunc = [np.mean])
</code></pre>
<h4 id="answer">answer</h4>
<pre><code class="language-python">df.pivot_table(
    index=&#39;animal&#39;, columns = &#39;visits&#39;, values = &#39;age&#39;, aggfunc = &#39;mean&#39;)</code></pre>
<h3 id="22-중복되지-않는-값-출력하기">22. 중복되지 않는 값 출력하기?</h3>
<p><code>df.drop_duplicates(subset=None, keep=&#39;first&#39;, inplace=False, ignore_index=False)</code>
<strong>Return DataFrame with duplicates rows removed.</strong>
keep : determines which duplicates to keep (first, last, false - drop all)
subset : only consider certain columns for identifying duplicates</p>
<p>answer</p>
<pre><code class="language-python"># 01. drop_duplicates
df.drop_duplicates(subset=&#39;A&#39;)

# 02. loc 함수, shift 함수
df.loc[df[&#39;A&#39;].shift() != df[&#39;A&#39;]]</code></pre>
<p>02번 풀이가 뭔가 싶을 수 있는데, <code>shift()</code>의 period defalt 값이 1로 shift 함수는 값을 한 칸씩 아래로 미는 함수다. 1칸씩 밑으로 밀었을 때, 서로 다른 값들이 있는 행만 선택해서 출력하면 중복값이 필터링된다. 이는 값들이 정렬되어 있기에 가능한 풀이다. </p>
<h3 id="23-given-a-dataframe-of-numeric-values-how-do-you-subtract-the-row-mean-from-each-element-in-the-row">23. given a dataframe of numeric values, how do you subtract the row mean from each element in the row?</h3>
<p>행의 각 요소에 행 평균값을 빼봐라</p>
<pre><code class="language-python">df = pd.DataFrame(np.random.random(size=(5,3)))

df.sub((df.mean(axis = 1) , axis = 0))</code></pre>
<p>혹시 axis가 헷갈리다면</p>
<ul>
<li><code>axis = 0</code>(default) : 행을 따라 아래로 계산(위에서 아래)</li>
<li><code>axis = 1</code> : 열을 따라 옆으로 계산(왼쪽에서 오른쪽으로)</li>
</ul>
<h3 id="24-suppose-you-have-dataframe-with-10-columns-of-real-numbers-which-column-of-numbers-has-the-smallest-sum-return-that-columns-label">24. suppose you have dataframe with 10 columns of real numbers. which column of numbers has the smallest sum? return that column&#39;s label.</h3>
<pre><code class="language-python"># 합이 가장 작은 컬럼의 값 
df.sum(axis = 0).min(axis = 0)

# 01. loc으로 위에서 찾은 값을 조건으로 설정하여 column label 불러올 수 있다.
df.loc[:, df.sum() == df.sum().min()].columns[0]

# 02. idxmin()
df.sum().idxmin()</code></pre>
<h3 id="25-how-do-you-count-how-many-unique-rows-a-dataframe-has-ie-ignore-all-rows-that-are-duplicates">25. how do you count how many unique rows a DataFrame has (i.e. ignore all rows that are duplicates)?</h3>
<p>unique한 행의 개수?</p>
<pre><code class="language-python">len(df.drop_duplicates(keep=False))</code></pre>
<p>unique한 행을 찾는 것이기 때문에 중복되는 행은 전부 삭제해야 한다. -&gt; <code>keep=False</code>
<code>len()</code>을 DataFrame에 사용하면 df의 행 개수를 반환한다. 
<code>df.shape[0]</code>을 사용해서 행 개수를 구할 수도 있다.</p>
]]></description>
        </item>
    </channel>
</rss>