Eksploracyjna analiza danych o sprzedaży alkoholu w stanie Iowa za pomocą pakietu BigQuery DataFrames

Eksploracyjna analiza danych o sprzedaży alkoholu w stanie Iowa za pomocą pakietu BigQuery DataFrames

Informacje o tym ćwiczeniu (w Codelabs)

subjectOstatnia aktualizacja: gru 4, 2024
account_circleAutorzy: Tim Swena

1. Omówienie

W tym praktycznym module użyjesz obiektów DataFrame BigQuery z notebooka Pythona w BigQuery Studio, aby oczyścić i przeanalizować publiczny zbiór danych o sprzedaży alkoholu w stanie Iowa. Korzystaj z możliwości BigQuery ML i funkcji zdalnych, aby uzyskiwać statystyki.

Utworzysz notatnik Pythona, aby porównać sprzedaż w różnych obszarach geograficznych. Można go dostosować do pracy z dowolnymi uporządkowanymi danymi.

Cele

Z tego modułu nauczysz się, jak:

  • Aktywowanie i używanie notatników Pythona w BigQuery Studio
  • Łączenie z BigQuery za pomocą pakietu BigQuery DataFrames
  • Tworzenie regresji liniowej za pomocą BigQuery ML
  • wykonywanie złożonych agregacji i złączeń za pomocą znanej już Ci składni podobnej do tej w pandas;

2. Wymagania

  • przeglądarka, np. Chrome lub Firefox;
  • projekt Google Cloud z włączonymi płatnościami;

Zanim zaczniesz

Aby wykonać instrukcje w tym laboratorium kodu, musisz mieć projekt Google Cloud z włączonym BigQuery Studio i połączone konto rozliczeniowe.

  1. W konsoli Google Cloud na stronie selektora projektu wybierz lub utwórz projekt Google Cloud.
  2. Sprawdź, czy w projekcie Google Cloud włączone są płatności. Dowiedz się, jak sprawdzić, czy w projekcie są włączone płatności.
  3. Postępuj zgodnie z instrukcjami, aby włączyć BigQuery Studio do zarządzania zasobami.

Przygotowanie BigQuery Studio

Utwórz pusty notatnik i połącz go ze środowiskiem wykonawczym.

  1. Otwórz BigQuery Studio w konsoli Google Cloud.
  2. Kliknij  obok przycisku +.
  3. Wybierz Notatnik w Pythonie.
  4. Zamknij selektor szablonów.
  5. Aby utworzyć nową komórkę kodu, kliknij + Kod.
  6. Zainstaluj najnowszą wersję pakietu BigQuery DataFrames z komórki kodu.Wpisz następujące polecenie.
    %pip install --upgrade bigframes --quiet
    Aby uruchomić komórkę kodu, kliknij przycisk Uruchom komórkę lub naciśnij Shift + Enter.

3. Czytanie publicznego zbioru danych

Zainicjuj pakiet BigQuery DataFrames, wykonując ten kod w nowej komórce kodu:

import bigframes.pandas as bpd

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

Uwaga: w tym samouczku używamy eksperymentalnego „trybu częściowego sortowania”, który umożliwia tworzenie bardziej wydajnych zapytań w połączeniu z filtracją w stylu pandas. Niektóre funkcje pakietu pandas, które wymagają ścisłej kolejności lub indeksu, mogą nie działać.

Sprawdź wersję pakietu bigframes za pomocą

bpd.__version__

Ten samouczek wymaga wersji 1.27.0 lub nowszej.

Sprzedaż detaliczna alkoholu w stanie Iowa

Zbiór danych o sprzedaży detalicznej napojów alkoholowych w stanie Iowa jest udostępniany w BigQuery w ramach programu Google Cloud dotyczącego publicznych zbiorów danych. Ten zbiór danych zawiera wszystkie zakupy hurtowe alkoholu w stanie Iowa przez sprzedawców na sprzedaż osobom fizycznym od 1 stycznia 2012 r. Dane są zbierane przez Wydział Napojów Alkoholowych w Departamencie Handlu w stanie Iowa.

