File size: 4,145 Bytes
57b5041
 
 
 
 
 
 
3a77c6f
 
 
 
57b5041
 
 
 
 
 
 
606cfa3
57b5041
3a77c6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57b5041
 
 
 
3a77c6f
 
57b5041
3a77c6f
 
 
 
57b5041
3a77c6f
 
 
 
 
57b5041
3a77c6f
 
 
 
 
 
 
57b5041
 
 
3a77c6f
 
 
57b5041
3a77c6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""

Sample Data Generator

=====================

Produces synthetic reference and drifted datasets for demonstration.



Features

--------

- age          : Customer age

- income       : Annual income ($)

- score        : Internal credit / risk score

- balance      : Account balance ($)

- transactions : Monthly transaction count

"""

import numpy as np
import pandas as pd


# Drift shift configuration 
_DRIFT_SHIFTS: dict[str, dict] = {
    "low": {
        "age":          {"mean_shift": 2, "std_scale": 1.05},
        "income":       {"mean_shift": 3000, "std_scale": 1.05},
        "score":        {"mean_shift": 5, "std_scale": 1.08},
        "balance":      {"mean_shift": 500, "std_scale": 1.05},
        "transactions": {"mean_shift": 2, "std_scale": 1.05},
    },
    "medium": {
        "age":          {"mean_shift": 8, "std_scale": 1.20},
        "income":       {"mean_shift": 12000, "std_scale": 1.25},
        "score":        {"mean_shift": 20, "std_scale": 1.30},
        "balance":      {"mean_shift": 3000, "std_scale": 1.25},
        "transactions": {"mean_shift": 8, "std_scale": 1.20},
    },
    "high": {
        "age":          {"mean_shift": 18, "std_scale": 1.50},
        "income":       {"mean_shift": 30000, "std_scale": 1.60},
        "score":        {"mean_shift": 45, "std_scale": 1.55},
        "balance":      {"mean_shift": 8000, "std_scale": 1.60},
        "transactions": {"mean_shift": 18, "std_scale": 1.50},
    },
}


def generate_reference_data(n: int = 2000, seed: int = 42) -> pd.DataFrame:
    """

    Generate a clean baseline (reference) dataset.



    Parameters

    ----------

    n    : Number of rows.

    seed : Random seed for reproducibility.



    Returns

    -------

    pd.DataFrame with columns [age, income, score, balance, transactions].

    """
    rng = np.random.default_rng(seed)

    return pd.DataFrame({
        "age":          rng.normal(loc=35,    scale=10,    size=n).clip(18, 80).round(0),
        "income":       rng.normal(loc=55000, scale=15000, size=n).clip(15000).round(2),
        "score":        rng.normal(loc=650,   scale=80,    size=n).clip(300, 850).round(0),
        "balance":      rng.normal(loc=12000, scale=5000,  size=n).clip(0).round(2),
        "transactions": rng.poisson(lam=20, size=n).clip(0),
    })


def generate_drifted_data(

    n: int = 2000,

    drift_level: str = "medium",

    seed: int = 123,

) -> pd.DataFrame:
    """

    Generate a dataset whose distributions have shifted from the reference.



    Parameters

    ----------

    n           : Number of rows.

    drift_level : One of ``"low"``, ``"medium"``, ``"high"``.

    seed        : Random seed.



    Returns

    -------

    pd.DataFrame with the same columns as reference, but shifted.

    """
    if drift_level not in _DRIFT_SHIFTS:
        raise ValueError(f"drift_level must be one of {list(_DRIFT_SHIFTS)}")

    shifts = _DRIFT_SHIFTS[drift_level]
    rng = np.random.default_rng(seed)

    age_shift = shifts["age"]
    income_shift = shifts["income"]
    score_shift = shifts["score"]
    balance_shift = shifts["balance"]
    txn_shift = shifts["transactions"]

    return pd.DataFrame({
        "age": rng.normal(
            loc=35 + age_shift["mean_shift"],
            scale=10 * age_shift["std_scale"],
            size=n,
        ).clip(18, 80).round(0),

        "income": rng.normal(
            loc=55000 + income_shift["mean_shift"],
            scale=15000 * income_shift["std_scale"],
            size=n,
        ).clip(15000).round(2),

        "score": rng.normal(
            loc=650 + score_shift["mean_shift"],
            scale=80 * score_shift["std_scale"],
            size=n,
        ).clip(300, 850).round(0),

        "balance": rng.normal(
            loc=12000 + balance_shift["mean_shift"],
            scale=5000 * balance_shift["std_scale"],
            size=n,
        ).clip(0).round(2),

        "transactions": rng.poisson(
            lam=20 + txn_shift["mean_shift"],
            size=n,
        ).clip(0),
    })