generate_charts_month_with_daily_min_max.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. #!/usr/bin/env python3
  2. import os
  3. import sys
  4. from datetime import datetime
  5. import calendar
  6. import time
  7. import numpy as np
  8. import matplotlib.pyplot as plt
  9. from matplotlib.colors import LinearSegmentedColormap
  10. from matplotlib.patches import Polygon
  11. import pandas as pd
  12. marker = 'o'
  13. markersize = 4
  14. linestyle = '-'
  15. linewidth = 2
  16. colors = {
  17. 'max': 'red',
  18. 'min': 'blue'
  19. }
  20. def read_file_values(path):
  21. temp = pressure = humidity = None
  22. temp_u = pressure_u = humidity_u = ''
  23. try:
  24. with open(path, 'r') as f:
  25. for line in f:
  26. if 'Temperature' in line:
  27. try:
  28. parts = line.split(':', 1)[1].strip().split()
  29. temp = float(parts[0])
  30. temp_u = ' '.join(parts[1:]) if len(parts) > 1 else ''
  31. except:
  32. pass
  33. elif 'Pressure' in line:
  34. try:
  35. parts = line.split(':', 1)[1].strip().split()
  36. pressure = float(parts[0])
  37. pressure_u = ' '.join(parts[1:]) if len(parts) > 1 else ''
  38. except:
  39. pass
  40. elif 'Humidity' in line:
  41. try:
  42. parts = line.split(':', 1)[1].strip().split()
  43. humidity = float(parts[0])
  44. humidity_u = ' '.join(parts[1:]) if len(parts) > 1 else ''
  45. except:
  46. pass
  47. except FileNotFoundError:
  48. return (None, '', None, '', None, '')
  49. return (temp, temp_u, pressure, pressure_u, humidity, humidity_u)
  50. def read_measurements(data_folder):
  51. records = []
  52. year_folder = str(datetime.now().year)
  53. for root, dirs, files in os.walk(data_folder):
  54. for filename in files:
  55. if not filename.endswith('.txt'):
  56. continue
  57. if filename == year_folder:
  58. continue
  59. filepath = os.path.join(root, filename)
  60. try:
  61. timestamp = int(filename[:-4])
  62. except ValueError:
  63. print(f"Filename not a timestamp, skipping: {filename} (in {root})")
  64. continue
  65. file_time = datetime.fromtimestamp(timestamp)
  66. temp, temp_u, pressure, pressure_u, humidity, humidity_u = read_file_values(filepath)
  67. # fallback to year subfolder file if some values missing
  68. if (temp is None or pressure is None or humidity is None):
  69. alt_path = os.path.join(data_folder, year_folder, filename)
  70. if os.path.isfile(alt_path):
  71. at, at_u, ap, ap_u, ah, ah_u = read_file_values(alt_path)
  72. if temp is None and at is not None:
  73. temp, temp_u = at, at_u
  74. if pressure is None and ap is not None:
  75. pressure, pressure_u = ap, ap_u
  76. if humidity is None and ah is not None:
  77. humidity, humidity_u = ah, ah_u
  78. if temp is not None and pressure is not None and humidity is not None:
  79. records.append({
  80. 'timestamp': file_time,
  81. 'date': file_time.date(),
  82. 'day': file_time.day,
  83. 'temperature': temp,
  84. 'temperature_unit': temp_u,
  85. 'pressure': pressure,
  86. 'pressure_unit': pressure_u,
  87. 'humidity': humidity,
  88. 'humidity_unit': humidity_u
  89. })
  90. else:
  91. print(f"Missing data in file: {filename} (in {root}). Skipping.")
  92. return pd.DataFrame(records)
  93. def filter_month(df, month, year):
  94. if df.empty:
  95. return df
  96. return df[(df['timestamp'].dt.month == month) & (df['timestamp'].dt.year == year)]
  97. def daily_min_max(df, col):
  98. g = df.groupby('date')[col].agg(['min', 'max']).reset_index()
  99. g['day'] = pd.to_datetime(g['date']).dt.day
  100. g = g.sort_values('day')
  101. return g
  102. def plot_min_max(daily_df, col, out_folder, year, month, unit=''):
  103. if daily_df.empty:
  104. print(f"No data to plot for {col}")
  105. return
  106. last_day = calendar.monthrange(year, month)[1]
  107. fig, ax = plt.subplots(figsize=(12, 6))
  108. ax.plot(daily_df['day'], daily_df['min'],
  109. marker=marker, markersize=markersize,
  110. linestyle=linestyle, linewidth=linewidth,
  111. label='Min', color=colors['min'], zorder=3)
  112. ax.plot(daily_df['day'], daily_df['max'],
  113. marker=marker, markersize=markersize,
  114. linestyle=linestyle, linewidth=linewidth,
  115. label='Max', color=colors['max'], zorder=4)
  116. xs = list(daily_df['day'])
  117. ys_max = list(daily_df['max'])
  118. ys_min = list(daily_df['min'])
  119. if len(xs) >= 2:
  120. verts = [(x, y) for x, y in zip(xs, ys_max)] + [(x, y) for x, y in zip(xs[::-1], ys_min[::-1])]
  121. cmap = LinearSegmentedColormap.from_list('blue_red', [colors['min'], colors['max']])
  122. global_min = min(ys_min)
  123. global_max = max(ys_max)
  124. if global_min == global_max:
  125. global_min -= 0.5
  126. global_max += 0.5
  127. gradient = np.linspace(0, 1, 256).reshape(256, 1)
  128. gradient = np.repeat(gradient, 10, axis=1)
  129. im = ax.imshow(gradient, aspect='auto', cmap=cmap,
  130. extent=(0.5, last_day + 0.5, global_min, global_max),
  131. origin='lower', alpha=0.35, zorder=1)
  132. poly = Polygon(verts, closed=True, transform=ax.transData)
  133. im.set_clip_path(poly)
  134. ax.set_xlim(1, last_day)
  135. xticks = list(range(1, last_day + 1))
  136. xtick_labels = [f"{d}." for d in xticks]
  137. ax.set_xticks(xticks)
  138. ax.set_xticklabels(xtick_labels, rotation=0)
  139. ylabel = col.capitalize()
  140. if unit:
  141. ylabel += f' in {unit}'
  142. ax.set_ylabel(ylabel)
  143. ax.set_xlabel(f'Day of Month ({year}-{month:02d})')
  144. ax.set_title(f'{col.capitalize()} - Daily Min/Max Value for {year}-{month:02d}')
  145. ax.grid(True, alpha=0.25)
  146. ax.legend()
  147. fig.tight_layout()
  148. out_path = os.path.join(out_folder, f'{col}_daily_min_max.png')
  149. fig.savefig(out_path)
  150. plt.close(fig)
  151. print(f"Saved plot: {out_path}")
  152. def ensure_folder(path):
  153. if not os.path.exists(path):
  154. os.makedirs(path, exist_ok=True)
  155. def pick_unit(series_unit_col):
  156. if series_unit_col is None or series_unit_col.empty:
  157. return ''
  158. vals = series_unit_col.dropna().astype(str)
  159. vals = vals[vals != '']
  160. return vals.mode().iat[0] if not vals.empty else ''
  161. def main():
  162. if len(sys.argv) < 2:
  163. print("Usage: python3 script.py <month_number (1-12)>")
  164. sys.exit(1)
  165. try:
  166. month = int(sys.argv[1])
  167. assert 1 <= month <= 12
  168. except:
  169. print("Month must be an integer between 1 and 12.")
  170. sys.exit(1)
  171. data_folder = 'temperature'
  172. if not os.path.isdir(data_folder):
  173. print(f"Data folder not found: {data_folder}")
  174. sys.exit(1)
  175. start_time = time.time()
  176. df = read_measurements(data_folder)
  177. if df.empty:
  178. print("No valid measurements found.")
  179. sys.exit(1)
  180. df['timestamp'] = pd.to_datetime(df['timestamp'])
  181. year = datetime.now().year
  182. df_month = filter_month(df, month, year)
  183. if df_month.empty:
  184. print(f"No measurements for {month}/{year}.")
  185. sys.exit(0)
  186. # pick most common unit per measurement type for the month
  187. units = {
  188. 'temperature': pick_unit(df_month.get('temperature_unit')),
  189. 'pressure': pick_unit(df_month.get('pressure_unit')),
  190. 'humidity': pick_unit(df_month.get('humidity_unit'))
  191. }
  192. out_folder = f'plots_{year}_{month:02d}'
  193. ensure_folder(out_folder)
  194. for col in ['temperature', 'pressure', 'humidity']:
  195. daily = daily_min_max(df_month, col)
  196. plot_min_max(daily, col, out_folder, year, month, unit=units.get(col, ''))
  197. elapsed = time.time() - start_time
  198. print(f"Verarbeitung abgeschlossen in {elapsed:.2f}s. Plots in {out_folder}")
  199. if __name__ == '__main__':
  200. main()