W BigQuery utwórz zapytanie do tabeli bigquery-public-data.iowa_liquor_sales.sales, aby przeanalizować sprzedaż detaliczną napojów alkoholowych w stanie Iowa. Użyj metody bigframes.pandas.read_gbq(), aby utworzyć DataFrame z ciągu zapytania lub identyfikatora tabeli.

Aby utworzyć obiekt DataFrame o nazwie „df”, uruchom w nowej komórce kodu ten kod:

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

Poznawanie podstawowych informacji o ramce danych

Aby pobrać małą próbkę danych, użyj metody DataFrame.peek().

Uruchom tę komórkę:

df.peek()

Oczekiwane dane wyjściowe:

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       ...                            

Uwaga: funkcja head() wymaga uporządkowania danych i jest zazwyczaj mniej wydajna niż funkcja peek(), jeśli chcesz wizualizować próbkę danych.

Podobnie jak w przypadku pakietu pandas, aby wyświetlić wszystkie dostępne kolumny i odpowiadające im typy danych, użyj właściwości DataFrame.dtypes. Są one udostępniane w sposób zgodny z biblioteką pandas.

Uruchom tę komórkę:

df.dtypes

Oczekiwane dane wyjściowe:

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

Metoda DataFrame.describe() wysyła zapytanie o podstawowe statystyki z ramy danych. Uruchom DataFrame.to_pandas(), aby pobrać te statystyki podsumowania jako strukturę DataFrame w bibliotece pandas.

Uruchom tę komórkę:

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

Oczekiwane dane wyjściowe:

        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. Wizualizowanie i oczyszczanie danych

Zbiór danych o sprzedaży alkoholu w sklepach detalicznych w stanie Iowa zawiera szczegółowe informacje geograficzne, m.in. o lokalizacji sklepów. Na podstawie tych danych możesz identyfikować trendy i różnice w różnych obszarach geograficznych.

Wizualizacja sprzedaży według kodu pocztowego

Dostępnych jest kilka wbudowanych metod wizualizacji, np. DataFrame.plot.hist(). Użyj tej metody, aby porównać sprzedaż alkoholu według kodu pocztowego.

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

Oczekiwane dane wyjściowe:

Histogram wolumenu

Wykres słupkowy pokazuje, w których kodach pocztowych sprzedano najwięcej alkoholu.

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

Oczekiwane dane wyjściowe:

Wykres słupkowy przedstawiający wolumen alkoholu w przypadku kodów pocztowych z największą sprzedażą

Oczyść dane

Niektóre kody pocztowe mają końcówkę .0. Być może gdzieś w zbiorze danych kody pocztowe zostały przypadkowo przekonwertowane na wartości zmiennoprzecinkowe. Użyj wyrażeń regularnych, aby oczyścić kody pocztowe i powtórz analizę.

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)
)

Oczekiwane dane wyjściowe:

Wykres słupkowy przedstawiający wolumen alkoholu w przypadku kodów pocztowych z największą sprzedażą

5. Odkrywanie korelacji w sprzedaży

Dlaczego niektóre kody pocztowe sprzedają się lepiej niż inne? Jedna z hipotez mówi, że wynika to z różnic w wielkości populacji. Kod pocztowy z większą liczbą ludności prawdopodobnie sprzeda więcej alkoholu.

Przetestuj tę hipotezę, obliczając korelację między liczbą ludności a objętością sprzedaży alkoholu.

Złączanie z innymi zbiorami danych

Złącz z danymi o populacji, np. z tabulacją obszarów kodów pocztowych w ramach amerykańskiego spisu ludności (American Community Survey).

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

Badanie American Community Survey określa stany za pomocą identyfikatorów GEOID. W przypadku obszarów tabulatury kodu pocztowego identyfikator GEOID jest równy kodowi pocztowemu.

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

Utwórz wykres punktowy, aby porównać liczbę ludności w obszarze spisu ludności z kodem pocztowym z ilością sprzedanego alkoholu.

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

Oczekiwane dane wyjściowe:

Wykres punktowy obszarów podziału kodów pocztowych według liczby ludności i sprzedanych litrów alkoholu

Obliczanie korelacji

Trend wygląda na w przybliżeniu liniowy. Dopasuj do tego modelu regresji liniowej, aby sprawdzić, jak dobrze populacja może przewidywać sprzedaż alkoholu.

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)

Sprawdź, jak dobrze pasuje, używając metody score.

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

