{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "\n", "import matplotlib as mpl\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Configuration" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Data\n", "GROUP_BY_COLUMN = 'run_id'\n", "BUDGETS = {} # Fill in with category -> budget. We'll generate some fake ones below\n", "\n", "LOWER_QUANTILE = 0.25\n", "UPPER_QUANTILE = 0.75\n", "HISTOGRAM_BINS = 20\n", "\n", "# Display\n", "COLOUR_ABOVE_BUDGET = 'red'\n", "COLOUR_BELOW_BUDGET = 'green'\n", "COLOUR_ABOVE_BUDGET_QUANTILE = 'pink'\n", "COLOUR_BELOW_BUDGET_QUANTILE = 'lightgreen'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Generate Sample Data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from scipy.stats import skewnorm\n", "from collections import defaultdict\n", "\n", "# Configuration\n", "CATEGORIES = (\"gameplay\", \"animation\", \"graphics\", \"engine\", \"physics\", \"ui\")\n", "TARGET_FRAME_TIME = 33\n", "SAMPLES_PER_RUN = 1000\n", "SAMPLES_PER_RUN_NOISE = 50\n", "NUMBER_OF_RUNS = 5\n", "FRAME_TIME_DELTA = 3\n", "MAX_OUTLIER = 10\n", "\n", "# Create some random data with each category taking roughly the same\n", "# proportion of the frame time\n", "avg_category_frame_time = TARGET_FRAME_TIME / len(CATEGORIES)\n", "\n", "df = pd.DataFrame()\n", "for run_id in range(NUMBER_OF_RUNS): \n", " run_data = {} \n", " for category in CATEGORIES:\n", " r = np.concatenate([\n", " skewnorm.rvs(np.random.uniform(-1,1,1), # randomize skew\n", " loc=np.random.uniform(avg_category_frame_time - FRAME_TIME_DELTA,\n", " avg_category_frame_time + FRAME_TIME_DELTA,\n", " 1), # randomize peak\n", " scale=np.random.uniform(0.5,2.0,1), # randomize scale\n", " size=SAMPLES_PER_RUN),\n", " np.random.uniform(0, MAX_OUTLIER, size=SAMPLES_PER_RUN_NOISE) # add some random noise\n", " ]) \n", " run_data[category] = r\n", " run_data[GROUP_BY_COLUMN] = np.array([run_id] * (SAMPLES_PER_RUN + SAMPLES_PER_RUN_NOISE))\n", " run_df = pd.DataFrame(run_data)\n", " df = pd.concat([df, run_df])\n", "\n", "# Create some random category budgets\n", "rand_values = np.random.uniform(0.5, 1, len(CATEGORIES)) # keep budgets roughly the same\n", "budget_values = rand_values / rand_values.sum() * TARGET_FRAME_TIME\n", "BUDGETS = dict(zip(CATEGORIES, budget_values))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Histograms for Latest Run" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "latest_run_df = df[df[GROUP_BY_COLUMN] == df[GROUP_BY_COLUMN].max()].drop(columns=[GROUP_BY_COLUMN])\n", "\n", "def split_rectangular_patch(patch, split_value, below_color, above_color):\n", " '''Helper function to split a vertical histogram patch'''\n", " h = patch.get_height()\n", " w = patch.get_width()\n", " x = patch.get_x() \n", " \n", " d = (split_value - x) / w\n", " \n", " if d > 1: \n", " # Patch is completely below split_value, no split required\n", " patch.set_facecolor(below_color)\n", " return (patch,)\n", " elif d < 0:\n", " # Patch is completely above split_value, no split required\n", " patch.set_facecolor(above_color)\n", " return (patch,)\n", " \n", " # Below patch\n", " patch.set_facecolor(below_color) \n", " patch.set_width(d * w)\n", "\n", " # Above patch\n", " new_patch = mpl.patches.Rectangle((split_value, 0), (1 - d) * w, h)\n", " new_patch.set_facecolor(above_color) \n", " return (patch, ax.add_patch(new_patch))\n", "\n", "hist_quantiles = (0.0, LOWER_QUANTILE, 0.5, UPPER_QUANTILE, 1.0)\n", "\n", "for category, values in latest_run_df.iteritems():\n", " \n", " budget = BUDGETS[category]\n", " category_quantiles = values.quantile(hist_quantiles)\n", " \n", " fig, ax = plt.subplots(1, 1) \n", " N, bins, patches = ax.hist(values, bins=HISTOGRAM_BINS)\n", " median_bin_height = 0\n", " \n", " # Colour patches based on whether they are above/below quantiles and/or budget\n", " for index, patch in enumerate(patches):\n", " \n", " upper_quantile_value = category_quantiles[UPPER_QUANTILE]\n", " lower_quantile_value = category_quantiles[LOWER_QUANTILE]\n", " \n", " bin_lower_value = bins[index]\n", " bin_upper_value = bins[index + 1]\n", " \n", " if bin_upper_value < lower_quantile_value or bin_lower_value > upper_quantile_value: \n", " split_rectangular_patch(patch, budget, COLOUR_BELOW_BUDGET_QUANTILE, COLOUR_ABOVE_BUDGET_QUANTILE)\n", " elif bin_lower_value > lower_quantile_value and bin_upper_value < upper_quantile_value:\n", " split_rectangular_patch(patch, budget, COLOUR_BELOW_BUDGET, COLOUR_ABOVE_BUDGET)\n", " elif bin_lower_value < lower_quantile_value and bin_upper_value > lower_quantile_value:\n", " lower_patch, upper_patch = split_rectangular_patch(patch,\n", " lower_quantile_value,\n", " COLOUR_BELOW_BUDGET_QUANTILE,\n", " COLOUR_BELOW_BUDGET) \n", " split_rectangular_patch(lower_patch, budget, COLOUR_BELOW_BUDGET_QUANTILE, COLOUR_ABOVE_BUDGET_QUANTILE)\n", " split_rectangular_patch(upper_patch, budget, COLOUR_BELOW_BUDGET, COLOUR_ABOVE_BUDGET)\n", " elif bin_lower_value < upper_quantile_value and bin_upper_value > upper_quantile_value:\n", " lower_patch, upper_patch = split_rectangular_patch(patch,\n", " upper_quantile_value,\n", " COLOUR_BELOW_BUDGET,\n", " COLOUR_BELOW_BUDGET_QUANTILE) \n", " split_rectangular_patch(lower_patch, budget, COLOUR_BELOW_BUDGET, COLOUR_ABOVE_BUDGET)\n", " split_rectangular_patch(upper_patch, budget, COLOUR_BELOW_BUDGET_QUANTILE, COLOUR_ABOVE_BUDGET_QUANTILE)\n", " \n", " # While we're here track median patch height\n", " if bin_lower_value <= category_quantiles[0.5] <= bin_upper_value: \n", " median_bin_height = patch.get_height()\n", " \n", " # Add median line \n", " ax.vlines(category_quantiles[0.5], 0, median_bin_height)\n", " \n", " # Add budget line\n", " ax.axvline(budget, color='black', label=f\"budget={budget:.1f}ms\", linestyle=':')\n", " \n", " # Configure plot\n", " ax.set_title(f\"{category.title()} Histogram\")\n", " ax.set_xlabel(\"Time (ms)\")\n", " ax.set_ylabel(\"Count\") \n", " ax.legend()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Historical Trends" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Configuration for our custom \"vertical boxplot\"\n", "BAR_WIDTH = 0.8\n", "BAR_TOP_MARGIN = 1\n", "\n", "# From https://docs.python.org/3.7/library/itertools.html#itertools-recipes\n", "import itertools\n", "def pairwise(iterable):\n", " \"s -> (s0,s1), (s1,s2), (s2, s3), ...\"\n", " a, b = itertools.tee(iterable)\n", " next(b, None)\n", " return zip(a, b)\n", "\n", "half_bar_width = BAR_WIDTH / 2.0\n", "historical_quantiles = (0, LOWER_QUANTILE, 0.5, UPPER_QUANTILE, 1)\n", "\n", "# Group data\n", "df_groupby = df.groupby(GROUP_BY_COLUMN)\n", "\n", "for category in CATEGORIES:\n", " # We're generating a custom graph for each category with a vertical box\n", " # showing quantiles for each group. Equivalent to the histogram viewed\n", " # from above.\n", " budget = BUDGETS[category]\n", " fig, ax = plt.subplots(1, 1) \n", " \n", " y_min = 1E10\n", " y_max = -1\n", " \n", " df_quantiles = df_groupby[category].quantile(historical_quantiles).unstack()\n", " \n", " for run_id, row in df_quantiles.iterrows(): \n", " for lower, upper in pairwise(historical_quantiles):\n", " \n", " lower_value = row[lower]\n", " upper_value = row[upper]\n", " \n", " if upper_value < budget or lower_value > budget:\n", " # Single patch, coloured based on above/below budget and above/below quartile\n", " patch_lower_upper = mpl.patches.Rectangle((run_id - half_bar_width, lower_value),\n", " BAR_WIDTH,\n", " upper_value - lower_value) \n", " if upper_value < budget:\n", " if lower in (0, UPPER_QUANTILE):\n", " patch_lower_upper.set_facecolor(COLOUR_BELOW_BUDGET_QUANTILE)\n", " else:\n", " patch_lower_upper.set_facecolor(COLOUR_BELOW_BUDGET)\n", " else:\n", " if lower in (0, UPPER_QUANTILE):\n", " patch_lower_upper.set_facecolor(COLOUR_ABOVE_BUDGET_QUANTILE)\n", " else:\n", " patch_lower_upper.set_facecolor(COLOUR_ABOVE_BUDGET)\n", " \n", " ax.add_patch(patch_lower_upper)\n", " else:\n", " # Split patch into region above and below budget\n", " patch_lower_budget = mpl.patches.Rectangle((run_id - half_bar_width, lower_value),\n", " BAR_WIDTH,\n", " budget - lower_value) \n", " if lower in (0, UPPER_QUANTILE):\n", " patch_lower_budget.set_facecolor(COLOUR_BELOW_BUDGET_QUANTILE)\n", " else:\n", " patch_lower_budget.set_facecolor(COLOUR_BELOW_BUDGET)\n", " \n", " ax.add_patch(patch_lower_budget)\n", " \n", " patch_budget_upper = mpl.patches.Rectangle((run_id - half_bar_width, budget),\n", " BAR_WIDTH, \n", " upper_value - budget) \n", " if lower in (0, UPPER_QUANTILE):\n", " patch_budget_upper.set_facecolor(COLOUR_ABOVE_BUDGET_QUANTILE)\n", " else:\n", " patch_budget_upper.set_facecolor(COLOUR_ABOVE_BUDGET)\n", " \n", " ax.add_patch(patch_budget_upper)\n", " \n", " ax.hlines(row[0.5], run_id - half_bar_width, run_id + half_bar_width)\n", " \n", " y_min = min(y_min, row.min())\n", " y_max = max(y_max, row.max())\n", " \n", " ax.set_xlim(-1, NUMBER_OF_RUNS)\n", " ax.set_ylim(y_min - BAR_TOP_MARGIN, y_max + BAR_TOP_MARGIN) \n", " \n", " # Add budget line\n", " ax.axhline(budget, color='black', label=f\"budget={budget:.1f}ms\", linestyle=':') \n", " \n", " # Use whichever locator makes sense here. I'm assuming integer groups.\n", " from matplotlib.ticker import MaxNLocator\n", " ax.xaxis.set_major_locator(MaxNLocator(integer=True, prune='both'))\n", " \n", " # Configure plot\n", " ax.set_title(f\"{category.title()} Historical Data\")\n", " ax.set_xlabel(GROUP_BY_COLUMN)\n", " ax.set_ylabel('Time (ms)')\n", " ax.legend()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 4 }