การวิเคราะห์ข้อมูลเชิงสำรวจเกี่ยวกับยอดขายสุราในไอโอวาโดยใช้แพ็กเกจ DataFrames ของ BigQuery

การวิเคราะห์ข้อมูลเชิงสำรวจเกี่ยวกับยอดขายสุราในไอโอวาโดยใช้แพ็กเกจ DataFrames ของ BigQuery

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ ธ.ค. 4, 2024
account_circleเขียนโดย Tim Swena

1 ภาพรวม

ในชั้นเรียนนี้ คุณจะใช้ DataFrame ของ BigQuery จากโน้ตบุ๊ก Python ใน BigQuery Studio เพื่อล้างและวิเคราะห์ชุดข้อมูลสาธารณะเกี่ยวกับยอดขายเครื่องดื่มแอลกอฮอล์ในไอโอวา ใช้ความสามารถของ BigQuery ML และฟังก์ชันระยะไกลเพื่อค้นพบข้อมูลเชิงลึก

คุณจะต้องสร้างโน้ตบุ๊ก Python เพื่อเปรียบเทียบยอดขายในพื้นที่ทางภูมิศาสตร์ต่างๆ ซึ่งสามารถปรับให้ทำงานกับ Structured Data ใดก็ได้

วัตถุประสงค์

ในบทแนะนำนี้ คุณจะได้เรียนรู้วิธีทํางานต่อไปนี้

  • เปิดใช้งานและใช้โน้ตบุ๊ก Python ใน BigQuery Studio
  • เชื่อมต่อกับ BigQuery โดยใช้แพ็กเกจ BigQuery DataFrames
  • สร้างการถดถอยเชิงเส้นโดยใช้ BigQuery ML
  • ดำเนินการรวมและเข้าร่วมที่ซับซ้อนโดยใช้ไวยากรณ์ที่คล้ายกับ Pandas ซึ่งคุ้นเคย

2 ข้อกำหนด

  • เบราว์เซอร์ เช่น Chrome หรือ Firefox
  • โปรเจ็กต์ Google Cloud ที่เปิดใช้การเรียกเก็บเงิน

ก่อนเริ่มต้น

หากต้องการทําตามวิธีการในโค้ดแล็บนี้ คุณจะต้องมีโปรเจ็กต์ Google Cloud ที่เปิดใช้ BigQuery Studio และบัญชีการเรียกเก็บเงินที่เชื่อมต่อ

  1. ในคอนโซล Google Cloud ให้เลือกหรือสร้างโปรเจ็กต์ Google Cloud ในหน้าตัวเลือกโปรเจ็กต์
  2. ตรวจสอบว่าเปิดใช้การเรียกเก็บเงินสำหรับโปรเจ็กต์ Google Cloud แล้ว ดูวิธีตรวจสอบว่าเปิดใช้การเรียกเก็บเงินในโปรเจ็กต์หรือไม่
  3. ทําตามวิธีการเพื่อเปิดใช้ BigQuery Studio สําหรับการจัดการชิ้นงาน

เตรียม BigQuery Studio

สร้างสมุดบันทึกเปล่าและเชื่อมต่อกับรันไทม์

  1. ไปที่ BigQuery Studio ในคอนโซล Google Cloud
  2. คลิก ข้างปุ่ม +
  3. เลือกโน้ตบุ๊ก Python
  4. ปิดตัวเลือกเทมเพลต
  5. เลือก + โค้ดเพื่อสร้างเซลล์โค้ดใหม่
  6. ติดตั้งแพ็กเกจ BigQuery DataFrames เวอร์ชันล่าสุดจากเซลล์โค้ด โดยพิมพ์คำสั่งต่อไปนี้
    %pip install --upgrade bigframes --quiet
    คลิกปุ่มเรียกใช้เซลล์หรือกด Shift + Enter เพื่อเรียกใช้เซลล์โค้ด

3 อ่านชุดข้อมูลสาธารณะ

เริ่มต้นใช้งานแพ็กเกจ BigQuery DataFrame โดยเรียกใช้โค้ดต่อไปนี้ในเซลล์โค้ดใหม่

import bigframes.pandas as bpd

bpd.options.bigquery.ordering_mode = "partial"
bpd.options.display.repr_mode = "deferred"

หมายเหตุ: ในบทแนะนํานี้ เราจะใช้ "โหมดการจัดเรียงบางส่วน" เวอร์ชันทดลอง ซึ่งช่วยให้การค้นหามีประสิทธิภาพมากขึ้นเมื่อใช้ร่วมกับการกรองแบบแพนด้า ฟีเจอร์บางอย่างของ Pandas ที่กำหนดให้ต้องจัดเรียงหรือมีดัชนีอย่างเคร่งครัดอาจไม่ทำงาน

ตรวจสอบเวอร์ชันแพ็กเกจ bigframes ด้วย

bpd.__version__

บทแนะนำนี้ต้องใช้เวอร์ชัน 1.27.0 ขึ้นไป

ยอดขายปลีกสุราในไอโอวา

ชุดข้อมูลการขายปลีกเครื่องดื่มแอลกอฮอล์ในไอโอวามีอยู่ใน BigQuery ผ่านโปรแกรมชุดข้อมูลสาธารณะของ Google Cloud ชุดข้อมูลนี้ประกอบด้วยการซื้อสุราแบบค้าส่งทั้งหมดในรัฐไอโอวาจากผู้ค้าปลีกเพื่อขายให้แก่บุคคลธรรมดานับตั้งแต่วันที่ 1 มกราคม 2012 ข้อมูลรวบรวมโดยแผนกเครื่องดื่มแอลกอฮอล์ภายในกระทรวงพาณิชย์ของรัฐไอโอวา

ใน BigQuery ให้ค้นหา bigquery-public-data.iowa_liquor_sales.sales เพื่อวิเคราะห์ยอดขายปลีกเหล้าในไอโอวา ใช้เมธอด bigframes.pandas.read_gbq() เพื่อสร้าง DataFrame จากสตริงการค้นหาหรือรหัสตาราง

เรียกใช้โค้ดต่อไปนี้ในเซลล์โค้ดใหม่เพื่อสร้าง DataFrame ชื่อ "df"

df = bpd.read_gbq_table("bigquery-public-data.iowa_liquor_sales.sales")

ดูข้อมูลพื้นฐานเกี่ยวกับ DataFrame

ใช้วิธีการ DataFrame.peek() เพื่อดาวน์โหลดข้อมูลตัวอย่างจำนวนเล็กน้อย

เรียกใช้เซลล์นี้:

df.peek()

ผลลัพธ์ที่คาดหวัง:

index   invoice_and_item_number date    store_number    store_name      ...
0       RINV-04620300080        2023-04-28      10197   SUNSHINE FOODS / HAWARDEN      
1       RINV-04864800097        2023-09-25      2621    HY-VEE FOOD STORE #3 / SIOUX CITY      
2       RINV-05057200028        2023-12-28      4255    FAREWAY STORES #058 / ORANGE CITY      
3       ...                            

หมายเหตุ: head() ต้องมีการจัดลําดับและโดยทั่วไปจะมีประสิทธิภาพน้อยกว่า peek() หากต้องการแสดงภาพตัวอย่างข้อมูล

เช่นเดียวกับ Pandas ให้ใช้พร็อพเพอร์ตี้ DataFrame.dtypes เพื่อดูคอลัมน์ทั้งหมดที่มีอยู่และประเภทข้อมูลที่เกี่ยวข้อง ซึ่งจะแสดงในลักษณะที่เข้ากันได้กับ Pandas

เรียกใช้เซลล์นี้:

df.dtypes

ผลลัพธ์ที่คาดหวัง:

invoice_and_item_number string[pyarrow]
date    date32[day][pyarrow]
store_number    string[pyarrow]
store_name      string[pyarrow]
address string[pyarrow]
city    string[pyarrow]
zip_code        string[pyarrow]
store_location  geometry
county_number   string[pyarrow]
county  string[pyarrow]
category        string[pyarrow]
category_name   string[pyarrow]
vendor_number   string[pyarrow]
vendor_name     string[pyarrow]
item_number     string[pyarrow]
item_description        string[pyarrow]
pack    Int64
bottle_volume_ml        Int64
state_bottle_cost       Float64
state_bottle_retail     Float64
bottles_sold    Int64
sale_dollars    Float64
volume_sold_liters      Float64
volume_sold_gallons     Float64

dtype: object

เมธอด DataFrame.describe() จะค้นหาสถิติพื้นฐานบางอย่างจาก DataFrame เรียกใช้ DataFrame.to_pandas() เพื่อดาวน์โหลดสถิติสรุปเหล่านี้เป็น DataFrame ของ Pandas

เรียกใช้เซลล์นี้:

df.describe("all").to_pandas()

ผลลัพธ์ที่คาดหวัง:

        invoice_and_item_number date    store_number    store_name      ...
nunique
30305765        <NA>    3158    3353    ...
std    
<NA>    <NA>    <NA>    <NA>    ...
mean    
<NA>    <NA>    <NA>    <NA>    ...
75%     <NA>    <NA>    <NA>    <NA>    ...
25%     <NA>    <NA>    <NA>    <NA>    ...
count  
30305765        <NA>    30305765        30305765        ...
min    
<NA>    <NA>    <NA>    <NA>    ...
50%     <NA>    <NA>    <NA>    <NA>    ...
max    
<NA>    <NA>    <NA>    <NA>    ...
9 rows × 24 columns

4 แสดงข้อมูลเป็นภาพและล้างข้อมูล

ชุดข้อมูลยอดขายปลีกเครื่องดื่มแอลกอฮอล์ในไอโอวาให้ข้อมูลทางภูมิศาสตร์แบบละเอียด รวมถึงตําแหน่งที่ตั้งของร้านค้าปลีก ใช้ข้อมูลเหล่านี้เพื่อระบุแนวโน้มและความแตกต่างในพื้นที่ทางภูมิศาสตร์

แสดงภาพยอดขายตามรหัสไปรษณีย์

มีวิธีการแสดงภาพในตัวหลายวิธี เช่น DataFrame.plot.hist() ใช้วิธีการนี้เพื่อเปรียบเทียบยอดขายสุราตามรหัสไปรษณีย์

volume_by_zip = df.groupby("zip_code").agg({"volume_sold_liters": "sum"})
volume_by_zip
.plot.hist(bins=20)

ผลลัพธ์ที่คาดหวัง:

ฮิสโตแกรมของปริมาณ

ใช้แผนภูมิแท่งเพื่อดูว่ารหัสไปรษณีย์ใดขายเครื่องดื่มแอลกอฮอล์มากที่สุด

(
  volume_by_zip
 
.sort_values("volume_sold_liters", ascending=False)
 
.head(25)
 
.to_pandas()
 
.plot.bar(rot=80)
)

ผลลัพธ์ที่คาดหวัง:

แผนภูมิแท่งของปริมาณเครื่องดื่มแอลกอฮอล์ในรหัสไปรษณีย์ที่มียอดขายสูงสุด

ล้างข้อมูล

รหัสไปรษณีย์บางรหัสจะมี .0 ต่อท้าย อาจเป็นเพราะระบบแปลงรหัสไปรษณีย์เป็นค่าทศนิยมโดยไม่ได้ตั้งใจในขั้นตอนการรวบรวมข้อมูล ใช้นิพจน์ทั่วไปเพื่อล้างรหัสไปรษณีย์และวิเคราะห์อีกครั้ง

df = (
    bpd.read_gbq_table("bigquery-public-data.iowa_liquor_sales.sales")
    .assign(
        zip_code=lambda _: _["zip_code"].str.replace(".0", "")
    )
)
volume_by_zip = df.groupby("zip_code").agg({"volume_sold_liters": "sum"})
(
  volume_by_zip
  .sort_values("volume_sold_liters", ascending=False)
  .head(25)
  .to_pandas()
  .plot.bar(rot=80)
)

