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.family | sans-serif (실제로는 DejaVu Sans) | DejaVu Sans에는 한글 글리프가 없어 한글이 □□로 나온다. Windows 기본 글꼴인 맑은 고딕으로 바꾼다. macOS는 AppleGothic |
axes.unicode_minus | True | 음수 눈금에 수학 기호 − (U+2212)를 쓰는데, 한글 글꼴에 이 글리프가 없으면 깨진다. False면 키보드 하이픈 - (U+002D)을 쓴다 |
pd.set_option
display.* 옵션은 표를 화면에 보여 주는 방식만 바꾼다. 데이터는 그대로다.
| 옵션 | 기본값 | 뜻 |
|---|---|---|
display.max_columns | 20 | 보여 줄 최대 컬럼 수. 넘으면 가운데를 ...로 줄인다. None이면 제한 없음 |
display.max_rows | 60 | 보여 줄 최대 행 수 |
display.max_colwidth | 50 | 셀 하나에 보여 줄 최대 글자 수. 서술문처럼 긴 글을 볼 때 늘린다 |
설정은 지금 켜진 커널에만 적용된다. 커널을 재시작하면 기본값으로 돌아가므로 노트북 맨 위에서 매번 실행한다. 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 형식
| CSV | Parquet | |
|---|---|---|
| 저장 방식 | 행 단위 텍스트 | 컬럼 단위 이진 파일 |
| 자료형 | 저장하지 않아 읽을 때 추측 | 저장되어 그대로 복원 |
| 크기 | 크다 | 압축되어 작다 |
| 메모장으로 열기 | 된다 | 안 된다 |
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()가 나오기 전까지 같은 축에 계속 덧그린다. 색을 지정하지 않아도 기본 색 순서(파랑 → 주황 → 초록 …)를 따라 다음 색이 쓰인다.
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.561 | 4년제 졸업자 중 56.1%가 남자 |
"all" 또는 True | 전체 합계 | 표 전체 | 0.158 | 1만 명 중 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)
str.contains(WORD)로 서술문에 낱말이 있으면 True인 컬럼을 만든다.- 직무마다 평균을 내면 True의 비율, 곧 그 낱말이 들어간 서술문의 비율이다. 100을 곱해 퍼센트로 바꾼다.
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확인 목록
설명 없이 코드만 보고 뜻을 말할 수 있으면 체크한다. 체크 상태는 이 브라우저에 저장된다.