Masking Addresses Based on Risk Classes

Overview

In the insurance industry, addresses are often assigned to different risk zones based on natural hazards such as heavy rain, earthquakes, or floods. These classifications influence how properties are evaluated and insured. For example, houses located in low-risk areas are typically insured differently than properties in high-risk zones.

The goal is to replace sensitive address information with alternative addresses that belong to the same or a similar risk class. This approach helps ensure consistent masking results while maintaining the relevance of the underlying risk data.

This guide describes a workflow-driven implementation for address masking and provides the required mapping table container, lookup table definitions, code examples, and extension points such as parameters, workflows, and stage hooks.

Address and risk class data must be provided by the user. To ensure efficient access during the modification process, the data is imported into XDM. The data can be supplied through a database, a CSV file, or a combination of both. While this guide covers all options, the primary focus is on the database-based approach.

The implementation described in this guide is based on a workflow-driven approach for risk class masking and includes the required mapping table container, lookup table definitions, code examples, and extension points such as parameters, workflows, and stage hooks.

Ready-to-use examples for the Task Stage Hook and the Modification Method are available for download and must be imported into the project environment before use. A detailed description of the download, required setup and configuration is provided throughout this guide. The Mapping Table Container, Workflow, and Modification Set must be created and configured for the specific project environment.

Before proceeding, ensure that the required resources for accessing the address and risk class data are available and properly configured. Depending on the configuration, this may be a valid CSV file, a valid database connection, or both.

Steps

1. Import of the xdm examples

The following ready-to-use examples must be downloaded and imported into the project environment before use. Ensure that all components are imported correctly.

Type

Display Name

Description

download example

Task Stage Hook

Fill MTC for Risk Classes

Task Stage Hook to fill the lookupTable with data.

Fill MTC for Risk Classes

Modification Methods

RiskClasses

Modification Method to modify address by risk classes.

Risk Classes

2. Create the Mapping Table Container

First, we create a mapping table container that is used to store the data for the lookup table.

Open the Mapping Table Container section from the menu and start creating a new container. Choose a unique name for the container. Next, select or create valid credentials for connecting to the XDM mapping table container. The final step is to choose a unique name for the variable.

The following table shows the standard settings for the mapping table container used in this guide:

Name

Credential

Variable

XDM Risk Lookup Table

XDM Lookup Table Credential

risklookupTable

If different names or objects are used, ensure that the corresponding adjustments are made in the subsequent code sections and xdm objects. This guide assumes the naming conventions and selections described above.

To verify the connection, open the Schema Browser from the right menu and check whether the connection to the container has been established successfully.

3. Configuration of the Task Stage Hook

The Hook Fill MTC for Risk Classes is predefined with following parameters:

Display Name

Data Type

Description

addressRiskConnection

CONNECTION

Database connection containing the risk class data.

addressRiskFile

FILE

File containing the data for risk class data.

The parameter addressRiskConnection defines the database connection containing the source data for addresses and risk classes. If a CSV file is used as a data source, configure the parameter addressRiskFile accordingly. Depending on the project setup, either one or both parameters can be used. Remove any parameters that are not required for the selected data source configuration.

In the mapping section, ensure that the mapping table container created in Chapter Creation of the Mapping Table Container is selected.

The guide is based on the following attributes, which must be available in the selected data source, regardless of whether a database or a CSV file is used:

Variable

Description

zip

postal code

city

city

street

street

housenumber

house number

gk

Flood Hazard Classes (1 - 4)

zone

zone of stream (B), coastal (K), island (I)

sgk

heavy rain class (1 - 3)

ems

seismic hazard estimation (1 - 12)

slz

snow load zones (1, 1a, 2, 2a, 3, 3a)

ldv

lightning density (0 - 4)

The groovy code for the hook is as follows and is already predefined :

// imports
import groovy.sql.Sql
import de.ubs.xdm3.batch.modification.MappingTableUtils
import de.ubs.xdm3.script.evaluator.ScriptValidationException


// build raw lookup table
// target  mapping Table container
def utilPOOL = new MappingTableUtils(risklookupTable)

// create table with index
def create_stmt  ="""CREATE TABLE riskByAddresses (
    zip VARCHAR(10) NOT NULL,
    city      VARCHAR(128) NOT NULL,
    street       VARCHAR(128) NOT NULL,
    houseNumber  VARCHAR(5)      NOT NULL,
    GK         VARCHAR(5)   NOT NULL,
    Zone       VARCHAR(5)   NOT NULL,
	SGK        VARCHAR(5)   NOT NULL,
	EMS        VARCHAR(5)   NOT NULL,
	SLZ        VARCHAR(5)   NOT NULL,
	LDV        VARCHAR(5)   NOT NULL
)"""

def sql_createIndex = """
CREATE INDEX IF NOT EXISTS idx_riskClasses ON riskByAddresses (gk,zone,sgk,ems,slz,ldv);
"""
def sql_createIndex2 = """
CREATE INDEX IF NOT EXISTS idx_address ON riskByAddresses (zip, city, street,houseNumber);
"""

def drop_stmt =  "DROP TABLE IF EXISTS riskByAddresses"
utilPOOL.executeStatement(drop_stmt)
utilPOOL.executeStatement(create_stmt)
utilPOOL.executeStatement(sql_createIndex)
utilPOOL.executeStatement(sql_createIndex2)


// data base as data source for risk and address data
// +++++++++++  read from source db
if(taskStageHook.addressRiskConnection){
  def jdbcConn = taskStageHook.addressRiskConnection.jdbcConnection
  Sql sql = Sql.newInstance(jdbcConn)

  String select_stmt = "SELECT zip, city, street, houseNumber, GK, Zone, SGK, EMS, SLZ, LDV FROM public.risklookupTable_source"

  sql.eachRow(select_stmt) { row ->

   def insert_stmt ="""INSERT INTO riskByAddresses (zip, city, street, houseNumber, GK , Zone, SGK, EMS, SLZ, LDV) VALUES
        ('${row.zip}','${row.city}','${row.street}','${row.housenumber}', '${row.GK}', '${row.Zone}', '${row.SGK}','${row.EMS}', '${row.SLZ}', '${row.LDV}')"""
        utilPOOL.executeStatement(insert_stmt)
  }
    sql.close()
}

// optional part
// read from source csv as data source for risk and address data
/*if (taskStageHook.addressRiskFile){
  def source_csv = taskStageHook.addressRiskFile.content  // optional

  source_csv.eachLine { line ->
      def values = line.split(',')

    if (values.size() == 10) {
          def zip = values[0].trim()
          def city = values[1].trim()
          def street = values[2].trim()
          def houseNumber = values[3].trim()
          def  GK = values[4].trim()
          def  Zone = values[5].trim()
          def  SGK = values[6].trim()
          def  EMS = values[7].trim()
          def  SLZ = values[8].trim()
          def  LDV = values[9].trim()

        def insert_stmt_csv = """INSERT INTO riskByAddresses (zip, city, street, houseNumber,  GK , Zone, SGK, EMS, SLZ, LDV) VALUES
       ('${zip}','${city}','${street}','${houseNumber}', '${GK}', '${Zone}', '${SGK}','${EMS}', '${SLZ}', '${LDV}')"""

      utilPOOL.executeStatement(insert_stmt_csv)
      }
  }
}*/

utilPOOL.close()

In the first step, the hook creates a database table that is used as a lookup table, named riskByAddresses, which is contained in the mapping table container XDM Risk Lookup Table. The table indexes are also created. If the table already exists, it is dropped.

In the next step, the data is read from the source database, and each row is written into the target lookup table.

The example implementation is based on a simplified data model and is intended as a template. The source data (addresses and risk classes) may be stored in different database tables, provided through CSV files with varying structures, or supplied by a combination of both sources. Therefore, configuration parameters and SQL statements may need to be adapted to the actual source and target structures.

