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 |
|
Task Stage Hook to fill the lookup table with data. |
|
Modification Methods |
|
Modification Method to modify addresses based on 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 |
|---|---|---|
|
|
|
| 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 |
|---|---|---|
|
|
Database connection containing the risk class data. |
|
|
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 |
|---|---|
|
postal code |
|
city |
|
street |
|
house number |
|
Flood Hazard Classes, values 1 - 4 |
|
zone of stream (B), coastal (K), island (I) |
|
heavy rain class, values 1 - 3 |
|
seismic hazard estimation, values 1 - 12 |
|
snow load zones, values 1, 1a, 2, 2a, 3, 3a |
|
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_stmtto match the actual database schema and data source. -
SQL index statements: Adjust
sql_createIndexandsql_createIndex2if required. -
SQL DROP statement: Adjust
drop_stmtif required. -
SQL SELECT statement: Adjust
select_stmtaccording to the source table structure. -
SQL INSERT statement: Adjust
insertStmtaccording 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 |
|
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 Code |
postal code |
STRING |
|
City |
city |
STRING |
|
Street |
street |
STRING |
|
House number |
house number |
STRING |
|
GK |
Flood Hazard Classes, values 1 - 4 |
STRING |
|
Zone |
zone of stream (B), coastal (K), island (I) |
STRING |
|
SGK |
heavy rain class, values 1 - 3 |
STRING |
|
EMS |
seismic hazard estimation, values 1 - 12 |
STRING |
|
SLZ |
snow load zones, values 1, 1a, 2, 2a, 3, 3a |
STRING |
|
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:
-
All required index variables are defined.
-
In check 1, the method verifies that each address has values for all relevant risk classes.
-
If risk class values are missing, the lookup table is searched for an identical address.
-
If an identical address is found, the missing risk class values are added to the current data row.
-
If no identical address is found, an exception is thrown.
-
In check 2, the lookup table is searched for an address with identical risk classes.
-
A hash-based selection is used to choose a matching replacement address.
-
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_riskmay 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.