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

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

물맨두 2025. 3. 12. 19:32

 

(🔒사실 그냥 지친 건 아니지… 아무래도 지칠 만하니까)

오늘 한 일은, 

  • SQL 공부
    • [코드카타] SQL 5문제 풀기 (74~78번) 2문제 풀기 (74~75번)
  • 파이썬 공부
    • [라이브 세션] 파이썬 2회차 수강하기
    • [코드카타] 알고리즘 1문제 풀기 (55번)
    • [개인 과제] 5~6번 풀기
    • [파이썬 종합반] 3주차 수강하기

 


 

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

74. 특정 기간 동안 대여 가능한 자동차들의 대여 비용 구하기

CAR_RENTAL_COMPANY_CAR 테이블과 CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블과 CAR_RENTAL_COMPANY_DISCOUNT_PLAN 테이블에서 자동차 종류가 '세단' 또는 'SUV' 인 자동차 중 2022년 11월 1일부터 2022년 11월 30일까지 대여 가능하고 30일간의 대여 금액이 50만원 이상 200만원 미만인 자동차에 대해서 자동차 ID, 자동차 종류, 대여 금액(컬럼명: FEE) 리스트를 출력하는 SQL문을 작성해주세요. 결과는 대여 금액을 기준으로 내림차순 정렬하고, 대여 금액이 같은 경우 자동차 종류를 기준으로 오름차순 정렬, 자동차 종류까지 같은 경우 자동차 ID를 기준으로 내림차순 정렬해주세요.

특별히 에러 메시지 안 마주치고 풀었는데 좀 까다로워서 어떤 흐름으로 쿼리를 풀었는지 기록한다.

 

--74번 풀이 과정 해설(1) 테이블 JOIN하기

SELECT *
FROM (
    SELECT car_id,
           car_type,
           daily_fee
    FROM car_rental_company_car
    WHERE (car_type IN ('세단', 'SUV')) 
     ) c
    INNER JOIN 
     (
    SELECT car_type,
           discount_rate
    FROM car_rental_company_discount_plan
    WHERE duration_type = '30일 이상'
     ) d
    ON c.car_type = d.car_type

문제에서 주어진 테이블은 총 3개다.

  • 테이블 car_rental_company_car 
    • car_id (∵결과 테이블에 조회해야 함)
    • car_type (∵세단과 SUV에 관한 정보만 필요함)
    • daily_fee (∵결과 테이블의 컬럼 fee를 계산할 때 해당 값이 필요함)
  • 테이블 car_rental_company_rental_history
    • start_date, end_date (∵2022년 11월 동안 대여 가능한 차를 조회할 때 날짜 데이터가 필요함)
  • 테이블 car_rental_company_discount_plan
    • car_type, duration, discount_rate (∵결과 테이블의 컬럼 fee를 계산할 때 차종과 대여 기간에 따른 할인율을 적용해야 함)

각 테이블별로 모두 문제를 푸는 데 필요한 컬럼들이 있어서 처음엔 3테이블을 모두 JOIN해야겠다고 생각했었다.

 

우선 fee를 계산하는 데 필요한 car_rental_company_car와 car_rental_company_discount_plan을 INNER JOIN했다

  • JOIN에 사용한 공통 컬럼은 car_type
  • car_rental_company_car 테이블은 WHERE절로 car_type이 세단, SUV인 데이터들만 사용하게끔
  • car_rental_company_discount_plan 테이블은 WHERE절로 duration이 30일 이상인 데이터만 사용하게끔

 

--74번 풀이 과정 해설(2) where절에 서브쿼리로 30일간(2022.11.01~2022.11.30) 대여 가능한 차들만 조회하기
SELECT *
FROM (
    SELECT car_id,
           car_type,
           daily_fee
    FROM car_rental_company_car
    WHERE (car_type IN ('세단', 'SUV')) AND
          (car_id NOT IN ( --(2)테이블 c의 WHERE절 2번째 조건으로 사용된 테이블 h
              SELECT car_id
              FROM car_rental_company_rental_history
              WHERE (end_date >= '2022-11-01') AND
                    (start_date <= '2022-11-30')
          ))
     ) c
    INNER JOIN 
     (
    SELECT car_type,
           discount_rate
    FROM car_rental_company_discount_plan
    WHERE duration_type = '30일 이상'
     ) d
    ON c.car_type = d.car_type

이제 car_rental_company_rental_history 테이블까지 마저 JOIN하려는데,

생각해보니 대여 기간 관련 정보를 결과 테이블에 조회해야 하는 게 아니고 그냥 문제에서 요구하는 기간 동안 빌릴 수 있는 차들을 조회하기 위한 조건을 걸기 위해 해당 테이블을 사용해야 하는 것이어서 car_rental_company_car 테이블의 WHERE절에 서브쿼리로 작성할 수 있겠다는 생각이 들었다.

 

그래서 car_rental_company_car 테이블의 car_id가 서브쿼리로 조회되는 car_rental_company_rental_history 테이블의 car_id를 비교해 필터링하기로 했다.

 

250312 그냥 머릿속으로만 생각하니까 헷갈려서 손으로 수직선에다가 날짜 범위 끼적여가면서 풀었다

날짜에 대한 조건을 어떻게 줘야 할지 고민하는 데 많은 시간을 소요했는데 작성한 쿼리처럼 최종적으로 조건을 작성했다.

  • 조건① : end_date >= '2022-11-01' 대여 종료일이 11월 1일을 포함한 그 이후 기간에 포함됨
    • 왜냐하면 그 전에 대여가 종료됐다면 start_date도 그 이전일 것이고 이 경우에 속하는 대여 기록은 볼 필요 없음
  • 조건② : start_date <= '2022-11-30' 대여 시작일이 11월 31일을 포함한 그 이전 기간에 포함됨
    • 왜냐하면 그 이후에 대여를 시작하는 기록 역시 볼 필요 없음
  • 조건① AND 조건② 두 조건을 모두 충족한다는 의미는 2022년 11월에 그 차는 대여 중이라는 의미

날짜 조건을 위와 같이 주고 나니 c.car_id NOT IN (h.11월에 대여 기록이 있는 car_id ) 형태의 쿼리가 완성됐다.

 

----74번 풀이 과정 해설(3)최종 쿼리(문제에서 요구한 조건들 마저 반영하기)
SELECT c.car_id,  #결과 테이블에 조회될 컬럼들 SELECT절에 작성
       c.car_type,
       FLOOR(c.daily_fee * 30 * (1 - d.discount_rate/100)) fee
FROM (
    SELECT car_id,
           car_type,
           daily_fee
    FROM car_rental_company_car
    WHERE (car_type IN ('세단', 'SUV')) AND
          (car_id NOT IN (
              SELECT car_id
              FROM car_rental_company_rental_history
              WHERE (end_date >= '2022-11-01') AND
                    (start_date <= '2022-11-30')
          ))
     ) c
    INNER JOIN 
     (
    SELECT car_type,
           discount_rate
    FROM car_rental_company_discount_plan
    WHERE duration_type = '30일 이상'
     ) d
    ON c.car_type = d.car_type
GROUP BY c.car_id, c.car_type #컬럼 fee를 계산하기 위한 GROUP BY절 작성
HAVING (fee >= 500000) AND (fee < 2000000) #문제에서 요구한 대여 금액의 범위를 HAVING절로 작성
ORDER BY fee DESC, c.car_type, c.car_id DESC #결과 테이블 정렬 조건 작성

 

그 이후는 크게 어려울 것 없이 결과 테이블에 조회될 내용들을 반영해 마저 쿼리를 작성하면 된다.

특이한 점이라면 fee의 값이 정수로 나타나도록 FLOOR() 함수를 사용하는 것과 HAVING절에 조건 작성 시 금액 범위 조건이 '50만원 이상 200만원 미만'이기 때문에 BETWEEN을 굳이 사용하지 않는 게 좋다는 정도 같다.

 

75. 자동차 대여 기록별 대여 금액 구하기

CAR_RENTAL_COMPANY_CAR 테이블과 CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블과 CAR_RENTAL_COMPANY_DISCOUNT_PLAN 테이블에서 자동차 종류가 '트럭'인 자동차의 대여 기록에 대해서 대여 기록 별로 대여 금액(컬럼명: FEE)을 구하여 대여 기록 ID와 대여 금액 리스트를 출력하는 SQL문을 작성해주세요. 결과는 대여 금액을 기준으로 내림차순 정렬하고, 대여 금액이 같은 경우 대여 기록 ID를 기준으로 내림차순 정렬해주세요.

--75번 첫 번째 제출한 쿼리
SELECT h.history_id,
       FLOOR(c.daily_fee * h.rental_duration * (1 - d.discount_rate/100)) fee
FROM (
    SELECT car_id,
           daily_fee
    FROM car_rental_company_car
    WHERE car_type = '트럭'
    ) c
    INNER JOIN
    (
    SELECT car_id,
           history_id,
           (DATEDIFF(end_date, start_date)+1) rental_duration,
           CASE WHEN DATEDIFF(end_date, start_date)+1 >= 90 THEN '90일 이상'
                WHEN (DATEDIFF(end_date, start_date)+1 < 90) AND (DATEDIFF(end_date, start_date)+1 >= 30) THEN '30일 이상'
                WHEN (DATEDIFF(end_date, start_date)+1 < 30) AND (DATEDIFF(end_date, start_date)+1 >= 7) THEN '7일 이상'
           ELSE '할인 없음' END duration_type
    FROM car_rental_company_rental_history
    ) h
    ON c.car_id = h.car_id
    INNER JOIN
    (
    SELECT duration_type,
           discount_rate
    FROM car_rental_company_discount_plan
    WHERE car_type = '트럭'
    ) d
    ON h.duration_type = d.duration_type
GROUP BY h.history_id
ORDER BY fee DESC, h.history_id DESC

첫 번째 작성한 쿼리를 실행했을 때 에러 뜨는 것 없이 결과 테이블이 조회되길래 제출했더니 틀렸다. 사실 이 정도로 쿼리가 복잡해지니 틀린 부분을 발견해서 수정할 생각을 하면 시작하기도 전부터 한숨이 나온다. 그렇지만 어쩌겠어, 내가 작성한 거야... 내가 해결해야지. (괴롭다 증말)

 

 멍하니 모니터를 바라보다가 car_rental_company_discount_plan 테이블(이후 d 테이블)을 JOIN할 때 INNER JOIN을 한 게 문제인가 싶었다.

테이블 d를 JOIN할 때 공통컬럼으로 h.duration_type = d.duration_type으로 조건을 주었는데, h.duration_type은 내가 테이블 d와 조인할 때 사용할 의도로 새로 만든 컬럼이다. CASE WHEN 구문을 사용해 만들었는데 이때 대여 기간이 7일 미만인 경우에 '할인 없음'이라는 조건까지 만들어뒀다. 그런데 INNER JOIN을 하게 되면서 할인율이 없는 경우의 데이터들이 누락된 게 아닌가 싶었다.

 

--75번 두 번째 제출한 쿼리[정답 처리]
SELECT sub.history_id,
       FLOOR(sub.daily_fee * sub.rental_duration * (1 - sub.discount_rate/100)) fee
FROM (
    SELECT c.daily_fee,
           h.history_id,
           h.rental_duration,
           IFNULL(d.discount_rate, 0) discount_rate
    FROM (
        SELECT car_id,
               daily_fee
        FROM car_rental_company_car
        WHERE car_type = '트럭'
        ) c
        INNER JOIN
        (
        SELECT car_id,
               history_id,
               (DATEDIFF(end_date, start_date)+1) rental_duration,
               CASE WHEN DATEDIFF(end_date, start_date)+1 >= 90 THEN '90일 이상'
                    WHEN (DATEDIFF(end_date, start_date)+1 < 90) AND (DATEDIFF(end_date, start_date)+1 >= 30) THEN '30일 이상'
                    WHEN (DATEDIFF(end_date, start_date)+1 < 30) AND (DATEDIFF(end_date, start_date)+1 >= 7) THEN '7일 이상'
               ELSE '할인 없음' END duration_type
        FROM car_rental_company_rental_history
        ) h
        ON c.car_id = h.car_id
        LEFT JOIN
        (
        SELECT duration_type,
               discount_rate
        FROM car_rental_company_discount_plan
        WHERE car_type = '트럭'
        ) d
        ON h.duration_type = d.duration_type
    ) sub
