Sparta/Theory

[250829] 스파르타코딩 본캠프 19일차 (1) - pandas 02

junecho 2025. 8. 29. 12:43

🟡 pandas 2차 강의 🟡

 그룹화                                                                  

🔰 .groupby() - 그룹화(Grouping)                                                                    

  • DataFrame.groupby()
  • 데이터 분석에서 매우 중요한 개념으로, 어떤 키를 기준으로 데이터를 묶은 뒤 각 그룹에 대한 통계치를 계산하는 작업
  • SQL의 GROUP BY 절과 유사한 기능
  • 그룹화를 한 후 합계(sum), 평균(mean), 개수(count) 등 다양한 집계(aggregation) 함수 적용 가능

 

단일 열로 그룹화

  • ex) 팁(tips) 데이터에서 **요일(day)**별로 팁 평균 구하기. 그룹 키는 'day', 집계 함수는 평균(mean)을 사용
import seaborn as sns

tips = sns.load_dataset('tips')
mean_tips_by_day = tips.groupby('day')['tip'].mean()
print(mean_tips_by_day)

*>>> 출력
day
Thur    2.771452
Fri     2.734737
Sat     2.993103
Sun     3.255132
Name: tip, dtype: float64*
  • 결과 형태 : groupby를 하고 나서 한 컬럼에 대해 집계(mean)까지 하면 Series 객체로 결과가 나옵니다. 만약 여러 컬럼에 대해 집계했다면 DataFrame으로 반환됩니다.
  • 여러 통계 한번에 :
    • 만약 각 그룹에 대해 여러 가지 통계를 한 번에 보고 싶다면 **agg()**메서드 사용
    • 예시) 요일별 팁의 평균과 합계를 모두 알고 싶다
tips.groupby('day')['tip'].agg(['mean', 'sum'])

*>>> 출력
          mean     sum
day                   
Thur  2.771452  171.83
Fri   2.734737   51.96
Sat   2.993103  260.40
Sun   3.255132  247.39*

 

다중 열로 그룹화

  • 그룹 키를 2개 이상 지정하면 **계층적 그룹(hierarchical grouping)**이 이루어짐
  • 예시) **요일별(day) 그리고 흡연여부별(smoker)**로 그룹을 나눈 뒤 각 그룹의 행 개수(식사 건수) 구하기
grouped = tips.groupby(['day', 'smoker'])['tip'].count()
print(grouped)                               # MultiIndex를 갖는 Series

*****>>> 출력
day   smoker
Thur  Yes       17
      No        45
Fri   Yes       15
      No         4
Sat   Yes       42
      No        45
Sun   Yes       19
      No        57*
print(grouped.reset_index(name='count'))

*>>> 출력
    day smoker  count
0  Thur    Yes     17
1  Thur     No     45
2   Fri    Yes     15
3   Fri     No      4
4   Sat    Yes     42
5   Sat     No     45
6   Sun    Yes     19
7   Sun     No     57*
print(grouped.unstack()) 

*>>> 출력
smoker  Yes  No
day            
Thur     17  45
Fri      15   4
Sat      42  45
Sun      19  57*
  • MultiIndex 결과 :
    • 두 개의 그룹키를 사용하면 인덱스가 두 단계(MultiIndex)로 나타남
    • 위 예시의 **grouped**를 그대로 출력한 결과smoker가 Yes No 인덱스 레벨로 나타남
    • .unstack() : 2차원 형태로 펼침 (피벗과 유사)
    • .reset_index() : 인덱스를 일반 컬럼으로 돌림

 

.agg() - 여러 집계 함수 사용

  • agg()에 딕셔너리를 사용하면 컬럼마다 다른 집계 함수를 적용할 수 있음
  • .agg(['mean','max','min'])처럼 한 컬럼에 여러 함수 적용 가능
  • 예시) 요일별로 total_bill과 tip의 합계와 평균을 동시에 계산하려면:
tips.groupby('day').agg({
    'total_bill': 'sum',
    'tip': 'mean'
})

*>>> 출력
 	 total_bill	     tip
day		
Thur	1096.33	2.771452
Fri	325.88	2.734737
Sat	1778.40	2.993103
Sun	1627.16	3.255132*

 

 

 데이터 처리                                                            

🔰 문자열 처리                                                                                                 

Series.str 접근자를 통해 이용

주요 문자열 메서드

  • Series.str.lower(), upper(), title(): 문자열의 대소문자 변환 (모두 소문자, 모두 대문자, 단어마다 첫 글자만 대문자)
  • Series.str.strip(): 좌우 공백 제거 (특정 문자 제거는 .strip('x'))
  • Series.str.contains('문자열'): 각 문자열이 특정 패턴을 포함하는지 True/False
  • Series.str.replace('패턴', '대체'): 문자열 치환 (정규식 패턴 사용 가능, 정규식 아닌 단순 치환은 regex=False 지정)
  • Series.str.split('구분자'): 구분자로 분할 -> 리스트 반환 (이후 .str[0]으로 첫 부분 추출 등 가능)
  • Series.str.cat(sep=','): 문자열 연결 (리스트처럼 이어붙이기)
  • Series.str.len(): 문자열 길이 반환 (숫자에는 NaN)

이 외에도 부분 문자열 추출 (slice), 정규표현식 extract, startswith, endswith, find 등등

  • 예시)
import pandas as pd

products = pd.Series([
    "Bush Somerset Collection Bookcase",
    "Hon Deluxe Fabric Upholstered Stacking Chairs, Rounded Back",
    "Self-Adhesive Address Labels",
    "Staples 8.5x11 Copy Paper"
])

print(products)

*>>> 출력
0                    Bush Somerset Collection Bookcase
1    Hon Deluxe Fabric Upholstered Stacking Chairs,...
2                         Self-Adhesive Address Labels
3                            Staples 8.5x11 Copy Paper*

str.contains()

  • 부분 문자열 검색
  • 반환값 True/False
  • ex) 상품명에 "Bookcase"라는 단어가 들어가는지를 찾고 싶다면
contains_bookcase = products.str.contains("Bookcase")
print("Contains 'Bookcase':", contains_bookcase.tolist())

>>>
Contains 'Bookcase': [True, False, False, False]

str.replace()

  • 문자열 치환
  • ex) 상품명에서 공백을 밑줄(_)로 바꾸고 싶다면:
underscored = products.str.replace(" ", "_", regex=False)
print(underscored)

*>>> 출력
0                    Bush_Somerset_Collection_Bookcase
1    Hon_Deluxe_Fabric_Upholstered_Stacking_Chairs,...
2                         Self-Adhesive_Address_Labels
3                            Staples_8.5x11_Copy_Paper*

regex=False를 준 것은 공백문자 ' '를 정규식이 아닌 리터럴로 처리하기 위해서

정규식 패턴 사용할 수도 있음 (예: str.replace(r"\s+", "_")는 연속 공백도 하나의 밑줄로 치환 등).

str.upper() / str.title()

  • products.str.upper() : 모든 글자를 대문자
  • products.str.lower() : 모든 글자를 소문자
  • title() : 각 단어가 Capitalize
print(products.str.title())

>> 출력
*0                    Bush Somerset Collection Bookcase
1    Hon Deluxe Fabric Upholstered Stacking Chairs,...
2                         Self-Adhesive Address Labels
3                            Staples 8.5X11 Copy Paper*

str.split()

  • 분할과 추출
  • 문자열을 원하는 구분자로 쪼개어 list를 반환
  • 만약 특정 패턴 앞부분만 추출하고 싶다면 str.split 후 리스트의 0번 요소를 가져오면 됨
brands = products.str.split().str[0]
print(brands)

>>>
0             Bush
1              Hon
2    Self-Adhesive
3          Staples
  • 정규 표현식 활용(선택)-무시 가능!
    • 판다스 문자열 메서드는 contains, replace, extract 등에서 정규식 사용 ⭕
    • ex) 제품명에 숫자가 들어있는지 확인하려면 정규식 \\d (숫자)로 contains
    • 결과가 [False, False, False, True] 로 출력되어 마지막 제품명 "Staples 8.5x11 Copy Paper"에만 숫자가 있음을 알 수 있음
has_digit = products.str.contains(r"\\d")
print(has_digit.tolist())

*>>> [False, False, False, True]*

 

