mirror of
https://github.com/wassname/ray.git
synced 2026-07-13 17:45:08 +08:00
Partially Use f string (#10218)
* flynt. trial 1. * Trial 1. * Addressed code review.
This commit is contained in:
+18
-22
@@ -53,8 +53,8 @@ def from_range(n: int, num_shards: int = 2,
|
||||
else:
|
||||
end = (i + 1) * shard_size
|
||||
generators.append(range(start, end))
|
||||
name = "from_range[{}, shards={}{}]".format(
|
||||
n, num_shards, ", repeat=True" if repeat else "")
|
||||
name = (f"from_range[{n}, shards={num_shards}"
|
||||
f"{', repeat=True' if repeat else ''}]")
|
||||
return from_iterators(
|
||||
generators,
|
||||
repeat=repeat,
|
||||
@@ -111,7 +111,7 @@ def from_actors(actors: List["ray.actor.ActorHandle"],
|
||||
name (str): Optional name to give the iterator.
|
||||
"""
|
||||
if not name:
|
||||
name = "from_actors[shards={}]".format(len(actors))
|
||||
name = f"from_actors[shards={len(actors)}]"
|
||||
return ParallelIterator([_ActorSet(actors, [])], name, parent_iterators=[])
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ class ParallelIterator(Generic[T]):
|
||||
return repr(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "ParallelIterator[{}]".format(self.name)
|
||||
return f"ParallelIterator[{self.name}]"
|
||||
|
||||
def _with_transform(self, local_it_fn, name):
|
||||
"""Helper function to create new Parallel Iterator"""
|
||||
@@ -290,7 +290,7 @@ class ParallelIterator(Generic[T]):
|
||||
... [0, 1, 2, 3]
|
||||
"""
|
||||
return self._with_transform(lambda local_it: local_it.batch(n),
|
||||
".batch({})".format(n))
|
||||
f".batch({n})")
|
||||
|
||||
def flatten(self) -> "ParallelIterator[T[0]]":
|
||||
"""Flatten batches of items into individual items.
|
||||
@@ -421,8 +421,7 @@ class ParallelIterator(Generic[T]):
|
||||
def make_gen_i(i):
|
||||
return lambda: base_iterator(num_partitions, i)
|
||||
|
||||
name = self.name + ".repartition[num_partitions={}]".format(
|
||||
num_partitions)
|
||||
name = self.name + f".repartition[num_partitions={num_partitions}]"
|
||||
|
||||
generators = [make_gen_i(s) for s in range(num_partitions)]
|
||||
worker_cls = ray.remote(ParallelIteratorWorker)
|
||||
@@ -449,7 +448,7 @@ class ParallelIterator(Generic[T]):
|
||||
... 2
|
||||
"""
|
||||
it = self.batch_across_shards().flatten()
|
||||
it.name = "{}.gather_sync()".format(self)
|
||||
it.name = f"{self}.gather_sync()"
|
||||
return it
|
||||
|
||||
def batch_across_shards(self) -> "LocalIterator[List[T]]":
|
||||
@@ -488,7 +487,7 @@ class ParallelIterator(Generic[T]):
|
||||
yield results
|
||||
futures = [a.par_iter_next.remote() for a in active]
|
||||
|
||||
name = "{}.batch_across_shards()".format(self)
|
||||
name = f"{self}.batch_across_shards()"
|
||||
return LocalIterator(base_iterator, SharedMetrics(), name=name)
|
||||
|
||||
def gather_async(self, batch_ms=0, num_async=1) -> "LocalIterator[T]":
|
||||
@@ -560,7 +559,7 @@ class ParallelIterator(Generic[T]):
|
||||
if timeout is not None:
|
||||
yield _NextValueNotReady()
|
||||
|
||||
name = "{}.gather_async()".format(self)
|
||||
name = f"{self}.gather_async()"
|
||||
local_iter = LocalIterator(base_iterator, SharedMetrics(), name=name)
|
||||
return local_iter
|
||||
|
||||
@@ -576,8 +575,7 @@ class ParallelIterator(Generic[T]):
|
||||
"""Return an iterator that is the union of this and the other."""
|
||||
if not isinstance(other, ParallelIterator):
|
||||
raise TypeError(
|
||||
"other must be of type ParallelIterator, got {}".format(
|
||||
type(other)))
|
||||
f"other must be of type ParallelIterator, got {type(other)}")
|
||||
actor_sets = []
|
||||
actor_sets.extend(self.actor_sets)
|
||||
actor_sets.extend(other.actor_sets)
|
||||
@@ -585,7 +583,7 @@ class ParallelIterator(Generic[T]):
|
||||
# keep an explicit reference to its parent iterator
|
||||
return ParallelIterator(
|
||||
actor_sets,
|
||||
"ParallelUnion[{}, {}]".format(self, other),
|
||||
f"ParallelUnion[{self}, {other}]",
|
||||
parent_iterators=self.parent_iterators + other.parent_iterators)
|
||||
|
||||
def select_shards(self,
|
||||
@@ -608,7 +606,7 @@ class ParallelIterator(Generic[T]):
|
||||
new_actor_set = _ActorSet(new_actors, old_actor_set.transforms)
|
||||
return ParallelIterator(
|
||||
[new_actor_set],
|
||||
"{}.select_shards({} total)".format(self, len(shards_to_keep)),
|
||||
f"{self}.select_shards({len(shards_to_keep)} total)",
|
||||
parent_iterators=self.parent_iterators)
|
||||
|
||||
def num_shards(self) -> int:
|
||||
@@ -674,7 +672,7 @@ class ParallelIterator(Generic[T]):
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
name = self.name + ".shard[{}]".format(shard_index)
|
||||
name = self.name + f".shard[{shard_index}]"
|
||||
return LocalIterator(base_iterator, SharedMetrics(), name=name)
|
||||
|
||||
|
||||
@@ -761,7 +759,7 @@ class LocalIterator(Generic[T]):
|
||||
return repr(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "LocalIterator[{}]".format(self.name)
|
||||
return f"LocalIterator[{self.name}]"
|
||||
|
||||
def transform(self, fn: Callable[[Iterable[T]], Iterable[U]]
|
||||
) -> "LocalIterator[U]":
|
||||
@@ -871,7 +869,7 @@ class LocalIterator(Generic[T]):
|
||||
self.base_iterator,
|
||||
self.shared_metrics,
|
||||
self.local_transforms + [apply_batch],
|
||||
name=self.name + ".batch({})".format(n))
|
||||
name=self.name + f".batch({n})")
|
||||
|
||||
def flatten(self) -> "LocalIterator[T[0]]":
|
||||
def apply_flatten(it):
|
||||
@@ -1013,7 +1011,7 @@ class LocalIterator(Generic[T]):
|
||||
LocalIterator(
|
||||
make_next(i),
|
||||
self.shared_metrics, [],
|
||||
name=self.name + ".duplicate[{}]".format(i)))
|
||||
name=self.name + f".duplicate[{i}]"))
|
||||
|
||||
return iterators
|
||||
|
||||
@@ -1038,8 +1036,7 @@ class LocalIterator(Generic[T]):
|
||||
for it in others:
|
||||
if not isinstance(it, LocalIterator):
|
||||
raise ValueError(
|
||||
"other must be of type LocalIterator, got {}".format(
|
||||
type(it)))
|
||||
f"other must be of type LocalIterator, got {type(it)}")
|
||||
|
||||
active = []
|
||||
parent_iters = [self] + list(others)
|
||||
@@ -1090,8 +1087,7 @@ class LocalIterator(Generic[T]):
|
||||
return LocalIterator(
|
||||
build_union,
|
||||
shared_metrics, [],
|
||||
name="LocalUnion[{}, {}]".format(self, ", ".join(map(str,
|
||||
others))))
|
||||
name=f"LocalUnion[{self}, {', '.join(map(str, others))}]")
|
||||
|
||||
|
||||
class ParallelIteratorWorker(object):
|
||||
|
||||
@@ -168,7 +168,7 @@ class AsyncResult:
|
||||
"""
|
||||
|
||||
if not self.ready():
|
||||
raise ValueError("{0!r} not ready".format(self))
|
||||
raise ValueError(f"{self!r} not ready")
|
||||
return not self._result_thread.got_error()
|
||||
|
||||
|
||||
@@ -355,8 +355,8 @@ class Pool:
|
||||
os.environ[RAY_ADDRESS_ENV]))
|
||||
ray.init()
|
||||
elif ray_address is not None:
|
||||
logger.info("Connecting to ray cluster at address='{}'".format(
|
||||
ray_address))
|
||||
logger.info(
|
||||
f"Connecting to ray cluster at address='{ray_address}'")
|
||||
ray.init(address=ray_address)
|
||||
# Local mode.
|
||||
else:
|
||||
|
||||
@@ -158,7 +158,7 @@ def test_multi_model(ray_start_2_cpus, num_workers):
|
||||
data = list(iterator)
|
||||
for i, (model, optimizer) in enumerate(
|
||||
zip(self.models, self.optimizers)):
|
||||
result["model_{}".format(i)] = train(
|
||||
result[f"model_{i}"] = train(
|
||||
model=model,
|
||||
criterion=self.criterion,
|
||||
optimizer=optimizer,
|
||||
|
||||
@@ -208,7 +208,7 @@ if __name__ == "__main__":
|
||||
# Trains num epochs
|
||||
train_stats1 = trainer.train()
|
||||
train_stats1.update(trainer.validate())
|
||||
print("iter {}:".format(i), train_stats1)
|
||||
print(f"iter {i}:", train_stats1)
|
||||
|
||||
dt = (time.time() - training_start) / 3
|
||||
print(f"Training on workers takes: {dt:.3f} seconds/epoch")
|
||||
|
||||
@@ -85,10 +85,7 @@ class TFTrainer:
|
||||
ports = ray.get(
|
||||
[worker.find_free_port.remote() for worker in self.workers])
|
||||
|
||||
urls = [
|
||||
"{ip}:{port}".format(ip=ips[i], port=ports[i])
|
||||
for i in range(len(self.workers))
|
||||
]
|
||||
urls = [f"{ips[i]}:{ports[i]}" for i in range(len(self.workers))]
|
||||
|
||||
# Get setup tasks in order to throw errors on failure
|
||||
ray.get([
|
||||
|
||||
@@ -230,7 +230,7 @@ def reserve_resources(num_cpus, num_gpus, retries=20):
|
||||
cuda_device_set = {}
|
||||
match_devices = bool(cuda_devices)
|
||||
if match_devices:
|
||||
logger.debug("Found set devices: {}".format(cuda_devices))
|
||||
logger.debug(f"Found set devices: {cuda_devices}")
|
||||
assert isinstance(cuda_devices, str)
|
||||
cuda_device_set = set(cuda_devices.split(","))
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ if __name__ == "__main__":
|
||||
num_workers = 2 if args.local else int(ray.cluster_resources().get(device))
|
||||
from ray.util.sgd.torch.examples.train_example import LinearDataset
|
||||
|
||||
print("Model: %s" % args.model)
|
||||
print(f"Model: {args.model}")
|
||||
print("Batch size: %d" % args.batch_size)
|
||||
print("Number of %ss: %d" % (device, num_workers))
|
||||
|
||||
@@ -131,7 +131,7 @@ if __name__ == "__main__":
|
||||
# Results
|
||||
img_sec_mean = np.mean(img_secs)
|
||||
img_sec_conf = 1.96 * np.std(img_secs)
|
||||
print("Img/sec per %s: %.1f +-%.1f" % (device, img_sec_mean, img_sec_conf))
|
||||
print(f"Img/sec per {device}: {img_sec_mean:.1f} +-{img_sec_conf:.1f}")
|
||||
print("Total img/sec on %d %s(s): %.1f +-%.1f" %
|
||||
(num_workers, device, num_workers * img_sec_mean,
|
||||
num_workers * img_sec_conf))
|
||||
|
||||
@@ -77,7 +77,7 @@ def benchmark_step():
|
||||
optimizer.step()
|
||||
|
||||
|
||||
print("Model: %s" % args.model)
|
||||
print(f"Model: {args.model}")
|
||||
print("Batch size: %d" % args.batch_size)
|
||||
device = "GPU"
|
||||
print("Number of %ss: %d" % (device, args.num_gpus))
|
||||
@@ -98,7 +98,7 @@ for x in range(args.num_iters):
|
||||
# Results
|
||||
img_sec_mean = np.mean(img_secs)
|
||||
img_sec_conf = 1.96 * np.std(img_secs)
|
||||
print("Img/sec per %s: %.1f +-%.1f" % (device, img_sec_mean, img_sec_conf))
|
||||
print(f"Img/sec per {device}: {img_sec_mean:.1f} +-{img_sec_conf:.1f}")
|
||||
print("Total img/sec on %d %s(s): %.1f +-%.1f" % (
|
||||
args.num_gpus,
|
||||
device,
|
||||
|
||||
@@ -118,7 +118,7 @@ def log(s, nl=True):
|
||||
print(s, end="\n" if nl else "")
|
||||
|
||||
|
||||
log("Model: %s" % args.model)
|
||||
log(f"Model: {args.model}")
|
||||
log("Batch size: %d" % args.batch_size)
|
||||
device = "GPU" if args.cuda else "CPU"
|
||||
log("Number of %ss: %d" % (device, hvd.size()))
|
||||
@@ -139,6 +139,6 @@ for x in range(args.num_iters):
|
||||
# Results
|
||||
img_sec_mean = np.mean(img_secs)
|
||||
img_sec_conf = 1.96 * np.std(img_secs)
|
||||
log("Img/sec per %s: %.1f +-%.1f" % (device, img_sec_mean, img_sec_conf))
|
||||
log(f"Img/sec per {device}: {img_sec_mean:.1f} +-{img_sec_conf:.1f}")
|
||||
log("Total img/sec on %d %s(s): %.1f +-%.1f" %
|
||||
(hvd.size(), device, hvd.size() * img_sec_mean, hvd.size() * img_sec_conf))
|
||||
|
||||
@@ -20,13 +20,12 @@ def mock_data(train_dir, val_dir):
|
||||
|
||||
def mock_class(base, n):
|
||||
random_cls = random.randint(per_cls * n, per_cls * n + per_cls)
|
||||
sub_dir = join(base, "n{:08d}".format(random_cls))
|
||||
sub_dir = join(base, f"n{random_cls:08d}")
|
||||
os.makedirs(sub_dir, exist_ok=True)
|
||||
|
||||
for i in range(total_imgs):
|
||||
random_img = random.randint(per_img * i, per_img * i + per_img)
|
||||
file = join(sub_dir,
|
||||
"ILSVRC2012_val_{:08d}.JPEG".format(random_img))
|
||||
file = join(sub_dir, f"ILSVRC2012_val_{random_img:08d}.JPEG")
|
||||
|
||||
PIL.Image.fromarray(np.zeros((375, 500, 3),
|
||||
dtype=np.uint8)).save(file)
|
||||
|
||||
@@ -193,11 +193,11 @@ def main(args):
|
||||
state_dict = trainer.state_dict()
|
||||
state_dict.update(epoch=epoch, args=args)
|
||||
torch.save(state_dict,
|
||||
os.path.join(args.output_dir, "model_{}.pth".format(epoch)))
|
||||
os.path.join(args.output_dir, f"model_{epoch}.pth"))
|
||||
|
||||
total_time = time.time() - start_time
|
||||
total_time_str = str(datetime.timedelta(seconds=int(total_time)))
|
||||
print("Training time {}".format(total_time_str))
|
||||
print(f"Training time {total_time_str}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
|
||||
@@ -49,8 +49,8 @@ class ConfusionMatrix(object):
|
||||
'IoU: {}\n'
|
||||
'mean IoU: {:.1f}').format(
|
||||
acc_global.item() * 100,
|
||||
['{:.1f}'.format(i) for i in (acc * 100).tolist()],
|
||||
['{:.1f}'.format(i) for i in (iu * 100).tolist()],
|
||||
[f'{i:.1f}' for i in (acc * 100).tolist()],
|
||||
[f'{i:.1f}' for i in (iu * 100).tolist()],
|
||||
iu.mean().item() * 100)
|
||||
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ def data_creator(config):
|
||||
if args.tokenizer_name else args.model_name_or_path,
|
||||
cache_dir=args.cache_dir if args.cache_dir else None,
|
||||
)
|
||||
logger.info("tokenizer instantiation time: {}".format(time.time() - start))
|
||||
logger.info(f"tokenizer instantiation time: {time.time() - start}")
|
||||
|
||||
train_dataset = load_and_cache_examples(
|
||||
args, args.task_name, tokenizer, evaluate=False)
|
||||
@@ -322,7 +322,7 @@ def main():
|
||||
# Prepare GLUE task
|
||||
args.task_name = args.task_name.lower()
|
||||
if args.task_name not in processors:
|
||||
raise ValueError("Task not found: %s" % (args.task_name))
|
||||
raise ValueError(f"Task not found: {args.task_name}")
|
||||
args.output_mode = output_modes[args.task_name]
|
||||
|
||||
logging.basicConfig(
|
||||
|
||||
@@ -133,7 +133,7 @@ def save_and_evaluate_checkpoints(args, model, tokenizer):
|
||||
model.to(args.device)
|
||||
result = evaluate(args, model, tokenizer, prefix=prefix)
|
||||
result = dict(
|
||||
(k + "_{}".format(global_step), v) for k, v in result.items())
|
||||
(k + f"_{global_step}", v) for k, v in result.items())
|
||||
results.update(result)
|
||||
|
||||
return results
|
||||
@@ -163,7 +163,7 @@ def evaluate(args, model, tokenizer, prefix=""):
|
||||
batch_size=args.eval_batch_size)
|
||||
|
||||
# Eval!
|
||||
logger.info("***** Running evaluation {} *****".format(prefix))
|
||||
logger.info(f"***** Running evaluation {prefix} *****")
|
||||
logger.info(" Num examples = %d", len(eval_dataset))
|
||||
logger.info(" Batch size = %d", args.eval_batch_size)
|
||||
eval_loss = 0.0
|
||||
|
||||
@@ -82,7 +82,7 @@ class TorchRunner:
|
||||
return loaders
|
||||
else:
|
||||
raise ValueError(
|
||||
"Number of loaders must be <= 2. Got {}".format(loaders))
|
||||
f"Number of loaders must be <= 2. Got {loaders}")
|
||||
# No great way of checking type otherwise
|
||||
return loaders, None
|
||||
|
||||
@@ -146,7 +146,7 @@ class TorchRunner:
|
||||
if not isinstance(self.models, Iterable):
|
||||
self.models = [self.models]
|
||||
assert all(isinstance(model, nn.Module) for model in self.models), (
|
||||
"All models must be PyTorch models: {}.".format(self.models))
|
||||
f"All models must be PyTorch models: {self.models}.")
|
||||
if self.use_gpu and torch.cuda.is_available():
|
||||
self.models = [model.cuda() for model in self.models]
|
||||
|
||||
@@ -189,7 +189,7 @@ class TorchRunner:
|
||||
info=None,
|
||||
iterator=None):
|
||||
"""Runs a training epoch and updates the model parameters."""
|
||||
logger.debug("Begin Training Step {}".format(self.epochs + 1))
|
||||
logger.debug(f"Begin Training Step {self.epochs + 1}")
|
||||
info = info or {}
|
||||
self._toggle_profiling(profile=profile)
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class TorchTrainer:
|
||||
if backend == "auto":
|
||||
backend = "nccl" if use_gpu else "gloo"
|
||||
|
||||
logger.debug("Using {} as backend.".format(backend))
|
||||
logger.debug(f"Using {backend} as backend.")
|
||||
self.backend = backend
|
||||
self.num_cpus_per_worker = num_cpus_per_worker
|
||||
self.use_gpu = use_gpu
|
||||
@@ -659,12 +659,12 @@ class TorchTrainer:
|
||||
"Failed to shutdown gracefully, forcing a shutdown.")
|
||||
|
||||
for worker in self.remote_workers:
|
||||
logger.warning("Killing worker {}.".format(worker))
|
||||
logger.warning(f"Killing worker {worker}.")
|
||||
ray.kill(worker)
|
||||
else:
|
||||
self.local_worker.shutdown()
|
||||
for worker in self.remote_workers:
|
||||
logger.debug("Killing worker {}.".format(worker))
|
||||
logger.debug(f"Killing worker {worker}.")
|
||||
ray.kill(worker)
|
||||
|
||||
self.local_worker = DeactivatedRunner()
|
||||
@@ -674,7 +674,7 @@ class TorchTrainer:
|
||||
"""Terminates models without giving up local resource reservation."""
|
||||
self.local_worker.shutdown(cleanup=False)
|
||||
for worker in self.remote_workers:
|
||||
logger.debug("Killing worker {}.".format(worker))
|
||||
logger.debug(f"Killing worker {worker}.")
|
||||
ray.kill(worker)
|
||||
self.local_worker = DeactivatedRunner()
|
||||
self.remote_workers = []
|
||||
|
||||
@@ -72,23 +72,18 @@ class TrainingOperator:
|
||||
self._models = models # List of models
|
||||
assert isinstance(
|
||||
models,
|
||||
Iterable), ("Components need to be iterable. Got: {}".format(
|
||||
type(models)))
|
||||
Iterable), (f"Components need to be iterable. Got: {type(models)}")
|
||||
self._optimizers = optimizers # List of optimizers
|
||||
assert isinstance(
|
||||
optimizers,
|
||||
Iterable), ("Components need to be iterable. Got: {}".format(
|
||||
type(optimizers)))
|
||||
assert isinstance(optimizers, Iterable), (
|
||||
f"Components need to be iterable. Got: {type(optimizers)}")
|
||||
self._train_loader = train_loader
|
||||
self._validation_loader = validation_loader
|
||||
self._world_rank = world_rank
|
||||
self._criterion = criterion
|
||||
self._schedulers = schedulers
|
||||
if schedulers:
|
||||
assert isinstance(
|
||||
schedulers,
|
||||
Iterable), ("Components need to be iterable. Got: {}".format(
|
||||
type(schedulers)))
|
||||
assert isinstance(schedulers, Iterable), (
|
||||
f"Components need to be iterable. Got: {type(schedulers)}")
|
||||
self._config = config
|
||||
self._use_fp16 = use_fp16
|
||||
self._device_ids = device_ids
|
||||
@@ -165,10 +160,9 @@ class TrainingOperator:
|
||||
desc = ""
|
||||
if info is not None and "epoch_idx" in info:
|
||||
if "num_epochs" in info:
|
||||
desc = "{}/{}e".format(info["epoch_idx"] + 1,
|
||||
info["num_epochs"])
|
||||
desc = f"{info['epoch_idx'] + 1}/{info['num_epochs']}e"
|
||||
else:
|
||||
desc = "{}e".format(info["epoch_idx"] + 1)
|
||||
desc = f"{info['epoch_idx'] + 1}e"
|
||||
_progress_bar = tqdm(
|
||||
total=info[NUM_STEPS] or len(self.train_loader),
|
||||
desc=desc,
|
||||
|
||||
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
||||
def setup_address():
|
||||
ip = ray.services.get_node_ip_address()
|
||||
port = find_free_port()
|
||||
return "tcp://{ip}:{port}".format(ip=ip, port=port)
|
||||
return f"tcp://{ip}:{port}"
|
||||
|
||||
|
||||
def setup_process_group(url, world_rank, world_size, timeout, backend="gloo"):
|
||||
@@ -28,7 +28,7 @@ def setup_process_group(url, world_rank, world_size, timeout, backend="gloo"):
|
||||
"""
|
||||
logger.debug("Connecting to {} world_rank: {} world_size: {}".format(
|
||||
url, world_rank, world_size))
|
||||
logger.debug("using {}".format(backend))
|
||||
logger.debug(f"using {backend}")
|
||||
if backend == "nccl" and "NCCL_BLOCKING_WAIT" not in os.environ:
|
||||
logger.debug(
|
||||
"Setting NCCL_BLOCKING_WAIT for detecting node failure. "
|
||||
|
||||
@@ -142,9 +142,9 @@ class TimerCollection:
|
||||
for k, t in self._timers.items():
|
||||
if t.count > 0:
|
||||
if mean:
|
||||
aggregates["mean_%s_s" % k] = t.mean
|
||||
aggregates[f"mean_{k}_s"] = t.mean
|
||||
if last:
|
||||
aggregates["last_%s_s" % k] = t.last
|
||||
aggregates[f"last_{k}_s"] = t.last
|
||||
return aggregates
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user