벡터화된 문자열 연산

파이썬의 가장 큰 장점 중 하나는 문자열 데이터를 매우 간편하게 다룰 수 있다는 점입니다. Pandas는 이를 기반으로 실제 데이터로 작업(읽기: 정리)할 때 필요한 통합 유형의 중요한 부분인 포괄적인 벡터화된 문자열 작업 세트를 제공합니다. 이번 장에서는 Pandas 문자열 작업 중 일부를 살펴본 다음 이를 사용하여 인터넷에서 수집한 가공되지 않은 복잡한 레시피 데이터를 정리하며 실습해 보겠습니다.

Pandas 문자열 연산 소개

이전 장에서 NumPy 및 Pandas와 같은 도구가 산술 연산을 일반화하여 많은 배열 요소에 대해 동일한 연산을 쉽고 빠르게 수행할 수 있는 방법을 살펴보았습니다. 예를 들어:

import numpy as np

x = np.array([2, 3, 5, 7, 11, 13])
x * 2
array([ 4,  6, 10, 14, 22, 26])

이러한 연산의 벡터화는 데이터 배열에 대한 연산 구문을 단순화합니다. 배열의 크기나 형태를 신경 쓸 필요 없이, 수행할 연산에만 집중합니다. 문자열 배열의 경우 NumPy는 이러한 간단한 액세스를 제공하지 않으므로 더 자세한 루프 구문을 사용하게 됩니다.

data = ["peter", "Paul", "MARY", "gUIDO"]
[s.capitalize() for s in data]
['Peter', 'Paul', 'Mary', 'Guido']

이는 일부 데이터를 작업하는 데는 충분할 수 있지만 누락된 값이 있으면 중단될 수 있으므로 이 접근 방식에서는 추가 확인이 필요합니다.

data = ["peter", "Paul", None, "MARY", "gUIDO"]
[s if s is None else s.capitalize() for s in data]
['Peter', 'Paul', None, 'Mary', 'Guido']

이러한 수동 접근 방식은 장황하고 불편할 뿐만 아니라 오류가 발생하기 쉽습니다.

Pandas에는 벡터화된 문자열 작업에 대한 요구 사항과 Pandas Seriesstr 속성 및 문자열을 포함하는 Index 개체를 통해 누락된 데이터를 올바르게 처리하기 위한 요구 사항을 모두 해결하는 기능이 포함되어 있습니다. 예를 들어 이 데이터로 Pandas Series를 생성하면 누락된 값 처리 기능이 내장된 str.capitalize 메서드를 직접 호출합니다.

import pandas as pd

names = pd.Series(data)
names.str.capitalize()
0    Peter
1     Paul
2     None
3     Mary
4    Guido
dtype: object

Pandas 문자열 메서드 목록

파이썬(Python)의 문자열 조작에 대해 잘 이해하고 있다면 대부분의 Pandas 문자열 구문은 사용 가능한 메서드를 나열하는 것만으로도 충분할 정도로 직관적입니다. 몇 가지 세부 사항에 대해 자세히 알아보기 전에 여기서부터 시작하겠습니다. 이 섹션의 예시에서는 다음 Series 객체를 사용합니다.

monte = pd.Series(
    [
        "Graham Chapman",
        "John Cleese",
        "Terry Gilliam",
        "Eric Idle",
        "Terry Jones",
        "Michael Palin",
    ]
)

파이썬 기본 문자열 메서드와 대응하는 메서드

거의 모든 파이썬(Python)의 내장 문자열 메소드는 Pandas의 벡터화된 문자열 메소드에 의해 미러링됩니다. 다음은 파이썬(Python) 문자열 메서드를 미러링하는 Pandas str 메서드 목록입니다.

len() 낮은() 번역() islower()
밝다() 상부() startswith() isupper()
rjust() 찾기() endswith() isnumeric()
센터() rfind() isalnum() isdecimal()
zfill() 인덱스() 이살파() 분할()
스트립() rindex() isdigit() rsplit()
rstrip() 자본화() isspace() 파티션()
lstrip() 스왑케이스() istitle() rpartition()

여기에는 다양한 반환 값이 있습니다. lower와 같은 일부는 일련의 문자열을 반환합니다.

monte.str.lower()
0    graham chapman
1       john cleese
2     terry gilliam
3         eric idle
4       terry jones
5     michael palin
dtype: object

그러나 일부는 숫자를 반환합니다.

monte.str.len()
0    14
1    11
2    13
3     9
4    11
5    13
dtype: int64

또는 부울 값:

monte.str.startswith("T")
0    False
1    False
2     True
3    False
4     True
5    False
dtype: bool

