spaceweather.gfz.gfz_daily()   B
last analyzed

Complexity

Conditions 6

Size

Total Lines 57
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 28
nop 5
dl 0
loc 57
rs 8.2746
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# Copyright (c) 2020--2024 Stefan Bender
2
#
3
# This module is part of pyspaceweather.
4
# pyspaceweather is free software: you can redistribute it or modify
5
# it under the terms of the GNU General Public License as published
6
# by the Free Software Foundation, version 2.
7
# See accompanying COPYING.GPLv2 file or http://www.gnu.org/licenses/gpl-2.0.html.
8
"""Python interface for space weather indices from GFZ Potsdam
9
10
GFZ space weather indices ASCII file parser for python [#]_.
11
Includes parser for the GFZ ASCII files, files in WDC format,
12
and for the Hpo 30 and 60 minute ASCII files [#]_.
13
For the file formats, see the `gfz_xxx_format.txt` files.
14
15
.. [#] https://kp.gfz-potsdam.de/en/
16
.. [#] https://kp.gfz-potsdam.de/en/hp30-hp60
17
"""
18
import os
19
import logging
20
from warnings import warn
21
22
import numpy as np
23
import pandas as pd
24
25
from .core import _assert_file_exists, _dl_file, _resource_filepath
26
27
__all__ = [
28
	"gfz_daily", "gfz_3h", "read_gfz",
29
	"read_gfz_hp",
30
	"get_gfz_age", "update_gfz",
31
	"update_gfz_hp30", "update_gfz_hp60",
32
	"GFZ_PATH_ALL", "GFZ_PATH_30D",
33
	"HP30_PATH_ALL", "HP30_PATH_30D",
34
	"HP60_PATH_ALL", "HP60_PATH_30D",
35
]
36
37
GFZ_URL_ALL = "https://kp.gfz-potsdam.de/app/files/Kp_ap_Ap_SN_F107_since_1932.txt"
38
GFZ_URL_30D = "https://kp.gfz-potsdam.de/app/files/Kp_ap_Ap_SN_F107_nowcast.txt"
39
GFZ_FILE_ALL = os.path.basename(GFZ_URL_ALL)
40
GFZ_FILE_30D = os.path.basename(GFZ_URL_30D)
41
GFZ_PATH_ALL = _resource_filepath(GFZ_FILE_ALL)
42
GFZ_PATH_30D = _resource_filepath(GFZ_FILE_30D)
43
44
HP30_URL_ALL = "https://kp.gfz.de/app/files/Hp30_ap30_complete_series.txt"
45
HP30_URL_30D = "https://kp.gfz.de/app/files/Hp30_ap30_nowcast.txt"
46
HP30_FILE_ALL = os.path.basename(HP30_URL_ALL)
47
HP30_FILE_30D = os.path.basename(HP30_URL_30D)
48
HP30_PATH_ALL = _resource_filepath(HP30_FILE_ALL)
49
HP30_PATH_30D = _resource_filepath(HP30_FILE_30D)
50
51
HP60_URL_ALL = "https://kp.gfz.de/app/files/Hp60_ap60_complete_series.txt"
52
HP60_URL_30D = "https://kp.gfz.de/app/files/Hp60_ap60_nowcast.txt"
53
HP60_FILE_ALL = os.path.basename(HP60_URL_ALL)
54
HP60_FILE_30D = os.path.basename(HP60_URL_30D)
55
HP60_PATH_ALL = _resource_filepath(HP60_FILE_ALL)
56
HP60_PATH_30D = _resource_filepath(HP60_FILE_30D)
57
58
59
def get_gfz_age(gfzpath, relative=True):
60
	"""Age of the downloaded data file
61
62
	Retrieves the last update time of the given file or full path.
63
64
	Parameters
65
	----------
66
	gfzpath: str
67
		Filename to check, absolute path or relative to the current dir.
68
	relative: bool, optional, default True
69
		Return the file's age (True) or the last update time (False).
70
71
	Returns
72
	-------
73
	upd: pandas.Timestamp or pandas.Timedelta
74
		The last updated time or the file age, depending on the setting
75
		of `relative` above.
76
		Raises ``IOError`` if the file is not found.
77
	"""
78
	_assert_file_exists(gfzpath)
79
	with open(gfzpath) as fp:
80
		for line in fp:
81
			# forward to last line
82
			pass
83
	upd = pd.to_datetime(line[:10].replace(" ", "-"), utc=True)