ผลลัพธ์ที่คาดหวัง:

แผนภูมิแท่งของปริมาณเครื่องดื่มแอลกอฮอล์ในรหัสไปรษณีย์ที่มียอดขายสูงสุด

5 ค้นหาความสัมพันธ์ในการขาย

เหตุใดรหัสไปรษณีย์บางรหัสจึงขายได้มากกว่ารหัสไปรษณีย์อื่นๆ สมมติฐานหนึ่งคือความแตกต่างของจำนวนประชากร รหัสไปรษณีย์ที่มีประชากรมากกว่ามีแนวโน้มที่จะขายสุราได้มากกว่า

ทดสอบสมมติฐานนี้โดยคํานวณความสัมพันธ์ระหว่างจํานวนประชากรกับปริมาณการขายสุรา

รวมกับชุดข้อมูลอื่นๆ

รวมกับชุดข้อมูลประชากร เช่น แบบสํารวจพื้นที่การแจกแจงรหัสไปรษณีย์ของการสำรวจชุมชนอเมริกันของสำนักงานสํามะโนประชากรของสหรัฐอเมริกา

census_acs = bpd.read_gbq_table("bigquery-public-data.census_bureau_acs.zcta_2020_5yr")

การสํารวจชุมชนอเมริกันระบุรัฐตาม GEOID ในกรณีของพื้นที่การสรุปข้อมูลรหัสไปรษณีย์ GEOID จะเท่ากับรหัสไปรษณีย์

volume_by_pop = volume_by_zip.join(
    census_acs
.set_index("geo_id")
)

สร้างผังกระจายเพื่อเปรียบเทียบประชากรในพื้นที่การแจกแจงรหัสไปรษณีย์กับปริมาณแอลกอฮอล์ที่ขายได้ (ลิตร)

(
    volume_by_pop
[["volume_sold_liters", "total_pop"]]
   
.to_pandas()
   
.plot.scatter(x="total_pop", y="volume_sold_liters")
)

ผลลัพธ์ที่คาดหวัง:

ผังกระจายของพื้นที่การแจกแจงข้อมูลรหัสไปรษณีย์ตามจำนวนประชากรและปริมาณสุราที่ขาย

คํานวณสหสัมพันธ์

แนวโน้มดูเป็นเส้นตรงโดยประมาณ ปรับโมเดลการหาค่าสัมประสิทธิ์การถดถอยเชิงเส้นกับข้อมูลนี้เพื่อดูว่าประชากรสามารถคาดการณ์ยอดขายสุราได้ดีเพียงใด

from bigframes.ml.linear_model import LinearRegression

feature_columns = volume_by_pop[["total_pop"]]
label_columns = volume_by_pop[["volume_sold_liters"]]

# Create the linear model
model = LinearRegression()
model.fit(feature_columns, label_columns)

ตรวจสอบความพอดีโดยใช้เมธอด score

model.score(feature_columns, label_columns).to_pandas()

ตัวอย่างเอาต์พุต:

        mean_absolute_error     mean_squared_error      mean_squared_log_error  median_absolute_error   r2_score        explained_variance
0       245065.664095   224398167097.364288     5.595021        178196.31289    0.380096        0.380096

วาดเส้นค่าสัมประสิทธิ์การถดถอยที่เหมาะสมที่สุด แต่เรียกใช้ฟังก์ชัน predict ในช่วงของค่าประชากร

import matplotlib.pyplot as pyplot
import numpy as np
import pandas as pd

line = pd.Series(np.arange(0, 50_000), name="total_pop")
predictions = model.predict(line).to_pandas()

zips = volume_by_pop[["volume_sold_liters", "total_pop"]].to_pandas()
pyplot.scatter(zips["total_pop"], zips["volume_sold_liters"])
pyplot.plot(
 
line,
 
predictions.sort_values("total_pop")["predicted_volume_sold_liters"],
 
marker=None,
 
color="red",
)

ผลลัพธ์ที่คาดหวัง:

แผนภูมิกระจายที่มีเส้นค่าสัมประสิทธิ์การถดถอย

การแก้ไขความแปรปรวนของค่าเบี่ยงเบนมาตรฐาน

ข้อมูลในแผนภูมิก่อนหน้าดูเหมือนว่าจะมีค่าความแปรปรวนไม่เท่ากัน ความแปรปรวนรอบเส้นค่าดีที่สุดจะเพิ่มขึ้นตามจำนวนประชากร

ปริมาณเครื่องดื่มแอลกอฮอล์ที่ซื้อต่อคนอาจค่อนข้างคงที่

volume_per_pop = (
    volume_by_pop[volume_by_pop['total_pop'] > 0]
    .assign(liters_per_pop=lambda df: df["volume_sold_liters"] / df["total_pop"])
)

(
    volume_per_pop[["liters_per_pop", "total_pop"]]
    .to_pandas()
    .plot.scatter(x="total_pop", y="liters_per_pop")
)

ผลลัพธ์ที่คาดหวัง:

ผังกระจายของลิตรต่อประชากร

คำนวณปริมาณแอลกอฮอล์ที่ซื้อโดยเฉลี่ยได้ 2 วิธีดังนี้

  1. ปริมาณเครื่องดื่มแอลกอฮอล์ที่ซื้อโดยเฉลี่ยต่อคนในไอโอวาคือเท่าใด
  2. ปริมาณเครื่องดื่มแอลกอฮอล์ที่ซื้อต่อคนโดยเฉลี่ยจากรหัสไปรษณีย์ทั้งหมดคือเท่าใด

ใน (1) แสดงปริมาณเครื่องดื่มแอลกอฮอล์ที่ซื้อในรัฐ ใน (2) จะเป็นรหัสไปรษณีย์เฉลี่ย ซึ่งไม่จำเป็นต้องเหมือนกับ (1) เนื่องจากรหัสไปรษณีย์แต่ละรหัสมีประชากรไม่เท่ากัน

