メインコンテンツまでスキップ

データサイエンス 100 本ノック(構造化データ加工編)with Polars

はじめに

  • 原本ではPandasを利用していますが、Polarsを利用して解いてみました
  • データはPostgreSQLからCSVを出力して利用しています
import math
from sklearn.model_selection import train_test_split
from sklearn.model_selection import TimeSeriesSplit
from imblearn.under_sampling import RandomUnderSampler
import polars as pl

df_customer = pl.read_csv("../data/customer.csv")
df_category = pl.read_csv("../data/category.csv")
df_product = pl.read_csv("../data/product.csv")
df_receipt = pl.read_csv("../data/receipt.csv")
df_store = pl.read_csv("../data/store.csv")
df_geocode = pl.read_csv("../data/geocode.csv")

演習問題


P-001: レシート明細データ(df_receipt)から全項目の先頭 10 件を表示し、どのようなデータを保有しているか目視で確認せよ

df_receipt.head(10)

P-002: レシート明細データ(df_receipt)から売上年月日(sales_ymd)、顧客 ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、10 件表示せよ

df_receipt.select(
pl.col("sales_ymd"), pl.col("customer_id"), pl.col("product_cd"), pl.col("amount")
).head(10)

P-003: レシート明細データ(df_receipt)から売上年月日(sales_ymd)、顧客 ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、10 件表示せよ。ただし、sales_ymd を sales_date に項目名を変更して抽出すること

df_receipt.select(
pl.col("sales_ymd").alias("sales_date"),
pl.col("customer_id"),
pl.col("product_cd"),
pl.col("amount"),
).head(10)

P-004: レシート明細データ(df_receipt)から売上日(sales_ymd)、顧客 ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の条件を満たすデータを抽出せよ

  • 顧客 ID(customer_id)が"CS018205000001"
df_receipt.select(
pl.col("sales_ymd"), pl.col("customer_id"), pl.col("product_cd"), pl.col("amount")
).filter(pl.col("customer_id") == "CS018205000001")

P-005: レシート明細データ(df_receipt)から売上日(sales_ymd)、顧客 ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の全ての条件を満たすデータを抽出せよ

  • 顧客 ID(customer_id)が"CS018205000001"
  • 売上金額(amount)が 1,000 以上
df_receipt.select(
pl.col("sales_ymd"), pl.col("customer_id"), pl.col("product_cd"), pl.col("amount")
).filter((pl.col("customer_id") == "CS018205000001") & (pl.col("amount") >= 1000))

P-006: レシート明細データ(df_receipt)から売上日(sales_ymd)、顧客 ID(customer_id)、商品コード(product_cd)、売上数量(quantity)、売上金額(amount)の順に列を指定し、以下の全ての条件を満たすデータを抽出せよ

  • 顧客 ID(customer_id)が"CS018205000001"
  • 売上金額(amount)が 1,000 以上または売上数量(quantity)が 5 以上
df_receipt.select(
pl.col("sales_ymd"),
pl.col("customer_id"),
pl.col("product_cd"),
pl.col("amount"),
pl.col("quantity"),
).filter(
(pl.col("customer_id") == "CS018205000001")
& ((pl.col("amount") >= 1000) | (pl.col("quantity") >= 5))
)

P-007: レシート明細データ(df_receipt)から売上日(sales_ymd)、顧客 ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の全ての条件を満たすデータを抽出せよ

  • 顧客 ID(customer_id)が"CS018205000001"
  • 売上金額(amount)が 1,000 以上 2,000 以下
df_receipt.select(
pl.col("sales_ymd"), pl.col("customer_id"), pl.col("product_cd"), pl.col("amount")
).filter(
(pl.col("customer_id") == "CS018205000001")
& (pl.col("amount") >= 1000)
& (pl.col("amount") <= 2000)
)

P-008: レシート明細データ(df_receipt)から売上日(sales_ymd)、顧客 ID(customer_id)、商品コード(product_cd)、売上金額(amount)の順に列を指定し、以下の全ての条件を満たすデータを抽出せよ

  • 顧客 ID(customer_id)が"CS018205000001"
  • 商品コード(product_cd)が"P071401019"以外
df_receipt.select(
pl.col("sales_ymd"), pl.col("customer_id"), pl.col("product_cd"), pl.col("amount")
).filter(
(pl.col("customer_id") == "CS018205000001") & (pl.col("product_cd") != "P071401019")
)

P-009: 以下の処理において、出力結果を変えずに OR を AND に書き換えよ

df_store.query('not(prefecture_cd == "13" | floor_area > 900)')

df_store.filter((pl.col("prefecture_cd") != 13) & (pl.col("floor_area") <= 900))

P-010: 店舗データ(df_store)から、店舗コード(store_cd)が"S14"で始まるものだけ全項目抽出し、10 件表示せよ

df_store.filter(pl.col("store_cd").str.starts_with("S14")).head(10)

P-011: 顧客データ(df_customer)から顧客 ID(customer_id)の末尾が 1 のものだけ全項目抽出し、10 件表示せよ

df_customer.filter(pl.col("customer_id").str.ends_with("1")).head(10)

P-012: 店舗データ(df_store)から、住所 (address) に"横浜市"が含まれるものだけ全項目表示せよ

df_store.filter(pl.col("address").str.contains("横浜市"))

P-013: 顧客データ(df_customer)から、ステータスコード(status_cd)の先頭がアルファベットの A〜F で始まるデータを全項目抽出し、10 件表示せよ

df_customer.filter(pl.col("status_cd").str.contains("^[A-F]")).head(10)

P-014: 顧客データ(df_customer)から、ステータスコード(status_cd)の末尾が数字の 1〜9 で終わるデータを全項目抽出し、10 件表示せよ

df_customer.filter(pl.col("status_cd").str.contains("[1-9]$")).head(10)

P-015: 顧客データ(df_customer)から、ステータスコード(status_cd)の先頭がアルファベットの A〜F で始まり、末尾が数字の 1〜9 で終わるデータを全項目抽出し、10 件表示せよ

df_customer.filter(pl.col("status_cd").str.contains("^[A-F].*[1-9]$")).head(10)

P-016: 店舗データ(df_store)から、電話番号(tel_no)が 3 桁-3 桁-4 桁のデータを全項目表示せよ

df_store.filter(pl.col("tel_no").str.contains(r"^\d{3}-\d{3}-\d{4}$"))

P-017: 顧客データ(df_customer)を生年月日(birth_day)で高齢順にソートし、先頭から全項目を 10 件表示せよ

df_customer.sort("birth_day").head(10)

P-018: 顧客データ(df_customer)を生年月日(birth_day)で若い順にソートし、先頭から全項目を 10 件表示せよ

df_customer.sort("birth_day", descending=True).head(10)