또 다른 것들은 각 요소에 대해 목록이나 기타 복합 값을 반환합니다.

monte.str.split()
0    [Graham, Chapman]
1       [John, Cleese]
2     [Terry, Gilliam]
3         [Eric, Idle]
4       [Terry, Jones]
5     [Michael, Palin]
dtype: object

토론을 계속하면서 이러한 종류의 목록 시리즈 객체에 대한 추가 조작을 보게 될 것입니다.

정규 표현식 활용

게다가, 정규식(regexps)을 받아 각 문자열 요소의 내용을 검사하고 파이썬(Python) 내장 re 모듈의 API 규칙 중 일부를 따르는 여러 가지 방법이 있습니다:

방법 설명
‘일치’ 각 요소에 대해 ’re.match’를 호출하여 부울을 반환합니다.
추출 각 요소에 대해 re.match를 호출하여 일치하는 그룹을 문자열로 반환합니다.
‘찾기’ 각 요소에 대해 re.findall을 호출합니다
교체 패턴 발생을 다른 문자열로 바꿉니다
포함 각 요소에 대해 re.search를 호출하여 부울
‘카운트’ 패턴 발생 횟수
분할 str.split과 동일하지만 정규 표현식을 허용합니다
rsplit str.rsplit과 동일하지만 정규 표현식을 허용합니다

이를 통해 우리는 광범위한 작업을 수행합니다. 예를 들어 각 요소의 시작 부분에 연속된 문자 그룹을 요청하여 각 요소에서 이름을 추출합니다.

monte.str.extract("([A-Za-z]+)", expand=False)
0     Graham
1       John
2      Terry
3       Eric
4      Terry
5    Michael
dtype: object

또는 문자열 시작(^) 및 문자열 끝($) 정규식 문자를 사용하여 자음으로 시작하고 끝나는 모든 이름을 찾는 등 더 복잡한 작업을 수행할 수도 있습니다.

monte.str.findall(r"^[^AEIOU].*[^aeiou]$")
0    [Graham Chapman]
1                  []
2     [Terry Gilliam]
3                  []
4       [Terry Jones]
5     [Michael Palin]
dtype: object

‘시리즈’ 또는 ‘DataFrame’ 항목 전체에 정규식을 간결하게 적용하는 기능은 데이터 분석 및 정리에 대한 많은 가능성을 열어줍니다.

그 외 유용한 메서드

마지막으로 다른 편리한 작업을 가능하게 하는 몇 가지 기타 방법이 있습니다.

방법 설명
‘얻다’ 각 요소를 색인화합니다
‘슬라이스’ 각 요소를 슬라이스
슬라이스_교체 각 요소의 슬라이스를 전달된 값으로 대체합니다
‘고양이’ 문자열 연결
‘반복’ 값 반복
‘정규화’ 유니코드 형식의 문자열을 반환합니다.
‘패드’ 문자열의 왼쪽, 오른쪽 또는 양쪽에 공백을 추가합니다
긴 문자열을 주어진 너비보다 짧은 길이의 줄로 분할합니다
‘가입’ 전달된 구분 기호를 사용하여 ’계열’의 각 요소에 있는 문자열을 결합합니다
get_dummies 더미 변수를 DataFrame으로 추출

벡터화된 항목 접근 및 슬라이싱

특히 getslice 작업을 사용하면 각 배열에서 벡터화된 요소에 액세스합니다. 예를 들어 str.slice(0, 3)을 사용하여 각 배열의 처음 세 문자의 조각을 얻을 수 있습니다. 이 동작은 파이썬(Python)의 일반 인덱싱 구문을 통해서도 가능합니다. 예를 들어 df.str.slice(0, 3)df.str[0:3]과 동일합니다.

monte.str[0:3]
0    Gra
1    Joh
2    Ter
3    Eri
4    Ter
5    Mic
dtype: object

df.str.get(i)df.str[i]를 통한 인덱싱도 유사합니다.

이러한 인덱싱 방법을 사용하면 split에서 반환된 배열 요소에 액세스할 수도 있습니다. 예를 들어 각 항목의 성을 추출하려면 splitstr 인덱싱을 결합합니다.

monte.str.split().str[-1]
0    Chapman
1     Cleese
2    Gilliam
3       Idle
4      Jones
5      Palin
dtype: object

더미 변수(Indicator Variables)

약간의 추가 설명이 필요한 또 다른 방법은 get_dummies 방법입니다. 이는 데이터에 일종의 코딩된 표시기가 포함된 열이 있는 경우 유용합니다. 예를 들어 A = “미국 출생”, B = “영국 출생”, C = “치즈를 좋아함”, D = “스팸을 좋아함”과 같은 코드 형식의 정보를 포함하는 데이터 세트가 있습니다.

full_monte = pd.DataFrame(
    {"name": monte, "info": ["B|C|D", "B|D", "A|C", "B|D", "B|C", "B|C|D"]}
)
full_monte
name info
0 Graham Chapman B|C|D
1 John Cleese B|D
2 Terry Gilliam A|C
3 Eric Idle B|D
4 Terry Jones B|C
5 Michael Palin B|C|D

get_dummies 루틴을 사용하면 이러한 표시 변수를 DataFrame으로 분할합니다.

full_monte["info"].str.get_dummies("|")
A B C D
0 0 1 1 1
1 0 1 0 1
2 1 0 1 0
3 0 1 0 1
4 0 1 1 0
5 0 1 1 1

이러한 작업을 빌딩 블록으로 사용하면 데이터를 정리할 때 끝없는 범위의 문자열 처리 절차를 구성합니다.

여기서는 이러한 방법에 대해 더 자세히 다루지 않지만 Pandas 온라인 설명서의 “텍스트 데이터 작업”을 읽거나 추가 리소스에 나열된 리소스를 참조하는 것이 좋습니다.

예제: 레시피 데이터 세트 활용

이러한 벡터화된 문자열 작업은 지저분한 실제 데이터를 정리하는 과정에서 가장 유용합니다. 여기에서는 웹의 다양한 소스에서 컴파일된 공개 레시피 데이터베이스를 사용하여 그 예를 살펴보겠습니다. 우리의 목표는 레시피 데이터를 재료 목록으로 구문 분석하여 우리가 가지고 있는 일부 재료를 기반으로 레시피를 빠르게 찾을 수 있도록 하는 것입니다. 이를 컴파일하는 데 사용된 스크립트는 https://github.com/fictivekin/openrecipes에서 찾을 수 있으며, 최신 버전의 데이터베이스에 대한 링크도 여기에 있습니다.

이 데이터베이스는 약 30MB이며 다음 명령을 사용하여 다운로드하고 압축을 풀 수 있습니다.

# repo = "https://raw.githubusercontent.com/jakevdp/open-recipe-data/master"
# !cd data && curl -O {repo}/recipeitems.json.gz
# !gunzip data/recipeitems.json.gz

데이터베이스는 JSON 형식이므로 pd.read_json을 사용하여 읽습니다(파일의 각 줄이 JSON 항목이므로 이 데이터 세트에는 lines=True가 필요합니다).

recipes = pd.read_json("data/recipeitems.json", lines=True)
recipes.shape
(173278, 17)

거의 175,000개의 레시피와 17개의 열이 있는 것을 살펴볼 수 있습니다. 한 행을 살펴보고 무엇이 있는지 살펴보겠습니다.

recipes.iloc[0]
_id                                {'$oid': '5160756b96cc62079cc2db15'}
name                                    Drop Biscuits and Sausage Gravy
ingredients           Biscuits\n3 cups All-purpose Flour\n2 Tablespo...
url                   http://thepioneerwoman.com/cooking/2013/03/dro...
image                 http://static.thepioneerwoman.com/cooking/file...
ts                                             {'$date': 1365276011104}
cookTime                                                          PT30M
source                                                  thepioneerwoman
recipeYield                                                          12
datePublished                                                2013-03-11
prepTime                                                          PT10M
description           Late Saturday afternoon, after Marlboro Man ha...
totalTime                                                           NaN
creator                                                             NaN
recipeCategory                                                      NaN
dateModified                                                        NaN
recipeInstructions                                                  NaN
Name: 0, dtype: object

거기에는 많은 정보가 있지만 그 중 대부분은 웹에서 스크랩한 일반적인 데이터처럼 매우 지저분한 형태입니다. 특히 성분 목록은 문자열 형식입니다. 우리는 관심 있는 정보를 주의 깊게 추출해야 합니다. 먼저 성분을 자세히 살펴보겠습니다.

recipes.ingredients.str.len().describe()
count    173278.000000
mean        244.617926
std         146.705285
min           0.000000
25%         147.000000
50%         221.000000
75%         314.000000
max        9067.000000
Name: ingredients, dtype: float64

성분의 길이는 평균 250자이며 최소 0자에서 최대 10,000자까지 표시됩니다!

궁금해서 어떤 레시피에 재료 목록이 가장 긴지 살펴보겠습니다.

recipes.name[np.argmax(recipes.ingredients.str.len())]
'Carrot Pineapple Spice & Brownie Layer Cake with Whipped Cream & Cream Cheese Frosting and Marzipan Carrots'

