diff --git a/docs/source/example_overview.ipynb b/docs/source/example_overview.ipynb index d559501..bc1e06b 100644 --- a/docs/source/example_overview.ipynb +++ b/docs/source/example_overview.ipynb @@ -15,17 +15,22 @@ }, { "cell_type": "code", - "execution_count": null, "id": "6a230b00c14cc64e", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-29T14:11:59.229832Z", + "start_time": "2026-01-29T14:11:54.050642Z" + } + }, "source": [ "from pathlib import Path\n", "\n", "import matplotlib.pyplot as plt\n", "\n", "from post_processing.dataclass.data_aplose import DataAplose" - ] + ], + "outputs": [], + "execution_count": 1 }, { "cell_type": "markdown", @@ -35,14 +40,34 @@ }, { "cell_type": "code", - "execution_count": null, "id": "c19ddde8bf965ee8", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-29T14:13:45.480964Z", + "start_time": "2026-01-29T14:13:44.483047Z" + } + }, "source": [ "yaml_file = Path(r\"resource/APOCADO_yaml.yml\")\n", "data = DataAplose.from_yaml(file=yaml_file)" - ] + ], + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: 'resource\\\\APOCADO_yaml.yml'", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mFileNotFoundError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[2]\u001B[39m\u001B[32m, line 2\u001B[39m\n\u001B[32m 1\u001B[39m yaml_file = Path(\u001B[33mr\u001B[39m\u001B[33m\"\u001B[39m\u001B[33mresource/APOCADO_yaml.yml\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m----> \u001B[39m\u001B[32m2\u001B[39m data = \u001B[43mDataAplose\u001B[49m\u001B[43m.\u001B[49m\u001B[43mfrom_yaml\u001B[49m\u001B[43m(\u001B[49m\u001B[43mfile\u001B[49m\u001B[43m=\u001B[49m\u001B[43myaml_file\u001B[49m\u001B[43m)\u001B[49m\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\Projets_Osmose\\Git\\post_processing_detections\\src\\post_processing\\dataclass\\data_aplose.py:483\u001B[39m, in \u001B[36mDataAplose.from_yaml\u001B[39m\u001B[34m(cls, file, concat)\u001B[39m\n\u001B[32m 460\u001B[39m \u001B[38;5;129m@classmethod\u001B[39m\n\u001B[32m 461\u001B[39m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34mfrom_yaml\u001B[39m(\n\u001B[32m 462\u001B[39m \u001B[38;5;28mcls\u001B[39m,\n\u001B[32m (...)\u001B[39m\u001B[32m 465\u001B[39m concat: \u001B[38;5;28mbool\u001B[39m = \u001B[38;5;28;01mTrue\u001B[39;00m,\n\u001B[32m 466\u001B[39m ) -> DataAplose | \u001B[38;5;28mlist\u001B[39m[DataAplose]:\n\u001B[32m 467\u001B[39m \u001B[38;5;250m \u001B[39m\u001B[33;03m\"\"\"Return a DataAplose object from a yaml file.\u001B[39;00m\n\u001B[32m 468\u001B[39m \n\u001B[32m 469\u001B[39m \u001B[33;03m Parameters\u001B[39;00m\n\u001B[32m (...)\u001B[39m\u001B[32m 481\u001B[39m \n\u001B[32m 482\u001B[39m \u001B[33;03m \"\"\"\u001B[39;00m\n\u001B[32m--> \u001B[39m\u001B[32m483\u001B[39m filters = \u001B[43mDetectionFilter\u001B[49m\u001B[43m.\u001B[49m\u001B[43mfrom_yaml\u001B[49m\u001B[43m(\u001B[49m\u001B[43mfile\u001B[49m\u001B[43m=\u001B[49m\u001B[43mfile\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 484\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mcls\u001B[39m.from_filters(filters, concat=concat)\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\Projets_Osmose\\Git\\post_processing_detections\\src\\post_processing\\dataclass\\detection_filter.py:71\u001B[39m, in \u001B[36mDetectionFilter.from_yaml\u001B[39m\u001B[34m(cls, file)\u001B[39m\n\u001B[32m 53\u001B[39m \u001B[38;5;129m@classmethod\u001B[39m\n\u001B[32m 54\u001B[39m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34mfrom_yaml\u001B[39m(\n\u001B[32m 55\u001B[39m \u001B[38;5;28mcls\u001B[39m,\n\u001B[32m 56\u001B[39m file: Path,\n\u001B[32m 57\u001B[39m ) -> DetectionFilter | \u001B[38;5;28mlist\u001B[39m[DetectionFilter]:\n\u001B[32m 58\u001B[39m \u001B[38;5;250m \u001B[39m\u001B[33;03m\"\"\"Return a DetectionFilter object from a YAML file.\u001B[39;00m\n\u001B[32m 59\u001B[39m \n\u001B[32m 60\u001B[39m \u001B[33;03m Parameters\u001B[39;00m\n\u001B[32m (...)\u001B[39m\u001B[32m 69\u001B[39m \n\u001B[32m 70\u001B[39m \u001B[33;03m \"\"\"\u001B[39;00m\n\u001B[32m---> \u001B[39m\u001B[32m71\u001B[39m \u001B[38;5;28;01mwith\u001B[39;00m \u001B[43mfile\u001B[49m\u001B[43m.\u001B[49m\u001B[43mopen\u001B[49m\u001B[43m(\u001B[49m\u001B[43mencoding\u001B[49m\u001B[43m=\u001B[49m\u001B[33;43m\"\u001B[39;49m\u001B[33;43mutf-8\u001B[39;49m\u001B[33;43m\"\u001B[39;49m\u001B[43m)\u001B[49m \u001B[38;5;28;01mas\u001B[39;00m yaml_file:\n\u001B[32m 72\u001B[39m parameters = yaml.safe_load(yaml_file)\n\u001B[32m 73\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mcls\u001B[39m.from_dict(parameters)\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\AppData\\Roaming\\uv\\python\\cpython-3.13.5-windows-x86_64-none\\Lib\\pathlib\\_local.py:537\u001B[39m, in \u001B[36mPath.open\u001B[39m\u001B[34m(self, mode, buffering, encoding, errors, newline)\u001B[39m\n\u001B[32m 535\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[33m\"\u001B[39m\u001B[33mb\u001B[39m\u001B[33m\"\u001B[39m \u001B[38;5;129;01mnot\u001B[39;00m \u001B[38;5;129;01min\u001B[39;00m mode:\n\u001B[32m 536\u001B[39m encoding = io.text_encoding(encoding)\n\u001B[32m--> \u001B[39m\u001B[32m537\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mio\u001B[49m\u001B[43m.\u001B[49m\u001B[43mopen\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mmode\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mbuffering\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mencoding\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43merrors\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mnewline\u001B[49m\u001B[43m)\u001B[49m\n", + "\u001B[31mFileNotFoundError\u001B[39m: [Errno 2] No such file or directory: 'resource\\\\APOCADO_yaml.yml'" + ] + } + ], + "execution_count": 2 }, { "cell_type": "markdown", diff --git a/src/post_processing/dataclass/data_aplose.py b/src/post_processing/dataclass/data_aplose.py index e1d3cae..e2302da 100644 --- a/src/post_processing/dataclass/data_aplose.py +++ b/src/post_processing/dataclass/data_aplose.py @@ -27,7 +27,7 @@ ) from post_processing.utils.metrics_utils import detection_perf from post_processing.utils.plot_utils import ( - agreement, + plot_agreement, heatmap, histo, overview, @@ -442,7 +442,7 @@ def plot( if mode == "agreement": bin_size = kwargs.get("bin_size") - return agreement(df=df_filtered, bin_size=bin_size, ax=ax) + return plot_agreement(df=df_filtered, bin_size=bin_size, ax=ax) if mode == "timeline": color = kwargs.get("color") diff --git a/src/post_processing/utils/plot_utils.py b/src/post_processing/utils/plot_utils.py index 8d12fa3..14e5c85 100644 --- a/src/post_processing/utils/plot_utils.py +++ b/src/post_processing/utils/plot_utils.py @@ -122,7 +122,7 @@ def histo( offset = i * bar_width.total_seconds() / 86400 bar_kwargs = { - "width": bar_width.total_seconds() / 86400, + "width": (bar_width.total_seconds() / 86400), "align": "edge", "edgecolor": "black", "color": color[i], @@ -469,16 +469,11 @@ def wrap_text(text: str) -> str: ax.set_xticklabels(new_labels, rotation=0) -def agreement( +def count_detections_within_timeframe( df: DataFrame, bin_size: Timedelta | BaseOffset, - ax: plt.Axes, -) -> None: - """Compute and visualise agreement between two annotators. - - This function compares annotation timestamps from two annotators over a time range. - It also fits and plots a linear regression line and displays the coefficient - of determination (R²) on the plot. + ) -> DataFrame: + """Counts the number of detections in df within bin_size timeframe. Parameters ---------- @@ -489,8 +484,10 @@ def agreement( bin_size : Timedelta | BaseOffset The size of each time bin for aggregating annotation timestamps. - ax : matplotlib.axes.Axes - Matplotlib axes object where the scatterplot and regression line will be drawn. + Returns + ------- + df_hist: Dataframe with columns = annotators and lines = number of detections + within the timebin defined by bin_size """ labels, annotators = get_labels_and_annotators(df) @@ -505,7 +502,6 @@ def agreement( ] # scatter plot - n_annot_max = bin_size.total_seconds() / df["end_time"].iloc[0] freq = ( bin_size if isinstance(bin_size, Timedelta) else str(bin_size.n) + bin_size.name @@ -513,20 +509,48 @@ def agreement( bins = date_range( start=df["start_datetime"].min().floor(bin_size), - end=df["start_datetime"].max().ceil(bin_size), + end=df["end_datetime"].max().ceil(bin_size), freq=freq, ) - df_hist = ( + return ( DataFrame( { annotators[0]: histogram(datetimes[0], bins=bins)[0], annotators[1]: histogram(datetimes[1], bins=bins)[0], }, ) - / n_annot_max ) + +def plot_agreement( + df: DataFrame, + bin_size: Timedelta | BaseOffset, + ax: plt.Axes, +) -> None: + """Compute and visualise agreement between two annotators. + + This function compares annotation timestamps from two annotators over a time range. + It also fits and plots a linear regression line and displays the coefficient + of determination (R²) on the plot. + + Parameters + ---------- + df : DataFrame + APLOSE-formatted DataFrame. + It must contain The annotations of two annotators. + + bin_size : Timedelta | BaseOffset + The size of each time bin for aggregating annotation timestamps. + + ax : matplotlib.axes.Axes + Matplotlib axes object where the scatterplot and regression line will be drawn. + + + + """ + labels, annotators = get_labels_and_annotators(df) + df_hist = count_detections_within_timeframe(df, bin_size) scatterplot(data=df_hist, x=annotators[0], y=annotators[1], ax=ax) coefficients = polyfit(df_hist[annotators[0]], df_hist[annotators[1]], 1)