지난 글에서 서울특별시 공중화장실 공중데이터를 판다스 데이터프레임으로 만들고 간단히 정제작업을 해 보았는데요. 정제한 데이터프레임을 가지고 태블로를 이용해서 아주 간단히만 시각화 작업을 진행해 보았습니다.

 

 

 

대시보드 구성 방법

  1. 서울시의 25개 구별 공중화장실 수 합계를 계산하여 그 수를 비교할 수 있도록 시각화했습니다.
  2. 대시보드의 왼쪽에는 지도를 배치하여 화장실의 수를 원의 크기와 색깔로 직관적으로 파악할 수 있도록 구성했습니다
    • 지도를 확대하면 보이지 않는 레이블을 모두 확인할 수 있어요.
    • 화장실 수가 많을수록 원의 크기가 큽니다.
    • 화장실 수가 많을수록 원의 색깔이 진합니다.
  3. 대시보드의 오른쪽에는 가로막대그래프를 배치하여 수치별로 좀더 직관적인 비교가 가능하도록 구성했습니다.
    • 오른쪽의 비교 파라미터를 이용해서 비교선을 100단위로 조절하면서 이동시킬 수 있어요.
    • 비교선을 움직이면서 비교선을 기준으로 색깔이 바뀌는 것을 확인할 수 있어요.

 

태블로 퍼블릭으로 보러가기 (클릭)

 

지하철

지하철

public.tableau.com

클릭하시면 태블로 퍼블릭 웹사이트에서 인터렉티브하게 직접 결과를 조절해 보실 수 있습니다.

 


 

 

<시각화 이후 생각해볼만한 것들>

 

1. 구별 화장실 수와 구별 면적의 관계는 어떻게 되는가?
2. 구별 화장실 수와 지하철 역의 개수의 상관관계가 있는가?
3. 구별 화장실 수와 구별 인구 수의 관계는 어떻게 되는가? 인구와 구별 화장실 총 수는 비례하는가?
4. 서울특별시 구별 장애인 화장실 데이터를 따로 구할 수 있는가? 구할 수 있다면, 전체 화장실과 장애인 화장실의 비율을 비교해 보자.
5. 상업 단지와 화장실 수의 상관관계가 있는가?

6. 관광 구역과 화장실 수의 상관관계가 있는가?

7. 공공화장실 중에서 지하철 역 화장실의 개수를 특정할 수 있는가? 있다면 그 비율은 어떻게 되는가?

8. 지하철 역 화장실 개수를 구할 수 있다면, 지하철 노선별 유동인구 데이터와 병합하여 화장실의 갯수가 적절하게 비치되어 있는지 비교해보자.

 

 

 

여기까지입니다 :-) 좀더 생각해볼 수 있을것 같지만 본격적인 개인 EDA 프로젝트 준비를 위해서 이번엔 이정도로 간단히만 포스팅을 마치려고 합니다. 쉽고 짧은 작업이었지만 맘에드는 공공데이터를 구하고, 정제하고, 시각화 후 생각해볼거리 도출까지 은근 시간이 걸렸네요. 다음엔 더 능숙하고 멋진 프로젝트를 가져와서 공유해보겠습니다. 감사합니다. :)

 

 


 

 

(+) 판다스 데이터프레임 CSV파일로 내보내기 한 후 태블로에 불러온 과정

 

t.to_csv("toilet_df.csv")

 

먼저 to_csv() 메소드를 통해 간단하게 csv파일로 내보내기를 해주었습니다. 저는 구글 코랩에서 실습을 진행했습니다.

 

 

 

 

 

구글 코랩의 왼쪽 파일메뉴에서 간단하게 바로 다운로드를 해서 다운로드 폴더에 넣어 주었는데요. 로컬 환경에 따로 다운로드하지않고 태블로 퍼블릭 환경에서 구글 드라이브를 연동해서 바로 오픈해도 됩니다.

 

 

 

 

 

태블로 퍼블릭 프로그램을 실행하고 로컬 환경에 다운로드한 toilet_df.csv 파일을 오픈했습니다.

 

 

 

따로 사용하지 않을 예정인 산지, 부지번 컬럼은 숨기기(Hide) 해주었습니다. 이후 시트 2개에서 작업을 하고 대시보드 1개에서 두개를 합쳐주는 방식으로 간단히 끝을 내주었습니다.

 

 

 

 
서울시 공중화장실 공공데이터를 가지고 아주 간단한 데이터 시각화, 분석 실습을 해 보려고 합니다.
먼저 이번 포스팅에는 파이썬 pandas 라이브러리를 이용해서 데이터 전처리 작업한 것을 간단히 정리해 보았습니다. 
데이터 시각화, 분석은 태블로 프로그램을 이용하여 마친 뒤 다음 포스팅에 이어서 올리도록 하겠습니다.
 
 
https://data.seoul.go.kr/dataList/OA-1370/S/1/datasetView.do

열린데이터광장 메인

데이터분류,데이터검색,데이터활용

data.seoul.go.kr

 
사용한 데이터 링크입니다.
 


 

1. pandas : 필요없는 컬럼 삭제, 인덱스 지정

import pandas as pd

t = pd.read_csv("toilet.csv", engine='python', encoding = "cp949")
t

 
pd.read_csv를 이용해서 데이터를 불러오는데 한글 깨짐이 좀 있어서 encoding = "cp949"를 이용해주니 깔끔하게 불러오기가 잘 되었습니다.

 
먼저 value_counst()를 이용해서 대략적으로 확인해보니 별 다른 정보가 담겨있지 않은 관계로 새주소명, 생성일 컬럼은 삭제하기로 결정했습니다. 또, 고유번호에 중복값이 없는것을 확인한 관계로 고유번호를 인덱스로 지정해주겠습니다.

# 새주소명, 생성일 컬럼 드랍(삭제)
t = t.drop('새주소명', axis=1)
t.drop('생성일', axis = 1, inplace = True)

# 고유번호 컬럼 중복값 없는지 확인
len(t['고유번호'].unique()) == len(t['고유번호'])  # True

# 고유번호 인덱스화
t.set_index('고유번호', inplace = True)

 
필요없는 컬럼을 삭제하고 인덱스를 고유번호로 바꾸어 주니 어느정도 보는 게 깔끔해졌습니다.
이제 구명과 법정동명을 확인해보려고 하는데요.
 
 

2. 구명

t['구명'].value_counts()

 
value_counts()를 이용해서 확인해 보니

  1. 끝에 '구'가 붙어 있지 않은 구
  2. 오타 작렬한 구
  3. 빌딩이 왜 여기서 나와? 갈암구는 또 어디야? 갈현송방차풀소는 뭐야?

이것들을 해결해줘야 할 것 같습니다.
먼저 구가 안붙어있는 것들에 구를 붙여줘 보기로 했습니다. (예:노원 > 노원구)

t['구명'] = t['구명'].apply(lambda x: x + '구' 
                          if x in ['동작', '금천', '강서', '양천', '노원', '관악', '영등포', '서대문'] 
                          else x)

 
'구명'이 동작/금천/강서/양천/노원/관악/영등포/서대문 중 하나인 경우
컬럼에 apply와 lambda 함수를 이용해서 끝에 '구'를 붙여 줬습니다. 해당사항이 없는 경우는 그냥 놔두도록 처리했습니다.

t['구명'].value_counts()


그 외 갈현송방차풀소~남서울빌딩에 해당하는 row들은 그냥 제거하겠습니다.

# 이것들에 해당하는 '구명'을 가진 row들을 제외한(~) 줄만 남겨서 t에 재할당
t = t[~t['구명'].isin(['갈현송방차풀소', '송북구', '송파ㅜ', '영등로구', 
                      '영등표구', '송파두성빌딩', '갈암구', '구로수', '남서울빌딩'])]
len(t['구명'].value_counts().index)
# 25

 
혹시 몰라 확인해 보니 총 25개의 구가 있는것이 잘 확인되었습니다. 검색해보니 서울에는 25개 자치구와 426개 행정동이 있다고 하네요! 서울 살면서도 계속 까먹어요... 상식으로 외워둬야지.
 
