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 |
|
Task Stage Hook to fill the lookupTable with data. |
|
Modification Methods |
|
Modification Method to modify address by 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 |
|
|
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 |
|
Database connection containing the risk class data. |
addressRiskFile |
|
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 |
|
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.