오늘은 축(axis)부터 살펴봤다. 표를 그대로 가져오면 두 개의 축을 갖는 데이터의 나열이라고 할 수 있다. 연도별 학생 성적이면 연도가 하나, 성적이 하나다.
Table of contents
Open Table of contents
데이터프레임에 인덱스 넣기
DataFrame 에 딕셔너리로 국어·영어·수학 성적을 넣었다. 이렇게만 하면 어떤 점수인지는 알 수 있지만 누구의 점수인지는 알 수 없다. 그래서 인덱스를 넣어준다.
df = pd.DataFrame({
"국어": [90, 85, 70],
"영어": [80, 95, 75],
"수학": [70, 90, 80]
}, index=["민수", "주성", "철수"])
df
pandas 는 데이터프레임에 축이라는 개념을 넣어놓고 우리가 넣은 데이터를 선택할 수 있게 해준다고 생각하면 된다.
- 인덱스 축은 숫자
0—df.index - 컬럼 축은 숫자
1—df.columns
숫자로 구분한다는 건, 나중에 어떤 함수에 축을 숫자로 넘겨줘야 하는 상황이 생기기 때문이다. 행을 축으로 줘야 하면 0, 열을 축으로 줘야 하면 1이다.
df.axes 를 출력하면 데이터프레임의 축들을 볼 수 있는데 df.index 값과 df.columns 값이 순서대로 반환된다. index 가 첫 번째라서 0, columns 가 두 번째라서 1 이 되는 것이다.
axis 번호를 그대로 믿었다가 헷갈렸다
여기까지 정리하고 실제로 axis 를 써봤는데 값이 생각과 달랐다.
df.sum(axis=0)
국어 245
영어 250
수학 240
dtype: int64
0번으로 축을 줬는데 컬럼을 합한 게 나왔다. 반대로 해보면 이렇다.
df.sum(axis=1)
민수 240
주성 270
철수 225
dtype: int64
axis=0 은 인덱스 축이니까 인덱스별 합계가 나올 거라고 생각했는데 정반대였다.
이유는 이렇다. axis 는 “무엇을 기준으로 묶을지” 가 아니라 “어느 방향으로 훑을지” 를 말한다. axis=0 이면 행 방향으로 위에서 아래로 훑으면서 계산하기 때문에 그 방향의 데이터가 합쳐져 사라지고, 남는 건 컬럼이다. axis=1 이면 열 방향으로 훑으니까 열이 합쳐지고 인덱스가 남는다.
그래서 결과의 길이로 기억하는 게 편하다. axis=0 이면 결과가 컬럼 개수만큼, axis=1 이면 행 개수만큼 나온다.
실제로 이걸 제일 많이 쓰게 되는 자리는 새 열을 파생시킬 때다.
df["총점"] = df.sum(axis=1)
행마다 하나씩 값이 나와야 새 열에 그대로 붙기 때문에 여기서는 axis=1 이다.
축이 정말 너무 헷갈리면 transpose 로 데이터프레임을 전치해서 쓰는 방법도 있다. 다만 데이터가 무거울 수 있으니 head(1) 을 붙여서 방향만 확인하는 식으로 쓰면 가볍다.
df.head(1).T
loc 로 원하는 데이터만 꺼내기
데이터를 보여줄 때는 도메인 지식이 없는 사람에게 보여준다고 생각해야 한다. 그분들에게는 시각화를 해줘야 가독성이 올라간다. 그러려면 표를 만들고 시각화를 하기 위해 필요한 데이터만 꺼낼 수 있어야 한다.
데이터프레임도 시리즈와 마찬가지로 loc 로 가져올 수 있고, 라벨을 기준으로 찾는다.
orders.loc["O002"]
order_date 2026-08-02 00:00:00
customer 서연
region 부산
product 모니터
quantity 1
unit_price 320000
paid True
total_amount 320000
Name: O002, dtype: object
loc 에 하나만 넣으면 행 라벨로 해석한다. 그런데 찾은 데이터 중에 고객 이름만 알고 싶을 때가 있다. 그럴 때는 행 라벨 뒤에 열 라벨을 준다.
orders.loc["O002", "customer"]
# 출력: '서연'
행렬의 개념으로 위치를 생각하면 좌표평면에서 쓰는 (x, y) 가 아니라 (y, x) 순서가 된다. 배열은 좌표계가 아니라 표라고 생각하는 게 편하다.
여러 개 가져오기
여러 개를 고를 거니까 loc 안에 배열을 넣어주면 된다.
orders.loc[
["O002", "O004"]
]
행 하나만 선택했을 때는 시리즈로 나왔는데 두 개를 선택하니까 데이터프레임으로 나온다. 그런데 하나만 선택하더라도 배열로 감싸서 가져오면 데이터프레임으로 나오게 할 수 있다. 시리즈로 가져오면 1차원, 데이터프레임으로 가져오면 2차원이고 각각 쓰임새가 다르다.
행에도 여러 개가 필요하고 열에도 여러 개가 필요하면 각각 배열로 넣는다.
orders.loc[
["O002", "O004"],
["order_date", "customer"]
]
슬라이싱
시리즈에서 슬라이싱이 됐는데 데이터프레임에서도 된다.
orders.loc["O001":"O004"]
행에는 당연히 되고 열에도 된다. 그럼 행에는 적용하지 않고 열에만 슬라이싱하거나 열만 골라낼 수는 없을까 싶었는데, 행 자리에 : 를 주면 된다.
orders.loc[:, ["customer", "total_amount"]]
: 는 “행은 전부” 라는 뜻이다. 우리가 원하는 데이터만 잘라내서 볼 때 이게 도움이 된다.
iloc — 위치 기반
라벨 말고 위치로 인덱싱하는 방법도 있다.
orders.iloc[0]
iloc 에도 배열을 줄 수 있다.
orders.iloc[
[0, 2, 4]
]
위치 기반 슬라이싱도 된다.
orders.iloc[1:4]
다만 이렇게 슬라이싱하는 건 파이썬의 배열 슬라이싱과 똑같이 동작해서 마지막 인덱스를 포함하지 않는다. loc 는 포함하기 때문에 혼동하면 안 된다. 이건 지난 글에서 시리즈를 볼 때 한 번 걸렸던 부분인데 데이터프레임에서도 그대로다.
loc 말고 at 도 있다. 다만 완전히 같지는 않고 at 은 값 하나만 꺼내는 용도다. 슬라이싱이나 배열은 못 넣는 대신 그만큼 빠르다.
조건으로 필터링하기
이제 필터링이다. 어떤 조건을 만족하는 값을 가진 데이터만 골라내는 것부터 봤다. 필터링할 때는 boolean mask 를 쓴다.
보통 조건은 열을 기준으로 잡는다. 데이터프레임에서 조건을 잡을 열을 하나 골라 시리즈 형태로 뽑고, 이 시리즈에 조건을 걸어 마스킹한다.
condition = orders["quantity"] >= 2
orders[condition]

조건을 만족하는 True 만 출력되고 나머지는 제외된다.
부등호뿐만 아니라 == 같은 파이썬 비교 연산자가 조건식을 만들 때 그대로 먹힌다. 그리고 필터링한 결과에서 특정 열만 골라 뽑아낼 수도 있다.
filtered = orders[orders["total_amount"] >= 300000]
filtered[
["customer", "product", "quantity"]
]

조건을 두 개 이상 쓰고 싶으면 시리즈에서 했던 것처럼 & 나 | 를 쓴다.
orders[
quantity_condition & total_condition
][["product", "quantity"]]
이런 식으로 조건을 만족하는 데이터를 가져오고 필요한 열만 추려서 보이게 할 수 있다.
이런 결과가 5년치, 10년치 누적 데이터를 기반으로 나올 수 있다면 고객 맞춤형 마케팅도 가능해진다. 그런데 여기서 중요한 게 도메인 지식이다. 도메인 지식이 없으면 데이터를 본다고 해도 인사이트를 얻을 수 없다. 왜 환적이 오래 걸리는지, 해양물류에서 어떤 데이터를 눈여겨봐야 하는지는 도메인을 알아야 보인다.
데이터 탐색 순서
강의 마지막에 데이터프레임을 처음 받았을 때 훑는 순서를 정리해줬다.
1. 데이터가 정상적으로 불러와졌는지 확인
df.head()
2. 데이터의 크기(모양) 확인
df.shape
3. 컬럼과 인덱스 확인
print(df.columns.to_list())
print(df.index.to_list())
# print(df.index) # 인덱스가 없는 RangeIndex 인 경우
4. 데이터 타입 확인
print(df.dtypes)
5. 전체 구조 확인
print(df.info())
6. 결측값 확인
각각의 열에 대해 결측값이 몇 개인지를 셀 수 있어야 한다.
df.isna().sum()
여기서도 axis 가 그대로 적용된다. sum() 에 아무것도 안 주면 기본이 axis=0 이라 컬럼별 결측값 개수가 나온다.
7. 기술통계 확인 (수치형 데이터 분포)
기술통계는 수치형 데이터에만 의미가 있다.
df[select_cols].describe()
# 바로 df.describe() 를 할 수도 있지만
# 범주형 데이터를 빼고 진행하는 것이 좋다.
8. 범주형 데이터 확인
범주형 데이터는 뭐가 들어있느냐가 중요하다. 범주형이라고 해놓고 100개가 있으면 범주라는 게 의미가 없다.
df["species"].unique()
# 범주형 데이터 중에 고유값만 출력
개수로 보고 싶으면 nunique() 를 쓴다. 그리고 마지막으로 각각의 범주가 얼마나 있는지 확인한다.
df["species"].value_counts()
# df["species"].value_counts().sort_index() 를 하면
# 정렬까지 해준다.
9. 샘플링
앞에서 head() 를 하긴 했지만 간단하게 샘플링을 해준다. head() 는 앞쪽만 보여주니까 뒤쪽이나 중간에 이상한 값이 있으면 안 걸린다.
df.sample(n=5, random_state=42)
정리
axis는 “묶는 기준” 이 아니라 “훑는 방향” 이다. 결과의 길이로 기억하는 게 덜 헷갈린다loc는 라벨,iloc는 위치. 슬라이싱할 때loc는 끝을 포함하고iloc는 포함하지 않는다- 행은 두고 열만 고르고 싶으면 행 자리에
:를 준다 - 필터링은 조건식으로 boolean mask 를 만들어 데이터프레임에 씌우는 것
더 공부해볼 것
at·iat와loc·iloc의 차이 — 메모할 때는at이loc와 기능이 같다고 적었는데, 확인해보니at은 단일 값 접근 전용이었다. 언제 속도 차이가 실제로 유의미해지는지 보고 싶다. → pandas: Fast scalar value getting and settingSettingWithCopyWarning— 필터링한 결과에 값을 대입하면 나오는 경고다. 필터링 결과가 원본의 복사본인지 뷰인지에 따라 달라진다는데 아직 정확히 모르겠다. → Returning a view versus a copyquery()메서드 — 조건이 여러 개일 때df[cond1 & cond2]대신 문자열로 쓸 수 있다고 한다. 가독성이 얼마나 나아지는지 비교해보고 싶다. → DataFrame.querydescribe(include="all")— 범주형까지 포함해서 기술통계를 내는 옵션. 8번 단계를 한 번에 대체할 수 있는지 궁금하다. → DataFrame.describe- numpy 의 axis — pandas 의
axis는 결국 numpy 에서 온 개념이다. 3차원 이상이 되면 축이 어떻게 되는지 보면 지금 헷갈린 부분이 정리될 것 같다. → NumPy: axes