입문 실습에 나온 pandas 기초 문법

notebooks/00_beginner.ipynb의 셀을 위에서부터 따라가며 나온 문법을 실제 코드로 설명한다. 숫자는 모두 표본 파일로 직접 계산한 값이다.

1설정 바꾸기0. 준비

import pandas as pd
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
pd.set_option("display.max_columns", 30)

세 줄 모두 라이브러리가 들고 있는 전역 설정값 하나를 바꾼다. matplotlib은 설정을 rcParams에, pandas는 pd.options에 모아 둔다.

plt.rcParams

matplotlib의 기본 설정이 담긴 딕셔너리 모양의 객체다. import할 때 matplotlibrc 파일을 읽어 채운다. 그림을 그릴 때 글꼴이나 선 굵기를 따로 정하지 않으면 여기서 기본값을 가져온다. 키는 "그룹.항목" 모양이고 없는 키나 틀린 형식의 값을 넣으면 바로 에러가 난다. 항목은 300개가 넘는다.

설정기본값바꾼 이유
font.familysans-serif (실제로는 DejaVu Sans)DejaVu Sans에는 한글 글리프가 없어 한글이 □□로 나온다. Windows 기본 글꼴인 맑은 고딕으로 바꾼다. macOS는 AppleGothic
axes.unicode_minusTrue음수 눈금에 수학 기호 − (U+2212)를 쓰는데, 한글 글꼴에 이 글리프가 없으면 깨진다. False면 키보드 하이픈 - (U+002D)을 쓴다

pd.set_option

display.* 옵션은 표를 화면에 보여 주는 방식만 바꾼다. 데이터는 그대로다.

옵션기본값뜻
display.max_columns20보여 줄 최대 컬럼 수. 넘으면 가운데를 ...로 줄인다. None이면 제한 없음
display.max_rows60보여 줄 최대 행 수
display.max_colwidth50셀 하나에 보여 줄 최대 글자 수. 서술문처럼 긴 글을 볼 때 늘린다

설정은 지금 켜진 커널에만 적용된다. 커널을 재시작하면 기본값으로 돌아가므로 노트북 맨 위에서 매번 실행한다. matplotlib 설정은 그 뒤에 그리는 그림부터 적용된다.

2데이터 열기1. 데이터 열기

BASE_URL = "https://kwonsuhyeon.github.io/personaex/data/"

df = pd.read_parquet(BASE_URL + "sample_random.parquet")
df.shape     # (10000, 26)

인터넷 주소로 파일을 읽는 원리

저장소에 GitHub Pages를 켜 두면 GitHub이 지정한 폴더의 파일을 웹 서버처럼 그대로 내보낸다. 주소는 https://<사용자명>.github.io/<저장소명>/<경로> 규칙을 따른다. 이 프로젝트는 docs/ 폴더를 공개하므로 로컬의 docs/data/sample_random.parquet가 위 주소로 나간다.

pd.read_parquet는 첫 인자가 http://나 https://로 시작하면 파일 전체(약 10MB)를 메모리로 내려받은 뒤 읽는다. 디스크에는 저장하지 않는다. read_csv, read_excel도 같은 방식으로 URL을 받는다.

BASE_URL 끝의 /가 빠지면 주소가 .../datasample_random.parquet이 되어 404 에러가 난다.

한 번 받아서 로컬에 두기

df.to_parquet("sample_random.parquet")          # 노트북과 같은 폴더에 저장
df = pd.read_parquet("sample_random.parquet")   # 이후엔 로컬에서 읽는다

URL로 읽으면 실행할 때마다 다시 내려받고, 인터넷이 끊기면 에러가 난다. 여러 번 쓸 파일은 로컬에 저장해 둔다.

parquet 형식

CSVParquet
저장 방식행 단위 텍스트컬럼 단위 이진 파일
자료형저장하지 않아 읽을 때 추측저장되어 그대로 복원
크기크다압축되어 작다
메모장으로 열기된다안 된다

pandas만으로는 parquet을 못 읽는다. pyarrow가 없으면 ImportError: Unable to find a usable engine이 뜬다.

df.shape는 괄호 없이

shape는 메서드가 아니라 속성이다. (행 수, 컬럼 수) 튜플을 돌려준다. df.shape()로 쓰면 'tuple' object is not callable 에러가 난다. Jupyter는 셀의 마지막 줄 값을 자동으로 출력하므로 print() 없이 결과가 보인다.

3컬럼 고르기2. 컬럼과 자료형

df.dtypes      # 컬럼마다 자료형. object는 문자열, int64는 정수
df.head(3)     # 처음 3행

여러 컬럼을 한 번에 고르기

demo_cols = [
    "sex",              # 성별. 남자 / 여자
    "age",              # 나이. 19~99세 정수
    "marital_status",   # 혼인 상태. 미혼 / 배우자있음 / 사별 / 이혼
    "education_level",  # 최종 학력. 무학부터 대학원까지 7단계
    "bachelors_field",  # 전공 계열 (11가지). 4년제 대학교·대학원만 값이 있고 나머지는 '해당없음'
    "occupation",       # 직업명. 2,000가지가 넘는다. 일이 없으면 '무직'
    "province",         # 사는 시도. 17개 (서울, 경기, 충청남 ...)
]
df[demo_cols].head(5)

대괄호 안에 넣는 것에 따라 결과가 달라진다.

쓴 것결과
df["age"]컬럼 하나. Series
df[["sex", "age"]]컬럼 여러 개. DataFrame
df[demo_cols]리스트 변수를 넣은 것. 위와 같다

리스트는 대괄호가 닫힐 때까지 줄바꿈을 무시한다. 그래서 한 줄에 하나씩 쓰고 옆에 주석을 달아도 한 줄로 쓴 것과 똑같이 동작한다. 마지막 항목 뒤의 쉼표도 문제없다.

province 값은 "도"가 빠진 형태(충청남, 경상북)와 줄임말(전북)이 섞여 있다. 시도 이름으로 거를 때는 이 표기를 그대로 쓴다.

4결측치 세기3-1. 빈 값이 있는가

df.isna().sum()    # 컬럼별 결측치 수

1단계 df.isna()

df와 모양이 같은 표를 새로 만든다. 칸이 비어 있으면 True, 값이 있으면 False다.

   sex    age            sex    age
0  남자    74      →   0  False  False
1  NaN    23          1  True   False
2  여자    NaN         2  False  True

2단계 .sum()

파이썬은 True를 1, False를 0으로 계산한다. 컬럼마다 위에서 아래로 더하면(axis=0, 기본값) 합이 곧 결측치 수다.

df.isna().sum().sum()     # 표 전체의 결측치 수
df.isna().mean() * 100    # 컬럼별 결측 비율(%)
df.isna().sum(axis=1)     # 행마다 빈 칸 수

True/False의 평균은 비율

이 문서에서 가장 자주 쓰는 성질이다. True, False, True, True의 평균은 (1+0+1+1)/4 = 0.75, 곧 True의 비율 75%다. 결측 비율, 여자 비율, 낱말 포함 비율이 모두 이 원리로 나온다.

결측치로 세지 않는 값. isna()가 빈 값으로 보는 것은 NaN, None, NaT, pd.NA뿐이다. 빈 문자열 "", "해당없음", "무직"은 글자이므로 세지 않는다. 이 데이터는 isna().sum()이 모두 0이지만 bachelors_field가 "해당없음"인 사람이 6,611명(66%)이다.
(df["bachelors_field"] == "해당없음").sum()    # 6611

5행 거르기3-2, 3-5, 4-1

불리언 인덱싱

대괄호 안에 True/False Series를 넣으면 True인 행만 남는다. pandas에서 행을 거르는 기본 방법이다.

df["sex"] == "남자"             # ① 행마다 True/False. 1만 개
df[df["sex"] == "남자"]         # ② True인 행(남자)만 남긴 DataFrame
df[df["sex"] == "남자"]["age"]  # ③ 그중 age 컬럼만. Series

df.loc[df["sex"] == "남자", "age"]   # ③과 같다. 행 조건, 컬럼을 쉼표로 나눠 한 번에

읽기만 할 때는 두 방식이 같다. 거른 결과에 값을 넣을 때는 .loc을 써야 경고가 나지 않는다.

조건 묶기

employed = df[(df["occupation"] != "무직") & ~df["occupation"].str.contains("현재 구직중")]
len(employed), len(df)    # (6084, 10000)
뜻파이썬 기본pandas Series
그리고and&
또는or|
아니다not~

