ergodic 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ergodic/__init__.py +216 -0
- ergodic/data/__init__.py +51 -0
- ergodic/data/_dataset.py +223 -0
- ergodic/data/_eventlog.py +126 -0
- ergodic/data/_frame.py +118 -0
- ergodic/data/_hierarchy.py +126 -0
- ergodic/data/_hts.py +118 -0
- ergodic/data/_panel.py +113 -0
- ergodic/data/_schema.py +311 -0
- ergodic/data/_tabular.py +60 -0
- ergodic/data/_timeseries.py +79 -0
- ergodic/datasets/__init__.py +35 -0
- ergodic/datasets/_bayesnet.py +210 -0
- ergodic/datasets/_bunch.py +43 -0
- ergodic/datasets/_bundled.py +263 -0
- ergodic/datasets/_tables.py +75 -0
- ergodic/datasets/data/birthweight.csv +4643 -0
- ergodic/datasets/data/germany.csv +749 -0
- ergodic/datasets/data/pension401k.csv +9916 -0
- ergodic/datasets/data/smoking.csv +1210 -0
- ergodic/discovery/__init__.py +94 -0
- ergodic/discovery/_bootstrap.py +336 -0
- ergodic/discovery/_ci.py +243 -0
- ergodic/discovery/_discover.py +70 -0
- ergodic/discovery/_encode.py +56 -0
- ergodic/discovery/_granger.py +108 -0
- ergodic/discovery/_lagged.py +172 -0
- ergodic/discovery/_registry.py +199 -0
- ergodic/discovery/_result.py +48 -0
- ergodic/discovery/_score.py +214 -0
- ergodic/discovery/_stats.py +71 -0
- ergodic/discovery/_tsresult.py +68 -0
- ergodic/discovery/algorithms/__init__.py +32 -0
- ergodic/discovery/algorithms/_astar.py +269 -0
- ergodic/discovery/algorithms/_boss.py +339 -0
- ergodic/discovery/algorithms/_fci.py +275 -0
- ergodic/discovery/algorithms/_ges.py +353 -0
- ergodic/discovery/algorithms/_glasso.py +521 -0
- ergodic/discovery/algorithms/_grasp.py +222 -0
- ergodic/discovery/algorithms/_hc.py +284 -0
- ergodic/discovery/algorithms/_lingam.py +361 -0
- ergodic/discovery/algorithms/_mmpc.py +271 -0
- ergodic/discovery/algorithms/_pc.py +287 -0
- ergodic/discovery/algorithms/_pcmci.py +546 -0
- ergodic/discovery/algorithms/_rfci.py +461 -0
- ergodic/discovery/algorithms/_skeleton.py +86 -0
- ergodic/discovery/ci/__init__.py +11 -0
- ergodic/discovery/ci/_discrete.py +68 -0
- ergodic/discovery/ci/_gcm.py +110 -0
- ergodic/discovery/ci/_linear.py +73 -0
- ergodic/discovery/ci/_mixed.py +64 -0
- ergodic/discovery/scores/__init__.py +11 -0
- ergodic/discovery/scores/_bdeu.py +95 -0
- ergodic/discovery/scores/_bic.py +146 -0
- ergodic/discovery/scores/_discrete_bic.py +85 -0
- ergodic/discovery/scores/_mixed_bic.py +111 -0
- ergodic/forecast/__init__.py +220 -0
- ergodic/forecast/_attribution.py +678 -0
- ergodic/forecast/_backtest.py +432 -0
- ergodic/forecast/_baselines.py +237 -0
- ergodic/forecast/_facade.py +734 -0
- ergodic/forecast/_fit.py +1057 -0
- ergodic/forecast/_mechanism.py +411 -0
- ergodic/forecast/_metrics.py +335 -0
- ergodic/forecast/_protocol.py +31 -0
- ergodic/forecast/_reconcile.py +634 -0
- ergodic/forecast/_result.py +347 -0
- ergodic/forecast/_scalers.py +140 -0
- ergodic/forecast/_schedule.py +449 -0
- ergodic/forecast/_staged.py +774 -0
- ergodic/forecast/_structure.py +129 -0
- ergodic/forecast/mechanisms/__init__.py +26 -0
- ergodic/forecast/mechanisms/_bass.py +253 -0
- ergodic/forecast/mechanisms/_ou.py +346 -0
- ergodic/forecast/mechanisms/_structural.py +360 -0
- ergodic/graph/__init__.py +77 -0
- ergodic/graph/_graph.py +598 -0
- ergodic/graph/_io.py +141 -0
- ergodic/graph/_marks.py +28 -0
- ergodic/graph/_meek.py +231 -0
- ergodic/graph/_pag.py +728 -0
- ergodic/graph/_primitives.py +184 -0
- ergodic/graph/_separation.py +231 -0
- ergodic/graph/_surgery.py +80 -0
- ergodic/identification/__init__.py +61 -0
- ergodic/identification/_criteria.py +573 -0
- ergodic/identification/_distribution.py +133 -0
- ergodic/identification/_evaluate.py +172 -0
- ergodic/identification/_functional.py +185 -0
- ergodic/identification/_id.py +276 -0
- ergodic/identification/_ida.py +163 -0
- ergodic/identification/_mediation.py +351 -0
- ergodic/identification/_recognize.py +110 -0
- ergodic/identification/_record.py +47 -0
- ergodic/inference/__init__.py +136 -0
- ergodic/inference/_crossfit.py +63 -0
- ergodic/inference/_design.py +32 -0
- ergodic/inference/_estimate.py +163 -0
- ergodic/inference/_estimator.py +817 -0
- ergodic/inference/_learner.py +140 -0
- ergodic/inference/_perunit.py +313 -0
- ergodic/inference/_refute.py +212 -0
- ergodic/inference/_registry.py +213 -0
- ergodic/inference/_result.py +111 -0
- ergodic/inference/_scores.py +227 -0
- ergodic/inference/_select.py +278 -0
- ergodic/inference/_stats.py +77 -0
- ergodic/inference/_target.py +36 -0
- ergodic/inference/estimators/__init__.py +49 -0
- ergodic/inference/estimators/_did.py +868 -0
- ergodic/inference/estimators/_dml.py +354 -0
- ergodic/inference/estimators/_doubly_robust.py +152 -0
- ergodic/inference/estimators/_forest.py +633 -0
- ergodic/inference/estimators/_frontdoor.py +66 -0
- ergodic/inference/estimators/_its.py +195 -0
- ergodic/inference/estimators/_iv.py +188 -0
- ergodic/inference/estimators/_mediation.py +599 -0
- ergodic/inference/estimators/_meta.py +126 -0
- ergodic/inference/estimators/_null.py +60 -0
- ergodic/inference/estimators/_plugin.py +423 -0
- ergodic/inference/estimators/_regression.py +169 -0
- ergodic/inference/estimators/_synthetic_control.py +361 -0
- ergodic/inference/estimators/_weighting.py +130 -0
- ergodic/inference/explain/__init__.py +34 -0
- ergodic/inference/explain/_blp.py +82 -0
- ergodic/inference/explain/_calibration.py +103 -0
- ergodic/inference/explain/_clan.py +100 -0
- ergodic/inference/explain/_gates.py +103 -0
- ergodic/inference/explain/_linalg.py +53 -0
- ergodic/inference/explain/_policy.py +459 -0
- ergodic/inference/explain/_rate.py +118 -0
- ergodic/inference/explain/_record.py +110 -0
- ergodic/inference/learners/__init__.py +17 -0
- ergodic/inference/learners/_linear.py +152 -0
- ergodic/inference/learners/_neighbors.py +107 -0
- ergodic/inference/learners/_pymc.py +149 -0
- ergodic/knowledge.py +444 -0
- ergodic/process/__init__.py +106 -0
- ergodic/process/_case_table.py +495 -0
- ergodic/process/_cases.py +108 -0
- ergodic/process/_conformance.py +461 -0
- ergodic/process/_decisions.py +527 -0
- ergodic/process/_filters.py +254 -0
- ergodic/process/_map.py +212 -0
- ergodic/process/_mine.py +489 -0
- ergodic/process/_net.py +261 -0
- ergodic/process/_panel.py +190 -0
- ergodic/process/_performance.py +169 -0
- ergodic/process/_soundness.py +103 -0
- ergodic/process/_tree.py +146 -0
- ergodic/process/_variants.py +64 -0
- ergodic/py.typed +0 -0
- ergodic/scm/__init__.py +51 -0
- ergodic/scm/_bounds.py +225 -0
- ergodic/scm/_compose.py +739 -0
- ergodic/scm/_district.py +111 -0
- ergodic/scm/_mechanism.py +762 -0
- ergodic/scm/_noise.py +99 -0
- ergodic/scm/_result.py +128 -0
- ergodic/scm/_scm.py +1038 -0
- ergodic-0.1.0.dist-info/METADATA +141 -0
- ergodic-0.1.0.dist-info/RECORD +165 -0
- ergodic-0.1.0.dist-info/WHEEL +4 -0
- ergodic-0.1.0.dist-info/licenses/LICENSE +202 -0
- ergodic-0.1.0.dist-info/licenses/NOTICE +42 -0
ergodic/__init__.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Ergodic: Causal AI.
|
|
3
|
+
|
|
4
|
+
Tools for causal discovery, causal inference, and process intelligence. The
|
|
5
|
+
shared objects live at the top level: the mixed-graph family (:class:`MixedGraph`
|
|
6
|
+
with the :func:`dag`, :func:`admg`, :func:`cpdag`, :func:`pag` builders) and
|
|
7
|
+
:class:`DomainKnowledge` for prior constraints. Identification turns a graph into
|
|
8
|
+
an estimand with :func:`identify_effect`.
|
|
9
|
+
|
|
10
|
+
Homepage: https://ergodic.ai
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from . import datasets
|
|
16
|
+
from .data import (
|
|
17
|
+
Dataset,
|
|
18
|
+
EventLog,
|
|
19
|
+
HierarchicalTSData,
|
|
20
|
+
Hierarchy,
|
|
21
|
+
Ordinal,
|
|
22
|
+
PanelData,
|
|
23
|
+
Schema,
|
|
24
|
+
SchemaInferenceWarning,
|
|
25
|
+
TabularData,
|
|
26
|
+
TimeSeriesData,
|
|
27
|
+
VariableType,
|
|
28
|
+
event_log,
|
|
29
|
+
hts,
|
|
30
|
+
panel,
|
|
31
|
+
tabular,
|
|
32
|
+
timeseries,
|
|
33
|
+
)
|
|
34
|
+
from .discovery import discover, granger
|
|
35
|
+
|
|
36
|
+
# `forecast` names both the pillar and its facade, and the function is what the
|
|
37
|
+
# top level exports: `from ergodic import forecast` gives the callable, while
|
|
38
|
+
# `from ergodic.forecast import backtest` still reaches the module. Import the
|
|
39
|
+
# function last so the binding is the deliberate one.
|
|
40
|
+
from .forecast import backtest, forecast
|
|
41
|
+
from .graph import (
|
|
42
|
+
Edge,
|
|
43
|
+
GraphKind,
|
|
44
|
+
InvalidGraphError,
|
|
45
|
+
Mark,
|
|
46
|
+
MixedGraph,
|
|
47
|
+
admg,
|
|
48
|
+
cpdag,
|
|
49
|
+
d_separated,
|
|
50
|
+
dag,
|
|
51
|
+
m_separated,
|
|
52
|
+
mixed,
|
|
53
|
+
pag,
|
|
54
|
+
)
|
|
55
|
+
from .identification import (
|
|
56
|
+
IDA,
|
|
57
|
+
Functional,
|
|
58
|
+
Hedge,
|
|
59
|
+
Identification,
|
|
60
|
+
IDResult,
|
|
61
|
+
Marginal,
|
|
62
|
+
MediationID,
|
|
63
|
+
Prob,
|
|
64
|
+
Product,
|
|
65
|
+
Ratio,
|
|
66
|
+
adjustment_set,
|
|
67
|
+
frontdoor_set,
|
|
68
|
+
ida,
|
|
69
|
+
identify_distribution,
|
|
70
|
+
identify_effect,
|
|
71
|
+
identify_mediation,
|
|
72
|
+
instruments,
|
|
73
|
+
is_frontdoor_set,
|
|
74
|
+
is_instrument,
|
|
75
|
+
is_valid_adjustment_set,
|
|
76
|
+
minimal_adjustment_set,
|
|
77
|
+
)
|
|
78
|
+
from .inference import (
|
|
79
|
+
CATEResult,
|
|
80
|
+
CausalForest,
|
|
81
|
+
EffectEstimate,
|
|
82
|
+
EffectTarget,
|
|
83
|
+
ITEResult,
|
|
84
|
+
MediationEstimate,
|
|
85
|
+
PerUnitEffect,
|
|
86
|
+
PolicyTree,
|
|
87
|
+
Refutation,
|
|
88
|
+
SelectionReport,
|
|
89
|
+
VariableImportance,
|
|
90
|
+
cate,
|
|
91
|
+
did,
|
|
92
|
+
estimate,
|
|
93
|
+
estimate_effect,
|
|
94
|
+
estimate_mediation,
|
|
95
|
+
event_study,
|
|
96
|
+
interrupted_time_series,
|
|
97
|
+
policy_tree,
|
|
98
|
+
refute_effect,
|
|
99
|
+
select_cate,
|
|
100
|
+
synthetic_control,
|
|
101
|
+
)
|
|
102
|
+
from .knowledge import DomainKnowledge, InconsistentKnowledgeError
|
|
103
|
+
from .process import (
|
|
104
|
+
case_table,
|
|
105
|
+
conformance,
|
|
106
|
+
decision_points,
|
|
107
|
+
decision_table,
|
|
108
|
+
discover_model,
|
|
109
|
+
kpi_panel,
|
|
110
|
+
process_map,
|
|
111
|
+
)
|
|
112
|
+
from .scm import (
|
|
113
|
+
SCM,
|
|
114
|
+
BinaryGumbelMax,
|
|
115
|
+
CategoricalGumbelMax,
|
|
116
|
+
GaussianAdditive,
|
|
117
|
+
SCMResult,
|
|
118
|
+
Sensitivity,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
__version__ = "0.1.0"
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"IDA",
|
|
125
|
+
"SCM",
|
|
126
|
+
"BinaryGumbelMax",
|
|
127
|
+
"CATEResult",
|
|
128
|
+
"CategoricalGumbelMax",
|
|
129
|
+
"CausalForest",
|
|
130
|
+
"Dataset",
|
|
131
|
+
"DomainKnowledge",
|
|
132
|
+
"Edge",
|
|
133
|
+
"EffectEstimate",
|
|
134
|
+
"EffectTarget",
|
|
135
|
+
"EventLog",
|
|
136
|
+
"Functional",
|
|
137
|
+
"GaussianAdditive",
|
|
138
|
+
"GraphKind",
|
|
139
|
+
"Hedge",
|
|
140
|
+
"HierarchicalTSData",
|
|
141
|
+
"Hierarchy",
|
|
142
|
+
"IDResult",
|
|
143
|
+
"ITEResult",
|
|
144
|
+
"Identification",
|
|
145
|
+
"InconsistentKnowledgeError",
|
|
146
|
+
"InvalidGraphError",
|
|
147
|
+
"Marginal",
|
|
148
|
+
"Mark",
|
|
149
|
+
"MediationEstimate",
|
|
150
|
+
"MediationID",
|
|
151
|
+
"MixedGraph",
|
|
152
|
+
"Ordinal",
|
|
153
|
+
"PanelData",
|
|
154
|
+
"PerUnitEffect",
|
|
155
|
+
"PolicyTree",
|
|
156
|
+
"Prob",
|
|
157
|
+
"Product",
|
|
158
|
+
"Ratio",
|
|
159
|
+
"Refutation",
|
|
160
|
+
"SCMResult",
|
|
161
|
+
"Schema",
|
|
162
|
+
"SchemaInferenceWarning",
|
|
163
|
+
"SelectionReport",
|
|
164
|
+
"Sensitivity",
|
|
165
|
+
"TabularData",
|
|
166
|
+
"TimeSeriesData",
|
|
167
|
+
"VariableImportance",
|
|
168
|
+
"VariableType",
|
|
169
|
+
"__version__",
|
|
170
|
+
"adjustment_set",
|
|
171
|
+
"admg",
|
|
172
|
+
"backtest",
|
|
173
|
+
"case_table",
|
|
174
|
+
"cate",
|
|
175
|
+
"conformance",
|
|
176
|
+
"cpdag",
|
|
177
|
+
"d_separated",
|
|
178
|
+
"dag",
|
|
179
|
+
"datasets",
|
|
180
|
+
"decision_points",
|
|
181
|
+
"decision_table",
|
|
182
|
+
"did",
|
|
183
|
+
"discover",
|
|
184
|
+
"discover_model",
|
|
185
|
+
"estimate",
|
|
186
|
+
"estimate_effect",
|
|
187
|
+
"estimate_mediation",
|
|
188
|
+
"event_log",
|
|
189
|
+
"event_study",
|
|
190
|
+
"forecast",
|
|
191
|
+
"frontdoor_set",
|
|
192
|
+
"granger",
|
|
193
|
+
"hts",
|
|
194
|
+
"ida",
|
|
195
|
+
"identify_distribution",
|
|
196
|
+
"identify_effect",
|
|
197
|
+
"identify_mediation",
|
|
198
|
+
"instruments",
|
|
199
|
+
"interrupted_time_series",
|
|
200
|
+
"is_frontdoor_set",
|
|
201
|
+
"is_instrument",
|
|
202
|
+
"is_valid_adjustment_set",
|
|
203
|
+
"kpi_panel",
|
|
204
|
+
"m_separated",
|
|
205
|
+
"minimal_adjustment_set",
|
|
206
|
+
"mixed",
|
|
207
|
+
"pag",
|
|
208
|
+
"panel",
|
|
209
|
+
"policy_tree",
|
|
210
|
+
"process_map",
|
|
211
|
+
"refute_effect",
|
|
212
|
+
"select_cate",
|
|
213
|
+
"synthetic_control",
|
|
214
|
+
"tabular",
|
|
215
|
+
"timeseries",
|
|
216
|
+
]
|
ergodic/data/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Typed datasets: the data objects discovery and inference read.
|
|
3
|
+
|
|
4
|
+
Each shape embeds a column :class:`Schema` (continuous, categorical, ordinal,
|
|
5
|
+
datetime, or excluded) and shares the accessors on :class:`Dataset`. The shapes
|
|
6
|
+
are tabular, time-series, panel, and hierarchical time-series; the event log
|
|
7
|
+
follows.
|
|
8
|
+
|
|
9
|
+
Build a dataset with :func:`tabular`, :func:`timeseries`, :func:`panel`, or
|
|
10
|
+
:func:`hts`, each of which takes a pandas or polars dataframe, a dict of columns,
|
|
11
|
+
or a numpy array.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ._dataset import Dataset
|
|
17
|
+
from ._eventlog import EventLog, event_log
|
|
18
|
+
from ._hierarchy import Hierarchy
|
|
19
|
+
from ._hts import HierarchicalTSData, hts
|
|
20
|
+
from ._panel import PanelData, panel
|
|
21
|
+
from ._schema import (
|
|
22
|
+
ColumnType,
|
|
23
|
+
Ordinal,
|
|
24
|
+
Schema,
|
|
25
|
+
SchemaInferenceWarning,
|
|
26
|
+
VariableType,
|
|
27
|
+
infer_schema,
|
|
28
|
+
)
|
|
29
|
+
from ._tabular import TabularData, tabular
|
|
30
|
+
from ._timeseries import TimeSeriesData, timeseries
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ColumnType",
|
|
34
|
+
"Dataset",
|
|
35
|
+
"EventLog",
|
|
36
|
+
"HierarchicalTSData",
|
|
37
|
+
"Hierarchy",
|
|
38
|
+
"Ordinal",
|
|
39
|
+
"PanelData",
|
|
40
|
+
"Schema",
|
|
41
|
+
"SchemaInferenceWarning",
|
|
42
|
+
"TabularData",
|
|
43
|
+
"TimeSeriesData",
|
|
44
|
+
"VariableType",
|
|
45
|
+
"event_log",
|
|
46
|
+
"hts",
|
|
47
|
+
"infer_schema",
|
|
48
|
+
"panel",
|
|
49
|
+
"tabular",
|
|
50
|
+
"timeseries",
|
|
51
|
+
]
|
ergodic/data/_dataset.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The dataset base: a typed, immutable column store the algorithms read.
|
|
3
|
+
|
|
4
|
+
``Dataset`` holds the normalized columns, the :class:`Schema`, and the accessors
|
|
5
|
+
that discovery and inference need: a float matrix, integer codes for categorical
|
|
6
|
+
columns, missing-value reporting, and a name check against a graph. It is
|
|
7
|
+
immutable, so retyping a column, selecting columns, or dropping rows returns a
|
|
8
|
+
new dataset. The concrete shapes (tabular, time-series, panel, and the rest)
|
|
9
|
+
extend it with their index columns.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field, replace
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from ._schema import (
|
|
20
|
+
Ordinal,
|
|
21
|
+
Schema,
|
|
22
|
+
VariableType,
|
|
23
|
+
_encode,
|
|
24
|
+
_missing_mask,
|
|
25
|
+
_resolve_type,
|
|
26
|
+
_unique_nonnull,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
31
|
+
from typing import Self
|
|
32
|
+
|
|
33
|
+
import numpy.typing as npt
|
|
34
|
+
import pandas as pd
|
|
35
|
+
|
|
36
|
+
from ..graph import MixedGraph
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, eq=False)
|
|
40
|
+
class Dataset:
|
|
41
|
+
"""A typed column store. Built through the shape factories, not directly."""
|
|
42
|
+
|
|
43
|
+
schema: Schema
|
|
44
|
+
n_rows: int
|
|
45
|
+
_store: Mapping[str, npt.NDArray[Any]] = field(repr=False)
|
|
46
|
+
|
|
47
|
+
# -- shape ---------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def names(self) -> tuple[str, ...]:
|
|
51
|
+
return self.schema.names
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def n_columns(self) -> int:
|
|
55
|
+
return len(self.schema)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def shape(self) -> tuple[int, int]:
|
|
59
|
+
return (self.n_rows, self.n_columns)
|
|
60
|
+
|
|
61
|
+
def __len__(self) -> int:
|
|
62
|
+
return self.n_rows
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def index_columns(self) -> tuple[str, ...]:
|
|
66
|
+
"""Columns that index the rows (time, entity, case), not modeled variables."""
|
|
67
|
+
return ()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def variables(self) -> tuple[str, ...]:
|
|
71
|
+
"""Column names the algorithms see: not the excluded ones, not the index."""
|
|
72
|
+
index = set(self.index_columns)
|
|
73
|
+
return tuple(name for name in self.schema.variables if name not in index)
|
|
74
|
+
|
|
75
|
+
# -- column access -------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def column(self, name: str) -> npt.NDArray[Any]:
|
|
78
|
+
"""A read-only copy of one column's raw values."""
|
|
79
|
+
if name not in self.schema:
|
|
80
|
+
raise KeyError(name)
|
|
81
|
+
arr = np.array(self._store[name], copy=True)
|
|
82
|
+
arr.setflags(write=False)
|
|
83
|
+
return arr
|
|
84
|
+
|
|
85
|
+
def codes(self, name: str) -> npt.NDArray[np.int64]:
|
|
86
|
+
"""Integer category codes for a categorical or ordinal column, -1 for missing.
|
|
87
|
+
|
|
88
|
+
A non-missing value outside the column's categories raises a ``ValueError``.
|
|
89
|
+
"""
|
|
90
|
+
col = self.schema[name]
|
|
91
|
+
if not col.is_discrete:
|
|
92
|
+
raise ValueError(f"column {name!r} is {col.kind.value}, not categorical or ordinal")
|
|
93
|
+
return _encode(self._store[name], col.categories, col.ordered, name)
|
|
94
|
+
|
|
95
|
+
def categories(self, name: str) -> tuple[Any, ...] | None:
|
|
96
|
+
return self.schema[name].categories
|
|
97
|
+
|
|
98
|
+
def matrix(self, names: Sequence[str] | None = None) -> npt.NDArray[np.float64]:
|
|
99
|
+
"""A float matrix of the named columns. Discrete columns become their codes,
|
|
100
|
+
and a missing value (an NA category or NaT) becomes NaN."""
|
|
101
|
+
chosen = list(names) if names is not None else list(self.variables)
|
|
102
|
+
if not chosen:
|
|
103
|
+
return np.empty((self.n_rows, 0), dtype=np.float64)
|
|
104
|
+
cols: list[npt.NDArray[Any]] = []
|
|
105
|
+
for name in chosen:
|
|
106
|
+
col = self.schema[name]
|
|
107
|
+
if col.is_continuous:
|
|
108
|
+
cols.append(np.asarray(self._store[name], dtype=np.float64))
|
|
109
|
+
elif col.is_discrete:
|
|
110
|
+
encoded = self.codes(name).astype(np.float64)
|
|
111
|
+
encoded[encoded == -1.0] = np.nan
|
|
112
|
+
cols.append(encoded)
|
|
113
|
+
elif col.is_datetime:
|
|
114
|
+
stamps = np.asarray(self._store[name], dtype="datetime64[ns]")
|
|
115
|
+
floats = stamps.view(np.int64).astype(np.float64)
|
|
116
|
+
floats[np.isnat(stamps)] = np.nan
|
|
117
|
+
cols.append(floats)
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"column {name!r} is excluded; it has no numeric form")
|
|
120
|
+
return np.asarray(np.column_stack(cols), dtype=np.float64)
|
|
121
|
+
|
|
122
|
+
def to_numpy(self) -> npt.NDArray[np.float64]:
|
|
123
|
+
return self.matrix()
|
|
124
|
+
|
|
125
|
+
def to_pandas(self) -> pd.DataFrame:
|
|
126
|
+
import pandas as pd
|
|
127
|
+
|
|
128
|
+
return pd.DataFrame({name: self._store[name] for name in self.schema.names})
|
|
129
|
+
|
|
130
|
+
# -- missing data --------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def missing(self) -> dict[str, int]:
|
|
133
|
+
"""Missing-value count per column."""
|
|
134
|
+
return {name: int(_missing_mask(self._store[name]).sum()) for name in self.schema.names}
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def has_missing(self) -> bool:
|
|
138
|
+
return any(count > 0 for count in self.missing().values())
|
|
139
|
+
|
|
140
|
+
def dropna(self) -> Self:
|
|
141
|
+
"""Drop every row with a missing value in any column."""
|
|
142
|
+
keep = np.ones(self.n_rows, dtype=bool)
|
|
143
|
+
for arr in self._store.values():
|
|
144
|
+
keep &= ~_missing_mask(arr)
|
|
145
|
+
store = {name: arr[keep] for name, arr in self._store.items()}
|
|
146
|
+
return replace(self, _store=store, n_rows=int(keep.sum()))
|
|
147
|
+
|
|
148
|
+
# -- immutable edits -----------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def take(self, rows: Sequence[int] | npt.NDArray[np.int64]) -> Self:
|
|
151
|
+
"""A new dataset with the rows at the given positions, in the given order.
|
|
152
|
+
|
|
153
|
+
Positions may repeat (a bootstrap resample) and may be negative
|
|
154
|
+
(Python indexing). Row identity is all this knows about; shapes with
|
|
155
|
+
row semantics (time series, panel) keep their index columns but it is
|
|
156
|
+
the caller's job to resample in a way their structure survives.
|
|
157
|
+
"""
|
|
158
|
+
idx = np.asarray(rows, dtype=np.int64)
|
|
159
|
+
if idx.ndim != 1:
|
|
160
|
+
raise ValueError(f"rows must be one-dimensional, got shape {idx.shape}")
|
|
161
|
+
if idx.size and (int(idx.min()) < -self.n_rows or int(idx.max()) >= self.n_rows):
|
|
162
|
+
raise IndexError(f"row positions out of range for {self.n_rows} rows")
|
|
163
|
+
store = {name: arr[idx] for name, arr in self._store.items()}
|
|
164
|
+
return replace(self, _store=store, n_rows=int(idx.size))
|
|
165
|
+
|
|
166
|
+
def select(self, names: Iterable[str]) -> Self:
|
|
167
|
+
"""A new dataset with the named columns, plus any index columns, in order."""
|
|
168
|
+
chosen = [str(n) for n in names]
|
|
169
|
+
absent = [n for n in chosen if n not in self.schema]
|
|
170
|
+
if absent:
|
|
171
|
+
raise KeyError(f"no such columns: {absent}")
|
|
172
|
+
for index_name in self.index_columns:
|
|
173
|
+
if index_name not in chosen:
|
|
174
|
+
chosen.append(index_name)
|
|
175
|
+
schema = Schema(tuple(self.schema[n] for n in chosen))
|
|
176
|
+
store = {n: self._store[n] for n in chosen}
|
|
177
|
+
return replace(self, schema=schema, _store=store)
|
|
178
|
+
|
|
179
|
+
def with_type(
|
|
180
|
+
self,
|
|
181
|
+
name: str,
|
|
182
|
+
kind: VariableType | str | Ordinal,
|
|
183
|
+
categories: Sequence[Any] | None = None,
|
|
184
|
+
) -> Self:
|
|
185
|
+
"""A new dataset with ``name`` retyped, categories read from data when needed."""
|
|
186
|
+
if name not in self.schema:
|
|
187
|
+
raise KeyError(name)
|
|
188
|
+
new = _resolve_type(name, _unique_nonnull(self._store[name]), kind, categories)
|
|
189
|
+
if new.is_discrete and (categories is not None or isinstance(kind, Ordinal)):
|
|
190
|
+
# encode once now, so declared categories that miss a value fail here
|
|
191
|
+
_encode(self._store[name], new.categories, new.ordered, name)
|
|
192
|
+
cols = tuple(new if c.name == name else c for c in self.schema.columns)
|
|
193
|
+
return replace(self, schema=Schema(cols))
|
|
194
|
+
|
|
195
|
+
def exclude(self, *names: str) -> Self:
|
|
196
|
+
"""A new dataset with the named columns marked excluded."""
|
|
197
|
+
out = self
|
|
198
|
+
for name in names:
|
|
199
|
+
out = out.with_type(name, VariableType.EXCLUDED)
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
# -- graph alignment -----------------------------------------------------
|
|
203
|
+
|
|
204
|
+
def alignment(self, graph: MixedGraph) -> list[str]:
|
|
205
|
+
"""Where the variables and the graph's nodes disagree, empty when they match."""
|
|
206
|
+
nodes = set(graph.nodes)
|
|
207
|
+
cols = set(self.variables)
|
|
208
|
+
out = [f"graph node {n!r} has no column" for n in sorted(nodes - cols)]
|
|
209
|
+
out += [f"column {c!r} is not a graph node" for c in sorted(cols - nodes)]
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
def aligns_with(self, graph: MixedGraph) -> bool:
|
|
213
|
+
"""True when the variables are exactly the graph's nodes."""
|
|
214
|
+
return not self.alignment(graph)
|
|
215
|
+
|
|
216
|
+
def __repr__(self) -> str:
|
|
217
|
+
parts = [
|
|
218
|
+
f"{len(names)} {kind.value}"
|
|
219
|
+
for kind in VariableType
|
|
220
|
+
if (names := self.schema.of_kind(kind))
|
|
221
|
+
]
|
|
222
|
+
body = ", ".join(parts)
|
|
223
|
+
return f"{type(self).__name__}(rows={self.n_rows}, cols={self.n_columns}: {body})"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Event log: variable-length traces of discrete events, keyed by a case.
|
|
3
|
+
|
|
4
|
+
Build it with :func:`event_log`, naming the case, activity, and timestamp
|
|
5
|
+
columns. Rows are events, grouped by case and ordered in time. The log reads as
|
|
6
|
+
the process-mining primitives the discovery algorithms need: the trace per case,
|
|
7
|
+
the distinct variants, and the directly-follows counts.
|
|
8
|
+
|
|
9
|
+
The projections to the grid shapes (case features to tabular, event rates to a
|
|
10
|
+
time-series) land with the process pillar. This is the container and its core
|
|
11
|
+
relations.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections import Counter
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from itertools import pairwise
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from ._dataset import Dataset
|
|
24
|
+
from ._frame import to_columns
|
|
25
|
+
from ._schema import infer_schema
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
29
|
+
|
|
30
|
+
from ._schema import Ordinal, VariableType
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, eq=False)
|
|
34
|
+
class EventLog(Dataset):
|
|
35
|
+
"""Events keyed by case, activity, and timestamp; one row per event."""
|
|
36
|
+
|
|
37
|
+
case: str
|
|
38
|
+
activity: str
|
|
39
|
+
timestamp: str
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def index_columns(self) -> tuple[str, ...]:
|
|
43
|
+
return (self.case, self.activity, self.timestamp)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def cases(self) -> tuple[Any, ...]:
|
|
47
|
+
return tuple(sorted(set(self._store[self.case].tolist())))
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def n_cases(self) -> int:
|
|
51
|
+
return len(set(self._store[self.case].tolist()))
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def activities(self) -> tuple[str, ...]:
|
|
55
|
+
return tuple(sorted(set(self._store[self.activity].tolist())))
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def n_activities(self) -> int:
|
|
59
|
+
return len(set(self._store[self.activity].tolist()))
|
|
60
|
+
|
|
61
|
+
def traces(self) -> dict[Any, tuple[str, ...]]:
|
|
62
|
+
"""The ordered activity sequence per case."""
|
|
63
|
+
cases = self._store[self.case].tolist()
|
|
64
|
+
acts = self._store[self.activity].tolist()
|
|
65
|
+
out: dict[Any, list[str]] = {}
|
|
66
|
+
for case_id, act in zip(cases, acts, strict=True):
|
|
67
|
+
out.setdefault(case_id, []).append(act)
|
|
68
|
+
return {case_id: tuple(seq) for case_id, seq in out.items()}
|
|
69
|
+
|
|
70
|
+
def variants(self) -> Counter[tuple[str, ...]]:
|
|
71
|
+
"""How many cases follow each distinct activity sequence."""
|
|
72
|
+
return Counter(self.traces().values())
|
|
73
|
+
|
|
74
|
+
def directly_follows(self) -> Counter[tuple[str, str]]:
|
|
75
|
+
"""Counts of each ``(a, b)`` where ``b`` immediately follows ``a`` in a case."""
|
|
76
|
+
out: Counter[tuple[str, str]] = Counter()
|
|
77
|
+
for trace in self.traces().values():
|
|
78
|
+
for first, second in pairwise(trace):
|
|
79
|
+
out[(first, second)] += 1
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
return (
|
|
84
|
+
f"EventLog(cases={self.n_cases}, events={self.n_rows}, activities={self.n_activities})"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def event_log(
|
|
89
|
+
data: Any,
|
|
90
|
+
*,
|
|
91
|
+
case: str,
|
|
92
|
+
activity: str,
|
|
93
|
+
timestamp: str,
|
|
94
|
+
columns: Sequence[str] | None = None,
|
|
95
|
+
types: Mapping[str, VariableType | str | Ordinal] | None = None,
|
|
96
|
+
exclude: Iterable[str] | None = None,
|
|
97
|
+
categorical_threshold: int = 10,
|
|
98
|
+
) -> EventLog:
|
|
99
|
+
"""Build an :class:`EventLog`, grouped by case and ordered by timestamp."""
|
|
100
|
+
store, n_rows, declared = to_columns(data, columns)
|
|
101
|
+
for role, name in (("case", case), ("activity", activity), ("timestamp", timestamp)):
|
|
102
|
+
if name not in store:
|
|
103
|
+
raise KeyError(f"{role} column {name!r} not found in {sorted(store)}")
|
|
104
|
+
overrides: dict[str, VariableType | str | Ordinal] = dict(types or {})
|
|
105
|
+
overrides.setdefault(case, "categorical")
|
|
106
|
+
overrides.setdefault(activity, "categorical")
|
|
107
|
+
if timestamp not in overrides:
|
|
108
|
+
overrides[timestamp] = "datetime" if store[timestamp].dtype.kind == "M" else "continuous"
|
|
109
|
+
schema = infer_schema(
|
|
110
|
+
store,
|
|
111
|
+
n_rows=n_rows,
|
|
112
|
+
declared=declared,
|
|
113
|
+
overrides=overrides,
|
|
114
|
+
exclude=exclude,
|
|
115
|
+
categorical_threshold=categorical_threshold,
|
|
116
|
+
)
|
|
117
|
+
order = np.lexsort((store[timestamp], store[case]))
|
|
118
|
+
ordered = {name: arr[order] for name, arr in store.items()}
|
|
119
|
+
return EventLog(
|
|
120
|
+
schema=schema,
|
|
121
|
+
n_rows=n_rows,
|
|
122
|
+
_store=ordered,
|
|
123
|
+
case=case,
|
|
124
|
+
activity=activity,
|
|
125
|
+
timestamp=timestamp,
|
|
126
|
+
)
|