The following elements should be reviewed and adjusted where necessary:

  • risklookupTable → replace the configured lookup table name with the project-specific lookup table.

  • SQL CREATE Statement → the statement defined in create_stmt may need to be adapted to reflect the actual database schema and data sources.

  • SQL Index statements → the statements defined in sql_createIndex and sql_createIndex may need to be adjusted depending on the target database table structure.

  • SQL DROP statements → the statement defined in drop_stmt may need to be adjusted depending on the target database table structure.

  • SQL SELECT Statement → the statement defined in select_stmt may need to be adjusted according to the target table structure.

  • SQL INSERT statement → the statement defined in insert_stmt may need to be adjusted depending on the target database table structure.

  • CSV File → The code for retrieving address and risk class data from a CSV file is currently commented out. If a CSV file is used as a data source, either exclusively or in combination with a database, the corresponding code sections must be reviewed, adapted, and enabled.

  • SQL INSERT statement → the statement defined in insert_stmt_csv may need to be adjusted depending on the target database table structure.

  • CSV structure → adjust the column order and mapping to match the structure of the input file.

4. Create a Workflow and execute the Hook

To execute the stage hook and populate the lookup table with risk class and address data, a workflow is required. Create a new workflow via the workflow section in the menu to start the process.

The following configurations are applied to this workflow:

In the General Settings section, choose a name for the workflow. In this example, the name is Fill RiskLookupTable.

In the Workflow Call Entries section, add the stage hook by selecting the following settings:

Type

Parent

Object

Task Stage Hook

Parent

Fill MTC for Risk Classes

As the script language, select Groovy in the Libraries section.

Then, go back to the General section to call the hook using HookRunner() in the script section of the workflow:

HookRunner()
   .hook("Fill MTC for Risk Classes")
   .parameter("addressRiskConnection", "Sample testing database as super user")
   .parameter("addressRiskFile", "riskData")
   .run()

The following elements should be reviewed and adjusted where necessary:

  • addressRiskConnection → the name of the database connection containing the address and risk class source data.

  • addressRiskFile → the name of the CSV file containing the address and risk class source data.

In our example, both parameters of the stage hook are taken into account. If a parameter is removed from the stage hook, the corresponding code block responsible for processing the associated data source (database or CSV file) must also be removed.

5. Validate the output

Check the data of the lookup Table risklookupTable of the mapping table container with the schema browser.

6. Mask address data by Modification Method

The next step is to work with the populated lookup table to mask address data. To do so, open the preconfigured modification method example RiskClasses. It is essential that the lookup table exists and is populated beforehand.

The modification method uses type Row, because it changes the whole address with city, street and postcode which are stored in different table columns in one step.

6.1 Parameter

Following parameters are predefined:

Variable

Display Name

Description

Data Tape

zip

ZipCode

postale code

STRING

city

City

city

STRING

street

Street

street

STRING

housenumber

House number

house number

STRING

gk

GK

Flood Hazard Classes (1 - 4)

STRING

zone

Zone

zone of stream (B), coastal (K), island (I)

STRING

sgk

SGK

heavy rain class (1 - 3)

STRING

ems

EMS

seismic hazard estimation (1 - 12)

STRING

slz

SLZ

snow load zones (1, 1a, 2, 2a, 3, 3a)

STRING

ldv

LDV

lightning density (0 - 4)

STRING

6.2 Modification Code

The following code is predefined in the init(), apply() and close() methods:

import de.ubs.xdm3.batch.modification.MappingTableUtils
import de.ubs.xdm3.batch.modification.ModificationException
import de.ubs.xdm3.batch.modification.CompositeKey
import de.ubs.xdm3.batch.modification.HashUtils

import groovy.sql.Sql

xdmLookupTable = 'riskByAddresses'


//idx columns to mask
gk = ctx.indexOf('gk')
zone = ctx.indexOf('zone')
sgk = ctx.indexOf('sgk')
ems = ctx.indexOf('ems')
slz = ctx.indexOf('slz')
ldv = ctx.indexOf('ldv')

zip = ctx.indexOf('zip')
city = ctx.indexOf('city')
street = ctx.indexOf('street')
housenumber = ctx.indexOf('housenumber')


/*
 * This function is called once before the processing starts. The method
 * is called once per table.
 */
def init() {
}

/*
 * This function should modify the row that is provided by the dataArray. The
 * method is called for each single row of the table.
 */