and, or, not은 값 하나의 참·거짓만 따진다. Series에 쓰면 The truth value of a Series is ambiguous 에러가 난다. &, |, ~는 칸마다 따로 계산해 True/False Series를 돌려준다.

괄호가 필요한 이유. &가 !=, ==보다 먼저 계산된다. 괄호 없이 쓰면 파이썬은 df["occupation"] != ("무직" & ...)로 읽어 에러를 낸다. 비교식은 항상 괄호로 감싼다. ~df[...].str.contains(...)에는 괄호가 없어도 되는데, .으로 잇는 메서드 호출이 ~보다 먼저 계산되기 때문이다.

같지 않다 vs 들어 있지 않다

코드True가 되는 경우쓰는 곳
s != "무직"값 전체가 정확히 "무직"이 아닐 때값이 딱 정해진 경우
s.str.contains("현재 구직중")글자가 어딘가에 들어 있을 때"전직 출납 창구 사무원, 현재 구직중"처럼 앞부분이 사람마다 다를 때
~s.str.contains("현재 구직중")글자가 들어 있지 않을 때str.contains에는 반대 버전이 없어서 ~로 뒤집는다
s.isin(["A", "B", "C"])목록 중 하나와 같을 때(s == "A") | (s == "B") | (s == "C")를 줄여 쓴 것

여러 값을 나란히 출력

len()은 행 수다. len(employed), len(df)처럼 쉼표로 나열하면 파이썬이 튜플로 묶어 (6084, 10000)으로 한 번에 보여 준다.

조건을 변수로 빼 두기

MY_DEPT = "컴퓨터공학"
MY_JOB = "소프트웨어 개발"
mine = jobs[(jobs["dept"] == MY_DEPT) & (jobs["job"] == MY_JOB)]

바꿀 값을 셀 위쪽 변수에 모아 두면, 두 값만 고쳐 아래 셀을 다시 실행해 다른 학과·직무를 볼 수 있다. 이름을 대문자로 쓴 것은 "사용자가 바꾸는 설정값"이라는 표시일 뿐 문법상 의미는 없다.

6요약 통계3-2, 3-3, 3-5

코드결과
df["age"].describe()개수, 평균, 표준편차, 최솟값, 사분위수(25·50·75%), 최댓값. 이 표본은 최솟값 19, 중앙값 51, 최댓값 99
df["sex"].value_counts()값마다 개수. 많은 순으로 정렬
df["sex"].value_counts(normalize=True)개수 대신 비율(0~1). 100을 곱하면 퍼센트
df["province"].value_counts().head(10)많은 순 10개
jobs.groupby("dept")["job"].unique()학과마다 어떤 직무 값이 있는지 목록으로
.round(2)소수 둘째 자리까지 반올림

출력 아래에 Name: proportion이나 Name: count가 붙는 것은 pandas 2.0부터 결과 Series에 이름을 붙이기 때문이다. 값에는 영향이 없다.

7그림 그리기3-2, 3-3, 4-4

히스토그램 두 개 겹치기

df[df["sex"] == "남자"]["age"].plot.hist(bins=range(15, 105, 5), histtype="step", linewidth=2, label="남자")
df[df["sex"] == "여자"]["age"].plot.hist(bins=range(15, 105, 5), histtype="step", linewidth=2, label="여자")
plt.legend()
plt.xlabel("나이")
plt.show()

Series의 .plot은 내부에서 matplotlib을 부른다. 괄호 안 인자도 대부분 matplotlib의 것이다.

인자·함수뜻
bins=range(15, 105, 5)막대 경계. range는 끝값을 빼므로 15, 20, …, 100의 18개 경계, 막대 17개. 각 구간은 왼쪽 포함·오른쪽 제외이고 마지막 95~100만 100을 포함한다. 숫자 하나(bins=17)를 주면 최솟값~최댓값을 등분해 경계가 19.0, 23.7…처럼 어중간해진다
histtype="step"막대 속을 칠하지 않고 윤곽선만. 겹쳐 그려도 뒤의 막대가 앞을 가리지 않는다. 기본값은 "bar", 윤곽을 따라 채우려면 "stepfilled"
linewidth=2선 두께. 기본값 1
label="남자"범례에 쓸 이름표. 이것만으로는 화면에 나오지 않는다
plt.legend()범례(legend)를 그린다. 어느 색 선이 어느 집단인지 알려 주는 작은 상자로, 앞에서 붙인 label을 모아 만든다
plt.xlabel("나이")x축 이름. 1절의 글꼴 설정 덕분에 한글이 나온다
plt.show()그림을 내보내고 "지금 그리는 그림"을 비운다. 없으면 그림 위에 <matplotlib.legend.Legend at 0x…> 같은 글자가 함께 찍힌다

