FAQ¶
This page collects frequently asked questions and solutions encountered while using SuperModelingFactory.
Environment and Dependencies¶
Why does import Modeling_Tool fail with _ARRAY_API not found?¶
Symptom
In a K8s (or other containerized) environment, running from Modeling_Tool.Core import * or from config import * produces the following error chain:
A module that was compiled using NumPy 1.x cannot be run in
NumPy 2.x as it may crash. ...
AttributeError: _ARRAY_API not found
...
NameError: name 'exit' is not defined
Root cause
The environment has NumPy 2.x installed (for example 2.2.6), but dependencies such as matplotlib and lightgbm were compiled against the NumPy 1.x ABI.
The ABI is incompatible with the current NumPy major version, which crashes the whole import chain:
NumPy 2.x installed
└─► matplotlib (compiled against NumPy 1.x) → _ARRAY_API not found
└─► lightgbm/compat.py fails to import matplotlib
└─► GBM_Tool.py fails to import lightgbm
└─► initialization of the remaining Model submodules is aborted
Solutions
Restart the Jupyter Kernel afterward. This is the simplest and most reliable fix.
If your environment policy does not allow downgrading NumPy, upgrade matplotlib and lightgbm to versions compatible with NumPy 2.x:
The main repository has switched to pure-Python source distribution (see PR #24),
and no longer depends on precompiled .so/.pyd extensions; just install numpy / lightgbm according to the matrix constraints.
If your current task (such as an inference workflow) does not need GBM/LR training, import only the necessary modules in config.py:
Affected versions
| Package | Incompatible versions | Fixed versions |
|---|---|---|
| NumPy | ≥ 2.0 | < 2.0 (downgrade) |
| matplotlib | < 3.9 | ≥ 3.9 |
| lightgbm | < 4.3 | ≥ 4.3 |
Why does the import fail with AttributeError: module 'numpy' has no attribute 'float'?¶
Symptom
NumPy has already been downgraded to 1.23.x, but running from Modeling_Tool.Core import * (or any import that triggers the top-level Modeling_Tool) still fails with:
File .../dask/array/numpy_compat.py, line 13
if (not np.allclose(np.divide(.4, 1, casting="unsafe"),
np.divide(.4, 1, casting="unsafe", dtype=np.float)) ...
AttributeError: module 'numpy' has no attribute 'float'.
`np.float` was a deprecated alias for the builtin `float`.
The aliases was originally deprecated in NumPy 1.20;
Root cause
GBM_Tool.py used to run import lightgbm as lgb at module top level, and lightgbm/compat.py tries an optional import of dask.array.
An old dask in the environment (older than 2022, roughly <2021.11) uses np.float in dask/array/numpy_compat.py.
That alias was deprecated in NumPy 1.20 and removed entirely in NumPy 1.24+.
Even with NumPy 1.23.x, this usage triggers an AttributeError, breaking every import that depends on Modeling_Tool.
The error chain:
from Modeling_Tool.Core import *
└─► Modeling_Tool/__init__.py (old) → from .Model import ...
└─► GBM_Tool.py → import lightgbm as lgb (module level)
└─► lightgbm/compat.py → from dask.array import ...
└─► dask/array/numpy_compat.py → np.float
└─► AttributeError: module 'numpy' has no attribute 'float'
Fixed in
This problem is fully fixed in the source by two changes:
GBM_Tool.py: the module-levelimport lightgbm as lgb/import xgboost as xgbwere removed; lightgbm/xgboost are now lazy-loaded inside each function or method that uses them (through the_get_lgb()/_get_xgb()helpers).Modeling_Tool/__init__.py: the top-level eagerfrom .Model import (...)block was removed in favor of lazy loading through__getattr__(consistent with the existingODPSRunnerlazy-loading pattern).
After the fix, import Modeling_Tool or from Modeling_Tool.Core import * no longer triggers the lightgbm → dask import chain; lightgbm/xgboost are imported only when you actually call Model symbols such as GradientBoostingModel or lgbm_quick_train.
Temporary workarounds (until the wheel is updated)
If you are using a compiled wheel package (/opt/conda/...) rather than a source install, the source fix does not take effect until the package is rebuilt. Use any of the following to work around it in the meantime:
The dask import in lightgbm/compat.py is an optional dependency wrapped in try/except; once dask is removed, lightgbm skips it and loads normally.
This suits inference/scoring scenarios that do not depend on dask.
dask fixed the np.float usage after 2021.11. Restart the kernel after upgrading.
Note: upgrading dask can bring in many dependency changes, so test it in an isolated environment.
# In config.py, import only modules that do not trigger lightgbm
from Modeling_Tool.Core import *
from Modeling_Tool.Sample import *
from Modeling_Tool.Eval import *
from Modeling_Tool.WOE import *
from Modeling_Tool.Feature import *
# Do not import Modeling_Tool.Model; inference scenarios usually do not need training features
Affected version combinations
| Condition | Notes |
|---|---|
| NumPy 1.20–1.23 + dask < 2021.11 | Triggers AttributeError: np.float (only a warning level in itself, but the old dask code turns it into a crash) |
| NumPy ≥ 1.24 + dask < 2021.11 | Same, but more severe (np.float has been removed entirely) |
| NumPy ≥ 1.24 + dask ≥ 2022.01 | Works |
| Source install of the latest SuperModelingFactory | Fixed; not affected |
After upgrading to 0.8.2, why does my notebook show many more warnings?¶
Cause
In 0.8.1 and earlier, Modeling_Tool.WOE.WOE_Monotone_Binner (loaded as soon as you import Modeling_Tool) and Modeling_Tool.Model.Backward_Tool executed warnings.filterwarnings("ignore") at import time. That suppresses all warnings in the entire Python process: SMF's own guard warnings (such as special-bin governance downgrades and declared special values that never occur in the fit sample), warnings from your own code, and deprecation notices from third-party libraries all disappeared. 0.8.2 removed both global settings, and warnings are back to Python's default behavior. Computed results are identical to before; these messages are simply visible again.
Likewise, in 0.8.1 and earlier, import Modeling_Tool also executed pd.set_option('future.no_silent_downcasting', True) (one occurrence each in Core/Binning_Tool.py, Core/kDataFrame.py, Core/Slope_Tool.py, and Core/ODPS_Tool.py). This is also a process-level switch: once SMF was imported, unrelated replace() / fillna() calls in your own code changed their downcasting behavior and began emitting downcasting warnings. 0.8.2 removed all four occurrences; SMF itself does not depend on the switch (the full regression shows no downcasting warnings without it). If your code really needs it, set it explicitly in your own script.
The same batch also removed pd.options.mode.chained_assignment = None (previously set at import time in Core/kDataFrame.py, Core/Binning_Tool.py, Core/ODPS_Tool.py, Core/Slope_Tool.py, and ExcelMaster/Template.py, and set once more when Report/Report_Tool.plot_woe was called). What it turned off is pandas' SettingWithCopyWarning — the alarm for real bugs of the "I modified a slice and the change didn't take effect" kind — and because it was process-level, your own code lost it too.
It had been turned off because SMF itself triggered the warning in 9 places across 6 modules. 0.8.2 changed all 9 to operate on SMF's own frames, so the warning can stay on without SMF ever tripping it. Resulting behavior changes:
| Location | Before | 0.8.2 |
|---|---|---|
run_binning / super_binning |
Could write the bin_num / bin_range columns back into the DataFrame you passed in |
Only in the return value; your table is untouched |
get_gains_table (when model is passed) |
Left a temporary _mdl_scr column on your table |
No longer leaves it |
plot_woe / plot_woe_group (public API) |
Lowercased your column names and replaced inf in woe/iv in place |
Your table is left as-is; the plots are unchanged |
scoring / select_sample_seed |
Only triggered the warning; the writes already landed on a copy | No change |
If your code depends on the old side effects in the first three rows (for example, looking up columns by lowercase name after calling plot_woe), do it explicitly yourself: df.columns = [c.lower() for c in df.columns].
0.8.2 also cleaned up the deprecation and numerical notices triggered by SMF's own calls (seaborn distplot / bw, log(0) in WOE/IV, pandas concat / groupby FutureWarnings, sklearn feature names, xlsxwriter single-cell merges, pyodps Schema, shap global random state), and fixed the problems this exposed (see the 0.8.2 changelog). The main warnings you may still see are:
| Warning | Source | Notes |
|---|---|---|
declared special value(s) never occur in the fit sample, bins have zero-mass class, keep_all_warn, etc. |
SMF guard warnings | Your data or configuration needs attention; see the WOE guide and the Feature guide |
sklearn UndefinedMetricWarning |
Single-class datasets (such as an OOT sample with no bad samples) | Metrics such as AUC / KS are recorded as NaN for that dataset |
sklearn notice that sample_weight applies only to the calibrator during calibration |
Weighted calibration | A weighting-semantics notice; worth reading |
xgboost Parameters: { ... } are not used |
Model parameters | Parameters passed to the native training interface (such as n_estimators) took no effect |
Silence only the warnings you don't want to see
import warnings
warnings.filterwarnings("ignore", message=r".*zero-mass class") # by message regex (matched from the start of the message)
warnings.filterwarnings("ignore", category=FutureWarning) # by category
Avoid the bare warnings.filterwarnings("ignore") again: it also silences SMF's guard warnings.
Evaluation fails with No module named 'IPython' outside a notebook¶
PerformanceEvaluator.evaluate, get_perf_summary, and some other evaluation functions print their result table with
IPython.display.display when display=True, which is the default. IPython is not a declared dependency of SMF, so a plain
Python environment without it raises ModuleNotFoundError. Either pass display=False (the table is still returned), or
install it with pip install ipython.
ODPS Access Key Configuration¶
How do I manage ODPS credentials and package imports uniformly with config.py?¶
Pain points
When using ODPSRunner in a Jupyter notebook or script, two problems are common:
- Every file repeats
os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"] = "..."by hand, and an AccessKey can easily be committed to Git by mistake; - Every file repeats a long list of
from Modeling_Tool.XXX import *lines, which is noisy and easy to get wrong by missing a submodule.
The recommended approach is to manage the AccessKey centrally in the system-level shared path /opt/workspace/.env, and keep a single config.py in the project root that loads it explicitly. Multiple projects can then share one AK instead of configuring each repository. Every notebook needs only one line at the top, from config import *, which both loads the credentials and imports the package.
What ODPSRunner reads
ODPSRunner() takes no arguments. It reads ALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRET,
ODPS_PROJECT and ODPS_ENDPOINT, all four required: a missing one raises KeyError (since 0.9.1 there are no
built-in defaults for the project and the endpoint). pyodps must be installed
(pip install 'supermodelingfactory[odps]').
Step 1: Install python-dotenv
The Modeling_Tool main package does not depend on python-dotenv, so install it separately once:
Step 2: Create the system-level shared .env
# Create the directory and hand it to the current user
sudo mkdir -p /opt/workspace
sudo chown $USER:$USER /opt/workspace
# Create the file and lock down its permissions (readable/writable only by the current user)
touch /opt/workspace/.env
chmod 600 /opt/workspace/.env
Then write the credentials with an editor:
# /opt/workspace/.env —— one copy shared by all projects
ALIBABA_CLOUD_ACCESS_KEY_ID=<your-access-key-id>
ALIBABA_CLOUD_ACCESS_KEY_SECRET=<your-access-key-secret>
ODPS_PROJECT=<your-project>
ODPS_ENDPOINT=https://service.<region>.maxcompute.aliyun.com/api
Security reminder
/opt/workspace/.envis outside the repository and cannot be tracked by Git — but you mustchmod 600it to stop other users on the same machine from reading it;- Do not share the
.envthrough Slack, email, or screenshots — use a secrets-management service instead (Vault, Alibaba Cloud KMS, etc.); - On a multi-user server, consider
~/.config/smf/.envinstead to avoid cross-user leaks.
Step 3: .gitignore in the project root (a precaution)
Although .env is not in the repository, it is still a good idea to add the following lines, so that if you ever copy .env into the project root by accident, it is not committed:
Step 4: Create config.py in the project root
# ═══════════════════════════════════════════════════════════════════════════
# config.py — shared project imports + environment variable loading
# ═══════════════════════════════════════════════════════════════════════════
# Usage: write one line at the top of any notebook / script:
#
# from config import *
#
# which does both of the following:
# 1. Loads the ODPS / Aliyun credentials from the system-level shared path
# /opt/workspace/.env into os.environ
# 2. Imports all Modeling_Tool submodules into the current namespace
#
# After editing .env, restart the Jupyter kernel for the change to take effect.
# /opt/workspace/.env must be chmod 600 so other users on the machine cannot read it.
# ═══════════════════════════════════════════════════════════════════════════
import os
import sys
from pathlib import Path
# ── Load the system-level shared .env ──
from dotenv import load_dotenv
ENV_PATH = Path("/opt/workspace/.env")
if ENV_PATH.is_file():
# override=False: variables already in the environment (e.g. injected by K8s) take priority over .env
load_dotenv(ENV_PATH, override=False)
else:
print(f"[config] WARNING: {ENV_PATH} not found; ODPS credentials may be missing.",
file=sys.stderr)
# ── Import all Modeling_Tool submodules ──
# Core — basic utilities: DateTimeUtils, load_model, save_model, calc_iv, calc_woe ...
# Sample — sample splitting: SampleSplitter, StratifiedSampler, SampleBalancer
# Eval — model evaluation: PerformanceEvaluator, GainsTableCalculator
# Feature — feature analysis: proc_means_by_grp, PSICalculator, CorrelationFilter
# WOE — WOE binning: WOE_Master, MonotoneWOEBinner, woe_transform
# Model — model training: LRMaster, GradientBoostingModel, BackwardVariableEliminator
import Modeling_Tool as smf # namespace backup: smf.Model.LRMaster stays available if an import * collides
from Modeling_Tool.Core import * # noqa: F401,F403
from Modeling_Tool.Sample import * # noqa: F401,F403
from Modeling_Tool.Eval import * # noqa: F401,F403
from Modeling_Tool.Feature import * # noqa: F401,F403
from Modeling_Tool.WOE import * # noqa: F401,F403
from Modeling_Tool.Model import * # noqa: F401,F403
Step 5: Use it in a notebook
# check: skip (needs your own config.py and ODPS credentials)
# First cell of the notebook
from config import *
# At this point:
# 1. os.environ already holds ALIBABA_CLOUD_ACCESS_KEY_ID / _SECRET / ODPS_PROJECT / ODPS_ENDPOINT
# 2. All SMF classes such as LRMaster / WOE_Master / PerformanceEvaluator are ready to use
odps = ODPSRunner() # no arguments; credentials are read from os.environ
df = odps.run_sql("SELECT * FROM ... LIMIT 100") # returns a DataFrame (to_df=True by default)
woe = WOE_Master(train_data=train_df, varlist=features, dep="bad_flag") # use directly, no extra import needed
lr = LRMaster(params={"C": 1.0})
Key design notes
Keeping the file at /opt/workspace/.env and loading it by absolute path solves three problems at once:
- Multiple projects share one AK — no need to paste a
.envinto every repository, and rotating credentials means changing one place; - It cannot be committed by accident with a repo push — it is not under a Git working tree at all;
- Easy to debug — the path is hard-coded, so when something goes wrong you can see at a glance which credentials were loaded; Jupyter startup does not rely on the unreliable
__file__or CWD.
override=False (the default) means that variables already present in os.environ are not overwritten by .env.
In containerized deployments (K8s, Docker, CI/CD), this ensures environment variables injected by operations always take priority over the local .env — otherwise local credentials could overwrite them by accident at release time.
Six from ... import * statements carry a naming-collision risk (a submodule imported later silently overrides same-named symbols imported earlier).
Keeping an explicit smf namespace as a fallback lets you write smf.Model.LRMaster to pin down the exact source when a collision happens.
python-dotenv is a project-level engineering convention, not a modeling-tool responsibility. The SMF main package only does "use these keys if they are in os.environ"; how they get into os.environ (dotenv / K8s Secret / startup script / manual export) is entirely up to the project.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
After from config import *, os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"] is still empty |
/opt/workspace/.env does not exist or the path is wrong |
Run ls -la /opt/workspace/.env to confirm the file exists; check that ENV_PATH in config.py is correct |
PermissionError: [Errno 13] when reading .env |
After chmod 600, the current user is not the owner |
Check the owner with ls -la /opt/workspace/.env; run sudo chown $USER:$USER /opt/workspace/.env |
Credentials did not update after editing .env |
The Jupyter kernel has cached os.environ |
Restart the kernel (menu: Kernel → Restart) |
ImportError: No module named 'dotenv' |
python-dotenv is not installed |
pip install python-dotenv |
The correct credentials were overwritten by .env after deploying to K8s |
override=True was used |
Switch back to override=False (the recommended default) |
| One notebook calls several data sources and the wrong AK is used | os.environ is process-level global state |
For multi-account scenarios in one process, pass explicit arguments such as ODPS(access_id=..., secret_access_key=...) |
Sample Weights¶
Which keyword passes sample weights: weight_col or sample_weight?¶
Typical scenarios: correcting sampling bias (giving original samples higher weight after oversampling), weighting by loan balance/amount, time-decay weighting, and so on.
| Argument | Where | Notes |
|---|---|---|
weight_col |
LRMaster (fit, stepwise_selection, get_aic, get_bic, calibrate_model), PerformanceEvaluator, GainsTableCalculator, Model_Evaluation_Tool, cross_risk, BackwardVariableEliminator |
Name of a column of the DataFrame you pass |
sample_weight |
GradientBoostingModel.fit and the single-backend classes, and the array-based evaluation functions (calc_roc, calc_pr, calc_equid_*, calc_lift_apt, the 'sample_weight' key of evaluate_performance datasets) |
A 1-D array with one weight per row |
Weights must be finite, non-negative, one per row, and sum to more than 0. APIs that accept both forms (for example
cross_risk) raise ValueError if you pass both; wgt and wgt_col are accepted as aliases there. LRMaster.fit
takes only weight_col.
If you trained with weights but forget to pass them at evaluation time, metrics are computed with equal weights (1 per row), which is inconsistent with the training objective. Pass the same weights consistently through the training and evaluation chain.
For the argument each API takes and the weighted metric semantics (N vs N_RAW, weighted AUC/KS/Lift), see Model Training: Sample Weights and Model Evaluation: Sample-Weighted Evaluation.
Known Issues¶
There are no open defects at the moment. The issues that the documentation audit and the end-to-end audit of the two pipelines found have been fixed; the v0.9.0 notes list them one by one, with the results that change.
A few behaviors are documented instead of changed, because changing them would move numbers that existing models rely on. They are listed in Known Issues with the parameter each one concerns: the different bins of the weighted and the unweighted screening when no WOE bins are used, the anchor-group coverage of correlated_detail, and the keep-all fallback of on_empty_stage (narrowed in
0.9.3: from the IV stage on it keeps at most
fallback_top_k features and never a leak drop).
If you have other questions, feel free to submit them in GitHub Issues.