Рассчитать количество дней в каждом месяце между двумя днями

1

Я ищу функцию, которая будет принимать 2 даты (прием и выписка) и финансовый год и возвращать количество дней в каждом месяце между этими датами.

Финансовый год начинается 1 апреля → 31 марта

В настоящее время у меня есть решение (ниже), которое представляет собой беспорядок в SPSS и Python, в конечном счете его необходимо будет внедрить обратно в SPSS, но в качестве функции Python гораздо более удобной, к сожалению, это означает, что она может использовать только стандартные библиотеки (не Pandas).

например

+-----------------+-----------------+------+--+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|    Admission    |    Discharge    |  FY  |  | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | Jan | Feb | Mar |
+-----------------+-----------------+------+--+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 01 January 2017 | 05 January 2017 | 1617 |  |   0 |   0 |   0 |   0 |   0 |   0 |   0 |   0 |   0 |   4 |   0 |   0 |
| 01 January 2017 | 05 June 2017    | 1617 |  |   0 |   0 |   0 |   0 |   0 |   0 |   0 |   0 |   0 |  31 |  28 |  31 |
| 01 January 2017 | 05 June 2017    | 1718 |  |  30 |  31 |   4 |   0 |   0 |   0 |   0 |   0 |   0 |   0 |   0 |   0 |
| 01 January 2017 | 01 January 2019 | 1718 |  |  30 |  31 |  30 |  31 |  31 |  30 |  31 |  30 |  31 |  31 |  28 |  31 |
+-----------------+-----------------+------+--+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+

Связанный - Как рассчитать количество дней между двумя данными датами?

Текущее решение (код SPSS)

 * Count the beddays.
 * Similar method to that used in Care homes.
 * 1) Declare an SPSS macro which will set the beddays for each month.
 * 2) Use python to run the macro with the correct parameters.
 * This means that different month lengths and leap years are handled correctly.
Define !BedDaysPerMonth (Month = !Tokens(1) 
   /MonthNum = !Tokens(1) 
   /DaysInMonth = !Tokens(1) 
   /Year = !Tokens(1))

 * Store the start and end date of the given month.
Compute #StartOfMonth = Date.DMY(1, !MonthNum, !Year).
Compute #EndOfMonth = Date.DMY(!DaysInMonth, !MonthNum, !Year).

 * Create the names of the variables e.g. April_beddays and April_cost.
!Let !BedDays = !Concat(!Month, "_beddays").

 * Create variables for the month.
Numeric !BedDays (F2.0).

 * Go through all possibilities to decide how many days to be allocated.
Do if keydate1_dateformat LE #StartOfMonth.
   Do if keydate2_dateformat GE #EndOfMonth.
      Compute !BedDays = !DaysInMonth.
   Else.
      Compute !BedDays = DateDiff(keydate2_dateformat, #StartOfMonth, "days").
   End If.
Else if keydate1_dateformat LE #EndOfMonth.
   Do if keydate2_dateformat GT #EndOfMonth.
      Compute !BedDays = DateDiff(#EndOfMonth, keydate1_dateformat, "days") + 1.
   Else.
      Compute !BedDays = DateDiff(keydate2_dateformat, keydate1_dateformat, "days").
   End If.
Else.
   Compute !BedDays = 0.
End If.

 * Months after the discharge date will end up with negatives.
If !BedDays < 0 !BedDays = 0.
!EndDefine.

 * This python program will call the macro for each month with the right variables.
 * They will also be in FY order.
Begin Program.
from calendar import month_name, monthrange
from datetime import date
import spss

#Set the financial year, this line reads the first variable ('year')
fin_year = int((int(spss.Cursor().fetchone()[0]) // 100) + 2000)

#This line generates a 'dictionary' which will hold all the info we need for each month
#month_name is a list of all the month names and just needs the number of the month
#(m < 4) + 2015 - This will set the year to be 2015 for April onwards and 2016 other wise
#monthrange takes a year and a month number and returns 2 numbers, the first and last day of the month, we only need the second.
months = {m: [month_name[m], (m < 4) + fin_year, monthrange((m < 4) + fin_year, m)[1]]  for m in range(1,13)}
print(months) #Print to the output window so you can see how it works

#This will make the output look a bit nicer
print("\n\n***This is the syntax that will be run:***")

#This loops over the months above but first sorts them by year, meaning they are in correct FY order
for month in sorted(months.items(), key=lambda x: x[1][1]):
   syntax = "!BedDaysPerMonth Month = " + month[1][0][:3]
   syntax += " MonthNum = " + str(month[0])
   syntax += " DaysInMonth = " + str(month[1][2])
   syntax += " Year = " + str(month[1][1]) + "."

   print(syntax)
   spss.Submit(syntax)
End Program.
  • 0
    Возможный дубликат Как рассчитать количество дней между двумя указанными датами?
  • 0
    Посмотрите на модуль datetime для правильного представления дат и dateutil для надежного разбора строк на даты.
Показать ещё 8 комментариев
Теги:
date
spss

7 ответов

3

Единственный способ, которым я могу придумать, - это циклически просматривать каждый день и анализировать месяц, которому он принадлежит:

import time, collections
SECONDS_PER_DAY = 24 * 60 * 60
def monthlyBedDays(admission, discharge, fy=None):

    start = time.mktime(time.strptime(admission, '%d-%b-%Y'))
    end = time.mktime(time.strptime( discharge, '%d-%b-%Y'))
    if fy is not None:
        fy = str(fy)
        start = max(start, time.mktime(time.strptime('01-Apr-'+fy[:2], '%d-%b-%y')))
        end   = min(end,   time.mktime(time.strptime('31-Mar-'+fy[2:], '%d-%b-%y')))
    days = collections.defaultdict(int)
    for day in range(int(start), int(end) + SECONDS_PER_DAY, SECONDS_PER_DAY):
        day = time.localtime(day)
        key = time.strftime('%Y-%m', day)  # use '%b' to answer the question exactly, but that not such a good idea
        days[ key ] += 1
    return days

output = monthlyBedDays(admission="01-Jan-2018", discharge="25-Apr-2018")
print(output)
# Prints:
# defaultdict(<class 'int'>, {'2018-01': 31, '2018-02': 28, '2018-03': 31, '2018-04': 25})

print(monthlyBedDays(admission="01-Jan-2018", discharge="25-Apr-2018", fy=1718))
# Prints:
# defaultdict(<class 'int'>, {'2018-01': 31, '2018-02': 28, '2018-03': 31})

print(monthlyBedDays(admission="01-Jan-2018", discharge="25-Apr-2018", fy=1819))
# Prints:
# defaultdict(<class 'int'>, {'2018-04': 25})

Обратите внимание, что выходные данные являются defaultdict, так что, если вы спросите его о количестве дней в каком-либо месяце (или о каком-либо ключе вообще), который не был записан (например, output['1999-12']), он вернет 0. Обратите внимание, что я использовал формат '%Y-%m' для клавиш вывода. Это значительно упрощает сортировку результатов и устранение неоднозначности между месяцами, которые происходят в разные годы, чем если вы используете тип ключа, который вы первоначально запрашивали ('%b''Jan').

  • 0
    Это большое спасибо, да, я думаю, что смогу справиться с FY из-за этого Одного вопроса, почему вы используете SECONDS_PER_DAY? Будет ли datetime.timedelta (days = 1) (из ответа Ральфа быть эквивалентным?
  • 0
    Вы можете использовать datetime вместо time - это был бы более современный подход, но оба работают, и я просто больше знаком со time .
Показать ещё 1 комментарий
2

Прежде всего, я предлагаю использовать экземпляры datetime.date, чтобы вы могли заранее проанализировать ваши даты примерно так:

import datetime
date = datetime.datetime.strptime('17-Jan-2018', '%d-%b-%Y').date()

Затем вы можете использовать что-то вроде этого, чтобы перебрать диапазон дат:

import datetime
import collections

def f(start_date, end_date, fy_str):
    # if the date range falls outside the financial year, cut it off
    fy_start = datetime.date(2000 + int(fy_str[:2]), 4, 1)
    if start_date < fy_start:
        start_date = fy_start
    fy_end = datetime.date(2000 + int(fy_str[2:]), 3, 31)
    if end_date > fy_end:
        end_date = fy_end

    month_dict = collections.defaultdict(int)

    date = start_date
    while date <= end_date:
        # the key holds year and month to make sorting easier
        key = '{}-{:02d}'.format(date.year, date.month)

        month_dict[key] += 1
        date += datetime.timedelta(days=1)

    return month_dict

Использование будет таким:

>>> d1 = datetime.date(2018, 2, 5)
>>> d2 = datetime.date(2019, 1, 17)


>>> r = f(d1, d2, '1718')
>>> for k, v in sorted(r.items()):
...     print(k, v)
2018-02 24
2018-03 31

>>> r = f(d1, d2, '1819')
>>> for k, v in sorted(r.items()):
...     print(k, v)
2018-04 30
2018-05 31
2018-06 30
2018-07 31
2018-08 31
2018-09 30
2018-10 31
2018-11 30
2018-12 31
2019-01 17
1

Вот один из способов:

def monthlyBeddays(adm, dis, fy):

    # Create a data range between admission and discharge dates
    # and replace each year with fy
    r = pd.Series(pd.date_range(adm, dis)).apply(lambda x: x.replace(year=fy))

    # Get the last day of month for each month within the range
    m = r.dt.day.groupby(r.dt.month).max()

    # Create the keys of the dictionary
    ix = r.dt.strftime("%b").unique()

    # Create the dictionary from the obtained keys and values
    return OrderedDict(zip(ix, m.values))

Пробный прогон

monthlyBeddays(adm = "01-Jan-2018", dis = "05-Mar-2018", fy = 1718)
#OrderedDict([('Jan', 31), ('Feb', 28), ('Mar', 5)])

подробности

Генерация диапазона дат, заменяющая годы на fy:

pd.Series(pd.date_range(adm, dis)).apply(lambda x: x.replace(year=fy))

0    1718-01-01
1    1718-01-02
2    1718-01-03
3    1718-01-04
4    1718-01-05
5    1718-01-06

Последний день каждого месяца можно получить, сгруппировав все дни по месяцам и взяв max:

r.dt.day.groupby(r.dt.month).max()

1    31
2    28
3     5
dtype: int64

Месяцы для возвращенного словаря можно получить из созданного диапазона с помощью:

r.dt.strftime("%b").unique()

#['Jan' 'Feb' 'Mar']

И, наконец, вы можете zip чтобы создать список кортежей и превратить его в упорядоченный словарь, используя collections.OrderedDict:

OrderedDict(zip(ix, m.values))
OrderedDict([('Jan', 31), ('Feb', 28), ('Mar', 5)])
1

Я думаю, что ответы многих людей были до того, как ОП дала важную информацию о том, как fy играет роль функции (правка: многие читали это правку, и теперь их ответы также обновляются). ОП хочет указать количество дней между admission и discharge которое проходит в течение финансового года (с 1819 по 01 апреля 2018 года по 31 марта 2019 года). И, очевидно, все знают, что число дней должно быть разделено на календарный месяц.

from datetime import datetime, timedelta

# Function taken from https://stackoverflow.com/a/13565185/9462009
def lastDateOfThisMonth(any_day):
    next_month = any_day.replace(day=28) + timedelta(days=4)
    return next_month - timedelta(days=next_month.day)

def monthlyBeddays(admission, discharge, fy):
    startFy = datetime.strptime('01-Apr-'+fy[:2], '%d-%b-%y')
    endFy = datetime.strptime('01-Apr-'+fy[2:], '%d-%b-%y')

    admissionDate = datetime.strptime(admission, '%d-%b-%Y')
    dischargeDate = datetime.strptime(discharge, '%d-%b-%Y')


    monthDates = {'Jan':0,'Feb':0,'Mar':0,'Apr':0,'May':0,'Jun':0,'Jul':0,'Aug':0,'Sep':0,'Oct':0,'Nov':0,'Dec':0}

    # if admitted after end of fy or discharged before beginning of fy, zero days counted
    if admissionDate > endFy or dischargeDate < startFy:
        return monthDates

    if admissionDate < startFy:
        # Jump ahead to start at the first day of fy if admission was prior to the beginning of fy
        now = startFy
    else:
        # If admission happened at or after the first day of fy, we begin counting from the admission date
        now = admissionDate

    while True:
        month = datetime.strftime(now,'%b')
        lastDateOfMonth = lastDateOfThisMonth(now)
        if now >= endFy:
            # If now is greater or equal to the first day of the next fy (endFy), we don't care about any of the following dates within the adm/dis date range
            break
        if month == datetime.strftime(dischargeDate,'%b') and datetime.strftime(now, '%Y') == datetime.strftime(dischargeDate, '%Y') and now >= startFy:
            # If we reach the discharge month, we count this month and we're done
            monthDates[month] = (dischargeDate - now).days # not adding one since in your example it seemed like you did not want to count the dischargeDate (Mar:4)
            break
        elif now < startFy:
            # If now is less than the first day of this fy (startFy), we move on from this month to the next month until we reach this fy
            pass
        else:
            # We are within this fy and have not reached the discharge month yet
            monthDates[month] = (lastDateOfMonth - now).days + 1
            month = datetime.strftime(now, '%b')
        now = lastDateOfMonth + timedelta(days=1) # advance to the 1st of the next month

    return monthDates

# Passes all six scenarios

# Scenario #1: admitted before fy, discharged before  fy (didn't stay at all during fy)
print(monthlyBeddays("01-Jan-2018", "30-Mar-2018", '1819')) # {'Jan': 0, 'Feb': 0, 'Mar': 0, 'Apr': 0, 'May': 0, 'Jun': 0, 'Jul': 0, 'Aug': 0, 'Sep': 0, 'Oct': 0, 'Nov': 0, 'Dec': 0}

# Scenario #2: admitted before fy, discharged during fy
print(monthlyBeddays("01-Jan-2018", "30-May-2018", '1819')) # {'Jan': 0, 'Feb': 0, 'Mar': 0, 'Apr': 30, 'May': 29, 'Jun': 0, 'Jul': 0, 'Aug': 0, 'Sep': 0, 'Oct': 0, 'Nov': 0, 'Dec': 0}

# Scenario #3: admitted during fy, discharged during fy
print(monthlyBeddays("15-Apr-2018", "30-May-2018", '1819')) # {'Jan': 0, 'Feb': 0, 'Mar': 0, 'Apr': 16, 'May': 29, 'Jun': 0, 'Jul': 0, 'Aug': 0, 'Sep': 0, 'Oct': 0, 'Nov': 0, 'Dec': 0}

# Scenario #4: admitted during fy, discharged after fy
print(monthlyBeddays("15-Apr-2018", "30-May-2019", '1819')) # {'Jan': 31, 'Feb': 28, 'Mar': 31, 'Apr': 16, 'May': 31, 'Jun': 30, 'Jul': 31, 'Aug': 31, 'Sep': 30, 'Oct': 31, 'Nov': 30, 'Dec': 31}

# Scenario #5: admitted before fy, discharged after fy (stayed the whole fy)
print(monthlyBeddays("15-Mar-2018", "30-May-2019", '1819')) # {'Jan': 31, 'Feb': 28, 'Mar': 31, 'Apr': 30, 'May': 31, 'Jun': 30, 'Jul': 31, 'Aug': 31, 'Sep': 30, 'Oct': 31, 'Nov': 30, 'Dec': 31}

# Scenario #6: admitted after fy, discharged after fy (didn't stay at all during fy)
print(monthlyBeddays("15-Mar-2018", "30-May-2019", '1718')) # {'Jan': 0, 'Feb': 0, 'Mar': 17, 'Apr': 0, 'May': 0, 'Jun': 0, 'Jul': 0, 'Aug': 0, 'Sep': 0, 'Oct': 0, 'Nov': 0, 'Dec': 0}
0

Спасибо за все отличные ответы. Я попытался внедрить некоторые из них обратно в SPSS, но это быстро становится действительно сложным и громоздким, пытаясь передать значения между ними...

Я придумал функцию tidy для анализа переменных даты SPSS в объектах Python datetime:

from datetime import datetime, timedelta

def SPSS_to_Python_date(date):
    spss_start_date = datetime(1582, 10, 14)
    return (spss_start_date + timedelta(seconds = date))

Что касается основной проблемы, то после недолгого размышления мне удалось упростить (я думаю) и улучшить надежность моего первоначального решения.

Define !BedDaysPerMonth (Month_abbr = !Tokens(1) 
    /AdmissionVar = !Default(keydate1_dateformat) !Tokens(1)
    /DischargeVar = !Default(keydate2_dateformat) !Tokens(1)
    /DelayedDischarge = !Default(0) !Tokens(1))

 * Compute the month number from the name abbreviation.
Compute #MonthNum = xdate.Month(Number(!Quote(!Concat(!Month_abbr, "-00")), MOYR6)).

 * Find out which year we need e.g for FY 1718: Apr - Dec = 2018, Jan - Mar = 2018.
Do if (#MonthNum >= 4).
    Compute #Year = !Concat("20", !substr(!Unquote(!Eval(!FY)), 1, 2)).
Else.
    Compute #Year = !Concat("20", !substr(!Unquote(!Eval(!FY)), 3, 2)).
End if.

 * Now we have the year work out the start and end dates for the month.
Compute #StartOfMonth = Date.DMY(1, #MonthNum, #Year).
Compute #EndOfMonth = Date.DMY(1, #MonthNum + 1, #Year) - time.days(1).

 * Set the names of the variable for this month e.g. April_beddays.
 * And then create the variable.
!Let !BedDays = !Concat(!Month_abbr, "_beddays").
Numeric !BedDays (F2.0).

 * Go through all possibilities to decide how many days to be allocated.
Do if !AdmissionVar LE #StartOfMonth.
    Do if !DischargeVar GT #EndOfMonth.
        * They were in hospital throughout this month.
        * This will be the maximum number of days in the month.
        Compute !BedDays = DateDiff(#EndOfMonth, #StartOfMonth, "days") + 1.
    Else if !DischargeVar LE #StartOfMonth.
        * The whole record occurred before the month began.
        Compute !BedDays = 0.
    Else.
        * They were discharged at some point in the month.
        Compute !BedDays = DateDiff(!DischargeVar, #StartOfMonth, "days").
    End If.
 * If we're here they were admitted during the month.
Else if !AdmissionVar LE #EndOfMonth.
    Do if !DischargeVar GT #EndOfMonth.
        Compute !BedDays = DateDiff(#EndOfMonth, !AdmissionVar, "days") + 1.
    Else.
        * Admitted and discharged within this month.
        Compute !BedDays = DateDiff(!DischargeVar, !AdmissionVar, "days").
    End If.
Else.
    * They were admitted in a future month.
    Compute !BedDays = 0.
End If.

 * If we are looking at Delayed Discharge records, we should count the last day and not the first.
 * We achieve this by taking a day from the first month and adding it to the last.
!If (!DelayedDischarge = 1) !Then
    Do if xdate.Month(!AdmissionVar) = xdate.Month(date.MOYR(#MonthNum, #Year))
        and xdate.Year(!AdmissionVar) =  #Year.
        Compute !BedDays = !BedDays - 1.
    End if.

    Do if xdate.Month(!DischargeVar) = xdate.Month(date.MOYR(#MonthNum, #Year))
        and xdate.Year(!DischargeVar) =  #Year.
        Compute !BedDays = !BedDays + 1.
    End if.
!ifEnd.

 * Tidy up the variable.
Variable Width !Beddays (5).
Variable Level !Beddays (Scale).

!EndDefine.

Затем он может быть (необязательно) запущен с использованием следующего бита Python.

from calendar import month_name
import spss

#Loop through the months by number in FY order
for month in (4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3):
   #To show what is happening print some stuff to the screen
   print(month, month_name[month])

   #Set up the syntax
   syntax = "!BedDaysPerMonth Month_abbr = " + month_name[month][:3]

   #print the syntax to the screen
   print(syntax)

   #run the syntax
   spss.Submit(syntax)
0

Используя только основные библиотечные модули, работая месяцами, а не днями:

from calendar import monthrange, month_name
from datetime import datetime
from dateutil.relativedelta import relativedelta

def days_by_month(admission, discharge):
    #Returns a dictionary with months and count of days that fall into them

    def fin_year_check(start_month, x):

        if start_month >= 4:
            return 4 <= x <= 15
        if start_month < 4:
            return 1 <= x < 4

    def modulo(x):
        #modulo modified

        if x%12 == 0:
            return 12
        return x%12

    date_format = "%Y-%m-%d"

    admission_date = datetime.strptime(admission, date_format)
    discharge_date = datetime.strptime(discharge, date_format)

    year = admission_date.year
    start_day = admission_date.day
    start_month = admission_date.month
    end_day = discharge_date.day

    num_of_months = (relativedelta(discharge_date, admission_date).years * 12
                     + relativedelta(discharge_date, admission_date).months)
    days_in_first_month = monthrange(admission_date.year,admission_date.month)[1]-start_day
    days_in_last_month = end_day

    months = [month_name[modulo(x)] for x in 
                 range(admission_date.month, admission_date.month + num_of_months + 1)
                 if fin_year_check(start_month, x)]

    full_days = []

    for x in range(admission_date.month, admission_date.month + num_of_months):
        if fin_year_check(start_month, x):
            fin_year = year + 1 if x > 12 else year
            full_days.append(monthrange(fin_year, modulo(x))[1])

    all_days = [days_in_first_month, *full_days[1:], days_in_last_month]

    result = dict(zip(months, all_days))
    return result

Некоторые примеры тестов:

days_by_month("2018-01-01", "2018-03-30")
#>>>{'January': 30, 'February': 28, 'March': 30}

days_by_month("2018-01-01", "2018-05-30")
#>>>{'January': 30, 'February': 28, 'March': 31}

days_by_month("2018-04-15", "2018-05-30")
#>>>{'April': 15, 'May': 30}
0

Вот мое предложение решения. Из того, что я понимаю, вы хотите количество дней в каждом месяце между двумя данными датами. Я не отформатировал месяцы (я оставил их как числа), но вам должно быть достаточно легко это сделать.

from datetime import date
from calendar import monthrange
from dateutil.relativedelta import *

#start and end dates
d0 = date(2008, 8, 18)
d1 = date(2008, 12, 26)
delta = d1 - d0
delta_days = delta.days #number of days between the two dates

#we create a copy of the start date so we can use it to iterate (so as to not to lose the initial date)
curr_d = d0
while(1):
    #we iterate over each month until we have no days left

    #if theere are more days in delta_days than in the month
    #the number of days in the current month is the maximum number of days in that month
    if delta_days > monthrange(curr_d.year, curr_d.month)[1]:
        number_of_days_in_curr_month = monthrange(curr_d.year, curr_d.month)[1]
        delta_days -= monthrange(curr_d.year, curr_d.month)[1]

    #the delta_days is smaller than the maximum days in the current month
    #the number of days in the current month is thus == to delta_days
    #we exit the while loop here
    else:
        number_of_days_in_curr_month = delta_days
        print('month number: ' + str(curr_d.month) + ', year: ' + str(curr_d.year) + ', days: ' + str(number_of_days_in_curr_month) )
        break
    print('month number: ' + str(curr_d.month) + ', year: ' + str(curr_d.year) + ', days: ' + str(number_of_days_in_curr_month) )

    #we increment the current month
    curr_d = curr_d + relativedelta(months=+1)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню