Risk-Based Address Masking

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 risk-based address masking. It 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 both options, the primary focus is on the database-based approach.

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. The Mapping Table Container, Workflow, and Modification Set must be created and configured for the specific project environment.

Prerequisites

Before proceeding, ensure that the required resources for accessing the address and risk class data are available and properly configured.

Depending on the selected setup, this may include:

  • a valid database connection containing address and risk class data

  • a valid CSV file containing address and risk class data

Steps

Step 1: Import the XDM Examples

Download and import the following ready-to-use examples into the project environment before using this guide.

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 lookup table with data.

Fill MTC for Risk Classes

Modification Methods

RiskClasses

Modification Method to modify addresses based on risk classes.

Risk Classes

Step 2: Create the Mapping Table Container

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 create a new container. Choose a unique name for the container. Then select or create valid credentials for connecting to the XDM Mapping Table Container. Finally, choose a unique variable name.

The following table shows the standard settings 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.

Step 3: Configure the Task Stage Hook

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

Display Name Data Type Description

addressRiskConnection

CONNECTION

Database connection containing the risk class data.

addressRiskFile

FILE

File containing the 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 Step 2: Create 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, values 1 - 4

zone

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

sgk

heavy rain class, values 1 - 3

ems

seismic hazard estimation, values 1 - 12

slz

snow load zones, values 1, 1a, 2, 2a, 3, 3a

ldv

lightning density, values 0 - 4

The Groovy code for the hook is predefined as follows:

import groovy.sql.Sql
import de.ubs.xdm3.script.evaluator.ScriptValidationException

// build raw lookup table
// target mapping table container
Sql sql_target = Sql.newInstance(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"

sql_target.execute(drop_stmt)
sql_target.execute(create_stmt)
sql_target.execute(sql_createIndex)
sql_target.execute(sql_createIndex2)

// general insert statement
def insertStmt = """
    INSERT INTO riskByAddresses
    (zip, city, street, houseNumber, GK, Zone, SGK, EMS, SLZ, LDV)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    """

// database as data source for risk and address data
if (taskStageHook.addressRiskConnection) {
    def jdbcConn = taskStageHook.addressRiskConnection.jdbcConnection
    Sql sql_source = Sql.newInstance(jdbcConn)

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

    sql_source.eachRow(select_stmt) { row ->

        sql_target.executeInsert(insertStmt, [
            row.zip,
            row.city,
            row.street,
            row.housenumber,
            row.GK,
            row.Zone,
            row.SGK,
            row.EMS,
            row.SLZ,
            row.LDV
        ])
    }

    sql_source.close()
}

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

    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()

            sql_target.executeInsert(insertStmt, [
                zip,
                city,
                street,
                houseNumber,
                GK,
                Zone,
                SGK,
                EMS,
                SLZ,
                LDV
            ])
        }
    }
}
*/

In the first step, the hook creates a database table named riskByAddresses. This table is used as lookup table and is stored in the Mapping Table Container XDM Risk Lookup Table. The required indexes are also created. If the table already exists, it is dropped and recreated.

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 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: Adjust create_stmt to match the actual database schema and data source.

  • SQL index statements: Adjust sql_createIndex and sql_createIndex2 if required.

  • SQL DROP statement: Adjust drop_stmt if required.

  • SQL SELECT statement: Adjust select_stmt according to the source table structure.

  • SQL INSERT statement: Adjust insertStmt according to the target lookup table structure.

  • CSV file: If a CSV file is used, review, adapt, and enable the commented CSV code block.

  • CSV structure: Adjust the column order and mapping to match the input file.

Step 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.

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 and 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 this 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.

Step 5: Validate the Output

Check the data in the lookup table riskByAddresses of the Mapping Table Container by using the Schema Browser.

Ensure that the table contains the imported address and risk class data before continuing with the Modification Method.

Step 6: Mask Address Data with the Modification Method

The next step is to use the populated lookup table to mask address data.

Open the preconfigured Modification Method example RiskClasses.

It is essential that the lookup table exists and is populated before the Modification Method is executed.

The Modification Method uses type Row, because it changes the complete address, including city, street, postal code, and house number, in one step.

Step 6.1: Review the Parameters

The following parameters are predefined:

Variable Display Name Description Data Type

zip

Zip Code

postal code

STRING

city

City

city

STRING

street

Street

street

STRING

housenumber

House number

house number

STRING

gk

GK

Flood Hazard Classes, values 1 - 4

STRING

zone

Zone

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

STRING

sgk

SGK

heavy rain class, values 1 - 3

STRING

ems

EMS

seismic hazard estimation, values 1 - 12

STRING

slz

SLZ

snow load zones, values 1, 1a, 2, 2a, 3, 3a

STRING

ldv

LDV

lightning density, values 0 - 4

STRING

Step 6.2: Review the 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 = ?
         """

        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 lookup table that has identical risk classes
    // update statement 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 -> throw 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
}

def close() {
}

The code works as follows:

  1. All required index variables are defined.

  2. In check 1, the method verifies that each address has values for all relevant risk classes.

  3. If risk class values are missing, the lookup table is searched for an identical address.

  4. If an identical address is found, the missing risk class values are added to the current data row.

  5. If no identical address is found, an exception is thrown.

  6. In check 2, the lookup table is searched for an address with identical risk classes.

  7. A hash-based selection is used to choose a matching replacement address.

  8. The original address is replaced by the selected address with a similar risk classification.

The method works with the data structure defined in the hook. If the data structure is changed, the SQL statements and field references must be adjusted accordingly.

The following elements should be reviewed and adjusted where necessary:

  • Index variables: 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, update the corresponding variables and all related code references.

  • SQL SELECT statement: The statement defined in stmt_risk may need to be adjusted according to the lookup table structure.

Benefits

This approach provides the following benefits:

  • Sensitive address data can be masked while preserving relevant risk classifications.

  • Replacement addresses are selected based on identical or similar risk classes.

  • The masking logic is reusable and can be adapted to project-specific data structures.

  • The implementation can be extended for additional risk attributes or other classification models.

Conclusion

This guide demonstrates a structured approach to risk-based address masking.

By combining a Mapping Table Container, a workflow, a stage hook, and a Modification Method, sensitive address data can be replaced with alternative addresses that preserve the relevant risk classification.

The approach supports database-based and CSV-based input data and can be adapted to project-specific data structures.

Overall, the solution provides a reusable and scalable framework for risk-based address masking.