{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "kz8lLSv6mVQo" }, "source": [ "# **\ud83e\udd16 Data Analysis & Visualization**" ] }, { "cell_type": "markdown", "metadata": { "id": "jpASMyIQMaAq" }, "source": [ "## **1.** \ud83d\udce6 Install required packages" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "f48c8f8c", "outputId": "ca8b5ef6-ebc4-4fc6-9373-e1577253fd21" }, "outputs": [], "source": [ "!pip install pandas matplotlib seaborn numpy textblob faker transformers vaderSentiment\n" ] }, { "cell_type": "markdown", "metadata": { "id": "NZd99NpKkKyp" }, "source": [ "## **2.** \u2705\ufe0f Load & inspect input datasets" ] }, { "cell_type": "markdown", "metadata": { "id": "_JBLmm508Uq2" }, "source": [ "### *a. Initial setup*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "eBDXPQz18Xrs" }, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import random" ] }, { "cell_type": "markdown", "metadata": { "id": "IL8lZbMm8m3k" }, "source": [ "### *b. \u270b\ud83c\udffb\ud83d\uded1\u26d4\ufe0f Create the df_reviews dataframe from the synthetic_book_reviews.csv file*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "fdgjghfO8uuq" }, "outputs": [], "source": [ "df_reviews = pd.read_csv(\"synthetic_book_reviews.csv\")" ] }, { "cell_type": "markdown", "metadata": { "id": "N-Dl37J0HLhU" }, "source": [ "### *c. \u270b\ud83c\udffb\ud83d\uded1\u26d4\ufe0f Create the df_sales dataframe from the synthetic_sales_data.csv file*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "6XZs3P7fHgQe" }, "outputs": [], "source": [ "df_sales = pd.read_csv(\"synthetic_sales_data.csv\")" ] }, { "cell_type": "markdown", "metadata": { "id": "MUI3SkmyrGQo" }, "source": [ "### *d. \u270b\ud83c\udffb\ud83d\uded1\u26d4\ufe0f Visualize the first few lines of the two final datasets: df_reviews and df_sales*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "p8FdQFXErOqE", "outputId": "b6673cc0-bb1d-4221-dcef-e1f9dec0d6c8" }, "outputs": [], "source": [ "print(df_sales.head())\n", "print(df_reviews.head())" ] }, { "cell_type": "markdown", "metadata": { "id": "Y3oqGHsmrQzx" }, "source": [ "### *d. Run a quality check on the datasets*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 1000 }, "id": "VArQGPoKrfLm", "outputId": "16e94f18-4f60-44ba-919e-fc129f7f440e" }, "outputs": [], "source": [ "def quality_check(df, name=\"DataFrame\"):\n", " print(f\"\\n\ud83d\udd0d Quality Check Report for: {name}\")\n", " print(\"=\" * (25 + len(name)))\n", "\n", " # Basic info\n", " print(f\"\\n\ud83d\udccf Shape: {df.shape}\")\n", " print(\"\\n\ud83d\udd20 Column Types:\")\n", " print(df.dtypes)\n", "\n", " # Missing values\n", " print(\"\\n\u2753 Missing Values:\")\n", " print(df.isnull().sum())\n", "\n", " # Duplicates\n", " duplicate_count = df.duplicated().sum()\n", " print(f\"\\n\ud83d\udccb Duplicate Rows: {duplicate_count}\")\n", "\n", " # Summary stats\n", " print(\"\\n\ud83d\udcca Summary Statistics:\")\n", " display(df.describe(include='all').transpose())\n", "\n", " # Sample rows\n", " print(\"\\n\ud83d\udc40 Sample Rows:\")\n", " display(df.sample(5))\n", "\n", "# Run checks\n", "quality_check(df_reviews, \"df_reviews\")\n", "quality_check(df_sales, \"df_sales\")\n" ] }, { "cell_type": "markdown", "metadata": { "id": "TTxUKDYINPxV" }, "source": [ "## **3.** \ud83c\udfad Perform sentiment analysis using VADER" ] }, { "cell_type": "markdown", "metadata": { "id": "OqhYU8rDxQRT" }, "source": [ "### *a. Initial setup*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "DNk5w8mNxSZ6" }, "outputs": [], "source": [ "from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer\n", "\n", "# \ud83e\udd16 Initialize VADER analyzer\n", "analyzer = SentimentIntensityAnalyzer()" ] }, { "cell_type": "markdown", "metadata": { "id": "P123TwSWxVAr" }, "source": [ "### *b. Create a function get_sentiment_label that will return the label negative, neutral, or positive based on the VADER analyzer's scoring of the text*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "89809e6f" }, "outputs": [], "source": [ "def get_sentiment_label(text):\n", " score = analyzer.polarity_scores(text)[\"compound\"]\n", " if score >= 0.05:\n", " return \"positive\"\n", " elif score <= -0.05:\n", " return \"negative\"\n", " else:\n", " return \"neutral\"" ] }, { "cell_type": "markdown", "metadata": { "id": "DS9eCZ95yQn3" }, "source": [ "### *c. \u270b\ud83c\udffb\ud83d\uded1\u26d4\ufe0f Apply get_sentiment_label to df_reviews column named review_text to get sentiment_label column*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "SpXzFaDfyM7I" }, "outputs": [], "source": [ "df_reviews[\"sentiment_label\"] = df_reviews[\"review_text\"].apply(get_sentiment_label)" ] }, { "cell_type": "markdown", "metadata": { "id": "5cnPCFFnyXN6" }, "source": [ "### *d. \u270b\ud83c\udffb\ud83d\uded1\u26d4\ufe0f View the first few lines of the resulting table df_reviews*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ODGyfjBSyZEO", "outputId": "b18ef853-21d0-47d0-d9c6-3db2fcb641ac" }, "outputs": [], "source": [ "print(df_reviews.head())" ] }, { "cell_type": "markdown", "metadata": { "id": "Qy3Hqm-FojvT" }, "source": [ "## **4.** \ud83d\udcca Use the following data visualization code snippets" ] }, { "cell_type": "markdown", "metadata": { "id": "lcjGSw2bzqtZ" }, "source": [ "### *a. Initial setup*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "p5LV2o1rzsiC" }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "import matplotlib.dates as mdates" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "tvaBtswpGS__", "outputId": "2800b619-e62e-4605-8a04-4a875859ef6b" }, "outputs": [], "source": [ "# ----------------------------\n", "# Outputs (for Hugging Face app)\n", "# ----------------------------\n", "# In the notebook: you still SEE interactive tables/plots inline.\n", "# For the Space dashboard: we also SAVE the same outputs as files.\n", "\n", "from pathlib import Path\n", "\n", "ART_DIR = Path(\"artifacts\")\n", "PY_FIG = ART_DIR / \"py\" / \"figures\"\n", "PY_TAB = ART_DIR / \"py\" / \"tables\"\n", "\n", "for p in [PY_FIG, PY_TAB]:\n", " p.mkdir(parents=True, exist_ok=True)\n", "\n", "print(\"\u2705 Output folders:\")\n", "print(\" -\", PY_FIG.resolve())\n", "print(\" -\", PY_TAB.resolve())\n" ] }, { "cell_type": "markdown", "metadata": { "id": "b9T1rkBe0AJU" }, "source": [ "### *b. Sample of 5 books for each popularity level for visualizations*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "sLdFmGqXqo_t" }, "outputs": [], "source": [ "sampled_titles = []\n", "for pop_score in sorted(df_reviews[\"popularity_score\"].dropna().unique()):\n", " all_titles = df_reviews[df_reviews[\"popularity_score\"] == pop_score][\"title\"].unique()\n", " sampled = random.sample(list(all_titles), min(5, len(all_titles)))\n", " sampled_titles.extend(sampled)" ] }, { "cell_type": "markdown", "metadata": { "id": "xq7-C8m70mMH" }, "source": [ "### *c. Copy relevant sales, reviews, and book names*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "laDdMece0qrq" }, "outputs": [], "source": [ "sampled_sales = df_sales[df_sales[\"title\"].isin(sampled_titles)].copy()\n", "sampled_reviews = df_reviews[df_reviews[\"title\"].isin(sampled_titles)].copy()\n", "sampled_books = df_reviews[df_reviews[\"title\"].isin(sampled_titles)].copy()" ] }, { "cell_type": "markdown", "metadata": { "id": "8YtfkG_A0wTy" }, "source": [ "### *d. Plot sales trends over time for the sampled books*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 0 }, "id": "1iTVzflW0Rkw", "outputId": "4193480a-0278-4a27-ad5d-3f7fcd700383" }, "outputs": [], "source": [ "# \ud83d\udd52 Ensure datetime format\n", "df_sales[\"month\"] = pd.to_datetime(df_sales[\"month\"])\n", "# \ud83c\udfa8 Color mapping\n", "popularity_colors = {\n", " 1: \"darkred\", 2: \"orangered\", 3: \"gold\", 4: \"mediumseagreen\", 5: \"royalblue\"\n", "}\n", "\n", "# \ud83d\udcc8 Plot 1: Sales trends\n", "plt.figure(figsize=(20, 8))\n", "for title in sampled_titles:\n", " row = sampled_books[sampled_books[\"title\"] == title].iloc[0]\n", " color = popularity_colors.get(row[\"popularity_score\"], \"gray\")\n", " subset = sampled_sales[sampled_sales[\"title\"] == title]\n", " plt.plot(subset[\"month\"], subset[\"units_sold\"], label=f\"{title} (Pop. {row['popularity_score']})\", color=color)\n", "\n", "plt.title(\"\ud83d\udcc8 Sales Trends Over Time (5 per Popularity Level)\")\n", "plt.xlabel(\"Month\")\n", "plt.ylabel(\"Units Sold\")\n", "plt.xticks(rotation=45)\n", "plt.legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize='small')\n", "plt.grid(True)\n", "plt.tight_layout()\n", "plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", "plt.savefig(PY_FIG / 'sales_trends_sampled_titles.png', dpi=150)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "lDpMkjDP1K6j" }, "source": [ "### *e. Plot sentiment_label distribution per book*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 0 }, "id": "dn1Jgd5R1KLu", "outputId": "78319480-be56-40d7-8935-c5745d9e37ed" }, "outputs": [], "source": [ "# \ud83c\udfa8 Give a new name to each book that includes the rating together with the title\n", "sampled_reviews[\"grouped_title\"] = sampled_reviews[\"rating\"].astype(str) + \"\u2605 | \" + sampled_reviews[\"title\"]\n", "\n", "# \ud83d\udcca Aggregate sentiment counts\n", "sentiment_counts = (\n", " sampled_reviews.groupby([\"grouped_title\", \"sentiment_label\"])\n", " .size()\n", " .unstack(fill_value=0)[[\"negative\", \"neutral\", \"positive\"]] # consistent order\n", ")\n", "\n", "# \ud83d\udcbe Save table for HF dashboard\n", "sentiment_counts.reset_index().to_csv(PY_TAB / 'sentiment_counts_sampled.csv', index=False)\n", "\n", "\n", "# \u2705 Plot stacked horizontal bars\n", "fig, ax = plt.subplots(figsize=(12, 14))\n", "sentiment_counts.plot.barh(\n", " stacked=True,\n", " ax=ax,\n", " color={\"negative\": \"royalblue\", \"neutral\": \"lightgray\", \"positive\": \"crimson\"}\n", ")\n", "\n", "plt.title(\"\ud83d\udcac Sentiment Distribution in Reviews (5 Books per Popularity Level)\", fontsize=14)\n", "plt.xlabel(\"Number of Reviews\")\n", "plt.ylabel(\"Book Title (Grouped by Popularity Score)\")\n", "plt.legend(title=\"Sentiment\", loc=\"lower right\")\n", "plt.grid(axis=\"x\", linestyle=\"--\", alpha=0.6)\n", "plt.tight_layout()\n", "plt.savefig(PY_FIG / 'sentiment_distribution_sampled_titles.png', dpi=150)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "rmgylC1ENCHy" }, "source": [ "## **5.** \ud83d\udd2e Forecast book sales with the following ARIMA code" ] }, { "cell_type": "markdown", "metadata": { "id": "jFV4JE1R3FKH" }, "source": [ "### *a. Initial setup*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Mh8Alha03H22" }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import matplotlib.dates as mdates\n", "import statsmodels.api as sm\n", "from itertools import product\n", "import matplotlib.cm as cm\n", "import warnings" ] }, { "cell_type": "markdown", "metadata": { "id": "gHucD8OW3U0w" }, "source": [ "### *b. Define function find_best_arima to try different ARIMA parameter values and return the best combination for each book's price forecast*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "477fa43f" }, "outputs": [], "source": [ "def find_best_arima(series, p_range=(0, 5), d_range=(0, 2), q_range=(0, 1)):\n", " best_aic = float(\"inf\")\n", " best_order = None\n", " best_model = None\n", "\n", " for p, d, q in product(range(p_range[0], p_range[1] + 1),\n", " range(d_range[0], d_range[1] + 1),\n", " range(q_range[0], q_range[1] + 1)):\n", " try:\n", " model = sm.tsa.ARIMA(series, order=(p, d, q))\n", " results = model.fit()\n", " if results.aic < best_aic:\n", " best_aic = results.aic\n", " best_order = (p, d, q)\n", " best_model = results\n", " except:\n", " continue\n", "\n", " return best_order, best_model" ] }, { "cell_type": "markdown", "metadata": { "id": "Rq5t1Hey3jkD" }, "source": [ "### *c. Plot the figure*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 0 }, "id": "DmxGdvLE3dHQ", "outputId": "9bb19d6b-2f68-4066-cbec-dd7eb80a9111" }, "outputs": [], "source": [ "# \ud83c\udfa8 Generate 25 highly distinct colors using HUSL (hue-saturation-lightness)\n", "colors = sns.color_palette(\"tab10\", len(sampled_titles))\n", "\n", "plt.figure(figsize=(16, 10))\n", "\n", "for i, title in enumerate(sampled_titles):\n", " book_sales = sampled_sales[sampled_sales[\"title\"] == title].copy()\n", " book_sales[\"month\"] = pd.to_datetime(book_sales[\"month\"])\n", " book_sales = book_sales.sort_values(\"month\").set_index(\"month\")\n", "\n", " with warnings.catch_warnings():\n", " warnings.simplefilter(\"ignore\")\n", " best_order, best_model = find_best_arima(book_sales[\"units_sold\"])\n", " if best_model is not None:\n", " forecast = best_model.get_forecast(steps=6)\n", " forecast_index = pd.date_range(start=book_sales.index[-1] + pd.DateOffset(months=1), periods=6, freq='MS')\n", "\n", " # \ud83d\udfe6 Plot observed sales (solid line)\n", " plt.plot(book_sales.index, book_sales[\"units_sold\"], color=colors[i], label=title, linewidth=2)\n", "\n", " # \ud83d\udfe0 Plot forecast (dotted line, same color)\n", " plt.plot(forecast_index, forecast.predicted_mean, linestyle=\"--\", color=colors[i], linewidth=2)\n", "\n", "# \ud83d\udcc8 Final formatting\n", "plt.title(\"\ud83d\udcc8 ARIMA Forecasts for Sampled Books (1 per Popularity Level)\", fontsize=14)\n", "plt.xlabel(\"Month\")\n", "plt.ylabel(\"Units Sold\")\n", "plt.xticks(rotation=45)\n", "plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))\n", "plt.grid(True)\n", "plt.legend(loc=\"center left\", bbox_to_anchor=(1, 0.5), fontsize=\"small\")\n", "plt.tight_layout()\n", "plt.savefig(PY_FIG / 'arima_forecasts_sampled_titles.png', dpi=150)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "SKBcx3fyCFly" }, "source": [ "## **6.** \ud83c\udff7\ufe0f Decide on price changes with a rule-based approach based on sentiment and future revenue" ] }, { "cell_type": "markdown", "metadata": { "id": "nY-vV2JJDZqu" }, "source": [ "### *a. Calculate average sales per book*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "nbDT_RHaDD2R" }, "outputs": [], "source": [ "avg_sales = df_sales.groupby(\"title\")[\"units_sold\"].mean().reset_index()\n", "avg_sales.columns = [\"title\", \"avg_units_sold\"]" ] }, { "cell_type": "markdown", "metadata": { "id": "94wi-RvkDf2z" }, "source": [ "### *b. Calculate sentiment distribution per book*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "fWjQ9IOXDk-M" }, "outputs": [], "source": [ "sentiment_counts = df_reviews.groupby([\"title\", \"sentiment_label\"]).size().unstack(fill_value=0)\n", "sentiment_counts[\"total\"] = sentiment_counts.sum(axis=1)\n", "sentiment_counts[\"positive_ratio\"] = sentiment_counts.get(\"positive\") / sentiment_counts[\"total\"]\n", "sentiment_counts[\"negative_ratio\"] = sentiment_counts.get(\"negative\") / sentiment_counts[\"total\"]" ] }, { "cell_type": "markdown", "metadata": { "id": "Vm10ym_iDtEW" }, "source": [ "### *c. Merge the calculated sales and sentiment characteristics*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "T-zlh6rBDpxg" }, "outputs": [], "source": [ "df_decision = avg_sales.merge(sentiment_counts, on=\"title\", how=\"left\")" ] }, { "cell_type": "markdown", "metadata": { "id": "1WIWDojyD7fK" }, "source": [ "### *d. \u270b\ud83c\udffb\ud83d\uded1\u26d4\ufe0f Create the pricing_decision function as a basic rule-based pricing decider based on sentiment and revenue*\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "b5qJCb46Dxfb" }, "source": [ "* If there are 120 or more average units sold and 0.6 or higher positive ratio, the decision should be to increase price.\n", "* If there are 60 or less average units sold and 0.4 or higher negative ratio, the decision should be to decrease price.\n", "* Otherwise, the price should be kept the same." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "XBzozedwD6yx" }, "outputs": [], "source": [ "def pricing_decision(row):\n", " if row[\"avg_units_sold\"] >= 120 and row[\"positive_ratio\"] >= 0.6:\n", " return \"Increase Price\"\n", " elif row[\"avg_units_sold\"] <= 60 and row[\"negative_ratio\"] >= 0.4:\n", " return \"Decrease Price\"\n", " else:\n", " return \"Keep Price\"" ] }, { "cell_type": "markdown", "metadata": { "id": "xmLEdF14EPAA" }, "source": [ "### *e. \u270b\ud83c\udffb\ud83d\uded1\u26d4\ufe0f Run the pricing_decision function and check out the first few decisions*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "TZ0ZhgHrEQJB", "outputId": "7a7dd343-32b5-47f4-f8b3-f9581fe52942" }, "outputs": [], "source": [ "df_decision[\"pricing_action\"] = df_decision.apply(pricing_decision, axis=1)\n", "\n", "print(df_decision.head())" ] }, { "cell_type": "markdown", "metadata": { "id": "WTkP2_-EApev" }, "source": [ "\n", "## **7.** \ud83d\udcbe Save Python outputs for the Hugging Face dashboard" ] }, { "cell_type": "markdown", "metadata": { "id": "3EIjfnokGpJv" }, "source": [ "\n", "This section exports **HF-ready artifacts** into a consistent folder structure:\n", "\n", "- `(root folder)py/figures/` (Python-generated visuals)\n", "- `(root folder)py/tables/` (tables/metrics)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ZJJ4PMgIApev", "outputId": "159cb98e-456f-4f6f-a73c-06ed396f1fb8" }, "outputs": [], "source": [ "\n", "import json\n", "\n", "# -------------------------\n", "# 1) Dashboard table (monthly) \u2014 reuse if already built\n", "# -------------------------\n", "if \"df_monthly\" in globals() and df_monthly is not None:\n", " df_dashboard = df_monthly.copy()\n", "else:\n", " # fallback: monthly units sold only\n", " df_dashboard = (\n", " df_sales.groupby(\"month\", as_index=False)\n", " .agg(total_units_sold=(\"units_sold\", \"sum\"))\n", " .sort_values(\"month\")\n", " )\n", "\n", "# Save the single overview dashboard table\n", "df_dashboard.to_csv(PY_TAB / \"df_dashboard.csv\", index=False)\n", "\n", "# -------------------------\n", "# 2) KPI summary (small json) \u2014 computed from raw df_sales + df_dashboard\n", "# -------------------------\n", "kpis = {\n", " \"n_titles\": int(df_sales[\"title\"].nunique()),\n", " \"n_months\": int(df_dashboard[\"month\"].nunique()),\n", " \"total_units_sold\": float(df_sales[\"units_sold\"].sum()),\n", "}\n", "\n", "# Only include revenue KPIs if df_dashboard contains it (since you said monthly revenue already exists)\n", "if \"total_revenue\" in df_dashboard.columns and df_dashboard[\"total_revenue\"].notna().any():\n", " kpis[\"total_revenue\"] = float(df_dashboard[\"total_revenue\"].sum())\n", "\n", "with open(PY_FIG / \"kpis.json\", \"w\", encoding=\"utf-8\") as f:\n", " json.dump(kpis, f, indent=2)\n", "\n", "# -------------------------\n", "# 3) Python tables (title-level quick inspection)\n", "# -------------------------\n", "df_by_title_units = (\n", " df_sales.groupby(\"title\", as_index=False)\n", " .agg(total_units_sold=(\"units_sold\", \"sum\"))\n", " .sort_values(\"total_units_sold\", ascending=False)\n", ")\n", "df_by_title_units.head(10).to_csv(PY_TAB / \"top_titles_by_units_sold.csv\", index=False)\n", "\n", "# Optional: title-level revenue table ONLY if df_sales already has per-row revenue\n", "if \"revenue\" in df_sales.columns and df_sales[\"revenue\"].notna().any():\n", " df_by_title_rev = (\n", " df_sales.groupby(\"title\", as_index=False)\n", " .agg(total_revenue=(\"revenue\", \"sum\"))\n", " .sort_values(\"total_revenue\", ascending=False)\n", " )\n", " df_by_title_rev.head(10).to_csv(PY_TAB / \"top_titles_by_revenue.csv\", index=False)\n", "\n", "print(\"\u2705 Exports written to artifacts/:\")\n", "print(\" - common/: df_dashboard.csv, kpis.json\")\n", "print(\" - py/tables/: top_titles_by_units_sold.csv (+ optional top_titles_by_revenue.csv)\")\n" ] }, { "cell_type": "markdown", "metadata": { "id": "0b4e76d3" }, "source": [ "\u2705 **Extra outputs for the R notebook**: `(root folder)common/r_input_title_level.csv` and `(root folder)common/r_input_monthly_revenue.csv` (these are the only two files the R portion needs)." ] } ], "metadata": { "colab": { "collapsed_sections": [ "jpASMyIQMaAq", "NZd99NpKkKyp", "TTxUKDYINPxV", "Qy3Hqm-FojvT", "rmgylC1ENCHy", "SKBcx3fyCFly", "WTkP2_-EApev" ], "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }