안녕하세요. 오늘은 튜터님과 함께 강의를 진행하고 풀었던 문제에 대해서 함께 풀어보고 코드를 하나하나 뜯어보는 시간을 가져보도록 하겠습니다. 제가 코드를 진행하면서 이 코드는 무슨 뜻이 있을까? 하는 것은 #으로 따로 적어두었으니 한번 같이 확인해보세요!
주어진 시나리오는
💡 당신은 레스토랑의 총괄 매니저로 승진했습니다. 이제 단순한 팁 분석을 넘어,
- 날짜가 없는 데이터에 시계열 정보를 입히고,
- 중복된 오류 데이터를 찾아 제거해야 합니다.
- 본사의 할인 정책을 적용하고 VIP 데이터를 통합한 뒤,
- 한글 보고서 작성을 위해 데이터를 변환(map, apply)하여 요약 통계를 냅니다
그럼 본격적인 데이터 분석 전에 먼저 어떤 데이터가 있는지 확인을 해보겠습니다.
df

위 데이터를 확인해보니 총가격, 팁으로 얼마를 주엇는지, 성별, 흡연 여부, 요일, 시간대 등 다양한 정보가 있는 것을 확인 할 수 있네요.
그럼 한번 문제 1번 부터 차근차근 시작해보도록 하겠습니다.
1) 데이터 준비 및 시계열 변환 (datetime)
tips 데이터에는 날짜가 없습니다. 실습을 위해 가상의 날짜(2024년 1월 1일부터)를 만들어 부여해 봅시다.
import seaborn as sns
import pandas as pd
import numpy as np
# 1. 데이터 불러오기
df = sns.load_dataset('tips')
#seaborn라이브러리에서 미국 레스토랑의 식사비와 팁 내역이 남긴 샘플 데이터 표를 df 가져와라.
df.head()

# 2. 가상의 날짜 데이터 생성 (datetime 변환 실습)
# 2024-01-01부터 데이터 개수만큼 날짜 생성
# Hint: pd.date_range()
dates = pd.date_range(start='2024-01-01', periods=len(df))
#periods = 몇일치의 데이터를 만들어달라라는 뜻 periods = 10이라고 적으면 10일치 날짜만 형성됨
#len(df) = 이것을 적어 df에 있는 244라는 값으로 변함
#따라서 periods = 244로 해석되어, 2024-01-01부터 244개의 날짜가 추가된거임
df['order_date'] = dates
#data를 사용한 order_data라는 새로운 컬럼을 형성시키겟다.
df.head()

2) 중복 데이터 제거하기 (duplicated())
시스템 오류로 인해 데이터 중간에 중복된 행이 들어갔다고 가정하고, 이를 해결해 봅니다.
# [상황 설정] 강제로 중복 데이터 만들기 (실습용)
# 첫 5줄을 복사해서 데이터 끝에 붙여넣음
df_dirty = pd.concat([df, df.iloc[:5]])
# df.iloc[:5] [:5]라고 적었기 때문에, 전체 데이터프레임 df에서 맨 위에서부터
딱 5줄(0번 행부터 4번 행까지)만 가위로 싹둑 잘라서 가져오라는 뜻
# pd.concat([df, ...]) 원래 있던 244줄짜리 표 밑에다가, 방금 복사한 맨 위 5줄을 똑같이 한 번 더 이어 붙여라!
print(f"중복 제거 전: {len(df_dirty)}행")
# Output: 중복 제거 전: 249행
# 1. 중복 데이터가 있는지 확인
print("중복된 행 개수:", df_dirty.duplicated().sum())
#duplicated는 중복 검사기
# Output: 중복된 행 개수: 5
# 1. 중복 데이터가 있는지 확인
print("중복된 행 개수:", df_dirty.duplicated())
#duplicated는 중복 검사기
# 처음 보는 데이터는 False로 중복된 데이터는 True로 변환됨
# Output: 중복된 행 개수: 5
# 2. 중복 제거 (첫 번째 값만 남김)
df_clean = df_dirty.drop_duplicates(keep='first')
# df_dirty 내에 같은 데이터가 존재할 시 keep='first'를 사용하여 맨 처음 나온 데이터만 남기고 뒤에 나오는 데이터는 싹다 제거해라.
print(f"중복 제거 후: {len(df_clean)}행")
# Output: 중복 제거 후: 244행
3) 날짜 정보 추출 및 정렬 (dt accessor, sort_values)
매출이 발생한 '월'과 '요일' 정보를 추출하고, 날짜순으로 데이터를 정렬합니다.
# 1. 요일, 월 정보를 추출해서 새로운 컬럼 만들기
# 'order_date'에서 월(month) 정보 추출 (1, 2, 3... 형태)
df['month'] = df['order_date'].dt.month
# 'order_date'에서 요일(weekday) 정보 추출 (0:월, 1:화 ... 6:일 형태)
df['weekday'] = df['order_date'].dt.weekday
df.head()

