Domain: Financial & Accounting

A corporate finance team is evaluating which business segments contribute most to the company’s risk-adjusted burden from completed transactions.

Data is provided across:
transactions.csv, clients.csv, fx_rates.csv, client_risk_history.csv, risk_table.csv, product_table.csv.

Only valid settled transactions are considered.

A transaction is eligible if:
- status == "SETTLED"
- amount != 0
- 0 ≤ days_to_settle ≤ 45
- sign convention:
  SALE → amount > 0  
  REFUND or CHARGEBACK → amount < 0
- client is not sanctioned (clients.csv)

FX conversion:
Convert all amounts to USD using fx_rates.csv.
If a rate for a currency on txn_date is missing, use the most recent prior available rate for that currency.

Risk tier assignment:
From client_risk_history.csv, select rows where:
effective_start ≤ txn_date ≤ effective_end

If multiple rows match, choose the row with the **latest effective_start**.

Join risk_table.csv to obtain:
tier_weight, risk_mult

Join product_table.csv to obtain:
product_mult

Rolling 14-day deduplication per client_id:
Sort transactions by txn_date ascending, then txn_id ascending.

While transactions remain:
- Let window_start = earliest remaining txn_date
- window_end = window_start + 14 days inclusive
- Within the window, keep the transaction with the largest:
  abs(usd_amount × product_mult)

If there is a tie, break it by:
1) larger (tier_weight × risk_mult)
2) earlier txn_date
3) smaller txn_id

Remove all transactions with txn_date ≤ kept_txn_date + 14 days inclusive.
Repeat.

Burden per retained transaction:
burden =
usd_amount
× tier_weight
× risk_mult
× product_mult
× ln(1 + days_to_settle)
× chargeback_penalty

Where:
chargeback_penalty = 1.15 for CHARGEBACK, else 1.

Aggregate burden by segment.
Rank segments by total burden descending.
Break ties alphabetically.

Let TOP5 be the top-5 segments.

FINAL =
sum of total burden across TOP5 segments,
rounded to six decimals.

Additional diagnostics (must also be computed):

A:
If chargeback_penalty is set to 1 for all retained transactions,
what segment ranks 5th?

B:
If ln(1 + days_to_settle) is replaced with (days_to_settle + 1),
does Retail still rank above Energy? (True/False)

Let P30 and P70 be the 30th and 70th percentiles of days_to_settle across retained transactions, computed using linear interpolation, each rounded to two decimals.

Use **strict interior filtering**:
retain only transactions where:
days_to_settle > P30 AND days_to_settle < P70

Recompute ranking on this subset.

C:
Does the TOP5 list differ from the base ranking? (True/False)

D:
Under this subset ranking,
does the segment in position 3 change? (True/False)

Let MID40_TOP5 be the top-5 segments ranked by total burden under the strict interior filter subset.

Return values:
FINAL
TOP5
A
B
C
D
P30
P70
MID40_TOP5

Grading rubric:
criterion: The model reports FINAL equal to 29456123.122610.
weight: 25
criterion: The model reports TOP5 segments in this order: Retail, Energy, Technology, Manufacturing, Logistics.
weight: 15
criterion: The model reports A equal to Logistics.
weight: 10
criterion: The model reports B equal to True.
weight: 10
criterion: The model reports C equal to True.
weight: 10
criterion: The model reports D equal to False.
weight: 10
criterion: The model reports P30 equal to 6.00.
weight: 5
criterion: The model reports P70 equal to 13.00.
weight: 5
criterion: The model reports MID40_TOP5 equal to Retail, Manufacturing, Technology, Financial_Services, Logistics.
weight: 10

Golden answer:
# Explanation

1. Filter eligible transactions: Keep only SETTLED rows with non-zero amount, 0 ≤ days_to_settle ≤ 45, correct sign conventions (SALE > 0, REFUND/CHARGEBACK < 0), and no sanctioned clients (clients.csv).

2. FX conversion: Multiply each amount by the fx_to_usd rate from fx_rates.csv on txn_date. If no rate exists for that date, use the most recent prior rate for that currency.

3. Risk tier join: From client_risk_history.csv, match rows where effective_start ≤ txn_date ≤ effective_end. If multiple rows match, keep the one with the latest effective_start. Join risk_table.csv for tier_weight and risk_mult.

4. Product multiplier: Join product_table.csv on product_line to get product_mult.