df = (
    bpd.read_gbq_table("bigquery-public-data.iowa_liquor_sales.sales")
    .assign(
        zip_code=lambda _: _["zip_code"].str.replace(".0", "")
    )
)
census_state = bpd.read_gbq(
    "bigquery-public-data.census_bureau_acs.state_2020_5yr",
    index_col="geo_id",
)

volume_per_pop_statewide = (
    df['volume_sold_liters'].sum()
    / census_state["total_pop"].loc['19']
)
volume_per_pop_statewide

ผลลัพธ์ที่คาดหวัง: 87.997

average_per_zip = volume_per_pop["liters_per_pop"].mean()
average_per_zip

ผลลัพธ์ที่คาดหวัง: 67.139

แสดงค่าเฉลี่ยเหล่านี้ในผัง คล้ายกับด้านบน

import numpy as np
import pandas as pd
from matplotlib import pyplot

line = pd.Series(np.arange(0, 50_000), name="total_pop")

zips = volume_per_pop[["liters_per_pop", "total_pop"]].to_pandas()
pyplot.scatter(zips["total_pop"], zips["liters_per_pop"])
pyplot.plot(line, np.full(line.shape, volume_per_pop_statewide), marker=None, color="magenta")
pyplot.plot(line, np.full(line.shape, average_per_zip), marker=None, color="red")

ผลลัพธ์ที่คาดหวัง:

ผังกระจายของลิตรต่อประชากร

อย่างไรก็ตาม ยังมีรหัสไปรษณีย์บางรหัสที่ค่าเบี่ยงเบนค่อนข้างสูง โดยเฉพาะในพื้นที่ที่มีประชากรน้อย คุณสามารถลองตั้งสมมติฐานเกี่ยวกับสาเหตุของข้อมูลนี้ เช่น รหัสไปรษณีย์บางรหัสอาจมีประชากรน้อยแต่มีการบริโภคสูงเนื่องจากมีร้านขายเหล้าเพียงแห่งเดียวในพื้นที่ หากเป็นเช่นนั้น การคํานวณตามจํานวนประชากรของรหัสไปรษณีย์รอบๆ อาจกรองค่าที่ผิดปกติเหล่านี้ออกได้

6 การเปรียบเทียบประเภทสุราที่ขาย

นอกจากข้อมูลทางภูมิศาสตร์แล้ว ฐานข้อมูลการขายปลีกเครื่องดื่มแอลกอฮอล์ในไอโอวายังมีรายละเอียดเกี่ยวกับสินค้าที่ขายด้วย การวิเคราะห์ข้อมูลเหล่านี้อาจช่วยให้เราทราบความแตกต่างของรสนิยมในแต่ละพื้นที่ทางภูมิศาสตร์

สำรวจหมวดหมู่

รายการต่างๆ จะจัดหมวดหมู่ไว้ในฐานข้อมูล มีหมวดหมู่กี่หมวดหมู่

import bigframes.pandas as bpd

bpd.options.bigquery.ordering_mode = "partial"
bpd.options.display.repr_mode = "deferred"

df = bpd.read_gbq_table("bigquery-public-data.iowa_liquor_sales.sales")
df.category_name.nunique()

ผลลัพธ์ที่คาดหวัง: 103

หมวดหมู่ใดได้รับความนิยมสูงสุดตามปริมาณ

counts = (
    df
.groupby("category_name")
   
.agg({"volume_sold_liters": "sum"})
   
.sort_values(["volume_sold_liters"], ascending=False)
   
.to_pandas()
)
counts
.head(25).plot.bar(rot=80)

แผนภูมิแท่งของหมวดหมู่สุรายอดนิยมที่ขาย

การทำงานกับประเภทข้อมูล ARRAY

วิสกี้ รัม วอดก้า และอื่นๆ แต่ละรายการมีหมวดหมู่หลายหมวดหมู่ ฉันต้องการจัดกลุ่มรายการเหล่านี้เข้าด้วยกัน

เริ่มต้นด้วยแยกชื่อหมวดหมู่ออกเป็นคำๆ โดยใช้เมธอด Series.str.split() เลิกซ้อนอาร์เรย์ที่สร้างนี้โดยใช้เมธอด explode()

category_parts = df.category_name.str.split(" ").explode()
counts
= (
    category_parts
   
.groupby(category_parts)
   
.size()
   
.sort_values(ascending=False)
   
.to_pandas()
)
counts
.head(25).plot.bar(rot=80)

คำตามจำนวนจากหมวดหมู่

category_parts.nunique()

ผลลัพธ์ที่คาดหวัง: 113

เมื่อดูแผนภูมิด้านบน ข้อมูลจะยังคงแยก VODKA ออกจาก VODKAS จำเป็นต้องมีการจัดกลุ่มเพิ่มเติมเพื่อยุบหมวดหมู่ให้เป็นชุดที่เล็กลง

7 การใช้ NLTK กับ DataFrame ของ BigQuery

เมื่อมีเพียงประมาณ 100 หมวดหมู่ คุณอาจเขียนวิธีการแก้ปัญหาแบบเฮอร์มิทิคส์หรือสร้างการแมปจากหมวดหมู่ไปยังประเภทเครื่องดื่มที่กว้างขึ้นด้วยตนเองได้ หรือจะใช้โมเดลภาษาขนาดใหญ่อย่าง Gemini เพื่อสร้างการแมปดังกล่าวก็ได้ ลองใช้ Codelab รับข้อมูลเชิงลึกจากข้อมูลที่ไม่เป็นโครงสร้างโดยใช้ DataFrame ของ BigQuery เพื่อใช้ DataFrame ของ BigQuery กับ Gemini

แต่ให้ใช้แพ็กเกจการประมวลผลภาษาที่เป็นธรรมชาติแบบดั้งเดิมอย่าง NLTK เพื่อประมวลผลข้อมูลเหล่านี้แทน เทคโนโลยีที่เรียกว่า "ตัวแยกคำ" สามารถผสานคำนามพหูพจน์และคำนามเอกพจน์ให้เป็นค่าเดียวกันได้ เป็นต้น