0 ignored issues
show
introduced by
The variable line does not seem to be defined in case the for loop on line 80 is not entered. Are you sure this can never be the case?
Loading history...
84
	if relative:
85
		return pd.Timestamp.utcnow() - upd
86
	return upd
87
88
89
def update_gfz(
90
	min_age="1d",
91
	gfzpath_all=None, gfzpath_30d=None,
92
	url_all=None, url_30d=None,
93
):
94
	"""Update the local space weather index data
95
96
	Updates the local space weather index data from the website [#]_,
97
	given that the 30-day file is older than `min_age`,
98
	or the combined (large) file is older than 30 days.
99
	If the data is missing for some reason, a download will be attempted nonetheless.
100
101
	All arguments are optional and changing them from the defaults should
102
	neither be necessary nor is it recommended.
103
104
	.. [#] https://kp.gfz-potsdam.de/en/
105
106
	Parameters
107
	----------
108
	min_age: str, optional, default "1d"
109
		The time after which a new download will be attempted.
110
		The online data is updated every day, thus setting this value to
111
		a shorter time is not needed and not recommended.
112
	gfzpath_all: `None` or str, optional, default `None`
113
		Filename for the large combined index file including the
114
		historic data, absolute path or relative to the current dir.
115
		`None` uses the package's default file location.
116
	gfzpath_30d: `None` or str, optional, default `None`
117
		Filename for the 30-day (nowcast) index file, absolute path or relative
118
		to the current dir.
119
		`None` uses the package's default file location.
120
	url_all: `None` or str, optional, default `None`
121
		The url of the "historic" data file.
122
		`None` uses the default url.
123
	url_30d: `None` or str, optional, default `None`
124
		The url of the data file containing the indices for the last 30 days.
125
		`None` uses the default url.
126
127
	Returns
128
	-------
129
	Nothing.
130
	"""
131
	def _update_file(gfzpath, url, min_age):
132
		if not os.path.exists(gfzpath):
133
			logging.info("{0} not found, downloading.".format(gfzpath))
134
			_dl_file(gfzpath, url)
135
			return
136
		if get_gfz_age(gfzpath) < pd.Timedelta(min_age):
137
			logging.info("not updating '{0}'.".format(gfzpath))
138
			return
139
		logging.info("updating '{0}'.".format(gfzpath))
140
		_dl_file(gfzpath, url)
141
142
	gfzpath_all = gfzpath_all or GFZ_PATH_ALL
143
	gfzpath_30d = gfzpath_30d or GFZ_PATH_30D
144
	url_all = url_all or GFZ_URL_ALL
145
	url_30d = url_30d or GFZ_URL_30D
146
147
	# Update the large file after 30 days
148
	_update_file(gfzpath_all, url_all, "30days")
149
	# Don't re-download before `min_age` has passed (1d)
150
	_update_file(gfzpath_30d, url_30d, min_age)
151
152
153
def update_gfz_hp30(
154
	min_age="1d",
155
	gfzpath_all=None, gfzpath_30d=None,
156
	url_all=None, url_30d=None,
157
):
158
	"""Updates the local Hp30 index data
159
160
	See Also
161
	--------
162
	update_gfz
163
	"""
164
	gfzpath_all = gfzpath_all or HP30_PATH_ALL
165
	gfzpath_30d = gfzpath_30d or HP30_PATH_30D
166
	url_all = url_all or HP30_URL_ALL
167
	url_30d = url_30d or HP30_URL_30D
168
	return update_gfz(
169
		min_age=min_age,
170
		gfzpath_all=gfzpath_all, gfzpath_30d=gfzpath_30d,
171
		url_all=url_all, url_30d=url_30d,
172
	)
173
174
175
def update_gfz_hp60(
176
	min_age="1d",
177
	gfzpath_all=None, gfzpath_30d=None,
178
	url_all=None, url_30d=None,
179
):
180
	"""Updates the local Hp60 index data
181
182
	See Also
183
	--------
184
	update_gfz
185
	"""
186
	gfzpath_all = gfzpath_all or HP60_PATH_ALL
187
	gfzpath_30d = gfzpath_30d or HP60_PATH_30D
188
	url_all = url_all or HP60_URL_ALL
189
	url_30d = url_30d or HP60_URL_30D
190
	return update_gfz(
191
		min_age=min_age,
192
		gfzpath_all=gfzpath_all, gfzpath_30d=gfzpath_30d,
193
		url_all=url_all, url_30d=url_30d,
194
	)
195
196
197
def read_gfz(gfzpath):
198
	"""Read and parse space weather index data file
199
200
	Reads the given file and parses it according to the space weather data format.
201
202
	Parameters
203
	----------
204
	gfzpath: str
205
		File to parse, absolute path or relative to the current dir.
206
207
	Returns
208
	-------
209
	gfz_df: pandas.DataFrame
210
		The parsed space weather data (daily values).
211
		Raises an ``IOError`` if the file is not found.
212
213
		The dataframe contains the following columns:
214
215
		"year", "month", "day":
216
			The observation date
217
		"bsrn":
218
			Bartels Solar Rotation Number.
219
			A sequence of 27-day intervals counted continuously from 1832 Feb 8.
220
		"rotd":
221
			Number of Day within the Bartels 27-day cycle (01-27).
222
		"Kp0", "Kp3", "Kp6", "Kp9", "Kp12", "Kp15", "Kp18", "Kp21":
223
			Planetary 3-hour Range Index (Kp) for 0000-0300, 0300-0600,
224
			0600-0900, 0900-1200, 1200-1500, 1500-1800, 1800-2100, 2100-2400 UT
225
		"Kpsum": Sum of the 8 Kp indices for the day.
226
			Expressed to the nearest third of a unit.
227
		"Ap0", "Ap3", "Ap6", "Ap9", "Ap12", "Ap15", "Ap18", "Ap21":
228
			Planetary Equivalent Amplitude (Ap) for 0000-0300, 0300-0600,
229
			0600-0900, 0900-1200, 1200-1500, 1500-1800, 1800-2100, 2100-2400 UT
230
		"Apavg":
231
			Arithmetic average of the 8 Ap indices for the day.
232
		"isn":
233
			International Sunspot Number.
234
			Records contain the Zurich number through 1980 Dec 31 and the
235
			International Brussels number thereafter.
236
		"f107_obs":
237
			Observed (unadjusted) value of F10.7.
238
		"f107_adj":
239
			10.7-cm Solar Radio Flux (F10.7) Adjusted to 1 AU.
240
			Measured at Ottawa at 1700 UT daily from 1947 Feb 14 until
241
			1991 May 31 and measured at Penticton at 2000 UT from 1991 Jun 01 on.
242
			Expressed in units of 10-22 W/m2/Hz.
243
		"D":
244
			Definitive indicator.
245
			0: Kp and SN preliminary
246
			1: Kp definitive, SN preliminary
247
			2: Kp and SN definitive
248
	"""
249
	_assert_file_exists(gfzpath)
250
	gfz = np.genfromtxt(
251
		gfzpath,
252
		skip_header=3,
253
		delimiter=[
254
		#  yy mm dd dd dm br db kp kp kp kp kp kp kp kp
255
			4, 3, 3, 6, 8, 5, 3, 7, 7, 7, 7, 7, 7, 7, 7,
256
		#  ap ap ap ap ap ap ap ap Ap sn f1 f2 def
257
			5, 5, 5, 5, 5, 5, 5, 5, 6, 4, 9, 9, 2,
258
		],
259
		dtype=(
260
			"i4,i4,i4,i4,f4,i4,i4,f4,f4,f4,f4,f4,f4,f4,"
261
			"f4,i4,i4,i4,i4,i4,i4,i4,i4,i4,i4,f8,f8,i4,"
262
		),
263
		names=[
264
			"year", "month", "day", "days", "days_m", "bsrn", "rotd",
265
			"Kp0", "Kp3", "Kp6", "Kp9", "Kp12", "Kp15", "Kp18", "Kp21",
266
			"Ap0", "Ap3", "Ap6", "Ap9", "Ap12", "Ap15", "Ap18", "Ap21", "Apavg",
267
			"isn", "f107_obs", "f107_adj", "D",
268
		]
269
	)
270
	gfz = gfz[gfz["year"] != -1]
271
	ts = pd.to_datetime([
272
		"{0:04d}-{1:02d}-{2:02d}".format(yy, mm, dd)
273
		for yy, mm, dd in gfz[["year", "month", "day"]]
274
	])
275
	gfz_df = pd.DataFrame(gfz, index=ts)
276
	# Sum Kp for compatibility with celestrak dataframe
277
	kpns = list(map("Kp{0}".format, range(0, 23, 3)))
278
	gfz_df.insert(15, "Kpsum", gfz_df[kpns].sum(axis=1))