P-019: レシート明細データ(df_receipt)に対し、1 件あたりの売上金額(amount)が高い順にランクを付与し、先頭から 10 件表示せよ。項目は顧客 ID(customer_id)、売上金額(amount)、付与したランクを表示させること。なお、売上金額(amount)が等しい場合は同一順位を付与するものとする

df_receipt.select(
pl.col("customer_id"),
pl.col("amount"),
pl.col("amount").rank(method="min", descending=True).alias("amount_rank"),
).sort(pl.col("amount_rank")).head(10)

P-020: レシート明細データ(df_receipt)に対し、1 件あたりの売上金額(amount)が高い順にランクを付与し、先頭から 10 件表示せよ。項目は顧客 ID(customer_id)、売上金額(amount)、付与したランクを表示させること。なお、売上金額(amount)が等しい場合でも別順位を付与すること

df_receipt.select(
pl.col("customer_id"),
pl.col("amount"),
pl.col("amount").rank(method="ordinal", descending=True).alias("amount_rank"),
).sort(pl.col("amount_rank")).head(10)

P-021: レシート明細データ(df_receipt)に対し、件数をカウントせよ

df_receipt.shape[0]

P-022: レシート明細データ(df_receipt)の顧客 ID(customer_id)に対し、ユニーク件数をカウントせよ

df_receipt.select(pl.col("customer_id")).n_unique()

P-023: レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)と売上数量(quantity)を合計せよ

df_receipt.select(
pl.col("store_cd"),
pl.col("amount"),
pl.col("quantity"),
).group_by(pl.col("store_cd")).sum()

P-024: レシート明細データ(df_receipt)に対し、顧客 ID(customer_id)ごとに最も新しい売上年月日(sales_ymd)を求め、10 件表示せよ

df_receipt.select(
pl.col("customer_id"),
pl.col("sales_ymd"),
).group_by(pl.col("customer_id")).max()

P-025: レシート明細データ(df_receipt)に対し、顧客 ID(customer_id)ごとに最も古い売上年月日(sales_ymd)を求め、10 件表示せよ

df_receipt.select(
pl.col("customer_id"),
pl.col("sales_ymd"),
).group_by(pl.col("customer_id")).min()

P-026: レシート明細データ(df_receipt)に対し、顧客 ID(customer_id)ごとに最も新しい売上年月日(sales_ymd)と古い売上年月日を求め、両者が異なるデータを 10 件表示せよ

df_receipt.select(
pl.col("customer_id"),
pl.col("sales_ymd"),
).group_by(pl.col("customer_id")).agg(
pl.col("sales_ymd").max().alias("max_sales_ymd"),
pl.col("sales_ymd").min().alias("min_sales_ymd"),
).filter(pl.col("max_sales_ymd") != pl.col("min_sales_ymd")).head(10)

P-027: レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)の平均を計算し、降順で TOP5 を表示せよ

df_receipt.select(
pl.col("store_cd"),
pl.col("amount"),
).group_by(pl.col("store_cd")).mean().sort("amount", descending=True)

P-028: レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)の中央値を計算し、降順で TOP5 を表示せよ

df_receipt.select(
pl.col("store_cd"),
pl.col("amount"),
).group_by(pl.col("store_cd")).median().sort("amount", descending=True).head(5)

P-029: レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに商品コード(product_cd)の最頻値を求め、10 件表示させよ

df_receipt.select(
pl.col("store_cd"),
pl.col("product_cd"),
).group_by(pl.col("store_cd")).agg(
pl.col("product_cd").mode().first().alias("mode_product_cd")
).head(10)

P-030: レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)の分散を計算し、降順で 5 件表示せよ

df_receipt.select(
pl.col("store_cd"),
pl.col("amount"),
).group_by(pl.col("store_cd")).agg(
pl.col("amount").var(ddof=0).alias("amount_var")
).sort("amount_var", descending=True).head(5)

P-031: レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)の標準偏差を計算し、降順で 5 件表示せよ

TIPS:

Pandas と Numpy で ddof のデフォルト値が異なることに注意しましょう

Pandas:
DataFrame.std(self, axis=None, skipna=None, level=None, ddof=1, numeric_only=None, **kwargs)
Numpy:
numpy.std(a, axis=None, dtype=None, out=None, ddof=0, keepdims=)
df_receipt.select(
pl.col("store_cd"),
pl.col("amount"),
).group_by(pl.col("store_cd")).agg(
pl.col("amount").std(ddof=0).alias("amount_std")
).sort("amount_std", descending=True).head(5)

P-032: レシート明細データ(df_receipt)の売上金額(amount)について、25%刻みでパーセンタイル値を求めよ

df_receipt.select(
pl.col("amount").quantile(p).alias(f"q{p}") for p in [0.25, 0.5, 0.75, 1]
).head(1)

P-033: レシート明細データ(df_receipt)に対し、店舗コード(store_cd)ごとに売上金額(amount)の平均を計算し、330 以上のものを抽出せよ

df_receipt.select(
pl.col("store_cd"),
pl.col("amount"),
).group_by(pl.col("store_cd")).agg(
pl.col("amount").mean().alias("amount_mean"),
).filter(pl.col("amount_mean") >= 330)

P-034: レシート明細データ(df_receipt)に対し、顧客 ID(customer_id)ごとに売上金額(amount)を合計して全顧客の平均を求めよ。ただし、顧客 ID が"Z"から始まるものは非会員を表すため、除外して計算すること

df_receipt.select(
pl.col("customer_id"),
pl.col("amount"),
).filter(pl.col("customer_id").str.starts_with("Z").not_()).group_by(
pl.col("customer_id")
).agg(
pl.col("amount").sum().alias("amount_mean"),
).mean()

P-035: レシート明細データ(df_receipt)に対し、顧客 ID(customer_id)ごとに売上金額(amount)を合計して全顧客の平均を求め、平均以上に買い物をしている顧客を抽出し、10 件表示せよ。ただし、顧客 ID が"Z"から始まるものは非会員を表すため、除外して計算すること

df_receipt.select(
pl.col("customer_id"),
pl.col("amount"),
).filter(pl.col("customer_id").str.starts_with("Z").not_()).group_by(
pl.col("customer_id")
).agg(
pl.col("amount").sum().alias("amount_sum"),
).filter(pl.col("amount_sum") >= pl.col("amount_sum").mean()).head(10)

P-036: レシート明細データ(df_receipt)と店舗データ(df_store)を内部結合し、レシート明細データの全項目と店舗データの店舗名(store_name)を 10 件表示せよ

df_receipt.join(
df_store.select(pl.col("store_cd"), pl.col("store_name")),
on="store_cd",
how="inner",
).head(10)

P-037: 商品データ(df_product)とカテゴリデータ(df_category)を内部結合し、商品データの全項目とカテゴリデータのカテゴリ小区分名(category_small_name)を 10 件表示せよ

df_product.join(
df_category.select(pl.col("category_small_cd"), pl.col("category_major_name")),
on="category_small_cd",
how="inner",
).head(10)