두 줄이 한 그림에 겹치는 이유. matplotlib은 지금 그리고 있는 축(current axes)을 하나 기억해 두고 plt.show()가 나오기 전까지 같은 축에 계속 덧그린다. 색을 지정하지 않아도 기본 색 순서(파랑 → 주황 → 초록 …)를 따라 다음 색이 쓰인다.

최솟값이 19세라 15~20 막대에는 19세 한 살만 들어간다. 다른 막대는 다섯 살을 담으므로 이 막대만 유난히 낮다. 19세가 적어서가 아니라 구간 폭이 달라서다.

y축은 사람 수다. 두 집단 크기가 많이 다르면 density=True를 넣어 막대 면적 합이 1이 되게 맞춘 뒤 모양을 비교한다.

가로 막대그림

order = ["무학", "초등학교", "중학교", "고등학교", "2~3년제 전문대학", "4년제 대학교", "대학원"]
(df["education_level"].value_counts(normalize=True) * 100).reindex(order).plot.barh(color="#2a78d6")
plt.xlabel("비율 (%)")
plt.ylabel("학력")
plt.show()
부분뜻
바깥 괄호 ( ... * 100).이 *보다 먼저 계산된다. 괄호가 없으면 100에 .plot을 붙이려다 에러가 난다
.reindex(order)많은 순 대신 order에 적은 순서로 다시 줄 세운다. 이름이 데이터 값과 한 글자라도 다르면 에러 없이 그 항목만 NaN이 되어 막대가 사라진다
.plot.barh()가로 막대(bar horizontal). 인덱스가 세로축 이름, 값이 막대 길이. 긴 한글 이름이 한 줄로 들어간다. 세로 막대는 .plot.bar()
color="#2a78d6"16진수 색 코드. 두 자리씩 빨강 2a(42)·초록 78(120)·파랑 d6(214). 파랑이 가장 세서 파란색
plt.xlabel / plt.ylabel가로 막대라 x축이 비율이다. y축 이름을 안 쓰면 인덱스 이름 education_level이 자동으로 붙는다

barh는 첫 항목을 맨 아래에 그린다. value_counts() 결과를 그대로 그리면 가장 많은 값이 맨 아래로 간다. 많은 것을 위로 올리려면 .sort_values()로 작은 순 정렬한 뒤 그린다. 학력처럼 높낮이가 있는 변수는 reindex로 순서를 정해 주는 편이 읽기 쉽다.

상자그림

jobs.boxplot(column="age", by="dept", grid=False)
plt.title("")
plt.suptitle("")
plt.ylabel("나이")
plt.show()
부분뜻
column="age", by="dept"dept 값마다 age의 상자를 하나씩 그린다
grid=False배경 격자를 끈다
plt.title("")pandas가 자동으로 붙인 축 제목 age를 지운다
plt.suptitle("")그림 맨 위의 Boxplot grouped by dept를 지운다

상자 가운데 선이 중앙값, 상자의 아래·위 끝이 1사분위수와 3사분위수다. 상자 밖으로 뻗은 선(수염)은 상자 길이의 1.5배 안에 있는 가장 먼 값까지 닿고 그 밖의 점은 따로 찍힌다.

8교차표3-4, 4-3

pd.crosstab(df["sex"], df["education_level"], normalize="index").round(3) * 100

pd.crosstab(행, 열)은 두 범주형 변수의 조합마다 사람 수를 센 표를 만든다. normalize는 사람 수 대신 비율을 넣으라는 옵션이고 값에 따라 무엇을 기준으로 나눌지가 달라진다. pandas에서 index는 행 쪽을 가리킨다.

