iBetter Books
수정

데이터셋 적재와 SQL 탐색

프로젝트를 시작하기 전에 데이터를 정합니다. 어떤 데이터를 쓸지 결정하는 것이 프로젝트의 방향을 결정합니다.

이 프로젝트는 온라인 소매점의 주문 내역을 다룬다고 가정합니다. 고객이 어떤 카테고리의 상품을 얼마나 주문했는지, 어느 지역에서 주문이 많은지, 어느 요일에 주문이 집중되는지를 탐색합니다. 현실적이고 탐색할 것이 충분히 있는 구조입니다.

외부 데이터 파일을 쓰면 "파일이 없다"는 오류로 다른 환경에서 노트북이 실행되지 않을 수 있습니다. 이 프로젝트는 코드로 데이터를 직접 생성하는 방식을 선택합니다. 이 선택 자체가 이미 재현성의 첫 번째 보장입니다.

노트북 구조 설계

프로젝트 노트북의 파일명은 eda_project.py입니다. marimo에서 marimo edit eda_project.py를 실행해 에디터를 엽니다. 챕터가 진행되면서 셀을 차례로 추가합니다.

marimo .py 파일의 실제 구조를 먼저 확인합니다. 에디터에서 편집할 때는 셀 단위로 작성하지만, 파일을 열어보면 이런 형태입니다.

import marimo__generated_with = "0.23.0"app = marimo.App()@app.celldef _():    import marimo as mo    import numpy as np    import pandas as pd    return mo, np, pd

이 구조는 marimo가 자동으로 관리합니다. 에디터에서 셀을 작성하면 파일에 @app.cell 데코레이터가 붙은 함수로 저장됩니다. 지금 단계에서 이 형식을 직접 편집할 필요는 없습니다. Ch 04에서 공유·배포를 논의할 때 다시 살펴봅니다.

셀 1: 라이브러리 임포트

프로젝트에서 사용하는 라이브러리를 한 셀에 모두 모읍니다. 이 셀이 노트북 전체의 의존성 기준선이 됩니다.

import marimo as moimport numpy as npimport pandas as pdimport plotly.express as px

marimo에서 import marimo as mo는 특별합니다. mo 변수를 통해 SQL 실행, 위젯 생성, 레이아웃 구성을 모두 합니다. 이 셀을 가장 처음에 두고 다른 셀이 여기서 나온 변수를 참조하도록 합니다.

셀 2: 합성 데이터 생성

고정 시드로 데이터를 생성합니다. np.random.default_rng(42)가 핵심입니다. 이 한 줄이 "누구나 같은 데이터를 얻는다"는 것을 보장합니다.

rng = np.random.default_rng(42)n_rows = 500categories = ["전자기기", "의류", "식품", "도서", "스포츠"]regions = ["서울", "부산", "대구", "인천", "광주"]weekdays = ["월", "화", "수", "목", "금", "토", "일"]df = pd.DataFrame({    "order_id": range(1, n_rows + 1),    "category": rng.choice(categories, size=n_rows),    "region": rng.choice(regions, size=n_rows),    "weekday": rng.choice(weekdays, size=n_rows),    "amount": rng.integers(5000, 300000, size=n_rows).astype(float),    "quantity": rng.integers(1, 10, size=n_rows),})# 결측값 일부 삽입 (실제 데이터처럼)missing_idx = rng.choice(n_rows, size=25, replace=False)df.loc[missing_idx, "amount"] = float("nan")df

데이터프레임 구조를 설명합니다.

컬럼 타입 설명
order_id int 주문 고유 번호
category str 상품 카테고리
region str 주문 지역
weekday str 주문 요일
amount float 주문 금액 (원). 25건은 결측
quantity int 주문 수량

amount에 의도적으로 25건의 결측값을 넣었습니다. 결측 처리를 탐색 단계에서 확인하기 위해서입니다.

셀 3: 기본 정보 SQL 탐색

df를 SQL에서 테이블 이름으로 바로 참조합니다. PART 04 Ch 02에서 배운 방식입니다. Python에서 만든 데이터프레임을 별도 등록 없이 SQL 안에서 씁니다.

overview = mo.sql(f"""    SELECT        COUNT(*) AS 전체_행수,        COUNT(amount) AS 금액_있는_행수,        COUNT(*) - COUNT(amount) AS 금액_결측수,        ROUND(AVG(amount), 0) AS 평균_금액,        MIN(amount) AS 최소_금액,        MAX(amount) AS 최대_금액    FROM df""")overview
(예시 출력)전체_행수 | 금액_있는_행수 | 금액_결측수 | 평균_금액 | 최소_금액 | 최대_금액    500   |     475       |     25      |  152348   |   5012   |  299876

COUNT(amount)는 결측값을 제외하고 셉니다. COUNT(*)와의 차이가 결측 건수입니다. SQL 한 줄로 기초 품질 지표를 확인할 수 있습니다.

셀 4: 카테고리별 집계

카테고리별 주문 수와 평균 금액을 집계합니다.

by_category = mo.sql(f"""    SELECT        category,        COUNT(*) AS 주문수,        COUNT(amount) AS 유효_주문수,        ROUND(AVG(amount), 0) AS 평균_금액,        ROUND(SUM(amount), 0) AS 총_금액    FROM df    GROUP BY category    ORDER BY 총_금액 DESC""")by_category
(예시 출력)category  | 주문수 | 유효_주문수 | 평균_금액 | 총_금액 전자기기  |  104   |    99      |  153,212  | 15,167,988 의류      |   98   |    93      |  151,023  | 14,045,139...

수치는 고정 시드(42)에 의해 결정됩니다. 이 노트북을 실행하는 사람은 누구나 같은 숫자를 봅니다.

셀 5: 지역·요일별 교차 집계

두 범주형 변수를 교차해서 살펴봅니다.

by_region_weekday = mo.sql(f"""    SELECT        region,        weekday,        COUNT(*) AS 주문수,        ROUND(AVG(amount), 0) AS 평균_금액    FROM df    WHERE amount IS NOT NULL    GROUP BY region, weekday    ORDER BY region, 주문수 DESC""")by_region_weekday

WHERE amount IS NOT NULL 조건으로 결측 행을 집계에서 제외했습니다. 결측 처리 방침을 SQL 안에서 명시적으로 선언하는 습관은 분석 재현성을 높입니다.

셀 6: 데이터 요약 출력

탐색 결과를 읽기 쉬운 형태로 정리합니다.

total_rows = int(overview["전체_행수"].iloc[0])missing_count = int(overview["금액_결측수"].iloc[0])avg_amount = float(overview["평균_금액"].iloc[0])mo.md(f"""### 데이터 요약- 전체 주문: **{total_rows:,}건**- 금액 결측: **{missing_count}건** ({missing_count / total_rows * 100:.1f}%)- 평균 주문 금액: **{avg_amount:,.0f}원**카테고리 {len(categories)}개, 지역 {len(regions)}개, 요일 {len(weekdays)}개로 구성된 합성 데이터입니다.""")

mo.md()에 f-string을 결합해 동적 텍스트를 만듭니다. overview SQL 결과에서 값을 꺼내 쓰기 때문에, 데이터가 바뀌면 이 텍스트도 자동으로 갱신됩니다.

SQL 출력 타입 고정

PART 04 Ch 01에서 설명한 것처럼 mo.sql()의 기본 반환 타입은 auto입니다. Polars가 설치된 환경이면 Polars DataFrame을, 그렇지 않으면 Pandas DataFrame을 반환합니다. 같은 코드라도 환경에 따라 반환 타입이 달라지면 재현성이 흔들립니다.

이 노트북은 pyproject.toml에 출력 타입을 Pandas로 고정합니다.

[tool.marimo.runtime]
default_sql_output = "pandas"

이 설정으로 모든 mo.sql() 호출이 Pandas DataFrame을 반환합니다. .iloc, .mean(), .sum() 같은 Pandas API를 환경 걱정 없이 쓸 수 있습니다. 재현성에서 환경 층 하나를 더 고정하는 것입니다.

여기까지 노트북의 상태

이 시점의 노트북 셀 구조를 정리합니다.

셀 1: import (mo, np, pd, px)셀 2: df 생성 (고정 시드 500행)셀 3: overview SQL (기초 통계)셀 4: by_category SQL (카테고리 집계)셀 5: by_region_weekday SQL (교차 집계)셀 6: mo.md() 요약 출력

df는 셀 2에서 한 번만 정의됩니다. 이후 모든 셀은 df를 읽기만 합니다. 변수 단일 정의 원칙이 지켜지고 있습니다. Ch 04에서 이것이 재현성에 어떤 의미를 갖는지 다시 살펴봅니다.

정리

  • 외부 파일 의존을 없애기 위해 np.random.default_rng(42)로 고정 시드 합성 데이터를 생성합니다. 누가 실행해도 동일한 데이터프레임이 만들어집니다.
  • Python 변수 dfmo.sql() 안에서 테이블 이름으로 직접 참조합니다. 별도 등록 절차가 없습니다.
  • COUNT(*)COUNT(컬럼)의 차이로 결측값을 간단히 확인합니다.
  • df는 셀 하나에서만 정의하고 나머지 셀은 읽기만 합니다. 이 원칙이 재현성의 기반입니다.