[내배캠] 데이터분석 6기/본캠프 기록

[본캠프 21일차] SQL 코드카타, 파이썬 코드카타, 파이썬 공부

물맨두 2025. 3. 19. 01:33

 

 

오늘 한 일은, 

  • SQL 공부
    • [코드카타] SQL 3문제 풀기 (91~93번)
  • 파이썬 공부
    • [코드카타] 알고리즘 1문제 풀기 (59번)
    • [라이브 세션] 파이썬 개인과제 해설 수강하기
    • [라이브 세션] 파이썬 5회차 수강하기
    • [데이터 전처리 & 시각화] 4주차 수강하기
  • 다섯 번째 아티클 스터디 진행하기 (누적은 20번째(11+4+5))

 

SQL 공부: [코드카타] SQL 문제 풀기 (91~93번)

92. (1251) Average Selling Price

Write a solution to find the average selling price for each product. average_price should be rounded to 2 decimal places. If a product does not have any sold units, its average selling price is assumed to be 0.
Return the result table in
any order.

SELECT p.product_id,
       IFNULL(ROUND(SUM(u.units * p.price) / SUM(u.units), 2), 0) average_price
FROM Prices p LEFT JOIN UnitsSold u ON (p.product_id = u.product_id) AND
                                       (u.purchase_date BETWEEN p.start_date AND p.end_date)
GROUP BY p.product_id

저런 방식으로 테이블을 JOIN 하는 것은 처음이었으나 사실 저 부분은 금방 작성했다. (시험 삼아 '되나?' 하고 작성해봤는데 그대로 됨)

 

그런데 SELECT절의 두 번째 컬럼 average_price를 계산하는 데 시간이 많이 걸렸다. 

 

아, 그리고 처음에는 UnitsSold 테이블에 Prices 테이블을 LEFT JOIN 했는데 테이블 u에 판매된 내역이 없을 경우 그냥 텅 빈 결과 테이블이 조회된다고 틀렸다고 해서 위와 같이 JOIN 하고 SELECT절과 GROUP BY절도 테이블 p의 product_id로 수정했더니 각 제품 id별로 0이 출력되어 조회됐다.

 


 

파이썬 공부①: [코드카타] 알고리즘 문제 풀기 (59번)

59. 덧칠하기

(문제 설명 역대급으로 길어)

문제가 길긴 한데 어쩌겠어… 해야지

 

def solution(n, m, section):
    answer = 0
    wall = [i for i in range(1, n+1)]
    status = []
    
    for i in wall:
        if i in section:
            status.append(0)
        else:
            status.append(1)

    return answer

출력값을 보면 status가 어떻게 값을 담고 있는지 확인할 수 있다.

우선 벽이 칠해진 상태를 나타내는 리스트인 status를 만들었다.

빈 리스트를 열어놓고 section에 해당 구역의 숫자가 있으면 0(=칠해지지 않음)을, 없으면 1(=칠해짐)을 추가해서 status 리스트를 채웠다.

 

def solution(n, m, section):
    answer = 0
    wall = [i for i in range(1, n+1)]
    status = []
    
    for i in wall:
        if i in section:
            status.append(0)
        else:
            status.append(1)
    
    while status.count(0) > 0: #status에서 0이 존재한다면
        j = status.index(0) #status에서 첫 번째로 0이 등장하는 인덱스 j를 구해서
        status[j:j+m] = [1 for k in range(0, m)] #status에서 j부터 롤러가 한 번 칠할 수 있는 부분을 칠하고(=숫자 1로 변경)
        answer += 1 #answer에 1회 추가
                
    return answer

그리고서 위와 같이 롤러를 칠한 횟수를 while 반복문으로 구하는 코드를 작성했다.

status에 0을 센 개수가 0보다 크다면 이하의 코드 블록을 반복한다.

  • (1) 현재 status 리스트에서 0이 처음으로 등장하는 인덱스 j를 구하고
  • (2) status에서 [ j : j+m ] (=롤러가 칠할 수 있는 범위) 만큼을 숫자 1로 바꿔줌
    • 위의 부분에서 리스트에서 일정 구간의 값을 한번에 바꾸는 법을 구글링해서 참고
    • 리스트 컴프리헨션으로 status[j:j+m]과 동일한 길이로 1로만 채워진 리스트를 생성함
    • [참고] [Python] 리스트 일정 구간 통째로 바꾸기
  • (3) 그리고 answer에 1회 추가

예시로 주어진 케이스들을 전부 통과하길래 제출했더니 런타임시간 초과 에러가 대거 발생하며 틀렸다는 메시지가 떴다.

계속 고민해보다가 정말 궁금해서 질문방으로 갔다.


250319 [코드카타] 알고리즘 문제 59번에 대한 튜터님의 답변 중 일부

일단 내가 '런타임 에러 = 시간 초과'를 동일하게 생각하고 있었는데 그게 아니었다.

  • 런타임 에러 : 프로그램이 비정상적으로 종료됨 (RecursionError, ZeroDivision, ValueError 등)
    • RecursionError : 파이썬이 정한 최대 재귀 깊이보다 재귀의 깊이가 더 깊어지면 발생함
    • ZeroDivisionError : 어떤 값을 0으로 나누려고 할 때 발생함
    • ValueError : 함수가 적절한 타입의 인수를 받았지만 그 값이 부적절할 때 발생함
  • 시간 초과 : 코딩테스트에서 해당 문제에 건 제한 시간 내에 해당 코드의 연산을 마치지 못함

 

내가 제출한 코드의 경우 시간 초과로 틀렸기에 코드의 실행 속도가 효율적이지 않아 코드를 최적화해야 한다는 것이었다.

이를 설명하기 위해 튜터님이 시간복잡도라는 개념을 설명해주셨다. 로그요..... 

 

[참고1] [코딩테스트 문제 풀이 전략] 시간 복잡도 / 시간 복잡도 줄이는 Tip

[참고2] [algorithm] 시간복잡도란? 시간복잡도 계산하는법 (O(1), O(n), O(log n))

  • 복잡도 :
    문제 해결에 필요한 입력값과 문제를 해결하는 프로그램이 주어졌을 때, 해당 프로그램이 입력값을 받아 동작하고 결과를 만들어내는 데 걸리는 정도
  • 시간 복잡도 (Big-O표기법 기준)
    : 해당 프로그램이 입력값을 받아 동작하고 결과를 만들어내는 데 시간이 소요되는 정도를 입력된 N의 크기에 따라 실행되는 조작의 수
    • O(1) : 문제를 해결하는 데 오직 한 단계만 처리함
    • O(log n) : 문제를 해결하는 데 필요한 단계들이 연산마다 특정 요인에 의해 줄어듬. 크키가 커질수록 처리 시간이 짧아짐.
    • O(n) : 문제를 해결하기 위한 단계의 수와 입력값 n이 1:1 관계를 가짐. 해당 알고리즘을 주의해야 하는 것은 이를 2번 중첩해 사용할 시 바로 시간 복잡도는 O(n²)가 되어 시간이 대폭 길어짐.
    • O(n log n) : 문제를 해결하기 위한 단계의 수가 N*(log2N)번만큼의 수행 시간이 소요됨
    • O(n²) : 문제를 해결하기 위한 단계의 수는 입력값의 n의 제곱. for문을 2중 중첩으로 사용하거나 배열끼리 값을 비교/조작하거나 하면 이 정도의 시간이 소요된다고 함.


위와 같은 정보들을 참고해서 내가 작성한 코드를 보면, 음ㅎㅎ

 

#튜터님의 최적화된 코드 (section 리스트의 크기만큼만 딱 1번 반복하고 answer를 도출함)

def solution(n, m, section):
    answer = 0  # 페인트칠 횟수를 저장할 변수
    
    # 롤러가 칠한 구역의 끝 지점을 추적하는 변수
    # 만약 롤러로 1~4번 구역을 칠했다면, roller_end = 4가 됨
    roller_end = 0
    
    # section 배열을 순서대로 확인 (section에는 페인트가 필요한 구역 번호가 담겨있음)
    for s in section:
        # 현재 확인 중인 구역(s)이 이전에 칠해진 영역(roller_end) 밖에 있는지 확인
        if s > roller_end:
            # 새로운 페인트칠 시작 (횟수 증가)
            answer += 1
            
            # 롤러로 페인트칠 시작점(s)부터 롤러 길이(m)만큼 칠함
            # 롤러 길이가 m이면 s부터 s+m-1까지 칠할 수 있음
            # 예: s=2, m=4일 때 2,3,4,5번 구역이 칠해짐
            roller_end = s + m - 1
    
    return answer  # 최소 페인트칠 횟수 반환

그래서 튜터님이 보여주신 코드를 보면 꼭 필요한 만큼만 반복을 한다는 게 무엇인지 바로 알 수 있었다. 이렇게 간단한 구조로 코드를 작성할 수 있었다니...

앞으로는 알고리즘 문제 푼다고 끝이 아니라 다른 사람들이 제출한 코드도 좀 확인하고 최적화된 코드는 무엇일지도 고민해보는 습관을 가져야겠다.

 


 

파이썬 공부②: [데이터 전처리 & 시각화] 4주차 수강하기

