Repository: theroggy/cropclassification (language: python)

Issue:
Comply (better) to pep8 using black



Discussion / hints:

Fix patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6c731edd..fc0291d6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,7 @@
 {
     "python.terminal.activateEnvironment": true,
     "python.pythonPath": "~\\Miniconda3\\envs\\cropclassification\\python.exe",
-    "terminal.integrated.shell.windows": "C:\\Windows\\System32\\cmd.exe"
+    "terminal.integrated.shell.windows": "C:\\Windows\\System32\\cmd.exe",
+    "editor.formatOnSave": true,
+    "python.formatting.provider": "black"
 }
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..7e5cbd47
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+# CHANGELOG
+
+## 0.1.0 (???)
+
+### Improvements
+
+- Use black to comply to pep8 + minor general improvements (#13)
+
+### Bugs fixed
+
+- 
+
+### Deprecations and compatibility notes
+
diff --git a/bin_util/calc_dias_weekly.py b/bin_util/calc_dias_weekly.py
index 412edc5c..2a265b80 100644
--- a/bin_util/calc_dias_weekly.py
+++ b/bin_util/calc_dias_weekly.py
@@ -6,34 +6,35 @@
 from datetime import datetime
 import os
 from pathlib import Path
-import shutil
 import sys
-sys.path.insert(0, str(Path(__file__).resolve().parent / '..'))
 
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
 from cropclassification.helpers import config_helper as conf
 from cropclassification.helpers import log_helper
 import cropclassification.preprocess.timeseries_util as ts_util
 
+
 def main():
-   
-    config_filepaths = [Path("config/general.ini"),
-                        Path("config/local_overrule.ini")]
+
+    config_paths = [Path("config/general.ini"), Path("config/local_overrule.ini")]
     year = 2018
 
     # Read the configuration files
-    conf.read_config(config_filepaths)
+    conf.read_config(config_paths)
 
     # Init logging
-    base_log_dir = conf.dirs.getpath('log_dir')
-    log_dir = base_log_dir / f"{os.sep}calc_dias_weekly{datetime.now():%Y-%m-%d_%H-%M-%S}"
+    base_log_dir = conf.dirs.getpath("log_dir")
+    log_dir = (
+        base_log_dir / f"{os.sep}calc_dias_weekly{datetime.now():%Y-%m-%d_%H-%M-%S}"
+    )
     log_level = conf.general.get("log_level")
     global logger
     logger = log_helper.main_log_init(log_dir, __name__, log_level)
     logger.info(f"Config used: \n{conf.pformat_config()}")
 
     # Get the config needed
-    timeseries_per_image_dir = conf.dirs.getpath('timeseries_per_image_dir')
-    timeseries_periodic_dir = conf.dirs.getpath('timeseries_periodic_dir')
+    timeseries_per_image_dir = conf.dirs.getpath("timeseries_per_image_dir")
+    timeseries_periodic_dir = conf.dirs.getpath("timeseries_periodic_dir")
 
     # Input features file depends on the year
     if year == 2017:
@@ -41,22 +42,24 @@ def main():
     elif year == 2018:
         input_features_filename = "Prc_BEFL_2018_2019-06-14_bufm5.shp"
     elif year == 2019:
-        #input_features_filename = "Prc_BEFL_2019_2019-06-25_bufm5.shp"
+        # input_features_filename = "Prc_BEFL_2019_2019-06-25_bufm5.shp"
         input_features_filename = "Prc_BEFL_2019_2019-07-02_bufm5.shp"
     else:
         raise Exception(f"Not a valid year: {year}")
 
     # Calculate!
-    input_parcel_filepath = conf.dirs.getpath('input_dir') / input_features_filename
+    input_parcel_path = conf.dirs.getpath("input_dir") / input_features_filename
     ts_util.calculate_periodic_data(
-            input_parcel_filepath=input_parcel_filepath,
-            input_base_dir=timeseries_per_image_dir,
-            start_date_str=f"{year}-03-15",
-            end_date_str=f"{year}-08-15",
-            #sensordata_to_get=conf.marker.getlist('sensordata_to_use'),
-            sensordata_to_get=['SENSORDATA_S1_COHERENCE'],
-            dest_data_dir=timeseries_periodic_dir,
-            force=False)
-
-if __name__ == '__main__':
-    main()
\ No newline at end of file
+        input_parcel_path=input_parcel_path,
+        input_base_dir=timeseries_per_image_dir,
+        start_date_str=f"{year}-03-15",
+        end_date_str=f"{year}-08-15",
+        # sensordata_to_get=conf.marker.getlist('sensordata_to_use'),
+        sensordata_to_get=["SENSORDATA_S1_COHERENCE"],
+        dest_data_dir=timeseries_periodic_dir,
+        force=False,
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin_util/calc_overlaps.py b/bin_util/calc_overlaps.py
index eee0c0f7..dd2afbe3 100644
--- a/bin_util/calc_overlaps.py
+++ b/bin_util/calc_overlaps.py
@@ -4,32 +4,28 @@
 """
 
 import os
-# TODO: the init of this doensn't seem to work properly... should be solved somewhere else?
-#os.environ["GDAL_DATA"] = r"C:\Tools\anaconda3\envs\orthoseg4\Library\share\gdal"
 from pathlib import Path
 import pprint
 
 # If not installed as package + it is higher in dir hierarchy, add root to sys.path
 import sys
-sys.path.insert(0, str(Path(__file__).resolve().parent / '..'))
-
-import geopandas as gpd
 import sqlite3
 
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
 import cropclassification.helpers.config_helper as conf
 import cropclassification.helpers.log_helper as log_helper
-import cropclassification.helpers.geofile as geofile_helper
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
+
 
 def main():
 
     # Read the configuration
-    segment_config_filepaths=[Path('../config/general.ini')]
-    conf.read_config(segment_config_filepaths)
-    
+    segment_config_paths = [Path("../config/general.ini")]
+    conf.read_config(segment_config_paths)
+
     # Main initialisation of the logging
     log_level = conf.general.get("log_level")
     logger = log_helper.main_log_init(conf.dirs.getpath('log_dir'), __name__, log_level)      
@@ -39,28 +35,32 @@ def main():
     logger.info(pprint.pformat(dict(os.environ)))
 
     # Init variables
-    #parcels_filepath = r"X:\GIS\GIS DATA\Percelen_ALP\Vlaanderen\Perc_VL_2019_2019-07-28\perc_2019_met_k_2019-07-28.shp"
-    #overlap_filepath = r"X:\Monitoring\OrthoSeg\sealedsurfaces\output_vector\sealedsurfaces_10\sealedsurfaces_10_orig.gpkg"
-    input_preprocessed_dir = conf.dirs.getpath('input_preprocessed_dir')
-    parcels_filepath = input_preprocessed_dir / 'Prc_BEFL_2019_2019-07-02_bufm5_32632.gpkg'
-    overlap_filepath = input_preprocessed_dir / 'Prc_BEFL_2019_2019-07-02_bufm5_32632.gpkg'
-    
+    # parcels_path = r"X:\GIS\GIS DATA\Percelen_ALP\Vlaanderen\Perc_VL_2019_2019-07-28\perc_2019_met_k_2019-07-28.shp"
+    # overlap_path = r"X:\Monitoring\OrthoSeg\sealedsurfaces\output_vector\sealedsurfaces_10\sealedsurfaces_10_orig.gpkg"
+    input_preprocessed_dir = conf.dirs.getpath("input_preprocessed_dir")
+    parcels_path = (
+        input_preprocessed_dir / "Prc_BEFL_2019_2019-07-02_bufm5_32632.gpkg"
+    )
+    overlap_path = (
+        input_preprocessed_dir / "Prc_BEFL_2019_2019-07-02_bufm5_32632.gpkg"
+    )
+
     # Read parcels file to memory (isn't that large...)
-    #parcels_gpd = geofile_helper.read_file(parcels_filepath)
-    
+    # parcels_gpd = geofile_helper.read_file(parcels_path)
+
     # Loop over parcels and calculate overlap
-    logger.info(f"Connect to {overlap_filepath}")
-    conn = sqlite3.connect(str(overlap_filepath)) 
+    logger.info(f"Connect to {overlap_path}")
+    conn = sqlite3.connect(str(overlap_path))
     conn.enable_load_extension(True)
 
-    #now we can load the extension
-    # depending on your OS and sqlite/spatialite version you might need to add 
+    # now we can load the extension
+    # depending on your OS and sqlite/spatialite version you might need to add
     # '.so' (Linux) or '.dll' (Windows) to the extension name
 
-    #mod_spatialite (recommended)
-    #conn.execute("SELECT load_extension('spatialite.dll')")  
-    conn.load_extension('mod_spatialite')
-    conn.execute('SELECT InitSpatialMetaData(1);')  
+    # mod_spatialite (recommended)
+    # conn.execute("SELECT load_extension('spatialite.dll')")
+    conn.load_extension("mod_spatialite")
+    conn.execute("SELECT InitSpatialMetaData(1);")
 
     """
     # libspatialite
@@ -68,7 +68,7 @@ def main():
     conn.execute('SELECT InitSpatialMetaData();')
     """
 
-    c = conn.cursor() 
+    c = conn.cursor()
 
     c.execute("SELECT sqlite_version()")
     for row in c:
@@ -79,12 +79,13 @@ def main():
         logger.info(f"Table: {row}")
 
     c.execute(
-            """SELECT t.uid, t.fid, MbrMinX(t.geom), ST_GeometryType(t.geom), ST_AsText(GeomFromGPB(t.geom))
+        """SELECT t.uid, t.fid, MbrMinX(t.geom), ST_GeometryType(t.geom), ST_AsText(GeomFromGPB(t.geom))
                  FROM info t
                  JOIN rtree_info_geom r ON t.fid = r.id
                  WHERE r.minx >= 50000
                    AND r.maxx <= 51000
-            """)
+            """
+    )
     """SELECT t.fid, ST_AsText(t.geom)
             FROM info t
             JOIN rtree_info_geom r ON t.fid = r.id
@@ -106,5 +107,6 @@ def main():
             break
         # do_stuff_with_row
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     main()
diff --git a/bin_util/clean_gee_timeseries_csvs.py b/bin_util/clean_gee_timeseries_csvs.py
index 53b378e5..36bfdf33 100644
--- a/bin_util/clean_gee_timeseries_csvs.py
+++ b/bin_util/clean_gee_timeseries_csvs.py
@@ -4,19 +4,20 @@
 
 from pathlib import Path
 import sys
-sys.path.insert(0, str(Path(__file__).resolve().parent / '..'))
 
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
 import cropclassification.helpers.config_helper as conf
 import cropclassification.preprocess.timeseries_calc_gee as ts_calc_gee
 
-def main():        
+
+def main():
     # Read the configuration
-    conf.read_config([Path('config/general.ini')])
+    conf.read_config([Path("config/general.ini")])
 
     # Go!
-    timeseries_periodic_dir = conf.dirs.getpath('timeseries_periodic_dir')
+    timeseries_periodic_dir = conf.dirs.getpath("timeseries_periodic_dir")
     ts_calc_gee.clean_gee_downloaded_csvs_in_dir(str(timeseries_periodic_dir))
-    
-if __name__ == '__main__':
-    main()
 
+
+if __name__ == "__main__":
+    main()
diff --git a/bin_util/clean_sqlites.py b/bin_util/clean_sqlites.py
index 500a80ab..2c5839bc 100644
--- a/bin_util/clean_sqlites.py
+++ b/bin_util/clean_sqlites.py
@@ -5,12 +5,11 @@
 
 import sqlite3
 
-def rename_table(sqlite_filepath: str,
-                 tablename_src: str,
-                 tablename_dest: str):
+
+def rename_table(sqlite_path: str, tablename_src: str, tablename_dest: str):
 
     # Create a connection object
-    connection  = sqlite3.connect(sqlite_filepath)
+    connection = sqlite3.connect(sqlite_path)
 
     # Get a cursor
     cursor = connection.cursor()
@@ -18,34 +17,36 @@ def rename_table(sqlite_filepath: str,
     # Rename the SQLite Table
     rename_table = f'ALTER TABLE "{tablename_src}" RENAME TO {tablename_dest}'
     try:
-        print(f"Execute {rename_table} on {os.path.basename(sqlite_filepath)}")
-        cursor.execute(rename_table)  
+        print(f"Execute {rename_table} on {os.path.basename(sqlite_path)}")
+        cursor.execute(rename_table)
     except Exception as ex:
         print(f"Exception: {ex}")
 
     # close the database connection
     connection.close()
 
+
 def main():
 
-    #dir = r"X:\Monitoring\Markers\playground\_timeseries_per_image\Prc_BEFL_2019_2019-06-25_bufm5"
-    #dir = r"X:\Monitoring\Markers\playground\_timeseries_per_image\Prc_BEFL_2018_2019-06-14_bufm5"
+    # dir = r"X:\Monitoring\Markers\playground\_timeseries_per_image\Prc_BEFL_2019_2019-06-25_bufm5"
+    # dir = r"X:\Monitoring\Markers\playground\_timeseries_per_image\Prc_BEFL_2018_2019-06-14_bufm5"
     dir = r"X:\Monitoring\Markers\playground\_timeseries_per_image\Prc_BEFL_2018_2019-07-02_bufm5"
-    #dir = r"/home/pierog/playground/_timeseries_per_image/Prc_BEFL_2019_2019-06-25_bufm5"
-    #dir = r"/home/pierog/playground/_timeseries_per_image/Prc_BEFL_2018_2019-06-14_bufm5"
+    # dir = r"/home/pierog/playground/_timeseries_per_image/Prc_BEFL_2019_2019-06-25_bufm5"
+    # dir = r"/home/pierog/playground/_timeseries_per_image/Prc_BEFL_2018_2019-06-14_bufm5"
     glob_search = os.path.join(dir, "*.sqlite")
 
     sqlite_files = glob.glob(glob_search)
 
-    for filepath in sqlite_files:
-        rename_table(filepath, 'default', 'info') 
+    for path in sqlite_files:
+        rename_table(path, "default", "info")
+
+        if path.endswith("_10m.sqlite"):
+            new_path = path.replace("_10m.sqlite", "-10m.sqlite")
+            os.rename(path, new_path)
+        elif path.endswith("_20m.sqlite"):
+            new_path = path.replace("_20m.sqlite", "-20m.sqlite")
+            os.rename(path, new_path)
 
-        if filepath.endswith('_10m.sqlite'):
-            new_filepath = filepath.replace('_10m.sqlite', '-10m.sqlite')
-            os.rename(filepath, new_filepath)
-        elif filepath.endswith('_20m.sqlite'):
-            new_filepath = filepath.replace('_20m.sqlite', '-20m.sqlite')
-            os.rename(filepath, new_filepath)
 
 # If the script is run directly...
 if __name__ == "__main__":
diff --git a/bin_util/parquet_tosqlite.py b/bin_util/parquet_tosqlite.py
index d81e4364..9e63766c 100644
--- a/bin_util/parquet_tosqlite.py
+++ b/bin_util/parquet_tosqlite.py
@@ -4,26 +4,31 @@
 import os
 from pathlib import Path
 import sys
-sys.path.insert(0, str(Path(__file__).resolve().parent / '..'))
+
+sys.path.insert(0, str(Path(__file__).resolve().parent / ".."))
 
 from cropclassification.helpers import pandas_helper as pdh
 
+
 def main():
 
-    dir = Path("X:/Monitoring/Markers/playground/pierog/tmp/Run_2019-06-25_007_imported")
-    in_filepaths = dir.glob("*.parquet")
+    dir = Path(
+        "X:/Monitoring/Markers/playground/pierog/tmp/Run_2019-06-25_007_imported"
+    )
+    in_paths = dir.glob("*.parquet")
 
     # Convert all files found
-    for in_filepath in in_filepaths:
+    for in_path in in_paths:
 
         # Read input file
-        print(f"Read {in_filepath}")
-        df = pdh.read_file(in_filepath)
+        print(f"Read {in_path}")
+        df = pdh.read_file(in_path)
 
         # Write to new file
-        out_filepath = in_filepath.parent / f"{in_filepath.stem}.sqlite"
-        print(f"Write {out_filepath}")
-        pdh.to_file(df, out_filepath)
+        out_path = in_path.parent / f"{in_path.stem}.sqlite"
+        print(f"Write {out_path}")
+        pdh.to_file(df, out_path)
+
 
 # If the script is run directly...
 if __name__ == "__main__":
diff --git a/bin_util/read_head.py b/bin_util/read_head.py
index 183d5cac..dffc3b3d 100644
--- a/bin_util/read_head.py
+++ b/bin_util/read_head.py
@@ -3,10 +3,13 @@
 Read first lines of a file.
 """
 
-base_dir = 'X:\\PerPersoon\\PIEROG\\Taken\\2018\\2018-05-04_Monitoring_Classificatie'
-filepath = base_dir + "\\2018_multicrop\\2018-09-26_Run1\\BEFL2018_bufm10_weekly_parcel_classdata.csv"
+base_dir = "X:\\PerPersoon\\PIEROG\\Taken\\2018\\2018-05-04_Monitoring_Classificatie"
+path = (
+    base_dir
+    + "\\2018_multicrop\\2018-09-26_Run1\\BEFL2018_bufm10_weekly_parcel_classdata.csv"
+)
 
 # Print the first 5 lines of the file
-with open(filepath, "r") as file:
+with open(path, "r") as file:
     for i in range(5):
         print(file.readline())
diff --git a/bin_util/timeseries_visualize.py b/bin_util/timeseries_visualize.py
index 7e2103f9..ffc9190c 100644
--- a/bin_util/timeseries_visualize.py
+++ b/bin_util/timeseries_visualize.py
@@ -7,50 +7,44 @@
 """
 
 import logging
-import os
 from pathlib import Path
 import sys
-sys.path.insert(0, str(Path(__file__).resolve().parent / '..'))
 
-
-import numpy as np
-import pandas as pd
-
-import cropclassification.helpers.config_helper as conf
+sys.path.insert(0, str(Path(__file__).resolve().parent / ".."))
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 
 # Constants for types of sensor data
-SENSORDATA_VV = 'VV' # Vertical Vertical
-SENSORDATA_VH = 'VH' # Vertical Horizontal
+SENSORDATA_VV = "VV"  # Vertical Vertical
+SENSORDATA_VH = "VH"  # Vertical Horizontal
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
-def show(input_parcel_filepath: Path,
-         filter_id: str):
+
+def show(input_parcel_path: Path, filter_id: str):
 
     # Load input data...
-    df = pdh.read_file(input_parcel_filepath)
+    df = pdh.read_file(input_parcel_path)
 
     # Just keep one parcel
-    id_column = 'UID'
+    id_column = "UID"
     df = df[df[id_column] == filter_id]
 
     # Remove all unnecessary columns
-    '''
+    """
     for column in df:
         if(not column.startswith(SENSORDATA_VV + '_')
            and not column.startswith(SENSORDATA_VH + '_')
            and not column == conf.columns['id']):
             df = df.drop(columns=column)
-    '''
+    """
 
     # Set index for transpose
     df.set_index(id_column, inplace=True)
@@ -59,7 +53,7 @@ def show(input_parcel_filepath: Path,
     df = df.transpose()
     df.reset_index(inplace=True)
 
-    '''    
+    """
     df.rename(columns={'index':'polarization_date'}, inplace=True)
 
     # Split the date and the polarization, the drop the original column
@@ -83,20 +77,23 @@ def show(input_parcel_filepath: Path,
 
     for column in df:
         logger.info(column)
-    '''
-    
+    """
+
     logger.info(df)
 
     # Plot
     df.plot()
-    
+
+
 # If the script is run directly...
 if __name__ == "__main__":
-        
+
     local_data_basedir = Path("X:/Monitoring/Markers/playground/pierog")
     local_data_dir = local_data_basedir / "2019_LANDCOVER/Run_2019-08-19_001"
-    input_parcel_filepath = local_data_dir / "Prc_BEFL_2019_2019-08-14_bufm5_weekly_parcel_classdata.sqlite"
-    show(input_parcel_filepath, filter_id = '0000280800C79F930000000C')
+    input_parcel_path = (
+        local_data_dir / "Prc_BEFL_2019_2019-08-14_bufm5_weekly_parcel_classdata.sqlite"
+    )
+    show(input_parcel_path, filter_id="0000280800C79F930000000C")
 
 
 #%%
diff --git a/cropclassification/calc_marker.py b/cropclassification/calc_marker.py
index 1a7c7429..35b0b8e3 100644
--- a/cropclassification/calc_marker.py
+++ b/cropclassification/calc_marker.py
@@ -9,7 +9,7 @@
 import shutil
 from typing import List
 
-from cropclassification.helpers import config_helper as conf 
+from cropclassification.helpers import config_helper as conf
 from cropclassification.helpers import dir_helper
 from cropclassification.helpers import log_helper
 from cropclassification.helpers import model_helper as mh
@@ -20,31 +20,35 @@
 from cropclassification.postprocess import classification_postprocess as class_post
 from cropclassification.postprocess import classification_reporting as class_report
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
-def calc_marker_task(
-        config_filepaths: List[Path],
-        default_basedir: Path):
+# -------------------------------------------------------------
+
+
+def calc_marker_task(config_paths: List[Path], default_basedir: Path):
     """
-    Runs a marker using the setting in the config_filepaths.
-    
+    Runs a marker using the setting in the config_paths.
+
     Args:
-        config_filepaths (List[Path]): the config files to load
+        config_paths (List[Path]): the config files to load
         default_basedir (Path): the dir to resolve relative paths in the config
             file to.
-    
+
     Raises:
         Exception: [description]
         Exception: [description]
     """
     # Read the configuration files
-    conf.read_config(config_filepaths, default_basedir=default_basedir)
-    
+    conf.read_config(config_paths, default_basedir=default_basedir)
+
     # Create run dir to be used for the results
-    reuse_last_run_dir = conf.calc_marker_params.getboolean('reuse_last_run_dir')
-    reuse_last_run_dir_config = conf.calc_marker_params.getboolean('reuse_last_run_dir_config')
-    run_dir = dir_helper.create_run_dir(conf.dirs.getpath('marker_dir'), reuse_last_run_dir)
+    reuse_last_run_dir = conf.calc_marker_params.getboolean("reuse_last_run_dir")
+    reuse_last_run_dir_config = conf.calc_marker_params.getboolean(
+        "reuse_last_run_dir_config"
+    )
+    run_dir = dir_helper.create_run_dir(
+        conf.dirs.getpath("marker_dir"), reuse_last_run_dir
+    )
     print(run_dir)
     if not run_dir.exists():
         os.makedirs(run_dir)
@@ -56,23 +60,27 @@ def calc_marker_task(
     logger.info(f"Config used: \n{conf.pformat_config()}")
 
     # If running in conda, export the environment
-    conda_env = os.environ.get('CONDA_DEFAULT_ENV')
+    conda_env = os.environ.get("CONDA_DEFAULT_ENV")
     if conda_env is not None:
         environment_yml_path = run_dir / f"{conda_env}.yml"
         logger.info(f"Export conda environment used to {environment_yml_path}")
         os.system(f"conda env export > {environment_yml_path}")
 
     # If the config needs to be reused as well, load it, else write it
-    config_used_filepath = run_dir / 'config_used.ini'
-    if(reuse_last_run_dir 
-       and reuse_last_run_dir_config
-       and run_dir.exists()
-       and config_used_filepath.exists()):
-        config_filepaths.append(config_used_filepath)
-        logger.info(f"Run dir config needs to be reused, so {config_filepaths}")
-        conf.read_config(config_filepaths=config_filepaths, default_basedir=default_basedir)
-        logger.info("Write new config_used.ini, because some parameters might have been added")
-        with open(config_used_filepath, 'w') as config_used_file:
+    config_used_path = run_dir / "config_used.ini"
+    if (
+        reuse_last_run_dir
+        and reuse_last_run_dir_config
+        and run_dir.exists()
+        and config_used_path.exists()
+    ):
+        config_paths.append(config_used_path)
+        logger.info(f"Run dir config needs to be reused, so {config_paths}")
+        conf.read_config(config_paths=config_paths, default_basedir=default_basedir)
+        logger.info(
+            "Write new config_used.ini, because some parameters might have been added"
+        )
+        with open(config_used_path, "w") as config_used_file:
             conf.config.write(config_used_file)
     else:
         # Copy the config files to a config dir for later notice
@@ -80,240 +88,290 @@ def calc_marker_task(
         if configfiles_used_dir.exists():
             shutil.rmtree(configfiles_used_dir)
         configfiles_used_dir.mkdir()
-        for config_filepath in config_filepaths:
-            shutil.copy(config_filepath, configfiles_used_dir)
+        for config_path in config_paths:
+            shutil.copy(config_path, configfiles_used_dir)
 
-        # Write the resolved complete config, so it can be reused 
+        # Write the resolved complete config, so it can be reused
         logger.info("Write config_used.ini, so it can be reused later on")
-        with open(config_used_filepath, 'w') as config_used_file:
+        with open(config_used_path, "w") as config_used_file:
             conf.config.write(config_used_file)
 
     # Read the info about the run
-    input_parcel_filename = conf.calc_marker_params.getpath('input_parcel_filename')
-    input_parcel_filetype = conf.calc_marker_params['input_parcel_filetype']
-    country_code = conf.calc_marker_params['country_code']
-    classes_refe_filename = conf.calc_marker_params.getpath('classes_refe_filename')
-    input_groundtruth_filename = conf.calc_marker_params.getpath('input_groundtruth_filename')
-    input_model_to_use_relativepath = conf.calc_marker_params.getpath('input_model_to_use_relativepath')
-
-    # Prepare input filepaths
+    input_parcel_filename = conf.calc_marker_params.getpath("input_parcel_filename")
+    input_parcel_filetype = conf.calc_marker_params["input_parcel_filetype"]
+    country_code = conf.calc_marker_params["country_code"]
+    classes_refe_filename = conf.calc_marker_params.getpath("classes_refe_filename")
+    input_groundtruth_filename = conf.calc_marker_params.getpath(
+        "input_groundtruth_filename"
+    )
+    input_model_to_use_relativepath = conf.calc_marker_params.getpath(
+        "input_model_to_use_relativepath"
+    )
+
+    # Prepare input paths
     if input_model_to_use_relativepath is not None:
-        input_model_to_use_filepath = conf.dirs.getpath('model_dir') / input_model_to_use_relativepath
-        if not input_model_to_use_filepath.exists():
-            raise Exception(f"Input file input_model_to_use_filepath doesn't exist: {input_model_to_use_filepath}")
+        input_model_to_use_path = (
+            conf.dirs.getpath("model_dir") / input_model_to_use_relativepath
+        )
+        if not input_model_to_use_path.exists():
+            raise Exception(
+                "Input file input_model_to_use_path doesn't exist: "
+                f"{input_model_to_use_path}"
+            )
     else:
-        input_model_to_use_filepath = None
+        input_model_to_use_path = None
 
-    input_dir = conf.dirs.getpath('input_dir')
-    input_parcel_filepath = input_dir / input_parcel_filename
+    input_dir = conf.dirs.getpath("input_dir")
+    input_parcel_path = input_dir / input_parcel_filename
     if input_groundtruth_filename is not None:
-        input_groundtruth_filepath = input_dir / input_groundtruth_filename
+        input_groundtruth_path = input_dir / input_groundtruth_filename
     else:
-        input_groundtruth_filepath = None
-    
-    refe_dir = conf.dirs.getpath('refe_dir')
-    classes_refe_filepath = refe_dir / classes_refe_filename
+        input_groundtruth_path = None
+
+    refe_dir = conf.dirs.getpath("refe_dir")
+    classes_refe_path = refe_dir / classes_refe_filename
 
     # Check if the necessary input files exist...
-    for path in [classes_refe_filepath, input_parcel_filepath]:
+    for path in [classes_refe_path, input_parcel_path]:
         if path is not None and not path.exists():
             message = f"Input file doesn't exist, so STOP: {path}"
             logger.critical(message)
             raise Exception(message)
 
     # Get some general config
-    data_ext = conf.general['data_ext']
-    output_ext = conf.general['output_ext']
-    geofile_ext = conf.general['geofile_ext']
-       
-    #-------------------------------------------------------------
+    data_ext = conf.general["data_ext"]
+    output_ext = conf.general["output_ext"]
+    geofile_ext = conf.general["geofile_ext"]
+
+    # -------------------------------------------------------------
     # The real work
-    #-------------------------------------------------------------
+    # -------------------------------------------------------------
     # STEP 1: prepare parcel data for classification and image data extraction
-    #-------------------------------------------------------------
+    # -------------------------------------------------------------
 
     # Prepare the input data for optimal image data extraction:
     #    1) apply a negative buffer on the parcel to evade mixels
     #    2) remove features that became null because of buffer
-    input_preprocessed_dir = conf.dirs.getpath('input_preprocessed_dir')
-    buffer = conf.marker.getint('buffer')       
-    input_parcel_nogeo_filepath = input_preprocessed_dir / f"{input_parcel_filename.stem}{data_ext}"
-    imagedata_input_parcel_filename = f"{input_parcel_filename.stem}_bufm{buffer}{geofile_ext}"
-    imagedata_input_parcel_filepath = input_preprocessed_dir / imagedata_input_parcel_filename
+    input_preprocessed_dir = conf.dirs.getpath("input_preprocessed_dir")
+    buffer = conf.marker.getint("buffer")
+    input_parcel_nogeo_path = (
+        input_preprocessed_dir / f"{input_parcel_filename.stem}{data_ext}"
+    )
+    imagedata_input_parcel_filename = (
+        f"{input_parcel_filename.stem}_bufm{buffer}{geofile_ext}"
+    )
+    imagedata_input_parcel_path = (
+        input_preprocessed_dir / imagedata_input_parcel_filename
+    )
     ts_util.prepare_input(
-            input_parcel_filepath=input_parcel_filepath,
-            output_imagedata_parcel_input_filepath=imagedata_input_parcel_filepath,
-            output_parcel_nogeo_filepath=input_parcel_nogeo_filepath)
+        input_parcel_path=input_parcel_path,
+        output_imagedata_parcel_input_path=imagedata_input_parcel_path,
+        output_parcel_nogeo_path=input_parcel_nogeo_path,
+    )
 
     # STEP 2: Get the timeseries data needed for the classification
-    #-------------------------------------------------------------
-    # Get the time series data (S1 and S2) to be used for the classification 
-    # Result: data is put in files in timeseries_periodic_dir, in one file per 
+    # -------------------------------------------------------------
+    # Get the time series data (S1 and S2) to be used for the classification
+    # Result: data is put in files in timeseries_periodic_dir, in one file per
     #         date/period
-    timeseries_periodic_dir = conf.dirs.getpath('timeseries_periodic_dir')
-    start_date_str = conf.marker['start_date_str']
-    end_date_str = conf.marker['end_date_str']
-    sensordata_to_use = conf.marker.getlist('sensordata_to_use')
-    parceldata_aggregations_to_use = conf.marker.getlist('parceldata_aggregations_to_use')
+    timeseries_periodic_dir = conf.dirs.getpath("timeseries_periodic_dir")
+    start_date_str = conf.marker["start_date_str"]
+    end_date_str = conf.marker["end_date_str"]
+    sensordata_to_use = conf.marker.getlist("sensordata_to_use")
+    parceldata_aggregations_to_use = conf.marker.getlist(
+        "parceldata_aggregations_to_use"
+    )
     base_filename = f"{input_parcel_filename.stem}_bufm{buffer}_weekly"
     ts.calc_timeseries_data(
-            input_parcel_filepath=imagedata_input_parcel_filepath,
-            input_country_code=country_code,
-            start_date_str=start_date_str,
-            end_date_str=end_date_str,
-            sensordata_to_get=sensordata_to_use,
-            base_filename=base_filename,
-            dest_data_dir=timeseries_periodic_dir)
+        input_parcel_path=imagedata_input_parcel_path,
+        input_country_code=country_code,
+        start_date_str=start_date_str,
+        end_date_str=end_date_str,
+        sensordata_to_get=sensordata_to_use,
+        base_filename=base_filename,
+        dest_data_dir=timeseries_periodic_dir,
+    )
 
     # STEP 3: Preprocess all data needed for the classification
-    #-------------------------------------------------------------
+    # -------------------------------------------------------------
     # Prepare the basic input file with the classes that will be classified to.
     # Remarks:
     #    - this is typically specific for the input dataset and result wanted!!!
     #    - the result is/should be a file with the following columns
     #           - id (=id_column): unique ID for each parcel
-    #           - classname (=class_column): the class that must 
+    #           - classname (=class_column): the class that must
     #             be classified to.
-    #             Remarks: - if in classes_to_ignore_for_train, class won't be used for training
+    #             Remarks: - if in classes_to_ignore_for_train, class not used for train
     #                      - if in classes_to_ignore, the class will be ignored
-    #           - pixcount:  
+    #           - pixcount:
     #             the number of S1/S2 pixels in the parcel.
     #             Is -1 if the parcel doesn't have any S1/S2 data.
-    classtype_to_prepare = conf.preprocess['classtype_to_prepare']
-    parcel_filepath = run_dir / f"{input_parcel_filename.stem}_parcel{data_ext}"
-    parcel_pixcount_filepath = timeseries_periodic_dir / f"{base_filename}_pixcount{data_ext}"
+    classtype_to_prepare = conf.preprocess["classtype_to_prepare"]
+    parcel_path = run_dir / f"{input_parcel_filename.stem}_parcel{data_ext}"
+    parcel_pixcount_path = (
+        timeseries_periodic_dir / f"{base_filename}_pixcount{data_ext}"
+    )
     class_pre.prepare_input(
-            input_parcel_filepath=input_parcel_nogeo_filepath,
-            input_parcel_filetype=input_parcel_filetype,
-            input_parcel_pixcount_filepath=parcel_pixcount_filepath,
-            classtype_to_prepare=classtype_to_prepare,
-            classes_refe_filepath=classes_refe_filepath,
-            output_parcel_filepath=parcel_filepath)
+        input_parcel_path=input_parcel_nogeo_path,
+        input_parcel_filetype=input_parcel_filetype,
+        input_parcel_pixcount_path=parcel_pixcount_path,
+        classtype_to_prepare=classtype_to_prepare,
+        classes_refe_path=classes_refe_path,
+        output_parcel_path=parcel_path,
+    )
 
     # Collect all data needed to do the classification in one input file
-    parcel_classification_data_filepath = run_dir / f"{base_filename}_parcel_classdata{data_ext}"
+    parcel_classification_data_path = (
+        run_dir / f"{base_filename}_parcel_classdata{data_ext}"
+    )
     ts.collect_and_prepare_timeseries_data(
-            input_parcel_filepath=input_parcel_nogeo_filepath,
-            timeseries_dir=timeseries_periodic_dir,
-            base_filename=base_filename,
-            output_filepath=parcel_classification_data_filepath,
-            start_date_str=start_date_str,
-            end_date_str=end_date_str,
-            sensordata_to_use=sensordata_to_use,
-            parceldata_aggregations_to_use=parceldata_aggregations_to_use)
+        input_parcel_path=input_parcel_nogeo_path,
+        timeseries_dir=timeseries_periodic_dir,
+        base_filename=base_filename,
+        output_path=parcel_classification_data_path,
+        start_date_str=start_date_str,
+        end_date_str=end_date_str,
+        sensordata_to_use=sensordata_to_use,
+        parceldata_aggregations_to_use=parceldata_aggregations_to_use,
+    )
 
     # STEP 4: Train and test if necessary... and predict
-    #-------------------------------------------------------------
-    markertype = conf.marker.get('markertype')
-    parcel_predictions_proba_all_filepath = run_dir / f"{base_filename}_predict_proba_all{data_ext}"
-    classifier_ext = conf.classifier['classifier_ext']
-    classifier_basefilepath = run_dir / f"{markertype}_01_mlp{classifier_ext}"    
+    # -------------------------------------------------------------
+    markertype = conf.marker.get("markertype")
+    parcel_predictions_proba_all_path = (
+        run_dir / f"{base_filename}_predict_proba_all{data_ext}"
+    )
+    classifier_ext = conf.classifier["classifier_ext"]
+    classifier_basepath = run_dir / f"{markertype}_01_mlp{classifier_ext}"
 
     # Check if a model exists already
-    if input_model_to_use_filepath is None:
-        best_model = mh.get_best_model(run_dir, acc_metric_mode='min')
+    if input_model_to_use_path is None:
+        best_model = mh.get_best_model(run_dir, acc_metric_mode="min")
         if best_model is not None:
-            input_model_to_use_filepath = best_model['filepath']
-            
+            input_model_to_use_path = best_model["path"]
+
     # if there is no model to use specified, train one!
-    parcel_test_filepath = None
-    parcel_predictions_proba_test_filepath = None
-    if input_model_to_use_filepath is None:
+    parcel_test_path = None
+    parcel_predictions_proba_test_path = None
+    if input_model_to_use_path is None:
 
         # Create the training sample...
-        # Remark: this creates a list of representative test parcel + a list of (candidate) training parcel
-        balancing_strategy = conf.marker['balancing_strategy']
-        parcel_train_filepath = run_dir / f"{base_filename}_parcel_train{data_ext}"
-        parcel_test_filepath = run_dir / f"{base_filename}_parcel_test{data_ext}"
+        # Remark: this creates a list of representative test parcel + a list of
+        # (candidate) training parcel
+        balancing_strategy = conf.marker["balancing_strategy"]
+        parcel_train_path = run_dir / f"{base_filename}_parcel_train{data_ext}"
+        parcel_test_path = run_dir / f"{base_filename}_parcel_test{data_ext}"
         class_pre.create_train_test_sample(
-                input_parcel_filepath=parcel_filepath,
-                output_parcel_train_filepath=parcel_train_filepath,
-                output_parcel_test_filepath=parcel_test_filepath,
-                balancing_strategy=balancing_strategy)
+            input_parcel_path=parcel_path,
+            output_parcel_train_path=parcel_train_path,
+            output_parcel_test_path=parcel_test_path,
+            balancing_strategy=balancing_strategy,
+        )
 
         # Train the classifier and output predictions
-        parcel_predictions_proba_test_filepath = run_dir / f"{base_filename}_predict_proba_test{data_ext}"
+        parcel_predictions_proba_test_path = (
+            run_dir / f"{base_filename}_predict_proba_test{data_ext}"
+        )
         classification.train_test_predict(
-                input_parcel_train_filepath=parcel_train_filepath,
-                input_parcel_test_filepath=parcel_test_filepath,
-                input_parcel_all_filepath=parcel_filepath,
-                input_parcel_classification_data_filepath=parcel_classification_data_filepath,
-                output_classifier_basefilepath=classifier_basefilepath,
-                output_predictions_test_filepath=parcel_predictions_proba_test_filepath,
-                output_predictions_all_filepath=parcel_predictions_proba_all_filepath)
+            input_parcel_train_path=parcel_train_path,
+            input_parcel_test_path=parcel_test_path,
+            input_parcel_all_path=parcel_path,
+            input_parcel_classification_data_path=parcel_classification_data_path,
+            output_classifier_basepath=classifier_basepath,
+            output_predictions_test_path=parcel_predictions_proba_test_path,
+            output_predictions_all_path=parcel_predictions_proba_all_path,
+        )
     else:
         # there is a classifier specified, so just use it!
         classification.predict(
-                input_parcel_filepath=parcel_filepath,
-                input_parcel_classification_data_filepath=parcel_classification_data_filepath,
-                input_classifier_basefilepath=classifier_basefilepath,
-                input_classifier_filepath=input_model_to_use_filepath,
-                output_predictions_filepath=parcel_predictions_proba_all_filepath)
+            input_parcel_path=parcel_path,
+            input_parcel_classification_data_path=parcel_classification_data_path,
+            input_classifier_basepath=classifier_basepath,
+            input_classifier_path=input_model_to_use_path,
+            output_predictions_path=parcel_predictions_proba_all_path,
+        )
 
     # STEP 5: if necessary, do extra postprocessing
-    #-------------------------------------------------------------    
-    '''if postprocess_to_groups is not None:
-        # TODO 
-    '''
+    # -------------------------------------------------------------
+    """if postprocess_to_groups is not None:
+        # TODO
+    """
 
     # STEP 6: do the default, mandatory postprocessing
-    #-------------------------------------------------------------
+    # -------------------------------------------------------------
     # If it was necessary to train, there will be a test prediction... so postprocess it
-    parcel_predictions_test_filepath = None
+    parcel_predictions_test_path = None
     parcel_predictions_test_geopath = None
-    if(input_model_to_use_filepath is None 
-       and parcel_test_filepath is not None 
-       and parcel_predictions_proba_test_filepath is not None):
-        parcel_predictions_test_filepath = run_dir / f"{base_filename}_predict_test{data_ext}"
-        parcel_predictions_test_geopath = run_dir / f"{base_filename}_predict_test{geofile_ext}"
+    if (
+        input_model_to_use_path is None
+        and parcel_test_path is not None
+        and parcel_predictions_proba_test_path is not None
+    ):
+        parcel_predictions_test_path = (
+            run_dir / f"{base_filename}_predict_test{data_ext}"
+        )
+        parcel_predictions_test_geopath = (
+            run_dir / f"{base_filename}_predict_test{geofile_ext}"
+        )
         class_post.calc_top3_and_consolidation(
-                input_parcel_filepath=parcel_test_filepath,
-                input_parcel_probabilities_filepath=parcel_predictions_proba_test_filepath,
-                input_parcel_geofilepath=input_parcel_filepath,
-                output_predictions_filepath=parcel_predictions_test_filepath,
-                output_predictions_geofilepath=parcel_predictions_test_geopath)
-        
+            input_parcel_path=parcel_test_path,
+            input_parcel_probabilities_path=parcel_predictions_proba_test_path,
+            input_parcel_geopath=input_parcel_path,
+            output_predictions_path=parcel_predictions_test_path,
+            output_predictions_geopath=parcel_predictions_test_geopath,
+        )
+
     # Postprocess predictions
-    parcel_predictions_all_filepath = run_dir / f"{base_filename}_predict_all{data_ext}"
-    parcel_predictions_all_geopath = run_dir / f"{base_filename}_predict_all{geofile_ext}"
-    parcel_predictions_all_output_filepath = run_dir / f"{base_filename}_predict_all_output{output_ext}"
+    parcel_predictions_all_path = run_dir / f"{base_filename}_predict_all{data_ext}"
+    parcel_predictions_all_geopath = (
+        run_dir / f"{base_filename}_predict_all{geofile_ext}"
+    )
+    parcel_predictions_all_output_path = (
+        run_dir / f"{base_filename}_predict_all_output{output_ext}"
+    )
     class_post.calc_top3_and_consolidation(
-            input_parcel_filepath=parcel_filepath,
-            input_parcel_probabilities_filepath=parcel_predictions_proba_all_filepath,
-            input_parcel_geofilepath=input_parcel_filepath,
-            output_predictions_filepath=parcel_predictions_all_filepath,
-            output_predictions_geofilepath=parcel_predictions_all_geopath,
-            output_predictions_output_filepath=parcel_predictions_all_output_filepath)
+        input_parcel_path=parcel_path,
+        input_parcel_probabilities_path=parcel_predictions_proba_all_path,
+        input_parcel_geopath=input_parcel_path,
+        output_predictions_path=parcel_predictions_all_path,
+        output_predictions_geopath=parcel_predictions_all_geopath,
+        output_predictions_output_path=parcel_predictions_all_output_path,
+    )
 
     # STEP 7: Report on the accuracy, incl. ground truth
-    #-------------------------------------------------------------
+    # -------------------------------------------------------------
     # Preprocess the ground truth data if it is provided
-    groundtruth_filepath = None
-    if input_groundtruth_filepath is not None:
-            groundtruth_filepath = run_dir / f"{input_groundtruth_filepath.stem}_classes{input_groundtruth_filepath.suffix}"
-            class_pre.prepare_input(
-                    input_parcel_filepath=input_groundtruth_filepath,
-                    input_parcel_filetype=input_parcel_filetype,
-                    input_parcel_pixcount_filepath=parcel_pixcount_filepath,
-                    classtype_to_prepare=conf.preprocess['classtype_to_prepare_groundtruth'],
-                    classes_refe_filepath=classes_refe_filepath,
-                    output_parcel_filepath=groundtruth_filepath)
+    groundtruth_path = None
+    if input_groundtruth_path is not None:
+        groundtruth_path = (
+            run_dir
+            / f"{input_groundtruth_path.stem}_classes{input_groundtruth_path.suffix}"
+        )
+        class_pre.prepare_input(
+            input_parcel_path=input_groundtruth_path,
+            input_parcel_filetype=input_parcel_filetype,
+            input_parcel_pixcount_path=parcel_pixcount_path,
+            classtype_to_prepare=conf.preprocess["classtype_to_prepare_groundtruth"],
+            classes_refe_path=classes_refe_path,
+            output_parcel_path=groundtruth_path,
+        )
 
     # If we trained a model, there is a test prediction we want to report on
-    if input_model_to_use_filepath is None and parcel_predictions_test_geopath is not None:
+    if input_model_to_use_path is None and parcel_predictions_test_geopath is not None:
         # Print full reporting on the accuracy of the test dataset
-        report_txt = Path(f"{str(parcel_predictions_test_filepath)}_accuracy_report.txt")
+        report_txt = Path(f"{str(parcel_predictions_test_path)}_accuracy_report.txt")
         class_report.write_full_report(
-                parcel_predictions_geopath=parcel_predictions_test_geopath,
-                output_report_txt=report_txt,
-                parcel_ground_truth_filepath=groundtruth_filepath)
+            parcel_predictions_geopath=parcel_predictions_test_geopath,
+            output_report_txt=report_txt,
+            parcel_ground_truth_path=groundtruth_path,
+        )
 
     # Print full reporting on the accuracy of the full dataset
-    report_txt = Path(f"{str(parcel_predictions_all_filepath)}_accuracy_report.txt")
+    report_txt = Path(f"{str(parcel_predictions_all_path)}_accuracy_report.txt")
     class_report.write_full_report(
-            parcel_predictions_geopath=parcel_predictions_all_geopath,
-            output_report_txt=report_txt,
-            parcel_ground_truth_filepath=groundtruth_filepath)
+        parcel_predictions_geopath=parcel_predictions_all_geopath,
+        output_report_txt=report_txt,
+        parcel_ground_truth_path=groundtruth_path,
+    )
 
     logging.shutdown()
-    
\ No newline at end of file
diff --git a/cropclassification/calc_timeseries.py b/cropclassification/calc_timeseries.py
index 5678b203..db6b143e 100644
--- a/cropclassification/calc_timeseries.py
+++ b/cropclassification/calc_timeseries.py
@@ -15,30 +15,31 @@
 from cropclassification.preprocess import timeseries_calc_dias_onda_per_image as calc_ts
 from cropclassification.preprocess import timeseries_util as ts_util
 
-def calc_timeseries_task(
-        config_filepaths: List[Path], 
-        default_basedir: Path):
+
+def calc_timeseries_task(config_paths: List[Path], default_basedir: Path):
     """
-    Runs a calculation of timeseries with the settings in the config_filepaths.
-    
+    Runs a calculation of timeseries with the settings in the config_paths.
+
     Args:
-        config_filepaths (List[Path]): the config files to load
+        config_paths (List[Path]): the config files to load
         default_basedir (Path): the dir to resolve relative paths in the config
             file to.
-    
+
     Raises:
         Exception: [description]
         Exception: [description]
     """
     # Read the configuration files
-    conf.read_config(config_filepaths, default_basedir=default_basedir)
+    conf.read_config(config_paths, default_basedir=default_basedir)
+
+    test = conf.calc_timeseries_params.getboolean("test")
 
-    test = conf.calc_timeseries_params.getboolean('test')
-    
     # As we want a weekly calculation, get nearest monday for start and stop day
-    start_date = ts_util.get_monday(conf.marker['start_date_str']) # output: vb 2018_2_1 - maandag van week 2 van 2018
-    end_date = ts_util.get_monday(conf.marker['end_date_str']) 
-    
+    start_date = ts_util.get_monday(
+        conf.marker["start_date_str"]
+    )  # output: vb 2018_2_1 - maandag van week 2 van 2018
+    end_date = ts_util.get_monday(conf.marker["end_date_str"])
+
     calc_year_start = start_date.year
     calc_year_stop = end_date.year
     calc_month_start = start_date.month
@@ -59,22 +60,26 @@ def calc_timeseries_task(
     logger = log_helper.main_log_init(log_dir, __name__, log_level)
     logger.info(f"Config used: \n{conf.pformat_config()}")
 
-    if test:         
-        logger.info(f"As we are testing, clean all test logging and use new log_dir: {log_dir}")
+    if test:
+        logger.info(
+            f"As we are testing, clean all test logging and use new log_dir: {log_dir}"
+        )
 
     # Write the consolidated config as ini file again to the run dir
-    config_used_filepath = log_dir / 'config_used.ini'
-    with open(config_used_filepath, 'w') as config_used_file:
+    config_used_path = log_dir / "config_used.ini"
+    with open(config_used_path, "w") as config_used_file:
         conf.config.write(config_used_file)
 
     # TODO: this shouldn't be hardcoded!
-    input_parcel_filename = conf.calc_timeseries_params.getpath('input_parcel_filename')
-    input_features_filename = Path(f"{input_parcel_filename.stem}_bufm5{input_parcel_filename.suffix}")
-    input_preprocessed_dir = conf.dirs.getpath('input_preprocessed_dir')
-    input_features_filepath = input_preprocessed_dir / input_features_filename
-    
-    # Init output dir 
-    timeseries_per_image_dir = conf.dirs.getpath('timeseries_per_image_dir')
+    input_parcel_filename = conf.calc_timeseries_params.getpath("input_parcel_filename")
+    input_features_filename = Path(
+        f"{input_parcel_filename.stem}_bufm5{input_parcel_filename.suffix}"
+    )
+    input_preprocessed_dir = conf.dirs.getpath("input_preprocessed_dir")
+    input_features_path = input_preprocessed_dir / input_features_filename
+
+    # Init output dir
+    timeseries_per_image_dir = conf.dirs.getpath("timeseries_per_image_dir")
     if not test:
         output_basedir = timeseries_per_image_dir
     else:
@@ -88,28 +93,28 @@ def calc_timeseries_task(
             shutil.rmtree(str(output_dir) + os.sep)
 
     # Temp dir + clean contents from it.
-    temp_dir = conf.dirs.getpath('temp_dir') / 'calc_dias'
+    temp_dir = conf.dirs.getpath("temp_dir") / "calc_dias"
     logger.info(f"Clean the temp dir {temp_dir}")
     if temp_dir.exists():
         # By adding a / at the end, only the contents are recursively deleted
         shutil.rmtree(str(temp_dir) + os.sep)
-        
+
     """
     # TEST to extract exact footprint from S1 image...
-    filepath = "/mnt/NAS3/CARD/FLANDERS/S1A/L1TC/2017/01/01/S1A_IW_GRDH_1SDV_20170101T055005_20170101T055030_014634_017CB9_Orb_RBN_RTN_Cal_TC.CARD/S1A_IW_GRDH_1SDV_20170101T055005_20170101T055030_014634_017CB9_Orb_RBN_RTN_Cal_TC.data/Gamma0_VH.img"
-    image = rasterio.open(filepath)
+    path = "/mnt/NAS3/CARD/FLANDERS/S1A/L1TC/2017/01/01/S1A_IW_GRDH_1SDV_20170101T055005_20170101T055030_014634_017CB9_Orb_RBN_RTN_Cal_TC.CARD/S1A_IW_GRDH_1SDV_20170101T055005_20170101T055030_014634_017CB9_Orb_RBN_RTN_Cal_TC.data/Gamma0_VH.img"
+    image = rasterio.open(path)
     geoms = list(rasterio.features.dataset_features(src=image, as_mask=True, precision=5))
-    footprint = gpd.GeoDataFrame.from_features(geoms)        
+    footprint = gpd.GeoDataFrame.from_features(geoms)
     logger.info(footprint)
-    footprint = footprint.simplify(0.00001)        
+    footprint = footprint.simplify(0.00001)
     logger.info(footprint)
     logger.info("Ready")
     # Start calculation
     """
 
-    ##### Process S1 GRD images #####
-    input_image_filepaths = []
-    for year in range(calc_year_start, calc_year_stop+1):
+    # Process S1 GRD images
+    input_image_paths = []
+    for year in range(calc_year_start, calc_year_stop + 1):
         # TODO: works, but doesn't seem to be the most elegant code...
         if year < calc_year_stop:
             month_stop = 12
@@ -119,36 +124,41 @@ def calc_timeseries_task(
             month_start = 1
         else:
             month_start = calc_month_start
-        for month in range(calc_month_start, calc_month_stop+1):
-            input_image_searchstr = f"/mnt/NAS*/CARD/FLANDERS/S1*/L1TC/{year}/{month:02d}/*/*.CARD"
-            input_image_searchstr = "X:/Monitoring/Markers/playground/tmp/*.CARD"
-            input_image_filepaths.extend(glob.glob(input_image_searchstr))
-    input_image_filepaths = list(set(input_image_filepaths))
-    logger.info(f"Found {len(input_image_filepaths)} S1 GRD images to process")
+        for month in range(calc_month_start, calc_month_stop + 1):
+            input_image_searchstr = (
+                f"/mnt/NAS*/CARD/FLANDERS/S1*/L1TC/{year}/{month:02d}/*/*.CARD"
+            )
+            input_image_paths.extend(glob.glob(input_image_searchstr))
+    logger.info(f"Found {len(input_image_paths)} S1 GRD images to process")
 
     if test:
         # Take only the x first images found while testing
-        input_image_filepaths = input_image_filepaths[:10]
-        logger.info(f"As we are only testing, process only {len(input_image_filepaths)} test images")
+        input_image_paths = input_image_paths[:10]
+        logger.info(
+            f"As we are only testing, process only {len(input_image_paths)} test images"
+        )
+
+    input_image_paths = [
+        Path(input_image_path) for input_image_path in input_image_paths
+    ]
 
-    input_image_filepaths = [Path(input_image_filepath) for input_image_filepath in input_image_filepaths]
-    
     try:
         calc_ts.calc_stats_per_image(
-                features_filepath=input_features_filepath,
-                id_column=conf.columns['id'],
-                image_paths=input_image_filepaths,
-                bands=['VV', 'VH'],
-                output_dir=output_dir,
-                temp_dir=temp_dir,
-                log_dir=log_dir,
-                log_level=log_level)
+            features_path=input_features_path,
+            id_column=conf.columns["id"],
+            image_paths=input_image_paths,
+            bands=["VV", "VH"],
+            output_dir=output_dir,
+            temp_dir=temp_dir,
+            log_dir=log_dir,
+            log_level=log_level,
+        )
     except Exception as ex:
         logger.exception(ex)
 
-    ##### Process S2 images #####
-    input_image_filepaths = []
-    for year in range(calc_year_start, calc_year_stop+1):
+    # Process S2 images
+    input_image_paths = []
+    for year in range(calc_year_start, calc_year_stop + 1):
         # TODO: works, but doesn't seem to be the most elegant code...
         if year < calc_year_stop:
             month_stop = 12
@@ -158,38 +168,46 @@ def calc_timeseries_task(
             month_start = 1
         else:
             month_start = calc_month_start
-        for month in range(month_start, month_stop+1):
-            input_image_searchstr = f"/mnt/NAS*/CARD/FLANDERS/S2*/L2A/{year}/{month:02d}/*/*.SAFE"
+        for month in range(month_start, month_stop + 1):
+            input_image_searchstr = (
+                f"/mnt/NAS*/CARD/FLANDERS/S2*/L2A/{year}/{month:02d}/*/*.SAFE"
+            )
             logger.info(f"Search for {input_image_searchstr}")
-            input_image_filepaths.extend(glob.glob(input_image_searchstr))    
-    logger.info(f"Found {len(input_image_filepaths)} S2 images to process")
+            input_image_paths.extend(glob.glob(input_image_searchstr))
+    logger.info(f"Found {len(input_image_paths)} S2 images to process")
 
     if test:
         # Take only the x first images found while testing
-        input_image_filepaths = input_image_filepaths[:10]
-        logger.info(f"As we are only testing, process only {len(input_image_filepaths)} test images")
-
-    # TODO: refactor underlying code so the SCL band is used regardless of it being passed here
-    max_cloudcover_pct = conf.timeseries.getfloat('max_cloudcover_pct')
-    input_image_filepaths = [Path(input_image_filepath) for input_image_filepath in input_image_filepaths]
+        input_image_paths = input_image_paths[:10]
+        logger.info(
+            f"As we are only testing, process only {len(input_image_paths)} test images"
+        )
+
+    # TODO: refactor underlying code so the SCL band is used regardless of it being
+    # passed here
+    max_cloudcover_pct = conf.timeseries.getfloat("max_cloudcover_pct")
+    input_image_paths = [
+        Path(input_image_path) for input_image_path in input_image_paths
+    ]
 
     try:
         calc_ts.calc_stats_per_image(
-                features_filepath=input_features_filepath,
-                id_column=conf.columns['id'],
-                image_paths=input_image_filepaths,
-                bands=conf.timeseries.getlist('s2bands'),
-                output_dir=output_dir,
-                temp_dir=temp_dir,
-                log_dir=log_dir,
-                log_level=log_level,
-                max_cloudcover_pct=max_cloudcover_pct)
+            features_path=input_features_path,
+            id_column=conf.columns["id"],
+            image_paths=input_image_paths,
+            bands=conf.timeseries.getlist("s2bands"),
+            output_dir=output_dir,
+            temp_dir=temp_dir,
+            log_dir=log_dir,
+            log_level=log_level,
+            max_cloudcover_pct=max_cloudcover_pct,
+        )
     except Exception as ex:
         logger.exception(ex)
 
-    ##### Process S1 Coherence images #####   
-    input_image_filepaths = []
-    for year in range(calc_year_start, calc_year_stop+1):
+    # Process S1 Coherence images
+    input_image_paths = []
+    for year in range(calc_year_start, calc_year_stop + 1):
         # TODO: works, but doesn't seem to be the most elegant code...
         if year < calc_year_stop:
             month_stop = 12
@@ -199,29 +217,38 @@ def calc_timeseries_task(
             month_start = 1
         else:
             month_start = calc_month_start
-        for month in range(calc_month_start, calc_month_stop+1):
-            input_image_searchstr = f"/mnt/NAS*/CARD/FLANDERS/S1*/L1CO/{year}/{month:02d}/*/*.CARD"
-            input_image_filepaths.extend(glob.glob(input_image_searchstr))  
-    logger.info(f"Found {len(input_image_filepaths)} S1 Coherence images to process")
+        for month in range(calc_month_start, calc_month_stop + 1):
+            input_image_searchstr = (
+                f"/mnt/NAS*/CARD/FLANDERS/S1*/L1CO/{year}/{month:02d}/*/*.CARD"
+            )
+            input_image_paths.extend(glob.glob(input_image_searchstr))
+    logger.info(f"Found {len(input_image_paths)} S1 Coherence images to process")
 
     if test:
         # Take only the x first images found while testing
-        input_image_filepaths = input_image_filepaths[:10]
-        logger.info(f"As we are only testing, process only {len(input_image_filepaths)} test images")
+        input_image_paths = input_image_paths[:10]
+        logger.info(
+            f"As we are only testing, process only {len(input_image_paths)} test images"
+        )
 
-    input_image_filepaths = [Path(input_image_filepath) for input_image_filepath in input_image_filepaths]
+    input_image_paths = [
+        Path(input_image_path) for input_image_path in input_image_paths
+    ]
 
     try:
-        calc_ts.calc_stats_per_image(features_filepath=input_features_filepath,
-                id_column=conf.columns['id'],
-                image_paths=input_image_filepaths,
-                bands=['VV', 'VH'],
-                output_dir=output_dir,
-                temp_dir=temp_dir,
-                log_dir=log_dir,
-                log_level=log_level)
+        calc_ts.calc_stats_per_image(
+            features_path=input_features_path,
+            id_column=conf.columns["id"],
+            image_paths=input_image_paths,
+            bands=["VV", "VH"],
+            output_dir=output_dir,
+            temp_dir=temp_dir,
+            log_dir=log_dir,
+            log_level=log_level,
+        )
     except Exception as ex:
         logger.exception(ex)
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     raise Exception("Not implemented")
diff --git a/cropclassification/cropclassification.py b/cropclassification/cropclassification.py
index 1d0e916a..dedcadc1 100644
--- a/cropclassification/cropclassification.py
+++ b/cropclassification/cropclassification.py
@@ -7,73 +7,101 @@
 import configparser
 from pathlib import Path
 import sys
-sys.path.insert(0, str(Path(__file__).resolve().parent / '..'))
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
 
 def main():
-       
-    ##### Interprete arguments #####
+
+    # Interprete arguments
     parser = argparse.ArgumentParser(add_help=False)
 
     # Optional arguments
-    optional = parser.add_argument_group('Optional arguments')
-    optional.add_argument('-t', '--tasksdir',
-            help='The path to the dir where tasks (*.ini) to be run can be found.')
-    # Add back help         
-    optional.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
-            help='Show this help message and exit')
+    optional = parser.add_argument_group("Optional arguments")
+    optional.add_argument(
+        "-t",
+        "--tasksdir",
+        help="The path to the dir where tasks (*.ini) to be run can be found.",
+    )
+    # Add back help
+    optional.add_argument(
+        "-h",
+        "--help",
+        action="help",
+        default=argparse.SUPPRESS,
+        help="Show this help message and exit",
+    )
     args = parser.parse_args()
 
     # If tasks dir is specified, use it
     if args.tasksdir is not None:
         return cropclassification(tasksdir=Path(args.tasksdir))
     else:
-        usertasksdir = Path.home() / 'cropclassification' / 'tasks'
+        usertasksdir = Path.home() / "cropclassification" / "tasks"
         if usertasksdir.exists():
             return cropclassification(tasksdir=usertasksdir)
-        else: 
-            print(f"Error: no tasksdir specified, and default tasks dir ({usertasksdir}) does not exist, so stop\n")
+        else:
+            print(
+                f"Error: no tasksdir specified, and default tasks dir ({usertasksdir}) "
+                "does not exist, so stop\n"
+            )
             parser.print_help()
             sys.exit(1)
 
+
 def cropclassification(tasksdir: Path):
-    
+
     # Get the tasks and treat them
-    task_paths = sorted(tasksdir.glob('task_*.ini'))
-    for task_path in task_paths:      
+    task_paths = sorted(tasksdir.glob("task_*.ini"))
+    for task_path in task_paths:
         # Create configparser and read task file!
         task_config = configparser.ConfigParser(
-                interpolation=configparser.ExtendedInterpolation(),
-                converters={'list': lambda x: [i.strip() for i in x.split(',')]},
-                allow_no_value=True)
+            interpolation=configparser.ExtendedInterpolation(),
+            converters={"list": lambda x: [i.strip() for i in x.split(",")]},
+            allow_no_value=True,
+        )
         task_config.read(task_path)
 
         # Now get the info we want from the task config
-        action = task_config['task'].get('action')
+        action = task_config["task"].get("action")
         default_basedir = task_path.parent.parent
 
         # Determine the config files to load
         script_dir = Path(__file__).resolve().parent
-        config_filepaths = [script_dir / 'general.ini']
-        extra_config_files_to_load = task_config['task'].getlist('extra_config_files_to_load')
+        config_paths = [script_dir / "general.ini"]
+        extra_config_files_to_load = task_config["task"].getlist(
+            "extra_config_files_to_load"
+        )
         if extra_config_files_to_load is not None:
             for config_file in extra_config_files_to_load:
-                config_file_formatted = Path(config_file.format(task_filepath=task_path, tasks_dir=task_path.parent))
+                config_file_formatted = Path(
+                    config_file.format(
+                        task_filepath=task_path,
+                        task_path=task_path,
+                        tasks_dir=task_path.parent,
+                    )
+                )
                 if not config_file_formatted.is_absolute():
-                    config_file_formatted = (default_basedir / config_file_formatted).resolve()
-                config_filepaths.append(Path(config_file_formatted))
+                    config_file_formatted = (
+                        default_basedir / config_file_formatted
+                    ).resolve()
+                config_paths.append(Path(config_file_formatted))
+
+        if action == "calc_marker":
+            from cropclassification import calc_marker
 
-        if action == 'calc_marker':
-            from cropclassification import calc_marker 
             calc_marker.calc_marker_task(
-                    config_filepaths=config_filepaths, 
-                    default_basedir=default_basedir)
-        elif action == 'calc_timeseries':
-            from cropclassification import calc_timeseries 
+                config_paths=config_paths, default_basedir=default_basedir
+            )
+        elif action == "calc_timeseries":
+            from cropclassification import calc_timeseries
+
             calc_timeseries.calc_timeseries_task(
-                    config_filepaths=config_filepaths, 
-                    default_basedir=default_basedir)
+                config_paths=config_paths, default_basedir=default_basedir
+            )
         else:
             raise Exception(f"Action not supported: {action}")
 
-if __name__ == '__main__':
-    main()
\ No newline at end of file
+
+if __name__ == "__main__":
+    main()
diff --git a/cropclassification/helpers/config_helper.py b/cropclassification/helpers/config_helper.py
index 72a8433c..284f99ca 100644
--- a/cropclassification/helpers/config_helper.py
+++ b/cropclassification/helpers/config_helper.py
@@ -5,88 +5,101 @@
 
 import configparser
 import json
-import os
 from pathlib import Path
 import pprint
 from typing import List, Optional
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
+
+
+def read_config(config_paths: List[Path], default_basedir: Optional[Path] = None):
 
-def read_config(
-        config_filepaths: List[Path], 
-        default_basedir: Path = None):
-            
     # Read the configuration
     global config
     config = configparser.ConfigParser(
-            interpolation=configparser.ExtendedInterpolation(),
-            converters={'list': lambda x: [i.strip() for i in x.split(',')],
-                        'listint': lambda x: [int(i.strip()) for i in x.split(',')],
-                        'listfloat': lambda x: [float(i.strip()) for i in x.split(',')],
-                        'dict': lambda x: json.loads(x),
-                        'path': lambda x: None if x is None else Path(x)},
-            allow_no_value=True)
-
-    # Check if all config filepaths are ok
-    for config_filepath in config_filepaths:
-        if not config_filepath.exists():
-            raise Exception(f"Config file doesn't exist: {config_filepath}")
-
-    config.read(config_filepaths)
-
-    # If the data_dir parameter is a relative path, try to resolve it towards 
+        interpolation=configparser.ExtendedInterpolation(),
+        converters={
+            "list": lambda x: [i.strip() for i in x.split(",")],
+            "listint": lambda x: [int(i.strip()) for i in x.split(",")],
+            "listfloat": lambda x: [float(i.strip()) for i in x.split(",")],
+            "dict": lambda x: json.loads(x),
+            "path": lambda x: None if x is None else Path(x),
+        },
+        allow_no_value=True,
+    )
+
+    # Check if all config paths are ok
+    for config_path in config_paths:
+        if not config_path.exists():
+            raise Exception(f"Config file doesn't exist: {config_path}")
+
+    config.read(config_paths)
+
+    # If the data_dir parameter is a relative path, try to resolve it towards
     # the default basedir of, if specfied.
-    data_dir = config['dirs'].getpath('data_dir')
+    data_dir = config["dirs"].getpath("data_dir")
     if not data_dir.is_absolute():
         if default_basedir is None:
-            raise Exception(f"Config parameter dirs.data_dir is relative, but no default_basedir supplied!")
+            raise Exception(
+                f"Config parameter dirs.data_dir is relative, but no default_basedir supplied!"
+            )
         data_dir_absolute = (default_basedir / data_dir).resolve()
-        print(f"Config parameter dirs.data_dir was relative, so is now resolved to {data_dir_absolute}")
-        config['dirs']['data_dir'] = data_dir_absolute.as_posix()
+        print(
+            f"Config parameter dirs.data_dir was relative, so is now resolved to {data_dir_absolute}"
+        )
+        config["dirs"]["data_dir"] = data_dir_absolute.as_posix()
 
-    # If the marker_basedir parameter is a relative path, try to resolve it towards 
+    # If the marker_basedir parameter is a relative path, try to resolve it towards
     # the default basedir of, if specfied.
-    marker_basedir = config['dirs'].getpath('marker_basedir')
+    marker_basedir = config["dirs"].getpath("marker_basedir")
     if not marker_basedir.is_absolute():
         if default_basedir is None:
-            raise Exception(f"Config parameter dirs.marker_basedir is relative, but no default_basedir supplied!")
+            raise Exception(
+                f"Config parameter dirs.marker_basedir is relative, but no default_basedir supplied!"
+            )
         marker_basedir_absolute = (default_basedir / marker_basedir).resolve()
-        print(f"Config parameter dirs.marker_basedir was relative, so is now resolved to {marker_basedir_absolute}")
-        config['dirs']['marker_basedir'] = marker_basedir_absolute.as_posix()
+        print(
+            f"Config parameter dirs.marker_basedir was relative, so is now resolved to {marker_basedir_absolute}"
+        )
+        config["dirs"]["marker_basedir"] = marker_basedir_absolute.as_posix()
 
-    global config_filepaths_used
-    config_filepaths_used = config_filepaths
+    global config_paths_used
+    config_paths_used = config_paths
 
-    # Now set global variables to each section as shortcuts    
+    # Now set global variables to each section as shortcuts
     global general
-    general = config['general']
+    general = config["general"]
     global calc_timeseries_params
-    calc_timeseries_params = config['calc_timeseries_params']
+    calc_timeseries_params = config["calc_timeseries_params"]
     global calc_marker_params
-    calc_marker_params = config['calc_marker_params']
+    calc_marker_params = config["calc_marker_params"]
     global marker
-    marker = config['marker']
+    marker = config["marker"]
     global timeseries
-    timeseries = config['timeseries']
+    timeseries = config["timeseries"]
     global preprocess
-    preprocess = config['preprocess']
+    preprocess = config["preprocess"]
     global classifier
-    classifier = config['classifier']
+    classifier = config["classifier"]
     global postprocess
-    postprocess = config['postprocess']
+    postprocess = config["postprocess"]
     global columns
-    columns = config['columns']
+    columns = config["columns"]
     global dirs
-    dirs = config['dirs']
-        
+    dirs = config["dirs"]
+
+
 def pformat_config():
-    message = f"Config files used: {pprint.pformat(config_filepaths_used)} \n"
+    message = f"Config files used: {pprint.pformat(config_paths_used)} \n"
     message += "Config info listing:\n"
-    message += pprint.pformat({section: dict(config[section]) for section in config.sections()})
+    message += pprint.pformat(
+        {section: dict(config[section]) for section in config.sections()}
+    )
     return message
-    
+
+
 def as_dict():
     """
     Converts a ConfigParser object into a dictionary.
@@ -99,4 +112,4 @@ def as_dict():
         the_dict[section] = {}
         for key, val in config.items(section):
             the_dict[section][key] = val
-    return the_dict
\ No newline at end of file
+    return the_dict
diff --git a/cropclassification/helpers/dir_helper.py b/cropclassification/helpers/dir_helper.py
index 8f3b2209..fbf56b27 100644
--- a/cropclassification/helpers/dir_helper.py
+++ b/cropclassification/helpers/dir_helper.py
@@ -2,14 +2,13 @@
 Helper regarding directory operations.
 """
 
-from datetime import datetime 
+from datetime import datetime
 import os
 from pathlib import Path
 import re
 
-def create_run_dir(
-        class_base_dir: Path,
-        reuse_last_run_dir: bool) -> Path:
+
+def create_run_dir(class_base_dir: Path, reuse_last_run_dir: bool) -> Path:
     """
     Create a new run dir, or get the last run dir.
 
@@ -24,11 +23,15 @@ def create_run_dir(
 
     # Look for all existing run dirs
     base_filename = f"Run_{datetime.now().strftime('%Y-%m-%d')}"
-    pattern = re.compile(base_filename + '_[0-9]{3}')
-    dir_list = [x.path for x in os.scandir(class_base_dir) if x.is_dir() and re.search(pattern, x.path)]
-
-    # No dir yet with the patern -> return new one 
-    if (dir_list is None or not any(dir_list)):
+    pattern = re.compile(base_filename + "_[0-9]{3}")
+    dir_list = [
+        x.path
+        for x in os.scandir(class_base_dir)
+        if x.is_dir() and re.search(pattern, x.path)
+    ]
+
+    # No dir yet with the patern -> return new one
+    if dir_list is None or not any(dir_list):
         return class_base_dir / f"{base_filename}_{1:03d}"
 
     # Get last run dir found... if we want to reuse it, return it
@@ -38,5 +41,5 @@ def create_run_dir(
 
     # We don't want to reuse it, so create next one
     _, last_dirname = os.path.split(last_dir)
-    last_iteration = int(last_dirname.replace(base_filename, 'tmp').split('_')[1])
+    last_iteration = int(last_dirname.replace(base_filename, "tmp").split("_")[1])
     return class_base_dir / f"{base_filename}_{last_iteration + 1:03d}"
diff --git a/cropclassification/helpers/geofile.py b/cropclassification/helpers/geofile.py
index 3c60c536..19ac3d64 100644
--- a/cropclassification/helpers/geofile.py
+++ b/cropclassification/helpers/geofile.py
@@ -12,83 +12,88 @@
 
 from cropclassification.helpers import pandas_helper as pdh
 
-def read_file(filepath: Path,
-              layer: str = '',
-              columns: Optional[List[str]] = None,
-              bbox = None) -> gpd.GeoDataFrame:
+
+def read_file(
+    path: Path, layer: str = "", columns: Optional[List[str]] = None, bbox=None
+) -> gpd.GeoDataFrame:
     """
-    Reads a file to a pandas dataframe. The fileformat is detected based on the filepath extension.
+    Reads a file to a pandas dataframe. The fileformat is detected based on the path
+    extension.
 
-    # TODO: think about if possible/how to support  adding optional parameter and pass them to next function, example encoding, float_format,...
+    # TODO: think about if possible/how to support  adding optional parameter and pass
+    # them to next function, example encoding, float_format,...
     """
-    _, ext = os.path.splitext(filepath)
+    _, ext = os.path.splitext(path)
     ext_lower = ext.lower()
-    
-    if layer == '':
-        layer = filepath.stem
-
-    if ext_lower == '.shp':
-        return gpd.read_file(str(filepath), bbox=bbox)
-    elif ext_lower == '.gpkg':
-        return gpd.read_file(str(filepath), layer=layer, bbox=bbox)
+
+    if layer == "":
+        layer = path.stem
+
+    if ext_lower == ".shp":
+        return gpd.read_file(str(path), bbox=bbox)
+    elif ext_lower == ".gpkg":
+        return gpd.read_file(str(path), layer=layer, bbox=bbox)
     else:
         raise Exception(f"Not implemented for extension {ext_lower}")
 
-def to_file(gdf: gpd.GeoDataFrame,
-            filepath: Path,
-            layer: str = '',
-            index: bool = True):
+
+def to_file(gdf: gpd.GeoDataFrame, path: Path, layer: str = "", index: bool = True):
     """
-    Reads a pandas dataframe to file. The fileformat is detected based on the filepath extension.
+    Reads a pandas dataframe to file. The fileformat is detected based on the path
+    extension.
 
-    # TODO: think about if possible/how to support adding optional parameter and pass them to next 
-    # function, example encoding, float_format,...
+    # TODO: think about if possible/how to support adding optional parameter and pass
+    # them to next function, example encoding, float_format,...
     """
-    _, ext = os.path.splitext(filepath)
+    _, ext = os.path.splitext(path)
     ext_lower = ext.lower()
 
-    if layer == '':
-        layer = filepath.stem
+    if layer == "":
+        layer = path.stem
 
-    if ext_lower == '.shp':
+    if ext_lower == ".shp":
         if index is True:
             gdf = gdf.reset_index(inplace=False)
-        gdf.to_file(str(filepath))
+        gdf.to_file(str(path))
 
-    elif ext_lower == '.gpkg':
-        gdf.to_file(str(filepath), layer=layer, driver="GPKG")
+    elif ext_lower == ".gpkg":
+        gdf.to_file(str(path), layer=layer, driver="GPKG")
     else:
         raise Exception(f"Not implemented for extension {ext_lower}")
-        
-def get_crs(filepath: Path):
-    with fiona.open(str(filepath), 'r') as geofile:
+
+
+def get_crs(path: Path):
+    with fiona.open(str(path), "r") as geofile:
         return geofile.crs
 
-def get_totalbounds(filepath: Path):
+
+def get_totalbounds(path: Path):
     """
     Gets the total bounds of a geofile. Return a tuple with the bounds and the crs.
 
     Remark: implementation is not at all efficient!!!
-    
+
     Args:
-        filepath (str): The filepath to the geofile
+        path (str): The path to the geofile
     """
-    gdf = gpd.read_file(str(filepath))
+    gdf = gpd.read_file(str(path))
     return (gdf.total_bounds, gdf.crs)
 
-def is_geofile(filepath: Path) -> bool:
+
+def is_geofile(path: Path) -> bool:
     """
-    Determines based on the filepath if this is a geofile.
+    Determines based on the path if this is a geofile.
     """
-    _, file_ext = os.path.splitext(filepath)
+    _, file_ext = os.path.splitext(path)
     return is_geofile_ext(file_ext)
 
+
 def is_geofile_ext(file_ext) -> bool:
     """
     Determines based on the file extension if this is a geofile.
     """
     file_ext_lower = file_ext.lower()
-    if file_ext_lower in ('.shp', '.gpkg', '.geojson'):
+    if file_ext_lower in (".shp", ".gpkg", ".geojson"):
         return True
     else:
         return False
diff --git a/cropclassification/helpers/log_helper.py b/cropclassification/helpers/log_helper.py
index 36f6bc91..3506e2b8 100644
--- a/cropclassification/helpers/log_helper.py
+++ b/cropclassification/helpers/log_helper.py
@@ -7,44 +7,46 @@
 import logging
 import os
 from pathlib import Path
-#-------------------------------------------------------------
-# Init logging
-#-------------------------------------------------------------
-
-def main_log_init(log_dir: Path,
-                  log_basefilename: str,
-                  log_level: str = "INFO"):
-    
+
+
+def main_log_init(log_dir: Path, log_basefilename: str, log_level: str = "INFO"):
+
     # Make sure the log dir exists
     if not log_dir.exists():
         os.makedirs(log_dir, exist_ok=True)
-        
+
     # Get root logger
-    logger = logging.getLogger('')
-    
+    logger = logging.getLogger("")
+
     # Set the general maximum log level...
     logger.setLevel(log_level)
     for handler in logger.handlers:
         handler.flush()
         handler.close()
-    
+
     # Remove all handlers and add the ones I want again, so a new log file is created for each run
     # Remark: the function removehandler doesn't seem to work?
     logger.handlers = []
-    
+
     ch = logging.StreamHandler()
     ch.setLevel(log_level)
-    #ch.setFormatter(logging.Formatter('%(levelname)s|%(name)s|%(message)s'))
-    #ch.setFormatter(logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|%(message)s',
+    # ch.setFormatter(logging.Formatter('%(levelname)s|%(name)s|%(message)s'))
+    # ch.setFormatter(logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|%(message)s',
     #                                  datefmt='%H:%M:%S,uuu'))
-    ch.setFormatter(logging.Formatter(fmt='%(asctime)s.%(msecs)03d|%(levelname)s|%(name)s|%(message)s',
-                                      datefmt='%H:%M:%S')) 
+    ch.setFormatter(
+        logging.Formatter(
+            fmt="%(asctime)s.%(msecs)03d|%(levelname)s|%(name)s|%(message)s",
+            datefmt="%H:%M:%S",
+        )
+    )
     logger.addHandler(ch)
-    
-    log_filepath = log_dir / f"{datetime.datetime.now():%Y-%m-%d_%H-%M-%S}_{log_basefilename}.log"
+
+    log_filepath = (
+        log_dir / f"{datetime.datetime.now():%Y-%m-%d_%H-%M-%S}_{log_basefilename}.log"
+    )
     fh = logging.FileHandler(filename=str(log_filepath))
     fh.setLevel(log_level)
-    fh.setFormatter(logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|%(message)s'))
+    fh.setFormatter(logging.Formatter("%(asctime)s|%(levelname)s|%(name)s|%(message)s"))
     logger.addHandler(fh)
-    
-    return logger
\ No newline at end of file
+
+    return logger
diff --git a/cropclassification/helpers/model_helper.py b/cropclassification/helpers/model_helper.py
index c7c086c2..55d16801 100644
--- a/cropclassification/helpers/model_helper.py
+++ b/cropclassification/helpers/model_helper.py
@@ -9,40 +9,42 @@
 from pathlib import Path
 from typing import Optional
 
-import pandas as pd 
-import keras as kr
+import pandas as pd
+from tensorflow import keras as kr
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
-#logger.setLevel(logging.INFO)
+# logger.setLevel(logging.INFO)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
+
 
 def get_max_data_version(model_dir: Path) -> int:
     """
     Get the maximum data version a model exists for in the model_dir.
-    
+
     Args
         model_dir: the dir to search models in
     """
     models_df = get_models(model_dir)
     if models_df is not None and len(models_df.index) > 0:
-        train_data_version_max = models_df['train_data_version'].max()
+        train_data_version_max = models_df["train_data_version"].max()
         return int(train_data_version_max)
     else:
         return -1
 
-def format_model_base_filename(segment_subject: str,
-                               train_data_version: int,
-                               model_architecture: str) -> str:
+
+def format_model_base_filename(
+    segment_subject: str, train_data_version: int, model_architecture: str
+) -> str:
     """
     Format the parameters into a model_base_filename.
-    
+
     Args
         segment_subject: the segment subject
         train_data_version: the version of the data used to train the model
@@ -51,16 +53,19 @@ def format_model_base_filename(segment_subject: str,
     retval = f"{segment_subject}_{train_data_version:02}_{model_architecture}"
     return retval
 
-def format_model_filename(segment_subject: str,
-                          train_data_version: int,
-                          model_architecture: str,
-                          acc_train: float,
-                          acc_val: float,
-                          acc_combined: float,
-                          epoch: int) -> str:
+
+def format_model_filename(
+    segment_subject: str,
+    train_data_version: int,
+    model_architecture: str,
+    acc_train: float,
+    acc_val: float,
+    acc_combined: float,
+    epoch: int,
+) -> str:
     """
     Format the parameters into a model_filename.
-    
+
     Args
         segment_subject: the segment subject
         train_data_version: the version of the data used to train the model
@@ -71,24 +76,30 @@ def format_model_filename(segment_subject: str,
         epoch: the epoch during training that reached these model weights
     """
     base_filename = format_model_base_filename(
-            segment_subject=segment_subject,
-            train_data_version=train_data_version,
-            model_architecture=model_architecture)
-    filename = format_model_filename2(model_base_filename=base_filename,
-                                      acc_train=acc_train,
-                                      acc_val=acc_val,
-                                      acc_combined=acc_combined,
-                                      epoch=epoch)
+        segment_subject=segment_subject,
+        train_data_version=train_data_version,
+        model_architecture=model_architecture,
+    )
+    filename = format_model_filename2(
+        model_base_filename=base_filename,
+        acc_train=acc_train,
+        acc_val=acc_val,
+        acc_combined=acc_combined,
+        epoch=epoch,
+    )
     return filename
 
-def format_model_filename2(model_base_filename: str,
-                           acc_train: float,
-                           acc_val: float,
-                           acc_combined: float,
-                           epoch: int) -> str:
+
+def format_model_filename2(
+    model_base_filename: str,
+    acc_train: float,
+    acc_val: float,
+    acc_combined: float,
+    epoch: int,
+) -> str:
     """
     Format the parameters into a model_filename.
-    
+
     Args
         model_base_filename: the base filename of the model
         acc_train: the accuracy reached for the training dataset
@@ -98,7 +109,8 @@ def format_model_filename2(model_base_filename: str,
     """
     return f"{model_base_filename}_{acc_combined:.5f}_{acc_train:.5f}_{acc_val:.5f}_{epoch}.hdf5"
 
-def parse_model_filename(filepath: Path) -> dict:
+
+def parse_model_filename(path: Path) -> dict:
     """
     Parse a model_filename to a dict containing the properties of the model:
         * segment_subject: the segment subject
@@ -107,19 +119,19 @@ def parse_model_filename(filepath: Path) -> dict:
         * acc_train: the accuracy reached for the training dataset
         * acc_val: the accuracy reached for the validation dataset
         * acc_combined: the average of the train and validation accuracy
-        * epoch: the epoch during training that reached these model weights        
-    
+        * epoch: the epoch during training that reached these model weights
+
     Args
-        filepath: the filepath to the model file
+        path: the path to the model file
     """
-    # Prepare filepath to extract info
-    param_values = filepath.stem.split("_")
+    # Prepare path to extract info
+    param_values = path.stem.split("_")
 
     # Now extract fields...
     segment_subject = param_values[0]
     train_data_version = int(param_values[1])
     model_architecture = param_values[2]
-    if(len(param_values) > 3):
+    if len(param_values) > 3:
         acc_combined = float(param_values[3])
         acc_train = float(param_values[4])
         acc_val = float(param_values[5])
@@ -129,99 +141,114 @@ def parse_model_filename(filepath: Path) -> dict:
         acc_train = 0.0
         acc_val = 0.0
         epoch = 0
-    
-    return {'filepath': filepath,
-            'filename': filepath.name,
-            'segment_subject': segment_subject,
-            'train_data_version': train_data_version,
-            'model_architecture': model_architecture,
-            'acc_combined': acc_combined,
-            'acc_train': acc_train,
-            'acc_val': acc_val,
-            'epoch': epoch}
-
-def get_models(model_dir: Path,
-               model_base_filename: str = None) -> pd.DataFrame:
+
+    return {
+        "path": path,
+        "filename": path.name,
+        "segment_subject": segment_subject,
+        "train_data_version": train_data_version,
+        "model_architecture": model_architecture,
+        "acc_combined": acc_combined,
+        "acc_train": acc_train,
+        "acc_val": acc_val,
+        "epoch": epoch,
+    }
+
+
+def get_models(
+    model_dir: Path, model_base_filename: Optional[str] = None
+) -> pd.DataFrame:
     """
-    Return the list of models in the model_dir passed. It is returned as a 
+    Return the list of models in the model_dir passed. It is returned as a
     dataframe with the columns as returned in parse_model_filename()
-    
+
     Args
         model_dir: dir containing the models
-        model_base_filename: optional, if passed, only the models with this 
+        model_base_filename: optional, if passed, only the models with this
             base filename will be returned
     """
 
-    # glob search string    
+    # glob search string
     if model_base_filename is not None:
-        model_weight_filepaths = glob.glob(f"{str(model_dir)}{os.sep}{model_base_filename}_*.hdf5")
+        model_weight_paths = glob.glob(
+            f"{str(model_dir)}{os.sep}{model_base_filename}_*.hdf5"
+        )
     else:
-        model_weight_filepaths = glob.glob(f"{str(model_dir)}{os.sep}*.hdf5")
+        model_weight_paths = glob.glob(f"{str(model_dir)}{os.sep}*.hdf5")
 
     # Loop through all models and extract necessary info...
     model_info_list = []
-    for filepath in model_weight_filepaths:
-        model_info_list.append(parse_model_filename(Path(filepath)))
-   
+    for path in model_weight_paths:
+        model_info_list.append(parse_model_filename(Path(path)))
+
     return pd.DataFrame.from_dict(model_info_list)
 
-def get_best_model(model_dir: Path,
-                   acc_metric_mode: str,
-                   model_base_filename: str = None) -> Optional[dict]:
+
+def get_best_model(
+    model_dir: Path, acc_metric_mode: str, model_base_filename: Optional[str] = None
+) -> Optional[dict]:
     """
-    Get the properties of the model with the highest combined accuracy for the highest 
+    Get the properties of the model with the highest combined accuracy for the highest
     traindata version in the dir.
-    
+
     Args
         model_dir: dir containing the models
-        acc_metric_mode: 
-        model_base_filename: optional, if passed, only the models with this 
+        acc_metric_mode:
+        model_base_filename: optional, if passed, only the models with this
             base filename will be taken in account
     """
     # Check validaty of input
-    acc_metric_mode_values = ['min', 'max']
-    if(acc_metric_mode not in acc_metric_mode_values):
-        raise Exception(f"Invalid value for mode: {acc_metric_mode}, should be one of {acc_metric_mode_values}")
+    acc_metric_mode_values = ["min", "max"]
+    if acc_metric_mode not in acc_metric_mode_values:
+        raise Exception(
+            f"Invalid value for mode: {acc_metric_mode}, should be one of {acc_metric_mode_values}"
+        )
 
     # Get list of existing models for this train dataset
-    model_info_df = get_models(model_dir=model_dir,
-                               model_base_filename=model_base_filename)
-    
+    model_info_df = get_models(
+        model_dir=model_dir, model_base_filename=model_base_filename
+    )
+
     # If no model_base_filename provided, take highest data version
     if model_base_filename is None:
         max_data_version = get_max_data_version(model_dir)
         if max_data_version == -1:
             return None
-        model_info_df = model_info_df.loc[model_info_df['train_data_version'] == max_data_version]
-        
+        model_info_df = model_info_df.loc[
+            model_info_df["train_data_version"] == max_data_version
+        ]
+
     if len(model_info_df) > 0:
-        if acc_metric_mode == 'max':
-            return model_info_df.loc[model_info_df['acc_combined'].values.argmax()]
+        if acc_metric_mode == "max":
+            return model_info_df.loc[model_info_df["acc_combined"].values.argmax()]
         else:
-            return model_info_df.loc[model_info_df['acc_combined'].values.argmin()]
-    else :
+            return model_info_df.loc[model_info_df["acc_combined"].values.argmin()]
+    else:
         return None
-    
-def save_and_clean_models(model_save_dir: Path,
-                          model_save_base_filename: str,
-                          acc_metric_mode: str,                   
-                          new_model=None,
-                          new_model_acc_train: float = None,
-                          new_model_acc_val: float = None,
-                          new_model_epoch: int = None,
-                          save_weights_only: bool = False,
-                          verbose: bool = True,
-                          debug: bool = False,
-                          only_report: bool = False):
+
+
+def save_and_clean_models(
+    model_save_dir: Path,
+    model_save_base_filename: str,
+    acc_metric_mode: str,
+    new_model=None,
+    new_model_acc_train: Optional[float] = None,
+    new_model_acc_val: Optional[float] = None,
+    new_model_epoch: Optional[int] = None,
+    save_weights_only: bool = False,
+    verbose: bool = True,
+    debug: bool = False,
+    only_report: bool = False,
+):
     """
-    Save the new model if it is good enough... en cleanup existing models 
+    Save the new model if it is good enough... en cleanup existing models
     if they are worse than the new or other existing models.
-    
+
     Args
         model_save_dir: dir containing the models
         model_save_base_filename: base filename that will be used
-        model_acc_metric_mode (str): use 'min' if the accuracy metrics should be 
-                as low as possible, 'max' if a higher values is better. 
+        model_acc_metric_mode (str): use 'min' if the accuracy metrics should be
+                as low as possible, 'max' if a higher values is better.
         new_model: optional, the keras model object that will be saved
         new_model_acc_train: optional: the accuracy on the train dataset
         new_model_acc_val: optional: the accuracy on the validation dataset
@@ -231,69 +258,87 @@ def save_and_clean_models(model_save_dir: Path,
         only_report: optional: only report which models would be cleaned up
     """
     # Check validaty of input
-    acc_metric_mode_values = ['min', 'max']
-    if(acc_metric_mode not in acc_metric_mode_values):
-        raise Exception(f"Invalid value for mode: {acc_metric_mode}, should be one of {acc_metric_mode_values}")
+    acc_metric_mode_values = ["min", "max"]
+    if acc_metric_mode not in acc_metric_mode_values:
+        raise Exception(
+            f"Invalid value for mode: {acc_metric_mode}, should be one of "
+            f"{acc_metric_mode_values}"
+        )
 
     # Get a list of all existing models
-    model_info_df = get_models(model_dir=model_save_dir,
-                               model_base_filename=model_save_base_filename)
+    model_info_df = get_models(
+        model_dir=model_save_dir, model_base_filename=model_save_base_filename
+    )
 
     # If there is a new model passed as param, add it to the list
-    new_model_filepath = None
-    if(new_model is not None and new_model_acc_train is not None 
-       and new_model_acc_val is not None and new_model_epoch is not None):
+    new_model_path = None
+    if (
+        new_model is not None
+        and new_model_acc_train is not None
+        and new_model_acc_val is not None
+        and new_model_epoch is not None
+    ):
         # Calculate combined accuracy
-        new_model_acc_combined = (new_model_acc_train+new_model_acc_val)/2
-        
-        # Build save filepath
+        new_model_acc_combined = (new_model_acc_train + new_model_acc_val) / 2
+
+        # Build save path
         new_model_filename = format_model_filename2(
-                model_base_filename=model_save_base_filename,
-                acc_combined=new_model_acc_combined,
-                acc_train=new_model_acc_train, acc_val=new_model_acc_val, 
-                epoch=new_model_epoch)
-        new_model_filepath = model_save_dir / new_model_filename
-        
+            model_base_filename=model_save_base_filename,
+            acc_combined=new_model_acc_combined,
+            acc_train=new_model_acc_train,
+            acc_val=new_model_acc_val,
+            epoch=new_model_epoch,
+        )
+        new_model_path = model_save_dir / new_model_filename
+
         # Append model to the retrieved models...
-        model_info_df = model_info_df.append({'filepath': new_model_filepath,
-                                              'filename': new_model_filename,
-                                              'acc_combined': new_model_acc_combined,
-                                              'acc_train': new_model_acc_train,
-                                              'acc_val': new_model_acc_val,
-                                              'epoch': new_model_epoch}, 
-                                             ignore_index=True)
-                            
-    # For each model found, check if there is one with ALL parameters 
+        model_info_df = model_info_df.append(
+            {
+                "path": new_model_path,
+                "filename": new_model_filename,
+                "acc_combined": new_model_acc_combined,
+                "acc_train": new_model_acc_train,
+                "acc_val": new_model_acc_val,
+                "epoch": new_model_epoch,
+            },
+            ignore_index=True,
+        )
+
+    # For each model found, check if there is one with ALL parameters
     # higher than itself. If one is found: delete model
     # Remark: the list is sorted before iterating it, this way the logging
     # is sorted from "worst to best"
-    if(acc_metric_mode == 'max'):
-        model_info_sorted_df = model_info_df.sort_values(by='acc_combined')
+    if acc_metric_mode == "max":
+        model_info_sorted_df = model_info_df.sort_values(by="acc_combined")
     else:
-        model_info_sorted_df = model_info_df.sort_values(by='acc_combined', ascending=False)
+        model_info_sorted_df = model_info_df.sort_values(
+            by="acc_combined", ascending=False
+        )
     for _, model_info in model_info_sorted_df.iterrows():
-        if(acc_metric_mode == 'max'):
+        if acc_metric_mode == "max":
             better_ones_df = model_info_df[
-                    (model_info_df['filepath'] != model_info['filepath']) 
-                    & (model_info_df['acc_combined'] >= model_info['acc_combined']) 
-                    & (model_info_df['acc_train'] >= model_info['acc_train'])
-                    & (model_info_df['acc_val'] >= model_info['acc_val'])] 
+                (model_info_df["path"] != model_info["path"])
+                & (model_info_df["acc_combined"] >= model_info["acc_combined"])
+                & (model_info_df["acc_train"] >= model_info["acc_train"])
+                & (model_info_df["acc_val"] >= model_info["acc_val"])
+            ]
         else:
             better_ones_df = model_info_df[
-                    (model_info_df['filepath'] != model_info['filepath']) 
-                    & (model_info_df['acc_combined'] <= model_info['acc_combined']) 
-                    & (model_info_df['acc_train'] <= model_info['acc_train'])
-                    & (model_info_df['acc_val'] <= model_info['acc_val'])] 
-        
+                (model_info_df["path"] != model_info["path"])
+                & (model_info_df["acc_combined"] <= model_info["acc_combined"])
+                & (model_info_df["acc_train"] <= model_info["acc_train"])
+                & (model_info_df["acc_val"] <= model_info["acc_val"])
+            ]
+
         # If one or more better ones are found, no use in keeping it...
         if len(better_ones_df) > 0:
-            if only_report is True:
+            if only_report:
                 logger.debug(f"DELETE {model_info['filename']}")
-            elif os.path.exists(model_info['filepath']):
+            elif model_info["path"].exists():
                 logger.debug(f"DELETE {model_info['filename']}")
-                os.remove(model_info['filepath'])
-                
-            if debug is True:
+                os.remove(model_info["path"])
+
+            if debug:
                 print(f"Better one(s) found for{model_info['filename']}:")
                 for _, better_one in better_ones_df.iterrows():
                     print(f"  {better_one['filename']}")
@@ -302,46 +347,61 @@ def save_and_clean_models(model_save_dir: Path,
             logger.debug(f"KEEP {model_info['filename']}")
 
             # If it is the new model that needs to be kept, save to disk
-            if(new_model_filepath is not None 
-               and only_report is not True
-               and model_info['filepath'] == new_model_filepath
-               and not os.path.exists(new_model_filepath)):
+            if (
+                new_model_path is not None
+                and new_model is not None
+                and only_report is not True
+                and model_info["path"] == new_model_path
+                and not new_model_path.exists()
+            ):
                 if save_weights_only:
-                    new_model.save_weights(new_model_filepath)
+                    new_model.save_weights(new_model_path)
                 else:
-                    new_model.save(new_model_filepath)
+                    new_model.save(new_model_path)
 
     if verbose is True or debug is True:
-        best_model = get_best_model(model_save_dir, acc_metric_mode, model_save_base_filename)
-        print(f"BEST MODEL: acc_combined: {best_model['acc_combined']}, acc_train: {best_model['acc_train']}, acc_val: {best_model['acc_val']}, epoch: {best_model['epoch']}")
-            
+        best_model = get_best_model(
+            model_save_dir, acc_metric_mode, model_save_base_filename
+        )
+        if best_model is not None:
+            print(
+                f"BEST MODEL: acc_combined: {best_model['acc_combined']}, acc_train: "
+                f"{best_model['acc_train']}, acc_val: {best_model['acc_val']}, epoch: "
+                f"{best_model['epoch']}"
+            )
+
+
 class ModelCheckpointExt(kr.callbacks.Callback):
-    
-    def __init__(self, 
-                 model_save_dir: Path, 
-                 model_save_base_filename: str,
-                 acc_metric_mode: str,
-                 acc_metric_train: str,
-                 acc_metric_validation: str,
-                 save_weights_only: bool = False,
-                 verbose: bool = True,
-                 only_report: bool = False):
+    def __init__(
+        self,
+        model_save_dir: Path,
+        model_save_base_filename: str,
+        acc_metric_mode: str,
+        acc_metric_train: str,
+        acc_metric_validation: str,
+        save_weights_only: bool = False,
+        verbose: bool = True,
+        only_report: bool = False,
+    ):
         """[summary]
-        
+
         Args:
             model_save_dir (Path): [description]
             model_save_base_filename (str): [description]
-            acc_metric_mode (str): use 'min' if the accuracy metrics should be 
-                    as low as possible, 'max' if a higher values is better. 
+            acc_metric_mode (str): use 'min' if the accuracy metrics should be
+                    as low as possible, 'max' if a higher values is better.
             acc_metric_train (str): [description]
             acc_metric_validation (str): [description]
             verbose (bool, optional): [description]. Defaults to True.
             only_report (bool, optional): [description]. Defaults to False.
         """
 
-        acc_metric_mode_values = ['min', 'max']
-        if(acc_metric_mode not in acc_metric_mode_values):
-            raise Exception(f"Invalid value for mode: {acc_metric_mode}, should be one of {acc_metric_mode_values}")
+        acc_metric_mode_values = ["min", "max"]
+        if acc_metric_mode not in acc_metric_mode_values:
+            raise Exception(
+                f"Invalid value for mode: {acc_metric_mode}, should be one of "
+                f"{acc_metric_mode_values}"
+            )
 
         self.model_save_dir = model_save_dir
         self.model_save_base_filename = model_save_base_filename
@@ -351,51 +411,57 @@ def __init__(self,
         self.save_weights_only = save_weights_only
         self.verbose = verbose
         self.only_report = only_report
-        
+
     def on_epoch_end(self, epoch, logs={}):
         logger.debug("Start in callback on_epoch_begin")
-        
+
         save_and_clean_models(
-                model_save_dir=self.model_save_dir,
-                model_save_base_filename=self.model_save_base_filename,
-                acc_metric_mode=self.acc_metric_mode,
-                new_model=self.model,
-                new_model_acc_train=logs.get(self.acc_metric_train),
-                new_model_acc_val=logs.get(self.acc_metric_validation),
-                new_model_epoch=epoch,
-                save_weights_only=self.save_weights_only,
-                verbose=self.verbose,
-                only_report=self.only_report)
-        
-if __name__ == '__main__':
-    
-    #raise Exception("Not implemented")
+            model_save_dir=self.model_save_dir,
+            model_save_base_filename=self.model_save_base_filename,
+            acc_metric_mode=self.acc_metric_mode,
+            new_model=self.model,
+            new_model_acc_train=logs.get(self.acc_metric_train),
+            new_model_acc_val=logs.get(self.acc_metric_validation),
+            new_model_epoch=epoch,
+            save_weights_only=self.save_weights_only,
+            verbose=self.verbose,
+            only_report=self.only_report,
+        )
+
+
+if __name__ == "__main__":
+
+    # raise Exception("Not implemented")
 
     # General inits
-    segment_subject = 'greenhouses'
-    base_dir = Path("X:/PerPersoon/PIEROG/Taken/2018/2018-08-12_AutoSegmentation")      
+    segment_subject = "greenhouses"
+    base_dir = Path("X:/PerPersoon/PIEROG/Taken/2018/2018-08-12_AutoSegmentation")
     traindata_version = 17
     model_architecture = "inceptionresnetv2+linknet"
-    
+
     project_dir = base_dir / segment_subject
-    
+
     # Init logging
     from . import log_helper
+
     log_dir = project_dir / "log"
     logger = log_helper.main_log_init(log_dir, __name__)
 
-    '''
+    """
     print(get_models(model_dir="",
                      model_basename=""))
-    '''
+    """
     # Test the clean_models function (without new model)
-    # Build save dir and model base filename 
+    # Build save dir and model base filename
     model_save_dir = project_dir / "models"
     model_save_base_filename = format_model_base_filename(
-            segment_subject, traindata_version, model_architecture)
-    
+        segment_subject, traindata_version, model_architecture
+    )
+
     # Clean the models (only report)
-    save_and_clean_models(model_save_dir=model_save_dir,
-                          model_save_base_filename=model_save_base_filename,
-                          acc_metric_mode='max',
-                          only_report=True)
+    save_and_clean_models(
+        model_save_dir=model_save_dir,
+        model_save_base_filename=model_save_base_filename,
+        acc_metric_mode="max",
+        only_report=True,
+    )
diff --git a/cropclassification/helpers/pandas_helper.py b/cropclassification/helpers/pandas_helper.py
index 009d8f24..ddd978d1 100644
--- a/cropclassification/helpers/pandas_helper.py
+++ b/cropclassification/helpers/pandas_helper.py
@@ -5,51 +5,65 @@
 
 from pathlib import Path
 import os
-from typing import List
+from typing import List, Optional
 
 import pandas as pd
 import sqlite3
 
+
 def read_file(
-        filepath: Path,
-        table_name: str = 'info',
-        columns: List[str] = None) -> pd.DataFrame:
+    path: Path, table_name: str = "info", columns: Optional[List[str]] = None
+) -> pd.DataFrame:
     """
-    Reads a file to a pandas dataframe. The fileformat is detected based on the filepath extension.
+    Reads a file to a pandas dataframe. The fileformat is detected based on the
+    path extension.
 
-    # TODO: think about if possible/how to support  adding optional parameter and pass them to next function, example encoding, float_format,...
+    # TODO: think about if possible/how to support  adding optional parameter and pass
+    # them to next function, example encoding, float_format,...
     """
     if columns is not None and type(columns) is not list:
         raise Exception(f"Parameter columns should be list, but is {type(columns)}")
 
-    ext_lower = filepath.suffix.lower()
-    if ext_lower == '.csv':
+    ext_lower = path.suffix.lower()
+    if ext_lower == ".csv":
         try:
-            data_read_df = pd.read_csv(str(filepath), usecols=columns, low_memory=False)
+            data_read_df = pd.read_csv(str(path), usecols=columns, low_memory=False)
         except UnicodeDecodeError:
             # If a unicode decode error is thrown, try again using ANSI encoding
-            data_read_df = pd.read_csv(str(filepath), usecols=columns, low_memory=False, encoding='ANSI')
+            data_read_df = pd.read_csv(
+                str(path), usecols=columns, low_memory=False, encoding="ANSI"
+            )
         return data_read_df
-    elif ext_lower == '.tsv':
+    elif ext_lower == ".tsv":
         try:
-            data_read_df = pd.read_csv(str(filepath), usecols=columns, sep='\t', low_memory=False)
+            data_read_df = pd.read_csv(
+                str(path), usecols=columns, sep="\t", low_memory=False
+            )
         except UnicodeDecodeError:
             # If a unicode decode error is thrown, try again using ANSI encoding
-            data_read_df = pd.read_csv(str(filepath), usecols=columns, sep='\t', low_memory=False, encoding='ANSI')
+            data_read_df = pd.read_csv(
+                str(path),
+                usecols=columns,
+                sep="\t",
+                low_memory=False,
+                encoding="ANSI",
+            )
         return data_read_df
-    elif ext_lower == '.parquet':
-        return pd.read_parquet(str(filepath), columns=columns)
-    elif ext_lower in ('.sqlite', '.gpkg'):
+    elif ext_lower == ".parquet":
+        return pd.read_parquet(str(path), columns=columns)
+    elif ext_lower in (".sqlite", ".gpkg"):
         sql_db = None
         try:
-            sql_db = sqlite3.connect(str(filepath))
+            sql_db = sqlite3.connect(str(path))
             if columns is None:
-                cols_to_select = '*'
+                cols_to_select = "*"
             else:
-                cols_to_select = ', '.join(columns)
-            data_read_df = pd.read_sql_query(f'select {cols_to_select} from "{table_name}"', sql_db)
+                cols_to_select = ", ".join(columns)
+            data_read_df = pd.read_sql_query(
+                f'select {cols_to_select} from "{table_name}"', sql_db
+            )
         except Exception as ex:
-            raise Exception(f"Error reading data from {str(filepath)}") from ex
+            raise Exception(f"Error reading data from {str(path)}") from ex
         finally:
             if sql_db is not None:
                 sql_db.close()
@@ -57,42 +71,54 @@ def read_file(
     else:
         raise Exception(f"Not implemented for extension {ext_lower}")
 
-def to_file(df: pd.DataFrame,
-            filepath: Path,
-            table_name: str = 'info',
-            index: bool = True,
-            append: bool = False):
+
+def to_file(
+    df: pd.DataFrame,
+    path: Path,
+    table_name: str = "info",
+    index: bool = True,
+    append: bool = False,
+):
     """
-    Reads a pandas dataframe to file. The fileformat is detected based on the filepath extension.
+    Reads a pandas dataframe to file. The file format is detected based on the path
+    extension.
 
-    # TODO: think about if possible/how to support  adding optional parameter and pass them to next function, example encoding, float_format,...
+    # TODO: think about if possible/how to support  adding optional parameter and pass
+    # them to next function, example encoding, float_format,...
     """
-    ext_lower = filepath.suffix.lower()
-    if ext_lower == '.csv':
+    ext_lower = path.suffix.lower()
+    if ext_lower == ".csv":
         if append:
             raise Exception("Append is not supported for csv files")
-        df.to_csv(str(filepath), float_format='%.10f', encoding='utf-8', index=index)
-    elif ext_lower == '.tsv':
+        df.to_csv(str(path), float_format="%.10f", encoding="utf-8", index=index)
+    elif ext_lower == ".tsv":
         if append:
             raise Exception("Append is not supported for tsv files")
-        df.to_csv(str(filepath), sep='\t', float_format='%.10f', encoding='utf-8', index=index)
-    elif ext_lower == '.parquet':
+        df.to_csv(
+            str(path), sep="\t", float_format="%.10f", encoding="utf-8", index=index
+        )
+    elif ext_lower == ".parquet":
         if append:
             raise Exception("Append is not supported for parquet files")
-        df.to_parquet(str(filepath), index=index)
-    elif ext_lower == '.sqlite':
-        if_exists = 'fail'
+        df.to_parquet(str(path), index=index)
+    elif ext_lower == ".sqlite":
+        if_exists = "fail"
         if append:
-            if_exists = 'append'
-        elif os.path.exists(filepath):
-            os.remove(filepath)
+            if_exists = "append"
+        elif os.path.exists(path):
+            os.remove(path)
         sql_db = None
-        try:    
-            sql_db = sqlite3.connect(str(filepath))
-            df.to_sql(name=table_name, con=sql_db, if_exists=if_exists, 
-                      index=index, chunksize=50000)
+        try:
+            sql_db = sqlite3.connect(str(path))
+            df.to_sql(
+                name=table_name,
+                con=sql_db,
+                if_exists=if_exists,
+                index=index,
+                chunksize=50000,
+            )
         except Exception as ex:
-            raise Exception(f"Error in to_file to file {str(filepath)}") from ex
+            raise Exception(f"Error in to_file to file {str(path)}") from ex
         finally:
             if sql_db is not None:
                 sql_db.close()
diff --git a/cropclassification/postprocess/classification_postprocess.py b/cropclassification/postprocess/classification_postprocess.py
index 3902b70a..8d9a2716 100644
--- a/cropclassification/postprocess/classification_postprocess.py
+++ b/cropclassification/postprocess/classification_postprocess.py
@@ -16,148 +16,173 @@
 import cropclassification.helpers.config_helper as conf
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
-#logger.setLevel(logging.DEBUG)
+# logger.setLevel(logging.DEBUG)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
-
-def calc_top3_and_consolidation(input_parcel_filepath: Path,
-                                input_parcel_probabilities_filepath: Path,
-                                input_parcel_geofilepath: Path,
-                                output_predictions_filepath: Path,
-                                output_predictions_geofilepath: Path,
-                                output_predictions_output_filepath: Optional[Path] = None,
-                                force: bool = False):
+# -------------------------------------------------------------
+
+
+def calc_top3_and_consolidation(
+    input_parcel_path: Path,
+    input_parcel_probabilities_path: Path,
+    input_parcel_geopath: Path,
+    output_predictions_path: Path,
+    output_predictions_geopath: Path,
+    output_predictions_output_path: Optional[Path] = None,
+    force: bool = False,
+):
     """
     Calculate the top3 prediction and a consolidation prediction.
 
-    Remark: in this logic the declared crop/class (class_declared) is used, as we want to compare 
-    with the declaration of the farmer, rather than taking into account corrections already.
-    
+    Remark: in this logic the declared crop/class (class_declared) is used, as we want
+    to compare with the declaration of the farmer, rather than taking into account
+    corrections already.
+
     Args:
-        input_parcel_filepath (Path): [description]
-        input_parcel_probabilities_filepath (Path): [description]
-        output_predictions_filepath (Path): [description]
-        output_predictions_geofilepath (Path): [description]
-        output_predictions_output_filepath (Path, optional): [description]. Defaults to None.
+        input_parcel_path (Path): [description]
+        input_parcel_probabilities_path (Path): [description]
+        output_predictions_path (Path): [description]
+        output_predictions_geopath (Path): [description]
+        output_predictions_output_path (Path, optional): [description].
+            Defaults to None.
         force (bool, optional): [description]. Defaults to False.
     """
-    
+
     # If force is false and output exists, already, return
-    if(force is False
-       and output_predictions_filepath.exists()):
-        logger.warning(f"calc_top3_and_consolidation: output file exist and force is False, so stop: {output_predictions_filepath}")
+    if force is False and output_predictions_path.exists():
+        logger.warning(
+            "calc_top3_and_consolidation: output file exist and force is False, so "
+            f"stop: {output_predictions_path}"
+        )
         return
 
     # Read input files
     logger.info("Read input file")
-    proba_df = pdh.read_file(input_parcel_probabilities_filepath)
+    proba_df = pdh.read_file(input_parcel_probabilities_path)
 
     top3_df = calc_top3(proba_df)
 
     # Read input files
     logger.info("Read input file")
-    input_parcel_df = pdh.read_file(input_parcel_filepath)
+    input_parcel_df = pdh.read_file(input_parcel_path)
 
     # All input parcels must stay in the output, so left join input with pred
-    top3_df.set_index(conf.columns['id'], inplace=True)
-    if input_parcel_df.index.name != conf.columns['id']:
-        input_parcel_df.set_index(conf.columns['id'], inplace=True)
+    top3_df.set_index(conf.columns["id"], inplace=True)
+    if input_parcel_df.index.name != conf.columns["id"]:
+        input_parcel_df.set_index(conf.columns["id"], inplace=True)
     cols_to_join = top3_df.columns.difference(input_parcel_df.columns)
-    pred_df = input_parcel_df.join(top3_df[cols_to_join], how='left')
+    pred_df = input_parcel_df.join(top3_df[cols_to_join], how="left")
 
     # The parcels added by the join don't have a prediction yet, so apply it
     # For the ignore classes, set the prediction to the ignore type
-    classes_to_ignore = conf.marker.getlist('classes_to_ignore')
-    pred_df.loc[pred_df[conf.columns['class_declared']].isin(classes_to_ignore), 
-                'pred1'] = pred_df[conf.columns['class_declared']]
-    # For all other parcels without prediction there must have been no data 
+    classes_to_ignore = conf.marker.getlist("classes_to_ignore")
+    pred_df.loc[
+        pred_df[conf.columns["class_declared"]].isin(classes_to_ignore), "pred1"
+    ] = pred_df[conf.columns["class_declared"]]
+    # For all other parcels without prediction there must have been no data
     # available for a classification, so set prediction to NODATA
-    pred_df['pred1'].fillna('NODATA', inplace=True)
-     
+    pred_df["pred1"].fillna("NODATA", inplace=True)
+
     # Add doubt columns
-    add_doubt_column(pred_df=pred_df, 
-                     new_pred_column=conf.columns['prediction_cons'],
-                     apply_doubt_pct_proba=True,
-                     apply_doubt_min_nb_pixels=True,
-                     apply_doubt_marker_specific=False)
-    add_doubt_column(pred_df=pred_df, 
-                     new_pred_column=conf.columns['prediction_full_alpha'],
-                     apply_doubt_pct_proba=True,                     
-                     apply_doubt_min_nb_pixels=True,
-                     apply_doubt_marker_specific=True)
+    add_doubt_column(
+        pred_df=pred_df,
+        new_pred_column=conf.columns["prediction_cons"],
+        apply_doubt_pct_proba=True,
+        apply_doubt_min_nb_pixels=True,
+        apply_doubt_marker_specific=False,
+    )
+    add_doubt_column(
+        pred_df=pred_df,
+        new_pred_column=conf.columns["prediction_full_alpha"],
+        apply_doubt_pct_proba=True,
+        apply_doubt_min_nb_pixels=True,
+        apply_doubt_marker_specific=True,
+    )
 
     # Calculate the status of the consolidated prediction (OK=usable, NOK=not)
-    pred_df.loc[pred_df[conf.columns['prediction_full_alpha']].isin(proba_df.columns), 
-                conf.columns['prediction_cons_status']] = 'OK'
-    pred_df[conf.columns['prediction_cons_status']].fillna('NOK', inplace=True)    
+    pred_df.loc[
+        pred_df[conf.columns["prediction_full_alpha"]].isin(proba_df.columns),
+        conf.columns["prediction_cons_status"],
+    ] = "OK"
+    pred_df[conf.columns["prediction_cons_status"]].fillna("NOK", inplace=True)
 
     logger.info("Write full prediction data to file")
-    pdh.to_file(pred_df, output_predictions_filepath)  
+    pdh.to_file(pred_df, output_predictions_path)
 
     # Output to geo file
-    input_parcel_gdf = gfo.read_file(input_parcel_geofilepath)
-    pred_gdf = gpd.GeoDataFrame(input_parcel_gdf.merge(pred_df, how='inner'))
-    gfo.to_file(pred_gdf, output_predictions_geofilepath)
+    input_parcel_gdf = gfo.read_file(input_parcel_geopath)
+    pred_gdf = gpd.GeoDataFrame(input_parcel_gdf.merge(pred_df, how="inner"))
+    gfo.to_file(pred_gdf, output_predictions_geopath)
 
     # Create final output file with the most important info
-    if output_predictions_output_filepath is not None:
+    if output_predictions_output_path is not None:
 
         # First add some aditional columns specific for the export
-        pred_df['markercode'] = conf.marker['markertype']
-        pred_df['run_id'] = conf.general['run_id']
+        pred_df["markercode"] = conf.marker["markertype"]
+        pred_df["run_id"] = conf.general["run_id"]
         today = datetime.date.today()
-        pred_df['cons_date'] = today
-        pred_df['modify_date'] = today
+        pred_df["cons_date"] = today
+        pred_df["modify_date"] = today
         logger.info("Write final output prediction data to file")
         pred_df.reset_index(inplace=True)
-        pred_df = pred_df[conf.columns.getlist('output_columns')] 
-        pdh.to_file(pred_df, output_predictions_output_filepath, index=False) 
+        pred_df = pred_df[conf.columns.getlist("output_columns")]
+        pdh.to_file(pred_df, output_predictions_output_path, index=False)
 
         # Write oracle sqlldr file
         table_name = None
         table_columns = None
-        if conf.marker['markertype'] in ['LANDCOVER', 'LANDCOVER-EARLY']:
-            table_name = 'mon_marker_landcover'
-            table_columns = ("layer_id, prc_id, versienummer, markercode, run_id, cons_landcover, "
-                             + "cons_status, cons_date date 'yyyy-mm-dd', landcover1, probability1, "
-                             + "landcover2, probability2, landcover3, probability3, "
-                             + "modify_date date 'yyyy-mm-dd'")
-        elif conf.marker['markertype'] in ['CROPGROUP', 'CROPGROUP-EARLY']:
-            table_name = 'mon_marker_cropgroup'
-            table_columns = ("layer_id, prc_id, versienummer, markercode, run_id, cons_cropgroup, "
-                             + "cons_status, cons_date date 'yyyy-mm-dd', cropgroup1, probability1, "
-                             + "cropgroup2, probability2, cropgroup3, probability3, "
-                             + "modify_date date 'yyyy-mm-dd'")
-        else: 
+        if conf.marker["markertype"] in ["LANDCOVER", "LANDCOVER-EARLY"]:
+            table_name = "mon_marker_landcover"
+            table_columns = (
+                "layer_id, prc_id, versienummer, markercode, run_id, cons_landcover, "
+                + "cons_status, cons_date date 'yyyy-mm-dd', landcover1, probability1, "
+                + "landcover2, probability2, landcover3, probability3, "
+                + "modify_date date 'yyyy-mm-dd'"
+            )
+        elif conf.marker["markertype"] in ["CROPGROUP", "CROPGROUP-EARLY"]:
+            table_name = "mon_marker_cropgroup"
+            table_columns = (
+                "layer_id, prc_id, versienummer, markercode, run_id, cons_cropgroup, "
+                + "cons_status, cons_date date 'yyyy-mm-dd', cropgroup1, probability1, "
+                + "cropgroup2, probability2, cropgroup3, probability3, "
+                + "modify_date date 'yyyy-mm-dd'"
+            )
+        else:
             table_name = None
-            logger.warning(f"Table unknown for marker type {conf.marker['markertype']}, so cannot write .ctl file")
+            logger.warning(
+                f"Table unknown for marker type {conf.marker['markertype']}, so cannot write .ctl file"
+            )
 
         if table_name is not None and table_columns is not None:
-            with open(str(output_predictions_output_filepath) + '.ctl', 'w') as ctlfile:
-                # SKIP=1 to skip the columns names line, the other ones to evade 
+            with open(str(output_predictions_output_path) + ".ctl", "w") as ctlfile:
+                # SKIP=1 to skip the columns names line, the other ones to evade
                 # more commits than needed
-                ctlfile.write("OPTIONS (SKIP=1, ROWS=10000, BINDSIZE=40000000, READSIZE=40000000)\n")     
+                ctlfile.write(
+                    "OPTIONS (SKIP=1, ROWS=10000, BINDSIZE=40000000, READSIZE=40000000)\n"
+                )
                 ctlfile.write("LOAD DATA\n")
-                ctlfile.write(f"INFILE '{output_predictions_output_filepath.name}'  \"str '\\n'\"\n")
+                ctlfile.write(
+                    f"INFILE '{output_predictions_output_path.name}'  \"str '\\n'\"\n"
+                )
                 ctlfile.write(f"INSERT INTO TABLE {table_name} APPEND\n")
-                # A tab as seperator is apparently X'9'  
+                # A tab as seperator is apparently X'9'
                 ctlfile.write("FIELDS TERMINATED BY X'9'\n")
                 ctlfile.write(f"({table_columns})\n")
 
+
 def calc_top3(proba_df: pd.DataFrame) -> pd.DataFrame:
 
     # Calculate the top 3 predictions
     logger.info("Calculate top3")
     proba_tmp_df = proba_df.copy()
     for column in proba_tmp_df.columns:
-        if column in conf.preprocess.getlist('dedicated_data_columns'):
+        if column in conf.preprocess.getlist("dedicated_data_columns"):
             proba_tmp_df.drop(column, axis=1, inplace=True)
 
     # Get the top 3 predictions for each row
@@ -175,215 +200,343 @@ def calc_top3(proba_df: pd.DataFrame) -> pd.DataFrame:
     top3_pred = np.concatenate([top3_pred_classes, top3_pred_values], axis=1)
     # Concatenate the ids, the classes and the top3 predictions
     id_class_top3 = np.concatenate(
-            [proba_df[[conf.columns['id'], conf.columns['class_declared']]].values, top3_pred], axis=1)
+        [
+            proba_df[[conf.columns["id"], conf.columns["class_declared"]]].values,
+            top3_pred,
+        ],
+        axis=1,
+    )
 
     # Convert to dataframe
-    top3_df = pd.DataFrame(id_class_top3,
-                           columns=[conf.columns['id'], conf.columns['class_declared'],
-                                    'pred1', 'pred2', 'pred3',
-                                    'pred1_prob', 'pred2_prob', 'pred3_prob'])
+    top3_df = pd.DataFrame(
+        id_class_top3,
+        columns=[
+            conf.columns["id"],
+            conf.columns["class_declared"],
+            "pred1",
+            "pred2",
+            "pred3",
+            "pred1_prob",
+            "pred2_prob",
+            "pred3_prob",
+        ],
+    )
 
     return top3_df
 
-def add_doubt_column(pred_df: pd.DataFrame,
-                     new_pred_column: str,
-                     apply_doubt_pct_proba: bool,
-                     apply_doubt_min_nb_pixels: bool,
-                     apply_doubt_marker_specific: bool):
+
+def add_doubt_column(
+    pred_df: pd.DataFrame,
+    new_pred_column: str,
+    apply_doubt_pct_proba: bool,
+    apply_doubt_min_nb_pixels: bool,
+    apply_doubt_marker_specific: bool,
+):
 
     # Calculate predictions with doubt column
-    classes_to_ignore = conf.marker.getlist('classes_to_ignore')
+    classes_to_ignore = conf.marker.getlist("classes_to_ignore")
 
-    # Init with the standard prediction 
-    pred_df[new_pred_column] = 'UNDEFINED'
+    # Init with the standard prediction
+    pred_df[new_pred_column] = "UNDEFINED"
 
     # If NODATA OR ignore class, retained those from pred1
-    pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED')
-                    & ((pred_df['pred1'] == 'NODATA') | (pred_df[conf.columns['class_declared']].isin(classes_to_ignore))),
-                new_pred_column] = pred_df['pred1']
+    pred_df.loc[
+        (pred_df[new_pred_column] == "UNDEFINED")
+        & (
+            (pred_df["pred1"] == "NODATA")
+            | (pred_df[conf.columns["class_declared"]].isin(classes_to_ignore))
+        ),
+        new_pred_column,
+    ] = pred_df["pred1"]
 
     # Doubt based on percentage probability
     if apply_doubt_pct_proba:
         # Apply doubt for parcels with a low percentage of probability -> = doubt!
-        doubt_proba1_st_2_x_proba2 = conf.postprocess.getboolean('doubt_proba1_st_2_x_proba2')
+        doubt_proba1_st_2_x_proba2 = conf.postprocess.getboolean(
+            "doubt_proba1_st_2_x_proba2"
+        )
         if doubt_proba1_st_2_x_proba2 is True:
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED')
-                            & (pred_df['pred1_prob'].map(float) < 2.0 * pred_df['pred2_prob'].map(float)),
-                        new_pred_column] = 'DOUBT:PROBA1<2*PROBA2'
-        
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (
+                    pred_df["pred1_prob"].map(float)
+                    < 2.0 * pred_df["pred2_prob"].map(float)
+                ),
+                new_pred_column,
+            ] = "DOUBT:PROBA1<2*PROBA2"
+
         # Apply doubt for parcels with prediction != unverified input
-        doubt_pred_ne_input_proba1_st_pct = conf.postprocess.getfloat('doubt_pred_ne_input_proba1_st_pct')
+        doubt_pred_ne_input_proba1_st_pct = conf.postprocess.getfloat(
+            "doubt_pred_ne_input_proba1_st_pct"
+        )
         if doubt_pred_ne_input_proba1_st_pct > 0:
             if doubt_pred_ne_input_proba1_st_pct > 100:
-                raise Exception(f"doubt_pred_ne_input_proba1_st_pct should be float from 0 till 100, not {doubt_pred_ne_input_proba1_st_pct}")
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df['pred1'] != pred_df[conf.columns['class_declared']])
-                            & (pred_df['pred1_prob'].map(float) < (doubt_pred_ne_input_proba1_st_pct/100)),
-                        new_pred_column] = 'DOUBT:PRED<>INPUT-PROBA1<X'
+                raise Exception(
+                    f"doubt_pred_ne_input_proba1_st_pct should be float from 0 till 100, not {doubt_pred_ne_input_proba1_st_pct}"
+                )
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df["pred1"] != pred_df[conf.columns["class_declared"]])
+                & (
+                    pred_df["pred1_prob"].map(float)
+                    < (doubt_pred_ne_input_proba1_st_pct / 100)
+                ),
+                new_pred_column,
+            ] = "DOUBT:PRED<>INPUT-PROBA1<X"
 
         # Apply doubt for parcels with prediction == unverified input
-        doubt_pred_eq_input_proba1_st_pct = conf.postprocess.getfloat('doubt_pred_eq_input_proba1_st_pct')
+        doubt_pred_eq_input_proba1_st_pct = conf.postprocess.getfloat(
+            "doubt_pred_eq_input_proba1_st_pct"
+        )
         if doubt_pred_eq_input_proba1_st_pct > 0:
             if doubt_pred_eq_input_proba1_st_pct > 100:
-                raise Exception(f"doubt_pred_ne_input_proba1_st_pct should be float from 0 till 100, not {doubt_pred_eq_input_proba1_st_pct}")
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED')
-                            & (pred_df['pred1'] == pred_df[conf.columns['class_declared']])
-                            & (pred_df['pred1_prob'].map(float) < (doubt_pred_eq_input_proba1_st_pct/100)),
-                        new_pred_column] = 'DOUBT:PRED=INPUT-PROBA1<X'
+                raise Exception(
+                    f"doubt_pred_ne_input_proba1_st_pct should be float from 0 till 100, not {doubt_pred_eq_input_proba1_st_pct}"
+                )
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df["pred1"] == pred_df[conf.columns["class_declared"]])
+                & (
+                    pred_df["pred1_prob"].map(float)
+                    < (doubt_pred_eq_input_proba1_st_pct / 100)
+                ),
+                new_pred_column,
+            ] = "DOUBT:PRED=INPUT-PROBA1<X"
 
     # Marker specific doubt
     if apply_doubt_marker_specific is True:
         # Apply some extra, marker-specific doubt algorythms
-        if conf.marker['markertype'] in ('LANDCOVER', 'LANDCOVER-EARLY'):
+        if conf.marker["markertype"] in ("LANDCOVER", "LANDCOVER-EARLY"):
             logger.info("Apply some marker-specific doubt algorythms")
 
             # Remarks:
-            #   - To be sure apply RISKY_DOUBT first, so if there would be 
+            #   - To be sure apply RISKY_DOUBT first, so if there would be
             #     overlapping criteria, RISKY_DOUBT "wins".
-            #   - Don't overwrite existing DOUBT on parcels, because the 
-            #     general doubt reasons should win compared to the marker 
+            #   - Don't overwrite existing DOUBT on parcels, because the
+            #     general doubt reasons should win compared to the marker
             #     specific doubt/risky_doubt reasons.
 
             # If less then 12 pixels, then set to doubt
-            pred_df.loc[(pred_df[conf.columns['pixcount_s1s2']] < 12),
-                        new_pred_column] = 'DOUBT:LITTLE-PARCELS'
+            pred_df.loc[
+                (pred_df[conf.columns["pixcount_s1s2"]] < 12), new_pred_column
+            ] = "DOUBT:LITTLE-PARCELS"
 
             # If parcel was declared as grassland, and is classified as arable, set to risky doubt
             # Remark: those gave 50% false positives for LANDCOVER marker
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['class_declared']] == 'MON_LC_GRASSES')
-                            & (pred_df['pred1'] == 'MON_LC_ARABLE'),
-                        new_pred_column] = 'RISKY_DOUBT:GRASS-SEEN-AS-ARABLE'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["class_declared"]] == "MON_LC_GRASSES")
+                & (pred_df["pred1"] == "MON_LC_ARABLE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:GRASS-SEEN-AS-ARABLE"
 
             # If parcel was declared as fallow, and is classified as something else, set to doubt
             # Remark: those gave 50% false positives for marker LANDCOVER
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['class_declared']] == 'MON_LC_FALLOW')
-                            & (pred_df['pred1'] != 'MON_LC_FALLOW'),
-                        new_pred_column] = 'RISKY_DOUBT:FALLOW-UNCONFIRMED'
-
-            # If parcel was declared as "9603: Boomkweek-sierplanten", but is not 
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["class_declared"]] == "MON_LC_FALLOW")
+                & (pred_df["pred1"] != "MON_LC_FALLOW"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:FALLOW-UNCONFIRMED"
+
+            # If parcel was declared as "9603: Boomkweek-sierplanten", but is not
             # classified as such: doubt
             # Remark: they gave 50% false positives for marker LANDCOVER
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['9603']))
-                            & (pred_df['pred1'] == 'MON_LC_GRASSES'),
-                        new_pred_column] = 'RISKY_DOUBT:BOOMSIER-SEEN-AS-GRASSES'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["crop_declared"]].isin(["9603"]))
+                & (pred_df["pred1"] == "MON_LC_GRASSES"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:BOOMSIER-SEEN-AS-GRASSES"
 
             # If parcel was declared as grain, but is not classified as MON_LC_ARABLE: doubt
             # Remark: - those gave > 50% false positives for marker LANDCOVER-EARLY
             #         - gave > 33 % false positives for marker LANDCOVER
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['39', '311', '321', '322', '331', '342', '639', '646']))
-                            & (pred_df['pred1'] != 'MON_LC_ARABLE'),
-                        new_pred_column] = 'RISKY_DOUBT:GRAIN-UNCONFIRMED'
-
-            # If parcel was declared as one of the following fabaceae, but is not 
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (
+                    pred_df[conf.columns["crop_declared"]].isin(
+                        ["39", "311", "321", "322", "331", "342", "639", "646"]
+                    )
+                )
+                & (pred_df["pred1"] != "MON_LC_ARABLE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:GRAIN-UNCONFIRMED"
+
+            # If parcel was declared as one of the following fabaceae, but is not
             # classified as such: doubt
             # Remark: - those gave > 50% false positives for marker LANDCOVER-EARLY
             #         - gave 33-100% false positives for marker LANDCOVER
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['43', '51', '52', '721', '722', '731', '732', '831', '931', '8410']))
-                            & (pred_df['pred1'] != 'MON_LC_FABACEAE'),
-                        new_pred_column] = 'RISKY_DOUBT:FABACEAE-UNCONFIRMED'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (
+                    pred_df[conf.columns["crop_declared"]].isin(
+                        [
+                            "43",
+                            "51",
+                            "52",
+                            "721",
+                            "722",
+                            "731",
+                            "732",
+                            "831",
+                            "931",
+                            "8410",
+                        ]
+                    )
+                )
+                & (pred_df["pred1"] != "MON_LC_FABACEAE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:FABACEAE-UNCONFIRMED"
 
             # Declared class was not correct, but groundtruth class is permanent
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED')
-                            & (pred_df[conf.columns['class_declared']] != pred_df['pred1'])
-                            & (pred_df['pred1'].isin(['MON_LC_BOS', 'MON_LC_HEIDE'])),
-                        new_pred_column] = 'RISKY_DOUBT:DECL<>PRED-PRED=' + pred_df['pred1'].map(str)
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["class_declared"]] != pred_df["pred1"])
+                & (pred_df["pred1"].isin(["MON_LC_BOS", "MON_LC_HEIDE"])),
+                new_pred_column,
+            ] = "RISKY_DOUBT:DECL<>PRED-PRED=" + pred_df["pred1"].map(str)
 
             # If parcel was declared as some arable crops (9534 = knolvenkel), and is classified as fabaceae, set to doubt
             # Remark: those gave 100% false positives for marker LANDCOVER
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED')
-                            & (pred_df[conf.columns['crop_declared']].isin(['9534']))
-                            & (pred_df['pred1'] == 'MON_LC_FABACEAE'),
-                        new_pred_column] = 'DOUBT:ARABLE-SEEN-AS-FABACEAE'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["crop_declared"]].isin(["9534"]))
+                & (pred_df["pred1"] == "MON_LC_FABACEAE"),
+                new_pred_column,
+            ] = "DOUBT:ARABLE-SEEN-AS-FABACEAE"
 
             # If parcel was declared as 'other herbs' or 'flowers', but is not confirmed as
             # MON_LC_ARABLE classified as such: doubt
             # Remark: - those gave > 50% false positives for marker LANDCOVER-EARLY
             #         - gave 33-50% false positives for marker LANDCOVER
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['956', '957', '9831']))
-                            & (pred_df['pred1'] != 'MON_LC_ARABLE'),
-                        new_pred_column] = 'RISKY_DOUBT:HERBS-UNCONFIRMED'
-
-            # If parcel was declared as 'aardbeien', but is not confirmed as MON_LC_ARABLE 
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["crop_declared"]].isin(["956", "957", "9831"]))
+                & (pred_df["pred1"] != "MON_LC_ARABLE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:HERBS-UNCONFIRMED"
+
+            # If parcel was declared as 'aardbeien', but is not confirmed as MON_LC_ARABLE
             # classified as such: doubt
             # Remark: - those gave > 50% false positives for marker LANDCOVER-EARLY
             #         - gave 33-50% false positives for marker LANDCOVER
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['9516']))
-                            & (pred_df['pred1'] != 'MON_LC_ARABLE'),
-                        new_pred_column] = 'RISKY_DOUBT:AARDBEIEN-UNCONFIRMED'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["crop_declared"]].isin(["9516"]))
+                & (pred_df["pred1"] != "MON_LC_ARABLE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:AARDBEIEN-UNCONFIRMED"
 
             # Declared class was not correct, but groundtruth class is permanent
             # TODO: probably dirty this is hardcoded here!
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED')
-                            & (pred_df[conf.columns['class_declared']] != pred_df['pred1'])
-                            & (pred_df['pred1'].isin(['MON_LC_BOS', 'MON_LC_HEIDE', 'MON_LC_OVERK_LOO'])),
-                      new_pred_column] = 'RISKY_DOUBT:DECL<>PRED-PRED=' + pred_df['pred1'].map(str)
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["class_declared"]] != pred_df["pred1"])
+                & (
+                    pred_df["pred1"].isin(
+                        ["MON_LC_BOS", "MON_LC_HEIDE", "MON_LC_OVERK_LOO"]
+                    )
+                ),
+                new_pred_column,
+            ] = "RISKY_DOUBT:DECL<>PRED-PRED=" + pred_df["pred1"].map(str)
 
             # If parcel was declared as 9410 and classified as ARABLE, set to doubt
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['9410']))
-                            & (pred_df['pred1'] == 'MON_LC_ARABLE'),
-                        new_pred_column] = 'RISKY_DOUBT:STAMSLABONEN-SEEN-AS-ARABLE'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["crop_declared"]].isin(["9410"]))
+                & (pred_df["pred1"] == "MON_LC_ARABLE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:STAMSLABONEN-SEEN-AS-ARABLE"
 
             # If parcel was declared as 9602 and classified as FRUIT, set to doubt
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['9602']))
-                            & (pred_df['pred1'] == 'MON_LC_FRUIT'),
-                        new_pred_column] = 'RISKY_DOUBT:BOOM/FRUITKWEEK-UNCONFIRMED'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["crop_declared"]].isin(["9602"]))
+                & (pred_df["pred1"] == "MON_LC_FRUIT"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:BOOM/FRUITKWEEK-UNCONFIRMED"
 
             # If parcel was declared as one of the following and classified as GRASSES, set to RISKY_DOUBT
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED')
-                            & (pred_df[conf.columns['crop_declared']].isin(['36', '895', '9582', '744' '9202', '9714','832', '9602', '9730']))
-                            & (pred_df['pred1'] == 'MON_LC_GRASSES'),
-                        new_pred_column] = 'RISKY_DOUBT:SEEN-AS-GRASSES'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (
+                    pred_df[conf.columns["crop_declared"]].isin(
+                        [
+                            "36",
+                            "895",
+                            "9582",
+                            "744" "9202",
+                            "9714",
+                            "832",
+                            "9602",
+                            "9730",
+                        ]
+                    )
+                )
+                & (pred_df["pred1"] == "MON_LC_GRASSES"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:SEEN-AS-GRASSES"
 
             # If parcel was declared as one of the following and classified as ARABLE, set to RISKY_DOUBT
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['601', '644', '932', '9412', '9584', '8411']))
-                            & (pred_df['pred1'] == 'MON_LC_ARABLE'),
-                        new_pred_column] = 'RISKY_DOUBT:SEEN-AS-ARABLE'
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (
+                    pred_df[conf.columns["crop_declared"]].isin(
+                        ["601", "644", "932", "9412", "9584", "8411"]
+                    )
+                )
+                & (pred_df["pred1"] == "MON_LC_ARABLE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:SEEN-AS-ARABLE"
 
             # If parcel was declared as one of the following and classified as FABACEAE, set to RISKY_DOUBT
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['crop_declared']].isin(['9546']))
-                            & (pred_df['pred1'] == 'MON_LC_FABACEAE'),
-                        new_pred_column] = 'RISKY_DOUBT:SAVOOIKOOL-SEEN-AS-FABACEAE'
-
-        elif conf.marker['markertype'] in ('CROPGROUP', 'CROPGROUP-EARLY'):
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["crop_declared"]].isin(["9546"]))
+                & (pred_df["pred1"] == "MON_LC_FABACEAE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:SAVOOIKOOL-SEEN-AS-FABACEAE"
+
+        elif conf.marker["markertype"] in ("CROPGROUP", "CROPGROUP-EARLY"):
             logger.info("Apply some marker-specific doubt algorythms")
-            
+
             # Red parcels declared as MON_TRITICALE always seem to be predicted wrong, so place them in doubt
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['class_declared']] != pred_df['pred1'])
-                            & (pred_df[conf.columns['class_declared']] == 'MON_TRITICALE'),
-                        new_pred_column] = 'RISKY_DOUBT:TRITICALE-UNCONFIRMED'           
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["class_declared"]] != pred_df["pred1"])
+                & (pred_df[conf.columns["class_declared"]] == "MON_TRITICALE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:TRITICALE-UNCONFIRMED"
 
             # Red parcels with MON_HEIDE seem to have a difficult time seeing the difference between MON_HEIDE and MON_GRASSEN
-            pred_df.loc[(pred_df[new_pred_column] == 'UNDEFINED') 
-                            & (pred_df[conf.columns['class_declared']] != pred_df['pred1'])
-                            & (pred_df['pred1'] == 'MON_HEIDE'),
-                        new_pred_column] = 'RISKY_DOUBT:HEIDE-UNCONFIRMED'
-
-            '''
+            pred_df.loc[
+                (pred_df[new_pred_column] == "UNDEFINED")
+                & (pred_df[conf.columns["class_declared"]] != pred_df["pred1"])
+                & (pred_df["pred1"] == "MON_HEIDE"),
+                new_pred_column,
+            ] = "RISKY_DOUBT:HEIDE-UNCONFIRMED"
+
+            """
             # Note: this will INCREASE the alpha errors, because we remove a large portion of red parcels to doubt (~886 parcels => 16 alpha errors)
             pred_df.loc[((pred_df[new_pred_column] == 'UNDEFINED') | (~pred_df[new_pred_column].str.startswith('DOUBT')))
                             & (pred_df[conf.columns['class_declared']] != pred_df['pred1'])
                             & (pred_df[conf.columns['class_declared']] == 'MON_GRASSEN'),
-                        new_pred_column] = 'RISKY_DOUBT:GRASSEN-UNCONFIRMED'              
-            '''           
+                        new_pred_column] = 'RISKY_DOUBT:GRASSEN-UNCONFIRMED'
+            """
 
     # Accuracy with few pixels might be lower, so set those to doubt
     if apply_doubt_min_nb_pixels is True:
-        pred_df.loc[(pred_df[conf.columns['pixcount_s1s2']] < conf.marker.getfloat('min_nb_pixels'))
-                        & (~pred_df[new_pred_column].str.startswith('DOUBT')),
-                    new_pred_column] = 'DOUBT:NOT_ENOUGH_PIXELS'
-
-    # Finally, predictions that have no value yet, get the original prediction      
-    pred_df.loc[pred_df[new_pred_column] == 'UNDEFINED',
-                new_pred_column] = pred_df['pred1']
+        pred_df.loc[
+            (
+                pred_df[conf.columns["pixcount_s1s2"]]
+                < conf.marker.getfloat("min_nb_pixels")
+            )
+            & (~pred_df[new_pred_column].str.startswith("DOUBT")),
+            new_pred_column,
+        ] = "DOUBT:NOT_ENOUGH_PIXELS"
+
+    # Finally, predictions that have no value yet, get the original prediction
+    pred_df.loc[pred_df[new_pred_column] == "UNDEFINED", new_pred_column] = pred_df[
+        "pred1"
+    ]
diff --git a/cropclassification/postprocess/classification_reporting.py b/cropclassification/postprocess/classification_reporting.py
index 2a2d620a..1a78c165 100644
--- a/cropclassification/postprocess/classification_reporting.py
+++ b/cropclassification/postprocess/classification_reporting.py
@@ -18,51 +18,66 @@
 from cropclassification.helpers import pandas_helper as pdh
 from cropclassification.postprocess import classification_postprocess as class_postpr
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
 # TODO: improve reporting to divide between eligible versus ineligible classes?
 # TODO?: report based on area instead of number parcel
 #     -> seems like being a bit "detached from reality", as for RFV the most important parameter is the number of parcels
 
+
 def write_full_report(
-        parcel_predictions_geopath: Path,
-        output_report_txt: Path,
-        parcel_ground_truth_filepath: Optional[Path] = None,
-        force: bool = False):
+    parcel_predictions_geopath: Path,
+    output_report_txt: Path,
+    parcel_ground_truth_path: Optional[Path] = None,
+    force: bool = False,
+):
     """Writes a report about the accuracy of the predictions to a file.
 
     Args:
-        parcel_predictions_filepath: File name of csv file with the parcel with their predictions.
+        parcel_predictions_path: File name of csv file with the parcel with their
+            predictions.
         prediction_columnname: Column name of the column that contains the predictions.
         output_report_txt: File name of txt file the report will be written to.
-        parcel_ground_truth_filepath: List of parcels with ground truth to calculate eg. alfa and
-            beta errors. If None, the part of the report that is based on this data is skipped
+        parcel_ground_truth_path: List of parcels with ground truth to calculate
+            eg. alfa and beta errors. If None, the part of the report that is based on
+            this data is skipped
 
     TODO: refactor function to split logic more...
     """
 
     # If force == False Check and the output file exists already, stop.
-    output_report_html = Path(str(output_report_txt).replace('.txt', '.html'))
+    output_report_html = Path(str(output_report_txt).replace(".txt", ".html"))
     if force is False and output_report_txt.exists() and output_report_html.exists():
-        logger.warning(f"collect_and_prepare_timeseries_data: output files already exist and force == False, so stop: {output_report_txt}")
+        logger.warning(
+            f"collect_and_prepare_timeseries_data: output files already exist and force == False, so stop: {output_report_txt}"
+        )
         return
 
     logger.info("Start write_full_report")
 
-    pandas_option_context_list = ['display.max_rows', None, 'display.max_columns', None, 
-                                  'display.max_colwidth', 300, 'display.width', 2000, 
-                                  'display.colheader_justify', 'left']
+    pandas_option_context_list = [
+        "display.max_rows",
+        None,
+        "display.max_columns",
+        None,
+        "display.max_colwidth",
+        300,
+        "display.width",
+        2000,
+        "display.colheader_justify",
+        "left",
+    ]
     logger.info(f"Read file with predictions: {parcel_predictions_geopath}")
     df_predict = gfo.read_file(parcel_predictions_geopath)
-    df_predict.set_index(conf.columns['id'], inplace=True)
+    df_predict.set_index(conf.columns["id"], inplace=True)
 
     # Convert all columns to numeric, for the actual numeric ones this will stick.
     # TODO: pretty ugly, hopefully becomes obsolete if pyogrio is used in gfo.to_file
@@ -75,284 +90,461 @@ def write_full_report(
     # Python template engine expects all values to be present, so initialize to empty
     empty_string = "''"
     html_data = {
-            'GENERAL_ACCURACIES_TABLE': empty_string,
-            'GENERAL_ACCURACIES_TEXT': empty_string,
-            'GENERAL_ACCURACIES_DATA': empty_string,
-            'CONFUSION_MATRICES_TABLE': empty_string,
-            'CONFUSION_MATRICES_DATA': empty_string,
-            'CONFUSION_MATRICES_CONSOLIDATED_TABLE': empty_string,
-            'CONFUSION_MATRICES_CONSOLIDATED_DATA': empty_string,
-            'PREDICTION_QUALITY_CONS_OVERVIEW_TEXT': empty_string,
-            'PREDICTION_QUALITY_CONS_OVERVIEW_TABLE': empty_string,
-            'PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TEXT': empty_string,
-            'PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TABLE': empty_string,
-            'PREDICTION_QUALITY_ALPHA_TEXT': empty_string,
-            'PREDICTION_QUALITY_BETA_TEXT': empty_string,
-            'PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TEXT': empty_string,
-            'PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TABLE': empty_string,
-            'PREDICTION_QUALITY_ALPHA_PER_CLASS_TEXT': empty_string,
-            'PREDICTION_QUALITY_ALPHA_PER_CLASS_TABLE': empty_string,
-            'PREDICTION_QUALITY_ALPHA_PER_CROP_TEXT': empty_string,
-            'PREDICTION_QUALITY_ALPHA_PER_CROP_TABLE': empty_string,       
-            'PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TEXT': empty_string,
-            'PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TABLE': empty_string,
-            'PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TEXT': empty_string,
-            'PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TABLE': empty_string,
-            'PREDICTION_QUALITY_BETA_PER_CLASS_TEXT': empty_string,
-            'PREDICTION_QUALITY_BETA_PER_CLASS_TABLE': empty_string,
-            'PREDICTION_QUALITY_BETA_PER_CROP_TEXT': empty_string,
-            'PREDICTION_QUALITY_BETA_PER_CROP_TABLE': empty_string,       
-            'PREDICTION_QUALITY_BETA_PER_PROBABILITY_TEXT': empty_string,
-            'PREDICTION_QUALITY_BETA_PER_PROBABILITY_TABLE': empty_string
+        "GENERAL_ACCURACIES_TABLE": empty_string,
+        "GENERAL_ACCURACIES_TEXT": empty_string,
+        "GENERAL_ACCURACIES_DATA": empty_string,
+        "CONFUSION_MATRICES_TABLE": empty_string,
+        "CONFUSION_MATRICES_DATA": empty_string,
+        "CONFUSION_MATRICES_CONSOLIDATED_TABLE": empty_string,
+        "CONFUSION_MATRICES_CONSOLIDATED_DATA": empty_string,
+        "PREDICTION_QUALITY_CONS_OVERVIEW_TEXT": empty_string,
+        "PREDICTION_QUALITY_CONS_OVERVIEW_TABLE": empty_string,
+        "PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TEXT": empty_string,
+        "PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TABLE": empty_string,
+        "PREDICTION_QUALITY_ALPHA_TEXT": empty_string,
+        "PREDICTION_QUALITY_BETA_TEXT": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TEXT": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TABLE": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_CLASS_TEXT": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_CLASS_TABLE": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_CROP_TEXT": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_CROP_TABLE": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TEXT": empty_string,
+        "PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TABLE": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TEXT": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TABLE": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_CLASS_TEXT": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_CLASS_TABLE": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_CROP_TEXT": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_CROP_TABLE": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_PROBABILITY_TEXT": empty_string,
+        "PREDICTION_QUALITY_BETA_PER_PROBABILITY_TABLE": empty_string,
     }
-    
-    # Build and write report...
-    with open(output_report_txt, 'w') as outputfile:
 
-        outputfile.write("************************************************************\n")
-        outputfile.write("********************* PARAMETERS USED **********************\n")
-        outputfile.write("************************************************************\n")
+    # Build and write report...
+    with open(output_report_txt, "w") as outputfile:
+
+        outputfile.write(
+            "************************************************************\n"
+        )
+        outputfile.write(
+            "********************* PARAMETERS USED **********************\n"
+        )
+        outputfile.write(
+            "************************************************************\n"
+        )
         outputfile.write("\n")
         message = "Main parameters used for the marker"
         outputfile.write(f"\n{message}\n")
-        html_data['PARAMETERS_USED_TEXT'] = message
+        html_data["PARAMETERS_USED_TEXT"] = message
 
         logger.info(f"{dict(conf.marker)}")
-        parameter_list = [['marker', key, value] for key, value in conf.marker.items()]
-        parameter_list += [['calc_marker_params', key, value] for key, value in conf.timeseries.items()]
-        parameter_list += [['timeseries', key, value] for key, value in conf.timeseries.items()]
-        parameter_list += [['preprocess', key, value] for key, value in conf.preprocess.items()]
-        parameter_list += [['classifier', key, value] for key, value in conf.classifier.items()]
-        parameter_list += [['postprocess', key, value] for key, value in conf.postprocess.items()]
-        
-        parameters_used_df = pd.DataFrame(parameter_list, columns=['parameter_type', 'parameter', 'value'])
+        parameter_list = [["marker", key, value] for key, value in conf.marker.items()]
+        parameter_list += [
+            ["calc_marker_params", key, value] for key, value in conf.timeseries.items()
+        ]
+        parameter_list += [
+            ["timeseries", key, value] for key, value in conf.timeseries.items()
+        ]
+        parameter_list += [
+            ["preprocess", key, value] for key, value in conf.preprocess.items()
+        ]
+        parameter_list += [
+            ["classifier", key, value] for key, value in conf.classifier.items()
+        ]
+        parameter_list += [
+            ["postprocess", key, value] for key, value in conf.postprocess.items()
+        ]
+
+        parameters_used_df = pd.DataFrame(
+            parameter_list, columns=["parameter_type", "parameter", "value"]
+        )
         with pd.option_context(*pandas_option_context_list):
             outputfile.write(f"\n{parameters_used_df}\n")
             logger.info(f"{parameters_used_df}\n")
-            html_data['PARAMETERS_USED_TABLE'] = parameters_used_df.to_html(index=False) 
-              
+            html_data["PARAMETERS_USED_TABLE"] = parameters_used_df.to_html(index=False)
+
         outputfile.write("\n")
-        outputfile.write("************************************************************\n")
-        outputfile.write("**************** RECAP OF GENERAL RESULTS ******************\n")
-        outputfile.write("************************************************************\n")
+        outputfile.write(
+            "************************************************************\n"
+        )
+        outputfile.write(
+            "**************** RECAP OF GENERAL RESULTS ******************\n"
+        )
+        outputfile.write(
+            "************************************************************\n"
+        )
         outputfile.write("\n")
-        outputfile.write("************************************************************\n")
-        outputfile.write("*             GENERAL CONSOLIDATED CONCLUSIONS             *\n")
-        outputfile.write("************************************************************\n")
+        outputfile.write(
+            "************************************************************\n"
+        )
+        outputfile.write(
+            "*             GENERAL CONSOLIDATED CONCLUSIONS             *\n"
+        )
+        outputfile.write(
+            "************************************************************\n"
+        )
         # Calculate + write general conclusions for consolidated prediction
         _add_prediction_conclusion(
-                in_df=df_predict,
-                new_columnname=conf.columns['prediction_conclusion_cons'],
-                prediction_column_to_use=conf.columns['prediction_full_alpha'],
-                detailed=False)
+            in_df=df_predict,
+            new_columnname=conf.columns["prediction_conclusion_cons"],
+            prediction_column_to_use=conf.columns["prediction_full_alpha"],
+            detailed=False,
+        )
 
         # Get the number of 'unimportant' ignore parcels and report them here
-        df_predict_unimportant = df_predict[df_predict[conf.columns['prediction_conclusion_cons']] == 'IGNORE_UNIMPORTANT']
-        # Now they can be removed for the rest of the reportings... 
+        df_predict_unimportant = df_predict[
+            df_predict[conf.columns["prediction_conclusion_cons"]]
+            == "IGNORE_UNIMPORTANT"
+        ]
+        # Now they can be removed for the rest of the reportings...
         df_predict = df_predict[
-                df_predict[conf.columns['prediction_conclusion_cons']] != 'IGNORE_UNIMPORTANT']
-
-        message = (f"Prediction conclusions cons general overview, for {len(df_predict.index)} predicted cases."
-                   + f"The {len(df_predict_unimportant.index)} IGNORE_UNIMPORTANT parcels are excluded from the reporting!")
+            df_predict[conf.columns["prediction_conclusion_cons"]]
+            != "IGNORE_UNIMPORTANT"
+        ]
+
+        message = (
+            f"Prediction conclusions cons general overview, for {len(df_predict.index)} predicted cases."
+            + f"The {len(df_predict_unimportant.index)} IGNORE_UNIMPORTANT parcels are excluded from the reporting!"
+        )
         outputfile.write(f"\n{message}\n")
-        html_data['GENERAL_PREDICTION_CONCLUSION_CONS_OVERVIEW_TEXT'] = message
-                
-        count_per_class = (df_predict
-                .groupby(conf.columns['prediction_conclusion_cons']).size().to_frame('count'))
-        values = 100*count_per_class['count']/count_per_class['count'].sum()
-        count_per_class.insert(loc=1, column='pct', value=values)
-        
-        with pd.option_context(*pandas_option_context_list):                                
+        html_data["GENERAL_PREDICTION_CONCLUSION_CONS_OVERVIEW_TEXT"] = message
+
+        count_per_class = (
+            df_predict.groupby(conf.columns["prediction_conclusion_cons"])
+            .size()
+            .to_frame("count")
+        )
+        values = 100 * count_per_class["count"] / count_per_class["count"].sum()
+        count_per_class.insert(loc=1, column="pct", value=values)
+
+        with pd.option_context(*pandas_option_context_list):
             outputfile.write(f"\n{count_per_class}\n")
             logger.info(f"{count_per_class}\n")
-            html_data['GENERAL_PREDICTION_CONCLUSION_CONS_OVERVIEW_TABLE'] = count_per_class.to_html()
-            html_data['GENERAL_PREDICTION_CONCLUSION_CONS_OVERVIEW_DATA'] = count_per_class.to_dict()
+            html_data[
+                "GENERAL_PREDICTION_CONCLUSION_CONS_OVERVIEW_TABLE"
+            ] = count_per_class.to_html()
+            html_data[
+                "GENERAL_PREDICTION_CONCLUSION_CONS_OVERVIEW_DATA"
+            ] = count_per_class.to_dict()
 
         # Output general accuracies
         outputfile.write("\n")
-        outputfile.write("************************************************************\n")
-        outputfile.write("*                   OVERALL ACCURACIES                     *\n")
-        outputfile.write("************************************************************\n")
+        outputfile.write(
+            "************************************************************\n"
+        )
+        outputfile.write(
+            "*                   OVERALL ACCURACIES                     *\n"
+        )
+        outputfile.write(
+            "************************************************************\n"
+        )
         overall_accuracies_list = []
 
         # Calculate overall accuracies for all parcels
         try:
-            oa = skmetrics.accuracy_score(
-                    df_predict[conf.columns['class']], df_predict['pred1'],
-                    normalize=True, sample_weight=None) * 100
+            oa = (
+                skmetrics.accuracy_score(
+                    df_predict[conf.columns["class"]],
+                    df_predict["pred1"],
+                    normalize=True,
+                    sample_weight=None,
+                )
+                * 100
+            )
             overall_accuracies_list.append(
-                    {'parcels': 'All', 'prediction_type': 'standard', 'accuracy': oa})
-
-            oa = skmetrics.accuracy_score(
-                    df_predict[conf.columns['class']], df_predict[conf.columns['prediction_full_alpha']],
-                    normalize=True, sample_weight=None) * 100
+                {"parcels": "All", "prediction_type": "standard", "accuracy": oa}
+            )
+
+            oa = (
+                skmetrics.accuracy_score(
+                    df_predict[conf.columns["class"]],
+                    df_predict[conf.columns["prediction_full_alpha"]],
+                    normalize=True,
+                    sample_weight=None,
+                )
+                * 100
+            )
             overall_accuracies_list.append(
-                    {'parcels': 'All', 'prediction_type': 'consolidated', 'accuracy': oa})
+                {"parcels": "All", "prediction_type": "consolidated", "accuracy": oa}
+            )
         except:
             logger.exception("Error calculating overall accuracies!")
 
         # Calculate while ignoring the classes to be ignored...
         df_predict_accuracy_no_ignore = df_predict[
-                ~df_predict[conf.columns['class']].isin(conf.marker.getlist('classes_to_ignore_for_train'))]
+            ~df_predict[conf.columns["class"]].isin(
+                conf.marker.getlist("classes_to_ignore_for_train")
+            )
+        ]
         df_predict_accuracy_no_ignore = df_predict_accuracy_no_ignore[
-                ~df_predict_accuracy_no_ignore[conf.columns['class']].isin(conf.marker.getlist('classes_to_ignore'))]
-        
-        oa = skmetrics.accuracy_score(
-                df_predict_accuracy_no_ignore[conf.columns['class']], 
-                df_predict_accuracy_no_ignore['pred1'],
-                normalize=True, sample_weight=None) * 100
+            ~df_predict_accuracy_no_ignore[conf.columns["class"]].isin(
+                conf.marker.getlist("classes_to_ignore")
+            )
+        ]
+
+        oa = (
+            skmetrics.accuracy_score(
+                df_predict_accuracy_no_ignore[conf.columns["class"]],
+                df_predict_accuracy_no_ignore["pred1"],
+                normalize=True,
+                sample_weight=None,
+            )
+            * 100
+        )
         overall_accuracies_list.append(
-                {'parcels': 'Exclude classes_to_ignore(_for_train) classes', 
-                 'prediction_type': 'standard', 'accuracy': oa})
-
-        oa = skmetrics.accuracy_score(
-                df_predict_accuracy_no_ignore[conf.columns['class']],
-                df_predict_accuracy_no_ignore[conf.columns['prediction_full_alpha']],
-                normalize=True, sample_weight=None) * 100
+            {
+                "parcels": "Exclude classes_to_ignore(_for_train) classes",
+                "prediction_type": "standard",
+                "accuracy": oa,
+            }
+        )
+
+        oa = (
+            skmetrics.accuracy_score(
+                df_predict_accuracy_no_ignore[conf.columns["class"]],
+                df_predict_accuracy_no_ignore[conf.columns["prediction_full_alpha"]],
+                normalize=True,
+                sample_weight=None,
+            )
+            * 100
+        )
         overall_accuracies_list.append(
-                {'parcels': 'Exclude classes_to_ignore(_for_train) classes', 
-                 'prediction_type': 'consolidated', 'accuracy': oa})
+            {
+                "parcels": "Exclude classes_to_ignore(_for_train) classes",
+                "prediction_type": "consolidated",
+                "accuracy": oa,
+            }
+        )
 
         # Calculate ignoring both classes to ignored + parcels not having a valid prediction
         df_predict_no_ignore_has_prediction = df_predict_accuracy_no_ignore.loc[
-                (df_predict_accuracy_no_ignore[conf.columns['prediction_full_alpha']] != 'NODATA')
-                   & (df_predict_accuracy_no_ignore[conf.columns['prediction_full_alpha']] != 'DOUBT:NOT_ENOUGH_PIXELS')]
-        oa = skmetrics.accuracy_score(
-                df_predict_no_ignore_has_prediction[conf.columns['class']],
-                df_predict_no_ignore_has_prediction['pred1'],
-                normalize=True, sample_weight=None) * 100
+            (
+                df_predict_accuracy_no_ignore[conf.columns["prediction_full_alpha"]]
+                != "NODATA"
+            )
+            & (
+                df_predict_accuracy_no_ignore[conf.columns["prediction_full_alpha"]]
+                != "DOUBT:NOT_ENOUGH_PIXELS"
+            )
+        ]
+        oa = (
+            skmetrics.accuracy_score(
+                df_predict_no_ignore_has_prediction[conf.columns["class"]],
+                df_predict_no_ignore_has_prediction["pred1"],
+                normalize=True,
+                sample_weight=None,
+            )
+            * 100
+        )
         overall_accuracies_list.append(
-                {'parcels': 'Exclude ignored ones + with prediction (= excl. NODATA, NOT_ENOUGH_PIXELS)', 
-                 'prediction_type': 'standard', 'accuracy': oa})
-
-        oa = skmetrics.accuracy_score(
-                df_predict_no_ignore_has_prediction[conf.columns['class']],
-                df_predict_no_ignore_has_prediction[conf.columns['prediction_full_alpha']],
-                normalize=True, sample_weight=None) * 100
+            {
+                "parcels": "Exclude ignored ones + with prediction (= excl. NODATA, NOT_ENOUGH_PIXELS)",
+                "prediction_type": "standard",
+                "accuracy": oa,
+            }
+        )
+
+        oa = (
+            skmetrics.accuracy_score(
+                df_predict_no_ignore_has_prediction[conf.columns["class"]],
+                df_predict_no_ignore_has_prediction[
+                    conf.columns["prediction_full_alpha"]
+                ],
+                normalize=True,
+                sample_weight=None,
+            )
+            * 100
+        )
         overall_accuracies_list.append(
-                {'parcels': 'Exclude ignored ones + with prediction (= excl. NODATA, NOT_ENOUGH_PIXELS)', 
-                 'prediction_type': 'consolidated', 'accuracy': oa})
+            {
+                "parcels": "Exclude ignored ones + with prediction (= excl. NODATA, NOT_ENOUGH_PIXELS)",
+                "prediction_type": "consolidated",
+                "accuracy": oa,
+            }
+        )
 
         # Output the resulting overall accuracies
-        message = 'Overall accuracies for different sub-groups of the data'
+        message = "Overall accuracies for different sub-groups of the data"
         outputfile.write(f"\n{message}\n")
-        html_data['OVERALL_ACCURACIES_TEXT'] = message
-
-        overall_accuracies_df = pd.DataFrame(overall_accuracies_list, 
-                                             columns=['parcels', 'prediction_type', 'accuracy'])
-        overall_accuracies_df.set_index(keys=['parcels', 'prediction_type'], inplace=True)
+        html_data["OVERALL_ACCURACIES_TEXT"] = message
+
+        overall_accuracies_df = pd.DataFrame(
+            overall_accuracies_list, columns=["parcels", "prediction_type", "accuracy"]
+        )
+        overall_accuracies_df.set_index(
+            keys=["parcels", "prediction_type"], inplace=True
+        )
         with pd.option_context(*pandas_option_context_list):
             outputfile.write(f"\n{overall_accuracies_df}\n")
             logger.info(f"{overall_accuracies_df}\n")
-            html_data['OVERALL_ACCURACIES_TABLE'] = overall_accuracies_df.to_html()        
+            html_data["OVERALL_ACCURACIES_TABLE"] = overall_accuracies_df.to_html()
 
         # Write the recall, F1 score,... per class
-        #message = skmetrics.classification_report(df_predict[gs.class_column]
+        # message = skmetrics.classification_report(df_predict[gs.class_column]
         #                                                , df_predict[gs.prediction_column]
         #                                                , labels=classes)
-        #outputfile.write(message)
-
-        outputfile.write("************************************************************\n")
-        outputfile.write("********************* DETAILED RESULTS *********************\n")
-        outputfile.write("************************************************************\n")
+        # outputfile.write(message)
+
+        outputfile.write(
+            "************************************************************\n"
+        )
+        outputfile.write(
+            "********************* DETAILED RESULTS *********************\n"
+        )
+        outputfile.write(
+            "************************************************************\n"
+        )
         outputfile.write("\n")
-        outputfile.write("************************************************************\n")
-        outputfile.write("*             DETAILED PREDICTION CONCLUSIONS              *\n")
-        outputfile.write("************************************************************\n")
+        outputfile.write(
+            "************************************************************\n"
+        )
+        outputfile.write(
+            "*             DETAILED PREDICTION CONCLUSIONS              *\n"
+        )
+        outputfile.write(
+            "************************************************************\n"
+        )
 
         # Calculate detailed conclusions for the predictions
         logger.info("Calculate the detailed conclusions for the predictions")
 
         # Write the conclusions for the consolidated predictions
         _add_prediction_conclusion(
-                in_df=df_predict,
-                new_columnname=conf.columns['prediction_conclusion_detail_cons'],
-                prediction_column_to_use=conf.columns['prediction_cons'],
-                detailed=True)
+            in_df=df_predict,
+            new_columnname=conf.columns["prediction_conclusion_detail_cons"],
+            prediction_column_to_use=conf.columns["prediction_cons"],
+            detailed=True,
+        )
         message = f"Prediction conclusions cons (doubt + not_enough_pixels) overview, for {len(df_predict.index)} predicted cases:"
         outputfile.write(f"\n{message}\n")
-        html_data['PREDICTION_CONCLUSION_DETAIL_CONS_OVERVIEW_TEXT'] = message
-        
-        count_per_class = (df_predict
-                .groupby(conf.columns['prediction_conclusion_detail_cons'])
-                .size().to_frame('count'))
-        values = 100*count_per_class['count']/count_per_class['count'].sum()
-        count_per_class.insert(loc=1, column='pct', value=values)
-        
-        with pd.option_context('display.max_rows', None, 'display.max_columns', None):                                
+        html_data["PREDICTION_CONCLUSION_DETAIL_CONS_OVERVIEW_TEXT"] = message
+
+        count_per_class = (
+            df_predict.groupby(conf.columns["prediction_conclusion_detail_cons"])
+            .size()
+            .to_frame("count")
+        )
+        values = 100 * count_per_class["count"] / count_per_class["count"].sum()
+        count_per_class.insert(loc=1, column="pct", value=values)
+
+        with pd.option_context("display.max_rows", None, "display.max_columns", None):
             outputfile.write(f"\n{count_per_class}\n")
             logger.info(f"{count_per_class}\n")
-            html_data['PREDICTION_CONCLUSION_DETAIL_CONS_OVERVIEW_TABLE'] = count_per_class.to_html()
+            html_data[
+                "PREDICTION_CONCLUSION_DETAIL_CONS_OVERVIEW_TABLE"
+            ] = count_per_class.to_html()
 
         # Calculate detailed conclusions for the predictions
         logger.info("Calculate the detailed conclusions for the predictions")
 
         # Write the conclusions for the consolidated predictions
         _add_prediction_conclusion(
-                in_df=df_predict,
-                new_columnname=conf.columns['prediction_conclusion_detail_full_alpha'],
-                prediction_column_to_use=conf.columns['prediction_full_alpha'],
-                detailed=True)
+            in_df=df_predict,
+            new_columnname=conf.columns["prediction_conclusion_detail_full_alpha"],
+            prediction_column_to_use=conf.columns["prediction_full_alpha"],
+            detailed=True,
+        )
         message = f"Prediction conclusions full alpha (doubt + not_enough_pixels) overview, for {len(df_predict.index)} predicted cases:"
         outputfile.write(f"\n{message}\n")
-        html_data['PREDICTION_CONCLUSION_DETAIL_FULL_ALPHA_OVERVIEW_TEXT'] = message
-        
-        count_per_class = (df_predict
-                .groupby(conf.columns['prediction_conclusion_detail_full_alpha'])
-                .size().to_frame('count'))
-        values = 100*count_per_class['count']/count_per_class['count'].sum()
-        count_per_class.insert(loc=1, column='pct', value=values)
-        
-        with pd.option_context('display.max_rows', None, 'display.max_columns', None):                                
+        html_data["PREDICTION_CONCLUSION_DETAIL_FULL_ALPHA_OVERVIEW_TEXT"] = message
+
+        count_per_class = (
+            df_predict.groupby(conf.columns["prediction_conclusion_detail_full_alpha"])
+            .size()
+            .to_frame("count")
+        )
+        values = 100 * count_per_class["count"] / count_per_class["count"].sum()
+        count_per_class.insert(loc=1, column="pct", value=values)
+
+        with pd.option_context("display.max_rows", None, "display.max_columns", None):
             outputfile.write(f"\n{count_per_class}\n")
             logger.info(f"{count_per_class}\n")
-            html_data['PREDICTION_CONCLUSION_DETAIL_FULL_ALPHA_OVERVIEW_TABLE'] = count_per_class.to_html()
+            html_data[
+                "PREDICTION_CONCLUSION_DETAIL_FULL_ALPHA_OVERVIEW_TABLE"
+            ] = count_per_class.to_html()
 
         outputfile.write("\n")
-        outputfile.write("************************************************************\n")
-        outputfile.write("*     CONFUSION MATRICES FOR PARCELS WITH PREDICTIONS      *\n")
-        outputfile.write("************************************************************\n")
+        outputfile.write(
+            "************************************************************\n"
+        )
+        outputfile.write(
+            "*     CONFUSION MATRICES FOR PARCELS WITH PREDICTIONS      *\n"
+        )
+        outputfile.write(
+            "************************************************************\n"
+        )
         # Calculate an extended confusion matrix with the standard prediction column and write
         # it to output...
-        df_confmatrix_ext = _get_confusion_matrix_ext(df_predict, 'pred1')
-        outputfile.write("\nExtended confusion matrix of the predictions: Rows: true/input classes, columns: predicted classes\n")
-        with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):
+        df_confmatrix_ext = _get_confusion_matrix_ext(df_predict, "pred1")
+        outputfile.write(
+            "\nExtended confusion matrix of the predictions: Rows: true/input classes, columns: predicted classes\n"
+        )
+        with pd.option_context(
+            "display.max_rows", None, "display.max_columns", None, "display.width", 2000
+        ):
             outputfile.write(f"{df_confmatrix_ext}\n")
-            html_data['CONFUSION_MATRICES_TABLE'] = df_confmatrix_ext.to_html()
-            html_data['CONFUSION_MATRICES_DATA'] = df_confmatrix_ext.to_json()
+            html_data["CONFUSION_MATRICES_TABLE"] = df_confmatrix_ext.to_html()
+            html_data["CONFUSION_MATRICES_DATA"] = df_confmatrix_ext.to_json()
 
         # Calculate an extended confusion matrix with the full alpha prediction column and write
         # it to output...
-        df_confmatrix_ext = _get_confusion_matrix_ext(df_predict, conf.columns['prediction_full_alpha'])
-        outputfile.write("\nExtended confusion matrix of the consolidated predictions: Rows: true/input classes, columns: predicted classes\n")
-        with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):
+        df_confmatrix_ext = _get_confusion_matrix_ext(
+            df_predict, conf.columns["prediction_full_alpha"]
+        )
+        outputfile.write(
+            "\nExtended confusion matrix of the consolidated predictions: Rows: true/input classes, columns: predicted classes\n"
+        )
+        with pd.option_context(
+            "display.max_rows", None, "display.max_columns", None, "display.width", 2000
+        ):
             outputfile.write(f"{df_confmatrix_ext}\n\n")
-            html_data['CONFUSION_MATRICES_CONSOLIDATED_TABLE'] = df_confmatrix_ext.to_html()
-            html_data['CONFUSION_MATRICES_CONSOLIDATED_DATA'] = df_confmatrix_ext.to_json()
+            html_data[
+                "CONFUSION_MATRICES_CONSOLIDATED_TABLE"
+            ] = df_confmatrix_ext.to_html()
+            html_data[
+                "CONFUSION_MATRICES_CONSOLIDATED_DATA"
+            ] = df_confmatrix_ext.to_json()
 
         # If the pixcount is available, write the OA per pixcount
-        if conf.columns['pixcount_s1s2'] in df_predict.columns:
-            pixcount_output_report_txt = Path(str(output_report_txt) + '_OA_per_pixcount.txt')
-            _write_OA_per_pixcount(df_parcel_predictions=df_predict,
-                                   output_report_txt=pixcount_output_report_txt,
-                                   force=force)
-        
+        if conf.columns["pixcount_s1s2"] in df_predict.columns:
+            pixcount_output_report_txt = Path(
+                str(output_report_txt) + "_OA_per_pixcount.txt"
+            )
+            _write_OA_per_pixcount(
+                df_parcel_predictions=df_predict,
+                output_report_txt=pixcount_output_report_txt,
+                force=force,
+            )
+
         # If a ground truth file is provided, report on the ground truth
-        if parcel_ground_truth_filepath is not None:
-            outputfile.write("************************************************************\n")
-            outputfile.write("*   REPORTING ON PREDICTION QUALITY BASED ON GROUND TRUTH  *\n")
-            outputfile.write("************************************************************\n")
+        if parcel_ground_truth_path is not None:
+            outputfile.write(
+                "************************************************************\n"
+            )
+            outputfile.write(
+                "*   REPORTING ON PREDICTION QUALITY BASED ON GROUND TRUTH  *\n"
+            )
+            outputfile.write(
+                "************************************************************\n"
+            )
 
             # Read ground truth
-            logger.info(f"Read csv with ground truth (with their classes): {parcel_ground_truth_filepath}")
-            df_parcel_gt = pdh.read_file(parcel_ground_truth_filepath)
-            df_parcel_gt.set_index(conf.columns['id'], inplace=True)
-            logger.info(f"Read csv with ground truth ready, shape: {df_parcel_gt.shape}")
+            logger.info(
+                f"Read csv with ground truth (with their classes): {parcel_ground_truth_path}"
+            )
+            df_parcel_gt = pdh.read_file(parcel_ground_truth_path)
+            df_parcel_gt.set_index(conf.columns["id"], inplace=True)
+            logger.info(
+                f"Read csv with ground truth ready, shape: {df_parcel_gt.shape}"
+            )
 
             # Join the prediction data
             cols_to_join = df_predict.columns.difference(df_parcel_gt.columns)
-            df_parcel_gt = df_predict[cols_to_join].join(df_parcel_gt, how='inner')
-            logger.info(f"After join of ground truth with predictions, shape: {df_parcel_gt.shape}")
+            df_parcel_gt = df_predict[cols_to_join].join(df_parcel_gt, how="inner")
+            logger.info(
+                f"After join of ground truth with predictions, shape: {df_parcel_gt.shape}"
+            )
 
             if len(df_parcel_gt.index) == 0:
                 message = "After join of ground truth with predictions the result was empty, so probably a wrong ground truth file was used!"
@@ -362,641 +554,960 @@ def write_full_report(
             # General ground truth statistics
             # ******************************************************************
             # Calculate the conclusions based on ground truth
-           
+
             # Calculate and write the result for the consolidated predictions
-            _add_gt_conclusions(df_parcel_gt, conf.columns['prediction_cons'])
+            _add_gt_conclusions(df_parcel_gt, conf.columns["prediction_cons"])
             message = f"Prediction quality cons (doubt + not_enough_pixels) overview, for {len(df_parcel_gt.index)} predicted cases in ground truth:"
             outputfile.write(f"\n{message}\n")
-            html_data['PREDICTION_QUALITY_CONS_OVERVIEW_TEXT'] = message
-            
-            count_per_class = (df_parcel_gt
-                    .groupby(f"gt_conclusion_{conf.columns['prediction_cons']}")
-                    .size().to_frame('count'))
-            values = 100*count_per_class['count']/count_per_class['count'].sum()
-            count_per_class.insert(loc=1, column='pct', value=values)
-            
-            with pd.option_context('display.max_rows', None, 'display.max_columns', None):                                
+            html_data["PREDICTION_QUALITY_CONS_OVERVIEW_TEXT"] = message
+
+            count_per_class = (
+                df_parcel_gt.groupby(f"gt_conclusion_{conf.columns['prediction_cons']}")
+                .size()
+                .to_frame("count")
+            )
+            values = 100 * count_per_class["count"] / count_per_class["count"].sum()
+            count_per_class.insert(loc=1, column="pct", value=values)
+
+            with pd.option_context(
+                "display.max_rows", None, "display.max_columns", None
+            ):
                 outputfile.write(f"\n{count_per_class}\n")
                 logger.info(f"{count_per_class}\n")
-                html_data['PREDICTION_QUALITY_CONS_OVERVIEW_TABLE'] = count_per_class.to_html()
+                html_data[
+                    "PREDICTION_QUALITY_CONS_OVERVIEW_TABLE"
+                ] = count_per_class.to_html()
 
             # Calculate and write the result for the full alpha predictions
-            _add_gt_conclusions(df_parcel_gt, conf.columns['prediction_full_alpha'])
+            _add_gt_conclusions(df_parcel_gt, conf.columns["prediction_full_alpha"])
             message = f"Prediction quality cons (doubt + not_enough_pixels) overview, for {len(df_parcel_gt.index)} predicted cases in ground truth:"
             outputfile.write(f"\n{message}\n")
-            html_data['PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TEXT'] = message
-            
-            count_per_class = (df_parcel_gt
-                    .groupby(f"gt_conclusion_{conf.columns['prediction_full_alpha']}")
-                    .size().to_frame('count'))
-            values = 100*count_per_class['count']/count_per_class['count'].sum()
-            count_per_class.insert(loc=1, column='pct', value=values)
-            
-            with pd.option_context('display.max_rows', None, 'display.max_columns', None):                                
+            html_data["PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TEXT"] = message
+
+            count_per_class = (
+                df_parcel_gt.groupby(
+                    f"gt_conclusion_{conf.columns['prediction_full_alpha']}"
+                )
+                .size()
+                .to_frame("count")
+            )
+            values = 100 * count_per_class["count"] / count_per_class["count"].sum()
+            count_per_class.insert(loc=1, column="pct", value=values)
+
+            with pd.option_context(
+                "display.max_rows", None, "display.max_columns", None
+            ):
                 outputfile.write(f"\n{count_per_class}\n")
                 logger.info(f"{count_per_class}\n")
-                html_data['PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TABLE'] = count_per_class.to_html()
+                html_data[
+                    "PREDICTION_QUALITY_FULL_ALPHA_OVERVIEW_TABLE"
+                ] = count_per_class.to_html()
 
             # Write the ground truth conclusions to files
-            pdh.to_file(df_parcel_gt, Path(str(output_report_txt) + "_groundtruth_pred_quality_details.tsv"))
-            output_path = Path(str(output_report_txt) + "_groundtruth_pred_quality_details.gpkg")
+            pdh.to_file(
+                df_parcel_gt,
+                Path(str(output_report_txt) + "_groundtruth_pred_quality_details.tsv"),
+            )
+            output_path = Path(
+                str(output_report_txt) + "_groundtruth_pred_quality_details.gpkg"
+            )
             gfo.to_file(gdf=df_parcel_gt, path=output_path)
 
             # Alpha and beta error statistics based on CONS prediction
-            # ******************************************************************            
-            # Pct Alpha errors=alpha errors/(alpha errors + real errors)  
+            # ******************************************************************
+            # Pct Alpha errors=alpha errors/(alpha errors + real errors)
             columnname = f"gt_conclusion_{conf.columns['prediction_cons']}"
-            alpha_numerator = len(df_parcel_gt.loc[df_parcel_gt[columnname] == 'FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA'].index)
-            alpha_denominator = (alpha_numerator 
-                    + len(df_parcel_gt.loc[df_parcel_gt[columnname].isin(
-                            ['FARMER-WRONG_PRED-CORRECT', 'FARMER-WRONG_PRED-WRONG'])].index))
+            alpha_numerator = len(
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname] == "FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA"
+                ].index
+            )
+            alpha_denominator = alpha_numerator + len(
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname].isin(
+                        ["FARMER-WRONG_PRED-CORRECT", "FARMER-WRONG_PRED-WRONG"]
+                    )
+                ].index
+            )
             if alpha_denominator > 0:
-                message = (f"Alpha error for cons: {alpha_numerator}/{alpha_denominator} = "
-                        + f"{(alpha_numerator/alpha_denominator):.02f}")
+                message = (
+                    f"Alpha error for cons: {alpha_numerator}/{alpha_denominator} = "
+                    + f"{(alpha_numerator/alpha_denominator):.02f}"
+                )
             else:
-                message = f"Alpha error for cons: {alpha_numerator}/{alpha_denominator} = ?"
+                message = (
+                    f"Alpha error for cons: {alpha_numerator}/{alpha_denominator} = ?"
+                )
 
             outputfile.write(f"\n{message}\n")
-            html_data['PREDICTION_QUALITY_ALPHA_TEXT'] = message
+            html_data["PREDICTION_QUALITY_ALPHA_TEXT"] = message
 
-            beta_numerator = len(df_parcel_gt.loc[df_parcel_gt[columnname] == 'FARMER-WRONG_PRED-DOESNT_OPPOSE:ERROR_BETA'].index)
-            beta_denominator = (beta_numerator 
-                    + len(df_parcel_gt.loc[df_parcel_gt[columnname].str.startswith('FARMER-WRONG_PRED-')].index))
+            beta_numerator = len(
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname]
+                    == "FARMER-WRONG_PRED-DOESNT_OPPOSE:ERROR_BETA"
+                ].index
+            )
+            beta_denominator = beta_numerator + len(
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname].str.startswith("FARMER-WRONG_PRED-")
+                ].index
+            )
             if beta_denominator > 0:
-                message = (f"Beta error for cons: {beta_numerator}/{beta_denominator} = "
-                           + f"{(beta_numerator/beta_denominator):.02f}")
+                message = (
+                    f"Beta error for cons: {beta_numerator}/{beta_denominator} = "
+                    + f"{(beta_numerator/beta_denominator):.02f}"
+                )
             else:
-                message = f"Beta error for cons: {beta_numerator}/{beta_denominator} = ?"
+                message = (
+                    f"Beta error for cons: {beta_numerator}/{beta_denominator} = ?"
+                )
 
             outputfile.write(f"\n{message}\n")
-            html_data['PREDICTION_QUALITY_BETA_TEXT'] = message
+            html_data["PREDICTION_QUALITY_BETA_TEXT"] = message
 
             # Alpha and beta error statistics based on CONS prediction
-            # ******************************************************************            
+            # ******************************************************************
             # Pct ALPHA errors=alpha errors/(alpha errors + real errors)
-            alpha_numerator_columns = ['FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA']
-            alpha_denominator_columns = ['FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA', 
-                                         'FARMER-WRONG_PRED-CORRECT', 'FARMER-WRONG_PRED-WRONG']
+            alpha_numerator_columns = ["FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA"]
+            alpha_denominator_columns = [
+                "FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA",
+                "FARMER-WRONG_PRED-CORRECT",
+                "FARMER-WRONG_PRED-WRONG",
+            ]
             columnname = f"gt_conclusion_{conf.columns['prediction_full_alpha']}"
             alpha_numerator = len(
-                    df_parcel_gt.loc[df_parcel_gt[columnname].isin(alpha_numerator_columns)].index)
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname].isin(alpha_numerator_columns)
+                ].index
+            )
             alpha_denominator = len(
-                    df_parcel_gt.loc[df_parcel_gt[columnname].isin(alpha_denominator_columns)].index)
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname].isin(alpha_denominator_columns)
+                ].index
+            )
             if alpha_denominator > 0:
-                message = (f"Alpha error full: {alpha_numerator}/{alpha_denominator} = "
-                        + f"{(alpha_numerator/alpha_denominator):.02f}")
+                message = (
+                    f"Alpha error full: {alpha_numerator}/{alpha_denominator} = "
+                    + f"{(alpha_numerator/alpha_denominator):.02f}"
+                )
             else:
                 message = f"Alpha error full: {alpha_numerator}/{alpha_denominator} = ?"
 
             outputfile.write(f"\n{message}\n")
-            html_data['PREDICTION_QUALITY_ALPHA_TEXT'] += '<br/>' + message
+            html_data["PREDICTION_QUALITY_ALPHA_TEXT"] += "<br/>" + message
 
             # Pct BETA errors=beta errors/(beta errors + real wrong farmer declarations)
-            beta_numerator_columns = ['FARMER-WRONG_PRED-DOESNT_OPPOSE:ERROR_BETA']
-            beta_denominator_columns = df_parcel_gt[columnname].loc[
-                    df_parcel_gt[columnname].str.startswith('FARMER-WRONG_PRED-')].unique()
+            beta_numerator_columns = ["FARMER-WRONG_PRED-DOESNT_OPPOSE:ERROR_BETA"]
+            beta_denominator_columns = (
+                df_parcel_gt[columnname]
+                .loc[df_parcel_gt[columnname].str.startswith("FARMER-WRONG_PRED-")]
+                .unique()
+            )
             beta_numerator = len(
-                    df_parcel_gt.loc[df_parcel_gt[columnname].isin(beta_numerator_columns)].index)
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname].isin(beta_numerator_columns)
+                ].index
+            )
             beta_denominator = len(
-                    df_parcel_gt.loc[df_parcel_gt[columnname].isin(beta_denominator_columns)].index)
+                df_parcel_gt.loc[
+                    df_parcel_gt[columnname].isin(beta_denominator_columns)
+                ].index
+            )
             if beta_denominator > 0:
-                message = (f"Beta error full: {beta_numerator}/{beta_denominator} = "
-                           + f"{(beta_numerator/beta_denominator):.02f}")
+                message = (
+                    f"Beta error full: {beta_numerator}/{beta_denominator} = "
+                    + f"{(beta_numerator/beta_denominator):.02f}"
+                )
             else:
                 message = f"Beta error full: {beta_numerator}/{beta_denominator} = ?"
 
             outputfile.write(f"\n{message}\n")
-            html_data['PREDICTION_QUALITY_BETA_TEXT'] += '<br/>' + message
+            html_data["PREDICTION_QUALITY_BETA_TEXT"] += "<br/>" + message
 
             # Some more detailed reports for alpha and beta errors
-            # ******************************************************************            
-            
+            # ******************************************************************
+
             # If the pixcount is available, write the number of ALFA errors per pixcount (for the prediction with doubt)
-            pred_quality_full_doubt_column = f"gt_conclusion_{conf.columns['prediction_full_alpha']}"
-            if conf.columns['pixcount_s1s2'] in df_parcel_gt.columns:
-                # ALPHA errors 
+            pred_quality_full_doubt_column = (
+                f"gt_conclusion_{conf.columns['prediction_full_alpha']}"
+            )
+            if conf.columns["pixcount_s1s2"] in df_parcel_gt.columns:
+                # ALPHA errors
                 message = f"Number of ERROR_ALFA parcels per pixcount for the ground truth parcels without applying doubt based on pixcount:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TEXT'] = message
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TEXT"] = message
 
                 # For pixcount report, use error conclusions without min_nb_pixels
                 class_postpr.add_doubt_column(
-                        pred_df=df_parcel_gt, 
-                        new_pred_column='pred_cons_no_min_pix',
-                        apply_doubt_pct_proba=True,
-                        apply_doubt_min_nb_pixels=False,
-                        apply_doubt_marker_specific=True)
-                _add_gt_conclusions(df_parcel_gt, 'pred_cons_no_min_pix')
-                
+                    pred_df=df_parcel_gt,
+                    new_pred_column="pred_cons_no_min_pix",
+                    apply_doubt_pct_proba=True,
+                    apply_doubt_min_nb_pixels=False,
+                    apply_doubt_marker_specific=True,
+                )
+                _add_gt_conclusions(df_parcel_gt, "pred_cons_no_min_pix")
+
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_" + "pred_cons_no_min_pix"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn=conf.columns['pixcount_s1s2'],
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=alpha_numerator_columns,
-                        error_codes_denominator=alpha_denominator_columns,
-                        error_type='alpha')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn=conf.columns["pixcount_s1s2"],
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=alpha_numerator_columns,
+                    error_codes_denominator=alpha_denominator_columns,
+                    error_type="alpha",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TABLE'] = df_per_column.to_html()
+                    html_data[
+                        "PREDICTION_QUALITY_ALPHA_PER_PIXCOUNT_TABLE"
+                    ] = df_per_column.to_html()
 
-                # BETA errors 
+                # BETA errors
                 message = f"Number of ERROR_BETA parcels per pixcount for the ground truth parcels without applying doubt based on pixcount:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TEXT'] = message
-                
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TEXT"] = message
+
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_" + "pred_cons_no_min_pix"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn=conf.columns['pixcount_s1s2'],
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=beta_numerator_columns,
-                        error_codes_denominator=beta_denominator_columns,
-                        error_type='beta')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn=conf.columns["pixcount_s1s2"],
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=beta_numerator_columns,
+                    error_codes_denominator=beta_denominator_columns,
+                    error_type="beta",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TABLE'] = df_per_column.to_html()
-                    
+                    html_data[
+                        "PREDICTION_QUALITY_BETA_PER_PIXCOUNT_TABLE"
+                    ] = df_per_column.to_html()
+
             # If cropclass is available, write the number of ALFA errors per cropclass (for the prediction with doubt)
-            if conf.columns['class_declared'] in df_parcel_gt.columns:
-                # ALPHA errors 
+            if conf.columns["class_declared"] in df_parcel_gt.columns:
+                # ALPHA errors
                 message = f"Number of ERROR_ALFA parcels per declared cropclass for the ground truth parcels without applying crop/class based doubt:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_ALPHA_PER_CLASS_TEXT'] = message
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_ALPHA_PER_CLASS_TEXT"] = message
 
                 # For class report, use error conclusions without marker specific stuff
                 class_postpr.add_doubt_column(
-                        pred_df=df_parcel_gt, 
-                        new_pred_column='pred_cons_no_marker_specific',
-                        apply_doubt_pct_proba=True,
-                        apply_doubt_min_nb_pixels=True,
-                        apply_doubt_marker_specific=False)
-                _add_gt_conclusions(df_parcel_gt, 'pred_cons_no_marker_specific')
+                    pred_df=df_parcel_gt,
+                    new_pred_column="pred_cons_no_marker_specific",
+                    apply_doubt_pct_proba=True,
+                    apply_doubt_min_nb_pixels=True,
+                    apply_doubt_marker_specific=False,
+                )
+                _add_gt_conclusions(df_parcel_gt, "pred_cons_no_marker_specific")
 
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_pred_cons_no_marker_specific"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn=conf.columns['class_declared'],
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=alpha_numerator_columns,
-                        error_codes_denominator=alpha_denominator_columns,
-                        error_type='alpha')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn=conf.columns["class_declared"],
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=alpha_numerator_columns,
+                    error_codes_denominator=alpha_denominator_columns,
+                    error_type="alpha",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_ALPHA_PER_CLASS_TABLE'] = df_per_column.to_html()
-                
-                # BETA errors 
+                    html_data[
+                        "PREDICTION_QUALITY_ALPHA_PER_CLASS_TABLE"
+                    ] = df_per_column.to_html()
+
+                # BETA errors
                 message = f"Number of ERROR_BETA parcels per declared cropclass for the ground truth parcels without applying crop/class based doubt:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_BETA_PER_CLASS_TEXT'] = message
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_BETA_PER_CLASS_TEXT"] = message
 
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_pred_cons_no_marker_specific"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn=conf.columns['class_declared'],
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=beta_numerator_columns,
-                        error_codes_denominator=beta_denominator_columns,
-                        error_type='beta')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn=conf.columns["class_declared"],
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=beta_numerator_columns,
+                    error_codes_denominator=beta_denominator_columns,
+                    error_type="beta",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_BETA_PER_CLASS_TABLE'] = df_per_column.to_html()
+                    html_data[
+                        "PREDICTION_QUALITY_BETA_PER_CLASS_TABLE"
+                    ] = df_per_column.to_html()
 
             # If crop is available, write the number of ALFA errors per cropclass (for the prediction with doubt)
-            if conf.columns['crop_declared'] in df_parcel_gt.columns:
-                # ALPHA errors 
+            if conf.columns["crop_declared"] in df_parcel_gt.columns:
+                # ALPHA errors
                 message = f"Number of ERROR_ALPHA parcels per declared crop for the ground truth parcels, without applying marker specific doubt:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_ALPHA_PER_CROP_TEXT'] = message
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_ALPHA_PER_CROP_TEXT"] = message
 
                 # For crop report, use error conclusions without marker specific stuff
                 class_postpr.add_doubt_column(
-                        pred_df=df_parcel_gt, 
-                        new_pred_column='pred_cons_no_marker_specific',
-                        apply_doubt_pct_proba=True,
-                        apply_doubt_min_nb_pixels=True,
-                        apply_doubt_marker_specific=False)
-                _add_gt_conclusions(df_parcel_gt, 'pred_cons_no_marker_specific')
+                    pred_df=df_parcel_gt,
+                    new_pred_column="pred_cons_no_marker_specific",
+                    apply_doubt_pct_proba=True,
+                    apply_doubt_min_nb_pixels=True,
+                    apply_doubt_marker_specific=False,
+                )
+                _add_gt_conclusions(df_parcel_gt, "pred_cons_no_marker_specific")
 
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_pred_cons_no_marker_specific"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn=conf.columns['crop_declared'],
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=alpha_numerator_columns,
-                        error_codes_denominator=alpha_denominator_columns,
-                        error_type='alpha')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn=conf.columns["crop_declared"],
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=alpha_numerator_columns,
+                    error_codes_denominator=alpha_denominator_columns,
+                    error_type="alpha",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_ALPHA_PER_CROP_TABLE'] = df_per_column.to_html()
+                    html_data[
+                        "PREDICTION_QUALITY_ALPHA_PER_CROP_TABLE"
+                    ] = df_per_column.to_html()
 
-                # BETA errors 
+                # BETA errors
                 message = f"Number of ERROR_BETA parcels per declared crop for the ground truth parcels, without applying marker specific doubt:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_BETA_PER_CROP_TEXT'] = message
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_BETA_PER_CROP_TEXT"] = message
 
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_pred_cons_no_marker_specific"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn=conf.columns['crop_declared'],
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=beta_numerator_columns,
-                        error_codes_denominator=beta_denominator_columns,
-                        error_type='beta')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn=conf.columns["crop_declared"],
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=beta_numerator_columns,
+                    error_codes_denominator=beta_denominator_columns,
+                    error_type="beta",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_BETA_PER_CROP_TABLE'] = df_per_column.to_html()
+                    html_data[
+                        "PREDICTION_QUALITY_BETA_PER_CROP_TABLE"
+                    ] = df_per_column.to_html()
 
             # If probability is available, write the number of ALFA errors per probability (for the prediction with doubt)
-            if 'pred1_prob' in df_parcel_gt.columns:
-                # ALPHA errors 
+            if "pred1_prob" in df_parcel_gt.columns:
+                # ALPHA errors
                 message = f"Number of ERROR_ALFA parcels per % probability for the ground truth parcels, without doubt based on probability:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TEXT'] = message
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TEXT"] = message
 
                 # For pixcount report, use error conclusions without doubt reasons
                 # that use the pct probability + round the probabilities
                 class_postpr.add_doubt_column(
-                        pred_df=df_parcel_gt, 
-                        new_pred_column='pred_cons_no_pct_prob',
-                        apply_doubt_pct_proba=False,
-                        apply_doubt_min_nb_pixels=True,
-                        apply_doubt_marker_specific=True)
-                _add_gt_conclusions(df_parcel_gt, 'pred_cons_no_pct_prob')
-                df_parcel_gt['pred1_prob_rounded'] = df_parcel_gt['pred1_prob'].round(2)
+                    pred_df=df_parcel_gt,
+                    new_pred_column="pred_cons_no_pct_prob",
+                    apply_doubt_pct_proba=False,
+                    apply_doubt_min_nb_pixels=True,
+                    apply_doubt_marker_specific=True,
+                )
+                _add_gt_conclusions(df_parcel_gt, "pred_cons_no_pct_prob")
+                df_parcel_gt["pred1_prob_rounded"] = df_parcel_gt["pred1_prob"].round(2)
 
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_" + "pred_cons_no_pct_prob"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn='pred1_prob_rounded',
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=alpha_numerator_columns,
-                        error_codes_denominator=alpha_denominator_columns,
-                        ascending=False,
-                        error_type='alpha')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn="pred1_prob_rounded",
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=alpha_numerator_columns,
+                    error_codes_denominator=alpha_denominator_columns,
+                    ascending=False,
+                    error_type="alpha",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TABLE'] = df_per_column.to_html()
+                    html_data[
+                        "PREDICTION_QUALITY_ALPHA_PER_PROBABILITY_TABLE"
+                    ] = df_per_column.to_html()
 
-                # BETA errors 
+                # BETA errors
                 message = f"Number of ERROR_BETA parcels per % probability for the ground truth parcels, without doubt based on probability:"
-                outputfile.write(f"\n{message}\n")            
-                html_data['PREDICTION_QUALITY_BETA_PER_PROBABILITY_TEXT'] = message
+                outputfile.write(f"\n{message}\n")
+                html_data["PREDICTION_QUALITY_BETA_PER_PROBABILITY_TEXT"] = message
 
                 # Calc data and write
                 pred_quality_column = "gt_conclusion_" + "pred_cons_no_pct_prob"
                 df_per_column = _get_errors_per_column(
-                        groupbycolumn='pred1_prob_rounded',
-                        df_predquality=df_parcel_gt,
-                        pred_quality_column=pred_quality_column,
-                        pred_quality_full_doubt_column=pred_quality_full_doubt_column,
-                        error_codes_numerator=beta_numerator_columns,
-                        error_codes_denominator=beta_denominator_columns,
-                        ascending=False,
-                        error_type='beta')
-                #df_per_column.dropna(inplace=True)
-                with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', 2000):                    
+                    groupbycolumn="pred1_prob_rounded",
+                    df_predquality=df_parcel_gt,
+                    pred_quality_column=pred_quality_column,
+                    pred_quality_full_doubt_column=pred_quality_full_doubt_column,
+                    error_codes_numerator=beta_numerator_columns,
+                    error_codes_denominator=beta_denominator_columns,
+                    ascending=False,
+                    error_type="beta",
+                )
+                # df_per_column.dropna(inplace=True)
+                with pd.option_context(
+                    "display.max_rows",
+                    None,
+                    "display.max_columns",
+                    None,
+                    "display.width",
+                    2000,
+                ):
                     outputfile.write(f"\n{df_per_column}\n")
                     logger.info(f"{df_per_column}\n")
-                    html_data['PREDICTION_QUALITY_BETA_PER_PROBABILITY_TABLE'] = df_per_column.to_html()
+                    html_data[
+                        "PREDICTION_QUALITY_BETA_PER_PROBABILITY_TABLE"
+                    ] = df_per_column.to_html()
 
-    with open(output_report_html, 'w') as outputfile:
+    with open(output_report_html, "w") as outputfile:
         script_dir = Path(__file__).resolve().parent
-        html_template_path = script_dir / 'html_rapport_template.html'
-        html_template_file = open(html_template_path).read()                        
+        html_template_path = script_dir / "html_rapport_template.html"
+        html_template_file = open(html_template_path).read()
         src = Template(html_template_file)
         # replace strings and write to file
         output = src.substitute(html_data)
         outputfile.write(output)
 
-def _get_confusion_matrix_ext(df_predict, prediction_column_to_use: str):
-    """ Returns a dataset with an extended confusion matrix. """
 
-    classes = sorted(np.unique(np.append(df_predict[prediction_column_to_use],
-                                         df_predict[conf.columns['class']])))
+def _get_confusion_matrix_ext(df_predict, prediction_column_to_use: str):
+    """Returns a dataset with an extended confusion matrix."""
+
+    classes = sorted(
+        np.unique(
+            np.append(
+                df_predict[prediction_column_to_use], df_predict[conf.columns["class"]]
+            )
+        )
+    )
     logger.debug(f"Input shape: {df_predict.shape}, Unique classes found: {classes}")
 
     # Calculate standard confusion matrix
-    np_confmatrix = skmetrics.confusion_matrix(df_predict[conf.columns['class']],
-                                               df_predict[prediction_column_to_use],
-                                               labels=classes)
+    np_confmatrix = skmetrics.confusion_matrix(
+        df_predict[conf.columns["class"]],
+        df_predict[prediction_column_to_use],
+        labels=classes,
+    )
     df_confmatrix_ext = pd.DataFrame(np_confmatrix, classes, classes)
 
     # Add some more columns to the confusion matrix
     # Insert column with the total nb of parcel for each class that were the input values
     # (=sum of the row of each class)
     values = df_confmatrix_ext[df_confmatrix_ext.columns].sum(axis=1)
-    df_confmatrix_ext.insert(loc=0, column='nb_input', value=values)
+    df_confmatrix_ext.insert(loc=0, column="nb_input", value=values)
     # Insert column with the total nb of parcel for each class that were predicted to have
     # this value (=sum of the column of each class)
     values = df_confmatrix_ext[df_confmatrix_ext.columns].sum(axis=0)
-    df_confmatrix_ext.insert(loc=1, column='nb_predicted', value=values)
+    df_confmatrix_ext.insert(loc=1, column="nb_predicted", value=values)
     # Insert column with the total nb of correctly predicted classes
-    df_confmatrix_ext.insert(loc=2, column='nb_predicted_correct', value=0)
+    df_confmatrix_ext.insert(loc=2, column="nb_predicted_correct", value=0)
     for column in df_confmatrix_ext.columns:
-        if column not in ['nb_input', 'nb_predicted', 'nb_predicted_correct']:
-            df_confmatrix_ext['nb_predicted_correct'][column] = df_confmatrix_ext[column][column]
+        if column not in ["nb_input", "nb_predicted", "nb_predicted_correct"]:
+            df_confmatrix_ext["nb_predicted_correct"][column] = df_confmatrix_ext[
+                column
+            ][column]
     # Insert column with the total nb of parcel for each class that were predicted to have
     # this value (=sum of the column of each class)
-    values = df_confmatrix_ext['nb_predicted'] - df_confmatrix_ext['nb_predicted_correct']
-    df_confmatrix_ext.insert(loc=3, column='nb_predicted_wrong', value=values)
+    values = (
+        df_confmatrix_ext["nb_predicted"] - df_confmatrix_ext["nb_predicted_correct"]
+    )
+    df_confmatrix_ext.insert(loc=3, column="nb_predicted_wrong", value=values)
     # Insert columns with percentages
-    values = df_confmatrix_ext['nb_predicted_correct']*100/df_confmatrix_ext['nb_input']
-    df_confmatrix_ext.insert(loc=4, column='pct_predicted_correct', value=values)
-    values = ((df_confmatrix_ext['nb_predicted'] - df_confmatrix_ext['nb_predicted_correct'])
-              * 100 / df_confmatrix_ext['nb_input'])
-    df_confmatrix_ext.insert(loc=5, column='pct_predicted_wrong', value=values)
+    values = (
+        df_confmatrix_ext["nb_predicted_correct"] * 100 / df_confmatrix_ext["nb_input"]
+    )
+    df_confmatrix_ext.insert(loc=4, column="pct_predicted_correct", value=values)
+    values = (
+        (df_confmatrix_ext["nb_predicted"] - df_confmatrix_ext["nb_predicted_correct"])
+        * 100
+        / df_confmatrix_ext["nb_input"]
+    )
+    df_confmatrix_ext.insert(loc=5, column="pct_predicted_wrong", value=values)
 
     return df_confmatrix_ext
 
-def _add_prediction_conclusion(in_df,
-                               new_columnname, 
-                               prediction_column_to_use,
-                               detailed: bool):
+
+def _add_prediction_conclusion(
+    in_df, new_columnname, prediction_column_to_use, detailed: bool
+):
     """
-    Calculate the "conclusions" for the predictions 
+    Calculate the "conclusions" for the predictions
 
     REMARK: calculating it like this, using native pandas operations, is 300 times faster than
             using DataFrame.apply() with a function!!!
     """
     # Get a lists of the classes to ignore
-    all_classes_to_ignore = (conf.marker.getlist('classes_to_ignore_for_train') 
-                             + conf.marker.getlist('classes_to_ignore'))
-    classes_to_ignore_unimportant = conf.marker.getlist('classes_to_ignore_unimportant') 
+    all_classes_to_ignore = conf.marker.getlist(
+        "classes_to_ignore_for_train"
+    ) + conf.marker.getlist("classes_to_ignore")
+    classes_to_ignore_unimportant = conf.marker.getlist("classes_to_ignore_unimportant")
+
+    # Add the new column with a fixed value first
+    in_df[new_columnname] = "UNDEFINED"
 
-    # Add the new column with a fixed value first 
-    in_df[new_columnname] = 'UNDEFINED'    
-    
     # Some conclusions are different if detailed info is asked...
     if detailed == True:
         # The classes that are defined as unimportant
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']].isin(classes_to_ignore_unimportant)),
-                  new_columnname] = 'IGNORE_UNIMPORTANT:INPUTCLASSNAME=' + in_df[conf.columns['class']].map(str)
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (
+                in_df[conf.columns["class_declared"]].isin(
+                    classes_to_ignore_unimportant
+                )
+            ),
+            new_columnname,
+        ] = "IGNORE_UNIMPORTANT:INPUTCLASSNAME=" + in_df[conf.columns["class"]].map(str)
         # Parcels that were ignored for trainig and/or prediction, get an ignore conclusion
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']].isin(all_classes_to_ignore)),
-                  new_columnname] = 'IGNORE:INPUTCLASSNAME=' + in_df[conf.columns['class']].map(str)
-        # If conclusion still UNDEFINED, check if doubt 
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use].str.startswith('DOUBT')),
-                  new_columnname] = 'DOUBT:REASON=' + in_df[prediction_column_to_use].map(str)
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use] == 'NODATA'),
-                  new_columnname] = 'DOUBT:REASON=' + in_df[prediction_column_to_use].map(str)
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (in_df[conf.columns["class_declared"]].isin(all_classes_to_ignore)),
+            new_columnname,
+        ] = "IGNORE:INPUTCLASSNAME=" + in_df[conf.columns["class"]].map(str)
+        # If conclusion still UNDEFINED, check if doubt
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (in_df[prediction_column_to_use].str.startswith("DOUBT")),
+            new_columnname,
+        ] = "DOUBT:REASON=" + in_df[prediction_column_to_use].map(str)
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (in_df[prediction_column_to_use] == "NODATA"),
+            new_columnname,
+        ] = "DOUBT:REASON=" + in_df[prediction_column_to_use].map(str)
     else:
         # The classes that are defined as unimportant
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']].isin(classes_to_ignore_unimportant)),
-                  new_columnname] = 'IGNORE_UNIMPORTANT'
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (
+                in_df[conf.columns["class_declared"]].isin(
+                    classes_to_ignore_unimportant
+                )
+            ),
+            new_columnname,
+        ] = "IGNORE_UNIMPORTANT"
         # Parcels that were ignored for trainig and/or prediction, get an ignore conclusion
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']].isin(all_classes_to_ignore)),
-                  new_columnname] = 'IGNORE'
-        # If conclusion still UNDEFINED, check if doubt 
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use].str.startswith('DOUBT')),
-                  new_columnname] = 'DOUBT'
-        # If conclusion still UNDEFINED, check if doubt 
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use].str.startswith('RISKY_DOUBT')),
-                  new_columnname] = 'RISKY_DOUBT'
-        in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use] == 'NODATA'),
-                  new_columnname] = 'DOUBT'
-
-    # If conclusion still UNDEFINED, check if prediction equals the input class 
-    in_df.loc[(in_df[new_columnname] == 'UNDEFINED')
-                & (in_df[conf.columns['class_declared']] == in_df[prediction_column_to_use]),
-              new_columnname] = 'OK:PREDICTION=INPUT_CLASS'
-    # If conclusion still UNDEFINED, prediction is different from input 
-    in_df.loc[in_df[new_columnname] == 'UNDEFINED',
-              new_columnname] = 'NOK:PREDICTION<>INPUT_CLASS'
-
-def _add_gt_conclusions(in_df, 
-                        prediction_column_to_use):
-    """ Add some columns with groundtruth conclusions. """
-    
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (in_df[conf.columns["class_declared"]].isin(all_classes_to_ignore)),
+            new_columnname,
+        ] = "IGNORE"
+        # If conclusion still UNDEFINED, check if doubt
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (in_df[prediction_column_to_use].str.startswith("DOUBT")),
+            new_columnname,
+        ] = "DOUBT"
+        # If conclusion still UNDEFINED, check if doubt
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (in_df[prediction_column_to_use].str.startswith("RISKY_DOUBT")),
+            new_columnname,
+        ] = "RISKY_DOUBT"
+        in_df.loc[
+            (in_df[new_columnname] == "UNDEFINED")
+            & (in_df[prediction_column_to_use] == "NODATA"),
+            new_columnname,
+        ] = "DOUBT"
+
+    # If conclusion still UNDEFINED, check if prediction equals the input class
+    in_df.loc[
+        (in_df[new_columnname] == "UNDEFINED")
+        & (in_df[conf.columns["class_declared"]] == in_df[prediction_column_to_use]),
+        new_columnname,
+    ] = "OK:PREDICTION=INPUT_CLASS"
+    # If conclusion still UNDEFINED, prediction is different from input
+    in_df.loc[
+        in_df[new_columnname] == "UNDEFINED", new_columnname
+    ] = "NOK:PREDICTION<>INPUT_CLASS"
+
+
+def _add_gt_conclusions(in_df, prediction_column_to_use):
+    """Add some columns with groundtruth conclusions."""
+
     # Add the new column with a fixed value first
     gt_vs_input_column = f"gt_vs_input_{prediction_column_to_use}"
     gt_vs_prediction_column = f"gt_vs_prediction_{prediction_column_to_use}"
     gt_conclusion_column = f"gt_conclusion_{prediction_column_to_use}"
-    all_classes_to_ignore = (conf.marker.getlist('classes_to_ignore_for_train') 
-                                + conf.marker.getlist('classes_to_ignore'))
+    all_classes_to_ignore = conf.marker.getlist(
+        "classes_to_ignore_for_train"
+    ) + conf.marker.getlist("classes_to_ignore")
 
     # Calculate gt_vs_input_column
     # If ground truth same as input class, farmer OK, unless it is an ignore class
-    in_df[gt_vs_input_column] = 'UNDEFINED'
-    in_df.loc[(in_df[gt_vs_input_column] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']] == in_df[conf.columns['class_groundtruth']])
-                    & (in_df[conf.columns['class_groundtruth']].isin(all_classes_to_ignore)),
-              gt_vs_input_column] = 'FARMER-CORRECT:IGNORE:DECLARED=GROUNDTRUTH=' + in_df[conf.columns['class_groundtruth']].map(str)
-    in_df.loc[(in_df[gt_vs_input_column] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']] == in_df[conf.columns['class_groundtruth']]),
-              gt_vs_input_column] = 'FARMER-CORRECT'
-    in_df.loc[(in_df[gt_vs_input_column] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']].isin(all_classes_to_ignore)),
-              gt_vs_input_column] = 'FARMER-WRONG:IGNORE:DECLARED=' + in_df[conf.columns['class_declared']].map(str)
-    in_df.loc[(in_df[gt_vs_input_column] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_groundtruth']].isin(all_classes_to_ignore)),
-              gt_vs_input_column] = 'FARMER-WRONG:IGNORE:GROUNDTRUTH=' + in_df[conf.columns['class_groundtruth']].map(str)
-
-    # If conclusion still UNDEFINED, farmer was simply wrong 
-    in_df.loc[in_df[gt_vs_input_column] == 'UNDEFINED',
-              gt_vs_input_column] = 'FARMER-WRONG'
+    in_df[gt_vs_input_column] = "UNDEFINED"
+    in_df.loc[
+        (in_df[gt_vs_input_column] == "UNDEFINED")
+        & (
+            in_df[conf.columns["class_declared"]]
+            == in_df[conf.columns["class_groundtruth"]]
+        )
+        & (in_df[conf.columns["class_groundtruth"]].isin(all_classes_to_ignore)),
+        gt_vs_input_column,
+    ] = "FARMER-CORRECT:IGNORE:DECLARED=GROUNDTRUTH=" + in_df[
+        conf.columns["class_groundtruth"]
+    ].map(
+        str
+    )
+    in_df.loc[
+        (in_df[gt_vs_input_column] == "UNDEFINED")
+        & (
+            in_df[conf.columns["class_declared"]]
+            == in_df[conf.columns["class_groundtruth"]]
+        ),
+        gt_vs_input_column,
+    ] = "FARMER-CORRECT"
+    in_df.loc[
+        (in_df[gt_vs_input_column] == "UNDEFINED")
+        & (in_df[conf.columns["class_declared"]].isin(all_classes_to_ignore)),
+        gt_vs_input_column,
+    ] = "FARMER-WRONG:IGNORE:DECLARED=" + in_df[conf.columns["class_declared"]].map(str)
+    in_df.loc[
+        (in_df[gt_vs_input_column] == "UNDEFINED")
+        & (in_df[conf.columns["class_groundtruth"]].isin(all_classes_to_ignore)),
+        gt_vs_input_column,
+    ] = "FARMER-WRONG:IGNORE:GROUNDTRUTH=" + in_df[
+        conf.columns["class_groundtruth"]
+    ].map(
+        str
+    )
+
+    # If conclusion still UNDEFINED, farmer was simply wrong
+    in_df.loc[
+        in_df[gt_vs_input_column] == "UNDEFINED", gt_vs_input_column
+    ] = "FARMER-WRONG"
 
     # Calculate gt_vs_prediction_column
-    # If ground truth same as prediction, prediction OK 
-    in_df[gt_vs_prediction_column] = 'UNDEFINED'
-    in_df.loc[(in_df[prediction_column_to_use] == in_df[conf.columns['class_groundtruth']]),
-              gt_vs_prediction_column] = 'PRED-CORRECT'
-    in_df.loc[(in_df[prediction_column_to_use] == in_df[conf.columns['class_groundtruth']])
-                    & (in_df[prediction_column_to_use].isin(all_classes_to_ignore)),
-              gt_vs_prediction_column] = 'PRED-CORRECT:IGNORE:PREDICTION=GROUNDTRUTH=' + in_df[prediction_column_to_use].map(str)                
+    # If ground truth same as prediction, prediction OK
+    in_df[gt_vs_prediction_column] = "UNDEFINED"
+    in_df.loc[
+        (in_df[prediction_column_to_use] == in_df[conf.columns["class_groundtruth"]]),
+        gt_vs_prediction_column,
+    ] = "PRED-CORRECT"
+    in_df.loc[
+        (in_df[prediction_column_to_use] == in_df[conf.columns["class_groundtruth"]])
+        & (in_df[prediction_column_to_use].isin(all_classes_to_ignore)),
+        gt_vs_prediction_column,
+    ] = "PRED-CORRECT:IGNORE:PREDICTION=GROUNDTRUTH=" + in_df[
+        prediction_column_to_use
+    ].map(
+        str
+    )
 
     # If declared class in ignored for trainig and/or prediction: an ignore conclusion
-    in_df.loc[(in_df[gt_vs_prediction_column] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_declared']].isin(all_classes_to_ignore)),
-              gt_vs_prediction_column] = 'PRED-WRONG:IGNORE:DECLARED=' + in_df[conf.columns['class_declared']].map(str)
-
-    # If conclusion still UNDEFINED, check if DOUBT 
-    in_df.loc[(in_df[gt_vs_prediction_column] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use].str.startswith('DOUBT')),
-              gt_vs_prediction_column] = 'PRED-DOUBT:REASON=' + in_df[prediction_column_to_use].map(str)
-    in_df.loc[(in_df[gt_vs_prediction_column] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use] == 'NODATA'),
-              gt_vs_prediction_column] = 'PRED-DOUBT:REASON=' + in_df[prediction_column_to_use].map(str)              
-
-    # If conclusion still UNDEFINED, check if RISKY_DOUBT 
-    in_df.loc[(in_df[gt_vs_prediction_column] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use].str.startswith('RISKY_DOUBT')),
-              gt_vs_prediction_column] = 'PRED-RISKY_DOUBT:REASON=' + in_df[prediction_column_to_use].map(str)
-    in_df.loc[(in_df[gt_vs_prediction_column] == 'UNDEFINED')
-                    & (in_df[prediction_column_to_use] == 'NODATA'),
-              gt_vs_prediction_column] = 'PRED-RISKY_DOUBT:REASON=' + in_df[prediction_column_to_use].map(str)              
+    in_df.loc[
+        (in_df[gt_vs_prediction_column] == "UNDEFINED")
+        & (in_df[conf.columns["class_declared"]].isin(all_classes_to_ignore)),
+        gt_vs_prediction_column,
+    ] = "PRED-WRONG:IGNORE:DECLARED=" + in_df[conf.columns["class_declared"]].map(str)
+
+    # If conclusion still UNDEFINED, check if DOUBT
+    in_df.loc[
+        (in_df[gt_vs_prediction_column] == "UNDEFINED")
+        & (in_df[prediction_column_to_use].str.startswith("DOUBT")),
+        gt_vs_prediction_column,
+    ] = "PRED-DOUBT:REASON=" + in_df[prediction_column_to_use].map(str)
+    in_df.loc[
+        (in_df[gt_vs_prediction_column] == "UNDEFINED")
+        & (in_df[prediction_column_to_use] == "NODATA"),
+        gt_vs_prediction_column,
+    ] = "PRED-DOUBT:REASON=" + in_df[prediction_column_to_use].map(str)
+
+    # If conclusion still UNDEFINED, check if RISKY_DOUBT
+    in_df.loc[
+        (in_df[gt_vs_prediction_column] == "UNDEFINED")
+        & (in_df[prediction_column_to_use].str.startswith("RISKY_DOUBT")),
+        gt_vs_prediction_column,
+    ] = "PRED-RISKY_DOUBT:REASON=" + in_df[prediction_column_to_use].map(str)
+    in_df.loc[
+        (in_df[gt_vs_prediction_column] == "UNDEFINED")
+        & (in_df[prediction_column_to_use] == "NODATA"),
+        gt_vs_prediction_column,
+    ] = "PRED-RISKY_DOUBT:REASON=" + in_df[prediction_column_to_use].map(str)
 
     # If groundtruth class in ignored for trainig and/or prediction: an ignore conclusion
-    in_df.loc[(in_df[gt_vs_prediction_column] == 'UNDEFINED')
-                    & (in_df[conf.columns['class_groundtruth']].isin(all_classes_to_ignore)),
-              gt_vs_prediction_column] = 'PRED-WRONG:IGNORE:GROUNDTRUTH=' + in_df[conf.columns['class_groundtruth']].map(str)
-
-    # If conclusion still UNDEFINED, it was wrong 
-    in_df.loc[in_df[gt_vs_prediction_column] == 'UNDEFINED',
-              gt_vs_prediction_column] = 'PRED-WRONG'
+    in_df.loc[
+        (in_df[gt_vs_prediction_column] == "UNDEFINED")
+        & (in_df[conf.columns["class_groundtruth"]].isin(all_classes_to_ignore)),
+        gt_vs_prediction_column,
+    ] = "PRED-WRONG:IGNORE:GROUNDTRUTH=" + in_df[conf.columns["class_groundtruth"]].map(
+        str
+    )
+
+    # If conclusion still UNDEFINED, it was wrong
+    in_df.loc[
+        in_df[gt_vs_prediction_column] == "UNDEFINED", gt_vs_prediction_column
+    ] = "PRED-WRONG"
 
     # Calculate gt_conclusion_column
-    # Declared class was correct  
-    in_df[gt_conclusion_column] = 'UNDEFINED'
-    in_df.loc[(in_df[gt_vs_input_column] == 'FARMER-CORRECT')
-                    & (in_df[gt_vs_prediction_column] == 'PRED-WRONG'),
-              gt_conclusion_column] = 'FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA'
-    in_df.loc[(in_df[gt_conclusion_column] == 'UNDEFINED')
-                    & (in_df[gt_vs_input_column] == 'FARMER-CORRECT'),
-                gt_conclusion_column] = 'FARMER-CORRECT_' + in_df[gt_vs_prediction_column].map(str)
+    # Declared class was correct
+    in_df[gt_conclusion_column] = "UNDEFINED"
+    in_df.loc[
+        (in_df[gt_vs_input_column] == "FARMER-CORRECT")
+        & (in_df[gt_vs_prediction_column] == "PRED-WRONG"),
+        gt_conclusion_column,
+    ] = "FARMER-CORRECT_PRED-WRONG:ERROR_ALPHA"
+    in_df.loc[
+        (in_df[gt_conclusion_column] == "UNDEFINED")
+        & (in_df[gt_vs_input_column] == "FARMER-CORRECT"),
+        gt_conclusion_column,
+    ] = "FARMER-CORRECT_" + in_df[gt_vs_prediction_column].map(str)
 
     # Declared class was not correct
-    in_df.loc[(in_df[gt_conclusion_column] == 'UNDEFINED')
-                    & (in_df[gt_vs_input_column] == 'FARMER-WRONG')
-                    & (in_df[conf.columns['class_declared']] == in_df[prediction_column_to_use]),
-              gt_conclusion_column] = 'FARMER-WRONG_PRED-DOESNT_OPPOSE:ERROR_BETA'
-    in_df.loc[(in_df[gt_conclusion_column] == 'UNDEFINED')
-                    & (in_df[gt_vs_input_column] == 'FARMER-WRONG'),
-              gt_conclusion_column] = 'FARMER-WRONG_' + in_df[gt_vs_prediction_column].map(str)
-    
+    in_df.loc[
+        (in_df[gt_conclusion_column] == "UNDEFINED")
+        & (in_df[gt_vs_input_column] == "FARMER-WRONG")
+        & (in_df[conf.columns["class_declared"]] == in_df[prediction_column_to_use]),
+        gt_conclusion_column,
+    ] = "FARMER-WRONG_PRED-DOESNT_OPPOSE:ERROR_BETA"
+    in_df.loc[
+        (in_df[gt_conclusion_column] == "UNDEFINED")
+        & (in_df[gt_vs_input_column] == "FARMER-WRONG"),
+        gt_conclusion_column,
+    ] = "FARMER-WRONG_" + in_df[gt_vs_prediction_column].map(str)
+
     # Declared or groundtruth class was ignore
-    in_df.loc[(in_df[gt_conclusion_column] == 'UNDEFINED'),
-              gt_conclusion_column] = in_df[gt_vs_input_column].map(str)
-         
+    in_df.loc[
+        (in_df[gt_conclusion_column] == "UNDEFINED"), gt_conclusion_column
+    ] = in_df[gt_vs_input_column].map(str)
+
+
 def _get_errors_per_column(
-            groupbycolumn: str,
-            df_predquality,
-            pred_quality_column: str,
-            pred_quality_full_doubt_column: str,
-            error_codes_numerator: List[str],
-            error_codes_denominator: List[str],
-            error_type: str,
-            ascending: bool = True):
-    """ Returns a dataset with detailed information about the number of alfa errors per column that was passed on"""
+    groupbycolumn: str,
+    df_predquality,
+    pred_quality_column: str,
+    pred_quality_full_doubt_column: str,
+    error_codes_numerator: List[str],
+    error_codes_denominator: List[str],
+    error_type: str,
+    ascending: bool = True,
+):
+    """Returns a dataset with detailed information about the number of alfa errors per column that was passed on"""
 
     # First filter on the parcels we need to calculate the pct alpha errors
     df_predquality_filtered = df_predquality[
-            df_predquality[pred_quality_column].isin(error_codes_denominator)]
+        df_predquality[pred_quality_column].isin(error_codes_denominator)
+    ]
 
     # Calculate the number of parcels per groupbycolumn, the cumulative sum + the pct of all
-    df_predquality_count = (df_predquality_filtered
-            .groupby(groupbycolumn).size().to_frame('count_all'))
+    df_predquality_count = (
+        df_predquality_filtered.groupby(groupbycolumn).size().to_frame("count_all")
+    )
     df_predquality_count.sort_index(ascending=ascending, inplace=True)
-    values = df_predquality_count['count_all'].cumsum(axis=0)
-    df_predquality_count.insert(loc=len(df_predquality_count.columns),
-                                column='count_all_cumulative',
-                                value=values)
-    values = (100 * df_predquality_count['count_all_cumulative']
-              / df_predquality_count['count_all'].sum())
-    df_predquality_count.insert(loc=len(df_predquality_count.columns),
-                                column='pct_all_cumulative',
-                                value=values)
+    values = df_predquality_count["count_all"].cumsum(axis=0)
+    df_predquality_count.insert(
+        loc=len(df_predquality_count.columns),
+        column="count_all_cumulative",
+        value=values,
+    )
+    values = (
+        100
+        * df_predquality_count["count_all_cumulative"]
+        / df_predquality_count["count_all"].sum()
+    )
+    df_predquality_count.insert(
+        loc=len(df_predquality_count.columns), column="pct_all_cumulative", value=values
+    )
 
     # Now calculate the number of alfa errors per groupbycolumn
     df_alfa_error = df_predquality_filtered[
-            df_predquality_filtered[pred_quality_column].isin(error_codes_numerator)]
-    df_alfa_per_column = (df_alfa_error
-            .groupby(groupbycolumn).size().to_frame(f"count_error_{error_type}")) 
-    df_alfa_per_column.sort_index(ascending=ascending, inplace=True)                         
-
-    # Now calculate the number of alfa errors with full doubt per groupbycolumn 
+        df_predquality_filtered[pred_quality_column].isin(error_codes_numerator)
+    ]
+    df_alfa_per_column = (
+        df_alfa_error.groupby(groupbycolumn)
+        .size()
+        .to_frame(f"count_error_{error_type}")
+    )
+    df_alfa_per_column.sort_index(ascending=ascending, inplace=True)
+
+    # Now calculate the number of alfa errors with full doubt per groupbycolumn
     df_alfa_error_full_doubt = df_predquality_filtered[
-            df_predquality_filtered[pred_quality_full_doubt_column].isin(error_codes_numerator)]
-    df_alfa_full_doubt_per_column = (df_alfa_error_full_doubt
-            .groupby(groupbycolumn).size().to_frame(f"count_error_{error_type}_full_doubt"))
-   
+        df_predquality_filtered[pred_quality_full_doubt_column].isin(
+            error_codes_numerator
+        )
+    ]
+    df_alfa_full_doubt_per_column = (
+        df_alfa_error_full_doubt.groupby(groupbycolumn)
+        .size()
+        .to_frame(f"count_error_{error_type}_full_doubt")
+    )
+
     # Join everything together
-    df_alfa_per_column = df_predquality_count.join(df_alfa_per_column, how='left')
-    df_alfa_per_column = df_alfa_per_column.join(df_alfa_full_doubt_per_column, how='left')
+    df_alfa_per_column = df_predquality_count.join(df_alfa_per_column, how="left")
+    df_alfa_per_column = df_alfa_per_column.join(
+        df_alfa_full_doubt_per_column, how="left"
+    )
 
-     # Now calculate the total number of parcels with full doubt applied per groupbycolumn
+    # Now calculate the total number of parcels with full doubt applied per groupbycolumn
     df_predquality_full_doubt_filtered = df_predquality[
-            df_predquality[pred_quality_full_doubt_column].isin(error_codes_denominator)]
-
-    values = (df_predquality_full_doubt_filtered
-            .groupby(groupbycolumn).size().to_frame('count_all_full_doubt'))
+        df_predquality[pred_quality_full_doubt_column].isin(error_codes_denominator)
+    ]
+
+    values = (
+        df_predquality_full_doubt_filtered.groupby(groupbycolumn)
+        .size()
+        .to_frame("count_all_full_doubt")
+    )
     df_alfa_per_column = pd.concat([df_alfa_per_column, values], axis=1)
- 
+
     # Finally calculate all alfa error percentages
-    values = df_alfa_per_column[f"count_error_{error_type}"].cumsum(axis=0).to_frame(f"count_error_{error_type}_cumulative")
-    df_alfa_per_column = pd.concat([df_alfa_per_column, values], axis=1)
-                                
-    values = (100 * df_alfa_per_column[f"count_error_{error_type}"] / df_alfa_per_column['count_all']).to_frame(f"pct_error_{error_type}_of_all")
-    #df_alfa_per_column.insert(loc=len(df_alfa_per_column.columns), column=f"pct_error_{error_type}_of_all", value=values)
+    values = (
+        df_alfa_per_column[f"count_error_{error_type}"]
+        .cumsum(axis=0)
+        .to_frame(f"count_error_{error_type}_cumulative")
+    )
     df_alfa_per_column = pd.concat([df_alfa_per_column, values], axis=1)
-    
-    values = ((100 * df_alfa_per_column[f"count_error_{error_type}_cumulative"] / df_alfa_per_column[f"count_error_{error_type}"].sum())).to_frame(f"pct_error_{error_type}_of_{error_type}_cumulative")
-    #df_alfa_per_column.insert(loc=len(df_alfa_per_column.columns), column=f"pct_error_{error_type}_of_{error_type}_cumulative", value=values) 
+
+    values = (
+        100
+        * df_alfa_per_column[f"count_error_{error_type}"]
+        / df_alfa_per_column["count_all"]
+    ).to_frame(f"pct_error_{error_type}_of_all")
+    # df_alfa_per_column.insert(loc=len(df_alfa_per_column.columns), column=f"pct_error_{error_type}_of_all", value=values)
     df_alfa_per_column = pd.concat([df_alfa_per_column, values], axis=1)
 
-    values = ((100 * df_alfa_per_column[f"count_error_{error_type}_cumulative"] / df_alfa_per_column['count_all'].sum())).to_frame(f"pct_error_{error_type}_of_all_cumulative")
-    #df_alfa_per_column.insert(loc=len(df_alfa_per_column.columns), column=f"pct_error_{error_type}_of_all_cumulative", value=values)
+    values = (
+        (
+            100
+            * df_alfa_per_column[f"count_error_{error_type}_cumulative"]
+            / df_alfa_per_column[f"count_error_{error_type}"].sum()
+        )
+    ).to_frame(f"pct_error_{error_type}_of_{error_type}_cumulative")
+    # df_alfa_per_column.insert(loc=len(df_alfa_per_column.columns), column=f"pct_error_{error_type}_of_{error_type}_cumulative", value=values)
     df_alfa_per_column = pd.concat([df_alfa_per_column, values], axis=1)
 
-    #MARINA
-    values = df_alfa_per_column[f"count_error_{error_type}_cumulative"] / df_alfa_per_column[f"count_all_cumulative"]
-    df_alfa_per_column.insert(loc=len(df_alfa_per_column.columns), column=f"new_column", value=values) 
+    values = (
+        (
+            100
+            * df_alfa_per_column[f"count_error_{error_type}_cumulative"]
+            / df_alfa_per_column["count_all"].sum()
+        )
+    ).to_frame(f"pct_error_{error_type}_of_all_cumulative")
+    # df_alfa_per_column.insert(loc=len(df_alfa_per_column.columns), column=f"pct_error_{error_type}_of_all_cumulative", value=values)
+    df_alfa_per_column = pd.concat([df_alfa_per_column, values], axis=1)
 
+    # MARINA
+    values = (
+        df_alfa_per_column[f"count_error_{error_type}_cumulative"]
+        / df_alfa_per_column[f"count_all_cumulative"]
+    )
+    df_alfa_per_column.insert(
+        loc=len(df_alfa_per_column.columns), column=f"new_column", value=values
+    )
 
     return df_alfa_per_column
 
-def _write_OA_per_pixcount(df_parcel_predictions: pd.DataFrame,
-                           output_report_txt: Path,
-                           force: bool = False):
-    """ Write a report of the overall accuracy that parcels per pixcount get. """
+
+def _write_OA_per_pixcount(
+    df_parcel_predictions: pd.DataFrame, output_report_txt: Path, force: bool = False
+):
+    """Write a report of the overall accuracy that parcels per pixcount get."""
     # If force == False Check and the output file exists already, stop.
     if force is False and output_report_txt.exists():
-        logger.warning(f"collect_and_prepare_timeseries_data: output file already exists and force == False, so stop: {output_report_txt}")
+        logger.warning(
+            f"collect_and_prepare_timeseries_data: output file already exists and force == False, so stop: {output_report_txt}"
+        )
         return
 
     # Write output...
     nb_predictions_total = len(df_parcel_predictions.index)
-    with open(output_report_txt, 'w') as outputfile:
+    with open(output_report_txt, "w") as outputfile:
         for i in range(40):
 
-            df_result_cur_pixcount = df_parcel_predictions[df_parcel_predictions[conf.columns['pixcount_s1s2']] == i]
+            df_result_cur_pixcount = df_parcel_predictions[
+                df_parcel_predictions[conf.columns["pixcount_s1s2"]] == i
+            ]
             nb_predictions_pixcount = len(df_result_cur_pixcount.index)
             if nb_predictions_pixcount == 0:
                 continue
 
-            overall_accuracy = 100.0*skmetrics.accuracy_score(df_result_cur_pixcount[conf.columns['class']], df_result_cur_pixcount['pred1'], normalize=True, sample_weight=None)
-            message = f"OA for pixcount {i:2}: {overall_accuracy:3.2f} %, with {nb_predictions_pixcount} elements ({100*(nb_predictions_pixcount/nb_predictions_total):.4f} % of {nb_predictions_total})"
+            overall_accuracy = 100.0 * skmetrics.accuracy_score(
+                df_result_cur_pixcount[conf.columns["class"]],
+                df_result_cur_pixcount["pred1"],
+                normalize=True,
+                sample_weight=None,
+            )
+            message = (
+                f"OA for pixcount {i:2}: {overall_accuracy:3.2f} %, with "
+                f"{nb_predictions_pixcount} elements "
+                f"({100*(nb_predictions_pixcount/nb_predictions_total):.4f} % "
+                f"of {nb_predictions_total})"
+            )
             logger.info(message)
             outputfile.write(f"{message}\n")
 
+
 # If the script is run directly...
 if __name__ == "__main__":
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/predict/classification.py b/cropclassification/predict/classification.py
index 7be89590..11c7f5e1 100644
--- a/cropclassification/predict/classification.py
+++ b/cropclassification/predict/classification.py
@@ -8,212 +8,286 @@
 import logging
 import os
 from pathlib import Path
+from typing import Optional
 
 import pandas as pd
 
 import cropclassification.helpers.config_helper as conf
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
-#logger.setLevel(logging.DEBUG)
+# logger.setLevel(logging.DEBUG)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
+
 
 def train_test_predict(
-        input_parcel_train_filepath: Path,
-        input_parcel_test_filepath: Path,
-        input_parcel_all_filepath: Path,
-        input_parcel_classification_data_filepath: Path,
-        output_classifier_basefilepath: Path,
-        output_predictions_test_filepath: Path,
-        output_predictions_all_filepath: Path,
-        force: bool = False):
-    """ Train a classifier, test it and do full predictions.
+    input_parcel_train_path: Path,
+    input_parcel_test_path: Path,
+    input_parcel_all_path: Path,
+    input_parcel_classification_data_path: Path,
+    output_classifier_basepath: Path,
+    output_predictions_test_path: Path,
+    output_predictions_all_path: Path,
+    force: bool = False,
+):
+    """Train a classifier, test it and do full predictions.
 
     Args
-        input_parcel_classes_train_filepath: the list of parcels with classes to train the classifier, without data!
-        input_parcel_classes_test_filepath: the list of parcels with classes to test the classifier, without data!
-        input_parcel_classes_all_filepath: the list of parcels with classes that need to be classified, without data!
-        input_parcel_classification_data_filepath: the data to be used for the classification for all parcels.
-        output_classifier_basefilepath: the file path where to save the classifier.
-        output_predictions_test_filepath: the file path where to save the test predictions.
-        output_predictions_all_filepath: the file path where to save the predictions for all parcels.
-        force: if True, overwrite all existing output files, if False, don't overwrite them.
+        input_parcel_classes_train_path: the list of parcels with classes to train the
+            classifier, without data!
+        input_parcel_classes_test_path: the list of parcels with classes to test the
+            classifier, without data!
+        input_parcel_classes_all_path: the list of parcels with classes that need to be
+            classified, without data!
+        input_parcel_classification_data_path: the data to be used for the
+            classification for all parcels.
+        output_classifier_basepath: the file path where to save the classifier.
+        output_predictions_test_path: the file path where to save the test predictions.
+        output_predictions_all_path: the file path where to save the predictions for
+            all parcels.
+        force: if True, overwrite all existing output files, if False, don't overwrite
+            them.
     """
 
     logger.info("train_test_predict: Start")
 
-    if(force is False
-       #and os.path.exists(output_classifier_basefilepath)
-       and output_predictions_test_filepath.exists()
-       and output_predictions_all_filepath.exists()):
-        logger.warning(f"predict: output files exist and force is False, so stop: {output_classifier_basefilepath}, {output_predictions_test_filepath}, {output_predictions_all_filepath}")
+    if (
+        force is False
+        # and os.path.exists(output_classifier_basepath)
+        and output_predictions_test_path.exists()
+        and output_predictions_all_path.exists()
+    ):
+        logger.warning(
+            "predict: output files exist and force is False, so stop: "
+            f"{output_classifier_basepath}, {output_predictions_test_path}, "
+            f"{output_predictions_all_path}"
+        )
         return
 
-    # Read the classification data from the csv so we can pass it on to the other functione to improve performance...
-    logger.info(f"Read classification data file: {input_parcel_classification_data_filepath}")
-    input_parcel_classification_data_df = pdh.read_file(input_parcel_classification_data_filepath)    
-    if input_parcel_classification_data_df.index.name != conf.columns['id']: 
-        input_parcel_classification_data_df.set_index(conf.columns['id'], inplace=True)
-    logger.debug('Read classification data file ready')
+    # Read the classification data from the csv so we can pass it on to the other
+    # functions to improve performance...
+    logger.info(
+        f"Read classification data file: {input_parcel_classification_data_path}"
+    )
+    input_parcel_classification_data_df = pdh.read_file(
+        input_parcel_classification_data_path
+    )
+    if input_parcel_classification_data_df.index.name != conf.columns["id"]:
+        input_parcel_classification_data_df.set_index(conf.columns["id"], inplace=True)
+    logger.debug("Read classification data file ready")
 
     # Train the classification
-    output_classifier_filepath = train(
-            input_parcel_train_filepath=input_parcel_train_filepath,
-            input_parcel_test_filepath=input_parcel_test_filepath,
-            input_parcel_classification_data_filepath=input_parcel_classification_data_filepath,
-            output_classifier_basefilepath=output_classifier_basefilepath,
-            force=force,
-            input_parcel_classification_data_df=input_parcel_classification_data_df)
+    output_classifier_path = train(
+        input_parcel_train_path=input_parcel_train_path,
+        input_parcel_test_path=input_parcel_test_path,
+        input_parcel_classification_data_path=input_parcel_classification_data_path,
+        output_classifier_basepath=output_classifier_basepath,
+        force=force,
+        input_parcel_classification_data_df=input_parcel_classification_data_df,
+    )
 
     # Predict the test parcels
-    predict(input_parcel_filepath=input_parcel_test_filepath,
-            input_parcel_classification_data_filepath=input_parcel_classification_data_filepath,
-            input_classifier_basefilepath=output_classifier_basefilepath,
-            input_classifier_filepath=output_classifier_filepath,
-            output_predictions_filepath=output_predictions_test_filepath,
-            force=force,
-            input_parcel_classification_data_df=input_parcel_classification_data_df)
+    predict(
+        input_parcel_path=input_parcel_test_path,
+        input_parcel_classification_data_path=input_parcel_classification_data_path,
+        input_classifier_basepath=output_classifier_basepath,
+        input_classifier_path=output_classifier_path,
+        output_predictions_path=output_predictions_test_path,
+        force=force,
+        input_parcel_classification_data_df=input_parcel_classification_data_df,
+    )
 
     # Predict all parcels
-    predict(input_parcel_filepath=input_parcel_all_filepath,
-            input_parcel_classification_data_filepath=input_parcel_classification_data_filepath,
-            input_classifier_basefilepath=output_classifier_basefilepath,
-            input_classifier_filepath=output_classifier_filepath,
-            output_predictions_filepath=output_predictions_all_filepath,
-            force=force,
-            input_parcel_classification_data_df=input_parcel_classification_data_df)
-
-def train(  input_parcel_train_filepath: Path,
-            input_parcel_test_filepath: Path,
-            input_parcel_classification_data_filepath: Path,
-            output_classifier_basefilepath: Path,
-            force: bool = False,
-            input_parcel_classification_data_df: pd.DataFrame = None) -> Path:
-    """ Train a classifier and test it by predicting the test cases. """
+    predict(
+        input_parcel_path=input_parcel_all_path,
+        input_parcel_classification_data_path=input_parcel_classification_data_path,
+        input_classifier_basepath=output_classifier_basepath,
+        input_classifier_path=output_classifier_path,
+        output_predictions_path=output_predictions_all_path,
+        force=force,
+        input_parcel_classification_data_df=input_parcel_classification_data_df,
+    )
+
+
+def train(
+    input_parcel_train_path: Path,
+    input_parcel_test_path: Path,
+    input_parcel_classification_data_path: Path,
+    output_classifier_basepath: Path,
+    force: bool = False,
+    input_parcel_classification_data_df: Optional[pd.DataFrame] = None,
+) -> Path:
+    """Train a classifier and test it by predicting the test cases."""
 
     logger.info("train_and_test: Start")
-    if(force is False
-       and output_classifier_basefilepath.exists()):
-        logger.warning(f"predict: classifier already exist and force == False, so don't retrain: {output_classifier_basefilepath}")
-        return output_classifier_basefilepath
+    if force is False and output_classifier_basepath.exists():
+        logger.warning(
+            "predict: classifier already exist and force == False, so don't retrain: "
+            f"{output_classifier_basepath}"
+        )
+        return output_classifier_basepath
 
     # If the classification data isn't passed as dataframe, read it from file
     if input_parcel_classification_data_df is None:
-        logger.info(f"Read classification data file: {input_parcel_classification_data_filepath}")
-        input_parcel_classification_data_df = pdh.read_file(input_parcel_classification_data_filepath)
-        if input_parcel_classification_data_df.index.name != conf.columns['id']:
-            input_parcel_classification_data_df.set_index(conf.columns['id'], inplace=True)
-        logger.debug('Read classification data file ready')
+        logger.info(
+            f"Read classification data file: {input_parcel_classification_data_path}"
+        )
+        input_parcel_classification_data_df = pdh.read_file(
+            input_parcel_classification_data_path
+        )
+        if input_parcel_classification_data_df.index.name != conf.columns["id"]:
+            input_parcel_classification_data_df.set_index(
+                conf.columns["id"], inplace=True
+            )
+        logger.debug("Read classification data file ready")
 
     # Read the train parcels
-    logger.info(f"Read train file: {input_parcel_train_filepath}")
-    train_df = pdh.read_file(input_parcel_train_filepath, 
-                             columns=[conf.columns['id'], conf.columns['class']])
-    if train_df.index.name != conf.columns['id']:
-        train_df.set_index(conf.columns['id'], inplace=True)
-    logger.debug('Read train file ready')
-
-    # Join the columns of input_parcel_classification_data_df that aren't yet in train_df
+    logger.info(f"Read train file: {input_parcel_train_path}")
+    train_df = pdh.read_file(
+        input_parcel_train_path, columns=[conf.columns["id"], conf.columns["class"]]
+    )
+    if train_df.index.name != conf.columns["id"]:
+        train_df.set_index(conf.columns["id"], inplace=True)
+    logger.debug("Read train file ready")
+
+    # Join the columns of input_parcel_classification_data_df that aren't yet in
+    # train_df
     logger.info("Join train sample with the classification data")
-    train_df = (train_df.join(input_parcel_classification_data_df, how='inner'))
+    train_df = train_df.join(input_parcel_classification_data_df, how="inner")
 
     # Read the test/validation data
-    logger.info(f"Read test file: {input_parcel_test_filepath}")
-    test_df = pdh.read_file(input_parcel_test_filepath, 
-                            columns=[conf.columns['id'], conf.columns['class']])
-    if test_df.index.name != conf.columns['id']:
-        test_df.set_index(conf.columns['id'], inplace=True)
-    logger.debug('Read test file ready')
+    logger.info(f"Read test file: {input_parcel_test_path}")
+    test_df = pdh.read_file(
+        input_parcel_test_path, columns=[conf.columns["id"], conf.columns["class"]]
+    )
+    if test_df.index.name != conf.columns["id"]:
+        test_df.set_index(conf.columns["id"], inplace=True)
+    logger.debug("Read test file ready")
 
     # Join the columns of input_parcel_classification_data_df that aren't yet in test_df
     logger.info("Join test sample with the classification data")
-    test_df = (test_df.join(input_parcel_classification_data_df, how='inner'))
+    test_df = test_df.join(input_parcel_classification_data_df, how="inner")
 
     # Train
-    if conf.classifier['classifier_type'].lower() == 'keras_multilayer_perceptron':
+    if conf.classifier["classifier_type"].lower() == "keras_multilayer_perceptron":
         import cropclassification.predict.classification_keras as class_core_keras
+
         return class_core_keras.train(
-                train_df=train_df, 
-                test_df=test_df,
-                output_classifier_basefilepath=output_classifier_basefilepath)
+            train_df=train_df,
+            test_df=test_df,
+            output_classifier_basepath=output_classifier_basepath,
+        )
     else:
         import cropclassification.predict.classification_sklearn as class_core_sklearn
+
         return class_core_sklearn.train(
-                train_df=train_df, 
-                output_classifier_basefilepath=output_classifier_basefilepath)
-
-def predict(input_parcel_filepath: Path,
-            input_parcel_classification_data_filepath: Path,
-            input_classifier_basefilepath: Path,
-            input_classifier_filepath: Path,
-            output_predictions_filepath: Path,
-            force: bool = False,
-            input_parcel_classification_data_df: pd.DataFrame = None):
-    """ Predict the classes for the input data. """
+            train_df=train_df,
+            output_classifier_basepath=output_classifier_basepath,
+        )
+
+
+def predict(
+    input_parcel_path: Path,
+    input_parcel_classification_data_path: Path,
+    input_classifier_basepath: Path,
+    input_classifier_path: Path,
+    output_predictions_path: Path,
+    force: bool = False,
+    input_parcel_classification_data_df: Optional[pd.DataFrame] = None,
+):
+    """Predict the classes for the input data."""
 
     # If force is False, and the output file exist already, return
-    if(force is False
-       and output_predictions_filepath.exists()):
-        logger.warning(f"predict: predictions output file already exists and force is false, so stop: {output_predictions_filepath}")
+    if force is False and output_predictions_path.exists():
+        logger.warning(
+            "predict: predictions output file already exists and force is false, so "
+            f"stop: {output_predictions_path}"
+        )
         return
 
     # Read the input parcels
-    logger.info(f"Read input file: {input_parcel_filepath}")
-    input_parcel_df = pdh.read_file(input_parcel_filepath,
-            columns=[conf.columns['id'], conf.columns['class'], conf.columns['class_declared']])
-    if input_parcel_df.index.name != conf.columns['id']:
-        input_parcel_df.set_index(conf.columns['id'], inplace=True)
-    logger.debug('Read train file ready')
+    logger.info(f"Read input file: {input_parcel_path}")
+    input_parcel_df = pdh.read_file(
+        input_parcel_path,
+        columns=[
+            conf.columns["id"],
+            conf.columns["class"],
+            conf.columns["class_declared"],
+        ],
+    )
+    if input_parcel_df.index.name != conf.columns["id"]:
+        input_parcel_df.set_index(conf.columns["id"], inplace=True)
+    logger.debug("Read train file ready")
 
     # For parcels of a class that should be ignored, don't predict
-    input_parcel_df = input_parcel_df.loc[~input_parcel_df[conf.columns['class_declared']]
-                                           .isin(conf.marker.getlist('classes_to_ignore'))]
+    input_parcel_df = input_parcel_df.loc[
+        ~input_parcel_df[conf.columns["class_declared"]].isin(
+            conf.marker.getlist("classes_to_ignore")
+        )
+    ]
 
     # get the expected columns from the classifier
-    datacolumns_filepath = glob.glob(os.path.join(os.path.dirname(input_classifier_filepath), "*datacolumns.txt"))[0]
-    with open(datacolumns_filepath, "r") as f:
-       input_classifier_datacolumns = ast.literal_eval(f.readline())
+    datacolumns_path = glob.glob(
+        os.path.join(os.path.dirname(input_classifier_path), "*datacolumns.txt")
+    )[0]
+    with open(datacolumns_path, "r") as f:
+        input_classifier_datacolumns = ast.literal_eval(f.readline())
 
     # If the classification data isn't passed as dataframe, read it from the csv
     if input_parcel_classification_data_df is None:
-        logger.info(f"Read classification data file: {input_parcel_classification_data_filepath}")
-        input_parcel_classification_data_df = pdh.read_file(input_parcel_classification_data_filepath)
-        if input_parcel_classification_data_df.index.name != conf.columns['id']:
-            input_parcel_classification_data_df.set_index(conf.columns['id'], inplace=True)
-        logger.debug('Read classification data file ready')
+        logger.info(
+            f"Read classification data file: {input_parcel_classification_data_path}"
+        )
+        input_parcel_classification_data_df = pdh.read_file(
+            input_parcel_classification_data_path
+        )
+        if input_parcel_classification_data_df.index.name != conf.columns["id"]:
+            input_parcel_classification_data_df.set_index(
+                conf.columns["id"], inplace=True
+            )
+        logger.debug("Read classification data file ready")
 
     # only take the required columns as expected by the classifier
-    input_parcel_classification_data_df = input_parcel_classification_data_df[input_classifier_datacolumns]
+    input_parcel_classification_data_df = input_parcel_classification_data_df[
+        input_classifier_datacolumns
+    ]
 
     # Join the data to send to prediction logic
     logger.info("Join input parcels with the classification data")
-    input_parcel_for_predict_df = input_parcel_df.join(input_parcel_classification_data_df, how='inner')   
+    input_parcel_for_predict_df = input_parcel_df.join(
+        input_parcel_classification_data_df, how="inner"
+    )
 
     # Predict!
-    logger.info(f"Predict using this model: {input_classifier_filepath}")
-    if conf.classifier['classifier_type'].lower() == 'keras_multilayer_perceptron':
+    logger.info(f"Predict using this model: {input_classifier_path}")
+    if conf.classifier["classifier_type"].lower() == "keras_multilayer_perceptron":
         import cropclassification.predict.classification_keras as class_core_keras
+
         class_core_keras.predict_proba(
-                parcel_df=input_parcel_for_predict_df,
-                classifier_basefilepath=input_classifier_basefilepath,
-                classifier_filepath=input_classifier_filepath,
-                output_parcel_predictions_filepath=output_predictions_filepath)
+            parcel_df=input_parcel_for_predict_df,
+            classifier_basepath=input_classifier_basepath,
+            classifier_path=input_classifier_path,
+            output_parcel_predictions_path=output_predictions_path,
+        )
     else:
         import cropclassification.predict.classification_sklearn as class_core_sklearn
+
         class_core_sklearn.predict_proba(
-                parcel_df=input_parcel_for_predict_df,
-                classifier_basefilepath=input_classifier_basefilepath,
-                classifier_filepath=input_classifier_filepath,
-                output_parcel_predictions_filepath=output_predictions_filepath)
+            parcel_df=input_parcel_for_predict_df,
+            classifier_basepath=input_classifier_basepath,
+            classifier_path=input_classifier_path,
+            output_parcel_predictions_path=output_predictions_path,
+        )
+
 
 # If the script is run directly...
 if __name__ == "__main__":
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/predict/classification_keras.py b/cropclassification/predict/classification_keras.py
index 0311b450..bb23fb9e 100644
--- a/cropclassification/predict/classification_keras.py
+++ b/cropclassification/predict/classification_keras.py
@@ -17,31 +17,32 @@
 import cropclassification.helpers.model_helper as mh
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 
 # Set number of parallel threads for keras
-'''
+"""
 num_cores = os.cpu_count()
 logger.info(f"Cores found: {num_cores}")
-config = tf.ConfigProto(intra_op_parallelism_threads=num_cores, 
-                        inter_op_parallelism_threads=num_cores, 
-                        allow_soft_placement=True, 
+config = tf.ConfigProto(intra_op_parallelism_threads=num_cores,
+                        inter_op_parallelism_threads=num_cores,
+                        allow_soft_placement=True,
                         device_count = {'CPU': num_cores})
 session = tf.Session(config=config)
 K.set_session(session)
-'''
+"""
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
-def train(train_df: pd.DataFrame,
-          test_df: pd.DataFrame,
-          output_classifier_basefilepath: Path) -> Path:
+
+def train(
+    train_df: pd.DataFrame, test_df: pd.DataFrame, output_classifier_basepath: Path
+) -> Path:
     """
     Train a classifier and output the trained classifier to the output file.
 
@@ -51,213 +52,314 @@ def train(train_df: pd.DataFrame,
             * global_settings.class_column: the class of the parcel
             * ... all columns that will be used as classification data
         test_df: pandas DataFrame containing the test/validation data.
-        output_classifier_filepath: the filepath where the classifier can be written
+        output_classifier_path: the path where the classifier can be written
     """
 
     # Prepare and check some input + init some variables
-    output_classifier_filepath_noext, output_ext = os.path.splitext(output_classifier_basefilepath)
-    output_classifier_classes_filepath = Path(f"{output_classifier_filepath_noext}_classes.txt")
-    output_classifier_datacolumns_filepath = Path(f"{output_classifier_filepath_noext}_datacolumns.txt")
-    
-    if output_ext.lower() != '.hdf5':
+    output_classifier_path_noext, output_ext = os.path.splitext(
+        output_classifier_basepath
+    )
+    output_classifier_classes_path = Path(f"{output_classifier_path_noext}_classes.txt")
+    output_classifier_datacolumns_path = Path(
+        f"{output_classifier_path_noext}_datacolumns.txt"
+    )
+
+    if output_ext.lower() != ".hdf5":
         message = f"Keras only supports saving in extension .hdf5, not in {output_ext}"
         logger.error(message)
         raise Exception(message)
 
     # Keras wants numeric classes, so prepare *_classes_df for that
-    # First create dict with the conversion, and save it 
-    classes_dict = {key:value for value, key in enumerate(train_df[conf.columns['class']].unique())}
-    with open(output_classifier_classes_filepath, "w") as file:
+    # First create dict with the conversion, and save it
+    classes_dict = {
+        key: value for value, key in enumerate(train_df[conf.columns["class"]].unique())
+    }
+    with open(output_classifier_classes_path, "w") as file:
         file.write(str(classes_dict))
     # Replace the string values with the ints
-    column_class = conf.columns['class']
+    column_class = conf.columns["class"]
 
-    # TODO: doesn't seem to be the safest way to implement this: what if the 
+    # TODO: doesn't seem to be the safest way to implement this: what if the
     # classes are integers instead of strings???
     train_df[column_class].replace(classes_dict, inplace=True)
     test_df[column_class].replace(classes_dict, inplace=True)
 
     # The test dataset also should only include classes we are training on...
-    # I don't exactly why (don't know why the notnull/isnull must be there), but this seems the 
-    # only way it works?
-    if test_df.dtypes[column_class] == 'object':
+    # I don't exactly why (don't know why the notnull/isnull must be there), but this
+    # seems the only way it works?
+    if test_df.dtypes[column_class] == "object":
         test_removed_df = test_df[test_df[column_class].str.isnumeric().notnull()]
-        logger.info(f"Removed following classes from test_classes_df: {test_removed_df[column_class].unique()}")
+        logger.info(
+            "Removed following classes from test_classes_df: "
+            f"{test_removed_df[column_class].unique()}"
+        )
         test_df = test_df[test_df[column_class].str.isnumeric().isnull()]
 
-    # Split the input dataframe in one with the train classes and one with the train data
+    # Split the input dataframe in one with the train classes and one with the train
+    # data
     train_classes_df = train_df[column_class]
-    cols_to_keep = train_df.columns.difference([conf.columns['id'], column_class])
+    cols_to_keep = train_df.columns.difference([conf.columns["id"], column_class])
     train_data_df = train_df[cols_to_keep]
     train_data_df.sort_index(axis=1, inplace=True)
 
     test_classes_df = test_df[column_class]
-    cols_to_keep = test_df.columns.difference([conf.columns['id'], column_class])
+    cols_to_keep = test_df.columns.difference([conf.columns["id"], column_class])
     test_data_df = test_df[cols_to_keep]
     test_data_df.sort_index(axis=1, inplace=True)
 
-    logger.info(f"Train file processed and rows with missing data removed, data shape: {train_data_df.shape}, labels shape: {train_classes_df.shape}")
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
-        logger.info(f"Resulting Columns for training data: {list(train_data_df.columns)}")
-    with open(output_classifier_datacolumns_filepath, "w") as file:
+    logger.info(
+        "Train file processed and rows with missing data removed, data shape: "
+        f"{train_data_df.shape}, labels shape: {train_classes_df.shape}"
+    )
+    with pd.option_context("display.max_rows", None, "display.max_columns", None):
+        logger.info(
+            f"Resulting Columns for training data: {list(train_data_df.columns)}"
+        )
+    with open(output_classifier_datacolumns_path, "w") as file:
         file.write(str(list(train_data_df.columns)))
 
-    classifier_type_lower = conf.classifier['classifier_type'].lower()
-    if classifier_type_lower != 'keras_multilayer_perceptron':
-        message = f"Unsupported classifier in conf.classifier['classifier_type']: {conf.classifier['classifier_type']}"
+    classifier_type_lower = conf.classifier["classifier_type"].lower()
+    if classifier_type_lower != "keras_multilayer_perceptron":
+        message = (
+            "Unsupported classifier in conf.classifier['classifier_type']: "
+            f"{conf.classifier['classifier_type']}"
+        )
         logger.critical(message)
         raise Exception(message)
 
     # Keras not only want numeric classes, it wants a column per class
-    train_classes_df = tf.keras.utils.to_categorical(train_classes_df, len(classes_dict))
+    train_classes_df = tf.keras.utils.to_categorical(
+        train_classes_df, len(classes_dict)
+    )
     test_classes_df = tf.keras.utils.to_categorical(test_classes_df, len(classes_dict))
 
     # Get some config from the config file
-    hidden_layer_sizes = conf.classifier.getlistint('multilayer_perceptron_hidden_layer_sizes')
+    hidden_layer_sizes = conf.classifier.getlistint(
+        "multilayer_perceptron_hidden_layer_sizes"
+    )
     if len(hidden_layer_sizes) == 0:
         raise Exception("Having no hidden layers is currently not supported")
-    max_iter = conf.classifier.getint('multilayer_perceptron_max_iter')
-    learning_rate_init = conf.classifier.getfloat('multilayer_perceptron_learning_rate_init')
-    
+    max_iter = conf.classifier.getint("multilayer_perceptron_max_iter")
+    learning_rate_init = conf.classifier.getfloat(
+        "multilayer_perceptron_learning_rate_init"
+    )
+
     # Create neural network
     model = tf.keras.models.Sequential()
     # Create the hidden layers as specified in config
-    dropout_pct = conf.classifier.getfloat('multilayer_perceptron_dropout_pct')
+    dropout_pct = conf.classifier.getfloat("multilayer_perceptron_dropout_pct")
     for i, hidden_layer_size in enumerate(hidden_layer_sizes):
         # For the first layer, the input size needs to be specified
         if i == 0:
-            model.add(tf.keras.layers.Dense(hidden_layer_size, activation='relu', input_shape=(len(train_data_df.columns),)))
+            model.add(
+                tf.keras.layers.Dense(
+                    hidden_layer_size,
+                    activation="relu",
+                    input_shape=(len(train_data_df.columns),),
+                )
+            )
             if dropout_pct > 0:
-                model.add(tf.keras.layers.Dropout(dropout_pct/100))
+                model.add(tf.keras.layers.Dropout(dropout_pct / 100))
         else:
-            model.add(tf.keras.layers.Dense(hidden_layer_size, activation='relu'))
+            model.add(tf.keras.layers.Dense(hidden_layer_size, activation="relu"))
             if dropout_pct > 0:
-                model.add(tf.keras.layers.Dropout(dropout_pct/100))
+                model.add(tf.keras.layers.Dropout(dropout_pct / 100))
 
     # Add the final layer that will produce the outputs
-    model.add(tf.keras.layers.Dense(len(classes_dict), activation='softmax'))
+    model.add(tf.keras.layers.Dense(len(classes_dict), activation="softmax"))
 
     # Prepare model for training + train!
     optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate_init)
-    model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])
-    
+    model.compile(optimizer=optimizer, loss="binary_crossentropy", metrics=["accuracy"])
+
     logger.info(f"Start fitting classifier:\n{model.summary()}")
-    acc_metric_mode = 'min'
-    reduce_lr_loss = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', mode=acc_metric_mode,
-            factor=0.2, patience=50, verbose=1, epsilon=1e-4)
+    acc_metric_mode = "min"
+    reduce_lr_loss = tf.keras.callbacks.ReduceLROnPlateau(
+        monitor="val_loss",
+        mode=acc_metric_mode,
+        factor=0.2,
+        patience=50,
+        verbose=1,
+        epsilon=1e-4,
+    )
     callbacks = [reduce_lr_loss]
 
-    # Several best_model strategies are possible, but it seems the standard/typical val_loss 
-    # gives the best results for this case.
-    best_model_strategy = 'VAL_LOSS'
-    if(best_model_strategy == 'VAL_LOSS'):
-        to_be_formatted_by_callback = "{val_loss:.5f}_{loss:.5f}_{val_loss:.5f}_{epoch:02d}"
-        best_model_filepath = f"{output_classifier_filepath_noext}_{to_be_formatted_by_callback}{output_ext}"
-        callbacks.append(tf.keras.callbacks.ModelCheckpoint(
-                best_model_filepath, save_best_only=True, 
-                monitor='val_loss', mode=acc_metric_mode))
-        callbacks.append(tf.keras.callbacks.EarlyStopping(monitor='val_loss', mode=acc_metric_mode,
-                patience=200, verbose=0))    
-    elif(best_model_strategy == 'LOSS'):
+    # Several best_model strategies are possible, but it seems the standard/typical
+    # val_loss gives the best results for this case.
+    best_model_strategy = "VAL_LOSS"
+    if best_model_strategy == "VAL_LOSS":
+        to_be_formatted_by_callback = (
+            "{val_loss:.5f}_{loss:.5f}_{val_loss:.5f}_{epoch:02d}"
+        )
+        best_model_path = (
+            f"{output_classifier_path_noext}_{to_be_formatted_by_callback}{output_ext}"
+        )
+        callbacks.append(
+            tf.keras.callbacks.ModelCheckpoint(
+                best_model_path,
+                save_best_only=True,
+                monitor="val_loss",
+                mode=acc_metric_mode,
+            )
+        )
+        callbacks.append(
+            tf.keras.callbacks.EarlyStopping(
+                monitor="val_loss", mode=acc_metric_mode, patience=200, verbose=0
+            )
+        )
+    elif best_model_strategy == "LOSS":
         to_be_formatted_by_callback = "{loss:.5f}_{loss:.5f}_{val_loss:.5f}_{epoch:02d}"
-        best_model_filepath = f"{output_classifier_filepath_noext}_{to_be_formatted_by_callback}{output_ext}"
-        callbacks.append(tf.keras.callbacks.ModelCheckpoint(
-                best_model_filepath, save_best_only=True, 
-                monitor='loss', mode=acc_metric_mode))
-        callbacks.append(tf.keras.callbacks.EarlyStopping(monitor='loss', mode=acc_metric_mode,
-                patience=100, verbose=0))
-    elif(best_model_strategy == 'AVG(VAL_LOSS,LOSS)'):
-        # Custom callback that saves the best models using both train and validation metric
-        callbacks.append(mh.ModelCheckpointExt(
-                output_classifier_basefilepath.parent, output_classifier_basefilepath.name, 
-                acc_metric_train='loss', acc_metric_validation='val_loss', 
-                acc_metric_mode=acc_metric_mode))
-        callbacks.append(tf.keras.callbacks.EarlyStopping(monitor='loss', mode=acc_metric_mode,
-                patience=100, verbose=0))
-
-    csv_log_filepath = Path(f"{output_classifier_filepath_noext}_train_log.csv")
-    callbacks.append(tf.keras.callbacks.CSVLogger(csv_log_filepath, append=True, separator=';'))
-    model.fit(train_data_df, train_classes_df, batch_size=128, epochs=max_iter, 
-              callbacks=callbacks, validation_data=(test_data_df, test_classes_df))
+        best_model_path = (
+            f"{output_classifier_path_noext}_{to_be_formatted_by_callback}{output_ext}"
+        )
+        callbacks.append(
+            tf.keras.callbacks.ModelCheckpoint(
+                best_model_path,
+                save_best_only=True,
+                monitor="loss",
+                mode=acc_metric_mode,
+            )
+        )
+        callbacks.append(
+            tf.keras.callbacks.EarlyStopping(
+                monitor="loss", mode=acc_metric_mode, patience=100, verbose=0
+            )
+        )
+    elif best_model_strategy == "AVG(VAL_LOSS,LOSS)":
+        # Custom callback that saves the best models using both train and validation
+        # metric
+        callbacks.append(
+            mh.ModelCheckpointExt(
+                output_classifier_basepath.parent,
+                output_classifier_basepath.name,
+                acc_metric_train="loss",
+                acc_metric_validation="val_loss",
+                acc_metric_mode=acc_metric_mode,
+            )
+        )
+        callbacks.append(
+            tf.keras.callbacks.EarlyStopping(
+                monitor="loss", mode=acc_metric_mode, patience=100, verbose=0
+            )
+        )
+
+    csv_log_path = Path(f"{output_classifier_path_noext}_train_log.csv")
+    callbacks.append(
+        tf.keras.callbacks.CSVLogger(csv_log_path, append=True, separator=";")
+    )
+    model.fit(
+        train_data_df,
+        train_classes_df,
+        batch_size=128,
+        epochs=max_iter,
+        callbacks=callbacks,
+        validation_data=(test_data_df, test_classes_df),
+    )
 
     # Get the best model after fitting to return it...
-    best_model_info = mh.get_best_model(output_classifier_basefilepath.parent, acc_metric_mode=acc_metric_mode)
-    best_model_filepath = best_model_info['filepath']
+    best_model_info = mh.get_best_model(
+        output_classifier_basepath.parent, acc_metric_mode=acc_metric_mode
+    )
+    best_model_path = best_model_info["path"]
+
+    return Path(best_model_path)
 
-    return Path(best_model_filepath)
 
-def predict_proba(parcel_df: pd.DataFrame,
-                  classifier_basefilepath: Path,
-                  classifier_filepath: Path,
-                  output_parcel_predictions_filepath: Path) -> pd.DataFrame:
+def predict_proba(
+    parcel_df: pd.DataFrame,
+    classifier_basepath: Path,
+    classifier_path: Path,
+    output_parcel_predictions_path: Path,
+) -> pd.DataFrame:
     """
-    Predict the probabilities for all input data using the classifier provided and write it
-    to the output file.
+    Predict the probabilities for all input data using the classifier provided and
+    write it to the output file.
 
     Args
         parcel_df: pandas DataFrame containing the data to classify. Columns:
             * global_settings.id_column: the id of the parcel.
             * global_settings.class_column: the class of the parcel. Isn't really used.
             * ... all columns that will be used as classification data.
-        classifier_filepath: the filepath where the classifier can be written.
+        classifier_path: the path where the classifier can be written.
     """
 
     # Some basic checks that input is ok
-    column_class = conf.columns['class']
-    column_class_declared = conf.columns['class_declared']
+    column_class = conf.columns["class"]
+    column_class_declared = conf.columns["class_declared"]
     parcel_df.reset_index(inplace=True)
-    if(conf.columns['id'] not in parcel_df.columns
-       or column_class not in parcel_df.columns):
-        message = f"Columns {conf.columns['id']} and {column_class} are mandatory for input parameter parcel_df!"
+    if (
+        conf.columns["id"] not in parcel_df.columns
+        or column_class not in parcel_df.columns
+    ):
+        message = (
+            f"Columns {conf.columns['id']} and {column_class} are mandatory for input "
+            "parameter parcel_df!"
+        )
         logger.critical(message)
         raise Exception(message)
 
     # Now do final preparation for the classification
     parcel_classes_df = parcel_df[column_class]
     cols_to_keep = parcel_df.columns.difference(
-            [conf.columns['id'], column_class, column_class_declared])
+        [conf.columns["id"], column_class, column_class_declared]
+    )
     parcel_data_df = parcel_df[cols_to_keep]
     parcel_data_df.sort_index(axis=1, inplace=True)
 
-    logger.info(f"Input predict file processed and rows with missing data removed, data shape: {parcel_data_df.shape}, labels shape: {parcel_classes_df.shape}")
+    logger.info(
+        "Input predict file processed and rows with missing data removed, data shape: "
+        f"{parcel_data_df.shape}, labels shape: {parcel_classes_df.shape}"
+    )
 
     # Check of the input data columns match the columns needed for the neural net
-    classifier_datacolumns_filepath = glob.glob(os.path.join(os.path.dirname(classifier_filepath), "*_datacolumns.txt"))[0]
-    with open(classifier_datacolumns_filepath, "r") as file:
+    classifier_datacolumns_path = glob.glob(
+        os.path.join(os.path.dirname(classifier_path), "*_datacolumns.txt")
+    )[0]
+    with open(classifier_datacolumns_path, "r") as file:
         classifier_datacolumns = ast.literal_eval(file.readline())
     if classifier_datacolumns != list(parcel_data_df.columns):
-        raise Exception(f"Input datacolumns for predict don't match needed columns for neural net: \ninput: {parcel_data_df.columns}, \nneeded: {classifier_datacolumns}" )
+        raise Exception(
+            "Input datacolumns for predict don't match needed columns for neural net: "
+            f"\ninput: {parcel_data_df.columns}, \nneeded: {classifier_datacolumns}"
+        )
 
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
+    with pd.option_context("display.max_rows", None, "display.max_columns", None):
         logger.info(f"Resulting Columns for predicting data: {parcel_data_df.columns}")
 
     # Load the classifier and predict
-    model = tf.keras.models.load_model(classifier_filepath)
+    model = tf.keras.models.load_model(classifier_path)
     logger.info(f"Predict classes with probabilities: {len(parcel_df.index)} rows")
     class_proba = model.predict(parcel_data_df)
-    logger.info(f"Predict classes with probabilities ready")
+    logger.info("Predict classes with probabilities ready")
 
     # Convert probabilities to dataframe, combine with input data and write to file
     # Load the classes from the classes file
-    classifier_classes_filepath = glob.glob(os.path.join(os.path.dirname(classifier_filepath), "*_classes.txt"))[0]
-    with open(classifier_classes_filepath, "r") as file:
+    classifier_classes_path = glob.glob(
+        os.path.join(os.path.dirname(classifier_path), "*_classes.txt")
+    )[0]
+    with open(classifier_classes_path, "r") as file:
         classes_dict = ast.literal_eval(file.readline())
 
     id_class_proba = np.concatenate(
-            [parcel_df[[conf.columns['id'], column_class, column_class_declared]].values, 
-             class_proba], axis=1)
-    cols = [conf.columns['id'], column_class, column_class_declared]
+        [
+            parcel_df[[conf.columns["id"], column_class, column_class_declared]].values,
+            class_proba,
+        ],
+        axis=1,
+    )
+    cols = [conf.columns["id"], column_class, column_class_declared]
     cols.extend(classes_dict)
     proba_df = pd.DataFrame(id_class_proba, columns=cols)
-    proba_df.set_index(keys=conf.columns['id'], inplace=True)
-    
+    proba_df.set_index(keys=conf.columns["id"], inplace=True)
+
     # If output path provided, write results
-    if output_parcel_predictions_filepath:
-        pdh.to_file(proba_df, output_parcel_predictions_filepath)
+    if output_parcel_predictions_path:
+        pdh.to_file(proba_df, output_parcel_predictions_path)
 
     return proba_df
 
+
 # If the script is run directly...
 if __name__ == "__main__":
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/predict/classification_sklearn.py b/cropclassification/predict/classification_sklearn.py
index af735526..76dcd30c 100644
--- a/cropclassification/predict/classification_sklearn.py
+++ b/cropclassification/predict/classification_sklearn.py
@@ -3,7 +3,7 @@
 Module that implements the classification logic.
 """
 
-import ast 
+import ast
 import glob
 import logging
 import os
@@ -20,18 +20,18 @@
 import cropclassification.helpers.config_helper as conf
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
-def train(train_df: pd.DataFrame,
-          output_classifier_basefilepath: Path) -> Path:
+
+def train(train_df: pd.DataFrame, output_classifier_basepath: Path) -> Path:
     """
     Train a classifier and output the trained classifier to the output file.
 
@@ -40,46 +40,64 @@ def train(train_df: pd.DataFrame,
             * global_settings.id_column: the id of the parcel
             * global_settings.class_column: the class of the parcel
             * ... all columns that will be used as classification data
-        output_classifier_basefilepath: the filepath where the classifier can be written
+        output_classifier_basepath: the path where the classifier can be written
     """
-    output_classifier_filepath_noext, _ = os.path.splitext(output_classifier_basefilepath)
-    output_classifier_filepath = output_classifier_basefilepath
-    output_classifier_datacolumns_filepath = Path(f"{output_classifier_filepath_noext}_datacolumns.txt") 
-
-    # Split the input dataframe in one with the train classes and one with the train data
-    train_classes_df = train_df[conf.columns['class']]
-    cols_to_keep = train_df.columns.difference([conf.columns['id'], conf.columns['class']])
+    output_classifier_path_noext, _ = os.path.splitext(output_classifier_basepath)
+    output_classifier_path = output_classifier_basepath
+    output_classifier_datacolumns_path = Path(
+        f"{output_classifier_path_noext}_datacolumns.txt"
+    )
+
+    # Split the input dataframe in one with the train classes and one with the train
+    # data
+    train_classes_df = train_df[conf.columns["class"]]
+    cols_to_keep = train_df.columns.difference(
+        [conf.columns["id"], conf.columns["class"]]
+    )
     train_data_df = train_df[cols_to_keep]
 
-    logger.info(f"Train file processed and rows with missing data removed, data shape: {train_data_df.shape}, labels shape: {train_classes_df.shape}")
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
+    logger.info(
+        "Train file processed and rows with missing data removed, data shape: "
+        f"{train_data_df.shape}, labels shape: {train_classes_df.shape}"
+    )
+    with pd.option_context("display.max_rows", None, "display.max_columns", None):
         logger.info(f"Resulting Columns for training data: {train_data_df.columns}")
-    with open(output_classifier_datacolumns_filepath, "w") as file:
+    with open(output_classifier_datacolumns_path, "w") as file:
         file.write(str(list(train_data_df.columns)))
 
     # Using almost all defaults for the classifier seems to work best...
-    logger.info('Start training')
-    classifier_type_lower = conf.classifier['classifier_type'].lower()
-    if classifier_type_lower == 'randomforest':
-        n_estimators = conf.classifier.getint('randomforest_n_estimators')
-        max_depth = conf.classifier.getint('randomforest_max_depth')
-        classifier = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth)
-    elif classifier_type_lower == 'nearestneighbor':
-        classifier = KNeighborsClassifier(weights='distance', n_jobs=-1)
-    elif classifier_type_lower == 'multilayer_perceptron':
-        hidden_layer_sizes = tuple(conf.classifier.getlistint('multilayer_perceptron_hidden_layer_sizes'))
-        max_iter = conf.classifier.getint('multilayer_perceptron_max_iter')
-        learning_rate_init = conf.classifier.getfloat('multilayer_perceptron_learning_rate_init')
+    logger.info("Start training")
+    classifier_type_lower = conf.classifier["classifier_type"].lower()
+    if classifier_type_lower == "randomforest":
+        n_estimators = conf.classifier.getint("randomforest_n_estimators")
+        max_depth = conf.classifier.getint("randomforest_max_depth")
+        classifier = RandomForestClassifier(
+            n_estimators=n_estimators, max_depth=max_depth
+        )
+    elif classifier_type_lower == "nearestneighbor":
+        classifier = KNeighborsClassifier(weights="distance", n_jobs=-1)
+    elif classifier_type_lower == "multilayer_perceptron":
+        hidden_layer_sizes = tuple(
+            conf.classifier.getlistint("multilayer_perceptron_hidden_layer_sizes")
+        )
+        max_iter = conf.classifier.getint("multilayer_perceptron_max_iter")
+        learning_rate_init = conf.classifier.getfloat(
+            "multilayer_perceptron_learning_rate_init"
+        )
         classifier = MLPClassifier(
-                max_iter=max_iter, hidden_layer_sizes=hidden_layer_sizes,
-                learning_rate_init=learning_rate_init)
-    elif classifier_type_lower == 'svm':
+            max_iter=max_iter,
+            hidden_layer_sizes=hidden_layer_sizes,
+            learning_rate_init=learning_rate_init,
+        )
+    elif classifier_type_lower == "svm":
         # cache_size=1000 (MB) should speed up training
         # probability=True is necessary to be able to use predict_proba
-        classifier = SVC(C=64.0, gamma=0.125, 
-                probability=True, cache_size=1000)
+        classifier = SVC(C=64.0, gamma=0.125, probability=True, cache_size=1000)
     else:
-        message = f"Unsupported classifier in conf.classifier['classifier_type']: {conf.classifier['classifier_type']}"
+        message = (
+            "Unsupported classifier in conf.classifier['classifier_type']: "
+            f"{conf.classifier['classifier_type']}"
+        )
         logger.critical(message)
         raise Exception(message)
 
@@ -87,56 +105,74 @@ def train(train_df: pd.DataFrame,
     classifier.fit(train_data_df, train_classes_df)
 
     # Write the learned model to a file...
-    logger.info(f"Write the learned model file to {output_classifier_filepath}")
-    joblib.dump(classifier, output_classifier_filepath)
-    
-    return output_classifier_filepath
-
-def predict_proba(parcel_df: pd.DataFrame,
-                  classifier_basefilepath: Path,
-                  classifier_filepath: Path,
-                  output_parcel_predictions_filepath: Path) -> pd.DataFrame:
+    logger.info(f"Write the learned model file to {output_classifier_path}")
+    joblib.dump(classifier, output_classifier_path)
+
+    return output_classifier_path
+
+
+def predict_proba(
+    parcel_df: pd.DataFrame,
+    classifier_basepath: Path,
+    classifier_path: Path,
+    output_parcel_predictions_path: Path,
+) -> pd.DataFrame:
     """
-    Predict the probabilities for all input data using the classifier provided and write it
-    to the output file.
+    Predict the probabilities for all input data using the classifier provided and
+    write it to the output file.
 
     Args
         parcel_df: pandas DataFrame containing the data to classify. Columns:
             * global_settings.id_column: the id of the parcel.
             * global_settings.class_column: the class of the parcel. Isn't really used.
             * ... all columns that will be used as classification data.
-        classifier_filepath: the filepath where the classifier can be written.
-        output_parcel_predictions_filepath: file to write the predictions to.
+        classifier_path: the path where the classifier can be written.
+        output_parcel_predictions_path: file to write the predictions to.
     """
 
     # Some basic checks that input is ok
-    column_class = conf.columns['class']
-    column_class_declared = conf.columns['class_declared']
+    column_class = conf.columns["class"]
+    column_class_declared = conf.columns["class_declared"]
     parcel_df.reset_index(inplace=True)
-    if(conf.columns['id'] not in parcel_df.columns
-       or column_class not in parcel_df.columns):
-        message = f"Columns {conf.columns['id']} and {column_class} are mandatory for input parameter parcel_df!"
+    if (
+        conf.columns["id"] not in parcel_df.columns
+        or column_class not in parcel_df.columns
+    ):
+        message = (
+            f"Columns {conf.columns['id']} and {column_class} are mandatory for input "
+            "parameter parcel_df!"
+        )
         logger.critical(message)
         raise Exception(message)
 
     # Now do final preparation for the classification
-    parcel_classes_df = parcel_df[conf.columns['class']]
-    cols_to_keep = parcel_df.columns.difference([conf.columns['id'], column_class, column_class_declared])
+    parcel_classes_df = parcel_df[conf.columns["class"]]
+    cols_to_keep = parcel_df.columns.difference(
+        [conf.columns["id"], column_class, column_class_declared]
+    )
     parcel_data_df = parcel_df[cols_to_keep]
 
-    logger.info(f"Train file processed and rows with missing data removed, data shape: {parcel_data_df.shape}, labels shape: {parcel_classes_df.shape}")
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
+    logger.info(
+        "Train file processed and rows with missing data removed, data shape: "
+        f"{parcel_data_df.shape}, labels shape: {parcel_classes_df.shape}"
+    )
+    with pd.option_context("display.max_rows", None, "display.max_columns", None):
         logger.info(f"Resulting Columns for training data: {parcel_data_df.columns}")
 
     # Check of the input data columns match the columns needed for the classifier
-    classifier_datacolumns_filepath = glob.glob(os.path.join(os.path.dirname(classifier_filepath), "*_datacolumns.txt"))[0]
-    with open(classifier_datacolumns_filepath, "r") as file:
+    classifier_datacolumns_path = glob.glob(
+        os.path.join(os.path.dirname(classifier_path), "*_datacolumns.txt")
+    )[0]
+    with open(classifier_datacolumns_path, "r") as file:
         classifier_datacolumns = ast.literal_eval(file.readline())
     if classifier_datacolumns != list(parcel_data_df.columns):
-        raise Exception(f"Input datacolumns for predict don't match needed columns for classifier: \ninput: {parcel_data_df.columns}, \nneeded: {classifier_datacolumns}" )
+        raise Exception(
+            "Input datacolumns for predict don't match needed columns for classifier: "
+            f"\ninput: {parcel_data_df.columns}, \nneeded: {classifier_datacolumns}"
+        )
 
     # Load the classifier
-    classifier = joblib.load(classifier_filepath)
+    classifier = joblib.load(classifier_path)
     logger.info(f"Classifier has the following columns: {classifier.classes_}")
 
     logger.info(f"Predict classes with probabilities: {len(parcel_df.index)} rows")
@@ -144,18 +180,25 @@ def predict_proba(parcel_df: pd.DataFrame,
     logger.info(f"Predict classes with probabilities ready")
 
     # Convert probabilities to dataframe, combine with input data and write to file
-    id_class_proba = np.concatenate([parcel_df[[conf.columns['id'], column_class, column_class_declared]].values, class_proba], axis=1)
-    cols = [conf.columns['id'], column_class, column_class_declared]
+    id_class_proba = np.concatenate(
+        [
+            parcel_df[[conf.columns["id"], column_class, column_class_declared]].values,
+            class_proba,
+        ],
+        axis=1,
+    )
+    cols = [conf.columns["id"], column_class, column_class_declared]
     cols.extend(classifier.classes_)
     proba_df = pd.DataFrame(id_class_proba, columns=cols)
 
     # If output path provided, write results
-    if output_parcel_predictions_filepath:
-        pdh.to_file(proba_df, output_parcel_predictions_filepath)
+    if output_parcel_predictions_path:
+        pdh.to_file(proba_df, output_parcel_predictions_path)
 
     return proba_df
 
+
 # If the script is run directly...
 if __name__ == "__main__":
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/predict/multicrop.py b/cropclassification/predict/multicrop.py
index 119a506c..074883bd 100644
--- a/cropclassification/predict/multicrop.py
+++ b/cropclassification/predict/multicrop.py
@@ -11,35 +11,37 @@
 import cropclassification.helpers.config_helper as conf
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
-def detect_multicrop(input_parcel_filepath: Path,
-                     input_parcel_timeseries_data_filepath: Path):
 
-    '''
-    logger.info(f"Read input file: {input_parcel_filepath}")
-    df_input_parcel = pd.read_csv(input_parcel_filepath, low_memory=False)
+def detect_multicrop(input_parcel_path: Path, input_parcel_timeseries_data_path: Path):
+
+    """
+    logger.info(f"Read input file: {input_parcel_path}")
+    df_input_parcel = pd.read_csv(input_parcel_path, low_memory=False)
     logger.debug('Read train file ready')
-    '''
+    """
 
     # If the classification data isn't passed as dataframe, read it from the csv
-    logger.info(f"Read classification data file: {input_parcel_timeseries_data_filepath}")
-    df_timeseries_data = pd.read_csv(input_parcel_timeseries_data_filepath, low_memory=False)
-    df_timeseries_data.set_index(conf.columns['id'], inplace=True)
-    logger.debug('Read classification data file ready')
+    logger.info(f"Read classification data file: {input_parcel_timeseries_data_path}")
+    df_timeseries_data = pd.read_csv(
+        input_parcel_timeseries_data_path, low_memory=False
+    )
+    df_timeseries_data.set_index(conf.columns["id"], inplace=True)
+    logger.debug("Read classification data file ready")
 
     # Add column with the max of all columns (= all stdDev's)
-    df_timeseries_data['max_stddev'] = df_timeseries_data.max(axis=1)
+    df_timeseries_data["max_stddev"] = df_timeseries_data.max(axis=1)
 
-    '''
+    """
     # Prepare the data to send to prediction logic...
     logger.info("Join train sample with the classification data")
     df_input_parcel_for_detect = (df_input_parcel#[[gs.id_column, gs.class_column]]
@@ -60,13 +62,13 @@ def detect_multicrop(input_parcel_filepath: Path,
 
     # Keep the parcels with the 1000 largest stdDev
     df_largest = df_input_parcel_for_detect.nlargest(1000, columns='max_stddev', keep='first')
-    '''
+    """
 
-    #df_result = df_timeseries_data['max_stddev'].to_frame()
+    # df_result = df_timeseries_data['max_stddev'].to_frame()
     df_result = df_timeseries_data
     logger.info(df_result)
 
     # Write to file
-    output_filepath = Path(str(input_parcel_timeseries_data_filepath) + '_largestStdDev.csv')
-    logger.info(f"Write output file: {output_filepath}")
-    pdh.to_file(df_result, output_filepath)
+    output_path = Path(str(input_parcel_timeseries_data_path) + "_largestStdDev.csv")
+    logger.info(f"Write output file: {output_path}")
+    pdh.to_file(df_result, output_path)
diff --git a/cropclassification/preprocess/classification_preprocess.py b/cropclassification/preprocess/classification_preprocess.py
index 529c42c0..9899a742 100644
--- a/cropclassification/preprocess/classification_preprocess.py
+++ b/cropclassification/preprocess/classification_preprocess.py
@@ -14,318 +14,479 @@
 import cropclassification.helpers.config_helper as conf
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
 # Get a logger...
 logger = logging.getLogger(__name__)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
-
-def prepare_input(input_parcel_filepath: Path,
-                  input_parcel_filetype: str,
-                  input_parcel_pixcount_filepath: Path,
-                  classtype_to_prepare: str,
-                  classes_refe_filepath: Path,
-                  output_parcel_filepath: Path,
-                  force: bool = False):
+# -------------------------------------------------------------
+
+
+def prepare_input(
+    input_parcel_path: Path,
+    input_parcel_filetype: str,
+    input_parcel_pixcount_path: Path,
+    classtype_to_prepare: str,
+    classes_refe_path: Path,
+    output_parcel_path: Path,
+    force: bool = False,
+):
     """
     Prepare a raw input file by eg. adding the classification classes to use for the
     classification,...
     """
 
     # If force == False Check and the output file exists already, stop.
-    if force is False and os.path.exists(output_parcel_filepath) is True:
-        logger.warning(f"prepare_input: output file already exists and force == False, so stop: {output_parcel_filepath}")
+    if force is False and os.path.exists(output_parcel_path) is True:
+        logger.warning(
+            f"prepare_input: output file exists + force is False: {output_parcel_path}"
+        )
         return
 
-    # If it exists, copy the refe file to the run dir, so we always keep knowing which refe was used
-    if classes_refe_filepath is not None:
-        shutil.copy(classes_refe_filepath, output_parcel_filepath.parent)        
-
-    if input_parcel_filetype == 'BEFL':
-        # classes_refe_filepath must exist for BEFL!
-        if not classes_refe_filepath.exists():
-            raise Exception(f"Input classes file doesn't exist: {classes_refe_filepath}")
-        
-        df_parceldata = befl.prepare_input(input_parcel_filepath=input_parcel_filepath,
-                                           classtype_to_prepare=classtype_to_prepare,
-                                           classes_refe_filepath=classes_refe_filepath,
-                                           output_dir=output_parcel_filepath.parent)
+    # If it exists, copy the refe file to the run dir, so we always keep knowing which
+    # refe was used
+    if classes_refe_path is not None:
+        shutil.copy(classes_refe_path, output_parcel_path.parent)
+
+    if input_parcel_filetype == "BEFL":
+        # classes_refe_path must exist for BEFL!
+        if not classes_refe_path.exists():
+            raise Exception(f"Input classes file doesn't exist: {classes_refe_path}")
+
+        df_parceldata = befl.prepare_input(
+            input_parcel_path=input_parcel_path,
+            classtype_to_prepare=classtype_to_prepare,
+            classes_refe_path=classes_refe_path,
+            output_dir=output_parcel_path.parent,
+        )
     else:
-        message = f"Unknown value for parameter input_parcel_filetype: {input_parcel_filetype}"
+        message = f"Unknown value for input_parcel_filetype: {input_parcel_filetype}"
         logger.critical(message)
         raise Exception(message)
 
     # Load pixcount data and join it
-    logger.info(f"Read pixcount file {input_parcel_pixcount_filepath}")
-    df_pixcount = pdh.read_file(input_parcel_pixcount_filepath)
+    logger.info(f"Read pixcount file {input_parcel_pixcount_path}")
+    df_pixcount = pdh.read_file(input_parcel_pixcount_path)
     logger.debug(f"Read pixcount file ready, shape: {df_pixcount.shape}")
-    if df_pixcount.index.name != conf.columns['id']: 
-        df_pixcount.set_index(conf.columns['id'], inplace=True)
+    if df_pixcount.index.name != conf.columns["id"]:
+        df_pixcount.set_index(conf.columns["id"], inplace=True)
 
-    df_parceldata.set_index(conf.columns['id'], inplace=True)
-    df_parceldata = df_parceldata.join(df_pixcount[conf.columns['pixcount_s1s2']], how='left')
-    df_parceldata[conf.columns['pixcount_s1s2']].fillna(value=0, inplace=True)
+    df_parceldata.set_index(conf.columns["id"], inplace=True)
+    df_parceldata = df_parceldata.join(
+        df_pixcount[conf.columns["pixcount_s1s2"]], how="left"
+    )
+    df_parceldata[conf.columns["pixcount_s1s2"]].fillna(value=0, inplace=True)
 
     # Export result to file
-    output_ext = os.path.splitext(output_parcel_filepath)[1]
+    output_ext = os.path.splitext(output_parcel_path)[1]
     for column in df_parceldata.columns:
         # if the output asked is a csv... we don't need the geometry...
-        if column == conf.columns['geom'] and output_ext == '.csv':
+        if column == conf.columns["geom"] and output_ext == ".csv":
             df_parceldata.drop(column, axis=1, inplace=True)
 
-    logger.info(f"Write output to {output_parcel_filepath}")
+    logger.info(f"Write output to {output_parcel_path}")
     # If extension is not .shp, write using pandas (=a lot faster!)
-    if output_ext.lower() != '.shp': 
-        pdh.to_file(df_parceldata, output_parcel_filepath)
+    if output_ext.lower() != ".shp":
+        pdh.to_file(df_parceldata, output_parcel_path)
     else:
-        df_parceldata.to_file(output_parcel_filepath, index=False)
+        df_parceldata.to_file(output_parcel_path, index=False)
 
-def create_train_test_sample(input_parcel_filepath: Path,
-                             output_parcel_train_filepath: Path,
-                             output_parcel_test_filepath: Path,
-                             balancing_strategy: str,
-                             force: bool = False):
-    """ Create a seperate train and test sample from the general input file. """
+
+def create_train_test_sample(
+    input_parcel_path: Path,
+    output_parcel_train_path: Path,
+    output_parcel_test_path: Path,
+    balancing_strategy: str,
+    force: bool = False,
+):
+    """Create a seperate train and test sample from the general input file."""
 
     # If force == False Check and the output files exist already, stop.
-    if(force is False
-            and output_parcel_train_filepath.exists() is True
-            and output_parcel_test_filepath.exists() is True):
-        logger.warning(f"create_train_test_sample: output files already exist and force == False, so stop: {output_parcel_train_filepath}, {output_parcel_test_filepath}")
+    if (
+        force is False
+        and output_parcel_train_path.exists() is True
+        and output_parcel_test_path.exists() is True
+    ):
+        logger.warning(
+            "create_train_test_sample: output files already exist and force is False: "
+            f"{output_parcel_train_path}, {output_parcel_test_path}"
+        )
         return
 
     # Load input data...
-    logger.info(f"Start create_train_test_sample with balancing_strategy {balancing_strategy}")
-    logger.info(f"Read input file {input_parcel_filepath}")
-    df_in = pdh.read_file(input_parcel_filepath)
+    logger.info(
+        f"Start create_train_test_sample with balancing_strategy {balancing_strategy}"
+    )
+    logger.info(f"Read input file {input_parcel_path}")
+    df_in = pdh.read_file(input_parcel_path)
     logger.debug(f"Read input file ready, shape: {df_in.shape}")
 
     # Init some many-used variables from config
-    class_balancing_column = conf.columns['class_balancing']
-    class_column = conf.columns['class']
+    class_balancing_column = conf.columns["class_balancing"]
+    class_column = conf.columns["class"]
 
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
+    with pd.option_context(
+        "display.max_rows", None, "display.max_columns", None
+    ):  # type: ignore
         count_per_class = df_in.groupby(class_balancing_column, as_index=False).size()
-        logger.info(f"Number of elements per classname in input dataset:\n{count_per_class}")
-
-    # The test dataset should be as representative as possible for the entire dataset, so create
-    # this first as a 20% sample of each class without any additional checks...
-    # Remark: group_keys=False evades that apply creates an extra index-level of the groups above
-    #         the data and evades having to do .reset_index(level=class_balancing_column_NAME, drop=True)
-    #         to get rid of the group level
-    df_test = df_in.groupby(class_balancing_column, group_keys=False).apply(pd.DataFrame.sample, frac=0.20)
-    logger.debug(f"df_test after sampling 20% of data per class, shape: {df_test.shape}")
+        logger.info(
+            f"Number of elements per classname in input dataset:\n{count_per_class}"
+        )
+
+    # The test dataset should be as representative as possible for the entire dataset,
+    # so create this first as a 20% sample of each class without any additional checks.
+    # Remark: group_keys=False evades that apply creates an extra index-level of the
+    #     groups above the data and evades having to do
+    #     .reset_index(level=class_balancing_column_NAME, drop=True)
+    #     to get rid of the group level
+    test_df = df_in.groupby(class_balancing_column, group_keys=False).apply(
+        pd.DataFrame.sample, frac=0.20
+    )
+    logger.debug(
+        f"df_test after sampling 20% of data per class, shape: {test_df.shape}"
+    )
 
     # The candidate parcel for training are all non-test parcel
-    df_train_base = df_in[~df_in.index.isin(df_test.index)]
-    logger.debug(f"df_train_base after isin\n{df_train_base}")
+    train_base_df = df_in[~df_in.index.isin(test_df.index)]
+    logger.debug(f"df_train_base after isin\n{train_base_df}")
 
     # Remove parcel with too few pixels from the train sample
-    min_pixcount = conf.marker.getfloat('min_nb_pixels_train')
-    df_train_base = df_train_base[df_train_base[conf.columns['pixcount_s1s2']] >= min_pixcount]
-    logger.debug(f"Number of parcels in df_train_base after filter on pixcount >= {min_pixcount}: {len(df_train_base)}")
+    min_pixcount = conf.marker.getfloat("min_nb_pixels_train")
+    train_base_df = train_base_df[
+        train_base_df[conf.columns["pixcount_s1s2"]] >= min_pixcount
+    ]
+    logger.debug(
+        "Number of parcels in df_train_base after filter on pixcount >= "
+        f"{min_pixcount}: {len(train_base_df)}"
+    )
 
     # Some classes shouldn't be used for training... so remove them!
-    logger.info(f"Remove 'classes_to_ignore_for_train' from train sample (= where {class_column} is in: {conf.marker.getlist('classes_to_ignore_for_train')}")
-    df_train_base = df_train_base[~df_train_base[class_column].isin(conf.marker.getlist('classes_to_ignore_for_train'))]
+    logger.info(
+        "Remove 'classes_to_ignore_for_train' from train sample (= where "
+        f"{class_column} is in: {conf.marker.getlist('classes_to_ignore_for_train')}"
+    )
+    train_base_df = train_base_df[
+        ~train_base_df[class_column].isin(
+            conf.marker.getlist("classes_to_ignore_for_train")
+        )
+    ]
 
     # All classes_to_ignore aren't meant for training either...
-    logger.info(f"Remove 'classes_to_ignore' from train sample (= where {class_column} is in: {conf.marker.getlist('classes_to_ignore')}")
-    df_train_base = df_train_base[~df_train_base[class_column].isin(conf.marker.getlist('classes_to_ignore'))]
+    logger.info(
+        f"Remove 'classes_to_ignore' from train sample (= where {class_column} is in: "
+        f"{conf.marker.getlist('classes_to_ignore')}"
+    )
+    train_base_df = train_base_df[
+        ~train_base_df[class_column].isin(conf.marker.getlist("classes_to_ignore"))
+    ]
 
     # Print the train base result before applying any balancing
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
-        count_per_class = df_train_base.groupby(class_balancing_column, as_index=False).size()
-        logger.info(f"Number of elements per classname for train dataset, before balancing:\n{count_per_class}")
+    with pd.option_context(
+        "display.max_rows", None, "display.max_columns", None
+    ):  # type: ignore
+        count_per_class = train_base_df.groupby(
+            class_balancing_column, as_index=False
+        ).size()
+        logger.info(
+            "Number of elements per classname for train dataset, before balancing:\n"
+            f"{count_per_class}"
+        )
 
     # Depending on the balancing_strategy, use different way to get a training sample
-    if balancing_strategy == 'BALANCING_STRATEGY_NONE':
-        # Just use 25% of all non-test data as train data -> 25% of 80% of data -> 20% of all data
-        # will be training date
-        # Remark: - this is very unbalanced, eg. classes with 10.000 times the input size than other
-        #           classes
-        #         - this results in a relatively high accuracy in overall numbers, but the small
-        #           classes are not detected at all
-        df_train = (df_train_base
-                    .groupby(class_balancing_column, group_keys=False)
-                    .apply(pd.DataFrame.sample, frac=0.25))
-
-    elif balancing_strategy == 'BALANCING_STRATEGY_MEDIUM':
-        # Balance the train data, but still use some larger samples for the classes that have a lot
-        # of members in the input dataset
-        # Remark: with the upper limit of 10.000 this gives still OK results overall, and also the
-        #         smaller classes give some results with upper limit of 4000 results significantly
-        #         less good.
-
-        # For the larger classes, favor them by leaving the samples larger but cap at upper_limit
+    if balancing_strategy == "BALANCING_STRATEGY_NONE":
+        # Just use 25% of all non-test data as train data -> 25% of 80% of data -> 20%
+        # of all data will be training date
+        # Remark: - this is very unbalanced, eg. classes with 10.000 times the input
+        #           size than other classes
+        #         - this results in a relatively high accuracy in overall numbers, but
+        #           the small classes are not detected at all
+        train_df = train_base_df.groupby(
+            class_balancing_column, group_keys=False
+        ).apply(pd.DataFrame.sample, frac=0.25)
+
+    elif balancing_strategy == "BALANCING_STRATEGY_MEDIUM":
+        # Balance the train data, but still use some larger samples for the classes
+        # that have a lot of members in the input dataset
+        # Remark: with the upper limit of 10.000 this gives still OK results overall,
+        #     and also the smaller classes give some results with upper limit of 4000
+        #     results significantly less good.
+
+        # For the larger classes, favor them by leaving the samples larger but cap at
+        # upper_limit
         upper_limit = 10000
         lower_limit = 1000
-        logger.info(f"Cap over {upper_limit}, keep the full number of training sample till {lower_limit}, samples smaller than that are oversampled")
-        df_train = (df_train_base.groupby(class_balancing_column).filter(lambda x: len(x) >= upper_limit)
-                    .groupby(class_balancing_column, group_keys=False)
-                    .apply(pd.DataFrame.sample, upper_limit))
+        logger.info(
+            f"Cap over {upper_limit}, keep the full number of training sample till "
+            f"{lower_limit}, samples smaller than that are oversampled"
+        )
+        train_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= upper_limit)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, upper_limit)
+        )
         # Middle classes use the number as they are
-        df_train = df_train.append(df_train_base
-                                   .groupby(class_balancing_column).filter(lambda x: len(x) < upper_limit)
-                                   .groupby(class_balancing_column).filter(lambda x: len(x) >= lower_limit))
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < upper_limit)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= lower_limit)
+        )
         # For smaller classes, oversample...
-        df_train = df_train.append(df_train_base
-                                   .groupby(class_balancing_column).filter(lambda x: len(x) < lower_limit)
-                                   .groupby(class_balancing_column, group_keys=False)
-                                   .apply(pd.DataFrame.sample, lower_limit, replace=True))
-
-    elif balancing_strategy == 'BALANCING_STRATEGY_MEDIUM2':
-        # Balance the train data, but still use some larger samples for the classes that have a lot
-        # of members in the input dataset
-        # Remark: with the upper limit of 10.000 this gives still OK results overall, and also the
-        #         smaller classes give some results with upper limit of 4000 results significantly
-        #         less good.
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < lower_limit)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, lower_limit, replace=True)
+        )
+
+    elif balancing_strategy == "BALANCING_STRATEGY_MEDIUM2":
+        # Balance the train data, but still use some larger samples for the classes
+        # that have a lot of members in the input dataset
+        # Remark: with the upper limit of 10.000 this gives still OK results overall,
+        #     and also the smaller classes give some results with upper limit of 4000
+        #     results significantly less good.
 
         # For the larger classes, leave the samples larger but cap
         cap_count_limit1 = 100000
         cap_train_limit1 = 30000
-        logger.info(f"Cap balancing classes over {cap_count_limit1} to {cap_train_limit1}")
-        df_train = (df_train_base
-                    .groupby(class_balancing_column).filter(lambda x: len(x) >= cap_count_limit1)
-                    .groupby(class_balancing_column, group_keys=False)
-                    .apply(pd.DataFrame.sample, cap_train_limit1))
+        logger.info(
+            f"Cap balancing classes over {cap_count_limit1} to {cap_train_limit1}"
+        )
+        train_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= cap_count_limit1)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, cap_train_limit1)
+        )
         cap_count_limit2 = 50000
         cap_train_limit2 = 20000
-        logger.info(f"Cap balancing classes between {cap_count_limit2} and {cap_count_limit1} to {cap_train_limit2}")
-        train_cap2_df = (df_train_base
-                .groupby(class_balancing_column).filter(lambda x: len(x) < cap_count_limit1)
-                .groupby(class_balancing_column).filter(lambda x: len(x) >= cap_count_limit2)
-                .groupby(class_balancing_column, group_keys=False))
+        logger.info(
+            f"Cap balancing classes between {cap_count_limit2} and {cap_count_limit1} "
+            f"to {cap_train_limit2}"
+        )
+        train_cap2_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < cap_count_limit1)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= cap_count_limit2)
+            .groupby(class_balancing_column, group_keys=False)
+        )
         if len(train_cap2_df) > 0:
-            df_train = df_train.append(train_cap2_df.apply(pd.DataFrame.sample, cap_train_limit2))
+            train_df = train_df.append(
+                train_cap2_df.apply(pd.DataFrame.sample, cap_train_limit2)
+            )
         cap_count_limit3 = 20000
         cap_train_limit3 = 10000
-        logger.info(f"Cap balancing classes between {cap_count_limit3} and {cap_count_limit2} to {cap_train_limit3}")
-        df_train = df_train.append(df_train_base
-                .groupby(class_balancing_column).filter(lambda x: len(x) < cap_count_limit2)
-                .groupby(class_balancing_column).filter(lambda x: len(x) >= cap_count_limit3)
-                .groupby(class_balancing_column, group_keys=False)
-                .apply(pd.DataFrame.sample, cap_train_limit3))
+        logger.info(
+            f"Cap balancing classes between {cap_count_limit3} and {cap_count_limit2} "
+            f"to {cap_train_limit3}"
+        )
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < cap_count_limit2)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= cap_count_limit3)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, cap_train_limit3)
+        )
         cap_count_limit4 = 10000
         cap_train_limit4 = 10000
-        logger.info(f"Cap balancing classes between {cap_count_limit4} and {cap_count_limit3} to {cap_train_limit4}")
-        df_train = df_train.append(df_train_base
-                .groupby(class_balancing_column).filter(lambda x: len(x) < cap_count_limit3)
-                .groupby(class_balancing_column).filter(lambda x: len(x) >= cap_count_limit4)
-                .groupby(class_balancing_column, group_keys=False)
-                .apply(pd.DataFrame.sample, cap_train_limit4))
+        logger.info(
+            f"Cap balancing classes between {cap_count_limit4} and {cap_count_limit3} "
+            f"to {cap_train_limit4}"
+        )
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < cap_count_limit3)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= cap_count_limit4)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, cap_train_limit4)
+        )
         oversample_count = 1000
         # Middle classes use the number as they are
-        logger.info(f"For classes between {cap_count_limit4} and {oversample_count}, just use all samples")
-        df_train = df_train.append(df_train_base
-                                   .groupby(class_balancing_column).filter(lambda x: len(x) < cap_count_limit4)
-                                   .groupby(class_balancing_column).filter(lambda x: len(x) >= oversample_count))
+        logger.info(
+            f"For classes between {cap_count_limit4} and {oversample_count}, just use "
+            "all samples"
+        )
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < cap_count_limit4)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= oversample_count)
+        )
         # For smaller classes, oversample...
-        logger.info(f"For classes smaller than {oversample_count}, oversample to {oversample_count}")
-        df_train = df_train.append(df_train_base
-                                   .groupby(class_balancing_column).filter(lambda x: len(x) < oversample_count)
-                                   .groupby(class_balancing_column, group_keys=False)
-                                   .apply(pd.DataFrame.sample, oversample_count, replace=True))
-
-    elif balancing_strategy == 'BALANCING_STRATEGY_PROPORTIONAL_GROUPS':
-        # Balance the train data, but still use some larger samples for the classes that have a lot
-        # of members in the input dataset
-        # Remark: with the upper limit of 10.000 this gives still OK results overall, and also the
-        #         smaller classes give some results with upper limit of 4000 results significantly
-        #         less good.
+        logger.info(
+            f"For classes smaller than {oversample_count}, oversample to "
+            f"{oversample_count}"
+        )
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < oversample_count)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, oversample_count, replace=True)
+        )
+
+    elif balancing_strategy == "BALANCING_STRATEGY_PROPORTIONAL_GROUPS":
+        # Balance the train data, but still use some larger samples for the classes
+        # that have a lot of members in the input dataset
+        # Remark: with the upper limit of 10.000 this gives still OK results overall,
+        #     and also the smaller classes give some results with upper limit of 4000
+        #     results significantly less good.
 
         # For the larger classes, leave the samples larger but cap
         upper_count_limit1 = 100000
         upper_train_limit1 = 30000
-        logger.info(f"Cap balancing classes over {upper_count_limit1} to {upper_train_limit1}")
-        df_train = (df_train_base
-                    .groupby(class_balancing_column).filter(lambda x: len(x) >= upper_count_limit1)
-                    .groupby(class_balancing_column, group_keys=False)
-                    .apply(pd.DataFrame.sample, upper_train_limit1))
+        logger.info(
+            f"Cap balancing classes over {upper_count_limit1} to {upper_train_limit1}"
+        )
+        train_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= upper_count_limit1)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, upper_train_limit1)
+        )
         upper_count_limit2 = 50000
         upper_train_limit2 = 20000
-        logger.info(f"Cap balancing classes between {upper_count_limit2} and {upper_count_limit1} to {upper_train_limit2}")
-        df_train = df_train.append(df_train_base
-                .groupby(class_balancing_column).filter(lambda x: len(x) < upper_count_limit1)
-                .groupby(class_balancing_column).filter(lambda x: len(x) >= upper_count_limit2)
-                .groupby(class_balancing_column, group_keys=False)
-                .apply(pd.DataFrame.sample, upper_train_limit2))
+        logger.info(
+            f"Cap balancing classes between {upper_count_limit2} and "
+            f"{upper_count_limit1} to {upper_train_limit2}"
+        )
+        train_limit2_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < upper_count_limit1)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= upper_count_limit2)
+            .groupby(class_balancing_column, group_keys=False)
+        )
+        if len(train_limit2_df) > 0:
+            train_df = train_df.append(
+                train_limit2_df.apply(pd.DataFrame.sample, upper_train_limit2)
+        )
         upper_count_limit3 = 20000
         upper_train_limit3 = 10000
-        logger.info(f"Cap balancing classes between {upper_count_limit3} and {upper_count_limit2} to {upper_train_limit3}")
-        df_train = df_train.append(df_train_base
-                .groupby(class_balancing_column).filter(lambda x: len(x) < upper_count_limit2)
-                .groupby(class_balancing_column).filter(lambda x: len(x) >= upper_count_limit3)
-                .groupby(class_balancing_column, group_keys=False)
-                .apply(pd.DataFrame.sample, upper_train_limit3))
+        logger.info(
+            f"Cap balancing classes between {upper_count_limit3} and "
+            f"{upper_count_limit2} to {upper_train_limit3}"
+        )
+        train_limit3_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < upper_count_limit2)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= upper_count_limit3)
+            .groupby(class_balancing_column, group_keys=False)
+        )
+        if len(train_limit3_df) > 0:
+            train_df = train_df.append(
+                train_limit3_df.apply(pd.DataFrame.sample, upper_train_limit3)
+            )
         upper_count_limit4 = 10000
         upper_train_limit4 = 5000
-        logger.info(f"Cap balancing classes between {upper_count_limit4} and {upper_count_limit3} to {upper_train_limit4}")
-        df_train = df_train.append(df_train_base
-                .groupby(class_balancing_column).filter(lambda x: len(x) < upper_count_limit3)
-                .groupby(class_balancing_column).filter(lambda x: len(x) >= upper_count_limit4)
-                .groupby(class_balancing_column, group_keys=False)
-                .apply(pd.DataFrame.sample, upper_train_limit4))
-
+        logger.info(
+            f"Cap balancing classes between {upper_count_limit4} and "
+            f"{upper_count_limit3} to {upper_train_limit4}"
+        )
+        train_limit4_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) < upper_count_limit3)
+            .groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= upper_count_limit4)
+            .groupby(class_balancing_column, group_keys=False)
+        )
+        if len(train_limit4_df) > 0:
+            train_df = train_df.append(
+                train_limit4_df.apply(pd.DataFrame.sample, upper_train_limit4)
+            )
         # For smaller balancing classes, just use all samples
-        df_train = df_train.append(
-                df_train_base.groupby(class_balancing_column).filter(lambda x: len(x) < upper_count_limit4))
-
-    elif balancing_strategy == 'BALANCING_STRATEGY_UPPER_LIMIT':
-        # Balance the train data, but still use some larger samples for the classes that have a lot
-        # of members in the input dataset
-        # Remark: with the upper limit of 10.000 this gives still OK results overall, and also the
-        #         smaller classes give some results with upper limit of 4000 results significantly
-        #         less good.
-
-        # For the larger classes, favor them by leaving the samples larger but cap at upper_limit
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column).filter(
+                lambda x: len(x) < upper_count_limit4
+            )
+        )
+
+    elif balancing_strategy == "BALANCING_STRATEGY_UPPER_LIMIT":
+        # Balance the train data, but still use some larger samples for the classes
+        # that have a lot of members in the input dataset
+        # Remark: with the upper limit of 10.000 this gives still OK results overall,
+        #     and also the smaller classes give some results with upper limit of 4000
+        #     results significantly less good.
+
+        # For the larger classes, favor them by leaving the samples larger but cap at
+        # upper_limit
         upper_limit = 10000
         logger.info(f"Cap over {upper_limit}...")
-        df_train = (df_train_base.groupby(class_balancing_column).filter(lambda x: len(x) >= upper_limit)
-                    .groupby(class_balancing_column, group_keys=False)
-                    .apply(pd.DataFrame.sample, upper_limit))
+        train_df = (
+            train_base_df.groupby(class_balancing_column)
+            .filter(lambda x: len(x) >= upper_limit)
+            .groupby(class_balancing_column, group_keys=False)
+            .apply(pd.DataFrame.sample, upper_limit)
+        )
         # For smaller classes, just use all samples
-        df_train = df_train.append(df_train_base
-                                   .groupby(class_balancing_column).filter(lambda x: len(x) < upper_limit))
-
-    elif balancing_strategy == 'BALANCING_STRATEGY_EQUAL':
-        # In theory the most logical way to balance: make sure all classes have the same amount of
-        # training data by undersampling the largest classes and oversampling the small classes.
-        df_train = (df_train_base.groupby(class_balancing_column, group_keys=False)
-                    .apply(pd.DataFrame.sample, 2000, replace=True))
+        train_df = train_df.append(
+            train_base_df.groupby(class_balancing_column).filter(
+                lambda x: len(x) < upper_limit
+            )
+        )
+
+    elif balancing_strategy == "BALANCING_STRATEGY_EQUAL":
+        # In theory the most logical way to balance: make sure all classes have the
+        # same amount of training data by undersampling the largest classes and
+        # oversampling the small classes.
+        train_df = train_base_df.groupby(
+            class_balancing_column, group_keys=False
+        ).apply(pd.DataFrame.sample, 2000, replace=True)
 
     else:
         raise Exception(f"Unknown balancing strategy, STOP!: {balancing_strategy}")
 
     # Log the resulting numbers per class in the train sample
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
-        count_per_class = df_train.groupby(class_balancing_column, as_index=False).size()
-        logger.info(f'Number of elements per class_balancing_column in train dataset:\n{count_per_class}')
+    with pd.option_context(
+        "display.max_rows", None, "display.max_columns", None
+    ):  # type: ignore
+        count_per_class = train_df.groupby(
+            class_balancing_column, as_index=False
+        ).size()
+        logger.info(
+            "Number of elements per class_balancing_column in train dataset:\n"
+            f"{count_per_class}"
+        )
         if class_balancing_column != class_column:
-            count_per_class = df_train.groupby(class_column, as_index=False).size()
-            logger.info(f'Number of elements per class_column in train dataset:\n{count_per_class}')
+            count_per_class = train_df.groupby(class_column, as_index=False).size()
+            logger.info(
+                "Number of elements per class_column in train dataset:\n"
+                f"{count_per_class}"
+            )
 
     # Log the resulting numbers per class in the test sample
-    with pd.option_context('display.max_rows', None, 'display.max_columns', None):
-        count_per_class = df_test.groupby(class_balancing_column, as_index=False).size()
-        logger.info(f'Number of elements per class_balancing_column in test dataset:\n{count_per_class}')
+    with pd.option_context(
+        "display.max_rows", None, "display.max_columns", None
+    ):  # type: ignore
+        count_per_class = test_df.groupby(class_balancing_column, as_index=False).size()
+        logger.info(
+            "Number of elements per class_balancing_column in test dataset:\n"
+            f"{count_per_class}"
+        )
         if class_balancing_column != class_column:
-            count_per_class = df_test.groupby(class_column, as_index=False).size()
-            logger.info(f'Number of elements per class_column in test dataset:\n{count_per_class}')
+            count_per_class = test_df.groupby(class_column, as_index=False).size()
+            logger.info(
+                "Number of elements per class_column in test dataset:\n"
+                f"{count_per_class}"
+            )
 
     # Write to output files
-    logger.info('Write the output files')
-    df_train.set_index(conf.columns['id'], inplace=True)
-    df_test.set_index(conf.columns['id'], inplace=True)
-    pdh.to_file(df_train, output_parcel_train_filepath)    # The ID column is the index...
-    pdh.to_file(df_test, output_parcel_test_filepath)      # The ID column is the index...
+    logger.info("Write the output files")
+    train_df.set_index(conf.columns["id"], inplace=True)
+    test_df.set_index(conf.columns["id"], inplace=True)
+    pdh.to_file(train_df, output_parcel_train_path)  # The ID column is the index...
+    pdh.to_file(test_df, output_parcel_test_path)  # The ID column is the index...
+
 
 # If the script is run directly...
 if __name__ == "__main__":
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/preprocess/classification_preprocess_BEFL.py b/cropclassification/preprocess/classification_preprocess_BEFL.py
index 8789674c..df3aee9f 100644
--- a/cropclassification/preprocess/classification_preprocess_BEFL.py
+++ b/cropclassification/preprocess/classification_preprocess_BEFL.py
@@ -7,7 +7,6 @@
 """
 
 import logging
-import os
 from pathlib import Path
 
 import cropclassification.helpers.config_helper as conf
@@ -18,36 +17,40 @@
 logger = logging.getLogger(__name__)
 
 # define some specific BEFL column names
-column_BEFL_earlylate = 'MON_EARLY_LATE'     # Is it an early or a late crop?
-column_BEFL_gesp_pm = 'GESP_PM'              # Gespecialiseerde productiemethode
-column_BEFL_gis_area = 'GRAF_OPP'            # GIS Area
-column_BEFL_status_perm_grass = 'STAT_BGV'   # Status permanent grassland
-column_BEFL_crop = 'GWSCOD_H'
-column_BEFL_crop_declared = 'GWSCOD_H_A'
-column_BEFL_crop_gt_verified = 'HOOFDTEELT_CTRL_COD'
-column_BEFL_crop_gt_unverified = 'HOOFDTEELT_CTRL_COD_ORIG'
-column_BEFL_latecrop = 'GWSCOD_N'
-
-# BEFL specific columns we want keep 
+column_BEFL_earlylate = "MON_EARLY_LATE"  # Is it an early or a late crop?
+column_BEFL_gesp_pm = "GESP_PM"  # Gespecialiseerde productiemethode
+column_BEFL_gis_area = "GRAF_OPP"  # GIS Area
+column_BEFL_status_perm_grass = "STAT_BGV"  # Status permanent grassland
+column_BEFL_crop = "GWSCOD_H"
+column_BEFL_crop_declared = "GWSCOD_H_A"
+column_BEFL_crop_gt_verified = "HOOFDTEELT_CTRL_COD"
+column_BEFL_crop_gt_unverified = "HOOFDTEELT_CTRL_COD_ORIG"
+column_BEFL_latecrop = "GWSCOD_N"
+
+# BEFL specific columns we want keep
 columns_BEFL_to_keep = [
-        column_BEFL_earlylate, 
-        column_BEFL_gesp_pm, 
-        column_BEFL_gis_area, 
-        column_BEFL_status_perm_grass,
-        column_BEFL_crop_gt_verified,
-        column_BEFL_crop_gt_unverified,
-        column_BEFL_crop,
-        column_BEFL_crop_declared,
-        column_BEFL_latecrop]
-
-#-------------------------------------------------------------
+    column_BEFL_earlylate,
+    column_BEFL_gesp_pm,
+    column_BEFL_gis_area,
+    column_BEFL_status_perm_grass,
+    column_BEFL_crop_gt_verified,
+    column_BEFL_crop_gt_unverified,
+    column_BEFL_crop,
+    column_BEFL_crop_declared,
+    column_BEFL_latecrop,
+]
+
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
-def prepare_input(input_parcel_filepath: Path,
-                  classtype_to_prepare: str,
-                  classes_refe_filepath: Path,
-                  output_dir: Path):
+
+def prepare_input(
+    input_parcel_path: Path,
+    classtype_to_prepare: str,
+    classes_refe_path: Path,
+    output_dir: Path,
+):
     """
     This function creates a file that is compliant with the assumptions used by the rest of the
     classification functionality.
@@ -57,160 +60,186 @@ def prepare_input(input_parcel_filepath: Path,
         - classname: a string column with a readable name of the classes that will be classified to
     """
     # Check if input parameters are OK
-    if not input_parcel_filepath.exists():
-        raise Exception(f"Input file doesn't exist: {input_parcel_filepath}")
+    if not input_parcel_path.exists():
+        raise Exception(f"Input file doesn't exist: {input_parcel_path}")
     else:
-        logger.info(f"Process input file {input_parcel_filepath}")
+        logger.info(f"Process input file {input_parcel_path}")
 
     # Read input file
-    logger.info(f"Read parceldata from {input_parcel_filepath}")
-    if geofile_util.is_geofile(input_parcel_filepath):
-        parceldata_df = geofile_util.read_file(input_parcel_filepath)
+    logger.info(f"Read parceldata from {input_parcel_path}")
+    if geofile_util.is_geofile(input_parcel_path):
+        parceldata_df = geofile_util.read_file(input_parcel_path)
     else:
-        parceldata_df = pdh.read_file(input_parcel_filepath)
+        parceldata_df = pdh.read_file(input_parcel_path)
     logger.info(f"Read Parceldata ready, info(): {parceldata_df.info()}")
 
     # Check if the id column is present...
-    if conf.columns['id'] not in parceldata_df.columns:
-        message = f"Column {conf.columns['id']} not found in input parcel file: {input_parcel_filepath}. Make sure the column is present or change the column name in global_constants.py"
+    if conf.columns["id"] not in parceldata_df.columns:
+        message = f"Column {conf.columns['id']} not found in input parcel file: {input_parcel_path}. Make sure the column is present or change the column name in global_constants.py"
         logger.critical(message)
         raise Exception(message)
 
-    # Now start prepare 
-    if classtype_to_prepare == 'CROPGROUP':
+    # Now start prepare
+    if classtype_to_prepare == "CROPGROUP":
         parceldata_df = prepare_input_cropgroup(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_declared,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_declared,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_cropgroup(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'CROPGROUP-GROUNDTRUTH':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "CROPGROUP-GROUNDTRUTH":
         return prepare_input_cropgroup(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_gt_verified,
-                column_output_class=conf.columns['class_groundtruth'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'CROPGROUP-EARLY':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_gt_verified,
+            column_output_class=conf.columns["class_groundtruth"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "CROPGROUP-EARLY":
         parceldata_df = prepare_input_cropgroup_early(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_declared,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_declared,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_cropgroup_early(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'CROPGROUP-EARLY-GROUNDTRUTH':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "CROPGROUP-EARLY-GROUNDTRUTH":
         return prepare_input_cropgroup_early(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_gt_verified,
-                column_output_class=conf.columns['class_groundtruth'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'LANDCOVER':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_gt_verified,
+            column_output_class=conf.columns["class_groundtruth"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "LANDCOVER":
         parceldata_df = prepare_input_landcover(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_declared,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_declared,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_landcover(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'MULTICROP':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "MULTICROP":
         parceldata_df = prepare_input_landcover(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_declared,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_declared,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_landcover(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'LANDCOVER-GROUNDTRUTH':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "LANDCOVER-GROUNDTRUTH":
         return prepare_input_landcover(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_gt_verified,
-                column_output_class=conf.columns['class_groundtruth'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'LANDCOVER-EARLY':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_gt_verified,
+            column_output_class=conf.columns["class_groundtruth"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "LANDCOVER-EARLY":
         parceldata_df = prepare_input_landcover_early(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_declared,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_declared,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_landcover_early(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'LANDCOVER-EARLY-GROUNDTRUTH':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "LANDCOVER-EARLY-GROUNDTRUTH":
         return prepare_input_landcover_early(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_gt_verified,
-                column_output_class=conf.columns['class_groundtruth'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'POPULAR-CROP':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_gt_verified,
+            column_output_class=conf.columns["class_groundtruth"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "POPULAR-CROP":
         parceldata_df = prepare_input_most_popular_crop(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_declared,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_declared,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_most_popular_crop(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'POPULAR-CROP-GROUNDTRUTH':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "POPULAR-CROP-GROUNDTRUTH":
         return prepare_input_most_popular_crop(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_gt_verified,
-                column_output_class=conf.columns['class_groundtruth'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'LATECROP':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_gt_verified,
+            column_output_class=conf.columns["class_groundtruth"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "LATECROP":
         parceldata_df = prepare_input_latecrop(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_latecrop,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_latecrop,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_latecrop(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_latecrop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'FABACEAE':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_latecrop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "FABACEAE":
         parceldata_df = prepare_input_fabaceae(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class_declared'],
-                classes_refe_filepath=classes_refe_filepath)
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class_declared"],
+            classes_refe_path=classes_refe_path,
+        )
         return prepare_input_fabaceae(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop,
-                column_output_class=conf.columns['class'],
-                classes_refe_filepath=classes_refe_filepath)
-    elif classtype_to_prepare == 'FABACEAE-GROUNDTRUTH':
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop,
+            column_output_class=conf.columns["class"],
+            classes_refe_path=classes_refe_path,
+        )
+    elif classtype_to_prepare == "FABACEAE-GROUNDTRUTH":
         return prepare_input_fabaceae(
-                parceldata_df=parceldata_df,
-                column_BEFL_cropcode=column_BEFL_crop_gt_verified,
-                column_output_class=conf.columns['class_groundtruth'],
-                classes_refe_filepath=classes_refe_filepath)          
+            parceldata_df=parceldata_df,
+            column_BEFL_cropcode=column_BEFL_crop_gt_verified,
+            column_output_class=conf.columns["class_groundtruth"],
+            classes_refe_path=classes_refe_path,
+        )
     else:
-        message = f"Unknown value for parameter classtype_to_prepare: {classtype_to_prepare}"
+        message = (
+            f"Unknown value for parameter classtype_to_prepare: {classtype_to_prepare}"
+        )
         logger.critical(message)
         raise Exception(message)
 
+
 def prepare_input_cropgroup(
-            parceldata_df,
-            column_BEFL_cropcode: str,
-            column_output_class: str,
-            classes_refe_filepath: Path):
+    parceldata_df,
+    column_BEFL_cropcode: str,
+    column_output_class: str,
+    classes_refe_path: Path,
+):
     """
     This function creates a file that is compliant with the assumptions used by the rest of the
     classification functionality.
@@ -223,72 +252,88 @@ def prepare_input_cropgroup(
     this format.
     """
     # Check if parameters are OK and init some extra params
-    #--------------------------------------------------------------------------
-    if not classes_refe_filepath.exists():
-        raise Exception(f"Input classes file doesn't exist: {classes_refe_filepath}")
+    # --------------------------------------------------------------------------
+    if not classes_refe_path.exists():
+        raise Exception(f"Input classes file doesn't exist: {classes_refe_path}")
 
     # Convert the crop to unicode, in case the input is int...
     if column_BEFL_cropcode in parceldata_df.columns:
-        parceldata_df[column_BEFL_cropcode] = parceldata_df[column_BEFL_cropcode].astype('unicode')
-        
+        parceldata_df[column_BEFL_cropcode] = parceldata_df[
+            column_BEFL_cropcode
+        ].astype("unicode")
+
     # Read and cleanup the mapping table from crop codes to classes
-    #--------------------------------------------------------------------------
-    logger.info(f"Read classes conversion table from {classes_refe_filepath}")
-    classes_df = pdh.read_file(classes_refe_filepath)
+    # --------------------------------------------------------------------------
+    logger.info(f"Read classes conversion table from {classes_refe_path}")
+    classes_df = pdh.read_file(classes_refe_path)
     logger.info(f"Read classes conversion table ready, info(): {classes_df.info()}")
 
     # Because the file was read as ansi, and gewas is int, so the data needs to be converted to
     # unicode to be able to do comparisons with the other data
-    classes_df[column_BEFL_cropcode] = classes_df['CROPCODE'].astype('unicode')
+    classes_df[column_BEFL_cropcode] = classes_df["CROPCODE"].astype("unicode")
 
     # Map column with the classname to orig classname
-    column_output_class_orig = conf.columns['class'] + '_orig'
-    classes_df[column_output_class_orig] = classes_df[conf.columns['class_refe']]
+    column_output_class_orig = conf.columns["class"] + "_orig"
+    classes_df[column_output_class_orig] = classes_df[conf.columns["class_refe"]]
 
     # Remove unneeded columns
     for column in classes_df.columns:
-        if(column not in [column_output_class_orig, column_BEFL_cropcode]
-           and column not in columns_BEFL_to_keep):
+        if (
+            column not in [column_output_class_orig, column_BEFL_cropcode]
+            and column not in columns_BEFL_to_keep
+        ):
             classes_df.drop(column, axis=1, inplace=True)
 
     # Set the index
     classes_df.set_index(column_BEFL_cropcode, inplace=True, verify_integrity=True)
 
     # Get only the columns in the classes_df that don't exist yet in parceldata_df
-    cols_to_join = classes_df.columns.difference(parceldata_df.columns) 
+    cols_to_join = classes_df.columns.difference(parceldata_df.columns)
 
     # Join/merge the classname
-    logger.info('Add the classes to the parceldata')
+    logger.info("Add the classes to the parceldata")
     parceldata_df = parceldata_df.merge(
-            classes_df[cols_to_join], how='left', left_on=column_BEFL_cropcode,
-            right_index=True, validate='many_to_one')
+        classes_df[cols_to_join],
+        how="left",
+        left_on=column_BEFL_cropcode,
+        right_index=True,
+        validate="many_to_one",
+    )
 
     # Copy orig classname to classification classname
     parceldata_df.insert(
-            loc=0, column=column_output_class, value=parceldata_df[column_output_class_orig])
-    
+        loc=0, column=column_output_class, value=parceldata_df[column_output_class_orig]
+    )
+
     # For rows with no class, set to UNKNOWN
-    parceldata_df.fillna(value={column_output_class: 'UNKNOWN'}, inplace=True) 
+    parceldata_df.fillna(value={column_output_class: "UNKNOWN"}, inplace=True)
 
     # If a column with extra info exists, use it as well to fine-tune the classification classes.
     if column_BEFL_gesp_pm in parceldata_df.columns:
         # Serres, tijdelijke overkappingen en loodsen
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['SER', 'SGM']), 
-                          column_output_class] = 'MON_OVERK_LOO'
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['PLA', 'PLO', 'NPO']), 
-                          column_output_class] = 'MON_OVERK_LOO'
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'LOO', 
-                          column_output_class] = 'MON_OVERK_LOO'           # Een loods is hetzelfde als een stal...
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CON', 
-                          column_output_class] = 'MON_CONTAINERS'          # Containers, niet op volle grond...
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(["SER", "SGM"]), column_output_class
+        ] = "MON_OVERK_LOO"
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(["PLA", "PLO", "NPO"]),
+            column_output_class,
+        ] = "MON_OVERK_LOO"
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm] == "LOO", column_output_class
+        ] = "MON_OVERK_LOO"  # Een loods is hetzelfde als een stal...
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm] == "CON", column_output_class
+        ] = "MON_CONTAINERS"  # Containers, niet op volle grond...
         # TODO: CIV, containers in volle grond, lijkt niet zo specifiek te zijn...
-        #parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
+        # parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
     else:
-        logger.warning(f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!")
+        logger.warning(
+            f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!"
+        )
 
     # Some extra cleanup: classes starting with 'nvt' or empty ones
-    #logger.info("Set classes that are still empty, not specific enough or that contain to little values to 'UNKNOWN'")
-    #parceldata_df.loc[parceldata_df[column_output_class].str.startswith('nvt', na=True), 
+    # logger.info("Set classes that are still empty, not specific enough or that contain to little values to 'UNKNOWN'")
+    # parceldata_df.loc[parceldata_df[column_output_class].str.startswith('nvt', na=True),
     #                  column_output_class] = 'UNKNOWN'
 
     # 'MON_ANDERE_SUBSID_GEWASSEN': very low classification rate (< 1%), as it is a group with several very different classes in it
@@ -300,19 +345,19 @@ def prepare_input_cropgroup(
     # 'MON_RAAPACHTIGEN': 25% correct classifications: rest spread over many other classes
     # 'MON_STRUIK': 10%
     #    TODO: nakijken, wss opsplitsen of toevoegen aan MON_BOOMKWEEK???
-    #classes_badresults = ['MON_ANDERE_SUBSID_GEWASSEN', 'MON_AARDBEIEN', 'MON_BRAAK', 'MON_KLAVER', 
+    # classes_badresults = ['MON_ANDERE_SUBSID_GEWASSEN', 'MON_AARDBEIEN', 'MON_BRAAK', 'MON_KLAVER',
     #                      'MON_MENGSEL', 'MON_POEL', 'MON_RAAPACHTIGEN', 'MON_STRUIK']
-    #parceldata_df.loc[parceldata_df[column_output_class].isin(classes_badresults), 
+    # parceldata_df.loc[parceldata_df[column_output_class].isin(classes_badresults),
     #                  column_output_class] = 'UNKNOWN'
 
     # MON_BONEN en MON_WIKKEN have omongst each other a very large percentage of false
     # positives/negatives, so they seem very similar... lets create a class that combines both
-    #parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BONEN', 'MON_WIKKEN']), 
+    # parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BONEN', 'MON_WIKKEN']),
     #                  column_output_class] = 'MON_BONEN_WIKKEN'
 
     # MON_BOOM includes now also the growing new plants/trees, which is too differenct from grown
     # trees -> put growing new trees is seperate group
-    #parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['9602', '9603', '9604', '9560']), 
+    # parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['9602', '9603', '9604', '9560']),
     #                  column_output_class] = 'MON_BOOMKWEEK'
 
     # 'MON_FRUIT': has a good accuracy (91%), but also has as much false positives (115% -> mainly
@@ -322,34 +367,53 @@ def prepare_input_cropgroup(
     # MON_FRUIT and MON_BOOM are permanent anyway, so not mandatory that they are checked in
     # monitoring process.
     # Conclusion: put MON_BOOM and MON_FRUIT to IGNORE_DIFFICULT_PERMANENT_CLASS
-    #parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BOOM', 'MON_FRUIT']), 
+    # parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BOOM', 'MON_FRUIT']),
     #                  column_output_class] = 'IGNORE_DIFFICULT_PERMANENT_CLASS'
 
     # Set classes with very few elements to IGNORE_NOT_ENOUGH_SAMPLES!
-    for _, row in parceldata_df.groupby(column_output_class).size().reset_index(name='count').iterrows():
-        if row['count'] <= 50:
-            logger.info(f"Class <{row[column_output_class]}> only contains {row['count']} elements, so put them to IGNORE_NOT_ENOUGH_SAMPLES")
-            parceldata_df.loc[parceldata_df[column_output_class] == row[column_output_class], 
-                              column_output_class] = 'IGNORE_NOT_ENOUGH_SAMPLES'
+    for _, row in (
+        parceldata_df.groupby(column_output_class)
+        .size()
+        .reset_index(name="count")
+        .iterrows()
+    ):
+        if row["count"] <= 50:
+            logger.info(
+                f"Class <{row[column_output_class]}> only contains {row['count']} elements, so put them to IGNORE_NOT_ENOUGH_SAMPLES"
+            )
+            parceldata_df.loc[
+                parceldata_df[column_output_class] == row[column_output_class],
+                column_output_class,
+            ] = "IGNORE_NOT_ENOUGH_SAMPLES"
 
     # Drop the columns that aren't useful at all
     for column in parceldata_df.columns:
-        if(column not in [column_output_class, conf.columns['id'], 
-                          conf.columns['class_groundtruth'], 
-                          conf.columns['class_declared']]
-             and column not in conf.preprocess.getlist('extra_export_columns')
-             and column not in columns_BEFL_to_keep):
+        if (
+            column
+            not in [
+                column_output_class,
+                conf.columns["id"],
+                conf.columns["class_groundtruth"],
+                conf.columns["class_declared"],
+            ]
+            and column not in conf.preprocess.getlist("extra_export_columns")
+            and column not in columns_BEFL_to_keep
+        ):
             parceldata_df.drop(column, axis=1, inplace=True)
         elif column == column_BEFL_gesp_pm:
-            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[column_BEFL_gesp_pm].str.replace(',', ';')
+            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[
+                column_BEFL_gesp_pm
+            ].str.replace(",", ";")
 
     return parceldata_df
 
+
 def prepare_input_fabaceae(
-            parceldata_df,
-            column_BEFL_cropcode: str,
-            column_output_class: str,
-            classes_refe_filepath: Path):
+    parceldata_df,
+    column_BEFL_cropcode: str,
+    column_output_class: str,
+    classes_refe_path: Path,
+):
     """
     This function creates a file that is compliant with the assumptions used by the rest of the
     classification functionality.
@@ -362,76 +426,94 @@ def prepare_input_fabaceae(
     this format.
     """
     # Check if parameters are OK and init some extra params
-    #--------------------------------------------------------------------------
-    if not classes_refe_filepath.exists():
-        raise Exception(f"Input classes file doesn't exist: {classes_refe_filepath}")
+    # --------------------------------------------------------------------------
+    if not classes_refe_path.exists():
+        raise Exception(f"Input classes file doesn't exist: {classes_refe_path}")
 
     # Convert the crop to unicode, in case the input is int...
     if column_BEFL_cropcode in parceldata_df.columns:
-        parceldata_df[column_BEFL_cropcode] = parceldata_df[column_BEFL_cropcode].astype('unicode')
-        
+        parceldata_df[column_BEFL_cropcode] = parceldata_df[
+            column_BEFL_cropcode
+        ].astype("unicode")
+
     # Read and cleanup the mapping table from crop codes to classes
-    #--------------------------------------------------------------------------
-    logger.info(f"Read classes conversion table from {classes_refe_filepath}")
-    classes_df = pdh.read_file(classes_refe_filepath)
+    # --------------------------------------------------------------------------
+    logger.info(f"Read classes conversion table from {classes_refe_path}")
+    classes_df = pdh.read_file(classes_refe_path)
     logger.info(f"Read classes conversion table ready, info(): {classes_df.info()}")
 
     # Because the file was read as ansi, and gewas is int, so the data needs to be converted to
     # unicode to be able to do comparisons with the other data
-    classes_df[column_BEFL_cropcode] = classes_df['CROPCODE'].astype('unicode')
+    classes_df[column_BEFL_cropcode] = classes_df["CROPCODE"].astype("unicode")
 
     # Map column with the classname to orig classname
-    column_output_class_orig = conf.columns['class'] + '_orig'
-    classes_df[column_output_class_orig] = classes_df[conf.columns['class_refe']]
+    column_output_class_orig = conf.columns["class"] + "_orig"
+    classes_df[column_output_class_orig] = classes_df[conf.columns["class_refe"]]
 
     # Remove unneeded columns
     for column in classes_df.columns:
-        if(column not in [column_output_class_orig, column_BEFL_cropcode]
-           and column not in columns_BEFL_to_keep):
+        if (
+            column not in [column_output_class_orig, column_BEFL_cropcode]
+            and column not in columns_BEFL_to_keep
+        ):
             classes_df.drop(column, axis=1, inplace=True)
 
     # Set the index
     classes_df.set_index(column_BEFL_cropcode, inplace=True, verify_integrity=True)
 
     # Get only the columns in the classes_df that don't exist yet in parceldata_df
-    cols_to_join = classes_df.columns.difference(parceldata_df.columns) 
+    cols_to_join = classes_df.columns.difference(parceldata_df.columns)
 
     # Filter the parcels to only keep the classes we are interested in
     # TODO: this shouldn't be hardcoded!
-    parceldata_df = parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['60', '700', '660', '732', '723'])]
+    parceldata_df = parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["60", "700", "660", "732", "723"])
+    ]
 
     # Join/merge the classname
-    logger.info('Add the classes to the parceldata')
+    logger.info("Add the classes to the parceldata")
     parceldata_df = parceldata_df.merge(
-            classes_df[cols_to_join], how='left', left_on=column_BEFL_cropcode,
-            right_index=True, validate='many_to_one')
+        classes_df[cols_to_join],
+        how="left",
+        left_on=column_BEFL_cropcode,
+        right_index=True,
+        validate="many_to_one",
+    )
 
     # Copy orig classname to classification classname
     parceldata_df.insert(
-            loc=0, column=column_output_class, value=parceldata_df[column_output_class_orig])
-    
+        loc=0, column=column_output_class, value=parceldata_df[column_output_class_orig]
+    )
+
     # For rows with no class, set to UNKNOWN
-    parceldata_df.fillna(value={column_output_class: 'UNKNOWN'}, inplace=True) 
+    parceldata_df.fillna(value={column_output_class: "UNKNOWN"}, inplace=True)
 
     # If a column with extra info exists, use it as well to fine-tune the classification classes.
     if column_BEFL_gesp_pm in parceldata_df.columns:
         # Serres, tijdelijke overkappingen en loodsen
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['SER', 'SGM']), 
-                          column_output_class] = 'MON_OVERK_LOO'
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['PLA', 'PLO', 'NPO']), 
-                          column_output_class] = 'MON_OVERK_LOO'
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'LOO', 
-                          column_output_class] = 'MON_OVERK_LOO'           # Een loods is hetzelfde als een stal...
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CON', 
-                          column_output_class] = 'MON_CONTAINERS'          # Containers, niet op volle grond...
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(["SER", "SGM"]), column_output_class
+        ] = "MON_OVERK_LOO"
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(["PLA", "PLO", "NPO"]),
+            column_output_class,
+        ] = "MON_OVERK_LOO"
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm] == "LOO", column_output_class
+        ] = "MON_OVERK_LOO"  # Een loods is hetzelfde als een stal...
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm] == "CON", column_output_class
+        ] = "MON_CONTAINERS"  # Containers, niet op volle grond...
         # TODO: CIV, containers in volle grond, lijkt niet zo specifiek te zijn...
-        #parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
+        # parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
     else:
-        logger.warning(f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!")
+        logger.warning(
+            f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!"
+        )
 
     # Some extra cleanup: classes starting with 'nvt' or empty ones
-    #logger.info("Set classes that are still empty, not specific enough or that contain to little values to 'UNKNOWN'")
-    #parceldata_df.loc[parceldata_df[column_output_class].str.startswith('nvt', na=True), 
+    # logger.info("Set classes that are still empty, not specific enough or that contain to little values to 'UNKNOWN'")
+    # parceldata_df.loc[parceldata_df[column_output_class].str.startswith('nvt', na=True),
     #                  column_output_class] = 'UNKNOWN'
 
     # 'MON_ANDERE_SUBSID_GEWASSEN': very low classification rate (< 1%), as it is a group with several very different classes in it
@@ -443,19 +525,19 @@ def prepare_input_fabaceae(
     # 'MON_RAAPACHTIGEN': 25% correct classifications: rest spread over many other classes
     # 'MON_STRUIK': 10%
     #    TODO: nakijken, wss opsplitsen of toevoegen aan MON_BOOMKWEEK???
-    #classes_badresults = ['MON_ANDERE_SUBSID_GEWASSEN', 'MON_AARDBEIEN', 'MON_BRAAK', 'MON_KLAVER', 
+    # classes_badresults = ['MON_ANDERE_SUBSID_GEWASSEN', 'MON_AARDBEIEN', 'MON_BRAAK', 'MON_KLAVER',
     #                      'MON_MENGSEL', 'MON_POEL', 'MON_RAAPACHTIGEN', 'MON_STRUIK']
-    #parceldata_df.loc[parceldata_df[column_output_class].isin(classes_badresults), 
+    # parceldata_df.loc[parceldata_df[column_output_class].isin(classes_badresults),
     #                  column_output_class] = 'UNKNOWN'
 
     # MON_BONEN en MON_WIKKEN have omongst each other a very large percentage of false
     # positives/negatives, so they seem very similar... lets create a class that combines both
-    #parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BONEN', 'MON_WIKKEN']), 
+    # parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BONEN', 'MON_WIKKEN']),
     #                  column_output_class] = 'MON_BONEN_WIKKEN'
 
     # MON_BOOM includes now also the growing new plants/trees, which is too differenct from grown
     # trees -> put growing new trees is seperate group
-    #parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['9602', '9603', '9604', '9560']), 
+    # parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['9602', '9603', '9604', '9560']),
     #                  column_output_class] = 'MON_BOOMKWEEK'
 
     # 'MON_FRUIT': has a good accuracy (91%), but also has as much false positives (115% -> mainly
@@ -465,34 +547,53 @@ def prepare_input_fabaceae(
     # MON_FRUIT and MON_BOOM are permanent anyway, so not mandatory that they are checked in
     # monitoring process.
     # Conclusion: put MON_BOOM and MON_FRUIT to IGNORE_DIFFICULT_PERMANENT_CLASS
-    #parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BOOM', 'MON_FRUIT']), 
+    # parceldata_df.loc[parceldata_df[column_output_class].isin(['MON_BOOM', 'MON_FRUIT']),
     #                  column_output_class] = 'IGNORE_DIFFICULT_PERMANENT_CLASS'
 
     # Set classes with very few elements to IGNORE_NOT_ENOUGH_SAMPLES!
-    for _, row in parceldata_df.groupby(column_output_class).size().reset_index(name='count').iterrows():
-        if row['count'] <= 50:
-            logger.info(f"Class <{row[column_output_class]}> only contains {row['count']} elements, so put them to IGNORE_NOT_ENOUGH_SAMPLES")
-            parceldata_df.loc[parceldata_df[column_output_class] == row[column_output_class], 
-                              column_output_class] = 'IGNORE_NOT_ENOUGH_SAMPLES'
+    for _, row in (
+        parceldata_df.groupby(column_output_class)
+        .size()
+        .reset_index(name="count")
+        .iterrows()
+    ):
+        if row["count"] <= 50:
+            logger.info(
+                f"Class <{row[column_output_class]}> only contains {row['count']} elements, so put them to IGNORE_NOT_ENOUGH_SAMPLES"
+            )
+            parceldata_df.loc[
+                parceldata_df[column_output_class] == row[column_output_class],
+                column_output_class,
+            ] = "IGNORE_NOT_ENOUGH_SAMPLES"
 
     # Drop the columns that aren't useful at all
     for column in parceldata_df.columns:
-        if(column not in [column_output_class, conf.columns['id'], 
-                          conf.columns['class_groundtruth'], 
-                          conf.columns['class_declared']]
-             and column not in conf.preprocess.getlist('extra_export_columns')
-             and column not in columns_BEFL_to_keep):
+        if (
+            column
+            not in [
+                column_output_class,
+                conf.columns["id"],
+                conf.columns["class_groundtruth"],
+                conf.columns["class_declared"],
+            ]
+            and column not in conf.preprocess.getlist("extra_export_columns")
+            and column not in columns_BEFL_to_keep
+        ):
             parceldata_df.drop(column, axis=1, inplace=True)
         elif column == column_BEFL_gesp_pm:
-            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[column_BEFL_gesp_pm].str.replace(',', ';')
+            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[
+                column_BEFL_gesp_pm
+            ].str.replace(",", ";")
 
     return parceldata_df
 
+
 def prepare_input_latecrop(
-            parceldata_df,
-            column_BEFL_cropcode: str,
-            column_output_class: str,
-            classes_refe_filepath: Path):
+    parceldata_df,
+    column_BEFL_cropcode: str,
+    column_output_class: str,
+    classes_refe_path: Path,
+):
     """
     This function creates a file that is compliant with the assumptions used by the rest of the
     classification functionality.
@@ -505,125 +606,174 @@ def prepare_input_latecrop(
     this format.
     """
     # Check if parameters are OK and init some extra params
-    #--------------------------------------------------------------------------
-    if not classes_refe_filepath.exists():
-        raise Exception(f"Input classes file doesn't exist: {classes_refe_filepath}")
+    # --------------------------------------------------------------------------
+    if not classes_refe_path.exists():
+        raise Exception(f"Input classes file doesn't exist: {classes_refe_path}")
 
     # Convert the crop to unicode, in case the input is int...
     if column_BEFL_cropcode in parceldata_df.columns:
-        parceldata_df[column_BEFL_cropcode] = parceldata_df[column_BEFL_cropcode].astype('unicode')
-        
+        parceldata_df[column_BEFL_cropcode] = parceldata_df[
+            column_BEFL_cropcode
+        ].astype("unicode")
+
     # Read and cleanup the mapping table from crop codes to classes
-    #--------------------------------------------------------------------------
-    logger.info(f"Read classes conversion table from {classes_refe_filepath}")
-    classes_df = pdh.read_file(classes_refe_filepath)
+    # --------------------------------------------------------------------------
+    logger.info(f"Read classes conversion table from {classes_refe_path}")
+    classes_df = pdh.read_file(classes_refe_path)
     logger.info(f"Read classes conversion table ready, info(): {classes_df.info()}")
 
     # Because the file was read as ansi, and gewas is int, so the data needs to be converted to
     # unicode to be able to do comparisons with the other data
-    classes_df[column_BEFL_cropcode] = classes_df['CROPCODE'].astype('unicode')
+    classes_df[column_BEFL_cropcode] = classes_df["CROPCODE"].astype("unicode")
 
     # Map column with the classname to orig classname
-    column_output_class_orig = conf.columns['class'] + '_orig'
-    classes_df[column_output_class_orig] = classes_df[conf.columns['class_refe']]
+    column_output_class_orig = conf.columns["class"] + "_orig"
+    classes_df[column_output_class_orig] = classes_df[conf.columns["class_refe"]]
 
     # Remove unneeded columns
     for column in classes_df.columns:
-        if(column not in [column_output_class_orig, 
-                          column_BEFL_cropcode,
-                          conf.columns['class_declared'],
-                          conf.columns['class']]
-           and column not in columns_BEFL_to_keep):
+        if (
+            column
+            not in [
+                column_output_class_orig,
+                column_BEFL_cropcode,
+                conf.columns["class_declared"],
+                conf.columns["class"],
+            ]
+            and column not in columns_BEFL_to_keep
+        ):
             classes_df.drop(column, axis=1, inplace=True)
 
     # Set the index
     classes_df.set_index(column_BEFL_cropcode, inplace=True, verify_integrity=True)
 
     # Get only the columns in the classes_df that don't exist yet in parceldata_df
-    cols_to_join = classes_df.columns.difference(parceldata_df.columns) 
+    cols_to_join = classes_df.columns.difference(parceldata_df.columns)
 
     # Join/merge the classname
-    logger.info('Add the classes to the parceldata')
+    logger.info("Add the classes to the parceldata")
     parceldata_df = parceldata_df.merge(
-            classes_df[cols_to_join], how='left', left_on=column_BEFL_cropcode,
-            right_index=True, validate='many_to_one')
+        classes_df[cols_to_join],
+        how="left",
+        left_on=column_BEFL_cropcode,
+        right_index=True,
+        validate="many_to_one",
+    )
 
     # Copy orig classname to classification classname
     parceldata_df.insert(
-            loc=0, column=column_output_class, value=parceldata_df[column_output_class_orig])
-    
+        loc=0, column=column_output_class, value=parceldata_df[column_output_class_orig]
+    )
+
     # For rows with no class, set to UNKNOWN
-    parceldata_df.fillna(value={column_output_class: 'UNKNOWN'}, inplace=True) 
+    parceldata_df.fillna(value={column_output_class: "UNKNOWN"}, inplace=True)
 
     # If a column with extra info exists, use it as well to fine-tune the classification classes.
     if column_BEFL_gesp_pm in parceldata_df.columns:
         # Serres, tijdelijke overkappingen en loodsen
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['SER', 'SGM']), 
-                          column_output_class] = 'MON_OVERK_LOO'
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['PLA', 'PLO', 'NPO']), 
-                          column_output_class] = 'MON_OVERK_LOO'
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'LOO', 
-                          column_output_class] = 'MON_OVERK_LOO'           # Een loods is hetzelfde als een stal...
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CON', 
-                          column_output_class] = 'MON_CONTAINERS'          # Containers, niet op volle grond...
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(["SER", "SGM"]), column_output_class
+        ] = "MON_OVERK_LOO"
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(["PLA", "PLO", "NPO"]),
+            column_output_class,
+        ] = "MON_OVERK_LOO"
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm] == "LOO", column_output_class
+        ] = "MON_OVERK_LOO"  # Een loods is hetzelfde als een stal...
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm] == "CON", column_output_class
+        ] = "MON_CONTAINERS"  # Containers, niet op volle grond...
         # TODO: CIV, containers in volle grond, lijkt niet zo specifiek te zijn...
-        #parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
+        # parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
     else:
-        logger.warning(f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!")
+        logger.warning(
+            f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!"
+        )
 
     # Set classes with very few elements to IGNORE_NOT_ENOUGH_SAMPLES!
-    for _, row in parceldata_df.groupby(column_output_class).size().reset_index(name='count').iterrows():
-        if row['count'] <= 50:
-            logger.info(f"Class <{row[column_output_class]}> only contains {row['count']} elements, so put them to IGNORE_NOT_ENOUGH_SAMPLES")
-            parceldata_df.loc[parceldata_df[column_output_class] == row[column_output_class], 
-                              column_output_class] = 'IGNORE_NOT_ENOUGH_SAMPLES'
+    for _, row in (
+        parceldata_df.groupby(column_output_class)
+        .size()
+        .reset_index(name="count")
+        .iterrows()
+    ):
+        if row["count"] <= 50:
+            logger.info(
+                f"Class <{row[column_output_class]}> only contains {row['count']} elements, so put them to IGNORE_NOT_ENOUGH_SAMPLES"
+            )
+            parceldata_df.loc[
+                parceldata_df[column_output_class] == row[column_output_class],
+                column_output_class,
+            ] = "IGNORE_NOT_ENOUGH_SAMPLES"
 
     # Drop the columns that aren't useful at all
     for column in parceldata_df.columns:
-        if(column not in [column_output_class, conf.columns['id'], 
-                          conf.columns['class_groundtruth'], 
-                          conf.columns['class_declared']]
-             and column not in conf.preprocess.getlist('extra_export_columns')
-             and column not in columns_BEFL_to_keep):
+        if (
+            column
+            not in [
+                column_output_class,
+                conf.columns["id"],
+                conf.columns["class_groundtruth"],
+                conf.columns["class_declared"],
+            ]
+            and column not in conf.preprocess.getlist("extra_export_columns")
+            and column not in columns_BEFL_to_keep
+        ):
             parceldata_df.drop(column, axis=1, inplace=True)
         elif column == column_BEFL_gesp_pm:
-            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[column_BEFL_gesp_pm].str.replace(',', ';')
+            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[
+                column_BEFL_gesp_pm
+            ].str.replace(",", ";")
 
     return parceldata_df
 
+
 def prepare_input_cropgroup_early(
-            parceldata_df,
-            column_BEFL_cropcode: str,
-            column_output_class: str,
-            classes_refe_filepath: Path):
+    parceldata_df,
+    column_BEFL_cropcode: str,
+    column_output_class: str,
+    classes_refe_path: Path,
+):
     """
-    Prepare a dataframe based on the BEFL input file so it onclused a classname 
+    Prepare a dataframe based on the BEFL input file so it onclused a classname
     column ready to classify the cropgroups for early crops.
     """
     # First run the standard landcover prepare
     parceldata_df = prepare_input_cropgroup(
-            parceldata_df, column_BEFL_cropcode, column_output_class, classes_refe_filepath)
+        parceldata_df, column_BEFL_cropcode, column_output_class, classes_refe_path
+    )
 
     # Set late crops to ignore
-    parceldata_df.loc[parceldata_df[column_BEFL_earlylate] != 'MON_TEELTEN_VROEGE', 
-                      column_output_class] = 'IGNORE_LATE_CROP'
-    
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_earlylate] != "MON_TEELTEN_VROEGE",
+        column_output_class,
+    ] = "IGNORE_LATE_CROP"
+
     # Set new grass to ignore
-    if column_BEFL_status_perm_grass in parceldata_df.columns: 
-        parceldata_df.loc[(parceldata_df[column_BEFL_cropcode] == '60')
-                            & ((parceldata_df[column_BEFL_status_perm_grass] == 'BG1')
-                                | (parceldata_df[column_BEFL_status_perm_grass].isnull())), 
-                          column_output_class] = 'IGNORE_NEW_GRASSLAND'
+    if column_BEFL_status_perm_grass in parceldata_df.columns:
+        parceldata_df.loc[
+            (parceldata_df[column_BEFL_cropcode] == "60")
+            & (
+                (parceldata_df[column_BEFL_status_perm_grass] == "BG1")
+                | (parceldata_df[column_BEFL_status_perm_grass].isnull())
+            ),
+            column_output_class,
+        ] = "IGNORE_NEW_GRASSLAND"
     else:
-        logger.warning(f"Source file doesn't contain column {column_BEFL_status_perm_grass}!")
+        logger.warning(
+            f"Source file doesn't contain column {column_BEFL_status_perm_grass}!"
+        )
 
     return parceldata_df
 
+
 def prepare_input_landcover(
-            parceldata_df,
-            column_BEFL_cropcode: str,
-            column_output_class: str,
-            classes_refe_filepath: Path):
+    parceldata_df,
+    column_BEFL_cropcode: str,
+    column_output_class: str,
+    classes_refe_path: Path,
+):
     """
     This function creates a file that is compliant with the assumptions used by the rest of the
     classification functionality.
@@ -636,114 +786,155 @@ def prepare_input_landcover(
     this format.
     """
     # Check if parameters are OK and init some extra params
-    #--------------------------------------------------------------------------
-    if not classes_refe_filepath.exists():
-        raise Exception(f"Input classes file doesn't exist: {classes_refe_filepath}") 
+    # --------------------------------------------------------------------------
+    if not classes_refe_path.exists():
+        raise Exception(f"Input classes file doesn't exist: {classes_refe_path}")
 
     # Convert the crop to unicode, in case the input is int...
     if column_BEFL_cropcode in parceldata_df.columns:
-        parceldata_df[column_BEFL_cropcode] = parceldata_df[column_BEFL_cropcode].astype('unicode')
+        parceldata_df[column_BEFL_cropcode] = parceldata_df[
+            column_BEFL_cropcode
+        ].astype("unicode")
 
     # Read and cleanup the mapping table from crop codes to classes
-    #--------------------------------------------------------------------------
-    logger.info(f"Read classes conversion table from {classes_refe_filepath}")
-    classes_df = pdh.read_file(classes_refe_filepath)
+    # --------------------------------------------------------------------------
+    logger.info(f"Read classes conversion table from {classes_refe_path}")
+    classes_df = pdh.read_file(classes_refe_path)
     logger.info(f"Read classes conversion table ready, info(): {classes_df.info()}")
 
     # Because the file was read as ansi, and gewas is int, so the data needs to be converted to
     # unicode to be able to do comparisons with the other data
-    classes_df[column_BEFL_cropcode] = classes_df['CROPCODE'].astype('unicode')
+    classes_df[column_BEFL_cropcode] = classes_df["CROPCODE"].astype("unicode")
 
     # Map column MON_group to orig classname
-    column_output_class_orig = column_output_class + '_orig'
-    classes_df[column_output_class_orig] = classes_df[conf.columns['class_refe']]
+    column_output_class_orig = column_output_class + "_orig"
+    classes_df[column_output_class_orig] = classes_df[conf.columns["class_refe"]]
 
     # Remove unneeded columns
     for column in classes_df.columns:
-        if(column not in [column_output_class_orig, column_BEFL_cropcode]
-           and column not in columns_BEFL_to_keep):
+        if (
+            column not in [column_output_class_orig, column_BEFL_cropcode]
+            and column not in columns_BEFL_to_keep
+        ):
             classes_df.drop(column, axis=1, inplace=True)
 
     # Set the index
     classes_df.set_index(column_BEFL_cropcode, inplace=True, verify_integrity=True)
 
     # Get only the columns in the classes_df that don't exist yet in parceldata_df
-    cols_to_join = classes_df.columns.difference(parceldata_df.columns) 
+    cols_to_join = classes_df.columns.difference(parceldata_df.columns)
 
     # Join/merge the classname
-    logger.info('Add the classes to the parceldata')
+    logger.info("Add the classes to the parceldata")
     parceldata_df = parceldata_df.merge(
-            classes_df[cols_to_join], how='left', left_on=column_BEFL_cropcode,
-            right_index=True, validate='many_to_one')
+        classes_df[cols_to_join],
+        how="left",
+        left_on=column_BEFL_cropcode,
+        right_index=True,
+        validate="many_to_one",
+    )
 
     # Copy orig classname to classification classname
-    parceldata_df.insert(loc=0, column=column_output_class, 
-                         value=parceldata_df[column_output_class_orig])
+    parceldata_df.insert(
+        loc=0, column=column_output_class, value=parceldata_df[column_output_class_orig]
+    )
 
     # If a column with extra info exists, use it as well to fine-tune the classification classes.
     if column_BEFL_gesp_pm in parceldata_df.columns:
         # Serres, tijdelijke overkappingen en loodsen
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['SER', 'PLA', 'PLO', 'SGM', 'NPO', 'LOO']), 
-                          column_output_class] = 'MON_LC_OVERK_LOO'
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(
+                ["SER", "PLA", "PLO", "SGM", "NPO", "LOO"]
+            ),
+            column_output_class,
+        ] = "MON_LC_OVERK_LOO"
         # Containers
-        parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm].isin(['CON']), 
-                          column_output_class] = 'MON_LC_IGNORE_DIFFICULT_PERMANENT_CLASS_NS'
+        parceldata_df.loc[
+            parceldata_df[column_BEFL_gesp_pm].isin(["CON"]), column_output_class
+        ] = "MON_LC_IGNORE_DIFFICULT_PERMANENT_CLASS_NS"
         # TODO: CIV, containers in volle grond, lijkt niet zo specifiek te zijn...
-        #parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
+        # parceldata_df.loc[parceldata_df[column_BEFL_gesp_pm] == 'CIV', class_columnname] = 'MON_CONTAINERS'   # Containers, niet op volle grond...
     else:
-        logger.warning(f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!")
+        logger.warning(
+            f"The column {column_BEFL_gesp_pm} doesn't exist, so this part of the code was skipped!"
+        )
 
     # Some extra cleanup: classes starting with 'nvt' or empty ones
-    logger.info("Set classes that are still empty, not specific enough or that contain to little values to 'UNKNOWN'")
-    parceldata_df.loc[parceldata_df[column_output_class].str.startswith('nvt', na=True), 
-                      column_output_class] = 'MON_LC_UNKNOWN'
+    logger.info(
+        "Set classes that are still empty, not specific enough or that contain to little values to 'UNKNOWN'"
+    )
+    parceldata_df.loc[
+        parceldata_df[column_output_class].str.startswith("nvt", na=True),
+        column_output_class,
+    ] = "MON_LC_UNKNOWN"
 
     # Drop the columns that aren't useful at all
     for column in parceldata_df.columns:
-        if(column not in [column_output_class, conf.columns['id'], 
-                          conf.columns['class_groundtruth'], 
-                          conf.columns['class_declared']]
-             and column not in conf.preprocess.getlist('extra_export_columns')
-             and column not in columns_BEFL_to_keep):
+        if (
+            column
+            not in [
+                column_output_class,
+                conf.columns["id"],
+                conf.columns["class_groundtruth"],
+                conf.columns["class_declared"],
+            ]
+            and column not in conf.preprocess.getlist("extra_export_columns")
+            and column not in columns_BEFL_to_keep
+        ):
             parceldata_df.drop(column, axis=1, inplace=True)
         elif column == column_BEFL_gesp_pm:
-            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[column_BEFL_gesp_pm].str.replace(',', ';')
+            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[
+                column_BEFL_gesp_pm
+            ].str.replace(",", ";")
 
     return parceldata_df
 
+
 def prepare_input_landcover_early(
-            parceldata_df,
-            column_BEFL_cropcode: str,
-            column_output_class: str,
-            classes_refe_filepath: Path):
+    parceldata_df,
+    column_BEFL_cropcode: str,
+    column_output_class: str,
+    classes_refe_path: Path,
+):
     """
-    Prepare a dataframe based on the BEFL input file so it onclused a classname 
+    Prepare a dataframe based on the BEFL input file so it onclused a classname
     column ready to classify the landcover for early crops.
     """
     # First run the standard landcover prepare
     parceldata_df = prepare_input_landcover(
-            parceldata_df, column_BEFL_cropcode, column_output_class, classes_refe_filepath)
+        parceldata_df, column_BEFL_cropcode, column_output_class, classes_refe_path
+    )
 
     # Set crops not in early crops to ignore
-    parceldata_df.loc[parceldata_df[column_BEFL_earlylate] != 'MON_TEELTEN_VROEGE', 
-                      column_output_class] = 'IGNORE_LATE_CROP'
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_earlylate] != "MON_TEELTEN_VROEGE",
+        column_output_class,
+    ] = "IGNORE_LATE_CROP"
 
     # Set new grass to ignore
     if column_BEFL_status_perm_grass in parceldata_df.columns:
-        parceldata_df.loc[(parceldata_df[column_BEFL_cropcode] == '60')
-                            & ((parceldata_df[column_BEFL_status_perm_grass] == 'BG1')
-                                | (parceldata_df[column_BEFL_status_perm_grass].isnull())), 
-                          column_output_class] = 'IGNORE_NEW_GRASSLAND'
+        parceldata_df.loc[
+            (parceldata_df[column_BEFL_cropcode] == "60")
+            & (
+                (parceldata_df[column_BEFL_status_perm_grass] == "BG1")
+                | (parceldata_df[column_BEFL_status_perm_grass].isnull())
+            ),
+            column_output_class,
+        ] = "IGNORE_NEW_GRASSLAND"
     else:
-        logger.warning(f"Source file doesn't contain column {column_BEFL_status_perm_grass}, so new grassland cannot be ignored!")
-                      
+        logger.warning(
+            f"Source file doesn't contain column {column_BEFL_status_perm_grass}, so new grassland cannot be ignored!"
+        )
+
     return parceldata_df
 
+
 def prepare_input_most_popular_crop(
-            parceldata_df,
-            column_BEFL_cropcode: str,
-            column_output_class: str,
-            classes_refe_filepath: Path):
+    parceldata_df,
+    column_BEFL_cropcode: str,
+    column_output_class: str,
+    classes_refe_path: Path,
+):
     """
     This function creates a file that is compliant with the assumptions used by the rest of the
     classification functionality.
@@ -759,33 +950,57 @@ def prepare_input_most_popular_crop(
     # Add columns for the class to use...
     parceldata_df.insert(0, column_output_class, None)
 
-    parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['60', '700', '3432']), column_output_class] = 'Grassland'  # Grassland
-    parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['201', '202']), column_output_class] = 'Maize'            # Maize
-    parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['901', '904']), column_output_class] = 'Potatoes'         # Potatoes
-    parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['311', '36']), column_output_class] = 'WinterWheat'       # Winter wheat of spelt
-    parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['91']), column_output_class] = 'SugarBeat'               # Sugar beat
-    parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['321']), column_output_class] = 'WinterBarley'           # Winter barley
-    parceldata_df.loc[parceldata_df[column_BEFL_cropcode].isin(['71']), column_output_class] = 'FodderBeat'              # Fodder beat
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["60", "700", "3432"]),
+        column_output_class,
+    ] = "Grassland"  # Grassland
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["201", "202"]), column_output_class
+    ] = "Maize"  # Maize
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["901", "904"]), column_output_class
+    ] = "Potatoes"  # Potatoes
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["311", "36"]), column_output_class
+    ] = "WinterWheat"  # Winter wheat of spelt
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["91"]), column_output_class
+    ] = "SugarBeat"  # Sugar beat
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["321"]), column_output_class
+    ] = "WinterBarley"  # Winter barley
+    parceldata_df.loc[
+        parceldata_df[column_BEFL_cropcode].isin(["71"]), column_output_class
+    ] = "FodderBeat"  # Fodder beat
 
     # Some extra cleanup: classes starting with 'nvt' or empty ones
-    logger.info("Set classes that are empty to 'IGNORE_UNIMPORTANT_CLASS' so they are ignored further on...")
-    parceldata_df.loc[parceldata_df[column_output_class].isnull(), column_output_class] = 'IGNORE_UNIMPORTANT_CLASS'
+    logger.info(
+        "Set classes that are empty to 'IGNORE_UNIMPORTANT_CLASS' so they are ignored further on..."
+    )
+    parceldata_df.loc[
+        parceldata_df[column_output_class].isnull(), column_output_class
+    ] = "IGNORE_UNIMPORTANT_CLASS"
 
     # Drop the columns that aren't useful at all
     for column in parceldata_df.columns:
-        if(column not in [conf.columns['id'], column_output_class]
-             and column not in conf.preprocess.getlist('extra_export_columns')
-             and column not in columns_BEFL_to_keep
-             and not column == column_BEFL_cropcode):
+        if (
+            column not in [conf.columns["id"], column_output_class]
+            and column not in conf.preprocess.getlist("extra_export_columns")
+            and column not in columns_BEFL_to_keep
+            and not column == column_BEFL_cropcode
+        ):
             parceldata_df.drop(column, axis=1, inplace=True)
         elif column == column_BEFL_gesp_pm:
-            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[column_BEFL_gesp_pm].str.replace(',', ';')
+            parceldata_df[column_BEFL_gesp_pm] = parceldata_df[
+                column_BEFL_gesp_pm
+            ].str.replace(",", ";")
 
     # Return result
     return parceldata_df
 
+
 # If the script is run directly...
 if __name__ == "__main__":
 
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/preprocess/timeseries.py b/cropclassification/preprocess/timeseries.py
index 75ea1e29..cb804143 100644
--- a/cropclassification/preprocess/timeseries.py
+++ b/cropclassification/preprocess/timeseries.py
@@ -14,33 +14,35 @@
 import cropclassification.helpers.pandas_helper as pdh
 import cropclassification.preprocess.timeseries_util as ts_util
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
+
 
 def calc_timeseries_data(
-        input_parcel_filepath: Path,
-        input_country_code: str,
-        start_date_str: str,
-        end_date_str: str,
-        sensordata_to_get: List[str],
-        base_filename: str,
-        dest_data_dir: Path):
+    input_parcel_path: Path,
+    input_country_code: str,
+    start_date_str: str,
+    end_date_str: str,
+    sensordata_to_get: List[str],
+    base_filename: str,
+    dest_data_dir: Path,
+):
     """
     Calculate timeseries data for the input parcels
 
     Args:
-        input_parcel_filepath (str): [description]
+        input_parcel_path (str): [description]
         input_country_code (str): [description]
         start_date_str (str): [description]
         end_date_str (str): [description]
-        sensordata_to_get (List[str]): an array with data you want to be calculated: 
+        sensordata_to_get (List[str]): an array with data you want to be calculated:
                 check out the constants starting with DATA_TO_GET... for the options.
         base_filename (str): [description]
         dest_data_dir (str): [description]
@@ -52,53 +54,64 @@ def calc_timeseries_data(
         os.makedirs(dest_data_dir)
 
     # As we want a weekly calculation, get nearest monday for start and stop day
-    start_date = ts_util.get_monday(start_date_str) # output: vb 2018_2_1 - maandag van week 2 van 2018
-    end_date = ts_util.get_monday(end_date_str) 
-    start_date_monday = start_date.strftime('%Y-%m-%d') # terug omzetten naar Y/M/D
-    end_date_monday = end_date.strftime('%Y-%m-%d')
-
-    logger.info(f"Start date {start_date_str} converted to monday before: {start_date}, end date {end_date_str} as well: {end_date}")
-    timeseries_calc_type = conf.timeseries['timeseries_calc_type']
-    if timeseries_calc_type == 'gee':
+    start_date = ts_util.get_monday(
+        start_date_str
+    )  # output: vb 2018_2_1 - maandag van week 2 van 2018
+    end_date = ts_util.get_monday(end_date_str)
+    start_date_monday = start_date.strftime("%Y-%m-%d")  # terug omzetten naar Y/M/D
+    end_date_monday = end_date.strftime("%Y-%m-%d")
+
+    logger.info(
+        f"Start date {start_date_str} converted to monday before: {start_date}, end "
+        f"date {end_date_str} as well: {end_date}"
+    )
+    timeseries_calc_type = conf.timeseries["timeseries_calc_type"]
+    if timeseries_calc_type == "gee":
         # Start!
         import cropclassification.preprocess.timeseries_calc_gee as ts_calc_gee
+
         return ts_calc_gee.calc_timeseries_data(
-                input_parcel_filepath=input_parcel_filepath,
-                input_country_code=input_country_code,
-                start_date_str=start_date_monday,
-                end_date_str=end_date_monday,
-                sensordata_to_get=sensordata_to_get,
-                base_filename=base_filename,
-                dest_data_dir=dest_data_dir)
-    elif timeseries_calc_type == 'onda':
+            input_parcel_path=input_parcel_path,
+            input_country_code=input_country_code,
+            start_date_str=start_date_monday,
+            end_date_str=end_date_monday,
+            sensordata_to_get=sensordata_to_get,
+            base_filename=base_filename,
+            dest_data_dir=dest_data_dir,
+        )
+    elif timeseries_calc_type == "onda":
         # Start!
         # TODO: start calculation of per image data on DIAS
-        #import cropclassification.preprocess.timeseries_calc_dias_onda_per_image as ts_calc
-        timeseries_per_image_dir = conf.dirs.getpath('timeseries_per_image_dir')
+        # import cropclassification.preprocess.timeseries_calc_dias_onda_per_image as
+        # ts_calc
+        timeseries_per_image_dir = conf.dirs.getpath("timeseries_per_image_dir")
 
         # Now all image data is available per image, calculate periodic data
         return ts_util.calculate_periodic_data(
-                input_parcel_filepath=input_parcel_filepath,
-                input_base_dir=timeseries_per_image_dir,
-                start_date_str=start_date_str,
-                end_date_str=end_date_str,
-                sensordata_to_get=sensordata_to_get,  
-                dest_data_dir=dest_data_dir)
+            input_parcel_path=input_parcel_path,
+            input_base_dir=timeseries_per_image_dir,
+            start_date_str=start_date_str,
+            end_date_str=end_date_str,
+            sensordata_to_get=sensordata_to_get,
+            dest_data_dir=dest_data_dir,
+        )
     else:
         message = f"Unsupported timeseries calculation type: {timeseries_calc_type}"
         logger.error(message)
         raise Exception(message)
 
+
 def collect_and_prepare_timeseries_data(
-        input_parcel_filepath: Path,
-        timeseries_dir: Path,
-        base_filename: str,
-        output_filepath: Path,
-        start_date_str: str,
-        end_date_str: str,
-        sensordata_to_use: List[str],
-        parceldata_aggregations_to_use: List[str],
-        force: bool = False):
+    input_parcel_path: Path,
+    timeseries_dir: Path,
+    base_filename: str,
+    output_path: Path,
+    start_date_str: str,
+    end_date_str: str,
+    sensordata_to_use: List[str],
+    parceldata_aggregations_to_use: List[str],
+    force: bool = False,
+):
     """
     Collect all timeseries data to use for the classification and prepare it by applying
     scaling,... as needed.
@@ -106,131 +119,168 @@ def collect_and_prepare_timeseries_data(
 
     # Some constants to choose which type of data to use in the marker.
     # Remark: the string needs to be the same as the end of the name of the columns in the csv files!
-    # TODO: I'm not really happy with both a list in the ini file + here... not sure what the 
+    # TODO: I'm not really happy with both a list in the ini file + here... not sure what the
     #       cleanest solution is though...
-    PARCELDATA_AGGRAGATION_MEAN = conf.general['PARCELDATA_AGGRAGATION_MEAN']      # Mean value of the pixels values in a parcel.
-    PARCELDATA_AGGRAGATION_STDDEV = conf.general['PARCELDATA_AGGRAGATION_STDDEV']  # std dev of the values of the pixels in a parcel
+    PARCELDATA_AGGRAGATION_MEAN = conf.general[
+        "PARCELDATA_AGGRAGATION_MEAN"
+    ]  # Mean value of the pixels values in a parcel.
+    PARCELDATA_AGGRAGATION_STDDEV = conf.general[
+        "PARCELDATA_AGGRAGATION_STDDEV"
+    ]  # std dev of the values of the pixels in a parcel
 
     # Constants for types of sensor data
-    SENSORDATA_S1 = conf.general['SENSORDATA_S1']                     # Sentinel 1 data
-    SENSORDATA_S1DB = conf.general['SENSORDATA_S1DB']                 # Sentinel 1 data, in dB
-    SENSORDATA_S1_ASCDESC = conf.general['SENSORDATA_S1_ASCDESC']     # Sentinel 1 data, divided in Ascending and Descending passes
-    SENSORDATA_S1DB_ASCDESC = conf.general['SENSORDATA_S1DB_ASCDESC'] # Sentinel 1 data, in dB, divided in Ascending and Descending passes
-    SENSORDATA_S2 = conf.general['SENSORDATA_S2']                     # Sentinel 2 data
-    SENSORDATA_S2gt95 = conf.general['SENSORDATA_S2gt95']             # Sentinel 2 data (B2,B3,B4,B8) IF available for 95% or area
-    SENSORDATA_S1_COHERENCE = conf.general['SENSORDATA_S1_COHERENCE']
+    SENSORDATA_S1 = conf.general["SENSORDATA_S1"]  # Sentinel 1 data
+    SENSORDATA_S1DB = conf.general["SENSORDATA_S1DB"]  # Sentinel 1 data, in dB
+    SENSORDATA_S1_ASCDESC = conf.general[
+        "SENSORDATA_S1_ASCDESC"
+    ]  # Sentinel 1 data, divided in Ascending and Descending passes
+    SENSORDATA_S1DB_ASCDESC = conf.general[
+        "SENSORDATA_S1DB_ASCDESC"
+    ]  # Sentinel 1 data, in dB, divided in Ascending and Descending passes
+    SENSORDATA_S2 = conf.general["SENSORDATA_S2"]  # Sentinel 2 data
+    SENSORDATA_S2gt95 = conf.general[
+        "SENSORDATA_S2gt95"
+    ]  # Sentinel 2 data (B2,B3,B4,B8) IF available for 95% or area
+    SENSORDATA_S1_COHERENCE = conf.general["SENSORDATA_S1_COHERENCE"]
 
     # If force == False Check and the output file exists already, stop.
-    if(force is False and output_filepath.exists() is True):
-        logger.warning(f"Output file already exists and force == False, so stop: {output_filepath}")
+    if force is False and output_path.exists() is True:
+        logger.warning(
+            f"Output file already exists and force == False, so stop: {output_path}"
+        )
         return
 
     # Init the result with the id's of the parcels we want to treat
-    result_df = pdh.read_file(input_parcel_filepath, columns=[conf.columns['id']])
-    if result_df.index.name != conf.columns['id']: 
-        result_df.set_index(conf.columns['id'], inplace=True)
+    result_df = pdh.read_file(input_parcel_path, columns=[conf.columns["id"]])
+    if result_df.index.name != conf.columns["id"]:
+        result_df.set_index(conf.columns["id"], inplace=True)
     nb_input_parcels = len(result_df.index)
-    logger.info(f"Parceldata aggregations that need to be used: {parceldata_aggregations_to_use}")
+    logger.info(
+        f"Parceldata aggregations that need to be used: {parceldata_aggregations_to_use}"
+    )
     logger.setLevel(logging.DEBUG)
 
     # Loop over all input timeseries data to find the data we really need
-    data_ext = conf.general['data_ext']
-    filepath_start = timeseries_dir / f"{base_filename}_{start_date_str}{data_ext}"
-    filepath_end = timeseries_dir / f"{base_filename}_{end_date_str}{data_ext}"
-    logger.debug(f'filepath_start_date: {filepath_start}')
-    logger.debug(f'filepath_end_date: {filepath_end}')
+    data_ext = conf.general["data_ext"]
+    path_start = timeseries_dir / f"{base_filename}_{start_date_str}{data_ext}"
+    path_end = timeseries_dir / f"{base_filename}_{end_date_str}{data_ext}"
+    logger.debug(f"path_start_date: {path_start}")
+    logger.debug(f"path_end_date: {path_end}")
 
     ts_data_files = timeseries_dir.glob(f"{base_filename}_*{data_ext}")
-    for curr_filepath in sorted(ts_data_files):
+    for curr_path in sorted(ts_data_files):
 
         # Only process data that is of the right sensor types
-        sensor_type = curr_filepath.stem.split('_')[-1]
+        sensor_type = curr_path.stem.split("_")[-1]
         if sensor_type not in sensordata_to_use:
-            logger.debug(f"SKIP: file is not in sensor types asked ({sensordata_to_use}): {curr_filepath}")
+            logger.debug(
+                f"SKIP: file is not in sensor types asked ({sensordata_to_use}): {curr_path}"
+            )
             continue
         # The only data we want to process is the data in the range of dates
-        if((str(curr_filepath) < str(filepath_start)) or (str(curr_filepath) >= str(filepath_end))):
-            logger.debug(f"SKIP: File is not in date range asked: {curr_filepath}")
+        if (str(curr_path) < str(path_start)) or (str(curr_path) >= str(path_end)):
+            logger.debug(f"SKIP: File is not in date range asked: {curr_path}")
             continue
-        # An empty file signifies that there wasn't any valable data for that period/sensor/...
-        if os.path.getsize(curr_filepath) == 0:
-            logger.info(f"SKIP: file is empty: {curr_filepath}")
+        # An empty file signifies that there wasn't any valable data for that
+        # period/sensor/...
+        if os.path.getsize(curr_path) == 0:
+            logger.info(f"SKIP: file is empty: {curr_path}")
             continue
 
         # Read data, and check if there is enough data in it
-        data_read_df = pdh.read_file(curr_filepath)
+        data_read_df = pdh.read_file(curr_path)
         nb_data_read = len(data_read_df.index)
-        data_available_pct = nb_data_read*100/nb_input_parcels
-        min_parcels_with_data_pct = conf.timeseries.getfloat('min_parcels_with_data_pct')
+        data_available_pct = nb_data_read * 100 / nb_input_parcels
+        min_parcels_with_data_pct = conf.timeseries.getfloat(
+            "min_parcels_with_data_pct"
+        )
         if data_available_pct < min_parcels_with_data_pct:
-            logger.info(f"SKIP: only data for {data_available_pct:.2f}% of parcels, should be > {min_parcels_with_data_pct}%: {curr_filepath}")
+            logger.info(
+                f"SKIP: only data for {data_available_pct:.2f}% of parcels, should be "
+                f"> {min_parcels_with_data_pct}%: {curr_path}"
+            )
             continue
 
         # Start processing the file
-        logger.info(f'Process file: {curr_filepath}') 
-        if data_read_df.index.name != conf.columns['id']: 
-            data_read_df.set_index(conf.columns['id'], inplace=True)
+        logger.info(f"Process file: {curr_path}")
+        if data_read_df.index.name != conf.columns["id"]:
+            data_read_df.set_index(conf.columns["id"], inplace=True)
 
-        # Loop over columns to check if there are columns that need to be dropped.       
+        # Loop over columns to check if there are columns that need to be dropped.
         for column in data_read_df.columns:
 
             # If it is the id column, continue
-            if column == conf.columns['id']:
+            if column == conf.columns["id"]:
                 continue
 
             # Check if the column is "asked"
             column_ok = False
             for parceldata_aggregation in parceldata_aggregations_to_use:
-                if column.endswith('_' + parceldata_aggregation):
+                if column.endswith("_" + parceldata_aggregation):
                     column_ok = True
             if column_ok is False:
                 # Drop column if it doesn't end with something in parcel_data_aggregations_to_use
-                logger.debug(f"Drop column as it's column aggregation isn't to be used: {column}")
+                logger.debug(
+                    f"Drop column as it's column aggregation isn't to be used: {column}"
+                )
                 data_read_df.drop(column, axis=1, inplace=True)
                 continue
 
             # Check if the column contains data for enough parcels
-            valid_input_data_pct = (1-(data_read_df[column].isnull().sum()/nb_input_parcels))*100
+            valid_input_data_pct = (
+                1 - (data_read_df[column].isnull().sum() / nb_input_parcels)
+            ) * 100
             if valid_input_data_pct < min_parcels_with_data_pct:
                 # If the number of nan values for the column > x %, drop column
-                logger.warn(f"Drop column as it contains only {valid_input_data_pct:.2f}% real data compared to input (= not nan) which is < {min_parcels_with_data_pct}%!: {column}")
+                logger.warn(
+                    f"Drop column as it contains only {valid_input_data_pct:.2f}% real "
+                    f"data compared to input (= not nan) which is "
+                    f"< {min_parcels_with_data_pct}%!: {column}"
+                )
                 data_read_df.drop(column, axis=1, inplace=True)
 
         # If S2, rescale data
         if sensor_type.startswith(SENSORDATA_S2):
             for column in data_read_df.columns:
-                logger.info(f"Column contains S2 data, so scale it by dividing by 10.000: {column}")
-                data_read_df[column] = data_read_df[column]/10000
+                logger.info(
+                    f"Column contains S2 data, so scale it by dividing by 10.000: {column}"
+                )
+                data_read_df[column] = data_read_df[column] / 10000
 
         # If S1 coherence, rescale data
         if sensor_type == SENSORDATA_S1_COHERENCE:
             for column in data_read_df.columns:
-                logger.info(f"Column contains S1 Coherence data, so scale it by dividing by 300: {column}")
-                data_read_df[column] = data_read_df[column]/300
+                logger.info(
+                    f"Column contains S1 Coherence data, so scale it by dividing by 300: {column}"
+                )
+                data_read_df[column] = data_read_df[column] / 300
 
         # Join the data to the result...
-        result_df = result_df.join(data_read_df, how='left')
+        result_df = result_df.join(data_read_df, how="left")
 
     # Remove rows with many null values from result
     max_number_null = int(0.6 * len(result_df.columns))
     parcel_many_null_df = result_df[result_df.isnull().sum(axis=1) > max_number_null]
     if len(parcel_many_null_df.index) > 0:
         # Write the rows with empty data to a file
-        parcel_many_null_filepath = Path(f'{str(output_filepath)}_rows_many_null.sqlite')
-        logger.warn(f"Write {len(parcel_many_null_df.index)} rows with > {max_number_null} of {len(result_df.columns)} columns==null to {parcel_many_null_filepath}")
-        pdh.to_file(parcel_many_null_df, parcel_many_null_filepath)
+        parcel_many_null_path = Path(f"{str(output_path)}_rows_many_null.sqlite")
+        logger.warn(
+            f"Write {len(parcel_many_null_df.index)} rows with > {max_number_null} of "
+            f"{len(result_df.columns)} columns==null to {parcel_many_null_path}"
+        )
+        pdh.to_file(parcel_many_null_df, parcel_many_null_path)
 
         # Now remove them from result
         result_df = result_df[result_df.isnull().sum(axis=1) <= max_number_null]
 
     # For rows with some null values, set them to 0
-    # TODO: first rough test of using interpolation doesn't give a difference, maybe better if
-    #       smarter interpolation is used (= only between the different types of data: 
-    #       S1_GRD_VV, S1_GRD_VH, S1_COH_VV, S1_COH_VH, ASC?, DESC?, S2
-    #result_df.interpolate(inplace=True)
+    # TODO: first rough test of using interpolation doesn't give a difference, maybe
+    # better if smarter interpolation is used (= only between the different types of
+    # data: S1_GRD_VV, S1_GRD_VH, S1_COH_VV, S1_COH_VH, ASC?, DESC?, S2
+    # result_df.interpolate(inplace=True)
     result_df.fillna(0, inplace=True)
-    
+
     # Write output file...
-    logger.info(f"Write output to file, start: {output_filepath}")
-    pdh.to_file(result_df, output_filepath)
+    logger.info(f"Write output to file, start: {output_path}")
+    pdh.to_file(result_df, output_path)
     logger.info(f"Write output to file, ready (with shape: {result_df.shape})")
diff --git a/cropclassification/preprocess/timeseries_calc_dias_onda_per_image.py b/cropclassification/preprocess/timeseries_calc_dias_onda_per_image.py
index 2f8e7e91..9c2e0a82 100644
--- a/cropclassification/preprocess/timeseries_calc_dias_onda_per_image.py
+++ b/cropclassification/preprocess/timeseries_calc_dias_onda_per_image.py
@@ -12,7 +12,7 @@
 import os
 from pathlib import Path
 import shutil
-import signal    # To catch CTRL-C explicitly and kill children
+import signal  # To catch CTRL-C explicitly and kill children
 import sys
 import time
 from typing import List, Optional, Tuple
@@ -21,7 +21,7 @@
 import geopandas as gpd
 import geofileops as gfo
 import pandas as pd
-import psutil    # To catch CTRL-C explicitly and kill children
+import psutil  # To catch CTRL-C explicitly and kill children
 import rasterio
 from rasterstats import zonal_stats
 from shapely.geometry import polygon as sh_polygon
@@ -32,24 +32,28 @@
 # General init
 logger = logging.getLogger(__name__)
 
+
 def calc_stats_per_image(
-        features_filepath: Path,
-        id_column: str,
-        image_paths: List[Path],
-        bands: List[str],
-        output_dir: Path,
-        temp_dir: Path,
-        log_dir: Path,
-        log_level: str,
-        max_cloudcover_pct: float = -1,
-        force: bool = False):
+    features_path: Path,
+    id_column: str,
+    image_paths: List[Path],
+    bands: List[str],
+    output_dir: Path,
+    temp_dir: Path,
+    log_dir: Path,
+    log_level: str,
+    max_cloudcover_pct: float = -1,
+    force: bool = False,
+):
     """
     Calculate the statistics.
     """
 
-    # TODO: probably need to apply some object oriented approach here for "image", because there are to many properties,... to be clean/clear this way.
-    # TODO: maybe passing the executor pool to a calc_stats_for_image function can have both the advantage of not creating loads of processes + 
-    # keep the cleanup logic after calculation together with the processing logic
+    # TODO: probably need to apply some object oriented approach here for "image",
+    # because there are to many properties,... to be clean/clear this way.
+    # TODO: maybe passing the executor pool to a calc_stats_for_image function can have
+    # both the advantage of not creating loads of processes + keep the cleanup logic
+    # after calculation together with the processing logic
 
     # Some checks on the input parameters
     if len(image_paths) == 0:
@@ -60,16 +64,17 @@ def calc_stats_per_image(
     os.makedirs(output_dir, exist_ok=True)
     os.makedirs(temp_dir, exist_ok=True)
     os.makedirs(log_dir, exist_ok=True)
-    
+
     # Create process pool for parallelisation...
     nb_parallel_max = multiprocessing.cpu_count()
     nb_parallel = nb_parallel_max
     with futures.ProcessPoolExecutor(nb_parallel) as pool:
 
-        # Loop over all images to start the data preparation for each of them in parallel...
+        # Loop over all images to start the data preparation for each of them in
+        # parallel...
         image_paths.sort()
         start_time = datetime.now()
-        esaSwitchedProcessingMethod = dateutil.parser.isoparse('2021-02-23').timestamp()
+        esaSwitchedProcessingMethod = dateutil.parser.isoparse("2021-02-23").timestamp()
         nb_todo = len(image_paths)
         nb_errors_max = 10
         nb_errors = 0
@@ -84,276 +89,399 @@ def calc_stats_per_image(
             # break out of the loop...
             while True:
 
-                ##### Start preparation for calculation on next image + features combo #####
+                # Start preparation for calculation on next image + features combo
 
-                # If not all images aren't prepared for processing yet, and there aren't too 
-                # many busy yet, prepare the next one 
-                if(i < len(image_paths) 
-                        and len(filter_on_status(image_dict, 'IMAGE_PREPARE_CALC_BUSY')) < nb_parallel_max):
+                # If not all images aren't prepared for processing yet, and there
+                # aren't too many busy yet, prepare the next one
+                if (
+                    i < len(image_paths)
+                    and len(filter_on_status(image_dict, "IMAGE_PREPARE_CALC_BUSY"))
+                    < nb_parallel_max
+                ):
 
-                    # Not too many busy preparing, so get next image_path to start prepare on
+                    # Not too many busy preparing, so get next image_path to start
+                    # prepare on
                     image_path_str = image_paths[i]
                     i += 1
 
                     # Get more detailed info about the image
                     try:
                         image_info = get_image_info(Path(image_path_str))
-                    except:
+                    except Exception:
                         # If not possible to get info for image, log and skip it
-                        nb_errors += 1 
-                        logger.exception(f"SKIP image, because error getting info for {image_path_str}")
+                        nb_errors += 1
+                        logger.exception(
+                            f"SKIP image: error getting info for {image_path_str}"
+                        )
                         nb_todo -= 1
                         continue
 
                     # If sentinel2 and cloud coverage too high... skip
-                    if(max_cloudcover_pct >= 0 
-                            and image_info['satellite'].startswith('S2') 
-                            and image_info['Cloud_Coverage_Assessment'] > max_cloudcover_pct):
-                        logger.info(f"SKIP image, Cloud_Coverage_Assessment: {image_info['Cloud_Coverage_Assessment']:0.2f} > {max_cloudcover_pct} for {image_path_str}")
+                    if (
+                        max_cloudcover_pct >= 0
+                        and image_info["satellite"].startswith("S2")
+                        and image_info["Cloud_Coverage_Assessment"] > max_cloudcover_pct
+                    ):
+                        logger.info(
+                            "SKIP image, Cloud_Coverage_Assessment: "
+                            f"{image_info['Cloud_Coverage_Assessment']:0.2f} > "
+                            f"{max_cloudcover_pct} for {image_path_str}"
+                        )
                         nb_todo -= 1
                         continue
 
                     # Fast-24h <- 2021-02-23 -> NRT-3H
-                    productTimelinessCategory = 'Fast-24h' if dateutil.parser.isoparse(image_info['acquisition_date']).timestamp() < esaSwitchedProcessingMethod else 'NRT-3h'
-                    
-                    # If sentinel1 and wrong productTimelinessCategory, skip: we only want 1 type to evade images used twice
-                    if image_info['satellite'].startswith('S1') and image_info['productTimelinessCategory'] != productTimelinessCategory:
-                        logger.info(f"SKIP image, productTimelinessCategory should be '{productTimelinessCategory}', but is: {image_info['productTimelinessCategory']} for {image_path_str}")
+                    productTimelinessCategory = (
+                        "Fast-24h"
+                        if dateutil.parser.isoparse(
+                            image_info["acquisition_date"]
+                        ).timestamp()
+                        < esaSwitchedProcessingMethod
+                        else "NRT-3h"
+                    )
+
+                    # If sentinel1 and wrong productTimelinessCategory, skip: we only
+                    # want 1 type to evade images used twice
+                    if (
+                        image_info["satellite"].startswith("S1")
+                        and image_info["productTimelinessCategory"]
+                        != productTimelinessCategory
+                    ):
+                        logger.info(
+                            f"SKIP image, productTimelinessCategory should be "
+                            f"'{productTimelinessCategory}', but is: "
+                            f"{image_info['productTimelinessCategory']} for "
+                            f"{image_path_str}"
+                        )
                         nb_todo -= 1
                         continue
 
                     # Create base output filename
                     orbit = None
-                    if image_info['satellite'].startswith('S1'):
-                        orbit = image_info['orbit_properties_pass']    
-                    output_base_filepath = format_output_filepath(
-                            features_filepath, Path(image_path_str), output_dir, orbit, band=None)
-                    output_base_dir, output_base_filename = os.path.split(output_base_filepath)
-                    output_base_busy_filepath = f"{output_base_dir}{os.sep}BUSY_{output_base_filename}"
+                    if image_info["satellite"].startswith("S1"):
+                        orbit = image_info["orbit_properties_pass"]
+                    output_base_path = format_output_path(
+                        features_path,
+                        Path(image_path_str),
+                        output_dir,
+                        orbit,
+                        band=None,
+                    )
+                    output_base_dir, output_base_filename = os.path.split(
+                        output_base_path
+                    )
+                    output_base_busy_path = (
+                        f"{output_base_dir}{os.sep}BUSY_{output_base_filename}"
+                    )
 
                     # Check for which bands there is a valid output file already
                     bands_done = 0
                     for band in bands:
-                        # Prepare the output filepaths...
-                        output_band_filepath = format_output_filepath(
-                                features_filepath, Path(image_path_str), output_dir, orbit, band)
-                        output_band_dir, output_band_filename = os.path.split(output_band_filepath)
-                        output_band_busy_filepath = f"{output_band_dir}{os.sep}BUSY_{output_band_filename}"
-
-                        # If a busy output file exists, remove it, otherwise we can get double data in it...
-                        if os.path.exists(output_band_busy_filepath):
-                            os.remove(output_band_busy_filepath)
+                        # Prepare the output paths...
+                        output_band_path = format_output_path(
+                            features_path,
+                            Path(image_path_str),
+                            output_dir,
+                            orbit,
+                            band,
+                        )
+                        output_band_dir, output_band_filename = os.path.split(
+                            output_band_path
+                        )
+                        output_band_busy_path = (
+                            f"{output_band_dir}{os.sep}BUSY_{output_band_filename}"
+                        )
+
+                        # If a busy output file exists, remove it, otherwise we can get
+                        # double data in it...
+                        if os.path.exists(output_band_busy_path):
+                            os.remove(output_band_busy_path)
 
                         # Check if the output file exists already
-                        if os.path.exists(output_band_filepath):
-                            if force == False:
-                                logger.debug(f"Output file for band exists already {output_band_filepath}")
+                        if os.path.exists(output_band_path):
+                            if not force:
+                                logger.debug(
+                                    f"Output file for band exists {output_band_path}"
+                                )
                                 bands_done += 1
                             else:
-                                os.remove(output_band_filepath)
+                                os.remove(output_band_path)
 
                     # If all bands already processed, skip image...
                     if len(bands) == bands_done:
-                        logger.info(f"SKIP image: output files for all bands exist already for {output_base_filepath}")
+                        logger.info(
+                            "SKIP image: output files for all bands exist already for "
+                            f"{output_base_path}"
+                        )
                         nb_todo -= 1
                         continue
 
                     # Start the prepare processing assync
                     # TODO: possibly it is cleaner to do this per band...
-                    future = pool.submit(prepare_calc, 
-                                        features_filepath,
-                                        id_column,
-                                        Path(image_path_str),
-                                        output_base_filepath,
-                                        temp_dir,
-                                        log_dir,
-                                        log_level,
-                                        nb_parallel_max)
-                    image_dict[image_path_str] = {'features_filepath': features_filepath,
-                                            'prepare_calc_future': future, 
-                                            'image_info': image_info,
-                                            'prepare_calc_starttime': datetime.now(),
-                                            'output_base_filepath': output_base_filepath,
-                                            'output_base_busy_filepath': output_base_busy_filepath,
-                                            'status': 'IMAGE_PREPARE_CALC_BUSY'}
-                    
+                    future = pool.submit(
+                        prepare_calc,
+                        features_path,
+                        id_column,
+                        Path(image_path_str),
+                        output_base_path,
+                        temp_dir,
+                        log_dir,
+                        log_level,
+                        nb_parallel_max,
+                    )
+                    image_dict[image_path_str] = {
+                        "features_path": features_path,
+                        "prepare_calc_future": future,
+                        "image_info": image_info,
+                        "prepare_calc_starttime": datetime.now(),
+                        "output_base_path": output_base_path,
+                        "output_base_busy_path": output_base_busy_path,
+                        "status": "IMAGE_PREPARE_CALC_BUSY",
+                    }
+
                     # Jump to next image to start the prepare_calc for it...
                     continue
 
-                ##### For images busy preparing: if ready: start real calculation in batches #####
+                # For images busy preparing: if ready: start real calculation in batches
 
                 # Loop through the busy preparations
-                for image_path_str in filter_on_status(image_dict, 'IMAGE_PREPARE_CALC_BUSY'):
-                    
+                for image_path_str in filter_on_status(
+                    image_dict, "IMAGE_PREPARE_CALC_BUSY"
+                ):
+
                     # If still running, go to next
                     image = image_dict[image_path_str]
-                    if image['prepare_calc_future'].running():
+                    if image["prepare_calc_future"].running():
                         continue
 
                     # Extract the result from the preparation
                     try:
                         # Get the result from the completed  prepare_inputs
-                        prepare_calc_result = image['prepare_calc_future'].result()
+                        prepare_calc_result = image["prepare_calc_future"].result()
 
-                        # If nb_features to be treated is 0... create (empty) output files and continue with next...
-                        if prepare_calc_result['nb_features_to_calc_total'] == 0:
+                        # If nb_features to be treated is 0... create (empty) output
+                        # files and continue with next...
+                        if prepare_calc_result["nb_features_to_calc_total"] == 0:
                             for band in bands:
-                                # Prepare the output filepath...
+                                # Prepare the output path...
                                 orbit = None
-                                if image['image_info']['satellite'].startswith('S1'):
-                                    orbit = image['image_info']['orbit_properties_pass']
-                                output_band_filepath = format_output_filepath(
-                                        features_filepath, Path(image_path_str), output_dir, orbit, band)
+                                if image["image_info"]["satellite"].startswith("S1"):
+                                    orbit = image["image_info"]["orbit_properties_pass"]
+                                output_band_path = format_output_path(
+                                    features_path,
+                                    Path(image_path_str),
+                                    output_dir,
+                                    orbit,
+                                    band,
+                                )
 
                                 # Create output file
-                                logger.info(f"No features found overlapping image, just create done file: {output_band_filepath}")                                
-                                create_file_atomic(output_band_filepath)
-                                image['status'] = 'IMAGE_CALC_DONE'
+                                logger.info(
+                                    "No features found in image: create done file: "
+                                    f"{output_band_path}"
+                                )
+                                create_file_atomic(output_band_path)
+                                image["status"] = "IMAGE_CALC_DONE"
 
                             # Jump to next image
                             continue
 
-                        # Add info about the result of the prepare_calc to the image info...
-                        image['image_prepared_path'] = prepare_calc_result['image_prepared_path']
-                        image['feature_batches'] = prepare_calc_result['feature_batches']
-                        image['nb_features_to_calc_total'] = prepare_calc_result['nb_features_to_calc_total']
-                        image['temp_features_dir'] = prepare_calc_result['temp_features_dir']
+                        # Add info about the result of the prepare_calc to image info
+                        image["image_prepared_path"] = prepare_calc_result[
+                            "image_prepared_path"
+                        ]
+                        image["feature_batches"] = prepare_calc_result[
+                            "feature_batches"
+                        ]
+                        image["nb_features_to_calc_total"] = prepare_calc_result[
+                            "nb_features_to_calc_total"
+                        ]
+                        image["temp_features_dir"] = prepare_calc_result[
+                            "temp_features_dir"
+                        ]
 
                         # Set status to calc_is_busy so we know calculation is busy...
-                        image['status'] = 'IMAGE_CALC_BUSY'
-                        image['calc_starttime'] = datetime.now()
-
-                        # Now loop through all prepared feature batches to start the statistics calculation for each
-                        logger.info(f"Start statistics calculation for {image_path_str}")  
-                        for features_batch in image['feature_batches']:
+                        image["status"] = "IMAGE_CALC_BUSY"
+                        image["calc_starttime"] = datetime.now()
+
+                        # Now loop through all prepared feature batches to start the
+                        # statistics calculation for each
+                        logger.info(
+                            f"Start statistics calculation for {image_path_str}"
+                        )
+                        for features_batch in image["feature_batches"]:
                             start_time_batch = datetime.now()
                             future = pool.submit(
-                                    calc_stats_image_gdf, 
-                                    features_batch['filepath'],
-                                    id_column,
-                                    image['image_prepared_path'],
-                                    bands,
-                                    image['output_base_busy_filepath'],
-                                    log_dir,
-                                    log_level,
-                                    start_time_batch)
-                            calc_stats_batch_dict[features_batch['filepath']] = {
-                                    'calc_stats_future': future,
-                                    'image_path': image_path_str,
-                                    'image_prepared_path': image['image_prepared_path'], 
-                                    'start_time_batch': start_time_batch,
-                                    'nb_items_batch': features_batch['nb_items'],
-                                    'status': 'BATCH_CALC_BUSY'}
+                                calc_stats_image_gdf,
+                                features_batch["path"],
+                                id_column,
+                                image["image_prepared_path"],
+                                bands,
+                                image["output_base_busy_path"],
+                                log_dir,
+                                log_level,
+                                start_time_batch,
+                            )
+                            calc_stats_batch_dict[features_batch["path"]] = {
+                                "calc_stats_future": future,
+                                "image_path": image_path_str,
+                                "image_prepared_path": image["image_prepared_path"],
+                                "start_time_batch": start_time_batch,
+                                "nb_items_batch": features_batch["nb_items"],
+                                "status": "BATCH_CALC_BUSY",
+                            }
 
                     except Exception as ex:
-                        message = f"Exception getting result of prepare_calc for {image}"
+                        message = (
+                            f"Exception getting result of prepare_calc for {image}"
+                        )
                         logger.exception(message)
                         nb_errors += 1
                         if nb_errors > nb_errors_max:
                             raise Exception(message) from ex
 
-                ##### For batches with calc busy, check if ready #####
+                # For batches with calc busy, check if ready
 
                 # Loop through the busy batche calculations
-                for calc_stats_batch_id in filter_on_status(calc_stats_batch_dict, 'BATCH_CALC_BUSY'):
-                    
+                for calc_stats_batch_id in filter_on_status(
+                    calc_stats_batch_dict, "BATCH_CALC_BUSY"
+                ):
+
                     # If it is not done yet, continue
                     calc_stats_batch_info = calc_stats_batch_dict[calc_stats_batch_id]
-                    if calc_stats_batch_info['calc_stats_future'].done() is False:
+                    if calc_stats_batch_info["calc_stats_future"].done() is False:
                         continue
 
                     try:
                         # Get the result
-                        result = calc_stats_batch_info['calc_stats_future'].result()
+                        result = calc_stats_batch_info["calc_stats_future"].result()
                         if not result:
                             raise Exception("Returned False?")
-                        
+
                         # Set the processed flag to True
-                        calc_stats_batch_info['status'] = 'BATCH_CALC_DONE'
-                        logger.debug(f"Ready processing batch of {calc_stats_batch_info['nb_items_batch']} for features image: {calc_stats_batch_info['image_path']}")
+                        calc_stats_batch_info["status"] = "BATCH_CALC_DONE"
+                        logger.debug(
+                            "Ready processing batch of "
+                            f"{calc_stats_batch_info['nb_items_batch']} for features "
+                            f"image: {calc_stats_batch_info['image_path']}"
+                        )
 
                     except Exception as ex:
-                        message = f"Exception getting result of calc_stats_image_gdf for {calc_stats_batch_info}"
+                        message = (
+                            "Exception getting result of calc_stats_image_gdf for "
+                            f"{calc_stats_batch_info}"
+                        )
                         logger.exception(message)
                         nb_errors += 1
                         if nb_errors > nb_errors_max:
-                            raise Exception(message) from ex 
+                            raise Exception(message) from ex
 
-                ##### For images with calc busy, check if they are ready #####
+                # For images with calc busy, check if they are ready
 
                 # Loop through busy image calculations
-                for image_path_str in filter_on_status(image_dict, 'IMAGE_CALC_BUSY'):
-                
-                    # If there are still batches busy for this image, continue to next image
+                for image_path_str in filter_on_status(image_dict, "IMAGE_CALC_BUSY"):
+
+                    # If still batches busy for this image, continue to next image
                     batches_busy = False
-                    for calc_stats_batch_id in filter_on_status(calc_stats_batch_dict, 'BATCH_CALC_BUSY'):
-                        if calc_stats_batch_dict[calc_stats_batch_id]['image_path'] == image_path_str:
+                    for calc_stats_batch_id in filter_on_status(
+                        calc_stats_batch_dict, "BATCH_CALC_BUSY"
+                    ):
+                        if (
+                            calc_stats_batch_dict[calc_stats_batch_id]["image_path"]
+                            == image_path_str
+                        ):
                             batches_busy = True
                             break
-                    if batches_busy == True:
+                    if batches_busy:
                         continue
-                    
+
                     # All batches are done, so the image is done
                     image = image_dict[image_path_str]
-                    image['status'] = 'IMAGE_CALC_DONE'
-            
+                    image["status"] = "IMAGE_CALC_DONE"
+
                     # If the preprocessing created a temp image file, clean it up
-                    image_prepared_path = image['image_prepared_path']
+                    image_prepared_path = image["image_prepared_path"]
                     if image_prepared_path != image_path_str:
-                        logger.info(f"Remove local temp image copy: {image_prepared_path}")
-                        if image_prepared_path.isdir():           
+                        logger.info(
+                            f"Remove local temp image copy: {image_prepared_path}"
+                        )
+                        if image_prepared_path.isdir():
                             shutil.rmtree(image_prepared_path, ignore_errors=True)
                         else:
                             os.remove(image_prepared_path)
-                    # If the preprocessing created temp pickle files with features, clean them up
-                    shutil.rmtree(image['temp_features_dir'], ignore_errors=True)
+
+                    # If the preprocessing created temp pickle files with features,
+                    # clean them up
+                    shutil.rmtree(image["temp_features_dir"], ignore_errors=True)
 
                     # Rename the (completed) output files
-                    output_base_filepath_noext, output_base_ext = os.path.splitext(image['output_base_filepath'])
-                    output_base_busy_filepath_noext, output_base_busy_ext = os.path.splitext(image['output_base_busy_filepath'])
+                    output_base_path_noext, output_base_ext = os.path.splitext(
+                        image["output_base_path"]
+                    )
+                    (
+                        output_base_busy_noext,
+                        output_base_busy_ext,
+                    ) = os.path.splitext(image["output_base_busy_path"])
                     for band in bands:
-                        # TODO: creating the output filepaths should probably be cleaner centralised
-                        output_band_busy_filepath = Path(f"{output_base_busy_filepath_noext}_{band}{output_base_busy_ext}")
-                        output_band_filepath = Path(f"{output_base_filepath_noext}_{band}{output_base_ext}")
+                        # TODO: creating the output paths should probably be cleaner
+                        # centralised
+                        output_band_busy_path = Path(
+                            f"{output_base_busy_noext}_{band}{output_base_busy_ext}"
+                        )
+                        output_band_path = Path(
+                            f"{output_base_path_noext}_{band}{output_base_ext}"
+                        )
 
                         # If BUSY output file exists, rename it
-                        if output_band_busy_filepath.exists():
-                            os.rename(output_band_busy_filepath, output_band_filepath)
+                        if output_band_busy_path.exists():
+                            os.rename(output_band_busy_path, output_band_path)
                         else:
                             # If BUSY output file doesn't exist, create empty file
-                            logger.info(f"No features found overlapping image after processing, create done file: {output_band_filepath}")
-                            create_file_atomic(output_band_filepath)
+                            logger.info(
+                                "No features found overlapping image after processing, "
+                                f"create done file: {output_band_path}"
+                            )
+                            create_file_atomic(output_band_path)
 
                     # Log the progress and prediction speed
                     logger.info(f"Ready processing image: {image_path_str}")
                     nb_done_latestbatch = 1
                     nb_done_total += nb_done_latestbatch
                     progress_msg = format_progress_message(
-                            nb_todo, nb_done_total, nb_done_latestbatch, 
-                            start_time, image['calc_starttime'])
+                        nb_todo,
+                        nb_done_total,
+                        nb_done_latestbatch,
+                        start_time,
+                        image["calc_starttime"],
+                    )
                     logger.info(progress_msg)
 
-                ##### Check if we are completely ready #####
+                # Check if we are completely ready
 
                 # This is the case if:
                 #     - no processing is needed (= empty image_dict)
-                #     - OR if all processing is started + is done (= status IMAGE_CALC_DONE)
-                if(len(image_dict) == 0
-                   or (i == len(image_paths)
-                       and len(image_dict) == len(filter_on_status(image_dict, 'IMAGE_CALC_DONE')))):                   
+                #     - OR if all processing is started + is done
+                #       (= status IMAGE_CALC_DONE)
+                if len(image_dict) == 0 or (
+                    i == len(image_paths)
+                    and len(image_dict)
+                    == len(filter_on_status(image_dict, "IMAGE_CALC_DONE"))
+                ):
                     if nb_errors == 0:
                         # Ready, so jump out of unending while loop
-                        break 
+                        break
                     else:
-                        raise Exception(f"Ready processing, but there were {nb_errors} errors!")                       
+                        raise Exception(
+                            f"Ready processing, but there were {nb_errors} errors!"
+                        )
 
                 # Some sleep before starting next iteration...
                 time.sleep(0.1)
 
         except KeyboardInterrupt:
             # If CTRL+C is used, shut down pool and kill children
-            print('You pressed Ctrl+C')
-            print('Worker processes are being stopped, followed by exit!')
+            print("You pressed Ctrl+C")
+            print("Worker processes are being stopped, followed by exit!")
 
             # Stop process pool + kill children + exit
             try:
@@ -366,67 +494,75 @@ def calc_stats_per_image(
             finally:
                 sys.exit(1)
 
-        logger.info(f"Time taken to calculate data for {nb_todo} images: {(datetime.now()-start_time).total_seconds()} sec")
+        logger.info(
+            f"Time taken to calculate data for {nb_todo} images: "
+            f"{(datetime.now()-start_time).total_seconds()} sec"
+        )
 
-def filter_on_status(dict: dict,
-                     status_to_check: str) -> List[str]:
-    """ 
+
+def filter_on_status(dict: dict, status_to_check: str) -> List[str]:
+    """
     Function to check the number of images that are being prepared for processing
     """
     keys_with_status = []
     for key in dict:
-        if dict[key]['status'] == status_to_check:
+        if dict[key]["status"] == status_to_check:
             keys_with_status.append(key)
     return keys_with_status
 
-def format_output_filepath(
-        features_filepath: Path, 
-        image_path: Path,
-        output_dir: Path,
-        orbit_properties_pass: Optional[str],
-        band: Optional[str]) -> Path:
+
+def format_output_path(
+    features_path: Path,
+    image_path: Path,
+    output_dir: Path,
+    orbit_properties_pass: Optional[str],
+    band: Optional[str],
+) -> Path:
     """
-    Prepare the output filepath.
+    Prepare the output path.
     """
     # Filename of the features
-    features_filename = os.path.splitext(os.path.basename(features_filepath))[0] 
+    features_filename = os.path.splitext(os.path.basename(features_path))[0]
     # Filename of the image -> remove .zip if it is a zip file
     image_filename = os.path.basename(image_path)
     image_filename_noext, image_ext = os.path.splitext(image_filename)
-    if image_ext.lower() == '.zip':
+    if image_ext.lower() == ".zip":
         image_filename = image_filename_noext
 
     # Interprete the orbit...
     if orbit_properties_pass is not None:
-        if orbit_properties_pass == 'ASCENDING':
-            orbit = '_ASC'
-        elif orbit_properties_pass == 'DESCENDING':
-            orbit = '_DESC'
+        if orbit_properties_pass == "ASCENDING":
+            orbit = "_ASC"
+        elif orbit_properties_pass == "DESCENDING":
+            orbit = "_DESC"
         else:
             message = f"Unknown orbit_properties_pass: {orbit_properties_pass}"
             logger.error(message)
             raise Exception(message)
     else:
-        orbit = ''
+        orbit = ""
 
-    # Prepare basic filepath
-    output_filepath_noext = os.path.join(
-            output_dir, f"{features_filename}__{image_filename}{orbit}")
+    # Prepare basic path
+    output_path_noext = os.path.join(
+        output_dir, f"{features_filename}__{image_filename}{orbit}"
+    )
 
     # If a band was specified
     if band is not None:
-        output_filepath_noext = f"{output_filepath_noext}_{band}"
-    
+        output_path_noext = f"{output_path_noext}_{band}"
+
     # Add extension
-    output_filepath = Path(f"{output_filepath_noext}.sqlite")
-    return output_filepath
+    output_path = Path(f"{output_path_noext}.sqlite")
+    return output_path
+
 
 def format_progress_message(
-        nb_todo: int, 
-        nb_done_total: int, 
-        nb_done_latestbatch: int, 
-        start_time: datetime, 
-        start_time_latestbatch: datetime) -> str:   
+    nb_todo: int,
+    nb_done_total: int,
+    nb_done_latestbatch: int,
+    start_time: datetime,
+    start_time_latestbatch: datetime,
+) -> str:
     """
     Returns a progress message based on the input.
 
@@ -437,51 +573,66 @@ def format_progress_message(
         start_time: datetime the processing started
         start_time_latestbatch: datetime the latest batch started
     """
-    time_passed_s = (datetime.now()-start_time).total_seconds()
-    time_passed_latestbatch_s = (datetime.now()-start_time_latestbatch).total_seconds()
+    time_passed_s = (datetime.now() - start_time).total_seconds()
+    time_passed_latestbatch_s = (
+        datetime.now() - start_time_latestbatch
+    ).total_seconds()
 
     # Calculate the overall progress
     large_number = 9999999999
     if time_passed_s > 0:
-        nb_per_hour = (nb_done_total/time_passed_s) * 3600
+        nb_per_hour = (nb_done_total / time_passed_s) * 3600
     else:
         nb_per_hour = large_number
-    hours_to_go = (int)((nb_todo-nb_done_total)/nb_per_hour)
-    min_to_go = (int)((((nb_todo-nb_done_total)/nb_per_hour)%1)*60)
+    hours_to_go = (int)((nb_todo - nb_done_total) / nb_per_hour)
+    min_to_go = (int)((((nb_todo - nb_done_total) / nb_per_hour) % 1) * 60)
 
     # Calculate the speed of the latest batch
-    if time_passed_latestbatch_s > 0: 
-        nb_per_hour_latestbatch = (nb_done_latestbatch/time_passed_latestbatch_s) * 3600
+    if time_passed_latestbatch_s > 0:
+        nb_per_hour_latestbatch = (
+            nb_done_latestbatch / time_passed_latestbatch_s
+        ) * 3600
     else:
         nb_per_hour_latestbatch = large_number
-    
+
     # Return formatted message
-    message = f"{hours_to_go}:{min_to_go} left for {nb_todo-nb_done_total} todo at {nb_per_hour:0.0f}/h ({nb_per_hour_latestbatch:0.0f}/h last batch)"
+    message = (
+        f"{hours_to_go}:{min_to_go} left for {nb_todo-nb_done_total} todo at "
+        f"{nb_per_hour:0.0f}/h ({nb_per_hour_latestbatch:0.0f}/h last batch)"
+    )
     return message
-    
+
+
 def prepare_calc(
-            features_filepath: Path,
-            id_column: str,
-            image_path: Path,
-            output_filepath: Path,
-            temp_dir: Path,
-            log_dir: Path,
-            log_level: str,
-            nb_parallel_max: int = 16) -> dict:
+    features_path: Path,
+    id_column: str,
+    image_path: Path,
+    output_path: Path,
+    temp_dir: Path,
+    log_dir: Path,
+    log_level: str,
+    nb_parallel_max: int = 16,
+) -> dict:
     """
     Prepare the inputs for a calculation.
 
     Returns True if succesfully completed.
-    Remark: easiest it returns something, when used in a parallel way: concurrent.futures likes it better if something is returned
+    Remark: easiest it returns something, when used in a parallel way:
+    concurrent.futures likes it better if something is returned
     """
-    # When running in parallel processes, the logging needs to be write to seperate files + no console logging
+    # When running in parallel processes, the logging needs to be write to seperate
+    # files + no console logging
     global logger
-    logger = logging.getLogger('prepare_calc')
+    logger = logging.getLogger("prepare_calc")
     logger.propagate = False
-    log_filepath = log_dir / f"{datetime.now():%Y-%m-%d_%H-%M-%S}_prepare_calc_{os.getpid()}.log"
-    fh = logging.FileHandler(filename=log_filepath)
+    log_path = (
+        log_dir / f"{datetime.now():%Y-%m-%d_%H-%M-%S}_prepare_calc_{os.getpid()}.log"
+    )
+    fh = logging.FileHandler(filename=log_path)
     fh.setLevel(log_level)
-    fh.setFormatter(logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|%(funcName)s|%(message)s'))
+    fh.setFormatter(
+        logging.Formatter("%(asctime)s|%(levelname)s|%(name)s|%(funcName)s|%(message)s")
+    )
     logger.addHandler(fh)
 
     ret_val = {}
@@ -490,7 +641,7 @@ def prepare_calc(
     logger.info(f"Start prepare_image for {image_path} to {temp_dir}")
     image_prepared_path = prepare_image(image_path, temp_dir)
     logger.debug(f"Preparing ready, result: {image_prepared_path}")
-    ret_val['image_prepared_path'] = image_prepared_path
+    ret_val["image_prepared_path"] = image_prepared_path
 
     # Get info about the image
     logger.info(f"Start get_image_info for {image_prepared_path}")
@@ -498,160 +649,179 @@ def prepare_calc(
     logger.info(f"image_info: {image_info}")
 
     # Load the features that overlap with the image.
-    # TODO: passing both bbox and poly is double, or not? 
+    # TODO: passing both bbox and poly is double, or not?
     # footprint epsg should be passed as well, or reproject here first?
     footprint_shape = None
-    if 'footprint' in image_info:
+    if "footprint" in image_info:
         logger.info(f"poly: {image_info['footprint']['shape']}")
-        footprint_shape = image_info['footprint']['shape']
+        footprint_shape = image_info["footprint"]["shape"]
 
-    if image_info['image_epsg'] == 'NONE':
-        raise Exception(f'target_epsg == NONE: {image_info}')
+    if image_info["image_epsg"] == "NONE":
+        raise Exception(f"target_epsg == NONE: {image_info}")
 
     features_gdf = load_features_file(
-            features_filepath=features_filepath,
-            target_epsg=image_info['image_epsg'],
-            columns_to_retain=[id_column, 'geometry'],
-            bbox=image_info['image_bounds'],
-            polygon=footprint_shape)
+        features_path=features_path,
+        target_epsg=image_info["image_epsg"],
+        columns_to_retain=[id_column, "geometry"],
+        bbox=image_info["image_bounds"],
+        polygon=footprint_shape,
+    )
 
     # Check if overlapping features were found, otherwise no use to proceed
     nb_todo = len(features_gdf.index)
-    ret_val['nb_features_to_calc_total'] = nb_todo
+    ret_val["nb_features_to_calc_total"] = nb_todo
     if nb_todo == 0:
-        logger.info(f"No features were found in the bounding box of the image, so return: {image_path}")
+        logger.info(f"No features found in the bounding box of the image: {image_path}")
         return ret_val
-    
+
     # Calculate the number per batch, but keep the number between 100 and 50000...
-    nb_per_batch = min(max(math.ceil(nb_todo/nb_parallel_max), 100), 50000)
+    nb_per_batch = min(max(math.ceil(nb_todo / nb_parallel_max), 100), 50000)
 
-    # The features were already sorted on x coordinate, so the features in the batches are 
-    # already clustered geographically 
-    features_gdf_batches = [features_gdf.loc[i:i+nb_per_batch-1,:] for i in range(0, nb_todo, nb_per_batch)]
+    # The features were already sorted on x coordinate, so the features in the batches
+    # are already clustered geographically
+    features_gdf_batches = [
+        features_gdf.loc[i: i + nb_per_batch - 1, :]
+        for i in range(0, nb_todo, nb_per_batch)
+    ]
 
     # Pickle the batches to temporary files
     # Create temp dir to put the pickles in... and clean or create it.
     # TODO: change dir so it is always unique
     temp_features_dirname = os.path.splitext(os.path.basename(image_path))[0]
     temp_features_dir = os.path.join(temp_dir, temp_features_dirname)
-    ret_val['temp_features_dir'] = temp_features_dir
+    ret_val["temp_features_dir"] = temp_features_dir
     if os.path.exists(temp_features_dir):
         logger.info(f"Remove dir {temp_features_dir + os.sep}")
         shutil.rmtree(temp_features_dir + os.sep)
     else:
         os.makedirs(temp_features_dir, exist_ok=True)
 
-    '''
+    """
     if os.path.exists(temp_features_dir):
         logger.info(f"Temp dir exists: {temp_features_dir}")
-    '''
+    """
 
-    # Loop over the batches, pickle them and add the filepaths to the result...
-    ret_val['feature_batches'] = []
+    # Loop over the batches, pickle them and add the paths to the result...
+    ret_val["feature_batches"] = []
     for i, features_gdf_batch in enumerate(features_gdf_batches):
         batch_info = {}
-        pickle_filepath = os.path.join(temp_features_dir, f"{i}.pkl")
-        logger.info(f"Write pkl of {len(features_gdf_batch.index)} features: {pickle_filepath}")
+        pickle_path = os.path.join(temp_features_dir, f"{i}.pkl")
+        logger.info(
+            f"Write pkl of {len(features_gdf_batch.index)} features: {pickle_path}"
+        )
         try:
-            features_gdf_batch.to_pickle(pickle_filepath)
+            features_gdf_batch.to_pickle(pickle_path)
         except Exception as ex:
-            raise Exception(f"Exception writing pickle: {pickle_filepath}: {ex}") from ex
-        batch_info["filepath"] = pickle_filepath
+            raise Exception(f"Exception writing pickle: {pickle_path}: {ex}") from ex
+        batch_info["path"] = pickle_path
         batch_info["nb_items"] = len(features_gdf_batch.index)
         ret_val["feature_batches"].append(batch_info)
 
     # Ready... return
     return ret_val
 
+
 def load_features_file(
-        features_filepath: Path,
-        columns_to_retain: List[str],
-        target_epsg: str,
-        bbox = None,
-        polygon = None) -> gpd.GeoDataFrame:
+    features_path: Path,
+    columns_to_retain: List[str],
+    target_epsg: str,
+    bbox=None,
+    polygon=None,
+) -> gpd.GeoDataFrame:
     """
     Load the features and reproject to the target crs.
 
     Remarks:
-        * Reprojected version is "cached" on disk so on a next call, it can be directly read.
-        * Locking and waiting is used to ensure correct results even if used in a parallel way.
+        * Reprojected version is "cached" so on a next call, it can be directly read.
+        * Locking and waiting is used to ensure correct results if used in parallel.
 
     Args
-        features_filepath: 
-        columns_to_retain: 
-        target_srs: 
+        features_path:
+        columns_to_retain:
+        target_srs:
         bbox: bounds of the area to be loaded, in the target_epsg
     """
-    # Load parcel input file and preprocess it: remove excess columns + reproject if needed.
-    # By convention, the features filename should end on the projection... so extract epsg from filename
+    # Load parcel file and preprocess it: remove excess columns + reproject if needed.
+    # By convention, the features filename should end on the projection... so extract
+    # epsg from filename
     start_time = datetime.now()
     features_epsg = None
-    if features_filepath.stem.find('_') != -1:
-        splitted = features_filepath.stem.split('_')
-        features_epsg = splitted[len(splitted)-1]
+    if features_path.stem.find("_") != -1:
+        splitted = features_path.stem.split("_")
+        features_epsg = splitted[len(splitted) - 1]
 
     # Determine the correct filename for the input features in the correct projection.
     if features_epsg != target_epsg:
-        features_prepr_filepath = features_filepath.parent / f"{features_filepath.stem}_{target_epsg:.0f}.gpkg"
+        features_prepr_path = (
+            features_path.parent / f"{features_path.stem}_{target_epsg:.0f}.gpkg"
+        )
     else:
-        features_prepr_filepath = features_filepath
+        features_prepr_path = features_path
 
-    # Prepare filename for a "busy file" to ensure proper behaviour in a parallel processing context
-    features_prepr_filepath_busy = Path(f"{str(features_prepr_filepath)}_busy")
+    # Prepare filename for a "busy file" to ensure proper behaviour in a parallel
+    # processing context
+    features_prepr_path_busy = Path(f"{str(features_prepr_path)}_busy")
 
-    # If the file doesn't exist yet in right projection, read original input file to reproject/write to new file with correct epsg
+    # If the file doesn't exist yet in right projection, read original input file to
+    # reproject/write to new file with correct epsg
     features_gdf = None
-    if (not features_prepr_filepath_busy.exists() 
-        and not features_prepr_filepath.exists()):
-        
-        # Create lock file in an atomic way, so we are sure we are the only process working on it. 
-        # If function returns true, there isn't any other thread/process already working on it
-        if create_file_atomic(features_prepr_filepath_busy):
+    if not (features_prepr_path_busy.exists() or features_prepr_path.exists()):
+
+        # Create lock file in an atomic way, so we are sure we are the only process
+        # working on it. If function returns true, there isn't any other thread/process
+        # already working on it
+        if create_file_atomic(features_prepr_path_busy):
 
             try:
                 # Read (all) original features + remove unnecessary columns...
-                logger.info(f"Read original file {features_filepath}")
+                logger.info(f"Read original file {features_path}")
                 start_time = datetime.now()
                 logging.getLogger("fiona.ogrext").setLevel(logging.INFO)
-                features_gdf = gfo.read_file(features_filepath)
-                logger.info(f"Read ready, found {len(features_gdf.index)} features, crs: {features_gdf.crs}, took {(datetime.now()-start_time).total_seconds()} s")
+                features_gdf = gfo.read_file(features_path)
+                logger.info(
+                    f"Read ready, found {len(features_gdf.index)} features, "
+                    f"crs: {features_gdf.crs}, took "
+                    f"{(datetime.now()-start_time).total_seconds()} s"
+                )
                 for column in features_gdf.columns:
-                    if column not in columns_to_retain and column not in ['geometry', 'x_ref']:
+                    if column not in columns_to_retain and column not in [
+                        "geometry",
+                        "x_ref",
+                    ]:
                         features_gdf.drop(columns=column, inplace=True)
 
                 # Reproject them
-                logger.info(f"Reproject features from {features_gdf.crs} to epsg {target_epsg}")
+                logger.info(
+                    f"Reproject features from {features_gdf.crs} to epsg {target_epsg}"
+                )
                 features_gdf = features_gdf.to_crs(epsg=target_epsg)
                 logger.info("Reprojected, now sort on x_ref")
-                
+
+                if features_gdf is None:
+                    raise Exception("features_gdf is None")
                 # Order features on x coordinate
-                if 'x_ref' not in features_gdf.columns:
-                    features_gdf['x_ref'] = features_gdf.geometry.bounds.minx
-                features_gdf.sort_values(by=['x_ref'], inplace=True)
+                if "x_ref" not in features_gdf.columns:
+                    features_gdf["x_ref"] = features_gdf.geometry.bounds.minx
+                features_gdf.sort_values(by=["x_ref"], inplace=True)
                 features_gdf.reset_index(inplace=True)
 
                 # Cache the file for future use
-                logger.info(f"Write {len(features_gdf.index)} reprojected features to {features_prepr_filepath}")
-                gfo.to_file(features_gdf, features_prepr_filepath, index=False)
-                logger.info(f"Reprojected features written")
-
-                """ 
-                # TODO: redo performance test of using pkl as cache file
-                features_gdf.to_pickle('file.pkl')
-                logger.info(f"Pickled")
-                df2 = pd.read_pickle('file.pkl')
-                logger.info(f"Pickle read")
-                """
+                logger.info(
+                    f"Write {len(features_gdf.index)} reprojected features to "
+                    f"{features_prepr_path}"
+                )
+                gfo.to_file(features_gdf, features_prepr_path, index=False)
+                logger.info("Reprojected features written")
+
             except Exception as ex:
                 # If an exception occurs...
-                message = f"Exception, so delete possibly not completed file: {features_prepr_filepath}"
+                message = f"Delete possibly incomplete file: {features_prepr_path}"
                 logger.exception(message)
-                gfo.remove(features_prepr_filepath)
+                gfo.remove(features_prepr_path)
                 raise Exception(message) from ex
-            finally:    
+            finally:
                 # Remove lock file as everything is ready for other processes to use it
-                if features_prepr_filepath_busy.exists():
-                    features_prepr_filepath_busy.unlink()  
+                features_prepr_path_busy.unlink()
 
             # Now filter the parcels that are in bbox provided
             if bbox is not None:
@@ -660,44 +830,41 @@ def load_features_file(
                 features_gdf = features_gdf.cx[xmin:xmax, ymin:ymax]
                 logger.info(f"Found {len(features_gdf.index)} features in bbox")
 
-                """ Slower + crashes?
-                logger.info(f"Filter only features in the bbox of image with spatial index {image_bounds}")
-                spatial_index = features_gdf.sindex
-                possible_matches_index = list(spatial_index.intersection(image_bounds))
-                features_gdf = features_gdf.iloc[possible_matches_index]
-                #features_gdf = features_gdf[features_gdf.intersects(image_shape)]
-                logger.info(f"Found {len(features_gdf.index)} features in the bbox of image {image_bounds}")                    
-                """
-
-    # If there exists already a file with the features in the right projection, we can just read the data
+    # If there exists already a file with the features in the right projection, we can
+    # just read the data
     if features_gdf is None:
 
-        # If a "busy file" still exists, the file isn't ready yet, but another process is working on it, so wait till it disappears
+        # If a "busy file" still exists, the file isn't ready yet, but another process
+        # is working on it, so wait till it disappears
         wait_secs_max = 600
         wait_start_time = datetime.now()
-        while features_prepr_filepath_busy.exists():
+        while features_prepr_path_busy.exists():
             time.sleep(1)
             wait_secs = (datetime.now() - wait_start_time).total_seconds()
             if wait_secs > wait_secs_max:
                 raise Exception(
                     f"Waited {wait_secs} for busy file "
-                    f"{features_prepr_filepath_busy} and it is still there!"
+                    f"{features_prepr_path_busy} and it is still there!"
                 )
 
-        logger.info(f"Read {features_prepr_filepath}")
+        logger.info(f"Read {features_prepr_path}")
         start_time = datetime.now()
-        features_gdf = gfo.read_file(features_prepr_filepath, bbox=bbox)
-        logger.info(f"Read ready, found {len(features_gdf.index)} features, crs: {features_gdf.crs}, took {(datetime.now()-start_time).total_seconds()} s")
-
-        # Order features on x_ref to (probably) have more clustering of features in further action... 
-        if 'x_ref' not in features_gdf.columns:
-            features_gdf['x_ref'] = features_gdf.geometry.bounds.minx
-        features_gdf.sort_values(by=['x_ref'], inplace=True)
+        features_gdf = gfo.read_file(features_prepr_path, bbox=bbox)
+        logger.info(
+            f"Read ready, found {len(features_gdf.index)} features, crs: "
+            f"{features_gdf.crs}, took {(datetime.now()-start_time).total_seconds()} s"
+        )
+
+        # Order features on x_ref to (probably) have more clustering of features in
+        # further action...
+        if "x_ref" not in features_gdf.columns:
+            features_gdf["x_ref"] = features_gdf.geometry.bounds.minx
+        features_gdf.sort_values(by=["x_ref"], inplace=True)
         features_gdf.reset_index(inplace=True)
 
         # To be sure, remove the columns anyway...
         for column in features_gdf.columns:
-            if column not in columns_to_retain and column not in ['geometry']:
+            if column not in columns_to_retain and column not in ["geometry"]:
                 features_gdf.drop(columns=column, inplace=True)
 
     # If there is a polygon provided, filter on the polygon (as well)
@@ -705,67 +872,89 @@ def load_features_file(
         logger.info("Filter polygon provided, start filter")
         polygon_gdf = gpd.GeoDataFrame(geometry=[polygon], crs="EPSG:4326", index=[0])
         logger.debug(f"polygon_gdf: {polygon_gdf}")
-        logger.debug(f"polygon_gdf.crs: {polygon_gdf.crs}, features_gdf.crs: {features_gdf.crs}")
+        logger.debug(
+            f"polygon_gdf.crs: {polygon_gdf.crs}, features_gdf.crs: {features_gdf.crs}"
+        )
         polygon_gdf = polygon_gdf.to_crs(features_gdf.crs)
         logger.debug(f"polygon_gdf, after reproj: {polygon_gdf}")
-        logger.debug(f"polygon_gdf.crs: {polygon_gdf.crs}, features_gdf.crs: {features_gdf.crs}")
-        features_gdf = gpd.sjoin(features_gdf, polygon_gdf, how='inner', predicate='within')
-        
+        logger.debug(
+            f"polygon_gdf.crs: {polygon_gdf.crs}, features_gdf.crs: {features_gdf.crs}"
+        )
+        features_gdf = gpd.sjoin(
+            features_gdf, polygon_gdf, how="inner", predicate="within"
+        )
+
         # Drop column added by sjoin
-        features_gdf.drop(columns='index_right', inplace=True)
-        '''
+        features_gdf.drop(columns="index_right", inplace=True)
+        """
         spatial_index = gdf.sindex
         possible_matches_index = list(spatial_index.intersection(polygon.bounds))
         possible_matches = gdf.iloc[possible_matches_index]
         precise_matches = possible_matches[possible_matches.intersects(polygon)]
-        '''
+        """
         logger.info(f"Filter ready, found {len(features_gdf.index)}")
-            
+
     # Ready, so return result...
-    logger.debug(f"Loaded {len(features_gdf)} to calculate on in {datetime.now()-start_time}")
+    logger.debug(
+        f"Loaded {len(features_gdf)} to calculate on in {datetime.now()-start_time}"
+    )
     return features_gdf
 
+
 def calc_stats_image_gdf(
-        features_gdf,
-        id_column: str,
-        image_path: Path,
-        bands: List[str],
-        output_base_filepath: Path,
-        log_dir: Path,
-        log_level: str,
-        future_start_time = None) -> bool:
+    features_gdf,
+    id_column: str,
+    image_path: Path,
+    bands: List[str],
+    output_base_path: Path,
+    log_dir: Path,
+    log_level: str,
+    future_start_time=None,
+) -> bool:
     """
+    Calculate stats for an image.
 
     Returns True if succesfully completed.
-    Remark: easiest it returns something, when used in a parallel way: concurrent.futures likes it better if something is returned
+    Remark: easiest it returns something, when used in a parallel way:
+        concurrent.futures likes it better if something is returned
     """
 
-    # TODO: the different bands should be possible to process in parallel as well... so this function should process only one band!
-    # When running in parallel processes, the logging needs to be write to seperate files + no console logging
+    # TODO: the different bands should be possible to process in parallel as well... so
+    # this function should process only one band!
+    # When running in parallel processes, the logging needs to be write to seperate
+    # files + no console logging
     global logger
-    logger = logging.getLogger('calc_stats_image_gdf')
+    logger = logging.getLogger("calc_stats_image_gdf")
     logger.propagate = False
-    log_filepath = log_dir / f"{datetime.now():%Y-%m-%d_%H-%M-%S}_calc_stats_image_gdf_{os.getpid()}.log"
-    fh = logging.FileHandler(filename=str(log_filepath))
+    log_path = (
+        log_dir
+        / f"{datetime.now():%Y-%m-%d_%H-%M-%S}_calc_stats_image_gdf_{os.getpid()}.log"
+    )
+    fh = logging.FileHandler(filename=str(log_path))
     fh.setLevel(log_level)
-    fh.setFormatter(logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|%(funcName)s|%(message)s'))
+    fh.setFormatter(
+        logging.Formatter("%(asctime)s|%(levelname)s|%(name)s|%(funcName)s|%(message)s")
+    )
     logger.addHandler(fh)
 
     # Log the time between scheduling the future and acually run...
     if future_start_time is not None:
-        logger.info(f"Start, {(datetime.now()-future_start_time).total_seconds()} after future was scheduled")
+        logger.info(
+            f"Start, {(datetime.now()-future_start_time).total_seconds()} after future "
+            "was scheduled"
+        )
 
     # If the features_gdf is a string, use it as file path to unpickle geodataframe...
     if isinstance(features_gdf, str):
-        features_gdf_pkl_filepath = features_gdf
-        logger.info(f"Read pickle: {features_gdf_pkl_filepath}")
-        features_gdf = pd.read_pickle(features_gdf_pkl_filepath)
+        features_gdf_pkl_path = features_gdf
+        logger.info(f"Read pickle: {features_gdf_pkl_path}")
+        features_gdf = pd.read_pickle(features_gdf_pkl_path)
         logger.info(f"Read pickle with {len(features_gdf.index)} features ready")
 
     # Init some variables
-    quality_band = 'SCL-20m'
+    quality_band = "SCL-20m"
     features_total_bounds = features_gdf.total_bounds
-    output_base_filepath_noext, output_ext = os.path.splitext(output_base_filepath)
+    output_base_path_noext, output_ext = os.path.splitext(output_base_path)
 
     # If the image has a quality band, check that one first so parcels with
     # bad pixels can be removed as the data can't be trusted anyway
@@ -778,87 +967,146 @@ def calc_stats_image_gdf(
         #   -> 2 (dark area pixels), 4 (vegetation), 5 (not_vegetated),
         #      6 (water), 7 (unclassified), 10 (thin cirrus)
         # List of classes: https://usermanual.readthedocs.io/en/latest/pages/ProductGuide.html#quality-indicator-bands
-        
+
         # Get the image data and calculate
-        logger.info(f"Calculate categorical counts for band {quality_band} on {len(features_gdf.index)} features")
-        category_map = {0.0: 'nodata', 1.0: 'saturated', 2.0: 'dark', 3.0: 'cloud_shadow',
-                        4.0: 'vegetation', 5.0: 'not_vegetated', 6.0: 'water', 7.0: 'unclassified',
-                        8.0: 'cloud_mediumproba', 9.0: 'cloud_highproba', 10: 'cloud_thincirrus', 
-                        11.0: 'snow'}
+        logger.info(
+            f"Calculate categorical counts for band {quality_band} on "
+            f"{len(features_gdf.index)} features"
+        )
+        category_map = {
+            0.0: "nodata",
+            1.0: "saturated",
+            2.0: "dark",
+            3.0: "cloud_shadow",
+            4.0: "vegetation",
+            5.0: "not_vegetated",
+            6.0: "water",
+            7.0: "unclassified",
+            8.0: "cloud_mediumproba",
+            9.0: "cloud_highproba",
+            10: "cloud_thincirrus",
+            11.0: "snow",
+        }
         # Define which columns contain good pixels and which don't
-        bad_pixels_cols = ['nodata', 'saturated', 'dark', 'snow',
-                           'cloud_mediumproba', 'cloud_highproba']
+        bad_pixels_cols = [
+            "nodata",
+            "saturated",
+            "dark",
+            "snow",
+            "cloud_mediumproba",
+            "cloud_highproba",
+        ]
         image_data = get_image_data(
-                image_path, bounds=features_total_bounds, bands=[quality_band], pixel_buffer=1)
-        # Before zonal_stats, do reset_index so index is "clean", otherwise the concat/insert/... 
-        # later on gives wrong results
+            image_path,
+            bounds=features_total_bounds,
+            bands=[quality_band],
+            pixel_buffer=1,
+        )
+        # Before zonal_stats, do reset_index so index is "clean", otherwise the
+        # concat/insert/... later on gives wrong results
         features_gdf.reset_index(drop=True, inplace=True)
-        features_stats = zonal_stats(features_gdf, image_data[quality_band]['data'], 
-                affine=image_data[quality_band]['transform'], prefix="", nodata=0, 
-                categorical=True,category_map=category_map)
+        features_stats = zonal_stats(
+            features_gdf,
+            image_data[quality_band]["data"],
+            affine=image_data[quality_band]["transform"],
+            prefix="",
+            nodata=0,
+            categorical=True,
+            category_map=category_map,
+        )
         features_stats_df = pd.DataFrame(features_stats)
         features_stats_df.fillna(value=0, inplace=True)
 
-        # Make sure the dataframe contains columns for all possible values 
+        # Make sure the dataframe contains columns for all possible values
         for i, category_key in enumerate(category_map):
             category_column = category_map[category_key]
             if category_column in features_stats_df.columns:
                 # Cast to int, otherwise is float
-                features_stats_df[category_column] = features_stats_df[category_column].astype('int32')
+                features_stats_df[category_column] = features_stats_df[
+                    category_column
+                ].astype("int32")
             else:
                 features_stats_df.insert(loc=i, column=category_column, value=0)
 
-        # Add bad pixels column 
+        # Add bad pixels column
         nb_bad_pixels_column = "nb_bad_pixels"
-        features_stats_df[nb_bad_pixels_column] = features_stats_df[bad_pixels_cols].sum(axis=1)
-        
+        features_stats_df[nb_bad_pixels_column] = features_stats_df[
+            bad_pixels_cols
+        ].sum(axis=1)
+
         # Add index and write to file
         features_stats_df.insert(loc=0, column=id_column, value=features_gdf[id_column])
-        output_band_filepath = Path(f"{output_base_filepath_noext}_{quality_band}{output_ext}")
-        logger.info(f"Write data for {len(features_stats_df.index)} parcels found to {output_band_filepath}")
-        pdh.to_file(features_stats_df, output_band_filepath, index=False, append=True)
+        output_band_path = Path(f"{output_base_path_noext}_{quality_band}{output_ext}")
+        logger.info(
+            f"Write data for {len(features_stats_df.index)} parcels found to "
+            f"{output_band_path}"
+        )
+        pdh.to_file(features_stats_df, output_band_path, index=False, append=True)
 
         # Use the nb_bad_pixels column to filter only parcels without bad pixels
-        features_gdf.insert(loc=0, column=nb_bad_pixels_column, value=features_stats_df[nb_bad_pixels_column])
+        features_gdf.insert(
+            loc=0,
+            column=nb_bad_pixels_column,
+            value=features_stats_df[nb_bad_pixels_column],
+        )
         features_gdf = features_gdf.loc[features_gdf[nb_bad_pixels_column] == 0]
         features_gdf.drop(columns=[nb_bad_pixels_column], inplace=True)
-        
+
         # Check if there are still features to be calculated
         if len(features_gdf.index) == 0:
-            logger.info(f"After checking quality band, no more features to be calculated, so stop")
+            logger.info(
+                "After checking quality band, no more features to be calculated: stop"
+            )
             return True
 
     # Loop over image bands
     features_total_bounds = features_gdf.total_bounds
     for band in bands:
         # The quality band is already treated, so skip it here
-        if band == 'SCL-20m':
+        if band == "SCL-20m":
             continue
 
         # Check if output file exists already...
-        output_band_filepath = Path(f"{output_base_filepath_noext}_{band}{output_ext}")
-        
+        output_band_path = Path(f"{output_base_path_noext}_{band}{output_ext}")
+
         # Get the image data and calculate statistics
         logger.info(f"Read band {band} for bounds {features_total_bounds}")
         image_data = get_image_data(
-                image_path, bounds=features_total_bounds, bands=[band], pixel_buffer=1)
-        
-        # Upsample the image to double resolution, so we can use pieces of pixels for small
-        # parcels without introducing big errors due to mixels
-        upsample_factor = 2
-        image_data_upsampled = (image_data[band]['data'].repeat(upsample_factor, axis=0)
-                                                        .repeat(upsample_factor, axis=1))
-        affine_upsampled = image_data[band]['transform'] * Affine.scale(1/upsample_factor)
+            image_path, bounds=features_total_bounds, bands=[band], pixel_buffer=1
+        )
 
-        logger.info(f"Calculate zonal statistics for band {band} on {len(features_gdf.index)} features")
-        # Before zonal_stats, do reset_index so index is "clean", otherwise the concat/insert/... 
-        # later on gives wrong results
+        # Upsample the image to double resolution, so we can use pieces of pixels for
+        # small parcels without introducing big errors due to mixels
+        upsample_factor = 2
+        image_data_upsampled = (
+            image_data[band]["data"]
+            .repeat(upsample_factor, axis=0)
+            .repeat(upsample_factor, axis=1)
+        )
+        affine_upsampled = image_data[band]["transform"] * Affine.scale(
+            1 / upsample_factor
+        )
+
+        logger.info(
+            f"Calculate zonal statistics for band {band} on "
+            f"{len(features_gdf.index)} features"
+        )
+        # Before zonal_stats, do reset_index so index is "clean", otherwise the
+        # concat/insert/... later on gives wrong results
         features_gdf.reset_index(drop=True, inplace=True)
-        features_stats = zonal_stats(features_gdf, image_data_upsampled, 
-                affine=affine_upsampled, prefix="", nodata=0, all_touched=False,
-                stats=['count', 'mean', 'median', 'std', 'min', 'max'])
+        features_stats = zonal_stats(
+            features_gdf,
+            image_data_upsampled,
+            affine=affine_upsampled,
+            prefix="",
+            nodata=0,
+            all_touched=False,
+            stats=["count", "mean", "median", "std", "min", "max"],
+        )
         features_stats_df = pd.DataFrame(features_stats)
-        features_stats_df['count'] = features_stats_df['count'].divide(upsample_factor*2)
+        features_stats_df["count"] = features_stats_df["count"].divide(
+            upsample_factor * 2
+        )
 
         # Add original id column to statistics dataframe
         features_stats_df.insert(loc=0, column=id_column, value=features_gdf[id_column])
@@ -866,19 +1114,26 @@ def calc_stats_image_gdf(
         # Remove rows with empty data
         features_stats_df.dropna(inplace=True)
         if len(features_stats_df.index) == 0:
-            logger.info(f"No data found for band {band}, so no use to process other bands")
+            logger.info(
+                f"No data found for band {band}, so no use to process other bands"
+            )
             return True
         features_stats_df.set_index(id_column, inplace=True)
-        logger.info(f"Write data for {len(features_stats_df.index)} parcels found to {output_band_filepath}")
-        pdh.to_file(features_stats_df, output_band_filepath, append=True)
+        logger.info(
+            f"Write data for {len(features_stats_df.index)} parcels found to "
+            f"{output_band_path}"
+        )
+        pdh.to_file(features_stats_df, output_band_path, append=True)
 
     return True
 
+
 def get_image_data(
-        image_path: Path,
-        bounds: Tuple[float, float, float, float],
-        bands: List[str],
-        pixel_buffer: int = 0) -> dict:
+    image_path: Path,
+    bounds: Tuple[float, float, float, float],
+    bands: List[str],
+    pixel_buffer: int = 0,
+) -> dict:
     """
     Reads the data from the image.
 
@@ -891,44 +1146,51 @@ def get_image_data(
     Args
         image_path: the path the image
         bounds: the bounds to be read, in coordinates in the projection of the image
-        bands: list of bands to be read, eg. "VV", "VH",... 
-        pixel_buffer: number to pixels to take as buffer around the bounds provided in pixels
+        bands: list of bands to be read, eg. "VV", "VH",...
+        pixel_buffer: number to pixels to take as buffer around the bounds provided in
+            pixels
     """
+    logger.info("get_image_data start")
     # Get info about the image
     image_info = get_image_info(image_path)
 
     # Now get the data
-    image_data = {}              # Dict for the transforms and the data per band
-    if image_info['image_type'] in ('CARD', 'SAFE'):
+    image_data = {}  # Dict for the transforms and the data per band
+    if image_info["image_type"] in ("CARD", "SAFE"):
         # Loop over bands and get data
         for band in bands:
-            band_relative_filepath = image_info['bands'][band]['relative_filepath']
-            image_band_filepath = image_path / band_relative_filepath
-            band_index = image_info['bands'][band]['bandindex']
-            logger.info(f"Read image data from {image_band_filepath}, with band_index: {band_index}")
+            band_relative_path = image_info["bands"][band]["relative_path"]
+            image_band_path = image_path / band_relative_path
+            band_index = image_info["bands"][band]["bandindex"]
+            logger.info(
+                f"Read image data from {image_band_path}, with band_index: {band_index}"
+            )
             image_data[band] = {}
-            with rasterio.open(str(image_band_filepath)) as src:
+            with rasterio.open(str(image_band_path)) as src:
                 # Determine the window we need to read from the image:
                 window_to_read = projected_bounds_to_window(
-                        bounds, src.transform, src.width, src.height, pixel_buffer) 
-                image_data[band]['transform'] = rasterio.windows.transform(window_to_read, 
-                                                                           src.transform) 
+                    bounds, src.transform, src.width, src.height, pixel_buffer
+                )
+                image_data[band]["transform"] = rasterio.windows.transform(
+                    window_to_read, src.transform
+                )
                 # Read!
                 # Remark: bandindex in rasterio is 1-based instead of 0-based -> +1
-                logger.debug(f"Read image data from {image_band_filepath}")
-                image_data[band]['data'] = src.read(band_index+1, window=window_to_read)
-
-                # If dB data should be converted to natural + the image band data is in dB
-                #if db_to_natural is True and image_info['bands'][band]['is_db'] is True:
-                #    image_data[band]['data'] = np.power(10, np.divide(image_data[band]['data'], 10))
-                logger.info(f"Image data read")
+                logger.debug(f"Read image data from {image_band_path}")
+                image_data[band]["data"] = src.read(
+                    band_index + 1, window=window_to_read
+                )
+                logger.info("Image data read")
     else:
         message = f"Format currently not supported: {image_path}"
         logger.error(message)
         raise NotImplementedError(message)
 
+    logger.info("get_image_data ready")
+
     return image_data
 
+
 def get_image_info(image_path: Path) -> dict:
 
     image_info = {}
@@ -936,199 +1198,260 @@ def get_image_info(image_path: Path) -> dict:
     # First determine general info of image
     image_path_noext, image_ext = os.path.splitext(image_path)
     _, image_basename_noext = os.path.split(image_path_noext)
-    image_basename_noext_split = image_basename_noext.split('_')
-    image_info['satellite'] = image_basename_noext_split[0]
-    
+    image_basename_noext_split = image_basename_noext.split("_")
+    image_info["satellite"] = image_basename_noext_split[0]
+
     # Specific code per image type
-    if image_ext.upper() == '.CARD':
-        
+    if image_ext.upper() == ".CARD":
+
         # This is a sentinel 1 image (GRD or coherence)
         # First extract and fill out some basic info
-        image_info['image_type'] = 'CARD'
-        image_info['image_id'] = image_basename_noext
-        
+        image_info["image_type"] = "CARD"
+        image_info["image_id"] = image_basename_noext
+
         # Read info from the metadata file
-        metadata_xml_filepath = image_path / 'metadata.xml'
-        metadata = ET.parse(str(metadata_xml_filepath))
+        metadata_xml_path = image_path / "metadata.xml"
+        metadata = ET.parse(str(metadata_xml_path))
         metadata_root = metadata.getroot()
 
-        logger.debug(f"Parse metadata info from {metadata_xml_filepath}")
+        logger.debug(f"Parse metadata info from {metadata_xml_path}")
 
-        try:            
+        try:
             # Get the filename
-            image_info['filename'] = metadata_root.find('productFilename').text
+            image_info["filename"] = metadata_root.find("productFilename").text
 
             # Get the footprint
-            image_info['footprint'] = {}
-            footprint = metadata_root.find('footprint')
+            image_info["footprint"] = {}
+            footprint = metadata_root.find("footprint")
             poly = footprint.find("{http://www.opengis.net/gml}Polygon")
-            image_info['footprint']['srsname'] = poly.attrib.get('srsName')
+            image_info["footprint"]["srsname"] = poly.attrib.get("srsName")
             linear_ring = []
-            #for coord in poly.findall("{http://www.opengis.net/gml}outerBoundaryIs/{http://www.opengis.net/gml}LinearRing/{http://www.opengis.net/gml}coord"):
+            # for coord in poly.findall("{http://www.opengis.net/gml}outerBoundaryIs/{http://www.opengis.net/gml}LinearRing/{http://www.opengis.net/gml}coord"):
             #    linear_ring.append((float(coord.findtext("{http://www.opengis.net/gml}X")),float(coord.findtext("{http://www.opengis.net/gml}Y"))))
-            coord_str = poly.find("{http://www.opengis.net/gml}outerBoundaryIs/{http://www.opengis.net/gml}LinearRing/{http://www.opengis.net/gml}coordinates")
+            coord_str = poly.find(
+                "{http://www.opengis.net/gml}outerBoundaryIs/{http://www.opengis.net/gml}LinearRing/{http://www.opengis.net/gml}coordinates"
+            )
             logger.debug(f"coord_str: {coord_str}, coord_str.text: {coord_str.text}")
-            coord_list = coord_str.text.split(' ')
+            coord_list = coord_str.text.split(" ")
             for coord in coord_list:
                 # Watch out, latitude (~y) is first, than longitude (~x)
-                # TODO: add check if the projection is in degrees (latlon) or coordinates (xy) instead of hardcoded latlon
-                y, x = coord.split(',') 
+                # TODO: add check if the projection is in degrees (latlon) or
+                # coordinates (xy) instead of hardcoded latlon
+                y, x = coord.split(",")
                 linear_ring.append((float(x), float(y)))
-            image_info['footprint']['shape'] = sh_polygon.Polygon(linear_ring)
+            image_info["footprint"]["shape"] = sh_polygon.Polygon(linear_ring)
 
             # get epsg
-            epsg = metadata_root.find('imageProjection/EPSG').text
-            image_info['image_epsg'] = float(epsg)
-            image_info['image_crs'] = 'EPSG:' + epsg
+            epsg = metadata_root.find("imageProjection/EPSG").text
+            image_info["image_epsg"] = float(epsg)
+            image_info["image_crs"] = "EPSG:" + epsg
         except Exception as ex:
-            raise Exception(f"Exception extracting info from {metadata_xml_filepath}") from ex
+            raise Exception(
+                f"Exception extracting info from {metadata_xml_path}"
+            ) from ex
 
         # Read info from the manifest.safe file
         manifest_xml_searchstring = "*_manifest.safe"
-        manifest_xml_filepaths = list(image_path.glob(manifest_xml_searchstring))
+        manifest_xml_paths = list(image_path.glob(manifest_xml_searchstring))
 
         # The number of .safe indicates whether it is a GRD or a Coherence image
-        nb_safefiles = len(manifest_xml_filepaths)
+        nb_safefiles = len(manifest_xml_paths)
         if nb_safefiles == 1:
 
             # Now parse the .safe file
-            manifest_xml_filepath = manifest_xml_filepaths[0]
+            manifest_xml_path = manifest_xml_paths[0]
 
             try:
-                manifest = ET.parse(str(manifest_xml_filepath))
+                manifest = ET.parse(str(manifest_xml_path))
                 manifest_root = manifest.getroot()
 
                 # Define namespaces...
-                ns = {'safe': "http://www.esa.int/safe/sentinel-1.0",
-                      's1': "http://www.esa.int/safe/sentinel-1.0/sentinel-1",
-                      's1sarl1': "http://www.esa.int/safe/sentinel-1.0/sentinel-1/sar/level-1"}
-
-                logger.debug(f"Parse manifest info from {metadata_xml_filepath}")
-                image_info['transmitter_receiver_polarisation'] = []
-                for polarisation in manifest_root.findall("metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:transmitterReceiverPolarisation", ns):
-                    image_info['transmitter_receiver_polarisation'].append(polarisation.text)
-                image_info['productTimelinessCategory'] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:productTimelinessCategory", ns).text
+                ns = {
+                    "safe": "http://www.esa.int/safe/sentinel-1.0",
+                    "s1": "http://www.esa.int/safe/sentinel-1.0/sentinel-1",
+                    "s1sarl1": "http://www.esa.int/safe/sentinel-1.0/sentinel-1/sar/level-1",
+                }
 
-                image_info['instrument_mode'] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/safe:platform/safe:instrument/safe:extension/s1sarl1:instrumentMode/s1sarl1:mode", ns).text
-                image_info['orbit_properties_pass'] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/safe:orbitReference/safe:extension/s1:orbitProperties/s1:pass", ns).text
-                image_info['acquisition_date'] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/safe:acquisitionPeriod/safe:startTime", ns).text
+                logger.debug(f"Parse manifest info from {metadata_xml_path}")
+                image_info["transmitter_receiver_polarisation"] = []
+                for polarisation in manifest_root.findall(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:transmitterReceiverPolarisation",
+                    ns,
+                ):
+                    image_info["transmitter_receiver_polarisation"].append(
+                        polarisation.text
+                    )
+                image_info["productTimelinessCategory"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:productTimelinessCategory",
+                    ns,
+                ).text
+
+                image_info["instrument_mode"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/safe:platform/safe:instrument/safe:extension/s1sarl1:instrumentMode/s1sarl1:mode",
+                    ns,
+                ).text
+                image_info["orbit_properties_pass"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/safe:orbitReference/safe:extension/s1:orbitProperties/s1:pass",
+                    ns,
+                ).text
+                image_info["acquisition_date"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/safe:acquisitionPeriod/safe:startTime",
+                    ns,
+                ).text
 
                 # Now have a look in the files themselves to get band info,...
                 # TODO: probably cleaner/easier to read from metadata files...
-                image_basename_noext_nodate = image_basename_noext[:image_basename_noext.rfind('_', 0)]                               
+                image_basename_noext_nodate = image_basename_noext[
+                    : image_basename_noext.rfind("_", 0)
+                ]
                 image_datadirname = f"{image_basename_noext_nodate}.data"
                 image_datadir = image_path / image_datadirname
-                band_filepaths = list(image_datadir.glob("*.img"))
+                band_paths = list(image_datadir.glob("*.img"))
 
                 # If no files were found, check if the images are in .tif format!
-                if len(band_filepaths) == 0:
+                if len(band_paths) == 0:
                     tif_path = image_path / "Gamma0_VH.tif"
                     if tif_path.exists():
-                        band_filepaths.append(tif_path)
+                        band_paths.append(tif_path)
                     tif_path = image_path / "Gamma0_VV.tif"
                     if tif_path.exists():
-                        band_filepaths.append(tif_path)
+                        band_paths.append(tif_path)
 
                 # If no files were found, error
-                if len(band_filepaths) == 0:
-                    message = f"No image files found in {image_datadir} or {image_path}!"
+                if len(band_paths) == 0:
+                    message = (
+                        f"No image files found in {image_datadir} or {image_path}!"
+                    )
                     logger.error(message)
                     raise Exception(message)
 
-                image_info['bands'] = {}
-                for i, band_filepath in enumerate(band_filepaths):
-                    # Extract bound,... info from the first file only (they are all the same)
+                image_info["bands"] = {}
+                for i, band_path in enumerate(band_paths):
+                    # Extract bound,... info from the first file only
+                    # (they are all the same)
                     if i == 0:
-                        #logger.debug(f"Read image metadata from {band_filepath}")
-                        with rasterio.open(str(band_filepath)) as src:
-                            image_info['image_bounds'] = src.bounds
-                            image_info['image_affine'] = src.transform
+                        # logger.debug(f"Read image metadata from {band_path}")
+                        with rasterio.open(str(band_path)) as src:
+                            image_info["image_bounds"] = src.bounds
+                            image_info["image_affine"] = src.transform
                             if src.crs is not None:
-                                image_info['image_crs'] = src.crs.to_string()
-                                image_info['image_epsg'] = src.crs.to_epsg()
-                        #logger.debug(f"Image metadata read: {image_info}")
-                    if band_filepath.stem == 'Gamma0_VH':
-                        band = 'VH'
-                    elif band_filepath.stem == 'Gamma0_VV':
-                        band = 'VV'
+                                image_info["image_crs"] = src.crs.to_string()
+                                image_info["image_epsg"] = src.crs.to_epsg()
+                        # logger.debug(f"Image metadata read: {image_info}")
+                    if band_path.stem == "Gamma0_VH":
+                        band = "VH"
+                    elif band_path.stem == "Gamma0_VV":
+                        band = "VV"
                     else:
-                        raise NotImplementedError(f"Filename not supported: {band_filepath}")
+                        raise NotImplementedError(
+                            f"Filename not supported: {band_path}"
+                        )
 
                     # Add specific info about the band
-                    image_info['bands'][band] = {}
-                    image_info['bands'][band]['filepath'] = band_filepath
-                    image_info['bands'][band]['relative_filepath'] = str(band_filepath).replace(str(image_path) + os.pathsep, "")
-                    image_info['bands'][band]['filename'] = band_filepath.name
-                    image_info['bands'][band]['bandindex'] = 0
+                    image_info["bands"][band] = {}
+                    image_info["bands"][band]["path"] = band_path
+                    image_info["bands"][band]["relative_path"] = str(band_path).replace(
+                        str(image_path) + os.pathsep, ""
+                    )
+                    image_info["bands"][band]["filename"] = band_path.name
+                    image_info["bands"][band]["bandindex"] = 0
 
             except Exception as ex:
-                raise Exception(f"Exception extracting info from {manifest_xml_filepath}") from ex
+                raise Exception(
+                    f"Exception extracting info from {manifest_xml_path}"
+                ) from ex
 
         elif nb_safefiles == 2 or nb_safefiles == 3:
             # 2 safe files -> coherence
             # Now parse the first .safe file
             # TODO: maybe check if the info in all safe files  are the same or?.?
-            manifest_xml_filepath = manifest_xml_filepaths[0]
+            manifest_xml_path = manifest_xml_paths[0]
 
             try:
-                manifest = ET.parse(str(manifest_xml_filepath))
+                manifest = ET.parse(str(manifest_xml_path))
                 manifest_root = manifest.getroot()
 
                 # Define namespaces...
-                ns = {"safe": "http://www.esa.int/safe/sentinel-1.0",
+                ns = {
+                    "safe": "http://www.esa.int/safe/sentinel-1.0",
                     "s1": "http://www.esa.int/safe/sentinel-1.0/sentinel-1",
-                    "s1sarl1": "http://www.esa.int/safe/sentinel-1.0/sentinel-1/sar/level-1"}
+                    "s1sarl1": "http://www.esa.int/safe/sentinel-1.0/sentinel-1/sar/level-1",
+                }
 
-                #logger.debug(f"Parse manifest info from {metadata_xml_filepath}")
+                # logger.debug(f"Parse manifest info from {metadata_xml_path}")
                 image_info["transmitter_receiver_polarisation"] = []
-                for polarisation in manifest_root.findall("metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:transmitterReceiverPolarisation", ns):
-                    image_info["transmitter_receiver_polarisation"].append(polarisation.text)
-                image_info['productTimelinessCategory'] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:productTimelinessCategory", ns).text
-                image_info["instrument_mode"] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/safe:platform/safe:instrument/safe:extension/s1sarl1:instrumentMode/s1sarl1:mode", ns).text
-                image_info["orbit_properties_pass"] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/safe:orbitReference/safe:extension/s1:orbitProperties/s1:pass", ns).text
-                image_info['acquisition_date'] = manifest_root.find("metadataSection/metadataObject/metadataWrap/xmlData/safe:acquisitionPeriod/safe:startTime", ns).text
+                for polarisation in manifest_root.findall(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:transmitterReceiverPolarisation",
+                    ns,
+                ):
+                    image_info["transmitter_receiver_polarisation"].append(
+                        polarisation.text
+                    )
+                image_info["productTimelinessCategory"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/s1sarl1:standAloneProductInformation/s1sarl1:productTimelinessCategory",
+                    ns,
+                ).text
+                image_info["instrument_mode"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/safe:platform/safe:instrument/safe:extension/s1sarl1:instrumentMode/s1sarl1:mode",
+                    ns,
+                ).text
+                image_info["orbit_properties_pass"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/safe:orbitReference/safe:extension/s1:orbitProperties/s1:pass",
+                    ns,
+                ).text
+                image_info["acquisition_date"] = manifest_root.find(
+                    "metadataSection/metadataObject/metadataWrap/xmlData/safe:acquisitionPeriod/safe:startTime",
+                    ns,
+                ).text
 
                 # Now have a look in the files themselves to get band info,...
                 # TODO: probably cleaner/easier to read from metadata files...
                 # For coherence filename, remove extra .LCO1 extension + date
-                #image_datafilebasename, _ = os.path.splitext(image_basename_noext)
-                #image_datafilebasename = image_datafilebasename[:image_datafilebasename.rindex('_')]
-                
-                metadata = ET.parse(str(image_path / 'metadata.xml'))
+                # image_datafilebasename, _ = os.path.splitext(image_basename_noext)
+                # image_datafilebasename = image_datafilebasename[:image_datafilebasename.rindex('_')]
+
+                metadata = ET.parse(str(image_path / "metadata.xml"))
                 metadata_root = metadata.getroot()
 
-                image_basename_noext_nodate = image_basename_noext[:image_basename_noext.rfind('_', 0)]
+                image_basename_noext_nodate = image_basename_noext[
+                    : image_basename_noext.rfind("_", 0)
+                ]
                 image_datafilename = f"{image_basename_noext_nodate}_byte.tif"
-                image_datafilepath = image_path / image_datafilename
-                
-                #logger.debug(f"Read image metadata from {image_datafilepath}")            
-                with rasterio.open(str(image_datafilepath)) as src:
-                    image_info['image_bounds'] = src.bounds
-                    image_info['image_affine'] = src.transform
+                image_datapath = image_path / image_datafilename
+
+                # logger.debug(f"Read image metadata from {image_datapath}")
+                with rasterio.open(str(image_datapath)) as src:
+                    image_info["image_bounds"] = src.bounds
+                    image_info["image_affine"] = src.transform
                     if src.crs is not None:
-                        image_info['image_crs'] = src.crs.to_string()
-                        image_info['image_epsg'] = src.crs.to_epsg()
+                        image_info["image_crs"] = src.crs.to_string()
+                        image_info["image_epsg"] = src.crs.to_epsg()
 
                 # Add specific info about the bands
                 image_info["bands"] = {}
-                band = 'VH'
-                image_info['bands'][band] = {}
-                image_info['bands'][band]['filepath'] = image_datafilepath
-                image_info['bands'][band]['relative_filepath'] = image_datafilename
-                image_info['bands'][band]['filename'] = image_datafilename
-                image_info['bands'][band]['bandindex'] = 0
-
-                band = 'VV'
-                image_info['bands'][band] = {}
-                image_info['bands'][band]['filepath'] = image_datafilepath
-                image_info['bands'][band]['relative_filepath'] = image_datafilename 
-                image_info['bands'][band]['filename'] = image_datafilename
-                image_info['bands'][band]['bandindex'] = 1
+                band = "VH"
+                image_info["bands"][band] = {}
+                image_info["bands"][band]["path"] = image_datapath
+                image_info["bands"][band]["relative_path"] = image_datafilename
+                image_info["bands"][band]["filename"] = image_datafilename
+                image_info["bands"][band]["bandindex"] = 0
+
+                band = "VV"
+                image_info["bands"][band] = {}
+                image_info["bands"][band]["path"] = image_datapath
+                image_info["bands"][band]["relative_path"] = image_datafilename
+                image_info["bands"][band]["filename"] = image_datafilename
+                image_info["bands"][band]["bandindex"] = 1
 
             except Exception as ex:
-                raise Exception(f"Exception extracting info from {manifest_xml_filepath}") from ex
+                raise Exception(
+                    f"Exception extracting info from {manifest_xml_path}"
+                ) from ex
         else:
-            message = f"Error: found {nb_safefiles} .safe files doing glob in {image_path} with {manifest_xml_searchstring}"
+            message = (
+                f"Error: found {nb_safefiles} .safe files doing glob in {image_path} "
+                f"with {manifest_xml_searchstring}"
+            )
             logger.error(message)
             raise Exception(message)
 
@@ -1141,63 +1464,79 @@ def get_image_info(image_path: Path) -> dict:
         image_info["image_id"] = image_basename_noext
 
         # Read info from the manifest.safe file
-        '''
+        """
         manifest_xml_searchstring = os.path.join(image_path, f"*_manifest.safe")
-        manifest_xml_filepaths = glob.glob(manifest_xml_searchstring)
+        manifest_xml_paths = glob.glob(manifest_xml_searchstring)
 
         # We should only find one .safe file (at the moment)
-        nb_safefiles = len(manifest_xml_filepaths)
+        nb_safefiles = len(manifest_xml_paths)
         if nb_safefiles == 1:
 
         # Now parse the metadata xml file
-        manifest_xml_filepath = manifest_xml_filepaths[0]
-        '''
-        metadata_xml_filepath = image_path / "MTD_MSIL2A.xml"
+        manifest_xml_path = manifest_xml_paths[0]
+        """
+        metadata_xml_path = image_path / "MTD_MSIL2A.xml"
         try:
-            metadata = ET.parse(str(metadata_xml_filepath))
+            metadata = ET.parse(str(metadata_xml_path))
             metadata_root = metadata.getroot()
 
             # Define namespaces...
-            #xsi:schemaLocation="https://psd-14.sentinel2.eo.esa.int/PSD/User_Product_Level-2A.xsd"
-            ns = {'n1': "https://psd-14.sentinel2.eo.esa.int/PSD/User_Product_Level-2A.xsd",
-                  'xsi': "http://www.w3.org/2001/XMLSchema-instance"}
-
-            #logger.debug(f"Parse metadata info from {metadata_xml_filepath}")
-            image_info['Cloud_Coverage_Assessment'] = float(metadata_root.find("n1:Quality_Indicators_Info/Cloud_Coverage_Assessment", ns).text)
-            image_info['acquisition_date'] = metadata_root.find("n1:General_Info/Product_Info/PRODUCT_START_TIME", ns).text
-            
+            # xsi:schemaLocation="https://psd-14.sentinel2.eo.esa.int/PSD/User_Product_Level-2A.xsd"
+            ns = {
+                "n1": "https://psd-14.sentinel2.eo.esa.int/PSD/User_Product_Level-2A.xsd",
+                "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+            }
+
+            # logger.debug(f"Parse metadata info from {metadata_xml_path}")
+            image_info["Cloud_Coverage_Assessment"] = float(
+                metadata_root.find(
+                    "n1:Quality_Indicators_Info/Cloud_Coverage_Assessment", ns
+                ).text
+            )
+            image_info["acquisition_date"] = metadata_root.find(
+                "n1:General_Info/Product_Info/PRODUCT_START_TIME", ns
+            ).text
+
         except Exception as ex:
-            raise Exception(f"Exception extracting info from {metadata_xml_filepath}") from ex
-            
+            raise Exception(
+                f"Exception extracting info from {metadata_xml_path}"
+            ) from ex
+
         # Now have a look in the files themselves to get band info,...
         image_datadir = image_path / "GRANULE"
-        band_filepaths = list(image_datadir.rglob(f"*.jp2"))
+        band_paths = list(image_datadir.rglob("*.jp2"))
 
-        metadata_filepath = list(image_datadir.rglob("MTD_TL.xml"))[0]
+        metadata_path = list(image_datadir.rglob("MTD_TL.xml"))[0]
         try:
             # read epsg
-            metadata = ET.parse(str(metadata_filepath))
+            metadata = ET.parse(str(metadata_path))
             metadata_root = metadata.getroot()
 
-            ns = {'n1': "https://psd-14.sentinel2.eo.esa.int/PSD/S2_PDI_Level-2A_Tile_Metadata.xsd",
-                  'xsi': "http://www.w3.org/2001/XMLSchema-instance"}
-
-            image_info['image_crs'] = metadata_root.find('n1:Geometric_Info/Tile_Geocoding/HORIZONTAL_CS_CODE', ns).text
-            image_info['image_epsg'] = float(image_info['image_crs'].replace('EPSG:', ''))
+            ns = {
+                "n1": "https://psd-14.sentinel2.eo.esa.int/PSD/S2_PDI_Level-2A_Tile_Metadata.xsd",
+                "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+            }
+
+            image_info["image_crs"] = metadata_root.find(
+                "n1:Geometric_Info/Tile_Geocoding/HORIZONTAL_CS_CODE", ns
+            ).text
+            image_info["image_epsg"] = float(
+                image_info["image_crs"].replace("EPSG:", "")
+            )
         except Exception as ex:
-            raise Exception(f"Exception extracting info from {metadata_filepath}") from ex
+            raise Exception(f"Exception extracting info from {metadata_path}") from ex
 
         # If no files were found, error!
-        if len(band_filepaths) == 0:
+        if len(band_paths) == 0:
             message = f"No image files found in {image_datadir}!"
             logger.error(message)
             raise Exception(message)
 
         image_info["bands"] = {}
-        for i, band_filepath in enumerate(band_filepaths):
-            band_filename = os.path.basename(band_filepath)
+        for i, band_path in enumerate(band_paths):
+            band_filename = os.path.basename(band_path)
             band_filename_noext, _ = os.path.splitext(band_filename)
-            band_filename_noext_split = band_filename_noext.split('_')
+            band_filename_noext_split = band_filename_noext.split("_")
 
             if len(band_filename_noext_split) == 4:
                 # IMG_DATA files
@@ -1209,82 +1548,96 @@ def get_image_info(image_path: Path) -> dict:
                 # QI_DATA files
                 band = band_filename_noext
             else:
-                message = f"Filename of band doesn't have supported format: {band_filepath}"
+                message = f"Filename of band doesn't have supported format: {band_path}"
                 logger.error(message)
                 raise Exception(message)
 
             # Add specific info about the band
-            image_info['bands'][band] = {}
-            image_info['bands'][band]['filepath'] = band_filepath
-            image_info['bands'][band]['relative_filepath'] = str(band_filepath).replace(str(image_path), '')[1:]
-            image_info['bands'][band]['filename'] = band_filename
-            image_info['bands'][band]['bandindex'] = 0
-
-            # Extract bound,... info 
-            logger.debug(f"Read image metadata from {band_filepath}")
-            with rasterio.open(str(band_filepath)) as src:
-                image_info['bands'][band]['bounds'] = src.bounds
-                image_info['bands'][band]['affine'] = src.transform                
+            image_info["bands"][band] = {}
+            image_info["bands"][band]["path"] = band_path
+            image_info["bands"][band]["relative_path"] = str(band_path).replace(
+                str(image_path), ""
+            )[1:]
+            image_info["bands"][band]["filename"] = band_filename
+            image_info["bands"][band]["bandindex"] = 0
+
+            # Extract bound,... info
+            logger.debug(f"Read image metadata from {band_path}")
+            with rasterio.open(str(band_path)) as src:
+                image_info["bands"][band]["bounds"] = src.bounds
+                image_info["bands"][band]["affine"] = src.transform
                 if src.crs is None:
-                    image_info['bands'][band]['crs'] = image_info['image_crs']
-                    image_info['bands'][band]['epsg'] = image_info['image_epsg']
+                    image_info["bands"][band]["crs"] = image_info["image_crs"]
+                    image_info["bands"][band]["epsg"] = image_info["image_epsg"]
                 else:
-                    image_info['bands'][band]['crs'] = src.crs.to_string()
-                    image_info['bands'][band]['epsg'] = src.crs.to_epsg()
-                
-            # Store the crs also on image level, and check if all bands have the same crs 
+                    image_info["bands"][band]["crs"] = src.crs.to_string()
+                    image_info["bands"][band]["epsg"] = src.crs.to_epsg()
+
+            # Store the crs also on image level + check if all bands have the same crs
             if i == 0:
-                image_info['image_bounds'] = image_info['bands'][band]['bounds']
-                #image_info['image_crs'] = image_info['bands'][band]['crs']
-                #image_info['image_epsg'] = image_info['bands'][band]['epsg']
+                image_info["image_bounds"] = image_info["bands"][band]["bounds"]
+                # image_info['image_crs'] = image_info['bands'][band]['crs']
+                # image_info['image_epsg'] = image_info['bands'][band]['epsg']
             else:
-                if(image_info['image_crs'] != image_info['bands'][band]['crs'] 
-                    or image_info['image_epsg'] != image_info['bands'][band]['epsg']):                       
+                if (
+                    image_info["image_crs"] != image_info["bands"][band]["crs"]
+                    or image_info["image_epsg"] != image_info["bands"][band]["epsg"]
+                ):
                     message = f"Not all bands have the same crs for {image_info}"
                     logger.error(message)
                     raise Exception(message)
-                
+
         logger.debug(f"Image metadata read: {image_info}")
 
     else:
         message = f"Not a supported image format: {image_path}"
         logger.error(message)
         raise NotImplementedError(message)
-    
+
     return image_info
 
+
 def projected_bounds_to_window(
-        projected_bounds,
-        image_transform, 
-        image_pixel_width: int,
-        image_pixel_height: int,
-        pixel_buffer: int = 0):
+    projected_bounds,
+    image_transform,
+    image_pixel_width: int,
+    image_pixel_height: int,
+    pixel_buffer: int = 0,
+):
     """
-    Returns a rasterio.windows.Window to be used in rasterio to read the part of the image specified.
+    Returns a rasterio.windows.Window to be used in rasterio to read the part of the
+    image specified.
 
     Args
         projected_bounds: bounds to created the window from, in projected coordinates
-        image_transform: Affine transform of the image you want to create the pixel window for
-        image_pixel_width: total width of the image you want to create the pixel window for, in pixels
-        image_pixel_height: total height of the image you want to create the pixel window for, in pixels
-        pixel_buffer: number to pixels to take as buffer around the bounds provided in pixels
+        image_transform: Affine transform of the image you want to create the pixel
+            window for
+        image_pixel_width: total width of the image you want to create the pixel window
+            for, in pixels
+        image_pixel_height: total height of the image you want to create the pixel
+            window for, in pixels
+        pixel_buffer: number to pixels to take as buffer around the bounds provided in
+            pixels
     """
-    # Take bounds of the features + convert to image pixels  
+    # Take bounds of the features + convert to image pixels
     xmin, ymin, xmax, ymax = projected_bounds
-    window_to_read_raw = rasterio.windows.from_bounds(xmin, ymin, xmax, ymax, image_transform)
-    
+    window_to_read_raw = rasterio.windows.from_bounds(
+        xmin, ymin, xmax, ymax, image_transform
+    )
+
     # Round so it only increases window size
-    window_to_read = window_to_read_raw.round_offsets('floor').round_lengths('ceil')
-    
-    # Now some general math on window properties, but as they are readonly properties, work on copy
+    window_to_read = window_to_read_raw.round_offsets("floor").round_lengths("ceil")
+
+    # Now some general math on window properties, but as they are readonly properties,
+    # work on copy
     col_off, row_off, width, height = window_to_read.flatten()
     # Add buffer of 1 pixel extra around
     col_off -= pixel_buffer
     row_off -= pixel_buffer
-    width += 2*pixel_buffer
-    height += 2*pixel_buffer
-    
-    # Make sure the offsets aren't negative, as the pixels that are 'read' there acually  
+    width += 2 * pixel_buffer
+    height += 2 * pixel_buffer
+
+    # Make sure the offsets aren't negative, as the pixels that are 'read' there acually
     # get some value instead of eg. nan...!
     if col_off < 0:
         width -= abs(col_off)
@@ -1292,13 +1645,13 @@ def projected_bounds_to_window(
     if row_off < 0:
         height -= abs(row_off)
         row_off = 0
-    
-    # Make sure there won't be extra pixels to the top and right that will be read 
+
+    # Make sure there won't be extra pixels to the top and right that will be read
     if (col_off + width) > image_pixel_width:
         width = image_pixel_width - col_off
     if (row_off + height) > image_pixel_height:
         height = image_pixel_height - row_off
-    
+
     # Ready... prepare to return...
     window_to_read = rasterio.windows.Window(col_off, row_off, width, height)
 
@@ -1306,22 +1659,24 @@ def projected_bounds_to_window(
     """
     bounds_to_read = rasterio.windows.bounds(window_to_read, image_transform)
     logger.debug(f"projected_bounds: {projected_bounds}, "
-                + f"window_to_read_raw: {window_to_read_raw}, window_to_read: {window_to_read}, " 
+                + f"window_to_read_raw: {window_to_read_raw}, window_to_read: {window_to_read}, "
                 + f"image_pixel_width: {image_pixel_width}, image_pixel_height: {image_pixel_height}, "
                 + f"file transform: {image_transform}, bounds_to_read: {bounds_to_read}")
     """
 
     return window_to_read
 
+
 def create_file_atomic(path: Path):
     """
     Create a lock file in an atomic way, so it is threadsafe.
 
-    Returns True if the file was created by this thread, False if the file existed already.
+    Returns True if the file was created by this thread, False if the file existed
+    already.
     """
     fd = None
     try:
-        fd = os.open(path,  os.O_CREAT | os.O_EXCL)
+        fd = os.open(path, os.O_CREAT | os.O_EXCL)
         return True
     except FileExistsError:
         return False
@@ -1330,9 +1685,7 @@ def create_file_atomic(path: Path):
             os.close(fd)
 
 
-def prepare_image(
-        image_path: Path,
-        temp_dir: Path) -> Path:
+def prepare_image(image_path: Path, temp_dir: Path) -> Path:
     """
     Prepares the input image for usages.
 
@@ -1341,10 +1694,10 @@ def prepare_image(
     Returns the path to the prepared file/directory.
     """
 
-    # If the input path is not a zip file, don't make local copy and just return image path
+    # If the input path is not a zip file, don't make local copy + just return path
     if image_path.suffix.lower() != ".zip":
         return image_path
-    else:    
+    else:
         # It is a zip file, so it needs to be unzipped first...
         # Create destination file path
         image_basename_withzipext = os.path.basename(image_path)
@@ -1352,37 +1705,43 @@ def prepare_image(
         image_unzipped_path = temp_dir / image_basename
 
         image_unzipped_path_busy = Path(f"{image_unzipped_path}_busy")
-        # If the input is a zip file, unzip file to temp local location if it doesn't exist yet
-        # If the file doesn't exist yet in right projection, read original input file to reproject/write to new file with correct epsg
-        if not (image_unzipped_path_busy.exists() 
-            or image_unzipped_path.exists()):
+        # If the input is a zip file, unzip file to temp local location if it doesn't
+        # exist yet. If the file doesn't exist yet in right projection, read original
+        # input file to reproject/write to new file with correct epsg
+        if not (image_unzipped_path_busy.exists() or image_unzipped_path.exists()):
 
             # Create temp dir if it doesn't exist yet
             os.makedirs(temp_dir, exist_ok=True)
 
-            # Create lock file in an atomic way, so we are sure we are the only process working on it. 
-            # If function returns true, there isn't any other thread/process already working on it
+            # Create lock file in an atomic way, so we are sure we are the only process
+            # working on it. If function returns true, there isn't any other
+            # thread/process already working on it
             if create_file_atomic(image_unzipped_path_busy):
 
                 try:
-                    logger.info(f"Unzip image {image_path} to local location {temp_dir}")
+                    logger.info(
+                        f"Unzip image {image_path} to local location {temp_dir}"
+                    )
 
                     # Create the dest dir where the file will be unzipped to + unzip!
                     if not image_unzipped_path.exists():
                         import zipfile
-                        with zipfile.ZipFile(image_path,"r") as zippedfile:
+
+                        with zipfile.ZipFile(image_path, "r") as zippedfile:
                             zippedfile.extractall(temp_dir)
-                finally:    
+                finally:
                     # Remove lock file when we are ready
                     os.remove(image_unzipped_path_busy)
 
-        # If a "busy file" still exists, the file isn't ready yet, but another process is working on it, so wait till it disappears
+        # If a "busy file" still exists, the file isn't ready yet, but another process
+        # is working on it, so wait till it disappears
         while image_unzipped_path_busy.exists():
             time.sleep(1)
-        
+
         # Now we are ready to return the path...
         return image_unzipped_path
 
-if __name__ == '__main__':
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+
+if __name__ == "__main__":
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/preprocess/timeseries_calc_gee.py b/cropclassification/preprocess/timeseries_calc_gee.py
index 64b7e531..64a88f70 100644
--- a/cropclassification/preprocess/timeseries_calc_gee.py
+++ b/cropclassification/preprocess/timeseries_calc_gee.py
@@ -73,50 +73,57 @@
 from cropclassification.helpers import geofile
 from cropclassification.helpers import pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 global_gee_tasks_cache = None
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # The real work
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 
-def calc_timeseries_data(input_parcel_filepath: Path,
-                         input_country_code: str,
-                         start_date_str: str,
-                         end_date_str: str,
-                         sensordata_to_get: List[str],
-                         base_filename: str,
-                         dest_data_dir: Path):
-    """ Calculate timeseries data for the input parcels
+
+def calc_timeseries_data(
+    input_parcel_path: Path,
+    input_country_code: str,
+    start_date_str: str,
+    end_date_str: str,
+    sensordata_to_get: List[str],
+    base_filename: str,
+    dest_data_dir: Path,
+):
+    """Calculate timeseries data for the input parcels
 
     args
-        data_to_get: an array with data you want to be calculated: check out the constants starting
-                     with DATA_TO_GET... for the options.
+        data_to_get: an array with data you want to be calculated: check out the
+            constants starting with DATA_TO_GET... for the options.
     """
-    ##### Check and init some stuff #####
+    # Check and init some stuff
     if sensordata_to_get is None:
         raise Exception("sensordata_to_get cannot be None")
     if not dest_data_dir.exists():
         os.mkdir(dest_data_dir)
 
     # To have a good precision, the vector input must be uploaded to gee in WGS84!
-    input_preprocessed_dir = conf.dirs.getpath('input_preprocessed_dir')
-    input_parcel_4326_filepath = input_preprocessed_dir / f"{input_parcel_filepath.stem}_4326.shp"
+    input_preprocessed_dir = conf.dirs.getpath("input_preprocessed_dir")
+    input_parcel_4326_path = (
+        input_preprocessed_dir / f"{input_parcel_path.stem}_4326.shp"
+    )
 
     # If the WGS84 version doesn't exist yet, create it
-    if(not os.path.exists(input_parcel_4326_filepath)):
-        input_parcel_gdf = geofile.read_file(input_parcel_filepath)
+    if not os.path.exists(input_parcel_4326_path):
+        input_parcel_gdf = geofile.read_file(input_parcel_path)
         target_epsg = 4326
-        logger.info(f"Reproject features from {input_parcel_gdf.crs} to epsg:{target_epsg}")
+        logger.info(
+            f"Reproject features from {input_parcel_gdf.crs} to epsg:{target_epsg}"
+        )
         input_parcel_4326_gdf = input_parcel_gdf.to_crs(epsg=target_epsg)
-        logger.info(f"Write reprojected features to {input_parcel_4326_filepath}")
-        geofile.to_file(input_parcel_4326_gdf, input_parcel_4326_filepath)
+        logger.info(f"Write reprojected features to {input_parcel_4326_path}")
+        geofile.to_file(input_parcel_4326_gdf, input_parcel_4326_path)
 
-    ##### Start calculation of the timeseries on gee #####
+    # Start calculation of the timeseries on gee
 
     logger.info("Start create_sentinel_timeseries_info")
     # On windows machines there seems to be an issue with gee. The following error is very common,
@@ -130,22 +137,25 @@ def calc_timeseries_data(input_parcel_filepath: Path,
     while done_success is False and nb_retries < 10:
         try:
             calculate_sentinel_timeseries(
-                    input_parcel_filepath=input_parcel_filepath,
-                    input_country_code=input_country_code,
-                    start_date_str=start_date_str,
-                    end_date_str=end_date_str,
-                    sensordata_to_get=sensordata_to_get,
-                    base_filename=base_filename,
-                    dest_data_dir=dest_data_dir)
+                input_parcel_path=input_parcel_path,
+                input_country_code=input_country_code,
+                start_date_str=start_date_str,
+                end_date_str=end_date_str,
+                sensordata_to_get=sensordata_to_get,
+                base_filename=base_filename,
+                dest_data_dir=dest_data_dir,
+            )
             done_success = True
 
         except OSError as ex:
             nb_retries += 1
-            if os.name == 'nt' and ex.winerror == 10048:
-                logger.warning(f"Exception [WinError {ex.winerror}] while trying calculate_sentinel_timeseries, retry! (Full exception message {ex})")
+            if os.name == "nt" and ex.winerror == 10048:
+                logger.warning(
+                    f"Exception [WinError {ex.winerror}] while trying calculate_sentinel_timeseries, retry! (Full exception message {ex})"
+                )
                 time.sleep(10)
             else:
-                raise 
+                raise
 
     # If it wasn't successful, log and stop.
     if done_success is False:
@@ -154,22 +164,25 @@ def calc_timeseries_data(input_parcel_filepath: Path,
         raise Exception(message)
 
     # Download the data from GEE
-    return_status = 'UNDEFINED'
+    return_status = "UNDEFINED"
     number_retries = 0
-    while return_status == 'UNDEFINED' or return_status == 'RETRY_NEEDED':
+    while return_status == "UNDEFINED" or return_status == "RETRY_NEEDED":
         # Download the results
         try:
-            logger.info('Now download needed timeseries files')
-            return_status = download_sentinel_timeseries(dest_data_dir=dest_data_dir,
-                                                         base_filename=base_filename)
+            logger.info("Now download needed timeseries files")
+            return_status = download_sentinel_timeseries(
+                dest_data_dir=dest_data_dir, base_filename=base_filename
+            )
 
             # Retry every 10 minutes
-            if return_status == 'RETRY_NEEDED':
-                logger.info('Not all data was available yet on google drive... try again in a few minutes...')
+            if return_status == "RETRY_NEEDED":
+                logger.info(
+                    "Not all data was available yet on google drive... try again in a few minutes..."
+                )
 
                 # Retry only 36 times, or +- 6 hours
                 if number_retries >= 70:
-                    return_status = 'STOP'
+                    return_status = "STOP"
                     message = "Retried a lot of times, but data still isn't available"
                     logger.error(message)
                     raise Exception(message)
@@ -184,13 +197,12 @@ def calc_timeseries_data(input_parcel_filepath: Path,
                 number_retries += 1
 
         except:
-            logger.error('ERROR downloading from google drive!')
+            logger.error("ERROR downloading from google drive!")
             raise
 
-def download_sentinel_timeseries(
-        dest_data_dir: Path,
-        base_filename: str):
-    """ Download the timeseries data from gee and clean it up. """
+
+def download_sentinel_timeseries(dest_data_dir: Path, base_filename: str):
+    """Download the timeseries data from gee and clean it up."""
 
     logger.info("Start download_sentinel_timeseries")
 
@@ -199,43 +211,45 @@ def download_sentinel_timeseries(
     global_gee_tasks_cache = None
 
     # The directory containing the list of filenames of files that need to be downloaded...
-    dest_data_dir_todownload = dest_data_dir / 'TODOWNLOAD'
+    dest_data_dir_todownload = dest_data_dir / "TODOWNLOAD"
 
     # Get the list of files to download...
-    csv_files_todownload = list(dest_data_dir_todownload.glob(f'{base_filename}_*.csv'))
+    csv_files_todownload = list(dest_data_dir_todownload.glob(f"{base_filename}_*.csv"))
     if len(csv_files_todownload) > 0:
         logger.info(f"Process files to download: {len(csv_files_todownload)}")
     else:
         logger.info("No files to download... stop.")
-        return 'DOWNLOAD_READY'
+        return "DOWNLOAD_READY"
 
     def connect_to_googledrive():
-        """ Connect to google drive API """
+        """Connect to google drive API"""
 
         # Setup the Drive v3 API
         # We need to be able to read file metadata and file contents
-        scopes = 'https://www.googleapis.com/auth/drive.readonly'
+        scopes = "https://www.googleapis.com/auth/drive.readonly"
 
         # The client secret file needs to be located in the same dir as the script otherwise it
         # doesn't seem to work
-        client_secret_file = 'client_secret_download-gee-data.json'
-        store = file.Storage('credentials.json')
+        client_secret_file = "client_secret_download-gee-data.json"
+        store = file.Storage("credentials.json")
         creds = store.get()
         if not creds or creds.invalid:
             flow = client.flow_from_clientsecrets(client_secret_file, scopes)
             creds = tools.run_flow(flow, store)
-        return build('drive', 'v3', http=creds.authorize(Http()))
+        return build("drive", "v3", http=creds.authorize(Http()))
 
     # Check for each todownload file if it is available on google drive...
-    return_status = 'ERROR'
+    return_status = "ERROR"
     drive_service = None
     for curr_csv_to_download_path in sorted(csv_files_todownload):
 
         # Get the basename of the file to download -> this will be the file name in google drive
         curr_csv_to_download_basename = os.path.basename(curr_csv_to_download_path)
-        dest_filepath = os.path.join(dest_data_dir, curr_csv_to_download_basename)
-        if os.path.isfile(dest_filepath) is True:
-            logger.error(f"SKIP, because file exists in destination dir, even though it is still in TODOWNLOAD dir as well???: {curr_csv_to_download_basename}")
+        dest_path = os.path.join(dest_data_dir, curr_csv_to_download_basename)
+        if os.path.isfile(dest_path) is True:
+            logger.error(
+                f"SKIP, because file exists in destination dir, even though it is still in TODOWNLOAD dir as well???: {curr_csv_to_download_basename}"
+            )
             continue
 
         # If we aren't connected yet to google drive... go for it...
@@ -243,66 +257,85 @@ def connect_to_googledrive():
             drive_service = connect_to_googledrive()
 
         # Search the file on google drive...
-        results = drive_service.files().list(
+        results = (
+            drive_service.files()
+            .list(
                 q=f"name = '{curr_csv_to_download_basename}' and trashed != true",
                 pageSize=100,
-                fields="nextPageToken, files(id, name)").execute()
-        items = results.get('files', [])
+                fields="nextPageToken, files(id, name)",
+            )
+            .execute()
+        )
+        items = results.get("files", [])
 
         # Check the result of the search
         if not items:
-            logger.warning(f"File to download not found on drive: {curr_csv_to_download_basename}")
-            return_status = 'RETRY_NEEDED'
+            logger.warning(
+                f"File to download not found on drive: {curr_csv_to_download_basename}"
+            )
+            return_status = "RETRY_NEEDED"
         elif len(items) > 1:
-            logger.error(f"More than one file found for filename: {curr_csv_to_download_basename}")
+            logger.error(
+                f"More than one file found for filename: {curr_csv_to_download_basename}"
+            )
         else:
             for item in items:
 
                 # Download the file
-                logger.info(f"Download file {curr_csv_to_download_basename} ({item['id']})")
-                request = drive_service.files().get_media(fileId=item['id'])
+                logger.info(
+                    f"Download file {curr_csv_to_download_basename} ({item['id']})"
+                )
+                request = drive_service.files().get_media(fileId=item["id"])
 
                 download_done = False
-                with open(dest_filepath, "wb") as dest_file:
-                    downloader = googleapiclient.http.MediaIoBaseDownload(dest_file, request)
+                with open(dest_path, "wb") as dest_file:
+                    downloader = googleapiclient.http.MediaIoBaseDownload(
+                        dest_file, request
+                    )
                     while download_done is False:
                         status, download_done = downloader.next_chunk()
-                        progress = status.progress()*100
+                        progress = status.progress() * 100
                         logger.info(f"Download now at {progress:.2f} %")
 
                 # Check if download was completed succesfully, and cleanup temp files...
                 if download_done is True:
-                    clean_gee_downloaded_csv(dest_filepath)
+                    clean_gee_downloaded_csv(dest_path)
                     os.remove(curr_csv_to_download_path)
-                elif os.path.isfile(dest_filepath):
-                    logger.error("Download wasn't completed succesfully, remove partially downloaded csv: {dest_filepath}!")
-                    os.remove(dest_filepath)
+                elif os.path.isfile(dest_path):
+                    logger.error(
+                        "Download wasn't completed succesfully, remove partially "
+                        f"downloaded csv: {dest_path}!"
+                    )
+                    os.remove(dest_path)
                 else:
-                    logger.error("Download wasn't completed succesfully and dest csv doesn't exist yet: {dest_filepath}!")
+                    logger.error(
+                        "Download wasn't completed succesfully and dest csv doesn't "
+                        f"exist yet: {dest_path}!"
+                    )
 
     # If there are no todownload files anymore... download is ready...
-    csv_files_todownload = list(dest_data_dir_todownload.glob(f'{base_filename}_*.csv'))
+    csv_files_todownload = list(dest_data_dir_todownload.glob(f"{base_filename}_*.csv"))
     if len(csv_files_todownload) == 0:
-        return_status = 'DOWNLOAD_READY'
+        return_status = "DOWNLOAD_READY"
 
     # Return true if all files were downloaded
     return return_status
 
+
 def clean_gee_downloaded_csvs_in_dir(dir_path: str):
-    """ 
-    Cleans csv's downloaded from gee by removing gee specific columns,... 
+    """
+    Cleans csv's downloaded from gee by removing gee specific columns,...
     """
 
     # Loop through all csv files in dir and remove the gee columns...
-    csv_files = glob.glob(os.path.join(dir_path, '*.csv'))
+    csv_files = glob.glob(os.path.join(dir_path, "*.csv"))
     for curr_csv in sorted(csv_files):
-        clean_gee_downloaded_csv(curr_csv,
-                                 remove_orig_csv=False)
+        clean_gee_downloaded_csv(curr_csv, remove_orig_csv=False)
+
 
-def clean_gee_downloaded_csv(csv_file: str,
-                             remove_orig_csv: bool = False):
-    """ 
-    Cleans a csv downloaded from gee by removing gee specific columns... 
+def clean_gee_downloaded_csv(csv_file: str, remove_orig_csv: bool = False):
+    """
+    Cleans a csv downloaded from gee by removing gee specific columns...
     """
 
     try:
@@ -312,46 +345,58 @@ def clean_gee_downloaded_csv(csv_file: str,
 
         # Check if output file exists already even though it is different from input file
         if output_file != csv_file and os.path.exists(output_file):
-            logger.warning(f"Output file exists already, so don't create it again: {output_file}")
+            logger.warning(
+                f"Output file exists already, so don't create it again: {output_file}"
+            )
         elif os.path.getsize(csv_file) == 0:
             # If input file is empty...
-            logger.info(f"File is empty, so just create new empty output file: {output_file}")
-            open(output_file, 'w').close()
+            logger.info(
+                f"File is empty, so just create new empty output file: {output_file}"
+            )
+            open(output_file, "w").close()
         else:
             # Read the file
             logger.info(f"Read file and remove gee specifice columns from {csv_file}")
 
-            # Sample 100 rows of data to determine dtypes, so floats can be read as float32 instead of the 
+            # Sample 100 rows of data to determine dtypes, so floats can be read as float32 instead of the
             # default float64. Writing those to eg. parquet is a lot more efficiënt.
             test_df = pd.read_csv(csv_file, nrows=100)
             float_cols = [c for c in test_df if test_df[c].dtype == "float64"]
             float32_cols = {c: np.float32 for c in float_cols}
 
             # Now read entire file
-            data_read_df = pd.read_csv(csv_file, engine='c', dtype=float32_cols)
-            
+            data_read_df = pd.read_csv(csv_file, engine="c", dtype=float32_cols)
+
             # Drop unnecessary gee specific columns...
             for column in data_read_df.columns:
-                if column in ['system:index', '.geo']:
+                if column in ["system:index", ".geo"]:
                     data_read_df.drop(column, axis=1, inplace=True)
-                elif column == 'count':
-                    logger.info(f"Rename count column to {conf.columns['pixcount_s1s2']}")
-                    data_read_df.rename(columns={'count': conf.columns['pixcount_s1s2']}, inplace=True)
+                elif column == "count":
+                    logger.info(
+                        f"Rename count column to {conf.columns['pixcount_s1s2']}"
+                    )
+                    data_read_df.rename(
+                        columns={"count": conf.columns["pixcount_s1s2"]}, inplace=True
+                    )
 
             # Set the id column as index
-            data_read_df.set_index(conf.columns['id'], inplace=True)
+            data_read_df.set_index(conf.columns["id"], inplace=True)
 
             # If there are data columns, write to output file
             if len(data_read_df.columns) > 0:
                 # Replace the original file by the cleaned one
-                logger.info(f"Write the file with the gee specific columns removed to a new file: {output_file}")
+                logger.info(
+                    f"Write the file with the gee specific columns removed to a new file: {output_file}"
+                )
                 pdh.to_file(data_read_df, Path(output_file), index=True)
             else:
-                logger.warning(f"No data columns found in file {csv_file}, so return!!!")
-                return            
+                logger.warning(
+                    f"No data columns found in file {csv_file}, so return!!!"
+                )
+                return
 
-        # If remove_orig_csv is True and the output filepath is different from orig filepath, 
-        # remove orig file. 
+        # If remove_orig_csv is True and the output path is different from orig path,
+        # remove orig file.
         if remove_orig_csv and output_file != csv_file:
             logger.info(f"Remove orig csv file: {csv_file}")
             os.remove(csv_file)
@@ -359,85 +404,102 @@ def clean_gee_downloaded_csv(csv_file: str,
     except Exception as ex:
         raise Exception(f"Error processing file {csv_file}") from ex
 
+
 def calculate_sentinel_timeseries(
-        input_parcel_filepath: Path,
-        input_country_code: str,
-        start_date_str: str,
-        end_date_str: str,
-        sensordata_to_get: List[str],
-        base_filename: str,
-        dest_data_dir: Path):
-    '''
+    input_parcel_path: Path,
+    input_country_code: str,
+    start_date_str: str,
+    end_date_str: str,
+    sensordata_to_get: List[str],
+    base_filename: str,
+    dest_data_dir: Path,
+):
+    """
     Credits: partly based on a gee S1 extraction script written by Guido Lemoine.
 
     TODO: it would be cleaner if feature of interest didn't have to be passed explicitly... but could be calculated from input_parcels... but too slow??
     TODO: some code is still old code from the original js version that could be cleaner by just writing it in python.
           this would also result in using less sockets, and less error that sockets can't be reused on windows ;-).
-    '''
+    """
 
     # Init some variables
-    dest_data_dir_todownload = dest_data_dir / 'TODOWNLOAD'
+    dest_data_dir_todownload = dest_data_dir / "TODOWNLOAD"
     if not dest_data_dir_todownload.exists():
         os.mkdir(dest_data_dir_todownload)
 
-    # Prepare filepath as it is available on gee
-    input_parcel_filepath_gee = conf.dirs.getpath('gee') / input_parcel_filepath.stem
+    # Prepare path as it is available on gee
+    input_parcel_path_gee = conf.dirs.getpath("gee") / input_parcel_path.stem
 
     # Initialize connection to server
     ee.Initialize()
 
-    if input_country_code == 'BEFL':
+    if input_country_code == "BEFL":
         # Define the bounds of flanders as region of interest
         region_of_interest = ee.Geometry.Polygon(
-            [[[3.219141559645095, 51.382839056214074],
-              [2.395251749811223, 51.08622074707946],
-              [2.6479083799461023, 50.75729132119862],
-              [2.8071930702097916, 50.67382488209542],
-              [4.05950498184086, 50.639001278789706],
-              [5.460117113586875, 50.67382291700078],
-              [5.927674232135132, 50.70514081680863],
-              [5.8355122769162335, 50.95084747021692],
-              [5.852391042458976, 51.06851086546833],
-              [5.86840409339311, 51.13983411545178],
-              [5.852835448995961, 51.18272203541686],
-              [5.460115101564611, 51.327099242895095],
-              [5.28459662023829, 51.413048026378355],
-              [5.045432234403279, 51.494974825956845],
-              [4.759572600621368, 51.50915727919978],
-              [4.2627231304370525, 51.47616009334954],
-              [4.17759696889857, 51.314226452568576]]])
+            [
+                [
+                    [3.219141559645095, 51.382839056214074],
+                    [2.395251749811223, 51.08622074707946],
+                    [2.6479083799461023, 50.75729132119862],
+                    [2.8071930702097916, 50.67382488209542],
+                    [4.05950498184086, 50.639001278789706],
+                    [5.460117113586875, 50.67382291700078],
+                    [5.927674232135132, 50.70514081680863],
+                    [5.8355122769162335, 50.95084747021692],
+                    [5.852391042458976, 51.06851086546833],
+                    [5.86840409339311, 51.13983411545178],
+                    [5.852835448995961, 51.18272203541686],
+                    [5.460115101564611, 51.327099242895095],
+                    [5.28459662023829, 51.413048026378355],
+                    [5.045432234403279, 51.494974825956845],
+                    [4.759572600621368, 51.50915727919978],
+                    [4.2627231304370525, 51.47616009334954],
+                    [4.17759696889857, 51.314226452568576],
+                ]
+            ]
+        )
     else:
         countries = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017")
-        region_of_interest = ee.Feature(countries
-                                        .filterMetadata('country_co', 'equals', input_country_code)
-                                        .union().first()).buffer(1000).geometry()
+        region_of_interest = (
+            ee.Feature(
+                countries.filterMetadata("country_co", "equals", input_country_code)
+                .union()
+                .first()
+            )
+            .buffer(1000)
+            .geometry()
+        )
 
     # TODO: add check if the ID column exists in the parcel file, otherwise he processes everything without ID column in output :-(!!!
 
     # First adapt start_date and end_date so they are mondays, so it becomes easier to reuse timeseries data
-    logger.info('Adapt start_date and end_date so they are mondays')
+    logger.info("Adapt start_date and end_date so they are mondays")
 
     start_date = ts_util.get_monday(start_date_str)
-    end_date = ts_util.get_monday(end_date_str)       # Remark: de end date is exclusive in gee filtering, so must be a monday as well...
-    start_date_str = start_date.strftime('%Y-%m-%d')
-    end_date_str = end_date.strftime('%Y-%m-%d')
+    end_date = ts_util.get_monday(
+        end_date_str
+    )  # Remark: de end date is exclusive in gee filtering, so must be a monday as well...
+    start_date_str = start_date.strftime("%Y-%m-%d")
+    end_date_str = end_date.strftime("%Y-%m-%d")
 
     # Keep the tasklist in a variable so it is loaded only once (if necessary)
 
-#    start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
-#    end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
+    #    start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
+    #    end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
 
     # Prepare the input vector data we want to add timeseries info to...
-    input_parcels = ee.FeatureCollection(input_parcel_filepath_gee)
+    input_parcels = ee.FeatureCollection(input_parcel_path_gee)
 
-    logging.info(f'Create sentinel timeseries from {start_date} till {end_date} for parcel in file {input_parcel_filepath_gee}')
+    logging.info(
+        f"Create sentinel timeseries from {start_date} till {end_date} for parcel in file {input_parcel_path_gee}"
+    )
 
     # Add some columns with feature info, to be able to eg. filter only polygons...
-#    def add_feature_info(feature):
-#        return feature.set('area', feature.area(), 'perimeter', feature.perimeter(), 'type', feature.geometry().type())
-#    bevl2017 = bevl2017.map(add_feature_info)
+    #    def add_feature_info(feature):
+    #        return feature.set('area', feature.area(), 'perimeter', feature.perimeter(), 'type', feature.geometry().type())
+    #    bevl2017 = bevl2017.map(add_feature_info)
 
-    '''
+    """
     # Buffer
     # Now define a function to buffer the parcels inward
     def bufferFeature(ft):
@@ -445,101 +507,140 @@ def bufferFeature(ft):
 #     return ft.set({ncoords: ft.geometry().coordinates().length()})
       return ft
     input_parcels = input_parcels.map(bufferFeature)
-    '''
+    """
 
     # Export non-polygons..
-#    bevl2017_nopoly = bevl2017.filterMetadata('type', 'not_equals', 'Polygon')
-#    ee.batch.Export.table.toDrive(collection = bevl2017_nopoly, folder = 'Monitoring', description = 'BEVL2017_no_polygon', fileFormat = 'KMZ')
+    #    bevl2017_nopoly = bevl2017.filterMetadata('type', 'not_equals', 'Polygon')
+    #    ee.batch.Export.table.toDrive(collection = bevl2017_nopoly, folder = 'Monitoring', description = 'BEVL2017_no_polygon', fileFormat = 'KMZ')
 
     # Filter the S1 data we want to have (VV and VH pol.)
-    s1 = (ee.ImageCollection('COPERNICUS/S1_GRD')
-          .filter(ee.Filter.eq('instrumentMode', 'IW'))
-          .filter(ee.Filter.eq('transmitterReceiverPolarisation', ['VV', 'VH']))
-          .filterBounds(region_of_interest)
-          .filterDate(start_date_str, end_date_str).sort('system:time_start'))
+    s1 = (
+        ee.ImageCollection("COPERNICUS/S1_GRD")
+        .filter(ee.Filter.eq("instrumentMode", "IW"))
+        .filter(ee.Filter.eq("transmitterReceiverPolarisation", ["VV", "VH"]))
+        .filterBounds(region_of_interest)
+        .filterDate(start_date_str, end_date_str)
+        .sort("system:time_start")
+    )
 
     # Remove ugly edges from S1
     def mask_s1_edge(img):
-        """ Masks the edges of S1 images where backscatter is close to zero... """
-        mask = (img.select(0).unitScale(-25, 5).multiply(255).toByte()
-                .connectedComponents(ee.Kernel.rectangle(1, 1), 100))
+        """Masks the edges of S1 images where backscatter is close to zero..."""
+        mask = (
+            img.select(0)
+            .unitScale(-25, 5)
+            .multiply(255)
+            .toByte()
+            .connectedComponents(ee.Kernel.rectangle(1, 1), 100)
+        )
         return img.updateMask(mask.select(0))
+
     s1 = s1.map(mask_s1_edge)
 
     # Functions to convert from/to dB
     def to_natural(img):
-        """ Converts an image to natural values from db values. """
-        return (ee.Image(10.0).pow(img.select('..').divide(10.0))
-                .copyProperties(img, ['system:time_start', 'orbitProperties_pass']))
+        """Converts an image to natural values from db values."""
+        return (
+            ee.Image(10.0)
+            .pow(img.select("..").divide(10.0))
+            .copyProperties(img, ["system:time_start", "orbitProperties_pass"])
+        )
 
     def to_db(img):
-        """ Converts an image to db values from natural values. """
+        """Converts an image to db values from natural values."""
         return ee.Image(img).log10().multiply(10.0)
+
     s1 = s1.map(to_natural)
 
     # Load interesting S2 images
-    s2 = (ee.ImageCollection('COPERNICUS/S2')
-          .filterBounds(region_of_interest)
-          .filterDate(start_date_str, end_date_str)
-          .filter(ee.Filter.lessThan('CLOUDY_PIXEL_PERCENTAGE', 10)))
+    s2 = (
+        ee.ImageCollection("COPERNICUS/S2")
+        .filterBounds(region_of_interest)
+        .filterDate(start_date_str, end_date_str)
+        .filter(ee.Filter.lessThan("CLOUDY_PIXEL_PERCENTAGE", 10))
+    )
 
     # Bits 10 and 11 are clouds and cirrus, respectively.
     cloud_bitmask = ee.Number(2).pow(10).int()
     cirrus_bitmask = ee.Number(2).pow(11).int()
 
     def mask_S2_clouds(image):
-        """ Mask the clouds of a sentinel 2 image. """
-        qa = image.select('QA60')
+        """Mask the clouds of a sentinel 2 image."""
+        qa = image.select("QA60")
         # Both flags should be set to zero, indicating clear conditions.
-        mask = qa.bitwiseAnd(cloud_bitmask).eq(0).And(qa.bitwiseAnd(cirrus_bitmask).eq(0))
+        mask = (
+            qa.bitwiseAnd(cloud_bitmask).eq(0).And(qa.bitwiseAnd(cirrus_bitmask).eq(0))
+        )
         return image.updateMask(mask)
+
     s2 = s2.map(mask_S2_clouds)
 
     # Olha's idea to create weekly mean images
-    step = 7 # in days
+    step = 7  # in days
+
     def nextday(date):
-        """ Returns the next day. """
+        """Returns the next day."""
         return ee.Date(start_date_str).advance(date, "day")
 
-    days = ee.List.sequence(0,
-                            ee.Date(end_date_str).difference(ee.Date(start_date_str), 'day'),
-                            step).map(nextday)
+    days = ee.List.sequence(
+        0, ee.Date(end_date_str).difference(ee.Date(start_date_str), "day"), step
+    ).map(nextday)
     periods = days.slice(0, -1).zip(days.slice(1))
 
     # Function to get a string representation for a period
     def get_period_str(period):
-        """ Gets a string representation for the period. """
-        return ee.Date(ee.List(period).get(0)).format('YYYYMMdd')
+        """Gets a string representation for the period."""
+        return ee.Date(ee.List(period).get(0)).format("YYYYMMdd")
 
     # Get the S1's for a period...
     def get_s1s_forperiod(period):
-        return s1.filterDate(ee.List(period).get(0), ee.List(period).get(1)).select(['VV', 'VH'])
+        return s1.filterDate(ee.List(period).get(0), ee.List(period).get(1)).select(
+            ["VV", "VH"]
+        )
 
     # Get the S1's for a period...
     def get_s1_forperiod(period):
         period_str = get_period_str(period)
-        return (s1.filterDate(ee.List(period).get(0), ee.List(period).get(1))
-                .mean()
-                .select(['VV', 'VH'], [ee.String('VV_').cat(period_str),
-                                       ee.String('VH_').cat(period_str)]))
+        return (
+            s1.filterDate(ee.List(period).get(0), ee.List(period).get(1))
+            .mean()
+            .select(
+                ["VV", "VH"],
+                [ee.String("VV_").cat(period_str), ee.String("VH_").cat(period_str)],
+            )
+        )
 
     # Get the S1's for a period...
     def get_s1_asc_forperiod(period):
         period_str = get_period_str(period)
-        s1_asc = s1.filter(ee.Filter.eq('orbitProperties_pass', 'ASCENDING'))
-        return (s1_asc.filterDate(ee.List(period).get(0), ee.List(period).get(1))
-                .mean()
-                .select(['VV', 'VH'], [ee.String('VV_ASC_').cat(period_str),
-                                       ee.String('VH_ASC_').cat(period_str)]))
+        s1_asc = s1.filter(ee.Filter.eq("orbitProperties_pass", "ASCENDING"))
+        return (
+            s1_asc.filterDate(ee.List(period).get(0), ee.List(period).get(1))
+            .mean()
+            .select(
+                ["VV", "VH"],
+                [
+                    ee.String("VV_ASC_").cat(period_str),
+                    ee.String("VH_ASC_").cat(period_str),
+                ],
+            )
+        )
 
     # Get the S1's for a period...
     def get_s1_desc_forperiod(period):
         period_str = get_period_str(period)
-        s1_desc = s1.filter(ee.Filter.eq('orbitProperties_pass', 'DESCENDING'))
-        return (s1_desc.filterDate(ee.List(period).get(0), ee.List(period).get(1))
-                .mean()
-                .select(['VV', 'VH'], [ee.String('VV_DESC_').cat(period_str),
-                                       ee.String('VH_DESC_').cat(period_str)]))
+        s1_desc = s1.filter(ee.Filter.eq("orbitProperties_pass", "DESCENDING"))
+        return (
+            s1_desc.filterDate(ee.List(period).get(0), ee.List(period).get(1))
+            .mean()
+            .select(
+                ["VV", "VH"],
+                [
+                    ee.String("VV_DESC_").cat(period_str),
+                    ee.String("VH_DESC_").cat(period_str),
+                ],
+            )
+        )
 
     # Get the S2's for a period...
     # Remark: median on an imagecollection apparently has a result that
@@ -562,7 +663,7 @@ def get_gee_tasklist():
     #         option to ignore completed ones. But there is an easy workaround: just change the
     #         basefilename in any way to ignore the completed ones.
     def check_if_task_exists(task_description: str, task_state_list) -> bool:
-        """ Checks if a task exists already on gee """
+        """Checks if a task exists already on gee"""
 
         # If the tasks aren't retrieved yet, do so...
         global global_gee_tasks_cache
@@ -572,65 +673,88 @@ def check_if_task_exists(task_description: str, task_state_list) -> bool:
         # Check if there is a task with this name and this state (there can be multiple tasks with
         # this name!)
         for task in global_gee_tasks_cache:
-            if(task['description'] == task_description
-               and task['task_type'] == 'EXPORT_FEATURES'
-               and task['state'] in task_state_list):
-                logger.debug(f"<check_if_task_exists> Task {task_description} found with state {task['state']}")
+            if (
+                task["description"] == task_description
+                and task["task_type"] == "EXPORT_FEATURES"
+                and task["state"] in task_state_list
+            ):
+                logger.debug(
+                    f"<check_if_task_exists> Task {task_description} found with state {task['state']}"
+                )
                 return True
-        logger.debug(f"<check_if_task_exists> Task {task_description} doesn't exist with any of the states in {task_state_list}")
+        logger.debug(
+            f"<check_if_task_exists> Task {task_description} doesn't exist with any of the states in {task_state_list}"
+        )
         return False
 
     def reduce_and_export(imagedata, reducer, export_descr: str):
-        """ Reduces the imagedata over the features and export to drive. """
+        """Reduces the imagedata over the features and export to drive."""
 
         # First check if the file exists already locally...
         # Format relevant local filename
-        export_filename = export_descr + '.csv'
+        export_filename = export_descr + ".csv"
         dest_fullpath = os.path.join(dest_data_dir, export_filename)
-        dest_fullpath_todownload = os.path.join(dest_data_dir_todownload, export_filename)
+        dest_fullpath_todownload = os.path.join(
+            dest_data_dir_todownload, export_filename
+        )
 
         # If the data is already available locally... go to next period
         if os.path.isfile(dest_fullpath):
-            logger.info(f"For task {export_descr}, file already available locally: SKIP")
+            logger.info(
+                f"For task {export_descr}, file already available locally: SKIP"
+            )
             return
 
         # If the data is already "ordered" in a previous run and is still busy processing, don't
         # start processing again
-        if (os.path.isfile(dest_fullpath_todownload)
-                and(check_if_task_exists(export_description, ['RUNNING', 'READY', 'COMPLETED']))):
-            logger.info(f"For task {export_descr}, file still busy processing or is ready on gee: SKIP")
+        if os.path.isfile(dest_fullpath_todownload) and (
+            check_if_task_exists(export_description, ["RUNNING", "READY", "COMPLETED"])
+        ):
+            logger.info(
+                f"For task {export_descr}, file still busy processing or is ready on gee: SKIP"
+            )
             return
 
         # Get the sentinel data for each parcel
         # Remark: from the input parcels, only keep the ID column...
-        imagedata_perparcel = imagedata.reduceRegions(collection=input_parcels.select([conf.columns['id']]),
-                                                      reducer=reducer,
-                                                      scale=10)
+        imagedata_perparcel = imagedata.reduceRegions(
+            collection=input_parcels.select([conf.columns["id"]]),
+            reducer=reducer,
+            scale=10,
+        )
 
         # Set the geometries to none, as we don't want to export them... and parameter
         # retainGeometry=False in select doesn't seem to work.
         def geom_to_none(feature):
-            """ Set the geom of the feature to none. """
+            """Set the geom of the feature to none."""
             return feature.setGeometry(None)
+
         imagedata_perparcel = imagedata_perparcel.map(geom_to_none)
 
         # Export to google drive
-        export_task = ee.batch.Export.table.toDrive(collection=imagedata_perparcel,
-                                                    folder='Monitoring',
-                                                    description=export_descr,
-                                                    fileFormat='CSV')
+        export_task = ee.batch.Export.table.toDrive(
+            collection=imagedata_perparcel,
+            folder="Monitoring",
+            description=export_descr,
+            fileFormat="CSV",
+        )
         ee.batch.Task.start(export_task)
 
         # Create file in todownload folder to indicate this file should be downloaded
         Path(dest_fullpath_todownload).touch()
 
     # If the file doesn't exist yet... export the parcel with all interesting columns to csv...
-    s1_for_count = (s1.filterDate(ee.List(periods.get(0)).get(0), ee.List(periods.get(0)).get(1))
-                    .mean().select(['VV'], ['pixcount']))
+    s1_for_count = (
+        s1.filterDate(ee.List(periods.get(0)).get(0), ee.List(periods.get(0)).get(1))
+        .mean()
+        .select(["VV"], ["pixcount"])
+    )
     export_description = f"{base_filename}_pixcount"
-    reduce_and_export(imagedata=s1_for_count,
-                      reducer=ee.Reducer.count(),
-                      export_descr=export_description)
+    reduce_and_export(
+        imagedata=s1_for_count,
+        reducer=ee.Reducer.count(),
+        export_descr=export_description,
+    )
 
     # Loop over all periods and export data per period to drive
     # Create the reducer we want to use...
@@ -639,14 +763,14 @@ def geom_to_none(feature):
     #         end with the aggregation/reduce type used, otherwise it doesn't and other code will
     #         break
     reducer = ee.Reducer.mean().combine(reducer2=ee.Reducer.stdDev(), sharedInputs=True)
-#    reducer = ee.Reducer.mean()
+    #    reducer = ee.Reducer.mean()
     nb_periods = periods.length().getInfo()
     logger.info(f"Loop through all <{nb_periods}> periods")
     for i in range(0, nb_periods):
 
         # Calculate the start and end dates of this period...
-        period_start_str = (start_date + timedelta(days=i*7)).strftime('%Y-%m-%d')
-        period_end_str = (start_date + timedelta(days=(i+1)*7)).strftime('%Y-%m-%d')
+        period_start_str = (start_date + timedelta(days=i * 7)).strftime("%Y-%m-%d")
+        period_end_str = (start_date + timedelta(days=(i + 1) * 7)).strftime("%Y-%m-%d")
         logger.debug(f"Process period: {period_start_str} till {period_end_str}")
 
         def merge_bands(image1, image2):
@@ -664,70 +788,98 @@ def merge_bands(image1, image2):
                 return image2
 
         # Get mean s1 image of the s1 images that are available in this period
-        SENSORDATA_S1 = conf.general['SENSORDATA_S1']
+        SENSORDATA_S1 = conf.general["SENSORDATA_S1"]
         if SENSORDATA_S1 in sensordata_to_get:
             # If the data is already available locally... skip
             sensordata_descr = f"{base_filename}_{period_start_str}_{SENSORDATA_S1}"
             if os.path.isfile(os.path.join(dest_data_dir, f"{sensordata_descr}.csv")):
-                logger.info(f"For task {sensordata_descr}, file already available locally: SKIP")
+                logger.info(
+                    f"For task {sensordata_descr}, file already available locally: SKIP"
+                )
                 return
             else:
                 # Now the real work
                 s1_forperiod = get_s1_forperiod([period_start_str, period_end_str])
-                reduce_and_export(imagedata=s1_forperiod,
-                                  reducer=reducer,
-                                  export_descr=sensordata_descr)
+                reduce_and_export(
+                    imagedata=s1_forperiod,
+                    reducer=reducer,
+                    export_descr=sensordata_descr,
+                )
 
         # Get mean s1 image of the s1 images that are available in this period
-        SENSORDATA_S1DB = conf.general['SENSORDATA_S1DB']
+        SENSORDATA_S1DB = conf.general["SENSORDATA_S1DB"]
         if SENSORDATA_S1DB in sensordata_to_get:
             # If the data is already available locally... skip
             sensordata_descr = f"{base_filename}_{period_start_str}_{SENSORDATA_S1DB}"
             if os.path.isfile(os.path.join(dest_data_dir, f"{sensordata_descr}.csv")):
-                logger.info(f"For task {sensordata_descr}, file already available locally: SKIP")
+                logger.info(
+                    f"For task {sensordata_descr}, file already available locally: SKIP"
+                )
                 return
             else:
                 # Now the real work
                 s1_forperiod = get_s1_forperiod([period_start_str, period_end_str])
-                reduce_and_export(imagedata=to_db(s1_forperiod),
-                                  reducer=reducer,
-                                  export_descr=sensordata_descr)
+                reduce_and_export(
+                    imagedata=to_db(s1_forperiod),
+                    reducer=reducer,
+                    export_descr=sensordata_descr,
+                )
 
         # Get mean s1 asc and desc image of the s1 images that are available in this period
-        SENSORDATA_S1_ASCDESC = conf.general['SENSORDATA_S1_ASCDESC']
+        SENSORDATA_S1_ASCDESC = conf.general["SENSORDATA_S1_ASCDESC"]
         if SENSORDATA_S1_ASCDESC in sensordata_to_get:
             # If the data is already available locally... skip
-            sensordata_descr = f"{base_filename}_{period_start_str}_{SENSORDATA_S1_ASCDESC}"
+            sensordata_descr = (
+                f"{base_filename}_{period_start_str}_{SENSORDATA_S1_ASCDESC}"
+            )
             if os.path.isfile(os.path.join(dest_data_dir, f"{sensordata_descr}.csv")):
-                logger.info(f"For task {sensordata_descr}, file already available locally: SKIP")
+                logger.info(
+                    f"For task {sensordata_descr}, file already available locally: SKIP"
+                )
             else:
                 # Now the real work
-                s1_asc_forperiod = get_s1_asc_forperiod([period_start_str, period_end_str])
-                s1_desc_forperiod = get_s1_desc_forperiod([period_start_str, period_end_str])
+                s1_asc_forperiod = get_s1_asc_forperiod(
+                    [period_start_str, period_end_str]
+                )
+                s1_desc_forperiod = get_s1_desc_forperiod(
+                    [period_start_str, period_end_str]
+                )
                 imagedata_forperiod = merge_bands(s1_asc_forperiod, s1_desc_forperiod)
-                reduce_and_export(imagedata=imagedata_forperiod,
-                                  reducer=reducer,
-                                  export_descr=sensordata_descr)
+                reduce_and_export(
+                    imagedata=imagedata_forperiod,
+                    reducer=reducer,
+                    export_descr=sensordata_descr,
+                )
 
         # Get mean s1 in DB, asc and desc image of the s1 images that are available in this period
-        SENSORDATA_S1DB_ASCDESC = conf.general['SENSORDATA_S1DB_ASCDESC']
+        SENSORDATA_S1DB_ASCDESC = conf.general["SENSORDATA_S1DB_ASCDESC"]
         if SENSORDATA_S1DB_ASCDESC in sensordata_to_get:
             # If the data is already available locally... skip
-            sensordata_descr = f"{base_filename}_{period_start_str}_{SENSORDATA_S1DB_ASCDESC}"
+            sensordata_descr = (
+                f"{base_filename}_{period_start_str}_{SENSORDATA_S1DB_ASCDESC}"
+            )
             if os.path.isfile(dest_data_dir / f"{sensordata_descr}.csv"):
-                logger.info(f"For task {sensordata_descr}, file already available locally: SKIP")
+                logger.info(
+                    f"For task {sensordata_descr}, file already available locally: SKIP"
+                )
             else:
                 # Now the real work
-                s1_asc_forperiod = get_s1_asc_forperiod([period_start_str, period_end_str])
-                s1_desc_forperiod = get_s1_desc_forperiod([period_start_str, period_end_str])
+                s1_asc_forperiod = get_s1_asc_forperiod(
+                    [period_start_str, period_end_str]
+                )
+                s1_desc_forperiod = get_s1_desc_forperiod(
+                    [period_start_str, period_end_str]
+                )
                 imagedata_forperiod = merge_bands(s1_asc_forperiod, s1_desc_forperiod)
-                reduce_and_export(imagedata=to_db(imagedata_forperiod),
-                                  reducer=reducer,
-                                  export_descr=sensordata_descr)
+                reduce_and_export(
+                    imagedata=to_db(imagedata_forperiod),
+                    reducer=reducer,
+                    export_descr=sensordata_descr,
+                )
 
         # Get mean s2 image of the s2 images that have (almost)cloud free images available in this
         # period
-        SENSORDATA_S2gt95 = conf.general['SENSORDATA_S2gt95']
+        SENSORDATA_S2gt95 = conf.general["SENSORDATA_S2gt95"]
         if SENSORDATA_S2gt95 in sensordata_to_get:
             sensordata_descr = f"{base_filename}_{period_start_str}_{SENSORDATA_S2gt95}"
 
@@ -736,34 +888,44 @@ def merge_bands(image1, image2):
             #         even if data is available.
             dest_fullpath = dest_data_dir / f"{sensordata_descr}.csv"
             if os.path.isfile(dest_fullpath):
-                logger.info(f"For task {sensordata_descr}, file already available locally: SKIP")
+                logger.info(
+                    f"For task {sensordata_descr}, file already available locally: SKIP"
+                )
             else:
                 # Get s2 images that are available in this period
                 s2s_forperiod = get_s2s_forperiod([period_start_str, period_end_str])
 
                 # If S2 available for entire flanders... export S2 as well
-                s2_pct_bevl = (region_of_interest.intersection(s2s_forperiod.geometry())
-                               .area().divide(region_of_interest.area()))
+                s2_pct_bevl = (
+                    region_of_interest.intersection(s2s_forperiod.geometry())
+                    .area()
+                    .divide(region_of_interest.area())
+                )
                 if s2_pct_bevl.getInfo() > 0.95:
-                    s2_forperiod = (s2s_forperiod.median()
-                                    .select(['B2', 'B3', 'B4', 'B8'],
-                                            [ee.String('S2B2_').cat(period_start_str),
-                                             ee.String('S2B3_').cat(period_start_str),
-                                             ee.String('S2B4_').cat(period_start_str),
-                                             ee.String('S2B8_').cat(period_start_str)
-                                            ]))
+                    s2_forperiod = s2s_forperiod.median().select(
+                        ["B2", "B3", "B4", "B8"],
+                        [
+                            ee.String("S2B2_").cat(period_start_str),
+                            ee.String("S2B3_").cat(period_start_str),
+                            ee.String("S2B4_").cat(period_start_str),
+                            ee.String("S2B8_").cat(period_start_str),
+                        ],
+                    )
                 else:
                     # Create an empty destination file so we'll know that we tested the 95% already
                     # and don't need to do it again...
                     dest_fullpath.touch()
                     continue
 
-#                logger.debug(f"S2 Bands: {ee.Image(s2_forperiod).bandNames().getInfo()}")
-                reduce_and_export(imagedata=s2_forperiod,
-                                  reducer=reducer,
-                                  export_descr=sensordata_descr)
+                #                logger.debug(f"S2 Bands: {ee.Image(s2_forperiod).bandNames().getInfo()}")
+                reduce_and_export(
+                    imagedata=s2_forperiod,
+                    reducer=reducer,
+                    export_descr=sensordata_descr,
+                )
+
 
 # If the script is run directly...
 if __name__ == "__main__":
-    logger.critical('Not implemented exception!')
-    raise Exception('Not implemented')
+    logger.critical("Not implemented exception!")
+    raise Exception("Not implemented")
diff --git a/cropclassification/preprocess/timeseries_util.py b/cropclassification/preprocess/timeseries_util.py
index eb0b7273..37b2f90c 100644
--- a/cropclassification/preprocess/timeseries_util.py
+++ b/cropclassification/preprocess/timeseries_util.py
@@ -8,7 +8,7 @@
 import gc
 import os
 from pathlib import Path
-from typing import List
+from typing import List, Optional
 
 import numpy as np
 import pandas as pd
@@ -18,163 +18,201 @@
 import cropclassification.helpers.geofile as geofile_util
 import cropclassification.helpers.pandas_helper as pdh
 
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # First define/init some general variables/constants
-#-------------------------------------------------------------
+# -------------------------------------------------------------
 # Get a logger...
 logger = logging.getLogger(__name__)
 
-IMAGETYPE_S1_GRD = 'S1_GRD'
-IMAGETYPE_S1_COHERENCE = 'S1_COH'
-IMAGETYPE_S2_L2A = 'S2_L2A'
+IMAGETYPE_S1_GRD = "S1_GRD"
+IMAGETYPE_S1_COHERENCE = "S1_COH"
+IMAGETYPE_S2_L2A = "S2_L2A"
 
-def prepare_input(input_parcel_filepath: Path,
-                  output_imagedata_parcel_input_filepath: Path,
-                  output_parcel_nogeo_filepath: Path = None,
-                  force: bool = False):
+
+def prepare_input(
+    input_parcel_path: Path,
+    output_imagedata_parcel_input_path: Path,
+    output_parcel_nogeo_path: Optional[Path] = None,
+    force: bool = False,
+):
     """
     This function creates a file that is preprocessed to be a good input file for
     timeseries extraction of sentinel images.
 
     Args
-        input_parcel_filepath (Path): input file
-        output_imagedata_parcel_input_filepath (Path): prepared output file
-        output_parcel_nogeo_filepath (Path): output file with a copy of the non-geo data
+        input_parcel_path (Path): input file
+        output_imagedata_parcel_input_path (Path): prepared output file
+        output_parcel_nogeo_path (Path): output file with a copy of the non-geo data
         force: force creation, even if output file(s) exist already
 
     """
-    ##### Check if parameters are OK and init some extra params #####
-    if not input_parcel_filepath.exists():
-        raise Exception(f"Input file doesn't exist: {input_parcel_filepath}")
-    
+    # Check if parameters are OK and init some extra params
+    if not input_parcel_path.exists():
+        raise Exception(f"Input file doesn't exist: {input_parcel_path}")
+
     # Check if the input file has a projection specified
-    if geofile_util.get_crs(input_parcel_filepath) is None:
-        message = f"The parcel input file doesn't have a projection/crs specified, so STOP: {input_parcel_filepath}"
+    if geofile_util.get_crs(input_parcel_path) is None:
+        message = (
+            "The parcel input file doesn't have a projection/crs specified, so STOP: "
+            f"{input_parcel_path}"
+        )
         logger.critical(message)
         raise Exception(message)
 
     # If force == False Check and the output file exists already, stop.
-    if(force is False 
-       and output_imagedata_parcel_input_filepath.exists()
-       and (output_parcel_nogeo_filepath is None or output_parcel_nogeo_filepath.exists())):
-        logger.warning("prepare_input: force == False and output files exist, so stop: " 
-                       + f"{output_imagedata_parcel_input_filepath}, "
-                       + f"{output_parcel_nogeo_filepath}")
+    if (
+        force is False
+        and output_imagedata_parcel_input_path.exists()
+        and (output_parcel_nogeo_path is None or output_parcel_nogeo_path.exists())
+    ):
+        logger.warning(
+            "prepare_input: force == False and output files exist, so stop: "
+            + f"{output_imagedata_parcel_input_path}, "
+            + f"{output_parcel_nogeo_path}"
+        )
         return
 
-    logger.info(f"Process input file {input_parcel_filepath}")
+    logger.info(f"Process input file {input_parcel_path}")
 
     # Create temp dir to store temporary data for tracebility
-    temp_output_dir = output_imagedata_parcel_input_filepath.parent / 'temp'
+    temp_output_dir = output_imagedata_parcel_input_path.parent / "temp"
     if not temp_output_dir.exists():
         os.mkdir(temp_output_dir)
 
-    ##### Read the parcel data and write nogeo version #####
-    parceldata_gdf = geofile_util.read_file(input_parcel_filepath)
-    logger.info(f'Parceldata read, shape: {parceldata_gdf.shape}')
+    # Read the parcel data and write nogeo version
+    parceldata_gdf = geofile_util.read_file(input_parcel_path)
+    logger.info(f"Parceldata read, shape: {parceldata_gdf.shape}")
 
     # Check if the id column is present and set as index
-    if conf.columns['id'] in parceldata_gdf.columns:
-        parceldata_gdf.set_index(conf.columns['id'], inplace=True)
+    if conf.columns["id"] in parceldata_gdf.columns:
+        parceldata_gdf.set_index(conf.columns["id"], inplace=True)
     else:
-        message = f"STOP: Column {conf.columns['id']} not found in input parcel file: {input_parcel_filepath}. Make sure the column is present or change the column name in global_constants.py"
+        message = (
+            f"STOP: Column {conf.columns['id']} not found in input parcel file: "
+            f"{input_parcel_path}. Make sure the column is present or change the "
+            "column name in global_constants.py"
+        )
         logger.critical(message)
         raise Exception(message)
-        
-    if(output_parcel_nogeo_filepath is not None
-       and (force is True or not output_parcel_nogeo_filepath.exists())):
-        logger.info(f"Save non-geo data to {output_parcel_nogeo_filepath}")
-        parceldata_nogeo_df = parceldata_gdf.drop(['geometry'], axis = 1)
-        pdh.to_file(parceldata_nogeo_df, output_parcel_nogeo_filepath)
-
-    ##### Do the necessary conversions and write buffered file #####
-    
+
+    if output_parcel_nogeo_path is not None and (
+        force is True or not output_parcel_nogeo_path.exists()
+    ):
+        logger.info(f"Save non-geo data to {output_parcel_nogeo_path}")
+        parceldata_nogeo_df = parceldata_gdf.drop(["geometry"], axis=1)
+        pdh.to_file(parceldata_nogeo_df, output_parcel_nogeo_path)
+
+    # Do the necessary conversions and write buffered file
+
     # If force == False Check and the output file exists already, stop.
-    if(force is False 
-       and output_imagedata_parcel_input_filepath.exists()):
-        logger.warning("prepare_input: force == False and output files exist, so stop: " 
-                       + f"{output_imagedata_parcel_input_filepath}")
+    if force is False and output_imagedata_parcel_input_path.exists():
+        logger.warning(
+            "prepare_input: force == False and output files exist, so stop: "
+            f"{output_imagedata_parcel_input_path}"
+        )
         return
 
-    logger.info('Apply buffer on parcel')
+    logger.info("Apply buffer on parcel")
     parceldata_buf_gdf = parceldata_gdf.copy()
 
     # resolution = number of segments per circle
-    buffer_size = -conf.marker.getint('buffer')
-    parceldata_buf_gdf[conf.columns['geom']] = (parceldata_buf_gdf[conf.columns['geom']]
-                                                .buffer(buffer_size, resolution=5))
+    buffer_size = -conf.marker.getint("buffer")
+    parceldata_buf_gdf[conf.columns["geom"]] = parceldata_buf_gdf[
+        conf.columns["geom"]
+    ].buffer(buffer_size, resolution=5)
 
     # Export buffered geometries that result in empty geometries
-    logger.info('Export parcels that are empty after buffer')
+    logger.info("Export parcels that are empty after buffer")
     parceldata_buf_empty_df = parceldata_buf_gdf.loc[
-            parceldata_buf_gdf[conf.columns['geom']].is_empty == True]
+        parceldata_buf_gdf[conf.columns["geom"]].is_empty == True
+    ]
     if len(parceldata_buf_empty_df.index) > 0:
-        parceldata_buf_empty_df.drop(conf.columns['geom'], axis=1, inplace=True)
-        temp_empty_filepath = temp_output_dir / f"{output_imagedata_parcel_input_filepath.stem}_empty.sqlite"
-        pdh.to_file(parceldata_buf_empty_df, temp_empty_filepath)
+        parceldata_buf_empty_df.drop(conf.columns["geom"], axis=1, inplace=True)
+        temp_empty_path = (
+            temp_output_dir / f"{output_imagedata_parcel_input_path.stem}_empty.sqlite"
+        )
+        pdh.to_file(parceldata_buf_empty_df, temp_empty_path)
 
     # Export parcels that don't result in a (multi)polygon
     parceldata_buf_notempty_gdf = parceldata_buf_gdf.loc[
-            parceldata_buf_gdf[conf.columns['geom']].is_empty == False]
+        parceldata_buf_gdf[conf.columns["geom"]].is_empty == False
+    ]
     parceldata_buf_nopoly_gdf = parceldata_buf_notempty_gdf.loc[
-            ~parceldata_buf_notempty_gdf[conf.columns['geom']].geom_type.isin(['Polygon', 'MultiPolygon'])]
+        ~parceldata_buf_notempty_gdf[conf.columns["geom"]].geom_type.isin(
+            ["Polygon", "MultiPolygon"]
+        )
+    ]
     if len(parceldata_buf_nopoly_gdf.index) > 0:
-        logger.info('Export parcels that are no (multi)polygons after buffer')
-        parceldata_buf_nopoly_gdf.drop(conf.columns['geom'], axis=1, inplace=True)      
-        temp_nopoly_filepath = temp_output_dir / f"{output_imagedata_parcel_input_filepath.stem}_nopoly.sqlite"
-        geofile_util.to_file(parceldata_buf_nopoly_gdf, temp_nopoly_filepath)
+        logger.info("Export parcels that are no (multi)polygons after buffer")
+        parceldata_buf_nopoly_gdf.drop(conf.columns["geom"], axis=1, inplace=True)
+        temp_nopoly_path = (
+            temp_output_dir / f"{output_imagedata_parcel_input_path.stem}_nopoly.sqlite"
+        )
+        geofile_util.to_file(parceldata_buf_nopoly_gdf, temp_nopoly_path)
 
     # Export parcels that are (multi)polygons after buffering
     parceldata_buf_poly_gdf = parceldata_buf_notempty_gdf.loc[
-            parceldata_buf_notempty_gdf[conf.columns['geom']].geom_type.isin(['Polygon', 'MultiPolygon'])]
+        parceldata_buf_notempty_gdf[conf.columns["geom"]].geom_type.isin(
+            ["Polygon", "MultiPolygon"]
+        )
+    ]
     for column in parceldata_buf_poly_gdf.columns:
-        if column not in [conf.columns['id'], conf.columns['geom']]:
+        if column not in [conf.columns["id"], conf.columns["geom"]]:
             parceldata_buf_poly_gdf.drop(column, axis=1, inplace=True)
-    logger.info(f"Export parcels that are (multi)polygons after buffer to {output_imagedata_parcel_input_filepath}")
-    geofile_util.to_file(parceldata_buf_poly_gdf, output_imagedata_parcel_input_filepath)
+    logger.info(
+        "Export parcels that are (multi)polygons after buffer to"
+        f"{output_imagedata_parcel_input_path}"
+    )
+    geofile_util.to_file(parceldata_buf_poly_gdf, output_imagedata_parcel_input_path)
     logger.info(parceldata_buf_poly_gdf)
 
-    message = ("The buffered file just has been prepared, so probably you now you probably need " 
-    + "to sync it to the DIAS and start the timeseries data extraction before proceding!")
+    message = (
+        "The buffered file just has been prepared, so probably you now you probably "
+        "need to sync it to the DIAS and start the timeseries data extraction before "
+        "proceding!"
+    )
     logger.warning(message)
     raise Exception(message)
 
+
 def calculate_periodic_data(
-            input_parcel_filepath: Path,
-            input_base_dir: Path,
-            start_date_str: str,   
-            end_date_str: str,    
-            sensordata_to_get: List[str],       
-            dest_data_dir: Path,
-            force: bool = False):
+    input_parcel_path: Path,
+    input_base_dir: Path,
+    start_date_str: str,
+    end_date_str: str,
+    sensordata_to_get: List[str],
+    dest_data_dir: Path,
+    force: bool = False,
+):
     """
-    This function creates a file that is a weekly summarize of timeseries images from DIAS.
+    This function creates a file that is a weekly summarize of timeseries images from
+    DIAS.
 
     TODO: add possibility to choose which values to extract (mean, min, max,...)?
-        
+
     Args:
-        input_parcel_filepath (Path): [description]
+        input_parcel_path (Path): [description]
         input_base_dir (Path): [description]
-        start_date_str (str): Start date in format %Y-%m-%d. Needs to be aligned already on the 
-                periods wanted + data on this date is included.
-        end_date_str (str): End date in format %Y-%m-%d. Needs to be aligned already on the 
-                periods wanted + data on this date is excluded.
-        sensordata_to_get ([]): 
+        start_date_str (str): Start date in format %Y-%m-%d. Needs to be aligned
+            already on the periods wanted + data on this date is included.
+        end_date_str (str): End date in format %Y-%m-%d. Needs to be aligned already on
+            the periods wanted + data on this date is excluded.
+        sensordata_to_get ([]):
         dest_data_dir (Path): [description]
         force (bool, optional): [description]. Defaults to False.
     """
-    logger.info('calculate_periodic_data')
+    logger.info("calculate_periodic_data")
 
     # Init
-    input_dir = input_base_dir / input_parcel_filepath.stem
+    input_dir = input_base_dir / input_parcel_path.stem
 
     # TODO: in config?
     input_ext = ".sqlite"
     output_ext = ".sqlite"
 
-    start_date = datetime.strptime(start_date_str, '%Y-%m-%d') 
-    end_date = datetime.strptime(end_date_str, '%Y-%m-%d') 
-    year = start_date_str.split("-")[0] 
+    start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
+    end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
+    year = start_date_str.split("-")[0]
 
     # Prepare output dir
     test = False
@@ -182,112 +220,136 @@ def calculate_periodic_data(
         dest_data_dir = Path(f"{str(dest_data_dir)}_test")
     if not dest_data_dir.exists():
         os.mkdir(dest_data_dir)
-    
+
     # Create Dataframe with all files with their info
-    logger.debug('Create Dataframe with all files and their properties')
+    logger.debug("Create Dataframe with all files and their properties")
     file_info_list = []
-    for filename in os.listdir(input_dir): 
+    for filename in os.listdir(input_dir):
         if filename.endswith(input_ext):
             # Get seperate filename parts
             file_info = get_file_info(input_dir / filename)
             file_info_list.append(file_info)
-    
+
     all_inputfiles_df = pd.DataFrame(file_info_list)
 
     # Loop over the data we need to get
-    id_column = conf.columns['id']
+    id_column = conf.columns["id"]
     for sensordata_type in sensordata_to_get:
 
-        logger.debug('Get files we need based on start- & stopdates, sensordata_to_get,...')
+        logger.debug(
+            "Get files we need based on start- & stopdates, sensordata_to_get,..."
+        )
         orbits = [None]
-        if sensordata_type == conf.general['SENSORDATA_S1_ASCDESC']:
+        if sensordata_type == conf.general["SENSORDATA_S1_ASCDESC"]:
             # Filter files to the ones we need
-            satellitetype = 'S1'
+            satellitetype = "S1"
             imagetype = IMAGETYPE_S1_GRD
-            bands = ['VV', 'VH']
-            orbits = ['ASC', 'DESC']
-            needed_inputfiles_df = all_inputfiles_df.loc[(all_inputfiles_df.date >= start_date) 
-                                                         & (all_inputfiles_df.date < end_date)
-                                                         & (all_inputfiles_df.imagetype == imagetype)
-                                                         & (all_inputfiles_df.band.isin(bands))
-                                                         & (all_inputfiles_df.orbit.isin(orbits))]
-        elif sensordata_type == conf.general['SENSORDATA_S2gt95']:
-            satellitetype = 'S2'
+            bands = ["VV", "VH"]
+            orbits = ["ASC", "DESC"]
+            needed_inputfiles_df = all_inputfiles_df.loc[
+                (all_inputfiles_df.date >= start_date)
+                & (all_inputfiles_df.date < end_date)
+                & (all_inputfiles_df.imagetype == imagetype)
+                & (all_inputfiles_df.band.isin(bands))
+                & (all_inputfiles_df.orbit.isin(orbits))
+            ]
+        elif sensordata_type == conf.general["SENSORDATA_S2gt95"]:
+            satellitetype = "S2"
             imagetype = IMAGETYPE_S2_L2A
-            bands = ['B02-10m', 'B03-10m', 'B04-10m', 'B08-10m', 'B11-20m', 'B12-20m']
-            needed_inputfiles_df = all_inputfiles_df.loc[(all_inputfiles_df.date >= start_date) 
-                                                         & (all_inputfiles_df.date < end_date) 
-                                                         & (all_inputfiles_df.imagetype == imagetype)                                                         
-                                                         & (all_inputfiles_df.band.isin(bands))]
-        elif sensordata_type == conf.general['SENSORDATA_S1_COHERENCE']:
-            satellitetype = 'S1'
+            bands = ["B02-10m", "B03-10m", "B04-10m", "B08-10m", "B11-20m", "B12-20m"]
+            needed_inputfiles_df = all_inputfiles_df.loc[
+                (all_inputfiles_df.date >= start_date)
+                & (all_inputfiles_df.date < end_date)
+                & (all_inputfiles_df.imagetype == imagetype)
+                & (all_inputfiles_df.band.isin(bands))
+            ]
+        elif sensordata_type == conf.general["SENSORDATA_S1_COHERENCE"]:
+            satellitetype = "S1"
             imagetype = IMAGETYPE_S1_COHERENCE
-            bands = ['VV', 'VH']
-            orbits = ['ASC', 'DESC']
-            needed_inputfiles_df = all_inputfiles_df.loc[(all_inputfiles_df.date >= start_date) 
-                                                         & (all_inputfiles_df.date < end_date) 
-                                                         & (all_inputfiles_df.imagetype == imagetype)                                                         
-                                                         & (all_inputfiles_df.band.isin(bands))]
+            bands = ["VV", "VH"]
+            orbits = ["ASC", "DESC"]
+            needed_inputfiles_df = all_inputfiles_df.loc[
+                (all_inputfiles_df.date >= start_date)
+                & (all_inputfiles_df.date < end_date)
+                & (all_inputfiles_df.imagetype == imagetype)
+                & (all_inputfiles_df.band.isin(bands))
+            ]
         else:
             raise Exception(f"Unsupported sensordata_type: {sensordata_type}")
 
         # There should also be one pixcount file
-        pixcount_filename = f"{input_parcel_filepath.stem}_weekly_pixcount{output_ext}"
-        pixcount_filepath = dest_data_dir / pixcount_filename
+        pixcount_filename = f"{input_parcel_path.stem}_weekly_pixcount{output_ext}"
+        pixcount_path = dest_data_dir / pixcount_filename
 
         # For each week
-        start_week = int(datetime.strftime(start_date , '%W'))
-        end_week = int(datetime.strftime(end_date , '%W'))
+        start_week = int(datetime.strftime(start_date, "%W"))
+        end_week = int(datetime.strftime(end_date, "%W"))
         for period_index in range(start_week, end_week):
 
-            # Get the date of the first day of period period_index (eg. monday for a week)
-            period_date = datetime.strptime(str(year) + '_' + str(period_index) + '_1', '%Y_%W_%w')
+            # Get the date of the first day of period period_index
+            # (eg. monday for a week)
+            period_date = datetime.strptime(
+                str(year) + "_" + str(period_index) + "_1", "%Y_%W_%w"
+            )
 
             # New file name
-            period_date_str_long = period_date.strftime('%Y-%m-%d') 
-            period_data_filename = f"{input_parcel_filepath.stem}_weekly_{period_date_str_long}_{sensordata_type}{output_ext}"
-            period_data_filepath = dest_data_dir / period_data_filename
+            period_date_str_long = period_date.strftime("%Y-%m-%d")
+            period_data_filename = f"{input_parcel_path.stem}_weekly_{period_date_str_long}_{sensordata_type}{output_ext}"
+            period_data_path = dest_data_dir / period_data_filename
 
             # Check if output file exists already
-            if period_data_filepath.exists() and pixcount_filepath.exists():
+            if period_data_path.exists() and pixcount_path.exists():
                 if force is False:
-                    logger.info(f"SKIP: force is False and file exists: {period_data_filepath}")
+                    logger.info(
+                        f"SKIP: force is False and file exists: {period_data_path}"
+                    )
                     continue
                 else:
-                    os.remove(period_data_filepath)
+                    os.remove(period_data_path)
 
             # Loop over bands and orbits (all combinations of bands and orbits!)
             logger.info(f"Calculate file: {period_data_filename}")
             period_data_df = None
-            gc.collect()                  # Try to evade memory errors
+            gc.collect()  # Try to evade memory errors
             for band, orbit in [(band, orbit) for band in bands for orbit in orbits]:
 
                 # Get list of files needed for this period, band
-                period_files_df = needed_inputfiles_df.loc[(needed_inputfiles_df.week == period_index)
-                                                           & (needed_inputfiles_df.band == band)]
-                
+                period_files_df = needed_inputfiles_df.loc[
+                    (needed_inputfiles_df.week == period_index)
+                    & (needed_inputfiles_df.band == band)
+                ]
+
                 # If an orbit to be filtered was specified, filter
                 if orbit is not None:
-                    period_files_df = period_files_df.loc[(period_files_df.orbit == orbit)]
+                    period_files_df = period_files_df.loc[
+                        (period_files_df.orbit == orbit)
+                    ]
 
                 if len(period_files_df) == 0:
                     logger.warn("No input files found!")
 
                 # Loop all period_files
                 period_band_data_df = None
-                statistic_columns_dict = {'count': [], 'max': [], 'mean': [], 'median': [], 'min': [], 'std': []}
-                for j, imagedata_filepath in enumerate(period_files_df.filepath.tolist()):
-                    
+                statistic_columns_dict = {
+                    "count": [],
+                    "max": [],
+                    "mean": [],
+                    "median": [],
+                    "min": [],
+                    "std": [],
+                }
+                for j, imagedata_path in enumerate(period_files_df.path.tolist()):
+
                     # If file has filesize == 0, skip
-                    imagedata_filepath = Path(imagedata_filepath)
-                    if imagedata_filepath.stat().st_size == 0:
-                        continue 
+                    imagedata_path = Path(imagedata_path)
+                    if imagedata_path.stat().st_size == 0:
+                        continue
 
                     # Read the file (but only the columns we need)
                     columns = [column for column in statistic_columns_dict]
                     columns.append(id_column)
 
-                    image_data_df = pdh.read_file(imagedata_filepath, columns=columns)
+                    image_data_df = pdh.read_file(imagedata_path, columns=columns)
                     image_data_df.set_index(id_column, inplace=True)
                     image_data_df.index.name = id_column
 
@@ -296,181 +358,231 @@ def calculate_periodic_data(
                     image_data_df.dropna(inplace=True)
                     nb_after_dropna = len(image_data_df.index)
                     if nb_after_dropna != nb_before_dropna:
-                        logger.warning(f"Before dropna: {nb_before_dropna}, after: {nb_after_dropna} for file {imagedata_filepath}")
+                        logger.warning(
+                            f"Before dropna: {nb_before_dropna}, after: "
+                            f"{nb_after_dropna} for file {imagedata_path}"
+                        )
                     if nb_after_dropna == 0:
                         continue
-                    
+
                     # recalculate duplicate rows (the -5 buffer can cause break ups?)
-                    image_data_recalculate_df = image_data_df.loc[image_data_df.index.duplicated()].groupby(id_column).agg({column: "mean" for column in statistic_columns_dict})
+                    image_data_recalculate_df = (
+                        image_data_df.loc[image_data_df.index.duplicated()]
+                        .groupby(id_column)
+                        .agg({column: "mean" for column in statistic_columns_dict})
+                    )
                     image_data_df = image_data_df.loc[~image_data_df.index.duplicated()]
                     image_data_df.append(image_data_recalculate_df)
 
                     # Rename columns so column names stay unique
                     for statistic_column in statistic_columns_dict:
-                        new_column_name = statistic_column + str(j+1)
-                        image_data_df.rename(columns={statistic_column: new_column_name},
-                                             inplace=True)
-                        image_data_df[new_column_name] = image_data_df[new_column_name].astype(float)
+                        new_column_name = statistic_column + str(j + 1)
+                        image_data_df.rename(
+                            columns={statistic_column: new_column_name}, inplace=True
+                        )
+                        image_data_df[new_column_name] = image_data_df[
+                            new_column_name
+                        ].astype(float)
                         statistic_columns_dict[statistic_column].append(new_column_name)
-                                            
-                    # Create 1 dataframe for all weekfiles - one row for each code_obj - using concat (code_obj = index)
+
+                    # Create 1 dataframe for all weekfiles
+                    #   - one row for each code_obj
+                    #   - using concat (code_obj = index)
                     if period_band_data_df is None:
-                        period_band_data_df = image_data_df                
+                        period_band_data_df = image_data_df
                     else:
-                        period_band_data_df = pd.concat([period_band_data_df, image_data_df], axis=1, sort=False)
+                        period_band_data_df = pd.concat(
+                            [period_band_data_df, image_data_df], axis=1, sort=False
+                        )
                         # Apparently concat removes the index name in some situations
                         period_band_data_df.index.name = id_column
-                        
+
                 # Calculate max, mean, min, ...
                 if period_band_data_df is not None:
-                    logger.debug('Calculate max, mean, min, ...')
-                    period_date_str_short = period_date.strftime('%Y%m%d')
+                    logger.debug("Calculate max, mean, min, ...")
+                    period_date_str_short = period_date.strftime("%Y%m%d")
                     # Remark: prefix column names: sqlite doesn't like a numeric start
                     if orbit is None:
-                        column_basename = f"TS_{period_date_str_short}_{imagetype}_{band}"
+                        column_basename = (
+                            f"TS_{period_date_str_short}_{imagetype}_{band}"
+                        )
                     else:
-                        column_basename = f"TS_{period_date_str_short}_{imagetype}_{orbit}_{band}"
+                        column_basename = (
+                            f"TS_{period_date_str_short}_{imagetype}_{orbit}_{band}"
+                        )
 
                     # Number of pixels
-                    # TODO: onderzoeken hoe aantal pixels best bijgehouden wordt : afwijkingen weglaten ? max nemen ? ...
-                    period_band_data_df[f"{column_basename}_count"] = np.nanmax(period_band_data_df[statistic_columns_dict['count']], axis=1)
+                    # TODO: onderzoeken hoe aantal pixels best bijgehouden wordt:
+                    # afwijkingen weglaten ? max nemen ? ...
+                    period_band_data_df[f"{column_basename}_count"] = np.nanmax(
+                        period_band_data_df[statistic_columns_dict["count"]], axis=1
+                    )
                     # Maximum of all max columns
-                    period_band_data_df[f"{column_basename}_max"] = np.nanmax(period_band_data_df[statistic_columns_dict['max']], axis=1)
+                    period_band_data_df[f"{column_basename}_max"] = np.nanmax(
+                        period_band_data_df[statistic_columns_dict["max"]], axis=1
+                    )
                     # Mean of all mean columns
-                    period_band_data_df[f"{column_basename}_mean"] = np.nanmean(period_band_data_df[statistic_columns_dict['mean']], axis=1)
+                    period_band_data_df[f"{column_basename}_mean"] = np.nanmean(
+                        period_band_data_df[statistic_columns_dict["mean"]], axis=1
+                    )
                     # Mean of all median columns
-                    period_band_data_df[f"{column_basename}_median"] = np.nanmean(period_band_data_df[statistic_columns_dict['median']], axis=1)
+                    period_band_data_df[f"{column_basename}_median"] = np.nanmean(
+                        period_band_data_df[statistic_columns_dict["median"]], axis=1
+                    )
                     # Minimum of all min columns
-                    period_band_data_df[f"{column_basename}_min"] = np.nanmin(period_band_data_df[statistic_columns_dict['min']], axis=1)
+                    period_band_data_df[f"{column_basename}_min"] = np.nanmin(
+                        period_band_data_df[statistic_columns_dict["min"]], axis=1
+                    )
                     # Mean of all std columns
-                    period_band_data_df[f"{column_basename}_std"] = np.nanmean(period_band_data_df[statistic_columns_dict['std']], axis=1)
+                    period_band_data_df[f"{column_basename}_std"] = np.nanmean(
+                        period_band_data_df[statistic_columns_dict["std"]], axis=1
+                    )
                     # Number of Files used
-                    period_band_data_df[f"{column_basename}_used_files"] = period_band_data_df[statistic_columns_dict['max']].count(axis=1)
-                                    
+                    period_band_data_df[
+                        f"{column_basename}_used_files"
+                    ] = period_band_data_df[statistic_columns_dict["max"]].count(axis=1)
+
                     # Only keep the columns we want to keep
-                    columns_to_keep = [f"{column_basename}_count", f"{column_basename}_max", 
-                                    f"{column_basename}_mean", f"{column_basename}_median", f"{column_basename}_min", 
-                                    f"{column_basename}_std", f"{column_basename}_used_files"] 
+                    columns_to_keep = [
+                        f"{column_basename}_count",
+                        f"{column_basename}_max",
+                        f"{column_basename}_mean",
+                        f"{column_basename}_median",
+                        f"{column_basename}_min",
+                        f"{column_basename}_std",
+                        f"{column_basename}_used_files",
+                    ]
                     period_band_data_df = period_band_data_df[columns_to_keep]
 
                     # Merge the data with the other bands/orbits for this period
                     if period_data_df is None:
                         period_data_df = period_band_data_df
                     else:
-                        period_data_df = pd.concat([period_band_data_df, period_data_df], axis=1, sort=False) 
+                        period_data_df = pd.concat(
+                            [period_band_data_df, period_data_df], axis=1, sort=False
+                        )
                         # Apparently concat removes the index name in some situations
                         period_data_df.index.name = id_column
-            
+
             if period_data_df is not None:
                 logger.info(f"Write new file: {period_data_filename}")
-                pdh.to_file(period_data_df, period_data_filepath)
+                pdh.to_file(period_data_df, period_data_path)
 
                 # Create pixcount file if it doesn't exist yet...
-                if not pixcount_filepath.exists():
-                    pixcount_s1s2_column = conf.columns['pixcount_s1s2']
-                   
-                    # Get max count of all count columns available 
-                    columns_to_use = [column for column in period_data_df.columns if column.endswith('_count')]
-                    period_data_df[pixcount_s1s2_column] = np.nanmax(period_data_df[columns_to_use], axis=1)
-                    
+                if not pixcount_path.exists():
+                    pixcount_s1s2_column = conf.columns["pixcount_s1s2"]
+
+                    # Get max count of all count columns available
+                    columns_to_use = [
+                        column
+                        for column in period_data_df.columns
+                        if column.endswith("_count")
+                    ]
+                    period_data_df[pixcount_s1s2_column] = np.nanmax(
+                        period_data_df[columns_to_use], axis=1
+                    )
+
                     pixcount_df = period_data_df[pixcount_s1s2_column]
                     pixcount_df.fillna(value=0, inplace=True)
-                    pdh.to_file(pixcount_df, pixcount_filepath)
+                    pdh.to_file(pixcount_df, pixcount_path)
 
-def get_file_info(filepath: Path) -> dict:
+
+def get_file_info(path: Path) -> dict:
     """
     This function gets info of a timeseries data file.
-    
+
     Args:
-        filepath (Path): The filepath to the file to get info about.
-        
+        path (Path): The path to the file to get info about.
+
     Returns:
         dict: a dict containing info about the file
     """
 
     try:
         # Split name on parcelinfo versus imageinfo
-        filename_splitted = filepath.stem.split("__")
+        filename_splitted = path.stem.split("__")
         filename_parcelinfo = filename_splitted[0]
         filename_imageinfo = filename_splitted[1]
 
         # Extract imageinfo
-        imageinfo_values = filename_imageinfo.split("_")    
+        imageinfo_values = filename_imageinfo.split("_")
 
-        # Satellite 
+        # Satellite
         satellite = imageinfo_values[0]
 
         # Get the date taken from the filename, depending on the satellite type
         # Remark: the datetime is in this format: '20180101T055812'
         imagetype = None
         filedatetime = None
-        if satellite.startswith('S1'):
+        if satellite.startswith("S1"):
             # Check if it is a GRDH image
-            if imageinfo_values[2] == 'GRDH':
+            if imageinfo_values[2] == "GRDH":
                 imagetype = IMAGETYPE_S1_GRD
-                filedatetime = imageinfo_values[4]  
-            elif imageinfo_values[1].startswith('S1'):
+                filedatetime = imageinfo_values[4]
+            elif imageinfo_values[1].startswith("S1"):
                 imagetype = IMAGETYPE_S1_COHERENCE
-                filedatetime = imageinfo_values[2]  
-        elif satellite.startswith('S2'):
+                filedatetime = imageinfo_values[2]
+        elif satellite.startswith("S2"):
             imagetype = IMAGETYPE_S2_L2A
-            filedatetime = imageinfo_values[2]  
+            filedatetime = imageinfo_values[2]
 
-        if(imagetype is None or filedatetime is None):
-            raise Exception(f"Unsupported file: {filepath}")
-        
-        filedate = filedatetime.split("T")[0]  
-        parseddate = datetime.strptime(filedate, '%Y%m%d') 
-        fileweek = int(parseddate.strftime('%W'))
+        if imagetype is None or filedatetime is None:
+            raise Exception(f"Unsupported file: {path}")
 
-        # Get the band 
-        fileband = imageinfo_values[-1]         # =last value
+        filedate = filedatetime.split("T")[0]
+        parseddate = datetime.strptime(filedate, "%Y%m%d")
+        fileweek = int(parseddate.strftime("%W"))
 
-        # For S1 images, get the orbit 
-        if satellite.startswith('S1'):
-            fileorbit = imageinfo_values[-2]    # =2nd last value
+        # Get the band
+        fileband = imageinfo_values[-1]  # =last value
+
+        # For S1 images, get the orbit
+        if satellite.startswith("S1"):
+            fileorbit = imageinfo_values[-2]  # =2nd last value
         else:
             fileorbit = None
 
         # The file paths of these files sometimes are longer than 256
         # characters, so use trick on windows to support this anyway
-        filepath_safe = filepath.as_posix()
-        if(os.name == 'nt'
-           and len(filepath.as_posix()) > 240):
-            if filepath_safe.startswith("//"):
-                filepath_safe = f"//?/UNC/{filepath_safe}"
+        path_safe = path.as_posix()
+        if os.name == "nt" and len(path.as_posix()) > 240:
+            if path_safe.startswith("//"):
+                path_safe = f"//?/UNC/{path_safe}"
             else:
-                filepath_safe = f"//?/{filepath_safe}"
+                path_safe = f"//?/{path_safe}"
 
         filenameparts = {
-                'filepath': filepath_safe,
-                'imagetype': imagetype,
-                'filestem' : filepath.stem,
-                'date' : parseddate,
-                'week' : fileweek,
-                'band' : fileband, 
-                'orbit' : fileorbit} # ASC/DESC
+            "path": path_safe,
+            "imagetype": imagetype,
+            "filestem": path.stem,
+            "date": parseddate,
+            "week": fileweek,
+            "band": fileband,
+            "orbit": fileorbit,
+        }  # ASC/DESC
 
     except Exception as ex:
-        message = f"Error extracting info from filename {filepath}"
+        message = f"Error extracting info from filename {path}"
         logger.exception(message)
         raise Exception(message) from ex
 
     return filenameparts
 
+
 def get_monday(input_date: str) -> datetime:
     """
     This function gets the first monday before the date provided.
-    She is being used to adapt start_date and end_date so they are mondays, so it becomes easier to reuse timeseries data
+    She is being used to adapt start_date and end_date so they are mondays, so it
+    becomes easier to reuse timeseries data
        - inputformat:  %Y-%m-%d
        - outputformat: datetime
     """
-    parseddate = datetime.strptime(input_date, '%Y-%m-%d')
-    year_week = parseddate.strftime('%Y_%W')
-    year_week_monday = datetime.strptime(year_week + '_1', '%Y_%W_%w') 
+    parseddate = datetime.strptime(input_date, "%Y-%m-%d")
+    year_week = parseddate.strftime("%Y_%W")
+    year_week_monday = datetime.strptime(year_week + "_1", "%Y_%W_%w")
     return year_week_monday
 
+
 # If the script is run directly...
 if __name__ == "__main__":
     raise Exception("Not implemented")
-    
\ No newline at end of file


Test patch:
diff --git a/cropclassification/postprocess/html_rapport_test.py b/cropclassification/postprocess/html_rapport_test.py
index 60382f30..842af2c5 100644
--- a/cropclassification/postprocess/html_rapport_test.py
+++ b/cropclassification/postprocess/html_rapport_test.py
@@ -1,20 +1,21 @@
 import os
 from pathlib import Path
 import sys
-[sys.path.append(i) for i in ['.', '..']]
 
+[sys.path.append(i) for i in [".", ".."]]
 import cropclassification.postprocess.classification_reporting as class_report
-import cropclassification.preprocess.classification_preprocess as class_pre
 
-base_dir = Path('x:/Monitoring/Markers/PlayGround/JoeBro')
-input_dir = base_dir / 'InputData'
-class_base_dir = base_dir / "HTML" 
-input_groundtruth_filepath = input_dir / "Prc_BEFL_2018_groundTruth.csv"
-parcel_pixcount_filepath = input_dir / "BEFL2018_bufm10_weekly_pixcount.csv"
+base_dir = Path("x:/Monitoring/Markers/PlayGround/JoeBro")
+input_dir = base_dir / "InputData"
+class_base_dir = base_dir / "HTML"
+input_groundtruth_path = input_dir / "Prc_BEFL_2018_groundTruth.csv"
+parcel_pixcount_path = input_dir / "BEFL2018_bufm10_weekly_pixcount.csv"
 
-parcel_predictions_all_filepath = class_base_dir / f"BEFL2018_bufm10_weekly_predict_all.csv"
-groundtruth_filepath = class_base_dir / "Prc_BEFL_2018_groundTruth_classes.csv"
-report_txt = class_base_dir / f"testje_accuracy_report.txt"
+parcel_predictions_all_path = (
+    class_base_dir / "BEFL2018_bufm10_weekly_predict_all.csv"
+)
+groundtruth_path = class_base_dir / "Prc_BEFL_2018_groundTruth_classes.csv"
+report_txt = class_base_dir / "testje_accuracy_report.txt"
 report_html = Path(str(report_txt).replace(".txt", ".html"))
 
 if report_txt.exists():
@@ -22,14 +23,9 @@
 if report_html.exists():
     os.remove(report_html)
 
-'''class_pre.prepare_input(input_parcel_filepath=input_groundtruth_filepath,
-                            input_filetype='BEFL',
-                            input_parcel_pixcount_filepath=parcel_pixcount_filepath,
-                            input_classtype_to_prepare="MONITORING_LANDCOVER_GROUNDTRUTH",
-                            output_parcel_filepath=groundtruth_filepath,                            
-                            force=True)'''
-
-class_report.write_full_report(parcel_predictions_filepath=parcel_predictions_all_filepath,
-                               output_report_txt=report_txt,
-                               parcel_ground_truth_filepath=groundtruth_filepath,
-                               force=True)
\ No newline at end of file
+class_report.write_full_report(
+    parcel_predictions_geopath=parcel_predictions_all_path,
+    output_report_txt=report_txt,
+    parcel_ground_truth_path=groundtruth_path,
+    force=True,
+)
diff --git a/tests/test_calc_timeseries.py b/tests/test_calc_timeseries.py
index f867e174..88c03b3e 100644
--- a/tests/test_calc_timeseries.py
+++ b/tests/test_calc_timeseries.py
@@ -1,52 +1,74 @@
-import datetime
+from datetime import datetime
 from pathlib import Path
 import sys
-sys.path.insert(0, str(Path(__file__).resolve().parent / '..'))
 
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
 from cropclassification.preprocess import timeseries_calc_dias_onda_per_image as calc_ts
 
+
 def get_testdata_dir() -> Path:
-    return Path(__file__).resolve().parent / 'data'
+    return Path(__file__).resolve().parent / "data"
+
 
 def test_calc_stats_per_image_s1_bs(tmpdir):
-    
+
     # Test raw version
-    input_features_filepath = Path('/srv/data/playground/_inputdata_preprocessed/Prc_BEFL_2020_2020-07-01_bufm5.shp')
-    input_image_filepaths = [Path('/mnt/NAS3/CARD/FLANDERS/S1A/L1TC/2020/07/11/S1A_IW_GRDH_1SDV_20200711T172447_20200711T172512_033410_03DF06_66B7_Orb_RBN_RTN_Cal_TC_20200714T100938.L1TC.CARD')]
-    tmp_dir = Path(tmpdir) / 'raw'
+    input_features_path = Path(
+        "/srv/data/playground/_inputdata_preprocessed/Prc_BEFL_2020_2020-07-01_bufm5.shp"
+    )
+    input_image_paths = [
+        Path(
+            "/mnt/NAS3/CARD/FLANDERS/S1A/L1TC/2020/07/11/S1A_IW_GRDH_1SDV_20200711T172447_20200711T172512_033410_03DF06_66B7_Orb_RBN_RTN_Cal_TC_20200714T100938.L1TC.CARD"
+        )
+    ]
+    tmp_dir = Path(tmpdir) / "raw"
     try:
-        start_time = datetime.datetime.now()
+        start_time = datetime.now()
         calc_ts.calc_stats_per_image(
-                features_filepath=input_features_filepath,
-                id_column='UID',
-                image_paths=input_image_filepaths,
-                bands=['VV', 'VH'],
-                output_dir=tmp_dir,
-                temp_dir=tmp_dir / 'tmp',
-                log_dir=tmp_dir / 'log')
-        print(f"calc_stats_per_image ready in {(datetime.datetime.now()-start_time).total_seconds():.2f}")
+            features_path=input_features_path,
+            id_column="UID",
+            image_paths=input_image_paths,
+            bands=["VV", "VH"],
+            output_dir=tmp_dir,
+            temp_dir=tmp_dir / "tmp",
+            log_dir=tmp_dir / "log",
+        )
+        print(
+            f"calc_stats_per_image ready in {(datetime.now()-start_time).total_seconds():.2f}"
+        )
     except Exception as ex:
-        raise Exception(f"Exception calculating for {input_features_filepath} on {input_image_filepaths}") from ex
+        raise Exception(
+            f"Exception calculating for {input_features_path} on {input_image_paths}"
+        ) from ex
 
     # Test .tif version
-    input_image_filepaths = [Path('/mnt/NAS5/SAMPLES_CARD/S1A_IW_GRDH_1SDV_20200711T172447_20200711T172512_033410_03DF06_66B7_Orb_RBN_RTN_Cal_TC_20200722T143946.L1TC.CARD')]
-    tmp_dir = Path(tmpdir) / 'tif'
+    input_image_paths = [
+        Path(
+            "/mnt/NAS5/SAMPLES_CARD/S1A_IW_GRDH_1SDV_20200711T172447_20200711T172512_033410_03DF06_66B7_Orb_RBN_RTN_Cal_TC_20200722T143946.L1TC.CARD"
+        )
+    ]
+    tmp_dir = Path(tmpdir) / "tif"
     try:
-        start_time = datetime.datetime.now()
+        start_time = datetime.now()
         calc_ts.calc_stats_per_image(
-                features_filepath=input_features_filepath,
-                id_column='UID',
-                image_paths=input_image_filepaths,
-                bands=['VV', 'VH'],
-                output_dir=tmp_dir,
-                temp_dir=tmp_dir / 'tmp',
-                log_dir=tmp_dir / 'log')
-        print(f"calc_stats_per_image ready in {(datetime.datetime.now()-start_time).total_seconds():.2f}")
+            features_path=input_features_path,
+            id_column="UID",
+            image_paths=input_image_paths,
+            bands=["VV", "VH"],
+            output_dir=tmp_dir,
+            temp_dir=tmp_dir / "tmp",
+            log_dir=tmp_dir / "log",
+        )
+        print(
+            f"calc_stats_per_image ready in {(datetime.now()-start_time).total_seconds():.2f}"
+        )
     except Exception as ex:
-        raise Exception(f"Exception calculating for {input_features_filepath} on {input_image_filepaths}") from ex
+        raise Exception(
+            f"Exception calculating for {input_features_path} on {input_image_paths}"
+        ) from ex
+
 
-if __name__ == '__main__':
-    import tempfile
-    tmpdir = '/srv/data/playground/tmp'#tempfile.gettempdir()
+if __name__ == "__main__":
+    tmpdir = "/srv/data/playground/tmp"  # tempfile.gettempdir()
     print(f"tmpdir used for test: {tmpdir}")
     test_calc_stats_per_image_s1_bs(tmpdir)