Przykładowe dane wyjściowe:

        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

Narysuj linię najlepiej pasującą do danych, ale wywołując funkcję predict w przypadku zakresu wartości populacji.

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",
)

Oczekiwane dane wyjściowe:

Wykres punktowy z linią trendu

Rozwiązywanie problemu heteroskedastyczności

Dane na poprzednim wykresie wydają się być heterocedystyczne. Wariancja wokół linii najlepszego dopasowania rośnie wraz z liczbą osób.

Być może ilość alkoholu kupowana przez jedną osobę jest stosunkowo stała.

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")
)

Oczekiwane dane wyjściowe:

Wykres punktowy litrów na osobę

Średnią liczbę litrów zakupionego alkoholu można obliczyć na 2 sposoby:

  1. Ile alkoholu kupuje przeciętna osoba w Iowa?
  2. Ile wynosi średnia ilość alkoholu kupowana przez osobę we wszystkich kodach pocztowych.

W przypadku (1) odzwierciedla ona ilość alkoholu kupowanego w całym stanie. W przypadku (2) odzwierciedla on średni kod pocztowy, który niekoniecznie będzie taki sam jak w przypadku (1), ponieważ różne kody pocztowe mają różne populacje.

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

Oczekiwany wynik: 87.997

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

Oczekiwany wynik: 67.139

Wykresuj te średnie w sposób podobny do opisanego powyżej.

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")

Oczekiwane dane wyjściowe:

Wykres punktowy litrów na osobę

Nadal istnieją kody pocztowe, które są dość dużymi wartościami odstapiającymi, zwłaszcza w obszarach o mniejszej liczbie ludności. Możesz się domyślić, dlaczego tak jest. Być może niektóre kody pocztowe mają niską liczbę ludności, ale wysokie spożycie, ponieważ zawierają jedyny sklep z alkoholem w danym regionie. Jeśli tak, obliczenie na podstawie populacji z okolicznych kodów pocztowych może wyrównać te wartości odstające.

6. Porównanie rodzajów sprzedawanego alkoholu

Oprócz danych geograficznych baza danych o sprzedaży detalicznej alkoholu w stanie Iowa zawiera również szczegółowe informacje o sprzedawanych produktach. Być może dzięki ich analizie uda się nam odkryć różnice w preferencjach w różnych obszarach geograficznych.

Odkrywaj kategorie

Produkty są kategoryzowane w bazie danych. Ile jest kategorii?

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()

Oczekiwany wynik: 103

Które kategorie są najpopularniejsze pod względem liczby wyświetleń?

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)

Wykres słupkowy przedstawiający najpopularniejsze kategorie alkoholu

Praca z typem danych TABELA

Istnieje kilka kategorii whisky, rumu, wódki itp. Chcę je jakoś pogrupować.

Zacznij od podzielenia nazw kategorii na osobne słowa za pomocą metody Series.str.split(). Rozwiń tablicę utworzoną za pomocą metody 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)

Słowa według liczby w kategoriach

category_parts.nunique()

Oczekiwany wynik: 113

Na wykresie powyżej widać, że dane dotyczące VODKA są nadal oddzielone od danych dotyczących VODKAS. Aby zwijać kategorie w mniejszy zbiór, musisz utworzyć więcej grup.

7. Korzystanie z pakietu NLTK w przypadku obiektów BigQuery DataFrames

Przy zaledwie około 100 kategorii można napisać pewne heurystyki lub nawet ręcznie utworzyć mapowanie kategorii na szerszy typ alkoholu. Można też użyć dużego modelu językowego, takiego jak Gemini, do utworzenia takiego mapowania. Aby używać BigQuery DataFrames z Gemini, skorzystaj z Codelab Pozyskiwanie informacji z nieuporządkowanych danych za pomocą BigQuery DataFrames.

Zamiast tego do przetwarzania tych danych użyj bardziej tradycyjnego pakietu do przetwarzania języka naturalnego, np. NLTK. Technologia zwana „stemmingiem” może łączyć rzeczowniki w liczbie mnogiej i pojedynczej w jedną wartość.

Używanie pakietu NLTK do tworzenia podstawy słowotwórczej