🔰 시간 데이터 처리                                                                                               

NumPy의 datetime64 타입을 기반으로, **Timestamp(타임스탬프)**와 DatetimeIndex(시간 인덱스) 등을 사용해 시계열 데이터를 효율적으로 처리

  • 날짜 형식 문자열을 datetime 객체로 변환
  • DatetimeIndex를 인덱스로 사용하는 시계열
  • 날짜/시간 속성 (년도, 월, 요일 등) 추출
  •  

날짜 형식 변환 (pd.to_datetime)

  • 일반적으로 CSV나 Excel에서 읽은 날짜는 문자열로 들어오는 경우 다수.
  • 이를 판다스 datetime 객체로 변환해야 연산이나 비교가 제대로 가능.
  • pd.to_datetime() 함수를 사용하면 문자열을 날짜/시간으로 파싱
  • to_datetime은 형식을 자동 인식하려 하지만, 실패할 경우 format 인자로 형식을 지정해 줄 수도 있음 (예: format='%d/%m/%Y').
dates = pd.to_datetime(orders['Order Date'], format='%d/%m/%Y')
print(dates.head())

*>>>
0   2017-11-08
1   2017-11-08
2   2017-06-12
3   2016-10-11
4   2016-10-11
Name: Order Date, dtype: datetime64[ns]*

DatetimeIndex와 시계열

orderstime = orders.set_index('Order Date')

인덱스가 시간으로 바뀌고, 정렬도 시간 순으로 자동 정렬되며, 기간 선택도 용이해짐 (orders['2016']처럼 연도 문자열로 슬라이싱 가능 등).

 

날짜/시간 속성 접근 (dt 접근자)

Series가 datetime64 타입인 경우 Series.dt를 통해 각 날짜의 구성 요소를 뽑을 수 있음

  • Series.dt.year, month, day, hour, minute 등: 연, 월, 일, 시, 분...
  • Series.dt.day_name(): 요일 이름 (예: Monday)
  • Series.dt.weekday: 요일 번호 (월=0,...일=6)
  • Series.dt.quarter: 분기(1~4)
  • Series.dt.days_in_month: 그 달의 일 수 등.
  • ex) Superstore 주문 데이터에 Order_Date 컬럼에서 연도와 요일을 새 컬럼으로 추출
orders = pd.read_csv("superstore_orders.csv")
dates = pd.to_datetime(orders['Order Date'], format='%d/%m/%Y')

orders['Year'] = dates.dt.year
orders['Weekday'] = dates.dt.day_name()
print(orders[['Order Date','Year','Weekday']].head(3))

*>>>
   Order Date  Year    Weekday
0  08/11/2017  2017  Wednesday
1  08/11/2017  2017  Wednesday
2  12/06/2017  2017     Monday*

 

 

 데이터 결합                                                            

🔰 .merge()                                                                                                       

  • pd.merge(df1, df2, how='inner', on='키')
  • 두 데이터프레임의 공통 컬럼을 키로 삼아 조인
  • how 인자는 SQL의 JOIN과 마찬가지로 'inner', 'left', 'right', 'outer' 등을 지정 가능
  • 만약 키 컬럼 이름이 다르면 left_on과 right_on을 각각 지정해줄 수 있음. 혹은 인덱스를 키로 쓰고 싶으면 left_index=True, right_index=True 옵션을 사용
  • ex)
    • 슈퍼스토어 데이터에는 Orders 테이블과 Returns 테이블이 있다고 가정하겠습니다.
    • Orders에는 모든 주문의 내역이 있고, Returns에는 반품된 주문의 ID 목록이 있습니다.
    • 반품 여부를 주문 데이터에 합치고 싶을 때, 두 테이블을 Order ID로 merge 하면 됩니다.
orders = pd.DataFrame({
    'Order ID': [101, 102, 103],
    'Product': ['Bookcase', 'Chair', 'Lamp'],
    'Profit': [100, 50, 20]
})
returns = pd.DataFrame({
    'Order ID': [102, 105],
    'Returned': ['Yes', 'Yes']
})
merged = orders.merge(returns, on='Order ID', how='left')
merged['Returned'] = merged['Returned'].fillna('No')
print(merged)

*>>>
   Order ID   Product  Profit Returned
