<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>taeyo_on.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 22 Dec 2025 04:49:16 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>taeyo_on.log</title>
            <url>https://velog.velcdn.com/images/taeyoon__/profile/d2b7696f-1cac-4b8c-b654-af8cde088f09/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. taeyo_on.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/taeyoon__" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[solvesql] 다음날도 서울숲의 미세먼지 농도는 나쁨]]></title>
            <link>https://velog.io/@taeyoon__/solvesql-%EB%8B%A4%EC%9D%8C%EB%82%A0%EB%8F%84-%EC%84%9C%EC%9A%B8%EC%88%B2%EC%9D%98-%EB%AF%B8%EC%84%B8%EB%A8%BC%EC%A7%80-%EB%86%8D%EB%8F%84%EB%8A%94-%EB%82%98%EC%81%A8</link>
            <guid>https://velog.io/@taeyoon__/solvesql-%EB%8B%A4%EC%9D%8C%EB%82%A0%EB%8F%84-%EC%84%9C%EC%9A%B8%EC%88%B2%EC%9D%98-%EB%AF%B8%EC%84%B8%EB%A8%BC%EC%A7%80-%EB%86%8D%EB%8F%84%EB%8A%94-%EB%82%98%EC%81%A8</guid>
            <pubDate>Mon, 22 Dec 2025 04:49:16 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>서울숲 일별 평균 대기오염도 데이터베이스는 2022년 서울숲 대기오염도 측정소에서 매일 기록한 대기오염 정보를 담고 있습니다.</p>
<p>measurements 테이블의 pm10 컬럼에는 다양한 대기오염도 측정 기준 중에서도 미세먼지(PM10) 농도가 기록되어 있습니다. 이 데이터를 이용하여 당일의 미세먼지 농도보다 바로 다음날의 미세먼지 농도가 더 안좋은 날을 찾아주세요. 결과는 아래 컬럼들을 포함해야 합니다.</p>
<blockquote>
</blockquote>
<ul>
<li>today: 당일 (YYYY-MM-DD)</li>
<li>next_day: 다음날 (YYYY-MM-DD)</li>
<li>pm10: 당일의 미세먼지 농도</li>
<li>next_pm10: 다음날의 미세먼지 농도</li>
</ul>
<h2 id="풀이">풀이</h2>
<pre><code>WITH t AS
  (SELECT measured_at today,
          lead(measured_at) over(order by measured_at) next_day,
          pm10,
          lead(pm10) over(order by measured_at) next_pm10
  FROM measurements
  )