5. Rolling 14-day deduplication per client: Sort by txn_date then txn_id. Repeatedly take the earliest remaining date as window_start, set window_end = window_start + 14 days (inclusive), keep the transaction with the largest abs(usd_amount x product_mult) in the window, breaking ties by larger (tier_weight x risk_mult), then earlier txn_date, then smaller txn_id. Remove all transactions with txn_date ≤ kept_txn_date + 14 days and repeat.

6. Burden: For each retained transaction: usd_amount x tier_weight x risk_mult x product_mult x ln(1 + days_to_settle) x chargeback_penalty (1.15 for CHARGEBACK, else 1).

7. Aggregate and rank: Sum burden by segment, rank descending, break ties alphabetically. TOP5 = top 5 segments. FINAL = sum of their burdens rounded to 6 decimals.

8. Diagnostic A: Recompute with chargeback_penalty = 1 for all transactions; report segment at rank 5.

9. Diagnostic B: Replace ln(1 + days_to_settle) with (days_to_settle + 1); report whether Retail still ranks above Energy.

10. Percentiles and strict interior filter: Compute P30 and P70 of days_to_settle across retained transactions. Apply strict interior filter: keep only rows where days_to_settle > P30 AND days_to_settle < P70. Recompute burden rankings on this subset for MID40_TOP5, C, and D.

11. C: Does the MID40 TOP5 list differ from the base TOP5? (set or order comparison)

12. D: Does the segment in position 3 change between base ranking and MID40 ranking?

# Code