0       101  Bookcase     100       No
1       102     Chair      50      Yes
2       103      Lamp      20       No*

 

🔰 .concat()                                                                                                      

  • pd.concat([df1, df2, ...], axis=0)
  • 컬럼이 동일한 둘 이상의 데이터프레임을 위아래로 이어붙임
  • 인덱스가 겹칠 수 있으니 무시하려면 ignore_index=True 옵션
  • axis=1로 지정하면 열 방향으로 옆으로 붙입니다. 이 경우 행 인덱스 기준으로 정렬되어 붙고, 맞지 않는 인덱스엔 NaN이 들어갑니다.
  • ex) 4대 도시 매출 데이터가 각각 별도 데이터프레임으로 주어졌을 때, 이를 하나로 합칠 수 있습니다:
df1 = pd.DataFrame({'City': ['Seoul','Busan'], 'Sales':[100,150]})
df2 = pd.DataFrame({'City': ['Daegu','Incheon'], 'Sales':[80,70]})
combined = pd.concat([df1, df2], ignore_index=True)
print(combined)

>>>
      City  Sales
0    Seoul    100
1    Busan    150
2    Daegu     80
3  Incheon     70

 

 

✅ apply, map, lambda                                          

🔰 .apply                                                                                                           

  • 데이터프레임이나 시리즈의 각 원소 또는 각 행/열에 임의의 함수를 적용할 수 있는 apply 계열 메서드를 제공
  • DataFrame.apply(func, axis=0) : 각 열(column)에 대해 함수를 적용 (default). axis=1로 지정하면 각 행(row)에 대해 함수를 적용. 결과는 함수 반환에 따라 Series 또는 DataFrame이 될 수 있음.
  • Series.apply(func) : Series의 각 값에 함수를 적용한 결과 Series를 반환 (element-wise 적용)
  • ex)
    • tips 데이터에서 각 행에 대해 팁 비율 (tip_pct = tip/total_bill)을 계산해 새로운 열로 추가
    • 벡터화 연산으로 tips['tip'] / tips['total_bill'] 이렇게 하면 빠르지만, 여기서는 apply를 활용
tips['tip_pct'] = tips.apply(lambda row: row['tip'] / row['total_bill'], axis=1)
print(tips[['total_bill','tip','tip_pct']].head())

*>>>
   total_bill   tip   tip_pct
0       16.99  1.01  0.059447
1       10.34  1.66  0.160542
2       21.01  3.50  0.166587
3       23.68  3.31  0.139780
4       24.59  3.61  0.146808*

각 행을 입력으로 받아 람다 함수에서 tip/total_bill을 계산해 반환한 결과가 tip_pct 열로 저장되었습니다. 위에서 0번 손님의 팁 비율이 약 5.95%, 2번 손님은 약 16.67% 등으로 나타났습니다.

  • 이 방식은 tips['tip'] / tips['total_bill']처럼 벡터화 연산으로 바로 계산하는 것에 비해 속도가 느릴 수 있지만, 임의의 복잡한 계산을 하는 경우 apply를 쓰면 간편합니다.
  • axis=1을 빼먹으면 기본 axis=0이라 컬럼별 작동하려고 해서 KeyError가 나니 주의합니다.
  • 열 기준 apply: axis=0 일 때, 전달되는 인자는 각 열을 Series로 본 함수입니다. 예를 들어 df.apply(np.mean)은 각 컬럼의 평균으로 이루어진 Series를 돌려줍니다. 또는 df.apply(lambda col: col.max() - col.min())는 각 컬럼의 range(최댓값-최솟값)를 계산해 줄 수 있습니다

 

🔰 .map                                                                                                           

  • Series.map(func or dict)
  • Series의 각 요소를 주어진 함수에 매핑하여 새로운 Series를 반환. 단, map은 원소별 동작만 가능하고 index나 다른 컬럼 정보는 접근❌ (그래서 행 종합 처리는 apply(axis=1) 사용).
  • map에는 함수 대신 딕셔너리Series를 넣어서 값 대체(lookup) 용도로도 많이 사용함
  • ex) 'sex' 컬럼이 Male/Female로 되어 있을 때 이를 1/0으로 바꾸는 'gender_code' 컬럼 추가:
tips['gender_code'] = tips['sex'].map({'Male': 1, 'Female': 0})
print(tips[['sex','gender_code']].head(3))

*>>>
      sex gender_code
0  Female           0
1    Male           1
2    Male           1*
  • ex) total_bill 금액이 20달러 이상이면 'expensive' 아니면 'cheap'이라는 레이블 생성
tips['price_label'] = tips['total_bill'].map(lambda x: 'expensive' if x >= 20 else 'cheap')
print(tips[['total_bill','price_label']].head(5))

>>>
   total_bill price_label
0       16.99       cheap
1       10.34       cheap
2       21.01   expensive
3       23.68   expensive
4       24.59   expensive

 

apply/map 사용 시 주의 (성능)

apply, map 등은 파이썬 레벨 함수를 각 원소에 적용하기 때문에 벡터화 연산보다 느릴 수 있습니다. 가능하면 판다스의 기본 연산이나 문자열 메서드 등 벡터화 기능을 활용하고, 정말 커스텀 로직이 필요할 때만 apply를 사용하세요.

예를 들어 위 tip_pct 계산은 tips['tip']/tips['total_bill']처럼 바로 계산하는 것이 apply(lambda)보다 수십 배 빠릅니다.

Series.map은 내부적으로 C로 구현된 딕셔너리 매핑 등을 쓰므로 비교적 빠른 편이지만, 복잡한 함수면 느려질 수 있습니다.

lambda와 함께 쓰는 경우

lambda (익명 함수)는 간단한 함수식을 일회용으로 사용할 때 편리합니다. 위 예시들처럼 사용했고, 물론 미리 정의한 함수명을 넣어도 됩니다. Python의 lambda는 lambda x: <표현식> 형태로 쓰며, 표현식 결과가 반환됩니다.

정리: apply, map은 판다스에 익숙해질수록 요긴하게 쓰이는 도구입니다. 특히 데이터 가공이나 피처 엔지니어링 단계에서 다양한 변형을 할 때 활용됩니다. 다만, 사용하는 함수의 구현에 따라 성능에 영향을 주니 큰 데이터셋에서는 주의해야 합니다.

 

 


🔰 과제                                                                                                  

실습

 


 

아티클

더보기

💡 오늘의 아티클 (주제)


주제 : 넷플릭스와 아마존은 데이터 분석을 어떻게 할까요?

  • 요약
    • 사용자 행동 데이터 분석의 중요성과 실제 기업 사례를 통한 비즈니스 개선 방안
  • 주요 포인트
    • 넷플릭스 : 콘텐츠 제작에 관한 여러 사용자 행동 데이터를 분석 후 도출된 인사이트를 바탕으로 특정 시리즈 제작에 투자하여 신규 가입자 발생 및 기존 유저 이탈률 감소
    • 아마존 : 사용자 행동 데이터 분석 후 웹사이트 로딩과 판매량의 상관관계를 도출해 이를 바탕으로 로딩 속도 개선을 통해 평균 구매 전환율을 13% 달성
    • 맞춤 광고 : 사용자의 검색/클릭 이력을 기반으로 맞춤형 광고를 제공함으로써 광고 효율성 극대화 및 비용 절감
  • 핵심 개념
    • 사용자 행동 데이터 (User Behavior Data) : 사용자가 서비스 내에서 행동한 대부분의 활동을 추적하는 데이터
    • 구매 전환율 : 특정 기간 동안 웹사이트를 방문한 전체 사용자 중 실제 구매를 완료한 사용자의 비율
  • 개인 인사이트
    아마존의 웹사이트 로딩 0.1초 지연이라는 매우 사소해 보이는 미시적인 데이터가 판매 1% 감소라는 어마무시한 작용으로 이어진다는 것은 역시 나비 효과 이론이 괜히 나온게 아니다.고로 사용자 활동 데이터는 세밀하고도 다양한 관점으로 해석할 수 있어야 한다고 생각한다.

아무것도 아닐 것 같은 작은 개선, 즉 적은 인풋으로 큰 성과 - 극대화된 아웃풋- 를 이루어내는 것이 기업들이 가장 원하고도 바라는 이상적인 투자일 것이다.