Pandas DataFrame 변경과 JSON API 데이터 처리 정리

Alchemist·4일 전

KT AIVLE

목록 보기
4/20

데이터 분석을 하다 보면 단순히 데이터를 조회하는 것보다 새로운 열을 만들고, 기존 값을 수정하고, 필요 없는 행이나 열을 제거하고, 외부 API 데이터를 DataFrame으로 변환하는 작업을 훨씬 자주 하게 된다.

Pandas는 이런 작업을 자연스럽게 처리할 수 있도록 DataFrame을 중심으로 다양한 기능을 제공한다.

이번 글에서는 다음 내용을 한 흐름으로 정리한다.

  • DataFrame에서 컬럼 생성과 값 변경
  • 인덱스의 역할과 set_index(), reset_index()
  • 인덱스가 맞지 않을 때 NaN이 생기는 이유
  • drop()을 이용한 행·열 삭제
  • inplace가 원본 데이터에 미치는 영향
  • JSON 구조 이해
  • Open API 응답을 Python 객체로 변환하는 과정
  • 중첩 JSON에서 필요한 데이터만 골라 DataFrame으로 만드는 방법
  • API 작업에서 자주 만나는 오류와 확인 순서

데이터 변경을 CRUD 관점에서 이해하기

개발에서는 데이터를 다루는 기본 동작을 흔히 CRUD라고 부른다.

구분의미Pandas에서의 예
Create생성컬럼 추가, 새로운 DataFrame 생성
Read조회인덱싱, 필터링, loc, iloc
Update수정특정 값 변경, 컬럼 덮어쓰기
Delete삭제drop()으로 행·열 제거

Pandas는 데이터베이스 자체는 아니지만, 분석용 데이터를 다룰 때 CRUD와 비슷한 흐름을 자주 사용한다.

원본 데이터
    ↓
필요한 컬럼 추가
    ↓
값 수정
    ↓
인덱스 재구성
    ↓
불필요한 행/열 제거
    ↓
분석 가능한 형태로 정리

1. DataFrame에 새로운 컬럼 만들기

먼저 간단한 사용자 행동 데이터를 만들어보자.

import pandas as pd

df = pd.DataFrame({
    "user_id": [101, 102, 103, 104, 105],
    "plan": ["free", "pro", "free", "pro", "free"],
    "purchase_count": [0, 3, 1, 5, 2]
})

df

새로운 컬럼을 추가하는 가장 단순한 방법은 존재하지 않는 컬럼 이름에 값을 대입하는 것이다.

df["status"] = "active"

이 경우 모든 행에 동일한 값이 들어간다.

   user_id  plan  purchase_count  status
0      101  free               0  active
1      102   pro               3  active
2      103  free               1  active
3      104   pro               5  active
4      105  free               2  active

즉,

df["새컬럼"] = 값

형태는 컬럼 생성과 값 할당을 동시에 수행한다.


행마다 다른 값을 넣고 싶다면

리스트, NumPy 배열, Series 등 여러 값을 넣을 수도 있다.

df["grade"] = ["D", "B", "C", "A", "B"]

중요한 점은 할당하는 데이터의 길이가 DataFrame의 행 개수와 맞아야 한다는 것이다.


2. 기존 컬럼 값을 다시 할당하면 수정이 된다

이미 존재하는 컬럼에 값을 다시 대입하면 기존 데이터가 변경된다.

df["grade"] = ["C", "A", "B", "A", "B"]

조건에 따라 값을 바꾸는 경우에는 loc가 유용하다.

df.loc[df["purchase_count"] >= 3, "grade"] = "VIP"

의미는 다음과 같다.

purchase_count가 3 이상인 행 선택
        ↓
grade 컬럼 선택
        ↓
"VIP"로 변경

3. Series를 컬럼으로 넣을 때는 인덱스를 꼭 확인해야 한다

Pandas에서 처음 헷갈리기 쉬운 부분 중 하나가 Series를 DataFrame 컬럼으로 추가할 때 단순히 위치 순서대로 들어가지 않을 수 있다는 점이다.

예를 들어 다음 DataFrame이 있다고 하자.

