From fddfec33375275e296a136d03a037e41be2b9585 Mon Sep 17 00:00:00 2001 From: Sermet Pekin Date: Fri, 6 Dec 2024 09:34:52 +0300 Subject: [PATCH] cross entropy --- .gitignore | 1 + micrograd/cross_entropy.py | 42 ++++++++ micrograd/engine.py | 11 ++ pyproject.toml | 5 +- test/test_crossEntropyLoss.py | 33 ++++++ torch_e2.py | 197 ++++++++++++++++++++++++++++++++++ torchexample.py | 65 ++++++++--- uv.lock | 71 +++++++++++- 8 files changed, 405 insertions(+), 20 deletions(-) create mode 100644 micrograd/cross_entropy.py create mode 100644 test/test_crossEntropyLoss.py create mode 100644 torch_e2.py diff --git a/.gitignore b/.gitignore index 561ba984..8624e774 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ ignore*/ data/ FashionMNIST/ raw/ +*.pth diff --git a/micrograd/cross_entropy.py b/micrograd/cross_entropy.py new file mode 100644 index 00000000..f70a0416 --- /dev/null +++ b/micrograd/cross_entropy.py @@ -0,0 +1,42 @@ +from typing import List + +from micrograd.engine import Value +import math + +class CrossEntropyLoss: + @staticmethod + def forward(logits: List[Value], target: int) -> Value: + """ + Computes CrossEntropyLoss for a single example. + + :param logits: List of Value objects, raw outputs (logits) from the model. + :param target: Integer index of the true class. + :return: Loss Value. + """ + # Step 1: Compute the exponentials of the logits + exp_logits = [logit.exp() for logit in logits] + + # Step 2: Compute the sum of the exponentials + sum_exp_logits = sum(exp_logits) + + # Step 3: Compute the softmax probabilities + probs = [exp_logit / sum_exp_logits for exp_logit in exp_logits] + + # Step 4: Compute the negative log-likelihood loss for the target class + loss = -probs[target].log() + + return loss + + @staticmethod + def batch_forward(batch_logits: List[List[Value]], batch_targets: List[int]) -> Value: + """ + Computes the average CrossEntropyLoss for a batch. + + :param batch_logits: List of List[Value] for all samples in the batch. + :param batch_targets: List of true class indices for the batch. + :return: Average loss Value. + """ + batch_loss = sum( + CrossEntropyLoss.forward(logits, target) for logits, target in zip(batch_logits, batch_targets) + ) + return batch_loss / len(batch_targets) diff --git a/micrograd/engine.py b/micrograd/engine.py index 0b9d304a..b0bfd35e 100644 --- a/micrograd/engine.py +++ b/micrograd/engine.py @@ -130,6 +130,17 @@ def _backward(): return out + def log(self) -> 'Value': + """Logarithm is only defined for positive values.""" + clamped_data = max(self.data, 1e-7) + out = Value(math.log(clamped_data), (self,), 'log') + + def _backward(): + self.grad += (1 / clamped_data) * out.grad + + out._backward = _backward + return out + def backward(self) -> None: # topological order all the children in the graph diff --git a/pyproject.toml b/pyproject.toml index c0a96089..998b914b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "micrograd" version = "0.2.0" authors = [ - { name = "Andrej Karpathy", email = "andrej.karpathy@gmail.com" }, + { name = "Sermet Pekin", email = "sermet.pekin@gmail.com" }, ] description = "A tiny scalar-valued autograd engine with a small PyTorch-like neural network library on top." @@ -13,6 +13,7 @@ dependencies = [ "black>=24.10.0", "graphviz>=0.20.3", "matplotlib>=3.7.5", + "pandas>=2.2.3", "pytest>=8.3.4", "ruff>=0.8.1", "scikit-learn>=1.3.2", @@ -23,7 +24,7 @@ dependencies = [ [project.urls] Source = "https://github.com/SermetPekin/micrograd" -Original = "https://github.com/karpathy/micrograd" +Inspired-by = "https://github.com/karpathy/micrograd" [build-system] diff --git a/test/test_crossEntropyLoss.py b/test/test_crossEntropyLoss.py new file mode 100644 index 00000000..6f0b0db5 --- /dev/null +++ b/test/test_crossEntropyLoss.py @@ -0,0 +1,33 @@ +import pytest +import torch +import torch.nn as nn +from micrograd.engine import Value +from micrograd.cross_entropy import CrossEntropyLoss # Assuming this is your custom implementation + +def test_micrograd_vs_torch_cross_entropy(): + # Define the logits and targets + logits_micrograd = [[Value(-1.0), Value(-2.0), Value(-3.0)], + [Value(0.5), Value(-1.5), Value(-0.5)]] + targets_micrograd = [0, 2] + + # Torch equivalent tensors + logits_torch = torch.tensor([[-1.0, -2.0, -3.0], + [0.5, -1.5, -0.5]], dtype=torch.float32) + targets_torch = torch.tensor([0, 2], dtype=torch.long) + + # Compute micrograd loss + loss_micrograd = CrossEntropyLoss.batch_forward(logits_micrograd, targets_micrograd) + micrograd_loss_value = loss_micrograd.data + + # Compute torch loss + criterion = nn.CrossEntropyLoss() + loss_torch = criterion(logits_torch, targets_torch) + torch_loss_value = loss_torch.item() + + # Print losses for debugging + print(f"Micrograd Loss: {micrograd_loss_value:.4f}") + print(f"Torch Loss: {torch_loss_value:.4f}") + + # Assert that the losses are approximately equal + assert abs(micrograd_loss_value - torch_loss_value) < 1e-4, \ + f"Losses do not match: Micrograd={micrograd_loss_value}, Torch={torch_loss_value}" diff --git a/torch_e2.py b/torch_e2.py new file mode 100644 index 00000000..909cbbe1 --- /dev/null +++ b/torch_e2.py @@ -0,0 +1,197 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from sklearn.model_selection import train_test_split +import pandas as pd +import numpy as np + +import matplotlib.pyplot as plt +from torch.utils.data import DataLoader +from torchvision import datasets +from torchvision.transforms import ToTensor + + +def init_data(): + def fnc(d: str): + dict_ = { + 'Setosa': 0, + 'Versicolor': 1, + 'Virginica': 2, + + } + return dict_.get(d, d) + + url = "https://gist.githubusercontent.com/netj/8836201/raw/6f9306ad21398ea43cba4f7d537619d0e07d5ae3/iris.csv" + df = pd.read_csv(url) + df['variety'] = df['variety'].apply(fnc) + return df + + +def process_data_for_torch(df): + X = df.drop('variety', axis=1) + y = df['variety'] + X = X.values + y = y.values + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + X_train = torch.FloatTensor(X_train) + X_test = torch.FloatTensor(X_test) + y_train = torch.LongTensor(y_train) + y_test = torch.LongTensor(y_test) + return X_train, X_test, y_train, y_test + + +class Model(nn.Module): + def __init__(self, in_feats: int = 4, out_feats: int = 3, hidden1=7, hidden2=7): + super(Model, self).__init__() + self.fc1 = nn.Linear(in_feats, hidden1) + self.fc2 = nn.Linear(hidden1, hidden2) + self.out = nn.Linear(hidden2, out_feats) + + def forward(self, x): + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + x = self.out(x) + return x + + +def with_torch(save=True): + df = init_data() + X_train, X_test, y_train, y_test = process_data_for_torch(df) + + model = Model() + + criterion = nn.CrossEntropyLoss() + optimizer = torch.optim.Adam(model.parameters(), lr=0.01) + + # model.parameters + epochs = 60 + losses = [] + for i in range(epochs): + y_pred = model.forward(X_train) + loss = criterion(y_pred, y_train) + + losses.append(loss.detach().numpy()) + + if i % 10 == 0: + print(f'Epoch : {i} and loss : {loss}') + + # backprop + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + if save: + # Save the model + torch.save(model.state_dict(), "iris_model.pth") + + with torch.no_grad(): + y_eval = model.forward(X_test) + loss = criterion(y_eval, y_test).item() + accuracy = calculate_accuracy(y_eval, y_test) + + print(f"Test Loss: {loss}") + print(f"Test Accuracy: {accuracy * 100:.2f}%") + return model, losses + + +def load_model(): + # Load the model + loaded_model = Model() + loaded_model.load_state_dict(torch.load("iris_model.pth")) + loaded_model.eval() + return loaded_model + + +def calculate_accuracy(_y_pred, _y_true): + y_pred_classes = torch.argmax(_y_pred, axis=1) # Get the predicted class + acc = (y_pred_classes == _y_true).sum().item() / len(_y_true) + return acc + + +# with_torch() + +def with_micrograd(): + from micrograd import Value + from micrograd import MLP + + # Define the Micrograd model + in_feats = 4 # Input features (Iris dataset) + hidden1 = 7 # Hidden layer 1 + hidden2 = 7 # Hidden layer 2 + out_feats = 3 # Output classes + + model = MLP(in_feats, [hidden1, hidden2, out_feats]) # Equivalent to your PyTorch model + df = init_data() + X_train, X_test, y_train, y_test = process_data_for_torch(df) + # Hyperparameters + learning_rate = 0.01 + epochs = 100 + losses = [] + + # # # One-hot encode y_train + # y_train_onehot = np.zeros((y_train.size, out_feats)) + # y_train_onehot[np.arange(y_train.size), y_train] = 1 + # One-hot encode y_train + y_train_onehot = np.zeros((y_train.shape[0], out_feats)) # Initialize with zeros + y_train_onehot[np.arange(y_train.shape[0]), y_train] = 1 # Set the appropriate index to 1 + + + # Training loop + for epoch in range(epochs): + epoch_loss = 0.0 + + for i in range(len(X_train)): + # Forward pass + inputs = [Value(x) for x in X_train[i]] + targets = [Value(y) for y in y_train_onehot[i]] + outputs = model(inputs) + + # Calculate Cross-Entropy Loss + exp_outputs = [o.exp() for o in outputs] + sum_exp_outputs = sum(exp_outputs) + probs = [o / sum_exp_outputs for o in exp_outputs] + loss = -sum(t * p.log() for t, p in zip(targets, probs)) + + epoch_loss += loss.data + + # Backpropagation + model.zero_grad() # Zero gradients + loss.backward() + + # Update weights + for param in model.parameters(): + param.data -= learning_rate * param.grad + + losses.append(epoch_loss / len(X_train)) + if epoch % 10 == 0: + print(f"Epoch {epoch}, Loss: {epoch_loss / len(X_train)}") + + + # Evaluation + correct = 0 + total = len(X_test) + + for i in range(len(X_test)): + inputs = [Value(x) for x in X_test[i]] + outputs = model(inputs) + predicted = np.argmax([o.data for o in outputs]) + if predicted == y_test[i]: + correct += 1 + + accuracy = correct / total + print(f"Test Accuracy: {accuracy * 100:.2f}%") + + # import matplotlib.pyplot as plt + + # Plot the losses + plt.plot(range(epochs), losses, label='Micrograd Loss') + plt.xlabel('Epochs') + plt.ylabel('Loss') + plt.title('Micrograd Loss Curve') + plt.legend() + plt.show() + + +with_micrograd() diff --git a/torchexample.py b/torchexample.py index ec984d74..71327f7e 100644 --- a/torchexample.py +++ b/torchexample.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import torch from torch import nn @@ -28,9 +27,9 @@ train_dataloader = DataLoader(training_data, batch_size=batch_size) test_dataloader = DataLoader(test_data, batch_size=batch_size) -for X, y in test_dataloader: - print(f"Shape of X [N, C, H, W]: {X.shape}") - print(f"Shape of y: {y.shape} {y.dtype}") +for X_Gl, y_Gl in test_dataloader: + print(f"Shape of X [N, C, H, W]: {X_Gl.shape}") + print(f"Shape of y: {y_Gl.shape} {y_Gl.dtype}") break # Get cpu, gpu or mps device for training. @@ -43,13 +42,14 @@ ) print(f"Using {device} device") + # Define model class NeuralNetwork(nn.Module): def __init__(self): super().__init__() self.flatten = nn.Flatten() self.linear_relu_stack = nn.Sequential( - nn.Linear(28*28, 512), + nn.Linear(28 * 28, 512), nn.ReLU(), nn.Linear(512, 512), nn.ReLU(), @@ -61,11 +61,13 @@ def forward(self, x): logits = self.linear_relu_stack(x) return logits -model = NeuralNetwork().to(device) -print(model) -loss_fn = nn.CrossEntropyLoss() -optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) +MODEL = NeuralNetwork().to(device) +print(MODEL) + +LOSS_FN = nn.CrossEntropyLoss() +OPTIMIZER = torch.optim.SGD(MODEL.parameters(), lr=1e-3) + def train(dataloader, model, loss_fn, optimizer): size = len(dataloader.dataset) @@ -86,6 +88,7 @@ def train(dataloader, model, loss_fn, optimizer): loss, current = loss.item(), (batch + 1) * len(X) print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]") + def test(dataloader, model, loss_fn): size = len(dataloader.dataset) num_batches = len(dataloader) @@ -99,11 +102,39 @@ def test(dataloader, model, loss_fn): correct += (pred.argmax(1) == y).type(torch.float).sum().item() test_loss /= num_batches correct /= size - print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n") - -epochs = 5 -for t in range(epochs): - print(f"Epoch {t+1}\n-------------------------------") - train(train_dataloader, model, loss_fn, optimizer) - test(test_dataloader, model, loss_fn) -print("Done!") \ No newline at end of file + print(f"Test Error: \n Accuracy: {(100 * correct):>0.1f}%, Avg loss: {test_loss:>8f} \n") + + +EPOCHS = 5 # 5 +for t in range(EPOCHS): + print(f"Epoch {t + 1}\n-------------------------------") + train(train_dataloader, MODEL, LOSS_FN, OPTIMIZER) + test(test_dataloader, MODEL, LOSS_FN) +print("Done!") + +torch.save(MODEL.state_dict(), "model.pth") +print("Saved PyTorch Model State to model.pth") + +MODEL_2 = NeuralNetwork().to(device) +MODEL_2.load_state_dict(torch.load("model.pth", weights_only=True)) + +classes = [ + "T-shirt/top", + "Trouser", + "Pullover", + "Dress", + "Coat", + "Sandal", + "Shirt", + "Sneaker", + "Bag", + "Ankle boot", +] + +MODEL.eval() +x, y_ = test_data[0][0], test_data[0][1] +with torch.no_grad(): + x = x.to(device) + pred = MODEL(x) + predicted, actual = classes[pred[0].argmax(0)], classes[y_] + print(f'Predicted: "{predicted}", Actual: "{actual}"') diff --git a/uv.lock b/uv.lock index d2a49ee7..f2c81696 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,8 @@ version = 1 requires-python = ">=3.10" resolution-markers = [ - "python_full_version < '3.12'", + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", "python_full_version >= '3.12'", ] @@ -460,6 +461,7 @@ dependencies = [ { name = "black" }, { name = "graphviz" }, { name = "matplotlib" }, + { name = "pandas" }, { name = "pytest" }, { name = "ruff" }, { name = "scikit-learn" }, @@ -473,6 +475,7 @@ requires-dist = [ { name = "black", specifier = ">=24.10.0" }, { name = "graphviz", specifier = ">=0.20.3" }, { name = "matplotlib", specifier = ">=3.7.5" }, + { name = "pandas", specifier = ">=2.2.3" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "ruff", specifier = ">=0.8.1" }, { name = "scikit-learn", specifier = ">=1.3.2" }, @@ -699,6 +702,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -844,6 +895,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + [[package]] name = "ruff" version = "0.8.1" @@ -1134,6 +1194,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + [[package]] name = "virtualenv" version = "20.28.0"