279
	return gfz_df
280
281
282
def read_gfz_hp(gfzhppath):
283
	"""Read and parse GFZ Hp30 and Hp60 index data file
284
285
	Reads the given file and parses it according to the Hp30 and Hp60 file format.
286
	File format descriptions in [#]_ and [#]_
287
288
	.. [#] https://kp.gfz-potsdam.de/app/format/Hpo_Hp30.txt
289
	.. [#] https://kp.gfz-potsdam.de/app/format/Hpo_Hp60.txt
290
291
	Parameters
292
	----------
293
	gfzhppath: str
294
		File to parse, absolute path or relative to the current dir.
295
296
	Returns
297
	-------
298
	hp_df: pandas.DataFrame
299
		The parsed space weather data with the 30 min or 60 min index.
300
		The index is returned timezone-naive but contains UTC timestamps.
301
		To convert to a timezone-aware index, use
302
		:meth:`pandas.DataFrame.tz_localize()`: ``hp_df.tz_localize("utc")``.
303
		Raises an ``IOError`` if the file is not found.
304
305
		The dataframe contains the following columns:
306
307
		"index":
308
			padas.DateTimeIndex of the middle times of the intervals.
309
		"year", "month", "day":
310
			The observation date.
311
		"hh_h":
312
			Starting time in hours of interval.
313
		"hh_m":
314
			Middle time in hours of interval.
315
		"days":
316
			Days since 1932-01-01 00:00 UT to start of interval.
317
		"days_m":
318
			Days since 1932-01-01 00:00 UT to middle of interval.
319
		"Hp":
320
			Hp index during to the interval (30 min or 60 min).
321
		"ap":
322
			ap index during to the interval (30 min or 60 min).
323
		"D":
324
			Reserved for future use, D = 0 for now.
325
	"""
326
	_assert_file_exists(gfzhppath)
327
	hp = np.genfromtxt(
328
		gfzhppath,
329
		delimiter=[
330
		#  yy mm dd hh hm ddd ddm hp ap  D
331
			4, 3, 3, 5, 6, 12, 12, 7, 5, 2,
332
		],
333
		dtype=(
334
			"i4,i4,i4,f4,f4,f4,f4,f4,i4,i4"
335
		),
336
		names=[
337
			"year", "month", "day", "hh_h", "hh_m", "days", "days_m", "Hp", "ap", "D",
338
		]
339
	)
340
	hp = hp[hp["year"] != -1]
341
	ts = pd.to_datetime([
342
		"{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}".format(
343
			yy, mm, dd, int(np.floor(hh_m)), int(60 * (hh_m - np.floor(hh_m)))
344
		)
345
		for yy, mm, dd, hh_m in hp[["year", "month", "day", "hh_m"]]
346
	])
347
	hp_df = pd.DataFrame(hp, index=ts)
348
	return hp_df
349
350
351
def read_gfz_wdc(gfzpath):
352
	"""Parse space weather index data file in WDC format
353
354
	Parses the GFZ index data in WDC format.
355
356
	Parameters
357
	----------
358
	gfzpath: str
359
		File to parse, absolute path or relative to the current dir.
360
361
	Returns
362
	-------
363
	gfz_df: pandas.DataFrame
364
		The parsed space weather data (daily values).
365
		Raises an ``IOError`` if the file is not found.
366
		The index is returned timezone-naive but contains UTC timestamps.
367
		To convert to a timezone-aware index, use
368
		:meth:`pandas.DataFrame.tz_localize()`: ``gfz_df.tz_localize("utc")``.
369
370
		The dataframe contains the following columns:
371
372
		"year", "month", "day":
373
			The observation date
374
		"bsrn":
375
			Bartels Solar Rotation Number.
376
			A sequence of 27-day intervals counted continuously from 1832 Feb 8.
377
		"rotd":
378
			Number of Day within the Bartels 27-day cycle (01-27).
379
		"Kp0", "Kp3", "Kp6", "Kp9", "Kp12", "Kp15", "Kp18", "Kp21":
380
			Planetary 3-hour Range Index (Kp) for 0000-0300, 0300-0600,
381
			0600-0900, 0900-1200, 1200-1500, 1500-1800, 1800-2100, 2100-2400 UT
382
		"Kpsum": Sum of the 8 Kp indices for the day.
383
			Expressed to the nearest third of a unit.
384
		"Ap0", "Ap3", "Ap6", "Ap9", "Ap12", "Ap15", "Ap18", "Ap21":
385
			Planetary Equivalent Amplitude (Ap) for 0000-0300, 0300-0600,
386
			0600-0900, 0900-1200, 1200-1500, 1500-1800, 1800-2100, 2100-2400 UT
387
		"Apavg":
388
			Arithmetic average of the 8 Ap indices for the day.
389
		"Cp":
390
			Cp index - the daily planetary character figure, a qualitative
391
			estimate of the overall level of geomagnetic activity for this day
392
			determined from the sum of the eight ap amplitudes,
393
			ranging from 0.0 to 2.5 in steps of 0.1.
394
		"C9":
395
			The contracted scale for Cp with only 1 digit, from 0 to 9.
396
	"""