การใช้ NLTK เพื่อตัดรากคำ

แพ็กเกจ NLTK มีวิธีการประมวลผลภาษาธรรมชาติที่เข้าถึงได้จาก Python ติดตั้งแพ็กเกจเพื่อลองใช้

%pip install nltk

ถัดไป ให้นําเข้าแพ็กเกจ ตรวจสอบเวอร์ชัน ซึ่งจะใช้ในบทแนะนำในภายหลัง

import nltk

nltk.__version__

วิธีหนึ่งในการทำให้คำเป็นมาตรฐานเพื่อ "สร้างราก" คำ ซึ่งจะนําส่วนต่อท้ายออก เช่น "s" ต่อท้ายสำหรับรายการที่แสดงเป็นพหูพจน์

def stem(word: str) -> str:
   
# https://www.nltk.org/howto/stem.html
   
import nltk.stem.snowball

   
# Avoid failure if a NULL is passed in.
   
if not word:
       
return word

   
stemmer = nltk.stem.snowball.SnowballStemmer("english")
   
return stemmer.stem(word)

ลองใช้กับคำ 2-3 คำ

stem("WHISKEY")

ผลลัพธ์ที่คาดหวัง: whiskey

stem("WHISKIES")

ผลลัพธ์ที่คาดหวัง: whiski

แต่การดำเนินการนี้ไม่ได้แมปวิสกี้กับวิสกี้ โปรแกรมแยกรากคำทำงานได้ไม่ดีกับคำพหูพจน์ที่ผิดปกติ ลองใช้โปรแกรมแยกคำที่เป็นรากศัพท์ ซึ่งใช้เทคนิคที่ซับซ้อนมากขึ้นในการระบุคำพื้นฐานที่เรียกว่า "รากศัพท์"

def lemmatize(word: str) -> str:
   
# https://stackoverflow.com/a/18400977/101923
   
# https://www.nltk.org/api/nltk.stem.wordnet.html#module-nltk.stem.wordnet
   
import nltk
   
import nltk.stem.wordnet


   
# Avoid failure if a NULL is passed in.
   
if not word:
       
return word

   
nltk.download('wordnet')
   
wnl = nltk.stem.wordnet.WordNetLemmatizer()
   
return wnl.lemmatize(word.lower())

ลองใช้กับคำ 2-3 คำ

lemmatize("WHISKIES")

ผลลัพธ์ที่คาดหวัง: whisky

lemmatize("WHISKY")

ผลลัพธ์ที่คาดหวัง: whisky

lemmatize("WHISKEY")

ผลลัพธ์ที่คาดหวัง: whiskey

ขออภัย โปรแกรมแยกคำที่เป็นรากศัพท์นี้ไม่ได้แมป "whiskey" กับคำที่เป็นรากศัพท์เดียวกับ "whiskies" เนื่องจากคํานี้มีความสําคัญอย่างยิ่งสําหรับฐานข้อมูลการขายเครื่องดื่มแอลกอฮอล์แบบค้าปลีกในไอโอวา ให้จับคู่คํานี้กับการสะกดแบบอเมริกันด้วยตนเองโดยใช้พจนานุกรม

def lemmatize(word: str) -> str:
   
# https://stackoverflow.com/a/18400977/101923
   
# https://www.nltk.org/api/nltk.stem.wordnet.html#module-nltk.stem.wordnet
   
import nltk
   
import nltk.stem.wordnet


   
# Avoid failure if a NULL is passed in.
   
if not word:
       
return word

   
nltk.download('wordnet')
   
wnl = nltk.stem.wordnet.WordNetLemmatizer()
   
lemma = wnl.lemmatize(word.lower())

   
table = {
       
"whisky": "whiskey",  # Use the American spelling.
   
}
   
return table.get(lemma, lemma)

ลองใช้กับคำ 2-3 คำ

lemmatize("WHISKIES")

ผลลัพธ์ที่คาดหวัง: whiskey

lemmatize("WHISKEY")

ผลลัพธ์ที่คาดหวัง: whiskey

ยินดีด้วย เครื่องมือแยกคำนี้ควรทำงานได้ดีในการจำกัดหมวดหมู่ให้แคบลง หากต้องการใช้กับ BigQuery คุณต้องติดตั้งใช้งานในระบบคลาวด์

ตั้งค่าโปรเจ็กต์สําหรับการทําให้ฟังก์ชันใช้งานได้

ก่อนที่คุณจะนำข้อมูลนี้ไปใช้งานในระบบคลาวด์เพื่อให้ BigQuery เข้าถึงฟังก์ชันนี้ได้ คุณจะต้องทำการตั้งค่าแบบครั้งเดียว

สร้างเซลล์โค้ดใหม่และแทนที่ your-project-id ด้วยรหัสโปรเจ็กต์ Google Cloud ที่คุณใช้สำหรับบทแนะนำนี้

project_id = "your-project-id"

สร้างบัญชีบริการที่ไม่มีสิทธิ์ใดๆ เนื่องจากฟังก์ชันนี้ไม่จำเป็นต้องเข้าถึงทรัพยากรในระบบคลาวด์

from google.cloud import iam_admin_v1
from google.cloud.iam_admin_v1 import types

iam_admin_client = iam_admin_v1.IAMClient()
request = types.CreateServiceAccountRequest()

account_id = "bigframes-no-permissions"
request.account_id = account_id
request.name = f"projects/{project_id}"

display_name = "bigframes remote function (no permissions)"
service_account = types.ServiceAccount()
service_account.display_name = display_name
request.service_account = service_account

account = iam_admin_client.create_service_account(request=request)
print(account.email)

ผลลัพธ์ที่คาดหวัง: bigframes-no-permissions@your-project-id.iam.gserviceaccount.com

สร้างชุดข้อมูล BigQuery เพื่อเก็บฟังก์ชัน

from google.cloud import bigquery

bqclient = bigquery.Client(project=project_id)
dataset = bigquery.Dataset(f"{project_id}.functions")
bqclient.create_dataset(dataset, exists_ok=True)