우리는 다른 집계 탐색을 수행합니다. 예를 들어 아침 식사에 대한 조리법 중 몇 개가 있는지 확인합니다(소문자와 대문자를 모두 일치시키는 정규식 구문 사용).

recipes.description.str.contains("[Bb]reakfast").sum()
3524

아니면 계피를 재료로 사용하는 레시피가 몇 개나 될까요?

recipes.ingredients.str.contains("[Cc]innamon").sum()
10526

우리는 레시피에 “cinamon”으로 성분의 철자가 틀린 것이 있는지 확인할 수도 있습니다.

recipes.ingredients.str.contains("[Cc]inamon").sum()
11

이는 Pandas 문자열 도구로 가능한 데이터 탐색 유형입니다. 파이썬(Python)이 정말 뛰어난 점은 이와 같은 데이터 정리입니다.

간단한 레시피 추천기

좀 더 나아가서 간단한 레시피 추천 시스템 작업을 시작해 보겠습니다. 재료 목록이 주어지면 우리는 해당 재료를 모두 사용하는 레시피를 찾고 싶습니다. 개념적으로는 간단하지만 데이터의 이질성으로 인해 작업이 복잡해집니다. 예를 들어 각 행에서 깨끗한 성분 목록을 추출하는 등의 쉬운 작업은 없습니다. 따라서 약간의 속임수를 쓰겠습니다. 일반적인 재료 목록부터 시작하여 해당 재료가 각 레시피의 재료 목록에 있는지 검색해 보겠습니다. 단순화를 위해 당분간은 허브와 향신료만 사용하겠습니다.

spice_list = [
    "salt",
    "pepper",
    "oregano",
    "sage",
    "parsley",
    "rosemary",
    "tarragon",
    "thyme",
    "paprika",
    "cumin",
]

그런 다음 각 성분이 목록에 나타나는지 여부를 나타내는 ‘True’ 및 ‘False’ 값으로 구성된 부울 ’DataFrame’을 구축합니다.

import re

spice_df = pd.DataFrame(
    {
        spice: recipes.ingredients.str.contains(spice, re.IGNORECASE)
        for spice in spice_list
    }
)
spice_df.head()
salt pepper oregano sage parsley rosemary tarragon thyme paprika cumin
0 False False False True False False False False False False
1 False False False False False False False False False False
2 True True False False False False False False False True
3 False False False False False False False False False False
4 False False False False False False False False False False

이제 예를 들어 파슬리, 파프리카, 타라곤을 사용하는 레시피를 찾고 싶다고 가정해 보겠습니다. 고성능 Pandas: eval()query()에서 자세히 설명하는 DataFrame의 ‘query’ 메서드를 사용하여 이를 매우 빠르게 계산합니다.

selection = spice_df.query("parsley & paprika & tarragon")
len(selection)
10

이 조합을 사용하면 10개의 레시피만 찾을 수 있습니다. 이 선택 항목에서 반환된 인덱스를 사용하여 해당 레시피의 이름을 찾아보겠습니다.

recipes.name[selection.index]
2069      All cremat with a Little Gem, dandelion and wa...
74964                         Lobster with Thermidor butter
93768      Burton's Southern Fried Chicken with White Gravy
113926                     Mijo's Slow Cooker Shredded Beef
137686                     Asparagus Soup with Poached Eggs
140530                                 Fried Oyster Po’boys
158475                Lamb shank tagine with herb tabbouleh
158486                 Southern fried chicken in buttermilk
163175            Fried Chicken Sliders with Pickles + Slaw
165243                        Bar Tartine Cauliflower Salad
Name: name, dtype: object

이제 우리는 레시피 선택 범위를 175,000에서 10으로 좁혔으므로 저녁 식사로 무엇을 요리할지에 대해 더 많은 정보를 바탕으로 결정을 내릴 수 있게 되었습니다.

레시피 추천 시스템 고도화

이 예제를 통해 Pandas 문자열 메서드를 통해 효율적으로 활성화되는 데이터 정리 작업 유형에 대해 조금이나마 알 수 있었기를 바랍니다. 물론 강력한 레시피 추천 시스템을 구축하려면 많이 더 많은 작업이 필요합니다! 각 레시피에서 전체 성분 목록을 추출하는 것은 작업의 중요한 부분입니다. 불행하게도 사용되는 형식이 매우 다양하기 때문에 이 프로세스는 상대적으로 시간이 많이 걸립니다. 이는 데이터 과학(Data Science)에서 실제 데이터를 정리하고 정리하는 것이 작업의 대부분을 구성하는 경우가 많다는 사실을 의미하며, Pandas는 이 작업을 효율적으로 수행하는 데 도움이 되는 도구를 제공합니다.