Scriptone

데이터 사이언스 100본 노크를 Pandas·Polars·SQL로 풀기(#21-#30)

이전 노크에 이어, 문제 21부터 30까지를 Pandas/Polars/SQL로 풀어보겠습니다.

문제

문제를 풀기 전에 첫 번째 코드 셀을 반드시 실행하세요. 실행하지 않으면 변수가 정의되지 않아 데이터 처리 문제가 발생할 수 있습니다. 잊지 말고 실행해 주세요.

문제 21

P-021: 레시피 상세 데이터(df_receipt)에 대해, 항목 수를 세어보세요.

# Pandas
len(df_receipt)
# Polars
len(df_receipt)
%%sql
SELECT COUNT(*) FROM receipt

문제 22

P-022: 레시피 상세 데이터(df_receipt)의 고객 ID(customer_id)에 대해, 유니크한 항목 수를 세어보세요.

# Pandas
len(df_receipt["customer_id"].unique())
# Polars
len(df_receipt["customer_id"].unique())
%%sql
SELECT COUNT(DISTINCT customer_id) FROM receipt

문제 23

P-023: 레시피 상세 데이터(df_receipt)에 대해, 상점 코드(store_cd)별로 판매 금액(amount)과 판매 수량(quantity)을 합산하세요.

# Pandas
df_receipt[["store_cd", "amount", "quantity"]].groupby("store_cd").sum()
# Polars
df_receipt.group_by("store_cd").agg(
    pl.col("amount").sum(), 
    pl.col("quantity").sum()
)
%%sql
SELECT
  store_cd,
  SUM(amount),
  SUM(quantity)
FROM
  receipt
GROUP BY
  store_cd

문제 24

P-024: 레시피 상세 데이터(df_receipt)에 대해, 고객 ID(customer_id)별로 가장 최근 판매 연월일(sales_ymd)을 구하고, 10건 표시하세요.

# Pandas
df_receipt[["customer_id", "sales_ymd"]].groupby("customer_id").max().head(10)
# Polars
df_receipt.group_by("customer_id").agg(pl.col("sales_ymd").max()).head(10)
%%sql
SELECT
  customer_id,
  MAX(sales_ymd)
FROM
  receipt
GROUP BY
  customer_id
LIMIT
  10

문제 25

P-025: 레시피 상세 데이터(df_receipt)에 대해, 고객 ID(customer_id)별로 가장 오래된 판매 연월일(sales_ymd)을 구하고, 10건 표시하세요.

# Pandas
df_receipt[["customer_id", "sales_ymd"]].groupby("customer_id").min().head(10)
# Polars
df_receipt.group_by("customer_id").agg(pl.col("sales_ymd").min()).head(10)
%%sql
SELECT
  customer_id,
  MIN(sales_ymd)
FROM
  receipt
GROUP BY
  customer_id
LIMIT
  10

문제 26

P-026: 레시피 상세 데이터(df_receipt)에 대해, 고객 ID(customer_id)별로 가장 최근 판매 연월일(sales_ymd)과 가장 오래된 판매 연월일을 구하고, 둘 다 다른 데이터를 10건 표시하세요.

# Pandas
df_tmp = df_receipt.groupby("customer_id").agg({"sales_ymd": ["max", "min"]}) .reset_index()
df_tmp.columns = ["customer_id", "sales_ymd_max", "sales_ymd_min"]
df_tmp.query("sales_ymd_max != sales_ymd_min").head(10)
# Polars
df_tmp = df_receipt.group_by("customer_id").agg(
    pl.col("sales_ymd").max().alias("sales_ymd_max"),
    pl.col("sales_ymd").min().alias("sales_ymd_min"),
)
df_tmp.filter(pl.col("sales_ymd_max") != pl.col("sales_ymd_min")).head(10)
%%sql
SELECT
  customer_id,
  MAX(sales_ymd) AS sales_ymd_max,
  MIN(sales_ymd) AS sales_ymd_min
FROM
  receipt
GROUP BY
  customer_id
HAVING 
  MAX(sales_ymd) <> MIN(sales_ymd)
LIMIT 10

문제 27

P-027: 레시피 상세 데이터(df_receipt)에 대해, 상점 코드(store_cd)별로 판매 금액(amount)의 평균을 계산하고, 내림차순으로 TOP5를 표시하세요.

# Pandas
df_receipt[["store_cd", "amount"]].groupby("store_cd").mean().sort_values("amount", ascending=False).head()
# Polars
df_receipt.group_by("store_cd").agg(pl.col("amount").mean()).sort("amount", descending=True).head()
%%sql
SELECT
  store_cd,
  AVG(amount) as amount_avg
FROM
  receipt
GROUP BY
  store_cd
ORDER BY
  amount_avg DESC
LIMIT
  5

문제 28

P-028: 레시피 상세 데이터(df_receipt)에 대해, 상점 코드(store_cd)별로 판매 금액(amount)의 중앙값을 계산하고, 내림차순으로 TOP5를 표시하세요.

# Pandas
df_tmp = df_receipt[["store_cd", "amount"]].groupby("store_cd").median().reset_index()
df_tmp.columns = ["store_cd", "amount_med"]
df_tmp.sort_values("amount_med", ascending=False).head()
# Polars
df_receipt.group_by("store_cd").agg(pl.col("amount").median()).sort("amount", descending=True).head()
%%sql
SELECT
  store_cd,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS amount_med
FROM
  receipt
GROUP BY
  store_cd
ORDER BY
  amount_med DESC
LIMIT
  5

문제 29

P-029: 레시피 상세 데이터(df_receipt)에 대해, 상점 코드(store_cd)별로 상품 코드(product_cd)의 최빈값을 구하고, 10건 표시하세요.

# Pandas
df_receipt.groupby("store_cd").product_cd.apply(lambda x: x.mode()).reset_index().head(10)
# Polars
df_receipt.group_by("store_cd").agg(
    pl.col("product_cd").mode()
).head(10)
%%sql
SELECT
  store_cd,
  MODE() WITHIN GROUP(ORDER BY product_cd)
FROM
  receipt
GROUP BY
  store_cd
LIMIT
  10

문제 30

P-030: 레시피 상세 데이터(df_receipt)에 대해, 상점 코드(store_cd)별로 판매 금액(amount)의 분산을 계산하고, 내림차순으로 5건 표시하세요.

# Pandas
df_receipt \
    .groupby("store_cd") \
    .amount.var(ddof=0) \
    .reset_index() \
    .sort_values('amount', ascending=False) \
    .head(5)
# Polars
df_receipt.group_by("store_cd").agg(
    pl.col("amount").var(ddof=0)
).sort("amount", descending=True).head(5)
%%sql
SELECT
    store_cd,
    VAR_POP(amount) AS amount_variance
FROM
    receipt
GROUP BY
    store_cd
ORDER BY
    amount_variance DESC
LIMIT 5;

문제 30까지의 소감

20문제까지는 정규 표현식을 사용한 문자열 처리 등이 중심이었지만, 문제 30까지는 기본 통계량을 사용한 데이터 수치화가 주요 내용이었습니다. SQL, Pandas, Polars에서 언어에 내장된 함수나 프레임워크의 메서드를 사용하여 쉽게 작성할 수 있음을 확인할 수 있었습니다.

또한, Polars는 Pandas보다 간단한 처리에서도 약간 길어지는 경향이 있지만, 기본 통계량 처리에서는 Polars다운 작성 방식을 유지하면서도 길이가 비슷하거나 안정적이었고, Pandas보다 간단하게 작성할 수 있는 경우도 있었습니다. 좋든 나쁘든 일정량의 처리가 필요하지만, 복잡해져도 비슷한 절차로 작성할 수 있고 매우 빠른 속도로 데이터 처리를 할 수 있다는 점에서 Polars의 매력을 다시 발견할 수 있었습니다. Pandas는 Polars 이전에 거의 유일한 선택지로 널리 오랫동안 사용되어 왔기 때문에 익숙한 사람이 많고 정보도 많아서 쉽게 사용할 수 있을 것입니다.

대규모 데이터를 고속으로 처리해야 할 때는 Polars가 유리하지만, 그 외의 많은 경우에는 Pandas도 여전히 강력한 선택지입니다.