dfm-python 0.2.6__py3-none-any.whl → 0.2.8__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.
- dfm_python/__init__.py +1 -1
- dfm_python/core/diagnostics.py +215 -6
- dfm_python/core/em.py +34 -28
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.8.dist-info}/METADATA +12 -4
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.8.dist-info}/RECORD +7 -7
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.8.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.8.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -50,7 +50,7 @@ Example (Low-level API - For advanced usage):
|
|
|
50
50
|
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
__version__ = "0.2.
|
|
53
|
+
__version__ = "0.2.8"
|
|
54
54
|
|
|
55
55
|
from .config import DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME
|
|
56
56
|
from .config import (
|
dfm_python/core/diagnostics.py
CHANGED
|
@@ -4,12 +4,10 @@ from typing import Optional, Dict, Any, Tuple, TYPE_CHECKING
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
PANDAS_AVAILABLE = False
|
|
12
|
-
pd = None
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from scipy.linalg import orthogonal_procrustes
|
|
9
|
+
|
|
10
|
+
PANDAS_AVAILABLE = True
|
|
13
11
|
|
|
14
12
|
from ..config import DFMConfig
|
|
15
13
|
|
|
@@ -96,6 +94,217 @@ def calculate_rmse(actual: np.ndarray, predicted: np.ndarray,
|
|
|
96
94
|
return rmse_overall, rmse_per_series
|
|
97
95
|
|
|
98
96
|
|
|
97
|
+
def evaluate_factor_estimation(
|
|
98
|
+
true_factors: np.ndarray,
|
|
99
|
+
estimated_factors: np.ndarray,
|
|
100
|
+
use_procrustes: bool = True
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""Evaluate how well factors are estimated (synthetic-data diagnostic).
|
|
103
|
+
|
|
104
|
+
This function compares known ``true_factors`` with ``estimated_factors``
|
|
105
|
+
(typically ``result.Z[:, :num_factors]``) and returns:
|
|
106
|
+
|
|
107
|
+
- Per-factor correlations (before any rotation)
|
|
108
|
+
- Optionally Procrustes-rotated factors and an overall correlation
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
true_factors : np.ndarray
|
|
113
|
+
Array of shape (T, k) with generated factors used to build the data.
|
|
114
|
+
estimated_factors : np.ndarray
|
|
115
|
+
Array of shape (T, k_est) with estimated factors. Only the first
|
|
116
|
+
``k = min(k, k_est)`` columns are compared.
|
|
117
|
+
use_procrustes : bool, default True
|
|
118
|
+
If True and SciPy is available, apply orthogonal Procrustes rotation
|
|
119
|
+
to align estimated factors with true factors before computing the
|
|
120
|
+
overall correlation.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
Dict[str, Any]
|
|
125
|
+
{
|
|
126
|
+
'num_factors': int,
|
|
127
|
+
'correlation_per_factor': np.ndarray (k,),
|
|
128
|
+
'overall_correlation': float or None,
|
|
129
|
+
'rotation_matrix': np.ndarray (k, k) or None,
|
|
130
|
+
'aligned_factors': np.ndarray (T, k) or None,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Notes
|
|
134
|
+
-----
|
|
135
|
+
- This is primarily intended for synthetic-data experiments where the
|
|
136
|
+
true latent factors are known.
|
|
137
|
+
- Correlations are computed factor-by-factor, ignoring any time points
|
|
138
|
+
with NaN values.
|
|
139
|
+
- Procrustes rotation is useful because factors are only identified up
|
|
140
|
+
to sign, scale (if not constrained), and rotation in general.
|
|
141
|
+
"""
|
|
142
|
+
if true_factors.ndim != 2 or estimated_factors.ndim != 2:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"true_factors and estimated_factors must be 2D, "
|
|
145
|
+
f"got shapes {true_factors.shape} and {estimated_factors.shape}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
T_true, k_true = true_factors.shape
|
|
149
|
+
T_est, k_est = estimated_factors.shape
|
|
150
|
+
if T_true != T_est:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Time dimension must match, got {T_true} and {T_est}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
k = min(k_true, k_est)
|
|
156
|
+
if k == 0:
|
|
157
|
+
raise ValueError("At least one factor is required for comparison.")
|
|
158
|
+
|
|
159
|
+
F_true = np.asarray(true_factors[:, :k], dtype=float)
|
|
160
|
+
F_est = np.asarray(estimated_factors[:, :k], dtype=float)
|
|
161
|
+
|
|
162
|
+
# Per-factor correlations (no rotation)
|
|
163
|
+
corr_per_factor = np.full(k, np.nan, dtype=float)
|
|
164
|
+
for j in range(k):
|
|
165
|
+
mask_j = np.isfinite(F_true[:, j]) & np.isfinite(F_est[:, j])
|
|
166
|
+
if np.sum(mask_j) > 1:
|
|
167
|
+
corr = np.corrcoef(F_true[mask_j, j], F_est[mask_j, j])[0, 1]
|
|
168
|
+
corr_per_factor[j] = float(corr)
|
|
169
|
+
|
|
170
|
+
rotation_matrix = None
|
|
171
|
+
aligned_factors = None
|
|
172
|
+
overall_corr = None
|
|
173
|
+
|
|
174
|
+
if use_procrustes:
|
|
175
|
+
# Center factors before Procrustes to focus on shape, not level.
|
|
176
|
+
F_true_centered = F_true - np.nanmean(F_true, axis=0, keepdims=True)
|
|
177
|
+
F_est_centered = F_est - np.nanmean(F_est, axis=0, keepdims=True)
|
|
178
|
+
|
|
179
|
+
# Replace any remaining NaNs with 0 before Procrustes (rare in synthetic tests)
|
|
180
|
+
F_true_clean = np.where(np.isfinite(F_true_centered), F_true_centered, 0.0)
|
|
181
|
+
F_est_clean = np.where(np.isfinite(F_est_centered), F_est_centered, 0.0)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
# Find R such that F_est_clean @ R ≈ F_true_clean
|
|
185
|
+
R, _ = orthogonal_procrustes(F_est_clean, F_true_clean) # type: ignore[arg-type]
|
|
186
|
+
rotation_matrix = R
|
|
187
|
+
aligned_factors = F_est @ R
|
|
188
|
+
|
|
189
|
+
mask_all = np.isfinite(F_true) & np.isfinite(aligned_factors)
|
|
190
|
+
if np.any(mask_all):
|
|
191
|
+
overall_corr = float(
|
|
192
|
+
np.corrcoef(
|
|
193
|
+
F_true[mask_all].ravel(),
|
|
194
|
+
aligned_factors[mask_all].ravel()
|
|
195
|
+
)[0, 1]
|
|
196
|
+
)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
_logger.debug(f"Procrustes rotation failed in evaluate_factor_estimation: {e}")
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"num_factors": k,
|
|
202
|
+
"correlation_per_factor": corr_per_factor,
|
|
203
|
+
"overall_correlation": overall_corr,
|
|
204
|
+
"rotation_matrix": rotation_matrix,
|
|
205
|
+
"aligned_factors": aligned_factors,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def evaluate_loading_estimation(
|
|
210
|
+
true_loadings: np.ndarray,
|
|
211
|
+
estimated_loadings: np.ndarray,
|
|
212
|
+
rotation_matrix: Optional[np.ndarray] = None
|
|
213
|
+
) -> Dict[str, Any]:
|
|
214
|
+
"""Evaluate loading matrix estimation accuracy (synthetic-data diagnostic).
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
true_loadings : np.ndarray
|
|
219
|
+
Array of shape (N, k_true) with the loadings used to generate data.
|
|
220
|
+
estimated_loadings : np.ndarray
|
|
221
|
+
Array of shape (N, k_est) with estimated loadings from the model
|
|
222
|
+
(typically ``result.C[:, :num_factors]``).
|
|
223
|
+
rotation_matrix : np.ndarray, optional
|
|
224
|
+
Orthogonal rotation matrix of shape (k_est, k_rot) returned by
|
|
225
|
+
:func:`evaluate_factor_estimation`. If provided, the estimated
|
|
226
|
+
loadings are rotated as ``estimated_loadings @ rotation_matrix``
|
|
227
|
+
before comparison.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
Dict[str, Any]
|
|
232
|
+
{
|
|
233
|
+
'num_factors': int,
|
|
234
|
+
'correlation_per_factor': np.ndarray (k,),
|
|
235
|
+
'rmse_per_series': np.ndarray (N,),
|
|
236
|
+
'overall_rmse': float,
|
|
237
|
+
'aligned_loadings': np.ndarray (N, k),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Notes
|
|
241
|
+
-----
|
|
242
|
+
- RMSE is computed per series (row-wise) across factors.
|
|
243
|
+
- This function assumes synthetic data where ``true_loadings`` are known.
|
|
244
|
+
"""
|
|
245
|
+
if true_loadings.ndim != 2 or estimated_loadings.ndim != 2:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"true_loadings and estimated_loadings must be 2D, "
|
|
248
|
+
f"got shapes {true_loadings.shape} and {estimated_loadings.shape}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
N_true, k_true = true_loadings.shape
|
|
252
|
+
N_est, k_est = estimated_loadings.shape
|
|
253
|
+
if N_true != N_est:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"Number of series (rows) must match, got {N_true} and {N_est}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Apply rotation if provided (align loadings with rotated factors)
|
|
259
|
+
L_est = np.asarray(estimated_loadings, dtype=float)
|
|
260
|
+
if rotation_matrix is not None:
|
|
261
|
+
if rotation_matrix.ndim != 2 or rotation_matrix.shape[0] != k_est:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"rotation_matrix shape {rotation_matrix.shape} incompatible "
|
|
264
|
+
f"with estimated_loadings shape {estimated_loadings.shape}"
|
|
265
|
+
)
|
|
266
|
+
L_est = L_est @ rotation_matrix
|
|
267
|
+
|
|
268
|
+
k = min(k_true, L_est.shape[1])
|
|
269
|
+
if k == 0:
|
|
270
|
+
raise ValueError("At least one factor/loading column is required.")
|
|
271
|
+
|
|
272
|
+
L_true = np.asarray(true_loadings[:, :k], dtype=float)
|
|
273
|
+
L_est_k = np.asarray(L_est[:, :k], dtype=float)
|
|
274
|
+
|
|
275
|
+
# Per-factor loading correlations
|
|
276
|
+
corr_per_factor = np.full(k, np.nan, dtype=float)
|
|
277
|
+
for j in range(k):
|
|
278
|
+
mask_j = np.isfinite(L_true[:, j]) & np.isfinite(L_est_k[:, j])
|
|
279
|
+
if np.sum(mask_j) > 1:
|
|
280
|
+
corr = np.corrcoef(L_true[mask_j, j], L_est_k[mask_j, j])[0, 1]
|
|
281
|
+
corr_per_factor[j] = float(corr)
|
|
282
|
+
|
|
283
|
+
# RMSE per series (row-wise across factors)
|
|
284
|
+
diff = L_true - L_est_k
|
|
285
|
+
mask = np.isfinite(L_true) & np.isfinite(L_est_k)
|
|
286
|
+
rmse_per_series = np.full(N_true, np.nan, dtype=float)
|
|
287
|
+
for i in range(N_true):
|
|
288
|
+
mask_i = mask[i, :]
|
|
289
|
+
if np.any(mask_i):
|
|
290
|
+
rmse_per_series[i] = float(
|
|
291
|
+
np.sqrt(np.mean(diff[i, mask_i] ** 2))
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if np.any(mask):
|
|
295
|
+
overall_rmse = float(np.sqrt(np.mean(diff[mask] ** 2)))
|
|
296
|
+
else:
|
|
297
|
+
overall_rmse = float("nan")
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
"num_factors": k,
|
|
301
|
+
"correlation_per_factor": corr_per_factor,
|
|
302
|
+
"rmse_per_series": rmse_per_series,
|
|
303
|
+
"overall_rmse": overall_rmse,
|
|
304
|
+
"aligned_loadings": L_est_k,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
99
308
|
def _display_dfm_tables(Res: DFMResult, config: DFMConfig, nQ: int) -> None:
|
|
100
309
|
"""Display DFM estimation output tables.
|
|
101
310
|
|
dfm_python/core/em.py
CHANGED
|
@@ -809,8 +809,8 @@ def em_step(params: EMStepParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
|
|
|
809
809
|
lambda_ridge = (trace_val / size_val) * 1e-5 # Scale factor
|
|
810
810
|
EZZ_lag_sub = EZZ_lag_sub + lambda_ridge * np.eye(size_val)
|
|
811
811
|
# Recompute condition number after regularization
|
|
812
|
-
|
|
813
|
-
|
|
812
|
+
eigenvals = np.linalg.eigvals(EZZ_lag_sub)
|
|
813
|
+
cond_num = (np.max(eigenvals) / max(np.min(eigenvals), 1e-12)) if np.max(eigenvals) > 0 else 1.0
|
|
814
814
|
EZZ_lag_inv = pinv(EZZ_lag_sub, rcond=1e-8) if cond_num > 1e12 else inv(EZZ_lag_sub)
|
|
815
815
|
except _NUMERICAL_EXCEPTIONS:
|
|
816
816
|
EZZ_lag_inv = pinv(EZZ_lag_sub)
|
|
@@ -1247,33 +1247,39 @@ def em_step(params: EMStepParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
|
|
|
1247
1247
|
# Q cap to prevent explosion: maximum allowed innovation variance for factors
|
|
1248
1248
|
Q_cap_factor = safe_get_attr(config, "max_innovation_variance", 1.0) if config is not None else 1.0
|
|
1249
1249
|
|
|
1250
|
-
#
|
|
1251
|
-
#
|
|
1252
|
-
# (
|
|
1250
|
+
# Normalize C for clock-frequency factors to prevent scale issues
|
|
1251
|
+
# Only normalize if C norm is in reasonable range (not too small, not too large)
|
|
1252
|
+
# This prevents Q from becoming too small (when C norm is small) or too large (when C norm is large)
|
|
1253
1253
|
if num_clock_factors > 0:
|
|
1254
|
-
#
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
Q_new[j,
|
|
1276
|
-
|
|
1254
|
+
# Check if there are slower-frequency series (tent weight constraints)
|
|
1255
|
+
has_slower_freq = (frequencies is not None and
|
|
1256
|
+
any(freq != clock for freq in frequencies if freq is not None))
|
|
1257
|
+
|
|
1258
|
+
# Only normalize if no slower-frequency series (to avoid violating tent weight constraints)
|
|
1259
|
+
if not has_slower_freq:
|
|
1260
|
+
for j in range(num_clock_factors):
|
|
1261
|
+
norm = np.linalg.norm(C_new[:, j])
|
|
1262
|
+
if norm > 1e-8:
|
|
1263
|
+
# Only normalize if norm is in reasonable range
|
|
1264
|
+
# Too small norm (< 0.1) would make Q too small
|
|
1265
|
+
# Too large norm (> 10) would make Q too large
|
|
1266
|
+
C_norm_min = 0.1 # Minimum norm to normalize
|
|
1267
|
+
C_norm_cap = safe_get_attr(config, "max_loading_norm", 10.0) if config is not None else 10.0
|
|
1268
|
+
|
|
1269
|
+
if C_norm_min <= norm <= C_norm_cap:
|
|
1270
|
+
# Normalize C column
|
|
1271
|
+
C_new[:, j] = C_new[:, j] / norm
|
|
1272
|
+
|
|
1273
|
+
# Adjust Q to preserve variance: Q[j,j] *= norm^2
|
|
1274
|
+
Q_adjustment = norm ** 2
|
|
1275
|
+
Q_new[j, j] = Q_new[j, j] * Q_adjustment
|
|
1276
|
+
|
|
1277
|
+
# Off-diagonal: Q[j,k] and Q[k,j] *= norm (for k != j)
|
|
1278
|
+
for k in range(Q_new.shape[0]):
|
|
1279
|
+
if k != j:
|
|
1280
|
+
Q_new[j, k] = Q_new[j, k] * norm
|
|
1281
|
+
Q_new[k, j] = Q_new[k, j] * norm
|
|
1282
|
+
# If norm is too small or too large, skip normalization to preserve stability
|
|
1277
1283
|
|
|
1278
1284
|
# Re-apply Q floor and cap after C normalization to prevent extreme values
|
|
1279
1285
|
# C normalization can make Q very large (if C norm was large) or very small (if C norm was small)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Dynamic Factor Model (DFM) estimation and nowcasting in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/dfm-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/yourusername/dfm-python#readme
|
|
@@ -709,12 +709,20 @@ Demonstrates:
|
|
|
709
709
|
|
|
710
710
|
## Project Status
|
|
711
711
|
|
|
712
|
-
**Version**: 0.2.
|
|
712
|
+
**Version**: 0.2.7
|
|
713
713
|
**Status**: Stable and production-ready
|
|
714
714
|
**Python**: 3.10+
|
|
715
715
|
**PyPI**: https://pypi.org/project/dfm-python/
|
|
716
716
|
|
|
717
|
-
### Recent Improvements (v0.2.
|
|
717
|
+
### Recent Improvements (v0.2.7)
|
|
718
|
+
|
|
719
|
+
- ✅ **Fixed C normalization issue**: C normalization now only applies when C norm is in reasonable range (0.1 ~ 10)
|
|
720
|
+
- Prevents Q from becoming too small when C norm is very small
|
|
721
|
+
- Prevents factor values from becoming extreme
|
|
722
|
+
- ✅ **Improved stability**: Conditional C normalization prevents pathological factor paths
|
|
723
|
+
- ✅ **Better diagnostics**: Added variance guard binding checks in tutorial
|
|
724
|
+
|
|
725
|
+
### Previous Improvements (v0.2.6)
|
|
718
726
|
|
|
719
727
|
- ✅ **Q matrix cap**: Factor innovation variances bounded above by 1.0 to prevent explosion
|
|
720
728
|
- ✅ **R matrix cap**: Observation error variances bounded above by 10.0 for standardized data
|
|
@@ -758,7 +766,7 @@ If you use `dfm-python` in your research, please cite:
|
|
|
758
766
|
author = {DFM Python Contributors},
|
|
759
767
|
year = {2025},
|
|
760
768
|
url = {https://pypi.org/project/dfm-python/},
|
|
761
|
-
version = {0.2.
|
|
769
|
+
version = {0.2.7}
|
|
762
770
|
}
|
|
763
771
|
```
|
|
764
772
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
dfm_python/__init__.py,sha256=
|
|
1
|
+
dfm_python/__init__.py,sha256=oz0p9JreUg6D9SfU2vUAFMRwf7JwHaa3dQDy87YHtIw,5589
|
|
2
2
|
dfm_python/api.py,sha256=-mmV7aaXOAqVW9HPhTEut7T6wN1EyW0rS3J0y-z-MRs,16221
|
|
3
3
|
dfm_python/config.py,sha256=bBiKfoETohNVJ3brRbY1330C1nQROybCR3co9iagIKs,46978
|
|
4
4
|
dfm_python/config_sources.py,sha256=6LTK7k5WJpnayMmtxtYwZ9SQi_hGXoz-Vg4sRGTabI0,21798
|
|
@@ -8,11 +8,11 @@ dfm_python/kalman.py,sha256=EXhloLbpzy7GKG9KFrSZt9pRp0y3CZyvycPVbvUWxq8,18074
|
|
|
8
8
|
dfm_python/news.py,sha256=iXbJnsvH5FgKToPzKnqIpTosrZu-QHXBNHjsqwMYrHo,32583
|
|
9
9
|
dfm_python/utils.py,sha256=qOAJm2KG71yP2a29iJVDGOC-ZUS_HPL5oAo16EgNXA4,15709
|
|
10
10
|
dfm_python/core/__init__.py,sha256=oDrhAoc1W_ODohgufQ-gN0uri8-vZI0EXsF0dysth_A,331
|
|
11
|
-
dfm_python/core/diagnostics.py,sha256=
|
|
12
|
-
dfm_python/core/em.py,sha256=
|
|
11
|
+
dfm_python/core/diagnostics.py,sha256=1ninWcQlWhJmfUZq9kky7IQoRf_6fI_IlVzzSb7apRQ,28085
|
|
12
|
+
dfm_python/core/em.py,sha256=jbtsFHynMDtblZTtlzgodX04e86BBB9hPn20MMnV3w0,72705
|
|
13
13
|
dfm_python/core/helpers.py,sha256=XJvWWEZlUqozBfFNcENKgbOOFkwlm-tGwO-u0LnAf-E,31541
|
|
14
14
|
dfm_python/core/numeric.py,sha256=IcIwcKtMhIoJ5-7Mz6UNYwTUtEa5W_ZMnQtF87v0SSc,26431
|
|
15
|
-
dfm_python-0.2.
|
|
16
|
-
dfm_python-0.2.
|
|
17
|
-
dfm_python-0.2.
|
|
18
|
-
dfm_python-0.2.
|
|
15
|
+
dfm_python-0.2.8.dist-info/METADATA,sha256=wY69TZa4fK9I4zY4t7hQUJksWuMCyxtGUS0Nr_C3SPk,28599
|
|
16
|
+
dfm_python-0.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
dfm_python-0.2.8.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
|
|
18
|
+
dfm_python-0.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|