P-038: 顧客データ(df_customer)とレシート明細データ(df_receipt)から、顧客ごとの売上金額合計を求め、10 件表示せよ。ただし、売上実績がない顧客については売上金額を 0 として表示させること。また、顧客は性別コード(gender_cd)が女性(1)であるものを対象とし、非会員(顧客 ID が"Z"から始まるもの)は除外すること

df_customer.filter(
pl.col("gender_cd").eq(1) & pl.col("customer_id").str.starts_with("Z").not_()
).join(
df_receipt,
on="customer_id",
how="left",
).group_by(pl.col("customer_id")).agg(
pl.col("amount").sum().alias("amount_sum"),
).head(10)

P-039: レシート明細データ(df_receipt)から、売上日数の多い顧客の上位 20 件を抽出したデータと、売上金額合計の多い顧客の上位 20 件を抽出したデータをそれぞれ作成し、さらにその 2 つを完全外部結合せよ。ただし、非会員(顧客 ID が"Z"から始まるもの)は除外すること


P-040: 全ての店舗と全ての商品を組み合わせたデータを作成したい。店舗データ(df_store)と商品データ(df_product)を直積し、件数を計算せよ

df_store.select("store_cd").with_columns(pl.lit(1).alias("one")).join(
df_product.select("category_major_cd").with_columns(pl.lit(1).alias("one")),
on="one",
how="full",
).shape[0]

P-041: レシート明細データ(df_receipt)の売上金額(amount)を日付(sales_ymd)ごとに集計し、前回売上があった日からの売上金額増減を計算せよ。そして結果を 10 件表示せよ

df_receipt.select(pl.col("sales_ymd"), pl.col("amount")).group_by(
pl.col("sales_ymd")
).sum().sort("sales_ymd").with_columns(
pl.col("sales_ymd").shift(1).alias("prev_sales_ymd"),
pl.col("amount").shift(1).alias("prev_amount"),
).with_columns((pl.col("amount") - pl.col("prev_amount")).alias("diff_amount")).head(10)

P-042: レシート明細データ(df_receipt)の売上金額(amount)を日付(sales_ymd)ごとに集計し、各日付のデータに対し、前回、前々回、3 回前に売上があった日のデータを結合せよ。そして結果を 10 件表示せよ

df_receipt.select(pl.col("sales_ymd"), pl.col("amount")).group_by(
pl.col("sales_ymd")
).sum().sort("sales_ymd").with_columns(
pl.col("sales_ymd").shift(1).alias("prev_sales_ymd"),
pl.col("amount").shift(1).alias("prev_amount"),
pl.col("sales_ymd").shift(2).alias("prev_sales_ymd2"),
pl.col("amount").shift(2).alias("prev_amount2"),
pl.col("sales_ymd").shift(3).alias("prev_sales_ymd3"),
pl.col("amount").shift(3).alias("prev_amount3"),
)

P-043: レシート明細データ(df_receipt)と顧客データ(df_customer)を結合し、性別コード(gender_cd)と年代(age から計算)ごとに売上金額(amount)を合計した売上サマリデータを作成せよ。性別コードは 0 が男性、1 が女性、9 が不明を表すものとする

ただし、項目構成は年代、女性の売上金額、男性の売上金額、性別不明の売上金額の 4 項目とすること(縦に年代、横に性別のクロス集計)。また、年代は 10 歳ごとの階級とすること。

