├── .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 |
--------------------------------------------------------------------------------