Domain: Business Operations & Analytics

You are given a single CSV file named **fifa21_raw_data_v2.csv** containing player data from FIFA 21. Your task is to write Python code that:

* Loads the CSV.
* Computes two KPIs for every player:

  1. **Player_Impact_KPI**
  2. **Value_for_Money_KPI**
* Outputs a JSON object containing the **top 15 players** for each KPI.

KPI Definitions

1. Player_Impact_KPI (0–100)

Compute an intermediate performance score:

perf = 0.45 * OVA + 0.20 * BOV + 0.20 * POT + 0.15 * BaseStats

Then compute the following normalized components using min–max normalization:

* perf_n = minmax(perf)
* value_n = minmax(ln(1 + Value))
* hits_n = minmax(ln(1 + Hits))
* wage_n = minmax(ln(1 + Wage))

Age curve:

age_curve = exp(-((Age - 27)^2) / (2 * 6^2))
age_n = minmax(age_curve)

Market component:

market_n = 0.70 * value_n + 0.30 * hits_n

Final KPI (clamped to [0, 1] before scaling):

kpi1 = 0.55 * perf_n + 0.25 * market_n + 0.15 * age_n - 0.05 * wage_n
Player_Impact_KPI = round(100 * clip(kpi1, 0, 1), 2)

2. Value_for_Money_KPI (0–100)

Use the same perf defined above.

Define an adjusted wage for this KPI:

* Any player with Wage missing or equal to zero is not eligible for this KPI and must have Value_for_Money_KPI = null in output.

For eligible rows:

vfm_raw = ln(1 + clip(perf, 0, infinity)) / ln(1 + Wage)

Then:

Value_for_Money_KPI = round(100 * minmax(vfm_raw), 2)

Normalization Rules (Required)

Implement:

minmax(x) = (x - min(x)) / (max(x) - min(x))

* Min and max must be computed ignoring missing values.
* If max == min (or there are not enough valid values), return missing values for that normalized series.
* Normalization must be performed over the full dataset rows where the source values are valid.

Sorting and tie-breaking (Required)

For each KPI list, sort by:

1. KPI (descending)
2. OVA (descending)
3. POT (descending)
4. ID (ascending)

Both KPI values must be rounded to 2 decimal places.

Use standard JSON types:

* numbers for numeric fields
* strings for text fields
* null for missing values

Output

Your program must print exactly one JSON object with exactly these two keys:

* "top_15_player_impact_kpi"
* "top_15_value_for_money_kpi"

Expected JSON output format (example structure only)

{
"top_15_player_impact_kpi": [
{
"ID": 200389,
"Name": "J. Oblak",
"Nationality": "Slovenia",
"Age": 27,
"Club": "Atlético Madrid",
"OVA": 91,
"POT": 93,
"BOV": 91,
"Value": 120000000.0,
"Wage": 125000.0,
"Hits": 150.0,
"Player_Impact_KPI": 86.55
}
// ... 14 more player objects
],
"top_15_value_for_money_kpi": [
{
"ID": 140293,
"Name": "A. Mirante",
"Nationality": "Italy",
"Age": 36,
"Club": "Roma",
"OVA": 78,
"POT": 78,
"BOV": 78,
"Value": 1700000.0,
"Wage": 500.0,
"Hits": 10.0,
"Value_for_Money_KPI": 100.0
}
// ... 14 more player objects
]
}

Please also provide the JSON generated by your code in the response.

Grading rubric:
criterion: The response includes exactly one Python code block.
weight: 5
criterion: The response includes exactly one JSON object (as plain text) with exactly two top-level keys: top_15_player_impact_kpi and top_15_value_for_money_kpi.
weight: 5
criterion: The JSON output matches the provided expected JSON exactly (character-for-character, including ordering and numeric formatting).
weight: 5
criterion: The code assigns perf using exactly: 0.45*OVA + 0.20*BOV + 0.20*POT + 0.15*Base Stats (same coefficients and column names).
weight: 5
criterion: The code defines age_curve using exactly: exp(-((Age - 27)**2) / (2 * 6**2)) (equivalent algebraic form allowed).
weight: 5
criterion: The code computes Player_Impact_KPI from kpi1 = 0.55*perf_n + 0.25*market_n + 0.15*age_n - 0.05*wage_n, applies clipping to [0,1], scales by 100, and rounds to 2 decimals.
weight: 5
criterion: The code sets Value_for_Money_KPI to null/NaN for every row where Wage is missing or equals 0.
weight: 5
criterion: The code computes Value_for_Money_KPI using vfm_raw = ln(1 + clip(perf,0,∞)) / ln(1 + Wage) followed by min–max scaling and rounding to 2 decimals.
weight: 10
criterion: The JSON list top_15_value_for_money_kpi contains an object with "ID": 251517 and "Value_for_Money_KPI": 97.27 as the 15th element (last position).
weight: 20
criterion: The JSON list top_15_player_impact_kpi contains an object with "ID": 200389 and "Player_Impact_KPI": 86.55 as the 1st element.
weight: 20
criterion: The code sorts each KPI top-15 list by: KPI descending, then OVA descending, then POT descending, then ID ascending.
weight: 5