def apply() {
    def sql = Sql.newInstance(risklookupTable)

    // Check 1: each address must have data for all possible risk classes
    def fields_str =  ['gk', 'zone', 'sgk', 'ems', 'slz', 'ldv']
    def fields_idx =  [gk, zone, sgk, ems, slz, ldv]

    if (fields_idx.any { !data[it] }) {
        // ++++ statement to get identical address -> update in case of different data structure
        def stmt_risk = """
            SELECT gk, zone, sgk, ems, slz, ldv
            FROM ${xdmLookupTable}
            WHERE zip = ?
              AND city = ?
              AND street = ?
              AND housenumber = ?
         """

        // Only one row exist with identical address
        def result_address = sql.rows(stmt_risk, [
            data[zip],
            data[city],
            data[street],
            data[housenumber]
        ])

        if (!result_address) {
              throw new ModificationException("No Data for this address in lookup Table" );
         }else{
              result_address.each { row ->
                  for (int i = 0; i < fields_str.size(); i++) {

                      def f = fields_str[i]
                      def f2 = fields_idx[i]

                      if (!data[f2]) {
                          data[f2] = row[i]
                      }
                  }
                }
        }
    }

    // Check 2: get an address from lookupTable, which have identical risk classes
    // ++++ statement to get identical addresses -> update in case of different data structure
    def stmt_getAddress = """
        SELECT zip, city, street, housenumber
        FROM ${xdmLookupTable}
        WHERE gk  = ?
          AND zone = ?
          AND sgk = ?
          AND ems = ?
          AND slz = ?
          AND ldv = ?
       """

     def result_byrisk = sql.rows(stmt_getAddress, [
          data[gk],
          data[zone],
          data[sgk],
          data[ems],
          data[slz],
          data[ldv]
      ])

      // no address found -> thrown exception
      if (!result_byrisk || result_byrisk.size() <= 1) {
            throw new ModificationException("No Data available")
        }

      result_byrisk.each{row -> println(row)}

      def key = "${data[zip]?.toString()?.trim()}${data[city]?.toString()?.trim()}${data[street]?.toString()?.trim()}${data[housenumber]?.toString()?.trim()}"
      def seed = HashUtils.hashStringLikeInt(key)
      def new_data = null

      for (int i = 0; i < result_byrisk.size(); i++) {

            def index = Math.floorMod(seed + i, result_byrisk.size())
            def candidate = result_byrisk[index]

           // normalize data
            def original = [
                data[zip]?.toString()?.trim(),
                data[city]?.toString()?.trim(),
                data[street]?.toString()?.trim(),
                data[housenumber]?.toString()?.trim()
            ]

            def candidateAddress = [
                candidate[zip]?.toString()?.trim(),
                candidate[city]?.toString()?.trim(),
                candidate[street]?.toString()?.trim(),
                candidate[housenumber]?.toString()?.trim()
            ]

            // use only in case of other data
            if (candidateAddress != original) {
                new_data = candidate
                break
            }
        }

       // update data
       data[zip] = new_data[0]
       data[city] = new_data[1]
       data[street] = new_data[2]
       data[housenumber] = new_data[3]

    return true
}

...

The following section explains the functionality of the code:

First, all needed variables for index are defined.

In check 1: Each address should have data for all possible risk classes. If not, check the lookup table for an identical address and retrieve the missing risk classes, adding them to the dataset. If no identical address is found, an exception is thrown.

In check 2: Retrieve an address from the lookup table that has identical risk classes. Use a hash to find a matching entry and select another one as the data to be modified. In the final step, the address is masked using a new address with a similar risk classification.

The method works with a data structure defined in the Hook. If any changes are made, the statement must be adjusted accordingly.

The following elements should be reviewed and adjusted where necessary:

  • index variable → update the index variable names according to the project-specific data structure.

  • lookUp Table → replace the configured lookup table name with the project-specific lookup table.

  • field variables → if the risk class field names differ from those used in the example, the corresponding variables and all related code references must be updated accordingly.

  • SQL SELECT Statement → the statement defined in stmt_risk may need to be adjusted according to the lookup Table structure.

Conclusion

This guide demonstrates a structured approach to building and using a lookup table for risk-based address masking. By combining a configurable workflow, a stage hook, and a flexible data source (database or CSV), the solution ensures consistent and reusable logic for handling risk classifications.

The implementation improves data protection by securely transforming sensitive address information while maintaining alignment with defined risk zones. In addition, the approach supports flexibility and extensibility, allowing the process to be adapted to different data structures and project requirements.

Overall, the solution provides a reliable and scalable framework for risk-based data masking in data-driven applications.