df = pd.DataFrame({
    "user_id": [101, 102, 103]
})

df.index = ["u1", "u2", "u3"]

그리고 별도의 Series를 만든다.

segment = pd.Series(["new", "regular", "vip"])

segment의 인덱스는 기본적으로 다음과 같다.

0, 1, 2

반면 df의 인덱스는 다음과 같다.

u1, u2, u3

이 상태에서 바로 할당하면:

df["segment"] = segment

Pandas는 위치가 아니라 인덱스 라벨을 기준으로 정렬(alignment) 하려고 한다.

두 객체의 인덱스가 서로 일치하지 않기 때문에 값이 정상적으로 매칭되지 않고 NaN이 생길 수 있다.

직접 실행한 과정에서도 DataFrame 인덱스를 문자열 라벨로 바꾼 뒤, 기본 RangeIndex를 가진 Series를 컬럼으로 할당했을 때 값 대신 NaN이 채워지는 현상을 확인할 수 있었다.

해결 방법: 인덱스를 맞춘 뒤 할당하기

segment.index = df.index
df["segment"] = segment

이제 각 인덱스가 정확히 대응되기 때문에 정상적으로 값이 들어간다.

Pandas는 단순한 위치 기반 배열이 아니라 인덱스 라벨을 기준으로 데이터를 정렬하는 자료구조다.


4. DataFrame 인덱스의 역할

DataFrame을 만들면 기본적으로 다음과 같은 인덱스가 생긴다.

df.index

예:

RangeIndex(start=0, stop=5, step=1)

인덱스는 각 행을 식별하는 라벨 역할을 한다.

기본 숫자 인덱스를 직접 바꿀 수도 있다.

df.index = ["user-a", "user-b", "user-c", "user-d", "user-e"]

이제 loc를 통해 라벨 기반으로 접근할 수 있다.

df.loc["user-c"]

5. set_index()로 특정 컬럼을 인덱스로 사용하기

기존 컬럼을 인덱스로 올리고 싶다면 set_index()를 사용한다.

df = pd.DataFrame({
    "user_id": [101, 102, 103],
    "plan": ["free", "pro", "free"],
    "purchase_count": [0, 3, 1]
})

user_id를 인덱스로 설정:

df.set_index("user_id", inplace=True)

이제 user_id는 일반 컬럼이 아니라 행을 식별하는 인덱스가 된다.

df.loc[102]

인덱스에는 보통 사용자 ID, 주문 번호, 상품 ID처럼 행을 식별할 수 있는 값을 사용하는 편이 이해하기 쉽다.


6. reset_index()로 일반 컬럼으로 되돌리기

현재 인덱스를 다시 컬럼으로 내리고 기본 숫자 인덱스를 복구하려면 reset_index()를 사용한다.

df.reset_index(inplace=True)

기존 인덱스는 일반 컬럼으로 돌아오고 새로운 RangeIndex가 생성된다.

기존 인덱스를 컬럼으로 남길 필요가 없다면:

df.reset_index(drop=True, inplace=True)

drop=True를 사용하면 기존 인덱스를 버리고 기본 숫자 인덱스만 만든다.


7. inplace=True는 무엇일까?

Pandas 메서드를 사용할 때 자주 등장하는 옵션이 inplace다.

df.reset_index(inplace=True)

inplace=True는 현재 DataFrame 객체 자체를 수정하겠다는 의미다.

반대로 기본값인 inplace=False에서는 변경된 결과를 새로운 객체로 반환하고 원본은 그대로 유지된다.


8. drop()으로 컬럼 삭제하기

특정 컬럼을 제거하려면 drop()을 사용한다.

df.drop("grade", axis=1)

axis=1은 컬럼 방향을 의미한다.

좀 더 읽기 쉽게 다음처럼 작성할 수도 있다.

df.drop("grade", axis="columns")

하지만 다음 코드만 실행하면:

df.drop("grade", axis=1)

삭제된 DataFrame이 반환될 뿐 원본 df는 그대로 유지된다.

직접 실행 결과에서도 drop()의 반환 결과에서는 컬럼이 사라졌지만, 원본 DataFrame을 다시 확인했을 때 해당 컬럼이 그대로 존재하는 것을 확인할 수 있었다.

방법 1: 새로운 DataFrame에 저장하기

df2 = df.drop("grade", axis=1)

방법 2: 원본을 직접 수정하기

df.drop("grade", axis=1, inplace=True)

9. 행 삭제하기

행을 삭제할 때는 axis=0을 사용한다.

df.drop(102, axis=0)

여기서 전달하는 값은 행의 위치가 아니라 인덱스 라벨이다.


10. JSON은 왜 API에서 자주 사용할까?

웹 API에서 데이터를 받아오면 응답 형식으로 JSON을 자주 만나게 된다.

{
    "user_id": 101,
    "name": "Kim",
    "plan": "pro"
}

JSON은 key: value 구조를 사용하며 Python 딕셔너리와 매우 비슷하다.

일반적인 처리 흐름은 다음과 같다.

API 서버
    ↓
JSON 문자열 응답
    ↓
Python dict / list로 변환
    ↓
필요한 데이터 추출
    ↓
Pandas DataFrame 생성

11. API 호출에 requests 사용하기

Python에서 HTTP 요청을 보낼 때 requests 라이브러리를 많이 사용한다.

import requests

직접 실행 환경에서는 처음 requests를 import했을 때 다음 오류가 발생했다.

ModuleNotFoundError: No module named 'requests'

현재 Jupyter Kernel이 사용하는 Python 환경에 requests가 설치되어 있지 않았기 때문이다.

Notebook에서는 다음처럼 설치할 수 있다.

%pip install requests

설치 이후 requests import가 정상적으로 동작하는 것을 확인할 수 있었다.


Notebook에서 %pip를 사용하는 이유

터미널에서 사용하는 pip와 Jupyter Kernel이 서로 다른 Python 환경을 가리키면 패키지를 설치했는데도 import가 되지 않을 수 있다.

Notebook 안에서 다음 명령을 사용하면 현재 Kernel 환경을 기준으로 설치하기 때문에 환경 혼동을 줄일 수 있다.

%pip install requests

실행 과정에서는 의존성 버전을 맞추기 위해 urllib3 버전을 다시 조정한 기록도 있었다.

%pip install "urllib3<2" --force-reinstall

환경마다 필요한 버전은 다를 수 있으므로 이런 명령은 오류 메시지나 패키지 호환성을 확인한 뒤 필요한 경우에만 사용하는 것이 좋다.


12. API 키는 코드에 직접 작성하지 않는 것이 좋다

API를 호출할 때 인증 키가 필요한 경우가 많다.

공개할 코드에서는 실제 키를 직접 작성하지 않는 것이 안전하다.

import os

api_key = os.environ.get("API_KEY")

Windows PowerShell:

$env:API_KEY="your-api-key"

macOS / Linux:

export API_KEY="your-api-key"

블로그나 Git 저장소에는 실제 인증 정보를 남기지 않는 것이 중요하다.


13. API 요청의 기본 구조

다음은 재현 가능한 형태로 단순화한 예시다.

import os
import requests

api_key = os.environ.get("API_KEY")

url = "https://api.example.com/resources"

params = {
    "serviceKey": api_key,
    "pageNo": 1,
    "numOfRows": 100,
    "type": "json"
}

response = requests.get(url, params=params)

URL을 직접 문자열로 이어 붙일 수도 있지만 params를 사용하면 요청 파라미터를 분리해서 관리하기 쉽다.


14. 응답이 정상인지 먼저 확인하기

JSON 파싱을 하기 전에 HTTP 요청이 성공했는지 먼저 확인하는 습관이 좋다.

print(response.status_code)

오류 응답이라면 예외를 발생시키도록 할 수도 있다.

response.raise_for_status()

응답 전체가 매우 길다면 일부만 확인한다.

print(response.text[:300])

직접 실행한 API 요청에서는 정상 처리 상태를 나타내는 응답과 함께 실제 데이터가 반환되는 것을 확인할 수 있었다.


15. JSON 문자열을 Python 객체로 변환하기

JSON 문자열은 json.loads()로 Python 객체로 바꿀 수 있다.

import json

data = json.loads(response.text)

변환 후에는 먼저 최상위 키를 확인해보는 것이 좋다.

data.keys()

직접 실행한 응답에서는 최상위 구조가 다음 두 키로 구성되어 있었다.

header
body

API마다 JSON 구조가 다르기 때문에 실제 응답의 키를 한 단계씩 확인하는 습관이 중요하다.


16. KeyError가 발생하면 JSON 구조부터 확인하자

중첩 JSON을 다루다 보면 다음과 같은 오류를 자주 만나게 된다.

KeyError: 'response'

직접 실행한 과정에서도 실제 최상위 키에 존재하지 않는 "response"를 사용해 접근하면서 KeyError가 발생했다.

예를 들어 실제 구조가 다음과 같다고 하자.

{
    "header": {...},
    "body": {...}
}

그런데 다음처럼 접근하면 오류가 발생한다.

data["response"]["body"]

"response"라는 키가 존재하지 않기 때문이다.

이럴 때는 다음처럼 한 단계씩 확인한다.

print(data.keys())
print(data["body"].keys())

중첩 JSON을 다룰 때 가장 기본적이고 효과적인 디버깅 방법이다.


17. 중첩 JSON에서 실제 데이터 목록 찾기

API 응답은 종종 다음처럼 여러 단계로 중첩되어 있다.

body
└── items
    └── item
        ├── record 1
        ├── record 2
        └── record 3

실제 데이터 목록까지 접근하는 코드는 다음 형태가 된다.

items = data["body"]["items"]["item"]

첫 번째 레코드를 확인하면 어떤 키가 들어 있는지 파악하기 쉽다.

print(items[0])

18. 필요한 필드만 추출하기

API는 많은 필드를 반환하지만 실제 분석에는 일부만 필요할 수 있다.

예를 들어 위치 관련 데이터에서 다음 네 값만 사용한다고 하자.

  • 주소
  • 위치명
  • 위도
  • 경도

독립적인 형태로 다시 작성하면:

rows = []

for item in items:
    rows.append({
        "address": item.get("address"),
        "location": item.get("location"),
        "latitude": item.get("latitude"),
        "longitude": item.get("longitude")
    })

item["address"] 대신 item.get("address")를 사용하면 해당 키가 없는 경우 즉시 KeyError가 발생하는 것을 줄일 수 있다.


19. 추출한 데이터를 DataFrame으로 변환하기

리스트 안에 딕셔너리가 있는 형태라면 바로 DataFrame으로 만들 수 있다.

df = pd.DataFrame(rows)

직접 실행한 API 처리 과정에서는 필요한 필드 네 개를 추출해 최종적으로 100행 × 4열 형태의 DataFrame을 생성할 수 있었다.

전체 흐름은 다음과 같다.

API 요청
    ↓
JSON 문자열 응답
    ↓
dict 변환
    ↓
중첩 구조 확인
    ↓
필요한 필드 추출
    ↓
DataFrame 생성

20. 더 간결하게 DataFrame을 만들기

각 레코드가 딕셔너리라면 리스트 컴프리헨션을 사용할 수도 있다.

rows = [
    {
        "address": item.get("address"),
        "location": item.get("location"),
        "latitude": item.get("latitude"),
        "longitude": item.get("longitude")
    }
    for item in items
]

df = pd.DataFrame(rows)

이 방식은 같은 목적을 조금 더 간결하게 표현한다.


21. API 데이터를 DataFrame으로 만든 뒤 확인할 것

변환 직후에는 기본 상태부터 확인하는 것이 좋다.

print(df.shape)
print(df.head())
print(df.dtypes)
print(df.isna().sum())

API 데이터에서는 다음 문제가 자주 발생한다.

빈 문자열
null
누락된 key
숫자가 문자열로 전달됨
예상과 다른 중첩 구조
페이지별 응답 개수 차이

직접 확인한 API 데이터에도 일부 주소 값이 빈 문자열인 레코드가 있었다.

따라서 API 수집 이후에는 거의 항상 전처리 단계가 필요하다.


22. 위도와 경도가 문자열이라면 숫자로 변환하기

API에서는 숫자처럼 보이는 값이 문자열로 전달되는 경우가 많다.

{
    "latitude": "37.1234",
    "longitude": "127.5678"
}

분석이나 시각화에 사용하려면 숫자형으로 변환하는 편이 좋다.

df["latitude"] = pd.to_numeric(
    df["latitude"],
    errors="coerce"
)

df["longitude"] = pd.to_numeric(
    df["longitude"],
    errors="coerce"
)

errors="coerce"를 사용하면 숫자로 변환할 수 없는 값은 NaN으로 처리된다.


23. 이번 과정에서 확인한 대표 오류

상황원인확인 방법
ModuleNotFoundError현재 환경에 패키지가 없음%pip install ... 후 import 재확인
KeyError예상한 JSON key가 실제 구조에 없음dict.keys()로 단계별 구조 확인
Series 할당 후 NaNSeries와 DataFrame 인덱스 불일치양쪽 .index 비교

이 문제들은 모두 문법 자체보다 현재 실행 환경과 데이터 구조를 먼저 확인해야 해결되는 문제라는 공통점이 있다.


24. 개발자 관점에서 이해하기

웹 개발 관점에서도 API 데이터 처리 흐름은 익숙하다.

Backend / Open API
        ↓
JSON Response
        ↓
Python dict / list
        ↓
Pandas DataFrame
        ↓
필터링 / 수정 / 집계
        ↓
분석 결과

프론트엔드에서는 다음과 같이 데이터를 받을 수 있다.

const response = await fetch("/api/users");
const data = await response.json();

Python에서는 비슷하게:

response = requests.get(url)
data = response.json()

처럼 사용할 수 있다.

이후 데이터를 Pandas의 DataFrame으로 바꾸면 열 단위 연산, 필터링, 집계, 전처리를 편리하게 수행할 수 있다.


핵심 정리

  • Pandas에서는 df["새컬럼"] = 값 형태로 컬럼을 생성하거나 수정할 수 있다.
  • Series를 DataFrame 컬럼으로 할당할 때는 인덱스 라벨이 맞는지 확인해야 한다.
  • set_index()는 특정 컬럼을 인덱스로 만들고, reset_index()는 인덱스를 다시 일반 컬럼으로 되돌릴 수 있다.
  • drop()은 기본적으로 변경된 DataFrame을 반환하며 원본을 바로 수정하지 않는다.
  • inplace=True를 사용하면 원본 객체에 변경 사항을 반영한다.
  • API 데이터는 주로 JSON → dict/list → DataFrame 흐름으로 처리한다.
  • 중첩 JSON에서는 먼저 keys()를 사용해 실제 구조를 확인하는 것이 중요하다.
  • 존재하지 않는 키에 접근하면 KeyError가 발생하므로 한 단계씩 구조를 탐색하는 습관이 좋다.
  • Jupyter에서 패키지 문제를 만났다면 현재 Kernel 환경에 설치되어 있는지 확인해야 한다.
  • API 키나 인증 정보는 코드와 블로그에 직접 노출하지 않고 환경 변수 등으로 관리하는 것이 좋다.

정리하며

Pandas에서 데이터를 변경하는 작업은 단순한 문법 암기보다 DataFrame이 인덱스를 기준으로 데이터를 정렬하고, 메서드에 따라 원본 변경 여부가 달라진다는 점을 이해하는 것이 중요하다.

또한 API 데이터를 분석할 때는 바로 DataFrame을 만드는 것보다 먼저 JSON 구조를 확인하고, 필요한 데이터가 어느 경로에 있는지 단계적으로 탐색하는 습관이 필요하다.

결국 데이터 분석에서 자주 반복되는 흐름은 다음처럼 정리할 수 있다.

데이터 수집
    ↓
구조 확인
    ↓
필요한 값 선택
    ↓
DataFrame 변환
    ↓
생성 / 수정 / 삭제
    ↓
분석 가능한 형태로 정리

이 흐름에 익숙해지면 이후 데이터 전처리, 병합, 그룹화, 시각화 작업도 훨씬 자연스럽게 이어갈 수 있다.

profile
html_programming_language

0개의 댓글