법정동은 426개를 일일이 확인하기 불가능 + 의미가 없는 것 같아서 일단 놔두도록 하겠습니다. 다음 포스팅에는 태블로를 이용하여 간단한 시각화를 해서 가져오도록 하겠습니다!
 

 

 

 
 
벌써 4주 차 회고를 작성합니다. 4주차도 매일 1시간 30분 일찍 도착해서 아침공부를 하고, 9-6 수업을 듣고, 6-9 저녁공부를 하고 집에 갔습니다. 뿌듯합니다.
 
 
분명 첫주에 패딩을 입고 다녔었는데... 이 회고를 작성하고 있는 오늘(일요일)의 낮 최고 기온이 28도에 육박했습니다. 그래서 반팔 반바지를 입고 나가서 카공을 하고 왔어요. 후텁지근했지만 아직 습하지는 않아서 따뜻하고 공기가 바삭한 게 저는 참 좋았습니다. 매일 하는 게 공부밖에 없는 단조로운 일상을 보내다 보니, 이런 날씨 변화가 주변을 좀 더 돌아보고 한숨 돌리게 하는 것이 반가웠습니다.
 
 
이번 주에 처음으로 이력서라는 걸 작성했습니다. 임용고시에 합격한 교사는 교육청에서 알아서 발령을 내 주기 때문에 이력서나 자기소개서 같은 걸 작성할 필요가 없습니다. 그래서 경험이 전무했어요. 다행히 지난번에 들었던 이력서와 자기소개서 작성 특강 내용이 큰 도움이 되었습니다. 특강 오신 강사님께서 공유해주신 기본 템플릿을 참고해서 기본 이력서를 국문과 영문으로 하나씩 작성하고, 혹시 몰라 노션 페이지도 간단하게 제작을 했습니다. 생각보다 오랜 시간과 많은 노력을 들여야 했습니다.
 
 
항상 학생들이 쓴 글을 평가하고 학부모들이 제출한 신청서를 심사하고 분류하는 일을 하던 제가 이제는 저좀 뽑아주십사 서류를 써서 제출하는 입장이 된 것이 재밌습니다. 사람 일이 어떻게 될지 모른다는 걸 매일 체감합니다. 과연 내가 인사 담당자라면 나의 이력서를 보고 마음이 동할까? 하는 한편의 메타인지를 발동해 가며 최대한 나를 잘 표현하고자 노력했어요. 힘들었지만 재밌기도 했습니다. 저의 성장 과정에 대해 돌아보고 스스로의 강점과 단점에 대해 숙고해 보는 경험을 통해 앞으로 원하는 기업에 지원하기까지 무엇을 더 보충하고 발전시켜야 할 것인지 플랜을 세울 수 있었습니다.
 
 
아무래도 아직 완성한 프로젝트가 하나도 없다 보니 프로젝트 칸은 비워뒀어요. 생각은 하고 있었지만, 텅 빈 프로젝트란을 보니 제대로 된 프로젝트 뭐든 하나라도 있어야한다는 생각이 확 들더라고요. 지금까지 저는 데이터분석과 머신러닝에 필요한 각종 프레임워크에 익숙해지고 기술적인 능력을 키우는 데 집중을 하고 있었거든요. 공부는 계속하되 가능한 빨리 개인 프로젝트를 구체화기로 마음먹었습니다. 다만 프로젝트 주제를 정하는 일이 생각보다 어려운 게 걱정입니다. 동기들과 이야기해 보면 다들 관심 있는 분야 하나정도 가지고 있더라고요. 금융이면 금융, 게임이면 게임, 유통이면 유통, 그런 거 말이죠. 저는 딱히 하나의 분야에 꽂혀 있지는 않고, 데이터 사이언티스트로서 머신러닝과 딥러닝을 아우를 수 있는 멀티플레이어로 성장하고 싶다.. 뭐 그런 거시적인 목표만 있는 상태입니다. 그래서 주제 구체화가 더욱 막막한데요. 앉아서 머릿속으로 고민만 하면 답이 나오지 않는다는 걸 인생 경험으로 알고 있기 때문에 먼저 데이터 수집부터 이것저것 시작해보려고 합니다.
 
 
이번주에 수업에서 웹 크롤링 기법 여러가지를 배웠는데, 크롤링을 해본 적이 없던 저에게 정말 큰 도움이 되었습니다. 소위 노가다성 작업이었음에도 불구하고 생각보다 훨씬 더 재밌었어요. 어떤 식으로 블로그에 정리를 할까 하다가, 수업 내용을 그대로 정리해서 올리면 편하긴 하겠지만 남는 게 없을 것 같아서, 간단한 토이 플젝을 진행해 봤습니다. 연금복권 720+ 당첨 결과를 크롤링 후 정제해서 데이터프레임화하고 간단한 분석을 해보는 실습을 하고 과정을 정리해 포스팅했어요. 정말 재밌었고 복습이 쏙쏙 됐습니다. 역시 스스로 생각하고 작업한 내용을 글로 출력하면 배운 내용을 체화하고 내 것으로 만드는 데 정말 큰 도움이 되네요. 다음 주에는 셀레니움 프레임워크 사용하는 법을 배우는데 큰 기대가 됩니다.
 
 
다음 주 회고에서는 주제를 좀더 구체화해서 가지고올 수 있기를 기대하며, 다들 일교차와 냉방병 조심하시고 건강하세요.
 
감사합니다.
 


 
파이썬 requests, BeautifulSoup 라이브러리를 이용한 웹크롤링 후 데이터분석 실습을 해보았습니다 :-) 
 
연금복권720+은 제가 한달에 2-3회정도 꾸준하게 구매하는 최애 복권인데요. 슬프게도 지금까지 제대로 당첨된 적은 단 한번도 없지만, 앞으로도 저는 꾸준히 구매를 할 예정인 아주아주 매력적인 복권입니다. 1등에 당첨이 되면 (세전) 700만원을 매월 20년동안 수령할 수 있어요. 동행복권 온라인 사이트에서 간단히 온라인 구매를 할 수도 있구요. 1등 번호는 온라인 1명, 오프라인 1명 총 2명이 당첨될 수 있습니다. 자세한 복권 구조는 동행복권 홈페이지를 참고해 보시구요.
 
복권의 경우 통계를 공부해보신 분들께는 아주 친숙한 소재이실텐데요. (저는 아닙니다.ㅋㅋㅋ) 동행복권 사이트에서는 복권 당첨번호를 엑셀파일로도 제공하고 통계 자료를 따로 분석해서 메뉴도로 제공하고 있습니다. 다만 저는 철저히 requests 라이브러리를 이용한 웹크롤링에 익숙해지기 위해서 엑셀 파일이나 통계자료를 건드리지 않고 처음부터 끝까지 혼자 힘으로 본 실습을 했습니다! 
 

 
[참고] 본 포스팅은 수리링 본인의 공부 기록을 목적으로 작성하였습니다. 해당 라이브러리에 대해 전혀 모르시는 분께서 보면서 따라하시기엔 많이 불친절하게 느껴질 수 있습니다. 참고하시고 봐 주시면 감사드리겠습니다 :-)
 
[참고] 본 포스팅은 책, 강의, 다른 사람의 포스팅을 참고하지 않은 스스로의 창작물입니다! 참고하여 포스팅 하시는 경우 출처 밝혀주심 감사드리겠습니다!
 


 
[실습 목차]

  1. 206회차로 모의 실습
  2. 원하는 회차 구간을 입력하면 모든 정보를 담아 데이터프레임으로 리턴하는 함수 작성
  3. 데이터프레임으로 간단한 데이터분석 (은근 재밌으니 귀찮으시면 이것만 보고 가세요...^^)

1-1. 숨은 URL 찾아내기

 
동행복권 사이트의 회차별 당첨번호 페이지(클릭)에 가 봅니다.
 

 
 
회차 바로가기 메뉴를 통해 원하는 회차를 선택해서 당첨 결과를 볼 수 있었습니다.
 

 
 
그런데 기본 URL에 회차 정보가 드러나지 않고 숨어 있어요. 206회를 조회해도, 200회를 조회해도 계속 같은 URL이 유지됩니다. 따라서 회차를 특정하여 정보를 뽑아낼 수가 없는 상황입니다. 우리는 회차를 조회할 수 있는 상세URL을 알아내야 해요.
 
문제상황을 해결하기 위해 크롬 웹브라우저의 inspection(개발자 도구) 메뉴의 Network 탭을 확인해 봅시다.
 

 
 
위와 같이 네트워크 탭을 켜둔 상태로 조회 버튼을 눌러봅니다. Name 탭의 맨 첫 번째 gameResult 어쩌구를 클릭한 다음 Payload를 확인합니다. (누가 봐도 수상한) Round: 206 이라는 정보를 확인했습니다. 기존 url 뒤에 &Round=206을 붙여 주면 될 것 같다는 합리적 의심을 해봅니다.
 

https://dhlottery.co.kr/gameResult.do?method=win720&amp;amp;Round=205

 
 
주소 뒤에 &Round=205 를 붙여넣고 엔터를 치니 205회 당첨결과 페이지로 잘 이동합니다 ㅎㅎ 찾았다 요놈! 이제 상세 url주소를 찾았으니 코드를 작성하면서 원하는 데이터를 뽑아내 보겠습니다.
 


 

1-2. requests, BeautifulSoup 라이브러리

* 본 실습에서 해당 라이브러리에 대한 상세 설명은 생략합니다

import requests
from bs4 import BeautifulSoup as BS

 
먼저 requests와 BeautifulSoup 라이브러리를 임포트해줍니다.

url = "https://dhlottery.co.kr/gameResult.do?method=win720&Round=206"
res = requests.get(url)
soup = BS(res.text, "html.parser")

 
우리가 찾아낸 url을 선언해 준 다음 차례대로 라이브러리에 넣어서 html 자료를 뽑아냅니다.

 
soup을 실행해 보니 html 정보가 잘 들어왔습니다 :)
저는 html 코드를 하나하나 뜯어보면서 원하는 정보를 뽑아내 봤어요.

nums = rows[0].find_all("span", {"class":"num"})

#조, 당첨번호
group = int(nums[0].find("span").text)
n_1 = int(nums[1].find("span").text)
n_2 = int(nums[2].find("span").text)
n_3 = int(nums[3].find("span").text)
n_4 = int(nums[4].find("span").text)
n_5 = int(nums[5].find("span").text)
n_6 = int(nums[6].find("span").text)

print(f"{group}조 {n_1}, {n_2}, {n_3}, {n_4}, {n_5}, {n_6}")
# '3조 4, 8, 9, 0, 7, 5'

 
먼저 제일 중요한 1등 조, 6개의 당첨번호를 뽑아봤습니다. 

# 등위(등수명) 
# rows[0]이므로 첫번째 1등을 구함 -> 나중에 인덱스를 바꾸어 다른 등수의 이름도 구할 수 있음
rank = rows[0].find_all("td")[0].text
rank
# '1등'

 
등수명도 뽑아봤어요. 이정도는 그냥 작성해도 되지만 연습삼아서 뽑아봤습니다 :)

# 당첨결과(매)
rank_counts = int(rows[0].find_all("td", {"class":"ta_right"})[-1].text.strip())
rank_counts
# 2

 
1등의 당첨 매수를 뽑아봤습니다. 206회차는 1등이 2명입니다. 연금복권 1등은 온라인/오프라인 각1명씩 최대 2명이 나올 수 있습니다. 가끔 1등이 1명밖에 없을 때도 많아요. 아주 드물게 0명일 때도 있는 거 같아요.

# 보너스 당첨번호 6자리
bonus_nums = []
for i in range(6):
    bonus_num = rows[7].find_all("span", {"class" : "num"})[i].find("span").text
    bonus_num = int(bonus_num)
    bonus_nums.append(bonus_num)

print(bonus_nums)
# [5, 8, 7, 6, 9, 5]

 
보너스 당첨번호 6자리도 뽑아봤습니다.

# 보너스 당첨결과(매)
bonus_counts = int(rows[7].find_all("td", {"class":"ta_right"})[-1].text.strip())
bonus_counts
# 10

 
10명이나 당첨됐네요.
 


 

2-1. 회차를 입력할 수 있는 함수로 작성해보기

206회차를 가지고 적당히 연습을 해 봤으니, 원하는 회차를 입력하면 하나씩 모두 조회해서 딕셔너리로 담아 리턴하는 함수를 작성해 보았습니다.

def win720(round):
	# 입력받은 회차 번호로 url을 만들고 정보를 받아냅니다.
    url = f"https://dhlottery.co.kr/gameResult.do?method=win720&Round={round}"
    res = requests.get(url)
    soup = BS(res.text, "html.parser")
    rows = soup.find("tbody").find_all("tr")
	
    # data_dict에 앞으로 하나씩 정보를 추가할 겁니다. 먼저 라운드 값을 첫 번째로 넣어줬습니다.
    data_dict = {"round":round}
    nums = rows[0].find_all("span", {"class":"num"})

    # 1등 조, 당첨번호
    group = int(nums[0].find("span").text)
    n_1 = int(nums[1].find("span").text)
    n_2 = int(nums[2].find("span").text)
    n_3 = int(nums[3].find("span").text)
    n_4 = int(nums[4].find("span").text)
    n_5 = int(nums[5].find("span").text)
    n_6 = int(nums[6].find("span").text)

    data_dict["group"] = group
    data_dict["n_1"] = n_1
    data_dict["n_2"] = n_2
    data_dict["n_3"] = n_3
    data_dict["n_4"] = n_4
    data_dict["n_5"] = n_5
    data_dict["n_6"] = n_6

    # 1-7등 당첨자수
    for i in range(7):
        rank_counts = rows[i].find_all("td", {"class":"ta_right"})[-1].text.strip()
        rank_counts = re.sub(",","", rank_counts)
        rank_counts = int(rank_counts)
        column_name = f"rank{i+1}"
        data_dict[column_name] = rank_counts

    # 보너스 당첨번호 6개
    for i in range(6):
        bonus_num = rows[7].find_all("span", {"class" : "num"})[i].find("span").text
        column_name = f"bonus_{int(i)+1}"
        data_dict[column_name] = bonus_num

    # 보너스 당첨자수
    bonus_counts = int(rows[7].find_all("td", {"class":"ta_right"})[-1].text.strip())
    data_dict["bonus"] = bonus_counts

    return data_dict

 
더럽게 길지만 그래도 잘 작동했습다^_^;;;; 
너무 길어져서 쓰면서 불길했는데 그래도 오류 수정 2-3번만에 원하는 대로 값이 나와서 다행이였어요

 
205회차로 테스트를 해 봤는데요. 조, 1등 넘버 6자리, 등수별 당첨매수, 보너스 번호 6자리, 보너스 당첨매수가 딕셔너리로 제대로 들어온 것을 확인했습니다 :) 이게 뭐라고 너무 재밌었어요 (ㅋㅋㅋㅋ)
 


 

2-2. 회차 구간을 설정하고 데이터프레임을 리턴하는 함수 작성하기

 
위에서 작성한 win720()함수를 가지고 원하는 회차 구간의 모든 정보를 담은 데이터프레임을 반환하는 함수를 작성해 주었습니다.

def lucky_chart(start, end):
    lucky_results = []

    for i in range(start, end+1):
        win = win720(i)
        values = list(win.values())
        print(values)
        print(len(values))
        lucky_results.append(values)

    columns = list(win.keys())
    print(columns)
    print(len(columns))
    
    import pandas as pd
    df = pd.DataFrame(lucky_results, columns = columns)
    
    return df.set_index("round")

 
중간 중간에 있는 print 함수들은 제가 함수를 작성하면서 중간 과정을 시각화하기 위해 굳이 넣어줬구요, 깔끔하게 없애줘도 됩니다.

history = lucky_chart(190, 206)

 
190회부터 206회차까지 럭키차트 함수를 돌려보았습니다.

 
요런식으로 진행상황을 시각화 하기 위해 print 함수를 넣어줬습니다. (중간에 오류가 있었어서 저런식으로 시각화 하면서 수정해줬어요!)
 

history

 
알흠다운 판다스 데이터프레임이 완성되었어요 ❤️
 


 

3. 간단 데이터분석

마지막으로 데이터분석은 1회차부터 206회차까지로 구간을 늘려서 진행했습니다! (1등은 제껍니다.)
 

  • 1등 조 비율이 어떻게 될까?
# 조별 value_counts() 구하기
group_counts = history['group'].value_counts()

# matplotlib 임포트
import matplotlib.pyplot as plt

# 차트 작성
plt.pie(group_counts,
        labels = group_counts.index,
        shadow = True,
        autopct = '%1.2f%%')
plt.title("Rank 1 groups ratio")
plt.legend()
plt.show()

 
아주.. 흥미로운.. 결과입니댜... 연금복권720+ 1회차부터 206회차까지 모든 데이터들을 살펴본 결과... 지금까지 가장 많은 1등을 배출한 조는 4조였습니다. 4조 > 1조 > 3조 > 5조 > 2조 순이네요. 
 
연금복권을 아는 분들께서는 이해를 하실텐데, 저는 혹시라도 번호 6개를 다 맞췄지만 조가 다를 때 2등이라도 당첨되도록 + 혹시라도 1등이 되면 2등도 동시 당첨되도록(ㅋㅋㅋ) 번호 6개를 고르고 나면 모든 조(1~5조)로 총 5줄(5,000원)을 구매해버립니다. 솔직히 이게 더 확률이 낮을 것 같기는 한데.......... 만약 특정 조를 골라서 구매해야 한다면 앞으로 저는 1조 또는 4조를 고르겠습니댜.
 

  • 지금까지 역대 2등은 몇명이 나왔을까?
import seaborn as sns

sns.set_style("darkgrid")
sns.set_palette("bright")

sns.barplot(history["rank2"].value_counts())
history['rank2'].agg(['min', 'max', 'mean'])
# min     0.0
# max     8.0
# mean    4.5

 
1등에 가려 2등의 당첨자 수는 사실 잘 확인해 본 적이 없는데요. 2등에 당첨되면 매달 100만원씩 10년간 수령할 수 있거든요. 2등이라도 당첨시켜 주신면 제가 굉장히 감사할텐데요. 몇 명이나 당첨되나 보니, 역대 2등 당첨자 수는 최대 8명, 최소 0명(ㅋㅋㅋㅋㅋㅋ), 평균 4.5명이 나왔다고 합니다. 그래프로 확인해 보니 2등이 한 명도 나오지 않은 회차가 20번이 넘네요? 실화냐?
 

  • 그럼 1등이 한 번도 안 나온 회차도 있을까?
history['rank1'].value_counts()
# 1    101
# 2     63
# 0     42
sns.countplot(data = history,
              x = 'rank1',
              legend = 'full')

아니 미친... 1등이 0명인 회차가 40회가 넘는다고? 1등이 2명 다 나온 적보다 1명밖에 안 나온 적이 더 많다고? 여러분 빨리 연금복권 사세요! 저거 다 우리돈이라고(흥분)
 

  • 1번부터 6번까지 각 자리마다 번호가 몇번씩 나왔을까
df = pd.DataFrame()

for i in range(1, 7):
    col = f"n_{i}"
    df[col] = history[col].value_counts().sort_index()

df
for i in range(1, 7):
    col = f"n_{i}"
    print(f"{i}번째 자리에서 가장 많이 나온 숫자는 {df[col].idxmax()}")

 
각 자리에서 가장 많이 나온 숫자는 순서대로 4 - 4 - 9 - 0 - 5 - 6 이었습니다. 이게 엄청 큰 의미가 있을지는 모르겠지만, 해당 자리에 어떤 숫자를 고를지 고민되신다면 이 정보도 참고해 봐도 좋을 것 같습니다.

for i in range(1, 7):
    col = f"n_{i}"
    print(f"{i}번째 자리에서 가장 조금 나온 숫자는 {df[col].idxmin()}")

 
반대로 각 자리별로 가장 조금 나온 숫자도 구해봤어요. 두 정보를 종합하면, 숫자 0 4 7 9가 자주 보이네요. 반대로 0 4 7 9를 제외한 1 2 3 5 6 8 을 고르는 것도 안전하게 갈 수 있는(?) 방법일 수도 있을 것 같고.. 복권의 세계는 정말 어렵네요.
 
tmi지만 저는 항상 첫자리를 6으로 구매를 하는데, 첫자리 6은 꼴찌 0에 이어서 두 번째로 나온 횟수가 적네요. 전략을 바꿔야 하나.... 고민이 되지만 (ㅋㅋㅋㅋㅋㅋㅋㅋㅋ) 복권 통계는 재미일 뿐이라고 생각합니다.
 
 
 
 
여기까지 간단 분석을 마쳐 보겠습니다! :-)
감사합니다.
 

 
 

 

https://school.programmers.co.kr/learn/courses/30/lessons/12945

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

오랜만에 피보나치 수열 문제 풀이를 했습니다. 처음에 간단한 재귀함수를 적용해서 풀이를 했더니 테스트는 모두 통과하는데 효율성 테스트에서 시간 초과로 계속 실패를 하는 문제가 발생했습니다. 그래서 효율성을 올리기 위해 메모화를 적용해서 풀이를 했더니 효율성 테스트도 모두 통과할 수가 있었습니다 :-)

 

간단하게 풀이방법을 설명해 보겠습니다!

 

 

피보나치 수열

 


1. 메모화 없이 기본 피보나치 수열 함수 작성하기

def basic_fibonacci(n):
    if n in [1, 2]:
        return 1
    if n == 3:
        return 2

    return basic_fibonacci(n-2) + basic_fibonacci(n-1)

 

만약 주어진 정수가 1 또는 2라면 바로 1을 리턴,

주어진 정수가 3이라면 바로 2를 리턴하도록 초기 설정을 해 줍니다.

4 이상의 정수 n이 주어졌을 때 재귀함수 형식으로 n-2와 n-1에 basic_fibonacci 함수를 적용한 값을 구해 더한 값을 리턴합니다.

basic_fibonacci(8), basic_fibonacci(9), basic_fibonacci(10), basic_fibonacci(11)
# (21, 34, 55, 89)

 

위 함수는 제대로 작동하지만, n의 값이 커질수록 효율성이 급격하게 떨어진다는 단점이 있어요. 메모화를 이용해서 함수의 효율을 높여보겠습니다.


2. 메모화를 적용한 피보나치 수열 함수 작성하기

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    if n in [2, 3, 4, 5, 6]:
        return memo[n]
    
    if n > 6:
        for i in range(7, n+1):
            if i in memo: pass
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    return memo[n]

 

하나씩 뜯어보겠습니다 :)

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):

 

가장 먼저 함수를 선언할 때 파라미터 값으로 memo라는 이름으로 딕셔너리를 포함해 주었습니다. 저는 간단하게 n이 1부터 6번째일때 피보나치 수열 값을 딕셔너리에 초기값으로 미리 메모를 해 두었습니다.

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    if n in [2, 3, 4, 5, 6]:
        return memo[n]

 

내가 구하고자 하는 것은 피보나치 수열의 n번째 값입니다. 만약 n이 2, 3, 4, 5, 6인 경우 (문제 조건에서 n은 2 이상의 정수라고 했으므로 1은 제외합니다) memo 딕셔너리의 해당하는 value 값을 바로 리턴하고 함수를 종료하게 됩니다.

    if n > 6:
        for i in range(7, n+1):
            if i in memo: pass
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    return memo[n]

 

만약 n이 6보다 크면 어떻게 해야 할까요?

 