การใช้ฟังก์ชันระยะไกล

เปิดใช้ Cloud Functions API หากยังไม่ได้เปิดใช้

!gcloud services enable cloudfunctions.googleapis.com

จากนั้นให้ติดตั้งใช้งานฟังก์ชันในชุดข้อมูลที่คุณเพิ่งสร้าง เพิ่มตัวตกแต่ง @bpd.remote_function ลงในฟังก์ชันที่คุณสร้างในขั้นตอนก่อนหน้า

@bpd.remote_function(
   
dataset=f"{project_id}.functions",
   
name="lemmatize",
   
# TODO: Replace this with your version of nltk.
   
packages=["nltk==3.9.1"],
   
cloud_function_service_account=f"bigframes-no-permissions@{project_id}.iam.gserviceaccount.com",
   
cloud_function_ingress_settings="internal-only",
)
def lemmatize(word: str) -> str:
   
# https://stackoverflow.com/a/18400977/101923
   
# https://www.nltk.org/api/nltk.stem.wordnet.html#module-nltk.stem.wordnet
   
import nltk
   
import nltk.stem.wordnet


   
# Avoid failure if a NULL is passed in.
   
if not word:
       
return word

   
nltk.download('wordnet')
   
wnl = nltk.stem.wordnet.WordNetLemmatizer()
   
lemma = wnl.lemmatize(word.lower())

   
table = {
       
"whisky": "whiskey",  # Use the American spelling.
   
}
   
return table.get(lemma, lemma)

การทำให้ใช้งานได้จะใช้เวลาประมาณ 2 นาที

การใช้ฟังก์ชันของรีโมต

เมื่อทำให้ใช้งานได้เสร็จแล้ว คุณจะทดสอบฟังก์ชันนี้ได้

lemmatize = bpd.read_gbq_function(f"{project_id}.functions.lemmatize")

words
= bpd.Series(["whiskies", "whisky", "whiskey", "vodkas", "vodka"])
words
.apply(lemmatize).to_pandas()

ผลลัพธ์ที่คาดหวัง:

0       whiskey
1       whiskey
2       whiskey
3       vodka
4       vodka

dtype: string

8 การเปรียบเทียบการบริโภคเครื่องดื่มแอลกอฮอล์ตามเขต

เมื่อฟังก์ชัน lemmatize พร้อมใช้งานแล้ว ให้ใช้ฟังก์ชันดังกล่าวเพื่อรวมหมวดหมู่

การค้นหาคำที่จะสรุปหมวดหมู่ได้ดีที่สุด

ก่อนอื่น ให้สร้าง DataFrame ของหมวดหมู่ทั้งหมดในฐานข้อมูล

df = bpd.read_gbq_table("bigquery-public-data.iowa_liquor_sales.sales")

categories
= (
    df
['category_name']
   
.groupby(df['category_name'])
   
.size()
   
.to_frame()
   
.rename(columns={"category_name": "total_orders"})
   
.reset_index(drop=False)
)
categories
.to_pandas()

ผลลัพธ์ที่คาดหวัง:

category_name   total_orders
0       100 PROOF VODKA 99124
1       100% AGAVE TEQUILA      724374
2       AGED DARK RUM   59433
3       AMARETTO - IMPORTED     102
4       AMERICAN ALCOHOL        24351
...     ...     ...
98      WATERMELON SCHNAPPS     17844
99      WHISKEY LIQUEUR 1442732
100     WHITE CREME DE CACAO    7213
101     WHITE CREME DE MENTHE   2459
102     WHITE RUM       436553
103 rows × 2 columns

ถัดไป ให้สร้าง DataFrame ของคำทั้งหมดในหมวดหมู่ ยกเว้นคำเติมบางคำ เช่น เครื่องหมายวรรคตอนและ "รายการ"

words = (
    categories.assign(
        words=categories['category_name']
        .str.lower()
        .str.split(" ")
    )
    .assign(num_words=lambda _: _['words'].str.len())
    .explode("words")
    .rename(columns={"words": "word"})
)
words = words[
    # Remove punctuation and "item", unless it's the only word
    (words['word'].str.isalnum() & ~(words['word'].str.startswith('item')))
    | (words['num_words'] == 1)
]
words.to_pandas()

ผลลัพธ์ที่คาดหวัง:

category_name   total_orders    word    num_words
0       100 PROOF VODKA 99124   100     3
1       100 PROOF VODKA 99124   proof   3
2       100 PROOF VODKA 99124   vodka   3
...     ...     ...     ...     ...
252     WHITE RUM       436553  white   2
253     WHITE RUM       436553  rum     2
254 rows × 4 columns

โปรดทราบว่าการทำ Lemmatization หลังจากการจัดกลุ่มจะช่วยลดภาระงานใน Cloud Function คุณสามารถใช้ฟังก์ชัน lemmatize กับแต่ละแถวหลายล้านแถวในฐานข้อมูลได้ แต่วิธีนี้จะใช้ทรัพยากรมากกว่าการใช้หลังจากการจัดกลุ่มและอาจต้องเพิ่มโควต้า

lemmas = words.assign(lemma=lambda _: _["word"].apply(lemmatize))
lemmas.to_pandas()

ผลลัพธ์ที่คาดหวัง:

category_name   total_orders    word    num_words       lemma
0       100 PROOF VODKA 99124   100     3       100
1       100 PROOF VODKA 99124   proof   3       proof
2       100 PROOF VODKA 99124   vodka   3       vodka
...     ...     ...     ...     ...     ...
252     WHITE RUM       436553  white   2       white
253     WHITE RUM       436553  rum     2       rum
254 rows × 5 columns

เมื่อเปลี่ยนคำเป็นคำพื้นฐานแล้ว คุณจะต้องเลือกคำพื้นฐานที่สรุปหมวดหมู่ได้ดีที่สุด เนื่องจากมีคำหน้าที่อยู่ในหมวดหมู่ไม่มากนัก ให้ใช้วิธีการเฮิวริสติกที่ว่าหากคำหนึ่งปรากฏในหมวดหมู่อื่นๆ หลายหมวดหมู่ ก็มีความเป็นไปได้ว่าคำนั้นจะเป็นคำสรุปที่ดีกว่า (เช่น วิสกี้)

