<jupyter_start><jupyter_text>Exploratory Data Analysis<jupyter_code>%load_ext lab_black
%load_ext autoreload
%autoreload 2<jupyter_output><empty_output><jupyter_text>Import Python modules<jupyter_code>import os
from datetime import datetime
from glob import glob

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.colors import LogNorm
from matplotlib.ticker import MaxNLocator
sns.set_context("talk")<jupyter_output><empty_output><jupyter_text>Import any custom modules<jupyter_code>%aimport src.utils
from src.utils import (
    show_df,  # Display first and last n rows of a DataFrame
    show_df_dtypes_nans,  # Show the missing values and column datatypes side-by-side
)<jupyter_output><empty_output><jupyter_text>[Table of Contents](table-of-contents)0. [About](about)1. [User Inputs](user-inputs)2. [Load processed and combined dataset of search results and listings](load-processed-and-combined-dataset-of-search-results-and-listings)   - 2.1. [Load data](load-data)3. [Cleaning](cleaning)   - 3.1. [Publisher](publisher)   - 3.2. [Rating](rating)   - 3.3. [Prices](prices)   - 3.4. [Date Columns](date-columns)4. [Exploratory Data Analysis](exploratory-data-analysis)   - 4.1. [Early Access](early-access)   - 4.2. [Platforms](platforms)   - 4.3. [Franchises](franchises)   - 4.4. [Publishers and Platforms](publishers-and-platforms)   - 4.5. [Publishers and Early Access listings](publishers-and-early-access-listings)   - 4.6. [Publishers and Multi-Language Support](publishers-and-multi-language-support)   - 4.7. [Developers](developers)   - 4.8. [Digital Rights Management](digital-rights-management)   - 4.9. [Genres and User-Defined Tags](genres-and-user-defined-tags)   - 4.10. [Ratings](ratings)   - 4.11. [Publishers](publishers)   - 4.12. [User Rating](user-rating)   - 4.13. [Listing Prices](listing-prices)   - 4.14. [Listing Prices over time](listing-prices-over-time)  0. [About](about) In this notebook, we'll explore the processed dataset created by merging the scraped search results with the scraped listings. Recall that only the dataset acquired using the `requests` library is being considered for analysis. See section 4, of `6_merge_searches_listings.ipynb` for details about this.  1. [User Inputs](user-inputs) Define variables that can be changed when running this notebook<jupyter_code>PROJ_ROOT_DIR = os.getcwd()
proc_data_filename = "processed_data.csv"
# Path to data
data_dir = os.path.join(PROJ_ROOT_DIR, "data")

# Path to data/processed
processed_data_dir = os.path.join(data_dir, "processed")

# Path to processed data file
processed_data_filepath = os.path.join(processed_data_dir, proc_data_filename)

# Path to reports directory for saving plots
reports_data_dir = os.path.join(PROJ_ROOT_DIR, "reports", "figures")

# Define list of months for heatmap
months = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
]<jupyter_output><empty_output><jupyter_text>2. [Load processed and combined dataset of search results and listings](load-processed-and-combined-dataset-of-search-results-and-listings)  2.1. [Load data](load-data) We'll begin by loading the processed data that was exported in `6_merge_searches_listings.ipynb` to a CSV file. Recall that this is a combination of the scraped search results and listings datasets so there are more columns here than in either of those two datasets<jupyter_code>%%time
df = pd.read_csv(processed_data_filepath)
print(len(df))
show_df(df, 1)
show_df_dtypes_nans(df)<jupyter_output>10035<jupyter_text>3. [Cleaning](cleaning)  3.1. [Publisher](publisher) The `Publisher` column consists of comma-separated strings. Replace this by a list where each element in the list is a listing publisher.<jupyter_code>show_df(
    df["Publisher"]
    .value_counts(dropna=False)
    .reset_index()
    .rename(columns={"Publisher": "num_listings", "index": "Publisher"}),
    5,
)<jupyter_output><empty_output><jupyter_text>Clean listings such as `ABC,Inc.` (replace by `ABC Inc.`) so that we can split unique publishers on commas- considers `NaN` or `(none)` to be zero publishers<jupyter_code>df["Publisher_cleaned"] = (
    df["Publisher"]
    .fillna("(none)")
    .str.replace("(none)", "None", regex=False)
    .str.lower()
    .str.replace(", i", " i")
    .str.replace(", l", " l")
    .str.replace(",i", " i")
    .str.replace(",l", " l")
)
show_df(df[["Publisher", "Publisher_cleaned"]], 10)<jupyter_output><empty_output><jupyter_text>3.2. [Rating](rating)<jupyter_code>df_ratings = (
    df["rating"]
    .replace(
        {
            "t": "Teen",
            "e": "Everyone",
            "m": "Mature",
            "rp": "Pending",
            "ec": "Early Childhood",
            "e10": "10 and Older",
            "ao": "Adults Only (18+)",
        }
    )
    .str.title()
    .fillna("No Rating")
    .value_counts(dropna=False)
    .reset_index()
    .rename(columns={"rating": "num_listings", "index": "rating"})
)
display(df_ratings)<jupyter_output><empty_output><jupyter_text>3.3. [Prices](prices)<jupyter_code># df_price_rating = df.dropna(subset=["original_price", "discount_pct"]).copy()
# df_price_rating.loc[
#     df_price_rating["original_price"].str.lower().str.contains("free"), "original_price"
# ] = 0<jupyter_output><empty_output><jupyter_text>Discount percent<jupyter_code>df["discount_pct_cleaned"] = (
    df["discount_pct"].str.replace("%", "").str.replace("-", "").astype(float)
)<jupyter_output><empty_output><jupyter_text>Original price<jupyter_code>df["original_price_cleaned"] = df["original_price"]
df["original_price_cleaned"] = df["original_price_cleaned"].fillna("0")
df.loc[
    df["original_price_cleaned"].fillna("0").str.contains("Free|Demo", case=False),
    "original_price_cleaned",
] = "0"
assert df["original_price_cleaned"][
    df["original_price_cleaned"].fillna("0").str.contains("Free|Demo", case=False)
].empty
df["original_price_cleaned"] = df["original_price_cleaned"].astype(float)
display(df[["original_price", "original_price_cleaned"]].sample(15))<jupyter_output><empty_output><jupyter_text>3.4. [Date Columns](date-columns)<jupyter_code>show_df(df[["Release Date", "Early Access Release Date"]], 5)
%%time
df["release_date_cleaned"] = pd.to_datetime(
    df["Release Date"].replace("Coming Soon", np.nan, regex=True)
)
%%time
df["early_access_release_date_cleaned"] = pd.to_datetime(df["Early Access Release Date"])<jupyter_output>CPU times: user 45.3 ms, sys: 0 ns, total: 45.3 ms
Wall time: 44.6 ms<jupyter_text>4. [Exploratory Data Analysis](exploratory-data-analysis)  4.1. [Early Access](early-access) **Plot the fraction of listings that is offers early access** Calculate the fraction of listings that offers early access- if a missing value appears in this column, then we'll consider it to be missing support for early access<jupyter_code>print(
    f"Fraction of games that offered early access = "
    f"{(1 - (df['Early Access Release Date'].isna().sum() / len(df))) * 100:.2f}"
)<jupyter_output>Fraction of games that offered early access = 10.92<jupyter_text>Another approach<jupyter_code>((len(df) - df["Early Access Release Date"].isna().sum()) / len(df)) * 100<jupyter_output><empty_output><jupyter_text>Get a `DataFrame` with the fraction of early-access and regular listings<jupyter_code>(~df["Early Access Release Date"].isna()).value_counts(normalize=True).reset_index()
df_early_release = (
    (~df["Early Access Release Date"].isna()).value_counts(normalize=True) * 100
).reset_index()
df_early_release["index"] = (
    df_early_release["index"].replace(True, "Early Release").replace(False, "Regular")
)
display(df_early_release)<jupyter_output><empty_output><jupyter_text>Plot the number of listings that support and do not support early access<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        x="index",
        y="Early Access Release Date",
        data=df_early_release,
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title(
        "Fraction of Listings by Accessibility Options", loc="left", fontweight="bold"
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "1_access_time.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>4.2. [Platforms](platforms) **What fraction of games are supported on all platforms?**<jupyter_code>platforms_wanted = ["win", "mac", "linux"]
for platform in platforms_wanted:
    df[platform] = df["platforms"].str.contains(platform)
all_platforms = df[(df["linux"] == True) & ((df["mac"] == True)) & (df["win"] == True)]
print(
    f"Fraction of games with support on all platforms = "
    f"{(len(all_platforms) / len(df)) * 100:.2f}"
)<jupyter_output>Fraction of games with support on all platforms = 20.80<jupyter_text>**Plot the fraction of all listings that are supported on each platform?** Reshape the original data, using the added columns (`win`, `mac`, `linux`)<jupyter_code>listings_platforms_reshaped = (
    df[["win", "mac", "linux", "url"]]
    .set_index("url")
    .unstack()
    .reset_index()
    .rename(columns={0: "has_support", "level_0": "platform"})
)
listings_platforms_reshaped = listings_platforms_reshaped[
    listings_platforms_reshaped["has_support"] == True
]
display(listings_platforms_reshaped.head(10))<jupyter_output><empty_output><jupyter_text>Get the number of listings that are either listed as unsupported on a given platform or for which there is a missing value for the same platform<jupyter_code>unsupported_missing_val = {}
for p in ["win", "mac", "linux"]:
    unsupported_missing_val[p] = df[df[p] == False].shape[0] + df[p].isna().sum()
print(unsupported_missing_val)
df_unsupported_missing_val = pd.DataFrame.from_dict(
    unsupported_missing_val, orient="index"
).reset_index()
df_unsupported_missing_val.columns = ["platform", "num_unsupported_and_missing"]
display(df_unsupported_missing_val)<jupyter_output><empty_output><jupyter_text>Get the number of listings supported by a given platform<jupyter_code>df_supported = (
    listings_platforms_reshaped.groupby("platform")["url"]
    .count()
    .reset_index()
    .rename(columns={"url": "num_listings"})
    .assign(num_rows=len(df))
)<jupyter_output><empty_output><jupyter_text>Merge number of listings supported and those for which support is not available or unlisted, by platform<jupyter_code>df_supported = df_supported.merge(
    df_unsupported_missing_val, on="platform", how="inner"
)<jupyter_output><empty_output><jupyter_text>Calculate the fraction of supported adn unsupported/unlisted listings as a percentage<jupyter_code>df_supported["pct_support"] = (
    df_supported["num_listings"] / df_supported["num_rows"]
) * 100
df_supported["platform"] = (
    df_supported["platform"]
    .str.title()
    .str.replace("Mac", "Mac OS")
    .str.replace("Win", "Windows")
)
display(df_supported)<jupyter_output><empty_output><jupyter_text>Plot fractions on a chart<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="pct_support",
        x="platform",
        data=df_supported.sort_values(by="pct_support", ascending=False),
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title("Fraction of Listings Supported", loc="left", fontweight="bold")
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "2_platforms.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>4.3. [Franchises](franchises) **What fraction of games are part of by a Franchise?**<jupyter_code>def calculate_frac_part_of_franchise(df):
    no_franchise = (len(df) - df["Franchise"].isna().sum()) / len(df)
    print(
        f"Number of games part of a Franchise = {100 * no_franchise:.2f}\n"
        f"Number of games not part of a franchise = {100*(1 - no_franchise):.2f}"
    )
calculate_frac_part_of_franchise(df)<jupyter_output>Number of games part of a Franchise = 33.80
Number of games not part of a franchise = 66.20<jupyter_text>**What fraction of games on each platform are part of a Franchise?**<jupyter_code>calculate_frac_part_of_franchise(df[df["win"] == True])
calculate_frac_part_of_franchise(df[df["mac"] == True])
calculate_frac_part_of_franchise(df[df["linux"] == True])<jupyter_output>Number of games part of a Franchise = 31.79
Number of games not part of a franchise = 68.21<jupyter_text>4.4. [Publishers and Platforms](publishers-and-platforms) **What fraction of Linux games are offered through a publisher? What is the fraction without a publisher?**<jupyter_code>linux_by_pubs = len(df[(df["linux"] == True) & (~df["Publisher"].isna())])
print(
    "Fraction of listings by a publisher with Linux support = "
    f"{(linux_by_pubs / len(df)) * 100:.3f}"
)
linux_no_pubs = len(df[(df["linux"] == True) & (df["Publisher"].isna())])
print(
    "Fraction of listings without a publisher with Linux support = "
    f"{(linux_no_pubs / len(df)) * 100:.3f}"
)<jupyter_output>Fraction of listings by a publisher with Linux support = 23.229
Fraction of listings without a publisher with Linux support = 0.090<jupyter_text>**What fraction of all games on all platforms are offered through a publisher? What is the fraction without a publisher?** Calculate the fraction of listings by platform with and without a publisher- with a publisher  - the `Publisher` column is not missing a value- without a publisher  - the `Publisher` column is missing a value<jupyter_code>d_platform_pub = {}
for p in ["win", "mac", "linux"]:
    d_platform_pub[p + "__True"] = (
        len(df[(df[p] == True) & (~df["Publisher"].isna())]) / len(df)
    ) * 100
    d_platform_pub[p + "__False"] = (
        len(df[(df[p] == True) & (df["Publisher"].isna())]) / len(df)
    ) * 100
df_platform_pub = (
    pd.DataFrame.from_dict(d_platform_pub, orient="index")
    .reset_index()
    .rename(columns={0: "pct_of_all_listings"})
)
display(df_platform_pub)<jupyter_output><empty_output><jupyter_text>Split the `index` column into `platform` and `publisher`<jupyter_code>df_platform_pub[["platform", "publisher"]] = df_platform_pub["index"].str.split(
    "__", expand=True
)
df_platform_pub["publisher"] = df_platform_pub["publisher"].map(
    {"True": "Yes", "False": "No"}
)
df_platform_pub["platform"] = (
    df_platform_pub["platform"]
    .str.title()
    .str.replace("Mac", "Mac OS")
    .str.replace("Win", "Windows")
)
display(df_platform_pub)<jupyter_output><empty_output><jupyter_text>Plot the fractions of listings through a publisher, by platform<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.catplot(
        data=df_platform_pub.sort_values(by="publisher"),
        kind="bar",
        x="platform",
        y="pct_of_all_listings",
        hue="publisher",
        palette="husl",
        alpha=0.9,
        height=6,
        saturation=0.25,
    )
    ax.despine(left=True)
    ax = plt.gca()
    ax.set_title("Fraction of Listings", loc="left", fontweight="bold")
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "3_platforms_publishers.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>The ratio of istings in each platform going through a publisher to those not going through one is shown below<jupyter_code>d_relative_pub_no_pub = {}
for p in ["Windows", "Mac OS", "Linux"]:
    thru_pubs = df_platform_pub[
        (df_platform_pub["platform"] == p) & (df_platform_pub["publisher"] == "Yes")
    ]["pct_of_all_listings"].iloc[0]
    no_pubs = df_platform_pub[
        (df_platform_pub["platform"] == p) & (df_platform_pub["publisher"] == "No")
    ]["pct_of_all_listings"].iloc[0]
    d_relative_pub_no_pub[p + "_ratio__pub_no_pub"] = thru_pubs / no_pubs
df_relative_pub_no_pub = pd.DataFrame.from_dict(d_relative_pub_no_pub, orient="index").T
display(df_relative_pub_no_pub)<jupyter_output><empty_output><jupyter_text>**Observations**1. Linux-compatible games require expertise due to the dearth of drivers/software to allow a Linux system to support a game. This expertise is more likely to be found at a publishing company than through a standalone developer.  4.5. [Publishers and Early Access listings](publishers-and-early-access-listings)<jupyter_code>print(df["Publisher"].isna().sum())<jupyter_output>36<jupyter_text>**What fraction of early-access games are offered through a publisher? What is the fraction without a publisher?** Count the number of listings with early access with and without a publisher- early access, without a publisher  - the `Publisher` column is missing a value  - the `Early Access...` column is not missing a value- early access, with a publisher  - the `Publisher` column is not missing a value  - the `Early Access...` column is not missing a value- no early access, without a publisher  - the `Publisher` column is missing a value  - the `Early Access...` column is missing a value- no early access, with a publisher  - the `Publisher` column is not missing a value  - the `Early Access...` column is missing a value<jupyter_code>early_access_no_pubs = len(
    df[(~df["Early Access Release Date"].isna()) & (df["Publisher"].isna())]
)
early_access_pubs = len(
    df[(~df["Early Access Release Date"].isna()) & (~df["Publisher"].isna())]
)
no_early_access_no_pubs = len(
    df[(df["Early Access Release Date"].isna()) & (df["Publisher"].isna())]
)
no_early_access_pubs = len(
    df[(df["Early Access Release Date"].isna()) & (~df["Publisher"].isna())]
)<jupyter_output><empty_output><jupyter_text>Convert each count of listings to a fraction<jupyter_code>d_no_early_acc_pubs = {
    "no_early_access__no_pubs": (no_early_access_no_pubs / len(df)) * 100,
    "no_early_access__pubs": (no_early_access_pubs / len(df)) * 100,
}
d_early_acc_pubs = {
    "early_access__no_pubs": (early_access_no_pubs / len(df)) * 100,
    "early_access__pubs": (early_access_pubs / len(df)) * 100,
}<jupyter_output><empty_output><jupyter_text>Put all fractions into `DataFrame`s<jupyter_code>df_no_early_acc_pubs = (
    pd.DataFrame.from_dict(d_no_early_acc_pubs, orient="index")
    .reset_index()
    .rename(columns={0: "pct_of_all_listings"})
)
df_early_acc_pubs = (
    pd.DataFrame.from_dict(d_early_acc_pubs, orient="index")
    .reset_index()
    .rename(columns={0: "pct_of_all_listings"})
)
display(df_no_early_acc_pubs)
display(df_early_acc_pubs)<jupyter_output><empty_output><jupyter_text>Split `index` column of `DataFrame`s<jupyter_code>df_no_early_acc_pubs[["early_access", "publisher"]] = df_no_early_acc_pubs[
    "index"
].str.split("__", expand=True)
df_early_acc_pubs[["early_access", "publisher"]] = df_early_acc_pubs["index"].str.split(
    "__", expand=True
)
display(df_no_early_acc_pubs)
display(df_early_acc_pubs)<jupyter_output><empty_output><jupyter_text>Vertically concatenate `DataFrame`s<jupyter_code>df_early_no_early_acc_pubs = pd.concat([df_early_acc_pubs, df_no_early_acc_pubs])
df_early_no_early_acc_pubs["early_access"] = df_early_no_early_acc_pubs[
    "early_access"
].map({"early_access": "Early Access", "no_early_access": "No Early Access"})
df_early_no_early_acc_pubs["publisher"] = df_early_no_early_acc_pubs["publisher"].map(
    {"pubs": "Yes", "no_pubs": "No"}
)
display(df_early_no_early_acc_pubs)<jupyter_output><empty_output><jupyter_text>Plot fractions<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.catplot(
        data=df_early_no_early_acc_pubs,
        kind="bar",
        x="early_access",
        y="pct_of_all_listings",
        hue="publisher",
        palette="husl",
        alpha=0.9,
        height=6,
        saturation=0.25,
    )
    ax.despine(left=True)
    # ax.set_axis_labels("", "Early Access Listings through a Publisher")
    ax = plt.gca()
    ax.set_title("Fraction of Listings", loc="left", fontweight="bold")
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "4_publisher_early_release.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>(OPTIONAL) Calculate the ratio of listings with a publisher to those without a publisher, depending on whether or not they support early-access<jupyter_code>pub_no_early = (df_early_no_early_acc_pubs["publisher"] == "Yes") & (
    df_early_no_early_acc_pubs["early_access"] == "No Early Access"
)
pub_early = (df_early_no_early_acc_pubs["publisher"] == "Yes") & (
    df_early_no_early_acc_pubs["early_access"] == "Early Access"
)
no_pub_no_early = (df_early_no_early_acc_pubs["publisher"] == "No") & (
    df_early_no_early_acc_pubs["early_access"] == "No Early Access"
)
no_pub_early = (df_early_no_early_acc_pubs["publisher"] == "No") & (
    df_early_no_early_acc_pubs["early_access"] == "Early Access"
)

d_relative_pub_no_pub_for_early_acc = {
    "No Early Access": (
        df_early_no_early_acc_pubs[pub_no_early]["pct_of_all_listings"].iloc[0]
        / df_early_no_early_acc_pubs[no_pub_no_early]["pct_of_all_listings"].iloc[0]
    ),
    "Early Access": (
        df_early_no_early_acc_pubs[pub_early]["pct_of_all_listings"].iloc[0]
        / df_early_no_early_acc_pubs[no_pub_early]["pct_of_all_listings"].iloc[0]
    ),
}
df_relative_pub_no_pub_for_early_acc = pd.DataFrame.from_dict(
    d_relative_pub_no_pub_for_early_acc, orient="index"
).rename(columns={0: "ratio_pub__no_pub"})
display(df_relative_pub_no_pub_for_early_acc)<jupyter_output><empty_output><jupyter_text>4.6. [Publishers and Multi-Language Support](publishers-and-multi-language-support) **What fraction of multi-language supported listings are offered through a publisher? What is the fraction without going through a publisher?**<jupyter_code>multi_lang_pubs = len(df[(df["num_languages"] > 1) & (~df["Publisher"].isna())])
print(
    "Fraction of listings with multi-language support that are "
    f"offered by a publisher = {(multi_lang_pubs / len(df)) * 100:.2f}"
)
multi_lang_no_pubs = len(df[(df["num_languages"] > 1) & (df["Publisher"].isna())])
print(
    "Fraction of listings with multi-language support that are not "
    f"offered by a publisher = {(multi_lang_no_pubs / len(df)) * 100:.2f}"
)<jupyter_output>Fraction of listings with multi-language support that are not offered by a publisher = 0.24<jupyter_text>4.7. [Developers](developers) **Show a table with each of the top 15 developers by the number of listings they support on Windows. Include the number of their supported listings on mac and linux as well. For any occurrences where multiple developers collaborated, consider each separately.** For every listing, get the developer and a separate row showing each supported platform<jupyter_code>def extract_platforms_counts(data):
    return pd.DataFrame.from_dict(
        {
            "win_support": (data["platform"] == "win").sum(),
            "mac_support": (data["platform"] == "mac").sum(),
            "linux_support": (data["platform"] == "linux").sum(),
        },
        orient="index",
    ).T
