okunfa 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.
okunfa/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """okunfa: synthetic control from scratch, with the inference that makes it
2
+ honest.
3
+
4
+ *Okunfa* is Yoruba for cause. The method (Abadie, Diamond, Hainmueller
5
+ 2010) answers "what would California's smoking have looked like without
6
+ Proposition 99?" by building a weighted combination of untreated states
7
+ that matches California before the law, then reading the difference after
8
+ it. The weights are the transparency: you can see exactly who the
9
+ counterfactual is made of.
10
+ """
11
+
12
+ from .core import SyntheticControl, SCFit
13
+ from .inference import (in_space_placebos, in_time_placebo, leave_one_out,
14
+ rmspe, rmspe_ratios)
15
+
16
+ __version__ = "0.1.0"
17
+ __all__ = ["SyntheticControl", "SCFit", "in_space_placebos",
18
+ "in_time_placebo", "leave_one_out", "rmspe", "rmspe_ratios"]
okunfa/core.py ADDED
@@ -0,0 +1,143 @@
1
+ """The estimator: nested optimization, exactly as the paper frames it.
2
+
3
+ Inner problem w*(V) = argmin_{w in simplex} (X1 - X0 w)' V (X1 - X0 w)
4
+ Outer problem V* = argmin_V || Z1 - Z0 w*(V) ||^2 (pre-period MSPE)
5
+
6
+ X are predictors (covariate averages plus outcome lags), Z the pre-period
7
+ outcome path. V is a diagonal matrix saying how much matching each
8
+ predictor matters; the outer loop chooses it so the resulting weights
9
+ track the treated unit's actual pre-period outcome as closely as
10
+ possible. The inner problem is a small QP solved with SLSQP and an
11
+ analytic gradient; the outer is Nelder-Mead over a softmax
12
+ parameterization of the simplex, so every candidate V is valid by
13
+ construction.
14
+ """
15
+
16
+ from dataclasses import dataclass, field
17
+
18
+ import numpy as np
19
+ from scipy.optimize import minimize
20
+
21
+
22
+ def _simplex_qp(H, f, n, x0=None):
23
+ """min 0.5 w'Hw + f'w s.t. w >= 0, sum w = 1."""
24
+ w0 = np.full(n, 1.0 / n) if x0 is None else x0
25
+
26
+ def obj(w):
27
+ return 0.5 * w @ H @ w + f @ w
28
+
29
+ def grad(w):
30
+ return H @ w + f
31
+
32
+ res = minimize(obj, w0, jac=grad, method="SLSQP",
33
+ bounds=[(0.0, 1.0)] * n,
34
+ constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1,
35
+ "jac": lambda w: np.ones_like(w)}],
36
+ options={"maxiter": 500, "ftol": 1e-12})
37
+ w = np.clip(res.x, 0, None)
38
+ return w / w.sum()
39
+
40
+
41
+ def _inner_weights(X1, X0, v):
42
+ """w*(V) for diagonal V given as a vector v."""
43
+ A = X0 * np.sqrt(v)[:, None]
44
+ b = X1 * np.sqrt(v)
45
+ H = 2.0 * (A.T @ A)
46
+ f = -2.0 * (A.T @ b)
47
+ return _simplex_qp(H, f, X0.shape[1])
48
+
49
+
50
+ @dataclass
51
+ class SCFit:
52
+ weights: np.ndarray # donor weights, sum to 1
53
+ v: np.ndarray # predictor importances, sum to 1
54
+ donors: list # donor names, aligned with weights
55
+ predictors: list # predictor names, aligned with v
56
+ synthetic: np.ndarray # synthetic outcome path, all periods
57
+ treated: np.ndarray # treated outcome path, all periods
58
+ periods: np.ndarray
59
+ pre_mask: np.ndarray
60
+ pre_rmspe: float = field(init=False)
61
+ post_rmspe: float = field(init=False)
62
+
63
+ def __post_init__(self):
64
+ gap = self.treated - self.synthetic
65
+ self.pre_rmspe = float(np.sqrt(np.mean(gap[self.pre_mask] ** 2)))
66
+ self.post_rmspe = float(np.sqrt(np.mean(gap[~self.pre_mask] ** 2)))
67
+
68
+ @property
69
+ def gap(self):
70
+ return self.treated - self.synthetic
71
+
72
+ def nonzero(self, tol=1e-3):
73
+ """The donors the counterfactual is actually made of."""
74
+ order = np.argsort(-self.weights)
75
+ return [(self.donors[i], float(self.weights[i]))
76
+ for i in order if self.weights[i] > tol]
77
+
78
+
79
+ class SyntheticControl:
80
+ """Fit a synthetic control for one treated unit.
81
+
82
+ Parameters
83
+ ----------
84
+ X1, X0 : predictor vector (k,) for the treated unit and matrix (k, J)
85
+ for the donors. Rows are standardized internally so V weighs
86
+ comparable quantities.
87
+ Z1, Z0 : pre-period outcome path (T0,) and matrix (T0, J) used by the
88
+ outer loop.
89
+ """
90
+
91
+ def __init__(self, outer_restarts=4, seed=0):
92
+ self.outer_restarts = outer_restarts
93
+ self.seed = seed
94
+
95
+ def fit_weights(self, X1, X0, Z1, Z0):
96
+ k = X1.shape[0]
97
+ # standardize predictor rows over (treated + donors)
98
+ allx = np.column_stack([X1, X0])
99
+ mu, sd = allx.mean(1), allx.std(1)
100
+ sd[sd == 0] = 1.0
101
+ X1s = (X1 - mu) / sd
102
+ X0s = (X0 - mu[:, None]) / sd[:, None]
103
+
104
+ def outer_loss(theta):
105
+ v = np.exp(theta - theta.max())
106
+ v = v / v.sum()
107
+ w = _inner_weights(X1s, X0s, v)
108
+ return float(np.mean((Z1 - Z0 @ w) ** 2))
109
+
110
+ rng = np.random.default_rng(self.seed)
111
+ best = None
112
+ starts = [np.zeros(k)] + [rng.normal(0, 1, k)
113
+ for _ in range(self.outer_restarts - 1)]
114
+ for th0 in starts:
115
+ res = minimize(outer_loss, th0, method="Nelder-Mead",
116
+ options={"maxiter": 2000, "xatol": 1e-6,
117
+ "fatol": 1e-10})
118
+ if best is None or res.fun < best.fun:
119
+ best = res
120
+ v = np.exp(best.x - best.x.max())
121
+ v = v / v.sum()
122
+ w = _inner_weights(X1s, X0s, v)
123
+ return w, v
124
+
125
+ def fit(self, *, outcome, periods, treated, donors, pre_mask,
126
+ predictors):
127
+ """outcome: dict name -> path over periods; predictors: dict
128
+ name -> dict unit -> scalar."""
129
+ periods = np.asarray(periods)
130
+ pre_mask = np.asarray(pre_mask, dtype=bool)
131
+ Z1 = np.asarray(outcome[treated], float)[pre_mask]
132
+ Z0 = np.column_stack([np.asarray(outcome[d], float)[pre_mask]
133
+ for d in donors])
134
+ pnames = list(predictors)
135
+ X1 = np.array([predictors[p][treated] for p in pnames], float)
136
+ X0 = np.array([[predictors[p][d] for d in donors]
137
+ for p in pnames], float)
138
+ w, v = self.fit_weights(X1, X0, Z1, Z0)
139
+ synth = np.column_stack([outcome[d] for d in donors]) @ w
140
+ return SCFit(weights=w, v=v, donors=list(donors),
141
+ predictors=pnames, synthetic=synth,
142
+ treated=np.asarray(outcome[treated], float),
143
+ periods=periods, pre_mask=pre_mask)
okunfa/datasets.py ADDED
@@ -0,0 +1,98 @@
1
+ """The Proposition 99 panel: cigarette sales in 39 states, 1970-2000.
2
+
3
+ The canonical dataset of Abadie, Diamond and Hainmueller (2010),
4
+ originally from Orzechowski and Walker's *Tax Burden on Tobacco*
5
+ (public government data), as distributed with the R Synth package and
6
+ mirrored in Facure's *Causal Inference for the Brave and True*. State
7
+ codes are the 39 donor-pool states in alphabetical order; the paper drops
8
+ states with their own large tobacco programs and DC.
9
+ """
10
+
11
+ import csv
12
+ import io
13
+ import urllib.request
14
+ from collections import defaultdict
15
+ from pathlib import Path
16
+
17
+ import numpy as np
18
+
19
+ URL = ("https://raw.githubusercontent.com/matheusfacure/"
20
+ "python-causality-handbook/master/"
21
+ "causal-inference-for-the-brave-and-true/data/smoking.csv")
22
+
23
+ STATES = [
24
+ "Alabama", "Arkansas", "California", "Colorado", "Connecticut",
25
+ "Delaware", "Georgia", "Idaho", "Illinois", "Indiana", "Iowa",
26
+ "Kansas", "Kentucky", "Louisiana", "Maine", "Minnesota",
27
+ "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada",
28
+ "New Hampshire", "New Mexico", "North Carolina", "North Dakota",
29
+ "Ohio", "Oklahoma", "Pennsylvania", "Rhode Island", "South Carolina",
30
+ "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia",
31
+ "West Virginia", "Wisconsin", "Wyoming",
32
+ ]
33
+
34
+ TREATMENT_YEAR = 1989 # Prop 99 took effect January 1989
35
+
36
+
37
+ def _cache_path():
38
+ d = Path.home() / ".cache" / "okunfa"
39
+ d.mkdir(parents=True, exist_ok=True)
40
+ return d / "smoking.csv"
41
+
42
+
43
+ def load_prop99(path=None):
44
+ """Return the panel as {column: {state: {year: value}}} plus years."""
45
+ p = Path(path) if path else _cache_path()
46
+ if not p.exists():
47
+ with urllib.request.urlopen(URL, timeout=60) as r:
48
+ p.write_bytes(r.read())
49
+ rows = list(csv.DictReader(io.StringIO(p.read_text())))
50
+
51
+ cols = ["cigsale", "lnincome", "beer", "age15to24", "retprice"]
52
+ panel = {c: defaultdict(dict) for c in cols}
53
+ years = set()
54
+ for r in rows:
55
+ st = STATES[int(r["state"]) - 1]
56
+ yr = int(r["year"])
57
+ years.add(yr)
58
+ for c in cols:
59
+ v = r[c]
60
+ if v not in ("", "NA"):
61
+ panel[c][st][yr] = float(v)
62
+ return panel, np.array(sorted(years))
63
+
64
+
65
+ def adh_design(panel, years):
66
+ """The exact design of ADH 2010, Table 1.
67
+
68
+ Predictors: 1980-88 averages of ln income, retail price, share aged
69
+ 15-24; 1984-88 average beer consumption; and cigarette sales in 1975,
70
+ 1980 and 1988 as outcome lags. Pre-period for the outer loop:
71
+ 1970-1988.
72
+ """
73
+ treated = "California"
74
+ donors = [s for s in STATES if s != treated]
75
+ outcome = {s: np.array([panel["cigsale"][s][y] for y in years])
76
+ for s in STATES}
77
+ pre_mask = years <= 1988
78
+
79
+ def avg(col, lo, hi):
80
+ return {s: float(np.mean([panel[col][s][y]
81
+ for y in range(lo, hi + 1)
82
+ if y in panel[col][s]]))
83
+ for s in STATES}
84
+
85
+ predictors = {
86
+ "ln income": avg("lnincome", 1980, 1988),
87
+ "retail price": avg("retprice", 1980, 1988),
88
+ "share aged 15-24": avg("age15to24", 1980, 1988),
89
+ "beer consumption": avg("beer", 1984, 1988),
90
+ "cigarette sales 1975": {s: panel["cigsale"][s][1975]
91
+ for s in STATES},
92
+ "cigarette sales 1980": {s: panel["cigsale"][s][1980]
93
+ for s in STATES},
94
+ "cigarette sales 1988": {s: panel["cigsale"][s][1988]
95
+ for s in STATES},
96
+ }
97
+ return dict(outcome=outcome, periods=years, treated=treated,
98
+ donors=donors, pre_mask=pre_mask, predictors=predictors)
okunfa/inference.py ADDED
@@ -0,0 +1,80 @@
1
+ """The inference: synthetic control without placebos is just a curve.
2
+
3
+ The method's own authors built its test: run the identical procedure on
4
+ every untreated unit (in-space placebos). If the treated unit's post/pre
5
+ RMSPE ratio towers over the placebo distribution, the effect is unlikely
6
+ to be luck; the permutation p-value is the rank of that ratio. A second
7
+ check runs the procedure at a fake earlier treatment date (in-time
8
+ placebo), where a real design should find nothing.
9
+ """
10
+
11
+ import numpy as np
12
+
13
+
14
+ def rmspe(gap, mask):
15
+ return float(np.sqrt(np.mean(np.asarray(gap)[mask] ** 2)))
16
+
17
+
18
+ def rmspe_ratios(fits):
19
+ """post/pre RMSPE ratio per fit dict {unit: SCFit}."""
20
+ return {u: f.post_rmspe / f.pre_rmspe for u, f in fits.items()}
21
+
22
+
23
+ def in_space_placebos(sc, *, outcome, periods, treated, donors, pre_mask,
24
+ predictors, verbose=False):
25
+ """Run the identical estimator with each donor cast as pseudo-treated.
26
+
27
+ Returns {unit: SCFit} including the genuinely treated unit.
28
+ """
29
+ fits = {}
30
+ units = [treated] + list(donors)
31
+ for u in units:
32
+ # a placebo's donor pool is the other CONTROL units only: the
33
+ # genuinely treated unit is post-treatment data and would
34
+ # contaminate every placebo counterfactual
35
+ pool = [d for d in donors if d != u]
36
+ fits[u] = sc.fit(outcome=outcome, periods=periods, treated=u,
37
+ donors=pool, pre_mask=pre_mask,
38
+ predictors=_reindex(predictors, units))
39
+ if verbose:
40
+ print(f" placebo {u}: pre {fits[u].pre_rmspe:.2f} "
41
+ f"post {fits[u].post_rmspe:.2f}")
42
+ return fits
43
+
44
+
45
+ def _reindex(predictors, units):
46
+ return {p: {u: predictors[p][u] for u in units if u in predictors[p]}
47
+ for p in predictors}
48
+
49
+
50
+ def permutation_pvalue(fits, treated):
51
+ """P(ratio >= treated's) under random permutation: rank / N."""
52
+ ratios = rmspe_ratios(fits)
53
+ r_t = ratios[treated]
54
+ rank = sum(1 for v in ratios.values() if v >= r_t)
55
+ return rank / len(ratios), ratios
56
+
57
+
58
+ def leave_one_out(sc, fit, *, outcome, periods, treated, pre_mask,
59
+ predictors, tol=1e-3):
60
+ """Refit dropping each nonzero-weight donor in turn: does the story
61
+ survive losing any single leg of the counterfactual?"""
62
+ out = {}
63
+ keep = [d for d, _ in fit.nonzero(tol)]
64
+ for drop in keep:
65
+ pool = [d for d in fit.donors if d != drop]
66
+ out[drop] = sc.fit(outcome=outcome, periods=periods,
67
+ treated=treated, donors=pool, pre_mask=pre_mask,
68
+ predictors=predictors)
69
+ return out
70
+
71
+
72
+ def in_time_placebo(sc, *, outcome, periods, treated, donors,
73
+ true_pre_mask, fake_treatment_period, predictors):
74
+ """Pretend treatment happened earlier; a sound design finds ~nothing
75
+ in the window that is actually untreated."""
76
+ periods = np.asarray(periods)
77
+ fake_pre = periods < fake_treatment_period
78
+ fake_pre &= true_pre_mask # never let real post-treatment data in
79
+ return sc.fit(outcome=outcome, periods=periods, treated=treated,
80
+ donors=donors, pre_mask=fake_pre, predictors=predictors)
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: okunfa
3
+ Version: 0.1.0
4
+ Summary: Synthetic control from scratch: the ADH estimator with the placebo inference that makes it honest
5
+ Author-email: Kenny Obidele <obidelek19@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Kenny0bi/okunfa
8
+ Project-URL: Issues, https://github.com/Kenny0bi/okunfa/issues
9
+ Keywords: causal-inference,synthetic-control,econometrics,policy-evaluation,placebo-tests
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: numpy>=1.24
18
+ Requires-Dist: scipy>=1.10
19
+
20
+ # okunfa
21
+
22
+ Synthetic control from scratch, verified against the paper that defined
23
+ the method, shipped as a library.
24
+
25
+ ```
26
+ pip install okunfa
27
+ ```
28
+
29
+ *Okunfa* is Yoruba for cause. The question causal inference keeps asking
30
+ is "compared to what?", and the synthetic control method (Abadie, Diamond,
31
+ Hainmueller 2010) gives the most transparent answer in the toolbox: build
32
+ the counterfactual as a weighted average of untreated units, weights
33
+ non-negative and summing to one, chosen to match the treated unit before
34
+ the intervention. The counterfactual comes with an ingredients list you
35
+ can argue with.
36
+
37
+ I implemented the full estimator on numpy and scipy (no causal libraries
38
+ anywhere), then pointed it at the method's founding result: California's
39
+ Proposition 99 tobacco program.
40
+
41
+ ## The reproduction
42
+
43
+ ![The ghost](assets/ghost.svg)
44
+
45
+ ADH 2010, Table 2, says synthetic California is Utah .334, Nevada .234,
46
+ Montana .199, Colorado .164, Connecticut .069. This implementation, run
47
+ on the same public panel with the same predictors, finds:
48
+
49
+ | state | published | okunfa |
50
+ |---|---|---|
51
+ | Utah | 0.334 | 0.335 |
52
+ | Nevada | 0.234 | 0.236 |
53
+ | Montana | 0.199 | 0.201 |
54
+ | Colorado | 0.164 | 0.160 |
55
+ | Connecticut | 0.069 | 0.068 |
56
+
57
+ All five within 0.005, every other state at zero, and the headline
58
+ effect lands where the paper put it: California's cigarette sales about
59
+ 26 packs per capita below its ghost by 2000. A test pins each published
60
+ weight to within 0.01, so the reproduction cannot silently rot.
61
+
62
+ ![The animation](assets/recipe.gif)
63
+
64
+ (Source: [assets/manim_recipe.py](assets/manim_recipe.py), video in
65
+ [assets/recipe.mp4](assets/recipe.mp4).)
66
+
67
+ ## The inference is the honest part
68
+
69
+ A synthetic control without placebos is just a curve that goes down.
70
+ The method's own significance test is to run the identical procedure on
71
+ every state that never passed the law:
72
+
73
+ ![The placebo storm](assets/storm.svg)
74
+
75
+ ![One in thirty-nine](assets/rank.svg)
76
+
77
+ California's post/pre RMSPE ratio is the most extreme of all 39 states,
78
+ a permutation p-value of 1/39 = 0.026, matching the paper's inference
79
+ exactly. Two more checks that the story is structural, not lucky:
80
+
81
+ ![Stress tests](assets/stress.svg)
82
+
83
+ Leave-one-out refits drop each ingredient state in turn (the gap
84
+ survives losing any single leg), and an in-time placebo pretends the law
85
+ passed in 1980 using only pre-1980 information (the design finds almost
86
+ nothing where nothing happened).
87
+
88
+ ## The estimator
89
+
90
+ The paper's nested optimization, implemented directly:
91
+
92
+ - **Inner problem**: given predictor importances V, the weights solve a
93
+ small quadratic program on the simplex, min (X1 - X0 w)' V (X1 - X0 w),
94
+ by SLSQP with an analytic gradient.
95
+ - **Outer problem**: V itself is chosen to minimize pre-period outcome
96
+ MSPE, by Nelder-Mead over a softmax parameterization, so every
97
+ candidate V is a valid distribution by construction, with multiple
98
+ restarts because the surface is not convex.
99
+
100
+ The chosen V is itself interpretable, and slightly deflating: the data
101
+ puts 87% of the matching weight on retail price and most of the rest on
102
+ the 1975 sales lag. The covariates everyone lists (income, beer, age
103
+ structure) barely matter once the outcome lags are in, which is a known
104
+ property of the method worth seeing with your own eyes.
105
+
106
+ ## Using it
107
+
108
+ ```python
109
+ from okunfa import SyntheticControl, in_space_placebos, permutation_pvalue
110
+ from okunfa.datasets import load_prop99, adh_design
111
+
112
+ panel, years = load_prop99() # fetches the public panel on demand
113
+ design = adh_design(panel, years)
114
+
115
+ sc = SyntheticControl()
116
+ fit = sc.fit(**design)
117
+ print(fit.nonzero()) # [('Utah', 0.335), ('Nevada', 0.236), ...]
118
+ print(fit.gap[-1]) # about -26 packs in 2000
119
+
120
+ fits = in_space_placebos(sc, **design)
121
+ p, ratios = permutation_pvalue(fits, "California")
122
+ print(p) # 0.026
123
+ ```
124
+
125
+ The API takes plain dicts and numpy arrays (unit -> outcome path,
126
+ predictor -> unit -> value), so it works on any panel, not just this one.
127
+
128
+ ## Honest limits
129
+
130
+ - Permutation inference with 39 units cannot say anything smaller than
131
+ p = 1/39. That is a property of the design, not a bug, and it is why
132
+ the paper reports exactly this number.
133
+ - The outer optimization is non-convex; different software finds slightly
134
+ different V (mine concentrates it more than the paper's table shows)
135
+ while landing on the same weights and the same gap. The reproduction
136
+ target is the weights and the effect, which are the identified objects.
137
+ - Synthetic control needs a long pre-period, a treated unit inside the
138
+ donor convex hull, and no spillovers onto donors. None of that is
139
+ checked for you; the placebo and leave-one-out tools exist so you
140
+ check it.
141
+ - One fit takes about three minutes on my 2014 CPU (the placebo sweep is
142
+ 39 of them). SLSQP in the inner loop is the cost of writing it plainly.
143
+
144
+ ## Reproduce it
145
+
146
+ ```bash
147
+ python -m venv .venv && .venv/bin/pip install numpy scipy pytest
148
+ bash data/get_data.sh # or let the loader fetch it
149
+
150
+ .venv/bin/python -m pytest tests/ -q # 6 contracts
151
+ .venv/bin/python benchmarks/repro_prop99.py # all stages, resumable
152
+ .venv/bin/python assets/make_visuals.py # the four figures
153
+ ```
154
+
155
+ ## Layout
156
+
157
+ - [okunfa/core.py](okunfa/core.py) the nested estimator: simplex QP
158
+ inside, Nelder-Mead over V outside
159
+ - [okunfa/inference.py](okunfa/inference.py) in-space placebos,
160
+ permutation p-values, leave-one-out, in-time placebo
161
+ - [okunfa/datasets.py](okunfa/datasets.py) the Prop 99 panel and the
162
+ exact ADH predictor design
163
+ - [benchmarks/repro_prop99.py](benchmarks/repro_prop99.py) the
164
+ reproduction, staged and resumable, everything into repro.json
165
+
166
+ ## Papers
167
+
168
+ - Abadie, Diamond, Hainmueller (2010), *Synthetic Control Methods for
169
+ Comparative Case Studies: Estimating the Effect of California's
170
+ Tobacco Control Program*, JASA. The method and the target result.
171
+ - Abadie & Gardeazabal (2003), *The Economic Costs of Conflict*. Where
172
+ the idea started.
173
+ - Abadie (2021), *Using Synthetic Controls: Feasibility, Data
174
+ Requirements, and Methodological Aspects*, JEL. The honest-limits
175
+ survey.
@@ -0,0 +1,8 @@
1
+ okunfa/__init__.py,sha256=kiQZ_p683h85zbMArLu5S_lPchOODUOd-dKNNcpgucc,790
2
+ okunfa/core.py,sha256=aFeQMLDQoCHK1ncN5-q2q-jwWHavrgtUvVSJYd7jWSQ,5428
3
+ okunfa/datasets.py,sha256=1VHayQGD_fgdYUaX9JwBrC7XzEovEPH0_vhF3TcGzJg,3701
4
+ okunfa/inference.py,sha256=5NoDoJugpKuRVvfdjIfm1LjZVnVc7KaSORy0rxVSV58,3195
5
+ okunfa-0.1.0.dist-info/METADATA,sha256=_gW0_YJrdruXyvRRSxPU2ZddYePIUO8Q42-XmwV2gp4,6936
6
+ okunfa-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ okunfa-0.1.0.dist-info/top_level.txt,sha256=KN06BhTyhQZrFO3S5R4F1KtOVTK5-HoR4hPp9PGV6K0,7
8
+ okunfa-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ okunfa