Matplotlib 라이브러리로 데이터 시각화 구현하기

plot() 메소드로 선 그래프 그리기 (기본)

import matplotlib.pyplot as plt #matplotlib 라이브러리 사용하기 위해 라이브러리 구현하기

x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]

plt.plot(x, y) #plot() 메소드로 x축과 y축 구현하기
plt.show() #그린 함수 보기

plt.plot(x, y)
plt.xlabel('X-axis') #x축 이름 설정
plt.ylabel('Y-axis') #y축 이름 설정
plt.title('Example') #그래프 제목 설정

 

#데이터프레임으로 그래프 그리기

import pandas as pd
df = pd.DataFrame({
    'A' : [1, 2, 3, 4, 5],
    'B' : [5, 4, 3, 2, 1]
})
df

df.plot(x='A', y='B')
plt.show()

 

#선 그래프에 스타일 주기

df.plot(x='A', y='B', 
        color='red', #그래프 색상 설정
        linestyle='--', #선 스타일은 점선으로
        marker='o', #마커는 원으로 
        label= 'Data Series') #범례의 이름은 Data Series로
plt.show()

#범례 주는 방법

#방법1. .plot() 메소드에서 label 옵션으로 주기
f.plot(x='A', y='B', color='red', linestyle='--', marker='o', label= 'Data Series')

#방법2. .legend() 메소드로 입력하기
ax = df.plot(x='A', y='B', color='red', linestyle='--', marker='o')
ax.legend(['Data Series'])

 

#plot을 변수에 할당해서 사용할 수 있는 메소드

ax = df.plot(x='A', y='B', color='red', linestyle='--', marker='o')
ax.legend(['Data Series']) #범례 설정
ax.set_xlabel('X-axis') #x축 이름 설정
ax.set_ylabel('Y-label') #y축 이름 설정
ax.set_title('Title') #도표 제목 설정
ax.text(3, 3, 'Some Text', fontsize=12) #설명 텍스트 삽입
ax.text(2, 4.5, 'Some Text2', fontsize=10)

 

#도표 크기 설정하기

#방법1: .figure() 메소드 사용하기
plt.figure(figsize=(5,3))

#방법2: .subplots() 메소드 사용하기
fig, ax = plt.subplots(figsize=(7, 4))
ax = df.plot(x='A', y='B', color='red', linestyle='--', marker='o', ax=ax) #여기에도 ax=ax라는 옵션을 줘야 함

 

그래프 6가지 그려보기

  • 선 그래프(Line Plot) : 데이터의 변화 및 추이를 시각화
  • 막대 그래프(Bar Plot) : 범주별 값의 크기를 시각적으로 비교
  • 히스토그램(Histogram) : 연속형 데이터의 분포, 빈도, 패턴 등을 이해할 때 사용
  • 원 그래프(Pie Chart) : 범주별 상대적 비율을 부채꼴 모양으로 시각화
  • 상자 그림(Box Plot) : 중앙값, 사분위수, 최솟값, 최댓값, 이상치를 비롯한 데이터의 통계적 특성을 파악하는 데 용이
  • 산점도(Scatter Plot) : 변수 간 관계, 군집, 이상치 등을 확인
#(1)선 그래프 그리기

#사용할 데이터 가공하기
import seaborn as sns
data = sns.load_dataset('flights')
data_grouped = data[['year', 'passengers']].groupby('year').sum().reset_index()

#선 그래프 그리기: .plot() 메소드 사용
plt.plot(data_grouped['year'], data_grouped['passengers'])
plt.xlabel('year')
plt.ylabel('passengers')
plt.show()

 

#(2) 막대 그래프 그리기

#사용할 데이터 만들기: .DataFrame() 메소드 사용하기
df = pd.DataFrame({
    '도시' : ['서울', '부산', '대전', '대구', '광주'],
    '인구' : [9400000, 3250000, 1500000, 2300000, 1400000]
})

#한글 사용 시 폰트 깨지는 경우 추가할 코드
plt.rcParams['font.family'] = 'AppleGothic' #맥북 기준은 AppleGothic 등이 있음
plt.rcParams['axes.unicode_minus'] = False

#막대 그래프 그리기: .bar() 메소드 사용하기
plt.bar(df['도시'], df['인구'])
plt.xlabel('도시')
plt.ylabel('인구')
plt.title('도시별 인구 수')
plt.show()

 

#(3) 히스토그램 그리기

#사용할 데이터 만들기
import numpy as np
data = np.random.randn(1000)

#히스토그램 그리기: .hist() 메소드 사용하기
plt.hist(data, bins=30) #bins는 데이터의 구간 수, 구간의 경계를 정의함
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.title('Histogram')
plt.show()

 