GROUP BY sub.history_id
ORDER BY fee DESC, sub.history_id DESC

첫 번째 쿼리의 결과 테이블과 비교했을 때, 두 번째 쿼리의 결과 테이블이 더 많은 데이터들을 포함하여 조회됐다.

 

두 번째 쿼리는 테이블 d를 이번에는 LEFT JOIN으로 합쳤다. 그러니 대여 기간이 7일 미만인 건들도 누락되지 않고 조회했다. 이제 discount_rate 컬럼의 NULL값을 0으로 대체해야 하는데 어떻게 해야 하는지 모르겠어서 지금까지의 작성한 쿼리를 냅다 서브쿼리로 묶어서 해당 서브쿼리를 FROM절에서 인라인 뷰로 사용해서 sub.discount_rate에 IFNULL() 함수를 사용해 NULL값을 0으로 대체했다. 그 이후는 첫 번째 쿼리에서 했듯이 컬럼 fee를 구하고 ORDER BY절로 정렬해서 제출했더니 정답 처리됐다.

 


 

파이썬 공부①: 파이썬 개인 과제 풀기

우선 어제 5번 문제를 풀다가 자러 가서 마저 이어서 풀러 고고!


( 👇 어제 파이썬 개인 과제 얼마나 풀었는지 보러 가기 👇 )

 

 

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

(  )(  ) 아침…내일배움캠프. 내일배움캠프면 내일 배워야 하는데 나는 왜 지금 이렇게 공부를 하고 있나, 그런 생각을 했다.………아무래도 얼른 주말이 와야 할 것 같다. 오늘 한 일은, SQL 공

maandoo.tistory.com


문제5

#문제5. 이메일 주소 유효성 프로그램

def validate_emails(email_list):
    for i in range(len(email_list)):
      if '@' in email_list[i]:
        id, domain = email_list[i].split('@')
        if len(id) > 0:
          if '.' in domain:
            return f'{email_list[i]}는 유효한 이메일 주소입니다.'
          else:
            f'{email_list[i]}는 유효하지 않은 이메일 주소입니다.'
        else:
          return f'{email_list[i]}는 유효하지 않은 이메일 주소입니다.'
      else:
        return f'{email_list[i]}는 유효하지 않은 이메일 주소입니다.'

어제까지 작성해 본 코드다. 이렇게 코드를 작성하고 코드를 실행했는데 email_list에 담긴 이메일 4개에 대한 판단 메시지가 뜨는 게 아니라 리스트 중 첫 번째 값에 대한 값만 떴다. 

 

for문에 email_list의 값들을 하나씩 출력해내는 것을 보면 for문이 잘못 써진 것은 아닌데 for문에 if문을 중첩시키면 또 다시 email_list[0]에 대해서만 유효성 검사 결과를 반환한다.

 

진짜 잘못된 점을 발견하지 못하겠어서 질문방에 질문을 올렸다.

그런데 충격적인 답변이 돌아왔다… return이 함수의 마침표 같아서 return을 한번 만나면 그냥 그대로 함수가 종료된다는 것이었다.