Pakiet NLTK udostępnia metody przetwarzania języka naturalnego, które można wywołać z Pythona. Zainstaluj pakiet, aby go wypróbować.

%pip install nltk

Następnie zaimportuj pakiet. Sprawdź wersję. Użyjesz go w dalszej części samouczka.

import nltk

nltk.__version__

Jednym ze sposobów standaryzacji słów jest ich „odszczupywanie”. Spowoduje to usunięcie wszystkich przyrostków, np. „s” w przypadku liczby mnogiej.

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)

Wypróbuj to na kilku słowach.

stem("WHISKEY")

Oczekiwany wynik: whiskey

stem("WHISKIES")

Oczekiwany wynik: whiski

Niestety nie udało się zmapować whisky na whisky. Rozdzielacze nie działają dobrze w przypadku nieregularnych form liczby mnogiej. Użyj lemmatizera, który wykorzystuje bardziej zaawansowane techniki do identyfikowania słowa podstawowego, zwanego „lemma”.

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())

Wypróbuj to na kilku słowach.

lemmatize("WHISKIES")

Oczekiwany wynik: whisky

lemmatize("WHISKY")

Oczekiwany wynik: whisky

lemmatize("WHISKEY")

Oczekiwany wynik: whiskey

Niestety ten lemmatizer nie mapuje słowa „whiskey” do tego samego lemmatu co słowo „whiskies”. Ponieważ to słowo jest szczególnie ważne w przypadku bazy danych o sprzedaży alkoholu w stanie Iowa, ręcznie zastosuj do niego pisownię amerykańską, korzystając z słownika.

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)

Wypróbuj to na kilku słowach.

lemmatize("WHISKIES")

Oczekiwany wynik: whiskey

lemmatize("WHISKEY")

Oczekiwany wynik: whiskey

Gratulacje! Ten lematyzator powinien dobrze działać w przypadku zawężania kategorii. Aby korzystać z niego w BigQuery, musisz go wdrożyć w chmurze.

Konfigurowanie projektu na potrzeby wdrażania funkcji

Zanim wdrożysz tę funkcję w chmurze, aby BigQuery mogła uzyskać do niej dostęp, musisz wykonać jednorazową konfigurację.

Utwórz nową komórkę kodu i zamień tekst your-project-id na identyfikator projektu Google Cloud, którego używasz w tym samouczku.

project_id = "your-project-id"

Utwórz konto usługi bez żadnych uprawnień, ponieważ ta funkcja nie wymaga dostępu do żadnych zasobów w chmurze.

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)

Oczekiwany wynik: bigframes-no-permissions@your-project-id.iam.gserviceaccount.com

Utwórz zbiór danych BigQuery, który będzie przechowywać funkcję.

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)

Wdrażanie funkcji zdalnej

Włącz interfejs Cloud Functions API, jeśli nie jest jeszcze włączony.

!gcloud services enable cloudfunctions.googleapis.com

Teraz wdróż funkcję w utworzonym właśnie zbiorze danych. Dodaj dekorator @bpd.remote_function do funkcji utworzonej w poprzednich krokach.

@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)

Wdrożenie powinno potrwać około 2 minut.

Korzystanie z funkcji pilota

Po zakończeniu wdrażania możesz przetestować tę funkcję.

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

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

Oczekiwane dane wyjściowe:

0       whiskey
1       whiskey
2       whiskey
3       vodka
4       vodka

dtype: string

8. Porównywanie spożycia alkoholu według powiatu

Teraz, gdy funkcja lemmatize jest dostępna, możesz jej używać do łączenia kategorii.

znalezienie słowa, które najlepiej podsumowuje kategorię;

Najpierw utwórz DataFrame wszystkich kategorii w bazie danych.

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()

Oczekiwane dane wyjściowe:

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

Następnie utwórz DataFrame ze wszystkimi słowami w kategoriach, z wyjątkiem kilku słów zastępczych, takich jak znaki interpunkcyjne i „item”.

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()

Oczekiwane dane wyjściowe:

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

Pamiętaj, że dzięki lemmatyzacji po zgrupowaniu zmniejszasz obciążenie funkcji w Cloud Functions. Funkcję lemmatize można zastosować do każdego z kilku milionów wierszy w bazie danych, ale będzie to droższe niż zastosowanie jej po zgrupowaniu i może wymagać zwiększenia limitu.

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

