{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Location data: how we can use it","metadata":{}},{"cell_type":"markdown","source":"* The long recordings for BirdCLEF 2023 test set were made in **Kenya**. Since most of the train records have **geographic coordinates**, we can try to use this information to train models and then use the coordinates of the central point of Kenya for inference.   \n* We do not know exactly in which geographic coordinate system the coordinates are, and I also didn't find this information on the site https://xeno-canto.org/, but most likely it is [WGS 84](https://gisgeography.com/wgs84-world-geodetic-system/).  \n\n* In this notebook I want to show one possible way of processing geographic coordinates before using them for training.","metadata":{}},{"cell_type":"code","source":"import json\nimport os\nfrom typing import Union\n\nimport geopandas as gpd\nimport geopy.distance\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom IPython.core.interactiveshell import InteractiveShell\nfrom mpl_toolkits import mplot3d\nfrom tqdm import tqdm\nfrom tqdm.notebook import tqdm_notebook\n\ntqdm_notebook.pandas()\nInteractiveShell.ast_node_interactivity = \"all\"","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2023-05-26T15:37:02.163786Z","iopub.execute_input":"2023-05-26T15:37:02.164316Z","iopub.status.idle":"2023-05-26T15:37:02.177244Z","shell.execute_reply.started":"2023-05-26T15:37:02.164279Z","shell.execute_reply":"2023-05-26T15:37:02.176017Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"data_dir = \"/kaggle/input/birdclef-2023\"\ntrain_df = pd.read_csv(os.path.join(data_dir, \"train_metadata.csv\"))\ntrain_df[\"path\"] = train_df[\"filename\"].progress_apply(\n    lambda x: os.path.join(data_dir, \"train_audio\", x)\n)\ntrain_df[\"count\"] = train_df[\"primary_label\"].replace(\n    train_df[\"primary_label\"].value_counts().to_dict()\n)","metadata":{"execution":{"iopub.status.busy":"2023-05-26T15:37:02.179797Z","iopub.execute_input":"2023-05-26T15:37:02.180315Z","iopub.status.idle":"2023-05-26T15:37:02.788795Z","shell.execute_reply.started":"2023-05-26T15:37:02.180256Z","shell.execute_reply":"2023-05-26T15:37:02.787434Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"num_missing_coordinates = train_df[\n    train_df[[\"latitude\", \"longitude\"]].isna().any(axis=1)\n].shape[0]\nprint(\n    f\"There are {num_missing_coordinates} records without coordinates, it is {num_missing_coordinates * 100 /train_df.shape[0]:.2f} percent of all records\"\n)","metadata":{"execution":{"iopub.status.busy":"2023-05-26T15:37:02.790554Z","iopub.execute_input":"2023-05-26T15:37:02.790985Z","iopub.status.idle":"2023-05-26T15:37:02.808799Z","shell.execute_reply.started":"2023-05-26T15:37:02.790950Z","shell.execute_reply":"2023-05-26T15:37:02.807558Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can  use different methods for filling nan-values, for example, by \"primary_label\" or by author.","metadata":{}},{"cell_type":"markdown","source":"# Record's geography","metadata":{}},{"cell_type":"markdown","source":"### Let's see the geographic distribution of train data","metadata":{}},{"cell_type":"code","source":"world = gpd.read_file(gpd.datasets.get_path(\"naturalearth_lowres\"))\n\ngdf = gpd.GeoDataFrame(\n    train_df,\n    geometry=gpd.points_from_xy(train_df.longitude, train_df.latitude),\n)  # in geopandas longitude is x coordinate, latitude - y","metadata":{"execution":{"iopub.status.busy":"2023-05-26T15:37:02.811422Z","iopub.execute_input":"2023-05-26T15:37:02.812346Z","iopub.status.idle":"2023-05-26T15:37:02.863730Z","shell.execute_reply.started":"2023-05-26T15:37:02.812300Z","shell.execute_reply":"2023-05-26T15:37:02.862299Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"ax = world.plot(color=\"white\", edgecolor=\"black\", figsize=(15, 12))\nplt.title(\"Geographic distribution of bird's records in train dataset\")\ngdf.plot(ax=ax, categorical=True, column=\"primary_label\", cmap=\"plasma_r\", markersize=5)\nplt.show();","metadata":{"execution":{"iopub.status.busy":"2023-05-26T15:37:02.865607Z","iopub.execute_input":"2023-05-26T15:37:02.866256Z","iopub.status.idle":"2023-05-26T15:37:07.226759Z","shell.execute_reply.started":"2023-05-26T15:37:02.866219Z","shell.execute_reply":"2023-05-26T15:37:07.225489Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"As we can see, most records are from Europe and Africa","metadata":{}},{"cell_type":"markdown","source":"# Kenya","metadata":{}},{"cell_type":"markdown","source":"#### Since the test audios were made in Kenya, we can calculate the coordinates of Kenya’s central point to use it for inference","metadata":{}},{"cell_type":"code","source":"country = \"Kenya\"\ncentroid = (\n    world[world[\"name\"] == country]\n    .to_crs(crs=3785)[\"geometry\"]\n    .centroid.to_crs(crs=world.crs)\n)\nkenya_longitude = centroid.x.item()\nkenya_latitude = centroid.y.item()\nkenya_coord = (kenya_latitude, kenya_longitude)\nprint(\n    f\"Kenya's centroid coordinates are:\\nlatitude: {kenya_latitude:.3f}\\nlongitude: {kenya_longitude:.3f}\"\n)","metadata":{"execution":{"iopub.status.busy":"2023-05-26T15:37:07.228403Z","iopub.execute_input":"2023-05-26T15:37:07.229532Z","iopub.status.idle":"2023-05-26T15:37:07.307247Z","shell.execute_reply.started":"2023-05-26T15:37:07.229477Z","shell.execute_reply":"2023-05-26T15:37:07.305966Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def calculate_distance(coord1, coord2):\n    latitude1, longitude1 = coord1\n    latitude2, longitude2 = coord2\n    return geopy.distance.geodesic(\n        (latitude1, longitude1), (latitude2, longitude2), ellipsoid=\"WGS-84\"\n    ).km","metadata":{"execution":{"iopub.status.busy":"2023-05-26T15:37:07.309654Z","iopub.execute_input":"2023-05-26T15:37:07.310365Z","iopub.status.idle":"2023-05-26T15:37:07.317099Z","shell.execute_reply.started":"2023-05-26T15:37:07.310319Z","shell.execute_reply":"2023-05-26T15:37:07.315904Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_df[\"distance2Kenya\"] = train_df[[\"latitude\", \"longitude\"]].apply(\n    lambda x: calculate_distance(x, kenya_coord) if all(x == x) else None, axis=1\n)","metadata":{"execution":{"iopub.status.busy":"2023-05-26T15:37:07.318947Z","iopub.execute_input":"2023-05-26T15:37:07.319544Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"distances = (1000, 3000)\nax = world.query('continent == \"Africa\"').plot(\n    color=\"white\", edgecolor=\"black\", figsize=(7, 7)\n)\nworld.query('name == \"Kenya\"').plot(ax=ax, color=\"green\")\nplt.title(\"Birds near Kenya\")\ngdf.query(\"distance2Kenya<=@distances[0]\").plot(\n    ax=ax,\n    categorical=True,\n    label=f\"distance <= {distances[0]}\",\n    color=\"deepskyblue\",\n    markersize=5,\n)\ngdf.query(\"distance2Kenya> @distances[0] and distance2Kenya<=@distances[1]\").plot(\n    ax=ax,\n    categorical=True,\n    label=f\"{distances[0]} < distance <= {distances[1]}\",\n    color=\"royalblue\",\n    markersize=5,\n)\nax.scatter(\n    kenya_longitude, kenya_latitude, label=\"Kenya centroid\", color=\"r\", marker=\"x\"\n)\nplt.legend()\nplt.show();","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### The question is how to feed coordinates to model?  \nThere may be several options:\n - as is, i.e. raw coordinate values,\n - normalize to any range (-1, 1 for example),\n - something else?..","metadata":{}},{"cell_type":"markdown","source":"# latitude & longitude to xyz","metadata":{}},{"cell_type":"markdown","source":"#### One of the possible ways is to convert geographic coordinates to [Cartesian coordinate system](https://en.wikipedia.org/wiki/Cartesian_coordinate_system). ","metadata":{}},{"cell_type":"code","source":"def wgs2cartesian(latitude, longitude):\n    rad_latitude = np.deg2rad(latitude)\n    rad_longitude = np.deg2rad(longitude)\n    x = np.cos(rad_latitude) * np.cos(rad_longitude)\n    y = np.cos(rad_latitude) * np.sin(rad_longitude)\n    z = np.sin(rad_latitude)\n    return np.array([x, y, z], dtype=\"float32\")","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"xyz = np.array(\n    [wgs2cartesian(x, y) for x, y in train_df[[\"latitude\", \"longitude\"]].values]\n)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig = plt.figure(figsize=(10, 10))\nplt.title(\"Coordinates in Cartesian coordinate system\")\nax = plt.axes(projection=\"3d\")\nax.scatter3D(xyz[:, 0], xyz[:, 1], xyz[:, 2], color=\"blue\", alpha=0.3)\nax.view_init(10, 20)\nplt.show();","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### We've got geographic coordinates as x-, y-, z- coordinates on the sphere with radius 1.  \n\n#### We can multiply these coordinates by Earth's radius and we'll get points on Earth's surface (whereas the Earth is not a perfect sphere and it is an approximation).  \n\n#### Let's check, will the simple model be able to calculate the distance between two points.","metadata":{}},{"cell_type":"markdown","source":"# Utils","metadata":{}},{"cell_type":"markdown","source":"### Dataloader","metadata":{}},{"cell_type":"code","source":"class CooordinateDataLoader:\n    \"\"\"Generate random coordinates\"\"\"\n\n    def __init__(\n        self,\n        n_samples: int = 10000,\n        batch_size: int = 500,\n        crs: Union[\"wgs\", \"xyz\"] = \"xyz\",\n        norm_coord: bool = True,\n        norm_dist: bool = True,\n    ):\n        self.n_samples = n_samples\n        self.batch_size = batch_size\n        self.crs = crs\n        self.norm_coord = norm_coord\n        self.norm_dist = norm_dist\n        self.device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n        self.earth_radius = geopy.distance.EARTH_RADIUS\n\n    def __len__(self):\n        return self.n_samples // self.batch_size\n\n    def calculate_distance(self, coordinates):\n        latitude1, longitude1, latitude2, longitude2 = coordinates\n        return geopy.distance.geodesic(\n            (latitude1, longitude1), (latitude2, longitude2), ellipsoid=\"WGS-84\"\n        ).km\n\n    def generate_epoch_data(self):\n        latitude = np.random.uniform(low=-90, high=90, size=(self.n_samples, 2))\n        longitude = np.random.uniform(low=-180, high=180, size=(self.n_samples, 2))\n        coord = np.stack(\n            [latitude[:, 0], longitude[:, 0], latitude[:, 1], longitude[:, 1]], 1\n        )\n        distance = np.array([self.calculate_distance(x) for x in coord])\n        if self.crs == \"xyz\":\n            coord, distance = self.data2xyz(coord, distance)\n        elif self.crs == \"wgs\":\n            coord, distance = self.data2wgs(coord, distance)\n        else:\n            raise ValueError('CRS must be one of [\"wgs\", \"xyz\"]')\n        return coord, distance\n\n    def data2xyz(self, coord, distance):\n        \"\"\"Convert geographic coordinates to cartesian system\"\"\"\n        coord = np.array(\n            [[wgs2cartesian(*x[:2]), wgs2cartesian(*x[2:])] for x in coord]\n        ).reshape(self.n_samples, -1)\n        if self.norm_dist:\n            distance /= self.earth_radius  # normalize distances to range (0, np.pi)\n        if not self.norm_coord:\n            coord *= (\n                self.earth_radius\n            )  # convert points from range (-1, 1) to range (-earth_radius, earth_radius)\n        return coord, distance\n\n    def data2wgs(self, coord, distance):\n        if self.norm_coord:  # normalize coordinates to range (-1, 1)\n            coord[:, [0, 2]] /= 90  # latitude\n            coord[:, [1, 3]] /= 180  # longitude\n        if self.norm_dist:\n            distance /= self.earth_radius  # normalize distances to range (0, np.pi)\n        return coord, distance\n\n    def __iter__(self):\n        coord, distance = self.generate_epoch_data()\n        for i in range(self.batch_size, self.n_samples + 1, self.batch_size):\n            batch_coord = (\n                torch.tensor(coord[i - self.batch_size : i]).float().to(self.device)\n            )\n            batch_distance = (\n                torch.tensor(distance[i - self.batch_size : i]).float().to(self.device)\n            )\n            yield batch_coord, batch_distance","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Train pipeline","metadata":{}},{"cell_type":"code","source":"def train(\n    model,\n    train_loader,\n    val_loader,\n    num_epochs,\n    optimizer,\n    scheduler,\n    loss_fn,\n):\n    history = {\"train_loss\": [], \"val_mae\": []}\n    mae_loss = nn.L1Loss(reduction=\"mean\")\n    for epoch in range(num_epochs):\n        train_bar = tqdm(\n            enumerate(train_loader),\n            total=len(train_loader),\n            desc=f\"Train: epoch #{epoch+1}/{num_epochs}\",\n        )\n        model = model.train()\n        running_loss = 0\n        for n, (batch_coord, batch_distance) in train_bar:\n            optimizer.zero_grad()\n            prediction = model(batch_coord).squeeze()\n            loss = loss_fn(prediction, batch_distance)\n            loss.backward()\n            optimizer.step()\n            running_loss += loss.item()\n            current_loss = running_loss / (n + 1)\n            current_lr = optimizer.param_groups[0][\"lr\"]\n            train_bar.set_postfix(\n                train_loss=f\"{current_loss:0.4f}\",\n                lr=f\"{current_lr:0.6f}\",\n            )\n        history[\"train_loss\"].append(current_loss)\n        val_bar = tqdm(\n            enumerate(val_loader),\n            total=len(val_loader),\n            desc=f\"Val: epoch #{epoch+1}/{num_epochs}\",\n        )\n        model = model.eval()\n        val_running_loss = 0\n        for n, (batch_coord, batch_distance) in val_bar:\n            with torch.no_grad():\n                prediction = model(batch_coord).squeeze()\n            mae = mae_loss(prediction, batch_distance)\n            val_running_loss += mae.item()\n            val_mae = val_running_loss / (n + 1)\n            if val_loader.norm_dist:\n                val_mae *= val_loader.earth_radius  # MAE will always be in km\n            val_bar.set_postfix(\n                val_mae=f\"{val_mae:0.4f}\",\n            )\n        history[\"val_mae\"].append(val_mae)\n        scheduler.step(val_mae)\n    return model, history","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Simple linear model","metadata":{}},{"cell_type":"code","source":"def distance_model(in_features: int = 4, out_features: int = 1024):\n    \"\"\"in_features = 4 for WGS and 6 for cartesian system\"\"\"\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    model = nn.Sequential(\n        nn.Linear(in_features=in_features, out_features=out_features, bias=True),\n        nn.ReLU(),\n        nn.Linear(in_features=out_features, out_features=out_features, bias=True),\n        nn.ReLU(),\n        nn.Linear(in_features=out_features, out_features=1, bias=True),\n    )\n    model.to(device)\n    return model","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# WGS 84","metadata":{}},{"cell_type":"markdown","source":"In order to reduce the size of the notebook's output, I will run here only 10 epochs.  \nBut I'll show the results on 50 epochs with and without normalization for WGS 84 and Cartesian systems.","metadata":{}},{"cell_type":"code","source":"n_train_samples = 50000\nn_val_samples = 10000\nnum_epochs = 10","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"wgs_train_loader = CooordinateDataLoader(\n    n_samples=n_train_samples,\n    crs=\"wgs\",\n    norm_coord=True,\n    norm_dist=True,\n)\nwgs_val_loader = CooordinateDataLoader(\n    n_samples=n_val_samples,\n    crs=\"wgs\",\n    norm_coord=True,\n    norm_dist=True,\n)\n\nmodel = distance_model(in_features=4)\n\nloss_fn = nn.MSELoss(reduction=\"sum\")\noptimizer = torch.optim.AdamW(\n    params=model.parameters(),\n    lr=0.001,\n)\nmae_loss = nn.L1Loss(reduction=\"mean\")\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode=\"min\", factor=0.5, patience=2, verbose=True\n)\n_, wgs_history = train(\n    model=model,\n    train_loader=wgs_train_loader,\n    val_loader=wgs_val_loader,\n    num_epochs=num_epochs,\n    optimizer=optimizer,\n    scheduler=scheduler,\n    loss_fn=loss_fn,\n)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# cartezian","metadata":{}},{"cell_type":"code","source":"cart_train_loader = CooordinateDataLoader(\n    n_samples=n_train_samples,\n    crs=\"xyz\",\n    norm_coord=True,\n    norm_dist=True,\n)\ncart_val_loader = CooordinateDataLoader(\n    n_samples=n_val_samples,\n    crs=\"xyz\",\n    norm_coord=True,\n    norm_dist=True,\n)\n\nmodel = distance_model(in_features=6)\n\nloss_fn = nn.MSELoss(reduction=\"sum\")\noptimizer = torch.optim.AdamW(\n    params=model.parameters(),\n    lr=0.001,\n)\nmae_loss = nn.L1Loss(reduction=\"mean\")\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode=\"min\", factor=0.7, patience=2, verbose=True\n)\ncart_model, cart_history = train(\n    model=model,\n    train_loader=cart_train_loader,\n    val_loader=cart_val_loader,\n    num_epochs=num_epochs,\n    optimizer=optimizer,\n    scheduler=scheduler,\n    loss_fn=loss_fn,\n)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plt.figure(figsize=(15, 7))\nfor n, k in enumerate([\"train_loss\", \"val_mae\"]):\n    plt.subplot(1, 2, n + 1)\n    plt.title(k)\n    plt.plot(cart_history[k], label=\"cartezian\")\n    plt.plot(wgs_history[k], label=\"wgs\")\n    plt.legend()\nplt.show();","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Results on 50 epochs","metadata":{}},{"cell_type":"markdown","source":"For every coordinate system I've trained models of the same architecture with the following data parameters:\n - normalized distance, normalized coordinates,\n - normalized distance, coordinates,\n - distance in km, normalized coordinates,\n - distance in km, coordinates.\n\n___\n\n - Distances were normalized to range(0, np.pi) with dividing by Earth radius.\n - WGS coordinates were normalized to range(-1, 1) with  dividing latitude by 90, longitude by 180.\n - Cartesian coordinates are in range (-1, 1).\n - Non-normalized Cartesian coordinates were multiplied by Earth radius.","metadata":{}},{"cell_type":"code","source":"with open(\"/kaggle/input/wgs-vs-cartesian-results/wgs_history.json\", \"r\") as f:\n    wgs_history = json.load(f)\nwith open(\"/kaggle/input/wgs-vs-cartesian-results/xyz_history.json\", \"r\") as f:\n    xyz_history = json.load(f)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Train loss and val MAE\n\npay attention to the y-axis values","metadata":{}},{"cell_type":"code","source":"for n in range(len(wgs_history)):\n    plt.figure(figsize=(15, 4))\n    wgs_h = wgs_history[n]\n    norm_coord = wgs_h[\"norm_coord\"]\n    norm_dist = wgs_h[\"norm_dist\"]\n    xyz_h = [\n        x\n        for x in xyz_history\n        if x[\"norm_coord\"] == norm_coord and x[\"norm_dist\"] == norm_dist\n    ][0]\n    plt.suptitle(\n        f\"coordinate normalization = {norm_coord}, distance normalization = {norm_dist}\"\n    )\n    plt.subplot(1, 2, 1)\n    plt.title(\"Train loss\")\n    plt.plot(wgs_h[\"train_loss\"], label=\"WGS 84\")\n    plt.plot(xyz_h[\"train_loss\"], label=\"Cartesian\")\n    plt.legend()\n    plt.subplot(1, 2, 2)\n    plt.title(\"Val MAE\")\n    plt.plot(wgs_h[\"val_mae\"], label=\"WGS 84\")\n    plt.plot(xyz_h[\"val_mae\"], label=\"Cartesian\")\n    plt.legend()\nplt.show();","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# MAE ","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(15, 6))\nfor n in range(len(wgs_history)):\n    wgs_h = wgs_history[n]\n    norm_coord = wgs_h[\"norm_coord\"]\n    norm_dist = wgs_h[\"norm_dist\"]\n    xyz_h = [\n        x\n        for x in xyz_history\n        if x[\"norm_coord\"] == norm_coord and x[\"norm_dist\"] == norm_dist\n    ][0]\n    plt.title(\"Val MAE\")\n    plt.plot(\n        wgs_h[\"val_mae\"],\n        label=f\"WGS 84    norm_coord:{norm_coord}, norm_dist:{norm_dist}\",\n    )\n    plt.plot(\n        xyz_h[\"val_mae\"],\n        label=f\"Cartesian norm_coord:{norm_coord}, norm_dist:{norm_dist}\",\n    )\n    plt.ylim(0, 1000)\n    plt.legend()\nplt.show();","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"min_xyz, min_wgs = 1e6, 1e6\nfor n in range(len(wgs_history)):\n    wgs_h = wgs_history[n]\n    norm_coord = wgs_h[\"norm_coord\"]\n    norm_dist = wgs_h[\"norm_dist\"]\n    xyz_h = [\n        x\n        for x in xyz_history\n        if x[\"norm_coord\"] == norm_coord and x[\"norm_dist\"] == norm_dist\n    ][0]\n    min_xyz_mae = np.min(xyz_h[\"val_mae\"])\n    min_xyz = np.min((min_xyz_mae, min_xyz))\n    min_wgs_mae = np.min(wgs_h[\"val_mae\"])\n    min_wgs = np.min((min_wgs_mae, min_wgs))\n    print(f\"norm_coord : {norm_coord}, norm_dist : {norm_dist}\")\n    print(\n        f\"WGS 84: min MAE = {min_wgs_mae:.2f}\\nCartesian: min MAE = {min_xyz_mae:.2f}\\n\"\n    )\nprint(f\"\\nBest WGS 84 MAE: {min_wgs:.2f}\\nBest Cartesian MAE: {min_xyz:.2f}\")","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### As we can see, converting geographic coordinates to Cartesian system allows the model to learn the proximity between two points more confident and precise:\n - the smalles MAE with WGS 84 coordinates is 52.38 km\n - whereas with Cartesian coordinate system MAE is 13.87 km\n - when coordinates and distance are in the same scale the results are much better","metadata":{}},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}