주어진 n의 값이 50일 때를 예시로 생각해 봅시다. 우리는 50번째 값을 구하기 위해 48, 49번째 값이 필요합니다.

49번째 값을 구하기 위해서는 47, 48번째가 필요하고

48번째 값을 구하기 위해서는 46, 47번째가 필요합니다.

결국 50번째 값을 구하기 위해서는 1부터 49번째까지 피보나치 수열 값을 모두 알아야 합니다.

 

그럼 메모화는 어떻게 작동할까요?

제가 만약 50번째 피보나치 수열 값을 구하기 전에,

40번째 피보나치 수열 값을 먼저 구했다고 생각해 보겠습니다.

fibonacci(40)
# 102334155

 

우리는 40번째 피보나치 수열 값을 구하기 위해 지금 1번째~39번째 피보나치 수열의 값을 열심히 구했습니다.

이제 다음으로 50번째 피보나치 수열 값을 구하려고 합니다.

 

 

먼저 메모화를 해놓지 않은 경우를 생각해 봅시다.

우리는 이미 1번부터 40번째까지의 값을 이전에 미리 구한 전적이 있지만

따로 기록을 해 두지 않았기 때문에 같은 계산을 또 반복해야 합니다.

1번째~49번째 피보나치 수열 값을 열심히 또 구하는 거죠.

 

하지만 제가 기록을(메모화를) 해 두었다면 얘기가 달라집니다.

즉 처음에 fibonacci(40)을 계산했을 때

memo 딕셔너리에 1번부터 40번까지 피보나치 수열 값을

key(n) : value(값) 형식으로 미리 저장을 해 두었다면,

우리는 fibonacci(50)을 구할 때 

1. 1번부터 40번까지는 간단히 딕셔너리에서 값을 찾아 가져올 수 있습니다.

2. 41번부터 49번째 값은 새로 구하고 딕셔너리에 값을 추가해주는 방식으로 계속해서 메모를 해나갈 수 있습니다.

 

+ 참고로 함수를 여러번 작동하면서 메모장에 추가하는 key와 value 값은 매번 초기화되지 않고 계속해서 누적 기록을 보관합니다.

 

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    # 주어진 정수가 2-6중 하나일 경우 바로 메모장에서 수열값을 찾아 리턴, 함수종료
    if n in [2, 3, 4, 5, 6]:
        return memo[n]
    
    # 주어진 정수가 6보다 큰 경우(7 이상)
    if n > 6:
    	# 7부터 n까지의 정수 i에 대해서
        for i in range(7, n+1):
            # 만약 메모장에 i값이 이미 메모되어 있다면 그냥 넘어가고
            if i in memo: 
                pass
            # 만약 메모장에 i값이 없다면 피보나치 수를 구해서 추가해주세요
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    # 메모장에서 n번째 피보나치 수열 값을 찾아 리턴해주세요
    return memo[n]

 

최종 피보나치 함수입니다 :)

내가 구하고자 하는 n의 피보나치 값을 찾기 위해 7부터 n-1번째의 수열값을 계속해서 차례대로 메모장에 누적 메모해나가는 방식입니다. 재귀함수랑 약간 비슷한듯 다른 느낌이네요!


3. 최종 문제풀이

마지막으로 문제 해결을 위해 피보나치 수열을 활용해서 soluton()함수를 작성했습니다. 저는 코딩테스트 문제를 풀 때 이렇게 함수를 나누어 작성하는것을 좋아합니다. 함수 안에 함수 작성하는거 시러요.....(개취)

def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
    if n in [2, 3, 4, 5, 6]:
        return memo[n]
    
    if n > 6:
        for i in range(7, n+1):
            if i in memo: 
                pass
            else:
                memo[i] = memo[i-2] + memo[i-1]
    
    return memo[n]

def solution(n):
    fibo_num = fibonacci(n)
    
    # fibonacci(30)< 1234567 < fibonacci(31)
    # 만약 n이 30 이하라면 바로 fibo_num을 리턴하고(몫이 0이므로 나머지와 동일)
    if n <= 30 : 
        return fibo_num
    # 만약 n이 30보다 크다면 fibo_num을 1234567으로 나눈 나머지를 리턴
    else :
        return fibo_num % 1234567

 

정확성: 100.0
합계: 100.0 / 100.0

 

 

 

 

풀이나 설명에 오류가 있다면 댓글로 알려주세요!

감사합니다! :-)

 

 

FutureWarning: use_inf_as_na option is deprecated and will be removed in a future version. Convert inf values to NaN before operating instead. with pd.option_context('mode.use_inf_as_na', True): 

 

주피터 노트북이나 코랩에서 pandas, seaborn, matplotlib 등 라이브러리를 사용할 때 위와 같이 퓨처 워닝 어쩌구 하면서 경고 메세지가 나타나서 꼴보기 싫은 경우가 있습니다.

 

이럴 때 warning 라이브러리를 임포트하는 방식으로 간단하게 해결이 가능합니다.

 

import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

 

before

 

after

 

 

 

 

 

코딩테스트 문제를 해결할 때면 2개 이상의 정수의 최대공약수 또는 최소공배수를 구해야 하는 경우가 종종 있습니다. 파이썬에서 최대공약수와 최소공배수는 넘파이 라이브러리를 이용해서 아주 간단하게 구해낼 수 있는데요. 

 

먼저 함수를 소개해 드리기 전에, 최대공약수와 최소공배수를 영어로 뭐라고 부르는지 알고 넘어갈게요.

  • gcd : greatest common division (최대공약수)
  • lcm : lowest common multiple (최소공배수)

이런 간단한 영어 정도는 숙지해 두시면 좀더 직관적으로 쉽게 프로그래밍하실 수 있어요 :-)

 

[1] 최대공약수 구하기

import numpy as np

 

먼저 넘파이 라이브러리를 임포트해줍니다.

np.gcd(12, 20)
# 4

np.gcd(30, 45)
# 15

 

np.gcd() 함수 안에 두 정수를 넣으면 두 정수의 최대공약수를 리턴합니다. gcd인 이유는 위에서 설명한 대로 greatest common divison의 약자 gcd가 최대공약수를 의미하기 때문입니다.

 

만약 3개 이상의 정수의 최대공약수를 구하고 싶다면 어떻게 할 수 있을까요?

np.gcd.reduce([15, 25, 35])
# 5

np.gcd.reduce([15, 27, 18])
# 3

before = np.arange(0, 20, 5)
after = np.gcd.reduce(before)
after
# 5

 

np.gcd.reduce() 함수 안에 리스트 또는 넘파이 어레이를 넣어주면 리스트 또는 어레이 안의 모든 정수의 최대 공약수를 리턴합니다.

 

 

[2] 최소공배수 구하기

np.lcm(10, 14)
# 70

np.lcm.reduce([10, 15, 20])
# 60

before = np.array([3, 7, 10])
after = np.lcm.reduce(before)
after
# 210

 

np.lcm() 함수 안에 두 정수를 넣으면 두 정수의 최소공배수를 리턴합니다. lcm인 이유는 위에서 설명한 대로 lowest common multiple의 약자 lcm이 최소공배수를 의미하기 때문입니다.

 

마찬가지로 np.lcm.reduce()  함수 안에 리스트 또는 넘파이 어레이를 넣어서 3개 이상의 정수의 최소공배수도 구해낼 수 있습니다.

 

 


 

 

알고 있으면 아주아주 유용한 파이썬 넘파이 모듈 이용해서 최소공배수/최대공약수 구하는 방법이었습니다.

 

감사합니다 :-)

 

 

문자열을 분리하고 변형해야 하는 코딩 테스트 문제에서 공백 문자가 연속으로 나오는 경우가 많이 있습니다. 공백을 기준으로 문자열을 분리할 때 자연스럽게 split() 함수를 사용하게 되는데, 이 경우 연속 공백을 상실하게 되는 문제점이 발생합니다.

 

연속 공백을 유지하면서 문자열을 분리하여 리스트로 만들기 위해 정규식(regular expression)을 사용할 수 있습니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/12951#

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