SELECT *
FROM t
WHERE next_pm10 &gt; pm10;</code></pre><p>이번에 SQL감을 다시 찾으려 solvesql 풀었던 문제를 다시 풀어 보고 있다.
사실 문제를 봤을때 원래는 머릿속에서 풀이가 정리되었지만, 
지금은 바로 떠오르지는 않았다.</p>
<p>그래도 조금 생각해봤을때, 윈도우 함수의 lead를 생각나서 적용했다.
문제를 잘못봐서 WHERE 구문을 반대로 작성해서 2번 정도 틀렸다ㅜㅜ
그래도 일주일 정도 1문제씩 풀면 원래 실력은 다시 나올것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[오늘의집 지원 후기]]></title>
            <link>https://velog.io/@taeyoon__/%EC%98%A4%EB%8A%98%EC%9D%98%EC%A7%91-%EC%A7%80%EC%9B%90-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@taeyoon__/%EC%98%A4%EB%8A%98%EC%9D%98%EC%A7%91-%EC%A7%80%EC%9B%90-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 02 Sep 2025 04:07:17 GMT</pubDate>
            <description><![CDATA[<p>오늘의집 DA 인턴 공고가 나왔길래 JD나 기술 블로그의 글을 봤을때, 나와 fit하다고 생각이 들어 지원하게 되었다.</p>
<p>솔직히 취준 기간 동안 서류에서 탈락을 많이 해서 자존감이 낮아진 상황이었는데, 다행히 서류합격을 해서 코테를 볼 수 있었다.
<img src="https://velog.velcdn.com/images/taeyoon__/post/833bf771-b952-4a75-88d5-c7e816fd7fbc/image.png" alt=""></p>
<p>처음 본 코테는 기존의 코테 사이트의 문제와는 조금 거리가 있었고 실무에서 사용할 만한 쿼리를 요구했다.</p>
<p>그런 쿼리들은 프로젝트나 개인 공부를 통해 여러번 작성한 경험이 있어서 그렇게 어렵게 느껴지진 않았다.</p>
<p>결과는 당연히 합격!</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/3d518227-bace-49e1-9df4-ed1e9fb5f4be/image.png" alt=""></p>
<p>인터뷰를 준비할 때는 지원동기, 자기소개, 프로젝트나 지금까지 했던 경험을 정리하면서 준비했던 것 같다
<img src="https://velog.velcdn.com/images/taeyoon__/post/c1a0c149-1da6-4b3e-963b-4eabb9b94ed9/image.png" alt=""></p>
<p>면접 당일에 찍은 사진!</p>
<p>면접 결과는???</p>
<p>안탑깝게도 탈락...
사실 메일 제목을 보고 어느정도 예감해서 크게 충격은 없었다.
그래도 면접까지 갔다는 거는 최소한 서류나 내 실력은 크게 문제 없다는 생각을 들게 해줘서 좋았다.(합격했으면 더 좋았겠지만)</p>
<p>그래도 조금 더 힘을 내자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[재귀 CTE 쿼리]]></title>
            <link>https://velog.io/@taeyoon__/%EC%9E%AC%EA%B7%80-CTE-%EC%BF%BC%EB%A6%AC</link>
            <guid>https://velog.io/@taeyoon__/%EC%9E%AC%EA%B7%80-CTE-%EC%BF%BC%EB%A6%AC</guid>
            <pubDate>Mon, 04 Aug 2025 04:20:55 GMT</pubDate>
            <description><![CDATA[<p>최근 recursive CTE를 활용한 쿼리를 작성해서 정리하려고 한다.</p>
<h3 id="문제-설명">문제 설명</h3>
<blockquote>
</blockquote>
<p>각 세대별 자식이 없는 개체의 수(COUNT)와 세대(GENERATION)를 출력하는 SQL문을 작성해주세요. 이때 결과는 세대에 대해 오름차순 정렬해주세요.
단, 모든 세대에는 자식이 없는 개체가 적어도 1개체는 존재합니다.</p>
<table>
<thead>
<tr>
<th>컬럼명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ID</code></td>
<td>INTEGER</td>
<td>대장균 개체 ID</td>
</tr>
<tr>
<td><code>PARENT_ID</code></td>
<td>INTEGER</td>
<td>부모 개체 ID (<code>NULL</code> 가능)</td>
</tr>
<tr>
<td><code>SIZE_OF_COLONY</code></td>
<td>INTEGER</td>
<td>개체의 크기</td>
</tr>
<tr>
<td><code>DIFFERENTIATION_DATE</code></td>
<td>DATE</td>
<td>분화된 날짜</td>
</tr>
<tr>
<td><code>GENOTYPE</code></td>
<td>INTEGER</td>
<td>형질 (비트 기반 인코딩)</td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
<p>이 문제에서는 generation 컬럼을 만들기 위해서는 recursive CTE 작성이 필수</p>
<h3 id="recursive-cte">RECURSIVE CTE</h3>
<p>WITH RECURSIVE 쿼리문을 작성하고 내부에 UNION을 활용해서 재귀를 구성한다
<img src="https://velog.velcdn.com/images/taeyoon__/post/2cb78684-8b1f-4335-b689-5ab1764121f3/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Anchor 쿼리</td>
<td>재귀의 시작점 (기준 노드)</td>
</tr>
<tr>
<td>Recursive 쿼리</td>
<td>자기 자신을 반복 호출하며 계층 확장</td>
</tr>
<tr>
<td><code>UNION ALL</code></td>
<td>재귀 호출을 이어 붙임 (중복 허용 시 <code>ALL</code>)</td>
</tr>
<tr>
<td>종료 조건</td>
<td>재귀 조건 안의 <code>WHERE</code> 또는 깊이 제한</td>
</tr>
</tbody></table>
<p>주의사항
종료 조건이 없다면 무한 루프 발생 (무조건 WHERE 등으로 제한 필요)</p>
<p>DBMS마다 RECURSIVE 키워드를 써야 할 수도 있고 안 써도 되는 경우도 있음
(예: PostgreSQL: WITH RECURSIVE, MySQL: WITH RECURSIVE, SQL Server: WITH)</p>
<p>MySQL은 기본적으로 1000회 재귀 제한 (max_recursion_depth)</p>
<p>UNION ALL 대신 UNION을 쓰면 중복 제거되나 성능 저하 가능</p>
<h3 id="정답-쿼리">정답 쿼리</h3>
<pre><code>WITH recursive t as (
    SELECT id, 1 generation
    FROM ecoli_data ed
    WHERE parent_id is null

    UNION ALL

    SELECT ecoli_data.id, generation + 1
    FROM ecoli_data
    JOIN t on ecoli_data.parent_id = t.id
)

SELECT count(*) `count`, generation 
FROM t
WHERE id not in (SELECT parent_id
                FROM ecoli_data
                WHERE parent_id is not null)
GROUP BY 2</code></pre><h3 id="리뷰">리뷰</h3>
<blockquote>
</blockquote>
<p>Anchor 쿼리: 최초 대장균 개체 (parent_id가 없는 개체)를 generation 1로 시작</p>
<blockquote>
</blockquote>
<p>Recursive 쿼리: 부모 개체를 따라가며 generation을 1씩 증가시킴</p>
<blockquote>
</blockquote>
<p><strong>마지막 조회하는 구문에서 not null 조건을 안넣으면 in 조건에서는 어떤 것도 반환하지 않음</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[비트 연산자 활용 쿼리 리뷰]]></title>
            <link>https://velog.io/@taeyoon__/%EB%B9%84%ED%8A%B8-%EC%97%B0%EC%82%B0%EC%9E%90-%ED%99%9C%EC%9A%A9-%EC%BF%BC%EB%A6%AC-%EB%A6%AC%EB%B7%B0</link>
            <guid>https://velog.io/@taeyoon__/%EB%B9%84%ED%8A%B8-%EC%97%B0%EC%82%B0%EC%9E%90-%ED%99%9C%EC%9A%A9-%EC%BF%BC%EB%A6%AC-%EB%A6%AC%EB%B7%B0</guid>
            <pubDate>Wed, 30 Jul 2025 05:45:47 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-1-프로그래머스">문제 1: 프로그래머스</h2>
<table>
<thead>
<tr>
<th>컬럼명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ID</code></td>
<td>INTEGER</td>
<td>대장균 개체 ID</td>
</tr>
<tr>
<td><code>PARENT_ID</code></td>
<td>INTEGER</td>
<td>부모 개체 ID (<code>NULL</code> 가능)</td>
</tr>
<tr>
<td><code>SIZE_OF_COLONY</code></td>
<td>INTEGER</td>
<td>개체의 크기</td>
</tr>
<tr>
<td><code>DIFFERENTIATION_DATE</code></td>
<td>DATE</td>
<td>분화된 날짜</td>
</tr>
<tr>
<td><code>GENOTYPE</code></td>
<td>INTEGER</td>
<td>형질 (비트 기반 인코딩)</td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
<p>🎯 문제 목표
다음 조건을 모두 만족하는 대장균 개체의 수(COUNT)를 구하라.</p>
<blockquote>
</blockquote>
<p>2번 형질을 보유하지 않음</p>
<blockquote>
</blockquote>
<p>1번 또는 3번 형질을 보유함</p>
<blockquote>
</blockquote>
<p>1번과 3번을 동시에 보유해도 포함</p>
<blockquote>
</blockquote>
<h3 id="접근-방식">접근 방식</h3>
<p>초반에는 genotype에서 나머지 연산자를 활용해서 쿼리를 작성함
-&gt; 쿼리를 이해하기 어려워서 비트 연산자 활용해서 문제를 풀었음 </p>
<h3 id="정답-쿼리">정답 쿼리</h3>
<pre><code>SELECT count(*) `count`
FROM ecoli_data
WHERE genotype &amp; 2 = 0
AND (genotype &amp; 4 &gt; 0 or genotype &amp; 1 &gt; 0)</code></pre><blockquote>
</blockquote>
<p>genotype &amp; 2 인 이유: 2번 형질을 포함하지 않아야 하니까 = 0으로 조건
genotype &amp; 4 &gt; 0 or genotype &amp; 1 &gt; 0:
genotype  &amp; 4 = 1로 하면 세번째 비트 값만 켜질 때만 필터링되서 &gt; 0으로 설정하면 모두 포함됨</p>
<hr>
<h2 id="문제-2-프로그래머스">문제 2: 프로그래머스</h2>
<blockquote>
</blockquote>
<p>DEVELOPERS 테이블에서 Front End 스킬을 가진 개발자의 정보를 조회하려 합니다. 
조건에 맞는 개발자의 ID, 이메일, 이름, 성을 조회하는 SQL 문을 작성해 주세요.
결과는 ID를 기준으로 오름차순 정렬해 주세요.</p>
<table>
<thead>
<tr>
<th>컬럼명</th>
<th>타입</th>
<th>제약조건</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>NAME</code></td>
<td><code>VARCHAR(N)</code></td>
<td><code>UNIQUE</code>, <code>NOT NULL</code></td>
<td>스킬 이름 (예: Python, HTML 등)</td>
</tr>
<tr>
<td><code>CATEGORY</code></td>
<td><code>VARCHAR(N)</code></td>
<td><code>NOT NULL</code></td>
<td>스킬 범주 (예: Front End, Back End 등)</td>
</tr>
<tr>
<td><code>CODE</code></td>
<td><code>INTEGER</code></td>
<td><code>UNIQUE</code>, <code>NOT NULL</code></td>
<td>스킬 코드 (2의 제곱수: 1, 2, 4, 8, 16 등)</td>
</tr>
</tbody></table>
<br>

<table>
<thead>
<tr>
<th>컬럼명</th>
<th>타입</th>
<th>제약조건</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ID</code></td>
<td><code>VARCHAR(N)</code></td>
<td><code>UNIQUE</code>, <code>NOT NULL</code></td>
<td>개발자 ID</td>
</tr>
<tr>
<td><code>FIRST_NAME</code></td>
<td><code>VARCHAR(N)</code></td>
<td><code>NULLABLE</code></td>
<td>이름</td>
</tr>
<tr>
<td><code>LAST_NAME</code></td>
<td><code>VARCHAR(N)</code></td>
<td><code>NULLABLE</code></td>
<td>성</td>
</tr>
<tr>
<td><code>EMAIL</code></td>
<td><code>VARCHAR(N)</code></td>
<td><code>UNIQUE</code>, <code>NOT NULL</code></td>
<td>이메일 주소</td>
</tr>
<tr>
<td><code>SKILL_CODE</code></td>
<td><code>INTEGER</code></td>
<td><code>NOT NULL</code></td>
<td>보유한 스킬들의 비트 코드 합산값</td>
</tr>
</tbody></table>
<h3 id="접근-방식-1">접근 방식</h3>
<blockquote>
</blockquote>
<p>비트 연산자를 사용해 어떤 스킬이 포함되어 있는지 확인
Front End 기술이 포함된 경우만 필터링 하고 &amp; &gt; 0을 활용해 기술을 포함하는지 확인
Exists 사용 이유: Exists는 조건을 만족하는 서브쿼리 결과가 1개라도 존재한면 true (Front End 기술이 1개라도 있으면 결과에 포함)</p>
<h3 id="정답-쿼리-1">정답 쿼리</h3>
<pre><code>SELECT id, email, first_name, last_name
FROM developers d
WHERE exists (SELECT 1
             FROM skillcodes s
             WHERE category = &#39;Front End&#39;
             AND (s.code &amp; d.skill_code) &gt; 0
             )
ORDER BY 1;</code></pre><hr>
<h2 id="문제-3-프로그래머스">문제 3: 프로그래머스</h2>
<blockquote>
</blockquote>
<p>부모의 형질을 모두 보유한 대장균의 ID(ID), 대장균의 형질(GENOTYPE), 부모 대장균의 형질(PARENT_GENOTYPE)을 출력하는 SQL 문을 작성해주세요. 
이때 결과는 ID에 대해 오름차순 정렬해주세요.</p>
<table>
<thead>
<tr>
<th>컬럼명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ID</code></td>
<td>INTEGER</td>
<td>대장균 개체 ID</td>
</tr>
<tr>
<td><code>PARENT_ID</code></td>
<td>INTEGER</td>
<td>부모 개체 ID (<code>NULL</code> 가능)</td>
</tr>
<tr>
<td><code>SIZE_OF_COLONY</code></td>
<td>INTEGER</td>
<td>개체의 크기</td>
</tr>
<tr>
<td><code>DIFFERENTIATION_DATE</code></td>
<td>DATE</td>
<td>분화된 날짜</td>
</tr>
<tr>
<td><code>GENOTYPE</code></td>
<td>INTEGER</td>
<td>형질 (비트 기반 인코딩)</td>
</tr>
</tbody></table>
<h3 id="접근-방식-2">접근 방식</h3>
<blockquote>
</blockquote>
<p>부모의 genotype 컬럼을 추가하는 CTE 구문을 작성한 후,
비트 연산자를 활용해서 자식의 genotype이 부모의 genotype을 포함한 것만 필터링.</p>
<h3 id="정답-쿼리-2">정답 쿼리</h3>
<pre><code>WITH t as
    (SELECT id, parent_id, genotype, (SELECT genotype
                                     FROM ecoli_data
                                     WHERE ed.parent_id = ecoli_data.id) parent_genotype
     FROM ecoli_data ed
    )


SELECT id, genotype, parent_genotype
FROM t
WHERE (genotype &amp; parent_genotype) = parent_genotype
ORDER BY 1;</code></pre><blockquote>
</blockquote>
<p>WHERE (genotype &amp; parent_genotype) = parent_genotype
genotype이 parent_genotype의 genotype을 포함</p>
<hr>
<h2 id="문제4-프로그래머스">문제4: 프로그래머스</h2>
<blockquote>
</blockquote>
<p>DEVELOPERS 테이블에서 GRADE별 개발자의 정보를 조회하려 합니다. GRADE는 다음과 같이 정해집니다.</p>
<blockquote>
</blockquote>
<p>A : Front End 스킬과 Python 스킬을 함께 가지고 있는 개발자
B : C# 스킬을 가진 개발자
C : 그 외의 Front End 개발자
GRADE가 존재하는 개발자의 GRADE, ID, EMAIL을 조회하는 SQL 문을 작성해 주세요.</p>
<blockquote>
</blockquote>
<p>결과는 GRADE와 ID를 기준으로 오름차순 정렬해 주세요.</p>
<h3 id="접근-방식-3">접근 방식</h3>
<blockquote>
</blockquote>
<p>CTE 절에서 case 구문을 이용해 grade 컬럼을 만들어야한다
case 구문에서는 비트 연산자를 활용해 가진 기술이나 카테고리의 포함여부를 확인</p>
<blockquote>
</blockquote>
<p>초반에는 case 순서를 C -&gt; B -&gt; A로 설정해서 C로 설정된 row의 경우 C의 조건이 맞던 무시하기 때문에 A가 1명도 없었다</p>
<blockquote>
</blockquote>
<p>복합조건을 앞에 배치해야한다는 사실을 잊고있어서 조금 시간이 걸렸다
case 구문을 A -&gt; B -&gt; C로 설정하니 잘 작동되었다.</p>
<h3 id="정답-쿼리-3">정답 쿼리</h3>
<pre><code>WITH t as
    (SELECT *,
        case when exists (SELECT 1
                         FROM skillcodes s
                         WHERE category = &#39;Front End&#39; and (s.code &amp; d.skill_code) &gt; 0)
                  and exists (SELECT 1
                             FROM skillcodes s
                             WHERE name = &#39;Python&#39; and (s.code &amp; d.skill_code) &gt; 0)
             then &#39;A&#39;

            when exists (SELECT 1
                          FROM skillcodes s
                          WHERE name = &#39;C#&#39; AND (s.code &amp; d.skill_code) &gt; 0) 
             then &#39;B&#39;

             when exists (SELECT 1
                         FROM skillcodes s
                         WHERE category = &#39;Front End&#39; AND (s.code &amp; d.skill_code) &gt; 0)
             then &#39;C&#39;
             end grade
FROM developers d
)

SELECT grade, id, email
FROM t
WHERE grade is not null
ORDER BY 1, 2;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[코딩테스트 리뷰2]]></title>
            <link>https://velog.io/@taeyoon__/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A6%AC%EB%B7%B02</link>
            <guid>https://velog.io/@taeyoon__/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A6%AC%EB%B7%B02</guid>
            <pubDate>Fri, 18 Jul 2025 05:34:05 GMT</pubDate>
            <description><![CDATA[<p>이번에 시간이 조금 걸렸던 sql 코테에 대해서 다시 리뷰하려고 한다.</p>
<h2 id="문제">문제</h2>
<blockquote>
</blockquote>
<h3 id="leetcode-find-product-recommendation-pairs-medium">Leetcode: Find Product Recommendation Pairs [medium]</h3>
<blockquote>
</blockquote>
<p>고객들의 구매 데이터를 분석하여, 같은 고객이 함께 구매한 상품 쌍(product pair) 중공통 구매자가 2명 초과인 상품 조합을 찾아보세요.</p>
<blockquote>
</blockquote>
<p>각 상품 쌍에 대해 다음 정보를 출력해야 합니다:</p>
<blockquote>
</blockquote>
<ul>
<li>product1_id, product2_id: 함께 구매된 상품 ID<blockquote>
</blockquote>
</li>
<li>product1_category, product2_category: 각각 상품의 카테고리<blockquote>
</blockquote>
</li>
<li>customer_count: 두 상품을 모두 구매한 고객 수<blockquote>
</blockquote>
또한, customer_count가 2명 초과인 조합만 필터링하고,
customer_count 기준으로 내림차순 정렬하세요.<blockquote>
</blockquote>
출력 결과
product1_id | product2_id | product1_category | product2_category | customer_cnt</li>
</ul>
<p>ProductPurchases 테이블</p>
<table>
<thead>
<tr>
<th>컬럼명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>user_id</code></td>
<td>INT</td>
<td>상품을 구매한 사용자 ID</td>
</tr>
<tr>
<td><code>product_id</code></td>
<td>INT</td>
<td>구매한 상품 ID</td>
</tr>
<tr>
<td><code>quantity</code></td>
<td>INT</td>
<td>구매 수량</td>
</tr>
</tbody></table>
<p>ProductInfo 테이블</p>
<table>
<thead>
<tr>
<th>컬럼명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>product_id</code></td>
<td>INT</td>
<td>상품 ID (Primary Key)</td>
</tr>
<tr>
<td><code>category</code></td>
<td>VARCHAR</td>
<td>상품 카테고리</td>
</tr>
<tr>
<td><code>price</code></td>
<td>INT</td>
<td>상품 가격</td>
</tr>
</tbody></table>
<h2 id="문제-접근-방식">문제 접근 방식</h2>
<blockquote>
</blockquote>
<p>처음에는 제품 쌍(product1, product2)별 product_id와 category 정보를 먼저 구성한 뒤,
이를 기반으로 JOIN을 이용해 함께 구매한 사용자 수를 구하고자 했습니다.
하지만 처음 구상한 방식대로는 잘 되지 않아 시행착오가 있었습니다.</p>
<blockquote>
</blockquote>
<p>특히 CROSS JOIN은 평소에 자주 사용하지 않던 연산이라,
조합을 구성하는 부분에서 논리적으로 정리가 잘 되지 않았던 점이 아쉬웠습니다.</p>
<blockquote>
</blockquote>
<p>최종적으로는 SELF JOIN과 CROSS JOIN을 활용하여 제품 간 조합을 구성하고,
중복 조합(예: 101–102 vs 102–101)을 제거하기 위해
조인 조건에 a.product_id &lt; b.product_id를 추가하는 방식으로 문제를 해결했습니다.</p>
<blockquote>
</blockquote>
<p>이 조건 덕분에 순서만 다른 동일한 조합이 중복으로 포함되는 것을 방지할 수 있었고, 불필요한 계산도 줄일 수 있었습니다.</p>
<h2 id="정답-쿼리">정답 쿼리</h2>
<pre><code>WITH t as 
    (
        SELECT user_id, pp.product_id, category
        FROM productpurchases pp
        JOIN productinfo pri on pp.product_id = pri.product_id
    ),

    t2 as 
    (
        SELECT a.product_id product1_id, b.product_id product2_id,
            a.category product1_category, b.category product2_category
        FROM t a
        CROSS JOIN t b on a.product_id &lt; b.product_id
        GROUP BY 1, 2
    ),

    t3 as
    (
        SELECT a.product_id product1_id, b.product_id product2_id,
                count(distinct a.user_id) customer_count
        FROM productpurchases a 
        JOIN productpurchases b on a.user_id = b.user_id and a.product_id &lt; b.product_id
        GROUP BY 1, 2

    )

SELECT t2.product1_id, t2.product2_id,
        product1_category, product2_category, customer_count
FROM t2
JOIN t3 on t2.product1_id = t3.product1_id and t2.product2_id = t3.product2_id
WHERE customer_count &gt; 2
ORDER BY 5 desc, 1, 2;</code></pre><h2 id="쿼리-최적화">쿼리 최적화</h2>
<blockquote>
</blockquote>
<p>사실 내가 처음 작성한 정답 쿼리는 구조가 깔끔하다고 보긴 어려웠고,
특히 t와 t2 테이블은 하나로 합쳐도 되겠다는 생각이 들었다.</p>
<blockquote>
</blockquote>
<p>이런 생각을 바탕으로 ChatGPT를 활용해 쿼리를 다시 구성해 보았고,
불필요한 단계들을 줄이고 의도에 더 집중된 구조로 개선할 수 있었다.</p>
<pre><code># 기존 주문 테이블에 카테고리 추가

WITH purchases_with_cat AS (
    SELECT pp.user_id, pp.product_id, pi.category
    FROM productpurchases pp
    JOIN productinfo pi ON pp.product_id = pi.product_id
),

# 앞에서 생성된 테이블을 바탕으로 self join 하여 제품 1, 2 별 주문 고객 수를 구함

product_pairs AS (
    SELECT
        a.product_id AS product1_id,
        b.product_id AS product2_id,
        a.category AS product1_category,
        b.category AS product2_category,
        COUNT(DISTINCT a.user_id) AS customer_count
    FROM purchases_with_cat a
    JOIN purchases_with_cat b
        ON a.user_id = b.user_id
       AND a.product_id &lt; b.product_id
    GROUP BY a.product_id, b.product_id, a.category, b.category
)

SELECT
    product1_id,
    product2_id,
    product1_category,
    product2_category,
    customer_count
FROM product_pairs
WHERE customer_count &gt; 2
ORDER BY customer_count DESC, product1_id, product2_id;</code></pre><blockquote>
</blockquote>
<p>가장 큰 차이점은 cross_join을 하지 않아서 메모리에 좋을 것 같다라는 생각을 했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코딩테스트 리뷰]]></title>
            <link>https://velog.io/@taeyoon__/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A6%AC%EB%B7%B0</link>
            <guid>https://velog.io/@taeyoon__/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A6%AC%EB%B7%B0</guid>
            <pubDate>Sat, 28 Jun 2025 03:01:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>그 동안 취업 준비하면서 코딩 테스트 문제를 하루에 2~3개씩 풀어보면서 한 번 틀렸거나 특별하게 풀었던 문제 정리</p>
<h2 id="hacker-rank-occupations">Hacker Rank: Occupations</h2>
<blockquote>
</blockquote>
<h3 id="문제">문제</h3>
<p><strong>테이블 이름:</strong> <code>OCCUPATIONS</code>
<strong>컬럼:</strong> <code>Name</code>, <code>Occupation</code>
<strong>목표:</strong></p>
<ul>
<li><code>Occupation</code> 열을 피벗(pivot)하여 각 직업(Doctor, Professor, Singer, Actor)을 <strong>열(Column)</strong>로 만들고,</li>
<li>각 <code>Occupation</code>에 해당하는 사람들의 <code>Name</code>을 <strong>알파벳 순으로 정렬</strong>하여 <strong>세로로 나열</strong>합니다.</li>
<li>이름의 수는 직업마다 다를 수 있으므로, 짧은 직업군에는 해당하지 않는 칸은 <code>NULL</code>로 채웁니다.</li>
<li><strong>컬럼 순서는 Doctor, Professor, Singer, Actor 순서</strong>로 고정입니다.</li>
</ul>
<blockquote>
</blockquote>
<h3 id="풀이">풀이</h3>
<ol>
<li>CTE t에서 row_number() 사용<ul>
<li>각 직업별 이름을 알파벳 순서로 정렬</li>
<li>각 이름에 번호를 부여해서, 나중에 group by</li>
</ul>
</li>
<li>case when 을 이용해 직업별로 하나의 컬럼 생성</li>
<li>group by num<ul>
<li>같은 순서에 해당하는 이름들을 하나의 행으로 묶음</li>
</ul>
</li>
<li>max() 사용 이유<ul>
<li>각 그룹에서 case when으로 필터링된 name은 하나뿐이므로, max()를 사용해서 그 값 추출</li>
<li>만약 해당 직업에 num 순서에 사람이 없으면 null 반환</li>
</ul>
</li>
</ol>
<blockquote>
</blockquote>
<h3 id="정답-쿼리">정답 쿼리</h3>
<pre><code>WITH 
t1  as (SELECT *, row_number() over(partition by occupation order by name) as num
        FROM occupations
)

SELECT 
    max(case when occupation = &#39;doctor&#39; then name else null end),
    max(case when occupation = &#39;professor&#39; then name else null end),
    max(case when occupation = &#39;singer&#39; then name else null end),
    max(case when occupation = &#39;actor&#39; then name else null end)
FROM t1
GROUP BY num;</code></pre><blockquote>
</blockquote>
<p>틀린 이유: 집계함수를 사용해서 그룹화된 1개의 값만을 가져오는 방법을 모르고 풀었었다</p>
<blockquote>
</blockquote>
<p>SQL은 집계함수(MAX, MIN 등)를 써야만 SELECT에서 GROUP BY 없이 컬럼을 출력할 수 있다</p>
<hr>
<h2 id="leet-code-primary-department-for-each-employee">Leet Code: Primary Department for Each Employee</h2>
<blockquote>
</blockquote>
<h3 id="문제-employee_id-당-department_id가-여려개-존재한-경우-primary_flag가-y인-경우의-department_id를-추출">문제: Employee_id 당 department_id가 여려개 존재한 경우 primary_flag가 &quot;Y&quot;인 경우의 department_id를 추출</h3>
<p>각 직원 아이디별로 primary 부서 아이디 추출</p>
<blockquote>
</blockquote>
<h3 id="초반-문제-접근">초반 문제 접근:</h3>
<p>CTE 나 서브쿼리를 이용해 id가 2개 이상인 직원을 필터링해서 기존 테이블과 조인하려 했음</p>
<blockquote>
</blockquote>
<h3 id="문제-풀이-방식">문제 풀이 방식:</h3>
<p>초반 문제 접근으로 쿼리를 짜면 쿼리가 복잡해지고 메모리 낭비도 심할꺼란 생각이 들었다.
&quot;윈도우 함수를 이용해서 primart_flag에 순위를 주면 어떨까?&quot;라는 생각을 기반으로 정답 쿼리를 만들게 됨.
가독성 측면이나 메모리 측면에서도 더 효율적이라고 생각이 됨.</p>
<blockquote>
</blockquote>
<h3 id="정답-쿼리-1">정답 쿼리</h3>
<pre><code>WITH t as
    (SELECT employee_id, department_id, row_number() over(partition by employee_id order by primary_flag) num
    FROM employee
    )

SELECT employee_id, department_id
FROM t
WHERE num = 1;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[태블로 정리]]></title>
            <link>https://velog.io/@taeyoon__/%ED%83%9C%EB%B8%94%EB%A1%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@taeyoon__/%ED%83%9C%EB%B8%94%EB%A1%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 27 Jun 2025 05:01:14 GMT</pubDate>
            <description><![CDATA[<h2 id="태블로-집계-방식">태블로 집계 방식</h2>
<p>태블로는 기본적으로 <code>합계</code>로 집계한다 - 필요한 집계 방식으로 변경해야 한다</p>
<h2 id="마크">마크</h2>
<blockquote>
</blockquote>
<p>데이터 포인트를 어떻게 표시할지 제어하는 요소
색상, 크기, 레이블, 세부 정보, 도구 설명, 도형이 있다.</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/17b5bf20-ce92-4754-bd50-6f1d8eab156e/image.png" alt=""></p>
<ol>
<li>&#39;국가/지역&#39;을 넣어 국가별 색상을 구분</li>
<li>&#39;세그먼트&#39;를 넣어 세그먼트별 도형 구분</li>
<li>&#39;세그먼트&#39;를 레이블로 표시</li>
<li>&#39;국가/지역&#39;을 레이블로 표시</li>
<li>&#39;주문 Id&#39;를 카운트(고유)로 집계 방식을 변경해서 레이블 표시 </li>
</ol>
<h2 id="데이터-타입">데이터 타입</h2>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/5de302aa-d3b2-41f2-843c-e673fc128c2e/image.png" alt=""></p>
<p>배송 날짜의 데이터 타입이 불연속형이라 데이터가 몇년의 데이터임에도 25년1월이 아닌 월별로 구분되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/ad30895f-ec5e-458d-aab3-4b7d3cec737f/image.png" alt=""></p>
<p>데이터 컬럼을 통해 연속성으로 변경을 눌러 연속형으로 변경해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/30eae1c9-4dac-4820-8a4e-4a1683c38935/image.png" alt=""></p>
<h2 id="필터">필터</h2>
<blockquote>
</blockquote>
<p>특정한 조건을 걸어 그에 해당하는 데이터만을 플롯에 나타낼 수 있다</p>
<h2 id="구간차원">구간차원</h2>
<blockquote>
</blockquote>
<p>구간차원을 나누어서 데이터를 일정한 구간으로 나눌 수 있다.
데이터 컬럼에서 우클릭 -&gt; 만들기 -&gt; 구간차원</p>
<h2 id="구성비율">구성비율</h2>
<p>마크에 넣은 데이터를 우클릭 -&gt; 퀵 테이블 계산 -&gt; 구성 비율</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[채무 불이행 여부 분류]]></title>
            <link>https://velog.io/@taeyoon__/%EC%B1%84%EB%AC%B4-%EB%B6%88%EC%9D%B4%ED%96%89-%EC%97%AC%EB%B6%80-%EB%B6%84%EB%A5%98</link>
            <guid>https://velog.io/@taeyoon__/%EC%B1%84%EB%AC%B4-%EB%B6%88%EC%9D%B4%ED%96%89-%EC%97%AC%EB%B6%80-%EB%B6%84%EB%A5%98</guid>
            <pubDate>Mon, 07 Apr 2025 05:07:06 GMT</pubDate>
            <description><![CDATA[<p>딥러닝 감 찾을 겸, 데이콘에 있는 채무 불이행 여부 분류 해커톤에 참가한 코드를 적어 보기로 했다.</p>
<h2 id="데이터-셋">데이터 셋</h2>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/6f6d3cd7-6b1c-41f4-b742-1309b3917b35/image.png" alt=""></p>
<p>데이터는 18개의 피처를 기반으로 채무불이행여부(0, 1)을 예측하는 문제이다.</p>
<h2 id="전처리">전처리</h2>
<p>우선 &quot;현재 직장 근속 연수&quot; 컬럼에서 근속 연수에 대한 숫자만 추출하기 위해 정규 표현식을 이용해서 숫자만 추출했다.</p>
<pre><code># 현재 직장 근속 연수 -&gt; 숫자만 추출
def year_int(x):
    return re.findall(r&#39;\d+&#39;, x)[0]

df[&#39;현재 직장 근속 연수&#39;] = df[&#39;현재 직장 근속 연수&#39;].apply(year_int)
test[&#39;현재 직장 근속 연수&#39;] = test[&#39;현재 직장 근속 연``수&#39;].apply(year_int)</code></pre><h3 id="범주형-변수-전처리">범주형 변수 전처리</h3>
<p>범주형 데이터에 포함되는 컬럼인 &quot;주거 형태&quot;, &quot;대출 목적&quot;, &quot;대출 상환 기간&quot;에 LabelEncoder, One-Hot Encoder 전부 적용해 본 결과 원-핫인코딩의 결과가 가장 좋아 최종적으로 pandas get_dummies 메서드를 이용해 원-핫 인코딩을 진행했다.</p>
<pre><code>df = pd.get_dummies(df, columns=[&#39;주거 형태&#39;, &#39;대출 목적&#39;, &#39;대출 상환 기간&#39;])
test = pd.get_dummies(test, columns=[&#39;주거 형태&#39;, &#39;대출 목적&#39;, &#39;대출 상환 기간&#39;])</code></pre><h3 id="소득대출-변수-전처리">소득/대출 변수 전처리</h3>
<p>소득/대출 관련 변수에 log1p를 적용시켜 이상치 완화, 분포 정규화를 시켰다.</p>
<pre><code>log_columns = [&quot;현재 미상환 신용액&quot;, &quot;월 상환 부채액&quot;, &quot;현재 대출 잔액&quot;]
for col in log_columns:
    X[col] = np.log1p(X[col])
    test[col] = np.log1p(test[col])</code></pre><h3 id="스케일링">스케일링</h3>
<p>스케일링의 경우 StandardScaler, MinMaxScaler 전부 해본 결과 MinMaxScaler를 적용시켰다.
클래스 불균형을 보완하기 위해 SMOTE를 사용했는데 -&gt; SMOTE를 사용하지 않아도 괜찮을 정도의 비율이였다.</p>
<pre><code>X = df.drop(&#39;채무 불이행 여부&#39;, axis=1)
y = df[&#39;채무 불이행 여부&#39;]

mm = MinMaxScaler()
X = mm.fit_transform(X)

mm = MinMaxScaler()
test_scaled = mm.fit_transform(test)

smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X, y)
X_train, X_test, y_train, y_test = train_test_split(X_resampled, y_resampled, test_size=0.3, random_state=42)</code></pre><h2 id="모델-설계">모델 설계</h2>
<p>사실 간단한 이진 분류 문제이기 때문에 처음에는 머신러닝을 이용해서 모델을 설계했지만 가장 높은 모델(LightGBM)의 점수가 0.57정도 밖에 되지 않아 딥러닝으로 모델을 설계했다.</p>
<p>모델은 3개의 은닉층을 가지고 활성화 함수는 출력층은 sigmoid 함수, 은닉층에는 leaky relu 함수를 사용했다.
조기 종료 옵션, 드롭아웃을 적용해 모델이 과적합을 방지했다.</p>
<pre><code>model = keras.Sequential([
    layers.Dense(128, activation=&#39;leaky_relu&#39;),
    layers.Dropout(0.3),
    layers.Dense(64, activation=&#39;leaky_relu&#39;),
    layers.Dropout(0.3),
    layers.Dense(32, activation=&#39;leaky_relu&#39;),
    layers.Dense(1, activation=&#39;sigmoid&#39;)
])

early_stopping = EarlyStopping(
    monitor=&#39;val_loss&#39;,
    patience=5,
    min_delta=0.001,
    restore_best_weights=True
)

model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.0015),
              loss=&#39;binary_crossentropy&#39;, metrics=[&#39;AUC&#39;])

history = model.fit(X_train, y_train, epochs=100, validation_data=(X_test, y_test),
                    batch_size=32, callbacks=[early_stopping], verbose=1)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/39cfd375-66c3-4a7d-a20c-b4e0067a58c4/image.png" alt=""></p>
<h2 id="결과-변환">결과 변환</h2>
<pre><code>test_pred_prob = model.predict(test_scaled)
test_pred = (test_pred_prob &gt;= 0.5).astype(int)

sub = pd.read_csv(&#39;sample_submission.csv&#39;)
sub[&#39;채무 불이행 확률&#39;] = test_pred

sub.to_csv(&#39;submission.csv&#39;, index=False)</code></pre><blockquote>
</blockquote>
<p>데이콘 결과 상위 25%로 평가지표 ROC-AUC 점수는 약 0.6 정도 나왔다.
사실 딥러닝을 오랜만에 설계하면서 감찾는게 목표였는데 이정도면 괜찮은 것 같다.
feature engineering을 하면 더 점수가 높게 나왔을 거지만 시간이 부족해서 그렇게 까지는 하지 못했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[협업 필터링]]></title>
            <link>https://velog.io/@taeyoon__/%ED%98%91%EC%97%85-%ED%95%84%ED%84%B0%EB%A7%81</link>
            <guid>https://velog.io/@taeyoon__/%ED%98%91%EC%97%85-%ED%95%84%ED%84%B0%EB%A7%81</guid>
            <pubDate>Wed, 26 Mar 2025 04:30:47 GMT</pubDate>
            <description><![CDATA[<h2 id="협업-필터링">협업 필터링</h2>
<blockquote>
</blockquote>
<p>사용자의 구매 패턴이나 평점을 가지고 다른 사람들의 구매 패턴, 평점을 통해서 추천을 하는 방법
추가적인 사용자의 개인정보나 아이템의 점보가 없이도 추천할 수 있는게 큰 장점
종류 - 최근접 이웃기반, 잠재 요인기반</p>
<h2 id="이웃기반">이웃기반</h2>
<p>이웃기반 협업 필터링은 메모리 기반 알고리즘으로 협업 필터링을 위해 개발되었다</p>
<p>알고리즘</p>
<ol>
<li>User-based collaborative filtering
사용자의 구매 패턴(평점)과 유사한 사용자를 찾아서 추천 리스트 생성</li>
</ol>
<p>-&gt; 유사한 사람을 찾는</p>
<ol start="2">
<li>Item-based collaborative filtering
특정 사용자가 준 점수간의 유사한 상품을 찾아서 추천 리스트 생성</li>
</ol>
<p>-&gt; 유사한 상품을 찾는</p>
<p>장점
간단하고 직관적인 접근 방식 때문에 구현 및 디버그가 쉬움
특정 Item을 추천하는 이유를 정당화하기 쉽고 Item기반 방법의 해석 가능성이 두드러짐
추천 리스트에 새로운 item과 user가 추가되어도 상대적으로 안정적</p>
<p>단점
User 기반 방법의 시간, 속도, 메모리가 많이 필요
희소성 떄문에 제한된 범위가 있음</p>
<h2 id="잠재요인-기반">잠재요인 기반</h2>
<p>Rating Matrix에서 빈 공산을 채우기위해 사용자와 상품을 잘 표현하는 차원을 찾는 방법(ex 행렬분해). 행렬 분해는 추천 시스템에서 사용되는 협업 필터링 알고리즘을 한 종류로 사용자-아이템 상호 작용 행렬을 두 개의 저차원 직사각형 행렬의 곱으로 분해하여 작동</p>
<h3 id="sgd">SGD</h3>
<p>고유값 분해와 같은 행렬을 대각화 하는 방법
Rating Matrix R과 User latent matrix U와 Item latent matrix V 내적의 차이를 최소화하는 U, V를 찾음</p>
<p>Gradient Descent를 통해 U, V를 업데이트
<img src="https://velog.velcdn.com/images/taeyoon__/post/8b447e74-dc9f-4499-bcb7-4babb9b9c3ea/image.png" alt=""></p>
<p>Gradient Descent를 통해 J를 U, V로 편미분한 값
<img src="https://velog.velcdn.com/images/taeyoon__/post/6bef01d9-5873-4052-bd2f-576038f2a2d6/image.png" alt=""></p>
<p>기울기 폭주 방지를 위해 규제항 추가해야 한다
<img src="https://velog.velcdn.com/images/taeyoon__/post/00c4732f-e315-425e-b8e6-27eafd7a4203/image.png" alt=""></p>
<p>편미분한 값을 이용해 한 번의 에포크로 각 Latent Matrix의 원소를 업데이트</p>
<h3 id="alsalternating-least-squares">ALS(Alternating Least Squares)</h3>
<p>SGD는 User Latent, Item Latent 두 개의 행렬을 최적화하는 방버
ALS는 두 행렬 중 하나를 고정시키고 다른 하나의 행렬을 순차적으로 반복하면서 최적화하는 방법 -&gt; 기존의 최적화 문제가 convex형태로 바뀌기 때문에 수렴된 행렬을 찾을 수 있다</p>
<h4 id="알고리즘">알고리즘</h4>
<ol>
<li>초기 아이템, 사용자 행렬을 초기화</li>
<li>아이템 행렬을 고정하고 사용자 행렬을 최적화</li>
<li>사용자 행렬을 고정하고 아이템 행렬을 최적화</li>
<li>2, 3 과정 반복</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[plotly go]]></title>
            <link>https://velog.io/@taeyoon__/plotly-go</link>
            <guid>https://velog.io/@taeyoon__/plotly-go</guid>
            <pubDate>Tue, 18 Mar 2025 08:41:14 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 사용했던 plotly의 graph objects를 정리해보겠다</p>
<h2 id="기본-문법">기본 문법</h2>
<pre><code>import plotly.graph_objects as go

# Figure 객체 생성
fig = go.Figure()

# Trace(데이터) 추가
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 1, 2], mode=&#39;lines+markers&#39;, name=&#39;Sample&#39;))

# 레이아웃 설정
fig.update_layout(title=&#39;Sample Plot&#39;, xaxis_title=&#39;X Axis&#39;, yaxis_title=&#39;Y Axis&#39;)

fig.show()</code></pre><h2 id="주요-구성-요소">주요 구성 요소</h2>
<h3 id="trace데이터-시각화-요소">Trace(데이터 시각화 요소)</h3>
<p><code>go.Scatter()</code>: 선 그래프, 산점도
<code>go.Bar()</code>: 막대 그래프
<code>go.Pie()</code>: 파이 차트
<code>go.Box()</code>: 박스 플롯
<code>go.Heatmap()</code>: 히트맵</p>
<h3 id="figure그래프-객체">Figure(그래프 객체)</h3>
<p><code>fig = go.Figure()</code>: 빈 Figure 객체 생성
<code>fig.add_trace(go.Scatter(...))</code>: 데이터 추가</p>
<h3 id="layout레이아웃-설정">Layout(레이아웃 설정)</h3>
<p><code>fig.update_layout(title=&#39;그래프 제목&#39;, xaxis_title=&#39;x축 제목&#39;, yaxis_title=&#39;y축 제목&#39;)</code>
<code>fig.update_xaxes(title_text=&#39;x축 제목&#39;)</code>
<code>fig.update_yaxes(title_text=&#39;y축 제목&#39;)</code></p>
<h2 id="프로젝트-시각화-코드">프로젝트 시각화 코드</h2>
<h3 id="카테고리별-국가별-주문-건수">카테고리별 국가별 주문 건수</h3>
<h4 id="각-카테고리와-국가에-대해-그래프-추가">각 카테고리와 국가에 대해 그래프 추가</h4>
<pre><code>fig = go.Figure()

categories = size_df[&#39;Category&#39;].unique()
countries = size_df[&#39;Country&#39;].unique()

for category in categories:
    for country in countries:
        category_data = size_df[(size_df[&#39;Category&#39;] == category) &amp; (size_df[&#39;Country&#39;] == country)]
        fig.add_trace(go.Scatter(
            x=category_data[&#39;year&#39;],
            y=category_data[&#39;주문건수&#39;],
            mode=&#39;lines+markers&#39;,
            name=f&quot;{country} - {category}&quot;,
            visible=(category == categories[0])  
        ))</code></pre><p><code>fig.add_trace(go.Scatter(...))</code>
-&gt; 각 국가 및 카테고리에 대한 라인 그래프
-&gt; <code>name=f&#39;{country} - {category}</code>로 각 그래프의 이름을 설정하여 그래프 레전드에 표시
-&gt; <code>visible=(category == categories[0])</code>로 첫 번째 카테고리의 그래프만 처음에 보이게 설정</p>
<h4 id="드롭다운-버튼-생성-각-카테고리마다-버튼을-생성해-선택하면-해당-카테고리만-볼-수-있도록-설정">드롭다운 버튼 생성: 각 카테고리마다 버튼을 생성해 선택하면 해당 카테고리만 볼 수 있도록 설정</h4>
<pre><code>dropdown_buttons = [
    {&quot;label&quot;: category, &quot;method&quot;: &quot;update&quot;, &quot;args&quot;: [{&quot;visible&quot;: [category == cat for cat in categories for _ in countries]}, 
                                                    {&quot;title&quot;: f&quot;{category} 카테고리별 국가별 주문건수&quot;}]}
    for category in categories
]</code></pre><p><code>label</code>: 드롭다운에 표시될 텍스트
<code>method: &#39;update&#39;</code>: 버튼 클릭 시 그래프를 업데이트 하도록 설정
<code>args</code> - <code>visible</code>: 각 카테고리의 국가별 주문 건수 그래프 중 해당 카테고리에 해당하는 것만 보이게 설정
<code>args</code> - <code>title</code>: 드롭다운 메뉴에서 카테고리를 선택할 때마다 그래프의 제목을 변경</p>
<h4 id="레이아웃-및-드롭다운-메뉴-설정">레이아웃 및 드롭다운 메뉴 설정</h4>
<pre><code>fig.update_layout(
    updatemenus=[{
        &quot;buttons&quot;: dropdown_buttons,
        &quot;direction&quot;: &quot;down&quot;,
        &quot;showactive&quot;: True,
        &quot;x&quot;: 0.1,
        &quot;xanchor&quot;: &quot;left&quot;,
        &quot;y&quot;: 1.15,
        &quot;yanchor&quot;: &quot;top&quot;,
    }],
    title=&quot;카테고리별 국가별 주문 건수&quot;,
    xaxis_title=&quot;Year&quot;,
    yaxis_title=&quot;주문건수&quot;
)

fig.show()</code></pre><p><code>updatemenus</code>: 드롭다운 메뉴를 레이아웃에 추가
 &nbsp; &nbsp; &nbsp; &nbsp; - <code>buttons</code>: 앞서 만든 드롭다운 버튼을 설정
 &nbsp; &nbsp; &nbsp; &nbsp; - <code>direction: &#39;down&#39;</code>: 드롭다운 버튼이 아래로 펼쳐지도록 설정
 &nbsp; &nbsp; &nbsp; &nbsp; - <code>showactive: True</code>: 현재 선택된 버튼이 활성화되어 표시
 &nbsp; &nbsp; &nbsp; &nbsp; - <code>x</code>, <code>xanchor</code>, <code>y</code>, <code>yanchor</code>: 드롭다운 버튼의 위치를 정한다(여기서는 그래프 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;위쪽에 배치하도록 설정)</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/c456ab7b-e49a-4ebd-b6ac-6347a2aa05c3/image.png" alt=""></p>
<h3 id="중앙값-주문-건수-비교">중앙값 주문 건수 비교</h3>
<h4 id="첫-번째-트레이스-판매-금액-중앙값">첫 번째 트레이스: 판매 금액 중앙값</h4>
<pre><code>fig.add_trace(go.Scatter(
    x=ch_furniture[&quot;year&quot;], 
    y=ch_furniture[&quot;sales median&quot;], 
    text=ch_furniture[&#39;sales median&#39;],
    textposition=&#39;top center&#39;,
    mode=&quot;lines+markers+text&quot;,  
    name=&quot;판매 금액 중앙값&quot;,
    line=dict(color=&quot;blue&quot;, width=2)
))</code></pre><p><code>text=</code>: 각 데이터 포인트 위에 판매 금액 중앙값을 표시
<code>mode=&#39;lines+marker+text&#39;</code>: 데이터 포인트를 선과 마커로 표시하며 텍스트도 표시
<code>line=dict(color=&#39;blue&#39;, width=2)</code>: 선의 색상을 파란색으로, 두께는 2로 설정</p>
<h4 id="두-번째-트레이스-주문-건수">두 번째 트레이스: 주문 건수</h4>
<pre><code>fig.add_trace(go.Scatter(
    x=ch_furniture[&quot;year&quot;], 
    y=ch_furniture[&quot;amount&quot;], 
    mode=&quot;lines+markers+text&quot;,  
    text=ch_furniture[&#39;amount&#39;],
    textposition=&#39;bottom center&#39;,
    name=&quot;주문건수&quot;,
    line=dict(color=&quot;red&quot;, width=2, dash=&quot;dash&quot;),
    yaxis=&quot;y2&quot;
))</code></pre><p><code>yaxis=&#39;y2&#39;</code>: 이 트레이스는 두 번째 y축(y2)에 배치되어 판매 금액의 중앙값과 주문 건수를 각각 다른 y축에 표시하도록 설정</p>
<h4 id="레이아웃-설정">레이아웃 설정</h4>
<pre><code>fig.update_layout(
    title=&quot;중국 가구 카테고리 연도별 판매 금액 중앙값, 주문건수&quot;,
    xaxis_title=&quot;Year&quot;,
    yaxis=dict(title=&quot;판매 금액 중앙값&quot;),
    yaxis2=dict(title=&quot;주문건수&quot;, overlaying=&quot;y&quot;, side=&quot;right&quot;),
    xaxis=dict(tickmode=&quot;linear&quot;, dtick=1),  
    legend=dict(x=0.9, y=1.2)
)
fig.show()</code></pre><p><code>yaxis2=dict(title=&#39;주문 건수&#39;...)</code>: 두 번째 y축을 추가하여
&nbsp; &nbsp; &nbsp; &nbsp; - <code>overlaying=&#39;y&#39;</code>: 첫 번째 y축과 겹쳐서 표시되도록 설정
&nbsp; &nbsp; &nbsp; &nbsp; - <code>side=&#39;right&#39;</code>: 두 번째 y축을 오른쪽에 배치
<code>xaxis=dict(tickmode=&#39;linear&#39;, dtick=1)</code>: x축의 눈금은 선형으로 설정, 각 눈금 간 간격을 1년으로 설정</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/25337f16-eb1e-45fa-ad26-a4e62f921cbf/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[고객 리텐션]]></title>
            <link>https://velog.io/@taeyoon__/%EA%B3%A0%EA%B0%9D-%EB%A6%AC%ED%85%90%EC%85%98-%ED%8D%BC%EB%84%90</link>
            <guid>https://velog.io/@taeyoon__/%EA%B3%A0%EA%B0%9D-%EB%A6%AC%ED%85%90%EC%85%98-%ED%8D%BC%EB%84%90</guid>
            <pubDate>Wed, 12 Mar 2025 08:48:43 GMT</pubDate>
            <description><![CDATA[<p>이커머스 데이터로 프로젝트를 진행했을 때, 분석에 사용한 리텐션 분석을 기록하려고 한다!</p>
<h2 id="고객-세그먼트">고객 세그먼트</h2>
<blockquote>
</blockquote>
<p>구매 고객을 VIP, 일반 고객 두 그룹으로 나누어 리텐션 분석을 진행했다.</p>
<h3 id="고객-세그먼트-코드">고객 세그먼트 코드</h3>
<pre><code>WITH Avg_price AS (
    SELECT AVG(total_purchase) AS avg_price
    FROM (
        SELECT user_id, SUM(price_usd) AS total_purchase
        FROM orders
        GROUP BY user_id
    ) AS user_purchase
),
VIP AS (
    SELECT o.user_id, SUM(o.price_usd) AS total_purchase, a.avg_price
    FROM orders o
    CROSS JOIN Avg_price a
    GROUP BY o.user_id, a.avg_price
    HAVING SUM(o.price_usd) &gt;= 1.5 * a.avg_price
),</code></pre><h2 id="리텐션-분석">리텐션 분석</h2>
<blockquote>
</blockquote>
<p>고객의 주문건을 기준으로 리텐션을 계산
첫 주문달을 기준으로 첫 달에 구매한 고객 수 집계
첫 주문 달에도 구매하고 두 번째 달에도 구매한 고객 수 집계
계속 이렇게 진행하면서 마지막 구매 달까지 리텐션을 계산한다</p>
<h3 id="세그먼트별-첫-주문일을-확인하는-코드">세그먼트별 첫 주문일을 확인하는 코드</h3>
<pre><code>first_order AS (
    SELECT DISTINCT user_id,
            CASE WHEN user_id IN (SELECT user_id
                                FROM vip) THEN &#39;VIP&#39;
            ELSE &#39;Regular&#39; END AS &#39;User_Segment&#39;, min(created_at) AS first_ord
    FROM orders
    GROUP BY 1
)</code></pre><h3 id="첫-주문-이후-n개월-주문-여부를-확인하는-코드">첫 주문 이후 n개월 주문 여부를 확인하는 코드</h3>
<pre><code>order_month AS (SELECT DISTINCT fo.user_id, User_segment,
        CASE WHEN created_at = date_format( date_format( first_ord, &#39;%Y-%m-01&#39;), &#39;%Y-%m-01&#39;) THEN 0
        WHEN created_at &gt; date_format( date_format( first_ord, &#39;%Y-%m-01&#39;), &#39;%Y-%m-01&#39;) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 1 month) THEN 1
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 1 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 2 month) THEN 2
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 2 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 3 month) THEN 3
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 3 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 4 month) THEN 4
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 4 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 5 month) THEN 5
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 5 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 6 month) THEN 6
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 6 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 7 month) THEN 7
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 7 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 8 month) THEN 8
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 8 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 9 month) THEN 9
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 9 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 10 month) THEN 10
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 10 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 11 month) THEN 11
        WHEN created_at &gt; date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 11 month) AND created_at &lt;= date_add(date_format( first_ord, &#39;%Y-%m-01&#39;), INTERVAL 12 month) THEN 12
        END AS month_num

FROM first_order fo JOIN orders o ON fo.user_id = o.user_id
ORDER BY month_num desc
)</code></pre><h3 id="연속으로-주문한-월의-수를-계산하는-코드">연속으로 주문한 월의 수를 계산하는 코드</h3>
<pre><code>seq_table AS (
    SELECT user_id, User_segment, month_num
            FROM order_month
)</code></pre><h3 id="조회하는-코드">조회하는 코드</h3>
<pre><code>SELECT User_segment,
        CASE WHEN month_num = 0 THEN &#39;m-0&#39;
        WHEN month_num = 1  THEN &#39;m-1&#39;
        WHEN month_num = 2  THEN &#39;m-2&#39;
        WHEN month_num = 3  THEN &#39;m-3&#39;
        WHEN month_num = 4  THEN &#39;m-4&#39;
        WHEN month_num = 5  THEN &#39;m-5&#39;
        WHEN month_num = 6  THEN &#39;m-6&#39;
        WHEN month_num = 7  THEN &#39;m-7&#39;
        WHEN month_num = 8  THEN &#39;m-8&#39;
        WHEN month_num = 9  THEN &#39;m-9&#39;
        WHEN month_num = 10 THEN &#39;m-10&#39;
        WHEN month_num = 11 THEN &#39;m-11&#39;
        WHEN month_num = 12 THEN &#39;m-12&#39;
        ELSE &#39;unknown&#39;
        END AS month_range,
        count(user_id) user_cnt
FROM seq_table
GROUP BY 1,2
ORDER BY 1,2;</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/f97bdc8b-fcd1-474b-99e7-22a3de354d69/image.png', width=60%>
</p>



<table>
<thead>
<tr>
<th>고객 구분</th>
<th>M-0</th>
<th>M-1</th>
<th>M-2</th>
<th>M-3</th>
<th>M-4</th>
</tr>
</thead>
<tbody><tr>
<td>VIP</td>
<td>28,032</td>
<td>7</td>
<td>4</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>일반 고객</td>
<td>3,664</td>
<td>280</td>
<td>150</td>
<td>40</td>
<td>2</td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
<h3 id="결론">결론:</h3>
<p>첫 구매 이후 지속적인 유지율이 낮다
일반 고객 대비 VIP 고객읠 평균 구매 빈도가 높지만, 시간이 지나며 유지율이 감소한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코호트, 리텐션]]></title>
            <link>https://velog.io/@taeyoon__/%EC%BD%94%ED%98%B8%ED%8A%B8-%EB%A6%AC%ED%85%90%EC%85%98</link>
            <guid>https://velog.io/@taeyoon__/%EC%BD%94%ED%98%B8%ED%8A%B8-%EB%A6%AC%ED%85%90%EC%85%98</guid>
            <pubDate>Tue, 04 Mar 2025 06:04:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>주제: 신규 고객 활성화 기준 설정
분석 방법: 코호트 분석을 통해 신규 고객이 활성화 될 수 있는 주문수 기준 설정
기간: 23년 7월 첫 주문 고객의 첫 주문일로부터 1달간 누적 주문수 별 리텐션 확인</p>
<h3 id="sql-코드">SQL 코드</h3>
<pre><code>WITH
fst AS (
    SELECT mem_no, first_ord_dt
    FROM first_ord_table_practice1 fotp
    WHERE 1=1
    AND first_ord_dt &gt;= &#39;2023-07-01&#39;
    AND first_ord_dt &lt;= &#39;2023-07-31&#39;
),
ord as(
    SELECT DISTINCT omp.mem_no, 
            first_ord_dt,
            ord_dt,
            sum(CASE WHEN first_ord_dt &lt; ord_dt AND ord_dt &lt;= date_add(first_ord_dt, INTERVAL 7 day) THEN 1
                ELSE 0 END) over(PARTITION BY omp.mem_no) as is_w1_ord,
            dense_rank() over(PARTITION BY omp.mem_no ORDER BY ord_dt) ord_seq
    FROM fst
    JOIN order_master_practive1 omp ON fst.mem_no = omp.mem_no
    WHERE 1=1
    AND ord_dt &gt;= &#39;2023-07-01&#39;
    AND ord_dt &lt;= &#39;2023-08-31&#39;
    ORDER BY 1, 2
)
SELECT ord_seq,
        CASE WHEN is_w1_ord &gt; 0 THEN 1 ELSE 0 END is_w1_ord,
        count(DISTINCT mem_no) mem_cnt
FROM ord
GROUP BY 1, 2
ORDER BY 2;</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/34e84f04-ed78-4848-9fef-4cd0f510e6c0/image.png" alt=""></p>
<p><code>dense_rank를 이용해서 회원별로 누적 주문수를 구한 다음 count 했다 - 이건 1개 또 배웠다</code></p>
<h3 id="파이썬-코드">파이썬 코드</h3>
<pre><code># 첫 주문 이후 1주일 이내 주문 여부에 따라 그룹 나누기
true = df[df.is_w1_ord == 1].sort_values(&#39;mem_cnt&#39;, ascending=False)
false = df[df.is_w1_ord == 0].sort_values(&#39;mem_cnt&#39;, ascending=False)

# 리텐션 구하기
true[&#39;retention&#39;] = true.mem_cnt/max(true.mem_cnt)
false[&#39;retention&#39;] = false.mem_cnt/max(false.mem_cnt)

# 시각화를 위해 데이터 합치기
new_df = pd.concat([true, false])

# 시각화
fig = px.line(data_frame=new_df, x=&#39;ord_seq&#39;, y=&#39;retention&#39;, color=&#39;is_w1_ord&#39;)
fig.update_traces(selector=dict(name=&#39;1&#39;), name=&#39;첫 구매 이후 1주이내 재구매 고객&#39;)
fig.update_traces(selector=dict(name=&#39;0&#39;), name=&#39;첫 구매 이후 1주이내 미구매 고객&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/116873d4-2481-47b2-80c1-4acf4cc3bfee/image.png" alt=""></p>
<h2 id="결론">결론</h2>
<blockquote>
</blockquote>
<p>1주 이내 신규 고객을 재구매 고객으로 만들어야 한다
첫 주문일 이후 고객이 활성화될 가능성을 높이려면 한 달 이내 5번 이상 주문해야 한다 - 기준</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LTV, 로그 데이터]]></title>
            <link>https://velog.io/@taeyoon__/LTV-%EB%A1%9C%EA%B7%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0</link>
            <guid>https://velog.io/@taeyoon__/LTV-%EB%A1%9C%EA%B7%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0</guid>
            <pubDate>Wed, 26 Feb 2025 14:58:39 GMT</pubDate>
            <description><![CDATA[<h2 id="ltvlifetime-value">LTV(Lifetime Value)</h2>
<blockquote>
</blockquote>
<p>고객 생애 가치로 유저가 서비스를 사용하는 기간동안 창출하는 매출
EX) 신규고객 획득비용(CAC) 기준 설정
    효율적인 마케팅 채널을 파악해 예산 분배</p>
<blockquote>
</blockquote>
<p>LTV = ARPU * 리텐션
ARPU = 유저당 주문금액(매출/유저수)
그룹을 코호트로 쪼개서 LTV 비교 분석</p>
<h3 id="실습-코드">실습 코드</h3>
<pre><code>WITH 
ord as(
    SELECT a.mem_no,
            b.age_range,
            b.first_ord_dt,
            a.ord_dt,
            a.order_amount
    FROM order_master_ltv a
    LEFT JOIN first_ord_table_ltv b ON a.mem_no = b.mem_no
)
,cohort as(
    SELECT age_range,
            CASE WHEN first_ord_dt = ord_dt THEN &#39;M-0&#39;
                WHEN first_ord_dt &lt; ord_dt AND date(ord_dt) &lt;= date_add(first_ord_dt, INTERVAL 1 month) THEN &#39;M-1&#39;
                WHEN date(ord_dt) &gt; date_add(first_ord_dt, INTERVAL 1 month) AND date(ord_dt) &lt;= date_add(first_ord_dt, INTERVAL 2 month) THEN &#39;M-2&#39;
                WHEN date(ord_dt) &gt; date_add(first_ord_dt, INTERVAL 2 month) AND date(ord_dt) &lt;= date_add(first_ord_dt, INTERVAL 3 month) THEN &#39;M-3&#39;
                WHEN date(ord_dt) &gt; date_add(first_ord_dt, INTERVAL 3 month) AND date(ord_dt) &lt;= date_add(first_ord_dt, INTERVAL 4 month) THEN &#39;M-4&#39;
                WHEN date(ord_dt) &gt; date_add(first_ord_dt, INTERVAL 4 month) AND date(ord_dt) &lt;= date_add(first_ord_dt, INTERVAL 5 month) THEN &#39;M-5&#39;
                ELSE &#39;over-5m&#39; END month_nm,
            count(DISTINCT mem_no) sample_cnt,
            round(avg(order_amount)) ARPU
    FROM ord
    GROUP BY 1, 2
    ORDER BY 1, 2
)
SELECT age_range,
        month_nm,
        sample_cnt,
        ARPU,
        sample_cnt * 1.00 / max(sample_cnt) over(PARTITION BY age_range) retention
FROM cohort
ORDER BY 1, 2</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/404a8729-7ac8-4ddd-8c61-861302b89b77/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>연령대</th>
<th>10대</th>
<th>20대</th>
<th>30대</th>
<th>40대</th>
<th>50대</th>
</tr>
</thead>
<tbody><tr>
<td>인당 LTV</td>
<td>55,882원</td>
<td>83,332원</td>
<td>111,645원</td>
<td>94,328원</td>
<td>96,027원</td>
</tr>
</tbody></table>
<h3 id="결론">결론</h3>
<blockquote>
</blockquote>
<p>30대가 첫 주문 이후 5개월까지 가장 높은 LTV를 나타냄</p>
<h2 id="로그데이터-분석">로그데이터 분석</h2>
<blockquote>
</blockquote>
<p>로그데이터: 고객이 서비스에 유입되어 남기는 행동 데이터
유입, 페이지 이동, 노출, 클릭 등 데이터가 이벤트 단위로 적재</p>
<p>분석 주제: 어떤 배너를 통해 유입된 고객의 주문 전환율이 더 높을까</p>
<h3 id="실습-코드-1">실습 코드</h3>
<pre><code>WITH 
log AS
    (SELECT DISTINCT mem_no,
            session_id,
            log_dt,
            cast(log_stamp AS datetime) log_stamp,
            min(referrer) OVER(PARTITION BY mem_no ORDER BY log_stamp) bnr_type
    FROM log_table
),
ord AS
    (SELECT DISTINCT log.mem_no,
            log_dt,
            log_stamp,
            bnr_type,
            cast(ord_stamp AS datetime) ord_stamp,
            count(DISTINCT ord_no) ord_cnt
    FROM log 
    LEFT JOIN order_master_log oml 
    ON log.mem_no = oml.mem_no 
    AND log_dt = date(ord_stamp)
    AND log_stamp &lt; CAST(ord_stamp AS datetime)
--     AND log.session_id = oml.session_id 
    GROUP BY 1, 2, 3, 4, 5
),
ord2 AS 
    (SELECT DISTINCT mem_no,
            bnr_type,
            CASE WHEN ord_cnt&gt;0 THEN 1 ELSE 0 END is_order
    FROM ord
)

SELECT bnr_type, sum(is_order)
FROM ord2
GROUP BY 1
ORDER BY 1;</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/42095090-b7f3-43c5-8acd-f46a9b38c692/image.png" alt=""></p>
<h3 id="결론-1">결론</h3>
<blockquote>
</blockquote>
<p>배너 1에 비해 배너 2로 유입되어 주문한 사람이 4배 차이로 차이가 있었지만
작은 데이터 이기 때문에 의미는 거의 없을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코호트 분석]]></title>
            <link>https://velog.io/@taeyoon__/%EC%BD%94%ED%98%B8%ED%8A%B8-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@taeyoon__/%EC%BD%94%ED%98%B8%ED%8A%B8-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Mon, 24 Feb 2025 01:10:46 GMT</pubDate>
            <description><![CDATA[<h2 id="코호트-분석">코호트 분석</h2>
<blockquote>
</blockquote>
<h3 id="코호트">코호트</h3>
<p><code>특정 기간 동안 공통된 특성이나 경험을 갖는 사용자 집단</code></p>
<h3 id="코호트-분석-1">코호트 분석</h3>
<p><code>시간을 두고 비슷한 그룹을 비교하는 방법</code></p>
<blockquote>
</blockquote>
<p>ex) 최근 유입 고객의 가입전환율이 낮아짐 -&gt; 광고채널별 유입고객 코호트 분석</p>
<h2 id="aarrr">AARRR</h2>
<blockquote>
</blockquote>
<h3 id="그로스-해킹">그로스 해킹</h3>
<p><code>사업의 핵심지표를 찾고, 성장시키는 방법을 찾는 활동</code></p>
<h3 id="aarrr해적지표">AARRR(해적지표)</h3>
<p><code>사용자 행동방식 5단계 지표</code>
<code>사용자 유치(Acquisition), 사용자 활성화(Activation), 사용자 유지(Retention), 추천(Referral), 매출(Revenue)</code></p>
<blockquote>
</blockquote>
<p>고객 관리에서 Retention은 아주 중요</p>
<h2 id="리텐션">리텐션</h2>
<blockquote>
</blockquote>
<h3 id="리텐션-1">리텐션</h3>
<p><code>&#39;고객 유지율&#39;로 서비스의 성공을 예측할 수 있는 가장 기본적인 핵심 지표</code></p>
<blockquote>
</blockquote>
<p>측정 방법
특정 기간 내에 이벤트를 발생시킨 유저의 비율을 계산
기간 내 1번 이상의 이벤트가 관측되면 한 번으로 카운트
Retention = 측정 Week 기간 내 서비스 이용 고객수/Week1서비스 이용 고객수</p>
<h2 id="실습">실습</h2>
<p>첫 구매 특가 상품을 구매한 고객이 더 잘 될 것인가?</p>
<p>가설: 첫 구매 특가 상품 구매 고객은 리텐션이 더 높을 것
독립변수: 첫 구매 특가 상품 구매 여부(1: 구매/0: 미구매)
종속변수: 고객 유지율(리텐션)
측정인원: 첫 구매 특가 상품 구매자 1,000명/미구매자 1,000명
기간: 1월 한 달간 첫구매 고객(1월 week1 주문고객 기준으로 week4까지 리텐션 비교)</p>
<h3 id="sql-코드">sql 코드</h3>
<blockquote>
</blockquote>
<p>T1: 고객 별 첫 주문 이후 몇 주차에 주문을 했는지
T2: 고객 별 추가 주문이 몇 번째인지(seq) 번호 부여
최종적으로, 각 고객의 첫 주문 이후 n주차에 추가 주문했는지 집계 </p>
<pre><code># 첫 주문 테이블
select *
from first_ord_table;

# 오더마스터 테이블
select *
from order_master_cohort omc ;

# 테이블 조인
select *
from first_ord_table fot
left join order_master_cohort omc on fot.mem_no = omc.mem_no; 

# 분석에 필요한 데이터 집계(첫 주문 후 n주차별 주문 여부 분석)
with 
T1 as (
    select distinct fot.mem_no,
            is_promotion,
            case when ord_dt = first_ord_dt then 0
                when ord_dt &gt; first_ord_dt and date(ord_dt) &lt;= date_add(date(first_ord_dt), interval 7 day) then 1
                when ord_dt &gt; date_add(date(first_ord_dt), interval 7 day) and date(ord_dt) &lt;= date_add(date(first_ord_dt), interval 14 day) then 2
                when ord_dt &gt; date_add(date(first_ord_dt), interval 14 day) and date(ord_dt) &lt;= date_add(date(first_ord_dt), interval 21 day) then 3    
                when ord_dt &gt; date_add(date(first_ord_dt), interval 21 day) and date(ord_dt) &lt;= date_add(date(first_ord_dt), interval 28 day) then 4
                else null end as week_number
    from first_ord_table fot 
    left join order_master_cohort omc on fot.mem_no = omc.mem_no 
)
# 각 고객별 첫 주문 이후 추가 주문 순서 부여
, T2 as (    
    select is_promotion, mem_no, week_number,
            row_number() over(partition by mem_no order by week_number) seq
    from T1
    where week_number is not null)

    select is_promotion,  -- 특가 상품 구매자/미구매자 코호트 
            case when week_number = 0 then &#39;1.w-0&#39;
                 when week_number = 1 and seq = 2 then &#39;2.w-1&#39;
                 when week_number = 2 and seq = 3 then &#39;3.w-2&#39;
                 when week_number = 3 and seq = 4 then &#39;4.w-3&#39;
                 when week_number = 4 and seq = 5 then &#39;5.w-4&#39; end as week_range,
            count(mem_no) as mem_cnt
    from T2
    group by 1,2
    order by 1,2;</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/1a4da42e-62a9-4f25-b78a-b95b755aa061/image.png' width=60%></p>


<h3 id="파이썬-시각화">파이썬 시각화</h3>
<pre><code>import plotly.express as px
import pandas as pd
import numpy as np
from matplotlib import rc
import matplotlib.pyplot as plt
plt.rc(&#39;font&#39;, family=&#39;NanumGothic&#39;)

cohort = pd.DataFrame({&#39;is_promotion&#39;: [&#39;0&#39;, &#39;0&#39;, &#39;0&#39;, &#39;0&#39;, &#39;0&#39;, &#39;1&#39;, &#39;1&#39;, &#39;1&#39;, &#39;1&#39;,&#39;1&#39;],
                       &#39;week_range&#39;: [&#39;w-0&#39;, &#39;w-1&#39;, &#39;w-2&#39;, &#39;w-3&#39;, &#39;w-4&#39;, &#39;w-0&#39;, &#39;w-1&#39;, &#39;w-2&#39;, &#39;w-3&#39;, &#39;w-4&#39;],
                       &#39;mem_cnt&#39;: [1000, 239, 104, 59, 45, 1000, 440, 266, 198, 155]})

cohort[&#39;retention&#39;] = cohort.mem_cnt/1000

fig = px.line(data_frame=cohort, x=&#39;week_range&#39;, y=&#39;retention&#39;, color=&#39;is_promotion&#39;,
              title=&#39;코호트 분석&#39;)
fig.update_traces(selector=dict(name=&#39;0&#39;), name=&#39;특가 상품 구매자&#39;)
fig.update_traces(selector=dict(name=&#39;1&#39;), name=&#39;특가 상품 미구매자&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/1d905feb-1251-4f64-b578-17508b2211d3/image.png" alt=""></p>
<center>



<table>
<thead>
<tr>
<th>프로모션여부</th>
<th>W-0</th>
<th>W-1</th>
<th>W-2</th>
<th>W-3</th>
<th>W-4</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1,000</td>
<td>44%</td>
<td>27%</td>
<td>20%</td>
<td>16%</td>
</tr>
<tr>
<td>0</td>
<td>1,000</td>
<td>24%</td>
<td>10%</td>
<td>6%</td>
<td>5%</td>
</tr>
<tr>
<td></center></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
<p>일반적인 코호트 분석 결과의 그래프는 점점 떨어지는 그래프
표로 표현할 때는 프로모션을 시작한 주에는 인원수를 작성하고 주차 별로 남은 인원의 비율 작성
특가 상품 구매 코호트가 1주가 지난 시점부터 높은 리텐션이 보인다
첫 구매 특가 상품 프로모션을 유지하는 방향으로 의사결정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이중차분법]]></title>
            <link>https://velog.io/@taeyoon__/%EC%9D%B4%EC%A4%91%EC%B0%A8%EB%B6%84%EB%B2%95</link>
            <guid>https://velog.io/@taeyoon__/%EC%9D%B4%EC%A4%91%EC%B0%A8%EB%B6%84%EB%B2%95</guid>
            <pubDate>Sun, 09 Feb 2025 15:25:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/taeyoon__/post/9384f31e-8715-460a-b4a7-5e6f8547076c/image.png" alt=""></p>
<h2 id="ab-test">A/B Test</h2>
<blockquote>
</blockquote>
<h2 id="ab-test-1">A/B Test</h2>
<p>A와 B를 대조하여 가설을 검정하는 실험방법
즉, 결과와 상관성이 높다고 생각되는 변수를 테스트하여 검정하는 방법</p>
<blockquote>
</blockquote>
<p>ex) 배너 위치를 변경하면 CTR(클릭전환율)이 증가할 것이다</p>
</br>

<h2 id="이중차분법difference-in-differences">이중차분법(Difference-In-Differences)</h2>
<blockquote>
</blockquote>
<p>- 독립변수와 종속변수 간 인과 효과를 측정하기 위한 인과추론법
- 실험군(Treatment): 실험자가 종속변수와 상관성이 높아 보이는 독립변수에 변화를 주는 그룹
- 대조군(Control): 실험군과 대조하기 위해 처치를 하지 않는 그룹
- 대조군과 실험군 두 그룹의 처치 시점 전후 지표 비교</p>
<blockquote>
</blockquote>
<center>
>
||처치 전|처치 후|
|---|---|---|
|실험군|a|A|
|대조군|b|B|
</center>
>
<center>
>
<br>
이중차분: (A-a)-(B-b) 
</center>  

<br>

<h2 id="실습">실습</h2>
<p>가설: 정액이 기재되어 있는 할인쿠폰의 주문전환율이 더 높을 것이다.
종속변수: 주문전환율(1: 실험기간 중 주문이 1번이라도 있는 경우/ 0: 실험기간 중 주문이 없는 경우)
독립변수: 쿠폰유형(5천원 할인쿠폰 / 5% 할인쿠폰)
대조그룹: 쿠폰 미지급 100명
실험그룹: 5천원 할인쿠폰 100명 / 5% 할인쿠폰 100명
관찰기간: 6/12 쿠폰발급(처치 이전: 6/5<del>6/11, 처치 이후: 6/12</del>6/18)
<br></p>
<blockquote>
</blockquote>
<p>mysql에서 컬럼명이 예약어와 겹칠 경우 
ex) group
백틱(``)을 감싸주면 인식 가능 </p>
<p>쿠폰 데이터와 오더 데이터를 조인하여 집단별 처치 전후의 차이를 알아봤다
몇 번을 주문했냐가 문제가 아니라 주문을 했는지 안했는지 확인하기 위해
고유 배달 번호가 아닌 고유 멤버 번호를 distinct하여 주문했으면 1, 안했으면 0으로 집계</p>
<pre><code># 이중차분법을 위한 그룹별 주문 수 집계
with 
T1 as(
    select ctt.mem_no
            , `group`
            , ord_no,
            case when ord_dt between &#39;2023-06-05&#39; and &#39;2023-06-11&#39; then &#39;처치 전&#39;
                when ord_dt between &#39;2023-06-12&#39; and &#39;2023-06-18&#39; then &#39;처치 후&#39; end as period
    from coupon_target_table ctt left join order_master_did omd 
    on ctt.mem_no = omd.mem_no
)

    select `group`
            , period
            , count(distinct mem_no) as ord_cnt
    from T1
    where period is not null
    group by 1, 2;</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/f068c42a-7c47-45c3-b26a-117584e64cc6/image.png" alt=""></p>
<p>각 샘플마다 100명이여서 그냥 퍼센트 남긴 채로 계산</p>
<center>


<table>
<thead>
<tr>
<th>그룹/CVR</th>
<th>처치 전</th>
<th>처치 후</th>
<th>처치 후-처치 전</th>
<th>이중차분</th>
</tr>
</thead>
<tbody><tr>
<td><center>Control</center></td>
<td><center>62%</center></td>
<td><center>56%</center></td>
<td><center>-6%</center></td>
<td><center>-</center></td>
</tr>
<tr>
<td>Test1_정율</td>
<td><center>36%</center></td>
<td><center>40%</center></td>
<td><center>4%</center></td>
<td>4%-(-6%)=10%</td>
</tr>
<tr>
<td>Test1_정액</td>
<td><center>61%</center></td>
<td><center>62%</center></td>
<td><center>1%</center></td>
<td><center>1%-(-6%)=7%</center></td>
</tr>
</tbody></table>
</center>


<br>


<h2 id="결론">결론</h2>
<blockquote>
</blockquote>
<p>정율쿠폰: 10%
정액쿠폰: 7% 
3% 차이 밖에 나지 않기 때문에 유의미한 결과는 아니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[부동산 데이터 분석]]></title>
            <link>https://velog.io/@taeyoon__/%EB%B6%80%EB%8F%99%EC%82%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@taeyoon__/%EB%B6%80%EB%8F%99%EC%82%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Mon, 03 Feb 2025 15:12:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/taeyoon__/post/661c78c5-541d-4c20-a307-5597fc0d8d81/image.png" alt=""></p>
<h2 id="크롤링">크롤링</h2>
<p>네이버 페이 부동산을 크롤링하여
100페이지가 넘지 않으면 에러가 날 수 있기 때문에 try except 사용했다</p>
<pre><code>article_list = [] 
for i in tqdm(range(1, 101)):
    try:
        url = f&#39;https://m.land.naver.com/cluster/ajax/articleList?itemId=&amp;mapKey=&amp;lgeo=&amp;showR0=&amp;rletTpCd=OPST%3AVL%3AOR&amp;tradTpCd=B2&amp;z=12&amp;lat=37.481021&amp;lon=126.951601&amp;btm=37.3398975&amp;lft=126.6762562&amp;top=37.6218785&amp;rgt=127.2269458&amp;totCnt=8360&amp;cortarNo=1162000000&amp;sort=rank&amp;page={i}&#39;

        user_agent = generate_user_agent()
        headers = {&#39;User-Agent&#39;:user_agent}

        res = requests.get(url, headers=headers)
        time.sleep(1) # 부하 방지를 위해 1초의 대기 시간 가진다


        article_json = res.json()
        article_body = article_json[&#39;body&#39;]
        article_list.append(article_body)
    except:
        break

article_list1 = [j for i in article_list for j in i]
data = pd.DataFrame(article_list1)</code></pre><p>필요한 컬럼만 사용하고 컬럼명 지정 후 엑셀 파일로 저장</p>
<pre><code>data = data[[&#39;atclNo&#39;, &#39;rletTpNm&#39;, &#39;flrInfo&#39;, &#39;rentPrc&#39;, &#39;hanPrc&#39;, &#39;spc1&#39;, &#39;spc2&#39;, &#39;direction&#39;, &#39;atclCfmYmd&#39;, &#39;repImgUrl&#39;, &#39;lat&#39;, &#39;lng&#39;, &#39;atclFetrDesc&#39;, &#39;tagList&#39;]]
data.columns = [&#39;물건번호&#39;, &#39;구분&#39;, &#39;층수(물건층/전체층)&#39;, &#39;월세&#39;, &#39;보증금&#39;, &#39;계약면적(m2)&#39;, &#39;전용면적(m2)&#39;, &#39;방향&#39;, &#39;확인일자&#39;, &#39;이미지&#39;, &#39;위도&#39;, &#39;경도&#39;, &#39;설명&#39;, &#39;태그&#39;]
data
data.to_excel(&#39;data.xlsx&#39;)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/05383a5c-512e-432a-b49c-fb42cfdcf69b/image.png" alt=""></p>
<h2 id="전처리">전처리</h2>
<h3 id="중복된-인덱스-컬럼-삭제">중복된 인덱스 컬럼 삭제</h3>
<pre><code>data.drop(&#39;Unnamed: 0&#39;, axis=1, inplace=True)</code></pre><h3 id="월세가-0원인-경우-삭제">월세가 0원인 경우 삭제</h3>
<pre><code>data = data.query(&#39;월세 &gt; 0&#39;)</code></pre><h3 id="보증금-숫자로-변환">보증금 숫자로 변환</h3>
<pre><code>data = data.query(&#39;~보증금.str.contains(&quot;억&quot;)&#39;)
data.보증금 = data.보증금.str.replace(&#39;,&#39;, &#39;&#39;).astype(int)</code></pre><h3 id="새로운-컬럼-생성">새로운 컬럼 생성</h3>
<p>물건층, 전체층을 분리한 후 비선호층에 대한 유무 확인하는 컬럼 생성</p>
<pre><code># 물건층, 전체층 분리
data[[&#39;물건층&#39;, &#39;전체층&#39;]] = data[&#39;층수(물건층/전체층)&#39;].str.split(&#39;/&#39;, expand=True)

# 비선호층을 구분하는 함수 
from re import T
def floor_info(target, total):
  try:
    if target in [&#39;B1&#39;, &#39;B2&#39;]:
      return &#39;y&#39;
    elif int(target) == 1 or int(target)/int(total) == 1:
      return &#39;y&#39;
    else:
      return &#39;n&#39;

  except ValueError:
    return &#39;n&#39;

# 비선호층여부 컬럼 생성
data[&#39;비선호층여부&#39;] = data.apply(lambda x: floor_info(x[&#39;물건층&#39;], x[&#39;전체층&#39;]), axis=1)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/50878dcc-9afc-4eb4-9dfd-745cb73e7dc4/image.png" alt=""></p>
<h3 id="데이터-필터링">데이터 필터링</h3>
<blockquote>
</blockquote>
<p>내가 원하는 조건 </p>
<ol>
<li>보증금 3,000만원 이하</li>
<li>지하, 반지하, 꼭대기층 x</li>
<li>북향 x</li>
</ol>
<pre><code>data_filtered = data.query(&#39;300&lt;= 보증금 &lt;= 3000 and 비선호층여부 == &quot;n&quot; and 전체층 !=&quot;1&quot; and ~방향.str.contains(&quot;북&quot;)&#39;)</code></pre><h4 id="태그-분리">태그 분리</h4>
<p>정규표현식을 이용해 태그 컬럼을 4개의 컬럼으로 분리</p>
<pre><code>data_filtered[[&#39;tag1&#39;, &#39;tag2&#39;, &#39;tag3&#39;, &#39;tag4&#39;]] = data_filtered[&#39;태그&#39;].str.replace(r&quot;\&#39;|\[|\]&quot;, &quot;&quot;, regex=True).str.split(&#39;, &#39;, expand=True)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/9fce9bbf-10a4-440b-9a12-ed523c7014dc/image.png" alt=""></p>
<h4 id="연식-컬럼-추가">연식 컬럼 추가</h4>
<pre><code># 연식 정보가 있는 데이터만 필터링
data_filtered = data_filtered.query(&#39;tag1.str.contains(&quot;년&quot;)&#39;) 
data_filtered[&#39;연식&#39;] = [int(i[0]) for i in data_filtered[&#39;tag1&#39;].str.split(&#39;년&#39;)]</code></pre><h4 id="필요한-컬럼만">필요한 컬럼만</h4>
<pre><code>data_filtered = data_filtered[[&#39;물건번호&#39;,&#39;월세&#39;,&#39;보증금&#39;,&#39;전용면적(m2)&#39;,&#39;방향&#39;,&#39;위도&#39;,&#39;경도&#39;,&#39;물건층&#39;,&#39;전체층&#39;,&#39;연식&#39;]]
data_filtered.head()</code></pre><h3 id="역까지의-거리-추가">역까지의 거리 추가</h3>
<pre><code>coordinate = pd.read_csv(&#39;서울시 역사마스터 정보.csv&#39;, encoding=&#39;cp949&#39;)
coordinate = coordinate.query(&#39;호선 == &quot;2호선&quot;&#39;)
station_list = [&#39;신대방&#39;, &#39;신림&#39;, &#39;봉천&#39;, &#39;서울대입구(관악구청)&#39;, &#39;낙성대&#39;, &#39;사당&#39;]
coordinate.query(&#39;역사명 in @station_list&#39;)</code></pre><p>역까지의 거리를 구하는 함수(haversine을 이용해 거리를 구했다)</p>
<pre><code>def distance(station_name, lat, long):
    station_lat = coordinate.query(f&#39;역사명 == &quot;{station_name}&quot;&#39;)[&#39;위도&#39;].values[0]
    station_long = coordinate.query(f&#39;역사명 == &quot;{station_name}&quot;&#39;)[&#39;경도&#39;].values[0]

    distance = haversine((station_lat, station_long), (lat, long), unit=&#39;m&#39;)

    return distance</code></pre><p> 각 지하철 역별로 자취방과의 직선거리를 구해 지하철역 이름의 컬럼에 저장</p>
<pre><code> for s in station_list:
  data_filtered[s] = data_filtered.apply(lambda x: distance(s, x[&#39;위도&#39;], x[&#39;경도&#39;]), axis=1)</code></pre><p> 모든 지하철역에 대한 거리를 구하는 것은 비효율적이다 
 그래서 역까지 거리가 가장 가까운 거리만 데이터에 추가</p>
<pre><code> data_filtered[&#39;역까지최소거리&#39;] = data_filtered.apply(lambda x:min([x[&#39;신대방&#39;], x[&#39;신림&#39;], x[&#39;봉천&#39;], x[&#39;서울대입구(관악구청)&#39;], x[&#39;낙성대&#39;], x[&#39;사당&#39;]]), axis=1)
data_filtered.head()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/852a1d96-dd35-4683-838d-8e89c01defc1/image.png" alt=""></p>
<p>역까지 최소거리를 구했으니 지하철역 컬럼은 삭제해도 된다</p>
<pre><code>data_filtered.drop(station_list, axis=1, inplace=True)</code></pre><h2 id="eda">EDA</h2>
<h3 id="각-항목의-박스플롯-확인">각 항목의 박스플롯 확인</h3>
<pre><code>for x in [&#39;월세&#39;, &#39;보증금&#39;, &#39;전용면적(m2)&#39;, &#39;연식&#39;, &#39;역까지최소거리&#39;]:
  fig = px.box(data_frame = data_filtered, x=x, width=700, height=400)
  fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/fbe23baf-d2b9-4110-82ed-7860c9188142/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>원하는 조건</p>
<ol>
<li>월세는 저렴할수록 good</li>
<li>전용면적이 클수록 good</li>
<li>연식이 오래되지 않으면</li>
<li>지하철 역에 가까울 수록<blockquote>
</blockquote>
pandas의 qcut을 이용해 등급을 매긴다</li>
</ol>
<pre><code>data_filtered[&#39;월세_등급&#39;] = pd.qcut(data_filtered[&#39;월세&#39;], 5, labels=[1, 2, 3, 4, 5])
data_filtered[&#39;전용면적_등급&#39;] = pd.qcut(data_filtered[&#39;전용면적(m2)&#39;], 5, labels=[1, 2, 3, 4, 5])
data_filtered[&#39;연식_등급&#39;] = pd.qcut(data_filtered[&#39;연식&#39;].rank(method=&#39;first&#39;), 5, labels=[1, 2, 3, 4, 5]) # rank(method=&#39;first&#39;) 한 이유는 중복이 많기 때문에 임의로 정해라라고 설정한 것
data_filtered[&#39;역까지최소거리_등급&#39;] = pd.qcut(data_filtered[&#39;역까지최소거리&#39;], 5, labels=[1, 2, 3, 4, 5])</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/585943ae-eef4-4d57-8931-97f01e5d4a83/image.png" alt=""></p>
<p>원하는 조건을 입력해 데이터 필터링</p>
<pre><code>data_filtered_choice = data_filtered.query(&#39;월세_등급 &lt; 3 and 전용면적_등급 &lt; 3 and 연식_등급 &lt;= 3 and 역까지최소거리_등급 &lt;= 3&#39;)</code></pre><h2 id="최종-시각화">최종 시각화</h2>
<p>folium을 이용해 지도에 시각화
마커를 클릭하면 링크를 확인할 수 있다</p>
<pre><code>f = folium.Figure(width=700, height=500)
m = folium.Map(location=[37.486313, 126.935378], zoom_start=14).add_to(f)

for idx in data_filtered_choice.index:
    lat = data_filtered_choice.loc[idx, &#39;위도&#39;]
    long = data_filtered_choice.loc[idx, &#39;경도&#39;]
    num = data_filtered_choice.loc[idx, &#39;물건번호&#39;]

    folium.Marker([lat, long]
                  , popup=f&quot;&lt;a href=https://m.land.naver.com/article/info/{num}&gt;링크&lt;/a&gt;&quot;
                  ).add_to(m)
m</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/154ba841-54a4-4c9a-90b6-2a46575080d3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[지리 데이터 시각화]]></title>
            <link>https://velog.io/@taeyoon__/%EC%A7%80%EB%A6%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94</link>
            <guid>https://velog.io/@taeyoon__/%EC%A7%80%EB%A6%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94</guid>
            <pubDate>Wed, 22 Jan 2025 09:20:32 GMT</pubDate>
            <description><![CDATA[<p>교통 데이터를 활용해 folium으로 데이터 시각화 수행</p>
<p>데이터 구조</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/bf7e89e2-1ecc-4e6b-8dd8-ad2efa26afaa/image.png" alt=""></p>
<p>2018년 이후 데이터, 2호선, 승차 인원에 관해 분석을 진행하기 위해 전처리 필요</p>
<h2 id="전처리">전처리</h2>
<h3 id="연도-월-컬럼-추가">연도, 월 컬럼 추가</h3>
<pre><code>data[&#39;연도&#39;] = pd.to_datetime(data[&#39;사용월&#39;], format=&#39;%Y%m&#39;).dt.year
data[&#39;월&#39;] = pd.to_datetime(data[&#39;사용월&#39;], format=&#39;%Y%m&#39;).dt.month</code></pre><h3 id="2018년-이후-2호선만-추출">2018년 이후, 2호선만 추출</h3>
<pre><code>data = data[(data.연도 &gt;= 2018) &amp; (data.호선명 == &#39;2호선&#39;)]
data = data.query(&#39;호선명 == &quot;2호선&quot; and 연도 &gt;= 2018&#39;)</code></pre><p>둘 다 같은 의미의 코드 지만 query를 이용해 이렇게 전처리 할 수도 있다</p>
<h3 id="지하철역-이름-전처리">지하철역 이름 전처리</h3>
<pre><code>[&#39;강남&#39;, &#39;강변(동서울터미널)&#39;, &#39;건대입구&#39;, &#39;교대(법원.검찰청)&#39;, &#39;구로디지털단지&#39;, &#39;구의(광진구청)&#39;, &#39;낙성대&#39;, &#39;낙성대(강감찬)&#39;, &#39;당산&#39;, &#39;대림(구로구청)&#39;, &#39;도림천&#39;, &#39;동대문역사문화공원&#39;, &#39;동대문역사문화공원(DDP)&#39;, &#39;뚝섬&#39;, &#39;문래&#39;, &#39;방배&#39;, &#39;봉천&#39;, &#39;사당&#39;, &#39;삼성(무역센터)&#39;, &#39;상왕십리&#39;, &#39;서울대입구(관악구청)&#39;, &#39;서초&#39;, &#39;선릉&#39;, &#39;성수&#39;, &#39;시청&#39;, &#39;신답&#39;, &#39;신당&#39;, &#39;신대방&#39;, &#39;신도림&#39;, &#39;신림&#39;, &#39;신설동&#39;, &#39;신정네거리&#39;, &#39;신촌&#39;, &#39;아현&#39;, &#39;양천구청&#39;, &#39;역삼&#39;, &#39;영등포구청&#39;, &#39;왕십리(성동구청)&#39;, &#39;용답&#39;, &#39;용두(동대문구청)&#39;, &#39;을지로3가&#39;, &#39;을지로4가&#39;, &#39;을지로입구&#39;, &#39;이대&#39;, &#39;잠실(송파구청)&#39;, &#39;잠실나루&#39;, &#39;잠실새내&#39;, &#39;종합운동장&#39;, &#39;충정로(경기대입구)&#39;, &#39;한양대&#39;, &#39;합정&#39;, &#39;홍대입구&#39;]</code></pre><p>지하철역 이름이 바뀐 경우가 있기 때문에 &quot;()&quot; 부분을 제거 해야한다</p>
<pre><code># 지하철역 컬럼을 &quot;(&quot;를 기준을 split하여 리스트에 넣고 그 중 첫번째 값을 꺼낸다 -&gt; 그러면 &quot;(&quot; 앞에 있는 것들만 추출된다 
data[&#39;지하철역&#39;] = [i[0] for i in data[&#39;지하철역&#39;].str.split(&#39;(&#39;)]</code></pre><h3 id="승차-인원-추출">승차 인원 추출</h3>
<p>사실 여기서 나는 개인적으로 isin으로 승차가 들어간 컬럼만 가져오는게 더 효율적이지 않을까 생각했는데 그렇게 되면 내가 원하는 컬럼 순서나 나중에 승차 컬럼만 필요할때 사용함에 있어서 불편함이 있기 때문에 이렇게 리스트 컴프리헨션을 사용하는게 더 적합하다.</p>
<pre><code>on_col = [i for i in data.columns if &#39;승차&#39; in i]
data = data[[&#39;사용월&#39;,&#39;연도&#39;,&#39;월&#39;,&#39;지하철역&#39;]+on_col]</code></pre><h3 id="합계">합계</h3>
<p>axis=1을 이용해 지하철역별 해당월의 승차 인원의 합을 구한다</p>
<pre><code>data[&#39;합계&#39;] = data[on_col].sum(axis=1)</code></pre><h2 id="eda">EDA</h2>
<h3 id="지하철역별-월평균-승차-인원">지하철역별 월평균 승차 인원</h3>
<pre><code>data_mean = data.groupby(&#39;지하철역&#39;)[[&#39;합계&#39;]].mean().reset_index().rename({&#39;합계&#39;:&#39;월평균&#39;}, axis=1).sort_values(&#39;월평균&#39;, ascending=False)</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/7960c296-ce3e-4178-bb9a-3880e5fc929c/image.png' width=30%></p>

<pre><code>fig = px.bar(data_frame = data_mean, x=&#39;지하철역&#39;, y=&#39;월평균&#39;, title=&#39;지하철역별 월평균 승차인원&#39;)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/05084561-1f8d-4b8e-ab17-d2a0a22ce5a1/image.png" alt=""></p>
<h3 id="연도월별-승차-인원-추세-파악">연도/월별 승차 인원 추세 파악</h3>
<h4 id="연도">연도</h4>
<pre><code>year_sum = data.query(&#39;연도 &lt;= 2022&#39;).groupby([&#39;연도&#39;])[[&#39;합계&#39;]].sum().reset_index()
year_sum[&#39;연도&#39;] = year_sum[&#39;연도&#39;].astype(str)

fig = px.line(data_frame=year_sum, x=&#39;연도&#39;, y=&#39;합계&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/52ec8703-7022-450d-b827-4060ef1fe846/image.png" alt=""></p>
<h4 id="월">월</h4>
<pre><code>month_sum = data.query(&#39;연도 &lt;= 2022&#39;).groupby([&#39;월&#39;])[[&#39;합계&#39;]].sum().reset_index()
month_sum[&#39;월&#39;] = month_sum[&#39;월&#39;].astype(str)

fig = px.line(data_frame=month_sum, x=&#39;월&#39;, y=&#39;합계&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/20c7e212-cb0f-4b05-a6e3-40ffa692980b/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>코로나가 시작된 2020년, 2021년에 인원이 많이 줄고(재택근무 영향) 2022년 부터 회복</p>
<h3 id="시간대별-가장-승차인원이-많은-역">시간대별 가장 승차인원이 많은 역</h3>
<pre><code>top10 = data_mean.sort_values(&#39;월평균&#39;, ascending=False).head(10)[&#39;지하철역&#39;]
top10</code></pre><blockquote>
</blockquote>
<p>승하차 인원이 가장 많은 역: 강남, 잠실, 홍대입구 ...</p>
<p>월평균 인원수가 많은 순서로 10개만 필터링해 시간당 월평균 인원수 구하기</p>
<pre><code>top10 = data_mean.sort_values(&#39;월평균&#39;, ascending=False).head(10)[&#39;지하철역&#39;]
top10_mean_hour = data.query(&#39;지하철역 in @top10&#39;).groupby(&#39;지하철역&#39;)[on_col].mean()
top10_mean_hour.columns = [i[:3] for i in top10_mean_hour.columns]</code></pre><p>히트맵 시각화</p>
<pre><code>top10_mean_hour.style.background_gradient(cmap=&#39;pink_r&#39;, axis=None).format(&#39;{:.0f}&#39;)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/f24e3eb6-ebaa-415e-8c6c-58d85c0330e4/image.png" alt=""></p>
<h3 id="시간대별-인원-클러스터링">시간대별 인원 클러스터링</h3>
<pre><code>hour_mean = data.groupby(&#39;지하철역&#39;)[on_col].mean()
hour_mean.columns = [i[:3] for i in hour_mean.columns]
hour_mean_pct = hour_mean.div(hour_mean.sum(axis=1), axis=0)</code></pre><p>KMeans 알고리즘을 이용해 군집화</p>
<pre><code>from sklearn.cluster import KMeans
from yellowbrick.cluster import KElbowVisualizer

model = KMeans()
visualizer = KElbowVisualizer(model, k=(1,10))
visualizer.fit(hour_mean_pct)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/a9359876-e7d0-45e5-a1de-4c84415a0b15/image.png" alt=""></p>
<p>주요 출근 시간대인 06시/18시를 확인</p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/cab01e23-7033-4cbe-92f7-c8b42428aece/image.png" alt=""></p>
<h4 id="군집별-해당하는-역명-확인">군집별 해당하는 역명 확인</h4>
<pre><code>for i in range(k):
    print(f&#39;cluster {i}&#39;)
    print(list(hour_mean_pct.query(f&#39;cluster == &quot;{i}&quot;&#39;).index))

cluster 0
[&#39;강남&#39;, &#39;교대&#39;, &#39;뚝섬&#39;, &#39;삼성&#39;, &#39;서초&#39;, &#39;선릉&#39;, &#39;성수&#39;, &#39;시청&#39;, &#39;역삼&#39;, &#39;을지로3가&#39;, &#39;을지로4가&#39;, &#39;을지로입구&#39;, &#39;한양대&#39;]
cluster 1
[&#39;강변&#39;, &#39;구의&#39;, &#39;낙성대&#39;, &#39;대림&#39;, &#39;봉천&#39;, &#39;상왕십리&#39;, &#39;서울대입구&#39;, &#39;신답&#39;, &#39;신대방&#39;, &#39;신림&#39;, &#39;신정네거리&#39;, &#39;아현&#39;, &#39;양천구청&#39;, &#39;용답&#39;, &#39;용두&#39;, &#39;잠실나루&#39;, &#39;잠실새내&#39;]
cluster 2
[&#39;건대입구&#39;, &#39;구로디지털단지&#39;, &#39;당산&#39;, &#39;도림천&#39;, &#39;동대문역사문화공원&#39;, &#39;문래&#39;, &#39;방배&#39;, &#39;사당&#39;, &#39;신당&#39;, &#39;신도림&#39;, &#39;신설동&#39;, &#39;신촌&#39;, &#39;영등포구청&#39;, &#39;왕십리&#39;, &#39;이대&#39;, &#39;잠실&#39;, &#39;종합운동장&#39;, &#39;충정로&#39;, &#39;합정&#39;, &#39;홍대입구&#39;]    </code></pre><h2 id="시각화">시각화</h2>
<p>위도 경도가 있는 데이터 </p>
<pre><code>coordinate = pd.read_csv(&#39;서울시 역사마스터 정보.csv&#39;, encoding=&#39;cp949&#39;)
coordinate.head()</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/8bd832a1-c0f9-47cc-95f1-2b2eaf92e934/image.png' width=50%></p>

<p>앞에서 진행한 전처리 방식과 동일하게 진행 다른점은 &quot;역사명&quot; 컬럼명을 &quot;지하철역&quot;으로 컬렴명 변경</p>
<pre><code>coordinate = coordinate.query(&#39;호선 == &quot;2호선&quot;&#39;)
coordinate[&#39;역사명&#39;] = [i[0] for i in coordinate[&#39;역사명&#39;].str.split(&#39;(&#39;)]
coordinate.rename({&#39;역사명&#39;:&#39;지하철역&#39;}, axis=1, inplace=True)
coordinate</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/6678d48b-b0a0-4793-a087-6f11fa25c578/image.png' width=60%></p>

<p>앞에서 구한 시간별 평균 데이터에서 &#39;지하철역&#39;, &#39;08시&#39;, &#39;18시&#39; 컬럼만 가져와서 위도/경도 데이터를 합친다</p>
<pre><code>hour_mean_merge = hour_mean.reset_index()[[&#39;지하철역&#39;,&#39;08시&#39;,&#39;18시&#39;]]
coordinate_merge = coordinate[[&#39;지하철역&#39;,&#39;위도&#39;,&#39;경도&#39;]]
hour_mean_coor = pd.merge(hour_mean_merge, coordinate_merge, on=&#39;지하철역&#39;)</code></pre><p>KMeans를 활용해 군집화하여 cluster이라는 컬럼으로 추가한다</p>
<pre><code>hour_mean_coor[&#39;cluster&#39;] = model.fit_predict(hour_mean_pct).astype(str)
hour_mean_coor.head()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/2dd1a0ae-d027-466d-b21e-8f2c376c3fcc/image.png" alt=""></p>
<h3 id="folium">folium</h3>
<p>승차 유형별로 지도에 시각화</p>
<pre><code>m = folium.Map(location=center, zoom_start=12)

for idx in hour_mean_coor.index:
    lat = hour_mean_coor.loc[idx, &#39;위도&#39;]
    long = hour_mean_coor.loc[idx, &#39;경도&#39;]
    title = hour_mean_coor.loc[idx, &#39;지하철역&#39;]

    if hour_mean_coor.loc[idx, &#39;cluster&#39;] == &quot;0&quot;:
        color = &#39;#000000&#39;
    elif hour_mean_coor.loc[idx, &#39;cluster&#39;] == &quot;1&quot;:
        color = &#39;#3A01DF&#39;
    else:
        color = &#39;#DF0101&#39;

    folium.CircleMarker([lat, long]
                        , radius=18
                        , color = color
                        , fill = color
                        , tooltip = title).add_to(m)
m</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/8da071ec-4fa1-429a-84d0-2621a29ec18e/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>0번 cluster: 주거 지역이 많이 분포
1번 cluster: 주거/상업 시설/회사가 비슷하게 분포
2번 cluster: 회사가 많이 분포</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[영화 흥행 요인 분석]]></title>
            <link>https://velog.io/@taeyoon__/%EC%98%81%ED%99%94-%ED%9D%A5%ED%96%89-%EC%9A%94%EC%9D%B8-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@taeyoon__/%EC%98%81%ED%99%94-%ED%9D%A5%ED%96%89-%EC%9A%94%EC%9D%B8-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Sat, 18 Jan 2025 04:27:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/taeyoon__/post/50b1d6e2-16fe-4500-ae92-661fcccf0711/image.png" alt=""></p>
<h2 id="데이터-탐색">데이터 탐색</h2>
<p><strong>movies 데이터</strong></p>
<ul>
<li>budget: 영화 예산 (단위: 달러)</li>
<li>genres: 모든 장르</li>
<li>homepage: 공식 홈페이지</li>
<li>id: 각 영화당 unique id</li>
<li>original_language: 원 언어</li>
<li>original_title: 원 제목</li>
<li>overview: 간략한 설명</li>
<li>popularity: TMDB에서 제공하는 인기도</li>
<li>production_companies: 모든 제작사</li>
<li>production_countries: 모든 제각국가</li>
<li>release_date: 개봉일</li>
<li>revenue: 흥행 수익 (단위: 달러)</li>
<li>runtime: 상영 시간</li>
<li>spoken_language: 사용된 모든 언어</li>
<li>status: 개봉 여부</li>
<li>title: 영문 제목</li>
<li>vote_avearage: TMDB에서 받은 평점 평균</li>
<li>vote_count: TMDB에서 받은 투표수</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/432965d7-f592-41e3-a050-a30a424dd977/image.png" alt=""></p>
<p><strong>credits 데이터</strong></p>
<ul>
<li>movie_id: 각 영화당 unique id</li>
<li>title: 영문 제목</li>
<li>cast: 모든 출연진</li>
<li>crew: 모든 제작진</li>
</ul>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/aaa34bab-5c66-43b0-98c3-722027361906/image.png" alt=""></p>
<h2 id="데이터-전처리">데이터 전처리</h2>
<h3 id="필요-없는-컬럼-제외">필요 없는 컬럼 제외</h3>
<pre><code>movies_df = movies[[&#39;id&#39;,&#39;budget&#39;,&#39;genres&#39;,&#39;title&#39;,&#39;release_date&#39;,&#39;revenue&#39;,&#39;vote_average&#39;,&#39;vote_count&#39;]]
credits_df = credits[[&#39;movie_id&#39;,&#39;crew&#39;,&#39;cast</code></pre><h3 id="movies-credits-데이터-결합">movies, credits 데이터 결합</h3>
<pre><code>data = pd.merge(movies_df, credits_df, left_on = &#39;id&#39;, right_on = &#39;movie_id&#39;).drop(&#39;movie_id&#39;, axis=1)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/c340bee1-d31f-418a-ae9d-ea2469649097/image.png" alt=""></p>
<h3 id="새로운-컬럼-만들기">새로운 컬럼 만들기</h3>
<h4 id="roi-컬럼">roi 컬럼</h4>
<pre><code>data[&#39;roi&#39;] = data[&#39;revenue&#39;] / data[&#39;budget&#39;]
data.head()</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/72110bf8-17fe-4d6c-bec0-cc7214566b51/image.png' width=70></p>

<h4 id="감독-컬럼">감독 컬럼</h4>
<p>crew 컬럼은 문자열 타입의 딕셔너리로 구성되있다</p>
<pre><code>data[&#39;crew&#39;][0]

&#39;[{&quot;credit_id&quot;: &quot;52fe48009251416c750aca23&quot;, &quot;department&quot;: &quot;Editing&quot;, &quot;gender&quot;: 0, &quot;id&quot;: 1721, &quot;job&quot;: &quot;Editor&quot;, &quot;name&quot;: &quot;Stephen E. Rivkin&quot;}, {&quot;credit_id&quot;: &quot;539c47ecc3a36810e3001f87&quot;, &quot;department&quot;: &quot;Art&quot;, &quot;gender&quot;: 2, &quot;id&quot;: 496, &quot;job&quot;: &quot;Production Design&quot;, &quot;name&quot;: &quot;Rick Carter&quot;}, {&quot;credit_id&quot;: &quot;54491c89c3a3680fb4001cf7&quot;, &quot;department&quot;: &quot;Sound&quot;, &quot;gender&quot;: 0, &quot;id&quot;: 900, &quot;job&quot;: &quot;Sound Designer&quot;, &quot;name&quot;: &quot;Christopher Boyes&quot;}</code></pre><p>ast 라이브러리를 이용해 문자열 -&gt; 리스트로 변경</p>
<pre><code>import ast

print(ast.literal_eval(data[&#39;crew&#39;][0])) 
data[&#39;crew&#39;] = data[&#39;crew&#39;].apply(ast.literal_eval) # 리스트 형태로 바꾼다</code></pre><p>감독의 이름을 리턴하는 함수</p>
<pre><code>def get_director(x):
    for i in x:
        if i[&#39;job&#39;] == &#39;Director&#39;:
            return i[&#39;name&#39;]</code></pre><p>apply 메서드를 이용해 감독의 이름을 리턴해 &#39;director&#39; 컬럼에 저장</p>
<pre><code>data[&#39;director&#39;] = data[&#39;crew&#39;].apply(get_director)</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/834cfdbd-de26-4b89-a888-7cb28fc63b22/image.png' width=70></p>

<h4 id="배우-컬럼">배우 컬럼</h4>
<p>배우 컬럼도 cast 컬럼을 ast 라이브러리를 이용해 문자열을 리스트로 저장</p>
<pre><code>data[&#39;cast_name&#39;] = data[&#39;cast&#39;].apply(lambda x: [i[&#39;name&#39;] for i in ast.literal_eval(x)])
# 문자열 -&gt; 리스트 -&gt; &#39;name&#39; key 만 가져와서 리스트로 저장
data.head()</code></pre><p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/059454bd-0ddf-4319-8bd4-8b91c613c1e4/image.png' width=70></p>

<h4 id="장르-컬럼">장르 컬럼</h4>
<p>장르도 앞에서 했던 것 처럼 새로운 &#39;main_genre&#39; 컬럼 생성</p>
<p align='center'>
<img src='https://velog.velcdn.com/images/taeyoon__/post/935bb547-7a26-468c-ad9c-a707444682ef/image.png' width=70></p>

<h3 id="데이터-타입-변경">데이터 타입 변경</h3>
<pre><code>data[&#39;release_date&#39;] = pd.to_datetime(data[&#39;release_date&#39;], format=&#39;%Y-%m-%d&#39;)
data[&#39;id&#39;] = data[&#39;id&#39;].astype(str)

# 연도, 월 컬럼 만들기
data[&#39;year&#39;] = data[&#39;release_date&#39;].dt.year
data[&#39;month&#39;] = data[&#39;release_date&#39;].dt.month</code></pre><h3 id="결측치-제거">결측치 제거</h3>
<pre><code>data.dropna(inplace=True)</code></pre><h2 id="eda">EDA</h2>
<h3 id="연도별-흥행-수익">연도별 흥행 수익</h3>
<pre><code>revenue_by_year = data.groupby(&#39;year&#39;)[[&#39;revenue&#39;]].sum().reset_index() # plotly의 x로 year가 들어가기 위해 reset_index 사용

fig = px.line(data_frame=revenue_by_year, x=&quot;year&quot;, y=&quot;revenue&quot;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/9cb92b91-18ce-492d-8c8c-b33935f4d732/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>2000년대 이후로 흥행 수익이 급격히 높아짐</p>
<h3 id="가장-흥행한-영화-10개">가장 흥행한 영화 10개</h3>
<pre><code>top = data.groupby(&#39;title&#39;)[&#39;revenue&#39;].sum().reset_index().sort_values(&#39;revenue&#39;, ascending=False).head(10)
fig = px.bar(data_frame=top, x=&#39;title&#39;, y=&#39;revenue&#39;, title=f&quot;흥행 수익 TOP 10 영화&quot;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/d4cdabb3-c33a-42a4-88ed-ee0b681e58d2/image.png" alt=""></p>
<h3 id="예산-투표수-상위-10개-영화">예산, 투표수 상위 10개 영화</h3>
<pre><code>title_dic = {&#39;budget&#39;:&#39;예산&#39;, &#39;vote_count&#39;:&#39;투표수&#39;}
for y in [&#39;budget&#39;,&#39;vote_count&#39;]:
    top = data.groupby(&#39;title&#39;)[[y]].sum().reset_index().sort_values(y, ascending=False).head(10)
    fig = px.bar(data_frame=top, x=&#39;title&#39;, y=y, title=f&quot;{title_dic[y]} TOP 10 영화&quot;)
    fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/04905997-ef48-4b19-93aa-e5d68fe75d08/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/931f0662-9759-42f6-ab42-4b65df25f15e/image.png" alt=""></p>
<h3 id="가장-흥행에-성공한-감독-배우">가장 흥행에 성공한 감독, 배우</h3>
<pre><code>top_director = data.groupby([&#39;director&#39;])[&#39;revenue&#39;].sum().reset_index().sort_values(&#39;revenue&#39;, ascending=False).head(10)
fig = px.bar(data_frame=top_director, x=&#39;director&#39;, y=&#39;revenue&#39;, title=f&quot;흥행 수익 TOP 10 감독&quot;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/e640eefb-9ead-4717-b807-050a519404a3/image.png" alt=""></p>
<p>흥행 수익이 높은 배우들의 목록을 확인하기 위해 explode 메서드를 활용해 확인</p>
<pre><code>revenue_cast = data[[&#39;revenue&#39;, &#39;cast_name&#39;]].explode(&#39;cast_name&#39;) # explode: 리스트 형태의 값을 여러행으로 전개</code></pre><pre><code>top_cast = revenue_cast.groupby(&#39;cast_name&#39;)[[&#39;revenue&#39;]].sum().reset_index().sort_values(&#39;revenue&#39;, ascending=False).head(10)
fig = px.bar(data_frame=top_cast, x=&#39;cast_name&#39;, y=&#39;revenue&#39;, title=f&quot;흥행 수익 TOP 10 배우&quot;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/142a2bd9-8f13-4059-805c-a39e04ea90ab/image.png" alt=""></p>
<h3 id="장르별-흥행-수익의-분포">장르별 흥행 수익의 분포</h3>
<pre><code>fig = px.box(data_frame = data, y = &#39;main_genre&#39;, x = &#39;revenue&#39;, hover_name = &#39;title&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/2154aa48-f285-4fd7-9876-158257b38ed1/image.png" alt=""></p>
<p>액션과 드라마 장르에 수익이 매우 높은 영화들이 많이 있지만, 중앙값을 비교했을 때, 다른 장르에 비해 높지 않다</p>
<pre><code>genre_avg_revenue = data.groupby(&#39;main_genre&#39;)[[&#39;revenue&#39;]].mean().reset_index()
fig = px.bar(data_frame = genre_avg_revenue, x = &#39;main_genre&#39;, y = &#39;revenue&#39;, title = &#39;장르별 흥행 수익 평균&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/f49210e2-aaed-4eb1-8b19-035bbabcc4f5/image.png" alt=""></p>
<p>장르별 영화 수익의 평균은 애니메이션&gt;어드벤쳐&gt;가족&gt;SF&gt;판타지&gt;액션 순서</p>
<pre><code>genre_sum_revenue = data.groupby(&#39;main_genre&#39;)[[&#39;revenue&#39;]].sum().reset_index()
fig = px.bar(data_frame = genre_sum_revenue, x = &#39;main_genre&#39;, y = &#39;revenue&#39;, title = &#39;장르별 흥행 수익 합계&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/fd5222e2-3e7a-4611-b5a5-b099ca9604f1/image.png" alt=""></p>
<p>흥행 수익 합계는 액션&gt;어드벤쳐&gt;드라마&gt;코미디&gt;애니메이션 순서</p>
<h3 id="연도별-장르별-수익">연도별 장르별 수익</h3>
<pre><code>revenue_by_year_genre = data.query(&#39;year &gt;= 1990&#39;).groupby([&#39;year&#39;,&#39;main_genre&#39;])[[&#39;revenue&#39;]].sum().reset_index()

fig = px.bar(data_frame=revenue_by_year_genre, x=&quot;year&quot;, y=&quot;revenue&quot;, color=&#39;main_genre&#39;, color_discrete_sequence=px.colors.qualitative.Light24_r)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/c4190b61-af22-4b69-a740-293fce301e97/image.png" alt=""></p>
<pre><code>revenue_by_year_genre_pct = pd.pivot_table(data=data.query(&#39;year &gt;= 1990&#39;), index=&#39;year&#39;, columns=&#39;main_genre&#39;, values=&#39;revenue&#39;, aggfunc=sum, fill_value=0, margins=True)
revenue_by_year_genre_pct = 100 * revenue_by_year_genre_pct.div(revenue_by_year_genre_pct.iloc[:,-1], axis=0).drop(&#39;All&#39;).drop(&#39;All&#39;, axis=1)
revenue_by_year_genre_pct = pd.melt(revenue_by_year_genre_pct.reset_index(), id_vars=&#39;year&#39;, value_name=&#39;pct&#39;)

fig = px.bar(data_frame=revenue_by_year_genre_pct, x=&quot;year&quot;, y=&quot;pct&quot;, color=&#39;main_genre&#39;, color_discrete_sequence=px.colors.qualitative.Light24_r)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/99b9e1b2-bbfe-4b93-9524-a9dec1d717b7/image.png" alt=""></p>
<p>2010년도부터 액션 영화의 흥행 수익 비중이 높아지기 시작
1990년 후반 ~ 2000년대 초반에는 드라마 장르의 흥행 수익이 높았다</p>
<pre><code>revenue_by_month_genre = data.query(&#39;year &gt;= 1990&#39;).groupby([&#39;month&#39;,&#39;main_genre&#39;])[[&#39;revenue&#39;]].sum().reset_index()

fig = px.bar(data_frame=revenue_by_month_genre, x=&quot;month&quot;, y=&quot;revenue&quot;, color=&#39;main_genre&#39;, color_discrete_sequence=px.colors.qualitative.Light24_r)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/99218986-bffd-4ed7-8b03-425f925155e7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/taeyoon__/post/ca813e34-15cd-4595-98f8-42e5b88c795a/image.png" alt=""></p>
<p>액션, 어드벤처 장르는 비교적 봄, 여름에 개봉 수익이 높았다
드라마 장르는 비교적 가을, 겨울에 개봉 수익이 높았다.
코미디 장르는 비교적 겨울에 개봉 수익이 높았다.</p>
<h3 id="수익-예산-투표수-평점의-상관관계">수익, 예산, 투표수, 평점의 상관관계</h3>
<pre><code>fig = px.imshow(data[[&#39;budget&#39;,&#39;revenue&#39;,&#39;vote_average&#39;,&#39;vote_count&#39;]].corr(), text_auto=&#39;.2f&#39;, color_continuous_scale=&#39;Purp&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/341bd763-c694-4462-a3fd-0fc6031897f7/image.png" alt=""></p>
<pre><code>for x in [&#39;budget&#39;, &#39;vote_count&#39;, &#39;vote_average&#39;]:
    fig = px.scatter(data_frame = data, x = x, y = &#39;revenue&#39;, hover_name = &#39;title&#39;, size = &#39;revenue&#39;, color = &#39;revenue&#39;
    , color_continuous_scale = px.colors.sequential.Sunsetdark, width = 700, height = 600, trendline = &#39;ols&#39;)
    fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/8dfe2cc2-c9eb-4a7d-abf1-f262cde1413f/image.png" alt="">
<img src="https://velog.velcdn.com/images/taeyoon__/post/b54f26d4-de76-459b-a564-2422dc6ecc01/image.png" alt="">
<img src="https://velog.velcdn.com/images/taeyoon__/post/dc46859a-c347-4ee0-a4e3-283fb0fca8b9/image.png" alt=""></p>
<ul>
<li>흥행 수익 상위 100개의 영화들에 대해서만 상관관계 확인</li>
</ul>
<pre><code>for x in [&#39;budget&#39;, &#39;vote_count&#39;, &#39;vote_average&#39;]:
    fig = px.scatter(data_frame = top100, x = x, y = &#39;revenue&#39;, hover_name = &#39;title&#39;, size = &#39;revenue&#39;, color = &#39;revenue&#39;
    , color_continuous_scale = px.colors.sequential.Emrld, width = 700, height = 600, trendline = &#39;ols&#39;, trendline_color_override=&#39;green&#39;)
    fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/c4515fbd-6313-40e9-a6ca-d1e499358a5c/image.png" alt="">
<img src="https://velog.velcdn.com/images/taeyoon__/post/b40beb9e-b6b3-414a-a4b9-5ddb5d8c2183/image.png" alt="">
<img src="https://velog.velcdn.com/images/taeyoon__/post/9093d52b-8ada-4e75-838a-4ed1d8ace02e/image.png" alt=""></p>
<p>예산과 투표수는 흥행 수익과 높은 양의 상관관계를 보이나, 평점 평균은 비교적 낮은 양의 상관관계를 볼 수 있다
흥행에 성공한 상위 100개로만 확인 했을때는, 그 상관관계가 더 낮아진다</p>
<h3 id="roi가-높으면서-흥행에-성공한-영화의-특징">ROI가 높으면서 흥행에 성공한 영화의 특징</h3>
<pre><code>top300 = data.sort_values(&#39;revenue&#39;, ascending=False).head(300)

fig = px.box(data_frame = top300, y = &#39;main_genre&#39;, x = &#39;roi&#39;, hover_name = &#39;title&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/759c0e8b-9067-451a-b461-02ba69f162c5/image.png" alt=""></p>
<p>액션의 경우 예산이 큰 영화가 많아 ROI가 높은 편은 아니고, 드라마/코미디/로맨스 영화가 ROI가 큰 영화들이 많다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[folium 정리]]></title>
            <link>https://velog.io/@taeyoon__/folium-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@taeyoon__/folium-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 17 Jan 2025 05:41:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/taeyoon__/post/a598f727-49ce-4cf2-889b-73c942e396e5/image.png" alt="">
기본 문법</p>
<pre><code>f = folium.Figure(width=가로길이, height=세로길이)
m = folium.Map(location=[위도, 경도], zoom_start=줌할정도).add_to(f)
m.save(&#39;test.html&#39;) # 지도 저장</code></pre><pre><code>f = folium.Figure(width=700, height=500) # 지도의 크기
m = folium.Map(location=[37.510781008592716, 127.09607026177875], zoom_start=16).add_to(f)</code></pre><h3 id="마커-추가하기">마커 추가하기</h3>
<pre><code># 장소 표시 마커
folium.Marker([위도, 경도], tooltip=마우스 오버시 나타남, popup=클릭시 나타남, icon=folium.Icon(color=색, icon=모양)).add_to(지도)

# 원 형태 마커
folium.CircleMarker([위도, 경도], radius=범위, color=색).add_to(지도)</code></pre><pre><code>folium.Marker([37.510781008592716, 127.09607026177875]
               , tooltip=&#39;롯데월드&#39;
              , icon = folium.Icon(color=&#39;red&#39;, icon=&#39;star&#39;)
              , popup = &#39;&lt;iframe src=&quot;https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/Lotte_World_Theme_Park.jpg/440px-Lotte_World_Theme_Park.jpg&quot;&gt;&lt;/iframe&gt;&#39;).add_to(m)

folium.CircleMarker([37.510781008592716, 127.09607026177875]
              , color = &#39;red&#39;
              , radius = 50).add_to(m)              </code></pre><pre><code>folium.Choropleth(geo_data=geo_json, fill_color=&#39;gray&#39;).add_to(m) # 각 구별 경계데이터를 구분</code></pre><pre><code>f = folium.Figure(width=700, height=500)
m = folium.Map(location=[37.566535, 126.9779692], zoom_start=11).add_to(f)
folium.Choropleth(geo_data=geo_json,
                  data=twosome_count,
                  columns=[&#39;시군구명&#39;, &#39;count&#39;],
                  key_on=&#39;properties.name&#39;,
                  fill_color=&#39;BuPu&#39;,
                  fill_opacity=0.7,
                  line_opacity=0.7,
                  legend_name=&#39;서울시 구별 투썸플레이스 매장수&#39;).add_to(m)</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/43a63c10-d23c-420b-9e74-df3ecae30847/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[plotly 정리]]></title>
            <link>https://velog.io/@taeyoon__/plotly-folium-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@taeyoon__/plotly-folium-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 15 Jan 2025 04:22:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/taeyoon__/post/6e7af7f5-ec50-4919-a38f-be804303baeb/image.png" alt="">
기본 문법</p>
<pre><code>fig = px.그래프종류(data_frame=데이터, x=X축 컬럼, y=Y축 컬럼, color=범례 컬럼, title=제목,
                 labels=dict(X축 컬럼=X축 라벨, Y축 컬럼=Y축 라벨),
                 width=그래프 가로길이, height=그래프 세로길이, text_auto=True/False)
fig.show()</code></pre><h2 id="산점도">산점도</h2>
<pre><code>px.scatter(data_frame=데이터, x= , y= , color=색, trendline=&#39;ols&#39;) 
# trendline은 추세선 추가</code></pre><p>facet_col: column 지정해서 분할</p>
<pre><code>fig = px.scatter(data_frame=penguins, x=&#39;bill_length_mm&#39;, y=&#39;bill_depth_mm&#39;, color=&#39;sex&#39;, trendline=&#39;ols&#39;, facet_col=&#39;island&#39;,
                 color_discrete_sequence=px.colors.qualitative.Set2, template=&#39;plotly_white&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/b151d90c-d6cc-4a67-9588-05ba5380addb/image.png" alt=""></p>
<h2 id="히스토그램">히스토그램</h2>
<p><code>px.histogram(data_frame=데이터, x= , y= , color=색)</code></p>
<pre><code>fig = px.histogram(data_frame=penguins, x=&#39;flipper_length_mm&#39;, color_discrete_sequence=px.colors.qualitative.Set2, template=&#39;plotly_white&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/31a72c6c-a13d-47bc-8b65-4e2a0e888d1c/image.png" alt=""></p>
<h2 id="상자-그림">상자 그림</h2>
<p><code>px.box(data_frame=데이터, x= , y= , color=색)</code></p>
<pre><code>fig = px.box(data_frame=penguins, x=&#39;body_mass_g&#39;, y=&#39;species&#39;, color=&#39;sex&#39;,
             color_discrete_sequence=px.colors.qualitative.Set2, template=&#39;plotly_white&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/13cecf81-20c3-438a-a1ce-d69df04f83aa/image.png" alt=""></p>
<h2 id="막대-그래프">막대 그래프</h2>
<pre><code>px.bar(data_frame=데이터, x= , y= , color=색, barmode=&#39;group&#39;)
# 쌓아서 올리지 않으면 barmode=&#39;group&#39;을 추가</code></pre><h3 id="barmode-없는-경우">barmode 없는 경우</h3>
<pre><code>fig = px.bar(data_frame=titanic_groupby, x=&#39;class&#39;, y=&#39;survived&#39;, color=&#39;sex&#39;,
             color_discrete_sequence=px.colors.qualitative.Set2, template=&#39;plotly_white&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/8e749f9a-c363-4b0a-8444-178108e9e0e1/image.png" alt=""></p>
<h3 id="barmodegroup-지정한-경우">barmode=&#39;group&#39; 지정한 경우</h3>
<pre><code>fig = px.bar(data_frame=titanic_groupby, x=&#39;class&#39;, y=&#39;survived&#39;, color=&#39;sex&#39;, barmode=&#39;group&#39;, text_auto=&#39;.2f&#39;,
             color_discrete_sequence=px.colors.qualitative.Set2, template=&#39;plotly_white&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/7977e76f-aa75-4364-b4b5-11a134b03721/image.png" alt=""></p>
<h2 id="선-그래프">선 그래프</h2>
<p><code>px.line(data_frame=데이터, x= , y= , color=색)</code></p>
<pre><code>fig = px.line(data_frame=flights, x=&quot;year&quot;, y=&quot;passengers&quot;, color=&#39;month&#39;
, color_discrete_sequence=px.colors.qualitative.Set2, template=&#39;plotly_white&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/022c28f0-8778-4005-8794-1dd8f245d18b/image.png" alt=""></p>
<h2 id="히트맵">히트맵</h2>
<p><code>px.imshow(데이터, text_auto=텍스트포맷, color_continuous_scale=컬러맵)</code></p>
<pre><code>fig = px.imshow(titanic_pivot, text_auto=&#39;.2f&#39;, color_continuous_scale=&#39;Purples&#39;)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/f9060f7d-96f0-42e6-acca-65fa1603a209/image.png" alt=""></p>
<h2 id="파이차트">파이차트</h2>
<p><code>px.pie(data_frame=데이터, values=값, name=라벨)</code></p>
<pre><code>fig = px.pie(df, values=&#39;tip&#39;, names=&#39;day&#39;, color_discrete_sequence=px.colors.qualitative.Pastel)
fig.show()</code></pre><p><img src="https://velog.velcdn.com/images/taeyoon__/post/c673a41d-efec-4468-b27a-740f38fe00ca/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>