{"id":20486297,"url":"https://github.com/undp-data/sids-data-platform-ml-backend","last_synced_at":"2026-04-20T07:31:54.194Z","repository":{"id":38366690,"uuid":"471389133","full_name":"UNDP-Data/SIDS-data-platform-ML-backend","owner":"UNDP-Data","description":null,"archived":false,"fork":false,"pushed_at":"2022-08-05T11:22:33.000Z","size":1387,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-05T16:40:47.880Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/UNDP-Data.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-03-18T13:58:16.000Z","updated_at":"2022-10-25T11:38:02.000Z","dependencies_parsed_at":"2022-08-25T05:01:43.180Z","dependency_job_id":null,"html_url":"https://github.com/UNDP-Data/SIDS-data-platform-ML-backend","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/UNDP-Data/SIDS-data-platform-ML-backend","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UNDP-Data%2FSIDS-data-platform-ML-backend","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UNDP-Data%2FSIDS-data-platform-ML-backend/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UNDP-Data%2FSIDS-data-platform-ML-backend/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UNDP-Data%2FSIDS-data-platform-ML-backend/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/UNDP-Data","download_url":"https://codeload.github.com/UNDP-Data/SIDS-data-platform-ML-backend/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UNDP-Data%2FSIDS-data-platform-ML-backend/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32037860,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-20T00:18:06.643Z","status":"online","status_checked_at":"2026-04-20T02:00:06.527Z","response_time":94,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-15T16:35:59.368Z","updated_at":"2026-04-20T07:31:54.173Z","avatar_url":"https://github.com/UNDP-Data.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SID ML Backend\n\n## Introduction\nImplementation for the SID ML model execution backend. Currently, implemented only for K-NN Imputes based prediction \nmodel as exposed it as an azure function service inside a Kubernetes cluster. \n\n## System Architecture\n\n\u003cimg src=\"./docs/images/layout2.png?raw=true\" height=\"500px\"\u003e\n\nThe system layout on Azure Kubernetes Service(AKS) is shown in the above diagram. Kubernetes cluster created inside an Azure Virtual Private Network (VNET), and all the external traffic coming through\nAzure load balancer resource configures an external IP address and connects the requested pods to the load balancer backend pool. Load balancing rules are created on the desired ports to allow customers' traffic to reach the application.\n\nThe machine learning model was developed in python and exposed as a RestAPI using FastAPI python framework wrapped inside an Azure function.\nSince it is wrapped as an Azure function, we can deploy it into the Azure cloud in multiple ways. \n1. Azure Function Service - Serverless Platform (Consumer plan)\n2. Azure Kubernetes Service(AKS) - Azure function package in a Docker container and deploy. \n\nIn the AKS context, Currently, we have only a single app service, and it will serve all the client requests. The ML-Backend is developed \nin the way that can support multiple models as a single Kubernetes service or multiple Kubernetes services. \n- Single Service is suitable if we use the same dataset for different models or multiple small datasets.\n- Multiple Services is suitable when we have multiple large datasets. With this approach, we can maximize node utilization. If we need to add a new service, we need to update the Kubernetes manifest and update the cluster.\n  \n### Scalability\nCurrent Kubernetes deployment automatically scales up and down using two ways,\n1. **Cluster Autoscaler** - Watches for pods that can't be scheduled on nodes because of resource constraints. \n   The cluster then automatically increases the number of nodes. The current cluster is configured for a maximum of three nodes and a minimum of one node.\n   Use the following Azure CLI command to update the autoscaler configuration.\n   ```\n   az aks update --resource-group myResourceGroup --name myAKSCluster --update-cluster-autoscaler --min-count 1 --max-count 5\n   ``` \n   \n2. **Horizontal pod autoscaler** - Uses the Metrics Server in a Kubernetes cluster to monitor the resource demand of pods. \n   If an application needs more resources, the number of pods is automatically increased to meet the demand.\n   Current parameters\n   - Minimum Replicas: 1\n   - Maximum Replicas: 5\n   - Target scale CPU utilization percentage: 50\n    \n    These values can update from [k8_keda.yml](deployment/nginxIngress/k8_keda.yml) file\n\n\n   ![Scaling Options](./docs/images/cluster-autoscaler.png?raw=true \"Title\")\n\nPlease refer [link](https://docs.microsoft.com/en-us/azure/aks/cluster-autoscaler) for more information.\n\n\n### Security\nSince the Kubernetes cluster is created inside a private network, an external party does not have direct access to the cluster \nexcept through the load balancer. \n\nAPI CORS enabled only for following domains\n   - https://lenseg.github.io/SIDSDataPlatform/\n   - https://sids-dashboard.github.io/SIDSDataPlatform/\n   - http://localhost\n   - http://localhost:8080\n\nAPI support only GET, POST and OPTIONS HTTP methods. \n\nCurrently, I did not enable any of the extra security features provided by Azure since they have a cost and can configure based on the requirement.\nSecurity features supported by Azure\n- DDoS Protection - [Pricing](https://azure.microsoft.com/en-gb/pricing/details/ddos-protection/)\n- Firewall - [Pricing](https://azure.microsoft.com/en-us/pricing/details/azure-firewall/#pricing)\n\n\n### Fault Tolerance\nSince the current minimum replica count and minimum node count is one, there can be 2~3 minutes of system unavailability\n. If we increase it to above 1, it will minimize the unavailable probability.\nBut the system automatically receivers from any system unavailability.\n\n### Storage\nUsing Azure Storage File Share to store model datasets. File Share mounted on the docker container at the startup as a volume with read and write access. \nAll the pods sharing the same storage. \n\n## Developer Guide\n### Folder Structure\nSource code repository structured in the following way\n\n- **main** - Root RESTAPI implementation and azure function configuration\n- **api_app** - FastAPI application configuration\n- **shared_dataloader** - Shared data loader to share data across multiple models\n- **common**: \n   - utility.py - System wide utility functions \n   - constants.py - System wide Constants \n   - errors.py - Define all the RestAPI custom error messages\n- **deployment**:\n   - setup.sh - Initial Kubernetes cluster related resource creation and configuration\n   - k8_keda.yml - Kubernetes replaces configuration. Can use this file to update cluster configuration. \n     Currently, changes to this file do not automatically deploy into the system via Github Actions. Need to \n     manually run ```kubectl apply -f ./deployment/k8_keda.yml```\n- **models** - Contains individual model implementation, RestAPI endpoints, model-specific constants and message definitions. \n\n## Available models\n- **twolvlImp** : This is an imputation model that for filling missing data for SIDS in country level indicator. In this approach (year-by-year approach) only a single year of data is taken into account for imputation. After which a target column is imputed according to the following methodology. \n  - In the pre-processing stage, the column of the target year is sliced from the original dataset. Then using pandas’ stack function the sliced data frame is reshaped into a country by indicator format. \n  - The new reshaped data frame is split into training and testing data based on the target column (indicator to be imputed). \n  - All columns, except the target column, are ranked by the amount of missing values they contain in the training data frame. For a given column, if more than 40% of the values are missing (i.e., measure = 40) the column will not be considered as a possible predictor and is removed from both training and testing data frames. \n  - Then, missing data in the predictor column is imputed using a standard imputer. The current implementation [KNN imputer](https://scikit-learn.org/stable/modules/generated/sklearn.impute.KNNImputer.html), [simple imputer](https://scikit-learn.org/stable/modules/generated/sklearn.impute.SimpleImputer.html#sklearn.impute.SimpleImputer) and [iterative imputer](https://scikit-learn.org/stable/modules/generated/sklearn.impute.IterativeImputer.html#sklearn.impute.IterativeImputer). \n  - After which dimensionality reduction is performed on the imputed predictors of the training data frame, reducing their number to 10. Available options include Recursive feature selection, principal component analysis and manual selection of features based on prior knowledge. PCA dimensionality reduction is the least computational intensive method and hence, used for generating current results. The selected features are also used to subset the test data frame.\n  - Then the model trainer function is used to fit the selected (or best) model and generate [prediction intervals](https://saattrupdan.github.io/2020-03-01-bootstrap-prediction/). Cross-validation is used to explore the parameter space via gridsearch and measure generalizability via normalized root mean squared error. The models available include, 'Random Forest Regressor', 'Gradient Boost Regressor','Extra tree Regressor'.  Due to the small size of data, both SIDS and non-SIDS are used to train the selected model. In order the accommodate for the small sample size of SIDS, SIDS observation are given higher weight during training using Inverse class weighting.\n- **CountryCorrelation** : contain functions for computing K-Means clustering and correlation on data with missing values.\n  - Correlation Function: Computes and returns countries most closely correlated to the selected SIDS country based on a subset of indicators (sub selected by category) for a given year. The original data is sliced based on category and year then reshaped using pandas’ stack functionality into a country by indicator format.\n  - Cluster Function: Clusters SIDS countries based on k-means algorithm. The original data is sliced based on category of indicators and year then reshaped using pandas’ stack functionality into a country by indicator format. Then k-means in combination with EM algorithm is used to fill gaps and assign countries to k clusters.\n- **Statimpute** : This folder contains the imputer function for interpolating an indicator's value across time for a given SIDS country. The function makes use of pandas [interpolate](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html) method and provides several methods such as linear, quadraitc, spline, etc.\n- **SurveyCluster** : This folder contains models for clustering crisis brueau survey data based on an accompanying metadata strcuture. K-modes clustering for categorical features, K-means clustering for numeric features and K-prototypes clustering for mixed group of features. The elbow method is also implemented to choose an optimal number of clusters.\n \n\n## Deployment\n\n### Setup Azure Kubernetes Cluster\n\n#### Prerequisites\n1. Azure core tools - [link](https://github.com/Azure/azure-functions-core-tools).\n2. Azure CLI - [link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)\n3. Terraform - [link](https://learn.hashicorp.com/tutorials/terraform/install-cli)\n   \n#### New AKS Cluster Creation\n1. Update variables in [main.tf](./deployment/terraform/main.tf). Variable descriptions mentioned in the file [variables.tf](./deployment/terraform/main/variables.tf).\n2. Login to Azure cli by executing ``az login``\n2. Run ``terraform apply`` from the project [deployment/terraform](./deployment/terraform) directory. This will create all the resources and kubernetes yml files. (If this command fail in the first attempt retry another time.)\n3. Upload the dataset files in to Azure file storage datasets container.\n4. Now we can set up CI/CD from GitHub actions. For that you need to add following secrets to GitHub repo,\n    1. ``REGISTRY_USERNAME = \u003ccontainer registry name\u003e``\n    2. ``REGISTRY_PASSWORD = \u003ccontainer registry password``. Get the password for Azure Container registry by executing command ``az acr credential show -n \u003cregistry name\u003e --query 'passwords[0].value' -o tsv``. \n    3. ``AZURE_CREDENTIALS = \u003ccredentials json\u003e``. To get credentials execute command ``az ad sp create-for-rbac --name \u003capp name\u003e --role contributor \\\n                                --scopes /subscriptions/\u003csubscription id\u003e/resourceGroups/\u003cresource group name\u003e \\\n                                --sdk-auth``\n       \n5. Update [main.yml](./.github/workflows/main.yml) env variables based on your values\n6. Commit all the changes to the git repo. It will trigger GitHub workflow and deploy all the Kubernetes services to the AKS cluster.\n7. Cluster will be ready in a few minutes. Can get the cluster public ip from Azure Console Kubernetes Services -\u003e Services and Ingresses -\u003e Ingress -\u003e External Address\n8. View swagger documentation from ``http://\u003cpublic ip\u003e/docs``\n\n#### Deploy on Existing Cluster\nImport your existing resources using ``terraform import \u003cmodule.\u003cmodule_name\u003e.\u003cresource module name\u003e.\u003cresource name\u003e /subscriptions/\u003csubscription id\u003e/\u003cresource address\u003e``. \n   E.g.,: Importing existing resource group named `ml-backend-group`\n   ```\n   terraform import module.dev_cluster.azurerm_resource_group.rg \\\n  /subscriptions/\u003csubscription id\u003e/resourceGroups/ml-backend-group\n   ``` \n#### Add Different Node Pool for Specific Service\n1. Create a new node pool with different configuration by adding below code to the Terraform cluster_creation.tf file. Change values based on your requirement.\n```\nresource \"azurerm_kubernetes_cluster_node_pool\" \"survey\" {\n  name                  = \"survey\"\n  kubernetes_cluster_id = azurerm_kubernetes_cluster.clusterNGINX[0].id\n  vm_size               = \"Standard_DS2_v2\"\n  node_count            = 1\n\n  tags = {\n    Environment = \"Testing\"\n  }\n}\n```\n2. Then we can set specific kubernetes deployment for this node by adding following code under deployment spec. [More](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/)\n```\nnodeSelector:\n    agentpool: survey\n```\n\n### CI/CD\nCI/CD implemented using Github Actions. [config file](./.github/workflows/main.yml). It performs the following actions\n- Build a docker container and push it to the Azure Container Registry (ACR)\n- Perform rollout pod restart in Kubernetes cluster.\n\n### Steps to Add New Model API\n1. Create new python module inside models similar to `sampleapi`\n2. Create a `__init__.py` file inside newly created module and define all the fast api endpoints. `tags` should be the description for the model.\n   Example\n    ```\n    from typing import Optional\n    from fastapi import APIRouter\n    from pydantic import BaseModel, Field\n    \n    \n    class SampleRequest(BaseModel):\n        requiredField: str = Field(..., title=\"This field is required\", example=\"required 123\")\n        optionalField: Optional[str] = Field(None, title=\"This field is optional\", example=\"optional 123\")\n    \n    \n    class SampleResponse(BaseModel):\n        resp1: str\n    \n    \n    router = APIRouter(\n        prefix=\"/sample_model\",\n        tags=[\"Sample Model\"],\n        responses={404: {\"description\": \"Not found\"}},\n    )\n    \n    \n    @router.post('/test_endpoint1', response_model=SampleResponse)\n    async def test_endpoint1(req: SampleRequest):\n        return SampleResponse(resp1=\"Test 1\")\n    \n    \n    @router.post('/test_endpoint2')\n    async def test_endpoint1(name: str):\n        return \"Hi \"+name\n\n   ```\n    This endpoint will automatically add in to the swagger documentation as a new session.\u003cbr\u003e\u003cbr\u003e\n\n    **IMPORTANT: Every model must have APIRouter object named as `router`.**\n    Reason for the above compulsory change:\n    - `router` object used for the FastAPI app routing. \n      If you don't have a variable like this, your model will not visible on the swagger api and REST API endpoints \n      will not available. All other functions and variables can rename and rearrange the way you want.\n    \n3.  Root scope `/params` endpoint returns the all models main endpoint definition. For that you need to add `openapi_extra={MAIN_ENDPOINT_TAG: True}` to the endpoint.\n    E.g.\n    ```\n    @router.post('/test_endpoint1', response_model=SampleResponse, openapi_extra={MAIN_ENDPOINT_TAG: True})\n    async def test_endpoint1(req: SampleRequest):\n        return SampleResponse(resp1=\"Test 1\")\n    ```\n\n4. Message definitions should be derived from `BaseDefinition`. It will  add `required_if` basic validation support to the definition.\n    For readability message definitions can move in to a separate file. \n\n    Add custom validators for message fields at the API level as below. \n    ```\n    @validator('requiredField')\n    def username_alphanumeric(cls, v):\n        assert v.isalnum(), 'must be alphanumeric'\n        return v\n     ```\n    please refer [pydantic validators](https://pydantic-docs.helpmanual.io/usage/validators/) for more information.\n\n\n5. Use `DATASET_PATH` environment variable for dataset loading.\n6. If you are shared same data loading across multiple models, create a data loader in `shared_dataloader` similar to [indicator_dataloader.py](./shared_dataloader/indicator_dataloader.py) and loaded data in the model as below code\n   ```\n   indicatorMeta, datasetMeta, indicatorData = data_loader.load_data(\"\u003cmodel service name\u003e\", data_importer())\n   ```\n7. By default, newly added endpoint will route through default Kubernetes service.\n\n#### Add Model Endpoint as a New Kubernetes Service\n1. It is better to serve as a different service on following reasons\n   - Different datasets. \n   - Different resource usage.\n2. It will enable independent resource configurability and scalability.\n3. Deployment steps as follows,\n   1. Execute command `python deployment/service_gen.py`. Script will request following information. \n      - **Service name** : This will be the Kubernetes service name. Avoid duplicate service names.\n      - **Model folder name** : This will be the folder name that added to the `models` python module.\n      - **Shared Volume** : If you need to have a storage that contains sharable resources across all the pods (ex: datasets). Selected `y` else `n`\n      - **Type of shared volume** :\n         - file - Azure Storage file share\n         - blob - Azure Storage blob storage\n      - **Name of the shared volume**\n         - file - File share name\n         - blob - Container name\n      - **Requested memory** : Amount of memory allocated at the pod startup. [Memory units](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory)\n      - **Requested cpu** : Amount of cpu allocated at the pod startup. [CPU units](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes)\n      - **Memory limit** : Maximum amount of memory allocated for the pod. [Memory units](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory)\n      - **CPU limit** : Maximum amount of cpu allocated for the pod. [CPU units](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes)\n      \n   2. When you develop python model, if you are doing model specific resource loading at the startup, please check the `SERVICE_MODEL` env variable to avoid unnecessary resource usage.\n   3. Once you commit these new changes, Cluster will be automatically update via CI/CD.\n\n## Testing\n\n### Load Testing\n#### Cluster Configuration\nTested using Jmeter tool.\n\n*Node Configuration*\n\n|Size|vCPU|Memory: GiB|Expected network bandwidth (Mbps)|\n| :---: | :---: |:---: |:---: |\n|Azure Standard_DS2_v2|2|7|1500\n\n- Minimum Node Count : 1\n- Maximum Node Count : 5\n\n\n*Pod Configuration*\n   - Pod Resource Limit\n      - CPU: 1\n      - Memory: 2 GiB\n   \n   - Pod Resource Request\n      - CPU: 0.5\n      - Memory: 1 GiB\n\n\n- Minimum Pod Replicas : 1\n- Maximum Pod Replicas : 6\n\n#### Test Scenario\nSent `Simple Request` mentioned below from 20 users within 1 minute, and repeat it for 20 times.\n\n#### Results\n1. Cluster started with single node, single pod.\n2. Within first 40 seconds expected pod count increased to 4 and node count increased to 3.\n3. All extra pods and nodes started within next 1 minute. \n4. Within this period received set of Gateway-Timeout responses. \n5. In next 1 minute expected pod count increased to 6, node count increased to 4.\n6. All pods up and running and responses got stable. \n7. Result as below\n\n| Request Count | Error Rate | Throughput |\n   | :---: | :---: | :---: |\n|400|30%|34.6 /min|\n\n\u003cimg src=\"./docs/images/ResponseTimeGraph.png?raw=true\" height=\"400px\"\u003e\n\n8. 10 minutes after the test, pods and node count dropped to 1.\n\n### Local Environment Setup\n1. Install Azure core tools - [link](https://github.com/Azure/azure-functions-core-tools).\n2. Copy dataset files to the dataset folder in the root directory.\n3. Run `func start host`. This will start the azure function locally.\n4. View swagger API from http://localhost:7071/docs.\n\nSimple  request\n```json\n{\n  \"manual_predictors\": [\"wdi-AG.LND.AGRI.K2\"],\n  \"target_year\": \"2001\",\n  \"target\": \"key-wdi-EG.ELC.ACCS.ZS\",\n  \"interpolator\": \"KNNImputer\",\n  \"scheme\": \"MANUAL\",\n  \"estimators\": 10,\n  \"model\": \"rfr\",\n  \"interval\": \"quantile\"\n}\n```\n\n\nTime-consuming request\n```json\n{\n  \"number_predictor\": 10,\n  \"target_year\": \"2001\",\n  \"target\": \"key-wdi-EG.ELC.ACCS.ZS\",\n  \"interpolator\": \"KNNImputer\",\n  \"scheme\": \"AFS\",\n  \"estimators\": 100,\n  \"model\": \"rfr\",\n  \"interval\": \"quantile\"\n}\n```\n## Notes\n### Deployments - Swagger Documentation\n#### Azure function - consumer plan\nhttps://sidsapi-basic.azurewebsites.net/docs#/\n\n#### Kubernetes - AKS\nhttps://ml-aks-ingress.eastus.cloudapp.azure.com/docs\n\nKubernetes endpoint is faster than consumer plan endpoint.\n\n### Time Estimation Automation\nCurrently, added a log in the two level imputation model and got a moving time average by following query.\n```\nContainerLog\n| parse LogEntry with * \"Time Consumed(s)=\" Time \" \" other_params \" scheme=\u003cSchema.\" scheme_e \": '\" scheme \"'\" remain_params\n| where LogEntry contains \"Time Consumed(s)=\"\n| extend time_parsed    = toint(Time)\n| summarize avg(time_parsed) by scheme\n```\nAutomated the process using [Azure Logic App](./deployment/TimeConsumptionAutomationTaskTemplate.json). Logic app generating a [JSON file](./time_consumption) in `dataset` Azure Storage Container.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fundp-data%2Fsids-data-platform-ml-backend","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fundp-data%2Fsids-data-platform-ml-backend","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fundp-data%2Fsids-data-platform-ml-backend/lists"}