normalize나누는 기준합이 1인 곳남자·4년제 대학교 칸읽는 법
없음나누지 않음-1585남자이면서 4년제인 사람이 1,585명
"index"행 합계행0.316남자 중 31.6%가 4년제
"columns"열 합계열0.5614년제 졸업자 중 56.1%가 남자
"all" 또는 True전체 합계표 전체0.1581만 명 중 15.8%가 4년제 남자

어느 쪽을 고를지는 기준 집단으로 정한다. 남자와 여자의 학력 분포를 비교하려면 "index"(행 = 성별), 학력별 성비를 보려면 "columns"다. 비율로 바꾸면 두 집단의 크기가 달라도 공정하게 비교할 수 있다. 뒤의 .round(3) * 100은 소수 셋째 자리에서 반올림한 뒤 퍼센트로 바꾼다.

9새 컬럼과 groupby4-2

jobs["여자"] = jobs["sex"] == "여자"
jobs["전문대이상"] = jobs["education_level"].isin(["2~3년제 전문대학", "4년제 대학교", "대학원"])
jobs.groupby("dept").agg(인원=("age", "size"), 나이중앙값=("age", "median"),
                         여자비율=("여자", "mean"), 전문대이상비율=("전문대이상", "mean")).round(2)

새 컬럼 만들기

대괄호 안에 없는 컬럼 이름을 쓰고 =로 값을 넣으면 표 오른쪽 끝에 컬럼이 새로 생긴다. 이미 있는 이름이면 덮어쓴다. 한글 이름도 된다. 오른쪽부터 읽으면 된다. jobs["sex"] == "여자"로 True/False를 만들고 그 결과를 여자 컬럼에 저장한다.

True/False로 만든 까닭은 4절의 성질 때문이다. 평균을 내면 곧 비율이 된다.

새 컬럼은 메모리의 jobs에만 생긴다. parquet 파일은 그대로다. 커널을 재시작하면 사라지고, 이 셀을 건너뛰면 여자를 쓰는 뒤 셀에서 KeyError가 난다.

groupby와 agg

groupby("dept")는 학과 값이 같은 행끼리 묶기만 한다. 뒤에 계산을 붙여야 결과가 나온다. .agg()는 묶음마다 여러 계산을 한꺼번에 한다. 괄호 안은 결과이름=("어느 컬럼으로", "무슨 계산") 형식이며 pandas에서는 이를 named aggregation이라고 부른다.

쓴 것뜻
인원=("age", "size")행 수. size는 어느 컬럼을 넣어도 같아서 형식상 age를 적었다. 비슷한 count는 NaN을 빼고 센다
나이중앙값=("age", "median")나이의 중앙값
여자비율=("여자", "mean")True/False의 평균이므로 여자 비율
전문대이상비율=("전문대이상", "mean")전문대 이상 비율
           인원  나이중앙값  여자비율  전문대이상비율
dept
고용서비스정책  1000   40.0   0.55      0.91
기계공학      1500   46.0   0.25      0.55
데이터경영     2000   41.0   0.36      0.91
...

두 컬럼으로 묶기

jobs.groupby(["dept", "job"]).agg(나이중앙값=("age", "median"), ...)

리스트로 두 컬럼을 주면 (학과, 직무) 조합마다 묶는다. 결과의 행 이름이 두 층이 되는데, 이를 MultiIndex라고 한다. 같은 학과 이름은 첫 줄에만 찍히고 아래는 비어 보인다.

10서술문 읽기5. 서술문 읽기와 낱말 세기

for _, r in mine.sample(3, random_state=42).iterrows():
    print("=" * 90)
    print(r["occupation"], "|", r["sex"], r["age"], "세 |", r["education_level"])
    print("[직업 페르소나]", r["professional_persona"])
    print("[경력 목표]", r["career_goals_and_ambitions"])