Golden answer:
```python
import json
import numpy as np
import pandas as pd


# ----------------------------
# Helpers
# ----------------------------
def parse_euro_money(x):
    """'€100M'/'€250K'/'€0' -> float euros"""
    if pd.isna(x):
        return np.nan
    s = str(x).strip().replace(",", "")
    if s in ("", "nan", "None"):
        return np.nan
    s = s.replace("€", "").strip()
    if s == "0":
        return 0.0

    mult = 1.0
    if s.endswith(("K", "k")):
        mult = 1e3
        s = s[:-1]
    elif s.endswith(("M", "m")):
        mult = 1e6
        s = s[:-1]
    elif s.endswith(("B", "b")):
        mult = 1e9
        s = s[:-1]

    try:
        return float(s) * mult
    except ValueError:
        return np.nan


def parse_hits(x):
    """'123' or '1.2K' -> float"""
    if pd.isna(x):
        return np.nan
    s = str(x).strip().replace(",", "")
    if s == "":
        return np.nan
    mult = 1.0
    if s.endswith(("K", "k")):
        mult = 1e3
        s = s[:-1]
    try:
        return float(s) * mult
    except ValueError:
        return np.nan


def minmax(series):
    s = pd.to_numeric(series, errors="coerce")
    mn, mx = s.min(skipna=True), s.max(skipna=True)
    if pd.isna(mn) or pd.isna(mx) or mx == mn:
        return pd.Series(np.nan, index=s.index)
    return (s - mn) / (mx - mn)


def top15_json_with_tiebreaks(df, score_col, fields):

    """

    Sort rules:

      1) KPI desc

      2) OVA desc

      3) POT desc

      4) ID asc

    """

    sorted_df = df.sort_values(

        by=[score_col, "OVA", "POT", "ID"],

        ascending=[False, False, False, True],

        kind="mergesort"  # stable sort for deterministic results

    ).head(15)

    return sorted_df[fields].replace({np.nan: None}).to_dict(orient="records")


# ----------------------------
# Load
# ----------------------------
path = "fifa21_raw_data_v2.csv"
df = pd.read_csv(path, low_memory=False)

# Clean the OVA column name if needed
if "↓OVA" in df.columns and "OVA" not in df.columns:
    df = df.rename(columns={"↓OVA": "OVA"})

# ----------------------------
# Parse-in-place: ONLY columns needed for 2 KPIs + clean Club for nicer output
# ----------------------------
if "Club" in df.columns:
    df["Club"] = df["Club"].astype(str).str.replace("\n", "", regex=False).str.strip()
    df.loc[df["Club"].isin(["nan", "None"]), "Club"] = np.nan

# Parse money columns IN PLACE (overwrite strings with numeric euros)
for col in ["Value", "Wage"]:
    if col in df.columns:
        df[col] = df[col].apply(parse_euro_money)
    else:
        df[col] = np.nan

# Parse Hits IN PLACE (overwrite string with numeric)
if "Hits" in df.columns:
    df["Hits"] = df["Hits"].apply(parse_hits)
else:
    df["Hits"] = np.nan

# Make sure performance inputs are numeric
for col in ["OVA", "BOV", "POT", "Base Stats", "Age"]:
    if col in df.columns:
        df[col] = pd.to_numeric(df[col], errors="coerce")
    else:
        df[col] = np.nan

# ----------------------------
# KPI 1: Player_Impact_KPI (0-100)
# Uses: OVA, BOV, POT, Base Stats, Value, Hits, Age, Wage (penalty)
# ----------------------------
perf = (
    0.45 * df["OVA"] +
    0.20 * df["BOV"] +
    0.20 * df["POT"] +
    0.15 * df["Base Stats"]
)
perf_n = minmax(perf)

value_log_n = minmax(np.log1p(df["Value"]))
hits_log_n  = minmax(np.log1p(df["Hits"]))
market_n = 0.70 * value_log_n + 0.30 * hits_log_n

age_curve = np.exp(-((df["Age"] - 27.0) ** 2) / (2 * (6.0 ** 2)))
age_n = minmax(age_curve)

wage_log_n = minmax(np.log1p(df["Wage"]))

kpi1 = (0.55 * perf_n + 0.25 * market_n + 0.15 * age_n - 0.05 * wage_log_n)
df["Player_Impact_KPI"] = (kpi1.clip(0, 1) * 100).round(2)

# ----------------------------
# KPI 2: Value_for_Money_KPI (0-100)
# Fix: Wage==0 treated as missing so it doesn't dominate rankings
# (No extra columns: we use a temporary Series)
# ----------------------------
wage_for_vfm = df["Wage"].replace(0, np.nan)
vfm_raw = np.log1p(perf.clip(lower=0)) / np.log1p(wage_for_vfm)

df["Value_for_Money_KPI"] = (minmax(vfm_raw) * 100).round(2)

# Remove VFM when wage is missing/zero (so free agents don't top the list)
df.loc[df["Wage"].isna() | (df["Wage"] == 0), "Value_for_Money_KPI"] = np.nan

# ----------------------------
# Output JSON: top 15 for each KPI
# ----------------------------
fields_common = ["ID", "Name", "Nationality", "Age", "Club", "OVA", "POT", "BOV", "Value", "Wage", "Hits"]

result = {
    "top_15_player_impact_kpi": top15_json_with_tiebreaks(
        df=df[df["Player_Impact_KPI"].notna()],
        score_col="Player_Impact_KPI",
        fields=fields_common + ["Player_Impact_KPI"]
    ),
    "top_15_value_for_money_kpi": top15_json_with_tiebreaks(
        df=df[df["Value_for_Money_KPI"].notna()],
        score_col="Value_for_Money_KPI",
        fields=fields_common + ["Value_for_Money_KPI"]
    )
}

print(json.dumps(result, indent=2, ensure_ascii=False))

```

