imsciences 0.9.6.3__py3-none-any.whl → 0.9.6.4__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.
Potentially problematic release.
This version of imsciences might be problematic. Click here for more details.
- imsciences/pull.py +356 -253
- {imsciences-0.9.6.3.dist-info → imsciences-0.9.6.4.dist-info}/METADATA +1 -1
- {imsciences-0.9.6.3.dist-info → imsciences-0.9.6.4.dist-info}/RECORD +7 -7
- {imsciences-0.9.6.3.dist-info → imsciences-0.9.6.4.dist-info}/LICENSE.txt +0 -0
- {imsciences-0.9.6.3.dist-info → imsciences-0.9.6.4.dist-info}/PKG-INFO-TomG-HP-290722 +0 -0
- {imsciences-0.9.6.3.dist-info → imsciences-0.9.6.4.dist-info}/WHEEL +0 -0
- {imsciences-0.9.6.3.dist-info → imsciences-0.9.6.4.dist-info}/top_level.txt +0 -0
imsciences/pull.py
CHANGED
|
@@ -380,265 +380,368 @@ class datapull:
|
|
|
380
380
|
############################################################### Seasonality ##########################################################################
|
|
381
381
|
|
|
382
382
|
def pull_seasonality(self, week_commencing, start_date, countries):
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# ---------------------------------------------------------------------
|
|
386
|
-
day_dict = {"mon": 0, "tue": 1, "wed": 2, "thu": 3, "fri": 4, "sat": 5, "sun": 6}
|
|
387
|
-
|
|
388
|
-
# ---------------------------------------------------------------------
|
|
389
|
-
# 1. Create daily date range from start_date to today
|
|
390
|
-
# ---------------------------------------------------------------------
|
|
391
|
-
date_range = pd.date_range(
|
|
392
|
-
start=pd.to_datetime(start_date),
|
|
393
|
-
end=datetime.today(),
|
|
394
|
-
freq="D"
|
|
395
|
-
)
|
|
396
|
-
df_daily = pd.DataFrame(date_range, columns=["Date"])
|
|
383
|
+
"""
|
|
384
|
+
Generates a DataFrame with weekly seasonality features.
|
|
397
385
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
lambda x: x - pd.Timedelta(days=(x.weekday() - day_dict[week_commencing]) % 7)
|
|
403
|
-
)
|
|
386
|
+
Args:
|
|
387
|
+
week_commencing (str): The starting day of the week ('mon', 'tue', ..., 'sun').
|
|
388
|
+
start_date (str): The start date in 'YYYY-MM-DD' format.
|
|
389
|
+
countries (list): A list of country codes (e.g., ['GB', 'US']) for holidays.
|
|
404
390
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
391
|
+
Returns:
|
|
392
|
+
pd.DataFrame: A DataFrame indexed by week start date, containing various
|
|
393
|
+
seasonal dummy variables, holidays, trend, and constant.
|
|
394
|
+
The date column is named 'OBS'.
|
|
395
|
+
"""
|
|
396
|
+
# ---------------------------------------------------------------------
|
|
397
|
+
# 0. Setup: dictionary for 'week_commencing' to Python weekday() integer
|
|
398
|
+
# ---------------------------------------------------------------------
|
|
399
|
+
day_dict = {"mon": 0, "tue": 1, "wed": 2, "thu": 3, "fri": 4, "sat": 5, "sun": 6}
|
|
400
|
+
if week_commencing not in day_dict:
|
|
401
|
+
raise ValueError(f"Invalid week_commencing value: {week_commencing}. Use one of {list(day_dict.keys())}")
|
|
402
|
+
|
|
403
|
+
# ---------------------------------------------------------------------
|
|
404
|
+
# 1. Create daily date range from start_date to today
|
|
405
|
+
# ---------------------------------------------------------------------
|
|
406
|
+
try:
|
|
407
|
+
start_dt = pd.to_datetime(start_date)
|
|
408
|
+
except ValueError:
|
|
409
|
+
raise ValueError(f"Invalid start_date format: {start_date}. Use 'YYYY-MM-DD'")
|
|
410
|
+
|
|
411
|
+
end_dt = datetime.today()
|
|
412
|
+
# Ensure end date is not before start date
|
|
413
|
+
if end_dt < start_dt:
|
|
414
|
+
end_dt = start_dt + timedelta(days=1) # Or handle as error if preferred
|
|
415
|
+
|
|
416
|
+
date_range = pd.date_range(
|
|
417
|
+
start=start_dt,
|
|
418
|
+
end=end_dt,
|
|
419
|
+
freq="D"
|
|
432
420
|
)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
421
|
+
df_daily = pd.DataFrame(date_range, columns=["Date"])
|
|
422
|
+
|
|
423
|
+
# ---------------------------------------------------------------------
|
|
424
|
+
# 1.1 Identify "week_start" for each daily row, based on week_commencing
|
|
425
|
+
# ---------------------------------------------------------------------
|
|
426
|
+
start_day_int = day_dict[week_commencing]
|
|
427
|
+
df_daily['week_start'] = df_daily["Date"].apply(
|
|
428
|
+
lambda x: x - pd.Timedelta(days=(x.weekday() - start_day_int) % 7)
|
|
436
429
|
)
|
|
437
|
-
# Create columns for specific holiday names
|
|
438
|
-
for date_hol, name in country_holidays.items():
|
|
439
|
-
col_name = f"seas_{name.replace(' ', '_').lower()}_{country.lower()}"
|
|
440
|
-
if col_name not in df_daily.columns:
|
|
441
|
-
df_daily[col_name] = 0
|
|
442
|
-
df_daily.loc[df_daily["Date"] == pd.Timestamp(date_hol), col_name] = 1
|
|
443
|
-
|
|
444
|
-
# ---------------------------------------------------------------------
|
|
445
|
-
# 3.1 Additional Special Days (Father's Day, Mother's Day, etc.)
|
|
446
|
-
# We'll add daily columns for each.
|
|
447
|
-
# ---------------------------------------------------------------------
|
|
448
|
-
# Initialize columns
|
|
449
|
-
extra_cols = [
|
|
450
|
-
"seas_valentines_day",
|
|
451
|
-
"seas_halloween",
|
|
452
|
-
"seas_fathers_day_us_uk",
|
|
453
|
-
"seas_mothers_day_us",
|
|
454
|
-
"seas_mothers_day_uk",
|
|
455
|
-
"seas_good_friday",
|
|
456
|
-
"seas_easter_monday",
|
|
457
|
-
"seas_black_friday",
|
|
458
|
-
"seas_cyber_monday",
|
|
459
|
-
]
|
|
460
|
-
for c in extra_cols:
|
|
461
|
-
df_daily[c] = 0 # default zero
|
|
462
|
-
|
|
463
|
-
# Helper: nth_weekday_of_month(year, month, weekday, nth=1 => first, 2 => second, etc.)
|
|
464
|
-
# weekday: Monday=0, Tuesday=1, ... Sunday=6
|
|
465
|
-
def nth_weekday_of_month(year, month, weekday, nth):
|
|
466
|
-
"""
|
|
467
|
-
Returns date of the nth <weekday> in <month> of <year>.
|
|
468
|
-
E.g. nth_weekday_of_month(2023, 6, 6, 3) => 3rd Sunday of June 2023.
|
|
469
|
-
"""
|
|
470
|
-
# 1st day of the month
|
|
471
|
-
d = datetime(year, month, 1)
|
|
472
|
-
# What is the weekday of day #1?
|
|
473
|
-
w = d.weekday() # Monday=0, Tuesday=1, ... Sunday=6
|
|
474
|
-
# If we want, e.g. Sunday=6, we see how many days to add
|
|
475
|
-
delta = (weekday - w) % 7
|
|
476
|
-
# This is the first <weekday> in that month
|
|
477
|
-
first_weekday = d + timedelta(days=delta)
|
|
478
|
-
# Now add 7*(nth-1) days
|
|
479
|
-
return first_weekday + timedelta(days=7 * (nth-1))
|
|
480
|
-
|
|
481
|
-
def get_good_friday(year):
|
|
482
|
-
"""Good Friday is 2 days before Easter Sunday."""
|
|
483
|
-
return easter(year) - timedelta(days=2)
|
|
484
|
-
|
|
485
|
-
def get_easter_monday(year):
|
|
486
|
-
"""Easter Monday is 1 day after Easter Sunday."""
|
|
487
|
-
return easter(year) + timedelta(days=1)
|
|
488
|
-
|
|
489
|
-
def get_black_friday(year):
|
|
490
|
-
"""
|
|
491
|
-
Black Friday = day after US Thanksgiving,
|
|
492
|
-
and US Thanksgiving is the 4th Thursday in November.
|
|
493
|
-
"""
|
|
494
|
-
# 4th Thursday in November
|
|
495
|
-
fourth_thursday = nth_weekday_of_month(year, 11, 3, 4) # weekday=3 => Thursday
|
|
496
|
-
return fourth_thursday + timedelta(days=1)
|
|
497
|
-
|
|
498
|
-
def get_cyber_monday(year):
|
|
499
|
-
"""Cyber Monday = Monday after US Thanksgiving, i.e. 4 days after 4th Thursday in Nov."""
|
|
500
|
-
# 4th Thursday in November
|
|
501
|
-
fourth_thursday = nth_weekday_of_month(year, 11, 3, 4)
|
|
502
|
-
return fourth_thursday + timedelta(days=4) # Monday after Thanksgiving
|
|
503
|
-
|
|
504
|
-
# Loop over each year in range
|
|
505
|
-
start_yr = int(start_date[:4])
|
|
506
|
-
end_yr = datetime.today().year
|
|
507
|
-
|
|
508
|
-
for yr in range(start_yr, end_yr + 1):
|
|
509
|
-
# Valentines = Feb 14
|
|
510
|
-
valentines_day = datetime(yr, 2, 14)
|
|
511
|
-
# Halloween = Oct 31
|
|
512
|
-
halloween_day = datetime(yr, 10, 31)
|
|
513
|
-
# Father's Day (US & UK) = 3rd Sunday in June
|
|
514
|
-
fathers_day = nth_weekday_of_month(yr, 6, 6, 3) # Sunday=6
|
|
515
|
-
# Mother's Day US = 2nd Sunday in May
|
|
516
|
-
mothers_day_us = nth_weekday_of_month(yr, 5, 6, 2)
|
|
517
|
-
mothering_sunday = easter(yr) - timedelta(days=21)
|
|
518
|
-
# If for some reason that's not a Sunday (rare corner cases), shift to Sunday:
|
|
519
|
-
while mothering_sunday.weekday() != 6: # Sunday=6
|
|
520
|
-
mothering_sunday -= timedelta(days=1)
|
|
521
|
-
|
|
522
|
-
# Good Friday, Easter Monday
|
|
523
|
-
gf = get_good_friday(yr)
|
|
524
|
-
em = get_easter_monday(yr)
|
|
525
|
-
|
|
526
|
-
# Black Friday, Cyber Monday
|
|
527
|
-
bf = get_black_friday(yr)
|
|
528
|
-
cm = get_cyber_monday(yr)
|
|
529
|
-
|
|
530
|
-
# Mark them in df_daily if in range
|
|
531
|
-
for special_date, col in [
|
|
532
|
-
(valentines_day, "seas_valentines_day"),
|
|
533
|
-
(halloween_day, "seas_halloween"),
|
|
534
|
-
(fathers_day, "seas_fathers_day_us_uk"),
|
|
535
|
-
(mothers_day_us, "seas_mothers_day_us"),
|
|
536
|
-
(mothering_sunday, "seas_mothers_day_uk"),
|
|
537
|
-
(gf, "seas_good_friday"),
|
|
538
|
-
(em, "seas_easter_monday"),
|
|
539
|
-
(bf, "seas_black_friday"),
|
|
540
|
-
(cm, "seas_cyber_monday"),
|
|
541
|
-
]:
|
|
542
|
-
# Convert to pd.Timestamp:
|
|
543
|
-
special_ts = pd.Timestamp(special_date)
|
|
544
|
-
|
|
545
|
-
# Only set if it's within your daily range
|
|
546
|
-
if (special_ts >= df_daily["Date"].min()) and (special_ts <= df_daily["Date"].max()):
|
|
547
|
-
df_daily.loc[df_daily["Date"] == special_ts, col] = 1
|
|
548
|
-
|
|
549
|
-
# ---------------------------------------------------------------------
|
|
550
|
-
# 4. Add daily indicators for last day & last Friday of month
|
|
551
|
-
# Then aggregate them to weekly level using .max()
|
|
552
|
-
# ---------------------------------------------------------------------
|
|
553
|
-
# Last day of month (daily)
|
|
554
|
-
df_daily["seas_last_day_of_month"] = df_daily["Date"].apply(
|
|
555
|
-
lambda d: 1 if d == d.to_period("M").to_timestamp("M") else 0
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
# Last Friday of month (daily)
|
|
559
|
-
def is_last_friday(date):
|
|
560
|
-
# last day of the month
|
|
561
|
-
last_day_of_month = date.to_period("M").to_timestamp("M")
|
|
562
|
-
last_day_weekday = last_day_of_month.weekday() # Monday=0,...Sunday=6
|
|
563
|
-
# Determine how many days we go back from the last day to get Friday (weekday=4)
|
|
564
|
-
if last_day_weekday >= 4:
|
|
565
|
-
days_to_subtract = last_day_weekday - 4
|
|
566
|
-
else:
|
|
567
|
-
days_to_subtract = last_day_weekday + 3
|
|
568
|
-
last_friday = last_day_of_month - pd.Timedelta(days=days_to_subtract)
|
|
569
|
-
return 1 if date == last_friday else 0
|
|
570
|
-
|
|
571
|
-
df_daily["seas_last_friday_of_month"] = df_daily["Date"].apply(is_last_friday)
|
|
572
|
-
|
|
573
|
-
# ---------------------------------------------------------------------
|
|
574
|
-
# 5. Weekly aggregation for holiday columns & monthly dummies
|
|
575
|
-
# ---------------------------------------------------------------------
|
|
576
|
-
# For monthly dummies, create a daily col "Month", then get_dummies
|
|
577
|
-
df_daily["Month"] = df_daily["Date"].dt.month_name().str.lower()
|
|
578
|
-
df_monthly_dummies = pd.get_dummies(
|
|
579
|
-
df_daily,
|
|
580
|
-
prefix="seas",
|
|
581
|
-
columns=["Month"],
|
|
582
|
-
dtype=int
|
|
583
|
-
)
|
|
584
|
-
# Recalculate 'week_start' (already in df_daily, but just to be sure)
|
|
585
|
-
df_monthly_dummies['week_start'] = df_daily['week_start']
|
|
586
|
-
|
|
587
|
-
# Group monthly dummies by .sum() or .mean()—we often spread them across the week
|
|
588
|
-
df_monthly_dummies = (
|
|
589
|
-
df_monthly_dummies
|
|
590
|
-
.groupby('week_start')
|
|
591
|
-
.sum(numeric_only=True) # sum the daily flags
|
|
592
|
-
.reset_index()
|
|
593
|
-
.rename(columns={'week_start': "Date"})
|
|
594
|
-
.set_index("Date")
|
|
595
|
-
)
|
|
596
|
-
# Spread monthly dummies by 7 to distribute across that week
|
|
597
|
-
monthly_cols = [c for c in df_monthly_dummies.columns if c.startswith("seas_month_")]
|
|
598
|
-
df_monthly_dummies[monthly_cols] = df_monthly_dummies[monthly_cols] / 7
|
|
599
|
-
|
|
600
|
-
# Group holiday & special-day columns by .max() => binary at weekly level
|
|
601
|
-
df_holidays = (
|
|
602
|
-
df_daily
|
|
603
|
-
.groupby('week_start')
|
|
604
|
-
.max(numeric_only=True) # if any day=1 in that week, entire week=1
|
|
605
|
-
.reset_index()
|
|
606
|
-
.rename(columns={'week_start': "Date"})
|
|
607
|
-
.set_index("Date")
|
|
608
|
-
)
|
|
609
430
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
431
|
+
# ---------------------------------------------------------------------
|
|
432
|
+
# 1.2 Calculate ISO week number for each DAY (for later aggregation)
|
|
433
|
+
# Also calculate Year for each DAY to handle year transitions correctly
|
|
434
|
+
# ---------------------------------------------------------------------
|
|
435
|
+
df_daily['iso_week_daily'] = df_daily['Date'].dt.isocalendar().week.astype(int)
|
|
436
|
+
df_daily['iso_year_daily'] = df_daily['Date'].dt.isocalendar().year.astype(int)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ---------------------------------------------------------------------
|
|
440
|
+
# 2. Build a weekly index (df_weekly_start) based on unique week_start dates
|
|
441
|
+
# ---------------------------------------------------------------------
|
|
442
|
+
df_weekly_start = df_daily[['week_start']].drop_duplicates().sort_values('week_start').reset_index(drop=True)
|
|
443
|
+
df_weekly_start.rename(columns={'week_start': "Date"}, inplace=True)
|
|
444
|
+
df_weekly_start.set_index("Date", inplace=True)
|
|
445
|
+
|
|
446
|
+
# Create individual weekly dummies (optional, uncomment if needed)
|
|
447
|
+
# dummy_columns = {}
|
|
448
|
+
# for i, date_index in enumerate(df_weekly_start.index):
|
|
449
|
+
# col_name = f"dum_{date_index.strftime('%Y_%m_%d')}"
|
|
450
|
+
# dummy_columns[col_name] = [0] * len(df_weekly_start)
|
|
451
|
+
# dummy_columns[col_name][i] = 1
|
|
452
|
+
# df_dummies = pd.DataFrame(dummy_columns, index=df_weekly_start.index)
|
|
453
|
+
# df_weekly_start = pd.concat([df_weekly_start, df_dummies], axis=1)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ---------------------------------------------------------------------
|
|
457
|
+
# 3. Public holidays (daily) from 'holidays' package + each holiday name
|
|
458
|
+
# ---------------------------------------------------------------------
|
|
459
|
+
start_year = start_dt.year
|
|
460
|
+
end_year = end_dt.year
|
|
461
|
+
years_range = range(start_year, end_year + 1)
|
|
462
|
+
|
|
463
|
+
for country in countries:
|
|
464
|
+
try:
|
|
465
|
+
country_holidays = holidays.CountryHoliday(
|
|
466
|
+
country,
|
|
467
|
+
years=years_range,
|
|
468
|
+
observed=False # Typically you want the actual date, not observed substitute
|
|
469
|
+
)
|
|
470
|
+
# Handle cases like UK where specific subdivisions might be needed for some holidays
|
|
471
|
+
# Example: if country == 'GB': country_holidays.observed = True # If observed are needed
|
|
472
|
+
except KeyError:
|
|
473
|
+
print(f"Warning: Country code '{country}' not found in holidays library. Skipping.")
|
|
474
|
+
continue # Skip to next country
|
|
475
|
+
|
|
476
|
+
# Daily indicator: 1 if that date is a holiday
|
|
477
|
+
df_daily[f"seas_holiday_{country.lower()}"] = df_daily["Date"].apply(
|
|
478
|
+
lambda x: 1 if x in country_holidays else 0
|
|
479
|
+
)
|
|
480
|
+
# Create columns for specific holiday names
|
|
481
|
+
for date_hol, name in sorted(country_holidays.items()): # Sort for consistent column order
|
|
482
|
+
# Clean name: lower, replace space with underscore, remove non-alphanumeric (except underscore)
|
|
483
|
+
clean_name = ''.join(c for c in name if c.isalnum() or c == ' ').strip().replace(' ', '_').lower()
|
|
484
|
+
clean_name = clean_name.replace('_(observed)', '').replace("'", "") # specific cleaning
|
|
485
|
+
col_name = f"seas_{clean_name}_{country.lower()}"
|
|
486
|
+
|
|
487
|
+
# Only create column if the holiday occurs within the df_daily date range
|
|
488
|
+
if pd.Timestamp(date_hol).year in years_range:
|
|
489
|
+
if col_name not in df_daily.columns:
|
|
490
|
+
df_daily[col_name] = 0
|
|
491
|
+
# Ensure date_hol is within the actual daily range before assigning
|
|
492
|
+
if (pd.Timestamp(date_hol) >= df_daily["Date"].min()) and (pd.Timestamp(date_hol) <= df_daily["Date"].max()):
|
|
493
|
+
df_daily.loc[df_daily["Date"] == pd.Timestamp(date_hol), col_name] = 1
|
|
494
|
+
|
|
495
|
+
# ---------------------------------------------------------------------
|
|
496
|
+
# 3.1 Additional Special Days (Father's Day, Mother's Day, etc.)
|
|
497
|
+
# ---------------------------------------------------------------------
|
|
498
|
+
extra_cols = [
|
|
499
|
+
"seas_valentines_day",
|
|
500
|
+
"seas_halloween",
|
|
501
|
+
"seas_fathers_day_us_uk", # Note: UK/US is 3rd Sun Jun, others vary
|
|
502
|
+
"seas_mothers_day_us", # Note: US is 2nd Sun May
|
|
503
|
+
"seas_mothers_day_uk", # Note: UK Mothering Sunday varies with Easter
|
|
504
|
+
"seas_good_friday",
|
|
505
|
+
"seas_easter_monday",
|
|
506
|
+
"seas_black_friday", # US-centric, but globally adopted
|
|
507
|
+
"seas_cyber_monday", # US-centric, but globally adopted
|
|
508
|
+
]
|
|
509
|
+
for c in extra_cols:
|
|
510
|
+
if c not in df_daily.columns: # Avoid overwriting if already created by holidays pkg
|
|
511
|
+
df_daily[c] = 0
|
|
512
|
+
|
|
513
|
+
# Helper: nth_weekday_of_month(year, month, weekday, nth)
|
|
514
|
+
def nth_weekday_of_month(year, month, weekday, nth):
|
|
515
|
+
d = datetime(year, month, 1)
|
|
516
|
+
w = d.weekday()
|
|
517
|
+
delta = (weekday - w + 7) % 7 # Ensure positive delta
|
|
518
|
+
first_weekday = d + timedelta(days=delta)
|
|
519
|
+
target_date = first_weekday + timedelta(days=7 * (nth - 1))
|
|
520
|
+
# Check if the calculated date is still in the same month
|
|
521
|
+
if target_date.month == month:
|
|
522
|
+
return target_date
|
|
523
|
+
else:
|
|
524
|
+
# This can happen if nth is too large (e.g., 5th Friday)
|
|
525
|
+
# Return the last occurrence of that weekday in the month instead
|
|
526
|
+
return target_date - timedelta(days=7)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def get_good_friday(year):
|
|
530
|
+
return easter(year) - timedelta(days=2)
|
|
531
|
+
|
|
532
|
+
def get_easter_monday(year):
|
|
533
|
+
return easter(year) + timedelta(days=1)
|
|
534
|
+
|
|
535
|
+
def get_black_friday(year):
|
|
536
|
+
# US Thanksgiving is 4th Thursday in November (weekday=3)
|
|
537
|
+
thanksgiving = nth_weekday_of_month(year, 11, 3, 4)
|
|
538
|
+
return thanksgiving + timedelta(days=1)
|
|
539
|
+
|
|
540
|
+
def get_cyber_monday(year):
|
|
541
|
+
# Monday after US Thanksgiving
|
|
542
|
+
thanksgiving = nth_weekday_of_month(year, 11, 3, 4)
|
|
543
|
+
return thanksgiving + timedelta(days=4)
|
|
544
|
+
|
|
545
|
+
def get_mothering_sunday_uk(year):
|
|
546
|
+
# Fourth Sunday in Lent (3 weeks before Easter Sunday)
|
|
547
|
+
# Lent starts on Ash Wednesday, 46 days before Easter.
|
|
548
|
+
# Easter Sunday is day 0. Sunday before is -7, etc.
|
|
549
|
+
# 4th Sunday in Lent is 3 weeks before Easter.
|
|
550
|
+
return easter(year) - timedelta(days=21)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# Loop over each year in range
|
|
554
|
+
for yr in range(start_year, end_year + 1):
|
|
555
|
+
try: # Wrap calculations in try-except for robustness
|
|
556
|
+
# Valentines = Feb 14
|
|
557
|
+
valentines_day = datetime(yr, 2, 14)
|
|
558
|
+
# Halloween = Oct 31
|
|
559
|
+
halloween_day = datetime(yr, 10, 31)
|
|
560
|
+
# Father's Day (US & UK) = 3rd Sunday (6) in June
|
|
561
|
+
fathers_day = nth_weekday_of_month(yr, 6, 6, 3)
|
|
562
|
+
# Mother's Day US = 2nd Sunday (6) in May
|
|
563
|
+
mothers_day_us = nth_weekday_of_month(yr, 5, 6, 2)
|
|
564
|
+
# Mother's Day UK (Mothering Sunday)
|
|
565
|
+
mothering_sunday = get_mothering_sunday_uk(yr)
|
|
566
|
+
|
|
567
|
+
# Good Friday, Easter Monday
|
|
568
|
+
gf = get_good_friday(yr)
|
|
569
|
+
em = get_easter_monday(yr)
|
|
570
|
+
|
|
571
|
+
# Black Friday, Cyber Monday
|
|
572
|
+
bf = get_black_friday(yr)
|
|
573
|
+
cm = get_cyber_monday(yr)
|
|
574
|
+
|
|
575
|
+
# Mark them in df_daily if in range
|
|
576
|
+
special_days_map = [
|
|
577
|
+
(valentines_day, "seas_valentines_day"),
|
|
578
|
+
(halloween_day, "seas_halloween"),
|
|
579
|
+
(fathers_day, "seas_fathers_day_us_uk"),
|
|
580
|
+
(mothers_day_us, "seas_mothers_day_us"),
|
|
581
|
+
(mothering_sunday,"seas_mothers_day_uk"),
|
|
582
|
+
(gf, "seas_good_friday"),
|
|
583
|
+
(em, "seas_easter_monday"),
|
|
584
|
+
(bf, "seas_black_friday"),
|
|
585
|
+
(cm, "seas_cyber_monday"),
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
for special_date, col in special_days_map:
|
|
589
|
+
if special_date is not None: # nth_weekday_of_month can return None edge cases
|
|
590
|
+
special_ts = pd.Timestamp(special_date)
|
|
591
|
+
# Only set if it's within the daily range AND column exists
|
|
592
|
+
if (special_ts >= df_daily["Date"].min()) and \
|
|
593
|
+
(special_ts <= df_daily["Date"].max()) and \
|
|
594
|
+
(col in df_daily.columns):
|
|
595
|
+
df_daily.loc[df_daily["Date"] == special_ts, col] = 1
|
|
596
|
+
except Exception as e:
|
|
597
|
+
print(f"Warning: Could not calculate special days for year {yr}: {e}")
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# ---------------------------------------------------------------------
|
|
601
|
+
# 4. Add daily indicators for last day & last Friday of month
|
|
602
|
+
# ---------------------------------------------------------------------
|
|
603
|
+
df_daily["is_last_day_of_month"] = df_daily["Date"].dt.is_month_end
|
|
604
|
+
|
|
605
|
+
def is_last_friday(date):
|
|
606
|
+
# Check if it's a Friday first
|
|
607
|
+
if date.weekday() != 4: # Friday is 4
|
|
608
|
+
return 0
|
|
609
|
+
# Check if next Friday is in the next month
|
|
610
|
+
next_friday = date + timedelta(days=7)
|
|
611
|
+
return 1 if next_friday.month != date.month else 0
|
|
612
|
+
|
|
613
|
+
df_daily["is_last_friday_of_month"] = df_daily["Date"].apply(is_last_friday)
|
|
614
|
+
|
|
615
|
+
# Rename for clarity prefix
|
|
616
|
+
df_daily.rename(columns={
|
|
617
|
+
"is_last_day_of_month": "seas_last_day_of_month",
|
|
618
|
+
"is_last_friday_of_month": "seas_last_friday_of_month"
|
|
619
|
+
}, inplace=True)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ---------------------------------------------------------------------
|
|
623
|
+
# 5. Weekly aggregation
|
|
624
|
+
# ---------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
# --- Aggregate flags using MAX (1 if any day in week is flagged) ---
|
|
627
|
+
# Select only columns that are indicators/flags (intended for max aggregation)
|
|
628
|
+
flag_cols = [col for col in df_daily.columns if col.startswith('seas_') or col.startswith('is_')]
|
|
629
|
+
# Ensure 'week_start' is present for grouping
|
|
630
|
+
df_to_agg = df_daily[['week_start'] + flag_cols]
|
|
631
|
+
|
|
632
|
+
df_weekly_flags = (
|
|
633
|
+
df_to_agg
|
|
634
|
+
.groupby('week_start')
|
|
635
|
+
.max() # if any day=1 in that week, entire week=1
|
|
636
|
+
.reset_index()
|
|
637
|
+
.rename(columns={'week_start': "Date"})
|
|
638
|
+
.set_index("Date")
|
|
639
|
+
)
|
|
641
640
|
|
|
641
|
+
# --- Aggregate Week Number using MODE ---
|
|
642
|
+
# Define aggregation function for mode (handling potential multi-modal cases by taking the first)
|
|
643
|
+
def get_mode(x):
|
|
644
|
+
modes = pd.Series.mode(x)
|
|
645
|
+
return modes[0] if not modes.empty else np.nan # Return first mode or NaN
|
|
646
|
+
|
|
647
|
+
df_weekly_iso_week_year = (
|
|
648
|
+
df_daily[['week_start', 'iso_week_daily', 'iso_year_daily']]
|
|
649
|
+
.groupby('week_start')
|
|
650
|
+
.agg(
|
|
651
|
+
# Find the most frequent week number and year within the group
|
|
652
|
+
Week=('iso_week_daily', get_mode),
|
|
653
|
+
Year=('iso_year_daily', get_mode)
|
|
654
|
+
)
|
|
655
|
+
.reset_index()
|
|
656
|
+
.rename(columns={'week_start': 'Date'})
|
|
657
|
+
.set_index('Date')
|
|
658
|
+
)
|
|
659
|
+
# Convert Week/Year back to integer type after aggregation
|
|
660
|
+
df_weekly_iso_week_year['Week'] = df_weekly_iso_week_year['Week'].astype(int)
|
|
661
|
+
df_weekly_iso_week_year['Year'] = df_weekly_iso_week_year['Year'].astype(int)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# --- Monthly dummies (spread evenly across week) ---
|
|
665
|
+
df_daily["Month"] = df_daily["Date"].dt.month_name().str.lower()
|
|
666
|
+
df_monthly_dummies_daily = pd.get_dummies(
|
|
667
|
+
df_daily[["week_start", "Month"]], # Only need these columns
|
|
668
|
+
prefix="seas_month",
|
|
669
|
+
columns=["Month"],
|
|
670
|
+
dtype=float # Use float for division
|
|
671
|
+
)
|
|
672
|
+
# Sum daily dummies within the week
|
|
673
|
+
df_monthly_dummies_summed = df_monthly_dummies_daily.groupby('week_start').sum()
|
|
674
|
+
# Divide by number of days in that specific week group (usually 7, except potentially start/end)
|
|
675
|
+
days_in_week = df_daily.groupby('week_start').size()
|
|
676
|
+
df_weekly_monthly_dummies = df_monthly_dummies_summed.div(days_in_week, axis=0)
|
|
677
|
+
|
|
678
|
+
# Reset index to merge
|
|
679
|
+
df_weekly_monthly_dummies.reset_index(inplace=True)
|
|
680
|
+
df_weekly_monthly_dummies.rename(columns={'week_start': 'Date'}, inplace=True)
|
|
681
|
+
df_weekly_monthly_dummies.set_index('Date', inplace=True)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ---------------------------------------------------------------------
|
|
685
|
+
# 6. Combine all weekly components
|
|
686
|
+
# ---------------------------------------------------------------------
|
|
687
|
+
# Start with the basic weekly index
|
|
688
|
+
df_combined = df_weekly_start.copy()
|
|
689
|
+
|
|
690
|
+
# Join the other aggregated DataFrames
|
|
691
|
+
df_combined = df_combined.join(df_weekly_flags, how='left')
|
|
692
|
+
df_combined = df_combined.join(df_weekly_iso_week_year, how='left')
|
|
693
|
+
df_combined = df_combined.join(df_weekly_monthly_dummies, how='left')
|
|
694
|
+
|
|
695
|
+
# Fill potential NaNs created by joins (e.g., if a flag column didn't exist) with 0
|
|
696
|
+
# Exclude 'Week' and 'Year' which should always be present
|
|
697
|
+
cols_to_fill = df_combined.columns.difference(['Week', 'Year'])
|
|
698
|
+
df_combined[cols_to_fill] = df_combined[cols_to_fill].fillna(0)
|
|
699
|
+
|
|
700
|
+
# Ensure correct types for flag columns (int)
|
|
701
|
+
for col in df_weekly_flags.columns:
|
|
702
|
+
if col in df_combined.columns:
|
|
703
|
+
df_combined[col] = df_combined[col].astype(int)
|
|
704
|
+
|
|
705
|
+
# Ensure correct types for month columns (float)
|
|
706
|
+
for col in df_weekly_monthly_dummies.columns:
|
|
707
|
+
if col in df_combined.columns:
|
|
708
|
+
df_combined[col] = df_combined[col].astype(float)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# ---------------------------------------------------------------------
|
|
712
|
+
# 7. Create weekly dummies for Week of Year & yearly dummies from aggregated cols
|
|
713
|
+
# ---------------------------------------------------------------------
|
|
714
|
+
df_combined.reset_index(inplace=True) # 'Date', 'Week', 'Year' become columns
|
|
715
|
+
|
|
716
|
+
# Create dummies from the aggregated 'Week' column
|
|
717
|
+
df_combined = pd.get_dummies(df_combined, prefix="seas", columns=["Week"], dtype=int, prefix_sep='_')
|
|
718
|
+
|
|
719
|
+
# Create dummies from the aggregated 'Year' column
|
|
720
|
+
df_combined = pd.get_dummies(df_combined, prefix="seas", columns=["Year"], dtype=int, prefix_sep='_')
|
|
721
|
+
|
|
722
|
+
# ---------------------------------------------------------------------
|
|
723
|
+
# 8. Add constant & trend
|
|
724
|
+
# ---------------------------------------------------------------------
|
|
725
|
+
df_combined["Constant"] = 1
|
|
726
|
+
df_combined.reset_index(drop=True, inplace=True) # Ensure index is 0, 1, 2... for trend
|
|
727
|
+
df_combined["Trend"] = df_combined.index + 1
|
|
728
|
+
|
|
729
|
+
# ---------------------------------------------------------------------
|
|
730
|
+
# 9. Rename Date -> OBS and select final columns
|
|
731
|
+
# ---------------------------------------------------------------------
|
|
732
|
+
df_combined.rename(columns={"Date": "OBS"}, inplace=True)
|
|
733
|
+
|
|
734
|
+
# Reorder columns - OBS first, then Constant, Trend, then seasonal features
|
|
735
|
+
cols_order = ['OBS', 'Constant', 'Trend'] + \
|
|
736
|
+
sorted([col for col in df_combined.columns if col.startswith('seas_')]) + \
|
|
737
|
+
sorted([col for col in df_combined.columns if col.startswith('dum_')]) # If individual week dummies were enabled
|
|
738
|
+
|
|
739
|
+
# Filter out columns not in the desired order list (handles case where dum_ cols are off)
|
|
740
|
+
final_cols = [col for col in cols_order if col in df_combined.columns]
|
|
741
|
+
df_combined = df_combined[final_cols]
|
|
742
|
+
|
|
743
|
+
return df_combined
|
|
744
|
+
|
|
642
745
|
def pull_weather(self, week_commencing, start_date, country_codes) -> pd.DataFrame:
|
|
643
746
|
"""
|
|
644
747
|
Pull weather data for a given week-commencing day and one or more country codes.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
imsciences/__init__.py,sha256=_HuYeLbDMTdt7GpKI4r6-d7yRPZgcAQ7yOW0-ydR2Yo,117
|
|
2
2
|
imsciences/geo.py,sha256=eenng7_BP_E2WD5Wt1G_oNxQS8W3t6lycRwJ91ngysY,15808
|
|
3
3
|
imsciences/mmm.py,sha256=qMh0ccOepehfCcux7EeG8cq6piSEoFEz5iiJbDBWOS4,82214
|
|
4
|
-
imsciences/pull.py,sha256=
|
|
4
|
+
imsciences/pull.py,sha256=aLgqFxbuCnSsyhlC3aWXcloeQGIGxY8KKeN8kNnns9c,95091
|
|
5
5
|
imsciences/unittesting.py,sha256=U177_Txg0Lqn49zYRu5bl9OVe_X7MkNJ6V_Zd6DHOsU,45656
|
|
6
6
|
imsciences/vis.py,sha256=2izdHQhmWEReerRqIxhY4Ai10VjL7xoUqyWyZC7-2XI,8931
|
|
7
|
-
imsciences-0.9.6.
|
|
8
|
-
imsciences-0.9.6.
|
|
9
|
-
imsciences-0.9.6.
|
|
10
|
-
imsciences-0.9.6.
|
|
11
|
-
imsciences-0.9.6.
|
|
12
|
-
imsciences-0.9.6.
|
|
7
|
+
imsciences-0.9.6.4.dist-info/LICENSE.txt,sha256=lVq2QwcExPX4Kl2DHeEkRrikuItcDB1Pr7yF7FQ8_z8,1108
|
|
8
|
+
imsciences-0.9.6.4.dist-info/METADATA,sha256=3UJiLbs9Uqjv6e1GBIppcQYVH3PUR5_yo2cs951MMq4,18846
|
|
9
|
+
imsciences-0.9.6.4.dist-info/PKG-INFO-TomG-HP-290722,sha256=RMcthCSyWmU6IBsXGL-nYqw0RP06pzjPKK3dzOQcU-8,18846
|
|
10
|
+
imsciences-0.9.6.4.dist-info/WHEEL,sha256=ixB2d4u7mugx_bCBycvM9OzZ5yD7NmPXFRtKlORZS2Y,91
|
|
11
|
+
imsciences-0.9.6.4.dist-info/top_level.txt,sha256=hsENS-AlDVRh8tQJ6-426iUQlla9bPcGc0-UlFF0_iU,11
|
|
12
|
+
imsciences-0.9.6.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|