lemma_counts = (
    lemmas
   
.groupby("lemma", as_index=False)
   
.agg({"total_orders": "sum"})
   
.rename(columns={"total_orders": "total_orders_with_lemma"})
)

categories_with_lemma_counts
= lemmas.merge(lemma_counts, on="lemma")

max_lemma_count
= (
    categories_with_lemma_counts
   
.groupby("category_name", as_index=False)
   
.agg({"total_orders_with_lemma": "max"})
   
.rename(columns={"total_orders_with_lemma": "max_lemma_count"})
)

categories_with_max
= categories_with_lemma_counts.merge(
    max_lemma_count
,
    on
="category_name"
)

categories_mapping
= categories_with_max[
    categories_with_max
['total_orders_with_lemma'] == categories_with_max['max_lemma_count']
].groupby("category_name", as_index=False).max()
categories_mapping
.to_pandas()

ผลลัพธ์ที่คาดหวัง:

        category_name   total_orders    word    num_words       lemma   total_orders_with_lemma max_lemma_count
0       100 PROOF VODKA 99124   vodka   3       vodka   7575769 7575769
1       100% AGAVE TEQUILA      724374  tequila 3       tequila 1601092 1601092
2       AGED DARK RUM   59433   rum     3       rum     3226633 3226633
...     ...     ...     ...     ...     ...     ...     ...
100     WHITE CREME DE CACAO    7213    white   4       white   446225  446225
101     WHITE CREME DE MENTHE   2459    white   4       white   446225  446225
102     WHITE RUM       436553  rum     2       rum     3226633 3226633
103 rows × 7 columns

เมื่อเรามีคําที่ตรงกันทั้งหมดรายการเดียวที่สรุปแต่ละหมวดหมู่แล้ว ให้ผสานข้อมูลนี้กับ DataFrame เดิม

df_with_lemma = df.merge(
    categories_mapping
,
    on
="category_name",
    how
="left"
)
df_with_lemma
[df_with_lemma['category_name'].notnull()].peek()

ผลลัพธ์ที่คาดหวัง:

        invoice_and_item_number ...     lemma   total_orders_with_lemma max_lemma_count
0       S30989000030    ...     vodka   7575769 7575769
1       S30538800106    ...     vodka   7575769 7575769
2       S30601200013    ...     vodka   7575769 7575769
3       S30527200047    ...     vodka   7575769 7575769
4       S30833600058    ...     vodka   7575769 7575769
5 rows × 30 columns

การเปรียบเทียบเขต

เปรียบเทียบยอดขายในแต่ละเขตเพื่อดูความแตกต่าง

county_lemma = (
    df_with_lemma
    .groupby(["county", "lemma"])
    .agg({"volume_sold_liters": "sum"})
    # Cast to an integer for more deterministic equality comparisons.
    .assign(volume_sold_int64=lambda _: _['volume_sold_liters'].astype("Int64"))
)

ค้นหาผลิตภัณฑ์ (Lemma) ที่ขายดีที่สุดในแต่ละเขต

county_max = (
    county_lemma
   
.reset_index(drop=False)
   
.groupby("county")
   
.agg({"volume_sold_int64": "max"})
)

county_max_lemma
= county_lemma[
    county_lemma
["volume_sold_int64"] == county_max["volume_sold_int64"]
]

county_max_lemma
.to_pandas()

ผลลัพธ์ที่คาดหวัง:

        volume_sold_liters      volume_sold_int64
county  lemma          
SCOTT   vodka  
6044393.1       6044393
APPANOOSE       whiskey
292490.44       292490
HAMILTON        whiskey
329118.92       329118
...     ...     ...     ...
WORTH   whiskey
100542.85       100542
MITCHELL        vodka  
158791.94       158791
RINGGOLD        whiskey
65107.8 65107
101 rows × 2 columns

แต่ละเขตมีความแตกต่างกันอย่างไร

county_max_lemma.groupby("lemma").size().to_pandas()

ผลลัพธ์ที่คาดหวัง:

lemma   
american        1
liqueur 1
vodka   15
whiskey 83

dtype: Int64

ในเขตส่วนใหญ่ วิสกี้เป็นผลิตภัณฑ์ที่ได้รับความนิยมสูงสุดตามปริมาณ ส่วนวอดก้าได้รับความนิยมสูงสุดใน 15 เขต เปรียบเทียบกับประเภทสุราที่ได้รับความนิยมสูงสุดทั่วรัฐ

total_liters = (
    df_with_lemma
   
.groupby("lemma")
   
.agg({"volume_sold_liters": "sum"})
   
.sort_values("volume_sold_liters", ascending=False)
)
total_liters
.to_pandas()

ผลลัพธ์ที่คาดหวัง:

        volume_sold_liters
lemma  
vodka  
85356422.950001
whiskey
85112339.980001
rum    
33891011.72
american        
19994259.64
imported        
14985636.61
tequila
12357782.37
cocktails
/rtd   7406769.87
...

วิสกี้และวอดก้ามีปริมาณเกือบเท่ากัน โดยวอดก้ามีปริมาณสูงกว่าวิสกี้เล็กน้อยในรัฐ

การเปรียบเทียบสัดส่วน

ยอดขายในแต่ละเขตมีความโดดเด่นอย่างไร อะไรทำให้เขตแตกต่างจากส่วนอื่นๆ ของรัฐ

ใช้ตัววัด h ของ Cohen เพื่อดูว่าปริมาณการขายเครื่องดื่มแอลกอฮอล์ใดแตกต่างไปตามสัดส่วนมากที่สุดจากสิ่งที่คาดไว้ตามสัดส่วนการขายทั่วรัฐ

import numpy as np

total_proportions = total_liters / total_liters.sum()
total_phi = 2 * np.arcsin(np.sqrt(total_proportions))

county_liters = df_with_lemma.groupby(["county", "lemma"]).agg({"volume_sold_liters": "sum"})
county_totals = df_with_lemma.groupby(["county"]).agg({"volume_sold_liters": "sum"})
county_proportions = county_liters / county_totals
county_phi = 2 * np.arcsin(np.sqrt(county_proportions))

cohens_h = (
   
(county_phi - total_phi)
   
.rename(columns={"volume_sold_liters": "cohens_h"})
   
.assign(cohens_h_int=lambda _: (_['cohens_h'] * 1_000_000).astype("Int64"))
)

เมื่อวัด h ของ Cohen สําหรับคําแต่ละคําแล้ว ให้หาความแตกต่างที่ใหญ่ที่สุดจากสัดส่วนระดับรัฐในแต่ละเขต

# Note: one might want to use the absolute value here if interested in counties
# that drink _less_ of a particular liquor than expected.
largest_per_county
= cohens_h.groupby("county").agg({"cohens_h_int": "max"})
counties
= cohens_h[cohens_h['cohens_h_int'] == largest_per_county["cohens_h_int"]]
counties
.sort_values('cohens_h', ascending=False).to_pandas()

ผลลัพธ์ที่คาดหวัง:

        cohens_h        cohens_h_int
county  lemma          
EL PASO liqueur
1.289667        1289667
ADAMS   whiskey
0.373591        373590
IDA     whiskey
0.306481        306481
OSCEOLA whiskey
0.295524        295523
PALO ALTO       whiskey
0.293697        293696
...     ...     ...     ...
MUSCATINE       rum    
0.053757        53757
MARION  rum    
0.053427        53427
MITCHELL        vodka  
0.048212        48212
WEBSTER rum    
0.044896        44895
CERRO GORDO     cocktails
/rtd   0.027496        27495
100 rows × 2 columns

ยิ่งค่า h ของ Cohen มีค่ามากเท่าใด ก็ยิ่งมีความเป็นไปได้สูงว่าปริมาณการบริโภคเครื่องดื่มแอลกอฮอล์ประเภทนั้นๆ เมื่อเทียบกับค่าเฉลี่ยของรัฐจะแตกต่างกันอย่างมีนัยสำคัญทางสถิติ สําหรับค่าบวกที่น้อยกว่า ความแตกต่างของปริมาณการใช้จะแตกต่างจากค่าเฉลี่ยระดับรัฐ แต่อาจเกิดจากความแตกต่างแบบสุ่ม

หมายเหตุ: ดูเหมือนว่าเขต EL PASO จะไม่ได้เป็นเขตในไอโอวา ซึ่งอาจบ่งบอกถึงความจำเป็นในการล้างข้อมูลอีกก่อนที่จะใช้ผลลัพธ์เหล่านี้อย่างเต็มรูปแบบ

การแสดงภาพเคาน์ตี

รวมกับตาราง bigquery-public-data.geo_us_boundaries.counties เพื่อรับพื้นที่ทางภูมิศาสตร์ของแต่ละเขต ชื่อเขตอาจซ้ำกันได้ในสหรัฐอเมริกา ดังนั้นให้กรองเพื่อรวมเฉพาะเขตจากไอโอวา รหัส FIPS สำหรับไอโอวาคือ "19"

counties_geo = (
    bpd.read_gbq("bigquery-public-data.geo_us_boundaries.counties")
    .assign(county=lambda _: _['county_name'].str.upper())
)
counties_plus = (
    counties
    .reset_index(drop=False)
    .merge(counties_geo[counties_geo['state_fips_code'] == '19'], on="county", how="left")
    .dropna(subset=["county_geom"])
    .to_pandas()
)
counties_plus

ผลลัพธ์ที่คาดหวัง:

county  lemma   cohens_h        cohens_h_int    geo_id  state_fips_code ...
0       ALLAMAKEE       american        0.087931        87930   19005   19      ...
1       BLACK HAWK      american        0.106256        106256  19013   19      ...
2       WINNESHIEK      american        0.093101        93101   19191   19      ...
...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...     ...
96      CLINTON tequila 0.075708        75707   19045   19      ...
97      POLK    tequila 0.087438        87438   19153   19      ...
98      LEE     schnapps        0.064663        64663   19111   19      ...
99 rows × 23 columns

ใช้ GeoPandas เพื่อแสดงภาพความแตกต่างเหล่านี้บนแผนที่

import geopandas

counties_plus = geopandas.GeoDataFrame(counties_plus, geometry="county_geom")

# https://stackoverflow.com/a/42214156/101923
ax = counties_plus.plot(figsize=(14, 14))
counties_plus.apply(
   
lambda row: ax.annotate(
       
text=row['lemma'],
       
xy=row['county_geom'].centroid.coords[0],
       
ha='center'
   
),
   
axis=1,
)

แผนที่ของเครื่องดื่มแอลกอฮอล์ที่แตกต่างจากสัดส่วนปริมาณการขายทั่วรัฐมากที่สุดในแต่ละเขต

9 ล้างข้อมูล

หากสร้างโปรเจ็กต์ Google Cloud ใหม่สำหรับบทแนะนำนี้ คุณสามารถลบโปรเจ็กต์เพื่อป้องกันการเรียกเก็บเงินเพิ่มเติมสำหรับตารางหรือทรัพยากรอื่นๆ ที่สร้างขึ้น

หรือจะลบ Cloud Functions, บัญชีบริการ และชุดข้อมูลที่สร้างขึ้นสำหรับบทแนะนำนี้ก็ได้

10 ยินดีด้วย

คุณได้ล้างและวิเคราะห์ Structured Data โดยใช้ DataFrame ของ BigQuery คุณได้สำรวจชุดข้อมูลสาธารณะของ Google Cloud, โน้ตบุ๊ค Python ใน BigQuery Studio, BigQuery ML, BigQuery Remote Functions และความสามารถของ BigQuery DataFrame ไปพร้อมๆ กัน ยอดเยี่ยมมาก

ขั้นตอนถัดไป