프로그래머스 JadenCase 문자열 만들기 문제를 가지고 방법을 설명해 보겠습니다. (다른 다양한 문제에서도 활용할 수 있습니다!)


 

문제 : JadenCase란 모든 단어의 첫 문자가 대문자이고, 그 외의 알파벳은 소문자인 문자열입니다. 단, 첫 문자가 알파벳이 아닐 때에는 이어지는 알파벳은 소문자로 쓰면 됩니다. 문자열 s가 주어졌을 때, s를 JadenCase로 바꾼 문자열을 리턴하는 함수, solution을 완성해주세요.

 

이렇게 공백문자가 연속해서 나올 수 있다는 조건이 있는 경우 split() 함수를 사용하면 공백 여러개가 모두 없어져버리므로 조심해야 합니다.

 

 

저는 먼저 단어 1개를 JadenCase로 바꾸어 리턴하는 함수 jaden을 작성했습니다.

def jaden(w):
    try:
    	# 첫 번째 글자가 정수일 때
        int(w[0]) 
        return w[0] + w[1:].lower() 
    except:
    	# 첫 번째 글자가 정수가 아닐 때
        return w[0].upper() + w[1:].lower()

 

이렇게 작성한 jaden 함수를 가지고 최종 solution 함수를 작성해 볼게요.

 

import re

 

먼저 정규식 패키지를 불러옵니다.

 

word = "Hello       World   Bye"

 

위의 예시를 단어와 공백문자로 구분한 리스트를 어떻게 만들 수 있을까요?

 

word = "Hello       World   Bye"
re.findall("\S+|\s+",word)
# ['Hello', '       ', 'World', '   ', 'Bye']
  • re.findall() : RE가 일치하는 모든 부분 문자열을 찾아 리스트로 반환합니다. 활용해서 원하는 구성요소를 찾아 리스트로 만들어줍니다.
  • \s+ : 일치하는 모든 공백 문자를 찾아줍니다. (여러 개의 공백도 하나로 평탄화하지 않고 유지한 채로 찾아줍니다.)
  • \S+ : 일치하는 모든 비공백 문자를 찾아줍니다.

결과를 보면 여러 개의 공백 문자가 잘 보존되어 있는 것을 볼 수 있습니다. 이제 최종 솔루션 함수를 작성해 볼게요.

 

def jaden(w):
    try:
    	# 첫 번째 글자가 정수일 때
        int(w[0]) 
        return w[0] + w[1:].lower() 
    except:
    	# 첫 번째 글자가 정수가 아닐 때
        return w[0].upper() + w[1:].lower()

import re

def solution(s):
	# 입력 받은 s를 whitespace와 nonwhitespace로 구분합니다
    words = re.findall("\S+|\s+", s)
    answer = ""
    for word in words:
    	# 만약 공백문자가 아니라면 jaden 함수를 적용해서 answer에 추가합니다.
        if word.isalnum(): 
        	answer += jaden(word)
        # 만약 공백문자라면 그냥 바로 answer에 추가합니다.
        else: 
        	answer += word
    return answer

 

 
정확성: 100.0
합계: 100.0 / 100.0
 

 

끝입니다! 정규식이 기억에 잘 남지는 않지만,
잘만 숙지해 두면 연속 공백문자 처리를 해야하는 문제에서 요긴하게 잘 쓸 수 있답니다 :-) 
읽어주셔서 감사합니다!

 

프로그래머스에 비트 연산을 해야하는 MySQL문제가 종종 보이는데요. 정리해놓으면 좋을 것 같아서 문제 풀이를 한번 작성해 보겠습니다.

 


https://school.programmers.co.kr/learn/courses/30/lessons/301646

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

문제 : 2번 형질을 보유하지 않으면서 1번이나 3번 형질을 보유하고 있는 대장균 개체의 수(COUNT)를 출력하는 SQL 문을 작성해주세요. 1번과 3번 형질을 모두 보유하고 있는 경우도 1번이나 3번 형질을 보유하고 있는 경우에 포함합니다.


[1] 2진법으로 계산해 보기

먼저 각 열의 GENOTYPE의 수를 2진법으로 바꾸어 출력해 보겠습니다. 주어진 정수 데이터에 BIN() 함수를 사용하면 정수를 2진법 수로 변환한 결과를 출력합니다.

SELECT BIN(GENOTYPE) AS GENOTYPE_BIN
FROM ECOLI_DATA

 

그럼 이제 출력된 결과를 CAST함수를 이용해서 CHAR 데이터타입으로 바꾼 뒤, WHERE문을 사용해서 필터링을 해 보겠습니다.

SELECT COUNT(*) AS `COUNT` 
FROM (SELECT ID,
             CAST(BIN(GENOTYPE) AS CHAR) AS GENOTYPE_BIN
             FROM ECOLI_DATA) A
WHERE A.GENOTYPE_BIN LIKE '1' OR
      A.GENOTYPE_BIN LIKE '%10_' OR
      A.GENOTYPE_BIN LIKE '%01'

 

WHERE문의 각 줄은 다음과 같은 결과를 필터링합니다.

  1. 서브쿼리문의 수가 1인 경우
  2. 서브쿼리가 101 또는 100인 경우
  3. 서브쿼리가 01로 끝나는 경우

마지막으로 SELECT COUNT(*) AS `COUNT`를 통해 필터링된 데이터 열의 갯수를 COUNT라는 컬럼명으로 출력하도록 해 주었습니다. 정답 통과입니다.

 

 


[2] 비트 연산으로 풀어보기

MySQL에는 비트(Bit) 단위로 논리 연산을 수행하는 연산자가 있습니다. 챗지피티한테 비트 연산자의 종류에 뭐가 있는지 테이블을 만들어 달라고 했는데요.

비트 AND (&) 비트별 AND 연산을 수행합니다. 두 비트가 모두 1이면 결과는 1이 되고, 그렇지 않으면 결과는 0이 됩니다.
비트 OR ( | ) 비트별 OR 연산을 수행합니다. 두 비트 중 하나라도 1이면 결과는 1이 되고, 둘 다 0이면 결과는 0이 됩니다.
비트 XOR (^) 비트별 XOR 연산을 수행합니다. 두 비트가 같으면 결과는 0이 되고, 다르면 결과는 1이 됩니다.
비트 NOT (~) 비트를 반전시킵니다. 0은 1로, 1은 0으로 변환됩니다.
비트 왼쪽 시프트 (<<) 모든 비트를 왼쪽으로 이동시킵니다. 오른쪽에 0으로 채워집니다.
비트 오른쪽 시프트 (>>) 모든 비트를 오른쪽으로 이동시킵니다. 왼쪽에 부호 비트와 같은 값으로 채워집니다.

 

네. 이런게 있다네요. (^_^;;) 저는 여기서 &연산자와 NOT 연산자를 사용해서 위의 문제를 풀어보겠습니다.

 

SELECT * FROM ECOLI_DATA
WHERE
    GENOTYPE & 5
    AND NOT GENOTYPE & 2

 

GENOTYPE & 5

  • 정수 5를 2진법 비트로 변환하면 101입니다. 따라서 &5는 값의 첫 번째, 세 번째 비트가 1인지 여부를 확인합니다.

NOT GENOTYPE & 2

  • 비트 NOT 연산은 비트를 반전시키는 역할을 합니다. 여기서 2의 이진 표현은 10입니다. 따라서 이 비트 연산은 GENOTYPE 열의 값에서 2번째 비트를 확인하고, 그 값을 반전시키므로, 2번째 비트가 0인지 여부를 확인합니다.
SELECT COUNT(ID) AS `COUNT`
FROM ECOLI_DATA
WHERE
    GENOTYPE & 5
    AND NOT GENOTYPE & 2

 

마지막으로 카운트한 값을 출력해 주면 정답 통과입니다.


 

비트 연산자는 간단하게 풀 수 있지만 까먹기 쉽다는 단점이 있는 것 같습니다.

그래도 이런 게 있다는 걸 알아두고 필요할 때마다 열심히 꺼내 보면서 익숙해 져야겠습니다.

 

 

 

 

