| name | Routing_and_Cost_Optimization |
| description | Solve payment routing and cost optimization problems on the dabstep dataset. Use this skill for questions like "which card scheme should merchant X steer traffic to for min/max fees?" or "which ACI should fraudulent transactions be moved to for lowest fees?" These questions require matching fee rules against transaction data for a merchant over a specific time period. |
Routing and Cost Optimization
This skill handles two types of optimization questions on the dabstep payment dataset:
- Card scheme routing: "Which card scheme should merchant X steer traffic to in order to pay min/max fees in period Y?"
- ACI optimization: "For merchant X in period Y, which ACI should fraudulent transactions be moved to for lowest possible fees?"
Answer format is always: {selection}:{fee} (e.g., SwiftCharge:37.21 or C:18.58), where fee is the total fee across relevant transactions rounded to 2 decimals.
Data Files
payments.csv: Transaction records (138,236 rows, year=2023 only). Key columns: merchant, card_scheme, day_of_year, is_credit, eur_amount, issuing_country, acquirer_country, aci, has_fraudulent_dispute
fees.json: 1,000 fee rules (card schemes: NexPay, GlobalCard, SwiftCharge, TransactPlus)
merchant_data.json: List of merchant objects with merchant, capture_delay, acquirer, merchant_category_code, account_type
acquirer_countries.csv: Maps acquirer name → country_code
manual.md: Definitions (read first for domain context)
Step-by-Step Algorithm
Step 1: Load data
import json, pandas as pd
df = pd.read_csv('payments.csv')
with open('fees.json') as f: fees = json.load(f)
with open('merchant_data.json') as f: merchants = json.load(f)
acq_countries = pd.read_csv('acquirer_countries.csv')
merchant_info = next(m for m in merchants if m['merchant'] == MERCHANT_NAME)
Step 2: Filter transactions by period
payments.csv uses day_of_year (1–365 for 2023). Convert months:
import pandas as pd
def get_month_range(month):
start = pd.Timestamp(2023, month, 1).dayofyear
end = (pd.Timestamp(2023, month+1, 1).dayofyear - 1) if month < 12 else 365
return start, end
start, end = get_month_range(MONTH_NUMBER)
df_period = df[(df['merchant'] == MERCHANT_NAME) &
(df['day_of_year'] >= start) & (df['day_of_year'] <= end)]
df_period = df[df['merchant'] == MERCHANT_NAME]
Month boundaries for 2023 (non-leap year):
- Jan:1–31, Feb:32–59, Mar:60–90, Apr:91–120, May:121–151, Jun:152–181
- Jul:182–212, Aug:213–243, Sep:244–273, Oct:274–304, Nov:305–334, Dec:335–365
Step 3: Compute period-level merchant metrics
These are needed to match fee rules with monthly_volume and monthly_fraud_level fields:
total_vol = df_period['eur_amount'].sum()
fraud_vol = df_period[df_period['has_fraudulent_dispute']]['eur_amount'].sum()
fraud_rate = fraud_vol / total_vol
def get_volume_category(vol):
if vol < 100_000: return '<100k'
elif vol < 1_000_000: return '100k-1m'
elif vol < 5_000_000: return '1m-5m'
else: return '>5m'
def get_fraud_category(rate):
if rate < 0.072: return '<7.2%'
elif rate < 0.077: return '7.2%-7.7%'
elif rate < 0.083: return '7.7%-8.3%'
else: return '>8.3%'
vol_cat = get_volume_category(total_vol)
fraud_cat = get_fraud_category(fraud_rate)
Step 4: Prepare merchant-level matching fields
def get_capture_delay_category(capture_delay):
"""Convert merchant capture_delay string to fee rule category."""
if capture_delay in ('manual', 'immediate'):
return capture_delay
try:
days = int(capture_delay)
if days < 3: return '<3'
elif days <= 5: return '3-5'
else: return '>5'
except:
return capture_delay
cap_cat = get_capture_delay_category(merchant_info['capture_delay'])
mcc = merchant_info['merchant_category_code']
account_type = merchant_info['account_type']
Step 5: Fee rule matching function (CRITICAL)
Important rules for null/empty handling:
null in any field → matches all values of that field
- Empty list
[] in account_type, merchant_category_code, or aci → also matches all (same as null)
def match_fee_rule(rule, card_scheme, is_credit, aci, intracountry,
account_type, cap_cat, mcc, vol_cat, fraud_cat):
if rule['card_scheme'] != card_scheme:
return False
if rule['account_type'] and account_type not in rule['account_type']:
return False
if rule['capture_delay'] is not None and rule['capture_delay'] != cap_cat:
return False
if rule['monthly_fraud_level'] is not None and rule['monthly_fraud_level'] != fraud_cat:
return False
if rule['monthly_volume'] is not None and rule['monthly_volume'] != vol_cat:
return
rule[] mcc rule[]:
rule[] rule[] != is_credit:
rule[] aci rule[]:
rule[] :
(rule[]) != intracountry:
():
rule[] + rule[] * amount /
intracountry per transaction: intracountry = (row['issuing_country'] == row['acquirer_country'])
Step 6a: Card scheme routing question
Sum fees for each card scheme over the period:
scheme_fees = {}
for scheme in df_period['card_scheme'].unique():
df_s = df_period[df_period['card_scheme'] == scheme]
total = 0.0
for _, row in df_s.iterrows():
intra = (row['issuing_country'] == row['acquirer_country'])
rules = [r for r in fees if match_fee_rule(
r, row['card_scheme'], row['is_credit'], row['aci'], intra,
account_type, cap_cat, mcc, vol_cat, fraud_cat
)]
if rules:
total += min(calc_fee(r, row['eur_amount']) for r in rules)
scheme_fees[scheme] = total
best_scheme = min(scheme_fees, key=scheme_fees.get)
answer = f"{best_scheme}:{scheme_fees[best_scheme]:.2f}"
Step 6b: ACI optimization question (fraudulent transactions)
Only operate on fraudulent transactions. Try each alternative ACI (A–F). Note: ACI 'G' has no fee rules — it's the current state being moved away from.
fraud_df = df_period[df_period['has_fraudulent_dispute']]
n_fraud = len(fraud_df)
aci_results = {}
for test_aci in ['A', 'B', 'C', 'D', 'E', 'F']:
total_fee = 0.0
no_rule_count = 0
for _, row in fraud_df.iterrows():
intra = (row['issuing_country'] == row['acquirer_country'])
rules = [r for r in fees if match_fee_rule(
r, row['card_scheme'], row['is_credit'], test_aci, intra,
account_type, cap_cat, mcc, vol_cat, fraud_cat
)]
if rules:
total_fee += min(calc_fee(r, row['eur_amount']) for r in rules)
else:
no_rule_count += 1
aci_results[test_aci] = {'total': total_fee, 'no_rule': no_rule_count}
valid = {a: v['total'] for a, v in aci_results.items() if v['no_rule'] == 0}
best_aci = min(valid, key=valid.get)
answer = f"{best_aci}:{valid[best_aci]:.2f}"
Key Gotchas
-
MCC not found in fee rules: Many merchant MCCs (e.g., 7997, 7993, 7372) do NOT appear in any fee rule's merchant_category_code list. These merchants are covered by rules with empty [] MCC lists — treat [] as "matches all MCCs" (same as null).
-
Capture delay conversion: Merchant capture_delay values can be numeric strings like "1", "2", "7". Convert them: <3, 3-5, >5. String values "manual" and "immediate" match directly.
-
No matching rule → fee = 0: When no rule matches a transaction, it contributes 0 to the total. For ACI optimization, an ACI with ANY unmatched transaction is excluded from consideration.
-
ACI 'G' has no fee rules: Fee rules only cover ACIs A–F. Transactions with current ACI 'G' simply won't match any rule. For ACI optimization questions, the goal is to find an alternative to the current ACI.
-
Take minimum fee when multiple rules match: If several rules all match a transaction, use the one with the lowest fee.
-
Monthly metrics use the full analysis period: For a monthly question, compute vol_cat and fraud_cat from all transactions of that merchant in that month. For a full-year question, use all transactions in 2023.
-
intracountry is a float in fees.json (1.0 = True, 0.0 = False): Cast with bool(rule['intracountry']) before comparing.
Performance Tip
For large datasets (full year = ~27k transactions), the per-transaction loop can be slow. Consider vectorizing or pre-filtering rules: