[{"data":1,"prerenderedAt":2021},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python":1987,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python":1996,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python":2001,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python":2002},{"id":4,"title":5,"body":6,"dateModified":1955,"datePublished":1955,"description":1956,"extension":1957,"faq":1958,"keyword":1967,"kind":1968,"meta":1969,"navigation":116,"path":1977,"seo":1978,"slug":1981,"stem":1982,"topics":1983,"type":1985,"__hash__":1986},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python\u002Findex.md","XIRR and XNPV Equivalents in Python",{"type":7,"value":8,"toc":1942},"minimark",[9,35,40,71,77,81,88,344,352,510,514,521,719,725,729,735,894,905,979,983,986,1180,1191,1195,1198,1514,1521,1643,1647,1650,1802,1805,1809,1854,1858,1877,1881,1887,1893,1899,1905,1909,1938],[10,11,12,13,17,18,21,22,17,25,28,29,34],"p",{},"Real cash flows do not arrive on neat annual dates. An investment is funded in January, pays a dividend in June, another the following January, and is sold in March two years later. Excel's ",[14,15,16],"code",{},"NPV"," and ",[14,19,20],{},"IRR"," assume equally spaced periods; ",[14,23,24],{},"XNPV",[14,26,27],{},"XIRR"," take the actual dates. When a report that used to live in a spreadsheet moves to Python, these two functions are often the ones people worry about, because finance colleagues will compare the numbers to the decimal. The good news is that both are short formulas — XNPV is a sum, XIRR is the rate that makes that sum zero — and a few lines of Python reproduce Excel exactly. This page belongs to ",[30,31,33],"a",{"href":32},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",".",[36,37,39],"h2",{"id":38},"prerequisites","Prerequisites",[41,42,47],"pre",{"className":43,"code":44,"language":45,"meta":46,"style":46},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl scipy\n","bash","",[14,48,49],{"__ignoreMap":46},[50,51,54,58,62,65,68],"span",{"class":52,"line":53},"line",1,[50,55,57],{"class":56},"sMTad","pip",[50,59,61],{"class":60},"srMev"," install",[50,63,64],{"class":60}," pandas",[50,66,67],{"class":60}," openpyxl",[50,69,70],{"class":60}," scipy\n",[10,72,73,76],{},[14,74,75],{},"cashflows.xlsx"," lists cash flows for three investments: money paid in is negative, money received is positive. Fund A has the five irregular flows from the introduction; Fund B has two contributions; Fund C never earns its money back.",[36,78,80],{"id":79},"step-1-xnpv-is-a-sum-with-actual365-discounting","Step 1 — XNPV is a sum with actual\u002F365 discounting",[10,82,83,84,87],{},"Excel's documented formula discounts each cash flow by ",[14,85,86],{},"(1 + rate)"," raised to the number of days since the first date, divided by 365:",[41,89,93],{"className":90,"code":91,"language":92,"meta":46,"style":46},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nflows = pd.read_excel(\"cashflows.xlsx\", parse_dates=[\"Date\"])\nfund_a = flows[flows[\"Investment\"] == \"Fund A\"]\n\ndef xnpv(rate, values, dates):\n    dates = pd.to_datetime(pd.Series(dates)).reset_index(drop=True)\n    years = (dates - dates.iloc[0]).dt.days \u002F 365.0\n    return float((pd.Series(values).reset_index(drop=True) \u002F (1 + rate) ** years).sum())\n\nprint(f\"XNPV at 8%: {xnpv(0.08, fund_a['CashFlow'], fund_a['Date']):,.2f}\")\n","python",[14,94,95,111,118,151,177,182,195,218,247,288,293],{"__ignoreMap":46},[50,96,97,101,105,108],{"class":52,"line":53},[50,98,100],{"class":99},"s-kum","import",[50,102,104],{"class":103},"skGVy"," pandas ",[50,106,107],{"class":99},"as",[50,109,110],{"class":103}," pd\n",[50,112,114],{"class":52,"line":113},2,[50,115,117],{"emptyLinePlaceholder":116},true,"\n",[50,119,121,124,127,130,133,136,140,142,145,148],{"class":52,"line":120},3,[50,122,123],{"class":103},"flows ",[50,125,126],{"class":99},"=",[50,128,129],{"class":103}," pd.read_excel(",[50,131,132],{"class":60},"\"cashflows.xlsx\"",[50,134,135],{"class":103},", ",[50,137,139],{"class":138},"sa561","parse_dates",[50,141,126],{"class":99},[50,143,144],{"class":103},"[",[50,146,147],{"class":60},"\"Date\"",[50,149,150],{"class":103},"])\n",[50,152,154,157,159,162,165,168,171,174],{"class":52,"line":153},4,[50,155,156],{"class":103},"fund_a ",[50,158,126],{"class":99},[50,160,161],{"class":103}," flows[flows[",[50,163,164],{"class":60},"\"Investment\"",[50,166,167],{"class":103},"] ",[50,169,170],{"class":99},"==",[50,172,173],{"class":60}," \"Fund A\"",[50,175,176],{"class":103},"]\n",[50,178,180],{"class":52,"line":179},5,[50,181,117],{"emptyLinePlaceholder":116},[50,183,185,188,192],{"class":52,"line":184},6,[50,186,187],{"class":99},"def",[50,189,191],{"class":190},"s_Opv"," xnpv",[50,193,194],{"class":103},"(rate, values, dates):\n",[50,196,198,201,203,206,209,211,215],{"class":52,"line":197},7,[50,199,200],{"class":103},"    dates ",[50,202,126],{"class":99},[50,204,205],{"class":103}," pd.to_datetime(pd.Series(dates)).reset_index(",[50,207,208],{"class":138},"drop",[50,210,126],{"class":99},[50,212,214],{"class":213},"sP0c6","True",[50,216,217],{"class":103},")\n",[50,219,221,224,226,229,232,235,238,241,244],{"class":52,"line":220},8,[50,222,223],{"class":103},"    years ",[50,225,126],{"class":99},[50,227,228],{"class":103}," (dates ",[50,230,231],{"class":99},"-",[50,233,234],{"class":103}," dates.iloc[",[50,236,237],{"class":213},"0",[50,239,240],{"class":103},"]).dt.days ",[50,242,243],{"class":99},"\u002F",[50,245,246],{"class":213}," 365.0\n",[50,248,250,253,256,259,261,263,265,268,270,273,276,279,282,285],{"class":52,"line":249},9,[50,251,252],{"class":99},"    return",[50,254,255],{"class":213}," float",[50,257,258],{"class":103},"((pd.Series(values).reset_index(",[50,260,208],{"class":138},[50,262,126],{"class":99},[50,264,214],{"class":213},[50,266,267],{"class":103},") ",[50,269,243],{"class":99},[50,271,272],{"class":103}," (",[50,274,275],{"class":213},"1",[50,277,278],{"class":99}," +",[50,280,281],{"class":103}," rate) ",[50,283,284],{"class":99},"**",[50,286,287],{"class":103}," years).sum())\n",[50,289,291],{"class":52,"line":290},10,[50,292,117],{"emptyLinePlaceholder":116},[50,294,296,299,302,305,308,312,315,318,321,324,327,330,333,336,339,342],{"class":52,"line":295},11,[50,297,298],{"class":213},"print",[50,300,301],{"class":103},"(",[50,303,304],{"class":99},"f",[50,306,307],{"class":60},"\"XNPV at 8%: ",[50,309,311],{"class":310},"sSjpA","{",[50,313,314],{"class":103},"xnpv(",[50,316,317],{"class":213},"0.08",[50,319,320],{"class":103},", fund_a[",[50,322,323],{"class":60},"'CashFlow'",[50,325,326],{"class":103},"], fund_a[",[50,328,329],{"class":60},"'Date'",[50,331,332],{"class":103},"])",[50,334,335],{"class":99},":,.2f",[50,337,338],{"class":310},"}",[50,340,341],{"class":60},"\"",[50,343,217],{"class":103},[10,345,346,347,351],{},"Two details match Excel's convention and are easy to get wrong: the divisor is always 365 (leap years do not change it), and the base date is the ",[348,349,350],"em",{},"first date in the list",", not the earliest one. The result, 8,241.05, is the value of Fund A's flows as of 15 January 2026, discounted at 8% a year.",[353,354,362,363,362,367,362,371,362,378,362,385,362,395,362,406,362,413,362,418,362,422,362,426,362,432,362,439,362,445,362,449,362,451,362,453,362,456,362,461,362,465,362,468,362,470,362,472,362,475,362,480,362,484,362,487,362,489,362,491,362,494,362,499,362,503,362,506,362,508],"svg",{"viewBox":355,"role":356,"ariaLabelledBy":357,"xmlns":360,"style":361},"4 0 752 279","img",[358,359],"xi-flows-t","xi-flows-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[364,365,366],"title",{"id":358},"Fund A's cash flows on their real dates",[368,369,370],"desc",{"id":359},"A timeline of Fund A. An outflow of 100,000 on 15 Jan 2026, inflows of 12,000 on 30 Jun 2026, 30,000 on 10 Jan 2027, 35,000 on 1 Sep 2027 and 45,000 on 31 Mar 2028. The gaps between them are uneven.",[372,373],"rect",{"x":374,"y":237,"width":375,"height":376,"fill":377},"4","752","279","#ffffff",[52,379],{"x1":380,"y1":381,"x2":382,"y2":381,"stroke":383,"style":384},"76.0","139.5","684.0","var(--muted,#5b6780)","stroke-width:2.5px",[372,386],{"x":387,"y":388,"width":389,"height":390,"rx":391,"fill":392,"stroke":393,"style":394},"20.0","16.0","136.0","91.5","12","#fce9e9","var(--danger,#dc2626)","stroke-width:2px",[396,397,399],"g",{"transform":398},"translate(76.0,29.0) scale(1.000)",[400,401],"path",{"d":402,"fill":403,"stroke":404,"style":405},"M3 11l18-8-8 18-2-8z","none","var(--accent-ink,#be185d)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[407,408,412],"text",{"x":409,"y":410,"style":411},"88.0","74.5","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","15 Jan 2026",[407,414,417],{"x":409,"y":415,"style":416},"93.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","−100,000",[52,419],{"x1":409,"y1":420,"x2":409,"y2":381,"stroke":383,"style":421},"107.5","stroke-width:1.5px",[423,424],"circle",{"cx":409,"cy":381,"r":425,"fill":393,"stroke":377,"style":394},"7.0",[372,427],{"x":428,"y":429,"width":389,"height":390,"rx":391,"fill":430,"stroke":431,"style":394},"166.0","171.5","#d9f4f1","var(--teal,#0f9488)",[396,433,435],{"transform":434},"translate(222.0,184.5) scale(1.000)",[400,436],{"d":437,"fill":403,"stroke":438,"style":405},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[407,440,444],{"x":441,"y":442,"style":443},"234.0","230.0","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","30 Jun 2026",[407,446,448],{"x":441,"y":447,"style":416},"248.5","+12,000 (0.46 yr)",[52,450],{"x1":441,"y1":381,"x2":441,"y2":429,"stroke":383,"style":421},[423,452],{"cx":441,"cy":381,"r":425,"fill":431,"stroke":377,"style":394},[372,454],{"x":455,"y":388,"width":389,"height":390,"rx":391,"fill":430,"stroke":431,"style":394},"312.0",[396,457,459],{"transform":458},"translate(368.0,29.0) scale(1.000)",[400,460],{"d":437,"fill":403,"stroke":438,"style":405},[407,462,464],{"x":463,"y":410,"style":443},"380.0","10 Jan 2027",[407,466,467],{"x":463,"y":415,"style":416},"+30,000 (0.99 yr)",[52,469],{"x1":463,"y1":420,"x2":463,"y2":381,"stroke":383,"style":421},[423,471],{"cx":463,"cy":381,"r":425,"fill":431,"stroke":377,"style":394},[372,473],{"x":474,"y":429,"width":389,"height":390,"rx":391,"fill":430,"stroke":431,"style":394},"458.0",[396,476,478],{"transform":477},"translate(514.0,184.5) scale(1.000)",[400,479],{"d":437,"fill":403,"stroke":438,"style":405},[407,481,483],{"x":482,"y":442,"style":443},"526.0","1 Sep 2027",[407,485,486],{"x":482,"y":447,"style":416},"+35,000 (1.63 yr)",[52,488],{"x1":482,"y1":381,"x2":482,"y2":429,"stroke":383,"style":421},[423,490],{"cx":482,"cy":381,"r":425,"fill":431,"stroke":377,"style":394},[372,492],{"x":493,"y":388,"width":389,"height":390,"rx":391,"fill":430,"stroke":431,"style":394},"604.0",[396,495,497],{"transform":496},"translate(660.0,29.0) scale(1.000)",[400,498],{"d":437,"fill":403,"stroke":438,"style":405},[407,500,502],{"x":501,"y":410,"style":443},"672.0","31 Mar 2028",[407,504,505],{"x":501,"y":415,"style":416},"+45,000 (2.21 yr)",[52,507],{"x1":501,"y1":420,"x2":501,"y2":381,"stroke":383,"style":421},[423,509],{"cx":501,"cy":381,"r":425,"fill":431,"stroke":377,"style":394},[36,511,513],{"id":512},"step-2-xirr-is-the-rate-where-xnpv-is-zero","Step 2 — XIRR is the rate where XNPV is zero",[10,515,516,517,520],{},"There is no closed formula for XIRR; Excel searches for it numerically. A bracketing root-finder such as SciPy's ",[14,518,519],{},"brentq"," is robust and fast:",[41,522,524],{"className":90,"code":523,"language":92,"meta":46,"style":46},"from scipy.optimize import brentq\n\ndef xirr(values, dates, low=-0.9999, high=10.0):\n    f = lambda r: xnpv(r, values, dates)\n    if f(low) * f(high) > 0:\n        raise ValueError(\"XNPV does not change sign between the bounds; no XIRR found\")\n    return brentq(f, low, high, xtol=1e-12)\n\nrate = xirr(fund_a[\"CashFlow\"], fund_a[\"Date\"])\nprint(f\"XIRR: {rate:.6%}\")\nprint(f\"check: XNPV at that rate = {xnpv(rate, fund_a['CashFlow'], fund_a['Date']):.6f}\")\n",[14,525,526,539,543,570,583,606,621,638,642,661,686],{"__ignoreMap":46},[50,527,528,531,534,536],{"class":52,"line":53},[50,529,530],{"class":99},"from",[50,532,533],{"class":103}," scipy.optimize ",[50,535,100],{"class":99},[50,537,538],{"class":103}," brentq\n",[50,540,541],{"class":52,"line":113},[50,542,117],{"emptyLinePlaceholder":116},[50,544,545,547,550,553,556,559,562,564,567],{"class":52,"line":120},[50,546,187],{"class":99},[50,548,549],{"class":190}," xirr",[50,551,552],{"class":103},"(values, dates, low",[50,554,555],{"class":99},"=-",[50,557,558],{"class":213},"0.9999",[50,560,561],{"class":103},", high",[50,563,126],{"class":99},[50,565,566],{"class":213},"10.0",[50,568,569],{"class":103},"):\n",[50,571,572,575,577,580],{"class":52,"line":153},[50,573,574],{"class":103},"    f ",[50,576,126],{"class":99},[50,578,579],{"class":99}," lambda",[50,581,582],{"class":103}," r: xnpv(r, values, dates)\n",[50,584,585,588,591,594,597,600,603],{"class":52,"line":179},[50,586,587],{"class":99},"    if",[50,589,590],{"class":103}," f(low) ",[50,592,593],{"class":99},"*",[50,595,596],{"class":103}," f(high) ",[50,598,599],{"class":99},">",[50,601,602],{"class":213}," 0",[50,604,605],{"class":103},":\n",[50,607,608,611,614,616,619],{"class":52,"line":184},[50,609,610],{"class":99},"        raise",[50,612,613],{"class":213}," ValueError",[50,615,301],{"class":103},[50,617,618],{"class":60},"\"XNPV does not change sign between the bounds; no XIRR found\"",[50,620,217],{"class":103},[50,622,623,625,628,631,633,636],{"class":52,"line":197},[50,624,252],{"class":99},[50,626,627],{"class":103}," brentq(f, low, high, ",[50,629,630],{"class":138},"xtol",[50,632,126],{"class":99},[50,634,635],{"class":213},"1e-12",[50,637,217],{"class":103},[50,639,640],{"class":52,"line":220},[50,641,117],{"emptyLinePlaceholder":116},[50,643,644,647,649,652,655,657,659],{"class":52,"line":249},[50,645,646],{"class":103},"rate ",[50,648,126],{"class":99},[50,650,651],{"class":103}," xirr(fund_a[",[50,653,654],{"class":60},"\"CashFlow\"",[50,656,326],{"class":103},[50,658,147],{"class":60},[50,660,150],{"class":103},[50,662,663,665,667,669,672,674,677,680,682,684],{"class":52,"line":290},[50,664,298],{"class":213},[50,666,301],{"class":103},[50,668,304],{"class":99},[50,670,671],{"class":60},"\"XIRR: ",[50,673,311],{"class":310},[50,675,676],{"class":103},"rate",[50,678,679],{"class":99},":.6%",[50,681,338],{"class":310},[50,683,341],{"class":60},[50,685,217],{"class":103},[50,687,688,690,692,694,697,699,702,704,706,708,710,713,715,717],{"class":52,"line":295},[50,689,298],{"class":213},[50,691,301],{"class":103},[50,693,304],{"class":99},[50,695,696],{"class":60},"\"check: XNPV at that rate = ",[50,698,311],{"class":310},[50,700,701],{"class":103},"xnpv(rate, fund_a[",[50,703,323],{"class":60},[50,705,326],{"class":103},[50,707,329],{"class":60},[50,709,332],{"class":103},[50,711,712],{"class":99},":.6f",[50,714,338],{"class":310},[50,716,341],{"class":60},[50,718,217],{"class":103},[10,720,721,722,724],{},"Fund A's XIRR is 13.7315%, and plugging it back into XNPV gives zero — the definition of the internal rate of return. ",[14,723,519],{}," needs two rates where XNPV has opposite signs; the default bracket from −99.99% to +1000% covers anything a realistic investment produces, and the explicit check turns \"no solution\" into a clear message instead of an obscure SciPy error.",[36,726,728],{"id":727},"step-3-why-npv-and-irr-give-a-different-answer","Step 3 — Why NPV and IRR give a different answer",[10,730,731,732,734],{},"Excel's ",[14,733,20],{}," treats the flows as one period apart, whatever their dates:",[41,736,738],{"className":90,"code":737,"language":92,"meta":46,"style":46},"import numpy as np\n\ndef irr_equal_periods(values):\n    f = lambda r: sum(v \u002F (1 + r) ** i for i, v in enumerate(values))\n    return brentq(f, -0.9999, 10.0)\n\nprint(f\"IRR (equal periods): {irr_equal_periods(list(fund_a['CashFlow'])):.4%}\")\nprint(f\"XIRR (actual dates): {rate:.4%}\")\n",[14,739,740,752,756,766,814,831,835,871],{"__ignoreMap":46},[50,741,742,744,747,749],{"class":52,"line":53},[50,743,100],{"class":99},[50,745,746],{"class":103}," numpy ",[50,748,107],{"class":99},[50,750,751],{"class":103}," np\n",[50,753,754],{"class":52,"line":113},[50,755,117],{"emptyLinePlaceholder":116},[50,757,758,760,763],{"class":52,"line":120},[50,759,187],{"class":99},[50,761,762],{"class":190}," irr_equal_periods",[50,764,765],{"class":103},"(values):\n",[50,767,768,770,772,774,777,780,783,785,787,789,791,794,796,799,802,805,808,811],{"class":52,"line":153},[50,769,574],{"class":103},[50,771,126],{"class":99},[50,773,579],{"class":99},[50,775,776],{"class":103}," r: ",[50,778,779],{"class":213},"sum",[50,781,782],{"class":103},"(v ",[50,784,243],{"class":99},[50,786,272],{"class":103},[50,788,275],{"class":213},[50,790,278],{"class":99},[50,792,793],{"class":103}," r) ",[50,795,284],{"class":99},[50,797,798],{"class":103}," i ",[50,800,801],{"class":99},"for",[50,803,804],{"class":103}," i, v ",[50,806,807],{"class":99},"in",[50,809,810],{"class":213}," enumerate",[50,812,813],{"class":103},"(values))\n",[50,815,816,818,821,823,825,827,829],{"class":52,"line":179},[50,817,252],{"class":99},[50,819,820],{"class":103}," brentq(f, ",[50,822,231],{"class":99},[50,824,558],{"class":213},[50,826,135],{"class":103},[50,828,566],{"class":213},[50,830,217],{"class":103},[50,832,833],{"class":52,"line":184},[50,834,117],{"emptyLinePlaceholder":116},[50,836,837,839,841,843,846,848,851,854,857,859,862,865,867,869],{"class":52,"line":197},[50,838,298],{"class":213},[50,840,301],{"class":103},[50,842,304],{"class":99},[50,844,845],{"class":60},"\"IRR (equal periods): ",[50,847,311],{"class":310},[50,849,850],{"class":103},"irr_equal_periods(",[50,852,853],{"class":213},"list",[50,855,856],{"class":103},"(fund_a[",[50,858,323],{"class":60},[50,860,861],{"class":103},"]))",[50,863,864],{"class":99},":.4%",[50,866,338],{"class":310},[50,868,341],{"class":60},[50,870,217],{"class":103},[50,872,873,875,877,879,882,884,886,888,890,892],{"class":52,"line":220},[50,874,298],{"class":213},[50,876,301],{"class":103},[50,878,304],{"class":99},[50,880,881],{"class":60},"\"XIRR (actual dates): ",[50,883,311],{"class":310},[50,885,676],{"class":103},[50,887,864],{"class":99},[50,889,338],{"class":310},[50,891,341],{"class":60},[50,893,217],{"class":103},[10,895,896,898,899,901,902,904],{},[14,897,20],{}," says 7.12%, ",[14,900,27],{}," says 13.73%. Fund A's five flows span just over two years, but ",[14,903,20],{}," spreads them over four periods, as if each were a year apart — so the money appears to work for twice as long and the return halves. For any schedule with uneven gaps, only the date-aware functions answer the question people are asking.",[353,906,362,912,362,915,362,918,362,922,362,930,362,936,362,940,362,945,362,948,362,951,362,954,362,959,362,962,362,966,362,969,362,974],{"viewBox":907,"role":356,"ariaLabelledBy":908,"xmlns":360,"style":911},"4 0 669 166",[909,910],"xi-compare-t","xi-compare-d","width:100%;max-width:669px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[364,913,914],{"id":909},"Fund A's return measured three ways",[368,916,917],{"id":910},"Fund A's IRR treating the flows as annual periods is 7.12 percent. XIRR using the actual dates is 13.73 percent. The XNPV at 8 percent is positive, consistent with a return above 8 percent.",[372,919],{"x":374,"y":237,"width":920,"height":921,"fill":377},"669","166",[372,923],{"x":387,"y":388,"width":924,"height":925,"rx":926,"fill":927,"stroke":928,"style":929},"150.1","28.0","7","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[407,931,935],{"x":932,"y":933,"style":934},"95.1","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","IRR (equal periods)",[372,937],{"x":938,"y":388,"width":939,"height":925,"rx":926,"fill":392,"stroke":393,"style":421},"180.1","219.1",[407,941,944],{"x":942,"y":933,"style":943},"407.2","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","7.12%",[372,946],{"x":387,"y":947,"width":924,"height":925,"rx":926,"fill":927,"stroke":928,"style":929},"56.0",[407,949,950],{"x":932,"y":410,"style":934},"XIRR (actual dates)",[372,952],{"x":938,"y":947,"width":953,"height":925,"rx":926,"fill":430,"stroke":431,"style":421},"422.6",[407,955,958],{"x":956,"y":410,"style":957},"610.7","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","13.73%",[372,960],{"x":387,"y":961,"width":924,"height":925,"rx":926,"fill":927,"stroke":928,"style":929},"96.0",[407,963,965],{"x":932,"y":964,"style":934},"114.5","8% hurdle rate",[372,967],{"x":938,"y":961,"width":968,"height":925,"rx":926,"fill":927,"stroke":928,"style":421},"246.2",[407,970,973],{"x":971,"y":964,"style":972},"434.3","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start","8% → XNPV +8,241",[407,975,978],{"x":463,"y":976,"style":977},"146.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","The flows span 2.2 years, but IRR treats them as four annual periods.",[36,980,982],{"id":981},"step-4-xirr-for-every-investment","Step 4 — XIRR for every investment",[10,984,985],{},"With a long table of flows, compute XIRR per group and keep failures visible rather than letting one bad group stop the run:",[41,987,989],{"className":90,"code":988,"language":92,"meta":46,"style":46},"def safe_xirr(group):\n    try:\n        return xirr(group[\"CashFlow\"], group[\"Date\"])\n    except ValueError:\n        return np.nan\n\nsummary = flows.groupby(\"Investment\").apply(\n    lambda g: pd.Series({\n        \"Invested\": -g.loc[g[\"CashFlow\"] \u003C 0, \"CashFlow\"].sum(),\n        \"Returned\": g.loc[g[\"CashFlow\"] > 0, \"CashFlow\"].sum(),\n        \"XNPV @8%\": xnpv(0.08, g[\"CashFlow\"], g[\"Date\"]),\n        \"XIRR\": safe_xirr(g),\n    }), include_groups=False)\nprint(summary.round(4))\n",[14,990,991,1001,1008,1025,1034,1041,1045,1060,1068,1097,1119,1142,1151,1167],{"__ignoreMap":46},[50,992,993,995,998],{"class":52,"line":53},[50,994,187],{"class":99},[50,996,997],{"class":190}," safe_xirr",[50,999,1000],{"class":103},"(group):\n",[50,1002,1003,1006],{"class":52,"line":113},[50,1004,1005],{"class":99},"    try",[50,1007,605],{"class":103},[50,1009,1010,1013,1016,1018,1021,1023],{"class":52,"line":120},[50,1011,1012],{"class":99},"        return",[50,1014,1015],{"class":103}," xirr(group[",[50,1017,654],{"class":60},[50,1019,1020],{"class":103},"], group[",[50,1022,147],{"class":60},[50,1024,150],{"class":103},[50,1026,1027,1030,1032],{"class":52,"line":153},[50,1028,1029],{"class":99},"    except",[50,1031,613],{"class":213},[50,1033,605],{"class":103},[50,1035,1036,1038],{"class":52,"line":179},[50,1037,1012],{"class":99},[50,1039,1040],{"class":103}," np.nan\n",[50,1042,1043],{"class":52,"line":184},[50,1044,117],{"emptyLinePlaceholder":116},[50,1046,1047,1050,1052,1055,1057],{"class":52,"line":197},[50,1048,1049],{"class":103},"summary ",[50,1051,126],{"class":99},[50,1053,1054],{"class":103}," flows.groupby(",[50,1056,164],{"class":60},[50,1058,1059],{"class":103},").apply(\n",[50,1061,1062,1065],{"class":52,"line":220},[50,1063,1064],{"class":99},"    lambda",[50,1066,1067],{"class":103}," g: pd.Series({\n",[50,1069,1070,1073,1076,1078,1081,1083,1085,1088,1090,1092,1094],{"class":52,"line":249},[50,1071,1072],{"class":60},"        \"Invested\"",[50,1074,1075],{"class":103},": ",[50,1077,231],{"class":99},[50,1079,1080],{"class":103},"g.loc[g[",[50,1082,654],{"class":60},[50,1084,167],{"class":103},[50,1086,1087],{"class":99},"\u003C",[50,1089,602],{"class":213},[50,1091,135],{"class":103},[50,1093,654],{"class":60},[50,1095,1096],{"class":103},"].sum(),\n",[50,1098,1099,1102,1105,1107,1109,1111,1113,1115,1117],{"class":52,"line":290},[50,1100,1101],{"class":60},"        \"Returned\"",[50,1103,1104],{"class":103},": g.loc[g[",[50,1106,654],{"class":60},[50,1108,167],{"class":103},[50,1110,599],{"class":99},[50,1112,602],{"class":213},[50,1114,135],{"class":103},[50,1116,654],{"class":60},[50,1118,1096],{"class":103},[50,1120,1121,1124,1127,1129,1132,1134,1137,1139],{"class":52,"line":295},[50,1122,1123],{"class":60},"        \"XNPV @8%\"",[50,1125,1126],{"class":103},": xnpv(",[50,1128,317],{"class":213},[50,1130,1131],{"class":103},", g[",[50,1133,654],{"class":60},[50,1135,1136],{"class":103},"], g[",[50,1138,147],{"class":60},[50,1140,1141],{"class":103},"]),\n",[50,1143,1145,1148],{"class":52,"line":1144},12,[50,1146,1147],{"class":60},"        \"XIRR\"",[50,1149,1150],{"class":103},": safe_xirr(g),\n",[50,1152,1154,1157,1160,1162,1165],{"class":52,"line":1153},13,[50,1155,1156],{"class":103},"    }), ",[50,1158,1159],{"class":138},"include_groups",[50,1161,126],{"class":99},[50,1163,1164],{"class":213},"False",[50,1166,217],{"class":103},[50,1168,1170,1172,1175,1177],{"class":52,"line":1169},14,[50,1171,298],{"class":213},[50,1173,1174],{"class":103},"(summary.round(",[50,1176,374],{"class":213},[50,1178,1179],{"class":103},"))\n",[10,1181,1182,1183,1186,1187,1190],{},"Fund B, with two contributions and two distributions, comes out at a sensible positive return. Fund C returned 22,000 of 40,000 and its XIRR is negative — still a valid answer, found because the bracket starts near −100%. A group whose flows are all negative or all positive has no XIRR at all; ",[14,1184,1185],{},"safe_xirr"," returns ",[14,1188,1189],{},"NaN"," for it, which shows up in the report as an empty cell to investigate.",[36,1192,1194],{"id":1193},"step-5-check-against-the-spreadsheet","Step 5 — Check against the spreadsheet",[10,1196,1197],{},"The strongest evidence for colleagues is the spreadsheet's own answer. Write Excel formulas next to the data, let LibreOffice calculate them, and compare:",[41,1199,1201],{"className":90,"code":1200,"language":92,"meta":46,"style":46},"import subprocess\nfrom openpyxl import Workbook, load_workbook\n\nwb = Workbook()\nws = wb.active\nfor d, v in zip(fund_a[\"Date\"], fund_a[\"CashFlow\"]):\n    ws.append([d.to_pydatetime(), int(v)])\nws[\"D1\"], ws[\"D2\"], ws[\"D3\"] = \"=XNPV(0.08,B1:B5,A1:A5)\", \"=XIRR(B1:B5,A1:A5)\", \"=IRR(B1:B5)\"\nwb.save(\"check.xlsx\")\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"xlsx\", \"--outdir\", \"recalc\", \"check.xlsx\"],\n               check=True, capture_output=True)\nsheet = load_workbook(\"recalc\u002Fcheck.xlsx\", data_only=True).active\nprint(\"sheet  XNPV, XIRR, IRR:\", [round(sheet[c].value, 8) for c in (\"D1\", \"D2\", \"D3\")])\nprint(\"python XNPV, XIRR, IRR:\", [round(x, 8) for x in (xnpv(0.08, fund_a[\"CashFlow\"], fund_a[\"Date\"]), rate,\n                                                        irr_equal_periods(list(fund_a[\"CashFlow\"])))])\n",[14,1202,1203,1210,1222,1226,1236,1246,1269,1280,1316,1326,1366,1386,1411,1456,1499],{"__ignoreMap":46},[50,1204,1205,1207],{"class":52,"line":53},[50,1206,100],{"class":99},[50,1208,1209],{"class":103}," subprocess\n",[50,1211,1212,1214,1217,1219],{"class":52,"line":113},[50,1213,530],{"class":99},[50,1215,1216],{"class":103}," openpyxl ",[50,1218,100],{"class":99},[50,1220,1221],{"class":103}," Workbook, load_workbook\n",[50,1223,1224],{"class":52,"line":120},[50,1225,117],{"emptyLinePlaceholder":116},[50,1227,1228,1231,1233],{"class":52,"line":153},[50,1229,1230],{"class":103},"wb ",[50,1232,126],{"class":99},[50,1234,1235],{"class":103}," Workbook()\n",[50,1237,1238,1241,1243],{"class":52,"line":179},[50,1239,1240],{"class":103},"ws ",[50,1242,126],{"class":99},[50,1244,1245],{"class":103}," wb.active\n",[50,1247,1248,1250,1253,1255,1258,1260,1262,1264,1266],{"class":52,"line":184},[50,1249,801],{"class":99},[50,1251,1252],{"class":103}," d, v ",[50,1254,807],{"class":99},[50,1256,1257],{"class":213}," zip",[50,1259,856],{"class":103},[50,1261,147],{"class":60},[50,1263,326],{"class":103},[50,1265,654],{"class":60},[50,1267,1268],{"class":103},"]):\n",[50,1270,1271,1274,1277],{"class":52,"line":197},[50,1272,1273],{"class":103},"    ws.append([d.to_pydatetime(), ",[50,1275,1276],{"class":213},"int",[50,1278,1279],{"class":103},"(v)])\n",[50,1281,1282,1285,1288,1291,1294,1296,1299,1301,1303,1306,1308,1311,1313],{"class":52,"line":220},[50,1283,1284],{"class":103},"ws[",[50,1286,1287],{"class":60},"\"D1\"",[50,1289,1290],{"class":103},"], ws[",[50,1292,1293],{"class":60},"\"D2\"",[50,1295,1290],{"class":103},[50,1297,1298],{"class":60},"\"D3\"",[50,1300,167],{"class":103},[50,1302,126],{"class":99},[50,1304,1305],{"class":60}," \"=XNPV(0.08,B1:B5,A1:A5)\"",[50,1307,135],{"class":103},[50,1309,1310],{"class":60},"\"=XIRR(B1:B5,A1:A5)\"",[50,1312,135],{"class":103},[50,1314,1315],{"class":60},"\"=IRR(B1:B5)\"\n",[50,1317,1318,1321,1324],{"class":52,"line":249},[50,1319,1320],{"class":103},"wb.save(",[50,1322,1323],{"class":60},"\"check.xlsx\"",[50,1325,217],{"class":103},[50,1327,1328,1331,1334,1336,1339,1341,1344,1346,1349,1351,1354,1356,1359,1361,1363],{"class":52,"line":290},[50,1329,1330],{"class":103},"subprocess.run([",[50,1332,1333],{"class":60},"\"soffice\"",[50,1335,135],{"class":103},[50,1337,1338],{"class":60},"\"--headless\"",[50,1340,135],{"class":103},[50,1342,1343],{"class":60},"\"--convert-to\"",[50,1345,135],{"class":103},[50,1347,1348],{"class":60},"\"xlsx\"",[50,1350,135],{"class":103},[50,1352,1353],{"class":60},"\"--outdir\"",[50,1355,135],{"class":103},[50,1357,1358],{"class":60},"\"recalc\"",[50,1360,135],{"class":103},[50,1362,1323],{"class":60},[50,1364,1365],{"class":103},"],\n",[50,1367,1368,1371,1373,1375,1377,1380,1382,1384],{"class":52,"line":295},[50,1369,1370],{"class":138},"               check",[50,1372,126],{"class":99},[50,1374,214],{"class":213},[50,1376,135],{"class":103},[50,1378,1379],{"class":138},"capture_output",[50,1381,126],{"class":99},[50,1383,214],{"class":213},[50,1385,217],{"class":103},[50,1387,1388,1391,1393,1396,1399,1401,1404,1406,1408],{"class":52,"line":1144},[50,1389,1390],{"class":103},"sheet ",[50,1392,126],{"class":99},[50,1394,1395],{"class":103}," load_workbook(",[50,1397,1398],{"class":60},"\"recalc\u002Fcheck.xlsx\"",[50,1400,135],{"class":103},[50,1402,1403],{"class":138},"data_only",[50,1405,126],{"class":99},[50,1407,214],{"class":213},[50,1409,1410],{"class":103},").active\n",[50,1412,1413,1415,1417,1420,1423,1426,1429,1432,1434,1436,1439,1441,1443,1445,1447,1449,1451,1453],{"class":52,"line":1153},[50,1414,298],{"class":213},[50,1416,301],{"class":103},[50,1418,1419],{"class":60},"\"sheet  XNPV, XIRR, IRR:\"",[50,1421,1422],{"class":103},", [",[50,1424,1425],{"class":213},"round",[50,1427,1428],{"class":103},"(sheet[c].value, ",[50,1430,1431],{"class":213},"8",[50,1433,267],{"class":103},[50,1435,801],{"class":99},[50,1437,1438],{"class":103}," c ",[50,1440,807],{"class":99},[50,1442,272],{"class":103},[50,1444,1287],{"class":60},[50,1446,135],{"class":103},[50,1448,1293],{"class":60},[50,1450,135],{"class":103},[50,1452,1298],{"class":60},[50,1454,1455],{"class":103},")])\n",[50,1457,1458,1460,1462,1465,1467,1469,1472,1474,1476,1478,1481,1483,1486,1488,1490,1492,1494,1496],{"class":52,"line":1169},[50,1459,298],{"class":213},[50,1461,301],{"class":103},[50,1463,1464],{"class":60},"\"python XNPV, XIRR, IRR:\"",[50,1466,1422],{"class":103},[50,1468,1425],{"class":213},[50,1470,1471],{"class":103},"(x, ",[50,1473,1431],{"class":213},[50,1475,267],{"class":103},[50,1477,801],{"class":99},[50,1479,1480],{"class":103}," x ",[50,1482,807],{"class":99},[50,1484,1485],{"class":103}," (xnpv(",[50,1487,317],{"class":213},[50,1489,320],{"class":103},[50,1491,654],{"class":60},[50,1493,326],{"class":103},[50,1495,147],{"class":60},[50,1497,1498],{"class":103},"]), rate,\n",[50,1500,1502,1505,1507,1509,1511],{"class":52,"line":1501},15,[50,1503,1504],{"class":103},"                                                        irr_equal_periods(",[50,1506,853],{"class":213},[50,1508,856],{"class":103},[50,1510,654],{"class":60},[50,1512,1513],{"class":103},"])))])\n",[10,1515,1516,1517,1520],{},"All three agree to eight decimal places. The same check against Excel itself is a matter of opening ",[14,1518,1519],{},"check.xlsx"," there; the formulas are standard.",[353,1522,362,1527,362,1530,362,1533,362,1536,362,1540,362,1547,362,1553,362,1556,362,1561,362,1567,362,1572,362,1576,362,1583,362,1588,362,1592,362,1595,362,1599,362,1602,362,1605,362,1611,362,1616,362,1620,362,1624,362,1627,362,1630,362,1635,362,1640],{"viewBox":1523,"role":356,"ariaLabelledBy":1524,"xmlns":360,"style":361},"4 0 752 142",[1525,1526],"xi-verify-t","xi-verify-d",[364,1528,1529],{"id":1525},"Proving the Python numbers match the spreadsheet",[368,1531,1532],{"id":1526},"The cash flows are written to a workbook with XNPV, XIRR and IRR formulas, LibreOffice recalculates and stores the results, and the stored values are compared with the Python functions.",[372,1534],{"x":374,"y":237,"width":375,"height":1535,"fill":377},"142",[372,1537],{"x":387,"y":388,"width":1538,"height":1539,"rx":391,"fill":927,"stroke":928,"style":394},"153.0","110.5",[396,1541,1543],{"transform":1542},"translate(84.5,29.0) scale(1.000)",[400,1544],{"d":1545,"fill":403,"stroke":1546,"style":405},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--text,#172033)",[407,1548,1552],{"x":1549,"y":1550,"style":1551},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Write flows +",[407,1554,1555],{"x":1549,"y":415,"style":1551},"formulas",[407,1557,1560],{"x":1549,"y":1558,"style":1559},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","=XNPV(…), =XIRR(…)",[52,1562],{"x1":1563,"y1":1564,"x2":1565,"y2":1564,"stroke":1566,"style":394},"178.0","71.2","200.0","var(--brand,#5b5cf0)",[1568,1569],"polygon",{"points":1570,"fill":1571},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[372,1573],{"x":1574,"y":388,"width":1538,"height":1539,"rx":391,"fill":1575,"stroke":1566,"style":394},"209.0","#f0f4ff",[396,1577,1579],{"transform":1578},"translate(273.5,29.0) scale(1.000)",[400,1580],{"d":1581,"fill":403,"stroke":1582,"style":405},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--brand-strong,#4338ca)",[407,1584,1587],{"x":1585,"y":1550,"style":1586},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Recalculate",[407,1589,1591],{"x":1585,"y":1590,"style":1559},"95.0","soffice --convert-to",[407,1593,1594],{"x":1585,"y":1558,"style":1559},"xlsx",[52,1596],{"x1":1597,"y1":1564,"x2":1598,"y2":1564,"stroke":1566,"style":394},"367.0","389.0",[1568,1600],{"points":1601,"fill":1571},"393.0,71.2 384.0,76.2 384.0,66.2",[372,1603],{"x":1604,"y":388,"width":1538,"height":1539,"rx":391,"fill":1575,"stroke":1566,"style":394},"398.0",[396,1606,1608],{"transform":1607},"translate(462.5,37.8) scale(1.000)",[400,1609],{"d":1610,"fill":403,"stroke":1582,"style":405},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14",[407,1612,1615],{"x":1613,"y":1614,"style":1586},"474.5","84.2","Read results",[407,1617,1619],{"x":1613,"y":1618,"style":1559},"103.8","data_only=True",[52,1621],{"x1":1622,"y1":1564,"x2":1623,"y2":1564,"stroke":1566,"style":394},"556.0","578.0",[1568,1625],{"points":1626,"fill":1571},"582.0,71.2 573.0,76.2 573.0,66.2",[372,1628],{"x":1629,"y":388,"width":1538,"height":1539,"rx":391,"fill":430,"stroke":431,"style":394},"587.0",[396,1631,1633],{"transform":1632},"translate(651.5,37.8) scale(1.000)",[400,1634],{"d":437,"fill":403,"stroke":438,"style":405},[407,1636,1639],{"x":1637,"y":1614,"style":1638},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Compare",[407,1641,1642],{"x":1637,"y":1618,"style":1559},"equal to 8 decimals",[36,1644,1646],{"id":1645},"step-6-xirr-for-an-investment-that-is-still-open","Step 6 — XIRR for an investment that is still open",[10,1648,1649],{},"Portfolio reports usually ask \"what has this returned so far?\" about investments that have not been sold. The standard answer is to treat the current valuation as if it were received on the reporting date: append it as a final positive flow, then compute XIRR as usual.",[41,1651,1653],{"className":90,"code":1652,"language":92,"meta":46,"style":46},"valuations = pd.DataFrame({\n    \"Investment\": [\"Fund B\", \"Fund C\"],\n    \"Date\": pd.to_datetime([\"2028-06-30\", \"2028-06-30\"]),\n    \"CashFlow\": [5000, 19000],          # value still held on the reporting date\n})\nas_of = pd.concat([flows, valuations], ignore_index=True).sort_values([\"Investment\", \"Date\"])\nprint(as_of.groupby(\"Investment\")[[\"CashFlow\", \"Date\"]]\n      .apply(lambda g: round(safe_xirr(g), 4)).rename(\"XIRR to 30 Jun 2028\"))\n",[14,1654,1655,1665,1683,1700,1722,1727,1755,1776],{"__ignoreMap":46},[50,1656,1657,1660,1662],{"class":52,"line":53},[50,1658,1659],{"class":103},"valuations ",[50,1661,126],{"class":99},[50,1663,1664],{"class":103}," pd.DataFrame({\n",[50,1666,1667,1670,1673,1676,1678,1681],{"class":52,"line":113},[50,1668,1669],{"class":60},"    \"Investment\"",[50,1671,1672],{"class":103},": [",[50,1674,1675],{"class":60},"\"Fund B\"",[50,1677,135],{"class":103},[50,1679,1680],{"class":60},"\"Fund C\"",[50,1682,1365],{"class":103},[50,1684,1685,1688,1691,1694,1696,1698],{"class":52,"line":120},[50,1686,1687],{"class":60},"    \"Date\"",[50,1689,1690],{"class":103},": pd.to_datetime([",[50,1692,1693],{"class":60},"\"2028-06-30\"",[50,1695,135],{"class":103},[50,1697,1693],{"class":60},[50,1699,1141],{"class":103},[50,1701,1702,1705,1707,1710,1712,1715,1718],{"class":52,"line":153},[50,1703,1704],{"class":60},"    \"CashFlow\"",[50,1706,1672],{"class":103},[50,1708,1709],{"class":213},"5000",[50,1711,135],{"class":103},[50,1713,1714],{"class":213},"19000",[50,1716,1717],{"class":103},"],          ",[50,1719,1721],{"class":1720},"s-wDw","# value still held on the reporting date\n",[50,1723,1724],{"class":52,"line":179},[50,1725,1726],{"class":103},"})\n",[50,1728,1729,1732,1734,1737,1740,1742,1744,1747,1749,1751,1753],{"class":52,"line":184},[50,1730,1731],{"class":103},"as_of ",[50,1733,126],{"class":99},[50,1735,1736],{"class":103}," pd.concat([flows, valuations], ",[50,1738,1739],{"class":138},"ignore_index",[50,1741,126],{"class":99},[50,1743,214],{"class":213},[50,1745,1746],{"class":103},").sort_values([",[50,1748,164],{"class":60},[50,1750,135],{"class":103},[50,1752,147],{"class":60},[50,1754,150],{"class":103},[50,1756,1757,1759,1762,1764,1767,1769,1771,1773],{"class":52,"line":197},[50,1758,298],{"class":213},[50,1760,1761],{"class":103},"(as_of.groupby(",[50,1763,164],{"class":60},[50,1765,1766],{"class":103},")[[",[50,1768,654],{"class":60},[50,1770,135],{"class":103},[50,1772,147],{"class":60},[50,1774,1775],{"class":103},"]]\n",[50,1777,1778,1781,1784,1787,1789,1792,1794,1797,1800],{"class":52,"line":220},[50,1779,1780],{"class":103},"      .apply(",[50,1782,1783],{"class":99},"lambda",[50,1785,1786],{"class":103}," g: ",[50,1788,1425],{"class":213},[50,1790,1791],{"class":103},"(safe_xirr(g), ",[50,1793,374],{"class":213},[50,1795,1796],{"class":103},")).rename(",[50,1798,1799],{"class":60},"\"XIRR to 30 Jun 2028\"",[50,1801,1179],{"class":103},[10,1803,1804],{},"Fund C, which looked like a heavy loss when only its cash distributions counted, still holds 19,000 of value; with that included it has slightly more than its 40,000 back, and its return to date is about 1.3% a year. Fund B rises from 10.8% to 14.1% once the 5,000 it still holds is counted. Fund A has no valuation row because it is fully realised, and its XIRR is unchanged. Sorting by date before computing keeps the first listed date as the earliest, which matters for XNPV (Step 1) even though XIRR itself does not depend on the base date. Always label such figures with their reporting date — an \"as of\" XIRR changes every time the valuation does.",[36,1806,1808],{"id":1807},"common-pitfalls","Common pitfalls",[1810,1811,1812,1820,1830,1836,1842],"ul",{},[1813,1814,1815,1819],"li",{},[1816,1817,1818],"strong",{},"Using 365.25 or calendar years."," Excel divides days by 365, always.",[1813,1821,1822,1825,1826,1829],{},[1816,1823,1824],{},"Sorting changes XNPV."," The base date is the first listed date; sort by date first, or the result is expressed as of a different day (XIRR is unaffected). Excel's documentation says dates before the first one return ",[14,1827,1828],{},"#VALUE!","; LibreOffice discounts them anyway.",[1813,1831,1832,1835],{},[1816,1833,1834],{},"No sign change."," All-negative or all-positive flows have no IRR; report it rather than returning a number.",[1813,1837,1838,1841],{},[1816,1839,1840],{},"Several sign changes."," Flows that switch between paying in and paying out more than once can have more than one XIRR; check the XNPV curve or use the modified IRR with explicit reinvestment rates.",[1813,1843,1844,1847,1848,1851,1852,34],{},[1816,1845,1846],{},"Rates as percentages."," ",[14,1849,1850],{},"xnpv(8, …)"," means 800%; pass ",[14,1853,317],{},[36,1855,1857],{"id":1856},"conclusion","Conclusion",[10,1859,1860,1861,1863,1864,1866,1867,1869,1870,1873,1874,1876],{},"XNPV discounts each cash flow by ",[14,1862,86],{}," to the power of days since the first listed date over 365; XIRR is the rate that makes that sum zero, found with a root-finder such as ",[14,1865,519],{},". Both reproduce Excel to many decimal places, unlike ",[14,1868,20],{},", which assumes equal periods and badly misstates returns on irregular schedules. Compute XIRR per investment with ",[14,1871,1872],{},"groupby",", return ",[14,1875,1189],{}," where no rate exists, and prove the numbers by recalculating Excel's own formulas in LibreOffice.",[36,1878,1880],{"id":1879},"frequently-asked-questions","Frequently asked questions",[10,1882,1883,1886],{},[1816,1884,1885],{},"How do I calculate XIRR in Python?","\nDefine XNPV as the sum of each cash flow divided by (1 + rate) to the power of days since the first date divided by 365, then find the rate where it equals zero with scipy.optimize.brentq over a bracket such as -0.9999 to 10.",[10,1888,1889,1892],{},[1816,1890,1891],{},"Does Python's XIRR match Excel exactly?","\nYes, when it uses the same convention: an actual\u002F365 day count measured from the first listed date. The functions on this page agree with spreadsheet XIRR and XNPV results to at least eight decimal places.",[10,1894,1895,1898],{},[1816,1896,1897],{},"Why is my IRR so different from XIRR?","\nIRR assumes the cash flows are exactly one period apart. When the real dates are irregular, that changes how long the money is invested, so IRR and XIRR can differ a lot. Use XIRR for dated flows.",[10,1900,1901,1904],{},[1816,1902,1903],{},"Why does XIRR fail for some investments?","\nIf all cash flows have the same sign there is no rate that makes the net present value zero, so no XIRR exists. Check for at least one negative and one positive flow and report missing results explicitly.",[36,1906,1908],{"id":1907},"related","Related",[1810,1910,1911,1917,1924,1931],{},[1813,1912,1913,1914,1916],{},"Up one level: ",[30,1915,33],{"href":32}," — Excel functions translated to Python.",[1813,1918,1919,1923],{},[30,1920,1922],{"href":1921},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fpmt-npv-and-irr-in-python\u002F","PMT, NPV and IRR in Python"," — the equal-period functions.",[1813,1925,1926,1930],{},[30,1927,1929],{"href":1928},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Ffv-pv-rate-and-nper-equivalents-in-python\u002F","FV, PV, RATE and NPER Equivalents in Python"," — the rest of Excel's time-value functions.",[1813,1932,1933,1937],{},[30,1934,1936],{"href":1935},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-cash-flow-forecast-report\u002F","Build a Cash Flow Forecast Report"," — where dated cash flows come from.",[1939,1940,1941],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":46,"searchDepth":113,"depth":113,"links":1943},[1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954],{"id":38,"depth":113,"text":39},{"id":79,"depth":113,"text":80},{"id":512,"depth":113,"text":513},{"id":727,"depth":113,"text":728},{"id":981,"depth":113,"text":982},{"id":1193,"depth":113,"text":1194},{"id":1645,"depth":113,"text":1646},{"id":1807,"depth":113,"text":1808},{"id":1856,"depth":113,"text":1857},{"id":1879,"depth":113,"text":1880},{"id":1907,"depth":113,"text":1908},"2026-10-02","Reproduce Excel's XNPV and XIRR for cash flows on irregular dates: the 365-day formula behind XNPV, a root-finder for XIRR that matches Excel to the last digit, why NPV and IRR give a different (wrong) answer for uneven dates, XIRR per investment with groupby, cases with no or several solutions, and a LibreOffice cross-check.","md",[1959,1961,1963,1965],{"q":1885,"a":1960},"Define XNPV as the sum of each cash flow divided by (1 + rate) to the power of days since the first date divided by 365, then find the rate where it equals zero with scipy.optimize.brentq over a bracket such as -0.9999 to 10.",{"q":1891,"a":1962},"Yes, when it uses the same convention: an actual\u002F365 day count measured from the first listed date. The functions on this page agree with spreadsheet XIRR and XNPV results to at least eight decimal places.",{"q":1897,"a":1964},"IRR assumes the cash flows are exactly one period apart. When the real dates are irregular, that changes how long the money is invested, so IRR and XIRR can differ a lot. Use XIRR for dated flows.",{"q":1903,"a":1966},"If all cash flows have the same sign there is no rate that makes the net present value zero, so no XIRR exists. Check for at least one negative and one positive flow and report missing results explicitly.","xirr and xnpv equivalents in python",null,{"breadcrumb":1970},[1971,1973,1976],{"name":1972,"item":243},"Home",{"name":1974,"item":1975},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":33,"item":32},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python",{"title":1979,"description":1980},"XIRR and XNPV in Python (Excel-Matching Results)","Calculate Excel XNPV and XIRR in Python with pandas and scipy: the actual\u002F365 discount formula, brentq for XIRR, matching Excel exactly, why IRR is wrong for irregular dates, XIRR per group, no-sign-change and multiple-root cases, and verification.","xirr-and-xnpv-equivalents-in-python","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python\u002Findex",[1984],"formula-equivalents","how-to","7XoaNmlWZ6ZphjkMuMqQ0FUrDR4GoG7Vl_6pZUnwKhk",[1988,1992],{"title":1989,"path":1990,"stem":1991,"children":-1},"VSTACK, HSTACK, TAKE and CHOOSECOLS Equivalents in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fvstack-hstack-take-and-choosecols-equivalents-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fvstack-hstack-take-and-choosecols-equivalents-in-pandas\u002Findex",{"title":1993,"path":1994,"stem":1995,"children":-1},"XLOOKUP Equivalent in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas\u002Findex",[1997,1999],{"path":1998,"title":33},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas",{"path":2000,"title":1974},"\u002Fadvanced-data-transformation-and-cleaning",[],[2003,2006,2009,2012,2015,2018],{"path":2004,"title":2005,"kind":1985},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files","The VLOOKUP Equivalent in pandas for Excel Files",{"path":2007,"title":2008,"kind":1985},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas","Extract Numbers and Codes from Text with pandas",{"path":2010,"title":2011,"kind":1985},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fsplit-full-names-into-first-and-last-names-with-pandas","Split Full Names into First and Last Names with pandas",{"path":2013,"title":2014,"kind":1985},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut","Bin Values into Ranges Like Excel FREQUENCY with pd.cut",{"path":2016,"title":2017,"kind":1985},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas","Calculate Percent of Total per Group in pandas",{"path":2019,"title":2020,"kind":1985},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas","Count Distinct Values per Group in pandas",1790972090722]