파이썬 기반 데이터분석을 위하여 Pandas 라이브러리 기반 문자 데이터를 다루는 방법을 알아보자.
※ 본 내용에서 사용하는 big_mart.csv 파일은 별도로 다운로드 받아야 한다.
※ big_mart.csv 다운받기 [클릭]
※ 상기 데이터는 Kaggle의 데이터 세트 페이지에서 상세 내용 확인 가능하다.
개요
데이터 분석을 함에 있어서 숫자만 다룰 수 없다. 지역명, 상품명, 상호명, 채팅내역 등 많은 내용이 문자(또는 문자열)로 되어있다. 그래서 내가 원하는 문자가 들어있는 행을 필터링하거나 특정 문자를 제거하는 등 다양한 작업이 필요할 수 있다. 파이썬에서는 문자를 다루기 위한 전문 라이브러리와 함수를 지원하긴 하지만 그 중에서도 Pandas 라이브러리를 사용할 수 있다.
문자 형식의 원소를 가지는 Pandas Series 객체는 .str 접근자(accessor)를 사용하여 관련 어트리뷰트 또는 메서드를 사용할 수 있다. 이와 관련해서 pandas.Series.str.capitalize()
부터 pandas.Series.str.get_dummies()
까지 총 54개의 어트리뷰트와 메서드가 있다.
※ Pandas 2.0.0 기준
실습
데이터 준비
실습을 위해 “big_mart.csv” 파일을 읽어오고 확인해보자.
1 | df = pd.read_csv("big_mart.csv") |
ProductID | FatContent | ProductType | OutletID | OutletSize | LocationType | OutletType | |
---|---|---|---|---|---|---|---|
0 | FDA15 | Low Fat | Dairy | OUT049 | Medium | Tier 1 | Supermarket Type1 |
1 | DRC01 | Regular | Soft Drinks | OUT018 | Medium | Tier 3 | Supermarket Type2 |
2 | FDN15 | Low Fat | Meat | OUT049 | Medium | Tier 1 | Supermarket Type1 |
3 | FDX07 | Regular | Fruits and Vegetables | OUT010 | NaN | Tier 3 | Grocery Store |
4 | NCD19 | Low Fat | Household | OUT013 | High | Tier 3 | Supermarket Type1 |
문자열 데이터를 다루기 위해 수치형 변수는 .select_dtypes()
메서드로 제외처리했다.
.len()
.str.len()
메서드는 문자열의 길이를 반환한다. 문자열 길이는 띄어쓰기 같은 특수문자 까지 포함하며 다음의 코드를 확인해보자.
1 | df["LocationType"].unique() |
“Tier 1” 원소의 문자 개수는 6이라고 산출되는 것을 알 수 있다. 이 메서드는 주로 특정 길이의 원소의 입력이 기대될 때 길이가 다른 문자가 있는지 확인하는 용도로 사용한다. 예를 들어 지금처럼 “LocationType” 변수에 “Tier” 다음에 띄어쓰기가 한 칸 있고 숫자가 하나 뒤따라오는 패턴이 기대될 때 다른 형식 또는 길이의 원소가 있는 경우를 검사하고자 하는 경우 활용하기도 한다. 그렇게 사용하기 위해서는 .value_counts()
메서드와 같이 사용한다.
1 | df["LocationType"].str.len().value_counts() |
만약 새로운 문자 “asdfasdf”가 추가된다면 상기 결과는 다음과 같이 바뀐다.
1 | aa = pd.concat([df["LocationType"], pd.Series(["asdfasdf"])], |
이렇게 .str.len()
메서드를 사용해서 규격 외의 원소를 탐지할 수 있다.
.replace()
.str.replace()
는 문자열의 일부를 치환하거나 제거할 때 사용하는 메서드이다. 그리고 .replace()
메서드와 용법이 다르니 용도에 맞게 사용해야 하겠다. 다음은 “Fat” 이라는 글자를 “x”로 치환하거나 없애는 예제이다.
1 | df["FatContent"][:4].str.replace(pat = "Fat", repl = "x") |
“pat” 인자는 “pattern”의 준말이며 일반 문자열 패턴 또는 정규표현식(regular expression)을 사용할 수 있다. 그리고 “repl”은 “replacement”의 준말이며 “pat”인자에 선언한 패턴에 해당하는 문자열을 대체할 문자열을 써준다. 상기 코드를 보면 “repl” 인자에 비어있는 문자열을 입력하게 되면 특정 패턴의 문자열을 제거할 수 있다는 것을 알 수 있다.
정규표현식을 사용하는 경우 다양하고 복잡한 문자열 패턴을 손쉽게 바꿀 수 있다. 하지만 해당 문법을 별도로 공부해야 하기에 텍스트마이닝을 하거나 문자열 데이터를 자주 다루지 않으면 현실적으로 외우고 사용하기 어렵다. 일단 다음의 유용한 예제만 소개하고 상세한 정규표현식 사용 예제는 별도의 게시글에서 다룰 예정이다.
1 | ser = pd.Series(["1200원", "10.23$", "1,345$"]) |
.contains()
.str.contains()
메서드는 특정 문자열 패턴을 만족하면 True
를 반환하고 그렇지 않으면 False
를 반환한다.
다음은 “at” 라는 글자가 들어간 원소에 대해 알아보는 코드이다. “Low Fat”와 “low fat”원소의 위치에 True
가 있는 것을 볼 수 있다.
1 | df["FatContent"].unique() |
반환값이 True
와 False
이기 때문에 “at”가 포함되는 원소의 개수를 세는 코드는 다음과 같다.
1 | df["FatContent"].str.contains("at").sum() |
다음은 “at” 패턴의 원소가 있는 행을 필터링 하는 코드이다. 꽤 자주 사용되는 코드 패턴이니 숙지하는 것이 좋다.
1 | df_sub = df.loc[df["FatContent"].str.contains("at"), ] |
ProductID | FatContent | ProductType | OutletID | OutletSize | LocationType | OutletType | |
---|---|---|---|---|---|---|---|
0 | FDA15 | Low Fat | Dairy | OUT049 | Medium | Tier 1 | Supermarket Type1 |
2 | FDN15 | Low Fat | Meat | OUT049 | Medium | Tier 1 | Supermarket Type1 |
4 | NCD19 | Low Fat | Household | OUT013 | High | Tier 3 | Supermarket Type1 |
7 | FDP10 | Low Fat | Snack Foods | OUT027 | Medium | Tier 3 | Supermarket Type3 |
10 | FDY07 | Low Fat | Fruits and Vegetables | OUT049 | Medium | Tier 1 | Supermarket Type1 |
.split()
.str.split()
메서드는 문자열 원소를 특정 패턴으로 분리하는 기능을 지원한다. 이를 확인하기 위해 다음과 같이 데이터를 준비하자.
1 | df_pt = df[["ProductType"]].drop_duplicates().reset_index(drop = True) |
ProductType | |
---|---|
0 | Dairy |
1 | Soft Drinks |
2 | Meat |
3 | Fruits and Vegetables |
4 | Household |
5 | Baking Goods |
6 | Snack Foods |
7 | Frozen Foods |
8 | Breakfast |
9 | Health and Hygiene |
10 | Hard Drinks |
11 | Canned |
12 | Breads |
13 | Starchy Foods |
14 | Others |
15 | Seafood |
띄어쓰기를 기준으로 분리하는 경우 다음과 같은 결과가 나오며 각 원소 내부에 리스트 형태로 원소가 배치된 것을 볼 수 있다.
1 | df_pt["ProductType"].str.split(" ") |
길이가 제각각인 리스트가 원소에 있을 경우 그 리스트 내부의 원소로의 접근이 매우 어렵다. 그래서 리스트 구조를 풀어버리기 위해 .explode()
메서드를 사용할 수 있다.
1 | df_pt["ProductType"].str.split(" ").explode() |
하지만 상기의 방법 밖에 없는 것은 아니다. “expand” 인자에 True
를 할당하면 데이터프레임을 반환한다.
1 | df_pt["ProductType"].str.split(" ", expand = True) |
0 | 1 | 2 | |
---|---|---|---|
0 | Dairy | None | None |
1 | Soft | Drinks | None |
2 | Meat | None | None |
3 | Fruits | and | Vegetables |
4 | Household | None | None |
5 | Baking | Goods | None |
6 | Snack | Foods | None |
7 | Frozen | Foods | None |
8 | Breakfast | None | None |
9 | Health | and | Hygiene |
10 | Hard | Drinks | None |
11 | Canned | None | None |
12 | Breads | None | None |
13 | Starchy | Foods | None |
14 | Others | None | None |
15 | Seafood | None | None |
특정 원소에 띄어쓰기가 두 번 있는 경우 해당 원소가 3개로 분리되며 하나도 없는 경우 그대로 값이 유지된다. 반환되는 데이터프레임의 열 개수는 분리하고자 하는 패턴 일치 횟수+1 만큼 생성이 되며 상기 결과의 경우 “Fruits and Vegetables”와 “Health and Hygiene”원소 때문에 열 개수가 3개로 생성되었다. 그리고 패턴 일치 횟수가 최대값이 아닌 원소의 경우 오른쪽에 “None”이 생성되는데 이는 결측치로 취급되며 관련 메서드가 올바르게 동작하는 것을 알 수 있다.
※ 결측치 관련 게시물 확인하기 [클릭]
1 | df_pt["ProductType"].str.split(" ", expand = True).fillna("") |
0 | 1 | 2 | |
---|---|---|---|
0 | Dairy | ||
1 | Soft | Drinks | |
2 | Meat | ||
3 | Fruits | and | Vegetables |
4 | Household | ||
5 | Baking | Goods | |
6 | Snack | Foods | |
7 | Frozen | Foods | |
8 | Breakfast | ||
9 | Health | and | Hygiene |
10 | Hard | Drinks | |
11 | Canned | ||
12 | Breads | ||
13 | Starchy | Foods | |
14 | Others | ||
15 | Seafood |
여기서 조금 불만은 반환된 데이터프레임 객체의 변수명이 단순 숫자로 되어있다는 것이다. 숫자로 된 변수명은 향후 코드 운용에서 걸림돌로 작용할 가능성이 크기 때문에 Pandas 2.0.0 버전에 새로 추가된 메서드인 .add_prefix()
를 사용해서 접두사를 추가하는 것도 방법이다.
1 | df_pt["ProductType"].str.split(" ", expand = True).add_prefix("type_") |
type_0 | type_1 | type_2 | |
---|---|---|---|
0 | Dairy | None | None |
1 | Soft | Drinks | None |
2 | Meat | None | None |
3 | Fruits | and | Vegetables |
4 | Household | None | None |
5 | Baking | Goods | None |
6 | Snack | Foods | None |
7 | Frozen | Foods | None |
8 | Breakfast | None | None |
9 | Health | and | Hygiene |
10 | Hard | Drinks | None |
11 | Canned | None | None |
12 | Breads | None | None |
13 | Starchy | Foods | None |
14 | Others | None | None |
15 | Seafood | None | None |
분리 전의 데이터프레임 객체와 분리 후 데이터프레임 객체를 concat()
함수로 이어붙이는 예제는 다음과 같다.
1 | pd.concat([df_pt, df_pt["ProductType"].str.split(" ", expand = True)], axis = 1) |
ProductType | 0 | 1 | 2 | |
---|---|---|---|---|
0 | Dairy | Dairy | None | None |
1 | Soft Drinks | Soft | Drinks | None |
2 | Meat | Meat | None | None |
3 | Fruits and Vegetables | Fruits | and | Vegetables |
4 | Household | Household | None | None |
5 | Baking Goods | Baking | Goods | None |
6 | Snack Foods | Snack | Foods | None |
7 | Frozen Foods | Frozen | Foods | None |
8 | Breakfast | Breakfast | None | None |
9 | Health and Hygiene | Health | and | Hygiene |
10 | Hard Drinks | Hard | Drinks | None |
11 | Canned | Canned | None | None |
12 | Breads | Breads | None | None |
13 | Starchy Foods | Starchy | Foods | None |
14 | Others | Others | None | None |
15 | Seafood | Seafood | None | None |
기타 유용한 메서드
여러 메서드가 있지만 .slice()
, .extract()
, .lower()
, .zfill()
메서드가 나름 많이 활용되는 축에 속하니 별도의 학습을 권장한다.