And the JSON output as required:

```JSON
{
  "top_15_player_impact_kpi": [
    {
      "ID": 200389,
      "Name": "J. Oblak",
      "Nationality": "Slovenia",
      "Age": 27,
      "Club": "Atlético Madrid",
      "OVA": 91,
      "POT": 93,
      "BOV": 91,
      "Value": 120000000.0,
      "Wage": 125000.0,
      "Hits": 150.0,
      "Player_Impact_KPI": 86.55
    },
    {
      "ID": 212831,
      "Name": "Alisson",
      "Nationality": "Brazil",
      "Age": 27,
      "Club": "Liverpool",
      "OVA": 90,
      "POT": 91,
      "BOV": 90,
      "Value": 102000000.0,
      "Wage": 160000.0,
      "Hits": 120.0,
      "Player_Impact_KPI": 85.45
    },
    {
      "ID": 192985,
      "Name": "K. De Bruyne",
      "Nationality": "Belgium",
      "Age": 29,
      "Club": "Manchester City",
      "OVA": 91,
      "POT": 91,
      "BOV": 91,
      "Value": 129000000.0,
      "Wage": 370000.0,
      "Hits": 207.0,
      "Player_Impact_KPI": 84.95
    },
    {
      "ID": 210257,
      "Name": "Ederson",
      "Nationality": "Brazil",
      "Age": 26,
      "Club": "Manchester City",
      "OVA": 88,
      "POT": 91,
      "BOV": 88,
      "Value": 92000000.0,
      "Wage": 195000.0,
      "Hits": 103.0,
      "Player_Impact_KPI": 84.87
    },
    {
      "ID": 192448,
      "Name": "M. ter Stegen",
      "Nationality": "Germany",
      "Age": 28,
      "Club": "FC Barcelona",
      "OVA": 90,
      "POT": 93,
      "BOV": 90,
      "Value": 110000000.0,
      "Wage": 260000.0,
      "Hits": 130.0,
      "Player_Impact_KPI": 84.83
    },
    {
      "ID": 212198,
      "Name": "Bruno Fernandes",
      "Nationality": "Portugal",
      "Age": 25,
      "Club": "Manchester United",
      "OVA": 87,
      "POT": 90,
      "BOV": 88,
      "Value": 105500000.0,
      "Wage": 195000.0,
      "Hits": 1500.0,
      "Player_Impact_KPI": 83.89
    },
    {
      "ID": 209331,
      "Name": "M. Salah",
      "Nationality": "Egypt",
      "Age": 28,
      "Club": "Liverpool",
      "OVA": 90,
      "POT": 90,
      "BOV": 90,
      "Value": 120500000.0,
      "Wage": 250000.0,
      "Hits": 246.0,
      "Player_Impact_KPI": 83.52
    },
    {
      "ID": 208722,
      "Name": "S. Mané",
      "Nationality": "Senegal",
      "Age": 28,
      "Club": "Liverpool",
      "OVA": 90,
      "POT": 90,
      "BOV": 90,
      "Value": 120500000.0,
      "Wage": 250000.0,
      "Hits": 189.0,
      "Player_Impact_KPI": 83.18
    },
    {
      "ID": 190871,
      "Name": "Neymar Jr",
      "Nationality": "Brazil",
      "Age": 28,
      "Club": "Paris Saint-Germain",
      "OVA": 91,
      "POT": 91,
      "BOV": 91,
      "Value": 132000000.0,
      "Wage": 270000.0,
      "Hits": 595.0,
      "Player_Impact_KPI": 82.9
    },
    {
      "ID": 212622,
      "Name": "J. Kimmich",
      "Nationality": "Germany",
      "Age": 25,
      "Club": "FC Bayern München",
      "OVA": 88,
      "POT": 90,
      "BOV": 88,
      "Value": 103000000.0,
      "Wage": 145000.0,
      "Hits": 339.0,
      "Player_Impact_KPI": 82.64
    },
    {
      "ID": 195864,
      "Name": "P. Pogba",
      "Nationality": "France",
      "Age": 27,
      "Club": "Manchester United",
      "OVA": 86,
      "POT": 87,
      "BOV": 86,
      "Value": 77500000.0,
      "Wage": 190000.0,
      "Hits": 538.0,
      "Player_Impact_KPI": 82.44
    },
    {
      "ID": 212462,
      "Name": "Alex Telles",
      "Nationality": "Brazil",
      "Age": 27,
      "Club": "Manchester United",
      "OVA": 84,
      "POT": 85,
      "BOV": 84,
      "Value": 44500000.0,
      "Wage": 145000.0,
      "Hits": 2300.0,
      "Player_Impact_KPI": 82.31
    },
    {
      "ID": 203376,
      "Name": "V. van Dijk",
      "Nationality": "Netherlands",
      "Age": 28,
      "Club": "Liverpool",
      "OVA": 90,
      "POT": 91,
      "BOV": 90,
      "Value": 113000000.0,
      "Wage": 210000.0,
      "Hits": 321.0,
      "Player_Impact_KPI": 82.26
    },
    {
      "ID": 192119,
      "Name": "T. Courtois",
      "Nationality": "Belgium",
      "Age": 28,
      "Club": "Real Madrid",
      "OVA": 89,
      "POT": 90,
      "BOV": 89,
      "Value": 82000000.0,
      "Wage": 250000.0,
      "Hits": 96.0,
      "Player_Impact_KPI": 81.63
    },
    {
      "ID": 200145,
      "Name": "Casemiro",
      "Nationality": "Brazil",
      "Age": 28,
      "Club": "Real Madrid",
      "OVA": 89,
      "POT": 89,
      "BOV": 89,
      "Value": 90500000.0,
      "Wage": 310000.0,
      "Hits": 175.0,
      "Player_Impact_KPI": 81.48
    }
  ],
  "top_15_value_for_money_kpi": [
    {
      "ID": 140293,
      "Name": "A. Mirante",
      "Nationality": "Italy",
      "Age": 36,
      "Club": "Roma",
      "OVA": 78,
      "POT": 78,
      "BOV": 78,
      "Value": 1700000.0,
      "Wage": 500.0,
      "Hits": 10.0,
      "Value_for_Money_KPI": 100.0
    },
    {
      "ID": 142902,
      "Name": "A. Pyatov",
      "Nationality": "Ukraine",
      "Age": 36,
      "Club": "Shakhtar Donetsk",
      "OVA": 76,
      "POT": 76,
      "BOV": 76,
      "Value": 825000.0,
      "Wage": 500.0,
      "Hits": 7.0,
      "Value_for_Money_KPI": 99.62
    },
    {
      "ID": 194209,
      "Name": "Y. El Arabi",
      "Nationality": "Morocco",
      "Age": 33,
      "Club": "Olympiacos CFP",
      "OVA": 78,
      "POT": 78,
      "BOV": 78,
      "Value": 6500000.0,
      "Wage": 500.0,
      "Hits": 8.0,
      "Value_for_Money_KPI": 99.22
    },
    {
      "ID": 234112,
      "Name": "Dodô",
      "Nationality": "Brazil",
      "Age": 21,
      "Club": "Shakhtar Donetsk",
      "OVA": 72,
      "POT": 86,
      "BOV": 73,
      "Value": 5500000.0,
      "Wage": 500.0,
      "Hits": 340.0,
      "Value_for_Money_KPI": 98.84
    },
    {
      "ID": 252931,
      "Name": "L. Ivanušec",
      "Nationality": "Croatia",
      "Age": 21,
      "Club": "Dinamo Zagreb",
      "OVA": 71,
      "POT": 79,
      "BOV": 74,
      "Value": 4200000.0,
      "Wage": 500.0,
      "Hits": 15.0,
      "Value_for_Money_KPI": 98.7
    },
    {
      "ID": 246618,
      "Name": "A. Hložek",
      "Nationality": "Czech Republic",
      "Age": 17,
      "Club": "Sparta Praha",
      "OVA": 74,
      "POT": 87,
      "BOV": 75,
      "Value": 9500000.0,
      "Wage": 500.0,
      "Hits": 602.0,
      "Value_for_Money_KPI": 98.65
    },
    {
      "ID": 259049,
      "Name": "J. Stejskal",
      "Nationality": "Czech Republic",
      "Age": 23,
      "Club": "SK Slavia Praha",
      "OVA": 70,
      "POT": 78,
      "BOV": 70,
      "Value": 3100000.0,
      "Wage": 500.0,
      "Hits": 7.0,
      "Value_for_Money_KPI": 98.34
    },
    {
      "ID": 230598,
      "Name": "J. Carreño",
      "Nationality": "Chile",
      "Age": 23,
      "Club": "Oriente Petrolero",
      "OVA": 69,
      "POT": 77,
      "BOV": 72,
      "Value": 3100000.0,
      "Wage": 500.0,
      "Hits": 27.0,
      "Value_for_Money_KPI": 97.97
    },
    {
      "ID": 198653,
      "Name": "M. Silva",
      "Nationality": "Uruguay",
      "Age": 37,
      "Club": "Club Libertad",
      "OVA": 73,
      "POT": 73,
      "BOV": 73,
      "Value": 350000.0,
      "Wage": 500.0,
      "Hits": 3.0,
      "Value_for_Money_KPI": 97.53
    },
    {
      "ID": 212137,
      "Name": "A. Vindheim",
      "Nationality": "Norway",
      "Age": 24,
      "Club": "Sparta Praha",
      "OVA": 71,
      "POT": 76,
      "BOV": 71,
      "Value": 2600000.0,
      "Wage": 500.0,
      "Hits": 10.0,
      "Value_for_Money_KPI": 97.5
    },
    {
      "ID": 253321,
      "Name": "M. Corozo",
      "Nationality": "Ecuador",
      "Age": 27,
      "Club": "LDU Quito",
      "OVA": 69,
      "POT": 72,
      "BOV": 71,
      "Value": 1600000.0,
      "Wage": 500.0,
      "Hits": 8.0,
      "Value_for_Money_KPI": 97.49
    },
    {
      "ID": 247103,
      "Name": "D. Hancko",
      "Nationality": "Slovakia",
      "Age": 22,
      "Club": "Sparta Praha",
      "OVA": 70,
      "POT": 81,
      "BOV": 72,
      "Value": 3300000.0,
      "Wage": 500.0,
      "Hits": 36.0,
      "Value_for_Money_KPI": 97.45
    },
    {
      "ID": 252064,
      "Name": "L. Krejčí",
      "Nationality": "Czech Republic",
      "Age": 21,
      "Club": "Sparta Praha",
      "OVA": 71,
      "POT": 78,
      "BOV": 73,
      "Value": 3600000.0,
      "Wage": 500.0,
      "Hits": 7.0,
      "Value_for_Money_KPI": 97.42
    },
    {
      "ID": 254909,
      "Name": "O. Dorley",
      "Nationality": "Liberia",
      "Age": 21,
      "Club": "SK Slavia Praha",
      "OVA": 73,
      "POT": 83,
      "BOV": 77,
      "Value": 6500000.0,
      "Wage": 550.0,
      "Hits": 146.0,
      "Value_for_Money_KPI": 97.32
    },
    {
      "ID": 251517,
      "Name": "J. Gvardiol",
      "Nationality": "Croatia",
      "Age": 18,
      "Club": "Dinamo Zagreb",
      "OVA": 69,
      "POT": 86,
      "BOV": 71,
      "Value": 3300000.0,
      "Wage": 500.0,
      "Hits": 652.0,
      "Value_for_Money_KPI": 97.27
    }
  ]
}
```