<?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>Fri, 23 Oct 2026 02:24:59 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[programmers: 전략망 둘로 나누기]]></title>
            <link>https://velog.io/@o_u--chan/programmers-%EC%A0%84%EB%9E%B5%EB%A7%9D-%EB%91%98%EB%A1%9C-%EB%82%98%EB%88%84%EA%B8%B0</link>
            <guid>https://velog.io/@o_u--chan/programmers-%EC%A0%84%EB%9E%B5%EB%A7%9D-%EB%91%98%EB%A1%9C-%EB%82%98%EB%88%84%EA%B8%B0</guid>
            <pubDate>Fri, 23 Oct 2026 02:24:59 GMT</pubDate>
            <description><![CDATA[<p>입력값 넣는 것까지 포함</p>
<pre><code class="language-python">import sys
import json

n = int(input())

# ? json.loads()를 통과시키면 문자열이 파이썬 리스트로 변환된다.
wires = json.loads(input())

# copy &amp; paste

gp = [[] for _ in range(n+1)]

for i in range(n-1):
    s, e = wires[i]
    gp[s].append(e)
    gp[e].append(s)

for i in range(1, n+1):
    gp[i].sort()

# visit = [False for _ in range(n+1)]

# dfs 생성 -&gt; return 값으로 cnt
def dfs(v, exclude_s, exclude_e, visit) -&gt; int:
    visit[v] = True
    cnt = 1

    for w in gp[v]:
        # 이번 차례에 무시할 간선 
        if (v == exclude_s and w == exclude_e) or (v == exclude_e and w == exclude_s):
            continue
        if visit[w] is False:
            cnt += dfs(w, exclude_s, exclude_e, visit)

    return cnt

min_diff = n+1

for ex_s, ex_e in wires:
        visit = [False for _ in range(n+1)]
        cnt_a = 0
        cnt_a = dfs(1, ex_s, ex_e, visit)
        tmp_result = abs(2*cnt_a - n)
        if tmp_result &lt; min_diff:
            min_diff = tmp_result

print(min_diff)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[헷갈리는 개념 정리 - (1)]]></title>
            <link>https://velog.io/@o_u--chan/%ED%97%B7%EA%B0%88%EB%A6%AC%EB%8A%94-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-1</link>
            <guid>https://velog.io/@o_u--chan/%ED%97%B7%EA%B0%88%EB%A6%AC%EB%8A%94-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-1</guid>
            <pubDate>Mon, 05 Oct 2026 08:27:33 GMT</pubDate>
            <description><![CDATA[<p>목차</p>
<ol>
<li>Canonical Table과 Checkpoint</li>
<li>Spark Checkpoint의 offsets/N과 commits/N</li>
<li>git diff --cached는 왜 사용할까?</li>
<li>Python 패키지에서 init.py를 만드는 이유</li>
<li>Malformed frame이란?</li>
<li>bytes를 int로 변환하는 방법</li>
<li>json.loads()의 역할</li>
<li>왜 schema가 아니라 schema.schema_str을 사용할까?</li>
<li>fastavro의 parse_schema()</li>
<li>왜 io.BytesIO(avro_body)를 사용할까?</li>
<li>parse_schema()의 반환값은 bytes일까?</li>
<li>schemaless_reader()는 무엇을 할까?</li>
<li>datetime.fromisoformat()</li>
<li>tzinfo와 Naive/Aware datetime</li>
<li>Naive와 Aware를 구분하는 방법</li>
<li>timezone.utc와 astimezone()</li>
</ol>
<ol>
<li>Canonical Table과 Checkpoint
Q. Canonical Table과 Checkpoint가 뭐야?
Spark Structured Streaming 파이프라인에서 둘은 역할이 완전히 다르다.
Checkpoint
Checkpoint는 스트리밍 작업의 진행 상태를 저장하는 위치다.
Spark가 Kafka 같은 소스에서 데이터를 읽을 때,</li>
</ol>
<ul>
<li><p>어디까지 읽었는지</p>
</li>
<li><p>어떤 배치를 처리했는지</p>
</li>
<li><p>어떤 배치가 완료됐는지
등의 정보를 기록한다.
예를 들어 파이프라인이 중간에 종료되더라도 checkpoint가 있다면 처음부터 다시 읽는 것이 아니라 기존 진행 상태를 바탕으로 작업을 복구할 수 있다.
Kafka
↓
Spark Structured Streaming
↓
Canonical Table</p>
<pre><code>  ↘ checkpoint/
     ├── offsets/
     ├── commits/
     └── ...</code></pre><p>즉, checkpoint는 데이터 자체를 저장하는 테이블이라기보다 스트리밍 쿼리의 복구와 진행 상태 관리를 위한 메타데이터 저장소라고 이해하면 된다.
Canonical Table
Canonical Table은 여러 시스템이나 후속 처리 단계에서 공통으로 사용할 수 있도록 일관된 스키마와 형식으로 정리한 표준 데이터 테이블이다.
Kafka에서 들어오는 원본 데이터는 Avro나 JSON 등 다양한 형식일 수 있다. 이를 후속 파이프라인에서 사용하기 편하도록 컬럼명, 타입, 스키마 등을 통일한 결과를 canonical 형태로 관리할 수 있다.
프로젝트 구조에 따라 Medallion Architecture의 Silver 계층과 비슷한 역할을 할 수도 있지만, canonical table이라는 용어 자체가 반드시 Silver를 의미하는 것은 아니다.
핵심 정리
Canonical Table은 정제된 데이터를 저장하는 곳, Checkpoint는 스트리밍 작업이 어디까지 진행됐는지를 저장하는 곳이다.</p>
</li>
</ul>
<ol start="2">
<li>Spark Checkpoint의 offsets/N과 commits/N
Q. commits/N이 뭐야?
Spark Structured Streaming의 checkpoint 디렉터리에는 offsets, commits 같은 로그가 만들어진다.
여기서 N은 micro-batch의 Batch ID라고 볼 수 있다.
예를 들어:
checkpoint/
├── offsets/
│   ├── 0
│   ├── 1
│   └── 2
└── commits/
 ├── 0
 ├── 1
 └── 2
offsets/N
Spark가 N번째 micro-batch에서 처리할 소스의 offset 범위를 기록한다.
쉽게 말하면:
&quot;N번째 배치에서는 여기까지 읽겠다.&quot;</li>
</ol>
<p>라는 계획에 가까운 정보다.
commits/N
해당 batch가 성공적으로 완료되었다는 사실을 나타내는 commit log다.
쉽게 말하면:
&quot;N번째 배치 처리를 완료했다.&quot;</p>
<p>라는 기록이다.
따라서 장애 후 재시작할 때 Spark는 checkpoint의 로그를 이용해 이전 처리 상태를 복원할 수 있다.
핵심 정리
offsets/N
→ N번째 배치가 처리할 offset 정보</p>
<p>commits/N
→ N번째 배치가 성공적으로 완료되었다는 기록
Checkpoint는 단순한 &quot;마지막 offset 숫자 하나&quot;가 아니라, Structured Streaming이 장애 복구를 위해 사용하는 여러 메타데이터 로그로 구성된다.
3. git diff --cached는 왜 사용할까?
Q. 그냥 git diff가 있는데 왜 git diff --cached를 써?
Git에는 크게 세 상태를 구분해서 생각할 수 있다.
Working Directory
      ↓ git add
Staging Area
      ↓ git commit
Repository
일반적인 git diff는 Working Directory와 Staging Area의 차이, 즉 아직 git add하지 않은 변경 사항을 보여준다.
반면,
git diff --cached
는 Staging Area와 마지막 커밋(HEAD)의 차이를 보여준다.
즉, 지금 커밋하면 실제로 어떤 변경 사항이 들어갈지를 확인할 수 있다.
--staged도 같은 의미로 사용할 수 있다.
git diff --staged
언제 유용할까?
커밋 직전에 다음을 확인할 때 유용하다.</p>
<ul>
<li>디버깅 코드가 실수로 포함됐는가?</li>
<li>원하지 않는 파일이 staging됐는가?</li>
<li>이번 커밋에 들어갈 변경 범위가 적절한가?</li>
<li>커밋 메시지와 실제 변경 내용이 일치하는가?
핵심 정리<pre><code>git diff
# 아직 add하지 않은 변경 확인
</code></pre></li>
</ul>
<p>git diff --cached</p>
<h1 id="이미-add되어-다음-commit에-들어갈-변경-확인">이미 add되어 다음 commit에 들어갈 변경 확인</h1>
<pre><code>

4. Python 패키지에서 __init__.py를 만드는 이유
Q. 디렉터리를 만들 때마다 __init__.py를 만드는 이유는 뭐야?
__init__.py는 해당 디렉터리를 Python 패키지로 구성할 때 사용하는 파일이다.
예를 들어:</code></pre><p>project/
└── models/
    ├── <strong>init</strong>.py
    ├── sensor.py
    └── equipment.py</p>
<pre><code>패키지를 import하면 __init__.py가 실행될 수 있으며, 패키지에서 외부에 노출할 객체를 정리하는 용도로도 사용할 수 있다.
예를 들어:</code></pre><h1 id="modelsinitpy">models/<strong>init</strong>.py</h1>
<p>from .sensor import SensorEvent
from .equipment import EquipmentConfig</p>
<pre><code>그러면 사용하는 쪽에서는 내부 파일을 각각 지정하는 대신 다음처럼 가져올 수 있다.</code></pre><p>from models import SensorEvent, EquipmentConfig</p>
<pre><code>Python 3.3 이후에는 namespace package가 도입되어 특정 경우 __init__.py 없이도 import가 가능하다. 따라서 모든 디렉터리에 무조건 필요한 파일은 아니다.
그래도 일반적인 Python 프로젝트에서는 다음 이유로 자주 사용한다.
- 명시적인 패키지 구조 표현
- 패키지 초기화 코드 실행
- 외부에 공개할 API 정리
- import 경로 단순화
5. Malformed frame이란?
Q. malformed frame은 무슨 의미지?
malformed는 정해진 형식에 맞지 않는, frame은 통신 프로토콜이 해석하는 데이터 단위를 의미한다.
따라서 malformed frame은 대략 다음 의미다.
수신한 데이터를 프로토콜 규칙에 따라 정상적으로 해석할 수 없다.

Kafka 같은 시스템에서 이런 오류가 발생한다면 단순히 &quot;Avro 데이터가 깨졌다&quot;는 의미로 한정해서 보면 안 된다.
예를 들어 다음과 같은 원인이 있을 수 있다.
- 클라이언트와 서버의 보안 프로토콜 불일치
- 잘못된 포트로 접속
- 예상한 네트워크 프로토콜과 실제 프로토콜이 다름
- 전송 데이터가 손상되거나 잘못된 형식으로 구성됨
예를 들어 Kafka broker가 SASL_SSL 연결을 기대하는데 클라이언트가 PLAINTEXT로 접속하면 서로 다른 프로토콜을 해석하려 하므로 통신 오류가 발생할 수 있다.
핵심
Malformed frame을 만나면 먼저 다음을 확인하는 것이 좋다.
1. host / port
2. security.protocol
3. SASL / SSL 설정
4. producer와 consumer의 protocol 설정
5. 실제 payload format
6. bytes를 int로 변환하는 방법
Q. bytes를 int로 쓰려면 그냥 int()로 변환하면 되나?
두 경우를 구분해야 한다.
1. bytes가 숫자 문자열을 표현하는 경우</code></pre><p>value = b&quot;123&quot;</p>
<p>print(int(value))</p>
<h1 id="123">123</h1>
<pre><code>이 경우 b&quot;123&quot;은 ASCII/UTF-8 기준으로 숫자 문자 &quot;123&quot;을 표현하고 있기 때문에 int()로 변환할 수 있다.
2. bytes 자체를 이진 정수로 해석하는 경우</code></pre><p>raw = b&quot;\x01\x02&quot;</p>
<p>value = int.from_bytes(raw, byteorder=&quot;big&quot;)</p>
<p>print(value)</p>
<h1 id="258">258</h1>
<pre><code>계산하면:
0x01 0x02
= 1 × 256 + 2
= 258

Little-endian으로 해석하면 결과가 달라진다.</code></pre><p>int.from_bytes(raw, byteorder=&quot;little&quot;)</p>
<h1 id="513">513</h1>
<pre><code>부호 있는 정수라면:
int.from_bytes(raw, byteorder=&quot;big&quot;, signed=True)
처럼 signed=True를 사용할 수 있다.
핵심 정리
int(b&quot;123&quot;)
은 문자열 &quot;123&quot;을 숫자 123으로 변환하는 것이고,
int.from_bytes(...)
는 바이트 배열 자체를 특정 byte order에 따라 정수로 해석하는 것이다.
7. json.loads()의 역할
Q. json.loads()는 무슨 역할을 수행해?
json.loads()는 **JSON 문자열을 Python 객체로 역직렬화(deserialization)**한다.
예를 들어:
</code></pre><p>import json</p>
<p>json_string = &#39;{&quot;name&quot;: &quot;Yuchan&quot;, &quot;role&quot;: &quot;Data Engineer&quot;}&#39;</p>
<p>data = json.loads(json_string)</p>
<p>print(data)</p>
<h1 id="name-yuchan-role-data-engineer">{&#39;name&#39;: &#39;Yuchan&#39;, &#39;role&#39;: &#39;Data Engineer&#39;}</h1>
<p>print(data[&quot;role&quot;])</p>
<h1 id="data-engineer">Data Engineer</h1>
<pre><code>변환 흐름은 다음과 같다.</code></pre><p>JSON String
    ↓ json.loads()
Python dict/list/str/int/...</p>
<pre><code>loads의 s는 string을 의미한다.
파일 객체에서 JSON을 직접 읽을 때는 json.load()를 사용할 수 있다.</code></pre><p>with open(&quot;data.json&quot;) as f:
    data = json.load(f)</p>
<pre><code>8. 왜 schema가 아니라 schema.schema_str을 사용할까?
Q. schema 자체가 JSON처럼 보이는데 왜 schema.schema_str을 사용하지?
중요한 것은 출력 모양이 아니라 실제 Python 타입이다.
Schema Registry 클라이언트가 반환한 schema가 라이브러리의 Schema 객체라면, 객체 내부에 실제 스키마 문자열이 별도 속성으로 들어 있을 수 있다.
개념적으로 보면:</code></pre><p>schema
└── Schema 객체
    ├── schema_str
    ├── schema_type
    └── ...</p>
<pre><code>schema.schema_str은 이 객체에서 실제 스키마 문자열을 꺼내는 것이다.
예를 들어 스키마 문자열이 다음과 같다면:</code></pre><p>{
  &quot;type&quot;: &quot;record&quot;,
  &quot;name&quot;: &quot;SensorEvent&quot;,
  &quot;fields&quot;: [
    {&quot;name&quot;: &quot;event_id&quot;, &quot;type&quot;: &quot;string&quot;}
  ]
}</p>
<pre><code>코드에서는 다음 흐름으로 처리할 수 있다.</code></pre><p>schema_dict = json.loads(schema.schema_str)</p>
<pre><code>즉:</code></pre><p>Schema 객체
   ↓ .schema_str
JSON 문자열
   ↓ json.loads()
Python dict</p>
<pre><code>핵심
print(schema)가 JSON처럼 보인다고 해서 schema 자체가 JSON 문자열이라는 뜻은 아니다.
항상 다음처럼 실제 타입을 확인하는 습관이 좋다.</code></pre><p>print(type(schema))
print(type(schema.schema_str))</p>
<pre><code>9. fastavro의 parse_schema()
Q. parse_schema()는 무슨 함수야?
fastavro.parse_schema()는 Python dictionary 형태의 Avro schema를 받아 fastavro가 사용할 수 있도록 파싱하고 준비하는 함수다.
예를 들어:</code></pre><p>from fastavro import parse_schema</p>
<p>schema = {
    &quot;type&quot;: &quot;record&quot;,
    &quot;name&quot;: &quot;SensorEvent&quot;,
    &quot;fields&quot;: [
        {&quot;name&quot;: &quot;event_id&quot;, &quot;type&quot;: &quot;string&quot;}
    ]
}</p>
<pre><code>parsed_schema = parse_schema(schema)
전체 흐름을 연결하면:</code></pre><p>Schema Registry
      ↓
schema.schema_str
      ↓
JSON String
      ↓ json.loads()
Python dict
      ↓ parse_schema()
Parsed Avro Schema</p>
<pre><code>parse_schema()는 스키마 구조를 파싱하면서 named schema 등의 정보를 fastavro가 처리할 수 있는 형태로 준비한다.
따라서 데이터를 bytes로 변환하는 함수는 아니다.

10. 왜 io.BytesIO(avro_body)를 사용할까?
Q. buffer = io.BytesIO(avro_body)를 선언하는 이유는 뭐야?
avro_body가 다음처럼 bytes라고 하자.
avro_body = b&quot;...&quot;
이 데이터는 메모리에 있는 단순한 bytes 객체다.
그런데 schemaless_reader()는 첫 번째 인자로 file-like object를 받는다.
따라서:</code></pre><p>import io</p>
<p>buffer = io.BytesIO(avro_body)</p>
<pre><code>를 사용해 bytes를 메모리 기반 파일처럼 읽을 수 있는 객체로 감싼다.
이제 다음과 같은 동작이 가능하다.
buffer.read(4)
buffer.read(8)
그리고 내부적으로 현재 어디까지 읽었는지 cursor 위치도 관리된다.</code></pre><p>bytes
  ↓ io.BytesIO()
file-like object
  ↓
schemaless_reader()</p>
<pre><code>실제 임시 파일을 디스크에 만들 필요가 없기 때문에 메모리에서 바로 처리할 수 있다.
11. parse_schema()의 반환값은 bytes일까?
Q. parse_schema()의 반환값이 bytes야?
아니다.
parse_schema()의 결과는 파싱된 Avro schema 구조다.
즉:
parsed_schema = parse_schema(schema)
에서 parsed_schema는 데이터를 직렬화한 결과가 아니다.
실제 bytes는 데이터를 직렬화할 때 만들어진다.
예를 들어:</code></pre><p>import io
from fastavro import parse_schema, schemaless_writer</p>
<p>parsed_schema = parse_schema(schema)</p>
<p>buffer = io.BytesIO()</p>
<p>schemaless_writer(
    buffer,
    parsed_schema,
    actual_data,
)</p>
<p>result = buffer.getvalue()</p>
<pre><code>흐름을 보면:</code></pre><p>schema
  ↓ parse_schema()
parsed schema</p>
<p>actual_data + parsed schema
  ↓ schemaless_writer()
buffer
  ↓ getvalue()
bytes</p>
<pre><code>핵심 정리
parse_schema()는 데이터를 만드는 함수가 아니라 데이터를 어떻게 읽고 쓸지 정의하는 스키마를 준비하는 함수다.

12. schemaless_reader()는 무엇을 할까?
Q. schemaless_reader() 함수에 대해서 설명해줘.
fastavro.schemaless_reader()는 Avro binary 데이터를 주어진 schema를 사용해 Python 객체로 역직렬화하는 함수다.
예를 들어:</code></pre><p>decoded_data = schemaless_reader(
    buffer,
    parsed_schema,
)</p>
<pre><code>입력은 다음과 같다.</code></pre><p>buffer
→ Avro binary data</p>
<p>parsed_schema
→ 데이터를 해석할 Avro schema
결과는 Python에서 다룰 수 있는 값이다.
Avro bytes
    +
Avro schema
    ↓
schemaless_reader()
    ↓
Python object</p>
<pre><code>왜 schemaless일까?
Avro Object Container File은 파일 자체에 스키마 등의 메타데이터를 포함할 수 있다.
하지만 Kafka 메시지처럼 개별 레코드를 Avro binary로 전달하는 방식에서는 메시지 본문에 전체 Avro schema를 매번 넣지 않고, 별도의 Schema Registry를 사용하는 구성이 흔하다.
이 경우 consumer는 schema를 외부에서 가져와 payload를 해석해야 한다.
전체 흐름은 다음과 같이 볼 수 있다.</code></pre><p>import io
import json
from fastavro import parse_schema, schemaless_reader</p>
<h1 id="1-registry에서-가져온-schema-문자열">1. Registry에서 가져온 schema 문자열</h1>
<p>schema_dict = json.loads(schema.schema_str)</p>
<h1 id="2-fastavro용-schema-준비">2. fastavro용 schema 준비</h1>
<p>parsed_schema = parse_schema(schema_dict)</p>
<h1 id="3-avro-payload를-file-like-object로-변환">3. Avro payload를 file-like object로 변환</h1>
<p>buffer = io.BytesIO(avro_body)</p>
<h1 id="4-역직렬화">4. 역직렬화</h1>
<p>decoded = schemaless_reader(buffer, parsed_schema)</p>
<pre><code>정리하면:</code></pre><p>Schema Registry
      ↓
schema.schema_str
      ↓
json.loads()
      ↓
parse_schema()
      ↓
parsed_schema
          <br>           + → schemaless_reader() → Python object
          /
Avro bytes
   ↓
BytesIO</p>
<pre><code>지금까지 따로 봤던 schema_str, json.loads(), parse_schema(), BytesIO, schemaless_reader()가 하나의 역직렬화 과정으로 연결된다.
13. datetime.fromisoformat()
Q. fromisoformat() 함수는 무슨 역할을 해?
fromisoformat()은 ISO 8601 형식의 날짜/시간 문자열을 Python의 date 또는 datetime 객체로 변환할 때 사용한다.</code></pre><p>from datetime import datetime</p>
<p>value = &quot;2026-10-05T16:58:42&quot;</p>
<p>dt = datetime.fromisoformat(value)</p>
<p>print(dt)</p>
<h1 id="2026-10-05-165842">2026-10-05 16:58:42</h1>
<pre><code>날짜만 처리할 수도 있다.</code></pre><p>from datetime import date</p>
<p>d = date.fromisoformat(&quot;2026-10-05&quot;)
strptime()을 사용한다면 포맷을 직접 지정해야 한다.
datetime.strptime(
    &quot;2026-10-05 16:58:42&quot;,
    &quot;%Y-%m-%d %H:%M:%S&quot;,
)</p>
<pre><code>반면 ISO 형식이라면:
datetime.fromisoformat(&quot;2026-10-05T16:58:42&quot;)
처럼 간결하게 처리할 수 있다.
14. tzinfo와 Naive/Aware datetime
Q. tzinfo는 timezone에 대한 정보지?
그렇다.
tzinfo는 datetime 객체가 어느 UTC offset/timezone을 기준으로 하는지 나타내는 시간대 정보다.
Python에서는 datetime을 크게 Naive와 Aware로 구분한다.
Naive datetime</code></pre><p>from datetime import datetime</p>
<p>dt = datetime.now()</p>
<p>print(dt.tzinfo)</p>
<h1 id="none">None</h1>
<pre><code>시간 값은 있지만 어느 시간대인지 알 수 없다.
2026-10-05 17:03:00
이 값만 보면 UTC인지 KST인지 판단할 수 없다.
Aware datetime</code></pre><p>from datetime import datetime, timezone</p>
<p>dt = datetime.now(timezone.utc)</p>
<pre><code>출력 예:
2026-10-05 08:03:00+00:00
+00:00이라는 UTC offset이 들어 있다.
데이터 엔지니어링에서 왜 중요할까?
여러 지역의 서버와 시스템이 데이터를 생성하면 로컬 시간만 저장할 경우 시간 순서가 꼬일 수 있다.
예를 들어:
한국 서버: 2026-10-05 17:00
미국 서버: 2026-10-05 04:00
timezone 정보가 없다면 두 시간이 실제로 어떤 시점을 의미하는지 정확히 비교하기 어렵다.
따라서 이벤트 시간은 UTC 기반의 aware timestamp로 관리하고, 표시 단계에서 필요한 로컬 timezone으로 변환하는 방식이 흔하다.
15. Naive와 Aware를 구분하는 방법
Q. Naive일 때랑 Aware일 때 어떻게 구분하지?
가장 간단하게는 tzinfo와 UTC offset을 확인한다.</code></pre><p>from datetime import datetime, timezone</p>
<p>naive_dt = datetime.now()
aware_dt = datetime.now(timezone.utc)</p>
<p>print(naive_dt.tzinfo)</p>
<h1 id="none-1">None</h1>
<p>print(aware_dt.tzinfo)</p>
<h1 id="utc">UTC</h1>
<pre><code>보다 명확하게 판별하려면:</code></pre><p>def is_aware(dt):
    return (
        dt.tzinfo is not None
        and dt.utcoffset() is not None
    )</p>
<pre><code>사용:</code></pre><p>print(is_aware(naive_dt))</p>
<h1 id="false">False</h1>
<p>print(is_aware(aware_dt))</p>
<h1 id="true">True</h1>
<pre><code>출력 결과에서도 대략 확인할 수 있다.
Naive
2026-10-05 17:03:00

Aware
2026-10-05 17:03:00+09:00
뒤에 +00:00, +09:00 같은 UTC offset이 붙어 있다면 aware datetime이라고 볼 수 있다.
16. timezone.utc와 astimezone()
Q. astimezone(), timezone.utc에 대해서 설명해줘.
두 기능은 비슷해 보이지만 역할이 다르다.
timezone.utc
UTC를 나타내는 timezone 객체다.</code></pre><p>from datetime import datetime, timezone</p>
<p>utc_time = datetime.now(timezone.utc)</p>
<p>print(utc_time)</p>
<pre><code>예:
2026-10-05 08:05:00+00:00
즉 timezone.utc는 UTC라는 시간대 자체를 표현하는 객체다.
astimezone()
이미 시간대 정보가 있는 datetime을 다른 timezone 기준으로 변환한다.</code></pre><p>from datetime import datetime, timezone, timedelta</p>
<p>utc_time = datetime.now(timezone.utc)</p>
<p>kst = timezone(timedelta(hours=9))</p>
<p>kst_time = utc_time.astimezone(kst)</p>
<pre><code>예를 들어:</code></pre><p>UTC
2026-10-05 08:05:00+00:00</p>
<pre><code>    ↓ astimezone(KST)</code></pre><p>KST
2026-10-05 17:05:00+09:00</p>
<pre><code>중요한 점은 astimezone()이 단순히 tzinfo 꼬리표만 바꾸는 것이 아니라는 것이다.
같은 절대 시점을 유지하면서 해당 timezone의 로컬 시간으로 변환한다.
즉:</code></pre><h1 id="0805-utc">08:05 UTC</h1>
<p>17:05 KST</p>
<pre><code>두 값은 표시되는 시각만 다르고 실제로는 같은 순간이다.
전체 흐름 다시 보기
이번에 공부한 내용 중 Avro 역직렬화 부분을 한 번에 연결하면 다음과 같다.</code></pre><p>Kafka Message
      ↓
bytes payload
      ↓
Avro framing/header 처리
      ↓
schema id 확인
      ↓
Schema Registry 조회
      ↓
Schema 객체
      ↓ .schema_str
JSON String
      ↓ json.loads()
Python dict
      ↓ parse_schema()
Parsed Avro Schema
      ↓
schemaless_reader(
    BytesIO(avro_body),
    parsed_schema
)
      ↓
Python Object
      ↓
timestamp parsing
      ↓ datetime.fromisoformat()
Aware datetime
      ↓ astimezone(timezone.utc)
UTC 기준 시간
      ↓
Spark / Table 적재</p>
<pre><code>그리고 Spark Structured Streaming에서는 처리 진행 상태를 checkpoint에 남긴다.</code></pre><p>Kafka
  ↓
Spark Structured Streaming
  ├── checkpoint
  │    ├── offsets
  │    └── commits
  │
  ↓
Canonical Table</p>
<p>```
결국 각각의 개념은 따로 떨어져 있는 것이 아니라,
Kafka에서 들어온 binary 데이터를 schema에 맞게 역직렬화하고 → 시간 정보를 표준화하고 → Spark가 처리한 결과를 테이블에 저장하면서 → checkpoint로 처리 상태를 관리하는 하나의 데이터 파이프라인 흐름
으로 연결된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[spark 내 array 함수]]></title>
            <link>https://velog.io/@o_u--chan/spark-%EB%82%B4-array-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@o_u--chan/spark-%EB%82%B4-array-%ED%95%A8%EC%88%98</guid>
            <pubDate>Fri, 14 Aug 2026 07:20:21 GMT</pubDate>
            <description><![CDATA[<ul>
<li><code>collect_list()</code> : 값들을 문자열로 모은다.</li>
<li><code>concat_ws()</code> : 배열을 특정 구분자로 합친다. ( ws = with seperator)</li>
<li><code>collect_set()</code> : 중복 값이 제거된 상태로 모은다.(집합처럼)</li>
<li><code>array_sort()</code> : 배열 내부를 정렬한다.(default = asc)</li>
<li><code>reverse()</code> : 배열 순서 뒤집기</li>
<li><code>array_remove()</code> : 배열 내부 특정 값 제거</li>
<li><code>array_compact()</code> : 배열 내 모든 Null 제거</li>
<li><code>array_distinct()</code> : 배열 중 중복되는 값들 중 첫번째 값만 남긴다</li>
</ul>
<pre><code class="language-python">from pyspark.sql import functions as F

df = tickets.withColumn(&quot;tags&quot;, F.array_remove(F.col(&quot;tags&quot;), &quot;misc&quot;))
df = df.withColumn(&quot;tags&quot;, F.array_distinct(F.col(&quot;tags&quot;)))
df_result = df.withColumn(&quot;tag_count&quot;, F.size(F.col(&quot;tags&quot;))) \
  .filter(F.size(F.col(&quot;tags&quot;)) &gt; 0) \
  .sort(F.col(&quot;ticket_id&quot;).asc())</code></pre>
<pre><code class="language-python">df_result = df.groupBy(&quot;category&quot;, &quot;sub_category&quot;).agg( \
  F.concat_ws(&quot;, &quot;, F.array_sort(F.collect_set(&quot;amount&quot;))).alias(&quot;amount&quot;) \
)</code></pre>
<p>```</p>
]]></description>
        </item>
        <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>
    </channel>
</rss>