```python
import pandas as pd
import numpy as np

txn = pd.read_csv('transactions.csv')
clients = pd.read_csv('clients.csv')
fx = pd.read_csv('fx_rates.csv')
risk_history = pd.read_csv('client_risk_history.csv')
risk_table = pd.read_csv('risk_table.csv')
product_table = pd.read_csv('product_table.csv')

# Step 1: Filter eligible transactions
txn = txn[txn['status'] == 'SETTLED']
txn = txn[txn['amount'] != 0]
txn = txn[(txn['days_to_settle'] >= 0) & (txn['days_to_settle'] <= 45)]
txn = txn[~((txn['txn_type'] == 'SALE') & (txn['amount'] < 0))]
txn = txn[~((txn['txn_type'].isin(['REFUND', 'CHARGEBACK'])) & (txn['amount'] > 0))]
sanctioned = set(clients[clients['is_sanctioned'] == 1]['client_id'])
txn = txn[~txn['client_id'].isin(sanctioned)]

# Step 2: FX conversion
fx['date'] = pd.to_datetime(fx['date'])
txn['txn_date'] = pd.to_datetime(txn['txn_date'])
fx_sorted = fx.sort_values(['currency', 'date'])
fx_lookup = {}
for _, row in fx_sorted.iterrows():
    fx_lookup.setdefault(row['currency'], []).append((row['date'], row['fx_to_usd']))

def get_fx_rate(currency, date):
    result = None
    for d, r in fx_lookup.get(currency, []):
        if d <= date:
            result = r
        else:
            break
    return result

txn['fx_rate'] = txn.apply(lambda row: get_fx_rate(row['currency'], row['txn_date']), axis=1)
txn['usd_amount'] = txn['amount'] * txn['fx_rate']

# Step 3: Risk tier join
risk_history['effective_start'] = pd.to_datetime(risk_history['effective_start'])
risk_history['effective_end'] = pd.to_datetime(risk_history['effective_end'])
txn_risk = txn.merge(
    risk_history[['client_id', 'effective_start', 'effective_end', 'risk_tier']],
    on='client_id', how='left'
)
txn_risk = txn_risk[
    (txn_risk['txn_date'] >= txn_risk['effective_start']) &
    (txn_risk['txn_date'] <= txn_risk['effective_end'])
]
txn_risk = txn_risk.sort_values('effective_start', ascending=False)
txn_risk = txn_risk.drop_duplicates(subset='txn_id', keep='first')

# Step 4: Join multipliers and segment
txn_risk = txn_risk.merge(risk_table, on='risk_tier', how='left')
txn_risk = txn_risk.merge(product_table, on='product_line', how='left')
txn_risk = txn_risk.merge(clients[['client_id', 'segment']], on='client_id', how='left')

# Step 5: Rolling 14-day deduplication per client
txn_risk['dedup_key'] = (txn_risk['usd_amount'] * txn_risk['product_mult']).abs()
txn_risk['tie_weight'] = txn_risk['tier_weight'] * txn_risk['risk_mult']

retained = []
for client_id, group in txn_risk.groupby('client_id'):
    remaining = group.sort_values(['txn_date', 'txn_id']).reset_index(drop=True)
    while len(remaining) > 0:
        window_end = remaining['txn_date'].min() + pd.Timedelta(days=14)
        window = remaining[remaining['txn_date'] <= window_end]
        candidates = window[window['dedup_key'] == window['dedup_key'].max()]
        if len(candidates) > 1:
            candidates = candidates[candidates['tie_weight'] == candidates['tie_weight'].max()]
        if len(candidates) > 1:
            candidates = candidates[candidates['txn_date'] == candidates['txn_date'].min()]
        if len(candidates) > 1:
            candidates = candidates.sort_values('txn_id')
        kept = candidates.iloc[0]
        retained.append(kept)
        remaining = remaining[remaining['txn_date'] > kept['txn_date'] + pd.Timedelta(days=14)]

retained_df = pd.DataFrame(retained)

# Step 6: Burden calculation
retained_df['chargeback_penalty'] = retained_df['txn_type'].apply(
    lambda x: 1.15 if x == 'CHARGEBACK' else 1.0
)
retained_df['burden'] = (
    retained_df['usd_amount']
    * retained_df['tier_weight']
    * retained_df['risk_mult']
    * retained_df['product_mult']
    * np.log(1 + retained_df['days_to_settle'])
    * retained_df['chargeback_penalty']
)

# Step 7: Aggregate and rank
seg = retained_df.groupby('segment')['burden'].sum().reset_index()
seg = seg.sort_values(['burden', 'segment'], ascending=[False, True])
top5 = seg.head(5)['segment'].tolist()
FINAL = round(seg.head(5)['burden'].sum(), 6)

# Diagnostic A
burden_no_cb = (
    retained_df['usd_amount'] * retained_df['tier_weight'] * retained_df['risk_mult']
    * retained_df['product_mult'] * np.log(1 + retained_df['days_to_settle'])
)
seg_a = retained_df.copy()
seg_a['burden_a'] = burden_no_cb.values
seg_a = seg_a.groupby('segment')['burden_a'].sum().reset_index()
seg_a = seg_a.sort_values(['burden_a', 'segment'], ascending=[False, True])
A = seg_a.iloc[4]['segment']

# Diagnostic B
seg_b = retained_df.copy()
seg_b['burden_b'] = (
    retained_df['usd_amount'] * retained_df['tier_weight'] * retained_df['risk_mult']
    * retained_df['product_mult'] * (retained_df['days_to_settle'] + 1)
    * retained_df['chargeback_penalty']
)
seg_b = seg_b.groupby('segment')['burden_b'].sum().reset_index()
seg_b = seg_b.sort_values(['burden_b', 'segment'], ascending=[False, True]).reset_index(drop=True)
B = seg_b[seg_b['segment'] == 'Retail'].index[0] < seg_b[seg_b['segment'] == 'Energy'].index[0]

# Percentiles and strict interior filter
P30 = retained_df['days_to_settle'].quantile(0.30)
P70 = retained_df['days_to_settle'].quantile(0.70)
mid40 = retained_df[(retained_df['days_to_settle'] > P30) & (retained_df['days_to_settle'] < P70)]
seg_mid = mid40.groupby('segment')['burden'].sum().reset_index()
seg_mid = seg_mid.sort_values(['burden', 'segment'], ascending=[False, True])
mid40_top5 = seg_mid.head(5)['segment'].tolist()

C = top5 != mid40_top5
D = top5[2] != mid40_top5[2]

print(f"FINAL = {FINAL}")
print(f"TOP5 = {top5}")
print(f"A = {A}")
print(f"B = {B}")
print(f"C = {C}")
print(f"D = {D}")
print(f"P30 = {P30:.2f}")
print(f"P70 = {P70:.2f}")
print(f"MID40_TOP5 = {mid40_top5}")
```

# Output

```
FINAL = 29456123.122610

TOP5 =
Retail
Energy
Technology
Manufacturing
Logistics

A = Logistics
B = True
C = True
D = False

P30 = 6.00
P70 = 13.00

MID40_TOP5 =
Retail
Manufacturing
Technology
Financial_Services
Logistics
```