**1. Approach (brief)**- For each feature detect type (numeric vs categorical). - Continuous: use two-sample Kolmogorov-Smirnov (ks_2samp). If many ties/ordinal or non-continuous, fall back to Mann-Whitney U. - Categorical: compute contingency table and use chi-squared; if any expected count < 5 use Fisher's exact (for 2x2) otherwise use chi2 with simulated p-value fallback not implemented here. - Correct p-values with Benjamini-Hochberg (FDR).**2. Code implementation**python
import pandas as pd
import numpy as np
from scipy import stats
def detect_covariate_shift(train_df, prod_df, feature_list):
results = {}
for f in feature_list:
a = train_df[f].dropna()
b = prod_df[f].dropna()
if a.empty or b.empty:
results[f] = {"p_value": np.nan, "test": "insufficient_data"}
continue
# numeric
if pd.api.types.is_numeric_dtype(a):
# use KS test; if many ties, use Mann-Whitney
if (pd.concat([a,b]).nunique() / len(pd.concat([a,b]))) < 0.1:
stat, p = stats.mannwhitneyu(a, b, alternative="two-sided")
test = "mannwhitneyu"
else:
stat, p = stats.ks_2samp(a, b, alternative="two-sided", mode="asymp")
test = "ks_2samp"
results[f] = {"statistic": float(stat), "p_value": float(p), "test": test}
else:
# categorical
ct = pd.crosstab(a, b)
# if 2x2 and small counts, use fisher
if ct.shape == (2,2) and (ct.values < 5).any():
stat, p = stats.fisher_exact(ct.values)
test = "fisher_exact"
results[f] = {"statistic": float(stat), "p_value": float(p), "test": test}
else:
chi2, p, dof, exp = stats.chi2_contingency(ct, correction=False)
results[f] = {"statistic": float(chi2), "p_value": float(p), "test": "chi2"}
# Multiple testing: Benjamini-Hochberg FDR
pvals = [(f, r["p_value"]) for f,r in results.items() if not np.isnan(r.get("p_value", np.nan))]
if pvals:
names, vals = zip(*pvals)
vals = np.array(vals)
m = len(vals)
order = np.argsort(vals)
ranked = np.empty(m, dtype=int)
ranked[order] = np.arange(1, m+1)
bh_adj = np.minimum.accumulate((m / ranked) * vals[order])[np.argsort(order)]
for name, adj in zip(names, bh_adj):
results[name]["p_value_adj_bh"] = float(min(adj,1.0))
return results
**3. Key concepts & reasoning**- KS compares full distributions; MWU compares location (robust to ties). - Chi-squared checks independence in contingency tables; Fisher exact for small counts. - Benjamini-Hochberg controls expected false discovery rate, suitable when testing many features.**4. Edge cases & assumptions**- Assumes feature dtype in DataFrames is meaningful; converts via pandas type checks. - Small sample sizes reduce power; report insufficient_data for empty splits. - For high-cardinality categorical features, consider grouping rare levels or using distance-based tests (e.g., PSI, Monte Carlo chi2). **5. Complexity**- O(n * m) roughly per feature (building tables / sorting for tests); dominated by sample size.