df_sales_summary = (
df_receipt.join(df_customer, on="customer_id", how="inner")
.select(
pl.col("gender_cd"),
(pl.col("age") // 10 * 10).alias("age"),
pl.col("amount"),
)
.group_by(pl.col("gender_cd"), pl.col("age"))
.agg(
pl.col("amount").sum().alias("amount_mean"),
)
.sort(by="age")
.pivot(on="gender_cd", index="age", values="amount_mean")
.rename({"0": "Male", "1": "Female", "9": "Unknown"})
)
df_sales_summary

P-044: 043 で作成した売上サマリデータ(df_sales_summary)は性別の売上を横持ちさせたものであった。このデータから性別を縦持ちさせ、年代、性別コード、売上金額の 3 項目に変換せよ。ただし、性別コードは男性を"00"、女性を"01"、不明を"99"とする

df_sales_summary.rename({"Male": "00", "Female": "01", "Unknown": "99"}).unpivot(
index=["age"], value_name="amount"
).sort(by="age")

P-045: 顧客データ(df_customer)の生年月日(birth_day)は日付型でデータを保有している。これを YYYYMMDD 形式の文字列に変換し、顧客 ID(customer_id)とともに 10 件表示せよ

df_customer = df_customer.with_columns(
pl.col("birth_day").cast(pl.Date).alias("birth_day")
)
df_customer.select(
pl.col("customer_id"),
pl.col("birth_day").dt.strftime("%Y%m%d").alias("birth_day"),
).head(10)

P-046: 顧客データ(df_customer)の申し込み日(application_date)は YYYYMMDD 形式の文字列型でデータを保有している。これを日付型に変換し、顧客 ID(customer_id)とともに 10 件表示せよ

df_customer.select(
pl.col("customer_id"),
pl.col("birth_day").cast(pl.Date).alias("birth_day"),
)

P-047: レシート明細データ(df_receipt)の売上日(sales_ymd)は YYYYMMDD 形式の数値型でデータを保有している。これを日付型に変換し、レシート番号(receipt_no)、レシートサブ番号(receipt_sub_no)とともに 10 件表示せよ

df_receipt.select(
pl.col("sales_ymd").cast(pl.String).str.to_date("%Y%m%d").alias("sales_date"),
pl.col("receipt_no"),
pl.col("receipt_sub_no"),
)

P-048: レシート明細データ(df_receipt)の売上エポック秒(sales_epoch)は数値型の UNIX 秒でデータを保有している。これを日付型に変換し、レシート番号(receipt_no)、レシートサブ番号(receipt_sub_no)とともに 10 件表示せよ

df_receipt.select(
pl.from_epoch(pl.col("sales_epoch")).cast(pl.Date).alias("sales_date"),
pl.col("receipt_no"),
pl.col("receipt_sub_no"),
).head(10)

P-049: レシート明細データ(df_receipt)の売上エポック秒(sales_epoch)を日付型に変換し、「年」だけ取り出してレシート番号(receipt_no)、レシートサブ番号(receipt_sub_no)とともに 10 件表示せよ

df_receipt.select(
pl.from_epoch(pl.col("sales_epoch")).cast(pl.Date).alias("sales_date").dt.year(),
pl.col("receipt_no"),
pl.col("receipt_sub_no"),
).head(10)

P-050: レシート明細データ(df_receipt)の売上エポック秒(sales_epoch)を日付型に変換し、「月」だけ取り出してレシート番号(receipt_no)、レシートサブ番号(receipt_sub_no)とともに 10 件表示せよ。なお、「月」は 0 埋め 2 桁で取り出すこと

df_receipt.select(
pl.from_epoch(pl.col("sales_epoch"))
.cast(pl.Date)
.alias("sales_date")
.dt.month()
.cast(pl.String)
.str.zfill(2),
pl.col("receipt_no"),
pl.col("receipt_sub_no"),
).head(10)

P-051: レシート明細データ(df_receipt)の売上エポック秒を日付型に変換し、「日」だけ取り出してレシート番号(receipt_no)、レシートサブ番号(receipt_sub_no)とともに 10 件表示せよ。なお、「日」は 0 埋め 2 桁で取り出すこと

df_receipt.select(
pl.from_epoch(pl.col("sales_epoch"))
.cast(pl.Date)
.alias("sales_date")
.dt.day()
.cast(pl.String)
.str.zfill(2),
pl.col("receipt_no"),
pl.col("receipt_sub_no"),
).head(10)

P-052: レシート明細データ(df_receipt)の売上金額(amount)を顧客 ID(customer_id)ごとに合計の上、売上金額合計に対して 2,000 円以下を 0、2,000 円より大きい金額を 1 に二値化し、顧客 ID、売上金額合計とともに 10 件表示せよ。ただし、顧客 ID が"Z"から始まるのものは非会員を表すため、除外して計算すること

df_receipt.group_by(pl.col("customer_id")).sum().select(
pl.col("customer_id"),
pl.col("amount").alias("sum_amount"),
pl.when(pl.col("amount") >= 2000).then(1).otherwise(0).alias("is_high_amount"),
).head(10)

P-053: 顧客データ(df_customer)の郵便番号(postal_cd)に対し、東京(先頭 3 桁が 100〜209 のもの)を 1、それ以外のものを 0 に二値化せよ。さらにレシート明細データ(df_receipt)と結合し、全期間において売上実績のある顧客数を、作成した二値ごとにカウントせよ

df_customer.select(
pl.col("customer_id"),
pl.col("postal_cd").str.slice(0, 3),
pl.when(
(pl.col("postal_cd").str.slice(0, 3) >= "100")
& (pl.col("postal_cd").str.slice(0, 3) <= "209")
)
.then(1)
.otherwise(0)
.alias("is_tokyo"),
).join(
df_receipt.select(pl.col("customer_id")),
on="customer_id",
how="inner",
).group_by(
pl.col("is_tokyo"),
).agg(
pl.col("customer_id").n_unique().alias("n_customers"),
).sort(by="is_tokyo")

P-054: 顧客データ(df_customer)の住所(address)は、埼玉県、千葉県、東京都、神奈川県のいずれかとなっている。都道府県毎にコード値を作成し、顧客 ID、住所とともに 10 件表示せよ。値は埼玉県を 11、千葉県を 12、東京都を 13、神奈川県を 14 とすること

df_customer.select(
pl.col("customer_id"),
pl.col("address"),
pl.col("address")
.str.slice(0, 3)
.replace(
{
"埼玉県": 11,
"千葉県": 12,
"東京都": 13,
"神奈川": 14,
}
)
.alias("pref_cd"),
)

P-055: レシート明細(df_receipt)データの売上金額(amount)を顧客 ID(customer_id)ごとに合計し、その合計金額の四分位点を求めよ。その上で、顧客ごとの売上金額合計に対して以下の基準でカテゴリ値を作成し、顧客 ID、売上金額合計とともに 10 件表示せよ。カテゴリ値は順に 1〜4 とする

  • 最小値以上第 1 四分位未満 ・・・ 1 を付与
  • 第 1 四分位以上第 2 四分位未満 ・・・ 2 を付与
  • 第 2 四分位以上第 3 四分位未満 ・・・ 3 を付与
  • 第 3 四分位以上 ・・・ 4 を付与
df_receipt.select(
pl.col("customer_id"),
pl.col("amount"),
).group_by(pl.col("customer_id")).agg(
pl.col("amount").sum().alias("sum_amount"),
).with_columns(
pl.when(pl.col("sum_amount") < pl.col("sum_amount").quantile(0.25))
.then(1)
.when(pl.col("sum_amount") < pl.col("sum_amount").quantile(0.5))
.then(2)
.when(pl.col("sum_amount") < pl.col("sum_amount").quantile(0.75))
.then(3)
.otherwise(4)
.alias("amount_quantile")
)

P-056: 顧客データ(df_customer)の年齢(age)をもとに 10 歳刻みで年代を算出し、顧客 ID(customer_id)、生年月日(birth_day)とともに 10 件表示せよ。ただし、60 歳以上は全て 60 歳代とすること。年代を表すカテゴリ名は任意とする

df_customer.select(
pl.col("customer_id"),
pl.col("birth_day"),
pl.col("age"),
).with_columns(
pl.when(pl.col("age") >= 60)
.then(60)
.otherwise(pl.col("age") // 10 * 10)
.alias("age")
).head(10)

P-057: 056 の抽出結果と性別コード(gender_cd)により、新たに性別 × 年代の組み合わせを表すカテゴリデータを作成し、10 件表示せよ。組み合わせを表すカテゴリの値は任意とする

df_customer.select(
pl.col("customer_id"),
pl.col("birth_day"),
pl.col("gender"),
pl.col("age"),
).with_columns(
pl.when(pl.col("age") >= 60)
.then(60)
.otherwise(pl.col("age") // 10 * 10)
.alias("age_group")
).with_columns(
pl.concat_str(
pl.col("gender"), pl.col("age_group").cast(pl.String).str.zfill(2)
).alias("gender_age_group")
)

P-058: 顧客データ(df_customer)の性別コード(gender_cd)をダミー変数化し、顧客 ID(customer_id)とともに 10 件表示せよ

df_customer.select(
pl.col("customer_id"),
pl.col("gender_cd"),
).to_dummies(["gender_cd"]).head(10)

P-059: レシート明細データ(df_receipt)の売上金額(amount)を顧客 ID(customer_id)ごとに合計し、売上金額合計を平均 0、標準偏差 1 に標準化して顧客 ID、売上金額合計とともに 10 件表示せよ。標準化に使用する標準偏差は、分散の平方根、もしくは不偏分散の平方根のどちらでも良いものとする。ただし、顧客 ID が"Z"から始まるのものは非会員を表すため、除外して計算すること

TIPS:

  • query()の引数 engine で'python'か'numexpr'かを選択でき、デフォルトはインストールされていれば numexpr が、無ければ python が使われます。さらに、文字列メソッドは engine='python'でないと query()内で使えません。
df_receipt.filter(pl.col("customer_id").str.starts_with("Z").not_()).group_by(
pl.col("customer_id")
).agg(
pl.col("amount").sum().alias("sum_amount"),
).with_columns(
(
(pl.col("sum_amount") - pl.col("sum_amount").mean())
/ pl.col("sum_amount").std(ddof=0)
).alias("z_score")
).sort(pl.col("customer_id"))

P-060: レシート明細データ(df_receipt)の売上金額(amount)を顧客 ID(customer_id)ごとに合計し、売上金額合計を最小値 0、最大値 1 に正規化して顧客 ID、売上金額合計とともに 10 件表示せよ。ただし、顧客 ID が"Z"から始まるのものは非会員を表すため、除外して計算すること

non_z_customer = pl.col("customer_id").str.starts_with("Z").not_()


df_receipt.filter(non_z_customer).group_by(pl.col("customer_id")).agg(
pl.col("amount").sum().alias("sum_amount"),
).with_columns(
(
(pl.col("sum_amount") - pl.col("sum_amount").min())
/ (pl.col("sum_amount").max() - pl.col("sum_amount").min())
).alias("z_score")
).sort(pl.col("customer_id")).head(10)

P-061: レシート明細データ(df_receipt)の売上金額(amount)を顧客 ID(customer_id)ごとに合計し、売上金額合計を常用対数化(底 10)して顧客 ID、売上金額合計とともに 10 件表示せよ。ただし、顧客 ID が"Z"から始まるのものは非会員を表すため、除外して計算すること

df_receipt.filter(non_z_customer).group_by(pl.col("customer_id")).agg(
pl.col("amount").sum().alias("sum_amount"),
).with_columns((pl.col("sum_amount").log10()).alias("z_score")).sort(
pl.col("customer_id")
).head(10)

P-062: レシート明細データ(df_receipt)の売上金額(amount)を顧客 ID(customer_id)ごとに合計し、売上金額合計を自然対数化(底 e)して顧客 ID、売上金額合計とともに 10 件表示せよ。ただし、顧客 ID が"Z"から始まるのものは非会員を表すため、除外して計算すること

df_receipt.filter(non_z_customer).group_by(pl.col("customer_id")).agg(
pl.col("amount").sum().alias("sum_amount"),
).with_columns((pl.col("sum_amount").log()).alias("z_score")).sort(
pl.col("customer_id")
).head(10)

P-063: 商品データ(df_product)の単価(unit_price)と原価(unit_cost)から各商品の利益額を算出し、結果を 10 件表示せよ

df_product.with_columns(
(pl.col("unit_price") - pl.col("unit_cost")).alias("unit_profit")
).head(10)

P-064: 商品データ(df_product)の単価(unit_price)と原価(unit_cost)から、各商品の利益率の全体平均を算出せよ。ただし、単価と原価には欠損が生じていることに注意せよ

df_product.select(
pl.col("unit_price"),
pl.col("unit_cost"),
).with_columns(
((pl.col("unit_price") - pl.col("unit_cost")) / pl.col("unit_price")).alias(
"unit_profit"
)
).mean().item(0, -1)

P-065: 商品データ(df_product)の各商品について、利益率が 30%となる新たな単価を求めよ。ただし、1 円未満は切り捨てること。そして結果を 10 件表示させ、利益率がおよそ 30%付近であることを確認せよ。ただし、単価(unit_price)と原価(unit_cost)には欠損が生じていることに注意せよ

formula = (pl.col("unit_cost") / 0.7).alias("new_price").floor()

df_product.select(pl.col("unit_cost")).with_columns(
formula.cast(pl.UInt32),
).with_columns(
((pl.col("new_price") - pl.col("unit_cost")) / pl.col("new_price")).alias(
"new_profit"
),
).head()

P-066: 商品データ(df_product)の各商品について、利益率が 30%となる新たな単価を求めよ。今回は、1 円未満を丸めること(四捨五入または偶数への丸めで良い)。そして結果を 10 件表示させ、利益率がおよそ 30%付近であることを確認せよ。ただし、単価(unit_price)と原価(unit_cost)には欠損が生じていることに注意せよ

formula = (pl.col("unit_cost") / 0.7).alias("new_price").round()

df_product.select(pl.col("unit_cost")).with_columns(
formula.cast(pl.UInt32),
).with_columns(
((pl.col("new_price") - pl.col("unit_cost")) / pl.col("new_price")).alias(
"new_profit"
),
).head()

P-067: 商品データ(df_product)の各商品について、利益率が 30%となる新たな単価を求めよ。今回は、1 円未満を切り上げること。そして結果を 10 件表示させ、利益率がおよそ 30%付近であることを確認せよ。ただし、単価(unit_price)と原価(unit_cost)には欠損が生じていることに注意せよ

formula = (pl.col("unit_cost") / 0.7).alias("new_price").ceil()

df_product.select(pl.col("unit_cost")).with_columns(
formula.cast(pl.UInt32),
).with_columns(
((pl.col("new_price") - pl.col("unit_cost")) / pl.col("new_price")).alias(
"new_profit"
),
).head()

P-068: 商品データ(df_product)の各商品について、消費税率 10%の税込み金額を求めよ。1 円未満の端数は切り捨てとし、結果を 10 件表示せよ。ただし、単価(unit_price)には欠損が生じていることに注意せよ

df_product.select(pl.col("unit_price")).with_columns(
pl.col("unit_price").mul(1.1).floor().alias("with_tax")
)

P-069: レシート明細データ(df_receipt)と商品データ(df_product)を結合し、顧客毎に全商品の売上金額合計と、カテゴリ大区分コード(category_major_cd)が"07"(瓶詰缶詰)の売上金額合計を計算の上、両者の比率を求めよ。抽出対象はカテゴリ大区分コード"07"(瓶詰缶詰)の売上実績がある顧客のみとし、結果を 10 件表示せよ

if "\ufeffproduct_cd" in df_product.columns:  # BOM
df_product = df_product.rename({"\ufeffproduct_cd": "product_cd"})

df_receipt.join(df_product, on="product_cd", how="inner").group_by(
pl.col("customer_id")
).agg(
pl.col("amount").sum().alias("sum_amount"),
pl.when(pl.col("category_major_cd") == 7)
.then(pl.col("amount"))
.otherwise(0)
.sum()
.alias("sum_amount_cat7"),
).head()

P-070: レシート明細データ(df_receipt)の売上日(sales_ymd)に対し、顧客データ(df_customer)の会員申込日(application_date)からの経過日数を計算し、顧客 ID(customer_id)、売上日、会員申込日とともに 10 件表示せよ(sales_ymd は数値、application_date は文字列でデータを保持している点に注意)

df_receipt.join(df_customer, on="customer_id", how="inner").with_columns(
(
pl.col("sales_ymd").cast(pl.String).str.strptime(pl.Date, "%Y%m%d")
- pl.col("application_date").cast(pl.String).str.strptime(pl.Date, "%Y%m%d")
).alias("days_diff")
).select(
pl.col("customer_id"),
pl.col("sales_ymd"),
pl.col("application_date"),
pl.col("days_diff"),
)

P-071: レシート明細データ(df_receipt)の売上日(sales_ymd)に対し、顧客データ(df_customer)の会員申込日(application_date)からの経過月数を計算し、顧客 ID(customer_id)、売上日、会員申込日とともに 10 件表示せよ(sales_ymd は数値、application_date は文字列でデータを保持している点に注意)。1 ヶ月未満は切り捨てること

df_receipt.select(pl.col("customer_id"), pl.col("sales_ymd")).unique().join(
df_customer.select(pl.col("customer_id"), pl.col("application_date")),
on="customer_id",
how="inner",
).with_columns(
pl.col("sales_ymd").cast(pl.String).str.strptime(pl.Date, "%Y%m%d"),
pl.col("application_date").cast(pl.String).str.strptime(pl.Date, "%Y%m%d"),
).with_columns(
(pl.col("sales_ymd").dt.year().sub(pl.col("application_date").dt.year()))
.mul(12)
.add(pl.col("sales_ymd").dt.month().sub(pl.col("application_date").dt.month()))
.alias("months_diff")
)

P-072: レシート明細データ(df_receipt)の売上日(df_customer)に対し、顧客データ(df_customer)の会員申込日(application_date)からの経過年数を計算し、顧客 ID(customer_id)、売上日、会員申込日とともに 10 件表示せよ(sales_ymd は数値、application_date は文字列でデータを保持している点に注意)。1 年未満は切り捨てること

df_receipt.select(pl.col("customer_id"), pl.col("sales_ymd")).unique().join(
df_customer.select(pl.col("customer_id"), pl.col("application_date")),
on="customer_id",
how="inner",
).with_columns(
pl.col("sales_ymd").cast(pl.String).str.strptime(pl.Date, "%Y%m%d"),
pl.col("application_date").cast(pl.String).str.strptime(pl.Date, "%Y%m%d"),
).with_columns(
(pl.col("sales_ymd").dt.year().sub(pl.col("application_date").dt.year()))
.mul(12)
.alias("years_diff")
)

P-073: レシート明細データ(df_receipt)の売上日(sales_ymd)に対し、顧客データ(df_customer)の会員申込日(application_date)からのエポック秒による経過時間を計算し、顧客 ID(customer_id)、売上日、会員申込日とともに 10 件表示せよ(なお、sales_ymd は数値、application_date は文字列でデータを保持している点に注意)。なお、時間情報は保有していないため各日付は 0 時 0 分 0 秒を表すものとする

df_receipt.select(pl.col("customer_id"), pl.col("sales_ymd")).unique().join(
df_customer.select(pl.col("customer_id"), pl.col("application_date")),
on="customer_id",
how="inner",
).with_columns(
pl.col("sales_ymd").cast(pl.String).str.strptime(pl.Datetime, "%Y%m%d"),
pl.col("application_date").cast(pl.String).str.strptime(pl.Datetime, "%Y%m%d"),
).with_columns(
(pl.col("sales_ymd").sub(pl.col("application_date")))
.dt.total_seconds()
.alias("epoch_diff")
)

P-074: レシート明細データ(df_receipt)の売上日(sales_ymd)に対し、当該週の月曜日からの経過日数を計算し、売上日、直前の月曜日付とともに 10 件表示せよ(sales_ymd は数値でデータを保持している点に注意)

df_receipt.with_columns(
pl.col("sales_ymd").cast(pl.String).str.strptime(pl.Date, "%Y%m%d"),
).with_columns(
(pl.col("sales_ymd").dt.weekday() - 1).alias("num_days_from_mon")
).select(
pl.col("sales_ymd"),
pl.col("num_days_from_mon"),
(pl.col("sales_ymd") - pl.duration(days=pl.col("num_days_from_mon"))).alias(
"monday_date"
),
)

P-075: 顧客データ(df_customer)からランダムに 1%のデータを抽出し、先頭から 10 件表示せよ

df_customer.sample(fraction=0.01).head(10)

P-076: 顧客データ(df_customer)から性別コード(gender_cd)の割合に基づきランダムに 10%のデータを層化抽出し、性別コードごとに件数を集計せよ

df_customer.group_by(pl.col("gender_cd")).len()
train_test_split(df_customer, test_size=0.1, stratify=df_customer["gender_cd"])[
1
].group_by(pl.col("gender_cd")).len()

P-077: レシート明細データ(df_receipt)の売上金額を顧客単位に合計し、合計した売上金額の外れ値を抽出せよ。なお、外れ値は売上金額合計を対数化したうえで平均と標準偏差を計算し、その平均から 3σ を超えて離れたものとする(自然対数と常用対数のどちらでも可)。結果は 10 件表示せよ

df_receipt.group_by(
pl.col("customer_id"),
).agg(pl.col("amount").sum().log().alias("log_sum_amount")).filter(
pl.col("log_sum_amount").gt(
pl.col("log_sum_amount").mean() + 3 * pl.col("log_sum_amount").std()
)
)

P-078: レシート明細データ(df_receipt)の売上金額(amount)を顧客単位に合計し、合計した売上金額の外れ値を抽出せよ。ただし、顧客 ID が"Z"から始まるのものは非会員を表すため、除外して計算すること。なお、ここでは外れ値を第 1 四分位と第 3 四分位の差である IQR を用いて、「第 1 四分位数-1.5×IQR」を下回るもの、または「第 3 四分位数+1.5×IQR」を超えるものとする。結果は 10 件表示せよ

def exp(x, y):
return pl.col(x).quantile(y)


def iqr(x: str):
return exp(x, 0.75) - exp(x, 0.25)


df_receipt.filter(pl.col("customer_id").str.starts_with("Z").not_()).group_by(
pl.col("customer_id"),
).agg(pl.col("amount").sum().alias("sum_amount")).filter(
pl.col("sum_amount").lt(exp("sum_amount", 0.25) - 1.5 * iqr("sum_amount"))
| pl.col("sum_amount").gt(exp("sum_amount", 0.75) + 1.5 * iqr("sum_amount"))
)

P-079: 商品データ(df_product)の各項目に対し、欠損数を確認せよ

df_product.null_count()

P-080: 商品データ(df_product)のいずれかの項目に欠損が発生しているレコードを全て削除した新たな商品データを作成せよ。なお、削除前後の件数を表示させ、079 で確認した件数だけ減少していることも確認すること

print(df_product.shape[0])
print(df_product.drop_nulls().shape[0])
df_product.drop_nulls()

P-081: 単価(unit_price)と原価(unit_cost)の欠損値について、それぞれの平均値で補完した新たな商品データを作成せよ。なお、平均値については 1 円未満を丸めること(四捨五入または偶数への丸めで良い)。補完実施後、各項目について欠損が生じていないことも確認すること

df_product.filter(pl.any_horizontal(pl.all().is_null()))
df_product.with_columns(
pl.exclude("product_cd").fill_null(pl.all().mean().floor())
).null_count()

P-082: 単価(unit_price)と原価(unit_cost)の欠損値について、それぞれの中央値で補完した新たな商品データを作成せよ。なお、中央値については 1 円未満を丸めること(四捨五入または偶数への丸めで良い)。補完実施後、各項目について欠損が生じていないことも確認すること

df_product.with_columns(
pl.exclude("product_cd").fill_null(pl.all().median().round())
).null_count()

P-083: 単価(unit_price)と原価(unit_cost)の欠損値について、各商品のカテゴリ小区分コード(category_small_cd)ごとに算出した中央値で補完した新たな商品データを作成せよ。なお、中央値については 1 円未満を丸めること(四捨五入または偶数への丸めで良い)。補完実施後、各項目について欠損が生じていないことも確認すること

df_product.join(
df_product.group_by(pl.col("category_small_cd")).agg(
pl.col("unit_price").median().round().alias("median_price"),
pl.col("unit_cost").median().round().alias("median_cost"),
),
on="category_small_cd",
how="inner",
).with_columns(
pl.col("unit_price").fill_null(pl.col("median_price")),
pl.col("unit_cost").fill_null(pl.col("median_cost")),
).null_count()

P-084: 顧客データ(df_customer)の全顧客に対して全期間の売上金額に占める 2019 年売上金額の割合を計算し、新たなデータを作成せよ。ただし、売上実績がない場合は 0 として扱うこと。そして計算した割合が 0 超のものを抽出し、結果を 10 件表示せよ。また、作成したデータに欠損が存在しないことを確認せよ

ans = (
df_receipt.with_columns(
pl.col("amount").cast(pl.Int32),
)
.join(df_customer, on="customer_id", how="right")
.group_by(pl.col("customer_id"))
.agg(
pl.col("amount")
.filter(pl.col("sales_ymd").is_between(pl.lit(20190101), pl.lit(20191231)))
.sum()
.alias("sum_amount_2019"),
pl.col("amount").sum().alias("sum_amount"),
)
.with_columns(
(pl.col("sum_amount_2019") / pl.col("sum_amount")).alias("amount_ratio")
)
.fill_nan(0)
.filter(pl.col("amount_ratio").gt(0))
)
print(ans.head(10))
print(ans.null_count())

P-085: 顧客データ(df_customer)の全顧客に対し、郵便番号(postal_cd)を用いてジオコードデータ(df_geocode)を紐付け、新たな顧客データを作成せよ。ただし、1 つの郵便番号(postal_cd)に複数の経度(longitude)、緯度(latitude)情報が紐づく場合は、経度(longitude)、緯度(latitude)の平均値を算出して使用すること。また、作成結果を確認するために結果を 10 件表示せよ

# BOM
if "\ufeffpostal_cd" in df_geocode.columns:
df_geocode = df_geocode.rename({"\ufeffpostal_cd": "postal_cd"})


df_customer.join(
df_geocode.group_by("postal_cd").agg(
pl.col("latitude").cast(pl.Float64).mean().alias("mean_latitude"),
pl.col("longitude").cast(pl.Float64).mean().alias("mean_longitude"),
),
on="postal_cd",
how="left",
).head(10)

P-086: 085 で作成した緯度経度つき顧客データに対し、会員申込店舗コード(application_store_cd)をキーに店舗データ(df_store)と結合せよ。そして申込み店舗の緯度(latitude)・経度情報(longitude)と顧客住所(address)の緯度・経度を用いて申込み店舗と顧客住所の距離(単位:km)を求め、顧客 ID(customer_id)、顧客住所(address)、店舗住所(address)とともに表示せよ。計算式は以下の簡易式で良いものとするが、その他精度の高い方式を利用したライブラリを利用してもかまわない。結果は 10 件表示せよ

緯度(ラジアン): ϕ経度(ラジアン): λ距離L=6371arccos(sinϕ1sinϕ2+cosϕ1cosϕ2cos(λ1λ2))\begin{align*} &\text{緯度(ラジアン): }\phi \\ &\text{経度(ラジアン): }\lambda \\ &\text{距離}L = 6371 \cdot \arccos(\sin \phi_1 \cdot \sin \phi_2 + \cos \phi_1 \cdot \cos \phi_2 \cdot \cos(\lambda_1 - \lambda_2)) \end{align*}
def distance_between(lat1: str, lon1: str, lat2: str, lon2: str):
def to_rad(x: pl.Expr) -> pl.Expr:
return pl.lit(math.pi / 180).mul(x)

return pl.lit(6371).mul(
(
to_rad(pl.col(lat1)).sin() * to_rad(pl.col(lat2)).sin()
+ to_rad(pl.col(lat1)).cos()
* to_rad(pl.col(lat2)).cos()
* (to_rad(pl.col(lon1)) - to_rad(pl.col(lon2))).cos()
).arccos()
)


df_customer.join(
df_geocode.group_by("postal_cd").agg(
pl.col("latitude").cast(pl.Float64).mean().alias("mean_latitude"),
pl.col("longitude").cast(pl.Float64).mean().alias("mean_longitude"),
),
on="postal_cd",
how="left",
).join(
df_store.with_columns(
pl.col("latitude").cast(pl.Float64),
pl.col("longitude").cast(pl.Float64),
),
left_on="application_store_cd",
right_on="store_cd",
how="inner",
).select(
pl.col("customer_id"),
pl.col("application_store_cd"),
pl.col("address"),
distance_between("latitude", "longitude", "mean_latitude", "mean_longitude").alias(
"distance"
),
).head(10)


P-087: 顧客データ(df_customer)では、異なる店舗での申込みなどにより同一顧客が複数登録されている。名前(customer_name)と郵便番号(postal_cd)が同じ顧客は同一顧客とみなして 1 顧客 1 レコードとなるように名寄せした名寄顧客データを作成し、顧客データの件数、名寄顧客データの件数、重複数を算出せよ。ただし、同一顧客に対しては売上金額合計が最も高いものを残し、売上金額合計が同一もしくは売上実績がない顧客については顧客 ID(customer_id)の番号が小さいものを残すこととする

df_customer.join(
df_receipt.with_columns(pl.col("amount").cast(pl.Int64))
.group_by(
pl.col("customer_id"),
)
.agg(
pl.col("amount").sum().alias("sum_amount"),
),
on="customer_id",
how="left",
).sort(["sum_amount", "customer_id"], descending=[True, False]).unique(
subset=["customer_name", "postal_cd"], keep="first"
)

P-088: 087 で作成したデータを元に、顧客データに統合名寄 ID を付与したデータを作成せよ。ただし、統合名寄 ID は以下の仕様で付与するものとする

  • 重複していない顧客:顧客 ID(customer_id)を設定
  • 重複している顧客:前設問で抽出したレコードの顧客 ID を設定

顧客 ID のユニーク件数と、統合名寄 ID のユニーク件数の差も確認すること。

ans = df_customer.join(
df_customer.join(
df_receipt.with_columns(pl.col("amount").cast(pl.Int64))
.group_by(
pl.col("customer_id"),
)
.agg(
pl.col("amount").sum().alias("sum_amount"),
),
on="customer_id",
how="left",
)
.sort(["sum_amount", "customer_id"], descending=[True, False])
.unique(subset=["customer_name", "postal_cd"], keep="first")
.select(["customer_name", "postal_cd", "customer_id"]),
on=["customer_name", "postal_cd"],
how="left",
).rename({"customer_id_right": "integrated_customer_id"})
print(ans)
print(
df_customer.unique("customer_id").shape[0],
ans.unique("integrated_customer_id").shape[0],
)

P-089: 売上実績がある顧客を、予測モデル構築のため学習用データとテスト用データに分割したい。それぞれ 8:2 の割合でランダムにデータを分割せよ

train, test = train_test_split(
df_customer.join(
df_receipt.group_by("customer_id")
.agg(pl.col("amount").cast(pl.Int64).sum().alias("sum_amount"))
.filter(pl.col("sum_amount").gt(0)),
on="customer_id",
how="inner",
),
test_size=0.2,
)


P-090: レシート明細データ(df_receipt)は 2017 年 1 月 1 日〜2019 年 10 月 31 日までのデータを有している。売上金額(amount)を月次で集計し、学習用に 12 ヶ月、テスト用に 6 ヶ月の時系列モデル構築用データを 3 セット作成せよ

ans = (
df_receipt.filter(
pl.col("sales_ymd").is_between(pl.lit(20170101), pl.lit(20191031))
)
.group_by(pl.col("sales_ymd") // 100)
.agg(pl.col("amount").sum())
.sort("sales_ymd", descending=False)
)

folds = []
tscv = TimeSeriesSplit(n_splits=3, max_train_size=12, test_size=6)
for fold in tscv.split(range(34)): # type: ignore
folds.append((ans[fold[0]], ans[fold[1]]))

print(folds[0][0], folds[0][1])

P-091: 顧客データ(df_customer)の各顧客に対し、売上実績がある顧客数と売上実績がない顧客数が 1:1 となるようにアンダーサンプリングで抽出せよ

ans = (
df_customer.join(
df_receipt.select("customer_id", "amount")
.group_by(
pl.col("customer_id"),
)
.agg(
pl.col("amount").sum().alias("sum_amount"),
)
.filter(
pl.col("sum_amount").gt(0),
),
on="customer_id",
how="left",
)
.with_columns(pl.col("sum_amount").is_null().cast(pl.UInt8).alias("has_sales"))
.to_pandas()
)

random_under_sampler = RandomUnderSampler(random_state=42)
sample = random_under_sampler.fit_resample(ans, ans["has_sales"])
len(sample[0]), sample[0].has_sales.sum()

P-092: 顧客データ(df_customer)の性別について、第三正規形へと正規化せよ

print(df_customer.select(pl.col("gender_cd"), pl.col("gender")).unique())
print(df_customer.drop("gender").head())

P-093: 商品データ(df_product)では各カテゴリのコード値だけを保有し、カテゴリ名は保有していない。カテゴリデータ(df_category)と組み合わせて非正規化し、カテゴリ名を保有した新たな商品データを作成せよ

if "\ufeffcategory_major_cd" in df_product.columns:
df_product = df_product.rename({"\ufeffcategory_major_cd": "category_major_cd"})

if "\ufeffcategory_major_cd" in df_category.columns:
df_category = df_category.rename({"\ufeffcategory_major_cd": "category_major_cd"})

df_product.join(
df_category,
on=["category_major_cd", "category_medium_cd", "category_small_cd"],
how="inner",
)

P-094: 093 で作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ

ファイル形式ヘッダ有無文字エンコーディング
CSV(カンマ区切り)有りUTF-8

ファイル出力先のパスは以下のようにすること

出力先
./data
from pathlib import Path


out_path = Path("./data/product_category.csv")
out_path.parent.mkdir(parents=True, exist_ok=True)

df_product.join(
df_category,
on=["category_major_cd", "category_medium_cd", "category_small_cd"],
how="inner",
).write_csv(out_path)

P-095: 093 で作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ

ファイル形式ヘッダ有無文字エンコーディング
CSV(カンマ区切り)有りCP932

ファイル出力先のパスは以下のようにすること。

出力先
./data
out_path = Path("./data/product_category_cp932.csv")
out_path.parent.mkdir(parents=True, exist_ok=True)

with open(out_path, "w", encoding="cp932") as f:
f.write(
df_product.join(
df_category,
on=["category_major_cd", "category_medium_cd", "category_small_cd"],
how="inner",
).write_csv()
)

P-096: 093 で作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ

ファイル形式ヘッダ有無文字エンコーディング
CSV(カンマ区切り)無しUTF-8

ファイル出力先のパスは以下のようにすること。

出力先
./data
out_path = Path("./data/product_category_no_header.csv")
out_path.parent.mkdir(parents=True, exist_ok=True)

df_product.join(
df_category,
on=["category_major_cd", "category_medium_cd", "category_small_cd"],
how="inner",
).write_csv(out_path, include_header=False)

P-097: 094 で作成した以下形式のファイルを読み込み、データを 3 件を表示させて正しく取り込まれていることを確認せよ

ファイル形式ヘッダ有無文字エンコーディング
CSV(カンマ区切り)有りUTF-8
pl.read_csv("./data/product_category.csv").head(3)

P-098: 096 で作成した以下形式のファイルを読み込み、データを 3 件を表示させて正しく取り込まれていることを確認せよ

ファイル形式ヘッダ有無文字エンコーディング
CSV(カンマ区切り)ヘッダ無しUTF-8
pl.read_csv("./data/product_category_no_header.csv", has_header=False).head(3)

P-099: 093 で作成したカテゴリ名付き商品データを以下の仕様でファイル出力せよ

ファイル形式ヘッダ有無文字エンコーディング
TSV(タブ区切り)有りUTF-8

ファイル出力先のパスは以下のようにすること

出力先
./data
out_path = Path("./data/product_category.tsv")
out_path.parent.mkdir(parents=True, exist_ok=True)

df_product.join(
df_category,
on=["category_major_cd", "category_medium_cd", "category_small_cd"],
how="inner",
).write_csv(out_path, separator="\t")

P-100: 099 で作成した以下形式のファイルを読み込み、データを 3 件を表示させて正しく取り込まれていることを確認せよ

ファイル形式ヘッダ有無文字エンコーディング
TSV(タブ区切り)有りUTF-8
pl.read_csv("./data/product_category.tsv", separator="\t").head(3)

これで100本終わりです。おつかれさまでした