397
	_assert_file_exists(gfzpath)
398
	gfz = np.genfromtxt(
399
		gfzpath,
400
		skip_header=3,
401
		delimiter=[
402
		#  yy mm dd br db kp kp kp kp kp kp kp kp kps
403
			2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
404
		#  ap ap ap ap ap ap ap ap Ap Cp C9
405
			3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
406
		],
407
		dtype=(
408
			"i4,i4,i4,i4,i4,i4,i4,i4,i4,i4,i4,i4,i4,i4,"
409
			"i4,i4,i4,i4,i4,i4,i4,i4,i4,f8,i4,"
410
		),
411
		names=[
412
			"year", "month", "day", "bsrn", "rotd",
413
			"Kp0", "Kp3", "Kp6", "Kp9", "Kp12", "Kp15", "Kp18", "Kp21", "Kpsum",
414
			"Ap0", "Ap3", "Ap6", "Ap9", "Ap12", "Ap15", "Ap18", "Ap21", "Apavg",
415
			"Cp", "C9",
416
		]
417
	)
418
	gfz = gfz[gfz["year"] != -1]
419
	ts = pd.to_datetime([
420
		"{0:04d}-{1:02d}-{2:02d}".format(2000 + yy if yy < 32 else 1900 + yy, mm, dd)
421
		for yy, mm, dd in gfz[["year", "month", "day"]]
422
	])
423
	gfz_df = pd.DataFrame(gfz, index=ts)
424
	gfz_df.loc[:, "year"] = ts.year
425
	# Adjust Kp to 0...9
426
	kpns = list(map("Kp{0}".format, range(0, 23, 3))) + ["Kpsum"]
427
	gfz_df[kpns] = 0.1 * gfz_df[kpns]
428
	return gfz_df
429
430
431
# Common arguments for the public daily and 3h interfaces
432
_GFZ_COMMON_PARAMS = """
433
Parameters
434
----------
435
gfzpath_all: `None` or str, optional, default `None`
436
	Filename for the large combined index file including the
437
	historic data, absolute path or relative to the current dir.
438
	`None` uses the package's default file location.
439
gfzpath_30d: `None` or str, optional, default `None`
440
	Filename for the 30-day (nowcast) index file,
441
	absolute path or relative to the current dir.
442
	`None` uses the package's default file location.
443
update: bool, optional, default False
444
	Attempt to update the local data if it is older than `update_interval`.
445
update_interval: str, optional, default "10days"
446
	The time after which the data are considered "old".
447
	By default, no automatic re-download is initiated, set `update` to true.
448
	The online data is updated every 3 hours, thus setting this value to
449
	a shorter time is not needed and not recommended.
450
gfz_format: `None` or str, optional, default `None`
451
	The file format to parse the files passed via `gfzpath_all` and `gfzpath_all`.
452
	Use `None`, "default", "gfz", or "standard" for the "standard" GFZ ASCII files.
453
	Use "wdc" to parse files in WDC format into a full-length `pandas.DataFrame`.
454
	Use "hp30" or "hp60" to read the Hp30 and Hp60 data files.
455
"""
456
457
_PARSERS = {
458
	"default": (read_gfz, update_gfz),
459
	"gfz": (read_gfz, update_gfz),
460
	"standard": (read_gfz, update_gfz),
461
	"wdc": (read_gfz_wdc, update_gfz),
462
	"hp30": (read_gfz_hp, update_gfz_hp30),
463
	"hp60": (read_gfz_hp, update_gfz_hp60),
464
}
465
466
467
def _doc_param(**sub):
468
	def dec(obj):
469
		obj.__doc__ = obj.__doc__.format(**sub)
470
		return obj
