jlprep 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.
jlprep/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ # jlprep/__init__.py
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Importando funções diretamente para facilitar o uso
6
+ from .cleaning import remove_duplicates, remove_outliers, clean_missing
7
+ from .encoding import encode_label, encode_onehot
8
+ from .exploration import summarize_data, value_counts_plot, check_data_quality
9
+ from .quality import check_class_balance
10
+ from .diagnostics import analyze_dataset
11
+ from .comparison import compare_datasets
12
+ from .reporting import generate_report
13
+
14
+ __all__ = ["remove_duplicates",
15
+ "remove_outliers",
16
+ "clean_missing",
17
+ "encode_label",
18
+ "encode_onehot",
19
+ "summarize_data",
20
+ "value_counts_plot",
21
+ "check_class_balance",
22
+ "check_data_quality",
23
+ "analyze_dataset",
24
+ "compare_datasets",
25
+ "generate_report"]
26
+
27
+
28
+ """
29
+ JlPrep - Biblioteca de pré-processamento de dados para Machine Learning
30
+
31
+ Desenvolvida por João Lucas para facilitar tarefas como:
32
+ - Limpeza de dados
33
+ - Tratamento de valores ausentes
34
+ - Codificação de variáveis categóricas
35
+ - Análise exploratória (em futuras versões)
36
+ """
jlprep/cleaning.py ADDED
@@ -0,0 +1,209 @@
1
+ __all__ = ["remove_duplicates",
2
+ "remove_outliers",
3
+ "clean_missing"]
4
+
5
+ import pandas as pd
6
+
7
+ def remove_duplicates(data: pd.DataFrame,
8
+ visualization: bool = False,
9
+ remove: bool = False) -> pd.DataFrame:
10
+ """
11
+ Remove ou visualiza duplicatas de um dataframe.
12
+
13
+ :param data: Dataframe com os dados a serem analisados.
14
+ :param visualization: Se True, visualiza as duplicatas no Dataframe.
15
+ :param remove: Se True, remove as duplicatas no Dataframe.
16
+ :return: Dataframe com ou sem duplicatas, dependendo do parâmetro 'remove'
17
+
18
+ """
19
+ #Verificando o comportamento
20
+ if visualization and remove:
21
+ print("Exibindo duplicatas")
22
+ print(data[data.duplicated()])
23
+ print("\nRemovendo duplicatas...")
24
+ return data.drop_duplicates()
25
+
26
+ elif visualization:
27
+
28
+ print("Exibindo duplicatas:")
29
+ return data[data.duplicated()]
30
+
31
+ elif remove:
32
+
33
+ print("Removendo duplicatas...")
34
+ return data.drop_duplicates()
35
+
36
+ else:
37
+
38
+ print("Nenhuma ação foi realizada. Use 'visualization' para visualizar ou 'remove' para remover duplicatas.")
39
+ return data
40
+
41
+
42
+
43
+ def remove_outliers(data: pd.DataFrame,
44
+ method:str = "iqr",
45
+ threshold: float = 1.5,
46
+ columns: list[str] | None = None) -> pd.DataFrame:
47
+
48
+ """
49
+ Remove outliers de um DataFrame com base em métodos estatísticos.
50
+
51
+ Parâmetros
52
+ ----------
53
+ data : pandas.DataFrame
54
+ Conjunto de dados a ser processado.
55
+ method : str, opcional (default='iqr')
56
+ Método usado para detecção de outliers. Pode ser:
57
+ - 'iqr': usa o intervalo interquartil (Q1 - 1.5*IQR, Q3 + 1.5*IQR)
58
+ - 'zscore': usa o desvio padrão em relação à média.
59
+ threshold : float, opcional (default=1.5)
60
+ Fator usado para definir o limite dos outliers.
61
+ - Para 'iqr': multiplicador do IQR.
62
+ - Para 'zscore': valor absoluto do Z-score máximo permitido.
63
+ columns : list ou None, opcional
64
+ Lista de colunas numéricas a serem analisadas. Se None, aplica em todas as colunas numéricas.
65
+
66
+ Retorna
67
+ -------
68
+ pandas.DataFrame
69
+ DataFrame sem os outliers detectados.
70
+
71
+ Lança
72
+ -----
73
+ ValueError
74
+ Se o método informado não for reconhecido ou se o threshold for menor ou igual a zero.
75
+
76
+ KeyError
77
+ Se alguma coluna informada não existir no DataFrame.
78
+
79
+ TypeError
80
+ Se alguma coluna informada não for numérica.
81
+ """
82
+
83
+ df = data.copy()
84
+
85
+ # Seleciona as colunas numéricas
86
+ if columns is None:
87
+ columns = df.select_dtypes(include=["number"]).columns.tolist()
88
+
89
+
90
+ # Valida as colunas
91
+ for col in columns:
92
+ if col not in df.columns:
93
+ raise KeyError(f"A coluna '{col}' não existe no DataFrame.")
94
+
95
+ # Verifica se as colunas são numéricas
96
+ if not pd.api.types.is_numeric_dtype(df[col]):
97
+ raise TypeError(f"A coluna '{col}' deve ser numérica")
98
+
99
+
100
+ if threshold <= 0:
101
+ raise ValueError("O threshold deve ser maior que zero.")
102
+
103
+ # Verifica o método escolhido
104
+ match method:
105
+ case "iqr":
106
+
107
+ for col in columns:
108
+ Q1 = df[col].quantile(0.25)
109
+ Q3 = df[col].quantile(0.75)
110
+ IQR = Q3 - Q1
111
+
112
+ lower_limit = Q1 - threshold * IQR
113
+ upper_limit = Q3 + threshold * IQR
114
+
115
+ df = df[(df[col] >= lower_limit) & (df[col] <= upper_limit)]
116
+
117
+ return df
118
+
119
+
120
+ case "zscore":
121
+
122
+ for col in columns:
123
+ mean = df[col].mean()
124
+ std = df[col].std()
125
+
126
+ z_scores = (df[col] - mean) / std
127
+
128
+ df = df[z_scores.abs() <= threshold]
129
+
130
+ return df
131
+
132
+ case _:
133
+ raise ValueError(f"Método '{method}' não reconhecido. Use 'iqr' ou 'zscore'.")
134
+
135
+
136
+
137
+
138
+ def clean_missing(
139
+ data: pd.DataFrame,
140
+ strategy: str = "drop",
141
+ columns: list[str] | None = None,
142
+ value: object | None = None,
143
+ threshold: float | None = None,
144
+ visualize: bool = False
145
+ ) -> pd.DataFrame:
146
+
147
+ """
148
+ Trata valores ausentes em um DataFrame com diferentes estratégias.
149
+
150
+ Parâmetros:
151
+ - data: DataFrame do pandas.
152
+ - strategy: Estratégia de tratamento. Pode ser 'drop', 'mean', 'median', 'mode', 'value' ou 'interpolate'.
153
+ - columns: Lista de colunas a serem tratadas. Se None, serão escolhidas com base na estratégia.
154
+ - value: Valor específico para preenchimento (usado apenas se strategy='value').
155
+ - threshold: Proporção de valores ausentes para remover colunas (usado apenas com strategy='drop').
156
+ - visualize: Se True, exibe o resumo de valores ausentes antes do tratamento.
157
+
158
+ Retorna:
159
+ - DataFrame com valores ausentes tratados.
160
+ """
161
+
162
+ df = data.copy()
163
+
164
+ if columns is None:
165
+ if strategy in ["mean", "median", "zscore"]:
166
+ columns = df.select_dtypes(include=["number"]).columns
167
+ elif strategy in ["mode", "value", "drop", "interpolate"]:
168
+ columns = df.columns
169
+ else:
170
+ raise ValueError(f"Estratégia '{strategy}' não reconhecida.")
171
+
172
+ match strategy:
173
+ case "drop":
174
+ if threshold is not None:
175
+
176
+ df = df.loc[:, df.isnull().mean() < threshold]
177
+ else:
178
+
179
+ df = df.dropna(subset=columns)
180
+
181
+ case "mean":
182
+ for col in columns:
183
+ df[col] = df[col].fillna(df[col].mean())
184
+
185
+ case "median":
186
+ for col in columns:
187
+ df[col] = df[col].fillna(df[col].median())
188
+
189
+ case "mode":
190
+ for col in columns:
191
+ mode = df[col].mode()
192
+ if not mode.empty:
193
+ df[col] = df[col].fillna(mode[0])
194
+
195
+ case "value":
196
+ if value is None:
197
+ raise ValueError("Para a estratégia 'value', o parâmetro 'value' deve ser fornecido.")
198
+ for col in columns:
199
+ df[col] = df[col].fillna(value)
200
+
201
+ case "interpolate":
202
+ df[columns] = df[columns].interpolate()
203
+
204
+ case _:
205
+ raise ValueError(f"Estratégia '{strategy}' não reconhecida.")
206
+
207
+ return df
208
+
209
+
jlprep/comparison.py ADDED
@@ -0,0 +1,55 @@
1
+
2
+ __all__ = ["comparison"]
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def compare_datasets(
8
+ original: pd.DataFrame,
9
+ processed: pd.DataFrame
10
+ ) -> dict:
11
+ """
12
+ Compara indicadores de dois DataFrames antes e depois
13
+ de um procedimento de pré-processamento.
14
+ """
15
+ if not isinstance(original, pd.DataFrame):
16
+ raise TypeError("original deve ser um DataFrame.")
17
+
18
+ if not isinstance(processed, pd.DataFrame):
19
+ raise TypeError("processed deve ser um DataFrame.")
20
+
21
+ def metrics(df):
22
+ total_cells = df.shape[0] * df.shape[1]
23
+ missing = int(df.isna().sum().sum())
24
+
25
+ return {
26
+ "rows": len(df),
27
+ "columns": len(df.columns),
28
+ "missing_values": missing,
29
+ "missing_percentage": round(
30
+ missing / total_cells * 100, 2
31
+ ) if total_cells else 0.0,
32
+ "duplicates": int(df.duplicated().sum()),
33
+ }
34
+
35
+ before = metrics(original)
36
+ after = metrics(processed)
37
+
38
+ return {
39
+ "before": before,
40
+ "after": after,
41
+ "changes": {
42
+ "row_difference": after["rows"] - before["rows"],
43
+ "column_difference": (
44
+ after["columns"] - before["columns"]
45
+ ),
46
+ "missing_difference": (
47
+ after["missing_values"]
48
+ - before["missing_values"]
49
+ ),
50
+ "duplicate_difference": (
51
+ after["duplicates"] - before["duplicates"]
52
+ ),
53
+ },
54
+ }
55
+
jlprep/diagnostics.py ADDED
@@ -0,0 +1,129 @@
1
+ __all__ = ["diagnostics"]
2
+
3
+ import pandas as pd
4
+
5
+
6
+ def analyze_dataset(df: pd.DataFrame, target: str | None = None) -> dict:
7
+ """
8
+ Analisa um DataFrame e gera um diagnóstico com recomendações.
9
+
10
+ A análise não modifica os dados originais.
11
+ """
12
+ if not isinstance(df, pd.DataFrame):
13
+ raise TypeError("df deve ser um DataFrame do Pandas.")
14
+
15
+ if target is not None and target not in df.columns:
16
+ raise KeyError(f"Coluna-alvo '{target}' não encontrada.")
17
+
18
+ alerts = []
19
+
20
+ # 1. Valores ausentes
21
+ for column in df.columns:
22
+ missing_count = int(df[column].isna().sum())
23
+
24
+ if missing_count == 0:
25
+ continue
26
+
27
+ missing_pct = (
28
+ missing_count / len(df) * 100 if len(df) else 0.0
29
+ )
30
+
31
+ if pd.api.types.is_numeric_dtype(df[column]):
32
+ suggestion = (
33
+ "Investigar a distribuição da variável e avaliar "
34
+ "imputação pela mediana ou outra estratégia adequada."
35
+ )
36
+ else:
37
+ suggestion = (
38
+ "Investigar os valores ausentes e avaliar "
39
+ "imputação pela moda ou criação de uma categoria."
40
+ )
41
+
42
+ alerts.append({
43
+ "type": "missing_values",
44
+ "column": column,
45
+ "count": missing_count,
46
+ "percentage": round(missing_pct, 2),
47
+ "recommendation": suggestion,
48
+ })
49
+
50
+ # 2. Registros duplicados
51
+ duplicates = int(df.duplicated().sum())
52
+
53
+ if duplicates:
54
+ alerts.append({
55
+ "type": "duplicates",
56
+ "count": duplicates,
57
+ "recommendation": (
58
+ "Verificar se os registros repetidos são erros "
59
+ "antes de removê-los."
60
+ ),
61
+ })
62
+
63
+ # 3. Outliers pelo método IQR
64
+ numeric_columns = df.select_dtypes(include="number").columns
65
+
66
+ for column in numeric_columns:
67
+ if column == target:
68
+ continue
69
+
70
+ values = df[column].dropna()
71
+
72
+ if len(values) < 4:
73
+ continue
74
+
75
+ q1 = values.quantile(0.25)
76
+ q3 = values.quantile(0.75)
77
+ iqr = q3 - q1
78
+
79
+ if iqr == 0:
80
+ continue
81
+
82
+ lower = q1 - 1.5 * iqr
83
+ upper = q3 + 1.5 * iqr
84
+
85
+ outlier_count = int(
86
+ ((values < lower) | (values > upper)).sum()
87
+ )
88
+
89
+ if outlier_count:
90
+ alerts.append({
91
+ "type": "outliers",
92
+ "column": column,
93
+ "count": outlier_count,
94
+ "method": "IQR",
95
+ "recommendation": (
96
+ "Investigar os valores extremos antes de "
97
+ "decidir pela remoção ou transformação."
98
+ ),
99
+ })
100
+
101
+ # 4. Balanceamento das classes
102
+ if target is not None:
103
+ counts = df[target].value_counts(dropna=True)
104
+
105
+ if len(counts) >= 2:
106
+ majority = int(counts.max())
107
+ minority = int(counts.min())
108
+ ratio = majority / minority
109
+
110
+ if ratio >= 3:
111
+ alerts.append({
112
+ "type": "class_imbalance",
113
+ "column": target,
114
+ "ratio": round(ratio, 2),
115
+ "recommendation": (
116
+ "Investigar o impacto do desbalanceamento "
117
+ "e avaliar métricas por classe e "
118
+ "estratégias de amostragem."
119
+ ),
120
+ })
121
+
122
+ return {
123
+ "rows": len(df),
124
+ "columns": len(df.columns),
125
+ "target": target,
126
+ "total_alerts": len(alerts),
127
+ "alerts": alerts,
128
+ }
129
+
jlprep/encoding.py ADDED
@@ -0,0 +1,116 @@
1
+ __all__ = ["encode_label",
2
+ "encode_onehot"]
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def encode_label(data: pd.DataFrame,
8
+ columns: list[str] | None = None,
9
+ return_mapping: bool = False) -> pd.DataFrame:
10
+
11
+
12
+ """
13
+
14
+ Aplica codificação label encoding em colunas categóricas de um Dataframe.
15
+
16
+ Cada valor único de texto é substituído por um número inteiro, facilitando o uso
17
+ de dados categóricos em algoritmos de Machine Learning.
18
+
19
+ Parâmetros:
20
+ ----------
21
+ data: pandas.Dataframe
22
+ O DataFrame de entrada que contém os dados a serem codificados
23
+
24
+ columns: list (opcional)
25
+ Lista de colunas a serem codificadas. Se "None", todas as colunas do tipo 'object' serão usadas.
26
+
27
+ return_mapping : bool, opcional
28
+ Se True, retorna também um dicionário com os mapeamentos de rótulo para número inteiro
29
+ para cada coluna codificada.
30
+
31
+ Retorna:
32
+ -------
33
+ pandas.DataFrame
34
+ O DataFrame com as colunas especificadas codificadas.
35
+
36
+ dict (opcional)
37
+ Um dicionário contendo os mapeamentos usados para cada coluna, se return_mapping=True.
38
+ """
39
+
40
+ if columns is None:
41
+ columns = data.select_dtypes(include=["object", "string", "category"]).columns
42
+
43
+ if return_mapping:
44
+ mapping_dict = {}
45
+
46
+ for col in columns:
47
+ unique_values = data[col].unique()
48
+ mapping = {label: idx for idx, label in enumerate(unique_values)}
49
+ data[col] = data[col].map(mapping)
50
+
51
+ if return_mapping:
52
+ mapping_dict[col] = mapping
53
+
54
+
55
+ if return_mapping:
56
+ return data, mapping_dict
57
+ return data
58
+
59
+
60
+
61
+ def encode_onehot(
62
+ data: pd.DataFrame,
63
+ columns: list[str] | None = None,
64
+ drop_first: bool = False
65
+ ) -> pd.DataFrame:
66
+ """
67
+ Codifica variáveis categóricas usando One-Hot Encoding.
68
+
69
+ Parâmetros
70
+ ----------
71
+ data : pandas.DataFrame
72
+ DataFrame contendo os dados.
73
+
74
+ columns : list[str] | None, opcional
75
+ Lista das colunas categóricas que serão codificadas.
76
+ Se None, todas as colunas dos tipos 'object', 'string' e 'category'
77
+ serão utilizadas.
78
+
79
+ drop_first : bool, opcional
80
+ Se True, remove a primeira categoria de cada coluna codificada,
81
+ evitando multicolinearidade.
82
+
83
+ Retorna
84
+ -------
85
+ pandas.DataFrame
86
+ Novo DataFrame contendo as colunas categóricas codificadas.
87
+
88
+ Levanta
89
+ -------
90
+ KeyError
91
+ Se alguma coluna informada não existir no DataFrame.
92
+
93
+ TypeError
94
+ Se alguma coluna informada não for categórica.
95
+ """
96
+
97
+ if columns is None:
98
+ columns = data.select_dtypes(include=["object", "string", "category"]).columns.tolist()
99
+
100
+ for col in columns:
101
+ if col not in data.columns:
102
+ raise KeyError(f"A coluna '{col}' não existe no DataFrame.")
103
+
104
+ if not (
105
+ pd.api.types.is_object_dtype(data[col])
106
+ or pd.api.types.is_string_dtype(data[col])
107
+ or pd.api.types.is_categorical_dtype(data[col])
108
+ ):
109
+ raise TypeError(f"A coluna '{col}' deve ser categórica.")
110
+
111
+ return pd.get_dummies(
112
+ data,
113
+ columns=columns,
114
+ drop_first=drop_first,
115
+ dtype=int
116
+ )
jlprep/exploration.py ADDED
@@ -0,0 +1,254 @@
1
+ # preprocess/exploration.py
2
+ from pandas.api.types import (
3
+ is_numeric_dtype,
4
+ is_categorical_dtype,
5
+ is_object_dtype,
6
+ is_bool_dtype,
7
+ )
8
+
9
+ import pandas as pd
10
+
11
+ __all__ = ["summarize_data",
12
+ "value_counts_plot",
13
+ "check_data_quality"]
14
+
15
+ def summarize_data(data: pd.DataFrame):
16
+ """
17
+ Gera um resumo exploratório do DataFrame:
18
+ - Tamanho (linhas/colunas)
19
+ - Tipos de dados por coluna
20
+ - Valores ausentes (contagem e %)
21
+ - Estatísticas por coluna:
22
+ * Numéricas: média, mediana, moda, min, max
23
+ * Categóricas (object/category/bool): moda, nº de categorias únicas, freq. da moda
24
+ Retorna:
25
+ dict: {nome_da_coluna: {estatísticas...}}
26
+ """
27
+ # 1) Tamanho
28
+ print("Número de linhas e colunas:", len(data), "linhas,", len(data.columns), "colunas")
29
+
30
+ # 2) Tipos
31
+ print("\nTipos de dados por coluna:")
32
+ print(data.dtypes)
33
+
34
+ # 3) Ausentes (contagem e %)
35
+ print("\nValores ausentes por coluna (ordem decrescente):")
36
+ print(data.isnull().sum().sort_values(ascending=False))
37
+
38
+ print("\nPorcentagem de valores ausentes por coluna:")
39
+ print(((data.isnull().sum() / len(data)) * 100).round(2))
40
+
41
+ # 4) Amostra
42
+ print("\n5 primeiras linhas do DataFrame:")
43
+ print(data.head(5))
44
+
45
+ # 5) Estatísticas por coluna
46
+ resumo = {}
47
+ for col in data.columns:
48
+ if is_numeric_dtype(data[col]):
49
+ s = data[col].dropna()
50
+ moda_series = s.mode()
51
+ moda = moda_series.iloc[0] if not moda_series.empty else None
52
+ resumo[col] = {
53
+ "média": s.mean() if not s.empty else None,
54
+ "mediana": s.median() if not s.empty else None,
55
+ "moda": moda,
56
+ "mínimo": s.min() if not s.empty else None,
57
+ "máximo": s.max() if not s.empty else None,
58
+ }
59
+ elif is_categorical_dtype(data[col]) or is_object_dtype(data[col]) or is_bool_dtype(data[col]):
60
+ s = data[col].dropna()
61
+ moda_series = s.mode()
62
+ moda = moda_series.iloc[0] if not moda_series.empty else None
63
+ freq_moda = s.value_counts().iloc[0] if not s.value_counts().empty else 0
64
+ resumo[col] = {
65
+ "moda": moda,
66
+ "n_categorias": s.nunique(),
67
+ "freq_moda": int(freq_moda),
68
+ }
69
+ else:
70
+ # Para tipos como datetime, etc., um resumo simples:
71
+ s = data[col].dropna()
72
+ resumo[col] = {
73
+ "tipo": str(data[col].dtype),
74
+ "n_únicos": s.nunique(),
75
+ }
76
+
77
+ # Imprime o resumo
78
+ for col, stats in resumo.items():
79
+ print(f"\nColuna: {col}")
80
+ for k, v in stats.items():
81
+ print(f" {k}: {v}")
82
+
83
+ return resumo
84
+
85
+
86
+ def value_counts_plot(
87
+ data: pd.DataFrame,
88
+ column: str,
89
+ normalize: bool = False,
90
+ top_n: int | None = None
91
+ ) -> None:
92
+
93
+ import matplotlib.pyplot as plt
94
+
95
+ """
96
+ Gera um gráfico de barras com a contagem (ou proporção) dos valores únicos
97
+ em uma coluna categórica.
98
+
99
+ Parâmetros
100
+ ----------
101
+ data : pandas.DataFrame
102
+ O DataFrame com os dados.
103
+ column : str
104
+ Nome da coluna categórica a ser analisada.
105
+ normalize : bool, opcional (default=False)
106
+ Se True, mostra proporções (%) em vez de contagens absolutas.
107
+ top_n : int, opcional
108
+ Se definido, mostra apenas as top N categorias mais frequentes.
109
+
110
+ Retorno
111
+ -------
112
+ None
113
+ Exibe o gráfico.
114
+ """
115
+
116
+ #Obtendo frequências
117
+ counts = data[column].value_counts(normalize=normalize)
118
+
119
+ if top_n is not None:
120
+ counts = counts.head(top_n)
121
+
122
+ counts.plot(kind="bar")
123
+ plt.title(f"Distribuição de valores - {column}")
124
+ plt.xlabel("Categorias")
125
+ plt.ylabel("Frequências (%)" if normalize else "Frequências")
126
+ plt.show()
127
+
128
+
129
+
130
+ def check_data_quality(data: pd.DataFrame):
131
+ """
132
+ Analisa a qualidade dos dados de um DataFrame.
133
+
134
+ Funcionalidades:
135
+ - Valores ausentes (%)
136
+ - Duplicatas por coluna (%)
137
+ - Outliers (IQR) para colunas numéricas (quantidade)
138
+ - Cardinalidade de colunas categóricas (com gráfico)
139
+
140
+ Parâmetros
141
+ ----------
142
+ data : pandas.DataFrame
143
+ O DataFrame a ser analisado.
144
+
145
+ Retornos
146
+ -------
147
+ dict
148
+ Dicionários com informações sobre valores ausentes, duplicatas,
149
+ outliers e cardinalidade.
150
+ """
151
+
152
+ import numpy as np
153
+ import matplotlib.pyplot as plt
154
+ from pandas.api.types import is_numeric_dtype, is_object_dtype, is_categorical_dtype
155
+
156
+ dict_missing_col = {}
157
+ dict_duplicated_col = {}
158
+ dict_outliers_col = {}
159
+ dict_cardinality = {}
160
+
161
+ total_rows = len(data)
162
+
163
+ # Valores ausentes
164
+ for col in data.columns:
165
+ percentual_missing = (data[col].isnull().sum() / total_rows) * 100
166
+ dict_missing_col[col] = round(float(percentual_missing), 2)
167
+
168
+ # Duplicatas
169
+ for col in data.columns:
170
+ duplicated_count = total_rows - data[col].nunique()
171
+ duplicated_percent = (duplicated_count / total_rows) * 100
172
+ dict_duplicated_col[col] = {
173
+ "quantidade": int(duplicated_count),
174
+ "percentual": round(float(duplicated_percent), 2)
175
+ }
176
+
177
+ # Outliers (IQR)
178
+ for col in data.columns:
179
+ if is_numeric_dtype(data[col]):
180
+ col_clean = data[col].dropna()
181
+
182
+ Q1 = np.percentile(col_clean, 25)
183
+ Q3 = np.percentile(col_clean, 75)
184
+ IQR = Q3 - Q1
185
+
186
+ lower_bound = Q1 - 1.5 * IQR
187
+ upper_bound = Q3 + 1.5 * IQR
188
+
189
+ outliers_count = ((col_clean < lower_bound) | (col_clean > upper_bound)).sum()
190
+ dict_outliers_col[col] = int(outliers_count)
191
+
192
+ # Cardinalidade (object + categorical)
193
+ for col in data.columns:
194
+ if is_object_dtype(data[col]) or is_categorical_dtype(data[col]):
195
+ unique_values = data[col].nunique()
196
+ proportion = (unique_values / total_rows) * 100
197
+
198
+ dict_cardinality[col] = {
199
+ "unicos": int(unique_values),
200
+ "percentual": round(float(proportion), 2)
201
+ }
202
+
203
+ # PRINT ORGANIZADO
204
+ print("\n=== QUALIDADE DOS DADOS ===\n")
205
+
206
+ print("🔹 Valores ausentes:")
207
+ for col, perc in dict_missing_col.items():
208
+ if perc > 0:
209
+ print(f"- {col}: {perc}%")
210
+
211
+ print("\n🔹 Duplicatas:")
212
+ for col, info in dict_duplicated_col.items():
213
+ if info["quantidade"] > 0:
214
+ print(f"- {col}: {info['quantidade']} ({info['percentual']}%)")
215
+
216
+ print("\n🔹 Outliers:")
217
+ has_outliers = False
218
+ for col, count in dict_outliers_col.items():
219
+ if count > 0:
220
+ print(f"- {col}: {count} outliers")
221
+ has_outliers = True
222
+ if not has_outliers:
223
+ print("- Nenhum outlier detectado")
224
+
225
+ print("\n🔹 Cardinalidade:")
226
+ if dict_cardinality:
227
+ for col, info in dict_cardinality.items():
228
+ print(f"- {col}: {info['unicos']} valores únicos ({info['percentual']}%)")
229
+
230
+ if info["percentual"] > 50:
231
+ print(f" ⚠️ Alta cardinalidade, considerar tratamento")
232
+ else:
233
+ print("- Nenhuma coluna categórica detectada")
234
+
235
+ # Gráfico de cardinalidade
236
+ if dict_cardinality:
237
+ cols = list(dict_cardinality.keys())
238
+ percents = [info["percentual"] for info in dict_cardinality.values()]
239
+
240
+ plt.figure(figsize=(10, 6))
241
+ plt.bar(cols, percents)
242
+ plt.xticks(rotation=45, ha="right")
243
+ plt.ylabel("Percentual de cardinalidade")
244
+ plt.title("Cardinalidade das colunas categóricas")
245
+ plt.show()
246
+
247
+ # Retorno estruturado
248
+ return {
249
+ "missing": dict_missing_col,
250
+ "duplicated": dict_duplicated_col,
251
+ "outliers": dict_outliers_col,
252
+ "cardinality": dict_cardinality
253
+ }
254
+
jlprep/quality.py ADDED
@@ -0,0 +1,122 @@
1
+ # preprocess/exploration.py
2
+ from pandas.api.types import (
3
+ is_numeric_dtype,
4
+ is_categorical_dtype,
5
+ is_object_dtype,
6
+ is_bool_dtype,
7
+ )
8
+
9
+ import pandas as pd
10
+ import matplotlib.pyplot as plt
11
+
12
+ __all__ = ["check_class_balance"]
13
+
14
+ def check_class_balance(
15
+ data: pd.DataFrame,
16
+ target: str,
17
+ plot: bool = False
18
+ ) -> dict:
19
+ """
20
+ Analisa o balanceamento das classes em uma variável alvo de um dataset.
21
+
22
+ Esta função calcula a distribuição das classes, identifica o tipo de problema
23
+ (classificação binária ou multiclasse), mede o nível de desbalanceamento por meio
24
+ da razão entre a classe majoritária e minoritária (imbalance ratio) e fornece
25
+ um diagnóstico com sugestões de tratamento.
26
+
27
+ Parâmetros
28
+ ----------
29
+ data : pandas.DataFrame
30
+ Dataset contendo a variável alvo.
31
+
32
+ target : str
33
+ Nome da coluna que representa a variável alvo.
34
+
35
+ plot : bool, opcional (default=False)
36
+ Se True, exibe um gráfico de barras com a distribuição das classes.
37
+
38
+ Retorna
39
+ -------
40
+ dict
41
+ Um dicionário contendo:
42
+ - n_classes
43
+ - problem_type
44
+ - imbalance_ratio
45
+ - status
46
+ - suggestion
47
+
48
+ Levanta
49
+ -------
50
+ KeyError
51
+ Se a coluna especificada não existir.
52
+
53
+ TypeError
54
+ Se o parâmetro target não for uma string.
55
+
56
+ ValueError
57
+ Se a variável alvo possuir menos de duas classes.
58
+ """
59
+
60
+ if not isinstance(target, str):
61
+ raise TypeError("O parâmetro 'target' deve ser uma string.")
62
+
63
+ if target not in data.columns:
64
+ raise KeyError(f"A coluna '{target}' não existe no DataFrame.")
65
+
66
+ col = data[target].dropna()
67
+
68
+ class_count = col.value_counts()
69
+ class_proportion = col.value_counts(normalize=True) * 100
70
+
71
+ n_classes = len(class_count)
72
+
73
+ if n_classes == 2:
74
+ problem = "Binary classification"
75
+ elif n_classes > 2:
76
+ problem = "Multiclass classification"
77
+ else:
78
+ raise ValueError("O dataset deve possuir pelo menos duas classes.")
79
+
80
+ max_class = class_count.max()
81
+ min_class = class_count.min()
82
+
83
+ imbalance_ratio = max_class / min_class
84
+
85
+ if imbalance_ratio <= 1.5:
86
+ status = "Dataset balanceado"
87
+ suggestion = "Nenhuma ação necessária."
88
+
89
+ elif imbalance_ratio <= 3:
90
+ status = "Dataset levemente desbalanceado"
91
+ suggestion = "Monitorar o desempenho do modelo."
92
+
93
+ elif imbalance_ratio <= 10:
94
+ status = "Dataset moderadamente desbalanceado"
95
+ suggestion = "Considerar técnicas de balanceamento."
96
+
97
+ else:
98
+ status = "Dataset altamente desbalanceado"
99
+ suggestion = "Aplicar técnicas de balanceamento (SMOTE, Oversampling, etc.)."
100
+
101
+ print("Distribuição das classes:\n")
102
+
103
+ for classe in class_count.index:
104
+ print(f"{classe}: {class_count[classe]} ({class_proportion[classe]:.2f}%)")
105
+
106
+ print(f"\nNúmero de classes: {n_classes}")
107
+ print(f"Tipo de problema: {problem}")
108
+ print(f"\nImbalance ratio: {imbalance_ratio:.2f}")
109
+ print(f"Status: {status}")
110
+ print(f"Sugestão: {suggestion}")
111
+
112
+ if plot:
113
+ class_count.plot(kind="bar", title="Distribuição das classes")
114
+ plt.show()
115
+
116
+ return {
117
+ "n_classes": n_classes,
118
+ "problem_type": problem,
119
+ "imbalance_ratio": imbalance_ratio,
120
+ "status": status,
121
+ "suggestion": suggestion
122
+ }
jlprep/reporting.py ADDED
@@ -0,0 +1,288 @@
1
+ """Geração de relatórios HTML a partir de analyze_dataset e compare_datasets."""
2
+ from __future__ import annotations
3
+
4
+ __all__ = ["reporting"]
5
+
6
+
7
+ from datetime import datetime
8
+ from html import escape
9
+ from pathlib import Path
10
+ from typing import Any
11
+ import webbrowser
12
+
13
+
14
+ def _text(value: Any) -> str:
15
+ return escape(str(value), quote=True)
16
+
17
+
18
+ def _number(value: Any) -> str:
19
+ return _text(value if value is not None else "—")
20
+
21
+
22
+ def generate_report(
23
+ diagnosis: dict,
24
+ comparison: dict | None = None,
25
+ output_path: str | Path = "jlprep_report.html",
26
+ title: str = "Relatório de qualidade dos dados — JlPrep",
27
+ open_browser: bool = False,
28
+ ) -> Path:
29
+ """Gera um relatório HTML local com diagnóstico e comparação."""
30
+
31
+ if not isinstance(diagnosis, dict) or not isinstance(
32
+ diagnosis.get("alerts"), list
33
+ ):
34
+ raise TypeError(
35
+ "diagnosis deve ser o retorno de analyze_dataset()."
36
+ )
37
+
38
+ if comparison is not None and not isinstance(comparison, dict):
39
+ raise TypeError(
40
+ "comparison deve ser o retorno de compare_datasets()."
41
+ )
42
+
43
+ path = Path(output_path).expanduser().resolve()
44
+
45
+ if path.suffix.lower() != ".html":
46
+ raise ValueError("O arquivo deve terminar em .html")
47
+
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+
50
+ labels = {
51
+ "missing_values": "Valores ausentes",
52
+ "duplicates": "Registros duplicados",
53
+ "outliers": "Valores extremos (IQR)",
54
+ "class_imbalance": "Desbalanceamento de classes",
55
+ }
56
+
57
+ metric_labels = {
58
+ "rows": "Registros",
59
+ "columns": "Colunas",
60
+ "missing_values": "Células ausentes",
61
+ "missing_percentage": "Ausentes (%)",
62
+ "duplicates": "Duplicatas",
63
+ }
64
+
65
+ alerts = diagnosis["alerts"]
66
+ alert_html = []
67
+
68
+ for alert in alerts:
69
+ if not isinstance(alert, dict):
70
+ continue
71
+
72
+ details = []
73
+
74
+ fields = [
75
+ ("column", "Coluna"),
76
+ ("count", "Ocorrências"),
77
+ ("percentage", "Percentual (%)"),
78
+ ("ratio", "Razão"),
79
+ ("method", "Método"),
80
+ ]
81
+
82
+ for key, label in fields:
83
+ if key in alert:
84
+ details.append(
85
+ f'<span class="chip">'
86
+ f'{label}: {_text(alert[key])}'
87
+ f'</span>'
88
+ )
89
+
90
+ recommendation = alert.get(
91
+ "recommendation",
92
+ "Investigar o indicador antes de decidir o tratamento.",
93
+ )
94
+
95
+ alert_html.append(
96
+ '<article class="alert">'
97
+ f'<h3>{_text(labels.get(alert.get("type"), alert.get("type", "Alerta")))}</h3>'
98
+ f'<div class="chips">{"".join(details)}</div>'
99
+ f'<p><strong>Recomendação:</strong> '
100
+ f'{_text(recommendation)}</p>'
101
+ '</article>'
102
+ )
103
+
104
+ if not alert_html:
105
+ alert_html = [
106
+ '<p class="empty">Nenhum alerta encontrado pelas regras '
107
+ 'implementadas. Isso não garante ausência de problemas.</p>'
108
+ ]
109
+
110
+ comparison_html = ""
111
+
112
+ if comparison is not None:
113
+ before = comparison.get("before", {})
114
+ after = comparison.get("after", {})
115
+
116
+ if not isinstance(before, dict) or not isinstance(after, dict):
117
+ raise ValueError(
118
+ "comparison precisa conter 'before' e 'after'."
119
+ )
120
+
121
+ rows = []
122
+
123
+ for key, label in metric_labels.items():
124
+ if key in before or key in after:
125
+ rows.append(
126
+ f'<tr><th scope="row">{_text(label)}</th>'
127
+ f'<td>{_number(before.get(key))}</td>'
128
+ f'<td>{_number(after.get(key))}</td></tr>'
129
+ )
130
+
131
+ comparison_html = (
132
+ '<section class="panel">'
133
+ '<h2>Comparação antes e depois</h2>'
134
+ '<p class="muted">Indicadores agregados; '
135
+ 'não identifica alterações individuais de registros.</p>'
136
+ '<div class="table-wrap"><table>'
137
+ '<thead><tr><th>Indicador</th>'
138
+ '<th>Antes</th><th>Depois</th></tr></thead>'
139
+ f'<tbody>{"".join(rows)}</tbody>'
140
+ '</table></div></section>'
141
+ )
142
+
143
+ generated = datetime.now().strftime("%d/%m/%Y %H:%M")
144
+
145
+ html = f"""<!doctype html>
146
+ <html lang="pt-BR">
147
+ <head>
148
+ <meta charset="utf-8">
149
+ <meta name="viewport" content="width=device-width, initial-scale=1">
150
+ <title>{_text(title)}</title>
151
+ <style>
152
+ :root {{
153
+ --bg: #f4f7fb;
154
+ --ink: #17243a;
155
+ --muted: #596b82;
156
+ --line: #dce5ef;
157
+ --brand: #2357a4;
158
+ --paper: #fff;
159
+ }}
160
+ * {{ box-sizing: border-box; }}
161
+ body {{
162
+ margin: 0;
163
+ background: var(--bg);
164
+ font: 16px/1.55 system-ui, sans-serif;
165
+ color: var(--ink);
166
+ }}
167
+ header {{
168
+ background: linear-gradient(110deg, #17396d, #2862b1);
169
+ color: white;
170
+ padding: 38px max(24px, calc((100vw - 1040px)/2));
171
+ }}
172
+ header p {{ opacity: .88; margin: 6px 0 0; }}
173
+ main {{
174
+ max-width: 1040px;
175
+ margin: 30px auto 70px;
176
+ padding: 0 20px;
177
+ }}
178
+ h1 {{ font-size: 2rem; margin: 0; }}
179
+ h2 {{ margin: 0 0 14px; font-size: 1.25rem; }}
180
+ h3 {{ margin: 0 0 8px; font-size: 1.03rem; }}
181
+ .cards {{
182
+ display: grid;
183
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
184
+ gap: 14px;
185
+ margin-bottom: 24px;
186
+ }}
187
+ .card, .panel {{
188
+ background: var(--paper);
189
+ border: 1px solid var(--line);
190
+ border-radius: 14px;
191
+ box-shadow: 0 4px 16px #17243a0a;
192
+ }}
193
+ .card {{ padding: 20px; }}
194
+ .card .value {{
195
+ font-size: 2rem;
196
+ font-weight: 750;
197
+ color: var(--brand);
198
+ }}
199
+ .card .label {{
200
+ color: var(--muted);
201
+ font-size: .9rem;
202
+ }}
203
+ .panel {{ padding: 24px; margin-bottom: 22px; }}
204
+ .alert {{
205
+ border-left: 4px solid #e6a43d;
206
+ background: #fffbf3;
207
+ padding: 16px 18px;
208
+ margin: 12px 0;
209
+ border-radius: 7px;
210
+ }}
211
+ .alert p {{ margin: 10px 0 0; }}
212
+ .chips {{ display: flex; gap: 8px; flex-wrap: wrap; }}
213
+ .chip {{
214
+ background: #e8eef7;
215
+ border-radius: 999px;
216
+ padding: 3px 10px;
217
+ font-size: .85rem;
218
+ }}
219
+ .muted, .empty {{ color: var(--muted); }}
220
+ .table-wrap {{ overflow-x: auto; }}
221
+ table {{ width: 100%; border-collapse: collapse; text-align: left; }}
222
+ th, td {{ padding: 12px 14px; border-bottom: 1px solid var(--line); }}
223
+ thead {{ background: #eaf1fa; }}
224
+ footer {{
225
+ text-align: center;
226
+ color: var(--muted);
227
+ font-size: .85rem;
228
+ margin-top: 25px;
229
+ }}
230
+ @media print {{
231
+ body {{ background: white; }}
232
+ .panel, .card {{ box-shadow: none; break-inside: avoid; }}
233
+ }}
234
+ </style>
235
+ </head>
236
+ <body>
237
+ <header>
238
+ <h1>{_text(title)}</h1>
239
+ <p>Gerado em {generated} · Diagnóstico automatizado</p>
240
+ </header>
241
+ <main>
242
+ <section class="cards">
243
+ <div class="card">
244
+ <div class="label">Registros</div>
245
+ <div class="value">{_number(diagnosis.get("rows"))}</div>
246
+ </div>
247
+ <div class="card">
248
+ <div class="label">Colunas</div>
249
+ <div class="value">{_number(diagnosis.get("columns"))}</div>
250
+ </div>
251
+ <div class="card">
252
+ <div class="label">Alertas</div>
253
+ <div class="value">{len(alerts)}</div>
254
+ </div>
255
+ <div class="card">
256
+ <div class="label">Variável-alvo</div>
257
+ <div class="value" style="font-size:1.3rem;overflow-wrap:anywhere">
258
+ {_text(diagnosis.get("target") or "Não definida")}
259
+ </div>
260
+ </div>
261
+ </section>
262
+
263
+ <section class="panel">
264
+ <h2>Diagnóstico e recomendações</h2>
265
+ {"".join(alert_html)}
266
+ </section>
267
+
268
+ {comparison_html}
269
+
270
+ <section class="panel">
271
+ <h2>Como interpretar</h2>
272
+ <p>Os alertas indicam situações que merecem investigação.
273
+ Valores extremos não são necessariamente erros, e nem toda
274
+ ausência ou duplicata deve ser removida. As recomendações
275
+ não substituem a análise do contexto dos dados.</p>
276
+ </section>
277
+
278
+ <footer>JlPrep · Relatório gerado localmente.</footer>
279
+ </main>
280
+ </body>
281
+ </html>"""
282
+
283
+ path.write_text(html, encoding="utf-8")
284
+
285
+ if open_browser:
286
+ webbrowser.open(path.as_uri())
287
+
288
+ return path
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: jlprep
3
+ Version: 0.1.0
4
+ Summary: Biblioteca Python para pré-processamento e análise da qualidade de dados
5
+ Author: Joao Lucas Moreira de Sousa
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: pandas
14
+ Requires-Dist: numpy
15
+ Requires-Dist: matplotlib
16
+
17
+ # jlprep
18
+
19
+ Biblioteca em Python para **análise e pré-processamento de dados** voltada para aplicações de Machine Learning.
20
+
21
+ A `jlprep` foi desenvolvida como parte de um **Trabalho de Conclusão de Curso (TCC)**, com foco na análise e melhoria da qualidade de dados utilizados em modelos de Machine Learning.
22
+
23
+ ---
24
+
25
+ ## Objetivo
26
+
27
+ A biblioteca busca centralizar etapas comuns do pré-processamento de dados, como:
28
+
29
+ * Limpeza de dados
30
+ * Codificação de variáveis categóricas
31
+ * Análise exploratória
32
+ * Avaliação da qualidade dos dados
33
+
34
+ Além disso, fornece **diagnósticos interpretáveis**, auxiliando na tomada de decisão antes do treinamento de modelos de Machine Learning.
35
+
36
+ ---
37
+
38
+ ## Contexto do TCC
39
+
40
+ Este projeto foi desenvolvido no contexto do tema:
41
+
42
+ > **Qualidade dos dados em Machine Learning (dados faltantes, desbalanceados e ruidosos)**
43
+
44
+ A proposta da `jlprep` é oferecer uma abordagem prática para identificação e tratamento desses problemas, reunindo funcionalidades essenciais em uma única biblioteca.
45
+
46
+ ---
47
+
48
+ ## Funcionalidades
49
+
50
+ ### Limpeza de dados
51
+
52
+ * Remoção de valores duplicados
53
+ * Tratamento de valores faltantes
54
+ * Remoção de outliers
55
+
56
+ ### Transformação
57
+
58
+ * Label Encoding
59
+ * One-Hot Encoding
60
+
61
+ ### Exploração de dados
62
+
63
+ * Resumo estatístico do dataset
64
+ * Visualização da distribuição de valores
65
+
66
+ ### Qualidade dos dados
67
+
68
+ * Verificação de inconsistências
69
+ * Análise de desbalanceamento de classes
70
+ * Sugestões de pré-processamento
71
+
72
+ ---
73
+
74
+ ## Instalação
75
+
76
+ Clone o repositório e acesse a pasta do projeto.
77
+
78
+ Em seguida, instale as dependências necessárias utilizando o arquivo `requirements.txt`:
79
+
80
+ ```bash
81
+ python -m pip install -r requirements.txt
82
+ ```
83
+
84
+ As principais dependências utilizadas pelo projeto são:
85
+
86
+ * `pandas`
87
+ * `matplotlib`
88
+ * `pytest`
89
+
90
+ ---
91
+
92
+ ## Testes
93
+
94
+ Os testes automatizados estão localizados na pasta `tests/`.
95
+
96
+ Para executar todos os testes, utilize:
97
+
98
+ ```bash
99
+ python -m pytest
100
+ ```
101
+
102
+ Para executar um teste específico:
103
+
104
+ ```bash
105
+ python -m pytest tests/test_encode_onehot.py
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Exemplo de uso
111
+
112
+ ```python
113
+ import pandas as pd
114
+
115
+ from jlprep import check_class_balance
116
+
117
+ df = pd.DataFrame({
118
+ "classe": ["A", "A", "B", "B", "A"]
119
+ })
120
+
121
+ resultado = check_class_balance(
122
+ df,
123
+ target="classe"
124
+ )
125
+
126
+ print(resultado)
127
+ ```
128
+
129
+ A função retorna um dicionário contendo informações sobre a quantidade de classes, o tipo de classificação, o nível de desbalanceamento e uma sugestão de tratamento.
130
+
131
+ ---
132
+
133
+ ## Estrutura do projeto
134
+
135
+ ```text
136
+ JlPrep/
137
+ ├── jlprep/
138
+ │ ├── __init__.py
139
+ │ ├── cleaning.py
140
+ │ ├── encoding.py
141
+ │ ├── exploration.py
142
+ │ └── quality.py
143
+ │
144
+ ├── tests/
145
+ │ ├── test_clean_missing.py
146
+ │ ├── test_encode_label.py
147
+ │ ├── test_encode_onehot.py
148
+ │ ├── test_remove_outliers.py
149
+ │ └── test_check_class_balance.py
150
+ │
151
+ ├── examples/
152
+ ├── requirements.txt
153
+ └── README.md
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Projeto acadêmico
159
+
160
+ A `jlprep` foi desenvolvida como parte de um Trabalho de Conclusão de Curso, com o objetivo de aplicar conceitos de programação em Python, pré-processamento de dados, qualidade de dados e testes automatizados em uma biblioteca voltada para aplicações de Machine Learning.
@@ -0,0 +1,12 @@
1
+ jlprep/__init__.py,sha256=G7F1Pm1CqC9h-0Yz-eDDd1z19iMhWULBtkO-EcUz2MM,1149
2
+ jlprep/cleaning.py,sha256=VH5AwLuJIqOJtKFIZqM1Q0vGNoDxdNv1SKoovcnyJAk,6600
3
+ jlprep/comparison.py,sha256=jFu23h_BCYRQstaFE68nG-dtdcIIb9mJgv0huQV_Vxc,1537
4
+ jlprep/diagnostics.py,sha256=d3qH4YgVd7LKRUerrPTTYBlI-kgC_rYUhxgwfDFON4w,3760
5
+ jlprep/encoding.py,sha256=-cmUGERgsxU-ulSFpZRXilCZqUaGMfml3uv8-Wu_nNI,3347
6
+ jlprep/exploration.py,sha256=4A3sU3XUWDi5P7OMxkjMprNxfbUE8So0gU-LAt52Oc8,8136
7
+ jlprep/quality.py,sha256=XnlzjUZ3uZdCUE1lc44uXYOO8s5zARchY2IeNQNJ0Pc,3517
8
+ jlprep/reporting.py,sha256=l68p7gcX5FdrAszVB89mWu14J_xBFezrWTzwT6rLxtE,8266
9
+ jlprep-0.1.0.dist-info/METADATA,sha256=obtheANuJN54oV-mqL_drF8Q87m0vLxmGXy8DlNLYBc,3962
10
+ jlprep-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ jlprep-0.1.0.dist-info/top_level.txt,sha256=GZfxq8otRhKKyaFna4n68IIDh1P-ISwvT6Eh1jZlfrc,7
12
+ jlprep-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
+ jlprep