%%time
df_platforms = (
    df[[p for p in platforms_wanted] + ["url", "Developer"]]
    .set_index(["url", "Developer"])
    .stack()
    .reset_index()
    .rename(columns={"level_2": "platform", 0: "support"})
    .astype({"Developer": "str"})
    .sort_values(by=["url"], ascending=True)
    .query("support == True")
)
show_df(df_platforms, 5)<jupyter_output><empty_output><jupyter_text>Aggregate over each developer and count the number of their listings with support for each of the platforms (sorting results based on the number Windows listings)<jupyter_code>%%time
df_platforms_by_developer = (
    df_platforms.groupby("Developer").apply(
        extract_platforms_counts
    ).reset_index(level=1, drop=True).nlargest(15, "win_support")
)
show_df(df_platforms_by_developer, 5)<jupyter_output><empty_output><jupyter_text>4.8. [Digital Rights Management](digital-rights-management) **What fraction of listings doesn't involve a DRM?** We'll assume that a missing value in the DRM column indicates that a DRM is not required<jupyter_code>drm_missing_frac = (df["drm"].isna().sum() / len(df)) * 100
print(f"Fraction of listings without a DRM = {drm_missing_frac:.2f}")<jupyter_output>Fraction of listings without a DRM = 88.23<jupyter_text>**Observations**1. Some games are free of DRMs ([1](https://www.digitaltrends.com/gaming/what-is-drm-in-video-games/)), but taking missing values in this column as being valid results in quite a large fraction of listings that don't require a DRM. We likely can't draw too many conclusions about this dataset that involves use of this column. **What are the top 10 Genres that includes a DRM?**<jupyter_code>drm_reqd = df[df["drm"].isna()]
listings_by_genre = (
    drm_reqd.groupby("Genre", as_index=False)["url"].count().nlargest(10, "url")
)
show_df(listings_by_genre)<jupyter_output><empty_output><jupyter_text>4.9. [Genres and User-Defined Tags](genres-and-user-defined-tags) **What fraction of listings are missing a Genre? a user-defined tag?**<jupyter_code>genre_missing = (df["Genre"].isna().sum() / len(df)) * 100
tag_missing = (df["user_defined_tags"].isna().sum() / len(df)) * 100
print(
    f"Fraction of listings without a Genre = {genre_missing:.2f}, "
    f"without a User-Defined Tag = {tag_missing:.2f}"
)<jupyter_output>Fraction of listings without a Genre = 0.12, without a User-Defined Tag = 0.00<jupyter_text>**How many Genres are covered by the retrieved listings?**<jupyter_code>print(f"Number of unique Genres in the scraped data = {df['Genre'].nunique():,}")<jupyter_output>Number of unique Genres in the scraped data = 660<jupyter_text>**Reshape the data to have all Genres as separate rows for a given listing** Create a [messy dataset](https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html) showing URL and Genre- for each listing, this will split all the genres from a single row to having a single genre per row<jupyter_code>%%time
df_genres_messy = (
    df["Genre"]
    .str.split(", ", expand=True)
    .assign(url=df["url"])
    .set_index("url")
    .unstack()
    .reset_index()
    .drop(columns=["level_0"])
    .dropna()
    .rename(columns={0: "Genre"})
)
show_df(df_genres_messy, 5)<jupyter_output><empty_output><jupyter_text>Aggregate over Genre and count the number of listings<jupyter_code>%%time
df_genres_messy_grouped = (
    df_genres_messy.groupby("Genre", as_index=False)["url"]
    .count()
    .sort_values(by="url", ascending=False)
)
display(df_genres_messy_grouped)<jupyter_output><empty_output><jupyter_text>Plot the aggregation output<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="Genre",
        x="url",
        data=df_genres_messy_grouped,
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title("Popularity of Fun and Adventure Games", loc="left", fontweight="bold")
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "5_genres.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>**Count the number of listings with exactly 20 tags (the maximum allowed on the Steam store) and less than 20 tags.** Reshape the data to have all tags as separate rows for a given listing. Get a list of all unique tags for a given listing<jupyter_code>%%time
df["num_tags"] = (
    df["user_defined_tags"]
    .dropna()
    .str.split(", ", expand=False)
    .apply(lambda x: len(x))
)
show_df(df[["user_defined_tags", "num_tags"]], 5)<jupyter_output><empty_output><jupyter_text>Count the number of listins with 20 or less than 20 tags<jupyter_code>%%time
df_numtags_lt_twenty = (df["num_tags"] < 20).value_counts().reset_index()
df_numtags_lt_twenty["index"] = df_numtags_lt_twenty["index"].map(
    {True: "Less than 20", False: "20 (max. allowed)"}
)
show_df(df_numtags_lt_twenty)<jupyter_output><empty_output><jupyter_text>Plot the number of listings with 20 or less than 20 tags<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="num_tags",
        x="index",
        data=df_numtags_lt_twenty,
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title("Large number of listings with 20 tags", loc="left", fontweight="bold")
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "6_user_defined_tags.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>**Observations**1. A Steam listing can be assigned a [minimum of 5 tags and a maximum of 20](https://www.askgamedev.com/everything-we-know-about-steam-tags-2020/). Plot the distribution of the number of tags<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.histplot(
        data=df[["num_tags"]],
        x="num_tags",
        log_scale=(False, True),
        color="steelblue",
    )
    ax.set_title(
        "Max. allowed user-defined tags assigned to listings",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(10)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "7_distribution_of_num_tags.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>4.10. [Ratings](ratings) **What fraction of listings are missing a rating?**<jupyter_code>rating_missing = (df["rating"].isna().sum() / len(df)) * 100
print(f"Fraction of listings without a rating = {rating_missing:.2f}")<jupyter_output>Fraction of listings without a rating = 76.59<jupyter_text>**What are the possible ratings?** We'll assign more human-readable names to the ratings<jupyter_code>ratings_mapper = {
    "t": "Teen",
    "e": "Everyone",
    "m": "Mature",
    "rp": "Pending",
    "ec": "Early Childhood",
    "e10": "10 and Older",
    "ao": "Adults Only (18+)",
}
%%time
df_ratings = (
    df["rating"]
    .replace(ratings_mapper)
    .str.title()
    .fillna("No Rating")
    .value_counts(dropna=False)
    .reset_index()
    .rename(columns={"rating": "num_listings", "index": "rating"})
)
show_df(df_ratings)<jupyter_output><empty_output><jupyter_text>Plot the number of listings by rating<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="rating",
        x="num_listings",
        data=df_ratings,
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title(
        "Prevalence of missing rating among listings",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(processed_data_dir, "8_listing_ratings.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>**What is the distribution of rating descriptors?** Create a messy dataset showing URL and `rating_descriptors`<jupyter_code>df_rd_messy = (
    df["rating_descriptors"]
    .str.replace(", ,", ",")
    .str.split(", ", expand=True)
    .assign(url=df["url"])
    .set_index("url")
    .unstack()
    .reset_index()
    .drop(columns=["level_0"])
    .dropna()
    .rename(columns={0: "rating_descriptor"})
)
show_df(df_rd_messy, 5)<jupyter_output><empty_output><jupyter_text>Aggregate by `rating_descriptor` and count the number of listings<jupyter_code>%%time
df_rd_messy_grouped = (
    df_rd_messy.groupby("rating_descriptor", as_index=False)["url"]
    .count()
    .nlargest(20, "url")
)
show_df(df_rd_messy_grouped)<jupyter_output><empty_output><jupyter_text>Plot the number of listings by rating descriptor<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="rating_descriptor",
        x="url",
        data=df_rd_messy_grouped,
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title(
        "Dominance of mature themes among rating descriptors",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(12)
    fig.set_figwidth(12)
    # fig.savefig(
    #     os.path.join(processed_data_dir, "9_rating_descriptors.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>4.11. [Publishers](publishers) **Add a column with the number of publishers for each listing**<jupyter_code>df["num_publishers"] = (
    df["Publisher_cleaned"].str.split(", ", expand=False).apply(lambda x: len(x))
)<jupyter_output><empty_output><jupyter_text>**Count the number of listings by the single and multiple publishers** Aggregate over the number of publishers and count the number of listings<jupyter_code>df_num_pubs = df.groupby("num_publishers", as_index=False)["url"].count()
df_num_pubs["multiple_pubs"] = df_num_pubs["num_publishers"] > 1
show_df(df_num_pubs)<jupyter_output><empty_output><jupyter_text>Count the number of listings for a single and multiple publishers<jupyter_code>single_multi_pubs = df_num_pubs.groupby("multiple_pubs", as_index=False)["url"].sum()
single_multi_pubs["multiple_pubs"] = single_multi_pubs["multiple_pubs"].map(
    {True: "Multiple", False: "Single"}
)
show_df(single_multi_pubs)<jupyter_output><empty_output><jupyter_text>Plot the number of listings for single and multiple publishers<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="url",
        x="multiple_pubs",
        data=single_multi_pubs,
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title(
        "Nearly all listings are released by a single publisher",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    ax.set_xticklabels(ax.get_xticklabels(), fontsize=22)
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "10_number_of_publishers.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>**What fraction of all available listings are offerred by a single publisher?**<jupyter_code>frac_single_pub = (
    df_num_pubs[df_num_pubs["num_publishers"] == 1]["url"].iloc[0]
    / df_num_pubs["url"].sum()
) * 100
print(f"Fraction by a single publisher = {frac_single_pub:.2f}")<jupyter_output>Fraction by a single publisher = 94.11<jupyter_text>**For each listing, the cleaned publishers column has all publisher names on the same row. For each listing, get every publisher on separate row (i.e. create a messy dataset for URL and publisher).** Split each publisher into a separate column, per listing<jupyter_code>pubs_url = df["Publisher_cleaned"].str.split(", ", expand=True).assign(url=df["url"])
show_df(pubs_url, 5)<jupyter_output><empty_output><jupyter_text>Create a messy dataset of URL and publisher<jupyter_code>pubs_url_reshaped = (
    pubs_url.set_index("url")
    .unstack()
    .reset_index()
    .drop(columns=["level_0"])
    .rename(columns={0: "Publisher_cleaned"})
    .dropna()
)
show_df(pubs_url_reshaped, 5)<jupyter_output><empty_output><jupyter_text>**How many unique publishers contribute games?**<jupyter_code>print(
    f"{pubs_url_reshaped['Publisher_cleaned'].nunique():,} publishers contributed listings"
)<jupyter_output>4,804 publishers contributed listings<jupyter_text>**Create a list of well-known video game publishers and count the number of listings by each of them.**- Lists of well-known publishers ([1](https://en.wikipedia.org/wiki/Video_game_publisher), [2](), [3](https://www.alltopeverything.com/top-10-biggest-video-game-companies/), [4](https://dfdnews.com/2021/04/23/2021-console-and-pc-game-publisher-sales-report-spend-grew-36-across-all-publishers-over-2020/))<jupyter_code>top_pubs = [
    "sony",
    "tencent",
    "activision",
    "electronic",
    "bandai",
    "microsoft",
    "netease",
    "rockstar",
    "valve",
    "ubisoft",
    "apple",
    "insomniac",
    "epic",
    "take-two",
    "aquaplus",
]<jupyter_output><empty_output><jupyter_text>Filter the cleaned publisher column to only include rows for the above list of publishers<jupyter_code>%%time
df_well_pubs_url_reshaped = pubs_url_reshaped[
    pubs_url_reshaped["Publisher_cleaned"].str.contains("|".join(top_pubs))
]
show_df(df_well_pubs_url_reshaped, 5)<jupyter_output><empty_output><jupyter_text>Count the number of listings associated with the above specified list of publishers<jupyter_code>df_num_listings_by_known_pubs = (
    df_well_pubs_url_reshaped.groupby("Publisher_cleaned", as_index=False)["url"]
    .count()
    .rename(columns={"url": "num_listings"})
    .nlargest(20, "num_listings")
)
show_df(df_num_listings_by_known_pubs)<jupyter_output><empty_output><jupyter_text>**Find the top 21 publishers overall (exclude collaborations between publishers)**<jupyter_code>df_num_listings_by_all_pubs = (
    pubs_url_reshaped.groupby("Publisher_cleaned", as_index=False)["url"]
    .count()
    .rename(columns={"url": "num_listings"})
)
df_num_listings_by_all_pubs["Publisher_cleaned"] = df_num_listings_by_all_pubs[
    "Publisher_cleaned"
].str.title()
show_df(df_num_listings_by_all_pubs.nlargest(21, "num_listings"))<jupyter_output><empty_output><jupyter_text>Plot the number of listings by the top 21 publishers<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="Publisher_cleaned",
        x="num_listings",
        data=df_num_listings_by_all_pubs.nlargest(20, "num_listings"),
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title(
        "Small number of listings released by top publishers",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(12)
    fig.set_figwidth(12)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "11_top_publishers_by_num_listings.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>4.12. [User Rating](user-rating) **Find the top 10 publishers with the most number of Very Positive user-ratings** Merge the reshaped publishers `DataFrame` with the original data<jupyter_code>pubs_url_reshaped_rating = pubs_url_reshaped.merge(
    df[["url", "overall_review_rating"]], on="url", how="left"
)
display(pubs_url_reshaped_rating.sample(10))<jupyter_output><empty_output><jupyter_text>Get the top 10 publishers as required<jupyter_code>df_user_ratings = (
    df.groupby("overall_review_rating", as_index=False)["url"]
    .count()
    .sort_values(by="url", ascending=False)
)
df_user_ratings = df_user_ratings[
    df_user_ratings["overall_review_rating"].str.contains("Positive|Negative")
]
df_user_ratings["positive"] = df_user_ratings["overall_review_rating"].str.contains(
    "Positive"
)
display(df_user_ratings)<jupyter_output><empty_output><jupyter_text>**Notes**1. The user-ratings distribution is skewed. There are almost no negative ratings for the scraped listings. This might not a good question to ask. There might be other issues related to the user rating on Steam if they are almost all very positive. Get the total number of ratings that are positive and those that are not positive<jupyter_code>df_user_ratings_grouped = df_user_ratings.groupby("positive", as_index=False)[
    "url"
].sum()
df_user_ratings_grouped["positive"] = (
    df_user_ratings_grouped["positive"].replace(True, "Yes").replace(False, "No")
)
display(df_user_ratings_grouped)<jupyter_output><empty_output><jupyter_text>Plot a chart of the total number of positive and non-positive ratings<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="url",
        x="positive",
        data=df_user_ratings_grouped.groupby("positive", as_index=False)["url"].sum(),
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title(
        "Strong bias towards positive user-ratings Steam web store",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(10)
    fig.set_figwidth(10)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "12_user_review_score.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>4.13. [Listing Prices](listing-prices) **Plot the distribution of (a) original listing price, (b) discount percentage and (c) discounted price** Histogram of original listing price<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.histplot(
        data=df[["original_price_cleaned"]],
        x="original_price_cleaned",
        log_scale=(False, True),
        color="steelblue",
    )
    ax.set_title(
        "Sub-$25 listings dominate Steam web store",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    ax.grid(color="lightgrey")
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "13_num_listings_below_25_dollars.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>Histogram of listing discounts (as a percentage)<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.histplot(
        data=df[["discount_pct_cleaned"]],
        x="discount_pct_cleaned",
        log_scale=(False, True),
        color="steelblue",
    )
    ax.set_title(
        "Distribution of available discounts",
        loc="left",
        fontweight="bold",
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "14_distribution_of_discount_pct.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>Histogram of discounted listing price<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.histplot(
        data=df[["discount_price"]],
        x="discount_price",
        log_scale=(False, True),
        color="steelblue",
    )
    ax.set_title("Distribution of discounted prices", loc="left", fontweight="bold")
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    ax.grid(color="lightgrey")
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(8)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "15_distribution_of_discount_price.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>4.14. [Listing Prices over time](listing-prices-over-time) **Show a heatmap of the median listing price by year and month since 2003. Do not include months of the year beyond the current month.** Append columns to the cleaned data to get the year and month (by name)<jupyter_code>df["release_year"] = df["release_date_cleaned"].dt.year
df["release_month"] = df["release_date_cleaned"].dt.month_name()<jupyter_output><empty_output><jupyter_text>Get- current year- all months of the current year excluding future months<jupyter_code>curr_year = datetime.now().year
curr_month = datetime.now().month - 1
months_upto_curr_month = months[:(curr_month)]
print(curr_year, months_upto_curr_month)<jupyter_output>2021 ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September']<jupyter_text>Count the number of listings by year and month- filter to only keep listings for the years from 2003 onwards upto the current year<jupyter_code>df_dt_agg = (
    df.dropna(subset=["release_year", "release_month"])
    .astype({"release_year": int})
    .query("release_year >= 2003 " "and release_year <= @curr_year")
    .groupby(["release_year", "release_month"], as_index=False)[
        "original_price_cleaned"
    ]
    .median()
)
show_df(df_dt_agg, 5)<jupyter_output><empty_output><jupyter_text>Set all future months of the current year to `None`<jupyter_code>df_dt_agg.loc[
    df_dt_agg.query(
        "release_year == @curr_year and release_month not in @months_upto_curr_month"
    ).index,
    "original_price_cleaned",
] = np.nan<jupyter_output><empty_output><jupyter_text>Pivot the aggregated data<jupyter_code>year_month_price = df_dt_agg.pivot(
    index="release_month", columns="release_year", values="original_price_cleaned"
)
show_df(year_month_price)<jupyter_output><empty_output><jupyter_text>Plot a heatmap of the orginal listing price by year and month, excluding year-month combinations from the future<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.heatmap(
        data=year_month_price.loc[months],
        linewidths=0.5,
        cmap="YlOrRd",
    )
    ax.set_title(
        "Median listing prices by year and month", loc="left", fontweight="bold"
    )
    ax.set_xlabel(None)
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(8)
    fig.set_figwidth(12)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "16_datetime_heatmap_price.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output><jupyter_text>**Plot the number of times each month the year has the most expensive median listing price.**<jupyter_code>def get_max_index_in_group(
    df, index_col="release_month", colname="original_price_cleaned"
):
    return df.set_index(index_col)[colname].idxmax()<jupyter_output><empty_output><jupyter_text>Do the following- get the median listing price by year and month- for each year, find the month with the highest median price<jupyter_code>df_max_month_by_year = (
    df.query("release_year >= 2003 and release_year <= 2020")
    .dropna(subset=["release_year"])
    .astype({"release_year": int})
    .groupby(["release_year", "release_month"], as_index=False)[
        "original_price_cleaned"
    ]
    .median()
    .groupby("release_year", as_index=False)
    .apply(
        get_max_index_in_group,
        index_col="release_month",
        colname="original_price_cleaned",
    )
    .rename(columns={None: "max_month"})
)
show_df(df_max_month_by_year)<jupyter_output><empty_output><jupyter_text>Merge the above aggregated results with the full list of months of the year in order to get numbers for all 12 calendar months (fill months that do not appear on this list of most expensive median price months with zeros)<jupyter_code>df_max_months = (
    df_max_month_by_year["max_month"]
    .value_counts()
    .reset_index()
    .merge(pd.Series(months, name="index").reset_index(), on="index", how="right")
    .drop(columns=["level_0"])
    .fillna(0)
    .astype({"max_month": int})
    .sort_values(by=["max_month"], ascending=False)
)
show_df(df_max_months)<jupyter_output><empty_output><jupyter_text>Plot the number of years for which each month has the the highest median price<jupyter_code>with sns.axes_style("ticks"):
    ax = sns.barplot(
        y="index",
        x="max_month",
        data=df_max_months,
        color="steelblue",
        saturation=0.5,
    )
    ax.set_title(
        "Most expensive months of the year (2003 - 2020)",
        loc="left",
        fontweight="bold",
    )
    ax.xaxis.set_major_locator(MaxNLocator(integer=True))
    ax.set_xlabel("Number of years as most expensive month")
    ax.set_ylabel(None)
    sns.despine()
    fig = plt.gcf()
    fig.set_figheight(10)
    fig.set_figwidth(10)
    # fig.savefig(
    #     os.path.join(reports_data_dir, "17_prices_by_month.png"),
    #     bbox_inches="tight",
    #     dpi=300,
    # )<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13579379995456328, 'fraction_numerical': 0.012391829653648361, 'mean_word_length': 4.66846171856708, 'pattern_counts': {'":': 44, '<': 340, '<?xml version=': 0, '>': 342, 'https://': 6, 'lorem ipsum': 0, 'www.': 3, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '88', 'dir': 'jupyter-structured-clean-dedup', 'id': '116444', 'license': 'MIT', 'n_tokens_mistral': 17932, 'n_tokens_neox': 16969, 'n_words': 3422, 'path': '7_eda_v2.ipynb', 'repo_name': 'elsdes3/steam-games-web-scraping-eda', 'size': '48419'}