morphtree 2.1.0__py3-none-win_amd64.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.
- morphtree/__init__.py +28 -0
- morphtree/__main__.py +3 -0
- morphtree/_experimental/__init__.py +1 -0
- morphtree/_experimental/admission.py +364 -0
- morphtree/_experimental/base.py +215 -0
- morphtree/_experimental/component_cache_v2.py +263 -0
- morphtree/_experimental/evolution_report.py +307 -0
- morphtree/_experimental/execution_v2.py +697 -0
- morphtree/_experimental/facade.py +289 -0
- morphtree/_experimental/integrated.py +748 -0
- morphtree/_experimental/learning.py +418 -0
- morphtree/_experimental/manifest.json +204 -0
- morphtree/_experimental/numeric_fixture.py +21 -0
- morphtree/_experimental/parameter_contract.py +38 -0
- morphtree/_experimental/postfit.py +51 -0
- morphtree/_experimental/preprocessing.py +133 -0
- morphtree/_experimental/processes.py +191 -0
- morphtree/_experimental/producer.py +542 -0
- morphtree/_experimental/product.py +156 -0
- morphtree/_experimental/product_cli.py +43 -0
- morphtree/_experimental/roundtrip_v3.py +311 -0
- morphtree/_experimental/runtime_io.py +65 -0
- morphtree/_experimental/sampling.py +26 -0
- morphtree/_experimental/support.py +281 -0
- morphtree/_experimental/target.py +59 -0
- morphtree/_experimental/training.py +53 -0
- morphtree/_experimental/worker.py +682 -0
- morphtree/_installed.py +17 -0
- morphtree/_native/__init__.py +1 -0
- morphtree/_native/bin/win_amd64/morphtree_kernels.dll +0 -0
- morphtree/_native/buffers.py +82 -0
- morphtree/_native/loader.py +39 -0
- morphtree/_native/manifest.json +29 -0
- morphtree/_native/runtime.py +48 -0
- morphtree/_native/src/histogram.c +61 -0
- morphtree/_native/src/traversal.c +51 -0
- morphtree/api.py +388 -0
- morphtree/api_compat.py +65 -0
- morphtree/backend.py +162 -0
- morphtree/booster.py +252 -0
- morphtree/cli.py +52 -0
- morphtree/config.py +262 -0
- morphtree/control.py +20 -0
- morphtree/data.py +300 -0
- morphtree/diagnostics.py +158 -0
- morphtree/estimators.py +156 -0
- morphtree/examples.py +25 -0
- morphtree/exporting.py +224 -0
- morphtree/legacy_artifacts.py +32 -0
- morphtree/metrics.py +156 -0
- morphtree/model.py +1010 -0
- morphtree/mutations.py +205 -0
- morphtree/native_lab_backend.py +79 -0
- morphtree/numpy_backend.py +180 -0
- morphtree/optuna_tuner.py +154 -0
- morphtree/persistence.py +414 -0
- morphtree/prediction_output.py +35 -0
- morphtree/prepared_bins.py +82 -0
- morphtree/public.py +106 -0
- morphtree/resource_plan.py +170 -0
- morphtree/scalable.py +781 -0
- morphtree/scalable_backend.py +74 -0
- morphtree/scalable_checkpoint.py +129 -0
- morphtree/scalable_data.py +255 -0
- morphtree/scalable_receipts.py +92 -0
- morphtree/scalable_target.py +68 -0
- morphtree/scalable_variants.py +133 -0
- morphtree/sklearn_view.py +79 -0
- morphtree/source_markers.py +25 -0
- morphtree/species.py +47 -0
- morphtree/temporary.py +65 -0
- morphtree/training_fragment.py +130 -0
- morphtree/tree.py +231 -0
- morphtree/tuning.py +150 -0
- morphtree-2.1.0.dist-info/METADATA +212 -0
- morphtree-2.1.0.dist-info/RECORD +79 -0
- morphtree-2.1.0.dist-info/WHEEL +4 -0
- morphtree-2.1.0.dist-info/entry_points.txt +2 -0
- morphtree-2.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Pure Python native category-equality splits; never order nominal codes.
|
|
2
|
+
|
|
3
|
+
Numeric operations delegate unchanged to the qualified reference backend.
|
|
4
|
+
Missing and unseen nominal values have separate explicit codes. Each node tests
|
|
5
|
+
one category versus the remainder; arbitrary subsets/target ordering are absent.
|
|
6
|
+
"""
|
|
7
|
+
from array import array
|
|
8
|
+
from bisect import bisect_right
|
|
9
|
+
import math
|
|
10
|
+
from .backend import PythonBackend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Column(array):
|
|
14
|
+
def __new__(cls,kind,values,nominal=False):return super().__new__(cls,kind,values)
|
|
15
|
+
def __init__(self,kind,values,nominal=False):self.nominal=nominal
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Hist(list):
|
|
19
|
+
def __init__(self,values,nominal=False):super().__init__(values);self.nominal=nominal
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ScalableBackend(PythonBackend):
|
|
23
|
+
name='private-python-columnar-nominal'
|
|
24
|
+
def __init__(self,nominal_sizes=None):self.nominal_sizes=nominal_sizes or {}
|
|
25
|
+
def bin(self,binner,X):
|
|
26
|
+
if not hasattr(X,'_compact_columns'):return super().bin(binner,X)
|
|
27
|
+
out=[]
|
|
28
|
+
for j,cuts in enumerate(binner.thresholds):
|
|
29
|
+
if j in self.nominal_sizes:out.append(Column('I',X.column(j),True))
|
|
30
|
+
else:out.append(Column('B' if binner.max_bins<=255 else 'H',(0 if math.isnan(v) else bisect_right(cuts,v)+1 for v in X.column(j))))
|
|
31
|
+
return out
|
|
32
|
+
def node_statistics(self,Xb,ids,g,h,u,features,need_missing,need_uncertainty):
|
|
33
|
+
if not need_missing or not self.nominal_sizes:
|
|
34
|
+
return super().node_statistics(Xb,ids,g,h,u,features,need_missing,need_uncertainty)
|
|
35
|
+
# Numeric missing is bin0; native nominal missing is fitted k+1.
|
|
36
|
+
# Unseen nominal k+2 is separate and must not trigger this local gate.
|
|
37
|
+
columns=tuple((Xb[f],self.nominal_sizes[f]+1 if f in self.nominal_sizes else 0) for f in features)
|
|
38
|
+
missing=sum(any(column[i]==code for column,code in columns) for i in ids)/max(len(ids),1)
|
|
39
|
+
G,H,uncertainty,_=super().node_statistics(Xb,ids,g,h,u,features,False,need_uncertainty)
|
|
40
|
+
return G,H,uncertainty,missing
|
|
41
|
+
def histogram(self,column,*args):return Hist(super().histogram(column,*args),getattr(column,'nominal',False))
|
|
42
|
+
def subtract(self,parent,child):return Hist(super().subtract(parent,child),getattr(parent,'nominal',False))
|
|
43
|
+
def fast_split(self,hist,total,depth,genome,params):
|
|
44
|
+
if not getattr(hist,'nominal',False):return super().fast_split(hist,total,depth,genome,params)
|
|
45
|
+
if genome.split_program not in (('OBJECTIVE_GAIN',),('OBJECTIVE_GAIN','BALANCE','MUL')):raise ValueError('Unsupported private nominal split program')
|
|
46
|
+
n,G,H=total[:3];counts,gs,hs=hist[:3];l1=params.l1
|
|
47
|
+
parent_l2=params.l2*(1+(params.depth_reg_strength*depth if genome.regularization_policy=='depth_scaled' else 0))
|
|
48
|
+
child_l2=params.l2*(1+(params.depth_reg_strength*(depth+1) if genome.regularization_policy=='depth_scaled' else 0))
|
|
49
|
+
def score(g,h,l2):return max(abs(g)-l1,0.)**2/max(h+l2,1e-12)
|
|
50
|
+
parent=score(G,H,parent_l2);best=None
|
|
51
|
+
for code in range(1,len(counts)):
|
|
52
|
+
ln,lg,lh=counts[code],gs[code],hs[code];rn,rg,rh=n-ln,G-lg,H-lh
|
|
53
|
+
if min(ln,rn)<params.min_samples_leaf or min(lh,rh)<params.min_hessian:continue
|
|
54
|
+
gain=.5*(score(lg,lh,child_l2)+score(rg,rh,child_l2)-parent)
|
|
55
|
+
if len(genome.split_program)>1:gain*=2*min(ln,rn)/n
|
|
56
|
+
if math.isfinite(gain) and gain>params.min_gain and (best is None or round(gain,12)>round(best[0],12)):best=(gain,code,True)
|
|
57
|
+
return best
|
|
58
|
+
def partition(self,column,ids,cut,missing_left):
|
|
59
|
+
if not getattr(column,'nominal',False):return super().partition(column,ids,cut,missing_left)
|
|
60
|
+
left=[];right=[]
|
|
61
|
+
for i in ids:(left if column[i]==cut else right).append(i)
|
|
62
|
+
return left,right
|
|
63
|
+
def predict_tree(self,nodes,Xb):
|
|
64
|
+
out=[]
|
|
65
|
+
for i in range(len(Xb[0])):
|
|
66
|
+
k=0
|
|
67
|
+
for _ in range(len(nodes)+1):
|
|
68
|
+
node=nodes[k]
|
|
69
|
+
if node.is_leaf:out.append(node.value);break
|
|
70
|
+
col=Xb[node.feature];b=col[i]
|
|
71
|
+
left=(b==node.threshold_bin) if getattr(col,'nominal',False) else node.missing_left if b==0 else b<=node.threshold_bin
|
|
72
|
+
k=node.left if left else node.right
|
|
73
|
+
else:raise ValueError('Invalid cyclic native tree')
|
|
74
|
+
return out
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Local atomic fitted-state checkpoints. Recovery never resumes training."""
|
|
2
|
+
import copy
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
from .persistence import atomic_text, decode_json_bytes, LoadLimits, read_regular_bytes
|
|
11
|
+
from .scalable_receipts import verify
|
|
12
|
+
from .temporary import directory as bounded_directory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _safe_directory(path):
|
|
16
|
+
path = Path(path)
|
|
17
|
+
if path.is_symlink() or getattr(path, 'is_junction', lambda: False)():
|
|
18
|
+
raise ValueError('Checkpoint directory cannot be a link')
|
|
19
|
+
if not path.is_dir():
|
|
20
|
+
raise ValueError('Checkpoint parent directory must exist')
|
|
21
|
+
return path.resolve()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _workspace(value, resources):
|
|
25
|
+
estimate = 8192
|
|
26
|
+
pending = [iter((value,))]
|
|
27
|
+
count = 0
|
|
28
|
+
while pending:
|
|
29
|
+
try:
|
|
30
|
+
node = next(pending[-1])
|
|
31
|
+
except StopIteration:
|
|
32
|
+
pending.pop()
|
|
33
|
+
continue
|
|
34
|
+
count += 1
|
|
35
|
+
estimate += 512
|
|
36
|
+
if isinstance(node, str):
|
|
37
|
+
estimate += len(node) * 32
|
|
38
|
+
elif isinstance(node, dict):
|
|
39
|
+
pending.append(iter(node.items()))
|
|
40
|
+
elif isinstance(node, (list, tuple)):
|
|
41
|
+
pending.append(iter(node))
|
|
42
|
+
if count % 128 == 0:
|
|
43
|
+
resources.reserve(estimate, 'Durable checkpoint workspace')
|
|
44
|
+
resources.reserve(estimate, 'Durable checkpoint workspace')
|
|
45
|
+
return estimate
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def commit(estimator, model, classes, resources, *, final):
|
|
49
|
+
from .scalable import digest
|
|
50
|
+
if estimator.checkpoint_dir is None:
|
|
51
|
+
return None
|
|
52
|
+
report = estimator.last_attempt_
|
|
53
|
+
events = report['events']
|
|
54
|
+
verify(events)
|
|
55
|
+
if final:
|
|
56
|
+
if report['status'] != 'COMPLETED' or events[-1]['event'] != 'FINAL_MODEL':
|
|
57
|
+
raise ValueError('Final checkpoint requires completed finalization')
|
|
58
|
+
elif digest(model) != report['last_verified_anchor']['model_sha256']:
|
|
59
|
+
raise ValueError('Checkpoint must match verified TRAIN anchor')
|
|
60
|
+
wanted = {event['parent_sha256'] for event in events if event['event'] == 'DIAGNOSIS'}
|
|
61
|
+
evidence = getattr(estimator, 'diagnostic_profiles_', {})
|
|
62
|
+
resources.reserve(8192, 'Refit configuration header before copying')
|
|
63
|
+
constructor = estimator._constructor_state()
|
|
64
|
+
template = dict(model=model, classes=classes, constructor=constructor, constructor_sha256=digest(constructor), report={k: v for k, v in report.items() if k != 'checkpoint' and (final or k != 'selection_checkpoint')}, evidence=dict(diagnostic_profiles={key: evidence[key] for key in wanted}))
|
|
65
|
+
estimate = _workspace(template, resources)
|
|
66
|
+
packet = copy.deepcopy(template)
|
|
67
|
+
packet['schema'] = 'SCALABLE_PRIVATE_MODEL_3'
|
|
68
|
+
if not final:
|
|
69
|
+
packet['report']['status'] = 'PRESERVED_VERIFIED_TRAIN_ANCHOR_NOT_FINAL_REFIT'
|
|
70
|
+
packet['receipt_sha256'] = digest(packet['report'])
|
|
71
|
+
text = json.dumps(packet, sort_keys=True, allow_nan=False) + '\n'
|
|
72
|
+
resources.reserve(estimate + len(text) * 8, 'Encoded durable checkpoint workspace')
|
|
73
|
+
directory = getattr(estimator, '_active_checkpoint_dir_', None)
|
|
74
|
+
if directory is None:
|
|
75
|
+
if estimator.checkpoint_dir == 'auto':
|
|
76
|
+
parent = _safe_directory(tempfile.gettempdir()) / 'morphtree-checkpoints'
|
|
77
|
+
parent.mkdir(exist_ok=True)
|
|
78
|
+
else:
|
|
79
|
+
parent = Path(estimator.checkpoint_dir)
|
|
80
|
+
parent = _safe_directory(parent)
|
|
81
|
+
directory = Path(bounded_directory(prefix='fit-', dir=parent))
|
|
82
|
+
estimator._active_checkpoint_dir_ = directory
|
|
83
|
+
estimator._checkpoint_sequence_ = 0
|
|
84
|
+
directory = _safe_directory(directory)
|
|
85
|
+
report['checkpoint_directory'] = str(directory)
|
|
86
|
+
if shutil.disk_usage(directory).free < len(text) * 4 + 8192:
|
|
87
|
+
raise OSError('Insufficient free disk for atomic checkpoint; previous model preserved')
|
|
88
|
+
name = f"model-{estimator._checkpoint_sequence_:04d}.json"
|
|
89
|
+
target = directory / name
|
|
90
|
+
if target.exists():
|
|
91
|
+
raise FileExistsError('An existing fitted checkpoint cannot be overwritten')
|
|
92
|
+
atomic_text(target, text)
|
|
93
|
+
record = dict(schema='SCALABLE_CHECKPOINT_POINTER_1', path=name, sha256=hashlib.sha256(text.encode('ascii')).hexdigest(), call_id=events[0]['call_id'], source_role='TRAIN_FULL' if final and estimator.refit_full else 'TRAIN', final=final)
|
|
94
|
+
if not final:
|
|
95
|
+
atomic_text(directory / 'SELECTED.json', json.dumps(record, sort_keys=True) + '\n')
|
|
96
|
+
atomic_text(directory / 'CURRENT.json', json.dumps(record, sort_keys=True) + '\n')
|
|
97
|
+
estimator._checkpoint_sequence_ += 1
|
|
98
|
+
receipt = dict(directory=str(directory), **record)
|
|
99
|
+
report['checkpoint'] = receipt
|
|
100
|
+
if not final:
|
|
101
|
+
report['selection_checkpoint'] = receipt
|
|
102
|
+
if final:
|
|
103
|
+
estimator.checkpoint_path_ = str(target)
|
|
104
|
+
return receipt
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def read_pointer(directory, resources, *, stage='current'):
|
|
108
|
+
if type(stage) is not str or stage not in ('current', 'selected'):
|
|
109
|
+
raise ValueError('Checkpoint stage must be current or selected')
|
|
110
|
+
directory = _safe_directory(directory)
|
|
111
|
+
pointer = directory / ('CURRENT.json' if stage == 'current' else 'SELECTED.json')
|
|
112
|
+
if pointer.is_symlink():
|
|
113
|
+
raise ValueError('Checkpoint pointer cannot be a link')
|
|
114
|
+
raw = read_regular_bytes(pointer,8192)
|
|
115
|
+
record = decode_json_bytes(raw, LoadLimits(max_bytes=8192), reserve=resources.reserve)
|
|
116
|
+
required = {'schema', 'path', 'sha256', 'call_id', 'source_role', 'final'}
|
|
117
|
+
if set(record) != required or record['schema'] != 'SCALABLE_CHECKPOINT_POINTER_1':
|
|
118
|
+
raise ValueError('Exact checkpoint pointer schema required')
|
|
119
|
+
name = record['path']
|
|
120
|
+
if type(name) is not str or len(name) != 15 or not name.startswith('model-') or not name.endswith('.json') or not name[6:10].isdigit():
|
|
121
|
+
raise ValueError('Checkpoint pointer must name a local numbered model')
|
|
122
|
+
if type(record['sha256']) is not str or len(record['sha256']) != 64 or any(c not in '0123456789abcdef' for c in record['sha256']) or type(record['call_id']) is not str or not record['call_id'] or type(record['final']) is not bool or record['source_role'] not in ('TRAIN', 'TRAIN_FULL') or not record['final'] and record['source_role'] != 'TRAIN':
|
|
123
|
+
raise ValueError('Invalid checkpoint identity or source role')
|
|
124
|
+
if stage == 'selected' and record['final']:
|
|
125
|
+
raise ValueError('Selected checkpoint must be a verified TRAIN anchor')
|
|
126
|
+
target = directory / name
|
|
127
|
+
if target.is_symlink() or target.resolve().parent != directory:
|
|
128
|
+
raise ValueError('Checkpoint model must be a local regular file')
|
|
129
|
+
return target, record
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Private scalable representation. No whole DataFrame/ndarray list conversion.
|
|
2
|
+
|
|
3
|
+
Source dictionaries encode input storage only. Learner vocabularies are fitted
|
|
4
|
+
again on each TRAIN/fold; SEARCH-only categories become unknown at validation.
|
|
5
|
+
"""
|
|
6
|
+
from array import array
|
|
7
|
+
from bisect import bisect_right
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import os
|
|
12
|
+
from numbers import Real
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from .source_markers import controlled_source
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def scalar(v,*,preserve_integral=False):
|
|
19
|
+
kind=type(v)
|
|
20
|
+
if kind is str or v is None:return v
|
|
21
|
+
if kind is int and preserve_integral:return v
|
|
22
|
+
if kind is float or kind is int or kind is bool:
|
|
23
|
+
f=float(v)
|
|
24
|
+
if math.isnan(f):return None
|
|
25
|
+
if math.isfinite(f):return f
|
|
26
|
+
raise ValueError('Finite numeric/string category/None/NaN/nullable scalar required')
|
|
27
|
+
if type(v).__module__.split('.')[0]=='numpy' and hasattr(v,'item'):v=v.item()
|
|
28
|
+
if v is None or (sys.modules.get('pandas') is not None and v is getattr(sys.modules['pandas'],'NA',None)):return None
|
|
29
|
+
if preserve_integral and type(v) is int:return v
|
|
30
|
+
if isinstance(v,Real):
|
|
31
|
+
f=float(v)
|
|
32
|
+
if math.isnan(f):return None
|
|
33
|
+
if math.isfinite(f):return f
|
|
34
|
+
if isinstance(v,str):return v
|
|
35
|
+
raise ValueError('Finite numeric/string category/None/NaN/nullable scalar required')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def category_key(v):
|
|
39
|
+
if not isinstance(v,str):
|
|
40
|
+
if type(v) is int:v=v if abs(v)>2**53 else float(v)
|
|
41
|
+
else:
|
|
42
|
+
v=float(v)
|
|
43
|
+
if abs(v)>2**53 and v.is_integer():v=int(v)
|
|
44
|
+
if v==0:v=0.0
|
|
45
|
+
return json.dumps(['s' if isinstance(v,str) else 'n',v],ensure_ascii=False,allow_nan=False,separators=(',',':'))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def canonical_category_key(key):
|
|
49
|
+
"""Exact numeric identity, including compatible legacy float key spelling."""
|
|
50
|
+
entry=json.loads(key)
|
|
51
|
+
if type(entry) is not list or len(entry)!=2:raise ValueError('Malformed typed category')
|
|
52
|
+
tag,value=entry
|
|
53
|
+
if not (tag=='s' and type(value) is str or tag=='n' and type(value) in (int,float)):raise ValueError('Malformed typed category')
|
|
54
|
+
value=scalar(value,preserve_integral=True)
|
|
55
|
+
if value is None:raise ValueError('Missing value is not a vocabulary entry')
|
|
56
|
+
canonical=category_key(value)
|
|
57
|
+
try:legacy=json.dumps([tag,value if tag=='s' else float(value)],ensure_ascii=False,allow_nan=False,separators=(',',':'))
|
|
58
|
+
except OverflowError:legacy=None
|
|
59
|
+
if key not in (canonical,legacy):raise ValueError('Noncanonical typed category')
|
|
60
|
+
return canonical
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def resident_bytes():
|
|
64
|
+
"""Read-only OS accounting; no learner arithmetic outside Python."""
|
|
65
|
+
try:
|
|
66
|
+
if os.name=='nt':
|
|
67
|
+
import ctypes
|
|
68
|
+
class Counters(ctypes.Structure):_fields_=[('cb',ctypes.c_ulong),('faults',ctypes.c_ulong)]+[(n,ctypes.c_size_t) for n in ('peak','rss','peak_paged','paged','peak_nonpaged','nonpaged','pagefile','peak_pagefile')]
|
|
69
|
+
c=Counters();c.cb=ctypes.sizeof(c)
|
|
70
|
+
if not ctypes.WinDLL('psapi',use_last_error=True).GetProcessMemoryInfo(ctypes.c_void_p(-1),ctypes.byref(c),c.cb):return None
|
|
71
|
+
return c.rss
|
|
72
|
+
with open('/proc/self/statm',encoding='ascii') as handle:
|
|
73
|
+
resident_pages=int(handle.read().split()[1])
|
|
74
|
+
return resident_pages*os.sysconf('SC_PAGE_SIZE')
|
|
75
|
+
except (OSError,ValueError,AttributeError):return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Input:
|
|
79
|
+
"""Inspect metadata first, then access one column or bounded block."""
|
|
80
|
+
def __init__(self,X):
|
|
81
|
+
if controlled_source(X):raise ValueError('Controlled sources cannot use ordinary input or inference')
|
|
82
|
+
self.X=X;self.frame=hasattr(X,'columns') and hasattr(X,'iloc');self.fitted_kinds=None
|
|
83
|
+
if hasattr(X,'format') and hasattr(X,'tocsr'):raise ValueError('Sparse input requires a separate qualified path')
|
|
84
|
+
if hasattr(X,'shape'):
|
|
85
|
+
if len(X.shape)!=2:raise ValueError('X must have exactly two dimensions')
|
|
86
|
+
self.n,self.width=map(int,X.shape)
|
|
87
|
+
elif isinstance(X,(list,tuple)):
|
|
88
|
+
self.n=len(X);self.width=len(X[0]) if self.n and isinstance(X[0],(list,tuple,array)) else 0
|
|
89
|
+
if any(not isinstance(r,(list,tuple,array)) or len(r)!=self.width for r in X):raise ValueError('Rectangular rows required')
|
|
90
|
+
else:raise ValueError('Supported dense table, ndarray or DataFrame required')
|
|
91
|
+
if self.n<1 or self.width<1:raise ValueError('X must contain rows and columns')
|
|
92
|
+
self.names=tuple(X.columns) if self.frame else None
|
|
93
|
+
if self.names is not None and (len(set(self.names))!=self.width or any(not isinstance(n,str) or not n for n in self.names)):raise ValueError('Unique nonempty string names required')
|
|
94
|
+
def column(self,j,start=0,stop=None):
|
|
95
|
+
stop=self.n if stop is None else stop
|
|
96
|
+
if self.frame:return iter(self.X.iloc[start:stop,j].array)
|
|
97
|
+
if hasattr(self.X,'shape'):return iter(self.X[start:stop,j])
|
|
98
|
+
return (self.X[i][j] for i in range(start,stop))
|
|
99
|
+
def forced_categorical(self,j):return self.fitted_kinds[j]=='categorical' if self.fitted_kinds is not None else self.frame and str(self.X.dtypes.iloc[j])=='category'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Resources:
|
|
103
|
+
def __init__(self,bytes_limit,seconds,check=None):
|
|
104
|
+
if type(bytes_limit) is not int or bytes_limit<1 or not math.isfinite(seconds) or seconds<=0:raise ValueError('Positive finite resource allowance')
|
|
105
|
+
self.bytes_limit=bytes_limit;self.started=time.monotonic();self.deadline=self.started+seconds;self.external=check;self.base_rss=resident_bytes();self.peak_rss=self.base_rss;self.last_memory_check=0.;self.pressure_reader=None;self.base_commit=None;self.peak_commit=None
|
|
106
|
+
def configure_time(self,seconds):
|
|
107
|
+
if type(seconds) not in (int,float) or not math.isfinite(seconds) or seconds<=0:raise ValueError('Positive finite resource allowance')
|
|
108
|
+
self.deadline=self.started+seconds
|
|
109
|
+
def check(self):
|
|
110
|
+
if time.monotonic()>self.deadline:raise TimeoutError('Finite scalable operation deadline')
|
|
111
|
+
if time.monotonic()-self.last_memory_check>.05:
|
|
112
|
+
self.last_memory_check=time.monotonic();rss=resident_bytes()
|
|
113
|
+
if rss is not None:
|
|
114
|
+
self.peak_rss=max(self.peak_rss or 0,rss)
|
|
115
|
+
if self.base_rss is not None and rss-self.base_rss>self.bytes_limit:raise MemoryError('Observed process RSS increase exceeds operation allowance; no rows were sampled')
|
|
116
|
+
if self.pressure_reader:
|
|
117
|
+
observation=self.pressure_reader();commit=observation.get('process_private_bytes')
|
|
118
|
+
if commit is not None:
|
|
119
|
+
self.peak_commit=max(self.peak_commit or 0,commit)
|
|
120
|
+
if self.base_commit is not None and commit-self.base_commit>self.bytes_limit:raise MemoryError('Observed process committed-memory increase exceeds operation allowance; no rows were sampled')
|
|
121
|
+
if observation['effective_available_bytes']<=0:raise MemoryError('Active RAM/commit/address-space headroom exhausted; no rows were sampled')
|
|
122
|
+
if self.external:self.external()
|
|
123
|
+
def reserve(self,n,description):
|
|
124
|
+
if n>self.bytes_limit:raise MemoryError(f'{description} requires estimated {n} bytes, allowance {self.bytes_limit}; no rows were sampled')
|
|
125
|
+
self.check()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Rows(Sequence):
|
|
129
|
+
_compact_columns=True
|
|
130
|
+
def __len__(self):return self.n
|
|
131
|
+
def __getitem__(self,i):
|
|
132
|
+
if isinstance(i,slice):return [self[j] for j in range(*i.indices(self.n))]
|
|
133
|
+
if i<0:i+=self.n
|
|
134
|
+
if not 0<=i<self.n:raise IndexError(i)
|
|
135
|
+
return Row(self,i)
|
|
136
|
+
def column(self,j):return (self.value(i,j) for i in range(self.n))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Row(Sequence):
|
|
140
|
+
def __init__(self,table,i):self.table=table;self.i=i
|
|
141
|
+
def __len__(self):return self.table.width
|
|
142
|
+
def __getitem__(self,j):
|
|
143
|
+
if isinstance(j,slice):return [self[k] for k in range(*j.indices(len(self)))]
|
|
144
|
+
if j<0:j+=len(self)
|
|
145
|
+
if not 0<=j<len(self):raise IndexError(j)
|
|
146
|
+
return self.table.value(self.i,j)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Table(Rows):
|
|
150
|
+
def __init__(self,columns,kinds,vocab,n,names,indices=None):
|
|
151
|
+
self.columns,self.kinds,self.vocab,self.n,self.names,self.indices=columns,kinds,vocab,n,names,indices;self.width=len(columns)
|
|
152
|
+
@classmethod
|
|
153
|
+
def read(cls,source,resources,start=0,stop=None):
|
|
154
|
+
source=source if isinstance(source,Input) else Input(source);stop=source.n if stop is None else stop;n=stop-start
|
|
155
|
+
if not 0<=start<stop<=source.n:raise ValueError('Nonempty bounded block')
|
|
156
|
+
name_bytes=sum(sys.getsizeof(name)+8*len(name) for name in source.names or ())
|
|
157
|
+
base_bytes=n*source.width*10+source.width*256+name_bytes
|
|
158
|
+
resources.reserve(base_bytes,'Compact source columns and schema names')
|
|
159
|
+
columns=[];kinds=[];vocab=[];vocab_bytes=0
|
|
160
|
+
for j in range(source.width):
|
|
161
|
+
resources.check();first=None
|
|
162
|
+
for i,value in enumerate(source.column(j,start,stop)):
|
|
163
|
+
if i%4096==0:resources.check()
|
|
164
|
+
if isinstance(value,str):resources.reserve(base_bytes+vocab_bytes+len(value)*32+512,'Initial scalar/JSON key workspace')
|
|
165
|
+
first=scalar(value)
|
|
166
|
+
if first is not None:break
|
|
167
|
+
forced=source.forced_categorical(j);cat=forced or isinstance(first,str);a=array('I' if cat else 'd');lookup={};words=[]
|
|
168
|
+
for i,v in enumerate(source.column(j,start,stop)):
|
|
169
|
+
if i%4096==0:resources.check()
|
|
170
|
+
if isinstance(v,str):resources.reserve(base_bytes+vocab_bytes+len(v)*32+512,'Scalar/JSON key workspace before allocation')
|
|
171
|
+
v=scalar(v,preserve_integral=cat)
|
|
172
|
+
if cat:
|
|
173
|
+
if v is None:a.append(0);continue
|
|
174
|
+
if not forced and not isinstance(v,str):raise ValueError('Mixed string/numeric column')
|
|
175
|
+
key=category_key(v)
|
|
176
|
+
if key not in lookup:
|
|
177
|
+
vocab_bytes+=sys.getsizeof(key)+96;resources.reserve(base_bytes+vocab_bytes,'Source columns and category dictionaries')
|
|
178
|
+
lookup[key]=len(words)+1;words.append(key)
|
|
179
|
+
a.append(lookup[key])
|
|
180
|
+
else:
|
|
181
|
+
if isinstance(v,str):raise ValueError('Mixed string/numeric column')
|
|
182
|
+
a.append(math.nan if v is None else v)
|
|
183
|
+
columns.append(a);kinds.append('categorical' if cat else 'numeric');vocab.append(tuple(words))
|
|
184
|
+
return cls(tuple(columns),tuple(kinds),tuple(vocab),n,source.names)
|
|
185
|
+
def take(self,indices):
|
|
186
|
+
ids=array('I' if len(self.columns[0])<2**32 else 'Q',(self.indices[i] if self.indices is not None else i for i in indices))
|
|
187
|
+
return Table(self.columns,self.kinds,self.vocab,len(ids),self.names,ids)
|
|
188
|
+
def value(self,i,j):return self.columns[j][self.indices[i] if self.indices is not None else i]
|
|
189
|
+
def column(self,j):
|
|
190
|
+
c=self.columns[j]
|
|
191
|
+
return iter(c) if self.indices is None else (c[i] for i in self.indices)
|
|
192
|
+
@property
|
|
193
|
+
def storage_bytes(self):return sum(sys.getsizeof(c) for c in self.columns)+sum(sys.getsizeof(s) for v in self.vocab for s in v)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Encoder:
|
|
197
|
+
"""One native nominal feature per category column, TRAIN-only vocabulary."""
|
|
198
|
+
def fit(self,table):
|
|
199
|
+
self.names=table.names;self.columns=[]
|
|
200
|
+
for j,kind in enumerate(table.kinds):
|
|
201
|
+
if kind=='numeric':self.columns.append(dict(kind='numeric'));continue
|
|
202
|
+
observed=set(table.column(j));vocab=sorted({canonical_category_key(table.vocab[j][i-1]) for i in observed if i})
|
|
203
|
+
self.columns.append(dict(kind='categorical',vocabulary=vocab))
|
|
204
|
+
return self
|
|
205
|
+
def state(self):return dict(schema='SCALABLE_TRAIN_ENCODER_1',names=list(self.names) if self.names is not None else None,columns=self.columns)
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_state(cls,state,resources):
|
|
208
|
+
if type(state) is not dict or set(state)!={'schema','names','columns'} or state['schema']!='SCALABLE_TRAIN_ENCODER_1':raise ValueError('Exact private encoder schema')
|
|
209
|
+
cols=state['columns'];names=state['names']
|
|
210
|
+
if type(cols) is not list or not cols:raise ValueError('Nonempty column schema')
|
|
211
|
+
if names is not None and (type(names) is not list or len(names)!=len(cols) or len(set(names))!=len(names) or any(type(n) is not str or not n for n in names)):raise ValueError('Invalid encoder names')
|
|
212
|
+
schema_bytes=len(cols)*256+sum(sys.getsizeof(name)+8*len(name) for name in names or ())
|
|
213
|
+
resources.reserve(schema_bytes,'Decoded encoder metadata before copies')
|
|
214
|
+
for c in cols:
|
|
215
|
+
if c==dict(kind='numeric'):continue
|
|
216
|
+
if type(c) is not dict or set(c)!={'kind','vocabulary'} or c['kind']!='categorical':raise ValueError('Invalid category schema')
|
|
217
|
+
v=c['vocabulary']
|
|
218
|
+
if type(v) is not list or any(type(s) is not str for s in v) or any(v[i]>=v[i+1] for i in range(len(v)-1)):raise ValueError('Canonical typed vocabulary required')
|
|
219
|
+
schema_bytes+=len(v)*128;canonical=set()
|
|
220
|
+
for key in v:
|
|
221
|
+
schema_bytes+=sys.getsizeof(key)*3
|
|
222
|
+
resources.reserve(schema_bytes+len(key)*32+512,'Typed vocabulary validation before JSON allocation')
|
|
223
|
+
normalized=canonical_category_key(key)
|
|
224
|
+
if normalized in canonical:raise ValueError('Duplicate equivalent numeric categories')
|
|
225
|
+
canonical.add(normalized)
|
|
226
|
+
obj=cls();obj.names=tuple(names) if names is not None else None;obj.columns=cols;return obj
|
|
227
|
+
def transform(self,table):
|
|
228
|
+
if self.names is None:
|
|
229
|
+
if table.names is not None:raise ValueError('Positional TRAIN requires positional prediction')
|
|
230
|
+
order=list(range(table.width))
|
|
231
|
+
else:
|
|
232
|
+
if table.names is None or set(table.names)!=set(self.names):raise ValueError('Feature schema mismatch')
|
|
233
|
+
positions={name:i for i,name in enumerate(table.names)};order=[positions[n] for n in self.names]
|
|
234
|
+
if len(order)!=len(self.columns):raise ValueError('Column count mismatch')
|
|
235
|
+
maps={};nominal={}
|
|
236
|
+
for j,(source,c) in enumerate(zip(order,self.columns)):
|
|
237
|
+
if c['kind']!=table.kinds[source]:
|
|
238
|
+
# An all-missing prediction column has no inferred string type.
|
|
239
|
+
if not(c['kind']=='categorical' and all(math.isnan(v) for v in table.column(source))):raise ValueError('Feature type mismatch')
|
|
240
|
+
if c['kind']=='categorical':
|
|
241
|
+
lookup={canonical_category_key(key):i+1 for i,key in enumerate(c['vocabulary'])};k=len(lookup);nominal[j]=k
|
|
242
|
+
maps[j]=[k+1]+[lookup.get(canonical_category_key(key),k+2) for key in table.vocab[source]]
|
|
243
|
+
return Encoded(table,order,maps,nominal)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Encoded(Rows):
|
|
247
|
+
def __init__(self,table,order,maps,nominal):self.source=table;self.order=order;self.maps=maps;self._nominal_sizes=nominal;self.n=table.n;self.width=len(order)
|
|
248
|
+
def value(self,i,j):
|
|
249
|
+
v=self.source.value(i,self.order[j])
|
|
250
|
+
return (self.maps[j][0] if self.source.kinds[self.order[j]]=='numeric' else self.maps[j][int(v)]) if j in self.maps else v
|
|
251
|
+
def column(self,j):
|
|
252
|
+
values=self.source.column(self.order[j])
|
|
253
|
+
if j not in self.maps:return values
|
|
254
|
+
if self.source.kinds[self.order[j]]=='numeric':return (self.maps[j][0] for _ in values)
|
|
255
|
+
return (self.maps[j][int(v)] for v in values)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Compact operation custody, not an authentication or scientific-usefulness claim.
|
|
2
|
+
|
|
3
|
+
Dataset buffers stay outside JSON. Digests bind typed source columns, schema,
|
|
4
|
+
ordered views and actual labels. Call-local completed-model caches never admit
|
|
5
|
+
foreign persisted results; chain verification does not authorize budget replay.
|
|
6
|
+
"""
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
MAX_OPERATION_BYTES=1024*1024
|
|
13
|
+
ENVELOPE={'receipt_schema','call_id','sequence','previous','sha256'}
|
|
14
|
+
FIELDS={
|
|
15
|
+
'CALL_STARTED':{'engine_sha256','scope','requested_task'},
|
|
16
|
+
'RESOURCE_PLAN':{'plan'},
|
|
17
|
+
'INPUT_PREPARED':{'rows','features','seconds','storage_bytes','input_identity'},
|
|
18
|
+
'NATIVE_FIT_STARTED':{'ordinal','phase','rows','source_rows','family','cache_key','component_key','effective_rows_sha256','effective_labels_sha256','seed','params_sha256','preprocessing_sha256','program_sha256','role','reserved_scalar_trees'},
|
|
19
|
+
'NATIVE_FIT_COMPLETED':{'ordinal','phase','seconds','component_key','actual_scalar_trees'},
|
|
20
|
+
'CACHE_HIT':{'phase','family','rows','cache_key'},
|
|
21
|
+
'SCREEN_COMPLETED':{'losses','parent','seconds'},
|
|
22
|
+
'OOF_FOLD':{'parent_sha256','program_sha256','fit_rows','held_rows','disjoint','encoder_sha256','fit_indices_sha256','held_indices_sha256'},
|
|
23
|
+
'DIAGNOSIS':{'parent_sha256','task','OOF_primary','fold_rows','TRAIN_only','fold_encoder_local','features_examined','region_count','profile_sha256','causal_status','seconds'},
|
|
24
|
+
'MUTATION':{'generation','parent_sha256','child_sha256','child_program','parent_SEARCH','child_SEARCH','decision','seconds'},
|
|
25
|
+
'PROGRAM_FROZEN':{'genome_sha256'},
|
|
26
|
+
'OPTUNA_ASK':{'number','genome_sha256'},
|
|
27
|
+
'OPTUNA_COMPLETE':{'number','SEARCH','parameters'},
|
|
28
|
+
'BIN_PREPARATION_CACHE':{'limit_bytes','retained_bytes','entries','hits','misses','bypassed','evictions','peak_retained_bytes','reused_rows','preparation_seconds','integrity_seconds'},
|
|
29
|
+
'FINAL_MODEL':{'input_rows','selection_TRAIN_rows','SEARCH_rows','final_fit_source_rows','refit_full','SEARCH_primary_before_refit','selection_metric_is_independent','nominal_columns','source_storage_bytes','all_rows_accounted'},
|
|
30
|
+
'FIT_STOPPED':{'error_type'},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def canonical(v):
|
|
35
|
+
raw=json.dumps(v,sort_keys=True,allow_nan=False,separators=(',',':')).encode('utf8')
|
|
36
|
+
if len(raw)>MAX_OPERATION_BYTES:raise ValueError('Operation metadata exceeds finite allowance; data belong in buffers/artifacts')
|
|
37
|
+
return raw
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sha(v):return hashlib.sha256(canonical(v)).hexdigest()
|
|
41
|
+
def framed(h,text):
|
|
42
|
+
raw=text.encode('utf8');h.update(len(raw).to_bytes(8,'little'));h.update(raw)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def schema_digest(names,kinds,vocabularies):
|
|
46
|
+
h=hashlib.sha256(b'SCALABLE_TYPED_SCHEMA_1')
|
|
47
|
+
framed(h,'POSITIONAL' if names is None else 'NAMED')
|
|
48
|
+
if names is not None:
|
|
49
|
+
for name in names:framed(h,name)
|
|
50
|
+
for kind,words in zip(kinds,vocabularies):
|
|
51
|
+
framed(h,kind);h.update(len(words).to_bytes(8,'little'))
|
|
52
|
+
for word in words:framed(h,word)
|
|
53
|
+
return h.hexdigest()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def encoder_digest(encoder):return schema_digest(encoder.names,[c['kind'] for c in encoder.columns],[c.get('vocabulary',()) for c in encoder.columns])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def data_identity(table,labels,role):
|
|
60
|
+
if role not in ('USER_INPUT','TRAIN','TRAIN_FULL'):raise ValueError('Explicit learning-source role')
|
|
61
|
+
if len(labels)!=len(table):raise ValueError('Aligned source/labels for identity')
|
|
62
|
+
h=hashlib.sha256(b'SCALABLE_ARRAY_SOURCE_1')
|
|
63
|
+
schema=schema_digest(table.names,table.kinds,table.vocab);framed(h,schema);framed(h,sys.byteorder)
|
|
64
|
+
for c in table.columns:
|
|
65
|
+
framed(h,c.typecode);h.update(len(c).to_bytes(8,'little'));h.update(memoryview(c))
|
|
66
|
+
source=h.hexdigest()
|
|
67
|
+
selected=hashlib.sha256(memoryview(table.indices)).hexdigest() if table.indices is not None else sha({'range_start':0,'range_stop':len(table)})
|
|
68
|
+
return dict(schema='SCALABLE_DATA_ID_1',source_sha256=source,schema_sha256=schema,effective_indices_sha256=selected,labels_sha256=hashlib.sha256(memoryview(labels)).hexdigest(),rows=len(table),features=table.width,role=role,byteorder=sys.byteorder,label_typecode=labels.typecode)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Ledger:
|
|
72
|
+
def __init__(self):self.call_id=uuid.uuid4().hex;self.previous='0'*64;self.sequence=0
|
|
73
|
+
def emit(self,event,fields):
|
|
74
|
+
if event not in FIELDS or set(fields)!=FIELDS[event]:raise ValueError('Exact operation metadata schema: '+event)
|
|
75
|
+
v=dict(event=event,**fields,receipt_schema='SCALABLE_OPERATION_1',call_id=self.call_id,sequence=self.sequence,previous=self.previous);v['sha256']=sha(v)
|
|
76
|
+
self.previous=v['sha256'];self.sequence+=1;return v
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def verify(events):
|
|
80
|
+
if type(events) is not list or not events or len(events)>10000:raise ValueError('Bounded nonempty receipt chain')
|
|
81
|
+
previous='0'*64;call_id=None
|
|
82
|
+
for i,event in enumerate(events):
|
|
83
|
+
if type(event) is not dict or event.get('event') not in FIELDS or set(event)!=FIELDS[event['event']]|ENVELOPE|{'event'}:raise ValueError('Invalid receipt fields')
|
|
84
|
+
if event['receipt_schema']!='SCALABLE_OPERATION_1' or type(event['sequence']) is not int or event['sequence']!=i or event['previous']!=previous:raise ValueError('Broken receipt order')
|
|
85
|
+
if type(event['call_id']) is not str or len(event['call_id'])!=32 or any(c not in '0123456789abcdef' for c in event['call_id']):raise ValueError('Malformed call identity')
|
|
86
|
+
if call_id is None:call_id=event['call_id']
|
|
87
|
+
if event['call_id']!=call_id:raise ValueError('Cross-call receipt replay')
|
|
88
|
+
body={k:v for k,v in event.items() if k!='sha256'}
|
|
89
|
+
if event['sha256']!=sha(body):raise ValueError('Changed operation receipt')
|
|
90
|
+
previous=event['sha256']
|
|
91
|
+
if events[0]['event']!='CALL_STARTED':raise ValueError('Missing call identity origin')
|
|
92
|
+
return dict(status='VERIFIED_HASH_CHAIN',events=len(events),call_id=call_id,final_sha256=previous,completion='FINAL_MODEL' if events[-1]['event']=='FINAL_MODEL' else 'PARTIAL_OR_NONTRAINING_CHAIN',authority='Integrity/custody only, not authorization or independent scientific validity')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Compact target normalization without ndarray/Series.tolist or regression sort."""
|
|
2
|
+
from array import array
|
|
3
|
+
import sys
|
|
4
|
+
from .scalable_data import scalar
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def label_scalar(value):
|
|
8
|
+
if type(value).__module__.split('.')[0]=='numpy' and hasattr(value,'item'):value=value.item()
|
|
9
|
+
# Class identifiers are not floating-point measurements. Preserve integer
|
|
10
|
+
# identity above2**53 rather than merging distinct labels through float().
|
|
11
|
+
return value if type(value) is int else scalar(value)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detect_target(y,task,check,*,reserve=None):
|
|
15
|
+
if getattr(y,'ndim',1)!=1 or isinstance(y,(str,bytes,dict)) or not hasattr(y,'__len__') or len(y)<4:raise ValueError('At least four one-dimensional scalar targets required')
|
|
16
|
+
if task not in ('regression','classification','binary','multiclass'):raise ValueError('Explicit classifier/regressor intent required')
|
|
17
|
+
# Width follows the addressable input,not a small arbitrary class ceiling.
|
|
18
|
+
code='d' if task=='regression' else 'Q' if len(y)>2**32 else 'I'
|
|
19
|
+
numeric=None;values=array(code);lookup={};count=0;vocabulary_bytes=0
|
|
20
|
+
if reserve:reserve(len(y)*values.itemsize*(1 if task=='regression' else 2),'Target buffers before iteration')
|
|
21
|
+
for value in y:
|
|
22
|
+
if count%4096==0:check()
|
|
23
|
+
if reserve and isinstance(value,str):reserve(len(y)*values.itemsize*2+vocabulary_bytes+len(value)*32+512,'Target scalar conversion workspace')
|
|
24
|
+
value=scalar(value) if task=='regression' else label_scalar(value)
|
|
25
|
+
if value is None:raise ValueError('Missing target is unsupported')
|
|
26
|
+
is_numeric=type(value) in (int,float)
|
|
27
|
+
if numeric is None:numeric=is_numeric
|
|
28
|
+
if numeric!=is_numeric:raise ValueError('Target must be consistently string or numeric')
|
|
29
|
+
if task=='regression' and not numeric:raise ValueError('Regression requires finite numeric targets')
|
|
30
|
+
if task=='regression':values.append(value)
|
|
31
|
+
else:
|
|
32
|
+
if value not in lookup:
|
|
33
|
+
vocabulary_bytes+=sys.getsizeof(value)+256
|
|
34
|
+
if reserve:reserve(len(y)*values.itemsize*2+vocabulary_bytes,'Target vocabulary/codes/remap workspace')
|
|
35
|
+
lookup[value]=len(lookup)
|
|
36
|
+
values.append(lookup[value])
|
|
37
|
+
count+=1
|
|
38
|
+
if count!=len(y):raise ValueError('Target iterator/length mismatch')
|
|
39
|
+
if task=='regression':return task,values,[]
|
|
40
|
+
classes=sorted(lookup)
|
|
41
|
+
if len(classes)<2 or task=='binary' and len(classes)!=2:raise ValueError('Distinct task-compatible labels required')
|
|
42
|
+
task='binary' if len(classes)==2 else 'multiclass'
|
|
43
|
+
remap=[None]*len(classes)
|
|
44
|
+
for i,value in enumerate(classes):remap[lookup[value]]=i
|
|
45
|
+
return task,array(code,(remap[v] for v in values)),classes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compact_oof_train_indices(n, held, resources):
|
|
49
|
+
"""Validated complement in ascending source order,without Python-int sets.
|
|
50
|
+
|
|
51
|
+
Index storage follows addressable rows; neither fold nor observations are
|
|
52
|
+
sampled. Every returned index is unmarked and every held index is marked,
|
|
53
|
+
which proves the logged disjointness without allocating a second large set.
|
|
54
|
+
"""
|
|
55
|
+
if type(n) is not int or n < 2:raise ValueError('Positive OOF partition size')
|
|
56
|
+
code='I' if n < 2**32 else 'Q'
|
|
57
|
+
resources.reserve(n*(1+array(code).itemsize),'OOF membership mask and compact complement')
|
|
58
|
+
marked=bytearray(n)
|
|
59
|
+
for position,index in enumerate(held):
|
|
60
|
+
if position%4096==0:resources.check()
|
|
61
|
+
if type(index) is not int or not 0<=index<n or marked[index]:raise ValueError('Unique bounded held indices required')
|
|
62
|
+
marked[index]=1
|
|
63
|
+
result=array(code)
|
|
64
|
+
for index in range(n):
|
|
65
|
+
if index%4096==0:resources.check()
|
|
66
|
+
if not marked[index]:result.append(index)
|
|
67
|
+
if not result or len(result)==n:raise ValueError('OOF needs nonempty disjoint train and held partitions')
|
|
68
|
+
return result
|