From 606528fdbfb7a2cc30f6e5d019377390599bfe0c Mon Sep 17 00:00:00 2001 From: Vas Date: Thu, 19 Sep 2024 20:24:17 +0300 Subject: [PATCH 01/16] feat: add line plot --- pyretailscience/plots/__init__.py | 0 pyretailscience/plots/line.py | 317 ++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 pyretailscience/plots/__init__.py create mode 100644 pyretailscience/plots/line.py diff --git a/pyretailscience/plots/__init__.py b/pyretailscience/plots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py new file mode 100644 index 0000000..abfbf9d --- /dev/null +++ b/pyretailscience/plots/line.py @@ -0,0 +1,317 @@ +"""This module provides versatile plotting functionality for creating line plots from pandas DataFrames. + +It is designed for plotting sequences that resemble time-based data, such as "days" or "months" since +an event, but it does not explicitly handle datetime values. For actual time-based plots (using datetime +objects), please refer to the `time_plot` module. + +The sequences used in this module can include values such as "days since an event" (e.g., -2, -1, 0, 1, 2) +or "months since a competitor store opened." **This module is not intended for use with actual datetime values**. +If a datetime or datetime-like column is passed as `x_col`, a warning will be triggered suggesting the use +of the `time_plot` module. + +Key Features: +-------------- +- **Plotting Sequences or Indexes**: Plot one or more value columns (`value_col`), supporting sequences + like -2, -1, 0, 1, 2 (e.g., months since an event), using either the index or a specified x-axis + column (`x_col`). +- **Custom X-Axis or Index**: Use any column as the x-axis (`x_col`), or plot based on the index if no + x-axis column is specified. +- **Multiple Lines**: Create separate lines for each unique value in `group_col` (e.g., categories). +- **Comprehensive Customization**: Easily customize titles, axis labels, legends, and optionally move + the legend outside the plot. +- **Pre-Aggregated Data**: The data must be pre-aggregated before plotting. No aggregation occurs in + this module. + +### Common Scenarios and Examples: + +1. **Basic Plot Showing Price Trends Since Competitor Store Opened**: + + This example demonstrates how to plot the `total_price` over the number of months since a competitor + store opened. The total price remains stable or increases slightly before the store opened, and then + drops randomly after the competitor's store opened. + + **Preparing the Data**: + ```python + import numpy as np + + # Convert 'transaction_datetime' to a datetime column if it's not already + df['transaction_datetime'] = pd.to_datetime(df['transaction_datetime']) + + # Resample the data by month + df['month'] = df['transaction_datetime'].dt.to_period('M') # Extract year and month + df_monthly = df.groupby('month').agg({'total_price': 'sum'}).reset_index() + + # Create the "months since competitor opened" column + # Assume the competitor opened 60% of the way through the data + competitor_opened_month_index = int(len(df_monthly) * 0.6) + df_monthly['months_since_competitor_opened'] = np.arange(-competitor_opened_month_index, len(df_monthly) - competitor_opened_month_index) + + # Simulate stable or increasing prices before competitor opened + df_monthly.loc[df_monthly['months_since_competitor_opened'] < 0, 'total_price'] *= np.random.uniform(1.05, 1.2) + + # Simulate a random drop after the competitor opened + df_monthly.loc[df_monthly['months_since_competitor_opened'] >= 0, 'total_price'] *= np.random.uniform(0.8, 0.95, size=len(df_monthly[df_monthly['months_since_competitor_opened'] >= 0])) + ``` + + **Plotting**: + ```python + ax = line.plot( + df=df_monthly, + value_col="total_price", # Plot 'total_price' values + x_col="months_since_competitor_opened", # Use 'months_since_competitor_opened' as the x-axis + title="Total Price Since Competitor Store Opened", # Title of the plot + x_label="Months Since Competitor Opened", # X-axis label + y_label="Total Price", # Y-axis label + ) + + plt.show() + ``` + + **Use Case**: This is useful when you want to illustrate the effect of a competitor store opening + on sales performance. The x-axis represents months before and after the event, and the y-axis shows + how prices behaved over time. + +--- + +2. **Plotting Price Trends by Category (Top 3 Categories)**: + + This example plots the total price for the top 3 categories before and after the competitor opened. + The data is resampled by month, split by category, and tracks the months since the competitor store opened. + + **Preparing the Data**: + ```python + import numpy as np + import pandas as pd + + # Convert 'transaction_datetime' to a datetime column if it's not already + df['transaction_datetime'] = pd.to_datetime(df['transaction_datetime']) + + # Resample the data by month and category + df['month'] = df['transaction_datetime'].dt.to_period('M') # Extract year and month + df_monthly = df.groupby(['month', 'category_0_name']).agg({'total_price': 'sum'}).reset_index() + + # Create a separate dataframe for unique months to track "months since competitor opened" + unique_months = df_monthly['month'].unique() + competitor_opened_month_index = int(len(unique_months) * 0.6) # Assume competitor opened 60% of the way through + + # Create 'months_since_competitor_opened' for each unique month + months_since_competitor_opened = np.concatenate([ + np.arange(-competitor_opened_month_index, 0), # Before competitor opened + np.arange(0, len(unique_months) - competitor_opened_month_index) # After competitor opened + ]) + + # Create a new dataframe with the 'months_since_competitor_opened' values and merge it back + months_df = pd.DataFrame({'month': unique_months, 'months_since_competitor_opened': months_since_competitor_opened}) + df_monthly = df_monthly.merge(months_df, on='month', how='left') + + # Filter to include months both before and after the competitor opened + df_since_competitor_opened = df_monthly[(df_monthly['months_since_competitor_opened'] >= -6) & # Include 6 months before + (df_monthly['months_since_competitor_opened'] <= 12)] # Include 12 months after + + # Identify top 3 categories based on total_price across the selected period + category_totals = df_since_competitor_opened.groupby('category_0_name')['total_price'].sum().sort_values(ascending=False) + + # Get the top 3 categories + top_categories = category_totals.head(3).index + + # Filter the dataframe to include only the top 3 categories + df_top_categories = df_since_competitor_opened[df_since_competitor_opened['category_0_name'].isin(top_categories)] + ``` + + **Plotting**: + ```python + ax = line.plot( + df=df_top_categories, + value_col="total_price", # Plot 'total_price' values + group_col="category_0_name", # Separate lines for each category + x_col="months_since_competitor_opened", # Use 'months_since_competitor_opened' as the x-axis + title="Total Price for Top 3 Categories (Before and After Competitor Opened)", # Title of the plot + x_label="Months Since Competitor Opened", # X-axis label + y_label="Total Price", # Y-axis label + legend_title="Category" # Legend title + ) + + plt.show() + ``` + + **Use Case**: Use this when you want to analyze the behavior of specific top categories before and after + an event, such as the opening of a competitor store. + +--- + +### Customization Options: +- **`value_col`**: The column or list of columns to plot (e.g., `'total_price'`). +- **`group_col`**: A column whose unique values will be used to create separate lines (e.g., `'category_0_name'`). +- **`x_col`**: The column to use as the x-axis (e.g., `'months_since_competitor_opened'`). **Warning**: If a datetime + or datetime-like column is passed, a warning will suggest using the `time_plot` module instead. +- **`title`**, **`x_label`**, **`y_label`**: Custom text for the plot title and axis labels. +- **`legend_title`**: Custom title for the legend based on `group_col`. +- **`move_legend_outside`**: Boolean flag to move the legend outside the plot. + +--- + +### Dependencies: +- `pandas`: For DataFrame manipulation and grouping. +- `matplotlib`: For generating plots. +- `pyretailscience.style.graph_utils`: For applying consistent graph styles across the plots. + +""" + +import logging + +import pandas as pd +from matplotlib.axes import Axes, SubplotBase + +import pyretailscience.style.graph_utils as gu +from pyretailscience.style.graph_utils import GraphStyles +from pyretailscience.style.tailwind import COLORS + +logging.basicConfig(format="%(message)s", level=logging.INFO) + + +def _check_datetime_column(df: pd.DataFrame, x_col: str) -> None: + """Checks if the x_col is a datetime or convertible to datetime. + + Issues a warning if the column is datetime-like, recommending + the use of a time-based plot. + + Args: + df (pd.DataFrame): The dataframe containing the column to check. + x_col (str): The column to check for datetime-like values. + """ + if pd.api.types.is_datetime64_any_dtype(df[x_col]): + logging.warning( + "The column '%s' is a datetime column. Consider using the 'time_plot' function for time-based plots.", + x_col, + ) + else: + try: + pd.to_datetime(df[x_col]) + logging.warning( + "The column '%s' can be converted to datetime. Consider using the 'time_plot' module for time-based plots.", + x_col, + ) + except (ValueError, TypeError): + pass + + +def plot( + df: pd.DataFrame, + value_col: str | list[str], + group_col: str | None = None, + x_col: str | None = None, + title: str | None = None, + x_label: str | None = None, + y_label: str | None = None, + legend_title: str | None = None, + ax: Axes | None = None, + source_text: str | None = None, + move_legend_outside: bool = False, + **kwargs: dict[str, any], +) -> SubplotBase: + """Plots the `value_col` over the specified `x_col` or index, creating a separate line for each unique value in `group_col`. + + Args: + df (pd.DataFrame): The dataframe to plot. + value_col (str or list of str): The column(s) to plot. + group_col (str, optional): The column used to define different lines. + x_col (str, optional): The column to be used as the x-axis. If None, the index is used. + title (str, optional): The title of the plot. + x_label (str, optional): The x-axis label. + y_label (str, optional): The y-axis label. + legend_title (str, optional): The title of the legend. + ax (Axes, optional): Matplotlib axes object to plot on. + source_text (str, optional): The source text to add to the plot. + move_legend_outside (bool, optional): Move the legend outside the plot. + **kwargs: Additional keyword arguments for Pandas' `plot` function. + + Returns: + SubplotBase: The matplotlib axes object. + """ + if x_col is not None: + _check_datetime_column(df, x_col) + + if group_col is not None: + unique_groups = df[group_col].unique() + colors = [ + COLORS[color][shade] + for shade in [500, 300, 700] + for color in ["green", "orange", "red", "blue", "yellow", "violet", "pink"] + ][: len(unique_groups)] + + ax = None + for i, group in enumerate(unique_groups): + group_df = df[df[group_col] == group] + + if x_col is not None: + ax = group_df.plot( + x=x_col, + y=value_col, + ax=ax, + linewidth=3, + color=colors[i], + label=group, + legend=True, + **kwargs, + ) + else: + ax = group_df.plot( + y=value_col, + ax=ax, + linewidth=3, + color=colors[i], + label=group, + legend=True, + **kwargs, + ) + else: + colors = COLORS["green"][500] + if x_col is not None: + ax = df.plot( + x=x_col, + y=value_col, + linewidth=3, + color=colors, + legend=False, + ax=ax, + **kwargs, + ) + else: + ax = df.plot( + y=value_col, + linewidth=3, + color=colors, + legend=False, + ax=ax, + **kwargs, + ) + + ax = gu.standard_graph_styles( + ax, + title=title if title else f"{value_col.title()} Over {x_col.title()}" if x_col else f"{value_col.title()} Over Index", + x_label=x_label if x_label else (x_col.title() if x_col else "Index"), + y_label=y_label or value_col.title(), + ) + + if move_legend_outside: + ax.legend(bbox_to_anchor=(1.05, 1)) + + if legend_title is not None: + ax.legend(title=legend_title) + + if source_text: + ax.annotate( + source_text, + xy=(-0.1, -0.2), + xycoords="axes fraction", + ha="left", + va="center", + fontsize=GraphStyles.DEFAULT_SOURCE_FONT_SIZE, + fontproperties=GraphStyles.POPPINS_LIGHT_ITALIC, + color="dimgray", + ) + + for tick in ax.get_xticklabels() + ax.get_yticklabels(): + tick.set_fontproperties(GraphStyles.POPPINS_REG) + + return ax From 86f3e713ed8fe9ba6475c108f194c54a059b480e Mon Sep 17 00:00:00 2001 From: Vas Date: Fri, 20 Sep 2024 12:46:21 +0300 Subject: [PATCH 02/16] chore: simplify code, reuse existing functionality and add more documentation examples --- pyretailscience/plots/line.py | 217 +++++++++++++-------------- pyretailscience/style/graph_utils.py | 2 +- pyretailscience/style/tailwind.py | 22 +-- 3 files changed, 105 insertions(+), 136 deletions(-) diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index abfbf9d..ba59e24 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -24,10 +24,50 @@ ### Common Scenarios and Examples: -1. **Basic Plot Showing Price Trends Since Competitor Store Opened**: +1. **Basic Plot Showing Daily Revenue Trends Since The Start of the Current Year**: + + This example demonstrates how to plot the `total_price` on a daily basis since the start of the year. + + **Preparing the Data**: + ```python + import numpy as np + import pandas as pd + + # Convert 'transaction_datetime' to a datetime column if it's not already + df['transaction_datetime'] = pd.to_datetime(df['transaction_datetime']) + + # Create a column for days since the start of the year + df['days_since_start_of_year'] = (df['transaction_datetime'] - pd.Timestamp(f'{df["transaction_datetime"].dt.year[0]}-01-01')).dt.days + + # Aggregate data by days since the start of the year + df_daily = df.groupby('days_since_start_of_year').agg({'total_price': 'sum'}).reset_index() + + ``` + + **Plotting**: + ```python + ax = line.plot( + df=df_daily, + value_col="total_price", # Plot 'total_price' values + x_col="days_since_start_of_year", # Use 'days_since_start_of_year' as the x-axis + title="Daily Revenue Since Start of Year", # Title of the plot + x_label="Days Since Start of Year", # X-axis label + y_label="Daily Revenue", # Y-axis label + ) + + plt.show() + ``` + + **Use Case**: This is useful when you want to illustrate revenue trends throughout the year on a daily basis. The + x-axis represents days since the last year and the y-axis shows how revenue behaved over this period. + +--- + + +2. **Basic Plot Showing Price Trends Since Competitor Store Opened**: This example demonstrates how to plot the `total_price` over the number of months since a competitor - store opened. The total price remains stable or increases slightly before the store opened, and then + store opened. The total revenue remains stable or increases slightly before the store opened, and then drops randomly after the competitor's store opened. **Preparing the Data**: @@ -46,7 +86,7 @@ competitor_opened_month_index = int(len(df_monthly) * 0.6) df_monthly['months_since_competitor_opened'] = np.arange(-competitor_opened_month_index, len(df_monthly) - competitor_opened_month_index) - # Simulate stable or increasing prices before competitor opened + # Simulate stable or increasing revenue before competitor opened df_monthly.loc[df_monthly['months_since_competitor_opened'] < 0, 'total_price'] *= np.random.uniform(1.05, 1.2) # Simulate a random drop after the competitor opened @@ -59,9 +99,9 @@ df=df_monthly, value_col="total_price", # Plot 'total_price' values x_col="months_since_competitor_opened", # Use 'months_since_competitor_opened' as the x-axis - title="Total Price Since Competitor Store Opened", # Title of the plot + title="Total Revenue Since Competitor Store Opened", # Title of the plot x_label="Months Since Competitor Opened", # X-axis label - y_label="Total Price", # Y-axis label + y_label="Total Revenue", # Y-axis label ) plt.show() @@ -69,13 +109,13 @@ **Use Case**: This is useful when you want to illustrate the effect of a competitor store opening on sales performance. The x-axis represents months before and after the event, and the y-axis shows - how prices behaved over time. + how revenue behaved over time. --- -2. **Plotting Price Trends by Category (Top 3 Categories)**: +3. **Plotting Price Trends by Category (Top 3 Categories)**: - This example plots the total price for the top 3 categories before and after the competitor opened. + This example plots the total revenue for the top 3 categories before and after the competitor opened. The data is resampled by month, split by category, and tracks the months since the competitor store opened. **Preparing the Data**: @@ -125,9 +165,9 @@ value_col="total_price", # Plot 'total_price' values group_col="category_0_name", # Separate lines for each category x_col="months_since_competitor_opened", # Use 'months_since_competitor_opened' as the x-axis - title="Total Price for Top 3 Categories (Before and After Competitor Opened)", # Title of the plot + title="Total Revenue for Top 3 Categories (Before and After Competitor Opened)", # Title of the plot x_label="Months Since Competitor Opened", # X-axis label - y_label="Total Price", # Y-axis label + y_label="Total Revenue", # Y-axis label legend_title="Category" # Legend title ) @@ -156,17 +196,17 @@ - `pyretailscience.style.graph_utils`: For applying consistent graph styles across the plots. """ - -import logging +import warnings +from typing import TYPE_CHECKING import pandas as pd from matplotlib.axes import Axes, SubplotBase import pyretailscience.style.graph_utils as gu -from pyretailscience.style.graph_utils import GraphStyles -from pyretailscience.style.tailwind import COLORS +from pyretailscience.style.tailwind import get_base_cmap -logging.basicConfig(format="%(message)s", level=logging.INFO) +if TYPE_CHECKING: + from matplotlib.colors import ListedColormap def _check_datetime_column(df: pd.DataFrame, x_col: str) -> None: @@ -179,46 +219,47 @@ def _check_datetime_column(df: pd.DataFrame, x_col: str) -> None: df (pd.DataFrame): The dataframe containing the column to check. x_col (str): The column to check for datetime-like values. """ - if pd.api.types.is_datetime64_any_dtype(df[x_col]): - logging.warning( - "The column '%s' is a datetime column. Consider using the 'time_plot' function for time-based plots.", - x_col, + if x_col not in df.columns: + msg = f"The column '{x_col}' is not present in the dataframe." + raise KeyError(msg) + + try: + pd.to_datetime(df[x_col], errors="raise") + warnings.warn( + f"The column '{x_col}' can be converted to datetime. Consider using the 'time_plot' module for time-based " + f"plots.", + UserWarning, + stacklevel=2, ) - else: - try: - pd.to_datetime(df[x_col]) - logging.warning( - "The column '%s' can be converted to datetime. Consider using the 'time_plot' module for time-based plots.", - x_col, - ) - except (ValueError, TypeError): - pass + + except (ValueError, TypeError): + return def plot( - df: pd.DataFrame, - value_col: str | list[str], - group_col: str | None = None, - x_col: str | None = None, - title: str | None = None, - x_label: str | None = None, - y_label: str | None = None, - legend_title: str | None = None, - ax: Axes | None = None, - source_text: str | None = None, - move_legend_outside: bool = False, - **kwargs: dict[str, any], + df: pd.DataFrame, + value_col: str | list[str], + x_label: str, + y_label: str, + title: str, + x_col: str | None = None, + group_col: str | None = None, + legend_title: str | None = None, + ax: Axes | None = None, + source_text: str | None = None, + move_legend_outside: bool = False, + **kwargs: dict[str, any], ) -> SubplotBase: """Plots the `value_col` over the specified `x_col` or index, creating a separate line for each unique value in `group_col`. Args: df (pd.DataFrame): The dataframe to plot. value_col (str or list of str): The column(s) to plot. - group_col (str, optional): The column used to define different lines. + x_label (str): The x-axis label. + y_label (str): The y-axis label. + title (str): The title of the plot. x_col (str, optional): The column to be used as the x-axis. If None, the index is used. - title (str, optional): The title of the plot. - x_label (str, optional): The x-axis label. - y_label (str, optional): The y-axis label. + group_col (str, optional): The column used to define different lines. legend_title (str, optional): The title of the legend. ax (Axes, optional): Matplotlib axes object to plot on. source_text (str, optional): The source text to add to the plot. @@ -231,66 +272,26 @@ def plot( if x_col is not None: _check_datetime_column(df, x_col) + colors: ListedColormap = get_base_cmap() + if group_col is not None: - unique_groups = df[group_col].unique() - colors = [ - COLORS[color][shade] - for shade in [500, 300, 700] - for color in ["green", "orange", "red", "blue", "yellow", "violet", "pink"] - ][: len(unique_groups)] - - ax = None - for i, group in enumerate(unique_groups): - group_df = df[df[group_col] == group] - - if x_col is not None: - ax = group_df.plot( - x=x_col, - y=value_col, - ax=ax, - linewidth=3, - color=colors[i], - label=group, - legend=True, - **kwargs, - ) - else: - ax = group_df.plot( - y=value_col, - ax=ax, - linewidth=3, - color=colors[i], - label=group, - legend=True, - **kwargs, - ) + pivot_df = df.pivot(index=x_col if x_col is not None else None, columns=group_col, values=value_col) else: - colors = COLORS["green"][500] - if x_col is not None: - ax = df.plot( - x=x_col, - y=value_col, - linewidth=3, - color=colors, - legend=False, - ax=ax, - **kwargs, - ) - else: - ax = df.plot( - y=value_col, - linewidth=3, - color=colors, - legend=False, - ax=ax, - **kwargs, - ) + pivot_df = df.set_index(x_col if x_col is not None else df.index)[value_col] + + ax = pivot_df.plot( + ax=ax, + linewidth=3, + color=colors.colors[: len(pivot_df.columns) if group_col else 1], + legend=(group_col is not None), + **kwargs, + ) ax = gu.standard_graph_styles( ax, - title=title if title else f"{value_col.title()} Over {x_col.title()}" if x_col else f"{value_col.title()} Over Index", - x_label=x_label if x_label else (x_col.title() if x_col else "Index"), - y_label=y_label or value_col.title(), + title=title, + x_label=x_label, + y_label=y_label, ) if move_legend_outside: @@ -299,19 +300,7 @@ def plot( if legend_title is not None: ax.legend(title=legend_title) - if source_text: - ax.annotate( - source_text, - xy=(-0.1, -0.2), - xycoords="axes fraction", - ha="left", - va="center", - fontsize=GraphStyles.DEFAULT_SOURCE_FONT_SIZE, - fontproperties=GraphStyles.POPPINS_LIGHT_ITALIC, - color="dimgray", - ) - - for tick in ax.get_xticklabels() + ax.get_yticklabels(): - tick.set_fontproperties(GraphStyles.POPPINS_REG) + if source_text is not None: + gu.add_source_text(ax=ax, source_text=source_text) - return ax + return gu.standard_tick_styles(ax) diff --git a/pyretailscience/style/graph_utils.py b/pyretailscience/style/graph_utils.py index 8f4dad4..b5462e0 100644 --- a/pyretailscience/style/graph_utils.py +++ b/pyretailscience/style/graph_utils.py @@ -132,7 +132,7 @@ def standard_tick_styles(ax: Axes) -> Axes: def not_none(value1: any, value2: any) -> any: - """Helper funciont that returns the first value that is not None. + """Helper function that returns the first value that is not None. Args: value1: The first value. diff --git a/pyretailscience/style/tailwind.py b/pyretailscience/style/tailwind.py index fc0a1ca..076fd8d 100644 --- a/pyretailscience/style/tailwind.py +++ b/pyretailscience/style/tailwind.py @@ -345,28 +345,8 @@ def get_base_cmap() -> ListedColormap: Returns: ListedColormap: A ListedColormap with all the Tailwind colors. """ - color_order = [ - "red", - "orange", - "yellow", - "green", - "teal", - "sky", - "indigo", - "purple", - "pink", - "slate", - "amber", - "lime", - "emerald", - "cyan", - "blue", - "violet", - "fuchsia", - "rose", - ] + color_order = ["green", "orange", "red", "blue", "yellow", "violet", "pink"] color_numbers = [500, 300, 700] - colors = [] colors = [COLORS[color][color_number] for color_number in color_numbers for color in color_order] return ListedColormap(colors) From 3640885f6fb58cda1482e3336cc3606447df9874 Mon Sep 17 00:00:00 2001 From: Vas Date: Fri, 20 Sep 2024 17:48:12 +0300 Subject: [PATCH 03/16] chore: add tests and documentation --- docs/analysis_modules.md | 44 + docs/api/plots/line.md | 3 + .../images/analysis_modules/line_plot.svg | 2015 +++++++++++++++++ mkdocs.yml | 1 + pyretailscience/plots/line.py | 256 +-- tests/plots/__init__.py | 0 tests/plots/test_line.py | 149 ++ 7 files changed, 2244 insertions(+), 224 deletions(-) create mode 100644 docs/api/plots/line.md create mode 100644 docs/assets/images/analysis_modules/line_plot.svg create mode 100644 tests/plots/__init__.py create mode 100644 tests/plots/test_line.py diff --git a/docs/analysis_modules.md b/docs/analysis_modules.md index ea4ff75..d504b30 100644 --- a/docs/analysis_modules.md +++ b/docs/analysis_modules.md @@ -7,6 +7,50 @@ social: ## Plots +### Line Plot + +
+ +![Image title](assets/images/analysis_modules/line_plot.svg){ align=right loading=lazy width="50%"} + +Line plots are particularly good for visualizing sequences that resemble time-based data, such as: + +- Days since an event (e.g., -2, -1, 0, 1, 2) +- Months since a competitor opened +- Tracking how metrics change across key events + +They are often used to compare trends across categories, show the impact of events on performance, and visualize changes over time-like sequences. + +Note: This module is not intended for actual datetime values. For time-based plots using dates, refer to the **timeline** module. + +
+ +Example: + +```python +import pandas as pd +from pyretailscience.plots import line + +df = pd.DataFrame({ + "months_since_event": range(-5, 6), + "category_A": [10000, 12000, 13000, 15000, 16000, 17000, 18000, 20000, 21000, 20030, 25000], + "category_B": [9000, 10000, 11000, 13000, 14000, 15000, 10000, 7000, 3500, 3000, 2800], +}) + +line.plot( + df=df, + value_col=["category_A", "category_B"], + x_label="Months Since Event", + y_label="Revenue (£)", + title="Revenue Trends across Categories", + x_col="months_since_event", + group_col=None, + source_text="Source: PyRetailScience - 2024", + move_legend_outside=True, +) +``` + + ### Waterfall Plot
diff --git a/docs/api/plots/line.md b/docs/api/plots/line.md new file mode 100644 index 0000000..3944567 --- /dev/null +++ b/docs/api/plots/line.md @@ -0,0 +1,3 @@ +# Line Plot + +::: pyretailscience.plots.line diff --git a/docs/assets/images/analysis_modules/line_plot.svg b/docs/assets/images/analysis_modules/line_plot.svg new file mode 100644 index 0000000..552c6b0 --- /dev/null +++ b/docs/assets/images/analysis_modules/line_plot.svg @@ -0,0 +1,2015 @@ + + + + + + + + 2024-09-20T17:30:12.256600 + image/svg+xml + + + Matplotlib v3.7.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mkdocs.yml b/mkdocs.yml index 4903793..25220be 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,7 @@ nav: - Standard Graphs: api/standard_graphs.md - Cross Shop Analysis: api/cross_shop.md - Product Association: api/product_association.md + - Line Plot: api/plots/line.md theme: name: material diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index ba59e24..b001651 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -1,203 +1,38 @@ -"""This module provides versatile plotting functionality for creating line plots from pandas DataFrames. +"""This module provides flexible functionality for creating line plots from pandas DataFrames. -It is designed for plotting sequences that resemble time-based data, such as "days" or "months" since -an event, but it does not explicitly handle datetime values. For actual time-based plots (using datetime -objects), please refer to the `time_plot` module. +It focuses on visualizing sequences that resemble time-based data, such as "days since an event" or "months since a +competitor opened." However, it does not explicitly handle datetime values. For actual time-based plots using +datetime objects, please refer to the **`timeline`** module. -The sequences used in this module can include values such as "days since an event" (e.g., -2, -1, 0, 1, 2) -or "months since a competitor store opened." **This module is not intended for use with actual datetime values**. -If a datetime or datetime-like column is passed as `x_col`, a warning will be triggered suggesting the use -of the `time_plot` module. +The sequences used in this module can include values like "days since an event" (e.g., -2, -1, 0, 1, 2) or "months +since a competitor store opened." **This module is not intended for use with actual datetime values**. If a datetime +or datetime-like column is passed as **`x_col`**, a warning will be triggered, suggesting the use of the +**`timeline`** module. -Key Features: --------------- -- **Plotting Sequences or Indexes**: Plot one or more value columns (`value_col`), supporting sequences - like -2, -1, 0, 1, 2 (e.g., months since an event), using either the index or a specified x-axis - column (`x_col`). -- **Custom X-Axis or Index**: Use any column as the x-axis (`x_col`), or plot based on the index if no - x-axis column is specified. -- **Multiple Lines**: Create separate lines for each unique value in `group_col` (e.g., categories). -- **Comprehensive Customization**: Easily customize titles, axis labels, legends, and optionally move - the legend outside the plot. -- **Pre-Aggregated Data**: The data must be pre-aggregated before plotting. No aggregation occurs in - this module. +### Core Features -### Common Scenarios and Examples: +- **Plotting Sequences or Indexes**: Plot one or more value columns (**`value_col`**) with support for sequences like +-2, -1, 0, 1, 2 (e.g., months since an event), using either the index or a specified x-axis column (**`x_col`**). +- **Custom X-Axis or Index**: Use any column as the x-axis (**`x_col`**) or plot based on the index if no x-axis column is specified. +- **Multiple Lines**: Create separate lines for each unique value in **`group_col`** (e.g., categories or product types). +- **Comprehensive Customization**: Easily customize plot titles, axis labels, and legends, with the option to move the legend outside the plot. +- **Pre-Aggregated Data**: The data must be pre-aggregated before plotting, as no aggregation occurs within the module. -1. **Basic Plot Showing Daily Revenue Trends Since The Start of the Current Year**: +### Use Cases - This example demonstrates how to plot the `total_price` on a daily basis since the start of the year. +- **Daily Trends**: Plot trends such as daily revenue or user activity, for example, tracking revenue since the start of the year. +- **Event Impact**: Visualize how metrics (e.g., revenue, sales, or traffic) change before and after an important event, such as a competitor store opening or a product launch. +- **Category Comparison**: Compare metrics across multiple categories over time, for example, tracking total revenue for the top categories before and after an event like the introduction of a new competitor. - **Preparing the Data**: - ```python - import numpy as np - import pandas as pd +### Limitations and Handling of Temporal Data - # Convert 'transaction_datetime' to a datetime column if it's not already - df['transaction_datetime'] = pd.to_datetime(df['transaction_datetime']) - - # Create a column for days since the start of the year - df['days_since_start_of_year'] = (df['transaction_datetime'] - pd.Timestamp(f'{df["transaction_datetime"].dt.year[0]}-01-01')).dt.days - - # Aggregate data by days since the start of the year - df_daily = df.groupby('days_since_start_of_year').agg({'total_price': 'sum'}).reset_index() - - ``` - - **Plotting**: - ```python - ax = line.plot( - df=df_daily, - value_col="total_price", # Plot 'total_price' values - x_col="days_since_start_of_year", # Use 'days_since_start_of_year' as the x-axis - title="Daily Revenue Since Start of Year", # Title of the plot - x_label="Days Since Start of Year", # X-axis label - y_label="Daily Revenue", # Y-axis label - ) - - plt.show() - ``` - - **Use Case**: This is useful when you want to illustrate revenue trends throughout the year on a daily basis. The - x-axis represents days since the last year and the y-axis shows how revenue behaved over this period. - ---- - - -2. **Basic Plot Showing Price Trends Since Competitor Store Opened**: - - This example demonstrates how to plot the `total_price` over the number of months since a competitor - store opened. The total revenue remains stable or increases slightly before the store opened, and then - drops randomly after the competitor's store opened. - - **Preparing the Data**: - ```python - import numpy as np - - # Convert 'transaction_datetime' to a datetime column if it's not already - df['transaction_datetime'] = pd.to_datetime(df['transaction_datetime']) - - # Resample the data by month - df['month'] = df['transaction_datetime'].dt.to_period('M') # Extract year and month - df_monthly = df.groupby('month').agg({'total_price': 'sum'}).reset_index() - - # Create the "months since competitor opened" column - # Assume the competitor opened 60% of the way through the data - competitor_opened_month_index = int(len(df_monthly) * 0.6) - df_monthly['months_since_competitor_opened'] = np.arange(-competitor_opened_month_index, len(df_monthly) - competitor_opened_month_index) - - # Simulate stable or increasing revenue before competitor opened - df_monthly.loc[df_monthly['months_since_competitor_opened'] < 0, 'total_price'] *= np.random.uniform(1.05, 1.2) - - # Simulate a random drop after the competitor opened - df_monthly.loc[df_monthly['months_since_competitor_opened'] >= 0, 'total_price'] *= np.random.uniform(0.8, 0.95, size=len(df_monthly[df_monthly['months_since_competitor_opened'] >= 0])) - ``` - - **Plotting**: - ```python - ax = line.plot( - df=df_monthly, - value_col="total_price", # Plot 'total_price' values - x_col="months_since_competitor_opened", # Use 'months_since_competitor_opened' as the x-axis - title="Total Revenue Since Competitor Store Opened", # Title of the plot - x_label="Months Since Competitor Opened", # X-axis label - y_label="Total Revenue", # Y-axis label - ) - - plt.show() - ``` - - **Use Case**: This is useful when you want to illustrate the effect of a competitor store opening - on sales performance. The x-axis represents months before and after the event, and the y-axis shows - how revenue behaved over time. - ---- - -3. **Plotting Price Trends by Category (Top 3 Categories)**: - - This example plots the total revenue for the top 3 categories before and after the competitor opened. - The data is resampled by month, split by category, and tracks the months since the competitor store opened. - - **Preparing the Data**: - ```python - import numpy as np - import pandas as pd - - # Convert 'transaction_datetime' to a datetime column if it's not already - df['transaction_datetime'] = pd.to_datetime(df['transaction_datetime']) - - # Resample the data by month and category - df['month'] = df['transaction_datetime'].dt.to_period('M') # Extract year and month - df_monthly = df.groupby(['month', 'category_0_name']).agg({'total_price': 'sum'}).reset_index() - - # Create a separate dataframe for unique months to track "months since competitor opened" - unique_months = df_monthly['month'].unique() - competitor_opened_month_index = int(len(unique_months) * 0.6) # Assume competitor opened 60% of the way through - - # Create 'months_since_competitor_opened' for each unique month - months_since_competitor_opened = np.concatenate([ - np.arange(-competitor_opened_month_index, 0), # Before competitor opened - np.arange(0, len(unique_months) - competitor_opened_month_index) # After competitor opened - ]) - - # Create a new dataframe with the 'months_since_competitor_opened' values and merge it back - months_df = pd.DataFrame({'month': unique_months, 'months_since_competitor_opened': months_since_competitor_opened}) - df_monthly = df_monthly.merge(months_df, on='month', how='left') - - # Filter to include months both before and after the competitor opened - df_since_competitor_opened = df_monthly[(df_monthly['months_since_competitor_opened'] >= -6) & # Include 6 months before - (df_monthly['months_since_competitor_opened'] <= 12)] # Include 12 months after - - # Identify top 3 categories based on total_price across the selected period - category_totals = df_since_competitor_opened.groupby('category_0_name')['total_price'].sum().sort_values(ascending=False) - - # Get the top 3 categories - top_categories = category_totals.head(3).index - - # Filter the dataframe to include only the top 3 categories - df_top_categories = df_since_competitor_opened[df_since_competitor_opened['category_0_name'].isin(top_categories)] - ``` - - **Plotting**: - ```python - ax = line.plot( - df=df_top_categories, - value_col="total_price", # Plot 'total_price' values - group_col="category_0_name", # Separate lines for each category - x_col="months_since_competitor_opened", # Use 'months_since_competitor_opened' as the x-axis - title="Total Revenue for Top 3 Categories (Before and After Competitor Opened)", # Title of the plot - x_label="Months Since Competitor Opened", # X-axis label - y_label="Total Revenue", # Y-axis label - legend_title="Category" # Legend title - ) - - plt.show() - ``` - - **Use Case**: Use this when you want to analyze the behavior of specific top categories before and after - an event, such as the opening of a competitor store. - ---- - -### Customization Options: -- **`value_col`**: The column or list of columns to plot (e.g., `'total_price'`). -- **`group_col`**: A column whose unique values will be used to create separate lines (e.g., `'category_0_name'`). -- **`x_col`**: The column to use as the x-axis (e.g., `'months_since_competitor_opened'`). **Warning**: If a datetime - or datetime-like column is passed, a warning will suggest using the `time_plot` module instead. -- **`title`**, **`x_label`**, **`y_label`**: Custom text for the plot title and axis labels. -- **`legend_title`**: Custom title for the legend based on `group_col`. -- **`move_legend_outside`**: Boolean flag to move the legend outside the plot. - ---- - -### Dependencies: -- `pandas`: For DataFrame manipulation and grouping. -- `matplotlib`: For generating plots. -- `pyretailscience.style.graph_utils`: For applying consistent graph styles across the plots. +- **Limited Handling of Temporal Data**: This module can plot simple time-based sequences, such as "days since an event," but it cannot manipulate or directly handle datetime or date-like columns. It is not optimized for actual datetime values. +If a datetime column is passed or more complex temporal plotting is needed, a warning will suggest using the **`timeline`** module, which is specifically designed for working with temporal data and performing time-based manipulation. +- **Pre-Aggregated Data Required**: The module does not perform any data aggregation, so all data must be pre-aggregated before being passed in for plotting. """ + import warnings -from typing import TYPE_CHECKING import pandas as pd from matplotlib.axes import Axes, SubplotBase @@ -205,36 +40,6 @@ import pyretailscience.style.graph_utils as gu from pyretailscience.style.tailwind import get_base_cmap -if TYPE_CHECKING: - from matplotlib.colors import ListedColormap - - -def _check_datetime_column(df: pd.DataFrame, x_col: str) -> None: - """Checks if the x_col is a datetime or convertible to datetime. - - Issues a warning if the column is datetime-like, recommending - the use of a time-based plot. - - Args: - df (pd.DataFrame): The dataframe containing the column to check. - x_col (str): The column to check for datetime-like values. - """ - if x_col not in df.columns: - msg = f"The column '{x_col}' is not present in the dataframe." - raise KeyError(msg) - - try: - pd.to_datetime(df[x_col], errors="raise") - warnings.warn( - f"The column '{x_col}' can be converted to datetime. Consider using the 'time_plot' module for time-based " - f"plots.", - UserWarning, - stacklevel=2, - ) - - except (ValueError, TypeError): - return - def plot( df: pd.DataFrame, @@ -269,10 +74,13 @@ def plot( Returns: SubplotBase: The matplotlib axes object. """ - if x_col is not None: - _check_datetime_column(df, x_col) - - colors: ListedColormap = get_base_cmap() + if x_col is not None and pd.api.types.is_datetime64_any_dtype(df[x_col]): + warnings.warn( + f"The column '{x_col}' is datetime-like. Consider using the 'timeline' module for time-based plots.", + UserWarning, + stacklevel=2, + ) + colors = get_base_cmap() if group_col is not None: pivot_df = df.pivot(index=x_col if x_col is not None else None, columns=group_col, values=value_col) @@ -282,7 +90,7 @@ def plot( ax = pivot_df.plot( ax=ax, linewidth=3, - color=colors.colors[: len(pivot_df.columns) if group_col else 1], + color=colors.colors[: len(pivot_df.columns) if group_col is not None else 1], legend=(group_col is not None), **kwargs, ) diff --git a/tests/plots/__init__.py b/tests/plots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/plots/test_line.py b/tests/plots/test_line.py new file mode 100644 index 0000000..04e48c8 --- /dev/null +++ b/tests/plots/test_line.py @@ -0,0 +1,149 @@ +"""Tests for the plots.line module.""" + +import warnings + +import matplotlib.pyplot as plt +import pandas as pd +import pytest +from matplotlib.axes import Axes +from matplotlib.colors import ListedColormap + +from pyretailscience.plots import line +from pyretailscience.style import graph_utils as gu + + +@pytest.fixture() +def sample_dataframe(): + """A sample dataframe for testing.""" + data = { + "x": pd.date_range("2023-01-01", periods=10, freq="D"), + "y": range(10, 20), + "group": ["A"] * 5 + ["B"] * 5, + } + return pd.DataFrame(data) + + +@pytest.fixture() +def _mock_get_base_cmap(mocker): + """Mock the get_base_cmap function to return a custom colormap.""" + cmap = ListedColormap(["#FF0000", "#00FF00", "#0000FF"]) + mocker.patch("pyretailscience.style.tailwind.get_base_cmap", return_value=cmap) + + +@pytest.fixture() +def _mock_gu_functions(mocker): + mocker.patch("pyretailscience.style.graph_utils.standard_graph_styles", side_effect=lambda ax, **kwargs: ax) + mocker.patch("pyretailscience.style.graph_utils.standard_tick_styles", side_effect=lambda ax: ax) + mocker.patch("pyretailscience.style.graph_utils.add_source_text", side_effect=lambda ax, source_text: ax) + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_with_group_col(sample_dataframe): + """Test the plot function with a group column.""" + _, ax = plt.subplots() + + result_ax = line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot", + x_col="x", + group_col="group", + ax=ax, + ) + expected_num_lines = 2 + + assert isinstance(result_ax, Axes) + assert len(result_ax.get_lines()) == expected_num_lines # One line for each group + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_without_group_col(sample_dataframe): + """Test the plot function without a group column.""" + _, ax = plt.subplots() + + result_ax = line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot Without Group", + x_col="x", + ax=ax, + ) + expected_num_lines = 1 + + assert isinstance(result_ax, Axes) + assert len(result_ax.get_lines()) == expected_num_lines # Only one line + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_warns_if_xcol_is_datetime(sample_dataframe, mocker): + """Test the plot function warns if the x_col is datetime-like.""" + mocker.patch("warnings.warn") + _, ax = plt.subplots() + + line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot Datetime Warning", + x_col="x", + group_col="group", + ax=ax, + ) + + warnings.warn.assert_called_once_with( + "The column 'x' is datetime-like. Consider using the 'timeline' module for time-based plots.", + UserWarning, + stacklevel=2, + ) + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_moves_legend_outside(sample_dataframe): + """Test the plot function moves the legend outside the plot.""" + _, ax = plt.subplots() + + # Test with move_legend_outside=True + result_ax = line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot Legend Outside", + x_col="x", + group_col="group", + ax=ax, + move_legend_outside=True, + ) + + expected_coords = (1.05, 1.0) + legend = result_ax.get_legend() + # Check if bbox_to_anchor is set to (1.05, 1) when legend is outside + bbox_anchor = legend.get_bbox_to_anchor()._bbox + + assert legend is not None + assert (bbox_anchor.x0, bbox_anchor.y0) == expected_coords + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_adds_source_text(sample_dataframe): + """Test the plot function adds source text to the plot.""" + _, ax = plt.subplots() + source_text = "Source: Test Data" + + result_ax = line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot Source Text", + x_col="x", + ax=ax, + source_text=source_text, + ) + + gu.add_source_text.assert_called_once_with(ax=result_ax, source_text=source_text) From 7f10fb2477d8c6150f9c3d49d6c4b9afa3016ed2 Mon Sep 17 00:00:00 2001 From: Vas Date: Fri, 20 Sep 2024 17:51:39 +0300 Subject: [PATCH 04/16] chore: add pytest-mock dependency --- poetry.lock | 204 +++++++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 1 + 2 files changed, 191 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index 876018c..91b4d6a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "anyio" version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -26,6 +27,7 @@ trio = ["trio (>=0.23)"] name = "appnope" version = "0.1.4" description = "Disable App Nap on macOS >= 10.9" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -37,6 +39,7 @@ files = [ name = "argon2-cffi" version = "23.1.0" description = "Argon2 for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -57,6 +60,7 @@ typing = ["mypy"] name = "argon2-cffi-bindings" version = "21.2.0" description = "Low-level CFFI bindings for Argon2" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -94,6 +98,7 @@ tests = ["pytest"] name = "arrow" version = "1.3.0" description = "Better dates & times for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -107,12 +112,13 @@ types-python-dateutil = ">=2.8.10" [package.extras] doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] +test = ["dateparser (>=1.0.0,<2.0.0)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (>=3.0.0,<4.0.0)"] [[package]] name = "asttokens" version = "2.4.1" description = "Annotate AST trees with source code positions" +category = "dev" optional = false python-versions = "*" files = [ @@ -131,6 +137,7 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] name = "async-lru" version = "2.0.4" description = "Simple LRU cache for asyncio" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -145,6 +152,7 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} name = "attrs" version = "24.2.0" description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -164,6 +172,7 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] name = "babel" version = "2.15.0" description = "Internationalization utilities" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -178,6 +187,7 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] name = "backports-strenum" version = "1.3.1" description = "Base class for creating enumerated constants that are also subclasses of str" +category = "dev" optional = false python-versions = ">=3.8.6,<3.11" files = [ @@ -189,6 +199,7 @@ files = [ name = "beautifulsoup4" version = "4.12.3" description = "Screen-scraping library" +category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -210,6 +221,7 @@ lxml = ["lxml"] name = "bleach" version = "6.1.0" description = "An easy safelist-based HTML-sanitizing tool." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -228,6 +240,7 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] name = "certifi" version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -239,6 +252,7 @@ files = [ name = "cffi" version = "1.17.0" description = "Foreign Function Interface for Python calling C code." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -318,6 +332,7 @@ pycparser = "*" name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -329,6 +344,7 @@ files = [ name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -428,6 +444,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -442,6 +459,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -453,6 +471,7 @@ files = [ name = "comm" version = "0.2.2" description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -470,6 +489,7 @@ test = ["pytest"] name = "contourpy" version = "1.2.1" description = "Python library for calculating contours of 2D quadrilateral grids" +category = "main" optional = false python-versions = ">=3.9" files = [ @@ -533,6 +553,7 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] name = "coverage" version = "7.6.1" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -620,6 +641,7 @@ toml = ["tomli"] name = "cycler" version = "0.12.1" description = "Composable style cycles" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -635,6 +657,7 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] name = "debugpy" version = "1.8.5" description = "An implementation of the Debug Adapter Protocol for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -666,6 +689,7 @@ files = [ name = "decorator" version = "5.1.1" description = "Decorators for Humans" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -677,6 +701,7 @@ files = [ name = "defusedxml" version = "0.7.1" description = "XML bomb protection for Python stdlib modules" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -688,6 +713,7 @@ files = [ name = "distlib" version = "0.3.8" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -699,6 +725,7 @@ files = [ name = "duckdb" version = "1.0.0" description = "DuckDB in-process database" +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -754,6 +781,7 @@ files = [ name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -768,6 +796,7 @@ test = ["pytest (>=6)"] name = "executing" version = "2.0.1" description = "Get the currently executing AST node of a frame, and other information" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -782,6 +811,7 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth name = "fastjsonschema" version = "2.20.0" description = "Fastest Python implementation of JSON schema" +category = "dev" optional = false python-versions = "*" files = [ @@ -796,6 +826,7 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc name = "filelock" version = "3.15.4" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -812,6 +843,7 @@ typing = ["typing-extensions (>=4.8)"] name = "fonttools" version = "4.53.1" description = "Tools to manipulate font files" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -877,6 +909,7 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] name = "fqdn" version = "1.5.1" description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +category = "dev" optional = false python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" files = [ @@ -888,6 +921,7 @@ files = [ name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." +category = "dev" optional = false python-versions = "*" files = [ @@ -905,6 +939,7 @@ dev = ["flake8", "markdown", "twine", "wheel"] name = "griffe" version = "0.48.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -920,6 +955,7 @@ colorama = ">=0.4" name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -931,6 +967,7 @@ files = [ name = "httpcore" version = "1.0.5" description = "A minimal low-level HTTP client." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -945,13 +982,14 @@ h11 = ">=0.13,<0.15" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] trio = ["trio (>=0.22.0,<0.26.0)"] [[package]] name = "httpx" version = "0.27.0" description = "The next generation HTTP client." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -962,20 +1000,21 @@ files = [ [package.dependencies] anyio = "*" certifi = "*" -httpcore = "==1.*" +httpcore = ">=1.0.0,<2.0.0" idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" version = "2.6.0" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -990,6 +1029,7 @@ license = ["ukkonen"] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1001,6 +1041,7 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1012,6 +1053,7 @@ files = [ name = "ipykernel" version = "6.29.5" description = "IPython Kernel for Jupyter" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1025,7 +1067,7 @@ comm = ">=0.1.1" debugpy = ">=1.6.5" ipython = ">=7.23.1" jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0.0 || >=5.1.0" matplotlib-inline = ">=0.1" nest-asyncio = "*" packaging = "*" @@ -1045,6 +1087,7 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio name = "ipython" version = "8.26.0" description = "IPython: Productive Interactive Computing" +category = "dev" optional = false python-versions = ">=3.10" files = [ @@ -1083,6 +1126,7 @@ test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "num name = "isoduration" version = "20.11.0" description = "Operations with ISO 8601 durations" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1097,6 +1141,7 @@ arrow = ">=0.15.0" name = "jedi" version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1116,6 +1161,7 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1133,6 +1179,7 @@ i18n = ["Babel (>=2.7)"] name = "joblib" version = "1.4.2" description = "Lightweight pipelining with Python functions" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1144,6 +1191,7 @@ files = [ name = "json5" version = "0.9.25" description = "A Python implementation of the JSON5 data format." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1155,6 +1203,7 @@ files = [ name = "jsonpointer" version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1166,6 +1215,7 @@ files = [ name = "jsonschema" version = "4.23.0" description = "An implementation of JSON Schema validation for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1195,6 +1245,7 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "jsonschema-specifications" version = "2023.12.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1209,6 +1260,7 @@ referencing = ">=0.31.0" name = "jupyter-client" version = "8.6.2" description = "Jupyter protocol implementation and client libraries" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1217,7 +1269,7 @@ files = [ ] [package.dependencies] -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0.0 || >=5.1.0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" tornado = ">=6.2" @@ -1231,6 +1283,7 @@ test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pyt name = "jupyter-core" version = "5.7.2" description = "Jupyter core package. A base package on which Jupyter projects rely." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1251,6 +1304,7 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout" name = "jupyter-events" version = "0.10.0" description = "Jupyter Event System library" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1276,6 +1330,7 @@ test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "p name = "jupyter-lsp" version = "2.2.5" description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1290,6 +1345,7 @@ jupyter-server = ">=1.1.2" name = "jupyter-server" version = "2.14.2" description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1302,7 +1358,7 @@ anyio = ">=3.1.0" argon2-cffi = ">=21.1" jinja2 = ">=3.0.3" jupyter-client = ">=7.4.4" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0.0 || >=5.1.0" jupyter-events = ">=0.9.0" jupyter-server-terminals = ">=0.4.4" nbconvert = ">=6.4.4" @@ -1326,6 +1382,7 @@ test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console name = "jupyter-server-terminals" version = "0.5.3" description = "A Jupyter Server Extension Providing Terminals." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1345,6 +1402,7 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> name = "jupyterlab" version = "4.2.5" description = "JupyterLab computational environment" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1379,6 +1437,7 @@ upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)" name = "jupyterlab-pygments" version = "0.3.0" description = "Pygments theme using JupyterLab CSS variables" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1390,6 +1449,7 @@ files = [ name = "jupyterlab-server" version = "2.27.3" description = "A set of server components for JupyterLab and JupyterLab like applications." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1415,6 +1475,7 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v name = "jupytext" version = "1.16.4" description = "Jupyter notebooks as Markdown documents, Julia, Python or R scripts" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1444,6 +1505,7 @@ test-ui = ["calysto-bash"] name = "kiwisolver" version = "1.4.5" description = "A fast implementation of the Cassowary constraint solver" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1557,6 +1619,7 @@ files = [ name = "loguru" version = "0.7.2" description = "Python logging made (stupidly) simple" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1575,6 +1638,7 @@ dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptio name = "markdown" version = "3.6" description = "Python implementation of John Gruber's Markdown." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1590,6 +1654,7 @@ testing = ["coverage", "pyyaml"] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1614,6 +1679,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1683,6 +1749,7 @@ files = [ name = "matplotlib" version = "3.9.1.post1" description = "Python plotting package" +category = "main" optional = false python-versions = ">=3.9" files = [ @@ -1735,6 +1802,7 @@ dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setupto name = "matplotlib-inline" version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1749,6 +1817,7 @@ traitlets = "*" name = "matplotlib-set-diagrams" version = "0.0.2" description = "Python drawing utilities for Venn and Euler diagrams visualizing the relationships between two or more sets." +category = "main" optional = false python-versions = ">3.6" files = [ @@ -1771,6 +1840,7 @@ tests = ["pytest", "pytest-cov", "pytest-mpl"] name = "mdit-py-plugins" version = "0.4.1" description = "Collection of plugins for markdown-it-py" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1790,6 +1860,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1801,6 +1872,7 @@ files = [ name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1812,6 +1884,7 @@ files = [ name = "mistune" version = "3.0.2" description = "A sane and fast Markdown parser with useful plugins and renderers" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1823,6 +1896,7 @@ files = [ name = "mkdocs" version = "1.6.0" description = "Project documentation with Markdown." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1853,6 +1927,7 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp name = "mkdocs-autorefs" version = "1.0.1" description = "Automatically link across pages in MkDocs." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1869,6 +1944,7 @@ mkdocs = ">=1.1" name = "mkdocs-get-deps" version = "0.2.0" description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1885,6 +1961,7 @@ pyyaml = ">=5.1" name = "mkdocs-jupyter" version = "0.24.8" description = "Use Jupyter in mkdocs websites" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1904,6 +1981,7 @@ pygments = ">2.12.0" name = "mkdocs-material" version = "9.5.31" description = "Documentation that simply works" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1933,6 +2011,7 @@ recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2. name = "mkdocs-material-extensions" version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1944,6 +2023,7 @@ files = [ name = "mkdocstrings" version = "0.24.3" description = "Automatic documentation from sources, for MkDocs." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1971,6 +2051,7 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] name = "mkdocstrings-python" version = "1.10.0" description = "A Python handler for mkdocstrings." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1986,6 +2067,7 @@ mkdocstrings = ">=0.24.2" name = "nbclient" version = "0.10.0" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -1995,7 +2077,7 @@ files = [ [package.dependencies] jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0.0 || >=5.1.0" nbformat = ">=5.1" traitlets = ">=5.4" @@ -2008,6 +2090,7 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>= name = "nbconvert" version = "7.16.4" description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2045,6 +2128,7 @@ webpdf = ["playwright"] name = "nbformat" version = "5.10.4" description = "The Jupyter Notebook format" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2055,7 +2139,7 @@ files = [ [package.dependencies] fastjsonschema = ">=2.15" jsonschema = ">=2.6" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0.0 || >=5.1.0" traitlets = ">=5.1" [package.extras] @@ -2066,6 +2150,7 @@ test = ["pep440", "pre-commit", "pytest", "testpath"] name = "nbstripout" version = "0.7.1" description = "Strips outputs from Jupyter and IPython notebooks" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2080,6 +2165,7 @@ nbformat = "*" name = "nest-asyncio" version = "1.6.0" description = "Patch asyncio to allow nested event loops" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -2091,6 +2177,7 @@ files = [ name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -2102,6 +2189,7 @@ files = [ name = "notebook-shim" version = "0.2.4" description = "A shim layer for notebook traits and config" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2119,6 +2207,7 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" name = "numpy" version = "1.26.4" description = "Fundamental package for array computing in Python" +category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2164,6 +2253,7 @@ files = [ name = "overrides" version = "7.7.0" description = "A decorator to automatically detect mismatch when overriding a method." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2175,6 +2265,7 @@ files = [ name = "packaging" version = "24.1" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2186,6 +2277,7 @@ files = [ name = "paginate" version = "0.5.6" description = "Divides large result sets into pages for easier browsing" +category = "dev" optional = false python-versions = "*" files = [ @@ -2196,6 +2288,7 @@ files = [ name = "pandas" version = "2.2.2" description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2268,6 +2361,7 @@ xml = ["lxml (>=4.9.2)"] name = "pandocfilters" version = "1.5.1" description = "Utilities for writing pandoc filters in python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2279,6 +2373,7 @@ files = [ name = "parso" version = "0.8.4" description = "A Python Parser" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2294,6 +2389,7 @@ testing = ["docopt", "pytest"] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2305,6 +2401,7 @@ files = [ name = "pexpect" version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." +category = "dev" optional = false python-versions = "*" files = [ @@ -2319,6 +2416,7 @@ ptyprocess = ">=0.5" name = "pillow" version = "10.4.0" description = "Python Imaging Library (Fork)" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2416,6 +2514,7 @@ xmp = ["defusedxml"] name = "platformdirs" version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2432,6 +2531,7 @@ type = ["mypy (>=1.8)"] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2447,6 +2547,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -2465,6 +2566,7 @@ virtualenv = ">=20.10.0" name = "prometheus-client" version = "0.20.0" description = "Python client for the Prometheus monitoring system." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2479,6 +2581,7 @@ twisted = ["twisted"] name = "prompt-toolkit" version = "3.0.47" description = "Library for building powerful interactive command lines in Python" +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -2493,6 +2596,7 @@ wcwidth = "*" name = "psutil" version = "6.0.0" description = "Cross-platform lib for process and system monitoring in Python." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2522,6 +2626,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" +category = "dev" optional = false python-versions = "*" files = [ @@ -2533,6 +2638,7 @@ files = [ name = "pure-eval" version = "0.2.3" description = "Safely evaluate AST nodes without side effects" +category = "dev" optional = false python-versions = "*" files = [ @@ -2547,6 +2653,7 @@ tests = ["pytest"] name = "pyarrow" version = "14.0.2" description = "Python library for Apache Arrow" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2595,6 +2702,7 @@ numpy = ">=1.16.6" name = "pycparser" version = "2.22" description = "C parser in Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2606,6 +2714,7 @@ files = [ name = "pygments" version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2620,6 +2729,7 @@ windows-terminal = ["colorama (>=0.4.6)"] name = "pymdown-extensions" version = "10.9" description = "Extension pack for Python Markdown." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2638,6 +2748,7 @@ extra = ["pygments (>=2.12)"] name = "pyparsing" version = "3.1.2" description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" optional = false python-versions = ">=3.6.8" files = [ @@ -2652,6 +2763,7 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "8.3.2" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2674,6 +2786,7 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2688,10 +2801,29 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2706,6 +2838,7 @@ six = ">=1.5" name = "python-json-logger" version = "2.0.7" description = "A python library adding a json log formatter" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2717,6 +2850,7 @@ files = [ name = "pytz" version = "2024.1" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -2728,6 +2862,7 @@ files = [ name = "pywin32" version = "306" description = "Python for Window Extensions" +category = "dev" optional = false python-versions = "*" files = [ @@ -2751,6 +2886,7 @@ files = [ name = "pywinpty" version = "2.0.13" description = "Pseudo terminal support for Windows from Python." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2766,6 +2902,7 @@ files = [ name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2828,6 +2965,7 @@ files = [ name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2842,6 +2980,7 @@ pyyaml = "*" name = "pyzmq" version = "26.1.0" description = "Python bindings for 0MQ" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2963,6 +3102,7 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} name = "referencing" version = "0.35.1" description = "JSON Referencing + Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2978,6 +3118,7 @@ rpds-py = ">=0.7.0" name = "regex" version = "2024.7.24" description = "Alternative regular expression module, to replace re." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3066,6 +3207,7 @@ files = [ name = "requests" version = "2.32.3" description = "Python HTTP for Humans." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3087,6 +3229,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "rfc3339-validator" version = "0.1.4" description = "A pure python RFC3339 validator" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3101,6 +3244,7 @@ six = "*" name = "rfc3986-validator" version = "0.1.1" description = "Pure python rfc3986 validator" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3112,6 +3256,7 @@ files = [ name = "rpds-py" version = "0.20.0" description = "Python bindings to Rust's persistent data structures (rpds)" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3224,6 +3369,7 @@ files = [ name = "ruff" version = "0.4.10" description = "An extremely fast Python linter and code formatter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3250,6 +3396,7 @@ files = [ name = "scikit-learn" version = "1.5.1" description = "A set of python modules for machine learning and data mining" +category = "main" optional = false python-versions = ">=3.9" files = [ @@ -3295,6 +3442,7 @@ tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc ( name = "scipy" version = "1.14.0" description = "Fundamental algorithms for scientific computing in Python" +category = "main" optional = false python-versions = ">=3.10" files = [ @@ -3337,6 +3485,7 @@ test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "me name = "send2trash" version = "1.8.3" description = "Send file to trash natively under Mac OS X, Windows and Linux" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -3353,6 +3502,7 @@ win32 = ["pywin32"] name = "setuptools" version = "72.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3363,12 +3513,13 @@ files = [ [package.extras] core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (>=1.11.0,<1.12.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shapely" version = "2.0.5" description = "Manipulation and analysis of geometric objects" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3414,13 +3565,14 @@ files = [ numpy = ">=1.14,<3" [package.extras] -docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] +docs = ["matplotlib", "numpydoc (>=1.1.0,<1.2.0)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] test = ["pytest", "pytest-cov"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3432,6 +3584,7 @@ files = [ name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3443,6 +3596,7 @@ files = [ name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3454,6 +3608,7 @@ files = [ name = "stack-data" version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" optional = false python-versions = "*" files = [ @@ -3473,6 +3628,7 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] name = "terminado" version = "0.18.1" description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3494,6 +3650,7 @@ typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] name = "threadpoolctl" version = "3.5.0" description = "threadpoolctl" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3505,6 +3662,7 @@ files = [ name = "tinycss2" version = "1.3.0" description = "A tiny CSS parser" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3523,6 +3681,7 @@ test = ["pytest", "ruff"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3534,6 +3693,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3545,6 +3705,7 @@ files = [ name = "tornado" version = "6.4.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3565,6 +3726,7 @@ files = [ name = "tqdm" version = "4.66.5" description = "Fast, Extensible Progress Meter" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3585,6 +3747,7 @@ telegram = ["requests"] name = "traitlets" version = "5.14.3" description = "Traitlets Python configuration system" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3600,6 +3763,7 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, name = "types-python-dateutil" version = "2.9.0.20240316" description = "Typing stubs for python-dateutil" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3611,6 +3775,7 @@ files = [ name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3622,6 +3787,7 @@ files = [ name = "tzdata" version = "2024.1" description = "Provider of IANA time zone data" +category = "main" optional = false python-versions = ">=2" files = [ @@ -3633,6 +3799,7 @@ files = [ name = "uri-template" version = "1.3.0" description = "RFC 6570 URI Template Processor" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3647,6 +3814,7 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake name = "urllib3" version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3664,6 +3832,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "virtualenv" version = "20.26.3" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3684,6 +3853,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "watchdog" version = "4.0.1" description = "Filesystem events monitoring" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3728,6 +3898,7 @@ watchmedo = ["PyYAML (>=3.10)"] name = "wcwidth" version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" optional = false python-versions = "*" files = [ @@ -3739,6 +3910,7 @@ files = [ name = "webcolors" version = "24.6.0" description = "A library for working with the color formats defined by HTML and CSS." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3754,6 +3926,7 @@ tests = ["coverage[toml]"] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" +category = "dev" optional = false python-versions = "*" files = [ @@ -3765,6 +3938,7 @@ files = [ name = "websocket-client" version = "1.8.0" description = "WebSocket client for Python with low level API options" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3781,6 +3955,7 @@ test = ["websockets"] name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3795,6 +3970,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] name = "wordcloud" version = "1.9.3" description = "A little word cloud generator" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3872,4 +4048,4 @@ pillow = "*" [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "78d59942313e76b631d069141dec13f0dfcc5f21ef729d3802fff2f85a823439" +content-hash = "79c5be21e7ce487ff7fa89ee0b556bb60044b47bebc823026f1ef857f9058d85" diff --git a/pyproject.toml b/pyproject.toml index bc9566a..d834120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ pytest-cov = "^4.1.0" nbstripout = "^0.7.1" ruff = "^0.4.8" pre-commit ="^3.6.2" +pytest-mock = "^3.14.0" [tool.poetry.group.examples.dependencies] jupyterlab = "^4.2.5" From 980fd6267d2e1fd21e0d7075b4d5f967b48fa722 Mon Sep 17 00:00:00 2001 From: Vas Date: Fri, 20 Sep 2024 17:59:14 +0300 Subject: [PATCH 05/16] chore: create plots directory for analysis_modules --- docs/analysis_modules.md | 2 +- docs/assets/images/analysis_modules/{ => plots}/line_plot.svg | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/assets/images/analysis_modules/{ => plots}/line_plot.svg (100%) diff --git a/docs/analysis_modules.md b/docs/analysis_modules.md index d504b30..8cd45ff 100644 --- a/docs/analysis_modules.md +++ b/docs/analysis_modules.md @@ -11,7 +11,7 @@ social:
-![Image title](assets/images/analysis_modules/line_plot.svg){ align=right loading=lazy width="50%"} +![Image title](assets/images/analysis_modules/plots/line_plot.svg){ align=right loading=lazy width="50%"} Line plots are particularly good for visualizing sequences that resemble time-based data, such as: diff --git a/docs/assets/images/analysis_modules/line_plot.svg b/docs/assets/images/analysis_modules/plots/line_plot.svg similarity index 100% rename from docs/assets/images/analysis_modules/line_plot.svg rename to docs/assets/images/analysis_modules/plots/line_plot.svg From 3f0a6907b2a90009e684974917840bbc764fab33 Mon Sep 17 00:00:00 2001 From: Vas Date: Fri, 20 Sep 2024 18:04:48 +0300 Subject: [PATCH 06/16] chore: improve line plot --- docs/analysis_modules.md | 6 +- .../analysis_modules/plots/line_plot.svg | 100 ++++++++---------- 2 files changed, 50 insertions(+), 56 deletions(-) diff --git a/docs/analysis_modules.md b/docs/analysis_modules.md index 8cd45ff..e74211d 100644 --- a/docs/analysis_modules.md +++ b/docs/analysis_modules.md @@ -33,13 +33,13 @@ from pyretailscience.plots import line df = pd.DataFrame({ "months_since_event": range(-5, 6), - "category_A": [10000, 12000, 13000, 15000, 16000, 17000, 18000, 20000, 21000, 20030, 25000], - "category_B": [9000, 10000, 11000, 13000, 14000, 15000, 10000, 7000, 3500, 3000, 2800], + "category A": [10000, 12000, 13000, 15000, 16000, 17000, 18000, 20000, 21000, 20030, 25000], + "category B": [9000, 10000, 11000, 13000, 14000, 15000, 10000, 7000, 3500, 3000, 2800], }) line.plot( df=df, - value_col=["category_A", "category_B"], + value_col=["category A", "category B"], x_label="Months Since Event", y_label="Revenue (£)", title="Revenue Trends across Categories", diff --git a/docs/assets/images/analysis_modules/plots/line_plot.svg b/docs/assets/images/analysis_modules/plots/line_plot.svg index 552c6b0..1aa9054 100644 --- a/docs/assets/images/analysis_modules/plots/line_plot.svg +++ b/docs/assets/images/analysis_modules/plots/line_plot.svg @@ -1,12 +1,12 @@ - + - 2024-09-20T17:30:12.256600 + 2024-09-20T18:02:44.469602 image/svg+xml @@ -22,8 +22,8 @@ +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -95,11 +95,11 @@ z +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -138,11 +138,11 @@ z +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -183,11 +183,11 @@ z +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -201,11 +201,11 @@ L 297.972727 32.88125 +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -517,16 +517,16 @@ z +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -573,11 +573,11 @@ z +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -606,11 +606,11 @@ z +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -628,11 +628,11 @@ L 404.5 131.806655 +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -650,11 +650,11 @@ L 404.5 87.285771 +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -806,7 +806,7 @@ L 297.972727 87.285771 L 328.409091 78.381594 L 358.845455 87.018646 L 389.281818 42.764886 -" clip-path="url(#p61598fcf60)" style="fill: none; stroke: #22c55e; stroke-width: 3; stroke-linecap: square"/> +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #22c55e; stroke-width: 3; stroke-linecap: square"/> +" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #22c55e; stroke-width: 3; stroke-linecap: square"/> - @@ -1268,7 +1268,7 @@ L 450.24 45.979688 " style="fill: none; stroke: #22c55e; stroke-width: 3; stroke-linecap: square"/> - + - + - - + + - - - + + - - + + @@ -2008,7 +2002,7 @@ z - + From cd4e0eea08d3912f656f3b7688ed32051010d5ab Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 15:05:47 +0300 Subject: [PATCH 07/16] chore: documentation improvements --- docs/analysis_modules.md | 5 +- .../analysis_modules/plots/line_plot.svg | 55 +++++++++---------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/docs/analysis_modules.md b/docs/analysis_modules.md index e74211d..f96c4cb 100644 --- a/docs/analysis_modules.md +++ b/docs/analysis_modules.md @@ -13,7 +13,7 @@ social: ![Image title](assets/images/analysis_modules/plots/line_plot.svg){ align=right loading=lazy width="50%"} -Line plots are particularly good for visualizing sequences that resemble time-based data, such as: +Line plots are particularly good for visualizing sequences that are ordered or sequential, but not necessarily categorical, such as: - Days since an event (e.g., -2, -1, 0, 1, 2) - Months since a competitor opened @@ -21,7 +21,7 @@ Line plots are particularly good for visualizing sequences that resemble time-ba They are often used to compare trends across categories, show the impact of events on performance, and visualize changes over time-like sequences. -Note: This module is not intended for actual datetime values. For time-based plots using dates, refer to the **timeline** module. +Note: While this module can handle datetime values on the x-axis, the **timeline** plot module has additional features that make working with datetimes easier, such as easily resampling the data to alternate time frames.
@@ -50,7 +50,6 @@ line.plot( ) ``` - ### Waterfall Plot
diff --git a/docs/assets/images/analysis_modules/plots/line_plot.svg b/docs/assets/images/analysis_modules/plots/line_plot.svg index 1aa9054..6a488d2 100644 --- a/docs/assets/images/analysis_modules/plots/line_plot.svg +++ b/docs/assets/images/analysis_modules/plots/line_plot.svg @@ -6,7 +6,7 @@ - 2024-09-20T18:02:44.469602 + 2024-09-23T10:34:29.830933 image/svg+xml @@ -25,9 +25,8 @@ L 523.675938 332.52125 L 523.675938 0 L 0 0 -L 0 332.52125 z -" style="fill: none"/> +" style="fill: #ffffff"/> @@ -43,16 +42,16 @@ z +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -95,11 +94,11 @@ z +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -138,11 +137,11 @@ z +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -183,11 +182,11 @@ z +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -201,11 +200,11 @@ L 297.972727 32.88125 +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -517,16 +516,16 @@ z +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -573,11 +572,11 @@ z +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -606,11 +605,11 @@ z +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -628,11 +627,11 @@ L 404.5 131.806655 +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -650,11 +649,11 @@ L 404.5 87.285771 +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #dad8d7; stroke-opacity: 0.5; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -806,7 +805,7 @@ L 297.972727 87.285771 L 328.409091 78.381594 L 358.845455 87.018646 L 389.281818 42.764886 -" clip-path="url(#p2b4c10462d)" style="fill: none; stroke: #22c55e; stroke-width: 3; stroke-linecap: square"/> +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #22c55e; stroke-width: 3; stroke-linecap: square"/> +" clip-path="url(#pfd3d4055e7)" style="fill: none; stroke: #22c55e; stroke-width: 3; stroke-linecap: square"/> + From f4ba900fd3bae517f3ea6c16b95295e13dc8986d Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 15:06:40 +0300 Subject: [PATCH 08/16] chore: use 'w' as default facecolor for plots --- pyretailscience/style/graph_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyretailscience/style/graph_utils.py b/pyretailscience/style/graph_utils.py index b5462e0..65e3ace 100644 --- a/pyretailscience/style/graph_utils.py +++ b/pyretailscience/style/graph_utils.py @@ -83,6 +83,7 @@ def standard_graph_styles( Returns: Axes: The graph with the styles applied. """ + ax.set_facecolor("w") ax.spines[["top", "right"]].set_visible(False) ax.grid(which="major", axis="x", color="#DAD8D7", alpha=0.5, zorder=1) ax.grid(which="major", axis="y", color="#DAD8D7", alpha=0.5, zorder=1) From 5bd512d456e0180931e4a8033115ce02adb5240d Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 15:07:20 +0300 Subject: [PATCH 09/16] chore: small code simplifications --- pyretailscience/plots/line.py | 36 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index b001651..6c9048b 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -1,8 +1,9 @@ """This module provides flexible functionality for creating line plots from pandas DataFrames. -It focuses on visualizing sequences that resemble time-based data, such as "days since an event" or "months since a -competitor opened." However, it does not explicitly handle datetime values. For actual time-based plots using -datetime objects, please refer to the **`timeline`** module. +It focuses on visualizing sequences that are ordered or sequential but not necessarily categorical, such as "days since +an event" or "months since a competitor opened." However, while this module can handle datetime values on the x-axis, +the **timeline** module has additional features that make working with datetimes easier, such as easily resampling the +data to alternate time frames. The sequences used in this module can include values like "days since an event" (e.g., -2, -1, 0, 1, 2) or "months since a competitor store opened." **This module is not intended for use with actual datetime values**. If a datetime @@ -44,9 +45,9 @@ def plot( df: pd.DataFrame, value_col: str | list[str], - x_label: str, - y_label: str, - title: str, + x_label: str | None = None, + y_label: str | None = None, + title: str | None = None, x_col: str | None = None, group_col: str | None = None, legend_title: str | None = None, @@ -60,9 +61,9 @@ def plot( Args: df (pd.DataFrame): The dataframe to plot. value_col (str or list of str): The column(s) to plot. - x_label (str): The x-axis label. - y_label (str): The y-axis label. - title (str): The title of the plot. + x_label (str, optional): The x-axis label. + y_label (str, optional): The y-axis label. + title (str, optional): The title of the plot. x_col (str, optional): The column to be used as the x-axis. If None, the index is used. group_col (str, optional): The column used to define different lines. legend_title (str, optional): The title of the legend. @@ -82,10 +83,10 @@ def plot( ) colors = get_base_cmap() - if group_col is not None: - pivot_df = df.pivot(index=x_col if x_col is not None else None, columns=group_col, values=value_col) - else: + if group_col is None: pivot_df = df.set_index(x_col if x_col is not None else df.index)[value_col] + else: + pivot_df = df.pivot(index=x_col if x_col is not None else None, columns=group_col, values=value_col) ax = pivot_df.plot( ax=ax, @@ -95,18 +96,15 @@ def plot( **kwargs, ) - ax = gu.standard_graph_styles( - ax, - title=title, - x_label=x_label, - y_label=y_label, - ) + ax = gu.standard_graph_styles(ax=ax, title=title, x_label=x_label, y_label=y_label) if move_legend_outside: ax.legend(bbox_to_anchor=(1.05, 1)) if legend_title is not None: - ax.legend(title=legend_title) + legend = ax.get_legend() + if legend is not None: + legend.set_title(legend_title) if source_text is not None: gu.add_source_text(ax=ax, source_text=source_text) From 90e2baa62553829b841676cdd85421207cffe580 Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 15:28:25 +0300 Subject: [PATCH 10/16] chore: make warning message more consistent --- pyretailscience/plots/line.py | 2 +- tests/plots/test_line.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index 6c9048b..6378678 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -77,7 +77,7 @@ def plot( """ if x_col is not None and pd.api.types.is_datetime64_any_dtype(df[x_col]): warnings.warn( - f"The column '{x_col}' is datetime-like. Consider using the 'timeline' module for time-based plots.", + f"The column '{x_col}' is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", UserWarning, stacklevel=2, ) diff --git a/tests/plots/test_line.py b/tests/plots/test_line.py index 04e48c8..435cd00 100644 --- a/tests/plots/test_line.py +++ b/tests/plots/test_line.py @@ -96,7 +96,7 @@ def test_plot_warns_if_xcol_is_datetime(sample_dataframe, mocker): ) warnings.warn.assert_called_once_with( - "The column 'x' is datetime-like. Consider using the 'timeline' module for time-based plots.", + "The column 'x' is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", UserWarning, stacklevel=2, ) From faf2568de1f68e2a19eb97f76aae8a5ddc0472fc Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 16:30:28 +0300 Subject: [PATCH 11/16] chore: move legend logic to standard graph utils --- pyretailscience/plots/line.py | 31 ++++++++++--------- pyretailscience/style/graph_utils.py | 11 +++++++ tests/plots/test_line.py | 45 +++++++++++++++++++++++----- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index 6378678..cf86ef0 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -45,14 +45,14 @@ def plot( df: pd.DataFrame, value_col: str | list[str], - x_label: str | None = None, - y_label: str | None = None, - title: str | None = None, + x_label: str, + y_label: str, + title: str, x_col: str | None = None, group_col: str | None = None, - legend_title: str | None = None, ax: Axes | None = None, source_text: str | None = None, + legend_title: str | None = None, move_legend_outside: bool = False, **kwargs: dict[str, any], ) -> SubplotBase: @@ -61,9 +61,9 @@ def plot( Args: df (pd.DataFrame): The dataframe to plot. value_col (str or list of str): The column(s) to plot. - x_label (str, optional): The x-axis label. - y_label (str, optional): The y-axis label. - title (str, optional): The title of the plot. + x_label (str): The x-axis label. + y_label (str): The y-axis label. + title (str): The title of the plot. x_col (str, optional): The column to be used as the x-axis. If None, the index is used. group_col (str, optional): The column used to define different lines. legend_title (str, optional): The title of the legend. @@ -96,15 +96,14 @@ def plot( **kwargs, ) - ax = gu.standard_graph_styles(ax=ax, title=title, x_label=x_label, y_label=y_label) - - if move_legend_outside: - ax.legend(bbox_to_anchor=(1.05, 1)) - - if legend_title is not None: - legend = ax.get_legend() - if legend is not None: - legend.set_title(legend_title) + ax = gu.standard_graph_styles( + ax=ax, + title=title, + x_label=x_label, + y_label=y_label, + legend_title=legend_title, + move_legend_outside=move_legend_outside, + ) if source_text is not None: gu.add_source_text(ax=ax, source_text=source_text) diff --git a/pyretailscience/style/graph_utils.py b/pyretailscience/style/graph_utils.py index 65e3ace..71257c7 100644 --- a/pyretailscience/style/graph_utils.py +++ b/pyretailscience/style/graph_utils.py @@ -67,6 +67,8 @@ def standard_graph_styles( title_pad: int = GraphStyles.DEFAULT_TITLE_PAD, x_label_pad: int = GraphStyles.DEFAULT_AXIS_LABEL_PAD, y_label_pad: int = GraphStyles.DEFAULT_AXIS_LABEL_PAD, + legend_title: str | None = None, + move_legend_outside: bool = False, ) -> Axes: """Apply standard styles to a Matplotlib graph. @@ -79,6 +81,8 @@ def standard_graph_styles( x_label_pad (int, optional): The padding below the x-axis label. Defaults to GraphStyles.DEFAULT_AXIS_LABEL_PAD. y_label_pad (int, optional): The padding to the left of the y-axis label. Defaults to GraphStyles.DEFAULT_AXIS_LABEL_PAD. + legend_title (str, optional): The title of the legend. If None, no legend title is applied. Defaults to None. + move_legend_outside (bool, optional): Whether to move the legend outside the plot. Defaults to False. Returns: Axes: The graph with the styles applied. @@ -112,6 +116,13 @@ def standard_graph_styles( labelpad=y_label_pad, ) + legend = ax.legend() if move_legend_outside or legend_title is not None else ax.get_legend() + if legend: + if move_legend_outside: + legend.set_bbox_to_anchor((1.05, 1)) + if legend_title: + legend.set_title(legend_title) + return ax diff --git a/tests/plots/test_line.py b/tests/plots/test_line.py index 435cd00..2ae0f64 100644 --- a/tests/plots/test_line.py +++ b/tests/plots/test_line.py @@ -107,7 +107,7 @@ def test_plot_moves_legend_outside(sample_dataframe): """Test the plot function moves the legend outside the plot.""" _, ax = plt.subplots() - # Test with move_legend_outside=True + # Create the plot with move_legend_outside=True result_ax = line.plot( df=sample_dataframe, value_col="y", @@ -120,13 +120,44 @@ def test_plot_moves_legend_outside(sample_dataframe): move_legend_outside=True, ) - expected_coords = (1.05, 1.0) - legend = result_ax.get_legend() - # Check if bbox_to_anchor is set to (1.05, 1) when legend is outside - bbox_anchor = legend.get_bbox_to_anchor()._bbox + # Assert that standard_graph_styles was called with move_legend_outside=True + gu.standard_graph_styles.assert_called_once_with( + ax=result_ax, + title="Test Plot Legend Outside", + x_label="X Axis", + y_label="Y Axis", + legend_title=None, + move_legend_outside=True, + ) + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_moves_legend_inside(sample_dataframe): + """Test the plot function moves the legend inside the plot.""" + _, ax = plt.subplots() + + # Create the plot with move_legend_outside=False + result_ax = line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot Legend Inside", + x_col="x", + group_col="group", + ax=ax, + move_legend_outside=False, + ) - assert legend is not None - assert (bbox_anchor.x0, bbox_anchor.y0) == expected_coords + # Assert that standard_graph_styles was called with move_legend_outside=False + gu.standard_graph_styles.assert_called_once_with( + ax=result_ax, + title="Test Plot Legend Inside", + x_label="X Axis", + y_label="Y Axis", + legend_title=None, + move_legend_outside=False, + ) @pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") From 11d4efd164d125a887b01d472d7359cebcb48f23 Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 16:49:16 +0300 Subject: [PATCH 12/16] chore: remove redundant parentheses --- tests/plots/test_line.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/plots/test_line.py b/tests/plots/test_line.py index 2ae0f64..3e79ab7 100644 --- a/tests/plots/test_line.py +++ b/tests/plots/test_line.py @@ -12,7 +12,7 @@ from pyretailscience.style import graph_utils as gu -@pytest.fixture() +@pytest.fixture def sample_dataframe(): """A sample dataframe for testing.""" data = { @@ -23,14 +23,14 @@ def sample_dataframe(): return pd.DataFrame(data) -@pytest.fixture() +@pytest.fixture def _mock_get_base_cmap(mocker): """Mock the get_base_cmap function to return a custom colormap.""" cmap = ListedColormap(["#FF0000", "#00FF00", "#0000FF"]) mocker.patch("pyretailscience.style.tailwind.get_base_cmap", return_value=cmap) -@pytest.fixture() +@pytest.fixture def _mock_gu_functions(mocker): mocker.patch("pyretailscience.style.graph_utils.standard_graph_styles", side_effect=lambda ax, **kwargs: ax) mocker.patch("pyretailscience.style.graph_utils.standard_tick_styles", side_effect=lambda ax: ax) From c8a2b038c22669f50006846ec6637307eda95756 Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 16:52:40 +0300 Subject: [PATCH 13/16] chore: place parentheses back --- tests/plots/test_line.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/plots/test_line.py b/tests/plots/test_line.py index 3e79ab7..2ae0f64 100644 --- a/tests/plots/test_line.py +++ b/tests/plots/test_line.py @@ -12,7 +12,7 @@ from pyretailscience.style import graph_utils as gu -@pytest.fixture +@pytest.fixture() def sample_dataframe(): """A sample dataframe for testing.""" data = { @@ -23,14 +23,14 @@ def sample_dataframe(): return pd.DataFrame(data) -@pytest.fixture +@pytest.fixture() def _mock_get_base_cmap(mocker): """Mock the get_base_cmap function to return a custom colormap.""" cmap = ListedColormap(["#FF0000", "#00FF00", "#0000FF"]) mocker.patch("pyretailscience.style.tailwind.get_base_cmap", return_value=cmap) -@pytest.fixture +@pytest.fixture() def _mock_gu_functions(mocker): mocker.patch("pyretailscience.style.graph_utils.standard_graph_styles", side_effect=lambda ax, **kwargs: ax) mocker.patch("pyretailscience.style.graph_utils.standard_tick_styles", side_effect=lambda ax: ax) From a021ec3c728a4843654a53d36929449d8bda65a8 Mon Sep 17 00:00:00 2001 From: Vas Date: Mon, 23 Sep 2024 17:13:50 +0300 Subject: [PATCH 14/16] chore: add more tests --- pyretailscience/plots/line.py | 7 +++ tests/plots/test_line.py | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index cf86ef0..d8f83c3 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -81,6 +81,13 @@ def plot( UserWarning, stacklevel=2, ) + + elif x_col is None and pd.api.types.is_datetime64_any_dtype(df.index): + warnings.warn( + "The DataFrame index is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", + UserWarning, + stacklevel=2, + ) colors = get_base_cmap() if group_col is None: diff --git a/tests/plots/test_line.py b/tests/plots/test_line.py index 2ae0f64..50c18fd 100644 --- a/tests/plots/test_line.py +++ b/tests/plots/test_line.py @@ -178,3 +178,94 @@ def test_plot_adds_source_text(sample_dataframe): ) gu.add_source_text.assert_called_once_with(ax=result_ax, source_text=source_text) + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_with_legend_title(sample_dataframe): + """Test the plot function with a legend title.""" + _, ax = plt.subplots() + + # Create the plot with a legend title + legend_title = "Test Legend" + result_ax = line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot with Legend Title", + x_col="x", + group_col="group", + ax=ax, + legend_title=legend_title, + ) + + # Assert that standard_graph_styles was called with the provided legend title + gu.standard_graph_styles.assert_called_once_with( + ax=result_ax, + title="Test Plot with Legend Title", + x_label="X Axis", + y_label="Y Axis", + legend_title=legend_title, + move_legend_outside=False, + ) + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_with_legend_title_and_move_outside(sample_dataframe): + """Test the plot function with both move_legend_outside=True and legend_title.""" + _, ax = plt.subplots() + + # Create the plot with both options + legend_title = "Test Legend" + result_ax = line.plot( + df=sample_dataframe, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot Legend Outside with Title", + x_col="x", + group_col="group", + ax=ax, + move_legend_outside=True, + legend_title=legend_title, + ) + + # Assert that standard_graph_styles was called with both options + gu.standard_graph_styles.assert_called_once_with( + ax=result_ax, + title="Test Plot Legend Outside with Title", + x_label="X Axis", + y_label="Y Axis", + legend_title=legend_title, + move_legend_outside=True, + ) + + +@pytest.mark.usefixtures("_mock_get_base_cmap", "_mock_gu_functions") +def test_plot_with_datetime_index_warns(sample_dataframe, mocker): + """Test the plot function with a datetime index and no x_col, expecting a warning.""" + df_with_datetime_index = sample_dataframe.set_index("x") + _, ax = plt.subplots() + + # Mock the warnings.warn method to check if it's called + mocker.patch("warnings.warn") + + # Create the plot with a datetime index and no x_col + result_ax = line.plot( + df=df_with_datetime_index, + value_col="y", + x_label="X Axis", + y_label="Y Axis", + title="Test Plot Datetime Index", + ax=ax, + ) + + # Assert that the plot was created + assert isinstance(result_ax, Axes) + + # Assert that the warning about datetime-like index was raised + warnings.warn.assert_called_once_with( + "The DataFrame index is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", + UserWarning, + stacklevel=2, + ) From 68542d1c4fadb67adb32c0e2bcf86a9e06ad23d4 Mon Sep 17 00:00:00 2001 From: Vas Date: Tue, 24 Sep 2024 13:23:25 +0300 Subject: [PATCH 15/16] chore: use time_line instead of timeline --- docs/analysis_modules.md | 2 +- pyretailscience/plots/line.py | 8 ++++---- tests/plots/test_line.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/analysis_modules.md b/docs/analysis_modules.md index f96c4cb..30bd59f 100644 --- a/docs/analysis_modules.md +++ b/docs/analysis_modules.md @@ -21,7 +21,7 @@ Line plots are particularly good for visualizing sequences that are ordered or s They are often used to compare trends across categories, show the impact of events on performance, and visualize changes over time-like sequences. -Note: While this module can handle datetime values on the x-axis, the **timeline** plot module has additional features that make working with datetimes easier, such as easily resampling the data to alternate time frames. +Note: While this module can handle datetime values on the x-axis, the **plots.time_line** plot module has additional features that make working with datetimes easier, such as easily resampling the data to alternate time frames.
diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index d8f83c3..a89307c 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -8,7 +8,7 @@ The sequences used in this module can include values like "days since an event" (e.g., -2, -1, 0, 1, 2) or "months since a competitor store opened." **This module is not intended for use with actual datetime values**. If a datetime or datetime-like column is passed as **`x_col`**, a warning will be triggered, suggesting the use of the -**`timeline`** module. +**`plots.time_line`** module. ### Core Features @@ -28,7 +28,7 @@ ### Limitations and Handling of Temporal Data - **Limited Handling of Temporal Data**: This module can plot simple time-based sequences, such as "days since an event," but it cannot manipulate or directly handle datetime or date-like columns. It is not optimized for actual datetime values. -If a datetime column is passed or more complex temporal plotting is needed, a warning will suggest using the **`timeline`** module, which is specifically designed for working with temporal data and performing time-based manipulation. +If a datetime column is passed or more complex temporal plotting is needed, a warning will suggest using the **`plots.time_line`** module, which is specifically designed for working with temporal data and performing time-based manipulation. - **Pre-Aggregated Data Required**: The module does not perform any data aggregation, so all data must be pre-aggregated before being passed in for plotting. """ @@ -77,14 +77,14 @@ def plot( """ if x_col is not None and pd.api.types.is_datetime64_any_dtype(df[x_col]): warnings.warn( - f"The column '{x_col}' is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", + f"The column '{x_col}' is datetime-like. Consider using the 'plots.time_line' module for time-based plots.", UserWarning, stacklevel=2, ) elif x_col is None and pd.api.types.is_datetime64_any_dtype(df.index): warnings.warn( - "The DataFrame index is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", + "The DataFrame index is datetime-like. Consider using the 'plots.time_line' module for time-based plots.", UserWarning, stacklevel=2, ) diff --git a/tests/plots/test_line.py b/tests/plots/test_line.py index 50c18fd..69c33d9 100644 --- a/tests/plots/test_line.py +++ b/tests/plots/test_line.py @@ -96,7 +96,7 @@ def test_plot_warns_if_xcol_is_datetime(sample_dataframe, mocker): ) warnings.warn.assert_called_once_with( - "The column 'x' is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", + "The column 'x' is datetime-like. Consider using the 'plots.time_line' module for time-based plots.", UserWarning, stacklevel=2, ) @@ -265,7 +265,7 @@ def test_plot_with_datetime_index_warns(sample_dataframe, mocker): # Assert that the warning about datetime-like index was raised warnings.warn.assert_called_once_with( - "The DataFrame index is datetime-like. Consider using the 'plots.timeline' module for time-based plots.", + "The DataFrame index is datetime-like. Consider using the 'plots.time_line' module for time-based plots.", UserWarning, stacklevel=2, ) From d30b2ac3396e7ba9a56f9cfc65cfc6a7a1012764 Mon Sep 17 00:00:00 2001 From: Vas Date: Tue, 24 Sep 2024 13:34:18 +0300 Subject: [PATCH 16/16] chore: make plot title and labels optional --- pyretailscience/plots/line.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyretailscience/plots/line.py b/pyretailscience/plots/line.py index a89307c..3ab9711 100644 --- a/pyretailscience/plots/line.py +++ b/pyretailscience/plots/line.py @@ -45,9 +45,9 @@ def plot( df: pd.DataFrame, value_col: str | list[str], - x_label: str, - y_label: str, - title: str, + x_label: str | None = None, + y_label: str | None = None, + title: str | None = None, x_col: str | None = None, group_col: str | None = None, ax: Axes | None = None, @@ -61,9 +61,9 @@ def plot( Args: df (pd.DataFrame): The dataframe to plot. value_col (str or list of str): The column(s) to plot. - x_label (str): The x-axis label. - y_label (str): The y-axis label. - title (str): The title of the plot. + x_label (str, optional): The x-axis label. + y_label (str, optional): The y-axis label. + title (str, optional): The title of the plot. x_col (str, optional): The column to be used as the x-axis. If None, the index is used. group_col (str, optional): The column used to define different lines. legend_title (str, optional): The title of the legend.