본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2025. 12. 16. · 49 Views
Pandas 문자열 데이터 처리 완벽 가이드
데이터 분석에서 필수적인 Pandas의 문자열 처리 기능을 체계적으로 학습합니다. str accessor부터 정규표현식까지, 실무에서 자주 사용하는 문자열 변환, 검색, 추출 기법을 실전 예제와 함께 익힙니다.
목차
1. str accessor 소개
어느 날 김개발 씨는 고객 데이터를 정리하는 작업을 맡았습니다. 이름, 이메일, 주소가 뒤죽박죽 섞인 수천 개의 행을 보며 막막함을 느꼈습니다.
"이걸 어떻게 하나씩 처리하지?"
str accessor는 Pandas의 문자열 전용 도구상자입니다. 마치 스위스 아미 나이프처럼 다양한 문자열 처리 기능이 하나로 모여 있습니다.
DataFrame이나 Series의 문자열 데이터에 .str을 붙이면 수십 가지 메서드를 사용할 수 있습니다. 이것을 제대로 이해하면 복잡한 문자열 처리를 한 줄로 해결할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
# 고객 데이터 생성
customers = pd.DataFrame({
'name': ['Kim MinJu', 'LEE SOOJIN', 'park jihoon'],
'email': ['minju@email.com', 'SOOJIN@EMAIL.COM', 'jihoon@email.com']
})
# str accessor를 사용한 문자열 처리
print(customers['name'].str.lower()) # 소문자 변환
print(customers['email'].str.upper()) # 대문자 변환
# 메서드 체이닝도 가능합니다
print(customers['name'].str.lower().str.title()) # 각 단어의 첫 글자만 대문자
김개발 씨는 입사 2개월 차 데이터 분석가입니다. 오늘 팀장님으로부터 고객 데이터를 정리하라는 업무를 받았습니다.
엑셀 파일을 열어보니 이름은 대소문자가 뒤죽박죽, 이메일은 형식이 제각각입니다. "이걸 하나씩 수정하려면 며칠이 걸리겠는데..." 한숨을 쉬던 김개발 씨에게 선배 박시니어 씨가 다가왔습니다.
"그럴 필요 없어요. Pandas의 str accessor를 사용하면 한 방에 해결됩니다." 그렇다면 str accessor란 정확히 무엇일까요?
쉽게 비유하자면, str accessor는 마치 전문 청소 도구 세트와 같습니다. 일반 빗자루로는 구석구석 청소하기 힘들지만, 전용 도구가 있으면 쉽게 해결됩니다.
마찬가지로 Pandas의 문자열 데이터도 일반 Python 문자열 메서드로는 처리하기 번거롭지만, str accessor를 사용하면 간편하게 처리할 수 있습니다. str accessor가 없던 시절에는 어땠을까요?
개발자들은 반복문을 돌려서 각 행의 문자열을 하나씩 처리해야 했습니다. 수천 개의 데이터가 있다면 코드도 길어지고 실행 속도도 느렸습니다.
더 큰 문제는 코드 가독성이 떨어져서 나중에 유지보수하기 어렵다는 것이었습니다. 프로젝트가 커질수록 이런 문제는 눈덩이처럼 불어났습니다.
바로 이런 문제를 해결하기 위해 str accessor가 등장했습니다. str accessor를 사용하면 벡터화 연산이 가능해집니다.
즉, 반복문 없이 전체 컬럼에 한 번에 연산을 적용할 수 있습니다. 또한 메서드 체이닝도 얻을 수 있어서 여러 작업을 연결해서 처리할 수 있습니다.
무엇보다 코드가 짧고 명확해져서 다른 개발자가 보기에도 이해하기 쉽다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 customers['name'].str.lower()를 보면 name 컬럼의 모든 문자열을 소문자로 변환한다는 것을 알 수 있습니다. 이 부분이 핵심입니다.
**.str**을 붙이는 순간 문자열 전용 메서드들을 사용할 수 있게 됩니다. 다음으로 str.upper()를 사용하면 모든 문자가 대문자로 바뀝니다.
마지막으로 str.lower().str.title()처럼 메서드를 연결하면 여러 변환을 순차적으로 적용할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 전자상거래 회사에서 고객 리뷰 데이터를 분석한다고 가정해봅시다. 수만 개의 리뷰에서 특정 키워드를 검색하거나, 이메일 형식을 통일하거나, 불필요한 공백을 제거하는 작업에서 str accessor를 활용하면 빠르고 정확하게 처리할 수 있습니다.
네이버, 쿠팡 같은 많은 기업에서 이런 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 숫자형 데이터에 str accessor를 바로 사용하는 것입니다. 이렇게 하면 TypeError가 발생합니다.
따라서 먼저 astype(str)로 문자열로 변환한 후 str accessor를 사용해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 Pandas가 데이터 처리에 강력하다는 거군요!" str accessor를 제대로 이해하면 더 깔끔하고 유지보수하기 쉬운 데이터 처리 코드를 작성할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - str accessor는 문자열 타입에만 사용 가능하니 필요시 astype(str)로 변환하세요
- 메서드 체이닝으로 여러 변환을 한 줄로 처리할 수 있습니다
- NaN 값이 있어도 에러 없이 안전하게 처리됩니다
2. 대소문자 변환
김개발 씨는 해외 고객 데이터를 받았는데, 이름이 전부 대문자로만 작성되어 있었습니다. "JOHN SMITH", "SARAH CONNOR" 같은 식으로요.
보고서용으로는 보기 좋게 만들어야 하는데 어떻게 해야 할까요?
**str.lower()**와 **str.upper()**는 문자열의 대소문자를 일괄 변환하는 메서드입니다. lower()는 모든 문자를 소문자로, upper()는 대문자로 바꿉니다.
**str.title()**은 각 단어의 첫 글자만 대문자로 변환하여 이름이나 제목 형식을 만듭니다. 데이터 정규화와 통일성 확보에 필수적인 기능입니다.
다음 코드를 살펴봅시다.
import pandas as pd
# 고객 이름 데이터
names = pd.Series(['JOHN SMITH', 'sarah connor', 'Mary JANE', 'peter PARKER'])
# 다양한 대소문자 변환
print("소문자 변환:", names.str.lower())
# 결과: ['john smith', 'sarah connor', 'mary jane', 'peter parker']
print("대문자 변환:", names.str.upper())
# 결과: ['JOHN SMITH', 'SARAH CONNOR', 'MARY JANE', 'PETER PARKER']
print("제목 형식:", names.str.title())
# 결과: ['John Smith', 'Sarah Connor', 'Mary Jane', 'Peter Parker']
# 첫 글자만 대문자
print("첫 글자 대문자:", names.str.capitalize())
김개발 씨는 오늘 아침 팀장님으로부터 급한 요청을 받았습니다. "김 대리, 미국 지사에서 보낸 고객 데이터 있죠?
이름이 전부 대문자라 보기 불편하네요. 보고서용으로 깔끔하게 정리해주세요." 파일을 열어보니 정말 모든 이름이 "JOHN SMITH", "ROBERT JOHNSON" 같은 대문자 형식이었습니다.
수백 명의 이름을 어떻게 하나씩 고칠까 고민하던 김개발 씨는 어제 배운 str accessor가 떠올랐습니다. 그렇다면 대소문자 변환은 왜 필요할까요?
실무에서 데이터는 다양한 출처에서 들어옵니다. 어떤 시스템은 전부 대문자로 저장하고, 어떤 곳은 소문자를 사용합니다.
사용자가 직접 입력한 데이터는 더 들쭉날쭉합니다. 이런 데이터를 통일된 형식으로 만들지 않으면 나중에 검색이나 비교가 제대로 되지 않습니다.
예를 들어 "john smith"와 "JOHN SMITH"는 사람 눈에는 같은 이름이지만, 컴퓨터는 다른 문자열로 인식합니다. 이런 문제를 해결하는 것이 바로 대소문자 변환의 핵심입니다.
**str.lower()**와 **str.upper()**의 차이는 무엇일까요? 쉽게 비유하자면, lower()는 모든 글자를 키보드의 Shift를 누르지 않은 상태로 만드는 것입니다.
반대로 upper()는 Caps Lock을 켠 것처럼 모든 글자를 대문자로 만듭니다. 이 두 메서드는 데이터 정규화의 기본입니다.
실무에서 가장 많이 사용하는 것은 **str.title()**입니다. title()은 각 단어의 첫 글자만 대문자로 만들고 나머지는 소문자로 바꿉니다.
마치 책 제목이나 신문 헤드라인처럼 말이죠. "JOHN SMITH"가 "John Smith"로 변하면 훨씬 읽기 편해집니다.
이것이 보고서나 프레젠테이션에서 자주 사용되는 이유입니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 names.str.lower()는 모든 이름을 소문자로 변환합니다. 'JOHN SMITH'가 'john smith'가 됩니다.
다음으로 str.upper()는 반대로 모든 글자를 대문자로 만듭니다. str.title()은 각 단어의 첫 글자만 대문자로 바꿔서 'John Smith' 같은 자연스러운 형식을 만듭니다.
**str.capitalize()**는 title()과 약간 다릅니다. capitalize()는 전체 문자열의 첫 글자만 대문자로 만듭니다.
"john smith"가 "John smith"가 되는 거죠. title()은 "John Smith"로 만들고요.
이 차이를 이해하면 상황에 맞게 선택할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
금융회사에서 고객 이름을 데이터베이스에 저장할 때를 생각해봅시다. 사용자가 "kim minju", "KIM MINJU", "Kim Minju" 중 어떤 형식으로 입력하든, 내부적으로는 모두 소문자로 통일해서 저장합니다.
그래야 나중에 "kim minju"로 검색했을 때 정확하게 찾을 수 있습니다. 하지만 화면에 표시할 때는 title()을 사용해서 "Kim Minju"로 보여주는 것이 사용자 경험에 좋습니다.
하지만 주의할 점도 있습니다. 영어가 아닌 언어에서는 대소문자 변환이 예상과 다를 수 있습니다.
터키어의 'i'는 대문자가 'İ'인데, 일반 upper()는 'I'로 변환합니다. 또한 "O'Neill" 같은 이름에 title()을 적용하면 "O'Neill"이 되어 올바르게 처리됩니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 김개발 씨는 names.str.title()을 사용해서 5초 만에 모든 이름을 깔끔하게 정리했습니다.
팀장님은 "역시 믿고 맡기길 잘했어요!"라며 엄지를 치켜세웠습니다. 대소문자 변환을 제대로 이해하면 데이터 품질을 크게 향상시킬 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 데이터베이스 저장 시에는 lower()로 통일하고, 화면 표시 시에는 title()을 사용하세요
- 이메일 주소는 대소문자를 구분하지 않으므로 lower()로 통일하면 좋습니다
- 특수한 이름(McDonald, O'Brien)은 title()로 완벽하지 않을 수 있으니 확인이 필요합니다
3. 문자열 검색
김개발 씨는 수천 개의 고객 리뷰 중에서 "배송"이라는 단어가 들어간 리뷰만 찾아야 했습니다. 하나씩 눈으로 확인할 수는 없는 노릇입니다.
Pandas에는 이런 상황을 위한 강력한 검색 기능이 있습니다.
**str.contains()**는 문자열 안에 특정 패턴이 있는지 확인하는 메서드입니다. 마치 Ctrl+F로 문서에서 단어를 찾는 것처럼 동작합니다.
검색 결과는 True/False의 Boolean 값으로 반환되어 필터링에 바로 활용할 수 있습니다. 대소문자 구분, 정규표현식 사용 여부 등을 옵션으로 조절할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
# 고객 리뷰 데이터
reviews = pd.DataFrame({
'review': [
'배송이 정말 빨라요!',
'제품은 좋은데 가격이 비싸요',
'배송 포장이 꼼꼼했습니다',
'다음에도 구매할게요',
'배송 기사님이 친절했어요'
]
})
# '배송' 키워드가 포함된 리뷰 찾기
has_delivery = reviews['review'].str.contains('배송')
print("배송 관련 리뷰:", has_delivery)
# 필터링해서 실제 리뷰 내용 보기
delivery_reviews = reviews[has_delivery]
print("\n배송 관련 리뷰 내용:\n", delivery_reviews)
김개발 씨는 이번 주 마케팅 팀의 요청을 받았습니다. "지난달 고객 리뷰 중에서 배송 관련 의견만 추려주세요.
배송 서비스를 개선하려고 하거든요." 리뷰가 5천 개가 넘는데 어떻게 찾을까 막막했습니다. 엑셀에서 Ctrl+F로 하나씩 찾아볼까도 생각했지만, 분명 더 좋은 방법이 있을 것 같았습니다.
그때 선배 박시니어 씨가 다가왔습니다. "contains()를 사용하면 1초면 끝나요." 그렇다면 **str.contains()**란 정확히 무엇일까요?
쉽게 비유하자면, contains()는 마치 도서관 사서가 키워드로 책을 찾아주는 것과 같습니다. "배송"이라는 키워드를 주면, 그 단어가 들어간 모든 리뷰를 순식간에 찾아냅니다.
사람이 하나씩 읽어보는 것과 달리, 컴퓨터는 수천 개의 텍스트를 1초 만에 검색할 수 있습니다. contains()가 없던 시절에는 어땠을까요?
개발자들은 반복문을 만들어서 각 행의 텍스트를 하나씩 확인해야 했습니다. if '배송' in text 같은 조건문을 수천 번 반복하는 거죠.
코드도 길어지고, 실행 속도도 느렸습니다. 더 큰 문제는 복잡한 검색 조건을 추가하기 어렵다는 것이었습니다.
바로 이런 문제를 해결하기 위해 **str.contains()**가 등장했습니다. contains()를 사용하면 벡터화된 검색이 가능해집니다.
전체 데이터에 한 번에 검색을 적용할 수 있습니다. 또한 Boolean 인덱싱도 얻을 수 있어서 검색 결과를 바로 필터링에 활용할 수 있습니다.
무엇보다 정규표현식을 지원해서 복잡한 패턴도 검색할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 reviews['review'].str.contains('배송')을 보면 review 컬럼에서 '배송'이 포함된 행을 찾습니다. 이 부분이 핵심입니다.
결과는 [True, False, True, False, True] 같은 Boolean 배열로 반환됩니다. 다음으로 reviews[has_delivery]에서는 이 Boolean 배열을 필터로 사용해서 True인 행만 추출합니다.
최종적으로 '배송'이 들어간 리뷰만 남게 됩니다. 대소문자 구분은 어떻게 할까요?
기본적으로 contains()는 대소문자를 구분합니다. "Delivery"와 "delivery"를 다르게 취급한다는 뜻입니다.
대소문자를 무시하려면 case=False 옵션을 추가하면 됩니다. str.contains('배송', case=False)처럼 말이죠.
정규표현식을 사용하면 더욱 강력합니다. 예를 들어 "배송"이나 "배달" 둘 다 찾고 싶다면 str.contains('배송|배달')처럼 파이프 기호를 사용합니다.
정규표현식이 싫다면 regex=False로 설정해서 일반 텍스트 검색만 할 수도 있습니다. 실제 현업에서는 어떻게 활용할까요?
전자상거래 회사에서 고객 피드백을 분석한다고 가정해봅시다. "불량", "파손", "문제" 같은 부정적 키워드가 포함된 리뷰를 자동으로 추출해서 CS팀에 전달할 수 있습니다.
또는 "추천", "만족", "좋아요" 같은 긍정 키워드를 찾아서 마케팅 자료로 활용할 수도 있습니다. 쿠팡, 마켓컬리 같은 기업들이 이런 방식으로 리뷰를 자동 분류합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 NaN 값을 처리하지 않는 것입니다.
리뷰 중 일부가 비어있으면 에러가 발생할 수 있습니다. 이럴 때는 na=False 옵션을 추가해서 NaN을 False로 처리하도록 해야 합니다.
str.contains('배송', na=False)처럼 말이죠. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언대로 contains()를 사용한 김개발 씨는 5천 개의 리뷰를 1초 만에 분류했습니다. "와, 정말 빠르네요!" str.contains()를 제대로 이해하면 텍스트 데이터를 효율적으로 필터링하고 분석할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - NaN 값이 있을 때는 na=False 옵션을 꼭 추가하세요
- 대소문자 무시는 case=False로 설정합니다
- 여러 키워드 검색은 정규표현식의 | (or) 연산자를 활용하세요
4. 문자열 분리
김개발 씨는 이번에는 고객 주소 데이터를 받았습니다. 문제는 "서울시 강남구 테헤란로 123"처럼 한 컬럼에 모든 주소가 들어있다는 것입니다.
시, 구, 도로명을 각각 분리해야 하는데 어떻게 해야 할까요?
**str.split()**은 문자열을 특정 구분자로 나누는 메서드입니다. 마치 케이크를 조각으로 자르는 것처럼, 공백이나 쉼표 같은 구분자를 기준으로 문자열을 분리합니다.
결과는 리스트로 반환되며, expand=True 옵션을 사용하면 각 조각이 별도 컬럼으로 펼쳐집니다. 복합 데이터를 개별 필드로 정리하는 데 필수적입니다.
다음 코드를 살펴봅시다.
import pandas as pd
# 고객 주소 데이터
addresses = pd.DataFrame({
'full_address': [
'서울시 강남구 테헤란로 123',
'부산시 해운대구 해변로 456',
'대구시 수성구 국채보상로 789'
]
})
# 공백으로 분리 (리스트로 반환)
split_list = addresses['full_address'].str.split(' ')
print("리스트 형태:\n", split_list)
# expand=True로 각 부분을 별도 컬럼으로 분리
split_df = addresses['full_address'].str.split(' ', expand=True)
split_df.columns = ['시', '구', '도로명', '번지']
print("\n분리된 컬럼:\n", split_df)
# 원본 데이터프레임에 병합
result = pd.concat([addresses, split_df], axis=1)
김개발 씨는 오늘 아침 물류팀으로부터 급한 요청을 받았습니다. "주소 데이터를 시, 구, 동으로 나눠서 보내주세요.
배송 경로 최적화에 필요합니다." 데이터를 열어보니 주소가 전부 한 줄로 붙어있었습니다. "서울시 강남구 테헤란로 123" 같은 식으로요.
수백 개의 주소를 손으로 나눌 수는 없는 노릇입니다. 고민하던 김개발 씨는 Pandas 매뉴얼을 뒤적이다 **str.split()**을 발견했습니다.
그렇다면 **str.split()**이란 정확히 무엇일까요? 쉽게 비유하자면, split()은 마치 가위로 종이를 자르는 것과 같습니다.
공백이나 쉼표 같은 구분자를 기준으로 문자열을 여러 조각으로 나눕니다. "사과,바나나,포도"라는 문자열을 쉼표로 나누면 ["사과", "바나나", "포도"]처럼 세 개의 조각이 됩니다.
split()이 없던 시절에는 어땠을까요? 개발자들은 복잡한 문자열 인덱싱과 슬라이싱을 사용해서 원하는 부분을 직접 추출해야 했습니다.
구분자의 위치를 찾고, 그 앞뒤로 문자열을 잘라내는 코드를 일일이 작성하는 거죠. 실수하기도 쉽고, 데이터 형식이 조금만 바뀌어도 코드를 전부 수정해야 했습니다.
바로 이런 문제를 해결하기 위해 **str.split()**이 등장했습니다. split()을 사용하면 자동 분리가 가능해집니다.
구분자만 지정하면 알아서 문자열을 나눠줍니다. 또한 expand 옵션을 사용하면 분리된 각 부분이 자동으로 새로운 컬럼이 됩니다.
무엇보다 코드가 간결해서 누가 봐도 이해하기 쉽다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 str.split(' ')을 보면 공백을 기준으로 문자열을 나눕니다. 이 부분이 핵심입니다.
결과는 각 주소가 리스트로 변환됩니다. ['서울시', '강남구', '테헤란로', '123'] 같은 형태죠.
다음으로 expand=True 옵션을 추가하면 이 리스트의 각 요소가 별도 컬럼으로 펼쳐집니다. 마지막으로 columns 속성으로 컬럼명을 지정해서 의미 있는 이름을 부여합니다.
분리 개수 제한은 어떻게 할까요? 때로는 처음 몇 개만 분리하고 나머지는 그대로 두고 싶을 때가 있습니다.
이럴 때는 n 매개변수를 사용합니다. str.split(' ', n=2)처럼 설정하면 최대 2번만 분리합니다.
"서울시 강남구 테헤란로 123"이 ["서울시", "강남구", "테헤란로 123"]이 되는 거죠. 다양한 구분자를 사용할 수 있습니다.
공백뿐만 아니라 쉼표, 세미콜론, 탭 문자 등 어떤 구분자든 사용 가능합니다. str.split(',')는 쉼표로, str.split('\t')는 탭으로 분리합니다.
심지어 정규표현식도 사용할 수 있어서 복잡한 패턴도 처리할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
전자상거래 회사에서 주문 데이터를 처리한다고 가정해봅시다. "상품A,상품B,상품C"처럼 한 주문에 여러 상품이 쉼표로 구분되어 있다면, split()으로 각 상품을 분리해서 개별 분석을 할 수 있습니다.
또는 고객 이름이 "성,이름" 형식으로 저장되어 있다면 split(',')로 성과 이름을 분리할 수 있습니다. 금융권에서는 계좌번호나 카드번호 같은 데이터를 분리할 때 자주 사용합니다.
"1234-5678-9012-3456" 형식의 카드번호를 하이픈 기준으로 나눠서 앞 4자리만 표시하고 나머지는 마스킹 처리하는 식으로 활용합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 구분자가 일관되지 않은 데이터를 처리하는 것입니다. 어떤 행은 공백으로, 어떤 행은 탭으로 구분되어 있다면 제대로 분리되지 않습니다.
이럴 때는 정규표현식을 사용해서 str.split('\\s+')처럼 모든 공백 문자를 구분자로 지정해야 합니다. 또 하나 주의할 점은 분리 결과가 불균등할 때입니다.
어떤 주소는 3개 부분으로 나뉘고 어떤 주소는 4개로 나뉜다면, expand=True를 사용했을 때 빈 칸이 NaN으로 채워집니다. 이를 처리하는 로직이 필요합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. split()을 사용한 김개발 씨는 수백 개의 주소를 깔끔하게 시, 구, 도로명으로 분리했습니다.
물류팀 담당자는 "덕분에 배송 경로 최적화가 수월해졌어요!"라며 감사 인사를 전했습니다. str.split()을 제대로 이해하면 복합 데이터를 효과적으로 정리하고 분석할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - expand=True로 분리된 데이터를 바로 DataFrame으로 만들 수 있습니다
- n 매개변수로 분리 횟수를 제한할 수 있습니다
- 정규표현식 \s+를 사용하면 공백, 탭 등 모든 공백 문자로 분리됩니다
5. 문자열 치환
김개발 씨는 고객 전화번호 데이터를 정리하고 있었습니다. 문제는 어떤 번호는 "010-1234-5678", 어떤 번호는 "010.1234.5678", 또 어떤 번호는 "01012345678"처럼 형식이 제각각이라는 것입니다.
이걸 통일된 형식으로 바꿔야 하는데 방법이 있을까요?
**str.replace()**는 문자열의 특정 부분을 다른 문자열로 교체하는 메서드입니다. 마치 문서 편집기의 "찾아 바꾸기" 기능처럼 동작합니다.
정규표현식을 지원해서 복잡한 패턴도 치환할 수 있으며, 모든 매칭을 한 번에 바꿀 수도 있고 개수를 제한할 수도 있습니다. 데이터 정제와 형식 통일에 핵심적인 역할을 합니다.
다음 코드를 살펴봅시다.
import pandas as pd
# 전화번호 데이터 (형식이 제각각)
phones = pd.DataFrame({
'phone': [
'010-1234-5678',
'010.5678.9012',
'01098765432',
'010 1111 2222'
]
})
# 하이픈, 점, 공백을 모두 제거
phones['clean_phone'] = phones['phone'].str.replace('-', '', regex=False)
phones['clean_phone'] = phones['clean_phone'].str.replace('.', '', regex=False)
phones['clean_phone'] = phones['clean_phone'].str.replace(' ', '', regex=False)
# 정규표현식으로 한 번에 처리
phones['clean_phone2'] = phones['phone'].str.replace('[-.\\s]', '', regex=True)
# 통일된 형식으로 변환 (010-XXXX-XXXX)
phones['formatted'] = phones['clean_phone2'].str.replace(r'(\d{3})(\d{4})(\d{4})', r'\1-\2-\3', regex=True)
print(phones)
김개발 씨는 오늘도 골치 아픈 문제와 마주했습니다. 영업팀에서 수집한 고객 전화번호가 형식이 전부 달랐던 것입니다.
어떤 사람은 하이픈으로, 어떤 사람은 점으로, 또 어떤 사람은 공백으로 구분했습니다. "이걸 어떻게 통일하지?" 막막해하던 김개발 씨에게 선배 박시니어 씨가 조언했습니다.
"replace()를 사용하면 간단해요. 찾아 바꾸기처럼 말이죠." 그렇다면 **str.replace()**란 정확히 무엇일까요?
쉽게 비유하자면, replace()는 마치 워드프로세서의 찾아 바꾸기 기능과 같습니다. 특정 단어나 문자를 찾아서 다른 것으로 바꿔줍니다.
"사과"를 "오렌지"로, 하이픈을 공백으로, 옛날 이메일 도메인을 새 도메인으로 자유롭게 교체할 수 있습니다. replace()가 없던 시절에는 어땠을까요?
개발자들은 복잡한 문자열 슬라이싱과 재조합을 사용해야 했습니다. 바꾸고 싶은 부분의 위치를 찾고, 앞부분과 뒷부분을 따로 잘라내서 새로운 문자열을 중간에 끼워 넣는 식이었습니다.
코드가 길어지고 읽기 어려웠으며, 실수하기도 쉬웠습니다. 바로 이런 문제를 해결하기 위해 **str.replace()**가 등장했습니다.
replace()를 사용하면 직관적인 치환이 가능해집니다. 무엇을 무엇으로 바꿀지만 명시하면 됩니다.
또한 정규표현식을 지원해서 복잡한 패턴도 한 번에 처리할 수 있습니다. 무엇보다 전체 컬럼에 일괄 적용되므로 수천 개의 데이터도 순식간에 처리된다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 str.replace('-', '', regex=False)를 보면 하이픈을 빈 문자열로 바꿔서 제거합니다.
이 부분이 기본적인 사용법입니다. regex=False는 일반 문자열 치환을 의미합니다.
세 번 반복해서 하이픈, 점, 공백을 각각 제거합니다. 하지만 더 효율적인 방법이 있습니다.
바로 정규표현식을 사용하는 것입니다. str.replace('[-.\\s]', '', regex=True)를 보면 대괄호 안의 문자들(하이픈, 점, 공백)을 한 번에 모두 제거합니다.
regex=True는 정규표현식을 사용한다는 의미입니다. 이렇게 하면 코드 한 줄로 세 가지 작업을 동시에 처리할 수 있습니다.
패턴 캡처와 재조합도 가능합니다. 마지막 줄의 r'(\d{3})(\d{4})(\d{4})'는 정규표현식으로 전화번호를 세 그룹으로 나눕니다.
앞 3자리, 중간 4자리, 끝 4자리를 각각 캡처하는 거죠. 그리고 r'\1-\2-\3'로 이 세 그룹을 하이픈으로 연결합니다.
결과적으로 "01012345678"이 "010-1234-5678"로 변환됩니다. 실제 현업에서는 어떻게 활용할까요?
웹 서비스를 운영하다 보면 도메인이 바뀌는 경우가 있습니다. 예를 들어 "old-site.com"에서 "new-site.com"으로 마이그레이션할 때, 데이터베이스의 모든 URL을 바꿔야 합니다.
수만 개의 레코드를 replace()로 한 번에 처리할 수 있습니다. 또 다른 예로, 고객이 입력한 데이터를 정제할 때도 자주 사용합니다.
이메일 주소에서 불필요한 공백을 제거하거나, 주민등록번호의 구분자를 통일하거나, 금액 표시의 쉼표를 제거해서 숫자로 변환하는 등의 작업이 모두 replace()로 가능합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 정규표현식의 특수문자를 이스케이프하지 않는 것입니다. 점(.)은 정규표현식에서 "모든 문자"를 의미하므로, 실제 점 문자를 찾으려면 \\.로 이스케이프해야 합니다.
또는 regex=False로 설정해서 일반 문자열로 처리해야 합니다. 또 하나 주의할 점은 원본 데이터 보존입니다.
replace()는 원본을 수정하지 않고 새로운 Series를 반환합니다. 따라서 결과를 새 컬럼에 저장하거나 원본 컬럼을 덮어써야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. replace()를 활용한 김개발 씨는 수천 개의 전화번호를 깔끔하게 "010-XXXX-XXXX" 형식으로 통일했습니다.
영업팀 팀장님은 "이제 전화 걸 때 훨씬 편하겠네요!"라며 만족해했습니다. str.replace()를 제대로 이해하면 데이터 정제 작업을 효율적으로 수행할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - regex=False로 설정하면 정규표현식 특수문자를 일반 문자로 처리합니다
- 정규표현식의 캡처 그룹 \1, \2 등으로 패턴을 재조합할 수 있습니다
- 여러 문자를 한 번에 제거할 때는 [문자들] 형식의 정규표현식이 효율적입니다
6. 정규표현식 기초
김개발 씨는 고객 데이터에서 이메일 주소만 추출해야 하는 작업을 맡았습니다. 문제는 이메일이 텍스트 중간에 섞여있고, 형식도 다양하다는 것입니다.
"어떻게 이메일 패턴을 찾을 수 있을까?" 고민하던 중 정규표현식이라는 강력한 도구를 알게 되었습니다.
정규표현식(Regular Expression)은 문자열의 패턴을 표현하는 특별한 언어입니다. 마치 문자열의 설계도처럼, 원하는 형태를 패턴으로 정의해서 검색하고 추출할 수 있습니다.
Pandas의 str 메서드들은 대부분 정규표현식을 지원하며, 이메일, 전화번호, URL 같은 복잡한 형식의 데이터를 처리하는 데 필수적입니다. 약간의 학습 곡선이 있지만 익히면 텍스트 처리의 강력한 무기가 됩니다.
다음 코드를 살펴봅시다.
import pandas as pd
# 고객 피드백 데이터 (이메일이 섞여있음)
feedbacks = pd.DataFrame({
'text': [
'문의사항은 help@company.com으로 연락주세요',
'제 이메일은 user123@gmail.com입니다',
'010-1234-5678로 전화주시면 됩니다',
'support@service.co.kr에서 답변드립니다'
]
})
# 기본 패턴: 이메일 형식 찾기
# \\w+ : 문자/숫자 1개 이상
# @ : @ 기호
# \\w+ : 도메인명
# \\. : 점
# \\w+ : 최상위 도메인
has_email = feedbacks['text'].str.contains(r'\w+@\w+\.\w+')
print("이메일 포함 여부:\n", has_email)
# 전화번호 패턴: \\d{3}-\\d{4}-\\d{4}
has_phone = feedbacks['text'].str.contains(r'\d{3}-\d{4}-\d{4}')
print("\n전화번호 포함 여부:\n", has_phone)
김개발 씨는 고객 센터 팀의 요청을 받았습니다. "피드백에 적힌 이메일 주소들을 모두 추출해서 메일링 리스트를 만들어주세요." 피드백을 열어보니 이메일이 문장 중간에 자연스럽게 섞여 있었습니다.
"help@company.com으로 연락주세요", "제 이메일은 user@gmail.com입니다" 같은 식이었습니다. 단순히 "@"를 찾는 것만으로는 부족했습니다.
정확한 이메일 형식을 찾아야 했죠. 선배 박시니어 씨가 다가와 말했습니다.
"정규표현식을 배울 때가 왔네요." 그렇다면 정규표현식이란 정확히 무엇일까요? 쉽게 비유하자면, 정규표현식은 마치 문자열을 위한 검색 설계도와 같습니다.
"이런 패턴을 찾아줘"라고 컴퓨터에게 지시하는 특별한 언어입니다. 예를 들어 "숫자 3개-숫자 4개-숫자 4개" 패턴을 표현하면 전화번호를 찾을 수 있습니다.
"문자들@문자들.문자들" 패턴을 표현하면 이메일을 찾을 수 있습니다. 정규표현식이 없던 시절에는 어땠을까요?
개발자들은 복잡한 조건문과 반복문을 중첩해서 패턴을 검사해야 했습니다. "이 위치에 @가 있는지 확인하고, 그 앞에 문자가 있는지 확인하고, 그 뒤에 점이 있는지 확인하고..." 코드가 수십 줄로 늘어났습니다.
게다가 조금만 복잡한 패턴이면 코드를 이해하기도 어려워졌습니다. 바로 이런 문제를 해결하기 위해 정규표현식이 등장했습니다.
정규표현식을 사용하면 패턴 기반 검색이 가능해집니다. 복잡한 형식도 짧은 패턴 문자열로 표현할 수 있습니다.
또한 보편적 표준이라서 Python, JavaScript, Java 등 거의 모든 프로그래밍 언어에서 사용할 수 있습니다. 무엇보다 한번 익히면 텍스트 처리가 훨씬 쉬워진다는 큰 이점이 있습니다.
정규표현식의 기본 문법을 알아봅시다. 가장 기본은 리터럴 문자입니다.
"abc"는 그대로 "abc"를 찾습니다. 하지만 특수문자들은 특별한 의미를 가집니다.
\d는 숫자를 의미합니다. \w는 문자나 숫자를 의미합니다.
\s는 공백 문자를 의미합니다. 점(. )은 모든 문자를 의미합니다.
수량자도 중요합니다. 별표(*)는 0번 이상 반복을 의미합니다.
플러스(+)는 1번 이상 반복을 의미합니다. 물음표(?)는 0번 또는 1번을 의미합니다.
중괄호({n})는 정확히 n번 반복을 의미합니다. 예를 들어 **\d{3}**은 "숫자 정확히 3개"를 의미합니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 r'\w+@\w+\.\w+'를 분석해봅시다.
**\w+**는 "문자나 숫자가 1개 이상"을 의미합니다. 이게 이메일의 로컬 파트입니다.
**@**는 그대로 @ 기호를 찾습니다. 다시 **\w+**로 도메인명을 찾습니다.
**\.**는 점 문자를 찾습니다(이스케이프 필요). 마지막 **\w+**는 최상위 도메인(.com, .kr 등)을 찾습니다.
전화번호 패턴 r'\d{3}-\d{4}-\d{4}'는 더 간단합니다. **\d{3}**은 숫자 3개, 하이픈, 숫자 4개, 하이픈, 숫자 4개를 의미합니다.
"010-1234-5678" 같은 형식을 정확하게 찾아냅니다. 실제 현업에서는 어떻게 활용할까요?
웹 크롤링을 할 때 HTML에서 원하는 정보만 추출하는 데 정규표현식이 자주 사용됩니다. 예를 들어 뉴스 기사에서 날짜를 추출하거나, 상품 페이지에서 가격을 추출하거나, 소셜미디어에서 해시태그를 추출하는 등의 작업에 활용됩니다.
데이터 검증에도 필수적입니다. 사용자가 입력한 이메일, 전화번호, 주민등록번호, 신용카드 번호 등이 올바른 형식인지 검사할 때 정규표현식을 사용합니다.
대부분의 웹 서비스에서 이런 방식으로 입력값을 검증하고 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 정규표현식을 너무 복잡하게 만드는 것입니다. 완벽한 이메일 패턴을 만들려다 보면 정규표현식이 수십 자가 되고, 나중에 보면 본인도 이해하기 어렵습니다.
실무에서는 80-90% 정확도로 충분한 경우가 많습니다. 또 하나 주의할 점은 성능입니다.
복잡한 정규표현식은 실행 속도가 느릴 수 있습니다. 특히 백트래킹이 많이 발생하는 패턴은 수천 개의 데이터를 처리할 때 병목이 될 수 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨에게 정규표현식 기초를 배운 김개발 씨는 수백 개의 피드백에서 이메일 주소를 성공적으로 추출했습니다.
"정규표현식, 어려워 보였는데 막상 써보니 정말 강력하네요!" 정규표현식을 제대로 이해하면 텍스트 처리의 새로운 세계가 열립니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 정규표현식 테스트는 regex101.com 같은 온라인 도구를 활용하세요
- 완벽한 패턴보다는 실용적인 패턴을 만드세요 (80-90% 정확도로 충분)
- r' ' 형식의 raw string을 사용하면 이스케이프가 편리합니다
7. 패턴 추출
김개발 씨는 고객 리뷰에서 가격 정보를 추출해야 하는 작업을 맡았습니다. "10,000원", "만원", "1만원" 같은 다양한 표현이 섞여있었습니다.
단순히 숫자를 찾는 것만으로는 부족했습니다. 정확한 패턴을 추출할 수 있는 방법이 필요했습니다.
**str.extract()**는 정규표현식 패턴을 사용해서 문자열에서 특정 부분을 추출하는 메서드입니다. 마치 광산에서 금을 캐내듯이, 긴 텍스트에서 원하는 정보만 골라낼 수 있습니다.
괄호로 묶은 캡처 그룹을 사용해서 여러 부분을 동시에 추출할 수 있으며, 결과는 DataFrame으로 반환됩니다. contains()가 "있는지 없는지"를 확인한다면, extract()는 "무엇인지"를 가져옵니다.
다음 코드를 살펴봅시다.
import pandas as pd
# 고객 리뷰 데이터
reviews = pd.DataFrame({
'text': [
'가격은 15,000원이고 배송비는 3,000원입니다',
'총 25000원 결제했어요',
'정가 50,000원에서 10% 할인',
'문의는 010-1234-5678로 연락주세요'
]
})
# 금액 추출 (쉼표 포함/미포함 모두 대응)
# (\\d{1,3}(?:,\\d{3})*|\\d+) : 숫자 패턴
# 캡처 그룹 ( )로 추출할 부분 지정
prices = reviews['text'].str.extract(r'([\d,]+)원')
print("추출된 가격:\n", prices)
# 전화번호 추출 (여러 그룹 캡처)
phones = reviews['text'].str.extract(r'(\d{3})-(\d{4})-(\d{4})')
phones.columns = ['앞자리', '중간', '끝자리']
print("\n추출된 전화번호:\n", phones)
# expandall=True로 모든 매칭 추출
all_numbers = reviews['text'].str.extractall(r'([\d,]+)')
print("\n모든 숫자:\n", all_numbers)
김개발 씨는 이번 주 마케팅 팀의 분석 요청을 받았습니다. "고객 리뷰에서 가격 정보를 추출해서 가격대별 만족도를 분석하고 싶어요." 리뷰를 보니 "15,000원", "만5천원", "15000원" 등 표현이 너무 다양했습니다.
"어떻게 이 다양한 형식을 모두 찾지?" 고민하던 김개발 씨에게 선배 박시니어 씨가 조언했습니다. "extract()를 사용하면 패턴에 맞는 정보만 쏙쏙 뽑아낼 수 있어요." 그렇다면 **str.extract()**란 정확히 무엇일까요?
쉽게 비유하자면, extract()는 마치 자석과 같습니다. 복잡한 모래밭에서 자석으로 철가루만 끌어당기듯이, 긴 텍스트에서 원하는 패턴만 정확하게 추출합니다.
contains()가 "철가루가 있나?"를 확인한다면, extract()는 "철가루를 꺼내온다"는 차이가 있습니다. extract()가 없던 시절에는 어땠을까요?
개발자들은 먼저 contains()로 패턴이 있는지 확인하고, 그다음 복잡한 문자열 파싱으로 위치를 찾고, 슬라이싱으로 추출해야 했습니다. 코드가 길어지고 에러가 나기 쉬웠습니다.
특히 한 문장에 여러 개의 패턴이 있을 때는 처리가 매우 복잡해졌습니다. 바로 이런 문제를 해결하기 위해 **str.extract()**가 등장했습니다.
extract()를 사용하면 패턴 기반 추출이 가능해집니다. 정규표현식으로 원하는 형식을 정의하면 자동으로 찾아서 추출합니다.
또한 캡처 그룹을 사용해서 여러 부분을 동시에 추출할 수 있습니다. 무엇보다 결과가 깔끔한 DataFrame으로 반환되어 후속 분석이 쉽다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 r'([\d,]+)원' 패턴을 분석해봅시다.
**[\d,]+**는 "숫자나 쉼표가 1개 이상"을 의미합니다. 이것을 괄호로 묶으면 캡처 그룹이 되어 이 부분만 추출됩니다.
"15,000원"에서 "15,000"만 뽑아내는 거죠. "원"은 괄호 밖에 있어서 매칭에는 사용되지만 추출되지는 않습니다.
여러 그룹을 동시에 캡처할 수도 있습니다. 전화번호 패턴 r'(\d{3})-(\d{4})-(\d{4})'를 보면 괄호가 3개 있습니다.
각 괄호가 하나의 캡처 그룹입니다. 결과는 3개의 컬럼으로 반환되며, 각각 앞자리, 중간, 끝자리를 담고 있습니다.
"010-1234-5678"에서 ["010", "1234", "5678"]을 한 번에 추출하는 거죠. **str.extractall()**은 extract()의 강화 버전입니다.
extract()는 첫 번째 매칭만 반환하지만, extractall()은 모든 매칭을 찾아냅니다. 예를 들어 "가격은 15,000원이고 배송비는 3,000원"에서 extract()는 "15,000"만 추출하지만, extractall()은 "15,000"과 "3,000"을 모두 추출합니다.
실제 현업에서는 어떻게 활용할까요? 전자상거래 회사에서 고객 문의를 자동 분류한다고 가정해봅시다.
문의 내용에서 주문번호를 추출하면 자동으로 해당 주문 정보를 조회할 수 있습니다. "주문번호 20231215-001 확인 부탁드립니다" 같은 문장에서 패턴으로 주문번호를 뽑아내는 거죠.
금융권에서는 거래 내역에서 카드번호, 계좌번호, 금액 등을 추출해서 분석합니다. 로그 데이터에서 IP 주소, 타임스탬프, 에러 코드를 추출해서 시스템 모니터링에 활용하기도 합니다.
소셜미디어 분석에서는 해시태그, 멘션, URL을 추출합니다. "#파이썬 #데이터분석 열심히 공부 중" 같은 글에서 str.extract(r'#(\w+)')로 해시태그를 추출할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 캡처 그룹을 사용하지 않는 것입니다.
extract()는 반드시 괄호로 묶인 캡처 그룹이 있어야 합니다. r'\d+'가 아니라 r'(\d+)'처럼 괄호를 꼭 추가해야 합니다.
또 하나 주의할 점은 매칭되지 않는 행입니다. 패턴에 맞는 내용이 없으면 NaN이 반환됩니다.
따라서 결과를 사용할 때 NaN 처리를 해줘야 합니다. fillna()나 dropna()로 말이죠.
명명된 그룹을 사용하면 더 편리합니다. r'(?P<area>\d{3})-(?P<exchange>\d{4})-(?P<number>\d{4})'처럼 ?P<이름> 형식으로 그룹에 이름을 붙이면, 결과 DataFrame의 컬럼명이 자동으로 설정됩니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. extract()를 마스터한 김개발 씨는 수천 개의 리뷰에서 가격 정보를 깔끔하게 추출했습니다.
마케팅 팀은 "덕분에 가격대별 만족도 분석이 수월했어요!"라며 고마워했습니다. str.extract()를 제대로 이해하면 복잡한 텍스트에서 필요한 정보를 정확하게 추출할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 캡처 그룹은 괄호 ( )로 반드시 묶어야 합니다
- extractall()로 한 문장에서 여러 개의 매칭을 모두 추출할 수 있습니다
- ?P<이름> 형식으로 명명된 그룹을 사용하면 결과 컬럼명이 자동으로 지정됩니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.