MySQL에서 JOIN을 이용하여 두 테이블 간의 정보를 조회할 때, 차집합(Set Difference)을 구해야 할 경우가 있습니다. 이럴 때 LEFT JOIN,RIGHT JOIN에 WHERE구문을 추가 활용하여 쉽게 표현해볼 수 있습니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/59044

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

관련 문제로 프로그래머스 MySQL 코딩테스트 3단계 문제 오랜 기간 보호한 동물(1) 풀이를 함께 첨부해 보겠습니다.

 

 


 

 

문제 상황 : 아직 입양을 못 간 동물 중, 가장 오래 보호소에 있었던 동물 3마리의 이름과 보호 시작일을 조회하는 SQL문을 작성해주세요. 이때 결과는 보호 시작일 순으로 조회해야 합니다.

 

 

SQL문을 실행하면 다음과 같이 나와야 합니다.


 

먼저 ANIMAL_INS 테이블에 ANIMAL_OUTS 테이블을 LEFT JOIN한 결과를 보겠습니다.

SELECT I.NAME, I.DATETIME
FROM ANIMAL_INS I
	LEFT JOIN ANIMAL_OUTS O
	ON I.ANIMAL_ID = O.ANIMAL_ID

 

아직 아무 조건을 걸어주지 않은 관계로 위 코드가 출력하는 결과는

SELECT NAME, DATETIME

FROM ANIMAL_INS와 동일합니다.

 

 

저는 여기서 A-B를 구하기 위해

A와 B의 교집합을 제거해 주려고 합니다.

어떻게 해볼 수 있을까요?

SELECT I.NAME, I.DATETIME
FROM ANIMAL_INS I
	LEFT JOIN ANIMAL_OUTS O
	ON I.ANIMAL_ID = O.ANIMAL_ID
WHERE O.ANIMAL_ID IS NULL

 

OUT 테이블의 아이디가 없는 데이터만 출력하도록 WHERE O.ANIMAL_ID IS NULL을 추가해 주었습니다.

 

이제 마지막으로 이 결과에서 가장 오래 보호소에 있었던 동물 3마리의 이름을 고르고, 결과는 보호 시작일 순으로 조회하도록 조건을 추가하겠습니다.

SELECT I.NAME, I.DATETIME
FROM ANIMAL_INS I
	LEFT JOIN ANIMAL_OUTS O
	ON I.ANIMAL_ID = O.ANIMAL_ID
WHERE O.ANIMAL_ID IS NULL
ORDER BY I.DATETIME ASC LIMIT 3;

 

정답 통과 입니다.

 

 

참고한 다이어그램

출처: 구글 이미지

 


 

비슷한 문제로 프로그래머스 3단계 '없어진 기록 찾기' 문제도 풀어보시면 좋을 것 같습니다 :-)

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/59042

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

ASAC 빅데이터 분석 & AI 5기 과정 3주 차가 지났습니다.

 

 

 

집에서 편하게 쉬엄쉬엄 공부를 하다가 매일 강의실에 물리적으로 출퇴근을 하는 일이 쉬운 일은 아니었습니다만, 이쯤 되니 다행히도 몸이 잘 적응을 한 것 같습니다. 아무래도 지난 7년간 학교에 출퇴근하던 일에 비교하면 (비교할 수 없을 만큼) 훨씬 살만하네요. 학교에서 학생을 대하고 가르치는 일은 체력적, 정신적으로 굉장히 큰 에너지를 소모하는데요. 그에 비하면 지금은 그냥 방학 같아요. 내가 하고 싶은 공부를 원 없이 할 수 있는 환경과 자원이 지원된다는 게 감사할 따름입니다.

 

과정을 시작하면서 제 자신과 약속한 것이 한 가지 있어요. 바로 출퇴근하는 월요일부터 금요일까지는 반드시 아침에 일찍 일어나자는 것이었는데요. 저는 저에 대해서 꽤 잘 알거든요. 내가 원하는 시각에 망설임 없이 일어나서 하루를 시작할 수 있느냐 없느냐가 저의 그날 하루 전체 생산성을 결정합니다. 지금까지 쉬엄쉬엄 편하게 공부해 왔으니, ASAC 과정에 참여하는 6개월 동안만큼은 죽었다 생각하고 일찍 일어나기로 했어요. 그래서 저는 매일 아침 5시 40분에 기상합니다. 간단히 씻고 준비를 마친 다음 6시 15분-20분에 집을 나서 6시 28분 지하철을 탑니다. 다행히도 3주간 잘 지켜왔습니다. 앞으로도 잘 지킬 거예요.

 

집이 꽤 먼 편이라 목적지까지 환승 없이 약 45분 정도가 걸리는데요. 출퇴근 길에는 책을 읽습니다. 책 읽는 걸 너무 좋아합니다. 더 나아가 책 읽는 제 자신이 좋습니다. 책을 읽을수록 생각이 확장되고 더 나은 사람이 되는 것을 느껴서 꾸준히 읽고 기록하려고 노력하고 있어요. 데이터를 분석하고 통찰하고 관철할 수 있는 사람이 되는 데 독서도 분명 큰 도움이 될 것이라고 생각합니다. 그런데 요즘 같은 시기에 따로 시간을 내어 책을 읽기는 또 어렵다 보니, 대중교통에서 보내는 시간을 활용하는 거예요. 저도 책을 멀리하던 시절에는 지하철이나 버스에서 책 꺼내서 읽는 사람 보면 괴물 같고 그랬는데 말이죠. 막상 직접 해 보니 오히려 숏폼 영상보다도 더 즐겁고 시간도 더 잘 갑니다. 

 

강의실에 도착하고 나면 7시 20-30분 사이가 되는데 그때부터 8시 50분까지 약 한 시간 조금 넘는 시간 동안 개인 공부를 합니다. 잠깐 쉬고 9시 10분부터 수업을 듣습니다. 6시에 수업이 끝나면 저녁을 먹고 8시까지 개인 공부를 합니다. 가끔 컨디션이 안 좋으면 7시 30분쯤 일찍 가고, 컨디션이 유독 좋으면 9시까지 공부를 합니다. 이렇게 말하고 보니 정말 공부에 미친 광인 같네요. 그래도 저도 사람이기 때문에 주말엔 늦잠도 좀 자고 맛있는 것도 먹고 맥주 한 잔 하기도 하면서 편히 지내고 있어요. 요즘 주말 공부 시간은 하루에 3-6시간 사이 정도인데 조금씩 늘려가려고 합니다.

 

 

 

수업에서 3주 차 초반까지는 파이썬 기본부터 심화 문법까지 리뷰를 하고 주요 알고리즘을 몇 가지 공부했습니다. 특히 저는 알고리즘 자체를 따로 배워 본 적이 따로 없었기 때문에 구현과 탐색 알고리즘을 배웠던 시간이 큰 도움이 되었어요. 강사님께서 여러 가지 실전 코딩테스트 문제들을 가지고 하나하나 설명을 해주시면서 알고리즘을 코테 문제에 어떻게 적용하는지 보여 주신 것이 참 좋았습니다. 덕분에 저도 자극을 받아서 블로그에 코딩 테스트 문제 풀이 방법을 작성해서 올리기 시작했는데요. 무언가 배웠을 때 그걸 남에게 설명할 수 있어야 진짜로 아는 것이라고 학생들한테 누누이 말하곤 했던 걸 이제는 스스로에게 되새고 있는 저를 보면서, 기분이 묘했습니다.

 

파이썬 과정이 끝나고 간단한 테스트가 있었습니다. 다행히 쉽게 느껴지는 문제들로 구성되어 있어 10분-15분 만에 빠르게 제출을 할 수 있었습니다. 다른 동기들에 비해 문제를 빠르게 해결한 편이었는데, 제가 특별히 잘나고 똑똑해서는 아닙니다. 블로그에 직접 정리하면서 반복하고 복습했던 스니펫들을 활용할 수 있는 문제가 정말 많이 나왔어요. 운이 좋았던 거죠. 공부한 것을 남에게 설명하기를 저는 블로그에 글을 쓰는 걸로 대신하고 있는데, 이게 정말 큰 의미가 있다는 걸 실감할 수 있었던 좋은 기회였습니다. 

 

