File size: 5,168 Bytes
e5ef030
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"""
One-time data prep: AMPds2 -> compact hourly parquet for the Space.

Usage
-----
    python prepare_data.py /path/to/dataverse_files.zip
    python prepare_data.py /path/to/AMPds2_folder
    python prepare_data.py            # uses the default Colab/Drive path below

Produces data/ampds2_hourly.parquet (small enough to commit to the HF Space).
Resamples the minutely AMPds2 active-power readings to hourly means.
"""
import io
import os
import sys
import zipfile
import tempfile
import numpy as np
import pandas as pd

DEFAULT_INPUT = "/content/drive/MyDrive/AMPds2/dataverse_files.zip"
OUT = "data/ampds2_hourly.parquet"

METER_RE = __import__("re").compile(r"^[A-Z][A-Z0-9]{1,2}E$")   # WHE, FGE, HPE, B1E, B2E, ...


def _read_table(buf_or_path, name):
    sep = "\t" if name.lower().endswith((".tab", ".tsv")) else ","
    return pd.read_csv(buf_or_path, sep=sep, low_memory=False)


def _score(df):
    """How likely this table is the wide active-power file (meter-code columns incl. WHE)."""
    cols = [str(c).strip().upper() for c in df.columns]
    meters = [c for c in cols if METER_RE.match(c)]
    return (len(meters), "WHE" in cols)


def _iter_tables(path):
    """Yield (name, dataframe) for every csv/tab/tsv found in a zip (incl. nested) or folder."""
    if zipfile.is_zipfile(path):
        with zipfile.ZipFile(path) as z:
            for m in z.namelist():
                low = m.lower()
                if low.endswith(".zip"):
                    with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tf:
                        tf.write(z.read(m)); nested = tf.name
                    try:
                        yield from _iter_tables(nested)
                    finally:
                        os.unlink(nested)
                elif low.endswith((".csv", ".tab", ".tsv")):
                    try:
                        yield m, _read_table(io.BytesIO(z.read(m)), m)
                    except Exception as e:
                        print("  skip", m, "->", e)
    elif os.path.isdir(path):
        for root, _, files in os.walk(path):
            for f in files:
                if f.lower().endswith((".csv", ".tab", ".tsv")):
                    fp = os.path.join(root, f)
                    try:
                        yield f, _read_table(fp, f)
                    except Exception as e:
                        print("  skip", f, "->", e)
    else:  # single file
        name = os.path.basename(path)
        yield name, _read_table(path, name)


def find_power_table(path):
    best, best_score, best_name = None, (-1, False), None
    for name, df in _iter_tables(path):
        # an active-power file is preferred (…_P… in AMPds2); compute a score regardless
        s = _score(df)
        bonus = (1, s[1]) if ("_p" in name.lower() or name.lower().startswith("electricity")) else (0, s[1])
        score = (s[0] + bonus[0] * 100, s[1])
        print(f"  candidate {name:40s} meters={s[0]:2d} has_WHE={s[1]}")
        if score > best_score:
            best, best_score, best_name = df, score, name
    if best is None or best_score[0] < 1:
        raise SystemExit("No AMPds2 power table found (need a CSV with meter-code columns like WHE, FGE).")
    print("  -> using:", best_name)
    return best


def to_hourly(df):
    df = df.copy()
    df.columns = [str(c).strip() for c in df.columns]
    upper = {c: c.upper() for c in df.columns}

    # timestamp: prefer an explicit UNIX seconds column, else first datetime-like column
    ts_col = next((c for c in df.columns if upper[c] in ("UNIX_TS", "TS", "TIMESTAMP", "TIME")), None)
    if ts_col is not None and np.issubdtype(df[ts_col].dropna().dtype, np.number):
        idx = pd.to_datetime(df[ts_col], unit="s")
    elif ts_col is not None:
        idx = pd.to_datetime(df[ts_col], errors="coerce")
    else:
        ts_col = df.columns[0]
        idx = pd.to_datetime(df[ts_col], errors="coerce")
        if idx.isna().mean() > 0.5:                     # maybe it's unix seconds in col 0
            idx = pd.to_datetime(pd.to_numeric(df[ts_col], errors="coerce"), unit="s")
    df.index = idx

    meters = [c for c in df.columns if METER_RE.match(upper[c]) and c != ts_col]
    out = df[meters].apply(pd.to_numeric, errors="coerce")
    out.columns = [upper[c] for c in meters]
    out = out[~out.index.isna()].sort_index()
    hourly = out.resample("h").mean()
    return hourly


def main():
    inp = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_INPUT
    if not os.path.exists(inp):
        raise SystemExit(f"Input not found: {inp}\nPass the path to dataverse_files.zip or the AMPds2 folder.")
    print("Reading AMPds2 from:", inp)
    raw = find_power_table(inp)
    print(f"  raw shape: {raw.shape}")
    hourly = to_hourly(raw)
    os.makedirs(os.path.dirname(OUT), exist_ok=True)
    hourly.index.name = "ts"
    hourly.to_parquet(OUT)
    mb = os.path.getsize(OUT) / 1e6
    print(f"\nWrote {OUT}  ({hourly.shape[0]} hours x {hourly.shape[1]} meters, {mb:.1f} MB)")
    print("Columns:", list(hourly.columns))
    print("Commit this parquet to your Space (or upload via the Files tab).")


if __name__ == "__main__":
    main()