471
	return dec
472
473
474
@_doc_param(params=_GFZ_COMMON_PARAMS)
475
def gfz_daily(
476
	gfzpath_all=None,
477
	gfzpath_30d=None,
478
	update=False,
479
	update_interval="10days",
480
	gfz_format=None,
481
):
482
	"""Combined daily Ap, Kp, and f10.7 index values
483
484
	Combines the "historic" and last-30-day data into one dataframe.
485
486
	All arguments are optional and changing them from the defaults should not
487
	be required neither should it be necessary nor is it recommended.
488
	{params}
489
	Returns
490
	-------
491
	gfz_df: pandas.DataFrame
492
		The combined parsed space weather data (daily values).
493
		Raises ``IOError`` if the data files cannot be found.
494
		The index is returned timezone-naive but contains UTC timestamps.
495
		To convert to a timezone-aware index, use
496
		:meth:`pandas.DataFrame.tz_localize()`: ``gfz_df.tz_localize("utc")``.
497
498
	See Also
499
	--------
500
	gfz_3h, read_gfz
501
	"""
502
	gfzpath_all = gfzpath_all or GFZ_PATH_ALL
503
	gfzpath_30d = gfzpath_30d or GFZ_PATH_30D
504
	gfz_format = gfz_format or "gfz"
505
	parse_func, update_func = _PARSERS[gfz_format.lower()]
506
	# ensure that the file exists and is up to date
507
	if (
508
		not os.path.exists(gfzpath_all)
509
		or not os.path.exists(gfzpath_30d)
510
	):
511
		warn("Could not find space weather data, trying to download.")
512
		update_func(gfzpath_all=gfzpath_all, gfzpath_30d=gfzpath_30d)
513
514
	if (
515
		get_gfz_age(gfzpath_all) > pd.Timedelta("30days")
516
		or get_gfz_age(gfzpath_30d) > pd.Timedelta(update_interval)
517
	):
518
		if update:
519
			update_func(gfzpath_all=gfzpath_all, gfzpath_30d=gfzpath_30d)
520
		else:
521
			warn(
522
				"Local data files are older than {0}, pass `update=True` or "
523
				"run `gfz.update_gfz()` manually if you need newer data.".format(
524
					update_interval
525
				)
526
			)
527
528
	df_all = parse_func(gfzpath_all)
529
	df_30d = parse_func(gfzpath_30d)
530
	return pd.concat([df_all, df_30d[df_all.index[-1]:].iloc[1:]])
531
532
533
@_doc_param(params=_GFZ_COMMON_PARAMS)
534
def gfz_3h(*args, **kwargs):
535
	"""3h values of Ap and Kp
536
537
	Provides the 3-hourly Ap and Kp indices from the full daily data set.
538
539
	Accepts the same arguments as `gfz_daily()`.
540
	All arguments are optional and changing them from the defaults should not
541
	be required neither should it be necessary nor is it recommended.
542
	{params}
543
	Returns
544
	-------
545
	gfz_df: pandas.DataFrame
546
		The combined Ap and Kp index data (3h values).
547
		The index values are centred at the 3h interval, i.e. at 01:30:00,
548
		04:30:00, 07:30:00, ... and so on.
549
		Raises ``IOError`` if the data files cannot be found.
550
		The index is returned timezone-naive but contains UTC timestamps.
551
		To convert to a timezone-aware index, use
552
		:meth:`pandas.DataFrame.tz_localize()`: ``gfz_df.tz_localize("utc")``.
553
554
	See Also
555
	--------
556
	gfz_daily
557
	"""
558
	daily_df = gfz_daily(*args, **kwargs)
559
	ret = daily_df.copy()
560
	apns = list(map("Ap{0}".format, range(0, 23, 3)))
561
	kpns = list(map("Kp{0}".format, range(0, 23, 3)))
562
	for i, (ap, kp) in enumerate(zip(apns, kpns)):
563
		ret[ap].index = daily_df[ap].index + pd.Timedelta((i * 3 + 1.5), unit="h")
564
		ret[kp].index = daily_df[kp].index + pd.Timedelta((i * 3 + 1.5), unit="h")
565
	gfz_ap = pd.concat(map(ret.__getitem__, apns))
566
	gfz_kp = pd.concat(map(ret.__getitem__, kpns))
567
	df = pd.DataFrame({"Ap": gfz_ap, "Kp": gfz_kp})
568
	return df.reindex(df.index.sort_values())
569