파이썬 과정이 끝나고는 짧고 콤팩트하게 MySQL을 배웠는데요. 저는 MySQL도 유데미 강의를 통해 한 번 훑고 왔기 때문에 강의 속도를 따라가는 데 큰 어려움이 없었습니다. 프로그래머스에서 MySQL 코딩테스트 문제도 제공하고 있거든요. 그걸 같이 풀면서 수업을 들으니 체화하는 데 훨씬 더 좋더라고요. MySQL이 무슨 대단한 알고리즘을 요구하는 언어는 아니기 때문에 엄청 어렵고 복잡하진 않더라도 이게 손 놓고 있으면 잊어버리기 십상이거든요. 꾸준히 놓지 않는 게 가장 중요하다고 생각되어서, 앞으로 프로그래머스 문제를 조금씩 야금야금 꾸준히 풀면서 리듬감을 유지해보려고 합니다.

 

 

 

만약 저의 회고를 참고하시고 다음 기수의 ASAC과정을 고려하고 계신 분이 계시다면, 반드시 파이썬과 MySQL의 기초 문법에 대한 (최소) 1회독을 끝낸 다음 지원을 하시라고 말씀드리고 싶습니다. 그게 정신건강에 좋습니다. 교육과정 자체가 노베이스 비기너를 위한 과정이 아닙니다. 굉장히 타이트한 교육과정이에요. 진도가 빠를 뿐만 아니라 난이도도 있습니다. 저는 비록 비전공자이기는 하지만 전공자에 뒤쳐지고 싶지 않아서 여러 가지 공부를 마친 다음 본 과정을 시작했기 때문에 아직은 수업을 따라가는 데 어려움이 없습니다. KDT 등록하면 알아서 어떻게든 되겠지- 와 같은 해이한 마음으로 시작하시면 크게 낭패를 보실 수 있다는 점을 반드시 숙지하시면 좋겠습니다.

 

그럼 4주 차도 맑은 눈의 광인 모드를 탑재하고 열심히 공부하고 돌아오겠습니다. 회고 읽어주셔서 감사합니다. :)

 

 

 

 

특정 열의 값에 대해 순위를(랭킹을) 매기기 위해서 다음과 같은 함수를 사용해볼 수 있습니다.

  • RANK
  • DENSE_RANK
  • ROW_NUMBER
  • NTILE

기본적인 함수 형식은 아래과 같습니다.

RANK() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )
DENSE_RANK() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )
ROW_NUMBER() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )
NTILE() OVER ( [PARTITION BY colName1] ORDER BY colName2 [DESC] )

# [대괄호] 안의 값은 선택사항입니다

 

이렇게 보면 외계어같지만 실제로는 사용이 쉬운 함수들 입니다. (진짜루요)

제가 갖고 있는 데이터베이스에서 Employee의 사번, 이름, 젠더, 샐러리(연봉) 4개를 저장한 임시 테이블 TEMP를 가지고 차례차례 함수를 적용시켜 보겠습니다.

 

1. RANK()

RANK()함수는 내가 저장한 행(column)에 순위를 매겨서 정렬한 결과값을 보여주는 함수입니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	RANK() OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

위의 쿼리에는 SALARY 행 내림차순을 기준으로 랭킹함수를 적용해서 rank라는 새로운 이름의 행(column)을 반환하도록 했습니다.

 

 

결과를 살펴봅시다. 잘 보시면 rank 행에 128등이 두 명입니다. 이유는 두 명의 salary 값이 중복되기 때문인데요. 이렇게 RANK() 함수에서는 tie : 중복값이 있는 데이터끼리는 같은 등수를 쉐어하게 됩니다. 그리고 그 등수를 쉐어하는 만큼 그 다음 등수는 밀려서 사라지게 되는데요. 128등이 두 명 오고 난 다음에 129등 없이 130등으로 시작하는 것을 보면 알 수 있습니다. 만약 128등이 3명이었다면 그 다음 등수는 131등으로 시작하겠죠.

 

2. DENSE_RANK()

DENSE_RANK() 함수는 RANK()와 거의 비슷하지만 약간 다릅니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	DENSE_RANK() OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

 

DENSE_RANK()에서는 같은 값, 중복값의 존재나 갯수와 관계없이 무조건 1씩 차례로 증가합니다. 랭킹이 밀리지 않아요. 127등이 두 명 있다고 그 다음 등수가 129로 시작하지 않고, 128으로 시작하는 것을 보면 알 수 있어요.

 

예를 들어 올림픽에서 금메달을 2명이 공동 수상한다고 가정을 했을 때, 금메달이 2명이라고 은메달은 아무도 주지 않고 동메달을 주면 안 되잖아요? 금메달을 받은 사람의 수와 관계 없이 은메달도 반드시 준다, 라고 보면 됩니다. (금메달 공동 수상이 실제로 가능한지는.... 저도 모르지만요...)

 

그런데 보니까 아까 RANK()에서는 128등이 두 명이었는데 이번엔 127등이 두 명이네요, 왜 그럴까요? 데이터 이미지 중간에 생략된 부분에서 같은 값을 가지는 tie가 두 명 있었습니다. 그래서 RANK()는 숫자가 하나 밀려서 128등 두 명이 되었고, DENSE_RANK()는 숫자가 밀리지 않아서 127등 두 명이 되었어요.

 

3. ROW_NUMBER()

ROW_NUMBER() 함수는 기준에 따라 랭킹 정렬을 하되, 중복값과 관계 없이 무조건 1부터 차례대로 행 번호를 매겨 반환합니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	ROW_NUMBER() OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

보시는 것처럼 같은 값이 있던 말던 무조건 1부터 시작해서 하나씩 줄번호를 매겨서 반환합니다.

 

직관적인 예시로 출석 번호나 키번호를 생각해볼 수 있을 것 같아요. 학급에 동명이인이 있다고 둘이 같은 출석번호를 쉐어하지는 않죠. 키가 같다고 키번호를 똑같이 쓰지도 않을 거구요. 이런 상황에서 ROW_NUMBER() 함수를 사용하면 되겠습니다.

 

4. NTILE()

NTILE 함수는 랭킹을 매기되, 내가 지정한 블럭 갯수만큼 구간을 나누어 등급 랭킹을 부여합니다.

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	NTILE(5) OVER(ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

위의 코드에서 NTILE(5)와 같이 NTILE 함수 안에 정수를 넣어 줬는데요. 이 말은 1, 2, 3, 4, 5등 구간으로 나누어 5개의 등급을 매겨 랭킹을 반환하라는 뜻이에요.

 

데이터 갯수가 많아 이렇게 초반부에는 1등급만 보이지만

샐러리를 내림차순으로 정렬한 다음 5개의 등급으로 나누어 랭킹이 1부터 5까지 차례대로 부여가 되었습니다.

 

5. PARTITION BY 추가 응용

그럼 PARTITION BY는 어떻게 활용할 수 있을까요?

SELECT emp_no, 
	CONCAT(first_name, ' ', last_name) AS full_name,
	salary,
	RANK() OVER(PARTITION BY emp_no ORDER BY SALARY DESC) AS `rank`
FROM TEMP;

 

 

 

제가 가지고 있던 TEMP 테이블에는 같은 사람의 샐러리가 매년 업데이트 되며 누적되어 있어서, 이렇게 이름에 따라서 샐러리 값을 여러 열(row)이 저장 되어 있었어요. OVER() 내부의 시작 부분에 PARTITION BY emp_no 를 추가해 주면서 직원 번호에 따라 파티션을 나누고 그 파티션 내부에서 랭킹을 매긴 값을 반환받았습니다. 이를 통해 직원별로 샐러리가(연봉이) 얼마나 상승했는지를 한 눈에 알아볼 수 있게 되었습니다.

+ Recent posts