Oczekiwane dane wyjściowe:

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

Teraz, gdy słowa zostały zlemmatyzowane, musisz wybrać lemmat, który najlepiej podsumowuje kategorię. W kategoriach nie ma zbyt wielu słów funkcyjnych, dlatego zastosuj heurystyk, że jeśli słowo pojawia się w wielu innych kategoriach, prawdopodobnie lepiej nadaje się na słowo podsumowujące (np. whiskey).

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()

Oczekiwane dane wyjściowe:

        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

Teraz, gdy mamy jedną lemmę podsumowującą każdą kategorię, scal ją z pierwotnym DataFrame.

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

Oczekiwane dane wyjściowe:

        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

Porównywanie powiatów

Porównaj sprzedaż w poszczególnych powiatach, aby zobaczyć, jakie są różnice.

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"))
)

Znajdź najlepiej sprzedający się produkt (lemma) w każdym kraju.

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()

Oczekiwane dane wyjściowe:

        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

Czym różnią się od siebie poszczególne hrabstwa?

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

Oczekiwane dane wyjściowe:

lemma   
american        1
liqueur 1
vodka   15
whiskey 83

dtype: Int64

W większości hrabstw whisky jest najpopularniejszym produktem pod względem objętości, a w 15 hrabstwach najpopularniejsza jest wódka. Porównaj to z najpopularniejszymi rodzajami alkoholu w całym stanie.

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

Oczekiwane dane wyjściowe:

        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
...

Whisky i wódka mają prawie taką samą objętość, przy czym wódka jest nieco mocniejsza niż whisky w całym stanie.

Porównywanie proporcji

Co jest wyjątkowego w sprzedaży w każdym hrabstwie? Co wyróżnia ten powiat na tle reszty stanu?

Użyj wskaźnika h według Cohena, aby sprawdzić, które wolumeny sprzedaży alkoholu różnią się najbardziej proporcjonalnie od wartości oczekiwanych na podstawie proporcji sprzedaży w całym stanie.

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"))
)

Teraz, gdy dla każdego leksymu zmierzono współczynnik h Cohena, znajdź w każdym hrabstwie największą różnicę w porównaniu z proporcją w całym stanie.

# 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()

Oczekiwane dane wyjściowe:

        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

Im większa wartość współczynnika h Cohena, tym większe prawdopodobieństwo, że ilość spożywanego alkoholu danego typu różni się istotnie statystycznie od średniej w danym stanie. W przypadku mniejszych wartości dodatnich różnica w konsumpcji różni się od średniej w całym stanie, ale może być spowodowana przypadkowymi różnicami.

Uwaga: hrabstwo El Paso nie jest hrabstwem w stanie Iowa, co może oznaczać, że przed pełnym poleganiem na tych wynikach należy jeszcze wyczyścić dane.

Wizualizacja hrabstw

Złącz z tabelą bigquery-public-data.geo_us_boundaries.counties, aby uzyskać obszar geograficzny dla każdego powiatu. Nazwy hrabstw nie są unikalne w Stanach Zjednoczonych, dlatego filtruj tak, aby uwzględnić tylko hrabstwa z Iowa. Kod FIPS stanu Iowa to „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

Oczekiwane dane wyjściowe:

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

Użyj pakietu GeoPandas, aby zwizualizować te różnice na mapie.

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,
)

Mapa alkoholu, który najbardziej różni się od proporcji sprzedaży w poszczególnych stanach

9. Czyszczenie danych

Jeśli na potrzeby tego samouczka został przez Ciebie utworzony nowy projekt Google Cloud, możesz go usunąć, aby uniknąć dodatkowych opłat za utworzone tabele lub inne zasoby.

Możesz też usunąć funkcje w Cloud Functions, konta usług i zbiory danych utworzone w ramach tego samouczka.

10. Gratulacje!

Oczyszczone i przeanalizowane dane uporządkowane za pomocą ramek danych BigQuery. W trakcie tej podróży poznaliśmy publiczne zbiory danych Google Cloud, notatniki Pythona w BigQuery Studio, uczenie maszynowe BigQuery, funkcje zdalne BigQuery oraz możliwości BigQuery DataFrames. Świetna robota!

Dalsze kroki