├── .github ├── ISSUE_TEMPLATE │ ├── 00-bug-issue.md │ ├── 30-feature-request.md │ └── 90-other-issues.md └── workflows │ ├── python-pytest.yml │ └── wheel-builder.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── README_zh_CN.md ├── docs ├── __init__.py ├── en_US │ ├── Makefile │ ├── make.bat │ ├── requirements.txt │ └── source │ │ ├── conf.py │ │ ├── index.rst │ │ └── sub │ │ ├── .DS_Store │ │ ├── api.rst │ │ ├── causal_discovery.rst │ │ ├── causal_model.rst │ │ ├── causal_model │ │ ├── backdoor.png │ │ ├── causal_model.rst │ │ ├── graph.rst │ │ ├── graph_expun.png │ │ ├── graph_un_arc.png │ │ ├── iv1.png │ │ ├── iv2.png │ │ └── prob.rst │ │ ├── discovery │ │ ├── DAGMA.rst │ │ ├── DYGolem.rst │ │ ├── DYNotears.rst │ │ ├── golem.rst │ │ └── notears.rst │ │ ├── est.rst │ │ ├── est_model │ │ ├── approx_bound.rst │ │ ├── causal_tree.rst │ │ ├── dml.rst │ │ ├── dr.rst │ │ ├── forest.rst │ │ ├── forest_based │ │ │ ├── cf.rst │ │ │ ├── ctcf.rst │ │ │ └── grf.rst │ │ ├── iv.rst │ │ ├── iv │ │ │ ├── .DS_Store │ │ │ ├── deepiv.rst │ │ │ └── iv_normal.rst │ │ ├── iv3.png │ │ ├── iv4.png │ │ ├── meta.rst │ │ └── score.rst │ │ ├── flow.png │ │ ├── inter_model │ │ ├── ce_interpreter.rst │ │ └── policy_interpreter.rst │ │ ├── interpreter.rst │ │ ├── intro.rst │ │ ├── policy.rst │ │ ├── quick_start.rst │ │ ├── ref.rst │ │ ├── structure_ylearn.png │ │ ├── sub.rst │ │ └── why.rst └── zh_CN │ ├── Makefile │ ├── make.bat │ ├── requirements.txt │ └── source │ ├── conf.py │ ├── index.rst │ └── sub │ ├── api.rst │ ├── causal_discovery.rst │ ├── causal_model.rst │ ├── causal_model │ ├── backdoor.png │ ├── causal_model.rst │ ├── graph.rst │ ├── graph_expun.png │ ├── graph_un_arc.png │ ├── iv1.png │ ├── iv2.png │ └── prob.rst │ ├── discovery │ └── notears.rst │ ├── est.rst │ ├── est_model │ ├── approx_bound.rst │ ├── causal_tree.rst │ ├── dml.rst │ ├── dr.rst │ ├── iv.rst │ ├── iv │ │ ├── .DS_Store │ │ ├── deepiv.rst │ │ └── iv_normal.rst │ ├── iv3.png │ ├── iv4.png │ ├── meta.rst │ └── score.rst │ ├── flow.png │ ├── inter_model │ ├── ce_interpreter.rst │ └── policy_interpreter.rst │ ├── interpreter.rst │ ├── intro.rst │ ├── policy.rst │ ├── quick_start.rst │ ├── ref.rst │ ├── structure_ylearn.png │ ├── sub.rst │ └── why.rst ├── example_usages ├── .gitattributes ├── case_study_bank.ipynb ├── case_study_ihdp.ipynb ├── case_study_marketing_promotion.ipynb ├── data │ ├── BankChurners.csv.zip │ └── marketing_promotion.csv.zip ├── introduction.ipynb ├── test_approx_bound.ipynb ├── test_causal_dis.ipynb ├── test_causal_dydis.ipynb ├── test_causal_forest.ipynb ├── test_causal_tree.ipynb ├── test_ce_interpreter.ipynb ├── test_ctcausal_forest.ipynb ├── test_deepiv.ipynb ├── test_dml.ipynb ├── test_gen_ts.ipynb ├── test_graph.ipynb ├── test_grf.ipynb ├── test_iv.ipynb ├── test_metalearner_and_dr.ipynb ├── test_model.ipynb ├── test_naive_grf.ipynb ├── test_policy_interpreter.ipynb ├── test_policy_model.ipynb ├── test_score.ipynb ├── test_uplift.ipynb └── test_why_identify.ipynb ├── fig ├── .DS_Store ├── QRcode_press.jpg ├── QRcode_survey.jpg ├── YLearn1.png ├── flow.png ├── flow_chart.png ├── flow_chart_cn.png ├── graph_expun.png ├── graph_un_arc.png ├── id_fig.png ├── iv2.png ├── latex_exp.png ├── structure_ylearn.png └── wechat_QRcode.png ├── pyproject.toml ├── requirements-bayesian.txt ├── requirements-extra.txt ├── requirements-notebook.txt ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── _common.py ├── _dgp.py ├── approx_bound_test.py ├── causal_tree_test.py ├── deep_iv_test.py ├── discovery_test.py ├── double_ml_test.py ├── doubly_robust_test.py ├── estimator_factory_test.py ├── iv_test.py ├── metalearner_test.py ├── permuted_learner_test.py ├── policy_model_test.py ├── utils_test.py └── why_test.py └── ylearn ├── __init__.py ├── _version.py ├── api ├── __init__.py ├── _identifier.py ├── _why.py ├── _wrapper.py ├── smoke.py └── utils.py ├── bayesian ├── __init__.py ├── _base.py ├── _collectors.py ├── _dag.py ├── _data.py ├── _modules.py └── _network.py ├── causal_discovery ├── Untitled-1.ipynb ├── __init__.py ├── _base.py ├── _discovery.py ├── _dydiscovery.py ├── _proxy_gcastle.py ├── _proxy_pgm.py ├── dag.py └── toy.py ├── causal_model ├── __init__.py ├── graph.py ├── model.py ├── other_identification │ ├── __init__.py │ ├── do_operator.py │ ├── gradient_based.py │ └── minimal_adjust_set.py ├── prob.py ├── scm.py └── utils.py ├── effect_interpreter ├── __init__.py ├── _export.py ├── ce_interpreter.py └── policy_interpreter.py ├── estimator_model ├── __init__.py ├── _factory.py ├── _generalized_forest │ ├── __init__.py │ ├── _base_forest.py │ ├── _grf.py │ └── tree │ │ ├── __init__.py │ │ ├── _criterion.pxd │ │ ├── _criterion.pyx │ │ ├── _grf_tree.py │ │ ├── _splitter.pxd │ │ ├── _splitter.pyx │ │ ├── _tree.pxd │ │ ├── _tree.pyx │ │ ├── _util.pxd │ │ ├── _util.pyx │ │ └── _util_lib.cpp ├── _naive_forest │ ├── __init__.py │ ├── _grf.py │ ├── _grf_tree.py │ ├── causal_forest.py │ └── utils.py ├── _nn.py ├── _permuted.py ├── _tree │ ├── __init__.py │ ├── tree_criterion.pxd │ ├── tree_criterion.pyx │ └── tree_criterion_lib.cpp ├── approximation_bound.py ├── base_models.py ├── causal_forest.py ├── causal_tree.py ├── deepiv.py ├── double_ml.py ├── doubly_robust.py ├── effect_score.py ├── ensemble.py ├── iv.py ├── meta_learner.py ├── propensity_score.py └── utils.py ├── exp_dataset ├── __init__.py ├── exp_data.py └── gen.py ├── policy ├── __init__.py ├── _tree │ ├── __init__.py │ ├── tree_criterion.pxd │ └── tree_criterion.pyx └── policy_model.py ├── sklearn_ex ├── __init__.py ├── _data_cleaner.py ├── _dataframe_mapper.py ├── _transformer.py └── cloned │ ├── .gitattributes │ ├── README.txt │ ├── __init__.py │ ├── scipy │ ├── README.txt │ ├── __init__.py │ ├── _complexstuff.h │ ├── _complexstuff.pxd │ └── _xlogy.pxd │ ├── tree │ ├── __init__.py │ ├── _criterion.pxd │ ├── _criterion.pyx │ ├── _splitter.pxd │ ├── _splitter.pyx │ ├── _tree.pxd │ ├── _tree.pyx │ ├── _utils.pxd │ └── _utils.pyx │ └── utils │ ├── __init__.py │ ├── _random.pxd │ ├── _random.pyx │ └── validation.py ├── stats_inference └── __init__.py ├── uplift ├── __init__.py ├── _metric.py ├── _model.py └── _plot.py └── utils ├── __init__.py ├── _common.py ├── _tic_tok.py ├── logging.py └── metrics.py /.github/ISSUE_TEMPLATE/00-bug-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Issue 3 | about: Use this template for reporting a bug 4 | labels: 'type:bug' 5 | 6 | --- 7 | 8 | Please make sure that this is a bug. 9 | 10 | **System information** 11 | - OS Platform and Distribution (e.g., CentOS 7.6): 12 | - Python version: 13 | - YLearn version: 14 | - Other Python packages(run `pip list`): 15 | 16 | 17 | **Describe the current behavior** 18 | 19 | 20 | **Describe the expected behavior** 21 | 22 | 23 | **Standalone code to reproduce the issue** 24 | Provide a reproducible test case that is the bare minimum necessary to generate 25 | the problem. If possible, please share a link to Jupyter notebook. 26 | 27 | 28 | **Are you willing to submit PR?(Yes/No)** 29 | 30 | 31 | **Other info / logs** 32 | Include any logs or source code that would be helpful to diagnose the problem. 33 | If including tracebacks, please include the full traceback. Large logs and files 34 | should be attached. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/30-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Use this template for raising a feature request 4 | labels: 'type:feature' 5 | 6 | --- 7 | 8 | Please make sure that this is a feature request. 9 | 10 | **System information** 11 | - YLearn version (you are using): 12 | - Are you willing to contribute it (Yes/No): 13 | 14 | 15 | **Describe the feature and the current behavior/state.** 16 | 17 | 18 | **Will this change the current api? How?** 19 | 20 | 21 | **Any Other info.** 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/90-other-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other Issues 3 | about: Use this template for any other non-support related issues 4 | labels: 'type:others' 5 | 6 | --- 7 | 8 | This template is for miscellaneous issues not covered by the other issue categories. 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | .vscode 4 | 5 | ### Python template 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | *.c 14 | 15 | # CPP extensions 16 | *.cpp 17 | !*_lib.cpp 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | dt_output/ 141 | log/ 142 | trial_store/ 143 | tmp/ 144 | catboost_info/ 145 | 146 | #dispatchers 147 | logs/ 148 | workdir/ 149 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | ylearn@zetyun.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the Contributor Covenant, 74 | version 2.1, available at 75 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to YLearn 2 | 3 | YLearn project welcome community contributors. To contribute to it, please follow guidelines below. As a start, you can check out outstanding issues. If you'd like to contribute to something else, please open a new issue for discussion. 4 | 5 | 6 | # Contributing Guidelines 7 | 8 | Code contributions via pull requests are appreciated. Before that, please make sure: 9 | 10 | 1. You are working with the latest source on the master branch. 11 | 2. You have checked existing open and closed issues to make sure they aren't addressed before. 12 | 3. You open an issue to discuss any significant work. 13 | 14 | The general contribuitng workflow are: 15 | 16 | 1. Fork the `YLearn` repo. 17 | 2. Clone the forked repo locally 18 | 3. Create a certain branch for the change 19 | 4. Make changes on the branch 20 | 5. Test your changes 21 | 6. Commit the changes to the branch with clear description 22 | 7. Submit a pull request(PR) 23 | 8. The PR will be merged to the main after verification by our maintainers 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | From python:3.8.13-buster 2 | 3 | ARG PIP_PKGS="ylearn lightgbm igraph jupyterlab gcastle pgmpy ipywidgets tqdm ipywidgets matplotlib shap" 4 | ARG PIP_OPTS="--disable-pip-version-check --no-cache-dir" 5 | #ARG PIP_OPTS="--disable-pip-version-check --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple/" 6 | 7 | # COPY sources.list /etc/apt/sources.list 8 | 9 | RUN apt update \ 10 | && apt install -y graphviz \ 11 | && apt clean \ 12 | && pip install $PIP_OPTS $PIP_PKGS \ 13 | && v=$(pip show ylearn|awk '/Version/{print($2)}') \ 14 | && echo ylearn version:$v \ 15 | && pip download --no-deps --no-binary ylearn --dest /tmp/ $PIP_OPTS ylearn==$v \ 16 | && tar xzf /tmp/ylearn-$v.tar.gz -C /tmp/ \ 17 | && mkdir -p /opt/datacanvas \ 18 | && cp -r /tmp/ylearn-$v/example_usages /opt/datacanvas/ \ 19 | && echo "#!/bin/bash\njupyter lab --notebook-dir=/opt/datacanvas --ip=0.0.0.0 --port=\$NotebookPort --no-browser --allow-root --NotebookApp.token=\$NotebookToken" > /entrypoint.sh \ 20 | && chmod +x /entrypoint.sh \ 21 | && rm -rf /var/lib/apt/lists \ 22 | && rm -rf /var/cache/* \ 23 | && rm -rf /var/log/* \ 24 | && rm -rf /root/.cache \ 25 | && rm -rf /tmp/* 26 | 27 | EXPOSE 8888 28 | 29 | ENV NotebookToken="" \ 30 | NotebookPort=8888 31 | 32 | CMD ["/entrypoint.sh"] 33 | 34 | # docker run --rm --name ylearn -p 8888:8888 -e NotebookToken=your-token datacanvas/ylearn 35 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements*.txt 2 | include downloads/eigen-3.4.0.tar.gz 3 | recursive-include example_usages * 4 | recursive-include ylearn *.cpp *.h 5 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/en_US/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | # SPHINXBUILD ?= sphinx-build 8 | SPHINXBUILD = python3.8 -msphinx 9 | SOURCEDIR = source 10 | BUILDDIR = build 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /docs/en_US/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/en_US/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | numpy 3 | pandas 4 | scikit-learn 5 | torch 6 | IPython 7 | networkx -------------------------------------------------------------------------------- /docs/en_US/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | import sys 18 | import os 19 | 20 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'YLearn' 24 | copyright = '2022, DataCanvasLab' 25 | author = 'DataCanvasLab' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.napoleon', 35 | 'sphinx.ext.autodoc', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # List of patterns, relative to source directory, that match files and 42 | # directories to ignore when looking for source files. 43 | # This pattern also affects html_static_path and html_extra_path. 44 | exclude_patterns = [] 45 | 46 | 47 | # -- Options for HTML output ------------------------------------------------- 48 | 49 | # The theme to use for HTML and HTML Help pages. See the documentation for 50 | # a list of builtin themes. 51 | # 52 | html_theme = 'sphinx_rtd_theme' 53 | 54 | # Add any paths that contain custom static files (such as style sheets) here, 55 | # relative to this directory. They are copied after the builtin static files, 56 | # so a file named "default.css" will overwrite the builtin "default.css". 57 | html_static_path = ['_static'] 58 | 59 | # import ylearn 60 | # print("cwd: ") 61 | # print(os.getcwd()) 62 | # build_cmd = f"cd ../.. && {sys.executable} setup.py build_ext --inplace" 63 | # # print(build_cmd) 64 | 65 | # os.system(build_cmd) 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/en_US/source/index.rst: -------------------------------------------------------------------------------- 1 | .. YLearn documentation master file, created by 2 | sphinx-quickstart on Tue Jun 7 18:29:01 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to YLearn's documentation! 7 | ================================== 8 | 9 | YLearn, a pun of "learn why", is a python package for causal learning which supports various aspects of causal inference ranging from causal effect identification, estimation, and causal graph discovery, etc. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | sub/sub 15 | sub/causal_model 16 | sub/est 17 | sub/causal_discovery 18 | sub/policy 19 | sub/interpreter 20 | sub/why 21 | sub/ref 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/.DS_Store -------------------------------------------------------------------------------- /docs/en_US/source/sub/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | **************************** 4 | API: Interacting with YLearn 5 | **************************** 6 | 7 | .. list-table:: All-in-one API 8 | 9 | * - Class Name 10 | - Description 11 | * - :py:class:`Why` 12 | - An API which encapsulates almost everything in YLearn, such as *identifying causal effects* and *scoring a trained estimator model*. It provides to users a simple and efficient way to use YLearn. 13 | 14 | .. list-table:: Causal Structures Discovery 15 | 16 | * - Class Name 17 | - Description 18 | * - :py:class:`CausalDiscovery` 19 | - Find causal structures in observational data. 20 | 21 | .. list-table:: Causal Model 22 | 23 | * - Class Name 24 | - Description 25 | * - :py:class:`CausalGraph` 26 | - Express the causal structures and support other operations related to causal graph, e.g., add and delete edges to the graph. 27 | * - :py:class:`CausalModel` 28 | - Encode causations represented by the :py:class:`CausalGraph`. Mainly support causal effect identification, e.g., backdoor adjustment. 29 | * - :py:class:`Prob` 30 | - Represent the probability distribution. 31 | 32 | .. list-table:: Estimator Models 33 | 34 | * - Class Name 35 | - Description 36 | * - :py:class:`GRForest` 37 | - A highly flexible nonparametric estimator (Generalized Random Forest, GRF) model which supports both discrete and continuous treatment. The unconfoundedness condition is required. 38 | * - :py:class:`CausalForest` 39 | - A generalized random forest combined with the local centering technique (i.e. double machine learning framework). The unconfoundedness condition is required. 40 | * - :py:class:`CTCausalForest` 41 | - A causal forest as an ensemble of a bunch of :py:class:`CausalTree`. Similar to the :py:class:`CausalTree`, the treatment should be binary. The unconfoundedness condition is required. 42 | * - :py:class:`ApproxBound` 43 | - A model used for estimating the upper and lower bounds of the causal effects. This model does not need the unconfoundedness condition. 44 | * - :py:class:`CausalTree` 45 | - A class for estimating causal effect with decision tree. The unconfoundedness condition is required. 46 | * - :py:class:`DeepIV` 47 | - Instrumental variables with deep neural networks. Must provide the names of instrumental variables. 48 | * - :py:class:`NP2SLS` 49 | - Nonparametric instrumental variables. Must provide the names of instrumental variables. 50 | * - :py:class:`DoubleML` 51 | - Double machine learning model for the estimation of CATE. The unconfoundedness condition is required. 52 | * - :py:class:`DoublyRobust` and :py:class:`PermutedDoublyRobust` 53 | - Doubly robust method for the estimation of CATE. The permuted version considers all possible treatment-control pairs. The unconfoundedness condition is required and the treatment must be discrete. 54 | * - :py:class:`SLearner` and :py:class:`PermutedSLearner` 55 | - SLearner. The permuted version considers all possible treatment-control pairs. The unconfoundedness condition is required and the treatment must be discrete. 56 | * - :py:class:`TLearner` and :py:class:`PermutedTLearner` 57 | - TLearner with multiple machine learning models. The permuted version considers all possible treatment-control pairs. The unconfoundedness condition is required and the treatment must be discrete. 58 | * - :py:class:`XLearner` and :py:class:`PermutedXLearner` 59 | - XLearner with multiple machine learning models. The permuted version considers all possible treatment-control pairs. The unconfoundedness condition is required and the treatment must be discrete. 60 | * - :py:class:`RLoss` 61 | - Effect score for measuring the performances of estimator models. The unconfoundedness condition is required. 62 | 63 | .. list-table:: Policy 64 | 65 | * - Class Name 66 | - Description 67 | * - :py:class:`PolicyTree` 68 | - A class for finding the optimal policy for maximizing the causal effect with the tree model. 69 | 70 | .. list-table:: Interpreter 71 | 72 | * - Class Name 73 | - Description 74 | * - :py:class:`CEInterpreter` 75 | - An object used to interpret the estimated CATE using the decision tree model. 76 | * - :py:class:`PolicyInterpreter` 77 | - An object used to interpret the policy given by some :py:class:`PolicyModel`. 78 | 79 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_discovery.rst: -------------------------------------------------------------------------------- 1 | Causal Discovery: Exploring the Causal Structures in Data 2 | ========================================================= 3 | 4 | .. toctree:: 5 | discovery/notears 6 | 7 | A fundamental task in causal learning is to find the underlying causal relationships, the so-called "causal structures", and apply 8 | them. Traditionally, these relationships might be revealed by designing randomized experiments or imposing interventions. However, 9 | such methods are too expensive or even impossible. Therefore, many techniques, e.g., the PC algorithm (see [Spirtes2001]_), have been suggested recently to analyze the causal 10 | structures by directly utilizing observational data. These techniques are named as *causal discovery*. 11 | 12 | The current version of YLearn implements a score-based method for causal discovery [Zheng2018]_. More methods will be added in later versions. 13 | 14 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_model.rst: -------------------------------------------------------------------------------- 1 | ***************************************************** 2 | Causal Model: The Representation of Causal Structures 3 | ***************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | causal_model/graph 9 | causal_model/causal_model 10 | causal_model/prob 11 | 12 | 13 | For a set of variables :math:`V`, its **causal structure** can be represented by a directed acyclic 14 | graph (DAG), where each node corresponds to an element of :math:`V` while each direct functional 15 | relationship among the corresponding variables can be represented by a link in the DAG. A causal 16 | structure guides the precise specification of how each variable is influenced by its parents in the 17 | DAG. For an instance, :math:`X \leftarrow W \rightarrow Y` denotes that :math:`W` is a parent, thus 18 | also a common cause, of :math:`X` and :math:`Y`. More specifically, for two distinct variables :math:`V_i` 19 | and :math:`V_j`, if their functional relationship is 20 | 21 | .. math:: 22 | 23 | V_j = f(V_i, \eta_{ij}) 24 | 25 | for some function :math:`f` and noise :math:`\eta`, then in the DAG representing the causal structure of the set of variables 26 | :math:`V`, there should be an arrow pointing to :math:`V_j` from :math:`V_i`. A detailed introduction to 27 | such DAGs for causal structures can be found in [Pearl]_. 28 | 29 | A causal effect, also named as causal estimand, can be expressed with the :math:`do`-operator according to 30 | [Pearl]_. As an example, 31 | 32 | .. math:: 33 | 34 | P(y|do(x)) 35 | 36 | denotes the probability function of :math:`y` after imposing the intervention :math:`x`. Causal structures 37 | are crucial to expressing and estimating interested causal estimands. YLearn implements an object, 38 | ``CausalGraph``, to support representations for causal structures and related operations of the 39 | causal structures. Please see :ref:`causal_graph` for details. 40 | 41 | YLearn concerns the intersection of causal inference and machine learning. Therefore we assume that we have 42 | abundant observational data rather than having the access to design randomized experiments. Then Given a DAG 43 | for some causal structure, the causal estimands, e.g., the average treatment effects (ATEs), usually can not 44 | be directly estimated from the data due to the counterfactuals which can never be observed. Thus it is 45 | necessary to convert these causal estimands into other quantities, which can be called as statistical estimands 46 | and can be estimated from data, before proceeding to any estimation. The procedure of converting a causal 47 | estimand into the corresponding statistical estimand is called **identification**. 48 | 49 | The object for supporting identification and other related operations of causal structures is ``CausalModel``. 50 | More details can be found in :ref:`causal_model`. 51 | 52 | In the language of Pearl's causal inference, it is also necessary to represent the results 53 | in the language of probability. For this purpose, YLearn also implements an object :class:`Prob` which is introduced in 54 | :ref:`prob`. 55 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_model/backdoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/causal_model/backdoor.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_model/graph_expun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/causal_model/graph_expun.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_model/graph_un_arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/causal_model/graph_un_arc.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_model/iv1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/causal_model/iv1.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_model/iv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/causal_model/iv2.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/causal_model/prob.rst: -------------------------------------------------------------------------------- 1 | .. _prob: 2 | 3 | ***************************** 4 | Representation of Probability 5 | ***************************** 6 | 7 | To represent and modifies probabilities such as 8 | 9 | .. math:: 10 | 11 | P(x, y|z), 12 | 13 | one can define an instance of :class:`Prob` and change its attributes. 14 | 15 | .. topic:: Examples 16 | 17 | The probabiity 18 | 19 | .. math:: 20 | 21 | \sum_{w}P(v|y)[P(w|z)P(x|y)P(u)] 22 | 23 | is composed of two parts: :math:`P(v|y)` and a product :math:`P(w|z)P(x|y)P(u)` and then they are sumed on :math:`w`. 24 | We first define the first part: 25 | 26 | .. code-block:: python 27 | 28 | from ylearn.causal_model.prob import Prob 29 | 30 | var = {'v'} 31 | conditional = {'y'} # the conditional set 32 | marginal = {'w'} # sum on w 33 | 34 | Now we continue to define the second part, the product, 35 | 36 | .. code-block:: python 37 | 38 | p1 = Prob(variables={'w'}, conditional={'z'}) 39 | p2 = Prob(variables={'x'}, conditional={'y'}) 40 | p3 = Prob(variables={'u'}) 41 | product = {p1, p2, p3} 42 | 43 | The final result is 44 | 45 | .. code-block:: python 46 | 47 | P = Prob(variables=var, conditional=conditional, marginal=marginal, product=product) 48 | P.show_latex_expression() 49 | 50 | >>> :math:`\sum_w P(v|y)[P(u)][P(w|z)][P(x|y)]` 51 | 52 | An instance of :class:`Prob` can also output the latex code 53 | 54 | .. code-block:: python 55 | 56 | P.parse() 57 | 58 | >>> '\\sum_{w}P(v|y)\\left[P(u)\\right]\\left[P(w|z)\\right]\\left[P(x|y)\\right]' 59 | 60 | .. py:class:: ylearn.causal_model.prob.Prob(variables=set(), conditional=set(), divisor=set(), marginal=set(), product=set()) 61 | 62 | Probability distribution, e.g., the probability expression 63 | 64 | .. math:: 65 | 66 | \sum_{w}P(v|y)[P(w|z)P(x|y)P(u)]. 67 | 68 | We will clarify below the meanings of our variables with this example. 69 | 70 | :param set, default=set() variables: The variables (:math:`v` in the above example) of the probability. 71 | :param set, default=set() conditional: The conditional set (:math:`y` in the above example) of the probability. 72 | :param set, default=set() marginal: The sum set (:math:`w` in the above example) for marginalizing the probability. 73 | :param set, default=set() product: If not set(), then the probability is composed of the first probability 74 | object :math:`(P(v|y))` and several other probabiity objects that are all saved 75 | in the set product, e.g., product = {P1, P2, P3} where P1 for :math:`P(w|z)`, 76 | P2 for :math:`P(x|y)`, and P3 for :math:`P(u)` in the above example. 77 | 78 | .. py:method:: parse 79 | 80 | Return the expression of the probability distribution. 81 | 82 | :returns: Expression of the encoded probabiity 83 | :rtype: str 84 | 85 | .. py:method:: show_latex_expression 86 | 87 | Show the latex expression. 88 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/discovery/DAGMA.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | DAGMA 3 | ******** 4 | 5 | A novel acyclicity characterization based on the log-determinant (log-det) function (DAGMA), 6 | which leverages the nilpotency property of directed acyclic graphs (DAGs) [Kevin2022]_. 7 | Then the DAGMA constrain and first-order optimizer is exploited to reveal the structures of 8 | directed acyclic graphs (DAGs). Specifically, for a given vector :math:`x \in \mathbb{R}^d` such 9 | that there exists a matrix :math:`V` which satisifies :math:`x = Vx + \eta` for some noise vector 10 | :math:`\eta \in \mathbb{R}^d`, the optimization problem can be summarized as follows: 11 | 12 | .. math:: 13 | 14 | \min_{W \in \mathbb{R}^{d\times d}} F(W)+\lambda_1\|W\|_1 \\ 15 | s.t. \quad & h^s_{ldet}(W) = 0, 16 | 17 | where :math:`F(W)` is a continuous function measuring :math:`\|x - Wx\|`, :math:`\lambda_1 ||W||_1` is a penalty term encouraging sparsity, i.e., having fewer edges,and 18 | 19 | .. math:: 20 | 21 | h^s_{ldet}(W) = -log det(sI- W \circ W) + d log(s) 22 | 23 | where :math:`\circ` is the Hadamard product. This optimization can then be solved with some optimization technique, such as gradient desscent. 24 | 25 | The YLearn class for the DAGMA algorithm is :class:`DagmaDiscovery`. 26 | 27 | 28 | .. topic:: Example 29 | 30 | .. code-block:: python 31 | 32 | import pandas as pd 33 | 34 | from ylearn.exp_dataset.gen import gen 35 | from ylearn.causal_discovery import DagmaDiscovery 36 | 37 | X = gen() 38 | X = pd.DataFrame(X, columns=[f'x{i}' for i in range(X.shape[1])]) 39 | Dagmacd = DagmaDiscovery(hidden_layer_dim=[3]) 40 | est = Dagmacd(X, threshold=0.01, return_dict=True) 41 | 42 | print(est) 43 | 44 | >>> OrderedDict([('x0', []), ('x1', ['x0', 'x2', 'x3', 'x4']), ('x2', ['x0']), ('x3', ['x0', 'x2', 'x4']), ('x4', ['x0', 'x2'])]) 45 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/discovery/DYGolem.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | DYGolem 3 | ******** 4 | 5 | The problem of estimating contemporaneous (intra-slice) and time-lagged (inter-slice) relationships between variables in a time-series simultaneously can also be solved by formulating 6 | a continuous optimization problem over real matrices with soft and sparse constraints enforcing the acyclicity condition [Ignavier2020]_. 7 | Specifically, for a given vector :math:`x \in \mathbb{R}^d` and its time-lagged versions :math:`y \in \mathbb{R}^{p \times d}` such that there exists a matrix :math:`V` and a matrix :math:`Q` which satisifies :math:`x = Vx + Qy + \eta` for some noise vector :math:`\eta \in \mathbb{R}^d`, the optimization problem can be summarized as follows: 8 | 9 | .. math:: 10 | 11 | \min_{W, A} \mathcal{S} (W,A;x) = \mathcal{L}(W,A;x) + \lambda_1 ||W||_1 + \lambda_2 h(W) + \lambda_3 ||A||_1 12 | 13 | where :math:`\mathcal{L}(W,A;x)` is the Maximum Likelihood Estimator (MLE): 14 | 15 | .. math:: 16 | 17 | \mathcal{L}(W,A;x) = 1/2 \sum_{i=1}^d log\left( \sum_{k=1}^n \|X-WX-YA\|^2 \right) - log|det(I-W)| 18 | 19 | If one further assumes that the noise variances are equal, it becomes 20 | 21 | .. math:: 22 | 23 | \mathcal{L}(W,A;x) = d/2 log\left(\sum_{i=1}^d \sum_{k=1}^n \|X-WX-YA\|^2 \right) - log|det(I-W)| 24 | 25 | where :math:`\lambda_1 \|W\|_1` and :math:`\lambda_3 \|A\|_1` is penalty term encouraging sparsity, i.e., having fewer edges, 26 | 27 | and :math:`\lambda_2 h(W)` is a penalty term encouraging DAGness on W. 28 | 29 | .. math:: 30 | h(W) = tr\left( e^{W \circ W} \right) 31 | 32 | where :math:`\circ` is the Hadamard product. This optimization can then be solved with some optimization technique, such as gradient desscent. 33 | 34 | 35 | The YLearn class for the DYGolem algorithm is :class:`DygolemCausalDiscovery`. 36 | 37 | .. topic:: Example 38 | 39 | .. code-block:: python 40 | 41 | import pandas as pd 42 | 43 | from ylearn.exp_dataset.gen import dygen 44 | from ylearn.causal_discovery import DygolemCausalDiscovery 45 | 46 | X, W_true, P_true = dygen(n=500, d=10, step=5, order=2) 47 | X = pd.DataFrame(X, columns=[f'x{i}' for i in range(X.shape[1])]) 48 | 49 | dygo = DygolemCausalDiscovery() 50 | w_est_dy, p_est_dy = dygo(X, threshold=0.3, order=2, step=5, return_dict=True) 51 | acc_dy = count_accuracy(W_true, w_est_dy != 0) 52 | print(acc_dy) 53 | for k in range(P_true.shape[2]): 54 | acc_dy = count_accuracy(P_true[:, :, k], p_est_dy[:, :, k] != 0) 55 | print(acc_dy) 56 | 57 | >>> {'fdr': 0.55, 'tpr': 0.9, 'fpr': 0.3142857142857143, 'shd': 12, 'nnz': 20} 58 | {'fdr': 0.0, 'tpr': 0.6, 'fpr': 0.0, 'shd': 7, 'nnz': 0} 59 | {'fdr': 0.0, 'tpr': 0.4, 'fpr': 0.0, 'shd': 8, 'nnz': 0} -------------------------------------------------------------------------------- /docs/en_US/source/sub/discovery/DYNotears.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | DYNOTEARS: Structure Learning from Time-Series Data 3 | ******** 4 | 5 | The problem of estimating contemporaneous (intra-slice) and time-lagged (inter-slice) relationships between variables in a time-series simultaneously can also be solved by formulating 6 | a continuous optimization problem over real matrices with the constraint enforcing the acyclicity condition [Zheng2018]_. 7 | Specifically, for a given vector :math:`x \in \mathbb{R}^d` and its time-lagged versions :math:`y \in \mathbb{R}^{p \times d}` such that there exists a matrix :math:`V` and a matrix :math:`Q` which satisifies :math:`x = Vx + Qy + \eta` for some noise vector :math:`\eta \in \mathbb{R}^d`, the optimization problem can be summarized as follows: 8 | 9 | .. math:: 10 | 11 | \min_{W, A \in \mathbb{R}^{d\times d}} & F(W, A) \\ 12 | s.t. \quad & h(W) = 0, 13 | 14 | where :math:`F(W, A)` is a continuous function measuring :math:`\|x - Wx - Ay\| + \lambda_W\|W\|_1 + \lambda_A\|A\|_1` and 15 | 16 | .. math:: 17 | 18 | h(W) = tr\left( e^{W \circ W} \right) 19 | 20 | where :math:`\circ` is the Hadamard product. This optimization can then be solved with some optimization technique, such as gradient desscent. 21 | 22 | The YLearn class for the DYNOTEARS algorithm is :class:`DyCausalDiscovery`. 23 | 24 | .. topic:: Example 25 | 26 | .. code-block:: python 27 | 28 | import pandas as pd 29 | 30 | from ylearn.exp_dataset.gen import dygen 31 | from ylearn.causal_discovery import DyCausalDiscovery 32 | 33 | X, W_true, P_true = dygen(n=500, d=10, step=5, order=2) 34 | X = pd.DataFrame(X, columns=[f'x{i}' for i in range(X.shape[1])]) 35 | dycd = DyCausalDiscovery() 36 | w_est_dy, p_est_dy = dycd(X, threshold=0.3, order=2, step=5, return_dict=True) 37 | acc_dy = count_accuracy(W_true, w_est_dy != 0) 38 | print(acc_dy) 39 | for k in range(P_true.shape[2]): 40 | acc_dy = count_accuracy(P_true[:, :, k], p_est_dy[:, :, k] != 0) 41 | print(acc_dy) 42 | 43 | >>> {'fdr': 0.0, 'tpr': 1.0, 'fpr': 0.0, 'shd': 0, 'nnz': 10} 44 | {'fdr': 0.0, 'tpr': 1.6, 'fpr': 0.0, 'shd': 0, 'nnz': 10} 45 | {'fdr': 0.8, 'tpr': 0.6, 'fpr': 0.22857142857142856, 'shd': 11, 'nnz': 10} -------------------------------------------------------------------------------- /docs/en_US/source/sub/discovery/golem.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Golem 3 | ******** 4 | 5 | The problem of revealing the structures of directed acyclic graphs (DAGs) can be solved 6 | by formulating a continuous optimization problem over real matrices with soft and sparse 7 | constraints enforcing the acyclicity condition [Ignavier2020]_. Specifically, for a given 8 | vector :math:`x \in \mathbb{R}^d` such that there exists a matrix :math:`V` which satisifies 9 | :math:`x = Vx + \eta` for some noise vector :math:`\eta \in \mathbb{R}^d`, the optimization problem can be summarized as follows: 10 | 11 | .. math:: 12 | 13 | \min_{W \in \mathbb{R}^{d\times d}} \mathcal{S} (W;x) = \mathcal{L}(W;x) + \lambda_1 ||W||_1 + \lambda_2 h(W) 14 | 15 | where :math:`\mathcal{L}(W;x)` is the Maximum Likelihood Estimator (MLE): 16 | 17 | .. math:: 18 | 19 | \mathcal{L}(W;x) = 1/2 \sum_{i=1}^d log\left( \sum_{k=1}^n ||X-WX||^2 \right) - log|det(I-W)| 20 | 21 | If one further assumes that the noise variances are equal, it becomes 22 | 23 | .. math:: 24 | 25 | \mathcal{L}(W;x) = d/2 log\left(\sum_{i=1}^d \sum_{k=1}^n ||X-WX||^2 \right) - log|det(I-W)| 26 | 27 | :math:`\lambda_1 ||W||_1` is a penalty term encouraging sparsity, i.e., having fewer edges, 28 | 29 | and :math:`\lambda_2 h(W)` is a penalty term encouraging DAGness on W. 30 | 31 | .. math:: 32 | 33 | h(W) = tr\left( e^{W \circ W} \right) 34 | 35 | where :math:`\circ` is the Hadamard product. This optimization can then be solved with some optimization technique, such as gradient desscent. 36 | 37 | The YLearn class for the Golem algorithm is :class:`GolemDiscovery`. 38 | 39 | .. topic:: Example 40 | 41 | .. code-block:: python 42 | 43 | import pandas as pd 44 | 45 | from ylearn.exp_dataset.gen import gen 46 | from ylearn.causal_discovery import GolemDiscovery 47 | 48 | X = gen() 49 | X = pd.DataFrame(X, columns=[f'x{i}' for i in range(X.shape[1])]) 50 | Gocd = GolemDiscovery(hidden_layer_dim=[3]) 51 | est = Gocd(X, threshold=0.01, return_dict=True) 52 | print(est) 53 | 54 | >>> OrderedDict([('x0', []), ('x1', ['x0', 'x2', 'x3', 'x4']), ('x2', ['x0']), ('x3', ['x0', 'x2', 'x4']), ('x4', ['x0', 'x2'])]) 55 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/discovery/notears.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | No-Tears 3 | ******** 4 | 5 | The problem of revealing the structures of directed acyclic graphs (DAGs) can be solved by formulating 6 | a continuous optimization problem over real matrices with the constraint enforcing the acyclicity condition [Zheng2018]_. 7 | Specifically, for a given vector :math:`x \in \mathbb{R}^d` such that there exists a matrix :math:`V` which satisifies :math:`x = Vx + \eta` for some noise vector :math:`\eta \in \mathbb{R}^d`, the optimization problem can be summarized as follows: 8 | 9 | .. math:: 10 | 11 | \min_{W \in \mathbb{R}^{d\times d}} & F(W) \\ 12 | s.t. \quad & h(W) = 0, 13 | 14 | where :math:`F(W)` is a continuous function measuring :math:`\|x - Wx\|` and 15 | 16 | .. math:: 17 | 18 | h(W) = tr\left( e^{W \circ W} \right) 19 | 20 | where :math:`\circ` is the Hadamard product. This optimization can then be solved with some optimization technique, such as gradient desscent. 21 | 22 | The YLearn class for the NO-TEARS algorithm is :class:`CausalDiscovery`. 23 | 24 | .. topic:: Example 25 | 26 | .. code-block:: python 27 | 28 | import pandas as pd 29 | 30 | from ylearn.exp_dataset.gen import gen 31 | from ylearn.causal_discovery import CausalDiscovery 32 | 33 | X = gen() 34 | X = pd.DataFrame(X, columns=[f'x{i}' for i in range(X.shape[1])]) 35 | cd = CausalDiscovery(hidden_layer_dim=[3]) 36 | est = cd(X, threshold=0.01, return_dict=True) 37 | 38 | print(est) 39 | 40 | >>> OrderedDict([('x0', []), ('x1', ['x0', 'x2', 'x3', 'x4']), ('x2', ['x0']), ('x3', ['x0', 'x2', 'x4']), ('x4', ['x0', 'x2'])]) -------------------------------------------------------------------------------- /docs/en_US/source/sub/est_model/forest.rst: -------------------------------------------------------------------------------- 1 | ***************************** 2 | Forest Estimator Models 3 | ***************************** 4 | 5 | 6 | Random forest is a widely used algorithm in machine learning. Many empirical properties of random forest including stability and the ability of flexible adaptation 7 | to complicated forms have made random forest and its variants as popular and reliable choices in a lot of tasks. It is then a natural and crucial idea to extend tree 8 | based models for causal effect estimation such as causal tree to forest based ones. These works are pioneered by [Athey2018]_. Similar to the case of machine 9 | learning, typically for causal effect estimation, forest estimator models have better performance than tree models while sharing equivalent interpretability and other 10 | advantages. Thus it is always recommended to try these estimator models first. 11 | 12 | In YLearn, we currently cover three types of forest estimator models for causal effect estimation under the unconfoundness asssumption: 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | forest_based/grf.rst 18 | forest_based/cf.rst 19 | forest_based/ctcf.rst 20 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/est_model/iv.rst: -------------------------------------------------------------------------------- 1 | ********************** 2 | Instrumental Variables 3 | ********************** 4 | 5 | 6 | Instrumental Variables (IV) deal with the case for estimating causal effects in the presence of unobserved confounding variables that simultaneously 7 | have effects on the treatment :math:`X` and the outcome :math:`Y`. A set of variables :math:`Z` is said to be a set of **instrumental variables** 8 | if for any :math:`z` in :math:`Z`: 9 | 10 | 1. :math:`z` has a causal effect on :math:`X`. 11 | 12 | 2. The causal effect of :math:`z` on :math:`Y` is fully mediated by :math:`X`. 13 | 14 | 3. There are no back-door paths from :math:`z` to :math:`Y`. 15 | 16 | In such case, we must first find the IV (which can be done by using the :class:`CausalModel`, see :ref:`identification`). For an instance, the variable 17 | :math:`Z` in the following figure can serve as a valid IV for estimating the causal effects of :math:`X` on :math:`Y` in the presence of the unobserved confounder 18 | :math:`U`. 19 | 20 | .. figure:: iv3.png 21 | 22 | Causal graph with IV 23 | 24 | YLearn implements two different methods related to IV: deepiv [Hartford]_, which utilizes the deep learning models to IV, and IV of nonparametric models [Newey2002]_. 25 | 26 | The IV Framework and Problem Setting 27 | ==================================== 28 | The IV framework aims to predict the value of the outcome :math:`y` when the treatment :math:`x` is given. Besides, there also exist some covariates vectors :math:`v` that 29 | simultaneously affect both :math:`y` and :math:`x`. There also are some unobserved confounders :math:`e` that potentially also affect :math:`y`, :math:`x` and :math:`v`. The core part 30 | of causal questions lies in estimating the causal quantity 31 | 32 | .. math:: 33 | 34 | \mathbb{E}[y| do(x)] 35 | 36 | in the following causal graph, where the set of causal relationships are determined by the set of functions 37 | 38 | .. math:: 39 | 40 | y & = f(x, v) + e\\ 41 | x & = h(v, z) + \eta\\ 42 | \mathbb{E}[e] & = 0. 43 | 44 | .. figure:: iv4.png 45 | 46 | Causal graph with IV and both observed and unobserved confounders 47 | 48 | The IV framework solves this problem by doing a two-stage estimation: 49 | 50 | 1. Estimate :math:`\hat{H}(z, v)` that captures the relationship between :math:`x` and the variables :math:`(z, v)`. 51 | 52 | 2. Replace :math:`x` with the predicted result of :math:`\hat{H}(z, v)` given :math:`(v, z)`. Then estimate :math:`\hat{G}(x, v)` to build the relationship between :math:`y` and :math:`(x, v)`. 53 | 54 | The final casual effects can then be calculated. 55 | 56 | IV Classes 57 | =========== 58 | 59 | .. toctree:: 60 | :maxdepth: 2 61 | 62 | iv/iv_normal 63 | iv/deepiv -------------------------------------------------------------------------------- /docs/en_US/source/sub/est_model/iv/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/est_model/iv/.DS_Store -------------------------------------------------------------------------------- /docs/en_US/source/sub/est_model/iv3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/est_model/iv3.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/est_model/iv4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/est_model/iv4.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/flow.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/interpreter.rst: -------------------------------------------------------------------------------- 1 | ****************************************** 2 | Interpreter: Explaining the Causal Effects 3 | ****************************************** 4 | 5 | To interpret the causal effects estimated by various estimator models, YLearn implements tree models :class:`CEInterpreter` for causal effect 6 | interpretabilities and :class:`PolicyInterpreter` for policy evaluation interpretabilities in the current version. 7 | 8 | 9 | .. toctree:: 10 | 11 | inter_model/ce_interpreter 12 | inter_model/policy_interpreter -------------------------------------------------------------------------------- /docs/en_US/source/sub/intro.rst: -------------------------------------------------------------------------------- 1 | *************************************** 2 | Overview of YLearn and Causal Inference 3 | *************************************** 4 | 5 | Machine learning has made great achievements in recent years. 6 | The areas in which machine learning succeeds are mainly for prediction, 7 | e.g., the classification of pictures of cats and dogs. However, machine learning is incapable of answering some 8 | questions that naturally arise in many scenarios. One example is for the **counterfactual questions** in policy 9 | evaluations: what would have happened if the policy had changed? Due to the fact that these counterfactuals can 10 | not be observed, machine learning models, the prediction tools, can not be used. These incapabilities of machine 11 | learning partly give rise to applications of causal inference in these days. 12 | 13 | Causal inference directly models the outcome of interventions and formalizes the counterfactual reasoning. 14 | With the aid of machine learning, causal inference can draw causal conclusions from observational data in 15 | various manners nowadays, rather than relying on conducting craftly designed experiments. 16 | 17 | A typical complete causal inference procedure is composed of three parts. First, it learns causal relationships 18 | using the technique called causal discovery. These relationships are then expressed either in the form of Structural 19 | Causal Models or Directed Acyclic Graphs (DAG). Second, it expresses the causal estimands, which are clarified by the 20 | interested causal questions such as the average treatment effects, in terms of the observed data. This process is 21 | known as identification. Finally, once the causal estimand is identified, causal inference proceeds to focus on 22 | estimating the causal estimand from observational data. Then policy evaluation problems and counterfactual questions 23 | can also be answered. 24 | 25 | YLearn, equipped with many techniques developed in recent literatures, is implemented to support the whole causal 26 | inference pipeline from causal discovery to causal estimand estimation with the help of machine learning. This is 27 | more promising especially when there are abundant observational data. 28 | 29 | Concepts in YLearn and their related problem settings 30 | ===================================================== 31 | There are 5 main components in YLearn corresponding to the causal inference pipeline. 32 | 33 | .. figure:: structure_ylearn.png 34 | 35 | *Components in YLearn* 36 | 37 | 1. *Causal Discovery*. Discovering the causal relationships in the observational data. 38 | 39 | 2. *Causal Model*. Representing the causal relationships in the form of ``CausalGraph`` and doing other related operations such as identification with ``CausalModel``. 40 | 41 | 3. *Estimator Model*. Estimating the causal estimands with various techniques. 42 | 43 | 4. *Policy Model*. Selecting the best policy for each individual. 44 | 45 | 5. *Interpreters*. Explain the causal effects and polices. 46 | 47 | These components are connected to give a full pipeline of causal inference, which are also encapsulated into a single API `Why`. 48 | 49 | .. figure:: flow.png 50 | 51 | *The pipeline of causal inference in YLearn*. Starting from the training data, one first uses the `CausalDiscovery` to reveal 52 | the causal structures in data, which will usually output a `CausalGraph`. The causal graph is then passed into the `CausalModel`, where 53 | the interested causal effects are identified and converted into statistical estimands. An `EstimatorModel` is then trained with the training data 54 | to model relationships between causal effects and other variables, i.e., estimating causal effects in training data. One can then 55 | use the trained `EstimatorModel` to predict causal effects in some new test dataset and evaluate the policy assigned to each individual or interpret 56 | the estimated causal effects. 57 | 58 | All APIs are introduced in :ref:`api`. 59 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/quick_start.rst: -------------------------------------------------------------------------------- 1 | *********** 2 | Quick Start 3 | *********** 4 | 5 | In this part, we first show several simple example usages of YLearn. These examples cover the most common functionalities. Then we present a case study with :class:`Why` to unveil the hidden 6 | causal relations in data. 7 | 8 | Example usages 9 | ============== 10 | 11 | We present several necessary example usages of YLearn in this section, which covers defining a causal graph, identifying the causal effect, and training an estimator model, etc. Please see their specific documentations to for more details. 12 | 13 | 1. Representation of causal graph 14 | 15 | Given a set of variables, the representation of its causal graph in YLearn requires a python :py:class:`dict` to denote the causal relations of variables, in which the *keys* of the :py:class:`dict` are children of all elements in the 16 | corresponding values which usually should be a list of names of variables. For an instance, in the simplest case, for a given causal graph :math:`X \leftarrow W \rightarrow Y`, we first define a python :py:class:`dict` for the causal relations, 17 | which will then be passed to :py:class:`CausalGraph` as a parameter: 18 | 19 | .. code-block:: python 20 | 21 | causation = {'X': ['W'], 'W':[], 'Y':['W']} 22 | cg = CausalGraph(causation=causation) 23 | 24 | :py:attr:`cg` will be the causal graph encoding the causal relation :math:`X \leftarrow W \rightarrow Y` in YLearn. If there exist unobserved confounders in the causal graph, then, aside from the observed variables, we should also define a python 25 | :py:class:`list` containing these causal relations. See :ref:`causal_graph` for more details. 26 | 27 | 2. Identification of causal effect 28 | 29 | It is crucial to identify the causal effect when we want to estimate it from data. The first step for identifying the causal effect is identifying the causal estimand. This can be easily done in YLearn. For an instance, suppose that we are interested in identifying the causal estimand :math:`P(Y|do(X=x))` in the causal graph `cg`, then we should 30 | first define an instance of :class:`CausalModel` and call the :py:func:`identify()` method: 31 | 32 | .. code-block:: python 33 | 34 | cm = CausalModel(causal_graph=cg) 35 | cm.identify(treatment={'X'}, outcome={'Y'}, identify_method=('backdoor', 'simple')) 36 | 37 | where we use the *backdoor-adjustment* method here. YLearn also support front-door adjustment, finding instrumental variables, and, most importantly, the general identification method developed in [Pearl]_ which is able to identify any causal effect if it is identifiable. 38 | 39 | 3. Estimation of causal effect 40 | 41 | The estimation of causal effects in YLearn is also fairly easy. It follows the common approach of deploying a machine learning model since YLearn focuses on the intersection of machine learning and causal inference in this part. Given a dataset, one can apply any 42 | :class:`EstimatorModel` in YLearn with a procedure composed of 3 steps: 43 | 44 | * Given data in the form of :class:`pandas.DataFrame`, find the names of `treatment, outcome, adjustment, covariate`. 45 | * Call :py:func:`fit()` method of :class:`EstimatorModel` to train the model. 46 | * Call :py:func:`estimate()` method of :class:`EstimatorModel` to estimate causal effects in test data. 47 | 48 | See :ref:`estimator_model` for more details. 49 | 50 | 4. Using the all-in-one API: Why 51 | 52 | For the purpose of applying YLearn in a unified and eaiser manner, YLearn provides the API :py:class:`Why`. :py:class:`Why` is an API which encapsulates almost everything in YLearn, such as identifying causal effects and scoring a trained estimator model. 53 | To use :py:class:`Why`, one should first create an instance of :py:class:`Why` which needs to be trained by calling its method :py:func:`fit()`, after which other utilities, such as :py:func:`causal_effect()`, :py:func:`score()`, and :py:func:`whatif()`, 54 | could be used. This procedure is illustrated in the following code example: 55 | 56 | .. code-block:: python 57 | 58 | from sklearn.datasets import fetch_california_housing 59 | 60 | from ylearn import Why 61 | 62 | housing = fetch_california_housing(as_frame=True) 63 | data = housing.frame 64 | outcome = housing.target_names[0] 65 | data[outcome] = housing.target 66 | 67 | why = Why() 68 | why.fit(data, outcome, treatment=['AveBedrms', 'AveRooms']) 69 | 70 | print(why.causal_effect()) 71 | 72 | -------------------------------------------------------------------------------- /docs/en_US/source/sub/ref.rst: -------------------------------------------------------------------------------- 1 | References 2 | ========== 3 | 4 | .. [Pearl] 5 | J. Pearl. Causility : models, reasoning, and inference. 6 | 7 | .. [Shpitser2006] 8 | S. Shpitser and J. Pearl. Identification of Joint Interventional Distributions in Recursive Semi-Markovian Causal Models. 9 | 10 | .. [Neal2020] 11 | B. Neal. Introduction to Causal Inference. 12 | 13 | .. [Funk2010] 14 | M. Funk, et al. Doubly Robust Estimation of Causal Effects. 15 | 16 | .. [Chern2016] 17 | 18 | V. Chernozhukov, et al. Double Machine Learning for Treatment and Causal Parameters. *arXiv:1608.00060.* 19 | 20 | .. [Athey2015] 21 | 22 | S. Athey and G. Imbens. Recursive Partitioning for Heterogeneous Causal Effects. *arXiv: 1504.01132.* 23 | 24 | .. [Schuler] 25 | 26 | A. Schuler, et al. A comparison of methods for model selection when estimating individual treatment effects. *arXiv:1804.05146.* 27 | 28 | .. [Nie] 29 | 30 | X. Nie, et al. Quasi-Oracle estimation of heterogeneous treatment effects. 31 | *arXiv: 1712.04912.* 32 | 33 | .. [Hartford] 34 | 35 | J. Hartford, et al. Deep IV: A Flexible Approach for Counterfactual Prediction. *ICML 2017.* 36 | 37 | .. [Newey2002] 38 | 39 | W. Newey and J. Powell. Instrumental Variable Estimation of Nonparametric Models. *Econometrica 71, no. 5 (2003): 1565–78.* 40 | 41 | .. [Kunzel2019] 42 | 43 | S. Kunzel2019, et al. Meta-Learners for Estimating Heterogeneous Treatment Effects using Machine Learning. 44 | 45 | .. [Angrist1996] 46 | 47 | J. Angrist, et al. Identification of causal effects using instrumental variables. *Journal of the American Statistical Association*. 48 | 49 | .. [Athey2020] 50 | 51 | S. Athey and S. Wager. Policy Learning with Observational Data. *arXiv: 1702.02896.* 52 | 53 | .. [Spirtes2001] 54 | 55 | P. Spirtes, et al. Causation, Prediction, and Search. 56 | 57 | .. [Zheng2018] 58 | 59 | X. Zheng, et al. DAGs with NO TEARS: Continuous Optimization for Structure Learning. *arXiv: 1803.01422.* 60 | 61 | .. [Athey2018] 62 | 63 | S. Authey, et al. Generalized Random Forests. *arXiv: 1610.01271* -------------------------------------------------------------------------------- /docs/en_US/source/sub/structure_ylearn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/en_US/source/sub/structure_ylearn.png -------------------------------------------------------------------------------- /docs/en_US/source/sub/sub.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | intro 8 | quick_start 9 | api -------------------------------------------------------------------------------- /docs/zh_CN/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | # SPHINXBUILD ?= sphinx-build 8 | SPHINXBUILD = python3.8 -msphinx 9 | SOURCEDIR = source 10 | BUILDDIR = build 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /docs/zh_CN/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/zh_CN/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | numpy 3 | pandas 4 | scikit-learn 5 | torch 6 | IPython 7 | networkx -------------------------------------------------------------------------------- /docs/zh_CN/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | import sys 18 | import os 19 | 20 | sys.path.insert(0, os.path.abspath("../../..")) 21 | # sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = "YLearn" 25 | copyright = "2022, DataCanvasLab" 26 | author = "DataCanvasLab" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.napoleon", 36 | "sphinx.ext.autodoc", 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = [] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = "sphinx_rtd_theme" 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ["_static"] 59 | 60 | # import ylearn 61 | # print("cwd: ") 62 | # print(os.getcwd()) 63 | # build_cmd = f"cd ../.. && {sys.executable} setup.py build_ext --inplace" 64 | # # print(build_cmd) 65 | 66 | # os.system(build_cmd) 67 | -------------------------------------------------------------------------------- /docs/zh_CN/source/index.rst: -------------------------------------------------------------------------------- 1 | .. YLearn documentation master file, created by 2 | sphinx-quickstart on Tue Jun 7 18:29:01 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 欢迎来到 YLearn 文档! 7 | ================================== 8 | 9 | YLearn是一个因果学习算法工具包,这个名字是“learn why”的谐音。它主要支持因果学习任务中的各类相关任务,从因果效应识别(causal effect idenfitication),到因果效应估计(causal effect estimation),到因果发现(causal discovery)等等,都可以通过YLearn实现,最大程度地降低了学习一个因果工具的成本。 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | sub/sub 15 | sub/causal_model 16 | sub/est 17 | sub/causal_discovery 18 | sub/policy 19 | sub/interpreter 20 | sub/why 21 | sub/ref 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | **************************** 4 | API: 与YLearn交互 5 | **************************** 6 | 7 | .. list-table:: 一体化的API 8 | 9 | * - 类名 10 | - 描述 11 | * - :py:class:`Why` 12 | - 一个API封装了YLearn中几乎所有的东西,比如 *识别因果效应* 和 *给一个训练过的估计模型打分* 。 它给用户提供了简单和有效的方法使用YLearn。 13 | 14 | .. list-table:: 因果结构发现 15 | 16 | * - 类名 17 | - 描述 18 | * - :py:class:`CausalDiscovery` 19 | - 发现观测数据中的因果结构。 20 | 21 | .. list-table:: 因果模型 22 | 23 | * - 类名 24 | - 描述 25 | * - :py:class:`CausalGraph` 26 | - 表示因果结构和支持因果图其他相关的操作,例如加减图中的边。 27 | * - :py:class:`CausalModel` 28 | - 编码由 :py:class:`CausalGraph` 表示的因果关系。主要支持因果效应识别,比如后门调整。 29 | * - :py:class:`Prob` 30 | - 表示概率分布。 31 | 32 | .. list-table:: 估计模型 33 | 34 | * - 类名 35 | - 描述 36 | * - :py:class:`ApproxBound` 37 | - 一个用于估计因果效应上下界的模型。该模型不需要无混杂条件。 38 | * - :py:class:`CausalTree` 39 | - 一个通过决策树估计因果效应的类。需要无混杂条件。 40 | * - :py:class:`DeepIV` 41 | - 具有深度神经网络的工具变量。必须提供工具变量的名字。 42 | * - :py:class:`NP2SLS` 43 | - 无参数的工具变量。必须提供工具变量的名字。 44 | * - :py:class:`DoubleML` 45 | - 双机器学习模型用于估计CATE。需要无混杂条件。 46 | * - :py:class:`DoublyRobust` and :py:class:`PermutedDoublyRobust` 47 | - 双鲁棒方法用于估计CATE。置换的版本考虑了所有可能的治疗控制对。需要无混杂条件且治疗必须是离散的。 48 | * - :py:class:`SLearner` and :py:class:`PermutedSLearner` 49 | - SLearner。 置换的版本考虑了所有可能的治疗控制对。需要无混杂条件且治疗必须是离散的。 50 | * - :py:class:`TLearner` and :py:class:`PermutedTLearner` 51 | - 使用了多个机器学习模型的TLearner。置换的版本考虑了所有可能的治疗控制对。需要无混杂条件且治疗必须是离散的。 52 | * - :py:class:`XLearner` and :py:class:`PermutedXLearner` 53 | - 使用了多个机器学习模型的XLearner。置换的版本考虑了所有可能的治疗控制对。需要无混杂条件且治疗必须是离散的。 54 | * - :py:class:`RLoss` 55 | - 通过测量估计模型的效果得到效果分。需要无混杂条件。 56 | 57 | .. list-table:: 策略 58 | 59 | * - 类名 60 | - 描述 61 | * - :py:class:`PolicyTree` 62 | - 一个通过树模型和最大化因果效应来找到最优的策略的类。 63 | 64 | .. list-table:: 解释器 65 | 66 | * - 类名 67 | - 描述 68 | * - :py:class:`CEInterpreter` 69 | - 一个使用决策树模型的对象,用于解释估计的CATE。 70 | * - :py:class:`PolicyInterpreter` 71 | - 一个用于解释由一些 :py:class:`PolicyModel` 给出的策略的对象。 72 | 73 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_discovery.rst: -------------------------------------------------------------------------------- 1 | 因果发现:探索数据中的因果结构 2 | ========================================================= 3 | 4 | .. toctree:: 5 | discovery/notears 6 | 7 | 在因果推断任务中,首先要找到潜在的因果关系,即所谓的因果结构。理论上,这些关系能够通过设计随机实验或者施加干涉被揭露。 然而,现实中这些方法的代价过于昂贵,有些甚至不可行。因此,近年来许多通过直接利用观测数据来分析因果结构的技术被提出来,如 PC算法(参考 [Spirtes2001]_) ,No-Tears算法(参考 [Zheng2018]_)。这些技术称为**因果发现** 。 8 | 9 | YLearn因果发现实现了一个基于分数的 No-Tears 算法。之后会陆续加入更多的方法。 10 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_model.rst: -------------------------------------------------------------------------------- 1 | ***************************************************** 2 | 因果模型:表示因果结构 3 | ***************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | causal_model/graph 9 | causal_model/causal_model 10 | causal_model/prob 11 | 12 | 13 | 对于一组变量 :math:`V`,它的 **因果结构** 能够被一个有向无环图(DAG)表示,其中 DAG 中每个节点对应于 :math:`V` 中的一个变量;变量间的直接函数关系由DAG中的一个带箭头的连接线表示。用 DAG 表示的因果结构能够精确地说明每一个变量是如何被它的父节点影响的。举个例子, :math:`X \leftarrow W \rightarrow Y` 表明 14 | :math:`W` 是一个父节点,也是变量 :math:`X` 和 :math:`Y` 共同的因。更具体一点,对于两个不同的变量 :math:`V_i` 和 :math:`V_j`,如果它们的函数关系是 15 | 16 | .. math:: 17 | 18 | V_j = f(V_i, \eta_{ij}) 19 | 20 | 其中 :math:`f` 为函数关系, :math:`\eta` 表示噪声,那么用 DAG 来表示这个因果结构时,应该有一个箭头从 :math:`V_i` 指向 :math:`V_j`。详细的介绍这种因果结构的DAGs可以在 [Pearl]_ 中找到。 21 | 22 | 根据 Pearl 的理论,因果效应(也称因果估计量)能够用 :math:`do-operator` 表示。例如表达式: 23 | 24 | .. math:: 25 | 26 | P(y|do(x)) 27 | 28 | 代表在施加干涉 :math:`x` 后 :math:`y` 的概率函数。因果结构对于表达和估计感兴趣的因果估计量至关重要。YLearn实现了一个对象 ``CausalGraph`` 29 | 来支持表示因果结构和对因果结构的相关操作。请参考 :ref:`causal_graph` 来获得更多细节。 30 | 31 | YLearn关心因果推断和机器学习的交叉应用,因此我们假设使用的数据是足够多的观测数据,而不是需要设计随机实验得到的实验数据。对于一个表示因果结构的DAG,因果估计量通常不能被直接的从数据中估计出来,(如平均治疗效应(ATEs))。因为反事实结果是不能够被观测到的。因此有必要在进行任何估计之前,将这些因果估计量转化为其他的能够从数据中估计出来的量,它们被称为统计估计量。把因果估计量转化为对应的统计估计量的过程称为 **识别**。 32 | 33 | 支持识别和其他因果结构相关操作的对象是 ``CausalModel``。更多的细节可以在 :ref:`causal_model` 中找到。 34 | 35 | 在Pearl的因果推断语言中,也有必要把结果表示为概率的语言。为了这个目的,YLearn也实现了一个对象 :class:`Prob`, 其在 :ref:`prob` 中介绍。 36 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_model/backdoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/causal_model/backdoor.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_model/graph_expun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/causal_model/graph_expun.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_model/graph_un_arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/causal_model/graph_un_arc.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_model/iv1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/causal_model/iv1.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_model/iv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/causal_model/iv2.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/causal_model/prob.rst: -------------------------------------------------------------------------------- 1 | .. _prob: 2 | 3 | ***************************** 4 | 概率表达式 5 | ***************************** 6 | 7 | YLearn能够输出和修改类似如下的概率表达式: 8 | 9 | .. math:: 10 | 11 | P(x, y|z), 12 | 13 | 用户能够定义一个 :class:`Prob` 的实例,以及改变它的属性. 14 | 15 | .. topic:: 举例 16 | 17 | 如一个概率表达式: 18 | 19 | .. math:: 20 | 21 | \sum_{w}P(v|y)[P(w|z)P(x|y)P(u)] 22 | 23 | 它由两个部分组成:一个条件概率:math:`P(v|y)` 和一个多概率的乘积 :math:`P(w|z)P(x|y)P(u)` ,然后这两个部分的积在给定的:math:`w` 下求和。 24 | 25 | 首先定义第一个条件概率:math:`P(v|y)` : 26 | 27 | .. code-block:: python 28 | 29 | from ylearn.causal_model.prob import Prob 30 | 31 | var = {'v'} 32 | conditional = {'y'} # the conditional set 33 | marginal = {'w'} # sum on w 34 | 35 | 然后, 继续定义第二个多条件概率的乘积 :math:`P(w|z)P(x|y)P(u)` 36 | 37 | .. code-block:: python 38 | 39 | p1 = Prob(variables={'w'}, conditional={'z'}) 40 | p2 = Prob(variables={'x'}, conditional={'y'}) 41 | p3 = Prob(variables={'u'}) 42 | product = {p1, p2, p3} 43 | 44 | 最终的结果可以表示为: 45 | 46 | .. code-block:: python 47 | 48 | P = Prob(variables=var, conditional=conditional, marginal=marginal, product=product) 49 | P.show_latex_expression() 50 | 51 | >>> :math:`\sum_w P(v|y)[P(u)][P(w|z)][P(x|y)]` 52 | 53 | :class:`Prob` 的实例还可以输出LaTex代码: 54 | 55 | .. code-block:: python 56 | 57 | P.parse() 58 | 59 | >>> '\\sum_{w}P(v|y)\\left[P(u)\\right]\\left[P(w|z)\\right]\\left[P(x|y)\\right]' 60 | 61 | .. py:class:: ylearn.causal_model.prob.Prob(variables=set(), conditional=set(), divisor=set(), marginal=set(), product=set()) 62 | 63 | 一个概率分布表达式如下: 64 | 65 | .. math:: 66 | 67 | \sum_{w}P(v|y)[P(w|z)P(x|y)P(u)]. 68 | 69 | 用上述例子来阐明参数的含义: 70 | 71 | :param set, default=set() variables: The variables (:math:`v` in the above example) of the probability. 72 | :param set, default=set() conditional: The conditional set (:math:`y` in the above example) of the probability. 73 | :param set, default=set() marginal: The sum set (:math:`w` in the above example) for marginalizing the probability. 74 | :param set, default=set() product: If not set(), then the probability is composed of the first probability 75 | object :math:`(P(v|y))` and several other probability objects that are all saved 76 | in the set product, e.g., product = {P1, P2, P3} where P1 for :math:`P(w|z)`, 77 | P2 for :math:`P(x|y)`, and P3 for :math:`P(u)` in the above example. 78 | 79 | .. py:method:: parse 80 | 81 | 返回概率分布的表达式 82 | 83 | :returns: Expression of the encoded probability 84 | :rtype: str 85 | 86 | .. py:method:: show_latex_expression 87 | 88 | 显示latex表达式 89 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/discovery/notears.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | No-Tears 算法 3 | ******** 4 | 5 | No-Tears 揭示了有向无环图 (DAG) 结构的问题可以通过在具有强制执行无环条件的约束的实矩阵上制定连续优化问题来解决 [Zheng2018]_. 6 | 具体地说,对于给定的向量 :math:`x \in \mathbb{R}^d` ,存在一个矩阵 :math:`V` ,满足某个噪声向量 :math:`x = Vx + \eta` ,使 :math:`\eta \in \mathbb{R}^d`的优化问题可以描述如下: 7 | 8 | .. math:: 9 | 10 | \min_{W \in \mathbb{R}^{d\times d}} & F(W) \\ 11 | s.t. \quad & h(W) = 0, 12 | 13 | 其中, :math:`F(W)` 是一个衡量 :math:`\|x - Wx\|` 的连续方程; 14 | 15 | 此外, 16 | 17 | .. math:: 18 | 19 | h(W) = tr\left( e^{W \circ W} \right) 20 | 21 | 其中 :math:`\circ` 是阿达玛乘积(Hadamard product)。整个公式可以通过一些优化技术来解决,比如梯度下降。 22 | 23 | NO-TEARS 算法的类是 :class:`CausalDiscovery`. 24 | 25 | .. topic:: 举例 26 | 27 | .. code-block:: python 28 | 29 | import pandas as pd 30 | 31 | from ylearn.exp_dataset.gen import gen 32 | from ylearn.causal_discovery import CausalDiscovery 33 | 34 | X = gen() 35 | X = pd.DataFrame(X, columns=[f'x{i}' for i in range(X.shape[1])]) 36 | cd = CausalDiscovery(hidden_layer_dim=[3]) 37 | est = cd(X, threshold=0.01, return_dict=True) 38 | 39 | print(est) 40 | 41 | >>> OrderedDict([('x0', []), ('x1', ['x0', 'x2', 'x3', 'x4']), ('x2', ['x0']), ('x3', ['x0', 'x2', 'x4']), ('x4', ['x0', 'x2'])]) 42 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/est_model/iv.rst: -------------------------------------------------------------------------------- 1 | ********************** 2 | 工具变量 3 | ********************** 4 | 5 | 工具变量(IV)处理这样的情况:在存在未观察到的混淆变量,其同时影响治疗 :math:`X` 和结果 :math:`Y` 时,对因果效应进行估计。一组变量 :math:`Z` 被称为一组 **工具变量** 如果对于 6 | 任何在 :math:`Z` 中的 :math:`z` 。 7 | 8 | 1. :math:`z` 对 :math:`X` 有因果效应。 9 | 10 | 2. :math:`z` 对 :math:`Y` 的因果效应完全由 :math:`X` 调节。 11 | 12 | 3. 从 :math:`z` 到 :math:`Y` 没有后门路径。 13 | 14 | 在这样的情况下,我们必须首先找到IV(其可以通过使用 :class:`CausalModel` 完成,参考 :ref:`identification` )。举个例子,变量 :math:`Z` 在下面的图中 15 | 可以作为一个合理的IV,在存在未观察到的混淆变量 :math:`U` 时估计 :math:`X` 对 :math:`Y` 的因果效应。 16 | 17 | .. figure:: iv3.png 18 | 19 | Causal graph with IV 20 | 21 | YLearn关于IV实现两个不同的方法:deepiv [Hartford]_ ,对IV使用深度学习模型和IV的无参数模型 [Newey2002]_ 。 22 | 23 | IV框架和问题设置 24 | ==================================== 25 | IV框架的目的是预测结果 :math:`y` 的值当治疗 :math:`x` 给定时。除此之外,还存在一些协变量向量 :math:`v` 其同时影响 :math:`y` 和 :math:`x`。 26 | 还有一些未观察到的混淆因素 :math:`e` 其潜在影响 :math:`y` ,:math:`x` 和 :math:`v` 。因果问题的核心部分是估计因果量。 27 | 28 | .. math:: 29 | 30 | \mathbb{E}[y| do(x)] 31 | 32 | 下面的因果图,其中因果关系的集合由函数的集合决定 33 | 34 | .. math:: 35 | 36 | y & = f(x, v) + e\\ 37 | x & = h(v, z) + \eta\\ 38 | \mathbb{E}[e] & = 0. 39 | 40 | .. figure:: iv4.png 41 | 42 | Causal graph with IV and both observed and unobserved confounders 43 | 44 | IV框架通过做两步估计解决这个问题: 45 | 46 | 1. 估计 :math:`\hat{H}(z, v)` 其捕获在 :math:`x` 和变量 :math:`(z, v)` 之间的关系。 47 | 48 | 2. 用预测的结果 :math:`\hat{H}(z, v)` 取代 :math:`x` 给定 :math:`(v, z)` 。接着估计 :math:`\hat{G}(x, v)` 来构建 :math:`y` 和 :math:`(x, v)` 之间的关系。 49 | 50 | 最终的因果效应能够被计算。 51 | 52 | IV Classes 53 | =========== 54 | 55 | .. toctree:: 56 | :maxdepth: 2 57 | 58 | iv/iv_normal 59 | iv/deepiv -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/est_model/iv/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/est_model/iv/.DS_Store -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/est_model/iv/deepiv.rst: -------------------------------------------------------------------------------- 1 | ****** 2 | DeepIV 3 | ****** 4 | 5 | DeepIV,开发于 [Hartford]_ ,是一个在存在未观察到的混淆因素的情况下,估计治疗和结果变量之间因果效应的方法。当工具变量(IV)存在时,它应用深度 6 | 学习方法来准确表征治疗和结果之间的因果关系。由于深度学习模型的表示力,它没有假设因果关系的任何参数形式。 7 | 8 | 训练一个DeepIV有两步,类似于正常IV方法的估计过程。具体的,我们 9 | 10 | 1. 训练一个神经网络,我们将其称为 *治疗网络* :math:`F(Z, V)` ,来估计治疗 :math:`X` 的分布,给定IV :math:`Z` 和协变量 :math:`V` 。 11 | 12 | 2. 训练另一个神经网络,我们将其称为 *结果网络* :math:`H(X, V)` ,来估计结果 :math:`Y` 给定治疗 :math:`X` 和协变量 :math:`V`。 13 | 14 | 最终的因果效应接着可以被结果网络 :math:`H(X, W)` 估计。举个例子,CATE :math:`\tau(v)` 被这样估计 15 | 16 | .. math:: 17 | 18 | \tau(v) = H(X=x_t, V=v) - H(X=x_0, W=v). 19 | 20 | 21 | 类结构 22 | ================ 23 | 24 | .. py:class:: ylearn.estimator_model.deepiv.DeepIV(x_net=None, y_net=None, x_hidden_d=None, y_hidden_d=None, num_gaussian=5, is_discrete_treatment=False, is_discrete_outcome=False, is_discrete_instrument=False, categories='auto', random_state=2022) 25 | 26 | :param ylearn.estimator_model.deepiv.Net, optional, default=None x_net: 表示对于连续的治疗的混合密度网络或者是对于离散的治疗的常见的分类网络。如果是 27 | None,默认的神经网络将被使用。参考 :py:class:`ylearn.estimator_model.deepiv.Net` 。 28 | :param ylearn.estimator_model.deepiv.Net, optional, default=None y_net: 表示结果网络。如果是None,默认的神经网络将被使用。 29 | :param int, optional, default=None x_hidden_d: DeepIV默认的x_net的隐藏层的维度。 30 | :param int, optional, default=None y_hidden_d: DeepIV默认的y_net的隐藏层的维度。 31 | :param bool, default=False is_discrete_treatment: 32 | :param bool, default=False is_discrete_instrument: 33 | :param bool, default=False is_discrete_outcome: 34 | 35 | :param int, default=5 num_gaussian: 使用混合密度网络时的高斯数,当治疗是离散的时候,其将被直接忽略。 36 | :param int, default=2022 random_state: 37 | :param str, optional, default='auto' categories: 38 | 39 | .. py:method:: fit(data, outcome, treatment, instrument=None, adjustment=None, approx_grad=True, sample_n=None, y_net_config=None, x_net_config=None, **kwargs) 40 | 41 | 训练DeepIV模型。 42 | 43 | :param pandas.DataFrame data: 训练估计器的训练数据集。 44 | :param list of str, optional outcome: 结果的名字。 45 | :param list of str, optional treatment: 治疗的名字。 46 | :param list of str, optional instrument: IV的名字。DeepIV必须提供。 47 | :param list of str, optional, default=None adjustment: 保证无混淆的调整集的名字,在当前版本其也可以被看作协变量。 48 | :param bool, default=True approx_grad: 是否使用近似梯度,和 [Hartford]_ 中一样。 49 | :param int, optional, default=None sample_n: 当使用approx_grad技术时,新样本的次数。 50 | :param dict, optional, default=None x_net_config: x_net的配置。 51 | :param dict, optional, default=None y_net_config: y_net的配置。 52 | 53 | :returns: 训练的DeepIV模型 54 | :rtype: DeepIV的实例 55 | 56 | .. py:method:: estimate(data=None, treat=None, control=None, quantity=None, marginal_effect=False, *args, **kwargs) 57 | 58 | 用量的类型估计因果效应。 59 | 60 | :param pandas.DataFrame, optional, default=None data: 测试数据。注意被设置为None,模型会使用训练数据。 61 | :param str, optional, default=None quantity: 返回的估计结果的选项。量的可能值包括: 62 | 63 | 1. *'CATE'* : 估计器将会估计CATE; 64 | 65 | 2. *'ATE'* : 估计器将会估计ATE; 66 | 67 | 3. *None* : 估计器将会估计ITE或CITE。 68 | :param int, optional, default=None treat: 治疗的值,默认是None。如果是None,那么模型会把treat设置为1。 69 | :param int, optional, default=None control: 控制的值,默认是None。如果是None,那么模型会把control设置为1。 70 | 71 | :returns: 估计的因果效应 72 | :rtype: torch.tensor 73 | 74 | .. py:method:: effect_nji(data=None) 75 | 76 | 用不同的治疗值计算因果效应。 77 | 78 | :returns: 不同治疗值的因果效应。 79 | :rtype: ndarray 80 | 81 | .. py:method:: comp_transormer(x, categories='auto') 82 | 83 | 把离散的治疗正确转变为独热向量。 84 | 85 | :param numpy.ndarray, shape (n, x_d) x: 包含治疗变量信息的数组。 86 | :param str or list, optional, default='auto' categories: 87 | 88 | :returns: 转变的独热向量。 89 | :rtype: numpy.ndarray -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/est_model/iv/iv_normal.rst: -------------------------------------------------------------------------------- 1 | ************************************ 2 | 无参工具变量 3 | ************************************ 4 | 5 | 两阶段最小平方 6 | ======================= 7 | 当结果 :math:`y` ,治疗 :math:`x` 和协变量 covariate :math:`v` 之间的关系假设是线性的,比如, [Angrist1996]_ , 8 | 9 | .. math:: 10 | 11 | y & = \alpha x + \beta v + e \\ 12 | x & = \gamma z + \lambda v + \eta, 13 | 14 | 那么IV框架变得直接:它首先给定 :math:`z` 和 :math:`v` 为 :math:`x` 训练一个线性模型。接着,在第二阶段,它把 :math:`x` 替换为估计值 :math:`\hat{x}` 来 15 | 为 :math:`y` 训练一个线性模型。这个过程被称为两阶段最小平方(2SLS)。 16 | 17 | 无参IV 18 | ================ 19 | 移除关于变量之间关系的线性假设,无参IV能够取代线性回归,通过线性投影到一系列有名的基函数 [Newey2002]_ 。 20 | 21 | 这个方法和传统的2SLS类似且在找到 :math:`x` , :math:`v` ,和 :math:`z` 的新特征之后也由两个阶段组成, 22 | 23 | .. math:: 24 | 25 | \tilde{z}_d & = f_d(z)\\ 26 | \tilde{v}_{\mu} & = g_{\mu}(v), 27 | 28 | 其由一些非线性函数(基函数) :math:`f_d` 和 :math:`g_{\mu}` 表示。在变换到新的空间后,我们接着 29 | 30 | 1. 拟合治疗模型: 31 | 32 | .. math:: 33 | 34 | \hat{x}(z, v, w) = \sum_{d, \mu} A_{d, \mu} \tilde{z}_d \tilde{v}_{\mu} + h(v, w) + \eta 35 | 36 | 2. 生成新的治疗x_hat,接着拟合结果模型 37 | 38 | .. math:: 39 | 40 | y(\hat{x}, v, w) = \sum_{m, \mu} B_{m, \mu} \psi_m(\hat{x}) \tilde{v}_{\mu} + k(v, w) 41 | + \epsilon. 42 | 43 | 最终因果效应能够被估计。举个例子,给定 :math:`v` ,CATE被估计为 44 | 45 | .. math:: 46 | 47 | y(\hat{x_t}, v, w) - y(\hat{x_0}, v, w) = \sum_{m, \mu} B_{m, \mu} (\psi_m(\hat{x_t}) - \psi_m(\hat{x_0})) \tilde{v}_{\mu}. 48 | 49 | YLearn在类 :class:`NP2SLS` 中实现了这个过程。 50 | 51 | 类结构 52 | ================ 53 | 54 | .. py:class:: ylearn.estimator_model.iv.NP2SLS(x_model=None, y_model=None, random_state=2022, is_discrete_treatment=False, is_discrete_outcome=False, categories='auto') 55 | 56 | :param estimator, optional, default=None x_model: 为了建模治疗的机器学习模型。任何合理的x_model应该实现 `fit` 和 `predict` 方法,默认是None。 57 | :param estimator, optional, default=None y_model: 为了建模结果的机器学习模型。任何合理的y_model应该实现 `fit` 和 `predict` 方法,默认是None。 58 | :param int, default=2022 random_state: 59 | :param bool, default=False is_discrete_treatment: 60 | :param bool, default=False is_discrete_outcome: 61 | :param str, optional, default='auto' categories: 62 | 63 | .. py:method:: fit(data, outcome, treatment, instrument, is_discrete_instrument=False, treatment_basis=('Poly', 2), instrument_basis=('Poly', 2), covar_basis=('Poly', 2), adjustment=None, covariate=None, **kwargs) 64 | 65 | 拟合NP2SLS。注意当treatment_basis和instrument_basis都有degree 1的时候,我们实际在做2SLS。 66 | 67 | :param DataFrame data: 模型的训练数据集。Training data for the model. 68 | :param str or list of str, optional outcome: 结果的名字。 69 | :param str or list of str, optional treatment: 治疗的名字。 70 | :param str or list of str, optional, default=None covariate: 协变量向量的名字。 71 | :param str or list of str, optional instrument: 工具变量的名字。 72 | :param str or list of str, optional, default=None adjustment: 调整变量的名字。Names of the adjustment variables. 73 | :param tuple of 2 elements, optional, default=('Poly', 2) treatment_basis: 转换原来的治疗向量的选项。第一个元素表示转换的基函数,第二个表示degree。现在第一个元素只支持'Poly'。 74 | :param tuple of 2 elements, optional, default=('Poly', 2) instrument_basis: 转换原来的工具向量的选项。第一个元素表示转换的基函数,第二个表示degree。现在第一个元素只支持'Poly'。 75 | :param tuple of 2 elements, optional, default=('Poly', 2) covar_basis: 转换原来的协变量向量的选项。第一个元素表示转换的基函数,第二个表示degree。现在第一个元素只支持'Poly'。 76 | :param bool, default=False is_discrete_instrument: 77 | 78 | .. py:method:: estimate(data=None, treat=None, control=None, quantity=None) 79 | 80 | 估计数据中治疗对结果的因果效应。 81 | 82 | :param pandas.DataFrame, optional, default=None data: 如果为None,数据将会被设置为训练数据。 83 | :param str, optional, default=None quantity: 返回的估计结果的选项。量的可能值包括: 84 | 85 | 1. *'CATE'* : 估计器将会估计CATE; 86 | 87 | 2. *'ATE'* : 估计器将会估计ATE; 88 | 89 | 3. *None* : 估计器将会估计ITE或CITE。 90 | :param float, optional, default=None treat: 施加干涉时治疗的值。如果是None,那么treat将会被设置为1。 91 | :param float, optional, default=None control: 治疗的值,这样治疗的效果是 :math:`y(do(x=treat)) - y (do(x = control))` 。 92 | 93 | :returns: 用量的类型估计的因果效应。 94 | :rtype: ndarray or float, optional 95 | 96 | .. py:method:: effect_nji(data=None) 97 | 98 | 用不同的治疗值计算因果效应。 99 | 100 | :param pandas.DataFrame, optional, default=None data: 给估计器估计因果效应的测试数据,注意如果data是None,估计器将会使用训练数据。 101 | 102 | :returns: 用不同的治疗值的因果效应。 103 | :rtype: ndarray 104 | 105 | .. topic:: 例子 106 | 107 | pass 108 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/est_model/iv3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/est_model/iv3.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/est_model/iv4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/est_model/iv4.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/flow.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/inter_model/ce_interpreter.rst: -------------------------------------------------------------------------------- 1 | .. _ce_int: 2 | 3 | ************* 4 | CEInterpreter 5 | ************* 6 | 7 | 对于由一个估计模型估计的CATE :math:`\tau(v)` ,比如, 双机器学习模型, :class:`CEInterpreter` 解释 8 | 由构建的一个用于建模 :math:`\tau(v)` 和谐变量 :math:`v` 之间关系的决策树得到的结果。然后能够使用拟合树模型的决策规则 9 | 来分析 :math:`\tau(v)`。 10 | 11 | 类结构 12 | ================ 13 | 14 | .. py:class:: ylearn.effect_interpreter.ce_interpreter.CEInterpreter(*, criterion='squared_error', splitter='best', max_depth=None, min_samples_split=2, min_samples_leaf=1, random_state=2022, max_leaf_nodes=None, max_features=None, min_impurity_decrease=0.0, min_weight_fraction_leaf=0.0, ccp_alpha=0.0, categories='auto') 15 | 16 | :param {"squared_error", "friedman_mse", "absolute_error", "poisson"}, default="squared_error" criterion: 用于测量划分的质量的函数。 17 | 支持的准则为"squared_error"用于均方误差,其等价于把方差减少作为特征选择的准则并且使用每一个终端节点的平均值最小化L2误差。"friedman_mse"为 18 | 潜在的划分用均方误差和Friedman的改进分数。"absolute_error"用于平均绝对误差,其通过使用每一个终端节点的中位数最小化L1误差。"poisson"使用减少泊松偏差来寻找划分。 19 | :param {"best", "random"}, default="best" splitter: 用于在每个节点选择划分的策略。支持的策略"best"用于选择最佳划分,"random"用于选择最佳随机划分。 20 | :param int, default=None max_depth: 树的最大深度。如果为None,那么节点可以一直扩展直到所有的叶子都是纯的或者所有的叶子都包含小于min_samples_split个样本。 21 | :param int or float, default=2 min_samples_split: 划分内部节点所需要的最小的样本数量: 22 | - 如果是int,那么考虑 `min_samples_split` 为最小数量。 23 | - 如果是float, 那么 `min_samples_split` 是一个分数并且 `ceil(min_samples_split * n_samples)` 是对于每一个划分最小的样本数量。 24 | :param int or float, default=1 min_samples_leaf: 在一个叶子节点需要的最小的样本数量。 25 | 一个在任意深度的划分点仅当它留下至少 ``min_samples_leaf`` 训练样本在它的左右分支时会被考虑。这可能有平滑模型的作用,尤其是在回归中。 26 | 27 | - 如果是int, 那么考虑 `min_samples_leaf` 为最小数量。 28 | - 如果是float, 那么 `min_samples_leaf` 是一个分数并且 `ceil(min_samples_leaf * n_samples)` 是对于每一个节点最小的样本数量。 29 | 30 | :param float, default=0.0 min_weight_fraction_leaf: 在一个叶子节点需要的所有权重总和(所有的输入样本)的最小加权分数。如果sample_weight没有被提供时,样本具有同样的权重。 31 | :param int, float or {"sqrt", "log2"}, default=None max_features: 寻找最佳划分时需要考虑的特征数量: 32 | 33 | - 如果是int,那么考虑 `max_features` 个特征在每个划分。 34 | - 如果是float,那么 `max_features` 是一个分数并且 `int(max_features * n_features)` 个特征在每个划分被考虑。 35 | - 如果是"sqrt",那么 `max_features=sqrt(n_features)` 。 36 | - 如果是"log2",那么 `max_features=log2(n_features)` 。 37 | - 如果是None,那么 `max_features=n_features` 。 38 | 39 | :param int random_state: 控制估计器的随机性。 40 | :param int, default to None max_leaf_nodes: 以最佳优先的方式使用 ``max_leaf_nodes`` 生成一棵树。 41 | 最佳节点被定义为杂质相对减少。 42 | 如果是None,那么叶子节点的数量没有限制。 43 | :param float, default=0.0 min_impurity_decrease: 一个节点将会被划分如果这个划分引起杂质的减少大于或者等于这个值。 44 | 加权的杂质减少方程如下 45 | 46 | N_t / N * (impurity - N_t_R / N_t * right_impurity - N_t_L / N_t * left_impurity) 47 | 48 | 其中 ``N`` 是样本的总数, ``N_t`` 是当前节点的样本数量, ``N_t_L`` 是左孩子节点的样本数量,并且 ``N_t_R`` 是右孩子节点的样本数量。 49 | ``N``, ``N_t``, ``N_t_R`` 以及 ``N_t_L`` 全都是指加权和,如果 ``sample_weight`` 被传入。 50 | 51 | .. py:method:: fit(data, est_model, **kwargs) 52 | 53 | 拟合CEInterpreter模型来解释基于数据和est_model估计的因果效应。 54 | 55 | :param pandas.DataFrame data: 输入样本,用于est_model估计因果效应和用于CEInterpreter拟合。 56 | :param estimator_model est_model: est_model应该为ylearn的任何合理的估计器模型且已经拟合过了并且能够估计CATE。 57 | 58 | :returns: Fitted CEInterpreter 59 | :rtype: CEInterpreter的实例 60 | 61 | .. py:method:: interpret(*, v=None, data=None) 62 | 63 | 在测试数据中解释拟合的模型。 64 | 65 | :param numpy.ndarray, optional, default=None v: ndarray形式的测试协变量。如果这被给出,那么数据将会被忽略并且模型会使用这个作为测试数据。 66 | :param pandas.DataFrame, optional, default=None data: DataFrame形式的测试数据。模型将仅使用这个如果v被设置为None。在这种情况下,如果数据也是None,那么训练的数据将会被使用。 67 | 68 | :returns: 对所有的样例解释的结果。 69 | :rtype: dict 70 | 71 | .. py:method:: plot(*, feature_names=None, max_depth=None, class_names=None, label='all', filled=False, node_ids=False, proportion=False, rounded=False, precision=3, ax=None, fontsize=None) 72 | 73 | 绘制拟合的树模型。 74 | 显示的样本计数由任何的可能存在的sample_weights加权。 75 | 可视化自动适应轴的大小。 76 | 使用 ``plt.figure`` 的 ``figsize`` 或者 ``dpi`` 参数来控制生成的大小。 77 | 78 | :returns: List containing the artists for the annotation boxes making up the 79 | tree. 80 | :rtype: annotations : list of artists 81 | 82 | .. topic:: 例子 83 | 84 | pass -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/interpreter.rst: -------------------------------------------------------------------------------- 1 | ****************************************** 2 | 解释器:解释因果效应 3 | ****************************************** 4 | 5 | 为了解释由不同的估计模型估计的因果效应,当前版本的YLearn为了因果效应的可解释性实现了树模型 :class:`CEInterpreter` 并且为了策略估计的可解释性 6 | 实现了 :class:`PolicyInterpreter` 。 7 | 8 | 9 | .. toctree:: 10 | 11 | inter_model/ce_interpreter 12 | inter_model/policy_interpreter -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/intro.rst: -------------------------------------------------------------------------------- 1 | *********************** 2 | YLearn与因果推断概述 3 | *********************** 4 | 5 | 机器学习近年来取得了巨大的成就。机器学习成功的领域主要在预测方面,比如:分类猫和狗的图片。然而,机器学习不能够回答在许多场景中自然地产生的问题。 6 | 一个例子是在策略评估中的 **反事实问题**:如果策略发生改变,会发生什么?事实上,由于这些反事实不能够被观测到。机器学习模型,预测工具不能够被使用。 7 | 机器学习的这些缺点是导致如今因果推断应用的部分原因。 8 | 9 | 因果推断直接的建模了干涉的结果,并将反事实推理形式化。在机器学习的帮助下,如今,因果推断能够从观察到的数据中以不同的方式得到因果的结论,而不是依靠精心设计的实验。 10 | 11 | 一个常见的完整的因果推断过程是由三部分组成。首先,它使用叫做因果发现的技术学习因果关系。这些关系将在之后以结构化因果模型或者有向无环图(DAG)的形式表示。 12 | 其次,它将根据观测到的数据来表示因果估计量。该因果估计量由感兴趣的因果问题,比如平均治疗效果阐明。这个过程被称为识别。最终,一旦因果估计量被识别了,因果推断将会专注 13 | 于从观测到的数据中估计因果估计量。接着,策略评估问题和反事实问题都能够被回答。 14 | 15 | YLearn, 配备了许多最近的文献中发展的技术,在机器学习的帮助下,用于支持从因果发现到因故估计量的估计的整个因果推断流程。这尤其在有大量的观测数据时,更有发展前景。 16 | 17 | YLearn中的概念和相关的问题设置 18 | ===================================================== 19 | 在YLearn中,关于因果推断流程有5个主要组成部分。 20 | 21 | .. figure:: structure_ylearn.png 22 | 23 | *Components in YLearn* 24 | 25 | 1. *因果发现*. 在观测数据中发现因果关系。 26 | 27 | 2. *因果模型*. 以 ``CausalGraph`` 的形式表示因果关系并做其他的相关操作比如用 ``CausalModel`` 识别。 28 | 29 | 3. *估计模型*. 使用不同的技术对因果估计量进行估计。 30 | 31 | 4. *策略模型*. 对每一个个体选择最好的策略。 32 | 33 | 5. *解释器*. 解释因果效应和策略。 34 | 35 | 这些组成部分被连接在一起形成一个完整的因果推断流程,被封装在一个API `Why` 中。 36 | 37 | .. figure:: flow.png 38 | 39 | *YLearn中因果推断的流程*. 从训练数据中, 首先使用 `CausalDiscovery` 来得到数据中的因果结构,通常输出一个 `CausalGraph` 。 40 | 因果图被传入 `CausalModel` , 其中,感兴趣的因果效应被识别并转换成统计估计量。一个 `EstimatorModel` 接着通过训练数据进行训练来建模 41 | 因果效应和其他变量之间的关系,等价于估计训练数据中的因果效应。之后,可以使用一个训练过的 `EstimatorModel` 在一些新的测试数据集上预测因果效应并估计 42 | 分配给每个个体的策略或者解释估计的因果效应。 43 | 44 | 所有的APIs将在 :ref:`api` 中介绍。 45 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/quick_start.rst: -------------------------------------------------------------------------------- 1 | *********** 2 | 快速开始 3 | *********** 4 | 5 | 在这部分中,我们首先展示几个简单的YLearn的用法示例。这些例子包含了大部分常见的功能。之后我们以一个使用 :class:`Why` 的例子来学习揭露数据中隐藏的因果关系。 6 | 7 | 示例用法 8 | ============== 9 | 10 | 我们在这部分中展示几个必要的YLearn的用法示例。细节请参考它们的具体的文档。 11 | 12 | 1. 因果图的表示 13 | 14 | 对一个给定的因果图 :math:`X \leftarrow W \rightarrow Y`, 该因果图由 :class:`CausalGraph` 表示 15 | 16 | .. code-block:: python 17 | 18 | causation = {'X': ['W'], 'W':[], 'Y':['W']} 19 | cg = CausalGraph(causation=causation) 20 | 21 | :py:attr:`cg` 将成为YLearn中的因果图的表示. 22 | 23 | 2. 识别因果效应 24 | 25 | 假如我们对识别因果估计量感兴趣 :math:`P(Y|do(X=x))` 在因果图 `cg` 中, 接着我们应该定义一个实例 :class:`CausalModel` 的实例并使用 :py:func:`identify()` 方法: 26 | 27 | .. code-block:: python 28 | 29 | cm = CausalModel(causal_graph=cg) 30 | cm.identify(treatment={'X'}, outcome={'Y'}, identify_method=('backdoor', 'simple')) 31 | 32 | 3. 估计因果效应 33 | 34 | 通过 :class:`EstimatorModel` 估计因果效应由4步组成: 35 | 36 | * 以 :class:`pandas.DataFrame` 的形式给出数据, 找到 `treatment, outcome, adjustment, covariate` 的名称。 37 | * 使用 :class:`EstimatorModel` 的 :py:func:`fit()` 方法来训练模型。 38 | * 使用 :class:`EstimatorModel` 的 :py:func:`estimate()` 方法来估计测试数据中的因果效应。 39 | 40 | 41 | 案例分析 42 | ========== -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/ref.rst: -------------------------------------------------------------------------------- 1 | 参考文献 2 | ========== 3 | 4 | .. [Pearl] 5 | J. Pearl. Causility : models, reasoning, and inference. 6 | 7 | .. [Shpitser2006] 8 | S. Shpitser and J. Pearl. Identification of Joint Interventional Distributions in Recursive Semi-Markovian Causal Models. 9 | 10 | .. [Neal2020] 11 | B. Neal. Introduction to Causal Inference. 12 | 13 | .. [Funk2010] 14 | M. Funk, et al. Doubly Robust Estimation of Causal Effects. 15 | 16 | .. [Chern2016] 17 | 18 | V. Chernozhukov, et al. Double Machine Learning for Treatment and Causal Parameters. *arXiv:1608.00060.* 19 | 20 | .. [Athey2015] 21 | 22 | S. Athey and G. Imbens. Recursive Partitioning for Heterogeneous Causal Effects. *arXiv: 1504.01132.* 23 | 24 | .. [Schuler] 25 | 26 | A. Schuler, et al. A comparison of methods for model selection when estimating individual treatment effects. *arXiv:1804.05146.* 27 | 28 | .. [Nie] 29 | 30 | X. Nie, et al. Quasi-Oracle estimation of heterogeneous treatment effects. 31 | *arXiv: 1712.04912.* 32 | 33 | .. [Hartford] 34 | 35 | J. Hartford, et al. Deep IV: A Flexible Approach for Counterfactual Prediction. *ICML 2017.* 36 | 37 | .. [Newey2002] 38 | 39 | W. Newey and J. Powell. Instrumental Variable Estimation of Nonparametric Models. *Econometrica 71, no. 5 (2003): 1565–78.* 40 | 41 | .. [Kunzel2019] 42 | 43 | S. Kunzel2019, et al. Meta-Learners for Estimating Heterogeneous Treatment Effects using Machine Learning. 44 | 45 | .. [Angrist1996] 46 | 47 | J. Angrist, et al. Identification of causal effects using instrumental variables. *Journal of the American Statistical Association*. 48 | 49 | .. [Athey2020] 50 | 51 | S. Athey and S. Wager. Policy Learning with Observational Data. *arXiv: 1702.02896.* 52 | 53 | .. [Spirtes2001] 54 | 55 | P. Spirtes, et al. Causation, Prediction, and Search. 56 | 57 | .. [Zheng2018] 58 | 59 | X. Zheng, et al. DAGs with NO TEARS: Continuous Optimization for Structure Learning. *arXiv: 1803.01422.* 60 | -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/structure_ylearn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/docs/zh_CN/source/sub/structure_ylearn.png -------------------------------------------------------------------------------- /docs/zh_CN/source/sub/sub.rst: -------------------------------------------------------------------------------- 1 | 用户指南 2 | ========== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | intro 8 | quick_start 9 | api -------------------------------------------------------------------------------- /example_usages/.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-documentation 2 | -------------------------------------------------------------------------------- /example_usages/data/BankChurners.csv.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/example_usages/data/BankChurners.csv.zip -------------------------------------------------------------------------------- /example_usages/data/marketing_promotion.csv.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/example_usages/data/marketing_promotion.csv.zip -------------------------------------------------------------------------------- /example_usages/introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### In this notebook, we need to show the case for applying both identification and estimation." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 4, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import sys\n", 17 | "import os\n", 18 | "sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath('./introduction.ipynb'))))" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 7, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from ylearn.estimator_model.base_models import BaseEstModel" 28 | ] 29 | } 30 | ], 31 | "metadata": { 32 | "interpreter": { 33 | "hash": "e0ecfad75f99b8578830c76494b15e8b8f0ed30d484e3d7b0b2aac43eb800e14" 34 | }, 35 | "kernelspec": { 36 | "display_name": "Python 3.8.12 ('causal')", 37 | "language": "python", 38 | "name": "python3" 39 | }, 40 | "language_info": { 41 | "codemirror_mode": { 42 | "name": "ipython", 43 | "version": 3 44 | }, 45 | "file_extension": ".py", 46 | "mimetype": "text/x-python", 47 | "name": "python", 48 | "nbconvert_exporter": "python", 49 | "pygments_lexer": "ipython3", 50 | "version": "3.8.12" 51 | }, 52 | "orig_nbformat": 4 53 | }, 54 | "nbformat": 4, 55 | "nbformat_minor": 2 56 | } 57 | -------------------------------------------------------------------------------- /fig/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/.DS_Store -------------------------------------------------------------------------------- /fig/QRcode_press.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/QRcode_press.jpg -------------------------------------------------------------------------------- /fig/QRcode_survey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/QRcode_survey.jpg -------------------------------------------------------------------------------- /fig/YLearn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/YLearn1.png -------------------------------------------------------------------------------- /fig/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/flow.png -------------------------------------------------------------------------------- /fig/flow_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/flow_chart.png -------------------------------------------------------------------------------- /fig/flow_chart_cn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/flow_chart_cn.png -------------------------------------------------------------------------------- /fig/graph_expun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/graph_expun.png -------------------------------------------------------------------------------- /fig/graph_un_arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/graph_un_arc.png -------------------------------------------------------------------------------- /fig/id_fig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/id_fig.png -------------------------------------------------------------------------------- /fig/iv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/iv2.png -------------------------------------------------------------------------------- /fig/latex_exp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/latex_exp.png -------------------------------------------------------------------------------- /fig/structure_ylearn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/structure_ylearn.png -------------------------------------------------------------------------------- /fig/wechat_QRcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataCanvasIO/YLearn/HEAD/fig/wechat_QRcode.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "oldest-supported-numpy", "cython", "urllib3"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements-bayesian.txt: -------------------------------------------------------------------------------- 1 | torch 2 | pyro-ppl 3 | -------------------------------------------------------------------------------- /requirements-extra.txt: -------------------------------------------------------------------------------- 1 | gcastle 2 | pgmpy 3 | -------------------------------------------------------------------------------- /requirements-notebook.txt: -------------------------------------------------------------------------------- 1 | jupyterlab 2 | ipywidgets 3 | jupyterlab_widgets 4 | matplotlib 5 | tqdm 6 | shap 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.16.5 2 | pandas>=0.25.3 3 | scikit-learn>=0.22.1,<1.7.0 4 | scipy 5 | networkx 6 | ipython 7 | torch 8 | joblib 9 | pydot 10 | tqdm 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/_common.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | 5 | try: 6 | from ylearn.estimator_model.causal_tree import CausalTree 7 | 8 | is_causal_tree_ready = True 9 | except ImportError: 10 | is_causal_tree_ready = False 11 | 12 | try: 13 | from ylearn.policy.policy_model import PolicyTree 14 | 15 | is_policy_tree_ready = True 16 | except ImportError: 17 | is_policy_tree_ready = False 18 | 19 | try: 20 | import torch 21 | 22 | is_torch_installed = True 23 | 24 | except ImportError: 25 | is_torch_installed = False 26 | 27 | if_causal_tree_ready = pytest.mark.skipif(not is_causal_tree_ready, reason='CausalTree is not ready') 28 | if_policy_tree_ready = pytest.mark.skipif(not is_policy_tree_ready, reason='PolicyTree is not ready') 29 | if_torch_ready = pytest.mark.skipif(not is_torch_installed, reason='not found torch') 30 | 31 | 32 | def validate_leaner(data_generator, leaner, 33 | fit_kwargs=None, estimate_kwargs=None, 34 | check_fitted=True, check_effect=True, 35 | check_effect_nji=False, 36 | ): 37 | # generate data 38 | data, test_data, outcome, treatment, adjustment, covariate = data_generator() 39 | 40 | # fit 41 | kwargs = {} 42 | if adjustment: 43 | kwargs['adjustment'] = adjustment 44 | if covariate: 45 | kwargs['covariate'] = covariate 46 | 47 | if fit_kwargs: 48 | kwargs.update(fit_kwargs) 49 | leaner.fit(data, outcome, treatment, **kwargs) 50 | if check_fitted: 51 | assert hasattr(leaner, '_is_fitted') and getattr(leaner, '_is_fitted') 52 | # 53 | # # estimate 54 | # kwargs = dict(quantity='ATE') 55 | # if estimate_kwargs: 56 | # kwargs.update(estimate_kwargs) 57 | # ate = leaner.estimate(**kwargs) 58 | # assert ate is not None 59 | # if check_effect: 60 | # assert isinstance(ate, (float, np.ndarray)) 61 | # if isinstance(ate, np.ndarray): 62 | # assert ate.dtype.kind == 'f' 63 | # assert len(ate.ravel()) == len(outcome) 64 | 65 | # estimate 66 | kwargs = dict(data=test_data, quantity=None) 67 | if estimate_kwargs: 68 | kwargs.update(estimate_kwargs) 69 | effect = leaner.estimate(**kwargs) 70 | assert effect is not None 71 | if check_effect: 72 | assert isinstance(effect, (np.ndarray, pd.Series)) 73 | assert effect.min() != effect.max() 74 | 75 | if check_effect_nji: 76 | effect_nji = leaner.effect_nji(test_data) 77 | assert isinstance(effect_nji, np.ndarray) 78 | assert effect_nji.shape[0] == len(test_data) 79 | assert effect_nji.shape[1] == len(outcome) 80 | 81 | return leaner, effect 82 | -------------------------------------------------------------------------------- /tests/approx_bound_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | from sklearn.ensemble import RandomForestClassifier 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from ylearn.estimator_model.approximation_bound import ApproxBound 8 | from . import _dgp 9 | from ._common import validate_leaner 10 | 11 | _test_settings = { 12 | # data_generator: (y_model,x_model,x_prob) 13 | _dgp.generate_data_x1b_y1_w0v5: (LinearRegression(), RandomForestClassifier(), None), 14 | # _dgp.generate_data_x2b_y1_w0v5: (LinearRegression(), RandomForestClassifier(), None), 15 | _dgp.generate_data_x1m_y1_w0v5: (LinearRegression(), RandomForestClassifier(), None), 16 | } 17 | 18 | 19 | @pytest.mark.parametrize('dg', _test_settings.keys()) 20 | def test_approx_bound(dg): 21 | y_model, x_model, x_proba = _test_settings[dg] 22 | dr = ApproxBound(x_model=x_model, y_model=y_model, x_prob=x_proba, random_state=2022) 23 | est, effect = validate_leaner(dg, dr, check_effect=False) 24 | 25 | # ApproxBound estimate effect: tuple(lower_bound, upper_bound) 26 | assert isinstance(effect, tuple) and len(effect) == 2 27 | assert isinstance(effect[0], (pd.Series, np.ndarray)) and effect[0].min() < effect[0].max() 28 | assert isinstance(effect[1], (pd.Series, np.ndarray)) and effect[1].min() < effect[1].max() 29 | -------------------------------------------------------------------------------- /tests/causal_tree_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from . import _dgp 4 | from ._common import validate_leaner, if_causal_tree_ready 5 | 6 | _test_settings = { 7 | # data_generator: options 8 | _dgp.generate_data_x1b_y1_w0v5: dict(min_samples_leaf=3, max_depth=5), 9 | _dgp.generate_data_x2b_y1_w0v5: dict(min_samples_leaf=3, min_samples_split=3, max_depth=5), 10 | _dgp.generate_data_x1m_y1_w0v5: dict(min_samples_leaf=3, max_depth=5), 11 | } 12 | 13 | 14 | @if_causal_tree_ready 15 | @pytest.mark.parametrize('dg', _test_settings.keys()) 16 | def test_causal_tree(dg): 17 | from ylearn.estimator_model.causal_tree import CausalTree 18 | 19 | options = _test_settings[dg] 20 | dr = CausalTree(**options) 21 | validate_leaner(dg, dr, check_effect=True) 22 | -------------------------------------------------------------------------------- /tests/deep_iv_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | 5 | from . import _dgp 6 | from ._common import if_torch_ready 7 | from .iv_test import validate_it 8 | 9 | try: 10 | import torch 11 | import torch.nn.functional as F 12 | from ylearn.estimator_model.deepiv import DeepIV 13 | except ImportError: 14 | pass 15 | 16 | _test_settings = { 17 | # data_generator: any 18 | _dgp.generate_data_x1b_y1_w5v0: 'tbd', 19 | _dgp.generate_data_x2b_y1_w5v0: 'tbd', 20 | _dgp.generate_data_x1m_y1_w5v0: 'tbd', 21 | } 22 | 23 | 24 | @if_torch_ready 25 | @pytest.mark.parametrize('dg', _test_settings.keys()) 26 | # @pytest.mark.xfail(reason='to be fixed: expected scalar type Double but found Float') 27 | def test_iv_with_params(dg): 28 | torch.set_default_dtype(torch.float64) 29 | 30 | # y_model, x_model = _test_settings[dg] 31 | dr = DeepIV(num_gaussian=10) 32 | validate_it(dg, dr, 33 | float64=True, 34 | fit_kwargs=dict( 35 | sample_n=2, 36 | lr=0.5, 37 | epoch=1, 38 | device='cpu', 39 | batch_size=1000 40 | ), ) 41 | 42 | 43 | @if_torch_ready 44 | # @pytest.mark.xfail(reason='to be fixed') 45 | def test_deep_iv_basis(): 46 | torch.set_default_dtype(torch.float64) 47 | dtype = torch.float64 48 | itype = torch.int64 49 | 50 | n = 5000 51 | 52 | # Initialize exogenous variables; normal errors, uniformly distributed covariates and instruments 53 | e = np.random.normal(size=(n, 1)) 54 | w = np.random.uniform(low=0.0, high=10.0, size=(n, 1)) 55 | z = np.random.uniform(low=0.0, high=10.0, size=(n, 1)) 56 | 57 | e, w, z = torch.tensor(e, dtype=dtype), torch.tensor(w, dtype=dtype), torch.tensor(z, dtype=dtype) 58 | weight_w = torch.randn(1, dtype=dtype) 59 | weight_z = torch.randn(1, dtype=dtype) 60 | 61 | def to_treatment(w, z, e): 62 | x = torch.sqrt(w) * weight_w + torch.sqrt(z) * weight_z + e 63 | x = (torch.sign(x) + 1) / 2 64 | return F.one_hot(x.reshape(-1).to(int)) 65 | 66 | # Outcome equation 67 | weight_x = torch.randn(2, 1, dtype=dtype) 68 | weight_wx = torch.randn(2, 1, dtype=dtype) 69 | 70 | def to_outcome(w, e, treatment_): 71 | wx = torch.mm(treatment_.to(dtype), weight_x) 72 | wx1 = (w * treatment_.to(dtype)).matmul(weight_wx) 73 | # wx1 = w 74 | return (wx ** 2) * 10 - wx1 + e / 2 75 | 76 | treatment = to_treatment(w, z, e) 77 | y = to_outcome(w, e, treatment) 78 | 79 | data_dict = { 80 | 'z': z.squeeze().to(dtype), 81 | 'w': w.squeeze().to(dtype), 82 | 'x': torch.argmax(treatment, dim=1).to(itype), 83 | 'y': y.squeeze().to(dtype) 84 | } 85 | data = pd.DataFrame(data_dict) 86 | 87 | # iv = DeepIV(is_discrete_treatment=True) 88 | # iv.fit( 89 | # data=data, 90 | # outcome='y', 91 | # treatment='x', 92 | # instrument='z', 93 | # adjustment='w', 94 | # device='cpu', 95 | # batch_size=2500, 96 | # lr=0.5, 97 | # epoch=1, 98 | # ) 99 | iv = DeepIV(num_gaussian=10) 100 | iv.fit( 101 | data=data, 102 | outcome='y', 103 | treatment='x', 104 | instrument='z', 105 | adjustment='w', 106 | sample_n=2, 107 | lr=0.5, 108 | epoch=1, 109 | device='cpu', 110 | batch_size=5000 111 | ) 112 | 113 | p = iv.estimate() 114 | print(p) 115 | -------------------------------------------------------------------------------- /tests/discovery_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | from ylearn.causal_discovery import CausalDiscovery 5 | from ylearn.exp_dataset.gen import gen 6 | from ._common import if_torch_ready 7 | 8 | 9 | @if_torch_ready 10 | def test_ndarray(): 11 | X1 = gen() 12 | # X1 = pd.DataFrame(X1, columns=[f'x{i}' for i in range(X1.shape[1])]) 13 | cd = CausalDiscovery(hidden_layer_dim=[3], device='cpu') 14 | est = cd(X1, threshold=0.01) 15 | print(est) 16 | assert isinstance(est, np.ndarray) 17 | assert est.shape[0] == est.shape[1] 18 | 19 | m = cd.matrix2dict(est) 20 | assert isinstance(m, dict) 21 | 22 | m = cd.matrix2df(est) 23 | assert isinstance(m, pd.DataFrame) 24 | 25 | 26 | @if_torch_ready 27 | def test_dataframe(): 28 | X1 = gen() 29 | X1 = pd.DataFrame(X1, columns=[f'x{i}' for i in range(X1.shape[1])]) 30 | cd = CausalDiscovery(hidden_layer_dim=[3], device='cpu') 31 | est = cd(X1, threshold=0.01) 32 | print(est) 33 | assert isinstance(est, pd.DataFrame) 34 | assert est.columns.to_list() == X1.columns.to_list() 35 | assert est.shape[0] == est.shape[1] 36 | 37 | 38 | @if_torch_ready 39 | def test_dataframe_disable_scale(): 40 | X1 = gen() 41 | X1 = pd.DataFrame(X1, columns=[f'x{i}' for i in range(X1.shape[1])]) 42 | cd = CausalDiscovery(hidden_layer_dim=[3], scale=False, device='cpu') 43 | est = cd(X1, threshold=0.01) 44 | print(est) 45 | assert isinstance(est, pd.DataFrame) 46 | assert est.columns.to_list() == X1.columns.to_list() 47 | assert est.shape[0] == est.shape[1] 48 | 49 | 50 | @if_torch_ready 51 | def test_dataframe_with_maxabs_sacler(): 52 | from sklearn.preprocessing import MaxAbsScaler 53 | X1 = gen() 54 | X1 = pd.DataFrame(X1, columns=[f'x{i}' for i in range(X1.shape[1])]) 55 | cd = CausalDiscovery(hidden_layer_dim=[3], scale=MaxAbsScaler(), device='cpu') 56 | est = cd(X1, threshold=0.01) 57 | print(est) 58 | assert isinstance(est, pd.DataFrame) 59 | assert est.columns.to_list() == X1.columns.to_list() 60 | assert est.shape[0] == est.shape[1] 61 | 62 | 63 | @if_torch_ready 64 | def test_return_dict(): 65 | X1 = gen() 66 | cd = CausalDiscovery(hidden_layer_dim=[3], device='cpu') 67 | est = cd(X1, threshold=0.01, return_dict=True) 68 | print(est) 69 | assert isinstance(est, dict) 70 | -------------------------------------------------------------------------------- /tests/doubly_robust_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sklearn.ensemble import GradientBoostingRegressor, RandomForestClassifier 3 | from sklearn.linear_model import LinearRegression 4 | 5 | from ylearn.estimator_model.doubly_robust import DoublyRobust 6 | from . import _dgp 7 | from ._common import validate_leaner 8 | 9 | _test_settings = { 10 | # data_generator: (x_model,y_model,yx_model) 11 | _dgp.generate_data_x1b_y1: (RandomForestClassifier(n_estimators=100, max_depth=100), 12 | GradientBoostingRegressor(n_estimators=100, max_depth=100), 13 | GradientBoostingRegressor(n_estimators=100, max_depth=100), 14 | ), 15 | _dgp.generate_data_x1b_y2: (RandomForestClassifier(n_estimators=100, max_depth=100), 16 | LinearRegression(), 17 | LinearRegression(), 18 | ), 19 | _dgp.generate_data_x1m_y1: (RandomForestClassifier(n_estimators=100, max_depth=100), 20 | GradientBoostingRegressor(n_estimators=100, max_depth=100), 21 | GradientBoostingRegressor(n_estimators=100, max_depth=100), 22 | ), 23 | } 24 | 25 | _test_settings_x2b = { 26 | # data_generator: (x_model,y_model,yx_model) 27 | _dgp.generate_data_x2b_y1: (RandomForestClassifier(n_estimators=100, max_depth=100), 28 | GradientBoostingRegressor(n_estimators=100, max_depth=100), 29 | GradientBoostingRegressor(n_estimators=100, max_depth=100), 30 | ), 31 | _dgp.generate_data_x2b_y2: (RandomForestClassifier(n_estimators=100, max_depth=100), 32 | LinearRegression(), 33 | LinearRegression(), 34 | ), 35 | } 36 | 37 | 38 | @pytest.mark.parametrize('dg', _test_settings.keys()) 39 | def test_doubly_robust(dg): 40 | x_model, y_model, yx_model = _test_settings[dg] 41 | dr = DoublyRobust(x_model=x_model, y_model=y_model, yx_model=yx_model, cf_fold=1, random_state=2022, ) 42 | validate_leaner(dg, dr) 43 | 44 | 45 | @pytest.mark.parametrize('dg', _test_settings.keys()) 46 | def test_doubly_robust_with_treat(dg): 47 | x_model, y_model, yx_model = _test_settings[dg] 48 | dr = DoublyRobust(x_model=x_model, y_model=y_model, yx_model=yx_model, cf_fold=1, random_state=2022, ) 49 | validate_leaner(dg, dr, 50 | fit_kwargs=dict(treat=1, control=0), 51 | ) 52 | 53 | 54 | @pytest.mark.parametrize('dg', _test_settings_x2b.keys()) 55 | def test_doubly_robust_x2b(dg): 56 | x_model, y_model, yx_model = _test_settings_x2b[dg] 57 | dr = DoublyRobust(x_model=x_model, y_model=y_model, yx_model=yx_model, cf_fold=1, random_state=2022, ) 58 | validate_leaner(dg, dr, 59 | fit_kwargs=dict(treat=[1, 1], control=[0, 0]), 60 | ) 61 | -------------------------------------------------------------------------------- /tests/estimator_factory_test.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import pytest 4 | 5 | from ylearn.estimator_model import ESTIMATOR_FACTORIES 6 | from . import _dgp 7 | from ._common import if_policy_tree_ready, is_policy_tree_ready, is_torch_installed 8 | 9 | 10 | @pytest.mark.parametrize('key', ESTIMATOR_FACTORIES.keys()) 11 | def test_list_factory(key): 12 | if key.lower().find('tree') >= 0 and not is_policy_tree_ready: 13 | return 14 | 15 | if (key.lower().find('div') >= 0 or key.lower().find('deep_iv') >= 0) and not is_torch_installed: 16 | return 17 | 18 | factory_cls = ESTIMATOR_FACTORIES[key] 19 | factory = factory_cls() 20 | assert factory is not None 21 | 22 | 23 | @pytest.mark.parametrize('key', ['slearner', 'tlearner', 'xlearner', 'dml', 'dr']) 24 | def test_Xb_Yc(key): 25 | data, test_data, outcome, treatment, adjustment, covariate = _dgp.generate_data_x1b_y1() 26 | 27 | factory = ESTIMATOR_FACTORIES[key]() 28 | est = factory(data, outcome[0], treatment, 'regression', 'binary', 29 | adjustment=adjustment, covariate=covariate, random_state=123) 30 | assert est is not None 31 | 32 | est.fit(data, outcome, treatment, adjustment=adjustment, covariate=covariate, n_jobs=1) 33 | effect = est.estimate(test_data) 34 | assert effect.shape[0] == len(test_data) 35 | 36 | 37 | @if_policy_tree_ready 38 | @pytest.mark.parametrize('key', ['tree', 'grf']) 39 | def test_Xb_Yc_tree(key): 40 | data, test_data, outcome, treatment, adjustment, covariate = _dgp.generate_data_x1b_y1() 41 | 42 | factory = ESTIMATOR_FACTORIES[key]() 43 | est = factory(data, outcome[0], treatment, 'regression', 'binary', 44 | adjustment=adjustment, covariate=covariate, random_state=123) 45 | assert est is not None 46 | 47 | fit_options = {} 48 | sig = inspect.signature(est.fit) 49 | if 'n_jobs' in sig.parameters.keys(): 50 | fit_options['n_jobs'] = 1 51 | 52 | est.fit(data, outcome, treatment, adjustment=adjustment, covariate=covariate, **fit_options) 53 | effect = est.estimate(test_data) 54 | assert effect.shape[0] == len(test_data) 55 | 56 | 57 | @pytest.mark.parametrize('key', ['slearner', 'tlearner', 'xlearner']) 58 | def test_Xb_Yb(key): 59 | data, test_data, outcome, treatment, adjustment, covariate = _dgp.generate_data_x1b_y1() 60 | m = data[outcome].values.mean() 61 | data[outcome] = (data[outcome] > m).astype('int') 62 | test_data[outcome] = (test_data[outcome] > m).astype('int') 63 | 64 | factory = ESTIMATOR_FACTORIES[key]() 65 | est = factory(data, outcome[0], treatment, 'binary', 'binary', 66 | adjustment=adjustment, covariate=covariate, random_state=123) 67 | assert est is not None 68 | 69 | est.fit(data, outcome, treatment, adjustment=adjustment, covariate=covariate, n_jobs=1) 70 | effect = est.estimate(test_data) 71 | assert effect.shape[0] == len(test_data) 72 | 73 | 74 | @if_policy_tree_ready 75 | @pytest.mark.parametrize('key', ['tree', 'grf', ]) 76 | def test_Xb_Yb_tree(key): 77 | data, test_data, outcome, treatment, adjustment, covariate = _dgp.generate_data_x1b_y1() 78 | m = data[outcome].values.mean() 79 | data[outcome] = (data[outcome] > m).astype('int') 80 | test_data[outcome] = (test_data[outcome] > m).astype('int') 81 | 82 | factory = ESTIMATOR_FACTORIES[key]() 83 | est = factory(data, outcome[0], treatment, 'binary', 'binary', 84 | adjustment=adjustment, covariate=covariate, random_state=123) 85 | assert est is not None 86 | 87 | est.fit(data, outcome, treatment, adjustment=adjustment, covariate=covariate) 88 | effect = est.estimate(test_data) 89 | assert effect.shape[0] == len(test_data) 90 | -------------------------------------------------------------------------------- /tests/iv_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | from sklearn.ensemble import RandomForestClassifier 5 | from sklearn.linear_model import LinearRegression 6 | from sklearn.metrics import r2_score 7 | 8 | from ylearn.estimator_model.iv import NP2SLS 9 | from . import _dgp 10 | 11 | 12 | def validate_it(data_generator, estimator, 13 | float32=False, 14 | float64=False, 15 | fit_kwargs=None, estimate_kwargs=None, 16 | check_fitted=True, check_effect=True): 17 | # generate data 18 | data, test_data, outcome, treatment, adjustment, covariate = data_generator() 19 | assert adjustment is not None 20 | 21 | if float32: 22 | cols = data.select_dtypes(include='float64').columns.tolist() 23 | if cols: 24 | data[cols] = data[cols].astype('float32') 25 | test_data[cols] = test_data[cols].astype('float32') 26 | elif float64: 27 | cols = data.select_dtypes(include='float32').columns.tolist() 28 | if cols: 29 | data[cols] = data[cols].astype('float64') 30 | test_data[cols] = test_data[cols].astype('float64') 31 | instrument = adjustment[:3] 32 | adjustment = adjustment[3:] 33 | 34 | # fit 35 | kwargs = dict(adjustment=adjustment, instrument=instrument) 36 | if covariate: 37 | kwargs['covariate'] = covariate 38 | 39 | if fit_kwargs: 40 | kwargs.update(fit_kwargs) 41 | estimator.fit(data, outcome, treatment, **kwargs) 42 | if check_fitted: 43 | assert hasattr(estimator, '_is_fitted') and getattr(estimator, '_is_fitted') 44 | 45 | # estimate with None 46 | kwargs = dict(data=None, quantity=None) 47 | if estimate_kwargs: 48 | kwargs.update(estimate_kwargs) 49 | pred = estimator.estimate(**kwargs) 50 | assert pred is not None 51 | if check_effect: 52 | # assert isinstance(pred, (np.ndarray, pd.Series)) 53 | assert pred.min() != pred.max() 54 | 55 | # estimate with test_data 56 | kwargs = dict(data=test_data, quantity=None) 57 | if estimate_kwargs: 58 | kwargs.update(estimate_kwargs) 59 | pred = estimator.estimate(**kwargs) 60 | assert pred is not None 61 | if check_effect: 62 | # assert isinstance(pred, (np.ndarray, pd.Series)) 63 | assert pred.min() != pred.max() 64 | 65 | # return leaner, pred 66 | 67 | 68 | _test_settings = { 69 | # data_generator: (y_model,x_model,x_prob) 70 | _dgp.generate_data_x1b_y1: (LinearRegression(), RandomForestClassifier()), 71 | _dgp.generate_data_x2b_y1: (LinearRegression(), RandomForestClassifier()), 72 | _dgp.generate_data_x1m_y1: (LinearRegression(), RandomForestClassifier()), 73 | } 74 | 75 | 76 | @pytest.mark.parametrize('dg', _test_settings.keys()) 77 | # @pytest.mark.xfail(reason='to be fixed: effect is tuple') 78 | def test_iv_with_params(dg): 79 | y_model, x_model = _test_settings[dg] 80 | dr = NP2SLS(x_model=x_model, y_model=y_model, 81 | is_discrete_treatment=True, 82 | is_discrete_outcome=False) 83 | validate_it(dg, dr) 84 | 85 | 86 | def test_iv_basis(): 87 | n = 5000 88 | 89 | # Initialize exogenous variables; normal errors, uniformly distributed covariates and instruments 90 | e = np.random.normal(size=(n,)) / 5 91 | x = np.random.uniform(low=0.0, high=10.0, size=(n,)) 92 | z = np.random.uniform(low=0.0, high=10.0, size=(n,)) 93 | 94 | # Initialize treatment variable 95 | # t = np.sqrt((x + 2) * z) + e 96 | t = np.sqrt(2 * z + x * z + x * x + x) + e 97 | # Show the marginal distribution of t 98 | y = t * t / 10 + x * x / 50 + e 99 | 100 | data_dict = { 101 | 'z': z, 102 | 'w': x, 103 | 'x': t, 104 | 'y': y 105 | } 106 | data = pd.DataFrame(data_dict) 107 | 108 | iv = NP2SLS() 109 | iv.fit( 110 | data=data, 111 | outcome='y', 112 | treatment='x', 113 | instrument='z', 114 | covariate='w', 115 | covar_basis=('Poly', 2), 116 | treatment_basis=('Poly', 2), 117 | instrument_basis=('Poly', 1), 118 | ) 119 | for i, x in enumerate([2, 5, 8]): 120 | # y_true = t*t / 10 - x*t/10 121 | y_true = t * t / 10 + x * x / 50 122 | 123 | test_data = pd.DataFrame( 124 | {'x': t, 125 | 'w': np.full_like(t, x), } 126 | ) 127 | y_pred = iv.estimate(data=test_data) 128 | s = r2_score(y_true, y_pred) 129 | print('score:', s, f'x={x}') 130 | -------------------------------------------------------------------------------- /tests/policy_model_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from itertools import product 3 | 4 | import pandas as pd 5 | import numpy as np 6 | np.random.seed(0) 7 | 8 | from sklearn.ensemble import RandomForestRegressor,RandomForestClassifier 9 | from sklearn.preprocessing import PolynomialFeatures 10 | 11 | from ylearn.exp_dataset.exp_data import single_continuous_treatment, single_binary_treatment 12 | from ylearn.estimator_model.double_ml import DoubleML 13 | from ylearn.effect_interpreter.ce_interpreter import CEInterpreter 14 | 15 | import pytest 16 | 17 | need_at_least_py37 = pytest.mark.skipif(sys.version_info[1] <= 6, reason="skip if <=python3.6") 18 | 19 | 20 | @need_at_least_py37 21 | class TestPolicyModel: 22 | 23 | def setup_class(cls): 24 | pass 25 | 26 | def run_interprete(self, data_func, dml): 27 | train, val, treatment_effect = data_func() 28 | adjustment = train.columns[:-4] 29 | covariate = 'c_0' 30 | outcome = 'outcome' 31 | treatment = 'treatment' 32 | 33 | def exp_te(x): return np.exp(2 * x) 34 | 35 | dat = np.array(list(product(np.arange(0, 1, 0.01), repeat=1))).ravel() 36 | 37 | data_test = pd.DataFrame({'c_0': dat}) 38 | true_te = np.array([exp_te(xi) for xi in data_test[covariate]]) 39 | 40 | dml.fit( 41 | train, 42 | outcome, 43 | treatment, 44 | adjustment, 45 | covariate, 46 | ) 47 | cei = CEInterpreter(max_depth=3) 48 | cei.fit(data=data_test, est_model=dml) 49 | 50 | ret_list = cei.decide(data_test) 51 | 52 | assert ret_list is not None 53 | assert ret_list.shape[0] == data_test.shape[0] 54 | return ret_list 55 | 56 | @pytest.mark.parametrize('covariate_transformer', [None, PolynomialFeatures(degree=3, include_bias=False)]) 57 | def test_binary(self, covariate_transformer): 58 | dml = DoubleML( 59 | x_model=RandomForestClassifier(), 60 | y_model=RandomForestRegressor(), 61 | cf_fold=2, 62 | covariate_transformer=covariate_transformer 63 | ) 64 | self.run_interprete(single_binary_treatment, dml) 65 | 66 | @pytest.mark.parametrize('covariate_transformer', [None, PolynomialFeatures(degree=3, include_bias=False)]) 67 | def test_continuous(self, covariate_transformer): 68 | dml = DoubleML( 69 | x_model=RandomForestRegressor(), 70 | y_model=RandomForestRegressor(), 71 | cf_fold=3, 72 | covariate_transformer=covariate_transformer 73 | ) 74 | self.run_interprete(single_continuous_treatment, dml) 75 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | from ylearn.estimator_model.utils import nd_kron 5 | from ylearn.utils import tit, tit_report, tic_toc 6 | 7 | 8 | def _nd_kron_original(x, y): 9 | dim = x.shape[0] 10 | assert dim == y.shape[0] 11 | kron_prod = np.kron(x[0], y[0]).reshape(1, -1) 12 | 13 | if dim > 1: 14 | for i, vec in enumerate(x[1:], 1): 15 | kron_prod = np.concatenate( 16 | (kron_prod, np.kron(vec, y[i]).reshape(1, -1)), axis=0 17 | ) 18 | 19 | return kron_prod 20 | 21 | 22 | def test_kron(): 23 | n = 10 24 | 25 | # test (n,1)x(n,3) 26 | x = np.random.random((n, 1)) 27 | y = np.random.random((n, 3)) 28 | k1 = nd_kron(x, y) 29 | k2 = _nd_kron_original(x, y) 30 | assert k1.shape == k2.shape 31 | assert (k1 == k2).all() 32 | 33 | # test (n,2)x(n,3) 34 | x = np.random.random((n, 2)) 35 | y = np.random.random((n, 3)) 36 | k1 = nd_kron(x, y) 37 | k2 = _nd_kron_original(x, y) 38 | assert k1.shape == k2.shape 39 | assert (k1 == k2).all() 40 | 41 | 42 | def foo(*args, **kwargs): 43 | for i, a in enumerate(args): 44 | print('arg', i, ':', a) 45 | for k, v in kwargs.items(): 46 | print('kwarg', k, ':', v) 47 | 48 | 49 | @tic_toc() 50 | def bar(*args, **kwargs): 51 | foo(*args, **kwargs) 52 | 53 | 54 | def test_tit(): 55 | fn = tit(foo) 56 | fn('a', 1, x='xxx') 57 | fn('b', 2, x='xxx') 58 | 59 | bar('b', 2, x='xxx') 60 | 61 | report = tit_report() 62 | assert isinstance(report, pd.DataFrame) 63 | 64 | fn_name = f'{foo.__module__}.{foo.__qualname__}' 65 | assert fn_name in report.index.tolist() 66 | assert report.loc[fn_name]['count'] == 2 67 | 68 | assert hasattr(bar, 'tic_toc_') 69 | bar_fn = bar.tic_toc_ 70 | fn_name = f'{bar_fn.__module__}.{bar_fn.__qualname__}' 71 | assert fn_name in report.index.tolist() 72 | assert report.loc[fn_name]['count'] == 1 73 | -------------------------------------------------------------------------------- /ylearn/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .api import Why 3 | -------------------------------------------------------------------------------- /ylearn/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.0' 2 | -------------------------------------------------------------------------------- /ylearn/api/__init__.py: -------------------------------------------------------------------------------- 1 | from ._identifier import Identifier, IdentifierWithDiscovery 2 | from ._why import Why 3 | -------------------------------------------------------------------------------- /ylearn/api/_wrapper.py: -------------------------------------------------------------------------------- 1 | from ylearn.effect_interpreter.policy_interpreter import PolicyInterpreter 2 | 3 | 4 | def _transform(fn): 5 | def _exec(obj, data, *args, **kwargs): 6 | assert isinstance(obj, WrappedPolicyInterpreter) 7 | 8 | transformer = getattr(obj, 'transformer', None) 9 | if transformer is not None and data is not None: 10 | data = transformer.transform(data) 11 | 12 | return fn(obj, data, *args, **kwargs) 13 | 14 | return _exec 15 | 16 | 17 | class WrappedPolicyInterpreter(PolicyInterpreter): 18 | @_transform 19 | def decide(self, data): 20 | return super().decide(data) 21 | 22 | @_transform 23 | def predict(self, data): 24 | return super().predict(data) 25 | 26 | @_transform 27 | def interpret(self, data=None): 28 | return super().interpret(data) 29 | -------------------------------------------------------------------------------- /ylearn/api/smoke.py: -------------------------------------------------------------------------------- 1 | from ylearn.api import Why 2 | from ylearn.exp_dataset.exp_data import single_binary_treatment 3 | 4 | 5 | def smoke(estimator='auto'): 6 | print('-' * 20, 'smoke with estimator', estimator, '-' * 20) 7 | 8 | train, val, _ = single_binary_treatment() 9 | te = train.pop('TE') 10 | te = val.pop('TE') 11 | adjustment = [c for c in train.columns.tolist() if c.startswith('w')] 12 | covariate = [c for c in train.columns.tolist() if c.startswith('c')] 13 | 14 | if estimator == 'grf': 15 | covariate.extend(adjustment) 16 | adjustment = None 17 | 18 | why = Why(estimator=estimator) 19 | why.fit(train, outcome='outcome', treatment='treatment', adjustment=adjustment, covariate=covariate) 20 | 21 | cate = why.causal_effect(val) 22 | print('CATE:\n', cate) 23 | 24 | auuc = why.score(val, scorer='auuc') 25 | print('AUUC', auuc) 26 | 27 | 28 | if __name__ == '__main__': 29 | from ylearn.utils import logging 30 | 31 | logging.set_level('info') 32 | for est in ['slearner', 'tlearner', 'xlearner', 'dr', 'dml', 'tree', 'grf']: 33 | smoke(est) 34 | 35 | print('\n') 36 | -------------------------------------------------------------------------------- /ylearn/bayesian/__init__.py: -------------------------------------------------------------------------------- 1 | from ._dag import DAG, DiGraph 2 | 3 | try: 4 | from ._data import DataLoader 5 | from ._network import BayesianNetwork, SviBayesianNetwork 6 | except ImportError: 7 | pass 8 | -------------------------------------------------------------------------------- /ylearn/bayesian/_base.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from collections import OrderedDict 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from pyro.poutine import Trace 8 | from ylearn.utils import to_repr 9 | 10 | 11 | class BObject(object): 12 | def __repr__(self): 13 | return to_repr(self) 14 | 15 | 16 | class BState(BObject): 17 | """ 18 | Bayesian network node state. 19 | """ 20 | 21 | def encode(self, value): 22 | """ 23 | expression value to trainable value 24 | """ 25 | raise NotImplementedError() 26 | 27 | def decode(self, value): 28 | """ 29 | trainable value to expression value 30 | """ 31 | raise NotImplementedError() 32 | 33 | 34 | class NumericalNodeState(BState): 35 | def __init__(self, mean, scale, min, max): 36 | self.mean = mean 37 | self.scale = scale 38 | self.min = min 39 | self.max = max 40 | 41 | def encode(self, value): 42 | return value 43 | 44 | def decode(self, value): 45 | return value 46 | 47 | encode.__doc__ = BState.encode.__doc__ 48 | decode.__doc__ = BState.decode.__doc__ 49 | 50 | 51 | class CategoryNodeState(BState): 52 | def __init__(self, classes): 53 | assert len(classes) >= 2 54 | self.classes = np.sort(classes) 55 | 56 | @property 57 | def n_classes(self): 58 | return len(self.classes) 59 | 60 | @property 61 | def is_binary(self): 62 | return len(self.classes) == 2 63 | 64 | def encode(self, value): 65 | """ 66 | classes to indices 67 | """ 68 | r = np.searchsorted(self.classes, value) 69 | return r 70 | 71 | def decode(self, value): 72 | """ 73 | indices to classes 74 | """ 75 | r = np.take(self.classes, value) 76 | return r 77 | 78 | 79 | class BCollector(BObject): 80 | """ 81 | Sample collector 82 | """ 83 | 84 | def __init__(self, name, state): 85 | self.name = name 86 | self.state = copy.copy(state) 87 | 88 | def __call__(self, value): 89 | raise NotImplementedError() 90 | 91 | def to(self, result): 92 | raise NotImplementedError() 93 | 94 | def to_df(self): 95 | result = OrderedDict() 96 | self.to(result) 97 | return pd.DataFrame(result) 98 | 99 | 100 | class BCollectorList(BObject): 101 | """ 102 | Sample collector list 103 | """ 104 | 105 | def __init__(self, names, collector_creator): 106 | assert isinstance(names, (list, tuple)) 107 | assert callable(collector_creator) or isinstance(collector_creator, (tuple, list)) 108 | 109 | if isinstance(collector_creator, (tuple, list)): 110 | assert len(names) == len(collector_creator) 111 | else: 112 | collector_creator = list(map(collector_creator, names)) 113 | 114 | self.names = names 115 | self.collectors = collector_creator 116 | 117 | def __call__(self, value): 118 | if isinstance(value, Trace): 119 | for name, collector in zip(self.names, self.collectors): 120 | collector(value.nodes[name]['value']) 121 | elif isinstance(value, dict): 122 | for name, collector in zip(self.names, self.collectors): 123 | collector(value[name]) 124 | else: 125 | raise ValueError(f'Unsupported value type: "{type(value).__name__}"') 126 | 127 | def to(self, result): 128 | for collector in self.collectors: 129 | collector.to(result) 130 | 131 | def to_df(self): 132 | result = OrderedDict() 133 | self.to(result) 134 | return pd.DataFrame(result) 135 | -------------------------------------------------------------------------------- /ylearn/bayesian/_collectors.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | from . import _base 4 | 5 | 6 | class NumberSampleCollector(_base.BCollector): 7 | def __init__(self, name, state): 8 | assert isinstance(state, _base.NumericalNodeState) 9 | super().__init__(name, state) 10 | 11 | self.value = None 12 | self.n_sample = 0 13 | 14 | def __call__(self, value): 15 | if self.value is None: 16 | self.value = value 17 | else: 18 | self.value += value 19 | self.n_sample += 1 20 | 21 | def to(self, result): 22 | result[self.name] = self.value / self.n_sample 23 | 24 | 25 | class CategorySampleCollector(_base.BCollector): 26 | def __init__(self, name, state, proba=False): 27 | assert isinstance(state, _base.CategoryNodeState) 28 | super().__init__(name, state) 29 | 30 | self.proba = proba 31 | self.ones = torch.eye(state.n_classes, dtype=torch.int) 32 | self.value = None 33 | self.n_sample = 0 34 | 35 | def __call__(self, value): 36 | value = self.ones.index_select(0, value) # one-hot 37 | if self.value is None: 38 | self.value = value 39 | else: 40 | self.value += value 41 | self.n_sample += 1 42 | 43 | def to(self, result): 44 | if self.proba: 45 | value = self.value / self.n_sample 46 | for i, c in enumerate(self.state.classes): 47 | result[f'{self.name}_{c}'] = value[:, i] 48 | else: 49 | value = self.state.decode(torch.argmax(self.value, dim=1)) 50 | result[self.name] = value 51 | -------------------------------------------------------------------------------- /ylearn/bayesian/_data.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import torch 3 | 4 | from . import _base 5 | 6 | 7 | class DataLoader(_base.BObject): 8 | def __init__(self, state=None, data=None): 9 | assert state is not None or data is not None 10 | assert state is None or isinstance(state, dict) 11 | assert data is None or isinstance(data, pd.DataFrame) 12 | 13 | # self.data = data 14 | self.state = DataLoader.state_of(data) if state is None else state 15 | 16 | def spread(self, data): 17 | df = data.copy() 18 | result = {} 19 | for c in df.columns.tolist(): 20 | v = self.state[c].encode(df[c].values) 21 | result[c] = torch.tensor(v) 22 | # 23 | # graph = self.graph 24 | # for node in graph.nodes: 25 | # parents = graph.get_parents(node) 26 | # if parents: 27 | # try: 28 | # result[f'{node}_inputs'] = torch.tensor(df[parents].values) 29 | # except: 30 | # pass 31 | 32 | return result 33 | 34 | @staticmethod 35 | def state_of(data: pd.DataFrame): 36 | state = {} 37 | for n in data.columns.tolist(): 38 | c = data[n] 39 | if c.dtype.kind == 'f': 40 | state[n] = _base.NumericalNodeState(c.mean(), c.std(), c.min(), c.max()) 41 | else: 42 | state[n] = _base.CategoryNodeState(c.unique()) 43 | 44 | return state 45 | -------------------------------------------------------------------------------- /ylearn/causal_discovery/Untitled-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from nn_dag import func\n", 10 | "from gen import gen\n" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [ 18 | { 19 | "name": "stdout", 20 | "output_type": "stream", 21 | "text": [ 22 | "(100, 5)\n" 23 | ] 24 | } 25 | ], 26 | "source": [ 27 | "X=gen()#输入数据,形式为(数据点的数目,节点数目)(这里是5个节点的100组数据点)\n", 28 | "print(X.shape)" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 6, 34 | "metadata": {}, 35 | "outputs": [ 36 | { 37 | "name": "stdout", 38 | "output_type": "stream", 39 | "text": [ 40 | "[[0. 0. 0. 0. 0. ]\n", 41 | " [1.6461781 0. 3.452942 0. 0. ]\n", 42 | " [1.7949979 0. 0. 0. 0. ]\n", 43 | " [0. 0.48947167 0. 0. 0.47931436]\n", 44 | " [1.3935319 0. 1.0132673 0. 0. ]]\n" 45 | ] 46 | } 47 | ], 48 | "source": [ 49 | "X=gen()\n", 50 | "W_est = func(X,extra_layer_dim=[3])#extra_layer_dim为中间的全连接层的尺寸结构,默认为空\n", 51 | "print(W_est)\n" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 9, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "name": "stdout", 61 | "output_type": "stream", 62 | "text": [ 63 | "0.999994877994358 2.3399700022186387e-05\n", 64 | "1.0089810198865068 0.00898101988650679\n", 65 | "[0.33889755]\n", 66 | "[0.58890243]\n" 67 | ] 68 | } 69 | ], 70 | "source": [ 71 | "import numpy as np\n", 72 | "from sklearn.linear_model import LinearRegression as LR\n", 73 | "def dat_gen():\n", 74 | " x = np.random.random_sample(20000)\n", 75 | " y=0.7*x +0.1*np.random.random_sample(20000)\n", 76 | " y=y/(np.max(y)-np.min(y))#对数据的归一化非常重要,直接决定拟合能否奏效\n", 77 | " return x,y\n", 78 | "\n", 79 | "x,y=dat_gen()\n", 80 | "print(np.max(x),np.min(x))\n", 81 | "print(np.max(y),np.min(y))\n", 82 | "lr=LR()\n", 83 | "lr.fit(x.reshape(-1, 1),y)\n", 84 | "print(lr.coef_)#这样算出来的权重更小,可以用来确认因果关系\n", 85 | "lr=LR()\n", 86 | "lr.fit(y.reshape(-1, 1),x)\n", 87 | "print(lr.coef_)" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [] 96 | } 97 | ], 98 | "metadata": { 99 | "interpreter": { 100 | "hash": "d207591e6ff77a7c5fb4cef0dd9fd3703274637a9d0902d2045beb3a65bf572a" 101 | }, 102 | "kernelspec": { 103 | "display_name": "Python 3.6.4 64-bit", 104 | "language": "python", 105 | "name": "python3" 106 | }, 107 | "language_info": { 108 | "codemirror_mode": { 109 | "name": "ipython", 110 | "version": 3 111 | }, 112 | "file_extension": ".py", 113 | "mimetype": "text/x-python", 114 | "name": "python", 115 | "nbconvert_exporter": "python", 116 | "pygments_lexer": "ipython3", 117 | "version": "3.6.4" 118 | }, 119 | "orig_nbformat": 4 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 2 123 | } 124 | -------------------------------------------------------------------------------- /ylearn/causal_discovery/__init__.py: -------------------------------------------------------------------------------- 1 | """Causal discovery algorithms. 2 | """ 3 | from ._base import BaseDiscovery 4 | 5 | try: 6 | from ._discovery import CausalDiscovery 7 | from ._discovery import GolemDiscovery 8 | from ._discovery import DagmaDiscovery 9 | from ._dydiscovery import DyCausalDiscovery 10 | from ._dydiscovery import DygolemCausalDiscovery 11 | 12 | except ImportError as e: 13 | _msg_cd = f'{e}, install pytorch and try again.' 14 | 15 | 16 | class CausalDiscovery(BaseDiscovery): 17 | def __init__(self, *args, **kwargs): 18 | raise ImportError(_msg_cd) 19 | 20 | try: 21 | from ._proxy_gcastle import GCastleProxy 22 | except ImportError as e: 23 | _msg_gcastle = f'{e}, install gcastle and try again.' 24 | 25 | 26 | class GCastleProxy(BaseDiscovery): 27 | def __init__(self, *args, **kwargs): 28 | raise ImportError(_msg_gcastle) 29 | 30 | try: 31 | from ._proxy_pgm import PgmProxy 32 | except ImportError as e: 33 | _msg_pgm = f'{e}, install pgmpy and try again.' 34 | 35 | 36 | class PgmProxy(BaseDiscovery): 37 | def __init__(self, *args, **kwargs): 38 | raise ImportError(_msg_pgm) 39 | except SyntaxError as e: 40 | _msg_pgm = f'{e}, re-install pgmpy and try again.' 41 | 42 | 43 | class PgmProxy(BaseDiscovery): 44 | def __init__(self, *args, **kwargs): 45 | raise SyntaxError(_msg_pgm) 46 | -------------------------------------------------------------------------------- /ylearn/causal_discovery/_base.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import networkx as nx 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from ylearn.utils import to_repr 8 | 9 | 10 | class BaseDiscovery: 11 | def __call__(self, data, *, return_dict=False, threshold=None, **kwargs): 12 | raise NotImplementedError() 13 | 14 | @staticmethod 15 | def trim_cycle(matrix): 16 | assert isinstance(matrix, np.ndarray) 17 | assert matrix.ndim == 2 and matrix.shape[0] == matrix.shape[1] 18 | 19 | g = nx.from_numpy_array(matrix, create_using=nx.DiGraph) # .reverse() 20 | 21 | def trim_with_weight(): 22 | edge_weights = nx.get_edge_attributes(g, 'weight') 23 | edges = sorted(edge_weights.keys(), key=lambda e: edge_weights[e], reverse=True) 24 | for X, Y in edges: 25 | if nx.has_path(g, Y, X): 26 | paths = list(nx.all_shortest_paths(g, Y, X, weight='weight')) 27 | for p in paths: 28 | es = sorted(zip(p[:-1], p[1:]), key=lambda e: edge_weights[e]) 29 | u, v = es[0] 30 | if g.has_edge(u, v): 31 | g.remove_edge(u, v) 32 | return True 33 | return False 34 | 35 | while trim_with_weight(): 36 | pass 37 | 38 | assert nx.is_directed_acyclic_graph(g) 39 | return nx.to_numpy_array(g) 40 | 41 | @staticmethod 42 | def matrix2dict(matrix, names=None, depth=None): 43 | assert isinstance(matrix, (np.ndarray, pd.DataFrame)) 44 | assert matrix.ndim == 2 and matrix.shape[0] == matrix.shape[1] 45 | assert names is None or len(names) == matrix.shape[0] 46 | 47 | matrix = matrix.copy() 48 | n = matrix.shape[0] 49 | 50 | if names is None: 51 | if isinstance(matrix, pd.DataFrame): 52 | names = matrix.columns.tolist() 53 | else: 54 | names = range(n) 55 | 56 | if isinstance(matrix, pd.DataFrame): 57 | matrix = matrix.values 58 | 59 | g = nx.from_numpy_array(matrix, create_using=nx.DiGraph).reverse() 60 | 61 | d = OrderedDict() 62 | for i, name in enumerate(names): 63 | t = set(c[1] for c in nx.dfs_edges(g, i, depth_limit=depth)) 64 | d[name] = [names[j] for j in range(n) if j in t] 65 | 66 | return d 67 | 68 | @staticmethod 69 | def matrix2array(matrix, names=None, ): 70 | """ 71 | Returns 72 | ------- 73 | table of: 74 | from,to,prob 75 | ... 76 | """ 77 | assert isinstance(matrix, (np.ndarray, pd.DataFrame)) 78 | assert matrix.ndim == 2 and matrix.shape[0] == matrix.shape[1] 79 | assert names is None or len(names) == matrix.shape[0] 80 | 81 | if names is None and isinstance(matrix, pd.DataFrame): 82 | names = matrix.columns.tolist() 83 | 84 | r = [] 85 | for row in range(matrix.shape[0]): 86 | for col in range(row + 1, matrix.shape[1]): 87 | if hasattr(matrix, 'iloc'): 88 | v = matrix.iloc[row, col] 89 | vt = matrix.iloc[col, row] 90 | else: 91 | v = matrix[row, col] 92 | vt = matrix[row, col] 93 | if abs(v) >= abs(vt): 94 | r.append([row, col, v]) 95 | else: 96 | r.append([col, row, vt]) 97 | 98 | if names is not None: 99 | r = [[names[row[0]], names[row[1]], row[2]] for row in r] 100 | 101 | return r 102 | 103 | @staticmethod 104 | def matrix2df(matrix, names=None, ): 105 | r = BaseDiscovery.matrix2array(matrix, names=names) 106 | return pd.DataFrame(r, columns=['from', 'to', 'prob']) 107 | 108 | def __repr__(self): 109 | return to_repr(self) 110 | -------------------------------------------------------------------------------- /ylearn/causal_discovery/_proxy_gcastle.py: -------------------------------------------------------------------------------- 1 | """ 2 | A proxy to Huawei Noah's Ark Lab gCastle. 3 | see: https://github.com/huawei-noah/trustworthyAI/tree/master/gcastle 4 | """ 5 | import copy 6 | import os 7 | import sys 8 | 9 | import numpy as np 10 | import pandas as pd 11 | 12 | from ylearn.utils import drop_none, set_random_state, logging 13 | from ._base import BaseDiscovery 14 | 15 | devnull = open(os.devnull, "w") 16 | stdout_orig = sys.stdout 17 | stderr_orig = sys.stderr 18 | try: 19 | sys.stdout = devnull 20 | sys.stderr = devnull 21 | from castle import algorithms as A 22 | from castle.common import BaseLearner 23 | finally: 24 | sys.stdout = stdout_orig 25 | sys.stderr = stderr_orig 26 | 27 | logger = logging.get_logger(__name__) 28 | 29 | 30 | class GCastleProxy(BaseDiscovery): 31 | def __init__(self, learner=None, random_state=None, **kwargs): 32 | assert learner is None or isinstance(learner, (str, BaseLearner)) 33 | if isinstance(learner, str): 34 | assert hasattr(A, learner), f'Not found learner "{learner}" from gcastle' 35 | c = getattr(A, learner) 36 | assert issubclass(c, BaseLearner) 37 | 38 | self.learner = learner 39 | self.options = kwargs.copy() 40 | self.random_state = random_state 41 | 42 | def _create_learner(self): 43 | if isinstance(self.learner, BaseLearner): 44 | return copy.copy(self.learner) 45 | else: 46 | c = getattr(A, self.learner) if self.learner is not None else A.PC 47 | return c(**self.options) 48 | 49 | def __call__(self, data, *, return_dict=False, threshold=None, **kwargs): 50 | assert isinstance(data, (np.ndarray, pd.DataFrame)) 51 | 52 | set_random_state(self.random_state) 53 | 54 | if isinstance(data, pd.DataFrame): 55 | columns = data.columns.tolist() 56 | data = data.values 57 | else: 58 | columns = None 59 | 60 | learner = self._create_learner() 61 | logger.info(f'discovery causation with {type(learner).__name__}') 62 | learner.learn(data) 63 | matrix = learner.causal_matrix 64 | 65 | if columns is not None: 66 | matrix = pd.DataFrame(matrix, columns=columns, index=columns) 67 | 68 | if return_dict: 69 | result = self.matrix2dict(matrix, **drop_none(threshold=threshold)) 70 | else: 71 | result = matrix 72 | 73 | return result 74 | -------------------------------------------------------------------------------- /ylearn/causal_discovery/_proxy_pgm.py: -------------------------------------------------------------------------------- 1 | """ 2 | A proxy to pgmpy structure estimators 3 | see: https://github.com/pgmpy/pgmpy 4 | """ 5 | import inspect 6 | 7 | import networkx as nx 8 | import numpy as np 9 | import pandas as pd 10 | from pgmpy import estimators as A 11 | 12 | from ylearn.utils import set_random_state, logging 13 | from ._base import BaseDiscovery 14 | 15 | logger = logging.get_logger(__name__) 16 | 17 | _default_options = dict( 18 | PC=dict(variant="stable", 19 | ci_test="pearsonr", # default continuous datasets. 20 | show_progress=False), 21 | ) 22 | 23 | 24 | class PgmProxy(BaseDiscovery): 25 | def __init__(self, learner='PC', random_state=None, **kwargs): 26 | assert isinstance(learner, str) and hasattr(A, learner), \ 27 | f'Not found learner "{learner}" from pgmpy.estimators' 28 | c = getattr(A, learner) 29 | assert issubclass(c, A.StructureEstimator) 30 | 31 | self.learner = learner 32 | self.options = kwargs.copy() 33 | self.random_state = random_state 34 | 35 | def _create_learner(self, data, options): 36 | c = getattr(A, self.learner) if self.learner is not None else A.PC 37 | 38 | kwargs = {} 39 | for k in inspect.signature(c.__init__).parameters.keys(): 40 | if k in options.keys(): 41 | kwargs[k] = options.pop(k) 42 | return c(data, **kwargs) 43 | 44 | def __call__(self, data, *, return_dict=False, threshold=None, **kwargs): 45 | assert isinstance(data, (np.ndarray, pd.DataFrame)) 46 | 47 | set_random_state(self.random_state) 48 | 49 | if isinstance(data, pd.DataFrame): 50 | df = data 51 | else: 52 | df = pd.DataFrame(data) 53 | 54 | options = _default_options.get(self.learner, {}).copy() 55 | options.update(**self.options) 56 | learner = self._create_learner(df, options) 57 | 58 | logger.info(f'discovery causation with {type(learner).__name__}') 59 | if isinstance(learner, A.PC): 60 | options['return_type'] = 'dag' 61 | dag = learner.estimate(**options) 62 | 63 | columns = df.columns.tolist() 64 | nodes = list(dag.nodes) 65 | assert set(nodes).issubset(set(columns)) 66 | 67 | matrix_learned = pd.DataFrame(nx.to_numpy_array(dag, nodelist=nodes, weight=None), 68 | columns=nodes, index=nodes) 69 | matrix_full = pd.DataFrame(np.zeros((df.shape[1], df.shape[1])), 70 | columns=columns, index=columns) 71 | matrix_full = (matrix_full + matrix_learned).fillna(0.0) 72 | 73 | if isinstance(data, pd.DataFrame): 74 | matrix = matrix_full 75 | else: 76 | matrix = matrix_full.values 77 | 78 | if return_dict: 79 | result = self.matrix2dict(matrix) 80 | else: 81 | result = matrix 82 | 83 | return result 84 | -------------------------------------------------------------------------------- /ylearn/causal_discovery/dag.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | 4 | 5 | def func(dat, pho=10, alpha=1, lambda1=1): 6 | cuda = torch.device("cuda" if torch.cuda.is_available() else "cpu") 7 | x = torch.tensor(dat, device=cuda).float() 8 | n = x.size()[0] 9 | batch_num = x.size()[1] 10 | w = torch.rand(n, n, device=cuda, requires_grad=True) 11 | 12 | def h(w): 13 | return torch.trace(torch.matrix_exp(w * w)) / n - 1 14 | 15 | def linear_loss(w): 16 | res = x - torch.matmul(w, x) 17 | return torch.sum(res * res) / (n * batch_num) 18 | 19 | def tot_loss(w, pho, alpha): 20 | return linear_loss(w) + h(w) * h(w) * 0.5 * pho + alpha * h(w) + lambda1 * torch.sum(w * w) / (n * n) 21 | 22 | optimizer = torch.optim.SGD([w], lr=0.1, momentum=0.9) 23 | 24 | def local_minimize(pho, alpha): 25 | for i in range(200): 26 | optimizer.zero_grad() 27 | l = tot_loss(w, pho, alpha) 28 | l.backward() 29 | optimizer.step() 30 | # print(i,w) 31 | 32 | h_ = h(w.clone().detach()) 33 | for _ in range(100): 34 | local_minimize(pho, alpha) 35 | alpha = alpha + pho * h(w.clone().detach()) 36 | print(w) 37 | 38 | return w.detach().cpu().numpy() 39 | 40 | 41 | def reg(x, y): 42 | cuda = torch.device("cuda" if torch.cuda.is_available() else "cpu") 43 | x = torch.tensor(x, device=cuda).float() 44 | y = torch.tensor(y, device=cuda).float() 45 | size = x.size()[0] 46 | batch_num = x.size()[1] 47 | w = torch.ones([size], device=cuda, requires_grad=True).float() 48 | wt = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], device=cuda).float() 49 | optimizer = torch.optim.SGD([w], lr=0.01, momentum=0.9) 50 | 51 | def l(w, x, y): 52 | res = y - torch.matmul(w, x) 53 | loss = torch.sum(res * res) / (size * batch_num) 54 | return loss 55 | 56 | for i in range(10000): 57 | optimizer.zero_grad() 58 | loss = l(w, x, y) 59 | loss.backward() 60 | optimizer.step() 61 | # w=torch.tensor(w-0.1*w.grad,device=cuda,requires_grad=True).float() 62 | 63 | # w = torch.tensor(w - 0.5 * w.grad,requires_grad=True).float() 64 | # print(w) 65 | print(loss) 66 | print(w) 67 | 68 | 69 | def dat_gen(): 70 | x1 = np.random.random_sample(20000) 71 | x2 = x1 + 0.001 * np.random.random_sample(20000) 72 | x3 = x2 - 0.001 * np.random.random_sample(20000) 73 | return np.array([x2, x1, x3]) 74 | 75 | 76 | # def dat_gen1(n, coef12=1, coef32=2, eps=1e-3, change_order=False): 77 | # x2 = np.random.normal(size=(n, )) 78 | # x1 = eps * np.random.normal(size=(n, )) + coef12 * x2 79 | # x3 = eps * np.random.normal(size=(n, )) + coef32 * x2 80 | # if change_order: 81 | # return np.array([x1, x2, x3]) 82 | # else: 83 | # return np.array([x2, x1, x3]) 84 | 85 | 86 | if __name__ == 'main': 87 | dat = dat_gen() 88 | print(f'chain result {func(dat)}') 89 | 90 | # dat = dat_gen1() 91 | # print(f'folk result {func(dat)}') 92 | 93 | # dat = dat_gen1() 94 | # print(f'folk new order {func(dat)}') -------------------------------------------------------------------------------- /ylearn/causal_discovery/toy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.linalg as slin 3 | import scipy.optimize as sopt 4 | from scipy.special import expit as sigmoid 5 | 6 | 7 | def notears_linear(X, lambda1, loss_type, max_iter=100, h_tol=1e-8, rho_max=1e+16, w_threshold=0.3): 8 | """Solve min_W L(W; X) + lambda1 ‖W‖_1 s.t. h(W) = 0 using augmented Lagrangian. 9 | Args: 10 | X (np.ndarray): [n, d] sample matrix 11 | lambda1 (float): l1 penalty parameter 12 | loss_type (str): l2, logistic, poisson 13 | max_iter (int): max num of dual ascent steps 14 | h_tol (float): exit if |h(w_est)| <= htol 15 | rho_max (float): exit if rho >= rho_max 16 | w_threshold (float): drop edge if |weight| < threshold 17 | Returns: 18 | W_est (np.ndarray): [d, d] estimated DAG 19 | """ 20 | def _loss(W): 21 | """Evaluate value and gradient of loss.""" 22 | M = X @ W 23 | if loss_type == 'l2': 24 | R = X - M 25 | loss = 0.5 / X.shape[0] * (R ** 2).sum() 26 | G_loss = - 1.0 / X.shape[0] * X.T @ R 27 | elif loss_type == 'logistic': 28 | loss = 1.0 / X.shape[0] * (np.logaddexp(0, M) - X * M).sum() 29 | G_loss = 1.0 / X.shape[0] * X.T @ (sigmoid(M) - X) 30 | elif loss_type == 'poisson': 31 | S = np.exp(M) 32 | loss = 1.0 / X.shape[0] * (S - X * M).sum() 33 | G_loss = 1.0 / X.shape[0] * X.T @ (S - X) 34 | else: 35 | raise ValueError('unknown loss type') 36 | return loss, G_loss 37 | 38 | def _h(W): 39 | """Evaluate value and gradient of acyclicity constraint.""" 40 | E = slin.expm(W * W) # (Zheng et al. 2018) 41 | h = np.trace(E) - d 42 | # # A different formulation, slightly faster at the cost of numerical stability 43 | # M = np.eye(d) + W * W / d # (Yu et al. 2019) 44 | # E = np.linalg.matrix_power(M, d - 1) 45 | # h = (E.T * M).sum() - d 46 | G_h = E.T * W * 2 47 | return h, G_h 48 | 49 | def _adj(w): 50 | """Convert doubled variables ([2 d^2] array) back to original variables ([d, d] matrix).""" 51 | return (w[:d * d] - w[d * d:]).reshape([d, d]) 52 | 53 | def _func(w): 54 | """Evaluate value and gradient of augmented Lagrangian for doubled variables ([2 d^2] array).""" 55 | W = _adj(w) 56 | loss, G_loss = _loss(W) 57 | h, G_h = _h(W) 58 | obj = loss + 0.5 * rho * h * h + alpha * h + lambda1 * w.sum() 59 | G_smooth = G_loss + (rho * h + alpha) * G_h 60 | g_obj = np.concatenate((G_smooth + lambda1, - G_smooth + lambda1), axis=None) 61 | return obj, g_obj 62 | 63 | n, d = X.shape 64 | w_est, rho, alpha, h = np.zeros(2 * d * d), 1.0, 0.0, np.inf # double w_est into (w_pos, w_neg) 65 | bnds = [(0, 0) if i == j else (0, None) for _ in range(2) for i in range(d) for j in range(d)] 66 | if loss_type == 'l2': 67 | X = X - np.mean(X, axis=0, keepdims=True) 68 | for _ in range(max_iter): 69 | w_new, h_new = None, None 70 | while rho < rho_max: 71 | sol = sopt.minimize(_func, w_est, method='L-BFGS-B', jac=True, bounds=bnds) 72 | w_new = sol.x 73 | h_new, _ = _h(_adj(w_new)) 74 | if h_new > 0.25 * h: 75 | rho *= 10 76 | else: 77 | break 78 | w_est, h = w_new, h_new 79 | alpha += rho * h 80 | if h <= h_tol or rho >= rho_max: 81 | break 82 | W_est = _adj(w_est) 83 | W_est[np.abs(W_est) < w_threshold] = 0 84 | return W_est 85 | 86 | 87 | def dat_gen(n): 88 | x1 = np.random.random_sample(n) 89 | x2 = np.exp(-x1)+0.001 * np.random.random_sample(n) 90 | x3 = np.exp(-x2)-0.001 * np.random.random_sample(n) 91 | return np.array([x1, x2, x3]).T 92 | 93 | 94 | for i in range(20): 95 | dat = dat_gen(1000) 96 | W_est = notears_linear(dat, lambda1=0.01, loss_type='l2') 97 | print(W_est)#跑出来每次都不一样。。。。 98 | 99 | # def dat_gen1(n=1000, coef12=1, coef32=2, eps=1e-3, change_order=False): 100 | # x2 = np.random.normal(size=(n, )) 101 | # x1 = eps * np.random.normal(size=(n, )) + coef12 * x2 102 | # x3 = eps * np.random.normal(size=(n, )) + coef32 * x2 103 | # if change_order: 104 | # return np.array([x1, x2, x3]).T 105 | # else: 106 | # return np.array([x2, x1, x3]).T 107 | 108 | 109 | # for i in range(5): 110 | # dat = dat_gen(1000) 111 | # print(f'chain result {notears_linear(dat, lambda1=0.01, loss_type="l2")}') 112 | 113 | # dat = dat_gen1() 114 | # print(f'folk result {notears_linear(dat,lambda1=0.01, loss_type="l2")}') 115 | 116 | # dat = dat_gen1(change_order=True) 117 | # print(f'folk new order {notears_linear(dat, lambda1=0.01, loss_type="l2")}') -------------------------------------------------------------------------------- /ylearn/causal_model/__init__.py: -------------------------------------------------------------------------------- 1 | # from . import graph 2 | # from . import model 3 | # from . import prob 4 | # from . import scm 5 | # #from . import identification 6 | from .graph import CausalGraph 7 | from .model import CausalModel 8 | from .prob import Prob 9 | from .scm import CausalStructuralModel 10 | -------------------------------------------------------------------------------- /ylearn/causal_model/other_identification/__init__.py: -------------------------------------------------------------------------------- 1 | from . import do_operator 2 | from . import gradient_based 3 | from . import instrumental_variables -------------------------------------------------------------------------------- /ylearn/causal_model/other_identification/do_operator.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Do: 4 | """Implementation of the do-operator. 5 | 6 | Attributes 7 | ---------- 8 | causation : CausalGraph or CausalStructureModel 9 | treatment : str 10 | 11 | Methods 12 | ---------- 13 | """ 14 | 15 | def __init__(self, causation): 16 | """ 17 | Parameters 18 | ---------- 19 | causation : CausalGraph or CausalStructureModel 20 | the causation structures in which the do-operator will be applied 21 | """ 22 | self.causation = causation 23 | 24 | def do(self, treatment, value): 25 | """Do the treatment=value in the graph or structural models. 26 | 27 | Parameters 28 | ---------- 29 | treatment : str 30 | value : float, optional 31 | the value of the treatment 32 | """ 33 | self.causation.remove_incoming_edges(treatment) 34 | # TODO 35 | 36 | # def remove_incoming_edges(self, node): 37 | # """remove the incoming edges to node 38 | 39 | # Parameters 40 | # ---------- 41 | # node : str 42 | # """ 43 | # pass 44 | 45 | 46 | class DoCalculus: 47 | # TODO: Rules of do-calculus are implemented as follows 48 | def rule_one(self): 49 | pass 50 | 51 | def rule_two(self): 52 | pass 53 | 54 | def rule_three(self): 55 | pass 56 | -------------------------------------------------------------------------------- /ylearn/causal_model/other_identification/gradient_based.py: -------------------------------------------------------------------------------- 1 | class GradientBased: 2 | def __init__(self) -> None: 3 | pass -------------------------------------------------------------------------------- /ylearn/causal_model/other_identification/minimal_adjust_set.py: -------------------------------------------------------------------------------- 1 | class AdjustmentSet: 2 | def __init__(self) -> None: 3 | pass -------------------------------------------------------------------------------- /ylearn/causal_model/prob.py: -------------------------------------------------------------------------------- 1 | from IPython.display import Latex 2 | 3 | 4 | class Prob: 5 | r""" 6 | Probability distribution, e.g., the probability expression 7 | \sum_{w}P(v|y)[P(w|z)P(x|y)P(u)]. We will clarify below the meanings 8 | of our variables. 9 | 10 | 11 | Attributes 12 | ---------- 13 | variables : set 14 | The variables (v in the above example) of the probability. 15 | 16 | conditional : set 17 | The conditional set (y in the above example). 18 | 19 | divisor : set 20 | Not defined yet. 21 | 22 | marginal : set 23 | The sum set (w in the above example) for marginalizing the probability. 24 | 25 | product : set 26 | If not set(), then the probability is composed of the first probability 27 | object (P(v|y)) and several other probabiity objects that are all saved 28 | in the set product, e.g., product = {P1, P2, P3} where P1 for P(w|z), 29 | P2 for P(x|y), and P3 for P(u) in the above example. 30 | 31 | Methods 32 | ---------- 33 | parse() 34 | Return the expression of the probability distribution. 35 | 36 | show_latex_expression() 37 | Show the latex expression. 38 | """ 39 | 40 | def __init__(self, 41 | variables=set(), 42 | conditional=set(), 43 | divisor=set(), 44 | marginal=set(), 45 | product=set()): 46 | """ 47 | Parameters 48 | ---------- 49 | variables : set 50 | 51 | conditional : set 52 | 53 | marginal : set 54 | elements are strings, summing over these elements will return the 55 | marginal distribution 56 | 57 | product : set 58 | set of Prob 59 | """ 60 | self.divisor = divisor 61 | self.variables = variables 62 | self.conditional = conditional 63 | self.marginal = marginal 64 | self.product = product 65 | 66 | def parse(self): 67 | """ 68 | Return the expression of the probability distribution. 69 | 70 | Returns 71 | ---------- 72 | expression : str 73 | """ 74 | # TODO 75 | expression = '' 76 | # First find the marginal set, -> \sum_{marginal set} 77 | if self.marginal: 78 | mar = ', ' 79 | mar = mar.join(self.marginal) 80 | expression = expression + '\\sum_{' + f'{mar}' + '}' 81 | 82 | # Find the variables, -> \sum_{marginal} P(variables|conditional) 83 | if self.variables: 84 | var = ', ' 85 | var = var.join(self.variables) 86 | if self.conditional: 87 | cond = ', ' 88 | cond = cond.join(self.conditional) 89 | expression = expression + f'P({var}|{cond})' 90 | else: 91 | expression += f'P({var})' 92 | 93 | # Find the products, -> 94 | # \sum_{marginal} P(var|cond) \sum_{prod1.mar}P(prod1.var|prod1.cond).. 95 | if self.product: 96 | for p in self.product: 97 | expression = expression + '\\left[' + p.parse() + '\\right]' 98 | 99 | return expression 100 | 101 | def show_latex_expression(self): 102 | """ 103 | Show the latex expression. 104 | """ 105 | return Latex(f'${self.parse()}$') 106 | 107 | # def __repr__(self) -> str: 108 | # return 'Prob' -------------------------------------------------------------------------------- /ylearn/causal_model/scm.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | 3 | 4 | class CausalStructuralModel: 5 | def __init__(self, causation): 6 | self.edges = self.build_edges(causation) 7 | pass 8 | 9 | def build_edges(self, causation): 10 | pass 11 | -------------------------------------------------------------------------------- /ylearn/causal_model/utils.py: -------------------------------------------------------------------------------- 1 | from re import I 2 | import networkx as nx 3 | 4 | from copy import deepcopy 5 | from itertools import combinations 6 | 7 | 8 | def ancestors_of_iter(g, x): 9 | """Return the ancestors of all nodes in x. 10 | 11 | Parameters 12 | ---------- 13 | x : set of str 14 | a set of nodes in the graph 15 | 16 | Returns 17 | ---------- 18 | set of str 19 | Ancestors of nodes x of the graph 20 | """ 21 | an = set() 22 | x = {x} if isinstance(x, str) else x 23 | 24 | for node in x: 25 | an.add(node) 26 | try: 27 | an.update(nx.ancestors(g, node)) 28 | except Exception: 29 | pass 30 | 31 | return an 32 | 33 | 34 | def descendents_of_iter(g, x): 35 | """Return the descendents of all nodes in x. 36 | 37 | Parameters 38 | ---------- 39 | x : set of str 40 | a set of nodes in the graph 41 | 42 | Returns 43 | ---------- 44 | set of str 45 | Descendents of nodes x of the graph 46 | """ 47 | des = set() 48 | x = {x} if isinstance(x, str) else x 49 | 50 | for node in x: 51 | des.add(node) 52 | try: 53 | des.update(nx.descendants(g, node)) 54 | except Exception: 55 | pass 56 | 57 | return des 58 | 59 | def remove_ingo_edges(graph, incoming=True, *S): 60 | """Remove incoming or outgoing edges for nodes of S in the graph. 61 | 62 | Parameters 63 | ---------- 64 | graph : nx.DiGraph, optional 65 | 66 | incoming : bool, optional 67 | If True, then all incoming edges of nodes in S will be removed, 68 | otherwise remove all outgoing edges of nodes in S, by default True 69 | 70 | Returns 71 | ------- 72 | nx.DiGraph, optional 73 | A deepcopy of graph where the incoming edges or outgoing edges of nodes 74 | in S are removed. 75 | """ 76 | g = deepcopy(graph) 77 | S = filter(None, S) 78 | 79 | for i in S: 80 | i = {i} if isinstance(i, str) else i 81 | check_nodes(g.nodes, i) 82 | for node in i: 83 | if incoming: 84 | g.remove_edges_from([(p, node) for p in g.predecessors(node)]) 85 | else: 86 | g.remove_edges_from([(node, c) for c in g.successors(node)]) 87 | 88 | return g 89 | 90 | def check_nodes(nodes=None, *S): 91 | """Check if nodes contained in S are present in nodes. 92 | 93 | Parameters 94 | ---------- 95 | nodes : list, optional 96 | A list of nodes which should include all nodes in S, by default None 97 | """ 98 | S = filter(None, S) 99 | for i in S: 100 | i = {i} if isinstance(i, str) else i 101 | for j in i: 102 | assert j in nodes, f'The node {j} is not in all avaliable nodes {nodes}' 103 | 104 | 105 | def check_ancestors_chain(dag, node, U): 106 | an = nx.ancestors(dag, node) 107 | 108 | if len(an) == 0: 109 | return True 110 | elif U in an: 111 | return False 112 | else: 113 | for n in an: 114 | if not check_ancestors_chain(dag, n, U): 115 | return False 116 | 117 | return True 118 | 119 | 120 | def powerset(iterable): 121 | """Return the power set of the iterable. 122 | 123 | Parameters 124 | ---------- 125 | iterable : container 126 | Can be a set or a list. 127 | 128 | Returns 129 | ---------- 130 | list 131 | The list of power set. 132 | """ 133 | # TODO: return a generator instead of a list 134 | s = list(iterable) 135 | power_set = [] 136 | for i in range(len(s) + 1): 137 | for j in combinations(s, i): 138 | power_set.append(set(j)) 139 | return power_set 140 | 141 | 142 | class IdentificationError(Exception): 143 | def __init__(self, info): 144 | super().__init__(self) 145 | self.info = info 146 | 147 | def __str__(self): 148 | return self.info 149 | -------------------------------------------------------------------------------- /ylearn/effect_interpreter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ylearn/estimator_model/__init__.py: -------------------------------------------------------------------------------- 1 | # from . import base_models, double_ml, doubly_robust, meta_learner,\ 2 | # propensity_score 3 | 4 | from ._factory import ESTIMATOR_FACTORIES 5 | from ._permuted import PermutedDoublyRobust 6 | from ._permuted import PermutedSLearner, PermutedTLearner, PermutedXLearner 7 | from .approximation_bound import ApproxBound 8 | from .base_models import BaseEstModel 9 | from .double_ml import DoubleML 10 | from .doubly_robust import DoublyRobust 11 | from .effect_score import RLoss, PredLoss 12 | from .ensemble import EnsembleEstModels 13 | from .iv import NP2SLS 14 | from .meta_learner import SLearner, TLearner, XLearner 15 | from .propensity_score import InversePbWeighting, PropensityScore 16 | 17 | try: 18 | from .deepiv import DeepIV 19 | except ImportError as e: # torch not ready 20 | _msg_deep_iv = f"{e}" 21 | 22 | class DeepIV: 23 | def __init__(self, *args, **kwargs): 24 | raise ImportError(_msg_deep_iv) 25 | 26 | try: 27 | from .causal_tree import CausalTree 28 | except ImportError as e: # cython extension not ready 29 | _msg_causal_tree = f"{e}" 30 | 31 | class CausalTree: 32 | def __init__(self, *args, **kwargs): 33 | raise ImportError(_msg_causal_tree) 34 | 35 | try: 36 | from ._generalized_forest._grf import GRForest 37 | except ImportError as e: # cython extension not ready 38 | _msg_grf = f"{e}" 39 | 40 | class GRForest: 41 | def __init__(self, *args, **kwargs): 42 | raise ImportError(_msg_grf) 43 | try: 44 | from .causal_forest import CausalForest 45 | except ImportError as e: # cython extension not ready 46 | _msg_causal_forest = f"{e}" 47 | 48 | class CausalForest: 49 | def __init__(self, *args, **kwargs): 50 | raise ImportError(_msg_causal_forest) 51 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/__init__.py: -------------------------------------------------------------------------------- 1 | from ._base_forest import BaseForest, BaseCausalForest 2 | from ._grf import GRForest 3 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/__init__.py: -------------------------------------------------------------------------------- 1 | from ._criterion import GrfTreeCriterion 2 | from ._splitter import GrfTreeBestSplitter 3 | from ._tree import GrfTreeBestFirstBuilder 4 | from ._grf_tree import GrfTree 5 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/_criterion.pxd: -------------------------------------------------------------------------------- 1 | from ylearn.sklearn_ex.cloned.tree._criterion cimport Criterion 2 | 3 | from ylearn.sklearn_ex.cloned.tree._tree cimport DOUBLE_t 4 | from ylearn.sklearn_ex.cloned.tree._tree cimport SIZE_t 5 | 6 | cdef class CriterionEx(Criterion): 7 | """ 8 | Criterion with treatment 9 | """ 10 | 11 | cdef const DOUBLE_t[:, ::1] treatment 12 | 13 | cdef int init_ex(self, const DOUBLE_t[:, ::1] y, const DOUBLE_t[:, ::1] treatment, DOUBLE_t* sample_weight, # fix 14 | double weighted_n_samples, SIZE_t* samples, SIZE_t start, 15 | SIZE_t end) nogil except -1 16 | 17 | 18 | cdef class GrfTreeCriterion(CriterionEx): 19 | cdef SIZE_t d_tr 20 | 21 | cdef double sq_sum_total 22 | 23 | cdef double[::1] sum_total # The sum of w*y. 24 | cdef double[::1] sum_left # Same as above, but for the left side of the split 25 | cdef double[::1] sum_right # Same as above, but for the right side of the split 26 | 27 | cdef double[:, ::1] grad 28 | cdef double[::1] sum_tr 29 | cdef double[::1] mean_tr 30 | cdef double[::1] mean_sum 31 | 32 | cdef double[::1] rho 33 | cdef double sum_rho 34 | cdef double sum_rho_left 35 | cdef double sum_rho_right 36 | # 37 | # cdef class TESTGrfTreeCriterion(CriterionEx): 38 | # cdef SIZE_t d_tr 39 | # 40 | # cdef double sq_sum_total 41 | # 42 | # cdef double[::1] sum_total # The sum of w*y. 43 | # cdef double[::1] sum_left # Same as above, but for the left side of the split 44 | # cdef double[::1] sum_right # Same as above, but for the right side of the split 45 | # 46 | # cdef double[:, ::1] grad 47 | # cdef double[::1] sum_tr 48 | # cdef double[::1] mean_tr 49 | # cdef double[::1] mean_sum 50 | # 51 | # cdef double[::1] rho 52 | # cdef double sum_rho 53 | # cdef double sum_rho_left 54 | # cdef double sum_rho_right 55 | # 56 | # cdef class TestMSE(CriterionEx): 57 | # cdef SIZE_t d_tr 58 | # 59 | # cdef double sq_sum_total 60 | # 61 | # cdef double[::1] sum_total # The sum of w*y. 62 | # cdef double[::1] sum_left # Same as above, but for the left side of the split 63 | # cdef double[::1] sum_right # Same as above, but for the right side of the split 64 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/_splitter.pxd: -------------------------------------------------------------------------------- 1 | 2 | from ylearn.sklearn_ex.cloned.tree._splitter cimport BestSplitter 3 | 4 | from ylearn.sklearn_ex.cloned.tree._tree cimport DOUBLE_t # Type of y, sample_weight 5 | from ylearn.sklearn_ex.cloned.tree._tree cimport SIZE_t # Type for indices and counters 6 | 7 | cdef class GrfTreeBestSplitter(BestSplitter): 8 | cdef const DOUBLE_t[:, ::1] treatment 9 | 10 | # new Methods 11 | cdef int init_ex(self, object X, const DOUBLE_t[:, ::1] y, const DOUBLE_t[:, ::1] treatment, 12 | DOUBLE_t* sample_weight) except -1 13 | 14 | # overridden 15 | cdef int node_reset(self, SIZE_t start, SIZE_t end, 16 | double* weighted_n_node_samples) nogil except -1 17 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/_splitter.pyx: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | cimport numpy as np 4 | from libc.stdio cimport printf 5 | 6 | 7 | np.import_array() 8 | 9 | from ylearn.sklearn_ex.cloned.tree._splitter cimport BestSplitter 10 | 11 | from ylearn.sklearn_ex.cloned.tree._tree cimport DOUBLE_t 12 | from ylearn.sklearn_ex.cloned.tree._tree cimport SIZE_t 13 | 14 | from ._criterion cimport CriterionEx 15 | 16 | cdef class GrfTreeBestSplitter(BestSplitter): 17 | cdef int init_ex(self, object X, const DOUBLE_t[:, ::1] y, const DOUBLE_t[:, ::1] treatment, 18 | DOUBLE_t* sample_weight) except -1: 19 | self.init(X,y,sample_weight) 20 | self.treatment = treatment 21 | 22 | cdef int node_reset(self, SIZE_t start, SIZE_t end, 23 | double* weighted_n_node_samples) nogil except -1: 24 | self.start = start 25 | self.end = end 26 | 27 | (self.criterion).init_ex(self.y, 28 | self.treatment, 29 | self.sample_weight, 30 | self.weighted_n_samples, 31 | self.samples, 32 | start, 33 | end) 34 | 35 | weighted_n_node_samples[0] = self.criterion.weighted_n_node_samples 36 | return 0 37 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/_tree.pxd: -------------------------------------------------------------------------------- 1 | # This is a fork from scikit-learn 2 | 3 | import numpy as np 4 | cimport numpy as np 5 | 6 | from ylearn.sklearn_ex.cloned.tree._tree cimport Tree 7 | from ylearn.sklearn_ex.cloned.tree._tree cimport BestFirstTreeBuilder 8 | from ._splitter cimport GrfTreeBestSplitter 9 | 10 | cdef class GrfTreeBestFirstBuilder(BestFirstTreeBuilder): 11 | cpdef build_ex(self, Tree tree, object X, np.ndarray y, np.ndarray treatment, 12 | np.ndarray sample_weight= *) 13 | 14 | cdef _init_splitter_ex(self, GrfTreeBestSplitter splitter, object X, np.ndarray y, np.ndarray treatment, 15 | np.ndarray sample_weight= *) 16 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/_tree.pyx: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | cimport numpy as np 3 | from libc.stdio cimport printf 4 | 5 | np.import_array() 6 | 7 | from ylearn.sklearn_ex.cloned.tree._tree cimport Tree 8 | from ylearn.sklearn_ex.cloned.tree._tree cimport BestFirstTreeBuilder 9 | 10 | from ylearn.sklearn_ex.cloned.tree._tree cimport DOUBLE_t 11 | 12 | from ._splitter cimport GrfTreeBestSplitter 13 | 14 | cdef class GrfTreeBestFirstBuilder(BestFirstTreeBuilder): 15 | cpdef build_ex(self, Tree tree, object X, np.ndarray y, np.ndarray treatment, 16 | np.ndarray sample_weight=None): 17 | """Build a decision tree from the training set (X, y, treatment).""" 18 | 19 | # check input 20 | X, y, sample_weight = self._check_input(X, y, sample_weight) 21 | 22 | # init splitter 23 | cdef GrfTreeBestSplitter splitter = self.splitter 24 | self._init_splitter_ex(splitter, X, y, treatment) 25 | # build tree 26 | self._build_tree(tree, splitter) 27 | 28 | cdef _init_splitter_ex(self, GrfTreeBestSplitter splitter, object X, np.ndarray y, np.ndarray treatment, 29 | np.ndarray sample_weight= None): 30 | cdef DOUBLE_t*sample_weight_ptr = NULL 31 | if sample_weight is not None: 32 | sample_weight_ptr = sample_weight.data 33 | 34 | # Recursive partition (without actual recursion) 35 | splitter.init_ex(X, y, treatment, sample_weight_ptr) 36 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/_util.pxd: -------------------------------------------------------------------------------- 1 | from ylearn.sklearn_ex.cloned.tree._tree cimport SIZE_t 2 | 3 | cdef int eigen_solve(int m, int n, int r, double* A, double *B, double *X) nogil except -1 4 | # cdef int eigen_solve_r(int m, int n, int r, double* A, double *B, double *X) nogil except -1 5 | cdef int eigen_pinv(int m, int n, const double *A, double *AI) nogil except -1 6 | # cdef int eigen_pinv_r(int m, int n, const double *A, double *AI) nogil except -1 7 | 8 | cdef int init_criterion( #/* input */ 9 | int d_y,int d_tr, int n_samples, 10 | const double *y, const double *tr, 11 | const double *sample_weight, const SIZE_t *samples, 12 | int start, int end, 13 | #/* output */ 14 | double *sum_total, double *mean_sum, 15 | double *sum_tr,double * mean_tr, 16 | double *rho, double *grad, 17 | double *weighted_n_node_samples, 18 | double *sum_rho 19 | ) nogil except -1 20 | 21 | # 22 | cpdef solve(double[:, ::1] A, double[:, ::1] B) 23 | 24 | cpdef pinv(double[:, ::1] A) 25 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_generalized_forest/tree/_util.pyx: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | cimport numpy as np 4 | 5 | from ylearn.sklearn_ex.cloned.tree._tree cimport DOUBLE_t 6 | from ylearn.sklearn_ex.cloned.tree._tree cimport SIZE_t 7 | 8 | cdef extern from "_util_lib.cpp": 9 | int eigen_solve(int m, int n, int r, double* A, double *B, double *X) nogil except -1 10 | int eigen_solve_r(int m, int n, int r, double* A, double *B, double *X) nogil except -1 11 | int eigen_pinv(int m, int n, const double *A, double *AI) nogil except -1 12 | int eigen_pinv_r(int m, int n, const double *A, double *AI) nogil except -1 13 | 14 | int init_criterion( #/* input */ 15 | int d_y,int d_tr, int n_samples, 16 | const double *y, const double *tr, 17 | const double *sample_weight, const SIZE_t *samples, 18 | int start, int end, 19 | #/* output */ 20 | double *sum_total, double *mean_sum, 21 | double *sum_tr,double * mean_tr, 22 | double *rho, double *grad, 23 | double *weighted_n_node_samples, 24 | double *sum_rho 25 | ) nogil except -1 26 | 27 | 28 | cpdef solve(double[:, ::1] A, double[:, ::1] B): 29 | assert A.shape[0] == B.shape[0] 30 | 31 | cdef int m=A.shape[0] 32 | cdef int n=A.shape[1] 33 | cdef int r=B.shape[1] 34 | cdef np.ndarray[DOUBLE_t, ndim=2] X = np.zeros([n, r], dtype=np.float64) 35 | 36 | cdef int code= eigen_solve_r(m,n,r,&A[0,0],&B[0,0],&X[0,0]) 37 | if code==0: 38 | return X 39 | else: 40 | raise ValueError(f'Failed to solve, code={code}') 41 | 42 | cpdef pinv(double[:, ::1] A): 43 | cdef int m=A.shape[0] 44 | cdef int n=A.shape[1] 45 | 46 | cdef np.ndarray[DOUBLE_t, ndim=2] AI = np.zeros([n,m], dtype=np.float64) 47 | cdef int code = eigen_pinv_r(m,n,&A[0,0], &AI[0,0]) 48 | 49 | if code==0: 50 | return AI 51 | else: 52 | raise ValueError(f'Failed to pinv, code={code}') 53 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_naive_forest/__init__.py: -------------------------------------------------------------------------------- 1 | from ._grf import NaiveGrf 2 | from ._grf_tree import _GrfTree 3 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_naive_forest/_grf.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import numpy as np 3 | 4 | from ._grf_tree import _GrfTree 5 | from .._generalized_forest import BaseCausalForest 6 | from .utils import inverse_grad 7 | from joblib import Parallel, delayed 8 | 9 | 10 | def _prediction(predict, w, v, v_train, lock, i): 11 | pred = predict(w, v, return_node=False).reshape(-1, 1) 12 | y_pred = predict(w, v_train, return_node=True) 13 | y_test_pred, y_test_pred_num = [], [] 14 | with lock: 15 | for p in y_pred: 16 | y_test_pred.append(p.value) 17 | y_test_pred_num.append(p.sample_num) 18 | 19 | return (y_test_pred == pred) / y_test_pred_num 20 | 21 | 22 | class NaiveGrf(BaseCausalForest): 23 | """Avoid using this class to estimate causal effect when assuming discrete treatment.""" 24 | 25 | def __init__( 26 | self, 27 | n_estimators=100, 28 | *, 29 | sub_sample_num=None, 30 | max_depth=None, 31 | min_split_tolerance=1e-5, 32 | n_jobs=None, 33 | random_state=None, 34 | categories="auto", 35 | ): 36 | base_estimator = _GrfTree() 37 | estimator_params = ("max_depth", "min_split_tolerance") 38 | self.min_split_tolerance = min_split_tolerance 39 | 40 | super().__init__( 41 | base_estimator=base_estimator, 42 | estimator_params=estimator_params, 43 | n_estimators=n_estimators, 44 | max_depth=max_depth, 45 | n_jobs=n_jobs, 46 | sub_sample_num=sub_sample_num, 47 | categories=categories, 48 | random_state=random_state, 49 | ) 50 | 51 | def fit(self, data, outcome, treatment, adjustment=None, covariate=None, **kwargs): 52 | super().fit( 53 | data, 54 | outcome, 55 | treatment, 56 | adjustment=adjustment, 57 | covariate=covariate, 58 | sample_weight=None, 59 | ) 60 | 61 | def _compute_alpha(self, v): 62 | # first implement a version which only take one example as its input 63 | lock = threading.Lock() 64 | w = v.copy() 65 | if self.n_outputs_ > 1: 66 | raise ValueError( 67 | "Currently do not support the number of output which is larger than 1" 68 | ) 69 | else: 70 | alpha = np.zeros((v.shape[0], self._v.shape[0])) 71 | 72 | alpha_collection = Parallel(n_jobs=self.n_jobs, verbose=self.verbose,)( 73 | delayed(_prediction)(e._predict_with_array, w, v, self._v[s], lock, i) 74 | for i, (e, s) in enumerate(zip(self.estimators_, self.sub_sample_idx)) 75 | ) 76 | for alpha_, s in zip(alpha_collection, self.sub_sample_idx): 77 | alpha[:, s] += alpha_ 78 | return alpha / self.n_estimators 79 | 80 | def _compute_aug(self, y, x, alpha): 81 | r"""Formula: 82 | We first need to repeat vectors in training set the number of the #test set times to enable 83 | tensor calculation. 84 | 85 | The first half is 86 | g_n^j_k = \sum_i \alpha_n^i (x_n^i_j - \sum_i \alpha_n^i x_n^i_j)(x_n^i_j - \sum_i \alpha_n^i x_n^i_j)^T 87 | while the second half is 88 | \theta_n^j = \sum_i \alpha_n^i (x_n^i_j - \sum_i \alpha_n^i x_n^i_j)(y_n^i - \sum_i \alpha_n^i y_n^i) 89 | which are then combined to give 90 | g_n^j_k \theta_{nj} 91 | 92 | Parameters 93 | ---------- 94 | y : ndarray 95 | outcome vector of the training set, shape (i,) 96 | x : ndarray 97 | treatment vector of the training set, shape(i, j) 98 | alpha : ndarray 99 | The computed alpha, shape (n, i) where i is the number of training set 100 | 101 | Returns 102 | ------- 103 | ndarray, ndarray 104 | the first is inv_grad while the second is theta_ 105 | """ 106 | n_test, n_train = alpha.shape 107 | x = np.tile(x.reshape((1,) + x.shape), (n_test, 1, 1)) 108 | x_dif = x - (alpha.reshape(n_test, -1, 1) * x).sum(axis=1).reshape( 109 | n_test, 1, -1 110 | ) 111 | grad_ = alpha.reshape(n_test, n_train, 1, 1) * np.einsum( 112 | "nij,nik->nijk", x_dif, x_dif 113 | ) 114 | grad_ = grad_.sum(1) 115 | inv_grad = inverse_grad(grad_) 116 | 117 | y = np.tile(y.reshape(1, -1), (n_test, 1)) 118 | y_dif = y - alpha * y 119 | theta_ = ((alpha * y_dif).reshape(n_test, n_train, 1) * x_dif).sum(1) 120 | 121 | return inv_grad, theta_ 122 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_naive_forest/causal_forest.py: -------------------------------------------------------------------------------- 1 | from ._grf import NaiveGrf 2 | from ..double_ml import DoubleML 3 | 4 | 5 | class NaiveCausalForest(DoubleML): 6 | """A version of causal forest with pure python implementation, i.e., it is super slow. Thus 7 | this one is only for experimental use. 8 | """ 9 | 10 | print("Do not use currently.") 11 | 12 | def __init__( 13 | self, 14 | x_model, 15 | y_model, 16 | cf_fold=1, 17 | random_state=2022, 18 | is_discrete_treatment=False, 19 | categories="auto", 20 | *, 21 | n_estimators=100, 22 | sub_sample_num=None, 23 | max_depth=None, 24 | min_split_tolerance=1e-5, 25 | n_jobs=None, 26 | ): 27 | yx_model = NaiveGrf( 28 | n_estimators=n_estimators, 29 | sub_sample_num=sub_sample_num, 30 | max_depth=max_depth, 31 | min_split_tolerance=min_split_tolerance, 32 | n_jobs=n_jobs, 33 | random_state=random_state, 34 | ) 35 | super().__init__( 36 | x_model, 37 | y_model, 38 | yx_model, 39 | cf_fold, 40 | random_state, 41 | is_discrete_treatment, 42 | categories, 43 | ) 44 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_naive_forest/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from numpy.linalg import inv 4 | 5 | 6 | def grad(x_dif, n): 7 | return np.einsum("ni,nj->nij", x_dif, x_dif).sum(axis=0) / n 8 | 9 | 10 | def grad_coef(x_dif, y_dif, ls_coef): 11 | return x_dif * (y_dif - np.einsum("nj,j->n", x_dif, ls_coef)).reshape(-1, 1) 12 | 13 | 14 | def inverse_grad(grad, eps=1e-5): 15 | Id = np.eye(grad.shape[-1]) * eps 16 | if grad.ndim > 2: 17 | Id = np.tile(Id, grad.shape[:-2] + (1, 1)) 18 | grad += Id 19 | else: 20 | grad += Id 21 | return inv(grad) 22 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_tree/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_tree/tree_criterion.pxd: -------------------------------------------------------------------------------- 1 | 2 | from ylearn.sklearn_ex.cloned.tree._criterion cimport RegressionCriterion 3 | 4 | cdef class HonestCMSE(RegressionCriterion): 5 | cdef double yt_sq_sum_total 6 | cdef double y0_sq_sum_total 7 | 8 | cdef double yt_sum_total 9 | cdef double y0_sum_total 10 | 11 | cdef double yt_sq_sum_left 12 | cdef double y0_sq_sum_left 13 | cdef double yt_sq_sum_right 14 | cdef double y0_sq_sum_right 15 | 16 | cdef double yt_sum_left 17 | cdef double y0_sum_left 18 | cdef double yt_sum_right 19 | cdef double y0_sum_right 20 | 21 | cdef int nt_total 22 | cdef int n0_total 23 | 24 | cdef int nt_left 25 | cdef int n0_left 26 | 27 | cdef int nt_right 28 | cdef int n0_right 29 | -------------------------------------------------------------------------------- /ylearn/estimator_model/_tree/tree_criterion_lib.cpp: -------------------------------------------------------------------------------- 1 | 2 | int on_update( /* inputs */ 3 | const double* y, const long* samples, const double* sample_weight, 4 | int start, int end, int pos, int new_pos, 5 | /* outputs */ 6 | double *yt_sum_left_, double* y0_sum_left_, 7 | double *yt_sq_sum_left_, double* y0_sq_sum_left_, 8 | int *nt_left_, int *n0_left_ 9 | ) 10 | { 11 | double yt_sum_left=0.0, y0_sum_left=0.0; 12 | double yt_sq_sum_left=0.0, y0_sq_sum_left=0.0; 13 | int nt_left=0, n0_left=0; 14 | int p, i; 15 | double wi, yi; 16 | 17 | if( (new_pos - pos) <= (end - new_pos) ){ 18 | for(p=pos; p0.0 ){ 23 | yt_sum_left += yi; 24 | yt_sq_sum_left += yi*yi; 25 | nt_left++; 26 | } else { 27 | y0_sum_left += yi; 28 | y0_sq_sum_left += yi*yi; 29 | n0_left++; 30 | } 31 | } 32 | } else { 33 | for(p=end - 1; p>new_pos - 1; p--){ 34 | i = samples[p]; 35 | wi = sample_weight[i]; 36 | yi = y[i]; 37 | if( wi>0.0 ){ 38 | yt_sum_left -= yi; 39 | yt_sq_sum_left -= yi*yi; 40 | nt_left--; 41 | } else { 42 | y0_sum_left -= yi; 43 | y0_sq_sum_left -= yi*yi; 44 | n0_left--; 45 | } 46 | } 47 | } 48 | 49 | *yt_sum_left_ = yt_sum_left; 50 | *y0_sum_left_ = y0_sum_left; 51 | *yt_sq_sum_left_ = yt_sq_sum_left; 52 | *y0_sq_sum_left_ = y0_sq_sum_left; 53 | *nt_left_ = nt_left; 54 | *n0_left_ = n0_left; 55 | return 0; 56 | } -------------------------------------------------------------------------------- /ylearn/estimator_model/ensemble.py: -------------------------------------------------------------------------------- 1 | class EnsembleEstModels: 2 | def __init__(self) -> None: 3 | raise NotImplementedError 4 | -------------------------------------------------------------------------------- /ylearn/exp_dataset/__init__.py: -------------------------------------------------------------------------------- 1 | from . import exp_data -------------------------------------------------------------------------------- /ylearn/policy/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ylearn/policy/_tree/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ylearn/policy/_tree/tree_criterion.pxd: -------------------------------------------------------------------------------- 1 | from ylearn.sklearn_ex.cloned.tree._criterion cimport RegressionCriterion 2 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/__init__.py: -------------------------------------------------------------------------------- 1 | from ._data_cleaner import DataCleaner 2 | from ._dataframe_mapper import DataFrameMapper 3 | from ._transformer import ArrayOneHotEncoder, SafeOrdinalEncoder, FeatureImportancesSelectionTransformer 4 | from ._transformer import general_preprocessor, general_estimator 5 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/README.txt: -------------------------------------------------------------------------------- 1 | This directory is a fork from scikit-learn 1.1.1 2 | See: https://github.com/scikit-learn/scikit-learn 3 | 4 | We do some refactor to support causal forest: 5 | * cdef _spliter.BestSplitter in pxd 6 | * cdef _tree.BestFirstTreeBuilder in pxd 7 | * extract _tree.BestFirstTreeBuilder.build() into _init_splitter() and _build_tree() 8 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/scipy/README.txt: -------------------------------------------------------------------------------- 1 | This directory is a fork from scipy 0.13.3 2 | See: https://github.com/scipy/scipy 3 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/scipy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/scipy/_complexstuff.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | #include 6 | 7 | #ifdef __cplusplus 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/scipy/_complexstuff.pxd: -------------------------------------------------------------------------------- 1 | # -*-cython-*- 2 | # 3 | # Common functions required when doing complex arithmetic with Cython. 4 | # 5 | 6 | cimport numpy as np 7 | cimport libc.math 8 | 9 | cdef extern from "_complexstuff.h": 10 | double npy_cabs(np.npy_cdouble z) nogil 11 | np.npy_cdouble npy_clog(np.npy_cdouble z) nogil 12 | np.npy_cdouble npy_cexp(np.npy_cdouble z) nogil 13 | double npy_log1p(double x) nogil 14 | int npy_isnan(double x) nogil 15 | int npy_isinf(double x) nogil 16 | int npy_isfinite(double x) nogil 17 | double inf "NPY_INFINITY" 18 | double pi "NPY_PI" 19 | double nan "NPY_NAN" 20 | 21 | ctypedef double complex double_complex 22 | 23 | ctypedef fused number_t: 24 | double 25 | double_complex 26 | 27 | cdef inline bint zisnan(number_t x) nogil: 28 | if number_t is double_complex: 29 | return npy_isnan(x.real) or npy_isnan(x.imag) 30 | else: 31 | return npy_isnan(x) 32 | 33 | cdef inline bint zisfinite(number_t x) nogil: 34 | if number_t is double_complex: 35 | return npy_isfinite(x.real) and npy_isfinite(x.imag) 36 | else: 37 | return npy_isfinite(x) 38 | 39 | cdef inline bint zisinf(number_t x) nogil: 40 | if number_t is double_complex: 41 | return not zisnan(x) and not zisfinite(x) 42 | else: 43 | return npy_isinf(x) 44 | 45 | cdef inline double zabs(number_t x) nogil: 46 | if number_t is double_complex: 47 | return npy_cabs((&x)[0]) 48 | else: 49 | return libc.math.fabs(x) 50 | 51 | cdef inline number_t zlog(number_t x) nogil: 52 | cdef np.npy_cdouble r 53 | if number_t is double_complex: 54 | r = npy_clog((&x)[0]) 55 | return (&r)[0] 56 | else: 57 | return libc.math.log(x) 58 | 59 | cdef inline number_t zexp(number_t x) nogil: 60 | cdef np.npy_cdouble r 61 | if number_t is double_complex: 62 | r = npy_cexp((&x)[0]) 63 | return (&r)[0] 64 | else: 65 | return libc.math.exp(x) 66 | 67 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/scipy/_xlogy.pxd: -------------------------------------------------------------------------------- 1 | # -*-cython-*- 2 | 3 | from ._complexstuff cimport zlog, zisnan, number_t 4 | 5 | cdef inline number_t xlogy(number_t x, number_t y) nogil: 6 | if x == 0 and not zisnan(y): 7 | return 0 8 | else: 9 | return x * zlog(y) 10 | 11 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/tree/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/tree/_criterion.pxd: -------------------------------------------------------------------------------- 1 | # Authors: Gilles Louppe 2 | # Peter Prettenhofer 3 | # Brian Holt 4 | # Joel Nothman 5 | # Arnaud Joly 6 | # Jacob Schreiber 7 | # 8 | # License: BSD 3 clause 9 | 10 | # See _criterion.pyx for implementation details. 11 | 12 | import numpy as np 13 | cimport numpy as np 14 | 15 | from ._tree cimport DTYPE_t # Type of X 16 | from ._tree cimport DOUBLE_t # Type of y, sample_weight 17 | from ._tree cimport SIZE_t # Type for indices and counters 18 | from ._tree cimport INT32_t # Signed 32 bit integer 19 | from ._tree cimport UINT32_t # Unsigned 32 bit integer 20 | 21 | cdef class Criterion: 22 | # The criterion computes the impurity of a node and the reduction of 23 | # impurity of a split on that node. It also computes the output statistics 24 | # such as the mean in regression and class probabilities in classification. 25 | 26 | # Internal structures 27 | cdef const DOUBLE_t[:, ::1] y # Values of y 28 | cdef DOUBLE_t* sample_weight # Sample weights 29 | 30 | cdef SIZE_t* samples # Sample indices in X, y 31 | cdef SIZE_t start # samples[start:pos] are the samples in the left node 32 | cdef SIZE_t pos # samples[pos:end] are the samples in the right node 33 | cdef SIZE_t end 34 | 35 | cdef SIZE_t n_outputs # Number of outputs 36 | cdef SIZE_t n_samples # Number of samples 37 | cdef SIZE_t n_node_samples # Number of samples in the node (end-start) 38 | cdef double weighted_n_samples # Weighted number of samples (in total) 39 | cdef double weighted_n_node_samples # Weighted number of samples in the node 40 | cdef double weighted_n_left # Weighted number of samples in the left node 41 | cdef double weighted_n_right # Weighted number of samples in the right node 42 | 43 | # The criterion object is maintained such that left and right collected 44 | # statistics correspond to samples[start:pos] and samples[pos:end]. 45 | 46 | # Methods 47 | cdef int init(self, const DOUBLE_t[:, ::1] y, DOUBLE_t* sample_weight, 48 | double weighted_n_samples, SIZE_t* samples, SIZE_t start, 49 | SIZE_t end) nogil except -1 50 | cdef int reset(self) nogil except -1 51 | cdef int reverse_reset(self) nogil except -1 52 | cdef int update(self, SIZE_t new_pos) nogil except -1 53 | cdef double node_impurity(self) nogil 54 | cdef void children_impurity(self, double* impurity_left, 55 | double* impurity_right) nogil 56 | cdef void node_value(self, double* dest) nogil 57 | cdef double impurity_improvement(self, double impurity_parent, 58 | double impurity_left, 59 | double impurity_right) nogil 60 | cdef double proxy_impurity_improvement(self) nogil 61 | 62 | cdef class ClassificationCriterion(Criterion): 63 | """Abstract criterion for classification.""" 64 | 65 | cdef SIZE_t[::1] n_classes 66 | cdef SIZE_t max_n_classes 67 | 68 | cdef double[:, ::1] sum_total # The sum of the weighted count of each label. 69 | cdef double[:, ::1] sum_left # Same as above, but for the left side of the split 70 | cdef double[:, ::1] sum_right # Same as above, but for the right side of the split 71 | 72 | cdef class RegressionCriterion(Criterion): 73 | """Abstract regression criterion.""" 74 | 75 | cdef double sq_sum_total 76 | 77 | cdef double[::1] sum_total # The sum of w*y. 78 | cdef double[::1] sum_left # Same as above, but for the left side of the split 79 | cdef double[::1] sum_right # Same as above, but for the right side of the split 80 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/tree/_utils.pxd: -------------------------------------------------------------------------------- 1 | # Authors: Gilles Louppe 2 | # Peter Prettenhofer 3 | # Arnaud Joly 4 | # Jacob Schreiber 5 | # Nelson Liu 6 | # 7 | # License: BSD 3 clause 8 | 9 | # See _utils.pyx for details. 10 | 11 | import numpy as np 12 | cimport numpy as np 13 | from ._tree cimport Node 14 | ###removed 15 | ### from ..neighbors._quad_tree cimport Cell 16 | 17 | ctypedef np.npy_float32 DTYPE_t # Type of X 18 | ctypedef np.npy_float64 DOUBLE_t # Type of y, sample_weight 19 | ctypedef np.npy_intp SIZE_t # Type for indices and counters 20 | ctypedef np.npy_int32 INT32_t # Signed 32 bit integer 21 | ctypedef np.npy_uint32 UINT32_t # Unsigned 32 bit integer 22 | 23 | 24 | cdef enum: 25 | # Max value for our rand_r replacement (near the bottom). 26 | # We don't use RAND_MAX because it's different across platforms and 27 | # particularly tiny on Windows/MSVC. 28 | RAND_R_MAX = 0x7FFFFFFF 29 | 30 | 31 | # safe_realloc(&p, n) resizes the allocation of p to n * sizeof(*p) bytes or 32 | # raises a MemoryError. It never calls free, since that's __dealloc__'s job. 33 | # cdef DTYPE_t *p = NULL 34 | # safe_realloc(&p, n) 35 | # is equivalent to p = malloc(n * sizeof(*p)) with error checking. 36 | ctypedef fused realloc_ptr: 37 | # Add pointer types here as needed. 38 | (DTYPE_t*) 39 | (SIZE_t*) 40 | (unsigned char*) 41 | (WeightedPQueueRecord*) 42 | (DOUBLE_t*) 43 | (DOUBLE_t**) 44 | (Node*) 45 | ###removed 46 | ### (Cell*) 47 | (Node**) 48 | 49 | cdef realloc_ptr safe_realloc(realloc_ptr* p, size_t nelems) nogil except * 50 | 51 | 52 | cdef np.ndarray sizet_ptr_to_ndarray(SIZE_t* data, SIZE_t size) 53 | 54 | 55 | cdef SIZE_t rand_int(SIZE_t low, SIZE_t high, 56 | UINT32_t* random_state) nogil 57 | 58 | 59 | cdef double rand_uniform(double low, double high, 60 | UINT32_t* random_state) nogil 61 | 62 | 63 | cdef double log(double x) nogil 64 | 65 | # ============================================================================= 66 | # WeightedPQueue data structure 67 | # ============================================================================= 68 | 69 | # A record stored in the WeightedPQueue 70 | cdef struct WeightedPQueueRecord: 71 | DOUBLE_t data 72 | DOUBLE_t weight 73 | 74 | cdef class WeightedPQueue: 75 | cdef SIZE_t capacity 76 | cdef SIZE_t array_ptr 77 | cdef WeightedPQueueRecord* array_ 78 | 79 | cdef bint is_empty(self) nogil 80 | cdef int reset(self) nogil except -1 81 | cdef SIZE_t size(self) nogil 82 | cdef int push(self, DOUBLE_t data, DOUBLE_t weight) nogil except -1 83 | cdef int remove(self, DOUBLE_t data, DOUBLE_t weight) nogil 84 | cdef int pop(self, DOUBLE_t* data, DOUBLE_t* weight) nogil 85 | cdef int peek(self, DOUBLE_t* data, DOUBLE_t* weight) nogil 86 | cdef DOUBLE_t get_weight_from_index(self, SIZE_t index) nogil 87 | cdef DOUBLE_t get_value_from_index(self, SIZE_t index) nogil 88 | 89 | 90 | # ============================================================================= 91 | # WeightedMedianCalculator data structure 92 | # ============================================================================= 93 | 94 | cdef class WeightedMedianCalculator: 95 | cdef SIZE_t initial_capacity 96 | cdef WeightedPQueue samples 97 | cdef DOUBLE_t total_weight 98 | cdef SIZE_t k 99 | cdef DOUBLE_t sum_w_0_k # represents sum(weights[0:k]) 100 | # = w[0] + w[1] + ... + w[k-1] 101 | 102 | cdef SIZE_t size(self) nogil 103 | cdef int push(self, DOUBLE_t data, DOUBLE_t weight) nogil except -1 104 | cdef int reset(self) nogil except -1 105 | cdef int update_median_parameters_post_push( 106 | self, DOUBLE_t data, DOUBLE_t weight, 107 | DOUBLE_t original_median) nogil 108 | cdef int remove(self, DOUBLE_t data, DOUBLE_t weight) nogil 109 | cdef int pop(self, DOUBLE_t* data, DOUBLE_t* weight) nogil 110 | cdef int update_median_parameters_post_remove( 111 | self, DOUBLE_t data, DOUBLE_t weight, 112 | DOUBLE_t original_median) nogil 113 | cdef DOUBLE_t get_median(self) nogil 114 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .validation import check_random_state -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/utils/_random.pxd: -------------------------------------------------------------------------------- 1 | # Authors: Arnaud Joly 2 | # 3 | # License: BSD 3 clause 4 | 5 | 6 | import numpy as np 7 | cimport numpy as np 8 | ctypedef np.npy_uint32 UINT32_t 9 | 10 | cdef inline UINT32_t DEFAULT_SEED = 1 11 | 12 | cdef enum: 13 | # Max value for our rand_r replacement (near the bottom). 14 | # We don't use RAND_MAX because it's different across platforms and 15 | # particularly tiny on Windows/MSVC. 16 | RAND_R_MAX = 0x7FFFFFFF 17 | 18 | cpdef sample_without_replacement(np.int_t n_population, 19 | np.int_t n_samples, 20 | method=*, 21 | random_state=*) 22 | 23 | # rand_r replacement using a 32bit XorShift generator 24 | # See http://www.jstatsoft.org/v08/i14/paper for details 25 | cdef inline UINT32_t our_rand_r(UINT32_t* seed) nogil: 26 | """Generate a pseudo-random np.uint32 from a np.uint32 seed""" 27 | # seed shouldn't ever be 0. 28 | if (seed[0] == 0): seed[0] = DEFAULT_SEED 29 | 30 | seed[0] ^= (seed[0] << 13) 31 | seed[0] ^= (seed[0] >> 17) 32 | seed[0] ^= (seed[0] << 5) 33 | 34 | # Note: we must be careful with the final line cast to np.uint32 so that 35 | # the function behaves consistently across platforms. 36 | # 37 | # The following cast might yield different results on different platforms: 38 | # wrong_cast = RAND_R_MAX + 1 39 | # 40 | # We can use: 41 | # good_cast = (RAND_R_MAX + 1) 42 | # or: 43 | # cdef np.uint32_t another_good_cast = RAND_R_MAX + 1 44 | return seed[0] % (RAND_R_MAX + 1) 45 | -------------------------------------------------------------------------------- /ylearn/sklearn_ex/cloned/utils/validation.py: -------------------------------------------------------------------------------- 1 | """Utilities for input validation""" 2 | 3 | # Authors: Olivier Grisel 4 | # Gael Varoquaux 5 | # Andreas Mueller 6 | # Lars Buitinck 7 | # Alexandre Gramfort 8 | # Nicolas Tresegnie 9 | # Sylvain Marie 10 | # License: BSD 3 clause 11 | 12 | from functools import wraps 13 | import warnings 14 | import numbers 15 | import operator 16 | 17 | import numpy as np 18 | 19 | 20 | def check_random_state(seed): 21 | """Turn seed into a np.random.RandomState instance. 22 | 23 | Parameters 24 | ---------- 25 | seed : None, int or instance of RandomState 26 | If seed is None, return the RandomState singleton used by np.random. 27 | If seed is an int, return a new RandomState instance seeded with seed. 28 | If seed is already a RandomState instance, return it. 29 | Otherwise raise ValueError. 30 | 31 | Returns 32 | ------- 33 | :class:`numpy:numpy.random.RandomState` 34 | The random state object based on `seed` parameter. 35 | """ 36 | if seed is None or seed is np.random: 37 | return np.random.mtrand._rand 38 | if isinstance(seed, numbers.Integral): 39 | return np.random.RandomState(seed) 40 | if isinstance(seed, np.random.RandomState): 41 | return seed 42 | raise ValueError( 43 | "%r cannot be used to seed a numpy.random.RandomState instance" % seed 44 | ) 45 | 46 | -------------------------------------------------------------------------------- /ylearn/stats_inference/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ylearn/uplift/__init__.py: -------------------------------------------------------------------------------- 1 | from ._metric import get_gain, get_qini, get_cumlift 2 | from ._metric import qini_top_point, gain_top_point, auuc_score, qini_score 3 | from ._model import UpliftModel 4 | from ._plot import plot_gain, plot_qini, plot_cumlift 5 | -------------------------------------------------------------------------------- /ylearn/uplift/_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | from ylearn.utils import check_fitted as _check_fitted 5 | from ._metric import get_gain, get_qini, get_cumlift, auuc_score, qini_score 6 | from ._plot import plot_gain, plot_qini, plot_cumlift 7 | 8 | 9 | class UpliftModel(object): 10 | def __init__(self): 11 | # fitted 12 | self.lift_ = None 13 | self.cumlift_ = None 14 | self.gain_ = None 15 | self.gain_top_point_ = None 16 | self.qini_ = None 17 | self.qini_top_point_ = None 18 | self.auuc_score_ = None 19 | self.qini_score_ = None 20 | self.random_ = None 21 | 22 | def fit(self, df_lift, outcome='y', treatment='x', true_effect=None, treat=1, control=0, random='RANDOM'): 23 | assert isinstance(df_lift, pd.DataFrame) 24 | 25 | # self.df_lift_ = df_lift 26 | 27 | self.cumlift_ = get_cumlift( 28 | df_lift, outcome=outcome, treatment=treatment, true_effect=true_effect, 29 | treat=treat, control=control, random_name=random) 30 | self.gain_, self.gain_top_point_ = get_gain( 31 | df_lift, outcome=outcome, treatment=treatment, true_effect=true_effect, 32 | treat=treat, control=control, random_name=random, return_top_point=True, 33 | normalize=False) 34 | self.qini_, self.qini_top_point_ = get_qini( 35 | df_lift, outcome=outcome, treatment=treatment, true_effect=true_effect, 36 | treat=treat, control=control, random_name=random, return_top_point=True, 37 | normalize=False) 38 | self.auuc_score_ = auuc_score( 39 | df_lift, outcome=outcome, treatment=treatment, true_effect=true_effect, 40 | treat=treat, control=control, random_name=random, 41 | normalize=True 42 | ) 43 | self.qini_score_ = qini_score( 44 | df_lift, outcome=outcome, treatment=treatment, true_effect=true_effect, 45 | treat=treat, control=control, random_name=random, 46 | normalize=True 47 | ) 48 | self.random_ = random 49 | self.lift_ = df_lift.copy() 50 | self._is_fitted = True 51 | 52 | return self 53 | 54 | @_check_fitted 55 | def get_cumlift(self): 56 | return self.cumlift_.copy() 57 | 58 | @_check_fitted 59 | def get_gain(self, normalize=False): 60 | gain = self.gain_ 61 | if normalize: 62 | gain = gain.div(np.abs(gain.iloc[-1, :]), axis=1) 63 | 64 | return gain.copy() 65 | 66 | @_check_fitted 67 | def get_qini(self, normalize=False): 68 | qini = self.qini_ 69 | if normalize: 70 | qini = qini.div(np.abs(qini.iloc[-1, :]), axis=1) 71 | 72 | return qini.copy() 73 | 74 | def _get_value_or_series(self, s, name=None): 75 | assert isinstance(s, pd.Series) 76 | if name is None: 77 | if self.random_ is not None: 78 | return s[s.index != self.random_].copy() 79 | else: 80 | return s.copy() 81 | else: 82 | return s[name] 83 | 84 | @_check_fitted 85 | def gain_top_point(self, name=None): 86 | return self._get_value_or_series(self.gain_top_point_, name) 87 | 88 | @_check_fitted 89 | def qini_top_point(self, name=None): 90 | return self._get_value_or_series(self.qini_top_point_, name) 91 | 92 | @_check_fitted 93 | def auuc_score(self, name=None): 94 | return self._get_value_or_series(self.auuc_score_, name) 95 | 96 | @_check_fitted 97 | def qini_score(self, name=None): 98 | return self._get_value_or_series(self.qini_score_, name) 99 | 100 | @_check_fitted 101 | def plot_qini(self, n_sample=100, normalize=False, **kwargs): 102 | plot_qini(self.get_qini(normalize=normalize), n_sample=n_sample, **kwargs) 103 | 104 | @_check_fitted 105 | def plot_gain(self, n_sample=100, normalize=False, **kwargs): 106 | plot_gain(self.get_gain(normalize=normalize), n_sample=n_sample, **kwargs) 107 | 108 | @_check_fitted 109 | def plot_cumlift(self, n_bins=10, **kwargs): 110 | plot_cumlift(self.get_cumlift(), n_bins=n_bins, **kwargs) 111 | -------------------------------------------------------------------------------- /ylearn/uplift/_plot.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | 5 | def _sample(df, n_sample, reset_index=True): 6 | assert isinstance(df, pd.DataFrame) and len(df) > n_sample 7 | 8 | idx = np.linspace(0, len(df) - 1, n_sample, endpoint=True, dtype='int') 9 | result = df.iloc[idx] 10 | if reset_index: 11 | # result = result.reset_index(drop=True) 12 | result.index = idx 13 | return result 14 | 15 | 16 | def _split(df_cumlift, n_bins): 17 | dfs = [] 18 | for x in df_cumlift.columns.tolist(): 19 | # if x == 'RANDOM': 20 | # continue # ignore it 21 | df = df_cumlift[[x]].copy() 22 | df['_k_'] = pd.qcut(df[x], n_bins, labels=np.arange(0, n_bins, 1)) 23 | df = df.groupby(by='_k_')[x].mean().sort_index(ascending=False) 24 | df.index = pd.RangeIndex(0, n_bins) # np.arange(0, bins, 1) 25 | df.name = x 26 | dfs.append(df) 27 | 28 | result = pd.concat(dfs, axis=1) if len(dfs) > 1 else dfs[0] 29 | return result 30 | 31 | 32 | def plot_cumlift(*cumlift, n_bins=10, **kwargs): 33 | assert all(isinstance(e, pd.DataFrame) or hasattr(e, 'get_cumlift') for e in cumlift) 34 | 35 | dfs = [e if isinstance(e, pd.DataFrame) else e.get_cumlift() for e in cumlift] 36 | dfs = [_split(df, n_bins) for df in dfs] 37 | df = pd.concat(dfs, axis=1) if len(dfs) > 1 else dfs[0] 38 | 39 | options = dict(rot=0, ylabel='cumlift') 40 | options.update(kwargs) 41 | df.plot.bar(**options) 42 | 43 | 44 | def plot_gain(*gain, n_sample=100, normalize=False, **kwargs): 45 | assert all(isinstance(e, pd.DataFrame) or hasattr(e, 'get_gain') for e in gain) 46 | 47 | dfs = [e if isinstance(e, pd.DataFrame) else e.get_gain(normalize=normalize) for e in gain] 48 | dfs = [_sample(df, n_sample) for df in dfs] 49 | df = pd.concat(dfs, axis=1) if len(dfs) > 1 else dfs[0] 50 | 51 | options = dict(ylabel='Gain', xlabel='Population', grid=True) 52 | options.update(kwargs) 53 | df.plot(**options) 54 | 55 | 56 | def plot_qini(*qini, n_sample=100, normalize=False, **kwargs): 57 | assert all(isinstance(e, pd.DataFrame) or hasattr(e, 'get_qini') for e in qini) 58 | 59 | dfs = [e if isinstance(e, pd.DataFrame) else e.get_qini(normalize=normalize) for e in qini] 60 | dfs = [_sample(df, n_sample) for df in dfs] 61 | df = pd.concat(dfs, axis=1) if len(dfs) > 1 else dfs[0] 62 | 63 | options = dict(ylabel='Qini', xlabel='Population', grid=True) 64 | options.update(kwargs) 65 | df.plot(**options) 66 | -------------------------------------------------------------------------------- /ylearn/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import sys as sys_ 2 | 3 | is_os_windows = sys_.platform.find('win') == 0 4 | is_os_darwin = sys_.platform.find('darwin') == 0 5 | is_os_linux = sys_.platform.find('linux') == 0 6 | 7 | try: 8 | from packaging.version import Version 9 | except ModuleNotFoundError: 10 | from distutils.version import LooseVersion as Version 11 | 12 | from ._common import const, set_random_state, infer_task_type, unique, to_repr 13 | from ._common import to_df, context, is_notebook, view_pydot 14 | from ._common import to_list, join_list, nmap 15 | from ._common import to_snake_case, to_camel_case, drop_none 16 | from ._common import check_fitted, check_fitted_ 17 | from ._tic_tok import tit, tic_toc, report_as_dataframe as tit_report 18 | from .metrics import calc_score 19 | -------------------------------------------------------------------------------- /ylearn/utils/metrics.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from collections import OrderedDict 3 | 4 | import numpy as np 5 | from sklearn import metrics as sk_metrics 6 | 7 | from ._common import const, infer_task_type, unique 8 | 9 | 10 | def _task_to_average(task): 11 | if task == const.TASK_MULTICLASS: 12 | average = 'macro' 13 | else: 14 | average = 'binary' 15 | return average 16 | 17 | 18 | def calc_score(y_true, y_preds, y_proba=None, *, 19 | metrics=None, task=None, pos_label=None, classes=None, average=None): 20 | if y_proba is None: 21 | y_proba = y_preds 22 | if len(y_proba.shape) == 2 and y_proba.shape[-1] == 1: 23 | y_proba = y_proba.reshape(-1) 24 | if len(y_preds.shape) == 2 and y_preds.shape[-1] == 1: 25 | y_preds = y_preds.reshape(-1) 26 | 27 | if task is None: 28 | task, _ = infer_task_type(y_true) 29 | 30 | if metrics is None: 31 | if task == const.TASK_REGRESSION: 32 | metrics = ['mae', 'mse', 'rmse'] 33 | else: 34 | metrics = ['accuracy','precision', 'recall', 'f1'] 35 | 36 | if task == const.TASK_BINARY and pos_label is None: 37 | if classes is None: 38 | classes = list(sorted(unique(y_true))) 39 | assert len(classes) == 2, 'classes of binary task should have two elements.' 40 | pos_label = classes[-1] 41 | 42 | if average is None: 43 | average = _task_to_average(task) 44 | 45 | recall_options = dict(average=average, labels=classes) 46 | if pos_label is not None: 47 | recall_options['pos_label'] = pos_label 48 | 49 | score = OrderedDict() 50 | for metric in metrics: 51 | if callable(metric): 52 | score[metric.__name__] = metric(y_true, y_preds) 53 | else: 54 | metric_lower = metric.lower() 55 | if metric_lower == 'auc': 56 | if len(y_proba.shape) == 2: 57 | if task == const.TASK_MULTICLASS: 58 | score[metric] = sk_metrics.roc_auc_score(y_true, y_proba, multi_class='ovo', labels=classes) 59 | else: 60 | score[metric] = sk_metrics.roc_auc_score(y_true, y_proba[:, 1]) 61 | else: 62 | score[metric] = sk_metrics.roc_auc_score(y_true, y_proba) 63 | elif metric_lower == 'accuracy': 64 | if y_preds is None: 65 | score[metric] = 0 66 | else: 67 | score[metric] = sk_metrics.accuracy_score(y_true, y_preds) 68 | elif metric_lower == 'recall': 69 | score[metric] = sk_metrics.recall_score(y_true, y_preds, **recall_options) 70 | elif metric_lower == 'precision': 71 | score[metric] = sk_metrics.precision_score(y_true, y_preds, **recall_options) 72 | elif metric_lower == 'f1': 73 | score[metric] = sk_metrics.f1_score(y_true, y_preds, **recall_options) 74 | elif metric_lower == 'mse': 75 | score[metric] = sk_metrics.mean_squared_error(y_true, y_preds) 76 | elif metric_lower == 'mae': 77 | score[metric] = sk_metrics.mean_absolute_error(y_true, y_preds) 78 | elif metric_lower == 'msle': 79 | score[metric] = sk_metrics.mean_squared_log_error(y_true, y_preds) 80 | elif metric_lower in {'rmse', 'rootmeansquarederror', 'root_mean_squared_error'}: 81 | score[metric] = np.sqrt(sk_metrics.mean_squared_error(y_true, y_preds)) 82 | elif metric_lower == 'r2': 83 | score[metric] = sk_metrics.r2_score(y_true, y_preds) 84 | elif metric_lower in {'logloss', 'log_loss'}: 85 | score[metric] = sk_metrics.log_loss(y_true, y_proba, labels=classes) 86 | 87 | return score 88 | --------------------------------------------------------------------------------