(출처 : https://m.blog.naver.com/hjy5405/222611745864)

그래서 return을 남발하지 않게 코드를 수정했다.

#문제5. 이메일 주소 유효성 프로그램
def validate_emails(email_list):
    answer = []
    for i in range(len(email_list)):
      if '@' in email_list[i]:
        id, domain = email_list[i].split('@')
        if len(id) > 0:
          if '.' in domain:
            answer.append(f'{email_list[i]}는 유효한 이메일 주소입니다.')
          else:
            answer.append(f'{email_list[i]}는 유효하지 않은 이메일 주소입니다.')
        else:
          answer.append(f'{email_list[i]}는 유효하지 않은 이메일 주소입니다.')
      else:
        answer.append(f'{email_list[i]}는 유효하지 않은 이메일 주소입니다.')
      return answer

그러면 return을 한 번만 써주면 되는 건가 싶어서 이메일 유효성에 대한 검사를 진행한 문구들을 answer라는 리스트에 담아주고

for문 종료 후에 answer를 반환하도록 작성했다. 그렇지만 내 의도대로 코드는 실행되지 않았고 여전히 email_list의 첫 번째 요소만 판단한 후에 validate_emails 함수가 종료됐다.

 

def validate_emails(email_list):
    answer = [] #각 이메일 주소들이 유효한지를 담을 빈 리스트 answer를 선언
    for email in email_list:
      if '@' not in email: 
        answer.append(f'{email} 유효하지 않은 이메일 주소입니다.') #먼저 '@'의 포함 여부를 판단하여 해당 문자가 없는 경우를 걸러냄
      else:
        id, domain = email.split('@') #.split() 메소드를 사용해 '@'의 앞은 id, 뒤는 domain으로 선언
        if (len(id) >= 1) and ('.' in domain) and (len(domain) >= 2): 
          answer.append(f'{email} 유효한 이메일 주소입니다.') #우선 유효한 이메일에 관한 모든 조건을 만족하는 이메일을 걸러냄
        else: answer.append(f'{email} 유효하지 않은 이메일 주소입니다.') 
    return answer #if문을 통해 유효 메시지들이 저장된 answer를 반환함

for문 구조가 너무 조잡한가 싶어서 좀 더 단순한 방식으로 위와 같이 코드를 정리했더니 드디어 원하는 방식으로 출력됐다!

(사실 통과된 코드를 보고서야 전의 코드를 다시 보면 조잡한 게 문제가 아니라 그냥 이상하게 썼다는 걸 드디어 알아차렸다)

 

난이도 : CHALLENGE

문제6

#문제6. 각 문자가 등장하는 빈도를 함께 출력하는 프로그램 구현

def remove_duplicates_and_count(s):
    result_with_frequency = []
		
    for i in s:
      if i not in result_with_frequency:
        result_with_frequency.append((i, s.count(i)))
    
    return result_with_frequency

처음 작성한 코드는 에러 없이 돌아갔지만 중복값에 아랑곳하지 않고 전부 담아냈다.

 

#문제 6. 각 문자가 등장하는 빈도를 함께 출력하는 프로그램 구현

def remove_duplicates_and_count(s):
    result_with_frequency = []
    letters = [] #result_with_frequency에 담긴 문자들을 저장하는 리스트를 선언
		
    for i in s:
      if i not in letters: #letters에 문자가 안 담겼을 경우로 if문을 수정함
        letters.append(i) 
        result_with_frequency.append((i, s.count(i)))
        
    return result_with_frequency

그래서 result_with_frequency뿐만 아니라 s에서 등장한 문자만을 담을 letters 리스트를 만들었다. for문 속 if문의 조건을 letters를 활용하여 result_with_frequency에 이미 담겼는지를 확인할 수 있도록 코드를 작성했더니 원하는 결과값을 얻을 수 있었다.

 


 

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

55. 카드 뭉치

(문제 설명이 길다 이젠)

#55번 문제의 첫 번째 코드
def solution(cards1, cards2, goal):
    for i in goal: #goal을 기준으로 반복문을 진행함
        if i == cards1[0]: #[조건1] goal[i]의 값과 cards1의 첫 번째 값이 일치하는지 비교함
            cards1.pop(0) #[조건1]을 통과하면 cards1에서 일치한 값을 삭제
        elif i == cards2[0]: #[조건2] [조건1]을 통과하지 못하면 cards2의 첫 번째 값과 일치하는지 비교함
            cards2.pop(0) #[조건2]를 통과하면 cards2에서 일치한 값을 삭제
        else:
            answer = 'No' #[조건1]도, [조건2]도 통과하지 못하면 answer는 "No"
    if (len(cards1) == 0) and (len(cards2) == 0):
        answer = 'Yes' #for문 종료 후 cards1과 cards2가 텅 비면 answer는 "Yes"
    return answer

위와 같이 작성한 코드를 실행했더니 1개는 성공했는데 다른 케이스에선 IndexError가 발생했다. 하 

cards1과 cards2가 텅 비어버렸을 때를 판단하는 if문이 따로 떨어져 있어서 그런가 싶어서 다음과 같이 코드문을 수정했다.

#55번 문제의 두 번째 코드
def solution(cards1, cards2, goal):
    answer  = 'Yes' #우선 answer를 Yes로 선언해놓음
    for i in goal:
        if (len(cards1) > 0) and (i == cards1[0]): #[조건1]에 cards1 리스트가 빈 상태가 아닌지를 판단하도록 추가
            cards1.pop(0)
        elif (len(cards2) > 0) and (i == cards2[0]): #[조건2]에 cards2 리스트가 빈 상태가 아닌지를 판단하도록 추가
            cards2.pop(0)
        else:
            answer = 'No'
    return answer

이렇게 수정했더니 이번에는 정답 처리됐다.

 

그렇지만 첫 번째 코드와 두 번째 코드 간 어떤 식으로 달리 연산하길래 이런 차이가 발생하는 건지 궁금해서 해당 부분에 대해 질문방에 또 게시글을 작성했다. 거의 뭐 질문방에서 살겠어 아주

#튜터님이 예시로 작성한 코드
def solution(cards1, cards2, goal):
    answer  = 'Yes'
    for i in goal:
        if len(cards1) and (i == cards1[0]): #len(cards1)가 0이면 false로 판단하기에 이렇게만 적어줘도 됨
            cards1.pop(0)
        elif len(cards2)  and (i == cards2[0]): #len(cards2)가 0이면 false로 판단(…)
            cards2.pop(0)
        else:
            answer = 'No'
    return answer

첫 번째 코드에서 IndexError가 발생했던 것은 cards1[0]에 해당하는 값이 없으면(=cards1 리스트에 담겨 있는 요소가 하나도 없음) 발생하는 문제였다. 그래서 두 번째 코드와 같이 'len(cards1) > 0'처럼 cards1 리스트가 비어 있지 않았다는 조건도 같이 들어가야 IndexError를 피할 수 있다.

 

그리고 조건문에서 숫자 0은 False로, 0이 아닌 값은 true로 인식하기 때문에 'len(cards1) > 0'라고 쓰지 않고 'len(cards1)'로 적어주는 것만으로도 작성한 조건문에선 동일하게 실행된다. 

[참고] [python] 파이썬, True, False 불(bool) 자료형 사용법 및 예제 총정리

 


 

파이썬 공부③: [데이터 분석 파이썬 종합반] 3주차 수강하기

조건문

  • if, elif, else 키워드를 사용해 특정 조건이 True인 경우에 특정 코드 블록을 실행하도록 프로그램의 흐름을 제어라는 구문
  • 들여쓰기와 띄어쓰기 주의할 것!!

반복문

for문

  • 리스트, 문자열, 딕셔너리 등 반복 가능한(iterable) 데이터 타입들에서 그 속의 각 객체들에게 해당 코드 블록을 반복해서 실행하는 구문
  • 반복문을 실전에서 사용한다면,
    • 데이터 정제 및 전처리(결측치 제거 등)
    • 모델의 성능 평가 등

while문

  • for문과 유사하나, 특정 조건이 True일 때 실행되는데 그 조건이 False가 될 때까지 특정 코드 블록을 반복해서 실행하는 구문
  • 무한 루프에 빠지지 않도록 주의할 것 !!
    • 반복문에서 반복 동작을 제어하는 제어문
      • break : break가 실행되면 해당 반복문에서 반복을 멈추고 나가는데, 주로 특정 조건에 부합할 경우 반복문에서 빠져나오기 위해 사용함
      • pass : 조건문에 넣어줄 조건이 딱히 없을 경우에 해당 위치에 pass를 사용하여 에러가 발생하지 않고 코드가 실행되도록 함
      • continue : break와 비교하여, 반복문은 계속 실행되는데 continue가 실행되면 해당 순번에서 진행할 하위 코드를 수행하지 않고 바로 다음 순번으로 넘어가게

3주차 Quiz

 


 

내일 추가 발제 있다는 공지,, 내가 그냥 잘못 본 거였으면 좋겠어. ……컴퓨터 너무 오래해서 시력 이슈 있는 듯(제발)