#(4) 원 그래프 그리기

#사용할 데이터 만들기
sizes = [30, 20, 25, 15, 10]
labels = ['A', 'B', 'C', 'D', 'E']

#원 그래프 그리기: .pie() 메소드 사용하기
plt.pie(sizes, labels=labels)
plt.title('Pie Chart')
plt.show()

 

# (5) 상자 그림 그리기

#사용할 데이터 가공하기
iris = sns.load_dataset('iris')
species = iris['species'].unique()
sepal_lenghts_list = [iris[iris['species'] == s]['sepal_length'].tolist() for s in species]

#상자 그림 그리기: .boxplot() 메소드 사용하기
plt.boxplot(sepal_lenghts_list, labels=species) #matplotlib 라이브러리 사용
plt.xlabel('Species')
plt.ylabel('Sepal Length')
plt.title('Box Plot')
plt.show()

#상자 그림 그리기: .boxplot() 메소드 사용하기
sns.boxplot(x='species', y='sepal_length', data=iris) #seaborn 라이브러리 사용
plt.show()

(좌) matplotlib 라이브러리 / (우) seaborn 라이브러리

더보기

상자 그림(box plot)에 대한 설명 더보기

  • 상자(box)
    데이터의 중앙값과 사분위수(25%, 75%)를 나타냄. 상자의 아래쪽 끝은 1사분위수, 상자의 위쪽 끝은 3사분위수, 그리고 상자의 가운데 선은 중앙값을 나타냄
  • 수염(whisker)
    상자에서 뻗어나가는 선은 일반적으로 1.5배의 사분위 범위로 계산함. 수염의 끝은 데이터의 최솟값과 최댓값을 나타냄
  • 이상치(outliers)
    수염을 벗어난 개별 데이터 포인트로, 일반적인 범위를 벗어나는 값들을 의미

 

#(6) 산점도 그리기

#사용할 데이터 가져오기
iris = sns.load_dataset('iris')

#산점도 그리기: .scatter() 메소드 사용하기
plt.scatter(iris['petal_length'], iris['petal_width'])
plt.xlabel('Petal Length')
plt.ylabel('Petal Width')
plt.show()

#산점도 그리기 하나 더
plt.scatter(iris['sepal_length'], iris['sepal_width'])
plt.xlabel('Sepal Length')
plt.ylabel('Sepal Width')
plt.show()

(+)
#상관계수 살펴보기: .corr() 메소드 사용하기
iris.corr(numeric_only=True) #숫자형 데이터만 사용하라고 괄호 안에 옵션값 설정(안 하면 데이터에 숫자형 아닌 데이터들이 있으면 에러 발생)

(좌) 꽃잎의 길이와 꽃잎의 너비 간 산점도 / (우) 꽃받침의 길이와 꽃받침의 너비 간 산점도
iris 데이터의 상관계수

더보기

상관관계 확인하기

  • 양의 상관관계 :
    산점도의 점들이 오른쪽 위를 향해 일직선으로 분포함 (=하나의 변수가 증가할 때 다른 변수도 증가하는 경향을 보임)
  • 음의 상관관계 :
    산점도의 점들이 왼쪽 위를 향해 일직선으로 분포함 (=하나의 변수가 증가할 때 다른 변수는 감소하는 경향을 보임)
  • 무상관 관계
    산점도의 점들이 무작위로 퍼져 있음 (=두 변수 간 상관관계가 거의 없음) 

 

피어슨 상관계수(Pearson correlation coefficient)

  • 두 변수 간 선형적 관계를 측정하기 위한 통계 방법으로, 연속형 변수들 간 상관관계를 평가하는 데 주로 사용함
    • -1에서 1 사이의 값을 가짐
    • 1에 가까울수록 강한 양의 선형관계를 나타냄
    • -1에 가까울수록 강한 음의 선형관계를 나타냄
    • 0에 가까울수록 선형관계가 거의 없거나 약한 관계를 보임

    • r이 -1.0과 -0.7 사이이면, 강한 음적 선형관계,
      r이 -0.7과 -0.3 사이이면, 뚜렷한 음적 선형관계,
      r이 -0.3과 -0.1 사이이면, 약한 음적 선형관계,
      r이 -0.1과 +0.1 사이이면, 거의 무시될 수 있는 선형관계,
      r이 +0.1과 +0.3 사이이면, 약한 양적 선형관계,
      r이 +0.3과 +0.7 사이이면, 뚜렷한 양적 선형관계,
      r이 +0.7과 +1.0 사이이면, 강한 양적 선형관계