부분뜻
mine.sample(3, random_state=42)행 3개를 무작위로 뽑는다. random_state를 고정하면 매번 같은 3명이 나온다. 숫자 42에 특별한 뜻은 없다. 다른 숫자를 넣으면 다른 3명이 나온다
.iterrows()행을 하나씩 꺼내 준다. 한 번에 (행 번호, 행 내용) 두 개를 준다
for _, r in ...두 값을 _와 r에 나눠 받는다. 행 번호는 쓰지 않으므로 "버리는 값"이라는 관례로 _라고 이름 붙였다. r["age"]처럼 컬럼 이름으로 그 사람의 값을 꺼낸다
들여쓰기for 줄 끝의 : 아래 들여 쓴 네 줄이 한 사람마다 반복된다
"=" * 90문자열에 정수를 곱하면 그만큼 반복한다. = 90개로 된 구분선
print(a, "|", b)쉼표로 나열한 값을 공백 하나씩 띄워 출력한다. 그래서 출력이 43 세처럼 숫자와 "세" 사이가 띄어진다

표 전체를 반복문으로 돌리는 일은 느리다. iterrows는 이 셀처럼 몇 줄만 사람이 읽기 좋게 찍을 때 쓴다. 계산은 컬럼 단위 연산으로 한다.

11글자 수와 낱말 포함 비율5-1, 5-2

문자열 컬럼 뒤에 .str을 붙이면 문자열 함수를 행마다 적용한다. 결과는 다시 Series다.

서술문 길이

jobs["글자수"] = jobs["professional_persona"].str.len()
jobs.groupby(["dept", "job"])["글자수"].median().sort_values()

str.len()은 글자 수다. 공백과 문장부호도 한 글자로 센다. 새 컬럼으로 저장한 뒤 (학과, 직무)마다 중앙값을 내고 sort_values()로 작은 순 정렬한다. 소프트웨어 개발 500명의 중앙값은 175자다.

특정 낱말이 들어간 비율

WORD = "고객"
jobs["포함"] = jobs["professional_persona"].str.contains(WORD)
(jobs.groupby(["dept", "job"])["포함"].mean() * 100).round(1).sort_values(ascending=False)
  1. str.contains(WORD)로 서술문에 낱말이 있으면 True인 컬럼을 만든다.
  2. 직무마다 평균을 내면 True의 비율, 곧 그 낱말이 들어간 서술문의 비율이다. 100을 곱해 퍼센트로 바꾼다.
  3. sort_values(ascending=False)는 큰 순 정렬이다. 기본값(True)은 작은 순이다.