근데 이렇게 적으니 month와 weekday에 글자가 아닌 숫자로 적어져있네요? 제가 원하는 건 month에 January가 나오고 weekday에는 Monday와 같은 형태로 나와야하는데 그렇다면 무엇을 건드려야 할까요?
# 1. 요일, 월 정보를 문자로 추출해서 새로운 컬럼 만들기
# %b는 'Jan', 'Feb' 같은 월의 약칭을 뜻합니다.
df['month'] = df['order_date'].dt.strftime('%B')
# strftime은 String Format Time의 약자로, 날짜를 내 입맛에 맞는 문자열 형태로 변환해 주는 함수입니다
# .day_name()을 쓰면 'Monday', 'Tuesday' 같은 전체 요일 이름이 나옵니다.
df['weekday'] = df['order_date'].dt.day_name()
df.head()

# 2. 최신 날짜 순으로 정렬해보기 (내림차순)
df_sorted = df.sort_values('order_date', ascending=False)
df_sorted.head()

4) 데이터 변환하기 (map,apply )
보고서를 읽기 편하게 영문을 한글로 바꾸고, 팁 비율에 따른 고객 등급을 매깁니다.
# 1. [Map] 성별을 한글로 변환
gender_map = {'Female': '여성', 'Male': '남성'}
df_sorted['성별'] = df_sorted['sex'].map(gender_map)
df_sorted.head()
1. gender_map = { ... }
- 파이썬의 딕셔너리(Dictionary) 구조를 활용해 데이터 변환 규칙을 만듭니다.
- 왼쪽(Key)에는 원래 데이터인 영어 단어를, 오른쪽(Value)에는 바꾸고 싶은 한글 단어를 적어 컴퓨터에게 기준을 제시합니다.
2. .map(gender_map)
- map() 함수는 판다스에서 "사전 뒤져서 알맹이 바꿔치기해라"라고 시키는 명령어입니다.
- df_sorted['sex'] 컬럼에 있는 데이터들을 위에서부터 한 줄씩 읽으면서, 방금 만든 gender_map 사전에 대조해 봅니다.
- Female을 발견하면? 사전을 찾아보고 여성으로 변환!
- Male을 발견하면? 사전을 찾아보고 남성으로 변환!
# 2. [Apply] 팁 비율(Tip Rate)에 따라 고객 등급 분류
# 팁이 총액의 20% 이상이면 'VIP', 아니면 'Normal'
def classify_customer(row):
# 팁 비율 = 팁 / 총액
tip_rate = row['tip'] / row['total_bill']
# 팁 비율이 20%(0.2) 이상인지 확인
if tip_rate >= 0.2:
return 'VIP'
else:
return 'Normal'
# axis=1을 써서 행 단위(가로 방향)로 함수를 적용
df_sorted['고객등급'] = df_sorted.apply(classify_customer, axis=1)
df_sorted[['성별', 'total_bill', 'tip', '고객등급']].head()
위 코드를 보시면 axis=1이라는 문구가 보이실 거에요.
df_sorted['고객등급'] = df_sorted.apply(classify_customer, axis=1) <- 이 코드가 무엇을 의미하는지 한번 살펴보겠습니다.
- axis=1 (가로 방향 셔틀 가동):
- 판다스가 표의 첫 번째 줄(0번 행)로 이동합니다. axis=1 명령을 받았기 때문에, 0번 손님의 sex, total_bill, tip 정보가 담긴 가로 한 줄을 통째로 움켜륍니다.
- classify_customer(row) (심사위원에게 전달):
- 움켜쥔 가로 한 줄(row)을 우리가 정의한 classify_customer 함수에 매개변수로 던집니다.
- 함수 내부에서 $팁 / 총액$ 비율을 계산(예: $3.00 / 16.99 \approx 0.176$)하고, 20% 미만이므로 Normal이라는 글자를 뱉어냅니다(return).
- df_sorted['고객등급'] = ... (새로운 기둥에 박제):
- 방금 뱉어낸 Normal이라는 글자를 0번 행의 '고객등급'이라는 새로운 칸에 착 붙여줍니다.
- 이 짓을 마지막 행까지 지치지 않고 반복(apply)합니다.

5) 데이터 합치기 1: 정보 병합 (merge)
본사에서 "요일별 할인율 정책" 테이블을 보내왔습니다. 이 정보를 기존 데이터와 합쳐서, 할인된 최종 결제 금액을 계산해야 합니다.
# 1. 요일별 할인율 테이블 생성 (미니 데이터프레임)
discount_policy = pd.DataFrame({
'day': ['Thur', 'Fri', 'Sat', 'Sun'],
'discount_rate': [0.05, 0.0, 0.1, 0.15] # 목:5%, 금:0%, 토:10%, 일:15%
})
discount_policy
# 2. merge로 병합 (기준: day)
# 왼쪽에 팁 데이터(df_sorted), 오른쪽에 할인율 데이터(df_discount)를 두고 'day' 기준으로 병합
df_merged = pd.merge(df_sorted, discount_policy, on='day', how='left')
df_merged.tail()
df_merged = pd.merge(df_sorted, discount_policy, on='day', how='left') <- 해당 코드를 보시면 바로 이해를 하셧나요..?
저는 바로 이해를 못해서 시간을 많이 소모했는데요 ㅋㅋ...
제가 이해한 바로 한번 설명을 해보겠습니다.
df_sorted 를 위에서부터 한 줄씩 읽는데, 요일에 적혀있는 데이터를 읽고 이를 discount_policy에서 얼마나 할인이 되는 지 알아내고 할인률을 새로운 컬럼을 형성시켜 pd.merge 이어 붙여라 이런 뜻의 코드입니다.
[왼쪽: 내 영수증 표] [오른쪽: 할인 책자]
요일 식대 팁 요일 할인율
0 Sat $16.99 $1.01 Thur 10%
1 Sun $10.34 $1.66 ◀──(요일 대조!)──▶ Fri 10%
2 Sun $21.01 $3.50 Sat 15%
Sun 15%

6) 데이터 합치기 2: 데이터 추가 (concat)
갑자기 직원이 "매니저님! VIP룸 매출 엑셀 파일이 따로 있었어요!"라며 데이터를 가져왔습니다. 기존 데이터 아래에 이어 붙여야 합니다.
# 1. VIP룸 추가 데이터 생성 (구조가 같아야 함)
vip_data = pd.DataFrame({
'total_bill': [150.0, 200.0],
'tip': [20.0, 30.0],
'sex': ['Male', 'Female'],
'smoker': ['No', 'No'],
'day': ['Sat', 'Sun'],
'time': ['Dinner', 'Dinner'],
'size': [5, 6],
'order_date': pd.to_datetime(['2024-02-01', '2024-02-02'])
})
vip_data
# 2. concat으로 위아래로 합치기
# ignore_index=True: 인덱스를 0부터 다시 매김
df_final = pd.concat([df_merged, vip_data], ignore_index=True)
print(f"합치기 전: {len(df_merged)}행, 합친 후: {len(df_final)}행")
# Output: 합치기 전: 244행, 합친 후: 246행
7) 그룹 집계 (groupby)
최종적으로 요일별로 "평균 매출"과 "평균 할인액"을 집계하여 보고서를 작성합니다.
# 요일별(day)로 묶어서 total_bill(평균, 최대값)과 tip(평균, 최대값) 확인
report = df_final.groupby('day').agg({
'total_bill': ['mean', 'max'],
'tip': ['mean', 'max']
}).round(2)
#round(2) 라고 적으면 소수점 2자리수까지 정리해줌
report
이상으로 오늘 세션에서 진행하며 배웟던 내용들을 사용하여 실습을 진행해보았는데요. 세션 외에도 이렇게 정리하면 편하지 않을까? 하는 내용들을 따로 정리하고 추가해나가면서 진행해 보았습니다.