str.contains는 기본적으로 찾을 글자를 정규표현식으로 해석한다. (, ., + 같은 기호가 든 낱말을 글자 그대로 찾으려면 regex=False를 넣는다.

12기술 목록의 낱말 세기5-3

skills = mine["skills_and_expertise_list"].str.findall(r"'([^']+)'")
skills = skills.explode()
words = skills.str.split(" ").explode()
words = words[words.str.len() >= 2]
words = words[~words.isin(["관리", "기술", "능력", "활용", "통한", "위한"])]
words.value_counts().head(20)

한 줄이 한 단계다. 소프트웨어 개발 직무 500명을 예로 단계마다 값이 어떻게 바뀌는지 따라간다.

① 겉모습은 리스트, 실제는 문자열

skills_and_expertise_list의 값은 리스트처럼 보이지만 자료형은 문자열(str)이다. 그래서 바로 항목을 꺼낼 수 없다.

"['C/C++ 기반 시스템 프로그래밍', '리눅스 커널 최적화 및 튜닝', '임베디드 시스템 아키텍처 설계', '레거시 코드 분석 및 유지보수']"

② str.findall로 항목 꺼내기

str.findall(패턴)은 정규표현식 패턴에 맞는 부분을 모두 찾아 리스트로 돌려준다. 패턴 r"'([^']+)'"을 쪼개 보면 이렇다.

조각뜻
r"..."raw 문자열. 역슬래시(\)를 특수문자로 해석하지 않는다. 정규표현식은 습관처럼 r을 붙인다
' … '양 끝의 작은따옴표를 글자 그대로 찾는다
[^']작은따옴표가 아닌 글자 하나. [^...]는 "이것 말고"라는 뜻
+앞의 것이 한 번 이상 이어짐
( )캡처 그룹. findall은 괄호 안의 부분만 돌려주므로 따옴표가 빠진다
['C/C++ 기반 시스템 프로그래밍', '리눅스 커널 최적화 및 튜닝', '임베디드 시스템 아키텍처 설계', '레거시 코드 분석 및 유지보수']   ← 이제 진짜 리스트

③ explode로 펼치기

explode()는 리스트가 든 칸을 항목 하나가 한 행이 되도록 펼친다. 500명의 리스트가 항목 2,077개로 늘어난다. 펼친 행들은 원래 행 번호를 그대로 물려받아 같은 번호가 여러 번 나온다.

2500    C/C++ 기반 시스템 프로그래밍
2500       리눅스 커널 최적화 및 튜닝
2500      임베디드 시스템 아키텍처 설계
...

세 사람짜리 작은 표로 보면 이렇다.

펼치기 전. 칸 하나에 값이 여러 개 뭉쳐 있다.

0  김철수  ['요리', '운전']
1  이영희  ['엑셀', '운전']
2  박민수  ['운전']

explode() 후. 칸 하나에 값 하나다.

0  요리
0  운전
1  엑셀
1  운전
2  운전
  • 행 수가 바뀐다. 3명이 5행이 된다. 행 수가 사람 수가 아니라 항목 수를 따른다. 실제 데이터에서는 500명이 2,077행이 된다.
  • 행 번호는 원래 사람의 번호를 쓴다. 왼쪽 숫자(인덱스)를 0, 1, 2…로 새로 매기지 않는다. 김철수에게서 나온 두 행이 모두 0번이다. 그래서 번호를 보면 누구의 항목이었는지 알 수 있다. 위의 2500이 네 번 나온 것도 네 기술이 모두 2500번 한 사람의 것이라는 뜻이다.

펼치는 이유

여러 사람의 리스트에 흩어져 있는 같은 항목을 한데 모아 세기 위해서다. value_counts()는 칸 하나를 값 하나로 본다.

펼치지 않고 세면

[요리, 운전]    1
[엑셀, 운전]    1
[운전]          1

리스트 전체를 값 하나로 보므로 세 리스트가 모두 다른 값이 된다. "운전"이 세 명에게 있다는 사실이 드러나지 않는다.

펼친 뒤에 세면

운전    3
요리    1
엑셀    1

리스트 안에 갇혀 있던 "운전"이 각각 한 행이 되어 3으로 합쳐진다.

세기에만 쓰는 것은 아니다. 항목 하나가 한 행이 되면 5절의 불리언 인덱싱을 그대로 쓸 수 있다. 아래 ⑤에서 words[words.str.len() >= 2]로 한 글자 낱말을 거르는 것처럼, 항목 단위로 거르거나 묶으려면 먼저 펼쳐야 한다.

행 번호가 겹치면 "0번 행"을 골랐을 때 여러 줄이 나온다. 번호를 0부터 새로 매기려면 explode(ignore_index=True)로 쓴다. 이 노트북은 낱말을 세기만 하므로 번호가 겹쳐도 결과가 같다.

④ 띄어쓰기로 쪼개서 다시 펼치기

str.split(" ")은 공백을 기준으로 잘라 리스트를 만든다. 다시 explode()하면 낱말 하나가 한 행이 된다. 항목 2,077개가 낱말 9,430개가 된다.

['C/C++', '기반', '시스템', '프로그래밍', '리눅스', ...]

⑤ 거르기 두 번

  • words[words.str.len() >= 2]: 두 글자 이상만 남긴다. "및" 같은 한 글자 낱말이 빠져 8,780개가 된다. 5절의 불리언 인덱싱과 같은 방식이다.
  • words[~words.isin([...])]: 목록에 있는 낱말을 뺀다. 어느 직무에나 흔히 나와 직무를 구별하는 데 도움이 안 되는 낱말들이다. 텍스트 분석에서는 이런 낱말을 불용어(stopword)라고 부른다.

⑥ 세기

value_counts().head(20)로 많이 나온 낱말 20개를 본다. 소프트웨어 개발 직무에서는 최적화(456), 설계(429), 시스템(349)이 앞에 온다.

띄어쓰기로 자르면 "데이터베이스를"과 "데이터베이스"가 다른 낱말로 세어진다. 조사를 떼어 내려면 형태소 분석기가 필요하다. 이 노트북은 기술 목록이 대부분 명사로 끝나는 짧은 구라서 띄어쓰기만으로도 쓸 만한 결과가 나온다.

13확인 목록

설명 없이 코드만 보고 뜻을 말할 수 있으면 체크한다. 체크 상태는 이 브라우저에 저장된다.